本來一篇文章就該搞定的。結果要分上下篇了。主要是最近頸椎很不舒服。同時還在做秒殺的需求也挺忙的。 現在不能久坐。看代碼的時間變少了。然后還買了兩本治療頸椎的書。在學着,不過感覺沒啥用。突然心里好害怕。如果頸椎病越來越重。以后的路怎么走。
現在上下班有跑步,然后坐一個小時就起來活動活動。然后在跟着同時們一起去打羽毛球吧。也快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。而不是多個進程綁定同一個端口?
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個請求。看服務處理進程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住。其他進程並沒有被喚醒。
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只回到了
中所以其他進程一直recvfrom =-1
f000_6000
而且由於正在處理
的進程不夠及時。這個消息沒處理。epoll的特性是會一直通知進程來處理。所以其他進程會一直讀自己的unix域。然后就一直recvfrom =-1
f000_6000
如下圖。沒貼全。除了進程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 check CTimeValue m_tvInterval; // Time check interval unsigned int m_dwCount; // Counter for auto re-schedule bool 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; else m_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)對大型系統。統計日志很重要。可以時事了解系統的狀態
2)一定要處理好多進程的關系
3)最后 一定要保護好身體。 身體才是根本啊~~~