作者:京东物流 杨建民

一、微服务架构起源

单体架构:可以理解为主要业务逻辑模块(我们编写的代码模块,不包括独立的中间件)运行在一个进程中的应用,最典型的是运行在一个 Tomcat 容器中,位于一个进程里。单体架构好处是技术门槛低、编程工作量少、开发简单快捷、调试方便、环境容易搭建、容易发布部署及升级,开发运维等总体成本很低、见效快。其缺点也明显:

(1)单体应用系统比较膨胀与臃肿,耦合度高,导致进行可持续开发和运维很困难。

(2)单体应用难以承载迅速增长的用户请求和需求。

基于 Spring Framework 的单体应用架构图

分布式架构核心思想是把一个单一进程的系统拆分为功能上相互协作又能独立部署在多个服务器上的一组进程,这样一来,系统可以根据实际业务需要,通过以下两种方式实现某些独立组件的扩容,提升吞吐量。

分布式架构是将一个庞大的单体应用拆分成多个独立运行的进程,这些进程能通过某种方式实现远程调用,因此,分布式架构要解决的第一个核心技术问题就是独立进程之间的远程通信。该问题的最早答案就是 RPC 技术(Remote Procedure Call),一种典型的微服务架构平台的结构示意图如下:

大家比较熟知的微服务架构框架有 Dubbo 与 Spring Cloud,之后比较成功的微服务架构基本都和容器技术挂钩了,其中最成功的、影响最大的当属 Kubernetes 平台了,与之相似的还有 Docker 公司推出的 Docker Swarm(在 2017 年底,Docker Swarm 也支持 Kubernetes 了)。

关于微服务架构的优势由于文章篇幅有限,不再展开,但任何技术都存在两面性,微服务架构具有一定的复杂性,如开发者必须掌握某种 RPC 技术,并且必须通过写代码来处理 RPC 速度过慢或者调用失败等复杂问题。为了解决微服务带来的编程复杂性问题,一种新的架构设计思想出现了,这就是 Service Mesh,Service Mesh 定义是:一个用于处理服务于服务之间通信(调用)的复杂的基础架构设施。从本质上看,Service Mesh 是一组网络代理程序组成的服务网络,这些代理会与用户程序部署在一起,充当服务代理,这种代理后来在 Google 的 Istio 产品架构中称为 “Sidecar”,其实就是采用了代理模式的思想去解决代码入侵及重复编码的问题,。下图给出了 Service Mesh 最简单的架构图。Servie Mesh 同样不是本次的主角,感兴趣的小伙伴可自行学习。

二、初识 k8s

官方原文是:K8s is an abbreviation derived by replacing the 8 letters “ubernete” with 8.

k8s 全称 kubernetes,名字来源于希腊语,意思为 “舵手” 或 “领航员”,它是第一代容器技术的微服务架构(第二代是 Servie Mesh)。

Kubernetes 最初源于谷歌内部的 Borg,提供了面向应用的容器集群部署和管理系统。Kubernetes 的目标旨在消除编排物理/虚拟计算,网络和存储基础设施的负担,并使应用程序运营商和开发人员完全将重点放在以容器为中心的原语上进行自助运营。Kubernetes 也提供稳定、兼容的基础(平台),用于构建定制化的 workflows 和更高级的自动化任务。

Kubernetes 具备完善的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建负载均衡器、故障发现和自我修复能力、服务滚动升级和在线扩容、可扩展的资源自动调度机制、多粒度的资源配额管理能力。

Kubernetes 还提供完善的管理工具,涵盖开发、部署测试、运维监控等各个环节。

2.1 k8s 架构与组件

Kubernetes 主要由以下几个核心组件组成:

  1. etcd 保存了整个集群的状态; 2.apiserver 提供了资源操作的唯一入口,并提供认证、授权、访问控制、API 注册和发现等机制;
  2. controller manager 负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
  3. scheduler 负责资源的调度,按照预定的调度策略将 Pod 调度到相应的机器上;
  4. kubelet 负责维护容器的生命周期,同时也负责 Volume(CVI)和网络(CNI)的管理;
  5. Container runtime 负责镜像管理以及 Pod 和容器的真正运行(CRI);
  6. kube-proxy 负责为 Service 提供 cluster 内部的服务发现和负载均衡;

2.2 k8s 设计理念

API 设计原则

API 对象是 k8s 集群中的管理操作单元。k8s 集群系统每支持一项新功能,引入一项新技术,一定会新引入对应的 API 对象,支持对该功能的管理操作。例如副本集 Replica Set 对应的 API 对象是 RS。

k8s 采用声明式操作,由用户定义 yaml,k8s 的 API 负责创建。每个对象都有 3 大类属性:元数据 metadata、规范 spec 和状态 status。元数据是用来标识 API 对象的,每个对象都至少有 3 个元数据:namespace,name 和 uid;除此以外还有各种各样的标签 labels 用来标识和匹配不同的对象,例如用户可以用标签 env 来标识区分不同的服务部署环境,分别用 env=dev、env=testing、env=production 来标识开发、测试、生产的不同服务。规范描述了用户期望 k8s 集群中的分布式系统达到的理想状态(Desired State),例如用户可以通过复制控制器 Replication Controller 设置期望的 Pod 副本数为 3;status 描述了系统实际当前达到的状态(Status),例如系统当前实际的 Pod 副本数为 2;那么复制控制器当前的程序逻辑就是自动启动新的 Pod,争取达到副本数为 3。

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

使用上述.yaml 文件创建 Deployment,是通过在 kubectl 中使用 kubectl create 命令来实现。将该.yaml 文件作为参数传递。如下例子:

$ kubectl create -f docs/user-guide/nginx-deployment.yaml --record

k8s 常见对象:Pod、复制控制器(Replication Controller,RC)、副本集(Replica Set,RS)、部署(Deployment)、服务(Service)、任务(Job)、存储卷(Volume)、持久存储卷和持久存储卷声明(Persistent Volume,PV、Persistent Volume Claim,PVC)、节点(Node)、ConfigMap、Endpoint 等。

控制机制设计原则

三、资源管理

容器云平台如何对租户可用资源进行精细管理,对平台的可用性、可维护性和易用性起着至关重要的作用,是容器云平台能够为用户提供丰富的微服务管理的基石。在云计算领域,资源可被分为计算资源、网络资源和存储资源三大类,也可被分别称作计算云、网络云和存储云。

3.1、计算资源管理

Namespace

在 k8s 集群中,提供计算资源的实体叫做 Node,Node 既可以是物理机服务器,也可以是虚拟机服务器,每个 Node 提供了 CPU、内存、磁盘、网络等资源。每个 Node(节点)具有运行 pod 的一些必要服务,并由 Master 组件进行管理,Node 节点上的服务包括 Docker、kubelet 和 kube-proxy。

通过引入 Namespace,k8s 将集群近一步划分为多个虚拟分组进行管理,Namespace 所实现 “分区” 是逻辑上的,并不与实际资源绑定,它用于多租户场景实现资源分区和资源最大化利用。

大多数 Kubernetes 资源(例如 pod、services、replication controllers 或其他)都在某些 Namespace 中,但 Namespace 资源本身并不在 Namespace 中。而低级别资源(如 Node 和 persistentVolumes)不在任何 Namespace 中。Events 是一个例外:它们可能有也可能没有 Namespace,具体取决于 Events 的对象。

Pod

Pod 是 Kubernetes 创建或部署的最小/最简单的基本单位,一个 Pod 代表集群上正在运行的一个进程。

一个 Pod 封装一个应用容器(也可以有多个容器),存储资源、一个独立的网络 IP 以及管理控制容器运行方式的策略选项。Pod 代表部署的一个单位:Kubernetes 中单个应用的实例,它可能由单个容器或多个容器共享组成的资源。

每个 Pod 都是运行应用的单个实例,如果需要水平扩展应用(例如,运行多个实例),则应该使用多个 Pods,每个实例一个 Pod。在 Kubernetes 中,这样通常称为 Replication。Replication 的 Pod 通常由 Controller 创建和管理。Controller 可以创建和管理多个 Pod,提供副本管理、滚动升级和集群级别的自愈能力。例如,如果一个 Node 故障,Controller 就能自动将该节点上的 Pod 调度到其他健康的 Node 上。

Container

docker 本身比较重,2015 年 OCI(Open Container Initiative)诞生,它定义了镜像标准、运行时标准和分发标准,由于 k8s 本身不具备创建容器的能力,是通过 kubelet 组件调用容器运行时 API 接口和命令来创建容器,Kubernete 与 容器运行时的关系是历史性的,也很复杂。但是随着 Kubernete 弃用 Docker ,目前主流的运行时主要是 containerd 和 CRI-O

“one-container-per-Pod” 模式是 Kubernetes 最常见的用法,一个 Pod 也可以有多个容器。

三个级别的计算资源管理

在 k8s 中,可以从 Namespace、Pod 和 Container 三个级别区管理资源的配置和限制。例如:

apiVersion: v1
kind: Pod
metadata:
  name: memory-demo-3
spec:
  containers:
  - name: memory-demo-3-ctr
    image: vish/stress
    resources:
      limits:
        memory: "1000Gi"
      requests:
        memory: "1000Gi"
    args:
    - -mem-total
    - 150Mi
    - -mem-alloc-size
    - 10Mi
    - -mem-alloc-sleep
    - 1s
apiVersion: v1
kind: LimitRange
metadata:
  name: mylimits
spec:
  limits:
  - max:
      cpu: "4"
      memory: 2Gi
    min:
      cpu: 200m
      memory: 6Mi
    maxLimitRequestRatio:
      cpu: 3
      memory: 2
    type: Pod
  - default:
      cpu: 300m
      memory: 200Mi
    defaultRequest:
      cpu: 200m
      memory: 100Mi
    max:
      cpu: "2"
      memory: 1Gi
    min:
      cpu: 100m
      memory: 3Mi
    maxLimitRequestRatio:
      cpu: 5
      memory: 4
    type: Container
apiVersion: v1
kind: ResourceQuota
metadata:
  name: pod-demo
spec:
  hard:
  request.cpu: "4"
  request.memory: 8GB
  limit.memory:16GB
  pods: "2"

3.2 网络资源管理

k8s 的 ip 模型

node Ip :node 节点的 ip,为物理 ip.

pod Ip:pod 的 ip,即 docker 容器的 ip,为虚拟 ip。

cluster Ip:service 的 ip,为虚拟 ip。提供一个集群内部的虚拟 IP 以供 Pod 访问。实现原理是通过 Linux 防火墙规则,属于 NAT 技术。当访问 ClusterIP 时,请求将被转发到后端的实例上,如果后端实例有多个,就顺便实现了负载均衡,默认是轮训方式。

跨主机容器网络方案

在 k8s 体系中,k8s 的网络模型设计的一个基本原则:每个 pos 都拥有一个独立的 IP 地址,而且假定所有的 Pod 都在一个可以直接联通的、扁平的网络空间中,不管它们是否运行在同一个 Node(宿主机)中,都可以直接通过对方的 IP 进行访问。但 k8s 本身并不提供跨主机的容器网络解决方案。公有云环境(例如 AWS、Azure、GCE)通常都提供了容器网络方案,但是在私有云环境下,仍然需要容器云平台位不同的租户提供各种容器网络方案。

目前,为容器设置 Overlay 网络是最主流的跨主机容器网络方案。Overlay 网络是指在不改变原有网络配置的前提下,通过某种额外的网络协议,将原 IP 报文封装起来形成一个逻辑上的网络。在 k8s 平台上,建议通过 CNI 插件的方式部署容器网络。

CNI(Container Network Interface)是 CNCF 基金会下的一个项目,由一组用于配置容器的网络接口的规范和库组成,它定义的是容器运行环境与网络插件之间的接口规范,仅关心容器创建时的网络配置和容器被销毁是网络资源的释放,并且一个容器可以绑定多个 CNI 网络插件加入网络中,如下图。

目前比较流程的 CNI 插件实现方式有 Flannel、Calico、macvlan、Open vSwitch、直接路由。

Ingress

在 k8s 集群内,应用默认以 Service 的形式提供服务,有 kube-proxy 实现 Service 到容器的负载均衡器的功能,如下图定义一个 mysql service:

kind: Service
apiVersion: v1
metadata:
  name: mysql-master
spec:
  selector:
    app: mysql-master
  ports:
      port: 3306
      targetPort: 3306

此时,集群外是无法访问这个 Service 的。对于需要 k8s 集群外的客户端提供服务的 Service,可以通过 Ingress 将服务暴露出去,并且如果该集群(网络)拥有真实域名,则还能将 Service 直接与域名进行对接。

k8s 将一个 Ingress 资源对象的定义和一个具体的 Ingress Controller 相结合来实现 7 层负载均衡器。Ingress Controller 在转发客户请求到后端服务时,将跳过 kube-proxy 提供的 4 层负载均衡器的功能,直接转发到 Service 的后端 Pod(Endpoints),以提高网络转发效率。

如上图展示了一个典型的 HTTP 层路由的 Ingress 例子,其中:

如下是一个典型的 Ingress 策略定义,Ingress Controller 将对目标地址http://mywebsite.com/demowebapp(webapp:8080/demo)的访问请求转发到集群内部服务的

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
   name: mywebsite-ingress
spec:
   rules:
   -host:mywebsite.com
    http:
       paths:
      - path: /demo
          backend:
           serviceName: webapp
           servicePort: 8080

常用的 Ingress Controller 有:Nginx、HAProxy、Traefik、apisix 等。

3.3 存储资源

k8s 支持的 Volume 类型

临时目录(emptyDir)

使用 emptyDir,当 Pod 分配到 Node 上时,将会创建 emptyDir,并且只要 Node 上的 Pod 一直运行,Volume 就会一直存。当 Pod(不管任何原因)从 Node 上被删除时,emptyDir 也同时会删除,存储的数据也将永久删除。注:删除容器不影响 emptyDir。

配置类

本地存储类

共享存储类

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
  annotations:
        "volume.alpha.kubernetes.io/node-affinity": '{
            "requiredDuringSchedulingIgnoredDuringExecution": {
                "nodeSelectorTerms": [
                    { "matchExpressions": [
                        { "key": "kubernetes.io/hostname",
                          "operator": "In",
                          "values": ["example-node"]
                        }
                    ]}
                 ]}
              }'
spec:
    capacity:
      storage: 100Gi
    accessModes:
    - ReadWriteOnce
    persistentVolumeReclaimPolicy: Delete
    storageClassName: local-storage
    local:
      path: /mnt/disks/ssd1

PV 与 PVC

PV 和 PVC 相互关系生命周期如上图所示,k8s 的共享存储供应模式包括静态模式(Static)和动态模式(Dynamic),资源供应的结果就是创建好的 PV。运维人员手动创建 PV 就是静态,而动态模式的关键就是 StorageClass,它的作用就是创建 PV 模板。

创建 StorageClass 里面需要定义 PV 属性比如存储类型、大小等;另外创建这种 PV 需要用到存储插件。最终效果是,用户提交 PVC,里面指定存储类型,如果符合我们定义的 StorageClass,则会为其自动创建 PV 并进行绑定。

下图通过 ymal 创建一个 StorageClass 对象

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs    // 存储分配器
parameters:
  type: gp2
reclaimPolicy: Retain   // 回收策略
mountOptions:
  - debug

StorageClass 和 PV、PVC 之间的运作关系如下图所示:

CSI

CSI(Container Storage Interface)与 k8s 的关系与 CNI 有点类似,CSI 旨在容器和共享存储之间建议一套标准的存储访问接口。在它诞生前,经历了 “in-tree” 方式、FlexVolume 模式。

CSI 规范用语将存储供应商代码与 k8s 代码完全解耦,存储插件的代码由存储供应商自行维护。

和 kube-apiserver 直接进行交互的是 K8S 官方直接提供的一些 sidecar 容器,这些 sidecar 直接部署即可。这些 sidecar 容器(主要是上图的三个主要部件)监听自己对应的 CRD,触发对应的操作,通过 UDS 接口直接调用 CSI driver 的接口(例如 CreateVolume() 、NodePublishVolme() 等)来实现对卷的操作。

要开发 CSI Drivers 一般来说实现以下几个服务:

允许调用者 (Kubernetes 组件和 CSI sidecar 容器) 识别驱动程序及其支持的可选功能。

NodePublishVolume, NodeUnpublishVolume 和 NodeGetCapabilities 是必须的。

所需的方法使调用者能够使卷在指定的路径上可用,并发现驱动程序支持哪些可选功能。

实现 CreateVolume、DeleteVolume 接口

3.4 多集群资源管理方案 - 集群联邦(Federation)

Federation 是 Kubernetes 的子项目,其设计目标是对多个 Kubernetess 集群进行统一管理,将用户的应用部署到不同地域的数据中心。Federation 引入了一个位于 Kubernetes 集群只上的控制平面,屏蔽了后端各 k8s 子集群,向客户提供了统一的管理入口,如下图:

Federation 控制平面 “封装” 了多个 k8s 集群的 Master 角色,提供了统一的 Master,包括 Federation API Server、Federation Controller Manafer,用户可以向操作单个集群一样操作 Federation,还统一了全部 k8s 集群的 DNS、ConfigMap,并将数据保存在集中的 etcd 数据库中。

写给读者

对于 k8s 初学者来说,对 k8s 的第一印象应该是概念多,名词多。《kubernetes 权威指南:企业级容器云实战》这本书从企业实践角度入手,讲述了技术的演进,并在很多场景提供了不同技术实现的对比,结合 k8s 中文社区,这本书可以作为学习 k8s 的入门书籍。本文实际上是此书的一篇读书笔记,文章从计算资源、网络资源、存储资源三个方向展开,介绍了 k8s 里一些常见的概念和对象。由于篇幅问题,很多也很重要的概念并没有在文中详细介绍,读者可根据自身情况展开补充学习。关于 k8s 核心组件及工作原理将在后续陆续推出。


↙↙↙阅读原文可查看相关链接,并与作者交流