October 7, 2017

Why You Need a Proper Init Process on Docker

You have to use proper init process when you create a docker image. Running a process on docker without it might lead unexpected results. Today, I'll explain about that.

The “init” Process and Orphans

On a Linux system, the process which has PID 1 is the root of the process tree. It is called “init”. The default signal handlers of the init process is different from other ordinary processes. And the init process has a special responsibility.

Generally, an ordinary process is reaped by its parent process at its termination. But occasionally, a parent process terminates earlier than its children. The children left by the parent are called “orphan”. And the init process, the ancestor of all processes is supposed to adopt those orphan processes and to reap zombie processes.

For example, you can confirm this mechanism by running below code.

#!/usr/bin/env ruby

Process.fork {
  File.open('output', 'w') do |f|
    f.puts "Parent: #{Process.ppid}" # will write parent's PID

    sleep 2 # wait for termination of the parent

    f.puts "Parent: #{Process.ppid}" # will write init's PID
  end
}

sleep 1
exit # exit w/o waiting (reaping) the child

After you run that code, the output of the child process can be confirmed by viewing the file “output”. The file will show you something like below.

Parent: 20935
Parent: 1

On the second output, the parent PID seems be changed. That means the child process had been adopted by the init process (PID: 1) since the original parent process had gone earlier.

Problems Using Docker w/o a Proper Init Process

The process table in a container is separated from the host side. And as I said above, the init process is supposed to treat its descendants. Therefore, if you don't use a “proper” init process, troubles such as unexpected process termination may happen.

For example, if the pid-1 process which has children is terminating and it doesn't wait its child processes, the children will be killed ungracefully. Children like those can't handle termination properly. It's equivalent to be killed by 9 (SIGKILL).

# The child will able to trap termination (SIGTERM)
docker run -it --rm ruby:alpine ruby -e "
  pid = Process.fork {
    trap(:TERM) { puts %(Terminating...); exit }
    sleep
  }
  Process.kill(:TERM, pid) # exit the child normally w/ SIGTERM
  Process.waitpid(pid)
"

# The child won't able to trap termination (equivalent to SIGKILL)
docker run -it --rm ruby:alpine ruby -e "
  pid = Process.fork {
    trap(:TERM) { puts %(Terminating...); exit }
    sleep
  }
  exit # just exit immediately
"

Even if those orphans are already dead, they will remain on the process table as zombie processes and won't be reaped. Thus they will keep consuming the kernel resources.

Using a Proper Init on Docker

If you use docker 1.13 or later, you can just pass --init option to docker run command. You can also use tini. And there are some more “proper” init programs such as s6-overlay.

References

© Hibariya 2020

Powered by Hugo & Kiss.