Kā viena smaga tērzēšana varēja nogāzt mūsu API
- septembrī, apmēram 17 minūtes pēc izlaiduma, abi mūsu ražošanas API serveri vienlaikus beidzās atmiņa, restartējās, un apmēram piecas minūtes slodzes balansētājam nebija neviena veselīga servera, uz kuru sūtīt trafiku. Mūsu pirmais labojums deva API divreiz vairāk atmiņas. Īstais cēlonis parādījās vēlāk tajā pašā dienā: vienas konkrētas tērzēšanas atvēršana lika API vienā pieprasījumā ielādēt 53 MB ziņojumu. Tērzēšanas vēsture tagad tiek ielādēta lapās, kas ierobežotas pēc izmēra, ne tikai skaita, un šī tērzēšana atveras ar 5 MB pirmo lapu, nevis avārē.
Kas notika
Mūsu API darbojas kā konteiners katrā no diviem serveriem aiz slodzes balansētāja. Katram konteineram bija atmiņas ierobežojums 384 MiB. Miera stāvoklī tas izmanto 130–140 MiB, tāpēc ierobežojums šķita pietiekami liels.
Abi konteineri sasniedza šo ierobežojumu kopā un avarēja ar „JavaScript heap out of memory”. Heap ir atmiņa, ko Node.js process izmanto objektiem, ar kuriem strādā. Tā kā abi serveri neizdevās vienlaikus, nebija kur maršrutēt pieprasījumus. Tas ir sliktākais atteices veids divu serveru uzstādījumā: rezerve palīdz tikai tad, ja kļūmes nenotiek vienlaikus.
Pirmais labojums: vairāk vietas
Laiks norādīja uz izlaidumu. Pēc izlaiduma aģenta gājieni, kurus izlaidums bija pārtraucis, tiek atsākti un saglabā savus transkriptus no jauna, un šis darba uzplūdums izskatījās kā acīmredzams atmiņas pieauguma avots.
Tāpēc pirmās izmaiņas paaugstināja API ierobežojumu no 384 MiB līdz 768 MiB. Mēs to piemērojām tiešraidē abiem serveriem tajā pašā dienā un iekomitējām izvietošanas konfigurācijā, lai nākamais izlaidums klusi neatgrieztu 384. Labojums, kas izdarīts ar roku uz servera, ko nākamais izlaidums atceļ, ir izplatīts veids, kā tā pati avārija atgriežas.
Vairāk atmiņas bija saprātīga drošības rezerve. Tā nebija skaidrojums.
Īstais cēlonis: viens pieprasījums, 71 MB
Vēlāk tajā dienā mēs reproducējām avāriju uz tērzēšanas kopijas, izmantojot ražošanas veco atmiņas ierobežojumu. Kopija avarēja API pati par sevi, bez izlaiduma.
Kad atver projektu, tīmekļa lietotne pieprasa API tērzēšanas vēsturi. Galapunkts atgrieza jaunākos 30 ziņojumus pilnībā. Vairumam tērzēšanu 30 ziņojumi ir mazi. Šai tērzēšanai jaunākie 30 ziņojumi datubāzē bija 53 MB, un atbilde — 71 MB. Vairākas aģenta atbildes bija 4–10 MB katra, galvenokārt apakšaģentu transkripti: palīgaģenti, ko galvenais aģents sāk, lai paralēli strādātu pie daļas uzdevuma, un kuru pilnais darbs tiek saglabāts kopā ar atbildi.
Šīs vienas atbildes veidošanai vajadzēja vairāk heap, nekā API bija. Un katrs šī projekta atvēršanas vai atsvaidzināšanas gadījums nosūtīja pieprasījumu no jauna. Viena tērzēšana varēja nogāzt jebkuru serveri, uz kuru tā nokļuva, un, tā kā lapu varēja pārlādēt, tā varēja sasniegt abus.
„30 ziņojumi” nekad nebija īsts izmēra ierobežojums. Tas bija skaita ierobežojums, kas nejauši bija mazs parastajām tērzēšanām.
Risinājums: lapas, ierobežotas pēc izmēra un skaita
Vēstures lapa tagad ir 30 ziņojumi vai apmēram 4 MB no tiem — atkarībā no tā, kas pienāk pirmais.
Tas notiek divos soļos:
- Vispirms pieprasi izmērus. API jautā datubāzei jaunāko ziņojumu ID un saglabātos izmērus, bez to satura. Šis vaicājums ir lēts neatkarīgi no tā, cik lieli ir ziņojumi.
- Ielādē tikai to, kas ietilpst. Virzoties no jaunākā uz vecāko, tas pievieno ziņojumus, līdz nākamais pārsniegtu lapu 4 MB. Tikai tad šīs rindas tiek ielādētas pilnībā.
Divi noteikumi to padara drošu:
- Jaunākais ziņojums vienmēr ietilpst lapā neatkarīgi no tā izmēra. Lapa, kas neatgrieztu neko, atstātu tērzēšanu iestrēgušu bez iespējas ritināt atpakaļ.
- Nekas netiek apgriezts. Neviens ziņojums netiek saīsināts vai izmests. Pārējā tērzēšana tiek ielādēta caur vadību „Scroll up for earlier messages”, ko vecākie ziņojumi jau izmantoja, pa vienai lapai.
Atradām vēl vienu vietu ar tādu pašu formu. Kad aptur gājienu, API pārbauda pēdējās dažas atbildes, lai redzētu, vai kāda ir tukša un jātīra. Tas to darīja, ielādējot astoņus veselus ziņojumus. Tērzēšanā ar vairāku megabaitu atbildēm tā ir tā pati problēma mazākā formā. Tagad tas jautā datubāzei jā vai nē — vai šim ziņojumam ir kāds saturs? — bez paša satura ielādes.
Kā to pārbaudījām
Tērzēšanas kopijā, darbojoties ar veco 384 MiB ierobežojumu:
- Pirmā lapa ir 5 MB. Pirms tam tas pats pieprasījums nogāza API.
- Piecas pārlūkprogrammas cilnes, kas vienlaikus atver tērzēšanu, sasniedz 154 MB atmiņas maksimumu.
- Ritinot atpakaļ pa visu tērzēšanu, atgriežas visi 190 ziņojumi, katrs tieši vienreiz, pareizā secībā.
Lapu izvēles loģikai ir arī vienību testi, tostarp tests, kas veidots no šīs tērzēšanas jaunāko ziņojumu reālajiem izmēriem. Pirmajā lapā atgriežas viens ziņojums, tas norāda, ka ir vairāk, un nākamā lapa sākas ar nākamo.
Ko no tā guvām
- Skaits nav izmēra ierobežojums. Jebkuram galapunktam, kas atgriež „jaunākos N” no kaut kā, kas var augt bez ierobežojuma, vajag arī baitu budžetu. Dati agrāk vai vēlāk atrod šo plaisu.
- Acīmredzamais aizdomu objekts ne vienmēr ir cēlonis. Izlaidums bija reāls papildu darba izraisītājs, un vairāk atmiņas bija vērts. Bet avārija reproducējās bez izlaiduma, un bez reproducēšanas mēs būtu apstājušies pie atmiņas palielināšanas.
- Divi serveri kopā neizdodas, ja saņem to pašu pieprasījumu. Rezerve pasargā pret viena servera krišanu. Tā nepasargā no pieprasījuma, kas nogalina jebkuru serveri, kuram tas nonāk — īpaši tādu, ko lietotājs dabiski mēģinās vēlreiz, atsvaidzinot lapu.
- Labo konfigurācijā, ne tikai uz mašīnas. Atmiņas palielinājums tajā pašā dienā nonāca izvietošanas konfigurācijā, tāpēc nākamais izlaidums to saglabāja.
Šis ir otrais nesenais atmiņas stāsts, kas saistīts ar aģenta izvadi. Pirmais bija statusa uzraksts, kas ieturēja 690 MB virkņu. Ja Mythex izmanto apakšaģentus, par to, kā tie darbojas, vari lasīt dokumentācijā.