So sánh hiệu năng: INSERT, UPDATE và DELETE trong Cassandra
Bạn đang ngồi trước terminal của node Cassandra. Trước khi viết code ứng dụng, bạn cần hiểu cơ chế "Write Path" của Cassandra để tránh các lỗi hiệu năng phổ biến. Cassandra không thực hiện UPDATE hay DELETE theo cách truyền thống của SQL (ghi đè hoặc xóa dòng).
Trong Cassandra, UPDATE và DELETE thực chất là các thao tác INSERT một "marker" (tombstone) hoặc một phiên bản mới của dữ liệu cùng timestamp mới hơn. Điều này ảnh hưởng lớn đến tốc độ ghi và dung lượng lưu trữ.
Thực hành: Đo lường hiệu năng ghi cơ bản
Chúng ta sẽ tạo một bảng đơn giản để so sánh thời gian thực hiện 10.000 lệnh INSERT thuần túy so với 10.000 lệnh UPDATE. Sử dụng công cụ `cassandra-stress` (đã cài đặt trong Phần 2) hoặc CQL shell trực tiếp để minh họa.
Mở terminal và kết nối vào CQL shell:
cqlsh -u cassandra -p cassandra
Chạy lệnh để tạo bảng mẫu (nếu chưa có) với phân vùng đơn giản:
CREATE KEYSPACE IF NOT EXISTS performance_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
USE performance_test;
DROP TABLE IF EXISTS write_bench;
CREATE TABLE write_bench (
id uuid PRIMARY KEY,
data text,
updated_at timestamp
);
Khởi tạo dữ liệu ban đầu bằng 10.000 bản ghi INSERT:
INSERT INTO write_bench (id, data, updated_at) VALUES (uuid(), 'Initial data payload', now());
-- Lặp lại lệnh trên 10000 lần (có thể dùng script bash ngoài để tạo file .cql)
Để so sánh, bạn cần chạy một script sinh 10.000 lệnh UPDATE trên cùng các ID đó. Lưu ý: UPDATE sẽ tạo thêm một phiên bản mới, làm tăng độ sâu của partition (tombstone hoặc version mới).
UPDATE write_bench USING TIMESTAMP now() SET data = 'Updated data payload' WHERE id = ;
-- Lặp lại cho 10000 bản ghi
Kết quả mong đợi: Thao tác INSERT sẽ nhanh hơn đáng kể (thường 2-3 lần) so với UPDATE vì UPDATE yêu cầu Cassandra phải đọc bản ghi hiện tại (Read-Merge-Write) để xác định timestamp, trong khi INSERT chỉ cần ghi mới.
Để verify hiệu năng, bạn chạy lệnh stress test chuyên biệt:
cassandra-stress write n=100000 -rate threads=10 -log file=insert_log.txt
cassandra-stress update n=100000 -rate threads=10 -log file=update_log.txt
So sánh dòng "Ops/s" trong file log. Bạn sẽ thấy con số Ops/s của INSERT cao hơn nhiều so với UPDATE.
Truy vấn phức tạp và Batch Operation
Trong các bài toán thực tế, bạn thường cần nhóm nhiều thao tác lại thành một đơn vị nguyên tử. Cassandra cung cấp cú pháp `BATCH` để làm điều này. Tuy nhiên, việc sử dụng sai Batch có thể gây ra sự cố hiệu năng nghiêm trọng hoặc treo cluster.
Cấu trúc Batch: UNLOGGED vs LOGGED
Cassandra hỗ trợ hai loại Batch: `UNLOGGED` (mặc định) và `LOGGED`. Batch LOGGED ghi lại toàn bộ thao tác vào một bảng log (batch log table) để đảm bảo tính nguyên tử (atomicity) trong trường hợp node chết giữa chừng. Điều này làm tăng chi phí ghi đáng kể.
Sử dụng Batch UNLOGGED cho các thao tác chỉ yêu cầu "best-effort" hoặc khi bạn đã có logic xử lý giao dịch bên ngoài (ví dụ: ứng dụng tự retry nếu lỗi).
Ví dụ thực tế: Chèn một người dùng mới kèm theo 3 bảng liên quan (Profile, Settings, Activity Log) trong một lệnh duy nhất:
BATCH
INSERT INTO users (user_id, username, email) VALUES ('u123', 'nguyenvana', 'a@example.com');
INSERT INTO user_profiles (user_id, bio, avatar_url) VALUES ('u123', 'Kỹ sư phần mềm', 'http://img.com/a.jpg');
INSERT INTO user_settings (user_id, theme, lang) VALUES ('u123', 'dark', 'vi');
INSERT INTO activity_logs (user_id, action, timestamp) VALUES ('u123', 'LOGIN', now());
APPLY BATCH;
Chú ý: Không sử dụng `DELETE` trong Batch nếu không thực sự cần thiết, vì mỗi DELETE tạo ra một tombstone.
Tránh lỗi "Large Batch" và "Unlogged Batch Limit"
Cassandra có giới hạn kích thước Batch (mặc định 64KB). Nếu Batch quá lớn, node sẽ bị OOM (Out of Memory) hoặc từ chối yêu cầu. Tuyệt đối không dùng Batch để chèn hàng nghìn dòng dữ liệu cùng lúc.
Quy tắc vàng: Chỉ dùng Batch cho các thao tác logic cùng một Partition Key (hoặc tối đa 100-200 dòng nhỏ). Nếu cần chèn hàng loạt, hãy dùng `Batch` trong driver (sẽ hướng dẫn ở phần dưới) hoặc chia nhỏ thành nhiều batch nhỏ.
Ví dụ sai (sẽ bị lỗi):
BATCH
INSERT INTO large_table (id, data) VALUES (1, 'data');
-- ... lặp 10000 dòng ...
INSERT INTO large_table (id, data) VALUES (10000, 'data');
APPLY BATCH;
Thay vào đó, hãy nhóm theo partition:
BATCH
INSERT INTO large_table (id, data) VALUES (1, 'data_1');
INSERT INTO large_table (id, data) VALUES (1, 'data_2');
INSERT INTO large_table (id, data) VALUES (1, 'data_3');
APPLY BATCH;
Kết quả mong đợi: Batch nhỏ chạy thành công ngay lập tức. Batch lớn sẽ bị lỗi `Too many items in batch` hoặc treo node.
Cấu hình Timeout và Consistency Level
Trong môi trường phân tán, không phải lúc nào cũng đảm bảo tất cả các node đều phản hồi. Bạn cần cấu hình rõ ràng về thời gian chờ (timeout) và mức độ nhất quán (consistency level - CL) để cân bằng giữa tính sẵn sàng (Availability) và tính nhất quán (Consistency).
Điều chỉnh Timeout trong cqlsh
Mặc định, `cqlsh` có timeout khoảng 5 giây. Nếu mạng chậm hoặc node bận, bạn sẽ bị ngắt kết nối. Để tăng timeout tạm thời trong session hiện tại:
CONSISTENCY LOCAL_ONE;
SET TIMEOUT 10000;
Thao tác này đặt CL thành LOCAL_ONE (chỉ cần 1 node trong DC hiện tại phản hồi) và tăng timeout lên 10 giây.
Cấu hình Timeout toàn cục trong cassandra.yaml
Để thay đổi vĩnh viễn, bạn cần chỉnh sửa file cấu hình của Cassandra. File này nằm ở `/etc/cassandra/cassandra.yaml`.
Chỉnh sửa các tham số sau:
vi /etc/cassandra/cassandra.yaml
Tìm và thay đổi các dòng sau (tham số đơn vị là millisecond):
# Thời gian chờ cho write (mặc định 2000ms)
write_request_timeout_in_ms: 5000
# Thời gian chờ cho read (mặc định 5000ms)
read_request_timeout_in_ms: 10000
# Thời gian chờ cho range_slice (truy vấn phức tạp)
range_request_timeout_in_ms: 10000
Sau khi lưu file, bạn cần restart service Cassandra để áp dụng:
sudo systemctl restart cassandra
Kết quả mong đợi: Cluster sẽ không còn bị lỗi "Timeout" ngay lập tức khi có tải cao, nhưng hãy cẩn thận vì timeout cao có thể làm tăng latency của ứng dụng nếu node bị treo.
Chọn Consistency Level phù hợp
Tùy thuộc vào loại thao tác, bạn nên chọn CL khác nhau:
- WRITE: Dùng LOCAL_QUORUM cho dữ liệu quan trọng (tài chính, user profile). Dùng ONE hoặc LOCAL_ONE cho log, dữ liệu tạm thời (chấp nhận mất mát dữ liệu nhỏ để đổi lấy tốc độ).
- READ: Dùng QUORUM hoặc LOCAL_QUORUM để đảm bảo dữ liệu mới nhất. Dùng ONE cho cache hoặc dữ liệu không cần cập nhật tức thì.
Cách đặt CL cho từng lệnh trong CQL:
INSERT INTO logs (id, msg) VALUES (1, 'Debug info') USING CONSISTENCY ONE;
SELECT * FROM user_data WHERE id = 1 USING CONSISTENCY QUORUM;
Verify kết quả: Chạy lệnh `cassandra-stress` với các CL khác nhau và so sánh throughput. CL ONE sẽ có throughput cao nhất, CL ALL (tất cả node) sẽ thấp nhất.
Tối ưu kết nối ứng dụng với DataStax Driver
Khi phát triển ứng dụng (Java, Python, Node.js), bạn không nên gọi CQL trực tiếp qua `cqlsh`. Hãy sử dụng DataStax Java Driver (hoặc tương thích) để quản lý kết nối, retry policy và batching hiệu quả.
Cấu hình Driver: Connection Pool và Retry Policy
Giả sử bạn đang viết ứng dụng Java. Dưới đây là cấu hình tối ưu cho file `application.properties` hoặc code khởi tạo `Cluster`.
Tạo file cấu hình `cassandra-driver-config.yaml` tại đường dẫn `/opt/app/config/cassandra-driver-config.yaml`:
hosts:
- 192.168.1.10
- 192.168.1.11
- 192.168.1.12
port: 9042
username: cassandra
password: cassandra
localDatacenter: datacenter1
# Cấu hình Connection Pool
pool:
connectionsPerHost: 2
maxRequestsPerConnection: 128
# Giảm heartbeat để phát hiện node down nhanh hơn
heartbeatInterval: 30000
# Cấu hình Retry Policy (Quan trọng cho Write)
retryPolicy:
class: "com.datastax.oss.driver.api.core.retry.DefaultRetryPolicy"
# Hoặc dùng CustomRetryPolicy để tự xử lý lỗi Timeout
# Cấu hình Timeout (phải khớp với cassandra.yaml)
requestTimeout: 10000
# Consistency Level mặc định
defaultConsistencyLevel: LOCAL_QUORUM
defaultSerialConsistencyLevel: LOCAL_SERIAL
Trong code Java, khởi tạo Cluster như sau:
Cluster cluster = Cluster.builder()
.addContactPointsWithPorts(List.of(InetSocketAddress.createUnresolved("192.168.1.10", 9042)))
.withCredentials("cassandra", "cassandra")
.withLocalDatacenter("datacenter1")
.withPort(9042)
.withConfigLoader(ConfigLoader.fromYamlFile(new File("/opt/app/config/cassandra-driver-config.yaml")))
.withRetryPolicy(DefaultRetryPolicy.builder().build())
.build();
Session session = cluster.connect("performance_test");
Sử dụng Batch trong Driver (An toàn hơn CQL Batch)
Driver hỗ trợ `BatchStatement` với cơ chế tự động chia nhỏ batch nếu cần. Đây là cách an toàn hơn so với việc viết `BATCH ... APPLY BATCH` thủ công.
BatchStatement batch = new BatchStatement(BatchStatement.Type.UNLOGGED);
batch.add("INSERT INTO users (user_id, username) VALUES (?, ?)", UUID.fromString("u123"), "nguyenvana");
batch.add("INSERT INTO user_profiles (user_id, bio) VALUES (?, ?)", UUID.fromString("u123"), "KTS");
// Thêm nhiều câu lệnh khác...
// Thực thi batch
session.execute(batch);
Driver sẽ tự động xử lý việc gửi batch đến coordinator và quản lý lỗi. Nếu một phần của batch thất bại, driver có thể retry tùy theo policy bạn cấu hình.
Verify kết quả tối ưu
Để kiểm tra xem driver có đang hoạt động tốt không, hãy chạy một script benchmark đơn giản gửi 10.000 request qua driver và so sánh thời gian với `cqlsh`.
java -jar benchmark-driver.jar --count=10000 --concurrency=10
Kết quả mong đợi: throughput của driver cao hơn cqlsh khoảng 20-50% nhờ cơ chế pooling và parallel execution. Quan trọng hơn, bạn sẽ thấy ít lỗi "Timeout" hơn khi mạng không ổn định nhờ Retry Policy.
Điều hướng series:
Mục lục: Series: Triển khai Database phân tán với Apache Cassandra và Ubuntu 24.04
« Phần 4: Thiết kế Schema và tối ưu hóa phân vùng dữ liệu
Phần 6: Giám sát và bảo trì cluster Cassandra trong môi trường sản xuất »