Pod Disruption — Graceful Termination Trên Windows
Vì Sao Quan Trọng
Graceful termination trên Linux dựa vào một cơ chế rất quen thuộc: kubelet gửi SIGTERM, app có terminationGracePeriodSeconds để tự cleanup, hết hạn thì SIGKILL. Toàn bộ mental model này xây trên POSIX signal. Windows không có POSIX signal — không có SIGTERM, không có SIGKILL ở kernel level theo nghĩa Linux. Nếu application code không được viết để nhận biết cơ chế shutdown tương đương của Windows, container sẽ luôn bị force-kill sau khi hết terminationGracePeriodSeconds, dù về mặt Kubernetes API mọi thứ "đúng chuẩn".
Hệ quả thực tế nếu bỏ qua điều này: mọi rolling update, node drain, hoặc scale-down đều có khả năng cắt ngang request đang xử lý, gây lỗi 5xx ngắn hạn ngay tại thời điểm deploy — kể cả khi team đã cấu hình terminationGracePeriodSeconds và readiness/liveness probe đầy đủ.
SIGTERM Không Tồn Tại — CTRL_SHUTDOWN_EVENT Thay Thế
Khi kubelet muốn dừng container gracefully, với Linux nó gửi SIGTERM tới process PID 1 trong container. Với Windows container, containerd/HCS giả lập tín hiệu tương đương bằng cách gửi console control event tới process:
Linux: kubelet → containerd → runc → kill(pid, SIGTERM)
Windows: kubelet → containerd → HCS shim → CTRL_SHUTDOWN_EVENT tới process consoleVấn đề then chốt: không phải mọi Windows process/framework đều tự động lắng nghe và xử lý CTRL_SHUTDOWN_EVENT theo cách hữu ích — application phải explicitly register handler, tương tự việc phải viết code bắt SIGTERM trên Linux (process.on('SIGTERM') ở Node.js, signal handler ở Go). Sự khác biệt là ecosystem .NET có sẵn abstraction chuẩn cho việc này (IHostApplicationLifetime), nhưng team vẫn phải chủ động dùng nó — mặc định generic Windows process không tự cleanup connection đang mở, không tự flush buffer, không tự drain in-flight request.
Pattern Chuẩn Cho .NET: IHostApplicationLifetime
Với ASP.NET Core / Generic Host, cách chuẩn để handle graceful shutdown:
public class GracefulShutdownService : IHostedService
{
private readonly IHostApplicationLifetime _lifetime;
public GracefulShutdownService(IHostApplicationLifetime lifetime)
{
_lifetime = lifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_lifetime.ApplicationStopping.Register(OnStopping);
return Task.CompletedTask;
}
private void OnStopping()
{
// 1. Đánh dấu readiness probe fail ngay lập tức
// (để load balancer/Service dừng route traffic mới)
// 2. Drain in-flight request đang xử lý
// 3. Đóng connection pool, flush log, cleanup resource
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}Kestrel (built-in ASP.NET Core server) tự động respect ApplicationStopping event và có shutdown timeout riêng (HostOptions.ShutdownTimeout) — cần đồng bộ giá trị này với terminationGracePeriodSeconds ở Pod spec, tương tự nguyên tắc "app shutdown timeout phải nhỏ hơn Kubernetes grace period" áp dụng chung cho mọi ngôn ngữ trên cả Linux/Windows.
spec:
terminationGracePeriodSeconds: 45 # Kubernetes chờ tối đa 45s
containers:
- name: app
env:
- name: DOTNET_SHUTDOWNTIMEOUTSECONDS
value: "30" # App tự cleanup trong 30s, để dư buffer 15s cho container fully exit.NET Framework Legacy: Vấn Đề Lớn Hơn
Với .NET Framework cũ (thường là lý do chính team phải chạy Windows container — legacy app không thể port sang .NET Core/.NET 5+), tình huống khó hơn nhiều:
- Không có
IHostApplicationLifetimesẵn có (đây là khái niệm từ .NET Core/Generic Host) - IIS-hosted application (rất phổ biến với .NET Framework) có shutdown sequence riêng của IIS/w3wp.exe, không map trực tiếp tới Kubernetes lifecycle
- Cần custom wrapper (ví dụ: một small watcher process nhận console event rồi gọi IIS graceful stop API, hoặc dùng
AppDomain.ProcessExithandler) — đây thường là phần tốn effort engineering nhất khi containerize legacy .NET Framework app, nhiều hơn cả phần networking/image size
Khuyến nghị thực tế: với .NET Framework/IIS workload, đầu tư thời gian viết một shutdown handler wrapper đúng cách trước khi go-live, không nên coi đây là optimization sau — thiếu nó nghĩa là mọi deploy đều risk lỗi request cho user thật.
Tác Động Tới Node Drain & Rolling Update Latency
Ngoài graceful shutdown ở application level, có một yếu tố vận hành khác cần tính: container Windows stop chậm hơn Linux container đáng kể về thời gian tuyệt đối, ngay cả khi graceful shutdown handler hoạt động đúng — do:
- HCS teardown container (release Job Object, cleanup HNS endpoint) có overhead cao hơn runc teardown trên Linux
- Nếu app dùng .NET, CLR shutdown (finalize object, release unmanaged resource) tốn thêm thời gian so với process Linux native binary exit gần như instant
Hệ quả vận hành:
- Node drain cho Windows node pool nên tính buffer thời gian dài hơn trong maintenance window planning so với ước tính dựa trên kinh nghiệm Linux
- PodDisruptionBudget (PDB) hoạt động semantics giống hoàn toàn Linux (không có khác biệt OS ở tầng Kubernetes API) — nhưng vì mỗi pod terminate chậm hơn, tổng thời gian drain cả node pool kéo dài hơn tương ứng, cần review lại
maxUnavailablenếu SLA maintenance window bị giới hạn chặt
Checklist Graceful Termination Cho Windows Workload
- Xác nhận application có register handler cho shutdown event tương đương (
IHostApplicationLifetime, hoặc custom handler cho .NET Framework) - Đồng bộ app-level shutdown timeout với
terminationGracePeriodSeconds, để buffer đủ cho container teardown overhead - Đảm bảo readiness probe fail ngay khi nhận shutdown signal, trước khi bắt đầu drain in-flight request — tránh traffic mới route vào pod đang đóng
- Test thực tế: gửi request liên tục trong lúc rolling update, verify không có lỗi 5xx — đừng chỉ tin vào cấu hình đúng trên giấy
- Tính buffer thời gian dài hơn cho maintenance window liên quan tới node drain trên Windows node pool