Accelerarea criptării discurilor Linux

Criptarea datelor în repaus este o necesitate pentru orice companie modernă de internet. Cu toate acestea, multe companii nu-și criptează discurile, deoarece se tem de potențiala penalizare a performanței cauzată de suprasolicitarea de criptare.

Criptarea datelor în repaus este vitală pentru Cloudflare, cu peste 200 de centre de date în întreaga lume. În această postare, vom investiga performanța de criptare a discurilor pe Linux și vom explica cum am făcut-o de cel puțin două ori mai rapidă pentru noi și pentru clienții noștri!

Criptarea datelor în repaus

Când vine vorba de criptarea datelor în repaus, există mai multe moduri în care aceasta poate fi implementată pe un sistem de operare (OS) modern. Tehnicile disponibile sunt strâns cuplate cu o stivă de stocare tipică a sistemului de operare. O versiune simplificată a stivei de stocare și a soluțiilor de criptare poate fi găsită în diagrama de mai jos:

În partea superioară a stivei se află aplicațiile, care citesc și scriu date în fișiere (sau fluxuri). Sistemul de fișiere din nucleul sistemului de operare ține evidența blocurilor din dispozitivul de blocuri subiacente care aparțin căror fișiere și traduce aceste citiri și scrieri de fișiere în citiri și scrieri de blocuri, însă specificul hardware al dispozitivului de stocare subiacent este abstractizat de sistemul de fișiere. În cele din urmă, subsistemul de blocuri transmite efectiv citirile și scrierile de blocuri către hardware-ul subiacent, folosind driverele de dispozitiv corespunzătoare.

Conceptul de stivă de stocare este de fapt similar cu bine-cunoscutul model OSI de rețea, în care fiecare strat are o viziune de nivel mai înalt a informațiilor, iar detaliile de implementare ale straturilor inferioare sunt abstractizate de straturile superioare. Și, similar modelului OSI, se poate aplica criptarea la diferite straturi (gândiți-vă la TLS vs. IPsec sau la un VPN).

Pentru datele în repaus putem aplica criptarea fie la nivelul straturilor de blocuri (fie în hardware, fie în software), fie la nivelul fișierelor (fie direct în aplicații, fie în sistemul de fișiere).

Criptare de blocuri vs. fișiere

În general, cu cât mai sus în stivă aplicăm criptarea, cu atât mai multă flexibilitate avem. Cu criptarea la nivel de aplicație, responsabilii cu întreținerea aplicației pot aplica orice cod de criptare pe care îl doresc la orice date particulare de care au nevoie. Dezavantajul acestei abordări este că, de fapt, trebuie să-l implementeze ei înșiși, iar criptarea, în general, nu este foarte prietenoasă cu dezvoltatorii: trebuie să cunoști detaliile unui anumit algoritm criptografic, să generezi corect chei, nonces, IV-uri etc. În plus, criptarea la nivel de aplicație nu valorifică memoria cache la nivel de sistem de operare și, în special, memoria cache de pagină Linux: de fiecare dată când aplicația trebuie să utilizeze datele, trebuie fie să le decripteze din nou, irosind cicluri de procesor, fie să implementeze propria „memorie cache” decriptată, ceea ce introduce mai multă complexitate în cod.

Criptarea la nivel de sistem de fișiere face ca criptarea datelor să fie transparentă pentru aplicații, deoarece sistemul de fișiere însuși criptează datele înainte de a le transmite subsistemului de blocuri, astfel încât fișierele sunt criptate indiferent dacă aplicația are sau nu suport pentru criptare. De asemenea, sistemele de fișiere pot fi configurate să cripteze doar un anumit director sau să aibă chei diferite pentru fișiere diferite. Cu toate acestea, această flexibilitate vine cu prețul unei configurații mai complexe. Criptarea sistemelor de fișiere este, de asemenea, considerată mai puțin sigură decât criptarea dispozitivelor de bloc, deoarece doar conținutul fișierelor este criptat. Fișierele au, de asemenea, metadate asociate, cum ar fi dimensiunea fișierului, numărul de fișiere, dispunerea arborelui de directoare etc., care sunt încă vizibile pentru un potențial adversar.

Criptarea în jos, la nivelul blocului (adesea denumită criptare a discului sau criptare completă a discului) face, de asemenea, ca criptarea datelor să fie transparentă pentru aplicații și chiar pentru sisteme de fișiere întregi. Spre deosebire de criptarea la nivel de sistem de fișiere, aceasta criptează toate datele de pe disc, inclusiv metadatele fișierelor și chiar spațiul liber. Cu toate acestea, este mai puțin flexibil – se poate cripta doar întregul disc cu o singură cheie, astfel încât nu există o configurație per director, per fișier sau per utilizator. Din punct de vedere criptografic, nu pot fi utilizați toți algoritmii criptografici, deoarece stratul de bloc nu mai are o imagine de ansamblu la nivel înalt a datelor, astfel încât trebuie să proceseze fiecare bloc în parte. Majoritatea algoritmilor obișnuiți necesită un fel de înlănțuire a blocurilor pentru a fi siguri, deci nu sunt aplicabili la criptarea discurilor. În schimb, au fost dezvoltate moduri speciale doar pentru acest caz specific de utilizare.

Deci, ce strat să alegeți? Ca întotdeauna, depinde… Criptarea la nivel de aplicație și de sistem de fișiere este, de obicei, alegerea preferată pentru sistemele client datorită flexibilității. De exemplu, fiecare utilizator de pe un desktop cu mai mulți utilizatori poate dori să își cripteze directorul personal cu o cheie pe care o deține și să lase unele directoare partajate necriptate. Dimpotrivă, în cazul sistemelor server, gestionate de companii SaaS/PaaS/IaaS (inclusiv Cloudflare), alegerea preferată este simplitatea configurației și securitatea – cu criptarea completă a discului activată, orice date din orice aplicație sunt criptate automat, fără excepții sau suprascrieri. Credem că toate datele trebuie să fie protejate fără a le sorta în găleți „importante” vs. „neimportante”, astfel încât flexibilitatea selectivă pe care o oferă straturile superioare nu este necesară.

Criptare hardware vs. software a discului

Când criptați datele la nivelul de blocuri, este posibil să o faceți direct în hardware-ul de stocare, dacă hardware-ul suportă acest lucru. Procedând astfel, de obicei, se obțin performanțe mai bune de citire/scriere și se consumă mai puține resurse din partea gazdei. Cu toate acestea, având în vedere că majoritatea firmware-ului hardware este proprietar, nu primește atât de multă atenție și revizuire din partea comunității de securitate. În trecut, acest lucru a dus la apariția unor defecte în unele implementări de criptare hardware a discurilor, ceea ce a făcut ca întregul model de securitate să devină inutil. Microsoft, de exemplu, a început de atunci să prefere criptarea discurilor pe bază de software.

Nu am vrut să ne expunem datele noastre și ale clienților noștri la riscul de a folosi soluții potențial nesigure și credem cu tărie în open-source. De aceea, ne bazăm doar pe criptarea software a discurilor în kernelul Linux, care este deschis și a fost auditat de mulți profesioniști în domeniul securității din întreaga lume.

Performanța criptării discurilor Linux

Noi nu urmărim doar să economisim costurile de lățime de bandă pentru clienții noștri, ci și să livrăm conținutul utilizatorilor de internet cât mai rapid posibil.

La un moment dat am observat că discurile noastre nu erau atât de rapide pe cât ne-am fi dorit să fie. Unele profilări, precum și un test rapid A/B au indicat criptarea discurilor Linux. Deoarece a nu cripta datele (chiar dacă se presupune că ar trebui să fie un cache public pe Internet) nu este o opțiune sustenabilă, am decis să aruncăm o privire mai atentă asupra performanțelor de criptare a discurilor Linux.

Device mapper și dm-crypt

Linux implementează criptarea transparentă a discurilor prin intermediul unui modul dm-crypt și dm-crypt însuși dm-crypt face parte din cadrul kernel-ului device mapper. Pe scurt, device mapper permite pre/post-procesarea cererilor IO pe măsură ce acestea călătoresc între sistemul de fișiere și dispozitivul bloc subiacent.

dm-crypt în special criptează cererile IO de „scriere” înainte de a le trimite mai jos pe stivă către dispozitivul bloc real și decriptează cererile IO de „citire” înainte de a le trimite către driverul sistemului de fișiere. Simplu și ușor! Sau nu-i așa?

Configurare de benchmarking

Pentru înregistrare, cifrele din această postare au fost obținute prin rularea comenzilor specificate pe un server Cloudflare G9 inactiv, scos din producție. Cu toate acestea, configurația ar trebui să fie ușor de reprodus pe orice laptop x86 modern.

În general, evaluarea comparativă a oricărui lucru în jurul unei stive de stocare este dificilă din cauza zgomotului introdus de hardware-ul de stocare în sine. Nu toate discurile sunt create la fel, așa că în scopul acestei postări vom folosi cele mai rapide discuri disponibile pe piață – adică niciun disc.

În schimb, Linux are o opțiune pentru a emula un disc direct în RAM. Din moment ce RAM este mult mai rapidă decât orice stocare persistentă, ar trebui să introducă puține prejudecăți în rezultatele noastre.

Comanda următoare creează un ramdisk de 4GB:

$ sudo modprobe brd rd_nr=1 rd_size=4194304$ ls /dev/ram0

Acum putem configura o instanță dm-crypt deasupra acestuia, permițând astfel criptarea pentru disc. Mai întâi, trebuie să generăm cheia de criptare a discului, să „formatăm” discul și să specificăm o parolă pentru a debloca cheia nou generată.

$ fallocate -l 2M crypthdr.img$ sudo cryptsetup luksFormat /dev/ram0 --header crypthdr.imgWARNING!========This will overwrite data on crypthdr.img irrevocably.Are you sure? (Type uppercase yes): YESEnter passphrase:Verify passphrase:

Cei care sunt familiarizați cu LUKS/dm-crypt ar fi putut observa că am folosit aici un antet detașat LUKS. În mod normal, LUKS stochează cheia de criptare a discului criptat prin parolă pe același disc ca și datele, dar, deoarece dorim să comparăm performanța de citire/scriere între dispozitivele criptate și cele necriptate, am putea suprascrie accidental cheia criptată în timpul evaluării comparative ulterioare. Păstrarea cheii criptate într-un fișier separat evită această problemă în scopul acestei postări.

Acum, putem „debloca” efectiv dispozitivul criptat pentru testele noastre:

$ sudo cryptsetup open --header crypthdr.img /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ ls /dev/mapper/encrypted-ram0/dev/mapper/encrypted-ram0

În acest moment putem compara performanța ramdisk-ului criptat față de cel necriptat: dacă citim/scriem date pe /dev/ram0, acestea vor fi stocate în text clar. În mod similar, dacă citim/scriem date pe /dev/mapper/encrypted-ram0, acestea vor fi decriptate/criptate pe parcurs de dm-crypt și stocate în text cifrat.

Este demn de remarcat faptul că nu creăm niciun sistem de fișiere deasupra dispozitivelor noastre de blocuri pentru a evita influențarea rezultatelor cu un sistem de fișiere.

Măsurarea debitului

Când vine vorba de testarea/benchmarking-ul de stocare, Flexible I/O tester este soluția obișnuită. Să simulăm o sarcină simplă de citire/scriere secvențială cu o dimensiune a blocurilor de 4K pe ramdisk fără criptare:

$ sudo fio --filename=/dev/ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=plainplain: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=21013MB, aggrb=1126.5MB/s, minb=1126.5MB/s, maxb=1126.5MB/s, mint=18655msec, maxt=18655msec WRITE: io=21023MB, aggrb=1126.1MB/s, minb=1126.1MB/s, maxb=1126.1MB/s, mint=18655msec, maxt=18655msecDisk stats (read/write): ram0: ios=0/0, merge=0/0, ticks=0/0, in_queue=0, util=0.00%

Comanda de mai sus va rula mult timp, așa că o vom opri după un timp. După cum putem vedea din statistici, suntem capabili să citim și să scriem aproximativ cu același debit în jurul valorii de 1126 MB/s. Să repetăm testul cu ramdisk-ul criptat:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1693.7MB, aggrb=150874KB/s, minb=150874KB/s, maxb=150874KB/s, mint=11491msec, maxt=11491msec WRITE: io=1696.4MB, aggrb=151170KB/s, minb=151170KB/s, maxb=151170KB/s, mint=11491msec, maxt=11491msec

Whoa, asta da scădere! Obținem acum doar ~147 MB/s, ceea ce este de peste 7 ori mai lent! Și asta pe o mașină complet inactivată!

Poate, criptografia este doar lentă

Primul lucru pe care l-am luat în considerare este să ne asigurăm că folosim cea mai rapidă criptografie. cryptsetup ne permite să evaluăm comparativ toate implementările cripto disponibile pe sistem pentru a o selecta pe cea mai bună:

$ sudo cryptsetup benchmark# Tests are approximate using memory only (no storage IO).PBKDF2-sha1 1340890 iterations per second for 256-bit keyPBKDF2-sha256 1539759 iterations per second for 256-bit keyPBKDF2-sha512 1205259 iterations per second for 256-bit keyPBKDF2-ripemd160 967321 iterations per second for 256-bit keyPBKDF2-whirlpool 720175 iterations per second for 256-bit key# Algorithm | Key | Encryption | Decryption aes-cbc 128b 969.7 MiB/s 3110.0 MiB/s serpent-cbc 128b N/A N/A twofish-cbc 128b N/A N/A aes-cbc 256b 756.1 MiB/s 2474.7 MiB/s serpent-cbc 256b N/A N/A twofish-cbc 256b N/A N/A aes-xts 256b 1823.1 MiB/s 1900.3 MiB/s serpent-xts 256b N/A N/A twofish-xts 256b N/A N/A aes-xts 512b 1724.4 MiB/s 1765.8 MiB/s serpent-xts 512b N/A N/A twofish-xts 512b N/A N/A

Se pare că aes-xts cu o cheie de criptare a datelor pe 256 de biți este cea mai rapidă aici. Dar pe care dintre ele o folosim de fapt pentru ramdisk-ul nostru criptat?

$ sudo dmsetup table /dev/mapper/encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0

Utilizăm aes-xts cu o cheie de criptare a datelor pe 256 de biți (numărați toate zerourile mascate în mod convenabil de instrumentul dmsetup – dacă doriți să vedeți octeții reali, adăugați opțiunea --showkeys la comanda de mai sus). Numerele nu se adună însă: cryptsetup benchmark ne spune mai sus să nu ne bazăm pe rezultate, deoarece „Testele sunt aproximative folosind doar memoria (fără IO de stocare)”, dar exact așa am configurat experimentul nostru folosind ramdisk-ul. Într-un caz oarecum mai rău (presupunând că citim toate datele și apoi le criptam/decriptam secvențial, fără paralelism), făcând un calcul „back-of-the-envelope”, ar trebui să obținem în jur de (1126 * 1823) / (1126 + 1823) =~696 MB/s, ceea ce este totuși destul de departe de 147 * 2 = 294 MB/s real (total pentru citiri și scrieri).

dm-crypt performance flags

În timp ce citeam pagina de manual cryptsetup am observat că are două opțiuni prefixate cu --perf-, care sunt probabil legate de reglarea performanței. Prima este --perf-same_cpu_crypt cu o descriere destul de criptică:

Perform encryption using the same cpu that IO was submitted on. The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs. This option is only relevant for open action.

Așa că activăm opțiunea

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-same_cpu_crypt /dev/ram0 encrypted-ram0

Rețineți: conform ultimei pagini de manual, există și o comandă cryptsetup refresh, care poate fi folosită pentru a activa aceste opțiuni în direct, fără a fi nevoie să „închidem” și să „redeschidem” dispozitivul criptat. Totuși, cryptsetup a noastră nu o suporta încă.

Verificarea dacă opțiunea a fost într-adevăr activată:

$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 same_cpu_crypt

Da, acum putem vedea same_cpu_crypt în ieșire, ceea ce este ceea ce doream. Să rulăm din nou benchmark-ul:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=1596.6MB, aggrb=139811KB/s, minb=139811KB/s, maxb=139811KB/s, mint=11693msec, maxt=11693msec WRITE: io=1600.9MB, aggrb=140192KB/s, minb=140192KB/s, maxb=140192KB/s, mint=11693msec, maxt=11693msec

Hmm, acum este ~136 MB/s, ceea ce este puțin mai rău decât înainte, deci nu este bine. Ce ziceți de a doua opțiune --perf-submit_from_crypt_cpus:

Disable offloading writes to a separate thread after encryption. There are some situations where offloading write bios from the encryption threads to a single thread degrades performance significantly. The default is to offload write bios to the same thread. This option is only relevant for open action.

Poate că ne aflăm în „o anumită situație” aici, așa că haideți să o încercăm:

$ sudo cryptsetup close encrypted-ram0$ sudo cryptsetup open --header crypthdr.img --perf-submit_from_crypt_cpus /dev/ram0 encrypted-ram0Enter passphrase for /dev/ram0:$ sudo dmsetup table encrypted-ram00 8388608 crypt aes-xts-plain64 0000000000000000000000000000000000000000000000000000000000000000 0 1:0 0 1 submit_from_crypt_cpus

Și acum benchmark-ul:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...Run status group 0 (all jobs): READ: io=2066.6MB, aggrb=169835KB/s, minb=169835KB/s, maxb=169835KB/s, mint=12457msec, maxt=12457msec WRITE: io=2067.7MB, aggrb=169965KB/s, minb=169965KB/s, maxb=169965KB/s, mint=12457msec, maxt=12457msec

~166 MB/s, ceea ce este un pic mai bine, dar tot nu este bine…

Întrebarea comunității

Fiind disperați am decis să căutăm sprijin pe internet și am postat descoperirile noastre pe lista de discuții dm-crypt, dar răspunsul pe care l-am primit nu a fost foarte încurajator:

Dacă cifrele vă deranjează, atunci acest lucru se datorează lipsei de înțelegere din partea dumneavoastră. Probabil că nu sunteți conștienți de faptul că criptarea este o operațiune cu greutate mare…

Am decis să facem o cercetare științifică pe această temă, tastând „is encryption expensive” (este criptarea costisitoare) în Google Search și unul dintre primele rezultate, care conține de fapt măsurători semnificative, este… propria noastră postare despre costul de criptare, dar în contextul TLS! Este o lectură fascinantă în sine, dar esența este următoarea: criptarea modernă pe hardware modern este foarte ieftină chiar și la scara Cloudflare (care face milioane de cereri HTTP criptate pe secundă). De fapt, este atât de ieftină încât Cloudflare a fost primul furnizor care a oferit SSL/TLS gratuit pentru toată lumea.

Digging into the source code

Când am încercat să folosim opțiunile personalizate dm-crypt descrise mai sus, am fost curioși de ce există acestea în primul rând și despre ce este vorba în această „descărcare”. Inițial ne așteptam ca dm-crypt să fie un simplu „proxy”, care doar criptează/decriptează datele pe măsură ce acestea trec prin stivă. Se pare că dm-crypt face mai mult decât să cripteze tampoanele de memorie și o diagramă (simplificată) a traseului de traversare IO este prezentată mai jos:

Când sistemul de fișiere emite o cerere de scriere, dm-crypt nu o procesează imediat – în schimb, o pune într-o coadă de lucru numită „kcryptd”. Pe scurt, o coadă de lucru a nucleului nu face decât să programeze o anumită activitate (criptarea în acest caz) pentru a fi efectuată la un moment ulterior, când este mai convenabil. Când vine „momentul”, dm-crypt trimite cererea către Linux Crypto API pentru criptarea efectivă. Cu toate acestea, Linux Crypto API-ul Linux modern este, de asemenea, asincron, astfel încât, în funcție de implementarea particulară pe care o va folosi sistemul dumneavoastră, cel mai probabil nu va fi procesată imediat, ci va fi pusă din nou în coadă pentru „mai târziu”. Când Linux Crypto API va efectua în cele din urmă criptarea, dm-crypt poate încerca să sorteze cererile de scriere în așteptare, punând fiecare cerere într-un arbore roșu-negru. Apoi, un fir separat al kernelului, din nou, la „ceva timp mai târziu”, ia efectiv toate cererile IO din arbore și le trimite în josul stivei.

Acum pentru cererile de citire: de data aceasta trebuie să obținem mai întâi datele criptate de la hardware, dar dm-crypt nu cere pur și simplu driverul pentru date, ci pune cererea în coadă într-o altă coadă de lucru numită „kcryptd_io”. La un moment dat, mai târziu, când avem efectiv datele criptate, le programăm pentru decriptare folosind coada de lucru „kcryptd”, cunoscută acum. „kcryptd” va trimite cererea către Linux Crypto API, care poate decripta datele și în mod asincron.

Pentru a fi corect, cererea nu traversează întotdeauna toate aceste cozi, dar partea importantă aici este că cererile de scriere pot fi puse în coadă de până la 4 ori în dm-crypt și cererile de citire de până la 3 ori. În acest moment ne întrebam dacă toate aceste cozi suplimentare pot cauza probleme de performanță. De exemplu, există o prezentare frumoasă de la Google despre relația dintre coada de așteptare și latența cozii. O concluzie cheie din prezentare este:

O cantitate semnificativă de latență de coadă se datorează efectelor de coadă

Atunci, de ce sunt toate aceste cozi acolo și putem să le eliminăm?

Arheologie Git

Nimeni nu scrie cod mai complex doar pentru distracție, în special pentru nucleul sistemului de operare. Așa că toate aceste cozi trebuie să fi fost puse acolo cu un motiv. Din fericire, sursa kernelului Linux este gestionată de git, așa că putem încerca să urmărim modificările și deciziile din jurul lor.

Coada de lucru „kcryptd” a fost în sursă încă de la începutul istoricului disponibil, cu următorul comentariu:

Nevoie pentru că ar fi foarte imprudent să se facă decriptarea într-un context de întrerupere, așa că bios-urile care se întorc de la cererile de citire sunt puse în coadă aici.

Atunci a fost doar pentru citire, dar chiar și așa – de ce ne pasă dacă este sau nu context de întrerupere, dacă Linux Crypto API va folosi probabil oricum un fir/coadă dedicat pentru criptare? Ei bine, în 2005 Crypto API nu era asincronă, așa că acest lucru era perfect logic.

În 2006 dm-crypt a început să folosească coada de lucru „kcryptd” nu numai pentru criptare, ci și pentru a trimite cereri IO:

Acest patch este conceput pentru a ajuta dm-crypt să respecte noile constrângeri impuse de următorul patch în -mm: md-dm-reduce-stack-usage-with-stacked-block-devices.patch

Se pare că scopul aici nu a fost de a adăuga mai multă simultaneitate, ci mai degrabă de a reduce utilizarea stivei nucleului, ceea ce are sens din nou, deoarece nucleul are o stivă comună pentru tot codul, deci este o resursă destul de limitată. Cu toate acestea, este demn de remarcat faptul că stiva kernelului Linux a fost extinsă în 2014 pentru platformele x86, așa că acest lucru ar putea să nu mai fie o problemă.

O primă versiune a cozii de lucru „kcryptd_io” a fost adăugată în 2007 cu intenția de a evita:

sarcina cauzată de multe cereri care așteaptă alocarea de memorie…

Procesarea cererilor se bloca pe o singură coadă de lucru aici, așa că soluția a fost să se adauge încă una. Are sens.

Nu suntem cu siguranță primii care se confruntă cu o degradare a performanțelor din cauza unei cozi de așteptare extinse: în 2011 a fost introdusă o modificare pentru a inversa condiționat o parte din coada de așteptare pentru cererile de citire:

Dacă există suficientă memorie, codul poate trimite direct bio în loc să pună la coadă această operațiune într-un fir separat.

Din păcate, la acea vreme, mesajele de confirmare ale kernelului Linux nu erau la fel de verboase ca în prezent, așa că nu există date de performanță disponibile.

În 2015 dm-crypt a început să sorteze scrierile într-un fir separat „dmcrypt_write” înainte de a le trimite în josul stivei:

Pe o mașină multiprocesor, cererile de criptare se termină într-o ordine diferită de cea în care au fost trimise. În consecință, cererile de scriere ar fi trimise într-o ordine diferită și ar putea cauza o degradare severă a performanțelor.

Aceasta are sens, deoarece accesul secvențial la disc obișnuia să fie mult mai rapid decât cel aleatoriu, iar dm-crypt întrerupea modelul. Dar acest lucru se aplică mai ales la discurile rotative, care erau încă dominante în 2015. S-ar putea să nu fie la fel de important cu SSD-urile rapide moderne (inclusiv SSD-urile NVME).

O altă parte a mesajului de confirmare merită menționată:

…în special permite programatorilor IO, cum ar fi CFQ, să sorteze mai eficient…

Menționează beneficiile de performanță pentru programatorul IO CFQ, dar programatorii Linux s-au îmbunătățit de atunci până în punctul în care programatorul CFQ a fost eliminat din kernel în 2018.

Același set de patch-uri înlocuiește lista de sortare cu un arbore roșu-negru:

În teorie, sortarea ar trebui să fie efectuată de planificatorul de disc subiacent, însă, în practică, planificatorul de disc acceptă și sortează doar un număr finit de cereri. Pentru a permite sortarea tuturor cererilor, dm-crypt trebuie să implementeze o sortare proprie.

Supraîncărcarea asociată cu sortarea bazată pe rbtree este considerată neglijabilă, astfel încât nu este utilizată condiționat.

Toate acestea au sens, dar ar fi bine să avem niște date de suport.

În mod interesant, în același set de patch-uri vedem introducerea cunoscutei noastre opțiuni „submit_from_crypt_cpus”:

Există unele situații în care descărcarea bioscrierii de la firele de criptare la un singur fir de execuție degradează semnificativ performanța

În general, putem vedea că fiecare modificare a fost rezonabilă și necesară, însă lucrurile s-au schimbat de atunci:

  • hardware-ul a devenit mai rapid și mai inteligent
  • Alocarea resurselor Linux a fost revizuită
  • Subsistemele Linux cuplate au fost rearhitecturate

Și este posibil ca multe dintre alegerile de proiectare de mai sus să nu fie aplicabile la Linux-ul modern.

„Curățarea”

Bazându-ne pe cercetările de mai sus am decis să încercăm să eliminăm toate cozile de așteptare suplimentare și comportamentul asincron și să revenim la dm-crypt la scopul său inițial: pur și simplu criptarea/decriptarea cererilor IO pe măsură ce acestea trec. Dar, de dragul stabilității și al unor analize comparative ulterioare, am ajuns să nu eliminăm codul propriu-zis, ci mai degrabă să adăugăm încă o opțiune dm-crypt, care, dacă este activată, ocolește toate cozile de așteptare/fiecare. Stegulețul ne permite să trecem de la comportamentul actual la cel nou în timpul execuției în condiții de încărcare completă a producției, astfel încât să putem reveni cu ușurință asupra modificărilor noastre în cazul în care observăm efecte secundare. Patch-ul rezultat poate fi găsit în depozitul Cloudflare GitHub Linux.

Synchronous Linux Crypto API

Din diagrama de mai sus ne amintim că nu toate cozile de așteptare sunt implementate în dm-crypt. API-ul Linux Crypto modern poate fi, de asemenea, asincron și, de dragul acestui experiment, dorim să eliminăm cozile și acolo. Totuși, ce înseamnă „poate fi”? Sistemul de operare poate conține implementări diferite ale aceluiași algoritm (de exemplu, AES-NI accelerat hardware pe platformele x86 și implementări AES generice în cod C). În mod implicit, sistemul îl alege pe cel mai „bun” pe baza priorității algoritmului configurat. dm-crypt permite înlocuirea acestui comportament și solicitarea unei anumite implementări a cifrului folosind prefixul capi:. Cu toate acestea, există o problemă. Să verificăm de fapt implementările AES-XTS (acesta este cifrul nostru de criptare a discului, vă amintiți?) disponibile pe sistemul nostru:

$ grep -A 11 'xts(aes)' /proc/cryptoname : xts(aes)driver : xts(ecb(aes-generic))module : kernelpriority : 100refcnt : 7selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : cryptd(__xts-aes-aesni)module : cryptdpriority : 451refcnt : 1selftest : passedinternal : yestype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : xts(aes)driver : xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 1selftest : passedinternal : notype : skcipherasync : yesblocksize : 16min keysize : 32max keysize : 64--name : __xts(aes)driver : __xts-aes-aesnimodule : aesni_intelpriority : 401refcnt : 7selftest : passedinternal : yestype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64

Vrem să selectăm în mod explicit un cifru sincron din lista de mai sus pentru a evita efectele de coadă în firele de execuție, dar singurele două suportate sunt xts(ecb(aes-generic)) (implementarea generică în C) și __xts-aes-aesni (implementarea accelerată de hardware x86). Cu siguranță o dorim pe cea din urmă, deoarece este mult mai rapidă (aici urmărim performanța), dar este marcată în mod suspect ca fiind internă (a se vedea internal: yes). Dacă verificăm codul sursă:

Marcați un cifru ca fiind o implementare de serviciu utilizabilă doar de un alt cifru și niciodată de un utilizator normal al API-ului de criptare a nucleului

Atunci acest cifru este menit să fie utilizat doar de alt cod de înfășurare în API-ul de criptare și nu în afara acestuia. În practică, acest lucru înseamnă că cel care apelează Crypto API trebuie să specifice în mod explicit acest indicator, atunci când solicită o anumită implementare a cifrului, dar dm-crypt nu o face, deoarece, prin proiectare, nu face parte din Linux Crypto API, ci mai degrabă dintr-un utilizator „extern”. Am corectat deja modulul dm-crypt, așa că am putea la fel de bine să adăugăm doar indicatorul relevant. Cu toate acestea, există o altă problemă cu AES-NI în special: x86 FPU. „Punctul flotant”, spuneți? De ce avem nevoie de matematică în virgulă mobilă pentru a face criptare simetrică, care ar trebui să se rezume doar la deplasări de biți și operații XOR? Nu avem nevoie de matematică, dar instrucțiunile AES-NI folosesc o parte din registrele CPU, care sunt dedicate FPU. Din păcate, kernelul Linux nu păstrează întotdeauna aceste registre în context de întrerupere din motive de performanță (salvarea/restaurarea FPU este costisitoare). Dar dm-crypt poate executa cod în context de întrerupere, așa că riscăm să corupem unele date ale altor procese și ne întoarcem la afirmația „ar fi foarte imprudent să facem decriptarea în context de întrerupere” din codul original.

Soluția noastră pentru a rezolva problema de mai sus a fost să creăm un alt modul Crypto API oarecum „inteligent”. Acest modul este sincron și nu derulează propria criptografie, ci este doar un „router” al cererilor de criptare:

  • dacă putem folosi FPU (și astfel AES-NI) în contextul de execuție curent, pur și simplu transmitem cererea de criptare către implementarea mai rapidă, „internă” __xts-aes-aesni (și o putem folosi aici, pentru că acum facem parte din Crypto API)
  • în caz contrar, transmitem cererea de criptare către implementarea mai lentă, generică, bazată pe C xts(ecb(aes-generic))

Utilizarea întregului lot

Să parcurgem procesul de utilizare împreună. Primul pas este de a lua patch-urile și de a recompila nucleul (sau doar de a compila dm-crypt și modulele noastre xtsproxy).

În continuare, haideți să repornim sarcina noastră de lucru IO într-un terminal separat, astfel încât să ne asigurăm că putem reconfigura kernelul în timpul execuției sub sarcină:

$ sudo fio --filename=/dev/mapper/encrypted-ram0 --readwrite=readwrite --bs=4k --direct=1 --loops=1000000 --name=cryptcrypt: (g=0): rw=rw, bs=4K-4K/4K-4K/4K-4K, ioengine=psync, iodepth=1fio-2.16Starting 1 process...

În terminalul principal asigurați-vă că noul nostru modul Crypto API este încărcat și disponibil:

$ sudo modprobe xtsproxy$ grep -A 11 'xtsproxy' /proc/cryptodriver : xts-aes-xtsproxymodule : xtsproxypriority : 0refcnt : 0selftest : passedinternal : notype : skcipherasync : noblocksize : 16min keysize : 32max keysize : 64ivsize : 16chunksize : 16

Reconfigurați discul criptat pentru a utiliza modulul nostru nou încărcat și activați stegulețul nostru dm-crypt corectat (trebuie să folosim instrumentul de nivel scăzut dmsetup, deoarece cryptsetup, evident, nu este conștient de modificările noastre):

$ sudo dmsetup table encrypted-ram0 --showkeys | sed 's/aes-xts-plain64/capi:xts-aes-xtsproxy-plain64/' | sed 's/$/ 1 force_inline/' | sudo dmsetup reload encrypted-ram0

Noi tocmai am „încărcat” noua configurație, dar pentru ca aceasta să aibă efect, trebuie să suspendăm/repornim dispozitivul criptat:

$ sudo dmsetup suspend encrypted-ram0 && sudo dmsetup resume encrypted-ram0

Și acum observați rezultatul. Putem să ne întoarcem la celălalt terminal care rulează jobul fio și să ne uităm la ieșire, dar pentru a face lucrurile mai frumoase, iată un instantaneu al debitului de citire/scriere observat în Grafana:


Wow, am mai mult decât dublat debitul! Cu un debit total de ~640 MB/s, suntem acum mult mai aproape de ~696 MB/s preconizat de mai sus. Cum rămâne cu latența IO? (Statistica await din instrumentul de raportare iostat):

Latența a fost și ea înjumătățită!

La producție

Până acum am folosit o configurație sintetică din care lipsesc unele părți ale stivei complete de producție, cum ar fi sistemele de fișiere, hardware-ul real și, cel mai important, volumul de lucru de producție. Pentru a ne asigura că nu optimizăm lucruri imaginare, iată un instantaneu al impactului de producție pe care aceste modificări îl aduc asupra părții de cache a stivei noastre:

Acest grafic reprezintă o comparație în trei direcții a timpilor de răspuns în cel mai rău caz (percentila 99) pentru o potrivire în cache pe unul dintre serverele noastre. Linia verde provine de la un server cu discuri necriptate, pe care îl vom folosi ca bază de referință. Linia roșie provine de la un server cu discuri criptate cu implementarea implicită de criptare a discurilor Linux, iar linia albastră provine de la un server cu discuri criptate și cu optimizările noastre activate. După cum se poate observa, implementarea implicită de criptare a discurilor Linux are un impact semnificativ asupra latenței cache-ului nostru în scenariile cele mai defavorabile, în timp ce implementarea corectată nu se poate distinge de cea care nu utilizează deloc criptarea. Cu alte cuvinte, implementarea îmbunătățită a criptării nu are niciun impact asupra vitezei de răspuns a memoriei cache, așa că, practic, o primim pe gratis! Este o victorie!

Suntem doar la început

Acest post arată cum o revizuire a arhitecturii poate dubla performanța unui sistem. De asemenea, am reconfirmat faptul că criptografia modernă nu este costisitoare și, de obicei, nu există nicio scuză pentru a nu vă proteja datele.

Am de gând să trimitem această lucrare pentru a fi inclusă în arborele sursă principal al kernelului, dar cel mai probabil nu în forma sa actuală. Deși rezultatele par încurajatoare, trebuie să ne amintim că Linux este un sistem de operare foarte portabil: rulează atât pe servere puternice, cât și pe dispozitive IoT mici cu resurse limitate și pe multe alte arhitecturi de procesoare. Versiunea actuală a patch-urilor nu face decât să optimizeze criptarea discurilor pentru o anumită sarcină de lucru pe o anumită arhitectură, dar Linux are nevoie de o soluție care să ruleze fără probleme peste tot.

Acestea fiind spuse, dacă credeți că cazul dvs. este similar și doriți să profitați acum de îmbunătățirile de performanță, puteți lua patch-urile și, sperăm, să oferiți feedback. Indicatorul de execuție facilitează comutarea din mers a funcționalității și se poate efectua un simplu test A/B pentru a vedea dacă beneficiază de un anumit caz sau configurație. Aceste patch-uri au rulat în rețeaua noastră vastă de peste 200 de centre de date pe cinci generații de hardware, astfel încât pot fi considerate în mod rezonabil ca fiind stabile. Bucurați-vă atât de performanță, cât și de securitate de la Cloudflare pentru toți!

Actualizare (11 octombrie 2020)

Patch-ul principal de pe acest blog (într-o formă ușor actualizată) a fost încorporat în kernelul Linux principal și este disponibil începând cu versiunea 5.9 și ulterior. Principala diferență este că versiunea mainline expune două stegulețe în loc de una, care oferă posibilitatea de a ocoli cozile de lucru dm-crypt pentru citiri și scrieri în mod independent. Pentru detalii, consultați documentația oficială dm-crypt.

Lasă un răspuns

Adresa ta de email nu va fi publicată.