網絡服務器開發總結

網絡服務器開發總結

一、概述
經過多年網絡服務器開發實戰,於此總結實踐體會。本文涉及到異步連接、異步域名解析、熱更新、過載保護、網絡模型與架構及協程等,但不會涉及accept4、epoll等基本知識點。

二、可寫事件
相信大多數初學者都會迷惑可寫事件的作用,可能覺得可寫事件沒有什麼意義。但在網絡服務器中監聽並處理可寫事件必不可少,其作用在於判斷連接是否可以發送數據,主要用於當網絡原因暫時無法立即發送數據時監聽。

當有數據需要發送到客戶端時則直接發送。若沒能立即完整發送,則先將其緩存到發送緩衝區,並監聽其可寫事件,當該連接可寫時則再發送之且不再監聽其可寫事件(防止濫用可寫事件)。

值得注意的是,對於指定網絡連接需要先將發送緩衝區數據發送完成後才能發送新數據,此也可能比較容易忽略,至少本人當年被坑過。

三、連接緩衝區
對於長連接來說,維持網絡連接緩衝區也必不可少。目前一些網絡服務器(如QQ寵物舊接入層)都沒有維持連接的接收與發送緩衝區,更不會在暫無法發送時監聽可寫事件。其直接接收數據並處理,若處理過程中遇到不完整數據包則直接丟掉,如此則可能導致該連接的後續網絡數據包大量出錯,從而導致丟包;在發送數據時也會在無法發送時直接丟棄。

對每一網絡連接均需要維持其接收與發送數據緩衝區,當連接可讀取時則先讀取數據到接收緩衝區,然後判斷是否完整並處理之;當向連接發送數據時一般都直接發送,若不能立即完整發送時則將其緩存到發送緩衝區,然後等連接可寫時再發送,但需要注意的是,若在可寫緩衝區非空且可寫之前需要發送新數據,則此時不能直接發送而是應該將其追加到發送緩衝區後統一發送,否則會導致網絡數據竄包。

連接緩衝區內存分配常採用slab內存分配策略,可以直接實現slab算法(如memcached),但推薦直接採用jemalloc與tcmalloc等(如redis)。

四、accept阻塞性
阻塞型listen監聽套接字,其accept時也可能會存在小概率阻塞。
當accept隊列爲空時,對於阻塞套接字時accept會導致阻塞,而非阻塞套接字則立即返回EAGAIN錯誤。因此bind與listen後應該將其設置爲非阻塞,並在accept時檢查是否成功。
此外listen_fd有可讀事件時不應僅accept一次,而最好循環accept直到其返回-1。

五、異步連接
網絡服務器常需要連接到其它後端服務器,但作爲服務器阻塞連接是不可接受的,因此需要異步連接。

異步連接時首先需要創建socket並設置爲非阻塞,然後connect連接該套接字即可。若connect返回0則表示連接立即建立成功;否則需要根據errno來判斷是連接出錯還是處於異步連接過程;若errno爲EINPROGRESS則表示仍然處於異步連接連接,需要epoll來監聽socket的可寫事件(注意不是可讀事件)。當可寫後通過getsockopt來獲取錯誤碼(即getsockopt(c->sfd, SOL_SOCKET, SO_ERROR, &err, (socklen_t*)&len);),若getsockopt返回0且錯誤碼err爲0則表示連接建立成功,否則連接失敗。

由於網絡異常或後端服務器重啓等原因,網絡服務器需要能夠自動異步斷線重連,同時也應該避免後端服務器不可用時無限重試,因此需要一些重連策略。假設需要存在最多M條連接到同類型後端服務器集羣的網絡連接,若當前有效網絡連接斷開且當前連接數(包括有效和異步連接中的連接)少於M/2時則立即進行異步連接。若該連接爲異步連接失敗則不能進行再次連接,以防止遠程服務器不可用時無限重連。當需要使用連接時,則可在M條連接隨機取N次來獲取有效連接,若遇到不可用連接則進行異步連接。若N次仍獲取不到有效連接則循環M條連接來得到有效連接對象。

六、異步域名解析
當僅知道後端服務器的域名時,異步連接前需要先域名解析出遠程服務器的IP地址(如WeQuiz接入層),同樣,阻塞式域名解析對於網絡服務器來說也不是好方式。

幸好linux系統提供getaddrinfo_a函數來支持異步域名解析。getaddrinfo_a函數可以同步或異步解析域名,參數爲GAI_NOWAIT時表示執行異步解析,函數調用會立即返回,但解析將在後臺繼續執行。異步解析完成後會根據sigevent設置來產生信號(SIGEV_SIGNAL)或啓動新線程來啓動指定函數(SIGEV_THREAD)。
struct gaicb* gai = (gaicb*)calloc(1, sizeof(struct gaicb));
gai->ar_name = config_ get_dns_url(); /* url */
struct sigevent sig;
sig.sigev_notify = SIGEV_SIGNAL;
sig.sigev_value.sival_ptr = gai;
sig.sigev_signo = SIGRTMIN; /* signalfd/epoll */
getaddrinfo_a(GAI_NOWAIT, &gai, 1, &sig);

對於異步完成後產生指定信號,需要服務器進行捕獲該信號並進一步解析出IP地址。爲了能夠在epoll框架中統一處理網絡連接、進程間通信、定時器與信號等,linux系統提供eventfd、timerfd與signalfd等。在此創建dns_signal_fd = signalfd(-1, &sigmask, SFD_NONBLOCK|SFD_CLOEXEC));並添加到epoll中;當異步完成後產生指定信號會觸發dns_signal_fd可讀事件;由read函數讀取到signalfd_siginfo對象,並通過gai_error函數來判斷異步域名解析是否成功,若成功則可遍歷gai->ar_result得到IP地址列表。

七、熱更新
熱更新是指更新可執行文件時正在運行邏輯沒有受到影響(如網絡連接沒有斷開等),但新網絡連接處理將會按更新後的邏輯處理(如玩家登陸等)。熱更新功能對接入層服務器(如遊戲接入服務器或nginx等)顯得更加重要,因爲熱更新功能大部分時候可以避免停機發布,且隨時重啓而不影響當前處理連接。
WeQuiz手遊接入服務器中熱更新的實現要點:
(1)在父進程中創建listenfd與eventfd,然後創建子進程、監聽SIGUSR1信號並等待子進程結束;而子進程將監聽listenfd與eventfd,並進入epoll循環處理。
(2)當需要更新可執行文件時,發送SIGUSR1信號給父進程則可;當父進程收到更新信號後,其通過eventfd來通知子進程,同時fork出新進程並execv新可執行文件;此時存在兩對父子進程。
(3)子進程通過epoll收到eventfd更新通知時,則不再監聽並關閉listenfd與eventfd。由於關閉listenfd則無法再監聽新連接,但現有網絡連接與處理則不受影響,不過其處理仍是舊邏輯。當所有客戶端斷開連接後,epoll主循環退出則該子進程結束。值得注意的是,由於無法通過系統函數來獲取到epoll處理隊列中的連接數,則需要應用層維持當前連接數,當其連接數等於0時則退出epoll循環。此時新子進程監聽listenfd並處理新網絡連接。
(4)當舊父進程等待到舊子進程退出信號後則也結束,此時僅存在一對父子進程,完成熱更新功能。

八、過載保護


對於簡單網絡服務器來說,達到100W級連接數(8G內存)與10W級併發量(千兆網卡)基本沒問題。但網絡服務器的邏輯處理比較複雜或交互消息包過大,若不對其進行過載保護則可能服務器不可用。尤其對於系統中關鍵服務器來說(如遊戲接入層),過載可能會導致長時間無法響應甚至整個系統雪崩。

網絡服務器的過載保護常有最大文件數、最大連接數、系統負載保護、系統內存保護、連接過期、指定地址最大連接數、指定連接最大包率、指定連接最大包量、指定連接最大緩衝區、指定地址或id黑白名單等方案。
(1)最大文件數
可以在main函數中通過setrlimit設置RLIMIT_NOFILE最大文件數來約束服務器所能使用的最大文件數。此外,網絡服務器也常用setrlimit設置core文件最大值等。

(2)最大連接數
由於無法通過epoll相關函數得到當前有效的連接數,故需要應用服務器維持當前連接數,即創建連接時累加並在關閉時遞減。可以在accept/accept4接受網絡連接後判斷當前連接數是否大於最大連接數,若大於則直接關閉連接即可。

(3)系統負載保護
通過定時調用getloadavg來更新當前系統負載值,可在accept/accept4接受網絡連接後檢查當前負載值是否大於最大負載值(如cpu數* 0.8*1000),若大於則直接關閉連接即可。

(4)系統內存保護
通過定時讀取/proc/meminfo文件系統來計算當前系統內存相關值,可在accept/accept4接受網絡連接後檢查當前內存相關值是否大於設定內存值(如交換分區內存佔用率、可用空閒內存與已使用內存百分值等),若大於則直接關閉連接即可。
g_sysguard_cached_mem_swapstat = totalswap == 0 ? 0 : (totalswap - freeswap) * 100 / totalswap;
g_sysguard_cached_mem_free = freeram + cachedram + bufferram;
g_sysguard_cached_mem_used = (totalram - freeram - bufferram - cachedram) * 100 / totalram;

(5)連接過期
連接過期是指客戶端連接在較長時間內沒有與服務器進行交互。爲防止過多空閒連接佔用內存等資源,故網絡服務器應該有機制能夠清理過期網絡連接。目前常用方法包括有序列表或散列表等方式來處理,但對後端服務器來說,輪詢總不是最佳方案。QQ寵物與WeQuiz接入層通過每一連接對象維持唯一timerfd描述符,而timerfd作爲定時機制能夠添加到epoll事件隊列中,當接收該連接的網絡數據時調用timerfd_settime更新空閒時間值,若空閒時間過長則epoll會返回並直接關閉該連接即可。雖然作爲首次嘗試(至少本人沒有看到其它項目中採用過),但接入服務器一直以來都比較穩定運行,應該可以放心使用。
c->tfd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK|TFD_CLOEXEC) ;
struct itimerspec timerfd_value;
timerfd_value.it_value.tv_sec = g_cached_time + settings.sysguard_limit_timeout;
timerfd_value.it_value.tv_nsec = 0;
timerfd_value.it_interval.tv_sec = settings.sysguard_limit_timeout;
timerfd_value.it_interval.tv_nsec = 0;
timerfd_settime(c->tfd, TFD_TIMER_ABSTIME, &timerfd_value, NULL) ;
add_event(c->tfd, AE_READABLE, c) ;

(6)指定地址最大連接數
通過維持key爲地址value爲連接數的散列表或紅黑樹,並在在accept/accept4接受網絡連接後檢查該地址對應連接對象數目是否大於指定連接數(如100),若大於則直接關閉連接即可。

(7)指定連接最大包率
連接對象維持單位時間內的服務器協議完整數據包數目,讀取網絡數據後則判斷是否爲完整數據包,若完整則數目累加,同時若當前讀取數據包間隔大於單位時間則計數清零。當單元時間內的完整數據包數目大於限制值(如80)則推遲處理數據包(即僅收取到讀取緩衝區中而暫時不處理或轉發數據包),若其數目大於最大值(如100)則直接斷開連接即可。當然也可以不需要推遲處理而直接斷開連接。

(8)指定連接最大數率
連接最大數率與連接最大包率的過載保護方式基本一致,其區別在於連接最大包率針對單位時間的完整數據包數目,而連接最大數率是針對單位時間的緩衝區數據字節數。

(9)指定連接最大緩衝區
可在recv函數讀取網絡包後判斷該連接對象的可讀緩衝區的最大值,若大於指定值(如256M)則可斷開連接;當然也可以針對連接對象的可寫緩衝區;此外,讀取完整數據包後也可檢查是否大於最大數據包。

(10)指定地址或id黑白名單
     可以設置連接ip地址或玩家id作爲黑白名單來拒絕服務或不受過載限制等,目前WeQuiz暫時沒有實現此過載功能,而將其放到大區logicsvr服務器中。

此外,還可以設置TCP_DEFER_ACCEPT與SO_KEEPALIVE等套接字選項來避免無效客戶端或清理無效連接等,如開啓TCP_DEFER_ACCEPT選項後,若操作系統在三次握手完成後沒有收到真正的數據則連接一直置於accpet隊列中,並且當同一客戶端連接(但不發送數據時)達到一定數目(如linux2.6+系統16左右)後則無法再正常連接;如開啓SO_KEEPALIVE選項則可以探測出因異常而無法及時關閉的網絡連接。
setsockopt(sfd, IPPROTO_TCP, TCP_DEFER_ACCEPT, (void*)&flags, sizeof(flags));
setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (int[]){1}, sizeof(int));
setsockopt(sfd, IPPROTO_TCP, TCP_KEEPIDLE, (int[]){600}, sizeof(int));
setsockopt(sfd, IPPROTO_TCP, TCP_KEEPINTVL, (int[]){30}, sizeof(int));
setsockopt(sfd, IPPROTO_TCP, TCP_KEEPCNT, (int[]){3}, sizeof(int));

九、超時或定時機制

超時或定時機制在網絡服務器中基本必不可少,如收到請求後需要添加到超時列表中以便無法異步處理時能夠超時回覆客戶端並清理資源。對於服務器來說,超時或定時機制並不需要真正定時器來實現,可以通過維持超時列表並在while循環或epoll調用後進行檢測處理即可。


定時器管理常使用最小堆(如libevent)、紅黑樹(如nginx)與時間輪(如linux)等方式。

應用層服務器通常不必自己實現最小堆或紅黑樹或時間輪等方式來實現定時器管理,而可採用stl或boost中多鍵紅黑樹來管理,其中超時時間作爲鍵,相關對象作爲值;而紅黑樹則自動按鍵排序,檢測時僅需要從首結點開始遍歷,直到鍵值大於當時時間即可;當然可以得到首結點的超時時間作爲epoll_wait的超時時間。此外,遊戲服務器上大區邏輯服務器或實時對戰服務器也常需要持久化定時器,可以通過boost庫將其持久化到共享內存。
(1)定時器管理對象
typedef std::multimap<timer_key_t, timer_value_t> timer_map_t;
typedef boost::interprocess::multimap<timer_key_t, timer_value_t, std::less<timer_key_t>, shmem_allocator_t> timer_map_t;

(2)定時器類
class clock_timer_t

{
public:
    static clock_timer_t &instance() {static clock_timer_t instance; return instance;         }
    static uint64_t rdtsc() {
                            uint32_t low, high;
                            __asm__ volatile ("rdtsc" : "=a" (low), "=d" (high));
                            return (uint64_t) high << 32 | low;
                   }

     static uint64_t now_us() {
                            struct timespec tv;
                            clock_gettime(CLOCK_REALTIME, &tv);
                            return (tv.tv_sec * (uint64_t)1000000 + tv.tv_nsec/1000);
                   }

    uint64_t now_ms() {
                            uint64_t tsc = rdtsc();
                            if (likely(tsc - last_tsc <= kClockPrecisionDivTwo && tsc >= last_tsc)) {
                                     return last_time;
                            }
                            last_tsc = tsc;
                            last_time = now_us() / 1000;
                            return last_time;
                   }
private:
                   const static uint64_t kClockPrecisionDivTwo = 500000;
                   uint64_t last_tsc;
                   uint64_t last_time;
                   clock_timer_t() : last_tsc(rdtsc()), last_time(now_us()/1000) { }
                   clock_timer_t(const clock_timer_t&);
                   const clock_timer_t &operator=(const clock_timer_t&);
};

(3)超時檢測函數(while或epoll循環中調用),可以返回超時對象集合,也可以返回最小超時時間。
timer_values_t xxsvr_timer_t::process_timer()
{
                   timer_values_t ret;
                   timer_key_t current = clock_timer_t::instance().now_ms();
                   timer_map_it it = timer_map->begin();
                   while (it != timer_map->end()) {
                            if (it->first > current) {
                                     return ret; //返回超時對象集合,而return it->first - current返回超時時間則.
                            }
                            ret.push_back(it->second);
                           timer_map->erase(it++);
                   }

                   return ret;
}

十、網絡模型
Linux存在阻塞、非阻塞、複用、信號驅動與異步等多種IO模型,但並非每一類型IO模型均能應用於網絡方面,如異步IO不能用於網絡套接字(如linux)。通過不同設計與相關IO模型可以歸納出一些通用的網絡模型,如常用的異步網絡模型包括reactor、proactor、半異步半同步(hahs)、領導者跟隨者(lf)、多進程異步模型與分佈式系統(server+workers)等。
(1)reactor
Reactor網絡模型常指採用單進程單線程形式,以epoll爲代表的IO複用的事件回調處理方式。此網絡在網絡服務器開發方面最爲常用(如redis),尤其對於邏輯相對簡單的服務器,因爲其瓶頸不在於cpu而在網卡(如千兆網卡)。

(2)proactor
Proactor網絡模型一般採用異步IO模式,目前常用於window操作系統,如完成端口 IOCP;在linux可以在socket描述符上使用aio,而macosx中無法使用。嘗試過socket + epoll + eventfd + aio模式,但無法成功;不過測試socket + sigio(linux2.4主流) + aio則可以。在linux服務器開發方面,異步IO一般只用於異步讀取文件方面,如nginx中使用filefd + O_DIRECT + posix_memalign + aio + eventfd + epoll模式(可禁用),但其也未必比直接讀取文件高效;而寫文件與網絡方面基本不採用異步IO模式。

(3)半異步半同步(hahs)
半異步半同步模型(HalfAsync-HalfSync)常採用單進程多線程形式,其包括一個監聽主線程與一組工作者線程,其中監聽線程負責接受請求,並選取處理當前請求的工作線程(如輪詢方式等),同時將請求添加該工作線程的隊列,然後通知該工作線程處理之,最後工作線程處理並回復。對於hahs模式,所有線程(包括主線程與工作線程)均存在各自的epoll處理循環,每一工作線程對應一個隊列,主要用於主線程與工作線程間數據通信,而主線程與工作線程間通知通信常採用pipe管道或eventfd方式,且工作線程的epoll會監聽該通知描述符。hahs模式應用也比較廣泛,如memcached與thrift等,此外zeromq消息隊列也採用類似模型。

/* 主線程main_thread_process */

while (!quit) 

{

ev_s = epoll_wait(...);
for (i = 0; i < ev_s; i++) {

if (events[i].data.fd == listen_fd)

 {

    accept4(….);

else if (events[i].events & EPOLLIN)

 {

    recv(…);
    select_worker(…);
    send_worker(…);
    notify_worker(…);
}
}


/* 工作線程worker_thread_process */


while (!quit) 

{

ev_s = epoll_wait(...);

for (i = 0; i < ev_s; i++)

 {

if (events[i].data.fd == notify_fd)

{

read(….);
do_worker(…);
}
}
}

(4)領導者跟隨者(lf)
領導者跟隨者模型(Leader-Follower)也常採用單進程多線程形式,其基本思想是一個線程作爲領導者,而其餘線程均爲該線程的跟隨者(本質上爲平等線程);當請求到達時,領導者首先獲取請求,並在跟隨者中選取一個作爲新領導者,然後繼續處理請求;在實現過程中,所有線程(包括領導者與跟隨者線程)均存在自各的epoll處理循環,其通過平等epoll等待,並用加鎖方式來讓系統自動選取領導線程。lf模式應用也比較廣泛,如webpcl與一些java開源框架等。lf模式與hahs模式均能夠充分利用多核特性,對於邏輯相對複雜的服務器其有效提高併發量。對於lf模式,所有線程均可平等利用epoll內核的隊列機制,而hahs模式需要主線程讀取並維持在工作線程的隊列中,故本人比較常用lf模型,如QQPet與WeQuiz項目中接入服務器。
while (!quit) {
         pthread_mutex_lock(&leader);
Loop:
         while (stats.curr_conns && !loop.nready && !quit)
                   loop.nready = epoll_wait(...);
         if (!quit) {
                   pthread_mutex_unlock(&leader);
                   break;
         }

         loop.nready--;
         int fd = loop.fired[loop.nready];
         conn *c = loop.conns[fd];
         if (!c) { close(fd); goto Loop; }
         loop.conns[fd] = NULL;
         pthread_mutex_unlock(&leader);
         do_worker(c);
}
(5)多進程異步模型

多進程異步模型(Leader-Follower)常採用主進程與多工作進程形式,主要偏用於沒有數據共享的無狀態服務器,如nginx與lighttpd等web服務器;其主進程主要用於管理工作進程組(如熱更新或拉起異常工作進程等),而工作進程則同時監聽與處理請求,但也容易引起驚羣,可以通過進程間的互斥鎖來避免驚羣(如nginx)。

綜上所述,常用網絡模型各有優缺點,如reacor足夠簡單,lf利用多核等。但其實有時並不必太過於在意單臺服務器性能(如連接數與併發量等),更應該着眼於整體架構的可線性擴容方面等(如網絡遊戲服務器)。當然一些特定應用服務器除外,如推送服務器偏向連接數,web服務器偏向併發量等。此外,閱讀nginx/zeromq/redis與memcached等優秀開源代碼來有效提高技術與設計能力,如Nginx可達幾百萬連接數與萬兆網絡環境至少可達50萬RPS;zeromq採用相對獨特設計讓其成爲最佳消息隊列之一。

十一、架構
系統架構往往依賴於具體業務,限於篇幅僅簡述WeQuiz手遊服務器的整體架構設計。遊戲常採用接入層、邏輯層與存儲層的通用三層設計,結合目錄服務器與大區間中轉服務器等構成整個遊戲框架。但不同於端遊頁遊,手遊具有弱網絡、碎片玩法與強社交性等特點,故整體架構不僅需要優雅解決斷線重連,還可以做到簡化管理、負載均衡、有效容災與方便擴容等。架構層面解決:引入轉發層。

轉發層可以避免因網絡環境或碎片玩法等導致玩家頻繁換大區而不斷加載數據問題,維持玩家在線大區信息,同時管理全部服務器信息與維持其存活性,其連接星狀結構也有效解耦服務器間關聯性,讓內部服務器不需關心其它服務器,從而簡化整體架構。
(1)斷線重連:轉發層router維持玩家大區信息,無論從那個接入層進入均可以到達指定大區,從而不會導致玩家數據重新加載等問題。

(2)簡化管理:僅需要router維持所有服務器信息,其它服務器均不需要任何服務器信息(包括router與同類服務器)。比如大區服務器需要判斷兩個玩家是否爲好友,僅需要調用router提供接口發送即可,不用指定任何地址,也不用關心好友服務器的任何信息(比如服務器的地址與數目及存活等)。其中router接口封裝tbus讀寫功能、自動心跳回復與映射關係回調構建功能,還維持所有router列表與最新存活router服務器。

(3)負載均衡:對於router來說,採用最近心跳機制,其它服務器需要轉發包時總會向最近收到心跳的router服務器發送。經統計,所有router轉發量基本一致。而其它服務器存在多種轉發模式,比如大區服務器,若新用戶上線則選擇大區人數最少大區轉發;其它服務器採用取模或隨機方式,基本做到負載均衡。

(4)有效容災:主要是基於心跳機制,router會定時發送心跳來探測所有服務器存活,當三次沒收到心跳回復,則將其標記爲不可用,轉發時不再向該服務器轉發。同時還會向該服務器發送間隔較大的心跳探測包(目前使用60秒),以便服務器恢復後可以繼續服務。如果router掛掉,則其它服務器不會收到該router心跳包,自然不會向其發包。

(5)方便擴容:如果需要添加其它服務器,僅需要向router配置文件的對應集羣中添加新服務器,router隨後會向該服務器發送探測心跳,收到心跳回復後則可以正常服務。如果需要添加router,僅需要複製一份router,其它服務器都不需要修改任何信息。Router會自動重建映射關係(發三次重建請求,如果失敗則將該大區去除),成功後再向所有服務器發送心跳包以表示router此時可以正常服務,而其它服務器收到router心跳包則將其維持到router列表(相關功能均由router接口自動完成)。

十二、協程
協程在python、lua與go等腳本語言得到廣泛應用,並且linux系統也原生支持c協程ucontext。協程可以與網絡框架(如epoll、libevent與nginx等)完美結合(如gevent等);一般做法是收到請求創建新協程並處理,若遇到阻塞操作(如請求後端服務)則保存上下文並切換到主循環中,當可處理時(如後端服務器回覆或超時)則通過上下文來找到指定協程並處理之。對於網絡層的阻塞函數,可以通過dlsym函數來掛載相應的鉤子函數,然後在鉤子函數中直接調用原函數,並在阻塞時切換處理,這樣應用層則可以直接調用網絡層的阻塞函數而不必手動切換。

遊戲服務器一般採用單線程的全異步模式,直接使用協程模式可能相對比較少,但在一些cgi調用形式的web應用(如遊戲社區或運營活動等)則逐步得到應用。比如QQ寵物社區遊戲原來採用apache+cgi/fcgi模式的阻塞請求處理,基本僅能達到每秒300併發量,通過strace觀察到時間基本消耗在網絡阻塞中,所以需要尋求一種代碼儘量兼容但能提高吞吐量的技術,從而協程成爲最佳選擇,即採用libevent+greenlet+python來開發新業務,而選擇nginx+module+ucontext來重用舊代碼,最後做到修改不到20行代碼則性能提高20倍(siege壓測實際業務可達到8kQPS)。

十三、其它
網絡服務器方面除了基本代碼開發以外,還涉及到構建、調試、優化、壓測與監控等方面,但由於最近新手遊項目開發任務比較重,將後期再逐步總結,現僅簡單羅列一下。
(1)構建
一直以來都使用cmake來構建各類工程(如linux服務器與window/macosx客戶端程序等),體會到cmake是最優秀的構建工具之一,其應用也比較廣泛,如mysql、cocos2dx與vtk等。
project(server)
add_executable(server server.c)
target_link_libraries(server pthread tcmalloc)
cmake .; make; make install

(2)調試
網絡服務器開發調試大部分情況都可以通過日誌來完成,必要時可以通過gdb調試,當然也可以在Linux系統下直接使用eclipse/gdb來可視化調試。
當程序異常時,有core文件直接使用gdb調試,如bt full查看全棧詳細信息或f跳到指定棧p查看相關信息;沒有core文件時則可以查看/var/log/message得到地址信息,然後通過addr2line或objdump來定位到相關異常代碼。
對於服務器來說,內存泄漏檢測也是必不可少的,其中valgrind爲最佳的內存泄漏檢測工具。
此外,其它常用的調試工具(編譯階段與運行階段)有nm、strings、strip、readelf、ldd、pstack、strace、ltrace與mtrace等。

(3)優化
網絡服務器優化涉及算法與技術等多個方面。

算法方面需要根據不同處理場景來選擇最優算法,如九宮格視野管理算法、跳躍表排行算法與紅黑樹定時器管理算法等,此外,還可以通過有損服務來設定最佳方案,如WeQuie中採用到的有損排行榜服務。

技術方面可以涉及到IO線程與邏輯分離、slab內存管理(如jemalloc與tcmalloc等)、socket函數(如accept4、readv、writev與sendfile64等)、socket選項(如TCP_CORK、TCP_DEFER_ACCEPT、SO_KEEPALIVE、TCP_NODELAY與TCP_QUICKACK等)、新實現機制(如aio、O_DIRECT、eventfd與clock_gettime等)、無鎖隊列(如CAS、boost::lockfree::spsc_queue與zmq::yqueue_t等)、異步處理(如操作mysql時採用異步接口庫libdrizzle、webscalesql或mongodb或redis異步接口與gevent類異步框架等)、協議選擇(如http或pb類型)、數據存儲形式(如mysql的blob類型、mongodb的bjson類型或pb類型等)、存儲方案(如mysql、mongodb、redis、bitcask與leveldb及hdfs等)、避免驚羣(如加鎖避免)、用戶態鎖(如nginx通過應用層的CAS實現(更好跨平臺性))、網絡狀態機、引用計數、時間緩存、CPU親緣性與模塊插件形式(如python、lua等)。

常用的調優工具有valgrind、strace、perf與gprof及google-perftools等,如valgrind的callgrind工具,可以在需要分析代碼段前後加上CALLGRIND_START_INSTRUMENTATION; CALLGRIND_TOGGLE_COLLECT; CALLGRIND_TOGGLE_COLLECT; CALLGRIND_STOP_INSTRUMENTATION;,然後運行valgrind --tool=callgrind --collect-atstart=no --instr-atstart=no ./webdir即可,得到分析結果文件還可用Kcachegrind可視化展示。

除了提高服務器運行效率外,還可以通過一些開發包或開源庫來提高服務器開發效率,如採用boost庫管理不定長對象的共享內存、python協程與go框架等。

(4)壓測
對於網絡服務器來說,壓力測試過程必不可少,其可用於評估響應時間與吞吐量,也可以有效檢查是否存在內存泄漏等,爲後期修正與優化提供依據。

對於http服務器,常用ab或siege等工具進行壓測,如./siege –c 500 –r 10000 –b –q http://10.193.0.102:8512/petcgi/xxx?cmd=yyy。
對於其它類型服務器一般都需要自己編寫壓測客戶端(如redis壓測工具),常用方法是直接創建多線程,每一線程使用libevent創建多連接與定時器等來異步請求與統計。

此外,若需要測試大量連接數,則可能需要多臺客戶機或創建多個虛擬ip地址。

(5)高可用性
服務器的高可用性實現策略包括主從機制(如redis等)、雙主機制(如mysql+keepalive/heartbeat)、動態選擇(如zookeeper)與對稱機制(如dynamo)等,如雙主機制可由兩臺等效機器的VIP地址與心跳機制來實現,常常採用keepalive服務,當然也可以由服務器自主實現,如服務器啓動時需要指定參數來標識其爲主機還是從機,同時主備需要通過心跳包來保持異常時切換,如

void server_t::ready_as_master()
{
  primary = 1; backup = 0;
  system("/sbin/ifconfig eth0:havip 10.2.2.147 broadcast 10.2.2.255 netmask 255.255.255.0 up"); //! 虛擬IP
  system("/sbin/route add -host 10.2.2.147 dev eth0:havip");
  system("/sbin/arping -I eth0 -c 3 -s 10.2.2.147 10.2.2.254");
  up("tcp://10.2.2.147:5555");
}

void server_t::ready_as_slave()
{
  primary = 0; backup = 1;
  system("/sbin/ifconfig eth0:havip 10.2.2.147 broadcast 10.2.2.255 netmask 255.255.255.0 down");
  down("tcp://10.2.2.147:5555");
}
當然這是相對簡單方式(其前提是主備機器均可正常通信),沒有考慮到異常情況(如主備機器間的網線斷開情況等),此時可以考慮用雙中心控制與動態選舉擇模式等。

(6)監控
Linux在服務器監控方面工具非常豐富,包括ps、top、ping、traceroute、nslookup、tcpdump、netstat、ss、lsof、nc、vmstat、iostat、dstat、ifstat、mpstat、pidstat、free、iotop、df、du、dmesg、gstack、strace與sar(如-n/-u/-r/-b/-q等)及/proc等,如ps auxw查看進程標記位(一般地D阻塞在IO、R在cpu、S表示未能及時被喚醒等),gstack pid查看進程當前棧信息,ss -s查看連接信息,sar -n DEV 1 5查看包量,sar -r 1 5查看內存使用情況,vmstat 1 5查看進程切換頻率,iotop或iostat -tdx 1或dstat -tclmdny 1查看磁盤信息與mpstat 2查看CPU信息及/proc/net/sockstat查看socket狀態等。此外有時最有效的是服務器日記文件。

十四、結束

除了網絡服務器基本開發技術之外,系統整體架構更爲重要(如可線性擴容性),後期有時間再詳細總結,對於網絡遊戲架構方面可參見WeQuiz手遊服務器架構與QQPet寵物架構設計等。


此文章轉載於:http://my.oschina.net/u/181613/blog/596022

發佈了305 篇原創文章 · 獲贊 598 · 訪問量 159萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章