Curve 是雲原生計算基金會 (CNCF) Sandbox 項目,是網易數帆發起開源的高性能、易運維、雲原生的分佈式存儲系統。
爲了讓大家更容易使用以及瞭解 Curve,我們期望接下來通過系列應用實踐文章,以專題的形式向大家展示 Curve。
本篇文章是Curve塊存儲應用實踐的第一篇,該系列文章包括:
- Curve塊存儲應用實踐一部曲之iSCSI
- Curve塊存儲應用實踐二部曲之nbd
- Curve塊存儲應用實踐三部曲之雲主機
- Curve塊存儲應用實踐四部曲之雲原生數據庫
- Curve塊存儲應用實踐五部曲之性能調優
iSCSI 及 tgt 簡介
tgt 是一個開源 iSCSI 服務器,詳情請見 tgt githu[1]。我們在開發 Curve 塊設備服務器時,想讓更多的系統能夠使用 Curve 塊設備,而不僅僅是 Linux 系統,iSCSI 協議是一個廣泛使用的塊設備協議,我們想修改 tgt 以便讓 Curve 提供 iSCSI 服務。
Curve 塊存儲
爲tgt提供了訪問 Curve 的驅動,詳見部署網絡高性能版本tgt[2] , 文檔裏有操作步驟,這樣用戶就可以在任何支持 iSCSI 的操作系統上使用 Curve 塊設備存儲,例如Windows。
Curve 在初步使用 tgt 時也遇到一些問題:
我們觀察到原版 tgt 使用單一主線程 epoll 來處理 iSCSI 命令,還包括管理平面的 unix domian socket 也在這個主線程裏。
在10 Gbit/s 網絡上甚至更快的網絡上,單線程(也即單cpu)處理 iSCSI 命令的速度已經跟不上需求了,一個線程對付多個target的情況下,多個iSCSI Initiator的請求速度稍微高一點,這個單線程的cpu使用率就100%忙碌。
所以本文的重點就是介紹tgt的性能優化。同時社區用戶使用過程中還遇到了nebd服務的單點和性能問題,社區用戶對此也進行了優化,詳情可參考創雲融達基於 Curve 的智慧稅務場景實踐。
Curve 對 tgt 的性能優化實踐
1. 使用多個線程做 epoll
實現多個event loop線程,每個線程負責一定數量的socket connection上的iSCSI命令處理。這樣就能發揮多cpu的處理能力。
2. 爲每個 target 創建一個 epoll 線程
爲了避免多個target共享一個epoll時依然可能出現超過單個cpu處理能力的問題,我們爲每一個 target設置了一個epoll線程。target epoll的cpu使用由OS負責調度,這樣在各target上可以 實現公平的cpu使用。當然如果網絡速度再快,依然會出現單個epoll線程處理不過來一個iSCSI target上的請求,但是目前這個方案依然是我們能做的最好方案。
3. 管理平面
管理平面保持了與原始tgt的兼容性。從命令行使用方面來說,沒有任何區別,沒有任何修改。管理平面在程序的主線程上提供服務,主線程也是一個epoll loop線程,這與原始的tgt沒有區別,它負責target,lun,login/logout,discover,session, connection等的管理。當Intiator連接到iSCSI 服務器時,總是先被管理平面線程所服務,如果該connection最後需要創建session去訪問某個target,那麼該connection會被遷移到對應的target的epoll線程上去。
4. 數據結構的鎖
爲每一個target提供一個mutex,當target epoll線程在運行時,這把鎖是被該線程鎖住的,這樣該線程可以任意結束一個sesssion或connection,當線程進入epoll_wait時,這把鎖是釋放了的,epoll_wait返回時又會鎖住這把鎖。我們修改了相關代碼,讓這個epoll線程不用遍歷target list,只存取它服務的target相關結構,這樣我們不需要target列表鎖。管理面也會增加、刪除一個session或者connection時,也需要鎖住這把target鎖。所以管理面和target epoll線程使用這個mutex來互斥,這樣就可以安全地訪問對應target上的session和connection了。
5. connection 建立 session
當login_finish成功時,login_finish有時候會創建session(如果沒有session存在)。login_finish在connection結構的字段migrate_to裏設置目標iSCSItarget。
6. 什麼時候做 connection 遷移
當調用返回到iscsi_tcp_event_handler時,因爲login_finish設置了migrate_to目標target,iscsi_tcp_event_handler就鎖住目標iscsi target結構,並把該connection的fd插入到目標target的evloop 裏面,完成遷移。
7. 設置 pthread name
設置各target event loop的線程在top中的名爲tgt/n, n爲target id,這樣容易用top之類的工具觀察哪一個target佔用的cpu高。
8. 舉個例子
假如MGMT要刪除一個target,下面的代碼說明了流程:
/* called by mgmt */ tgtadm_err tgt_target_destroy(int lld_no, int tid, int force) { struct target *target; struct acl_entry *acl, *tmp; struct iqn_acl_entry *iqn_acl, *tmp1; struct scsi_lu *lu; tgtadm_err adm_err; eprintf("target destroy\n"); /* * 這裏因爲控制面是單線程的,而且SCSI IO線程不會刪除target, * 所以我們找target的時候並不需要鎖 */ target = target_lookup(tid); if (!target) return TGTADM_NO_TARGET; /* * 這裏要鎖住target,因爲我們要刪除數據結構,所以不能和iscsi io * 線程一起共享,必須在scsi 線程釋放了鎖時進行 */ target_lock(target); if (!force && !list_empty(&target->it_nexus_list)) { eprintf("target %d still has it nexus\n", tid); target_unlock(target); return TGTADM_TARGET_ACTIVE; } … /* 以上步驟刪除了所有資源 ,可以釋放鎖了 */ target_unlock(target); if (target->evloop != main_evloop) { /* 通知target上的evloop停止,並等待evloop 線程退出 */ tgt_event_stop(target->evloop); if (target->ev_td != 0) pthread_join(target->ev_td, NULL); /* 下面把evloop的資源刪除乾淨 */ work_timer_stop(target->evloop); lld_fini_evloop(target->evloop); tgt_destroy_evloop(target->evloop); }
性能優化結果
我們爲tgt配置了3塊盤,一塊 Curve 塊存儲卷,兩塊本地盤
<target iqn.2019-04.com.example:curve.img01> backing-store cbd:pool//iscsi_test_ bs-type curve </target> <target iqn.2019-04.com.example:local.img01> backing-store /dev/sde </target><target iqn.2019-04.com.example:local.img02> backing-store /dev/sdc </target>
使用本機登錄iscsi iscsiadm --mode node --portal 127.0.0.1:3260 --login
爲fio設置存取這些 iSCSI 的塊設備,使用:
[global] rw=randread direct=1 iodepth=128 ioengine=aio bsrange=16k-16k runtime=60 group_reporting [disk01] filename=/dev/sdx [disk02] filename=/dev/sdy size=10G [disk03] filename=/dev/sdz size=10G
測試結果如下:
下面是未經優化的fio成績,IOPS 38.8K
下面是經過多線程優化的fio成績,IOPS 60.9K
<原創作者:徐逸鋒,Curve PMC>
參考[1]:https://github.com/fujita/tgt