# Essential CDC newarch 设计与实现说明

## 1. 文档目的

本文面向 reviewer、SRE、接手开发者和 agent，说明 `serverless-service` 中 **Essential CDC newarch** 的最终实现形态：它如何与老链路并存，如何创建和维护 shard 级共享 TiCDC cluster，怎样通过 FastFlow DAG 完成 create / scale / delete / migrate / upgrade，以及上线前需要满足哪些 dataplane 条件。

本文以当前分支代码为准，不记录演进过程中的临时状态，只描述 **最新实现**。

---

## 2. 一句话结论

- Essential CDC newarch 已经形成独立闭环：**create / scale / delete / migrate / spec-list / ops upgrade** 都有明确 API 和状态机。
- shared TiCDC cluster 的复用维度是 **`(cloud_provider, region, kubernetes)`**，不是 Premium 的 shared pool。
- FastFlow executor 已经从 br-worker 中拆出，运行在独立的 **`workflow` work mode / `serverless-svc-workflow`** 进程里。
- shared cluster 已经支持 **版本显式升级**：`ticdc_clusters.version` 只允许通过 ops API 修改，不会因为配置变更而被全量隐式滚动。
- delete 流程已经补齐 **kernel 删除、shared cluster 缩容 / 回收、cert 清理、PVC 清理**；released 资源行由 worker 周期性 GC。
- create 流程已经对齐 `dataflow-service-ng` 的 **3-phase create intent**：先持久化 pending kernel row，再调用 TiCDC kernel，最后激活为 active；kernel create 失败后用 list-based probe 做幂等确认，避免 TiCDC kernel orphan。
- worker 收敛已经对齐 `dataflow-service-ng` 的 **mark running/deleted 后 ack resource actual=expected** 语义，避免 resource actual 卡住导致 DAG 永久等待。
- 当前唯一已知的 dev 环境硬阻塞仍然是 **dataplane PD < 9.0** 导致的 `GetGCState` 缺失，而不是 serverless-service 自身逻辑问题。

---

## 3. 代码结构总览

### 3.1 业务入口

- `internal/domain/service/cdc.go`：Create / Start / Stop / Edit / Scale / Delete / Sync 入口
- `internal/domain/service/cdc_migrate.go`：老架构 → newarch 迁移
- `internal/domain/service/cdc_specifications.go`：`ListChangefeedSpecifications`

### 3.2 newarch 核心

- `internal/domain/service/cdc_newarch.go`
  - shard 命名
  - `rcu -> TiCDC spec` 选择
  - `TidbCluster` CR 构造 / reconcile / readiness
  - `ticdc_clusters` 持久化
  - upgrade 流程
- `internal/domain/service/cdc_newarch_client.go`
  - TiCDC v2 HTTP client
- `internal/domain/service/cdc_newarch_storageclass.go`
  - 自动创建 / 复用 per-tier StorageClass
- `internal/domain/service/cdc_newarch_cert.go`
  - cert-manager Certificate 创建
- `internal/domain/service/cdc_newarch_resource_worker.go`
  - worker 侧 shard 收敛

### 3.3 workflow

- `internal/workflow/dag/changefeed/{create,scale,delete}.go`
- `internal/workflow/action/changefeed/{create,scale,delete}/*.go`
- `internal/workflow/action/changefeed/scheduler.go`
- `internal/workflow/impl/workflow.go`
- `internal/workflow/runtime.go`

### 3.4 API 层

- `idl/idl/v1beta1/cdc.proto`
  - `MigrateChangefeed`
  - `ListChangefeedSpecifications`
- `idl/idl/rpc/cdc_ops.proto`
  - `ChangefeedOpsService`
  - `ListTiCDCClusters`
  - `GetTiCDCCluster`
  - `UpgradeTiCDCCluster`
- `internal/service/cdc_svc/cdc_v1beta1.go`
- `internal/service/ops_svc/cdc_rpc.go`

### 3.5 持久化模型

| 表 / Entity | 作用 |
| --- | --- |
| `cdc_changefeeds` | 主记录；newarch 复用此表，新增 `arch` / `rcu` |
| `ti_cdc_changefeed_resources` | 每个业务 changefeed 一条 shard 资源记录 |
| `kernel_ti_cdc_changefeeds` | 记录 kernel changefeed 与 shared cluster 绑定 |
| `ticdc_clusters` | 每个 shard 一条 shared cluster 记录 |

---

## 4. 运行时拓扑

当前部署形态由 5 个 work mode 组成：

| 进程 / Pod | 子命令 | 责任 |
| --- | --- | --- |
| `serverless-svc-http` | `http` | REST API，提交 DAG，不执行 DAG |
| `serverless-svc-grpc` | `grpc` | gRPC API，提交 DAG，不执行 DAG |
| `serverless-svc-worker` | `worker` | 常规 worker / DB migrate / global workers |
| `serverless-svc-br-worker` | `brworker` | BR/CDC worker，负责 newarch resource sync 与 released row GC |
| `serverless-svc-workflow` | `workflow` | **唯一**运行 FastFlow executor 的进程 |

关键原则：

1. **写 DAG** 和 **跑 DAG** 分离：
   - HTTP / gRPC 只调用 `RunDagWithTags`
   - `workflow` 进程才会 `StartExecutor()`
2. **resource sync** 不在 workflow 进程里，而在 br-worker 周期执行
3. 非 `workflow` mode 的进程 **不能** 触发 FastFlow executor

---

## 5. 核心设计

### 5.1 shared cluster 复用域

Essential newarch 的 shared TiCDC cluster 按以下维度复用：

- `cloud_provider`
- `region`
- `kubernetes`

因此，同一个 region 下如果落在不同 regional kubernetes shard，就不会共享同一个 TiCDC cluster。

### 5.2 shared cluster 命名

```text
ess-ticdc-{region-with-dashes}-{sha1(provider:region:kubernetes)[:8]}
```

实现：`buildEssentialTiCDCClusterName()`

### 5.3 资源表：`ti_cdc_changefeed_resources`

每个业务 changefeed 一条，核心字段：

| 字段 | 含义 |
| --- | --- |
| `cluster_id + changefeed_id` | 业务唯一键 |
| `cloud_provider + region + kubernetes` | shard 聚合键 |
| `ticdc_cluster_name` | 当前 shared cluster 名 |
| `expected_specification` | 当前 changefeed 期望规格 |
| `actual_specification` | worker 确认后回填的已收敛规格 |

shard 的目标规格通过 **聚合所有同 shard、`expected_specification != ""` 的记录** 计算得出。

### 5.4 shared cluster 表：`ticdc_clusters`

每个 shard 一条，唯一键 `(cloud_provider, region, kubernetes)`。

核心字段：

| 字段 | 含义 |
| --- | --- |
| `ticdc_cluster_name` | shared TiCDC CR 名 |
| `version` | 当前目标 TiCDC image version |
| `expected_spec` | shard 聚合后的目标规格 |
| `actual_spec` | 当前已 ready 的规格 |
| `state` | `creating` / `running` / `upgrading` / `deleted` |

设计约束：

1. `version` 在首次创建时落当前配置值
2. 之后 **不会** 被普通 reconcile 覆盖
3. 修改现有 shard 的 `version` 只能通过 `UpgradeNewArchTiCDCCluster()`

### 5.5 kernel 记录表：`kernel_ti_cdc_changefeeds`

每个业务 changefeed 一条：

| 字段 | 含义 |
| --- | --- |
| `ticdc_cluster_name` | 归属的 shared cluster |
| `kernel_changefeed_id` | TiCDC kernel 侧 changefeed id |
| `kernel_status` | `pending` / `active` / `deleted` |

状态转移：

- create 的 cluster action 开始时先持久化 create intent：`pending`
- create 的 kernel action 成功后：`active`
- delete 的 kernel action 成功后：`deleted`

### 5.6 门禁模型

newarch 不依赖“看到资源行就认为可用”，而是严格要求：

- `expected_specification != ""`
- `expected_specification == actual_specification`

只有同时满足时，才认为 shard 级 TiCDC 资源已经 ready。

这比 `dataflow-service-ng` 中的宽松 `expected == actual` 更严格，可以避免 released 行或陈旧行被误判 ready。

### 5.7 keyspace 参数来源

TiCDC OpenAPI 里的 `keyspace` 参数必须使用 **keyspace token / keyspace-name**，不能传本地 DB 的 `keyspace_id`。

当前正确来源：

```go
mgmtSvc.GetCluster(..., CLUSTER_VIEW_BASIC).
  GetServerlessCluster().
  GetCluster().
  GetUserPrefix()
```

也就是 management-service 返回的 `serverless.cluster.user_prefix`。

---

## 6. 配置模型

`cdc.newArch` 当前核心配置：

| 字段 | 作用 |
| --- | --- |
| `enabled` | 总开关 |
| `namespace` | dataplane TiCDC 所在 namespace |
| `upstreamNamespace` / `upstreamClusterName` | 上游 serverless cluster 引用 |
| `regionConfig[].baseImage / version` | per-region TiCDC 基镜像和 tag override，用于按 region 灰度发布；resolver 的最高优先级（见 §11.1）|
| `baseImage` | 全局 TiCDC 基镜像兜底；resolver 的最低优先级（见 §11.1）|
| `baseImagePerProvider` | per-provider 基镜像 map（`aws` / `gcp` / `azure` / `alicloud` 各配一个）。不同云的 registry layout 不同（ECR vs ACR vs GCR），一个字符串覆盖不了 |
| `version` | 全局 TiCDC 镜像 tag 兜底 |
| `issuerKind` / `issuerName` | cert-manager issuer 配置 |
| `rcuTiers` | **用户面** RCU 档位；`ListChangefeedSpecifications` 直接返回 |
| `cpuPerRcu` | 1 rcu 折算多少 CPU；picker 用 `totalRcu × cpuPerRcu = totalCpu`，默认 `1` |
| `instanceTypes` | **运维面** CDC 机型目录；picker 从中选机型 + 算 replicas |
| `nodeSelector` / `tolerations` | 写入 TiCDC `ComponentSpec`，控制调度；未配置时默认调度到 16c / 64Gi adhoc ON_DEMAND 节点 |

### 6.1 create 前校验

当 `arch=ticdc_newarch` 时，`validateCreateChangefeedDefaults()` 当前要求：

- `cdc.newArch.enabled = true`
- 目标 region 能通过 `cdc.newArch.regionConfig[].baseImage` / `baseImagePerProvider` / `baseImage` 解析出 TiCDC base image
- 目标 region 能通过 `cdc.newArch.regionConfig[].version` / `version` 解析出 TiCDC image tag
- `cdc.newArch.issuerName` 非空
- `cdc.newArch.instanceTypes` 非空（为空时回退到默认目录：单 16c 机型）
- `changefeed.rcu > 0`

StorageClass 按 instance type 自动建 / 复用 `cdc-{cloud}-{iops}-{throughput}`；PVC 大小来自选中 `instanceTypes[].storageGi`。

### 6.2 RCU 档位（用户面）

source of truth 是配置文件，不是数据库。

- `ListChangefeedSpecifications` 返回 `cdc.newArch.rcuTiers`，UI 用它给用户选档
- 每档只暴露 `rcu` + `rpsLimit`；机型细节（cpu / memory / storage / iops）**不向外暴露**
- 如果配置为空，fallback 到 `DefaultNewArchRcuTiers`（2 / 4 / 8 / 16 / 32 / 64 / 128 / 256 rcu）

约束：

1. 必须按 `rcu` 升序
2. `rpsLimit` 只做展示，不在 serverless-service 内部执行限速

### 6.3 Instance Type 目录 + 选型算法（运维面）

shard 级 reconcile 把所有 active resource 的 `expected_specification` 聚合成 `totalRcu`，然后通过 picker 选出一个 `(instanceType, replicas)` 用来渲染 `TidbCluster` CR：

```text
totalCpu = totalRcu × cpuPerRcu
foreach instType in instanceTypes (asc by CpuPerReplica):
    tierRcu := instType.CpuPerReplica / cpuPerRcu
    if totalRcu <= tierRcu: return (instType, 1)
between two tiers: snap to nearest by midpoint (single replica)
exceed largest tier: use largest, replicas = round_half_up(totalCpu / largest.CpuPerReplica)
```

具体行为：

- **欠配 → 单 replica 兜底**：低于最小档的 demand 也会拉起 1 个最小机型 pod。
- **介于两档之间 → snap 到最近**：算法与老 spec-ladder 一致，midpoint 决定上下取。
- **超出最大档单 replica 容量 → 横向扩**：保持最大机型，replicas = `round_half_up(totalCpu / largestCpuPerReplica)`。

#### 默认目录

为了让任意一条 changefeed 至少拉起一台 16c 机器（避免在 dev/staging 上凭借「小档机型」掩盖大流量场景的真实表现），`DefaultNewArchInstanceTypes` 当前只有一项：

```text
{ Name: "16c", CpuPerReplica: 16, MemoryGi: 64, StorageGi: 200, Iops: 12000, ThroughputMbs: 1000 }
```

→ 即使 2rcu 的单 changefeed 也会拉起 `16c × 1`；24rcu 总量则按 `round(24/16) = 2` 算出 `16c × 2`。

#### 自定义目录

env 配置可以提供多档机型（按 `cpuPerReplica` 升序）：

```yaml
cdc:
  newArch:
    cpuPerRcu: 1
    instanceTypes:
      - { name: small,  cpuPerReplica: 2,  memoryGi: 8,  storageGi: 50,  iops: 4000,  throughputMbs: 400 }
      - { name: medium, cpuPerReplica: 4,  memoryGi: 16, storageGi: 50,  iops: 4000,  throughputMbs: 400 }
      - { name: large,  cpuPerReplica: 8,  memoryGi: 32, storageGi: 100, iops: 6000,  throughputMbs: 500 }
      - { name: xlarge, cpuPerReplica: 16, memoryGi: 64, storageGi: 200, iops: 12000, throughputMbs: 1000 }
```

#### Per-replica 而不是 per-cluster

每条 `instanceType` 描述的是 **单个 pod** 的规格：

- `CpuPerReplica / MemoryGi`：写入 TiCDC `Requests/Limits`
- `StorageGi`：每个 pod 自己的 PVC 大小（不是整 shard 合计）
- `Iops / ThroughputMbs`：传给自动建的 StorageClass

总 cpu / 总存储 = 单台 × replicas，由 picker 决定 replicas。

---

## 7. API 形态

### 7.1 用户侧 API

| 能力 | HTTP | 说明 |
| --- | --- | --- |
| create changefeed | `POST /v1beta1/clusters/{cluster_id}/changefeeds` | 显式 `arch=ticdc_newarch`；开启 newarch 后默认路由到 newarch |
| scale changefeed | `POST ...:scale` / 对应 gRPC | 仅 newarch |
| delete changefeed | `DELETE /v1beta1/clusters/{cluster_id}/changefeeds/{changefeed_id}` | 触发 delete DAG |
| migrate | `POST /api/v1/serverless/ops/cdc/clusters/{cluster_id}/changefeeds/{changefeed_id}:migrate` | ops-only；pause old → checkpoint → create newarch |
| list specs | `GET /v1beta1/clusters/{cluster_id}/changefeedSpecifications` | 返回 RCU 档位 |

### 7.2 运维侧 Ops API

`ChangefeedOpsService` 暴露 shared TiCDC cluster 级别的运维接口：

| RPC | HTTP | 说明 |
| --- | --- | --- |
| `ListTiCDCClusters` | `GET /api/v1/serverless/ops/cdc/clusters` | 列出所有 shard |
| `GetTiCDCCluster` | `GET /api/v1/serverless/ops/cdc/{provider}/{region}/{kubernetes}` | 查看单 shard |
| `UpgradeTiCDCCluster` | `POST /api/v1/serverless/ops/cdc/{provider}/{region}/{kubernetes}:upgrade` | 写入目标 version，置 `upgrading`，立即触发一次 reconcile |

`UpgradeTiCDCCluster` 的语义是：

1. 先更新 DB 中 `ticdc_clusters.version`
2. 再把 `state` 置为 `upgrading`
3. 立即触发 reconcile
4. 如果 DB 已成功但 dataplane reconcile 失败，API 仍然返回 row + error，调用方能看见已经落库的新 intent，后续靠 background sync 追平

---

## 8. 请求分流与主流程

### 8.1 请求分流

```mermaid
flowchart TD
    A[Create / Start / Stop / Edit / Scale / Delete / Migrate API] --> B{arch / action}
    B -->|replication_worker| C[老链路]
    B -->|ticdc_newarch + create| D[newarch create DAG]
    B -->|ticdc_newarch + scale| E[newarch scale DAG]
    B -->|ticdc_newarch + delete| F[newarch delete DAG]
    A -->|migrate| G[pause old + create newarch with checkpoint TSO]
```

### 8.2 newarch 主流程

```mermaid
flowchart TD
    subgraph Create
        C1[CreateChangefeed API] --> C2[写 cdc_changefeeds state=creating]
        C2 --> C3[提交 Create DAG]
        C3 --> C4[CreateOrScaleTiCDCCluster: 写 pending intent + resource row]
        C4 --> C5[worker 回填 actual_specification]
        C5 --> C6[CreateChangefeed: load-or-create pending + 调 kernel]
        C6 --> C7[激活 kernel row: pending -> active]
        C7 --> C8[Final: creating -> running]
    end

    subgraph Scale
        S1[ScaleChangefeed API] --> S2[transaction: state=scaling + 提交 Scale DAG]
        S2 --> S3[ScaleTiCDCCluster]
        S3 --> S4[worker 回填 actual_specification]
        S4 --> S5[Final: scaling -> running]
    end

    subgraph Delete
        D1[DeleteChangefeed API] --> D2[transaction: state=deleting + 提交 Delete DAG]
        D2 --> D3[CancelUnfinishedDagIns]
        D3 --> D4[DeleteChangefeed kernel]
        D4 --> D5[DeleteOrScaleTiCDCCluster]
        D5 --> D6[Deregister private link]
        D6 --> D7[Final: deleted]
    end

    subgraph Migrate
        M1[MigrateChangefeed API] --> M2[pause old changefeed]
        M2 --> M3[读 checkpoint_ts]
        M3 --> M4[CreateChangefeed arch=ticdc_newarch start_ts=checkpoint_ts]
        M4 --> M5[复用 normal create DAG]
    end
```

---

## 9. 详细流程

### 9.1 Create

入口：`cdcService.CreateChangefeed`

执行顺序：

1. 校验 cluster 状态
2. 补默认值（含 arch / rcu）
3. 对 newarch 做配置校验
4. 做业务校验与网络预检
5. 写主表 `cdc_changefeeds`，状态设为 `creating`
6. 提交 create DAG

Create DAG：

| Action | 作用 |
| --- | --- |
| `CreateOrScaleTiCDCCluster` | 先持久化 kernel pending intent，再写 resource row、计算 shard 目标规格、创建或更新 shared CR |
| `CreateChangefeed` | load-or-create kernel pending intent，调用 TiCDC v2 API 创建 kernel changefeed，确认成功后置 `active` |
| `Final` | `creating -> running` |

Create 的 kernel 侧采用与 `dataflow-service-ng` 相同的 3-phase 语义：

1. **Phase 1：persist create intent**
   - 使用业务 `changefeed.ID` 作为 deterministic `kernel_changefeed_id`
   - 先 load existing kernel row；不存在才插入 `kernel_status=pending`
   - 如果已有 `active` row，直接认为已经收敛，不会重新调用 kernel create，也不会把 active 降级回 pending
2. **Phase 2：create or verify kernel changefeed**
   - 调 TiCDC v2 `CreateChangefeed`
   - `ErrChangeFeedAlreadyExists` / already exists 视为成功
   - 其他错误会再用 `ListChangefeeds(state=all)` 探测 kernel 是否已经拥有该 `changefeed_id`
   - 如果 list probe 能看到 changefeed，视为成功；否则保留原始错误，让 DAG retry
3. **Phase 3：activate**
   - kernel 已确认拥有 changefeed 后，才把 `kernel_ti_cdc_changefeeds.kernel_status` 从 `pending` 写成 `active`

如果 Phase 2 失败，pending row 会保留，DAG retry 会复用同一个 deterministic kernel ID，避免 “kernel 已创建但 DB 没指针” 的 orphan changefeed。

### 9.2 Scale

入口：`cdcService.ScaleChangefeed`

约束：

- 必须是 newarch
- 当前 state 必须是 `running`
- `rcu > 0` 且与现值不同

实现要点：

- `state=scaling` 与提交 DAG 在同一个 transaction 里
- DAG 提交失败会自动回滚，不留下半翻转状态

Scale DAG：

| Action | 作用 |
| --- | --- |
| `ScaleTiCDCCluster` | 更新 `expected_specification`，reconcile shard |
| `Final` | `scaling -> running` |

### 9.3 Delete

入口：`cdcService.DeleteChangefeed`

Delete DAG：

| Action | 作用 |
| --- | --- |
| `CancelUnfinishedDagIns` | 收敛同 changefeed 未完成 DAG |
| `DeleteChangefeed` | 删除 kernel changefeed，更新 kernel row |
| `DeleteOrScaleTiCDCCluster` | shard 内还有活跃 changefeed 就缩容，否则删 shared cluster |
| `Deregister` | 注销 private link |
| `Final` | 软删主记录 |

最终删除动作已包含：

- 删除 `TidbCluster` CR
- 删除两个 cert-manager `Certificate`
- 删除 `ticdc-sort-kv-{cr}-ticdc-{ordinal}` PVC

Delete kernel changefeed 是幂等的：如果 TiCDC service / kernel changefeed 已经不存在，仍然会把本地 kernel row 推进到 `deleted`，避免 shared TiCDC cluster 已被回收后，业务 changefeed 永久卡在 `deleting`。

**不会** 删除：

- cluster-scoped StorageClass（跨 shard / 跨实例档共享）
- cert-manager Issuer

### 9.4 Migrate

入口：

- gRPC：`MigrateChangefeed`
- HTTP：`POST /v1beta1/clusters/{cluster_id}/changefeeds/{changefeed_id}:migrate`

语义：

1. 读取源 changefeed
2. 源必须是 `replication_worker`
3. 如源在 `running / warning`，先执行 pause
4. 通过 clusterproxy 直接读取最新 `checkpoint_ts`
5. 构造新的 newarch changefeed：
   - `arch = ticdc_newarch`
   - `start_position.mode = FROM_TSO`
   - `start_position.tso = checkpoint_ts`
   - sink / filter 复用源配置
6. 走标准 create 流程

设计边界：

- 不自动删除源
- pause 与 create 不在同一个事务中
- API 是 best-effort，可重试但不去重

### 9.5 shared cluster Upgrade

入口：`UpgradeTiCDCCluster`

流程：

1. 获取现有 `ticdc_clusters` row
2. 更新 `version`
3. 将 `state` 设为 `upgrading`
4. 立即触发 reconcile，把目标 version 写进 dataplane `TidbCluster` CR
5. operator 执行 rolling update

状态保持规则：

- 如果 row 处于 `upgrading`，普通 reconcile **不会** 把它写回 `creating`
- 只有收敛到 `running`，或被回收到 `deleted`，才会清除 `upgrading`

---

## 10. Reconcile、幂等与状态所有权

### 10.1 状态所有权

实现里刻意避免多个写者抢同一个状态：

- changefeed 主状态的 owner 是 API + DAG Final action
- `deleting -> deleted` 的 owner 是 delete DAG，不允许 worker 抢写
- `ticdc_clusters.state=upgrading` 的 owner 是 ops upgrade API；普通 reconcile 不能覆盖回 `creating`

### 10.2 幂等点

以下创建路径都容忍已存在错误：

- `createNewArchTiCDCCR`
- `ensureCert`
- `ensureNewArchStorageClass`
- `CreateChangefeed` kernel call

对 dataplane 的 `AlreadyExists` / HTTP 409 会当成功处理，避免 shard 间、reconcile 间 TOCTOU 抖动。

kernel changefeed create 还额外做 list-based idempotency probe：当 TiCDC create 返回 timeout / context canceled / opaque regional error 时，通过 `ListChangefeeds(state=all)` 判断 kernel 是否实际已经创建成功。这样即使 response 丢失，也不会重复生成孤儿 changefeed。

### 10.3 worker 收敛

br-worker 周期执行：

1. `syncNewArchChangefeedResources`
2. shard 级 `actual_specification` 回填
3. `ticdc_clusters.expected/actual/state` 同步
4. released row GC

worker 与 `dataflow-service-ng` 的对齐点：

- CR ready 且规格匹配时，`markNewArchTiCDCClusterRunning` 会同时写 `ticdc_clusters.state=running`、`actual_spec=expected_spec`，并把同 shard resource 的 `actual_specification` ack 到 `expected_specification`
- CR 不存在且 shard 无 active resource 时，`markNewArchTiCDCClusterDeleted` 会同时写 `ticdc_clusters.state=deleted`，并把 released resource 的 `actual_specification` ack 到空 expected
- CR 仍存在但 shard 无 active resource 时，worker 先删除 / 清理 dataplane，再在 DB transaction 里标记 deleted 并 ack resource
- CR 不存在但 shard 仍有 active resource 时，worker 写 `creating` + expected spec，等待下一轮 reconcile 重建

regular CDC sync worker 的兼容性约束：

- 同一个 Essential cluster 可以同时存在 `replication_worker` 和 `ticdc_newarch` changefeed
- `SyncChangefeeds` 必须按 `cluster_id + arch` 分组，不能只按 `cluster_id` 分组
- 否则老路径 list 结果会被拿来同步 newarch changefeed，导致 newarch 在老 TiCDC 列表里“查不到”后被误标记为 `deleted`

worker 额外有两个重要约束：

- 只有 `cdc.newArch.enabled=true` 的 pod 才允许执行 newarch sync
- 构造的 service stub 必须带 `transProvider`，保证 shard 内多张表原子更新

### 10.4 released row GC

delete 流程不会直接删 `ti_cdc_changefeed_resources` 行，而是先把：

- `expected_specification = ""`
- `actual_specification = ""`

worker 每轮会调用：

```go
DeleteStaleReleasedRows(ctx, 7*24*time.Hour)
```

把释放超过 7 天的资源行硬删除。

---

## 11. Dataplane 资源渲染

### 11.1 `TidbCluster` CR

shared TiCDC dataplane 资源使用 operator v1 `pingcap.com/v1alpha1 TidbCluster`，只启用 TiCDC 组件，并引用上游 `serverless-cluster`。

渲染要点：

- `spec.version` 来自 `ticdc_clusters.version`（首次创建从 `cdc.newArch.regionConfig[].version` 或全局 `cdc.newArch.version` 落库；之后只能通过 `UpgradeTiCDCCluster` ops API 改）
- `spec.ticdc.baseImage` 通过 `config.ResolveNewArchBaseImage(provider, region)` 解析（三层）：
  1. `cdc.newArch.regionConfig[].baseImage` — per-region 覆盖（用于某 region 拉本地 registry，或灰度不同 TiCDC image）
  2. `cdc.newArch.baseImagePerProvider[<provider>]` — per-provider 默认（aws / gcp / azure / alicloud 各一个）
  3. `cdc.newArch.baseImage` — 全局兜底字符串
- `spec.ticdc.config` 使用 **TOML 字符串**
- `spec.ticdc.replicas` 来自 picker 的 replicas 计算结果
- `spec.ticdc.requests/limits` 用选中 `instanceType` 的 `cpuPerReplica` / `memoryGi`
- `spec.ticdc.storageVolumes[*].storageSize` 来自选中 `instanceType` 的 `storageGi`（per-replica，每个 pod 自己一份 PVC）
- `spec.ticdc.nodeSelector / tolerations` 来自 `cdc.newArch` 配置
- `metadata.labels["tidbcloud.com/cdc-instance-type"]` 标记当前选中的机型，方便 ops 观测

### 11.2 StorageClass

按 instance type 自动创建 / 复用：

```text
cdc-{cloud}-{iops}-{throughput}
```

不同云的 provisioner / parameter 会分别匹配：

- AWS：`ebs.csi.aws.com`
- GCP：`pd.csi.storage.gke.io`
- Azure：`disk.csi.azure.com`
- Alicloud：`diskplugin.csi.alibabacloud.com`

> StorageClass 是 cluster-scoped，但 dbaas-proxy 服务端的请求校验要求 `K8sResourceRequest.Namespace` 字段非空，否则直接 400。`ensureNewArchStorageClass` 在 Get / Create 调用上都会复用 `cdc.newArch.namespace`，值在 apiserver 端会被忽略，只为通过 proxy 层校验。

### 11.3 cert-manager 依赖

每个 shared cluster 需要两张 Certificate：

- `{cr}-cluster-client-secret`
- `{cr}-ticdc-cluster-secret`

对应 issuer 来自：

- `cdc.newArch.issuerKind`
- `cdc.newArch.issuerName`

---

## 12. Dataplane 依赖与 RBAC

上线前必须确认 dataplane 具备：

1. `dbaas-proxy / clusterproxy` 能写：
   - `pingcap.com/v1alpha1 tidbclusters`
   - `cert-manager.io/*`
   - `storage.k8s.io/storageclasses`
2. `tidb-operator v1` 支持 heterogeneous `TidbCluster`
3. `cert-manager` 已部署，并且对应 Issuer 存在

至少需要的权限包括：

```yaml
- apiGroups: ["pingcap.com"]
  resources: ["tidbclusters"]
  verbs: ["create", "get", "list", "watch", "update", "delete"]
- apiGroups: ["cert-manager.io"]
  resources: ["certificates"]
  verbs: ["create", "get", "list", "watch", "update", "delete"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["create", "get", "list", "watch", "update", "delete"]
```

---

## 13. 与老链路的兼容关系

| 层次 | `replication_worker` | `ticdc_newarch` |
| --- | --- | --- |
| 主记录 | `cdc_changefeeds` | `cdc_changefeeds` |
| 中间表 | 老 `cdc_clusters` | `ti_cdc_changefeed_resources` / `kernel_ti_cdc_changefeeds` / `ticdc_clusters` |
| 调度方式 | polling worker | FastFlow + resource worker |
| 数据面 | 既有 replication-worker | shard 级 shared TiCDC cluster |
| scale | 不支持 | 独立 API |
| migrate | 源 | 目标 |

兼容边界：

1. 主表共用，中间表不共用
2. service 入口共用，按 `arch` 分流
3. 未开启 `cdc.newArch.enabled` 时默认仍走 `replication_worker`

---

## 14. Dev e2e 现状

2026-05-27 在 nightly alicloud `ap-southeast-1` 用 PR 镜像补跑兼容性 e2e：

| 场景 | 结果 |
| --- | --- |
| 老路径 create / list | `REPLICATION_WORKER` changefeed 可正常创建并进入 `running`；与 newarch 同 cluster 共存时，常规 worker 不再把 newarch 误同步成 `deleted` |
| newarch create | `TICDC_NEWARCH` changefeed 从 `creating` 收敛到 `running`；`cdc_changefeeds` / `ti_cdc_changefeed_resources` / `ti_cdc_clusters` / `kernel_ti_cdc_changefeeds` 均收敛到 active + expected=actual |
| newarch scale | 2rcu → 8rcu 控制面和 TiDBCluster spec 正常更新；Alicloud 当前 nodepool 对第二个 TiCDC pod 报 `RecommendEmpty.InstanceTypeNoStock`，属于 dataplane 容量不足；随后 8rcu → 4rcu 控制面回到 `running` + expected=actual |
| newarch delete | 即使前序扩容留下 Pending pod，delete DAG 仍能把 kernel row / resource / shared cluster 标记 deleted，并清理 Alicloud `TidbCluster` / `StatefulSet` / PVC / cert |
| migrate | 首次触发发现迁移代码把 DB 中已加密的 source sink secret 原样复制给 CreateChangefeed，导致 MySQL precheck 报 `crypto/rsa: message too long for RSA key size`；已修复为迁移构建 VO 时深拷贝 sink/filter 并解密 secret，再交给 CreateChangefeed 重新校验和加密。用修复后的镜像重试通过：source old-path 被暂停，新 newarch 以 source checkpoint TSO 作为 `start_tso` 创建并进入 `running` |
| cleanup | 本轮 E2E 创建的 old-path、migrated newarch 均已 DELETE；DB 行进入 `deleted`，newarch resource expected/actual 清空，kernel row / shared cluster deleted，Alicloud `ess-ticdc` 资源已清空 |

已在 `dev-us-east-1-f01` 验证通过的场景（基于 PR HEAD）：

| 场景 | 验证点 |
| --- | --- |
| migration | 一次建出 3 张 newarch 表 + `cdc_changefeeds.arch / rcu` |
| `ListChangefeedSpecifications` | 返回 `cdc.newArch.rcuTiers`（5 档：2/4/8/16/32 rcu + rpsLimit）|
| create 1×2rcu | 默认 catalog 选 16c × 1，PVC 200Gi gp3，label `tidbcloud.com/cdc-instance-type=16c` |
| scale-out（POST 22rcu → totalRcu=24） | picker 走 `largest × replicas`：16c × **2 replicas**，2 个 pod Running |
| 老的 spec-ladder 聚合（2+2+4=8rcu） | shared cluster expected/actual 都到 `8rcu`（即使在默认单 16c catalog 下也 8 ≤ 16，单 replica）|
| delete 全部 | 15s 内 TidbCluster + 2 Certificate + **multi-replica 全部 PVC** + shard `state=deleted` |
| ops API `List / Get / Upgrade` | List 列 shard；Get 单 shard；Upgrade 写 version + state=upgrading + 立刻 reconcile |
| 6.3 picker 默认目录 | `DefaultNewArchInstanceTypes` 单 16c 兜底，任何 rcu 都拉起 16c 机型 |

当前唯一已知 blocker：

```text
CDC:ErrStartTsBeforeGC
```

根因不是 serverless-service，而是 dev dataplane 的 PD 版本过低：

- newarch TiCDC image 期望使用 `GetGCState`
- dev PD v7.5.0 不提供该 gRPC
- fallback 路径对 keyspace v2 的 GC safepoint 判断有 bug

**结论**：上线前 dataplane PD 必须 ≥ 9.0。

> dev 上无法把 cf 真正走到 `running` 状态（cf 永远卡 CREATING），但 shared cluster 的 reconcile / spec-match / cert + PVC 全生命周期都已经在 e2e 里覆盖到。Scale API 本身因为要求 `cf.state=running` 在 dev 上无法直接调，等 PD 升级后可一次性验通。

---

## 15. Rollout / Review Checklist

### 15.1 配置

- [ ] `cdc.newArch.enabled=true`
- [ ] 每个目标 region 都能通过 `cdc.newArch.regionConfig[].baseImage` / `baseImagePerProvider` / `baseImage` 解析出 TiCDC base image
- [ ] 每个目标 region 都能通过 `cdc.newArch.regionConfig[].version` / `version` 解析出 TiCDC image tag；灰度发布优先改 region override，不改 worker config
- [ ] `cdc.newArch.issuerName` 非空
- [ ] `cdc.newArch.rcuTiers` 合法且按 `rcu` 升序（UI 选档用）
- [ ] `cdc.newArch.instanceTypes` 非空且按 `cpuPerReplica` 升序（picker 用）
- [ ] `cdc.newArch.cpuPerRcu` 设置合理（默认 1）
- [ ] 如目标 nodepool 有 taint，补齐 `nodeSelector` / `tolerations`

### 15.2 部署

- [ ] `serverless-svc-workflow` 已部署
- [ ] workflow pod 能看到 FastFlow executor 启动日志
- [ ] br-worker 仍负责 newarch resource sync 与 stale row GC

### 15.3 Dataplane

- [ ] clusterproxy RBAC 完整
- [ ] tidb-operator v1 支持 TiCDC-only heterogeneous CR
- [ ] cert-manager + issuer 工作正常
- [ ] PD 版本 ≥ 9.0

### 15.4 功能烟测

- [ ] `arch=ticdc_newarch` create 一条 changefeed
- [ ] 创建多条 changefeed 验证 shard 聚合扩容
- [ ] delete 一条或全部 changefeed 验证缩容 / 回收
- [ ] migrate 一条老 changefeed，确认 `start_ts = checkpoint_ts`
- [ ] ops API upgrade 一个 shared cluster，确认 `state=upgrading` 和 CR version 更新

---

## 16. 已知限制

1. private link 注册 / 注销与 DB 事务仍然不是原子的
2. `rpsLimit` 目前只对外展示，不由 serverless-service 执行限速
3. release 行是异步 GC，不是 delete 事务内立即物理删除
4. current dev 环境无法把 create 真正跑到业务 running，原因是 dataplane PD 版本，不是本服务逻辑

---

## 17. Dev e2e 重跑手册（alicloud tmptest）

> 2026-05-26 在 alicloud `dev-ap-southeast-1-f01` 跑通 create → scale → delete 全链路的实际操作。下一次重跑时按这里走。
>
> 为什么走 alicloud：dev us-east-1 dataplane 的 `replication-worker` 被 disable，PD 版本也偏低；alicloud 是当前唯一 PD 健康的 dev shard。

### 17.0 关键资源 / kubeconfig

| 用途 | 路径 / 标识 |
| --- | --- |
| 全局 serverless-svc 跑的 K8s (us-west-2) | `/Users/jiangjianyuan/work/git/pingcap/dataflow-service/dev-base.kubeconfig` |
| Namespace（全局） | `nightly-ms` |
| Alicloud dataplane K8s | `/Users/jiangjianyuan/work/git/pingcap/ali-southeast-1-f01.kubeconfig` |
| Dataplane namespace | `tidb-serverless` |
| 测试集群 | `cluster_id=10923159920656144191`（keyspace 3340, alicloud ap-southeast-1, project 3200439, org 60205）|
| serverless DB（nightly） | host `nightly-serverless-rdsae42f63.c6owagykop0g.us-west-2.rds.amazonaws.com`, user `nightly_serverless`, pass `'UEGJ%EawyvSR8lH_demZUl53'`, db `serverless_nightly` |
| 测试 sink（公网 MySQL） | `34.208.189.148:3306` user `root` pass `test123456!` — clusterproxy 在 alicloud 这一侧能连上 |
| 测试用 PR 镜像 | `385595570414.dkr.ecr.us-west-2.amazonaws.com/dbaas/nightly/serverless-service:<PR-commit>`（auto-bumper 会自动同步到 nightly grpc/worker/http；workflow STS 不在 main 上所以 nightly-ms 无 workflow pod，由 tmptest 自带）|
| Alicloud 上 TiCDC 容器镜像 | `tidbcloud-registry-vpc.ap-southeast-1.cr.aliyuncs.com/tidbcloud/ticdc:v8.5.4-nextgen.202510.7`（VPC registry，nightly 节点能拉；不要用 AWS ECR）|

### 17.1 一次性 dataplane RBAC（已做过的话跳过）

dbaas-proxy SA (`tidb-admin/dbaas-proxy`) 默认没有 cert-manager + storageclass 权限，create DAG 会 403。**alicloud `dev-ap-southeast-1-f01` 已经手工补好**，下一次只需要 verify：

```bash
KC=ali-southeast-1-f01.kubeconfig
KUBECONFIG=$KC kubectl auth can-i create certificates.cert-manager.io \
  -n tidb-serverless --as=system:serviceaccount:tidb-admin:dbaas-proxy   # 期望: yes
KUBECONFIG=$KC kubectl auth can-i list storageclasses.storage.k8s.io \
  --as=system:serviceaccount:tidb-admin:dbaas-proxy                       # 期望: yes
```

补权限的 patch（如果重建了 dataplane 需要重新打）：在 ClusterRole `dbaas-proxy` 末尾加：

```yaml
- apiGroups: [cert-manager.io]
  resources: [certificates, issuers]
  verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [storage.k8s.io]
  resources: [storageclasses]
  verbs: [get, list, watch]
```

apply 后 `kubectl -n tidb-admin rollout restart deploy dbaas-proxy`。

aws-shared-cd 那边的等价 PR 是 `feat/dbaas-proxy-cdc-newarch-rbac`，没合就先手补。

### 17.2 tmptest 命名空间组合（**四个 pod 都要起**）

放在 `nightly-ms` 下，bare Pods，imagePullSecret 走默认 SA。所有 yaml 模板见 `/tmp/tmptest-cdc-alicloud/`，下次重跑直接 `kubectl apply -f /tmp/tmptest-cdc-alicloud/`。

| Pod | mode arg | 必要原因 |
| --- | --- | --- |
| `tmptest-cdc-http` | `http` | HTTP REST gateway, `:8080`, 用 `kubectl port-forward` 进来 |
| `tmptest-cdc-grpc` | `grpc` | 内部 ops RPC, `:10000`，目前没直接用但和 http 配套 |
| `tmptest-cdc-workflow-1` | `workflow` | **fastflow DAG executor — pod name 必须 `-N` 结尾**（fastflow keeper 用正则 `^.+-(\d+)$` 校验 worker key）|
| `tmptest-cdc-br-worker` | `brworker`（**不是 `br-worker`**） | **`cdcSyncCreatingWorker` 在 BRWorker 里注册**（`global_workers.go` 第二个 `newPeriodWorkerWithFixedDelay` 块），10s 一次跑 `syncNewArchChangefeedResources`，负责把 `ti_cdc_clusters.actual_specification` 从空补成 expected |

四个 pod 都 mount `tmptest-cdc-config` ConfigMap 和 `serverless-infra-kubeconfig` Secret，serviceAccount 复用 `central`。

### 17.3 tmptest ConfigMap 关键设置

完整 yaml 在 `/tmp/tmptest-cdc-alicloud/01-config.yaml`，几个**必须**的点：

```yaml
server:
  stage: dev               # IsTestStage() 返回 true → skipLoopback 生效
  rbacEnabled: false       # 否则 RBAC 卡 org 60205 校验
worker:
  regionConfig:
    - region: ap-southeast-1
      provider: alicloud
      k8sConfig:
        - kubernetes: dev-ap-southeast-1-f01
          clusterProxyUrl: nlb-9ezcc77q6t3gsdg5ym.ap-southeast-1.nlb.aliyuncsslbintl.com
          k8sIngressUrl: regionalsvc01.ap-southeast-1.dev.shared.alicloud.tidbcloud.com
          forceNewClustersToCanaryPool: true
          disableCanaryToStandardPool: true
          forceNewEssentialToCanaryPool: true
cdc:
  newArch:
    enabled: true
    namespace: tidb-serverless
    upstreamNamespace: tidb-serverless
    upstreamClusterName: serverless-cluster
    # per-region 覆盖：alicloud 节点拉不到 AWS ECR / GCR，必须走 VPC ACR；
    # version 也可按 region 覆盖，用于灰度发布。
    regionConfig:
      - provider: alicloud
        region: ap-southeast-1
        baseImage: tidbcloud-registry-vpc.ap-southeast-1.cr.aliyuncs.com/tidbcloud/ticdc
        version: v8.5.4-nextgen.202510.7
    # per-provider 兜底（每种云一个 registry）；region 没配 baseImage 时回退到这里
    baseImagePerProvider:
      aws:      385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/ticdc
      alicloud: tidbcloud-registry-vpc.ap-southeast-1.cr.aliyuncs.com/tidbcloud/ticdc
    # 三层都没配时的全局兜底
    # baseImage: 385595570414.dkr.ecr.us-west-2.amazonaws.com/tidbcloud/ticdc
    version: v8.5.4-nextgen.202510.7
    issuerKind: ClusterIssuer
    issuerName: tidb-serverless-issuer
    rcuTiers:
      - { rcu: 2,  rpsLimit: 5000  }
      - { rcu: 4,  rpsLimit: 10000 }
      - { rcu: 8,  rpsLimit: 20000 }
      - { rcu: 16, rpsLimit: 40000 }
      - { rcu: 32, rpsLimit: 80000 }
    cpuPerRcu: 1
    instanceTypes:
      - { name: "16c", cpuPerReplica: 16, memoryGi: 64, storageGi: 200, iops: 12000, throughputMbs: 1000 }
    # 这里与代码内置默认值保持一致；tmptest 如需跑在更小节点上可自行覆盖。
    nodeSelector:
      serverless.tidbcloud.com/node: adhoc
      adhoc-cpu: "16"
      adhoc-memory: "64"
      serverless.tidbcloud.com/capacity-type: ON_DEMAND
    tolerations:
      - key: serverless.tidbcloud.com/node
        operator: Equal
        value: adhoc
        effect: NoSchedule
mysql:
  host: nightly-serverless-rdsae42f63.c6owagykop0g.us-west-2.rds.amazonaws.com
  user: nightly_serverless
  port: 3306
  database: serverless_nightly
```

剩余的 `server.secret`、`auth0`、`svcaddr`、`gardener`、`infraAPI` 等等照抄 `kubectl get cm serverless-svc-config-xxxxxx -o yaml` 现成的就行。

### 17.4 重跑步骤（精简）

```bash
KC_NIGHT=/Users/jiangjianyuan/work/git/pingcap/dataflow-service/dev-base.kubeconfig
KC_ALI=/Users/jiangjianyuan/work/git/pingcap/ali-southeast-1-f01.kubeconfig

# 1. 起一个 mysql 客户端 pod（exec 进去查 DB）
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms run mysql-debug --image=mysql:8.0 \
  --restart=Never --command -- sleep 14400
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms wait pod/mysql-debug --for=condition=Ready

# 2. 清空 newarch 表 + 失败 DAG（每次重跑前都要做，否则 stale 行会让 picker 把 rcu 加错）
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms exec mysql-debug -- mysql \
  -h<host> -u<user> -p<pass> -D serverless_nightly --connect-timeout=10 \
  -e "DELETE FROM ti_cdc_changefeed_resources;
      DELETE FROM ti_cdc_clusters;
      DELETE FROM kernel_ti_cdc_changefeeds;
      UPDATE dag_instances SET status='failed', reason='pre-test cleanup'
        WHERE dag_id LIKE 'cdc-newarch%' AND status IN ('running','init','scheduled','retrying');"

# 3. 部署 tmptest pods + cm + service
KUBECONFIG=$KC_NIGHT kubectl apply -f /tmp/tmptest-cdc-alicloud/

# 4. 等 4 个 tmptest pod Running
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms get pod -l tmptest=true -w   # Ctrl-C when all Running

# 5. port-forward http pod 到本地，借 skipLoopback 跳过 auth
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms port-forward pod/tmptest-cdc-http 18080:8080 &
sleep 4
curl -s -o /dev/null -w "healthz: %{http_code}\n" http://127.0.0.1:18080/healthz   # 期望 200

# 6. Create changefeed
curl -s -X POST -H 'Content-Type: application/json' \
  -d @/tmp/tmptest-cdc-alicloud/create-cf.json \
  "http://127.0.0.1:18080/v1beta1/clusters/10923159920656144191/changefeeds"

# 7. Scale（举例 2rcu -> 8rcu）
curl -s -X POST -H 'Content-Type: application/json' \
  -d '{"cluster_id":"10923159920656144191","changefeed_id":"<cfid>","rcu":8}' \
  "http://127.0.0.1:18080/v1beta1/clusters/10923159920656144191/changefeeds/<cfid>:scale"

# 8. Delete
curl -s -X DELETE \
  "http://127.0.0.1:18080/v1beta1/clusters/10923159920656144191/changefeeds/<cfid>"
```

`create-cf.json`（已在 `/tmp/tmptest-cdc-alicloud/create-cf.json`）：

```json
{
  "cluster_id": "10923159920656144191",
  "display_name": "tmptest-newarch-xx",
  "arch": "TICDC_NEWARCH",
  "rcu": 2,
  "sink": {
    "type": "MYSQL",
    "mysql": {
      "network":        { "network_type": "PUBLIC", "public_endpoint": "34.208.189.148:3306" },
      "authentication": { "user_name": "root",      "password": "test123456!" }
    }
  },
  "filter":         { "filter_rule": ["*.*"], "mode": "IGNORE_NOT_SUPPORT_TABLE" },
  "start_position": { "mode": "FROM_NOW" }
}
```

### 17.4.1 2026-05-27 兼容性 e2e 实际流程

这一段记录的是 2026-05-27 在 nightly alicloud 上按 PR 镜像实际跑过的完整过程。后续 agent 如果要复现，不要只跑 newarch create / delete，必须同时保留一条 old-path changefeed，验证常规 worker 在同一 cluster 内按 `arch` 隔离同步。

#### A. 部署待测镜像

PR workflow 会把镜像打到 nightly ECR：

```bash
IMAGE=385595570414.dkr.ecr.us-west-2.amazonaws.com/dbaas/nightly/serverless-service:<commit>
```

正式 nightly deployment 会由 auto-bumper 更新，可以先看它是否已滚到目标 commit：

```bash
KC_NIGHT=/Users/jiangjianyuan/work/git/pingcap/dataflow-service/dev-base.kubeconfig

kubectl --kubeconfig $KC_NIGHT -n nightly-ms get deploy \
  serverless-svc-http-server serverless-svc-grpc-server \
  serverless-svc-worker serverless-svc-br-worker \
  -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[*].image}{"\n"}{end}'

kubectl --kubeconfig $KC_NIGHT -n nightly-ms rollout status deploy/serverless-svc-http-server --timeout=180s
kubectl --kubeconfig $KC_NIGHT -n nightly-ms rollout status deploy/serverless-svc-worker --timeout=180s
kubectl --kubeconfig $KC_NIGHT -n nightly-ms rollout status deploy/serverless-svc-br-worker --timeout=180s
kubectl --kubeconfig $KC_NIGHT -n nightly-ms rollout status deploy/serverless-svc-grpc-server --timeout=180s
```

正式 HTTP service 有 RBAC，直接 port-forward `svc/serverless-svc-http` 调 v1beta1 API 会得到 `403 permission denied`。本轮验证仍然使用 tmptest HTTP，因为 tmptest config 里 `server.rbacEnabled=false` 且 `stage=dev`，可走 skipLoopback。

如果 `/tmp/tmptest-cdc-alicloud/*.yaml` 里的 image 还是旧 commit，先替换：

```bash
perl -pi -e 's#serverless-service:[a-f0-9]{40}#serverless-service:<commit>#g' \
  /tmp/tmptest-cdc-alicloud/02-workflow-pod.yaml \
  /tmp/tmptest-cdc-alicloud/03-grpc-pod.yaml \
  /tmp/tmptest-cdc-alicloud/04-http-pod.yaml \
  /tmp/tmptest-cdc-alicloud/05-worker-pod.yaml \
  /tmp/tmptest-cdc-alicloud/06-br-worker-pod.yaml
```

然后重新起 tmptest：

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms apply \
  -f /tmp/tmptest-cdc-alicloud/01-config.yaml \
  -f /tmp/tmptest-cdc-alicloud/02-workflow-pod.yaml \
  -f /tmp/tmptest-cdc-alicloud/03-grpc-pod.yaml \
  -f /tmp/tmptest-cdc-alicloud/04-http-pod.yaml \
  -f /tmp/tmptest-cdc-alicloud/05-worker-pod.yaml \
  -f /tmp/tmptest-cdc-alicloud/06-br-worker-pod.yaml

kubectl --kubeconfig $KC_NIGHT -n nightly-ms wait \
  pod/tmptest-cdc-http pod/tmptest-cdc-worker pod/tmptest-cdc-br-worker pod/tmptest-cdc-workflow-1 \
  --for=condition=Ready --timeout=300s
```

#### B. 准备 DB 查询 pod

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms run mysql-debug \
  --image=public.ecr.aws/docker/library/mysql:8.0 \
  --restart=Never --command -- sleep 14400

kubectl --kubeconfig $KC_NIGHT -n nightly-ms wait pod/mysql-debug \
  --for=condition=Ready --timeout=180s
```

后续查询统一用：

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms exec mysql-debug -- \
  env MYSQL_PWD='<serverless-db-password>' mysql \
  -h nightly-serverless-rdsae42f63.c6owagykop0g.us-west-2.rds.amazonaws.com \
  -u nightly_serverless -D serverless_nightly --connect-timeout=10 \
  -e "<SQL>"
```

#### C. 老路径兼容性基线

先创建一条 old-path changefeed，确认老路径仍然能正常 running。这个 changefeed 不要拿去 migrate，先作为混合架构同步基线。

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms port-forward pod/tmptest-cdc-http 18084:8080

curl -sS -X POST "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds" \
  -H "content-type: application/json" \
  --data-binary @/tmp/tmptest-cdc-alicloud/create-old-cf.json
```

期望：

- API 返回 `arch=REPLICATION_WORKER`，初始 `state=CREATING`
- 轮询 GET 后进入 `RUNNING`
- DB 中 `cdc_changefeeds.arch='replication_worker'`
- `ti_cdc_changefeed_resources` 和 `kernel_ti_cdc_changefeeds` 对该 old-path changefeed 没有行

查询：

```sql
select id, state, arch, rcu, checkpoint_tso, updated_at
from cdc_changefeeds
where id='<old_cfid>';

select * from ti_cdc_changefeed_resources where changefeed_id='<old_cfid>';
select * from kernel_ti_cdc_changefeeds where changefeed_id='<old_cfid>';
```

List API 也要验一次：

```bash
curl -sS "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds?page_size=20"
```

注意：本轮 tmptest 环境里带 `page_token=1` 会返回 `400 invalid page token`，所以兼容性 list 验证用不带 token 的第一页即可。

#### D. newarch create + mixed arch sync worker 验证

再创建一条 newarch changefeed：

```bash
curl -sS -X POST "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds" \
  -H "content-type: application/json" \
  --data-binary @/tmp/tmptest-cdc-alicloud/create-newarch-cf.json
```

轮询 DB，直到：

```sql
select id, state, arch, rcu, checkpoint_tso, error, updated_at
from cdc_changefeeds
where id='<newarch_cfid>';

select changefeed_id, expected_specification, actual_specification, updated_at
from ti_cdc_changefeed_resources
where changefeed_id='<newarch_cfid>';

select changefeed_id, ticdc_cluster_name, kernel_status, updated_at, deleted_at
from kernel_ti_cdc_changefeeds
where changefeed_id='<newarch_cfid>';

select id, ticdc_cluster_name, state, expected_specification, actual_specification, deleted_at
from ti_cdc_clusters
where ticdc_cluster_name='ess-ticdc-ap-southeast-1-3e09733f';
```

期望：

- `cdc_changefeeds.state=running`
- `arch=ticdc_newarch`
- resource `expected_specification == actual_specification`
- kernel row `kernel_status=active`
- shared cluster `state=running` 且 expected/actual 一致

同时确认 old-path 基线仍是 running：

```sql
select id, state, arch, rcu, updated_at
from cdc_changefeeds
where id in ('<old_cfid>', '<newarch_cfid>');
```

这个点覆盖了本 PR 中最关键的兼容性 bug：常规 worker 必须按 `cluster_id + arch` 分组同步。否则同一 cluster 里同时存在 old-path 和 newarch 时，如果分组第一条是 old-path，worker 会拿 old TiCDC list 去同步 newarch，导致 newarch 在 list 里查不到后被误标记为 `deleted`。

workflow 日志里应能看到三阶段创建：

- insert `kernel_ti_cdc_changefeeds`，`kernel_status=pending`
- resource actual ready 后调用 kernel create
- update `kernel_status=active`
- 主表从 `creating` 改为 `running`

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms logs pod/tmptest-cdc-workflow-1 --tail=200 | \
  grep -E '<newarch_cfid>|CreateChangefeed|kernel_status|active|error|failed'
```

dataplane 检查：

```bash
KC_ALI=/Users/jiangjianyuan/work/git/pingcap/ali-southeast-1-f01.kubeconfig

kubectl --kubeconfig $KC_ALI -n tidb-serverless get tc,sts,pod,pvc,cert | grep ess-ticdc
```

期望 `TidbCluster` Ready=True，`StatefulSet 1/1`，pod Running，PVC Bound，两个 cert True。

#### E. scale 兼容性和容量不足判定

先做 2rcu → 8rcu：

```bash
curl -sS -X POST \
  "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds/<newarch_cfid>:scale" \
  -H "content-type: application/json" \
  --data-binary '{"cluster_id":"10923159920656144191","changefeed_id":"<newarch_cfid>","rcu":8}'
```

期望控制面：

- API 返回 `state=SCALING`
- `cdc_changefeeds.rcu=8`
- `ti_cdc_changefeed_resources.expected_specification='8rcu'`
- `ti_cdc_clusters.expected_specification='8rcu'`
- TiDBCluster CR spec 被更新为 2 replicas、cpu 4、memory 8Gi

本轮 Alicloud nodepool 对第二个 TiCDC pod 报 `RecommendEmpty.InstanceTypeNoStock`，因此 dataplane 可能停在：

- `StatefulSet 1/2`
- `pod/*-ticdc-1 Pending`
- PVC for `ticdc-1` Pending
- event 包含 `ProvisionNodeFailed` / `InstanceTypeNoStock`

这种情况下不要把它判成 serverless-service 控制面失败。确认方法：

```bash
kubectl --kubeconfig $KC_ALI -n tidb-serverless describe pod ess-ticdc-ap-southeast-1-3e09733f-ticdc-1
kubectl --kubeconfig $KC_ALI -n tidb-serverless get tc ess-ticdc-ap-southeast-1-3e09733f -o yaml
```

为了继续覆盖 delete，可以再 scale down 到 4rcu：

```bash
curl -sS -X POST \
  "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds/<newarch_cfid>:scale" \
  -H "content-type: application/json" \
  --data-binary '{"cluster_id":"10923159920656144191","changefeed_id":"<newarch_cfid>","rcu":4}'
```

期望控制面回到：

- `cdc_changefeeds.state=running, rcu=4`
- resource `4rcu / 4rcu`
- shared cluster `running, 4rcu / 4rcu`

注意：本轮观察到 TiDBCluster CR spec 已经降到 1 replica，但 StatefulSet 还短时间残留 2 replicas，Ready condition 是 `TiCDCCaptureNotReady`。后续 delete DAG 仍能清理这种半失败扩容残留，这是 delete 覆盖点。

#### F. delete newarch 覆盖半失败扩容残留

```bash
curl -sS -X DELETE \
  "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds/<newarch_cfid>"
```

期望：

```sql
select id, state, arch, rcu, deleted_at, updated_at
from cdc_changefeeds
where id='<newarch_cfid>';

select changefeed_id, expected_specification, actual_specification
from ti_cdc_changefeed_resources
where changefeed_id='<newarch_cfid>';

select changefeed_id, ticdc_cluster_name, kernel_status, deleted_at
from kernel_ti_cdc_changefeeds
where changefeed_id='<newarch_cfid>';

select id, ticdc_cluster_name, state, expected_specification, actual_specification, deleted_at
from ti_cdc_clusters
where ticdc_cluster_name='ess-ticdc-ap-southeast-1-3e09733f';
```

- 主表 `state=deleted`
- resource expected/actual 均为空
- kernel row `kernel_status=deleted`
- shared cluster `state=deleted`，expected/actual 均为空
- old-path 基线仍然 running
- Alicloud 上 `kubectl get tc,sts,pod,pvc,cert | grep ess-ticdc` 无输出

#### G. migrate 需要单独 old-path changefeed

迁移会按设计暂停 source old-path changefeed，所以不要使用 C 步的兼容性基线。重新创建一条 migration 专用 old-path changefeed：

```bash
curl -sS -X POST "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds" \
  -H "content-type: application/json" \
  --data-binary @/tmp/tmptest-cdc-alicloud/create-old-cf.json
```

等它 running 且 checkpoint 出现：

```sql
select id, state, arch, rcu, checkpoint_tso, updated_at
from cdc_changefeeds
where id='<migrate_source_old_cfid>';
```

触发 migrate：

```bash
curl -sS -X POST \
  "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds/<migrate_source_old_cfid>:migrate" \
  -H "content-type: application/json" \
  --data-binary '{
    "cluster_id":"10923159920656144191",
    "changefeed_id":"<migrate_source_old_cfid>",
    "rcu":4,
    "display_name":"tmptest-migrated-newarch-0527"
  }'
```

期望 API 返回：

- 新 changefeed ID
- `arch=TICDC_NEWARCH`
- `state=CREATING`
- `startPosition.mode=FROM_TSO`
- `startPosition.tso == source.checkpoint_tso`

然后查 DB：

```sql
select id, name, state, arch, rcu, start_tso, checkpoint_tso, error, updated_at, deleted_at
from cdc_changefeeds
where id in ('<migrated_newarch_cfid>', '<migrate_source_old_cfid>', '<old_baseline_cfid>')
order by created_at desc;

select changefeed_id, ticdc_cluster_name, expected_specification, actual_specification, updated_at
from ti_cdc_changefeed_resources
where changefeed_id='<migrated_newarch_cfid>';

select changefeed_id, ticdc_cluster_name, kernel_status, updated_at, deleted_at
from kernel_ti_cdc_changefeeds
where changefeed_id='<migrated_newarch_cfid>';
```

期望：

- source old-path `state=paused`
- migrated newarch `state=running`
- migrated newarch `start_tso == source.checkpoint_tso`
- resource `4rcu / 4rcu`
- kernel `active`
- 兼容性 old baseline 仍然 running，直到最后 cleanup

本轮迁移首次失败过一次，错误是：

```text
connect mysql failed, reason: unable to connect sql url 34.208.189.148:3306 error crypto/rsa: message too long for RSA key size
```

根因：迁移逻辑把 DB 中已经加密的 source sink secret 原样复制到新 VO，`CreateChangefeed` 的 precheck 和 `ToEntity` 又把它当明文使用。修复点是 `internal/domain/service/cdc_migrate.go`：构建 migrated VO 时深拷贝 sink/filter，并把 MySQL / Kafka / CloudStorage 的 secret 解密成明文，再交给现有 `CreateChangefeed` 做 validate、precheck 和重新加密。后续如果又看到同类 RSA error，优先检查是不是迁移路径又传了密文给 create/test connection。

#### H. cleanup

测试结束按 API 删除所有本轮创建的 changefeed，不要只手动删 DB：

```bash
for cf in <migrated_newarch_cfid> <migrate_source_old_cfid> <old_baseline_cfid>; do
  curl -sS -X DELETE \
    "http://127.0.0.1:18084/v1beta1/clusters/10923159920656144191/changefeeds/$cf"
done
```

最终确认：

```sql
select id, name, state, arch, rcu, deleted_at, updated_at
from cdc_changefeeds
where id in ('<migrated_newarch_cfid>', '<migrate_source_old_cfid>', '<old_baseline_cfid>')
order by id;

select changefeed_id, expected_specification, actual_specification
from ti_cdc_changefeed_resources
where changefeed_id in ('<migrated_newarch_cfid>', '<migrate_source_old_cfid>', '<old_baseline_cfid>');

select changefeed_id, kernel_status, deleted_at
from kernel_ti_cdc_changefeeds
where changefeed_id in ('<migrated_newarch_cfid>', '<migrate_source_old_cfid>', '<old_baseline_cfid>');

select id, ticdc_cluster_name, state, expected_specification, actual_specification, deleted_at
from ti_cdc_clusters
where ticdc_cluster_name='ess-ticdc-ap-southeast-1-3e09733f';
```

期望：

- 三条 `cdc_changefeeds.state=deleted`
- newarch resource expected/actual 清空
- newarch kernel row `deleted`
- shared cluster `deleted`
- Alicloud 上 `kubectl --kubeconfig $KC_ALI -n tidb-serverless get tc,sts,pod,pvc,cert | grep ess-ticdc` 无输出

最后清掉 tmptest 和 mysql-debug：

```bash
kubectl --kubeconfig $KC_NIGHT -n nightly-ms delete pod \
  tmptest-cdc-http tmptest-cdc-worker tmptest-cdc-br-worker \
  tmptest-cdc-workflow-1 tmptest-cdc-grpc mysql-debug \
  --ignore-not-found --wait=false
```

### 17.5 验证用查询（每一步看一下，确认状态前进）

| 检查 | 期望值 |
| --- | --- |
| `SELECT id, state FROM cdc_changefeeds WHERE id='<cfid>'` | creating → running（delete 后 deleted）|
| `SELECT state, expected_specification, actual_specification FROM ti_cdc_clusters WHERE deleted_at IS NULL` | running, expected == actual |
| `SELECT changefeed_id, expected_specification, actual_specification FROM ti_cdc_changefeed_resources` | expected == actual |
| `SELECT changefeed_id, kernel_status FROM kernel_ti_cdc_changefeeds WHERE deleted_at IS NULL` | `active` |
| `kubectl --kubeconfig=$KC_ALI -n tidb-serverless get tc | grep ess-ticdc` | READY=True |
| `kubectl --kubeconfig=$KC_ALI -n tidb-serverless get pod | grep ess-ticdc` | `<replicas>/1 Running` |
| `SELECT checkpoint_tso FROM cdc_changefeeds WHERE id='<cfid>'` | 数字会向前推进 |
| delete 后 `kubectl --kubeconfig=$KC_ALI -n tidb-serverless get tc,sts,pvc,cert,secret | grep ess-ticdc` | 空 |

### 17.6 故障排查 cheat sheet

| 现象 | 原因 | 处理 |
| --- | --- | --- |
| workflow pod CrashLoop, log "worker key format is incorrect" | fastflow 要 pod name `<x>-N` | 用 `tmptest-cdc-workflow-1`，**不要**用 `tmptest-cdc-workflow` |
| 403 Forbidden `you have no permission to access org 60205` | RBAC 没关 | config 里加 `server.rbacEnabled: false` 后 bounce http pod |
| 403 cert-manager.io is forbidden | dbaas-proxy SA 缺权限 | 见 §17.1 |
| 400 namespace is required（create storage class） | 历史 bug：cluster-scoped 资源也要传 namespace 字段。**已在 `ensureNewArchStorageClass` 中固定填 `cfg.Namespace` 修好** | 检查 tmptest config 的 `cdc.newArch.namespace` 是否非空 |
| newarch shared TiCDC 的 PVC 一直 `Pending`，event 显示 `no storage class is set` | tmptest config 的 `cdc.newArch.instanceTypes[]` 把 `iops / throughputMbs` 都留成了 0，`storageClassName(cloud, spec)` 会返回空串，最终 TidbCluster 的 PVC 模板不会带 StorageClass，workflow 就会一直卡在 `newarch changefeed resource is not ready yet` | 给测试用 instance type 补非 0 的 `iops`（Alicloud 至少要有这个；例如 `4000`）和 `throughputMbs`（例如 `400`），然后删掉已经用错模板创建出来的 `ess-ticdc-*` TidbCluster / PVC，让 br-worker 按新配置重建 |
| ImagePullBackOff, "no basic auth credentials" | baseImage 指到 alicloud 拉不下来的 registry（如 AWS ECR） | 在 `cdc.newArch.regionConfig[].baseImage` 配 per-region override，或在 `cdc.newArch.baseImagePerProvider` 配 per-provider 默认 |
| Pod Pending FailedScheduling "0/N nodes available" | instanceTypes 太大 | 把 instanceTypes 改成单 4c × 8GB 或更小 |
| `ti_cdc_clusters` 一直 `creating` / `actual_specification=''` | 没有 br-worker | 起 `tmptest-cdc-br-worker`（**mode = `brworker`** 一个词）|
| DAG 一直 retrying "newarch changefeed resource is not ready yet" | 同上 |  |
| tmptest pods 莫名消失 | 不知道是谁在 reap nightly-ms 下无主 pod | 重新 `kubectl apply -f /tmp/tmptest-cdc-alicloud/` 即可；后续要做更稳的应该改成 Deployment/StatefulSet |
| 老 cf 留下 stale row（`cfd-xxxx` 已删但 `ti_cdc_changefeed_resources` 还在） | delete 后某些 DAG 中断没清完 | 手 `DELETE FROM ti_cdc_changefeed_resources WHERE changefeed_id='<old>'` 再继续 |

### 17.7 收尾清理

```bash
KC_NIGHT=/Users/jiangjianyuan/work/git/pingcap/dataflow-service/dev-base.kubeconfig
# 删 tmptest 资源
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms delete pod,svc,cm -l tmptest=true --wait=false
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms delete cm tmptest-cdc-config --ignore-not-found
KUBECONFIG=$KC_NIGHT kubectl -n nightly-ms delete pod mysql-debug --ignore-not-found
# port-forward 杀掉
pkill -f "port-forward pod/tmptest-cdc-http"
```

dataplane 上的 alicloud 资源由 delete DAG 自己清理，正常情况下不需要手动收。

---

## 18. 最终结论

**Essential CDC newarch 当前已经具备完整的控制面实现：用户侧可以 create / scale / delete / migrate，运维侧可以 list / inspect / upgrade shard 级 shared TiCDC cluster；shared cluster 的版本、规格、调度与回收都已有明确 owner 和收敛路径。上线前剩余的重点不在 serverless-service 代码，而在 dataplane 依赖是否齐全，尤其是 clusterproxy RBAC、cert-manager issuer，以及 PD 版本。**
