В какой-то момент написанного кода становится слишком много для того, чтобы просто на него забить.
Предыстория
Я backend разработчик с опытом около 3-х лет, пишу в основном на Golang. Проработал в нескольких крупных российских компаниях. Сейчас я параллельно со своей работой пытаюсь сделать удобный, дешевый VPN сервис с высокой пропускной способностью. В этой статье я хочу просто рассказать про жизненный цикл своего проекта. Возможно кому-то будет просто интересно почитать, а кто-то может почерпнуть что-то новое для себя.
Как и у всех жителей в РФ в какой-то момент времени у меня возникла потребность использовать VPN. Я начал использовать платный VPN сервис, но потом блокировки добрались и до него. Блокировки начали происходить все чаще и чаще, и я понял что надо попробовать сделать свой.
Я начал выбирать протокол для VPN и через некоторое время мой выбор пал на wireguard. Причиной данного выбора стала простота настройки, высокая пропускная способность, наличие sdk для различных платформ, а также приятное в использовании приложение.
Я арендовал сервер, и настроил на нем wireguard. В интернете есть много статей, в которых рассказывается, как настраивается wireguard на сервере. Я пользовался данной статьей от digital ocean. Сделал себе конфигурацию и начал пользоваться своим собственным сервером. Моя история могла закончиться на этом месте, но, как оказалось, я был не единственным, кому нужен был VPN. Сначала VPN понадобился членам моей семьи, потом VPN понадобился друзьям, потом друзьям друзей и тд.
Серверы стоят довольно дешево, можно за 10 евро арендовать сервер, который будет обслуживать до 200 пользователей, при этом у некоторых cloud провайдеров практически отсутствует ограничение по трафику. Bandwidth в 1 Гбит тоже очень сложно забить. Самое тонкое место оказалось CPU.
Когда количество пользователей начало увеличиваться, я принял решение автоматизировать процесс получения доступа к своим серверам за небольшую оплату. Количество пользователей быстро выросло, и надо было думать над тем, как балансировать нагрузку на какой-то пул серверов.
Немного про деплой
В качестве клиента, с помощью которого пользователи могут получать конфигурации для подключения к серверам, я решил выбрать телеграм бот. Для таких целей телеграм бот идеальный клиент, так как не требует больших временных затрат на разработку. При появлении большого количества кнопок и действий в телеграм боте код быстро деградирует, и его становится сложно понимать, поэтому я написал отдельную либу для работы с кнопками в телеграм боте. В ближайшее время я опубликую эту библиотеку.
Я решил сделать отдельный сервер, который занимается вопросами подписки пользователей, далее - master_server. Серверы на которые установлен wireguard, и идет трафик от пользователей, далее буду называть - slave_server.
В то время я не знал про github actions, поэтому использовал самописную либу для мониторинга обновлений в github, для пересборки проекта при обновлении master branch.
https://github.com/tarmalonchik/git_trigger (не рекомендую пользоваться этим)
Я уже опубликовывал эту библиотеку на хабре, и мне в комментариях посоветовали перейти на github actions, что я в последствии и сделал.
Так как на master_server стоит http_server, grpc_server и большое количество воркеров для обработки телеграма, рассылок уведомлений и тд, пришлось написать отдельную библиотеку для запуска сервиса. Тут есть graceful shutdown с таймаутом, обработка паник, а также возможность добавлять воркеры на лету.
https://github.com/vkidmode/server-core (этим можно пользоваться)
Про взаимодействие slave_server и master_server я расскажу подробнее в следующей статье.
Проблема балансировки
Минимальная конфигурация wireguard выглядит примерно вот так:
[Interface]
PrivateKey = <client_private_key>
Address = <cient_address_on_server>/32
DNS = 8.8.8.8, 1.1.1.1
[Peer]
PublicKey = <server_private_key>
AllowedIPs = 0.0.0.0/0
Endpoint = <server_ip_address>:<server_port>
Можно заметить, что в данной конфигурации явно прописан IP адрес сервера. Это одна из проблем, так как хочется иметь возможность поменять сервер. Если просто прописать в конфигурации адрес сервера, то при изменении сервера придется вносить изменения во все конфигурации. Некоторые пользователи устанавливают VPN своим родителям, бабушкам, дедушкам, которые не будут иметь возможности поменять IP адрес в случае необходимости.
Я решил найти балансировщик нагрузки, для того чтобы указывать в конфигурации wireguard адрес load balancer. Так как пакеты wireguard передаются по UDP протоколу, надо искать балансировщик UDP трафика. Данная схема должна была выглядеть вот так:
![](https://habrastorage.org/getpro/habr/upload_files/5f4/071/6dd/5f40716ddb025ad77a03f14801bceaa2.png)
Для того чтобы реализовать данную схему, необходимо использовать балансировщик нагрузки. Тут стоит упомянуть, что балансировка бывает на разных уровнях: Прикладной, Транспортный и Сетевой. В моем кейсе я использовал балансировку на сетевом транспортном уровне. Также бывают разные алгоритмы балансировки. К примеру Round Robin и Least Connections. К слову, балансировка Round Robin - это самый тупой и простой способ балансировки, можно сказать, рандомная балансировка. Балансировка по алгоритму Least Connections более предпочтительна, так как она учитывает количество соединений, когда решает куда отправить запрос.
Некоторые cloud провайдеры, такие как hetzner или digital ocean предоставляют отдельные серверы, выполняющие роль балансировщика нагрузки. Но, как оказалось, они предоставляют только балансировку tcp трафика. Связано это с тем, что балансировка tcp и udp происходит по-разному. Нам важно, чтобы пакеты в рамках одного запроса отправлялись на один и тот же сервер, иначе один запрос размажется на несколько серверов, и мы не получим ответа. Tcp запрос имеет сессию, поэтому можно производить балансировку по tcp сессии, udp же не имеет такого понятия как сессия, поэтому механизмы балансировки tcp и udp различные.
В итоге, я остановил свой выбор на nginx. Nginx дает возможность балансировать udp трафик. Это делается довольно просто. Для этого в конфигурации nginx есть отдельный блок для настойки балансировки:
upstream stream_backend {
least_conn;
server server1:51810;
server server2:51810;
server server3:51810;
}
У nginx есть еще более продвинутые методы балансировки, нежели Least Connections. К примеру least_time
, который учитывает не только количество соединений, но также среднее время ответа с сервера. Для меня эта настройка не имеет особого смысла, так как мой сервер будет проксировать запросы через себя, и nginx будет учитывать время ответа каких-то внешних ресурсов, поэтому использовал просто least_conn.
Под выбором алгоритма балансировки необходимо указать пулл серверов. То есть теперь для добавления сервера необходимо всего лишь поправить конфигурацию в nginx и сделать service nginx reload
.
Плюсы и минусы данного решения
Изначально предполагалось, что балансировка нагрузки с помощью load balancer - хорошая идея. Я думал, что большая часть вычислительных мощностей серверов расходуется на encryption/decryption upd пакетов в wireguard, и load balancer не будет сильно нагружаться, так как занимается только пробрасыванием пакетов. Но я ошибался, после того как я нагрузил серверы, оказалось, что нагрузка на load balancer получается примерно такой же как и на конечном сервере. Выходит, что балансировать нагрузку таким образом (в моем кейсе) не самая лучшая идея. Дополнительную проблему создает то, что трафик считается на load balancer как суммарный трафик со всех серверов. Так что, даже в том случае, если бы CPU нагрузка на load balancer была низкой, мне пришлось бы платить двойную стоимость за трафик.
Плюсы:
Легко добавлять и убирать новые серверы
Качественная балансировка, нагрузка равномерно распределяется по серверам (она не учитывает количество трафика от пиров, но лучше, чем слепое распределение)
Минусы:
Load Balancer сервер нужно выбирать мощный, а значит, увеличивается стоимость
Переплата за трафик
Существует единая точка отказа, если Load Balancer чувствует себя плохо, то все клиенты испытывают проблемы
Сложность масштабирования, если количество серверов превысит 10-15, то невозможно будет найти сервер на роль балансировщика
Ограниченность канала балансировщика.
Далее
Эта статья не о том, как надо делать или как не надо, а скорее описание процесса проб и ошибок. Если эта статья покажется кому-то интересной, то я напишу, что было дальше, какие проблемы возникали, и как они были решены.