第一章:从Scala Akka到Go的迁移动因与整体挑战
在高并发、低延迟的微服务架构演进中,团队逐步意识到基于JVM的Scala Akka系统在资源开销、启动时长与运维复杂度方面面临持续增长的压力。单个Akka集群节点常驻内存达1.2GB以上,冷启动耗时超过8秒,且JVM GC波动易导致P99延迟毛刺;相比之下,Go编译生成的静态二进制可将内存占用压缩至80MB以内,启动时间稳定在40ms量级。
核心迁移动因
- 资源效率瓶颈:Akka Actor模型依赖线程池与消息队列,而JVM线程栈默认1MB,千级Actor即消耗GB级内存;Go goroutine初始栈仅2KB,支持百万级轻量协程
- 部署与可观测性割裂:Akka需配套配置JMX、Prometheus JMX Exporter及Logback多级日志路由;Go原生支持pprof、expvar与结构化日志(如
zerolog),指标采集无需额外代理 - 团队技能收敛需求:后端团队中Go熟练者占比已达73%,而Scala高级开发者持续流失,维护成本年增22%
典型架构差异对照
| 维度 | Scala Akka | Go(标准库 + gRPC + Gorilla) |
|---|---|---|
| 并发模型 | Actor(邮箱+调度器) | Goroutine + Channel(CSP范式) |
| 服务发现 | Akka Discovery + Consul插件 | 内置DNS SRV解析 + etcd客户端集成 |
| 错误传播 | Future.failed / PipeTo |
error返回值 + xerrors链式包装 |
迁移中的关键认知转变
放弃“Actor对等迁移”执念,转而采用显式协程编排:
// 示例:替代Akka Ask模式的超时请求封装
func askService(ctx context.Context, client pb.UserServiceClient, req *pb.GetUserRequest) (*pb.User, error) {
// ctx已携带timeout/cancellation,无需手动管理超时通道
resp, err := client.GetUser(ctx, req)
if err != nil {
return nil, xerrors.Errorf("failed to get user: %w", err) // 保留调用栈
}
return resp, nil
}
此函数可被任意goroutine安全调用,错误通过context.WithTimeout统一控制,避免Akka中ask()隐式创建临时Actor带来的生命周期泄漏风险。
真正的挑战不在于语法转换,而在于重构对“失败”的建模方式——Akka依赖监督策略(Supervision Strategy)实现容错,而Go要求开发者在每个IO边界显式处理error并决策重试、降级或熔断。
第二章:核心Actor模型概念的Go等价实现
2.1 ActorRef与Go中的Actor句柄:通道封装与生命周期管理
在 Go 中模拟 Actor 模型时,ActorRef 的等价物并非裸指针,而是带生命周期语义的通道封装体。
封装结构设计
type ActorRef struct {
inbox chan Message // 非缓冲通道,确保消息串行化处理
closed atomic.Bool // 原子标志位,支持并发安全关闭检查
done <-chan struct{} // 关闭通知通道,供外部等待终止
}
inbox 是 Actor 的唯一入口,强制消息顺序;done 由 Actor 内部 close() 触发,供监督者协调生命周期。
生命周期状态迁移
| 状态 | closed.Load() |
done 是否关闭 |
可否投递消息 |
|---|---|---|---|
| 运行中 | false | nil | ✅ |
| 正在关闭 | true | nil | ❌(丢弃) |
| 已终止 | true | closed | ❌(panic) |
终止流程(mermaid)
graph TD
A[外部调用 Stop()] --> B[设置 closed=true]
B --> C[向 inbox 发送 shutdown 消息]
C --> D[Actor 处理完队列后 close(done)]
2.2 DeathWatch机制的Go化重构:基于context取消与goroutine哨兵的故障感知
Go 中原生无 Actor 模型的 DeathWatch,需用组合式原语重建——核心是监听者生命周期与被监控者状态的双向绑定。
哨兵 goroutine 的启动与退出契约
启动时接收 context.Context 和目标 chan error;退出前必须向 done 通道发送信号,确保观察者可感知终止。
func startWatcher(ctx context.Context, targetID string, errCh chan<- error) {
// 使用 WithCancel 衍生子上下文,便于主动终止
watchCtx, cancel := context.WithCancel(ctx)
defer cancel() // 确保资源清理
go func() {
select {
case <-watchCtx.Done():
errCh <- fmt.Errorf("watcher for %s canceled: %w", targetID, watchCtx.Err())
case <-time.After(5 * time.Second): // 模拟探测超时
errCh <- fmt.Errorf("target %s unresponsive", targetID)
}
}()
}
逻辑分析:
watchCtx继承父ctx的取消/超时信号;defer cancel()防止 goroutine 泄漏;错误通过errCh异步通知,解耦监控逻辑与业务处理。
故障传播路径对比
| 特性 | Erlang/OTP DeathWatch | Go 重构方案 |
|---|---|---|
| 取消触发源 | 进程退出 | context.CancelFunc 或超时 |
| 通知方式 | DOWN 消息投递 |
channel 发送 error |
| 观察者注册成本 | O(1) 内置支持 | 手动 goroutine + channel |
graph TD
A[监控方调用 Watch] --> B[启动带 ctx 的哨兵 goroutine]
B --> C{是否收到 target 终止信号?}
C -->|是| D[向 errCh 发送 DOWN 类错误]
C -->|否| E[等待 ctx.Done 或超时]
E --> D
2.3 Persistent FSM状态机迁移:使用BadgerDB+Go结构体实现事件溯源与状态快照
核心设计思想
将状态机生命周期解耦为事件追加(Append-Only)与快照压缩(Snapshotting)双通道:事件持久化至BadgerDB的events键空间,定期将当前结构体状态序列化为snapshot键值对。
状态迁移关键代码
type OrderFSM struct {
ID string `json:"id"`
Status string `json:"status"` // pending → confirmed → shipped
Version uint64 `json:"version"` // 事件序号,用于幂等校验
}
func (f *OrderFSM) ApplyEvent(db *badger.DB, evt Event) error {
return db.Update(func(txn *badger.Txn) error {
// 写入事件:key = "evt|order_123|v5", value = JSON(evt)
key := fmt.Sprintf("evt|%s|v%d", f.ID, f.Version+1)
if err := txn.Set([]byte(key), evt.Marshal()); err != nil {
return err
}
// 更新状态结构体(内存态)
f.Status = evt.NextStatus
f.Version++
// 写入快照(每10个事件触发一次)
if f.Version%10 == 0 {
snapKey := fmt.Sprintf("snap|%s", f.ID)
txn.Set([]byte(snapKey), json.Marshal(f))
}
return nil
})
}
逻辑分析:
ApplyEvent在单事务中完成事件写入、内存状态跃迁与条件快照。Version既是事件序号也是乐观锁版本号;snap|前缀确保快照可被独立读取;BadgerDB的LSM-tree特性天然支持高吞吐事件追加。
快照与事件协同机制
| 场景 | 恢复方式 | 延迟影响 |
|---|---|---|
| 服务重启 | 先加载最新快照,再重放后续事件 | O(Δevents) |
| 跨节点同步 | 传输快照 + 增量事件日志 | 可控带宽 |
graph TD
A[新事件到达] --> B{Version % 10 == 0?}
B -->|Yes| C[序列化当前FSM为快照]
B -->|No| D[仅追加事件]
C --> E[写入 snap|<id>]
D --> E
E --> F[返回更新后状态]
2.4 Supervisor Strategy与Go错误传播树:panic恢复、errgroup协同与重启策略建模
panic恢复:defer + recover 的边界控制
Go 中无法跨 goroutine 捕获 panic,需在每个潜在崩溃点显式防御:
func guardedWorker(id int, jobChan <-chan string) {
defer func() {
if r := recover(); r != nil {
log.Printf("worker %d panicked: %v", id, r)
// 触发 supervisor 的 restart 策略
}
}()
for job := range jobChan {
process(job) // 可能 panic
}
}
recover() 仅对同 goroutine 内 defer 链有效;id 用于故障溯源,log 输出为 supervisor 提供重启决策依据。
errgroup 协同:统一错误传播与取消
errgroup.Group 实现“任一失败即整体退出”,天然适配 supervisor 的 OneForOne 策略:
| 字段 | 说明 |
|---|---|
Go(func() error) |
启动受管子任务,错误自动聚合 |
Wait() |
阻塞至全部完成或首个 error 返回 |
错误传播树建模
graph TD
Root[Supervisor] --> A[Worker-1]
Root --> B[Worker-2]
Root --> C[Worker-3]
A --> A1[DB Conn]
B --> B1[HTTP Client]
C --> C1[Cache Layer]
重启策略按子树隔离:A1 崩溃仅重启 A 分支,避免级联震荡。
2.5 Router与负载均衡:从Akka RoutingPool到Go Worker Pool + 权重调度器实战
在分布式任务分发场景中,静态线程池已难以应对异构节点的处理能力差异。Akka 的 RoutingPool 提供了 round-robin、random 等内置路由策略,但缺乏动态权重感知能力。
权重感知的 Go Worker Pool 设计
type WeightedWorker struct {
ID string
Weight int
Capacity int64
Busy atomic.Int64
}
func (w *WeightedWorker) Score() float64 {
return float64(w.Weight) / (1 + float64(w.Busy.Load())) // 权重越高、负载越低,得分越高
}
逻辑分析:
Score()采用加权倒数负载模型,Weight由节点 CPU/内存规格归一化得出(如 4C8G → 8),Busy原子记录并发请求数。该设计避免饥饿,支持热扩容。
调度策略对比
| 策略 | 动态权重 | 故障自动剔除 | 实时指标依赖 |
|---|---|---|---|
| Akka RoundRobin | ❌ | ❌ | ❌ |
| Go 均衡轮询 | ❌ | ✅ | ❌ |
| Go 权重调度器 | ✅ | ✅ | ✅(Prometheus 拉取) |
调度流程示意
graph TD
A[Task Arrival] --> B{Select Worker}
B --> C[Fetch all Workers' Weight & Load]
B --> D[Compute Score per Worker]
B --> E[Pick Top-1 by Score]
E --> F[Dispatch & Inc Busy]
第三章:消息传递与容错语义的精准对齐
3.1 消息投递保证(At-Least-Once/Exactly-Once)在Go中的事务性通道实现
数据同步机制
实现 Exactly-Once 投递需结合幂等写入与事务性消息确认。核心在于:消费位点提交与业务状态更新必须原子化。
关键实现模式
- 使用
sql.Tx包裹业务DB写入与offset持久化 - 消费者从Kafka/Pulsar拉取消息后,暂存于内存通道(
chan *Message),仅在事务成功后才标记为“已处理”
func (c *TransactionalConsumer) Process(ctx context.Context, msg *kafka.Message) error {
tx, err := c.db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback()
// 1. 业务逻辑写入(如订单创建)
if err := c.insertOrder(tx, msg.Value); err != nil { return err }
// 2. 同一事务内更新消费位点
if err := c.updateOffset(tx, msg.TopicPartition); err != nil { return err }
return tx.Commit() // 仅当两者均成功才提交
}
逻辑分析:
insertOrder与updateOffset共享同一*sql.Tx,确保原子性;若任一失败,Rollback()阻止重复消费。参数msg.TopicPartition包含分区与偏移量,是幂等性的锚点。
| 保证类型 | 实现方式 | Go典型工具 |
|---|---|---|
| At-Least-Once | 消费后立即提交offset,失败则重试 | sarama.SyncProducer |
| Exactly-Once | offset与业务状态共用数据库事务 | database/sql + Kafka |
graph TD
A[消息拉取] --> B{事务开始}
B --> C[业务DB写入]
B --> D[offset写入]
C & D --> E{是否全部成功?}
E -->|是| F[Commit → 消息确认]
E -->|否| G[Rollback → 触发重试]
3.2 Ask模式与Future超时控制:基于channel select + time.Timer的异步响应封装
在Actor模型中,Ask模式用于发起带响应的异步请求,但原生channel不具备超时能力。需结合time.Timer与select实现可控的Future语义。
核心机制:select + Timer协同
func Ask(actorAddr string, msg interface{}, timeout time.Duration) (interface{}, error) {
respCh := make(chan interface{}, 1)
timer := time.NewTimer(timeout)
defer timer.Stop()
// 发送请求(假设已封装为异步投递)
sendAsync(actorAddr, msg, respCh)
select {
case resp := <-respCh:
return resp, nil
case <-timer.C:
return nil, fmt.Errorf("ask timeout after %v", timeout)
}
}
逻辑分析:respCh容量为1避免goroutine泄漏;timer.Stop()防止误触发;select公平等待响应或超时,无竞态。
超时策略对比
| 策略 | 内存开销 | 精度 | 可取消性 |
|---|---|---|---|
time.After |
高(每次新建Timer) | 中 | ❌ |
复用*time.Timer |
低 | 高 | ✅ |
数据同步机制
- 响应通道必须为带缓冲channel(
make(chan, 1)),确保发送不阻塞; sendAsync内部需保证最多一次写入,避免重复响应竞争。
3.3 Stash与Deferred Message:Go中临时消息队列与状态驱动的延迟分发设计
在高并发事件处理系统中,Stash 作为内存级暂存区,配合 DeferredMessage 实现基于业务状态的精准延迟投递。
核心结构设计
Stash是线程安全的 map[string][]*DeferredMessage,按 key(如用户ID)隔离上下文;DeferredMessage封装 payload、触发条件(如minStatus = "confirmed")和 TTL。
消息入 stash 示例
type DeferredMessage struct {
Payload []byte
Condition func(state map[string]interface{}) bool
ExpireAt time.Time
}
stash.Put("user_123", &DeferredMessage{
Payload: []byte(`{"action":"notify"}`),
Condition: func(s map[string]interface{}) bool {
return s["status"] == "confirmed" && s["retries"] < 3 // 状态+次数双校验
},
ExpireAt: time.Now().Add(5 * time.Minute),
})
该代码将消息绑定至 user_123 上下文,并声明仅当状态满足且重试未超限时才触发。Condition 函数提供运行时动态判定能力,ExpireAt 防止永久滞留。
触发机制流程
graph TD
A[State Update] --> B{Stash.HasKey?}
B -->|Yes| C[Eval All DeferredMessage.Condition]
C -->|True| D[Deliver & Remove]
C -->|False| E[Keep & Recheck on next state change]
| 特性 | Stash | 普通 DelayQueue |
|---|---|---|
| 触发依据 | 业务状态变更 | 固定时间到期 |
| 冗余控制 | 按 key 去重合并 | 无上下文感知 |
| 扩展性 | 支持条件表达式 | 仅支持时间维度 |
第四章:持久化、集群与可观测性能力迁移
4.1 Journal与Snapshot存储适配:从Akka Persistence插件到Go EventStore接口抽象与CockroachDB集成
为统一事件溯源基础设施,需将 Akka Persistence 的 Journal(事件日志)与 SnapshotStore(快照存储)抽象为 Go 语言可插拔的 EventStore 接口:
type EventStore interface {
Append(ctx context.Context, streamID string, events []Event, expectedVersion int64) error
Load(ctx context.Context, streamID string, fromVersion int64) ([]Event, error)
SaveSnapshot(ctx context.Context, streamID string, version int64, snapshot any) error
LoadSnapshot(ctx context.Context, streamID string) (int64, any, error)
}
该接口屏蔽了底层差异:Akka 插件依赖 PersistentActor 生命周期钩子,而 Go 实现需主动管理事务边界与并发控制。expectedVersion 参数保障乐观并发控制(OCC),避免事件覆盖。
CockroachDB 集成要点
- 使用
SERIALIZABLE隔离级别保证快照与事件版本一致性 events表按(stream_id, version)复合主键,支持高效范围查询
| 组件 | Akka Persistence | Go EventStore |
|---|---|---|
| Journal 写入 | Plugin-driven | 显式 Append() 调用 |
| 快照序列化 | Jackson JSON | encoding/gob + 可选 Protobuf |
graph TD
A[Domain Actor] -->|Persist event| B[EventStore.Append]
B --> C[CockroachDB INSERT INTO events]
C --> D[ON CONFLICT DO NOTHING with version check]
4.2 Cluster Sharding的Go替代方案:基于Consul服务发现与一致性哈希的分片Actor注册中心
传统Akka Cluster Sharding在Go生态中缺乏原生对应,需构建轻量、可扩展的分片Actor注册中心。
核心设计原则
- Actor ID → 一致性哈希环定位唯一Shard节点
- 节点上下线由Consul健康检查自动触发环重平衡
- 元数据(Shard→Node映射)通过Consul KV强一致同步
一致性哈希实现(带虚拟节点)
type HashRing struct {
hash stdhash.Hash
nodes []string
vNodes map[uint32]string // 虚拟节点哈希 → 实际节点
}
func (r *HashRing) AddNode(node string, vCount int) {
for i := 0; i < vCount; i++ {
r.hash.Write([]byte(fmt.Sprintf("%s#%d", node, i)))
key := r.hash.Sum32()
r.vNodes[key] = node
r.nodes = append(r.nodes, node)
r.hash.Reset()
}
}
逻辑分析:vCount=128 提升负载均衡性;fmt.Sprintf("%s#%d", node, i) 保证虚拟节点均匀散列;Sum32() 输出32位哈希适配Consul KV路径长度限制。
注册流程时序(mermaid)
graph TD
A[Actor启动] --> B[向Consul注册服务]
B --> C[获取当前Shard分配快照]
C --> D[计算ActorID哈希并定位Shard]
D --> E[向目标节点发起Actor注册请求]
E --> F[更新Consul KV /shards/{shardId} = nodeAddr]
| 组件 | 选型理由 |
|---|---|
| 服务发现 | Consul(支持健康检查+KV+Watch) |
| 哈希算法 | jump consistent hash(O(1)插入/查询) |
| 分片粒度 | 每Shard承载≤500个Actor,避免热点 |
4.3 Distributed Publish-Subscribe:使用NATS JetStream构建跨节点事件总线与Topic路由表同步
JetStream 的分布式流能力天然支持多节点事件总线,通过 --cluster 模式启动的 NATS 服务器可自动形成 Raft 组,保障元数据(如 Stream 和 Consumer 配置)强一致性。
数据同步机制
JetStream 使用内建的 Raft 日志复制协议同步 Stream 元数据和消费偏移,确保 Topic 路由表(即 Subject-based Stream 映射关系)在集群所有节点实时一致。
示例:声明式 Topic 路由注册
# 创建带多副本的流,绑定主题前缀,实现逻辑路由表注册
nats stream add \
--subjects "order.*" \
--replicas 3 \
--storage file \
ORDERS
--subjects "order.*":定义通配符路由规则,将order.created、order.shipped等事件统一归入ORDERS流;--replicas 3:启用跨节点 Raft 复制,保障路由表变更(如新增 subject)原子广播;- 所有参与集群的 NATS Server 实例自动同步该 Stream 元数据,无需外部协调服务。
| 组件 | 同步内容 | 一致性模型 |
|---|---|---|
| Stream 元数据 | Subjects、Replicas、Retention | 强一致(Raft commit) |
| 消息数据 | Payload + sequence | 最终一致(异步复制) |
graph TD
A[Producer] -->|Publish order.created| B[NATS Node 1]
B --> C[Raft Log Append]
C --> D[NATS Node 2]
C --> E[NATS Node 3]
D & E --> F[Stream Metadata Synced]
4.4 Metrics与Tracing注入:OpenTelemetry SDK在Go Actor生命周期钩子中的埋点实践
在Actor模型中,将可观测性能力深度融入生命周期钩子(如 Started()、Stopped()、Receive())是实现细粒度诊断的关键路径。
埋点时机选择
Started():记录Actor初始化延迟与并发启动分布Receive():捕获每条消息处理耗时、错误率与上下文传播链路Stopped():追踪资源释放耗时与异常终止标记
OpenTelemetry SDK集成示例
func (a *MyActor) Receive(ctx actor.Context) {
// 从Actor上下文提取并注入trace span
span := trace.SpanFromContext(ctx.Context())
ctx = trace.ContextWithSpan(context.WithValue(ctx.Context(), "actor.id", a.ID), span)
// 记录处理指标
metrics.Must(otelMeter).RecordBatch(
ctx,
[]metric.Record{
{Value: 1, Instrument: msgReceivedCounter},
{Value: float64(time.Since(a.lastMsgTime).Microseconds()), Instrument: msgLatencyHist},
},
)
}
该代码在每次消息接收时复用当前span上下文,避免跨goroutine丢失链路;
msgReceivedCounter为int64Counter类型计数器,msgLatencyHist为float64Histogram直方图,单位为微秒。ctx.Context()已由Actor runtime自动携带父span,无需手动StartSpan。
钩子埋点效果对比
| 钩子方法 | 支持Trace Span | 支持Metrics打点 | 自动Context传播 |
|---|---|---|---|
Started() |
✅ | ✅ | ✅ |
Receive() |
✅ | ✅ | ✅ |
Stopped() |
⚠️(需显式结束) | ✅ | ❌(span已结束) |
graph TD
A[Actor Started] --> B[启动Span + 初始化指标]
B --> C[Receive消息]
C --> D[延续Span + 打点延迟/计数]
D --> E{消息处理完成?}
E -->|是| F[自动上报Span]
E -->|否| C
第五章:迁移路径总结与Go Actor框架选型建议
迁移阶段划分与关键决策点
在实际落地中,某金融风控平台将Actor模型迁移划分为三个不可跳过的阶段:协议抽象层剥离(耗时2.5人月)、状态机驱动的Actor化重构(含状态持久化适配,4.2人月)、生产灰度验证(双写比对+熔断降级,1.8人月)。其中,第二阶段发现原有基于Redis Pub/Sub的事件分发机制无法满足Actor间消息顺序性要求,最终改用基于Raft共识的本地消息队列(使用etcd WAL实现),将消息乱序率从12.7%降至0.03%。
主流Go Actor框架横向对比
以下为2024年Q2实测数据(基准测试环境:AWS c6i.4xlarge,Go 1.22,消息吞吐量单位:msg/s):
| 框架 | 启动1000 Actor耗时(ms) | 单Actor峰值吞吐 | 内存占用/Actor(MB) | 持久化支持 | 热重载能力 |
|---|---|---|---|---|---|
| Asynq(改造版) | 89 | 14,200 | 3.2 | ✅(Redis) | ❌ |
| Goka + Kafka | 217 | 8,900 | 11.6 | ✅(Kafka) | ✅(动态Topic订阅) |
| go-actor(原生) | 42 | 22,800 | 1.9 | ❌ | ✅(Runtime注入) |
| Dapr Actor | 305 | 5,100 | 28.4 | ✅(Pluggable Store) | ✅(Sidecar模式) |
注:go-actor在无持久化场景下性能最优,但其
ActorRef生命周期需手动管理;Dapr虽内存开销大,却在某电商订单履约系统中因内置分布式追踪与TLS加密能力,降低运维复杂度47%。
生产环境故障模式与规避策略
某物流调度系统在采用Goka Actor时遭遇典型问题:Kafka分区再平衡导致Actor状态丢失。解决方案并非简单增加session.timeout.ms,而是引入状态快照双写机制——每次状态变更同时写入Kafka偏移量+本地LevelDB快照,重启时优先加载最新快照再回放增量日志。该方案使平均恢复时间(MTTR)从42秒压缩至1.3秒。
混合架构下的渐进式演进路径
不推荐“全量替换”式迁移。某IoT设备管理平台采用如下路径:
- 新增设备指令下发模块 → 使用go-actor构建无状态Worker池
- 将旧有MQTT网关Consumer → 改造为Actor System入口(保留原有协议解析逻辑)
- 历史设备会话状态 → 通过gRPC调用遗留Java服务,逐步用Actor StatefulSet替代
// 关键代码:Actor入口适配器(兼容旧MQTT消息结构)
func (a *DeviceActor) Receive(ctx actor.Context) {
switch msg := ctx.Message().(type) {
case *mqtt.Message:
// 自动提取device_id作为Actor ID
deviceID := extractDeviceID(msg.Payload)
a.System.PoisonPill(deviceID) // 触发状态清理
a.System.Spawn(fmt.Sprintf("device/%s", deviceID), newDeviceActor)
}
}
监控与可观测性增强实践
在Prometheus指标体系中新增三类核心指标:
actor_lifecycle_total{state="created|restarted|stopped"}actor_mailbox_length{actor_type="device", instance="cluster-1"}actor_persistence_latency_seconds{operation="save|load"}
结合Grafana看板实时追踪Actor Mailbox堆积趋势,当actor_mailbox_length > 5000持续30秒自动触发告警并执行kubectl exec -it actor-pod -- actorctl drain <id>命令。
选型决策树图示
flowchart TD
A[是否需要强一致性状态持久化?] -->|是| B[是否已有Kafka集群?]
A -->|否| C[选择go-actor或Asynq]
B -->|是| D[Goka + Kafka]
B -->|否| E[Dapr Actor + Redis]
D --> F[检查Kafka Topic分区数 ≥ Actor并发数]
E --> G[评估Sidecar资源开销是否可接受] 