Kubernetes 设计模式笔记 —— Singleton Service

Singleton Service 模式会保证在某个特定的时间点,有且只有一个应用实例是活跃的。这个模式可以在应用内部实现,也可以完全交给 Kubernetes 去处理。

Kubernetes 最大的优势之一就是能够轻松、透明地扩展应用,只需要一条命令式的语句 kubectl scale 就能伸缩 Pod,或者通过声明式的方式修改 controller(比如 ReplicaSet)定义来实现,甚至基于应用负载动态地完成应用扩展。
在 Kubernetes 中,多实例指的就是 Pod 的多个副本,能够提升应用的吞吐量和可用性,Service 则负责分配请求。

但是在一些特殊的场景中,同一时间只允许唯一一个应用实例运行。比如某个周期性执行的定时任务,若存在该任务的多个实例同时运行,则其中每一个实例都会在规定的时间间隔后触发一次该任务,导致预期之外的重复执行。
另一个例子比如,某个应用服务需要轮询特定的系统资源(文件系统或数据库等),我们想要确保只有一个应用实例甚至只有一个线程在执行这类操作。
还比如,我们必须按照特定的顺序消费来自消息代理的消息时,这里的单线程消费者也是一种单例。

运行同一个 Pod 实例的多个副本并令它们同时处于活跃状态,属于 active-active 拓扑结构,单例模式需要的是 active-possive(或者叫做 master-slave)结构。即只有一个实例是活跃的,其他所有的实例都是被动的,等待需要的时候被唤醒。

Out-of-Application Locking

顾名思义,通过外部的管理程序确保应用只有唯一一个实例在运行,应用本身并不知晓自己是否是单例。Out-of-application locking mechanism

Kubernetes 实现单例模式的方式是,启动一个副本唯一的 Pod,但这并不能确保 Pod 是高可用的。因此还必须通过控制器(比如 ReplicaSet)来管理单例,令其具备高可用的能力。
这种结构严格来说并不是 active-passive (没有配置 passive 实例)的,但是具有同样的效果。Kubernetes 会确保任何时候都会有一个 Pod 实例在运行,控制器会持续进行健康检查,修复失败的 Pod。

最需要额外注意的是副本数量,避免意外地被修改成大于 1 的值。事实上任何时候都只有唯一的应用实例在运行的说法不是完全准确的。Kubernetes 中的组件比如 ReplicaSet,会优先考虑可用性而非一致性。这意味着对于副本数量来说,ReplicaSet 会实行至少一个而不是最多一个的策略。在某些特殊的情况下,即便有 replicas: 1 的配置,也会出现多个实例同时在运行的情况。
最常见的情形比如当某个节点失效时,与整个 Kubernetes 集群的连接丢失,ReplicaSet 控制器就会在另一个健康的节点上创建一个新的 Pod 实例,并且不会提前确认断连的节点上的原 Pod 是否已经关闭。类似的情况也会在修改副本数量或者将 Pod 重新分配给另一个节点时出现。

单例模式可以具有弹性和恢复能力,但是从定义来看,并不具备高可用性。它通常更倾向于一致性而非可用性。同样更倾向于一致性的 Kubernetes 资源是 StatefulSet。如果需要严格意义上的单例模式,StatefulSet 是更好的选择,但是它同样会增加系统的复杂度。

In-Application Locking

在分布式环境中,控制服务实例数量的一种方式就是分布式锁。当实例中的服务组件被激活时,它会尝试获取一个锁,成功获取到锁则服务处于活跃状态。此时任何其他未获取到锁的服务实例则等待并不断地尝试获取锁,直到占用的锁被释放。
In-application locking mechanism

在面向对象的概念中,单例就是一个保存在类的静态变量中的对象实例。类本身知晓该实例是单例,并且在定义中不允许为同一个进程实例化多个实例。
在分布式系统中,就意味着容器化应用本身在设计上,就不允许同一时间下有多于一个的实例处于活跃状态,不管实际上启动了多少个 Pod。上述实现需要借助分布式锁,比如 ZooKeeper,Consul,Redis 或 Etcd 等。

结论

如果使用场景需要强 singleton 保证,就不能借助 ReplicaSet 实现的应用外部的锁机制。ReplicaSet 的设计目标在于保证 Pod 的可用性而不是满足 at-most-one 语义。会有很多错误场景,同一个 Pod 的两个副本短时间内并发地运行。
若上述情况是不可接受的,则可以使用 StatefulSet 或者引入应用内的锁机制。
在另外一些场景中,只有容器化应用的一部分是需要作为 singleton 运行的。比如一个容器化应用提供 HTTP 服务(能够安全地扩展),同时还包含必须是 singleton 的轮询组件。这时候使用应用外部的锁机制,会阻止整个应用被扩展。我们因此必须将 singleton 组件从原本的部署中分离出来。
或者借助应用内的锁机制,只对 singleton 组件加锁。此时就可以透明地扩展整个应用,多个 HTTP 服务副本提供访问,singleton 组件则以 active-passive 模式运行。

参考资料

Kubernetes Patterns