Payment gateway A/B test with HAProxy
Last updated
Last updated
https://habr.com/ru/companies/avito/articles/336508/
Мы в Avito внимательно следим за развитием других классифайдов по всему миру. И конечно, нам интересны лучшие практики работы с такой непростой системой, как биллинг. Сегодня я публикую перевод поста моего коллеги по группе Naspers (Avito входит в её состав), М. Рафай Алема, инженера-программиста из Dubizzle. Это ведущий сайт объявлений в ОАЭ, входит в OLX Group — сеть крупнейших онлайн-рынков в 45 странах мира с более чем 1,9 млрд. посещений, 37 млрд. просмотров страниц и 54 млн. объявлений ежемесячно. Тема заинтересует всех, кто занимается созданием и развитием собственного платежного сервиса.
Представьте, что вам нужно переписать существующий веб-сервис, чтобы перейти на новый платежный шлюз (payment service provider) по различным практическим причинам. Вашей первой мыслью, вероятно, будет полностью заменить старый шлюз на новый и запустить его. Но это немного наивно, особенно если вы работаете с платежными шлюзами, у которых есть соглашения об уровне обслуживания, соглашения с обслуживающими банками, скрипты для обнаружения рисков и мошеннических действий и т.д. Эти факторы делают процесс перехода более рискованным с точки зрения операций, доходов, удержания клиентов и, в конечном итоге, успеха бизнеса. В этом посте мы рассмотрим подход, который применили мы, чтобы снизить риски при смене платежного шлюза, и почему это так важно.
Страница оформления заказа Dubizzle
Наша старая платежная система написана на Python 2 и тесно связана со старым платежным шлюзом. Когда мы впервые взялись за проблему, подумали, что интегрировать новый платежный шлюз в существующие потоки, URL-адреса и Python Bottle будет несложно. Начав работать над первым прототипом, мы поняли, что создаем спагетти-код, поскольку потоки API у этих двух платежных шлюзов были совершенно разными. В API старого платежного шлюза было иначе: для оптимизации обработки пользователей и платежей в значительной степени использовались Redis и Gevent, повторять это в API нового шлюза не было абсолютно никакой необходимости.
Платежный сервис в старом платежном шлюзе
A/B тесты очень важны для принятия решений при разработке продукта. В Dubizzle такие тесты, как правило, больше ориентированы на то, с чем сталкивается пользователь: на переходы между страницами, на компоненты на страницах и их расположение, на фичи. Однако они усложняют ситуацию, если вы хотите протестировать базовые системы, которые сильно зависят друг от друга.
Работая над прототипом, мы поняли, что A/B тест, который мы хотели провести, не должен выполняться с использованием таких инструментов, как Optimizely, даже если бы нам удалось каким-то образом интегрировать новый шлюз в те же пользовательские отображения и потоки. И вот почему.
Использование внешнего JavaScript-кода в платежном сервисе, который может фиксировать данные кредитных карт, представляет собой огромную угрозу для безопасности.
Применение подхода Optimizely потребовало бы от нас реализации логики A/B тестирования для каждого отображения веб-сервиса, чтобы обеспечить направление запросов ото всех отображений транзакции в нужный платежный шлюз с использованием cookie. Это потребовало бы следующего кода для каждого отображения и выглядело бы не очень аккуратно:
При отладке двух разных платежных шлюзов, использующих одни и те же пространства имен (например, в Redis), генерации и регистрации UUID неизбежно возникли бы проблемы, и нам мгновенно пришлось бы их решать.
Если пользователь оказывается в контрольной группе A (веб-сервис, взаимодействующий со старым платежным шлюзом), этот веб-сервис должен позаботиться о том, чтобы процесс оплаты этого пользователя продолжался в том же платежном шлюзе, в котором он был начат. Так, веб-сервис не должен инициировать транзакции в старом платежном шлюзе и пытаться завершить их в новом. Эту проблему можно решить, используя sticky sessions, которые поддерживаются Optimizely и HAProxy.
Когда мы начали понимать проблему, с которой столкнулись, мы решили разработать новый платежный сервис, интегрированный с новым шлюзом, и сравнить их производительность с помощью A/B теста (50/50). A/B тест должен был соответствовать, по меньшей мере, следующим требованиям:
Внешний вид формы ввода данных платежной карты в новом сервисе должен точно соответствовать старой форме, чтобы ни одна операция не была нарушена. В данном случае под операцией понимается нажатие на кнопку “Оплатить”.
Никакие внутренние серверные задачи или вызовы API не должны увеличивать количество сбоев транзакций в новом сервисе по сравнению со старым. Это означает, что большая часть пользовательских процессов должна, по существу, протекать аналогично тому, как это происходит с использованием старого шлюза.
Платежный сервис, интегрированный с новым платежным шлюзом
Исключив Optimizely, мы решили использовать для проведения теста HAProxy. Это мощный балансировщик нагрузки четвёртого и седьмого уровней сетевой модели OSI с широким набором функций. Одной из таких функций является возможность привязки запроса к бэкенду с использованием cookie на седьмом уровне.
Мы настроили HAProxy на три бэкенда:
Основной бэкенд (main)
Бэкенд старого сервиса (old service)
Бэкенд нового сервиса (new service)
Ниже приведен пример бэкенда HAProxy:
Возможно, вы подумали: зачем нам статичные бэкенды? Это станет понятно, когда мы доберемся до frontend HAProxy, поэтому давайте сперва рассмотрим бэкенд main.
Мы выбрали Weighted Round Robin (WRR), чтобы сбалансировать нагрузку на old-service-old-pg и new-service-new-pg. Это имеет смысл в A/B тесте, где нужно просто разделить трафик между группами A и B, учитывая, что запрос, попадающий в группу A, не должен попасть в группу B на протяжении всей сессии. Мы добились этого с помощью директивы cookie HAProxy. Допустив, что любой пользователь, который инициирует транзакцию, завершит её в течение 2 часов, мы настроили HAProxy так, чтобы cookie удалялась через 2 часа и создавалась новая на основе результатов WRR.
Это дало нам два очень важных результата:
Мы смогли настраивать весовые коэффициенты в A/B тесте с охватом всех пользователей в течение 2 часов без нарушения сессий или пользовательских потоков.
Повысили вероятность того, что пользователь испробует транзакции в обоих сервисах в течение всего времени A/B теста. Это помогло нам в выявлении проблем, связанных с тем, что один шлюз отказывался принимать кредитную карту, которая была до этого принята другим шлюзом. Мы были поражены, увидев, как все может начать рушиться при большем масштабе, когда задействовано несколько каскадных систем на разных континентах.
В нашей схеме есть небольшая уязвимость, которую вы, вероятно, пока не заметили. Рассмотрим следующую схему:
Процесс оплаты через HAProxy
Пользователь начинает транзакцию в старом платежном сервисе на отметке 45 минут после получения cookie. Затем он переходит на страницу 3-D Secure банка на отметке 1 час 59 минут и перенаправляется обратно на наш URL-адрес страницы успешной оплаты после отметки 2 часа. Поскольку HAProxy настроен на maxlife cookie 2 часа, HAProxy отменяет cookie сессии и пытается вставить новый после перенаправления на URL-адрес страницы успешной оплаты. Если нам не повезет, WRR может привязать новую сессию к новому платежному сервису, который не знает, как обрабатывать перенаправление на страницу успешной оплаты, инициированное старым сервисом.
В нашем случае мы решили игнорировать эту проблему, так как знали по опыту, что пользователи, инициировавшие транзакцию, обычно завершают ее в течение двух часов. Но представьте, с какой проблемой мы столкнулись бы, задав параметр maxlife
равным, например, 1 минуте?
Давайте вернемся к разговору о том, почему мы использовали бэкенды old_service
и new_service
, а также main
. В HAProxy обычно конфигурируют прокси frontend
, который обрабатывает все списки контроля доступа (ACL). В нашем случае это выглядело так:
Эти вебхуки и эндпоинты, которые можно видеть в конфигурации, представляют собой конкретные правила, необходимые для обработки запросов, исходящих из внешних шлюзов. Поскольку старый и новый сервисы не способны взаимодействовать с платежными шлюзами друг друга, применение WRR для балансировки нагрузки приведет к тому, что почти все запросы дадут 404-ю или 400-ю ошибку. Кроме того, поскольку эти запросы поступают из платежных шлюзов, в персистентности нет никакой необходимости, потому что они не содержат сценариев, охватывающих различные правила (все запросы обрабатываются мгновенно при получении кода 200).
Лучшее место для решения этой проблемы — сам балансировщик, поэтому мы настроили ACL, чтобы направлять поток запросов на соответствующие серверы приложений через статичные бэкенды old_service
и new_service
. Иными словами, происходит своего рода беседа между платежным шлюзом и HAProxy, который должен перенаправить запрос на соответствующий сервер приложений.
Платежный шлюз: Привет, я внутренний запрос от старого платежного шлюза, и я сообщаю, что успешно получил платеж.
Теперь, когда все проблемы решены, стоит задаться вопросом, как протестировать реализацию такого сложного A/B теста.
HAProxy ведет очень подробные логи для отладки сложных систем. Рассмотрим несколько примеров из A/B теста.
Обратим внимание на флажки NI, VU и VN в каждом запросе в отношении одного идентификатора заказа. Эти флажки позволяют понять как клиент, сервер и HAProxy обработали cookie. Это наиболее важная информация, на которую стоит обратить внимание при тестировании и отладке.
Процитируем документацию HAProxy:
—:
NI :
WRR определит группу пользователя.
VU :
VN:
Так HAProxy находит текущую группу для пользователя и направляет его в соответствующий бэкенд.
Обратите внимание, что когда HAProxy выбирает сервер new-service-new-pg
и задает cookie, все последующие запросы от этого пользователя направляются на new-service-new-pg
через бэкенд main
.
Если запрос соответствует одному из списков ACL, HAProxy направляет запрос без установки cookie. Это также гарантирует, что результаты A/B теста не будут искажены HTTP-вызовами, исходящими от ботов.
Архитектура A/B теста
DNS и граничный узел — общие для всех наших микросервисов. Вся магия A/B-теста начинается после того, как трафик пройдет через вышестоящий балансировщик нагрузки (тоже HAProxy).
Хотя для нас такое решение подошло как нельзя лучше, есть множество других способов более сложной балансировки не только с использованием привязки по Cookie. Shopify очень хорошо описал решение с Nginx и OpenResty в своем блоге.
Благодарю автора, который любезно дал согласие на перевод и публикацию здесь. Можно связаться с ним напрямую в твиттере: mrafayaleem.