边缘网关有高配(4 核 8G,跑 Docker 或 K3s)也有低配(单核 256M,跑不了容器,只能原生进程)。如果用 if-else 在每处代码里区分这三种运行时,项目会变成一团乱麻。本文用策略模式拆解这个多态问题------Docker 容器、原生进程、K3s Pod,三种形态一个接口,实现"加一种部署形态只需写一个新类、不改一行旧代码"。
一、开篇场景:三种节点,三种命运
你的边缘平台发布了。三个客户同时接入:
- 客户 A 的网关:x86 工控机,4 核 8G,Ubuntu 20.04,装了 Docker
- 客户 B 的网关:ARM 小板子,单核 256M,跑的是精简版 Linux,Docker 根本装不上
- 客户 C 的网关:x86 工控机,8 核 16G,技术团队有 K8s 经验,希望模块以 K3s Pod 形式运行,方便用 Helm Chart 部署和 K8s 生态工具
客户 A 说:"你们的模块用 Docker 跑,隔离好、好升级。"
客户 B 说:"我没 Docker,你们得支持原生进程。"
客户 C 说:"我会在网关上装 K3s,你们得支持把模块部署成 Pod,最好还能支持 Helm Chart。"
现在的问题是------同一套代码要同时支持三种完全不同的模块管理方式:
| 操作 | Docker 方式 | 进程方式 | K3s Pod 方式 |
|---|---|---|---|
| 创建模块 | Docker API ContainerCreate | 启动子进程 + 写入 cgroup | client-go Pods().Create() + Secret |
| 启动模块 | Docker API ContainerStart | 向进程发 SIGCONT | K3s 自动调度启动,Start 只做等待确认 |
| 停止模块 | Docker API ContainerStop | 向进程发 SIGTERM | Pods().Delete() 优雅终止 |
| 删除模块 | Docker API ContainerRemove | kill 进程 + 清除 cgroup | 删 Pod + ConfigMap + Secret |
| 查询状态 | Docker API ContainerInspect | 查 /proc/{pid}/status | Pods().Get() 查 Pod Phase |
如果你在代码里到处写 if type == "docker" { ... } else { ... },一个月后你的代码会长这样:
go
func StartModule(moduleId string, moduleType string) error {
if moduleType == "docker" {
// 50 行 Docker API 调用
} else if moduleType == "process" {
// 50 行进程管理代码
} else if moduleType == "k8s" {
// 50 行 K8s API 调用
}
}
7 个操作 × 3 种类型 = 21 个分支。再往后加?每加一种就是 7 个新分支。这就是典型的开闭原则反面教材。
二、概念铺垫:策略模式为什么适合这里
前置知识 :策略模式来自 GoF 23 种设计模式。这篇文章还会提到三个 SOLID 设计原则------开闭原则 (对扩展开放、对修改关闭,加新功能不改旧代码)、里氏替换 (子类随时可以替换父类而不影响程序正确性)、单一职责(一个类只做一件事)。你不需背定义,文章里的代码会自然体现这些原则。
本文涉及的 Go 包 :
"os""fmt""sync""syscall""os/exec""path/filepath""time""context"
"github.com/docker/docker/client""github.com/docker/docker/api/types""github.com/docker/docker/api/types/container""github.com/docker/docker/api/types/network"
2.1 策略模式的本质
策略模式(Strategy Pattern)的核心思想就一句话:
定义一组算法(策略),把它们封装成独立的类,让它们可以互相替换。调用方只管"做这件事",不关心"怎么做"。
用在这里:
- 算法 = Docker 创建模块的代码、Process 创建模块的代码、K3s Pod 创建模块的代码
- 封装成独立的类 =
DockerModuleManager、ProcessModuleManager、K8sModuleManager - 互相替换 = 通过类型字段自动选
- 调用方不管怎么做 = DeployMaster 只调
mgr.Start(moduleId),不关心底层是 Docker 还是进程还是 K3s Pod
2.2 Go 的 interface 天然支持策略模式
Go 没有继承,但有 interface。策略模式在 Go 中就是三样东西:
- 一个 interface(定义能力契约)
- 多个 struct 实现 interface(各自有不同策略)
- 一个 注册表(按类型找到对应的实现)
这比 Java 的抽象类 + 子类继承更直接,比 C++ 的虚函数表更安全(编译期类型检查)。
通用原理 :策略模式的本质是多态 (Polymorphism)------面向对象编程的核心概念。Go 通过 interface 实现了"鸭子类型"的多态:只要一个 struct 实现了 interface 里所有方法,它就是那个类型,不需要显式声明继承关系。另外,Proxy 路由器的"查表 + 委托"模式本质上是依赖注入(Dependency Injection)的变体------调用方依赖抽象接口,具体实现由外部注入。第 21 篇 DataBridge 的插件工厂、第 20 篇 EdgeRuntimeSDK 的六种 Client,用的都是同一套思想。
三、方案设计:三层结构------接口、实现、路由器
3.1 整体架构
┌──────────────────────────────────────────┐
│ DeployMaster / 其他调用方 │
│ 只需要知道 moduleId + moduleType │
│ proxy.Start(moduleId, moduleType) │
└────────────────┬─────────────────────────┘
│
┌────────────────▼─────────────────────────┐
│ ModuleManagerProxy │
│ │
│ moduleMgrMap = { │
│ "docker": DockerModuleManager, │
│ "process": ProcessModuleManager, │
│ "k8s": K8sModuleManager, │
│ } │
│ │
│ func Start(moduleId, mType string) error│
│ return proxy.moduleMgrMap[mType] │
│ .Start(moduleId) │
│ } │
└──┬──────────────┬─────────────┬──────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Docker │ │ Process │ │ K8s │
│ ModuleMgr│ │ ModuleMgr│ │ ModuleMgr│
│ │ │ │ │ │
│ Create() │ │ Create() │ │ Create() │
│ Start() │ │ Start() │ │ Start() │
│ Stop() │ │ Stop() │ │ Stop() │
│ Remove() │ │ Remove() │ │ Remove() │
│ List() │ │ List() │ │ List() │
└──────────┘ └──────────┘ └──────────┘
3.2 第一步:定义接口------统一契约
先不管具体怎么实现,定义"一个模块管理器应该能做什么":
go
// 模块管理器接口------所有部署形态都必须实现
type ModuleManager interface {
Create(req *ModuleDeployRequest) (*ModuleInfo, error)
Start(moduleId string) error
Stop(moduleId string) error
Restart(moduleId string) error
Remove(moduleId string) error
List() ([]*ModuleInfo, error)
Get(moduleId string) (*ModuleInfo, error)
// 在容器/进程/Pod 内执行命令
Exec(moduleId string, cmd string) (*ExecResult, error)
}
这个接口是最小能力契约 ------只定义了"所有部署形态都必须能做"的操作。像 Pause()/Unpause() 这种只有 Docker 支持的操作,不放在接口里,调用方通过类型断言单独处理。
3.3 第二步:实现具体策略
Docker 实现的关键逻辑:
go
type DockerModuleManager struct {
client *docker.Client // Docker Engine API 客户端
networkMgr *NetworkManager // Docker bridge 网络管理
db *ModuleRepo // 本地 SQLite
}
func (m *DockerModuleManager) Create(req *ModuleDeployRequest) (*ModuleInfo, error) {
// 容器的名字规则:moduleId 转成合法的容器名
containerName := sanitizeContainerName(req.ModuleID)
// 将业务配置转换为 Docker API 需要的 ContainerConfig
config := &container.Config{
Image: req.Image,
Env: buildEnvList(req.Envs),
}
hostConfig := &container.HostConfig{
Binds: buildVolumeBinds(req.Volumes),
PortBindings: buildPortMap(req.Ports),
Resources: container.Resources{
Memory: req.MemoryLimit,
NanoCPUs: req.CPULimit * 1e9,
},
NetworkMode: "iot-edge-bridge", // 接入自定义 Docker 网络
}
// 第一步:自动创建宿主机挂载目录
m.ensureMountDirectories(req.Volumes)
// 第二步:创建 Docker 容器
resp, err := m.client.ContainerCreate(ctx, config, hostConfig, nil, containerName)
if err != nil {
// 处理容器名冲突:异常重启后可能残留同名容器
if isConflictError(err) {
m.client.ContainerRemove(ctx, containerName, types.ContainerRemoveOptions{Force: true})
resp, err = m.client.ContainerCreate(...) // 重试
}
}
// 第三步:将容器 ID 写入本地数据库
m.db.SaveModuleInfo(&ModuleInfo{
ModuleID: req.ModuleID,
ContainerID: resp.ID,
Type: "docker",
})
return &ModuleInfo{ID: resp.ID, Name: containerName}, nil
}
Process 实现的关键逻辑:
go
type ProcessModuleManager struct {
processIndex map[string]*os.Process // moduleId → OS 进程实例
cgroupMgr *CgroupManager // cgroup 资源隔离
mu sync.RWMutex
}
func (m *ProcessModuleManager) Create(req *ModuleDeployRequest) (*ModuleInfo, error) {
// 第一步:准备可执行文件路径
execPath := filepath.Join(req.WorkDir, req.Executable)
// 第二步:构造启动命令
cmd := exec.Command(execPath, req.Args...)
cmd.Env = buildEnvList(req.Envs)
cmd.Dir = req.WorkDir
// 第三步:创建进程(但不启动)
err := cmd.Start()
// 此时进程已经诞生,但马上被 pause 信号挂起
// 等 Start() 方法被调用时再发送继续信号
// 第四步:将进程写入 cgroup 做资源隔离
m.cgroupMgr.AddProcess(cmd.Process.Pid, &CgroupConfig{
MemoryLimit: req.MemoryLimit,
CPULimit: req.CPULimit,
})
// 第五步:建立索引
m.mu.Lock()
m.processIndex[req.ModuleID] = cmd.Process
m.mu.Unlock()
return &ModuleInfo{PID: cmd.Process.Pid, Type: "process"}, nil
}
func (m *ProcessModuleManager) Stop(moduleId string) error {
m.mu.RLock()
proc, ok := m.processIndex[moduleId]
m.mu.RUnlock()
if !ok {
return ErrModuleNotFound
}
// 优雅停止:先发 SIGTERM,等 10 秒,没死再发 SIGKILL
proc.Signal(syscall.SIGTERM)
done := make(chan error, 1)
go func() {
_, err := proc.Wait()
done <- err
}()
select {
case <-done:
return nil
case <-time.After(10 * time.Second):
proc.Signal(syscall.SIGKILL) // 10 秒优雅期过后强制 kill
return nil
}
}
3.4 第三步:Proxy 路由器------一行代码扩展
go
type ModuleManagerProxy struct {
moduleMgrMap map[string]ModuleManager // "docker"→Docker实现, "process"→Process实现, "k8s"→K3s Pod实现
}
func NewModuleManagerProxy() *ModuleManagerProxy {
return &ModuleManagerProxy{
moduleMgrMap: make(map[string]ModuleManager),
}
}
// 注册一个策略实现------这是扩展的入口
func (p *ModuleManagerProxy) Register(mType string, mgr ModuleManager) {
p.moduleMgrMap[mType] = mgr
}
// 所有操作方法都是一行委托
func (p *ModuleManagerProxy) Start(moduleId, mType string) error {
mgr, ok := p.moduleMgrMap[mType]
if !ok {
return fmt.Errorf("不支持的模块类型: %s", mType)
}
return mgr.Start(moduleId)
}
func (p *ModuleManagerProxy) Stop(moduleId, mType string) error {
mgr := p.moduleMgrMap[mType]
return mgr.Stop(moduleId)
}
// ... Create、Remove、List 都是同样的委托模式
3.5 初始化时注册
go
func InitNodeCore() *ModuleManagerProxy {
proxy := NewModuleManagerProxy()
// 注册 Docker 策略
dockerClient, _ := client.NewClientWithOpts(client.FromEnv)
proxy.Register("docker", &DockerModuleManager{
client: dockerClient,
networkMgr: NewNetworkManager(dockerClient),
})
// 注册 Process 策略
proxy.Register("process", &ProcessModuleManager{
processIndex: make(map[string]*os.Process),
cgroupMgr: NewCgroupManager(),
})
// 如果节点上检测到 K3s(内存 ≥ 1GB 且 /etc/rancher/k3s/k3s.yaml 存在)
// 可选注册 K3s Pod 策略
if k3sConfig, err := loadK3sConfig(); err == nil {
k3sClient, _ := kubernetes.NewForConfig(k3sConfig)
proxy.Register("k8s", &K8sModuleManager{clientset: k3sClient})
}
return proxy
}
3.6 扩展新策略------K3s Pod 运行时
硬件门槛:K3s 需要 ≥512MB 内存(它本身占用约 300MB)。这意味着你的边缘网关至少是 1GB 以上的工控机才能跑这个策略。256MB 小板子走 Process 策略就好------策略模式的价值正在于此:同一个接口,不同硬件自动选不同策略。
澄清一个常见误解 :K3s ≠ KubeEdge。K3s 是一套完整的 K8s (Master + Worker 都在你的边缘节点上),断网后完全自治------可以独立创建/删除 Pod、调度资源。KubeEdge 是 K8s 的远程代理------Master 在云端,边缘只有一个 Agent(EdgeCore),断网后不能创建新 Pod。本方案支持的是 K3s,不是 KubeEdge。KubeEdge 在第 24 篇作为"云原生边缘方案"会有详细对比。
如果你的边缘节点内存充足(4 核 8G 的工控机),可以在上面部署 K3s(轻量 K8s),然后所有业务模块以 Pod 形式运行。K3s 暴露的是标准 K8s API------K8sModuleManager 用同一个 client-go 库,接入 K3s 只需要把 kubeconfig 指向本地 K3s Server:
go
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
type K8sModuleManager struct {
clientset *kubernetes.Clientset
}
func (m *K8sModuleManager) Create(req *ModuleDeployRequest) (*ModuleInfo, error) {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: req.ModuleID,
Labels: map[string]string{"app": req.ModuleID},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: req.ModuleID,
Image: req.Image,
Env: buildEnvVars(req.Envs),
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resourceQuantity(req.MemoryLimit),
corev1.ResourceCPU: resourceQuantityMilli(req.CPULimit),
},
},
}},
},
}
result, err := m.clientset.CoreV1().Pods("iot-edge").Create(ctx, pod, metav1.CreateOptions{})
if err != nil {
return nil, err
}
return &ModuleInfo{PodName: result.Name, Type: "k8s"}, nil
}
// Start:K3s Pod 创建即启动(restartPolicy: Always),这里只做状态确认
func (m *K8sModuleManager) Start(moduleID string) error {
pod, err := m.getPod(moduleID)
if err != nil {
return err
}
// 如果 Pod 因为 OOM 等原因被驱逐了,K3s 会自动重建(restartPolicy)
// 这里只等待 Pod 进入 Running 状态,超时 30s
return m.waitForPhase(moduleID, corev1.PodRunning, 30*time.Second)
}
// Stop:优雅终止------先发 SIGTERM,等 terminationGracePeriodSeconds(默认 30s),超时 SIGKILL
func (m *K8sModuleManager) Stop(moduleID string) error {
// Delete Pod 本身就包含优雅终止流程
return m.clientset.CoreV1().Pods("iot-edge").Delete(
ctx, moduleID, metav1.DeleteOptions{GracePeriodSeconds: ptr(30)},
)
}
// Remove:清理 Pod + 关联资源(ConfigMap、Secret、PVC)
func (m *K8sModuleManager) Remove(moduleID string) error {
ns := "iot-edge"
// 1. 删 Pod(如果还在)
m.clientset.CoreV1().Pods(ns).Delete(ctx, moduleID, metav1.DeleteOptions{})
// 2. 删关联 ConfigMap(模块影子配置)
m.clientset.CoreV1().ConfigMaps(ns).Delete(ctx, moduleID+"-shadow", metav1.DeleteOptions{})
// 3. 删关联 Secret(MQTT 凭证)
m.clientset.CoreV1().Secrets(ns).Delete(ctx, moduleID+"-creds", metav1.DeleteOptions{})
return nil
}
// List:列出所有业务模块 Pod
func (m *K8sModuleManager) List() ([]*ModuleInfo, error) {
pods, _ := m.clientset.CoreV1().Pods("iot-edge").List(ctx, metav1.ListOptions{
LabelSelector: "managed-by=deploymaster",
})
result := make([]*ModuleInfo, 0, len(pods.Items))
for _, p := range pods.Items {
result = append(result, &ModuleInfo{
PodName: p.Name,
Status: string(p.Status.Phase), // Running/Pending/Failed
Type: "k8s",
})
}
return result, nil
}
// waitForPhase:轮询等待 Pod 进入指定阶段
func (m *K8sModuleManager) waitForPhase(name string, phase corev1.PodPhase, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
pod, err := m.getPod(name)
if err != nil {
return err
}
if pod.Status.Phase == phase {
return nil
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("Pod %s 超时未进入 %s", name, phase)
}
4. K3s Pod 如何拿到 MQTT 凭证和模块影子?
这是"模块怎么知道自己是谁"的问题。在 Process/Docker 策略里,NodeCore 通过环境变量注入 MODULE_ID 和 AUTH_TOKEN。在 K3s 策略里,同样的信息通过 K8s Secret 注入 Pod:
go
// Create 里附加:为模块创建 MQTT 凭证 Secret
func (m *K8sModuleManager) ensureModuleSecret(req *ModuleDeployRequest) error {
// 向 NodeCore 申请这个模块的 MQTT 凭证
creds, _ := m.nodeCore.GetModuleCredentials(req.ModuleID)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: req.ModuleID + "-creds"},
StringData: map[string]string{
"MODULE_ID": req.ModuleID,
"AUTH_TOKEN": creds.Token,
"MQTT_BROKER": "messagehub.iot-edge.svc:1883",
"SHADOW_TOPIC": fmt.Sprintf("/modules/%s/shadow", req.ModuleID),
},
}
_, err := m.clientset.CoreV1().Secrets("iot-edge").Create(ctx, secret, metav1.CreateOptions{})
return err
}
// Pod 里用 envFrom 把 Secret 映射为环境变量
// Pod.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{{
// SecretRef: &corev1.SecretEnvSource{
// LocalObjectReference: corev1.LocalObjectReference{Name: req.ModuleID + "-creds"},
// },
// }}
模块代码里还是同一行 EdgeRuntimeSDK.CreateAppClientFromEnv()------它从环境变量读参数,不关心底层是 NodeCore UDS 注入的还是 K8s Secret 注入的。
5. Helm Chart 部署支持
如果部署清单里带了 chart_name,Create 不走 Pod 构造,而是调 Helm SDK helm install:
go
func (m *K8sModuleManager) Create(req *ModuleDeployRequest) (*ModuleInfo, error) {
// Helm Chart 路径
if req.ChartName != "" {
helmClient := helm.NewClient(m.kubeconfig)
_, err := helmClient.Install(helm.InstallRequest{
Name: req.ModuleID,
Chart: req.ChartName,
Version: req.ChartVersion,
Values: req.Values, // 从模块影子传入
Namespace: "iot-edge",
})
return &ModuleInfo{Type: "helm", ReleaseName: req.ModuleID}, err
}
// 否则走标准 Pod 创建...
}
6. K3s Pod 生命周期 → FSM 状态机的映射
第 9 篇的 FSM 状态环在 K3s 策略下的映射:
| FSM 状态 | K3s Pod 操作 | 说明 |
|---|---|---|
| Idle → Created | Pods().Create() |
构造 Pod Spec + Secret,提交给 K3s |
| Created → Running | Pod 自动 Running | Pod 创建后 K3s 调度+启动,Start() 只做等待确认 |
| Running → NotRunning | Pods().Delete() |
K3s 优雅终止(SIGTERM → 30s → SIGKILL) |
| NotRunning → Idle | Pods().Delete() + 清 Secret/ConfigMap |
彻底清理 |
K3s 的 restartPolicy: Always 接管了进程崩溃后的重启------如果 Pod 里的模块 OOM 崩溃,K3s 自动重新拉起来。这和第 5 篇 NodeCore 的两阶段恢复是互补关系:NodeCore 恢复平台服务,K3s 恢复业务 Pod。
为什么类型叫
k8s不叫k3s? 因为 K3s 暴露的是标准 K8s API,你的 ModuleManager 用的是client-go------标准 K8s Go 客户端库。就像type: docker指"用 Docker API 管理容器"而不是指"Docker CE"这个产品,type: k8s指的是"用 K8s API 管理 Pod",不管是 K3s、MicroK8s 还是标准 K8s 提供这个 API,对接方式完全一样。如果你给 K3s 单独注册一个k3s类型,代码逻辑和k8s一模一样------纯重复。这也是策略模式的核心价值:接口定义的是"用什么协议",不是"用谁家的产品"。
改动量 : 1 个新文件 + 1 行注册代码。Proxy 和其他调用方------零改动 。低配盒子注册 process、Docker 盒子注册 docker、高配 K3s 盒子注册 k8s,同一次 proxy.Start(),底层走完全不同的路径,但调用方完全无感。
3.7 Docker 网络管理------让模块之间能互相通信
Docker 模块容器不能孤立运行------它们需要和 MessageHub(MQTT Broker)、NodeCore(UDS API)通信。我们为所有模块容器创建一个共享的 bridge 网络:
go
type NetworkManager struct {
client *docker.Client
networkID string
}
func (n *NetworkManager) EnsureNetwork() error {
networks, _ := n.client.NetworkList(ctx, types.NetworkListOptions{})
for _, net := range networks {
if net.Name == "iot-edge-bridge" {
n.networkID = net.ID
return nil
}
}
// 网络不存在 → 创建
resp, err := n.client.NetworkCreate(ctx, "iot-edge-bridge",
types.NetworkCreate{
Driver: "bridge",
IPAM: &network.IPAM{
Config: []network.IPAMConfig{{
Subnet: "169.254.254.0/24", // 链路本地地址段,不与任何真实网络冲突
}},
},
})
n.networkID = resp.ID
return err
}
// 模块容器创建后接入网络
func (n *NetworkManager) Connect(containerID string) error {
return n.client.NetworkConnect(ctx, n.networkID, containerID, nil)
}
容器启动时的网络自愈 :Docker 网桥可能因内核升级或 Docker 重启被删除,但容器仍保留旧的网络配置引用,导致 ContainerStart 报 network not found:
go
func (m *DockerModuleManager) Start(moduleID string) error {
containerID, _ := m.db.GetContainerID(moduleID)
err := m.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
if err != nil {
if isNetworkNotFound(err) {
// 网桥被删了 → 重建网桥 + 重新连接容器
m.networkMgr.EnsureNetwork()
m.networkMgr.Connect(containerID)
// 再次尝试启动
err = m.client.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
}
}
return err
}
3.8 挂载目录自动创建
模块可能需要挂载宿主机目录(如配置文件、数据持久化目录)。Docker 不会自动创建这些目录------如果目录不存在,Docker 会把它当作文件而非目录挂载,导致启动失败:
go
func (m *DockerModuleManager) ensureMountDirectories(volumes []VolumeConfig) {
for _, v := range volumes {
if v.Type == "bind" {
// 创建宿主机目录(如果不存在)
os.MkdirAll(v.Source, 0755)
// 设置目录所有者为 uid=1000, gid=1000
// 避免容器内进程因权限问题无法写入
os.Chown(v.Source, 1000, 1000)
}
}
}
四、Go 核心骨架:完整调用链路
不用策略模式 vs 用了策略模式------同样要做的事,代码差在哪里?
go
// ===== 反例:不用策略模式,DeployMaster 里写死三种运行时 =====
func (dm *DeployMaster) createModuleV1(config *ModuleConfig) error {
switch config.Type {
case "docker":
// 50 行 Docker API ContainerCreate + ContainerStart
dockerClient.ContainerCreate(ctx, ..., containerName)
dockerClient.ContainerStart(ctx, containerID, ...)
case "process":
// 50 行 exec.Command + cgroup + syscall
cmd := exec.Command(execPath, config.Args...)
cmd.Start()
cgroupMgr.AddProcess(cmd.Process.Pid, ...)
case "k8s":
// 50 行 client-go Pods().Create() + waitForRunning
k8sClient.CoreV1().Pods("iot-edge").Create(ctx, pod, ...)
waitForPodRunning(moduleID)
}
// 每加一种运行时,这个函数膨胀 50 行
return nil
}
// ===== 正例:用了策略模式,DeployMaster 只关心"做什么" =====
func (dm *DeployMaster) createModule(config *ModuleConfig) error {
req := &ModuleDeployRequest{
ModuleID: config.ModuleID,
Image: config.Image,
Type: config.Type, // "docker" / "process" / "k8s"
MemoryLimit: config.MemoryLimit,
CPULimit: config.CPULimit,
Envs: config.Envs,
Volumes: config.Volumes,
}
// 一行调用------Proxy 根据 type 自动路由到正确的 ModuleManager
info, err := dm.proxy.Create(req, config.Type)
if err != nil {
return err
}
return dm.proxy.Start(config.ModuleID, config.Type)
// 未来加第四种运行时?只改 InitNodeCore 里的一行 Register
}
DeployMaster 的 50 行 vs 5 行,这就是策略模式把"复杂度"转化为"扩展性"的威力。
五、边界与反模式
反模式一:interface 太"胖"
错误做法:
go
type ModuleManager interface {
Create() // Docker 支持,Process 支持
Stop() // Docker 支持,Process 支持
Pause() // 只有 Docker 支持!
Unpause() // 只有 Docker 支持!
Exec() // 只有 Docker 支持!
Rollback() // 逻辑耦合,不应该放在这个接口里
}
为什么错 :Process 实现被迫写 3 个 return errors.New("不支持")。调用方不知道该不该调 Pause,只能靠运行时错误判断------设计期丢失的类型安全。
正确做法 :接口只放所有策略都支持 的方法。Pause/Unpause 等特有方法通过独立的 DockerExtendedFeature 接口 + 类型断言来调用:
go
if extendFeature, ok := mgr.(DockerExtendedFeature); ok {
extendFeature.Pause(moduleId)
}
反模式二:Proxy 里面写业务逻辑
错误做法:
go
func (p *ModuleManagerProxy) Start(moduleId, mType string) error {
mgr := p.moduleMgrMap[mType]
// 这里开始做事前校验...
if moduleId == "system_core" {
return errors.New("不能停止核心模块")
}
// 这里开始做日志...
log.Infof("正在启动模块 %s", moduleId)
// 这里开始做状态更新...
db.UpdateStatus(moduleId, "starting")
// ...终于调用 mgr.Start 了
return mgr.Start(moduleId)
}
为什么错 :Proxy 的角色是路由器 ,不是业务层。校验、日志、状态更新应该由调用方(DeployMaster)负责。一旦 Proxy 里写了业务逻辑,扩展新策略时不得不关心这些逻辑会不会冲突。
正确做法:Proxy 只做查表 + 委托,一行不多。
反模式三:忘记并发安全------processIndex 无锁
错误做法:
go
type ProcessModuleManager struct {
processIndex map[string]*os.Process // 没有锁
}
正确做法 :sync.RWMutex。因为模块列表查询(读)的频率远高于创建/删除(写),用读写锁比 sync.Mutex 效率更高。
六、小结
策略模式在这个场景的关键价值:
- 开闭原则:新增部署形态只需 1 个新文件 + 1 行注册,Proxy 和其他调用方不改
- 里氏替换 :
proxy.Start(moduleId, mType)的行为在任何部署形态下语义一致 - 单一职责:Proxy 只管路由,Docker 只管容器,Process 只管进程,K8s 只管 Pod------各司其职
- 编译期安全:Go interface 保证你不可能"忘记实现某个方法"
这篇我们解决了"怎么管理模块"的问题。下一篇要解决一个更棘手的问题------节点突然断电重启后,这几十个模块怎么自动恢复到正确状态?
本文是《边缘平台架构沉思录:Go 架构推演与工程决策》系列的第 4 篇。