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.

Benzer Konular

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir