Pule rdzeni i router
Dla kogo
Ta strona jest dla operatora instalacji Orbito, który uruchamia platformę na kilku serwerach. Autor modułu nie musi nic z tego robić: moduł działa tak samo na jednym i na wielu serwerach, a SDK samo łączy się ponownie, gdy firma zmienia proces rdzenia.
Rdzeń Orbito (core-back) może działać jako jeden proces albo jako pula rdzeni — kilka procesów (członków) na kilku serwerach. Firma należy do jednej puli i jest przypięta do jednego członka, który obsługuje ją w całości: REST panelu, WebSocket, połączenia modułów, automatyzacje i kontenery. Gdy członek albo cały serwer pada, jego firmy w ciągu kilkunastu sekund przejmują pozostali członkowie puli. Wdrożenie nowej wersji rdzenia odbywa się bez przestoju: członek oddaje firmy po kolei (drain), dostaje nowy obraz i wraca.
Przed członkami stoi router (core-router) — bezstanowy proces na każdym serwerze aplikacyjnym, który kieruje każde żądanie i każde połączenie modułu do członka, który w tej chwili trzyma firmę.
Topologia
Cloudflare Load Balancer (health check GET /health)
┌───────────────────┴───────────────────┐
app1 app2 app3
┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────┐
│ nginx → router │ │ nginx → router │ │ router (opcja) │
│ członek app1-a (pula A) │ WG │ członek app2-a (pula B) │ WG │ członek app3-a │
│ członek app1-b (pula B) │◄────────►│ członek app2-b (pula B) │◄──►│ Mongo: arbiter │
│ Mongo: dane │ │ Mongo: dane │ │ k3s: agent │
│ moduły: lokalny Docker │ │ k3s: serwer, moduły │ │ │
└──────────────────────────┘ └──────────────────────────┘ └──────────────────┘
Każda warstwa na ścieżce żądania istnieje na co najmniej dwóch serwerach: wejście (Cloudflare), router, członkowie rdzenia, moduły (k3s) i baza (replica set).
Komponenty
| Komponent | Rola |
|---|---|
| Cloudflare Load Balancer | wejście https://<domena panelu>; kieruje tylko do zdrowych serwerów (monitor GET /health routera) |
| nginx (na każdym serwerze) | statyczny front, /api, /ws i /health → lokalny router |
router core-router | czyta z Mongo przypięcia firm i adresy członków; kieruje REST, WS i RPC modułów do właściciela firmy |
| członek puli | ten sam obraz rdzenia, konfigurowany przez ENV; zgłasza się heartbeatem i trzyma dzierżawy swoich firm |
| MongoDB replica set | jedyny wspólny stan: dane, definicje pul, członkowie, dzierżawy firm |
| WireGuard | sieć prywatna między serwerami: router → członek, członek → Mongo, Mongo ↔ Mongo, członek → API k3s |
| k3s | moduły firm, które mają przetrwać awarię serwera; router w klastrze jako Deployment ×2 + Service core-router |
Jak router rozpoznaje firmę
| Ruch | Skąd firma |
|---|---|
| REST panelu | nagłówek x-company-id albo ścieżka /companies/:companyId/... |
REST bez firmy (logowanie, lista firm, /admin/*…) | dowolny żywy członek puli domyślnej (round-robin) |
POST /webhooks/* | rozesłany do wszystkich żywych członków |
| WebSocket | parametr ?companyId= w adresie połączenia |
| RPC modułu | token modułu z pierwszej ramki rejestracji (claim companyId) |
Moduł nigdy nie zna adresu rdzenia — dzwoni do routera na swoim serwerze (lokalny Docker: brama sieci kontenerów; k3s: Service core-router), a router łączy go z właścicielem firmy po WireGuard. Dzięki temu przejęcie firmy przez innego członka nie restartuje kontenerów modułów.
Pule, członkowie, dzierżawy
- Pula jest definiowana w panelu administratora (strona „Pule”): nazwa, region, obsługiwane środowiska modułów (np.
local-docker,k3s) i flaga puli domyślnej — jedna na region; trafiają do niej firmy bez przypisanej puli. - Członek dołącza do puli przez ENV (
CORE_POOL) i co 2 s odnawia swój wpis w bazie. Wpis starszy niż 6 s oznacza członka martwego. Członek, którego region albo środowiska nie pokrywają puli, nie przejmuje firm (widać go w panelu i w logu). - Dzierżawa firmy — firmę przejmuje ten członek, któremu pierwszemu uda się atomowy zapis „wolna albo wygasła”. Członek trzyma najwyżej równy udział firm puli i nie podkrada ich innym; nowy członek dostaje firmy, gdy się zwolnią albo po ręcznym „rebalance” w panelu.
Firma na lokalnym Dockerze jednego serwera musi należeć do puli, której wszyscy członkowie stoją na tym serwerze — chroni ją to przed awarią procesu rdzenia, ale nie serwera. Pełną odporność na awarię serwera mają firmy, których moduły działają w wielowęzłowym k3s.
Kolejność uruchamiania
- Zegar i sieć. NTP na każdym serwerze (router ocenia żywotność członków własnym zegarem). Mesh WireGuard między serwerami, firewall tunelu, Docker startujący po tunelu.
- MongoDB replica set — dwa węzły z danymi i arbiter, po WireGuard, z
keyFilei osobnymi użytkownikami dla rdzenia (readWrite+dbAdmin) i routera (read). Przed konwersją istniejącego węzła zrób kopię. - Klucze JWT rdzenia — ta sama para Ed25519 na każdym serwerze (wolumen
keys/). Nie generuj nowych: podpisują tokeny modułów i sesji, a klucz publiczny weryfikuje repozytorium modułów i router. - Pule w panelu — przynajmniej jedna pula domyślna w regionie. Bez definicji puli członek nie przejmuje firm.
- Członkowie i router na każdym serwerze (
docker compose up -d). Router kieruje wyłącznie do członków zgłoszonych w bazie, więc stawiaj go razem z członkami — rdzeń bezCORE_POOLnie zgłasza się i nie może stać za routerem. - nginx na każdym serwerze:
/api,/ws,/health→ router; ten sam build frontu na każdym serwerze. - k3s (jeśli moduły działają w klastrze): router w klastrze, NetworkPolicy modułów (egress tylko DNS i router),
advertiseśrodowiska wskazujące Servicecore-router. - Cloudflare — load balancer z originem każdego serwera i monitorem
/health.
Konfiguracja członka
Członek to obraz rdzenia z konfiguracją wyłącznie w ENV i wolumenach. Wartości wspólne (DB_URI z wszystkimi węzłami replica setu, klucze JWT, sekrety szyfrowania, rejestr obrazów, repozytorium modułów, poczta, AI) muszą być identyczne u wszystkich członków — firma przechodzi między nimi w locie. Wartości per członek:
CORE_POOL=eu1 # pula; brak = tryb zgodności (jeden proces, wszystkie firmy)
CORE_MEMBER=app2-a # unikalny w całym systemie
CORE_REGION=eu # musi się zgadzać z regionem puli (domyślnie eu)
CORE_ADVERTISE_HOST=10.8.0.2 # adres WireGuard, pod którym router dociera do członka (wymagany z CORE_POOL)
REST_PORT=3023
WS_PORT=3024
MODULE_RUNTIMES_FILE=/app/runtimes.yaml
Porty publikuj na hoście pod tymi samymi numerami i tylko na adresie WireGuard — członek ogłasza routerowi CORE_ADVERTISE_HOST + swoje porty.
Plik środowisk (runtimes.yaml) członka opisuje środowiska modułów i nasłuch RPC, na który dzwoni router. listen.port = port publikowany, advertise = adres lokalnego routera, który dostają moduły:
default: k3s
rpc:
upstream: # nazwa = rpc.upstreamListener routera
listen: { host: 0.0.0.0, port: 7723 }
advertise: { host: core-router.orbito-modules.svc, port: 7801 }
runtimes:
k3s:
type: kubernetes
rpc: upstream
kubeconfig: /app/k8s/k3s.kubeconfig
namespace: orbito-modules
Nasłuch wskazany routerowi musi być bez TLS — router mówi do członka gołym TCP po WireGuard. Zmiana advertise odtwarza kontenery modułów raz (adres RPC jest zapisany w etykiecie kontenera); przejście firmy między członkami — nie.
Konfiguracja routera
Plik YAML (CORE_ROUTER_CONFIG, domyślnie ./config.yaml), ten sam na każdym serwerze — router nie musi wiedzieć, gdzie stoi:
mongo:
uri: mongodb://orbito-router:<hasło>@10.8.0.1:27020,10.8.0.2:27020,10.8.0.3:27020/orbitov2?replicaSet=rs0&authSource=admin
http:
port: 8080 # REST, WS i /health routera
# stripPrefix: /api # domyślnie; '' gdy nginx sam zdejmuje /api
rpc:
plain:
port: 7801 # moduły z lokalnego Dockera
upstreamListener: upstream
jwtPublicKeyFile: /app/keys/jwt-public.pem
Zalecany układ: nginx przekazuje /api i /ws do routera bez przepisywania ścieżki, a prefiks /api zdejmuje router. Konfiguracja jest sprawdzana przy starcie — błąd zatrzymuje proces z nazwą pola. W k3s konfiguracja routera jest sekretem (zawiera hasło do bazy).
Odpowiedzi routera, które warto znać przy diagnozie:
| Odpowiedź | Znaczenie |
|---|---|
503 COMPANY_UNASSIGNED + Retry-After: 2 | firma nie ma w tej chwili żywego właściciela (trwa przejęcie); panel ponawia odczyt |
503 NO_CORE_MEMBER | pula domyślna nie ma żywego, niedrenowanego członka |
502 CORE_MEMBER_UNREACHABLE | członek nie odpowiada |
GET /health → 200 / 503 | router czyta bazę i pula domyślna ma żywego członka — na tym opiera się monitor Cloudflare |
Cloudflare
- Rekord panelu proxowany (pomarańczowa chmurka), load balancer z originem każdego serwera aplikacyjnego, równe wagi, bez session affinity — router i tak kieruje po firmie.
- Monitor: HTTPS
GET /healthz nagłówkiemHostpanelu, oczekiwany kod 200. - SSL Full (strict); ten sam certyfikat originu na każdym serwerze (Cloudflare Origin CA albo Let's Encrypt z wyzwaniem DNS-01 — HTTP-01 za load balancerem trafia na losowy origin).
- WebSockets włączone. Proxy Cloudflare zrywa połączenie po ~100 s ciszy; panel łączy się wtedy ponownie.
- Prawdziwy adres klienta:
CF-Connecting-IP(real_ip_headerw nginx).
WireGuard i Mongo
- Mesh ze statycznymi kluczami (np.
10.8.0.1–3, port51820/udp). Publicznie zostają tylko nginx (443) i — opcjonalnie — port TLS routera. Kontenery modułów lokalnego Dockera nie mogą sięgać do adresów tunelu (firewall wDOCKER-USER, bo porty Dockera omijająINPUT). - Replica set: dwa węzły z danymi (priorytety 2 i 1) i arbiter na trzecim serwerze; connection string zawiera wszystkie trzy adresy, więc klient łączy się, nawet gdy jeden serwer leży. Wszyscy klienci bazy (rdzeń, router, repozytorium modułów) dostają nowe URI z użytkownikiem.
Praca z pulą
Wdrożenie nowej wersji rdzenia bez przestoju — po kolei dla każdego członka:
docker compose -f app2.yml pull core-app2-a
# panel „Pule” → drain członka app2-a; poczekaj, aż liczba jego firm spadnie do 0
docker compose -f app2.yml up -d core-app2-a # nowy proces; pierwszy heartbeat zdejmuje drain
Drain ma sens, gdy pula ma innego żywego członka. Zatrzymanie kontenera (SIGTERM) też oddaje firmy od razu, bez czekania na wygaśnięcie dzierżaw.
Przeniesienie firmy do innej puli — panel „Firmy” albo node scripts/set-company-pool.js <companyId> <pula> --apply. Rdzeń odrzuci pulę, która nie istnieje, nie ma żywego członka albo nie obsługuje środowiska modułów firmy. Przepięcie wykonują członkowie w ciągu kilku sekund.
Diagnostyka — node scripts/pools-doctor.js (tylko odczyt z bazy, działa także, gdy żaden rdzeń nie odpowiada): pule, członkowie żywi / martwi / drenowani z ich firmami i firmy bez właściciela (kod wyjścia 2). GET /health członka zwraca 503 przy braku bazy albo w trakcie drainu.
Wycofanie — rdzeń bez CORE_POOL działa jak pojedynczy proces obsługujący wszystkie firmy. Zatrzymaj członków i router, uruchom jeden rdzeń bez CORE_POOL i skieruj nginx z powrotem na jego porty. Kontenery lokalnego Dockera zostają, o ile moduły mają ten sam adres RPC co przed zmianą.
Ograniczenia
- Czas przejęcia po awarii członka to ~10–15 s (wygaśnięcie dzierżawy 6 s, przejęcie, ponowne połączenie modułów do 5 s). W tym czasie żądania do firmy dostają
503 COMPANY_UNASSIGNED. - Zdarzenia automatyzacji w locie giną przy przejęciu firmy (jak przy restarcie rdzenia); wstrzymane biegi przeżywają.
- Firmy na lokalnym Dockerze nie przetrwają awarii swojego serwera.
- Zegar — rozjechany czas serwera fałszuje ocenę, który członek żyje; NTP jest obowiązkowy.
- Pamięć — każdy członek to ~150–300 MB, router ~50–80 MB.
- Pierwszy start rdzenia z adopcją kontenerów (wersja z pulami) odtwarza kontenery modułów raz — wybierz okno niskiego ruchu. Kolejne restarty i przejęcia firm modułów nie restartują.
