понедельник, 25 января 2021 г.

[work.opensource.c++] arataga: что это вообще и зачем мы публикуем это в OpenSource?

arataga -- это работающий прототип socks5+http/1.1 прокси сервера, который мы в прошлом году разрабатывали для одного из наших клиентов. К сожалению, этот прототип остался невостребованным. Ну а чтобы не пропадать добру и самопиара ради, мы решили открыть его исходники.

Как все развивалось

Дело было так: с 2019-го года мы работали с заказчиком, который эксплуатировал у себя некий старый прокси-сервер. Весьма старый, написанный с применением модели thread-per-connection, да еще и оставшийся без сопровождения. Собственно, мы как раз и занимались его доработкой под нужды заказчика.

Где-то к концу весны 2020-го стало понятно, что больше ничего хорошего из этого прокси-сервера не выжать. Что нужно его заменять на что-то новое, написанное с нуля или же переделанное готовое (типа nginx или envoy после обработки напильником).

Изначально в этот проект влезать не хотелось вообще, т.к. объем работ предполагался большим, сроки короткими, поэтому наших ресурсов могло тупо не хватить. Но мы согласились помочь в написании ТЗ для будущих исполнителей и уже в процессе подготовки ТЗ сложилось ощущение, что здесь можно будет применить и SObjectizer, и RESTinio, и что при должном разбиении на итерации можно будет справиться и своими скромными силами. Посему ввязались в разработку нового прокси-сервера под требования заказчика.

В итоге в довольно сжатые (как мне кажется) сроки мы сделали прототип, который уже нужно было запускать на нормальное тестирование на площадке клиента... Но сам клиент утратил интерес к этой разработке и перестал реагировать на внешние раздражители.

Что стало неприятным сюрпризом, тем более, что до этого никаких проблем с заказчиком не было. И даже по ходу работ над arataga мы взаимодействовали по другим вопросам и он оплачивал нам сделанные ранее работы.

Наши обязательства перед клиентом закончились. У нас на руках остался arataga и с этим нужно было что-то делать. Например, выбросить и забыть. Или же открыть.

Зачем нужно было делать arataga?

У заказчика были следующие и, как мне представляется, местами весьма специфические условия:

  • нужно было поддерживать протоколы socks5 и http/1.1. Причем желательно было иметь возможность автоматически определять протокол, который использует клиент;
  • на одном прокси-сервере нужно было открывать несколько тысяч точек входа (от 5 до 10 тысяч нужно было уметь поддерживать изначально). Каждая точка входа -- это уникальное сочетание IP адреса и TCP-порта (на серверах клиента было доступно сразу несколько десятков IP-адресов, так что 10K уникальных точек входа не было проблемой вообще);
  • к одному прокси-серверу могло подключаться несколько десятков тысяч разных пользователей. При этом процедура аутентификации должна укладываться в десяток миллисекунд, растягивать ее даже на 50ms, не говоря уже про сотню-другую миллисекунд очень плохо;
  • один прокси-сервер должен быть поддерживать одновременно несколько десятков тысяч параллельных подключений (30-40 тысяч нужно было уметь держать сразу);
  • нужно было спокойно поддерживать принятие новых подключений с темпом в 1000-1500 в секунду;
  • нужно было уметь ограничивать трафик клиента на всех его подключениях к одной точке входа. Например, если клиенту выдали лимит в 10MiB/s, а он сделал 10 параллельных подключений к точке входа, то суммарно его трафик по этим десяти подключениям не должен был превышать 10MiB/s. Плюс к этому нужно было еще и накладывать лимиты для отдельных доменов. Например, общее ограничение для клиента 10MiB/s, но на vk.com этот лимит должен быть снижен до 5MiB/s;
  • управлять прокси-сервером (т.е. засылать в него новую конфигурацию и обновленные списки пользователей) нужно было через POST-запросы на административный HTTP-вход.

Мне сложно судить, насколько это серьезные требования с точки зрения настоящего highload-а. Но для нашей маленькой команды выглядело достаточно внушительно. Тем более, что опыта разработки подобных прокси у нас не было.

Почему не стали дорабатывать какое-то другое решение?

С учетом того, что мы специализируемся на C++ (но можем, при необходимости, в чистый C), то бегло рассмотрели пару существующих OpenSource вариантов: nginx и envoy (кажется рассматривался и какой-то третий проект, но я уже не помню, к сожалению).

Вроде бы вот так сразу взять и удовлетворить все потребности готовыми сторонними продуктами не удавалось, требовались те или иные доработки. А вносить подобные доработки в чужие большие проекты, которые преследуют свои собственные цели и развиваются по чужим планам -- это достаточно рискованно. Наши правки не факт, что примут в upstream. А даже если и примут, то когда и в каком виде? И насколько быстро/просто будет затем вносить еще какие-то специфические правки?

Если же не примут, то тогда тянуть отдельный форк в который время от времени нужно будет вливать изменения из upstream-а?

Плюс к тому, въехать в проект масштаба envoy -- это задачка не из простых.

А еще большого запаса по времени не было. После очередного аврала по приведению старого прокси-сервера в чувство складывалось ощущение, что новое решение нужно начинать внедрять в течении двух-трех месяцев, а лучше бы еще раньше.

В общем решили, что сделать свое решение с нуля на своих технологиях будет надежнее в плане того, чтобы со временем наполнять проект фичами, которые нужны будут именно этому заказчику и только ему.

Возможно, я был сильно не прав, принимая такое решение. Но историю уже не изменишь.

Что в итоге получилось?

В итоге где-то за 2.5 месяца мы сделали работающий прототип, который вроде как выполнял озвученные в ТЗ требования.

Этот прототип мы как могли протестировали у себя локально. Но создать нагрузку, сравнимую с тем, что возникает на серверах заказчика у нас не было физической возможности. А когда мы стали пытаться организовать тестирование на мощностях клиента, клиент перестал выходить на связь.

На своих тестовых стендах прогонял по 15-20K соединений через arataga. Все работало. Больше нагрузить не удавалось, т.к. быстро исчерпывался запас свободных портов. Нужно было больше IP-адресов, больше мощностей... Как раз то, что мы рассчитывали получить на тестовом стенде у заказчика :(

В августе 2020 запустил экземпляр arataga на сервере, который арендую у российского провайдера для личных нужд. И этот экземпляр помогал мне ходить в Интернет когда этот самый Интернет рубили в РБ (правда, тут все зависело от провайдера). Также я через этот экземпляр временами смотрю прямые трансляции на сайтах российских ТВ-каналов, когда те ограничивают доступ с белорусских IP-адресов.

Так что arataga, в основном, работает.

Поскольку это прототип, на котором мы хотели проверить основные проектные решения под нормальной нагрузкой, то в нем есть ряд еще нереализованных фич. Например:

  • собственная асинхронная процедура резолвинга доменных имен с последовательным перебором адресов DNS-серверов, перечисленных в конфигурации. Текущая версия arataga опирается на базовый функционал Asio, который использует локальный резолвинг;
  • текущая тривиальная реализация передачи данных между клиентом и целевым узлом чувствительна к длительности пинга между узлами, поскольку использует один буфер, в который сперва читаются данные с одной стороны, а затем прочитанные данные отсылаются в другую сторону. Более продвинутая реализация должна была бы использовать несколько подобных буферов. Пока в часть из них выполнялось бы чтение, из другой части велась бы запись ранее прочитанных данных;
  • в конфигурации поддерживается только команда disabled_ports, перечисляющая TCP-порты, на которые доступ запрещен. По-хорошему, нужна и обратная команда enabled_ports.

Плюс, при более объемном тестировании наверняка бы вскрылись какие-то косяки в реализации.

Вообще, по моим ощущениям, еще бы месяц тестирования и опытной эксплуатации у заказчика, и arataga бы оказался полностью готов к запуску "в бой". Ну а так он остался в виде работающего прототипа.

Почему мы открываем arataga?

Есть три причины. Две основные, третья так, в довесок.

Во-первых, arataga является отличным примером использования SObjectizer и RESTinio в реальных условиях. Т.е. это не какой-то простенький HelloWorld на сотню-другую строк. И даже не приближенный к реальности демо-проект вроде Shrimp-а. Это именно что код, который мы писали с прицелом на запуск в продакшен. Так что если кто-то хочет увидеть, как используются SObjectizer и RESTinio в реальных проектах, то лучше arataga у нас в наличии ничего нет и даже не предвидится. Эдакий "Shrimp на максималках".

Во-вторых, arataga показывает на что мы способны. Открытые проекты, вроде SObjectizer-а или RESTinio -- это хорошо, конечно, но это библиотеки. Тогда как arataga разработан с нуля под требования клиента и он демонстрирует наши возможности в плане разработки софта под заказ.

Плюс к тому, мы рассматриваем arataga как открытое приглашение к сотрудничеству. Заинтересовавшиеся могут посмотреть, попробовать. Если кому-то чего-то не хватает, то давайте пообщаемся. Можно взять и доработать arataga под чьи-то специфические нужды.

Ну а третья причина -- это возможность использовать arataga в качестве полигона для проверки новых версий SObjectizer, so5extra и RESTinio.

Что в arataga было сделано непосредственно перед публикацией на github?

Я сказал, что arataga можно рассматривать как отличную иллюстрацию наших возможностей по разработке софта на заказ. Но для того, чтобы эта иллюстрация была достоверной, нужно сказать, что в arataga было переделано/доделано/добавлено непосредственно перед публикацией на github-е.

Был сделан переход на RESTinio-0.6.13. Как раз новая фича sync_chain из версии 0.6.13 была задействована для последовательной проверки управляющих запросов, приходящих на административный HTTP-вход.

Немного был изменен код по парсингу конфигурации. Некоторые параметры были переименованы (т.к. старые названия имели смысл только когда arataga рассматривался в качестве замены старому прокси-серверу у клиента), некоторые параметры были сделаны более гибкими. Немного был подчищен код за счет использования возможностей easy_parser из RESTinio-0.6.11 (этих возможностей в RESTinio еще не было, когда парсинг конфигурации был написан).

Код arataga был скомпилирован GCC 10 и clang 11. Исправлено несколько предупреждений от компиляторов, которые не были видны на использовавшемся изначально GCC 8.

Добавлены файлы README.md и README_USER_LIST.md.

Файлы README_CMDLINE.md, README_CONFIG.md, curl_examples.md остались практически такими, как мы их подготовили к выходу на тестирование у заказчика. Разве что отразили самые свежие изменения в названии команд конфигурационного файла.

Все остальное осталось таким же, каким оно было на момент приостановки работ в конце июля 2020-го. Т.е. качество и вид исходного кода, включая комментарии в коде -- это все именно в том виде, в котором мы и ведем разработку. Комментарии и документация на русском, поскольку проект делался для российского заказчика.

Доработка arataga перед публикацией заняла около четырех дней. Два из которых -- это обновление/дополнение документации и написание этого поста.

Общие трудозатраты на arataga -- порядка 2.5 месяцев умноженные на 1.25. Большую часть работ делал я сам, местами мне помогал коллега. Объем кода ~16KLOC не считая тестов:

cloc arataga
      91 text files.
      91 unique files.                              
       0 files ignored.

github.com/AlDanial/cloc v 1.74  T=0.19 s (490.5 files/s, 131612.5 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C++                             29           1948           2026          11504
C/C++ Header                    56           1397           2655           4772
Ruby                             6             38              0             77
-------------------------------------------------------------------------------
SUM:                            91           3383           4681          16353
-------------------------------------------------------------------------------

По срокам мы ошиблись со своими предположениями раза в 1.5. Изначально я надеялся, что сможем уложиться в 7-8 недель перед выходом на тестирование у заказчика. Но трудно было предсказывать не имея хорошего представления об объеме задачи по проксированию HTTP.

Конструктивные особенности arataga

Готовится отдельная статья для Хабра, в которой про архитектуру и реализацию arataga собираюсь рассказать подробнее. Надеюсь через четыре-пять дней ее опубликовать (upd: вот эта статья). Сейчас же кратко и тезисно по основным пунктам.

Старый прокси-сервер, который использовался у заказчика, работал по схеме thread-per-connection. С учетом всего того геморроя, к котором эта схема приводит под большими нагрузками, в arataga мы задействовали что-то вроде thread-per-core.

Суть в том, что arataga при старте запускает N так называемых io-threads. Где N задается либо пользователем, либо вычисляется автоматически как (nCPU-2), где nCPU -- это количество вычислительных ядер.

Фактически, роль io-thread исполняет экземпляр SObjectizer-овского диспетчера asio_one_thread из so5extra. Т.е. arataga запускает N экземпляров asio_one_thread, у каждого из которых есть свой собственный объект asio::io_context для выполнения операций с сетью.

Каждая точка входа, описанная в конфигурации, реализуется в виде SObjectizer-овского агента. Этот агент привязывается к тому или иному экземпляру asio_one_thread (т.е. к одной из созданных io-threads). Далее этот агент живет только на той io-thread, к которой он был привязан. И работает этот агент только с тем io_context-ом, который был создан для этой io-thread.

Так же на каждую io-thread arataga создает отдельного агента dns_resolver (для разрешения доменных имен) и отдельного агента authentificator (для аутентификации подключившихся клиентов). Оба эти агента привязываются к диспетчеру asio_one_thread для этой io-thread.

В результате получается, что агенты, реализующие точки входа, в основном взаимодействуют только с сущностями (в первую очередь io_context, dns_resolver и authentificator), которые живут на той же самой io-threads. Получается что-то вроде схемы thread-per-core.

На отдельных рабочих нитях живут другие сущности arataga. Во-первых, это агент config_processor, который обрабатывает изменения в конфигурации. Во-вторых, это административный HTTP-вход, который был реализован средствами RESTinio.

HTTP-запросы, которые приходят на административный HTTP-вход, обрабатываются RESTinio асинхронно: они пересылаются в виде SObjectizer-овских сообщений агентам, которые в состоянии конкретный запрос обработать.

Возможности RESTinio были задействованы не только для реализации административного HTTP-входа. Так, средства для работы с HTTP-заголовками из RESTinio использовались при проксировании соединений по протоколу HTTP. А easy_parser из RESTinio использовался для работы с конфигурацией.

В общем-то, как раз части, которые были завязаны на SObjectizer и RESTinio, были сделаны быстро и вызвали меньше всего забот. Кому-то может показаться, что SObjectizer здесь был и не нужен, что можно было бы обойтись голыми нитями и какими-то самодельными аналогами thread-safe очередей. Но у меня другое мнение :)

Перспективы arataga

Перспективы напрямую зависят от реакции публики.

Попробует кто-то использовать arataga и предоставит нам фидбэк -- можно будет подумать и развитии.

Но самые хорошие перспективы будут разве что в том случае, если кто-то захочет вложиться в развитие arataga финансово. Под заказ мы готовы дорабатывать arataga, в том числе и в виде закрытых кастомизированных версий. Так что если кому-то что-то нужно, то выходите на связь, с удовольствием обсудим варианты.

Если же вкладываться в arataga никто не пожелает, то, вероятнее всего, arataga будет обновляться по мере выхода новых версий RESTinio/SObjectizer. Ну или когда мы сами будем натыкаться на какие-то косяки при использовании arataga в личных целях.

Тема стороннего вклада в развитие arataga сложная. Вероятно, мы не будем принимать pull-request-ы, код в которых лицензируется только под AGPL, т.к. мы хотим сохранить возможность делать свои приватные форки. А если принимать чужие PR-а, в которых код будет под AGPL, то такой возможности у нас не останется. Если же автор PR согласен передать нам все права на свои изменения (за исключением авторских), то мы, конечно же, рассмотрим такой PR.

В общем, если нашли в arataga какой-то косяк, то открывайте issue. Руки дойдут -- исправим. Если же хотите запилить в arataga что-то свое, то нет проблем -- делайте форк и дорабатывайте его под AGPL сколько угодно. Мы только от всей души удачи вам пожелаем.

Заключение

В заключение остается сказать что мы получили важный и серьезный урок. Опыт неприятный, но когда-то приходится учиться на своем опыте, каким бы горьким он не был.

Зато у нас оказался проект, который демонстрирует наши возможности. Надеюсь, эти возможности заинтересуют потенциальных клиентов. А внимание клиентов нам не помешает. Но об этом в другой раз.

Напоследок у меня просьба: если вы пришли сюда по ссылке в какой-то из соцсетей (вроде Facebook, LinkedIn или Twitter), то не сочтите за труд, поделитесь этой ссылкой. Чем больше людей увидят этот пост, тем лучше все может сложится и для arataga, и для нас.

Да, если кому-то интересно, то по-белорусски "аратага" -- это пахарь.


PS. Поскольку это важный пост, то он повисит сверху до конца января.

PPS. Не буду озвучивать ни заказчика, ни каких-либо названий и имен. Как и не буду погружаться в детали и кого-то обвинять. 2020-й был странным годом и мало ли что у кого могло пойти не так (у многих как раз и пошло). Я и сам, наверное, мог бы разрулить ситуацию по-другому. Но, опять-таки, в 2020-ом временами было вовсе не до работы. Посему что случилось, то случилось. Выводы сделаны, если кого-то и назначать виновным, то только меня самого.

Комментариев нет: