# теория · шаг 4 из 7
Соединение: где кончается один запрос
Коротко
HTTP ничего не знает про сеть - он живёт поверх соединения, которое кто-то установил. Установка стоит времени, поэтому соединение переиспользуют, и отсюда растёт половина настроек nginx.
- Каждый запрос - это круг по сети. Установка соединения - ещё один круг, TLS - от одного до двух сверх того.
- Соединение живёт дольше запроса: по умолчанию nginx держит его открытым 75 секунд после ответа.
- Чтобы переиспользовать соединение, обе стороны должны знать, где кончается
ответ: либо
Content-Length, либоTransfer-Encoding: chunked. - Бэкенд, ответивший без длины и закрывший соединение, не ломает клиенту keep-alivekeep-aliveДоговорённость не закрывать соединение после ответа, чтобы следующий запрос не платил за установку заново. nginx держит его открытым 75 секунд по умолчанию.: nginx перекодирует ответ в chunkedchunkedСпособ передать ответ, размер которого выяснится только по ходу: каждый кусок начинается со своей длины, а кусок нулевой длины означает конец. Второй способ - заранее известный Content-Length..
Если счёт кругов и разница Content-Length с chunked знакомы - листай до «Теперь сам».
Сначала ответь сам
Стенд с искусственной задержкой: 50 миллисекунд в одну сторону. Запрашиваем файл в 300 байт по обычному HTTP. Сколько займёт?
connect=0.051 total=0.102
Сто две миллисекунды на триста байт. Данные тут ни при чём: время ушло на два круга. Первый - установить TCP-соединениеTCP-соединениеУстановленный канал между клиентом и сервером, по которому байты приходят в том же порядке, в каком отправлены. HTTP живёт поверх него: сначала соединение, потом уже запросы. Установка стоит одного круга по сети., второй - спросить и получить ответ. Файл мог быть в десять раз меньше, разницы бы не было.
Отсюда простое следствие, которое стоит за очень многими настройками: чтобы страница открывалась быстрее, надо сокращать не байты, а круги.
На что это похоже
Звонок по телефону. Сначала набираешь номер и ждёшь, пока на том конце снимут трубку и скажут «алло» - это круг, и вопроса в нём ещё не было. Потом задаёшь вопрос и ждёшь ответ - второй круг.
Если вопросов три, у тебя два варианта. Позвонить трижды - трижды набирать и трижды слушать «алло». Или задать все три в одном разговоре, не вешая трубку. И если разговор дорогой, второй вариант выигрывает не немного, а на треть.
Именно это и делает keep-alive. А трубку, которую держат «на всякий случай», рано или поздно кладут: сидеть с открытой линией без дела тоже стоит денег.
Механизм: из чего складывается время
Правило
Соединение и запрос - разные вещи с разным сроком жизни. Запрос кончается ответом, соединение живёт дальше и ждёт следующего.
Разбор: где кончается ответ
Чтобы отправить второй запрос в то же соединение, клиент обязан понять, где кончился первый ответ. Способов ровно два.
Длина известна заранее - сервер ставит Content-Length, клиент отсчитывает
столько байт и знает, что дальше начнётся следующий ответ. Так отдаётся файл с
диска: его размер известен.
Длина заранее неизвестна - тогда Transfer-Encoding: chunked: тело идёт
кусками, у каждого в начале написан его размер, а кусок нулевой длины означает
конец. Так отдаётся всё, что генерируется на лету.
Есть и третий способ, оставшийся от старых времён: не говорить ничего и просто закрыть соединение. Он работает, но убивает переиспользование - соединения-то больше нет. Проверено на стенде: бэкенд ответил без длины и закрыл связь, а nginx клиенту отдал так:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Connection: keep-alive
nginx перекодировал ответ в chunked и сохранил соединение с клиентом. Он знает, где ответ кончился (бэкенд закрыл связь), и умеет пересказать это клиенту на нормальном языке. Плата за старый бэкенд ложится на nginx, а не на посетителя.
Разбор: где наивное правило подводит
Соединение не бессмертно. nginx держит его открытым keepalive_timeout
секунд после ответа. Умолчание - 75, и замер это подтверждает: простаивающее
соединение закрылось через 75,1 секунды. Клиент, который решит переиспользовать
его на 80-й секунде, получит обрыв и должен уметь переспросить.
keep-alive у клиента и у бэкенда - разные настройки. Соединение «браузер -
nginx» держится само; соединение «nginx - бэкенд» до недавнего времени открывалось
заново на каждый запрос, и это была самая частая причина «nginx медленный».
Начиная с 1.29.7 keepalive к бэкенду включён по умолчанию, и строка
proxy_http_version 1.1; из старых руководств больше не нужна. Подробно - в
разделе про upstream.
HTTP/1.1 отвечает строго по очереди. Даже в одном соединении второй запрос ждёт, пока закончится первый: очередь одна. Отсюда трюк с несколькими соединениями на домен в браузерах и отсюда же смысл HTTP/2Разбирается в разделе 10, глава «HTTP/2 сегодня»: Что HTTP/2 меняет на самом деле, где в одном соединении едут десятки независимых потоков. Это десятый раздел курса.
Что ломается без этого
Типовая беда - «оптимизировали картинки, а быстрее не стало». Если страница тянет сорок мелких файлов, время съедают не байты, а круги: на мобильной связи с задержкой 60 мс сорок запросов в одну очередь - это секунды ожидания, сколько файлы ни ужимай.
Вторая - самодельный клиент, который на каждый запрос открывает новое соединение и удивляется нагрузке на сервер. Каждое соединение стоит памяти, дескриптора и двух кругов по сети, а на TLS ещё и вычислений.
Зачем это в работе
Понять, куда ушло время, помогает разбивка того же curl:
curl -o /dev/null -s -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ответ=%{time_starttransfer} итого=%{time_total}\n' https://shop.local/
Числа читаются как лесенка: каждое следующее включает предыдущее. Большой разрыв
между tcp и tls - дорогое рукопожатиеРукопожатиеУ TCP это три коротких сообщения, у TLS - обмен параметрами шифрования. Пока рукопожатие не закончилось, ни один байт запроса не отправлен.; между tls и ответ - думает бэкенд;
между ответ и итого - тело едет по медленному каналу.
Теперь сам
Мобильное приложение жалуется: первый экран открывается за две секунды, хотя данных там на 40 килобайт и бэкенд отвечает за 30 миллисекунд. Приложение делает двенадцать запросов к API. Что происходит и куда смотреть?
Считай круги, а не байты. Если приложение открывает соединение на каждый запрос,
это двенадцать установок плюс двенадцать TLS-рукопожатий, и при задержке в 60 мс
получается больше двух секунд одного ожидания. Смотреть надо $upstream_response_time
(он подтвердит, что бэкенд быстрый) и число соединений с одного клиента, а чинить -
на стороне приложения: переиспользовать соединение и по возможности сложить
двенадцать запросов в меньшее число.
Главное
HTTP живёт поверх соединения, и время уходит на круги: TCP - один, TLS 1.3 - ещё
один, TLS 1.2 - два, сам запрос - последний. Соединение переживает запрос и
закрывается через keepalive_timeout, по умолчанию 75 секунд. Переиспользовать
его можно, только если понятно, где кончился ответ: Content-Length для
известной длины, chunked для генерируемой на лету. Бэкенд без длины nginx
перекодирует в chunked и соединение с клиентом сохранит. В HTTP/1.1 очередь в
соединении одна - это и есть причина появления HTTP/2.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий