Уже более 3 лет наша команда занимается разработкой такого важного компонента сети оператора как PCRF. Policy and Charging Rules Function (PCRF) – решение для управления политиками обслуживания абонента в сетях LTE (3GPP), позволяющее в реальном времени назначать ту или иную политику, принимая во внимание сервисы, подключенные у абонента, его местонахождение, качество сети в данном месте в данный момент, время суток, объем потребленного трафика и т.д. Под политикой в данном контексте подразумевается доступный абоненту набор сервисов и параметры QoS (качества обслуживания). Анализируя соотношение цена-качество для различных продуктов в данной области от разнообразных поставщиков, мы приняли решение разрабатывать свой продукт. И вот уже более 2 лет, наш PCRF успешно работает на коммерческой сети компании Yota. Решение полностью софтовое, с возможностью устанавливать даже на обычные виртуальные сервера. Работает в коммерции на Red Hat Linux, но в целом возможна установка и под другие Linux-системы.
Из всех возможностей нашего PCRF самыми успешными были признаны:
- гибкий инструмент для непосредственного принятия решения о политиках абонентов, основанный на языке Lua, позволяющий службе эксплуатации легко и на лету изменять алгоритм назначения политик;
- поддержка разнообразных PCEF (Policy and Charging Enforcement Function – компонент, непосредственно устанавливающий политики абонентам), DPI (Deep Packet Inspection – компонент для анализа пакетов трафика, в частности позволяющий подсчитывать объем потребленного трафика по категориям), AF (Application Function – компонент, описывающий потоки данных сервиса и информирующий о ресурсах, требующихся сервису). Все эти узлы сети могут устанавливаться в любом количестве, поддерживается множество сессий от различных компонентов сети на одного абонента. Нами было проведено множество IOT со многими крупными производителями такого рода оборудования;
- целое семейство внешних интерфейсов для систем, находящихся в сети, и система мониторинга, описывающая все происходящие в системе процессы;
- масштабируемость и производительность.
Собственно, далее в статье речь пойдет об одном из многих критериев именно последнего.
У нас есть ресурс, на котором мы полгода назад выложили образ для тестирования, доступный всем под соответствующей лицензией, список поставщиков оборудования, с которыми у нас проведены IOT-ы, пакет документов по продукту и несколько статей на английском о нашем опыте разработки (про Lua-based движок, например, или разнообразное тестирование).
Когда речь идет о производительности, есть множество критериев, по которым она оценивается. В статье о тестировании на нашем ресурсе довольно подробно описаны нагрузочные тесты и инструменты, которые мы использовали. Здесь же мне бы хотелось остановить на таком параметре, как использование CPU.
Я буду сравнивать результаты, полученные на тесте с 3000 транзакций в секунду и сценарии следующего вида:
- CCR-I – установка сессии абонента,
- CCR-U – обновление информации о сессии с информацией об объеме потребленного трафика абонентом,
- CCR-T – окончание сессии с информацией об объеме потребленного трафика абонентом.
В версии 3.5.2, выпущенной нами в первом квартале прошлого года, нагрузка CPU на этом сценарии была довольно высокой и составляла 80%. Мы смогли опустить ее до 35% в версии 3.6.0, стоящей в коммерческой сети на данный момент, и еще до 27% в версии 3.6.1, находящейся на данный момент на этапе стабилизации.
Несмотря на такую огромную разницу, мы не совершили никакого чуда, а просто провели 7 простых оптимизаций, которые я и опишу ниже. Быть может, в вашем продукте вы тоже сможете воспользоваться чем-то из приведенного, чтобы сделать его лучше с точки зрения использования CPU.
Прежде всего хочется сказать, что большинство оптимизаций касалось взаимодействия базы данных и логики приложения. Более продуманное использование запросов и кеширование информации – это, пожалуй, главное, что мы сделали. Для анализа времени работы запросов на БД, нам пришлось сделать свою утилиту. Дело в том, что изначально в приложении использовалась база Oracle TimesTen, которая не имеет встроенных развитых средств мониторинга. А после внедрения PostgreSQL, мы решили, что использовать одно средство для сравнения двух баз, это правильно, так что оставили свою утилиту. Кроме того, наша утилита позволяет не собирать данные постоянно, а включать/выключать ее по мере надобности, например, в коммерческой сети с небольшим ростом загрузки CPU, но зато с возможность проанализовать сразу на продакшене, какой запрос вызывает проблемы в данный момент.
Утилита называет tt_perf_info и просто замеряет время, потраченное на разные этапы исполнения запроса: fetch, непосредственно исполнение, количество вызовов в секунду, процент, занимаемый от общего времени. Время выводится в микросекундах. Топ 15 запросов на версии 3.5.2 и 3.6.1 можно увидеть в таблицах по ссылкам:
3.5.2 top 15
3.6.1 top 15 (пустые клетки соответствуют значению 0 в этой версии)
Оптимизация 1: уменьшение коммитов
Если внимательно посмотреть на результат вывода tt_perf_info на разных версиях, то можно заметить, что количество вызовов pcrf.commit было уменьшено с 12006 раз в секунду до 1199, то есть в 10 раз! Вполне очевидное решение, пришедшее нам в голову, заключалось в том, чтобы проверять, действительно ли произошли какие-то изменения в базе, и только в случае положительного ответа производить коммит. Например, для UPDATE запроса в PCRF делается проверка количества изменившихся записей. Если оно равно 0, то коммит не производится. Аналогично с DELETE.
Оптимизация 2: удаление MERGE запроса
На базе Oracle TimesTen было замечено, что MERGE запрос устанавливает лок на всю таблицу. Что в условиях постоянно конкурирующих за таблицы процессов, приводило к очевидным проблемам. Так что мы просто заменили все MERGE запросы на комбинацию из GET-UPDATE-INSERT. Если запись есть, она обновляется, если нет – добавляется новая. Мы даже не стали оборачивать все это в транзакцию, а рекурсивно вызвали функцию в случае неудачи. На псевдокоде это выглядит примерно так:
our_db_merge_function() {
if (db_get() == OK) {
if (db_update() == OK) {
return OK;
} else {
return out_db_merge_function();
}
} else {
if (db_insert() == OK) {
return OK;
} else {
return out_db_merge_function();
}
}
}
На практике это почти всегда отрабатывает без рекурсивного вызова, так как конфликты по одной записи все же происходят редко.
Оптимизация 3: кеширование конфигурации для подсчета объемов потребляемого абонентами трафика
Алгоритм подсчета объема потребляемого трафика по 3GPP спецификации имеет довольно сложную структуру. В версии 3.5.2 вся конфигурация хранилась в базе и представляла из себя таблицы мониторинг ключей и аккумуляторов с отношением многие-ко-многим. Также система поддерживала суммирование аккумуляторов трафика от разных внешних систем в одно значение на PCRF и эта настройка хранилась в БД. Как следствие, при приходе очередных данных о накопленном объеме, происходила сложная выборка по базе.
В 3.6.1 большая часть конфигурации была вынесена в xml файл с нотификацией процессов об изменении данного файла и подсчетом контрольной суммы по конфигурационной информации. Также, текущая информация о подписке на мониторинг траффика хранится в блобе, привязанном к каждой пользовательской сессии. Вычитывание и запись блоба – операция несомненно более быстрая и менее ресурсоемкая, чем огромная выборка из таблиц с отношением многие-ко-многим.
Оптимизация 4: уменьшение количества вывозов Lua движка
Lua движок вызывается на каждый запрос типа CCR-I, CCR-U and RAR, обрабатывающийся в PCRF, и исполняет Lua скрипт, описывающий алгоритм выбора политики, так как вероятно изменение политики абонента при обработке данных запросов. Но идея чек-суммы нашла свое применение и здесь. В 3.6.1 версии мы сохранили всю информацию, от которой может зависеть реальное изменение политики, в отдельную структуру и стали считать контрольную сумму по ней. Соответственно, движок стал дергаться только в случае реальных изменений.
Оптимизация 5: вынос сетевой конфигурации из БД
Сетевая конфигурация также хранится в Базе Данных с самых ранних версий PCRF. В релизе 3.5.2 логика приложения и сетевая часть довольно сильно пересекались по таблицам с настройками сети, так как логический модуль регулярно читал параметры соединений из БД, а сетевая часть пользовалась БД, как хранилищем всей сетевой информации. В версии 3.6.1 информация для сетевой части была перенесена в разделяемую память, а в основную логику добавлены периодические процессы, обновляющие ее при изменениях в базе. Тем самым были уменьшены локи по общим таблицам в базе.
Оптимизация 6: выборочный разбор команд Diameter
PCRF общается со внешними системами по протоколу Diameter, анализируя и разбирая множество команд в единицу времени. Эти команды, как правило, содержат множество полей (avp) внутри себя, но далеко не каждой компоненте нужны все поля. Часто используется только несколько полей из первой (заголовочной) части команды, такие как Destination/Origin Host/Realm, или поля, позволяющие идентифицировать абонента или сессию, то есть id (которые тоже зачастую расположены в начале). И только один-два основных процесса используют все поля сообщения. Поэтому в версии 3.6.1 были введены маски, описывающие, какие поля необходимо вычитывать для данной компоненты. А также убраны почти все операции копирования памяти. Фактически в памяти осталось только исходное сообщение, а все процессы используют структуры с указателями на необходимые части, данные копируются внутри процессов уже по строгой необходимости.
Оптимизация 7: кеширование времени
Когда PCRF стал обрабатывать более 10000 транзакций в секунду, стало заметно, что процесс логирования занимает существенную часть времени и CPU. Иногда кажется, что логами можно пожертвовать в пользу большей производительности, но оператор должен иметь возможность воспроизвести всю картину происходящего в сети и на конкретной компоненте. Поэтому мы сели анализировать и выяснили, что самая частая запись в логе – это метка времени и даты. Конечно же в каждой записи в логе она присутствует. И тогда, ограничив точность времени секундой, мы просто стали кешировать строчку с текущим временем и переписывать ее только на следующую секунду.
Все эти семь оптимизаций, наверняка, покажутся опытному high-performance разработчику простыми и очевидными. Нам они тоже показались такими, но только когда мы их осознали и реализовали. Самое хорошее решение часто лежит на поверхности, но его и сложнее всего увидеть. Так что резюмирую:
- Проверять, что данные реально изменяются;
- Стараться максимально уменьшить количество локов на целые таблицы;
- Кешировать и выносить конфигурационные данные из базы;
- Делать только те действия, которые действительно нужны, даже если кажется, что проще сделать весь список.
Автор: anastasiak2512