Redis là một in-memory database phổ biến được sử dụng rộng rãi trên thế giới. Nó phổ biến đến nỗi nhiều bạn bè của tôi đồng nhất định nghĩa cache server với Redis, giống như cách nhiều người từng gọi xe máy là xe Honda.
Ngược dòng lịch sử và drama
Redis ra đời năm 2009, là đứa con tinh thần của chàng lãng tử xứ Sicily (Italy) Salvatore Sanfilippo “Antirez”. Ban đầu, Redis được tạo ra để tăng tính mở rộng (scalability) của start-up của Antirez, nhưng nó nhanh chóng được cộng đồng open source đón nhận bởi sự đơn giản và hiệu năng vượt trội. Lúc bấy giờ, Redis vẫn là một dự án hoàn toàn “mở”, nghĩa là ai cũng có thể tự do sử dụng, sửa đổi, đóng góp và tái phân phối Redis một cách miễn phí.
Năm 2015, một công ty tên Redis Labs trở thành nhà tài trợ chính thức cho dự án Redis. Antirez đầu quân cho Redis Labs.
Năm 2020, Antirez quá mệt mỏi sau 11 năm maintain, fix bugs của Redis, đã quyết định rút lui1, và chuyển giao toàn bộ quyền sở hữu trí tuệ và nhãn hiệu lại cho Redis Labs. Giá trị của thương vụ không được tiết lộ. Anh viết trong blog chia tay:
I would say that what I write is useful just as a side effect, but my first goal is to make something that is, in some way, beautiful. In essence, I would rather be remembered as a bad artist than a good programmer.
Năm 2021, Redis Labs rebrand thành Redis Ltd2.
Năm 2024, Redis Ltd, trong một nỗ lực để IPO vào năm 2025, đã đổi giấy phép open source thành dạng dual license. Với giấy phép mới, về cơ bản, các công ty điện toán đám mây như Google, Amazon, Microsoft nếu muốn tiếp tục phân phối Redis trên cloud, phải trả tiền cho Redis Ltd.
Các công ty khác vẫn được tiếp tục sử dụng các phiên bản mới của Redis miễn phí, với điều kiện không được cạnh tranh trực tiếp với Redis Ltd. Như thế nào là “cạnh tranh trực tiếp” thì do Redis Ltd quyết định. Nếu công ty bạn sử dụng Redis, và may mắn trở thành unicorn, thì không loại trừ khả năng một ngày đẹp trời, Redis Ltd sẽ đến gõ cửa và xin tí huyết.
Cộng đồng ngay lập tức phản ứng, bởi mặc dù Redis Ltd có mọi quyền legal nhưng lại không có nhiều đóng góp trong quá trình phát triển Redis3, nay lại muốn kiếm tiền trên mồ hôi của anh em. Amazon nhanh tay tạo bản fork Valkey ngay trước ngày giấy phép mới có hiệu lực. Với sự giúp sức của các kỹ sư từ Google, Oracle, Snap4; Valkey sau này thậm chí còn có hiệu năng tốt hơn hẳn Redis.Giáo sư Andy Pavlo của đại học danh tiếng CMU nhận định:
Redis Ltd đã đánh giá quá cao rào cản kỹ thuật để xây dựng một hệ thống đơn giản như Redis, nó thấp hơn nhiều so với việc xây dựng một database đầy đủ tính năng như Postgres.
Tôi hoàn toàn đồng ý với cụ Pavlo. Năm 2023, tôi từng clone Redis bằng Golang trong chưa đầy 1 tháng với nhiều cấu trúc dữ liệu hay ho như skiplist, geohash, scalable bloom filter, count-min sketch. (Anh em lướt qua có thể cho 1 star nhé: https://github.com/quangh33/memkv, cảm ơn!).
Các công ty công nghệ bừng tỉnh, nhận ra ngoài kia còn có nhiều giải pháp thay thế tốt hơn cả Redis về mặt hiệu năng.
Valkey 8.0 đạt thông lượng 1 triệu QPS với chỉ một instance5, và hoàn toàn miễn phí.
Dragonflydb có thông lượng lớn gấp 25 lần Redis, với latency p99 chậm hơn Redis chỉ 0.2ms.
Redis is “slow” by design
Okay, enough for drama, cùng tìm hiểu vì sao Redis lại “chậm” dữ vậy. (Tôi đặt chữ chậm trong ngoặc kép bởi Redis vẫn là một tượng đài về hiệu năng, và hầu hết chúng ta không cần tới 1 triệu QPS).
Một cách ngắn gọn, Redis chậm hơn so với các giải pháp khác vì kiến trúc single-threaded. Nghĩa là mọi command bạn gửi lên Redis đều được thực thi bởi 1 thread duy nhất. Dù bạn deploy lên một server 32 core, thì Redis chỉ sử dụng 1 thread. Theo tôi, đây là một lựa chọn design khá hợp lý của Antirez bởi nhiều lí do. Let’s dive deeper.
Blocking I/O
I/O là viết tắt của Input/Output. Mọi hoạt động đọc, ghi dữ liệu của một hệ thống thường được gọi là I/O.
Khi client gửi 1 command lên server, về bản chất client đang gửi data lên 1 socket. Nói thêm, socket là một endpoint để 2 máy tính có thể giao tiếp với nhau qua network, nó được định nghĩa bởi (địa chỉ IP, port number). Để đọc data từ socket, server cần listen (lắng nghe) socket file descriptor (fd) và gọi system call read:
ssize_t read(int fd, void *buf, size_t count);
Bản chất của system call read là một blocking call, nghĩa là nếu tại thời điểm server gọi read, data chưa sẵn sàng ở socket, thì server sẽ chuyển vào trạng thái sleep (blocking), và đợi cho đến khi data sẵn sàng để đọc thì “tỉnh dậy” để đọc. Rõ ràng, đây là một sự lãng phí tài nguyên, bởi thay vì đi làm việc gì đó hữu ích, thì thread của server lại “đi ngủ”.
Một trong những cách phổ biến để hạn chế sự lãng phí này là mô hình One thread per connection: với mỗi client, server sẽ sử dụng 1 thread riêng biệt để xử lý I/O từ client đó. Nhờ đó, nếu 1 thread đang bị blocking thì các thread khác vẫn có thể tiếp tục hoạt động. Mô hình này được các database như MySQL, Postgres sử dụng (thực ra Postgres dùng process-per-connection model). Mô hình này có 2 nhược điểm:
Số lượng connection giới hạn: việc tạo mới thread cho mỗi connection dẫn tới server không thể chấp nhận quá nhiều client truy cập cùng lúc.
Lock contention: nhiều thread cùng muốn thay đổi 1 data trong database có thể dẫn đến hiện tượng race condition.
Để khắc phục race condition, chúng ta phải sử dụng locking: khi một thread đang lock và xử lý data thì các thread khác muốn access cùng một data sẽ phải đợi cho đến khi lock được release.
acquire_lock read(x) x = x + 1 write(x) release_lock
Tất nhiên, không có bữa trưa nào là miễn phí, sử dụng locking dẫn đến hiện tượng lock contention: việc nhiều thread cùng phải đợi lock được release ảnh hưởng nghiêm trọng đến performance của hệ thống. Tôi đoán Antirez đã nghĩ đến tình huống này, và quyết định không sử dụng mô hình one thread per connection cho Redis. Thay vào đó, Redis sử dụng một cơ chế khác: I/O Multiplexing.
I/O Multiplexing
Ý tưởng của I/O Multiplexing là: thay vì gọi system call read khi chưa chắc data đã sẵn sàng ở socket hay chưa thì server yêu cầu Linux kernel theo dõi (monitor) socket và thông báo cho server chỉ khi nào data đã sẵn sàng để đọc.
Ý tưởng này hoàn toàn khả thi nhờ các system call epoll của Linux (hay kqueue trong MacOS):
epoll_create1: tạo 1 instance epoll mới, trả về file descriptor (FD) của epoll
epoll_ctl: cung cấp các FD cần monitor (trong trường hợp của chúng ta là các socket) và yêu cầu Linux kernel monitor và gửi notify khi bất kỳ FD nào có data.
epoll_wait: trả về những FD mà data đã sẵn sàng cho I/O.
Pseudo code cách Redis tiếp nhận và xử lý command từ client như sau:
// tạo một socket mới
serverFD = create_socket()
// trỏ socket vào cổng localhost:8081
syscall.bind(serverFD, "0.0.0.0", 8081)
// lắng nghe connection mới ở localhost:8081
syscall.listen(serverFD)
// tạo 1 epoll instance mới và
// yêu cầu nó monitor serverFD socket
ioMultiplexer = syscall.epoll_create1()
ioMultiplexer.monitor(serverFD)
for {
// get tất cả những FD đã ready for IO
events = ioMultiplexer.wait()
for event in events {
if event == serverFD {
// nếu server socket có connection mới thì
// chấp nhận connection đó và thêm nó vào
// danh sách monitor của epoll
connFD = syscall.accept(serverFD)
ioMultiplexer.monitor(connFD)
} else {
// nếu client FD có data mới sẵn sàng để
// đọc, chứng tỏ 1 client đang gửi command
// cho server
command = syscall.read(event)
// thực thi command
execute(command)
}
}
}
Đoạn code trên chính là phiên bản thô sơ của cái gọi là Event Loop mà người ta vẫn hay hỏi bạn trong interview. Toàn bộ đoạn code chỉ dùng 1 thread và không sử dụng locking. Các bạn có thể đọc code Golang chi tiết ở đây https://github.com/quangh33/memkv/blob/main/internal/server/server.go, tôi đã comment khá đầy đủ từng dòng một. Ngoài ra, cho bạn nào muốn tìm hiểu thêm, người bạn Tú Huỳnh của tôi có một blog khá chi tiết về cách epoll hoạt động ở đây.
Cơ chế I/O Multiplexing hoạt động hiệu quả trên Redis bởi 2 lí do:
data trong Redis được lưu trên RAM, nên bước execute(command) ở đoạn code trên được thực thi nhanh chóng, và không trở thành bottleneck.
network I/O is slow: thời gian để nhận command từ client thường chậm hơn nhiều so với thời gian thực thi chính command đó.
Nhược điểm của mô hình này chỉ bộc lộ khi số lượng command chạm đến ngưỡng vượt quá sức chịu đựng của 1 CPU core. Những giải pháp như Valkey hay Dragonflydb ra đời để khắc phục nhược điểm này. Hẹn gặp các bạn trong bài viết tới về Valkey và Dragonflydb.
Cheers, until next time!
bổ ích quá, hy vọng anh tiếp tục series này mãi!!!
Đọc mấy bài viết của bro hay với chill phết, đã login lại để sub xD