Quota Manager

Материал из BiTel WiKi

Перейти к: навигация, поиск

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

Содержание

Описание

У Cisco есть приложение - Quota Manager, работающее в связке с SCE. Это приложение позволяет в реальном времени гибко управлять скоростью клиента в зависимости от потреблённого объема трафика. Казалось бы, BGBilling тоже имеет такой функционал, но в нём не хватает гибкости. В частности, в модуле Inet есть узел тарифного плана "Диапазон трафика", но максимальный уровень детализации - 1 час. К тому же, час не плавающий - строго привязан к стрелкам часов. Чтобы реализовать гибкое квотирование без применения SCE, было решено реализовать собственное управление квотами в виде расширения для модуля Inet.

Постановка задачи

Нужно сделать тариф на VPN:

  • Скорость: 100Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 50Мбит
  • Скорость: 50Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 25Мбит
  • Скорость: 25Мбит, если клиент скачал за последний час более 1GB - опускаемся на скорость 10Мбит
  • Если клиент на любой "ступеньке" (профиле квот) просидел 1 час и скачал от от 750MB до 1GB, он поднмается на ступеньку вверх
  • Если клиент на любом профиле просидел 1 час и скачал от от 500MB до 750MB, он поднмается сразу на 2 профиля вверх
  • и т.д.
  • "Скачал" - на самом деле учитывается трафик в обе стороны (входящий и исходящий) в сумме
  • Время "1 час" на практике можно задать любое. Например, полчаса. Либо опускать вниз в течение часа, а "поднимать" уже через полчаса
  • Когда клиент переходит на другой профиль, данные о потреблённой квоте в прошлом профиле теряются, т.е. он начинает "с чистого листа". Это ещё одна фича, которую нельзя реализовать узлом "диапазон трафика" в Inet.
  • На практике, если клиент начинает качать, то он падает вниз раньше, чем через час. Но чтобы подняться, ему придётся ждать не менее часа.

Реализация

Расширение представляет из себя jar-файл (Файл:Custom inet quota.zip), а также класс-узел тарифного плана в динамическом коде. Требуется настройка BGInetAccess, BGInetAccounting, создание доп таблиц в mysql, конфигурирование тарифа.

Ограничения

* Работает только с Rаdius-типами трафиков, т.к. используются соответсвующие события аккаунтинга.
* Не тестировалось на версиях BG 6+
* Работает только в realtime, т.е. не следует в зависимости от квоты менять тарификацию.

HOWTO

  1. Кладём custom_inet_quota.jar в /usr/local/BGBillingServer/lib/app/
  2. Делаем ./update.sh в BGInetAccess, BGInetAccounting
  3. Создаём таблицы в базе:
    CREATE TABLE `custom_inet_quota_[MID]` (
     `servId` int(11) NOT NULL,
     `nodeId` bigint(20) NOT NULL,
     `name` varchar(64) NOT NULL,
     `expireTime` bigint(20) NOT NULL,
     `penaltyExpiredTime` bigint(20) NOT NULL,
     `params` varchar(256) DEFAULT NULL,
     PRIMARY KEY (`servId`,`nodeId`)
     )
     
     CREATE TABLE `custom_inet_quota_slices_[MID]` (
     `servId` int(11) NOT NULL,
     `nodeId` bigint(20) NOT NULL,
     `amount` bigint(20) NOT NULL,
     `endTime` bigint(20) NOT NULL,
     PRIMARY KEY (`servId`,`nodeId`, `endTime`)
     )

    где [MID] - id модуля Inet

  4. Прописываем в inet_access.xml:
    ...
            <context name="radius">
                    <bean name="quotaCollector" class="ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector">
                            <param name="app">access</param>
                    </bean>
    ...
  5. Прописываем в inet_accounting.xml:
    ...
            <context name="radius">
                     <bean name="quotaCollector" class="ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector">
                             <param name="app">accounting</param>
                     </bean>
    ...
  6. Рестартуем BGBillingServer, BGBillingScheduler, BGInetAccess, BGInetAccounting
  7. Убеждаемся, что всё Access и Accounting запустились - проверяем наличие ошибок в логах, а также строчки "starting QuotaCollector"
  8. Добавляем в дин код класс ru.dsi.bgbilling.modules.inet.dyn.tariff.server.QuotaProfileTariffTreeNode:
    package ru.dsi.bgbilling.modules.inet.dyn.tariff.server;
     
    import ru.bitel.bgbilling.kernel.tariff.tree.server.TariffTreeNode;
    import ru.bitel.bgbilling.kernel.tariff.tree.server.TreeContext;
    import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffContext;
    import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffRequest;
    import ru.bitel.bgbilling.modules.inet.tariff.server.InetTariffWorkerContext;
    import ru.bitel.common.ParameterMap;
    import ru.bitel.common.Preferences;
    import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaCollector;
    import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaHolder;
    import ru.dsi.bgbilling.modules.inet.accounting.quota.QuotaProfile;
     
    import java.util.Set;
     
    public class QuotaProfileTariffTreeNode extends TariffTreeNode<InetTariffRequest, InetTariffContext, TreeContext, InetTariffWorkerContext> {
     
        private final QuotaCollector quotaCollector;
        //Имя профиля
        private final String name;
        private final ParameterMap params;
     
        public QuotaProfileTariffTreeNode(int id, ParameterMap parameterMap) {
            super(id, parameterMap);
            this.params = new Preferences(parameterMap, "","\n");//parameterMap;
            this.name = parameterMap.get("name", "");//null - дефолтный профиль, самый последний по порядку
            this.quotaCollector = QuotaCollector.getInstance();
            this.logger.debug("QuotaProfileTariffTreeNode created for nodeId="+id);
        }
     
        /**
         * @param treeNodeId
         * @param parentTreeNodeId
         * @param req
         * @param ctx
         * @param treeContext
         * @param workerContext
         * @return
         */
        @Override
        protected int executeImpl(Long treeNodeId,
                                  Long parentTreeNodeId,
                                  InetTariffRequest req,
                                  InetTariffContext ctx,
                                  TreeContext treeContext,
                                  InetTariffWorkerContext workerContext){
     
            //this.logger.debug("execute name = '"+this.name+"' (realtime="+ctx.realtime+")");
            //Узел работает только в режиме realtime!
            if(!ctx.realtime){
                return 0;
            }
     
            if(null==quotaCollector){
                this.logger.debug("quotaCollector == null");
                return 0;
            }
     
            //Нужно для случая, когда есть текущий профиль, но в тарифе он не найден (изменился тариф, например) -
            //тогда мы должны попасть в дефолтный узел тарифа. А для этого нужно проверить,
            //не заходили ли мы уже в какой-либо из вышестоящих
            Set<Long> acceptedSet = req.getAcceptedSet(QuotaProfileTariffTreeNode.class);
            if(acceptedSet.contains(parentTreeNodeId)){//Уже заходили в узел квоты этим тарифным запросом, не тратим время
                //this.logger.debug("acceptedSet = "+acceptedSet+" contains "+parentTreeNodeId);
                return 0;
            }
     
            int servId = req.inetServRuntime.getInetServ().getId();
     
            QuotaHolder quotaHolder = quotaCollector.getQuotaProfile(servId, parentTreeNodeId);
     
            if (quotaHolder!=null && this.name.equals(quotaHolder.name)){//Наш профиль, обрабатываем
                if(quotaHolder.quota==null){
                    //нужно создать квоту!
                    try {
                        quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));
                    }catch (Exception ex){
                        logger.error(ex.getMessage(), ex);
                    }
                }else{
                    logger.debug("["+this.name+"] servId="+servId+" amount consumed = "+quotaHolder.quota.getTotalAmount()+"/"+quotaHolder.quota.quotaSize);
                    quotaHolder.expireTime=System.currentTimeMillis()+quotaHolder.quota.expirePeriod;
                }
     
                acceptedSet.add(parentTreeNodeId);
                return 1;
            }else if(null == this.name || "".equals(this.name) || "default".equals(this.name)){//Дефолтный профиль - обрабатываем
                //нужно создать квоту!
                this.logger.debug("default quota profile");
                try {
                    quotaCollector.putQuota(servId, parentTreeNodeId, new QuotaProfile(this.params));
                }catch (Exception ex){
                    logger.error(ex.getMessage(), ex);
                }
                acceptedSet.add(parentTreeNodeId);
                return 1;
            }
            return 0;
        }
    }
  9. Конфигурируем тарифный план: Файл:quotas.png Вот кнофиги всех профилей квот (узлов тарифа "Обработчик"):
    • 10Mbit:
    expire.period=3660
    name=10Mbit
    penalty.period=3600
    profile.up=25Mbit
    profile.up.349525333=default
    profile.up.699050666=50Mbit
    quota.size=1048576000
    slice.count=6
    slice.period=600
    traffic.types=1,2
    • 25Mbit:
    expire.period=3660
    name=25Mbit
    penalty.period=3600
    profile.down=10Mbit
    profile.up=50Mbit
    profile.up.699050666=default
    quota.size=1048576000
    slice.count=6
    slice.period=600
    traffic.types=1,2
    • 50Mbit:
    expire.period=3660
    name=50Mbit
    penalty.period=3600
    profile.down=25Mbit
    profile.up=default
    quota.size=1048576000
    slice.count=6
    slice.period=600
    traffic.types=1,2
    • 100Mbit:
    expire.period=3660
    name=default
    profile.down=50Mbit
    quota.size=1048576000
    slice.count=6
    slice.period=600
    traffic.types=1,2
  10. Жмём "Оповестить об изменениях"
  11. Тестируем тариф. При смене скоростей в логах Accounting-сервера можно увидеть такие записи:


    GeSHi Error: GeSHi could not find the language txt (using path /home/user/wiki.bitel.ru/mediawiki-1.15.1/extensions/SyntaxHighlight_GeSHi/geshi/geshi/) (code 2)

    Вы должны указать язык следующим образом: <source lang="html">...</source>

    Поддерживаемые языки:

    abap, actionscript, ada, apache, applescript, asm, asp, autoit, bash, basic4gl, blitzbasic, bnf, c, c_mac, caddcl, cadlisp, cfdg, cfm, cpp, cpp-qt, csharp, css, d, delphi, diff, div, dos, dot, eiffel, fortran, freebasic, genero, gml, groovy, haskell, html4strict, idl, ini, inno, io, java, java5, javascript, latex, lisp, lua, m68k, matlab, mirc, mpasm, mysql, nsis, objc, ocaml, ocaml-brief, oobas, oracle8, pascal, per, perl, php, php-brief, plsql, python, qbasic, rails, reg, robots, ruby, sas, scheme, sdlbasic, smalltalk, smarty, sql, tcl, text, thinbasic, tsql, vb, vbnet, vhdl, visualfoxpro, winbatch, xml, xpp, z80

  12. Также можно включить дебаг в log4j и получить более подробную информацию по потреблению квот

Исходники

https://github.com/Cromeshnic/BG-Quota-Manager

В следующих сериях

* Подробнее расписать параметры конфигурации профилей квот
* Описать механизм работы
* Реализовать просмотр текущего статуса квот и истории смены скоростей в ЛК для клиентов
Личные инструменты