Hãy nghĩ tới chi phí gửi hàng. Nếu bạn gửi một trăm món đồ nhỏ đi lẻ từng gói, bạn trả tiền đóng thùng, dán tem, và làm thủ tục một trăm lần — phần "bao bì" ấy có khi đắt hơn cả món hàng. Nhưng nếu bạn xếp cả trăm món vào một thùng rồi gửi một lần, bạn chỉ trả phần thủ tục đó đúng một lần, và nó tan biến vào giữa khối lượng hàng. TLS trong Redis hoá ra hành xử y hệt: chi phí mã hoá không tính trên mỗi lệnh, nó tính trên mỗi bản ghi truyền đi — nên khi bạn gộp lệnh lại thành đường ống, cái giá tưởng như đắt đỏ gần như biến mất. Bài này đo hai lớp bảo vệ của Redis và cái giá thật của từng lớp, và ít nhất một con số sẽ làm bạn nhướn mày.

Redis mặc định không có mật khẩu, không mã hoá, và mọi khách đều làm được mọi thứ.

ACL: chặn theo lệnh và theo mẫu khoá cùng lúc

ACL SETUSER doc on >mk1 ~bo-dem:* +get +mget +exists

Kết quả khi đăng nhập bằng người dùng đó:

GET bo-dem:x    ->  v
GET khac:y      ->  NOPERM No permissions to access a key
SET bo-dem:x    ->  NOPERM ... no permissions to run the 'set' command
DEL bo-dem:x    ->  NOPERM ... 'del'
FLUSHALL        ->  NOPERM ... 'flushall'
CONFIG GET      ->  NOPERM ... 'config|get'

Hai loại chặn hoàn toàn khác nhau, và hai thông báo khác nhau — bạn biết ngay là thiếu quyền lệnh hay thiếu quyền khoá. Chú ý dòng cuối: quyền phân tới mức lệnh con (config|get tách khỏi config|set). Đây là mức chi tiết mà requirepass không có.

Cú pháp đáng nhớ:

Ký hiệu Nghĩa
~mau:* Khoá khớp mẫu này
+lenh / -lenh Cho phép / cấm một lệnh
+@read, +@write, +@admin Cả nhóm lệnh
+@all / -@all Tất cả / không gì
&kenh:* Kênh Pub/Sub
on / off Bật hoặc tắt người dùng

Người dùng default mặc định là ~* &* +@all và không có mật khẩu. Đây là điều đầu tiên nên sửa:

ACL SETUSER default off
ACL SETUSER app on >mat-khau-manh ~app:* +@read +@write

Tắt default thay vì đặt mật khẩu cho nó — như vậy mọi kết nối buộc phải khai danh tính rõ ràng.

TLS: đắt lúc bắt tay, gần miễn phí lúc chạy

Thường TLS Chênh
Bắt tay + PING (p50) 0,164 ms 1,062 ms ×6,5
Thông lượng, không đường ống 18.789/s 16.626/s −11,5%
Thông lượng, đường ống 100 461.274/s 477.027/s bằng nhau
CPU máy chủ cùng tải 38,71% 54,26% +40%

Dòng thứ ba là điều bất ngờ: có đường ống, TLS không chậm hơn chút nào — 477.027 so với 461.274, chênh lệch nằm trong nhiễu.

Lý do chính là cái ẩn dụ đóng thùng ở đầu bài: chi phí mã hoá tính trên bản ghi TLS, không trên từng lệnh. Một lô 100 lệnh đi trong vài bản ghi, và phần mã hoá nhỏ hơn nhiều so với công việc Redis phải làm để xử lý 100 lệnh. Không có đường ống thì mỗi lệnh là một bản ghi riêng — trả phí "đóng thùng" một trăm lần — và 11,5% chính là cái giá đó.

Nhưng CPU tăng thật

38,71% lên 54,26% cho cùng một lượng công việc. Với Redis chạy một luồng, đó là 40% trần năng lực bị ăn mất. Thông lượng tối đa của bạn giảm tương ứng — bạn chỉ không thấy điều đó cho tới khi chạm trần.

Và bắt tay chậm 6,5 lần cộng dồn với kết quả ở phần 31: mở kết nối mới mỗi lệnh vốn đã chậm 5,5 lần, thêm TLS thì tệ hơn nữa. Bể kết nối chuyển từ tuỳ chọn sang bắt buộc.

Ba lớp, theo thứ tự đáng làm

1. Đừng để Redis nghe trên mạng công khai. Rẻ nhất và hiệu quả nhất:

bind 127.0.0.1 10.0.1.5
protected-mode yes

Phần lớn sự cố Redis bị xâm nhập không phải do mật khẩu yếu mà do cổng 6379 mở ra Internet. Redis 3.2 trở đi có protected-mode chặn kết nối ngoài khi chưa đặt mật khẩu, nhưng đừng dựa vào nó — hãy khai bind cho đúng.

2. ACL cho từng ứng dụng. Miễn phí về hiệu năng, và nó chặn được lớp lỗi lớn nhất: một dịch vụ gọi FLUSHALL vì lập trình viên gõ nhầm, hoặc một dịch vụ đọc dữ liệu của dịch vụ khác.

Đặc biệt đáng cấm: FLUSHALL, FLUSHDB, CONFIG, DEBUG, SHUTDOWN, KEYS. Cái cuối không vì bảo mật mà vì lý do đo được ở phần 11.

3. TLS khi lưu lượng đi qua mạng không tin cậy. Trong một VPC riêng, cân nhắc; qua Internet, bắt buộc.

Vài chi tiết khi bật TLS

Cổng riêng. tls-port 6380 tách khỏi port 6379. Đặt port 0 để tắt hẳn cổng không mã hoá — nếu không, ai cũng có thể bỏ qua TLS.

tls-auth-clients mặc định yes, tức khách phải có chứng chỉ. Trong phép đo tôi đặt no cho gọn, nhưng yes mới là cấu hình đúng: nó biến TLS từ "mã hoá đường truyền" thành "xác thực hai chiều".

Bản sao và Cluster cần cấu hình riêng. tls-replication yes và tls-cluster yes — thiếu chúng thì lưu lượng nhân bản vẫn đi bằng văn bản rõ, và đó là toàn bộ dữ liệu của bạn.

Chứng chỉ hết hạn làm sập cụm. Redis không tự nạp lại chứng chỉ; cần CONFIG SET tls-cert-file ... hoặc khởi động lại. Đặt lịch nhắc.

Cái ACL và TLS không bảo vệ

Nhắc lại từ các phần trước, vì nó dễ bị bỏ qua khi đã bật bảo mật:

  • ACL không ngăn được KEYS làm nghẽn máy chủ nếu bạn cho phép nó (phần 11).
  • TLS không ngăn được một khách đọc chậm chiếm 200 MB bộ đệm (phần 30).
  • Cả hai đều không thay thế được maxmemory và giới hạn bộ đệm đầu ra.

Bảo mật và ổn định là hai trục khác nhau, và Redis mặc định yếu ở cả hai.

Mẫu số chung: chi phí cố định mỗi lần truyền, khấu hao bằng cách gộp lô

Cái làm TLS "gần miễn phí khi có đường ống" không phải chuyện riêng của Redis — nó là một quy luật lặp đi lặp lại ở mọi tầng: khi một thao tác gánh một khoản chi phí cố định mỗi lần thực hiện (bắt tay, đóng khung, một vòng đi-về mạng, một lần fsync), thì cách rẻ nhất để làm nó biến mất không phải là tối ưu bản thân chi phí đó, mà là gộp nhiều việc vào một lần để chia đều khoản cố định ấy ra. Một trăm lệnh lẻ trả phí đóng khung một trăm lần; một đường ống trả đúng một lần.

Trong thế giới Java, đây đúng là lý do JDBC addBatch()/executeBatch() nhanh gấp bội so với chạy từng INSERT: cái đắt là vòng đi-về tới CSDL, không phải bản thân câu lệnh, nên gộp 1.000 dòng vào một lô khấu hao trọn vẹn phần round-trip. Cũng đúng với HTTPS keep-alive/reuse kết nối — bắt tay TLS đắt (đúng cái ×6,5 ở đây), nên tái dùng kết nối để không bắt tay lại mỗi request là bắt buộc, y như bể kết nối ở trên.

Trong Go, bufio.Writer tồn tại chính vì mỗi lời gọi write xuống socket có chi phí cố định; gộp vào bộ đệm rồi flush một lần là cùng một mẹo. HTTP/2 ghép nhiều luồng trên một kết nối TLS cũng để khấu hao đúng khoản bắt tay đó.

Còn ở cơ sở dữ liệu, đây là toàn bộ lý do "chatty ORM" chậm: N+1 query trả phí round-trip N lần, còn một câu IN (...) hay multi-row insert trả một lần — network overhead per-statement được gộp lại. Cùng nguyên tắc với việc gom nhiều thay đổi vào một transaction để chia sẻ một lần commit/fsync.

Mẫu số chung: khi chi phí bám theo số lần truyền chứ không theo khối lượng dữ liệu, đừng tối ưu chi phí — hãy giảm số lần truyền bằng cách gộp lô; khoản cố định càng lớn so với việc thật, khấu hao càng lời. Nên câu hỏi đúng khi một thao tác "an toàn hơn nhưng chậm" (bật TLS, bật transaction, thêm một chặng mạng) không phải "nó đắt bao nhiêu mỗi lệnh" mà là "chi phí này cố định mỗi lần truyền hay tỷ lệ với dữ liệu — nếu cố định, tôi đã gộp lô chưa". Với Redis, gộp lô tên là đường ống, và nó biến 11,5% thành số không.

Muốn tự kiểm ba thứ dễ sai nhất:

redis-cli acl list
redis-cli acl whoami
redis-cli config get bind protected-mode port tls-port | paste - -

Ba dấu hiệu:

  • user default on nopass ~* &* +@all — bất kỳ ai kết nối được đều làm được mọi thứ, kể cả FLUSHALL.
  • bind là 0.0.0.0 hoặc trống — Redis đang nghe trên mọi giao diện.
  • port khác 0 trong khi tls-port đã đặt — cổng không mã hoá vẫn mở, và TLS của bạn là tuỳ chọn chứ không phải bắt buộc.

Phần sau: theo dõi Redis — chỉ số nào đáng cảnh báo và chỉ số nào gây hiểu nhầm.