天天看點

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程序下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

本來一篇文章就該搞定的。結果要分上下篇了。主要是最近頸椎很不舒服。同時還在做秒殺的需求也挺忙的。 現在不能久坐。看代碼的時間變少了。然後還買了兩本治療頸椎的書。在學着,不過感覺沒啥用。突然心裡好害怕。如果頸椎病越來越重。以後的路怎麼走。

           現在上下班有跑步,然後坐一個小時就起來活動活動。然後在跟着同時們一起去打羽毛球吧。也快30的人了。現在發覺身體才是真的。其他都沒什麼意思。兄弟們也得注意~~

廢話不多說。下面介紹下netio。

          netio在系統中主要是一個分包的作用。netio本事沒有任何的業務處理。拿到包以後進行簡單的處理。再根據請求的指令字發送到對應的業務處理程序去。

2.1 多程序下是監聽socket的方式

1)比如我們想建立三個程序同時處理一個端口下到來的請求。

2)父程序先建立socket。然後再listen。注意這個時候父程序frok。 2個程序出來。加上父程序就是3個程序

3)每個程序單獨建立位元組epoll_create和epoll_wait. 并把socket放到epoll_wait裡

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

以上是平台多程序下監聽同一個端口的方式。我們下面探究下為什麼要這麼做

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個程序

我們先做幾個實驗然後再具體分析

a)實驗一

正常請求。我們慢慢的按順序發送十個請求。每個請求都是新的請求。

我們看下netio處理的程序pid  發現每次都是2358

開始我以為會這五個請求來競争來取socket接收到的請求化。那麼每次處理的請求的子程序應該不一樣才對。但是每次都是同一個請求

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:
大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

b)實驗二

 我們先并發2個請求。看服務處理程序pid還是2358

這個時候我們用戶端fork8個程序并發請求服務。發現2357和2358開始交替處理

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:
大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

c)實驗三

我們在epoll_wait後面.recv之前    加上sleep(100000)

然後發送一個請求。發現每個程序被喚醒以後 但是因為sleep阻塞了。 然後會接着喚醒别的程序來處理。每次喚醒都會被阻塞。一直到5個程序全部被阻塞

d)實驗四

我們在epoll_wait後面recv 後面 加上是sleep(100000)

然後發送一個請求。發現有一個程序recv處理完以後。sleep住。其他程序并沒有被喚醒。

<code>after epoll_wait pid:2358</code>

當我們并發兩個請求的時候。發現喚醒了兩個程序

四個實驗已經做完現在我們來具體分析下原因

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之前會怎麼樣?

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

把epoll放到fork 之前。當發送一個請求的時候  發現也是隻喚醒了一個程序來處理。

這裡其實跟epoll放到fork之後是一樣的。

但這裡有個很蛋痛的地方 。

我們系統用到了unix域來做消息通知。當container處理完消息。會發送uinx域來通知neito來處理回包。

但是回包的時候。不知道為什麼  5個程序都被喚醒了來被處理。最後有三個程序被主動結束了。

下面是我自己的了解

a)

首先

是程序id為

6000

的netio處理的資料。

當container回包實際資料隻會通知

f000_6000的uinx域中

如下圖。有5個程序。就有5個uinx域

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

b)

但是由于多程序公用一個epoll。其他程序也被喚醒了。然後判斷發現這個fd是uxin域的類型。

然後就會去不停的讀取自己對應的unix域檔案。

但是其實沒有消息的。container隻回到了

f000_6000

中是以其他程序一直recvfrom =-1

而且由于正在處理

的程序不夠及時。這個消息沒處理。epoll的特性是會一直通知程序來處理。是以其他程序會一直讀自己的unix域。然後就一直recvfrom =-1

如下圖。沒貼全。除了程序6000其他程序列印了一堆這樣的資訊

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

c)

最後我們讀程序6000從的uinx域中讀到資料後。

其他程序剛好這個時候拿到fd是被處理過的。這個時候再來處理這個fd就是未定義的。

而我們對未定義的fd會直接stop程序。是以最後三個程序被主動關閉了

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

這裡我們小結一下

因為沒有看核心代碼 所有對這種情況隻有靠實驗和猜了。。。。。跪求大神指導

1、首先針對TCP端口核心應該是做了特殊處理。是以epoll在fork前還是後。如果處理及時。應該都是隻有一個程序被喚醒。來處理。處理不及時會依次喚醒别的程序。并不會造成驚群現象(就是那種臨界資源多個程序來搶這個包。最後隻有一個程序能搶到包。但是做實驗發現好像并不是競争的關系)

2、但是針對unix域的fd。公用一個epoll沒有特殊處理。就會造成驚群現象。并且多個程序都能拿到這個fd來處理。

先看下圖。netio定時器所處在的位置。

由于epoll_wait了10毫秒。無論是否有請求觸發。

每隔10毫秒都會輪詢一次。這樣可以防止當container通知netio的時候。消息丢失而導緻netio不能處理的情況

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

        定時器是一個比較重要的概念。每個服務程序都會有個定時器來處理定時任務。這裡介紹下netio的定時器.  這裡檢查定時時間事件做兩件事。一個是找出已經到達的時間事件。并執行子類的具體處理函數。第二個是給自動時間事件續期。

         netio初始化的時候。會注冊一個60秒的循環時間事件。即每60秒會執行一次時間事件。這個時間事件有以下幾個動作。1)清除逾時的socket   2)查詢本地指令字清單 3)定時輸出netio的統計資訊

3.1   定時器的資料結構

netio的定時器資料結構是最小堆。即最近的定時任務是在最小堆上

a)申請了65個大小的二維數組

這裡為什麼申請1+64.其實隻有64個可用。 

其中的一個指針是用來輔助最小堆算法的。

即m_pNodeHeap[0] = NULL;   是一直指向空的。

最小堆的最小值  是m_pNodeHeap【1】.

b) CNode成員變量的意義

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:
大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:
大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:
大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

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~~~~~

當輸出完狀态資訊後。會把netio的一些狀态初始化為0.因為我們的狀态輸出是統計一分鐘的狀态。比如這一分鐘的請求包個數。比率。丢包個數等。

3.4  定時請求指令字

這裡還是比較重要的是。比如我們新加了一個服務。如果不重新開機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  請求到了的時間戳

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

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

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

4.2  netio_perform.log 日志分析

以下都是一份鐘的統計資料

大型分布式C++架構《四:netio之請求包中轉站 上》一、多程式下的socket epoll以及“驚群現象”二、netio之定時器三、netio 之日志分析最後:

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)最後 一定要保護好身體。   身體才是根本啊~~~

繼續閱讀