Ошибки H13 (Соединение закрыто без ответа) при уменьшении масштаба Heroku
Я запускаю приложение Django в образе Docker с uWSGI, supervisor и nginx на Heroku.
Я часто получаю ошибки H13 (Соединение закрыто без ответа) при уменьшении масштаба приложения:
Эта проблема генерирует следующие события журнала:
2022-10-12T09:35:13.231318+00:00 heroku web.3 - - State changed from up to down
2022-10-12T09:35:13.774228+00:00 heroku web.3 - - Stopping all processes with SIGTERM
2022-10-12T09:35:14.028602+00:00 heroku router - - at=error code=H13 desc="Connection closed without response" method=GET path="/comments/api/assets-uuidasset/xxxx-xxxx-xxxx-xxxx-xxxxx/count/?_=1665564563"
Я предполагаю, что проблема заключается либо в том, что сокет не закрывается по сигналу SIGTERM, либо nginx закрывается неаккуратно по сигналу SIGTERM (он должен получить SIGQUIT для изящного завершения работы) или что-то подобное.
Первый случай описан в этой статье, посвященной Puma и Ruby: https://www.schneems.com/2019/07/12/puma-4-hammering-out-h13sa-debugging-story/
Второй случай описан здесь: https://canonical.com/blog/avoiding-dropped-connections-in-nginx-containers-with-stopsignal-sigquit
После трех недель работы я наконец смог устранить эту проблему.
Короткий ответ:
Избегайте использования Heroku для запуска образов Docker, если это возможно.
Heroku посылает SIGTERM
ВСЕМ процессам в dyno, с чем очень трудно справиться. Вам понадобится патч почти для каждого процесса внутри контейнера Docker, чтобы он считался с SIGTERM
и красиво завершался.
Стандартным способом завершения контейнера Docker является команда docker stop
, которая посылает SIGTERM
ТОЛЬКО корневому процессу (точке входа), где с ним можно разобраться.
Heroku имеет очень произвольный процесс завершения экземпляра, несовместимый с существующими приложениями, а также с существующими развертываниями образов Docker. И согласно моему общению с Heroku, они не могут изменить это в будущем.
Длинный ответ:
Выявлена не одна проблема, а 5 отдельных проблем. Для успешного завершения экземпляра должны быть выполнены следующие условия:
- Nginx должен завершаться первым и запускаться последним (чтобы маршрутизатор Heroku перестал посылать запросы, это похоже на Puma), и это должно быть изящно, что обычно делается с помощью сигнала SIGQUIT. .
- Другие приложения должны завершаться изящно в правильном порядке - в моем случае сначала Nginx, затем Gunicorn и последним PGBouncer. Порядок завершения приложений важен - например, PGBouncer должен завершиться после Gunicorn, чтобы не прерывать выполнение SQL-запросов.
- <8>> должен перехватывать сигнал
docker-entrypoint.sh
. Это не проявилось, когда я тестировал локально.SIGTERM
Для достижения этой цели мне пришлось работать с каждым приложением отдельно:
Nginx:
Мне нужно было подправить Nginx для переключения сигналов SIGTERM
и SIGQUIT
, поэтому я выполнил следующую команду в своем Dockerfile:
# Compile nginx and patch it to switch SIGTERM and SIGQUIT signals
RUN curl -L http://nginx.org/download/nginx-1.22.0.tar.gz -o nginx.tar.gz \
&& tar -xvzf nginx.tar.gz \
&& cd nginx-1.22.0 \
&& sed -i "s/ QUIT$/TIUQ/g" src/core/ngx_config.h \
&& sed -i "s/ TERM$/QUIT/g" src/core/ngx_config.h \
&& sed -i "s/ TIUQ$/TERM/g" src/core/ngx_config.h \
&& ./configure --without-http_rewrite_module \
&& make \
&& make install \
&& cd .. \
&& rm nginx-1.22.0 -rf \
&& rm nginx.tar.gz
uWSGI/Gunicorn:
Я отказался от uWSGI и перешел на Gunicorn (который изящно завершается на SIGTERM
), но в итоге мне все равно пришлось его патчить, потому что он должен завершаться позже, чем Nginx. Я отключил сигнал SIGTERM
и отобразил его функцию на SIGUSR1
.
Моя исправленная версия находится здесь: https://github.com/PetrDlouhy/gunicorn/commit/1414112358f445ce714c5d4f572d78172b993b79
Я устанавливаю его с помощью:
RUN poetry run pip install -e git+https://github.com/PetrDlouhy/gunicorn@no_sigterm#egg=gunicorn[gthread] \
&& cd `poetry env info -p`/src/gunicorn/ \
&& git config core.repositoryformatversion 0 # Needed for Dockerfile.test only untill next version of Dulwich is released \
&& cd /project
PGBouncer:
Я также развернул PGBouncer, который мне пришлось модифицировать, чтобы он не реагировал на SIGTERM
с:
# Compile pgbouncer and patch it to switch SIGTERM and SIGQUIT signals
RUN curl -L https://github.com/pgbouncer/pgbouncer/releases/download/pgbouncer_1_17_0/pgbouncer-1.17.0.tar.gz -o pgbouncer.tar.gz \
&& tar -xvzf pgbouncer.tar.gz \
&& cd pgbouncer-1.17.0 \
&& sed -i "s/got SIGTERM, fast exit/PGBouncer got SIGTERM, do nothing/" src/main.c \
&& sed -i "s/ exit(1);$//g" src/main.c \
&& ./configure \
&& make \
&& make install \
&& cd .. \
&& rm pgbouncer-1.17.0 -rf \
&& rm pgbouncer.tar.gz
Его все еще можно изящно опустить с помощью SIGINT
.
docker-entrypoint.sh
Мне пришлось заманить SIGTERM
в ловушку docker-entrypoint.sh
с помощью:
_term() {
echo "Caught SIGTERM signal. Do nothing here, because Heroku already sent signal everywhere."
}
trap _term SIGTERM
супервайзер
Чтобы не получать ошибки R12
, все процессы должны завершаться до 30-секундного льготного периода Heroku. Я добился этого, установив приоритеты в supervisord.conf
:
[supervisord]
nodaemon=true
[program:gunicorn]
command=poetry run newrelic-admin run-program gunicorn wsgi:application -c /etc/gunicorn/gunicorn.conf.py
priority=2
stopsignal=USR1
...
[program:nginx]
command=/usr/local/nginx/sbin/nginx -c /etc/nginx/nginx.conf
priority=3
...
[program:pgbouncer]
command=/usr/local/bin/pgbouncer /project/pgbouncer/pgbouncer.ini
priority=1
stopsignal=INT
...
Тестирование решений:
Для того чтобы проверить, что происходит, мне пришлось разработать некоторые методы тестирования, которые могут пригодиться в разных, но похожих случаях.
Я создал представление, которое ждет 10 секунд перед ответом, и привязал его к /slow_view
url.
Затем я запустил сервер в экземпляре Docker, сделал запрос к медленному представлению с помощью curl -I "http://localhost:8080/slow_view"
и сделал второе подключение к экземпляру Docker и выполнил команду kill с помощью pkill -SIGTERM .
или, например, pkill -SIGTERM gunicorn
.
Я также могу выполнить команду kill на тестировании Heroku dyno, где я подключился с помощью heroku ps:exec --dyno web.1 --app my_app
.