Всем привет! Меня зовут Алексей Кременьков, я старший разработчик в Яндекс Почте. В этой статье расскажу, как мы работаем с большим количеством шардов PostgreSQL: как создавали собственный сервис динамического шардирования Sharpei, как развивали инфраструктуру под него и как проходил переезд на облачное решение. В конце разберёмся, какие плюсы и минусы мы смогли найти в этом решении.

Сервисы Яндекс 360 используют миллионы пользователей, и с ростом аудитории растёт нагрузка на хранение и масштабирование данных. На примере Почты расскажу, как устроено хранение большого объёма метаданных: сейчас это более 700 шардов PostgreSQL, обрабатывающих около 300 тысяч запросов в секунду. 80% из них — чтение, 20% — запись. Это типичный OLTP‑нагруженный сервис, и с таким объёмом приходится быть очень изобретательными.

Как было раньше
Раньше Почта жила в Oracle. Шардирование тогда выглядело довольно просто: информация о том, в каком шарде находится пользователь, хранилась в Яндекс ID — это внутренний сервис Яндекса, который мы не контролировали. При запросе в бэкенд мы получали от него нужную информацию и передавали её в стандартный инструментарий Oracle.
Всё это работало через OCI Driver, который брал на себя вопросы отказоустойчивости и масштабируемости. Архитектура самого Oracle‑кластера при этом была вертикальной: шардов было немного, но каждый — очень большой. Переносами пользователей между шардовыми нодами занималась отдельная команда администраторов (MDB), а мы, как команда разработки, в это почти не вмешивались. Редкие миграции выполнялись вручную.
Со временем стало понятно, что нужно уходить с Oracle. Подробнее о причинах и сложностях этого процесса можно послушать в докладе Володи Бородина — он тогда был тем самым админом. А здесь сосредоточимся на том, как мы реализовали шардирование уже в PostgreSQL.
Новый сервис шардирования
Важные элементы надёжной работы с таким большим объёмом данных и таким количеством запросов — шардирование и отказоустойчивость. Шардирование — потому что в один шард всё не запихнёшь, а отказоустойчивость — потому что всегда есть риск, что часть хостов станут недоступны по той или иной причине. Мы также регулярно проводим учения и проверяем сервисы в режиме деградации одного из дата‑центров, так как при проблемах на учениях мы можем их быстро прекратить, а при реальных сбоях у нас такой возможности не будет, и надо быть готовыми.
При переезде из Oracle в PostgreSQL мы написали новый отдельный сервис шардирования — Sharpei. Рассмотрим чуть подробнее, как было всё устроено изначально.

Когда запросы прилетали в наши бэкенды, бэкенд обменивал аутентификационные данные (логин, пароль, cookie, token или что‑то подобное) через Яндекс ID на идентификатор пользователя и имя шарда.
Затем бэкенд скармливал это имя шарда в оракловый драйвер OCCI, дальше внутри этого драйвера была реализована вся логика отказоустойчивости. В частности, в специальном файле /etc/tnsnames.ora были записаны имя шарда и список хостов, которые в этот шард входят, его обслуживают. Oracle сам решал, кто из них мастер, кто реплика, кто жив, кто мёртв и так далее В итоге шардирование было реализовано средствами внешнего сервиса, а отказоустойчивость — средствами драйвера Oracle.
Стоит отметить, что в Oracle масштабирование строилось в основном вертикально — так диктовали особенности лицензирования и оплаты. Поэтому набор шардов почти не менялся, а обновлять описание в tnsnames.ora приходилось крайне редко. Пользователи тоже мигрировали между шардами нечасто, и данные о том, где находится пользователь, в Яндекс ID обновлялись лишь изредка.
При переезде же в PostgreSQL после различных предварительных экспериментов мы пришли к другой схеме хранения. Мы стали делать базы намного меньше, но в большем количестве, плюс по две реплики в каждом шарде. Это позволило нам заворачивать читающую нагрузку на реплики. То есть в Oracle всё обслуживалось с мастера плюс была одна реплика для обеспечения надёжности, а в PostgreSQL было три хоста поменьше вместо двух, но чтение заворачивалось в реплики. В случае с Oracle мы масштабировались вертикально, в случае с PostgreSQL масштабируемся горизонтально. Это повысило надёжность, так как при отключении дата‑центров (ДЦ) на учениях у нас оставалось в живых два хоста и при выпадении одного из них шард всё ещё оставался доступным. Ну и при обычной работе выпадение всех трёх хостов в разных ДЦ, обслуживающих один шард, — намного более редкая ситуация, чем одновременное выпадение двух хостов.
В связи с такими изменениями схемы хранения изменение набора шардов, как и перемещение пользователей между шардами, стало более частой ситуацией. К тому же изначально основная функциональность бэкенда Почты была реализована в виде одного большого сервиса‑монолита на С++. Но потом появились дополнительные сервисы на других языках программирования, мы переходили на микросервисную архитектуру и планировали со временем распилить монолит на набор сервисов поменьше, за которыми было бы проще следить и которыми было бы легче управлять. Поэтому даже изначально доставлять актуальное описание всех шардов tnsnames.ora до всех бэкендов было непросто, благо tnsnames.ora менялся редко, да и сервисов было немного. Но в связи со всеми планируемыми изменениями это уже представляло реальную проблему.
Кроме того, из‑за значительно более частого перемещения пользователей между шардами ожидался серьёзный рост модифицирующей нагрузки в Яндекс ID. Учитывая, что это сторонний относительно нас сервис, вносить в него какие‑либо изменения специально для Почты было затруднительно.
Плюс ко всему в рамках эксплуатации ещё в Oracle возникал ряд проблем и пожеланий, которые было достаточно сложно решить и реализовать в рамках текущей архитектуры. Например, разделение пользователей по шардам по различным продуктовым принципам, таким как геораспределённость, активность и прочее.
В итоге мы решили уйти от зависимости от Яндекс ID плюс файла с информацией о шардах и создать собственный сервис шардирования. При этом Яндекс ID остался в цепочке — мы по‑прежнему аутентифицируемся в нём и получаем User ID, но всё остальное теперь делает наш собственный новый сервис шардирования Sharpei, который в отдельной базе ShardDB хранит важную для нас информацию:
о пользователях;
об их распределении по шардам;
о самих шардах;
об их хостах;
о дата‑центрах, где расположены эти хосты;
и прочую необходимую для эффективного шардирования информацию.

Итого:
Sharpei вытаскивает необходимую информацию о шардировании пользователя, формирует строку подключения в зависимости от потребностей: например, нужно подключение к базе для изменений или будут только читающие запросы. И отдает её другим компонентам, при этом любой сервис, написанный на любом языке, получает возможность единообразно подключаться к БД.
Sharpei выбирает подходящий шард для новых пользователей, учитывая различную продуктовую логику.
Первая версия шардирования
Главным требованием при разработке новой архитектуры шардирования для нас стала отказоустойчивость. Для Почты стандартное SLA предполагает 99,99% доступности, и это требование оставалось для нас базовым.
Второй критерий — способность выдерживать высокую нагрузку: в наши базы идёт нагрузка больше 300 тысяч RPS.
Третий критерий — минимальная задержка при получении информации о шардировании: не более 5 мс на 95-м перцентиле.
Четвёртый критерий — гибкость и расширяемость системы.
В совокупности это означало, что сервис должен быть фактически прозрачным для клиента: работать быстро, надёжно, «из‑под капота» и без дополнительных накладных расходов на стороне потребителя.
На первом этапе мы пришли к следующей схеме нашего сервиса шардирования:

Отказоустойчивость и высокая нагрузка
Основная проблема здесь в том, что мы должны обеспечить высокую отказоустойчивость нашего сервиса шардирования при очень высоком рейте запросов в наши шарды, а для практически каждого такого такого запроса мы должны определить, в каком шарде лежат данные пользователя. При этом сами пользователи создаются и переносятся между шардами относительно редко, по сравнению с количеством запросов на получение информации о том, где лежит пользователь: сейчас у нас рейт запросов на создание новых пользователей порядка 10 RPS, в то время как общее количество запросов о шарде пользователя достигает 300 тысяч.
Таким образом, большинство запросов в сервис шардирования Sharpei, а следовательно, и БД шардирования ShardDB — читающие. Достаточно логично было поднять в ShardDB большое количество читающих реплик, чтобы обеспечить надёжность и возможность выдерживать большую нагрузку. Кроме того, мы поселили инстансы нашего сервиса шардирования на тех же хостах, что и ShardDB. Соответственно, читающие запросы в сервис за информацией о пользователе приходят на балансировщик и равномерно распределяются по разным живым инстансам Sharpei (у сервиса есть специальная handler, которая говорит о его работоспособности и которую проверяет балансер).
Каждый Sharpei для читающих запросов ходит в БД локально, в реплику, развёрнутую на том же хосте. Если же приходит модифицирующий запрос (перемещение или регистрация пользователя), то каждый Sharpei дополнительно знает, где расположен мастер, и идёт в него. В итоге нагрузка равномерно распределяется по всем инстансам сервиса и по всем репликам ShardDB. Так как наш сервис шардирования стоит на критическом пути работы большинства наших сервисов, то количество инстансов Sharpei и реплик ShardDB у нас развёрнуто с кратным запасом. В результате мы можем выдерживать большую нагрузку и защищены от выхода из строя отдельных хостов БД.
Минимизация задержки при получении информации о шардировании
Практически для любого запроса в базу надо получить из сервиса шардирования информацию о шарде пользователя (строку подключения к базе данных, где лежат данные пользователя), при этом для одного пользовательского запроса может быть много запросов в базу данных. Например, при входе пользователя в Почту надо запросить информацию о папках пользователя, о его метках, настройках, о списке писем в дефолтной папке и прочую информацию. Таким образом, время запроса информации из сервиса шардирования становится очень критичным, и его желательно максимально оптимизировать, чтобы избежать тормозов в пользовательских интерфейсах.
У нас есть дата‑центры в разных регионах, и задержки между, например, Москвой и Казахстаном достигают ~50 мс. В рамках нашей архитектуры мы стараемся, чтобы все запросы максимально быстро выполнялись внутри одного ДЦ. Запросы в Sharpei приходят сначала в инстанс балансера в некотором ДЦ. Затем балансер перенаправляет запрос в конкретный инстанс Sharpei, стараясь выбрать инстанс в том же ДЦ, если в нём есть достаточное количество живых инстансов, а Sharpei идёт для основных читающих запросов в локальную реплику БД. В результате в большинстве случаев у нас все запросы между разными сервисами и БД работают в рамках одного ДЦ, а местами в рамках одного хоста.
Далее для минимизации времени ответа мы задумались о кешировании данных о размещении пользователей по шардам. Но возникли вопросы по поддержанию актуальности кеша (например, после трансфера между шардами), его эффективности, учитывая то, что запрос может прилететь на разные инстансы Sharpei, а также вероятность появления дополнительных точек отказа и багов. Во время экспериментов мы увидели, что, как правило, все данные по активным пользователям помещаются в кеш самой базы данных и в результате кеш в самом сервисе особо пользы не приносит, а только добавляет избыточную сложность. По итогу оказалось что главное — правильно настроить параметры БД, чтобы основные данные помещались в кеш.
Дальнейшие оптимизации и реализация различных бизнес-сценариев
Как я писал ранее, мы решили не использовать кеш с информацией о распределении пользователей по шардам. Но если посмотреть на схему с архитектурой нашего сервиса шардирования, то видно, что мы всё‑таки используем local cache. Для чего же он нам понадобился?
Схему архитектуры Sharpei я уже приводил. Теперь приведу схему непосредственно отдельного шарда, где лежат данные конкретного пользователя.

То есть у типичного шарда структура состоит из трёх хостов (исходя из соображений надёжности и необходимых ресурсов), живущих в разных дата‑центрах. На тех же хостах живут и разные админ‑утилиты для обслуживания шардов: кроны для асинхронной чистки устаревших данных, утилиты для сбора метрик и тому подобное
При запросах пользователя мы должны подключиться либо к мастеру для пишущих запросов, либо к реплике без лага, либо к произвольному живому хосту, в зависимости от конкретной задачи. При этом не хотелось бы при каждом запросе к БД для поиска подходящего хоста перебирать все хосты и подключаться к ним, проверять, живой хост или нет, мастер ли это, есть ли лаг на реплике. Плюс на тот момент в драйверах PostgreSQL в принципе не было подходящей функциональности. Ну и конечных клиентов Sharpei хотелось максимально избавить от боли выбора подходящего способа подключения.
В итоге мы в Sharpei сделали кеш данных о шардах. Раз в секунду мы опрашиваем все хосты всех шардов и собираем информацию о том, какой хост является мастером, какие лаги на репликах, какие хосты живые, после чего сохраняем всю эту информацию в кеше. Таким образом, клиенту Sharpei только надо передать ID пользователя и желаемый способ подключения: нужен мастер или реплика без лага либо подойдёт любой хост. В ответ Sharpei запросит из базы информацию о том, в каком шарде пользователь, возьмёт информацию о шарде из кеша и сформирует подходящую строку подключения, учитывая пожелания пользователя.
Далее, как я писал, важно, чтобы все запросы максимально быстро ходили внутри одного ДЦ. Для работы с Sharpei мы этого добились. Но хотелось бы, чтобы и запросы непосредственно в шарды с данными пользователя делались по возможности в ДЦ, расположенных поближе к пользователю. Кроме того, в ряде случаев есть требования, чтобы данные пользователя хранились локально, то есть в дата‑центрах его страны. Плюс есть ещё ряд случаев, когда мы хотим селить пользователя в строго определённые шарды. Например, ящики разработчиков Почты мы поселили в один специальный шард, чтобы некоторые потенциально опасные новые фичи или большие изменения можно было выкатить на этот шард и предварительно протестировать на себе.
Для решения этих проблем в базе ShardDB мы для шардов сохраняем их типы, а для хостов шардов — ДЦ, где они расположены. При регистрации новых пользователей мы смотрим на страну пользователя и другие его параметры и выбираем подходящий шард. Понятно, что пользователь в реальности может использовать почту из другой страны, а не из места регистрации, но в большинстве случаев такое распределение по шардам позволяет уменьшить задержки при работе с почтой.
Следующая проблема, которую мы решали, — наличие целого зоопарка различных по производительности хостов при переезде из Oracle в PostgreSQL. На момент переезда у нас не было достаточного количества единообразного оборудования под новые базы в PostgreSQL. Поэтому для оптимизации использования железа мы дополнительно разметили наши шарды по типу используемого железа: холодные шарды с только HDD‑хранилищем, тёплые — с SSD и горячие — тоже с SSD‑хранилищем, но их меньше на одно процессорное ядро, потому что процессор там активнее используется. Соответственно, на горячие шарды мы селим наиболее активных пользователей, а тех, кто перестал использовать почту, переселяем на холодные шарды.
Первые проблемы и их решение
На ранних этапах эксплуатации мы столкнулись с дисбалансом: на нагруженных шардах одна реплика брала на себя почти всю нагрузку, в то время как вторая простаивала. Причина оказалась в упрощённой логике первой версии: при запросе мы выбирали первый доступный хост. Решение оказалось простым: добавили рандомизацию среди подходящих реплик. После этого нагрузка распределилась равномерно, и мы перестали упираться в ресурсы.
Вторая серьёзная проблема возникла во время массовой миграции пользователей в PostgreSQL. После отладки процессов мы включили перенос в полную силу, и нагрузка на ShardDB резко возросла. В обычном режиме пишущие запросы не превышали 10 RPS, но во время пиков миграции значение выросло в сотни и тысячи раз. Мастер упёрся в пропускную способность сети: все изменения приходилось реплицировать сразу на все хосты.
Чтобы справиться с нагрузкой, мы перешли от схемы «один мастер + n реплик» к каскадной репликации. В каждом из четырёх дата‑центров появился основной хост, один из которых назначался мастером. За каждым из них закреплялся собственный набор реплик. Такая архитектура позволила равномерно распределить сетевой трафик: задержки и overhead больше не концентрировались в одной точке, а распределялись по всем ДЦ.

В результате мы в разы снизили сетевую нагрузку во время массовых миграций и перестали испытывать проблемы.
Регистрация пользователей
Теперь пару слов о том, как у нас устроена регистрация пользователей. При регистрации нового пользователя нам формально нужно одновременно и транзакционно создать запись в трёх местах: в Яндекс ID (внешнем для нас сервисе), в ShardDB и MetaDB.
Для начала мы решили немного упростить себе жизнь и сделать основную схему регистрации «ленивой»: при создании аккаунта в Яндекс ID Почта ещё ничего не знает о пользователе. Первая инициализация происходит при первом обращении в Почту. При запросе в Sharpei, если в базе ShardDB нет информации о пользователе, то мы идём в Яндекс ID, проверяя, что пользователь зарегистрировал Почту, и если так и есть, то переходим к транзакционному созданию записи в двух базах: ShardDB и MetaDB.
Преимущество схемы в том, что мы не создаём данные для «мёртвых душ» — аккаунтов, которые никогда не открыли интерфейс. Это экономит ресурсы и место.
Теоретически можно было бы решить задачу асинхронными тасками, которые ретраят создание записей до успеха. Но мы пошли другим путём и использовали двухфазный коммит с PREPARE TRANSACTION — это встроенный механизм PostgreSQL для атомарных транзакций, охватывающих несколько баз.

Схема выглядела так: мы подготавливаем транзакции в обеих базах через PREPARE TRANSACTION. Если обе операции успешны, фиксируем сначала MetaDB, затем ShardDB. После PREPARE TRANSACTION транзакция перестаёт быть связана с сессией, а её состояние сохраняется на диске — высокая гарантия, что коммит завершится даже при аварийной остановке. Механизм показал себя надёжным, но золотой пулей не стал: PostgreSQL не решает всё сам за тебя, пришлось дополнять систему дополнительными костылями.
Во‑первых, у нас много микросервисов: при заходе пользователя в Почту в Sharpei может прилететь множество одновременных запросов и будет множество попыток одновременной регистрации пользователя в наших базах. Пришлось добавить явную блокировку в базе во время регистрации по ID пользователя через pg_try_advisory_xact_lock.
Во‑вторых, в результате у нас получилось по несколько запросов в каждую из баз. Эти запросы могут закончиться с разными результатами, и надо все варианты правильно обработать, где‑то поретраить, где‑то сделать rollback, если регистрация невозможна. То есть получилась достаточно запутанная схема, где надо было ничего не пропустить.
В‑третьих, всё равно оставалась небольшая вероятность, что что‑то пойдёт не так и в одной из баз останется незавершённая PREPARE TRANSACTION, например если как раз перед последним коммитом умрёт по каким‑либо причинам Sharpei. При этом PostgreSQL плохо относится к долгим незавершённым PREPARE TRANSACTION, и у него даже есть специальные лимиты на их количество.
Кроме того, такие незавершённые двухфазные коммиты будут блокировать регистрацию пользователя. Для очистки «висячих» транзакций мы сделали специальную джобу. Она каждую минуту проверяет PREPARE TRANSACTION старше минуты: если изменения уже применены в параллельной базе — коммитим, иначе откатываем. Так мы гарантируем, что зависшие регистрации не блокируют работу.
Трансферы пользователей между шардами
Так как у нас теперь много шардов, то регулярно на некоторых из них заканчивается место или на один шард попадает много сверхактивных пользователей, которые генерируют повышенную нагрузку. В таких случаях нам надо перенести пользователей из проблемных шардов в новые. Есть и ряд других ситуаций, когда нам необходимы переносы между шардами по продуктовым или техническим причинам.
Теперь подробнее про сам процесс переноса. Можно было пойти путём, похожим на то, как устроена регистрация, то есть использовать двухфазные коммиты. Но, как упоминалось ранее, PostgreSQL не любит долгих PREPARE TRANSACTION, а перенос пользователей между шардами — значительно более долгий процесс, по сравнению с регистрацией нового пользователя. В итоге мы решили использовать другую схему:
Сначала мы ставим lock на изменения в исходном шарде — пользователь становится read‑only.При этом получить все письма, которые ему посылались в это время, он сможет, так как они на время сложатся в специальную очередь, откуда потом дообработаются, когда пользователь снова станет доступен на запись.
Затем копируем данные на новый шард.
Обновляем в ShardDB информацию о том, где теперь хранится пользователь. После этого он считается перенесённым и доступен в новом шарде, в том числе и для модификаций.
В старом шарде ставим специальный флаг, что пользователь оттуда уже переехал.
Снимаем lock.
Асинхронная задача позже очищает остаточные данные из старого шарда.
Зачем нужен флаг в старом шарде? Тут несколько причин. Во‑первых, удаление старых данных пользователя может занять довольно продолжительное время, при этом никак не влияет на работоспособность пользователя после переноса, и хотелось исключить этот процесс из самого трансфера. Во‑вторых, так мы можем запускать чистку, например, ночью, когда нагрузка значительно меньше. Ну а в‑третьих, особенно поначалу, мы не хотели удалять данные пользователя сразу — вдруг что‑то пойдёт не так, и тогда в течение некоторого времени можно откатить изменения.
Эта схема работы показала себя хорошо. Из недостатков — то, что пользователь уходит в RO (read‑only) на некоторое время. Пользователь с миллионом писем трансферится (и находится в RO) в пределах получаса. А пользователей, у которых больше миллиона писем, у нас очень небольшой процент. Плюс мы, как правило, запускаем трансферы по ночам, когда влияние на пользователей минимально.
Доработки сервиса при переезде в облачные решения
Первоначально все шарды, база Sharpei и сам Sharpei разворачивались на обычном физическом железе. А поддержкой и разработкой сервиса занимались две команды. Одна из них разрабатывала непосредственно сервис Sharpei и вспомогательные сервисы вроде трансфера пользователей между шардами. И была отдельная команда инфраструктуры, куда входили в том числе и админы БД. Они разворачивали и конфигурировали базы данных, разворачивали Sharpei, следили за здоровьем баз. Но после переезда из Oracle и настройки всей новой инфраструктуры у них появилось свободное время плюс большой опыт и экспертность в БД, умении их разворачивать, настраивать и следить за здоровьем. В итоге они со временем запилили облачную платформу для БД, за которую и стали отвечать.
И настало время переезда в облака. По договорённости с облачной командой они поддерживали ещё некоторое время жизнь нашего сервиса на железе, но мы пообещали, что приложим максимум усилий, чтобы побыстрее переехать в облака.
Переезд шардов в облака
Первым делом мы начали переносить именно шарды: их было на порядок больше, чем хостов с Sharpei, и поддержка этой инфраструктуры на железе отнимала у облачной команды больше всего ресурсов.
Сам перенос данных проблем не вызвал — он отличался только временем. У нас уже был сервис трансферов, который сначала использовался при миграции с Oracle в PostgreSQL, а позже для балансировки нагрузки между шардами. Для него разницы между железными и облачными шардами не существовало.
Сложности возникли с сопутствующей инфраструктурой. На физических хостах вместе с базой работали скрипты: они собирали метрики, выполняли крон‑задачи по чистке и другие фоновые операции. Часть этой функциональности, например, типовые метрики вроде занятого места, нагрузки, ошибок, перестроения индексов, переехала в облачную платформу. Но специфичные для наших сервисов метрики и задачи переехать не могли. Кроме того, в облаке хосты оказались изолированы, и мы больше не могли деплоить на них свои скрипты напрямую.
Мы решили переписать всё в отдельные сервисы: сделали сервис для сбора метрик и сервис для регулярных фоновых задач, которые обрабатывают все шарды. Вся нужная информация о шардах уже хранилась в ShardDB, и мы просто использовали её как источник правды.
Раньше создание и настройку баз брала на себя инфраструктурная команда со своими скриптами. После переезда в облако нам пришлось разработать собственные утилиты, которые используют API‑платформы: они создают новый шард, добавляют пользователей, накатывают все миграции и регистрируют его в ShardDB с нужными параметрами.
Мы также автоматизировали трансферы. Раньше коллеги вручную определяли перегруженные шарды, собирали кандидатов на перенос и генерировали скриптами таски. Теперь этим занимаются наши сервисы мониторинга и фоновых операций. В ShardDB мы добавили флаги «разрешена регистрация» и «разрешён трансфер», на которые ориентируются сервисы. Если загрузка превышает пороги (например, 75% — закрываем шард для новых пользователей, 85% — запускаем автогенерацию тасок на перенос), система реагирует автоматически.
Для оптимизации мы применили эвристику: переносим «средних» пользователей — не тех, у кого слишком мало писем (мало эффекта), и не тех, у кого их слишком много (слишком долгий read‑only). Джоба по генерации тасок запускается ночью, чтобы все трансферы прошли без заметного влияния на пользователей.
Отдельная джоба закрытия шардов от новых регистраций и трансферов работает каждые полчаса — этого хватает, чтобы не перегружать систему. В результате рутинные переносы полностью ушли в автоматизацию: вручную нам остаётся только запускать утилиту для создания новых шардов, когда это нужно.
Переезд ShardDB и Sharpei в облака
Вторая, более сложная часть переезда в облако — это миграция связки Sharpei + ShardDB. Тут задача уже не просто в переносе данных: нужно было сделать это без заметного даунтайма и без серьёзной просадки в производительности, особенно при отделении базы от сервиса. Как мы это решали?
Мы начали с переноса самого сервиса Sharpei с хостов ShardDB: как я уже писал, подселить свои сервисы на хосты базы данных, поднятые в облаке, мы не могли. И одной из главных проблем, возникших при этом, стала потеря производительности из‑за того, что перестали работать локальные запросы между Sharpei и ShardDB для читающих запросов и сменяться на кросс‑ДЦ‑запрос. И для решения этой проблемы мы разработали компонент VANGA.
Идея простая:
Мы разметили все хосты ShardDB дата‑центрами этих хостов.
Каждый инстанс Sharpei знает о том, в каком ДЦ он запущен.
В Sharpei мы сделали кеш, похожий на тот, что используется для шардов БД. В нём мы храним и обновляем раз в секунду информацию о всех хостах ShardDB, об их живости, лагах репликации, об их роли (мастер/реплика), о том, в каком ДЦ расположен.
При необходимости похода из Sharpei в реплику ShardDB мы проверяем, что есть достаточное количество живых реплик в нужном ДЦ (чтобы не сложить нагрузкой весь ДЦ, если в нём умерло несколько хостов), и если их достаточно, то выбираем произвольный хост в нужном ДЦ. Если же достаточного количества живых хостов ShardDB в нужном ДЦ нет, то мы выбираем произвольный хост из всего набора реплик, без привязки к ДЦ.

В итоге походы в Sharpei в большинстве случаев бывают внутри одного ДЦ, нагрузка равномерно распределена по всем репликам и нет затрат на поиск подходящей живой реплики с подключением к каждой. В результате заметной потери производительности мы не получили. Кроме того, система стала более гибкой и масштабируемой. Нам уже не надо держать одинаковое количество инстансов Sharpei и хостов ShardDB.
После разделения Sharpei и ShardDB настала пора перевезти в облака саму базу ShardDB. Так как от Sharpei зависят все остальные наши сервисы, работающие с нашей шардированной базой, то главной целью было перевезти базу без даунтайма и c минимальным временем режима read‑only. Для этого мы пошли следующим путём:
Создаём новый кластер в облачной платформе.
Настраиваем репликацию. Для копирования данных и синхронизации мы используем Data Transfer — это сервис логической репликации внутри Yandex Cloud. Его преимущество — минимальный даунтайм: можно читать в старом кластере и писать в него, пока идёт репликация.
Минимальный read‑only и переключение. После завершения копирования и перехода к логической репликации изменений мы временно переводим старый кластер в read‑only и дожидаемся, пока до нового кластера реплицируются оставшиеся изменения и пропадет лаг репликации. После этого мы полностью переключаем приложение на новый кластер в облаке.
Весь процесс у нас прошёл без проблем.
Развитие сервиса
Стоит сказать, что схема ShardDB с каскадной репликацией требовала заметных усилий в поддержке. И на этапе миграции ShardDB в облака мы решили отказаться от этой схемы. Мы стали использовать более мощные инстансы хостов под БД, что позволило снизить их количество. Тут сыграло свою роль как то, что железо стало мощнее, так и то, что мы отселили Sharpei с хостов БД, которые тоже съедали значительную часть общих ресурсов. Кроме того, пропускная способность сети между хостами баз тоже выросла. Это в совокупности нам позволило упростить схему и отказаться от каскадной репликации.
Ранее я упоминал, что одна из проблем текущей схемы трансферов — это проблема с продолжительным read‑only при переносе пользователей с большим количеством писем. Частично мы снизили критичность проблемы тем, что стараемся избегать переноса таких пользователей и наша различная автоматика их старается не трогать. Но бывают случаи, когда обойтись без переноса мы не можем, например когда нам надо по той или иной причине вынести всех пользователей из шарда. И в новых версиях PostgreSQL появилась функциональность, которая смогла помочь нам уменьшить период read‑only, — это логическая репликация. Начиная с 15 версии логическая репликация поддерживает ещё и фильтрацию строк по некоторому условию.
Что же это за механизм? Логическая репликация начинается с копирования снимка данных в базе данных публикации. По завершении этой операции изменения на стороне публикации передаются подписчику в реальном времени, когда они происходят. Подписчик применяет изменения в том же порядке, в каком они вносятся на узле публикации, так что для публикаций в рамках одной подписки гарантируется транзакционная целостность. По умолчанию подписчикам передаются все данные из всех опубликованных таблиц. Однако множество реплицируемых данных можно ограничить, используя фильтр строк.
Пока мы не стали внедрять это решение в основной сервис. Сначала тестируем его на новом проекте с более простой архитектурой и небольшим объёмом данных. Первые результаты хорошие: механизм сокращает время read‑only, сейчас идут отладка и оптимизация. После обкатки мы планируем внедрить его и в Почту.
Использование подобного механизма описывалось ранее, когда речь шла о миграции ShardDB в облако. Только там это делалось разово, данные переносились все, и для удобства всё делалось через специальный сервис Data Transfer. Теперь же просто необходимо сделать аналогичные операции, но напрямую в сервисе трансфера, плюс нужно добавить фильтрацию по ID пользователя.
Итоги
К чему же мы пришли на текущий момент:
Получили высокоотказоустойчивый сервис — четыре девятки по доступности.
Производительность: время ответа Sharpei — менее 5 мс на 97-м перцентиле.
Сервис прекрасно масштабируется: сейчас поддерживается 700+ шардов, и это число продолжает расти.
-
Максимально автоматизировали все процессы и снизили нагрузку на инфраструктурную поддержку сервиса:
все сервисы в облаках и деплоятся по кнопке;
базы тоже в облаках, новые шарды добавляются путём простого запуска специальной утилиты без каких‑то сложных настроек;
пользователи между шардами для балансировки нагрузки перемещаются полностью автоматически.
Кто пользуется?
Яндекс Почта — главный и самый старый клиент, исторически развивавший сервис.
Яндекс Диск — думаю, в представлении не нуждается.
Data Sync — внутренний сервис Яндекс 360, которым пользуются и другие сервисы Яндекса. Он обеспечивает синхронизацию данных между приложениями.

Сервис продолжает активно развиваться. Конечно, не всё идеально. Есть вещи, которые работают, но могли бы работать лучше. Вот ключевые узкие места:
Read‑only при трансфере
Во время трансфера пользователь попадает в read‑only. Да, ненадолго — в среднем на пару минут. Есть подходы, позволяющие уменьшить это время почти до нуля, но пока что это приемлемый компромисс. Если вы ни разу не видели плашку «Системные работы» в Яндекс Почте — значит, всё прошло гладко. И мы работаем над внедрением механизмов, которые улучшат этот момент.Решение не совсем универсальное
Пока каждый новый потребитель, по сути, делает форк: отдельная регистрация, своя конфигурация, кастомная логика. Пока нет развёртывания сервиса по кнопке для нового потребителя. Но мы работаем над улучшениями: сейчас пытаемся сделать максимально общие конфиги, которые требуют минимальных настроек под конкретного клиента, если ему подходит стандартный флоу.Нет поддержки мультишардирования
Пользователь может находиться только в одном шарде. Для Почты это не проблема: есть ID пользователя, он всегда живёт в одном месте. Но другим клиентам такая схема подходит не всегда. Это ограничивает потенциальную аудиторию решения.
Спасибо всем, кому хватило сил прочитать эту статью до конца.
Отдельная благодарность Кириллу Григорьеву — статья во многом родилась во время подготовки его доклада на Highload++.
Если вам интересны подробности, то вот подборка интересных докладов:
Кирилл Григорьев рассказывает про Sharpei на Highload.
Володя Бородин рассказывает про миграцию с Oracle — полезно для понимания, как подходить к крупным трансферам.
И история о SPQR от команды Yandex Cloud — если вы ищете открытое решение для шардирования PostgreSQL, обязательно обратите внимание.
AdrianoVisoccini
Уже даже фотку собаки настоящую в статью не ставят. Оно же даже не походе на шарпея...
господи, куда мы катимся?!