Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Processes, Threads and Ruby

morhekil
April 22, 2013

Processes, Threads and Ruby

Introduction into working with processes and threads in Ruby, together with a deeper look into some underlying differences between various Ruby versions and implementations

morhekil

April 22, 2013
Tweet

More Decks by morhekil

Other Decks in Programming

Transcript

  1. POSIX • Portable Operating System Interface • Fully POSIX-compliant: OS

    X, QNX, Solaris • Mostly POSIX-compliant: GNU/Linux, *BSD • Ruby implements lots of POSIX functionality, and often - with exactly the same API (Kernel#fork for fork(2), Process.wait for wait(2), etc) Monday, April 22, 13
  2. BASIC PROCESS ATTRIBUTES #  ps  -­‐eo  pid,ppid,ni,user,thcount,args  |  grep  -­‐E

     'PID|unicorn'    PID    PPID    NI  USER          THCNT  COMMAND 11883          1      0  www                    2  unicorn_rails  master  -­‐c  config/unicorn.rb 11977  11883      0  www                    3  unicorn_rails  worker[0]  -­‐c  config/unicorn.rb 11982  11883      0  www                    3  unicorn_rails  worker[1]  -­‐c  config/unicorn.rb 11987  11883      0  www                    3  unicorn_rails  worker[2]  -­‐c  config/unicorn.rb 11993  11883      0  www                    3  unicorn_rails  worker[3]  -­‐c  config/unicorn.rb • Process ID • Parent Process ID • Nice level • Owner • Thread count • Name Monday, April 22, 13
  3. FORKING • fork(2) - “create child process” system call •

    parent’s PID is child’s PPID • child receives a copy of parent’s memory • child receives parent’s open file descriptors (files, sockets, etc) • child’s memory is independent of parent’s memory Monday, April 22, 13
  4. FORKING PID = 100 FILE HDLS MEMORY PID = 114

    FILE HDLS MEMORY PPID = 100 Monday, April 22, 13
  5. FORKING IN RUBY: IF/ELSE if  fork    puts  "executing  if

     block" else    puts  "executing  else  block" end Monday, April 22, 13
  6. FORKING IN RUBY: IF/ELSE if  fork    puts  "executing  if

     block" else    puts  "executing  else  block" end %  ruby  tmp.rb executing  if  block executing  else  block Monday, April 22, 13
  7. FORKING IN RUBY: IF/ELSE if  fork    puts  "executing  if

     block" else    puts  "executing  else  block" end %  ruby  tmp.rb executing  if  block executing  else  block Confusing? Let’s rewrite it a bit Monday, April 22, 13
  8. FORKING IN RUBY: IF/ELSE puts  "I  am  #{Process.pid}"   if

     fork    puts  "executing  if  block  in  #{Process.pid}" else    puts  "executing  else  block  #{Process.pid}" end Monday, April 22, 13
  9. FORKING IN RUBY: IF/ELSE puts  "I  am  #{Process.pid}"   if

     fork    puts  "executing  if  block  in  #{Process.pid}" else    puts  "executing  else  block  #{Process.pid}" end %  ruby  tmp.rb I  am  18290 executing  if  block  in  18290 executing  else  block  in  18291 Monday, April 22, 13
  10. FORKING IN RUBY: IF/ELSE puts  "I  am  #{Process.pid}"   if

     fork    puts  "executing  if  block  in  #{Process.pid}" else    puts  "executing  else  block  #{Process.pid}" end %  ruby  tmp.rb I  am  18290 executing  if  block  in  18290 executing  else  block  in  18291 Kernel#fork returns: •in child process - nil •in parent process - pid of the child process Monday, April 22, 13
  11. FORKING IN RUBY: BLOCK fork  do    #  child  process

     code    puts  "I  am  a  child" end   #  parent  process  code puts  "I  am  the  parent" •child process exits at the end of the block •parent process skips the block Monday, April 22, 13
  12. MEMORY MANAGEMENT • when child exits, its memory is destroyed

    • use case: fork child processes to run memory-hungry code WHY? - Ruby is bad at releasing memory back to the system - so ruby processes grow, but don’t shrink Monday, April 22, 13
  13. COPY-ON-WRITE • when child is forked, its memory is not

    really copied until it’s been written to • so we don’t have to copy the whole memory at once Monday, April 22, 13
  14. COPY-ON-WRITE • when child is forked, its memory is not

    really copied until it’s been written to • so we don’t have to copy the whole memory at once • but only if we’re using Ruby 2.0! Monday, April 22, 13
  15. RUBY GC: 2.0 VS 1.9 GC path for bitmap marking

    landed in 2.0:  gc_mark(rb_objspace_t  *objspace,  VALUE  ptr,  int  lev)  {          register  RVALUE  *obj; +        register  uintptr_t  *bits;          obj  =  RANY(ptr);          if  (rb_special_const_p(ptr))  return;  /*  special  const  not  marked  */          if  (obj-­‐>as.basic.flags  ==  0)  return;              /*  free  cell  */ -­‐        if  (obj-­‐>as.basic.flags  &  FL_MARK)  return;    /*  already  marked  */ -­‐        obj-­‐>as.basic.flags  |=  FL_MARK; +        bits  =  GET_HEAP_BITMAP(ptr); +        if  (MARKED_IN_BITMAP(bits,  ptr))  return;    /*  already  marked  */ +        MARK_IN_BITMAP(bits,  ptr);          objspace-­‐>heap.live_num++; +        register  uintptr_t  *bits; +        bits  =  GET_HEAP_BITMAP(ptr); +        if  (MARKED_IN_BITMAP(bits,  ptr))  return;    /*  already  marked  */ +        MARK_IN_BITMAP(bits,  ptr); -­‐        if  (obj-­‐>as.basic.flags  &  FL_MARK)  return;    /*  already  marked  */ -­‐        obj-­‐>as.basic.flags  |=  FL_MARK; In 1.9: •GC marks stored in the objects •memory writes in every object on every GC run •memory gets copied by OS In 2.0: •GC marks stored in external bitmap •no memory writes in the objects •no copies of unchanged memory https://github.com/ruby/ruby/commit/50675fdba1125a841ed494cb98737c97bd748900#L3L1641 Monday, April 22, 13
  16. PROCESS EXIT CODE • returned when process finished executing •

    0 usually indicates success, 1 and other - error • but it is merely a matter of interpretation • “errors” can be interpreted as program-specific responses Monday, April 22, 13
  17. PROCESS EXIT CODES: RUBY code  =  ARGV.first.to_i exit  code %

     ruby  tmp.rb  0 %  echo  $? 0 %  ruby  tmp.rb  5 %  echo  $? 5 Kernel#exit(status  =  true) Kernel#exit!(status  =  false) Monday, April 22, 13
  18. PROCESS EXIT CODES: RUBY code  =  ARGV.first.to_i exit  code %

     ruby  tmp.rb  0 %  echo  $? 0 %  ruby  tmp.rb  5 %  echo  $? 5 %  ruby  tmp.rb  5  &&  echo  "Yep" %  ruby  tmp.rb  1  ||  echo  "Nope" Nope %  ruby  tmp.rb  0  &&  echo  "Yep" Yep basic shell logic: 0 = success 1 = failure } convention Kernel#exit(status  =  true) Kernel#exit!(status  =  false) Monday, April 22, 13
  19. PROCESS NAME • can be changed in runtime • can

    be controlled by the process itself • is often overlooked as a simple, but powerful communication media Monday, April 22, 13
  20. PROCESS NAME: RUBY 1.upto(10)  do  |n|    $0  =  "zomg

     process:  #{n*10}%"    sleep  2 end %  watch  "ps  ax  |  grep  zomg" Every  2.0s:  ps  ax  |  grep  zomg 43121  s003    S+          0:00.02  zomg  process:  40% Monday, April 22, 13
  21. PROCESS NAME: RUBY 1.upto(10)  do  |n|    $0  =  "zomg

     process:  #{n*10}%"    sleep  2 end %  watch  "ps  ax  |  grep  zomg" Every  2.0s:  ps  ax  |  grep  zomg 43121  s003    S+          0:00.02  zomg  process:  40% Process name can communicate any status: •task progress •request being executed •job being run •number of workers •etc Monday, April 22, 13
  22. CASE STUDY 1.1: BRIDGE class  ApplicationController  <  ActionController::Base    around_filter

     :manage_memory        def  manage_memory        old_0  =  $0                                                                                                                                                                                                                                                                                                                                                begin                                                                                                                                                          $0  =  "rails:#{request.method}  #{controller_name}##{action_name}#{'.xhr'  if  request.xhr?}"            yield        ensure            $0  =  old_0        end    end end web workers: •mostly informational in this case •but sometimes can be useful to cross-reference with other stuck processes (e.g. long db queries) Monday, April 22, 13
  23. CASE STUDY 1.2: BRIDGE #  lib/stable_master.rb class  StableMaster    def

     run_jobs        old_0  =  $0        while  @running            ActiveRecord::Base.verify_active_connections!            if(@running  &&  (job_id  =  (read_pipe.readline  rescue  nil)))                job  =  Job.find(job_id.chomp.to_i)                $0  =  "stablemaster:  #{job.class}##{job.id}"            end        end        $0  =  old_0    end end stable master / horses: •similar status messages for stable master and idle workers •can easily identify and kill troublesome jobs •look around in stable_master.rb for more Monday, April 22, 13
  24. CASE STUDY 1.2: BRIDGE #  lib/stable_master.rb class  StableMaster    def

     run_jobs        old_0  =  $0        while  @running            ActiveRecord::Base.verify_active_connections!            if(@running  &&  (job_id  =  (read_pipe.readline  rescue  nil)))                job  =  Job.find(job_id.chomp.to_i)                $0  =  "stablemaster:  #{job.class}##{job.id}"            end        end        $0  =  old_0    end end stable master / horses:            ActiveRecord::Base.verify_active_connections! ProTip: AR tends to lose db connections, so reconnect in child processes Monday, April 22, 13
  25. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names Monday, April 22, 13
  26. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end Monday, April 22, 13
  27. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end        @child  =  fork(job)  do forking a child to run the job Monday, April 22, 13
  28. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end            reconnect reconnecting to the server Monday, April 22, 13
  29. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end            perform(job,  &block) and executing the job Monday, April 22, 13
  30. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end            wait_for_child parent process waits for child to finish Monday, April 22, 13
  31. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names    def  process_job(job,  &block)        #  ...        @child  =  fork(job)  do            reconnect            run_hook  :after_fork,  job            unregister_signal_handlers            perform(job,  &block)            exit!  unless  options[:run_at_exit_hooks]        end        if  @child            wait_for_child            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled?        #  ...        end        done_working    end            job.fail(DirtyExit.new($?.to_s))  if  $?.signaled? and marks the job as failed if it’s been killed with a signal Monday, April 22, 13
  32. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names #  Processes  a  given  job  in  the  child. def  perform(job)    procline  "Processing  #{job.queue}  since  #{Time.now.to_i}  [#{job.payload_class_name}]"    begin        run_hook  :before_perform,  job        job.perform        run_hook  :after_perform,  job    rescue  Object  =>  e        job.fail(e)        failed!    else        Resque.logger.info  "done:  #{job.inspect}"    ensure        yield  job  if  block_given?    end end            perform(job,  &block) and executing the job Monday, April 22, 13
  33. CASE STUDY 2: RESQUE #  Processes  a  given  job  in

     the  child. def  perform(job)    procline  "Processing  #{job.queue}  since  #{Time.now.to_i}  [#{job.payload_class_name}]"    begin        run_hook  :before_perform,  job        job.perform        run_hook  :after_perform,  job    rescue  Object  =>  e        job.fail(e)        failed!    else        Resque.logger.info  "done:  #{job.inspect}"    ensure        yield  job  if  block_given?    end end            perform(job,  &block) displaying job details via process name    procline  "Processing  #{job.queue}  since  #{Time.now.to_i}  [#{job.payload_class_name}]" Redis based processing queue •forks a child per job to contain memory bloat •communicates runtime status via process names Monday, April 22, 13
  34. CASE STUDY 2: RESQUE Redis based processing queue •forks a

    child per job to contain memory bloat •communicates runtime status via process names more source code: https://github.com/resque/resque/blob/master/lib/resque/worker.rb Monday, April 22, 13
  35. COLLECTING CHILDREN Process.wait •waits for the next child to exit

    •sets $? with Process::Status Process::Status pid child’s process id exited? true if exited normally exitstatus byte-sized exit status signalled? true if interrupted by a signed (kill’ed) success? true if exited with an exit code of 0 Monday, April 22, 13
  36. COLLECTING CHILDREN Finer-grade control Process.wait(pid=-­‐1,  flags=0) Process.waitpid(pid=-­‐1,  flags=0) returns  pid

     of  exited  child Process.wait(pid=-­‐1,  flags=0) Process.waitpid(pid=-­‐1,  flags=0) returns  pid  and  Process::Status  of  exited  child include  Process fork  {  exit  99  }                                  #=>  27429 wait                                                          #=>  27429 $?.exitstatus                                        #=>  99   pid  =  fork  {  sleep  3  }                      #=>  27440 Time.now                                                  #=>  2008-­‐03-­‐08  19:56:16  +0900 waitpid(pid,  Process::WNOHANG)      #=>  nil Time.now                                                  #=>  2008-­‐03-­‐08  19:56:16  +0900 waitpid(pid,  0)                                    #=>  27440 Time.now                                                  #=>  2008-­‐03-­‐08  19:56:19  +0900 Process.fork  {  exit  99  }      #=>  27437 pid,  status  =  Process.wait2 pid                                                #=>  27437 status.exitstatus                    #=>  99 Monday, April 22, 13
  37. REAP YOUR ZOMBIES • Exit status of a process is

    available until collected • Exited process becomes a zombie until reaped puts  fork  {  exit  0  } sleep >  ps  axf  |  grep  ruby 10828  pts/2    S+      0:00    |      \_  ruby  tmp.rb 10829  pts/2    Z+      0:00    |              \_  [ruby]  <defunct> Monday, April 22, 13
  38. DEAD CHILDREN = ZOMBIES • Dead children become zombies (even

    if for a short time) • Zombies can’t be killed • Lots of zombies - something’s wrong somewhere Monday, April 22, 13
  39. DEAD CHILDREN = ZOMBIES Make sure to always use Process.wait

    et al to reap child processes! • Dead children become zombies (even if for a short time) • Zombies can’t be killed • Lots of zombies - something’s wrong somewhere Monday, April 22, 13
  40. CASE STUDY 3: UNICORN “I like Unicorn because it’s Unix”

    (c) Ryan Tomayko / GitHub •Mongrel minus threads plus Unix processes •leans heavily on OS kernel to balance connections and manage workers Monday, April 22, 13
  41. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end Monday, April 22, 13
  42. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end        begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true master loop reaps exited workers until stopped Monday, April 22, 13
  43. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return collecting status of the next exited worker Monday, April 22, 13
  44. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end                proc_name  'master' renaming the master process when doing zero- downtime deploys Monday, April 22, 13
  45. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil wpid is PID of the reaped worker - close communication pipe and remove its data Monday, April 22, 13
  46. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m) generate log message and put it into normal or error log depending on exit status Monday, April 22, 13
  47. UNICORN AS A REAPER https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb#L380 class  Unicorn::HttpServer    def  join

           begin            reap_all_workers            case  SIG_QUEUE.shift            when  nil                master_sleep(sleep_time)            #  when  ...  <-­‐  handling  signals  here            end        end  while  true        stop  #  gracefully  shutdown  all  workers  on  our  way  out    end    def  reap_all_workers        begin            wpid,  status  =  Process.waitpid2(-­‐1,  Process::WNOHANG)            wpid  or  return            if  reexec_pid  ==  wpid                logger.error  "reaped  #{status.inspect}  exec()-­‐ed"                self.reexec_pid  =  0                self.pid  =  pid.chomp('.oldbin')  if  pid                proc_name  'master'            else                worker  =  WORKERS.delete(wpid)  and  worker.close  rescue  nil                m  =  "reaped  #{status.inspect}  worker=#{worker.nr  rescue  'unknown'}"                status.success?  ?  logger.info(m)  :  logger.error(m)            end        rescue  Errno::ECHILD            break        end  while  true    end end        begin keep doing it until all exited workers are reaped        rescue  Errno::ECHILD            break        end  while  true Monday, April 22, 13
  48. READ THE CODE Ryan Tomayko, “I like Unicorn because it’s

    Unix” http://tomayko.com/writings/unicorn-is-unix Official site http://unicorn.bogomips.org/ Source code HTTP_Server: https://github.com/defunkt/unicorn/blob/master/lib/unicorn/http_server.rb Monday, April 22, 13
  49. MORE IPC • Signals • Pipes • Sockets (TCP, Unix)

    but there’s enough to make a whole separate talk Monday, April 22, 13
  50. MORE ABOUT PROCESSES Jesse Storimer, “Working with Unix processes” http://www.workingwithunixprocesses.com/

    Eric Wong, “Unix System Programming in Ruby” http://librelist.com/browser/usp.ruby/ Monday, April 22, 13
  51. THREADS AREN’T PROCESSES •a process has many threads (at least

    two in Ruby) •each thread has its own: - stack - registers - execution pointer Monday, April 22, 13
  52. THREADS AREN’T PROCESSES •a process has many threads (at least

    two in Ruby) •each thread has its own: - stack - registers - execution pointer but the memory is shared between all threads! Monday, April 22, 13
  53. WHAT IS “SHARED MEMORY” counter  =  0   worker  =

     Proc.new  do    1.upto(1000)  {  counter  +=  1  }    worker_id  =  $$    puts  "worker  #{worker_id}:  #{counter}" end   fork(&worker) fork(&worker)   Process.waitall puts  "parent  counter  =  #{counter}" Processes %  ruby  tmp.rb process  23307:  1000 process  23308:  1000 parent  counter  =  0 counter  =  0   worker  =  Proc.new  do    1.upto(1000)  {  counter  +=  1  }    worker_id  =  Thread.current.object_id    puts  "worker  #{worker_id}:  #{counter}" end   [    Thread.new(&worker),  Thread.new(&worker) ].each(&:join)   puts  "counter  =  #{counter}" Threads %  ruby  tmp.rb thread  70336137980280:  1774 thread  70336137980180:  2000 counter  =  2000 %  ruby  -­‐v ruby  2.0.0p0  (2013-­‐02-­‐24  revision  39474) Monday, April 22, 13
  54. WHAT IS “SHARED MEMORY” counter  =  0   worker  =

     Proc.new  do    1.upto(1000)  {  counter  +=  1  }    worker_id  =  $$    puts  "worker  #{worker_id}:  #{counter}" end   fork(&worker) fork(&worker)   Process.waitall puts  "parent  counter  =  #{counter}" Processes %  ruby  tmp.rb process  23307:  1000 process  23308:  1000 parent  counter  =  0 counter  =  0   worker  =  Proc.new  do    1.upto(1000)  {  counter  +=  1  }    worker_id  =  Thread.current.object_id    puts  "worker  #{worker_id}:  #{counter}" end   [    Thread.new(&worker),  Thread.new(&worker) ].each(&:join)   puts  "counter  =  #{counter}" Threads %  ruby  tmp.rb thread  70336137980280:  1774 thread  70336137980180:  2000 counter  =  2000 %  ruby  -­‐v ruby  2.0.0p0  (2013-­‐02-­‐24  revision  39474) counter var is shared counter var is separate Monday, April 22, 13
  55. IMPLICATIONS • context switch can occur at any time in

    any thread • make sure your threads are not read-writing common data • compound operations (like ||= or +=) can be interrupted in the middle! • use Thread.current[:varname] if you need to Monday, April 22, 13
  56. IMPLICATIONS • context switch can occur at any time in

    any thread • make sure your threads are not read-writing common data • compound operations (like ||= or +=) can be interrupted in the middle! • use Thread.current[:varname] if you need to Example: memoization class  Client    def  self.channel        @c  ||=  Channel.new    end end bad class  Client    def  self.channel        Thread.current[:channel]  ||=  Channel.new    end end better Monday, April 22, 13
  57. GREEN THREADS • Ruby < 1.9 - Ruby-owned thread management

    • Invisible to and unmanageable by OS kernel • OS still runs a single thread by process • Concurrency, but not parallelization • Green threads are NOT UNIX Monday, April 22, 13
  58. GREEN THREADS • Ruby < 1.9 - Ruby-owned thread management

    • Invisible to and unmanageable by OS kernel • OS still runs a single thread by process • Concurrency, but not parallelization • Green threads are NOT UNIX SUCK! Monday, April 22, 13
  59. NATIVE THREADS IN MRI • Ruby 1.9 and 2.0 •

    allow for truly parallel execution • but only on blocking IO • Global Interpreter Lock (GIL) on Ruby code execution Monday, April 22, 13
  60. GIL • Protects MRI internals from thread safety issues •

    Allows MRI to operate with non-thread-safe C extensions • Isn’t going away any time soon Monday, April 22, 13
  61. GIL • Protects MRI internals from thread safety issues •

    Allows MRI to operate with non-thread-safe C extensions • Isn’t going away any time soon Makes sure your Ruby code will NEVER run in parallel on MRI Monday, April 22, 13
  62. PURE RUBY CODE AND GIL require  'benchmark' require  'digest/sha2'  

    worker  =  Proc.new  do    200_000.times  {  Digest::SHA512.hexdigest('DEADBEEF')  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end Monday, April 22, 13
  63. PURE RUBY CODE AND GIL require  'benchmark' require  'digest/sha2'  

    worker  =  Proc.new  do    200_000.times  {  Digest::SHA512.hexdigest('DEADBEEF')  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end %  ruby-­‐2.0.0-­‐p0  tmp.rb              ...                real single  ...      (    1.935500) multi    ...      (    2.093167) Monday, April 22, 13
  64. PURE RUBY CODE AND GIL require  'benchmark' require  'digest/sha2'  

    worker  =  Proc.new  do    200_000.times  {  Digest::SHA512.hexdigest('DEADBEEF')  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end %  ruby-­‐2.0.0-­‐p0  tmp.rb              ...                real single  ...      (    1.935500) multi    ...      (    2.093167) GIL doesn’t allow pure Ruby code to run in parallel, thus the same time as sequential code Monday, April 22, 13
  65. PURE RUBY CODE AND GIL require  'benchmark' require  'digest/sha2'  

    worker  =  Proc.new  do    200_000.times  {  Digest::SHA512.hexdigest('DEADBEEF')  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end %  ruby-­‐2.0.0-­‐p0  tmp.rb              ...                real single  ...      (    1.935500) multi    ...      (    2.093167) %  jruby-­‐1.7.3  tmp.rb                ...            real single    ...  (    2.450000) multi      ...  (    1.089000) jRuby doesn’t have GIL, so fully parallel execution Monday, April 22, 13
  66. PURE RUBY CODE AND GIL %  jruby-­‐1.7.3  tmp.rb    

               ...            real single    ...  (    2.450000) multi      ...  (    1.089000) Monday, April 22, 13
  67. PURE RUBY CODE AND GIL %  jruby-­‐1.7.3  tmp.rb    

               ...            real single    ...  (    2.450000) multi      ...  (    1.089000) why only 2x faster, if we’re running 5 threads? Monday, April 22, 13
  68. PURE RUBY CODE AND GIL %  jruby-­‐1.7.3  tmp.rb    

               ...            real single    ...  (    2.450000) multi      ...  (    1.089000) why only 2x faster, if we’re running 5 threads? one thread per physical CPU core Monday, April 22, 13
  69. CPU-BOUND THREADS • run mostly Ruby code and calculations •

    with GIL - execute concurrently, but not in parallel • for parallel processing - must be split into processes, or run on non-MRI implementation • scaling limited to the number of physical cores Monday, April 22, 13
  70. IO-BOUND THREADS • spend most of their time waiting for

    blocking IO operations • can run in parallel on Ruby 1.9 • scale beyond physical cores, but still up to a limit Monday, April 22, 13
  71. IO-BOUND THREADS require  'benchmark' require  'open-­‐uri'   worker  =  Proc.new

     do    5.times  {  open  'http://google.com'  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end Monday, April 22, 13
  72. IO-BOUND THREADS require  'benchmark' require  'open-­‐uri'   worker  =  Proc.new

     do    5.times  {  open  'http://google.com'  } end   Benchmark.bm  do  |bb|    bb.report  'single'  do        5.times(&worker)    end      bb.report  'multi'  do        5.times.map  {  Thread.new(&worker)  }.each(&:join)    end end %  ruby-­‐2.0.0-­‐p0  tmp.rb              ...                real single  ...      (  13.714725) multi    ...      (    3.539165) %  jruby-­‐1.7.3  tmp.rb                ...            real single    ...  (  15.273000) multi      ...  (    3.820000) •not affected by GIL •scale proportionally to the number of threads Monday, April 22, 13
  73. FIBERS ARE NEW GREEN • user-space threads introduced in Ruby

    1.9 • scheduled by Ruby, not OS kernel • very useful for event-based code • need to behave cooperatively to be efficient Monday, April 22, 13
  74. THREAD EXEC CONTROLS • mutexes • semaphores • conditional variables

    • thread-safe data structures Monday, April 22, 13
  75. IN THE WILD: SIDEKIQ • Efficient messaging processing • Unlike

    Resque, utilizes threads instead of processes • Shines with IO-heavy workers http://sidekiq.org Monday, April 22, 13
  76. IN THE WILD: CELLULOID • Framework to work with threads

    and build concurrent apps • Allows to work with threads as with regular app objects • Easy async calls to other threads http://celluloid.io Monday, April 22, 13
  77. MORE ON THREADS ET AL Jesse Storimer, “Working with Ruby

    threads” http://www.workingwithrubythreads.com yours truly, “Concurrent programming and threads in Ruby - reading list” http://speakmy.name/2013/04/02/concurrent-programming-and-threads-in-ruby-reading-list/ Monday, April 22, 13
  78. “Those who don’t understand Unix are condemned to reinvent it,

    poorly” Henry Spencer Monday, April 22, 13