设计模式综合实战十:基于Docker实现容器编排(外观+策略+命令+观察者)

说实话,这个题目一看就有点「大杂烩」的感觉。四个设计模式凑在一起,还要跟Docker容器编排扯上关系。但我得说,这正是真实项目的常态——你很少会只用一种模式解决问题。

我记得几年前帮一个团队重构微服务部署系统,他们用Shell脚本硬编码了所有容器启动逻辑。每次新增服务类型,就得改脚本,改完还容易出Bug。我当时就想,这地方要是能用设计模式梳理一下,会清爽很多。

今天我们就来聊聊,怎么用外观、策略、命令、观察者这四个模式,搭一个灵活的容器编排框架。

整体架构:四个模式各司其职

先看看它们分别负责什么:

  • 外观模式:给上层提供一个简单的接口,隐藏Docker API的复杂性
  • 策略模式:不同的部署策略(滚动更新、蓝绿部署、金丝雀发布)可以互相替换
  • 命令模式:把每个操作(启动、停止、重启)封装成命令,支持撤销和日志
  • 观察者模式:容器状态变化时,通知各个监控组件

说白了,外观模式是门面,策略模式是脑子,命令模式是手脚,观察者模式是耳目。四个配合起来,就是一个完整的编排系统。

核心思路:不要把所有逻辑塞到一个类里。每个模式只负责一件事,组合起来反而更强大。

SVG架构图:模式协作关系

容器编排系统 - 设计模式协作图 外观模式 (OrchestratorFacade) deploy() / rollback() / scale() 策略模式 (DeployStrategy) 滚动更新 / 蓝绿部署 / 金丝雀 命令模式 (DockerCommand) start / stop / restart / remove 观察者模式 (ContainerObserver) 状态变化通知 / 日志 / 告警 Docker Engine / Docker API 容器生命周期管理 / 网络 / 存储 上层调用外观接口,外观委托给策略和命令,观察者监听容器事件 外观 策略 命令 观察者 Docker

外观模式:统一入口

先看外观模式。它的作用很简单——把Docker的底层API藏起来,给调用方一个清爽的接口。

我见过太多项目,业务代码里直接调Docker SDK,到处都是try-catch,看着就头疼。外观模式就是把这些脏活累活包起来。

// Java 示例:容器编排外观
public class OrchestratorFacade {
    private DeployStrategy strategy;
    private CommandInvoker invoker;
    private ContainerObserver observer;

    public OrchestratorFacade(DeployStrategy strategy) {
        this.strategy = strategy;
        this.invoker = new CommandInvoker();
        this.observer = new ContainerObserver();
    }

    // 对外只暴露这一个方法
    public void deploy(ServiceConfig config) {
        // 1. 策略决定怎么部署
        List<ContainerSpec> specs = strategy.generateSpecs(config);

        // 2. 命令模式执行具体操作
        for (ContainerSpec spec : specs) {
            Command cmd = new StartContainerCommand(spec);
            invoker.execute(cmd);
        }

        // 3. 观察者开始监听
        observer.watch(specs);
    }

    public void rollback(ServiceConfig config) {
        invoker.undoLast();
    }
}

你看,调用方只需要 new 一个 OrchestratorFacade,然后调 deploy() 就行。至于里面用了什么策略、执行了什么命令、怎么监听,全都不用管。

我的习惯:外观类的方法名一定要用业务语言,比如 deploy、rollback、scale,不要出现 dockerCreateContainer 这种技术术语。调用方不关心你用的是Docker还是K8s。

策略模式:部署策略可插拔

策略模式在这里解决的是「怎么部署」的问题。不同的场景需要不同的策略:

策略名称 适用场景 特点
滚动更新 常规发布 逐个替换容器,服务不中断
蓝绿部署 关键业务 两套环境,瞬间切换
金丝雀发布 灰度测试 先放少量流量验证

用策略模式实现,就是定义一个接口,然后每种策略写一个实现类:

// 策略接口
public interface DeployStrategy {
    List<ContainerSpec> generateSpecs(ServiceConfig config);
}

// 滚动更新策略
public class RollingUpdateStrategy implements DeployStrategy {
    @Override
    public List<ContainerSpec> generateSpecs(ServiceConfig config) {
        // 生成逐个替换的容器规格
        List<ContainerSpec> specs = new ArrayList<>();
        for (int i = 0; i < config.getReplicas(); i++) {
            specs.add(new ContainerSpec.Builder()
                .image(config.getImage())
                .name(config.getName() + "-v2-" + i)
                .build());
        }
        return specs;
    }
}

// 蓝绿部署策略
public class BlueGreenStrategy implements DeployStrategy {
    @Override
    public List<ContainerSpec> generateSpecs(ServiceConfig config) {
        // 生成完整的新版本环境
        List<ContainerSpec> specs = new ArrayList<>();
        for (int i = 0; i < config.getReplicas(); i++) {
            specs.add(new ContainerSpec.Builder()
                .image(config.getImage())
                .name(config.getName() + "-green-" + i)
                .build());
        }
        return specs;
    }
}

调用方想换策略?直接 new 不同的实现类传给外观就行。我当年重构那个项目时,把硬编码的部署逻辑拆成策略后,新增一种部署方式只需要加一个类,改一行配置。爽得很。

命令模式:操作可记录、可撤销

命令模式在这里解决的是「操作怎么管理」的问题。每个容器操作(启动、停止、重启)都是一个命令对象,可以记录日志,也可以撤销。

为什么要撤销?你想啊,部署到一半发现镜像有问题,你得能回滚。命令模式天然支持这个。

// 命令接口
public interface Command {
    void execute();
    void undo();
}

// 启动容器命令
public class StartContainerCommand implements Command {
    private ContainerSpec spec;
    private DockerClient client;

    public StartContainerCommand(ContainerSpec spec) {
        this.spec = spec;
        this.client = DockerClientBuilder.getInstance().build();
    }

    @Override
    public void execute() {
        // 调用Docker API启动容器
        CreateContainerResponse response = client.createContainerCmd(spec.getImage())
            .withName(spec.getName())
            .exec();
        client.startContainerCmd(response.getId()).exec();
        System.out.println("容器 " + spec.getName() + " 已启动");
    }

    @Override
    public void undo() {
        // 撤销就是停止并删除容器
        List<Container> containers = client.listContainersCmd()
            .withNameFilter(spec.getName())
            .exec();
        for (Container c : containers) {
            client.killContainerCmd(c.getId()).exec();
            client.removeContainerCmd(c.getId()).exec();
        }
        System.out.println("容器 " + spec.getName() + " 已回滚");
    }
}

// 命令调用器
public class CommandInvoker {
    private Stack<Command> history = new Stack<>();

    public void execute(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }

    public void undoLast() {
        if (!history.isEmpty()) {
            Command cmd = history.pop();
            cmd.undo();
        }
    }
}

我曾经踩过的坑:命令的 undo() 方法一定要考虑幂等性。比如容器已经被手动删除了,undo() 再调删除就会报错。我的做法是在 undo() 里先检查容器是否存在,不存在就直接跳过。

观察者模式:状态变化及时通知

最后一个模式是观察者。容器启动后,它的状态会不断变化——运行中、异常退出、健康检查失败……这些变化需要通知给监控系统、日志系统、告警系统。

观察者模式就是干这个的。容器状态变化时,通知所有注册的观察者。

// 观察者接口
public interface ContainerObserver {
    void onContainerEvent(ContainerEvent event);
}

// 日志观察者
public class LogObserver implements ContainerObserver {
    @Override
    public void onContainerEvent(ContainerEvent event) {
        System.out.println("[LOG] 容器 " + event.getContainerName() 
            + " 状态变为: " + event.getStatus());
    }
}

// 告警观察者
public class AlertObserver implements ContainerObserver {
    @Override
    public void onContainerEvent(ContainerEvent event) {
        if (event.getStatus() == ContainerStatus.CRASHED) {
            System.out.println("[ALERT] 容器 " + event.getContainerName() 
                + " 崩溃了!请立即处理!");
        }
    }
}

// 事件源 - 被观察者
public class ContainerEventSource {
    private List<ContainerObserver> observers = new ArrayList<>();

    public void addObserver(ContainerObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(ContainerObserver observer) {
        observers.remove(observer);
    }

    public void notifyObservers(ContainerEvent event) {
        for (ContainerObserver observer : observers) {
            observer.onContainerEvent(event);
        }
    }

    // 模拟容器状态变化
    public void simulateContainerCrash(String containerName) {
        ContainerEvent event = new ContainerEvent(containerName, ContainerStatus.CRASHED);
        notifyObservers(event);
    }
}

实际项目中,观察者模式通常跟事件驱动结合。Docker的Event API会推送容器事件,我们收到事件后通知观察者。这样监控系统就能实时感知容器状态。

组合使用:一个完整的部署流程

好了,四个模式都讲完了。我们看看它们怎么配合工作:

  1. 调用方调 OrchestratorFacade.deploy(config)
  2. 外观把config传给策略,策略生成容器规格列表
  3. 外观遍历规格列表,为每个规格创建StartContainerCommand
  4. 命令调用器执行命令,启动容器
  5. 外观注册观察者,监听容器状态
  6. 容器状态变化时,观察者自动通知日志、告警等组件

整个过程,调用方只做了一件事——调了一个方法。这就是外观模式的价值。而内部,策略、命令、观察者各司其职,互不干扰。

总结一下:这个组合的精髓在于「分离关注点」。外观管接口,策略管算法,命令管操作,观察者管通知。每个模式只关心自己那一亩三分地,组合起来却能解决复杂的编排问题。

说实话,设计模式学到这个份上,你会发现它们不再是孤立的知识点,而是可以像乐高积木一样拼装的工具。你想想看,如果不用这些模式,你得在一个类里塞多少逻辑?启动容器、判断策略、记录日志、处理异常……全搅在一起,改一个地方可能引发连锁反应。

用设计模式,说白了就是给自己留条后路。今天用滚动更新,明天想换蓝绿部署?改一行代码就行。今天只记录日志,明天想加个钉钉告警?加一个观察者类就行。这种灵活性,才是设计模式真正的价值所在。


公众号:蓝海资料掘金营,微信deep3321