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.
@thorstenball Unicorn Unix Magic Tricks fork(2) • system call (man 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, …)
@thorstenball Unicorn Unix Magic Tricks How does Unicorn use fork(2)? # 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
@thorstenball Unicorn Unix Magic Tricks pipe(2) • system call (man 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
@thorstenball Unicorn Unix Magic Tricks Ruby and pipe(2) # pipe.rb 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!'
@thorstenball Unicorn Unix Magic Tricks How does Unicorn use pipe(2)? • Pipe between each worker process and master process • Pipe only used inside master process • Pipe used to coordinate daemonization of process
@thorstenball Unicorn Unix Magic Tricks select(2) • system call (man 2 select) • monitor file descriptors until they are ready to read/ write • blocking
@thorstenball Unicorn Unix Magic Tricks Signal Handling • You can define signal actions • You can ignore signals • You can redefine signal actions • Some signals can’t be caught/ignored (KILL)
@thorstenball Unicorn Unix Magic Tricks Ruby and signal handling # 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
@thorstenball Unicorn Unix Magic Tricks $ ruby signals.rb My PID 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
@thorstenball Unicorn Unix Magic Tricks Unicorn and signals • QUIT - 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
@thorstenball Unicorn Unix Magic Tricks Unicorn and signals • Master 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
@thorstenball Unicorn Unix Magic Tricks Scaling workers with signals • 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
@thorstenball Unicorn Unix Magic Tricks Hot Reload • Master and 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
@thorstenball Unicorn Unix Magic Tricks Hot Reload - #reexec def 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
@thorstenball Unicorn Unix Magic Tricks Hot Reload - #reexec • 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
@thorstenball Unicorn Unix Magic Tricks Hot Reload - #reexec • 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
@thorstenball Unicorn Unix Magic Tricks That’s why! • Debugging • Design and Architecture • One more level of abstraction • Concurrency • It’s not magic!