Tek Bir Ağır Sohbet API’mizi Nasıl Çökertebilirdi
28 Eylül’de, bir sürümden yaklaşık 17 dakika sonra, üretimdeki iki API sunucumuzun ikisi de aynı anda bellek yetersizliğine düştü, yeniden başladı ve yaklaşık beş dakika boyunca yük dengeleyici trafik gönderebileceği sağlıklı bir sunucu bulamadı. İlk düzeltmemiz API’ye iki katı bellek verdi. Asıl neden o günün ilerleyen saatlerinde ortaya çıktı: belirli bir sohbeti açmak, API’den tek bir istekte 53 MB mesaj yüklemesini istiyordu. Sohbet geçmişi artık sayıya ek olarak boyuta göre de sınırlanmış sayfalar halinde yükleniyor ve o sohbet çökme yerine 5 MB’lık bir ilk sayfayla açılıyor.
Neler çöktü
API’miz, bir yük dengeleyicinin arkasında iki sunucudan her birinde bir konteyner olarak çalışıyor. Her konteynerin bellek sınırı 384 MiB. Boştayken 130–140 MiB kullanıyor, bu yüzden sınır bol bir pay gibi görünüyordu.
Her iki konteyner de bu sınıra aynı anda ulaştı ve “JavaScript heap out of memory” ile çöktü. Heap, bir Node.js sürecinin üzerinde çalıştığı nesneler için kullandığı bellektir. İki sunucu da aynı anda başarısız olduğu için istekleri yönlendirecek hiçbir yer kalmadı. İki sunuculu bir kurulum için bu en kötü kesinti türüdür: yedeklilik yalnızca hatalar aynı anda olmadığında işe yarar.
İlk düzeltme: daha fazla alan
Zamanlama dağıtımı işaret ediyordu. Bir sürümden sonra, dağıtımın kestiği agent turları devam ettirilir ve transkriptlerini yeniden kaydeder; bu çalışma patlaması bellek artışının bariz kaynağı gibi göründü.
Bu yüzden ilk değişiklik API’nin sınırını 384 MiB’den 768 MiB’ye çıkardı. Bunu iki sunucuda da aynı gün canlı olarak uyguladık ve bir sonraki dağıtımın 384’ü sessizce geri koymaması için dağıtım yapılandırmasına da işledik. Bir sunucuda elle yapılan ve bir sonraki dağıtımın geri aldığı bir düzeltme, aynı kesintinin geri gelmesinin yaygın bir yoludur.
Daha fazla bellek makul bir güvenlik payıydı. Bir açıklama değildi.
Asıl neden: bir istek, 71 MB
Daha sonra o gün, eski üretim bellek sınırını kullanarak bir sohbetin kopyasında çökmeyi yeniden ürettik. Kopya, bir dağıtım olmadan API’yi tek başına çökertti.
Bir projeyi açtığınızda web uygulaması sohbetin geçmişini API’den ister. Uç nokta en yeni 30 mesajı tam olarak döndürüyordu. Çoğu sohbet için 30 mesaj küçüktür. Bu sohbet için ise en yeni 30 mesaj veritabanında 53 MB, yanıt olarak da 71 MB idi. Agentin yanıtlarından birkaçı her biri 4–10 MB’tı; çoğu alt agentlerin transkriptleriydi: ana agentin görevin bir parçası üzerinde paralel çalışmak için başlattığı yardımcı agentler, tam çalışmaları yanıtla birlikte kaydedilir.
Bu tek yanıtı oluşturmak API’nin sahip olduğundan daha fazla heap gerektirdi. Ve projeyi her açışınızda ya da yenilemenizde istek yeniden gönderiliyordu. Tek bir sohbet, hangi sunucuya düşerse onu çökertebilirdi ve sayfa yenilenebildiği için iki sunucuya da ulaşabilirdi.
“30 mesaj” hiçbir zaman gerçek bir boyut sınırı olmamıştı. Normal sohbetler için küçük olduğu için tesadüfen işe yarayan bir sayı sınırıydı.
Düzeltme: boyuta göre de sınırlanan sayfalar
Artık bir geçmiş sayfası 30 mesaj ya da yaklaşık 4 MB, hangisi önce dolarsa.
İki adımda çalışır:
- Önce boyutları iste. API, veritabanından en yeni mesajların kimliklerini ve saklı boyutlarını, içeriklerini almadan ister. Bu sorgu mesajlar ne kadar büyük olursa olsun ucuzdur.
- Yalnızca sığanı yükle. En yeniden en eskiye doğru ilerleyerek, bir sonrakinin sayfayı 4 MB’ın üzerine çıkaracağı noktaya kadar mesaj ekler. Ardından yalnızca bu satırlar tam olarak yüklenir.
İki kural bunu güvenli tutar:
- En yeni mesaj her zaman sayfaya girer, boyutu ne olursa olsun. Hiçbir şey döndürmeyen bir sayfa sohbeti takılı bırakır ve geriye kaydırmanın bir yolu kalmaz.
- Hiçbir şey kesilmez. Hiçbir mesaj kırpılmaz ya da atılmaz. Sohbetin geri kalanı, eski mesajlar için zaten kullanılan “Daha eski mesajlar için yukarı kaydır” denetimiyle, sayfa sayfa yüklenir.
Aynı şekle sahip bir yer daha bulduk. Bir turu durdurduğunuzda API, son birkaç yanıtın boş olup olmadığını ve temizlenmesi gerekip gerekmediğini kontrol eder. Bunu, sekiz tam mesajı yükleyerek yapıyordu. Çok megabaytlık yanıtları olan bir sohbette, bu küçük bir biçimde aynı sorundu. Artık veritabanına evet-hayır bir soru soruyor — bu mesajın herhangi bir içeriği var mı? — içeriğin kendisini yüklemeden.
Nasıl doğruladık
Kopya sohbette, eski 384 MiB sınırıyla çalışırken:
- İlk sayfa 5 MB. Önceden aynı istek API’yi çökertiyordu.
- Sohbeti aynı anda açan beş tarayıcı sekmesi, 154 MB bellekte zirve yaptı.
- Sohbetin tamamında geriye kaydırmak, 190 mesajın her birini bir kez ve sırayla döndürdü.
Sayfa seçme mantığının da birim testleri var; bunlardan biri sohbetin en yeni mesajlarının gerçek boyutlarından oluşturuldu. İlk sayfa bir mesajla geri döner, daha fazlası olduğunu söyler ve sonraki sayfa bir sonrakinden başlar.
Bundan çıkardıklarımız
- Sayı bir boyut sınırı değildir. Sınırsız büyüyebilen bir şeyin “en yeni N”ini döndüren her uç noktanın bir bayt bütçesine de ihtiyacı var. Veri, boşluğu er geç bulur.
- Bariz şüpheli her zaman neden değildir. Dağıtım gerçek bir tetikleyiciydi ve ekstra bellek yine de değerliydi. Ama çökme dağıtım olmadan da yeniden üretildi; onu yeniden üretmeseydik, bellek artışında durup kalacaktık.
- İki sunucu, aynı isteği aldıklarında birlikte düşer. Yedeklilik bir sunucunun ölmesine karşı korur. Herhangi bir sunucuyu öldüren bir isteğe karşı korumaz; özellikle de kullanıcının yenileyerek doğal olarak tekrar deneyeceği bir isteğe karşı.
- Düzeltmeyi yalnızca kutuda değil, yapılandırmada yapın. Bellek artışı aynı gün dağıtım yapılandırmasına girdi; böylece bir sonraki dağıtım onu korudu.
Bu, agentin çıktısıyla ilgili son zamanlardaki ikinci bellek hikâyemiz. Birincisi, 690 MB dize kuyruğa alan bir durum etiketi. Mythex’te alt agentleri kullanıyorsanız, nasıl çalıştıklarını dokümanlarda okuyabilirsiniz.