Gunicorn: Уточнить, что/как работают таймаут и грациозный_таймаут

Созданный на 3 апр. 2017  ·  30Комментарии  ·  Источник: benoitc/gunicorn

(Извините за монолог здесь: простые вещи усложнились, и в итоге я копался в стеке. Надеюсь, то, что я задокументировал, будет полезно для читателя.)

Как я понял, по умолчанию:

  • После 30 секунд (настраивается с помощью timeout ) обработки запроса главный процесс gunicorn отправляет SIGTERM рабочему процессу, чтобы инициировать плавный перезапуск.
  • Если рабочий процесс не выключается в течение следующих 30 секунд (настраивается с помощью graceful_timeout ), главный процесс отправляет SIGKILL . Похоже, что этот сигнал также отправляется, когда рабочий процесс корректно завершает работу в течение периода graceful_timeout (https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9).

Вопросы:

  • Сигналы правильные?
  • Что на самом деле происходит, когда рабочий процесс gunicorn (sync) получает эти сигналы? Как он сообщает приложению WSGI, что сигнал был перехвачен и что-то должно произойти (хорошо, я предполагаю, что он просто «передает его»)?
  • Как, например, Flask обрабатывает сигнал SIGTERM — что на практике происходит при обработке запроса? Он просто устанавливает флаг для приложения WSGI (на уровне werkzeug), что оно должно завершить работу после завершения обработки запроса? Или SIGTERM уже как-то влияет на текущую обработку запросов - убивает соединения ввода-вывода или что-то еще, чтобы ускорить обработку запросов...?

На SIGKILL я думаю, что обработка запроса просто принудительно прерывается.

Я мог бы подать небольшой PR, чтобы улучшить документы об этом, если я пойму, как все работает на самом деле.

Discussion Documentation

Самый полезный комментарий

@tuukkamustonen --timeout не означает тайм-аут запроса. Это предназначено для проверки работоспособности рабочих. Для рабочих процессов синхронизации это работает как тайм-аут запроса, поскольку рабочий процесс не может делать ничего, кроме обработки запроса. Сердцебиение асинхронных рабочих процессов происходит даже тогда, когда они обрабатывают длительные запросы, поэтому, если рабочий процесс не заблокируется/не зависнет, он не будет уничтожен.

Возможно, было бы неплохо изменить название, если другие люди посчитают это запутанным.

Все 30 Комментарий

Хм, я думаю, что https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 подтверждает мои предположения о том, что SIGTERM просто настраивает worker на отключение после завершения обработки запроса (и настраивает worker не принимать все новые связи).

Похоже, то, как я интерпретировал timeout и graceful_timeout , неверно. Оба периода на самом деле относятся ко времени начала обработки запроса. Таким образом, по умолчанию, поскольку оба параметра установлены на 30 секунд, изящный перезапуск не включен. Если я сделаю что-то вроде --graceful-timeout 15 --timeout 30 , это должно означать, что изящный перезапуск инициируется через 15 секунд, а рабочий принудительно завершается через 30 секунд, если запрос не был завершен до этого.

Однако похоже, что если ответ возвращается между graceful_timeout и timeout , то рабочий все-таки не перезапускается? Разве не должно?

Я тестировал app.py :

import time
from flask import Flask

app = Flask(__name__)

@app.route('/foo')
def foo():
    time.sleep(3)
    return 'ok'

Потом:

12:51 $ gunicorn app:app --timeout 5 --graceful-timeout 1
[2017-04-03 12:51:37 +0300] [356] [INFO] Starting gunicorn 19.6.0
[2017-04-03 12:51:37 +0300] [356] [INFO] Listening at: http://127.0.0.1:8000 (356)
[2017-04-03 12:51:37 +0300] [356] [INFO] Using worker: sync
[2017-04-03 12:51:37 +0300] [359] [INFO] Booting worker with pid: 359

Затем я отправляю curl localhost:8000/foo , который возвращается через 3 секунды. Но в пушке ничего не происходит - я не вижу следов инициированного или произошедшего изящного перезапуска?

Кажется, что при timeout выбрасывается SystemExit(1,) , прерывая текущую обработку запроса в Flask. Какой код или сигнал его генерирует, сказать не могу.

Это исключение передается через стек Flask, и его перехватывают любые обработчики teardown_request . Времени достаточно, чтобы что-то записать, но если вы делаете time.sleep(1) или что-то еще, требующее много времени в обработчике, это тихо уничтожается. Это как если бы было время 100-200 мс, прежде чем процесс был фактически принудительно завершен, и мне интересно, что это за задержка. Это не изящный тайм-аут, этот параметр не влияет на задержку. Я бы ожидал, что процесс будет просто принудительно убит на месте, вместо того, чтобы видеть, как SystemExit пробрасывается через стек, но затем потенциально может убить процесс в воздухе.

На самом деле, я не вижу, чтобы graceful_timeout делал что-либо — возможно, он не поддерживается для sync worker’ов, а может быть, он не работает «отдельно» (или вместе с timeout ) — только когда вы вручную отправляете SIGTERM ?

Также может быть странным то, что https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392 вообще не проверяет флаг graceful . Я предполагаю, что https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390 гарантирует, что self.WORKERS пусто, поэтому изящный тайм-аут не ожидает при выполнении неграциозной остановки.

@benoitc @tilgovi Не хочешь помочь? Надеюсь, мои записи выше имеют смысл...

@tuco86 tuco86 graceful timeout доступен только тогда, когда вы выходите из арбитра, обновляете его (USR2), отправляете сигнал HUP арбитру или отправляете сигнал QUIT рабочему. Т.е. используется только тогда, когда действие нормальное

Тайм-аут здесь для того, чтобы занятые работники не блокировали другие запросы. Если они не уведомляют арбитра за время, меньшее, чем timeout , рабочий процесс просто завершается, а соединение с клиентом закрывается.

Хорошо. Имеет ли timeout какой-либо эффект, когда вы:

выйти из арбитра, обновить его (USR2), отправить сигнал HUP арбитру или отправить сигнал QUIT рабочему

Я имею в виду, что, если рабочий не выключится через graceful_timeout - после этого сработает timeout , и рабочие будут принудительно убиты, или пользователь может вызвать SIGQUIT на случай, если они не умрут изящно?

Сигнал QUIT рабочему

Я предполагаю, что вы имели в виду TERM здесь (поскольку QUIT задокументировано как _быстрое завершение работы_ как для мастера, так и для рабочих)?

если рабочий не выключится в течение льготного времени, он будет убит без какой-либо другой задержки.

Конечно. Спасибо, что прояснили ситуацию!

@benoitc Вопрос в контексте этого старого билета - что на самом деле означает последнее предложение в документации timeout ?

Обычно устанавливается на тридцать секунд. Устанавливайте это значение заметно выше только в том случае, если вы уверены в последствиях для работников синхронизации. Для несинхронизирующих рабочих это просто означает, что рабочий процесс все еще обменивается данными и не привязан к продолжительности времени, необходимому для обработки одного запроса.

Не будучи носителем английского языка, мне трудно это понять. Означает ли это, что timeout не поддерживается для несинхронизированных воркеров (потому что я, кажется, наблюдаю это: я использую gthread и тайм-аут не срабатывает и не убивает слишком медленные запросы )?

@tuukkamustonen --timeout не означает тайм-аут запроса. Это предназначено для проверки работоспособности рабочих. Для рабочих процессов синхронизации это работает как тайм-аут запроса, поскольку рабочий процесс не может делать ничего, кроме обработки запроса. Сердцебиение асинхронных рабочих процессов происходит даже тогда, когда они обрабатывают длительные запросы, поэтому, если рабочий процесс не заблокируется/не зависнет, он не будет уничтожен.

Возможно, было бы неплохо изменить название, если другие люди посчитают это запутанным.

@tilgovi timeout просто отлично, хотя что-то вроде worker_timeout может быть более описательным. Я просто сначала запутался, потому что timeout и graceful_timeout объявлены рядом друг с другом в документации, поэтому мой мозг предположил, что они тесно связаны, хотя на самом деле это не так.

Для рабочих процессов синхронизации это работает как тайм-аут запроса, поскольку рабочий процесс не может делать ничего, кроме обработки запроса. Сердцебиение асинхронных рабочих процессов происходит даже тогда, когда они обрабатывают длительные запросы, поэтому, если рабочий процесс не заблокируется/не зависнет, он не будет уничтожен.

У вас есть пример, когда timeout срабатывает с несинхронизированными воркерами? Это то, чего на самом деле никогда не должно происходить - может быть, только если есть ошибка, из-за которой рабочий блокируется / зависает?

Это правильно. Асинхронный рабочий процесс, полагающийся на ядро ​​цикла обработки событий, может выполнять процедуру с интенсивным использованием ЦП, которая не завершается в течение времени ожидания.

Другими словами, не только ошибка. Хотя иногда это может указывать на ошибку, например вызов блокирующей функции ввода-вывода, когда протокол asyncio был бы более подходящим.

Хороший пример - застревание в задаче с интенсивным использованием ЦП, спасибо.

Вызов блокировки ввода-вывода в асинхронном коде также является одним из них, но я не уверен, как это применимо к этому контексту — я запускаю традиционное приложение Flask с блокирующим кодом, но запускаю его с асинхронным рабочим процессом ( gthread ) без каких-либо исправлений обезьяны. И работает нормально. Я знаю, что на самом деле это больше не относится к контексту этого билета, но разве смешивание и сопоставление асинхронного/синхронного кода, подобного этому, не вызывает проблем?

И какой интервал сердцебиения? Какое разумное значение использовать для timeout с несинхронизированными воркерами?

Рабочий поток gthread не является асинхронным, но у него есть основной поток для сердцебиения, поэтому он также не будет истекать по тайм-ауту. В случае с этим воркером вы, вероятно, не увидите тайм-аут, если только воркер не будет сильно перегружен или, что более вероятно, вы вызовете модуль расширения C, который не выпускает GIL.

Вероятно, вам не нужно менять тайм-аут, если только вы не начнете видеть рабочие тайм-ауты.

Хорошо. Еще одна вещь:

Рабочий процесс gthread не является асинхронным

Может немного сбивать с толку тот факт, что рабочий процесс gthread не является асинхронным, а указан как рабочий процесс «AsyncIO» на странице http://docs.gunicorn.org/en/stable/design.html#asyncio -workers. Помимо этого, использование «потоков» не требует asyncio, так что это также вызывает вопросы у читателя. Просто говорю это с точки зрения наивного пользователя, я уверен, что все это технически обосновано.

Вкратце, рабочий процесс gthread реализован с помощью asyncio , но он порождает потоки для обработки кода синхронизации. Поправьте меня, если ошибаюсь.

Рад, что вы спросили!

Многопоточный рабочий процесс не использует asyncio и не наследуется от базового асинхронного рабочего класса.

Мы должны уточнить документацию. Я думаю, что он мог быть указан как асинхронный, потому что тайм-аут рабочего процесса обрабатывается одновременно, что делает его более похожим на асинхронный рабочий процесс, чем на синхронный рабочий процесс в отношении способности обрабатывать длинные запросы и параллельные запросы.

Было бы здорово уточнить документацию и сделать так, чтобы она точнее описывала всех воркеров.

да, работник gthreads не должен быть указан в работнике asyncio. может быть, лучше иметь раздел, описывающий дизайн каждого рабочего?

Повторно открываем это, чтобы мы могли отслеживать его как работу, чтобы уточнить раздел о типах рабочих процессов и тайм-аутах.

@тилгови

--timeout не означает тайм-аут запроса. Это предназначено для проверки работоспособности рабочих. Для рабочих процессов синхронизации это работает как тайм-аут запроса, поскольку рабочий процесс не может делать ничего, кроме обработки запроса. Сердцебиение асинхронных рабочих процессов происходит даже тогда, когда они обрабатывают длительные запросы, поэтому, если рабочий процесс не заблокируется/не зависнет, он не будет уничтожен.

Доступна ли опция тайм-аута запроса для асинхронных рабочих? Другими словами, как заставить арбитра убить воркера, который не обработал запрос в течение заданного времени?

@aschatten , к сожалению, нет. См. также №1658.

убить работника, который не обработал запрос в течение заданного времени

Поскольку рабочий процесс может обрабатывать несколько запросов одновременно, уничтожение всего рабочего процесса из-за истечения времени ожидания одного запроса звучит довольно экстремально. Не приведет ли это к тому, что все остальные запросы будут напрасно убиты?

Я помню, что uWSGI планировал ввести уничтожение на основе потоков в версии 2.1 или около того, хотя, вероятно, даже это применимо только к синхронным/потоковым рабочим процессам (и мои воспоминания об этом смутны).

Поскольку рабочий процесс может обрабатывать несколько запросов одновременно, уничтожение всего рабочего процесса из-за истечения времени ожидания одного запроса звучит довольно экстремально. Не приведет ли это к тому, что все остальные запросы будут напрасно убиты?

Подход может быть таким же, как и для max_request , где для каждого рабочего типа существует отдельная реализация.

Мы работаем над релизом на этой неделе, после чего _может_ пора переходить на R20, где мы планируем заняться несколькими важными вещами. Это может быть подходящее время, чтобы сделать текущий тайм-аут правильным тайм-аутом запроса для каждого типа рабочего процесса.

Комментирую здесь вместо того, чтобы создавать отдельную проблему, поскольку я пытаюсь понять, как должен работать тайм-аут, и я не уверен, является ли это ошибкой или нет.

Неожиданное поведение IMO, которое я вижу, таково:

Каждый запрос max-requests'th (тот, после которого будет перезапущен воркер) истекает по тайм-ауту, тогда как остальные запросы выполняются успешно. В приведенном ниже примере выполняются 4 запроса, запросы 1, 2 и 4 выполняются успешно, а запрос 3 не выполняется.

Соответствующая конфигурация:

  • рабочий поток
  • обслуживание запроса занимает больше времени, чем время ожидания
  • max-requests не равен нулю
import time

def app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    time.sleep(5)
    return [b"Hello World\n"]

стрелорог:

gunicorn --log-level debug -k gthread -t 4 --max-requests 3 "app:app"
...
[2018-02-08 10:11:59 +0200] [28592] [INFO] Starting gunicorn 19.7.1
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] Arbiter booted
[2018-02-08 10:11:59 +0200] [28592] [INFO] Listening at: http://127.0.0.1:8000 (28592)
[2018-02-08 10:11:59 +0200] [28592] [INFO] Using worker: gthread
[2018-02-08 10:11:59 +0200] [28595] [INFO] Booting worker with pid: 28595
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] 1 workers
[2018-02-08 10:12:06 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:11 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:15 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:20 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:23 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:23 +0200] [28595] [INFO] Autorestarting worker after current request.
[2018-02-08 10:12:27 +0200] [28592] [CRITICAL] WORKER TIMEOUT (pid:28595)
[2018-02-08 10:12:27 +0200] [28595] [INFO] Worker exiting (pid: 28595)
[2018-02-08 10:12:28 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:28 +0200] [28599] [INFO] Booting worker with pid: 28599
[2018-02-08 10:12:32 +0200] [28599] [DEBUG] GET /
[2018-02-08 10:12:37 +0200] [28599] [DEBUG] Closing connection.
^C[2018-02-08 10:12:39 +0200] [28592] [INFO] Handling signal: int

Клиент:

[salonen<strong i="19">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="20">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="21">@mac</strong> ~]$ curl http://127.0.0.1:8000
curl: (52) Empty reply from server
[salonen<strong i="22">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World

какой там должен быть план? Я имею в виду следующее:

  • [ ] обновить описание воркера (если еще нужно)
  • [ ] задокументировать протокол для обнаружения мертвых или заблокированных воркеров

Должен ли он быть 20.0 или мы можем отложить его?

отсрочка.

Эй, так это не будет частью 20.0?

Это может быть подходящее время, чтобы сделать текущий тайм-аут правильным тайм-аутом запроса для каждого типа рабочего процесса.

уточнено. @ lucas03 lucas03 неясно, что такое тайм-аут запроса. пожалуйста, откройте билет, если вам нужно что-то конкретное?.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги