Контейнер Python останавливается медленно - SIGTERM не передан процессу python? - PullRequest
2 голосов
/ 11 июля 2020

Я сделал простой python веб-сервер на основе этого примера , который работает внутри Docker

FROM python:3-alpine
WORKDIR /app

COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

COPY src src
CMD ["python", "/app/src/api.py"]
ENTRYPOINT ["/app/entrypoint.sh"]

Entrypoint:

#!/bin/sh
echo starting entrypoint
set -x
exec "$@"

Остановка контейнер занял очень много времени, хотя оператор exec с синтаксисом массива JSON должен передать его процессу python. Я предположил, что проблема с SIGTERM не передается в контейнер. Я добавил следующее в свой сценарий api.py для обнаружения SIGTERM

def terminate(signal,frame):
  print("TERMINATING")

if __name__ == "__main__":
    signal.signal(signal.SIGTERM, terminate)

    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s" % (hostName, serverPort))
    webServer.serve_forever()

Выполнено без Docker python3 api/src/api.py, я попытался

kill -15 $(ps -guaxf | grep python | grep -v grep | awk '{print $2}')

отправить SIGTERM ( 15 - это его числовой код ). Сценарий печатает TERMINATING , поэтому мой обработчик событий работает. Теперь я запускаю контейнер Docker, используя docker -compose, и нажимаю CTRL + C. Docker говорит изящно останавливается ... (нажмите Ctrl + C еще раз, чтобы принудительно) , но не выводит сообщение о завершении из обработчика событий.

Я также пытался запустить docker - составить в отдельном режиме, затем запустить docker-compose kill -s SIGTERM api и просмотреть логи. По-прежнему нет сообщения от обработчика событий.

Ответы [ 2 ]

2 голосов
/ 11 июля 2020

Docker запускает ваше приложение по умолчанию на переднем плане, поэтому, как и PID 1, это говорит о том, что процесс с PID 1 в качестве специального значения и определяет c защиты в Linux.

Это выделено в документации docker run:

Примечание

Процесс, запущенный как PID 1 внутри контейнера, обрабатывается Linux особым образом: он игнорирует любой сигнал с действием по умолчанию. В результате процесс не будет завершен на SIGINT или SIGTERM, если он не закодирован для этого.

Источник: https://docs.docker.com/engine/reference/run/#foreground

Чтобы исправить это, вы можете запустить контейнер в режиме одного контейнера с флагом --init из docker run:

Вы можете использовать флаг --init, чтобы указать, что процесс инициализации должен использоваться как PID 1 в контейнере. Указание процесса инициализации гарантирует, что обычные обязанности системы инициализации, такие как сбор ie процессов зомба, выполняются внутри созданного контейнера.

Источник: https://docs.docker.com/engine/reference/run/#specify -an-init-process

Такая же конфигурация возможна в docker-compose, просто указав init: true в контейнере.

An пример:

version: "3.8"
services:
  web:
    image: alpine:latest
    init: true

Источник: https://docs.docker.com/compose/compose-file/#init

1 голос
/ 13 июля 2020

Поскольку скрипт запускается как pid 1 по желанию, а установка init: true в docker-compose.yml, похоже, ничего не меняет, я взял более глубокий диск в этом топе c. Это заставляет меня выяснять несколько ошибок, которые я сделал:

Ведение журнала

Подход к печати сообщения при обнаружении SIGTERM был разработан как простой тестовый пример, чтобы увидеть, работает ли это в основном, прежде чем я забочусь об остановке сервера. Но я заметил, что сообщение не появляется по двум причинам:

Буферизация вывода

При запуске долгосрочного процесса в python, таком как HTTP-сервер (или любой while True l oop для пример), отображается без вывода при запуске контейнера, прикрепленного с docker-compose up (без флага -d). Чтобы получать живые журналы, нам нужно запустить python с флагом -u или установить переменную env PYTHONUNBUFFERED=TRUE.

Нет конвейера журнала после остановки

Но основная проблема заключалась не в буферизация вывода (это всего лишь примечание, поскольку мне интересно, почему не было вывода журнала из контейнера). При отмене контейнера docker-compose останавливает отправку журналов на консоль. Это означает, что с логической точки зрения он не может отображать ничего, что происходит ПОСЛЕ CTRL + C нажата .

Чтобы получить эти журналы, нам нужно дождаться, пока docker-compose не остановит контейнер, и запустить docker-compose logs. Он напечатает все, включая те, которые сгенерированы после нажатия CTRL + C. Используя docker-compose logs, я обнаружил, что SIGTERM передается в контейнер, и мой обработчик событий работает.

Остановка веб-сервера

С этими знаниями я попытался остановить экземпляр веб-сервера. Сначала это не работает, потому что недостаточно просто позвонить по номеру webServer.server_close(). После выполнения любой работы по очистке требуется явный выход:

def terminate(signal,frame):
  print("Start Terminating: %s" % datetime.now())
  webServer.server_close()
  sys.exit(0)

Когда sys.exit() не вызывается, процесс продолжает работать, что приводит к ~ 10 секундам ожидания, прежде чем Docker убьет его.

Полный рабочий пример

Вот демонстрационный скрипт, реализующий все, что я узнал:

from http.server import BaseHTTPRequestHandler, HTTPServer
import signal
from datetime import datetime
import sys, os

hostName = "0.0.0.0"
serverPort = 80

class MyServer(BaseHTTPRequestHandler):
  def do_GET(self):
    self.send_response(200)
    self.send_header("Content-Type", "text/html")
    self.end_headers()
    self.wfile.write(bytes("Hello from Python Webserver", "utf-8"))

webServer = None

def terminate(signal,frame):
  print("Start Terminating: %s" % datetime.now())
  webServer.server_close()
  sys.exit(0)

if __name__ == "__main__":
    signal.signal(signal.SIGTERM, terminate)

    webServer = HTTPServer((hostName, serverPort), MyServer)
    print("Server started http://%s:%s with pid %i" % ("0.0.0.0", 80, os.getpid()))
    webServer.serve_forever()

Работая в контейнере, его можно было очень быстро остановить, не дожидаясь Docker чтобы убить процесс:

$ docker-compose up --build -d
$ time docker-compose down
Stopping python-test_app_1 ... done
Removing python-test_app_1 ... done
Removing network python-test_default

real    0m1,063s
user    0m0,424s
sys     0m0,077s
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...