大型分布式C++框架《四:netio之请求包中转站 上》
生活随笔
收集整理的這篇文章主要介紹了
大型分布式C++框架《四:netio之请求包中转站 上》
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
本來一篇文章就該搞定的。結果要分上下篇了。主要是最近頸椎很不舒服。同時還在做秒殺的需求也挺忙的。 現在不能久坐??创a的時間變少了。然后還買了兩本治療頸椎的書。在學著,不過感覺沒啥用。突然心里好害怕。如果頸椎病越來越重。以后的路怎么走。
? ? ? ? ? ?現在上下班有跑步,然后坐一個小時就起來活動活動。然后在跟著同時們一起去打羽毛球吧。也快30的人了。現在發覺身體才是真的。其他都沒什么意思。兄弟們也得注意~~
廢話不多說。下面介紹下netio。
? ? ? ? ? netio在系統中主要是一個分包的作用。netio本事沒有任何的業務處理。拿到包以后進行簡單的處理。再根據請求的命令字發送到對應的業務處理進程去。
?
?
一、多進程下的socket epoll以及“驚群現象”
2.1 多進程下是監聽socket的方式
1)比如我們想創建三個進程同時處理一個端口下到來的請求。
2)父進程先創建socket。然后再listen。注意這個時候父進程frok。 2個進程出來。加上父進程就是3個進程 3)每個進程單獨創建字節epoll_create和epoll_wait. 并把socket放到epoll_wait里以上是平臺多進程下監聽同一個端口的方式。我們下面探究下為什么要這么做 2.1.1、為什么要fork出來的子進程來繼承父進程的socket。而不是多個進程綁定同一個端口? 首先如果一個端口正在被使用,無論是TIME_WAIT、CLOSE_WAIT、還是ESTABLISHED狀態。 這個端口都不能被復用,這里面自然也是包括不能被用來LISTEN(監聽)。所以在三個進程里分別listen和bind端口肯定會失敗的 2.1.2 、為什么不能使用SO_REUSEADDR來是多個進程監聽同一個端口? 首先我們來看下SO_REUSEADDR的用途 服務端重啟的時候會進入到TIME_WAIT的狀態。這個時候我們在bind的端口會失敗的。但是我們不可能等TIME_WAIT狀態過去在重啟服務 因為TIME_WAIT可能會在一分鐘以上。這個時候我們設置為SO_REUSEADDR就是使得端口處在TIME_WAIT時候,可以復用監聽。 注意SO_REUSEADDR只是在TIME_WAIT 重啟服務的時候有用。如果你是多個進程要bind同一個端口。且IP相同。那么即使你設置了SO_REUSEADDR也會失敗 因為SO_REUSEADDR允許在同一端口上啟動同一服務器的多個實例,只要每個實例捆綁一個不同的本地IP地址即可。對于TCP,我們根本不可能啟動捆綁相同IP地址和相同端口號的多個服務器。 2.1.3、TIME_WAIT的作用。為什么要TIME_WAIT的?
因為TCP實現必須可靠地終止連接的兩個方向(全雙工關閉), 一方必須進入 TIME_WAIT 狀態,因為可能面臨重發最終ACK的情形。 否則的會發送RST 2.1.4、多進行下實現監聽同一個端口的原因 因為創建子進程的時候,復制了一份socket資源給子進程。其實可以這么理解。其實只有父進程一個socket綁定了端口。其他子進程只是使用的是復制的socket資源 2.1.5、epoll放到fork之后會怎么樣?
netio起5個進程 james 2356 1 0 08:22 pts/0 00:00:00 ./netio netio_config.xml james 2357 2356 0 08:22 pts/0 00:00:00 ./netio netio_config.xml james 2358 2356 0 08:22 pts/0 00:00:00 ./netio netio_config.xml james 2359 2356 0 08:22 pts/0 00:00:00 ./netio netio_config.xml james 2360 2356 0 08:22 pts/0 00:00:00 ./netio netio_config.xml 我們先做幾個實驗然后再具體分析 a)實驗一 正常請求。我們慢慢的按順序發送十個請求。每個請求都是新的請求。 我們看下netio處理的進程pid ?發現每次都是2358 開始我以為會這五個請求來競爭來取socket接收到的請求化。那么每次處理的請求的子進程應該不一樣才對。但是每次都是同一個請求 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2358 b)實驗二 我們先并發2個請求??捶仗幚磉M程pid還是2358 after epoll_wait pid:2358 after epoll_wait pid:2358
這個時候我們客戶端fork8個進程并發請求服務。發現2357和2358開始交替處理
after epoll_wait pid:2358 after epoll_wait pid:2358 after epoll_wait pid:2357 after epoll_wait pid:2358 after epoll_wait pid:2357 after epoll_wait pid:2358 after epoll_wait pid:2357 after epoll_wait pid:2358 c)實驗三 我們在epoll_wait后面.recv之前 ? ?加上sleep(100000) 然后發送一個請求。發現每個進程被喚醒以后 但是因為sleep阻塞了。 然后會接著喚醒別的進程來處理。每次喚醒都會被阻塞。一直到5個進程全部被阻塞 after epoll_wait pid:2358 after epoll_wait pid:2357 after epoll_wait pid:2359 after epoll_wait pid:2356 after epoll_wait pid:2360 d)實驗四 我們在epoll_wait后面recv 后面 加上是sleep(100000) 然后發送一個請求。發現有一個進程recv處理完以后。sleep住。其他進程并沒有被喚醒。| 1 | after epoll_wait pid:2358 |
當我們并發兩個請求的時候。發現喚醒了兩個進程
after epoll_wait pid:2357 after epoll_wait pid:2359 四個實驗已經做完現在我們來具體分析下原因 1)實驗一中為什么 ?每次只有一個進程來處理。 首先我們三個進程都epoll_wait同一個fd。按理來說這個時候其實應該喚醒三個進程都來處理的。但是每次都只有一個進程來處理。如果是進程競爭處理的話。別的子進程應該也有機會來處理的。但是沒有。這就是我們所謂的“驚群”現象。但是并沒有發生。 查了下資料發現。內核采用的不是競爭。而是分配策略。在有多個子進程epoll_wait 同一個fd的時候。會選擇一個子進程來處理這個消息。 并不會喚醒其他子進程。 2)實驗二中 并發增大的時候 為什么會開始有多個子進程來處理。 其實這里做的很有意思。內核輪流喚醒監聽fd的子進程。如果子進程很快處理完。那么就一直讓這個子進程來處理fd.但是如果子進程處理不完。速度沒那么快。會接著喚醒別的子進程來處理這個fd. 即fd事件到達的時候。內核會先去看下A進程是否繁忙。如果不繁忙 。則讓這個A進程一直處理。如果發現A進程繁忙中。會去查看進程B是否繁忙。如果不繁忙則B處理 ?后面的子進程以此類推 所以我們看到 并發請求增大的時候 ?開始有多個子進程來處理了 3)實驗三、5個進程為什么都被喚醒了? 其實就是上面說的。被sleep住了。我們認為進程就是繁忙狀態中。會依次通知其他進程來處理。 4)實驗四 ?為什么只有一個進程被喚醒處理 我們在sleep放在recv之后。發現只有一個進程被喚醒。 我們可以任務進程已經接受并處理了任務。所有不需要再通知其他進程了 這里我們小結下: epoll放在fork之后這種方式不會引起驚群現象。 會輪詢選擇其中一個子進程來處理。如果子進程來不及處理。則會通知另外一個子進程來處理。 但是以上結論是做實驗和查資料得來的。并沒有看內核源碼。所有如果有看過內核源碼的同學。希望能指點下。 2.1.6、epoll放到fork之前會怎么樣? 把epoll放到fork 之前。當發送一個請求的時候 ?發現也是只喚醒了一個進程來處理。 這里其實跟epoll放到fork之后是一樣的。 ? 但這里有個很蛋痛的地方 。 我們系統用到了unix域來做消息通知。當container處理完消息。會發送uinx域來通知neito來處理回包。 但是回包的時候。不知道為什么 ?5個進程都被喚醒了來被處理。最后有三個進程被主動結束了。 ? 下面是我自己的理解 a) 首先 是進程id為 6000 的netio處理的數據。 當container回包實際數據只會通知 f000_6000的uinx域中 如下圖。有5個進程。就有5個uinx域 b) 但是由于多進程公用一個epoll。其他進程也被喚醒了。然后判斷發現這個fd是uxin域的類型。 然后就會去不停的讀取自己對應的unix域文件。 但是其實沒有消息的。container只回到了 f000_6000 中所以其他進程一直recvfrom =-1 而且由于正在處理 f000_6000 的進程不夠及時。這個消息沒處理。epoll的特性是會一直通知進程來處理。所以其他進程會一直讀自己的unix域。然后就一直recvfrom =-1 如下圖。沒貼全。除了進程6000其他進程打印了一堆這樣的信息 c) 最后我們讀進程6000從的uinx域中讀到數據后。 其他進程剛好這個時候拿到fd是被處理過的。這個時候再來處理這個fd就是未定義的。 而我們對未定義的fd會直接stop進程。所以最后三個進程被主動關閉了 這里我們小結一下 因為沒有看內核代碼 所有對這種情況只有靠實驗和猜了。。。。。跪求大神指導 1、首先針對TCP端口內核應該是做了特殊處理。所以epoll在fork前還是后。如果處理及時。應該都是只有一個進程被喚醒。來處理。處理不及時會依次喚醒別的進程。并不會造成驚群現象(就是那種臨界資源多個進程來搶這個包。最后只有一個進程能搶到包。但是做實驗發現好像并不是競爭的關系) 2、但是針對unix域的fd。公用一個epoll沒有特殊處理。就會造成驚群現象。并且多個進程都能拿到這個fd來處理。二、netio之定時器
?
先看下圖。netio定時器所處在的位置。 由于epoll_wait了10毫秒。無論是否有請求觸發。 每隔10毫秒都會輪詢一次。這樣可以防止當container通知netio的時候。消息丟失而導致netio不能處理的情況 定時器是一個比較重要的概念。每個服務進程都會有個定時器來處理定時任務。這里介紹下netio的定時器. ?這里檢查定時時間事件做兩件事。一個是找出已經到達的時間事件。并執行子類的具體處理函數。第二個是給自動時間事件續期。 netio初始化的時候。會注冊一個60秒的循環時間事件。即每60秒會執行一次時間事件。這個時間事件有以下幾個動作。1)清除超時的socket ? 2)查詢本地命令字列表 3)定時輸出netio的統計信息 3.1 ? 定時器的數據結構 netio的定時器數據結構是最小堆。即最近的定時任務是在最小堆上 a)申請了65個大小的二維數組 const uint32_t DEFAULT_QUEUE_LEN = 1 + 64;m_pNodeHeap = new CNode*[DEFAULT_QUEUE_LEN]; 這里為什么申請1+64.其實只有64個可用。? 其中的一個指針是用來輔助最小堆算法的。 即m_pNodeHeap[0]?=?NULL; ? 是一直指向空的。 最小堆的最小值 ?是m_pNodeHeap【1】. b) CNode成員變量的意義 struct CNode {CNode(ITimerHandler *pTimerHandler = NULL, int iTimerID = 0) : m_pTimerHandler(pTimerHandler), m_iTimerID(iTimerID), m_dwCount(0) , bEnable(true){ }ITimerHandler *m_pTimerHandler;int m_iTimerID;CTimeValue m_tvExpired; // TimeValue for first checkCTimeValue m_tvInterval; // Time check intervalunsigned int m_dwCount; // Counter for auto re-schedulebool bEnable; }; m_pTimerHandler 主要是用來保存父類指針。當時間事件觸發的時候。通過父類指針找到繼承類。來處理具體的 時間事件 m_tvExpired 記錄過期時間。比如一個事件過期時間是10秒。那么m_tvExpired就是存的當前時間+10秒這個值。每次比較的時候。拿最小堆的的這個值跟當前時間比對。如果當前時間小于m_tvExpired說明 沒有任何時間時間被觸發。如果當前時間大于這個值。則認為需要處理時間事件 m_dwCount 這個是用來設置自動過期時間的次數。比如我們有一個時間事件。我們希望它執行三次。每次的間隔以為1分鐘。那么這個值設置為3. 當第一次到達時間時間的時候。我們發現這個值大于0.則對m_tvExpired賦值當前時間+1分鐘 重新進入最小堆。然后m_dwCount減一。 下次依然是這樣處理。直到m_dwCount這個值到0.我們就認為不需要再自動給這個時間設置定時任務。 bEnable 這個值是用來判斷這個事件事件是否還有效。這里的做法很有意思。當一個時間事件執行完。或者不需要的時候。我們先是幫它設置為false.等下次check時間最堆的時候。如果發現這個時間事件是無效的。這個時候在delete m_tvInterval 這個是時間事件的間隔時間。比如我的時間事件是每10秒執行一次。則這個值就設置為10秒 m_iTimerID 這個是時間事件的唯一ID這個值是自增的。每來一個新時間事件。m_iTimerID都會+1. 因為初始化的時候。這個值為1了。所以最開始的時間事件m_iTimerID的值為2?
3.2 ? 定時清理無效連接 a)首先如果有客戶端connet進來。 netio會把這個connet的fd保存下來 還有到來的時間。 m_mapTcpHandle[iTcpHandle]?=?(int)time(NULL); b ) 每次接受到數據 都會更新這個時間 c ) netio有個配置文件。 我們一般是設置為10秒。每次檢查定時事件的時候。都會去檢查m_mapTcpHandle。 用當前時間 跟?m_mapTcpHandle里面保存的時間比較。當發現超過10秒的時候 我們認為這個連接時無效的。然后會把這個fd關閉。并刪除 d) 所以如果 ?要保持長連接的話。 ?需要客戶端不停的發送心跳包。來更新這個時間 3.3 ?定時統計netio的統計信息 這有個很有意思的地方。 在初始化的時候。我們已經注冊了一個每60秒執行一次輸出的netio狀態的時間事件 ? 前面我們說了m_dwCount這個值用來控制自動時間事件的次數。我們每一分鐘會輸出netio的狀態信息。 這就是固定的時間事件。我們入參dwCount設置0.那么系統就認為你這個時間事件是需要無限循環的。 就設置了一個最大值 (unsigned int )-1。 這個值算出來是4294967295。? 我們以每分鐘一次來計算。差不多要8000多年才能把這個m_dwCount減為0~~~~~ if (dwCount > 0)m_l_pNode->m_dwCount = dwCount;elsem_l_pNode->m_dwCount = (unsigned int)-1; 當輸出完狀態信息后。會把netio的一些狀態初始化為0.因為我們的狀態輸出是統計一分鐘的狀態。比如這一分鐘的請求包個數。比率。丟包個數等。 3.4 ?定時請求命令字 這里還是比較重要的是。比如我們新加了一個服務。如果不重啟netio是不知道的。但是我們會沒隔一分鐘去請求所有的命令字。 可以發現有心的服務加了進來。三、netio 之日志分析
?
4.1、?netio_debug.log日志分析 下面分析下簡單netio的日志。這里就大概介紹了 。不具體在介紹netio的值了 。 多看看就知道是什么意思的。? 還是請求道container. ?container發現參數校驗不對直接把包丟回給netio的回包隊列 如下圖 a)192.168.254.128:58638 ? ?當請求到來的時候會打印請求的IP和端口 b)Handle?=?00700008 ??但是這個socket不是原生的。是經過處理的。 c)ConnNum?=?1 ?當前有多個連接數 d)?Timestamp?=?1460881087 ?請求到了的時間戳 如下圖 a) ?SendMsgq?REQUEST?START ? ? ? ? ? 這里是把內容丟到消息隊列里 b)?_NotifyNext?REQUEST?START ? ? ? ? ?這里是通知container的uinx域。有消息丟到了你的消息隊列里 c)?OnRecvFrom?REQUEST?START ? ? ? ? 這里是接收到了container發來的uinx域 。告訴netio我已經處理完。丟到你的回包消息隊列里去了。你趕緊去處理吧 d)?OnEventFire?request:0 ? ? ? ? ? ? ? ? ? ? 從消息隊列里拿到數據并開始處理。 ?回給客戶端包 e)OnClose?REQUEST?START ? ? ? ? ? ? ? ?客戶端發送close socket信號。 ?服務端接收后。關閉socket 4.2 ?netio_perform.log 日志分析 以下都是一份鐘的統計數據 PkgRecv ? ? ? ? ? ? ? ? ? ? ?收到的包 PkgSent ? ? ? ? ? ? ? ? ? ? ?發送出去的包 ErrPkgSent ? ? ? ? ? ? ? ? 錯誤的包。 PkgPushFail ? ? ? ? ? ? ? 這個暫時沒用到 PkgSendFail ? ? ? ? ? ? ? 這個是netio ?包發送的時候 。發不出去的個數 BytesRecv ? ? ? ? ? ? ? ? ? 收到的字節數 BytesSent ? ? ? ? ? ? ? ? ? 發送出去的字節數 MaxConn ? ? ? ? ? ? ? ? ? 最大連接數。這個值不是一份內的最大值。是從開始到輸出統計是。最高的同時連接數據 TcpConnTimeout ? ? ? 因為超時。netio自動關閉的TCP連接。 Cmd[0x20630001] ? ? ?是netio從回包隊列中拿到。命令字 Count[15] ? ? ? ? ? ? ? ? ? 該命令字一分鐘內總共拿到的回包總數 AverageTime[0]? ? ? ? ?每個包的平均處理時間。 這里是拿這15個包從netio-container-netio這期間的總時間 除以 15得到的平均時間 ?單位是毫秒 MaxTime[1] ? ? ? ? ? ? ? ?這15個包中耗時最長的一個包。所耗時間 AverageRspLen[89] ? ?平均每個包回給客戶端的字節數 MaxRspLen[89] ? ? ? ? ? 最大的一個回包字節數 Ratio[100] ? ? ? ? ? ? ? ? ? ?這里先會拿到一個一分鐘內netio接受包的總個數?(這里指客戶端來的請求包) ?。然后用用0x20630001命令字的個數來除以總包數再乘以100。得到0x20630001在這一分鐘內。所占處理包的比重。 后面接著的一串是 命令字0x20630001的分布在不同相應時間的個數 最后一天是正對一分鐘所有命令字包的統計?
?
最后:
1)對大型系統。統計日志很重要??梢詴r事了解系統的狀態
2)一定要處理好多進程的關系
3)最后 一定要保護好身體。 ? 身體才是根本啊~~~
總結
以上是生活随笔為你收集整理的大型分布式C++框架《四:netio之请求包中转站 上》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何不编程,采集网站评论信息?(视频教程
- 下一篇: MVC与MVVM框架