最近一段时间我们团队在重构一个系统,这个系统涉及到调用下游的服务,服务提供的协议是下游系统自研的A协议。重构的系统自然是要适配这个协议的。在我们按照我们的认知重构了这个系统后,有两个问题需要解决:
1. 重构的系统性能如何?
2. 如何保证重构的系统和原来的系统所有功能都一致?
对于第一点,方法很简单,压测。下游系统众多,不太可能给我们提供压测的环境。而且这只是验证我们系统,也不必麻烦其他团队。于是外部接口我们计划全用桩。
对于第二点,做全流程对比测试。当相同输入数据同时流经两个系统时,自身系统调用外部系统的参数一致,两套系统外部调用返回的数据一致,同时两套系统的产物一致,就可以保证重构的系统所有功能是一致的。当然还得覆盖全部场景。
本文的重点是第一点,压测桩的实现。后续有时间会写个对比测试的文章,记录一下。
初步方案有了,来看下实际情况和如何实施这个方案。
需求调研
压测和对比测试的优先级我们内部的优先级是先压测,再对比测试,因为已经有手工的功能验证,可以保证整体上没有大的问题,但是性能如果不达标可能会存在方案的变更。一旦出现,可能是更急需解决的问题。
针对压测桩,当时的情况大致是这样的:
- 系统调用外部服务存在近20个server,30个接口;
- 每个接口在适配某个用例时参数是可变的;
- 每个接口使用的协议是A协议,内部传输的数据是pb序列化后的二进制数据;
- 后续还会有其他重构系统,外部调用的接口会增加,但协议是一致的,只是业务数据变了;
- 开发测试完到压测开始的时间不多,只有2-3天时间。
要在2-3天时间内完成支持近20个server,30个接口的压测桩的开发,难度还是有的。由于系统调用外部A的编解码不是我开发的,这里还存在研究学习A编解码的工作。难归难,任务也得完成。
具体方案制定
首先确定了使用go开发,效率高。虽然公司内有trpc-go,但在那时我对trpc-go了解甚少,光学习trpc-go估计就得花费个2-3天。成本太大了。还是自己搞个简单的server来的快些。
其次找相关开发了解的A协议如何实现的。在研究后发现这个协议主体很简单,这个协议将需要调用的接口映射到cmdid(单系统唯一的一个数值),根据cmdid路由到指定server不同的接口。也就是只要有cmdid,就能知道调用的是当前服务的哪个接口。但在不同的server下这个cmdid不是唯一的。
桩需要提供n个server,m个func。如果针对每个server做单独处理,时间上肯定来不及,而且以后有新接口需要接入还得再次开发。要解决一套代码提供n个server,m个func,有两个问题急需解决:
- 多个svr相同cmdid的路由问题。
- 每个接口的pb结构体不同,在不硬编码结构体的情况下如何实现支持多pb数据。
对第一个问题,可以用不同端口来解决。每个端口提供一个server,每个server内就可以保证cmdid的唯一,这样路由的问题就解决了。对第二个问题,在查阅了相关资料后发现,pb是可以使用反射动态获取定义在proto源文件中定义的结构的。那这个问题似乎也解决了。就是传入数据是json,通过反射的方式从proto文件中动态生成这个结构体,然后将json数据映射到这个结构体内,再序列化为二进制数据。这样可以不生成固定的结构体,新增接口也不需要改代码,只需要提供相应的proto文件,指定如何路由就可以快速提供新接口的能力。
编码实施
到这一步已经对当前这个需求如何实现有了十分明确的认知,明确了实现这个需求的方案。剩下的就是一些编码工作了,编码工作相对简单,这里只拆解一下步骤。
- 底层编解码的实现。
- 根据配置路由到具体server的func接口的实现
- 每个请求使用一个协程处理。
编码工作还算顺利,用时1.5天,但30多个接口的配置适配调试用了1天。所以这个需求从接手到完成编码工作正好用时3天,在预期范围内。
实践验证
桩的逻辑比较简单,在性能上应该不是什么瓶颈,最耗时的部分应该是使用反射把json转为pb的过程。于是我用go提供的基准测试,简单测试下json转pb对象的过程。在devcloud机器上单核心测试,在json字节长度4k(实际和这个差不多,可能还会有更长的),可以跑到1w/s。考虑到协程的调度和一些其他的逻辑,在8核机器上应该可以达到5w的并发。桩的性能并不是关键,不要太差就可以了。因此也没在这里做更详细的测试。
一切看着都很顺利,时间符合预期,桩的性能也能达到要求。但实际真的会很顺利吗?
一切准备就绪后上压测环境开始压测,刚开始一两分钟都很正常,桩正确返回了数据,但后来开始存在异常了。我们的服务没有正确收到回包,抛了异常。这个情况也不是每个请求必现,而是大约占1%。最开始以为是我们的服务存在问题,在增加一些日志后排查应该不是程序自身的问题。又通过其他方法验证了,不是程序问题,而是桩的问题。
再来细看一下桩的表现。开始一段时间都是正常的,压力上来后99%的调用都是OK的,1%没有正确回包。这说明主要的逻辑没有问题,问题出在当压力过大时,有一些场景没考虑到。再回顾下具体实现:
- 底层编解码的实现。
- 根据配置路由到具体server的func接口的实现
- 每个请求使用一个协程处理。
从表现上看,第二点是没问题的。第三点可能有问题,因为压力大了,协程变多了,在协程到上限后可能会出现一些没考虑到的情况。而第一点的嫌疑最大。因为我们的程序是按照长链接来调用的。但是在实现编解码的解析帧数据时我是按照短连接的方式处理的。从缓冲区中取数据,取完数据处理完后关闭连接。如果一个缓冲区中存在多个帧数据,后面的数据将不会处理,直接丢弃。
问题改进
现在只是怀疑是这两点产生的,但并不能确定,如果解决了后还是存在问题,那就白白浪费了时间。评估了下如何最快修复问题的方案后决定不在这个简单server上修复,而是使用trpc-go框架。在trpc-go上实现A编解码,将上层的逻辑迁移过去。由于我对trpc-go并不熟悉,因此安排了个组内的大佬K来支持trpc-go的A编解码的工作。我负责迁移上层的svr。
trpc-go app的文档稍详细些,我这块的迁移工作很快完成了。但是编解码的文档似乎就少了。只能对着仓库中的其他编解码来实现。实现的过程还是比较曲折的。各种报错,各种尝试。花了1天的时间才完成编解码的工作。
最终验证
这次使用的是trpc-go框架的能力,因此,1和3的问题应该会一并解决。实际情况也确实如此,再次部署压测环境后,一切正常。可正常压测。桩的性能也和预期差不多,在8核机器上可达到5w/s。
后续的桩相关的新增需求与代码的重构会再起一篇文章,下次再写。
参考文献
内部文章暂不列出。