Vertical vs Horizontal Scaling
| Tiêu chí | Vertical Scaling | Horizontal Scaling |
|---|---|---|
| Cách thực hiện | Tăng CPU/RAM của server | Thêm nhiều server |
| Giới hạn | Có giới hạn phần cứng | Gần như không giới hạn |
| Downtime khi scale | Có (restart server) | Không |
| Complexity | Thấp (không đổi code) | Cao (stateless, shared state) |
| Cost curve | Tăng exponential | Tăng linear |
| Single point of failure | Có | Không (nếu >= 3 instances) |
| Phù hợp | DB server, legacy app | Stateless API, web server |
Guideline: Scale vertically trước khi horizontal (đơn giản hơn). Khi đã hit giới hạn vertical hoặc cần HA, chuyển sang horizontal.
Azure Auto-Scale
Rule-Based Auto-Scale cho App Service
bash# Tạo autoscale setting
az monitor autoscale create \
--resource-group myRG \
--resource myAppServicePlan \
--resource-type Microsoft.Web/serverfarms \
--name myAutoscale \
--min-count 2 \
--max-count 10 \
--count 2
# Scale out khi CPU > 70%
az monitor autoscale rule create \
--resource-group myRG \
--autoscale-name myAutoscale \
--condition "CpuPercentage > 70 avg 5m" \
--scale out 2
# Scale in khi CPU < 30%
az monitor autoscale rule create \
--resource-group myRG \
--autoscale-name myAutoscale \
--condition "CpuPercentage < 30 avg 10m" \
--scale in 1
Auto-Scale Best Practices
csharp// Health endpoint trả về metrics cho custom auto-scale
app.MapGet("/metrics/load", async (ProductDbContext db) =>
{
var activeConnections = db.Database.GetDbConnection().State;
var pendingRequests = RequestMetrics.PendingCount;
var avgResponseTime = RequestMetrics.AverageResponseTimeMs;
return Results.Ok(new
{
pendingRequests,
avgResponseTimeMs = avgResponseTime,
timestamp = DateTimeOffset.UtcNow
});
});
Các auto-scale rules quan trọng:
| Metric | Scale Out threshold | Scale In threshold | Cooldown |
|---|---|---|---|
| CPU % | > 70% trong 5 phút | < 30% trong 10 phút | 5 phút |
| Memory % | > 80% trong 5 phút | < 40% trong 10 phút | 5 phút |
| HTTP Queue Length | > 100 trong 3 phút | < 10 trong 10 phút | 5 phút |
| Custom (pending requests) | > 50/instance | < 10/instance | 3 phút |
Lưu ý: Scale in cooldown nên dài hơn scale out để tránh flapping (scale out/in liên tục).
Back-of-Envelope Estimation
Kỹ thuật ước tính nhanh capacity cần thiết cho system design interview và planning thực tế.
Các con số cần nhớ
| Resource | Giá trị tham khảo |
|---|---|
| 1 web server (ASP.NET Core) | ~5,000-10,000 requests/giây |
| 1 SQL Server query (indexed) | ~1-5ms |
| 1 Redis GET | ~0.5-1ms |
| Network round trip (same region) | ~1-2ms |
| Network round trip (cross region) | ~50-150ms |
| SSD random read | ~0.1ms |
| 1 GB network transfer | ~10ms (10Gbps link) |
| 1 triệu users/tháng | ~0.4 requests/giây average |
Ví dụ: E-commerce platform
Yêu cầu: 1 triệu orders/ngày
Bước 1: Tính QPS (Queries Per Second)
- 1M orders/ngày = 1,000,000 / 86,400 ≈ 12 orders/giây (average)
- Peak = 10x average = 120 orders/giây
- Mỗi order cần ~5 API calls (product, inventory, payment, order, notification)
- Peak API QPS = 120 × 5 = 600 requests/giây
Bước 2: Tính server capacity
- 1 ASP.NET Core server: ~5,000 RPS (simple CRUD)
- Nhưng order flow phức tạp: ~1,000 RPS/server (DB calls, external API)
- Cần: 600/1,000 = 0.6 → tối thiểu 2 servers (HA)
- Với headroom 3x: 3-4 servers
Bước 3: Tính storage
- Mỗi order: ~2KB data (order + items + payment)
- 1M orders/ngày = 2GB/ngày
- 1 năm = 730GB
- Với indexes và logs: ~2TB/năm
Bước 4: Tính bandwidth
- Average response size: 5KB
- Peak: 600 × 5KB = 3MB/giây = 24Mbps
- Comfortable: 100Mbps link là đủ
Ví dụ: Chat application
Yêu cầu: 500K DAU, mỗi user gửi 50 messages/ngày
Bước 1: Message throughput
- 500K × 50 = 25M messages/ngày
- Average: 25M / 86,400 ≈ 290 messages/giây
- Peak (10x): 2,900 messages/giây
Bước 2: Storage
- Mỗi message: ~500 bytes (text + metadata)
- 25M × 500B = 12.5GB/ngày
- 1 năm: ~4.5TB
- Với media (10% messages có ảnh, avg 200KB):
2.5M × 200KB = 500GB/ngày → cần Blob Storage
Bước 3: WebSocket connections
- 500K DAU, ~30% online cùng lúc = 150K concurrent connections
- Mỗi SignalR connection: ~10KB memory
- 150K × 10KB = 1.5GB RAM cho connections
- 1 server handle ~20K connections → cần 8 servers
Performance Benchmarking
BenchmarkDotNet
csharp[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class SerializationBenchmark
{
private readonly Product _product = new()
{
Id = 1, Name = "Test Product", Price = 99.99m,
Description = "A sample product for benchmarking",
Categories = Enumerable.Range(1, 5).Select(i => $"Cat{i}").ToList()
};
[Benchmark(Baseline = true)]
public string SystemTextJson()
=> JsonSerializer.Serialize(_product);
[Benchmark]
public byte[] MessagePack()
=> MessagePackSerializer.Serialize(_product);
[Benchmark]
public string NewtonsoftJson()
=> JsonConvert.SerializeObject(_product);
}
// Kết quả mẫu:
// | Method | Mean | Allocated |
// |---------------- |----------:|----------:|
// | SystemTextJson | 198.5 ns | 312 B |
// | MessagePack | 89.3 ns | 128 B |
// | NewtonsoftJson | 512.7 ns | 1,024 B |
Load Testing với k6
javascript// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up
{ duration: '5m', target: 100 }, // Sustain
{ duration: '2m', target: 500 }, // Peak load
{ duration: '5m', target: 500 }, // Sustain peak
{ duration: '2m', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p95<500', 'p99<1000'], // p95 < 500ms, p99 < 1s
http_req_failed: ['rate<0.01'], // Error rate < 1%
},
};
export default function () {
const res = http.get('https://myapp.azurewebsites.net/api/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has products': (r) => JSON.parse(r.body).length > 0,
});
sleep(1);
}
bash# Chạy load test
k6 run load-test.js
# Chạy với output sang InfluxDB để visualize trên Grafana
k6 run --out influxdb=http://localhost:8086/k6 load-test.js
Database Connection Pooling
csharp// Connection string với pooling config
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
"Server=myserver;Database=mydb;User Id=app;Password=***;" +
"Min Pool Size=10;" + // Giữ tối thiểu 10 connections
"Max Pool Size=100;" + // Tối đa 100 connections
"Connection Lifetime=300;" + // Recycle connection sau 5 phút
"Connect Timeout=10;" + // Timeout nếu không lấy được connection trong 10s
"Encrypt=True;TrustServerCertificate=False"));
Tính toán pool size
Công thức: Pool Size = (Core Count × 2) + Disk Spindles
Ví dụ thực tế:
- 4 CPU cores, SSD
- Pool Size = (4 × 2) + 1 = 9 → làm tròn 10
- Nhưng cần tính cả số instances:
- 4 App Service instances × Max Pool Size 100 = 400 connections
- Azure SQL S3: max 200 concurrent sessions → cần giảm pool size
- Adjusted: Max Pool Size = 200 / 4 = 50 per instance
Queue-Based Load Leveling
Dùng message queue để hấp thụ traffic spike, xử lý bất đồng bộ.
csharp// API Controller - enqueue thay vì xử lý trực tiếp
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
var orderId = Guid.NewGuid();
// Validate cơ bản và enqueue
var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(
new OrderMessage { OrderId = orderId, Request = request }))
{
MessageId = orderId.ToString(),
ContentType = "application/json",
Subject = "order-created"
};
await _serviceBusSender.SendMessageAsync(message);
// Trả về 202 Accepted ngay lập tức
return Accepted(new { orderId, status = "processing" });
}
// Worker - xử lý từ queue với controlled concurrency
public class OrderProcessorWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
var processor = _client.CreateProcessor("orders", new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 10, // Giới hạn concurrency
AutoCompleteMessages = false,
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(10)
});
processor.ProcessMessageAsync += async args =>
{
var order = JsonSerializer.Deserialize<OrderMessage>(args.Message.Body);
await ProcessOrderAsync(order!, args.CancellationToken);
await args.CompleteMessageAsync(args.Message);
};
processor.ProcessErrorAsync += args =>
{
_logger.LogError(args.Exception, "Error processing message");
return Task.CompletedTask;
};
await processor.StartProcessingAsync(ct);
}
}
Trade-off Summary
| Quyết định | Option A | Option B | Guideline |
|---|---|---|---|
| Scale strategy | Vertical (đơn giản) | Horizontal (resilient) | Vertical cho DB, Horizontal cho API |
| Auto-scale metric | CPU-based | Custom metric | Custom metric chính xác hơn nhưng phức tạp hơn |
| Queue technology | Azure Storage Queue ($0.01/triệu) | Service Bus ($0.05/triệu) | Storage Queue cho simple, Service Bus cho enterprise |
| Connection pool size | Lớn (high throughput) | Nhỏ (conserve DB resource) | Tính dựa trên DB max connections / số instances |
| Sync vs Async processing | Sync (đơn giản, real-time) | Async + Queue (resilient) | Async cho mọi thứ không cần response ngay |