本發(fā)明涉及高性能網(wǎng)絡流量處理領域,具體涉及一種高吞吐數(shù)據(jù)流處理方法。
背景技術:
在目前的網(wǎng)絡項目中,經常會進行多層次的架構設計,如分為前端數(shù)據(jù)采集層、數(shù)據(jù)匯聚和處理層、數(shù)據(jù)存儲層等,處于數(shù)據(jù)匯聚和處理層上的服務器通常需要處理幾百個前端的連接,上千兆比特每秒的瞬時流量,并且還存在“浪涌”現(xiàn)象,即流量的峰谷值相差巨大,峰值流量過大導致服務器接收和處理數(shù)據(jù)不及時,甚至宕機,因此對服務器提出了較高的性能要求。為了保證實時、可靠的接收和處理來自前端服務器的數(shù)據(jù)流,目前已有的方案主要是對數(shù)據(jù)匯聚和處理層的服務器進行硬件擴容,利用服務器集群來保證高吞吐數(shù)據(jù)的實時處理,但是這并不能充分發(fā)揮服務器的單機處理能力,造成了資源的浪費。為了提高服務器單機的數(shù)據(jù)接收和處理能力,目前設計架構時的常見的方法有:
(1)接收線程對應客戶端,每個接收線程對應唯一的接收隊列,對應唯一的處理線程:
這種方法適用于每個客戶端不間斷發(fā)送大量數(shù)據(jù)流給匯聚服務器,缺點是單客戶端對應了單一的接收線程和單一的處理線程,不能夠平衡接收線程數(shù)和處理線程數(shù)的比例,當接收數(shù)據(jù)的速度大于處理數(shù)據(jù)的速度時,會造成接收隊列積壓數(shù)據(jù),處理不及時;當接收隊列的速度遠小于處理數(shù)據(jù)的速度時,造成處理線程數(shù)過多,資源浪費;
(2)接收線程對應客戶端,每個接收線程對應唯一的接收隊列,使用處理線程池,加鎖形式讀寫隊列數(shù)據(jù):
這種方法在接收數(shù)據(jù)時,單客戶端對應單一的接收線程和接收隊列,處理數(shù)據(jù)時,采用處理線程池,接收線程加寫鎖對隊列寫入數(shù)據(jù),處理線程加讀鎖和互斥鎖讀出數(shù)據(jù)進行處理。這種方式的缺點是當客戶端處于間歇式的發(fā)送數(shù)據(jù)時,接收線程數(shù)過多,造成資源浪費;另外頻繁的加鎖和解鎖造成額外的開銷,導致性能的降低;
(3)接收線程池,對應若干接收隊列,使用處理線程池,加鎖形式讀寫隊列數(shù)據(jù):
這種方式在接收數(shù)據(jù)時使用了線程池,客戶端不再對應唯一的接收線程,并通過異步的方式快速從系統(tǒng)緩沖區(qū)中讀取數(shù)據(jù),避免了IO阻塞等待;處理時采用了線程池,可以配置接收線程和處理線程的比例,缺點是接收線程池和處理線程池對隊列處于多讀多寫的方式,需要加互斥鎖,造成的系統(tǒng)開銷較大。
技術實現(xiàn)要素:
針對上述已有方法存在的缺點和問題,本發(fā)明公開了一種高吞吐數(shù)據(jù)流處理方法。
為了實現(xiàn)上述目的,本發(fā)明采用以下技術方案:
一種高吞吐數(shù)據(jù)流處理方法,具體步驟包括:
(1)讀入接收層,調度層,業(yè)務層和轉發(fā)層的配置參數(shù);
(2)根據(jù)讀入的配置參數(shù),創(chuàng)建接收層的客戶端狀態(tài)維護線程、監(jiān)聽線程、讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程,業(yè)務層的業(yè)務線程,調度層的調度線程,轉發(fā)層的發(fā)送線程;并將上述不同的線程分別綁定到不同的cpu核心上運行;
(3)如果有客戶端請求連接,則通過監(jiān)聽線程判斷接收的是否是合法的套接字,將合法的套接字加入套接字監(jiān)聽集合并在客戶端狀態(tài)表中記錄該套接字的索引位置,已接收數(shù)據(jù)長度,時間戳和客戶端計數(shù)器;如果套接字監(jiān)聽集合中有套接字狀態(tài)改變,則通過讀寫事件監(jiān)控線程判斷套接字監(jiān)聽集合中狀態(tài)發(fā)生改變的套接字是否產生讀寫事件,并將產生讀事件的套接字放入待接收套接字隊列中;如果待接收套接字隊列不為空,則通過數(shù)據(jù)接收線程從待接收套接字隊列中取出套接字,并將從客戶端狀態(tài)表中獲取的該套接字的完整數(shù)據(jù)包放入數(shù)據(jù)接收隊列,同時更新客戶端狀態(tài)表中該套接字的已接收數(shù)據(jù)長度和時間戳;客戶端狀態(tài)維護線程遍歷客戶端狀態(tài)表中每個套接字的時間戳,如果套接字接收的時間戳超過預設的超時時間,則關閉該套接字,并刪除該套接字在客戶端狀態(tài)表中的數(shù)據(jù)信息;如果數(shù)據(jù)接收隊列不為空,調度線程則從數(shù)據(jù)接收隊列中取出數(shù)據(jù),放入對應的任務隊列;如果任務隊列不為空,業(yè)務處理線程則從自己對應的任務隊列中取出任務數(shù)據(jù)進行處理,并將緩存的處理結果放入結果隊列;如果結果隊列不為空,發(fā)送線程采用輪詢的方式從各個結果隊列中取出數(shù)據(jù)發(fā)送給遠端系統(tǒng),在讀結果隊列時加讀鎖和互斥鎖,完成后解鎖。
進一步地,步驟(1)中,所述參數(shù)包括:接收層可使用的cpu核心數(shù)recv_cpu_num,接收層線程數(shù)recv_thread_num,接收隊列的長度recv_q_len,監(jiān)聽的端口號recv_listen_port,接收數(shù)據(jù)超時時間recv_timeout;調度層可使用的cpu核心數(shù)move_cpu_num,調度層線程數(shù)move_thread_num;業(yè)務層可使用的cpu核心數(shù)bsns_cpu_num,業(yè)務層線程數(shù)bsns_thread_num,任務隊列的長度bsns_task_q_len,結果隊列的長度bsns_result_q_len;轉發(fā)層可使用的cpu核心數(shù)send_cpu_num,轉發(fā)層線程數(shù)send_thread_num,轉發(fā)目的IP及端口send_dst_ip/send_dst_port,響應http報文長send_back_http_len。
進一步地,步驟(2)中,將不同的線程分別綁定到不同的cpu核心上運行包括以下幾種情況:
(2.1)如果recv_cpu_num等于1,那么客戶端狀態(tài)維護線程、監(jiān)聽線程、讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程都綁定于0號核心;
(2.2)如果recv_cpu_num大于1,那么客戶端狀態(tài)維護線程、監(jiān)聽線程綁定于0號核心,讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程綁定于1~recv_cpu_num-1號核心;
(2.3)如果move_cpu_num大于等于1,那么調度線程都綁定于recv_cpu_num~recv_cpu_num+move_cpu_num-1號核心;
(2.4)如果bsns_cpu_num等于1,那么所有業(yè)務線程都綁定于recv_cpu_num+move_cpu_num核心上;
(2.5)如果bsns_cpu_num大于1,那么應急業(yè)務線程綁定于recv_cpu_num+move_cpu_num號核心,其他業(yè)務線程綁定于recv_cpu_num+move_cpu_num+1~recv_cpu_num+move_cpu_num+bsns_cpu_num-1號核心;
(2.6)如果send_cpu_num大于等于1,那么所有的發(fā)送線程都綁定于recv_cpu_num+move_cpu_num+bsns_cpu_num~recv_cpu_num+move_cpu_num+bsns_cpu_num+send_cpu_num-1號核心。
進一步地,步驟(2)中,將不同的線程分別綁定到不同的cpu核心運行的方法,包括:Linux系統(tǒng)下使用API:pthread_setaffinity_np(),Windows系統(tǒng)下使用API:SetThreadAffinityMask()。
進一步地,步驟(3)中,將合法的套接字加入套接字監(jiān)聽集合的同時,在客戶端狀態(tài)表中,將該套接字索引位置設置為1,表示有效,將該套接字已接收數(shù)據(jù)長度重置為0,時間戳設置為當前時間,客戶端計數(shù)器加1。
進一步地,步驟(3)中,所述讀寫事件監(jiān)控線程遍歷套接字監(jiān)聽集合中每個狀態(tài)發(fā)生改變的套接字,判斷套接字是否產生讀事件,如果是,則將該套接字在狀態(tài)表中的時間戳更新為當前時間,并將該套接字放入待接收套接字隊列,如果待接收套接字隊列已滿,則將套接字重新放入套接字監(jiān)聽集合,并記錄待接收套接字隊列滿的日志信息;如果套接字產生的是寫事件,則忽略該事件,將套接字重新放入套接字監(jiān)聽集合;如果是其他非正常事件,則關閉套接字,在客戶端狀態(tài)表中,將該套接字索引位置設置為0,表示失效,將客戶端計數(shù)器減1。
進一步地,步驟(3)中,所述數(shù)據(jù)接收線程從客戶端狀態(tài)表中不斷接收取出的套接字的已接收數(shù)據(jù)長度及保存的半包數(shù)據(jù),后續(xù)接收的數(shù)據(jù)追加在它后面,每達到一個完整的數(shù)據(jù)包,則將其放入數(shù)據(jù)接收隊列;如果套接字數(shù)據(jù)已經讀取完,并且最后的接收的數(shù)據(jù)包不完整,則更新客戶端狀態(tài)表中該套接字已接收數(shù)據(jù)長度為當前接收的半包長度,并將半包數(shù)據(jù)存入客戶端狀態(tài)表;如果最后接收的數(shù)據(jù)包完整,則將接收數(shù)據(jù)長度置為0;最后更新套接字的時間戳為當前時間。
進一步地,步驟(3)中,在客戶端狀態(tài)表中將被關閉的套接字的索引位置置0,客戶端計數(shù)器減1;遍歷完客戶端狀態(tài)表后的客戶端狀態(tài)維護線程進入休眠(Sleep)狀態(tài)。
進一步地,步驟(3)中,所述調度線程編號為i時,負責從編號為i+n*move_thread_num的數(shù)據(jù)接收隊列中取出數(shù)據(jù),放入編號為i+m*move_thread_num的任務隊列;在將數(shù)據(jù)放入任務隊列的過程中,如果該任務隊列已滿,則將m+1,嘗試放入下一個相應的任務隊列,如果該調度線程對應的任務隊列都已滿,則放入應急任務隊列中;0<=n<=move_per_inq,move_per_inq表示每個隊列當前的任務數(shù)量,0<=m<=bsns_thread_num/move_thread_num,每次調度成功則將n和m都加1。
進一步地,步驟(3)中,所述業(yè)務處理線程從任務隊列中取出任務進行處理,直至緩存的處理結果達到閾值數(shù)量或超過某個超時時間,將緩存的處理結果一次性放入結果隊列,等待被發(fā)送,在將結果放入結果隊列時,給結果隊列加寫鎖,完成后解鎖。
本發(fā)明的有益效果如下:
與已公開的方法相比,具有如下優(yōu)點:
1)接收線程數(shù)和處理線程數(shù)比例可根據(jù)業(yè)務需求配置平衡,充分利用系統(tǒng)的CPU和內存資源;
2)不同類型的線程分配在不同cpu核上運行,防止cpu操作密集型的線程干擾IO操作密集型的線程;
3)異步方式對數(shù)據(jù)接收和處理,提升系統(tǒng)整體吞吐率;
4)對接收的數(shù)據(jù)通過無鎖的任務調度方法盡可能快的將其送入業(yè)務處理線程進行處理,減少鎖開銷,提高數(shù)據(jù)處理的實時性。
附圖說明
圖1是本發(fā)明高吞吐數(shù)據(jù)流處理原理框圖。
圖2顯示本發(fā)明客戶端狀態(tài)表結構。
圖3是本發(fā)明異步數(shù)據(jù)接收流程圖。
圖4是本發(fā)明數(shù)據(jù)調度流程圖。
具體實施方式
針對高吞吐數(shù)據(jù)流的實時高效處理問題,本發(fā)明的總體思路是通過異步數(shù)據(jù)接收的方式,使用少量的接收線程應對盡可能多的客戶端數(shù)據(jù)發(fā)送請求,充分利用接收線程的cpu時間;對接收的數(shù)據(jù)通過無鎖的任務調度方法盡可能快的將其送入業(yè)務處理線程進行處理,保證數(shù)據(jù)處理的實時性;另外,由于數(shù)據(jù)接收是一種IO密集型操作,而數(shù)據(jù)處理是一種cpu密集型操作,將兩層分離,分別運行在不同范圍的cpu核上,保證數(shù)據(jù)的接收和處理過程互不干擾,不會出現(xiàn)數(shù)據(jù)處理線程長時間占用cpu,數(shù)據(jù)接收線程得不到系統(tǒng)調度運行的情況。方法的原理圖如圖1,下面介紹本方法的具體實施過程:
(1)讀入配置參數(shù):讀入由用戶設置的各項配置信息,包括接收層可使用的cpu核心數(shù)recv_cpu_num,接收層線程池大小(即接收層線程數(shù))recv_thread_num,接收隊列的長度recv_q_len,監(jiān)聽的端口號recv_listen_port,接收數(shù)據(jù)超時時間recv_timeout;調度層可使用的cpu核心數(shù)move_cpu_num,調度層線程數(shù)move_thread_num;業(yè)務層可使用的cpu核心數(shù)bsns_cpu_num,業(yè)務層線程數(shù)bsns_thread_num,任務隊列的長度bsns_task_q_len,結果隊列的長度bsns_result_q_len,其他與具體業(yè)務相關的參數(shù);轉發(fā)層可使用的cpu核心數(shù)send_cpu_num,轉發(fā)層線程數(shù)send_thread_num,轉發(fā)目的IP及端口send_dst_ip/send_dst_port,響應http報文長send_back_http_len;上述四層參數(shù)的設置可通過實際線上測試估計出;
(2)根據(jù)讀入?yún)?shù),創(chuàng)建接收層的客戶端狀態(tài)維護線程、監(jiān)聽線程、讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程,業(yè)務層的業(yè)務線程,調度層的調度線程,轉發(fā)層的發(fā)送線程;并將上述線程綁定到特定的cpu核心上運行,隔離各層的操作,保證處理的實時性;各線程的核數(shù)分配方法如下:
(2.1)如果recv_cpu_num等于1,那么客戶端狀態(tài)維護線程、監(jiān)聽線程、讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程都綁定于0號核心;
(2.2)如果recv_cpu_num大于1,那么客戶端狀態(tài)維護線程、監(jiān)聽線程綁定于0號核心,讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程綁定于1~recv_cpu_num-1號核心,即讀寫事件監(jiān)控線程、每個數(shù)據(jù)接收線程可以由操作系統(tǒng)調度運行在1~recv_cpu_num-1號任何核心上;
(2.3)如果move_cpu_num大于等于1,那么調度線程都綁定于recv_cpu_num~recv_cpu_num+move_cpu_num-1號核心,即每個調度線程可以由操作系統(tǒng)調度運行在recv_cpu_num~recv_cpu_num+move_cpu_num-1核心上;
(2.4)如果bsns_cpu_num等于1,那么所有業(yè)務線程都綁定于recv_cpu_num+move_cpu_num核心上;
(2.5)如果bsns_cpu_num大于1,那么應急業(yè)務線程綁定于recv_cpu_num+move_cpu_num號核心,其他業(yè)務線程綁定于recv_cpu_num+move_cpu_num+1~recv_cpu_num+move_cpu_num+bsns_cpu_num-1號核心;
(2.6)如果send_cpu_num大于等于1,那么所有的發(fā)送線程都綁定于recv_cpu_num+move_cpu_num+bsns_cpu_num~recv_cpu_num+move_cpu_num+bsns_cpu_num+send_cpu_num-1號核心;
舉例:假設接收層可使用的cpu核心數(shù)recv_cpu_num為3,調度層可使用的cpu核心數(shù)move_cpu_num為2,業(yè)務處理層可使用的cpu核心數(shù)bsns_cpu_num為15,轉發(fā)層可使用的cpu核心數(shù)send_cpu_num為3:
則:客戶端狀態(tài)維護線程、監(jiān)聽線程綁定于0號核心,讀寫事件監(jiān)控線程、數(shù)據(jù)接收線程綁定于1~2號核心;調度線程都綁定于3~4號核心;應急業(yè)務線程綁定于5號核心;其他業(yè)務線程綁定于6~19號核心;所有的發(fā)送線程都綁定于20~22號核心。
綁定線程至cpu的方法,Linux下可使用API:pthread_setaffinity_np(),Windows系統(tǒng)下可使用API:SetThreadAffinityMask()。
(3)如果有客戶端請求連接,轉入步驟(4);如果套接字監(jiān)聽集合中有套接字狀態(tài)改變,產生讀寫事件,轉入步驟(5);如果待接收套接字隊列不為空,轉入步驟(6);如果客戶端狀態(tài)維護線程阻塞時間超過預設超時間,轉入步驟(8);如果數(shù)據(jù)接收隊列不為空,轉入步驟(9);如果任務隊列不為空,轉入步驟(11);如果結果隊列不為空,轉入步驟(12);
(4)監(jiān)聽線程判斷accept的是否是合法的套接字(值大于0),不合法則轉入步驟(3);否則,將該套接字加入套接字監(jiān)聽結合,實現(xiàn)時采用了epoll庫,調用的API為operate_fd_epoll(),參數(shù)模式為“EPOLLIN|EPOLLONESHOT”,同時在客戶端狀態(tài)表(圖2所示)中,設置該套接字索引位置設置為1,表示有效,重置狀態(tài)表中該套接字已接收數(shù)據(jù)長度為0,時間戳設置為當前時間,客戶端計數(shù)器加1;轉入步驟(3);
(5)讀寫事件監(jiān)控線程遍歷套接字監(jiān)聽集合中每個狀態(tài)發(fā)生改變的套接字,判斷套接字是否產生讀事件,如果是,則將該套接字在狀態(tài)表中的時間戳更新為當前時間,并將該套接字放入待接收套接字隊列,如果隊列已滿,則將套接字重新放入套接字監(jiān)聽集合,并記錄待接收套接字隊列滿的日志信息;如果套接字產生的是寫事件,則忽略該事件,將套接字重新放入套接字監(jiān)聽集合;如果是其他非正常事件,則關閉套接字,在客戶端狀態(tài)表中,將該套接字索引位置設置為0,表示失效,將客戶端計數(shù)器減1,轉入步驟(3);
(6)數(shù)據(jù)接收線程從待接收套接字隊列中取出套接字,從客戶端狀態(tài)表中該套接字的已接收數(shù)據(jù)長度及保存的半包數(shù)據(jù),后續(xù)接收的數(shù)據(jù)追加在它后面;轉入步驟(7);
(7)不斷接收套接字數(shù)據(jù),每達到一個完整的數(shù)據(jù)包,則將其放入數(shù)據(jù)接收隊列;如果套接字數(shù)據(jù)已經讀取完,并且最后的接收的數(shù)據(jù)包不完整,則更新客戶端狀態(tài)表中該套接字已接收數(shù)據(jù)長度為當前接收的半包長度,并將半包數(shù)據(jù)存入狀態(tài)表;如果最后接收的數(shù)據(jù)包完整,則將接收數(shù)據(jù)長度置為0;最后更新套接字的時間戳為當前時間,返回步驟(3);
異步數(shù)據(jù)接收的流程圖可以參考圖3,recv_len表示當前數(shù)據(jù)包已接收的長度,recv_buffer表示當前已接收的數(shù)據(jù),new_recv_len表示一次從套接字中接收的數(shù)據(jù)長度。
(8)客戶端狀態(tài)維護線程遍歷狀態(tài)表中每個套接字的時間戳,判斷與當前時間間隔是否超過30分鐘,如果超過,則判定該套接字已經意外斷開,關閉該套接字,并在狀態(tài)表中將該套接字的索引位置置0,客戶端計數(shù)器減1;遍歷完則線程進入Sleep狀態(tài),轉入步驟(3);
(9)編號為i的調度線程,負責從編號為i+n*move_thread_num(0<=n<=move_per_inq)的數(shù)據(jù)接收隊列中取出數(shù)據(jù),放入編號為i+m*move_thread_num(0<=m<=bsns_thread_num/move_thread_num)的任務隊列;在將數(shù)據(jù)放入任務隊列的過程中,如果該任務隊列已滿,則將m+1,嘗試放入下一個相應的任務隊列,如果該調度線程對應的任務隊列都已滿,則放入應急任務隊列中;每次調度成功則將n和m都加1;由于對數(shù)據(jù)接收隊列和任務隊列的操作都是一讀一寫模式,因此避免了隊列讀寫時加鎖的開銷,調度完數(shù)據(jù)轉入步驟(3);
調度方法的流程圖如圖4所示,舉例如下:
假設數(shù)據(jù)接收隊列in_q編號0~9,任務隊列dst_q編號0-4,每個調度線程最多處理3個接收隊列,即move_per_inq取值3,共需要4個調度線程,下面用move_thead表示調度線程:
move_thread[0]負責將in_q[0]、in_q[4]、in_q[8]中的數(shù)據(jù)拷貝至dst_q[0]、dst_q[4],每次都是一次性拷貝盡量多的數(shù)據(jù),如in_q[0]中目前有10個數(shù)據(jù)包,dst_q[0]中目前空余容量為5,則一次性從in_q中取出5個元素拷貝至dst_q[0];如果在拷貝過程中,dst_q[0]以滿,則拷貝至dst[1],dst[1]也滿,則將數(shù)據(jù)拷貝至應急隊列中;每次拷貝完成,則拷貝下一個源隊列in_q至下一個目的隊列dst_q中;
move_thread[1]負責將in_q[1]、in_q[5]、in_q[9]中的數(shù)據(jù)拷貝至dst_q[1]中;
move_thread[2]負責將in_q[2]、in_q[6]中的數(shù)據(jù)拷貝至dst_q[2]中;
move_thread[3]負責將in_q[3]、in_q[7]中的數(shù)據(jù)拷貝至dst_q[3]中。
(10)業(yè)務處理線程取出自己對應任務隊列中的任務數(shù)據(jù)進行處理,并緩存處理結果;轉入步驟(11);
(11)如果任務隊列不為空,從任務隊列中取出任務進行處理,直至緩存的結果達到閾值數(shù)量(4000)或超過某個超時時間(30s),將緩存的結果一次性放入結果隊列,等待被發(fā)送,在將結果放入隊列時,給隊列加寫鎖,完成后解鎖;如果任務隊列為空,則表示沒有待處理的任務,轉入步驟(3)進入等待狀態(tài);
(12)發(fā)送線程采用輪詢的方式從各個結果隊列中取出數(shù)據(jù)發(fā)送給遠端系統(tǒng),在讀結果隊列時加讀鎖和互斥鎖,完成后解鎖。