Echilibrare simplificată a sarcinii NGINX cu Loadcat
Publicat: 2022-03-11Aplicațiile web care sunt proiectate pentru a fi scalabile orizontal necesită adesea unul sau mai multe noduri de echilibrare a sarcinii. Scopul lor principal este de a distribui traficul de intrare pe serverele web disponibile într-un mod corect. Capacitatea de a crește capacitatea generală a unei aplicații web prin simpla creștere a numărului de noduri și prin adaptarea echilibratorilor de încărcare la această schimbare se poate dovedi a fi extrem de utilă în producție.
NGINX este un server web care oferă funcții de echilibrare a sarcinii de înaltă performanță, printre multe dintre celelalte capabilități ale sale. Unele dintre aceste funcții sunt disponibile doar ca parte a modelului lor de abonament, dar versiunea gratuită și open source este încă foarte bogată în funcții și vine cu cele mai esențiale funcții de echilibrare a încărcăturii din cutie.
În acest tutorial, vom explora mecanica interioară a unui instrument experimental care vă permite să configurați instanta dvs. NGINX din mers pentru a acționa ca un echilibrator de încărcare, abstragând toate detaliile esențiale ale fișierelor de configurare NGINX, oferind un web- interfață de utilizator bazată. Scopul acestui articol este de a arăta cât de ușor este să începeți să construiți un astfel de instrument. Merită menționat că proiectul Loadcat este puternic inspirat de NodeBalancers de la Linode.
NGINX, Servere și Upstream
Una dintre cele mai populare utilizări ale NGINX este reverse-proxying-ul solicitărilor de la clienți către aplicațiile de server web. Deși aplicațiile web dezvoltate în limbaje de programare precum Node.js și Go pot fi servere web autonome, a avea un proxy invers în fața aplicației de server reală oferă numeroase beneficii. Un bloc „server” pentru un caz de utilizare simplu ca acesta într-un fișier de configurare NGINX poate arăta cam așa:
server { listen 80; server_name example.com; location / { proxy_pass http://192.168.0.51:5000; } }
Acest lucru ar face ca NGINX să asculte pe portul 80 pentru toate cererile care sunt direcționate către example.com și să le transmită pe fiecare aplicație de server web care rulează la 192.168.0.51:5000. De asemenea, am putea folosi aici adresa IP loopback 127.0.0.1 dacă serverul de aplicații web rula local. Vă rugăm să rețineți că fragmentul de mai sus nu are unele modificări evidente care sunt adesea folosite în configurația reverse-proxy, dar este păstrat în acest fel pentru concizie.
Dar dacă am dori să echilibrăm toate solicitările primite între două instanțe ale aceluiași server de aplicații web? Aici devine utilă directiva „în amonte”. În NGINX, cu directiva „upstream”, este posibil să se definească mai multe noduri back-end printre care NGINX va echilibra toate cererile primite. De exemplu:
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; } }
Observați cum am definit un bloc „în amonte”, denumit „noduri”, format din două servere. Fiecare server este identificat printr-o adresă IP și numărul portului pe care îl ascultă. Cu aceasta, NGINX devine un echilibrator de încărcare în cea mai simplă formă. În mod implicit, NGINX va distribui cererile primite într-un mod round-robin, unde prima va fi redirecționată către primul server, a doua către al doilea server, a treia către primul server și așa mai departe.
Cu toate acestea, NGINX are mult mai multe de oferit atunci când vine vorba de echilibrarea încărcăturii. Vă permite să definiți greutăți pentru fiecare server, să le marcați ca temporar indisponibile, să alegeți un alt algoritm de echilibrare (de exemplu, există unul care funcționează pe baza hash-ului IP al clientului), etc. Aceste caracteristici și directive de configurare sunt toate bine documentate pe nginx.org . În plus, NGINX permite ca fișierele de configurare să fie schimbate și reîncărcate din mers, fără nicio întrerupere.
Configurabilitatea lui NGINX și fișierele de configurare simple fac cu adevărat ușor de adaptat la multe nevoi. Și există deja o mulțime de tutoriale pe Internet care vă învață exact cum să configurați NGINX ca echilibrator de încărcare.
Loadcat: Instrumentul de configurare NGINX
Există ceva fascinant la programele care, în loc să facă ceva pe cont propriu, configurează alte instrumente pentru a face acest lucru pentru ele. Ei nu fac cu adevărat mare lucru decât poate să ia intrările utilizatorilor și să genereze câteva fișiere. Cele mai multe dintre beneficiile pe care le culegeți din aceste instrumente sunt de fapt caracteristici ale altor instrumente. Dar, cu siguranță, ele ușurează viața. În timp ce încercam să configurez un echilibrator de încărcare pentru unul dintre propriile mele proiecte, m-am întrebat: de ce să nu fac ceva similar pentru NGINX și capabilitățile sale de echilibrare a sarcinii?
Loadcat s-a născut!
Loadcat, construit cu Go, este încă la început. În acest moment, instrumentul vă permite să configurați NGINX numai pentru echilibrarea sarcinii și terminarea SSL. Oferă utilizatorului o interfață grafică simplă bazată pe web. În loc să trecem prin caracteristicile individuale ale instrumentului, să aruncăm o privire la ceea ce se află dedesubt. Fiți conștienți, totuși, dacă cineva îi place să lucreze manual cu fișierele de configurare NGINX, ar putea găsi puțină valoare într-un astfel de instrument.
Există câteva motive pentru a alege Go ca limbaj de programare pentru aceasta. Una dintre ele este că Go produce binare compilate. Acest lucru ne permite să construim și să distribuim sau să implementăm Loadcat ca un binar compilat pe servere la distanță, fără a ne face griji cu privire la rezolvarea dependențelor. Ceva care simplifică foarte mult procesul de configurare. Desigur, binarul presupune că NGINX este deja instalat și că există un fișier de unitate systemd pentru el.
În cazul în care nu sunteți inginer Go, nu vă faceți griji deloc. Go este destul de ușor și distractiv pentru a începe. În plus, implementarea în sine este foarte simplă și ar trebui să puteți urmări cu ușurință.
Structura
Instrumentele Go build impun câteva restricții asupra modului în care vă puteți structura aplicația și lăsați restul în seama dezvoltatorului. În cazul nostru, am împărțit lucrurile în câteva pachete Go în funcție de scopurile lor:
- cfg: încarcă, analizează și furnizează valori de configurare
- cmd/loadcat: pachetul principal, conține punctul de intrare, se compilează în binar
- date: conține „modele”, utilizează un depozit de chei/valoare încorporat pentru persistență
- feline: conține funcționalități de bază, de exemplu, generarea fișierelor de configurare, mecanismul de reîncărcare etc.
- ui: conține șabloane, handlere URL etc.
Dacă ne uităm mai atent la structura pachetului, în special în cadrul pachetului feline, vom observa că tot codul specific NGINX a fost păstrat într-un subpachet feline/nginx. Acest lucru se face astfel încât să putem păstra restul logicii aplicației generice și să extindem suportul pentru alți echilibratori de încărcare (de exemplu, HAProxy) în viitor.
Punct de intrare
Să începem de la pachetul principal pentru Loadcat, găsit în „cmd/loadcatd”. Funcția principală, punctul de intrare al aplicației, face trei lucruri.
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) }
Pentru a menține lucrurile simple și a face codul mai ușor de citit, tot codul de gestionare a erorilor a fost eliminat din fragmentul de mai sus (și, de asemenea, din fragmentele de mai jos în acest articol).
După cum puteți vedea din cod, încărcăm fișierul de configurare pe baza indicatorului de linie de comandă „-config” (care este implicit „loadcat.conf” în directorul curent). În continuare, inițializam câteva componente, și anume pachetul de bază feline și baza de date. În cele din urmă, începem un server web pentru interfața grafică bazată pe web.
Configurare
Încărcarea și analizarea fișierului de configurare este probabil cea mai ușoară parte aici. Folosim TOML pentru a codifica informațiile de configurare. Există un pachet îngrijit de analiză TOML disponibil pentru Go. Avem nevoie de foarte puține informații de configurare de la utilizator și, în majoritatea cazurilor, putem determina valori implicite corecte pentru aceste valori. Următoarea structură reprezintă structura fișierului de configurare:
struct { Core struct { Address string Dir string Driver string } Nginx struct { Mode string Systemd struct { Service string } } }
Și iată cum poate arăta un fișier tipic „loadcat.conf”:
[core] address=":26590" dir="/var/lib/loadcat" driver="nginx" [nginx] mode="systemd" [nginx.systemd] service="nginx.service"
După cum putem vedea, există o similitudine între structura fișierului de configurare codificat TOML și structura afișată deasupra acestuia. Pachetul de configurare începe prin setarea unor valori implicite corecte pentru anumite câmpuri ale structurii și apoi analizează fișierul de configurare peste el. În cazul în care nu reușește să găsească un fișier de configurare la calea specificată, creează unul și mai întâi aruncă valorile implicite în el.
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 }
Date și persistență
Faceți cunoștință cu Bolt. Un magazin de chei/valoare încorporat scris în pur Go. Vine ca un pachet cu un API foarte simplu, acceptă tranzacții de la cutie și este deranjant de rapid.
În cadrul datelor pachetului, avem structuri care reprezintă fiecare tip de entitate. De exemplu, avem:
type Balancer struct { Id bson.ObjectId Label string Settings BalancerSettings } type Server struct { Id bson.ObjectId BalancerId bson.ObjectId Label string Settings ServerSettings }
… unde o instanță de Balancer reprezintă un singur echilibrator de încărcare. Loadcat vă permite în mod eficient să echilibrați cererile pentru mai multe aplicații web printr-o singură instanță a NGINX. Fiecare echilibrator poate avea apoi unul sau mai multe servere în spate, unde fiecare server poate fi un nod back-end separat.
Deoarece Bolt este un magazin cheie-valoare și nu acceptă interogări avansate de baze de date, avem o logică la nivelul aplicației care face acest lucru pentru noi. Loadcat nu este conceput pentru a configura mii de echilibrare cu mii de servere în fiecare dintre ele, așa că, desigur, această abordare naivă funcționează foarte bine. De asemenea, Bolt funcționează cu chei și valori care sunt felii de octeți și de aceea codificăm structurile BSON înainte de a le stoca în Bolt. Implementarea unei funcții care preia o listă de structuri Balancer din baza de date este prezentată mai jos:
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 }
Funcția ListBalancers începe o tranzacție numai în citire, iterează peste toate cheile și valorile din compartimentul „echilibratoare”, decodifică fiecare valoare într-o instanță a structurii Balancer și le returnează într-o matrice.

Depozitarea unui echilibrator în găleată este aproape la fel de simplă:
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) }) }
Funcția Put atribuie anumite valori implicite anumitor câmpuri, analizează certificatul SSL atașat în configurarea HTTPS, începe o tranzacție, codifică instanța structurii și o stochează în găleată împotriva ID-ului echilibrantului.
În timpul analizării certificatului SSL, două informații sunt extrase folosind codificarea standard a pachetului/pem și stocate în SSLOptions în câmpul Setări : numele DNS și amprenta digitală.
Avem, de asemenea, o funcție care caută servere după echilibrator:
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 }
Această funcție arată cât de naivă este abordarea noastră cu adevărat. Aici, citim efectiv întregul compartiment „servere” și filtram entitățile irelevante înainte de a returna matricea. Dar, din nou, acest lucru funcționează foarte bine și nu există niciun motiv real pentru a-l schimba.
Funcția Put pentru servere este mult mai simplă decât cea a structurii Balancer , deoarece nu necesită atât de multe linii de cod pentru setarea implicită și câmpuri calculate.
Controlul NGINX
Înainte de a folosi Loadcat, trebuie să configuram NGINX pentru a încărca fișierele de configurare generate. Loadcat generează fișierul „nginx.conf” pentru fiecare echilibrator sub un director după ID-ul echilibrantului (un șir scurt hexadecimal). Aceste directoare sunt create sub un director „out” la cwd
. Prin urmare, este important să configurați NGINX pentru a încărca aceste fișiere de configurare generate. Acest lucru se poate face folosind o directivă „include” în blocul „http”:
Editați /etc/nginx/nginx.conf și adăugați următoarea linie la sfârșitul blocului „http”:
http { include /path/to/out/*/nginx.conf; }
Acest lucru va determina NGINX să scaneze toate directoarele găsite sub „/path/to/out/”, să caute fișierele numite „nginx.conf” în fiecare director și să încarce fiecare pe care le găsește.
În pachetul nostru de bază, feline, definim un driver de interfață. Orice structură care oferă două funcții, Generare și Reîncărcare , cu semnătura corectă se califică drept driver.
type Driver interface { Generate(string, *data.Balancer) error Reload() error }
De exemplu, structura Nginx din pachetele 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") } }
Generare poate fi invocat cu un șir care conține calea către directorul de ieșire și un pointer către o instanță de struct Balancer . Go oferă un pachet standard pentru modelarea textului, pe care driverul NGINX îl utilizează pentru a genera fișierul final de configurare NGINX. Șablonul constă dintr-un bloc „în amonte” urmat de un bloc „server”, generat pe baza modului în care este configurat echilibrul:
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'; } } `))
Reîncărcare este cealaltă funcție din structura Nginx care face ca NGINX să reîncarce fișierele de configurare. Mecanismul utilizat se bazează pe modul în care este configurat Loadcat. În mod implicit, se presupune că NGINX este un serviciu systemd care rulează ca nginx.service, astfel încât [sudo] systemd reload nginx.service
ar funcționa. Cu toate acestea, în loc să execute o comandă shell, stabilește o conexiune la systemd prin D-Bus folosind pachetul github.com/coreos/go-systemd/dbus.
GUI bazat pe web
Cu toate aceste componente la locul lor, vom încheia totul cu o interfață simplă de utilizator Bootstrap.
Pentru aceste funcționalități de bază, sunt suficiente câțiva soluții de gestionare a rutei 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
Parcurgerea fiecărui traseu individual poate să nu fie cel mai interesant lucru de făcut aici, deoarece acestea sunt aproape paginile CRUD. Simțiți-vă absolut liber să aruncați o privire la codul ui al pachetului pentru a vedea cum au fost implementați handlere pentru fiecare dintre aceste rute.
Fiecare funcție de gestionare este o rutină care fie:
- Preia datele din depozitul de date și răspunde cu șabloane randate (folosind datele preluate)
- Analizează datele formularelor primite, face modificările necesare în depozitul de date și folosește pachetul feline pentru a regenera fișierele de configurare NGINX
De exemplu:
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) }
Tot ceea ce face funcția ServeServerNewForm este să preia un echilibrator din depozitul de date și să redă un șablon, TplServerList în acest caz, care preia lista de servere relevante folosind funcția Servere de pe echilibrator.
Funcția HandleServerCreate , pe de altă parte, analizează încărcătura POST primită din corp într-o structură și utilizează acele date pentru a instanția și a persista o nouă structură Server în depozitul de date înainte de a utiliza pachetul feline pentru a regenera fișierul de configurare NGINX pentru echilibrator.
Toate șabloanele de pagină sunt stocate în fișierul „ui/templates.go” și fișierele HTML de șablon corespunzătoare pot fi găsite în directorul „ui/templates”.
Încercând
Implementarea Loadcat pe un server la distanță sau chiar în mediul dvs. local este foarte ușoară. Dacă rulați Linux (64 de biți), puteți prelua o arhivă cu un binar Loadcat pre-construit din secțiunea Lansări a depozitului. Dacă te simți puțin aventuros, poți clona depozitul și compila singur codul. Deși, experiența în acest caz poate fi puțin dezamăgitoare , deoarece compilarea programelor Go nu este cu adevărat o provocare. Și în cazul în care rulați Arch Linux, atunci aveți noroc! A fost construit un pachet pentru distribuire pentru comoditate. Pur și simplu descărcați-l și instalați-l folosind managerul de pachete. Pașii implicați sunt subliniați în mai multe detalii în fișierul README.md al proiectului.
După ce ați configurat și rulat Loadcat, îndreptați browserul web către „http://localhost:26590” (presupunând că rulează local și ascultă pe portul 26590). Apoi, creați un echilibrator, creați câteva servere, asigurați-vă că ascultă ceva pe acele porturi definite și voilà, ar trebui să aveți cereri de echilibrare a încărcăturii NGINX între acele servere care rulează.
Ce urmeaza?
Acest instrument este departe de a fi perfect și, de fapt, este un proiect destul de experimental. Instrumentul nici măcar nu acoperă toate funcționalitățile de bază ale NGINX. De exemplu, dacă doriți să stocați în cache activele servite de nodurile back-end la nivelul NGINX, va trebui totuși să modificați manual fișierele de configurare NGINX. Și asta face lucrurile interesante. Există multe care se pot face aici și exact asta urmează: acoperă și mai multe dintre funcțiile de echilibrare a încărcăturii NGINX - cele de bază și, probabil, chiar și pe cele pe care NGINX Plus le are de oferit.
Încercați Loadcat. Verificați codul, bifurcați-l, schimbați-l, jucați-vă cu el. De asemenea, spuneți-ne dacă ați construit un instrument care configurează alt software sau ați folosit unul care vă place foarte mult în secțiunea de comentarii de mai jos.