Redis database và cơ chế ghi file lên Disk

1. Redis tổng quan

Redis là một hệ quản trị dữ liệu phát triển lưu trữ database dưới dạng key-value. Redis hỗ trợ nhiều dạng cấu trúc dữ liệu như: Hash, Set, Sorted Set, List, String. Redis lưu trữ dữ liệu key-value trên RAM với tốc độ truy cập rất nhanh, ngoài ra Redis cũng có cơ chế lưu dữ liệu trên ổ đĩa cứng phòng cho trường hợp gặp sự cố như tắt nguồn, chế server hoặc khi restart lại server.

2. Những điểm nhận thấy nổi bật trong Redis

Redis có những đặc điểm nổi bật như:

  • Cập nhật thêm mới và xóa các dữ liệu trên redis rất nhanh chóng vì nó hỗ trợ nhiều command tiện ích.
  • Redis lấy và save dữ liệu trên RAM nhưng tại một thời điểm thì dữ liệu được lưu trữ vào file lưu trên disk.
  • Redis lưu dữ liệu dưới dạng Key-Value kiểu dữ liệu của Key nhưng kiểu dữ liệu của Value thì đa dạng hơn có thể là: List, Set, Sorted Set, Hash...
  • Có hỗ trợ multiple database với nhiều commands có thể tự động remove key-value từ một database tới một database khác.
  • Redis hỗ trợ mở rộng master-slave giúp chúng ta lưu trữ an toàn hoặc linh hoạt mở rộng lưu trữ data.

3. Data Modules

Không giống MySQL, Redis không có table mà chỉ lưu trữ data dưới dạng key-value. Cũng có hệ thống lưu trữ khác là memcache cũng làm vậy, nhưng kiểu dữ liệu của memcache bị hạn chế, không đa dạng bằng Redis. Dưới đây là sơ lược về các kiểu dữ liệu Redis dùng để lưu data:

STRING: Có thể là string, integer hoặc float. Redis có thể làm việc với cả string, từng phần của string, cũng như tăng/giảm giá trị của integer, float. LIST: Danh sách liên kết của các strings. Redis hỗ trợ các thao tác push, pop từ cả 2 phía của list, trim dựa theo offset, đọc 1 hoặc nhiều items của list, tìm kiếm và xóa giá trị. SET: Tập hợp các string là unique trong set và không sắp xếp. Redis hỗ trợ các thao tác thêm, đọc, xóa từng phần tử, kiểm tra sự xuất hiện của phần tử trong tập hợp. Ngoài ra Redis còn hỗ trợ các phép toán tập hợp, gồm intersect/union/difference. HASH: Lưu trữ hash table của các cặp key-value, trong đó key được sắp xếp ngẫu nhiên, không theo thứ tự nào cả. Redis hỗ trợ các thao tác thêm, đọc, xóa từng phần tử, cũng như đọc tất cả giá trị. ZSET (sorted set): Là 1 danh sách, trong đó mỗi phần tử là map của 1 string (member) và 1 floating-point number (score), danh sách được sắp xếp theo score này. Redis hỗ trợ thao tác thêm, đọc, xóa từng phần tử, lấy ra các phần tử dựa theo range của score hoặc của string.

Mình sẽ giới thiệu các module vào các bài chia sẻ tiếp theo, các bạn nhớ theo dõi nhé..! Trong bài này mình sẽ đi sâu phần tích IO/Persistent Redis.

4. Phân tích ghi file lên Disk (IO/Persistent Redis)

Như đã biết là redis lưu trữ dữ liệu trên RAM, do vậy việc đọc và ghi dữ liệu sẽ thao tác trên RAM. Mà những người làm trong ngành CNTT thì đều biết thao tác trên RAM sẽ rất là nhanh, và nhanh hơn nhiều so với thao tác trên ổ đĩa cứng. Nhưng chắc rằng mọi người rẽ tự hỏi, dữ liệu sẽ như thế nào khi mà server bị tắt? Rõ ràng là toàn bộ dữ liệu lưu trên RAM sẽ bị mất, vậy trong trường hợp này thì redis làm thế để bảo toàn được dữ liệu đã lưu trữ trên RAM. Đó là những gi mà mình muốn chia với các bạn trong mục này. Có 2 mục tiêu mà redis cần pải lưu dữ liệu trên ổ cứng là:

  • Cho phép redis có thể phục hồi lại dữ liệu trên memory bằng việc đọc file
  • Phục vụ mục đích sao lưu server, dữ liệu được ghi ra file rdb sẽ được gửi đến các redis slave server.

Redis cung cấp 2 phương thức chính cho việc sao lưu dữ liệu ra ổ cứng, đó là ghi ra file RDB ghi ra file AOF

1. RDB (Redis DataBase file) Dữ liệu ghi ra file RDB thực hiện sao lưu vào ổ cứng sau mỗi khoảng thời gian nhất định, dữ liệu dưới dạng đã mã hóa để giảm kích thước ghi trên đĩa. Cự thể định dạng được lưu như sau: Với những key ngắn, việc dùng 32 bit để mô tả key là thừa thãi, nên redis quy định những key ngắn được mã hóa sử dụng 2 bit đầu tiên của 1 byte.

00|000000 => 00, độ dài dữ liệu mô tả bởi 6 bit 
01|000000 00000000 => 01, độ dài dữ liệu là 14 bit
10|000000 [số 32 bit] => 1 chuỗi độ dài 32 bit sẽ theo sau
11|000000 => obj được encode đặc biệt sẽ theo sau byte này. 6 bit sau sẽ xác định kiểu object.

Kiểu object được định nghĩa như sau:

#define REDIS_RDB_ENC_INT8 0 /* 8 bit signed integer */ 
#define REDIS_RDB_ENC_INT16 1 /* 16 bit signed integer */ 
#define REDIS_RDB_ENC_INT32 2 /* 32 bit signed integer */ 
#define REDIS_RDB_ENC_LZF 3 /* string compressed with FASTLZ */

Ưu điểm RDB file cho phép người dùng lưu các version khác nhau của database, rất thuận tiện khi có sự cố xảy ra. Bằng việc lưu trữ data vào 1 file cố định, người dùng có thể dễ dàng chuyển data đến các data centers, máy chủ khác nhau. RDB file giúp tối ưu hóa hiệu năng của Redis. Tiến trình Redis chính sẽ chỉ làm các công việc trên RAM, bao gồm các thao tác cơ bản được yêu cầu từ phía client như thêm/đọc/xóa, trong khi đó 1 tiến trình con sẽ đảm nhiệm các thao tác disk I/O. Cách tổ chức này giúp tối đa hiệu năng của Redis. Khi restart server, dùng RDB làm việc với lượng data lớn sẽ có tốc độ cao hơn là dùng AOF.

Nhược điểm RDB không phải là lựa chọn tốt nếu bạn muốn giảm thiểu tối đa nguy cơ mất mát dữ liệu. Thông thường người dùng sẽ set up để tạo RDB snapshot 5 phút 1 lần (hoặc nhiều hơn).  Do vậy, trong trường hợp có sự cố, Redis không thể hoạt động, dữ liệu trong những phút cuối sẽ bị mất.  RDB cần dùng fork() để tạo tiến trình con phục vụ cho thao tác disk I/O. Trong trường hợp dữ liệu quá lớn, quá trình fork() có thể tốn thời gian và server sẽ không thể đáp ứng được request từ client trong vài milisecond hoặc thậm chí là 1 second tùy thuộc vào lượng data và hiệu năng CPU.

2. AOF (Append Only File) AOF file lưu lại tất cả dữ liệu dưới dạng command và được nhóm lại thành các block. Các command chính là thao tác write mà server có thể hiểu, thao tác write này sẽ được chạy lại khi restart server hoặc tái thiết lập dataset ban đầu. Các block được tổ chức dưới dạng danh sách liên kết. Mỗi block có độ lớn 10MB là vì trong trường hợp redis server chịu tải cao, số lượng key được cập nhật lớn, nếu kích thước buffer lớn, việc realloc buffer dùng cho các command với tốc độ lớn không đảm bảo. File AOF thực hiện ghi dữ liệu là của background threads. Môt loạt thread sẽ chia sẻ 1 job queue và thay nhau đợi việc từ job queue. Mỗi khi có job mới, thread sẽ chạy và thực thi job được mô tả. Có 2 loại job đơn giản:

REDIS_BIO_CLOSE_FILE: đóng file`
REDIS_BIO_AOF_FSYNC: thực hiện việc flush dữ liệu từ buffer của kernel xuống buffer của đĩa cứng.
process -> job 1 -> job2 -> ... background threads

Tạo ra các job là công việc của child process. Để thực hiện ghi dữ liệu ra đĩa cứng, redis sẽ fork ra 1 process con. Process con này sẽ tạo ra việc cho các background threads. Một đặc điểm cùa aof file đấy là dữ liệu trong các block mới sẽ không được ghi trực tiếp vào file aof hiện tại, mà sẽ được ghi vào file tạm thời. Khi việc ghi dữ liệu hoàn thành, redis mới tiến hành ghi đè file tạm lên file thật. Việc này đảm bảo trong trường hợp hệ thống có sự cố, file aof cũ vẫn được duy trì, giúp phục hồi phần nào dữ liệu.

Ưu điểm Sử dụng AOF sẽ giúp đảm bảo dataset được bền vững hơn so với dùng RDB. Người dùng có thể config để Redis ghi log theo từng câu query hoặc mỗi giây 1 lần. Redis ghi log AOF theo kiểu thêm vào cuối file sẵn có, do đó tiến trình seek trên file có sẵn là không cần thiết. Ngoài ra, kể cả khi chỉ 1 nửa câu lệnh được ghi trong file log (có thể do ổ đĩa bị full), Redis vẫn có cơ chế quản lý và sửa chữa lối đó (redis-check-aof). Redis cung cấp tiến trình chạy nền, cho phép ghi lại file AOF khi dung lượng file quá lớn. Trong khi server vẫn thực hiện thao tác trên file cũ, 1 file hoàn toàn mới được tạo ra với số lượng tối thiểu operation phục vụ cho việc tạo dataset hiện tại. Và 1 khi file mới được ghi xong, Redis sẽ chuyển sang thực hiện thao tác ghi log trên file mới.

Nhược điểm File AOF thường lớn hơn file RDB với cùng 1 dataset. AOF có thể chậm hơn RDB tùy theo cách thức thiết lập khoảng thời gian cho việc sao lưu vào ổ cứng. Tuy nhiên, nếu thiết lập log 1 giây 1 lần có thể đạt hiệu năng tương đương với RDB. Developer của Redis đã từng gặp phải bug với AOF (mặc dù là rất hiếm), đó là lỗi AOF không thể tái tạo lại chính xác dataset khi restart Redis. Lỗi này chưa gặp phải khi làm việc với RDB bao giờ.

5. Kết luận

Qua phân tích ưu nhược từ mục 4 thì câu hỏi đặt ra là, chúng ta nên dùng phương pháp ghi file RDB hay file AOF?  Mỗi phương pháp đều có ưu/nhược điểm riêng, và có lẽ cần nhiều thời gian để làm việc với Redis mà có thể đưa ra lựa chọn thích hợp. Theo một thống kê không chính thức thì mình biết nhiều người chọn AOF bởi nó đảm bảo toàn vẹn dữ liệu rất tốt, nhưng phần lớn developer lại khuyến cáo nên dùng RDB, bởi nó rất thuận tiện cho việc backup database, tăng tốc độ cho quá trình restart cũng như tránh được bug của AOF. Hai trường hợp redis sử dụng tính năng Copy-on-Write của linux khi fork process con, do vậy hiệu năng không vì fork process con mà suy giảm. Vậy thì tần suất ghi dữ liệu là bao nhiêu? Ai chịu trách nhiệm fork process con? Thực chất redis định nghĩa 1 giá trị gọi là tần số ghi: REDIS_DEFAULT_HZ với giá trị mặc định là 10 (redis.h). Như vậy trong 1s, redis sẽ thực hiện 10 lần việc gọi hàm fork. Toàn bộ thao tác ghi dữ liệu redis và thao tác với các key hết hạn được thực hiện bởi 1 hệ thống các "cron". Hàm cron thực hiện việc validate các key là: databaseCron. Hàm cron thực hiện ghi dữ liệu là serverCron. Hàm serverCron sẽ được gọi theo cơ chế bất đồng bộ (thư viện của chính redis) với tần số 1/1000s. Với REDIS_DEFAULT_HZ là 10, cứ 100 lần gọi, serverCron sẽ thực hiện fork child process 1 lần để ghi dữ liệu xuống bộ đĩa cứng. Bài viết này mình xin dừng tại đây, các bạn nhớ đón đọc bài viết về redis tiếp theo..!!