Хабрахабр

Архитектура платежной системы. Банальности, проверенные опытом

Главное в платежной системе — взять денежки, перевести записи из одной таблички в ту же самую табличку со знаком «минус». Звучит не очень сложно, пока не пришли юристы. Платежные системы во всем мире облагаются огромным количеством всевозможных отягощений и указаний. Поэтому в рамках разработки платежной системы приходится все время балансировать на грани между тяжелым enterprise и вполне нормальным масштабируемым web-приложением.

Под катом рассказ Филиппа Дельгядо (dph) на Highload++ про опыт, накопившийся за несколько лет работы над платежной системой для российского легального букмекерского бизнеса, про ошибки, но и про некоторые достижения, и про то, как грамотно смешать, но не взбалтывать, web с enterprise.

В последние годы в основном занимается нагруженными проектами на Java и регулярно делится своим опытом на разных конференциях.
О спикере: Филипп Дельгядо за свою карьеру чем только не занимался — от двузвенок на
Visual Basic до хардкорного SQL.

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

Поскольку команда маленькая, денег не очень много, особенно в начале. Мы — это довольно маленькая команда: 10 программистов, в основном это бэкенд-раработчики и только два человека на фронтэнде, четверо QA и я, и плюс какой-то менеджмент.

Платежная система

Вообще, платежная система — это очень просто: взять денежки, перевести записи из одной таблички в ту же самую табличку со знаком «минус» — в общем-то и всё!

Пока не пришли юристы. Реально так оно и есть, платежная система — очень простая штука. Поэтому в рамках разработки платежной системы приходится все время балансировать на грани между тяжелым enterprise и вполне нормальным масштабируемым web-приложением. Платежные системы во всем мире облагаются огромным количеством всевозможных отягощений и указаний, как именно правильно переводить деньги из одной таблички в другую, как именно взаимодействовать с пользователями, что им можно обещать, чего нельзя обещать, за что отвечаем, за что не отвечаем.

От enterprise у нас следующее.

Мы работаем с деньгами.
Поэтому, у нас сложный бухгалтерский учет, необходим высокий уровень надежности, высокий SLA (потому что простой системы не любим ни мы, ни пользователи), и высокая ответственность — мы должны точно знать, куда и какие деньги пользователя ушли и что с ними в данный момент вообще происходит.

Мы являемся НКО (небанковской кредитной организацией).
Это практически банк, только кредиты не можем выдавать.

  • У нас отчетность перед Центробанком;
  • У нас отчетность перед Финмониторингом;
  • У нас много коллег в банковской части компании и с банковским опытом;
  • Мы вынужденно взаимодействуем с автоматизированной банковской системой.

У нас есть юристы. Вот часть законов, которые регулируют поведение платежных систем в данный момент в Российской Федерации:

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

При этом, кроме того, что мы такой чистый enterprise, почти банк, мы при этом еще вполне себе web-компания.

  • Для нас принципиально удобство пользователя, потому что рынок высококонкурентный, и если мы не будем заботиться о нашем пользователе, он от нас уйдет и денег у нас не будет совсем.
  • Мы вынуждены делать частые выкладки, потому что бизнес активно развивается. Сейчас у нас релизы 23 раза в неделю, что гораздо чаще, чем у крупных банков, где, говорят, недавно наконец начали делать релизы один раз в 3 месяца и очень этим гордятся.
  • Минимальный time-to-market: как только пришла какая-то идея, нужно как можно быстрее ее запустить в реальную жизнь — желательно быстрее, чем конкуренты.
  • У нас не очень много денег в отличие от многих крупных банков. Взять и залить все деньгами нам не удается, приходится как-то выкручиваться, принимать какие-то решения.

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

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

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

Микросервис в моем понимании — это то, что проще переписать, чем разбираться и рефакторить. Разумеется, сервисная архитектура. Микросервисами это назвать категорически нельзя. Поэтому у нас, разумеется, не микросервис, а нормальные полноценные большие компоненты.

Основной сайт, видимый пользователю у нас сделан на чистом HTML+CSS с минимумом JS. Кроме того, Redis для кэширования, Angular для внутренней кухни.

И понятное дело, Kafka.

Сервисы

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

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

Не хочется из-за того, что какой-то шлюз с одним из банков-контрагентов почему-то поломался, брать и выкладывать всю платежную логику: не дай бог, там будет неправильная выкладка, человеческий фактор — и все рухнет. Есть требования надежности. Поэтому все равно приходится все делить на относительно небольшие сервисы.

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

И главный для нас принцип выделения чего-то в отдельный сервис — если можно придумать очевидное имя для этого самого сервиса.

«Процессинг» или «Отчеты» — это более-менее нормальное имя, «Фигня, которая работает с репликой базы данных» — это уже плохое имя. Явно это не один сервис — либо несколько, либо часть одного большого.

Четырех этих требований нам совершенно достаточно для выделения отдельных
сервисов.

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

При этом для каждого сервиса прописывается отдельная логика повторов запросов и кэширования результатов. Сами сервисы взаимодействуют через JSON RPC по http(s), как в любом web-мире. В результате даже при падении какого-либо сервиса вся система продолжает нормально работать, а пользователь ничего не замечает.

Компоненты

Kafka

Это не очередь сообщения, Kafka у нас — это только транспортный уровень между сервисами, с гарантией доставки и понятной надежностью/кластеризацией. Т.е. если из сервиса А в сервис В надо что-то переслать, проще положить сообщение в Kafkу, из Kafkи сервис сам заберет, что надо. И тогда можно и не думать обо всей этой логике для повторов и кэширования, Kafka сильно упрощает такое взаимодействие. Сейчас мы пытаемся как можно больше всего переводить на Kafka, это тоже такой непрерывный процесс.

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

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

Вообще, для платежных систем иметь два независимых хранилища данных — это стандартная практика, без нее жить просто страшно — мне, например.

Логи разработки

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

Мониторинг

Мониторинг у нас на Prometheus + Grafana. Честно говоря, Prometheu’ом я недоволен.

В чем проблема?

  1. Prometheus прекрасен, когда вам надо собирать из готовых стандартных компонентов какие-то данные, и у вас этих компонентов очень много. У нас довольно мало машин. У нас 40 разных сервисов и это примерно 150 виртуалок, это не очень много. Если же мы хотим собирать через Prometheus, какую-то бизнес-мониторинговую информацию, например количество платежей, идущих через определенный шлюз, или количество событий в нашей внутренней очереди, то приходится писать довольно много кода на стороне клиента. Причем код, к сожалению, не очень простой, разработчикам приходится активно разбираться во внутренней логике и том, как именно Prometheus что-то считает.
  2. Prometheus нельзя использовать как честный event oriented time-series db. Я не могу взять и сказать, что есть событие начала платежа, событие конца платежа, а все прочие метрики он пускай сам посчитает. Я вынужден на клиенте все нужные мне метрики заранее считать, и если вдруг мне надо какую-нибудь из них поменять, — это очередная выкладка продакшн-компонента, что очень неудобно.
  3. Очень сложно делать интегрированные метрики. Если мне надо собрать общую метрику для некоторого количества сервисов (например, перцентили времени отклика клиентам по всем серверам фронтенда), то через Prometheus сделать это нереально даже теоретически. Я могу только делать какое-то непонятное среднее суммирование уже на уровне Grafana. Сам Prometheus это сделать не может.

Поэтому я всерьез думаю куда-нибудь уйти.

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

Использование базы данных

Вообще, платеж — это довольно сложно. Ниже примерное описание контекста платежа: множество кортежей (ассоциативных массивов), списков кортежей, кортежей списков, каких-то параметров. И все это постоянно меняется из-за изменений бизнес-логики.

Как следствие, нужен ORM, нужна сложная логика миграции при добавлении колонки. Если это делать честно, будет много таблиц, много связей между ними. Т.е. на самом деле добавление nullable column не атомарная бесплатная операция, как многие думают. Напомню, что, в PostgreSQL даже простое добавление новой nullable колонки в таблицу может привести (в некоторых специфических ситуациях) к тому, что на долгое время данная таблица будет вообще недоступна. Мы на это даже разочек наткнулись.

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

Практики работы с JSON

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

Во-первых, надо сразу думать о возможных конфликтах.

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

В Java, к счастью, с такими сериализаторами проблем нет. Для решения этой задачи обычно достаточно найти хороший сериализатор / десериализатор, которому вы можете в явном виде сказать, что вот это поле из JSON надо преобразовать в такой-то набор полей, эти вещи сериализовывать так-то, а если чего-то нет, то заменить на значение по умолчанию и т.д. Мой любимый — Jackson.

Обязательно в базе данных надо хранить версию структуры, которую вы пишете.

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

Это избавление себя от легаси, избавление себя от технического долга. Когда вы выпустили новую версию, и у вас появилась новая структура данных, вы просто делаете скрипт миграции, который пробегает по всей базе, находит все старые версии структуры, читает их, записывает в новом формате, и у вас через какое-то довольно ограниченное время, в базе остается максимум 2-3 различные версии данных, и вы не мучаетесь с поддержкой всего разнообразия того, что у вас накопилось за многие годы.

Для PostgreSQL — надо выбрать между json и jsonb.

У нас, например, использовался JSON, потому что мы начинали довольно давно. Когда-то в этом выборе еще был смысл. Поэтому в продакшене лучше лишний раз внутрь json-объектов в базе данных не залезать, только в случае какой-то поддержки или исправления неисправностей. Напоминаю, что тип данных JSON — это просто текстовое поле, и чтобы залезть куда-нибудь внутрь, его каждый раз будет парсить PostgreSQL. По-хорошему, в вашем SQL-коде вообще не должно быть команд работы с json-полями.

Когда мы, например, храним оригинальные входящие к нам данные, мы всегда используем только JSON. Если же использовать JSONB, то PostgreSQL все аккуратненько разбирает в бинарный формат, но не сохраняет оригинальный вид JSON-объекта.

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

PCI DSS. Из простого сделать сложное, и как web становится enterprisе

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

Надежность сервиса реализовывали через active-standby — потому что сервис маленький, перезапускается быстро, 3-5 секунд прочие компоненты его точно подождут, поэтому нет смысла громоздить какую-то сложную кластерную систему.

Перед запуском мы начали проходить аудит PCI DSS, и выяснилось, что есть довольно жесткие требования контроля доступа к данным, которые, в случае нашего аудитора, сводились к тому, что:

  1. Не должно быть одного человека, который может прочесть всю информацию из базы данных. Это должны быть минимум несколько человек, которые совместно должны получать доступ.
  2. Требуется регулярная смена ключей доступа.
  3. PCI DSS требует при любой обнаруженной уязвимости обновлять инфраструктуру, а поскольку уязвимости в операционной системе и инфраструктурном ПО находятся довольно часто, значит, систему надо тоже обновлять довольно часто.

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

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

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

Для генерации ключа использутся отдельная виртуалочка, на которой: Понятно, что после перехода на схему Шамира в сервисе появляется логика генерации и смены ключей.

  • генерируется ключ,
  • раздается админам,
  • виртуалочка убивается.

В результате исходный ключ никто не может узнать, потому что он создается в присутствии СБ-шников, на быстроумирающей системе, а дальше раздаются уже только «порожденные» ключи

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

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

Из нормального простого веба мы радостно перешли в entreprise. Таким образом, простой очевидный сервис в несколько десятков строчек становится довольно сложной конструкцией: со сложной логикой старта, с кластеризацией, с довольно сложными инструкциями по сопровождению. Тем более, что топ-менеджмент и бизнес, посмотрев на все это дело, сказал, что теперь надо все данные на всякий случай шифровать примерно таким же образом, и Active-Active ему тоже всюду нравится. И, к сожалению, такое происходит довольно часто — гораздо чаще, чем хотелось бы. И эти желания бизнеса, прямо скажем, реализовать не всегда просто.

Логика платежа. Из сложного сделать простое

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

Они могут зависнуть у любого из контрагентов, главное, чтобы не у нас. Надежно — это значит, что мы всегда знаем, на чьей стороне деньги, а если у нас сейчас все упало, с кого эти деньги требовать. И мы должны точно знать, у кого они зависли так, чтобы все это можно было подтвердить и сообщить пользователю И, конечно, желательно, что бы всевозможных проблем было как можно меньше.

Finite State Machine


Разумеется, вначале у нас была FSM — нормальная стейт-машина, каждое событие обрабатывается в транзакции. Текущее состояние тоже сохраняем в СУБД. Реализовали все сами.

Первая проблема — у нас есть одновременные события.

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

Кроме того многие шаги имеют четкое максимальное время выполнения (тайм-аут), и эти времена тоже нужно где-то хранить, обрабатывать, смотреть, когда события наступления таймаутов происходят (и они тоже иногда бывают одновременными).

Как следствие, получилась система с организацией высокой доступности только через Active-Standby и с кучей специальной логики восстановления контекстов и тайм-аутов. Все это реализовывалось через логику блокировок внутри Java-машины, потому что в базе данных это было делать не очень легко.

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

Все было замечательно, но потребовался Active-Active.

Active-Active

Во-первых, мы захотели использовать схему Шамира, ну а также появились и другие хотелки: давайте мы будем выкладывать новую версию только на 3% пользователей; давайте часто менять логику платежа; хочется ее выкладывать с нулевым простоем и т.д.

И мы начали в очередной раз разбираться — что такое платеж? Делать распределенные блокировки — это грустно, делать тайм-ауты распределенные тоже грустно. Платеж — это множество событий, которые надо строго последовательно обрабатывать, это сложное изменяемое состояние, и обработка платежей должна идти параллельно.

Правильно, платеж — это актор. Кто узнал определение?

Есть прекрасная Akka, есть временами странный, но прикольный Vert.x, есть гораздо менее используемый Quasar. В Java много разных моделей акторов. Они все замечательные, но у них есть один фундаментальный недостаток (и не тот, о котором вы подумали) — у них недостаточные гарантии.

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

Мы долго на это смотрели, думали, не допилить ли нам что-нибудь до вменяемого состояния, но потом сделали свой велосипед: очередь в PostgreSQL через select for update skip locked.

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

Skip locked

Это такая прекрасная штука для реализации очередей в PostgreSQL. На самом деле данный механизм есть во всех базах данных, кроме, по-моему, MySQL.

События отсортированы по автоинкрементному ключу ID, все обычно. Предположим, у нас есть две таблички: табличка с нашими акторами — flow, и табличка событий для этих акторов, она связана по колонке flow. Пишем SQL-запрос.

Если никаких блокировок в табличке нету, запрос работает ровно как нормальный for update — берет и ставит блокировку на первую строчку, которую мы выбрали, т.е. на строку с первым актором и на строку с первым событием для этого актора. Выбираем самое первое событие в самом первом из flow, указав магическое for update skip locked.

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

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

На дешевом железе мы получали порядка 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 официально не сообщается, но, мягко говоря, немалая,. Vertica работает быстро, качественно, надежно, просто, а Tableau Server все это показывает. К счастью, выяснилось, что на наших объемах все это на самом деле не так и дорого, потому что до одного терабайта данных Vertica — халявная, Community Edition нас абсолютно устраивает, до терабайта нам еще далеко. Tableau тоже не очень дешевый. Вплоть до того, что нам нужно было меньше лицензий, чем минимальный пакет, который продает Tableau. А поскольку лицензия на Tableau нам нужна только на небольшое количество разработчиков и топ-менеджеров, то это стоит какие-то полне нормальные деньги.

Оно недорого и ставится с нуля, не задумываясь. Оказалось, что такое нормальное, совершенно классическое тяжелое enterprise-решение, является еще и нормальным web-решением. Пока у вас не очень много данных — советую. Vertica меня пока радует: в ней многие именно аналитические вещи решаются очень красиво. Впрочем, в эксплуатации она требовательна к пониманию принципов ее работы, нужно в них разобраться перед использованием.

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

Контент

У нас довольно много текстов:

  1. Юридическая информация, которую мы обязаны предоставлять, оферты и т.д.;
  2. Информация о контрагентах, включая комиссии, которые мы берем с пользователей;
  3. Инструкции для пользователей;
  4. Собственный маленький блог;
  5. Информация об ошибках и прочее.

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

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

Простые CМS не подходят, потому что: И мы начали думать, как бы все это автоматизировать и сделать CМS.

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

Не простые CМS — слишком дорогие во всех смыслах, они и стоят обычно больших денег, и их интеграция весьма неочевидна, потому что там напридумано много всего.

Но от мысли поставить Git топ-менеджерам и копирайтером и научить их им пользоваться, мы подумали-подумали и в ужасе отказались, потому что все-таки git не для нормальных людей. Идеальным решением было бы поставить у всех, кто работает с текстами, банальный Git: пусть они все написанные тексты отправляют прямо в репозитарий.

Но, к сожалению, JetBrains такого редактора до сих пор не сделали, хотя я давно их просил. Самое идеальное решение было бы, наверное, редактор текстов, встроенный прямо в IntelliJ IDEA, где можно сложность использования Git аккуратненько скрыть.

Пришлось опять делать велосипед:

  • Простой редактор текста.
  • Простой редактор html, потому что вместо сложного редактора текста проще попросить нашего же фронтендера сверстать все в html, но при этом выкладывать через систему CMS и систему контроля качества.
  • Простая концепция версий — есть пакеты изменений, публикация идет целиком пакетами, восстановление при необходимости всего контента тоже по одной кнопочке. И очень простой порядок работы с текстами.
  • Категорический запрет (просто нет такой кнопочки) что-нибудь изменить прямо на продакшне. Все можно изменить только через задачу в Jira, дальше она попадает в тестовую версию, с тестовой версии на продакшен уже переносят отдельно обученные люди — все, что они могут, это переносить готовые пакеты с тестовой версии на продакшн, после того как получены все подтверждения, без возможности вносить правки.

То есть пришлось написать, по сути дела, микро-портальчик в стиле enterprise.

Но ни одной embedded CMS для больших систем я на рынке просто не нашел, и, по-моему, их до сих пор нет. Честно говоря, если бы я мог такое решение купить — я предпочел бы это купить. Я, честно говоря, уже который раз пишу ее с нуля, и очень жалко, что до сих пор никто это не сделал за меня.

Выводы


Что из всего этого можно сказать? Что жизнь на грани довольно интересна.

Иногда можно заимствовать не только идеи, но и конкретные решения типа как Vertica, если они дешевые. Когда у вас задачи одновременно и из web, и из enterprise, можно заимствовать разные идеи из мира корпораций, у них довольно много вещей продумано.

Конечно, можно кого-нибудь переманить из Почты России, но они привыкли к настолько большим серверам, что мы явно для них слишком мелкие. Честно, если бы я нашел дешевую поддержку для IBM DB2 — я бы реализовывал бы проект на ней, я ее очень люблю, она дешевая и очень надежная, но найти поддержку этой базы за разумные деньги в России сложно.

Ну и большие проблемы из мира enterprise можно решать в web-стиле довольно просто, чем мы постоянно занимаемся.

Архитектура — понятие динамическое.

Бывает архитектура, которая более-менее вас удовлетворяет в конкретный момент времени. Не бывает хорошей архитектуры вообще. Архитектура проекта — это процесс, а не результат. Время меняется — архитектура меняется, и надо постоянно быть в этому готовым, и всегда вкладывать ресурсы в развитие архитектуры.

Мы умеем, поэтому у нас все получается просто, быстро, ненапряжно, и очень небольшой командой мы делаем довольно сложны Java и SQL — это реально круто, если вы умеете это готовить.

Новости

Даты проведения HighLoad++ 2018 переносятся на 8 и 9 ноября, чтобы разминуться с Percona Live.

Прием заявок на Highload++ Siberia, которая пройдет 25 и 26 июня в Новосибирске, уже закончился, поданные доклады опубликованы на сайте, цена билетов уже не минимальная, но еще и не слишком выросла — идеальный момент для бронирования.

В тему сегодняшней статьи можно отметить такие заявки: До РИТ++ осталось совсем немного времени, программа активно формируется.

  • «Возможности ClickHouse для продвинутых» от собственно разработчика ClickHouse Алексея Миловидова.
  • Юрий Лилеков с докладом о том, зачем разработчику статистика, или как улучшить качество продукта?
  • Александр Сербул расскажет об особенностях lambda-архитектур, платформе микросервисов Amazon Lambda, а также подводных камнях и победах с Node.JS и многопоточной Java.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть