7 простых оптимизаций, уменьшивших нагрузку на CPU с 80% до 27%

    Уже более 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 транзакций в секунду и сценарии следующего вида:
    1. CCR-I – установка сессии абонента,
    2. CCR-U – обновление информации о сессии с информацией об объеме потребленного трафика абонентом,
    3. CCR-T – окончание сессии с информацией об объеме потребленного трафика абонентом.

    В версии 3.5.2, выпущенной нами в первом квартале прошлого года, нагрузка CPU на этом сценарии была довольно высокой и составляла 80%. Мы смогли опустить ее до 35% в версии 3.6.0, стоящей в коммерческой сети на данный момент, и еще до 27% в версии 3.6.1, находящейся на данный момент на этапе стабилизации.
    image
    Несмотря на такую огромную разницу, мы не совершили никакого чуда, а просто провели 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 разработчику простыми и очевидными. Нам они тоже показались такими, но только когда мы их осознали и реализовали. Самое хорошее решение часто лежит на поверхности, но его и сложнее всего увидеть. Так что резюмирую:
    1. Проверять, что данные реально изменяются;
    2. Стараться максимально уменьшить количество локов на целые таблицы;
    3. Кешировать и выносить конфигурационные данные из базы;
    4. Делать только те действия, которые действительно нужны, даже если кажется, что проще сделать весь список.

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      0
      А что из себя представляет ваш DPI? Насколько он deep? Умеете ли вы отличаться трафик Skype от HTTPS в случае с TLS на 443 порту? UDP Голосовой/видео от онлайн игр?
        +2
        а почему «ваш»? Автор PCRF же пишет.
          +2
          Анастасия пишет о разработке PCRF. Это компонент сети, который управлет DPI. Т.е. обычно в DPI нет базы пользователей. Как только приходит первый пакет с какого-то IP, DPI спрашивает PCRF «Какой доступ дать этому абонненту?». PCRF на основе тех правил, которые в нем настроены а также не основе информации о поьзователе выдает доступ.
          С учетом того что трафик пользователя может проходить через несколько DPI последовательно, а также в процесс может быть вовлечено и другое оборудование задача оказывается не очень простой.
            +2
            Да, как тут уже ответили, команда пишет PCRF. А с DPI работает сторонними. Если интересно, то список DPI, с которыми произведены IOT и которые (некоторые) стоят на продакшен сети:
            Sandvine Policy Traffic Switch (PTS, Policy Enforcement)
            Procera PRE with JET integration module JSM
            Cisco SCE8000
            +3
            Молодцы, почему-то сразу вспомнил:
            Поэтому любая версия 2.0 по сравнению с 1.0 должна по идее не только обладать поддержкой новых форматов B,C,D, но занимать меньше места, жрать меньше ресурсов и работать со старыми A, B заметно быстрее за счет оптимизации старого кода.

            (с) Каганов
              +1
              Спасибо. Вообще на продукте старательно поддерживается цикл разработки в несколько этапов — добавление нового функционала, стабилизация с заморозкой кода, прогон тестов на производительность, в частности длительных на несколько дней-недель, оптимизация по результатам. Порою жизнь (она же коммерция) вносит свои коррективы в эти планы, но от этого только веселее.
              +2
              так держать!
                +2
                Продукт в итоге получился очень хорошим.

                Использовал его еще на этапе пилота, однако даже тогда было понятно, что потенциал огромный.
                И, как видно, этот потенциал успешно реализуется.
                  0
                  Скоро появится возможность попробовать еще и DRA.

                Only users with full accounts can post comments. Log in, please.