В последние 4.5 года я много рассказывал на Хабре про такие
OpenSource проекты, как SObjectizer и RESTinio. Но вот об
использовании SObjectizer и/или RESTinio в реальных проектах пока
еще ни разу не удавалось поговорить (была лишь одна статья от стороннего
автора).
Причина простая: мы не можем обсуждать те проекты, в которых мы
сами применяли SObjectizer/RESTinio, ибо NDA. Равно как и не можем
рассказывать о тех чужих проектах, о которых узнали в частном
общении. Так что с наглядными примерами использования
SObjectizer/RESTinio в реальной жизни всегда была напряженка.
Дабы как-то улучшить ситуацию пару лет назад мы даже сделали
небольшой демо-проект Shrimp и
опубликовали здесь серию статей о нем (раз, два, три). Но все-таки это было не
более чем демонстрация.
К счастью или к несчастью, но далеко не самый удачный 2020-й год
предоставил нам возможность показать как же
выглядит реальный проект, в разработке которого SObjectizer и
RESTinio активно используются. И в данной статье я попробую
рассказать о том, как и для чего SObjectizer и RESTinio применяются
в arataga, исходники которого можно
найти на GitHub.
Тем, кто хочет больше узнать об arataga и причинах его появления
на GitHub-е, рекомендую прочитать этот блог-пост. Здесь же в
двух словах скажу лишь, что arataga -- это socks5+http/1.1
прокси-сервер, который затачивался под использование в следующих
условиях (перечислены те из них, которые актуальны с точки зрения
внутренней архитектуры arataga):
-
много точек входа, счет идет на тысячи (8 тысяч нужно было
поддерживать сразу). У каждой точки входа уникальное сочетание
IP+port;
-
подключения на одну точку входа могут идти с темпом в несколько
десятков в секунду, параллельно на одной точке входа могут "висеть"
сотни подключений;
-
одновременно могут существовать десятки тысяч подключений, общий
темп появления новых подключений на всех точках входа может быть от
1500 в секунду;
-
аутентификация клиентов должна укладываться в единицы
миллисекунд;
-
обновленная конфигурация и новые списки пользователей, которым
разрешена работа с прокси-сервером, периодически доставляется через
HTTP POST на специальный административный HTTP-вход.
Далее в статье я сосредоточусь на архитектурных и технических
моментах, не затрагивая тему "зачем это все было нужно?"
Вероятно, для того, чтобы лучше понимать изложенное ниже, нужно
иметь общие представления о SObjectizer-овских агентах, диспетчерах
и почтовых ящиках (mbox-ах). Старая обзорная статья может в
этом помочь (хотя сам SObjectizer за это время несколько
изменился).
Тезисно о принятых проектных и архитектурных решениях
Многопроцессность или многопоточность?
Прежде всего нам предстояло сделать выбор между реализацией
arataga в виде мультипроцессного или мультипоточного
приложения.
В случае мультипроцессного приложения потребовалось бы иметь
процесс-родитель, который бы запускал N дочерних
процессов-исполнителей. Процесс-родитель отвечал бы за управление
дочерними процессами и за распределение конфигурации/списков
пользователей между дочерними процессами. Дочерние же процессы
открывали бы точки входа и обслуживали бы входящие соединения от
клиентов и исходящие к целевым хостам.
Каждый процесс при этом мог бы быть простым однопоточным
процессом (может быть за исключением родительского, в котором могло
бы потребоваться больше одного рабочего потока).
Достаточно простая и надежная схема, почти что в духе unix way.
Но связываться с ней не очень хотелось из-за большого количества
IPC-взаимодействия между родительским и дочерними процессами: в
одну сторону должна идти управляющая информация, в другую --
диагностическая.
Вариант в виде многопоточного приложения выглядел проще, т.к.
все взаимодействующие сущности оказывались в рамках одного общего
адресного пространства и распространение любой информации в любых
направлениях не представляло вообще никакой проблемы.
Плюс к тому, у нас в руках был большой молоток, SObjectizer, с
помощью которого разрабатывать многопоточные приложения гораздо
проще, чем при ручной работе с std::thread, std::mutex и
std::condition_variable и т.п. Если выразить основные сущности
приложения в виде SObjectizer-овских агентов, распределить этих
агентов должным образом по рабочим нитям и организовать их
взаимодействие на базе асинхронных сообщений, то ужасы
многопоточности можно спокойно обойти стороной. Что, собственно, в
очередной раз и произошло.
Сколько рабочих потоков нужно?
Здесь все было понятно изначально, еще даже до начала разработки
arataga, поскольку старый прокси-сервер, на замену которого arataga
и разрабатывался, использовал модель thread-per-connection. Когда
счет одновременно живущих соединений идет на десятки тысяч, то
thread-per-connection не есть хорошо.
Поэтому об использовании в arataga схемы thread-per-connection
речь не шла вообще. Сразу же был взят курс на применение чего-то
похожего на thread-per-core.
Было решено под обслуживание подключений отводить не все
доступные вычислительные ядра, а использовать формулу (nCPU-2), где
nCPU -- это количество ядер.
Так, если на машине 8 ядер, то в arataga будет создано 6 рабочих
потоков, которые будут отвечать за обслуживание соединений. Т.е.
работой с трафиком будут заняты 6 ядер. Два оставшихся ядра будут
доступны для других рабочих потоков внутри arataga. Плюс и самой ОС
нужно что-то оставить, а то не очень приятно удаленно подключаться
к работающему серверу по ssh, когда у него все ядра загружены под
100%
Что будет агентом, а что не будет?
Изначально в работе arataga выделялось несколько типов операций,
которые должны были выполняться на разных рабочих контекстах:
-
разбор конфигурации. Обработка конфигурации для нескольких тысяч
точек входа может занимать некоторое время (например, 5ms). Сюда
входит и парсинг/валидация конфигурации, и сохранение ее локальной
копии, и удаление устаревших точек входа, которых нет в новой
конфигурации, и создание новых точек входа, добавленных в
обновленной конфигурации;
-
разбор списка пользователей и формирование структур для
представления этого списка в памяти. Это также может занимать
некоторое время, поскольку списки пользователей могут насчитывать
несколько десятков тысяч строк;
-
обработка точек входа. Т.е. создание серверных сокетов на
заданных IP+port, прием новых подключений с учетом ограничений на
максимальное количество параллельных подключений и т.д.;
-
обслуживание принятых подключений. Т.е. вычитывание данных от
клиента, определение типа протокола, осуществление проксирования
подключения в зависимости от типа протокола и команды, пришедшей от
клиента.
Первые три операции явно напрашивались на то, чтобы быть
представленными в виде отдельных агентов, которые привязываются к
тому или иному диспетчеру.
А вот по поводу последнего, т.е. обслуживания уже принятых
подключений, можно было поступить по-разному. Можно было попытаться
представить каждое подключение в виде отдельного агента.
Но этот подход меня лично не вдохновлял по нескольким
причинам.
Во-первых, дело в том, что обслуживание разных команд в рамках
одного протокола (не говоря уже про разные протоколы) может
существенным образом отличаться. И если попытаться все эти различия
запихнуть в одного агента, то этот агент распухнет до
неприличия.
Во-вторых, не очень понятно, в чем смысл использовать здесь
агента. Взаимодействие с сетью предполагалось делать с
использованием Asio и асинхронных операций.
Т.е., если вызывается Asio-шный async_read_some
, то
когда-то Asio вызовет для этой операции completion_handler. И если
мне нужно передать результат async_read_some
агенту,
то в completion_handler нужно будет отослать агенту сообщение,
которое агент обработает. Что, вообще-то говоря, не бесплатно, т.к.
сперва на каком-то io_context
будет запланирован вызов
completion_handler, а затем, когда до completion_handler дойдет
очередь, будет запланирован вызов обработчика события у агента. И
только затем когда-то будет вызван этот самый обработчик. Что
наводит на мысль "а не слишком ли много приходится платить за
концептуальную чистоту?" Тем более, что по прошлому опыту я знал,
что когда операции чтения/записи сокета доставляются до агента в
виде сообщений, то как-то кардинально это работу с сетью не
упрощает. Может быть даже наоборот.
Добавим сюда еще и то, что регистрация/дерегистрация агентов в
SObjectizer все-таки не самая дешевая операция (это связано с
механизмом коопераций и гарантиями транзакционности операции
регистрации). Конечно, если принимать новые подключения с темпом в
2000-3000 в секунду, то "тормоза" SObjectizer-а здесь не проявятся,
SObjectizer может регистрировать/дерегистрировать кооперации с
гораздо более высоким темпом. Но все-таки у частого
создания/удаления агентов есть своя цена, и если ее можно не
платить, то лучше ее не платить.
В результате было принято решение, что агентом будет точка
входа, но вот отдельные подключения агентами не будут. Вместо
этого, агент, представляющий точку входа, будет обслуживать все
подключения к этой точке.
Для чего планировалось использовать RESTinio?
Изначально предполагалось, что посредством RESTinio будет
реализован административный HTTP-вход. Причем не в виде
SObjectizer-овского агента, а просто единственная отдельная нить,
на которой будет работать RESTinio-сервер.
Библиотека RESTinio, действительно, была использована таким
очевидным образом. А вот о паре менее очевидных способов применения
RESTinio в arataga речь пойдет ниже.
Погрузимся в подробности
Некоторые детали использования SObjectizer
В этой части бегло пройдемся по некоторым моментам использования
SObjectizer в коде arataga. Выбор чисто субъективный, мне
показалось, что именно они наиболее показательны и/или не
обычны.
В рамках одной статьи невозможно обсудить все аспекты применения
SObjectizer в проекте arataga, поэтому что-то неизбежно останется
"за кадром". Так что если у кого-то из читателей возникнут вопросы,
то постараюсь ответить в комментариях.
Агент startup_manager и "глобальный таймер"
Работа arataga начинается с запуска агента startup_manager. Этот агент
отвечает за последовательный запуск основных "компонентов" arataga:
сперва стартует агент user_list_processor, затем агент
config_processor, затем уже запускается отдельная нить с
RESTinio-сервером для административного HTTP-входа.
Каждый из запускаемых startup_manager агентов должен затем
подтвердить свой успешный запуск. Если этого не произошло за
отведенное время, то startup_manager прерывает работу приложения
вообще.
В агенте startup_manager можно обнаружить один непривычный трюк:
запуск при старте периодического
сообщения one_second_timer_t
, которое отсылается
раз в секунду на специальный mbox.
В какой-то мере это преждевременная оптимизация.
Дело в том, что всем точкам входа нужно время от времени
контролировать тайм-ауты выполняемых операций. Например, нужно
принудительно закрывать соединения, в которых долго нет никакой
активности.
Обычно это выполняется посредством отложенного сообщения: агент
отсылает самому себе отложенное на 1 секунду сообщение, а когда оно
приходит, то проверяет времена жизни подключений, времена
отсутствия активностей в них и т.д. После чего шлет себе следующее
отложенное сообщение. Или же вместо перепосылки отложенных
сообщений один раз запускает периодическое сообщение.
Но когда агентов, нуждающихся в подобных периодических
проверках, будут тысячи, то это будет означать, что раз в секунду
будут порождаться тысячи отложенных/периодических сообщений.
Ну и зачем, спрашивается, это делать, если достаточно всего
одного сообщения, которое будет отсылаться в отдельный mbox один
раз в секунду? И на которое смогут подписаться столько получателей,
сколько потребуется.
Вот как раз для того, чтобы каждый агент, обслуживающий точку
входа, не запускал свое собственное периодическое сообщение для
контроля тайм-аутов, агент startup_manager делает это посредством
one_second_timer_t
.
Понятие io_thread и ее реализация на базе
asio_one_thread-диспетчера
Выше говорилось, что в реализации arataga было решено
придерживаться модели, похожей на thread-per-core, при этом
(nCPU-2) рабочих потока будет выделено под выполнение операций с
сетью. Такие рабочие нити получили условное название io_thread
(далее io_thread == "I/O нить" и поэтому "io_thread" будет иметь
женский род).
Т.к. работа с сетью в arataga работа идет посредством Asio, то
для следования идее thread-per-core было решено сделать так, чтобы
на каждой io_thread работал свой собственный экземпляр
asio::io_context
.
Т.е. нам нужна была рабочая нить, которая создает экземпляр
asio::io_context, а затем вызывает для него run()
.
После чего на этой нити нужно было бы еще как-то разместить и
агентов, реализующих точки входа. И чтобы одна и та же нить
обслуживала и операции Asio, и события SObjectizer-овских
агентов.
В arataga для этих целей просто используются экземпляры asio_one_thread-диспетчера из
so5extra. Каждый такой диспетчер -- это отдельный рабочий поток
(т.е. io_thread).
Агент config_processor, о котором речь пойдет ниже, создает N экземпляров
asio_one_thread-диспетчеров, а затем просто привязывает к этим
экземплярам агентов, обслуживающих точки входа.
Агенты authentificator и dns_resolver, их дублирование на
каждой io_thread
Подключающихся к arataga клиентов нужно аутентифицировать. Эта
задача была поручена отдельному агенту authentificator. Когда
агенту, обслуживающему точку входа, нужно аутентифицировать
клиента, то отсылается сообщение агенту authentificator, который
выполняет аутентификацию и шлет назад результат аутентификации.
Еще агентам, которые обслуживают точки входа, нужно выполнять
резолвинг доменных имен. За эту операцию отвечает еще один
специальный агент -- dns_resolver. Тут используется подобная схема:
агент-точка-входа отсылает агенту dns_resolver сообщение, а тот
присылает назад результат поиска доменного имени.
Мне показалось, что если уж мы пытаемся следовать модели
thread-per-core, то логичным было бы сделать отдельную копию
агентов dns_resolver и authentificator для каждой из io_thread.
Поэтому эти агенты создаются сразу же вслед за очередным
asio_one_thread-диспетчером и привязываются к только что
созданному диспетчеру.
Распределение агентов по диспетчерам
В результате получилась следующая картинка:
У агентов config_processor и user_list_processor есть
собственные рабочие потоки, которые реализуются посредством
штатного диспетчера SObjectizer под названием one_thread.
Каждая io_thread представлена отдельным диспетчером
asio_one_thread из so5extra. И на каждом таком диспетчере работает
сразу несколько (десятков, сотен, тысяч) агентов acl_handler.
Агент config_processor
Агент config_processor отвечает за обработку конфигурации
arataga, поддержание списка существующих точек входа, их
распределение между io_threads, и за создание/удаление точек входа
при изменении конфигурации.
При своем старте config_processor пытается прочитать локальную
копию конфигурационного файла. Если это удалось, то
config_processor принимает поднятую из конфига информацию за
текущую конфигурацию и создает описанные в ней точки входа
(регистрирует агентов acl_handler).
Если же локальной копии конфигурации нет или же прочитать ее не
удалось, то config_processor ждет, пока придет новая конфигурация
от административного HTTP-входа.
Когда от административного HTTP-входа поступает новая
конфигурация, что после ее успешной обработки config_processor
рассылает уведомления об изменении параметров arataga на отдельный
multi-producer/multi-consumer mbox. Так изменения в конфигурации
становятся доступны всем, кто в них заинтересован.
В работе config_processor с acl_handler есть важная особенность:
сейчас в arataga агент acl_handler намертво привязывается к
конкретному asio_one_thread и живет на этом asio_one_thread до тех
пор, пока не будет удален.
Эта особенность может привести к ситуации, когда из конфигурации
удалили, скажем, 1000 точек входа, и в результате на одной из
io_threads осталось заметно меньше работающих агентов, чем на
других io_threads.
Эта диспропорция может быть устранена при добавлении новых точек
входа. В этом случае config_processor при создании новых агентов
acl_handler будет привязывать их к тем диспетчерам asio_one_thread,
на которых сейчас меньше всего живых acl_handler.
Но если новые точки входа не создаются, то диспропорция
останется и config_processor не будет перераспределять старые
acl_handler между диспетчерами asio_one_thread. По двум
причинам:
-
во-первых, в SObjectizer нет возможности перепривязать агента с
одного диспетчера на другой. Поэтому к какому диспетчеру агента
изначально привязали, на таком он и будет продолжать работать;
-
во-вторых, в каждом агенте acl_handler может существовать
множество Asio-шных объектов, завязанных на конкретный экземпляр
asio::io_context
. Если перемещать содержимое
acl_handler с одной io_thread на другую, то нужно будет и
переконструировать эти Asio-шные объекты.
Чтобы не возиться со всем этим добром было решено смириться с
такими диспропорциями в первой версии aragata. Может на практике
они вообще не будут столь серьезными, чтобы оказывать влияние на
производительность. Ну а если таки будут, вот тогда можно было бы и
озадачиться проблемой миграции точки входа с одной io_thread на
другую.
Агент user_list_processor
Агент user_list_processor отвечает за работу со списком
пользователей.
При своем старте user_list_processor пытается прочитать
локальную копию списка пользователей. Если это удалось, то
локальная копия становится актуальным списком. Если не удалось, то
user_list_processor ждет поступления нового списка от
административного HTTP-входа.
Когда от административного HTTP-входа прилетает новый список
пользователей, то user_list_processor пытается его обработать. И,
если это получается успешно, то обновленный список рассылается на
отдельный multi-producer/multi-consumer mbox на которые подписаны
агенты authentificator. Что позволяет authentificator-ам получать
обновленные списки пользователей сразу после того, как
user_list_processor завершит их обработку.
Агент acl_handler
Агент acl_handler является, наверное, самым сложным и объемным
агентом в arataga (hpp-файл, cpp-файл). Его задача -- это
создать серверный сокет для точки входа, принимать и обслуживать
подключения к этому серверному сокету.
В связи с этим в рамках данной статьи можно выделить несколько
аспектов в реализации acl_handler.
Во-первых, acl_handler инициирует асинхронные I/O операции с
помощью Asio, в частности, вызывает async_accept
у
Asio-шного asio::ip::tcp::acceptor
. В
async_accept
передается completion-handler в виде
лямбды, где напрямую вызываются методы агента. Такие вызовы
безопасны потому, что агент привязан к asio_one_thread диспетчеру.
Этот диспетчер специально создан для того, чтобы на одном рабочем
контексте можно было работать и с агентами, и с Asio. Если бы
acl_handler привязывался к какому-то другому диспетчеру (например,
к штатному one_thread-диспетчеру), то этого делать было бы
нельзя.
Во-вторых, логика у acl_handler достаточно тривиальная, но даже
и здесь нашлось место для применения иерархического конечного
автомата (которые в SObjectizer-е уже довольно давно поддерживаются):
Иерархический автомат потребовался здесь потому, что acl_handler
должен ограничивать количество одновременных подключений к точке
входа. Поэтому, когда это количество достигает разрешенного
максимума, агент переходит в состояние
st_too_many_connections
, в котором он делает
практически всю свою обычную работу, за исключением новых вызовов
async_accept
.
В-третьих, агент acl_handler не занимается сам обслуживанием
принятых подключений и обработкой протоколов socks5/http. Вместо
этого для каждого соединения создается т.н. connection_handler. Это
объект, в который отдается принятое подключение и который уже с
этим подключением непосредственно работает. Т.е. читает из него
данные, пытается определить протокол, пытается обслуживать
подключение согласно того или иного протокола.
Т.е. когда в результате async_connect
у acl_handler
появляется новый asio::ip::tcp::socket
, то создается
объект, реализующий интерфейс
connection_handler_t
, и новый socket
отдается этому только что созданному connection_handler-у. Больше
про новое подключение acl_handler ничего не знает. Далее
acl_handler лишь хранит у себя умный указатель на
connection_handler и лишь время от времени дергает у
connection_handler-а метод on_timer
(тот самый
"глобальный таймер" о котором речь шла выше). А вот
connection_handler дальше живет своей собственной и весьма
непростой жизнью.
Вообще говоря, как раз тот кусок arataga, который относится к
connection_handler-ам, является самым мудреным в проекте. Вероятно,
далеко не самым удачным. И, скорее всего, именно он мог бы стать
первым кандидатом на рефакторинг по итогам опытной эксплуатации.
Поскольку первая версия arataga создавалась в высоком темпе, в
условиях сжатых сроков, то ничего лучше сходу придумать не удалось.
Возможно, здесь следовало бы применить stackful-короутины. Но
экспериментировать с ними не было времени. Так что, если история с
arataga получит продолжение (что вряд ли, но вдруг), к этому куску
arataga нужно будет сделать еще один, более вдумчивый подход. Пока
что получилось то, что получилось :(
Использование retained_mbox для распространения
конфигурации
Примечательным моментом использования SObjectizer-овских mbox-ов
в arataga является применение retained_mbox из so5extra
для распространения информации о текущей конфигурации и списках
пользователей.
Особенностью retained_mbox является то, что retained_mbox хранит
у себя последние отосланные сообщения (по одному для каждого типа
отосланных сообщений). И когда кто-то делает новую подписку на
некий тип сообщения, то retained_mbox сразу же после завершения
подписки отсылает этому подписчику последнее сохраненное сообщение
этого типа.
В arataga это свойство retained_mbox используется для того,
чтобы агенты authentificator сразу же могли получить текущий список
пользователей. Сценарий работы такой:
-
стартует агент user_list_processor. Он читает локальную копию
списка пользователей и отсылает сообщение с этим списком в
retained_mbox. На этом этапе подписчиков у retained_mbox еще нет,
но нас это не волнует;
-
затем стартует агент config_processor, который читает локальную
копию конфигурации и создает io_threads вместе с
authentificator-ами;
-
каждый запущенный authentificator должен получить список
пользователей, для чего authentificator-ы подписываются на
соответствующее сообщение из retained_mbox-а;
-
поскольку в retained_mbox уже есть сообщение со списком
пользователей, то это сообщение сразу же отсылается каждому новому
подписчику.
Благодаря этому authentificator-у при старте не нужно ни у кого
ничего явно запрашивать.
Правда, платить за это нужно тем, что authentificator вынужден делать подписку в
so_evt_start
, а не в предназначенном для этого
so_define_agent
.
Некоторые детали использования RESTinio
Теперь попробуем пробежаться по нескольким аспектам применения
RESTinio в arataga. Тем более, что одно из применений было
очевидным, тогда как одно совсем не очевидным.
Первое применение RESTinio, самое очевидное: административный
HTTP-вход
Очевидное применение, ради которого RESTinio и подтягивался в
проект arataga, -- это реализация административного HTTP-входа. Как
раз здесь простота асинхронной обработки входящих запросов
посредством RESTinio и была нужна с самого начала.
В этом месте RESTinio используется классическим способом.
Запускается RESTinio-сервер, этот сервер принимает входящие
запросы, выполняет их первичную валидацию. После чего пересылает
запрос во внутрь arataga и ждет ответа. Когда ответ сформирован, то
RESTinio отсылает его клиенту.
Упомянуть здесь можно разве лишь то, что для взаимодействия
RESTinio- и SObjectizer-частей приложения были сделаны
вспомогательные интерфейсы (раз, два). Когда startup_manager
запускает RESTinio-сервер, то отдает серверу реализацию интерфейса
requests_mailbox_t
. RESTinio-сервер через этот
интерфейс передает в SObjectizer-часть arataga входящие запросы. И
каждый входящий запрос сопровождается реализацией интерфейса
replier_t
. Посредством этого интерфейса реальные
обработчики запроса могут отвечать RESTinio-серверу.
Два эти интерфейса скрывают от RESTinio-части существование
SObjectizer-части arataga и наоборот. Что мне показалось полезным.
Хотя бы плане сокращения времени компиляции отдельных
cpp-файлов.
Второе применение RESTinio, не очевидное, но вполне ожидаемое:
работа с HTTP-заголовками при проксировании HTTP-соединений
Второе применение стало для меня лично неожиданным, хотя оно
было вполне себе логичным. Дело в том, что применить RESTinio в его
текущем виде для проксирования HTTP-соединений не представляется
возможным в силу нынешних архитектурных особенностей RESTinio.
Поэтому работу с проксируемыми HTTP-соединениями в arataga
пришлось делать "с нуля". И одной из составляющей этой работы
является разбирательство с HTTP-заголовками в запросах/ответах, а
также с содержимым некоторых из них.
И вот когда у меня руки дошли до обработки HTTP-заголовков в
проксируемых соединениях, то я внезапно (c) осознал, что в RESTinio
уже есть инструментарий для этого. Чем и воспользовался, хотя
изначально о таком даже и не думал.
Третье применение RESTinio, совсем не очевидное: синтаксический
разбор конфигурации
А вот третье, совсем не очевидное, применение RESTinio не то,
чтобы было сразу мной задумано. Но такой вариант начал
просматриваться чуть ли не с самого начала.
В arataga нужно было парсить два типа конфигурационных файлов
(собственно конфигурация и список пользователя). У каждого из
которых был свой, уже устоявшийся к тому времени синтаксис.
Плюс к тому в этот синтаксис хотелось внести небольшие, но
важные с точки зрения простоты использования, дополнения. Вроде
суффиксов, которые бы обозначали объем данных или времена
таймаутов. Например:
acl.io.chunk_size 16kib # Вместо 16384timeout.authentification 1200ms # Вместо 1200timeout.connect_target 7s # Вместо 7000
Ну и для того, чтобы не делать парсинг конфигов вручную только
штатными средствами стандартной библиотеки C++ (очень куцей в этом
плане), нужно было задействовать какой-то генератор парсеров. А
зачем тащить в проект еще что-то, если нечто подобное уже есть в
RESTinio?
Так что easy_parser из RESTinio, который изначально появился в
RESTinio для упрощения разбора HTTP-заголовков, пригодился в
arataga и для разбора конфигурации.
Любопытный, на мой взгляд, пример использования easy_parser-а из
RESTinio можно найти здесь. Именно этот
transfer_speed_p()
используется для реализации команд
конфигурации вроде:
bandlim.in 850kibbandlim.out 700kib
Заключение
Реальные проекты, подобные arataga, являются отличным полигоном,
на котором разработчик библиотеки может увидеть насколько удобна
его библиотека вне тепличных условий уютного манямирка. Как
правило, при использовании библиотеки в новом проекте отлично
проявляются просчеты в дизайне библиотеки и/или же обнажаются
подводные камни, о которых ты пока что даже и не подозревал. Из
чего затем вырастают вполне себе конкретные идеи о том, куда
следует двигаться дальше.
С RESTinio это сработало на 100%. Опыт применения RESTinio в
arataga дал пинка под за толчок в сторону разработки пусть
приблизительного, но аналога middleware из ExpressJS. Первый
результат уже доступен в
0.6.13. И, если пойти на слом API в ветке 0.7, то можно будет
поддержать и цепочки асинхронных обработчиков.
Но еще более важным итогом использования RESTinio в arataga
стало изменение лично моих взглядов на
позиционирование RESTinio в мире аналогичных фреймворков для
C++.
Так что для RESTinio испытание arataga оказалось исключительно
полезным.
А вот с SObjectizer-ом получилось скучно и обыденно. Он просто
работал. Ничего не пришлось подкручивать или добавлять. Кой-какие
мысли о потенциально привлекательных направлениях дальнейшего
движения появились. Но пока не вполне оформившиеся и не имеющие
однозначно четкого подтверждения того, что это реально необходимо
делать.
Я же надеюсь, что данная статья даст читателям, которые с
любопытством посматривают на RESTinio и/или SObjectizer(+so5extra), но пока что сами эти
проекты не пробовали, возможность взглянуть на реальный код,
написанный с применением наших инструментов. И сделать собственные
выводы о том, нравится ли увиденное или нет.
Что касается перспектив самих RESTinio и SObjectizer/so5extra,
то тут все не так однозначно и без
внешней поддержки, боюсь, их развитие приостановится. Впрочем,
это уже совсем другая история