Переключили API на тестовых девайсах на работу через CDN, проверяем: iOS работает, Android тоже вроде работает, хотя постойте. В Android приложении работают только GET и HEAD запросы, а POST, PUT и тд падают с 502. После недолгого разбирательства и сравнения трафика iOS и Android приложений выясняем, что Android отправляет заголовок «Transfer-Encoding:chunked» в запросах.
Пробуем дернуть страницу API curl'ом:
curl https://cdn.api.example.com -XPOST -d 'test=data'
Работает. А что если попробовать вот так:
curl https://cdn.api.example.com -XPOST -d 'test=data' -H 'Transfer-Encoding: chunked'
Ага, не работает, при том, что без использования CDN такие запросы отлично проходят.
В access логах нашего nginx видим, что запросы упали с кодом 400 «Bad request».
Но может быть проблема в том, что curl отправляет заголовок «Transfer-Encoding:chunked», но не формирует данные должным образом. Проверим этот вариант написав небольшой скрипт на Python, который отправляет данные чанками.
import requests
import logging
import httplib as http_client
http_client.HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
def test():
yield 'data'
yield 'test'
s = requests.Session()
data = s.post('https://cdn.api.example.com', data=test())
Скрипт висит 30 секунд (30 секунд это request write timeout в настройках CDN) и вываливается с ошибкой.
В выводе видно следующее:
send: 'POST cdn.api.example.com HTTP/1.1\r\nHost: cdn.api.example.com\r\nConnection: keep-alive\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.18.4\r\nTransfer-Encoding: chunked\r\n\r\n'
send: '4'
send: '\r\n'
send: 'data'
send: '\r\n'
send: '4'
send: '\r\n'
send: 'test'
send: '\r\n'
send: '0\r\n\r\n'
reply: 'HTTP/1.1 502 Bad Gateway\r\n'
header: Date: Mon, 11 Dec 2017 22:05:04 GMT
header: Connection: Keep-Alive
header: Accept-Ranges: bytes
header: Cache-Control: max-age=10
header: Content-Length: 0
header: X-HW: 1
Видно, что запрос корректный, после последнего чанка идет сообщение «0\r\n\r\n» нулевой длины, сообщающее web-серверу, что все чанки переданы. Но сервер CDN продолжает ждать еще чанки и через 30 секунд отваливается по таймауту.
Но еще рано сваливать всю вину на CDN. Как мы помним до нашего nginx запрос доходит, но отваливается с кодом 400, возможно ли, что виноват наш nginx? Проверим это сделав дамп трафика и выбрав в Wireshark опцию «Follow TCP Stream», чтобы видеть данные в читабельном формате:
POST / HTTP/1.1
Date: Tue, 12 Dec 2017 07:19:48 GMT
Host: cdn.api.example.com
Connection: Keep-Alive
Accept-Encoding: gzip, deflate
Accept: */*
User-Agent: python-requests/2.18.4
Transfer-Encoding: chunked
Как видно nginx получил заголовки, но POST data до него ни в каком виде не дошла и когда сервер CDN отдает клиенту 502 и разрывает соединение с nginx ему не остается ничего, кроме как записать в лог сообщение о том, что он получил невалидный запрос.
Рассмотрим последнюю возможность, может быть CDN не обязан работать с «Transfer-Encoding:chunked» и мы сами виноваты, что использовали его в приложении? Почитаем, что про это думает RFC 7230. То, что мы ищем нашлось в секциях 3.3.1 и 4.1. По стандарту использование «Transfer-Encoding:chunked» разрешено как в запросах, так и в ответах. Отдельно указывается, что это обязательная часть HTTP/1.1 и она должна поддерживаться во всех приложениях, реализующих данный стандарт.
Мы собрали все доказательства того, что проблема в неправильной работе HTTP сервера на стороне CDN. Пишем тикет в саппорт и после долгого выяснения всех деталей проблемы и общения с их инженерами получаем замечательный ответ.
We have confirmed that this is not a bug in our system and that chunked encoding in request is not working by design.После такого даже и добавить особо нечего. Отдельно хочу заметить, что проблема не возникла, если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx, а не писали свою с такими фичами «by design». Остерегайтесь подделок HTTP протокола.
Комментарии (21)
firk
17.12.2017 13:11Поддержу Highwinds в данном споре. Chunked encoding совсем не для того, как вы его используете — он для случаев, когда на стороне источника потока данных есть какая-то относительно сложная логика, данных много и заранее неизвестно, когда они закончатся. Тело запроса к cdn — определённо не тот случай.
larrabee Автор
17.12.2017 13:23+1он для случаев, когда на стороне источника потока данных есть какая-то относительно сложная логика, данных много и заранее неизвестно, когда они закончатся.
Да, и это не обязательно должен быть сервер, это может быть и клиент. И в спецификации HTTP сказано, что этот функционал должен быть реализован.firk
17.12.2017 13:44+1Да, может быть и клиент. Но (может я конечно не так понял?) в вашем случае нет множества данных которые неизвестно когда закончатся, а есть короткий запрос.
Зачем вот слать вот такое:
Transfer-Encoding: chunked\r\n\r\n
4\r\ndata\r\n
4\r\ntest\r\n
0\r\n\r\nКогда можно послать так:
Content-Length: 8\r\n\r\n
datatest.
Отдельно хочу заметить, что проблема не возникла, если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx, а не писали свою
Как раз написали свою, оптимизированную под работу в режиме CDN (а не http-сервера общего назначения, где может быть обоснованно нужен первый вариант), для сокращения расходов на эксплуатацию, и смогли предложить вам дешевые услуги. По-моему это с их стороны правильное действие.
larrabee Автор
17.12.2017 13:49+1Ну это всего лишь тестовый скрипт. В Android приложении, как я понимаю дело было в том, что программисты отправляли в http клиент данные через stream buffer, а он отправлял данные чанками по мере получения.
mayorovp
17.12.2017 13:45А вы рассматривайте CDN как не поддерживающую POST-запросы вообще. Спецификация HTTP это не запрещает :-)
larrabee Автор
17.12.2017 13:57К сожалению не получится. Когда подключали cdn проводили тестирование скорости и получился следующий топ (от самого быстрого к медленному, замеряли время dom ready на клиентах):
1) Статика на основном домене через CDN
2) Статика на основном домене без CDN
3) Статика на отдельном домене через CDN
4) Статика на отдельном домене без CDN
То есть использовать CDN есть смысл только в 1 случае, когда через cdn проксируется весь трафик сайта, а статика лежит на том же домене. Работать в таком режиме без POST запросов естественно нельзя. Вообще тема достаточно большая и если интересно могу написать отдельную статью о том, как мы это замеряли и к каким выводам пришли. ;)lexore
18.12.2017 00:08Очень странно, что у вас статика на отдельном домене медленнее.
Я бы вам рекомендовал найти причину этого, т.к. обычно ситуация ровно обратная.
Может у вас для разных доменов разные ssl сертификаты (ну и 2 https соединения установить дольше, чем одно)?
Вообще, заводя логику через CDN, вы расставляете очень много грабель.
А если те ребята так обращаются, с RFC…
Вы бы узнали, например, как они обрабатывают другие заголовки, например Cache-Control.larrabee Автор
18.12.2017 09:50Да, у нас есть своя специфика. Во первых сайт довольно легкий, страница весит около 500 кб, то есть она грузится достаточно быстро и дополнительный tcp+ssl хендшейк на отдельный домен тратит больше времени, чем выигрывает. Особенно если клиент в US или Австралии (сервера у нас в Европе). А если соединение по HTTP/2, то разница еще заметнее.
symbix
17.12.2017 17:14если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx
Справедливости ради, nginx очень долго — до версии 1.3.9 — не поддерживал chunked encoding в запросах. Может, у них форк старого nginx.
xcore78
18.12.2017 23:09Один штрих для понимания картины: вы нашли проблему в фазе тестирования сервиса или в продакшене?
larrabee Автор
18.12.2017 23:23Нашли уже после того, как выкатили сайт через CDN (и он пока продолжает так работать, т.к. данная проблема не затрагивает браузеры), но до включения CDN для REST API (оно работает на отдельном домене).
Tortortor
«Мы собрали на руках карт-бланш» — когда хотелось выпендриться, но получилось только опозориться.
gats
Материал интересный, зачем так язвить?