Sabah 02:47'de telefonum çaldı. İzleme sistemi, üretim sunucusunda disk doluluğunun yüzde 98'e vurduğunu söylüyordu. Uykulu gözlerle bağlandım, df çıktısına baktım ve anlamadım: uygulama verisi 60 GB civarıydı, disk 500 GB'tı. du ile katmanlara inince gerçek ortaya çıktı: /var/lib/docker/containers altında tek bir konteynerin log dosyası 312 GB olmuştu. Docker'a geçeli üç hafta olmuştu ve kimse json-file log sürücüsünün varsayılan olarak sınırsız yazdığını okumamıştı.
O gece, araç takip platformumuzu konteynere taşıma maceramızın yedi dersinden ilkini öğrendik. Kalan altısı da peşinden geldi. Sırayla anlatayım; belki bizim ödediğimiz bedeli siz ödemezsiniz.
Önce sahneyi kurayım: neden geçiyorduk? Üç üretim sunucumuz, üç yılda elle yapılmış yüzlerce küçük dokunuşla birbirinden habersizce farklılaşmıştı. Birinde libssl'in bir sürümü, diğerinde başkası; bir kütüphane güncellemesi A sunucusunda sorunsuz, B'de felaket. Yeni bir servisi üretime almak, "hangi makinede hangi bağımlılık var" arkeolojisiyle yarım gün sürüyordu. Docker bize tek bir şey vaat ediyordu ve o vaadi tuttu: imaj neyse üretimde çalışan o. Ama bu vaadin etrafında, kimsenin broşürde anlatmadığı bir mayın tarlası vardı. Yedi mayına bastık, yedisinin de yerini işaretledik.
Log Sürücüsü Sizi Sormaz, Diski Doldurur
Ders bir: Docker'ın varsayılan log yapılandırması üretim için tuzaktır. Bizim TCP dinleyicimiz her cihaz paketini debug seviyesinde logluyordu; günde 9 GB metin. Çözüm iki satırdı: max-size ve max-file ayarlarıyla konteyner başına 10'ar megabaytlık 5 dosya sınırı. Sonrasında logları merkezi bir yere akıtmaya başladık ama o ilk gece daemon.json'daki iki satır yetmişti. Bu ayarın host seviyesinde olduğunu ve mevcut konteynerlere geriye dönük uygulanmadığını da aynı gece öğrendik; hepsini yeniden oluşturmak gerekti.
1.9 Gigabaytlık İmajla Deploy Olmaz
İlk Dockerfile'ımız ubuntu:20.04 üzerine kurulu, derleme araçları dahil her şeyi taşıyan 1.9 GB'lık bir canavardı. Her deploy'da bu imajı üç sunucuya çekmek 6-7 dakika sürüyordu; acil bir yama gerektiğinde bu süre insana saat gibi geliyor. Multi-stage build'e geçtik: derleme aşaması ayrı, çalışma aşaması alpine tabanlı. İmaj 214 MB'a indi, katman önbelleği düzgün kullanılınca deploy 50 saniyeye düştü. Yan kazanç: küçük imajda saldırı yüzeyi de küçük; güvenlik taramasındaki uyarı sayısı 140'tan 12'ye indi.
latest Diye Bir Sürüm Yoktur
Üçüncü dersi bir cuma öğleden sonrası öğrendik. Staging'de her şey yeşildi; üretime aynı compose dosyasıyla çıktık ve rapor servisi açılmadı. Sebep: imaj etiketi latest'ti ve iki ortam imajı farklı saatlerde çekmişti. Aradaki dört saatte base imajın yeni minor sürümü yayınlanmış, bir kütüphanenin davranışı değişmişti. O günden sonra kural netleşti: üretime giden her imaj commit hash'iyle etiketlenir. latest kelimesi bizim sözlükte "hangi sürüm olduğunu bilmiyorum" demektir.
Konteyner Ölür, Veri de Onunla Ölür
Dördüncü ders en acısıydı. Küçük bir yardımcı servis, işlenmemiş SMS kuyruğunu konteyner içindeki bir SQLite dosyasında tutuyordu. Bir güncelleme sırasında konteyner silinip yeniden oluşturulunca 4 saatlik gönderilmemiş bildirim uçtu. Müşterilere açıklaması zor bir sessizlik yaşandı. Kalıcı olması gereken her yol için named volume tanımladık ve daha önemlisi bir envanter çıkardık: her servisin "konteyner silinirse ne kaybolur" sorusuna yazılı cevabı olmak zorunda. Cevabı "hiçbir şey" olmayan servis üretime çıkamıyor.
Volume dersinin bir de ikinci perdesi var: yedekleme. Veriyi named volume'a taşıyınca rahatlamıştık; ta ki bir tatbikatta o volume'ların gece yedeğine hiç girmediğini fark edene kadar. Eski yedek script'i belli dizinleri kopyalıyordu ve /var/lib/docker/volumes onun dünyasında yoktu. Volume envanterini yedek script'ine bağladık ve daha önemlisi, her ay bir volume'u rastgele seçip yedekten geri dönme provası yapmayı takvime koyduk. Alınmış ama hiç geri yüklenmemiş yedek, umuttan ibarettir.
PID 1 Olmak Sandığınızdan Zor
Beşinci ders sinsiydi. Deploy sırasında bazı GPS bağlantılarının son birkaç saniyelik verisi kayboluyordu. Kayıp o kadar küçüktü ki aylarca "cihaz kaynaklı" dedik. Gerçek sebep: uygulamamız Dockerfile'da shell formunda başlatıldığı için PID 1 olan kabuktu ve SIGTERM uygulamaya hiç ulaşmıyordu. Docker 10 saniye bekleyip SIGKILL basıyor, tampondaki veriler diske inemeden süreç ölüyordu. exec formuna geçip sinyal yakalayan düzgün bir kapanış rutini yazınca kayıp sıfırlandı. Graceful shutdown konteyner dünyasında lüks değil; veri bütünlüğünün ta kendisi.
Bu dersin bir dipnotu var: kapanış süresi de ayarlanabilir ve ayarlanmalı. Varsayılan 10 saniyelik bekleme, tampon boşaltan ve TCP bağlantılarını düzgün kapatan bir servis için dardı; stop_grace_period ile 30 saniyeye çıkardık. Tersine, durumsuz API konteynerlerinde 10 saniye bile gereksizdi; onlar 2 saniyede çekiliyor. Kapanış davranışını servis servis düşünmek, deploy sırasındaki o küçük ama sinir bozucu hata dalgalanmalarını tamamen bitirdi.
Konteynerin Saati Kimin Saati?
Altıncı ders komik görünür ama değildi: geçişten sonraki ilk sabah bir müşteri, gece 03:00'te mesai raporu e-postası aldığını söyledi. Konteynerler UTC'de çalışıyordu, host İstanbul saatindeydi ve zamanlanmış işler üç saat kaymıştı. Kalıcı çözümümüz konteynerlere saat dilimi dağıtmak değil, uygulamanın her yerde UTC konuşup yalnızca kullanıcıya gösterirken çevirmesi oldu. Bu disiplin sonradan bambaşka dertleri de kesti.
Sınır Koymazsanız Komşuyu Yerler
Yedinci ders: kaynak limiti tanımlanmamış konteyner, aynı host'taki herkesin ortak düşmanıdır. Rapor servisimiz büyük bir PDF üretirken belleği şişirdi, kernel'in OOM killer'ı devreye girdi ve kurban olarak aynı makinedeki veritabanı konteynerini seçti. O günden beri her konteynerin bellek ve CPU limiti compose dosyasında yazılı; limiti olmayan servis kod incelemesinden geçemiyor. Limitler ilk başta gereksiz bürokrasi gibi gelir; ilk OOM cinayetinden sonra sigorta poliçesine dönüşür.
Bedava Sekizinci Ders: Orkestrasyona Direnmek
Bir de para ödemeden öğrendiğimiz sekizinci ders var. Docker'a geçer geçmez etraftan "Kubernetes'e ne zaman geçiyorsunuz" soruları başladı. Ciddi ciddi değerlendirdik ve üç sunucu, on beş konteyner için kurulmuş bir Kubernetes'in bize çözeceği problemden çok, öğrenip işleteceğimiz yeni problem getireceğine karar verdik. docker-compose artı sağlam bir deploy script'i, bizim ölçeğimizde işi görüyor; restart politikaları ve healthcheck'lerle kendi kendini toparlama ihtiyacımızın yüzde 90'ı karşılanıyor. Orkestratör ihtiyacı ölçekle gelir; ölçek gelmeden kurulan orkestratör, iki kişilik altyapı ekibinin bütün mesaisini yutan bir hobi projesine dönüşür. Bu kararı her çeyrek yeniden gözden geçiriyoruz; şimdilik her seferinde aynı kapıya çıktı.
Bir yıl sonra tabloya bakınca geçişin bilançosu net: deploy süremiz dakikalardan saniyelere indi, "bende çalışıyor" cümlesi toplantılardan silindi, yeni geliştirici makinesinin kurulumu iki günden bir saate düştü. Yedi ders bize toplamda birkaç gece uykusu ve bir müşteri özür telefonuna mal oldu; verdiği rahatlığın yanında ucuz bile sayılır.
Yedi dersin ortak paydası şu: Docker'ın kendisi bizi hiç üzmedi; varsayılanlara güvenmek üzdü. Konteyner araçları hızla olgunlaşıyor ama varsayılan ayarlar hâlâ geliştirici deneyimi için seçilmiş durumda, üretim dayanıklılığı için değil. Geçişi planlarken bu yedi başlığı bir kontrol listesi gibi kullanabilirsiniz. Kendi geçiş hikâyenizde tıkandığınız bir nokta varsa iletişim sayfasından ulaşın; muhtemelen aynı çukura biz de düşmüşüzdür.