Uproszczone równoważenie obciążenia NGINX z Loadcat
Opublikowany: 2022-03-11Aplikacje internetowe zaprojektowane do skalowania w poziomie często wymagają co najmniej jednego węzła równoważenia obciążenia. Ich głównym celem jest sprawiedliwe rozłożenie ruchu przychodzącego na dostępne serwery internetowe. Możliwość zwiększenia ogólnej wydajności aplikacji internetowej po prostu przez zwiększenie liczby węzłów i dostosowanie systemów równoważenia obciążenia do tej zmiany może okazać się niezwykle przydatna w produkcji.
NGINX to serwer sieciowy, który oferuje funkcje równoważenia obciążenia o wysokiej wydajności, wśród wielu innych jego możliwości. Niektóre z tych funkcji są dostępne tylko jako część ich modelu subskrypcji, ale wersja bezpłatna i open source jest nadal bardzo bogata w funkcje i zawiera najważniejsze funkcje równoważenia obciążenia po wyjęciu z pudełka.
W tym samouczku omówimy wewnętrzną mechanikę eksperymentalnego narzędzia, które pozwala skonfigurować instancję NGINX w locie, aby działała jako system równoważenia obciążenia, odrzucając wszystkie najdrobniejsze szczegóły plików konfiguracyjnych NGINX, zapewniając zgrabną sieć oparty na interfejsie użytkownika. Celem tego artykułu jest pokazanie, jak łatwo rozpocząć budowę takiego narzędzia. Warto wspomnieć, że projekt Loadcat jest mocno inspirowany NodeBalancers firmy Linode.
NGINX, serwery i upstream
Jednym z najpopularniejszych zastosowań NGINX jest odwrotne przekazywanie żądań od klientów do aplikacji serwera WWW. Chociaż aplikacje internetowe opracowane w językach programowania, takich jak Node.js i Go, mogą być samowystarczalnymi serwerami internetowymi, posiadanie zwrotnego serwera proxy przed rzeczywistą aplikacją serwera zapewnia wiele korzyści. Blok „serwer” dla prostego przypadku użycia takiego jak ten w pliku konfiguracyjnym NGINX może wyglądać mniej więcej tak:
server { listen 80; server_name example.com; location / { proxy_pass http://192.168.0.51:5000; } }
To spowodowałoby, że NGINX nasłuchiwałby na porcie 80 dla wszystkich żądań, które są kierowane do example.com i przekazywały każde z nich do jakiejś aplikacji serwera WWW działającej w 192.168.0.51:5000. Moglibyśmy również użyć tutaj adresu IP sprzężenia zwrotnego 127.0.0.1, jeśli serwer aplikacji sieci Web działał lokalnie. Zwróć uwagę, że w powyższym fragmencie brakuje pewnych oczywistych poprawek, które są często używane w konfiguracji odwrotnego proxy, ale jest zachowany w ten sposób dla zwięzłości.
Ale co, gdybyśmy chcieli zrównoważyć wszystkie przychodzące żądania między dwiema instancjami tego samego serwera aplikacji internetowych? Tutaj przydaje się dyrektywa „upstream”. W NGINX, dzięki dyrektywie „upstream”, możliwe jest zdefiniowanie wielu węzłów zaplecza, wśród których NGINX będzie równoważyć wszystkie przychodzące żądania. Na przykład:
upstream nodes { server 192.168.0.51:5000; server 192.168.0.52:5000; } server { listen 80; server_name example.com; location / { proxy_pass http://nodes; } }
Zwróć uwagę, jak zdefiniowaliśmy blok „upstream” o nazwie „węzły”, składający się z dwóch serwerów. Każdy serwer jest identyfikowany przez adres IP i numer portu, na którym nasłuchuje. Dzięki temu NGINX staje się systemem równoważenia obciążenia w najprostszej formie. Domyślnie NGINX będzie dystrybuować przychodzące żądania w sposób okrężny, w którym pierwsze z nich zostanie przekazane do pierwszego serwera, drugie do drugiego serwera, trzecie do pierwszego serwera i tak dalej.
Jednak NGINX ma znacznie więcej do zaoferowania, jeśli chodzi o równoważenie obciążenia. Pozwala zdefiniować wagi dla każdego serwera, oznaczyć je jako tymczasowo niedostępne, wybrać inny algorytm równoważenia (np. jest taki, który działa w oparciu o hash IP klienta) itp. Te funkcje i dyrektywy konfiguracyjne są dobrze udokumentowane na nginx.org . Co więcej, NGINX umożliwia zmianę i ponowne ładowanie plików konfiguracyjnych w locie, prawie bez przerw.
Konfigurowalność NGINX i proste pliki konfiguracyjne sprawiają, że bardzo łatwo jest dostosować go do wielu potrzeb. W Internecie istnieje już mnóstwo samouczków, które dokładnie uczą, jak skonfigurować NGINX jako system równoważenia obciążenia.
Loadcat: Narzędzie konfiguracyjne NGINX
Jest coś fascynującego w programach, które zamiast robić coś samodzielnie, konfigurują inne narzędzia, aby zrobiły to za nich. Tak naprawdę nie robią nic poza tym, że mogą pobierać dane wejściowe użytkownika i generować kilka plików. Większość korzyści, jakie czerpiesz z tych narzędzi, to w rzeczywistości cechy innych narzędzi. Ale z pewnością ułatwiają życie. Próbując skonfigurować load balancer dla jednego z moich własnych projektów, zastanawiałem się: dlaczego nie zrobić czegoś podobnego dla NGINX i jego możliwości równoważenia obciążenia?
Narodził się Loadcat!
Loadcat, zbudowany z Go, jest wciąż w powijakach. W tej chwili narzędzie pozwala skonfigurować NGINX tylko do równoważenia obciążenia i zakończenia SSL. Zapewnia użytkownikowi prosty internetowy interfejs graficzny. Zamiast przechodzić przez poszczególne funkcje narzędzia, rzućmy okiem na to, co kryje się pod spodem. Należy jednak pamiętać, że jeśli ktoś lubi ręczną pracę z plikami konfiguracyjnymi NGINX, takie narzędzie może mieć niewielką wartość.
Jest kilka powodów, dla których warto wybrać Go jako język programowania. Jednym z nich jest to, że Go tworzy skompilowane pliki binarne. To pozwala nam budować i dystrybuować lub wdrażać Loadcat jako skompilowany plik binarny na zdalnych serwerach bez martwienia się o rozwiązywanie zależności. Coś, co znacznie upraszcza proces konfiguracji. Oczywiście plik binarny zakłada, że NGINX jest już zainstalowany i istnieje dla niego plik jednostki systemd.
Jeśli nie jesteś inżynierem Go, nie martw się wcale. Go jest dość łatwe i przyjemne na początek. Co więcej, sama implementacja jest bardzo prosta i powinieneś być w stanie łatwo ją śledzić.
Struktura
Narzędzia do budowania Go nakładają kilka ograniczeń na to, jak możesz ustrukturyzować swoją aplikację, a resztę pozostawić programistom. W naszym przypadku podzieliliśmy rzeczy na kilka pakietów Go na podstawie ich celów:
- cfg: ładuje, analizuje i dostarcza wartości konfiguracyjne
- cmd/loadcat: pakiet główny, zawiera punkt wejścia, kompiluje do postaci binarnej
- dane: zawiera „modele”, używa wbudowanego magazynu klucza/wartości w celu utrwalenia
- feline: zawiera podstawową funkcjonalność, np. generowanie plików konfiguracyjnych, mechanizm przeładowania, itp.
- interfejs użytkownika: zawiera szablony, programy obsługi adresów URL itp.
Jeśli przyjrzymy się bliżej strukturze pakietu, zwłaszcza w pakiecie feline, zauważymy, że cały kod specyficzny dla NGINX jest przechowywany w podpakietu feline/nginx. Dzieje się tak, abyśmy mogli zachować ogólną logikę aplikacji i rozszerzyć obsługę innych systemów równoważenia obciążenia (np. HAProxy) w przyszłości.
Punkt wejścia
Zacznijmy od głównego pakietu Loadcat, znajdującego się w „cmd/loadcatd”. Główna funkcja, punkt wejścia aplikacji, robi trzy rzeczy.
func main() { fconfig := flag.String("config", "loadcat.conf", "") flag.Parse() cfg.LoadFile(*fconfig) feline.SetBase(filepath.Join(cfg.Current.Core.Dir, "out")) data.OpenDB(filepath.Join(cfg.Current.Core.Dir, "loadcat.db")) defer data.DB.Close() data.InitDB() http.Handle("/api", api.Router) http.Handle("/", ui.Router) go http.ListenAndServe(cfg.Current.Core.Address, nil) // Wait for an “interrupt“ signal (Ctrl+C in most terminals) }
Aby zachować prostotę i ułatwić odczytanie kodu, cały kod obsługi błędów został usunięty z powyższego fragmentu (a także z fragmentów w dalszej części tego artykułu).
Jak widać z kodu, ładujemy plik konfiguracyjny na podstawie flagi wiersza poleceń „-config” (która domyślnie to „loadcat.conf” w bieżącym katalogu). Następnie inicjujemy kilka komponentów, a mianowicie pakiet core feline i bazę danych. Wreszcie uruchamiamy serwer WWW dla GUI opartego na sieci.
Konfiguracja
Ładowanie i parsowanie pliku konfiguracyjnego jest prawdopodobnie najłatwiejszą częścią tutaj. Używamy TOML do kodowania informacji konfiguracyjnych. Dla Go dostępny jest zgrabny pakiet analizujący TOML. Potrzebujemy bardzo mało informacji konfiguracyjnych od użytkownika iw większości przypadków możemy określić rozsądne wartości domyślne dla tych wartości. Poniższa struktura reprezentuje strukturę pliku konfiguracyjnego:
struct { Core struct { Address string Dir string Driver string } Nginx struct { Mode string Systemd struct { Service string } } }
A oto jak może wyglądać typowy plik „loadcat.conf”:
[core] address=":26590" dir="/var/lib/loadcat" driver="nginx" [nginx] mode="systemd" [nginx.systemd] service="nginx.service"
Jak widać, istnieje podobieństwo między strukturą pliku konfiguracyjnego zakodowanego w formacie TOML a strukturą pokazaną powyżej. Pakiet konfiguracyjny zaczyna się od ustawienia pewnych rozsądnych wartości domyślnych dla pewnych pól struktury , a następnie analizuje plik konfiguracyjny nad nim. Jeśli nie znajdzie pliku konfiguracyjnego w określonej ścieżce, tworzy go i najpierw zrzuca do niego wartości domyślne.
func LoadFile(name string) error { f, _ := os.Open(name) if os.IsNotExist(err) { f, _ = os.Create(name) toml.NewEncoder(f).Encode(Current) f.Close() return nil } toml.NewDecoder(f).Decode(&Current) return nil }
Dane i trwałość
Poznaj Bolta. Wbudowany magazyn klucz/wartość napisany w czystym Go. Jest dostarczany jako pakiet z bardzo prostym API, obsługuje transakcje od razu i jest niepokojąco szybki.
W ramach danych pakietu mamy struktury reprezentujące każdy typ encji. Na przykład mamy:
type Balancer struct { Id bson.ObjectId Label string Settings BalancerSettings } type Server struct { Id bson.ObjectId BalancerId bson.ObjectId Label string Settings ServerSettings }
… gdzie wystąpienie Balancer reprezentuje pojedynczy system równoważenia obciążenia. Loadcat skutecznie umożliwia równoważenie żądań dla wielu aplikacji internetowych za pośrednictwem jednej instancji NGINX. Każdy balancer może wtedy mieć za sobą jeden lub więcej serwerów, przy czym każdy serwer może być oddzielnym węzłem zaplecza.
Ponieważ Bolt jest magazynem klucz-wartość i nie obsługuje zaawansowanych zapytań do bazy danych, mamy logikę po stronie aplikacji, która robi to za nas. Loadcat nie jest przeznaczony do konfigurowania tysięcy równoważników z tysiącami serwerów w każdym z nich, więc naturalnie to naiwne podejście działa dobrze. Ponadto Bolt działa z kluczami i wartościami, które są wycinkami bajtów, i dlatego kodujemy struktury BSON przed zapisaniem ich w Bolt. Poniżej przedstawiono implementację funkcji pobierającej listę struktur Balancer z bazy danych:
func ListBalancers() ([]Balancer, error) { bals := []Balancer{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { bal := Balancer{} bson.Unmarshal(v, &bal) bals = append(bals, bal) } return nil }) return bals, nil }
Funkcja ListBalancers uruchamia transakcję tylko do odczytu, iteruje po wszystkich kluczach i wartościach w wiadrze „balancers”, dekoduje każdą wartość do instancji struktury Balancer i zwraca je w tablicy.

Przechowywanie wyważarki w wiadrze jest prawie równie proste:
func (l *Balancer) Put() error { if !l.Id.Valid() { l.Id = bson.NewObjectId() } if l.Label == "" { l.Label = "Unlabelled" } if l.Settings.Protocol == "https" { // Parse certificate details } else { // Clear fields relevant to HTTPS only, such as SSL options and certificate details } return DB.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("balancers")) p, err := bson.Marshal(l) if err != nil { return err } return b.Put([]byte(l.Id.Hex()), p) }) }
Funkcja Put przypisuje pewne wartości domyślne do określonych pól, analizuje dołączony certyfikat SSL w konfiguracji HTTPS, rozpoczyna transakcję, koduje instancję struktury i przechowuje ją w zasobniku zgodnie z identyfikatorem balancera.
Podczas analizowania certyfikatu SSL dwie informacje są wyodrębniane przy użyciu standardowego kodowania/pem pakietu i przechowywane w SSLOptions w polu Ustawienia : nazwy DNS i odcisk palca.
Mamy też funkcję wyszukującą serwery przez balancer:
func ListServersByBalancer(bal *Balancer) ([]Server, error) { srvs := []Server{} DB.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("servers")) c := b.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { srv := Server{} bson.Unmarshal(v, &srv) if srv.BalancerId.Hex() != bal.Id.Hex() { continue } srvs = append(srvs, srv) } return nil }) return srvs, nil }
Ta funkcja pokazuje, jak naiwne jest nasze podejście. Tutaj efektywnie czytamy cały zasobnik „serwery” i odfiltrowujemy nieistotne jednostki przed zwróceniem tablicy. Ale z drugiej strony, to działa dobrze i nie ma prawdziwego powodu, aby to zmieniać.
Funkcja Put dla serwerów jest znacznie prostsza niż funkcja Balancer , ponieważ nie wymaga tylu wierszy kodu ustawień domyślnych i pól obliczanych.
Kontrolowanie NGINX
Przed użyciem Loadcat musimy skonfigurować NGINX, aby załadować wygenerowane pliki konfiguracyjne. Loadcat generuje plik „nginx.conf” dla każdego balancera w katalogu według identyfikatora balancera (krótki ciąg szesnastkowy). Te katalogi są tworzone w katalogu „out” w cwd
. Dlatego ważne jest, aby skonfigurować NGINX do ładowania tych wygenerowanych plików konfiguracyjnych. Można to zrobić za pomocą dyrektywy „include” wewnątrz bloku „http”:
Edytuj /etc/nginx/nginx.conf i dodaj następujący wiersz na końcu bloku „http”:
http { include /path/to/out/*/nginx.conf; }
Spowoduje to, że NGINX przeskanuje wszystkie katalogi znalezione w „/path/to/out/”, wyszuka pliki o nazwie „nginx.conf” w każdym katalogu i załaduje każdy znaleziony.
W naszym podstawowym pakiecie feline definiujemy interfejs Driver . Każda struktura , która udostępnia dwie funkcje, Generate i Reload , z poprawną sygnaturą kwalifikuje się jako sterownik.
type Driver interface { Generate(string, *data.Balancer) error Reload() error }
Na przykład struct Nginx pod pakietami feline/nginx:
type Nginx struct { sync.Mutex Systemd *dbus.Conn } func (n Nginx) Generate(dir string, bal *data.Balancer) error { // Acquire a lock on n.Mutex, and release before return f, _ := os.Create(filepath.Join(dir, "nginx.conf")) TplNginxConf.Execute(f, /* template parameters */) f.Close() if bal.Settings.Protocol == "https" { // Dump private key and certificate to the output directory (so that Nginx can find them) } return nil } func (n Nginx) Reload() error { // Acquire a lock on n.Mutex, and release before return switch cfg.Current.Nginx.Mode { case "systemd": if n.Systemd == nil { c, err := dbus.NewSystemdConnection() n.Systemd = c } ch := make(chan string) n.Systemd.ReloadUnit(cfg.Current.Nginx.Systemd.Service, "replace", ch) <-ch return nil default: return errors.New("unknown Nginx mode") } }
Generate można wywołać za pomocą ciągu zawierającego ścieżkę do katalogu wyjściowego i wskaźnik do wystąpienia struktury Balancer . Go udostępnia standardowy pakiet szablonów tekstowych, którego sterownik NGINX używa do generowania końcowego pliku konfiguracyjnego NGINX. Szablon składa się z bloku „w górę”, po którym następuje blok „serwer”, wygenerowany na podstawie konfiguracji wyważarki:
var TplNginxConf = template.Must(template.New("").Parse(` upstream {{.Balancer.Id.Hex}} { {{if eq .Balancer.Settings.Algorithm "least-connections"}} least_conn; {{else if eq .Balancer.Settings.Algorithm "source-ip"}} ip_hash; {{end}} {{range $srv := .Balancer.Servers}} server {{$srv.Settings.Address}} weight={{$srv.Settings.Weight}} {{if eq $srv.Settings.Availability "available"}}{{else if eq $srv.Settings.Availability "backup"}}backup{{else if eq $srv.Settings.Availability "unavailable"}}down{{end}}; {{end}} } server { {{if eq .Balancer.Settings.Protocol "http"}} listen {{.Balancer.Settings.Port}}; {{else if eq .Balancer.Settings.Protocol "https"}} listen {{.Balancer.Settings.Port}} ssl; {{end}} server_name {{.Balancer.Settings.Hostname}}; {{if eq .Balancer.Settings.Protocol "https"}} ssl on; ssl_certificate {{.Dir}}/server.crt; ssl_certificate_key {{.Dir}}/server.key; {{end}} location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://{{.Balancer.Id.Hex}}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; } } `))
Reload to druga funkcja w strukturze Nginx , która powoduje ponowne załadowanie plików konfiguracyjnych przez NGINX. Zastosowany mechanizm opiera się na konfiguracji Loadcat. Domyślnie zakłada, że NGINX jest usługą systemd działającą jako nginx.service, tak że [sudo] systemd reload nginx.service
będzie działać. Jednak zamiast wykonywać polecenie powłoki, nawiązuje połączenie z systemd przez D-Bus przy użyciu pakietu github.com/coreos/go-systemd/dbus.
GUI oparte na sieci Web
Mając wszystkie te komponenty na swoim miejscu, opakujemy to wszystko prostym interfejsem użytkownika Bootstrap.
W przypadku tych podstawowych funkcji wystarczy kilka prostych programów obsługi tras GET i POST:
GET /balancers GET /balancers/new POST /balancers/new GET /balancers/{id} GET /balancers/{id}/edit POST /balancers/{id}/edit GET /balancers/{id}/servers/new POST /balancers/{id}/servers/new GET /servers/{id} GET /servers/{id}/edit POST /servers/{id}/edit
Przeglądanie każdej indywidualnej trasy może nie być najciekawszą rzeczą do zrobienia tutaj, ponieważ są to w zasadzie strony CRUD. Zachęcamy do rzucenia okiem na kod interfejsu użytkownika pakietu, aby zobaczyć, jak zaimplementowano procedury obsługi dla każdej z tych tras.
Każda funkcja obsługi to procedura, która:
- Pobiera dane z magazynu danych i odpowiada renderowanymi szablonami (przy użyciu pobranych danych)
- Analizuje przychodzące dane formularzy, wprowadza niezbędne zmiany w magazynie danych i wykorzystuje pakiet feline do ponownego wygenerowania plików konfiguracyjnych NGINX
Na przykład:
func ServeServerNewForm(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) TplServerNewForm.Execute(w, struct { Balancer *data.Balancer }{ Balancer: bal, }) } func HandleServerCreate(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"])) r.ParseForm() body := struct { Label string `schema:"label"` Settings struct { Address string `schema:"address"` } `schema:"settings"` }{} schema.NewDecoder().Decode(&body, r.PostForm) srv := data.Server{} srv.BalancerId = bal.Id srv.Label = body.Label srv.Settings.Address = body.Settings.Address srv.Put() feline.Commit(bal) http.Redirect(w, r, "/servers/"+srv.Id.Hex()+"/edit", http.StatusSeeOther) }
Wszystko, co robi funkcja ServeServerNewForm , to pobieranie balancera z datastore i renderowanie szablonu, w tym przypadku TplServerList , który pobiera listę odpowiednich serwerów za pomocą funkcji Servers w balancerze.
Z drugiej strony funkcja HandleServerCreate analizuje przychodzący ładunek POST z treści do struktury i używa tych danych do utworzenia wystąpienia i utrwalenia nowej struktury serwera w magazynie danych przed użyciem pakietu kociego do ponownego wygenerowania pliku konfiguracyjnego NGINX dla modułu równoważącego.
Wszystkie szablony stron są przechowywane w pliku „ui/templates.go”, a odpowiadające im pliki szablonów HTML można znaleźć w katalogu „ui/templates”.
Wypróbowanie
Wdrażanie Loadcat na zdalnym serwerze lub nawet w środowisku lokalnym jest bardzo łatwe. Jeśli używasz Linuksa (64-bitowego), możesz pobrać archiwum z gotowymi plikami binarnymi Loadcat z sekcji Wydania repozytorium. Jeśli masz ochotę na przygodę, możesz sklonować repozytorium i samodzielnie skompilować kod. Chociaż doświadczenie w tym przypadku może być nieco rozczarowujące , ponieważ kompilacja programów Go nie jest tak naprawdę wyzwaniem. A jeśli używasz Arch Linux, masz szczęście! Dla wygody zbudowano pakiet dla dystrybucji. Po prostu pobierz i zainstaluj za pomocą swojego menedżera pakietów. Wymagane kroki są opisane bardziej szczegółowo w pliku README.md projektu.
Po skonfigurowaniu i uruchomieniu Loadcat, skieruj przeglądarkę internetową na „http://localhost:26590” (zakładając, że działa lokalnie i nasłuchuje na porcie 26590). Następnie utwórz balanser, utwórz kilka serwerów, upewnij się, że coś nasłuchuje na tych zdefiniowanych portach i voila, powinieneś mieć przychodzące żądania równoważenia obciążenia NGINX między tymi uruchomionymi serwerami.
Co dalej?
To narzędzie jest dalekie od doskonałości, a w rzeczywistości jest to dość eksperymentalny projekt. Narzędzie nie obejmuje nawet wszystkich podstawowych funkcji NGINX. Na przykład, jeśli chcesz buforować zasoby obsługiwane przez węzły zaplecza w warstwie NGINX, nadal będziesz musiał ręcznie modyfikować pliki konfiguracyjne NGINX. I to sprawia, że wszystko jest ekscytujące. Jest wiele do zrobienia i dokładnie to, co będzie dalej: objęcie jeszcze większej liczby funkcji równoważenia obciążenia NGINX - tych podstawowych i prawdopodobnie nawet tych, które ma do zaoferowania NGINX Plus.
Wypróbuj Loadcat. Sprawdź kod, rozwidlej go, zmień go, baw się nim. Daj nam również znać, jeśli zbudowałeś narzędzie, które konfiguruje inne oprogramowanie lub korzystałeś z takiego, które naprawdę lubisz w sekcji komentarzy poniżej.