漫谈游戏服务端优化思路

闲来放假无事,想结合这么些年做游戏的经验,漫谈怎么做服务端的优化。从功能的需求到落地这个角度考虑,优化的可以分为需求层,设计层实现层,语言细节层。如果从资源的角度考虑,优化应该分为网络,cpu,内存,io等。下面分别针对这2个层次来漫谈下优化怎么做。

需求到落地的角度

  • 需求层: 和策划撕逼,减少需求
  • 设计层:理解策划的需求,抽象出好的模型,用合理的数据结构和算法。这个往往也是优化的重点,最常见的是不同容器的选择。
  • 语言细节层:这个具体要profile了,甚至有些优化很反直觉,比如某个排序算法,甚至复杂度N^2比logN要快(由于缓存友好性)等。具体数据要profile来对比,所以优化工具的应用就尤其重要了。

资源的角度

cpu

cpu的优化,实践中首先要有个比较好的工具能找到热点函数,知道不同函数的执行时间,游戏服务端我接触的大多数是c++ 和 lua开发的。这类工具主要思路是2种,一类是插桩类型 第二类是注入或者中端。

插桩需要程序员手工插入宏或者函数的方式计时,这个最灵活,但是对代码侵入很大,而且需要重新编译。而且性能消耗比较大。一般只会在内服环境下压测,排查问题。

hook的方式,替换目标函数的的方法,比如典型的用tcmalloc 排查内存泄露时,就是用tcmalloc LDPRELOAD替换ptmalloc的方式。这种往往不需要编译,但是要重新链接或者说打包。

中断的方式,通过定时产生信号打断程序,获取调用栈,通过概率统计的方式获取profile数据。overhead相对不是那么大,甚至可以挂生产环境去。目前公司的工具overhead大概10%左右的cpu额外消耗,完全可以生产环境监控。这种方式额外的收益还可以排查到死循环,通过gdb attach的当时道理也是一样。

实际开发中,逻辑层也要监控,比如mainloop的tick超时一定要打印出log出来,就可以知道卡顿的频率了,以及卡顿时长,这样的指标供优化时使用。

排查tick超时,首先要构造出重现的场景,以及profile环境的搭建,还需要对业务或者代码深入理解,然后猜测,修改,再次profile跑数据测试。千万不要没有数据支撑的profile,因为就算你猜对了,也不能保证下次可以优化成功。根据profile的数据,发现消耗多的函数,首先应该问自己,这个函数是否真的需要执行吗?或者说运行这么多次或者说运行这么长时间真的合理么??然后带着疑问去猜测,修改,再次profile。优化对方式一定是修改几行代码,跑下看数据在改这样的方式,而不是一下修改一大堆代码在跑。

按照经验来说,大多数cpu瓶颈在AOI/网络/寻路,这些部分发现瓶颈经验性的办法就是用c++来重写。有些逻辑如果消耗重了,如果可以放到客户端放客户端的跑。脚本,这里不谈具体某一种脚本,大多数脚本想提高性能可以jit化。如果该做的都做了,用户实在太多,当然要保证架构可以水平拓展,尽量通过堆机器可以线性的提高用户数,消除系统中的单点。

如果想提高单台机器的性能,可以考虑逻辑多线程化,多线程模型比多进程模型好的地方在于可以提高线程之间的通信效率,更方便做资源的共享和交换。良好的多线程设计应该是,极少的资源竞争,自动的任务调度,以及较好的伸缩性。比如unity的job system就是个例子,只要描述好job之间的依赖性关系,框架自动调度。skynet的actor模型也是很好的例子,单个actor内部串行,不同的actor可以在不同的线程上运行。

适合做并发

  • 网络消息的收发(序列化/压缩/加密)
  • aoi/寻路等其他和其他模块耦合较低的任务

其实也不是说用了多线程一定就是快,lock-free也不代表就是快。多线程同步会引入lock带来额外消耗,还有原子操作对cache不友好。所谓的lock-free队列正常情况下比普通队列慢一个数量级。还是那句话,还是得看profile数据说话。

内存

内存不足99%的情况都是泄露导致的,线上一旦发生内存泄露相当难查,内存泄露大多数并不是简单的new了后,忘记delete了。而是逻辑代码编写过程中,把逻辑上明明应该释放的内存,一直被某个全局对象引用者。我排查这种问题,首先挂个tcmalloc上去,在两采样点做快照对比内存的diff的增量函数去推测可能潜在的泄漏函数。脚本层排查也是类似的思路。

脚本gc相关问题,lua的自动gc其实挺不好控制,我觉得对延迟敏感的进程大多数时候应该关闭gc,根据业务的需求来调度gc,最简单的是凌晨人少的时候,手动gc。还有不同进程轮流gc,gc的时候,避免玩家在这个进程。

内存泄露排查相关工具:tcmalloc,valgrind

还有一些其他方法来减少内存占用

  • 策划data,使用c++紧凑型的数据结构存储,或者存储在数据库里,让数据库帮忙做LRU,或者放入共享内存里。
  • 开启KSM,内存自动合并相同的页,并且copy-on-write。对使用者透明。

其实正常写很难把内存撑爆,核心就是持续监控,及时发现(通过svn版本对比很容易发现)。

网络协议

核心问题主要是延迟和流量
延迟的产生以下几个步骤

  • 硬件+os延迟
  • 引擎层消息处理延迟
  • 脚本层逻辑的处理延迟(客户端+服务端)
  • 网络模块收发延迟(客户端+服务端)
  • 消息包传输延迟
    首先要确定好延迟具体哪一层才好优化,而不是一有问题就让网络协议接锅。
    游戏一般采用状态同步或者帧同步的方式
状态同步
  • 服务端跑战斗计算较重
  • 下行发状态,流量较高
  • 断线重连容易做,恢复状态快照即可
  • 天然防作弊(也不是绝对,很多游戏为了手感,客户的先行,信任客户的的位置)
帧同步
  • 战斗跑客户端
  • 服务端很轻,只是转发个cmd(没有验证端的情况)
  • 流量较小
  • 断线重连要追帧,较慢。
  • 开发简单(扯,不同步起来查死你)

底层协议tcp还是udp,如果追求低延迟底层协议要用udp,对于移动cmd这种不需要可靠的操作,只需要保序就可以了,可以允许丢包,纯udp都可以。现在主流都是采用基于udp的kcp协议。tcp设计是为了整个网络考虑的,所以指数退避,以及慢启动等对游戏本身并不友好的设计。kcp通过冗余包的方式,提高流量对抗丢包来减少延迟,《王者荣耀》就是通过3倍的流量来对抗丢包。
不可靠包传输也有劣势,无法采用流式压缩,消息包如果比较大,由于mtu限制,会被拆分多个包在链路层,消息包越多,送达率也越低。
状态同步也好,帧同步也好,如果要降低流量,就要减少小包的发送次数,减少包头占比,提高有效负载率。帧同步的命令比较简单,往往有效负载率不到20%,如果合并一些指令,就可以大大减少流量。同理状态同步中,位置的同步是个高频的操作,也是个流量大头,想办法合包非常必要。
流量相关工具:NetworkEmulation(模拟延迟,控制丢包率),360手机助手(查看手机流量消耗),Wireshark(抓包工具)

注意事项

开发过程中也要主要避免过早的优化,和过度优化,尤其服务端,达到可用的层度就可以了,机器成本根本比想象的低很多,只要有玩家不怕没机器。这点和客户端不同,客户端优化好一点,就能覆盖更多的机型,可以支持更多的用户。性能优化开发后期后,应该持续监控,以及把当成日常任务一样来做。因为大多数性能问题都可以对比svn的版本记录可以排查出。避免过早优化的原因的优化会掣肘开发进度。

你可能感兴趣的:(漫谈游戏服务端优化思路)