SLO ve Error Budget: Hedef Belirlemek, Nine'ların Matematiği ve Error Budget Policy
SLO, bugünkü ölçümden ve kullanıcı beklentisinden türetilen ve SLA'dan sıkı olan bir hedeftir; aradaki pay error budget'tır ve bittiğinde ne olacağını önceden yazılmış bir policy söyler. Bookstore checkout'u için yüzde 99,5 availability ve 800 ms latency SLO'sunu dokümana, Prometheus rule'larına ve OpenSLO'ya döküyor, nine'ları dakikaya çeviriyor ve yeni bucket ile kısa pencerenin bütçeyi nasıl yanlış gösterdiğini ölçüyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (file-based app ile dotnet run), OpenTelemetry .NET 1.21, Prometheus 3.16.0 ve OpenSLO'nun oslo CLI'ı 0.13.0 ile denenmiştir.
Önce Tahmin Et
Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) checkout'u için 30 günlük bir availability SLO'su (Service Level Objective; belirli bir zaman penceresinde tutturulması hedeflenen güvenilirlik seviyesi) koyduk: yüzde 99,5. Laboratuvarda ödeme sağlayıcısının dört dakikalık bir arızası ve arka plandaki binde birlik hata oranı toplam 53 checkout'u başarısız yaptı. Prometheus'a kalan error budget'ı (hata bütçesi) sorduk ve şu cevabı aldık: -1.599727387380033, yani bütçenin yüzde 160'ı fazladan harcanmış. Dört dakikalık bir arıza bir aylık bütçeyi gerçekten bitirir mi? İkinci soru: aynı anda latency SLO'su için kalan bütçe -92.35 çıktı; checkout'ların yüzde 99,7'si 1 saniyenin altında olduğu halde. Hangisi doğru? İki cevap da yazının ortasında ve ikisi de rakamların değil, sorunun yanlış olmasından kaynaklanıyor.
SLA, SLO ve SLI
Üç kavram sık karıştırılıyor. SLI (Service Level Indicator; servisin kullanıcıya ne kadar iyi hizmet verdiğini gösteren ölçüm) bir önceki yazıda tanımladığımız oran: geçerli checkout isteklerinin yüzde kaçı başarılı. SLO bu oranın hedefi: "30 günlük kayan pencerede yüzde 99,5". SLA (Service Level Agreement; müşteriye verilen, tutturulamazsa yaptırımı olan söz) ise ticari bir sözleşme: hedef kaçırılırsa para iadesi, indirim ya da sözleşme feshi gibi bir sonucu var.
Bookstore bireysel müşteriye SLA vermiyor; ama Kurumsal Sipariş tarafında bir okulun satın alma sorumlusu Nevin Hanım'la yapılan sözleşmede "sipariş portalı ayda yüzde 99 erişilebilir" maddesi olabilir. Kural basit: SLO, SLA'dan sıkı olmalı. Yüzde 99'luk bir SLA için iç hedef yüzde 99,5 olursa, SLO kaçırıldığında hâlâ SLA'yı korumak için zaman vardır. SLA'yı hukuk ve satış ekibi yazar; SLO'yu servisi işleten ekip koyar ve gerektiğinde değiştirir.
Hedefi Nereden Bulursun?
İki kaynak var: servisin bugünkü performansı ve kullanıcının beklentisi. Önce ölç. Bookstore'un checkout'u arızasız 7 dakikalık bir pencerede yüzde 100 availability gösterdi ve checkout'ların yüzde 99,7'si 800 ms'nin altında kaldı. Sahte ödeme sağlayıcısı isteklerin binde birini reddettiği için uzun dönemde beklenen availability yüzde 99,9. Bugünkü değerin hemen altında bir hedef, servisin bugün tutturabildiği bir hedeftir. Sonra kullanıcıya bak: müşteri ödeme sayfasında bir saniyeden uzun bekleyince sayfayı yeniliyor ya da vazgeçiyor; 800 ms tarayıcıya ve ağa pay bırakan bir eşik.
Bazen kullanıcıyı mutlu edecek hedef bugün tutturulamaz. SRE (Site Reliability Engineering; güvenilirliği yazılım mühendisliğiyle yönetme disiplini) Workbook'u bu durum için aspirational SLO'yu (ulaşılmak istenen ama henüz bağlayıcı olmayan hedef) öneriyor: sıkı hedef ölçülür ve izlenir, ama error budget policy onu açıkça "aksiyon gerektirmez" diye işaretler. Böylece ekip sürekli alarm durumunda yaşamadan hedefe doğru ilerlemesini takip eder. Bookstore için latency'de yüzde 99,9'u 800 ms'nin altında tutmak böyle bir hedef olabilir.
Nine'ların Matematiği
Hedefin ne demek olduğunu dakikaya ve isteğe çevirmek tartışmayı somutlaştırır. Bunun için repoya küçük bir file-based app ekledik; .NET 10'da tek bir .cs dosyası proje dosyası olmadan çalışıyor:
// Bir SLO için error budget hesabı: bir hedef ne kadar kesintiye ve kaç başarısız isteğe izin verir.
// Kullanım: dotnet run tools/slo-math/slo-math.cs -- <windowDays> <requestsPerDay> <target%>...
// Örnek: dotnet run tools/slo-math/slo-math.cs -- 30 5500 99 99.5 99.9 99.95 99.99
using System.Globalization;
CultureInfo inv = CultureInfo.InvariantCulture;
int windowDays = int.Parse(args[0], inv);
long requestsPerDay = long.Parse(args[1], inv);
decimal[] targets = [.. args.Skip(2).Select(a => decimal.Parse(a, inv))];
double windowMinutes = windowDays * 24 * 60;
long requestsInWindow = requestsPerDay * windowDays;
Console.WriteLine($"window: {windowDays} days = {windowMinutes:N0} minutes, {requestsInWindow:N0} requests");
Console.WriteLine($"{"target",-8} {"budget",8} {"downtime",12} {"per day",10} {"failed requests",16} {"per day",8}");
foreach (decimal target in targets)
{
double budget = (double)(100m - target) / 100;
double downMinutes = windowMinutes * budget;
double failed = requestsInWindow * budget;
Console.WriteLine(
$"{target.ToString(inv) + "%",-8} {budget,8:P2} {FormatMinutes(downMinutes),12} {FormatMinutes(downMinutes / windowDays),10} " +
$"{failed,16:N0} {failed / windowDays,8:N1}");
}
static string FormatMinutes(double minutes) =>
minutes >= 60 ? $"{minutes / 60:0.0} h" : minutes >= 1 ? $"{minutes:0.0} min" : $"{minutes * 60:0} s";
Bookstore'da günde yaklaşık 1.290 sipariş var (Veritabanı Performansı ve Scaling serisinin son yazısındaki sayı). 30 günlük pencere için:
dotnet run tools/slo-math/slo-math.cs -- 30 1290 99 99.5 99.9 99.95 99.99
window: 30 days = 43,200 minutes, 38,700 requests
target budget downtime per day failed requests per day
99% 1.00% 7.2 h 14.4 min 387 12.9
99.5% 0.50% 3.6 h 7.2 min 194 6.5
99.9% 0.10% 43.2 min 1.4 min 39 1.3
99.95% 0.05% 21.6 min 43 s 19 0.6
99.99% 0.01% 4.3 min 9 s 4 0.1
Yüzde 99,5 ayda 194 başarısız checkout ya da 3,6 saatlik tam kesinti demek. Her nine bütçeyi on kat küçültüyor: yüzde 99,99'da ayda 4 başarısız sipariş, tek bir kötü deploy'un geri alınmasından bile kısa 4,3 dakika. Hedefi bir tablodan seçerken bu satırları yan yana görmek "yüzde 99,99 olsun" isteğini çabuk gerçekçi bir yere çekiyor.
Pencere de önemli. Kayan pencere (rolling window, her an son 30 günü sayan) kullanıcının yaşadığına daha yakın: ayın son günündeki büyük bir kesinti ertesi gün unutulmaz. Takvim penceresi (ay başında sıfırlanan) planlama ve raporlama için daha kolay. Workbook'taki örnekler dört haftalık pencere kullanıyor; 28 gün her zaman aynı sayıda hafta sonu içerdiği için hafta sonu trafiği farklı olan servislerde karşılaştırma tutarlı kalıyor. Aynı hedef 28 günde biraz daha küçük bir bütçe verir:
dotnet run tools/slo-math/slo-math.cs -- 28 1290 99.5
window: 28 days = 40,320 minutes, 36,120 requests
target budget downtime per day failed requests per day
99.5% 0.50% 3.4 h 7.2 min 181 6.5
Bir de bağımlılık zinciri var: checkout ödeme sağlayıcısını ve veritabanını senkron çağırıyorsa, bu ikisinin availability'leri çarpılarak checkout'un erişebileceği tavanı belirliyor. Hesabın kendisi System Design serisinin back-of-the-envelope estimation ve availability hesabını anlatan ikinci yazısında; buradaki sonucu şu: SLO, sert bağımlılıklarının söz verdiğinden daha iddialı olamaz.
800 ms İçin Histogram'a Bucket Eklemek
Latency SLO'su "geçerli checkout'ların yüzde 99'u 800 ms'nin altında". Bir önceki yazıda gördük ki histogram yalnız bucket sınırlarına kadar tam sayabiliyor ve ASP.NET Core'un varsayılan sınırlarında 0,8 yok. İki yol var: eşiği 0,75 ya da 1 saniyeye çekmek, ya da histogram'a 0,8 sınırını eklemek. Eşik kullanıcıdan geldiği için ikincisini seçtik. OpenTelemetry'de bunu bir View yapıyor:
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
// ASP.NET Core'un varsayılan sınırlarına 0.8 eklendi: checkout latency SLO'su 800 ms'nin altındaki istekleri sayıyor
// ve bir histogram ancak kendi bucket sınırlarından birine kadar tam sayabilir.
.AddView("http.server.request.duration", new ExplicitBucketHistogramConfiguration
{
Boundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 0.8, 1, 2.5, 5, 7.5, 10]
}))
View metric adına göre uygulanıyor, yani bütün route'ların histogram'ı bir bucket daha kazanıyor; her route ve status kombinasyonu için bir seri fazla. Yalnız checkout için ayrı bir histogram tutmak da mümkün, ama SLI'ı framework'ün kendi metric'inden okumak daha az kod demek. SLI rule'undaki eşik de le="0.8" oldu. API'yi yeniden başlattıktan sonra Prometheus'taki checkout bucket'ları:
sum by (le) (http_server_request_duration_seconds_bucket{http_route="/api/orders/", http_response_status_code="201"})
{le="0.5"} 1030
{le="0.75"} 1030
{le="1"} 1030
...
{le="+Inf"} 1032
{le="0.8"} 123
le="0.8" ötekilerden çok küçük, çünkü yalnız yeni process'in serisinde var; eski process'in serileri bu bucket'ı hiç taşımadı. Bunun sonucunu birazdan göreceğiz.
Error Budget'ı Prometheus'ta Hesaplamak
SLO'yu 30 günlük pencere üzerinden recording rule'lara yazdık. Kalan bütçe 1 ise hiç dokunulmamış, 0 ise tükenmiş, negatifse SLO kaçırılmış:
# Checkout SLO'ları (docs/slo/checkout.md): availability %99,5, latency %99'u 800 ms altında, kayan 30 gün.
# Kalan error budget: 1 = dokunulmamış, 0 = tükenmiş, 0'ın altı = SLO kaçırılmış.
groups:
- name: bookstore-slo
interval: 1m
rules:
- record: slo:checkout_valid:increase30d
expr: |
sum(increase(http_server_request_duration_seconds_count{job="bookstore-api", http_route="/api/orders/",
http_request_method="POST", http_response_status_code=~"[235]..|429"}[30d]))
- record: slo:checkout_availability:ratio30d
expr: |
sum(increase(http_server_request_duration_seconds_count{job="bookstore-api", http_route="/api/orders/",
http_request_method="POST", http_response_status_code=~"[23].."}[30d]))
/ slo:checkout_valid:increase30d
- record: slo:checkout_availability:budget_remaining
expr: 1 - (1 - slo:checkout_availability:ratio30d) / (1 - 0.995)
- record: slo:checkout_latency:ratio30d
expr: |
sum(increase(http_server_request_duration_seconds_bucket{job="bookstore-api", http_route="/api/orders/",
http_request_method="POST", http_response_status_code=~"[235]..|429", le="0.8"}[30d]))
/ slo:checkout_valid:increase30d
- record: slo:checkout_latency:budget_remaining
expr: 1 - (1 - slo:checkout_latency:ratio30d) / (1 - 0.99)
30 günlük bir increase pahalı bir sorgu; interval: 1m bu grubun her dakika, öteki rule'ların ise 15 saniyede bir hesaplanmasını sağlıyor. Laboratuvardaki sonuç:
{__name__="slo:checkout_valid:increase30d"} 4100.429934210527
{__name__="slo:checkout_availability:ratio30d"} 0.9870013630630998
{__name__="slo:checkout_availability:budget_remaining"} -1.599727387380033
{__name__="slo:checkout_latency:ratio30d"} 0.06644434382685678
{__name__="slo:checkout_latency:budget_remaining"} -92.35556561731424
Önce Tahmin Et'in cevabı burada. "30 günlük" pencerede yalnız yaklaşık 30 dakikalık veri var: 4.100 geçerli checkout. Bu pencerede 53 hata yüzde 1,3 ediyor ve yüzde 0,5'lik bütçeyi 2,6 kez aşıyor. Gerçek trafikte, günde 1.290 checkout ile 30 günde 38.700 checkout'luk bir pencerede aynı 53 hata, 194'lük bütçenin yaklaşık yüzde 27'si olurdu: can sıkıcı ama SLO'yu kaçırtmayan bir olay. Pencere dolmadan hesaplanan bütçe yanıltır; yeni bir SLO'nun ilk 30 gününde bütçeyi "o ana kadarki veri" diye etiketle.
Latency tarafı ise tamamen yanlış, ve bu bir tuzak: pay yalnız yeni process'in le="0.8" serisini sayıyor, payda ise eski process'lerin isteklerini de. Aynı dönem için var olan le="1" bucket'ıyla hesap yüzde 99,7 çıkıyor. Histogram'a bucket eklediğin gün o bucket'ın geçmişi yok; 30 gün dolana kadar SLO'yu eski bir sınırla (0,75 ya da 1) hesapla ya da pencereyi değişiklik anından başlat.
SLO'yu Yazıya Dökmek
Recording rule bir hesap; SLO ise ekibin üzerinde anlaştığı bir karar. Kararın yeri repodaki bir doküman: kim sahibi, hangi SLI, neden bu hedef, neyi ölçmüyor. Bookstore'da docs/slo/checkout.md şöyle başlıyor:
# SLO: Checkout
Status: Agreed (2026-11) · Owner: Emre (Bookstore.Api) · Product: Selin · Next review: 2027-02
## Objectives (rolling 30 days)
| SLO | Target | Error budget | At 1,290 checkouts/day |
|---|---|---|---|
| Availability | 99.5 % | 0.5 % | about 194 failed checkouts, or 3.6 hours of full outage |
| Latency | 99 % under 800 ms | 1 % | about 387 checkouts slower than 800 ms |
## Not covered (known blind spots)
- Requests that never reach the API (DNS, load balancer, API process down). Planned: a synthetic checkout probe.
- Correctness of the order total and payment amount.
- The institutional bulk order import (a separate SLO is planned).
"Not covered" bölümü önemli: bir önceki yazıda API kapalıyken SLI'ın hiçbir şey görmediğini ölçmüştük. Bunu dokümana yazmak, bir incident'ta "SLO yeşildi ama müşteriler şikâyet ediyordu" sürprizini önlüyor.
Error budget policy ise bütçe bittiğinde ne olacağını önceden söylüyor. Kararın incident sırasında, stres altında değil sakin bir günde verilmesi bütün amacı. Bookstore'un policy'sinin kalbi bir tablo:
| Remaining budget (30 days) | Releases | Work |
|---|---|---|
| above 50 % | normal release cadence | normal |
| 0–50 % | normal, but risky changes (schema, payment flow) need a rollback plan in the PR | look at what spent the budget |
| below 0 (SLO missed) | only fixes for the SLO, security fixes and data fixes until back above 0 | the next sprint starts with reliability items |
İki karar daha policy'de yazılı. Ödeme sağlayıcısının sebep olduğu hatalar da bütçeden düşüyor, çünkü müşteri kimin hatası olduğuna bakmıyor; ama bu durumda yapılacak iş "sağlayıcıyı beklemek" değil, bizim tarafımızdaki timeout, fallback ve müşteriye gösterilen mesaj. Tek bir incident 30 günlük bütçenin yüzde 20'sinden fazlasını harcarsa postmortem zorunlu; Workbook'taki örnek policy de benzer bir eşik kullanıyor. Anlaşmazlıkta kimin karar verdiği de yazılı: Selin ve Emre birlikte, uzlaşamazlarsa engineering manager.
SLO'yu araçtan bağımsız bir formatta tutmak istersen OpenSLO (SLO'ları YAML (girintiye dayalı, yapılandırma dosyalarında yaygın veri formatı) ile tanımlamak için açık bir spesifikasyon) var. Aynı availability SLO'su deploy/slo/checkout.openslo.yaml dosyasında:
apiVersion: openslo/v1
kind: SLO
metadata:
name: checkout-availability
displayName: Checkout availability
spec:
description: Valid checkout requests (POST /api/orders, 4xx except 429 excluded) that did not fail on our side.
service: bookstore-api
indicator:
metadata:
name: checkout-availability-sli
spec:
ratioMetric:
counter: true
good:
metricSource:
type: Prometheus
spec:
query: sum(http_server_request_duration_seconds_count{job="bookstore-api", http_route="/api/orders/", http_request_method="POST", http_response_status_code=~"[23].."})
total:
metricSource:
type: Prometheus
spec:
query: sum(http_server_request_duration_seconds_count{job="bookstore-api", http_route="/api/orders/", http_request_method="POST", http_response_status_code=~"[235]..|429"})
timeWindow:
- duration: 30d
isRolling: true
budgetingMethod: Occurrences
objectives:
- displayName: 99.5 percent of checkouts succeed
target: 0.995
oslo validate -f deploy/slo/checkout.openslo.yaml
Valid!
Hedefi yüzde olarak yazmak (target: 99.5) sık bir hata ve oslo onu yakalıyor:
Errors in bad.openslo.yaml:
Validation for v1.SLO 'checkout-availability' has failed for the following properties:
- 'spec.objectives[0].target' with value '99.5':
- should be less than '1'
Error: Configuration is not valid!
Araç desteği sınırlı: bir sonraki yazıda kullanacağımız Sloth 0.16 OpenSLO'nun eski v1alpha sürümünü okuyabiliyor, bizim openslo/v1 dosyamızı ise "could not load with any of the supported spec types" diyerek reddetti. Küçük bir ekip için Markdown doküman ve recording rule'lar yeterli; OpenSLO, SLO sayısı büyüdüğünde ve birden çok araç aynı tanımı okumak zorunda kaldığında işe yarıyor.
Düşük Trafikte Yüzde SLO
Kurumsal sipariş portalı günde ortalama 40 sipariş alıyor. Aynı hedef orada ne demek?
dotnet run tools/slo-math/slo-math.cs -- 30 40 99.5 99
window: 30 days = 43,200 minutes, 1,200 requests
target budget downtime per day failed requests per day
99.5% 0.50% 3.6 h 7.2 min 6 0.2
99% 1.00% 7.2 h 14.4 min 12 0.4
Ayda 6 başarısız sipariş. Tek bir öğle arasında ödeme sağlayıcısının iki dakikalık sorunu bütçeyi bitirebilir; tersine, gece boyunca hiç istek gelmediği için portal tamamen kapalı olsa bile SLI bunu görmez. Az veriyle yüzde hesaplamak istatistiksel olarak anlamsız. Seçenekler: synthetic trafik üretmek (dakikada bir sahte kurumsal sipariş), benzer küçük servisleri tek bir SLO'da birleştirmek, pencereyi uzatmak (90 gün) ya da hedefi sayı olarak yazmak ("ayda en fazla 3 başarısız sipariş, hepsine postmortem").
Üretimde Dikkat
SLO'yu düzenli gözden geçir. Bookstore'un dokümanında bir sonraki review tarihi yazılı. Trafik, mimari ve kullanıcı beklentisi değişir; bir yıl önce konan hedef bugün ya çok gevşek ya çok sıkı olabilir.
Hedefi ölçülen değerin üstüne koyma. Bugün yüzde 99,9 ölçülen bir servise yüzde 99,95 SLO koymak, policy'yi ilk günden "release yok" moduna sokar. Daha sıkı hedef isteniyorsa aspirational SLO olarak başla.
Sert bağımlılıklarının sözlerini öğren. Ödeme sağlayıcısının kendi SLA'sı yüzde 99,9 ise checkout için yüzde 99,95 hedeflemek, ancak sağlayıcı hatalarını yutan bir fallback'in varsa anlamlı.
Bütçeyi haftalık konuş. On-call devir toplantısında kalan bütçe tek satırlık bir gündem maddesi olabilir; ayın sonunda sürprizle karşılaşmamanın en ucuz yolu bu.
Sık Düşülen Tuzaklar
Bucket eklediğin gün SLO'yu yeni sınırla hesaplamak. Yukarıda gördüğümüz gibi kalan bütçe -92 çıkar ve bir alert bağlıysa sebepsiz yere çalar.
Yüzde 100 ya da "olabildiğince yüksek" hedef. Bütçesi olmayan bir hedef, error budget'ın bütün fikrini ortadan kaldırır; ilk incident'ta SLO kaçırılır ve bir daha kimse ciddiye almaz.
Policy'siz SLO. Bütçe bittiğinde ne olacağı yazılı değilse, karar o gün en yüksek sesle konuşana kalır. SLO'nun değeri bir grafikten değil, bağladığı karardan geliyor.
SLA'yı SLO olarak kullanmak. SLA'ya eşit bir SLO kaçırıldığında artık müşteriye para iadesi yapıyorsundur; erken uyarı payı kalmaz.
Küçük Bir Deneme
★☆☆ slo-math.cs ile 90 günlük pencerede yüzde 99,9 hedefinin günde 1.290 checkout için kaç başarısız siparişe izin verdiğini bul. Aynı hedefin 30 günlük karşılığıyla karşılaştır; uzun pencere bir incident'ın etkisini nasıl değiştiriyor?
★★☆ Arama yolculuğu (GET /api/books) için bir SLO dokümanı yaz: SLI, hedef, gerekçe ve "Not covered" bölümü. Hedefi koymadan önce trafik aracıyla 10 dakikalık bir ölçüm al.
★★★ slo.yaml'daki latency rule'larını, histogram'a bucket eklendiği andan itibaren hesaplayacak şekilde değiştir (ipucu: le="0.8" serisi olmayan dönemi paydadan da çıkar). Sonucu le="1" ile hesaplanan değerle karşılaştır.
Bütçe artık ölçülüyor, ama birinin ona bakması gerekiyor. Bir sonraki yazıda bütçenin ne hızla tükendiğini ölçen burn rate'i ve Workbook'taki multiwindow, multi-burn-rate alert'leri Prometheus rule'larına çeviriyoruz; ödeme sağlayıcısını bozup alert'in ne zaman çaldığını ve ne zaman sustuğunu izliyoruz.