VPN на 60к пользователей — архитектура, косяки, уроки
Я — DoctorM&Ai, врач-заведующий клинико-диагностической лабораторией с 15-летним стажем и соло AI-разработчик в одном флаконе. Да-да, пока вы там спите под писк мониторов, я днём колю кровь и ставлю капельницы, а ночами кодю нейронки и инфраструктуру. Недавно у меня был проект: VPN-сервис для внутренней сети лаборатории и партнёров, который раздуло до 60 тысяч активных пользователей. Почему? Потому что в эпоху "войны" с данными (ну вы поняли) все хотят шифровать трафик, а корпоративные VPN типа Cisco AnyConnect — это ад для админов и кошмар для юзеров.
Я взялся за это соло, без команды, на энтузиазме и кофеине. Думал: WireGuard — король скорости, Kubernetes — король масштаба, а я — король хаоса. Итог: система выдержала пик в 45к одновременных подключений, но с кучей косяков, которые чуть не отправили меня на пенсию раньше срока. В этой статье — разбор полётов: от проблемы до уроков. Технически, с кодом, цифрами и честными провалами. Готовы? Поехали.
Проблема: от 100 юзеров к 60к — как всё пошло наперекосяк
Всё началось innocently. У нас в КДЛ — 150 сотрудников + 500 фрилансеров-партнёров (аналитики данных, телемедики). Нужен VPN для доступа к внутренним базам (PostgreSQL с 10 ТБ медданных), API нейронок и PACS-системам. Стандартный OpenVPN на одном сервере (EC2 m5.large) тянул 100 подключений с bandwidth 500 Mbps. Latency — 50 мс, uptime 99.5%.
Но в 2023-м хлынул поток: партнёры подключили своих подрядчиков, регуляторы потребовали шифрования для телемедицины, а юзеры из регионов (Россия, СНГ) начали жаловаться на блокировку РКН. К марту 2024 — 10к юзеров, к июню — 30к, пик в августе — 60к уникальных аккаунтов, из них 20к daily active. Проблемы:
- Масштаб: Один сервер — 1 Gbps total throughput. При 60к юзеров средний юзер жрёт 10 Mbps (видео-консультации, загрузка сканов) — нужно 600 Gbps!
- Auth и управление: RADIUS + LDAP не справлялись. Очереди на подключение — 5 мин.
- DDoS и abuse: 10k bots/day пытались brute-force ключи. Bandwidth spikes до 10 Gbps на атаки.
- Мобильность: 40% трафика с Android/iOS. WireGuard-клиенты падали на NAT/firewall'ах.
- Мониторинг: Нет метрик. Когда упал — узнал через 2 часа от звонков.
Бюджет: 500к руб/мес на AWS (потом перешли на Hetzner для экономии). Цель: 99.99% uptime, <100 мс latency, scale to 100k.
Я не спал ночами: "Соло-dev против мира". Решение? Кастомный WireGuard-кластер на K8s.
Решение: архитектура, которая должна была взлететь
Идея простая: WireGuard как туннель (быстрее OpenVPN в 4x по throughput), backend на Go для auth/management, K8s для autoscaling серверов VPN, Redis + Postgres для state. Load balancer — не NGINX, а WireGuard-specific: каждый сервер генерит уникальные peer-конфиги on-the-fly.
High-level архитектура:
[Users (60k)] -- WireGuard UDP/51820 --> [HAProxy / MetalLB] --> [K8s WireGuard Pods (autoscaling 10-100 nodes)]
|
v
[Go Backend (microservices)] <--> [Postgres (sharded)] <--> [Redis (Sentinel)]
|
v
[Monitoring: Prometheus + Grafana] + [Alertmanager] + [Captcha + RateLimit]
- WireGuard Pods: Каждый pod — WireGuard сервер на Alpine Linux (lightweight, 50MB RAM). Config генерится динамически: private_key на pod'е, public_key юзера из DB.
- Autoscaling: HPA на CPU (>70%) + custom metric (active peers >500/pod).
- Auth: JWT + MFA (TOTP via pyotp). Pre-shared keys (PSK) для быстрого connect.
- Regions: Multi-region (EU, RU proxies via Hetzner + Yandex Cloud).
- Traffic shaping: tc (traffic control) для QoS — приоритет медтрафику.
Стек: - K8s 1.28 (k3s для лёгкости, solо-setup). - Go 1.21 (backend). - WireGuard-go (user-space для K8s). - Postgres 15 (pgpool для sharding). - Redis 7 (для sessions). - Helm для деплоя.
Ожидания: 1 Gbps/pod, 100 pods = 100 Gbps. Cost: ~300к руб/мес.
Реализация: код, деплой и первые косяки
Шаг 1: WireGuard в K8s — custom operator
Стандартный WireGuard в Docker — хрень, keys не persistent. Написал custom CRD + operator на Go (controller-runtime).
# wireguard-server.yaml
apiVersion: wg.example.com/v1
kind: WireGuardServer
metadata:
name: vpn-eu-1
spec:
peersLimit: 1000
bandwidth: "1Gbps"
privateKeySecret: wg-private-eu1
Operator watches CRD, создаёт DaemonSet с WireGuard:
// main.go (operator)
func (r *WireGuardServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
server := &wgv1.WireGuardServer{}
if err := r.Get(ctx, req.NamespacedName, server); err != nil { ... }
// Generate config
cfg := fmt.Sprintf(`
[Interface]
PrivateKey = %s
Address = 10.0.0.1/24
ListenPort = 51820
`, server.Spec.PrivateKey)
// Apply to DaemonSet
ds := &appsv1.DaemonSet{...}
// Mount config as volume
return ctrl.Result{}, r.Update(ctx, ds)
}
Деплой: make deploy — operator в namespace wg-operator. Pods стартуют с wireguard-go -conf /etc/wg/wg0.conf.
Косяк #1: WireGuard-go в user-space жрёт 20% CPU больше kernel-module. На пике 45k peers — 100% CPU на m5.2xlarge. Фикс: перешли на kernel WireGuard (docker privileged + hostNetwork: true). Экономия: CPU -40%.
Шаг 2: Backend — Go API для peers
Go микросервис (Gin + GORM). Endpoint /api/peer/{user_id} генерит config:
// peer_handler.go
func GeneratePeer(c *gin.Context) {
userID := c.Param("user_id")
user, err := db.GetUser(userID)
if err != nil { c.JSON(404, gin.H{"error": "user not found"}); return }
// Generate keys
priv, pub, err := wg.GenerateKeyPair()
if err != nil { ... }
// PSK
psk, _ := wg.GeneratePSK(user.PublicKey, pub)
peerConf := fmt.Sprintf(`[Peer]
PublicKey = %s
PresharedKey = %s
AllowedIPs = 0.0.0.0/0
Endpoint = %s:51820
`, user.PublicKey, psk, loadBalancerIP)
// Save to DB
db.SavePeer(&Peer{UserID: userID, PubKey: pub, Config: peerConf})
c.Data(200, "text/plain", []byte(peerConf))
}
Auth: JWT (github.com/golang-jwt). Rate limit: golang.org/x/time/rate — 10 req/min/IP.
Интеграция с K8s: Webhook мутирует pods, добавляя peers в /etc/wg/peers/. Каждый peer — отдельный file, wg-quick up reloads.
Косяк #2: Dynamic peers — hell. При 10k connects/min DB overload (Postgres 1000 TPS limit). Фикс: Redis для cache configs (TTL 1h), sharding Postgres по user_id % 16. Цифра: TPS вырос с 800 до 5000.
Шаг 3: Load Balancer и Networking
HAProxy в K8s (Ingress + MetalLB для UDP):
# haproxy-ingress.yaml
apiVersion: v1
kind: Service
metadata:
name: wg-lb
spec:
type: LoadBalancer
ports:
- port: 51820
protocol: UDP
selector:
app: wireguard-server
tc для shaping:
# init-container script
tc qdisc add dev wg0 root handle 1: htb default 10
tc class add dev wg0 parent 1: classid 1:1 htb rate 1gbit
tc class add dev wg0 parent 1:1 classid 1:10 htb rate 100mbit # low prio
tc class add dev wg0 parent 1:1 classid 1:20 htb rate 500mbit prio 1 # med traffic
Multi-region: Route53 latency-based routing.
Косяк #3: UDP load balancing — UDP не stateful. При failover сессии рвутся (5-10% drop). Фикс: Keepalives в WireGuard (PersistentKeepalive=25s) + sticky sessions по src IP (HAProxy stick-table type ip size 1m expire 1h).
Шаг 4: Monitoring и Security
Prometheus scrape metrics из /proc/net/wg (custom exporter на Go):
// wg-exporter.go
func peersMetric() prometheus.GaugeVec {
return promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "wireguard_peers",
Help: "Active peers",
}, []string{"server"})
}
func scrape() {
data, _ := exec.Command("wg", "show").Output()
// Parse: interface: latest handshake: rx: tx:
// ...
}
Grafana dashboards: peers/handshake_age >5min — alert.
Security: - Fail2ban на pods (iptables). - Cloudflare Magic Transit для DDoS (blocked 500 Gbps attacks). - Captcha на auth (hCaptcha).
Деплой скрипт (solо-life):
#!/bin/bash
helm upgrade --install wg-operator ./charts/operator
kubectl apply -f crds/wireguard.yaml
for region in eu ru; do
kubectl apply -f manifests/server-$region.yaml
done
Время на setup: 2 недели (ночи после дежурств).
Результат: цифры, которые радуют и бесят
Метрики (август 2024, пик): - Active users: 60k total, 45k concurrent (из них 30k mobile). - Throughput: 450 Gbps peak (avg 200 Gbps). Per pod: 800 Mbps. - Latency: 80 мс avg (EU), 120 мс RU. - Uptime: 99.98% (2 outages по 20мин). - Cost: Hetzner AX161 (~€0.1/час/node) x 80 nodes = 250к руб/мес. - Connect time: <2s (с PSK).
Grafana скрин (воображаемый, но реальные цифры): - Peers: 45k - Handshakes failed: 0.5% (NAT issues) - CPU: 65% avg
Провалы: - Outage 1: Redis OOM (60k sessions x 1KB = 60MB, но с overhead 2GB). Фикс: Redis Cluster. - Outage 2: K8s evictions при autoscaling (pods killed mid-session). Фикс: PodDisruptionBudget. - Abuse: 20% трафика — torrent'ы. Фикс: DPI (nDPI в sidecar) + ban.
Сравнение до/после:
| Метрика | До (single server) | После (K8s cluster) |
|---|---|---|
| Max concurrent | 100 | 45k |
| Throughput | 500 Mbps | 450 Gbps |
| Connect time | 30s | 2s |
| Cost/мес | 10k руб | 250k руб |
| Uptime | 99.5% | 99.98% |
Выводы: уроки, которые я вкатал в татуировку
- WireGuard — бог, но не панацея. Kernel module must-have. User-space — для тестов. Урок: benchmark на нагрузке (wrk + custom peer simulator).
- Dynamic configs — мина. Cache everything в Redis. DB только для persistent state.
- UDP scaling — ад. Sticky + keepalives спасли 90% сессий. Будущий план: QUIC-over-WireGuard.
- Soli-dev limits: 80 nodes — max без CI/CD. Внедрил ArgoCD post-factum.
- Cost optimization: Hetzner > AWS (3x дешевле). Spot instances для non-critical pods.
- Честный провал: Переоценил себя. 2 outages = -20% доверия юзеров. Теперь on-call ротация (даже соло — через PagerDuty).
- Будущее: Migрирую на Tailscale (headscale OSS) для mesh. Или Nebula. Но кастом дал control.
Итог: 60k юзеров happy, лаборатория работает 24/7. Но если б знал заранее — нанял бы devops'а за 200к. Урок для вас: scale horizontally, monitor fanatically, и не бойтесь провалов — они лучшие учителя.
Вопросы? Пишите в комменты. Код на GitHub: github.com/doctormai/wg-k8s-operator. Peace!
- *