Мы в 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 тесты хороши, пока они просты
A/B тесты очень важны для принятия решений при разработке продукта. В Dubizzle такие тесты, как правило, больше ориентированы на то, с чем сталкивается пользователь: на переходы между страницами, на компоненты на страницах и их расположение, на фичи. Однако они усложняют ситуацию, если вы хотите протестировать базовые системы, которые сильно зависят друг от друга.
Работая над прототипом, мы поняли, что A/B тест, который мы хотели провести, не должен выполняться с использованием таких инструментов, как Optimizely, даже если бы нам удалось каким-то образом интегрировать новый шлюз в те же пользовательские отображения и потоки. И вот почему.
- Использование внешнего JavaScript-кода в платежном сервисе, который может фиксировать данные кредитных карт, представляет собой огромную угрозу для безопасности.
- Применение подхода Optimizely потребовало бы от нас реализации логики A/B тестирования для каждого отображения веб-сервиса, чтобы обеспечить направление запросов ото всех отображений транзакции в нужный платежный шлюз с использованием cookie. Это потребовало бы следующего кода для каждого отображения и выглядело бы не очень аккуратно:
if cookie == ‘OLD’:
use old payment gateway API
else:
use new payment gateway API
- При отладке двух разных платежных шлюзов, использующих одни и те же пространства имен (например, в Redis), генерации и регистрации UUID неизбежно возникли бы проблемы, и нам мгновенно пришлось бы их решать.
Если пользователь оказывается в контрольной группе A (веб-сервис, взаимодействующий со старым платежным шлюзом), этот веб-сервис должен позаботиться о том, чтобы процесс оплаты этого пользователя продолжался в том же платежном шлюзе, в котором он был начат. Так, веб-сервис не должен инициировать транзакции в старом платежном шлюзе и пытаться завершить их в новом. Эту проблему можно решить, используя sticky sessions, которые поддерживаются Optimizely и HAProxy.
История о двух платежных сервисах
Когда мы начали понимать проблему, с которой столкнулись, мы решили разработать новый платежный сервис, интегрированный с новым шлюзом, и сравнить их производительность с помощью A/B теста (50/50). A/B тест должен был соответствовать, по меньшей мере, следующим требованиям:
- Внешний вид формы ввода данных платежной карты в новом сервисе должен точно соответствовать старой форме, чтобы ни одна операция не была нарушена. В данном случае под операцией понимается нажатие на кнопку “Оплатить”.
- Никакие внутренние серверные задачи или вызовы API не должны увеличивать количество сбоев транзакций в новом сервисе по сравнению со старым. Это означает, что большая часть пользовательских процессов должна, по существу, протекать аналогично тому, как это происходит с использованием старого шлюза.
Платежный сервис, интегрированный с новым платежным шлюзом
A/B тест с помощью HAProxy
Исключив Optimizely, мы решили использовать для проведения теста HAProxy. Это мощный балансировщик нагрузки четвёртого и седьмого уровней сетевой модели OSI с широким набором функций. Одной из таких функций является возможность привязки запроса к бэкенду с использованием cookie на седьмом уровне.
Мы настроили HAProxy на три бэкенда:
- Основной бэкенд (main)
- Бэкенд старого сервиса (old service)
- Бэкенд нового сервиса (new service)
Ниже приведен пример бэкенда HAProxy:
backend main
balance roundrobin
cookie SERVERID insert indirect nocache maxlife 2h
option forwardfor
option http-server-close
option http-pretend-keepalive
timeout queue 5000
timeout connect 5000
timeout server 50000
server old-service-old-pg old-service.eu-west-1.elasticbeanstalk.com:80 weight 128 cookie old_v1 check
server new-service-new-pg new-service.eu-west-1.elasticbeanstalk.com:80 weight 128 cookie new_v1 check
backend old_service
option forwardfor
option http-server-close
option http-pretend-keepalive
timeout queue 5000
timeout connect 5000
timeout server 50000
server old-service-old-pg-static old-service.eu-west-1.elasticbeanstalk.com:80 check
backend new_service
option forwardfor
option http-server-close
option http-pretend-keepalive
timeout queue 5000
timeout connect 5000
timeout server 50000
server new-service-new-pg-static new-service.eu-west-1.elasticbeanstalk.com:80 check
Возможно, вы подумали: зачем нам статичные бэкенды? Это станет понятно, когда мы доберемся до 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). В нашем случае это выглядело так:
frontend all
bind *:80
timeout client 50000
default_backend main
acl is_old_webhook path_reg ^/webhook-old.*
acl is_refund path_reg ^/refund-endpoint.*
acl is_new_webhook path_reg ^/webhook-new.*
use_backend old_service if is_old_webhook
use_backend new_service if is_refund
use_backend new_service if is_new_webhook
Эти вебхуки и эндпоинты, которые можно видеть в конфигурации, представляют собой конкретные правила, необходимые для обработки запросов, исходящих из внешних шлюзов. Поскольку старый и новый сервисы не способны взаимодействовать с платежными шлюзами друг друга, применение WRR для балансировки нагрузки приведет к тому, что почти все запросы дадут 404-ю или 400-ю ошибку. Кроме того, поскольку эти запросы поступают из платежных шлюзов, в персистентности нет никакой необходимости, потому что они не содержат сценариев, охватывающих различные правила (все запросы обрабатываются мгновенно при получении кода 200).
Лучшее место для решения этой проблемы — сам балансировщик, поэтому мы настроили ACL, чтобы направлять поток запросов на соответствующие серверы приложений через статичные бэкенды old_service
и new_service
. Иными словами, происходит своего рода беседа между платежным шлюзом и HAProxy, который должен перенаправить запрос на соответствующий сервер приложений.
Платежный шлюз: Привет, я внутренний запрос от старого платежного шлюза, и я сообщаю, что успешно получил платеж.
HAProxy: Привет, я тебя знаю. Пройди в дверь old_service для доступа к целевому приложению.
Целевое приложение: Привет, я знаю, как с тобой быть. Давай активируем заказ этого пользователя. За это я выдам тебе код 200.
Мониторинг потоков запросов
Теперь, когда все проблемы решены, стоит задаться вопросом, как протестировать реализацию такого сложного A/B теста.
HAProxy ведет очень подробные логи для отладки сложных систем. Рассмотрим несколько примеров из A/B теста.
Feb 22 09:16:39 ip-1-x-x-x haproxy[9046]: 1.x.a.c:2459 [22/Feb/2017:09:16:25.623] all main/new-service-new-pg 13783/0/0/101/13884 200 8241 - - --NI 22/22/0/1/0 0/0 "GET /step1/1725770 HTTP/1.1"
Feb 22 09:18:23 ip-1-x-x-x haproxy[9046]: 1.x.a.c:2629 [22/Feb/2017:09:18:12.291] all main/new-service-new-pg 7223/0/1/3881/11105 200 327 - - --VU 19/19/0/1/0 0/0 "POST /step2/1725770 HTTP/1.1"
Feb 22 09:19:30 ip-1-x-x-y haproxy[9402]: 1.x.b.z:59238 [22/Feb/2017:09:19:26.761] all main/new-service-new-pg 3714/0/1/45/3760 200 4711 - - --VN 34/34/0/1/0 0/0 "GET /success-redirect/1725770 HTTP/1.1"
Feb 22 09:16:51 ip-1-x-x-x haproxy[9046]: 1.x.a.c:2459 [22/Feb/2017:09:16:49.146] all new_service/new-service-new-pg-static 2047/0/0/5/2052 200 226 - - ---- 29/29/0/1/0 0/0 "POST /webhook-new HTTP/1.1"
Обратим внимание на флажки NI, VU и VN в каждом запросе в отношении одного идентификатора заказа. Эти флажки позволяют понять как клиент, сервер и HAProxy обработали cookie. Это наиболее важная информация, на которую стоит обратить внимание при тестировании и отладке.
Процитируем документацию HAProxy:
—: Привязка по cookie отключена. Это тот случай, когда мы не проводим A/B-тест.
NI : Cookie не выставлена клиентом и была сгенерирована в ответ. Обычно это происходит при первом запросе от каждого пользователя и позволяет подсчитать количество реальных пользователей. WRR определит группу пользователя.
VU : Cookie предоставлена клиентом; последняя дата посещения устарела, поэтому в ответ предоставлена обновленная cookie. Это также возможно, если даты нет вообще или она указана, но параметр maxidle не задан и cookie может быть задан неограниченный срок.
VN: Cookie предоставлена клиентом. Имеет место в большинстве ответов, если у клиента уже есть cookie. Так HAProxy находит текущую группу для пользователя и направляет его в соответствующий бэкенд.
Обратите внимание, что когда HAProxy выбирает сервер new-service-new-pg
и задает cookie, все последующие запросы от этого пользователя направляются на new-service-new-pg
через бэкенд main
.
Если запрос соответствует одному из списков ACL, HAProxy направляет запрос без установки cookie. Это также гарантирует, что результаты A/B теста не будут искажены HTTP-вызовами, исходящими от ботов.
Архитектура A/B теста
Архитектура A/B теста
DNS и граничный узел — общие для всех наших микросервисов. Вся магия A/B-теста начинается после того, как трафик пройдет через вышестоящий балансировщик нагрузки (тоже HAProxy).
Заключение
Хотя для нас такое решение подошло как нельзя лучше, есть множество других способов более сложной балансировки не только с использованием привязки по Cookie. Shopify очень хорошо описал решение с Nginx и OpenResty в своем блоге.
От переводчика
Благодарю автора, который любезно дал согласие на перевод и публикацию здесь. Можно связаться с ним напрямую в твиттере: mrafayaleem. А если вам хочется развивать подобные сервисы, посмотрите наши вакансии на Моем Круге: нам в Avito нужны разработчики высоконагруженных систем, в т. ч. разработчики биллинга.
Приходилось ли сталкиваться с такими задачами? Давайте обсуждать в комментариях.
Автор: Виталий Леонов