Files
2026-07-13 12:34:57 +08:00

137 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多 Worker 模式指南
myboot 0.2.0 起,多 worker`workers > 1`)模式下**每个 worker 进程独立完成组件实例化**:
client / service / controller 都在各自的 worker 进程内创建,不再共享父进程 fork 前的实例。
本文是多 worker 相关能力的一站式说明。
## 启动方式
```python
# main.py
app = create_app(name="my-app", auto_discover_package="app")
if __name__ == "__main__":
app.run(workers=4, app_path="main:app.get_fastapi_app()")
```
```yaml
# conf/config.yaml 亦可
server:
workers: 4
```
无需再手动调用 `auto_discover()` / `apply_auto_configuration()`——框架在每个
worker 内自动完成引导(`bootstrap_worker()`)。
## Worker 信息
```python
app.worker_id # 当前 worker ID,从 1 开始;单进程为 1
app.worker_count # worker 总数
app.is_primary_worker # 是否主 worker(适合"只跑一份"的逻辑)
```
## Client 生命周期
**建连可以直接写在 `__init__` 里**——0.2.0 起 client 在各 worker 内实例化,
不存在 fork 共享连接问题:
```python
@client()
class RedisClient:
def __init__(self):
self.conn = redis.Redis(...) # 安全:本 worker 进程内新建
def close(self): # 可选:定义即获得自动清理
self.conn.close()
```
定义了 `close()` 方法(同步或 async)的 client,框架会在 worker 停止时
`worker_stop_hooks` 之后、`shutdown_hooks` 之前)自动调用。若你也在自己的
shutdown 钩子里关闭,建议把 `close()` 实现为幂等——框架的兜底调用会把二次
close 的异常降为 warning,不影响关闭流程。
## Worker 生命周期钩子
```python
from myboot.core.decorators import on_worker_start, on_worker_stop
@on_worker_start
def warm_up():
"""每个 worker 启动时各触发一次(startup_hooks 之后、调度器启动之前)"""
@on_worker_stop(order=1)
def cleanup():
"""每个 worker 停止时各触发一次(调度器停止之后)"""
```
> Windows 限制:多 worker 下父进程以 `terminate()` 硬终止 worker
> `@on_worker_stop` 与 client 自动 close 在 Windows 上不保证执行;
> 关键清理逻辑不要只依赖它们。
## Primary-first 初始化协调:`run_primary_first`
典型场景:4 个 worker 都去下载同一个 3GB 模型是浪费——应该 primary 下载,
其余 worker 等它完成后直接从本地加载:
```python
from myboot.utils import run_primary_first
from myboot.core.decorators import on_worker_start
@on_worker_start
def load_model():
run_primary_first(
"sasrec-model", # 协调标识(同一标识共享一次协调)
primary_fn=download_and_load, # 仅 primary 执行
secondary_fn=load_from_local, # 其余 worker 等 primary 完成后执行
timeout=600, # 等待上限(秒)
)
```
语义保证:
- `secondary_fn` 缺省时其余 worker 也执行 `primary_fn`(即"等完再做同样的事");
- primary 失败 → 其余 worker 立即抛 `RuntimeError`(含原始异常信息),不会傻等到超时;
- 等待超时 → `TimeoutError`
- 单进程模式退化为直接调用 `primary_fn()`,代码无需分支;
- 基于临时目录标记文件实现,跨 spawn/fork、跨平台(不依赖 fcntl),
但**不支持跨机器**协调。
## 定时任务与多 worker:任务级 `all_workers`
默认所有定时任务**只在 primary worker 执行一份**(防止重复)。但"刷新本 worker
内存态"的任务必须每个 worker 各跑一份,否则其余 worker 永远用旧数据:
```python
@component()
class RefreshJobs:
@interval(hours=24, all_workers=True) # 每个 worker 各执行
def refresh_local_cache(self):
self.items = load_items() # 刷新的是本进程内存
@cron("0 2 * * *") # 默认:仅 primary 执行一份
def daily_report(self):
send_report()
```
怎么选:
| 任务类型 | 配置 | 原因 |
|---|---|---|
| 刷新进程内缓存/内存清单 | `all_workers=True` | 数据在各 worker 自己内存里 |
| 发报表 / 写数据库 / 调外部 API | 默认 | 跑多份会重复副作用 |
全局开关 `scheduler.on_all_workers: true`(所有任务都在全部 worker 跑)仍然有效,
但通常任务级参数是更对的粒度。`scheduler.enabled: false` 全局禁用一切定时任务。
## 服务作用域 `scope`
```python
@service() # 默认 singleton:每个 worker 进程内一个实例
@service(scope="request") # 每个请求(asyncio 任务上下文)一个实例
@service(scope="factory") # 每次注入解析都创建新实例
```
`request` 作用域适合存放请求级上下文,避免单例服务里的共享可变状态。
`@client` 同样支持 `scope` 参数。