Harbor

Chọn NoSQL: vì sao Cassandra, DynamoDB, MongoDB hay DB scale

Các họ NoSQL (key-value, document, wide-column, graph, time-series) hợp với workload nào, và khung quyết định để chọn giữa Postgres, NewSQL, Cassandra, DynamoDB, MongoDB, Redis. Giải thích cụ thể vì sao chọn Cassandra, cái phải bỏ đi, kèm 5 tình huống từ các bài thiết kế và những sai lầm hay gặp.

17 phút đọcHạ tầng theo quy mô

Câu hỏi cần trả lời trước khi chọn

Câu hỏi hay gặp nhất sau khi bạn vẽ xong sơ đồ hệ thống là: "Em chọn database nào, vì sao?". Câu trả lời yếu là chọn theo tên: "Cassandra vì nó scale". Câu trả lời tốt đi từ workload: dữ liệu được đọc và ghi theo cách nào, khối lượng bao nhiêu, cần ràng buộc gì. Bài này đưa ra một khung quyết định, và trả lời thẳng câu hỏi "vì sao chọn Cassandra hay một DB scale ngang thay vì Postgres".

Điều đầu tiên cần nhớ: Postgres là mặc định đúng cho phần lớn hệ thống. Bạn chỉ rời nó khi có lý do cụ thể, bằng con số. Bài Database scale cho thấy Postgres đi được rất xa trước khi cần shard hay NewSQL. Bài này tập trung vào phía bên kia: khi nào NoSQL thật sự là lựa chọn đúng.

Grokking ch.9 nêu NoSQL được ưa chuộng nhờ thiết kế đơn giản (không phải xử lý impedance mismatch, ví dụ lưu dữ liệu một nhân viên trong một document thay vì nhiều bảng), khả năng scale ngang trên cụm lớn, độ sẵn sàng cao nhờ replication, hỗ trợ dữ liệu bán cấu trúc và chi phí.1 Cùng chương cũng nêu hai nhược điểm: thiếu chuẩn hoá nên chuyển từ loại NoSQL này sang loại khác khó, và tính nhất quán yếu hơn, không có toàn vẹn khoá chính và khoá ngoại như database quan hệ.2

Các họ NoSQL và workload hợp với từng họ

Grokking chia NoSQL thành bốn loại: key-value, document, graph, columnar.3 Seven Databases in Seven Weeks cũng xếp các database theo "thể loại" tương tự: quan hệ (Postgres), key-value (Riak, Redis), column-oriented (HBase), document (MongoDB, CouchDB), graph (Neo4J).4 Bảng dưới thêm time-series, mà Grokking nhắc trong bài Distributed Monitoring.5

HọMô hìnhWorkload hợpVí dụ
Key-valueTra giá trị theo khoáSession, cache, profile tra theo ID, giỏ hàngRedis, DynamoDB, Riak
DocumentLưu cả document JSON/BSON, schema linh hoạtCatalog sản phẩm nhiều thuộc tính, CMS, dữ liệu hierarchicalMongoDB, Firestore
Wide-columnBảng chia theo partition key, sắp xếp theo clustering keyGhi khối lượng lớn, timeline, event log, truy vấn theo khoá biết trướcCassandra, ScyllaDB, HBase
GraphNode và cạnhMạng xã hội, gợi ý, quan hệ nhiều tầngNeo4J
Time-seriesMẫu có timestamp, nén và downsampleMetrics, sensor, giám sátxem Time-series database

Mô tả use case theo sách:

  • Key-value: Grokking nói hợp với ứng dụng theo phiên (session), mỗi phiên có một ID làm khoá.6 Xem thêm Redis và DynamoDB.
  • Document: hợp với dữ liệu catalog không cấu trúc, ví dụ sản phẩm có hàng nghìn thuộc tính, và hệ quản lý nội dung.7 MongoDB TDG mô tả nó là database hướng document, thay khái niệm "row" bằng mô hình linh hoạt hơn, và thiết kế để scale ngang.8
  • Graph: hợp với ứng dụng xã hội, nơi giá trị nằm ở quan hệ giữa các thực thể.9
  • Columnar: Grokking nêu hợp với truy vấn tổng hợp và phân tích, vì chỉ đọc cột cần tính (ví dụ cộng cột số tiền trong sổ giao dịch).10 Với Cassandra, Grokking lại dùng nó cho dữ liệu ghi nhiều và cần luôn sẵn sàng (xem phần Cassandra bên dưới). Hai cách nhìn này khác nhau: "columnar" trong bảng phân loại của Grokking gộp cả nhóm wide-column lẫn kho phân tích.
  • Time-series: chuỗi mẫu theo thời gian được lưu theo thứ tự thời gian, cần bền vững vì thiếu lịch sử thì hệ giám sát vô dụng.5

Khung quyết định

Năm câu hỏi, đi lần lượt:

  1. Access pattern có biết trước và cố định không? Nếu bạn liệt kê được 3 đến 5 query chính và ít khi thêm query mới, mô hình "query-first" của wide-column và key-value dùng được. Nếu cần truy vấn ad-hoc, báo cáo, join, hãy ở lại SQL.
  2. Khối lượng ghi có vượt sức một primary không? Tính ghi/giây ở đỉnh và dung lượng cộng dồn theo cách của Grokking ch.5 (chia theo ngày, theo giây, nhân kích thước bản ghi và hệ số replication).11 Một primary mạnh thường đủ cho vài nghìn ghi/giây (quy tắc ước lượng của khoá học, cần load test).12
  3. Có cần transaction nhiều dòng hoặc join không? Cần ACID nhiều bảng (tiền, tồn kho) thì SQL hoặc NewSQL. MongoDB TDG nói thẳng rằng việc bỏ join và transaction nhiều dòng là quyết định kiến trúc để dễ scale.13
  4. Có cần scale ngang tuyến tính và đa vùng không? Nếu đa vùng active-active, chịu được eventual consistency, DB kiểu Dynamo hợp. Nếu đa vùng mà vẫn cần SQL và ACID, nhìn NewSQL.
  5. Năng lực vận hành của team? Cụm Cassandra hay NewSQL tự host cần người hiểu repair, compaction, rebalancing. Dịch vụ managed (DynamoDB, bản managed của các hệ khác) giảm việc nhưng tốn tiền và khoá vào nhà cung cấp.
Đang vẽ sơ đồ…

Sơ đồ là khung của khoá học, tổng hợp từ Grokking ch.9: quan hệ khi cần ACID và dữ liệu vừa một node, NoSQL khi dữ liệu lớn hoặc không cấu trúc.14

Bảng so sánh các lựa chọn

Lựa chọnMạnhYếuHợp khi
Postgres (+replica, rồi shard)SQL, ACID, join, hệ sinh thái lớnGhi và dung lượng bị trần theo một primary; shard tốn côngMặc định, dữ liệu dưới vài TB, truy vấn đa dạng
NewSQLSQL + ACID + scale ngang, đa vùngĐộ trễ ghi cao hơn, chi phí, tương thích SQL chưa tuyệt đốiCần ACID ở quy mô lớn, nhiều vùng
Cassandra/ScyllaGhi rất lớn, không master, đa DC, tunable consistencyKhông join, query-first, tombstone, vận hànhGhi nhiều, access pattern cố định, vượt một node
DynamoDBManaged hoàn toàn, scale theo cấu hình, ít vận hànhMô hình khoá cứng, lock-in, chi phí theo throughputĐã ở AWS, team nhỏ, key-value quy mô lớn
MongoDBDocument linh hoạt, truy vấn phong phú trên một collectionKhông join phức tạp, transaction hạn chế (theo sách)Dữ liệu dạng document, schema thay đổi
RedisRất nhanh, cấu trúc dữ liệu phong phúDữ liệu nằm RAM, dung lượng bị giới hạn bởi bộ nhớCache, session, leaderboard, rate limit

Các cột "Mạnh/Yếu" cho Cassandra, DynamoDB, NewSQL là nhận định tổng quát của khoá học; thư viện không có sách về từng sản phẩm đó.15 Với Redis, Seven Databases xếp nó ở nhóm scale dọc truyền thống, còn MongoDB, HBase, Riak thiên về scale ngang.16

Vì sao chọn Cassandra

Cassandra là lựa chọn của bài toán mà Postgres một primary không còn đủ ghi và bạn chấp nhận bỏ join để đổi lấy scale. Xem Cassandra cho phần đầy đủ. Dưới đây là những ý quyết định.

Ghi nhanh nhờ LSM

Cassandra ghi bằng cách append vào commit log và cập nhật bảng trong RAM, sau đó flush thành file bất biến (SSTable) và gộp nền (compaction). Không có đọc-trước-khi-ghi, không cập nhật tại chỗ. Cơ chế LSM của Cassandra chưa có sách trong thư viện, chỉ có HBase trong Seven Databases mô tả write-ahead log tương tự.1718 Kết quả: ghi gần như tuần tự, nên chịu được lượng ghi lớn hơn một B-tree trong database quan hệ cùng phần cứng (nhận định của khoá học).

Partition key quyết định mọi thứ

Mỗi bảng Cassandra là map hai tầng: partition key -> { clustering key -> cột }. Partition key được băm để chọn node trên vòng; mọi dòng cùng partition nằm cạnh nhau và đã sắp xếp theo clustering key. Cách phân bố dữ liệu trên vòng, virtual node và replication thuộc mô hình Dynamo mà Grokking mô tả.19 Hệ quả: query tốt là query đọc đúng một partition. Ví dụ "50 tin nhắn mới nhất của kênh X" là một lần đọc tuần tự.

Không join, mô hình hoá theo query

Bạn không chuẩn hoá rồi join. Bạn liệt kê query trước, mỗi query một bảng, chấp nhận trùng dữ liệu. Muốn cả "tin nhắn theo kênh" và "tin nhắn theo tác giả" thì ghi hai bảng. Grokking cũng nêu NoSQL đổi chuẩn hoá và tính nhất quán chặt lấy scale và linh hoạt.2 Phương pháp query-first là thực hành riêng của Cassandra, chưa có sách kiểm chứng.17

Tunable consistency và quorum

Mỗi câu lệnh chọn bao nhiêu replica phải trả lời. Mô hình chung: n bản sao, ghi cần w xác nhận, đọc hỏi r node, và nếu r + w > n thì đọc luôn thấy lần ghi gần nhất. Grokking viết quy tắc này là r + w > n; Seven Databases viết W + R > N và định nghĩa quorum là N/2 + 1.2021

Tên các mức (ONE, QUORUM, LOCAL_QUORUM, ALL) là của Cassandra.17

Ví dụ số, RF (N) = 3:

Ghi (W)Đọc (R)W + RCó > 3?Kết quả
2 (QUORUM)2 (QUORUM)4CóĐọc luôn thấy lần ghi gần nhất
1 (ONE)1 (ONE)2KhôngCó thể đọc dữ liệu cũ
3 (ALL)1 (ONE)4CóNhất quán, nhưng một replica chết là ghi lỗi

Với N = 5, quorum là 5/2 + 1 = 3 (chia nguyên), nên 3 + 3 = 6 > 5.22

Những thứ phải bỏ đi

  • Join, truy vấn ad-hoc, báo cáo. Thêm query mới thường nghĩa là thêm bảng mới và backfill.
  • Transaction nhiều dòng. Chỉ có nguyên tử ở mức hẹp; kiểm tra điều kiện (lightweight transaction) đắt.17
  • Sửa và xoá rẻ. Xoá sinh tombstone; dùng Cassandra làm queue là bẫy kinh điển.
  • Ràng buộc toàn vẹn. Không khoá ngoại, không unique ngoài khoá chính.
  • Đơn giản vận hành. Phải repair, giám sát partition lớn, kiểm soát compaction.

Trong thực tế lớn, Cassandra cũng không phải lúc nào cũng thắng. Grokking kể Twitter khoảng năm 2010 dùng Cassandra để thay MySQL nhưng không thay hết được vì những hạn chế của nó, rồi sau đó tự xây key-value store Manhattan.23 Bài học: Cassandra là công cụ cho đúng một lớp workload, không phải "database tốt hơn" ở mọi mặt.

Còn ScyllaDB tương thích mô hình Cassandra, nên mô hình dữ liệu trong bài này áp dụng tương tự.17

Trong phỏng vấn: Khi chọn Cassandra, nói ngay ba điều: workload ghi nặng và vượt một primary (kèm con số), partition key và clustering key của từng bảng, và bạn bỏ gì (join, transaction). Câu trả lời đó cho thấy bạn hiểu cái giá, không chỉ cái tên.

Năm tình huống từ các bài thiết kế

Các tình huống dưới đây bám theo lựa chọn của từng bài toán trong khoá. Ở quy mô nhỏ nhiều bài vẫn chọn Postgres; DB scale xuất hiện khi con số đẩy tới.

1. Tin nhắn chat: Chat

  • Workload: ghi rất nhiều, đọc "tin mới nhất của hội thoại", gần như không sửa, không join.
  • Chọn: Postgres ở mức 1M DAU; Cassandra hoặc ScyllaDB ở 50M+ DAU, partition key (conversation_id, bucket), bucket theo thời gian để partition không phình vô hạn.
  • Vì sao: access pattern cố định, ghi cộng dồn lớn, cần sẵn sàng cao. Grokking dùng Cassandra cho kho ghi nhiều và liên tục lớn lên như vị trí tài xế và chuyến đi đã hoàn tất.24
  • Bỏ gì: tìm kiếm full-text trong tin nhắn (đẩy sang Elasticsearch), join với bảng user.

2. Sự kiện tracking: Container tracking

  • Workload: sự kiện vị trí thêm vào liên tục, đọc theo mã container; booking cần nhất quán.
  • Chọn: Postgres cho booking và cả sự kiện (bảng riêng, cột JSONB) vì khối lượng vừa; chỉ chuyển phần sự kiện sang wide-column hoặc time-series nếu ghi vượt sức một primary.
  • Vì sao: thiết kế theo nguyên tắc chỉ thêm khi cần. Mô hình sự kiện append-only hợp với wide-column, nhưng đừng chuyển sớm.
  • Bỏ gì (nếu chuyển): join giữa sự kiện và booking, phải lưu mã booking trong từng sự kiện.

3. Feed: News feed

  • Workload: đọc timeline rất nhiều, timeline dựng sẵn theo user.
  • Chọn: Postgres với một câu SQL có index ở quy mô nhỏ; ở quy mô lớn, timeline lưu sẵn trong key-value store hoặc wide-column theo user_id, kèm cache.
  • Vì sao: Grokking Instagram lưu timeline của người dùng theo userID trong key-value store, và ghi chú rằng dịch vụ hiện đại thường dùng cả SQL lẫn NoSQL: dữ liệu quan hệ trong SQL, dữ liệu lỏng như timeline trong NoSQL.2526
  • Bỏ gì: timeline tính lúc đọc bằng join; nội dung bài và quan hệ follow vẫn nên ở SQL.

4. Thanh toán: Thanh toán

  • Workload: ghi ít hơn nhiều so với chat, nhưng mỗi giao dịch phải đúng, không mất, không trừ hai lần.
  • Chọn: Postgres (managed, có standby), không chọn Cassandra hay DynamoDB cho sổ cái.
  • Vì sao: cần ACID và ràng buộc toàn vẹn. Grokking nêu quan hệ phù hợp khi cần thuộc tính ACID.14 MongoDB TDG cũng khuyên hệ thống cần transaction nên dùng kho dữ liệu khác.27
  • Bỏ gì: không bỏ gì. Nếu sau này cần scale ngang cho sổ cái, cân nhắc NewSQL hoặc shard theo tài khoản và dùng saga cho chuyển tiền xuyên shard (xem Database scale).

5. Metrics: Metrics monitoring

  • Workload: hàng triệu mẫu mỗi giây, ghi theo thứ tự thời gian, đọc theo khoảng thời gian, downsample, TTL.
  • Chọn: time-series database; Postgres giữ cấu hình và metadata.
  • Vì sao: Grokking nói khi có hàng triệu thứ cần giám sát, lượng dữ liệu rất lớn và time-series database giúp lưu bền các mẫu theo thứ tự thời gian.5 Cassandra với compaction theo cửa sổ thời gian là phương án thứ hai nếu đã có sẵn cụm.
  • Bỏ gì: query linh hoạt kiểu SQL; bù bằng ngôn ngữ truy vấn riêng của TSDB.

Sai lầm hay gặp

Sai lầm hay gặp: Chọn NoSQL vì hype hoặc vì "nghe nói scale tốt". Nếu bạn không tính ra được chiều nào vượt sức Postgres (dung lượng, ghi, độ trễ), bạn đang trả giá mất join và transaction cho vấn đề chưa tồn tại.

  • Cassandra cho dữ liệu nhỏ. Dữ liệu vài chục GB, vài trăm ghi/giây mà dựng cụm ba node Cassandra là lãng phí. Grokking nêu rõ quan hệ hợp khi dữ liệu vừa một node.14
  • MongoDB như database quan hệ. Chuẩn hoá thành nhiều collection rồi cố join trong ứng dụng. MongoDB TDG nói không giỏi join nhiều loại dữ liệu theo nhiều chiều và không hỗ trợ transaction đầy đủ.27 Nếu dữ liệu vốn quan hệ, dùng Postgres. Nếu cần linh hoạt schema, Postgres JSONB thường đủ (nhận định của khoá học).
  • Dùng Cassandra làm queue hoặc xoá/cập nhật liên tục, sinh tombstone chất đống. Dùng Kafka cho việc đó.
  • Mô hình hoá Cassandra như SQL, rồi dùng ALLOW FILTERING và secondary index. Query phải hỏi gần như mọi node.
  • Partition key ít giá trị (ví dụ country) hoặc tăng đơn điệu, tạo hot partition. Sharding hash theo khoá cardinality cao phân bố đều hơn.28
  • Quên rằng eventual consistency có hậu quả. Số dư, tồn kho, ghế ngồi không chấp nhận đọc cũ. Xem CAP.
  • Dùng một database cho mọi việc. Grokking chỉ ra các dịch vụ hiện đại thường kết hợp SQL và NoSQL cho từng loại dữ liệu.26
  • Chọn theo đội ngũ của người khác. Công ty lớn có đội vận hành riêng; team 5 người thì chọn dịch vụ managed hoặc Postgres.

Tóm tắt khi trả lời phỏng vấn

  1. Bắt đầu bằng Postgres, nói rõ vì sao đủ ở quy mô hiện tại, kèm con số ước lượng.12
  2. Chỉ ra nút thắt đầu tiên bạn sẽ chạm: dung lượng, ghi hay đọc.
  3. Với từng loại dữ liệu, chọn công cụ theo access pattern: sổ cái ở SQL, timeline và tin nhắn ở wide-column hoặc key-value, metric ở time-series, cache ở Redis.
  4. Nêu cái bạn bỏ khi chọn NoSQL (join, transaction, truy vấn ad-hoc) và cách bù (bảng theo query, search index riêng, saga).13
  5. Nối với CAP: nói rõ phần nào chấp nhận eventual consistency.

Xem thêm Sharding, Chọn database và So sánh database quan hệ.

Nguồn trích dẫn

  1. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Non-relational databases" (các đặc điểm: simple design, horizontal scaling, availability, semi-structured data, cost). ↩

  2. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Drawbacks of NoSQL databases" (lack of standardization, consistency). ↩ ↩2

  3. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Types of NoSQL databases". ↩

  4. Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.1 Introduction · mục "The Genres" (relational, key-value, column-oriented, document, graph). ↩

  5. Grokking Modern System Design · ch.13 Distributed Monitoring · mục "Persist the data" (time-series database). ↩ ↩2 ↩3

  6. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Key-value database" (use case session-oriented). ↩

  7. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Document database". ↩

  8. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.1 Introduction · mục "Ease of Use" và "Easy Scaling" (MongoDB là database hướng document, thay row bằng mô hình linh hoạt). ↩

  9. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Graph database". ↩

  10. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Columnar database". ↩

  11. Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations · mục "Requests estimation", "Storage requirements". ↩

  12. Quy tắc ước lượng của khoá học, xem bài Chọn server. Không phải số liệu trích từ sách; cần load test với ứng dụng thật. ↩ ↩2

  13. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.1 Introduction · mục "...Without Sacrificing Speed", đoạn nói join và transaction nhiều dòng bị bỏ để dễ scale trong hệ phân tán. ↩ ↩2

  14. Grokking Modern System Design · ch.9 Databases · bài "Types of Databases", mục "Choose the right database" (bảng Relational và Non-relational). ↩ ↩2 ↩3

  15. Chưa có nguồn trong thư viện: thư viện không có sách về DynamoDB, ScyllaDB, Spanner, CockroachDB, TiDB hay YugabyteDB; phần so sánh chỉ là nhận định tổng quát, không có phiên bản, giá hay benchmark. ↩

  16. Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.1 Introduction · mục "The Genres", đoạn nói database nào thiên về scale ngang (MongoDB, HBase, Riak) và scale dọc truyền thống (Postgres, Neo4J, Redis). ↩

  17. Chưa có nguồn trong thư viện: thư viện không có sách riêng về Cassandra (cơ chế LSM, compaction, tombstone, CQL, các consistency level, query-first); phần này chỉ nêu khái niệm cơ bản, không đưa số liệu, mặc định hay giá của sản phẩm. ↩ ↩2 ↩3 ↩4 ↩5

  18. Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.4 HBase · phần nói về write-ahead log (WAL). ↩

  19. Grokking Modern System Design · ch.10 Key-value Store · thiết kế key-value store kiểu Dynamo (ring, virtual node, replication, sloppy quorum). ↩

  20. Grokking Modern System Design · ch.10 Key-value Store · mục về consistency ("Usage of r and w", r + w > n). ↩

  21. Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.3 Riak · mục "Consistency by quorum: W+R > N" (tr. 77). ↩

  22. Lý thuyết · Quorum: với n bản sao, ghi chờ w xác nhận, đọc hỏi r node. Nếu r + w > n thì tập node đọc và tập node ghi luôn giao nhau ít nhất một node (nguyên lý chuồng bồ câu: |R| + |W| > n nên hai tập con của n phần tử phải chung phần tử), nên lần đọc gặp ít nhất một bản sao có lần ghi gần nhất. Quorum đa số là floor(n/2) + 1. Ví dụ n = 3, w = 2, r = 2: 2 + 2 = 4 > 3, giao nhau ít nhất 1 node; với w = 1, r = 1: 1 + 1 = 2, không bảo đảm gì. Với n = 5, quorum là 3, vì 3 + 3 = 6 > 5. Hệ quả: chịu được mất n - w node khi ghi và n - r node khi đọc, ví dụ n = 3 với w = r = 2 chịu mất 1 node. ↩

  23. Grokking Modern System Design · ch.31 Twitter · mục "Manhattan" (Twitter dùng Cassandra thay MySQL, sau đó tự xây key-value store). ↩

  24. Grokking Modern System Design · ch.30 Uber · phần chọn Cassandra cho vị trí tài xế và chuyến đi đã hoàn tất (yêu cầu sẵn sàng cao, dữ liệu lớn tăng liên tục). ↩

  25. Grokking Modern System Design · ch.33 Instagram · phần lưu timeline theo userID trong key-value store. ↩

  26. Grokking Modern System Design · ch.33 Instagram · ô Note "Most modern services use both SQL and NoSQL stores". ↩ ↩2

  27. MongoDB: The Definitive Guide 2e (file .zip, tải về giải nén) · ch.1 Introduction · mục "When Not to Use MongoDB". ↩ ↩2

  28. Grokking Modern System Design · ch.9 Databases · bài "Data Partitioning", mục "Hash-based sharding". ↩