Преамбула: в один из дней мы решили подключить к нашему сайту CDN, для того, чтобы радовать пользователей более быстрой загрузкой страниц. После некоторых поисков выбор пал на Highwinds, т.к. они заявляли, что поддерживают весь нужный функционал и с ними удалось договориться на очень вкусную цену. После успешного перевода сайта на работу через Highwinds мы решили— а почему бы не переключить на них и наше REST API для мобильных приложений. И тут начались интересности.

Переключили 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)


  1. Tortortor
    17.12.2017 11:04
    -4

    «Мы собрали на руках карт-бланш» — когда хотелось выпендриться, но получилось только опозориться.


    1. gats
      17.12.2017 11:26
      +4

      Материал интересный, зачем так язвить?


  1. aensidhe
    17.12.2017 11:56

    В идеале бы не ссылаться на устаревший RFC (в новом, насколько я помню, плюс-минус тоже самое, но).


    1. larrabee Автор
      17.12.2017 12:14

      Спасибо, заменил ссылку на RFC 7230.


  1. mickvav
    17.12.2017 12:01
    +3

    Материал действительно интересный, но у «кард-бланш» значение несколько другое, проверьте ;).


    1. larrabee Автор
      17.12.2017 12:10
      +2

      Спасибо, заменил на более подходящее по смыслу выражение.


  1. sumanai
    17.12.2017 12:18

    Вот поэтому и дёшево. Надеюсь вы сменили провайдера CDN?


    1. larrabee Автор
      17.12.2017 12:26

      Пока еще думаем как с этим жить дальше и к кому можно уйти.


    1. tyomitch
      17.12.2017 15:56

      Дешевле разрабатывать собственный кривой сервер, чем взять открытый и бесплатный??


  1. firk
    17.12.2017 13:11

    Поддержу Highwinds в данном споре. Chunked encoding совсем не для того, как вы его используете — он для случаев, когда на стороне источника потока данных есть какая-то относительно сложная логика, данных много и заранее неизвестно, когда они закончатся. Тело запроса к cdn — определённо не тот случай.


    1. larrabee Автор
      17.12.2017 13:23
      +1

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

      Да, и это не обязательно должен быть сервер, это может быть и клиент. И в спецификации HTTP сказано, что этот функционал должен быть реализован.


      1. 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-сервера общего назначения, где может быть обоснованно нужен первый вариант), для сокращения расходов на эксплуатацию, и смогли предложить вам дешевые услуги. По-моему это с их стороны правильное действие.


        1. larrabee Автор
          17.12.2017 13:49
          +1

          Ну это всего лишь тестовый скрипт. В Android приложении, как я понимаю дело было в том, что программисты отправляли в http клиент данные через stream buffer, а он отправлял данные чанками по мере получения.


      1. mayorovp
        17.12.2017 13:45

        А вы рассматривайте CDN как не поддерживающую POST-запросы вообще. Спецификация HTTP это не запрещает :-)


        1. larrabee Автор
          17.12.2017 13:57

          К сожалению не получится. Когда подключали cdn проводили тестирование скорости и получился следующий топ (от самого быстрого к медленному, замеряли время dom ready на клиентах):
          1) Статика на основном домене через CDN
          2) Статика на основном домене без CDN
          3) Статика на отдельном домене через CDN
          4) Статика на отдельном домене без CDN
          То есть использовать CDN есть смысл только в 1 случае, когда через cdn проксируется весь трафик сайта, а статика лежит на том же домене. Работать в таком режиме без POST запросов естественно нельзя. Вообще тема достаточно большая и если интересно могу написать отдельную статью о том, как мы это замеряли и к каким выводам пришли. ;)


          1. lexore
            18.12.2017 00:08

            Очень странно, что у вас статика на отдельном домене медленнее.
            Я бы вам рекомендовал найти причину этого, т.к. обычно ситуация ровно обратная.
            Может у вас для разных доменов разные ssl сертификаты (ну и 2 https соединения установить дольше, чем одно)?


            Вообще, заводя логику через CDN, вы расставляете очень много грабель.
            А если те ребята так обращаются, с RFC…
            Вы бы узнали, например, как они обрабатывают другие заголовки, например Cache-Control.


            1. larrabee Автор
              18.12.2017 09:50

              Да, у нас есть своя специфика. Во первых сайт довольно легкий, страница весит около 500 кб, то есть она грузится достаточно быстро и дополнительный tcp+ssl хендшейк на отдельный домен тратит больше времени, чем выигрывает. Особенно если клиент в US или Австралии (сервера у нас в Европе). А если соединение по HTTP/2, то разница еще заметнее.


        1. symbix
          17.12.2017 15:40
          +2

          В этом случае CDN на POST должен отвечать 405 Method Not Allowed :-)


  1. symbix
    17.12.2017 17:14

    если бы Highwinds использовали любую Open Source реализацию HTTP сервера, например Varnish или Nginx

    Справедливости ради, nginx очень долго — до версии 1.3.9 — не поддерживал chunked encoding в запросах. Может, у них форк старого nginx.


  1. xcore78
    18.12.2017 23:09

    Один штрих для понимания картины: вы нашли проблему в фазе тестирования сервиса или в продакшене?


    1. larrabee Автор
      18.12.2017 23:23

      Нашли уже после того, как выкатили сайт через CDN (и он пока продолжает так работать, т.к. данная проблема не затрагивает браузеры), но до включения CDN для REST API (оно работает на отдельном домене).