Mert Özen Her Satırda Daha İleri
DevOps Linux ve Shell (geliştirici için) — #12

Sunucuda Sorun Giderme Playbook'u: "Site Açılmıyor"dan Root Cause'a Uçtan Uca

Mert Özen 14 Kas 2026 17 dk 2 görüntülenme
Sunucuda Sorun Giderme Playbook'u: "Site Açılmıyor"dan Root Cause'a Uçtan Uca

Site açılmadığında önce kesintiyi bitir, sonra sebebi bul: uptime, dmesg, free, df, systemctl --failed, journalctl -p err, ss ve top ile 60 saniyede kaynakları ele, sonra Nginx error log'u ve journal ile hipotezi daralt. Lab'da ürettiğim üç Bookstore vakasıyla deploy sonrası crash loop ve 502'yi, CPU çalan bir yedek job'ını ve süresi dolmuş certificate'ı gerçek çıktılarla çözüyoruz.

Sürüm notu: Bu yazıdaki komutlar Ubuntu Server 24.04.5 LTS (kernel 6.8, systemd 255, Nginx 1.24, OpenSSL 3.0.13) çalıştıran 2 CPU'lu, 1 GB RAM'li bir test VM'inde ve aynı ağdaki bir Ubuntu 24.04 istemcide; Bookstore.Api .NET SDK 10.0.402 ile derlenip ASP.NET Core 10.0.15 runtime'ıyla, vakadaki hatalı release .NET SDK 11.0.100 ile derlenerek denenmiştir.

Önce Tahmin Et

Öğleden sonra sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) yeni release'i deploy edildi. Deploy script'i şunu yazıp durdu:

Job for bookstore-api.service failed because the control process exited with error code.
See "systemctl status bookstore-api.service" and "journalctl -xeu bookstore-api.service" for details.

Birkaç saniye sonra müşteri hizmetlerinden Gül yazıyor: "Site açılmıyor." İstemciden bakıyorsun:

ada@my-pc:~$ curl -s -i http://10.20.30.10/api/books | head -n 4
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0 (Ubuntu)
Date: Sat, 14 Nov 2026 17:41:05 GMT
Content-Type: text/html

Sunucuya girip ilk refleksle bozuk service'leri listeliyorsun:

$ systemctl --failed
  UNIT LOAD ACTIVE SUB DESCRIPTION

0 loaded units listed.

Uygulama çalışmıyor ama systemd'ye göre bozuk bir service yok. Neden? Cevap ilk vakada.

Bu yazı serinin önceki on bir yazısını bir araya getiriyor: "site açılmıyor" ya da "site yavaş" şikâyeti geldiğinde hangi sırayla neye bakılacağını gösteren bir playbook ve lab'da gerçekten ürettiğim üç Bookstore vakası. Debugging ve Problem Çözme serisinin ilk yazısındaki yöntem burada da geçerli: gözlem, hipotez, tek bir deney. Fark, gözlem araçlarının sunucu komutları olması.

İlk 60 Saniye

Bir şikâyet geldiğinde, hipotez kurmadan önce sunucunun genel durumuna bakan sabit bir kontrol listesi işini hızlandırır. Bu listeyi Brendan Gregg'in Netflix'te yayımladığı "Linux Performance Analysis in 60,000 Milliseconds" yazısından uyarladım; o yazı performansa odaklanıyor, ben service ve log kontrollerini ekledim. Yukarıdaki 502'den yirmi saniye sonra çalıştırdım:

$ uptime
 17:41:26 up 4 min,  1 user,  load average: 0.83, 1.07, 0.56
$ sudo dmesg -T | tail -n 3
[Sat Nov 14 17:37:36 2026] loop0: detected capacity change from 0 to 8
[Sat Nov 14 17:37:40 2026] audit: type=1400 audit(1794677859.916:120): apparmor="STATUS" operation="profile_replace" info="same as current profile, skipping" profile="unconfined" name="/usr/lib/snapd/snap-confine" pid=919 comm="apparmor_parser"
[Sat Nov 14 17:37:42 2026] systemd-journald[313]: /var/log/journal/7a5216d613204956b9683a4a5dcfa072/user-1000.journal: Journal file uses a different sequence number ID, rotating.
$ free -h
               total        used        free      shared  buff/cache   available
Mem:           960Mi       289Mi       536Mi       1.0Mi       277Mi       670Mi
Swap:             0B          0B          0B
$ df -h / /var/log
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        19G  2.6G   16G  15% /
/dev/vda1        19G  2.6G   16G  15% /
$ systemctl --failed
  UNIT LOAD ACTIVE SUB DESCRIPTION

0 loaded units listed.
$ journalctl -p err -b --since -15min
Nov 14 17:41:02 bookstore-dev systemd[1]: Failed to start bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
Nov 14 17:41:13 bookstore-dev systemd[1]: Failed to start bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
Nov 14 17:41:24 bookstore-dev systemd[1]: Failed to start bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
$ sudo ss -tlnp
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=784,fd=7),("nginx",pid=782,fd=7),("nginx",
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=855,fd=3),("systemd",pid=1,fd=91))
LISTEN 0      4096      127.0.0.54:53        0.0.0.0:*    users:(("systemd-resolve",pid=495,fd=17))
LISTEN 0      4096   127.0.0.53%lo:53        0.0.0.0:*    users:(("systemd-resolve",pid=495,fd=15))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=855,fd=4),("systemd",pid=1,fd=92))
$ top -bn1 | head -n 5
top - 17:41:27 up 4 min,  1 user,  load average: 0.83, 1.07, 0.56
Tasks: 124 total,   1 running, 123 sleeping,   0 stopped,   0 zombie
%Cpu(s):  7.1 us,  7.1 sy,  0.0 ni, 85.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :    960.1 total,    534.0 free,    291.9 used,    277.5 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    668.2 avail Mem

Her satır bir soruyu eler. uptime: makine yeni mi açıldı, load anormal mi (onuncu yazı)? Burada ikisi de hayır. dmesg: kernel bir şey söylüyor mu, OOM (Out Of Memory; belleğin tükenmesi) killer, disk hatası, network kartı? Son satırlar açılıştan kalma, yeni bir şey yok. free ve df: bellek ya da disk tükenmiş mi? Hayır. systemctl --failed: boş. journalctl -p err: işte ilk iz, 11 saniyede bir tekrarlanan "Failed to start bookstore-api.service". ss -tlnp: Nginx 80'de, ama 5000'de kimse dinlemiyor; sekizinci yazıda bu listede 127.0.0.1:5000 satırı vardı. top: CPU boşta.

Altmış saniyede kaynak sorunu olmadığını, uygulamanın ayakta olmadığını ve systemd'nin onu tekrar tekrar başlatmaya çalıştığını öğrendik. Kontrol listesinin değeri bulduklarından çok eledikleri: artık disk, bellek ya da network'e bakmıyoruz.

Vaka 1: Deploy Sonrası 502

502'nin Nginx tarafındaki karşılığı ve service'in durumu:

$ sudo tail -n 1 /var/log/nginx/bookstore.error.log
2026/11/14 17:41:05 [error] 782#782: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 10.20.30.5, server: _, request: "GET /api/books HTTP/1.1", upstream: "http://127.0.0.1:5000/api/books", host: "10.20.30.10"
$ systemctl status bookstore-api --no-pager -n 0
● bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel)
     Loaded: loaded (/etc/systemd/system/bookstore-api.service; enabled; preset: enabled)
     Active: activating (auto-restart) (Result: exit-code) since Sat 2026-11-14 17:41:34 UTC; 4s ago
    Process: 2448 ExecStart=/usr/bin/dotnet /opt/bookstore/current/Bookstore.Api.dll (code=exited, status=150)
   Main PID: 2448 (code=exited, status=150)
        CPU: 337ms
$ systemctl show -p NRestarts bookstore-api
NRestarts=3

Tahminin cevabı Active satırında: activating (auto-restart). Service çöküyor, Restart=always onu 10 saniye sonra yeniden başlatıyor ve iki deneme arasında durumu "failed" değil "yeniden başlamayı bekliyor". systemctl --failed yalnızca vazgeçilmiş unit'leri gösterir; crash loop'taki bir service orada görünmez. Beşinci yazıda bunun sebebini görmüştük: systemd'nin varsayılan sınırı 10 saniyede 5 başlatma, bizim unit'te iki deneme arası 10 saniye, sınıra hiç ulaşılmıyor.

Nginx tarafında üç farklı 502 mesajı üç farklı yeri gösterir. 111: Connection refused: upstream adresinde dinleyen yok, ya process ölü ya da başka bir port'ta (sekizinci yazıdaki yanlış port vakası). upstream timed out: process dinliyor ama cevap vermiyor, takılmış ya da çok yavaş. upstream prematurely closed connection: process isteği alıp cevap vermeden bağlantıyı kapattı, genelde istek sırasında çöktü. Bizimki birincisi ve ss çıktısıyla uyumlu. Sırada process'in neden ölü olduğu:

$ journalctl -u bookstore-api -b -n 18 --no-pager -o cat
...
You must install or update .NET to run this application.
App: /opt/bookstore/releases/2026-11-14-7/Bookstore.Api.dll
Architecture: x64
Framework: 'Microsoft.NETCore.App', version '11.0.0' (x64)
.NET location: /usr/lib/dotnet/
The following frameworks were found:
  10.0.15 at [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
Learn more:
https://aka.ms/dotnet/app-launch-failed
To install missing framework, download:
https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=11.0.0&arch=x64&rid=ubuntu.24.04-x64&os=ubuntu.24.04
bookstore-api.service: Main process exited, code=exited, status=150/n/a
bookstore-api.service: Failed with result 'exit-code'.
Failed to start bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
$ dotnet --list-runtimes
Microsoft.AspNetCore.App 10.0.15 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 10.0.15 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
$ readlink /opt/bookstore/current
/opt/bookstore/releases/2026-11-14-7

Root cause net: yeni release net11.0 hedefliyor. .NET 11 10 Kasım'da yayımlandı, Emre bir branch'te geçişi denedi, branch merge edildi ve CI yeni build'i deploy etti; sunucuda ise yalnızca dokuzuncu yazıda kurduğumuz .NET 10 runtime'ı var. Çıkış kodu 150, .NET host'unun "framework bulunamadı" hata kodunun (0x80008096) son byte'ı; uygulama koduna hiç gelinmedi, bu yüzden uygulama log'u yok, yalnızca host'un mesajı var.

Production'da sıra önce kesintiyi bitirmek, sonra düzeltmek. Release klasörleri ve current symlink'i (ikinci yazı) tam bu an için var; geri dönüş bir symlink değişikliği:

$ ls /opt/bookstore/releases/ | tail -n 2
2026-11-14-6
2026-11-14-7
$ sudo -u deploy ln -sfn /opt/bookstore/releases/2026-11-14-6 /opt/bookstore/current
$ sudo -u deploy sudo /usr/bin/systemctl restart bookstore-api
$ systemctl is-active bookstore-api
active
ada@my-pc:~$ curl -s -o /dev/null -w "%{http_code}\n" http://10.20.30.10/api/books
200

Site geri geldi. Kalıcı düzeltme ayrı bir karar: ya sunucuya aspnetcore-runtime-11.0'ı kurmak (dokuzuncu yazıdaki yollarla) ya da projeyi .NET 10'da tutmak. Bu kararı gece yarısı sunucuda değil, ertesi gün ekipte ver. 2026-11-14-7 klasörünü de silme: hangi build'in sorun çıkardığını incelemek için kanıt.

Crash Loop'u Görünür Yapmak

Bu vakada systemd'nin sonsuz denemesi sorunu gizledi: "failed" bir service olsaydı monitoring onu görürdü. Unit'in [Unit] bölümüne başlatma sınırı ekledim (repoda deploy/linux/systemd/bookstore-api.service):

[Unit]
Description=Bookstore API (ASP.NET Core on Kestrel)
After=network.target
StartLimitIntervalSec=300
StartLimitBurst=5

5 dakika içinde 5 başlatma. RestartSec=10 ile bu yaklaşık bir dakikalık bir pencere: geçici bir sorunda (veritabanı birkaç saniye kapalı) uygulama yine kendini toparlar, ama açılışta her seferinde çöken bir release bir dakika sonra failed olur. Aynı hatalı release'i yeniden deploy edip 75 saniye bekledim:

$ systemctl --failed
  UNIT                  LOAD   ACTIVE SUB    DESCRIPTION
● bookstore-api.service loaded failed failed Bookstore API (ASP.NET Core on Kestrel)

Legend: LOAD   → Reflects whether the unit definition was properly loaded.
        ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
        SUB    → The low-level unit activation state, values depend on unit type.

1 loaded units listed.
$ journalctl -u bookstore-api -n 3 -o cat
bookstore-api.service: Start request repeated too quickly.
bookstore-api.service: Failed with result 'exit-code'.
Failed to start bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).
$ sudo -u deploy ln -sfn /opt/bookstore/releases/2026-11-14-6 /opt/bookstore/current
$ sudo -u deploy sudo /usr/bin/systemctl restart bookstore-api; echo "exit=$?"
Job for bookstore-api.service failed because the control process exited with error code.
See "systemctl status bookstore-api.service" and "journalctl -xeu bookstore-api.service" for details.
exit=1

Artık systemctl --failed sorunu gösteriyor ve monitoring bir unit'in failed olmasını kolayca yakalayabilir. Ama yeni bir tuzak çıktı: geri dönüşten sonraki restart da reddedildi. Sınır elle yapılan başlatmaları da sayıyor; journal'da yine "Start request repeated too quickly" var. Sayaç, aralık dolana kadar ya da systemctl reset-failed çalışana kadar sıfırlanmıyor. deploy kullanıcısının sudo yetkisi yalnızca restart ve status olduğu için, sudoers'a (üçüncü yazı) bu tek komutu ekledim:

deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart bookstore-api, /usr/bin/systemctl status bookstore-api, /usr/bin/systemctl reset-failed bookstore-api
$ sudo visudo -cf /tmp/bookstore-deploy
/tmp/bookstore-deploy: parsed OK
$ sudo -u deploy sudo /usr/bin/systemctl reset-failed bookstore-api
$ sudo -u deploy sudo /usr/bin/systemctl restart bookstore-api
$ systemctl is-active bookstore-api
active
ada@my-pc:~$ curl -s -o /dev/null -w "%{http_code}\n" http://10.20.30.10/api/books
200

Geri dönüş adımı artık ln -sfn, reset-failed, restart. Bunu deploy script'ine koy ve bir adım daha ekle: restart'tan sonra health endpoint'ini kontrol et, cevap gelmezse otomatik olarak önceki release'e dön. Web Server'lar ve Hosting serisinin systemd yazısındaki deploy script'i bu smoke test'i yapıyor, Bash Scripting serisinin gerçek script yazısı da hata yönetimiyle birlikte yazıyor. Unit'e OnFailure= ile failed olduğunda çalışacak bir bildirim unit'i bağlamak da mümkün.

Vaka 2: "Site Yavaş"

İkinci şikâyet daha belirsiz: site açılıyor ama yavaş. Önce ölç, çünkü "yavaş" bir sayı değil. Networking ve HTTP serisinin yavaş istek yazısındaki curl -w ile 60 ardışık istek gönderip süreleri sıraladım:

ada@my-pc:~$ for i in $(seq 1 60); do curl -s -o /dev/null -w "%{time_total}\n" http://10.20.30.10/api/books; done | sort -n | awk '{t[NR]=$1; s+=$1} END {printf "n=%d avg=%.3fs p50=%.3fs p95=%.3fs max=%.3fs\n", NR, s/NR, t[int(NR*0.5)], t[int(NR*0.95)], t[NR]}'
n=60 avg=0.038s p50=0.029s p95=0.073s max=0.298s

Normalde aynı ölçüm p50 3 ms ve p95 5–17 ms veriyor; şimdi p50 29 ms, en yavaş istek 300 ms. Sunucuda ilk 60 saniye listesi bu sefer başka bir yeri gösterdi:

$ uptime
 17:48:47 up 12 min,  1 user,  load average: 1.71, 1.20, 0.77
$ top -bn1 -w 110 -o %CPU | sed -n '1,3p;7,10p'
top - 17:48:48 up 12 min,  1 user,  load average: 1.71, 1.20, 0.77
Tasks: 117 total,   1 running, 116 sleeping,   0 stopped,   0 zombie
%Cpu(s): 91.7 us,  8.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   3961 booksto+  20   0  281576 109276   2212 S 120.0  11.1   0:22.22 xz
   3463 booksto+  20   0  261.2g  83120  64988 S  40.0   8.5   0:20.63 dotnet
   4064 ada       20   0   15116   5472   3348 R   6.7   0.6   0:00.10 top
$ ps -o pid,user,unit,%cpu,cmd -C xz
    PID USER     UNIT                            %CPU CMD
   3961 booksto+ bookstore-db-backup.service      138 xz -T0 -3 -c /var/lib/bookstore/backup/BookstoreDb.bak
$ systemctl status bookstore-db-backup --no-pager -n 0 | head -n 8
● bookstore-db-backup.service - Nightly BookstoreDb backup
     Loaded: loaded (/etc/systemd/system/bookstore-db-backup.service; static)
     Active: activating (start) since Sat 2026-11-14 17:48:31 UTC; 16s ago
TriggeredBy: ● bookstore-db-backup.timer
   Main PID: 3959 (bookstore-db-ba)
      Tasks: 4 (limit: 1106)
     Memory: 105.7M (peak: 105.7M)
        CPU: 22.678s

CPU'nun boşta kısmı sıfır, wa sıfır: I/O değil CPU. En çok CPU'yu xz kullanıyor, iki core'un %120'si. ps'in unit sütunu process'in hangi systemd unit'ine ait olduğunu söylüyor: beşinci yazıdaki gece yedeği. Birisi yedeği daha küçük olsun diye xz -T0 ile (bütün core'ları kullanarak) sıkıştıracak şekilde değiştirmiş. Lab'da yedeği elle başlattım; gerçek hayatta bu, timer'ın saati kaydığında ya da büyüyen yedek sabaha, trafiğin arttığı saate sarktığında olur. Yine lab'da veritabanı olmadığı için yedek script'i burada SQL Server dump'ı yerine 128 MB'lık bir dosyayı sıkıştırıyor; CPU'ya etkisi aynı.

Yedeği durdurmak bir çözüm değil, öncelik vermek çözüm. systemd'de her service kendi cgroup'unda çalışıyor ve CPU payı CPUWeight= ile ayarlanıyor (varsayılan 100). Yedek unit'ine iki satır ekledim (repoda deploy/linux/systemd/bookstore-db-backup.service):

# arka plan işi: CPU meşgulken bookstore-api'ye yol ver
Nice=10
CPUWeight=20
$ systemctl show -p Nice -p CPUWeight bookstore-db-backup
CPUWeight=20
Nice=10
ada@my-pc:~$ for i in $(seq 1 60); do curl ... ; done | sort -n | awk ...
n=60 avg=0.008s p50=0.006s p95=0.012s max=0.082s
$ top -bn1 -w 110 -o %CPU | sed -n "7,9p"
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   6540 booksto+  30  10  281576 106380   2216 S 156.2  10.8   0:22.22 xz
   6659 ada       20   0   15116   5476   3348 R  12.5   0.6   0:00.13 top
$ cat /sys/fs/cgroup/system.slice/cgroup.subtree_control
cpu memory pids
$ cat /sys/fs/cgroup/system.slice/bookstore-db-backup.service/cpu.weight /sys/fs/cgroup/system.slice/bookstore-api.service/cpu.weight
20
100

Yedek hâlâ çalışıyor ve hâlâ CPU'nun %156'sını kullanıyor, ama istekler normale döndü: p50 6 ms. CPUWeight bir sınır değil, rekabet anındaki pay: CPU boşsa yedek hepsini kullanır, Bookstore.Api bir şey istediğinde 100'e 20 oranında öncelik alır. Bu sistemde ayardan önce system.slice'ta cpu controller açık değildi (cgroup.subtree_control'de yalnızca memory pids vardı); CPUWeight kullanan ilk unit ile systemd onu açtı. Nice=10 aynı cgroup içindeki process'ler arasında işe yarıyor; ikisini birlikte yazmak hangi durumda olursa olsun yedeğin geri planda kalmasını sağlıyor. Yedeğin trafiğin düşük olduğu saatte bittiğini de ayrıca kontrol et.

Yavaşlığın bu lab'da üretmediğim iki sık sebebi daha var. Bellek yetmiyorsa swap kullanımı (onuncu yazıdaki vmstat'ta si/so) ve kernel'in geri alma işi istekleri yavaşlatır; MemoryHigh'a ulaşan bir service de yavaşlar. Bookstore'un SQL Server bağlantı havuzu tükenirse ise sunucu komutları normal görünür: CPU boşta, disk sakin, ama istekler havuzdan bağlantı beklerken zaman aşımına uğrar. Bunu Linux araçları değil uygulamanın metric'leri gösterir; dotnet-counters ile bakmak Debugging ve Problem Çözme serisinin hang ve yavaşlık yazısında.

Vaka 3: Certificate Süresi Dolmuş

Üçüncü şikâyet tarayıcıdan geliyor: "Bağlantınız gizli değil." Lab'da Nginx'e HTTPS için kendi lab CA'mın (certificate imzalayan otorite) imzaladığı ve 12 Kasım'da süresi dolan bir certificate koydum; istemci bu CA'ya güveniyor:

ada@my-pc:~$ curl -sS --cacert ~/bookstore-lab-ca.crt --resolve bookstore.test:443:10.20.30.10 https://bookstore.test/api/books
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
ada@my-pc:~$ openssl s_client -connect 10.20.30.10:443 -servername bookstore.test < /dev/null 2>/dev/null | openssl x509 -noout -subject -enddate
subject=CN = bookstore.test
notAfter=Nov 12 00:00:00 2026 GMT

Sunucu tarafında service'ler sağlıklı, port'lar açık, ilk 60 saniye listesi temiz çıkar; sorun TLS (HTTPS'in şifreleme protokolü) handshake'inde, istemcinin kararında. curl'ün mesajındaki SSL (TLS'in eski adı) de aynı şeyi söylüyor. openssl s_client sunucunun gönderdiği certificate'ı alıyor, openssl x509 içinden bitiş tarihini okuyor: iki gün önce dolmuş. Sunucuda Nginx'in hangi dosyayı kullandığını ve o dosyanın durumunu kontrol ettim:

$ sudo nginx -T 2>/dev/null | grep -E "^\s*ssl_certificate "
    ssl_certificate /etc/ssl/bookstore/bookstore.test.crt;
$ openssl x509 -in /etc/ssl/bookstore/bookstore.test.crt -noout -enddate -checkend 0; echo "exit=$?"
notAfter=Nov 12 00:00:00 2026 GMT
Certificate will expire
exit=1

nginx -T o anda yüklü olan bütün config'i basar; -checkend N certificate'ın N saniye içinde dolup dolmayacağını söyler ve dolacaksa 1 ile çıkar. Dikkat: süresi dolmuş certificate'la nginx -t hiçbir uyarı vermedi, Nginx onu sorunsuz yükledi. Certificate'ı yeniledim (lab CA'mla 90 günlük yenisini imzaladım) ve Nginx'i reload ettim:

$ sudo install -m 644 renewed.crt /etc/ssl/bookstore/bookstore.test.crt
$ sudo nginx -t && sudo systemctl reload nginx
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ openssl x509 -in /etc/ssl/bookstore/bookstore.test.crt -noout -enddate -checkend 1209600; echo "exit=$?"
notAfter=Feb 12 17:56:32 2027 GMT
Certificate will not expire
exit=0
ada@my-pc:~$ curl -sS --cacert ~/bookstore-lab-ca.crt --resolve bookstore.test:443:10.20.30.10 -o /dev/null -w "%{http_code}\n" https://bookstore.test/api/books
200

Gerçek bir sunucuda certificate Let's Encrypt'ten gelir ve yenilemeyi certbot'un timer'ı yapar; o zaman ilk bakılacak yer systemctl list-timers ve yenileme service'inin journal'ı olur. Yenileme genelde sessizce başarısız olur: port 80 firewall'da kapatılmış, DNS değişmiş ya da yeni dosya alınmış ama Nginx reload edilmemiş. Bu lab'da certbot yok, o kısmı çalıştırmadım; Let's Encrypt ile otomatik yenileme ve certificate'ları izleme SSL/TLS Certificate'ları ve HTTPS serisinde. -checkend 1209600 (14 gün) kalıcı çözümün ilk adımı: bunu bir monitoring kontrolü yap, süre 14 günün altına düşünce alarm versin.

Zaman Çizelgesi ve Kalıcı Düzeltme

Vaka kapandıktan sonra ne olduğunu sırasıyla yaz. journalctl farklı kaynakları tek bir zaman sırasında birleştirebiliyor; + "veya" demek:

$ journalctl -b --since 17:40:50 -o short-iso --no-pager _COMM=sudo + _SYSTEMD_UNIT=bookstore-api.service + UNIT=bookstore-api.service | grep -E "COMMAND=/usr/bin/(ln|systemctl)|Main process exited|Started|restart counter" | sed -E 's/ bookstore-dev / /' | cut -c1-150
2026-11-14T17:41:01+00:00 sudo[2269]:      ada : PWD=/home/ada ; USER=deploy ; COMMAND=/usr/bin/ln -sfn /opt/bookstore/releases/2026-11-14-7 /opt/book
2026-11-14T17:41:01+00:00 sudo[2272]:   deploy : PWD=/home/ada ; USER=root ; COMMAND=/usr/bin/systemctl restart bookstore-api
2026-11-14T17:41:02+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:41:12+00:00 systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 1.
2026-11-14T17:41:13+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:41:23+00:00 systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 2.
2026-11-14T17:41:24+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:41:34+00:00 systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 3.
2026-11-14T17:41:34+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:41:44+00:00 systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 4.
2026-11-14T17:41:46+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:41:56+00:00 systemd[1]: bookstore-api.service: Scheduled restart job, restart counter is at 5.
2026-11-14T17:41:57+00:00 systemd[1]: bookstore-api.service: Main process exited, code=exited, status=150/n/a
2026-11-14T17:42:06+00:00 sudo[2766]:      ada : PWD=/home/ada ; USER=deploy ; COMMAND=/usr/bin/ln -sfn /opt/bookstore/releases/2026-11-14-6 /opt/book
2026-11-14T17:42:06+00:00 sudo[2769]:   deploy : PWD=/home/ada ; USER=root ; COMMAND=/usr/bin/systemctl restart bookstore-api
2026-11-14T17:42:09+00:00 systemd[1]: Started bookstore-api.service - Bookstore API (ASP.NET Core on Kestrel).

Deploy 17:41:01'de, ilk çökme bir saniye sonra, Nginx'in ilk 502'si 17:41:05'te (error log), geri dönüş 17:42:06'da, servis 17:42:09'da ayakta: 68 saniyelik bir kesinti. Bu çizelgede kimin ne yaptığı sudo kayıtlarından, sistemin ne yaptığı systemd'den geliyor; kimse hafızasından yazmıyor. Debugging ve Problem Çözme serisinin log'lardan sebep bulma yazısı aynı çizelgeyi uygulama log'larıyla ve correlation ID ile kuruyor.

Kalıcı düzeltme, bu vakanın bir daha aynı şekilde olmamasını sağlayan değişikliktir, sunucuda yapılan elle düzeltme değil. Üç vakadan çıkanlar repoda: unit'te başlatma sınırı, deploy kullanıcısına reset-failed yetkisi, yedek unit'inde CPU önceliği. Repoda olmayanlar monitoring ve süreç işi: failed unit alarmı, p95 süre alarmı, certificate bitiş alarmı, CI'da hedef framework'ün sunucudaki runtime ile karşılaştırılması. Debugging ve Problem Çözme serisinin root cause analysis yazısı bu adımları blameless postmortem formatında yazmayı, ilerideki Observability serisi alarmların kendisini anlatıyor.

Üretimde Dikkat

Önce kesintiyi bitir, sonra sebebi anla. Geri dönüş yolu (önceki release, önceki config) hazır ve denenmiş olmalı; kesinti sırasında ilk kez denenen bir geri dönüş, ikinci vakadaki gibi kendi tuzağını çıkarabilir. Kanıtı geri dönüşten önce topla: systemctl status ve journal çıktısı birkaç saniye sürer ve sonradan yeniden üretilmesi zor olabilir.

Kontrol listesini yazılı tut. Bu yazıdaki ilk 60 saniye komutlarını ekibin runbook'una koy; stres altında ezberden çalışan biri sırayı atlar. Komutları bir script'e dökmek kolay, ama çıktıyı okumayı script yapamaz.

Tek bir şikâyet tek bir sebep demek değil. Vaka 1'de 502 vardı, ama aynı anda disk de dolu olabilirdi. Kontrol listesi bu yüzden "bulduğun ilk şeyde durma" demek için de var.

Sık Düşülen Tuzaklar

Production'da deneme yanılma ile birden çok şeyi aynı anda değiştirmek. Nginx'i restart edip, runtime'ı güncelleyip, config'i değiştirip site düzelirse hangisinin düzelttiğini bilemezsin ve diğer ikisi yeni bir sorun getirmiş olabilir. Bir hipotez, bir değişiklik, bir ölçüm.

systemctl --failed boş diye service'leri sağlıklı saymak. Crash loop'taki service orada görünmez; systemctl status'ta activating (auto-restart) ve NRestarts'a bak.

Uygulama log'u yok diye uygulamanın hiç çalışmadığını düşünmemek. Host seviyesindeki hatalar (eksik runtime, yanlış path, izin) uygulama koduna gelmeden olur; mesajları journal'da systemd'nin satırlarının arasındadır.

"Yavaş" şikâyetinde ölçmeden tahmin etmek. Önce sayı al (curl -w, Nginx log'undaki $request_time), sonra sunucuya bak, düzeltmeden sonra aynı ölçümü tekrarla.

Certificate sorununu sunucu metric'lerinde aramak. TLS hatası istemcide olur; sunucu sağlıklı görünür. Dışarıdan openssl s_client ve curl -v ile bak.

Küçük Bir Deneme

★☆☆ Kendi sunucunda ilk 60 saniye listesini çalıştır ve her komutun çıktısı için tek cümlelik bir not yaz: "normal" demek için neye baktın?

★★☆ Bir test sunucusunda unit'e var olmayan bir ExecStart path'i yaz, systemctl status, NRestarts ve systemctl --failed'ın StartLimitBurst olmadan ve onunla nasıl davrandığını karşılaştır. Nginx error log'unda hangi 502 mesajını görüyorsun?

★★★ Bookstore.Api'nin rapor endpoint'ini (/api/reports/sales?seconds=N) Nginx'in proxy_read_timeout'undan uzun süren bir istekle çağır ve 502 yerine 504 gördüğünü doğrula. Error log'daki mesajı bu yazıdaki üç mesajla karşılaştır ve bir zaman çizelgesi çıkar.

Ubuntu 24.04 üzerinde bir sunucuyu kurmayı, işletmeyi ve sorun çözmeyi gördük. Serinin son yazısında ileriye ve geriye bakıyoruz: Ubuntu 26.04 ile gelen sudo-rs ve Rust coreutils'in senin script'lerine etkisi ve SysV, ifconfig ve iptables döneminden kalma eski sunucuları okumak.