Привет! Хочу рассказать историю миграции с Tarantool версии 1.5 на 1.6 в одном из наших проектов. Как вы думаете, нужно ли заниматься миграцией на новую версию, если и так все работает? Насколько легко это сделать, если у вас уже написано достаточно много кода? Как не затронуть живых пользователей? С какими трудностями можно столкнуться при таких изменениях? Какой вообще профит от переезда? Ответы на все вопросы можно найти в этой статье.
Архитектура сервиса
Речь пойдет о нашем сервисе рассылки пуш-уведомлений. На Хабре уже есть статья о рассылке сообщений «по всей базе пользователей». Это одна из частей нашего сервиса. Весь код изначально был написан на Python 2.7. Мы используем стек uwsgi, gevent, ну и, конечно же, Tarantool! Сервис рассылает уже около одного миллиарда пуш-уведомлений в сутки. С такой нагрузкой справляются два кластера по восемь серверов в каждом. Архитектура сервиса выглядит, как показано на рисунке:
Архитектура сервиса рассылки пуш-уведомлений
Рассылкой уведомлений занимаются несколько серверов из кластера, обозначенные на рисунке как Node 1, Node N. Сервис рассылок взаимодействует с облачными платформами apple push notification service и firebase cloud messaging. Каждый сервер обрабатывает HTTP-трафик от мобильных приложений. Рассылающие серверы полностью одинаковые. Если один из них выйдет из строя, то вся нагрузка автоматически распределится по остальным серверам в кластере.
Наши пользователи, их настройки и пуш-токены хранятся в Tarantool. На рисунке это Tarantool Storage. Для распределения нагрузки на чтение используются две реплики. Код сервиса рассчитан на временную недоступность мастера или реплик. Если одна из реплик не отвечает, то запрос типа select выполняется к следующей доступной реплике или мастеру. Все запросы на запись в мастер делаются через очередь Tarantool Queue. Если мастер недоступен, то очередь накапливает запросы до тех пор, пока мастер не будет готов к работе.
Долгое время у нас был один мастер с базой данных Tarantool, который использовался для хранения пуш-токенов. При 10 000 запросов в секунду на запись достаточно одного мастера. А для распределения 100 000 запросов на чтение мы используем несколько реплик. Пока один мастер справляется с записью данных, нагрузку на чтение можно распределять путем добавления новых реплик.
Архитектура сервиса изначально была рассчитана на рост нагрузки и горизонтальное масштабирование. Какое-то время мы могли легко расти горизонтально, добавляя новые серверы для рассылки уведомлений. Но можно ли расти бесконечно с такой архитектурой?
Проблема в том, что инстанс с Tarantool мастером — он всего один. Он работает на отдельном сервере и вырос до 50 Гбайт. Две реплики размещены на втором сервере. Они стали занимать 50 * 2 = 100 Гбайт. Получились достаточно тяжелые инстансы Tarantool. При рестарте они взлетают не мгновенно. Да и размер свободной памяти на сервере с репликами достиг предела, который говорит о том, что нужно что-то менять.
Шардинг базы данных
Напрашивается решение с шардингом базы данных. Берем большой инстанс Tarantool мастер и делаем несколько маленьких инстансов! Уже есть готовые решения для шардинга: раз и два. Но все они работают только с Tarantool 1.6.
Как всегда, есть еще одно «но». Почта Mail.Ru генерирует огромный трафик событий о новых письмах, прочтении писем, их удалении и т. п. Всего лишь часть этих событий необходимо доставить в мобильные приложения в виде пуш-уведомлений. Обработка этого трафика — достаточно ресурсоемкая задача. Поэтому сервис Почты Mail.Ru фильтрует ненужные события и отправляет в наш сервис лишь полезную часть трафика. Для фильтрации событий Почте Mail.Ru необходимо получать информацию об установке мобильного приложения. Для этого используются запросы в наши реплики Tarantool. То есть разделить один Tarantool на несколько уже становится сложнее. Нужно рассказать всем сторонним сервисам о том, как устроен наш шардинг. Это сильно усложнит систему, особенно когда потребуется решардинг. Разработка сильно затянется, так как нужно дорабатывать уже несколько сервисов.
Одно из возможных решений — использовать proxy, который притворится Tarantool’ом и сделает распределение запросов по нескольким шардам. В таком случае изменять сторонние сервисы не потребуется.
Итак, а что мы получим от шардинга?
- увеличатся возможности масштабирования, можно расти дальше;
- уменьшится размер Tarantool инстансов, они будут запускаться быстрее, это уменьшит время простоя системы при авариях;
- за счет шардинга мы будем распределять нагрузку от Tarantool по CPU на одном сервере более эффективно, сможем задействовать больше ядер;
- получим более эффективное использование памяти сервера, 100 Гбайт под мастер и 100 Гбайт под реплики.
В нашем сервисе применяется Tarantool 1.5. Развитие этой версии Tarantool остановлено. Ну а если мы будем делать шардинг, разрабатывать proxy, то почему не заменить старый Tarantool 1.5 на новый Tarantool 1.6?
Какие еще есть плюсы у Tarantool 1.6
Наш сервис написан на Python, и для работы с Tarantool мы используем коннектор github.com/tarantool/tarantool-python. Для Tarantool 1.5 задействуется протокол iproto, упаковка и распаковка данных в коннекторе делается на Python при помощи вызовов struct.pack / struct.unpack. Для Python-коннектора Tarantool 1.6 используется библиотека msgpack. Предварительные бенчмарки показали, что распаковка и упаковка на msgpack потребляет немного меньше процессорного времени по сравнению с iproto. Переход на 1.6 может освободить ресурсы CPU в кластере.
Немного о будущем
Apple разработала новый протокол для рассылки пуш-уведомлений в iOS-устройства. Он отличается от предыдущей версии, основан на HTTP/2, в нем реализована поддержка скрытия пуш-уведомлений. Максимальный размер отправляемых данных одного пуш-уведомления составляет 4 Кбайт (в старом протоколе — 2 Кбайт).
Для отправки уведомлений в Android-устройства мы используем сервис Google
Firebase Cloud Messaging. В нем появилась поддержка шифрования содержимого пуш-уведомлений для браузера Сhrome.
К сожалению, в Python до сих пор нет хороших библиотек для работы с HTTP/2. Также нет библиотек для поддержки шифрования уведомлений Google. А что еще хуже, нужно заставить работать существующие библиотеки с фреймворками gevent и asyncio. Это послужило поводом задуматься о трудностях поддержки нашего сервиса в будущем. Мы рассмотрели вариант использования golang. В go есть хорошая поддержка всех новых плюшек от Apple и Google. Но опять проблема: в golang нет поддержки Tarantool 1.5. Грусть, боль, тлен. Нет! Это не про нас. :)
Итак, для развития нашего сервиса необходимо решить следующие задачи:
- поддержать шардинг;
- перейти на Tarantool 1.6;
- создать proxy, который будет обрабатывать запросы от клиентов в формате протокола для Tarantool 1.5;
- перевести систему очередей на работу с Tarantool 1.6;
- обновить боевой кластер так, чтобы это не затронуло наших пользователей.
Разрабатываем proxy
Мы выбрали golang в качестве языка для proxy. Этот язык очень удобен для решения подобного класса задач. После Python непривычно кодить на компилируемом языке с проверкой типов. Отсутствие классов и исключений вызвало некоторые сомнения. Но несколько месяцев работы показали, что можно прекрасно справляться и без этих вещей. Горутины и каналы в go — это очень круто и удобно с точки зрения разработки. Такие инструменты, как benchmark-тесты, мощный профилировщик golang, golint, gofmt, очень помогают и ускоряют процесс разработки. Поддержка языка сообществом, конференции, блоги, статьи про go — все это вызывает только восхищение!
Итак, у нас появился tarantool-proxy. Он принимает соединения от клиентов, обеспечивает взаимодействие по протоколу Tarantool 1.5 и распределяет запросы по нескольким инстансам Tarantool 1.6. Нагрузку на чтение, как и раньше, можно масштабировать при помощи реплик. При внедрении нового решения мы предусмотрели возможность отката. Для этого мы модифицировали наш Python-код. Все запросы на запись мы продублировали в tarantool-proxy и дополнительно в «старый» инстанс с Tarantool 1.5. Фактически наш код не изменился, но начал работать с Tarantool 1.6 через proxy. Вы спросите: да зачем так сложно? Не будет же отката? Нет, откат был. И не один.
Несмотря на то что мы проводили нагрузочное тестирование, после первого старта tarantool-proxy потреблял слишком много CPU. Откатывали, профайлили, фиксили. После второго старта tarantool-proxy потреблял много памяти, целых 3 Гбайт. Профилировщик golang снова помог найти проблему.
Профилировщик включается достаточно просто:
import (
_ "net/http/pprof"
"net/http"
)
go http.ListenAndServe(netprofile, nil)
Запускаем снятие профиля:
go tool pprof -inuse_space tarantool-proxy http://127.0.0.1:8895/debug/pprof/heap
Визуализируем результаты профилирования:
Entering interactive mode (type "help" for commands)
(pprof) top20
1.74GB of 1.75GB total (99.38%)
Dropped 122 nodes (cum <= 0.01GB)
flat flat% sum% cum cum%
1.74GB 99.38% 99.38% 1.74GB 99.58% main.tarantool_listen.func1
0 0% 99.38% 1.75GB 99.89% runtime.goexit
(pprof) list main.tarantool_listen.func1
Total: 1.75GB
ROUTINE ======================== main.tarantool_listen.func1 in /home/work/src/tarantool-proxy/daemon.go
1.74GB 1.74GB (flat, cum) 99.58% of Total
. . 37:
. . 38: //run tarantool15 connection communicate
. . 39: go func() {
. . 40: defer conn.Close()
. . 41:
1.74GB 1.74GB 42: proxy := newProxyConnection(conn, listenNum, tntPool, schema)
. 3.50MB 43: proxy.processIproto()
. . 44: }()
. . 45: }
. . 46:}
. . 47:
. . 48:func main() {
(pprof)
Видна проблема с потреблением памяти, которую нужно исправить. Все это мы запускали на боевом сервере под нагрузкой. Кстати, есть отличная статья о профилировании в go. Она очень помогла нам найти проблемные места в коде.
После исправления всех неполадок tarantool-proxy мы еще одну неделю наблюдали за работой сервиса по новой схеме. И потом окончательно отказались от Tarantool 1.5, убрали все запросы в него из Python-кода.
Как сделать миграцию данных с Tarantool 1.5 в 1.6?
Для миграции данных с 1.5 на 1.6 все уже готово из коробки github.com/tarantool/migrate. Берем снапшот 1.5 и заливаем его в 1.6 инстансы. Для шардинга удаляем ненужные данные. Немного терпения — и у нас появился новый Tarantool storage. Все сторонние сервисы получили доступ к новому кластеру через tarantool-proxy.
С какими трудностями еще мы столкнулись?
Пришлось мигрировать код lua-процедур под 1.6. Но, честно говоря, это не потребовало значительных усилий. Еще одна особенность 1.6 — в нем нет запросов вида
select * from space where key in (1,2,3)
Пришлось переписать на несколько запросов в цикле вида
for key in (1,2,3):
select * from space where key = ?
Также мы взяли новую tarantool-queue, которая поддерживается разработчиками и работает с Tarantool 1.6. Были моменты, когда приходилось работать в Python как с Tarantool 1.5, так и с Tarantool 1.6 — одновременно.
Устанавливаем коннектор для 1.5 при помощи pip и переименовываем его:
pip install tarantool<0.4
mv envlibpython2.7site-packagestarantool envlibpython2.7site-packagestarantool15
Далее устанавливаем коннектор для 1.6:
pip install tarantool
В Python-коде делаем следующее:
# todo use 1.6
import tarantool15 as tarantool
tnt = tarantool.connect(host, port)
tnt.select(1, "foo", index=0)
import tarantool
tnt = tarantool.connect(host, port)
tnt.select(1, "bar", index="baz")
Таким образом, поддержка одновременно нескольких версий Tarantool в Python не требует больших усилий. Мы постепенно выбросили 1.5 инстансы с очередями и полностью перешли на Tarantool 1.6.
Обновление продакшн
Если вы думаете, что мы быстро обновили продакшн и все сразу заработало, то это далеко не так. Например, после первой попытки перейти на новую tarantool-queue с 1.6 графики со средним временем ответа наших сервисов выглядели так:
Среднее время ответа сервисов
На графиках зеленого цвета хорошо виден рост среднего время обработки HTTP-запросов для uwsgi, и не только. Но после нескольких итераций поиска причин такого роста графики нормализовались. А вот так выглядят итоговые графики Load Average и потребления CPU на боевом сервере:
Load Average
Использование CPU
Зеленые графики показывают, что мы освободили немного ресурсов нашего железа. Учитывая то, что наши нагрузки перманентно растут, мы получили небольшой запас по железу на текущей конфигурации кластера.
Подводим итоги
Мы потратили несколько месяцев работы для перехода на Tarantool 1.6. И еще около месяца на шардинг стораджа и обновление продакшн. Дорабатывать существующую систему с большой нагрузкой достаточно сложно. Наш сервис постоянно меняется. В живом проекте есть постоянные ошибки, которые требуют вмешательства разработчиков. Постоянно появляются новые продуктовые фичи-хотелки, которые также требуют изменений в уже существующем коде.
Нельзя останавливать разработку, особенно для такой долгой задачи. Всегда приходится думать о возможных вариантах отката к прежнему состоянию. И самое главное, разработку приходится внедрять небольшими итерациями.
Что дальше?
Решардинг — это один из оставшихся нерешенных вопросов. Пока он не стоит остро, у нас есть время оценить все работы и сделать прогноз, когда решардинг нам понадобится. Также мы планируем переписать часть сервиса на golang. Возможно, сервис будет потреблять еще меньше CPU. Тогда придется сделать еще одну статью. :)
Спасибо всем разработчикам, инженерам по эксплуатации Почты Mail.Ru, команде Tarantool и всем тем, кто принимал участие в миграции, помогал и вдохновлял на поддержку и развитие нашего сервиса. Это было круто!
Ссылки, использованные при написании статьи:
- Tarantool — tarantool.org
- Tarantool Queue — github.com/tarantool/queue
- Asyncio Tarantool Queue, вставай в очередь
- Профилирование и оптимизация программ на Go
Автор: Mail.Ru Group