设计模式综合实战十:基于Docker实现容器编排(外观+策略+命令+观察者)
说实话,这个题目一看就有点「大杂烩」的感觉。四个设计模式凑在一起,还要跟Docker容器编排扯上关系。但我得说,这正是真实项目的常态——你很少会只用一种模式解决问题。
我记得几年前帮一个团队重构微服务部署系统,他们用Shell脚本硬编码了所有容器启动逻辑。每次新增服务类型,就得改脚本,改完还容易出Bug。我当时就想,这地方要是能用设计模式梳理一下,会清爽很多。
今天我们就来聊聊,怎么用外观、策略、命令、观察者这四个模式,搭一个灵活的容器编排框架。
整体架构:四个模式各司其职
先看看它们分别负责什么:
- 外观模式:给上层提供一个简单的接口,隐藏Docker API的复杂性
- 策略模式:不同的部署策略(滚动更新、蓝绿部署、金丝雀发布)可以互相替换
- 命令模式:把每个操作(启动、停止、重启)封装成命令,支持撤销和日志
- 观察者模式:容器状态变化时,通知各个监控组件
说白了,外观模式是门面,策略模式是脑子,命令模式是手脚,观察者模式是耳目。四个配合起来,就是一个完整的编排系统。
核心思路:不要把所有逻辑塞到一个类里。每个模式只负责一件事,组合起来反而更强大。
SVG架构图:模式协作关系
外观模式:统一入口
先看外观模式。它的作用很简单——把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会推送容器事件,我们收到事件后通知观察者。这样监控系统就能实时感知容器状态。
组合使用:一个完整的部署流程
好了,四个模式都讲完了。我们看看它们怎么配合工作:
- 调用方调
OrchestratorFacade.deploy(config) - 外观把config传给策略,策略生成容器规格列表
- 外观遍历规格列表,为每个规格创建StartContainerCommand
- 命令调用器执行命令,启动容器
- 外观注册观察者,监听容器状态
- 容器状态变化时,观察者自动通知日志、告警等组件
整个过程,调用方只做了一件事——调了一个方法。这就是外观模式的价值。而内部,策略、命令、观察者各司其职,互不干扰。
总结一下:这个组合的精髓在于「分离关注点」。外观管接口,策略管算法,命令管操作,观察者管通知。每个模式只关心自己那一亩三分地,组合起来却能解决复杂的编排问题。
说实话,设计模式学到这个份上,你会发现它们不再是孤立的知识点,而是可以像乐高积木一样拼装的工具。你想想看,如果不用这些模式,你得在一个类里塞多少逻辑?启动容器、判断策略、记录日志、处理异常……全搅在一起,改一个地方可能引发连锁反应。
用设计模式,说白了就是给自己留条后路。今天用滚动更新,明天想换蓝绿部署?改一行代码就行。今天只记录日志,明天想加个钉钉告警?加一个观察者类就行。这种灵活性,才是设计模式真正的价值所在。
公众号:蓝海资料掘金营,微信deep3321