结合Kubernetes解读微服务的12要素

10天前

Kubernetes的容器编排模型是如何对各组12要素原则进行直接的支持的?

本文由公众号EAWorld翻译发表,转载需注明出处。

作者:Michael D. Elder 
译者:白小白 
原题:Kubernetes & 12-factor apps 
原文:http://t.cn/EoBns1o

关于12原则的更多内容,可以进一步阅读《全网首发:逐一解读云原生应用开发“12-Factors”》

如果你在使用容器来构建应用的话,一定听过什么是“12要素原则”。“12要素”为开发微服务提供了一组明确的指引。人们相信只要遵循这些原则,就可以更容易的运行、扩展和部署应用与服务。

在讲述“12要素原则”的时候,笔者习惯于将这些原则按普遍的场景进行分组。对我来说,12要素最终是关于如何编码、部署和运营的原则。这些是软件交付生命周期里最常见的场景,为多数开发者和DevOps整合团队所熟知。

那么在使用Kubernetes的过程中,如何在构建微服务的时候应用12要素原则呢?事实上,12要素原则对Kubernetes的发展和演进过程产生了深远的影响。接下来的内容,我将逐一分析,Kubernetes的容器编排模型是如何对各组12要素原则进行直接的支持的。

一、与编码有关的要素

关于源码管理,需要考虑的因素是基准代码、构建和部署生命周期以及最终如何维持生产环境和开发环境的一致性。


一个基本的软件交付周期图

Source control all the things 源码控制一切

  • 要素一:一份基准代码,多份部署
Kubernetes中大量的使用声明式结构。应用的全部信息都经由YAML或者JSON实现基于文本的表达。容器本身则被描述为Dockerfile这样的源码格式,在构建的过程中,可以反复的将文本格式的Dockerfile转为容器镜像。因为从镜像到容器部署环节都被封装为文本形态,可以很容易的实现对所有事物的源码控制,而最常用的工具是Git。

在Kubernetes中,很多东西可以用声明的方式来进行描述。《Kubernetes in the Enterprise》这本书中记录了很多完整的示例,相关的代码都在Github上。

如果应用需要跨越开发、用户验收、生产等不同的环境来运行的话,最常用的方法是使用GitOps交付模型来实现多分支的管理,来区分环境之间的差异。

  • 要素五:严格分离构建、发布和运行
如标题所述,为了遵循这一原则,构建、发布和运行环境要实现严格的分离。最典型的实现方式是工件管理:一旦代码提交,构建随之开始,而后生成容器镜像并发布到镜像库。如果使用了Helm(Kubernetes的包管理器,类似Ubuntu的apt-get),Kubernetes应用会同时被打包和发布到Helm库。通过对二进制文件或者镜像文件的重新构建,所有这些发布物可以在不同的环境中进行复用和部署,并且可以保证不会这个过程中引入任何未知的变更。

  • 要素十:开发环境与生产环境等价
遵循这一等价原则,可以避免下面这种恼人的对话,“在我那好好的,怎么在你那就不行了呢”。容器(或者莫不如说是Kubernetes)让应用的交付和运行依赖实现了标准化,这意味着可以将任何事物以相同的方式部署到任何地方。因此,如果在生产环境使用了高可用的MySQL配置,就可以在开发集群中部署相同的架构。通过在前期的开发环境中建立生产环境的等价架构,通常可以避免一些不可预见的差异,而这些差异可能对于应用的正常运行(甚至是失败)产生至关重要的影响。

二、与部署有关的要素

构建的价值仅在成功部署时才能得以体现。在12要素中,有很大比例的原则描述了相关的最佳实践,包括微服务该如何部署,如何处理依赖,以及如何解析其他微服务的细节。

微服务的可靠性取决于其最不可靠的依赖。

如何理解这句话呢,答案就在下面的这张Kubernetes架构图中:


Pod以及相关的Kubernetes对象

  • 要素二:显式声明依赖关系
对于上面那句话的理解,首先需要考虑依赖关系的构建,12要素中关于依赖关系的阐述参照了构建管理的原则。然而我仍旧倾向于将依赖要素放在与部署有关的分组中,因为对于其他API或者数据存储的依赖将对微服务的可靠性产生广泛的影响。Kubernetes引入了readiness和liveness的探针机制,可以执行运行期的依赖检查。readiness探针可以验证在某个时间点或者时间段,是否存在健康的后端service用以响应请求。而liveness探针可以确认service本身的健康性。在给定的时间窗口内,如果两个指标之一触发了失败的临界值,Pod将被重启。

译注:
此处作者对于liveness和readiness的理解是有误解的。liveness指标实际上标志了容器可以正常工作的底线,只有在超过这一底线时,容器才会被重启。而readiness指标则标志了容器可以正常工作的上线,不满足readiness的要求,容器并不会被重启,而仅会标志为“非正常”状态。举例来说,Tomcat的应用启动成功后就是liveness,但只有在spring容器初始化、数据库连接等相关过程完成后,才是readiness。
更多内容参见http://t.cn/EorvPAh

建议花时间阅读《Release It!》这本书,领略书中的智慧,以及使用书中描述的架构模式(熔断器,Fail Fast,Timeouts等)来改进应用程序的可靠性。

在环境中存储配置

按照这一要素的要求,开发者需要将配置源码存储在进程的环境变量表中,如ENV VARs。通过配置与代码的分离,微服务将彻底的独立于环境,可以不进行任何源码级的变更就移植到另一环境。Kubernetes提供了ConfigMaps和Secrets对象用于配置源码的管理(事实上Secrets在未经额外的加密的情况下不应纳入源码管理)。容器可以在运行时获取配置的细节。将配置信息存储为环境变量有利于系统的扩展以及处理日渐增长的服务需求。

  • 要素六:以无状态的进程运行应用
在Kubernetes中,容器镜像作为Pod中的进程运行。来自12要素的观察发现,Linux内核已经通过围绕进程模型的资源共享实现了大量的优化。Kubernetes或者说容器只是提供了一个界面来实现更好的隔离,让处于同一主机的容器进程可以并行不悖。进程模型的应用使得系统扩展和故障恢复的管理变得更加容易。一般来说,进程应该是无状态的,这样才能以副本的方式实现工作负载的横向扩展。但在Kubernetes中,也有诸如数据库/缓存这类有状态的工作负载。

应该使用持久的数据存储来按需保存应用的状态,应用进程的所有实例都可以通过配置文件来发现这些存储。在基于Kubernetes的应用中,Pod的多个副本同时运行,请求可能被路由到任何一个副本,因此,微服务不可能期待粘滞会话(即让用户在一次会话周期内的所有请求始终转发到同一个特定的对象)。

得益于进程模型的机制,所有的service都可以很容易的通过创建更多的进程实例来实现扩展,Kubernetes提供了很多控制器来完成这项工作,如ReplicaSets, Deployments, StatefulSet, ReplicationController等等。

参见如下代码片段的第2行至第7行,关于副本的部分。

# Application to deploy
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: watson-conversation-app
spec:
  replicas: 2 # tells deployment to run 2 pods matching the template
  template: # create pods using pod definition in this template
    metadata:
      labels:
        app: watson-conversation-app
        tier: frontend
    spec:
      containers:
      - name: watson-conversation-app
        image: mycluster.icp:8500/default/conversation-simple:latest
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
          - name: WORKSPACE_ID
            valueFrom:
              configMapKeyRef:
                name: car-dashboard-config
                key: workspace_id
          - name: CONVERSATION_SERVICE_CAR
            valueFrom:
              secretKeyRef:
                name: binding-conversation-service-car
                key: binding
watson-conversation.yaml hosted with ❤ by GitHub

Kubernetes的部署文件列示了所需的副本数量的声明(如第7行所示)

  • 要素四:把后端服务当作附加资源
我们通常把网络环境这类依赖定义为“后端服务”。正确的做法是把这些 上游服务的生命周期独立于微服务本身的生命周期来考虑。无论是后端服务的附加或者剥离,都不应该影响微服务本身正常响应的能力。

举例来说,如果应用需要与数据库进行交互,就需要设定一些连接细节,来隔离应用与数据库的交互行为,可以使用动态的服务发现,或者是使用Kubernetes Secret的Config配置来实现。接下来,需要考虑的是网络请求是否实现了容错机制,以保证即使后端服务发生了运行时失败,也不会触发微服务的级联失败(《Release It!》书中有更详尽的阐述)。相关的后端服务应该运行在独立的容器中,或者集群以外的什么地方。微服务不应该关注交互的细节,所有与数据库的交互行为都通过API来完成。

  • 要素七:通过端口绑定提供服务
在生产环境中,多个微服务提供了不同的功能,服务间的通信需要经由良好定义的协议来达成。可以使用Kubernetes Service对象来声明和解析集群内外相关服务的网络端点。

在容器出现以前,任何时候,如果需要部署一个新的服务或者更新现有服务的版本的话,就需要花大量的时间解决主机上的端口冲突问题。容器的隔离机制以及Linux内核的网络命名空间机制,使得在单一主机的相同端口上运行多个进程或者同一个微服务的多个版本成为可能。从而,Kubernetes的Service对象就可以向所有主机暴露微服务池,并且对入站请求实现基本的负载均衡。

在Kubernetes中,Service对象是声明式的,并且会自动完成路由到Pod的相关请求的负载均衡工作。

处理对Pod的相关请求的负载均衡的Service声明示例:

# Service to expose frontend
apiVersion: v1
kind: Service
metadata:
  name: watson-conversation-app
  labels:
    app: watson-conversation-app
    tier: frontend
spec:
  type: NodePort
  ports:
  - port: 3000
  selector:
    app: watson-conversation-app
    tier: frontend
watson-conversation-service.yaml hosted with ❤ by GitHub

三、与运营有关的要素

要素八(并发),要素九(可处置性),要素十一(日志)和要素十二(任务管理)与如何简化微服务的运营相关。

Kubernetes聚焦于多个Pod的简单部署单元如何按需创建和销毁,单独的Pod本身毫无价值。

  • 要素八:通过进程模型进行扩展
要素六所提到的进程模型在并发机制的实现上大放异彩。前文说过,Kubernetes可以通过不同种类的生命周期控制器来实现无状态应用的运行时扩展。所需要的副本数量以声明式模型定义并可以在运行时变更。同样,Kubernetes也定义了很多用于管理并发的生命周期控制器,如ReplicationControllers, ReplicaSets, Deployments, StatefulSets, Jobs和DaemonSets。

下面的动画展示了副本添加的过程:



ReplicaSet可以用来添加更多的Pod。新创建的Pod会自动响应来自Service对象的入站请求。

基于对CPU、内存等相关计算资源以及其他外部指标的阈值监测,Kubernetes引入了自动扩展的机制。Horizontal Pod Autoscaler (HPA) 可以实现在一个Deployment或者ReplicaSet中自动的扩展Pod的数量。


HPA基于指标的观测来添加Pod

谈及自动扩展,关于Pod纵向扩展以及集群扩展的话题值得关注。纵向的Pod扩展适用于有状态的应用。Kubernetes把CPU和内存作为触发器,来向Pod中添加更多的计算资源。也可以使用自定义的指标代替HPA来触发自动扩展。


Vertical Pod Autoscaler扩展了容器的可用内存

  • 要素九:快速启动和优雅终止
12要素原则中的可处置性,考虑的是如何使用Linux内核的信号机制来与运行的进程交互。遵循可处置性的原则,微服务可以快速启动并且可以随时消亡而不影响用户体验。对于无状态的微服务来说,实现与部署相关的原则有助于达成可处置性。事实上,藉由前文提到的livenessProbes和readinessProbes探针机制,Kubernetes会销毁在给定的时间窗口内处于不健康的Pod。

  • 要素十一:把日志当作事件流
对于容器来说,所有的日志通常会记入到stdout或者stderr文件描述符。此处很重要的设计原则是,容器不应该管理用于日志输出的内部文件,而应交由容器编排系统来收集日志并进行分析和归档。在Kubernetes中,日志收集一般作为公共服务存在。以我本人的工作经历来说,可以使用Elasticsearch-Logstash-Kibana组合来平滑的实现这一点。

  • 要素十二:把后台管理任务当作一次性进程运行
像是数据库迁移/备份/恢复/日常维护这类管理任务,应该与微服务的基本运行时逻辑隔离并解耦。在Kubernetes中,Job控制器可以创建一次性运行的Pod,或者是按照日程规划执行不同活动的Pod。Job控制器可以用来实现业务逻辑,因为Kubernetes会将API令牌加载到Pod中,所以也可以使用Job控制器来与Kubernetes编排系统进行交互。

通过对此类管理任务的隔离,可以在未来简化微服务的行为,进而减少可能出现的潜在失败。

四、结语

如果读者对这个话题很感兴趣,建议点赞转发与朋友分享本文的观点。如果你有幸参加KubeCon Europe,欢迎来与我和Brad Topol面基,并且就此话题深入探讨,我们会分享Kubernetes相关能力的更多演示。

除了架构微服务的12原则以外,我和另一位作者,Shikha Srivastava,也识别了生产环境中遗漏的7条原则,Shikha很快会就此成文。敬请期待。


关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享,长按二维码关注

COMMENTS

需要 后方可回复
如果没有账号可以 一个帐号。