Redis 源碼閱讀 ——— 網(wǎng)絡(luò)模塊
概述
redis 是cs架構(gòu),網(wǎng)絡(luò)采用epoll 模型,單線程處理每個(gè)請(qǐng)求。
很多同學(xué)對(duì)單線程有些疑問(wèn)吝沫,簡(jiǎn)單的解釋一下 redis 單線程的意思,redis 服務(wù)端雖說(shuō)是單線程递礼,但是可以同時(shí) 持有很多connection惨险,每個(gè)connection 都可以同時(shí)發(fā)請(qǐng)求,只不過(guò)在 redis 服務(wù)端脊髓,一個(gè)一個(gè)的處理每個(gè)connection 發(fā)過(guò)來(lái)的request辫愉, 通俗點(diǎn)說(shuō)就是,很多請(qǐng)求都能發(fā)過(guò)來(lái)供炼,redis 會(huì)存下來(lái)(其實(shí)是存在每個(gè)connection socket 內(nèi)核緩沖區(qū))一屋,一個(gè)一個(gè)處理。
為什么單線程處理效率如此之高袋哼?
- 幾乎所有的操作全部是內(nèi)存操作冀墨,內(nèi)存操作非常快(如果有一些系統(tǒng)調(diào)用涛贯,磁盤操作诽嘉,單線程不會(huì)快的)
- 單線程避免了使用鎖(memcache 使用了多線程,因?yàn)槎嗔随i之類的弟翘,也沒(méi)比redis快多少)
EPOLL 介紹
如果想讀懂 redis 網(wǎng)絡(luò)相關(guān)的代碼虫腋,必須先搞清楚 epoll 的使用,epoll 說(shuō)白了就是監(jiān)聽 fd(file descriptor稀余,操作 fd 其實(shí)就是操作socket)悦冀,每當(dāng) fd 上面有消息的時(shí)候(比如 可讀,可寫 消息等)睛琳,就會(huì)得到通知盒蟆,這樣就可以處理了。epoll 主要好處是可以同時(shí)監(jiān)聽多個(gè) fd(可以持有多個(gè) client 連接)师骗,epoll 只有在 持有很多連接历等,并且每個(gè)連接都不是特別活躍的時(shí)候 效率才高,其他的情況辟癌,不見得比 poll,select 高寒屯。
epoll 使用只需要三步:
- int epoll_create(int size);
創(chuàng)建一個(gè)epoll的句柄,size用來(lái)告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大黍少。這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù)寡夹,給出最大監(jiān)聽的fd+1的值处面。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后要出,它就是會(huì)占用一個(gè)fd值鸳君,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的患蹂,所以在使用完epoll后,必須調(diào)用close()關(guān)閉砸紊,否則可能導(dǎo)致fd被耗盡 - int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊(cè)函數(shù)传于,它不同與select()是在監(jiān)聽事件時(shí)告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊(cè)要監(jiān)聽的事件類型醉顽。第一個(gè)參數(shù)是epoll_create()的返回值沼溜,第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來(lái)表示:EPOLL_CTL_ADD:注冊(cè)新的fd到epfd中游添;
EPOLL_CTL_MOD:修改已經(jīng)注冊(cè)的fd的監(jiān)聽事件
-
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd系草;
第三個(gè)參數(shù)是需要監(jiān)聽的fd,第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事唆涝,struct epoll_event結(jié)構(gòu)如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;struct epoll_event {
__uint32_t events; /* Epoll events /
epoll_data_t data; / User data variable */
};
events可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對(duì)應(yīng)的文件描述符可以讀(包括對(duì)端SOCKET正常關(guān)閉)找都;
EPOLLOUT:表示對(duì)應(yīng)的文件描述符可以寫; EPOLLPRI:表示對(duì)應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來(lái))廊酣; EPOLLERR:表示對(duì)應(yīng)的文件描述符發(fā)生錯(cuò)誤能耻; EPOLLHUP:表示對(duì)應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式亡驰,這是相對(duì)于水平觸發(fā)(Level Triggered)來(lái)說(shuō)的晓猛。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后凡辱,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話戒职,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生,類似于select()調(diào)用透乾。參數(shù)events用來(lái)從內(nèi)核得到事件的集合洪燥,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè) maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size续徽,參數(shù)timeout是超時(shí)時(shí)間(毫秒蚓曼,0會(huì)立即返回,-1將不確定钦扭,也有說(shuō)法說(shuō)是永久阻塞)纫版。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí)客情。
Redis 中 epoll 的使用
Redis epoll 封裝介紹
redis 跟 網(wǎng)絡(luò)相關(guān)的代碼寫的比較簡(jiǎn)潔其弊,主要就兩處
- 不同操作系統(tǒng)的 epoll 代碼癞己,都在 ae_epoll.c ae_evport.c ae_kqueue.c ae_select.c 中, linux 使用 ae_epoll.c , mac 使用 ae_kqueue.c
- 對(duì) epoll 代碼的封裝在 ae.c 中
- aeCreateEventLoop 是對(duì) epoll_create 的封裝
- aeCreateFileEvent 是對(duì) epoll_ctl 的封裝梭伐,同時(shí)會(huì)將rfileProc痹雅, wfileProc 兩個(gè)處理消息的回調(diào)函數(shù)一起封裝
- aeProcessEvents 是對(duì) epoll_wait 的封裝
- aeMain 是一個(gè)死循環(huán),不停的調(diào)用 aeProcessEvents, redis 就是在這里不停的收到 client 的 request糊识, 并且一個(gè)一個(gè)處理
aeCreateEventLoop:
創(chuàng)建 aeEventLoop 結(jié)構(gòu)體
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
aeFileEvent *events; /* Registered events */
aeFiredEvent *fired; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
這個(gè)結(jié)構(gòu)體中主要就是 events, fired 兩個(gè)aeFileEvent類型變量绩社,aeFileEvent 中 rfileProc, wfileProc 是兩個(gè)回調(diào)函數(shù)赂苗, 分別處理讀時(shí)間愉耙, 寫時(shí)間, events 是aeCreateFileEvent 函數(shù)調(diào)用時(shí) 為其賦值拌滋,fird 是 監(jiān)聽到有消息來(lái)的時(shí)候 為其賦值朴沿,在 ae_epoll.c 中 aeApiPoll 函數(shù)。
總結(jié)一下败砂, 服務(wù)啟動(dòng) aeCreateEventLoop 創(chuàng)建 aeEventLoop 類型的變量赌渣, 將需要監(jiān)控的 fd, 通過(guò) aeCreateFileEvent 監(jiān)聽(同時(shí)將 賦值 rfileProc, wfileProc 回調(diào)函數(shù))昌犹, aeProcessEvents 監(jiān)聽到有消息需要處理的時(shí)候坚芜, 會(huì)使用 rfileProc, wfileProc 回調(diào)函數(shù)處理消息。所以祭隔,讀 Redis 網(wǎng)絡(luò)相關(guān)代碼 货岭,其實(shí)只是看 aeCreateFileEvent(監(jiān)聽fd,設(shè)置對(duì)fd的回調(diào)函數(shù)) 在哪些地方被調(diào)用就可以了疾渴。
Redis 關(guān)鍵代碼
-
initServer(server.c )無(wú)關(guān)代碼刪除:
void initServer(void) { server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR); listenToPort(server.port,server.ipfd,&server.ipfd_count); 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."); } } 這段代碼在默認(rèn)開啟redis-server 的情況下千贯,server.ipfd 代表的fd 是 6379 打開的socket, 在6379監(jiān)聽到的消息搞坝,都調(diào)用 acceptTcpHandler 函數(shù)
acceptTcpHandler(networking.c)無(wú)關(guān)代碼刪除
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
acceptCommonHandler(cfd,0,cip);
}
}
static void acceptCommonHandler(int fd, int flags, char *ip) {
client *c;
c = createClient(fd)
}
client *createClient(int fd) {
aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
}
這段代碼很清晰的表明了搔谴, 對(duì)于6379 過(guò)來(lái)的請(qǐng)求,全部 使用acceptTcpHandler 函數(shù)生成一個(gè)新的fd桩撮, 在同時(shí)將這個(gè)fd 放在 eventloop 中監(jiān)聽敦第,并且 使用 readQueryFromClient 來(lái)處理readQueryFromClient(networking.c) 無(wú)關(guān)代碼刪除
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
nread = read(fd, c->querybuf+qblen, readlen);
processInputBuffer(c);
}
readQueryFromClient 就是把請(qǐng)求內(nèi)容讀出來(lái), 在調(diào)用 processInputBuffer 處理店量, processInputBuffer 就是 redis 里面各種業(yè)務(wù)邏輯了芜果,不在介紹
Epoll 總結(jié)
redis 網(wǎng)絡(luò)相關(guān)代碼其實(shí)就是一句話 使用 epoll 處理每一個(gè)請(qǐng)求,也沒(méi)什么好學(xué)習(xí)的融师。右钾。。。舀射。窘茁。。
Redis 如何處理TCP 粘包脆烟,拆包
粘包山林,拆包介紹
tcp是面向流的, 所以tcp對(duì)數(shù)據(jù)內(nèi)容毫無(wú)感知,收到就放在緩沖區(qū)里面等待用戶讀取邢羔,所以從server端讀出來(lái)的數(shù)據(jù)驼抹,可能是按照發(fā)送順序(tcp 保證不亂不丟)的任何內(nèi)容, 這樣 server 端如果無(wú)法識(shí)別出來(lái)一個(gè)完整的數(shù)據(jù)就出錯(cuò)了拜鹤。解決辦法有兩種
- 特定分隔符砂蔽,比如 http 的一個(gè) request 是以 \r\n\r\n 結(jié)尾的,在服務(wù)端就可以一直讀到這個(gè)特定的 \r\n\r\n 署惯,通過(guò)這種方式可以區(qū)分出來(lái)一個(gè) request 的數(shù)據(jù)
- 指定長(zhǎng)度, 比如在 前4個(gè)字節(jié)中 存放這條消息的長(zhǎng)度镣隶,這樣就知道就可以通過(guò) read 函數(shù)正確的讀出數(shù)據(jù)极谊。
特定字符的辦法優(yōu)勢(shì)是,不用浪費(fèi)空間存長(zhǎng)度安岂,但是現(xiàn)在的計(jì)算機(jī)環(huán)境通城岵可以忽略這個(gè)浪費(fèi),劣勢(shì)是 每個(gè)字符都需要判斷才能保證正確域那。效率低咙边。
指定長(zhǎng)度的辦法是浪費(fèi)空間存長(zhǎng)度,但是效率高次员,所以基本上可以說(shuō)任何時(shí)候都采用第二種方法
Redis 如何處理
set a 1 這條指令败许,按照 redis 協(xié)議,會(huì)翻譯成
*3
$3
set
$1
a
$1
1
*3 表示有3行數(shù)據(jù)淑蔚, $3 表示 有3個(gè)字符
屬于哪種方法讀者自己感受下市殷。
Reids 的聰明之處
在我沒(méi)讀redis代碼的時(shí)候,我一直認(rèn)為 從緩沖區(qū) 讀出來(lái)自己需要的長(zhǎng)度刹衫,處理好以后醋寝,在從緩沖區(qū)里繼續(xù)讀,看了redis 代碼以后带迟,我才發(fā)現(xiàn)自己 too yong, redis 是這樣做的(networking.c readQueryFromClient 函數(shù)音羞, 代碼有刪減)
readlen = PROTO_IOBUF_LEN;
qblen = sdslen(c->querybuf);
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
nread = read(fd, c->querybuf+qblen, readlen);
define PROTO_IOBUF_LEN (102416) / Generic I/O buffer size */
redis 每次都讀緩沖區(qū)的大小,如果最后一條消息不完整仓犬,下次計(jì)算一下長(zhǎng)度嗅绰,繼續(xù)讀,因?yàn)檫@個(gè)騷操作(知道了其實(shí)就是常規(guī)操作),讓效率大大提升了办陷,不然每次使用 read 系統(tǒng)調(diào)用貌夕,非常影響性能,特別對(duì)于 redis 這種單線程模型程序影響就更大了民镜。
Redis如何處理 half connection
half connection 介紹
client -> server, 雖然我們都說(shuō)connection啡专,其實(shí) 就是client 開著一個(gè) fd, server 開著一個(gè) fd制圈,兩個(gè)fd之間可以互相通信们童,關(guān)閉的時(shí)候 一個(gè) fd 跟另外一個(gè) fd 說(shuō)我準(zhǔn)備關(guān)閉了(tcp 四次揮手), 不過(guò)如果有的極端情況(在大規(guī)模server端是常規(guī)情況),比如拔網(wǎng)線鲸鹦,關(guān)機(jī)慧库,網(wǎng)絡(luò)異常等原因(具體我也沒(méi)試驗(yàn)過(guò)), 可能發(fā)不出任何消息 就斷了,另外一個(gè) fd 就在那里傻傻的等著馋嗜,這就出現(xiàn)了 half connection 的情況齐板。
如何處理 half connection
- 一般的處理辦法就是心跳檢查,服務(wù)端會(huì) 定時(shí)的 ping 客戶端葛菇,如果連續(xù)幾次 都 ping 不通甘磨,那么就會(huì)主動(dòng)斷開鏈接
為什么不使用 keep_alive 處理
Host Requirements RFC羅列有不使用它的三個(gè)理由:
- 在短暫的故障期間,它們可能引起一個(gè)良好連接(good connection)被釋放(dropped)
- 它們消費(fèi)了不必要的寬帶
- 在以數(shù)據(jù)包計(jì)費(fèi)的互聯(lián)網(wǎng)上它們(額外)花費(fèi)金錢眯停。然而济舆,在許多的實(shí)現(xiàn)中提供了存活定時(shí)器。
這種說(shuō)法有它的道理莺债,但是并不能說(shuō)服我不使用 keep alive滋觉,最能說(shuō)服我的是在知乎上看過(guò)的一句話, keep_alive 只能保證 tcp 是正常的齐邦,但是不能保證 用戶程序是正常的椎侠,自己感受一下這些話。
Redis 如何處理
server.c clientsCronHandleTimeout 函數(shù)
if (server.maxidletime &&
!(c->flags & CLIENT_SLAVE) && /* no timeout for slaves */
!(c->flags & CLIENT_MASTER) && /* no timeout for masters */
!(c->flags & CLIENT_BLOCKED) && /* no timeout for BLPOP */
!(c->flags & CLIENT_PUBSUB) && /* no timeout for Pub/Sub clients */
(now - c->lastinteraction > server.maxidletime))
{
serverLog(LL_VERBOSE,"Closing idle client");
freeClient(c);
return 1;
} else if (c->flags & CLIENT_BLOCKED) {
......
通過(guò)代碼可以看出來(lái)侄旬,redis 根本就既沒(méi)有用 keep_alive , 也沒(méi)有用 ping肺蔚, 而是簡(jiǎn)單粗暴的通過(guò) client 最后一次訪問(wèn)server 的時(shí)間 條件來(lái)判斷,不管 這條連接是不是正常的儡羔,這樣同樣可以解決 half connection 問(wèn)題宣羊。
我知道 redis 肯定要處理 half connection 的問(wèn)題,所以我開始找 ping 相關(guān)代碼汰蜘,但是沒(méi)找到仇冯,后來(lái)就老老實(shí)實(shí)從定時(shí)相關(guān)代碼里面看,才找到族操。
第一反應(yīng)苛坚,覺(jué)得比較奇怪比被,為什么好的連接也給斷開了呢,是不是redis比較傻泼舱,能不能給他提個(gè)優(yōu)雅處理的pr等缀, 后來(lái)仔細(xì)想想,果然還是我自己 too yong, 作為服務(wù)端娇昙,連接資源還是比較寶貴的尺迂,如果長(zhǎng)時(shí)間不訪問(wèn)服務(wù)端斷開本來(lái)就是很合理的,而且如果用我開始覺(jué)得優(yōu)雅的心跳冒掌,簡(jiǎn)直就是災(zāi)難噪裕,因?yàn)?redis 是單線程的,心跳都是一次網(wǎng)絡(luò)交互股毫。膳音。。铃诬。
我看到過(guò)很多寫心跳處理 half connection 的代碼祭陷,原來(lái)一直覺(jué)得這就是最好的方法,學(xué)習(xí)了 redis 我才發(fā)現(xiàn)別有洞天趣席,而且我仔細(xì)思考了下颗胡,感覺(jué)大部分時(shí)候 redis 這種處理方法更科學(xué)。
感悟
都說(shuō) redis 代碼簡(jiǎn)潔吩坝,易讀,不過(guò)沒(méi)想到竟簡(jiǎn)潔如斯Q颇琛6で蕖! tcp 粘包 拆包的處理闸迷, half connection 的處理嵌纲,都讓我有一種別開生面的感覺(jué)。其實(shí)文章里只寫了有代表性的東西腥沽,時(shí)間有限無(wú)法一一列舉逮走,代碼組織,架構(gòu)都讓我覺(jué)得提升不少今阳, 讀 redis 真的是一種享受师溅,建議看過(guò)這篇文章的朋友都看一看 redis 代碼,不要覺(jué)得難盾舌,其實(shí)你發(fā)現(xiàn)比讀你同事的垃圾代碼 容易多了墓臭。。妖谴。窿锉。