歷史原因,一直使用 libev 作為服務底層;異步框架雖然性能比較高,但新人學習和使用門檻非常高,而且串行的邏輯被打散為狀態機,這也會嚴重影響生產效率。
用同步方式實現異步功能,既保證了異步性能優勢,又使得同步方式實現源碼思路清晰,容易維護,這是協程的優勢。帶著這樣的目的學習微信開源的一個輕量級網絡協程庫:libco 。
1. 概述
libco 是輕量級的協程庫,看完下面幾個帖子,應該能大致搞懂它的工作原理。
2. 問題
帶著問題學習 libco:
搞清這幾個概念:阻塞,非阻塞,同步,異步,鎖。
協程是什么東西,與進程和線程有啥關系。
協程解決了什么問題。
協程在什么場景下使用。
協程切換原理。
協程切換時機。
協程需要上鎖嗎?
libco 主要有啥功能。(協程管理,epoll/kevent,hook)
3. libco 源碼結構布局
將 libco 的源碼結構展開,這樣方便理清它的內部結構關系。
4. mysql 測試
測試目標:測試 libco 協程性能,以及是否能將 mysqlclient 同步接口進行異步改造。
測試系統:CentOS Linux release 7.7.1908 (Core)
測試源碼:github。
4.1. 測試源碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58/* 數據庫信息。 */
typedef struct db_s {
std::string host;
int port;
std::string user;
std::string psw;
std::string charset;
} db_t;
/* 協程任務。 */
typedef struct task_s {
int id; /* 任務 id。 */
db_t* db; /* 數據庫信息。 */
MYSQL* mysql; /* 數據庫實例指針。 */
stCoRoutine_t* co; /* 協程指針。 */
} task_t;
/* 協程處理函數。 */
void* co_handler_mysql_query(void* arg) {
co_enable_hook_sys();
...
/* 同步方式寫數據庫訪問代碼。 */
for (i = 0; i < g_co_query_cnt; i++) {
g_cur_test_cnt++;
/* 讀數據庫 select。 */
query = "select * from mytest.test_async_mysql where id = 1;";
if (mysql_real_query(task->mysql, query, strlen(query))) {
show_error(task->mysql);
return nullptr;
}
res = mysql_store_result(task->mysql);
mysql_free_result(res);
}
...
}
int main(int argc, char** argv) {
...
/* 協程個數。 */
g_co_cnt = atoi(argv[1]);
/* 每個協程 mysql query 次數。 */
g_co_query_cnt = atoi(argv[2]);
/* 數據庫信息。 */
db = new db_t{"127.0.0.1", 3306, "root", "123456", "utf8mb4"};
for (i = 0; i < g_co_cnt; i++) {
task = new task_t{i, db, nullptr, nullptr};
/* 創建協程。 */
co_create(&(task->co), NULL, co_handler_mysql_query, task);
/* 喚醒協程。 */
co_resume(task->co);
}
/* 循環處理協程事件邏輯。 */
co_eventloop(co_get_epoll_ct(), 0, 0);
...
}
5. hook
在 Centos 系統,查看 hook 是否成功,除了測試打印日志,其實還有其它比較直觀的方法。
5.1. strace
用 strace 查看底層的調用,我們看到 mysql_real_connect 內部的 connect,被 hook 成功,connect 前,被替換為 libco 的 connect 了。socket 在 connect 前,被修改為 O_NONBLOCK 。
1
2
3
4
5# strace -s 512 -o /tmp/libco.log ./test_libco 1 1
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 4
fcntl(4, F_GETFL) = 0x2 (flags O_RDWR)
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
connect(4, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now inprogress)
5.2. gdb
上神器 gdb,在 co_hook_sys_call.cpp 文件的 read 和 write 函數下斷點。
命中斷點,查看函數調用堆棧,libco 在 Centos 系統能成功 hook 住 mysqlclient 的阻塞接口。
1
2
3
4
5
6
7
8
9
10#0 read (fd=fd@entry=9, buf=buf@entry=0x71fc30, nbyte=nbyte@entry=19404) at co_hook_sys_call.cpp:299
#1 0x00007ffff762b30a in read (__nbytes=19404, __buf=0x71fc30, __fd=9) at /usr/include/bits/unistd.h:44
#2 my_read (Filedes=Filedes@entry=9, Buffer=Buffer@entry=0x71fc30 "", Count=Count@entry=19404, MyFlags=MyFlags@entry=0)
at /export/home/pb2/build/sb_0-37309218-1576675139.51/rpm/BUILD/mysql-5.7.29/mysql-5.7.29/mysys/my_read.c:64
#3 0x00007ffff7624966 in inline_mysql_file_read (
src_file=0x7ffff78424b0 "/export/home/pb2/build/sb_0-37309218-1576675139.51/rpm/BUILD/mysql-5.7.29/mysql-5.7.29/mysys/charset.c",
src_line=383, flags=0, count=19404, buffer=0x71fc30 "", file=9)
at /export/home/pb2/build/sb_0-37309218-1576675139.51/rpm/BUILD/mysql-5.7.29/mysql-5.7.29/include/mysql/psi/mysql_file.h:1129
#4 my_read_charset_file (loader=loader@entry=0x7ffff7ed7270, filename=filename@entry=0x7ffff7ed7320 "/usr/share/mysql/charsets/Index.xml",
myflags=myflags@entry=0) at /export/home/pb2/build/sb_0-37309218-1576675139.51/rpm/BUILD/mysql-5.7.29/mysql-5.7.29/mysys/charset.c:383
6. 壓測結果
從測試結果看,單進程單線程,多個協程是“同時”進行的,“并發”量也隨著協程個數增加而增加,跟測試預期一樣。
這里只測試協程的”并發性”,實際應用應該是用戶比較多,每個用戶的 sql 命令比較少的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14# ./test_libco 1 10000
id: 0, testcnt: 10000, cur spend time: 1.778823
total cnt: 10000, total time: 1.790962, avg: 5583.591448
# ./test_libco 2 10000
id: 0, testcnt: 10000, cur spend time: 2.328348
id: 1, testcnt: 10000, cur spend time: 2.360431
total cnt: 20000, total time: 2.373994, avg: 8424.620726
# ./test_libco 3 10000
id: 0, testcnt: 10000, cur spend time: 2.283759
id: 2, testcnt: 10000, cur spend time: 2.352147
id: 1, testcnt: 10000, cur spend time: 2.350272
total cnt: 30000, total time: 2.370038, avg: 12658.024719
7. mysql 連接池
用 libco 共享棧簡單造了個連接池,在 Linux 壓力測試單進程 10w 個協程,每個協程讀 10 個 sql 命令(相當于 1000w 個包),并發處理能力 8k/s,在可接受范圍內。
1
2# ./test_mysql_mgr r 100000 10
total cnt: 1000000, total time: 125.832877, avg: 7947.048692
壓測源碼(github)。
mysql 連接池簡單實現(github)。
壓測發現每個 mysql 連接只能獨立運行在固定的協程里,否則大概率會出現問題。
libco hook 技術雖然將 mysqlclient 阻塞接口設置為非阻塞,但是每個 mysqlclient 連接,必須一次只能處理一個命令,像同步那樣!非阻塞只是方便協程切換到其它空閑協程進行工作,充分利用原來阻塞等待的時間。而且 mysqlclient 本來就是按照同步的邏輯來寫的,一個連接,一次只能處理一個包,不可能被你設置為非阻塞后,一次往 mysql server 發 N 個包,這樣肯定會出現不可預料的問題。
libco 協程切換成本不高,主要是 mysqlclient 耗費性能,參考火焰圖。
壓測頻繁地申請內存空間也耗費了不少性能(參考火焰圖的 __brk),嘗試添加 jemalloc 優化,發現 jemalloc 與 libco 一起用在 Linux 竟然出現死鎖!!!
8. 小結
通過學習其他大神的帖子,走讀源碼,寫測試代碼,終于對協程有了比較清晰的認知。
測試 libco,Centos 功能正常,但 MacOS 下不能成功 Hook 住 mysqlclient 阻塞接口。
libco 是輕量級的,它主要應用于高并發的 IO 密集型場景,所以你看到它綁定了多路復用模型。
雖然測試效果不錯,如果你考慮用 libco 去造一個 mysql 連接池,還有不少工作要做。
libco 很不錯,所以我選擇 golang 🐶。
9. 參考