Asp.Net Core Dependency Injection ve Servis Ömürleri

Giriş

Bu makalede Asp.Net Core Dependency Injection ve servis ömürleri (servis lifetimes) hakkında konuşacağız. Ayrıca üretim (production) sürecinde nasıl kullanılacağı, nasıl işledikleri hakkında bazı ipucu ve öneriler de paylaşacağım. Servis kapsayıcısının (service container) farklı servis ömürleriyle servisleri nasıl yönettiğini ve izlediğini gösteren örnek kod ekledim. Bu makalede genel olarak Dependency Injection ile ilgili az bahsedilen konulara ve arkaplanda ne şekilde çalıştığı ile ilgili bilgilere yer verdim. Dependency Injection nedir, nasıl kullanırım diye merak ediyorsanız giriş seviyesi bir çok güzel anlatımlı makaleler var, bu konuyla ilgili öncelikli olarak Microsoft’un kendi dökümanlarını öneririm.

Asp.Net Core, ekstra bir ayar yapmanıza ihtiyaç duymadan dependency injection modelini kullanır. Diğer taraftan, bir .Net Core konsol uygulaması yazıyorsanız, Dependency Injection kullanmak için servis kapsayıcıyı kendiniz ayarlamalısınız. Bu aslında karmaşık bir işlem değil, verdiğim örnek kod ve repository (kod deposu)’de nasıl yapıldığını görebilirsiniz. Bir şablon kullanarak oluşturduğunuz yeni bir Asp.Net Core Web uygulaması, otomatik olarak Startup isimli, servis kayıtlarını ve servis ilk hazırlıklarını yapabileceğiniz bir sınıf (class) oluşturur. Çoğunlukla uygulamanızın ihtiyaç duyduğu servisleri bu sınıfın ConfigureServices metodunda tanımlarsınız.

Asp.Net Core’da varsayılan olarak Dependency Injection (DI) (Bağımlılık Enjeksiyonu) için Microsoft.Extensions.DependencyInjection kütüphanesi kullanılmaktadır. Bu kütüphane çoğu uygulamaya yeterli olacak bir çok özellik içerir. Bu kütüphanenin sunduğundan daha fazla özelliğe ihtiyaç duyarsanız, üçüncü parti kütüphaneleri direk olarak veya entegre bir şekilde (önerilen) kullanabilirsiniz. Özelleştirilmiş servis ömürleri, alt servis kapsayıcıları (child service containers) ve benzeri ekstra özelliklere ihtiyacınız yoksa varsayılan kütüphaneyi kullanmanızı tavsiye ederim, çünkü bu kütüphane hem hafif, hem performanslı hem de .Net framework içinde yer aldığı için sürekli olarak geliştirilip güncel tutulmaktadır.

Neden Dependency Injection (Bağımlılık Ekleme)?

  • Servis kayıdı için bir arabirim (interface) veya soyut bir temel sınıf (abstract base class) kullanabilirsiniz. Bu yazdığınız kodun test edilebilir olmasına yardımcı olur. Ayrıca, farklı ortamlar veya uygulama ayarları için servisin farklı tanımlamalarını da (implementation) kaydedebileceğiniz için esneklik sağlar. Örnek olarak, arka uç servisinizin (backend service) kullanıcı tarafından yüklenen belgeleri depoladığını ve sunduğunu varsayalım. IDocumentStorageService (IDokumanDepolamaServisi) şeklinde depolama hizmetinizi soyutlarsanız, kendi bilgisayarınızda yerel geliştirmeniz (local development) için FileDocumentStorageService (IDosyaDokumanDepolama Servisi), birim testleri (unit tests) için MemoryDocumentStorageService (IHafizaDokumanDepolamaServisi), Azure Depolama Hizmetlerini (Azure Storage Services) kullanmak için AzureDocumentStorageService (IAzureDokumanDepolamaServisi) kullanabilirsiniz. Diğer bulut hizmeti sağlayıcılarıyla kullanmak için servisin diğer tanımlamalarını da yazabilirsiniz.
  • Framework (çerçeve) içinde tanımlı olan servis kapsayıcısı (service container), bağımlı servisleri (dependent services) otomatik olarak servisin oluşturma metoduna (constructor) enjekte eder. Ayrıca, servisleri ve bunların bağımlılıklarını oluşturmaktan ve temizlenebilir (disposable) servisleri izlemekten sorumludur. Bu, bağımlı kaynakları temizlemek ve bellek sızıntılarını (memory leaks) önlemek için daha az kod yazmanıza yardımcı olacaktır.

Servis Kapsamı (Service Scope) nedir?

Servis kapsamını (service scope), kısa ömürlü bir alt kapsayıcı (child container) gibi düşünebilirsiniz. Servis kapsamında çözümlenen tüm temizlenebilir (disposable) scoped ve transient servisler, servis kapsamı ile beraber temizleneceklerdir (dispose). Asp.Net’te sunucuya gelen her istek (request) ile beraber yeni bir servis kapsamı oluşturulur, bu nedenle istek sona erdiğinde istek içinde çözümlenen tüm servisler isteğe bağlı olan servis kapsamı ile beraber temizlenir. Bu, hem izolasyon sağlar, hem de bellek sızıntılarını (memory leaks) önlemeye yardımcı olur. İzolasyon için, her istek, sadece tenant ve kullanıcı kapsamındaki verilere erişebilen servisler oluşturabilir.

Servis ömürleri nedir?

Varsayılan dependency injection (bağılılık ekleme) framework’ü (çerçeve) bize üç adet servis ömrü sunar. Bu servis ömürleri, servislerin ne şekilde çözümleneceğini ve temizleneceğini belirler.

  1. Transient (Geçici): Servis sağlayıcıdan, her servis talep edildiğinde yeni bir tane servis oluşturulur. Eğer servis temizlenebilir ise, servis kapsamı bu servise ait oluşturulan tüm örnekleri (instances) takip eder ve servis kapsamı sona erdiğinde hepsini temizler.
  2. Singleton (Tekton): Hazır bir örnek ile kaydedilmediyse, bu servislerin tek örneği oluşturulur. Eğer servis kapsayıcı tarafından örneği oluşturuldu ise, bu servisler kök kapsam (root scope) tarafından takip edilir. Bunun anlamı bu servisler kök kapsam sona ermediği sürece hayatta kalırlar. Eğer singleton servisiniz temizlenebilir ise, tanımlanmış tipi (implemented type) veya servis sağlayıcı fabrikası (service provider factory) olarak kaydetmediyseniz, hali hazırdaki bir örnekle kaydettirdiyseniz, servis kapsayıcı bu servisi takip edip temizlemez. Bu durumda servis kapsayıcısı sona erdiğinde manuel olarak temizlemelisiniz.
  3. Scoped (Kapsamlı): Her service kapsamı için yeni bir örnek oluşturulur. Bu servisler, servis kapsamı içinde singleton (tekton) gibi davranırlar. Eğer servis temizlenebilir ise, servis kapsamı sona erip ortadan kaldırıldığında otomatik olarak temizlenir.

Örnek KodÖrnek repository burada

bulabilirsiniz.

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-000-servicelifetimes-cs

Yukarıdaki örnek koddan oluşan çıktı
Dikkat ederseniz yukarıdaki örnek kodda her servis için oluşturma ve temizleme evrelerini raporluyoruz. Ayrıca servisin mevcut bir örneğinin mi yoksa yeni bir örneğinin mi oluşturulduğundan emin olmak için SayHello metodunu çağırıyoruz.

Servis ömürleri örnek kod çıktısı

Temizlenebilir Servisler (Disposable Services)

Bir servis IDisposable ve/veya IAsyncDisposable arabirimini (interface) tanımlıyorsa temizlenebilir kabul edilmektedir.

Bir singleton servisi temizlenebilir ise ve hazır bir örnek ile kaydedildiyse, bu servis, servis kapsayıcısı tarafından takip edilip temizlenmez. Genellikle kök servis kapsayıcısı, uygulama sona ererken ortadan kaldırıldığı için bu sorun olmayabilir. Ancak hal böyle değilse, service kapsayıcısı ortadan kaldırıldıktan sonra manuel olarak servisi temizlemez iseniz, bellek sızıntısı oluşacaktır. Lütfen aşağıdaki kodu inceleyiniz.

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-010-servicelifetime-singleton-tracking-cs

Eğer transient servisiniz temizlenebilir ise, bu servisleri kök kapsam dışındaki kapsamlarda oluşturmalısınız. Kök kapsamlar genellikle uygulama sona erdiğinde ortadan kaldırıldığı için, temizlenebilir transient servisinizi kök kapsamda çözümlerseniz, bu servisin oluşturulan her örneği uygulama süresince hayatta kalacaklardır. Bu da sürekli olarak hafızada yer kaplayacakları anlamına gelir ve bellek sızıntısı oluşturabilirler. Temizlenebilir transient servisler için önerilen oluşturma yöntemi, bir servis fabrikası (service factory) kullanmaktır. Eğer transient servisinizin diğer servislere bağımlılığı varsa, service fabrika metoduna mevcut servis sağlayıcısını (service provider) referans olarak gönderebilirsiniz. Bu yöntemi kullandığınızda, service kapsayıcısı temizlenebilir transient servisleri takip etmeyecektir, bu durumda servise artık ihtiyaç olmadığınızda, servisi temizlemek sizin sorumluluğunuzdadır.

Örnek Kod;

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-020-servicelifetime-disposable-services-cs

Servis Kapsamları temizlenebilir servisleri nasıl takip eder?

Aşaığıdaki şemalarda Kök Kapsam “Root Scope”, İstek “Request”, Kapsam “Scope” terimlerine karşılık gelmektedir.

İsteklere bağlı kapsamların oluşması

Asp.Net Core’da her istekte yeni bir servis kapsamı oluşturulur. İstek, yanıt döndürüp veya hata ile karşılaşıp sona erdiğinde beraberinde oluşturulan servis kapsamı ve servis kapsamı ile beraber çözümlenip temizlenmek üzere takip edilen tüm servisler temizlenir.

Servislerin ömürlerine ve çözümlendikleri kapsayıcıya göre takip edilmesi

Servis kapsamından…
* Bir scoped servis talep edildiğinde;
* Servis kapsamı, servisin zaten servis kapsamı içinde bir örneği yoksa yeni bir örneğini oluşturur.
* Servis kapsamı scoped servisleri her zaman takip eder.
* Bir transient servis talep edildiğinde;
* Servis kapsamı, her zaman servisin yeni bir örneğini oluşturur.
* Servis kapsamı, sadece temizlenebilir transient servisleri takip eder.
* Bir singleton service talep edildiğinde;
* Eğer servisin henüz oluşturulmuş bir örneği yoksa, kök kapsam servisin bir örneğini oluşturur.
* Kök kapsam, kapsayıcı tarafından oluşturulmuş singleton servisleri her zaman takip eder.

Servisler Nasıl Oluşturulur?

Tüm servis örnekleri talep üzerine oluşturulur, bu nedenle servis kapsayıcısına, çok fazla sayıda farklı singleton servis kaydetmiş bile olsanız, bu servisleri hali hazırda oluşturulmuş bir örnek ile kaydetmediyseniz, sadece gerektiğinde oluşturulurlar. Bu, kaydetme şeklinize göre uygulamanızın başlatılırken geçen süreyi ve kullanılan kaynakları etkiler.

Eğer servisinizi bir servis tanımlama tipi (service implementation type) ile kaydederseniz;
* Servisiniz ilk talep edildiğinde reflection (yansıma) kullanılarak oluşturulur.
* Servisiniz tekrar talep edildiğinde, eğer bir singleton servis değilse, yine reflection kullanılarak oluşturulur, ancak servis motoru ikinci talepten sonra arka planda bir servis oluşturma fabrikası (service creation factory) derler.
* Servis fabrikası derlemesinden sonra, servis yeniden talep edildiğinde bu servis fabrikası kullanılarak oluşturulur ve bu noktadan sonra servisin yeni örneklerini oluşturmak oldukça hızlı olacaktır. Eğer servis, arka planda servis fabrikası derlenir iken tekrar talep edilirse, henüz servis fabrikası hazır olmadığı için 2 kereden fazla reflection ile oluşturulabilir.
* Bu yöntem aslında bir çok framework tarafından kullanılmaktadır, çünkü genellikle servis fabrikası derlemek, Activator.CreateInstance kullanarak reflection ile oluşturmaktan daha uzun sürer. Servis, singleton ise bir kereden fazla örneği oluşturulmayacağı için hiç derleme olmaz. Eğer bir scoped veya transient servis bir kereden fazla oluşturulmadıysa, bu yöntem sayesinde gereksiz derleme önlenmiş olur. Ayrıca servis ilk talep edildiğinde önceden derleme yapılsaydı, uygulamanın başlama sürelerini uzatırdı.
* Çoğu zaman bu yöntem işe yarar. Uygulamanın başlangıçtaki ilk ısınma süresi (warm-up time) uygulama performansını etkiliyor ise, servisinizi kaydettirirken bir servis fabrikası kullanmayı göz önüne alabilirsiniz. Bu şekilde reflection ve derleme olmayacağı için servis örneği oluşturma daha hızlı olacaktır.

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-030-servicelifetime-transientservice-registration-cs

Servisleriniz için ömür belirleme

Bağlam (context) veya fonksiyonelite ile

  • Singleton kullan;
    • Servisiniz, önbellekleme (cache) servisleri gibi paylaşılan bir duruma (state) sahip ise. Sabit olmayan, değiştirilebilir bir duruma (mutable state) sahip singleton servisleri thread safety (iş parçacıkları arasında güvenli bir erişim) için bir kilitleme mekanizması kullanmayı düşünmelidir.
    • Servisiniz durumsuz (staless) ise. Eğer servis implementasyonunuz, oldukça hafif ve nadir kullanılıyorsa, transient bir servis olarak kaydettirmeyi de göz önüne almalısınız.
  • Scoped kullan;
    • Servisinizin istek süresince bir singleton gibi tek bir örneğinin olmamasını istiyorsanız. Asp.Net Core’da her istek kendi servis kapsamına sahiptir. Veritabanı ve repository servisleri genellikle scoped servis olarak kaydedilirler. EntityFramework Core‘daki DbContext te varsayılanda bir scoped servis olarak kaydedilir. Scoped servis ömrü, istek süresince çözümlenmiş tüm servislerin aynı DbContext örneğini kullanmasını sağlar.
  • Transient kullan;
    • Servisiniz, yürütme bağlamı (execution context) içinde özel (paylaşılmayan) bir duruma sahipse.
    • Servisiniz aynı anda birden fazla iş parçacığı (thread) tarafından kullanılacaksa ve iş parçacığı için erişim güvenli değilse (not thread safe).
    • Servisiniz, HttpClient gibi transient ve kısa ömürlü olması gereken bir bağımlılığa sahip ise.

Bağımlılık ile

  • Singleton servisler diğer singleton servislere bağımlı olabilir. Singleton servisler transient servisleri de bağımlılık olarak kullanabilir ancak farkında olunması gereken nokta şudur ki, bu bağımlı olunan transient servisler de singleton servisler hayatta olduğu sürece yaşar. Bu da genellikle uygulamanın ömrü ile aynıdır.
  • Singleton servisleri, en iyi uygulama olarak (best practices), scoped servisleri bağımlılık olarak kullanmamalıdır. Çünkü bu şekilde scoped servis bir singleton gibi davranır, ve mimari anlamında genellikle istenen bu değildir. Scoped servisler diğer scoped servisleri ve singleton servisleri bağımlılık olarak kullanabilir. Bu servisler aynı zamanda transient servisleri de bağımlılık olarak kullanabilir, ancak bu durumda tasarımınızı gözden geçirip, bu kullanmak istediğiniz transient servisi scoped bir servis olarak kaydedip kaydemeyeceğinizi değerlendirmenizi öneririm.
  • Transient servisler tüm servisleri bağımlılık olarak kullabilirler. Bu servisler adından da anlaşılacağı üzere çoğunlukla kısa ömürlü geçici servislerdir.
  • Bir singleton veya scoped servisin bağımlılığı olarak transient servis kullanmak isterseniz, bu servis için bir servis fabrikası kullanmayı göz önünde bulundurmalısınız. Transient servisiniz temizlenebilir ise, servis fabrikası kullanmanızı öneririm.

Faydalı Öneriler

  • Kök kapsamında oluşturulan scoped servisler, temel olarak tek örnekli olur ve singleton servis gibi davranırlar, çünkü kök kapsamının ömrü boyunca takip edilip temizlenmezler. Scoped servisin bağımlılıkları varsa bu servisler de yine kök kapsamı içinde çözümlenirler. Bir scoped servisi kök kapsamdan çözümlediğinizde, eğer bu servisin temizlenebilir bir transient servise bağımlılığı varsa, transient servis te benzer şekilde kök kapsam tarafından takip edilip, kök kapsamının ömrü boyunca temizlenmeyecektir.

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-040-servicelifetime-scopedservice-correctresolution-cs

  • Diğer bir kötü tasarım ise bir singleton servisin, bir scoped servisi bağımlılık olarak kullanmasıdır. Bu tür yanlış servis referanslarını önlemek için servis kapsayıcıyı derlerken validateScopes parametresini kullanabilirsiniz. Asp.Net Core tarafından kullanılan kök servis kapsayıcı bu parametreyi true olarak kullanmaktadır.

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-050-servicelifetime-scopevalidation-cs

  • Mümkün olduğunca servis bulucu deseni (service locator pattern) kullanmaktan kaçının. Ayrıca, IServiceProvider‘ın GetService metodunu manuel kullanmaktansa dependency injection (otomatik bağımlılık belirleme) kullanın. Bu, daha kolay test etmenizi, bakım yapmanızı ve okunabilir koda sahip olmanızı sağlar.

Bunu yapmaktan kaçının

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-060-servicelifetime-avoid-manualresolution-cs

Servis sağlayıcıya statik olarak erişmekten kaçının

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-070-servicelifetime-avoid-servicelocator-pattern-cs

Daha iyi bir yöntem

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-080-servicelifetime-use-autoresolution-cs

  • Mvc denetleyicinizin (Mvc Controller) birden fazla uç noktası (endpoint) varsa ve bir veya daha fazla uç noktanız, diğer uç noktalar tarafından ortak kullanılmayan belirli bir servisi kullanıyorsa, Asp.Net Core Mvc, FromServicesAttribute özniteliği (attribute) sayesinde özel bir bağlama (custom binding) sağlar. Servis parametrenizde bu özniteliği kullanırsanız, Mvc servisleri, eylemi (action) çağırmadan önce servisi otomatik olarak çözümler ve metod parametresine bağlar.

Örnek kod;

https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-090-servicelifetime-controller-fromservicesattribute-cs

İlkler

Bu makaleyi yayınlayabilmek için, aşağıdaki öğeleri ilk defa yaptım.
* Bu benim genele açık ilk makalem. Dökümantasyon, bilgi tabanı (knowledge base), firma içi paylaşım ve eğitim amaçlı daha önce makale yazdım ancak bu benim genele açık yayınladığım ilk makalem. Şu anda çalıştığım PEAKUP firmasına, boş zamanlarımda yaptıklarımı fikri mülkiyet olarak etiketlemek yerine, beni topluluğa (community) paylaşmaya ve katkıda bulunmaya teşvik ettikleri için minnettarım. Onlar da aynı benim benim gibi topluluğa katkıda bulunmanın oldukça önemli olduğunu düşünmekteler.
* Azure Static Web Apps servislerini ilk defa kullandım. Azure Static Web Apps Azure’a eklenmiş yeni yönetilen (managed) servislerden biri ve daha önce denemeye fırsatım olmadı. Ben şu sıralar uygulamaları yayınlamak için genellikle Azure Kubernetes Servisleri, Azure App Servisleri ve Function Apps’leri kullanıyorum.
* İlk defa Docker Hub’a public 2 adet docker imajı paylaştım. Üretim için çoğunlukla private container registries (özel kapsayıcı kayıt defterleri), özellikle Azure Container Registry kullanıyorum.
* İlk defa Git Actions kullandım. Geliştirdiğimiz kurumsal uygulamalar için özel kod depoları (repositories) ve Azure DevOps Pipeline‘ları kullanıyoruz. GitHub tarafından sunulan, Git Actions’ı uzun zamandır denemeyi düşünüyordum ve sonunda, hedefime ulaşmak için kullandığım bir adımın bir parçası olarak başardım.
* İlk defa özel (custom) bir git action geliştirmem gerekti. Yani.. tek yaptığım mevcut bir repository’yi fork edip üzerinde düzenlemeler yapmaktı, ama yine de sayılır değil mi?
* Vue.js’i üretim (production) için ilk defa kullandım. Bu basit bir proje ama yine de, daha önce üretim için hiç kullanmadığınız bir teknoloji ile üretilmiş ve başkalarının kullanımına sunulmuş bir uygulama oldukça önemli bir konu. Ayrıca halihazırda oluşturulan kod üzerinde, bana uygun bir hale getirmek için bir çok düzenleme yapmış olmam da oldukça heyecan verici.

Krediler

Kapak resmi
Fotoğraf Hans-Peter Gauster tarafından Unsplash üzerinde

Önü Alınmayan CPU Yükselişi ve Profiling Aracılığıyla Tespit Edilmesi

Bu problem boyunca yaptığımız ciddi code refactor, test ve daha az kaynak ile daha fazla performans elde ettiğimiz çalışmaları makale içerisine dahil etmedim. 8 Mart 2021 günü 2 Instance olacak şekilde Azure Web Application’da çalışan Velocity uygulaması bir anda CPU göstergelerinde ciddi artışlar olduğu ile ilgili hatalar fırlatmaya başladı. PEAKUP’da bu tarz problemlere yaklaşımımız genelde “öncelikle yangını söndür, söndüremiyorsan kontrol altına al ve süreci inceleyip, problem ortadan kaldır” şeklindedir.

Her ne kadar Auto Scaling ile ilgili Azure ayarlarını yapmış olsak da her bir Velocity müşterisi onboarding (tüm içeriklerin girildiği ve kullanıcı eğitimlerinin verildiği) sürecini tamamladıktan sonra kendi içerisindeki kullanıcılarına portal kullanımını duyurdukları zaman sistemi ikinci bir ekranda genel olarak izliyoruz. Anlık gelen kullanıcı sayısını Power BI üzerinden kendi geliştirdiğimiz Analytics Tool’u ile izliyoruz. Bu tarih için planladığımız, olası ansızın gelebilecek bir yük yoktu. Ancak ürünün kullanımı gereği bazı müşteriler portal içerisinden duyuru yaptığında da ansızın bir yük gelebiliyordu. Bu durumda Azure üzerinde otomatik olarak ölçeklendirme için CPU, Memory vb. metrikler üzerinde kurguladığımız alarmlar devreye giriyor ve anında olası bir problemin önüne geçiyorduk.

Bu defa taşıdığımız yük, günlük trafiğimizin ortalamasında olmasına rağmen 12 Instance açıkken bile bir türlü CPU ile ilgili problemi çözemiyorduk. Üzerinden toplantı odaları, yaklaşan toplantılarım vb. bilgileri çektiğimiz ONE uygulaması ve Authenticator uygulaması da herhangi bir spike ile karşı karşıya kalmamasına rağmen Velocity’de sıra dışı bir durum vardı.

Problemi gidermek için kontrol ettiğimiz, denediğimiz bazı adımlar;

  • Bağlı çalışan tüm diğer uygulamaların kaynakları ve veri tabanları yeterli seviyede yükü kaldırıyor mu?
    • Buradaki amacımız Velocity ve diğer tüm ürünlerimizin ortak olarak kullandığı Authenticator ya da bağlı olduğu herhangi bir servisin veritabanı tarafında ya da backend tarafda gönderdiğimiz talepler karşısında herhangi bir bottleneck yaşanıp yaşanmadığını kontrol etmek oldu. Hiçbir problem tespit edilemedi. Aksine fazla kaynak rezerve edildiği tespit edilip bazı kaynaklarda düşüşe gidildi.
  • Öncelikle SQL Server içerisinde Created alanına göre anormal bir veri artışı var mı?
    • Bazı müşteriler ürünü beyaz ve mavi yaka şeklinde ayırıp bu kullanıcılara portallarını belli bir arayla açabiliyorlar. Son gelen kullanıcı sayısında herhangi bir ani artış olup olmadığını inceledik. Ek olarak Velocity’nin free versiyonu tarafından gelebilecek olası bir artış var mı yok mu bu metrikleri kontrol ettik ve sıradışı bir durum tespit edemedik.
  • JSON Formatındaki Verinin Büyüklüğü Bizi Ne Kadar Etkiliyor?
    • Neredeyse iki yıllık haberleşme kayıtlarını, günlük verilerin girildiği, yüzbinlerce telefon numarasının olduğu bazı widgetların içerisindeki veriler işlenirken acaba CPU tarafında buna bağlı bir kilitlenme mi var sorusu aklımıza geldi. Bunun için müşterilerimizden en büyük veriye sahip olan Widgetlardan beş tane seçip bunlara, test ortamında random veriler girmeye başladık ve CPU tarafında hiçbir değişim olmadığını tespit ettik. Sorun yine çözülmedi!
  • Diğer uygulamalara bağlanıp veri çeken Widgetlarımız(Twitter, Toplantı Odaları, İzin Uygulaması, Haberler vb.) widgetlar tarafında mı bir problem var?
    • Bir Intranetin en önemli olan hizmetlerinden olan Toplantı Odaları, Yaklaşan Toplantılarım, Dış Kaynak Haberleri, Bugün Kimler Uzaktan çalışıyor gibi katma değeri en yüksek ama en fazla CPU tüketimi sağlayan widgetları tek tek kontrol ettik. Her birisini teker teker kullanımdan kaldırıp sonrasında CPU ile ilgili durumu monitör ettik. Ciddi bir düşüş gözlemlendi.
  • Problem Spesifik bir serviste mi? Başka servisleri de etkiliyor mu?
    • Bunu görmek için, Velocity’nin kullandığı tüm servislere Apache’nin AB toolu ile hem localde hem de DEV Stage içerisinde Linux bir makine üzerinden 1000 tane 50 Concurrent request göndererek metrikleri takip ettik. Velocity dışında One servisinde bir problem keşfedildi ve giderildi. Ancak sorun Velocity tarafında sürmeye devam etti.

Yüksek CPU kullanacak tüm Widgetları tek tek incelemeye başladık. Önce portaldan kaldırıp sonrasında oluşan yükün artış veya azalış durumuna göre hareket edecektik.

İlgili widgetlarla ilgili tam bir factoring süreci başlatmak üzereyken problemin tekrar yaşandığını ancak bu defa CPU’daki yükselişlerin daha kısa süre içerisinde normale döndüğünü gözlemledik.

Problemin JSON tipindeki verileri işlediğimiz bir aşamadan kaynaklanmadığından emindim. Çünkü CPU tarafındaki kaynakları tüketebilecek diğer tüm işlemler (Image Processing, Video Processing) başka bir servis içerisinde, döviz-hava durumu gibi widgetlar da başka bir servis içerisinden geliyordu. Bu nedenle JSON tarafında en çok işlem yaptığımız methodları inceleyip tekrar testlere başladık.

Velocity içerisindeki Dependency Injection apısı tamamen kullanıcını yaptığı ilk request sonrasında kurgulanmaktadır. Kullanıcının bağlı olduğu Tenant Hybird mi yani İstanbul’daki bir sunucuda mı çalıştığı yoksa Azure üzerinde Cloud’da mı çalıştığı yapılan ilk requestten sonrasında kurgulanmaktadır. Bu aşamada gelen kullanıcının kim olduğunun tanımını yapan endpointi yani tüm isteklerin aktığı OnActionExecutionAsync methodunu incelemeye başladım.

HttpClient kütüphanesi bu gibi arka arkaya yapılan işlemler için önerilmediğinden dolayı IHttpClientFactory tarafından aldığımız CreateClient methoduyla gelen tüm HttpClient objelerini saatlik olarak isimlendirmeye başladık.

İlk başlarda yaşadığımız spike problemi devam etse de CPU’daki %100 ve üzeri oranda takılı kalma problemi yine süre olarak ciddi anlamda düşüş gösterse de problem hala devam etti.

Problemi daha detaylı incelemek için Azure’un Web Application’a özel olarak sunduğu, tüm metrikleri inceleyebileceğiniz bir uygulaması bulunmaktadır. Bu uygulama application Insight olarak geçer. Velocity’de temel özellikleriyle birlikte açık olan bu uygulamanın tüm Profiling ile ilgili özelliklerini açmaya karar verdik.

Kaynakları arttırmaya devam edip, Azure Application Insight ile problemi araştırmaya, metrikleri toplamaya devam ederken şununla karşılaştık. Application Insight sonrasında ortalama olarak çok küçük sayıda diyebileceğimiz isteklerde bile uygulamada CPU %110’ün üzerine çıkmaya başladı. Araştırdığımızda buna başka geliştiricilerin de maruz kaldığını öğrendik. Haliyle Application Insight tarafını tamamen kapattık ve sorun büyük oranda çözülmüş gibi görünmeye başladı.

 

Sonrasındaki bir hafta boyunca herhangi bir problem olmadan devam eden süreç ilerleyen zamanlarda yüksek Memory Exception hatalarıyla tekrar baş göstermeye başladı… Uygulamalarımızı Web Application içerisinde InProcess olacak şekilde doğrudan IIS’e vermekteydik. Application Insight’ın yarattığı yükü kaldırmak ve direkt olarak performansı koruyabilmek için Kestrel üzerinden bir Reverse Proxy yarattık ve artık hataları çok daha derinlemesine görmeye başladık.

 

Uygulama içerisinde Kestrel sayesinde her bir Request ve SQL sorgusunun çıktısını görmeye başladık. Artık Application Insight’ı açıp bunun yanında ek olarak uygulamanın çalıştığı anda yarattığı hataları görebilir hale geldik. Takım arkadaşlarım Fatih Doğan ve Fatih Memiş’in de bu aşamada dahil olduğu süreçte Code Review aşamasına geçtik. Bottleneck (dar boğaz) yaşadığımız yerin daha uygulamaya gelen ilk isteğin işlendi süreçten başlayıp son aşamasına kadar bütün kodu satır satır kontrol etmeye başladık. Bu esnada artık Kestrel’dan hem daha mantıklı hem de daha yardımcı olacak hataları almaya başladık. Zira uygulama bir yerden sonra CPU tarafında herhangi bir problem yaşamasa da memory tarafında ciddi şekilde sorunlar yaşadığımızı ve bunun kurguladığımız Dependecy Injection mimarisinden oluştuğunu keşfettik.

Normal web uygulamalarında tüm Dependency’ler Startup içerisinde tanımlanırken, Velocity’de neredeyse bütün database ile alakalı Dependency’ler ilk requesttin geldiği BaseController içerisinde tanımlanıyor. Bunun sebebi gelen talebi karşılarken minimum seviyede kaynağı ayağa kaldırmak ve bazı davranışsal değişikliklerin request bazında olabilmesini sağlayabilmektir.

Dependency yapısını yönetmek için Startup.cs içerisinde ayağa Singleton olarak kaldırdığımız IServiceCollection Instance’ı problemin ana kaynağı gibi görünmeye başladı.

İhtiyaç olduğu kadar Depdency’i ayağa kaldırdığımız aşamada kullandığımız IServiceCollection sınıfının Scope ile ilgili işlemler bittikten sonra artık kullandığı kaynakları Memory’den atmadığını ve bunun bütün uygulamayı etkilediğini fark ettik. Normalde içerisinden çektiğimiz BuildServiceProvider methodu ile çektiğimiz bir servisi artık IServiceProvider ile yüklemeye karar verdik.

Geçişi sağladıktan sonra metrikleri takip ettiğimizde çok ciddi düşüşün olduğunu fark ettik. Bottleneck’e sebep olan şeyin Startup’da Singleton olarak eklediğimiz IServiceCollection Instance’ının BaseController içerisinde tekrar yeni bir IServiceCollection üzerinden alınması olduğunu keşfettik ve IServiceCollection yerine IServiceProvider ile ihtiyacımız olan Dependency kurgusunu sağladık. Böylece sorun ortadan kalkmış oldu.

Bu süreçte yaşanan sorunlar nedeniyle bizi hoşgörüyle karşılayan tüm Velocity müşterilerine, Fatih Doğan, Fatih Memiş ve tüm Velocity takımına teşekkür ederim.