Graceful shutdown com Node.js e Docker

Otávio Paulino Pace
5 min readJun 25, 2018

--

Eu caindo graciosamente \o/

O que é graceful shutdown?

Quando uma aplicação é terminada ou interrompida, ela pode estar no meio de uma operação, uma requisição a outro serviço por exemplo. Caso isso ocorra, inconsistências podem acontecer. Portanto, o ideal é que quando a aplicação seja terminada ela saiba lidar com isso e finalizar tudo que estiver no meio, seja terminar uma requisição já iniciada ou liberar recursos. Isso é graceful shutdown.

Para realizar isso, é necessário utilizar os sinais de comunicação entre processos do sistema operacional.

O que são sinais IPC (Inter-Process Communication)?

Sabe quando usa Ctrl + C ou kill com umpidpara finalizar um programa no terminal? Basicamente isso envia um sinal para ele ser interrompido/terminado, ou seja, você já envia sinais para processos!

Tabela com alguns sinais e suas descrições e ações padrão

O funcionamento deles é o seguinte: caso seja enviado um sinal a um processo, o sistema operacional vai chamar a rotina desse processo para o sinal, caso ela não exista, ele utiliza a padrão. Exemplo: se eu utilizo Ctrl + C em um programa, é enviado um sinal de SIGINT para ele, caso ele tenha declarado uma função para lidar com esse evento, ela será chamada, se não, será utilizada a padrão que é interromper o processo.

Como escutar um sinal no Node.js?

Nada melhor para aprender algo do que ter um exemplo prático! No código abaixo são definidas 3 funções que irão ser chamadas quando o processo receber 3 sinais distintos. SIGTERM é o sinal para terminação, código 15. SIGINT é o sinal para interrupção, ele por exemplo é o utilizado no Ctrl + C e possui código 2. Já o SIGHUP é o de reload de arquivos de configuração ou de finalização da sessão do terminal, seu código é 1.

Esse programa irá se comportar da seguinte forma: nas três primeiras linhas estarão sendo sobreescritas as funcionalidades caso ocorra algum dos três sinais, depois de 10 em 10 segundos será escrito no terminal doing useless work!, isso será utilizado para representar algum trabalho que o script estaria fazendo.

Executando o script acima com node signal.js pode se observar o seguinte resultado no terminal após pressionar Ctrl + C algumas vezes no teclado.

^Cint
^Cint
^Cint
doing useless work!

Basicamente nosso programa está recebendo o sinal para ser interrompido através do Ctrl + C. Como definimos uma outra função para ser chamada ao invés da padrão (finalização do processo), ela está sendo chamada no lugar, e assim colocando no console int para cada sinal SIGINT.

Caso não tenha percebido, acabamos de criar um processo que não pode ser finalizado, seja por SIGINT (Ctrl + C), SIGTERM ou fechando o terminal (SIGHUP). Como podemos pará-lo de vez?

Se quiser finalizar o processo, terá de usar o programa kill. Por padrão ele envia um sinal que não pode ser escutado (SIGKILL) e que finaliza o processo independente do que estiver acontecendo. Tudo que esse programa precisa é do pid do processo, para conseguir esse valor é só utilizar comando:

> ps ax | grep "node signal.js"4562 pts/2    Sl+    0:00 node signal.js

Então executar kill -9 4562. O 9 é o número do sinal de SIGKILL.

Caso tente ser espertinho e tentar escutar o sinal SIGKILL, irá receber um erro, pois ele não pode ser escutado, assim como alguns outros sinais específicos como SIGSTOP.

Como realizar graceful shutdown de um servidor HTTP?

O exemplo acima contempla um servidor HTTP bem simples com somente uma rota. Caso alguém realize uma requisição nessa rota, irá demorar 10 segundos para ser respondido.

Se ocorrer uma requisição na rota /wait e o servidor for terminado no meio do processo, o cliente irá ficar sem resposta.

> curl localhost:8000/wait
curl: (52) Empty reply from server

Para resolver isso iremos utilizar o que vimos no exemplo dos sinais:

Observe que o código acima só tem uma adição, a função setupGracefulShutdown, que irá configurar o que o programa deve fazer quando o servidor for terminado.

Caso tente simular o comportamento anterior nesse novo servidor, o cliente irá receber a resposta conforme pediu e esses serão os logs do programa:

> node express
Started wait!
^CSIGINT happened!
Done!
server closed!

Mas o que de fato está acontecendo e o que é esse server.close(callback)?

Considerando o cenário em que o cliente envia uma requisição e o servidor recebe um sinal de SIGINT, basicamente ele irá chamar o callback que inicia a função server.close. Essa função não é do express, e sim do módulo nativo http do Node.js. Link para documentação: https://nodejs.org/api/http.html#http_server_close_callback.

A função listen do express, retorna uma instância da classe Server do módulo http, por isso podemos chamar a função close no retorno dela.

Essa função basicamente faz com que o servidor pare de receber requisições e termine todas as que estão em andamento, assim que isso ocorrer, irá chamar nosso callback passado para ele. Quando isso ocorre, o cliente recebe sua resposta e enfim terminamos o servidor com o devido código de finalização.

Quanto ao número 128, isso é um padrão pois o código de saída é restrito a um inteiro de 8 bits (0–255).

Lembrando que é de nossa responsabilidade chamar process.exit, já que sobreescrevemos a funcionalidade padrão de terminar o processo.

Agora utilizando Docker

Bom para isso, iremos criar um package.json e um Dockerfile.

Para construir a imagem iremos utilizar o seguinte comando:

docker build . -t graceful-server

E para iniciar o contêiner:

docker run -p 8000:8000 --name=graceful-server graceful-server

Excelente! Vamos agora testar para ver se comportamento continua como o esperado:

> curl localhost:8000/wait
curl: (52) Empty reply from server

Hmm, que estranho. Por que nosso servidou deixou de escutar os sinais?

Vamos entrar no contêiner e tentar entender o que acontece:

> docker exec -it graceful-server /bin/sh
/ # ps -o ppid,pid,comm
PPID PID COMMAND
0 1 npm
1 16 sh
16 17 node
0 23 sh
23 39 ps

Aqui PPID significa parent pid, PID significa process id e COMMAND é o comando atrelado ao processo.

Nosso servidor está rodando normalmente, porém, quem iniciou ele foi umshell script, e quem iniciou este foi o npm. Basicamente nosso sinal não está sendo propagado a frente. Existem algumas issues em repositórios relacionados ao Node e NPM, porém este é comportamento que temos hoje.

Uma forma simples de resolver isso é trocar o CMD [“npm", “start"] no Dockerfile por CMD ["node”, “server.js"].

Assim nossos logs no servidor ficam da serguinte forma:

> docker run -p 8000:8000 --name=graceful-server graceful-server
Started wait!
^CSIGINT happened!
Done!
server closed!

E no cliente:

> curl localhost:8000/wait
Done!⏎

Uhulll!! Agora sabe o que é graceful shutdown e como implementar utilizando Node.js e Docker!

Eu após a caída graciosa :)

--

--