第 4 篇:策略模式实践——用统一接口管理 Docker 容器、原生进程与 K3s Pod

边缘网关有高配(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 创建模块的代码
  • 封装成独立的类 = DockerModuleManagerProcessModuleManagerK8sModuleManager
  • 互相替换 = 通过类型字段自动选
  • 调用方不管怎么做 = DeployMaster 只调 mgr.Start(moduleId),不关心底层是 Docker 还是进程还是 K3s Pod

2.2 Go 的 interface 天然支持策略模式

Go 没有继承,但有 interface。策略模式在 Go 中就是三样东西:

  1. 一个 interface(定义能力契约)
  2. 多个 struct 实现 interface(各自有不同策略)
  3. 一个 注册表(按类型找到对应的实现)

这比 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_IDAUTH_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_nameCreate 不走 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 重启被删除,但容器仍保留旧的网络配置引用,导致 ContainerStartnetwork 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 个新文件 + 1 行注册,Proxy 和其他调用方不改
  2. 里氏替换proxy.Start(moduleId, mType) 的行为在任何部署形态下语义一致
  3. 单一职责:Proxy 只管路由,Docker 只管容器,Process 只管进程,K8s 只管 Pod------各司其职
  4. 编译期安全:Go interface 保证你不可能"忘记实现某个方法"

这篇我们解决了"怎么管理模块"的问题。下一篇要解决一个更棘手的问题------节点突然断电重启后,这几十个模块怎么自动恢复到正确状态?


本文是《边缘平台架构沉思录:Go 架构推演与工程决策》系列的第 4 篇。

相关推荐
小蓝波8 小时前
docker 的命令
java·docker·容器
2301_8011847510 小时前
docker 工作
java·docker·容器
TDengine (老段)11 小时前
# TDengine TMQ 最佳实践 — 可靠消费、容错与监控
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
至乐活着11 小时前
ELK日志系统从零搭建到实战:Nginx日志采集与可视化全攻略
nginx·elk·elasticsearch·docker·日志系统
想你依然心痛12 小时前
AtomCode 在 DevOps 场景中的实战:CI/CD 流水线脚本自动生成
运维·ci/cd·docker·devops·自动化部署·github actions
摇滚侠12 小时前
DockerHub 在国内有没有替代
docker
是大强14 小时前
Docker中文件修改的三种方法
运维·docker·容器
会周易的程序员1 天前
microLog 的本地日志读取接口 log_reader — 本地日志文件读取工具开发指南
linux·物联网·架构·嵌入式·日志·iot·aiot