Redis xử lý lệnh bằng một luồng duy nhất. Trên máy 16 nhân, điều đó nghe như lãng phí. Bài này đo xem nút thắt thật nằm ở đâu.
Thông lượng theo số kết nối
| Kết nối | ops/s |
|---|---|
| 1 | 25.381 |
| 2 | 47.695 |
| 4 | 91.743 |
| 8 | 194.805 |
| 16 | 271.493 |
| 32 | 306.122 |
| 64 | 270.270 |
| 128 | 260.870 |
Một máy chủ một luồng mà tăng gần tuyến tính từ 1 lên 8 kết nối: 25.381 → 194.805, tức 7,7 lần cho 8 lần số kết nối.
Nếu nút thắt là CPU của Redis thì con số phải phẳng ngay từ đầu — một luồng thì không xử lý được nhiều hơn dù có bao nhiêu khách. Nó không phẳng, nên nút thắt nằm ở chỗ khác.
Đỉnh ở 32 kết nối, rồi giảm dần. Thêm khách sau điểm đó chỉ thêm việc chuyển ngữ cảnh và thêm socket phải theo dõi.
Nút thắt là số lượt đi về
Với một kết nối và không đường ống, mỗi lệnh là một chu kỳ đầy đủ: gửi, chờ, nhận. Trong lúc chờ, kết nối đó không có gì đang bay trên dây.
Bằng chứng: giữ nguyên một kết nối, chỉ gộp nhiều lệnh vào mỗi lượt gửi.
| Đường ống | ops/s |
|---|---|
-P 1 |
25.850 |
-P 8 |
202.429 |
-P 64 |
1.136.364 |
Gấp 44 lần, vẫn chỉ một kết nối. Không đổi cấu hình Redis, không thêm nhân, không thêm luồng.
Và kết hợp cả hai — 50 kết nối với đường ống 16 — cho 3.125.000 ops/s.
Con số đó lớn hơn kết quả tốt nhất không đường ống hơn mười lần, trên cùng một tiến trình một luồng.
CPU thật của Redis
Chạy khách ở một container riêng để đo được CPU của riêng máy chủ:
50 kết nối, không đường ống ~89% chưa hết một nhân
50 kết nối, đường ống 16 ~100% đúng một nhân, kín
Không đường ống, Redis không dùng hết nổi một nhân — nó dành thời gian trong lời gọi hệ thống đọc và ghi socket, mỗi lời gọi mang đúng một lệnh.
Có đường ống, một lời gọi read mang về 16 lệnh, và luồng xử lý chạy hết công suất. 100% là trần thật: đó là toàn bộ những gì một luồng có.
Và 15 trong 16 nhân nằm không, ở cả hai trường hợp.
Một lần đo hỏng
Lần đầu tôi chạy khách trong cùng container với Redis và đo được 181% cho một máy chủ một luồng.
Con số đó bất khả thi, và đó là cách tôi biết phép đo đã hỏng: docker stats đo cả cgroup, mà redis-benchmark chạy bằng docker exec cũng nằm trong cgroup đó. Tôi đang đo tổng của máy chủ và khách.
Bài học chung: khi đo một tiến trình, hãy chắc rằng khách không nằm cùng chỗ được tính. Nếu con số vượt qua giới hạn vật lý của thứ bạn đang đo, đừng tìm cách giải thích nó — hãy nghi phép đo trước.
Qua mạng thì mất bao nhiêu
Toàn bộ số ở trên đo qua loopback. Chuyển khách sang container khác:
| Loopback | Qua mạng | |
|---|---|---|
| 1 kết nối, không đường ống | 25.381 | 22.949 |
| 50 kết nối, không đường ống | ~306.000 | 233.100 |
| 1 kết nối, đường ống 64 | 1.136.364 | 852.878 |
| 50 kết nối, đường ống 16 | 3.125.000 | 2.202.643 |
Mất khoảng 25–30% ở mọi cấu hình. Đây vẫn là mạng ảo của Docker trên cùng một máy; qua mạng thật giữa hai máy chủ, khoảng cách lớn hơn nhiều — và đường ống càng quan trọng hơn, vì mỗi lượt đi về tốn nhiều hơn.
Điều này nghĩa là gì cho ứng dụng của bạn
Dùng đường ống khi có nhiều lệnh không phụ thuộc nhau. Đọc 100 khoá thì gửi một lượt, đừng gọi GET một trăm lần:
p = r.pipeline(transaction=False)
for k in keys:
p.get(k)
values = p.execute()
Với những lệnh có sẵn dạng nhiều khoá thì càng đơn giản: MGET, MSET, HMGET. Chúng là một lệnh, một lượt đi về.
Dùng lại kết nối. Mở kết nối mới cho mỗi thao tác là thêm một lượt bắt tay TCP vào mỗi lệnh. Mọi thư viện đều có bể kết nối; hãy dùng.
Đừng đặt Redis quá xa. Vì thời gian gần như hoàn toàn là đi về, độ trễ mạng cộng thẳng vào mỗi lệnh. Redis ở vùng khác là hỏng thiết kế, không phải chậm một chút.
Đừng mua máy nhiều nhân cho Redis. Một tiến trình dùng đúng một nhân. Muốn dùng hết máy thì chạy nhiều tiến trình Redis trên các cổng khác nhau, hoặc dùng Redis Cluster — sê-ri sẽ đo cả hai.
io-threads không phải đa luồng
Redis 6 thêm io-threads, và tên gọi gây hiểu nhầm. Chúng chỉ đọc và ghi socket song song; việc thực thi lệnh vẫn một luồng duy nhất, và đó là chủ ý thiết kế — không khoá, không tranh chấp, mọi lệnh là nguyên tử miễn phí.
Mặc định io-threads 1, tức tắt. Nó chỉ giúp khi bạn ở đúng tình huống mà phép đo trên cho thấy: nhiều kết nối, không đường ống, tốn thời gian trong lời gọi hệ thống. Nếu ứng dụng đã dùng đường ống thì bật nó gần như không đổi gì.
Nhân tiện, tiến trình Redis có 6 luồng trong bảng tiến trình, không phải một. Những luồng kia làm việc nền: giải phóng bộ nhớ lớn, fsync cho AOF, đóng tệp. Chúng không chạm vào không gian khoá.
Thử ba mươi giây
Đo xem ứng dụng của bạn đang tốn bao nhiêu vào lượt đi về:
redis-cli info stats | grep -E 'total_commands_processed|instantaneous_ops'
redis-cli info clients | grep -E 'connected_clients|blocked_clients'
redis-cli --latency -i 5
Rồi so instantaneous_ops_per_sec với CPU của tiến trình Redis:
top -p $(pgrep -x redis-server) -b -n 1 | tail -2
Ops cao mà CPU dưới 50% nghĩa là bạn đang bị chặn ở lượt đi về, không phải ở Redis — và đường ống sẽ cho bạn thêm vài lần thông lượng mà không cần đổi phần cứng.
Phần sau: các kiểu dữ liệu — đo xem mỗi kiểu tốn bao nhiêu byte và nhanh chậm ra sao.