Kubernetes
Kubernetes(常简称为K8s)是用于自动部署、扩展和管理容器化(containerized)应用程序的开源系统
架构
master:用于控制集群
- API服务器:外部访问入口
- Scheduler:调度应用(为应用分配工作节点)
- Controller Manager:执行集群级别的功能
- etcd:存储集群配置的分布式数据存储
工作节点:运行用户部署应用的节点
- 容器运行时:Docker 或者其他容器
- Kubelet:与API服务器通信 管理当前节点的容器
- kube-proxy:负责组件之间的负载均衡
分布式
- Kubenetes系统组件间只能通过API服务器通信
- 为了保证高可用性, master的每个组件可以有多个实例
etcd
只有API服务器才能直接与etcd通信
数据在etcd中存储的是一个层次级目录结构 末端节点存储的json数据
集群一致性保证:raft算法
API 服务器
- 认证授权
- 通知客户端资源变更
安全防护
- pod 使用 service accounts机制进行认证
kubectl get sa # 获取服务账户
kubectl create serviceaccount foo # 创建
- 使用sa:
spec:
serviceAccountName: foo
RBAC控制:使用插件
调度器
利用 API 服务器的监听机制等待新创建的 pod, 然后给每个新的、 没有节点集的 pod 分配节点
调度过程是很复杂的:
- 选择可用节点
- 选择最佳节点
- 高级调度
- 如何保证节点副本分布尽可能均匀
控制管理器
确保系统真实状态朝 API 服务器定义的期望的状态收敛
- rc rs控制器 deployment控制器...
Kubelet
- 在 API 服务器中创建Node 资源, 等待pod分配给它并启动pod
- 向API服务器提供监控
- 当pod从 API服务器删除, kubelet也会删除pod
kube-proxy
确保用户可以访问后端的pod
两种模式:
控制器协作
pod 到底是什么
网络
相同节点的pod通信:
不同节点的pod通信:
只有当所有节点连接到相同网关的时候 上述方案才有效
服务的实现
服务暴露的外部ip与端口通过每个节点上的kube-proxy实现
暴露的这个ip是虚拟的 主要是用来做映射用的 当kube-proxy接收到这个ip的请求 就会查找映射 转发请求
高可用集群
应用高可用:
- 水平扩展
- 主从架构
master高可用:
- etcd自身会进行数据同步
- API 服务器是无状态的
- 控制器与调度器会进行主从选举 只有leader才会进行调度控制工作
优点
- 简化部署
- 充分利用硬件
- 健康检查 自修复
- 自动扩容
在K8S中运行应用
根据描述信息生成对应的pod 在pod中运行容器
K8S会保证集群中的容器数量实例 在容器死亡时 会启动新容器替补
K8S 在运行时可根据需求动态调整副本数量
通过kube-proxy能进行服务连接动态切换
本地运行K8S
- 安装minikube
- 安装kubectl
minikube start \
--image-mirror-country=cn \
--registry-mirror='https://t9ab0rkd.mirror.aliyuncs.com' \
--image-repository='registry.cn-hangzhou.aliyuncs.com/google_containers'
部署第一个应用
kubectl run kubia --image=luksa/kubia --port=8080 # 创建容器运行
kubectl get pods # 获取pod
kubectl get rc
kubectl port-forward kubia 8080:8080 # 开启端口转发
kubectl get pods -o wide # 查看应用在哪个节点
kubectl scale rc kubia --replicas=3 # 水平扩容
逻辑架构:
- RC用来确保始终有pod运行
- 使用http服务来完成外部请求到pod的映射
pod
一组紧密相关的容器 独立的逻辑机器
一 个 pod 中的所有容器都在相同的 network 和 UTS 命名空间下运行
每个 pod 都有自己的 IP 地址, 并且可以通过这个专门的网络实现 pod 之间互相访问
pod的使用:
- 倾向于单个pod单个容器
使用yml创建pod
apiVersion: v1
kind: Pod
metadata:
name: kubia-manual
labels:
env: test # 指定一个标签
spec:
nodeSelector: # 选择特定标签的节点
super: "true"
containers:
- image: luksa/kubia
name: kubia
ports:
- containerPort: 8080
protocol: TCP
kubectl create -f kubia-manual.yaml
kubectl logs kubia-manual # 查看日志
标签
kubectl get po --show-labels
kubectl label po kubia-manual createtion_method=manual # 修改标签
kubectl label node minikube super=true
kubectl get po -l createtion_method=manual # 根据标签筛选
注解
注解也是键值对
kubectl annotate pod kubia-manual wang.ismy/name="cxk"
命名空间
命名空间简单为对象划分了一个作用域
kubectl get ns
kubectl get po -n kube-system # 获取命名空间下的pod
kubectl create namespace custom-namespace # 创建命名空间
kubectl create -f kubia-manual.yaml -n custom-namespace # 指定命名空间
停止与移除
kubectl delete po kubia-manual # 根据名字删除
副本机制
k8s 会保证 pod 以及 容器的健康运行
存活探针
当存活探针探测失败 容器就会重启
- 创建
apiVersion: v1
kind: Pod
metadata:
name: kubia-liveness
spec:
containers:
- image: luksa/kubia-unhealthy
name: kubia
livenessProbe: # 存活探针
httpGet: # 返回2xx 或者 3xx就代表活着
path: /
port: 8080
ReplicationController
创建和管理一个pod的多个副本
- 创建
apiVersion: v1
kind: ReplicationController
metadata:
name: kubia
spec:
replicas: 3
selector:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
ports:
- containerPort: 8080
控制器通 过 创建 一 个新的替代pod来响应pod的删除操作
通过更改标签的方式来实现rc与pod的关联
- 扩容
kubectl scale rc kubia --replicas=10
- 删除
kubectl delete rc kubia
ReplicaSet
ReplicaSet 会 替代 rc
rs 的pod 选择器的表达能力更强
- 创建
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia
DaemonSet
由DaemonSet 创建的pod 会绕过调度程序 会在所有集群节点上运行(或者也可以通过指定nodeSelector
在其他节点运行)
- 创建
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: ssd-monitor
spec:
selector:
matchLabels:
app: ssd-monitor
template:
metadata:
labels:
app: ssd-monitor
spec:
nodeSelector:
disk: ssd
containers:
- name: main
image: luksa/ssd-monitor
Job
允许运行 一 种 pod, 该 pod 在内部进程成功结束时, 不重启容器。
- 创建
apiVersion: batch/v1
kind: Job
metadata:
name: batch-job
spec:
completions: 5 # 运行pod数
parallelism: 2 # 并行运行数
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
CronJob
- 创建
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: cron-job
spec:
schedule: "0,15,30,45 * * * *"
jobTemplate:
spec:
template:
metadata:
labels:
app: batch-job
spec:
restartPolicy: OnFailure
containers:
- name: main
image: luksa/batch-job
服务
是一种为一组功能相同的 pod 提供单一不变的接入点的资源
- 创建
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
服务间的发现
- 通过环境变量
kubectl exec kubia-9knkg -- env
- 通过DNS
域名:kubia.default.svc.cluster.local
如果在同一命名空间下 直接使用 kubia即可
Endpoint
暴露一个服务的 IP 地址和端口的列表
kubectl get endpoints kubia
暴露服务给外部
- NodePort:每个集群节点都会在节点上打开一个端口 将在该端口上接收到的流量重定向到基础服务
apiVersion: v1
kind: Service
metadata:
name: kubia-nodeport
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30123
selector:
app: kubia
通过nodeip:30123 访问
- 负载均衡器将流量重定向到跨所有节点的节点端口。客户端通过负载均衡器的 IP 连接到服务
apiVersion: v1
kind: Service
metadata:
name: kubia-loadbalancer
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: kubia
通过externalip:一个随机端口访问
- Ingress 只需要 一 个公网 IP 就能为许多服务提供访问
启用:
minikube addons enable ingress
就绪探针
- 创建
# kubia-rc.yaml
spec:
containers:
- name: kubia
image: luksa/kubia
readinessProbe:
exec:
command:
- ls
- /var/ready # 该文件存在 容器才被认为就绪
服务故障排除
- 确保从集群内连接到服务的集群IP
- 服务的集群IP 是虚拟IP, 是无法ping通的
- 如果已经定义了就绪探针, 请确保 它返回成功;否则该pod不会成为服务的一部分
- 确认某个容器是服务的 一 部分
- 检查是否连接到服务公开的端口,而不是目标端口
- 尝试直接连接到podIP以确认pod正在接收正确端口上的 连接
- 法通过pod的IP 访问应用, 请确保应用不是仅绑定 到本地主机
卷
卷是 pod 的 一 个组成部分, 因此像容器 一 样在 pod 的规范中定义
在容器之间共享数据
emptyDir:pod被删除时 卷的内容就会丢失
- 创建
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
containers:
- image: luksa/fortune
name: html-genrator
volumeMounts:
- name: html
mountPath: /var/htdocs
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html # 使用html卷
mountPath: /usr/share/nginx/html # 挂载到容器的位置
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes: # 创建一个卷
- name: html
emptyDir: {}
gitRepo:以git仓库文件填充目录文件
apiVersion: v1
kind: Pod
metadata:
name: gitrepo-volume-pod
spec:
containers:
- image: nginx:alpine
name: web-server
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
ports:
- containerPort: 80
protocol: TCP
volumes:
- name: html
gitRepo:
repository: https://github.com/luksa/kubia-website-example.git
revision: master
directory: .
访问工作节点文件
hostPath 卷指向节点文件系统上的特定文件或目录
持久化存储
- gce持久盘
- aws弹性块存储
- nfs卷
持久卷
- 创建持久卷
apiVersion: v1
kind: PersistentVolume
metadata:
name: mongodb-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
- ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /tmp/mongodb
- 创建持久卷声明
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongodb-pvc
spec:
resources:
requests:
storage: 1Gi
accessModes:
- ReadWriteOnce
storageClassName: "" # 动态持久卷
- 容器使用持久卷
# ...
volumes:
- name: mongodb-data
persistentVolumeClaim:
claimName: mongodb-pvc
动态持久卷
- 创建StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: k8s.io/minikube-hostpath
parameters:
type: pd-ssd
声明是通过名称引用它的 方便之处主要是在不同集群之间移植
参数配置
通过定义传递参数:
- image: luksa/fortune:args
args: ["2"]
使用环境变量:
- image: luksa/fortune:env
env:
- name: INTERVAL
value: "30"
ConfigMap
类似于配置中心:
- 创建
kubectl create configmap fortunes-config --from-literal=sleep-interval=25
- 单个环境变量使用
- image: luksa/fortune:env
env:
- name: INTERVAL
valueFrom:
configMapKeyRef:
name: fortunes-config
key: sleep-interval
- 一次传递所有环境变量
- image: luksa/fortune:env
env:
envFrom:
- prefix: CONFIG_
configMapRef:
name: fortunes-config
args: ["${CONFIG_xxx}"] # 传递到命令行
- 挂载到卷
volumes:
- name: config
configMap:
name: configmap
- 更新配置
kubectl edit configmap xxx
Secret
存储与分发敏感信息
- 创建
kubectl create secret generic fortune-https --from-file=https.key
- 挂载卷使用
- image: xxx
volumeMounts:
- name: keys
mountPath: /etc/nginx/keys/
volumes:
- name: keys
secret:
secretName: fortune-https
- 环境变量使用
env:
- name: FOO_SECRET
valueFrom:
secretKeyRef:
name: fortune-https
key: name
pod 元数据访问
Downward API
通过环境变量:
env:
- name: POD IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: CONTAINER CPU REQUEST MILLICORES
valueFrom:
resourceFieldRef:
resource: requests.cpu
divisor: lm
通过卷:
volumes:
- name: downward
downwardAPI:
items:
- path: "podName"
fieldRef:
fieldPath: metadata.name
使用 K8S API 服务器
REST API:
- 启动kubectl proxy
curl http://localhost:8001/apis/batch/v1/jobs
在 pod 内部使用
客户端API
Deployment
更新应用:
- 删除旧版本pod 启动新版本pod
- 会造成短暂的服务不可用
- 启动新版本pod 删除旧版本pod
使用rc进行滚动升级
书上通过rolling-update的方法已经过时
使用 Deployment 声明式升级
- 创建
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubia
spec:
replicas: 3
selector:
matchLabels:
app: kubia
template:
metadata:
name: kubia
labels:
app: kubia
spec:
containers:
- image: luksa/kubia:v1
name: nodejs
kubectl create -f kubia-dep-v1.yaml --record # 加上该参数会记录历史版本号
- 更新版本
kubectl set image deployment kubia nodejs=luksa/kubia:v2
- 回滚
kubectl rollout undo deployment kubia
使用 - -to-revision=xxx 回滚到特定版本
- 升级速率控制
rollingUpdate :
maxSurge: 1 # 最多允许超过的副本数
maxunavailable: 0 # 最多允许多少百分比pod不可用
使用rollout pause 暂停滚动升级 部分软件版本就不一样 金丝雀发布
minReadySeconds属性指定新创建的pod至少要成功运行多久之后 , 才能 将其视为可用
如果 一 个新的pod 运行出错, 并且在minReadySeconds时间内它的就绪探针出现了失败, 那么新版本的滚动升级将被阻止
- 使用kubectl apply升级Deployment
StatefulSet
如何复制有状态的pod?
Statefulset 保证了pod在重新调度后保留它们的标识和状态
每个pod都有专属于它的持久卷
K8S保证不会有两个相同标识和持久卷的pod同时运行
使用
- 创建持久卷
- 创建控制 Service
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None
selector:
app: kubia
ports:
- name: http
port: 80
- 创建StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: kubia
spec:
serviceName: kubia
replicas: 2
selector:
matchLabels:
app: kubia # has to match .spec.template.metadata.labels
template:
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
- 使用一个 Service 来访问 Pod
apiVersion: v1
kind: Service
metadata:
name: kubia-public
spec:
selector:
app: kubia
ports:
- port: 80
targetPort: 8080
发现伙伴节点
- 容器内部通过DNS SRV 记录
安全
pod 使用宿主节点的Linux命名空间
- 使用宿主节点的网络命名空间
spec:
hostNetwork: true
- 使用宿主节点的端口而不使用宿主节点的网络命名空间
如果使用hostport 一个节点只能有一个相同的pod
- 使用宿主的PID与IPC空间
spec:
hostPID: true
hostIPC: true
开启后 相同节点的pod的进程之间就是可见的 可通信的
安全上下文
spec:
securityContext:
# ... pod 级别的
containers:
securityContext:
runAsUser: 405 # 以指定用户运行
runAsNonRoot: true # 禁止以root运行
privileged: true # 在特权模式下允许
capabilities:
add:
- SYS_TIME # 开放硬件时间修改权限
drop:
- CHOWN # 禁用文件所有者修改权限
readOnlyRootFilesystem: true # 禁止在根目录写文件
pod 网络隔离
- 网络策略
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-netpolicy
spec:
podSelector:
matchLabels:
app: database # 对该标签的pod生效
ingress: # 只允许来自匹配下面标签的pod请求
- from:
- podSelector:
matchLabels:
app: webserver
ports:
- port: 5432
计算资源管理
申请资源
spec:
containers:
- image: busybox
command: ["dd", "if=/dev/zero", "of=/dev/null"]
name: main
resources:
requests:
cpu: 200m # 申请200毫核 也就说20%CPU
memory: 10Mi # 申请10M内存
添加了requests对调度的影响:
通过设置资源requests我们指定了pod对资源需求的最小值。
调度器不关心资源的实际使用了 而是关心各pod所定义的requests资源量
限制资源
resources:
limits:
cpu: 1 # 允许最大使用1核
memory: 20Mi # 内存允许最大 20M
超过limits的情况:
- cpu:进程分配到的CPU不会超过指定的
- 内存:如果内存超过limit 则容器会被杀掉
QoS 等级
通过定义优先级决定资源不足时杀谁
- BestEffort 优先级最低
- 没有设置requess和limits都属于这个级别
- Guaranteed 优先级最高
- cpu和内存都要设置requests 和 limits
- 所有容器都要设置资源量
- requests 与 limits必须相等
- Burstable 其他的pod都属于这个等级
限制命名空间中的pod
- LimitRange插件
- ResourceQuota
监控 pod
- Heapster
自动伸缩与集群
- 基于CPU使用率的自动伸缩
kubectl autoscale deployment kubia --cpu-percent=30 --min=1 --max=5
- 纵向扩容
自动修改CPU与内存大小
集群节点扩容
新节点启动后,其上运行的Kubelet会联系API服务器,创建 一 个Node资源以注册该节点
当一 个节点被选中下线,它首先会被标记为不可调度, 随后运行其上的pod 将被疏散至其他节点
高级调度
污点和容忍度
限制哪些pod可以被调度到某 一 个节点
kubectl describe node minikube | grep Taints # 查看节点污点
NoSchedule 表示如果 pod 没有容忍这些污点, pod 则不能被调度到包含这些污点的节点上
PreferNoSchedule 是 NoSchedule 的 一 个宽松的版本, 表示尽量阻止pod 被调度到这个节点上, 但是如果没有其他节点可以调度, pod 依然会被调度到这个节点上
NoExecute会影响正在节点上运行着的 pod 。 如果在 一 个节点上添加了 NoExecute 污点, 那些在该节点上运行着的pod, 如果没有容忍这个 NoExecute 污点, 将会从这个节点去除
添加污点
kubectl taint node minikube node-type=production:NoSchedule
- pod添加容忍度
spec:
replicas: 5
template:
spec:
...
tolerations:
- key: node-type
operator: Equal
value: production
effect: NoSchedule
节点亲缘性
这种机制允许你通知 Kubemetes将 pod 只调度到某个几点子集上面
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"
最佳实践
pod 的生命周期
- 应用必须意识到会被杀死或者重新调度
- ip与主机名会发生变化
- 使用卷解决数据写入问题
- 不断重启的pod不会被重新调度
- 固定顺序启动pod
- 使用init容器
- 应用要处理好其他依赖没有准备好的情况
- 生命周期钩子
- postStart
- preStop
- pod的关闭
客户端请求处理
- pod启动时避免客户端连接断开
- 使用一个就绪探针来探测pod是否准备好接受请求了
- pod关闭时避免请求断开
- 停止接受新连接
- 等待所有请求完成
- 关闭应用
让应用方便运行与管理
- 可管理的容器镜像
- 镜像太大难以传输 镜像太小会缺失很多工具
- 合理给镜像打标签
- 不要使用latest 使用具体版本号
- 使用多维度的标签
- 使用注解描述额外信息
- 使用/dev/termination-log 写入失败信息
- 日志
- 将日志打印到标准输出方便查看
- 集中式日志系统
应用扩展
CRD对象
- 创建
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: websites.extensions.example.com
spec:
scope: Namespaced
group: extensions.example.com
version: v1
names:
kind: Website
singular: website
plural: websites
- 创建CRD实例
apiVersion: extensions.example.com/v1
kind: Website
metadata:
name: kubia
spec:
gitRepo: https://github.com/luksa/kubia-website-example.git
服务目录
服务目录就是列出所有服务的目录。 用户可以浏览目录并自行设置目录中列出的服务实例