一次壓力測試Bug排查-epoll使用避坑指南

本文始發於我的公衆號:兩猿社,原創不易,求個關注html

Bug復現

使用Webbench對服務器進行壓力測試,建立1000個客戶端,併發訪問服務器10s,正常狀況下有接近8萬個HTTP請求訪問服務器。web

結果顯示僅有7個請求被成功處理,0個請求處理失敗,服務器也沒有返回錯誤。此時,從瀏覽器端訪問服務器,發現該請求也不能被處理和響應,必須將服務器重啓後,瀏覽器端才能訪問正常。編程

<div align=center><img src="http://ww1.sinaimg.cn/large/005TJ2c7ly1gc6n34smiqj30fq05lq3l.jpg" height="200"/> </div>瀏覽器


排查過程

經過查詢服務器運行日誌,對服務器接收HTTP請求鏈接,HTTP處理邏輯兩部分進行排查。服務器

日誌中顯示,7個請求報文爲:GET / HTTP/1.0的HTTP請求被正確處理和響應,排除HTTP處理邏輯錯誤併發

<div align=center><img src="http://ww1.sinaimg.cn/large/005TJ2c7ly1gc6nhkp8gbj30jd0eiq7i.jpg" height="300"/> </div>socket

所以,將重點放在接收HTTP請求鏈接部分。其中,服務器端接收HTTP請求的鏈接步驟爲socket -> bind -> listen -> accept;客戶端鏈接請求步驟爲socket -> connect。函數

listen

#include<sys/socket.h>
int listen(int sockfd, int backlog)
  • 函數功能,把一個未鏈接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的鏈接請求。根據TCP狀態轉換圖,調用listen致使套接字從CLOSED狀態轉換成LISTEN狀態
  • backlog是隊列的長度,內核爲任何一個給定的監聽套接口維護兩個隊列:
    • 未完成鏈接隊列(incomplete connection queue),每一個這樣的 SYN 分節對應其中一項:已由某個客戶發出併到達服務器,而服務器正在等待完成相應的 TCP 三次握手過程。這些套接口處於 SYN_RCVD 狀態
    • 已完成鏈接隊列(completed connection queue),每一個已完成 TCP 三次握手過程的客戶對應其中一項。這些套接口處於ESTABLISHED狀態

connect

  • 當有客戶端主動鏈接(connect)服務器,Linux 內核就自動完成TCP 三次握手,該項就從未完成鏈接隊列移到已完成鏈接隊列的隊尾,將創建好的鏈接自動存儲到隊列中,如此重複。

accept

  • 函數功能,從處於ESTABLISHED狀態的鏈接隊列頭部取出一個已經完成的鏈接(三次握手以後)。
  • 若是這個隊列沒有已經完成的鏈接,accept函數就會阻塞,直到取出隊列中已完成的用戶鏈接爲止。
  • 若是,服務器不能及時調用 accept取走隊列中已完成的鏈接,隊列滿掉後,TCP就緒隊列中剩下的鏈接都得不處處理,同時新的鏈接也不會到來。

從上面的分析中能夠看出,accept若是沒有將隊列中的鏈接取完,就緒隊列中剩下的鏈接都得不處處理,也不能接收新請求,這個特性與壓力測試的Bug十分相似測試


定位accept

//對文件描述符設置非阻塞
int setnonblocking(int fd){
    int old_option=fcntl(fd,F_GETFL);
    int new_option=old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}

//將內核事件表註冊讀事件,ET模式,選擇開啓EPOLLONESHOT
void addfd(int epollfd,int fd,bool one_shot)
{
    epoll_event event;
    event.data.fd=fd;
    event.events=EPOLLIN|EPOLLET|EPOLLRDHUP;
    if(one_shot)
        event.events|=EPOLLONESHOT;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
    setnonblocking(fd);
}

//建立內核事件表
epoll_event events[MAX_EVENT_NUMBER];
int epollfd=epoll_create(5);
assert(epollfd!=-1);

//將listenfd設置爲ET邊緣觸發
addfd(epollfd,listenfd,false);

int number=epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);

if(number<0&&errno!=EINTR)
{
    printf("epoll failure\n");
    break;
}

for(int i=0;i<number;i++)
{
    int sockfd=events[i].data.fd;

    //處理新到的客戶鏈接
    if(sockfd==listenfd)
    {
        struct sockaddr_in client_address;
        socklen_t client_addrlength=sizeof(client_address);
		
		//定位accept
		//從listenfd中接收數據
        int connfd=accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
        if(connfd<0)
        {
            printf("errno is:%d\n",errno);
            continue;
        }
		//TODO,邏輯處理
    }
}

分析代碼發現,web端和服務器端創建鏈接,採用epoll的邊緣觸發模式同時監聽多個文件描述符。spa

epoll的ET、LT

  • LT水平觸發模式
    • epoll_wait檢測到文件描述符有事件發生,則將其通知給應用程序,應用程序能夠不當即處理該事件。
    • 當下一次調用epoll_wait時,epoll_wait還會再次嚮應用程序報告此事件,直至被處理。
  • ET邊緣觸發模式
    • epoll_wait檢測到文件描述符有事件發生,則將其通知給應用程序,應用程序必須當即處理該事件。
    • 必需要一次性將數據讀取完,使用非阻塞I/O,讀取到出現eagain

從上面的定位分析,問題多是錯誤使用epoll的ET模式


代碼分析修改

嘗試將listenfd設置爲LT阻塞,或者ET非阻塞模式下while包裹accept對代碼進行修改,這裏以ET非阻塞爲例。

for(int i=0;i<number;i++)
{
    int sockfd=events[i].data.fd;

    //處理新到的客戶鏈接
    if(sockfd==listenfd)
    {
        struct sockaddr_in client_address;
        socklen_t client_addrlength=sizeof(client_address);
		
		//從listenfd中接收數據
		//這裏的代碼出現使用錯誤
		while ((connfd = accept (listenfd, (struct sockaddr *) &remote, &addrlen)) > 0){
	        if(connfd<0)
	        {
	            printf("errno is:%d\n",errno);
	            continue;
	        }
			//TODO,邏輯處理
		}
    }
}

將代碼修改後,從新進行壓力測試,問題獲得解決,服務器成功完成75617個訪問請求,且沒有出現任何失敗的狀況。壓測結果以下:

<div align=center><img src="http://ww1.sinaimg.cn/large/005TJ2c7ly1gc6n55oypzj30fv05jaap.jpg" height="200"/> </div>


覆盤總結

  • Bug緣由
    • established狀態的鏈接隊列backlog參數,歷史上被定義爲已鏈接隊列和未鏈接隊列兩個的大小之和,大多數實現默認值爲5。當鏈接較少時,隊列不會變滿,即便listenfd設置成ET非阻塞,不使用while一次性讀取完,也不會出現Bug
    • 若此時1000個客戶端同時對服務器發起鏈接請求,鏈接過多會形成established 狀態的鏈接隊列變滿。但accept並無使用while一次性讀取完,只讀取一個。所以,鏈接過多致使TCP就緒隊列中剩下的鏈接都得不處處理,同時新的鏈接也不會到來。
  • 解決方案
    • 將listenfd設置成LT阻塞,或者ET非阻塞模式下while包裹accept便可解決問題。

該Bug的出現,本質上對epoll的ET和LT模式實踐編程較少,沒有深入理解和深刻應用。

若是以爲有所收穫,請順手點個關注吧,大家的舉手之勞對我來講很重要。

<div align=center><img src="http://ww1.sinaimg.cn/large/005TJ2c7ly1gc5hnxxwzqj30ij0cvjt8.jpg" height="350"/> </div>

原文出處:https://www.cnblogs.com/qinguoyi/p/12355519.html

相關文章
相關標籤/搜索