Çiçeksepeti’nin ASPNET Core ve Linux Geçişi

ÇSTech ❤️ .NET Core ❤️ Linux

Çiçeksepeti Teknoloji olarak, her zaman teknolojiyi en yakından takip eden ve uygulayan bir ekip olduk.

2018'in başlarında, ciceksepeti.com ile benzer bir altyapıya sahip ciceksepetidukkan.com’un ASPNET Core’a geçirilmesiyle birlikte başlayan süreç, aynı yılın Kasım ayında ciceksepeti.com’un da ASPNET Core altyapısı ile production ortamına alınmasıyla tamamlandı. 2016'nın ortalarından bu yana gelen .NET Core tecrübemiz ise süreci en iyi şekilde yönetmemizi sağladı.

Bu yazıda, ciceksepeti.com’un ana uygulamasını nasıl ASPNET Core altyapısına geçirdiğimizden, bu geçişin bize sağladığı faydalardan, karşılaştığımız sorunlardan ve bu sorunlara ürettiğimiz çözümlerden bahsedeceğim.


ÇSTech olarak bundan yaklaşık iki buçuk yıl önce, mikroservis mimarisine ilk adımları atmamızla birlikte .NET Core kullanmaya başladık. İki yıldan daha uzun zamandır production ortamında .NET Core’u Kubernetes üzerinde kullanıyor, problemlere çözümler üretiyor ve uyguluyoruz.

Basit bir uygulamayı core ile yazmak ya da core geçişini yapmak elbette pek de zor değil. Fakat hali hazırda üzerinde geliştirmeler yapılmaya devam edilen, günün her saati yük altında olan, içerisinde çok sayıda bağımlılığı olan, .net framework ile yazılmış bir uygulamayı linux makinada core ile çalışır hale getirmek için doğru bir planlama ve sıkı bir çalışma gerekiyor.

Zaten uzun zamandır production ortamında yönetmekte olduğumuz bir kubernetes cluster’ımız olmasına rağmen ana uygulamamızı linux makina üzerinde nginx proxy arkasında host etmeye karar verdik.

Neden .NET Core?

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/choose-aspnet-framework

  • Daha hızlı ve stabil çalışan bir sistem ile kullanıcı deneyimini iyileştirmek.
  • Cross-platform oluşu.
  • Server maliyetlerini azaltacak olması.
  • Yeni teknolojinin sunduklarından faydalanabilmek.
  • Geliştirme ekibinin son teknoloji ile çalışma motivasyonu.

Şeklinde kısaca özetleyebilirim.

Neden sorusuna birçoğumuzun vereceği farklı cevaplar olsa da ortak kararın eğer yapılabiliyorsa bu geçişi yapmak olacağına eminim.

ASPNET Core Geçişi ile Ne Değişti?

Bu geçiş süreci, core’a geçiş yapmanın yanında, önemli kod düzenlemeleri yapmamız için de olanak sağladı.
  • Yeni geliştirilen ve geliştirilecek olan kütüphane ve araçlara tam uyum sağlayabilir hale geldik.
  • .NET Core’un performans üstünlüğüne, kod içerisinde yapılan iyileştirmeler de eklenince sunucu sayısı azaldı.
  • Düşük makina sayılarında bile düşük cpu ve ram kullanımı ile daha stabil bir sistem elde edildi.
  • Windows makinadan linux makinalara geçildi.
  • Sunucu maliyetlerinde çok ciddi oranda azalma oldu.
  • Saniyelere ulaşabilen response time değerleri milisaniye seviyelerine düştü.
  • Yapılan iyileştirmelerle birlikte hata sayılarında azalma oldu.
  • Yeni build pipeline’ı ve .NET Core’un build performansı sayesinde build süreleri azaldı.
  • Geliştirme ekibi artık platform bağımsız şekilde kod yazabilir hale geldi.

Neden Linux Makina Üzerinde Host Ettik?

.NET Core’a geçirdiğiniz uygulamanızı container içerisinde host edebilir hatta orkestrasyonu için kubernetes de kullanabilirsiniz. Bu durumda verilecek kesin bir karar olmadığını ve duruma göre en iyi yolun seçilmesi gerektiği kanaatindeyim.

Biz uygulamamızı, linux makinada bir linux servisi olarak nginx proxy arkasında host ettik çünkü;

  • Her ne kadar içerisindeki birçok yapı mikroservislere bölünmüş ve yükü hafifletilmiş de olsa halen monolith ve büyük çaplı bir uygulamaya sahiptik.
  • Kaynak tüketiminin bir container için fazla olduğunu düşündük.
  • EC2/Compute Engine yönetimi konusunda infrastructure ekibinin deneyimi yüksekti.
Bir uygulamayı container içine almak tek başına yeterince anlamlı değil. Bunu anlamlı kılan şey ise, mikroservis mantığı ve orkestrasyon araçlarıdır.

Nasıl Bir Yol İzledik?

Hala core geçişine başlamamış ya da henüz bitirmemiş olanlar için belki de en önemli kısım burası. Bu konuyu alt başlıklar halinde ele almak istiyorum.

  • Uygulamanın genel değerlendirmesi.
  • Core’da karşılığı olmayan bağımlılıkların çözülmesi.
  • Temel kütüphanelerin soyutlanması.
  • Uygulanan VCS(git) branch modeli.
  • Kod içerisindeki dönüşümün gerçekleştirilmesi.
  • CI Pipeline.
  • Linux makina üzerinde çalıştırılması.
  • İsteklerin ASPNET Core uygulamasına yönlendirilmesi.

Uygulamanın Genel Değerlendirmesi

Solution içerisinde yaklaşık 100 adet proje var. Bunların bir çoğu plugin olarak eklenmiş durumda. Aynı solution içerisinde 4 farklı web sitesine ve 2 farklı api’ye destek veriliyor.

Çoğu pluginlerin içerisinde olsa da çok sayıda third party bağımlılık bulunuyor. Bunlar arasında bankaların ödeme sistemleri için sunduğu eski dll’lerden tutun WCF servislere, core’da karşılığı hiç olmayan kütüphane ve araçlara kadar birçok şey var.

Paralelde devam eden geliştirmeler ve bug fixler süreçten etkilenmemeli; core için yapılan geliştirmelerle kolaylıkla merge edilebilmeli. Geliştime ekibine ekstra maliyet çıkarılmamalı. Belki de idare etmesi en zor kısımlardan biri burası.

Her şey bittiğinde ve core projesine geçiş yaptığımızda tüm eski cookie’ler çalışır vaziyette olmalı ve kullanıcı hiçbir fark hissetmemeli.


.NET Core’da Karşılığı Olmayan Bağımlılıkların Çözülmesi

Haziran 2016'da ASP.NET Core 1.0 çıktığından beri epey zaman geçmiş olsa da hala core için alternatifi olmayan kütüphaneler ve uygulamalar bulunmakta.

Bizim için en büyük değişiklik ORM aracı olarak PLINQ yerine EF kullanmak oldu. EF Core sıfırdan yazıldığı için “GroupBy” gibi bazı temel LINQ query’lerini bile desteklemiyordu(Artık destekliyor.). Desteklenmeyen her query, DB’den bazen tüm tablonun çekilip memory’de işlenmesi anlamına gelebilir. Bu durumlarda EF warning logu oluşturuyor. Bu logları takip edebilirsiniz ya da bu gibi durumlarda exception fırlatılmasını sağlayabilirsiniz.

Kendi bünyemizde kullandığımız tüm WCF servislerini REST olarak kullanacak şekilde güncelledik.

Ödeme yöntemlerinin bazılarının kullanmak zorunda bıraktığı eski dll’leri farklı api’lerde konumlandırarak proje içerisinden çıkardık.

Core için karşılığını bulamadığımız dll’ler için farklı alternatifler ve farklı implementasyonlar yaptık.


Temel Kütüphanelerin Soyutlanması

Caching, Redis, RabbitMQ, AutoMapper, repository, audit, EF Core dbcontext, IOC ile ilgili ekstra implementasyonlar, event mekanizması, scope safe asenkron mekanizması, api yardımcısı, plugin ve tema yapıları, ortak attribute sınıfları, startup adımını sadeleştirecek yapılar, bir web projesi için kullanılacak temel sınıflar gibi uygulamaya özel olmayan her türlü kod parçasını ayıklayıp core’a uyumlu kütüphaneler haline getirdik.

Daha önceden içerisinde birçok implementasyon barındıran ve bazı web projeleri ve servisler tarafından kullanılan bir kütüphanemiz zaten vardı. Core geçişi ile aslında bu kütüphaneyi en sade haline getirip birçok eksiğini de kapatmış olduk.

Böylece, ana projede minimum kod değişikliği ile core geçişini yapmak için bir adım daha atmış olduk. Bu adımlar, ana proje üzerindeki core için yapılacak değişikliklere ayıracağımız zamanı en aza indirdiği için oldukça önemliydi. Unutmayalım ki ana proje üzerinde bu geçişten tamamen bağımsız olan geliştirmeler devam etmekteydi.


Geliştirme ortamında, editör olarak Rider, işletim sistemi olarak macOS üzerinde çalıştık. iterm2, zsh ve VS Code diğer önemli araçlardı.

Uygulanan VCS(git) Branch Modeli

Bir branch modeli düşünürken temel hedefimiz, mevcut geliştirmeler devam ederken(feature-1, feature-2) .NET Core ile ilgili geliştirmeleri(core) de paralel olarak yürütebilmekti.

1- Yeni bir repository oluşturmak ve core ile ilgili geliştirmelere burda devam etmek ilk akla gelen seçeneklerden biri. Temiz bir sayfada temiz bir başlangıç fikri sanki çok iyiymiş gibi görünüyor. Fakat bu durumda devam eden diğer geliştirmeleri düzenli olarak bu yeni repository’e de kopyalama zorunluluğu doğacaktı. Kopyalarken gözden kaçan şeyler de ayrı bir problem olurdu. Belki de bazı geliştirmeleri kopyalamayı unutacaktık. Hem de frontend kodlarında hiç değişiklik yapmamanıza rağmen o geliştirmeleri de taşımak zorunda olacaktık.

2- Yeni branch açıp mevcut geliştirmeleri kopyalamak da yine yeni bir repository oluşturmaktan pek farklı sayılmaz.

3- Yeni branch açıp mevcut geliştirmeleri merge etmek ise hem git metodolojisi için daha anlamlı hem de kopyalama angaryasından kurtarıyor. Biz bu yolu seçtik. Herhangi bir geliştirmeyi de gözden kaçırma şansınız kalmıyor. Core için yapılacak değişiklikler backend tarafında. Frontend tarafı için conflict yok. Harika! Peki ya backend tarafındaki merge conflict’ler? Size de içinden çıkılmaz bir hal alır gibi mi geliyor? Aslında durum o kadar kötü değil. Birkaç madde ile açıklayayım.

  • .csproj’lar kökten değişiyor. Her zaman conflict çıkarırlar(değişiklik varsa). Use local deyin ve geçin. Sadece yeni paket eklenmişse sorun yaşarsınız. O da zaten build alınca görülüp düzeltilir.
  • global.asax, web.config artık yok. Her zaman conflict çıkarır(değişiklik varsa). Değişikliğin ne olduğunu inceleyip use local deyin. Sonra değişikliği core tarafında uygulayın.
  • package.json’lar her zaman conflict çıkarır(değişiklik varsa). Onların hepsini sildik. Use local deyin ve geçin.
  • ChildAction’ların hepsi ViewComponent oldu yani dosyaların yeri bile değişti. Her zaman conflict çıkarır(değişiklik varsa). Burada dikkat. Yapılan değişikliği bulup ViewComponent içerisinde aynısını yapmanız gerekir.
  • Proje içerisinden ayırıp kütüphanelere taşıdığınız kodlar conflict çıkarırlar(değişiklik varsa). Gerekli değişiklikler testpit edilip kütüphane üzerinde uygulanmalı.
  • Yukarıdaki maddeler hariç diğer her türlü değişiklik çoğunlukla hiç conflict çıkarmaz ya da normalde yaşadığımız conflict çözme sürecinden daha zor olmazlar.

Kod İçerisindeki Dönüşümün Gerçekleştirilmesi

İşte ne kadar kısa sürerse o kadar iyi olan ama hiç de kısa sürmeyecek adıma da geldik. Tüm kod değişiklikleri, deployment adımları ve tüm testler bitip .NET Core altyapısı production ortamına alınana kadar hem hızlı hem de dikkatli olmamız gereken bir süreç başladı.

Kod içerisindeki dönüşüme başlamak demek, paralelde devam eden diğer geliştirmeleri düzenli olarak merge edebiliyor olmak da demek. Bu yüzden bu adıma geçmeden önce yukarıda bahsettiğim adımların haricinde sizin için ana koda dokunmadan önce çözülebilecek farklı adımlar varsa çözmeli ve planı iyi yapmalısınız.

Master branch’inden bir branch aldık ve core ile ilgili tüm değişiklikleri burada yaptık.

Peki Nereden Başladık?

.csproj. Projeyi .NET.Core yapan referanslar değil de nedir? Sln içeriğinde bir değişiklik olmuyor. .csproj dosyaları ise baştan aşağı değişip eskisine göre çok daha sade bir hal alıyor. Eğer yapılacak işe hakimsek, .csproj dosyalarını olabilecek en sade halde tutabiliriz. Bunun için birkaç ipucu ve örnek vermek istiyorum.

  • Sadece application(Web, API, Test) projelerinin TargetFramework’ü netcoreapp olması yeterli. Solution içerisindeki diğer tüm projeler netstandard olarak kalmalı.
  • Örneğin; Project.Service diye bir projeniz var ve içerisinde Project.Core referans verilmiş durumda. Project.Web’e Project.Service’i referans verdiğinizde Project.Web içerisinde tüm Project.Core referansları kullanılabilir haldedir. Bunu bilerek, proje referanslarını olabilecek en sade halde tutun.
  • netcoreapp2.1 ve üstü için bir Web ya da API projesinde gerekli birçok bağımlılığı içeren Microsoft.AspNetCore.App paketini kullanın.
  • Build edilebilir parçalar halinde ilerlemek için en temel kütüphanenizden değişiklik yapmaya başlayın. Örneğin: Project.Core. Ardından Project.Data ve en son Project.Web gibi.

Örnek Web projesi .csproj dosyası:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework
<PreserveCompilationContext>true</PreserveCompilationContext>
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
        <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup>
    <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
    <ItemGroup>
<ProjectReference Include="..\Project.Manager\Project.Manager.csproj" />
    </ItemGroup>
</Project>

Otomatik olarak typescript build işlemi başlatılmasına ihtiyacınız yoksa TypeScriptCompileBlocked true yapmalısınız. API ve boş Web templateleri için belirtmeye gerek yok.

MvcRazorCompileOnPublish false olarak ayarlandıysa PreserveCompilationContext true olmalı ki runtime bağımlılıkları pakete dahil olup view dosyalarınızı çalıştırabilsin. MvcRazorCompileOnPublish true(varsayılan) ise view dosyalarınız dll olarak pakete dahil olur yani PreserveCompilationContext tag’ini kaldırabilirsiniz. API ve boş Web templateleri için hiçbirini belirtmeye gerek yok.

Örnek bir library .csproj dosyası:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Project.Services.csproj" />
</ItemGroup>
    <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.1.1" />
</ItemGroup>
</Project>

Bu .csproj örneklerinin büyük çaplı bir projede çok daha karmaşık olacağını düşünmeyin. Çoğu hiç değişmemekle birlikte, 3–5 satırdan fazla değişiklik olmuyor.

Uygulamadaki tüm projelerin .csproj dosyalarını en temel projeden başlayarak değiştirdik ve gerekli kod düzenlemeleri ile birlikte ilk hedefimiz tekrar build olabilen bir uygulama elde etmekti.

dotnet build! :) & dotnet run? :/

Uygulama kök dizininde dotnet build komutunu çalıştırıp 0 warning ve 0 error ile biten bir build gördükten sonra sırada uygulamanın en azından hatasız bir şekilde ayağa kalkması ve eksikleri olsa bile anasayfanın hatasız açılması vardı.

Uygulamanın startup’ı burada önemli bir adım. Biz bunun için geliştirdiğimiz bir library’i aracı olarak kullanıyoruz. Bu library, bizim belirlediğimiz ayarlara göre autofac ile birleştirilmiş bir IoC, projenin her noktasında .NET Core’un sunduğu veya Autofac’in sunduğu dependency registration implementasyonları yapma imkanı, otomatik olarak tüm db mappinglerinin register edilmesi, startup’da çalışmasını istediğimiz ekstra taskların yürütülmesi ve tüm bunların istenilen şekilde senkronize edilmesini sağlıyor.

  • Tüm bağımlılık registration işlemlerini tek satır halinde tutup, kompleks olanları da ServiceCollectionExtension ve ApplicationBuilderExtensions altına konumlandırdık.
  • Her projenin kendi bağımlılıklarını kendisinin register etmesini sağladık. Ana startup dosyası Web/API projesi altında metodları içerisinde tek satır kod olacak şekilde konumlandırıldı.

Böylece temiz, düzenli ve okunması kolay bir startup elde ettik. Aşağıdaki gibi kısaca örneklenebilir:

Project.Data
|_ InfraStructure
|_ Startup.cs
Project.Service
|_ InfraStructure
|_ Extensions
|_ ApplicationBuilderExtensions
|_ ServiceCollectionExtensions
|_ Startup.cs
Project.Web
|_ InfraStructure
|_ Extensions
|_ ServiceCollectionExtensions
|_ WebStartup.cs
|_ Startup.cs
|_ Program.cs

appsettings.json, global.asax, package.json

web.config’deki “appSettings” tag’i yerine artık appsettings.json var. Ortama göre farklılaştırmak için ise appsettings.Development.json şeklinde isimlendirmek yeterli oluyor. Hem appsettings.json hem de appsettings.Development.json varsa öncelik appsettings.Development.json’da olur ve appsettings.Development.json’da eksik olan değerler appsettings.json’dan okunur. Yani ortama göre değişmeyen değerleri her iki dosyaya da aktarmak zorunda değilsiniz.

global.asax artık yok. Buraya yazdığınız kodları startup configurasyonunuzda uygun yerlere taşımalısınız.

package.json dosyalarınızı da silip unutabilirsiniz.

IHttpContextAccessor

Her zamanki gibi static kullanımdan kaçınıyoruz. Artık HttpContext’i direk kullanmak yerine IHttpContextAccessor interface’i üzerinden kullanmalıyız. Uygulamanın bir çok yerinde değişiklik yapmamızı gerektirse de basit bir işlem.

IHttpClientFactory

HttpClient’ı nasıl kullanıyorsunuz? Her gerektiğinde using içerisinde yenisini oluşturarak mı yoksa uygulama için singleton bir HttpClient mı? Bazı senaryolarda, ilk kullanım HttpClient’ı dispose etse de connection açık kalmaya devam ediyor. Singleton kullanım ise darboğaz oluşturabilir. HttpClientFactory bu işi pooling ile çözüyor ve sizi dispose oldu olmadı, acaba using mi kullanmalı gibi dertlerin hepsinden kurtarıyor. Bu önemli değişikliği yapmayı unutmayalım.

View Components

Artık ChildAction’lara elveda. Bunların hepsini ViewComponent’e çevirip farklı dosyalar altında konumlandırıyoruz. Conflict çıkardığında en çok uğraştıracak yerlerin başında geliyor.

View Dosyalarındaki Değişiklikler

Uygulamanızın çalışmasını etkileyen çok fazla değişiklik olmayacak. ChildAction yerine ViewComponent çağırmanız gereken yerler en bariz olanı. Bu noktada ASP.NET Core TagHelpers hakkında bilgi sahibi olmakta fayda var.

Tüm Kodu Async Çalışacak Hale Getirmek

DB’den ön yüze kadar(View içerisindeki her türlü işlem dahil olmak üzere) her adımı asenkron olarak çalıştırmak(async await) .NET Core geçişiniz ile birlikte yapılabilecek önemli bir geliştirme. Fakat çok fazla kod değişikliği gerektireceğinden bu adımı geçiş sonrasına bırakmanızı tavsiye ederim.

MinWorkerThread gibi değişkenlerin değerleriyle oynamak yerine Thread pool’un verimli bir şekilde kullanılmasını, uygulamanızın daha performanslı ve scalable olmasını istiyorsanız bu yapılacak en önemli iyileştirmelerdendir.

SyncronizationContext’in .NET Core’da olmayışı ve bu sayede deadlock yaşama ihtimalinden uzak kod yazabilmenin avantajları kullanılmalı.

Elbette, kod içerisinde bu bahsettiğimden daha fazla değişiklik olacak fakat bunlar genelde çok basit ve problemleri build zamanında görülebilir değişiklikler.

CI Pipeline

Bir CI pipeline’nına .NET Core’a geçmeden önce de sahiptik. Kod değişikliği uzak repository’e push’lanır, Windows makinada kurulu olan TeamCity tool’u yardımıyla build alınır, Octopus üzerinden Windows makinaya deploy edilirdi. IIS’de gerekli ayarlar önceden yapıldığından sadece binding değiştirmek yeni kodun çalışır hale gelmesi için yeterliydi ve tabiki bu süreç otomatik olarak yürütülürdü.


.NET Core Build ve Publish

.NET Core geçişi ile birlikte TeamCity yerine, diğer .NET Core ve go projelerimiz için kullanmakta olduğumuz circleci’ı kullanmaya karar verdik. Windows için otomatize edilmiş eski yapıyı artık kullanamayacağımızdan build almak, testleri koşturmak ve paket oluşturup bu paketi Linux makinaya deploy edecek scriptleri yazmamız gerekiyordu. Kubernetes’e deployment yaparken kullandığımız pipeline ise container ve ECR üzerinden çalıştığı için build almak için işimize yaramıyordu.

Kod repository’e push’landıktan sonrası için izlenen yol; eski(.NET Framework) ve yeni(.NET Core) olmak üzere basitçe şu şekilde:

Build adımları için yeni ufak çaplı bash scriptler yazdık. Bazı önemli noktalar ise şöyle:

  • Uygulamadaki third party referanslarda bir değişiklik olmadığı sürece dotnet restore yapılmamalı. Önceden restore edilmiş paketler kullanılmalı.
  • dotnet restore komutu sadece bir defa çalıştırılmalı. dotnet build , dotnet test ve dotnet publish gibi komutların da build ve restore komutlarını tekrar çalıştırdığı unutulmamalı. bu komutlara yerine göre — no-restore— no-build argümanları verilmeli.
  • dotnet restore veya npm install gibi komutlar ile gelen paket dosyaları nihai pakete dahil edilmemeli.
  • .pdb dosyaları da pakette olacak fakat isterseniz silebilirsiniz. Yine de bazen profile etmek istediğinizde olmaları faydalı.

İçerisinde 100'e yakın proje ve çok sayıda View barındıran çiçeksepeti için oluşan paketin boyutu, zip’lenmiş halde 43mb oldu. Boyutun çoğunu frontend library’lerin oluşturduğunu da belirteyim. Çalışma zamanında gerekmese de deployment öncesi S3'e pushlayıp cdn arkasında konumlandırmak için gerekliydi. Frontend ve backend olarak iki ayrı paket yapmayı düşünebilirsiniz.

Bonus: Circleci üzerinde .NET Core’da paketlerini restore etmek yerine cache’den kullanmakla ilgili konfigurasyonlar (not: container buildleri için docker layer caching kullanılmalı.):

https://gist.github.com/turgayozgur/e5fe22ded70868c7e61c1dba0addfff0


ASPNET Core Linux Deployment

Windows makinada IIS ile çalışan .NET Framework uygulamaları zero-downtime olacak şekilde deploy etme işini IIS üstleniyor. Deployment sonrası binding değiştirmek yeterli oluyor.

Her ne kadar IIS büyük kolaylık sağlasa da Windows deployment’larında kimi zaman GAC, kimi zaman kısa süre dll not found hataları ile yüzleştiğimiz zamanlarda Load Balancer’a tak çıkar şeklinde zero-downtime deployment yapmamız gereken durumlar oldu.

Artık Linux makinaya geçiş yaptığımız için henüz yeterince yetenekli olmayan Kestrel’in önünde Nginx konumlandırdık. Linux makina üzerinde aşağıdaki gibi bir yapı oluşturmuş olduk:

var
|_ aspnetcore
|_ Example.Application.Name
|_ 1001
|_ 1005
|_ 1005-01
etc
|_ systemd
|_ system
|_ Example.Application.Name-1005-01.service
|_ nginx
|_ sites-available
|_ Example.Application.Name

1001, 1005, 1005–01 gibi versiyon numaraları ile adlandırdığımız klasörlere kod deploy ediliyor. Linux servisi systemd altında versiyona özgü olarak ayarlanmış durumda. Son olarak sadece bir adet Nginx konfigurasyonu var. Sadece o an çalışmakta olan versiyona yönlendirmeyi yapıyor.

Zero-downtime deployment işini Nginx ve yazdığımız deployment scripti(bash) ile hallettik.

Linux makinada Nginx ile ASPNET Core zero-downtime deployment yazıma aşağıdaki linkten ulaşabilirsiniz.


Karşılaşılan Diğer Sorunlar ve Çözümleri

.NET Core geçişi sırasında beklenmeyecek ya da sebebini bulmakta zorlamanız muhtemel olan birkaç problemden bahsedeceğim.

Too Many Open Files

Linux makinada çalışan bir servisin açabileceği ve sistem tarafından belirlenmiş maksimum dosya sayısı vardır. Hem sistem tarafından belirlenmiş olanı hem de linux servisiniz için gerekli olanı artırmanız gerekebilir.

Sistem için bu değeri 640000 yapmak:

# increase the max open file limit for the system
echo "fs.file-max = 640000" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Linux servisiniz için bu değeri 640000 yapmak için ise servis tanımına LimitNOFILE=640000 ekleyin:

[Unit]
Description=bla bla bla
[Service]
WorkingDirectory=/any/path/here
...
LimitNOFILE=640000
...

Daha detaylısı ise şurada:

Request Hang

HttpClient yerine IHttpClientFactory kullanımından bahsetmiştim. Yanlış kullanımdan dolayı requestler hang olup tüm uygulama cevap veremez hale gelebilir. IHttpClientFactory .NET Core 2.1 ile geldi. Daha önceki bir versiyon kullanıyorsanız ya da henüz IHttpClientFactory kullanmayan third party bir bağımlılığınız varsa(StackExchange.Redis 2.0 altı gibi.) bu problemi yaşayabilirsiniz.

Uygulamanız Nginx proxy arkasında ise çözüm için yukarıda bahsettiğim gibi LimitNOFILE değerini arttırın ve uygulamanıza ait Nginx konfigurasyonuna aşağıdaki satırları ekleyin ve Nginx’i reload edin:

proxy_buffering off;
proxy_read_timeout 7200;

Ascii Cookie

Attığınız request daha Kestrel’den 400 alıyorsa büyük ihtimalle bu problemi yaşıyorsunuz demektir. Bu sorun içerisinde ascii olmayan karakter taşıyan cookie’lerinizden kaynaklanıyor. Bizde yok demeyin. Third party bir library getirir koyar ve kullanıcılarınız hata alsa dahi ruhunuz bile duymaz. .NET Core 2.2 versiyonu ile düzeltildi. Diğer versiyonlar için, eğer Nginx proxy kullanıyorsanız cookie’den ascci olmayan karakterleri silen bir konfigurasyon(lua kodu) işinizi görür. Bu çözümü daha önce yorum olarak yazmıştım. Linkini paylaşıyorum:

Eski Login Cookie‘ler

Eskiden FormAuthentication ile oluşturduğumuz cookie’lerimiz artık .NET Core ile kullanılabilir durumda değil malesef. Yeterince güvenli bulmayan Microsoft’un Security ekibi de geriye dönük bu desteği vermiyor.

Production ortamına kodu çıktığınızda tüm kullanıcılarınız tekrar login olsun istemezsiniz değil mi?

Linkini paylaştığım paket ile bunun üstesinden gelebilirsiniz. İhtiyacınız olan şey sadece daha önce kullandığınız MachineKey değerleri.

OWIN Tabanlı Api Token

Hali hazırda, API’larınız için token üretip kullanıyorsanız malesef artık buna da destek verilmiyor.

FormAuthentication ile benzer olarak bu sefer de API kullanıcılarınızın(mesela mobil uygulamanızı indirmiş ve login olmuş) siz production ortamına çıktığınızda logout olmasını istemezsiniz.

Bu konuda malesef gerçekçi bir çözüm bulamadık. O yüzden ben çözdüm ve bir library haline getirdim. Linki paylaşıyorum:

Linux Path

Özellikle geliştirme ortamınız Windows ise çoğunlukla bu konu gözden kaçar. Linux, path segmentlerini ayırmak için/ kullanır. Windows ise hem / hem de \ kullanır. O yüzden hiçbir zaman ters slash kullanmamak gerekiyor.

HttpContext ve Request İlişkisi

Request bitince HttpContext null’a çekilmiyor. .NET Core 2.1 ve altı için bu problem geçerli. Asenkron bir kod bloğu içerisinde HttpContext’in durumuna göre işlem yapıyorsanız ya da buna benzer request tamamlandıktan sonra çalışıp HttpContext’e bakma ihtimali olan bir kod bloğunuz varsa kararsız çalışacaktır. 2.2 versiyonu ile birlikte bu sorunun giderildiğini okudum. Henüz deneme fırsatımız olmadı ama problemi yaşadık.

Server GC & Workstation GC

Lütfen uydu alıcınızın ayarları ile oynamayın :) Burada aslında bir hatadan bahsetmiyorum. Küçük bir uyarı olsun. Uygulamada cpu ya da memory ile ilgili her sorun yaşadığımda bu değerler ile oynayan ve çözüme ulaşan/ulaşmayan insanların yorumlarını gördüm. .NET Core 2.1.4 ve üzerinde bu tarz problemlerin çözümü bu değil. Sistem stabil. Lütfen bunu bilerek, problemi kodunuzda arayın.

Bu iki GC tipi hakında da kısaca şunu söyleyebilirim; Server GC memory kullanımını yüksek tutup cpu kaynaklarını daha az tüketmeyi ve uygulama performansını en üst düzeyde tutmayı hedefler. Varsayılan olarak Server GC çalışır. Workstation GC ise agresiftir. Sık çalışır. GC’nin sık çalışması maliyettir unutmayalım. Daha detaylısını ise şöyle ekliyorum:

Linux .NET Core Profiling

Memory ve cpu konusuna değinmişken linux makinada nasıl profile yaparız ona da değinmek gerekir.

İlk opsiyon perfcollect. İlgili linki paylaşıyorum:

.NET Core 2.1 versiyonu ile gelip daha dökümantasyonu olmayan, issue’lardan bulup çıkardığım ikinci yol ise; aşağıdaki iki satırı konsolda çalıştırdıktan sonra aynı konsol ekranında dotnet run demek ve sorunu gözlemleyecek kadar bekleyip basitce Ctrl+C ile uygulamayı durdurmak. Sonuç olarak, perfcollect’e benzer şekilde perfview ile okunabilen dosyalar üretir.

export COMPlus_EnableEventPipe=1
export COMPlus_EventPipeConfig=Microsoft-DotNETCore-SampleProfiler:1:5
dotnet run

Konuyla ilgili iki adet faydalı issue paylaşıyorum.


Bu geçiş sürecinde çok çalıştık, çok şey tecrübe ettik ve öğrendik. Zevk aldık. Sonuçları da yazının en başında belirttiğim gibi beklediğimizden de iyi oldu.

Okuduğunuz için teşekkürler.