Üretimde Rust: Sinyal İşleyicimizi Neden Yeniden Yazdık
Sinyal işleme motorumuzu Go'dan Rust'a yeniden yazdık. İşte nedeni, öğrendiklerimiz ve elde ettiğimiz 4 kat verim artışı.
Altı ay önce, ham tarayıcı sinyallerini normalleştirilmiş, karma alınabilir özellik vektörlerine dönüştüren bileşen olan sinyal işleme motorumuzu Go'dan Rust'a yeniden yazma kararını aldık. Bu, hafife alarak verdiğimiz bir karar değildi. Go uygulamamız çalışıyordu. Test edilmişti. Dağıtılmıştı. Ancak boru hattımızda darboğaz hâline gelmişti ve verimde basamak fonksiyonu düzeyinde bir iyileşmeye ihtiyacımız vardı. İşte olanlar.
Go'yu Neden Aştık
Sinyal işleyicimiz hesaplama açısından yoğun bir iş yapar: JSON yüklerini ayrıştırmak, 130'dan fazla sinyale normalleştirme fonksiyonları uygulamak, özel karmalar hesaplamak ve kimlik belirleme karma vektörleri oluşturmak. Go'da bu iş CPU'ya bağlıydı ve Go'nun çöp toplayıcısı ölçekte bir soruna dönüştü. Her sinyal işleme döngüsü ara nesneler ayırıyordu — ayrıştırılmış JSON düğümleri, normalleştirilmiş dize değerleri, karma arabellekleri — ve bunlar GC baskısı yaratıyordu.
Saniyede 30 bin olayda, Go sinyal işleyicimiz birkaç saniyede bir 2-5 ms'lik GC duraklamaları sergiliyordu. Bu duraklamalar kabul edilebilirdi. Saniyede 50 bin olayda GC duraklamaları 8-15 ms'ye çıktı ve daha sık meydana geldi. Saniyede 80 bin olayda — 3. çeyrek için öngörülen yükümüz — GC duraklamaları p99 gecikmesinin SLA'mızı aşmasına neden olurdu. Ya daha fazla sunucuya (pahalı) ya da daha verimli bir uygulamaya ihtiyacımız vardı.
Neden Rust
Üç seçeneği değerlendirdik: Go uygulamasını optimize etmek (sync.Pool, arena tahsisi, GOGC ayarı), C++ ile yeniden yazmak ve Rust ile yeniden yazmak. Go optimizasyonu %30'luk bir iyileşme sağladı ancak GC sorununu temelden çözmedi. C++, güvenlik açısından kritik bir sistemde bellek güvenliği kaygıları nedeniyle reddedildi. Rust; sıfır maliyetli soyutlamalar, çöp toplayıcı olmaması ve derleme zamanında zorunlu kılınan bellek güvenliği garantileri sunuyordu.
Rust ekosistemi ayrıca ihtiyacımız olan her şey için olgun kütüphanelere sahipti: JSON ayrıştırma için serde, yüksek performanslı karma crate'leri ve asenkron I/O için tokio. Öğrenme eğrisi gerçekti — ekibimizin derin Go deneyimi vardı ancak sınırlı Rust deneyimi — ancak performans özellikleri tam olarak ihtiyacımız olan şeydi.
Yeniden Yazma Süreci
Sinyal işleyiciyi, boru hattımızın geri kalanıyla gRPC üzerinden iletişim kuran bağımsız bir hizmet olarak yeniden yazdık. Bu, onu Go uygulamasının yanında dağıtmamıza ve trafiği kademeli olarak kaydırmamıza olanak sağladı. Yeniden yazma, üç mühendisin dört haftasını aldı — çekirdek uygulama için iki hafta ve test, kıyaslama ile uç durum işleme için iki hafta.
En zorlu yön, dilin kendisi değil, Go uygulamasıyla davranışsal eşitlik sağlamaktı. Aynı girdiyle her iki uygulamayı da çalıştıran ve aynı çıktıyı ürettiklerini doğrulayan bir karşılaştırma düzeneği oluşturduk. Bu süreçte 14 ince fark keşfettik — çoğunlukla kayan nokta işleme, Unicode normalleştirmesi ve JSON ayrıştırma uç durumlarıyla ilgili.
Performans Sonuçları
Rust uygulaması sinyalleri Go'nun 3,2 ms'sine karşılık ortalama 0,8 ms'de işliyor — 4 kat iyileşme. Aynı iş yükü için bellek kullanımı 2,1 GB'tan 340 MB'a düştü. Çöp toplayıcı olmadığı için GC duraklaması da yok. Aynı verimde CPU kullanımı %60 azaldı, bu da her sunucunun 4 kat daha fazla trafik işlediği anlamına geliyor.
Saniyede 80 bin olayda, Rust uygulaması sıfır duraklamayla 1,4 ms'lik bir p99 işleme süresini koruyor. Bu manevra payı, öngörülebilir gelecekte sinyal işleme performansını yeniden ele almamıza gerek olmayacağı anlamına geliyor. Azalan CPU ve bellek kullanımı ayrıca doğrudan daha düşük altyapı maliyetlerine dönüşüyor — 12 sinyal işleme sunucusundan 8'ini devre dışı bıraktık.
Öğrenilen Dersler
Rust ile yeniden yazmak, bizim özel durumumuz için — CPU'ya bağlı, tahsis açısından yoğun, gecikmeye duyarlı bir iş yükü — buna değdi. HTTP alım katmanımızı veya ClickHouse sorgu hizmetimizi Rust ile yeniden yazmazdık, çünkü bu bileşenler I/O'ya bağlıdır ve Go bunları verimli şekilde işler. Ders, "her şeyi Rust ile yeniden yazın" değil, "Rust'ı sıfır maliyetli soyutlamalarının ve belirlenimci performansının en çok önem taşıdığı yerde kullanın" dersidir.
En büyük sürpriz, Rust derleyicisinin yeniden yazma sırasında ne kadar çok şey yakaladığıydı. Go uygulamamızdaki birkaç gizli hata — paylaşılan arabelleklerde yarış koşulları, karma hesaplamasında tamsayı taşması ve hatalı biçimlendirilmiş girdide sınır dışı erişim — Rust'ta derleme zamanı hataları olarak yakalandı. Derleyici zorlayıcıdır ancak doğruluk açısından kendini amorti eder.
Açıkçası ilk hafta acı vericiydi. Sarah, beyaz tahtada bir "ödünç denetleyicisiyle kavgalar" çetelesi tuttu — ekip saymayı bırakmadan önce 47'ye ulaştık. Ancak üçüncü haftaya gelindiğinde, derlenen kod çalıştı gitti. Gizemli üretim panikleri yok, yük altında veri yarışları yok. Bu ödünleşim, sıcak yol üzerindeki her şey için buna değer.