自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

用C語言擼了個DBProxy

開發(fā) 后端
筆者在閱讀了一大堆源碼后,就會情不自禁產(chǎn)生造輪子的想法。于是花了數(shù)個周末的時間用C語言擼了一個DBProxy(MySQL協(xié)議)。在筆者的github中給這個DBProxy起名為Hero。

[[341464]]

前言

筆者在閱讀了一大堆源碼后,就會情不自禁產(chǎn)生造輪子的想法。于是花了數(shù)個周末的時間用C語言擼了一個DBProxy(MySQL協(xié)議)。在筆者的github中給這個DBProxy起名為Hero。

為什么采用C語言

筆者一直有C情節(jié),求學(xué)時候一直玩C。工作之后,一直使用Java,就把C漸漸放下了。在筆者最近一年閱讀了一堆關(guān)于linux Kernel(C)和MySQL(C++)的源碼后,就萌生了重拾C的想法。同時用純C的話,勢必要從基礎(chǔ)開始造一大堆輪子,這也符合筆者當(dāng)時造輪子的心境。

造了哪些輪子

習(xí)慣了Java的各種好用的類庫框架之后,用純C無疑就是找虐。不過,既然做了這個決定,跪著也得搞完。在寫Hero的過程中。很大一部分時間就是在搭建基礎(chǔ)工具,例如:

  • Reactor模型
  • 內(nèi)存池
  • packet_buffer
  • 協(xié)議分包處理
  • 連接池
  • ......

下面在這篇博客里面一一道來

DBProxy的整體原理

Hero(DBProxy)其實(shí)就是自己偽裝成MySQL,接收到應(yīng)用發(fā)過來的SQL命令后,再轉(zhuǎn)發(fā)到后端。如下圖所示:

 

由于Hero在解析SQL的時候,可以獲取各種信息,例如事務(wù)這個信息就可以通過set auto_commit和begin等命令存在連接狀態(tài)里面,再根據(jù)解析出來的SQL判斷其是否需要走主庫。這樣就可以對應(yīng)用透明的進(jìn)行主從分離以至于分庫分表等操作。

當(dāng)然了,筆者現(xiàn)在的Hero剛把基礎(chǔ)的功能搭建好(協(xié)議、連接池等),對連接狀態(tài)還沒有做進(jìn)一步的處理。

Reactor模式

Hero的網(wǎng)絡(luò)模型采用了Reactor模式,而且是多線程模型,同時采用epoll的水平觸發(fā)。

采用多線程模型

為什么采用多線程,純粹是為了編寫代碼簡單。多進(jìn)程的話,還得考慮worker進(jìn)程間負(fù)載均衡問題,例如nginx就在某個worker進(jìn)程達(dá)到7/8最大連接數(shù)的時候拒絕獲取連接從而轉(zhuǎn)給其它worker。多線程的話,在accept線程里面通過取模選擇一個worker線程就可以輕松的達(dá)到簡單的負(fù)載均衡結(jié)果。

采用epoll水平觸發(fā)

為什么采用epoll的水平觸發(fā),純粹也是為了編寫代碼簡單。如果采用邊緣觸發(fā)的話,需要循環(huán)讀取直到read返回字節(jié)數(shù)為0為止。然而如果某個連接特別活躍,socket的數(shù)據(jù)一直讀不完,會造成其它連接饑餓,所以必須還得自己寫個均衡算法,在讀到一定程度后,去選擇其它連接。

Reactor

整體Reactor模型如下圖所示:

 

其實(shí)代碼是很簡單的,如下面代碼所示的就是reactor中的accept處理:

  1. // 中間省略了大量的錯誤處理 
  2. int init_reactor(int listen_fd,int worker_count){ 
  3.     // 注意,這邊需要是unsigned 防止出現(xiàn)負(fù)數(shù) 
  4.     unsigned int current_worker = 0; 
  5.     for(;;){ 
  6.         int numevents = 0; 
  7.         int retval = epoll_wait(reactor->master_fd,reactor->events,EPOLL_MAX_EVENTS,500); 
  8.         ...... 
  9.         for(j=0; j < numevents; j++){ 
  10.             client_fd = accept(listen_fd, (struct sockaddr *) &client_addr, &client_len)) 
  11.             poll_add_event(reactor->worker_fd_arrays[current_worker++%reactor->worker_count],conn->sockfd,EPOLLOUT,conn) 
  12.             ...... 
  13.          } 

上面代碼中,每來一個新的連接current_worker都自增,這樣取模后便可在連接層面上對worker線程進(jìn)行負(fù)載均衡。

而worker線程則通過pthread去實(shí)現(xiàn),如下面代碼所示:

  1. // 這里的worker_count根據(jù)調(diào)用get_nprocs得到的對應(yīng)機(jī)器的CPU數(shù)量 
  2. // 注意,由于docker返回的是宿主機(jī)CPU數(shù)量,所以需要自行調(diào)整 
  3.     for(int i=0;i<worker_count;i++){ 
  4.         reactor->worker_fd_arrays[i] = epoll_create(EPOLL_MAX_EVENTS); 
  5.         if(reactor->worker_fd_arrays[i] == -1){ 
  6.            goto error_process; 
  7.         } 
  8.         // 通過pthread去創(chuàng)建worker線程 
  9.         if(FALSE == create_and_start_rw_thread(reactor->worker_fd_arrays[i],pool)){ 
  10.             goto error_process; 
  11.         } 
  12.     } 

而worker線程的處理也是按照標(biāo)準(zhǔn)的epoll水平觸發(fā)去處理的:

  1. static void* rw_thread_func(void* arg){ 
  2.     ...... 
  3.     for(;;){ 
  4.         int numevents = 0; 
  5.         int retval = epoll_wait(epfd,events,EPOLL_MAX_EVENTS,500); 
  6.         ...... 
  7.         for(j=0; j < numevents; j++){ 
  8.             if(event & EPOLLHUP){ 
  9.                 // 處理斷開事件 
  10.                 ...... 
  11.             }else if(event & EPOLLERR){ 
  12.                 // 處理錯誤事件 
  13.                 ...... 
  14.             } 
  15.  
  16.         }else { 
  17.             if(event & EPOLLIN){ 
  18.                 handle_ready_read_connection(conn); 
  19.                 continue
  20.             } 
  21.             if(event & EPOLLOUT){ 
  22.                 hanlde_ready_write_connection(conn); 
  23.                 continue
  24.             } 
  25.         } 
  26.     } 
  27.     ...... 

內(nèi)存池

為什么需要內(nèi)存池

事實(shí)上,筆者在最開始編寫的時候,是直接調(diào)用標(biāo)準(zhǔn)庫里面的malloc的。但寫著寫著,就發(fā)現(xiàn)一些非常坑的問題。例如我要在一個請求里面malloc數(shù)十個結(jié)構(gòu),同時每次malloc都有失敗的可能,那么我的代碼可能寫這樣。

  1. void do_something(){ 
  2.     void * a1 = (void*)malloc(sizeof(struct A)); 
  3.     if(a1 == NULL){ 
  4.         goto error_process; 
  5.     } 
  6.     ...... 
  7.     void * a10 = (void*)malloc(sizeof(struct A)); 
  8.     if(a10 == NULL
  9.         goto error_process; 
  10.     } 
  11. error_process: 
  12.     if(NULL != a1){ 
  13.         free(a1); 
  14.     } 
  15.     ...... 
  16.     if(NULL != a10){ 
  17.         free(a10); 
  18.     } 

寫著寫著,筆者就感覺完全不可控制了。尤其是在各種條件分支加進(jìn)去之后,可能本身aN這個變量都還沒有被分配,那么就還不能用(NULL != aN),還得又一堆復(fù)雜的判斷。

有了內(nèi)存池之后,雖然依舊需要仔細(xì)判斷內(nèi)存池不夠的情況,但至少free的時候,只需要把內(nèi)存池整體給free掉即可,如下面代碼所示:

  1. void do_something(mem_pool* pool){ 
  2.     void * a1 = (void*)mem_pool_alloc(sizeof(struct A),pool); 
  3.     if(a1 == NULL){ 
  4.         goto error_process; 
  5.     } 
  6.     ...... 
  7.     void * a10 = (void*)mem_pool_alloc(sizeof(struct A),pool); 
  8.     if(a10 == NULL
  9.         goto error_process; 
  10.     } 
  11. error_process: 
  12.     // 直接一把全部釋放 
  13.     mem_pool_free(pool); 

內(nèi)存池的設(shè)計(jì)

為了編寫代碼簡單,筆者采用了比較簡單的設(shè)計(jì),如下圖所示:

 

這種內(nèi)存池的好處就在于分配很多小對象的時候,不必一一去清理,直接連整個內(nèi)存池都重置即可。而且由于每次free的基本都是mem_block大小,所以產(chǎn)生的內(nèi)存碎片也少。不足之處就在于,一些可以被立即銷毀的對象只能在最后重置內(nèi)存池的時候才銷毀。但如果都是小對象的話,影響不大。

內(nèi)存池的分配優(yōu)化

考慮到內(nèi)存對齊,每次申請內(nèi)存的時候都按照sizeof(union hero_aligin)進(jìn)行最小內(nèi)存分配。這是那本采用\<\>的推薦的對齊大小。

  1. 采用<<c interface and implemention>>的實(shí)現(xiàn) 
  2. union hero_align{ 
  3.     int i; 
  4.     long l; 
  5.     long *lp; 
  6.     void *p; 
  7.     void (*fp)(void); 
  8.     float f; 
  9.     double d; 
  10.     long double ld; 
  11. }; 

真正的對齊則是參照nginx的寫法:

  1. size = (size + sizeof(union hero_align) - 1) & (~(sizeof(union hero_align) - 1)); 

其實(shí)筆者一開始還打算做一下類似linux kernel SLAB緩存中的隨機(jī)著色機(jī)制,這樣能夠緩解false sharing問題,想想為了代碼簡單,還是算了。

為什么需要packet_buffer

packet_buffer是用來存儲從socket fd中讀取或?qū)懭氲臄?shù)據(jù)。

設(shè)計(jì)packet_buffer的初衷就是要重用內(nèi)存,因?yàn)橐粋€連接反復(fù)的去獲取寫入數(shù)據(jù)總歸是需要內(nèi)存的,只在連接初始化的時候去分配一次內(nèi)存顯然是比反復(fù)分配再銷毀效率高。

為什么不直接用內(nèi)存池

上文中說到,銷毀內(nèi)存必須將池里面的整個數(shù)據(jù)重置。如果packet_buffer和其它的數(shù)據(jù)結(jié)構(gòu)在同一內(nèi)存池中分配,要重用它,那么在packet_buffer之前分配的數(shù)據(jù)就不能被清理。如下圖所示:

 

這樣顯然違背了筆者設(shè)計(jì)內(nèi)存池是為了釋放內(nèi)存方便的初衷。

另外,如果使用內(nèi)存池,那么從sockfd中讀取/寫入的數(shù)據(jù)就可能從連續(xù)的變成一個一個mem_block分離的數(shù)據(jù),這種不連續(xù)性對數(shù)據(jù)包的處理會特別麻煩。如下圖所示:

 

當(dāng)然了,這種非連續(xù)的分配方式,筆者曾經(jīng)在閱讀lwip協(xié)議時見過(幫某實(shí)時操作系統(tǒng)處理一個詭異的bug),lwip在嵌入式這種內(nèi)存稀缺的環(huán)境中使用這種方式從而盡量避免大內(nèi)存的分配。

所以筆者采取了在連接建立時刻一次性分配一個比較大的內(nèi)存來處理單個請求的數(shù)據(jù),如果這個內(nèi)存也滿足不了,則realloc(當(dāng)然了realloc也有坑,需要仔細(xì)編寫)。

packet_buffer結(jié)構(gòu)

 

packet_buffer這種動態(tài)改變大小而且地址上連續(xù)的結(jié)構(gòu)為處理包結(jié)構(gòu)提供了便利。

由于realloc的時候,packet_buffer->buffer本身指向的地址可能會變,所以盡量避免直接操作內(nèi)部buffer,非得使用內(nèi)部buffer的時候,不能將其賦予一個局部變量,否則buffer變化,局部變量可能指向了之前廢棄的buffer。

MySQL協(xié)議分包處理

MySQL協(xié)議基于tcp(當(dāng)然也有unix域協(xié)議,這里只考慮tcp)。同時Hero采用的是非阻塞IO模式,讀取包時,recv系統(tǒng)調(diào)用可能在包的任意比特位置上返回。這時候,就需要仔細(xì)的處理分包。

MySQL協(xié)議外層格式

MySQL協(xié)議是通過在幀頭部加上length field的設(shè)計(jì)來處理分包問題。如下圖所示:

 

Hero的處理

Hero對于length field采用狀態(tài)機(jī)進(jìn)行處理,這也是通用的手法。首先讀取3byte+1byte的packet_length和sequenceId,然后再通過packet_length讀取剩下的body長度。如下圖所示:

 

連接池

Hero的連接池造的還比較粗糙,事實(shí)上就是一個數(shù)組,通過mutex鎖來控制并發(fā)的put/get

 

連接管理部分尚未開發(fā)。

MySQL協(xié)議格式處理

相較于前面的各種輪子,MySQL協(xié)議本身反倒顯得輕松許多,唯一復(fù)雜的地方在于握手階段的加解密過程,但是MySQL是開源的,筆者直接將MySQL本身對于握手加解密的代碼copy過來就行了。以下代碼copy自MySQL-5.1.59(密碼學(xué)太高深,這個輪子不造也罷):

  1. // 摘自MySQL-5.1.59,用作password的加解密 
  2. void scramble(char *to, const char *message, const char *password) { 
  3.   SHA1_CONTEXT sha1_context; 
  4.   uint8 hash_stage1[SHA1_HASH_SIZE]; 
  5.   uint8 hash_stage2[SHA1_HASH_SIZE]; 
  6.   mysql_sha1_reset(&sha1_context); 
  7.   /* stage 1: hash password */ 
  8.   mysql_sha1_input(&sha1_context, (uint8 *) password, (uint) strlen(password)); 
  9.   mysql_sha1_result(&sha1_context, hash_stage1); 
  10.   /* stage 2: hash stage 1; note that hash_stage2 is stored in the database */ 
  11.   mysql_sha1_reset(&sha1_context); 
  12.   mysql_sha1_input(&sha1_context, hash_stage1, SHA1_HASH_SIZE); 
  13.   mysql_sha1_result(&sha1_context, hash_stage2); 
  14.   /* create crypt string as sha1(message, hash_stage2) */; 
  15.   mysql_sha1_reset(&sha1_context); 
  16.   mysql_sha1_input(&sha1_context, (const uint8 *) message, SCRAMBLE_LENGTH); 
  17.   mysql_sha1_input(&sha1_context, hash_stage2, SHA1_HASH_SIZE); 
  18.   /* xor allows 'from' and 'to' overlap: lets take advantage of it */ 
  19.   mysql_sha1_result(&sha1_context, (uint8 *) to); 
  20.   my_crypt(to, (const uchar *) to, hash_stage1, SCRAMBLE_LENGTH); 

剩下的無非就是按格式解釋包中的各個字段,然后再進(jìn)行處理而已。典型代碼段如下:

  1. int handle_com_query(front_conn* front){ 
  2.     char* sql = read_string(front->conn->read_buffer,front->conn->request_pool); 
  3.     int rs = server_parse_sql(sql); 
  4.     switch(rs & 0xff){ 
  5.         case SHOW: 
  6.             return handle_show(front,sql,rs >> 8); 
  7.         case SELECT
  8.             return handle_select(front,sql,rs >> 8); 
  9.         case KILL_QUERY: 
  10.                 ...... 
  11.         default
  12.             return default_execute(front,sql,FALSE); 
  13.     } 
  14.     return TRUE

需要注意的是,hero在后端連接backend返回result_set結(jié)果集并拷貝到前端連接的write_buffer的時候,前端連接可能正在寫入,也會操縱write_buffer。所以在這種情況下要通過Mutex去保護(hù)write_buffer(packet_buffer)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

性能對比

下面到了令人激動的性能對比環(huán)節(jié),筆者在一個4核8G的機(jī)器上用hero和另一個用java nio寫的成熟DBProxy做對比。兩者都是用show databases,這條sql并不會路由到后端的數(shù)據(jù)庫,而是純內(nèi)存返回。這樣筆者就能知道筆者自己造的reactor框架的性能如何,以下是對比情況:

  1. 用作對比的兩個server的代碼IO模型 
  2. hero(c):epoll 水平觸發(fā) 
  3. proxy(java):java nio(內(nèi)部也是epoll 水平觸發(fā)) 
  4. benchMark: 
  5. 服務(wù)器機(jī)器 
  6. 4核8G 
  7. CPU主屏2399.996MHZ 
  8. cache size:4096KB 
  9. 壓測機(jī)器: 
  10. 16核64G,jmeter 
  11. 同樣配置下,壓測同一個簡單的sql 
  12. hero(c):3.6Wtps/s 
  13. proxy(java):3.6Wtps/s 
  14. tps基本沒有差別,因?yàn)槠款i是在網(wǎng)絡(luò)上 
  15. CPU消耗: 
  16. hero(c):10% cpu 
  17. proxy(java):15% cpu 
  18. 內(nèi)存消耗: 
  19. hero(c):0.2% * 8G 
  20. proxy(java):48.3% * 8G 
  21. 結(jié)論: 
  22. 對于IO瓶頸的情況,用java和C分別處理簡單的組幀/解幀邏輯,C語言帶來的微小收益并不能讓tps有顯著改善。 

Hero雖然在CPU和內(nèi)存消耗上有優(yōu)勢,但是限于網(wǎng)絡(luò)瓶頸,tps并沒有明顯提升-_-!

相比于造輪子時候付出的各種努力,投入產(chǎn)出比(至少在表面上)是遠(yuǎn)遠(yuǎn)不如用Netty這種非常成熟的框架的。如果是工作,那我會毫不猶豫的用后者。

總結(jié)

造輪子是個非常有意思的過程,在這個過程中能夠強(qiáng)迫筆者去思考平時根本無需思考的地方。

造輪子的過程也是非常艱辛的,造出來的輪子也不一定比現(xiàn)有的輪子靠譜。但造輪子的成就感還是滿滿的^_^

github鏈接

https://github.com/alchemystar/hero

碼云鏈接

 

https://gitee.com/alchemystar/hero

本文轉(zhuǎn)載自微信公眾號「解Bug之路」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系解Bug之路公眾號。

 

責(zé)任編輯:武曉燕 來源: 解Bug之路
相關(guān)推薦

2022-04-22 08:22:50

MVCCMySQLC++

2020-05-28 11:00:40

Flutter代碼框架

2021-12-12 18:18:15

代碼元宇宙Python

2019-06-25 10:46:04

Flutter開發(fā)APP

2020-02-17 13:45:27

抓取代碼工具

2020-11-04 07:56:19

工具Linux 翻譯

2023-12-07 12:59:46

C語言循環(huán)隊(duì)列代碼

2021-02-03 07:56:08

版本游戲邏輯

2021-04-27 07:52:19

StarterSpring Boot配置

2019-12-06 10:59:21

編程語言C語言開 發(fā)

2021-11-04 17:23:03

Java對象 immutable

2022-01-21 07:35:06

LRU緩存java

2021-11-29 07:47:57

gRPCGUI客戶端

2022-05-07 13:52:22

Feign 增強(qiáng)包K8s

2021-05-24 06:40:59

C語言Linux軟件庫

2011-07-20 16:23:14

C++

2020-08-14 10:01:25

編程神經(jīng)網(wǎng)絡(luò)C語言

2020-10-13 16:30:31

語言鏈表數(shù)組

2022-10-08 08:15:55

GScriptGo 語言

2022-09-14 08:01:54

語法樹編譯器語法糖
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號