Chuyện của dev's blog

Redis được viết như thế nào

 26/09/2017  3320

Chắc hẳn các bạn dev đều đã từng dùng Redis để Lưu trữ session hay làm Cache cho hệ thống của mình. Redis được viết bằng ngôn ngữ C và cá nhân mình thấy C là một ngôn ngữ rất hay và tối ưu cho các hệ thống cần performance cao.
Vì vậy mình viết bài này để chia sẻ cách tìm hiểu bên trong Redis cho các bạn có cơ sở cùng ngâm cứu tiếp nhé.

Ai cũng đều biết để sử dụng Redis mình có 2 cách: Cài theo dạng package hoặc Build từ source.
Bài này mình sẽ nói cách "Build từ source" bằng cách dùng command "make" để các bạn biết cách research source code Redis như thế nào.

Bài này viết dựa trên source code của Redis version 3.2.x tại đây https://github.com/antirez/redis/tree/3.2
Để build và run Redis thì mình di chuyển vào thư mục chứa source code của Redis và chạy các command sau:

$ make
$ ./src/redis-server /path/to/redis.conf

Sau khi bạn chạy command "make" thì sẽ có 1 file "redis-server" bên trong thư mục "src". File này là 1 "excutable file", giống như file .exe trên Window vậy.
Vì vậy mình cần phải nắm 1 số thuật ngữ cơ bản sau:

Compilation & Linking

Compilation

Đây là quá trình tạo ra các "Object" file từ các file source code (.c, .cpp, .cc). "Object" file sẽ chứa "machine language instructions" tương ứng.

Linking

Đây là quá trình tạo ra 1 "Executable file" từ nhiều "Object" file.

Make

Đây là tên của 1 "Build automation tool" để build "Executable program" từ source code bằng cách đọc file "Makefile" chứa các rule giúp "Make" biết build như thế nào.

Nếu bạn xem nội dung của "Makefile" thì sẽ có đoạn sau:

cd src && $(MAKE) [email protected]

Các command này sẽ di chuyển vào thư mục "src" và chạy command "make" bên trong thư mục này. Và dĩ nhiên là bên trong thư mục "src" sẽ có 1 "Makefile" chứa các rule để build Redis như thế nào:

REDIS_SERVER_NAME=redis-server

$(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ)
$(REDIS_LD) -o [email protected] $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a $(REDIS_GEOHASH_OBJ) $(FINAL_LIBS)

Đoạn trên có nghĩa là để build ra được file "redis-server" thì cần "Linking" các "Object" file có trong list "$(REDIS_SERVER_OBJ)":

REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o server.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o geo.o

Main function

Các file .o sẽ được "Compile" từ các file ".c" với tên tương ứng. Và trong mỗi chương trình C thì bao giờ cũng có 1 function tên là "main". Function này sẽ được gọi đầu tiên khi chạy "Executable file" là "redis-server".

Mình tìm thấy function "main" trong file "server.c":

...
initServerConfig();
...
initServer();
...
aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);

initServerConfig

Function này thiết lập các giá trị mặc định cho biến "server" và có thể overwrite từ file "redis.conf".

server.commands = dictCreate(&commandTableDictType,NULL);
server.orig_commands = dictCreate(&commandTableDictType,NULL);
populateCommandTable();
...

"populateCommandTable" function sẽ đăng kí tất cả các command trong Redis như: set, get, setnx, setex...

struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},

Nhìn vào đây mình có thể đoán được là khi dùng "get" thì thực sự sẽ chạy function "getCommand". Function này được định nghĩa trong file "t_string.c".
Redis có nhiều file tương ứng với nhiều "Data type": t_hash.c, t_list.c, t_set.c, t_string.c, t_zset.c.

initServer

Function này sẽ tạo ra 1 "Event loop" và mở 1 TCP hay Unix domain socket và listen trên đó tùy vào giá trị file config:

server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
...
for (j = 0; j < server.ipfd_count; j++) {
    if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
        acceptTcpHandler,NULL) == AE_ERR)
        {
        serverPanic(
            "Unrecoverable error creating server.ipfd file event.");
        }
}
if (server.sofd > 0 && aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
    acceptUnixHandler,NULL) == AE_ERR) serverPanic("Unrecoverable error creating server.sofd file event.");
  • aeCreateEventLoop: tạo 1 Event loop (xem file ae.c) và gán vào biến server
  • aeCreateFileEvent: Nếu mình config Redis listen trên TCP thì khi connect Redis sẽ gọi "acceptTcpHandler" function, còn nếu listen trên Unix domain socket thì sẽ gọi "acceptUnixHandler" function

aeMain

Chạy "Event loop", mỗi lần loop sẽ check xem có I/O event hay time events nào hay không, nếu có sẽ xử lý các event này.

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

Nói đơn giản là khi bạn connect đến Redis, thì system sẽ sinh ra 1 cấu trúc gọi là "File descriptor" được định danh bằng 1 số Integer. Redis sẽ track số này, khi bạn run 1 command vd như "get" thì Redis sẽ biết được "File descriptor" nào có "event" và xử lý các event này trong mỗi lần loop => Vì vậy người ta mới gọi là "Event loop".

String data type

Ở đây mình chỉ nói sơ về kiểu dữ liệu đơn giản nhất là "String", source code nằm trong file "t_string.c". Còn các kiểu khác các bạn có thể xem source code trong các file: t_hash.c, t_list.c, t_set.c, t_zset.c...

Vd mình chạy command sau để gán chuỗi "Hello" vào key "mykey" trong Redis:

SET mykey "Hello"

Bạn thử tưởng tượng nếu bạn implement bằng PHP thì sẽ viết như sau:

$values['mykey'] = 'hello';

Như các bạn đã biết thì trong ngôn ngữ C không có "Associative array". Nghĩa là các "key" là Integer chứ không phải String. Vì vậy phải cần 1 "Hash function" chuyển các key dạng String sang Integer để có thể lưu trữ được.
Redis dùng "MurmurHash2" được viết trong file "dict.c". Tương tự PHP cũng dùng hàm Hash này để implement "Associative array".

Thực ra để implement một Data type thì bên trong Redis có nhiều cái hơn nữa, nhưng mình chỉ nói những cái đơn giản nhất để tránh phức tạp vấn đề, và cho các bạn có sơ sở nghiên cứu tiếp.

Tổng kết

Bài viết này giúp bạn có cái nhìn tổng quan về cấu trúc bên trong của Redis, từ đó có thể research cách Redis được phát triển như thế nào để cùng nhau ôn lại bộ môn "Cấu trúc dữ liệu và Giải thuật" nhé :)