我之前帮朋友排查过一个小型游戏服的卡顿问题,高峰期用户在线不到两千,服务器负载就跑到九成以上,操作响应经常延迟好几秒。很多人第一反应是加机器升配置,但调整之后问题还是反复出现。最后查下来,问题都出在几个很基础的配置上,改完之后负载直接降到三成,运行一直稳定。

很多做后端开发的朋友第一次接游戏服的运维工作,都会把注意力放在业务逻辑优化上,这不能说错,但很多隐藏的瓶颈并不在业务层。游戏服务和常见的web服务运行逻辑有很大区别,照搬web服务的配置经验,很容易踩坑。

基础配置的常见误区

第一个最容易踩的坑,就是文件描述符限制没有调整。Linux系统默认对每个进程能打开的文件描述符数量做了限制,默认值一般是1024。很多人不知道这个限制,部署的时候从来没改过。

游戏服务大部分用长连接和客户端通信,每个TCP连接都会占用一个文件描述符,加上程序打开的日志文件、数据库文件,总用量很容易超过默认限制。平时在线用户少的时候,连接数只有几百,不会出问题,一到高峰期,在线人数上来,新连接申请不到文件描述符,就会被直接拒绝,老连接也容易因为资源不够出现超时。

我之前接触的那个卡牌游戏断线问题就是这样,开发把限制改成了4096,觉得足够用了,没想到周末高峰期在线到三千八,加上数据库连接、程序打开的临时文件,总用量超过了四千,直接触发限制,新玩家进不来,老玩家不断断线重连。改到16384之后,这个问题就再也没出现过。

第二个常见误区是日志级别没有调整。很多开发在开发测试阶段开了全量debug日志,方便排查问题,上线的时候忘记改回info级别,就直接跑起来了。很多人觉得不就是打几行文字,能消耗多少资源,实际上高峰期的时候,每秒几千甚至几万行日志写入,对磁盘IO的压力比想象中大很多。

我之前帮着排查的那个卡顿案例,就是debug日志没关,高峰期每秒要写四万多行日志,用的是普通机械盘,IO利用率直接跑到100%。不管是游戏服务写数据,还是数据库读存档,都要等IO空闲,整个链路的延迟都上去了,用户操作当然会卡。把日志级别改成info之后,每天的日志量从几十G降到两三G,IO利用率直接降到10%以下,卡顿问题马上解决了。

更麻烦的是,如果日志和游戏服务、数据库放在同一个磁盘分区,日志占满IO会引发连锁反应,数据库读写变慢,业务逻辑处理变慢,最后整个服务都会被拖垮。这种问题平时不明显,一到用户量上来就马上暴露,排查的时候很容易没想到是日志的问题。

第三个误区是内存配置不合理。不管是用Java还是C++开发游戏服务,内存配置不对都会出大问题。用Java的开发朋友,经常走两个极端,一种是把堆内存设得太小,高峰期对象创建多了,频繁触发GC,尤其是Full GC,一次停顿就是好几秒,用户操作之后半天没响应,直接就掉线了。另一种是把堆内存设得太大,比如8G内存的物理机,直接给堆分了6G,剩下的内存留给操作系统和页缓存,不够用了就会用到swap分区。swap是用磁盘模拟的内存,读写速度比物理内存慢几百倍,一旦用到swap,整个服务的响应速度会直接掉到不能用的程度。

C++开发的服务虽然没有GC的问题,但很多人不会主动回收空闲连接的内存,大量玩家退出之后,连接对应的状态数据一直留在内存里,慢慢积累,最后把内存占满,触发系统OOM把进程杀掉,直接就是服务宕机的大问题。

第四个误区是连接池和空闲连接配置不对。游戏服务需要和数据库交互,大多数用连接池复用连接,很多人按照web服务的经验,把连接池大小设成十几或者二十,觉得足够用。web服务是短连接,请求处理完就释放连接,复用率高,游戏服务不一样,高峰期大量玩家同时上下线,都要读写数据库,连接不够用的时候,请求就要排队,排队时间太长就超时,玩家就卡在上不了线。

也有人走到另一个极端,把连接池大小设成好几千,觉得连接越多越好,其实数据库本身对连接数也有限制,太多连接同时跑,会导致数据库上下文切换开销变大,整体性能反而下降,同样会拖慢整个服务。

还有空闲连接的超时问题,很多人把超时时间设得太长,甚至不设超时。很多用户退出游戏的时候,直接关掉客户端,不会给服务端发退出消息,服务端就会一直保持这个连接,占着文件描述符和内存。如果超时时间设成几个小时甚至一天,积累下来,几千个空闲连接会占走很多资源,高峰期新连接进来就没有资源可用了。

排查问题的基本顺序

遇到游戏服卡顿、连接异常的问题,很多人上来就改业务代码,查了好几天什么都没发现,其实按照我自己的经验,百分之七八十的问题都出在系统层和基础配置上,先查基础配置再看业务,能省很多时间。

第一步先看文件描述符的使用情况,先看系统配置的最大限制是多少,再看当前进程已经用了多少,剩下的余量够不够应付高峰期。很多人改了配置之后忘记重启服务,配置根本没生效,自己还不知道,白查半天。

第二步看系统的整体负载和CPU使用率,先看是不是整体CPU占满了,再看有没有单个核心占满的情况。有些单线程处理的模块,比如消息广播,高峰期一下子堆了很多消息,会把单个核心占满,整体CPU使用率看起来不高,但服务已经卡了,这种情况很容易漏看。

第三步看内存和swap的使用情况,先看总内存用了多少,有没有用到swap,swap的使用率有多高。如果swap使用率超过10%,基本就能确定内存配置不对,要么调整堆大小,要么加物理内存,这个问题解决起来很快。

第四步看磁盘IO的利用率和读写延迟,哪怕是固态硬盘,持续满负载IO也会出问题,机械盘就更不用说了。如果IO利用率长期超过90%,就要找是什么东西在占IO,最常见的就是日志、还有数据库的频繁写入,找到之后调整就行。

第五步看TCP连接数,统计一下不同状态的连接有多少,特别是ESTABLISHED状态的连接,看看是不是有大量的空闲连接没有释放。如果连接数比当前在线用户数多很多,基本就能确定空闲超时配置不对,需要调整超时时间,主动回收空闲连接。

这些步骤走完,大部分常见问题都能找到原因,真的找不到了再去查业务逻辑和代码,效率会高很多。我自己排查过的五六个游戏服问题,只有一个是业务逻辑的死循环导致的,剩下的全都是基础配置的问题。

上线前的检查要点

很多小团队做游戏,大部分精力都放在玩法开发和美术资源上,对服务端的基础配置不重视,上线出问题之后才慌着排查,其实只要花一两个小时,按照清单检查一遍,就能避免大部分常见问题。

首先要检查的就是文件描述符限制,根据预估的最大在线人数,把限制调到预估量的两倍以上,留够余量,避免突发的用户增长超过限制。改完配置一定要重启服务,确认配置生效。

然后检查日志级别,线上环境一定要关掉debug日志,只保留info以上级别的日志,要是担心出问题找不到日志,可以做滚动日志,按大小或者按天切割,避免单个日志文件太大占满磁盘空间。还要注意不要把日志存在系统盘,避免日志把系统盘占满导致整个服务器宕机。

接下来检查内存配置,根据服务器的物理内存大小调整,给操作系统和页缓存留至少两成以上的内存,不要把堆设得太大,也不要设得太小,根据压测的结果调整到合适的范围。如果是C++开发的服务,一定要做好空闲连接的内存回收,定期清理不用的状态数据。

然后检查连接池和空闲连接配置,连接池大小根据压测的结果调整,一般来说,按照数据库的CPU核心数调整,大概是核心数的两到三倍就足够应付大部分场景,不用设太大,也不要太小。空闲连接的超时时间,一般设个十几分钟到半小时就足够,太长会积累资源,太短会频繁重建连接,影响性能。

最后就是压力测试,上线之前一定要模拟预估的最大在线人数跑一段时间,看各个指标有没有异常。不用太复杂的工具,能模拟并发连接和基本操作就行,重点看连接数上去之后,各个基础指标有没有超过阈值,有没有出现连接拒绝、响应延迟飙升的情况。很多问题只有在高峰期才会出现,提前压测就能找出来,不用等到用户进来了才出问题。

从实际接触过的案例来看,小型游戏项目出的性能和稳定性问题,绝大多数都是这些基础的配置问题,不是什么复杂的架构问题,只要做好提前检查,就能省很多后面排查的时间。很多人觉得这些细节不重要,反正能跑就行,结果一到高峰期就出问题,反而要花更多时间去排查,还影响用户体验。

总的来说,游戏服务的运维和性能优化,核心是要匹配长连接的运行特点,不能直接照搬web服务的经验,多注意基础配置的调整,提前做好检查和压测,大部分问题都是可以避免的。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐