Hastanelerin ameliyathane ve yoğun bakım bloklarında hayatı idame ettiren en kritik altyapı, duvarların içinden geçen merkezi medikal gaz ve oksijen boru hattıdır. Ana kriyojenik tanktan gelen saf oksijen, duvarlardaki özel prizlere (vanalara) 4 bar basınçla ulaştırılır.

Şimdi bu hastanede çalışan sorumsuz bir teknisyenin veya stajyerin davranışını hayal edin: Gelen her hastaya oksijen maskesi takmak için duvardaki vanaya bir hortum takıyor ve vanayı sonuna kadar açıyor. Fakat hasta muayene olup taburcu olduğunda, o hortumu prizden çekmiyor veya vanayı kapatmıyor! Hortum yere atılmış, saf oksijen tıslayarak boş odaya kaçmaktadır. İkinci hasta gelir; ona yeni bir vana açılır, o da kapatılmaz. Onuncu, ellinci, yüzüncü hasta... Duvardaki 150 oksijen prizinin tamamından boşluğa gaz fışkırmaktadır!

Sonuç ne olur? Ana boru hattındaki basınç 4 bardan anında 0.5 bara çakılır. Ameliyathanede açık kalp ameliyatı yapılan masadaki hastanın solunum cihazı alarm vermeye başlar: "Oksijen basıncı yetersiz!" Teknisyen ise jeneratör dairesine koşup "Bize daha büyük oksijen tankı alın, bu hastanenin tankı yetersiz" diye bağırır! Sorun tankın küçüklüğünde değil; arkasında açık bırakılan o yüzlerce sorumsuz vanadadır.

Veritabanı mühendisliğinde ve web uygulamalarında her gün yüzlerce web sitesini bir anda karanlığa gömen ve sistemleri kilitleyen felaket tam olarak bu açık vanalar faciasıdır: MySQL max_connections sınırının tükenmesi ve ekrana çarpan Too Many Connections hatası.

Salı 11:15: "Veritabanı Çöktü, RAM Yetmiyor!"

Orta ölçekli bir e-ticaret sitesi veya haber portalı... Anlık ziyaretçi sayısı 150-200 civarındadır. Donanım gayet güçlüdür: 8 çekirdekli işlemci, 32 GB RAM.

Aniden sitede hayat durur. Ziyaretçiler hangi sayfaya tıklarsa tıklasın karşılarına beyaz bir sayfa ve üzerinde tek bir satır hata çıkar:

User eticaret_user already has more than 'max_user_connections' active connections
veya:
Fatal error: Uncaught mysqli_sql_exception: Too many connections in /home/site/db.php:12

Sitenin kurucusu ve yazılımcısı çılgına döner. Yazılımcı hemen hosting firmasına yüksek öncelikli bir bilet açar:

"Sunucumuzun veritabanı motoru çöktü! Sürekli 'Too many connections' hatası veriyor. Sistemde 200 kişi var, sunucunun RAM'i yetersiz kalıyor! Acil olarak sunucuya 32 GB daha RAM ekleyin ve MySQL bağlantı limitini 5.000 yapın!"

Yazılımcı sorunun donanımda olduğuna o kadar emindir ki, faturayı anında hosting firmasına kesmiştir.

Perdenin Arkasındaki Otopsi: Yüzlerce "Sleep" Zombisi

Sistem yöneticisi sunucuya SSH ile bağlanır. MySQL konsoluna root yetkisiyle girer (MySQL, acil durumlar için root kullanıcısına özel 1 adet ekstra bağlantı payı saklar: max_connections + 1).

Mühendis o ünlü sorguyu çalıştırır:

SHOW PROCESSLIST;

Ve ekranda dökülen tablo, tam bir açık vana mezarlığıdır:

+-----+---------------+-----------+-------+---------+------+-------+------------------+
| Id  | User          | Host      | db    | Command | Time | State | Info             |
+-----+---------------+-----------+-------+---------+------+-------+------------------+
| 142 | eticaret_user | localhost | db_et | Sleep   | 148  |       | NULL             |
| 143 | eticaret_user | localhost | db_et | Sleep   | 145  |       | NULL             |
| 144 | eticaret_user | localhost | db_et | Sleep   | 142  |       | NULL             |
| ... | ...           | ...       | ...   | Sleep   | ...  |       | NULL             |
| 293 | eticaret_user | localhost | db_et | Sleep   | 68   |       | NULL             |
+-----+---------------+-----------+-------+---------+------+-------+------------------+
151 rows in set (0.00 sec)

Ekranda çalışan tek bir ağır SQL sorgusu bile yoktur! Bütün bağlantıların komut sütununda ne yazmaktadır? Sleep!

Yani bağlantılar uykudadır, hiçbir şey yapmamaktadır.

Yazılımcının yazdığı PHP kodu, her sayfa isteğinde veritabanına yeni bir TCP soketi açmış (new PDO(...) veya mysqli_connect); sorguyu çekmiş, HTML'i ekrana basmış; fakat işi bittiğinde o veritabanı bağlantısını kapatmamıştır!

Üstüne üstlük yazılımcı "performans olsun" diye veritabanı bağlantı ayarlarına ezbere şu parametreyi eklemiştir:

// Faciaya davetiye:
$options = [
    PDO::ATTR_PERSISTENT => true // Kalıcı bağlantı (pconnect)!
];

Kalıcı bağlantı (Persistent Connection), PHP-FPM worker'ı yaşadığı sürece o soketi asla kapatmaz; duvardaki vanayı açık bırakıp gider!

Ziyaretçiler geldikçe yeni bağlantılar açılır. Ve MySQL'in varsayılan güvenlik sınırı olan 151 bağlantı (veya kullanıcının max_user_connections = 30 sınırı) 2 dakika içinde damlasına kadar dolar! 152. ziyaretçi kapıya geldiğinde MySQL kapıyı kilitler: "Kusura bakma, içeride 151 kişi var ve hepsi vanaları açık bırakıp uykuya dalmış; sana verecek tek bir soketim bile kalmadı!"

max_connections = 5000 Yapmanın Ölümcül Bedeli

Yazılımcının önerdiği o "kolaycı" çözüm devreye alınırsa ne olur? Yani sistem yöneticisi my.cnf içine max_connections = 5000 yazarsa?

MySQL'de her bir açık bağlantı, işletim sisteminden bağımsız bir thread ve bellek alanı tahsis eder (sort_buffer_size, read_buffer_size, join_buffer_size).

150 bağlantı yerine aynı anda 5.000 bağlantı açıldığında, MySQL saniyeler içinde sunucudaki 32 GB RAM'in tamamını emer! Linux çekirdeğinin hafıza koruma polisi olan OOM Killer (Out of Memory Killer) devreye girer ve sunucunun en çok RAM tüketen sürecini kafasından vurarak öldürür: kill -9 mysqld.

Veritabanı anında çöker, InnoDB dosyaları bozulma tehlikesine girer. Limiti bilinçsizce artırmak, açık vanaların arkasına devasa bir baraj bağlayıp odayı suyla basmaktan farksızdır.

Temiz Kod ve Cerrahi Hijyen Protokolü

Too Many Connections kabusunu kökünden çözmenin yolu donanım satın almak değil, kodun ve veritabanının disiplinini sağlamaktır:

  1. Bağlantıyı Kapatın (Explicit Close): Kodunuzda veritabanıyla işiniz bittiği saniyede o soketi serbest bırakın:
    // PDO için bağlantıyı sonlandır:
    $pdo = null;
    // MySQLi için:
    $mysqli->close();
  2. Kalıcı Bağlantıları (Persistent Connections) Kapatın: Ne yaptığınızı ve PHP-FPM havuzunun sınırlarını çok iyi bilmiyorsanız, PDO::ATTR_PERSISTENT seçeneğini asla true yapmayın! Bırakın her istek işi bitince temiz bir şekilde kapansın.
  3. wait_timeout Değerini Düşürün: MySQL varsayılan olarak boşta bekleyen (Sleep) bir bağlantıyı tam 28.800 saniye (8 saat!) boyunca açık tutar! Bu tam bir israftır. my.cnf dosyasında bu süreyi makul bir seviyeye indirin:
    wait_timeout = 60
    interactive_timeout = 60
    Bu ayar sayesinde 60 saniye boyunca hiçbir sorgu çalıştırmayan yetim bağlantılar MySQL tarafından otomatik olarak sonlandırılır; vanalar kendi kendine kapanır.
  4. Bağlantı Havuzlayıcı (Connection Pooler - ProxySQL): Çok yüksek trafikli sistemlerde PHP ile MySQL arasına ProxySQL yerleştirin. ProxySQL, 10.000 web isteğini arkadaki sadece 50 adet MySQL bağlantısı üzerinden paylaştırarak sıfır gecikmeyle yönetir.

Hortumları açık bırakıp kaçan personelin oksijen tankını suçlaması ne kadar absürtse; bağlantıyı kapatmayıp sunucunun RAM'ine laf atan yazılımcı da o kadar mesleki körlük içindedir. Mühendislik, sınırsız kaynak tüketmek değil; eldeki kaynağı en yüksek verimle ve saygıyla yönetme sanatıdır.