Có hai loại cánh cửa trong kỹ thuật hệ thống. Cửa hai chiều: bước qua thấy không ổn thì quay lại, không mất gì — cứ thử thoải mái. Và cửa một chiều: bước qua rồi cửa khoá lại sau lưng, không có đường về. Với cửa hai chiều, kiểm tra kỹ lưỡng là phí thời gian; với cửa một chiều, mọi thứ phải chắc chắn trước khi bước. Cái bẫy chết người là tưởng một cánh cửa hai chiều trong khi nó là một chiều. Nâng cấp Redis chính là một cánh cửa như thế: quy trình chuyển đổi nhẹ nhàng đến bất ngờ, nhưng bước cuối khoá lại sau lưng bạn, và bài này đo cả hai mặt.
Nâng cấp Redis nghe như phải có cửa sổ bảo trì. Tôi đo một quy trình không cần cửa sổ nào — và đo cả cái giá của nó.
Ba bước, đo thật
1. Dựng bản mới làm bản sao của bản cũ
docker run -d --name rnew redis:7-alpine \
redis-server --replicaof rold 6379
đồng bộ xong sau 0,6 giây, đủ 200.000 khoá
Redis 7.4 đọc được luồng nhân bản của Redis 6.2 mà không cần cấu hình gì. Đây là điều cả quy trình dựa vào.
2. Đổi vai trò
redis-cli -h rnew replicaof no one
redis-cli -h rold replicaof rnew 6379
hai lệnh mất 0,12 giây
Bản mới thành máy chính; bản cũ thành bản sao của nó. Từ giây phút này, bản cũ chỉ còn để dự phòng.
3. Khách hàng trong suốt quá trình
ghi thành công 3.842 | thất bại 1 | gián đoạn 0,03 giây
Một lệnh ghi thất bại trên 3.843 lần thử. Khách hàng của tôi chỉ làm hai việc: bắt lỗi, thử host kia. Không cần Sentinel, không cần proxy — với một lần chuyển đổi có kế hoạch, logic thử lại đơn giản là đủ.
Nhưng đường về thì không có
Tôi thử ngược lại: cho bản 6.2 làm bản sao của bản 7.4.
# Failed trying to load the MASTER synchronization DB from disk
* Reconnecting to MASTER rnew:6379 after failure
# Failed trying to load the MASTER synchronization DB from disk
master_link_status: down
dbsize: 0
Bản cũ không đọc nổi định dạng RDB của bản mới. Nó thử lại vô hạn, và cơ sở dữ liệu vẫn trống. Nghĩa là quy trình này một chiều: sau khi đổi vai trò, bạn không lùi lại được bằng cách đảo hai lệnh REPLICAOF — bản 6.2 đã không còn dữ liệu và không nhận được nữa. Cửa đã khoá sau lưng. Muốn lùi thì phải khôi phục từ bản sao lưu chụp trước khi nâng cấp, và chấp nhận mất mọi thay đổi kể từ đó.
Nên làm gì trước bước 2
Vì bước 2 không đảo ngược được, mọi việc kiểm tra phải xong trước nó.
Chụp RDB của bản cũ và mang ra khỏi máy. Đây là đường lùi duy nhất. Mất 0,1 giây như đo ở phần 18.
Kiểm ứng dụng chạy được với bản mới. Trong lúc bản mới còn là bản sao, nó đọc được. Trỏ một phần lưu lượng đọc sang đó và xem có gì hỏng không — đây là cửa sổ thử nghiệm miễn phí mà quy trình này cho không.
Đọc ghi chú phát hành. Giữa 6.2 và 7.0 có vài thay đổi thật: ziplist thành listpack (phần 5), cách tính maxmemory-clients, và một số lệnh đổi hành vi ở biên. Chúng ít khi làm hỏng, nhưng đọc mười phút rẻ hơn nhiều so với gỡ rối sau khi đã không lùi được.
Kiểm INFO server của cả hai. Chắc chắn bạn đang nâng đúng thứ mình nghĩ.
Nhảy cóc phiên bản
Redis không có ràng buộc "phải qua từng phiên bản" như Kubernetes. Nhân bản từ 6.2 sang 7.4 chạy thẳng, và đó là hai phiên bản chính cách nhau. Nhưng càng xa thì càng nhiều thay đổi hành vi tích lại, và bạn không có đường lùi. Với khoảng cách lớn — 5.x lên 7.x chẳng hạn — cân nhắc đi hai chặng, mỗi chặng có bản sao lưu riêng.
Khi có Sentinel hoặc Cluster
Với Sentinel, đừng gõ REPLICAOF bằng tay — Sentinel sẽ thấy trạng thái không khớp và sửa lại. Dùng:
redis-cli -p 26379 sentinel failover mymaster
Quy trình: nâng cấp các bản sao trước (chúng không phục vụ ghi), rồi gọi SENTINEL FAILOVER để chuyển sang một bản sao đã nâng cấp, rồi nâng nốt máy chính cũ. Thời gian chuyển đổi khi đó là con số đo ở phần 22: khoảng 4,5 giây cho Sentinel, 6,27 giây từ góc nhìn khách — chậm hơn 0,03 giây ở bài này, vì Sentinel phải phát hiện và bầu chọn thay vì được ra lệnh trực tiếp.
Với Cluster, nâng từng nút một, và nâng bản sao trước nút chính của nó. CLUSTER FAILOVER trên một bản sao đã nâng cấp làm việc chuyển đổi có kiểm soát, không mất dữ liệu.
Nâng cấp phiên bản vá
Với phiên bản vá cùng dòng — 7.4.1 lên 7.4.2 — quy trình vẫn như trên, nhưng bạn có đường lùi: hai bản cùng dòng đọc được RDB của nhau. Đây là lý do nên nâng phiên bản vá thường xuyên thay vì để dồn: mỗi lần rẻ, có thể lùi, và bạn tránh được việc phải nhảy hai phiên bản chính một lúc.
Mẫu số chung: cửa một chiều đòi kiểm tra trước, và nhiều cửa một chiều cải trang thành hai chiều
Cái quyết định cách nâng cấp Redis — và cách nâng cấp gần như mọi hệ thống trạng thái — nằm ở một câu hỏi duy nhất: hành động này đảo ngược được không. Công sức bạn phải bỏ vào kiểm tra trước tỉ lệ nghịch với khả năng lùi lại sau. Cửa hai chiều thì cứ thử; cửa một chiều thì mọi thứ phải chắc trước ngưỡng cửa. Và cái bẫy — như định dạng RDB mới mà bản cũ không đọc nổi — là một cửa một chiều nấp trong một quy trình trông có vẻ đối xứng.
Trong thế giới Java và di trú CSDL, đây đúng là khác biệt giữa ADD COLUMN (cửa hai chiều — thêm cột không phá bản cũ, lùi được) và DROP COLUMN hay RENAME (cửa một chiều — dữ liệu cột đó biến mất, và bản ứng dụng cũ không còn chạy). Đây là toàn bộ lý do có mẫu expand/contract: tách một thay đổi phá vỡ thành các bước hai-chiều xếp chồng, chỉ để bước một-chiều cuối cùng xảy ra khi mọi thứ khác đã chắc.
Trong Go và API công khai, xuất bản v2 của một endpoint là hai chiều tới khi khách hàng đầu tiên phụ thuộc vào nó — sau đó gỡ v1 thành một chiều; và một lần ghi vào một hàng đợi/topic đã có consumer đọc là không rút lại được, khác hẳn một lần ghi vào buffer nội bộ.
Còn ở cơ sở dữ liệu nói chung, ddl-auto của Hibernate sinh ALTER TABLE không rollback được (đã bàn ở loạt bài blog này), format nâng cấp của storage engine (đúng ca RDB ở đây), và việc bật một feature flag thay đổi cách dữ liệu được ghi — tất cả là cửa một chiều đội lốt thao tác thường.
Mẫu số chung: mỗi thao tác hoặc là đảo ngược được (thử rồi lùi, chi phí kiểm tra trước thấp) hoặc một chiều (lùi được chỉ bằng khôi phục từ bản sao lưu cũ, mất mọi thứ kể từ đó) — và mức độ kỹ lưỡng bạn cần trước khi bấm nút phải khớp với vế đó; nguy hiểm nhất là một cửa một chiều nằm giấu trong một quy trình trông đối xứng. Nên trước bất kỳ nâng cấp/di trú/đổi cấu hình nào, câu hỏi đúng không phải "nó có chạy không" mà là "nếu sai, tôi lùi bằng cách nào — đảo lại một lệnh, hay khôi phục từ bản sao lưu và mất dữ liệu; và bước nào trong quy trình này là cái cửa khoá sau lưng". Chụp bản sao lưu trước bước đó, đúng như chụp RDB trước khi đổi vai trò.
Muốn tự kiểm xem bạn có sẵn sàng nâng cấp không:
redis-cli info server | grep -E 'redis_version|os|multiplexing_api'
redis-cli info replication | grep -E 'role|connected_slaves|master_link_status'
redis-cli info persistence | grep -E 'rdb_last_bgsave_status|rdb_changes_since_last_save'
Ba câu hỏi:
- Có bản sao nào không? Không có thì bạn cần dựng một cái trước, và đó là bước tốn thời gian nhất.
rdb_last_bgsave_statuscóokkhông? Nếu không, bạn đang không có đường lùi.- Ứng dụng có thử lại khi ghi thất bại không? Đây là điều kiện duy nhất để một lệnh thất bại trở thành vô hình thay vì thành lỗi cho người dùng.
Câu cuối là câu quan trọng nhất, và nó thuộc về mã ứng dụng chứ không thuộc về Redis.
Phần sau: Redis trong Docker cho môi trường thật — những cấu hình mặc định phải đổi.