Около 5 лет назад я участвовал в миграции OCS с Oracle на Apache Ignite и обновлении стека самого чарджинга. Это была огромная по масштабам задача, так как она существенно меняла не только основную базу данных, но и систему доставки CDRs и модернизировала стек. Задача была выполнена. Хочу рассказать об одной интересной проблеме, которая связана с NATS — его решили использовать как очередь для экспорта CDRs в Hadoop.
В старой версии OCS Oracle выполнял роль временного хранилища CDRs, и особых проблем не было — связка работала годами. Так как от Oracle отказались, было решено модернизировать доставку CDRs и использовать NATS. NATS согласно документации был современным и дешёвым решением, а документация обещала отсутствие серьёзных проблем. В реальности же NATS никто не тестировал — просто посмотрели документацию, сделали небольшой стресс‑тест и решили: идём в NATS, тем более там есть поддержка кластера и, как казалось тогда, это должно работать без проблем. Сказано — сделано.

В подобной архитектуре ничего не предвещало беды, но требования были следующими
CDRs должны поступать в Hadoop согласно времени генерации
NATS не должен перемешивать CDRs
CDRs не должны дублироваться
В случае краха мастера всё должно переезжать автоматически или с минимальными усилиями со стороны поддержки.
NATS должен хранить события около суток (использовалась персистентность)
При анализе требований всё выглядело нормально: была сильная вера в то, что NATS справится. Не буду драматизировать, но все тесты включая стресс‑тест — 7 дней под 80% нагрузкой, — система прошла успешно. Если бы всё было хорошо, статьи про это не было бы.
Первый продакшн
Решение было поставлено в продакшн, проработало несколько месяцев — и начались первые сюрпризы:
Выяснилось, что по какой‑то причине NATS начал дублировать события, и экспортеры стали получать дубликаты. Поначалу казалось, что что‑то не так, но проблема с дубликатами нарастала.
Обнаружилось, что иногда при невыясненных обстоятельствах события застревали в очередях и случалось переупорядочивание. Причины мы так и не смогли выяснить, думаю, что проблема не решена до сих пор.
Иногда по какой‑то причине NATS отклонял входящие события. Иногда это случалось из‑за того, что кластер разрушался под нагрузкой, а иногда без видимых причин.
Проводились разные эксперименты с разными топиками, очередями и прочим. Но ни одно решение не давало стабильный результат, и тесты в лабе порой проходили по 2–3 недели, чтобы понять — а работает ли NATS или нет.
Наши недооценки
Мы напрасно заложились на то, что NATS будет неким Silver Bullet и всё будет так, как мы запланировали. В результате наших исследований выяснено:
NATS гарантирует естественный порядок событий в очереди: кто первый пришёл, тот первый вышел. Но иногда из‑за конкуренции экспортеров под трафиком возникают дублирования.
NATS не способен сортировать события по таймстемпу, который лежит внутри события. Он его просто игнорирует и сортирует строго по времени поступления.
Сериализация в JSON оказалась достаточно затратной и под нагрузкой вносила серьёзный overhead на обработку событий самого OCS, которые более важны, чем CDRs.
Как мы решили эту проблему и что из этого вышло
Было решено писать обёртку, которая задерживала события от экспортера и сама выполняла переупорядочивание.
Обёртка работала с Hadoop и сама проверяла, не задублировал ли что‑то NATS.
Обёртка стала неподдерживаемой — любое вмешательство или неосторожность могла разрушить всё.
Обёртка не могла гибко скейлиться, так как она не могла общаться с соседним экспортером. Решения из серии «загрузить в БД или куда‑то ещё» не прошли валидацию из‑за того, что решение получалось громоздким и потребляло много ресурсов.
В итоге мы потеряли scaling для экспортера
Итоги
На данный момент я знаю, что проблема с NATS и экспортом CDRs не решена. Обёртка превратилась в некого монстра, но продолжает работать, и в ней находят новые проблемы. На переработку решения времени не выделяют. Тестирование превратилось в квест.
Как бы я решал это сейчас
Если бы эта проблема решалась сейчас, я бы предложил собственное решение — HurriCache. HurriCache позволяет решать подобные задачи. Из набора контейнеров HurriCache я бы предложил использовать OrderedMap, в качестве веса — таймстемп из CDRs. Экспортеры вычитывали бы данные, используя атомарную операцию streamElementInRangeOrderedMap по таймстемпу. Импортеры использовали бы вставку через addElementOrderedMap, используя таймстемп в качестве веса. В HurriCache вес — это uint64.
Архитектура бы выглядела следующим образом

Как это решило бы старые проблемы
Проблема дубликатов. На схеме экспортеры используют операцию
getAndRemoveElementWithWeight. Это атомарная операция: элемент читается и удаляется из OrderedMap за один шаг. Если экспортер упадёт сразу после чтения, но до удаления (как это было бы при неатомарной операции), при рестарте он забрал бы тот же CDR — или следующий, который уже мог забрать его коллега. В старой схеме экспортер был один из‑за сложной обёртки, здесь мы получили бы scaling из коробки.Проблемы падения. Если экспортер упал во время операции — это не решено ни в прошлой схеме, ни в текущей.
Проблема переупорядочивания. В NATS порядок зависел от времени поступления в брокер. В HurriCache (через OrderedMap) порядок определяется весом — таймстемпом самого CDR (
addElementWithWeight). OCS кладёт данные с весом, равным времени генерации события. Экспортер вычитывает диапазон и получает данные строго отсортировенными по времени генерации, а не по времени попадания в очередь. Даже если OCS worker положил данные позже, они всё равно будут вычитаны экспортером, так как запрос всегда идёт с гэпом, чтобы подобрать все события из прошлого.Проблема конкуренции. Несколько OCS worker node могут писать в один кластер HurriCache параллельно — операции добавления масштабируются. Несколько экспортеров могут читать из него параллельно, так как они работают с непересекающимися диапазонами весов (временными окнами) или используют атомарное удаление, исключающее гонку за одним элементом.
Нагрузка на OCS. Убирается необходимость сериализации в JSON для передачи в брокер сообщений. HurriCache отлично работает с бинарными данными, что позволяет уменьшить стоимость сериализации и снизить CPU overhead на стороне OCS.
Отказоустойчивость. HurriCache представлен как кластер. Если один узел падает, данные остаются доступными. Если падает экспортер — данные не теряются и не дублируются, а ждут следующего цикла обработки. В данном случае отказоустойчивость не изменилась, так как NATS тоже работает в кластере и к этой функции претензий не было.
Вместо заключения
Данный опыт научил тому, что не всегда стоит доверять документации. Документация — это хорошо, но нет ничего лучше, чем качественно проведённые нагрузочные тесты, и не стоит ими пренебрегать, даже в ущерб времени. Оно точно окупится, но позднее.

