Bölüm 11 / 14
HttpClient
IHttpClientFactory, dış servis çağırma, Polly ile yeniden deneme.
11 dakikalık okuma
4. hattın oluşturucu desenler dersinde
IHttpClientFactory'yi "fabrikanın gerçek bir üretim problemini çözmesi" diye
örnek göstermiş, yapısal desenler dersinde DelegatingHandler zincirinin
dekoratör olduğunu söylemiş ve ikisinin ayrıntısını bu derse bırakmıştık. Konu göründüğünden
geniştir: HttpClient'ın yanlış kullanımı, üretimde uygulama açılışta
çalışıp bir süre sonra bağlantı kuramaz hâle gelen hataların en yaygın sebebidir.
01Ne öğreneceksin
- İki karşıt tuzak: soket tükenmesi ve bayat DNS
IHttpClientFactory'nin handler havuzu ve handler yaşam süresi- Üç kayıt biçimi: temel, adlandırılmış, tipli istemci
- Tipli istemcinin yaşam süresi ve esir bağımlılık tuzağı
- JSON yardımcıları, akışlı okuma ve iptal jetonu
DelegatingHandlerzinciri: kimlik, korelasyon, loglama- Zaman aşımı, yeniden deneme, devre kesici ve idempotentlik
- Yeniden deneme fırtınası ve
Retry-After
02İki karşıt tuzak
HttpClient IDisposable uyguladığı için ilk sezgi onu
using ile kullanmaktır. Bu sezgi yanlıştır:
// Tuzak 1 — her çağrıda yeni istemci
using var istemci = new HttpClient(); // her Dispose sonrası soket TIME_WAIT'te bekler
var yanit = await istemci.GetAsync(adres); // yük altında: SocketException, "adres kullanımda"
// Tuzak 2 — sonsuza kadar yaşayan tek istemci
static readonly HttpClient _istemci = new(); // DNS değişikliğini fark etmez, bağlantı bayatlar
Birinci tuzakta sorun nesne değil, altındaki sokettir. Kapatılan bir TCP
bağlantısı işletim sisteminde bir süre (tipik olarak dakikalarca) TIME_WAIT
durumunda bekler; saniyede yüzlerce istek yapan bir servis kısa sürede kullanılabilir
port aralığını tüketir. Belirtisi tanıdıktır: uygulama açılışta sorunsuz çalışır, yük
arttıkça bağlantı hataları başlar.
İkinci tuzak bunun aşırı düzeltmesidir. Tek bir istemci soket sorununu çözer ama açtığı bağlantıları süresiz kullanır ve DNS değişikliğini görmez: hedef servis yeni bir IP'ye taşındığında (bulut ortamlarında olağan) istemci eski adrese gitmeye devam eder.
IHttpClientFactory ikisini birden çözer: HttpClient nesnesi
kısa ömürlüdür, ama altındaki HttpMessageHandler havuzdan gelir ve
paylaşılır. Handler'lar varsayılan olarak 2 dakika yaşar, sonra havuzdan
çıkarılır — bu süre, DNS'in yeniden çözülmesini garanti eden penceredir. Süre
ayarlanabilir:
builder.Services.AddHttpClient("odeme")
.SetHandlerLifetime(TimeSpan.FromMinutes(5)); // varsayılan 2 dk
Kıdemli seviyede bilinen bir alternatif daha vardır: tek bir uzun ömürlü istemci
kullanıp bağlantı yenilemeyi SocketsHttpHandler.PooledConnectionLifetime ile
yönetmek. Fabrika ile çözülen problemi doğrudan çözer ve fabrikanın getirdiği düzeni
(adlandırılmış yapılandırma, handler zinciri) istemeyen basit senaryolarda tercih
edilebilir. Bir servis içinde ikisinden biri seçilir; karıştırmak yapılandırmayı
izlenemez hâle getirir.
03Kayıt biçimleri ve tipli istemci
Üç biçim vardır; üretimde tipli istemci tercih edilir.
// 1) Temel — fabrikadan adsız istemci
builder.Services.AddHttpClient();
public sealed class X(IHttpClientFactory fabrika)
{
public async Task Cagir() { var c = fabrika.CreateClient(); /* ... */ }
}
// 2) Adlandırılmış — yapılandırma ada bağlı, çağıran adı bilmek zorunda
builder.Services.AddHttpClient("odeme", c =>
{
c.BaseAddress = new Uri("https://odeme.ornek.local/");
c.Timeout = TimeSpan.FromSeconds(10);
});
// 3) Tipli — yapılandırma ve kullanım tek tipte; ad yazım hatası imkânsız
builder.Services.AddHttpClient<OdemeIstemcisi>((sp, c) =>
{
var ayar = sp.GetRequiredService<IOptions<OdemeAyarlari>>().Value; // 3. ders
c.BaseAddress = new Uri(ayar.TabanAdres);
c.Timeout = TimeSpan.FromSeconds(ayar.ZamanAsimiSaniye);
c.DefaultRequestHeaders.Add("X-Kaynak-Uygulama", "siparis-servisi");
});
Taban adres ve zaman aşımı gibi değerlerin koda gömülmemesi, 3. dersteki options deseninin doğrudan uygulamasıdır: ortam değiştiğinde (geliştirme, test, üretim) yalnız yapılandırma değişir. Tipli istemci sınıfı sade kalır:
public sealed class OdemeIstemcisi(HttpClient http, ILogger<OdemeIstemcisi> log)
{
public async Task<OdemeSonucu> Tahsil(OdemeIstegi istek, CancellationToken ct)
{
var yanit = await http.PostAsJsonAsync("tahsilatlar", istek, ct);
if (yanit.StatusCode is HttpStatusCode.Conflict)
return OdemeSonucu.ZatenIslenmis; // beklenen iş durumu
yanit.EnsureSuccessStatusCode(); // beklenmeyen → istisna
return await yanit.Content.ReadFromJsonAsync<OdemeSonucu>(ct)
?? throw new InvalidOperationException("Boş yanıt gövdesi.");
}
}
Yaşam süresi tuzağı. Tipli istemci transient kaydedilir ve içindeki
HttpClient'ın ömrü handler yaşam süresine bağlıdır. Bu tipi bir
singleton'a enjekte edersen, 4. dersteki esir bağımlılık hatasını yaparsın:
istemci uygulama boyunca yaşar, handler asla yenilenmez ve DNS sorunu geri döner. Kural:
tipli istemci scoped ya da transient bileşenlerden kullanılır; singleton bir servis
gerekiyorsa içine IHttpClientFactory enjekte edilir ve istemci her çağrıda
yeniden alınır.
04İstek, yanıt ve iptal
JSON uzantıları (System.Net.Http.Json) çoğu işi kısaltır:
GetFromJsonAsync<T>, PostAsJsonAsync,
ReadFromJsonAsync<T>. Bilinmesi gereken davranış farkları:
EnsureSuccessStatusCodeileIsSuccessStatusCode: birincisi başarısız kodda istisna fırlatır. Beklenen iş durumlarını (404 "kayıt yok", 409 "zaten işlenmiş") istisnaya çevirmek pahalıdır ve akışı bozar; bunlarStatusCodekontrolüyle ele alınır, gerçekten beklenmeyen durumlar istisnaya bırakılır.- Büyük yanıtlar:
GetAsyncvarsayılan olarak gövdeyi tamamen tamponlar. Büyük dosya ya da uzun liste çekiyorsanHttpCompletionOption.ResponseHeadersReadile başlıklar gelir gelmez dönülür ve gövde akış olarak okunur; bellek kullanımı sabit kalır. CancellationTokenher çağrıya verilir. Denetleyicideki jeton (8. ders) buraya kadar taşınmalıdır: istemci bağlantıyı keserse dış çağrı da iptal edilir. Taşınmazsa istemci gitmiş olsa bile sunucu dış servisi beklemeye devam eder.- Zaman aşımı iptalden ayırt edilmelidir.
HttpClient.Timeoutaşıldığında fırlatılanTaskCanceledException'ın iç istisnasıTimeoutException'dır; kullanıcı iptalinde ise iç istisna yoktur ve jeton işaretlidir. Loglarda bu ikisini ayırmazsan "çok fazla iptal var" diye yanlış bir tablo okursun.
try
{
using var kaynak = CancellationTokenSource.CreateLinkedTokenSource(ct);
kaynak.CancelAfter(TimeSpan.FromSeconds(3)); // bu çağrıya özel süre
var yanit = await http.GetAsync(yol, HttpCompletionOption.ResponseHeadersRead, kaynak.Token);
await using var akis = await yanit.Content.ReadAsStreamAsync(kaynak.Token);
// akış üzerinden işle
}
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
{
log.LogWarning("Dış servis zaman aşımına uğradı: {Yol}", yol);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// istemci vazgeçti; hata değil
}
05DelegatingHandler zinciri
4. hatta söz verilen dekoratör karşılığı budur: her handler bir sonrakini sarar, isteğin gidişinde ve yanıtın dönüşünde iş yapabilir — middleware boru hattının istemci tarafındaki ikizi.
public sealed class KorelasyonHandler(IHttpContextAccessor erisimci) : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage istek, CancellationToken ct)
{
var iz = erisimci.HttpContext?.TraceIdentifier;
if (iz is not null) istek.Headers.TryAddWithoutValidation("X-Korelasyon-No", iz);
return await base.SendAsync(istek, ct); // zincirin devamı
}
}
builder.Services.AddTransient<KorelasyonHandler>();
builder.Services.AddHttpClient<OdemeIstemcisi>(...)
.AddHttpMessageHandler<KorelasyonHandler>() // sıra önemlidir: en dıştan içe
.AddHttpMessageHandler<KimlikHandler>();
Tipik kullanımlar: kimlik (jetonu alıp Authorization başlığına
koymak, süresi dolunca yenilemek), korelasyon (istek kimliğini alt servislere
taşımak — 19. hattaki izlenebilirliğin temeli), loglama ve başlık
normalleştirme. Sıra, tıpkı middleware'de olduğu gibi ekleme sırasıdır ve anlamı
değiştirir: loglama handler'ı kimlik handler'ının dışındaysa jetonsuz isteği loglar,
içindeyse jetonlu — ve jeton loglanmamalıdır (5. ders).
İki tuzak. Birincisi: handler'lar havuzlanır ve paylaşılır, bu yüzden
örnek alanında istek verisi tutulmaz. İkincisi: handler singleton gibi yaşadığından
kurucusuna scoped servis alınamaz — IHttpContextAccessor bu kısıtı aşmak için
vardır, çünkü kendisi singleton'dır ve o anki isteği AsyncLocal üzerinden
bulur. Bu erişimciyi bir arka plan işinden çağırırsan HttpContext
null'dur; 12. derste bu ayrıntı önemli olacak.
06Dayanıklılık: yeniden deneme ve devre kesici
Ağ üzerinden yapılan her çağrı başarısız olabilir. Dört temel politika vardır ve birlikte kullanılırlar:
| Politika | Neyi çözer | Riski |
|---|---|---|
| Zaman aşımı | Yanıt vermeyen servisin kaynağı tutmasını | Çok kısa süre → gereksiz hata |
| Yeniden deneme | Geçici hatalar (ağ titremesi, 503) | Yükü katlar; idempotent olmayan işlemi bozar |
| Devre kesici | Çöken servise yığılmayı | Yanlış eşikte sağlıklı trafiği keser |
| Eşzamanlılık sınırı | Tek bir bağımlılığın tüm havuzu tüketmesini | Dar sınır → kuyruk |
.NET tarafında bunlar Polly ile ve .NET 8'den beri
Microsoft.Extensions.Http.Resilience paketiyle kurulur. Standart yığın tek
satırla eklenir:
builder.Services.AddHttpClient<OdemeIstemcisi>(...)
.AddStandardResilienceHandler(o =>
{
o.Retry.MaxRetryAttempts = 3;
o.Retry.UseJitter = true; // eşzamanlı denemeleri dağıtır
o.AttemptTimeout.Timeout = TimeSpan.FromSeconds(3);
o.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(12);
o.CircuitBreaker.FailureRatio = 0.5;
});
Üç kural, bu konudaki kıdem ölçütüdür:
- Yalnız idempotent işlem yeniden denenir. GET, PUT ve DELETE tanımı gereği yinelenebilir; POST değildir. "Ödeme al" isteğini körlemesine yeniden denemek çift tahsilat üretir — ilk istek aslında başarılı olup yanıt yolda kaybolmuş olabilir. Çözüm idempotentlik anahtarıdır: istemci her mantıksal işlem için benzersiz bir anahtar üretir, sunucu aynı anahtarla gelen ikinci isteği tekrar işlemez, ilk sonucu döndürür.
- Üstel geri çekilme + rastgelelik (jitter) şart. Sabit aralıklı yeniden deneme,
çöken bir servis toparlanmaya çalışırken tüm istemcilerin aynı anda üstüne gitmesine yol
açar — yeniden deneme fırtınası. Süre üstel artmalı ve üzerine rastgele bir pay
eklenmelidir. Ayrıca sunucu
Retry-Afterbaşlığı gönderiyorsa (429, 503) ona uyulur. - Yeniden deneme katmanları çarpışır. Hem istemcide hem ara katmanda hem de çağıran serviste üçer deneme varsa, tek bir kullanıcı isteği alt servise 27 çağrı olarak gider. Yeniden deneme tek bir katmanda, tercihen çağrının kaynağına en yakın yerde yapılır.
Devre kesicinin mantığı da tanımlanmalıdır: hata oranı eşiği aşınca devre açılır ve çağrılar hedefe hiç gitmeden hızlıca reddedilir; bir süre sonra yarı açık duruma geçip tek bir deneme isteği gönderilir, başarılıysa devre kapanır. Amaç, çöken servisi rahat bırakmak ve çağıranın iş parçacıklarını boşuna bekletmemektir. Bu politikaların dağıtık sistem bağlamındaki tam tartışması 19. hattaki dayanıklılık dersindedir; burada önemli olan, dış çağrının her zaman bir zaman aşımı ve bir hata planıyla yazılmasıdır.
Son olarak izlenebilirlik: IHttpClientFactory ile yapılan çağrılar
kendiliğinden loglanır ve Activity üzerinden izlere bağlanır (5. ders, 19.
hat). Dış çağrının süresi, durum kodu ve yeniden deneme sayısı ölçülmezse yavaşlığın
kaynağı kendi kodunda mı yoksa bağımlılıkta mı olduğu bilinemez.
07Mini alıştırma — Kırılgan bağımlılık
Yaklaşık 20 dakika · iki uygulama (biri "dış servis")
- Sahte dış servis: ikinci bir web uygulaması yaz; bir ucu isteklerin
yarısında 500 dönsün, bir ucu 5 saniye beklesin, bir ucu 429 +
Retry-After: 2dönsün. - Tipli istemci: ilk uygulamada
AddHttpClient<T>ile tipli istemci kur; taban adresi ve zaman aşımınıappsettings.json'danIOptionsile oku. - Zaman aşımı ayrımı: 5 saniyelik uca 2 saniyelik zaman aşımıyla git.
Yakaladığın istisnanın tipi ve
InnerException'ı ne? İstemciyi durdurup (isteği iptal ederek) aynı çağrıyı tekrarla ve farkı yaz. - Handler zinciri: korelasyon başlığı ekleyen bir
DelegatingHandleryaz; dış serviste başlığın geldiğini doğrula. - Sıra etkisi: bir loglama handler'ı ekle ve zincirde kimlik handler'ının önüne, sonra arkasına al. Loglanan başlıklar nasıl değişti?
- Yeniden deneme:
AddStandardResilienceHandlerekle, %50 hata veren uca 20 istek gönder ve dış servisin gördüğü toplam istek sayısını say. Beklediğin sayı ile gerçek sayı uyuşuyor mu? - İdempotentlik: aynı yeniden denemeyi bir POST ucuna uygula; dış serviste kaç kayıt oluştu? Sonra istekleri bir idempotentlik anahtarıyla gönder ve sunucuda aynı anahtarı ikinci kez işlememeyi uygula.
- Soket kanıtı (isteğe bağlı): döngüde
new HttpClient()ile 500 istek yap ve işletim sistemindeTIME_WAITsoketlerin arttığını gör (Linux'tass -s, Windows'tanetstat -an). Sonra fabrikayla tekrarla.
08Kendini sına
new HttpClient() neden sorunludur, IHttpClientFactory ne çözer?
İki karşıt tuzak vardır. Her çağrıda yeni istemci üretip
Dispose etmek, altındaki TCP bağlantısını kapatır ve soket bir süre
TIME_WAIT'te bekler; yoğun serviste kullanılabilir port aralığı tükenir —
belirtisi, uygulamanın açılışta çalışıp yük arttıkça bağlantı hatası vermesidir. Bunu
aşırı düzelten statik tek istemci ise açtığı bağlantıları süresiz kullanır ve DNS
değişikliğini görmez; hedef yeni IP'ye taşındığında eski adrese gitmeye devam eder.
IHttpClientFactory ikisini birden çözer: HttpClient nesnesi kısa
ömürlüdür ama altındaki HttpMessageHandler havuzdan gelir ve paylaşılır;
handler varsayılan olarak iki dakika yaşayıp havuzdan çıkarılır, böylece DNS yeniden
çözülür. Alternatif olarak tek uzun ömürlü istemci +
SocketsHttpHandler.PooledConnectionLifetime kullanılabilir; ikisinden biri
seçilir, karıştırılmaz.
Tipli istemci nedir, yaşam süresi neden önemlidir?
Tipli istemci, AddHttpClient<T> ile kaydedilen ve
kurucusundan HttpClient alan bir sınıftır; yapılandırma (taban adres, zaman
aşımı, varsayılan başlıklar) ile kullanım aynı tipte toplanır ve adlandırılmış istemcideki
"ad yazım hatası" riski ortadan kalkar. Taban adres gibi değerler kod yerine
yapılandırmadan, IOptions ile okunur. Yaşam süresi kritiktir: tipli istemci
transient kaydedilir ve içindeki HttpClient'ın ömrü handler yaşam süresine
bağlıdır. Bu tipi bir singleton'a enjekte etmek esir bağımlılıktır: istemci
uygulama boyunca yaşar, handler asla yenilenmez ve DNS sorunu geri döner. Singleton bir
servis dış çağrı yapacaksa içine IHttpClientFactory enjekte edilir ve istemci
her çağrıda yeniden alınır.
DelegatingHandler ne işe yarar, hangi tuzakları vardır?
İstemci tarafındaki dekoratör zinciridir: her handler bir sonrakini
sarar, isteğin gidişinde ve yanıtın dönüşünde iş yapabilir — middleware boru hattının
ikizidir. Tipik kullanımlar kimlik jetonu eklemek ve yenilemek, korelasyon/izleme
kimliğini alt servislere taşımak, loglama ve başlık normalleştirmedir; zincire
AddHttpMessageHandler ile takılır ve ekleme sırası anlamı değiştirir —
loglama kimlikten önce mi sonra mı çalışıyor, jetonlu isteği mi jetonsuzu mu görüyor
(jeton loglanmamalıdır). İki tuzak: handler'lar havuzlanıp paylaşıldığı için örnek
alanında isteğe özgü durum tutulmaz; ve handler singleton gibi yaşadığından kurucusuna
scoped servis alınamaz — o anki isteğe erişmek için singleton olan
IHttpContextAccessor kullanılır, ama arka plan işlerinde
HttpContext null olur.
Dış servis çağrısını dayanıklı hâle nasıl getirirsin?
Dört politika birlikte kullanılır: zaman aşımı (yanıt vermeyen
servisin kaynağı tutmasını engeller), yeniden deneme (geçici hatalar için), devre kesici
(çöken servise yığılmayı keser; açık → yarı açık → kapalı döngüsüyle çalışır) ve
eşzamanlılık sınırı. .NET'te bunlar Polly ile ya da
AddStandardResilienceHandler ile kurulur. Üç kural: (1) yalnız
idempotent işlem yeniden denenir — GET/PUT/DELETE yinelenebilir, POST değildir; ödeme
gibi işlemlerde körlemesine deneme çift tahsilat üretir, çözüm idempotentlik anahtarıdır.
(2) Geri çekilme üstel olmalı ve rastgelelik (jitter) içermelidir, yoksa çöken
servis toparlanırken tüm istemciler aynı anda üstüne gider — yeniden deneme fırtınası;
sunucu Retry-After gönderiyorsa ona uyulur. (3) Yeniden deneme
katmanları çarpışır: üç katmanda üçer deneme, tek isteği 27 çağrıya çevirir; deneme tek
katmanda yapılır. Ayrıca her dış çağrının süresi, durum kodu ve deneme sayısı ölçülür;
aksi hâlde yavaşlığın kaynağı bilinemez.
09Sınav köşesi
Yazılıda nasıl sorulur
(1) Klasik soru: HttpClient neden using ile
kullanılmamalıdır; soket tükenmesi nedir. (2) IHttpClientFactory hangi iki
problemi çözer; handler yaşam süresi ne işe yarar. (3) Adlandırılmış ve tipli istemci
farkı. (4) DelegatingHandler nedir, hangi desene karşılık gelir.
(5) EnsureSuccessStatusCode ile IsSuccessStatusCode farkı.
(6) Zaman aşımı ile iptal nasıl ayırt edilir. (7) Yeniden deneme hangi HTTP metotları
için güvenlidir; idempotentlik nedir. (8) Devre kesici durumları. (9) Kod yazma: bir
dış servisi çağıran tipli istemci ve bir handler yazınız. (10) Tipli istemciyi
singleton'a enjekte etmenin sonucu.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Problem — "HttpClient'ta iki karşıt tuzak vardır: her çağrıda
yeni istemci soketleri TIME_WAIT'te tüketir, statik tek istemci DNS
değişikliğini kaçırır." (2) Çözüm — "IHttpClientFactory istemciyi kısa
ömürlü tutar ama handler'ı havuzlar ve iki dakikada bir yeniler; ikisini birden çözer."
(3) Uygulama — "tipli istemci kullanırım; taban adresi ve zaman aşımını
IOptions'tan okurum, ortak başlık ve kimlik işini
DelegatingHandler zincirine koyarım — bu zincir dekoratör desenidir."
(4) Dayanıklılık — "her dış çağrının zaman aşımı, yeniden denemesi ve devre kesicisi
olur; AddStandardResilienceHandler ile kurarım." (5) Kıdem vuruşu —
"yeniden denemeyi yalnız idempotent işlemlerde yaparım; POST'u ancak idempotentlik
anahtarıyla yeniden denerim, yoksa çift tahsilat riski var. Geri çekilmeyi üstel ve
jitter'lı tutarım ve denemeyi tek katmanda yaparım — katmanlar çarpışırsa tek istek
onlarca çağrıya dönüşür. Tipli istemciyi singleton'a enjekte etmem; bu esir
bağımlılıktır ve handler yenilenmez."
