Core dump epidemiyolojisi: 18 yıllık bir hatayı düzeltmek
Veri altyapımızdaki zorlu çökmeleri ayıklamak için popülasyon düzeyinde analiz kullanmak.
OpenAI’ın modelleri ve ajanları, çıkarım anında, yani modeller sorunuz üzerinde düşünürken ilgili verileri aramak için giderek daha fazla ölçeklenebilir veri altyapısına dayanıyor. Bu hizmetlerden bazıları C++ ile yazılmıştır; C++’ın sistem üzerindeki düşük seviyeli denetimi performansı en üst düzeye çıkarıp bellek kullanımını azaltmamızı sağlar. Ölçeklenirken bu verimlilik önemli, ancak C++’ta bellek güvenliği olmaması, hataların yanlış ya da var olmayan bellek adreslerine yazarak çökmelere yol açabileceği anlamına gelir.
Birkaç ay önce, ChatGPT veri altyapımızın özel bir parçası olan ve birçok veri eklentisi ile konuşmalarda arama için kritik öneme sahip Rockset hizmetinin içinden kaynaklanan bazı çökmeler gözlemledik. Bu çökmelerin her birinde normal bir C++ işlevi bitmiş, sonra sahte bir adrese dönmüş gibi görünüyordu; bu da komut işaretçisi artık koda işaret etmediği için çekirdeğin programı durdurmasına neden oluyordu. Bazen yığın çerçevesindeki dönüş adresi yuvası NULL oluyordu. Bazen de yığın işaretçisi CPU yazmacının kendisi 8 bayt kaymış görünüyordu; sanki %rsp normal yürütmenin ortasında bir şekilde azaltılmıştı. Her iki durumda da çökme dönüş sırasında gerçekleşiyordu.
Bunlar uygulama kodu için normal hata biçimleri değildir. Yalnızca kaydedilmiş bir dönüş adresine denk gelen başıboş bir yazma mümkündür, ama son derece düşük olasılıklıdır. Satır içi assembly, setcontext veya longjmp (hiçbirini kullanmıyoruz) olmadan %rsp’yi 8 bayt hizasız yapan bir hata daha da tuhaftır; çünkü derlenmiş kod bu yazmacı doğrudan yalnızca işlev prologu ve epilogunda ayarlar. Bizim (veya ChatGPT’nin) aklına gelen her hipoteze karşı güçlü kanıtlar vardı; bu yüzden hata imkânsız görünüyordu.
Başta tek bir sorun sandığımız şeyin sonunda, tesadüfen aynı anda keşfedilmiş alakasız iki hata olduğu ortaya çıktı. Birincisi, bir Azure ana makinesinde sessiz donanım bozulmasıydı; CPU matematiği doğru yapmıyordu. İkincisi, yaygın kullanılan bir açık kaynak kitaplığı olan GNU libunwind’de 18 yıllık, fark edilmemiş bir yarış koşuluydu.
Bu yazı, epidemiyolog gibi düşünüp tüm çökme popülasyonuna ilişkin kaliteli bir veri kümesi oluşturarak görünürde açıklanamaz çökmeleri nasıl belirleyip düzelttiğimizin hikâyesidir.
Önce Rockset’i biraz daha derinlemesine ele alalım. Rockset, OpenAI’da eşitleme bağlayıcıları gibi birçok dahili kullanım için yararlandığımız, arama ve gerçek zamanlı analizlere yönelik bulut yerel bir veri sistemidir (Rockset, 2024’te OpenAI tarafından satın alındı). ChatGPT’nin soruları yanıtlarken veya eylem gerçekleştirirken ilgili bilgileri arayabilmesi için, bir çalışma alanının bilgi tabanının güncel dizinini korumada akış güncellemeleri kullanılır.
Rockset’in yürütme katmanı C++ ile yazılmıştır. C++ dili CPU’ya düşük seviyeli erişim sağlar; bu performans ve verimlilik için iyidir, ancak uygulama hatalarının geçersiz bellek erişimlerine ve segfault’lara yol açabileceği anlamına gelir. Bunları izlemek için çökme olduğunda yığın izini günlüğe yazan folly’nin ölümcül sinyal işleyicisini kullanıyor ve ilgili core dump’ları (programın çöktüğü andaki durumunun anlık görüntüsü) daha sonra analiz için Azure blob depolamaya yüklüyoruz. Rockset’in tüm sorgu işleme yaprakları kopyalanır; bu da bir çökmenin istemciye etkisini en aza indirir. Ancak her segfault, güvenilirlik ve kalite hedeflerimize ulaşmak için düzeltilmesi gereken bir hataya karşılık gelir.
İlk yaklaşımımız bu core’ları geleneksel bir hata ayıklama sorunu gibi ele almaktı: birkaç core dump’ı çok yakından incelemek, hipotezler kurmak ve bunları tek tek elemek.
Çökmelerin çoğu DocumentTree::updateDocument adlı bir yöntemde gerçekleşti. Bu çökmelerde updateDocument bilinmeyen bir X işlevini çağırmış, X etkinken yığın bozulmuş, ardından X yürütülebilir kod olmayan bir adrese dönmüş gibi görünüyordu. Bazı durumlarda X’in yeni çıkarılmış çerçevesi, kaydedilmiş dönüş adresinin NULL olması dışında geçerli görünüyordu. Diğer durumlarda yığın işaretçisinin kendisi yanlış görünüyordu, ancak bir sonraki geçerli çerçeve yine updateDocument gibi duruyordu.
Yığının ne zaman bozulduğunu bilmiyorduk; bu da çok büyük bir arama alanı bırakıyordu. updateDocument, yoğun satır içileştirmeye uğrayan büyük bir yöntemdir; bu yüzden X için aday sayısı eziciydi.
Bu bizim C++ kodumuzdaki bir hata mıydı? Bir derleyici veya bağlama sorunu mu? Çalışma zamanı kitaplıklarımızdan birinde bir sorun mu? Sinyal teslimi veya bağlam değiştirme etrafında bir Linux çekirdeği hatası mı? Daha da nadir bir şey mi? Bu başıboş bir yazmaysa, ASAN hazırlık ortamımız neden yakalamamıştı?
Sorunun tüm örneklerini belirlemek için uygulama düzeyi günlüklerimizi kullanmayı denedik; ancak yığın bozulması hatalarını yalnızca günlüklerden sınıflandırmak zordur, çünkü günlüğe yazılan yığın izleri de bozuk ya da eksiktir. Hem yanlış pozitif hem de yanlış negatif üretmeyen bir günlük sorgusu kuramadık. Daha fazla core’u elle inceledik ve bazı ek örnekler bulduk, ancak bu süreç güvenilir bir veri kümesi sağlayamayacak kadar emek yoğundu.
Soruşturmanın bu aşamasında, çökmeleri birden çok bölgede ve birden çok donanım türünde gördüğümüz için bir donanım hatasını (yanlış biçimde) eledik; bu yüzden hâlâ yalnızca yazılım kaynaklı nedenleri arıyorduk. Birkaç gün boyunca tek bir hizasız %rsp çökmesine çok derinlemesine daldık; yığın ve yazmaç içeriklerini kullanarak çökme öncesi geçmişi yeniden kurduk. Bu bazı olası ipuçları üretti, ancak tüm hataların aynı nedenden kaynaklandığı yönündeki ilk sonucumuzu bırakmadığımız için bizi açmaza saplanmaktan kurtarmadı.
Soruşturmanın dönüm noktasına gelmeden önce, core dosyalarından ne tür bilgiler çıkardığımızı açıklamak önemli.
Rockset -fno-omit-frame-pointer ile derlenir; bu yüzden etkin yığın çerçevesine her zaman %rbp üzerinden ulaşılır ve çağıranlar çerçeve işaretçilerinden oluşan bağlı bir liste oluşturur.
Linux x86_64 üzerinde AMD64 System V ABI, %rsp’nin altındaki 128 baytı red zone olarak ayırır. Bu bölge kullanıcı alanı kodunun kullanımına açıktır ve önemlisi çekirdek, ABI sözleşmesinin parçası olarak sinyal teslim ederken burayı ezmemeyi vaat eder.
Red zone, dönüş sonrası bir çökmeyi ayıklamamızda merkezîydi; çünkü dönüşten önceki bazı bilgileri korur. Bir SIGSEGV tetiklendiğinde, folly’nin ölümcül sinyal işleyicisi çöken iş parçacığının yığınında çalışır. Artık etkin olmayan yığın çerçeveleri (işlevleri döndüğü için) son 128 bayt dışında sinyal işleyicisi tarafından ezilir. Bu yüzden “X’in yeni çıkarılmış yığın çerçevesi, NULL dönüş adresi dışında geçerli görünüyordu” gibi şeyler söyleyebiliyoruz. Red zone etkin olmayan çerçevelerin bir kısmını, bazen de yalnızca bir çerçevenin kuyruğunu korur.
İlgili tüm işlevlerin çok küçük olduğu bir hizasız yığın çökmesi bulduk. Bu sayede %rsp’nin görece basit bir işlevin yürütülmesi sırasında hizasız hâle geldiğini ve sonrasında başka çağrıların başarılı olduğunu görebildik. Program ancak etkin işlev sonunda dönmeye çalıştığında çöktü. Bu kod yollarının hiçbiri istisna, satır içi assembly, setcontext veya longjmp kullanmıyordu; dolayısıyla yığın işaretçisi gerçekten core’un gösterdiği gibi değiştiyse, kullanıcı alanı kodundaki makul hiçbir hata bunu açıklamıyordu.
Bu bizi çekirdeğe yöneltti.
Rockset sinyalleri çoğu programdan daha agresif kullanır. Sorgu yürütme, veri alışverişi yapan çok sayıda hafif göreve bölünür. Bu, yüksek QPS iş yüklerini verimli işlemek için önemlidir; ancak birçok sorgunun işi aynı iş parçacığı havuzuna çoklandığından sorgu başına CPU muhasebesini zorlaştırır.
Çözümümüz coarse_thread_cputime_clock adını verdiğimiz mekanizmadır; clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) çağrısını her görev sınırında örnekleme yapılabilecek kadar düşük ek yükle yaklaşık olarak hesaplar. timer_create API’si, CPU zamanının birikimi dâhil zamanın geçişine dair çeşitli kavramlara göre periyodik sinyal teslimi planlamak için kullanılabilir. Her birkaç milisaniyelik CPU zamanında bir sinyal (SIGUSR2) teslim edilecek şekilde planlıyoruz; o anda sinyal işleyicisi iş parçacığına yerel bir değeri güncelliyor. Birçok görev çalışırken kaba saatin ilerlediğini görmese de tüm deltaların toplamı, bir sorgunun gerçek CPU zamanı için yanlılıksız bir tahmin verir.
Sinyalleri bu kadar sık teslim ettiğimiz için, bağlam değiştirme veya sinyal teslimi çevresinde nadir bir çekirdek hatası makul görünüyordu. Hata raporlarını, çekirdek kaynak kodunu ve Azure’a özgü çekirdek yamalarını okumaya zaman harcadık. Stres testleri denedik. İlgili görünen hiçbir şey bulamadık.
O noktada geri çekilip farklı bir yaklaşım denemeye karar verdik.
Böyle bir sorunu ayıklamanın iki genel yolu vardır.
Biri bir tür doktor gibi davranmaktır: tek bir hastaya odaklanmak, çok sayıda test yapmak ve ayrıntılı kanıtlardan tek bir vakayı teşhis etmeye çalışmak.
Diğeri ise daha çok epidemiyolog gibi davranmaktır: tüm popülasyona bakmak ve tek bir vakanın gösteremeyeceği örüntüler olup olmadığını sormak. Hata belirli bir sürümde mi başladı? Tek bir donanım SKU’su (belirli CPU ve sunucu modeli), tek bir bölge veya tek bir çekirdek sürümüyle ilişkili mi? Tek bir sendrom gibi görünen şeyin içinde birden çok ayrı küme saklı mı?
Biz çoğunlukla doktor modundaydık. Kilit değişim, kaliteli popülasyon verisi toplamamız gerektiğine karar vermemizdi.
Sorunun tüm örneklerini otomatik bulmaya yönelik önceki denemelerimiz, günlükler üzerinde metin araması yapmaya çalıştığımız için başarısız oldu. Core dump’ların kendisi çok daha fazla bilgi içeriyor, ancak bunlara elle bakmak ölçeklenmiyordu. Core dump’ları otomatik analiz edebilecek bir işlem hattı kurmak için emek harcamaya karar verdik.
ChatGPT’ye her core dosyasının bir önekini indiren, yazmaçları çıkaran, günlükleri kullanarak bilinen yanlış pozitifleri filtreleyen ve çökmeyi otomatik olarak return-to-null, misaligned-stack veya other diye etiketleyen bir betik yazdırdık. Sonra bu betiği, önceki yıldaki her üretim Rockset core dump’ı üzerinde paralel çalıştırdık.
Dönüm noktası buydu.
Temiz bir veri kümemiz olur olmaz korelasyonlar hemen belirdi. Tek bir tuhaf hata sandığımız şey aslında iki ayrı çökme popülasyonuydu.
return-to-null core’ları birçok kümeye ve coğrafi bölgeye yayılmıştı. Sıklıkları yakın zamanda artmıştı, ancak net bir başlangıç tarihi veya temiz bir altyapı sınırı yoktu.
misaligned-stack çökmeleri ise tamamen farklı görünüyordu. Hepsi tek bir bölgeden geliyordu, net bir başlangıç tarihleri vardı ve uzun süredir çalışan düğümlerde hiç gerçekleşmiyordu. Birden fazla Azure VM’si (bulutta barındırılan sanal makineler) söz konusu olsa da örüntü, üzerine hangi VM denk gelirse ona sorun çıkaran kötü donanımlı tek bir fiziksel makineye benziyordu.
İki hatayı zihnimizde birbirine karıştırdığımızı o anda anladık. Her iki hatadan gelen karşı örnekleri karıştırdığımız için tek ve tutarlı bir açıklama bulamıyorduk.
Temiz bir Kubernetes düğümleri ve zaman damgaları listesiyle, hizasız yığın çökmelerini tek bir fiziksel ana makineye kadar izleyebildik; bunu engelleme listesine almak kolaydı.
Birkaç haftalık stres testinden sonra bile o ana makinedeki yazmaç bozulmasını kontrollü bir ortamda yeniden üretemedik. Ancak sorunlu ana makine hizmet dışına alınınca hizasız yığın çökmeleri ortadan kalktı.
Kötü ana makineyi kaldırmak, aynı sorunun yeniden yaşanmasını önlemediği için kalıcı bir çözüm değildir. Ancak yazılımı, benzer bir sorun tekrarlarsa kolayca saptanıp ele alınacak şekilde değiştirebiliriz. Yinelemeyi yalnızca günlüklerden saptayabilmek için (core dump gerekmeden) ölümcül sinyal işleyicimizi yazmaç durumunu içerecek şekilde iyileştirdik. Denetim düzlemini, VM’lerin genellikle geri dönüştürülmek yerine yeniden kullanılacağı şekilde değiştirdik; bu, altyapı yığınımızın bizim düzeyimizde kötü düğüm tespitini çok kolaylaştırıyor. Runbook’larımızı (ve ekibimizin zihinsel modellerini) bu olasılığı içerecek şekilde güncelledik.
Kötü ana makine çökmeleri ayrılınca, kalan return-to-null core’ları üzerinde akıl yürütmek çok daha kolaylaştı. Daha önce istisna unwinding’ini elemiştik; çünkü karşı örneklerimiz olduğunu düşünüyorduk: istisnaların kesinlikle kullanılmadığı kod yollarındaki çökmeler. Ama bu karşı örneklerin hepsi donanım bozulması kümesindendi.
Kalan core’lara bunu akılda tutarak yeniden baktığımızda, bu sonucun tam ters yönde olduğunu gördük: çökmelerin hepsi istisna unwinding’i sırasında oluyordu.
C++ bir istisna fırlattığında, çalışma zamanı hangi catch bloğunun bunu alacağını ve yol boyunca hangi yıkıcıların veya temizleme işleyicilerinin çalışacağını bulmak zorundadır. Derleyici bu meta veriyi üretir, ancak asıl eşleştirme çalışma zamanında dinamik olarak gerçekleşir.
İstisna unwinding’i, throw çağıran işlev tarafından değil, ortaya çıkan derlenmiş kodun çağırdığı yardımcı işlevler tarafından yapılır. Bu çalışma zamanı yordamları yığını inceler, yığında bulunan işlevlere ilişkin meta verileri getirir, temizleme işleyicilerini ve catch bloklarını dinamik olarak arar ve sonra denetimi bu konumlardan birine aktarır. Denetimi aktarmak, aradaki tüm yığın çerçevelerinin (yardımcı işlevlerinkiler dâhil) unwind edilmesini içerir.
Operasyonel olarak bu, normal bir çağrı ve dönüşten çok longjmp veya fiber geçişine yakındır. Callee-save yazmaçları ile yığın çerçevesi yazmaçları %rbp ve %rsp geri yüklenmelidir.
İkili dosyamız, C++ istisna unwinding’i yapan işlevlerin uygulamalarını içeren iki kitaplığa bağlanır: libgcc ve GNU libunwind. Dinamik bağlayıcının seçtiği tanımlar GNU libunwind’inkilerdi. Bu bizi şaşırttı; sembol sürümleme kuralları nedeniyle libgcc uygulamasının kazanmasını bekliyorduk. Ancak çalışan ikilileri incelediğimizde durumun böyle olmadığını gördük.
Bu noktada, tek bir hata olduğunu düşündüğümüzde yaptığımız başka bir varsayımı gevşetince çalışma hipotezimiz değişti.
Belki de sıradan bir işlevin NULL’a döndüğünü görmüyorduk. Belki denetim aktarılmadan önce hedef komut işaretçisinin NULL hâline geldiği bir unwind aktarımı—fiilen setcontext tarzı yazmaç geri yüklemesi—görüyorduk. Başka deyişle, yığındaki yanlış bir dönüş adresi yuvası değil, unwind kitaplığından gelen yanlış veri.
Bu, sorunu dramatik biçimde daralttı. Ya GNU libunwind yanlış hedef durumunu hesaplıyordu ya da doğru durumu hesaplıyor ve bir şey uygulanmadan önce onu bozuyordu.
GNU libunwind kaynağını okuduk ve yığın üzerinde bir ucontext_t sentezlediğini, temizleme işleyicisinin çerçevesi için istenen yazmaç durumunu doldurduğunu, sonra bu struct’a bir işaretçiyi dahili bir assembly yordamına verdiğini gördük: _Ux86_64_setcontext.
Bu noktada tüm parçalar elimizdeydi.
Sentezlenen ucontext_t, _Ux86_64_setcontext tarafından o işlevin yürütülmesi sırasında unwind edilen yığın çerçevelerinden birinde yaşar. _Ux86_64_setcontext, %rsp’yi değiştirdikten sonra struct’tan okuma yapıyor olabilir miydi; yani struct’ın artık etkin yığının parçası olmadığı noktada? Bu, onu sık gelen SIGUSR2 gibi bir sinyal teslimiyle ezilmeye açık hâle getirirdi.
Yanıt evetti.
Kullandığımız GNU libunwind sürümünde _Ux86_64_setcontext’in son altı komutu aşağıdadır; bunlar çoğunlukla bellekten hedef yazmaca yükleme yapan mov komutlarından oluşur:
(%rdi, yığında ayrılmış ucontext_t’ye işaret eder; UC_MCONTEXT_* makroları ise yalnızca belirli bir yazmacın saklandığı sabit ofsete genişler.)
İlk komut yarış penceresinin başlangıcıdır. %rsp’yi etkin yığının yeni tabanına işaret edecek şekilde günceller. Bu olur olmaz %rdi’nin işaret ettiği struct artık etkin yığının (veya red zone’un) parçası değildir ve çekirdek için artık dokunulmaz değildir.
Genellikle bu sorun çıkarmaz; ancak bir sinyal tam doğru (yanlış?) anda gelirse çekirdek sinyal çerçevesini %rsp-128 konumunda oluşturur. Bu, %rdi’nin işaret ettiği belleğin üzerine yazabilir.
Bu, bir sonraki komut UC_MCONTEXT_GREGS_RIP(%rdi) okumadan önce olursa, geri yüklenen komut işaretçisi bozulabilir. Bizim çökmelerimizde NULL hâline geldi.
Hata buydu.
Bu assembly, bizi şaşırtan gözlemlerden birini de açıklıyor: X işlevinin önceki yığın çerçevesinin dönüş adresi yuvasında neden NULL vardı.
setcontext, %rdi dâhil tüm yazmaçları geri yüklemek üzere yazılmıştı; bu yüzden denetim aktarımının son anında UC_MCONTEXT_GREGS_RIP(%rdi) okumak için o yazmacı kullanamaz. Bunun yerine değeri daha önce okur, yığına kaydeder, birkaç yazmacı daha geri yükler ve ardından kaydedilmiş değeri okuyup denetimi aktarmak için retq kullanır.
Core’larda “bir işlev NULL’a döndü” gibi görünen şey aslında “unwinder yığında bir hedef dönüş adresi sentezledi, ama aktarım tamamlanmadan önce o hedef bozuldu” idi. Dönüş adresi yuvasının bozulmasının yerinde gerçekleşmesi gerektiğini varsaymıştık; çünkü (bozulabilir) verinin dönüş adresi yuvasına bilerek yazıldığı bir yer bilmiyorduk.
Bu hatayı absürt gösteren şey, yarış penceresinin ne kadar dar olmasıdır. Bu tür bir yarış koşulunda dış olayın (sinyalin), başka bir iş parçacığının attığı iki adımın arasında gerçekleşmesi gerekir. Bu adımlar birbirine ne kadar yakınsa yarış koşulunun gerçekleşme olasılığı o kadar düşer.
Bu vakada savunmasız pencere kelimenin tam anlamıyla tek komut genişliğinde! Bir sinyalin, %rsp değiştirildikten sonra ama bir sonraki komut %rip’yi yüklemeden önce teslim edilmesi gerekir. Modern süperskaler, sıra dışı yürütmeli bir CPU’da böyle birkaç basit komut çevrim başına çalıştırılabilir; bu yüzden yarış penceresi kabaca yüz pikosaniyedir.
Bu yarışı bulduğumuzda ilk tepkimiz, gözlenen çökme oranını açıklayamayacak kadar nadir olması gerektiğiydi. Filo genelinde günde bir düzineden fazla return-to-null çökmesi görüyorduk. İstisna temizliği sırasında tek komutluk bir yarış bunu gerçekten açıklayabilir miydi?
Fermi tahminine başvurduk. Savunmasız pencere saniye mertebesindeyse ve SIGUSR2 her saniyelik CPU zamanında geliyorsa, her istisna temizleme işleyicisi veya catch bloğunun yarışı kaybetme olasılığı yaklaşık olur.
Rockset, dahili alma geri basıncı mekanizmasının parçası olarak istisnaları kullanır. Tek bir aşırı yüklü ana makine saniyede mertebesinde istisna fırlatabilir. Bu, geri basınç kullanan bir ana makinede arızalar arası ortalama sürenin saniye, yani birkaç saatte bir çökme olduğu anlamına gelir. Filo ölçeğinde bu, gözlenen çökme sıklığını açıklamaya fazlasıyla yeter.
GNU libunwind hatası eskidir—18 yıldan da eski; C++ istisna unwinding’i destekleyen ilk x86_64 sürümünde mevcuttu.
Peki neden şimdi ortaya çıktı?
Çökme oranı kabaca kaç istisna fırlatıldığı ve kaç sinyal teslim edildiğiyle orantılıdır. Ayrıca sinyal işleyicisinin ne kadar yığın tükettiğine de bağlıdır.
Rockset bu üç eksenin hepsinde sıra dışıdır. Normal aşırı yük denetiminin parçası olarak yüksek oranlarda istisna fırlatıyoruz; coarse_thread_cputime_clock nedeniyle SIGUSR2’yi alışılmadık sıklıkta teslim ediyoruz; ayrıca bu yılın başında birleşen sinyalleri hesaba katabilmek için timer_getoverrun çağrısı ekleyerek SIGUSR2 işleyicisinin daha fazla yığın kullanmasına yol açtık.
Bu son değişiklik önemli görünüyordu. İşleyici yeterince az yığın kullanıyorsa eski ucontext_t belleğine ulaşıp onu ezmeyebilir. Bu değişiklikten önce bu çökmeleri hiç gözlemlemiyoruz. Değişiklikten sonra oran, geri basınç mekanizmasını zorlayan bazı kullanım alanları için yükü artırana kadar düşük kaldı.
Başka deyişle libunwind hatası hep oradaydı; ancak istisna oranımız, sinyal oranımız ve işleyici yığın kullanımımızın çarpımı ancak yakın zamanda operasyonel olarak görünür olduğu eşiği aştı.
Bu mekanizma, hem donanım hatasının hem de libunwind hatasının çoğunlukla DocumentTree::updateDocument içinde çökme yaratması tesadüfünü de açıklıyor. libunwind kaynaklı çökmeler bu yönteme güçlü biçimde yatkındı; çünkü alma geri basıncı uygulamak için istisna fırlattığımız noktada bu yöntem her zaman etkindir. %rsp hizasızlığı çökmeleri için de güçlü biçimde seçiliyordu; çünkü kötü donanım düğümü, toplu alma için kullandığımız ve CPU zamanının çoğunu bu yöntemde geçiren bir SKU’ya aitti.
İlk hafifletmemiz GNU libunwind’den libgcc’nin unwinder’ına geçmek oldu. Bu kendi başına iyi bir ödünleşimdi: libgcc uygulaması, büyük VM’lere ölçeklenirken önemli olan kilit çekişmesini azaltmaya yönelik çok çalışmadan yararlandı.
Ayrıca kendi kendine yeterli bir yeniden üretici ve GNU libunwind için bir düzeltme(yeni bir pencerede açılır) upstream’e gönderdik ve diğer unwinder’larda benzer bir sorun olmadığını doğruladık.
Bu hata ayıklama yolculuğu bize dinamik bağlama, DWARF unwind meta verileri, Linux sinyal teslimi, System V ABI ve C++ istisna mekanizmasının ayrıntıları hakkında çok şey öğretti. Ama ana ders bunların hepsinden daha basitti.
En önemli adım, zekice assembly okumak veya ayrıntılara dair derin bilgi değildi. Kaliteli bir veri kümesi oluşturmaktı. Bu veri kümesi yokken iki ayrı olguyu tek bir hikâyede karıştırıyor ve akıl yürüterek bu karışıklıktan çıkmaya çalışıyorduk. Doğru ve eksiksiz popülasyon verisine sahip olduğumuzda sorunun yapısı açıkça görüldü: bir çökme popülasyonu kötü bir ana makineye, diğeri libunwind’deki bir yarışa aitti. Veri iyileşince hata ayıklama da kolaylaştı.
Rockset gibi altyapı sistemleri için bu çok önemlidir. Bu soruşturma, derin enstrümantasyona, otomatik incelemelere ve operasyonel araçlarımızda sürekli iyileştirmelere bağlılığımızı güçlendirdi. Güvenilirlik yalnızca hataları ortaya çıktıktan sonra düzeltmek değildir; imkânsız görünen sorunları teşhis edilebilir ve çözülebilir hâle getiren veriyi, iş akışlarını ve becerileri oluşturmaktır.
Yazarlar
By Nathan Bronson, Member of Technical Staff


