Vivaldi Social’e ne oldu?
(thomasp.vivaldi.net)- 8 Temmuz 2023’te Vivaldi Social’in Mastodon instance’ında eski kullanıcı hesapları ortadan kayboldu ve sonunda 198 hesap tek bir uzak hesapta birleştirildi.
- Bunun nedeni doğrudan silme ya da saldırı değil, Mastodon’un hesap birleştirme davranışı ile Vivaldi Social’in Makara tabanlı PostgreSQL replikasyon yapılandırmasının çakışması sonucu işlem sırasının bozulmasıydı.
- Hesaplar silinmiş gibi görünse de kullanıcı adları yeniden atanmış ve avatar ile header görselleri de birlikte kaybolmuştu; bu da sorunun Mastodon uygulamasının iç işleyişi içinde daraltılmasını sağladı.
- Operasyon ekibi tüm veritabanını geri almayı hazırlarken, seçici kurtarma script’lerini de paralel yürütüp hesap, gönderi, takip, takipçi ve ilişki verilerini geri getirdi.
- Mastodon v4.1.5, Sidekiq worker’larında Makara kullanımını engelleyen ve hesap birleştirme sırasını düzelten değişiklikleri içeriyor; replikasyon DB kullanan sunucu yöneticileri worker’ların okuma yolunu kontrol etmeli.
198 hesabın kaybolduğu hafta sonu olayı
- 8 Temmuz 2023 Cumartesi günü saat 17:25 CEST civarında Vivaldi Social sekmesi yeniden giriş istedi ve giriş sonrası ana zaman akışının boş olduğu görüldü.
- Aynı belirti diğer sistem yöneticisi hesaplarında da ortaya çıktı; veritabanı incelemesinde etkilenen hesapların silindikten sonra kullanıcı yeniden giriş yaptığında yeni hesapmış gibi tekrar oluşturulduğu anlaşıldı.
- Vivaldi Social’de Cuma 23:00 UTC’de alınmış gece yedeği vardı ve operasyon ekibi geri yükleme olasılığını doğrulamak için yedek dosyasını kopyalamaya başladı.
- Normal Mastodon hesap silmelerinde kullanıcı adı kalıcı olarak rezerve edilir ve yeniden kullanılamaz; bu olayda ise aynı kullanıcı adı yeniden atanmıştı, yani normal bir silme değildi.
Silme işlemi devam ediyordu
- Başta ID’si 142’den düşük eski hesaplar kaybolmuş görünüyordu; 19:10’da ise ID’si 217’den düşük hesapların da kaybolduğu görüldü ve silmenin sürdüğü ortaya çıktı.
- 19:18’de Mastodon geliştiricilerinden yardım istendi; Renaud yanıt verdi, ardından Claire ve Eugen de incelemeye katıldı.
- 19:20’de Mastodon Docker instance’ları yeniden başlatılınca silme durdu ve veritabanındaki en düşük hesap ID’si 236 oldu.
- Olay süresince silinen veya birleştirilen hesap sayısının toplamda 198 olduğu doğrulandı.
Saldırı değil, uygulama davranışı olduğu anlaşıldı
- Operasyon ekibi ve Mastodon geliştiricileri,
UserCleanupScheduler’ın “unconfirmed” hesapları silmiş olabileceğini kontrol etti; ancak silinen kullanıcılar ilgili sorgu koşullarını karşılamadığından bu ihtimal elendi. - Olaydan 48 saat önce Mastodon 4.1.3’e yükseltme yapılmış olduğundan, v4.1.2 ile v4.1.3 arasındaki değişiklikler ve Vivaldi’nin yayımladığı değişiklikler de incelendi ama ilgili bir neden bulunamadı.
- Dosya sisteminde silinen hesapların avatar ve header görsellerinin de kaybolmuş olması, bunun basit bir doğrudan DB silmesi değil, Mastodon uygulamasının silme işlemini gerçekten yürüttüğünü gösterdi.
- Loglarda ve dosya sisteminde ihlal ya da saldırı izi arandı fakat kanıt bulunamadı; Mastodon v4.1.3’teki güvenlik düzeltmeleriyle ilişkili bir exploit ihtimali de doğrulanamadı.
- Cumartesi gecesi hesap silme davranışı için ek loglar yazan bir patch dağıtıldı; patch’li sürüm 00:29 CEST’te dağıtıldıktan sonra ekip dinlenmeye çekildi.
Kritik ipucu: tek bir uzak hesapta toplanan gönderiler
- Pazar 13:56’da, Vivaldi güvenlik uzmanı Yngve’nin profil sayfasının HTTP 500 hatası verdiği bildirildi; bu hesap, silinen 198 hesap arasında değildi.
- Loglarda aynı uzak Mastodon instance’ındaki aynı hesap tekrar tekrar görünüyordu; metinde bu hesap
social.example.comüzerindeki bir hesap olarak takma adla anılıyor. - Bu uzak hesabın status’larını sorgulayan sorgu 17.600 satır döndürdü.
- 14:43’te, silinen tüm hesapların tüm status’larının
social.example.comüzerindeki tek bir kullanıcıya yeniden atandığı yedekle karşılaştırılarak doğrulandı. - 15:00 sonrasında
AccountMergingWorkerlogları, Rails konsolu ve ek DB sorguları üzerinden, hesap birleştirme worker’ının tüm hesapları tek bir uzak hesapta birleştirdiği hipotezi güçlendi.
Kök neden: hesap birleştirme ve PostgreSQL replikasyon gecikmesi
- Vivaldi Social, PostgreSQL 2 sunuculu replikasyon yapılandırması kullanıyordu ve worker süreçleri Makara üzerinden standby sunucudan veritabanı okuması yapabiliyordu.
- Claire’in 17:28’de ortaya koyduğu olay senaryosu şöyleydi:
- Vivaldi Social,
social.example.comüzerinden gelen bir hesap adı değişikliği bildirimini aldı. - Yeni hesap veritabanında oluşturulurken
URIalanınullolarak kaydedildi. - Ardından yeni hesabın
URIdeğeri uzak hesabın doğru değeriyle güncellendi. - Redis üzerinden, eski hesaptaki verileri yeni hesaba birleştirecek
AccountMergingWorkerçalışmasının yürütülmesi planlandı. - Veritabanı replikasyon gecikmesi nedeniyle,
URIataması ile worker çalışmasının zamanlanması sırası gerçek okuma anında bozuldu.
- Vivaldi Social,
- Mastodon instance’ındaki tüm yerel hesapların
URIdeğerinullolduğundan, worker aynıURIdeğerine sahip hesapları yeni uzak hesapta birleştirirken tüm yerel hesaplar eşleşti. - Geliştiriciler, veritabanı yükü arttığında ve replikasyon gecikmesi uzadığında bunun daha kolay tetiklenebileceğini düşünüyor.
- Operasyon ekibi ve Mastodon geliştiricileri, bu yapılandırmanın kök neden olma ihtimalinin çok yüksek olduğu sonucuna vardı.
Patch ve yapılandırma değişiklikleri
- Neden daraltıldıktan sonra operasyon ekibi veri kurtarmaya odaklandı, Claire ise tekrarını önleyecek patch’i yazmayı üstlendi.
- Hlini patch’i uygulama ve artık önerilmeyen replikasyon yapılandırmasını değiştirme işini aldı.
- 17:58’de dağıtım sırasında sorun yaşandı ve o hafta sonunun tek tam kesintisi yaşandı; 18:18’de Vivaldi Social yeniden ayağa kalktı.
- 18:44’te patch ve yapılandırma değişiklikleri başarıyla dağıtıldı ve aynı olayın yeniden yaşanmayacağı düşünüldü.
Kurtarma: tam rollback yerine seçici geri yükleme
- Başta tüm veritabanını geri alma düşünülse de, bilinen performans sorunları nedeniyle yedek
.dumpdosyasını.sqlformatına dönüştürüp 54 GB’lık metin dosyasını düzenlemeyi gerektiren karmaşık bir süreç gerekiyordu. - Operasyon ekibi tam geri yükleme süreci ile seçici geri yüklemeyi paralel yürüttü.
- Hlini, 54 GB’lık
.sqldosyasını düzenleyip tam geri yükleme hazırlığını sürdürdü. - Thomas, silinen hesapları ve ilgili verileri geri getiren bir script yazdı.
- Hlini, 54 GB’lık
- Script yazımı sırasında PDO sorgu parametresi binding’inin referansla ele alınmasından kaynaklanan bir hata yapıldı; bunu Ísak buldu.
- 23:04’te, etkilenen 198 kullanıcının user, account ve identity kayıtlarını düzelten ilk bölüm tamamlandı.
- 23:55’te status, follows, followers ve ilişki verilerini olay öncesi duruma döndüren seçici kurtarma script’i tamamlandı.
Seçici kurtarma tamamlandı ve sonraki düzeltmeler
- Veritabanı ilişki kısıtları nedeniyle kurtarma 2 aşamada yapıldı.
- Önce 198 kişinin tamamı için user/account/identity kayıtları geri yüklendi.
- Sonrasında kalan ilişki verileri geri yüklendi.
- Bazı kullanıcılar olaydan sonra tekrar giriş yapıp yeni takipler oluşturduğundan duplicate key hataları oluştu; script, geri yüklenemeyecek mevcut kayıtları silip daha yeni kayıtları koruyacak şekilde güncellendi.
- Pazartesi 01:27 CEST’te script’in son işi tamamlandı, 01:40’ta ise ana akışın yeniden indekslenmesi bitti.
- Sonuç olarak 198 hesabın ana akışı geri getirildi ve tam rollback gerekli olmadı.
- Pazartesi ve Salı günleri ek sorunlar da düzeltildi.
- Kullanıcı adında sembol bulunan 6 hesabın giriş sorunu
- 198 hesabın web ayar verilerinin kaybı
- Takipçi sayısı, gönderi sayısı gibi profil sayaçlarındaki hatalar
- Yanlış veri içeren 4 hesap
Mastodon’un resmi düzeltmeleri
- Mastodon geliştiricileri, Makara tabanlı replikasyon yapılandırmasında Mastodon kullanmanın riskleri konusunda diğer sunucu yöneticilerini uyardı.
- Bunun, Vivaldi Social gibi büyük instance’larda ancak düşünülebilecek ve bu yüzden nadir görülen bir yapı olduğu belirtildi.
- Mastodon v4.1.5, bu olayla ilgili iki düzeltme içeriyor.
UTC’ye göre olay zaman çizelgesi
- Cumartesi 15:15: Harici bir instance’dan gelen hesap adı değişikliği mesajı Vivaldi Social’e ulaştı ve hatalı hesap birleştirme işi başladı.
- Cumartesi 15:25: Olayın ilk işaretleri gözlemlendi.
- Cumartesi 17:20: Docker container’ları yeniden başlatıldıktan sonra hesap birleştirme işi durdu; 15:15 ile 17:20 arasında toplam 198 hesap silindi/birleştirildi.
- Pazar 13:00: Olası kök neden belirlendi.
- Pazar 14:25: Kök neden doğrulandı.
- Pazar 21:55: Veri kurtarma başlatıldı.
- Pazar 23:27: Veri kurtarma tamamlandı.
- Pazartesi 10:40: Kullanıcı adında sembol bulunan 6 hesap düzeltildi.
- Pazartesi 11:05: Kaybolan web ayar verileri geri yüklendi.
- Salı 15:31: Hatalı sayaç değerleri düzeltildi.
- Salı 16:01: Yanlış veri içeren 4 hesap düzeltildi.
1 yorum
Hacker News yorumları
Harika bir retrospektifti; özellikle uykusuzluk gibi insani maliyetlerin karmaşık arıza giderme süreçlerini ne kadar etkilediğini de iyi yansıtmış
En çok göze çarpan kısım, “yeni hesapların URI alanında null değerle veritabanında oluşturulmuş olması”ydı
Veritabanıyla ilgili postmortem’lere her baktığımda, NULL neredeyse her zaman olay mahallinin yakınlarında saklanıyor. NULL suçlu olmasa bile her zaman sorgulanacaklar listesine alınmalı
Tavsiye olarak, NULL’a sentinel değer olarak bel bağlamamak ve mümkünse veritabanında hiç izin vermemek daha iyi. Bir faydası var gibi görünse de, yıllar sonra veri modelinin anlamı değiştiğinde, zararsız görünen bir ifade NULL ya da NOT NULL bekleyip beklenmedik sonuç üreten, bulunması zor bir hataya dönüşerek o faydayı götürür
Bu olay bir yarış durumuydu; ama yerel hesaplarla uzak hesaplar tiplerle açıkça ayrılmış olsaydı işlem sırası önemli olmayabilirdi ve hesap birleştirme kodu da daha dar bir kapsamla sınırlandırılabilirdi
Null, verinin tamamen geçerli bir değeridir ve öyle ele alınmalıdır. Boolean için -1 kullanmak ya da string için boş değer kullanmak gibi varsayılanlar, NULL olsaydı çalışma zamanında hata verecek bir sistemi görünürde çalışır hale getirebilir; ama bu sistemin beklendiği gibi çalıştığı anlamına gelmez, sadece sessizleşir
NULL’ı örtbas etme isteğini anlıyorum; ama “yokluk” da “varlık” kadar verinin geçerli bir durumudur ve sistemler genellikle bunu kabul edecek şekilde yazılmalıdır
Bu durumda sorunun veritabanındaki NULL değil, uygulama katmanındaki NULL olduğunu düşünüyorum
NULL, bir tür Maybe monad gibi zorunlu olarak ele alınması gereken bir değerse, eninde sonunda ele alınır ve üzerinde düşünülür. Boş string de olsa, kullanılan dilin null string’i de olsa, kendi yaptığın özel işaret değeri de olsa çok fark etmez
Çoğu durumda uygulayıcılar önce Git tarzı merge conflict’in gerektirdiği kaygıları ve etkileşim gereksinimlerini düşünmeli, sonra da o başlangıç noktasından problem alanına uygun basitleştirici varsayımlar kurmalıdır
Mastodon kaynak koduna https://github.com/mastodon/mastodon/blob/main/app/workers/a... bakınca, birleştirme isteğini başlatan tarafın asenkron birleştirme yürütücüsüne ilettiği “hangi ID’lerden birleştirileceğine” dair açık bir liste bile yok gibi görünüyor; bu yüzden böyle bir şeyin yaşanması an meselesiymiş
Bu Mastodon eleştirisi değil. Ben de bizzat çok daha kötü yarış durumları içeren birleştirme mantığı yazdım ve bunun zararını gördüm. Aslında https://opencollective.com/mastodon gibi gönüllü bir projede böyle bir özelliğin var olması bile şaşırtıcı. Yine de dikkat edilmesi gereken bir örnek
Daha derinde, gerçeklik dağınıktır; veritabanı da gerçeklik dağınık diye işlem yapmayı reddedemeyeceği için NULL’dan kaçınılamaz. Örneğin hitap biçimini, ön unvanı ve son unvanı modelleyip bu verilerle tam bir selamlama oluşturmak istediğinizi düşünün; en azından son unvanı olmayan insanlar vardır. NULL saklamasanız bile selamlamayı oluşturmak için kullandığınız JOIN sonucunda NULL elde edersiniz
Belirli NULL değerlerini ortadan kaldırabilirsiniz; ama gerçek dünyada “uygun değil” ya da “bilinmiyor”un sık sık geçerli bir değer olduğu gerçeğini ortadan kaldıramazsınız ve veritabanı bunu ele almak zorundadır
Burada bana tanıdık gelen akış şu: “tam veritabanı yedeğimiz var, o halde tam geri yükleme yaparız” diye başlayıp, “tam geri yükleme zor; kesinti ve yan etkileri var”a gitmesi, sonra yeniden “akıllıca davranıp sadece eksik verileri kısmi olarak geri yükleyebiliriz”e dönmesi, manuel yapılırken tuhaf bir hatayla karşılaşılması, sonunda geçici olarak hazırlanmış seçici geri yüklemenin dağıtılması ve en son eksik kalan beş verinin temizlenmesi. Umarım altıncısını kaçırmamışlardır
Kim yedekleme/geri yükleme provası yaparsa yapsın, her seferinde böyle ilerler. Sonuçta yedek imajından hangi verinin geri alınacağına karar vermek her zaman uygulama düzeyinde yapılması gereken bir iş haline gelir
Yine de bu durumda sorunun ne olduğunu tam anlayamadım. Son sağlıklı yedekten her şeyi geri yüklemek, arada atılan bazı gönderilerin kaybolması nedeniyle üzücü olurdu; ama manuel çalışma ve belirsizlik yerine anında çözüm sağlayan yöntemdi
Mastodon geliştirme ekibinden Renaud, Claire ve Eugen’in beklenenden fazla yardımcı olduğu kısmı etkileyiciydi
Vivaldi’nin Mastodon’a maddi destek verip vermediğini bilmiyorum; sponsorlar sayfasında da adını bulamadım. Değilse, bu olayın Vivaldi’nin ya da Mastodon kullanan diğer şirketlerin sponsorluk veya destek sözleşmesi düşünmesine vesile olmasını umarım
Sponsorluk açık ve gerçekten büyük etki yaratıyor. Projede tam zamanlı insanların olması çok önemli; ancak şu anda teknik tarafta kurucu Eugen dışında yalnızca 1 tam zamanlı geliştirici ve 1 DevOps sorumlusu var
Uzun zamandır okuduğum postmortem’ler içinde oldukça iyilerden biriydi
2 ve 3 numaralı adımların atomik olarak işlenmemesi sorun gibi geliyor. Elbette bunu yapmanın önemsiz olmamasının nedenleri vardır, ama kodu henüz görmedim; bir ara bakmam gerekecek
Atomik hale getirmek önemsizmiş gibi görünüyor
Daha önce buna gerek yokmuş sadece. Atomik olmaması sorun yaratmıyor; tabii biri sidekiq’i eski bir veritabanı sunucusuna, yani bir replikaya bağlamak gibi kötü bir yapılandırma yapmadığı sürece. Burada ana sorun o yapılandırma gibi görünüyor
İlk kez devasa bir SQL dökümünü geri yüklemem gerektiğinde vim’in onu okurken gerçekten segmentation fault verdiğini görmeyi unutamıyorum
O zaman split(1) büyüsünü, yani dosyayı parçalara bölmeyi keşfettim. Büyük dökümü her tablo için bir dosya olacak şekilde parçalara ayırdım
Elbette tek bir tablo da devasa olabilir, ama en azından dosyalar daha tekdüze hale geliyor ve sed ya da awk gibi başka araçlarla sorguları dönüştürmek kolaylaşıyor
Yine de veriyi geri yüklemek için dökümü düzenlemeniz gereken noktadaysanız, geri yükleme prosedüründe ciddi bir şeyler yanlış demektir. Tabii gerçekten o durumda kaldığınızda bu bilgi pek işe yaramıyor
Geçici çözüm, her şeyi kademeli olarak işleyen ve dosyaları ortak öneklere göre alt dizinlere taşıyan bir Python betiği yazmaktı
“Claire log girdisinin tüm stack trace’ini istedi ve onu da loglardan çıkarabildik” kısmında kaşlarım kalktı
Bu ya derin bir vudu büyüsü ya da kod/ayarlar Xeon’u 286 seviyesine indiriyor. Her istek başına megabaytlar etmiyor mu?
Ruby on Rails’in varsayılan davranışı bu. 500 veya bilinmeyen bir hata olursa stack trace’i basar; içeriği de satır numarası ve dosya yolu civarındadır
Tasarımı epey kötü bir Rails uygulaması işletiyorum; az önce kontrol ettim, tek bir 500’ün stack trace’i 5KiB idi. 500 hatası yaklaşık saatte bir kez oluyor, yani günde 1MiB bile etmiyor
Çağrı yığınını yakında tutmak aslında performans açısından gayet makul. Java’nın varsayılan exception davranışı da her exception ile birlikte stack trace’i taşımaktır; yazdırmasanız bile böyledir, ama Java uygulamaları gayet çalışır. Zaten nasıl döneceğini bilmesi gerektiği için çağrı yığını elinizdedir; ek gereken bilgi yalnızca dosya adı ve satır numarası debug sembolleridir. Ruby’de dilin yapısı gereği bu bilgi zaten gerekir
“Mastodon instance’ındaki tüm yerel hesapların URI alanı null değer olduğundan hepsi eşleşti” nasıl mümkün olabilir?
NULL = NULL FALSE olarak değerlendirilir. SQL üç değerli mantık, daha doğrusu Kleene’in zayıf üç değerli mantığını kullanır ve NULL’a herhangi bir operatör uygulandığında sonuç NULL olur
URI sütununda NULL değer olan hesapların sorguyla nasıl eşleştiğini bilmiyorum. NULL, NULL’a eşit diye karşılaştırılmaz. Bu korkunç bir Rails büyüsü mü?
Kullanıcı adında sembol olan 6 kullanıcının giriş yapamadığı ve bunun kurtarma betiğindeki bir hatadan kaynaklandığı için kolayca düzeltildiği kısmı görünce UTF-8 yine iş başında dedim