A talk about the Unicorn webserver, how it works, why it looks like magic and why, in the end, it's not magic but Unix that's behind the great features of Unicorn.
2 fork) • splits processes in two (“a fork in the road”) • fork(2) splits a process into parent and child • children inherit a lot from their parent processes (data, stack, heap, working directory, …)
2 pipe) • We can use them outside the shell! • Pipes are two file descriptors • One read end, one write end • Great for communication between processes • Pipes are inherited when forking
read_end, write_end = IO.pipe fork do read_end.close write_end.write('Hello from your child!') write_end.close end write_end.close Process.wait message = read_end.read read_end.close puts "Received from child: '#{message}'"
read_end, write_end = IO.pipe fork do read_end.close write_end.write('Hello from your child!') write_end.close end write_end.close Process.wait message = read_end.read read_end.close puts "Received from child: ‘#{message}'" # => Received from child: 'Hello from your child!'
does socket, bind, listen • Workers inherit listening socket and pipe • Workers call select and accept on socket and pipe • Workers pass connections to Rack/Rails app • Unicorn even allows multiple listening sockets!
signals.rb trap(:SIGKILL) do puts "You won't see this" end trap(:SIGQUIT) do puts "SIGQUIT received" end trap(:SIGUSR1) do puts "SIGUSR1 received" end puts "My PID is #{Process.pid}. Send me some signals!" sleep 100
is 31950. Send me some signals! SIGUSR1 received SIGQUIT received zsh: killed ruby signals.rb $ kill -USR1 31950 $ kill -QUIT 31950 $ kill -KILL 31950 ! Server Client Ruby and signal handling
- graceful shutdown, let workers finish the work • TERM and INT - immediate shutdown • USR1 - reopen log files • USR2 - hot reload! • TTIN/TTOU - increase/decrease the number of workers • HUP - reload the configuration file • WINCH - keep master running, gracefully stop workers
process sets up a self-pipe and a queue • Signal handlers write signal name into the queue, write to self- pipe • Workers inherit signal handlers, ignore most of them • Master process calls IO.select on self-pipe to check for signals in main loop • Signals are sent from master to worker via pipes
defines lambda that loads application • If preload_app is true, master calls the lambda • Application is loaded into memory • Master calls fork, spawns workers • Workers have application in memory
Master process traps TTIN and TTOU signals • Signal handlers write signal to queue, awake master • In the master main loop: master reads signal, changes worker_processes count • In new iteration of main loop the master sees that workers need to be increased/decreased • Master spawns missing workers or writes QUIT signal to worker pipe
spawn_missing_workers worker_nr = -1 until (worker_nr += 1) == @worker_processes WORKERS.value?(worker_nr) and next worker = Worker.new(worker_nr) before_fork.call(self, worker) if pid = fork WORKERS[pid] = worker worker.atfork_parent else after_fork_internal worker_loop(worker) exit end end rescue => e @logger.error(e) rescue nil exit! end
worker processes are happily doing their work • Master process receives USR2 signal • Signal handler queues up signal, writes to self-pipe • Master process reads signal and calls its #reexec method
reexec if reexec_pid > 0 begin Process.kill(0, reexec_pid) logger.error "reexec-ed child already running PID:#{reexec_pid}" return rescue Errno::ESRCH self.reexec_pid = 0 end end if pid old_pid = "#{pid}.oldbin" begin self.pid = old_pid # clear the path for a new pid file rescue ArgumentError logger.error "old PID:#{valid_pid?(old_pid)} running with " \ "existing pid=#{old_pid}, refusing rexec" return rescue => e logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}" return end end self.reexec_pid = fork do listener_fds = {} LISTENERS.each do |sock| # IO#close_on_exec= will be available on any future version of # Ruby that sets FD_CLOEXEC by default on new file descriptors # ref: http://redmine.ruby-lang.org/issues/5041 sock.close_on_exec = false if sock.respond_to?(:close_on_exec=) listener_fds[sock.fileno] = sock end ENV['UNICORN_FD'] = listener_fds.keys.join(',') Dir.chdir(START_CTX[:cwd]) cmd = [ START_CTX[0] ].concat(START_CTX[:argv]) # avoid leaking FDs we don't know about, but let before_exec # unset FD_CLOEXEC, if anything else in the app eventually # relies on FD inheritence. (3..1024).each do |io| next if listener_fds.include?(io) io = IO.for_fd(io) rescue next prevent_autoclose(io) io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end # exec(command, hash) works in at least 1.9.1+, but will only be # required in 1.9.4/2.0.0 at earliest. cmd << listener_fds if RUBY_VERSION >= "1.9.1" logger.info "cmd: #{cmd}" logger.info "executing #{cmd.inspect} (in #{Dir.pwd})" before_exec.call(self) exec(*cmd) end proc_name 'master (old)' end
Return if already re-executing • Write PID to pidfile.pid.old • fork! Parent saves PID of new child and returns • Child writes FD number of listening sockets to ENV variable • Child closes unneeded sockets and files • Child calls exec with the original arguments: turns into new Unicorn master process
New master process boots up with new application code • New master process checks ENV for socket FDs • Casts socket FDs into socket objects (IO.for_fd) • Spawns off workers, which start select/accept loop • No “address already in use”: sockets are inherited! • Two sets of master/worker processes running • Old process is now safe to be killed