Ошибки H13 (Соединение закрыто без ответа) при уменьшении масштаба Heroku

Я запускаю приложение Django в образе Docker с uWSGI, supervisor и nginx на Heroku.

Я часто получаю ошибки H13 (Соединение закрыто без ответа) при уменьшении масштаба приложения:

enter image description here

Эта проблема генерирует следующие события журнала:

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.

Вернуться на верх