Přeskoč na hlavní obsah
OpenAI

30. června 2026

Inženýrství

Epidemiologie core dumpů: oprava 18 let staré chyby

Použití analýzy na úrovni celé populace k ladění záludných crashů v naší datové infrastruktuře.

Načítání…

Modely a agenti OpenAI se čím dál víc opírají o škálovatelnou datovou infrastrukturu, aby mohli vyhledávat relevantní data v okamžiku inference: když modely přemýšlí nad tvou otázkou. Některé z těchto služeb jsou napsané v C++, jehož nízkoúrovňová kontrola nad systémem nám umožňuje maximalizovat výkon a minimalizovat využití paměti. Tyto výhody efektivity jsou důležité, jak škálujeme, ale absence paměťové bezpečnosti v C++ znamená, že chyby můžou způsobit crashe zápisem na nesprávné nebo neexistující paměťové adresy.

Před pár měsíci jsme uvnitř služby Rockset zaznamenali několik crashů. Jde o zakázkovou část naší datové infrastruktury pro ChatGPT, která je klíčová pro spoustu datových pluginů a pro vyhledávání v konverzacích. Při každém z těchto crashů to vypadalo, že běžná funkce v C++ doběhla a pak se vrátila na fiktivní adresu. To způsobilo, že jádro program zastavilo, protože ukazatel instrukcí už neukazoval na kód. Někdy byl slot s návratovou adresou v rámci zásobníku NULL. Někdy se zdálo, že samotný registr CPU pro ukazatel zásobníku byl posunutý o 8 bajtů, jako by se %rsp nějak snížil uprostřed normálního běhu. V obou případech došlo ke crashi po návratu.

Tohle nejsou běžné režimy selhání aplikačního kódu. Je možné, že zbloudilý zápis dopadne jen na uloženou návratovou adresu, ale je to extrémně nepravděpodobné. Chyba, která rozhodí zarovnání %rsp o 8 bez použití inline assembly, setcontext nebo longjmp (nic z toho nepoužíváme), je ještě podivnější, protože zkompilovaný kód tenhle registr přímo upravuje jen ve funkčním prologu a epilogu. Ke každá hypotéze, která nás (nebo ChatGPT) napadla, byly silné důkazy proti, takže chyba se jevila jako nemožná.

To,co jsme považovali za jeden problém,se nakonec ukázalo jako dvě nesouvisející chyby,které jsme čistě náhodou objevili ve stejnou dobu. Ta první: tiché poškození hardwaru na jednom hostu v Azure, kde CPU prostě nepočítalo správně. Za druhé: 18 let stará race condition v GNU libunwind, nepovšimnutá chyba v široce používané open source knihovně.

Tenhle příspěvek je příběh o tom,jak jsme identifikovali a opravili zdánlivě nevysvětlitelné crashe tak, že jsme uvažovali jako epidemiolog a vybudovali kvalitní datovou sadu o celé populaci crashů.

První pokus o ladění: pečlivé zkoumání několika core dumpů

Nejdřív se pojďme podívat na Rockset víc do hloubky. Je to cloud-native datový systém pro vyhledávání a analytiku v reálném čase, který v OpenAI používáme pro řadu interních use casů, třeba pro synchronizační konektory (OpenAI koupila Rockset v roce 2024). Streamované aktualizace se používají k udržování aktuálního indexu znalostní báze pracovního prostoru, aby ChatGPT mohl při odpovídání na otázky nebo provádění akcí vyhledat relevantní informace.

Spouštěcí vrstva Rocksetu je napsaná v C++. Jazyk C++ poskytuje nízkoúrovňový přístup k CPU, což je dobré pro výkon a efektivitu, ale znamená to, že aplikační chyby můžou vést k neplatným přístupům do paměti a segfaultům. Abychom je lépe dohledali, používáme fatal signal handler z folly, který při crashi zaloguje stack trace, a odpovídající core dumpy (snímek stavu programu v okamžiku crashu) nahráváme do Azure Blob Storage pro pozdější analýzu. Všechny listy zpracování dotazů v Rocksetu jsou replikované, což minimalizuje dopad crashe na klienta. Každý segfault ale odpovídá chybě, kterou je potřeba opravit, abychom splnili naše cíle v oblasti spolehlivosti a kvality.

Náš první přístup byl brát tyhle core dumpy jako běžný ladicí problém: velmi detailně prozkoumat pár core dumpů, vytvořit hypotézy a postupně je jednu po druhé vyvracet.

Většina crashů se objevila v metodě DocumentTree::updateDocument. U těchto crashů to vypadalo, že updateDocument zavolala nějakou neznámou funkci X, při běhu X se poškodil zásobník a pak se X vrátila na adresu, která nebyla spustitelný kód. V některých případech vypadal právě odebraný rámec X jako platný, až na to, že jeho uložená návratová adresa byla NULL. V jiných případech vypadal špatně samotný ukazatel zásobníku, ale další platný rámec pořád vypadal, že patří updateDocument.

Nevěděli jsme, kdy se zásobník poškozuje, což znamenalo obrovský prostor pro hledání. updateDocument je velká metoda, ve které se hodně inlinuje, takže počet kandidátů na X byl obrovský.

Byla to chyba v našem kódu C++? Něco v kompilátoru nebo linkování? Problém v některé z našich runtime knihoven? Chyba v linuxovém jádře kolem doručování signálů nebo přepínání kontextu? Něco ještě vzácnějšího? A pokud šlo o zbloudilý zápis, proč to nezachytilo naše staging prostředí ASAN?

Zkoušeli jsme použít logy na úrovni aplikace, abychom našli všechny výskyty problému, ale chyby s poškozením zásobníku se z logů špatně třídí – zalogované stack trace jsou totiž samy poškozené nebo chybí. Nepodařilo se nám sestavit logovací dotaz, který by neměl zároveň falešně pozitivní i falešně negativní výsledky. Ručně jsme prohlédli další core dumpy a našli pár dalších příkladů, ale bylo to moc pracné na to, abychom z toho měli důvěryhodný datový soubor.

V téhle fázi vyšetřování jsme (nesprávně) vyloučili hardwarovou chybu, protože jsme viděli crashe napříč více regiony a více typy hardwaru. Proto jsme dále hledali čistě softwarové příčiny. Pár dní jsme šli extrémně do hloubky u jednoho crashe s rozhozeným zarovnáním %rsp a rekonstruovali jsme historii před crashem pomocí obsahu zásobníku a registrů. To přineslo pár možných stop, ale protože jsme se nedokázali pustit našeho úvodního závěru, že všechny chyby mají stejnou příčinu, nikam nás to neposunulo.

Stopy ze zásobníku

Než se dostaneme k bodu zlomu našeho vyšetřování, je důležité vysvětlit, jaký typ informací jsme z core souborů získávali.

Rockset se kompiluje s -fno-omit-frame-pointer, takže aktivní zásobníkový rámec je vždy dosažitelný přes %rbp a volající tvoří propojený seznam ukazatelů na rámce.

Na Linuxu x86_64 vyhrazuje AMD64 System V ABI také 128 bajtů pod %rsp jako red zone. Tato oblast je k dispozici kódu v userspace a hlavně: jádro slibuje, že ji při doručování signálu nepřepíše, jako součást ABI kontraktu.

Red zone byla pro naše ladění crashe po návratu klíčová, protože zachovává část informací z doby před návratem. Když se vyvolá SIGSEGV, spustí se fatal signal handler z folly na zásobníku vlákna, které spadlo. Zásobníkové rámce, které už nejsou aktivní (protože se jejich funkce vrátila), signal handler přepíše, s výjimkou posledních 128 bajtů. Proto jsme schopni říkat věci jako: „Právě odebraný zásobníkový rámec X vypadal validně, až na návratovou adresu NULL.“ Red zone zachovává část neaktivních rámců,nebo někdy jen konec jednoho neaktivního rámce.

Diagram zásobníku ukazuje poškozené rámce zásobníku, které mohou přepsat návratové adresy a způsobit crashe.

Našli jsme jeden crash kvůli nezarovnanému zásobníku, kde byly všechny zapojené funkce hodně malé. Díky tomu jsme viděli, že se %rsp během běhu relativně jednoduché funkce přestal správně zarovnávat a že potom další volání proběhla úspěšně. Program spadl až ve chvíli, kdy se aktivní funkce konečně pokusila vrátit. Žádná z těchto cest kódem nepoužívala výjimky, inline assembly, setcontext ani longjmp, takže pokud se ukazatel zásobníku opravdu změnil tak, jak core dump naznačoval, žádná uvěřitelná chyba v userspace kódu ten problém nevysvětlovala.

To nás nasměrovalo ke kernelu.

Rockset používá signály agresivněji než většina programů. Spouštění dotazů je rozdělené do mnoha lehkých úloh, které si mezi sebou předávají data. To je důležité pro efektivní obsluhu workloadů s vysokým QPS, ale ztěžuje to účtování CPU na úrovni jednotlivých dotazů, protože se práce pro mnoho dotazů multiplexuje do stejného thread poolu.

Našemu řešení říkáme coarse_thread_cputime_clock, což aproximuje clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) dostatečně levně na to, abychom mohli vzorkovat na každé hranici úlohy. API timer_create se dá použít k naplánování periodického doručování signálu podle několika pojetí plynutí času, včetně akumulace času CPU. Naplánujeme doručení signálu (SIGUSR2) každých pár milisekund času CPU; v tu chvíli signal handler aktualizuje hodnotu v thread-local úložišti. I když u spousty úloh během jejich běhu coarse clock vůbec neposkočí, sečtení všech delt dává nezkreslený odhad skutečného času CPU pro dotaz.

Protože signály doručujeme tak často, dávalo smysl, že by mohlo jít o vzácnou chybu jádra kolem přepínání kontextu nebo doručování signálů. Strávili jsme čas čtením bug reportů, zdrojového kódu jádra a patchů jádra specifických pro Azure. Zkoušeli jsme zátěžové testy. Nenašli jsme nic, co by se zdálo souviset.

V tu chvíli jsme se rozhodli udělat krok zpátky a zkusit jiný přístup.

Doktor, nebo epidemiolog?

Existují dva obecné způsoby, jak ladit problém tohoto typu.

Jedna možnost je postupovat trochu jako doktor: zaměřit se na jednoho pacienta, udělat spoustu testů a z detailních důkazů zkusit diagnostikovat jeden konkrétní případ.

Druhá možnost je jednat spíš jako epidemiolog: podívat se na celou populaci a ptát se, jestli existují vzorce, které se z jednoho případu nedají vyčíst. Začala chyba v konkrétní verzi? Koreluje s jedním hardwarovým SKU (konkrétním CPU a modelem serveru), jedním regionem nebo jednou verzí jádra? Neskrývá se uvnitř toho, co vypadá jako jeden syndrom, několik různých clusterů?

Většinu času jsme byli v režimu doktora. Klíčový posun nastal, když jsme se rozhodli, že potřebujeme nasbírat kvalitní populační data.

Čištění dat

Naše předchozí pokusy automaticky najít všechny výskyty problému selhaly, protože jsme se snažili používat textové vyhledávání uvnitř logů. Samotné core dumpy obsahují mnohem víc informací, ale ruční procházení se nedalo škálovat. Rozhodli jsme se investovat úsilí do vybudování pipeline, která by core dumpy dokázala automaticky analyzovat.

Nechali jsme ChatGPT napsat skript, který stáhl prefix každého core souboru, vytáhl registry, pomocí logů odfiltroval známá falešná pozitiva a automaticky označil crash jako return-to-null, misaligned-stack nebo jiný. Pak jsme ten skript spustili paralelně nad každým produkčním core dumpem Rocksetu z předchozího roku.

To byl bod zlomu.

Jakmile jsme měli čistou datovou sadu, korelace se objevily okamžitě. To, co jsme brali jako jednu divnou chybu, byly ve skutečnosti dvě oddělené populace crashů.

Core dumpy s návratem na null byly rozprostřené napříč mnoha clustery a geografickými regiony. Jejich četnost v poslední době vzrostla, ale nebylo tam žádné jasné datum začátku ani čistá hranice v infrastruktuře.

Crashe typu misaligned-stack vypadaly úplně jinak. Všechny pocházely z jednoho regionu, měly jasné datum začátku a nikdy se neobjevily na uzlech, které běžely dlouhou dobu. I když se týkaly více Azure VM (virtuálních strojů hostovaných v cloudu), vzorec vypadal jako jeden fyzický stroj se špatným hardwarem, který způsoboval problémy každé VM, který na něm zrovna skončila.

Bodový graf četnosti crashů podle clusterů v čase ukazuje, že většina crashů se soustředí v clusterech 2, 3 a 6, se špičkou v clusteru 1 ke konci období.

To byl moment, kdy nám došlo, že jsme si v hlavě spojovali dohromady dvě různé chyby. A protože jsme míchali protipříklady z obou chyb, nedokázali jsme najít jediné soudržné vysvětlení.

Chyba č. 1: špatný hostitel

S čistým seznamem uzlů Kubernetes a časových značek jsme dokázali crashe s nesprávně zarovnaným zásobníkem dohledat k jedinému fyzickému hostiteli, který šel snadno dát na denylist.

Poškození registrů se nám na tomto hostiteli nepodařilo reprodukovat v kontrolovaném prostředí ani po několika týdnech zátěžového testování. Jakmile byl problematický hostitel vyřazen z provozu, crashe s nesprávně zarovnaným zásobníkem zmizely.

Odstranění špatného hostitele není trvalé řešení v tom smyslu, že nezabrání novému výskytu téhož problému. Můžeme ale změnit software tak, aby se podobný problém při opakování snadno odhalil a vyřešil. Vylepšili jsme fatal signal handler, aby zahrnoval stav registrů a případné nové výskyty aby šlo zjistit už z logů (bez core dumpu). Změnili jsme control plane tak, aby se VM obvykle používaly znovu, a ne recyklovaly, což na naší úrovni infrastrukturního stacku výrazně usnadňuje detekci špatných uzlů. Aktualizovali jsme také runbooky (a mentální modely našeho týmu), aby s touto možností počítaly.

Po oddělení crashů způsobených špatným hostitelem se o zbývajících core dumpech s návratem na NULL uvažovalo mnohem snáz. Nejdřív jsme vyloučili unwinding výjimek, protože jsme si mysleli, že máme protiargumenty: crashe v cestách kódu, kde se výjimky určitě nepoužívaly. Jenže všechny tyto protiargumenty pocházely z clusteru poškození hardwaru.

Když jsme se s tímto vědomím znovu podívali na zbývající core dumpy, zjistili jsme, že závěr byl přesně opačný: všechny crashe nastávaly během unwindingu výjimek.

Zpracování výjimek je dynamické předání kontroly

Když C++ vyhodí výjimku, runtime musí zjistit, který blok catch ji má převzít a které destruktory nebo úklidové handlery se mají cestou spustit. Kompilátor tato metadata emituje, ale vlastní párování probíhá dynamicky za běhu.

Unwinding výjimky ve skutečnosti neprovádí funkce, která vyvolá throw, ale pomocné funkce volané výsledným zkompilovaným kódem. Tyto runtime rutiny prohlédnou zásobník, načtou metadata o funkcích nalezených na zásobníku, dynamicky vyhledají úklidové handlery a bloky catch a pak předají řízení do jednoho z těchto míst. Předání řízení zahrnuje odvinutí všech mezilehlých rámců zásobníku (včetně rámců pomocných funkcí).

Provozně je to mnohem blíž k longjmp nebo přepnutí fiberu než k běžnému volání a návratu. Je nutné obnovit registry ukládané volaným i registry rámce zásobníku %rbp a %rsp.

Náš binární soubor linkuje proti dvěma knihovnám obsahujícím implementace funkcí pro odvíjení výjimek C++: libgcc a GNU libunwind. Dynamický linker vybral definice z GNU libunwind. To nás překvapilo; čekali jsme, že kvůli pravidlům verzování symbolů vyhraje implementace libgcc, ale kontrola běžících binárek ukázala opak.

Vyvrácení jednoho zbývajícího předpokladu

V tomto bodě se naše pracovní hypotéza změnila, protože jsme přestali lpět na dalším předpokladu z doby, kdy jsme si mysleli, že existuje jen jedna chyba.

Možná jsme neměli před očima běžný návrat funkce na NULL. Možná jsme neměli před očima přenos při odvíjení — fakticky obnovu registrů ve stylu setcontext — kdy cílový ukazatel instrukce před předáním řízení získal hodnotu NULL. Jinými slovy šlo o nesprávná data z unwind knihovny, ne o nesprávný slot návratové adresy na zásobníku.

Tím se problém výrazně zúžil. Buď GNU libunwind počítal špatný cílový stav, nebo počítal správný stav a něco ho poškodilo, než se mohl použít.

Přečetli jsme zdrojový kód GNU libunwind a zjistili, že na zásobníku syntetizuje ucontext_t, vyplní požadovaný stav registrů pro rámec úklidového handleru a pak předá ukazatel na tuto strukturu interní assemblerové rutině: _Ux86_64_setcontext.

A to už jsme měli všechny dílky skládačky.

Syntetizovaný ucontext_t žije v jednom z rámců zásobníku, které _Ux86_64_setcontext během svého běhu unwinduje. Četl _Ux86_64_setcontext ze struktury poté, co změnil %rsp, tedy ve chvíli, kdy už struktura nebyla součástí aktivního zásobníku? To by ji vystavilo přepsání při doručení signálu, například našeho častého SIGUSR2.

Chyba č. 2: chyba v libunwind

Odpověď zněla ano.

Tady je posledních šest instrukcí _Ux86_64_setcontext ve verzi GNU libunwind, kterou jsme používali; skládají se hlavně z instrukcí mov, které načítají z paměti do cílového registru:

Prostý text

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 ukazuje na ucontext_t alokovaný na zásobníku a makra UC_MCONTEXT_* se jen rozbalí na pevný offset, kde je konkrétní registr uložen.)

První instrukce je začátek race window. Aktualizuje %rsp, aby ukazoval na nové dno aktivního zásobníku. Jakmile k tomu dojde, struktura, na kterou ukazuje %rdi, už není součástí aktivního zásobníku (ani red zone) a pro kernel už není nedotknutelná.

Obvykle to potíže nezpůsobí, ale pokud signál dorazí přesně ve správný (špatný?) okamžik, kernel vytvoří rámec signálu na %rsp-128. Tím může přepsat paměť, na kterou ukazuje %rdi.

Pokud se to stane dřív, než další instrukce přečte UC_MCONTEXT_GREGS_RIP(%rdi), obnovený ukazatel instrukce se může poškodit. Při našich crashech se změnil na NULL.

To je právě ta chyba.

Proč se core dumpy maskovaly jako obyčejné špatné návraty

Tato sestava také vysvětluje jedno z pozorování, které nás mátlo: proč měla funkce X ve slotu návratové adresy předchozího rámce zásobníku hodnotu NULL.

setcontext byl napsán tak, aby obnovil všechny registry včetně %rdi, takže tento registr nemůže v posledním okamžiku před předáním řízení použít ke čtení UC_MCONTEXT_GREGS_RIP(%rdi). Místo toho hodnotu přečte dřív, uloží ji na zásobník, obnoví několik dalších registrů a potom pomocí retq uloženou hodnotu přečte a předá řízení.

To, co v core dumpech vypadalo jako „funkce se vrátila na NULL“, bylo ve skutečnosti „unwinder na zásobníku syntetizoval cílovou návratovou adresu, ale ta se před dokončením předání poškodila“. Předpokládali jsme, že slot návratové adresy se musel poškodit na místě, protože jsme neznali žádná místa, kde by se (poškoditelná) data do slotu návratové adresy psala záměrně.

Race window o jediné instrukci

Tato chyba působí absurdně kvůli tomu, jak úzké je její race window. U tohoto typu race condition musí vnější událost (signál) nastat mezi dvěma kroky provedenými jiným vláknem. Čím blíž jsou tyto kroky u sebe, tím méně pravděpodobné je, že race condition nastane.

V tomto případě je okno zranitelnosti široké doslova jednu instrukci! Signál musí být doručen po změně %rsp, ale předtím, než další instrukce načte %rip. Moderní superskalární out-of-order CPU zvládne několik takových jednoduchých instrukcí za cyklus, takže race window má řádově sto pikosekund.

Když jsme tuto race condition našli, první reakce byla, že musí být příliš vzácná na to, aby vysvětlila pozorovanou četnost crashů. V celé flotile jsme viděli více než tucet crashů s návratem na NULL denně. Mohla to opravdu způsobovat race condition o jediné instrukci během úklidu po výjimce?

Použili jsme Fermiho odhad. Pokud je okno zranitelnosti řádově 101010^{-10} sekund a SIGUSR2 přichází každých 10210^{-2} sekund CPU času, má každý handler úklidu po výjimce nebo blok catch zhruba 10810^{-8} pravděpodobnost, že prohraje závod.

Rockset používá výjimky jako součást interního mechanismu backpressure při ingestu. Jeden přetížený hostitel může vyhazovat řádově 10410^{4} výjimek za sekundu. To znamená střední dobu mezi selháními hostitele používajícího backpressure 10410^{4} sekund, tedy jeden crash za několik hodin. V měřítku celé flotily to bohatě stačí k vysvětlení pozorované četnosti crashů.

Proč se chyba v libunwind objevila právě teď?

Chyba v GNU libunwind je stará — přes 18 let, přítomná už v první verzi pro x86_64, která podporovala unwinding výjimek C++.

Proč se tedy projevila až teď?

Četnost crashů je zhruba úměrná tomu, kolik výjimek se vyhodí a kolik signálů se doručí. Závisí také na tom, kolik zásobníku signal handler spotřebuje.

Rockset je neobvyklý ve všech třech směrech. Výjimky vyhazujeme vysokou rychlostí jako součást běžné regulace přetížení; kvůli coarse_thread_cputime_clock doručujeme SIGUSR2 neobvykle často; a letos jsme handler SIGUSR2 přidáním volání timer_getoverrun přiměli používat víc zásobníku, abychom zvládli sloučené signály.

Zdá se, že poslední změna byla důležitá. Pokud handler používá dost málo zásobníku, nemusí dosáhnout na zastaralou paměť ucontext_t a přepsat ji. Před touto změnou tyto crashe vůbec nepozorujeme. Po změně zůstala četnost nízká, dokud jsme u některých případů použití nezvýšili zátěž a tím nezatížili mechanismus backpressure.

Jinými slovy, chyba v libunwind tam byla vždy, ale součin naší četnosti výjimek, četnosti signálů a využití zásobníku handlerem teprve nedávno překročil práh, kdy začala být provozně viditelná.

Tento mechanismus také vysvětluje shodu, že hardwarová chyba i chyba v libunwind nastávaly většinou uvnitř DocumentTree::updateDocument. Crashe z libunwind byly k této metodě silně náchylné, protože je vždy aktivní ve chvíli, kdy vyhazujeme výjimku pro uplatnění ingest backpressure. Silně k jejímu výběru docházelo i u crashů s vychýleným %rsp, protože špatný hardwarový uzel měl SKU používané pro hromadný ingest, který v této metodě tráví většinu CPU času.

Naší okamžitou mitigací byl přechod z GNU libunwind na unwinder z libgcc. To byl samo o sobě dobrý kompromis: implementace v libgcc těžila z velkého množství práce ke snížení lock contention, což je důležité při škálování na velké VM.

Do GNU libunwind jsme také upstreamovali samostatný reproducer a opravu(otevře se v novém okně) a ověřili, že ostatní unwindery podobný problém nemají.

Jak účinná je diagnóza na úrovni populace

Tento proce ladění nás toho hodně naučil o detailech dynamického linkování, unwind metadat DWARF, doručování signálů v Linuxu, System V ABI a mechanismu výjimek v C++. Hlavní poučení ale bylo mnohem prostší.

Nejdůležitějším krokem nebylo chytré čtení sestav ani hluboká znalost detailů. Bylo to vytvoření kvalitní datové sady. Bez ní jsme spojovali dva odlišné jevy do jednoho příběhu a snažili se z toho zmatku vyvodit správný závěr. Jakmile jsme měli přesná a úplná data o výskytu, struktura problému začala být zřejmá: jedna skupina crashů patřila špatnému hostiteli a druhá race condition v libunwind. Jakmile se zlepšila data, zjednodušilo se i ladění.

U infrastrukturních systémů, jako je Rockset, na tom hodně záleží. Toto vyšetřování posílilo náš závazek k hluboké instrumentaci, automatizovaným vyšetřováním a průběžnému zlepšování provozních nástrojů. Spolehlivost není jen o opravování chyb poté, co nastanou — je o budování dat, workflow a dovedností, které dělají z neřešitelných problémů problémy diagnostikovatelné a řešitelné.

Autoři

By Nathan Bronson, Member of Technical Staff