awk ile Uygulama Performans Loglarından Yüzdelik Dilim ve Yanıt Süresi Dağılımı Analizi
Prodüksiyon ortamında bir servis yavaşladığında, ilk içgüdü genellikle ortalama yanıt sürelerine bakmak olur. Ama ortalama, sizi yanıltır. 999 isteğin 10ms’de tamamlandığı, birinin 10 saniye sürdüğü bir sistemde ortalama hâlâ “iyi” görünebilir. İşte bu yüzden yüzdelik dilimler (percentile) gerçek hayatta çok daha anlamlı. P95, P99 değerleri olmadan bir servisin gerçek performansını anlamak mümkün değil. Bu yazıda, production log dosyalarından sadece awk kullanarak nasıl anlamlı percentile analizleri çıkarabileceğinizi göstereceğim.
Neden awk?
Python, R, Elasticsearch, Grafana… Bunların hepsi harika araçlar. Ama bir müşteri şikayeti geldiğinde, SSH açıp log sunucusuna bağlandığınızda, elinizin altında sadece terminal varken ne yaparsınız? awk her Linux sisteminde var, bağımlılığı yok, büyük dosyalarda bile şaşırtıcı derecede hızlı çalışıyor. Birkaç satır awk kodu ile gigabaytlık log dosyalarını dakikalar içinde analiz edebilirsiniz.
Ayrıca şunu da söyleyeyim: awk öğrenmek sizi farklı bir seviyeye taşır. Sadece log analizi için değil, metin işlemenin her alanında. Yıllarca sistem yöneticiliği yapıp awk‘ı yüzeysel kullanan insanlar gördüm. O potansiyeli boşa harcamak yazık olur.
Log Formatını Tanımak
Önce ne tür loglarla çalışacağımızı netleştirelim. Nginx access log formatı tipik olarak şöyle görünür:
192.168.1.45 - - [12/Jan/2025:14:23:01 +0300] "GET /api/v2/users HTTP/1.1" 200 1423 0.045
192.168.1.12 - - [12/Jan/2025:14:23:02 +0300] "POST /api/v2/orders HTTP/1.1" 201 892 0.823
192.168.1.78 - - [12/Jan/2025:14:23:02 +0300] "GET /api/v2/products HTTP/1.1" 200 5421 0.012
Buradaki son alan (0.045, 0.823, 0.012) saniye cinsinden yanıt süresi. Nginx konfigürasyonunuzda $request_time ile bu değeri log formatına eklemeniz gerekiyor. Eğer henüz eklemediyseniz, şimdi tam zamanı.
Uygulama logları farklı formatlarda olabilir. Mesela bir Java uygulamasının custom log formatı:
2025-01-12 14:23:01 INFO RequestFilter - method=GET path=/api/users status=200 duration_ms=45 user_id=1234
2025-01-12 14:23:02 INFO RequestFilter - method=POST path=/api/orders status=201 duration_ms=823 user_id=5678
2025-01-12 14:23:03 INFO RequestFilter - method=GET path=/api/products status=200 duration_ms=12 user_id=9012
Her iki format için de örnekler vereceğim. Konsept aynı, sadece alan ayrıştırma değişiyor.
Temel Yanıt Süresi Çıkarımı
İlk adım, log dosyasından sadece yanıt sürelerini çıkarmak. Nginx formatı için:
awk '{print $NF}' /var/log/nginx/access.log | sort -n > /tmp/response_times.txt
$NF son alanı verir. Ama dikkat: Bazı log satırlarında son alan farklı bir şey olabilir. Daha güvenli bir yaklaşım:
awk '
/^[0-9]/ {
# Son alanın sayısal değer olduğunu kontrol et
if ($NF ~ /^[0-9]+.?[0-9]*$/) {
print $NF
}
}
' /var/log/nginx/access.log | sort -n > /tmp/response_times.txt
Java uygulama logu için duration_ms değerini çıkaralım:
awk '
/duration_ms=/ {
for (i=1; i<=NF; i++) {
if ($i ~ /^duration_ms=/) {
split($i, arr, "=")
print arr[2]
}
}
}
' /var/log/app/application.log | sort -n > /tmp/response_times.txt
Percentile Hesaplama Mantığı
Percentile hesaplamanın birkaç yöntemi var. En yaygın kullanılan “nearest rank” yöntemi şu şekilde çalışır: P95 için toplam veri sayısını 0.95 ile çarpıp en yakın tam sayıya yuvarlıyorsunuz. O indeksteki değer P95’iniz oluyor.
awk ile bu hesaplamayı tek geçişte yapmak için önce tüm değerleri bir diziye yüklemek gerekiyor:
awk '
BEGIN {
count = 0
}
{
values[count++] = $1 + 0
}
END {
if (count == 0) {
print "Veri bulunamadi"
exit
}
# Percentile hesaplama fonksiyonu
# Not: Dizi zaten sort -n ile siralandi
p50_idx = int(count * 0.50)
p75_idx = int(count * 0.75)
p90_idx = int(count * 0.90)
p95_idx = int(count * 0.95)
p99_idx = int(count * 0.99)
p999_idx = int(count * 0.999)
# Sinir kontrolu
if (p50_idx >= count) p50_idx = count - 1
if (p75_idx >= count) p75_idx = count - 1
if (p90_idx >= count) p90_idx = count - 1
if (p95_idx >= count) p95_idx = count - 1
if (p99_idx >= count) p99_idx = count - 1
if (p999_idx >= count) p999_idx = count - 1
printf "Toplam istek : %dn", count
printf "Minimum : %.3f msn", values[0]
printf "P50 (medyan) : %.3f msn", values[p50_idx]
printf "P75 : %.3f msn", values[p75_idx]
printf "P90 : %.3f msn", values[p90_idx]
printf "P95 : %.3f msn", values[p95_idx]
printf "P99 : %.3f msn", values[p99_idx]
printf "P99.9 : %.3f msn", values[p999_idx]
printf "Maksimum : %.3f msn", values[count-1]
}
' /tmp/response_times.txt
Bu scripti çalıştırdığınızda şuna benzer bir çıktı alırsınız:
Toplam istek : 48291
Minimum : 1.000 ms
P50 (medyan) : 34.000 ms
P75 : 89.000 ms
P90 : 187.000 ms
P95 : 342.000 ms
P99 : 891.000 ms
P99.9 : 2341.000 ms
Maksimum : 15823.000 ms
P50 ile P99 arasındaki farka bakın. Ortalama size muhtemelen 60-70ms gibi makul bir değer gösterirdi. Ama %1’lik kullanıcı kitleniz 891ms bekliyordu. Yüksek trafikli bir serviste bu, her 100 kullanıcıdan birinin kötü deneyim yaşaması demek.
Endpoint Bazında Analiz
Gerçek dünyada “genel P99 kötü” demek yeterli değil. Hangi endpoint problemli? Bunu bulmak için gruplama yapmanız gerekiyor:
awk '
{
# Nginx log formatı: IP - - [tarih] "METHOD /path HTTP/version" status bytes time
# $7 = path, $NF = response time
# Sorgu parametrelerini temizle
path = $7
gsub(/?.*/, "", path)
# Veriyi topla
times[path][counts[path]++] = $NF + 0
sum[path] += $NF + 0
}
END {
for (path in counts) {
n = counts[path]
# Basit bubble sort (kucuk veri setleri icin)
# Buyuk veri setleri icin once sort | awk yapisi tercih edin
do {
swapped = 0
for (i = 0; i < n-1; i++) {
if (times[path][i] > times[path][i+1]) {
tmp = times[path][i]
times[path][i] = times[path][i+1]
times[path][i+1] = tmp
swapped = 1
}
}
} while (swapped)
p95_idx = int(n * 0.95)
p99_idx = int(n * 0.99)
if (p95_idx >= n) p95_idx = n - 1
if (p99_idx >= n) p99_idx = n - 1
avg = sum[path] / n
printf "%-40s istek:%-6d avg:%-8.1f p95:%-8.1f p99:%.1fn",
path, n, avg, times[path][p95_idx], times[path][p99_idx]
}
}
' /var/log/nginx/access.log | sort -k5 -t: -rn | head -20
Önemli not: Bu yaklaşım, her endpoint için tüm değerleri bellekte tutar. Çok sayıda farklı path ve milyonlarca log satırı varsa bellek sorunuyla karşılaşabilirsiniz. Bu durumda, önce endpoint’e göre gruplayıp sonra analiz etmek daha sağlıklı:
# Önce endpoint ve süreleri çıkar, endpoint'e göre sırala
awk '{gsub(/?.*/, "", $7); print $7, $NF}' /var/log/nginx/access.log
| sort -k1,1 -k2,2n
| awk '
BEGIN { current_path = ""; count = 0 }
{
if ($1 != current_path) {
if (current_path != "" && count > 0) {
p95_idx = int(count * 0.95)
p99_idx = int(count * 0.99)
if (p95_idx >= count) p95_idx = count - 1
if (p99_idx >= count) p99_idx = count - 1
printf "%-45s cnt:%-6d p95:%-8.3f p99:%.3fn",
current_path, count, values[p95_idx], values[p99_idx]
}
current_path = $1
count = 0
delete values
}
values[count++] = $2 + 0
}
END {
if (current_path != "" && count > 0) {
p95_idx = int(count * 0.95)
p99_idx = int(count * 0.99)
if (p95_idx >= count) p95_idx = count - 1
if (p99_idx >= count) p99_idx = count - 1
printf "%-45s cnt:%-6d p95:%-8.3f p99:%.3fn",
current_path, count, values[p95_idx], values[p99_idx]
}
}
'
Bu yaklaşım çok daha bellek verimli çünkü her seferinde sadece bir endpoint’in verilerini tutuyor.
Zaman Bazlı Percentile Analizi
“Dün gece saat kaçta performans düştü?” sorusuna cevap vermek için saatlik percentile analizi çok işe yarıyor:
awk '
{
# [12/Jan/2025:14:23:01 +0300] formatından saat çıkar
match($4, /([0-9]{2}:[0-9]{2})/, arr)
# Sadece saati al (dakika bazında cok granüler olur)
split(arr[1], time_parts, ":")
hour = time_parts[1]
response_time = $NF + 0
data[hour][hour_count[hour]++] = response_time
hour_sum[hour] += response_time
}
END {
print "Saat | Istek | Ort(ms) | P95(ms) | P99(ms)"
print "-----+-------+---------+---------+--------"
n_hours = asorti(hour_count, sorted_hours)
for (h = 1; h <= n_hours; h++) {
hour = sorted_hours[h]
n = hour_count[hour]
# Siralama (bu ornekte kucuk veri seti varsayilmistir)
for (i = 0; i < n-1; i++) {
for (j = i+1; j < n; j++) {
if (data[hour][i] > data[hour][j]) {
tmp = data[hour][i]
data[hour][i] = data[hour][j]
data[hour][j] = tmp
}
}
}
p95_idx = int(n * 0.95)
p99_idx = int(n * 0.99)
if (p95_idx >= n) p95_idx = n - 1
if (p99_idx >= n) p99_idx = n - 1
printf " %s | %5d | %7.1f | %7.1f | %6.1fn",
hour, n, hour_sum[hour]/n, data[hour][p95_idx], data[hour][p99_idx]
}
}
' /var/log/nginx/access.log
Historgram Çıktısı ile Dağılım Analizi
Sayılar bazen yeterli değil, dağılımı görmek istiyorsunuz. ASCII histogram her zaman işe yarıyor:
sort -n /tmp/response_times.txt | awk '
BEGIN {
bucket_size = 50 # 50ms araliklar
max_bar_width = 50
}
{
bucket = int($1 / bucket_size) * bucket_size
count[bucket]++
total++
if ($1 > max_val) max_val = $1
}
END {
# En yuksek bucket sayisini bul (bar genisligini normalize etmek icin)
for (b in count) {
if (count[b] > max_count) max_count = count[b]
}
printf "nYanit Suresi Dagilimi (Bucket: %dms)n", bucket_size
printf "%-12s %-8s %-8s %sn", "Aralik(ms)", "Sayi", "Yuzde", "Grafik"
printf "%-12s %-8s %-8s %sn", "----------", "----", "------", "------"
for (b = 0; b <= max_val; b += bucket_size) {
if (count[b] > 0) {
pct = count[b] / total * 100
bar_len = int(count[b] / max_count * max_bar_width)
bar = ""
for (i = 0; i < bar_len; i++) bar = bar "#"
printf "%-12s %-8d %5.1f%% |%sn",
b "-" (b+bucket_size-1), count[b], pct, bar
}
}
printf "nToplam: %d istekn", total
}
'
Çıktı şuna benzer bir şey olacak:
Yanit Suresi Dagilimi (Bucket: 50ms)
Aralik(ms) Sayi Yuzde Grafik
---------- ---- ------ ------
0-49 18432 38.2% |##################################################
50-99 12891 26.7% |###################################
100-149 8234 17.1% |######################
150-199 4123 8.5% |###########
200-249 2341 4.8% |######
250-299 1203 2.5% |###
300-499 891 1.8% |##
500+ 401 0.8% |#
Bu dağılıma baktığınızda hemen görüyorsunuz: Uzun kuyruk var. O %0.8’lik 500ms üzeri istekler neden bu kadar uzun sürüyor?
Yavaş İstekleri Filtrelemek ve Örüntü Bulmak
P99 değerinizi buldunuz, şimdi o yavaş isteklerin kim olduğunu bulmanız gerekiyor:
awk '
{
response_time = $NF + 0
# 500ms üzeri istekleri filtrele (nginx formatı)
if (response_time > 0.500) {
gsub(/?.*/, "", $7)
path = $7
method = $6
gsub(/"/, "", method)
status = $9
slow_paths[path]++
slow_methods[method " " path]++
}
}
END {
print "=== En Cok Yavaslik Yasayan Endpointler ==="
n = asorti(slow_paths, sorted, "@val_num_desc")
for (i = 1; i <= n && i <= 10; i++) {
printf "%5d istek | %sn", slow_paths[sorted[i]], sorted[i]
}
}
' /var/log/nginx/access.log
HTTP Status Kodlarına Göre Percentile Karşılaştırması
Başarılı istekler ile hata alan isteklerin yanıt süreleri farklı mı? Genellikle 500 hataları çok daha hızlı dönüyor (circuit breaker, early fail) ya da çok daha yavaş (timeout). Bunu görmek için:
awk '
{
status = $9
response_time = $NF + 0
# Sadece 2xx, 4xx, 5xx gruplarını al
if (status ~ /^2/) status_group = "2xx"
else if (status ~ /^4/) status_group = "4xx"
else if (status ~ /^5/) status_group = "5xx"
else next
data[status_group][group_count[status_group]++] = response_time
}
END {
for (group in group_count) {
n = group_count[group]
# Bubble sort
for (i = 0; i < n-1; i++) {
for (j = i+1; j < n; j++) {
if (data[group][i] > data[group][j]) {
tmp = data[group][i]
data[group][i] = data[group][j]
data[group][j] = tmp
}
}
}
p50 = data[group][int(n*0.50)]
p95 = data[group][int(n*0.95) < n ? int(n*0.95) : n-1]
p99 = data[group][int(n*0.99) < n ? int(n*0.99) : n-1]
printf "HTTP %s -> istek:%d P50:%.3fs P95:%.3fs P99:%.3fsn",
group, n, p50, p95, p99
}
}
' /var/log/nginx/access.log
Gerçek Dünya Senaryosu: Gece Yarısı İncidenti
Şöyle bir senaryo: Gece 02:00’de monitoring alarmı çaldı, P99 latency 2 saniyeyi geçti. Sabah geldiğinizde postmortem için log analizi yapmanız gerekiyor.
#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
DATE="12/Jan/2025"
START_HOUR="01"
END_HOUR="03"
echo "=== Incident Analizi: $DATE $START_HOUR:00 - $END_HOUR:00 ==="
echo ""
# Sadece incident zamanındaki logları filtrele ve analiz et
grep "[$DATE:" "$LOG_FILE" | awk -v sh="$START_HOUR" -v eh="$END_HOUR" '
{
# Saati çıkar
match($4, /:([0-9]{2}):/, arr)
hour = arr[1]
if (hour < sh || hour > eh) next
response_time = $NF + 0
gsub(/?.*/, "", $7)
path = $7
all_times[total++] = response_time
path_times[path][path_count[path]++] = response_time
}
END {
# Genel istatistik
# Siralama
for (i = 0; i < total-1; i++) {
for (j = i+1; j < total; j++) {
if (all_times[i] > all_times[j]) {
tmp = all_times[i]; all_times[i] = all_times[j]; all_times[j] = tmp
}
}
}
printf "Toplam istek : %dn", total
printf "P50 : %.3f snn", all_times[int(total*0.50)]
printf "P95 : %.3f snn", all_times[int(total*0.95) < total ? int(total*0.95) : total-1]
printf "P99 : %.3f snn", all_times[int(total*0.99) < total ? int(total*0.99) : total-1]
printf "MAX : %.3f snn", all_times[total-1]
print "n=== En Yavas Endpointler (P99 bazli) ==="
for (path in path_count) {
n = path_count[path]
if (n < 10) continue # Az ornekli endpointleri atla
for (i = 0; i < n-1; i++) {
for (j = i+1; j < n; j++) {
if (path_times[path][i] > path_times[path][j]) {
tmp = path_times[path][i]
path_times[path][i] = path_times[path][j]
path_times[path][j] = tmp
}
}
}
p99_idx = int(n*0.99)
if (p99_idx >= n) p99_idx = n-1
printf "P99:%.3fs cnt:%d %sn", path_times[path][p99_idx], n, path
}
}
' | sort -t: -k2 -rn | head -15
Bu script size o gece yarısı tam olarak hangi endpoint’in ne zaman patlak verdiğini gösterecek. Postmortem toplantısında “galiba şuydu” yerine somut verilerle geleceksiniz.
Performans Trendini İzlemek: Günlük Karşılaştırma
Son olarak, günlük P95/P99 trendini takip etmek için küçük bir script:
#!/bin/bash
# Birden fazla log dosyasını karşılaştır (nginx daily rotation varsayımı)
for log_file in /var/log/nginx/access.log.{7..1} /var/log/nginx/access.log; do
if [ ! -f "$log_file" ]; then continue; fi
date_label=$(stat -c %y "$log_file" | cut -d' ' -f1)
result=$(awk '
{
val = $NF + 0
if (val > 0 && val < 100) {
data[count++] = val
}
}
END {
if (count < 2) { print "VERI_YOK"; exit }
for (i=0; i<count-1; i++)
for (j=i+1; j<count; j++)
if (data[i]>data[j]) { t=data[i]; data[i]=data[j]; data[j]=t }
p95 = data[int(count*0.95) < count ? int(count*0.95) : count-1]
p99 = data[int(count*0.99) < count ? int(count*0.99) : count-1]
printf "istek:%d p95:%.3f p99:%.3f", count, p95, p99
}
' "$log_file")
echo "$date_label | $result | $(basename $log_file)"
done
Dikkat Edilmesi Gereken Noktalar
Tüm bu güçlü araçları kullanırken aklınızda tutmanız gereken birkaç pratik nokta var:
- Bellek kullanımı:
awktüm değerleri RAM’de tutar. 10 milyon satırlık log dosyasında bellek dolabilir. Büyük dosyalar için öncesort, sonraawkpipeline’ı kullanın.
- Sıralama varsayımı: Percentile hesaplamalarının çoğu verinin sıralandığını varsayar.
sort -nile önceden sıralamanız kritik.
- Log rotation: Cron ile çalışan bir analiz scriptiniz varsa,
logrotatezamanlamasını göz önünde bulundurun. Yarım saat önceki loglar zaten rotate edilmiş olabilir.
- Zaman dilimi: Sunucularınız UTC’de log yazıyor ama siz Türkiye saatiyle çalışıyorsanız, saat filtrelerinde +3 farkı hesaplayın.
- Yeterli örnek sayısı: Bir endpoint’e sadece 5 istek geldiyse P99 istatistiksel olarak anlamsız. En az 100-200 örnek olan endpointleri analiz edin.
Sonuç
awk ile percentile analizi yapmak ilk bakışta karmaşık görünebilir, ama bir kez altyapıyı kurduğunuzda inanılmaz hızlı içgörüler elde edebiliyorsunuz. Elasticsearch’e veri göndermeden, Grafana açmadan, Python ortamı kurmadan, sadece terminal ile production’daki gerçek problemi bulabiliyorsunuz.
Ortalama yanıt süreleri sizi kandırır. P95, P99 değerleri gerçeği gösterir. “Sistemimiz iyi çalışıyor” dediğinizde arkasındaki %1’lik kullanıcı kitlesinin ne yaşadığını bilmiyorsanız, aslında hiçbir şey bilmiyorsunuz demek. Bu scriptleri kendi log formatlarınıza uyarlayın, cron ile otomatize edin, alarmlarınıza P99 eşiklerini ekleyin. Ortalama değil, yüzdelikler üzerine karar verin.
