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-ом временами было вовсе не до работы. Посему что случилось, то случилось. Выводы сделаны, если кого-то и назначать виновным, то только меня самого.
Комментариев нет:
Отправить комментарий