InnoDB Deadlock Tespiti ve Çözümü
Geceleri production veritabanında alarm zilleri çalmaya başladığında, log dosyasında “Deadlock found when trying to get lock” mesajını görmek her sysadmin’in kabusu haline gelir. InnoDB deadlock’ları, yanlış anlaşıldığında saatlerce uğraştıran ama doğru araçlarla birkaç dakikada teşhis edilebilen sorunlardır. Bu yazıda deadlock’ların anatomisini inceleyecek, gerçek dünya senaryoları üzerinden tespit ve çözüm yöntemlerini ele alacağız.
InnoDB Deadlock Nedir?
Deadlock, iki veya daha fazla transaction’ın birbirinin tuttuğu kilitleri beklemesi durumunda ortaya çıkar. Transaction A, Transaction B’nin kilitlediği bir kaynağı beklerken, Transaction B de Transaction A’nın kilitlediği kaynağı bekliyorsa sistem bir kilitlenme durumuna girer. InnoDB bu durumu otomatik olarak tespit eder ve “kurban” seçerek bir transaction’ı geri alır.
Meseleyi somutlaştıralım: E-ticaret sisteminde sipariş ve stok tablolarını aynı anda güncelleyen iki farklı süreç düşünün. Süreç 1 önce sipariş tablosunu kilitleyip ardından stok tablosuna geçmeye çalışırken, Süreç 2 önce stok tablosunu kilitleyip ardından sipariş tablosuna erişmeye çalışıyor. İşte bu klasik deadlock senaryosu.
Deadlock Tespiti: İlk Adımlar
InnoDB Status Çıktısını Okuma
Deadlock yaşandığında yapılacak ilk şey InnoDB’nin durum çıktısını incelemektir. Bu çıktı son yaşanan deadlock hakkında detaylı bilgi içerir.
mysql -u root -p -e "SHOW ENGINE INNODB STATUSG" | grep -A 50 "LATEST DETECTED DEADLOCK"
Bu komutun çıktısı şuna benzer bir şey gösterir:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-15 03:42:17 0x7f8b4c2a1700
*** (1) TRANSACTION:
TRANSACTION 421938, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 1523, OS thread handle 140234567890944, query id 89234 192.168.1.10 app_user updating
UPDATE orders SET status='processing' WHERE id=5001
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 156 page no 4 n bits 72 index PRIMARY of table `ecommerce`.`orders`
Çıktıyı okurken dikkat edilecek alanlar:
- TRANSACTION: Deadlock’a karışan transaction’ın ID’si ve ne kadar süredir aktif olduğu
- WAITING FOR THIS LOCK: Hangi kilidi beklediği
- HOLDS THE LOCK: Zaten hangi kilidi tuttuğu
- WE ROLL BACK TRANSACTION: InnoDB’nin kurban olarak seçtiği transaction
Deadlock Log Dosyasına Yazma
Varsayılan olarak InnoDB sadece son deadlock’u bellekte tutar. Production sistemlerde deadlock geçmişini kalıcı olarak saklamak için şu yapılandırma gereklidir:
# /etc/mysql/mysql.conf.d/mysqld.cnf veya /etc/my.cnf dosyasına ekle
[mysqld]
innodb_print_all_deadlocks = ON
log_error = /var/log/mysql/error.log
Yapılandırmayı aktif etmek için:
# Önce mevcut değeri kontrol et
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';"
# Runtime'da değiştir (kalıcı değil, test için)
mysql -u root -p -e "SET GLOBAL innodb_print_all_deadlocks = ON;"
# Servisi yeniden başlatmaya gerek kalmadan kalıcı yapmak için
mysql -u root -p -e "SET PERSIST innodb_print_all_deadlocks = ON;"
Performance Schema ile Deadlock İzleme
MySQL 8.0 ve üzerinde Performance Schema, deadlock analizi için çok daha zengin veri sunar:
mysql -u root -p << 'EOF'
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM
information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
EOF
Gerçek Dünya Senaryosu: E-Ticaret Deadlock Analizi
Diyelim ki production sisteminde şu hata tekrar tekrar geliyor:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Uygulama loglarını incelediğinizde iki farklı kod yolunun çakıştığını görüyorsunuz. Birincisi sipariş oluşturma akışı, ikincisi stok güncelleme akışı. Sorunu yeniden üretmek için:
# Terminal 1: Sipariş oluşturma simülasyonu
mysql -u root -p ecommerce << 'EOF'
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 101;
SELECT SLEEP(3);
UPDATE orders SET status = 'confirmed' WHERE order_id = 5001;
COMMIT;
EOF
# Terminal 2: Stok güncelleme simülasyonu (aynı anda çalıştır)
mysql -u root -p ecommerce << 'EOF'
START TRANSACTION;
UPDATE orders SET status = 'pending' WHERE order_id = 5001;
SELECT SLEEP(3);
UPDATE inventory SET stock = stock + 2 WHERE product_id = 101;
COMMIT;
EOF
Bu iki transaction aynı anda çalıştığında klasik deadlock oluşur. SHOW ENGINE INNODB STATUS çıktısında tam olarak hangi satırların kilitlendiğini göreceksiniz.
Kilit Bekleyenleri Canlı İzleme
# Aktif lock wait'leri sürekli izle
watch -n 1 'mysql -u root -p"SifreBuraya" -e "
SELECT
waiting_pid,
waiting_query,
blocking_pid,
blocking_query,
wait_age,
locked_table
FROM sys.innodb_lock_waits;" 2>/dev/null'
Bu komut her saniye güncellenen bir görünüm sunar ve deadlock öncesi durumu yakalamanıza yardımcı olur.
Deadlock Çözüm Stratejileri
1. Transaction Sırasını Tutarlı Hale Getirme
En etkili ve temiz çözüm, tüm kod yollarının aynı sırayla kilitleme yapmasını sağlamaktır. Yukarıdaki senaryoda her iki işlem de önce inventory ardından orders tablosunu güncellemeli:
mysql -u root -p ecommerce << 'EOF'
-- Her iki transaction için de aynı sıra: önce inventory, sonra orders
START TRANSACTION;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 101;
UPDATE orders SET status = 'confirmed' WHERE order_id = 5001;
COMMIT;
EOF
Bu basit değişiklik deadlock olasılığını dramatik biçimde düşürür.
2. Index Kullanımını Optimize Etme
InnoDB satır bazlı kilit kullandığı için kötü yazılmış sorgular beklenenden çok daha fazla satırı kilitleyebilir. Eksik index, tablo taramasına yol açar ve bu da gereksiz kilitlemelere neden olur.
# Problemli sorguyu analiz et
mysql -u root -p ecommerce -e "
EXPLAIN SELECT * FROM orders
WHERE customer_email = '[email protected]'
FOR UPDATE;"
Eğer type kolonunda ALL görüyorsanız, tablo taraması yapılıyor demektir. Index eklemek hem performansı artırır hem de deadlock riskini azaltır:
mysql -u root -p ecommerce -e "
ALTER TABLE orders ADD INDEX idx_customer_email (customer_email);
ANALYZE TABLE orders;"
3. Transaction Süresini Kısaltma
Uzun süre açık kalan transaction’lar deadlock riskini artırır. Uygulama kodunda transaction içinde yapılan harici API çağrıları, dosya işlemleri veya uzun hesaplamalar ciddi sorun yaratır.
# Uzun süre açık kalan transaction'ları tespit et
mysql -u root -p -e "
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_seconds,
trx_query,
trx_rows_locked,
trx_rows_modified
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
ORDER BY duration_seconds DESC;"
30 saniyeden uzun açık kalan transaction varsa bunlar hem deadlock hem de performans sorunlarının kaynağıdır. Uygulama geliştirme ekibine aktarılması gereken bir bulgudur.
4. SELECT … FOR UPDATE Kullanımını Gözden Geçirme
Uygulamalarda sık yapılan hata, okuma işlemleri için FOR UPDATE kullanmaktır. Bu kilitleri gereksiz yere artırır:
mysql -u root -p ecommerce << 'EOF'
-- Kötü: Sadece okuyacaksak FOR UPDATE gereksiz
START TRANSACTION;
SELECT stock FROM inventory WHERE product_id = 101 FOR UPDATE;
-- Uzun bir hesaplama...
COMMIT;
-- İyi: Sadece güncelleyeceksek FOR UPDATE kullan
START TRANSACTION;
SELECT stock FROM inventory WHERE product_id = 101;
-- Hesaplama sonucuna göre güncelleme gerekiyorsa:
UPDATE inventory SET stock = stock - 1 WHERE product_id = 101 AND stock > 0;
COMMIT;
EOF
5. Deadlock Sonrası Otomatik Yeniden Deneme
Uygulama katmanında deadlock’ları zarif biçimde ele almak gerekir. Bir shell script ile bu mantığı test edebilirsiniz:
#!/bin/bash
# deadlock_retry.sh - Deadlock durumunda otomatik yeniden deneme
MAX_RETRIES=3
RETRY_DELAY=0.5
DB_HOST="localhost"
DB_USER="app_user"
DB_PASS="sifre"
DB_NAME="ecommerce"
execute_with_retry() {
local query="$1"
local attempt=1
while [ $attempt -le $MAX_RETRIES ]; do
result=$(mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME"
-e "$query" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "Sorgu basarili (deneme $attempt)"
return 0
fi
# 1213 deadlock hata kodunu kontrol et
if echo "$result" | grep -q "1213|Deadlock"; then
echo "Deadlock tespit edildi, deneme $attempt/$MAX_RETRIES"
sleep $RETRY_DELAY
# Her denemede bekleme süresini artır
RETRY_DELAY=$(echo "$RETRY_DELAY * 2" | bc)
attempt=$((attempt + 1))
else
echo "Farkli bir hata: $result"
return 1
fi
done
echo "Maximum deneme sayisina ulasildi, islem basarisiz"
return 1
}
# Test
execute_with_retry "UPDATE orders SET status='processing' WHERE order_id=5001;"
Önleyici Tedbirler ve Monitoring
Deadlock Frekansını İzleme
Deadlock sayısını Prometheus veya benzeri bir monitoring sistemine aktarmak için şu sorguyu kullanabilirsiniz:
#!/bin/bash
# innodb_deadlock_monitor.sh
# Cron ile her 5 dakikada bir çalıştır
LOG_FILE="/var/log/mysql/deadlock_stats.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
DEADLOCK_COUNT=$(mysql -u monitor_user -p"monitor_pass" -N -e "
SELECT VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';" 2>/dev/null)
LOCK_TIMEOUTS=$(mysql -u monitor_user -p"monitor_pass" -N -e "
SELECT VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_lock_timeouts';" 2>/dev/null)
echo "$TIMESTAMP | Deadlocks: $DEADLOCK_COUNT | Lock Timeouts: $LOCK_TIMEOUTS" >> "$LOG_FILE"
# Son 5 dakikadaki artışı kontrol et (basit threshold)
PREV_COUNT=$(tail -2 "$LOG_FILE" | head -1 | awk -F'Deadlocks: ' '{print $2}' | awk '{print $1}')
DIFF=$((DEADLOCK_COUNT - PREV_COUNT))
if [ "$DIFF" -gt 10 ]; then
echo "UYARI: Son 5 dakikada $DIFF deadlock tespit edildi!" |
mail -s "MySQL Deadlock Alarmı" [email protected]
fi
InnoDB Lock Timeout Ayarı
Deadlock tespit mekanizması yanı sıra lock timeout değerini de yapılandırmak gerekir:
mysql -u root -p -e "
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SHOW VARIABLES LIKE 'innodb_deadlock_detect';"
# Timeout'u azalt (varsayılan 50 saniyedir, production'da genelde düşürülür)
mysql -u root -p -e "SET GLOBAL innodb_lock_wait_timeout = 10;"
# MySQL 8.0.18+ için deadlock detection'ı kapatma opsiyonu
# Yüksek yoğunluklu sistemlerde detection overhead'i azaltmak için
# mysql -u root -p -e "SET GLOBAL innodb_deadlock_detect = OFF;"
# Bu durumda sadece timeout mekanizması çalışır, dikkatli kullanın
innodb_lock_wait_timeout: Transaction bir kilidi bu kadar saniye bekledikten sonra otomatik olarak hata verir innodb_deadlock_detect: Aktifken InnoDB deadlock’ları proaktif olarak tespit eder, çok fazla thread varsa CPU maliyeti olabilir
Slow Query Log ile Korelasyon
Deadlock’lar genellikle yavaş sorgularla iç içe geçer. Slow query log’u etkinleştirerek aynı zaman dilimine düşen yavaş sorguları inceleyin:
# Slow query log konfigürasyonu
mysql -u root -p -e "
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL log_queries_not_using_indexes = ON;"
# Deadlock zamanlarını slow query logla karşılaştır
grep "LATEST DETECTED DEADLOCK" /var/log/mysql/error.log |
awk '{print $1, $2}' |
while read date time; do
echo "=== Deadlock: $date $time ==="
grep -A 5 "$date $time" /var/log/mysql/slow.log 2>/dev/null | head -20
done
Pt-deadlock-logger ile Profesyonel Takip
Percona Toolkit’in pt-deadlock-logger aracı deadlock’ları parse ederek bir tabloya kaydeder, bu sayede tarihsel analiz yapılabilir:
# Percona Toolkit kurulumu
apt-get install percona-toolkit # Debian/Ubuntu
# veya
yum install percona-toolkit # RHEL/CentOS
# Deadlock log tablosu oluştur
mysql -u root -p -e "
CREATE DATABASE IF NOT EXISTS percona_tools;
USE percona_tools;
CREATE TABLE IF NOT EXISTS deadlocks (
server varchar(128) NOT NULL,
ts timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
thread int unsigned NOT NULL,
txn_id bigint unsigned NOT NULL,
txn_time smallint unsigned NOT NULL,
user varchar(16) NOT NULL,
hostname varchar(64) NOT NULL,
ip varchar(16) NOT NULL,
db varchar(64) NOT NULL,
tbl varchar(64) NOT NULL,
idx varchar(64) NOT NULL,
lock_type varchar(16) NOT NULL,
lock_mode varchar(1) NOT NULL,
wait_hold varchar(1) NOT NULL,
victim tinyint unsigned NOT NULL,
query text NOT NULL,
PRIMARY KEY (server, ts, thread)
);"
# pt-deadlock-logger çalıştır
pt-deadlock-logger
--user=root
--password=sifre
--host=localhost
--dest D=percona_tools,t=deadlocks
--run-time=3600
--interval=10 &
# Kayıtlı deadlock'ları sorgula
mysql -u root -p percona_tools -e "
SELECT
ts,
db,
tbl,
query,
victim,
lock_mode
FROM deadlocks
ORDER BY ts DESC
LIMIT 20;"
Sık Karşılaşılan Deadlock Kalıpları
Gap Lock Deadlock’ları
InnoDB’nin REPEATABLE READ isolation level’ında kullandığı gap lock’lar beklenmedik deadlock’lara yol açabilir. Özellikle INSERT işlemlerinde görülür:
# Gap lock kaynaklı deadlock'ları tespit etmek için
mysql -u root -p -e "
SELECT
engine_lock_id,
engine_transaction_id,
object_schema,
object_name,
index_name,
lock_type,
lock_mode,
lock_status,
lock_data
FROM performance_schema.data_locks
WHERE lock_mode LIKE '%GAP%';"
Gap lock sorunlarını azaltmak için READ COMMITTED isolation level değerlendirilebilir, ancak bu replication consistency açısından dikkat gerektirir:
# Session bazlı isolation level değişikliği (test için)
mysql -u root -p -e "SET SESSION transaction_isolation = 'READ-COMMITTED';"
# Global değişiklik (dikkatli kullanın)
# mysql -u root -p -e "SET GLOBAL transaction_isolation = 'READ-COMMITTED';"
Sonuç
InnoDB deadlock’ları kaçınılmaz olmak zorunda değil. Doğru araçlarla sistematik bir yaklaşım izlendiğinde hem mevcut deadlock’ları hızla çözmek hem de yenilerinin önüne geçmek mümkün.
Özetlemek gerekirse: İlk adım her zaman SHOW ENGINE INNODB STATUS çıktısını okuyarak çakışan transaction’ların tam olarak hangi kaynakları beklediğini anlamak. innodb_print_all_deadlocks aktif edilerek tarihsel veri toplamak, Performance Schema sorguları ile canlı kilit durumunu izlemek analiz sürecini büyük ölçüde kolaylaştırır.
Uzun vadeli çözüm için transaction sırasını tutarlı hale getirmek, index eksikliklerini gidermek ve transaction süresini kısaltmak en etkili stratejilerdir. Uygulama katmanında deadlock hatası için exponential backoff ile yeniden deneme mantığı eklenmesi de sistemin dayanıklılığını artırır.
Production’da pt-deadlock-logger gibi profesyonel araçlarla sürekli monitoring kurulması, deadlock’ların uygulama hatalarına dönüşmeden tespit edilmesini sağlar. Monitoring, log analizi ve önleyici yapılandırma bir arada uygulandığında InnoDB deadlock’ları yönetilebilir ve çözülebilir bir sorun olmaktan öteye geçmez.
