# 1. 概念

# 1.1. k8s

kubernetes,简称K8s,是用8代替名字中间的8个字符“ubernete”而成的缩写。

k8s是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes 的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes 提供了应用部署,规划,更新,维护的一种机制。

一个简单的k8s系统如下图所示,由一个master node 和 任意数量的 worker node. 当开发提交 app 描述文件(比如描述运行多少个副本,暴露端口,指定的镜像,探活, 更新策略等)到 master 节点,然后 k8s 把 app 部署到 worker nodes. 至于 app 部署到哪个 woker node, 我们并不关心。

一个更详细的k8s系统如下图所示, 该 k8s cluster, 由一个 master node 和3个 worker node 组成。

# 1.2. Container

一个 Pod 内可以有多个容器 container

在Pod中,容器也有分类:

  • 标准容器 Application Container。
  • 初始化容器 Init Container。
  • 边车容器 Sidecar Container。
  • 临时容器 Ephemeral Container。

一般来说,我们部署的大多是标准容器( Application Container)。

# 1.3. 虚拟机和容器的区别

每个虚拟机运行在自己的 Linux 内核上(即下图中 Guest OS, 每个 VM 可以安装不同版本的 linux 系统),而容器都是调用同一个宿主机内核(如果一个容器化的应用需要一个特定的内核版本,那么它可能不能在每台机器上工作,也不能将一个 x86 架构编译的应用容器化之后,又期望它在 arm 架构的机器上运行)。

# 1.4. 为什么多个容器比单个容器中包含多个进程要好

容器之所以被设计为单个容器只运行一个进程(除非进程本身产生子进程),是因为如果单个容器中运行多个不相关的进程,那么开发人员需要保持这些所有进程都运行OK, 并且管理他们的日志等(比如,两个进程,其中一个生产者进程,一个消费者进程,如果消费者进程crash之后,我们需要考虑该进程重启的机制)。

# 1.5. Node

Node 是真正运行容器的主机,负责管理镜像和容器以及 cluster 内的服务发现和负载均衡。

# 1.6. Pod

Pod是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。

最小xx单位“要么就是事物的衡量标准单位,要么就是资源的闭包、集合。前者比如长度米、时间秒;后者比如一个”进程“是存储和计算的闭包,一个”线程“是CPU资源(包括寄存器、ALU等)的闭包。

同样的,Pod 就是 K8S 中一个服务的闭包。这么说的好像还是有点玄乎,更加云里雾里了。简单来说,Pod 可以被理解成一群可以共享网络、存储和计算资源的容器化服务的集合。再打个形象的比喻,在同一个 Pod 里的几个 Docker 服务/程序,好像被部署在同一台机器上,可以通过 localhost 互相访问,并且可以共用 Pod 里的存储资源(这里是指 Docker 可以挂载Pod内的数据卷,数据卷暂时理解为“需要手动 mount 的磁盘”)。

同一个 Pod 之间的 Container 可以通过 localhost 互相访问,并且可以挂载 Pod 内所有的数据卷;但是不同的 Pod 之间的 Container 不能用 localhost 访问,也不能挂载其他 Pod 的数据卷。

要点如下:

  • 可以把 pod 看作一个独立的机器,一个 pod 中可以运行一个或者多个容器,这些容器之间共享相同的 ip 和 port 空间
  • 一个 pod 的所有容器都运行在同一个 woker node 中,一个 pod 不会跨越两个 worker node.
  • 由于大多数容器的文件系统来自于容器镜像,所以每个容器的文件系统与其他容器是完全隔离的,但是可以使用 Volume 在容器间共享文件目录。
  • pod 是短暂的, 他们随时的会启动或者关闭。也就是这如果某个 pod 被销毁之后,重新创建的pod 的 ip 可能会变化。

# 1.7. 为什么需要 pod

前边已经提到,容器被设计为每个容器只运行一个进程,那么多个进程就不能聚集在一个单独的容器,但是容器之间是彼此完全隔离的,多个进程分布在对应的多个容器中,进程之间无法做到资源共享(比如,前边提到到生产者/消费进程,他们通过共享内存和信号量来通信,但是如果生产者进程和消费者进程分布在两个容器中,则IPC是相互隔离的,导致无法通信)。所以我们需要一种更高级的结构来将容器绑定在一起,并且将它们作为一个单元进行管理(即:多个容器间共享某些资源),这就是为什么需要 pod 的根本原理。

# 1.8. Label

标签是一个可以附加到资源的任意 key-value 对(一个标签就是一个key/value对,每个资源可以拥有多个标签), 然后通过 Selector (即标签选择器)来选择具有确切标签的资源。

# 1.9. Deployment 和 ReplicaSet(简称RS)

一个 Deployment 控制器为 Pods 和 ReplicaSets 提供声明式的更新能力。

你负责描述 Deployment 中的 目标状态,而 Deployment 控制器以受控速率更改实际状态, 使其变为期望状态。你可以定义 Deployment 以创建新的 ReplicaSet,或删除现有 Deployment,并通过新的 Deployment 收养其资源。

Deployment 的作用是管理和控制 Pod 和 ReplicaSet,管控它们运行在用户期望的状态中。哎,打个形象的比喻,Deployment 就是包工头,主要负责监督底下的工人Pod干活,确保每时每刻有用户要求数量的Pod在工作。如果一旦发现某个工人 Pod 不行了,就赶紧新拉一个 Pod 过来替换它。

ReplicaSet 的目的是维护一组在任何时候都处于运行状态的 Pod 副本的稳定集合。 因此,它通常用来保证给定数量的、完全相同的 Pod 的可用性。

再来翻译下:ReplicaSet(副本控制器) 的作用就是管理和控制 Pod,管控他们好好干活。但是,ReplicaSet 受控于 Deployment。形象来说,ReplicaSet就是总包工头手下的小包工头

新的问题又来了:如果都是为了管控 Pod 好好干活,为什么要设置 Deployment 和 ReplicaSet 两个层级呢,直接让 Deployment 来管理不可以吗?

之所以有 deployment 和 replicaSet 两个对象, 是遵守 Single responsibility principle 即单一功能原则,认为对象应该仅具有一种单一功能的概念。

deployment 相对 ReplicaSet 有版本的概念,而 ReplicaSet 没有,所以可以很方便的针对 deployment 进行版本更新和回滚

另外,在具体实现版本更新的时候,K8S采用滚动更新,即再创建一个新的 ReplicaSet 对象,用来管理新版本的多个 pod,等新的 ReplicaSet 的各个 pod 都 ready 后,才会把老的 ReplicaSet 杀掉。这也是符合“不可变基础设施”的 Cloud Native 设计原则的。

所以,多一层抽象后,符合了 Single Responsible 的设计原则,同时带来了极大的灵活性好处。

# 1.10. Service 和 Ingress

前文介绍的 Deployment、ReplicationController 和 ReplicaSet 主要管控Pod程序服务;那么,Service 和 Ingress 则负责管控Pod网络服务

K8S中的服务(Service)并不是我们常说的“服务”的含义,而更像是网关层,是若干个Pod的流量入口、流量均衡器

那么,为什么要 Service 呢?

私以为在这一点上,官方文档讲解地非常清楚:

Kubernetes Pod 是有生命周期的。 它们可以被创建,而且销毁之后不会再启动。 如果您使用 Deployment 来运行您的应用程序,则它可以动态创建和销毁 Pod。

每个 Pod 都有自己的 IP 地址,但是在 Deployment 中,在同一时刻运行的 Pod 集合可能与稍后运行该应用程序的 Pod 集合不同。

这导致了一个问题: 如果一组 Pod(称为“后端”)为群集内的其他 Pod(称为“前端”)提供功能, 那么前端如何找出并跟踪要连接的 IP 地址,以便前端可以使用工作量的后端部分?

Service 是 K8S 服务的核心,屏蔽了服务细节,统一对外暴露服务接口,真正做到了“微服务”。举个例子,我们的一个服务A,部署了3个备份,也就是3个 Pod;对于用户来说,只需要关注一个 Service 的入口就可以,而不需要操心究竟应该请求哪一个 Pod。优势非常明显:一方面外部用户不需要感知因为 Pod 上服务的意外崩溃、K8S 重新拉起 Pod 而造成的 IP 变更,外部用户也不需要感知因升级、变更服务带来的Pod替换而造成的 IP 变化,另一方面,Service 还可以做流量负载均衡

但是,Service 主要负责 K8S 集群内部的网络拓扑。那么集群外部怎么访问集群内部呢?这个时候就需要 Ingress 了,官方文档中的解释是:

Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。

Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管。

翻译一下:Ingress 是整个K8S集群的接入层,复杂集群内外通讯

笔者把 Ingress 和 Service 的关系绘制网络拓扑关系图如下,希望对理解这两个概念有所帮助:

要点如下: 为什么需要 Service?

  • pod 是短暂的, 随时都可能被销毁
  • 新的 pod 创建之前不能确定该 pod 分配的 ip
  • 水平伸缩也就以为着多个 pod 可能提供相同的服务,客户端不想也不关心每个 pod 的 ip, 相反,客户端期望通过一个单一的 ip 地址进行访问多个 pod.

服务是一种为一组功能相同的pod提供单一不变的接入点的资源。当服务存在时,该服务的 ip 地址和端口(创建服务的时候,通过 ports[n].portports[n].targetPort 指定了服务端口到 pod 端口的映射)不会改变。客户端通过 ip 和 port 与服务建立连接,然后这些连接会被路由到提供该服务的某个 pod 上(通过负载均衡)

# 1.11. namespace 命名空间

和前文介绍的所有的概念都不一样,namespace 跟 Pod没有直接关系,而是K8S另一个维度的对象。或者说,前文提到的概念都是为了服务Pod的,而namespace则是为了服务整个K8S集群的。

那么,namespace是什么呢?

Kubernetes 支持多个虚拟集群,它们底层依赖于同一个物理集群。 这些虚拟集群被称为名字空间。

翻译一下:namespace 是为了把一个K8S集群划分为若干个资源不可共享的虚拟集群而诞生的

也就是说,可以通过在 K8S 集群内创建 namespace 来分隔资源和对象。比如我有2个业务A和B,那么我可以创建 ns-a 和 ns-b 分别部署业务A和B的服务,如在 ns-a 中部署了一个 deployment,名字是 hello,返回用户的是“hello a”;在 ns-b 中也部署了一个 deployment,名字恰巧也是 hello,返回用户的是“hello b”(要知道,在同一个 namespace 下 deployment 不能同名;但是不同 namespace 之间没有影响)。

# 1.12. NodePort

Service 用标签选择一组 Pod,然后用这个名称作为内部域名提供服务,这一层上可以算作是个简单的服务注册发现组件。

这个对象有几种类型,ClusterIP 类型的服务通常用作本集群内部的互相通信;NodePort 类型的服务会在集群每个节点上开放同一个端口,对外提供服务;如果集群在公有云上部署或者有兼容的负载均衡支持的环境里运行,还可以使用 Loadbalancer 类型的服务,可以自动跟负载均衡设施打通,得到外部 IP 之类的支持。

Ingress 资源更近一步,可以在集群边缘为暴露出来的服务提供域名、rewrite、认证之类的更高级功能,Ingress 对象是由 ingress 控制器实现的,不同控制器会有不同的附加功能。

NodePort 示意:

# 2. yaml 配置

# 2.1. Deployment

下面几个值相同,都是 deployment.${keyName}.${domain} 的格式,keyName 大概率是命名空间的意思。

  • metadata.name
  • spec.selector.matchLabels.k8s-app
  • spec.template.metadata.labels.k8s-app

此外,该 yaml 还需要镜像地址,每次发布都会变。

最后的 imagePullSecrets 是可选的,用于集群内拉取镜像,如果开启了 tcr 插件,则不需要。

apiVersion: apps/v1
kind: Deployment
metadata:
  # deploymentName
  name: deployment.nginx.test.icabe.game
spec:
  minReadySeconds: 0
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      # deploymentName
      k8s-app: deployment.nginx.test.icabe.game
  template:
    metadata:
      labels:
        # deploymentName
        k8s-app: deployment.nginx.test.icabe.game
    spec:
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
      nodeSelector: {}
      dnsPolicy: ClusterFirst
      containers:
        - name: nginx
          image: icabe-tcr.tencentcloudcr.com/icabe/pmd-mobile.icabe.wg.platform.feature.login:1704448623620-428
          ports:
            - name: container
              containerPort: 80
              protocol: TCP
          volumeMounts:
            - name: corefile
              mountPath: /data/corefile
              readOnly: false
            - name: logs
              mountPath: /data/logs
              readOnly: false
      volumes:
        - name: corefile
          hostPath:
            path: /data/corefile
        - name: logs
          hostPath:
            path: /data/logs
      imagePullSecrets: []

# 2.2. Service

serviceNameingress 中的 paths.backend.service.name 相同,本例中都为 service-nginx-test-icabe-game

deploymentNameDeployment 中的 name 相同,本例中都为 deployment.nginx.test.icabe.game

nodePort0,说明为系统自动分配。

示例如下:

apiVersion: v1
kind: Service
metadata:
  # serviceName
  name: service-nginx-test-icabe-game
  labels: {}
  annotations: {}
spec:
  type: NodePort
  selector:
    # deployName
    k8s-app: deployment.nginx.test.icabe.game
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: container
      nodePort: 0

# 2.3. Ingress

metadata.name 格式为 ingress-${domain}-${i}iclb 的索引值。

lb-g9buwd3i 是域名配置配置的 clb

spec.tls.hostsspec.tls.secretName 格式分别为 [domain]${domain}.cert

格式示例:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-test.icabe.game-0
  annotations:
    kubernetes.io/ingress.class: qcloud
    kubernetes.io/ingress.existLbId: lb-g9buwd3i
spec:
  tls:
    - hosts:
        - test.icabe.game
      secretName: test.icabe.game.cert
  rules:
    - host: test.icabe.game
      http:
        paths:
          - backend:
              service:
                # serviceName
                name: service-nginx-test-icabe-game
                port:
                  number: 80
            path: /
            pathType: Prefix

# 3. 其他概念

  1. TCR

指的是容器镜像服务(Tencent Container Registry,TCR),可参考 https://cloud.tencent.com/product/tcr

  1. BCS

指的是蓝鲸容器管理平台(Blueking Container Service),可参考 https://github.com/TencentBlueking/bk-bcs

# 4. 常见问题

  1. 为什么更新BCS模板集的那个插件,总是提示没权限?

感觉这里的权限校验有问题,插件的默认使用的权限是流水线最后更新人,也就是下面的 app_code, app_secret 都是流水线最后更新人的,但是 bk_user_name 又是取的流水线启动者的,如果二者不匹配,自然是报权限异常的。

def apigw_auth_params():
    params = python_atom_sdk.get_input()
    return {
        "bk_username": python_atom_sdk.get_pipeline_start_user_name(),
        "bk_app_code": get_region_info(params.get("region")).get("app_code"),
        "bk_app_secret": get_region_info(params.get("region")).get("app_secret"),
    }
  1. 前端多分支发布如何实现的

发布的时候每个域名都是单独的镜像,对应集群不同的 nodePort。

同一个域名下不同的路径,都是打到一起的,是通过拷贝公共产物的方式。多分支其实就是同一容器下的不同路径。