Pull to refresh
HFLabs
Повышаем качество клиентских данных для бизнеса

Как живут клиентские данные в Yota

Reading time 6 min
Views 2.9K

Привет, Хабр! 10 лет назад мы запустили систему Customer Data Integration (CDI) в Yota. Речь о софте для обработки персональных данных абонентов. CDI в Yota работает совсем не так, как у других. У них это центральная система и единственная, где хранится информация об абонентах. Дальше я расскажу, как это всё устроено, на чём мы ломали копья, и почему всё вышло так, а не иначе.

Меня зовут Никита Назаров, я технический директор HFLabs. Мы работаем с данными, внедряем CDI в банках, страховых, телекоме.  

Как выглядит классическое внедрение CDI? Сердце любого телеком-оператора с точки зрения данных — биллинг. В нём хранятся счета абонентов. Основная сущность биллинга — лицевой счёт, к которому привязаны MSISDN (абонентский номер), ICCID (ID сим-карты) и персданные. Помимо биллинга обычно есть ещё несколько баз данных. Все они заливают в CDI данные клиентов. На их основе формируются те самые «золотые карточки» с актуальными данными. Без дубликатов и неверных сведений. 

В 2012 году, когда мы только пришли в Yota, у них был биллинг со счетами и клиентскими данными. А еще — старенькая CRM, где также хранилась информация о клиентах. Для продавцов был отдельный портал, данные оттуда также ехали в биллинг, а из биллинга в CRM. Также клиенты могли регистрироваться на сайте, и эти данные в итоге тоже оказывались в биллинге. При этом Yota жила с прицелом, что набор сервисов будет расти. А значит, будет много точек, где данные о клиентах меняются. 

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

Идея

В те времена когда деревья были большими основным продуктом у оператора были модемы, в просторечье — свистки. А ещё был проект создания банка. Первоначально внедрять CDI начали в одном из подразделений — удостоверяющем центре (УЦ). Фишка была в том, что клиента сначала идентифицировали в УЦ, и дальше он мог свисток оформить или пойти за услугами в банк. 

Ребята из Yota считали, что всё нужно делать быстро. Буквально — в реал-тайме. Так и появилась мысль: а если у нас есть CDI, и он умеет работать в режиме реального времени, зачем мы везде храним данные? Давайте их отовсюду уберём! 

Как раз в этот момент оператор начинал внедрять новую CRM. И сразу решил, что в ней будет только ID сим-карты. А если нужно на экране показать данные пользователя, пусть CRM идет в CDI за карточкой клиента. Дальше они, насколько я в курсе, спорили с Microsoft и продавили именно такое решение.

Тут сделаю отступление. За последние годы я не раз слышал от заказчиков: если делать нормально, то сначала внедрять CDI, а потом CRM. А не наоборот, как чаще всего и бывает. Переделывать уже внедрённую CRM нерационально. Мы сейчас всем советуем не ломать существующие системы, но новые строить как раз по такой схеме. 

Итак, Yota принципиально решила не создавать никакие решения с персональными данными. Начали с CRM, потом выпилили персональные данные из других источников. В итоге даже портал агента теперь льётся сразу в CDI. И где бы у тебя клиент не появился, его сразу все видят.

Под капотом CDI

CDI в Yota (в данном случае мы говорим о нашем «Едином клиенте») — система уровня Mission critical. Для её работы важны две вещи: 

  • реал-тайм. Под ним все понимают разное, иногда это и 20 секунд на поиск. Но в нашем случае — сотня миллисекунд на поиск максимум; 

  • отказоустойчивость. 

В самом простом сценарии решение CDI развернуто на двух серверах — сервере базы данных и сервере приложения. Схематично это выглядит вот так: 

Первый челлендж — быстрый поиск. Полнотекстовый поиск в базе данных (БД) на тот момент работал плохо, поэтому мы реализовали его на уровне приложения с помощью библиотеки Apache Lucene: все изменения клиентских данных сохраняются и в БД, и в поисковом индексе самого приложения. Кстати, за счёт такого подхода к написанию кода, мы легко переехали с Oracle на PostgreSQL: нам не пришлось переписывать никаких хранимых процедур и функций, обошлись адаптацией sql-диалекта в паре особо специфичных мест.

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

При запуске любого изменения в БД открывается транзакция. Дальше идет атомарный блок бизнес-логики: блокируем клиента и его атрибуты, что-то меняем… Финал — фиксация изменений в БД или откат транзакции целиком. Те данные, которые успешно сохранились в БД, должны попасть в поисковый индекс приложения. По умолчанию запись в поисковый индекс отвязана от БД и выполняется асинхронно. Это значит, что в памяти приложения накапливаются изменения и с определённой периодичностью сбрасываются на диск. Это могло занимать до 10 секунд, и только после этого данные становились доступными для внешних потребителей. 

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

Едем дальше. Сгорел сервер, что делать?

Теперь про отказоустойчивость. У Yota на тот момент была лицензия на Oracle. Она позволяет нескольким экземплярам Oracle работать с общей БД, при этом все они имеют эквивалентное состояние. Мы пошли по аналогичному пути.

Под «Единым клиентом» есть два сервера приложений — назовём их ЕК1 и ЕК2. Их возможности идентичны. Обе эти ноды обладают полным слепком данных и полным набором функциональных возможностей. Но если в схеме с одной базой данных и одним приложением мы можем гарантировать одинаковые состояния БД и поискового индекса, то для независимо работающих нод всё чуть сложнее. Да, теоретически мы могли бы обеспечить идентичное состояние поисковых индексов приложений, но это, прямо скажем, очень дорого с точки зрения производительности. В итоге каждая из нод формирует события по изменениям, которые обрабатываются на ней, и сохраняет их в локальную очередь. И все наши ноды читают события друг друга и применяют изменения к собственным поисковым индексам.

На схемах показал, как это происходит, если данные клиента меняются: 

Это начальное состояние: в системе-источнике с идентификатором 1 лежит запись с именем Вася.  Эта запись сохранена в «Едином клиенте» и в БД, состояние поисковых индексов приложений идентично.
Это начальное состояние: в системе-источнике с идентификатором 1 лежит запись с именем Вася.  Эта запись сохранена в «Едином клиенте» и в БД, состояние поисковых индексов приложений идентично.
Теперь в источнике имя записи 1 изменилось на Петя. Балансировщик отправляет запрос в приложение 1, оно обрабатывает запрос, сохраняет изменения в БД и формирует событие, описывающее это изменение, в локальной очереди. Приложение 2 читает эту очередь и применяет аналогичные изменения к собственному поисковому индексу.
Теперь в источнике имя записи 1 изменилось на Петя. Балансировщик отправляет запрос в приложение 1, оно обрабатывает запрос, сохраняет изменения в БД и формирует событие, описывающее это изменение, в локальной очереди. Приложение 2 читает эту очередь и применяет аналогичные изменения к собственному поисковому индексу.

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

Такое решение можно назвать не самым оптимальным — зачем целую копию поискового индекса держать на каждом инстансе приложения? Давайте просто нарежем индекс на отдельные сегменты и по красоте разложим их на соответствующее число приложений. Сложность в том, что нечёткий полнотекстовый поиск предполагает, что мы будем искать сразу по всем данным: по паспорту, по ФИО, по номеру телефона... Всего у нас больше десятка разных атрибутов, то есть ключей кластеризации для каждой записи. Про то, насколько сложнее становится разворачивать и управлять таким решением, даже говорить не стоит. Классическая задачка по шардированию, скажете вы? У нас нет красивого ответа, поэтому с радостью обсудим ваш опыт в решении аналогичных задач в комментариях))

Диагностика

Вообще в проекте мы многое меняли по ходу пьесы. И, признаюсь, много чему сами научились. Например, в части работы с библиотеками для формирования поискового индекса и обеспечения SLA. А ещё неплохо прокачали диагностику.

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

а) сверить количественные показатели — сравнить общее число записей в базе данных и поисковом индексе;

б) сверить контент. Например, можно посмотреть, что по конкретному ID в базе лежит ФИО и адрес, а в кэше только ФИО. По итогам такой сверки можно выбрать кривые записи и точечно актуализировать их состояние. 

Начинали мы, кстати, с того, что умели делать в «Едином клиенте» только полное перестроение поискового индекса, блокирующее операции записи. А затем перешли ещё и к точечному конкурентному перестроению по блоку записей, так как появились заказчики, у которых за сутки, пока мы кэш перестраивали, накапливалось слишком много непримененных изменений. Индекс в этом случае находится в состоянии eventually consistent.

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

Кстати, о данных мы рассказываем не только на Хабре, но и в нашем телеграм-канале, заходите https://t.me/hflabs_official 

Tags:
Hubs:
+14
Comments 15
Comments Comments 15

Articles

Information

Website
www.hflabs.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
Максим Пименов