.NET ile Mikroservis Mimarisi: Tasarım ve Uygulama
# .NET ile Mikroservis Mimarisi
Mikroservisler, büyük sistemleri bağımsız deploy edilebilir parçalara böler — ama bu dönüşüm ancak servis sınırları ve operasyonel olgunluk yerindeyse işe yarar. Dağıtık sistemler üzerinde çalıştığım projelerde, başarılı bir mikroservis geçişi ile sancılı bir sürecin arasındaki fark her zaman hazırlık aşamasına dayanıyordu: neden parçalıyorsunuz sorusunu cevaplayabilmek, nasıl sorusundan çok daha önemli.
Tasarım Temelleri
Servis Sınırları
Mikroservis mimarisinde en kritik karar, sınırları nereye çizdiğinizdir. Yanlış çizilen sınırlar, yerini aldığı monolit'ten daha fazla bağımlılık yaratır.
Dağıtık sistemler kurduğum projelerde, ekiplerin en çok pişman olduğu şey çok erken bölmek. İyi yapılandırılmış bir monolit ile başlayın, dikişleri belirleyin, sonra ancak net bir operasyonel veya organizasyonel neden olduğunda ayrıştırın.
Bounded Context ve Domain-Driven Design
Mikroservis sınırları, DDD bounded context'leri ile hizalanmalı. Her servis tutarlı bir domain modelini kapsar:
İki servis aynı kavrama ihtiyaç duyduğunda (ör. "Ürün"), her biri kendi temsilini tutar. Sipariş Servisi yalnızca `ProductId`, `Name` ve `Price` bilgisine ihtiyaç duyarken, Envanter Servisi `ProductId`, `SKU`, `WarehouseLocation` ve `StockCount` bilgilerine ihtiyaç duyar.
API Gateway Pattern
API Gateway, istemciler ile servisleriniz arasında oturarak çapraz kesen sorunları ele alır:
class=class="code-string">"code-comment">// Program.cs — YARP tabanlı API Gateway
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection(class="code-string">"ReverseProxy"));
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter(class="code-string">"default", opt =>
{
opt.PermitLimit = class="code-number">100;
opt.Window = TimeSpan.FromMinutes(class="code-number">1);
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapReverseProxy();
app.Run();Servisler Arası İletişim Kalıpları
Doğru iletişim kalıbını seçmek kritik önemdedir. Tek bir en iyi seçenek yoktur — her birinin farklı bağlamlarda önemli olan artı ve eksileri vardır.
HTTP/REST
En basit seçenek. Anlık cevap gerektiren senkron istek-yanıt akışlarında iyi çalışır.
Artıları: Evrensel araç desteği, kolay hata ayıklama, okunabilir payload'lar
Eksileri: Çağıran ve çağrılan arasında sıkı bağımlılık, zincirleme hatalar, daha yüksek gecikme
class=class="code-string">"code-comment">// Dayanıklılık politikalarıyla tiplenmiş HTTP istemcisi
builder.Services.AddHttpClient<IOrderServiceClient, OrderServiceClient>(client =>
{
client.BaseAddress = new Uri(class="code-string">"https:class="code-commentclass="code-string">">//order-service:class="code-number">5001");
client.Timeout = TimeSpan.FromSeconds(class="code-number">10);
})
.AddStandardResilienceHandler();gRPC
HTTP/2 üzerine inşa edilmiş binary protokol. Performansın önemli olduğu dahili servisler arası çağrılar için ideal.
Artıları: .proto dosyaları ile güçlü kontratlar, streaming desteği, JSON'dan yaklaşık 10 kat daha hızlı serializasyon
Eksileri: gRPC-Web olmadan tarayıcı dostu değil, hata ayıklaması zor, proto yönetimi gerektirir
class=class="code-string">"code-comment">// inventory.proto
syntax = class="code-string">"proto3";
option csharp_namespace = class="code-string">"InventoryService.Grpc";
service InventoryGrpc {
rpc CheckStock (StockRequest) returns (StockResponse);
rpc ReserveItems (ReserveRequest) returns (ReserveResponse);
rpc StreamStockUpdates (StockSubscription) returns (stream StockUpdate);
}
message StockRequest {
string product_id = class="code-number">1;
string warehouse_id = class="code-number">2;
}
message StockResponse {
string product_id = class="code-number">1;
int32 available_quantity = class="code-number">2;
bool is_available = class="code-number">3;
}
message ReserveRequest {
string order_id = class="code-number">1;
repeated ReserveItem items = class="code-number">2;
}
message ReserveItem {
string product_id = class="code-number">1;
int32 quantity = class="code-number">2;
}
message ReserveResponse {
bool success = class="code-number">1;
string reservation_id = class="code-number">2;
string failure_reason = class="code-number">3;
}
message StockSubscription {
repeated string product_ids = class="code-number">1;
}
message StockUpdate {
string product_id = class="code-number">1;
int32 new_quantity = class="code-number">2;
string timestamp = class="code-number">3;
}class=class="code-string">"code-comment">// gRPC sunucu implementasyonu
public class InventoryGrpcService : InventoryGrpc.InventoryGrpcBase
{
private readonly IInventoryRepository _repository;
private readonly ILogger<InventoryGrpcService> _logger;
public InventoryGrpcService(
IInventoryRepository repository,
ILogger<InventoryGrpcService> logger)
{
_repository = repository;
_logger = logger;
}
public override async Task<StockResponse> CheckStock(
StockRequest request, ServerCallContext context)
{
var stock = await _repository.GetStockAsync(
request.ProductId, request.WarehouseId);
return new StockResponse
{
ProductId = request.ProductId,
AvailableQuantity = stock?.Quantity ?? class="code-number">0,
IsAvailable = (stock?.Quantity ?? class="code-number">0) > class="code-number">0
};
}
}RabbitMQ ile Asenkron Mesajlaşma
Anlık yanıt gerektirmeyen iş akışları için mesaj kuyrukları, servisleri hem zaman hem de erişilebilirlik açısından birbirinden ayırır. Üretim sistemlerinde en çok başvurduğum kalıp budur.
class=class="code-string">"code-comment">// MassTransit ile RabbitMQ consumer
public class OrderCreatedConsumer : IConsumer<OrderCreatedEvent>
{
private readonly IInventoryRepository _inventory;
private readonly ILogger<OrderCreatedConsumer> _logger;
public OrderCreatedConsumer(
IInventoryRepository inventory,
ILogger<OrderCreatedConsumer> logger)
{
_inventory = inventory;
_logger = logger;
}
public async Task Consume(ConsumeContext<OrderCreatedEvent> context)
{
var order = context.Message;
_logger.LogInformation(
class="code-string">"Sipariş {OrderId} için envanter rezervasyonu işleniyor", order.OrderId);
try
{
foreach (var item in order.Items)
{
await _inventory.ReserveStockAsync(
item.ProductId, item.Quantity, order.OrderId);
}
await context.Publish(new InventoryReservedEvent
{
OrderId = order.OrderId,
ReservedAt = DateTime.UtcNow
});
}
catch (InsufficientStockException ex)
{
_logger.LogWarning(ex,
class="code-string">"Sipariş {OrderId} için yetersiz stok", order.OrderId);
await context.Publish(new InventoryReservationFailedEvent
{
OrderId = order.OrderId,
Reason = ex.Message
});
}
}
}
class=class="code-string">"code-comment">// MassTransit kayıt yapılandırması
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<OrderCreatedConsumer>();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host(class="code-string">"rabbitmq", class="code-string">"/", h =>
{
h.Username(class="code-string">"guest");
h.Password(class="code-string">"guest");
});
cfg.ReceiveEndpoint(class="code-string">"inventory-order-created", e =>
{
e.ConfigureConsumer<OrderCreatedConsumer>(ctx);
e.UseMessageRetry(r => r.Intervals(
TimeSpan.FromSeconds(class="code-number">1),
TimeSpan.FromSeconds(class="code-number">5),
TimeSpan.FromSeconds(class="code-number">15)));
});
});
});Kalıp Karşılaştırma Özeti
| Kriter | HTTP/REST | gRPC | Asenkron Mesajlaşma |
|---|---|---|---|
| Bağımlılık | Yüksek | Yüksek | Düşük |
| Gecikme | Orta | Düşük | Değişken |
| Güvenilirlik | Retry gerektirir | Retry gerektirir | Yerleşik dayanıklılık |
| Hata ayıklama | Kolay | Orta | Zor |
| En uygun olduğu alan | Dış API'lar | Dahili yüksek performans | Olay güdümlü iş akışları |
Monolit'ten Mikroservislere Geçiş
Üzerinde çalıştığım her başarılı mikroservis projesi, sıfırdan değil monolit geçişi olarak başladı. İşte savaş alanında test edilmiş bir strateji.
Aşama 1: Monolit'i Hazırlayın
Bir şey çıkarmadan önce, dahili yapıyı düzenleyin. Monolit içinde gelecekteki servis sınırlarınızı yansıtan modül sınırları oluşturun.
Aşama 2: Strangler Fig ile Ayrıştırma
Bir seferde bir servis çıkarın, en az bağımlı domain'den başlayarak. Trafiği, monolit mi yoksa yeni servis mi çağırılacağına karar veren bir cephe (facade) üzerinden yönlendirin.
Aşama 3: Veri Göçü
Bu en zor kısımdır. Her servis kendi veri deposuna sahip olmalıdır.
Aşama 4: Geçiş Tamamlama
Konteynerizasyon ve Orkestrasyon
Docker Yapılandırması
İyi yapılandırılmış bir Dockerfile, üretim imajlarını minimal tutmak için çok aşamalı build kullanır:
# Build aşaması
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["OrderService/OrderService.csproj", "OrderService/"]
RUN dotnet restore "OrderService/OrderService.csproj"
COPY . .
WORKDIR "/src/OrderService"
RUN dotnet publish -c Release -o /app/publish --no-restore
# Çalışma zamanı aşaması
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
RUN adduser --disabled-password --gecos "" appuser
USER appuser
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "OrderService.dll"]Yerel Geliştirme için Docker Compose
# docker-compose.yml
version: "3.8"
services:
order-service:
build:
context: .
dockerfile: OrderService/Dockerfile
ports:
- "5001:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__OrderDb=Host=order-db;Database=orders;Username=postgres;Password=postgres
- RabbitMq__Host=rabbitmq
depends_on:
order-db:
condition: service_healthy
rabbitmq:
condition: service_healthy
inventory-service:
build:
context: .
dockerfile: InventoryService/Dockerfile
ports:
- "5002:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__InventoryDb=Host=inventory-db;Database=inventory;Username=postgres;Password=postgres
- RabbitMq__Host=rabbitmq
depends_on:
inventory-db:
condition: service_healthy
rabbitmq:
condition: service_healthy
payment-service:
build:
context: .
dockerfile: PaymentService/Dockerfile
ports:
- "5003:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__PaymentDb=Host=payment-db;Database=payments;Username=postgres;Password=postgres
- RabbitMq__Host=rabbitmq
depends_on:
payment-db:
condition: service_healthy
rabbitmq:
condition: service_healthy
order-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: orders
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- order-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
inventory-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: inventory
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- inventory-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
payment-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: payments
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- payment-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672"
- "15672:15672"
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 10s
timeout: 5s
retries: 5
volumes:
order-data:
inventory-data:
payment-data:Sağlık Kontrolleri ve Dayanıklılık
Her mikroservis sağlık bilgisi sunmalıdır. Bu olmadan, orkestratörler akıllı yönlendirme veya yeniden başlatma kararları veremez.
class=class="code-string">"code-comment">// Program.cs — Sağlık kontrolü yapılandırması
builder.Services.AddHealthChecks()
.AddNpgSql(
builder.Configuration.GetConnectionString(class="code-string">"OrderDb")!,
name: class="code-string">"postgresql",
tags: new[] { class="code-string">"db", class="code-string">"ready" })
.AddRabbitMQ(
new Uri(class="code-string">"amqp:class="code-commentclass="code-string">">//guest:guest@rabbitmq:class="code-number">5672"),
name: class="code-string">"rabbitmq",
tags: new[] { class="code-string">"messaging", class="code-string">"ready" })
.AddCheck<OrderProcessingHealthCheck>(
class="code-string">"order-processing",
tags: new[] { class="code-string">"custom", class="code-string">"ready" });
var app = builder.Build();
class=class="code-string">"code-comment">// Canlılık: Süreç hayatta mı?
app.MapHealthChecks(class="code-string">"/health/live", new HealthCheckOptions
{
Predicate = _ => false class=class="code-string">"code-comment">// Kontrol yok, sadece uygulamanın yanıt verdiğini doğrular
});
class=class="code-string">"code-comment">// Hazırlık: Bu servis trafik alabilir mi?
app.MapHealthChecks(class="code-string">"/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains(class="code-string">"ready"),
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});class=class="code-string">"code-comment">// Özel sağlık kontrolü implementasyonu
public class OrderProcessingHealthCheck : IHealthCheck
{
private readonly IOrderRepository _repository;
public OrderProcessingHealthCheck(IOrderRepository repository)
{
_repository = repository;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var stuckOrders = await _repository.GetStuckOrdersCountAsync(
TimeSpan.FromMinutes(class="code-number">30), cancellationToken);
if (stuckOrders > class="code-number">50)
return HealthCheckResult.Unhealthy(
$class="code-string">"{stuckOrders} sipariş class="code-number">30 dakikadan fazla işleme takılmış");
if (stuckOrders > class="code-number">10)
return HealthCheckResult.Degraded(
$class="code-string">"{stuckOrders} sipariş işleme takılmış");
return HealthCheckResult.Healthy();
}
}Dağıtık İzleme ve Gözlemlenebilirlik
Bir monolit'te stack trace her şeyi anlatır. Mikroservislerde tek bir kullanıcı isteği beş servisten geçebilir — ve dağıtık izleme olmadan hata ayıklama neredeyse imkansızdır.
OpenTelemetry Entegrasyonu
class=class="code-string">"code-comment">// Program.cs — OpenTelemetry kurulumu
builder.Services.AddOpenTelemetry()
.ConfigureResource(res => res
.AddService(
serviceName: class="code-string">"OrderService",
serviceVersion: class="code-string">"class="code-number">1.0.class="code-number">0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation()
.AddSource(class="code-string">"MassTransit")
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(class="code-string">"http:class="code-commentclass="code-string">">//otel-collector:class="code-number">4317");
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter(class="code-string">"OrderService.Metrics")
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(class="code-string">"http:class="code-commentclass="code-string">">//otel-collector:class="code-number">4317");
}));Korelasyonlu Yapılandırılmış Loglama
class=class="code-string">"code-comment">// Trace korelasyonlu Serilog yapılandırması
builder.Host.UseSerilog((context, config) => config
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty(class="code-string">"ServiceName", class="code-string">"OrderService")
.Enrich.WithSpanId()
.Enrich.WithTraceId()
.WriteTo.Console(new JsonFormatter())
.WriteTo.Seq(class="code-string">"http:class="code-commentclass="code-string">">//seq:class="code-number">5341"));Ne Ölçülmeli
Gözlemlenebilirliğin üç sütunu farklı amaçlara hizmet eder:
Dağıtık sistemler kurduğum projelerde, en değerli gösterge paneli her servis endpoint'i için p99 gecikmeyi hata oranı ile yan yana gösterir. Bu iki çizgi birbirinden ayrılmaya başladığında, kesinti olmadan önce oluşan bir sorun var demektir.
Yaygın Mikroservis Hataları
Birden fazla mikroservis geçişinde çalıştıktan sonra, ekiplerin tekrar tekrar düştüğü kalıplar bunlar.
1. Dağıtık Monolit
Kod tabanını servislere böldünüz ama her istek hâlâ sırayla beşinin üzerinden senkron çağrılar gerektiriyor. Mikroservislerin tüm karmaşıklığına sahipsiniz ama bağımsızlığından yoksunuz. Servis A'yı, Servis B'yi de deploy etmeden deploy edemiyorsanız, bunlar bağımsız servisler değildir.
2. Erken Ayrıştırma
Domain sınırlarını anlamadan bölmek yanlış servis kesimlerine yol açar. İki servisi birleştirmek, birini bölmekten çok daha zordur. Modüler bir monolit ile başlayın ve sınır için güçlü kanıtınız olduğunda ayrıştırın.
3. Paylaşılan Veritabanı
İki servisin aynı veritabanı tablosunu okuması tüm amacı yok eder. Şema değişiklikleri koordineli deploy'lara dönüşür. Her servis kendi verisine sahip olmalıdır, bu biraz tekrar anlamına gelse bile.
4. Veri Tutarlılığını Görmezden Gelmek
Servisler arasında dağıtık transaction'lar (iki fazlı commit) ölçekte çalışmaz. Nihai tutarlılığı benimseyin, çok adımlı iş akışları için Saga pattern uygulayın ve hatalar için telafi edici aksiyonlar tasarlayın.
5. Kontrat Testi Yok
Tüm servisleri ayağa kaldıran entegrasyon testleri yavaş, kırılgan ve CI'da güvenilir çalışmaz. Servislerin tüm servislerin çalışmasına ihtiyaç duymadan API şekilleri üzerinde anlaştığını doğrulamak için tüketici güdümlü kontrat testleri (örneğin Pact) kullanın.
6. Yetersiz Operasyonel Yatırım
Mikroservisler ciddi altyapı gerektirir: servis başına CI/CD, merkezi loglama, dağıtık izleme, sağlık kontrolleri, uyarılar, sır yönetimi. Bunlar için platform olgunluğunuz yoksa, mikroservisler sizi yavaşlatır.
7. Çok Hızlı Çok Fazla Servis
İlk çeyrekte 30 servis oluşturan ekipler gördüm. Her birinin kendi CI pipeline'ı, izlemesi, nöbet rotasyonu farkındalığı ve belgelendirmesi gerekiyor. 2-3 ayrıştırma ile başlayın, platform yeteneklerini oluşturun, sonra hızlanın.
Ne Zaman Mikroservis?
Her sistem mikroservislerden fayda görmez. Şu durumlarda haklıdırlar:
Şu durumlarda haklı değiller:
Sonuç
Mikroservisler, varsayılan bir teknik yükseltme değil, organizasyonel ve mimari bir karardır. Teknoloji — .NET, Docker, RabbitMQ, gRPC — kolay kısımdır. Zor kısım sınırları doğru çizmek, gözlemlenebilirliğe yatırım yapmak ve üretimde düzinelerce bağımsız servisi çalıştırmak için operasyonel disiplini oluşturmaktır. İş bağlamı gerçekten gerektirdiğinde mikroservisleri seçin ve ayrıştırmaya yatırım yapmadan önce platforma yatırım yapın.
Sisteminiz için hazırlık değerlendirmesi ve düşük riskli bir geçiş planı oluşturmak için birlikte çalışabiliriz.
İlgili Makaleler
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı
.NET projelerinde Clean Architecture uygulayın. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için rehber.
.NET CI/CD: GitHub Actions ve Azure DevOps ile Otomatik Deployment
.NET projeleri için CI/CD pipeline tasarlayın. GitHub Actions, Azure DevOps ve deployment stratejileri.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç