网络服务器开发热点分析与解决

发表于2015-04-29
评论0 3.6k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594


一、概述

经过多年网络服务器开发实战,于此总结实践体会。本文讲解异步连接、异步域名解析、热更新、过载保护网络模型与架构设计协程等但不会涉及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[]){(rand()%100)+500}, 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_value_t> timer_map_t;

typedef boost::interprocess::multimap timer_value_t, std::less, 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操作系统,如完成端口 IOCPlinux可以在socket描述符上使用aiomacosx中无法使用尝试过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);

              while (stats.curr_conns && !loop.nready && !quit)

                            loop.nready = epoll_wait(...);

              if (!quit) {

                            pthread_mutex_unlock(&leader);

                            break;

              }

              conn *c = loop.conns[loop.fired[loop.nready--]];

              loop.conns[c->sfd] = 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行代码则性能提高20siege压测实际业务可达到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内存管理(如jemalloctcmallocsocket函数(如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)及/procps 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

 

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台