测试用例存储方案的演进
MVP阶段:文件存储
做测评功能的时候遇到个需求,需要生成大量测试用例,每个用例都是json格式。
最开始MVP版本时,后端和算法工具部署在一起,直接用命令行调用。算法工具生成的测试用例就直接存成json文件,丢到指定目录里,后端读文件列表就完事了。
结果很快就踩坑了。前端要分页展示,文件名虽然是按自增编号命名的,但读出来的顺序却是乱的。没办法,只能在后端加个list,把文件名都读进来排个序,然后再按这个顺序分页返回。
V1.0:gRPC + 关系型数据库
产品要升级到分布式架构了,后端和算法服务得分开部署,各跑各的容器,中间通讯采用的微服务主流的gRPC协议。这种情况下结果json文件存储的方案就不行了,改成了直接通过gRPC把测试用例数据返回给后端。后端拿到数据后得存起来。
当时的做法是在MySQL的执行记录表里加了个字段,把所有测试用例的json数据都塞进去。因为实际使用时有可能一次执行生成几千上万个用例,不可能一个用例一条记录,那表就爆了。
这方案能用,但有个明显的性能问题:前端分页查询的时候,后端得先把整个字段的内容读出来,解析成json,然后再截取对应页的数据返回。测试用例一多,这个开销就很明显了。每次查询都要把几万条数据全读出来解析一遍,性能问题很大。
V2.0:MongoDB文档数据库
性能问题摆在那,得想办法解决。解决方案就是用MongoDB来存测试用例,文档型数据库天生就适合存这种json数据,而且集合的概念正好符合我们的场景。
一次执行对应一个集合,每个测试用例就是集合里的一个文档。这样分页查询就简单了,直接用MongoDB的skip和limit,不用再把所有数据都读出来。性能提升立竿见影。
后端调用算法工具的演进历程
MVP阶段:命令行调用
最开始的MVP版本,后端调用算法工具用的是最简单粗暴的方式:Go后端直接执行命令行,调Python程序。
这种方式实现起来快,但限制也很明显——后端和算法工具必须部署在同一个环境里,而且得提前配好Python的依赖环境。不过对于快速验证产品可行性来说,够用了。
V1.0:gRPC服务化(踩坑之旅)
产品要做分布式了,后端和算法工具得能分开部署。中间通讯采用的微服务主流的gRPC协议,算法工具改造成长期运行的gRPC服务。
环境是隔离了,但新问题又来了:算法工具得支持多任务并行。按这个思路走下去,就得在gRPC服务端实现一套任务管理功能。
当时确实是这么干的,过程中还踩了个大坑:算法同学之前写命令行工具的时候,有些类用了单例模式。改成服务后,第二次、第三次执行的结果居然都和第一次一样。排查了半天才发现是单例的锅,状态没清理干净。
多任务并行是实现了,但又暴露出一个更严重的问题:长连接不稳定。
因为整个调用流程还是沿用MVP的思路,一次调用就是一次完整的任务执行,中间一直保持着gRPC长连接。如果网络抖动导致连接断了,后端这边任务就中断了,但算法服务那边还在空跑着,最终的结果不会返回给后端储存下来。
由于当时因为项目比较紧急,没时间去做架构改动,所以为了解决这个问题,临时加了两个机制:
- 加了keepalive保活机制
- 在算法服务端加了个逻辑:连接断开就自动停止任务,正好也能对应上手动停止的需求——后端只要关闭连接就行
但这治标不治本啊。手动停止确实需要停,但网络问题导致的断开不应该停任务。
更要命的是,后来产品又提了个新需求:长时间稳定性测试,要求算法工具能稳定运行超过72小时。这下长连接的问题就彻底绕不过去了,必须得想个根本的解决方案。
V2.0:两种解决方案的权衡
为了解决长连接的问题,我想了两个方案。
方案一:完整的任务管理系统
这个方案的思路是让算法工具自己实现一套完整的任务管理,把gRPC接口从原来的单一执行接口扩展成:
- 任务启动接口
- 任务停止接口
- 任务状态查询接口
每个任务有个唯一的taskID。如果网络断了,后端只要拿着taskID重新连接,就能继续获取任务的实时日志和状态。
听起来挺完美的,但仔细一想,这相当于让每个算法工具都得开发一套后端系统。算法同学本来只需要关注算法逻辑,现在还得处理任务调度、状态管理、并发控制这些工程问题。开发和维护的工作量都不小,而且容易出bug。
最后没采用这个方案。
方案二:容器化任务(最终方案)
灵感来源
这个方案源自于Kubernetes的Job/Pod模式:一个任务对应一个Pod,任务结束Pod就销毁。这不就是天然的任务管理模型吗?
K8S的设计理念给了我启发:
- 一个Job对应一个任务,执行完成后Pod自动清理
- Pod有独立的生命周期,不依赖外部连接
- 通过API可以查询Pod状态、获取日志
- 任务失败可以自动重试,成功则标记完成
这个思路完全可以先用Docker实现,未来再平滑迁移到K8S。我们的任务管理问题,本质上不就是容器管理吗?
如果把算法工具改回命令行形式,一个任务对应一个容器实例:
- 任务管理?用Docker API就行
- 并发控制?每个容器独立运行,天然隔离
- 状态残留?容器销毁,一切清零
- 网络断开?容器继续跑,后端重连继续收集日志
这个思路一下子就打通了,我们不需要自己实现任务管理系统,Docker已经帮我们做好了。
方案设计
基于这个思路,最终的方案是:把算法工具改回命令行形式,一个任务对应一个容器实例。
具体实现:
- 环境彻底解耦:不管在哪部署,只要有Docker就能跑
- 一个任务一个容器:后端收到请求后,根据任务类型拉取对应镜像,启动一个独立的容器实例
- 生命周期绑定:任务结束,容器自动销毁,资源立即回收,不会有状态残留
- 后端统一管理:容器的创建、监控、日志收集、销毁全由后端负责
最关键的是,网络断开不影响任务执行。容器在那跑着,后端断线重连后继续收集日志就行。算法同学也不用操心任务管理、并发控制这些工程问题,专心写算法逻辑就好。
这个方案既解决了长连接的问题,又降低了系统复杂度,还把任务管理的能力从算法服务转移到了更成熟的Docker平台上。