EngineeringSolution ArchitectSystem Design

Session 04 - Capacity Planning & Scaling

Vertical vs Horizontal Scaling

Tiêu chíVertical ScalingHorizontal Scaling
Cách thực hiệnTăng CPU/RAM của serverThêm nhiều server
Giới hạnCó giới hạn phần cứngGần như không giới hạn
Downtime khi scaleCó (restart server)Không
ComplexityThấp (không đổi code)Cao (stateless, shared state)
Cost curveTăng exponentialTăng linear
Single point of failureCóKhông (nếu >= 3 instances)
Phù hợpDB server, legacy appStateless 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:

MetricScale Out thresholdScale In thresholdCooldown
CPU %> 70% trong 5 phút< 30% trong 10 phút5 phút
Memory %> 80% trong 5 phút< 40% trong 10 phút5 phút
HTTP Queue Length> 100 trong 3 phút< 10 trong 10 phút5 phút
Custom (pending requests)> 50/instance< 10/instance3 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ớ

ResourceGiá 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 địnhOption AOption BGuideline
Scale strategyVertical (đơn giản)Horizontal (resilient)Vertical cho DB, Horizontal cho API
Auto-scale metricCPU-basedCustom metricCustom metric chính xác hơn nhưng phức tạp hơn
Queue technologyAzure Storage Queue ($0.01/triệu)Service Bus ($0.05/triệu)Storage Queue cho simple, Service Bus cho enterprise
Connection pool sizeLớn (high throughput)Nhỏ (conserve DB resource)Tính dựa trên DB max connections / số instances
Sync vs Async processingSync (đơn giản, real-time)Async + Queue (resilient)Async cho mọi thứ không cần response ngay