Dayanıklılık, sınanmadıkça bir temennidir

Bir gömülü sistemin "dayanıklı" olduğunu söylemek tek başına ölçülebilir bir iddia değildir. Bu cümle ancak arızanın adı konduğunda, kabul kriteri yazıldığında ve aynı arıza tekrar üretilebildiğinde anlam kazanır.

Sahada görülen arızaların büyük kısmı da egzotik değildir: fiş çekilir, bir konnektör gevşer, karşı uç bozuk bir çerçeve yollar, disk dolar, saat ileri atlar. Bunların hepsi masada kasten üretilebilir. Hata enjeksiyonu, bu üretimi rastgele bir kurcalama seansı olmaktan çıkarıp tekrarlanabilir bir doğrulama faaliyetine çeviren disiplindir.

Aşağıdaki metin bir sonuç tablosu değil, bir yöntem tarifidir: senaryo sınıfları, her sınıfın kabul kriteri, kampanyanın tekrarlanabilir hâle nasıl getirileceği ve kaydın arızadan nasıl sağ çıkarılacağı.

Kabul kriteri koşudan önce yazılır

Hata enjeksiyonunda en sık yapılan hata, önce kesip sonra "ne oldu" diye bakmaktır. Görülen şey sonradan gerekçelendirilir: cihaz yeniden başladı, demek ki dayanıklı. Oysa yeniden başlamak bazı sistemler için kabul, bazıları için ret sebebidir.

Bu yüzden her senaryo, enjeksiyon başlamadan önce üç satırla tanımlanmalıdır:

  • Tetik: arıza tam olarak neye göre, nerede uygulanır.
  • Gözlem noktası: sonuç sistemin hangi sınırından okunur.
  • Kabul: hangi davranış geçer, hangisi kalır — "çalıştı" değil, ölçülebilir bir ifadeyle.

Gözlem noktasının cihazın kendi içi olmaması önemlidir. Kendi diskine günlük yazan bir sistemde dolu disk senaryosunun kanıtı da yazılamaz; gücü kesilen bir cihazda son satır hiçbir zaman diske inmez.

Beş senaryo sınıfı

Senaryoları sınıflandırmanın pratik faydası şudur: her sınıf sistemin farklı bir katmanını hedefler ve farklı bir kabul kriteri gerektirir. Sınıfları birbirine karıştırmak, "test ettik" demenin en kolay ve en yanıltıcı yoludur.

Güç kesintisi

Hedef katman kalıcı depolama ve açılış zinciridir.

Kesinti yazılımdan uygulanamaz. reboot -f bir güç kesintisi değildir; blok katmanına ve sürücülere düzgün kapanma imkânı verir, yani sınamak istediğiniz yolu tam olarak atlar. Gereken şey bir röle kartı ya da uzaktan komut kabul eden bir güç kaynağıdır.

Kesintinin nereye denk geldiği sonucun tamamını belirler. Uygulama kritik bölüme girerken bir GPIO'yu kaldırsın; enjeksiyon bu kenarı tetikleyici alsın ve üzerine tohumu kayıt altına alınmış rastgele bir gecikme eklesin. Tohum kayıtlı değilse bulunan arıza bir daha üretilemez, düzeltmenin doğrulaması da mümkün olmaz.

Bu senaryoda sınanan mimari kararlar bellidir: kök dosya sistemi salt okunur mu (SquashFS üzerine tmpfs overlay), değişken veri atomik mi yazılıyor (geçici dosyaya yaz, fsync(), rename(), ardından dizin tanıtıcısında yine fsync()), güncelleme A/B yuva değiştiriyor mu, U-Boot tarafında bootcount / bootlimit / altbootcmd üçlüsü gerçekten geri dönüş yaptırıyor mu. Depolama tarafında JEDEC eMMC 5.1 (JESD84-B51) ile tanımlı Power Off Notification ve Reliable Write özelliklerinin BSP'de kullanılıp kullanılmadığı ayrı bir sorudur ve cevabı dosya sistemi seçiminden bağımsızdır.

Kabul kriteri şu biçimde yazılır: her çevrimde cihaz göreve hazır duruma açılır; yapılandırma dosyası ya eski ya yeni sürümdür, hiçbir çevrimde kısmi değildir; geri dönüş sayacı sonlu kalır, yuva değiştirme döngüye girmez.

Bus hatası

Hedef katman haberleşme sürücüsü ve onun üzerindeki durum makinesidir.

CAN bu sınıfın en öğretici örneğidir, çünkü hata davranışı standartla tanımlıdır. ISO 11898-1'e göre denetleyici, hata sayaçlarına bakarak error-active, error-passive ve bus-off durumları arasında geçer; bus-off, düğümün hattan kendini çekmesi demektir. ISO 11898-2 hattın iki ucunda sonlandırma öngörür — bir ucun sökülmesi tek başına geçerli bir enjeksiyon senaryosudur. Aynı şekilde CAN FD'de veri alanının azami 64 bayt olması (ISO 11898-1) sınır durumu üretmek için hazır bir tutamaktır.

Linux tarafında senaryo çoğunlukla SocketCAN üzerinden kurulur:

ip link set can0 type can bitrate 500000 restart-ms 0 berr-reporting on
ip link set can0 up
ip -details -statistics link show can0

restart-ms 0 bilinçli bir tercihtir: otomatik yeniden başlatma kapalıyken bus-off olayı uygulamaya kadar görünür kalır. restart-ms bir değere ayarlandığında sürücü sessizce toparlar ve uygulama hattın koptuğunu hiç öğrenmez. Hangi davranışın doğru olduğu ürüne bağlıdır; sınanması gereken de tam olarak bu karardır. Uygulama tarafında CAN_RAW soketine can_err_mask verilerek hata çerçevelerinin görülüp görülmediği ayrıca doğrulanır.

I2C'de karşılık gelen senaryo SCL'in düşük tutulmasıdır. Bir slave clock stretching sırasında kilitlenirse hat kilitlenir ve kurtarma, saat hattına darbe göndermekten geçer. Linux'ta bu, sürücünün i2c_bus_recovery_info yapısını doldurup doldurmadığı sorusudur — doldurmuyorsa senaryonun sonucu baştan bellidir.

Kabul kriteri: düğüm bus-off'a girdiğinde uygulama tanımlı bir güvenli duruma geçer ve olayı dışarıya bildirir; hat düzeldiğinde yeniden katılım tanımlı bir üst sınır içinde gerçekleşir; sayaçlar sonradan okunabilir kalır.

Bozuk paket

Hedef katman protokol ayrıştırıcısıdır. Buradaki arızalar en sinsileridir, çünkü sistem hiç çökmeden yanlış davranabilir.

Ağ tarafında netem bu işin standart aracıdır:

tc qdisc add dev eth0 root netem loss 5% corrupt 1% reorder 25% 50%
tc qdisc change dev eth0 root netem delay 20ms 5ms distribution normal
tc qdisc del dev eth0 root

Ancak netem yalnızca taşıma katmanını bozar. Asıl kazanç, uygulama protokolünün kendi çerçevesini kasten yalancı hâle getirmektir: uzunluk alanı gövdeden büyük olan, TLV zinciri tampon sınırını aşan, sağlama toplamı tutmayan, çerçeve ayracı gövdenin içinde geçen mesajlar. Bunlar Scapy ile ham çerçeve olarak elde üretilebilir; daha verimlisi, ayrıştırıcıyı doğrudan bir fuzzer'a bağlamaktır — süreç içi bir harness için libFuzzer ya da AFL++, ağ protokolleri için boofuzz.

Kabul kriteri: hiçbir girdi çökmeye, sınırsız bellek ayırmaya ya da sonsuz döngüye yol açmaz; hatalı mesaj sayacı artar; ayrıştırıcı bir sonraki geçerli ayraçta senkronu geri kazanır; gözcü (watchdog) devreye girmez. Son madde önemlidir: gözcünün kurtardığı bir çökme "geçti" değil, "gizlendi" demektir.

Dolu disk

Hedef katman uygulamanın yazma yolları ve hata denetimidir.

Senaryoyu kurmanın en temiz yolu bütün diski doldurmak değil, küçük bir alan kullanmaktır — değişken veri dizini, boyutu sınırlı bir tmpfs'e ya da ayrı ve küçük bir bölüme bağlanır. Böylece ENOSPC tekrarlanabilir biçimde ve kısa sürede üretilir.

Burada ext4'ün ayrılmış blok payı bir tuzaktır. tune2fs -m ile tanımlanan bu pay yalnızca ayrıcalıklı süreçlere açıktır; root olarak çalışan bir servis dolu diskte hâlâ yazabilirken, kendi kullanıcısıyla çalışan başka bir servis ENOSPC alır. Aynı cihazda iki farklı sonuç görürsünüz ve sebebini kodda ararsınız.

Günlük tarafında sınır konfigürasyonla verilir; /etc/systemd/journald.conf içinde:

[Journal]
Storage=volatile
RuntimeMaxUse=32M
SystemMaxUse=64M

Kabul kriteri: ENOSPC hiçbir koşulda kalıcı yapılandırmayı bozmaz; birincil işlev sürmeye devam eder; en eski günlük düşer, en yenisi değil; ve "disk doldu" olayının kendisi dışarıdan görülebilir. Göremediğiniz bir arıza sahada da görünmeyecektir.

Saat sıçraması

Hedef katman zamanlayıcılar, zaman aşımları ve zaman damgasına bağlı her karardır.

Enjeksiyon basittir; sonuçları basit değildir:

timedatectl set-ntp false
date -s "2035-01-01 00:00:00"
date -s "1970-01-01 00:00:10"

Klasik arıza, bekleme çağrılarının duvar saatine bağlanmasıdır. POSIX'te pthread_cond_timedwait() varsayılan olarak CLOCK_REALTIME kullanır; saat geri alındığında bekleyen iş parçacığı olması gerekenden çok daha uzun süre bekler. Doğrusu, koşul değişkeni oluşturulurken saati değiştirmektir:

pthread_condattr_t attr;
pthread_condattr_init(&attr);
pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
pthread_cond_init(&cond, &attr);

Aynı ayrım Linux'ta timerfd_create() için de geçerlidir. CLOCK_REALTIME üzerinde mutlak zamanlı kurulan bir zamanlayıcıya TFD_TIMER_CANCEL_ON_SET bayrağı verildiğinde, saat adım attığı anda bekleme iptal edilir — yani uygulamaya açıkça "senin zaman varsayımın bozuldu" denir. Süre ölçen her yol ise CLOCK_MONOTONIC üzerinde olmalıdır; askıya alma süresinin de sayılması gerekiyorsa CLOCK_BOOTTIME.

İkinci klasik arıza pilsiz RTC'dir. Cihaz Unix epoch başlangıcında, yani 1 Ocak 1970'te açılır; TLS el sıkışması sertifika geçerlilik penceresi yüzünden başarısız olur; zaman sunucusuna ulaşılamaz; saat düzelmez. Kilit tamamlanır. Bunun tasarımdaki karşılığı, "zaman henüz güvenilir değil" durumunun açıkça modellenmesi ve hem OTA hem günlük zaman damgalarının bu duruma göre davranmasıdır.

Kabul kriteri: hiçbir zamanlayıcı saat sıçramasıyla süresiz askıda kalmaz; günlük satırları hem monotonik hem duvar saati damgası taşır; zaman güvenilir değilken sistemin davranışı belgelenmiş bir tercihtir, kaza değil.

Tekrarlanabilirlik: kampanyanın künyesi

Bir hata enjeksiyonu ancak bir başkası aynı sonucu üretebiliyorsa kanıttır. Bunun için her koşuya bir künye eşlik etmelidir:

  • Görüntünün (image) özet değeri — üretime çıkacak görüntünün ta kendisi, hata ayıklama servisleri açık bir geliştirme görüntüsü değil.
  • uname -a ve /proc/cmdline çıktısı.
  • Enjeksiyon zamanlamasını üreten rastgele tohum.
  • Tetik tanımı ve tetikten enjeksiyona kadar geçen gecikme.
  • Tekrar sayısı ve her tekrarın sonucu — özet değil, tek tek.

Kampanya bir tabloya yazılır ve sürüm kontrolüne girer. Donanım döngüsünü otomatikleştirmek için labgrid ya da LAVA gibi hazır çerçeveler kullanılabilir; asıl mesele araç değil, koşunun elle tekrar edilebilir olmasıdır.

Tek arıza enjekte etmek de yeterli değildir. Sahadaki arızalar bileşiktir: disk dolmaya yakınken OTA yazarken gelen bir gerilim çökmesi üç senaryonun kesişimidir ve üçü ayrı ayrı geçmiş olabilir.

Kayıt disiplini: kanıt arızadan sağ çıkmalı

Enjeksiyon kaydı, enjekte edilen arızanın yok edemeyeceği bir yerde tutulmalıdır. Bu pratikte üç kanal demektir:

  1. Dış seri kayıt. Konsol çıktısı ana bilgisayarda kesintisiz toplanır; cihazın gücü gitse de satırlar yerinde durur.
  2. pstore / ramoops. CONFIG_PSTORE_RAM ile ayrılan bellek bölgesi, sıcak yeniden başlatma boyunca panik ve oops kayıtlarını taşır; sonrasında /sys/fs/pstore/ altından okunur. Bölge çekirdek komut satırında ramoops.mem_address ve ramoops.mem_size ile tanımlanır, ramoops.ecc ile hata düzeltme açılabilir.
  3. Olay işaretleri. Enjeksiyon anı bir GPIO kenarıyla dışarıya basılır; böylece kayıt ile arıza aynı zaman ekseninde buluşur.

Cihazın kendi diskine yazılan günlük, tam da sınadığınız senaryolarda ilk kaybedilen şeydir.

Nerede yanlış gider

Arıza savunmasız pencereye hiç denk gelmez. İşlemler arasında kesilen güç her seferinde temiz açılış verir ve ekip yanlış bir güvenle sahaya çıkar. Çözüm, tetiği kritik bölümün başlangıcına bağlamak ve gecikmeyi tohumlu rastgelelikle taramaktır.

Geliştirme görüntüsü sınanır. Hata ayıklama servisleri, açık seri konsol ve farklı bir dosya sistemi düzeni, üretim görüntüsünde var olan arızayı tamamen gizleyebilir. Sınanan şey sevk edilecek yapının kendisi olmalıdır.

Gözcü her testi geçirir. Otomatik yeniden başlatma açıkken hemen her senaryo "kurtarıldı" sonucuyla biter. "Doğru davrandı" ile "gözcü kurtardı" ayrı sayılmalı ve ayrı raporlanmalıdır.

Sürücü sessizce toparlar. CAN'da restart-ms, ağda yeniden bağlanma mantığı, blok katmanında yeniden deneme — hepsi arızayı uygulamadan gizler. Kampanyanın bir bölümü bu otomatizmalar kapalıyken koşulmalıdır ki altta ne olduğu görülsün.

Doldurulan disk beklenen şekli üretmez. fallocate ile ayrılan alan ile gerçekten yazılmış veri farklı davranır; ayrılmış blok payı ayrıcalıklı süreçleri korur; sonuç servisin çalıştığı kullanıcıya göre değişir.

Depolama aygıtının arızası yazılım arızası sanılır. Güç kesintisi eMMC'nin kendi eşleme tablolarını bozabilir; bu, dosya sistemi katmanının üstünde çözülecek bir sorun değildir. Katmanı ayırmadan haftalar yanlış yerde harcanır.

Kabul kriteri sonradan yazılır. Sonucu gördükten sonra tanımlanan başarı ölçütü ölçüt değildir; sadece anlatının bir parçasıdır.

Karar kriteri

Bir sürümün hata enjeksiyonu açısından çıkmaya hazır olup olmadığına dört soruyla karar verilir:

  1. Matris tam mı? Ürünün maruz kalacağı beş sınıfın her biri için en az bir senaryo tanımlı mı, yoksa yalnızca kurulumu kolay olanlar mı koşuldu?
  2. Kriter önce mi yazıldı? Her senaryonun kabul cümlesi koşudan önce yazılmış ve koşudan sonra değiştirilmemiş mi?
  3. Tekrar üretilebilir mi? Bulunan her arıza, tohum ve künye ile bir başkası tarafından yeniden üretilebiliyor mu?
  4. Kanıt sağ çıktı mı? Kayıt, enjekte edilen arızanın yok edemeyeceği bir kanalda mı duruyor?

Kaynak kısıtlıysa öncelik sırası tartışmalı değildir. Yazma sırasında güç kesintisi ilk sırada gelir, çünkü verinin kendisini bozar ve sahada en zor toparlananıdır. Ardından bozuk paket gelir, çünkü çökmeden yanlış davranma riski taşır. Bus hatası, dolu disk ve saat sıçraması bunların arkasına yerleşir; ancak zamanla ilgili kararlar en geç fark edilen ve en pahalıya patlayan sınıftır, çünkü hatası bir çökme değil, sessiz bir yanlış karardır.

Son bir not: hata enjeksiyonu bir kalite kapısı olduğu kadar bir mimari geri bildirimdir. Bir senaryoyu geçirmek için kod yerine tasarımı değiştirmeniz gerekiyorsa, kampanya asıl işini yapmış demektir.