Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is advantage of Tini? #8

Closed
anentropic opened this issue Oct 7, 2015 · 41 comments
Closed

What is advantage of Tini? #8

anentropic opened this issue Oct 7, 2015 · 41 comments
Assignees

Comments

@anentropic
Copy link

Hi,

| noticed the official Jenkins image was using Tini, so I was curious what it is.

It looks like it must be useful and probably solves some issues I don't know about. Could you explain very briefly in a 'Linux for dummies' kind of way what is the advantage of Tini vs just running a shell script directly as the CMD?

I have a few containers with a docker-entrypoint.sh type of script that basically do an exec "$@" at the end - should I be using Tini instead?

@krallin
Copy link
Owner

krallin commented Oct 7, 2015

Good question! This is going to be a bit long, so bear with me (I know you asked for brief, sorry about that :x).

First, let's talk a little bit about Docker. When you run a Docker container, Docker proceeds to isolate it from the rest of the system. That isolation happens at different levels (e.g. network, filesystem, processes).

Tini isn't really concerned with the network or the filesystem, so let's focus on what matters in the context of Tini: processes.

Each Docker container is a PID namespace, which means that the processes in your container are isolated from other processes on your host. A PID namespace is a tree, which starts at PID 1, which is commonly called init.

Note: when you run a Docker container, PID 1 is whatever you set as your ENTRYPOINT (or if you don't have one, then it's either your shell or another program, depending on the format of your CMD).

Now, unlike other processes, PID 1 has a unique responsibility, which is to reap zombie processes.

Zombie processes are processes that:

  • Have exited.
  • Were not waited on by their parent process (wait is the syscall parent processes use to retrieve the exit code of their children).
  • Have lost their parent (i.e. their parent exited as well), which means they'll never be waited on by their parent.

When a zombie is created (i.e. which happens when its parent exits, and therefore all chances of it ever being waited by it are gone), it is reparent to init, which is expected to reap it (which means calling wait on it).

In other words, someone has to clean up after "irresponsible" parents that leave their children un-wait'ed, and that's PID 1's job.

That's what Tini does, and is something the JVM (which is what runs when you do exec java ...) does not do, which his why you don't want to run Jenkins as PID 1.

Note that creating zombies is usually frowned upon in the first place (i.e. ideally you should be fixing your code so it doesn't create zombies), but for something like Jenkins, they're unavoidable: since Jenkins usually runs code that isn't written by the Jenkins maintainers (i.e. your build scripts), they can't "fix the code".

This is why Jenkins uses Tini: to clean up after build scripts that create zombies.


Now, Bash actually does the same thing (reaping zombies), so you're probably wondering: why not use Bash as PID 1?

One problem is, if you run Bash as PID 1, then all signals you send to your Docker container (e.g. using docker stop or docker kill) end up sent to Bash, which does not forward them anywhere (unless you code it yourself). In other words, if you use Bash to run Jenkins, and then run docker stop, then Jenkins will never see the stop command!

Tini fixes by "forwarding signals": if you send a signal to Tini, then it sends that same signal to your child process (Jenkins in your case).

A second problem is that once your process has exited, Bash will proceed to exit as well. If you're not being careful, Bash might exit with exit code 0, whereas your process actually crashed (0 means "all fine"; this would cause Docker restart policies to not do what you expect). What you actually want is for Bash to return the same exit code your process had.

Note that you can address this by creating signal handlers in Bash to actually do the forwarding, and returning a proper exit code. On the other hand that's more work, whereas adding Tini is a few lines in your Dockerfile.


Now, there would be another solution, which would be to add e.g. another thread in Jenkins to reap zombies, and run Jenkins as PID 1.

This isn't ideal either, for two reasons:

First, if Jenkins runs as PID 1, then it's difficult to differentiate between process that were re-parented to Jenkins (which should be reaped), and processes that were spawned by Jenkins (which shouldn't, because there's other code that's already expecting to wait them). I'm sure you could solve that in code, but again: why write it when you can just drop Tini in?

Second, if Jenkins runs as PID 1, then it may not receive the signals you send it!

That's a subtlety in PID 1. Unlike other unlike processes, PID 1 does not have default signal handlers, which means that if Jenkins hasn't explicitly installed a signal handler for SIGTERM, then that signal is going to be discarded when it's sent (whereas the default behavior would have been to terminate the process).

Tini does install explicit signal handlers (to forward them, incidentally), so those signals no longer get dropped. Instead, they're sent to Jenkins, which is not running as PID 1 (Tini is), and therefore has default signal handlers (note: this is not the reason why Jenkins uses Tini, they use it for signal reaping, but it was used in the RabbitMQ image for that reason).


Note that there are also a few extras in Tini, which would be harder to reproduce in Bash or Java (e.g. Tini can register as a subreaper so it doesn't actually need to run as PID 1 to do its zombie-reaping job), but those are mostly useful for specialist use cases.

Hope this helps!

Here are some references you might be interested in to learn more about that topic:

Finally, do note that there are alternatives to Tini (like Phusion's base image).

Tini differentiates with:

  • Doing everything PID 1 needs to do and nothing else. Things like reading environment files, changing users, process supervision are out of scope for Tini (there are other, better tools for those)
  • It requires zero configuration to do its job properly (Tini >= 0.6 will also warn you if you're not running it properly).
  • It's got a lot of tests.

Cheers,

@krallin
Copy link
Owner

krallin commented Oct 7, 2015

As to whether you should be using Tini.

Obviously, it's not always needed (e.g. I run http://apt-browse.org/ in a dozen Docker containers, and only one of them uses Tini), but here are a few heuristics:

  • Do you have zombie ("defunct") processes in your container? If so, either fix whatever is generating them or — if you can't (like Jenkins) — use Tini.
  • Is whatever process you exec in your entrypoint registering signal handlers? A good way to figure this out might be to check whether your process responds properly to e.g. docker stop (or if it waits for 10 seconds before exiting)

Now, Tini is transparent, so if you're unsure, adding it shouldn't hurt.

Cheers,

@krallin krallin self-assigned this Oct 7, 2015
@anentropic
Copy link
Author

thanks for the detailed info!

one thing I need to clarify... I understood that when I exec "$@" it 'replaces' the current bash script with the executed file... in that situation do I have to worry about bash not forwarding signals? or bash is out of the picture then, and I just have to ensure that the exec'ed script responds to signals?

@krallin
Copy link
Owner

krallin commented Oct 7, 2015

When you run exec, then bash is out of the picture, so forwarding signals isn't needed, however, you need to to ensure that what you exec'ed (which is presumably now running as PID 1):

  • ... sets up signal handlers (because the default ones aren't there).
  • ... is capable of reaping zombies if you're creating any.

Cheers,

@krallin
Copy link
Owner

krallin commented Oct 7, 2015

(Just closing this since it's not really an issue, but just let me know if you have follow up questions)

@krallin krallin closed this as completed Oct 7, 2015
@anentropic
Copy link
Author

no prob, thanks!

maybe put the two links on the readme?

https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/
https://github.com/docker-library/official-images#init

@cmeury
Copy link
Contributor

cmeury commented Aug 25, 2016

The long explanation/answer might be a good extension to the short chapter in the main README.

@waleedka
Copy link

You need to put this explanation in the README file. People shouldn't have to search the issues to figure out what the tool does.

krallin added a commit that referenced this issue Jan 2, 2017
Adds a summary of #8 and a link to that discussion.
@krallin
Copy link
Owner

krallin commented Jan 2, 2017

Thanks @cmeury @waleedka! I'm adding a short summary in the README and a link to this issue in there: #70

krallin added a commit that referenced this issue Jan 2, 2017
Adds a summary of #8 and a link to that discussion.
krallin added a commit that referenced this issue Jan 2, 2017
Adds a summary of #8 and a link to that discussion.
@oroc95
Copy link

oroc95 commented Jan 23, 2017

Hi

Can i start supervisord with tini ? Is there a sense to do it ?

What thé difference between s6 ?
http://skarnet.org/software/s6/

@ghost
Copy link

ghost commented Nov 3, 2017

Hi @krallin, i'm currently trying to reproduce what you said about bash.

You wrote:

One problem is, if you run Bash as PID 1, then all signals you send to your Docker container (e.g. using docker stop or docker kill) end up sent to Bash, which does not forward them anywhere (unless you code it yourself).

To verify that i wrote a small c program:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

static void sighandler(int signo){
  printf("C Signal handler received signal %d!\n", signo);
  printf("Terminating now =)\n");
  exit(EXIT_SUCCESS);
}

int main(int argc, char** argv){
  signal(SIGTERM, sighandler);
  signal(SIGINT, sighandler);
  printf("Hello World!\n");
  while(1){ ;; }
  return 0;
}

I tried the following startup commands:

docker run -ti -v $(pwd):/mnt/host --entrypoint /mnt/host/a.out ubuntu
docker run -ti --init -v $(pwd):/mnt/host ubuntu bash -c /mnt/host/a.out
docker run -ti --init -v $(pwd):/mnt/host ubuntu /mnt/host/a.out
docker run -ti --init -v $(pwd):/mnt/host ubuntu bash -c /mnt/host/a.out
docker run -ti -v $(pwd):/mnt/host ubuntu bash -c /mnt/host/a.out
docker run -d -v $(pwd):/mnt/host ubuntu bash -c /mnt/host/a.out

For each run, with --init and without --init the signal handler is being executed correctly, when using docker stop container. Does the current bash version forward all signals by default when running a command with -c or am i doing something wrong here?

Thanks =)

Additional Info:

Running on gentoo stable, docker version 17.03.2-ce

@krallin
Copy link
Owner

krallin commented Nov 3, 2017

@fbe,

When you run bash -c with a single command, bash actually exec's the command, so your command ends up running as PID 1 (not bash).

Compare:

thomas@mocha4 ~ % docker run --rm -it ubuntu bash -c 'ps auxww'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  25976  1480 pts/0    Rs+  13:37   0:00 ps auxww
thomas@mocha4 ~ % docker run --rm -it ubuntu bash -c 'true && ps auxww'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.0  0.0  18028  2708 pts/0    Ss+  13:37   0:00 bash -c true && ps auxww
root         7  0.0  0.0  34424  2816 pts/0    R+   13:37   0:00 ps auxww

(this means the zombie reaping from bash is gone, obviously, since you are no longer using bash as PID 1).

@ghost
Copy link

ghost commented Nov 3, 2017

Lesson learned, thank you =)

@TerryE
Copy link

TerryE commented Mar 11, 2022

Hi @krallin. Very nice utility thanks.

There is another usecase that might be worth documenting in a docker context: Docker logging system is through STDOUT or STDERR of the PID 1 process. Hence any other process running in the container can log to /proc/1/fd/1 or /proc/1/fd/2 -- so long as these both link to the original Docker created pipes. However, if the PID process has reopened either as a file (for example if httpd has configured the access or error log to file) then this link is broken. Using a tini wrapper in this case retains the access to these FDs for negligible overhead memory or process time (since signals are rarely issued to the PID 1 process anyway).

@JiaoDingjun
Copy link

good

rblaine95 added a commit to rblaine95/muse that referenced this issue May 15, 2022
* Add tini to reap zombie processes
* krallin/tini#8
rblaine95 added a commit to rblaine95/muse that referenced this issue May 15, 2022
* Add tini to reap zombie processes
* krallin/tini#8
codetheweb pushed a commit to museofficial/muse that referenced this issue May 15, 2022
* feat: Add tini

* Add tini to reap zombie processes
* krallin/tini#8

* Update changelog
vlulla added a commit to vlulla/vim_templates that referenced this issue Mar 30, 2023
See this comment
krallin/tini#8 (comment) for why
it is a good idea to run tini.
@x-yuri
Copy link

x-yuri commented Jul 13, 2024

thomas@mocha4 ~ % docker run --rm -it ubuntu bash -c 'true && ps auxww'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  2.0  0.0  18028  2708 pts/0    Ss+  13:37   0:00 bash -c true && ps auxww
root         7  0.0  0.0  34424  2816 pts/0    R+   13:37   0:00 ps auxww

#8 (comment)

Adding another command to the list (bash -c 'ps auxww' -> bash -c 'true && ps auxww') doesn't suppress fork since 4.4, but this does: bash -c 'true; ps auxww'. The supposedly relevant lines from the changelog:

execute_cmd.c

  • execute_command_internal: AND_AND, OR_OR: call should_suppress_fork
    for the RHS of && and ||, make `make' invocations marginally more
    efficient

And that (bash -c 'true; ps auxww') no longer suppresses fork since 5.1. The supposedly relevant lines from the changelog:

b. Bash attempts to optimize the number of times it forks when executing
commands in subshells and from `bash -c'.

--

PID 1 does not have default signal handlers

#8 (comment)

I can confirm that.

@x-yuri
Copy link

x-yuri commented Oct 14, 2024

I guess I've figured it out how to determine if a program needs --init. To quote the gist:

With ruby and go programs you probably won't have problems. If a program doesn't set a SIGTERM handler, then the cleanup is apparently not needed, the language runtime will set a handler and it'll terminate on SIGTERM. Otherwise the program's SIGTERM handler will terminate it.

In languages where it's not the case to determine if you need --init run the program in a container, make sure it's running under PID 1 and send SIGTERM to it. If it terminates, then --init apparently is not needed.

I'm leaving aside the issue with zombie processes here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests