• QQ空间
  • 回复
  • 收藏

基于容器应用设计的原则,模式和反模式

东方头条 2019-11-15 18:16:59 科技

容器和容器编排(Kubernetes)的广泛使用,让我们可以轻松的构建基于微服务的“云原生”(Cloud Native)的应用。容器成为了云时代的新的编程单元,类似面向对象概念下的对象,J2EE中的组件或者函数式编程中的函数。

在面向对象时代,有许多著名的设计原则,模式和反模式等,例如:SOLID (单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)Design Patterns: Elements of Reusable Object-Oriented SoftwareAnti-Pattern

在新的容器背景下,相应的原则和模式有助于帮助我们更好的构建“云原生”的应用。我们可以看到,这些原则和模式并非对之前模式的颠覆和推翻,更像是适应新环境的演进版本。

原则

单一职责原则 SINGLE CONCERN PRINCIPLE (SCP)

与OO的单一功能相对应,每一个容器应该提供单一的职责,只关注于做好一件事。单一职责使得容器更容易重用。通常容器对应于一个进程,而该进程专注于做好一件事。

高可观测性原则 HIGH OBSERVABILITY PRINCIPLE (HOP)

容器像对象一样,应该是一个封装良好的黑盒子。但是在云的环境下,这个黑盒子应该提供良好的观测接口,使得其在云的环境下得到相应的监控和管理。这样,整个应用才能提供一致的生命周期的管理。

可观测性包含:提供健康检查 Health Check,或者心跳提供状态把日志输出到标准输出(STDOUT)和标准出错(STDERR)等等

生命周期确认原则 LIFE-CYCLE CONFORMANCE PRINCIPLE (LCP)

生命周期确认原则指的是容器应该提供和平台交互来处理相应的生命周期的变化。

捕获并响应Terminate (SIGTERM)信号,来尽快优雅的终止服务进程,以避免kill (SIGKILL)信号强行终止进程。例如一下的NodeJS代码。process.on("SIGTERM", function () { console.log("Received SIGTERM. Exiting.") server.close(function () { process.exit(0); }); });返回退出码process.exit(0);

镜像不可变原则 IMAGE IMMUTABILITY PRINCIPLE (IIP)

在运行时,配置可以不同,但是镜像应该是不可变的。

我们可以理解为镜像是个类,是容器是对象实例,类是不变的,而容器是拥有不同配置参数的镜像实例。

进程用完既丢原则 PROCESS DISPOSABILITY PRINCIPLE (PDP)

在云环境下,我们应该假定所有的容器都是临时的,它随时有可能被其它的容器实例所替代。

这也就意味着需要把容器的状态保存在容器之外。并且尽可能快速的启动和终止容器。通常越小的容器就越容易实现这一点。

自包含原则 SELF-CONTAINMENT PRINCIPLE (S-CP)

容器在构建的时候应该包含所有的依赖,也就是所说容器在运行时不应该有任何的外部依赖。

限制运行资源原则 RUNTIME CONFINEMENT PRINCIPLE (RCP)

容器的最佳实践应该是在运行时指定容器对资源配置的需求。例如需要多少的内存,CPU等等。这样做可以使得容器编排能都更有效的调度和管理资源。

模式

许多容器应用的模式和Pod的概念相关,Pod是Kubernetes为了有效的管理容器而提出的概念,它是容器的集合,我们可以理解为“超容器”(我随便发明的)。Pod包含的容器之间就好像运行在同一台机器上,这些容器共享Localhost主机地址,可以本机通信,共享卷等等。

Kubernetes 类似云上OS,提供了用容器构建云原生应用的最佳实践。我们看看这些常见的模式都有什么。

边车(侧斗)(Sidecar)

Sidecar是最常见的模式,在同一个Pod中,我们需要把不同的责任分在不同的容器中,对外部提供一个完整的功能。

这样的例子有很多,例如:上图中的Node后端和提供缓存的RedisWeb服务器和收集日志的服务Web服务器和负责监控服务器性能数据的服务

这样做有点类似面向对象的组合模式,好处有很多:

应用单一职责原则,每一个容器只负责专注做好一件事。隔离,容器之间不会出现互相竞争资源,当一个次要功能(例如日志收集或者缓存)失效或者崩溃的时候,对主要功能的影响降至最小。可以对每一个容器进行独立的生命周期管理可以对每一个容器进行独立的弹性扩张可以方便的替换其中一个容器

代理(大使)容器

类似于面向对象的Proxy模式,利用Pod中一个容器提供对外的访问连接。如下图中Node后端总是通过Service Discovery容器来和外部进行通信。

这样做,负责Node模块开发的只需要假定所有的通信都是来自于本机,而把通信的复杂性交给代理容器,去处理诸如负载均衡,安全,过滤请求,必要时中断通信等功能。

适配器容器

大家常常会把面向对象的Proxy模式,Bridge模式和Adapter模式搞混,因为单单从UML关系图上来看,它们都大同小异。似乎只是取了不同的名字。事实也确实如此,就像几乎所有的OO模式都是组合模式的衍生,所有容器模式都是边车模式的衍生。

在下图的例子中,如果Logging Adapter的名字不提及Adapter,我们不会认为这是个适配器模式。

其实适配器模式关注的是如果把Pod内部的不同容器的功能通过适配器统一的暴漏出来。在上图中,如果我们再多加一个容器,它同时会向卷中写入日志的化,这样就更清楚了。Logging Adapter适配不同容器用不同的接口提供的日志,并提供统一的访问接口。

name: bad

spec:

template:

metadata:

name: bad

spec:

restartPolicy: Never

containers:

- name: box

image: busybox

command: ["/bin/sh", "-c", "exit 1"]

如果你尝试在你的cluster里面创建以上的Job,你可能会碰到如下的状态。

$ kubectl describe jobs

Name:bad

Namespace:default

Image(s):busybox

Selector:controller-uid=18a6678e-11d1-11e7-8169-525400c83acf

Parallelism:1

Completions:1

Start Time:Sat, 25 Mar 2017 20:05:41 -0700

Labels:controller-uid=18a6678e-11d1-11e7-8169-525400c83acf

job-name=bad

Pods Statuses:1 Running / 0 Succeeded / 24 Failed

No volumes.

Events:

FirstSeenLastSeenCountFromSubObjectPathTypeReasonMessage

------------------------------------------------------------

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-fws8g

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-321pk

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-2pxq1

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-kl2tj

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-wfw8q

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-lz0hq

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-0dck0

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-0lm8k

1m1m1{job-controller }NormalSuccessfulCreateCreated pod: bad-q6ctf

1m1s16{job-controller }NormalSuccessfulCreate(events with common reason combined)

因为任务快速失败。Kubernetes认为任务没能成功启动,尝试创建新的容器以恢复这个失败,导致的Cluster会在短时间创建大量的容器,这样的结果可能会消耗大量的计算资源。

在Spec中使用.spec.activeDeadlineSeconds来避免这个问题。这个参数定了等待多长时间重试失败的Job。