重构服务的一些想法
重构服务的一些想法
最近对一个服务进行了大重构(不仅仅是代码的重构,还有构建、部署和单元测试等),之前很多实践的经验都应用上了,实践下来效果比较满意。
模块设计
需要明确服务的核心功能
- 执行时机(被谁驱动)
- 执行内容
- 和非核心功能的关系
从模块话的角度看,这三个部分其实都可以独立实现,这样更利于单元测试用例的编写,扎实的单元测试覆盖率大大提高对稳定性的信心。
执行时机一般都是外部驱动,如收到任务、请求甚至内部定时器驱动。
核心功能的执行内容一般不多,但是实现需要严谨,不要轻易放过错误。因为被非核心功能依赖,这部分的稳定可以减少非核心功能不必要的防御性代码,
和非核心功能的关系参考内核的各种 HOOK 实现,核心功能提供 HOOK 点,非核心功能在这些 HOOK 点上被执行,这样各模块就被解藕了。
构建
尽量选择依赖少的三方库,除非没得选(一般是性能要求)。
构建尽量静态链接,如果存在三方依赖但手动实现简单的话,那么可以考虑手动进行实现。
CI 构建的方式应该和手动构建方式并存(前者调用后者),在规模不是非常大的情况下 CI 速度是没有本地编译的速度快的,这样对开发环境的更新可以效率高一些。
三方工具的构建应该持久化构建过程,比如使用 Dockerfile 来保存构建过程,这样有信创这种需求过来一般稍加修改即可。
C++ 尽量使用 cmake,不用写 makefile及可以生成 compile_commands.json。
代码规范
锁只出现在公有函数中,私有函数只实现功能,不考虑资源同步的情况(如果必须控制小粒度另说)。
业务代码优先定义好接口,由外部逻辑调用接口进行驱动,业务直接实现接口即可,接口定义好坏的一个判断参考:后续增加业务无需修改驱动逻辑,新增接口实现业务逻辑就被集成进去。
代码尽量模块化,模块化清晰一个判断参考:能够直接通过单元测试启动这个模块。
函数实现显式化阻塞逻辑,然后交给外部调用决定是否需要启动线程/协程将其放至后台。
Go 的函数参数只放必要参数,比如创建一个命名空间,那么命名空间名称就是关键的参数,超时时间这种就可以设计成 option 实现。
性能
以 profile 为准,不重写标准库函数,一般性能热点不会出现在这些地方,况且标准库也是会进化的(如早期 nginx 优化的 memcpy 早已比不上现在 glibc 的实现了)
优先实现功能,功能收敛后进行 profile,根据热点进行优化,在实现的时候可以使用一些常见的优化,如 C++ 中的 thread_local
,Go 中的 sync.Pool
单元测试
核心功能尽量 100% 覆盖
非核心功能覆盖到关键路径
日志
日志分文件,不同的模块可以通过不同的日志文件进行区分
日志分等级
- 在服务内部没有状态改变的情况,默认等级为 INFO 且日志不会增加,定时器的逻辑不能使用 INFO
- 热点路径的日志只使用 TRACE 等级,并且日志代码语句放在日志等级判断逻辑里面,避免不必要的 CPU/Memory 消耗。
日志可动态调整,应对出现问题又保持现场的场景(生产环境)
部署
支持 systemd/docker 的方式