Веб-сервис как система реального времени

в 13:08, , рубрики: DNS, ttl, Блог компании Mail.Ru Group, веб-сервис, почта mail.ru, метки: , , ,

В начале декабря в Санкт-Петербурге при партнерстве Mail.Ru Group прошел полуфинал чемпионата мира по программированию ACM ICPC. В рамках чемпионата я встречался с участниками и рассказывал о том, как сделать веб-сервис системой реального времени; а сейчас хочу поделиться своим докладом на Хабре.

Говоря о системе реального времени, мы представляем атомную станцию, самолет или нечто подобное, где от скорости реакции информационной системы зависит жизнь людей. Если в системе реального времени команда будет тормозить 10 секунд из-за сборки мусора, последствия могут быть более чем плачевными. Реакция должна быть моментальной, причем за гарантированное время.

При работе веб-сервиса, конечно, жизнь человека не зависит от того, насколько быстро он открыл письмо в почте, но требования к веб-сервису почти такие же. Еще 15 лет назад, когда пользователь кликал на ссылку, он ожидал реакции 10 секунд; для медленного интернета того времени это было нормально. Современный интернет – это широкие каналы, быстрые компьютеры. У пользователей все работает быстро, и они ждут от сервисов того же.

Когда пользователь куда-то кликает, он ожидает моментально получить реакцию на свой клик. Что такое моментально? Для человека комфортной задержкой считается время отклика порядка 200 миллисекунд, хотя на самом деле человеческий глаз различает время около 10 миллисекунд. Веб-сервис должен реагировать на действия пользователя не более чем за 200 миллисекунд — чем меньше, тем лучше.

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

Из чего складывается время обработки запроса

Время обработки запроса к веб-сервису складывается из следующих вещей:

1 Первая – это DNS-запрос. DNS-запрос, конечно, происходит не всегда, потому что клиент кэширует у себя результаты DNS-резолвинга на какое-то время. В принципе, он может кэшироваться надолго, но у DNS-записи есть такая вещь как TTL. Если оно маленькое, клиент обязан при протухании TTL делать еще один DNS запрос. Это время.

2 Дальше происходит коннект к серверу со стороны браузера. Коннект – это несколько сетевых раундтрипов, причем если это HTTPS-ный коннект, то их еще больше. Это тоже время.

3 После коннекта следует отправка запроса по сети. Браузер отправляет запрос, который обрабатывается на сервере.

4 Потом браузер получает ответ от сервера, рендерит HTML в красивое представление, и только после этого показывает его пользователю.

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

Поскольку нельзя объять необъятное, в этой статье я рассмотрю обработку на сервере и расскажу, как мы в Почте Mail.Ru добиваемся высокой скорости работы.

Что происходит на сервере

Обработка на сервере тоже состоит из нескольких последовательных действий.

Веб-сервер должен:

1 принять коннект;
2 распарсить запрос;
3 передать запрос в обработчик;
4 выполнить всю бизнес-логику запроса;
5 получить ответ из обработчика;
6 отдать ответ клиенту.

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

Давайте рассмотрим на конкретном примере чтения сообщения в Почте Mail.Ru, что же происходит внутри обработки запросов и какая там бизнес-логика.

Веб сервис как система реального времени

Веб сервис как система реального времени

Веб сервис как система реального времени

Что происходит внутри обработки readmsg? Обработчик бизнес-логики в веб-сервере должен первым делом проверить сессию и выяснить, на какой из серверов-хранилищ идти за заголовком письма. Дальше он запрашивает данные о заголовке письма, это поход на еще один сервер. После этого он запрашивает текст письма, потом парсит письмо на предмет наличия в нем XSS-ов, которые он, естественно, оттуда вырезает. Дальше идет проверка картинок на фишинг, и в конце — отправка данных во внутреннюю систему статистики для антиспама.

Таким образом, обработка запроса — это большое количество последовательных действий. Все устроено как сэндвич: один сервер обращается к другому, тот — к следующему, и мы имеем очень много действий, выполняемых на разных серверах, но строго последовательно.
Здесь перечислены только самые основные действия. Сейчас все они выполняются последовательно. Мы работаем над распараллеливанием этих процессов, но пока оптимизируем то, что есть.

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

Проверка сессии

Сессии нужно хранить в in-memory базе данных. Никакие MySQL, Postgres или Oracle я использовать для этих целей не рекомендую. Почему? Потому что в любой SQL-ной базе данных может произойти нечто, непредсказуемое по времени исполнения — например, индекс вытеснился из кэша — она обратится на диск, и вы почти никак не сможете ей в этом помешать (теоретически, конечно, сможете, но нужно всегда об этом помнить, потому что база данных все хранит на диске). Обращение на диск в нагруженной базе данных непредсказуемо по времени. Может нет-нет, да и залипнуть.

Кроме того, SQL-ная база данных общего назначения выполняет много разной лишней логики — и не потому что она глупая, а потому что она слишком общая, не заточена под вашу конкретную задачу. Что это значит в рамках проверки сессии? То, что мы пошли в какую-то базу данных и стали делать SELECT, а база данных тоже последовательно выполняет большое количество действий, которые продолжают увеличивать общее время отклика.

Получается, что в SQL хранить нельзя, только NoSQL хранилище, только в памяти, только хардкор. В Mail.Ru в качестве in-memory хранилища мы используем нашу собственную базу данных Tarantool. Он читает только из памяти, пишет в память и на диск. Запись тоже выполняется очень быстро, потому что используется только APPEND, нет random seek-ов. Соответственно в большинстве случаев ответ от Tarantool приходит за доли миллисекунды.

Хранилище почты

Теперь рассмотрим запрос к хранилищу почты. Тут от диска никуда не деться, но простор для оптимизации остается.

Во-первых, сам протокол работы с хранилищем должен иметь минимальное количество сетевых раундтрипов. В идеале один, максимум два. Потому что каждый раундтрип может залипнуть просто потому, что внутренние сети тоже не идеальны, может где-то что-то случайно тормозить. Чем больше раундтрипов, тем выше вероятность, что вы эту случайность поймаете. А если вы эту случайность словите хотя бы на одном раундтрипе из, например, 30, составляющих весь запрос — все, у вас весь запрос тормозит. Следовательно, чем меньше раундтрипов, тем ниже эта вероятность.

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

Парсинг письма

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

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

Фишинг

Поверка картинок на фишинг очень похожа на обращение к сессиям в том смысле, что антифишинговая база должна быть полностью in-memory (типа Memcached или Tarantool), и нужно делать один запрос для всех картинок в письме. Если в письме 100 картинок, не надо делать 100 запросов к мемкэшу. Даже в рамках одного коннекта 100 запросов – это 100 раундтрипов. Делаем один запрос и достаем из него все bulk-ом. Каждый раундтрип – это задержка плюс сколько-то десятых миллисекунды, даже для внутренней сети. И эта задержка, во-первых, наращивается, а во-вторых, она может где-то взять и превратиться, например, в 100 миллисекунд. Случайно. И все. Опять же, чем больше запросов, тем больше вероятность, что это произойдет.

Статистика

Отправка статистики также должна выполняться по минималистичному протоколу. В идеале по UDP, но даже если это TCP, должны быть выставлены таймауты, чтобы она работала контролируемое время, не больше чем сколько-то миллисекунд. Помимо этого, статистика не должна использовать сторонние библиотеки. Если сторонние библиотеки используются, вы должны быть уверены в них полностью.

Тут есть хороший пример: представьте, что к вам приходит парень из соседнего отдела. Он просит в ваш код, который работает в онлайне, добавить свою логику по отправке статистики, например, в отдел антиспама, и дает вам свою библиотеку. Вы наивно вставляете его код к себе, через него отправляете статистику, выкладываете на продакшн, у вас все тормозит. Почему? А потому, что эта библиотека, например, копит какую-нибудь очередь в памяти, очереди переполняются, и у нее от этого сносит крышу. Казалось бы, вы видели в коде таймаут. Оказывается, это был только таймаут на чтение, но не таймаут на коннект.

Поэтому не верьте никаким параметрам с названием timeout в сторонних библиотеках. Не верьте ничему и никому, верьте только своим глазам. Если вы отвечаете за этот кусок функционала, вы должны сами видеть весь код, который вы используете, понимать полностью, как он работает. Иногда дешевле и быстрее написать что-то свое или использовать что-то общеизвестное, что точно не может тормозить.

Выводы

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

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

Будьте уверены в производительности стороннего кода, если вы его используете.

Обкладывайте всё и вся таймаутами.

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

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

Автор: danikin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js