Gunicorn: Ошибка POST при возврате ответа> 13k на Heroku

Созданный на 4 авг. 2014  ·  34Комментарии  ·  Источник: benoitc/gunicorn

Привет, мы столкнулись с этой проблемой в производственной среде с помощью Flask + Gunicorn + Heroku и не смогли найти причину или обходной путь.

Для одного конкретного запроса POST с параметрами POST запрос завершится ошибкой H18 (sock = backend) в маршрутизаторе Heroku, указывая на то, что сервер закрыл сокет, когда этого не должно было быть.

Мы начали уменьшать размер ответа этой отказавшей конечной точки, пока не сузили его до отметки около 13 тыс. Если мы отправим менее 13 КБ, ответ всегда будет работать. Если бы мы отправили более 13к, ответ почти всегда не работал.

Код для его воспроизведения доступен по адресу https://github.com/erjiang/gunicorn-issue - просто разверните репо в Heroku как есть и следуйте инструкциям в README.

( Feedback Requested unconfirmed help wanted - Bugs -

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

Мне удалось воспроизвести, используя тестовый пример на https://github.com/erjiang/gunicorn-issue (который использует gunicorn 19.9.0, Python 2.7.14, синхронизатор, --workers 4 ). Следует отметить, что вывод журнала доступа Gunicorn сообщает, что, по его мнению, он вернул HTTP 200.

Обновление до Python 3.7.3 + gunicorn master и уменьшение до --workers 1 не повлияло на воспроизводимость, однако переключение с синхронизатора на gevent привело к тому, что ошибка возникла реже (хотя это все еще происходило). Использование --log-level debug не выявило ничего существенного (единственным дополнительным выводом во время запроса была строка [DEBUG] POST /test1 ).

Затем я попробовал --spew , но проблема больше не воспроизводилась. Это заставило меня попробовать добавить time.sleep(1) до того , как resp.close() здесь , которые так же предотвратить проблему.

Таким образом, похоже, что причина в том, что буфер отправки сокета может быть не пустым во время close() , что может привести к потере ответа:

Примечание. close() освобождает ресурс, связанный с соединением, но не обязательно немедленно закрывает соединение. Если вы хотите своевременно закрыть соединение, вызовите shutdown() перед close() .

(См. Https://docs.python.org/3/library/socket.html#socket.socket.close)

Добавление sock.shutdown(socket.SHUT_RDWR) ( документы ) до sock.close() здесь решен вопрос для меня. Альтернативным исправлением, возможно, могло бы быть использование SO_LINGER , хотя, судя по тому, что я читал, здесь есть компромиссы.

Документы по этой теме найти сложно, но я обнаружил:
https://stackoverflow.com/questions/8874021/close-socket-directly-after-send-unsafe
https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable

Надеюсь, это поможет :-)

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

Действительно полезный отчет, спасибо @erjiang.

У меня нет учетной записи heroku для тестирования. Может ли кто-нибудь с такой учетной записью проверить это? cc @tilgovi @kennethreitz

Рада, но я, вероятно, не скоро доберусь до этого.

В качестве быстрой проверки работоспособности я запустил его локально и проверил несколько вещей с помощью curl, чтобы сравнить официантку и Gunicorn:

  • [x] Content-Length то же самое
  • [x] То же содержание тела
  • [x] Одна и та же кодировка передачи (ни один из них не указывает фрагментированный, оба используют Content-Length)

Затем мне интересно, есть ли различия на уровне TCP. Я tcpdump их и посмотрю, замечу ли я что-нибудь подозрительное.

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

@tilgovi Я предполагаю, что поведение, которое вы видите с официанткой, может быть воспроизведено с помощью потокового рабочего. В любом случае, спасибо, что позаботились об этом :)

Всем привет,
У меня такая же проблема. Удалось ли кому-нибудь из вас изучить этот вопрос более тщательно?
@tilgovi @erjiang @benoitc

Ваше здоровье
Максим

@maximkgn вы тоже пользуетесь

Я использую django 1.7.
У нас был определенный пост-ответ, который всегда был длиннее 13 КБ, и с определенной вероятностью ~ 0,5 ответ в клиенте был бы усечен до чуть более 13 КБ. В журналах heroku мы увидели ту же ошибку h18, и после того, как мы убедились, что в нашем коде python нет ошибок, мы должны были сделать вывод, что это происходит на уровне пулеметчика между heroku и нашим python.
Когда мы перешли на официантку / uwsgi, ошибка перестала происходить ..

@maximkgn что произойдет, если вы воспользуетесь настройкой --threads ?

Кто-нибудь может это проверить?

У меня такая же проблема с флягой и пулеметом (проверенные версии 19.3 и 19.4.5). @benoitc Я пробовал 1, 2 и 4 потока (с параметром --threads), и это не имеет значения.

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

@cbaines как выглядят запросы?

Friendpaste может принимать более 1 миллиона сообщений .... так что, конечно, нет ограничений внутри Gunicorn.

никогда не имел ответа. закрытие проблемы, так как она не воспроизводится. Не стесняйтесь открывать его снова, если это необходимо.

Все еще воспроизводится после обновления зависимостей, включая Flask 1.0.2 и gunicorn 19.9.0. Было бы неплохо привлечь внимание кого-нибудь в Heroku по этому поводу - я слышал, что у них есть несколько преданных Python людей.

См. Последнюю фиксацию здесь: https://github.com/erjiang/gunicorn-issue/

Я также регулярно получаю эту ошибку H18 при большом запросе GET.

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

тело ответа - 21,54 КБ

Все еще воспроизводится после обновления зависимостей, включая Flask 1.0.2 и gunicorn 19.9.0. Было бы неплохо привлечь внимание кого-нибудь в Heroku по этому поводу - я слышал, что у них есть несколько преданных Python людей.

См. Последнюю фиксацию здесь: https://github.com/erjiang/gunicorn-issue/

Я создал заявку в службу поддержки на Heroku. Буду обновлять здесь, если из этого появится что-нибудь полезное.

@benoitc выглядит так, как будто @erjiang предоставил воспроизводимый пример. Можем ли мы открыть это резервное копирование?

Повторно открыт. Я сам назначу и посмотрю, когда смогу.

Все еще воспроизводится после обновления зависимостей, включая Flask 1.0.2 и gunicorn 19.9.0. Было бы неплохо привлечь внимание кого-нибудь в Heroku по этому поводу - я слышал, что у них есть несколько преданных Python людей.
См. Последнюю фиксацию здесь: https://github.com/erjiang/gunicorn-issue/

Я создал заявку в службу поддержки на Heroku. Буду обновлять здесь, если из этого появится что-нибудь полезное.

Ты получил ответ от героку?

Мне удалось воспроизвести, используя тестовый пример на https://github.com/erjiang/gunicorn-issue (который использует gunicorn 19.9.0, Python 2.7.14, синхронизатор, --workers 4 ). Следует отметить, что вывод журнала доступа Gunicorn сообщает, что, по его мнению, он вернул HTTP 200.

Обновление до Python 3.7.3 + gunicorn master и уменьшение до --workers 1 не повлияло на воспроизводимость, однако переключение с синхронизатора на gevent привело к тому, что ошибка возникла реже (хотя это все еще происходило). Использование --log-level debug не выявило ничего существенного (единственным дополнительным выводом во время запроса была строка [DEBUG] POST /test1 ).

Затем я попробовал --spew , но проблема больше не воспроизводилась. Это заставило меня попробовать добавить time.sleep(1) до того , как resp.close() здесь , которые так же предотвратить проблему.

Таким образом, похоже, что причина в том, что буфер отправки сокета может быть не пустым во время close() , что может привести к потере ответа:

Примечание. close() освобождает ресурс, связанный с соединением, но не обязательно немедленно закрывает соединение. Если вы хотите своевременно закрыть соединение, вызовите shutdown() перед close() .

(См. Https://docs.python.org/3/library/socket.html#socket.socket.close)

Добавление sock.shutdown(socket.SHUT_RDWR) ( документы ) до sock.close() здесь решен вопрос для меня. Альтернативным исправлением, возможно, могло бы быть использование SO_LINGER , хотя, судя по тому, что я читал, здесь есть компромиссы.

Документы по этой теме найти сложно, но я обнаружил:
https://stackoverflow.com/questions/8874021/close-socket-directly-after-send-unsafe
https://blog.netherlabs.nl/articles/2009/01/18/the-ultimate-so_linger-page-or-why-is-my-tcp-not-reliable

Надеюсь, это поможет :-)

Полный STR:

  1. Создайте бесплатную учетную запись Heroku на https://signup.heroku.com
  2. Установите Heroku CLI (см. Https://devcenter.heroku.com/articles/heroku-cli)
  3. Войдите в интерфейс командной строки, используя heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (это создает бесплатное приложение Heroku со случайно сгенерированным именем и настраивает удаленный git с именем heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (не работает> 75% случаев)
  8. По завершении запустите heroku destroy чтобы удалить приложение.

@tilgovi Похоже, @edmorley представил правдоподобное объяснение того, что не так. Сможете ли вы взглянуть и увидеть, что такое правильное исправление? Я также мог бы отправить PR, чтобы добавить sock.shutdown() но я не знаю достаточно, чтобы сказать, правильное ли это исправление или это отрицательно повлияет на другие ситуации.

Здравствуйте, я столкнулся с той же проблемой с размером ответа 503 КБ. Данные ответа представляют собой массив JSON.
Наблюдаемое поведение :

  1. Я вижу усеченное тело ответа, а http-клиент (Chrome, curl) все еще ожидает ответа.
  2. ~ 75% запросов имеют время ответа от 120 до 130 секунд. Остальные запросы разрешаются менее чем за 400 мс.
  3. Запросы с небольшим размером ответа выполняются быстро.

Это воспроизводимо на обоих:

  1. локальная установка Docker в Windows 10
  2. Запуск контейнера докеров на AWS ECS

Настройка среды выполнения
изображение meinheld-gunicorn-docker с тегом _python3.6_ с Python 3.6.7, Flask 1.0.2, flask-restplus 0.12.1, simpe Flask-caching

Конфигурация Docker : 3 процессора, ОЗУ 1024 МБ

Конфигурация Gunicorn :

  • рабочие = 2 * ЦП + 1 (рекомендуется документом)
  • Threads = 1 (такое же поведение с потоками 2 * CPU)
  • worker_class = " яйцо: meinheld # gunicorn_worker "

В https://github.com/benoitc/gunicorn/issues/2015 у кого-то еще были проблемы с зависанием meinheld worker, и использование другого типа worker решило проблему. Интересно, есть ли с этим общая проблема. @stapetro можешь попробовать другого

Привет @jamadden!
Ваше предложение устранило проблему. Нет проблем с рабочими классами _gevent_ и _gthread_. Я отошел от меня. Спасибо за быстрый ответ и помощь! :)

Полный STR:

  1. Создайте бесплатную учетную запись Heroku на https://signup.heroku.com
  2. Установите Heroku CLI (см. Https://devcenter.heroku.com/articles/heroku-cli)
  3. Войдите в интерфейс командной строки, используя heroku login
  4. git clone https://github.com/erjiang/gunicorn-issue && cd gunicorn-issue
  5. heroku create (это создает бесплатное приложение Heroku со случайно сгенерированным именем и настраивает удаленный git с именем heroku )
  6. git push heroku master
  7. curl --data "foo=bar" https://YOUR_GENERATED_APP_NAME.herokuapp.com/test1 (не работает> 75% случаев)
  8. По завершении запустите heroku destroy чтобы удалить приложение.

У меня было очень похожее поведение в моем приложении, и я обнаружил, что при использовании curl -H вместо curl --data (поскольку это запрос GET) он работает для моего приложения (Django, Gunicorn, Heruko). Я не тестировал приложение Gunicorn-issue. Думал, что это может быть кому-то полезно.

@mikkelhn Yesss. Приложение с Flask / Flask RestPlus и Gunicorn ведет себя следующим образом: ответ на запрос POST дает ошибку 503 [если полезная нагрузка> 13 КБ], тогда как ошибка не возникает , если приложение отвечает на GET. Точно такой же код!
Кто-нибудь может объяснить это очень раздражающее поведение? Переключение на официантку - единственный способ решить эту проблему? Я считаю, что модификация Gunicorn «вручную» не является жизнеспособным решением ...

Я пошел дальше и открыл PR, чтобы вызвать shutdown () перед close (). Честно говоря, немного странно, что Heroku продолжает рекомендовать Gunicorn, хотя он по умолчанию не работает на Heroku.

Если, как правильно утверждает @erijang , Heroku рекомендует Gunicorn, когда Gunicorn не подходит: какие простые и жизнеспособные альтернативы Gunicorn (и как их лучше всего настроить на Heroku)?
AFAIK, многие клиенты выбирают Heroku только потому, что он не требует глубоких знаний в архитектуре серверов и деталях конфигурации ...: |

@RinaldoNani, что ты имеешь в виду? Также о каком работнике идет речь? .

@benoitc Эта проблема затрагивает несколько типов рабочих, как указано в:
https://github.com/benoitc/gunicorn/issues/840#issuecomment -482491267

Привет @benoitc. Как я уже упоминал в предыдущем посте, мы развернули довольно простое приложение Flask / FlaskRestPlus на Heroku, внимательно следуя рекомендациям Heroku по развертыванию серверных приложений Python / Flask (которые, как я понимаю, предполагают использование синхронизации Gunicorn "веб" рабочие ).

Поведение нашего приложения отражает заголовок этой беседы.

Протестировано локально, все работает нормально, приложение без проблем доставляет 20k + JSON; но когда приложение развертывается на Heroku, проблема с ошибкой 503 становится систематической: даже при буквально полном отсутствии трафика вывод не доставляется.
Как указывали другие, журналы показывают, что на уровне HTTP все выглядит нормально (регистрируется код ответа 200).
Если полезная нагрузка меньше 13 КБ, Heroku / Gunicorn отвечает на POST, как и ожидалось.
Мы последовали идее @mikkelhn об

Мы не являемся экспертами по Gunicorn и, честно говоря, ожидали, что наш простой вариант использования может работать «из коробки».
Если у вас есть предложения, которые помогут нам, мы будем бесконечно благодарны :)

@RinaldoNani Снято в темноте ... где-нибудь в обработчике запросов попробуйте прочитать все request.data . Например:

@route('/whatever', methods=['POST'])
def whatever_handler():
    str(request.data)
    return flask.jsonify(...)

Это как-то влияет на ваши ошибки?

Я пишу это в 1:00 после того, как уже более 2 недель возился с проблемой H18 (не мог дождаться, чтобы поделиться).

Я работаю с огромным набором данных и отвечаю на записи от 18 до 20 тысяч для построения графика. H18 возникла как очень случайная ошибка. Иногда он работал бы нормально, но вызывал бы ошибку «Длина заголовка содержимого не совпадает» во всех браузерах. Я перепробовал почти все решения, обсуждаемые по этой проблеме, но безуспешно. Я попробовал две вещи, которые, наконец, сработали:

  1. Изменен запрос POST на GET.
  2. Мои данные имели значения NaN / Null, поэтому я изменил свою модель и предоставил значение по умолчанию. (Думаю, это решило проблему)
    После этого я перестал получать эту ошибку.
    Надеюсь, это может кому-то помочь!
Была ли эта страница полезной?
0 / 5 - 0 рейтинги