之前的文章,我们聊了Docker,提到一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境。似乎有了Docker,部署应用已经足够简单,那为什么还需要k8s等编排工具?这篇文章我们就来聊聊k8s要解决什么问题,以及它的一些核心概念。

Docker技术成熟后,容器发展就开始关注编排层面。比如Docker公司自己推出的docker-compose ,docker-swarm、以及Google和RedHat主导的k8s 。

k8s的直接理论基础来自于Google 公司在 2015 年 4 月发布的 Borg 论文了。

而Google在大规模集群的的调度编排方面有非常深后的技术积累。所以k8s项目一开始就站上了一个他人难以企及的高度。而一些核心特性也是直接来自于Borg系统,以至于一些概念让人摸不着头脑,比如Pod, Service等核心概念。

为什么需要k8s

容器编排的重要性

如果把Docker看做操作系统上的应用程序(当然这里的操作系统是分布式的),那k8s就是一个分布式操作系统,而且是一个很智能的操作系统。如果没有编排工具,你要启动一些“软件”,你只能一个个去启动(
“`docker run ..“` )。

有了编排工具,你只需要在一个文件中定义好你需要的“软件”,k8s就会帮你启动好。这里docker-compose可以认为是一个单机版“操作系统”,而swarm和k8s则可以完成分布式下环境下的容器编排,并且k8s功能更加丰富强大,所以它成为容器编排事实上的标准。

前面说k8s智能,是因为k8s知道你定义的那张“软件”清单(yaml文件),它会随时监测(
“`控制循环“`)当前的状态是否和你定义的一致,并进行动态修复(“`滚动更新Pod,重启,或删除Pod,Pod可以理解是对一个或多个容器的抽象“`),直到符合你yaml中的期望状态。

而且这张清单,可不止能定义你需要启动哪些容器,并且能够定义它们的依赖关系,启动顺序等非常多的属性。而这正是k8s编排中所谓的声明式API。

调度和编排的区别

调度的目的是找到一个合适的节点将容器运行起来,而编排则涵盖了调度,并且深入到了容器的生命周期、依赖关系等更深的层次。

比如HDFS,HBase,往往有一个master节点,用来协调各个机器的worker节点,保证数据保存在合理的节点上;还有Yarn在分配MR任务执行的资源时,也是类似的原理。其特点就是如何找到一个合理的节点,让数据或任务去保存或运行,而这个过程就是调度。

但是如果仅仅是帮我们把容器运行起来,其实根本不需要k8s。而Borg论文中则指出了一个非常重要的观点:

运行在大规模集群中的各种任务之间,实际上存在着各种各样的关系。这些关系的处理,才是作业编排和管理系统最困难的地方。

首先我们看一个简单的场景,来理解为什么编排能力才是我们更看重的东西。我们以部署一个wordpress博客网站为例:

  • 方式1:最传统的方式。

你需要安装配置mysql, 安装nginx服务器,以及wordpress程序等等…..

仅仅部署几个应用,你就能觉得它的复杂,配置的繁琐。可以想象,如果是几十上百个应用,自动化的过程不可避免。

  • 方式2:独立的Docker容器
#1.拉去镜像
docker pull mysql
docker pull wordpress
#2.启动mysql
docker run --name w-mysql -e MYSQL_ROOT_PASSWORD=123456 -d mysql
#3.启动wordpress,并连接mysql
docker run --name w-wordpress --link w-mysql:db -p 80:80 -d wordpress:latest

可以看到,安装过程只需要拉取镜像,启动即可,而不用关心软件的配置,版本等信息。简化了不少,但是还是手动去启动,拉取镜像等,如果容器很多,工作量依然很大,这只是个博客系统比较简单,真实的业务场景下,容器应用间的依赖关系,网络通信,数据持久化,状态等等我们关心的问题非常复杂,用这种方式处理起来非常棘手。

  • 方式3:docker-compose 轻量编排工具

定义一个
“`docker-compose-wordpress.yml“`

version: '3.3'
services:
   db:
     image: mysql:5.7
     container_name: mysql
     security_opt:
       - seccomp:unconfined
     ports:
       - 3306:3306     
     volumes:
       - /opt/module/mysql/conf:/etc/mysql/conf.d
       - /opt/module/mysql/data:/var/lib/mysql
       - /opt/module/mysql/logs:/var/log/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: 123456
       MYSQL_DATABASE: wordpress
   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "80:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: root
       WORDPRESS_DB_PASSWORD: 123456
       WORDPRESS_DB_NAME: wordpress

这里的配置文件虽然看着很多,其实它的层次很清楚。services我们可以认为是一组应用,是一种抽象,具体到这里可以认为是博客服务。

博客服务需要mysql和wordpress两个容器配合,所以紧接着db(名称可以自定义)指的就是mysql,在后边则描述了对应的镜像,端口等信息。

而wordpress,有一个关键字depends_on,则描述了它和db的依赖关系。通过这样一个描述文件。

这样一个文件,用的语法就是yaml文件的语法,相信开发过java微服务的肯定很熟悉,我们的配置文件大多都是用这种方式配置。

然后我们只需要执行如下命令,即可启动对应的容器,并且相互关联:

#docker-compose指令 
docker-compose -f docker-compose-wordpress.yml up -d

此时查看容器运行情况:

[root@VM_0_13_centos module]# docker-compose ps
       Name                     Command               State                 Ports
-----------------------------------------------------------------------------------------------
module_wordpress_1   docker-entrypoint.sh apach ...   Up      0.0.0.0:80->80/tcp
mysql                docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp, 33060/tcp

或者直接用docker命令查看:

[root@VM_0_13_centos module]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
64153ac017a3        wordpress:latest    "docker-entrypoint..."   3 weeks ago         Up 3 weeks          0.0.0.0:80->80/tcp                  module_wordpress_1
fde105317ddd        mysql:5.7           "docker-entrypoint..."   3 weeks ago         Up 7 days           0.0.0.0:3306->3306/tcp, 33060/tcp   mysql

可以看到docker-compose其实为我们做了三件事

  • 根据yaml定义去拉取相关镜像文件(如果本地没有)
  • 启动相关容器
  • 建立容器关联关系:这里就是连接mysql

从上边这个例子,我们可以看到,面对复杂的容器运维工作,你只需要告诉编排工具你需要什么(定义yaml)其他的工作就交给等编排工具去做就行。当然部署一个wordpress这样一个轻量级的应用,用docker或者docker-compose就完全够用,如果用k8s去做,又是怎样一番场景,我们继续往下看。

K8s核心概念

面向API对象编程

从前面的例子,我们看到docker-compose其实就是在以容器为基本单位,帮我们部署应用。但在k8s里,编排调度的最小单位并不是容器,而是Pod. 有了Docker容器,为什么还需要一个Pod? 前面我们说Docker容器就好比云计算操作系统中的应用,k8s相当于操作系统,那Pod就是进程组。即Pod是对一组容器(一个或多个)的抽象。之所以做这样一层抽象,是因为在 Borg 项目的开发和实践过程中,Google 公司的工程师们发现,他们部署的应用,往往都存在着类似于“进程和进程组”的关系。在同一个Pod中,可以直接通过localhost通信,并且可以共享网络栈和Volume。

如果用Pod描述上面的博客应用,yaml如下:

apiVersion: v1
kind: Pod
metadata:
  name: wordpress
  namespace: blog
spec:
  containers:
  - name: wordpress
    image: wordpress
    ports:
    - containerPort: 80
      name: wdport
    env:
    - name: WORDPRESS_DB_HOST
      value: localhost:3306
    - name: WORDPRESS_DB_USER
      value: root
    - name: WORDPRESS_DB_PASSWORD
      value: root@123
  - name: mysql
    image: mysql:5.7
    imagePullPolicy: IfNotPresent
    args:  # 新版本镜像有更新,需要使用下面的认证插件环境变量配置才会生效
    - --default_authentication_plugin=mysql_native_password
    - --character-set-server=utf8mb4
    - --collation-server=utf8mb4_unicode_ci
    ports:
    - containerPort: 3306
      name: dbport
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: root@123
    - name: MYSQL_DATABASE
      value: wordpress
    - name: MYSQL_USER
      value: wordpress
    - name: MYSQL_PASSWORD
      value: wordpress
    volumeMounts:
    - name: db
      mountPath: /var/lib/mysql
  volumes:
  - name: db
    hostPath:
      path: /var/lib/mysql

像这样的一个 YAML 文件,对应到 Kubernetes 中,就是一个 API Object(API 对象),K8s是面向API编程,Pod是最基本的一个API对象。Pod,而不是容器,才是 Kubernetes 项目中的最小编排单位。将这个设计落实到 API 对象上,容器(Container)就成了 Pod 属性里的一个普通的字段。这个Pod里 containers字段就是来定义容器的。这里volumes:定义了一个数据卷,指向了宿主机的/var/lib/mysql,而容器里则用volumeMounts字段来做关联挂载。

而Deployment则是更高层次的一种API对象,它可以直接控制Pod. 在K8s中,这种用一种对象控制另一种对象的方式,就是控制器模式,也是面向API编程的直观体现。比如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

这里我们定义了一个Deployment对象,它控制的Pod数量是两个,而Pod的具体属性,又通过template这个模板去生成。即描述的是一个Nginx。这样k8s通过控制循环,始终保证有两个nginx Pod实例运行。

所谓 Deployment,是一个定义多副本应用(即多个副本 Pod)的对象。Deployment 扮演的正是 Pod 的控制器的角色。这样的每一个 API 对象都有一个叫作 Metadata 的字段,这个字段就是 API 对象的“标识”,即元数据,它也是我们从 Kubernetes 里找到这个对象的主要依据。这其中最主要使用到的字段是 Labels。

而像 Deployment 这样的控制器对象,就可以通过这个 Labels 字段从 Kubernetes 中过滤出它所关心的被控制对象。比如,在上面这个 YAML 文件中,Deployment 会把所有正在运行的、携带“app: nginx”标签的 Pod 识别为被管理的对象,并确保这些 Pod 的总数严格等于两个。

而这个过滤规则的定义,是在 Deployment 的“spec.selector.matchLabels”字段。我们一般称之为:Label Selector。

一个 Kubernetes 的 API 对象的定义,大多可以分为 Metadata 和 Spec 两个部分。前者存放的是这个对象的元数据,对所有 API 对象来说,这一部分的字段和格式基本上是一样的;而后者存放的,则是属于这个对象独有的定义,用来描述它所要表达的功能。

核心功能全景图

运行在Pod中的应用是向客户端提供服务的守护进程,比如,nginx、tomcat、etcd等等,它们都是受控于控制器的资源对象,存在生命周期,我们知道Pod资源对象在自愿或非自愿终端后,只能被重构的Pod对象所替代,属于不可再生类组件。而在动态和弹性的管理模式下,Service为该类Pod对象提供了一个固定、统一的访问接口和负载均衡能力。所以Service的作用就是为Pod提供一个代理,从而代替 Pod 对外暴露一个固定的网络地址。

对上边的yaml进行改造:因为我们的数据库一般和业务要分离,从而保证数据的安全性。

所以我们将mysql和wordpress拆分为两个pod,并用service统一声明其对外暴露的端口。

“`wp-db.yaml“`

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deploy
  namespace: blog
  labels:
    app: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:5.7
        imagePullPolicy: IfNotPresent
        args:
        - --default_authentication_plugin=mysql_native_password
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
        ports:
        - containerPort: 3306
          name: dbport
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: rootPassW0rd
        - name: MYSQL_DATABASE
          value: wordpress
        - name: MYSQL_USER
          value: wordpress
        - name: MYSQL_PASSWORD
          value: wordpress
        volumeMounts:
        - name: db
          mountPath: /var/lib/mysql
      volumes:
      - name: db
        hostPath:
          path: /var/lib/mysql


---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: blog
spec:
  selector:
    app: mysql
  ports:
  - name: mysqlport
    protocol: TCP
    port: 3306
    targetPort: dbport

“`wp.yaml“`

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-deploy
  namespace: blog
  labels:
    app: wordpress
spec:
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - name: wordpress
        image: wordpress
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80
          name: wdport
        env:
        - name: WORDPRESS_DB_HOST
          value: 10.244.30.123:3306
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          value: wordpress

---
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  namespace: blog
spec:
  type: NodePort
  selector:
    app: wordpress
  ports:
  - name: wordpressport
    protocol: TCP
    targetPort: wdport
    nodePort: 30090
    port: 80

这里声明为NodePort类型,这是Service和宿主机做映射的一种端口类型,后边有机会再分享。

而针对像大数据等作业任务,又有Job等API对象,而像负载均衡等能力,则提供Ingress Controller 去对接我们比较熟悉的Nginx,k8s的核心就是可编程性,即你完全可以根据自己的需要编写自己的API对象,网络插件等等。而这种能力,让k8s的适应能力非常强。所以如果仅仅用k8s是部署一个wordpress这样的博客服务,你会感觉有点杀鸡用牛刀的感觉,甚至有点繁琐。

但k8s天生要解决的就是复杂的问题,就是要解决真实运维场景下,大规模复杂的容器编排场景。而它的API也在不断丰富,使用门槛也在慢慢降低,但无疑k8s在云计算的领域是绝对的王者,围绕它建立的生态体系在不断壮大。后续我也会分享在大数据领域,比如Flink集群和k8s深度结合等相关的实践。

发表回复

您的电子邮箱地址不会被公开。