WEBSITE ĐANG PHÁT TRIỂN

Read replica vs sharding - chọn sai thì refactor rất đau

Read replica và sharding đều là cách scale database - nhưng giải quyết hai vấn đề hoàn toàn khác nhau. Chọn nhầm thì sau này migrate cực kỳ đau. Đây là cách tôi quyết định, dựa trên kinh nghiệm thực tế.

Cái dự án phải refactor database

Năm 2022, tôi tham gia một dự án e-commerce đang gặp database performance problems. Team trước đó đã implement sharding - chia data theo shop_id, mỗi shard là một database riêng.

Nghe hợp lý. Nhưng khi tôi nhìn vào usage pattern: 80% queries là read (product listing, search, analytics). Write chỉ chiếm 20%. Và read queries thường cần join data across nhiều shops (global search, admin reports).

Sharding đã giải quyết vấn đề không tồn tại - write scalability - trong khi bỏ qua vấn đề thực sự: read scalability.

Migration từ sharded sang read replica architecture mất 3 tháng và là một trong những projects đau đớn nhất tôi từng làm.


Quay lại chuyện kỹ thuật: 2 vấn đề, 2 giải pháp

Read replica giải quyết gì?

Vấn đề: Database bị overload bởi read queries. Write throughput vẫn ổn, nhưng SELECT queries chiếm hết resources.

Giải pháp: Replicate data sang một hoặc nhiều read-only instances. Writes vẫn vào primary, reads được route sang replicas.

           ┌─────────────────┐
           │   Application   │
           └────────┬────────┘
                    │
          ┌─────────┴──────────┐
          │                    │
     ┌────▼───┐          ┌─────▼──────┐
     │ Write  │          │    Read    │
     │Primary │──────────▶  Replica   │
     └────────┘  replicate └──────────┘

Khi nào dùng:

  • Read:write ratio > 3:1
  • Query patterns đa dạng, khó partition
  • Cần reporting/analytics không ảnh hưởng production
  • Data model complex, nhiều joins

Giới hạn:

  • Vẫn chỉ scale reads - writes vẫn phụ thuộc vào primary
  • Replication lag: replica có thể lag vài milliseconds đến vài seconds
  • Không giúp nếu vấn đề là write throughput

Sharding giải quyết gì?

Vấn đề: Database quá lớn để fit trên một server, hoặc write throughput đã maxed out primary.

Giải pháp: Chia data thành partitions (shards), mỗi shard là một database riêng. Writes được distributed across shards.

           ┌─────────────────┐
           │   Application   │
           └────────┬────────┘
                    │
          ┌─────────┴──────────┐
          ▼                    ▼
     ┌────────┐          ┌────────┐
     │ Shard 1│          │ Shard 2│
     │User A-M│          │User N-Z│
     └────────┘          └────────┘

Khi nào dùng:

  • Dataset quá lớn cho một server (TB scale)
  • Write throughput đã maxed out
  • Queries mostly access data của một entity cụ thể (user_id, tenant_id)
  • Không cần cross-shard queries

Giới hạn:

  • Cross-shard queries cực kỳ đau: phải query tất cả shards rồi merge
  • Shard key chọn sai thì hotspot
  • Schema changes phải apply cho tất cả shards
  • Transactions across shards rất phức tạp

Framework quyết định

Vấn đề của bạn là gì?
  │
  ├── CPU/IO cao do reads?
  │     └── Read replica trước
  │
  ├── Storage quá lớn?
  │     └── Sharding - với shard key cẩn thận
  │
  ├── Write throughput maxed out?
  │     └── Sharding - hoặc xem xét queue-based approach
  │
  └── Cả reads lẫn writes?
        └── Sharding + read replica per shard (complex nhưng đúng)

Quy tắc vàng: Đừng shard trước khi thật sự cần.

Read replica thêm vào production environment thường là 1-2 ngày. Sharding thì có thể mất vài tháng và là một-trong-những-migration-đau-nhất của đời developer.


Implement Read Replica với .NET và PostgreSQL

// DbContext configuration với read replica routing
public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Primary connection string - writes go here
        optionsBuilder.UseNpgsql(
            Environment.GetEnvironmentVariable("PRIMARY_DB_CONNECTION"));
    }
}

// Separate read-only context
public class ReadOnlyDbContext : ApplicationDbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Read replica connection string
        optionsBuilder.UseNpgsql(
            Environment.GetEnvironmentVariable("REPLICA_DB_CONNECTION"));
    }

    // Prevent any write operations
    public override int SaveChanges() =>
        throw new InvalidOperationException("Read-only context cannot save changes");
}

// Service layer - use correct context
public class ProductQueryService
{
    private readonly ReadOnlyDbContext _readContext; // Queries use replica

    public async Task<List<Product>> GetProductsByCategoryAsync(int categoryId)
    {
        return await _readContext.Products
            .Where(p => p.CategoryId == categoryId && p.IsActive)
            .OrderBy(p => p.Name)
            .ToListAsync();
    }
}

public class OrderService
{
    private readonly ApplicationDbContext _writeContext; // Writes use primary

    public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
    {
        var order = new Order { /* ... */ };
        _writeContext.Orders.Add(order);
        await _writeContext.SaveChangesAsync();
        return order;
    }
}

Lưu ý về replication lag:

// Với critical reads sau write - cần dùng primary, không dùng replica
public class OrderConfirmationService
{
    private readonly ApplicationDbContext _primaryContext; // Không phải replica

    public async Task<OrderStatus> GetStatusAfterCreationAsync(int orderId)
    {
        // Đây là read ngay sau write - dùng primary để tránh "read your own writes" problem
        return await _primaryContext.Orders
            .Where(o => o.Id == orderId)
            .Select(o => o.Status)
            .SingleAsync();
    }
}

Kinh nghiệm thực tế: Checklist trước khi quyết định

  1. Profile hiện tại: Đo read/write ratio thực tế. Đừng estimate.
  2. Query patterns: Cross-entity queries nhiều không? Nếu có → sharding sẽ đau.
  3. Growth projection: Dataset sẽ lớn đến đâu trong 2 năm?
  4. Team capacity: Sharding phức tạp hơn nhiều. Team có thể maintain không?
  5. Start simple: Read replica trước - nếu vẫn không đủ, xem xét sharding.

Triết lý

Database architecture là một trong những quyết định hardest-to-change. Không như application code có thể refactor dễ dàng, thay đổi database sharding strategy đòi hỏi migration data, downtime planning, và testing phức tạp.

"Premature optimization is the root of all evil" - nhưng với database, late optimization cũng rất đau.

Đánh giá đúng từ đầu - và chọn solution đơn giản nhất đủ giải quyết vấn đề hiện tại, với headroom cho growth gần nhất.


Bạn đã gặp tình huống này chưa?

Bạn đang dùng strategy nào - read replica, sharding, hay cả hai? Và nếu bạn đã từng migrate - bạn có tip gì? 👇


/Son Do - believe in basic

#1percentbetter #SolutionArchitecture #Database #SystemDesign #PostgreSQL #SQLServer


Bài viết liên quan

Xem thêm
Solution Architecture & System Design

PostgreSQL vs SQL Server - quyết định tôi đã đưa ra và tại sao

Không có database "tốt hơn". Có database phù hợp hơn với context của bạn. Tôi dùng SQL Server 20 năm và PostgreSQL 5 năm - cả hai đều excellent, nhưng chúng khác nhau ở những trade-off quan trọng mà architect cần hiểu rõ. Năm 2019, tôi nhận một project mới - một startup fintech đang xây MVP. Họ hỏi tôi: "Anh Son, chúng tôi nên dùng database gì?" Câu hỏi đơn giản. Nhưng tôi không trả lời ngay. Thay vào đó, tôi hỏi lại: "Tech stack của team là gì? Ai sẽ maintain database này sau khi tôi đi? Infrastructure của các bạn là on-premise hay cloud? Budget cho licensing là bao nhiêu?" Họ nhìn tôi hơi ngớ ngẩn. Họ expected một câu trả lời technical - benchmark performance, feature comparison. Tôi lại hỏi về organization và budget. Vì đó mới là thứ thực sự quyết định.

Solution Architecture & System Design

Conway's Law - tại sao architecture của bạn trông giống org chart

Conway's Law nói rằng hệ thống bạn build sẽ phản chiếu cấu trúc tổ chức của team. Đây không phải lý thuyết học thuật - đây là lý do tại sao microservices nhiều dự án VN thất bại, và tại sao đôi khi cần thay đổi org chart trước khi thay đổi architecture. Năm 2021, tôi bắt đầu làm tư vấn cho một công ty fintech. Họ muốn chuyển từ monolith sang microservices - xu hướng lúc đó ai cũng nói về. Tôi vào, review codebase, rồi nhìn sang org chart. Và tôi thấy ngay. Monolith của họ có 3 module lớn: Accounting, Lending, và Customer Management. Org chart của họ cũng có 3 team tương ứng: Team Kế toán, Team Tín dụng, và Team Khách hàng. Ba team này không nói chuyện với nhau nhiều - mỗi team có lead riêng, backlog riêng, sprint riêng. Tôi đoán trước microservices của họ sẽ trông như thế nào nếu họ tự migrate: đúng 3 service, tương ứng với 3 team. Không nhiều hơn, không ít hơn. Và đó chính xác là điều xảy ra sau 6 tháng họ tự làm trước khi gọi tôi vào. 3 services, ranh giới business không đúng, coupling vẫn còn đầy, chỉ là coupling giờ đi qua HTTP thay vì function call. Distributed monolith - thứ tệ nhất của cả hai thế giới. Conway's Law đã xảy ra, nhưng không ai để ý.