Введение

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

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

И последняя цель статьи - добиться стабильной обработки контрактов в течении долгого времени (более часа).

Описание изменений

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

Сервис аккаунтов

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

Таким образом, если раньше сервис аккаунтов выглядел так, как показано на рисунке 1, то после шардирования он функционирует так, как показано на рисунке 2.

Фрагменты архитектуры сервиса Аккаунт
Рисунок 1. Фрагмент архитектуры сервиса Аккаунт до шардирования
Рисунок 1. Фрагмент архитектуры сервиса Аккаунт до шардирования
Рисунок 2. Фрагмент архитектуры сервиса Аккаунт после шардирования
Рисунок 2. Фрагмент архитектуры сервиса Аккаунт после шардирования

Теперь, для регистрации пользователя сервис hq-account-signup должен будет не только зарегистрировать для идентификатора аккаунта шард, сходить в кейклок, но так же отправить запрос на создание нужных данных в сервис hq-account-api соответствующего шарда, что позволит держать в БД нужные записи.

Сервис контрактов

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

Фрагменты архитектуры сервиса Контракт
Рисунок 3. Фрагмент архитектуры сервиса Контракт до шардирования
Рисунок 3. Фрагмент архитектуры сервиса Контракт до шардирования
Рисунок 4. Фрагмент архитектуры сервиса Контракт после шардирования
Рисунок 4. Фрагмент архитектуры сервиса Контракт после шардирования

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

Такая организация сервиса контрактов позволяет добавлять или убирать шарды по необходимости. Например вывести шард из эксплуатации, что бы провести очистку БД или другие операции. Это возможно из-за того, что каждый контракт имеет свою ассоциацию с шардом, хранимую в contract_shard. Для большей ясности в структуру добавлены так же шарды сервиса балансов (вы можете заменить все упоминания balance, на debt, что бы получить ту же самую картину, но для работы с сервисом задолженностей). Сервису баланс не нужно узнавать у микросервиса contract-shard, какому шарду сервиса контрактов нужно отвечать, так как эта информация прописывается в самом событии создания платежа. Это позволяет упростить взаимодействия между микросервисами и не делать лишних запросов.

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

Особенностью такого подхода является возможность реализовать такую фичу как распределенный кошелек. В данный момент это сделано только для горячих кошельков - для таких кошельков происходит назначение сразу нескольких шардов, которые при необходимости выбираются случайно. Таким образом кошелек, на который может приходится по миллиону платежей в секунду, может принимать их не приводя к серьезным проблемам производительности для других кошельков. Очевидно, что для работы с такими кошельками нужны специальные API, способные агрегировать информацию между разными шардами и предоставлять пользователю. Так как подобный функционал может быть нужен только юридическим лицам (и то не всем, так как обычные кошельки могут спокойно обрабатывать до 100 платежей в секунду), то автором такое API не разрабатывалось.

Нагрузочное тестирование

Автором помимо шардирования, так же был проведен рефакторинг библиотеки по работе с очередями сообщений. Это позволило не только улучшить стабильность работы, но так же улучшить стабильность обработки и предоставить возможность обрабатывать неуспешные сообщения в специальной очереди повторов. Автор рекомендует читателю заглянуть в репозиторий и ознакомиться с изменениями вот тут kafka-consumer.

Ранее автор приводил лишь частично результаты НТ, в данной статье будут приведены графики полностью. Само НТ можно разделить на две части. Первая - до изменения конфигурации самой БД и попытки оптимизировать инфраструктуру, а так же до добавления ресурсов. Второй этап - существенно увеличены ресурсы, выделяемые виртуальным машинам, а так же последствия изменения конфигурации серверов PostgreSQL.

Для тестирования было создано миллион учетных записей, что бы гарантировать случайность переводов. Каждому пользователю затем было начислено 1 000 000 000 рублей на счет, что бы в ходе тестов они точно не закончились.

Часть первая

На графиках ниже отображена сессия нагрузочного тестирования первой части. До 16:03 все тесты перезапускались с очисткой таблиц от созданных контрактов. С 16:03 и до 18:10 автор не трогал систему и позволил ей работать с постоянной нагрузкой в 40 контрактов в секунду на каждый узел. Под конец этого теста количество контрактов в очереди на обработку достигло десяти тысяч. Следует обратить внимание на пик времени обработки сообщения в 17:30. Таймаут на обработку выставлен в 5 секунд, но по каким-то причинам метрика, которую генерирует сам сервис, это не метрика кафки - ушла в небеса до 5 минут. Как оказалось в дальнейшем, на следующий день, автор выяснил, что проблемой оказалось недостаточное количество IOPS, которое может предоставить дисковая подсистема на безбуфферных консьюмерских nvme-накопителях. При этом на всех etcd-машинах кластера (как кубернетиса, так и patroni), в логах были ошибки о том, что fdatasync выполяется слишком должно. Причем все это происходит синхронно на всех узлах.

С 18:30 автор искал оптимальные параметры для работы PostgreSQL, так как чекпоинты срабатывали постоянно и по причине WAL, а не time, как это рекомендуется инструкцией. В итоге остановился на том, что время чекпоинта - 30 минут, максимальный и минимальный размеры wal - 30 Гб и 2 Гб соответственно. Так же количество общих буфферов (shared_buffers) было увеличено с 1 Гб до 16 Гб (сервера PostgreSQL получили увеличение памяти с 8 Гб до 32 Гб).

Графики первой части
Рисунок 5. Время на завершение контракта
Рисунок 5. Время на завершение контракта
Рисунок 6. Среднее время обработки одного сообщения микросервисом node-contract-processor (p99)
Рисунок 6. Среднее время обработки одного сообщения микросервисом node-contract-processor (p99)
Рисунок 7. Количество завершенных контрактов в секунду
Рисунок 7. Количество завершенных контрактов в секунду

Под конец дня размер БД достиг 22 Гб (на следующий день уже дошло до 40 Гб). Это размер занятого на диске пространства, т.е. он включает в себя wal, система расположена на другом диске. Таблицы контрактов не очищались в дальнейшем.

Часть вторая

К этому моменту автор еще не смотрел на логи etcd и не убрал огромное число ошибок, сыпавшее в журнал patroni, что не позволяло увидеть ошибки подключения к etcd. Поэтому первой причиной отказа видел БД, редис и кафку. Кафка отпала сразу, так как у нее были стабильные метрики работы и отправка, задержки и прочие параметры были ровные и менялись только при изменении или включении/отключении тестов.

Для анализа работы с БД были добавлены две новые метрики - время загрузки изменений из БД и время выборки сохраненного состояния контракта из кеша. Автор ожидал увидеть там пики, похожие на те, что были на рисунке 6. Однако как оказалось, никаких пиков там не было, как показано на рисунках 8 и 9.

Графики второй части
Рисунок 8. Среднее время загрузки изменений из БД (p99)
Рисунок 8. Среднее время загрузки изменений из БД (p99)
Рисунок 9. Среднее время загрузки состояния из redis (p99)
Рисунок 9. Среднее время загрузки состояния из redis (p99)
Рисунок 10. Среднее время обработки одного сообщения микросервисом node-contract-processor (p99)
Рисунок 10. Среднее время обработки одного сообщения микросервисом node-contract-processor (p99)
Рисунок 11. Время на завершение контракта
Рисунок 11. Время на завершение контракта
Рисунок 12. Количество завершенных контрактов в секунду
Рисунок 12. Количество завершенных контрактов в секунду

Из рисунков 10, 11 и 12, особенно 10, хорошо видно, что среднее время обработки все равно пробивает порог в 5 секунд, согласно которым обработка должна отваливаться. И метрика была бы не выше 5 секунд. После очистки логов от шума ошибок и анализа журналов других виртуальных машин и их сервисов, автором были обнаружены записи etcd на всех инсталляциях (а их шесть, три для кубернетиса и еще три для патрони), предупреждения о том, что fdatasync выполняется слишком долго, беглым взглядом автор увидел значения в 3 и более секунд ожидания подтверждения записи на диск!

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

Вывод нагрузочного тестирования от автора следующий: несмотря на оптимизацию инфраструктуры, проведенную по итогам эксплуатации ceph, и отказа от него, автору не удалось получить достаточную производительность дисковой подсистемы, которая выражается в резком падении производительности после заполнения SLC кеша накопителей и существенному снижению количества доступных IOPS для стабильной работы виртуальных машин. В частности это выражается в замораживании работы виртуальных машин, пока они ожидают отклика о завершении операций.

Следует отметить, что автор использует безбуфферные nvme-накопители, и отличием их от серверных решений является только наличие DRAM и более мощного контроллера у последних. Для того, что бы не останавливаться на такой плохой ноте, автором было принято решение поиграться с конфигурацией zfs, которая и оперирует устройствами, с целью улучшения производительность дисковой подсистемы гипервизора.

Часть третья

В zfs есть возможность отключения синхронной записи на диски, заставляя ее отправить команду о завершении синхронной операции сразу, при этом данные будут писаться в очередь асинхронной записи в памяти гипервизора. Добиться этого можно устанавливая sync=disabled на датасет, так как диски виртуальных машин - это все же датасет, только с особым типом zvol, то такая возможность есть. Автором были модифицированы таким образом диски СУБД, а так же kafka и Redis. Именно эти сервисы соответственно порядку оказывали существенное влияние на всю дисковую подсистему перегружая ее возможности по IOPS.

И хотя 16 Гб для кеша достаточно много, как показал опыт эксплуатации zfs автора, было принято решение увеличить его в 4 раза. Уходить в память, то по полной программе.

Теперь после тюнинга и подбора параметров, например потребовалось уменьшить число подключений к СУБД у микросервисов с 8 до 5, так в процессе НТ уже начали возникать ошибки, что у PostgreSQL остались подключения только для супер-пользователя. Необходимо все же делать свои отдельные инстансы СУБД для узлов и HQ. Несмотря на избыточные ресурсы, оказалось, что 8 ядер более чем достаточно, что бы обслуживать запросы всей системы, включая keycloak.

Теперь наконец-то получилось провести долговременное НТ. Максимум был обнаружен приблизительно на 50 контрактах в секунду на узел и превышение иногда приводила к накоплению очереди. Так как приблизительно половина контрактов проходила между узлами, то эффективное максимальное количество обработанных контрактов в секунду всей системой - примерно 75. БД в ходе теста выросла с 40 Гб до 90 Гб. Ошибка с зависшими контрактами в статусе завершения все так же присутствует, но в результате выполнения НТ в третьей части их оказалось всего 5.

О баге

Баг редкий, но точка входа теперь ясна, сообщение из класса обработчика пишется в лог, но обновления БД, отправки сообщений не происходит. В deadLetter ничего не уходит, даже повтора обработки, сообщение в журнале появляется только один раз на узел.

Графики третьей части
Рисунок 13. Среднее время загрузки изменений из БД (p99)
Рисунок 13. Среднее время загрузки изменений из БД (p99)
Рисунок 14. Среднее время загрузки состояния из redis (p99)
Рисунок 14. Среднее время загрузки состояния из redis (p99)
Рисунок 15. Среднее время обработки одного сообщения микросервисами балансов и контрактов (p99)
Рисунок 15. Среднее время обработки одного сообщения микросервисами балансов и контрактов (p99)
Рисунок 16. Время на завершение контракта
Рисунок 16. Время на завершение контракта
Рисунок 17. Количество завершенных контрактов в секунду
Рисунок 17. Количество завершенных контрактов в секунду
Рисунок 18. Утилизация ЦПУ по неймспейсам
Рисунок 18. Утилизация ЦПУ по неймспейсам
Рисунок 19. Утилизация ЦПУ PostgreSQL, реплики для чтения не использовались совсем
Рисунок 19. Утилизация ЦПУ PostgreSQL, реплики для чтения не использовались совсем

Последние конфигурации можно найти в этой директории.

Заключение

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

Шардирование - это прекрасный инструмент для масштабирования системы и разделения ее на фрагменты, которые могут быть расположены в разных регионах. Применительно к платежной системе Mireapay - это позволяет создавать множество шардов сервиса контрактов, не обладающие высокой производительностью (потому что автор не ставил целью оптимизацию и сейчас на 1 физическое ядро приходится примерно 2 контракта в секунду с учетом всех работающих серверов - все физические сервера, с 32 ядрами каждый, были загружены на 30% по ЦПУ в ходе НТ с нагрузкой примерно 66 контрактов в секунду), что позволяет добиваться любых показателей производительности по количеству контрактов в секунду на конкретном узле. А способ шардирования по идентификатору контракта позволяет добавлять или убавлять шарды по необходимости или сезонности, например платежную систему можно заранее подготовить к сезонным распродажам, добавив шарды контрактов в узел. Таким образом систему можно масштабировать не только вертикально, но и горизонтально, не перезапуская уже работающие шарды. Выводить любые из них для эксплуатационных операций, например очистки БД и кешей. Такой подход допустим, потому что сервис контрактов не отвечает за долговременное хранение информации о контрактах.

Вторым преимуществом шардирования является возможность создавать распределенные кошельки, когда балансы, задолженности или история контрактов может хранится на разных шардах, что позволит крупным юридическим лицам, требующим обрабатывать миллионы контрактов в секунду, осуществлять свою деятельность без необходимости создавать тысячи корр. счетов. Разумеется не обязательно для юридических лиц вообще использовать те же самые сервисы баланса, предполагающих строгое соответствие кредита/дебита. Всегда можно использовать классическую структуру на основе ежедневного сведения кредита и дебита. Однако следует уточнить, что платежная система Mireapay - это не только управление балансом кошелька, но так же задолженности и история контрактов. При миллионах контрактов в секунду ни один шард не справится с такой нагрузкой в одиночку, даже если нагрузка будет только от одного кошелька. Такой подход позволит крупным юридическим лицам экономить существенные суммы средств, так как позволяет использовать общие мощности, масштабированием которых занимается оператор платежной системы, согласно SLA для всех участников, оплачивающих соответствующую услугу.

В купе с возможностью использовать дублирующие вычисления, когда на каждый шард существует зеркало, слушающее те же самые топики (кроме топиков повторов), и пишущие в те же самые БД, можно полностью нивелировать проблему отказов и дать более жесткие по времени гарантии на обработку контракта. А в случае разработки на языках, таких как Java, отказаться от необходимости тратить ресурсы на прогрев или предварительную компиляцию байт-кода. Все что нужно - это рассчитать время, которое должно проходить прежде чем зеркало после перезапуска выйдет на нужную производительность. Данная возможность не реализована в данный момент, но необходимые приготовления уже есть.

И последний важный вывод этой статьи - автору удалось достичь стабильной производительности дисковой подсистемы, за счет приведения всех операций записи к асинхронной для виртуальных машин создающих большую часть IOPS. Это позволило провести долговременное нагрузочное тестирование с целью определения стабильности системы и возможности длительно обрабатывать одну и ту же нагрузку. Минусом такого подходя является возможность потери данных, так как запись на диск осуществляется раз в 5 секунд (по умолчанию), и если произойдут проблемы на самом гипервизоре, то эти данные будут утеряны. Такой подход неприемлем для прода, но в качестве дев-окружения и пре-прода является допустимым, так как никакой ценной информации нет.

Из-за особенностей экономической политики ЦБ и Минфина РФ, автором было принято решение остановиться на полученных результатах и не продолжать разработку. Последние 7-8 лет были золотыми для IT-индустрии, что позволило автору вместо покупки квартиры, которую можно было бы купить, не трать автор время на проекты, воспользоваться этим шансом для науки. Рождение идеи о цикле этой статьи возникли у автора еще во время работы над проектом Appercut. Сама идея казалось абсолютно неподъемной и не реализуемой. Однако статья о Миллиарде абитуриетов МИРЭА, а так же, дополнительно, опыт работы в edna, убедили автора сформировать план, хоть тяжелый и долгий, но уже реализуемый за 1-2 года состоящий из трех частей - минимум, золотая середина и максимум. Часть этого проекта сделана намеренно близко к тому, с чем автор работал в компании edna, считая что так будет проще перенести результаты на продукты компании. В знак благодарности Пахурову Константину и его команде.

Золотое время IT, по всей видимости, подошло к концу. Хотя у автора еще много идей, ведь бесконечность пространства и времени - это не единственные бесконечности нашего мира, есть другие, например бесконечность сложности - а ведь именно из-за нерешенности этой проблемы существующие системы AI никогда не станут AGI, сколько бы ресурсов в них не бросали. На эти идеи уже нет и не будет ресурсов, а даже если удастся их собрать (что вряд), то уже не будет времени, даже если, как это обычно делает ЦБ, не станет в очередной раз сжигать накопления граждан.

В ходе работы автором было затрачено не мало средств и времени, разумеется автор не рассчитывал, что кто-то ему что-то будет возмещать. Целью являлось предоставление технической базы, что бы когда, самое позднее, в начале 30х правительство РФ будет кардинально переформатировать экономику и финансы страны, ЛПР понимал что можно требовать и какие ресурсы на это потребуются. Какие возможности может предложить технологическая база на данный момент. Данная работа строилась по принципу поиска возможностей, а не создания готового решения, так как на него все равно не хватило бы ресурсов автора. Этот проект исследовательский.

Но если военкомат Академического округа города Москвы перестанет наконец морозить и выдаст военный билет, то автор будет очень благодарен.

Один подарок автор все же получит - наконец-то можно постричься =)

Комментарии (0)