Bioyino - agregator de metrici distribuit, scalabil

Deci colectați valori. Așa cum suntem. De asemenea, colectăm valori. Desigur, necesar pentru afaceri. Astăzi vom vorbi despre prima legătură a sistemului nostru de monitorizare - un server de agregare compatibil cu statsd bioyino, de ce am scris-o și de ce am abandonat brubeck.

Bioyino - agregator de metrici distribuit, scalabil

Din articolele noastre anterioare (1, 2) puteți afla că până cândva am adunat mărci folosind Brubeck. Este scris în C. Din punct de vedere al codului, este la fel de simplu ca o mufă (acest lucru este important atunci când vrei să contribui) și, cel mai important, gestionează volumele noastre de 2 milioane de metrici pe secundă (MPS) la vârf. fara probleme. Documentația indică suport pentru 4 milioane MPS cu un asterisc. Aceasta înseamnă că veți obține cifra menționată dacă configurați corect rețeaua pe Linux. (Nu știm câți MPS puteți obține dacă lăsați rețeaua așa cum este). În ciuda acestor avantaje, am avut câteva plângeri serioase despre brubeck.

Revendicarea 1. Github, dezvoltatorul proiectului, a încetat să-l mai susțină: publicând patch-uri și corecții, acceptându-i pe ale noastre și (nu doar pe ale noastre) PR. În ultimele luni (undeva din februarie-martie 2018), activitatea s-a reluat, dar înainte au fost aproape 2 ani de calm deplin. În plus, proiectul este în curs de dezvoltare pentru nevoile interne Gihub, care poate deveni un obstacol serios în calea introducerii de noi funcții.

Revendicarea 2. Acuratețea calculelor. Brubeck colectează un total de 65536 de valori pentru agregare. În cazul nostru, pentru unele metrici, în perioada de agregare (30 de secunde), pot ajunge mult mai multe valori (1 la vârf). Ca urmare a acestei eșantionări, valorile maxime și minime par inutile. De exemplu, așa:

Bioyino - agregator de metrici distribuit, scalabil
Așa cum a fost

Bioyino - agregator de metrici distribuit, scalabil
Cum ar fi trebuit să fie

Din același motiv, sumele sunt în general calculate incorect. Adăugați aici o eroare cu un overflow float pe 32 de biți, care, în general, trimite serverul la segfault atunci când primește o metrică aparent nevinovată și totul devine grozav. Apropo, eroarea nu a fost remediată.

Și, în cele din urmă, Revendicarea X. La momentul scrierii, suntem gata să-l prezentăm tuturor celor 14 implementări mai mult sau mai puțin funcționale de statsd pe care le-am putut găsi. Să ne imaginăm că o infrastructură unică a crescut atât de mult încât acceptarea a 4 milioane MPS nu mai este suficientă. Sau chiar dacă nu a crescut încă, dar valorile sunt deja atât de importante pentru tine încât chiar și scăderile scurte de 2-3 minute în topuri pot deveni deja critice și pot provoca crize de depresie insurmontabilă în rândul managerilor. Deoarece tratarea depresiei este o sarcină ingrată, sunt necesare soluții tehnice.

În primul rând, toleranța la greșeală, astfel încât o problemă bruscă pe server să nu provoace o apocalipsă zombie psihiatrică în birou. În al doilea rând, scalarea pentru a putea accepta mai mult de 4 milioane de MPS, fără a pătrunde adânc în stiva de rețea Linux și a crește calm „în lățime” la dimensiunea necesară.

Deoarece aveam loc de scalare, am decis să începem cu toleranța la erori. "DESPRE! Toleranta la greseli! Este simplu, o putem face”, ne-am gândit și am lansat 2 servere, ridicând câte o copie de brubeck pe fiecare. Pentru a face acest lucru, a trebuit să copiem traficul cu valori pe ambele servere și chiar să scriem pentru asta utilitate mică. Am rezolvat problema toleranței la erori cu asta, dar... nu foarte bine. La început totul părea grozav: fiecare brubeck colectează propria sa versiune de agregare, scrie date pe Graphite o dată la 30 de secunde, suprascriind vechiul interval (acest lucru se face pe partea Graphite). Dacă un server eșuează brusc, avem întotdeauna un al doilea cu propria copie a datelor agregate. Dar iată problema: dacă serverul eșuează, pe grafice apare un „fierăstrău”. Acest lucru se datorează faptului că intervalele de 30 de secunde ale lui Brubeck nu sunt sincronizate, iar în momentul unui accident unul dintre ele nu este suprascris. Când pornește al doilea server, se întâmplă același lucru. Destul de tolerabil, dar vreau mai bine! Nici problema scalabilității nu a dispărut. Toate valorile încă „zboară” către un singur server și, prin urmare, suntem limitați la aceleași 2-4 milioane MPS, în funcție de nivelul rețelei.

Dacă vă gândiți puțin la problemă și, în același timp, săpați zăpada cu o lopată, atunci vă poate veni în minte următoarea idee evidentă: aveți nevoie de un statsd care poate funcționa în modul distribuit. Adică unul care implementează sincronizarea între noduri în timp și metrici. „Desigur, probabil că o astfel de soluție există deja”, am spus noi și am mers la Google…. Și n-au găsit nimic. După ce parcurgeți documentația pentru diferite statistici (https://github.com/etsy/statsd/wiki#server-implementations din 11.12.2017 decembrie XNUMX), nu am găsit absolut nimic. Aparent, nici dezvoltatorii, nici utilizatorii acestor soluții nu au întâlnit încă atât de multe valori, altfel ar veni cu siguranță ceva.

Și apoi ne-am amintit despre statistica „jucării” - bioyino, care a fost scrisă la hackatonul Just for Fun (numele proiectului a fost generat de scenariu înainte de începerea hackatonului) și am realizat că avem nevoie urgentă de propriile noastre statistici. Pentru ce?

  • pentru că există prea puține clone statistice în lume,
  • deoarece este posibil să se ofere toleranța și scalabilitatea la erori dorite sau aproape de acestea (inclusiv sincronizarea valorilor agregate între servere și rezolvarea problemei trimiterii conflictelor),
  • deoarece este posibil să se calculeze valorile mai precis decât o face Brubeck,
  • pentru că puteți colecta singur statistici mai detaliate, pe care Brubeck practic nu ni le-a furnizat,
  • pentru că am avut șansa de a-mi programa propria aplicație de laborator la scară distribuită de hiperperformanță, care nu va repeta complet arhitectura unui alt hiperperformanță similar pentru... ei bine, asta este.

Pe ce sa scriu? Desigur, în Rust. De ce?

  • pentru că exista deja o soluție prototip,
  • pentru că autorul articolului îl cunoștea deja pe Rust la acel moment și era dornic să scrie ceva în el pentru producție, cu posibilitatea de a-l pune în sursă deschisă,
  • deoarece limbile cu GC nu sunt potrivite pentru noi din cauza naturii traficului primit (aproape în timp real) și pauzele GC sunt practic inacceptabile,
  • pentru că aveți nevoie de performanță maximă comparabilă cu C
  • pentru că Rust ne oferă concurență fără teamă și dacă am începe să-l scriem în C/C++, am fi acumulat și mai multe vulnerabilități, depășiri de buffer, condiții de cursă și alte cuvinte înfricoșătoare decât brubeck.

A existat și un argument împotriva Rust. Compania nu avea experiență în crearea de proiecte în Rust, iar acum nici nu intenționăm să o folosim în proiectul principal. Prin urmare, au existat temeri serioase că nimic nu va funcționa, dar am decis să riscăm și am încercat.

A trecut timpul...

În cele din urmă, după mai multe încercări eșuate, prima versiune funcțională a fost gata. Ce s-a întâmplat? Asta s-a intamplat.

Bioyino - agregator de metrici distribuit, scalabil

Fiecare nod primește propriul set de valori și le acumulează și nu agregează valori pentru acele tipuri în care setul lor complet este necesar pentru agregarea finală. Nodurile sunt conectate între ele printr-un fel de protocol de blocare distribuită, care vă permite să selectați dintre ele singurul (aici am plâns) care este demn de a trimite metrice către Cel Mare. Această problemă este în prezent rezolvată de Consul, dar în viitor ambițiile autorului se extind la proprii implementare Raft, unde cel mai demn va fi, desigur, nodul lider consens. Pe lângă consens, nodurile destul de des (o dată pe secundă în mod implicit) trimit vecinilor acele părți ale valorilor pre-agregate pe care au reușit să le colecteze în acea secundă. Se dovedește că scalarea și toleranța la erori sunt păstrate - fiecare nod deține în continuare un set complet de metrici, dar metricile sunt trimise deja agregate, prin TCP și codificate într-un protocol binar, astfel încât costurile de duplicare sunt reduse semnificativ în comparație cu UDP. În ciuda numărului destul de mare de valori de intrare, acumularea necesită foarte puțină memorie și chiar mai puțin CPU. Pentru merticii noștri extrem de compresibili, aceasta este doar câteva zeci de megaocteți de date. Ca un bonus suplimentar, nu primim rescrieri inutile de date în Graphite, așa cum a fost cazul cu burbeck.

Pachetele UDP cu metrici sunt dezechilibrate între nodurile de pe echipamentele de rețea printr-un simplu Round Robin. Desigur, hardware-ul rețelei nu analizează conținutul pachetelor și, prin urmare, poate trage mult mai mult de 4M de pachete pe secundă, ca să nu mai vorbim de metrici despre care nu știe deloc. Dacă ținem cont de faptul că metricile nu vin pe rând în fiecare pachet, atunci nu prevedem probleme de performanță în acest loc. Dacă un server se prăbușește, dispozitivul de rețea detectează rapid acest fapt (în 1-2 secunde) și scoate serverul blocat din rotație. Ca urmare a acestui fapt, nodurile pasive (adică, non-lider) pot fi activate și dezactivate practic fără a observa scăderi pe diagrame. Maximul pe care îl pierdem face parte din valorile care au venit în ultima secundă. O pierdere/oprire/comutație bruscă a unui lider va crea în continuare o anomalie minoră (intervalul de 30 de secunde este încă desincronizat), dar dacă există comunicare între noduri, aceste probleme pot fi minimizate, de exemplu, prin trimiterea de pachete de sincronizare. .

Câteva despre structura internă. Aplicația este, desigur, multithreaded, dar arhitectura de threading este diferită de cea folosită în brubeck. Firele din brubeck sunt aceleași - fiecare dintre ele este responsabilă atât pentru colectarea informațiilor, cât și pentru agregarea. În bioyino, lucrătorii sunt împărțiți în două grupuri: cei responsabili pentru rețea și cei responsabili pentru agregare. Această diviziune vă permite să gestionați mai flexibil aplicația în funcție de tipul de metrici: acolo unde este necesară o agregare intensivă, puteți adăuga agregatoare, unde există mult trafic de rețea, puteți adăuga numărul de fluxuri de rețea. În prezent, pe serverele noastre lucrăm în 8 fluxuri de rețea și 4 de agregare.

Partea de numărare (responsabilă pentru agregare) este destul de plictisitoare. Bufferele umplute de fluxurile de rețea sunt distribuite între fluxurile de numărare, unde sunt ulterior analizate și agregate. La cerere, se oferă valori pentru trimiterea către alte noduri. Toate acestea, inclusiv trimiterea de date între noduri și lucrul cu Consul, sunt efectuate asincron, rulând pe cadru tokio.

Mult mai multe probleme în timpul dezvoltării au fost cauzate de partea de rețea responsabilă cu primirea de metrici. Scopul principal al separării fluxurilor de rețea în entități separate a fost dorința de a reduce timpul petrecut de un flux nu pentru a citi datele din soclu. Opțiunile care foloseau UDP asincron și recvmsg obișnuit au dispărut rapid: primul consumă prea mult spațiu de utilizator CPU pentru procesarea evenimentelor, al doilea necesită prea multe schimbări de context. Prin urmare, acum este folosit recvmmsg cu tampoane mari (și tampoane, domnilor ofițeri, nu sunt nimic pentru voi!). Suportul pentru UDP obișnuit este rezervat pentru cazurile ușoare în care recvmmsg nu este necesar. În modul cu mai multe mesaje, este posibil să se realizeze principalul lucru: în marea majoritate a timpului, firul de rețea grăbește coada sistemului de operare - citește datele din socket și le transferă în buffer-ul spațiului utilizator, trecând doar ocazional la oferirea tamponului umplut către agregatoare. Coada din socket practic nu se acumulează, numărul de pachete abandonate practic nu crește.

Nota

În setările implicite, dimensiunea bufferului este setată să fie destul de mare. Dacă te hotărăști brusc să încerci singur serverul, s-ar putea să întâlnești faptul că după trimiterea unui număr mic de metrici, acestea nu vor ajunge în Graphite, rămânând în bufferul de flux de rețea. Pentru a lucra cu un număr mic de valori, trebuie să setați bufsize și task-queue-size la valori mai mici în configurație.

În sfârșit, câteva topuri pentru iubitorii de diagrame.

Statistici privind numărul de valori primite pentru fiecare server: peste 2 milioane MPS.

Bioyino - agregator de metrici distribuit, scalabil

Dezactivarea unuia dintre noduri și redistribuirea valorilor primite.

Bioyino - agregator de metrici distribuit, scalabil

Statistici privind valorile de ieșire: un singur nod trimite întotdeauna - șeful raidului.

Bioyino - agregator de metrici distribuit, scalabil

Statistici ale funcționării fiecărui nod, luând în considerare erorile din diverse module de sistem.

Bioyino - agregator de metrici distribuit, scalabil

Detalierea valorilor primite (numele valorilor sunt ascunse).

Bioyino - agregator de metrici distribuit, scalabil

Ce intenționăm să facem în continuare cu toate acestea? Desigur, scrie cod, la naiba...! Proiectul a fost planificat inițial să fie open-source și va rămâne așa pe toată durata vieții sale. Planurile noastre imediate includ trecerea la propria noastră versiune de Raft, schimbarea protocolului peer la unul mai portabil, introducerea de statistici interne suplimentare, noi tipuri de valori, remedieri de erori și alte îmbunătățiri.

Desigur, toată lumea este binevenită să ajute la dezvoltarea proiectului: creați PR, Probleme, dacă este posibil vom răspunde, vom îmbunătăți etc.

Acestea fiind spuse, asta-i tot, oameni buni, cumpărați-ne elefanții!



Sursa: www.habr.com

Adauga un comentariu