社交,是游戏玩家的一项基本需求,那么,在游戏中,成熟稳定的聊天系统担负着玩家交流的重要使命。
做为一家从不 996 的游戏创业公司,我们的两款产品《世界争霸》和《农场小镇》都在使用自研的聊天系统。随着在线人数逐渐增多,系统的稳定性和成本面临着更多的考验。于是,升级技术栈势在必行。
至此,核心目标已经出现,以保障性能为前提,同时做到省事和省钱。最终,腾讯云的云函数产品进入了我们的视线。
云函数,无需服务器,省去运维烦恼,只需要关注于业务逻辑代码,可谓省事。按量付费,用多少花多少,避免业务低谷期的资源浪费,可谓省钱。非常适合游戏聊天系统 API 这种复杂度低的中小型需求。
那么接下来我们关注的是,现有系统能不能无缝迁移过去,也就是云函数能不能满足目前所有的特定需求,我们一个一个来说。
原来的 API 部分是采用 swoole 做为底层扩展,部署在腾讯云的 CVM 上,并使用腾讯云的负载均衡来接收外部请求。代码层面则是使用了 composer 进行包管理,一款开源的 easyswoole 框架做为 http 业务的架子。
换用云函数的方案的话,非代码层面就变成了腾讯云 API 网关加云函数来提供服务,而为了方便,依然需要继续使用 composer 进行包管理。原来基于 swoole 的 http 框架无法继续使用,改代码的重点就在这里。
首先就是逻辑入口。我们需要确保用一个云函数来处理所有请求,毕竟云函数个数是有限的,而业务需求是无限的。
那么入口其实就只是一个路由而已,而我们需要做的,就是定义一种简单的路由格式,并在云函数入口代码处得到需要的信息,并转给原有的类进行处理,并返回特定的内容。
以下是一个简单的 url
格式例子:https://url/controller/action?query
只需要解析云函数给出的 path,就能得到 controller 和 action,做一些判断后,调用相应类的方法,然后返回。基于这样的入口,原有的逻辑处理类就可以被调用到了。
其次,需要处理一下原来的逻辑处理类的父类,弃用框架后需要自己来做一个基本功能的父类,比如获取 querystring 内容、解析 body,返回统一格式的返回值等,这里就不细说。
上图中用 php 作为示例,不同的语言,思路都类似,说白了就是个适配问题。 另外代码里还有一些需要改动的地方,比如数据库配置信息,云函数可以用环境变量来传递。比如原来的耗时任务的异步操作,这个后续会说到。
快速发布的能力很重要,因为我们在迁移过程中,会反复得尝试各种东西。那为什么不用本地测试呢?因为进行迁移时云函数本地测试的功能还不支持 PHP。使用 API 网关和云函数的组合时,发布流程是这样子的:
$LATEST
版本$LATEST
版本打一个新版本号这是一个很麻烦的过程。一开始做迁移时,第三步还不支持 API 调用,无法做自动化步骤。当然后来已经支持了这个能力。也有更简单的方案:API 网关直接指向云函数的 $LATEST
版本。然后部署云函数即可。不过这个方案只适合测试阶段,不适合线上阶段的发布。
我们这边是两者结合起来进行。稳定的功能,用稳妥的版本发布流程走。新的功能,新建一个 API 路径,指向 $LATEST
版本,这样随时发布云函数也不会影响线上功能。
这里我们曾遇到过一个坑,就是发布 API 网关时,有时会遇到资源超限的情况。后来查明是因为云函数的并发实例有限,当发布新的 API 版本时,请求会进入新的实例,而旧实例此时还没有释放,两种实例数量相加就会遇到超限的情况。这时候需要向腾讯云申请提高限额才行。
由于是迁移,那原有系统里依然还有许多部分需要继续使用,因此需要云函数可以和原有的内网进行通信。云函数本身支持部署到已有的内网中,因此内网互通很容易做到。
但是此时又遇到了一个坑,由于我们的 API 服务需要向外发出请求,而内网云函数没有访问外网的能力,这时候就需要一个 NAT 网关才可以让云函数访问外网,具体的方案可以参考腾讯云文档,这里不再赘述。
需要注意的是,NAT 网关的外网解决方案下,最好单独给云函数分配一个子网,因为使用已有的子网绑定 NAT 网关,会导致出口 IP 变化。如果该子网下的机器 IP 刚好在某些白名单下,就会造成影响。
以前的日志都是直接落盘,定期压缩转储。而迁移到云函数之后,就需要利用云函数的日志机制了。云函数可以直接投递日志到腾讯云的日志服务中,任何直接输出的信息,都会直接作为日志投递,因此需要规划一下日志内容。
我们这边把函数入口的原始信息、URL 路径、客户端 IP、解析后的参数以及业务日志等等都进行了输出,方便快速定位和查询。另外不需要单独输出一次返回值,云函数自己会打印出来。
还有一点需要注意的就是关于日志索引的问题,开启日志投递后,需要打开索引才能看到日志。如果日志内容中,包含索引分词符,设置索引时要记得从分词符中删除相应的关键字,否则那个内容就被分割了。
其实云函数日志这块还存在着一些不足,比如跟 API 网关的日志是分离的。毕竟 HTTP 的原始入口是 API 网关,这就导致一些问题追踪比较困难。
前文中提到了耗时任务需要改造。这里来细说一下。我们原来的方案是用 swoole 的 task 去处理耗时任务。而由于腾讯云函数的 PHP 环境支持 swoole,于是可以这样改造:
但是这种方案制造出来的进程不归我们管理。经过测试发现,打印出来的日志,跑到其他请求里去了。也不知道这个进程的计时怎么算,会不会暗地里被干掉。所以我们采用了保险一点的方案 —— 消息队列。
腾讯云的消息队列服务有 ckafka,我们封装了一个通用结构的消息体,发给 ckafka,然后 ckafka 会触发另一个云函数(运行着耗时任务的代码)。采用通用结构的好处是,我们可以忽略消息队列的主题,有任何想要异步操作的任务,只需写在被 ckafka 触发的云函数里,然后把要触发的名字和参数发给 ckafka 就行了。
使用 Ckafka 时也遇到过一些小问题:Ckafka 主题默认开一个分区。如果消费速度不理想,可以新增分区试试。此时要记得云函数这边先删一下触发器,再加回来。
这里的配置文件指的不是数据库配置之类比较小的内容。而是需要经常更新的大文本。比如我们是聊天服务,那就会涉及到屏蔽词库,这个文件很大,而且会经常更新。
原来的方案是这样的:配置文件单独有个 git 库,策划提交后,执行 jenkins,然后由 jenkins 上传文件到 cvm,并进行 reload。改成云函数后,没有办法单独上传配置文件了,只能将文件放在代码里,然后步骤变成了策划提交 git,通知程序员,然后程序员发布云函数。这种方式太不优雅了。所以我们最终改成了这种方法:策划提交 git,jenkins 从 git 拿下来往 cos 上传,然后云函数去 cos 拉取。
但这里有个性能问题。就是云函数拉取 cos 这一步可能会慢。因此不能每一个请求,都去拉一次文件。那就意味着需要把一次拉取的内容保存在内存里,但是这样就无法保证实时变更,因为我们无法统一管理云函数的内存。于是我们采用了折中方案,内存中保存文件内容和上一次拉取时间,如果超过 5 分钟,就重新拉取一次。这样可以保证相对的实时性和性能。对于目前的需求来说,也足够了。
就这样,我们迁移过程中的特定需求都可以搞定,迁移工作也就顺利推进下去了。接下来说一下我们迁移到云函数之后带来的一些好处:
腾讯云云函数给我们带来了这么多好处,我们也在盘点,还有哪些功能是可以使用腾讯云云函数的!
最后,我再谈谈自己在云函数使用过程中的一些体会:
第一点,云函数本质上,是拿一部分 CPU 和内存出来帮用户执行一次代码,所以代码的时间复杂度和空间复杂度很重要,我们在使用时一定要优化代码逻辑,否则就会多花钱。
第二点,如果云函数能够由开发者来触发安全杀死旧进程就更好了。这意味着我们可以自己管理内存初始化的时机,可以确保在某一时刻之后,所有实例的内存都是我们想要的状态。可以把它理解为一种重启。
第三点,云函数属于被触发型的服务,目前在进行问题追踪时,源头分散在其他服务上,很难追踪全部流程。这个就需要业界同仁一起的努力啦。
近期,腾讯云云函数又增加了新功能:灰度发布、层(Layer)、默认的高级日志检索等,非常实用,也期待未来更多新特性发布!
我们诚邀您来体验最便捷的 Serverless 开发和部署方式。在试用期内,相关联的产品及服务均提供免费资源和专业的技术支持,帮助您的业务快速、便捷地实现 Serverless!
详情可查阅:Serverless Framework 试用计划
3 秒你能做什么?喝一口水,看一封邮件,还是 —— 部署一个完整的 Serverless 应用?
复制链接至 PC 浏览器访问:https://serverless.cloud.tencent.com/deploy/express
3 秒极速部署,立即体验史上最快的 Serverless HTTP 实战开发!
传送门:
- GitHub: github.com/serverless
- 官网:serverless.com
欢迎访问:Serverless 中文网,您可以在 最佳实践 里体验更多关于 Serverless 应用的开发!
推荐阅读:《Serverless 架构:从原理、设计到项目实战》