Serviciile de backend HubSpot sunt scrise aproape toate în Java. Avem peste 1.000 de microservicii în mod constant construite și implementate. Când vine timpul să implementăm și să rulăm una dintre aplicațiile noastre Java, dependențele sale trebuie să fie prezente pe calea clasei pentru ca aceasta să funcționeze. Anterior, am gestionat acest lucru folosind plugin-ul maven-shade pentru a construi un JAR gras. Aceasta ia aplicația și toate dependențele sale și le grupează într-un JAR masiv. Acest JAR este imuabil și nu are dependențe externe, ceea ce îl face ușor de implementat și de rulat. De ani de zile, astfel am împachetat toate aplicațiile noastre Java și a funcționat destul de bine, dar a avut unele dezavantaje grave.

noastre

Primul număr pe care l-am lovit este că JAR-urile nu trebuie să fie agregate astfel. Pot exista fișiere cu aceeași cale prezentă în mai multe JAR-uri și în mod implicit pluginul de umbră include primul fișier din JAR-ul gras și aruncă restul. Acest lucru a provocat unele erori cu adevărat frustrante până când ne-am dat seama ce se întâmplă (de exemplu, Jersey folosește fișiere META-INF/servicii pentru a descoperi automat furnizorii și acest lucru a făcut ca unii furnizori să nu fie înregistrați). Din fericire, pluginul pentru umbră acceptă transformatoare de resurse care vă permit să definiți o strategie de îmbinare atunci când întâlnește fișiere duplicate, astfel încât am putut rezolva această problemă. Cu toate acestea, este încă un „gotcha” suplimentar de care trebuie să fie conștienți toți dezvoltatorii noștri.

Cealaltă problemă, mai mare, cu care ne-am confruntat este că acest proces este lent și ineficient. Folosind una dintre aplicațiile noastre ca exemplu, conține 70 de fișiere de clasă în valoare totală de 210 KB atunci când sunt ambalate ca JAR. Dar după ce rulăm pluginul de umbră pentru a-și grupa dependențele, ajungem cu un JAR gras care conține 101.481 de fișiere și cântărește la 158 MB. Combinarea a 100.000 de fișiere mici într-o singură arhivă este lentă. Încărcarea acestui JAR pe S3 la sfârșitul construcției este lentă. Descărcarea acestui JAR la momentul implementării este lentă (și poate satura plăcile de rețea de pe serverele noastre de aplicații dacă avem o mulțime de implementări simultane).

Cu peste 100 de ingineri care se angajează constant, avem de obicei până la 1.000-2.000 de versiuni pe zi. Cu fiecare dintre aceste versiuni încărcând un JAR gras, am generat 50-100 GB de artefacte de construcție pe zi . Și cea mai dureroasă parte este cât de multe duplicări există între fiecare dintre aceste artefacte. Aplicațiile noastre au o mulțime de suprapuneri în ceea ce privește bibliotecile terță parte, de exemplu, toate folosesc Guice, Jackson, Guava, Logback etc. Imaginați-vă câte exemplare ale acestor biblioteci avem în S3!

Găsirea unui mod mai bun

În cele din urmă, am decis că trebuie să găsim o modalitate mai bună de a face acest lucru. Una dintre alternative este utilizarea maven-dependency-plugin pentru a copia toate dependențele aplicației în directorul de construire. Apoi, când tarăm folderul de construire și îl încărcăm în S3, acesta va include toate dependențele, așa că avem în continuare versiunile imuabile pe care le dorim. Acest lucru ne economisește timpul de rulare a pluginului de umbră și complexitatea pe care o adaugă. Cu toate acestea, nu reduce dimensiunea artefactelor de construcție, deci durează încă ceva timp să încărcați tarball-ul la sfârșitul construcției, ceea ce înseamnă, de asemenea, că pierdem încă o cantitate uriașă de spațiu stocând aceste artefacte de construcție și apoi este încă nevoie de mult timp pentru a descărca aceste artefacte la implementare.

Vă prezentăm SlimFast

Folosind exemplul de aplicație dinainte, ce se întâmplă dacă tocmai am încărcat JAR-ul de 210 KB? Imaginați-vă cât de rapidă ar fi construirea (se pare că este cu până la 60% mai rapidă). Imaginați-vă cât spațiu am economisi în S3 (peste 99%). Imaginați-vă cât timp și I/O am economisi la implementări. Pentru a face acest lucru, am scris propriul nostru plugin Maven numit SlimFast. Se leagă implicit de faza de implementare și încarcă toate dependențele aplicației în S3 individual. Pe hârtie, acest lucru face construirea mai lentă, dar trucul este că trebuie să facă acest lucru doar dacă dependența nu există deja în S3. Și întrucât dependențele aplicațiilor noastre nu se schimbă foarte des, acest pas este de obicei un no-op. Pluginul generează un fișier JSON cu informații despre toate artefactele de dependență din S3, astfel încât să le putem descărca ulterior. La momentul implementării, descărcăm toate dependențele aplicației, dar stocăm aceste artefacte în cache pe fiecare dintre serverele noastre de aplicații, astfel încât acest pas este, de obicei, și o operațiune interzisă. Rezultatul net este că, la momentul construirii, încărcăm JAR-ul subțire al aplicației, care este de doar câteva sute de kiloocteți. La momentul implementării, trebuie doar să descărcăm același JAR subțire, care durează o fracțiune de secundă.

Rezultatele

După ce am lansat acest lucru, am trecut de la producerea a 50-100 GB de artefacte de construcție pe zi la mai puțin de 1 GB. În plus, neexecutarea pluginului de umbră și încărcarea JAR-urilor grase pe S3 au avut beneficii imense în ceea ce privește viteza de construire. Iată un grafic care arată timpii de construcție înainte și după schimbarea pentru unele dintre proiectele noastre:

Executăm această configurație în producție de peste 4 luni și a funcționat excelent. Consultați readm-ul SlimFast pentru informații mai detaliate despre cum să îl configurați și informați-ne cum funcționează pentru dvs.!