Ruka hadi kwenye maudhui kuu
OpenAI

30 Juni 2026

Uhandisi

Epidemiolojia ya core dump: kurekebisha hitilafu ya miaka 18

Kutumia uchanganuzi wa kiwango cha jamii kutatua mianguko migumu katika miundombinu yetu ya data.

Inapakia…

Miundo na mawakala wa OpenAI wanazidi kutegemea miundombinu ya data inayoweza kupanuka ili kutafuta data husika wakati wa inferensi: wakati miundo inafikiria swali lako. Baadhi ya huduma hizi zimeandikwa kwa C++, ambayo udhibiti wake wa kiwango cha chini wa mfumo hutuwezesha kuongeza utendaji na kupunguza matumizi ya kumbukumbu. Faida hizo za ufanisi ni muhimu tunapopanua, lakini ukosefu wa usalama wa kumbukumbu katika C++ humaanisha hitilafu zinaweza kusababisha programu kuanguka kwa kuandika kwenye anwani za kumbukumbu zisizo sahihi au zisizopo.

Miezi michache iliyopita tuliona mianguko kadhaa ndani ya huduma ya Rockset, sehemu maalum ya miundombinu yetu ya data ya ChatGPT ambayo ni muhimu kwa programu-jalizi nyingi za data na utafutaji kwenye mazungumzo. Katika kila muanguko, function ya kawaida ya C++ ilionekana kumaliza kisha kurudi kwenye anwani bandia, hivyo kernel ikasimamisha programu kwa sababu instruction pointer haikuelekeza tena kwenye code. Wakati mwingine nafasi ya anwani ya kurudi kwenye stack frame ilikuwa NULL. Wakati mwingine rejista ya CPU ya stack pointer yenyewe ilionekana kupotoka kwa baiti 8, kana kwamba %rsp ilikuwa imepunguzwa kwa namna fulani katikati ya utekelezaji wa kawaida. Katika hali zote mbili, muanguko ulitokea wakati wa kurudi.

Hizi si hali za kawaida za kushindwa kwa code ya programu. Uandishi uliopotea unaogonga anwani ya kurudi iliyohifadhiwa pekee unawezekana, lakini ni nadra sana. Hitilafu inayopotosha %rsp kwa 8 bila kuhusisha inline assembly, setcontext, au longjmp (ambazo hatutumii) ni ya ajabu zaidi, kwa sababu code iliyokusanywa hurekebisha rejista hiyo moja kwa moja tu kwenye prologue na epilogue ya function. Kila dhana ambayo sisi (au ChatGPT) tuliweza kufikiria ilikuwa na ushahidi mzito dhidi yake, kwa hivyo hitilafu ilionekana haiwezekani.

Tulilodhani kuwa tatizo moja hatimaye lilibainika kuwa hitilafu mbili zisizohusiana, zilizogunduliwa kwa bahati wakati mmoja. Kwanza, uharibifu wa kimya wa hardware kwenye host moja ya Azure, ambapo CPU haikufanya hesabu kwa usahihi. Pili, race condition ya miaka 18 katika GNU libunwind, hitilafu isiyotambuliwa katika maktaba huria inayotumiwa sana.

Chapisho hili ni hadithi ya jinsi tulivyotambua na kurekebisha mianguko iliyoonekana kutoelezeka kwa kufikiri kama mtaalamu wa epidemiolojia na kujenga seti ya data ya ubora kuhusu jamii nzima ya mianguko.

Jaribio la kwanza la debugging: kuchunguza kwa makini core dump chache

Kwanza, tuichunguze Rockset kwa kina zaidi. Ni mfumo wa data wa cloud-native kwa utafutaji na uchanganuzi wa wakati halisi tunaotumia kwa matumizi mengi ya ndani OpenAI, kama vile sync connectors (Rockset ilinunuliwa na OpenAI mwaka 2024). Masasisho ya mtiririko hutumiwa kudumisha faharasa iliyosasishwa ya hazina ya maarifa ya eneokazi ili ChatGPT iweze kutafuta taarifa husika inapojibu maswali au kutekeleza vitendo.

Tabaka la utekelezaji la Rockset limeandikwa kwa C++. Lugha ya C++ hutoa ufikiaji wa kiwango cha chini kwa CPU, jambo zuri kwa utendaji na ufanisi, lakini humaanisha hitilafu za programu zinaweza kusababisha ufikiaji batili wa kumbukumbu na segfaults. Ili kusaidia kuzifuatilia tunatumia fatal signal handler ya folly kurekodi stack trace muanguko unapotokea, na tunapakia core dump husika (picha ya hali ya programu ilipoanguka) kwenye Azure blob storage kwa uchanganuzi wa baadaye. Majani yote ya uchakataji wa hoja ya Rockset yanarudufiwa, jambo linalopunguza athari ya muanguko kwa mteja. Hata hivyo, kila segfault inalingana na hitilafu inayohitaji kurekebishwa ili kufikia malengo yetu ya kutegemeka na ubora.

Mbinu yetu ya awali ilikuwa kushughulikia core hizi kama tatizo la kawaida la debugging: kuchunguza core dump chache kwa kina, kuunda dhana, na kuziondoa moja baada ya nyingine.

Mianguko mingi ilitokea katika method inayoitwa DocumentTree::updateDocument. Katika mianguko hii ilionekana kuwa updateDocument ilikuwa imeita function X isiyojulikana, stack ikaharibika X ilipokuwa active, kisha X ikarudi kwenye anwani ambayo haikuwa code inayotekelezeka. Katika baadhi ya visa, frame ya X iliyokuwa imetoka tu ilionekana halali isipokuwa anwani yake ya kurudi iliyohifadhiwa ilikuwa NULL. Katika visa vingine, stack pointer yenyewe ilionekana si sahihi, lakini frame halali inayofuata bado ilionekana kuwa updateDocument.

Hatukujua stack ilikuwa ikiharibika lini, jambo lililoacha eneo kubwa sana la kutafuta. updateDocument ni method kubwa inayofanyiwa inlining nyingi, kwa hiyo idadi ya wagombea wa X ilikuwa kubwa mno.

Je, hii ilikuwa hitilafu katika code yetu ya C++? Tatizo la compiler au linkage? Tatizo katika mojawapo ya maktaba zetu za runtime? Hitilafu ya Linux kernel kuhusu uwasilishaji wa signal au context switching? Kitu adimu zaidi? Kama huu ulikuwa uandishi uliopotea, kwa nini haukukamatwa na mazingira yetu ya staging ya ASAN?

Tulijaribu kutumia logi zetu za kiwango cha programu kutambua matukio yote ya tatizo, lakini hitilafu za kuharibu stack ni ngumu kuainisha kwa logi pekee kwa sababu stack traces zilizorekodiwa zenyewe huwa zimeharibika au hazipo. Hatukuweza kuunda hoja ya logi ambayo haikuwa na false positives na false negatives kwa pamoja. Tulikagua core zaidi kwa mkono na kupata mifano ya ziada, lakini mchakato huo ulihitaji kazi nyingi mno kutoa seti ya data ya kuaminika.

Katika hatua hii ya uchunguzi, tuliondoa kimakosa uwezekano wa hitilafu ya hardware, kwa sababu tuliona mianguko katika maeneo kadhaa na aina kadhaa za hardware, hivyo bado tulikuwa tukitafuta sababu za software pekee. Kwa siku chache, tulichunguza kwa kina muanguko mmoja wa %rsp iliyopotoshwa, tukijenga upya historia ya kabla ya muanguko kwa kutumia yaliyomo kwenye stack na rejista. Hili lilitoa vidokezo kadhaa, lakini kwa kuwa hatukuachana na hitimisho letu la awali kwamba hitilafu zote zilikuwa na chanzo kimoja, halikututoa kwenye mkwamo.

Vidokezo kutoka kwenye stack

Kabla ya kufikia hatua iliyobadilisha uchunguzi wetu, ni muhimu kueleza aina ya taarifa tulizokuwa tukitoa kutoka kwenye faili za core.

Rockset hukusanywa kwa -fno-omit-frame-pointer, kwa hivyo stack frame iliyo active hufikiwa daima kupitia %rbp, na callers huunda orodha iliyounganishwa ya frame pointers.

Kwenye Linux x86_64, AMD64 System V ABI pia huhifadhi baiti 128 chini ya %rsp kama red zone. Eneo hilo linapatikana kwa code ya userspace na, muhimu zaidi, kernel huahidi kutoliharibu inapowasilisha signal, kama sehemu ya mkataba wa ABI.

Red zone ilikuwa muhimu katika debugging yetu ya muanguko wa baada ya kurudi, kwa sababu huhifadhi baadhi ya taarifa za kabla ya kurudi. SIGSEGV inapochochewa, fatal signal handler ya folly huendeshwa kwenye stack ya thread inayoanguka. Stack frames ambazo haziko active tena (kwa sababu function zao zimerudi) zitaharibiwa na signal handler, isipokuwa baiti 128 za mwisho. Ndiyo maana tunaweza kusema mambo kama “stack frame ya X iliyokuwa imetoka tu ilionekana halali, isipokuwa anwani ya kurudi ya NULL.” Red zone huhifadhi baadhi ya frames zisizo active, au wakati mwingine mkia tu wa frame moja isiyo active.

Mchoro wa stack unaoonyesha stack frames zilizoharibika zinazoweza kuandika juu ya anwani za kurudi na kusababisha mianguko.

Tulipata muanguko mmoja wa stack iliyopotoshwa ambapo functions zote zilizohusika zilikuwa ndogo sana. Hilo lilituwezesha kuona kwamba %rsp ilikuwa imepotoshwa wakati wa utekelezaji wa function rahisi kiasi, na kwamba miito zaidi ilifaulu baadaye. Programu ilianguka tu wakati function iliyo active ilipojaribu hatimaye kurudi. Hakuna njia hizo za code zilizotumia exceptions, inline assembly, setcontext, au longjmp, kwa hiyo kama stack pointer ilibadilika kweli kama core ilivyodokeza, hakuna hitilafu ya kuaminika katika code ya userspace iliyoeleza tatizo.

Hilo lilitusukuma kuelekea kernel.

Rockset hutumia signals kwa ukali zaidi kuliko programu nyingi. Utekelezaji wa hoja hugawanywa katika tasks nyingi nyepesi zinazobadilishana data. Hili ni muhimu kwa kushughulikia workloads za high-QPS kwa ufanisi, lakini hufanya uhasibu wa CPU kwa kila hoja kuwa mgumu kwa kuwa kazi ya hoja nyingi huunganishwa kwenye thread pool ileile.

Suluhisho letu ni kitu tunachokiita coarse_thread_cputime_clock, kinachokadiria clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) kwa gharama ndogo ya kutosha kusampuli kwenye kila mpaka wa task. API ya timer_create inaweza kutumiwa kupanga uwasilishaji wa signal wa mara kwa mara kulingana na dhana kadhaa za kupita kwa muda, ikiwemo mkusanyiko wa muda wa CPU. Tunapanga signal (SIGUSR2) iwasilishwe kila baada ya milisekunde chache za muda wa CPU, ambapo signal handler husasisha thamani ya thread-local. Ingawa tasks nyingi hazioni saa ya jumla ikisonga wakati zinatekelezwa, kujumlisha deltas zote hutoa makadirio yasiyo na upendeleo ya muda halisi wa CPU kwa hoja.

Kwa sababu tunawasilisha signals mara nyingi sana, hitilafu adimu ya kernel kuhusu context switching au uwasilishaji wa signal ilionekana kuwa ya kuaminika. Tulitumia muda kusoma ripoti za hitilafu, source code ya kernel, na patches za kernel maalum kwa Azure. Tulijaribu stress tests. Hatukuweza kupata chochote kilichoonekana kuhusiana.

Wakati huo tuliamua kurudi nyuma na kujaribu mbinu tofauti.

Daktari au mtaalamu wa epidemiolojia?

Kuna njia mbili pana za kufanya debugging ya tatizo kama hili.

Moja ni kutenda kama aina fulani ya daktari: kuzingatia mgonjwa mmoja, kufanya vipimo vingi, na kujaribu kugundua kisa kimoja kutokana na ushahidi wa kina.

Nyingine ni kutenda zaidi kama mtaalamu wa epidemiolojia: kuangalia jamii nzima na kuuliza kama kuna mifumo ambayo kisa kimoja hakiwezi kufichua. Je, hitilafu ilianza kwenye toleo fulani maalum? Je, inahusiana na SKU moja ya hardware (CPU na modeli maalum ya seva), eneo moja, au toleo moja la kernel? Je, kuna makundi kadhaa tofauti yaliyofichwa ndani ya kile kinachoonekana kama dalili moja?

Kwa kiasi kikubwa tulikuwa katika hali ya daktari. Mabadiliko muhimu yalikuwa kuamua kwamba tulihitaji kukusanya data ya jamii yenye ubora wa juu.

Kusafisha data

Majaribio yetu ya awali ya kupata kiotomatiki matukio yote ya tatizo yalishindwa kwa sababu tulikuwa tukijaribu kutumia utafutaji wa maandishi kwenye logi. Core dump zenyewe zina taarifa nyingi zaidi, lakini kuziangalia kwa mkono hakukuwa kunapanuka. Tuliamua kuweka juhudi kujenga pipeline inayoweza kuchambua core dump kiotomatiki.

Tuliifanya ChatGPT iandike script iliyopakua prefix ya kila faili la core, ikatoa rejista, ikachuja false positives zinazojulikana kwa kutumia logi, na kuweka lebo ya muanguko kiotomatiki kama return-to-null, misaligned-stack, au other. Kisha tuliendesha script hiyo sambamba kwenye kila core dump ya Rockset ya uzalishaji kutoka mwaka uliopita.

Hii ndiyo ilikuwa hatua ya mgeuko.

Mara tu tulipopata seti safi ya data, uhusiano ulionekana mara moja. Tulichokuwa tukikichukulia kama hitilafu moja ya ajabu kwa kweli kilikuwa jamii mbili tofauti za mianguko.

Core za return-to-null zilienea katika clusters na maeneo mengi ya kijiografia. Mzunguko wake ulikuwa umeongezeka hivi karibuni, lakini hakukuwa na tarehe dhahiri ya kuanza wala mpaka safi wa miundombinu.

Mianguko ya misaligned-stack ilionekana tofauti kabisa. Yote ilitoka eneo moja, ilikuwa na tarehe wazi ya kuanza, na haikutokea kamwe kwenye nodi zilizokuwa zimekuwa zikiendeshwa kwa muda mrefu. Ingawa ilihusisha VM kadhaa za Azure (mashine pepe zilizopangishwa kwenye wingu), muundo ulionekana kama mashine moja ya kimwili yenye hardware mbovu iliyosababisha matatizo kwa VM yoyote iliyotua juu yake.

Mchoro wa nukta wa viwango vya mianguko kwa cluster kadiri muda unavyopita, ukionyesha mianguko mingi ikiwa imejikita katika clusters 2, 3, na 6, huku kukiwa na ongezeko la ghafla katika cluster 1 karibu na mwisho wa kipindi.

Huo ndio wakati tulipotambua kuwa kiakili tulikuwa tumeunganisha hitilafu mbili. Kwa sababu tulikuwa tunachanganya mifano pinzani kutoka hitilafu zote mbili, hatukuweza kupata maelezo moja thabiti.

Hitilafu #1: host mbovu

Tukiwa na orodha safi ya nodi za Kubernetes na timestamps, tuliweza kufuatilia mianguko ya stack iliyopotoshwa hadi host moja ya kimwili, ambayo ilikuwa rahisi kuiweka kwenye denylist.

Hatukuweza kurudia uharibifu wa rejista kwenye host hiyo katika mazingira yanayodhibitiwa, hata baada ya wiki kadhaa za stress testing. Hata hivyo, mara host yenye tatizo ilipotolewa kwenye huduma, mianguko ya stack iliyopotoshwa ilitoweka.

Kuondoa host mbovu si suluhisho la kudumu, kwa maana hakuzuii kutokea tena kwa tatizo lilelile. Hata hivyo, tunaweza kubadilisha software ili tatizo kama hilo likirudia, litambuliwe na kushughulikiwa kwa urahisi. Tuliboresha fatal signal handler yetu ili ijumuisha hali ya rejista, ili tuweze kugundua kurudia kwake kutoka kwenye logi pekee (bila kuhitaji core dump). Tulibadilisha control plane ili VM zitumike tena mara nyingi badala ya kurekebishwa upya, jambo linalorahisisha sana kugundua nodi mbovu katika kiwango chetu cha tabaka la miundombinu. Pia tulisasisha runbooks zetu (na miundo ya mawazo ya timu yetu) kujumuisha uwezekano huu.

Baada ya kutenganisha mianguko ya host mbovu, core zilizobaki za return-to-null zikawa rahisi zaidi kuzifasiri. Awali tulikuwa tumeondoa exception unwinding kwa sababu tulidhani tuna mifano pinzani: mianguko katika njia za code ambako exceptions bila shaka hazikutumiwa. Lakini mifano hiyo pinzani yote ilitoka kwenye cluster ya uharibifu wa hardware.

Tuliporudi kuchunguza core zilizobaki tukiwa na hilo akilini, tuligundua kuwa hitimisho hili lilikuwa kinyume kabisa: mianguko yote ilikuwa ikitokea wakati wa exception unwinding.

Exception handling ni uhamisho wa udhibiti wa dynamic

C++ inapotupa exception, runtime lazima igundue ni catch block ipi inapaswa kuipokea na ni destructors au cleanup handlers zipi zinapaswa kuendeshwa njiani. Compiler hutoa metadata hii, lakini ulinganishaji halisi hutokea kwa dynamic wakati wa runtime.

Exception unwinding haifanywi kwa kweli na function inayoita throw, bali na helper functions zinazoitwa na code iliyokusanywa inayotokana nayo. Routines hizo za runtime huchunguza stack, hupata metadata kuhusu functions zilizo kwenye stack, hutafuta kwa dynamic cleanup handlers na catch blocks, kisha huhamisha udhibiti kwenda mojawapo ya maeneo hayo. Kuhamisha udhibiti hujumuisha ku-unwind stack frames zote zilizo kati (zikiwemo za helper functions).

Kiutendaji, hii iko karibu zaidi na longjmp au fiber switch kuliko mwito na kurudi kwa kawaida. Rejista za callee save lazima zirejeshwe, pamoja na rejista za stack frame %rbp na %rsp.

Binary yetu hu-link dhidi ya maktaba mbili zenye utekelezaji wa functions zinazofanya C++ exception unwinding: libgcc na GNU libunwind. Definitions za GNU libunwind ndizo zilizochaguliwa na dynamic linker. Hilo lilitushangaza; tulitarajia utekelezaji wa libgcc ushinde kwa sababu ya kanuni za symbol versioning; hata hivyo, kukagua binaries zinazoendeshwa kulionyesha sivyo ilivyokuwa.

Kuondoa dhana moja ya mwisho

Katika hatua hii dhana yetu ya kazi ilibadilika, tulipolegeza dhana nyingine tuliyokuwa tumeweka tulipodhani kulikuwa na hitilafu moja tu.

Labda hatukuwa tunaona function ya kawaida ikirudi kwa NULL. Labda tulikuwa tunaona uhamisho wa unwind—kimsingi urejeshaji wa rejista wa mtindo wa setcontext—ambapo instruction pointer ya lengwa ilikuwa imekuwa NULL kabla ya udhibiti kuhamishwa. Kwa maneno mengine, data isiyo sahihi kutoka maktaba ya unwind badala ya nafasi isiyo sahihi ya anwani ya kurudi kwenye stack.

Hilo lilipunguza tatizo kwa kiasi kikubwa. Ama GNU libunwind ilikuwa inakokotoa hali lengwa isiyo sahihi, au ilikuwa inakokotoa hali sahihi na kitu fulani kilikuwa kinaiharibu kabla haijatumiwa.

Tulisoma source ya GNU libunwind na kugundua kwamba huunda ucontext_t kwenye stack, hujaza hali ya rejista inayotakiwa kwa frame ya cleanup handler, kisha hukabidhi pointer ya struct hiyo kwa routine ya ndani ya assembly: _Ux86_64_setcontext.

Katika hatua hii tulikuwa na vipande vyote.

ucontext_t iliyoundwa huishi katika mojawapo ya stack frames zinazounwindiwa na _Ux86_64_setcontext, wakati wa utekelezaji wa function hiyo. Je, _Ux86_64_setcontext ilikuwa inasoma kutoka struct baada ya kubadilisha %rsp, ambapo struct hiyo haikuwa tena sehemu ya stack iliyo active? Hilo lingeifanya iwe hatarini kuharibiwa na uwasilishaji wa signal, kama SIGUSR2 yetu ya mara kwa mara.

Hitilafu #2: hitilafu ya libunwind

Jibu lilikuwa ndiyo.

Haya ndiyo maagizo sita ya mwisho ya _Ux86_64_setcontext katika toleo la GNU libunwind tulilokuwa tukitumia, ambayo mengi ni maagizo ya mov yanayopakia kutoka kumbukumbu hadi rejista lengwa:

Maandishi ya Kawaida

1
74: mov UC_MCONTEXT_GREGS_RSP(%rdi),%rsp
2
75:
3
76: /* push the return address on the stack */
4
77: mov UC_MCONTEXT_GREGS_RIP(%rdi),%rcx
5
78: push %rcx
6
79:
7
80: mov UC_MCONTEXT_GREGS_RCX(%rdi),%rcx
8
81: mov UC_MCONTEXT_GREGS_RDI(%rdi),%rdi
9
82: retq

(%rdi inaelekeza kwenye ucontext_t iliyotengwa kwenye stack, na macros za UC_MCONTEXT_* hupanuka tu kuwa offset thabiti ambapo rejista fulani huhifadhiwa.)

Instruction ya kwanza ndiyo mwanzo wa dirisha la race. Husasisha %rsp ili ielekeze kwenye chini mpya ya stack iliyo active. Mara tu hili linapotokea, struct inayoelekezwa na %rdi si sehemu tena ya stack iliyo active (au red zone), na si marufuku tena kwa kernel kuigusa.

Kwa kawaida hili halisababishi matatizo, lakini signal ikifika wakati sahihi kabisa (au mbaya?), kernel itajenga signal frame kwenye %rsp-128. Hilo linaweza kuandika juu ya kumbukumbu inayoelekezwa na %rdi.

Ikiwa hilo litatokea kabla ya instruction inayofuata kusoma UC_MCONTEXT_GREGS_RIP(%rdi), basi instruction pointer iliyorejeshwa inaweza kuharibika. Katika mianguko yetu, iligeuka kuwa NULL.

Hiyo ndiyo hitilafu.

Kwa nini core zilijificha kama kurudi vibaya kwa kawaida

Assembly hii pia inaeleza mojawapo ya uchunguzi uliotuchanganya: kwa nini function X ilikuwa na NULL kwenye nafasi ya anwani ya kurudi ya stack frame iliyotangulia.

setcontext iliandikwa kurejesha rejista zote, ikiwemo %rdi, kwa hivyo haiwezi kutumia rejista hiyo kusoma UC_MCONTEXT_GREGS_RIP(%rdi) katika dakika ya mwisho ya uhamisho wa udhibiti. Badala yake, husoma thamani mapema, huihifadhi kwenye stack, hurejesha rejista chache zaidi, kisha hutumia retq kusoma thamani iliyohifadhiwa na kuhamisha udhibiti.

Kilichoonekana kwenye core kama “function ilirudi kwa NULL” kwa kweli kilikuwa “unwinder iliunda anwani lengwa ya kurudi kwenye stack, lakini lengo hilo lilikuwa limeharibika kabla ya uhamisho kukamilika.” Tulidhani uharibifu wa nafasi ya anwani ya kurudi lazima utokee hapohapo, kwa sababu hatukujua sehemu yoyote ambako data (inayoweza kuharibika) iliandikwa kwa makusudi kwenye nafasi ya anwani ya kurudi.

Dirisha la race la instruction moja

Kinachofanya hitilafu hii ionekane ya kipuuzi ni jinsi dirisha hili la race lilivyo finyu. Katika aina hii ya race condition, tukio la nje (signal) lazima litokee kati ya hatua mbili zinazochukuliwa na thread nyingine. Kadiri hatua hizo zilivyo karibu, ndivyo uwezekano wa race condition kutokea unavyopungua.

Katika kisa hiki, dirisha dhaifu lina upana wa instruction moja kihalisi! Signal lazima iwasilishwe baada ya %rsp kubadilishwa, lakini kabla ya instruction inayofuata kupakia %rip. Instructions kadhaa rahisi kama hii zinaweza kuendeshwa kwa kila cycle kwenye CPU ya kisasa ya super-scalar out-of-order, hivyo dirisha la race ni takriban pikosekunde mia moja.

Tulipopata race hii, mwitikio wetu wa kwanza ulikuwa kwamba lazima iwe adimu mno kueleza kiwango cha mianguko tulichoona. Tulikuwa tunaona zaidi ya mianguko kumi na miwili ya return-to-null kwa siku kote kwenye fleet. Je, race ya instruction moja wakati wa kusafisha exception kweli ingeweza kueleza hilo?

Tuligeukia makadirio ya Fermat. Ikiwa dirisha dhaifu liko takriban sekunde 101010^{-10} na SIGUSR2 hufika kila sekunde 10210^{-2} za muda wa CPU, basi kila exception cleanup handler au catch block ina uwezekano wa takriban 10810^{-8} wa kupoteza race.

Rockset hutumia exceptions kama sehemu ya utaratibu wake wa ndani wa backpressure wakati wa ingest. Host moja iliyozidiwa inaweza kutupa exceptions takriban 10410^{4} kwa sekunde. Hilo linamaanisha wastani wa muda kati ya kushindwa kwa host inayotumia backpressure ni sekunde 10410^{4}, au muanguko mmoja kila baada ya saa chache. Katika kiwango cha fleet, hilo linatosha zaidi kueleza marudio ya mianguko yaliyoonekana.

Kwa nini hitilafu ya libunwind ilionekana sasa?

Hitilafu ya GNU libunwind ni ya zamani—zaidi ya miaka 18, ikiwa katika toleo la kwanza la x86_64 lililounga mkono C++ exception unwinding.

Kwa nini basi ilijitokeza sasa?

Kiwango cha mianguko kinakaribiana na uwiano wa idadi ya exceptions zinazotupwa na signals zinazowasilishwa. Pia hutegemea kiasi cha stack kinachotumiwa na signal handler.

Rockset si ya kawaida kwenye mihimili yote mitatu. Tunatupa exceptions kwa viwango vya juu kama sehemu ya kawaida ya kudhibiti kuzidiwa; tunawasilisha SIGUSR2 mara nyingi isivyo kawaida kwa sababu ya coarse_thread_cputime_clock; na mapema mwaka huu tuliifanya handler ya SIGUSR2 itumie stack zaidi kwa kuongeza mwito wa timer_getoverrun, ili tuweze kuhesabu signals zilizounganishwa.

Mabadiliko hayo ya mwisho yanaonekana kuwa muhimu. Ikiwa handler inatumia stack kidogo vya kutosha, huenda isifikie na kuandika juu ya kumbukumbu ya zamani ya ucontext_t. Kabla ya mabadiliko hayo, hatuoni mianguko hii kabisa. Baada ya mabadiliko, kiwango kilibaki chini hadi tulipoongeza mzigo kwa baadhi ya matumizi yaliyosisitiza utaratibu wa backpressure.

Kwa maneno mengine, hitilafu ya libunwind imekuwapo daima, lakini zao la kiwango chetu cha exceptions, kiwango cha signals, na matumizi ya stack ya handler lilivuka hivi karibuni tu kizingiti kilichoifanya ionekane kiutendaji.

Utaratibu huu pia unaeleza bahati kwamba hitilafu ya hardware na hitilafu ya libunwind zilianguka zaidi ndani ya DocumentTree::updateDocument. Mianguko kutoka libunwind ilielemea sana method hii, kwa sababu huwa active kila mara tunapotupa exception ili kutumia backpressure ya ingest. Pia ilichaguliwa sana kwa mianguko ya kupotoshwa kwa %rsp kwa sababu nodi ya hardware mbovu ilikuwa ya SKU tunayotumia kwa ingest ya bulk, inayotumia muda mwingi wa CPU katika method hiyo.

Hatua yetu ya haraka ya kupunguza athari ilikuwa kubadili kutoka GNU libunwind hadi unwinder ya libgcc. Hilo lilikuwa badilishano zuri lenyewe: utekelezaji wa libgcc umenufaika na kazi nyingi ya kupunguza msuguano wa lock, jambo muhimu unapopanua hadi VM kubwa.

Pia tulipeleka upstream reproducer inayojitosheleza na marekebisho(fungua katika dirisha jipya) kwa GNU libunwind, na kuthibitisha kuwa unwinders nyingine hazina tatizo kama hilo.

Nguvu ya utambuzi wa kiwango cha jamii

Safari hii ya debugging ilitufundisha mengi kuhusu maelezo mahususi ya dynamic linking, metadata ya DWARF unwind, uwasilishaji wa signal wa Linux, System V ABI, na mitambo ya C++ exceptions. Lakini somo kuu lilikuwa rahisi kuliko hayo yote.

Hatua muhimu zaidi haikuwa kusoma assembly kwa werevu au maarifa ya kina ya maelezo. Ilikuwa kujenga seti ya data ya ubora wa juu. Bila seti hii ya data, tulikuwa tunachanganya matukio mawili tofauti kuwa hadithi moja na kujaribu kujitoa kwenye mkanganyiko kwa kufikiri tu. Mara tulipopata data sahihi na kamili ya jamii, muundo wa tatizo ulionekana wazi: jamii moja ya mianguko ilitokana na host mbovu, na nyingine ilitokana na race katika libunwind. Data ilipoboreka, debugging ikawa rahisi.

Kwa mifumo ya miundombinu kama Rockset, hilo ni muhimu sana. Uchunguzi huu uliimarisha dhamira yetu kwa instrumentation ya kina, uchunguzi wa kiotomatiki, na maboresho endelevu ya zana zetu za uendeshaji. Kutegemeka si tu kurekebisha hitilafu baada ya kutokea—ni kujenga data, workflows, na ujuzi unaogeuza matatizo yanayoonekana haiwezekani kuwa yanayotambulika na kutatulika.

Waandishi

By Nathan Bronson, Member of Technical Staff