Kubernetes
Kubernetes 发音 /Koo-ber-nay-tace/
或者 /Koo-ber-netties/
。
解决的问题
微服务部署和配置困难。
- 简化应用程序的部署。开发者无须知道背后有多少台机器需要部署,也无需知道自己的 APP 运行在哪几台机器上。
- 对于资源的更为高效的利用。K8S 可以在任意时刻将 APP 迁移到其他 worker 节点上,以便更好的利用资源。
- 健康检查。node 挂掉后,自动将 APP 调度到其他节点上。
- 自动伸缩。K8S 可以自己关注资源的利用率,动态调整 APP 的实例数量。
概念解释
VM 和容器
APP 运行在 VM 中
APP 运行在容器中
Pod
一个 Pod 是一组互相协作的容器构成的,一般情况下,一个 Pod 仅包含一个容器:
容器设计初衷就是为了一个容器仅运行一个进程的理念去运行程序,如果运行多个,那么它们的状态、生命周期、日志等都难以管理。所以,我们需要站在更高层级来去管理、组织、单元化分布在多个容器中的多个进程,这就是引入 Pod 的意义。Pod 来去给一组容器中的各个进程提供一种假象,让这些进程认为各自都运行在一个容器里似的,同时在需要隔离的时候,也能提供隔离的能力。
运行在相同 Pod 中的各个容器,它们的 IP 和端口资源是共享的,所以不要让它们引起端口冲突。Pod 是轻量级的,应该将一个应用程序分为多个 Pod,而不是在一个 Pod 中塞入所有东西。仅当这些进程是互相协作、必须同时运行、必须同时扩展、对外是一个整体服务等这种情况下,可以考虑将多个容器放在一个 Pod 中进行管理。
使用 cubectl create
来创建一个 Pod:
$ kubectl create -f kubia-manual.yaml
创建好之后,可以通过命令 kubectl get po kubia-manual -o yaml
来查看此 Pod 的完整 Yaml 配置。可以使用命令 kebectl get pods
可以查看当前的 Pod 列表,以及它们的状态。
通过 Docker 查看应用日志:
$ docker logs <容器 ID>
通过 K8S 查看 Pod 日志:
$ kubectl logs kubia-manual
$ kubectl logs kubia-manual -c <具体的容器名>
如果不想通过 Service 的方式访问 Pod,而是要简单调试、或其他原因想访问 Pod,那么 K8S 提供了端口转发功能,如下流量将本地的 8888 端口流量转到 Pod 的 8080 端口:
$ kubectl port-forward kubia-manual 8888:8080
为了更方便的管理、组织 Pod,还可以给 Pod 打标签。可以在创建 Pod 的时候打标签:
metadata:
labels:
key1=value1
key2=value2
这样可以给 kubectl get
命令使用 --show-labels
来显示所有 Pod 的标签。
可以给 Pod 标注元信息,放置更多的描述信息:
metadata:
annotations:
kubernetes.io/created-by:
{"kind":"SerializedReference", "apiVersion":"v1","reference":{"kind":"ReplicationController", "namespace":"default", ...
一个 Pod 可以拥有多个标签,所以选中此标签和其他标签的时候,里面选中的资源可能有重叠。而 K8S 也可以通过命名空间的方式来组织 Pod,来提供一种管理不同 Pod 的作用域,可以将这些 Pod 划分为不重叠的、独立的组,例如生产、开发、QA环境组等。
通过命令 kubectl get ns
查看所有命名空间,kubectl
命令默认情况下展示的都是 default
命名空间的资源,通过如下命令展示其他命名空间的资源:
$ kubectl get po --namespace kube-system
创建 Pod 的时候指定命名空间:
kind: Namespace
删除 Pod 的命令 kubectl delete po kubia-gpu
会发送 SIGTERM
信号给所有的进程,然后等待最多 30 秒,否则直接发送 SIGKILL
信号。
Docker 运行 K8S
需要将它们打包制作成镜像才可以运行在 K8S 中的应用。<tag>
是可选的,如果不提供,默认应该是 latest
,其表示的含义是版本号。
$ docker run <image>:<tag>
K8S 集群
下面是 K8S 集群的概览图:
K8S 运行 APP
开发者告诉 K8S 的 master 节点,哪些 APP 必须部署在一起,每一个 APP 需要部署几个实例,K8S 就会自动按照要求将这些 APP 部署到 worker 节点上。
K8S 由 master 和 worker 节点构成,其中 K8S 管理控制台位于 master 节点上,可以通过此平台管理整个 K8S 系统;而 worker 节点用于实际运行 APP。
master 节点包含:
- K8S API Server
- Scheduler: 调度 APP,分配 worker 节点等
- Control Manager: 跟踪 worker 节点状态,处理节点失败等
- etcd: 存储集群的配置信息
worker 节点包含:
- Docker: 或者其他容器
- Kubelet: 调用 API Server,控制 Container
- K8S Service Proxy: 流量的负载均衡
在 K8S 中运行程序,必须首先将其制作成容器的镜像,然后推送到镜像仓库,然后告诉 K8S 此 APP 的描述文件。
控制器
HTTP 探针:
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe:
httpGet: # HTTP GET 探针
path: /
port: 8080
initialDelaySeconds: 15 # 15 秒之后发送第一个探针
ReplicationController 保证 Pod 总是处于运行状态,如果某个 Pod 因为某些原因宕掉了,那么 ReplicationController 将会创建一个新的 Pod 以替代旧的 Pod。
使用命令 kubectl create -f kubia-rc.yaml
创建一个 ReplicationController :
apiVersion: v1
kind: ReplicationController # 指定类型
metadata:
name: kubia
spec:
replicas: 3 # 指定需要 3 个 Pod
selector:
app: kubia # Pod Selector
template: # template 下面的这些是创建一个新的 Pod 所需要的模板
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080
ReplicationController 可以随时通过命令 kubectl edit rc kubia
修改 Pod 模板:
ReplicaSet 是用来替代 ReplicationController 的,它有更多的 Pod Selector。创建一个 ReplicaSet :
apiVersion: apps/v1beta2
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
它有更为丰富的 label 选择器:
selector:
matchExpressions:
- key: app # Pod 必须有一个 key 是 app 的 label
operator: In
values:
- kubia # label 的 value 必须位于 kubia 里面
DaemonSets 确保一个 Worker Node 只运行一个 Pod 副本,假设你运行的是一个日志采集器程序,那么可能会使用 DaemonSets
:
Service:与 Pod 沟通
K8S Service 提供了对于一组提供相同服务的 Pod 的一个固定入口。
通过 kubia-svc.yaml
文件创建 Service,port: 80
表示 Service 监听在 80 端口,targetPort: 8080
表示 Service 将要用某种路由算法转发到多个 Pod 的 8080 端口,app: kubia
表示所有具有 app = kubia
标签的 Pod 都属于这个 Service:
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
通过 kubectl get svc
命令查看默认命名空间的所有的 Service 列表,EXTERNAL-IP
表示外网 IP:
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes 10.3.240.1 <none> 443/TCP 35m
kubia-http 10.3.246.185 104.155.74.57 8080:31348/TCP 1m
会话 Session 附着,某一个 Client 的请求总是转发到某一个 Pod:
spec:
sessionAffinity: ClientIP
可以通过 ENV
环境变量,查看 Service 的地址:
$ kubectl exec kubia-3inly env
KUBIA_SERVICE_HOST=10.111.249.153
KUBIA_SERVICE_PORT=80
也可以通过 FQDN 查看 Service 地址,其中 backend-database
表示 Service 名称,default
表示命名空间,svc.cluster.local
是一个可配置的集群 Domain 的前缀:
backend-database.default.svc.cluster.local
验证是否可以联通:
$ kubectl exec -it kubia-3inly bash
[email protected]:/# curl http://kubia.default.svc.cluster.local
You’ve hit kubia-5asi2
[email protected]:/# curl http://kubia.default
You’ve hit kubia-3inly
[email protected]:/# curl http://kubia
You’ve hit kubia-8awf3
查看一个 Service 背后可以访问哪些 Pod :
$ kubectl describe svc kubia
Selector: app=kubia
Endpoints: 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080
刚才讨论的只是能让 K8S Cluster 内的各个 Pod 访问 Service ,有三种方式可以让外部的 Client (外网) 访问 Service:
- 指定 Service 的类型为
NodePort
:
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30123
selector:
app: kubia
- 指定 Service 的类型为
LoadBalancer
,这是NodePort
的扩展类型
metadata:
name: kubia-loadbalancer
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
获取 LoadBalancer 的 IP:
$ kubectl get svc kubia-loadbalancer
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubia-loadbalancer 10.111.241.153 130.211.53.173 80:32143/TCP 1m
- 创建 Ingress 资源
一个 Ingress,就可以管控多个 Service。如果使用 LoadBalancer 的话,那么每个 Service 必须配备一个 IP :
创建 Ingress :
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: kubia
spec:
rules:
- host: kubia.example.com
http:
paths:
- path: /
backend:
serviceName: kubia-nodeport
servicePort: 80
获取 Ingress IP:
$ kubectl get ingresses
配置 DNS:
$ vi /etc/hosts
192.168.99.100 kubia.example.com
使用 Ingress 访问 Pod:
ConfigMaps 和秘钥
ConfigMap: 传递参数给 K8S 的 Pod
在 Yaml 中通过 env
参数定义环境变量,这个环境变量是定义在 Container 级别,而非 Pod 级别:
kind: Pod
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL
value: "30"
name: html-generator
使用 ConfigMap 避免硬编码环境变量。
从命令行创建 ConfigMap
:
$ kubectl create configmap myconfigmap --from-literal=foo=bar --from-literal=bar=baz --from-literal=one=two
$ kubectl get configmap myconfigmap -o yaml
从文件或者目录创建 ConfigMap
:
# key 就是文件名,value 就是文件内容
$ kubectl create configmap my-config --from-file=config-file.conf
# 目录中的每个文件名都作为 key
$ kubectl create configmap my-config --from-file=/path/to/dir
使用命令 kubectl get configmap fortune-config -o yaml
查看 ConfigMap 定义:
apiVersion: v1
data:
sleep-interval: "25" # 只有一个配置项
kind: ConfigMap
metadata:
creationTimestamp: 2016-08-11T20:31:08Z
name: fortune-config # ConfigMap 的名字,通过这个名字来引用这个 Config
namespace: default
resourceVersion: "910025"
selfLink: /api/v1/namespaces/default/configmaps/fortune-config
uid: 88c4167e-6002-11e6-a50d-42010af00237
Pod 通过环境变量或者 configMap volume 来使用 ConfigMap :
ConfigMap 传递给容器,作为环境变量存在,如果 Pod 引用了不存在的 ConfigMap 那么 Pod 将拒绝启动,当然也可以将 ConfigMap
作为可选的 configMapKeyRef.optional: true
:
apiVersion: v1
kind: Pod
metadata:
name: fortune-env-from-configmap
spec:
containers:
- image: luksa/fortune:env
env:
- name: INTERVAL # 给容器定义了一个环境变量
valueFrom: # 从 ConfigMap 引用这个值
configMapKeyRef:
name: fortune-config # ConfigMap 的名字
key: sleep-interval # ConfigMap 引用的 key
ConfigMap 作为命令行参数:
apiVersion: v1
kind: Pod
metadata:
name: fortune-args-from-configmap
spec:
containers:
- image: luksa/fortune:args
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortune-config
key: sleep-interval
args: ["$(INTERVAL)"] # 命令行参数引用环境变量参数
使用 configMap volume 来将 ConfigMap 作为文件:
apiVersion: v1
kind: Pod
metadata:
name: fortune-configmap-volume
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d # 将 volume ConfigMap 挂载到这个位置
readOnly: true
volumes:
- name: config # volume 引用 ConfigMap
configMap:
name: fortune-config
查看挂载的目录下是否有文件:
$ kubectl exec fortune-configmap-volume -c web-server ls /etc/nginx/conf.d
每一个 Pod 都有一个自动附带的 secret volume:
$ kubectl get secrets
$ kubectl describe secrets
ca.crt: 1139 bytes
namespace: 7 bytes
token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
创建 Secret:
$ openssl genrsa -out https.key 2048
$ openssl req -new -x509 -key https.key -out https.cert -days 3650 -subj/CN=www.kubia-example.com
$ echo bar > foo
$ kubectl create secret generic fortune-https --from-file=https.key --from-file=https.cert --from-file=foo
Secret 的每一项以 Base64 展示,ConfigMap 都是明文展示。如下是 Secret 的 Yaml 文件展示:
$ kubectl get secret fortune-https -o yaml
apiVersion: v1
data:
foo: YmFyCg==
https.cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCekNDQ...
https.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcE...
kind: Secret
Volume
更新
滚动更新:
滚动更新策略:
spec:
strategy:
rollingUpdate:
maxSurge: 1 # 默认值是 25%
maxUnavailable: 0 # 默认值也是 25%
type: RollingUpdate
当前线上存活 Pod 是 3 个,maxSurge = 1
表示部署的时候,最多存在的 Pod 个数是 3 + 1 = 4
个,maxUnavailable = 0
表示部署的时候,至少 3 - 0 = 3
个 Pod 必须处于存活状态:
当 maxUnavailable = 1
的时候,那么至少 3 - 1 = 2
个 Pod 必须处于存活状态:
控制更新,用一小部分流量验证新的版本:
$ kubectl set image deployment kubia nodejs=luksa/kubia:v4
# 暂停更新
$ kubectl rollout pause deployment kubia
# 恢复更新
$ kubectl rollout resume deployment kubia
minReadySeconds
一个新的 Pod 起来之后,多久才可以对外宣传自己准备好接受请求了,时间不到,更新不会继续:
spec:
minReadySeconds: 10
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
readiness probe 如果失败的话,那么流量不会转发到这台 Pod 上:
K8S 机制
K8S 架构
(1)使用 etcd 存储资源
Pod、ReplicationControllers、Services、Secrets 等的 manifests 都存储在 etcd 中,唯一与 etcd 有关联的是 API Server,其他所有组件的读或者写都得通过 API Server 才能访问 etcd,在 Kubernetes 中,它所有的数据都存储在 /registry
路径下:
$ etcdctl ls /registry
(2)API Server
API Server 收到请求后是如何执行的:
API Server 如何通知 Client 资源发生了变动:
(3)kubelet
- 通过 API Server 创建 Node 资源
- 通过 API Server 监控哪些 Pods 调度给了 Node,然后告诉 Docker 从镜像启动 Container
- 持续不断汇报状态、事件、资源消耗给 API Server
(4)kube-proxy
本节部分内容来自阿里云社区《深入浅出K8S》
在 K8S 集群中,服务的实现,实际上是为每一个集群节点上,部署了一个反向代理 Sidecar。而所有对集群服务的访问,都会被节点上的反向代理转换成对服务后端容器组的访问。
K8S 集群的服务,本质上是负载均衡,即反向代理;在实际实现中,这个反向代理,并不是部署在集群某一个节点上,而是作为集群节点的边车,部署在每个节点上的。
K8S 集群节点实现服务反向代理的方法,目前主要有三种,即 userspace、iptables 以及 ipvs。
userspace 模式:作为早期实现,proxy 运行了一个进程,负责配置 iptables 路由规则,以便将连接重定向到 proxy server:
现在更好的实现模式是 iptables 模式:直接将 packets 重定向到随机选择的后端的 Pod 上,而无须额外的将它们再转发到 proxy server 上。userspace 模式,packets 必须经过 proxy server,也就是必须到 user space,而 iptables 模式则直接在 kernel space 区。两者有比较显著的性能差异。
另外一点,userspace 模式执行的是 round-robin 的方式选择 Pod,而 iptables 模式是随机选择 Pod。
K8S 网络
K8S Inter-pod 网络通过 Container Network Interface (CNI)插件建立。
Pod A 与 Pod B 之间的 IP 没有经过 NAT 转换:
一个 Node 里边的 Pod A 与 Pod B 连接在同一个网桥上:
不同 Node 里边的 Pod A 与 Pod B 需要以某种方式才能通信:
K8S 调度器
默认调度算法:
- 过滤出符合要求的能够被调度的节点。
- 对这些节点进行排序,选择最佳的一个节点,如果多个节点具有相同的分数,那么会采用 round-robin 算法来选取一个节点。
K8S 水平伸缩
每一个 node 上的 Kubelet 里面都有一个 cAdvisor ,负责收集各种指标,然后通过位于集群级别的 Heapster 进行指标聚合。
根据一个或者多个指标计算 APP 所需要的 Pods 数量:
最后 Autoscaler Controller 更新副本数量:
K8S 高可用
高可用需要运行多个 master 节点:
K8S 资源控制
Hard 约束的 Pod :
apiVersion: v1
kind: Pod
metadata:
name: requests-pod
spec:
containers:
- image: busybox
command: ["dd", "if=/dev/zero", "of=/dev/null"]
name: main
resources:
requests:
cpu: 200m # CPU 200 millicores,1/5 单核 CPU 的时间
memory: 10Mi # 申请的内存大小: 10MB
apiVersion: v1
kind: Pod
metadata:
name: limited-pod
spec:
containers:
- image: busybox
command: ["dd", "if=/dev/zero", "of=/dev/null"]
name: main
resources:
limits:
cpu: 1 # 最多允许容器使用 1 个 CPU
memory: 20Mi # 最多允许使用 20 MB
Kublet 包含了一个 Agent 称作 cAdvisor,来收集 Node 上的多个 Container 的资源消耗情况。将整个集群的整个静态数据收集起来需要运行一个额外的组件:Heapster。Heapster 作为一个正常的 Pod 而存在。
最佳实践
(1)妥善处理所有客户请求:
应用程序正在启动的时候,确保没有请求进来:
- 指定 readiness probe。如果不指定的话,Pod 就会被认为已经准备好接受请求了。
应用程序正在关闭的时候,妥善处理当前现存的客户请求:
pre-stop hook 防止 broken 链接:
lifecycle:
preStop:
exec:
command:
- sh
- -c
- "sleep 5"