網絡服務器開發總結(轉:http://my.oschina.net/u/181613/blog/596022)

1、概述java

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

 

 

2、可寫事件mysql

相信大多數初學者都會迷惑可寫事件的做用,可能以爲可寫事件沒有什麼意義。但在網絡服務器中監聽並處理可寫事件必不可少,其做用在於判斷鏈接是否能夠發送數據,主要用於當網絡緣由暫時沒法當即發送數據時監聽。react

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

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

 

 

3、鏈接緩衝區nginx

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

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

鏈接緩衝區內存分配常採用slab內存分配策略,能夠直接實現slab算法(如memcached),但推薦直接採用jemalloctcmalloc等(如redis)。算法

 

 

4、accept阻塞性

阻塞型listen監聽套接字,其accept時也可能會存在小几率阻塞。

accept隊列爲空時,對於阻塞套接字時accept會致使阻塞,而非阻塞套接字則當即返回EAGAIN錯誤。所以bindlisten後應該將其設置爲非阻塞,並在accept時檢查是否成功。

此外listen_fd有可讀事件時不該僅accept一次,而最好循環accept直到其返回-1

 

 

5、異步鏈接

網絡服務器常須要鏈接到其它後端服務器,但做爲服務器阻塞鏈接是不可接受的,所以須要異步鏈接。

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

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

 

 

6、異步域名解析

當僅知道後端服務器的域名時,異步鏈接前須要先域名解析出遠程服務器的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系統提供eventfdtimerfdsignalfd等。在此建立dns_signal_fd = signalfd(-1, &sigmask, SFD_NONBLOCK|SFD_CLOEXEC));並添加到epoll中;當異步完成後產生指定信號會觸發dns_signal_fd可讀事件;由read函數讀取到signalfd_siginfo對象,並經過gai_error函數來判斷異步域名解析是否成功,若成功則可遍歷gai->ar_result獲得IP地址列表。

 

 

7、熱更新

熱更新是指更新可執行文件時正在運行邏輯沒有受到影響(如網絡鏈接沒有斷開等),但新網絡鏈接處理將會按更新後的邏輯處理(如玩家登錄等)。熱更新功能對接入層服務器(如遊戲接入服務器或nginx等)顯得更加劇要,由於熱更新功能大部分時候能夠避免停機發布,且隨時重啓而不影響當前處理鏈接。

WeQuiz手遊接入服務器中熱更新的實現要點:

1)在父進程中建立listenfdeventfd,而後建立子進程、監聽SIGUSR1信號並等待子進程結束;而子進程將監聽listenfdeventfd,並進入epoll循環處理。

2)當須要更新可執行文件時,發送SIGUSR1信號給父進程則可;當父進程收到更新信號後,其經過eventfd來通知子進程,同時fork出新進程並execv新可執行文件;此時存在兩對父子進程。

3)子進程經過epoll收到eventfd更新通知時,則再也不監聽並關閉listenfdeventfd。因爲關閉listenfd則沒法再監聽新鏈接,但現有網絡鏈接與處理則不受影響,不過其處理還是舊邏輯。當全部客戶端斷開鏈接後,epoll主循環退出則該子進程結束。值得注意的是,因爲沒法經過系統函數來獲取到epoll處理隊列中的鏈接數,則須要應用層維持當前鏈接數,當其鏈接數等於0時則退出epoll循環。此時新子進程監聽listenfd並處理新網絡鏈接。

4)當舊父進程等待到舊子進程退出信號後則也結束,此時僅存在一對父子進程,完成熱更新功能。

 

 

8、過載保護

對於簡單網絡服務器來講,達到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_ACCEPTSO_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));

 

 

9、超時或定時機制

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

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

應用層服務器一般沒必要本身實現最小堆或紅黑樹或時間輪等方式來實現定時器管理,而可採用stlboost中多鍵紅黑樹來管理,其中超時時間做爲鍵,相關對象做爲值;而紅黑樹則自動按鍵排序,檢測時僅須要從首結點開始遍歷,直到鍵值大於當時時間便可;固然能夠獲得首結點的超時時間做爲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)超時檢測函數(whileepoll循環中調用),能夠返回超時對象集合,也能夠返回最小超時時間。

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;

}

 

 

10、網絡模型

Linux存在阻塞、非阻塞、複用、信號驅動與異步等多種IO模型,但並不是每一類型IO模型均能應用於網絡方面,如異步IO不能用於網絡套接字(如linux)。經過不一樣設計與相關IO模型能夠概括出一些通用的網絡模型,如經常使用的異步網絡模型包括reactorproactor、半異步半同步(hahs)、領導者跟隨者(lf)、多進程異步模型與分佈式系統(server+workers)等。

1reactor

Reactor網絡模型常指採用單進程單線程形式,以epoll爲表明的IO複用的事件回調處理方式。此網絡在網絡服務器開發方面最爲經常使用(如redis),尤爲對於邏輯相對簡單的服務器,由於其瓶頸不在於cpu而在網卡(如千兆網卡)。

 

2proactor

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模式應用也比較普遍,如memcachedthrift等,此外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模型,如QQPetWeQuiz項目中接入服務器。

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)常採用主進程與多工做進程形式,主要偏用於沒有數據共享的無狀態服務器,如nginxlighttpdweb服務器;其主進程主要用於管理工做進程組(如熱更新或拉起異常工做進程等),而工做進程則同時監聽與處理請求,但也容易引發驚羣,能夠經過進程間的互斥鎖來避免驚羣(如nginx)。

 

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

 

 

11、架構

系統架構每每依賴於具體業務,限於篇幅僅簡述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接口自動完成)。

 

 

12、協程

協程在pythonluago等腳本語言獲得普遍應用,而且linux系統也原生支持c協程ucontext。協程能夠與網絡框架(如epolllibeventnginx等)完美結合(如gevent等);通常作法是收到請求建立新協程並處理,若遇到阻塞操做(如請求後端服務)則保存上下文並切換到主循環中,當可處理時(如後端服務器回覆或超時)則經過上下文來找到指定協程並處理之。對於網絡層的阻塞函數,能夠經過dlsym函數來掛載相應的鉤子函數,而後在鉤子函數中直接調用原函數,並在阻塞時切換處理,這樣應用層則能夠直接調用網絡層的阻塞函數而沒必要手動切換。

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

 

 

十3、其它

網絡服務器方面除了基本代碼開發之外,還涉及到構建、調試、優化、壓測與監控等方面,但因爲最近新手遊項目開發任務比較重,將後期再逐步總結,現僅簡單羅列一下。

1)構建

一直以來都使用cmake來構建各種工程(如linux服務器與window/macosx客戶端程序等),體會到cmake是最優秀的構建工具之一,其應用也比較普遍,如mysqlcocos2dxvtk等。

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獲得地址信息,而後經過addr2lineobjdump來定位到相關異常代碼。

對於服務器來講,內存泄漏檢測也是必不可少的,其中valgrind爲最佳的內存泄漏檢測工具。

此外,其它經常使用的調試工具(編譯階段與運行階段)有nmstringsstripreadelflddpstackstraceltracemtrace等。

 

3)優化

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

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

技術方面能夠涉及到IO線程與邏輯分離、slab內存管理(如jemalloctcmalloc等)、socket函數(如accept4readvwritevsendfile64等)、socket選項(如TCP_CORKTCP_DEFER_ACCEPTSO_KEEPALIVETCP_NODELAYTCP_QUICKACK等)、新實現機制(如aioO_DIRECTeventfdclock_gettime等)、無鎖隊列(如CASboost::lockfree::spsc_queuezmq::yqueue_t等)、異步處理(如操做mysql時採用異步接口庫libdrizzlewebscalesqlmongodbredis異步接口與gevent類異步框架等)、協議選擇(如httppb類型)、數據存儲形式(如mysqlblob類型、mongodbbjson類型或pb類型等)、存儲方案(如mysqlmongodbredisbitcaskleveldbhdfs等)、避免驚羣(如加鎖避免)、用戶態鎖(如nginx經過應用層的CAS實現(更好跨平臺性))、網絡狀態機、引用計數、時間緩存、CPU親緣性與模塊插件形式(如pythonlua等)。

經常使用的調優工具備valgrindstraceperfgprofgoogle-perftools等,如valgrindcallgrind工具,能夠在須要分析代碼段先後加上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服務器,經常使用absiege等工具進行壓測,如./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在服務器監控方面工具很是豐富,包括pstoppingtraceroutenslookuptcpdumpnetstatsslsofncvmstatiostatdstatifstatmpstatpidstatfreeiotopdfdudmesggstackstracesar(如-n/-u/-r/-b/-q等)及/proc等,如ps auxw查看進程標記位(通常地D阻塞在IORcpuS表示未能及時被喚醒等),gstack pid查看進程當前棧信息,ss -s查看鏈接信息,sar -n DEV 1 5查看包量,sar -r 1 5查看內存使用狀況,vmstat 1 5查看進程切換頻率,iotopiostat -tdx 1dstat -tclmdny 1查看磁盤信息與mpstat 2查看CPU信息及/proc/net/sockstat查看socket狀態等。此外有時最有效的是服務器日記文件。

 

十4、結束

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

相關文章
相關標籤/搜索