容器化入门(五)--k8s进阶
本文内容主要来源于极客时间《Kubernetes 入门实战课》,夹杂了自己在学习过程中的思考和相关记录
1.为什么需要Deployment:
我们除了一次性的这种离线业务,还有很多是在线业务,要求实时在线。最简单的Pod具备监控内部容器状态、异常时自动重启容器的能力,但是在Pod本身被误删、Pod所在节点宕机离线等场景下,还是无法保证可靠性。 在线应用还应当具备多实例、平滑扩容、版本升级回滚等特性,Pod没有这些能力
解决这些问题办法也很简单,参考k8s惯用的“单一职责”和“对象组合”的风格,我们要再创建一个新的对象类型,它来管理Pod,并且它自己具备可靠性、升级回滚、平滑扩容等能力。
2.Deployment的YAML参考:还是可以使用之前的方法获取一份YAML模板(deploy是Deployment的简写)
export out="--dry-run=client -o yaml"
kubectl create deploy ngx-dep --image=nginx:alpine $out
YAML模板如下,可以发现,它里面是嵌套了Pod的定义(参考spec.template
)
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: ngx-dep
name: ngx-dep
spec:
replicas: 1
selector:
matchLabels:
app: ngx-dep
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ngx-dep
spec:
containers:
- image: nginx:alpine
name: nginx
resources: {}
status: {}
3.Deployment的关键属性解析:
replicas
:副本数量,指明要运行多少个Pod实例,k8s会持续监视Pod运行状态,如果因为各种意外导致Pod数量不满足目标状态,它就会通过apiserer、schedule等组件选择新的节点、创建新的Pod直至数量与期望状态一致replicas
:选择器、筛选出要被Deployment管理的Pod对象,下属字段matchLabels
定义了要被管理的Pod应当携带的label,它必须和template
中定义的Pod的labels对应上,否则Deployment就找不到要控制的Pod对象,apiserver也会在创建的时候进行校验。这里这种贴标签的方式其实体现的是Deployment和Pod的这种松耦合关系,方便其他的对象再去管理这些Pod,也方便这些Pod有需要时可以脱离原Deployment的管理(灰度发布等场景下,老版本的Pod逐步下线) 下图展示了Deployment对象定义中的关键信息:
4.Deployment的相关操作
根据yaml创建对象:
kubectl apply -f ngx-dep.yaml
查询状态:
kubectl get deployment
kubectl get deploy
其中:READY指的是运行的POD数量;UP-TO-DATE指的是已经更新到最新状态的POD数量;AVAILABLE更近一步、表示状态健康能对外提供服务的POD数量;AGE代表Deployment创建到现在所运行的时间
查询被管理的Pod:
kubectl get pod
模拟Pod被异常删除的场景,发现新Pod被自动拉起
应用伸缩:可以动态调整副本数 kubectl scale --replicas=5 deploy ngx-dep
注意,scale
只能临时调整,如果系统重启Deployment重新根据yaml拉起后副本数还是yaml中描述的,所以如果想要长期生效,最好还是改yaml、再通过applay命令生效
label字段的进阶用法:可以在 kubeclt get
、kubectl delete
等命令中通过-l参数根据标签来筛选对象,支持==
,!=
,in
,notin
等表达式
kubectl get pod -l 'app in (nginx)'
kubectl get pod -l 'app in (nginx,ngx-dep,ngx)'
5.为什么需要有Daemonset?Deployment解决了应用可靠性、平滑扩容等问题,但是它不关注Pod到底运行在哪些节点上,只保证Pod数量够;有一类业务不是完全独立与系统运行的,需要与主机存在绑定关系才有意义,需要保证每个节点一个,例如网络应用、节点监控应用、节点安全应用等。Daemonset的目标是每个节点运行且只运行一个Pod
6.Daemonset的YAML模板:当前kubectl不支持通过kubectl create
创建Daemonset对象,所以没法再用之前的方法创建模板。
可以在官网下载一份YAML示例(另外,其实Daemonset和Deployemnt的YAML高度相似,可以从Deployemnt简单修改就可以得到Daemonset的YAML)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: redis-ds
labels:
app: redis-ds
spec:
selector:
matchLabels:
name: redis-ds
template:
metadata:
labels:
name: redis-ds
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
跟Deployemnt的YAML相比,只是少了一个repelicas字段以及一些对象基础信息变了而已,其他完全一致
7.污点和容忍度:
用途:为了方便在节点上调度Pod,k8s引入了污点的概念,它的作用是给节点贴标签。给节点打了标签后,就可以给Pod定义筛选机制、能让k8s在调度Pod时做一些控制调整,例如某个Pod因为某些原因不能运行到某节点上,这个筛选机制就是Pod的容忍度。如果Pod无法容忍某节点的污点,则该Pod无法调度到该节点上
查看节点污点Taints:
kubectl describe node master
减少一个污点,注意最后的减号
kubectl taint node master node-role.kubernetes.io/master:NoSchedule-
增加一个污点
kubectl taint node master node-role.kubernetes.io/master:NoSchedule-
修改Pod的容忍度,让它能容忍一些污点.要写清楚污点的名称和效果,同时写清楚如何匹配污点(Exists)。Pod默认不会容忍任何污点
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
operator: Exists
在k8s中,master一般具备一个 node-role.kubernetes.io/master
污点,为了使得ds能在mater上也能调度一个pod,可以在pod的定义中写清楚Pod的容忍度。当然,你也可以修改master节点的污点,但是这会导致其他Pod调度也会出现调整、将变更扩大了
8.使用DaemonSet:
根据yaml创建DaemonSet
kubectl apply -f redis-ds.yaml
查询状态:
kubectl get ds
9.静态Pod:DaemonSet 是在 Kubernetes 里运行节点专属 Pod 最常用的方式,但它不是唯一的方式,Kubernetes 还支持另外一种叫“静态 Pod”的应用部署手段,直接在节点/etc/kubernetes/manifests
目录下定义对应的Pod即可,静态Pod非常特殊、不受k8s管理:
10.Service:Service用来解决服务发现的问题,所谓服务发现就是如果让系统内部动态稳定(例如,Pod的个数稳定、但是Pod因为某些原因发生了漂移、它的IP随之变化)的服务对外提供稳定的服务,针对这种场景典型的解决思路就是负载均衡,在前端和后端之间加入一个中间层,屏蔽后端的变化,为前端提供一个稳定的服务。Service这种api资源就是k8s的负载均衡,用来解决服务发现这一关键问题。k8s中的Service可以由iptables技术实现,但是也可以由userspace(性能更差)和ipvs(性能更好)技术实现
11.Service的YAML描述:
还是可以使用之前的技巧,使用 --dry-run=client -o yaml
生成Service对象的YAML模板,只不过要使用kubectl expose
指令,同时要注意的是,kubectl expose
指令支持给多种对象创建服务,Pod、Deployment、DaemonSet都可以。同时要通过--port
和--target-port
分别指定映射端口和容器端口。
export out="--dry-run=client -o yaml"
kubectl expose deploy ngx-dep --port=80 --target-port=80 $out
可以发现Service的YAML非常简单。在spec中只有两个关键字段, selector
用于选择要代理的Pod,ports
用于表示Service对外体现的端口、内部Pod的端口以及使用的协议
apiVersion: v1
kind: Service
metadata:
name: ngx-svc
spec:
selector:
app: ngx-dep
ports:
- port: 80
targetPort: 80
protocol: TCP
12.在k8s中使用Service:
改造下ngx-dep,使得接口请求时能返回主机名等信息。我们将nginx的配置文件使用ConfigMap来管理:
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location / {
default_type text/plain;
return 200
'srv : $server_addr:$server_port\nhost: $hostname\nuri : $request_method $host $request_uri\ndate: $time_iso8601\n';
}
}
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
replicas: 2
selector:
matchLabels:
app: ngx-dep
template:
metadata:
labels:
app: ngx-dep
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
部署ConfigMap、Deployment以及Service对象:
kubectl apply -f ngx-conf-cm.yaml
kubectl apply -f ngx-dep-ex.yaml
kubectl apply -f ngx-svc.yaml
可以查询他们的状态:
可以查看svc代理了哪些后台的ip地址,并确认跟deployment创建的Pod ip地址一致
kubectl describe svc ngx-dep
测试svc的负载均衡效果:因为Service、Pod的IP地址都是k8s集群的内部地址,我们需要 kube exec
进入到Pod内部或者ssh登录集群节点,用curl来访问svc的ip:
我们可以删除一个Pod,看看deploy创建出新的Pod后,svc对象代理的后端IP会不会刷新。经过确认,svc自动刷新了后端代理的Pod的IP地址,但是svc对外的IP保持不变,这样就实现了对外提供稳定的服务。 需要注意,Service的IP只是一个虚拟IP, 只用来转发流量,所以是无法ping通的
13.使用域名的方式来使用Service:
为什么需要:Service对象的IP地址是静态的,保持稳定,可以使用k8s的DNS插件,为Service创建容易使用的域名 名字空间namespce:k8s中的namespace用于实现对API对象的隔离和分组。可以使用 kubectl get ns
获取。k8s有一个默认的命名空间default,如果没有显示指定,API对象会在这个namespace中。为了避免冲突,名字空间是域名的一部分Service对象的域名是 对象.名字空间.svc.cluster.local
.但很多时候,可以简写为对象.名字空间
甚至对象
Kubernetes 也为每个 Pod 分配了域名,形式是“IP 地址. 名字空间.pod.cluster.local”,但需要把 IP 地址里的 . 改成 - 。比如地址 10.10.1.87,它对应的域名就是 10-10-1-87.default.pod
14.如何让Service对外暴露服务
前面看到我们使用Service还不能真正提供服务、它最终对外的IP地址是一个集群内部的IP地址, 这是因为我们使用的负载均衡类型是“ClusterIP”,这种类型的负载均衡的IP只能在集群内部访问 Service实际总共支持四种类型:“ClusterIP”是默认的一种,其他三种分别是“ExternalName”、“LoadBalancer”和“NodePort”,“ExternalName”、“LoadBalancer”主要由云服务商提供 如果我们在 kubectl expose
的时候加上--type=NodePort
或者在YAML里添加字段type:NodePort
后,Service除了对后端Pod做负载均衡之外会在每个节点上创建出一个独立的端口,用这个端口可以向外部提供服务
apiVersion: v1
...
spec:
...
type: NodePort
我们看一下效果。
直接访问集群节点的外部IP地址对应端口,即可访问服务
NodePort类型的Service示意图如下:NodePort类型Serive的缺点:
端口数量有限,默认只在30000-32767 每个节点都开端口,然后使用kube-proxy路由到真正的Service,对于很多计算节点大的集群来说带来了很多网络通信成本 它要求向外界暴露节点的IP地址,很多时候因为安全原因还要再搭一个反向代理,增加了方案的复杂度
15.使用VirtualBox创建的虚拟机安装k8s过程中遇到的几个坑:
在本节遇到了几个非常奇怪的问题,折腾了比较久的时间,在此总结下:
问题1:kubectl exec
时报错error: unable to upgrade connection: pod does not exist
:
问题原因:虚拟机的网络配置了两张网卡(一张是HostOnly、一张是NAT转化),针对第二张网卡,所有虚拟机的地址都是10.0.3.15/24。k8s管理节点时节点的IP都是10.0.3.15/24,可以从 kubectl get nodes -o wide
看到两个节点IP都是10.0.3.15/24,这导致网络存在一点问题解决办法:在 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
配置文件中通过Environment="KUBELET_EXTRA_ARGS=--node-ip=<worker IP address>"
显式指明节点IP地址,然后重启kubeletsudo systemctl stop kubelet; sudo systemctl daemon-reload; sudo systemctl start kubelet
问题2:kubectl exec
进入节点使用域名访问服务时,域名无法解析。curl ngx-svc curl: (6) Could not resolve host: ngx-svc
。根据网上定位指导,发现Pod内发送的dns请求根本无法到达dns pod
问题原因:针对多网卡场景,使用flannel时需要指明网卡,否则可能导致一些诡异的网络问题 解决办法:在对应的kube-flannel.yml通过参数指定网卡名。然后使用 kubectl apply -f
重新应用
16.为什么需要Ingress:
Service本质是一个四层负载均衡,仅能根据IP地址和端口做一些简单的组合和判断,在TCP/IP上转发流量;而我们的应用是跑在七层的HTTP/HTTPS上的, 想要更多的高级路由条件,如主机名、uri、请求头等 Service还有一个缺点,它比较适合集群内部的服务。如果想要把服务暴露到集群外只能用NodePort或者LoadBalancer,缺乏足够的灵活。这就导致一种很无奈的局面:我们的服务空有一身本领,却没有合适的机会大展拳脚 需要引入一个七层的负载均衡对象,同时这个对象还要承担流量总入口的角色,统管南北向的所有流量,让外部客户安全、顺畅、便捷的访问内部服务。Ingress应用而生,Ingress单词的本意是入口
17.什么是Ingress Controller:
对比Service来理解:Service本身只是一些iptables规则,真正配置这些规则、应用这些规则的是kube-proxy。Ingress本身也只是一份HTTP路由规则,真正要把这些规则实施运行起来需要另外一个组件,这就是Ingress COntroller。它能够读取、应用Ingress规则,处理和调度流量 Ingress Controller需要做的功能太复杂,并且与上层的业务强相关,所以k8s并没有提供标准的内置实现,把Ingress Controller的实现交给了社区。其中使用最广泛的是Nginx公司开发实现的Ingress Controller
18.什么是Ingress Class:
随着Ingress的大量应用,发现了很多问题,例如Ingress规则太多导致Ingress Controller负载重、项目想引入多个不同的Ingress Controller k8s又提出Ingress Class的概念,让它插在Ingress 和 Ingress Controller 中间,作为流量规则和控制器的协调人,解除了 Ingress 和 Ingress Controller 的强绑定关系。Kubernetes 用户可以转向管理 Ingress Class,用它来定义不同的业务逻辑分组,简化 Ingress 规则的复杂度
19.Ingress和Ingress Class的YAML描述文件:
通过 kubectl api-resources
获取到他们的基本信息,包括APIVERSION、KINDE等。
$ kubectl api-resources | grep ingress
ingressclasses networking.k8s.io/v1 false IngressClass
ingresses ing networking.k8s.io/v1 true Ingress
建议还是通过 kubectl create
来创建模板文件。它需要2个额外的参数:--class
指定从属的Ingress Class对象;--rule
指定路由规则,基本形式是URI=Service
即特定的HTTP路径转发到Service对象。
export out="--dry-run=client -o yaml"
kubectl create ing ngx-ing --rule="ngx.test/=ngx-svc:80" --class=ngx-ink $out
获取到的yaml文件如下:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ngx-ing
spec:
ingressClassName: ngx-ink
rules:
- host: ngx.text
http:
paths:
- backend:
service:
name: ngx-svc
port:
number: 80
path: /
pathType: Exact
其中要重点关注的2个关键字段:ingressClassName
:指明使用哪种Ingress Class;rule
:路由规则,规则较为复杂,支持host、http path以及更近一步的路径匹配(可以是精确匹配Exact或者前缀匹配Prefix)
Ingress Class本身无实际功能,只是联系Ingress和Ingress Controller。它的定义非常简单,在 spec
中指明controller即可。这里我们使用Nginx开发的nginx.org/ingress-controller
.
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: ngx-ink
spec:
controller: nginx.org/ingress-controller
20.在k8s中使用Ingress/Ingress Class:
准备对应的yaml, 可以将Ingress和Ingress Class 两个对象的定义放到一个描述文件中,使用 ---
隔开即可
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ngx-ing
spec:
ingressClassName: ngx-ink
rules:
- host: ngx.text
http:
paths:
- backend:
service:
name: ngx-svc
port:
number: 80
path: /
pathType: Exact
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: ngx-ink
spec:
controller: nginx.org/ingress-controller
创建对象 kubectl apply -f
。
使用 kubectl describe
查看对应的信息
21.在k8s中使用Ingress Controller:
安装Nginx Ingress Controller的基本工作,创建一个独立的命名空间、设置对应的账号和权限、创建一个cm和secret用来配置HTTP/HTTPS服务等。参考
https://github.com/chronolaw/k8s_study/tree/master/ingress
进行安装部署Ingress Controller。我们选择Deployment方式部署,Nginx提供了参考示例。我们只需要做简单修改:改名字、改更精简的镜像、加上args参数
-ingress-class=ngx-ink
用于管理我们创建的class
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-kic-dep
namespace: nginx-ingress
spec:
replicas: 1
selector:
matchLabels:
app: ngx-kic-dep
template:
metadata:
labels:
app: ngx-kic-dep
#annotations:
#prometheus.io/scrape: "true"
#prometheus.io/port: "9113"
#prometheus.io/scheme: http
spec:
serviceAccountName: nginx-ingress
containers:
#- image: nginx/nginx-ingress:2.2.0
- image: nginx/nginx-ingress:2.2-alpine
imagePullPolicy: IfNotPresent
name: nginx-ingress
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
- name: readiness-port
containerPort: 8081
- name: prometheus
containerPort: 9113
readinessProbe:
httpGet:
path: /nginx-ready
port: readiness-port
periodSeconds: 1
securityContext:
allowPrivilegeEscalation: true
runAsUser: 101 #nginx
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
args:
- -ingress-class=ngx-ink
- -health-status
- -ready-status
- -nginx-status
- -nginx-configmaps=$(POD_NAMESPACE)/nginx-config
- -default-server-tls-secret=$(POD_NAMESPACE)/default-server-secret
部署 Deployment并查询状态
kubectl apply -f ngx-kic-dep.yaml
kubectl get deploy -n nginx-ingress
kubectl get pod -n nginx-ingress
从整个过程中, 梳理API对象之间的关系
还差最后一步,Ingress Controller本质是一个Pod,要对外提供服务,第一种方法是直接使用端口转发
kubectl port-forward -n nginx-ingress ngx-kic-dep-8859b7b86-llqmm 8080:80 &
第二种办法, 创建一个NodePort类型的Service
apiVersion: v1
kind: Service
metadata:
name: ngx-kic-svc
namespace: nginx-ingress
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: ngx-kic-dep
type: NodePort