在熟悉了Docker相关的操作之后, 我们现在就可以来对大规模集群的容器编排下手了.
容器编排工具栈
我们之前在说Docker的时候就提到过了编排与自动化的概念, 当时我们还引用了维基百科的概念. 我们在使用Ansible的时候, 通过编写playbook就是在进行自动化的编排. 除此之外, 还有puppet, saltstack等等. 而Ansible这些对应的单元是一个个的虚拟机, 在我们引入了Docker之后, 就会发生些许变化, 当管理的对象发生的变化的时候, 我们也要改变我们所使用的工具栈.
首先就是Docker自己的Docker Compose, 但是这个工具面向的只是一个Docker Host, 也就是说, 他只能对一台主机的多个容器进行编排. 为了能够扩展到多主机的集群环境, 另一个软件就是Docker Swarm, 他可以将多个主机的 容器资源抽象成一个大的资源池, 进行整合工作. 但是将一个Host加入到这个Swarm成员有需要另一个工具, 叫做Docker Machine. 这三个工具, 组成了早期Docker编排的三剑客.
第二组工具的核心是Mesos, 一个Apache基金会下的分布式系统kernel, 能够对计算进行抽象并提供API支持, 它原生提供了容器支持. 但是说白了Mesos就是一个资源分配工具, 或者说是一个IDC的操作系统. 因此我们还需要一个容器的托管工具, 因为Mesos并不能直接运行容器, 这就是Marathon, 这是一个容器的编排框架.
最后一种常见的工具, 也就是我们即将探讨的Kubernetes. 就目前来看, Kubernetes已经占据了最多的份额.
Kubernetes
Kubernetes源自于Google内部的Borg项目, 据说google在内部使用容器技术已经超过了十年秘而不宣, 没想到Docker横空出世, 自己私藏的东西竟然被民间发现了, 于是使用Go语言重新实现了Borg项目. 这就是Kubernetes的来由.
Kubernetes能够实现自动装箱. 啥意思嘞? 就是根据资源之间的依赖关系自动的进行部署. 另外还可以自我修复, 当应用程序崩溃后, 得益于容器技术, 我们不再像之前一样需要进行复杂的部署流程了, 直接kill掉出问题的容器然后新启动一个就行了. 另外, Kubernetes还可以实现水平扩展, 只要物理服务器的资源足够, 就可以不断的新建容器. 以及, 服务发现以及负载均衡, 自动发布和回滚, 密钥和配置管理, 存储编排, 批量处理执行.
按照我们之前的理解, 其实Kubernetes也是一个集群, 在几个是主机上安装Kubernetes应用程序, 接着让他们协同工作. 值得注意的是, 这里的主机间是存在角色的, 类似我们之前所学到的MySQL的主从模型, 存在一个或多个Master Node和多个Worker Nodes. 这些Worker就是用来提供计算资源以及存储的, 说白了他们其实就是容器节点, 对于容器的启动, 调度等等操作就由Master Node来进行, 用户向Master发送请求, 有Master来分析后端当前的负载情况来进行具体的操作. 顺便说一个, Kubernetes也可以托管Kubernetes.
Kubernetes的架构概述和基础概念
既然我们说了是向Master发送请求, 那么必然会有一个API的组件, 在Kubernetes的Master Node上, 这个组件就叫做API Server, 除此之外, 必然还有一个叫做调度器(Scheduler)的组件, 他时刻关注后端的CPU, RAM存储等情况来对资源进行调度.
上面谈论的都是Kubernetes的Master, 但是对于容器, 我们之前在说到的Docker的时候提到过容器的健康状态检测(Health Check), 这就依赖在Worker上的应用程序**kubelet
**了. 这个程序会监控当前主机上的容器状态并作报告. 现在假设, 这个Worker Node, 也就是我们运行容器的主机宕机了而不是容器崩溃的话, 在整个主机上托管的服务就都下线了, 这个时候就要做上面提到的自我修复了. 这些控制器会监控自己创建的容器健康状态, 如果有问题就进行调整.
我们再来贴个图:
但是这个控制器组件(kubelet)也是会出问题的, 因此我们在主节点上有Controller Manager组件, 与容器节点上的kubelet应用程序通过API Server进行通信, 在这个组件的层次上, 我们进行冗余. 值得一提的是, Kubernetes包含许多类型的控制器,
- Node Controller: Responsible for noticing and responding when nodes go down.
- Replication Controller: Responsible for maintaining the correct number of pods for every replication controller object in the system.
- Endpoints Controller: Populates the Endpoints object (that is, joins Services & Pods).
- Service Account & Token Controllers: Create default accounts and API access tokens for new namespaces.
在Kubernetes之上, 最小的部署单元叫做**Pod
**. 同时也是最基本的执行单元(逻辑上). 这个东西封装了容器, 以及配套的网络, 存储等等, 是一个对容器的抽象. 一个Pod里面可以有多个容器, 也可以只存在一个. 这个Pod
使用标签(label)来表示元数据的, 这个label是一个kv形式的数据, 为了选择和挑选不同的资源, 还有label selector
.
Pod管理是由Kubernetes的Controller管理的. 我们可以把Pod分成两种类型的:
- 自主式Pod
- 控制器管理Pod, 控制器有:
- ReplicationController
- ReplicaSet (不直接使用)
- Deployment: 无状态应用
- StatefulSet: 有状态应用
- DaemonSet
- Job, Cronjob
Pod有其自己的生命周期, 如果我们使用了控制器启动了一个pod, 那么这些pod就会被我们所指定的控制器监控并且受控制.
那么说到现在, 我们就有2个概念了, 分别是Pod和Controller, 他们组成了Kubernetes的工作负载. 但是, 想象这样的一种场景, pod是有自己的IP地址的, 当控制器kill掉一个创建一个新的pod的时候, IP地址也会发生变化. 这样的话, 我的前端如何追踪这个IP, 并且找到后端接口的地址呢? 由此我们就引入了**服务(Service)**的概念.
你可以把服务理解成对pod访问策略的一个抽象, Service通过我们之前说的selector来找到对应的Pod. 当我们Pod重建, IP发生变化, 但是label中的某些元数据没有变化, Service正式通过这种机制来找到对应的Pod的.
说到这里, 可能会有点晕了. 没事, 我们来窥探一个正在运行的Pod都有哪些信息:
1 | [root@kbs-master ~]# kubectl describe pods myapp-5d587c4d45-8859f |
这里是我已经启动了一个双节点Kubernetes集群, 在master节点上执行的结果, 从describe的结果中我们可以看到:
- myapp运行在node1/192.168.10.125
- 有一个app=myapp的label
- 有一个10.244.1.18的IP
- 被ReplicaSet控制器控制
- 有一个叫做myapp的容器
- … (后面对我们这里的内容不重要, 就跳过了)
刚刚说到, service通过label来寻找pod, 我们来看一个service:
1 | [root@kbs-master ~]# kubectl describe svc myapp |
请看! 这里的Selector是不是和上面的label一模一样? 接着我们可以看到这是一个web服务, 因为service对应的端口是80, 你可能会疑惑为什么这个service有三个终端IP, 我们刚刚在上面查看myapp的信息的时候, 说到这是一个由replicaSet控制器控制的pod. 从名字你也可以看出来啦, 这是一个保证复制数量的控制器:
1 | [root@kbs-master ~]# kubectl describe deployment myapp |
一共有三个, 当前的状态就是三个都在.
OK, 我们来看一下host的网络状态:
发现什么奇怪的现象了吗? 没错 我们service的IP地址并没有出现在这里.
flannel
和cni
都是10.244.0.X
网段的, 他们是为Pod服务的, ens33是我们本机的物理网卡. Service的10.101.214.X
网段在哪里?
相信聪明的你一定想到了, 此时查看一下iptables你就会发现, Service的这个IP实际上是个虚拟IP, 所谓的根据Service访问Pod服务其实就是一个NAT规则而已:
注意这里要在pod的主机上查看嗷.
借用Kubernetes官方的图, 其实就是这样子:
那么, 说道这里. 我们就可以小结一下了.
我们从最小的单元Pod
出发, 在我们创建之后, Kubernetes会为Pod
加上一些元数据. 接着, 我们定义一个控制器(e.g: ReplicaSet), 用来做高可用. 这个时候, 我们就有了服务, 但是需要为这些服务提供一个固定的访问接口才行. 因此, 我们需要通过某种手段, 来确保当后端服务的资源变动时, 接口也能知道并且及时改变, 这里的一种解决方案就是通过Pod
的元数据以及iptables的NAT规则来进行动态的调整.
由此, 我们也可以看出来Kubernetes的网络构架是什么样子的. 也就是, Pod间组成一个集群私有网络, Node间组成一个私有网络, 而节点可以通过kube-proxy
来访问Pod私有网络, Service使用虚拟IP组成网络. 用一张图来说明就是:
Pod
我们先来剖析一下一个Pod包含哪些东西, 首先我们的Pod中最重要的一个部件就是主容器. 一般情况下, 在我们的主容器启动前, 我们会先进行一系列的初始化的操作, 这个初始化也是交给容器来执行的. 我们可以把它叫做init container
. 这个初始化包含一些环境设定, 以及为了主容器的执行做一些准备. 当然这些初始化容器可能不止一个, 如果有多个, 他们是串行执行的. 当所有的初始化工作结束, 主容器就会开始执行, 这个时候会有一个post start
的 过程, 当主容器刚刚执行的时候, 用户可以手动插入这个钩子. 所以当然了, 在结束之前, 同样也有一个钩子叫做pre stop
. 除此之外, 还会有两个健康状态检测, 其中一个是存活状态检测, 另外一个就是检测主容器是否已经准备好, 也就是是否可以提供服务.
另外, 每一个Pod都会包含一个固定的, 容量很小的容器, 叫做pause
. 每一个Pod的各个容器都使用这个pause
的网络栈和存储卷.
Pod是有生命周期的, 他的状态有: Pending
, Running
, Failed
, Succeeded
, Unknown
. 在我们创建Pod的时候, 会先将此Pod的状态信息写入到etcd中. Pod会先被调度到我们的某个节点上, 这需要scheduler一直参与. 当决定好在哪个节点上创建并且执行的时候, apiserver会再把etcd中的状态信息更新.
在我们Kubernetes中, 为了启动一个Pod需要这样的过程:
Init容器是干什么的? 简单的说, 这是一个和我们应用程序容器分离的镜像, 我们可以把一些通用的工具集成到InitC中, 这样我们的应用程序镜像就不需要在来做这些操作, 从而专注于自己的业务功能. 另外, InitC可以作为我们主容器的一个访问代理来使用, 例如访问文件. 当然, 通过InitC我们还可以实现简单的同步操作.
当检测到我们Pod中的InitC容器启动失败,这个时候就会根据我们设置的重启策略(restartPolicy)来决定做哪些决策. 一共有三种:
- Always (默认)
- OnFailure
- Never
看名字就能理解就不赘述了. 当然一直重启会对我们的系统造成额外的压力, 因此重启的时间间隔是随着重启次数不断增加的, 最多为300s.
接下来我们说说上文中提到的Pod健康状态检测, Kubernetes可以通过execAction, httpGetAction或者tcpSocketAction来检测, 这三种也就是Kubelet调用的由容器实现的处理程序. 包含一些探测间隔, 超时设定, 失败次数门限等等设定. 探针会获得三种结果: 成功, 失败, 未知.
- livenessProbe: 指示容器是否正在运行
- readinessProbe: 指示容器是否正常提供服务
Pod Controller
接下来我们来着重说一下Pod的控制器.
当我们新建一个Pod的时候, 删除之, 该pod并不会重建. 因为这是一个自主式的Pod, 它并没有被控制器管理. 因此, 自主式Pod的时候并不常见, 通过模板内嵌到控制器才是我们更好管理的一个方式.
我们先再次来列举一次, 常用的Pod Controller:
- ReplicaSet: 创建指定数量的Pod副本, 确保数量始终满足用户设定的值, 支持扩容缩容.
- ReplicationController: 和ReplicaSet目的一致, 但是不支持set选择器. 推荐使用ReplicaSet而非ReplicationController
- Deployment: 工作在ReplicaSet之上, 支持滚动更新和回滚, 声明时配置.(Recommended) 仅用于无状态应用.
- DaemonSet: 用于确保每个节点指运行一个Pod, 常用一些系统级别的应用.
- Job: 可以按照用户指定数量启动, 一次性. 按照工作是否完成来决定是否重建. 当任务完成直接退出.
- Cronjob: 周期性的Job.
- StatefulSet: 每一个Pod单独管理, 针对有状态的应用. 需要脚本嵌入模板来执行操作.
手写脚本需要运维人员有很好的运维技能, 因此, 如果厂商或者第三方有提供维护脚本就会很方便. 因此, Kubernetes在1.2版本推出了一种资源类型TPR (Third Party Resources), 在1.7版本废止, 因为1.8版本中推出了CDR (Custom Defined Resources)
接下来我们就开始看这些控制器吧.
来看一个rs的模板吧:
1 | apiVersion: apps/v1 |
接下来是一个deploy的模板:
1 | apiVersion: apps/v1 |
Service
我们再来说一下Service这个资源, 在Kubernetes上, Pod存在生命周期, 为了给Pod提供一个固定的访问接口(中间层), 也就是这里的Service. 另外, 我们的Service名称解析强依赖与Kubernetes中的DNS服务, 也就是所谓CoreDNS
, 旧版本的Kubernetes使用的是kube-dns
.
Service在Kubernetes中有着四种类型:
- ClusterIP: 默认类型, 自动分配一个Cluster内部的IP, 使得服务只能从集群内部访问.
- NodePort: 在ClusterIP的基础上为Service在每台机器上绑定一个端口, 这样就可以通过
NodeIP:NodePort
来访问到该服务 - LoadBalancer: 在NodePort的基础上, 借助cloud provider创建一个外部的负载均衡器, 并将请求转发给
NodeIP: NodePort
- ExternalName: 通过返回一个CNAME记录来使服务映射到
externalName
, 不创建代理 (kube-dns>1.7/CoreDNS>0.0.8)
说到Service, 我们还是要重复一下之前说过的, Kubernetes的网络:
- node network
- pod network
- cluster network (虚拟的IP)
我们之前也说过, worker node通过使用kube-proxy和master node的api service进行通信, 通过Kubernetes的watch机制. 接下来我们说三种Service的代理模式:
第一种(userspace)Pod之间的访问机制是通过访问内核的iptables规则, 然后会被Service转到本地监听的套接字上, 也就是kube-proxy, 然后由它处理和分发, 接着还会再走内核的iptables分发到目标节点的kube-proxy, 最后到达目标Pod. 这种方法十分的麻烦, 因此我们有第二种方法.
第二种就是直接使用iptables, 而不使用kube-proxy. 当然了, 我们还是需要kube-proxy来维护netfilter规则的. 另外, 还可以直接使用ipvs来调度.
Kubernetes1.14之后默认使用的是ipvs, 如果ipvs没有被激活, 则会降级到iptables.
写一个简单的redis服务:
1 | apiVersion: v1 |
但事实上, 从我们的Service到Pod, 会经过一个Endpoint. 这个Endpoint资源本质上我们是可以手动指定的. 如果我们没有修改默认的资源记录域名, 一般来说都是这样的(例如上面的redis):
1 | redis.default.svc.cluster.local. |
也就是服务名+名称空间+Domain
另外, 还有一种无头服务(Headless Service), 也就是不指定ClusterIP, 指定为None或者空字符串, 这样对Pod的请求就会直达到Pod而不会由Service牵头, 这时候的Service可以提供分组服务而不用于调度.
Ingress
我们之前说过想要在外部访问到我们的Pod, 可使用NodePort. 我们使用ipvs或者iptables来进行访问的调度, 但是我们知道这两个都是处在4层的. 考虑一种简单的情况, 假设我们的站点使用的是HTTPS的协议, 那这个证书显然不应该是后端Pod的IP而应该是我们调度器的IP, 因为用户访问的域名解析结果显然应该是调度器的IP嘛. 那么问题就显而易见了, 因为我们真正提供服务的Pod和证书信息不匹配, 这个时候我们就需要在请求到达Pod之前, 将HTTPS的头拿掉, 也就要求我们要有一个七层的负载均衡器在前面. 这样, 我们的Pod就使用HTTP即可, 而在互联网中使用HTTPS就好, 然后在接入层卸载SSL.
基于此, 我们可以启动一个Pod然后共享宿主机的IP地址, 而这个Pod拥有能够七层调度的能力(可以把它想象成一个Nginx), 这样我们就不使用Service了. 当然, 如果这样做的话, 我们的这个Pod就必须是单点. 这个时候我们可以使用DaemonSet控制器来进行控制.
我们可以通过打上污点(taint)的方式, 让我们的控制器控制仅仅在若干个节点上进行, 并且让这些节点只运行这个调度器, 为我们集群提供一个统一的七层调度访问入口.
事实上, 这个东西就是Ingress Controller, 虽然他也叫做Controller, 但它并不是master node上的Controller Manager的子组件, 而是一个独立的应用. 这种方式看似美好, 但是存在一个问题, 因为我们的Ingress Controller并不能像我们的Service一样对后端的Pod有实时的watch, 由于Pod存在生命周期, 有可能Pod会被替换掉, 同时IP地址也会发生改变. 所以这个时候我们就可以考虑使用之前说过的headless service了, 让这个服务仅用来对我们的Pod做分组, 我们还是使用Ingress Controller进行调度, 这个时候我们就可以声明一个Ingress资源, 从而定义前端的调度规则. 并且它还可以注入到当前的配置中, 当后端发生变化我们的Ingress会知道, 于是他可以生成配置信息然后通知当前的Pod进行重载.
安装ingress很简单, 直接照着nginx官网的教程走就可以了. 当然, 除了Nginx, 也有很多ingress的实现方案, 例如HAProxy, 这里我们就用Nginx来做测试了.
1 | [root@kbs-demo deployments]# kubectl get pods -n nginx-ingress |
存储
我们之前说过, Pod存在生命周期, 那么我们存在在Pod内部存储空间的数据就会随着Pod的重启而消失. 因此, 我们就需要在Pod自有文件存储之外的地方, 例如我们的节点文件系统. 这样在一定程度上我们就拥有了持久化存储能力. 问题是, 我们的Pod是有调度的. 因此我们直接丢到节点存储上是不合理的. 所以我们应该存储在脱离节点存储的位置上, 例如NFS.
Kubernetes的存储, 我们可以展开四个话题:
- ConfigMap
- Secret
- Volume
- PV-PVC
我们就一个一个来说吧.
首先说到ConfigMap, 这是个啥呢, 简单来说, 这就是一个向容器中注入配置信息的机制, 这就像是Kubernetes的配置文件注册中心(这样举例好像不对…但是API和思想差不多).
创建一个ConfigMap主要有三种方式, 通过目录, 使用文件以及命令行传参. 使用起来也挺简单, 我们在Pod的资源清单中env
内部写上就可以了.
1 | env: |
另外, 我们在导入之后也可以在容器的command
中进行引用. 除此之外, 我们还可以把我们configMap的文件直接挂载到我们的Pod中.
而Secret, 可以用来存储我们的一些密码密钥等需要被加密的配置数据. Secret可以使用Volume挂载或者当做会环境变量来使用. 使用起来Secret和ConfigMap十分相似. 我们Secret有三种类型:
- Service Account: 用来访问Kubernetes API, 由Kubernetes自动创建, 并且会自动挂载到Pod的
/run/secrets/kubernetes.io/serviceaccount
目录中 - Opaque: Base64编码格式, 存储密码密钥等
- Kubernetes.io/dockerconfigjson: 用来存储私有的docker registry的认证信息
因此我们配置容器化应用的方式就有这些了:
- 自定义命令行参数:
args: []
- 把配置文件直接丢到镜像中
- 环境变量
- Cloud Native的应用程序一般直接通过环境变量加载配置
- 通过
entrypoint
脚本来预处理变量为配置文件中的配置信息
- 存储卷
上面的两个更多的都是为了向Pod内部注入配置信息而非存储, 接下来我们来说说存储卷Volume.
准确的说, 我们的存储卷是属于Pod的而不是容器. 我们说过, 每一个Pod都会启动一个超级小的容器, 叫做pause
或者叫基础架构容器. 我们所有的容器都使用的pause
的网络命名空间.
Kubernetes支持非常多的存储卷或者存储服务, 我们可以使用explain
来看看支持的情况, 或者去官网看文档啦哈哈哈.
在Kubernetes的存储中, 重头戏应该就是PV和PVC了, 这两个玩意的全称就是PersistentVolume
和PersistentVolumeClaim
. 一个大概的流程就是, 我们负责存储的工程师来创建和部署存储服务(NFS, Ceph…), 接着由负责管理Kubernetes集群的工程师来提供PV, PV会成为存储系统对Kubernetes的存储抽象层, 接着由用户来声明PVC向PV发起获取存储空间的请求.
另外, 我们还可以声明一个Kubernetes的资源类型, 叫StorageClass
, 来划分不同级别的PV从而根据不同的服务需求指标来提供给用户. 这样我们的PVC就可以不针对某一个特定的PV来申请资源而可以通过对StorageClass
来发请求. 存储系统必须要提供相应的RESTful API.
StatefulSet控制器
StatefulSet是针对我们有状态的应用设计的, 不同于无状态应用我们可以随意的重建和重启. 那么到底什么样的应用属于StatefulSet嘞?
- 稳定且唯一的网络标识符
- 稳定且持久的存储
- 有序平滑的部署和扩展
- 有序平滑的删除和终止
- 有序的滚动更新
我们着重来说一下他的滚动更新吧, 默认使用的更新方法是分区更新, 我们可以从文档中看到:
1 | [root@kbs-demo ~]# kubectl explain sts.spec.updateStrategy |
那么分区更新是啥嘞? 我们会定义一个分区分界, 例如N, 那么当所有标识>=N的Pod都会被更新. 假设我们现在一共有5个Pod, 我定义成N=4, 那么从Pod-0开始, 第一个满足条件(也仅有这个)的就是Pod-4, 当我们发布了此更新之后, 如果没有问题, 我们就将N改成0即可. (金丝雀发布)
认证和serviceaccount
我们之前了解过, Kubernetes使用api-server
来作为整体的管理控制入口, 还可以通过Ingress暴露出来的服务端口来访问. 但是显然我们不能允许来路不明的请求来访问管理接口, 因此我们就需要进行安全认证. Kubernetes使用了基于Role的模型.
任何客户端访问, 都需要3步, 即:
- 认证
- 授权检查
- 准入控制
对于认证而言, 最常用的两种就是基于HTTP和HTTPS, 通过交换token和交换证书(双向), 来进行客户端身份的认证. 而对于授权, 同样也有多种认证方式, 但目前最常用的就是RBAC了, 也就是基于角色的访问控制.