mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-08-30 12:45:32 +08:00
docs(timer): 定时器动态管控技能文档补充四章节
新增并发数动态调整、手动触发定向、多节点协调、clamp 有效域钳制四章节,含 12 个示例对照与 4 个坑位说明。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -5,11 +5,12 @@ description: "内置秒级定时器(php think timer)的使用与扩展规范
|
||||
|
||||
# timer(内置秒级定时器)
|
||||
|
||||
## 核心机制(你要记住的 3 件事)
|
||||
## 核心机制(你要记住的 4 件事)
|
||||
|
||||
1) 任务配置统一从 `app_file_path('common/command/timer/config.php')` 读取
|
||||
2) 每个配置会按 `concurrency` 自动展开成多份任务实例,并自动注入 `concurrency_id` / `concurrency_count`
|
||||
3) “是否该执行”主要由定时器侧的 Cache 节流控制(可选叠加控制器侧的防刷保护)
|
||||
1) 任务配置统一从 `app_file_path('common/command/timer/config.php')` 读取
|
||||
2) 每个配置会按 `concurrency` 自动展开成多份任务实例,并自动注入 `concurrency_id` / `concurrency_count`;`concurrency` 支持后台动态覆盖(DB 字段为 NULL 时继承代码默认,详见「动态控制 > 动态并发机制」)
|
||||
3) "是否该执行"主要由定时器侧的 Cache 节流控制(可选叠加控制器侧的防刷保护)
|
||||
4) 进程运行期间支持热加载:后台改并发、手动触发某节点跑一次,都无需重启 `php think timer`(详见「动态控制」章节)
|
||||
|
||||
相关实现入口:
|
||||
|
||||
@@ -209,6 +210,9 @@ site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_do
|
||||
| `timeout` | `86400` | 请求响应超时时间(秒) |
|
||||
| `max_handles` | `100` | curl multi 最大并发句柄数 |
|
||||
| `select_timeout` | `0.001` | curl_multi_select 超时(秒) |
|
||||
| `force_reload_interval` | `15` | 热加载兜底 reload 间隔(秒),防 cache 丢失导致脏标记漏检;`.env` 用 `TIMER_FORCE_RELOAD_INTERVAL` 覆盖 |
|
||||
| `trigger_ttl` | `300` | 手动触发 TTL(秒),超时未消费自动复位 `manual_trigger`;`.env` 用 `TIMER_TRIGGER_TTL` 覆盖 |
|
||||
| `drain_max_lifetime` | `86400` | drain 实例最长存活(秒),超时强制移除防永不 drain;`.env` 用 `TIMER_DRAIN_MAX_LIFETIME` 覆盖 |
|
||||
| `clear_log_days` | `3` | ClearLog 任务清理 debug_log 表的保留天数,支持从 `.env` 的 `TIMER_CLEAR_LOG_DAYS` 覆盖 |
|
||||
|
||||
## 常见坑位(快速自检)
|
||||
@@ -217,6 +221,10 @@ site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_do
|
||||
- `concurrency` 与控制器侧 `$concurrency` 不一致:会触发 `concurrency id/count error`
|
||||
- 只依赖控制器侧 `$frequency`:它只是防刷,不是调度;调度频率以定时器侧 Cache 节流为准
|
||||
- 本地开发忘记 `--local`:`site` 任务默认请求 `sysconfig('site','site_domain')` 指向的生产域名,不带 `--local` 会直接打到线上
|
||||
- **concurrency 设成 0 或负数**:会被有效域钳制为 1 并打 `[timer] clamp` 告警日志(防静默吞任务)。后台 UI 也会拒绝 0,想"继承默认"应该留空(写 NULL),而不是填 0
|
||||
- **手动触发依赖 timer 进程在跑**:手动触发走的是定时器调度链路(写 `manual_trigger=1` 等 timer 消费),不是后台直接同步调控制器。如果 timer 进程没启动,触发请求会一直挂着,直到 `trigger_ttl`(默认 300 秒)超时自动复位。所以触发前先确认 `php think timer` 在运行
|
||||
- **缩容 drain 有延迟**:缩容时多余实例不会立刻消失,要等在飞请求完成(`pending` 里没有该 key)才移除。极端慢的任务 drain 会很慢,`drain_max_lifetime`(默认 86400 秒)是兜底上限,超时强制移除。需要立刻见效只能重启 timer 进程(但重启会放弃所有 drain 连续性)
|
||||
- **新增 `system_timer_config` 字段后必须清 `runtime/cache/`**:ThinkPHP ORM 有字段 schema 缓存,即使 `fields_cache=false`,某些路径仍可能缓存旧字段列表。新增字段(如 `concurrency` / `trigger_node_id`)后不清缓存,`Db::name('system_timer_config')->update(...)` 会报 `fields not exists`。`php think clear --type=cache` 或手动删 `runtime/cache/` 即可
|
||||
|
||||
## 多节点协调
|
||||
|
||||
@@ -239,7 +247,7 @@ site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_do
|
||||
| `auto`(默认) | 两阶段 DB 行锁竞争:BEGIN / SELECT FOR UPDATE / UPDATE / COMMIT 抢占,抢占成功后释放锁再执行。每个频率窗口内只有一个节点执行 | 通用场景 |
|
||||
| `main` | 仅主节点执行 | 需要集中处理的任务 |
|
||||
| `all` | 所有节点各自独立执行 | 节点本地清理等 |
|
||||
| `manual` | 仅当 DB 中 `manual_trigger=1` 时执行,执行后自动重置为 0。通过管理后台触发 | 运维手动触发 |
|
||||
| `manual` | 该类型的任务**永不自动执行**,执行全靠后台手动触发(`manual_trigger=1` 时被消费)。注意:手动触发现在跨 `run_type` 通用,任何 `run_type` 的任务都能在后台手动触发一次,`manual` 类型只是"只接受手动触发"的极端形态(详见「动态控制 > 手动触发升级」) | 仅手动触发的任务 |
|
||||
|
||||
### 节点注册与主节点管理
|
||||
|
||||
@@ -249,12 +257,13 @@ site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_do
|
||||
|
||||
### 自动机制(开发者无需手动调用)
|
||||
|
||||
- **配置同步**:定时器启动时 `TimerService::syncConfigToDatabase()` 将 PHP 配置同步到 `system_timer_config` 表,不覆盖 `run_type`、`status` 等管理字段(以 UI 设置为准)
|
||||
- **配置同步**:定时器启动时 `TimerService::syncConfigToDatabase()` 将 PHP 配置同步到 `system_timer_config` 表,不覆盖 `run_type`、`status`、`concurrency` 等管理字段(以 UI/DB 为准)。新任务进表时 `concurrency` 留 NULL 表示继承代码默认
|
||||
- **热加载**:后台改 `concurrency` 或手动触发时,`TimerConfig` 控制器在 DB commit 成功后递增脏标记 cache key `timer_config_version`;`runLoop` 每轮检测该标记变化即触发 reload(按 task 粒度合并 diff,不全量重建)。另有每 15 秒的兜底 reload 防 cache 丢失。详见「动态控制」章节
|
||||
- **执行日志**:`TimerControllerBase::execute()` 自动包裹 `do()` 并调用 `logStart()` / `logEnd()`。配置中写 `/do`,运行时 `TimerServiceBase` 自动重写为 `/execute` 以触发日志包裹。`host_id` 会自动注入到 site 任务的 URL 参数中
|
||||
|
||||
### 相关数据表
|
||||
|
||||
- `ul_system_timer_config`:任务协调配置(run_type、status、manual_trigger 等)
|
||||
- `ul_system_timer_config`:任务协调配置。核心字段:`run_type`、`status`、`manual_trigger`、`concurrency`(nullable,NULL=继承代码默认,正整数=覆盖)、`trigger_node_id`(手动触发定向节点)、`last_trigger_time`(触发时间戳,配合 TTL 自动复位)、`last_execute_node` / `last_execute_time`(auto 模式竞争用)
|
||||
- `ul_system_timer_log`:执行日志记录
|
||||
- `ul_system_host`:节点注册信息(含 `is_master` 字段标识主节点)
|
||||
|
||||
@@ -262,10 +271,273 @@ site 任务会按站点域名发起请求,默认从 `sysconfig('site','site_do
|
||||
|
||||
| 页面 | 路径 | 功能 |
|
||||
|------|------|------|
|
||||
| 定时器配置 | `/admin/system.timer_config/index` | 管理 run_type、status、手动触发 |
|
||||
| 定时器配置 | `/admin/system.timer_config/index` | 管理 run_type、status、手动触发、动态并发(含节点叠加视角),详见「动态控制 > 节点中心 UI」 |
|
||||
| 定时器日志 | `/admin/system.timer_log/index` | 只读执行日志查看 |
|
||||
| 主机列表 | `/admin/system.host/index` | 节点状态、主节点管理 |
|
||||
|
||||
## 动态控制(动态并发 / 手动触发升级 / 节点中心 UI)
|
||||
|
||||
这一章覆盖进程运行期间的动态能力:后台改并发实时生效、对任意任务手动触发一次来调试、按节点视角管理多节点执行情况。核心入口都在 `extend/base/common/command/TimerBase.php` 和 `app/admin/controller/system/TimerConfig.php`。
|
||||
|
||||
### 动态并发机制(后台可调 + 热加载)
|
||||
|
||||
#### 1. concurrency 优先级链
|
||||
|
||||
`concurrency` 的生效值按下面的链路解析,框架升级改默认值能自动惠及没定制过的任务:
|
||||
|
||||
```
|
||||
代码基线(timer/config.php 的 concurrency)
|
||||
↓ 被覆盖
|
||||
DB system_timer_config.concurrency(nullable)
|
||||
↓ NULL 表示"继承代码默认"
|
||||
↓ 正整数表示"覆盖代码默认"
|
||||
生效值(再经下面的有效域钳制)
|
||||
```
|
||||
|
||||
- DB 字段为 `NULL`:继承代码默认(存量任务、新任务进表时都留 NULL,向后兼容)
|
||||
- DB 字段为正整数:覆盖代码默认
|
||||
- DB 字段为 `0` 或负数:会被钳制为 1 并告警(防静默吞任务,见常见坑位)
|
||||
|
||||
#### 2. 热加载机制
|
||||
|
||||
后台改 `concurrency` 或触发手动执行时,`TimerConfig` 控制器在 DB commit 成功后递增脏标记:
|
||||
|
||||
```php
|
||||
// app/admin/controller/system/TimerConfig.php 的 trigger() / edit()
|
||||
$row->save([...]);
|
||||
Cache::inc('timer_config_version'); // 必须在 commit 成功之后(D18,防脏读)
|
||||
```
|
||||
|
||||
`runLoop` 每轮检测这个标记变化:
|
||||
|
||||
```php
|
||||
// extend/base/common/command/TimerBase.php 的 runLoop()
|
||||
$current_version = Cache::get('timer_config_version', 0); // 故意不打 tag,防被 Cache::tag()->clear() 连带清掉
|
||||
$force_interval = (int) Config::get('timer.force_reload_interval', 15);
|
||||
if ($current_version !== $this->lastVersion || (time() - $this->lastForceReload) >= $force_interval) {
|
||||
$effective_map = $this->computeEffectiveConcurrency(); // 钳制有效域
|
||||
$this->reloadRequestList($effective_map); // 合并式 reload,不全量重建
|
||||
$this->checkTriggerTtl(); // TTL 复位检查
|
||||
$this->lastVersion = $current_version;
|
||||
$this->lastForceReload = time();
|
||||
}
|
||||
```
|
||||
|
||||
两层探测:
|
||||
- 脏标记(实时):后台一改,下个 tick 就 reload
|
||||
- 兜底 reload(每 15 秒):防 cache 丢失导致脏标记漏检,最坏 15 秒内也能追上
|
||||
|
||||
> 多节点前提:脏标记跨节点传播依赖共享 cache(Redis)。文件 cache 下节点间互不可见,只能靠每 15 秒兜底 reload 兜底。生产多节点务必用 Redis cache。
|
||||
|
||||
#### 3. 有效域钳制
|
||||
|
||||
reload 时对每个任务的 concurrency 三重钳制:
|
||||
|
||||
```
|
||||
effective = max(1, min(DB 值 ?? 代码值, 控制器 $concurrency cap, 剩余 max_handles 预算))
|
||||
```
|
||||
|
||||
- 控制器 cap:`TimerControllerBase::$concurrency` 现在是"最大并发上限"语义(放行 `concurrency_id < cap` 的所有分片),DB 实际并发不会超过它
|
||||
- max_handles 预算:全局 curl multi 句柄上限(默认 100)减去其他任务已占的 active 实例数
|
||||
- 超限(`effective != raw`)会写 `[timer] clamp task={name} db={v} effective={n}` 告警日志
|
||||
|
||||
#### 4. 优雅增缩容
|
||||
|
||||
reload 用**合并**而非**重建**(保留其他任务的 draining 实例,不影响其 drain 连续性):
|
||||
|
||||
- 扩容:append 新实例,`concurrency_id` 从该任务现有 `max(id) + 1` 续编(保证 id 单调递增不重用)
|
||||
- 缩容:把超出的实例从高 id 开始置 `state=draining`,停止发新请求,保留在结构中等在飞请求完成
|
||||
- 缩容时清掉被 drain 实例的节流 cache key(`timer_request_{name}_{id}`),防下次扩容回原值被旧频率窗口误判跳过
|
||||
- drain 最长存活上限(`drain_max_lifetime`,默认 86400 秒):draining 实例超过该时长强制移除,防任务卡死导致永不 drain
|
||||
|
||||
drain 完成的判定与移除发生在 `runLoop` 遍历时:
|
||||
|
||||
```php
|
||||
// draining 实例:pending 无此 key 或超过 drain_max_lifetime 则移除
|
||||
if ($state === 'draining') {
|
||||
$drain_max = (int) Config::get('timer.drain_max_lifetime', Config::get('timer.timeout', 86400));
|
||||
$timed_out = ($drain_started > 0 && (time() - $drain_started) > $drain_max);
|
||||
if (!isset($pending[$key]) || $timed_out) {
|
||||
unset($this->requestList[$arr_key]);
|
||||
Log::info('[timer] drain-done task=' . $name . ' id=' . $request_item['concurrency_id'] . ($timed_out ? ' (timeout)' : ''));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 崩溃恢复契约(重要边界)
|
||||
|
||||
drain 状态是纯内存的。**进程重启后以 DB concurrency 为准全重建(`generateAllRequestList` 产出 0..N-1 完整分片),放弃 drain 连续性**。重启前正在 draining 的实例不会恢复 draining,直接按新 DB 值重建为 active。`concurrency_id` 的稳定性也只在单进程生命周期内保证,重启后重置为 0..N-1。
|
||||
|
||||
#### 6. 遍历不变量
|
||||
|
||||
`runLoop` 直接遍历 `$this->requestList` 属性,**不再用局部副本**(旧实现 `$request_list = $this->requestList` 是 COW 拷贝,reload 改属性但 foreach 仍遍历旧副本会导致 reload 永不生效)。PHP foreach 进入时拷贝数组值,while 每轮重新 foreach,reload 修改属性后下一轮可见,mid-iteration append 也安全。
|
||||
|
||||
#### 7. 统一日志格式(验收 grep 基准)
|
||||
|
||||
| 事件 | 日志格式 |
|
||||
|------|---------|
|
||||
| reload | `[timer] reload task={name} concurrency {old}->{new} effective={n}` |
|
||||
| drain 开始 | `[timer] drain task={name} id={id}` |
|
||||
| drain 完成 | `[timer] drain-done task={name} id={id}`(超时则尾部加 ` (timeout)`) |
|
||||
| 手动触发消费 | `[timer] trigger task={name} node={id} consumed` |
|
||||
| 触发 TTL 复位 | `[timer] trigger-expire task={name} node={id} reset` |
|
||||
| 并发被钳制 | `[timer] clamp task={name} db={v} effective={n}` |
|
||||
|
||||
### 手动触发升级
|
||||
|
||||
#### 1. 跨 run_type 通用
|
||||
|
||||
手动触发不再限定 `manual` 类型。任何 `run_type`(auto / main / all / manual)的任务都能在后台手动触发一次。`shouldExecuteTask` 的判定顺序:
|
||||
|
||||
1. call 类型直接放行
|
||||
2. **手动触发优先检查**(穿透 status):读 config,若 `manual_trigger=1`,原子条件 UPDATE 抢占
|
||||
3. `status=0` 拦截自动调度(手动已在步骤 2 穿透,所以已停用的任务也能手动触发一次)
|
||||
4. `run_type` 调度(manual 类型在此 return false,即永不自动,执行全靠步骤 2)
|
||||
|
||||
#### 2. 穿透 status 的调试工作流
|
||||
|
||||
常见调试场景:把任务 status 临时关掉(停自动调度),反复手动触发验证,验证完再打开。手动触发穿透 status 让这个工作流顺畅:
|
||||
|
||||
```
|
||||
status=0(关自动) → 后台手动触发 → 定向节点跑一次 → 看日志 → 再触发 → ... → status=1(开自动)
|
||||
```
|
||||
|
||||
#### 3. 必带 trigger_node_id(定向单节点)
|
||||
|
||||
手动触发**必须定向到选中节点**(后台 UI 强制选节点才能触发,空值会被拒绝)。这是机制约束也是调试场景的正确语义:
|
||||
|
||||
- `manual_trigger` 是全局单标志位,单赢家原子 UPDATE 只能定向单节点,无法实现"全节点各跑一次"
|
||||
- 手动触发本质=调试某节点跑一次;all 模式的"全节点"需求靠正常调度(`status=1` + `all` run_type 自动每节点跑),不靠手动触发
|
||||
|
||||
#### 4. 原子条件 UPDATE(防多节点竞态)
|
||||
|
||||
抢占用原子条件 UPDATE,避免旧实现 `find → update` 的竞态:
|
||||
|
||||
```php
|
||||
// extend/base/common/command/TimerBase.php 的 shouldExecuteTask()
|
||||
$affected = Db::name('system_timer_config')
|
||||
->where('task_name', $task_name)
|
||||
->where('manual_trigger', 1)
|
||||
->where('trigger_node_id', $current_node_id) // 定向本节点
|
||||
->update([
|
||||
'manual_trigger' => 0,
|
||||
'trigger_node_id' => null,
|
||||
'last_trigger_time' => null,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
if ($affected === 1) {
|
||||
Log::info('[timer] trigger task=' . $task_name . ' node=' . $current_node_id . ' consumed');
|
||||
return true;
|
||||
}
|
||||
// affected=0:本节点不是目标,fall through 走正常调度
|
||||
```
|
||||
|
||||
多节点同时触发同一任务同一节点时,只有一个节点能 `affected=1` 消费成功。
|
||||
|
||||
#### 5. bypass 频率节流(但仍写 cache)
|
||||
|
||||
`runLoop` 把手动触发预检提到 cache 节流检查之前,让手动触发立即消费、不受 frequency 窗口阻塞:
|
||||
|
||||
```php
|
||||
// runLoop() 内
|
||||
$bypass_throttle = !empty($task_cfg['manual_trigger'])
|
||||
&& !empty($task_cfg['trigger_node_id'])
|
||||
&& $task_cfg['trigger_node_id'] === HostService::getNodeId();
|
||||
|
||||
if (!$bypass_throttle) {
|
||||
$last_exec_time = Cache::get($cache_key, 0);
|
||||
if ($last_exec_time >= time() - $request_item['frequency']) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 无论 bypass 与否都写节流 cache(防 all/main 无 DB 频率兜底的任务跨 tick 双发)
|
||||
Cache::tag($cache_tag)->set($cache_key, time());
|
||||
```
|
||||
|
||||
关键:**bypass 节流检查,但不 bypass 节流写入**。只 bypass 检查会导致 all/main(无 DB 频率兜底)的任务在同一个 tick 内被多个实例双发。
|
||||
|
||||
#### 6. TTL 自动复位
|
||||
|
||||
触发指向离线节点时(节点宕机/网络异常),`manual_trigger=1` 会卡住。`runLoop` 的 reload 兜底分支检查 TTL:
|
||||
|
||||
```php
|
||||
// checkTriggerTtl()
|
||||
$ttl = (int) Config::get('timer.trigger_ttl', 300);
|
||||
// manual_trigger=1 且 last_trigger_time 超过 300s 未消费 → 复位
|
||||
Db::name('system_timer_config')
|
||||
->where('id', $r['id'])
|
||||
->where('manual_trigger', 1) // 防并发消费后误复位
|
||||
->update(['manual_trigger' => 0, 'trigger_node_id' => null, 'last_trigger_time' => null]);
|
||||
Log::info('[timer] trigger-expire task=' . $r['task_name'] . ' node=' . $r['trigger_node_id'] . ' reset');
|
||||
```
|
||||
|
||||
#### 7. consume-then-fail 取舍
|
||||
|
||||
触发被消费后(`affected=1`),如果随后 `getAsync` 请求失败(网络/控制器异常),触发**不会自动重试**。这是原子单赢家机制的固有取舍:标志位已清,无法回滚。运维看错误日志后手动重新触发即可。
|
||||
|
||||
### 节点中心 UI
|
||||
|
||||
定时器配置页(`/admin/system.timer_config/index`)重构为两层叠加结构。
|
||||
|
||||
#### 1. 配置基础层(默认,永远可见)
|
||||
|
||||
全局任务配置表,呈现所有任务的 `task_name` / `run_type` / `status` / `concurrency` / `trigger_node_id` 等字段。**不依赖节点选中**,无节点在线时也能管理全局配置、改并发。
|
||||
|
||||
#### 2. 节点叠加层(选节点后显示)
|
||||
|
||||
表格上方有节点选择器(调 `hostList` 接口拉在线节点)。选中节点后:
|
||||
|
||||
- 多出**归属列**:基于 `run_type` + 当前选中节点是否主节点判断:
|
||||
|
||||
| run_type | 当前节点是主节点 | 当前节点非主节点 |
|
||||
|----------|------------------|------------------|
|
||||
| `all` | 当前自动 | 当前自动 |
|
||||
| `main` | 当前(主)自动 | 仅主节点 |
|
||||
| `auto` | 竞争 | 竞争 |
|
||||
| `manual` | 仅手动 | 仅手动 |
|
||||
| status=0 | 已停用 | 已停用 |
|
||||
|
||||
- **手动触发按钮**:定向到选中节点,无节点选中时按钮禁用。触发走 `trigger` 接口,POST body 必带 `trigger_node_id`
|
||||
|
||||
#### 3. concurrency 双通道编辑
|
||||
|
||||
- 行内编辑(layui table edit):直接改成正整数,校验必须 `>= 1`;想恢复"继承默认"请走 edit 表单传空值
|
||||
- edit 表单:默认/自定义切换,提交空值写 NULL(继承代码默认),提交正整数写覆盖值
|
||||
|
||||
后端归一化逻辑(`TimerConfig::normalizeConcurrency`):空字符串/null 写 NULL;正整数写 int;0 或负数或非数字直接拒绝。
|
||||
|
||||
#### 4. all 模式预估提示
|
||||
|
||||
`run_type=all` 的任务改 concurrency 时,UI 提示"全网预估 = 在线节点数 × 此值",防每节点并发乘节点数爆量。
|
||||
|
||||
### 多节点约束
|
||||
|
||||
动态并发是多节点环境下的能力,有几个硬约束必须记住。
|
||||
|
||||
#### 1. 并发语义:每节点并发
|
||||
|
||||
`concurrency` 管的是**每个节点**的并发实例数,不是全网总并发。`run_type` 管**哪些节点跑**,`concurrency` 管**每节点跑几个实例**,两者正交。
|
||||
|
||||
- `run_type=all` + `concurrency=3` + 5 个在线节点 = 全网 15 个并发实例(每节点 3 个)
|
||||
- `run_type=main` + `concurrency=3` + 5 个节点(1 主 4 从) = 全网 3 个并发实例(只主节点跑 3 个)
|
||||
|
||||
#### 2. 脏标记跨节点传播依赖共享 cache
|
||||
|
||||
`timer_config_version` 脏标记存在 cache 里。多节点要实时感知彼此的配置变更,**必须用共享 cache(Redis)**。文件 cache 下节点间互不可见,后台改并发只能靠每 15 秒的兜底 reload 追上,最坏有 15 秒延迟。
|
||||
|
||||
#### 3. drain 不跨进程重启
|
||||
|
||||
drain 状态(`state=draining`、`drain_started_at`)是纯内存的。进程重启后以 DB concurrency 为准全重建 0..N-1 完整分片,**放弃 drain 连续性**。重启前正在 draining 的实例直接变 active。
|
||||
|
||||
#### 4. concurrency_id 稳定性仅单进程生命周期
|
||||
|
||||
`concurrency_id` 的"缩容 drain 高 id / 扩容 max+1 续编"规则只在单进程生命周期内保证。进程重启后 id 重置为 0..N-1,分片编号会变。如果你的业务逻辑强依赖固定分片号(比如按 `concurrency_id` 哈希分桶),要意识到重启会重新分配。
|
||||
|
||||
#### 5. 仅覆盖 site 类型
|
||||
|
||||
动态并发、热加载、drain 这些能力**只覆盖 `site` 类型任务**。`call` 类型任务始终在所有节点执行,不受 concurrency 热加载影响(技能文档本来也不推荐 call,优先用 site)。
|
||||
|
||||
### 日志清理
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user