Günümüz dünyasında ihtiyaçlara binaen gün içerisinde servislere çok sık deploy çıkılabilmekte ve service özelinde yeni geliştirmeler devreye alınabilmektedir. Kimi zaman bu servisler anlık kesintinin problem olmayacağı kimi zaman da tek bir request özelinde bile kesinti yaşandığında müşteri tarafının olumsuz bir şekilde etkilenebileceği servisler olabilmektedir. Bu kesinti durumunu ortadan kaldırmak için gerek Kubernetes özelinde gerek de ASP.NET ile geliştirilen bir servis özelinde neler yapabileceğimize yazı kapsamında değiniyor olacağım.
İçerik
- Kubernetes tarafında pod'un sonlandırılması, oluşturulması ve update stratejileri
- .Net tarafında
Host
yapısının incelenmesi -
IHostedLifecycleService
,IHostedService
veIHostApplicationLifetime
interface'lerinin incelenmesi - Host'un shutdown sürecinin incelenmesi
-
Kind
ile Kubernetes Cluster oluşturulması - Örnek
.Net
projesi,Dockerfile
veDeployment
manifest'in oluşturulması - Servisin Kubernetes Cluster'a deploy edilmesi ve testin gerçekleştirilmesi
- Kubernetes Ingress ve Kubernetes Control Plane arasındaki gecikme
Kubernetes Üzerinde Update Stratejisi
Kubernetes tarafında, deployment objesinin manifestinde .spec.strategy.type
altında update stratejisi belirtilmektedir. Bu strateji Recreate
ya da verilmediği taktirde default davranış olan RollingUpdate
'dir.
Bu makele kapsamında uygulamanın bir Deployment objesi olarak yayınlandığı baz alınmıştır. StatefulSets ve DaemonSets objelerinde
.spec.updateStrategy.type
spec'di altında bu stratejiler belirlenmektedir. Bu stratejiler iseOnDelete
veRollingUpdate
'dir.
Recreate
spec:
replicas: 10
strategy:
type: Recreate
Recreate
update stratejisinde; öncesinde bütün pod'lar terminate edilir ve ardından yeni versiyona ait pod'lar ayağa kaldırılır. Yukarıda verilen manifest'e göre öncesinde Kubernetes bütün 10 pod'u öldürecek ve ardından yeni pod'ları ayağa kaldıracaktır. Bu stratejide muhtemel down-time'a (kesinti) neden olacaktır. Bunun sebebi ise Kubernetes'in öncesinde bütün pod'ları; yeni pod'lar oluşup yerlerine geçmeden terminate etmesidir.
RollingUpdate
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: %25
maxSurge: %25
RollingUpdate
update stratejisinde ise kademeli olarak pod'lar yeni versiyonlu image ile replace edilir. Öncelikle pod yeni image numarası ile oluşturulur ve ardından eski image numaralı pod öldürülür. Bu operasyon bütün pod'lar için gerçekleşene kadar devam eder.
maxSurge
ve maxUnavailable
parametreleri RollingUpdate
stratejisinde belirtilmektedir.
-
maxUnavailable
: Update aşamasında unavailable olabilecek pod sayısını belirtmektedir. Değer olarak yüzdelik ya da doğrudan pod sayısı verilebilir. Optional bir field'dır ve default değeri%25
'dir. -
maxSurge
: Deployment manifest içerisinde belirtilen replica sayısının üstünde olabilecek pod sayısını ifade etmektedir.maxUnavailable
'da olduğu gibi yüzdelik ya da doğrudan pod sayısı değeri verilebilir. Optional bir field'dır ve default değeri%25
'dir.
Yukarıda belirtilen field'lar için yüzdelik değer verildiği durumlarda; pod sayısı tam sayı gelmez ise
maxUnavailable
için bu değer aşağıyamaxSurge
için ise bu değer yukarıya yuvarlanır. Örneğin 1 replica belirtildiği durumdamaxSurge
değeri yukarı yuvarlanarak 1'emaxUnavailable
değeri ise aşağıya yuvarlanarak 0'a karşılık gelmektedir. Bu durumda önce bir pod oluşturulur ve running duruma geçtikten sonra hali hazırdaki pod terminate edilir.
Rolling Update stratejisi ile beraber Kubernetes uygulamanın minimum kesintiye uğrayarak deploy edilmesini sağlamaktadır. Aşağıda Rolling Update operasyonu sırasında tek replica'ya sahip bir deployment için oluşan aşamalar ve örnek deployment manifest aktarılmıştır;
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
resources:
limits:
cpu: "0.3"
memory: "75Mi"
requests:
cpu: "0.1"
memory: "50Mi"
strategy:
type: RollingUpdate
kubectl set image deployment/nginx-deployment nginx=nginx:1.14.1
komutu çalıştırılarak image versiyonu update edildiğinde aşağıdaki aşamalar gerçekleştirilmektedir;
-
maxSurge
değeri 1 olduğundan öncelikle yeni image numarası ile bir pod ayağa kaldırılmaya başlanır. - Pod status'ü
Running
'e geçtikten sonra hali hazırda olan Pod'a Kubernetes tarafındanSIGTERM
sinyali gönderilir. Pod'a yeni gelen isteklerin yönlendirilmesi kesilir ve hali hazırda olan isteklerin tamamlanması için açık connection'larspec.terminationGracePeriodSeconds
süresi zarfınca beklenir. - Pod'un terminate olması
spec.terminationGracePeriodSeconds
süresinde fazla ise Kubernetes tarafındanSIGKILL
sinyali gönderilir ve pod kill edilir.
Kubernetes tarafında bu süre beklenir fakat uygulama tarafında da belirtilen
POSIX
sinyali neticesinde graceful shutdown için işlemin yapılması gerekmektedir.
Yukarıdaki deployment için aşağıda image versiyonu update edildiğinde pod status'leri sırası ile verilmiştir;
kubectl get pods -n default -w
NAME READY STATUS RESTARTS AGE
nginx-deployment-59994fb97c-5j4fv 1/1 Running 0 8m8s
nginx-deployment-59994fb97c-g789c 1/1 Running 0 8m9s
nginx-deployment-59994fb97c-nddlf 1/1 Running 0 8m9s
nginx-deployment-5fffc966ff-8crmb 0/1 Pending 0 1s
nginx-deployment-5fffc966ff-8crmb 0/1 Pending 0 1s
nginx-deployment-5fffc966ff-8crmb 0/1 ContainerCreating 0 1s
nginx-deployment-5fffc966ff-8crmb 1/1 Running 0 1s
nginx-deployment-59994fb97c-5j4fv 1/1 Terminating 0 8m16s
nginx-deployment-5fffc966ff-52knq 0/1 Pending 0 0s
nginx-deployment-5fffc966ff-52knq 0/1 Pending 0 0s
nginx-deployment-5fffc966ff-52knq 0/1 ContainerCreating 0 0s
nginx-deployment-59994fb97c-5j4fv 0/1 Terminating 0 8m16s
nginx-deployment-5fffc966ff-52knq 1/1 Running 0 1s
nginx-deployment-59994fb97c-g789c 1/1 Terminating 0 8m18s
nginx-deployment-5fffc966ff-jwmtt 0/1 Pending 0 0s
nginx-deployment-5fffc966ff-jwmtt 0/1 Pending 0 0s
nginx-deployment-5fffc966ff-jwmtt 0/1 ContainerCreating 0 0s
nginx-deployment-59994fb97c-5j4fv 0/1 Terminating 0 8m17s
nginx-deployment-59994fb97c-5j4fv 0/1 Terminating 0 8m17s
nginx-deployment-59994fb97c-5j4fv 0/1 Terminating 0 8m17s
nginx-deployment-59994fb97c-g789c 0/1 Terminating 0 8m18s
nginx-deployment-5fffc966ff-jwmtt 1/1 Running 0 1s
nginx-deployment-59994fb97c-g789c 0/1 Terminating 0 8m19s
nginx-deployment-59994fb97c-g789c 0/1 Terminating 0 8m19s
nginx-deployment-59994fb97c-g789c 0/1 Terminating 0 8m19s
nginx-deployment-59994fb97c-nddlf 1/1 Terminating 0 8m19s
nginx-deployment-59994fb97c-nddlf 0/1 Terminating 0 8m19s
nginx-deployment-59994fb97c-nddlf 0/1 Terminating 0 8m20s
nginx-deployment-59994fb97c-nddlf 0/1 Terminating 0 8m20s
nginx-deployment-59994fb97c-nddlf 0/1 Terminating 0 8m20s
Belirtilen
spec.terminationGracePeriodSeconds
değeri pod özelinde tanımlanmaktadır. Default değeri 30s'dir.Sidecar container'lara
SIGTERM
sinyali gönderilmeden önce aynı pod içerisinde bulunan main container terminate edilip sonrasında sidecar container'lara tanımlandıkları sıranın tersi yöndeSIGTERM
sinyali gönderilmektedir. Bu şekilde pod içerisinde sidecar container'a ihtiyaç kalmadığı durumda sonlandırılması sağlanmış olurDeployment içerisinde yer alan nginx uygulaması
SIGTERM
sinyali aldığında açık connection'lar da dahil olmak üzere hızlıca process'i sonlandırır. Bu sebeple graceful shutdown edilebilmesi içinSIGTERM
sinyali geldiğinde track edilmeli veSIGQUIT
sinyali verilmelidir. Nginx tarafında graceful shutdown yapılabilmesi içinSIGQUIT
sinyali beklemektedir. Bunun için bash ile operasyon gerçekleştirilebilir.
.Net Host Model
.Net Core framework ile dünyamıza gelen yeni Host
model yaklaşımı ile birlikte Host
objesi içerisinde uygulamanın ihtiyaç duyacağı kaynaklarının ve hayat döngüsünde olan fonksiyonlarının sarmalanması amaçlanmıştır. Belirtilen yapı ile de aynı zamanda default template'ler içerisinde yer alan bir çok boilerplate kodun da çıkarılması amaçlanmıştır. Bu obje üzerinde belli düzenlemeler yapılarak uygulama türüne göre aşağıda belirtilen fonksiyonaliteler default'da hazır gelmekte ve ihtiyaca binaen düzenlenebilmektedir;
- Dependency Injection (DI) yapısı
- Logging
- Configuration
- App Shutdown süreci
-
IHostedService
implementasyonu
Belirtilen Host
model yaklaşımı .Net framework versiyonları değiştikçe uygulama templatelerinde de farklılık göstermiştir. Aşağıda 3 farklı yaklaşım .Net versiyonları ile belirtilmiştir;
.NET Core 2.x
sürümü ile birlikte WebHost
sınıfı altında yer alan CreateDefaultBuilder
method'u ile Host
model oluşturulmakta ve configüre edilmektedir.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Yukarıda dotnet
cli'ı ile belirtilen sürümde bir ASP.Net Core
projesi sıfırdan oluşturulduğunda Program.cs
dosyasında yer alan kod bloğu aktarılmıştır.
.NET Core 3.x
ile .NET 5
versiyonları arasında Host
model yaklaşımında büyük değişiklik sağlanarak web projelerinin de generic Host
üzerinden oluşturulması için yaklaşım değiştirilmiştir. Bu değişim ile beraber aynı base kod kullanılarak Host
model üzerinden worker servisler, gRPC servisler, Windows servisler geliştirilebilir hale gelmiştir. Bu yöntem ile IWebHostBuilder
interface'i yerine IHostBuilder
interface'i üzerinde Host
model kurgulanmıştır.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Yukarıda .NET Core 3.x
ile .NET 5
versiyonları arasında dotnet
cli ile sıfırdan oluşturulan ASP.NET
projesinin Program.cs
dosyasında yer alan kod bloğu paylaşılmıştır.
Yukarıda paylaşılan iki yaklaşımda da Startup
sınıfının ayrılmaz bir şekilde web uygulamasına bağımlı olduğu gözden kaçırılmamalıdır fakat IHostBuilder
interface'i üzerinde konumlanan Host
model yaklaşımında web uygulamaları dışında farklı uygulamalarında geliştirilebileceğini aktarmıştık. Bu uygulamalarda Startup
sınıfına ihtiyaç bulunmayabilmektedir. (Örneğin Configure
adlı method uygulama içerisinde yer alan middleware'leri ayarlamak için kullanılmaktadır fakat worker service'ler için böyle bir configürasyona ihtiyaç bulunmamaktadır.) Bu sebeple framework'ü geliştirenler ConfigureWebHostDefaults
extension method'u ile bu durumu aşmışlardır.
.NET 6
ile birlikte iki farklı dosya üzerinde yapılan configürasyonlar (Startup.cs
ve Program.cs
) tek dosya üzerinde birleştirilmiş ve Host
model IHostApplicationBuilder
sınıfı üzerine konumlandırılmıştır. dotnet
ekibi Migration notlarında bu yaklaşımı Minimal Hosting
olarak ifade etmişler ve Minimal API
'ı default web template olarak konumlandırmışlardır. ref
namespace Example.Processor.Api;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
Yukarıda .NET 6
versiyonu ile dotnet
cli'da sıfırdan oluşturulan ASP.NET
projesinin Program.cs
dosyasında yer alan kod bloğu paylaşılmıştır. Görüldüğü gibi bütün configürasyonlar tek bir dosya içerisinde konumlandırılıyor ve Startup
sınıfı bulunmuyor. Ek olarak bu yaklaşım ile beraber Host.CreateDefaultBuilder
method'u yerine WebApplication.CreateBuilder
method'u kullanmakta ve IHostBuilder
yerine IHostApplicationBuilder
dönülmektedir. .NET
geliştiricileri .NET 7
ile birlikte de Host.CreateApplicationBuilder
method'u tanıtmış ve yaklaşım olarak Web ve Non-Web uygulamalar için Host
model yaklaşımını aşağıda belirtildiği gibi sürdürülmesini tavsiye etmiştir. Bu konuda David Fowler'n yorumuna ulaşabilirsiniz.
- Web Application'lar için örnek yaklaşım
var builder = WebApplication.CreateBuilder();
builder.Logging.AddConsole();
builder.Services.AddOptions<MyOptions>().BindConfiguration("MyConfig");
builder.Services.AddHostedService<MyWorker>();
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.Run();
- Non-Web Application'lar için örnek yaklaşım
var builder = Host.CreateApplicationBuilder();
builder.Logging.AddConsole();
builder.Services.AddOptions<MyOptions>().BindConfiguration("MyConfig");
builder.Services.AddHostedService<MyWorker>();
var host = builder.Build();
host.Run();
WebApplication
sınıfı web uygulaması için gerekli olan 3 interface'i implemente etmiştir;
-
IHost
- Host'un başlatılması ve sonlandırılmasından sorumludur. -
IApplicationBuilder
- Middleware pipeline'ları oluşturmak için kullanılır -
IEndpointRouteBuilder
- Endpoint'ler için kullanılır
Aynı zamanda aşağıda belirtilen 3 service'i de HostApplicationBuilder.Build()
methodu çağrıldığı an DI container'a otomatik olarak register edilmektedir;
-
IHostApplicationLifetime
- Herhangi bir sınıfa inject edilerek graceful shutdown ve başlatma sonrası operasyonları handle etmek için kullanılır -
IHostLifetime
- Uygulamanın ne zaman başlayacağını ya da sonlanacağını kontrol eder. -
IHostEnvironment
- Uygulama ismi, Root Path, Environment Name vb. bilgiler almak için kullanılır.
IHostApplicationLifetime
, IHostLifecycleService
, IHostedService
Interface'lerinin İncelenmesi
Belirtilen interface'leri ve bu interface'ler ile implemente edilen Lifetime event'lerini kavrayabilmek için aşağıda örnek bir IHostedService
implementasyonu aktarılmıştır;
BackgroundService
birabstract
sınıftır ve background service'ler oluşturmak için kullanılır.IHostedService
iseBackgroundService
tarafından implemente edilen bir interface'dir. İçerisindeHost
'u yönetmek için method'lar barındırır.Worker Service
ise background service oluşturmak için bir template'dir.dotnet new worker
command'ı ile oluşturulur.Sadece
IHostedService
implemente edilerek de background processing gerçekleştirilebilecek service'ler oluşturmak mümkündür fakat burada uygulamanın lifetime event'leri dinlenerek service'de graceful shutdown operasyonunun manuel implemente edilmesi gerekmektedir.BackgroundService
'de ise override edilenExecuteAsync
method'na parametre olarak geçilenCancellationToken
yapılan operasyonlarda kontrol edilerek graceful shutdown operasyonu daha rahat bir şekilde gerçekleştirilebilir.
public class Worker : IHostedLifecycleService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime)
{
applicationLifetime.ApplicationStarted.Register(OnStarted);
applicationLifetime.ApplicationStopping.Register(OnStopping);
applicationLifetime.ApplicationStopped.Register(OnStopped);
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedService StartAsync has been called");
return Task.CompletedTask;
}
public Task StartedAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedLifecycleService StartedAsync has been called");
return Task.CompletedTask;
}
public Task StartingAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedLifecycleService StartingAsync has been called");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedService StopAsync has been called");
return Task.CompletedTask;
}
public Task StoppedAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedLifecycleService StoppedAsync has been called");
return Task.CompletedTask;
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("IHostedLifecycleService StoppingAsync has been called");
return Task.CompletedTask;
}
private void OnStarted()
{
_logger.LogInformation("IHostApplicationLifetime OnStarted has been called");
}
private void OnStopped()
{
_logger.LogInformation("IHostApplicationLifetime OnStopped has been called");
}
private void OnStopping()
{
_logger.LogInformation("IHostApplicationLifetime OnStopping has been called");
}
}
Bir background processing yapmak istediğim için IHostedService
'i implemente etmek gerektiğini iletmiştim. Burada IHostedLifecycleService
kullanmamın sebebi ise zaten bu interface'in IHostedService
'den kalıtım alıyor olmasıdır. Bu interface .Net 8
ile beraber getirildi. Bu sayede uygulamanın lifecycle döngüsüne daha rahat bir şekilde müdehale edebiliyor ve operasyon gerçekleştirebiliyoruz. İçerisinde 4 yeni method barındırıyor. Bunlar StartingAsync
, StartedAsync
, StoppingAsync
ve StoppedAsync
'dir. Ek olarak IHostApplicationLifetime
'ın host build edilirken otomatik olarak register edildiğini iletmiştim. Bu sınıf altında bulunan 3 property aslında birer CancellationToken
ve Host'un lifetime'na göre tetikleniyorlar. Bu token'lara yukarıda ilettiğim şekilde register oluyorum.
Yukarıda oluşturduğum Hosted Service
'i DI container'a aşağıda ilettiğim gibi register ediyorum.
using Example.Worker.Service;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
Uygulamayı çalıştırıp ve ardından da CTRL+C
yaparak sonlandırdığımda aşağıdaki console çıktısı karşıma çıkıyor.
info: Example.Worker.Service.Worker[0]
IHostedLifecycleService StartingAsync has been called
info: Example.Worker.Service.Worker[0]
IHostedService StartAsync has been called
info: Example.Worker.Service.Worker[0]
IHostedLifecycleService StartedAsync has been called
info: Example.Worker.Service.Worker[0]
IHostApplicationLifetime OnStarted has been called
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Codes\Example.Worker.Service
info: Example.Worker.Service.Worker[0]
IHostApplicationLifetime OnStopping has been called
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
info: Example.Worker.Service.Worker[0]
IHostedLifecycleService StoppingAsync has been called
info: Example.Worker.Service.Worker[0]
IHostedService StopAsync has been called
info: Example.Worker.Service.Worker[0]
IHostedLifecycleService StoppedAsync has been called
info: Example.Worker.Service.Worker[0]
IHostApplicationLifetime OnStopped has been called
C:\Codes\Example.Worker.Service\bin\Debug\net8.0\Example.Worker.Service.exe (process 35456) exited with code 0.
Çıktıyı kontrol ettiğimizde sıranın aşağıdaki gibi olduğu görünmektedir;
-
IHostedLifecycleService.StartingAsync
çağrıldığı görülmüştür. Uygulama başlamadan önce çağrılan method'dur. -
IHostedService.StartAsync
çağrılmıştır. Host'un service'i başlatmaya hazır olduğunda çağrılan method'dur. -
IHostedLifecycleService.StartedAsync
çağrılmıştır.IHostedService.StartAsync
hemen sonra yani başlatma işlemi bittikten sonra çağrılır. -
IHostApplicationLifetime.ApplicationStarted
Host'un tamamen başladığını ifade eder.
Uygulama stop edildikten sonra aşağıdaki sıralamanın gerçekleştiği gözlemlenmiştir;
-
IHostApplicationLifetime.ApplicationStopping
uygulama graceful shutdown operasyonu gerçekleştirmeye başladığında tetiklenir. -
IHostedLifecycleService.StoppingAsync
uygulama sonlandırılmaya başlanırken öncesinde çağrılır.IHostedService.StopAsync
operasyonunun hemen öncesinde yer alır. -
IHostedService.StopAsync
uygulama graceful shutdown operasyonu gerçekleştirir. -
IHostedLifecycleService.StoppedAsync
uygulamanın graceful shutdown operasyonunu tamamlandığında çağrılır. -
IHostApplicationLifetime.ApplicationStopped
uygulamanın graceful shutdown operasyonunu tamamlandığını ifade eder.
Burada yer alan diğer önemli bir nokta ise uygulamanın CTRL+C
kombinasyonu ile sonlandırılıyor olmasıdır. Host default'da otomatik olarak IHostLifetime
interface'ni ConsoleLifetime
olarak register etmiştir. Web ve Background Service için de bu geçerlidir. IHostLifetime
interface'de uygulamanın ne zaman başlayacağını ve sonlanacağını kontrol ettiğini ifade etmiştim. ConsoleLifetime
içerisinde de yukarıda ilettiğim kombinasyon ile tuşlara basılarak uygulamaya SIGINT
sinyali vermiş oluyoruz. Kubernetes'de ilk bölümde anlattığım nedenlerden dolayı pod'un sonlandırılması için container'a SIGTERM
sinyali göndermektedir. Bu belirtilen sinyaller neticesinde graceful shutdown operasyonu gerçekleştirilir.
.Net 6
öncesinde posix signal'ler desteklenip signal tipine göre handle edilmiyordu..Net 6
sonrasıConsoleLifetime
,SIGINT
,SIGQUIT
,SIGTERM
sinyallerini dinleyerek graceful shutdown operasyonunu gerçekleştirebilir hale geldi.
Host'un Shutdown Süreci
Default'da .Net 6
ve sonrasında generic host'un IHostLifetime
interface'ni ConsoleLifetime
olarak implemente edildiğini aktarmıştım. Host'un da gracefully olarak sonlandırılması için ConsoleLifetime
'a aşağıdaki sinyaller gönderilerek bu operasyon gerçekleştirilebilir;
-
SIGINT
ya daCTRL+C
-
SIGQUIT
ya daCTRL+BREAK
(Windows) -
SIGTERM
(Kubernetes tarafında pod'un sonlandırılması için container'a gönderilen sinyaldocker stop
)
.Net 6
öncesindeSIGTERM
sinyali geldiğinde gracefully olarak shut down operasyonu gerçekleştirilemiyordu. Bu durum ile alakalı work around olarakConsoleLifetime
tarafında,System.AppDomain.ProcessExit
event'ni dinlenerekProcessExit
thread'i stop edilir ve host'un durması beklenirdi.
Yukarıda gracefully shut down operasyonu sırasında gerçekleşen süreç aktarılmıştır. Sırasıyla;
- Kubernetes'den ya da kullanıcıdan
SIGTERM
sinyali geliyor. Bu sinyal neticesinde deIHostApplicationLifetime
altında bulunanStopApplication()
method'u tetikleniyor veApplicationStopping
event'i fırlatılıyor. ÖncesindeIHost.WaitForShutdownAsync
methodu bu belirtilen event'i dinliyor ve event tetiklendiğinden dolayıMain
işletimi ublock'luyor. -
IHost.StopAsync()
method'u tetikleniyor ve bu method içerisinden deIHostedService.StopAsync()
tetiklenerek her bir hosted service'in stop edilmesini sağlayıp ardından da stop edildiğine dair event'leri fırlatıyor. - Son olarak
IHost.WaitForShutdownAsync
tamamlanıyor ve uygulamanın işletmesi gereken code block'ları yürütülüp gracefully shut-down operasyonu gerçekleştiriliyor.
Host tarafında configürasyon gerçekleştirilerek ShutdownTimeout
ayarlanması mümkündür. Bu değer IHost.StopAsync()
method'u için belirtilen timeout süresidir ve default değeri 30 saniyedir.
Kind
İle Kubernetes Cluster'ın Ayağa Kaldırılması
OpenSource olarak geliştirilen Kind
ürünü ile local'de hızlı bir şekilde Kubernetes cluster'ları ayağa kaldırmak mümkündür. Bu makale için de geliştirilen uygulamanın yayınlanacağı Kubernetes cluster'ı Kind
ürünü ile oluşturulmuştur.
Öncelikle Kind
cli'ı local'e aşağıda belirtilen komut ile yüklüyoruz;
curl.exe -Lo kind-windows-amd64.exe https://kind.sigs.k8s.io/dl/v0.23.0/kind-windows-amd64
Move-Item .\kind-windows-amd64.exe c:\some-dir-in-your-PATH\kind.exe
Her seferinde cli'ın olduğu dizine gidip işlem yapmamak için belirttiğiniz dizini
Path
Environment Variable'ı içerisine eklemeyi unutmayınız.
Ardından 3 worker node içeren bir cluster ayağa kaldırmak için aşağıda belirtilen yaml dosyası oluşturulur.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
-
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 8081
protocol: TCP
- containerPort: 443
hostPort: 8443
protocol: TCP
- role: worker
- role: worker
- role: worker
Yukarıda ilettiğim yaml dosyasını incelediğimizde; her bir node rolü için tanım olduğu ve local'de 8081 ve 8443 port'larına gönderilen isteklerin cluster üzerinde kuracağımız ingress controller'a forward etmek için tanımlandığı gözlenmektedir.
kind create cluster --config .\kind-cluster.yaml
Yukarıda ilettiğim komut ile cluster'ı ayağa kaldırıyorum
kind create cluster --config .\kind-config\kind-cluster.yaml
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.30.0) 🖼
✓ Preparing nodes 📦 📦 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
Ardından aşağıda ilettiğim komut ile ingress controller'ı kuruyorum;
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
Kind
'a özel hazırlanan bu manifest dosyası içerisinde bazı patch'ler ve forward operasyonu için ayarlamar mevcuttur.
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created
Yukarıdaki işlemler tamamlandıktan sonra cluster'ın kullanıma hazır hale gelmiş bulunuyor.
Web API'ın Oluşturulması
.Net 8
ile bir web api projesi oluşturduğumuzda karşıma aşağıda belirtildiği gibi bir Program.cs
dosyası çıkıyor.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
İhtiyaç olmayan service'ler kaldırılmıştır
Controller ve Action isimlerini aşağıda belirtildiği gibi düzenliyorum. Bir önceki bölümde ShutdownTimeout
'un default değerinin 30 saniye olduğu belirtmiştim. Operasyon hata alması için action içerisinde 35 saniye bir bekleme gerçekleştirip response dönecek şekilde düzenleme yapıyorum.
[ApiController]
[Route("[controller]")]
public class PingPongController : ControllerBase
{
private readonly ILogger<PingPongController> _logger;
public PingPongController(ILogger<PingPongController> logger)
{
_logger = logger;
}
[HttpGet(Name = "Ping")]
public async Task<IActionResult> Ping()
{
await Task.Delay(TimeSpan.FromSeconds(35));
return Ok("Pong");
}
}
Kind
ile oluşturduğumuz Kubernetes cluster'a deployment'ın gerçekleştirilebilmesi için aşağıda ilettiğim deployment manifest'i oluşturuyorum.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pingapi-deployment
labels:
app: pingapi
spec:
replicas: 1
selector:
matchLabels:
app: pingapi
template:
metadata:
labels:
app: pingapi
spec:
containers:
- name: pingapi
image: pingapi:0.1
ports:
- containerPort: 8080
resources:
limits:
cpu: "0.5"
memory: "150Mi"
requests:
cpu: "0.1"
memory: "150Mi"
.Net 8
ile birlikte yayınlanan image'larda uygulamanın çalıştığı default port80
'den8080
'e değiştirilmiştir. Bu sebeple manifest'de port bilgisi8080
olarak ayarlanmıştır. İlgili doc
Dockerfile
'ı aşağıda belirttiğim gibi basitçe oluşturuyorum;
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
COPY ./Publish .
ENTRYPOINT ["dotnet", "Example.Ping.Api.dll"]
Service'in Deploy Edilmesi ve Testin Gerçekleştirilmesi
Öncelikle uygulamamı Publish
dizini altında publish ediyorum
dotnet publish -o ./Publish
Ardından image'ı build ediyorum;
docker build -t pingapi:0.1 .
Cluster üzerinde deployment manifest'i apply etmeden önce etiketlediğim image'ı aşağıda belirtilen komut ile her bir node üzerine aktarıyorum, diğer türlü bir image repository'e bu image'ı atmam ve cluster tarafından pull etmem gibi bir ihtiyaç oluşacaktı. Kind burda bize kolaylık sağlıyor.
kind load docker-image pingapi:0.1
kind load docker-image pingapi:0.1
Image: "pingapi:0.1" with ID "sha256:2e5cfec8e475ed2d2ccfd0ae9753a7f5feda0e01de0081718ab678203d25edcf" not yet present on node "kind-worker3", loading...
Image: "pingapi:0.1" with ID "sha256:2e5cfec8e475ed2d2ccfd0ae9753a7f5feda0e01de0081718ab678203d25edcf" not yet present on node "kind-worker", loading...
Image: "pingapi:0.1" with ID "sha256:2e5cfec8e475ed2d2ccfd0ae9753a7f5feda0e01de0081718ab678203d25edcf" not yet present on node "kind-control-plane", loading...
Image: "pingapi:0.1" with ID "sha256:2e5cfec8e475ed2d2ccfd0ae9753a7f5feda0e01de0081718ab678203d25edcf" not yet present on node "kind-worker2", loading...
Deployment'ı aşağıda belirttiğim komutu ile apply ediyorum;
kubectl apply -f .\Kubernetes\deployment.yaml
Container'ın 8080
portunu aşağıda belirttiğim komut ile forward ediyorum;
kubectl port-forward deployment/pingapi-deployment -n default 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Artık teste başlayabiliriz, öncelikle httpstat http://localhost:8080/pingpong/ping
komutu ile istek atıyorum ardından da pod'u delete etmek için kubectl delete pod/pingapi-deployment-5c78cbdfc-bfd9b
komutunu çalıştırıyorum. Beklediğimiz üzere hem Kubernetes tarafında terminationGracePeriodSeconds
süresinin deployment manifest'de belirtmediğimiz için default değer olan 30 saniye olmasından hem de .Net
tarafında Host
objesinin ShutdownTimeout
belirtmediğimiz için default değer olan 30 saniye olmasından dolayı attığımız istek için hatada da göründüğü üzere bağlantının sonlandırıldığı görünmektedir.
Yeni bir image versiyonu ile deploy çıkılarak da benzer senaryo test edilebilir buradaki amaç pod'a
SIGTERM
sinyali göndermek olduğu için pod silme operasyonu ile test işlemi gerçekleştirilmiştir.
Bu durumu aşmak için öncelikle uygulama tarafında Host
'u configüre ediyorum ardından da deployment manifest altında spec.terminationGracePeriodSeconds
adlı değeri güncelliyorum.
Program.cs
dosyasının yeni hali aşağıda belirtildiği gibi oluşturulmuştur;
namespace Example.Ping.Api;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureHostOptions(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(45));
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
deployment.yaml
dosyasının da yeni hali aşağıdaki gibidir;
apiVersion: apps/v1
kind: Deployment
metadata:
name: pingapi-deployment
labels:
app: pingapi
spec:
replicas: 1
selector:
matchLabels:
app: pingapi
template:
metadata:
labels:
app: pingapi
spec:
containers:
- name: pingapi
image: pingapi:0.2
ports:
- containerPort: 8080
resources:
limits:
cpu: "0.5"
memory: "150Mi"
requests:
cpu: "0.1"
memory: "150Mi"
terminationGracePeriodSeconds: 50
deployment manifest içerisinde yeni build ettiğimiz image'ı
pingapi:0.2
olarak update ediyoruz.
Belirtilen günceleme yapıldıktan sonra testi tekrar uyguladığımızda bir problem ile karşılaşılmadığı gözlemlenmektedir;
Kubernetes Ingress ile Control Plane Arasındaki Gecikme
Yukarıda yaptığımız ayarlamalara rağmen yoğun kullanılan service'ler özelinde RollingUpdate
aşamasında birkaç istek özelinde de olsa hata alma ihtimalleri bulunmaktadır. Bunun sebebi ingress ve control plane arasındaki gecikmeden kaynaklanmaktadır.
Bunun sebebi Kubernetes içerisinde ingress ve Kubernetes control plane farklı entity'lerdir ve bağımsız olarak operasyonlarını gerçekleştirmektedir. Kubernetes bir pod'un sonlandırmak istediğinde control plane sonlandırılacak pod'ları Service'den kaldırır ve ingress'de bundan haberdar olur ve sonlandırlacak pod'lara request'leri yönlendirmeyi sonlandırır. Bu iki operasyon arasında bir gecikme mevcuttur çünkü ingress belli aralıklarla Service'lerde yapılan bu değişiklikleri kendi tarafında güncellemektedir. Bu durum da terminating
status içerisinde bulunan pod'lara az bir request özelinde isteklerin iletilebilmesine sebep olmaktadır.
..Net
tarafında uygulama IHost.StopAsync()
çağrıldıktan sonra hali hazırda açık olan connection'lar üzerinden yeni requestlerin gelmesi ve aynı zamanda da yeni bir connection açılmasına izin vermez. Bu sebeple de bu aradaki gecikme IHost.StopAsync()
işlemi başladığında yeni isteklerin gelebileceği anlamına gelmektedir. Bu durum istek atan taraf için hata almasına neden olacaktır.
Bu duruma bir çözüm olarak aşağıda ileteceğim yöntem dotnet ekibi tarafından tavsiye edilmiştir. ref
Önceki bölümlerde IHostLifetime
'ın uygulamanın ne zaman başlayacağını ya da durdurulacağını kontrol ettiği aktarılmıştı. Bu sebeple öncelikle yeni bir IHostLifetime
implementasyonu gerçekleştiriyoruz.
using System.Runtime.InteropServices;
public class DelayedShutdownHostLifetime : IHostLifetime, IDisposable
{
private IHostApplicationLifetime _applicationLifetime;
private TimeSpan _delay;
private IEnumerable<IDisposable>? _disposables;
public DelayedShutdownHostLifetime(IHostApplicationLifetime applicationLifetime, TimeSpan delay) {
_applicationLifetime = applicationLifetime;
_delay = delay;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task WaitForStartAsync(CancellationToken cancellationToken)
{
_disposables = new IDisposable[]
{
PosixSignalRegistration.Create(PosixSignal.SIGINT, HandleSignal),
PosixSignalRegistration.Create(PosixSignal.SIGQUIT, HandleSignal),
PosixSignalRegistration.Create(PosixSignal.SIGTERM, HandleSignal)
};
return Task.CompletedTask;
}
protected void HandleSignal(PosixSignalContext ctx)
{
ctx.Cancel = true;
Task.Delay(_delay).ContinueWith(t => _applicationLifetime.StopApplication());
}
public void Dispose()
{
foreach (var disposable in _disposables ?? Enumerable.Empty<IDisposable>())
{
disposable.Dispose();
}
}
}
Yapılan işlem incelendiğinde uygulama en başta ayağa kalkarken belli POSIX
sinyalleri için registration operasyonu gerçekleştirilmiştir. Bu sayede uygulama tarafına bu sinyallerden biri geldiğinde IHostApplicationLifetime.StopApplication()
operasyonunu başlatmadan önce Task.Delay
ile bir gecikme tanımlanmıştır. Bu gecikme sayesinde gracefully shut-down işlemi belirtilen sinyaller geldiği an başlamayacak ve belirtilen gecikme zarfında da pod'a gelen yeni istekler karşılanabilecektir.
Son aşama olarak Program.cs
altında yeni oluşturduğum IHostLifetime
implementasyonunu register ediyorum.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureHostOptions(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(45));
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSingleton<IHostLifetime>(sp =>
new DelayedShutdownHostLifetime(sp.GetRequiredService<IHostApplicationLifetime>(), TimeSpan.FromSeconds(5)));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
}
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
HostApplicationBuilder.Build()
method'u içerisindeIHostApplicationLifetime
veIHostLifetime
'ın service olarak register edildiği aktarılmıştı. BuradaIHostLifetime
tekrar register edilerek belirtilenDelayedShutdownHostLifetime
implementasyonu ile kullanılması sağlanmıştır.
Son yapılan işlemle beraber Kubernetes tarafında kesintisiz bir deployment işlemi sağlanmış bulunmaktadır. Okuduğunuz için teşekkürler :)
Referanslar
https://learn.microsoft.com/en-us/dotnet/core/extensions/workers
https://learn.microsoft.com/en-us/dotnet/core/extensions/generic-host?tabs=appbuilder#host-shutdown
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostlifetime?view=net-8.0
https://blog.sebastian-daschner.com/entries/zero-downtime-updates-kubernetes
Top comments (0)