網絡服務器開發總結

jopen 8年前發布 | 36K 次閱讀 網絡工具包

 

網絡服務器開發總結

 

 

一、概述

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

 

 

二、可寫事件

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

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

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

 

 

三、連接緩沖區

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

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

連接緩沖區內存分配常采用slab內存分配策略,可以直接實現slab算法(如memcached),但推薦直接采用jemalloctcmalloc等(如redis)。

 

 

四、accept阻塞性

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

accept隊列為空時,對于阻塞套接字時accept會導致阻塞,而非阻塞套接字則立即返回EAGAIN錯誤。因此bindlisten后應該將其設置為非阻塞,并在accept時檢查是否成功。

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

 

 

五、異步連接

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

異步連接時首先需要創建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條連接來得到有效連接對象。

 

 

六、異步域名解析

當僅知道后端服務器的域名時,異步連接前需要先域名解析出遠程服務器的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地址列表。

 

 

七、熱更新

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

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

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

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

3)子進程通過epoll收到eventfd更新通知時,則不再監聽并關閉listenfdeventfd。由于關閉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_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));

 

 

九、超時或定時機制

超時或定時機制在網絡服務器中基本必不可少,如收到請求后需要添加到超時列表中以便無法異步處理時能夠超時回復客戶端并清理資源。對于服務器來說,超時或定時機制并不需要真正定時器來實現,可以通過維持超時列表并在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;

}

 

 

十、網絡模型

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采用相對獨特設計讓其成為最佳消息隊列之一。

 

 

十一、架構

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

 

 

十二、協程

協程在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)。

 

 

十三、其它

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

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狀態等。此外有時最有效的是服務器日記文件。

 

十四、結束

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

歡迎rtx(baokaichen)email(chenbk@foxmail.com)指正與討論。Ths

 

 


來自: http://my.oschina.net/u/181613/blog/596022

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!