Usando o Upstart para gerenciar o unicórnio com rbenv + bundler binstubs com ruby-local-exec shebang

5

Tudo bem, isso está derretendo meu cérebro. Pode ter algo a ver com o fato de eu não entender o Upstart tão bem quanto deveria. Desculpe antecipadamente pela longa pergunta.

Estou tentando usar o Upstart para gerenciar o processo mestre Unicorn de um aplicativo Rails. Aqui está o meu atual /etc/init/app.conf :

description "app"

start on runlevel [2]
stop on runlevel [016]

console owner

# expect daemon

script
  APP_ROOT=/home/deploy/app
  PATH=/home/deploy/.rbenv/shims:/home/deploy/.rbenv/bin:$PATH
  $APP_ROOT/bin/unicorn -c $APP_ROOT/config/unicorn.rb -E production # >> /tmp/upstart.log 2>&1
end script

# respawn

Isso funciona muito bem - os Unicorns começam bem. O que não é ótimo é que o PID detectado não é do mestre Unicorn, é de um processo sh . Isso em si também não é tão ruim - se eu não estivesse usando a estratégia de implantação zero-downtime automagical do Unicorn. Porque logo após eu enviar -USR2 para o meu mestre Unicorn, um novo mestre aparece, e o antigo morre ... e o mesmo acontece com o processo sh . Então, o Upstart acha que meu trabalho morreu e não consigo mais reiniciá-lo com restart ou pará-lo com stop se eu quiser.

Eu brinquei com o arquivo de configuração, tentando adicionar -D à linha Unicorn (assim: $APP_ROOT/bin/unicorn -c $APP_ROOT/config/unicorn.rb -E production -D ) para daemonizar Unicorn, e adicionei a linha expect daemon , mas isso não funcionou . Eu tentei expect fork também. Várias combinações de todas essas coisas podem fazer com que start e stop sejam interrompidas, e então o Upstart fica realmente confuso sobre o estado do trabalho. Então eu tenho que reiniciar a máquina para consertá-lo.

Acho que o Upstart está tendo problemas para detectar quando / se o Unicorn está bifurcando porque estou usando rbenv + o ruby-local-exec shebang no meu script $APP_ROOT/bin/unicorn . Aqui está:

#!/usr/bin/env ruby-local-exec
#
# This file was generated by Bundler.
#
# The application 'unicorn' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'pathname'
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
  Pathname.new(__FILE__).realpath)

require 'rubygems'
require 'bundler/setup'

load Gem.bin_path('unicorn', 'unicorn')

Além disso, o script ruby-local-exec é assim:

#!/usr/bin/env bash
#
# 'ruby-local-exec' is a drop-in replacement for the standard Ruby
# shebang line:
#
#    #!/usr/bin/env ruby-local-exec
#
# Use it for scripts inside a project with an '.rbenv-version'
# file. When you run the scripts, they'll use the project-specified
# Ruby version, regardless of what directory they're run from. Useful
# for e.g. running project tasks in cron scripts without needing to
# 'cd' into the project first.

set -e
export RBENV_DIR="${1%/*}"
exec ruby "$@"

Portanto, há um exec com o qual estou preocupado. Ele aciona um processo Ruby, que aciona o Unicorn, que pode ou não se desmembrar, o que tudo acontece a partir de um processo sh ... o que me faz duvidar seriamente da capacidade do Upstart de rastrear tudo isso bobagem.

O que estou tentando fazer é possível? Pelo que entendi, a sub-rotina expect no Upstart só pode ser informada (via daemon ou fork ) para esperar no máximo dois garfos.

    
por codykrieger 29.12.2011 / 12:33

3 respostas

3

Na verdade, uma limitação do upstart é que ele não pode rastrear daemons que fazem o que o unicórnio está fazendo ... ser fork / exec e sair do processo principal. Acredite ou não, o sshd faz a mesma coisa no SIGHUP, e se você olhar, o /etc/init/ssh.conf garante que o sshd seja executado em primeiro plano. Esse também é um dos motivos pelos quais o apache2 ainda usa um script init.d.

Parece que o gunicorn na verdade se daemoniza ao receber o SIGUSR1, forking e, em seguida, sair. Isso seria confuso para qualquer um dos gerentes de processo que tentam manter um processo ativo.

Eu acho que você tem duas opções. 1 é só não usar o SIGUSR1 e parar / iniciar o gunicorn quando precisar.

A outra opção é não usar o rastreamento de pid do upstart, e faça isso:

start on ..
stop on ..

pre-start exec gunicorn -D --pid-file=/run/gunicorn.pid
post-stop exec kill 'cat /run/gunicorn.pid'

Não é tão sexy quanto o acompanhamento de pid, mas pelo menos você não precisará escrever um script init.d inteiro.

(aliás, isso não tem nada a ver com os shebangs / execs. Ambas as coisas funcionam exatamente como executar um executável comum, então eles não causariam nenhum garfo extra).

    
por 18.01.2012 / 18:05
7

Escolhi uma solução diferente da do SpamapS .. Também estou executando um aplicativo com preload_app = true, gerenciado pelo Upstart.

Quando eu estava procurando resolver esse problema sozinho, eu estava usando o "exec" do Upstart para iniciar meu aplicativo ("exec bundle exec unicorn_rails blah blah"). Então eu encontrei sua pergunta, e percebi que ao invés de usar o "exec" do Upstart para especificar meu executável, eu poderia usar uma sub-rotina de script, que seria executada em seu próprio processo, o processo que o Upstart iria assistir. >

Então, meu arquivo de configuração do Upstart inclui isto:

respawn

script
  while true; do
    if [ ! -f /var/www/my_app/shared/pids/unicorn.pid ]; then
      # Run the unicorn master process (this won't return until it exits).
      bundle exec unicorn_rails -E production -c /etc/unicorn/my_app.rb >>/var/www/my_app/shared/log/unicorn.log
    else
      # Someone restarted the master; wait for the new master to exit.
      PID='cat /var/www/my_app/shared/pids/unicorn.pid'
      while [ -d /proc/$PID ]; do
        sleep 2
      done
      # If we get here, the master has exited, either because someone restarted
      # it again (in which case there's already a new master running), or
      # it died for real (in which case we'll need to start a new process).
      # The sleep above is a tradeoff between polling load and mimizing the
      # restart delay when the master dies for real (which should hopefully be
      # rare).
    fi
  done
end script

O before_fork no meu arquivo de configuração do Unicorn é exatamente como sugerido no exemplo do site unicornio, link :

before_fork do |server, worker|
  ActiveRecord::Base.connection.disconnect! if defined?(ActiveRecord::Base)

  old_pid = '/var/www/my_app/shared/pids/unicorn.pid.oldbin'
  if server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
  sleep 0.5
end

Então: na inicialização, o script Upstart não encontra o pidfile, então ele executa o unicorn_rails, que continua rodando.

Mais tarde, reimplementamos nosso aplicativo e uma tarefa do Capistrano aciona a reinicialização do aplicativo por meio de:

kill -USR2 'cat /var/www/my_app/shared/pids/unicorn.pid'

Isto diz ao antigo mestre Unicorn para iniciar um novo processo mestre Unicorn, e como o novo mestre inicia os trabalhadores, o bloco unicorn before_fork envia sinais TTOU para o master antigo para desligar os trabalhadores antigos (graciosamente), então QUIT uma vez só um trabalhador foi embora.

Aquele QUIT faz com que o antigo mestre seja encerrado (mas somente quando novos trabalhadores já estão manipulando a carga), então o "bundle exec unicorn_rails" retorna no script de unicórnio. Esse script, em seguida, faz um loop ao redor, vê o pidfile existente e aguarda a saída do processo. Não vai sair até a próxima implantação, mas vamos dar uma volta novamente se isso acontecer; nós também damos a volta novamente sempre que o mestre morre.

Se o script bash morrer, o Upstart irá reiniciá-lo, porque esse é o processo que está assistindo (como você vê se você já fez status my_app - Upstart relata o PID do script bash. Você ainda pode stop my_app , ou restart my_app , que não faz nada gracioso.

    
por 17.10.2012 / 23:54
0

De acordo com a documentação de upstart , você pode informar ao upstart para não enviar os sinais TERM e KILL para um serviço dizendo isso no bloco pre-stop . Tudo o que você precisa fazer é dizer start no seu pre-stop e ele abortará o envio do sinal.

Juntamente com o truque acima, por Bryan, que descobriu que um script também pode incluir um loop infinito e não ser o processo real de unicórnio - criei um exemplo que funciona usando essa técnica.

O exemplo é bastante simples, ele lida com o envio de USR2 ao executar o upstart stop unicorn . E também irá reaparecer um novo unicórnio se por algum motivo todos os unicórnios morrerem por conta própria.

O código e a saída dos testes estão aqui - link

    
por 17.08.2013 / 08:41