A BEAM binárisok megfejtése: Hogyan tettük DNS-könyvünket villámgyorsává (NIF-ek nélkül)
Amikor olyan platformot üzemeltetsz, amely domain regisztrációt, DNS-t hostingot és felhőinfrastruktúrát kezel, mint amilyet a NameOceannál is, a teljesítmény nem csak egy „szép-egy-egy” extra jellem, hanem a megbízhatóság alapja. Ezért örülünk, amikor okos optimalizálási történeteket hallunk az Erlang ökoszisztémából, és ezért szeretnénk bemutatni egy kedvencünket.
Nemrég egy lenyűgöző, mélyreható elemzést találtunk a DNSimple csapatától a dns_erlang, az ő nyílt forráskódú DNS-könyvtük optimalizálásáról. Az eredményeik megdöbbentőek voltak: 12%-kal kevesebb CPU-használat lekérdezésenként és 14%-kal több lekérdezés feldolgozása másodpercenként – ez mind a tiszta BEAM-optimalizációból ered, NIF-ek nélkül. Bárki, aki Elixirben vagy Erlangban épít nagy átmeneti kapacitású rendszereket, ennek elengedhetetlen olvasmányát kell elolvasnia.
Bontsuk ki, mi teszi ezt lehetségessé.
A BEAM bináris mentális modellje
Íme valami, ami meglepít sok Elixir fejlesztőt: a binárisokhoz való hozzáfűzés egy szoros ciklusban nem olyan költséges, mint gondolnád.
Amikor valami ilyesmit írsz:
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Azt hihetnéd, hogy ez $O(n^2)$ – minden iteráció egy új binárist hoz létre és másolja át az egészet. De a BEAM okosabb, mint ez.
Mivel a binárisok immutábilak (változatlanok), a fordító képes bizonyítani, hogy ha az acc éppen egy korábbi hozzáfűzés során jött létre, és nincs más referenciája, akkor biztonságos közvetlenül a meglévő memóriába írni. Az első hozzáfűzés némi tartalék kapacitást allokál, a subsequent (utsequent) hozzáfűzések pedig ebbe írnak. Az eredmény? Amortizált $O(1)$ minden hozzáfűzésre – pontosan úgy működik, mint a std::vector a C++-ban.
A csapda? Ha a bináris „elszabadul” (escapes) – vagyis egy folyamat-szótartóba (process dictionary) kerül, visszaérkezik a hívónak, vagy hozzáadódik valami máshoz – a buli véget ér. Visszatérsz a másolgatáshoz.
Ez a mintázat mindenhol megjelenik a bináris protokollok kezelésénél. Amikor iteratívan építed a DNS válaszokat, a komulátor (accumulator) folyamatának szoros tartása hatalmas fontosságú.
Szub-binárisok: Ingyenes szeletelés (mondanám)
A binárisok tesztelése (matching) létrehoz egy olyan struktúrát, amit a BEAM szub-binárisnak nevez – apró kifejezések, amelyek a gyári adatokra mutatnak másolás nélkül. Amikor ezt írod:
<<Header:12/binary, Rest/binary>> = Packet
Nincs adatmásolás. A Rest egyszerűen csak egy ablak az eredeti csomagra.
De itt van a finomság, ami elcsábítja az embereket: a BEAM belsőleg megőriz egy tesztelési kontextust, amikor rekurzív tesztelést végzel, ami lényegében egy byte-okon végigszaladó kurzor. Nem hoz létre valódi szub-binárist, amíg te nem hagyod elszabadulni – például vissza nem adod, el nem tárolod, vagy nem mész vele valahová, ahol megmaradhat.
Ez rendkívül fontos a DNS parsoolásánál. Minden lekérdezés binárisként érkezik, össze van hasonlítva a protokoll struktúrájával, és választ kell generálnia. Megérteni, hogy mikor tereted a munkát, és mikor csak nézelődsz az adatokon, az a különbség egy skálázható parsor és egy olyan parsor között, amely a terhelés alatt elborul.
Miért fontos az egészszám-ábrázolás jobban, mint gondolnád
Egy 64 bites BEAM-en az egészszámok néhány bitet igényelnek a típus-tagjukhoz. Ez így 60 bitet hagy szabadon az értéknek. Azok az egészszámok, amelyek beleférnek ebbe a térbe, „kis egészszámok” (small integers) – azon értékek, amelyek közvetlenül a folyamat regiszterszövegeiben élnek, soha nem érintik a heap-et.
Minden, ami nagyobb, „bignum”-vá válik, ami heap allokációt és gyűjtőrendszer (garbage collection) kezelést igényel. Ez kuslyán hangzik, de kritikusvá válik, amikor a bináris protokolloknál szükséges bit-manipulációkat végzed.
Ezt a technikát SWAR-nak (SIMD Within A Register) nevezik – több értéket egyetlen egészszámba csomagolnak, hogy az CPU szintjén párhuzamosan tudjanak velük műveletet végezni. A DNS esetén ez azt jelenti, hogy minden műveletnél rengeteg byte értékű adatot parsolunk. De csak akkor működik, ha a csomagolt értékeid beleférnek a „kis egészszámok” optimális tartományába.
Ha nem, nemcsak a memória-allokációs költséget fizeted – hanem potenciálisan olyan gyűjtőciklusokat indítasz el, amelyek egyébként nem léteznének. Millió lekérdezésenként ezek a ciklusok összeadódnak.
Mit jelent ez a kódodnak
Íme a gyakorlati tanács: nem kell NIF-eket írnod vagy C-hez nyúlnod a komoly teljesítménybeli javulásért. Értendő, hogyan gondolkodik a BEAM az adatokról.
Néhány mintázat, amit érdemes elsajátítani:
1. Tartsd szorosra a bináris akkumulátorokat. A csak hozzáfűvő (append-only) ciklusok a barátai. Amint a bináris elszabadul, a költsékmodell drasztikusan megváltozik.
2. Használj helyi tesztelést (match in place), ahol lehetséges. Használd a pattern matching-et az adatok megtekintéséhez, a másolás helyett. A BEAM erre optimalizálva van.
3. Figyelj az egészszámok méretére. Arithmətikai vagy bit-manipulációs műveleteknél gondold át, hogy az értékeid a „kis egészszámok” tartományában maradnak-e.
4. Profilozz a redukciók figyelembevételével. A BEAM a redukciók (körülbelül a függvényhívások) alapján ütemez. Egy „forró” ciklus, amely kevesebb függvényhívásban végez több tesztelést, kevesebb munkát jelent a scheduler szempontjából.
Bárki, aki olyan rendszereket épít, amelyek bináris protokollokat kezelnek – legyen az DNS, WebSocket frame-ek vagy egyedi protokoll-implementációk –, ezek a minták közvetlenül alkalmazhatók. A BEAM hihetetlen eszközöket ad a gyors kód írásához, de csak akkor hasznosak, ha megérted a modellt.
Láttuk már csapatokat, akik NIF-ekhez vagy Rust NIF-ekhez nyúltak, amikor a BEAM szemantikai megértése 90%-os javulást hozott volna anélkül, hogy a komplexitást is meg kellett volna teremteniük. A runtime okosabb, mint ahogy gyakran hisszük – a mi feladatunk olyan kódot írni, amely lehetővé teszi, hogy megmutassa a tudását.
A NameOceannél mindig a nagy skálájú teljesítményre gondolunk. Legyen szó egy következő projektedhez indított Vibe Hostingról, vagy egy nagy forgalmú alkalmazás DNS-ének kezeléséről, ezek az alapok megértése segít olyan rendszereket építeni, amelyek gyorsak maradnak, amikor a teendők megáramlanak.
Sok szerencsét a kódoláshoz, és legyen a binárisaid mindig hatékonyan hozzáfűthetők!