Меня зовут Иван, я руководитель серверной разработки в Ситимобил. Сегодня я расскажу о том, что собой представляет эта самая серверная разработка, с какими проблемами мы сталкивались и как планируем развиваться.
Начало роста
Мало кто знает, что Ситимобил существует уже давно, 13 лет. Когда-то это была небольшая компания, работавшая только в Москве. Разработчиков было очень мало, и они хорошо знали, как устроена система — ведь они сами ее создавали. Рынок такси тогда еще только начинал развиваться, нагрузки были копеечные. У нас даже не было задачи обеспечивать отказоустойчивость и масштабирование.
В 2018-м компания Mail.Ru Group инвестировала в Ситимобил, и мы начали быстро расти. На переписывание платформы или хотя бы существенный рефакторинг времени не было, нужно было развивать ее функциональные возможности, чтобы догнать нашего основного конкурента, и быстро нанимать людей. Когда я пришел в компанию, бэкендом занималось всего 20 разработчиков, и почти все они были на испытательном сроке. Поэтому мы решили «срывать низко висящие фрукты»: делать простые изменения, дающие огромный результат.
На тот момент у нас был монолит на PHP и три сервиса на Go, а также одна мастер-база на MySQL, к которой обращался монолит (сервисы использовали в качестве хранилищ Redis и EasticSearch). И постепенно с ростом нагрузки тяжелые запросы системы стали замедлять работу базы.
Что с этим можно было сделать?
Сперва мы сделали очевидный шаг: поставили в production слейв. Но если на него придет много тяжелых запросов, выдержит ли он? Также было очевидно, что при обилии запросов для составления аналитических отчетов слейв начнет отставать. А сильное отставание слейвов могло негативно сказаться на работоспособности всего Ситимобила. В результате мы поставили еще один слейв для аналитиков. Его отставание никогда не приводит к проблемам на проде. Но даже этого нам показалось мало. Мы написали репликатор таблиц из MySQL в Clickhouse. И сегодня аналитика живет в своем контуре, используя стек, который больше предназначен для OLAP.
В таком виде система проработала какое-то время, потом стали появляться функции, более требовательные к железу. Запросов тоже становилось с каждой неделей всё больше и больше: не проходило и недели без нового рекорда. Кроме того, мы заложили под нашу систему мину замедленного действия. Раньше у нас была только одна точка отказа — мастер-база MySQL, но с добавлением слейва таких точек стало две: мастер и слейв. Сбой любой из этих машин привёл бы к полному отказу системы.
Чтобы защититься от этого, мы стали использовать локальный прокси для выполнения health check слейвов. Это позволило использовать много слейвов без изменения кода. Мы ввели регулярные автоматические проверки состояния каждого слейва и его общих метрик:
- la;
- отставание слейва;
- доступность порта;
- количество блокировок и т.д.
Если превышается определённый порог, система выводит слейв из нагрузки. Но при этом допускается вывод не больше половины слейвов, чтобы из-за роста нагрузки на оставшиеся не устроить даунтайм самим себе. В качестве прокси мы применили HAProxy и сразу же внесли в бэклог план по переходу на ProxySQL. Выбор был несколько странным, но наши админы уже имели хороший опыт работы с HAProxy, а проблема стояла остро и требовала скорейшего решения. Таким образом, мы создали отказоустойчивую по слейвам систему, которая достаточно легко масштабировалась. При всей своей простоте она никогда нас не подводила.
Дальнейший рост
По мере развития бизнеса мы нашли еще одно узкое место в своей системе. При изменении внешних условий — например, пошёл ливень в крупном регионе — стремительно росло количество заказов такси. В таких ситуациях водители не успевали среагировать достаточно быстро, и возникал дефицит машин. Пока заказы распределялись, они в цикле создавали нагрузку на слейвы MySQL.
Мы нашли удачное решение — Tarantool. Переписывать под него систему было тяжело, поэтому мы решили проблему иначе: с помощью инструмента mysql-tarantool-replication сделали репликацию некоторых таблиц из MySQL в Tarantool. Все запросы на чтение, которые возникали при дефиците машин, мы начали транслировать в Tarantool и с тех пор нас больше не волнуют грозы и ураганы! А проблему с точкой отказа мы решили еще проще: сразу поставили несколько реплик, к которым обращаемся с healthcheck’ами через HAProxy. Каждый экземпляр Tarantool реплицируется отдельным репликатором. В качестве приятного бонуса мы также решили проблему отстающих слейвов на этом участке кода: репликация из MySQL в Tarantool работает гораздо быстрее, чем из MySQL в MySQL.
Однако наша мастер-база по-прежнему являлась точкой отказа и не масштабировалась по операциям записи. Решать эту проблему мы стали таким образом.
Во-первых, в то время мы уже начали активно создавать новые сервисы (например антифрод, про который мои коллеги уже писали). Причем к сервисам сразу же предъявлялось требование масштабируемости хранилищ. Для Redis мы начали использовать только Redis-cluster, а для Tarantool — Vshard. Там, где мы используем MySQL, для новой логики мы стали применять Vitess. Такие базы сразу являются шардируемыми, поэтому проблем с записью почти никаких, а если вдруг возникнут, то их будет легко решить добавлением серверов. Сейчас мы используем Vitess только для некритичных сервисов и изучаем подводные камни, но, в перспективе, он будет на всех MySQL-базах.
Во-вторых, поскольку внедрять Vitess для уже имевшейся логики было тяжело и долго, мы пошли более простым, хотя и менее универсальным путем: начали распределять мастер-базу по разным серверам, таблица за таблицей. Нам очень повезло: выяснилось, что основную нагрузку на запись создают таблицы, некритичные для главной функциональности. И когда мы выносим такие таблицы, то не создаем дополнительных точек отказа бизнеса. Главным врагом для нас была сильная связанность таблиц в коде с помощью JOIN’ов (бывали JOIN’ы и по 50-60 таблиц). Их мы беспощадно выпиливали.
Теперь пора вспомнить о двух очень важных паттернах проектирования highload-систем:
- Graceful degradation. Идея в том, что критичная функциональность не должна отказывать из-за сбоев некритичной функциональности. Например, если недоступна база, в которой мы храним историю поездок, то это не повод запрещать создавать заказы, назначать их водителям и т.д. Не будь этого паттерна, с ростом количества хранилищ у нас росло бы и количество точек полного отказа.
- Circuit breaker. Этот паттерн менее известен, но тоже очень полезен. Допустим, у нас таймаутит база, хоть и не критичная, но к которой мы обращаемся по любому запросу. К чему это приведет? У пользователей всё будет тормозить (и эта функциональность всё равно не будет работать из-за graceful degradation). Более того, количество активных FPM-воркеров будет расти, возникнет угроза дефицита этого ресурса. Если какая-то база (или сервис) всё равно постоянно выдает ошибки, то нет смысла постоянно к ней обращаться. При достижении пороговой частоты ошибок мы начинаем просто сразу отдавать ошибку, которую корректно обрабатываем в течение какого-то времени, а потом опять проверяем доступность базы (или сервиса).
Итак, мы стали худо-бедно масштабироваться, но по-прежнему имелись точки отказов.
Тогда мы решили обратиться к полусинхронной репликации (и успешно её внедрили). В чём её особенность? Если при обычной асинхронной репликации уборщица в дата-центре выльет ведро воды на сервер, то последние транзакции не успеют отреплицироваться на слейвы и потеряются. А мы должны быть уверены, что в этом случае у нас не будет серьезных проблем после того, как один из слейвов станет новым мастером. В результате мы решили вообще не терять транзакции, и для этого воспользовались полусинхронной репликацией. Теперь слейвы могут отставать, но даже если сервер мастер-базы будет уничтожен, информация обо всех транзакциях сохранится хотя бы на одном слейве.
Это было первым шагом на пути к успеху. Вторым шагом стало использование утилиты orchestrator. Также мы постоянно мониторим все MySQL в системе. Если выходит из строя мастер-база, то автоматика сделает мастером самый свежий слейв (а с учетом полусинхронной репликации в нем будут все транзакции) и переключит на него всю нагрузку на запись. Так что мы теперь способны пережить историю с уборщицей и ведром воды.
Что дальше?
Когда я пришёл в Ситимобил, у нас было три сервиса и монолит. Сегодня их уже более 20. И главное, что сдерживает наш рост, — у нас до сих пор единая мастер-база. Мы героически с этим бьемся и разделяем её на отдельные базы.
Как нам развиваться дальше?
Расширять набор микросервисов. Это позволит решить многие стоящие сегодня перед нами проблемы. Например, в команде уже нет ни одного человека, который знает устройство всей системы. А поскольку из-за быстрого роста у нас не всегда актуальная документация, — да и поддерживать её очень сложно, — новичкам трудно вникать в курс дел. А если система будет состоять из многочисленных сервисов, то написать документацию по каждому из них будет несравненно проще. Да и количество кода для единовременного изучения сильно уменьшается.
Переход на Go. Я очень люблю этот язык, но всегда верил, что переписывать работающий код с языка на язык нецелесообразно. Но в последнее время опыт показал, что библиотеки PHP, даже стандартные и самые популярные, не самого высокого качества. То есть очень многие библиотеки мы патчим. Допустим, команда SRE патчила стандартную библиотеку взаимодействия с RabbitMQ: выяснилось, что не работала такая базовая функция, как time out. И чем глубже мы с командой SRE разбираемся в этих проблемах, тем очевиднее становится, что в PHP действительно мало кто думает о таймаутах, мало кто заботится о тестировании библиотек, мало кто думает о блокировках. Почему это становится для нас проблемой? Потому что решения на Go гораздо легче поддерживать.
Чем еще мне импонирует Go? Писать на нём, как ни странно, очень просто. К тому же Go позволяет легко создавать различные платформенные решения. В этом языке очень мощный набор стандартных инструментов. Если у нас по какой-то причине начинает внезапно тормозить наш бекэнд, то достаточно зайти по конкретному URL и можно посмотреть всю статистику — диаграмму выделения памяти, понять, где простаивает процесс. А в PHP сложнее выявить проблемы с производительностью
Кроме того, в Go очень хорошие линтеры — программы, которые автоматически находят за вас самые типичные ошибки. Большинство из них описано в статье «50 оттенков Go», и линтеры прекрасно их обнаруживают.
Продолжить шардирование баз. Будем переходить на Vitess на всех сервисах.
Перевод PHP-монолита на redis-cluster. В наших сервисах redis-cluster показал себя прекрасно. К сожалению, внедрить его в PHP сложнее. В монолите используются команды, которые не поддерживаются redis-cluster’ом (и это хорошо, такие команды приносят больше проблем, чем пользы).
Будем исследовать проблемы RabbitMQ. Есть мнение, что RabbitMQ — не самое надежное ПО. Мы будем изучать этот вопрос, находить и решать проблемы. Возможно, подумаем о переходе на Kafka или Tarantool.
Автор: Иван Ремень