-
游戏服务器常见问题的解决方法分享
在游戏开发中,我们经常会遇到一些技术难题,而其引发的bug则会影响整个游戏的品质。 过载保护、集群、服务器通信、并发选型等方面的问题,是中小团队常常的技术难题,本文分享了一些专家在坐诊过程中提到的解决方法,希望对大家有所帮助。 问题一:玩家登录时拉取好友信息,但好友服务繁忙导致登录失败。 解决方法: 1、分离关键路径上非关键调用,缩短事务流程,避免周边服务异常阻塞登录。 2、服务熔断机制,超出处理能力快速失败,防止雪崩。 3、按用户隔离事务,避兔单个用户请求阻塞影响到其他用户。 问题二:压测并发登录对redis产生很大压力。 解决方法:redis数据表数量多,一次事务会产生多个 redis请求,小表合并为大表。 Wade:服务器进程的管理一般比较简单,有很多还是用配置文件静态组织的。同时往往进程间通信的手段比较缺乏,没有使用消息队列中间件,甚至还有用 Redis 来做通信组件使用的。为了提高集群管理的自动化水平,使用 ZooKeeper 是一个比较常见的方法。 Zc:redis一般做为内存缓存来使用,不宜将关键数据存放在redis中.其数据安全性并不如一般的DB。在使用过程中也需要参考性能基线,控制访问频率和流量。 问题三:外部服务有延迟,调用到的业务流程中产生卡顿。 解决方法:业务侧增加缓存:同玩好友msdk+最近角色id+角色信息。 Wade:很多团队对于过载保护不够重视,往往只在最外层接入客户端一侧有最大连接数或者最大会话数的限制。而对于内部的多个进程,比如访问数据库的进程,就没有太多的负载保护。由于游戏是带状态的进程比较多,所以负载均衡往往也做的不多,基本上是按状态所在进程去转发处理请求。 Zc:注意缓存和降级处理。外部平台数据,尽量缓存,提高访问体验。当发现外部服务出现故障,或本身出现负载风险时,应降级服务。 Jovi:msdk midas平台特权等api接入工作,游戏业务可以建立一个隔离层专门处理这块需求,避兔过分侵入游戏逻辑,更容易控制。 问题四:运营和客服接口修改玩家数据,会与正常游戏的数据回写产生竞争。 解决方法:使用类似邮件机制去修改数据。 Zc:多线程开发中,经常会有线程池用尽或线程死锁导致服务质量下降。建议将线程池根据业务需求合理分类,不同业务间有合理的负载配比,不会相互影响。非关键流程需要延后或者异步化处理,避免卡死关键流程。 同时,合理的线程模型可以有效减少线程间竞争。对确实需要竞争的资源在流程入口处统一有序加锁,避免在逻辑过程中,随意嵌套取锁竞争。并且,给锁加个超时时间,避免业务中断。 Jovi:确保同一时刻只有单个数据修改点,有助于避免数据竞争。建议设计时采用CQRS方式,采用独立的数据表和服务记录事件,汇总到单一修改服务上执行。 Wade:并发编程是服务器端最常见的问题,一般会用多线程或者非阻塞两种方法之一解决。对于天然支持多线程的语言,如JAVA,很多开发者倾向多线程,好处是代码编写起来比较方便,但是这就要很清醒的对各种对象进行锁的操作,或者熟练使用类似 java.util.concurrent 这种多线程工具库。而如果使用非阻塞,好处是不会有锁的问题,但代码被分割到各个回调函数中,可读性非常糟糕,所以有的团队会使用“协程”或者 Promise 之类的工具来缓解这个问题,但这也引入了更多的复杂性。 下面详细介绍一下游戏服务器端架构中的调度架构,方便大家理解。 a) 单进程游戏服务器 最简单的游戏服务器只有一个进程,是一个单点。这个进程如果退出,则整个游戏世界消失。在此进程中,由于需要处理并发的客户端的数据包,因此产生了多种选择方法: [图-单进程调度模型] 同步-动态多线程 每接收一个用户会话,就建立一个线程。这个用户会话往往就是由客户端的TCP连接来代表,这样每次从socket中调用读取或写出数据包的时候,都可以使用阻塞模式,编码直观而简单。有多少个游戏客户端的连接,就有多少个线程。但是这个方案也有很明显的缺点,就是服务器容易产生大量的线程,这对于内存占用不好控制,同时线程切换也会造成CPU的性能损失。更重要的多线程下对同一块数据的读写,需要处理锁的问题,这可能让代码变的非常复杂,造成各种死锁的BUG,影响服务器的稳定性。 同步-多线程池 为了节约线程的建立和释放,建立了一个线程池。每个用户会话建立的时候,向线程池申请处理线程的使用。在用户会话结束的时候,线程不退出,而是向线程池“释放”对此线程的使用。线程池能很好的控制线程数量,可以防止用户暴涨下对服务器造成的连接冲击,形成一种排队进入的机制。但是线程池本身的实现比较复杂,而“申请”、“释放”线程的调用规则需要严格遵守,否则会出现线程泄露,耗尽线程池。 异步-单线程/协程 在游戏行业中,采用Linux的epoll作为网络API,以期得到高性能,是一个常见的选择。游戏服务器进程中最常见的阻塞调用就是网路IO,因此在采用epoll之后,整个服务器进程就可能变得完全没有阻塞调用,这样只需要一个线程即可。这彻底解决了多线程的锁问题,而且也简化了对于并发编程的难度。但是,“所有调用都不得阻塞”的约束,并不是那么容易遵守的,比如有些数据库的API就是阻塞的;另外单进程单线程只能使用一个CPU,在现在多核多CPU的服务器情况下,不能充分利用CPU资源。异步编程由于是基于“回调”的方式,会导致要定义很多回调函数,并且把一个流程里面的逻辑,分别写在多个不同的回调函数里面,对于代码阅读非常不利。——针对这种编码问题,协程(Coroutine)能较好的帮忙,所以现在比较流行使用异步+协程的组合。不管怎样,异步-单线程模型由于性能好,无需并发思维,依然是现在很多团队的首选。 异步-固定多线程 这是基于异步-单线程模型进化出来的一种模型。这种模型一般有三类线程:主线程、IO线程、逻辑线程。这些线程都在内部以全异步的方式运行,而他们之间通过无锁消息队列通信。 b) 多进程游戏服务器 多进程的游戏服务器系统,最早起源于对于性能问题需求。由于单进程架构下,总会存在承载量的极限,越是复杂的游戏,其单进程承载量就越低,因此开发者们一定要突破进程的限制,才能支撑更复杂的游戏。 一旦走上多进程之路,开发者们还发现了多进程系统的其他一些好处:能够利用上多核CPU能力;利用操作系统的工具能更仔细的监控到运行状态、更容易进行容灾处理。多进程系统比较经典的模型是“三层架构”: 在多进程架构下,开发者一般倾向于把每个模块的功能,都单独开发成一个进程,然后以使用进程间通信来协调处理完整的逻辑。这种思想是典型的“管道与过滤器”架构模式思想——把每个进程看成是一个过滤器,用户发来的数据包,流经多个过滤器衔接而成的管道,最后被完整的处理完。由于使用了多进程,所以首选使用单进程单线程来构造其中的每个进程。这样对于游戏程序开发来说,结构清晰简单很多,也能获得更高的性能。 [图-经典的三层模型] 尽管有很多好处,但是多进程系统还有一个需要特别注意的问题——数据存储。由于要保证数据的一致性,所以存储进程一般都难以切分成多个进程。就算对关系型数据做分库分表处理,也是非常复杂的,对业务类型有依赖的。而且如果单个逻辑处理进程承载不了,由于其内存中的数据难以分割和同步,开发者很难去平行的扩展某个特定业务逻辑。他们可能会选择把业务逻辑进程做成无状态的,但是这更加加重了存储进程的性能压力,因为每次业务处理都要去存储进程处拉取或写入数据。 除了数据的问题,多进程架构也带来了一系列运维和开发上的问题:首先就是整个系统的部署更为复杂了,因为需要对多个不同类型进程进行连接配置,造成大量的配置文件需要管理;其次是由于进程间通讯很多,所以需要定义的协议也数量庞大,在单进程下一个函数调用解决的问题,在多进程下就要定义一套请求、应答的协议,这造成整个源代码规模的数量级的增大;最后是整个系统被肢解为很多个功能短小的代码片段,如果不了解整体结构,是很难理解一个完整的业务流程是如何被处理的,这让代码的阅读和交接成本巨高无比,特别是在游戏领域,由于业务流程变化非常快,几经修改后的系统,几乎没有人能完全掌握其内容。 游戏解决方案。详询MMCloud客服QQ95015688 。
-
运维人必收藏的最全Linux服务器程序规范
除了网络通信外,服务器程序还必须考虑许多其他细节问题,零碎,但基本上都是模板式的。 Linux服务器程序一般以后台形式运行。后台程序又称守护进程。它没有控制终端,因而也不会意外接受用户输入。守护进程的父进程一般是init进程(pid=1)。 Linux服务器程序通常有一套日志系统,它至少能输出日志到文件,有的高级服务器可以输出日志到专门的UDP服务器。大部分后台进程都在/var/log下有自己的日志目录。 Linux服务器程序一般以某个专门的非root身份运行。mysqld, httpd, syslogd等后台进程,并分别有自己的运行账户mysql, apache, syslog。‘ Linux服务器通常时可配置的。服务器程序通常处理很多命令选项,如果一次运行的选项太多,则克拉一用配置文件来管理。绝大多数服务器程序都有配置文件并存放在/etc下。 Linux服务器程序通常在启动时生成一个PID文件并存入/var/run目录中,以记录该后台进程的PID。 Linux服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷,比如进程可用文件描述符总数和内存总量等。 01 日志 1.Linux系统日志: Linux提供一个守护进程来处理系统日志–syslogd, 升级版–rsyslogd。 rsyslogd守护进程可以接收用户进程输出日志,可以接受内核日志。 用户进程时通过调用syslog函数生成系统日志的。 该函数将日志输出到一个unix本地域socket类型(AF_UNIX)的文件/dev/log中,rsyslogd则监听该文件以获取用户进程的输出。 内核日志在以前的系统上时通过另一个守护进程rklogd来管理的,rsyslogd利用额外的模块实现了相同的功能。内核日志由printk等换树打印至内核环状缓存中。环状缓存的内容直接映射到/proc/kmsg。 rsyslogd通过读取该文件获得内核日志,默认调试信息保存在/var/log/debug,普通信息保存至/var/log/messages,内核信息:/var/log/kern.log。配置文件:/etc/rsyslog.conf,主要设置内核日志输入路径,是否接受UDP日志,及其监听端口(默认514 /etc/services)是否接受TCP日志及其监听端口,日志文件权限,包含哪些配置文件。 2.syslog() 应用程序使用syslog()与守护进程rsyslogd通信。 该函数采用可变参数(第二个参数message和第三个参数。。。)来结构化输出。 priority:设施值 (按位异或) 日志级别。设施值默认:LOG_USER,下面针对默认设施值,讨论日志级别。 2.1下面这个函数可以改变syslog的默认输出方式,进一步结构化日志内容 (1)ident:指定字符串将被添加到日志消息的日期和时间之后,通常设为程序的名字。 (2)logopt:对后续syslog调用的行为进行配置,它可取下列值的按位异或 (3)facility: 用来修改ysyslog默认设施值 此外,日志过滤也很重要,程序再开发阶段可能需要输出很多调试信息,而发布之后,我们又要将这些调试信息关闭,解决这个问题的方法并不是再程序发布之后,删除调试代码(日后可能还会用到),而是缉拿但地设置日志掩码,使日志级别大于日志掩码的日志被系统忽略。 2.2下面这个函数用于设置syslog的日志掩码。 maskpri:指定日志掩码值,该函数始终回成功,它返回调用进程先前的日志掩码值。 2.3关闭日志功能: 02 用户信息 1.UID, EUID, GID, EGID 用户信息对于服务器安全很重要,大多说服务器以root启动, 非root运行 基础知识: 一个进程拥有两个用户ID, UID, EUID, EUID存在的目的是为了方便资源的访问, 它使得运行程序的用户拥有该程序的有效用户权限,比如,su用来更改账户信息,但修改账户时su程序的所有者是root,在普通用户运行su程序时,其有效用户就是该程序的所有者root, 有效用户为root的进程称为特权进程,EGID与EUID类似,下面演示uid, euid区别: 将生成的可执行文件,所有者设置为root,并设置该文件set-user-id标志,然后运行。 从测试输出结果看,进程的uid是启动程序的用户id, 而euid是root。 2.切换用户 03 进程间关系 1.进程组: Linux下每一个进程都属于一个进程组,因此他们除了pid之外,还有进程组ID(PGID)。我们用如下函数获取指定进程组PGID. 成功返回pid, 失败-1,设置errno。 如果pid与pgid相同,则由pid指定的进程别设置为进程组首领:如果pid为0, 表示当前进程的PGID为pgid;如果pgid为0, 则使用pid作为目标pgid。setpid函数成功时返回0, 失败-1, 设置errno。 一个进程只能设置自己或者其子进程的PGID。并且, 当子进程调用exec系列函数后,我们也不能再在父进程中对他设置PGID。 2.会话 (1)一些有关联的进程将组成一个会话, 下面的函数用于创建一个会话: 该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非首领的进程, 调用该函数不仅创建新会话, 而且有如下额外效果。 调用进程成为会话的首领,此时该进程时新会话的唯一成员。 新建一个进程组,其PGID就是调用进程的PID, 调用进程成为该组的首领。 调用进程将甩开终端(如果有) 该函数成功时返回新的进程组PGID, 失败-1, errno。 Linux进程并未提供所谓会话ID的概念, 但Linux系统认为它等于会话首领所在的进程组的PGID, (2)并提供了如下函数读取SID 3.用ps命令查看进程关系 执行ps命令可查看进程,进程组和会话之间的关系。 在bash_shell 下执行ps和less命令,所以ps和less命令的父进程时bash命令,这个可以从PPID(父进程PID)一列看出。 这三条命令创建了一个会话(SID是2962)和两个进程组(PGID:2962, 3102)bash命令的PID,PGID和SID都相同,显然它时会话的首领, 也就是组2962的首领。ps时3102的首领, 04 系统资源限制 Linux上运行的程序都会受到资源限制的影响,比如物理设备限制(cpu数量,内存数量等),系统策略限制(cup时间等),以及具体实现的限制(文件名最大长度)Linux系统资源限制可以通过如下一对函数来读取和设置: getrlimit , setrlimit rlimit 结构体定义如下: 成功返回0, 失败-1, 置errno rlim_t 是一个整数类型,它描述资源级别 rlim_cur 成员指定资源的软限制,建议性的,最好不要超越的限制,如果超越,系统可能向进程发送信号,并终止运行,如果当前进程CPU时间超过软限制,系统将向进程发送SIGXCPU信号;当文件尺寸超过其软限制时,系统将向进程发送SIZEXFSZ信号。 rlim_max 成员指定资源的硬限制。硬限制一般是软限制的上限,普通程序可以减小应限制,而只有以root身份运行的程序才能增加硬限制,此外我们可以使用ulimit命令修改当前shell环境下的资源限制(软/硬)这种修改对该shell启动的所有后续程序都有效,我们也可以通过修改配置文件来改变系统软限制和应限制,而这种修改时永久的。 05 改变工作目录和根目录 有些服务器程序好需要改变工作目录和根目录(web /var/www) 获取当前进程工作目录和改变进程的工作目录的函数: buf参数指向的内存用于存储当前工作目录的绝对路径,size指定其大小 如果当前目录的绝对路径超度(+1 (‘’))超过了size,则getcwd返回NULL,errno:ERANG。 chdir中path指向要切换到的目录。成功0, 失败-1 置errno。 改变进程根目录:chroot chroot并不改变进程的当前工作目录,调用chroot之后,仍需要调用chdir(“/”)来将工作转至新的工作目录,之后原来的文件描述符依然生效。所以可以利用早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(目录). 06 服务器程序后台化 最后,如何在代码中让一个进程以守护进程的防止运行,守护进程的编写遵循一定的步骤,下面一个实例。 实际上,linux提供了完成同样功能的库函数: nochdir:传0则工作目录将被设置为”/”,否则继续使用当前工作目录。 noclose:传0标准输入输出,标准错误输出都被重定向到,dev/null,否则继续使用原来的设备,成功0, 失败-1 置error。 MMCloud15年的IDC运营经验,推出全球海外服务器租用托管、机柜租用、带宽租用、虚拟主机、云主机、CDN等业务,同时提供高防服务器安全服务,欢迎广大客户来电咨询!