Canary:一个关于服务生命周期的小想法
适用读者:用 Python 写后端、写着写着开始琢磨”服务之间的关系该怎么组织”的开发者。本篇聊聊 Canary Framework 是怎么从一个小想法长出来的。
起点:FastAPI 很好,但我还想要一层
最开始我用的是 FastAPI。它设计得很好,也很克制:只做它该做的事。
但用久了,拿它和 Java 的 Spring 生态一对比,会感觉 Python 这边并没有一个统一的框架。FastAPI 有依赖注入,但没有 ORM 的部分;每个项目都要自己把各种库拼起来。
于是我就想,给自己设计一个小脚手架。
很多复杂业务,需要一层业务层来缓冲接口层和服务层之间的依赖关系。在 FastAPI 里做这件事是可以的,但需要预先做一个良好的设计——什么放在哪、谁依赖谁、谁先初始化。这对我来说是一个心智负担,至少我是这样感觉的。
而且在用 AI 写代码的时候,这种”靠约定维持”的结构很容易被 AI 的一些小想法带偏:今天在这里多建一个全局对象,明天在那里多加一段初始化,结构就慢慢散了。
每个服务都有自己的生命
所以我开始想服务之间的关系。它其实和微服务的概念差不多:每个服务都有自己的生命——会初始化,会启动,最后会停止。
1 | from canary_framework import Canary, init, start, stop |
这样一来,只要我知道一个服务的这三个 API——init()、start()、stop()——我就可以管理这个服务了,不需要知道它内部做了什么。
依赖:一个美妙的递归分治
接下来的问题是,服务之间会有依赖。我不可能在一个服务里做太多的工作,所以会让一个服务依赖另一个服务:
1 | from canary_framework import dep |
dep() 声明了依赖,同时 self.database 对类型检查器来说就是 Database,IDE 可以直接补全。
有了依赖,就需要在当前服务启动之前,先启动它的所有依赖。而启动它的依赖,又会重复同样的语义——先启动依赖的依赖,再启动它自己。
**这是一个美妙的递归分治。**我只需要对最上层的服务说一句”启动”,整棵树就会按顺序起来:
1 | async with UserService() as service: |
顺序、循环依赖与并发:一张图
递归分治有三个问题要处理:循环依赖、启动顺序、并发。
我的做法是在启动时维护一张依赖子图:从要启动的服务出发,沿着 dep() 把它依赖的所有服务收集起来。拿到这张图之后:
- 循环依赖在建图的时候就能发现,在任何服务真正运行之前就报错;
- 拓扑排序给出了顺序,自然就可以做到”依赖先启动,自己先回收”;
- 互不依赖的服务可以同时启动,每个服务只需要等自己的依赖完成。
停止是启动的镜像:先停自己,再尝试停依赖。一个依赖可能被好几个服务共用,所以只有在没有人还在用它的时候才会真的停下来。
阶段:不引入状态机
加入并发之后,图中节点的运行顺序已经不可预知了。同一个服务可能被好几条依赖路径同时走到,如果不加处理,它的生命周期就可能被重复执行。
这里我没有引入状态机这种比较重的概念,而是用了一个简单的阶段:每个阶段声明自己的前置阶段。因为生命周期是固定的,所以:
- 前置阶段没执行,当前阶段就不会执行——比如没有
init就调用start,会直接报错,而不是悄悄跳过; - 已经执行过的阶段会被跳过,同一个服务的同一个阶段只会运行一次。
init、start、stop 本身就是三个阶段。这个 API 我也开放出来了,大家可以定义自己的阶段,挂在某个生命周期之后:
1 | from canary_framework import Canary, Phase, enter, init, leave |
失败处理:最谨慎的部分
失败处理是一个程序中最谨慎、最重要的部分,对一个框架来说尤其如此。我要考虑清楚:哪些错误应该暴露给使用者,哪些是框架内部可以捕捉处理的,以及出错之后资源怎么回收。
这里做了几个我觉得挺有意思的机制:
- **失败的服务自己收拾。**一个服务在启动时抛出异常,会立刻执行它自己的
stop,把已经拿到的资源释放掉。 - **依赖不足就不启动。**依赖它的服务不会再启动,并且会释放它们已经占用的依赖。同时在启动的其他服务不会被中途打断,执行完之后,如果没人用了,也会被回收。所以
start()抛出异常的时候,它启动的一切都已经收拾干净了,重试只需要再调用一次。 - **回收不会被打断。**如果启动过程中被取消(比如超时、Ctrl-C),回收仍然会完整执行完,再把取消传递出去。
- **错误如实暴露。**使用者看到的,是自己的钩子抛出的那个异常;同一个失败不会被重复报告,框架内部的异常也不会漏出去。回收过程中如果又出错,会以 note 的形式附在最初的异常上,不会盖住它。
为了验证这些,我用随机化测试生成各种依赖图、各种失败和取消的组合,检查”每个获取的资源都恰好被回收一次”这类性质。
最后
剩下的就是一些整理的工作:测试、性能、文档、示例等等。
1 | pip install canary-framework |
- 文档:https://hotcocoacanary.github.io/Canary-Framework/zh/
- 源码:https://github.com/HotcocoaCanary/Canary-Framework
- 示例(FastAPI 服务、常驻守护进程):https://github.com/HotcocoaCanary/Canary-Framework-Example
这个项目里有大量 AI 生成的内容,代码、测试、文档都有。我已经尽可能地抽时间审阅生成的内容,但肯定还有疏漏,欢迎大家指正。我只是一个新手,也感谢 AI 让我的想法能这么快落地。
如果你对它感兴趣,或者觉得哪里设计得不对,欢迎在 Discussions 里告诉我。谢谢大家。