Главное в платежной системе — взять денежки, перевести записи из одной таблички в ту же самую табличку со знаком «минус». Звучит не очень сложно, пока не пришли юристы. Платежные системы во всем мире облагаются огромным количеством всевозможных отягощений и указаний. Поэтому в рамках разработки платежной системы приходится все время балансировать на грани между тяжелым enterprise и вполне нормальным масштабируемым web-приложением.
Под катом рассказ Филиппа Дельгядо (dph) на Highload++ про опыт, накопившийся за несколько лет работы над платежной системой для российского легального букмекерского бизнеса, про ошибки, но и про некоторые достижения, и про то, как грамотно смешать, но не взбалтывать, web с enterprise.
О спикере: Филипп Дельгядо за свою карьеру чем только не занимался — от двузвенок на
Visual Basic до хардкорного SQL. В последние годы в основном занимается нагруженными проектами на Java и регулярно делится своим опытом на разных конференциях.
Три года мы делаем нашу платежную систему, из них два года мы в продакшене. Два года назад я рассказывал, как сделать платежную систему за один год, но с тех пор, разумеется, в нашем решении много что изменилось.
Мы — это довольно маленькая команда: 10 программистов, в основном это бэкенд-раработчики и только два человека на фронтэнде, четверо QA и я, и плюс какой-то менеджмент. Поскольку команда маленькая, денег не очень много, особенно в начале.
Платежная система
Вообще, платежная система — это очень просто: взять денежки, перевести записи из одной таблички в ту же самую табличку со знаком «минус» — в общем-то и всё!
Реально так оно и есть, платежная система — очень простая штука. Пока не пришли юристы. Платежные системы во всем мире облагаются огромным количеством всевозможных отягощений и указаний, как именно правильно переводить деньги из одной таблички в другую, как именно взаимодействовать с пользователями, что им можно обещать, чего нельзя обещать, за что отвечаем, за что не отвечаем. Поэтому в рамках разработки платежной системы приходится все время балансировать на грани между тяжелым enterprise и вполне нормальным масштабируемым web-приложением.
От enterprise у нас следующее.
Мы работаем с деньгами.
Поэтому, у нас сложный бухгалтерский учет, необходим высокий уровень надежности, высокий SLA (потому что простой системы не любим ни мы, ни пользователи), и высокая ответственность — мы должны точно знать, куда и какие деньги пользователя ушли и что с ними в данный момент вообще происходит.
Мы являемся НКО (небанковской кредитной организацией).
Это практически банк, только кредиты не можем выдавать.
- У нас отчетность перед Центробанком;
- У нас отчетность перед Финмониторингом;
- У нас много коллег в банковской части компании и с банковским опытом;
- Мы вынужденно взаимодействуем с автоматизированной банковской системой.
У нас есть юристы. Вот часть законов, которые регулируют поведение платежных систем в данный момент в Российской Федерации:
Каждый из этих законов довольно сложно реализовывать, потому что еще есть куча подзаконных актов, реальных историй использования, и со всем этим приходится работать.
При этом, кроме того, что мы такой чистый enterprise, почти банк, мы при этом еще вполне себе web-компания.
- Для нас принципиально удобство пользователя, потому что рынок высококонкурентный, и если мы не будем заботиться о нашем пользователе, он от нас уйдет и денег у нас не будет совсем.
- Мы вынуждены делать частые выкладки, потому что бизнес активно развивается. Сейчас у нас релизы 23 раза в неделю, что гораздо чаще, чем у крупных банков, где, говорят, недавно наконец начали делать релизы один раз в 3 месяца и очень этим гордятся.
- Минимальный time-to-market: как только пришла какая-то идея, нужно как можно быстрее ее запустить в реальную жизнь — желательно быстрее, чем конкуренты.
- У нас не очень много денег в отличие от многих крупных банков. Взять и залить все деньгами нам не удается, приходится как-то выкручиваться, принимать какие-то решения.
Мы используем Java, потому что найти на рынке разработчиков, хотя бы примерно понимающих про надежность работы с базами данных в мире Java несколько проще, чем на других языках.
Я знаю только три базы данных, с которыми можно делать платежную систему, но одна из них очень дорогая, для второй трудно найти поддержку и она тоже не бесплатная. В итоге PostgreSQL для нас — самый оптимальный вариант: легко найти вменяемую поддержку, и в общем-то за небольшие деньги можно вообще не думать о том, что происходит с базами данных: у вас все чистенько, красивенько и гарантированно.
В проекте используется немножко Kotlin — скорее для развлечения и для того, чтобы смотреть в будущее. Пока Kotlin в основном используется как некоторый скриптовый язык, плюс некоторые небольшие сервисы.
Разумеется, сервисная архитектура. Микросервисами это назвать категорически нельзя. Микросервис в моем понимании — это то, что проще переписать, чем разбираться и рефакторить. Поэтому у нас, разумеется, не микросервис, а нормальные полноценные большие компоненты.
Кроме того, Redis для кэширования, Angular для внутренней кухни. Основной сайт, видимый пользователю у нас сделан на чистом HTML+CSS с минимумом JS.
И понятное дело, Kafka.
Сервисы
Конечно, я предпочел бы жить без сервисов. Был бы один большой монолит, никаких проблем со связностью, никаких проблем с версионированием: взял, написал, выложил. Все просто.
Но приходят требования по безопасности. У нас есть персональные данные в системе, персональные данные должны храниться и обрабатываться отдельно, с особыми ограничениями. У нас есть информация по банковским картам, она тоже должна жить в отдельной части системы с соответствующими требованиями к аудиту каждого изменения кода и требованиями к доступу к данным. Поэтому приходится все резать на компоненты.
Есть требования надежности. Не хочется из-за того, что какой-то шлюз с одним из банков-контрагентов почему-то поломался, брать и выкладывать всю платежную логику: не дай бог, там будет неправильная выкладка, человеческий фактор — и все рухнет. Поэтому все равно приходится все делить на относительно небольшие сервисы.
Но раз уж мы начинаем делить систему на компоненты по требованиям безопасности и надежности, имеет смысл в отдельные сервисы выделять и все то, что требует собственного хранилища. Т.е. для части базы, которая вообще не зависит от всей прочей системы, проще сделать отдельный сервис.
И главный для нас принцип выделения чего-то в отдельный сервис — если можно придумать очевидное имя для этого самого сервиса.
«Процессинг» или «Отчеты» — это более-менее нормальное имя, «Фигня, которая работает с репликой базы данных» — это уже плохое имя. Явно это не один сервис — либо несколько, либо часть одного большого.
Четырех этих требований нам совершенно достаточно для выделения отдельных
сервисов.
Разумеется, у нас все еще остаются микромонолиты, которые мы продолжаем резать, потому что у них накапливается слишком много слов и слишком много имен для одного сервиса. Это постоянный процесс перераспределения ответственности.
Сами сервисы взаимодействуют через JSON RPC по http(s), как в любом web-мире. При этом для каждого сервиса прописывается отдельная логика повторов запросов и кэширования результатов. В результате даже при падении какого-либо сервиса вся система продолжает нормально работать, а пользователь ничего не замечает.
Компоненты
Kafka
Это не очередь сообщения, Kafka у нас — это только транспортный уровень между сервисами, с гарантией доставки и понятной надежностью/кластеризацией. Т.е. если из сервиса А в сервис В надо что-то переслать, проще положить сообщение в Kafkу, из Kafkи сервис сам заберет, что надо. И тогда можно и не думать обо всей этой логике для повторов и кэширования, Kafka сильно упрощает такое взаимодействие. Сейчас мы пытаемся как можно больше всего переводить на Kafka, это тоже такой непрерывный процесс.
Ну и кроме всего прочего, это резервный источник данных о всех наших операциях. Я, конечно, параноик, поскольку специфика работы способствует. Я видел (довольно давно и не в этом проекте), как коммерческая база данных за кучу-кучу денег в некий момент начала писать ерунду не только в собственные файлы базы данных, но и во все реплики и в бэкап. И данные пришлось восстанавливать из логов, потому что это были данные о проведенных платежах, и без них компания на следующий день могла спокойно закрываться.
Я не очень люблю вытаскивать важные данные из логов, поэтому я лучше буду складывать всю необходимую информацию в ту же Kafkу. Если вдруг у меня произойдет какая-нибудь невозможная ситуация, я, по крайней мере, знаю, откуда забирать резервные данные, никак не связанные с основным хранилищем.
Вообще, для платежных систем иметь два независимых хранилища данных — это стандартная практика, без нее жить просто страшно — мне, например.
Логи разработки
Разумеется, логов у нас много и разных. Логи разработки мы сейчас сохраняем в Kafkу и дальше загружаем в Clickhouse, потому что, как выяснилось, так проще и дешевле. Тем более, заодно изучаем Clickhouse, что полезно на будущее. Впрочем, про работу с логами можно делать отдельный доклад.
Мониторинг
Мониторинг у нас на Prometheus + Grafana. Честно говоря, Prometheu’ом я недоволен.
В чем проблема?
- Prometheus прекрасен, когда вам надо собирать из готовых стандартных компонентов какие-то данные, и у вас этих компонентов очень много. У нас довольно мало машин. У нас 40 разных сервисов и это примерно 150 виртуалок, это не очень много. Если же мы хотим собирать через Prometheus, какую-то бизнес-мониторинговую информацию, например количество платежей, идущих через определенный шлюз, или количество событий в нашей внутренней очереди, то приходится писать довольно много кода на стороне клиента. Причем код, к сожалению, не очень простой, разработчикам приходится активно разбираться во внутренней логике и том, как именно Prometheus что-то считает.
- Prometheus нельзя использовать как честный event oriented time-series db. Я не могу взять и сказать, что есть событие начала платежа, событие конца платежа, а все прочие метрики он пускай сам посчитает. Я вынужден на клиенте все нужные мне метрики заранее считать, и если вдруг мне надо какую-нибудь из них поменять, — это очередная выкладка продакшн-компонента, что очень неудобно.
- Очень сложно делать интегрированные метрики. Если мне надо собрать общую метрику для некоторого количества сервисов (например, перцентили времени отклика клиентам по всем серверам фронтенда), то через Prometheus сделать это нереально даже теоретически. Я могу только делать какое-то непонятное среднее суммирование уже на уровне Grafana. Сам Prometheus это сделать не может.
Поэтому я всерьез думаю куда-нибудь уйти.
Дальше я буду рассказывать несколько отдельных кейсов о том, какие у нас были архитектурные вызовы, как мы их решали, что было хорошо и что при этом было плохо.
Использование базы данных
Вообще, платеж — это довольно сложно. Ниже примерное описание контекста платежа: множество кортежей (ассоциативных массивов), списков кортежей, кортежей списков, каких-то параметров. И все это постоянно меняется из-за изменений бизнес-логики.
Если это делать честно, будет много таблиц, много связей между ними. Как следствие, нужен ORM, нужна сложная логика миграции при добавлении колонки. Напомню, что, в PostgreSQL даже простое добавление новой nullable колонки в таблицу может привести (в некоторых специфических ситуациях) к тому, что на долгое время данная таблица будет вообще недоступна. Т.е. на самом деле добавление nullable column не атомарная бесплатная операция, как многие думают. Мы на это даже разочек наткнулись.
Все это довольно неприятно и грустно, хочется всего этого избежать, в особенности при использовании ORM. Поэтому мы убираем все эти большие и сложные сущности в JSON, просто потому что реально, кроме как на сервере приложения, нигде все эти данные и структуры целиком не нужны. Я использую такой подход уже лет 10 и, наконец, замечаю, что это становится если не мейнстримом, то, по крайней мере, общепризнанной практикой.
Практики работы с JSON
Как правило, храня сложные бизнес-данные в базе данных в виде JSON, по производительности вы ничего не проиграете, а может быть, даже временами и выиграете. Дальше я расскажу, как это сделать, чтобы нечаянно не прострелить себе ногу.
Во-первых, надо сразу думать о возможных конфликтах.
Когда-то у вас была версия объекта с одним набором полей данных, вы выпустили другую версию, где уже другой набор полей данных, вам надо каким-то образом читать старый JSON и преобразовывать его в удобный для вас объект.
Для решения этой задачи обычно достаточно найти хороший сериализатор / десериализатор, которому вы можете в явном виде сказать, что вот это поле из JSON надо преобразовать в такой-то набор полей, эти вещи сериализовывать так-то, а если чего-то нет, то заменить на значение по умолчанию и т.д. В Java, к счастью, с такими сериализаторами проблем нет. Мой любимый — Jackson.
Обязательно в базе данных надо хранить версию структуры, которую вы пишете.
Т.е. рядом с каждым полем, где у вас хранится JSON, должно быть еще одно поле, где хранится версия. В первую очередь, это нужно, чтобы не бесконечно поддерживать код понимания старой версии новой.
Когда вы выпустили новую версию, и у вас появилась новая структура данных, вы просто делаете скрипт миграции, который пробегает по всей базе, находит все старые версии структуры, читает их, записывает в новом формате, и у вас через какое-то довольно ограниченное время, в базе остается максимум 2-3 различные версии данных, и вы не мучаетесь с поддержкой всего разнообразия того, что у вас накопилось за многие годы. Это избавление себя от легаси, избавление себя от технического долга.
Для PostgreSQL — надо выбрать между json и jsonb.
Когда-то в этом выборе еще был смысл. У нас, например, использовался JSON, потому что мы начинали довольно давно. Напоминаю, что тип данных JSON — это просто текстовое поле, и чтобы залезть куда-нибудь внутрь, его каждый раз будет парсить PostgreSQL. Поэтому в продакшене лучше лишний раз внутрь json-объектов в базе данных не залезать, только в случае какой-то поддержки или исправления неисправностей. По-хорошему, в вашем SQL-коде вообще не должно быть команд работы с json-полями.
Если же использовать JSONB, то PostgreSQL все аккуратненько разбирает в бинарный формат, но не сохраняет оригинальный вид JSON-объекта. Когда мы, например, храним оригинальные входящие к нам данные, мы всегда используем только JSON.
Нам пока JSONB не нужен, но в данный момент, действительно имеет смысл всегда использовать JSONB и об этом уже не думать. Разница в производительности стала практически нулевой, даже на простое чтение и запись.
PCI DSS. Из простого сделать сложное, и как web становится enterprisе
Еще на этапе разработки, задолго до выхода в продакшен, был у нас маленький простой сервис с данными банковских карт, включая и собственно номер карты, который мы, разумеется, шифровали средствами PostgreSQL. При этом теоретически руководитель эксплуатации мог, наверно, найти где-нибудь ключик этого шифрования и что-нибудь узнать, но мы ему вполне доверяли.
Надежность сервиса реализовывали через active-standby — потому что сервис маленький, перезапускается быстро, 3-5 секунд прочие компоненты его точно подождут, поэтому нет смысла громоздить какую-то сложную кластерную систему.
Перед запуском мы начали проходить аудит PCI DSS, и выяснилось, что есть довольно жесткие требования контроля доступа к данным, которые, в случае нашего аудитора, сводились к тому, что:
- Не должно быть одного человека, который может прочесть всю информацию из базы данных. Это должны быть минимум несколько человек, которые совместно должны получать доступ.
- Требуется регулярная смена ключей доступа.
- PCI DSS требует при любой обнаруженной уязвимости обновлять инфраструктуру, а поскольку уязвимости в операционной системе и инфраструктурном ПО находятся довольно часто, значит, систему надо тоже обновлять довольно часто.
Для начала мы перестаем доверять руководителю эксплуатации и пытаемся придумать схему, когда у нас нет одного человека, который знает ключи.
Логичным образом приходим к схеме Шамира. Это способ генерации ключа, когда на основании готового ключа генерируется несколько ключей, любое подмножество из которых может породить исходный ключ.
Например, вы формируете длинный ключ, сразу разбиваете его на 5 кусочков так, чтобы любые три из них могли породить исходный. После чего раздаете эти три эксплуатации, два храните в сейфе на всякий случай, если кто-то заболеет, попадет под автобус и т.д., и спокойно живете. Оригинальный длинный ключ вам уже не нужен, только эти кусочки.
Понятно, что после перехода на схему Шамира в сервисе появляется логика генерации и смены ключей. Для генерации ключа использутся отдельная виртуалочка, на которой:
- генерируется ключ,
- раздается админам,
- виртуалочка убивается.
В результате исходный ключ никто не может узнать, потому что он создается в присутствии СБ-шников, на быстроумирающей системе, а дальше раздаются уже только «порожденные» ключи
При смене ключей оказывается, что у нас одновременно может быть два актуальных ключа в системе: один старый, один новый, часть данных зашифрована старым, и нужна процедура перешифровки на новый ключ.
Поскольку для того, чтобы запустить компоненту, теперь требуется два или три человека, это занимает уже не 30 секунд, а несколько минут. Поэтому простой компонента при перезапуске будет занимать уже несколько минут, и приходится переходить на схему Active-Active, с несколькими одновременно работающими экземплярами.
Таким образом, простой очевидный сервис в несколько десятков строчек становится довольно сложной конструкцией: со сложной логикой старта, с кластеризацией, с довольно сложными инструкциями по сопровождению. Из нормального простого веба мы радостно перешли в entreprise. И, к сожалению, такое происходит довольно часто — гораздо чаще, чем хотелось бы. Тем более, что топ-менеджмент и бизнес, посмотрев на все это дело, сказал, что теперь надо все данные на всякий случай шифровать примерно таким же образом, и Active-Active ему тоже всюду нравится. И эти желания бизнеса, прямо скажем, реализовать не всегда просто.
Логика платежа. Из сложного сделать простое
Как я уже говорил, платеж — это довольно сложно. Ниже нарисована примерная схема процесса перевода денег от пользователя к конечному контрагенту, но на схеме далеко не все. В процессе платежа много зависимостей от каких-то внешних сущностей: есть банки, есть контрагенты, есть банковская информационая система, есть транзакции, проводки, и все это должно работать надежно.
Надежно — это значит, что мы всегда знаем, на чьей стороне деньги, а если у нас сейчас все упало, с кого эти деньги требовать. Они могут зависнуть у любого из контрагентов, главное, чтобы не у нас. И мы должны точно знать, у кого они зависли так, чтобы все это можно было подтвердить и сообщить пользователю И, конечно, желательно, что бы всевозможных проблем было как можно меньше.
Finite State Machine
Разумеется, вначале у нас была FSM — нормальная стейт-машина, каждое событие обрабатывается в транзакции. Текущее состояние тоже сохраняем в СУБД. Реализовали все сами.
Первая проблема — у нас есть одновременные события.
Мы обрабатываем событие, связанное, например, с подтверждением пользователем начала платежа. В это время приходит событие от контрагента, отменяющее возможность проведения операции и это событие тоже нужно обработать. Поэтому в логике работы у нас появляются какие-то блокировки по ресурсам, ожидания снятия блокировок и т.д. К счастью, вначале вся обработка платежей проходила на одной машине и блокировки можно было реализовывать на уровне JVM.
Кроме того многие шаги имеют четкое максимальное время выполнения (тайм-аут), и эти времена тоже нужно где-то хранить, обрабатывать, смотреть, когда события наступления таймаутов происходят (и они тоже иногда бывают одновременными).
Все это реализовывалось через логику блокировок внутри Java-машины, потому что в базе данных это было делать не очень легко. Как следствие, получилась система с организацией высокой доступности только через Active-Standby и с кучей специальной логики восстановления контекстов и тайм-аутов.
У нас довольно маленькая нагрузка, всего десятки платежей в секунду, даже меньше сотни в случае максимального потенциального пика. При этом, правда, даже десять платежей в секунду приводит к сотне запросов (отдельных шагов) в секунду. Это небольшие нагрузки, поэтому одной машины нам почти всегда хватает.
Все было замечательно, но потребовался Active-Active.
Active-Active
Во-первых, мы захотели использовать схему Шамира, ну а также появились и другие хотелки: давайте мы будем выкладывать новую версию только на 3% пользователей; давайте часто менять логику платежа; хочется ее выкладывать с нулевым простоем и т.д.
Делать распределенные блокировки — это грустно, делать тайм-ауты распределенные тоже грустно. И мы начали в очередной раз разбираться — что такое платеж? Платеж — это множество событий, которые надо строго последовательно обрабатывать, это сложное изменяемое состояние, и обработка платежей должна идти параллельно.
Кто узнал определение? Правильно, платеж — это актор.
В Java много разных моделей акторов. Есть прекрасная Akka, есть временами странный, но прикольный Vert.x, есть гораздо менее используемый Quasar. Они все замечательные, но у них есть один фундаментальный недостаток (и не тот, о котором вы подумали) — у них недостаточные гарантии.
Ни один из них не гарантирует доставку сообщений между акторами, у всех из них есть проблема с работой внутри транзакции в базе данных.
Мы долго на это смотрели, думали, не допилить ли нам что-нибудь до вменяемого состояния, но потом сделали свой велосипед: очередь в PostgreSQL через select for update skip locked.
Все решение влезло в тысячу строчек кода и заняло примерно две человеко-недели на разработку и две человеко-недели на тестирование-доводки. При этом многие наши внутренние потребности, которые в той же Akka нормально не сделать, оказались выполнены.
Skip locked
Это такая прекрасная штука для реализации очередей в PostgreSQL. На самом деле данный механизм есть во всех базах данных, кроме, по-моему, MySQL.
Предположим, у нас есть две таблички: табличка с нашими акторами — flow, и табличка событий для этих акторов, она связана по колонке flow. События отсортированы по автоинкрементному ключу ID, все обычно. Пишем SQL-запрос.
Выбираем самое первое событие в самом первом из flow, указав магическое for update skip locked. Если никаких блокировок в табличке нету, запрос работает ровно как нормальный for update — берет и ставит блокировку на первую строчку, которую мы выбрали, т.е. на строку с первым актором и на строку с первым событием для этого актора.
Запускаем тот же запрос второй раз и он делает ровно то же самое, но пропуская уже заблокированные строчки. Поэтому он выберет первое событие во втором акторе (третья строка в таблице) и повесит на него блокировку.
Предположим, за это время мы закончили обработку первого из событий, удалили его и закрыли транзакцию. Блокировка снялась, поэтому в следующий раз выполнив запрос мы получим первое, на данный момент, событие, в первом акторе.
Это все работает достаточно быстро и надежно. На дешевом железе мы получали порядка 1000 подобных операций в секунду, при условии, что каждая из них тормозит где-то по 10 миллисекунд. Я использовал подобный подход несколько раз, весь код пишется буквально в три строчки и очень легко приделывать к такой очереди всякие удобные вещи.
Что мы получаем с такой очередью?
Все сообщения транзакционные: мы начали транзакцию, в ней что-то делаем с базой данных, в ней посылаем куда-то сообщения в другие акторы, если транзакция откатывается, сообщения тоже будут не посланы, что безумно удобно.
Можно не думать об отправке сообщений, отменяющих предыдущие, не думать о том, что все сообщения надо посылать пачкой только в конце обработки и после коммита. Вообще о многих вещах перестаешь думать. Например, не нужно думать о блокировках, потому что у вас все события обрабатываются последовательно, для чего, собственно, и придуманы акторы.
В своей реализации мы еще и добавили сложную политику обработки ошибок, потому что 80% логики платежа — это на самом деле обработка возможных ошибок: пользователь куда-то ушел, контрагент ответил какой-то ерундой, у пользователя вообще нет денег или контрагент не работает и надо выбрать другого контрагента, другой шлюз, и так далее. Там безумное количество разной сложной логики по обработке всевозможных ошибок.
Для нас это решение эффективно — 100 платежей в секунду нас устраивает.
Но это решение очень ограниченной применимости — свой велосипед, который можно применять довольно мало где. И у него очень жесткие ограничения по производительности. То есть коллегам из Яндекс.Денег такое не посоветую, потому что у них бывают черные пятницы, и 100 платежей в секунду им явно мало. У нас, к счастью, черной пятницы не бывает, у нас очень конкретный рынок, и поэтому мы можем спокойненько обойтись подобным решением. При этом это честный свой велосипед, честный enterprise-подход — OpenSource библиотеки в данном случае нам не очень подходит.
Сеть и транзакции
На бумаге все было гладко. Мы это внедрили, запустили — работает. И вдруг приходит проблема — упал один из шлюзов.
Шлюз — это реализация протокола взаимодействия с кем-то из поставщиков денег. Ну, упал и упал, пользователи ничего не заметили, мы переключились на резервный с другим контрагентом и начали разбираться, почему. Оказалось, что кончились соединения в пуле. Причина непонятна, вроде бы на шлюзе была не такая уж и большая нагрузка, чтобы исчерпать все соединения.
Начинаем разбираться и выясняем: у нас контрагент стал отвечать на сетевые запросы не за полсекунды, как было раньше, а за одну минуту. Поскольку у нас обработк азапроса к контрагенту является шагом платежа, то она выполняется в транзакции. Когда много транзакций начинают выполняться долго то соединений с БД на все поступающие запросы уже не хватает. Такое нормальное поведение: когда у вас очень много длинных транзакций, у вас соединения почему-то начинают заканчиваться.
Мы начали думать, что я этим делать. Самое первое — можно увеличить число соединений. К сожалению, в PostgreSQL есть вполне понятные лимиты на максимальное количество соединений на ядро, и оно не очень большое — порядка сотни. Потому что у PostgreSQL, напоминаю, каждое соединение — это один процесс. А процессов все-таки совсем много, десятки тысяч или сотни тысяч дешево сделать не получится. А если у нас контрагенты начинают отвечать раз в минуту, то одновременных транзакций может потребоваться и больше.
Можно попытаться сетевой вызов сделать асинхронным, то есть каждый шаг разбить на два. Каждый раз, когда нам нужно кого-то дернуть из контрагентов, нужно сделать вызов, сохранить состояние в контекстную базу данных, получить от него ответ. Обработка ответа попадет на этот же актор, мы поднимем состояние из базы данных, сделаем еще что-нибудь нужное. Но при этом у нас многократно увеличивается количество шагов в платеже, и в наше требование — 100 платежей в секунду — мы уже не укладываемся. Да и логика работы становится сложной.
Остается только управлять гарантиями сохранения. Нам не всегда нужна жесткая транзакционность, нам не всегда нужно жесткое восстановление при сетевом вызове, мы почти всегда можем его повторить. Поэтому нам нужно не все уметь делать через базу данных, какие-то вещи надо уметь делать в обход базы данных, в обход транзакций.
К сожалению, стандартного решения, которое бы позволяло тонко управлять гарантиями сохранения для конкретного события, нет. Сейчас я его пытаюсь писать, но, честно, реализовать skip locked на каком-нибудь Redis на Lua оказывается, довольно нетривиальная задача. Если я все это сделаю, я обязательно про это расскажу.
В качестве временного решения мы разбили процесс платежа на несколько отдельных акторов, исполняемых на разных СУБД (и на разных серверах). Это позволило внести асинхронные запросы там, где это было необходимо и решить текущие проблемы.
Главный вывод — если у вас в системе где-то появились акторы, они рано или поздно поползут всюду. Если вы думаете: «у нас будет актор в отдельном кусочке и производительности нам хватает», это не так. У вас в конечном итоге через год разработки выяснится, что все хотят их использовать там, где надо, и там, где не очень надо, и они всюду. Просто попробовать не удастся!
Учет и контроль. Бюджетный Business Intelligence
У нас платежная система, то есть деньги, а деньги любят, когда их считают. Поэтому очень быстро к нам пришел бизнес с просьбой сделать Business Intelligence систему. Данных у нас не очень много, какие-то сотни гигабайт, нужно это только топ-менеджменту, сотен аналитиков у нас не наличествует. И главное — надо делать «быстро и дешево».
Power BI — быстро и дешево?
Берем PowerBI — это решение от Microsoft: система генерирует нужные данные в виде csv, csv загружаются в облако, из облака они загружаются в PowerBI. Дешево, быстро, просто, сделано буквально на коленке, почти совсем без привлечения программистов. Отчеты написать в csv — это несложно.
Но оказалось, что дешево — это если у вас мало данных, и быстро — если мало данных. Как только у нас объем данных превысил 1 Гб, выяснилось, что и обрабатывается это довольно долго, и главное, в некий момент Microsoft изменил условия использования сервиса и он стал очень платным как раз примерно начиная с размера в 1 Гб. И выяснилось, что это нам уже несколько не по карману.
Пошли смотреть, что можно сделать.
ClickHouse
Первая мысль — ура, есть же ClickHouse! Кидаем все наши события в Kafkу, оттуда пачками выгружаем в ClickHouse, получается круто, модно, хайпово, аналитика должна работать быстро, все должно быть бесплатно, вообще прекрасно и замечательно. Но результат из ClickHouse надо где-то показывать. На данный момент с Clickhouse лучше всех работает Redash. Сделали тестовую версию Redash, показали бизнесу — те сказали, что с этим они работать не будут, потому что выглядит оно, мягко говоря, уродливо и некоторых милых бизнесу вещей типа drill-down там просто нет.
Начали выяснять, а о чем вообще мечтает бизнес. Бизнес мечтает о чем-нибудь типа Tableau, где все красиво. Tableau лучше всего интегрируется с Vertica, и получается прекрасная, по-идее, система: все события кидаем в Kafka, с Kafka перекидываем в Vertica.
Vertica работает быстро, качественно, надежно, просто, а Tableau Server все это показывает. Одно но — стоимость лицензии Vertica официально не сообщается, но, мягко говоря, немалая,. Tableau тоже не очень дешевый. К счастью, выяснилось, что на наших объемах все это на самом деле не так и дорого, потому что до одного терабайта данных Vertica — халявная, Community Edition нас абсолютно устраивает, до терабайта нам еще далеко. А поскольку лицензия на Tableau нам нужна только на небольшое количество разработчиков и топ-менеджеров, то это стоит какие-то полне нормальные деньги. Вплоть до того, что нам нужно было меньше лицензий, чем минимальный пакет, который продает Tableau.
Оказалось, что такое нормальное, совершенно классическое тяжелое enterprise-решение, является еще и нормальным web-решением. Оно недорого и ставится с нуля, не задумываясь. Vertica меня пока радует: в ней многие именно аналитические вещи решаются очень красиво. Пока у вас не очень много данных — советую. Впрочем, в эксплуатации она требовательна к пониманию принципов ее работы, нужно в них разобраться перед использованием.
При этом я думаю, что если мы через несколько лет вырастем за пределы терабайта, то к этому времени у нас как раз уже будет хорошая экспертиза по ClickHouse, Tableau к этому времени явно сделает к нему адаптор, и мы аккуратненько переползем на бесплатный ClickHouse за какое-то вполне разумное время.
Контент
У нас довольно много текстов:
- Юридическая информация, которую мы обязаны предоставлять, оферты и т.д.;
- Информация о контрагентах, включая комиссии, которые мы берем с пользователей;
- Инструкции для пользователей;
- Собственный маленький блог;
- Информация об ошибках и прочее.
Ошибка в этой информации довольно болезненна. Например, если мы выложили не те комиссии, которые мы реально взимаем, пользователи потом могут очень сильно на нас обидеться, а, главное, на нас могут обидеться контролирующие органы, что гораздо хуже. Поэтому тексты для нас — это тоже код: нужно проверять его перед публикацией, в его подготовке задействовано много людей, ошибки дорого стоят.
Вначале у нас текст был просто частью фронтенда: все тексты верстались верстальщиками-фронтендерами, потом шли в тестирование, вычитывалось, показывалось на демо-стенде, потом уже шли в продакшн. Но текст меняется слишком часто, и так делать было просто дорого.
И мы начали думать, как бы все это автоматизировать и сделать CМS. Простые CМS не подходят, потому что:
- сложно с разделением тестовой среды и продакшена;
- непонятно, как тестировать текст;
- сложно работать большому количеству пользователей;
- сложно интегрировать с большой Java-системой.
Не простые CМS — слишком дорогие во всех смыслах, они и стоят обычно больших денег, и их интеграция весьма неочевидна, потому что там напридумано много всего.
Идеальным решением было бы поставить у всех, кто работает с текстами, банальный Git: пусть они все написанные тексты отправляют прямо в репозитарий. Но от мысли поставить Git топ-менеджерам и копирайтером и научить их им пользоваться, мы подумали-подумали и в ужасе отказались, потому что все-таки git не для нормальных людей.
Самое идеальное решение было бы, наверное, редактор текстов, встроенный прямо в IntelliJ IDEA, где можно сложность использования Git аккуратненько скрыть. Но, к сожалению, JetBrains такого редактора до сих пор не сделали, хотя я давно их просил.
Пришлось опять делать велосипед:
- Простой редактор текста.
- Простой редактор html, потому что вместо сложного редактора текста проще попросить нашего же фронтендера сверстать все в html, но при этом выкладывать через систему CMS и систему контроля качества.
- Простая концепция версий — есть пакеты изменений, публикация идет целиком пакетами, восстановление при необходимости всего контента тоже по одной кнопочке. И очень простой порядок работы с текстами.
- Категорический запрет (просто нет такой кнопочки) что-нибудь изменить прямо на продакшне. Все можно изменить только через задачу в Jira, дальше она попадает в тестовую версию, с тестовой версии на продакшен уже переносят отдельно обученные люди — все, что они могут, это переносить готовые пакеты с тестовой версии на продакшн, после того как получены все подтверждения, без возможности вносить правки.
То есть пришлось написать, по сути дела, микро-портальчик в стиле enterprise.
Честно говоря, если бы я мог такое решение купить — я предпочел бы это купить. Но ни одной embedded CMS для больших систем я на рынке просто не нашел, и, по-моему, их до сих пор нет. Я, честно говоря, уже который раз пишу ее с нуля, и очень жалко, что до сих пор никто это не сделал за меня.
Выводы
Что из всего этого можно сказать? Что жизнь на грани довольно интересна.
Когда у вас задачи одновременно и из web, и из enterprise, можно заимствовать разные идеи из мира корпораций, у них довольно много вещей продумано. Иногда можно заимствовать не только идеи, но и конкретные решения типа как Vertica, если они дешевые.
Честно, если бы я нашел дешевую поддержку для IBM DB2 — я бы реализовывал бы проект на ней, я ее очень люблю, она дешевая и очень надежная, но найти поддержку этой базы за разумные деньги в России сложно. Конечно, можно кого-нибудь переманить из Почты России, но они привыкли к настолько большим серверам, что мы явно для них слишком мелкие.
Ну и большие проблемы из мира enterprise можно решать в web-стиле довольно просто, чем мы постоянно занимаемся.
Архитектура — понятие динамическое.
Не бывает хорошей архитектуры вообще. Бывает архитектура, которая более-менее вас удовлетворяет в конкретный момент времени. Время меняется — архитектура меняется, и надо постоянно быть в этому готовым, и всегда вкладывать ресурсы в развитие архитектуры. Архитектура проекта — это процесс, а не результат.
Java и SQL — это реально круто, если вы умеете это готовить. Мы умеем, поэтому у нас все получается просто, быстро, ненапряжно, и очень небольшой командой мы делаем довольно сложны
Новости
Даты проведения HighLoad++ 2018 переносятся на 8 и 9 ноября, чтобы разминуться с Percona Live.
Прием заявок на Highload++ Siberia, которая пройдет 25 и 26 июня в Новосибирске, уже закончился, поданные доклады опубликованы на сайте, цена билетов уже не минимальная, но еще и не слишком выросла — идеальный момент для бронирования.
До РИТ++ осталось совсем немного времени, программа активно формируется. В тему сегодняшней статьи можно отметить такие заявки:
- «Возможности ClickHouse для продвинутых» от собственно разработчика ClickHouse Алексея Миловидова.
- Юрий Лилеков с докладом о том, зачем разработчику статистика, или как улучшить качество продукта?
- Александр Сербул расскажет об особенностях lambda-архитектур, платформе микросервисов Amazon Lambda, а также подводных камнях и победах с Node.JS и многопоточной Java.
Автор: mi5ha6in