当前位置:   article > 正文

【运维知识大神篇】运维界的超神器Kubernetes教程7(NodePort+名称空间+ipvs工作模式+可用探针和启动探针+静态Pod+Pod优雅终止+Pod创建删除流程+rc和svc实现应用升级)_nodeport原理

nodeport原理

本篇文章继续给大家介绍运维界的超神器Kubernetes教程,内容较多,细细品味,你将对Kubernetes有更多的理解,包括Service的NodePort类型,名称空间的增删查,切换kube-proxy工作模式为生产环境常用的ipvs,可用性检查探针readinessProbe和启动探针startProbe,静态Pod,Pod的优雅终止,Pod的创建删除流程,利用rc和svc实现应用升级。

目录

Service的NodePort类型

一、NodePort案例展示

二、NodePort原理介绍 

三、自定义svc端口

名称空间

一、查看

二、创建

三、删除

切换kube-proxy工作模式为ipvs

可用性检查探针readinessProbe

一、案例展示

启动探针startupProbe

一、案例展示

静态Pod

修改svc的NodePort类型的端口范围

Pod优雅的终止

一、案例展示

二、portstart与启动检查与初始化容器的优先级

Pod的创建、删除流程(Pod生命周期)

一、创建Pod

二、删除Pod

利用rc和svc实现应用升级​​​​​​​


Service的NodePort类型

先前文章里介绍过用Service可以对Pod进行自动发现和负载均衡,但是负载均衡的IP是10.200.x.x段的,我们的用户并不能通过这个IP访问到Pod中容器的业务,所以,我们要用到NodePort类型,这也是继hostnetwork、hostIP第三个提到的可以访问到Pod的方法,相较于前两个优点是多了负载均衡的功能。

一、NodePort案例展示

编写svc资源清单

  1. [root@Master231 svc]# cat 04-svc-NodeIP.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: myweb-nodeport
  6. spec:
  7. # 指定svc的类型为NodePort,也就是在默认的ClusterIP基础之上监听所有worker节点的端口而已。
  8. type: NodePort
  9. # 基于标签选择器关联Pod
  10. selector:
  11. apps: web
  12. # 配置端口映射
  13. ports:
  14. # 指定Service服务本身的端口号
  15. - port: 8888
  16. # 后端Pod提供服务的端口号
  17. targetPort: 80

准备启动的Pod,对svc资源清单进行测试,发现自动映射到了宿主机的32212端口

  1. [root@Master231 rc]# kubectl apply -f 02-rc-nginx.yaml
  2. configmap/nginx.conf unchanged
  3. replicationcontroller/koten-rc created
  4. [root@Master231 rc]# kubectl get pods -o wide --show-labels
  5. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
  6. koten-rc-4b7rj 1/1 Running 0 2m16s 10.100.1.143 worker232 <none> <none> apps=web
  7. koten-rc-4zz2k 1/1 Running 0 2m16s 10.100.1.145 worker232 <none> <none> apps=web
  8. koten-rc-7xmbf 1/1 Running 0 2m16s 10.100.2.65 worker233 <none> <none> apps=web
  9. koten-rc-8q5s9 1/1 Running 0 2m16s 10.100.1.142 worker232 <none> <none> apps=web
  10. koten-rc-t5klq 1/1 Running 0 2m16s 10.100.2.63 worker233 <none> <none> apps=web
  11. koten-rc-t8rl7 1/1 Running 0 2m16s 10.100.1.144 worker232 <none> <none> apps=web
  12. koten-rc-v9jk6 1/1 Running 0 2m16s 10.100.2.64 worker233 <none> <none> apps=web
  13. [root@Master231 svc]# kubectl apply -f 04-svc-NodeIP.yaml
  14. service/myweb-nodeport created
  15. [root@Master231 svc]# kubectl get svc
  16. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  17. kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 4m58s
  18. myweb-nodeport NodePort 10.200.98.50 <none> 8888:32212/TCP 5s

通过访问节点和端口号发现三个节点都可以访问到,此时有三个端口我们需要区别,80是Pod内容器的端口,8888是映射到svc的端口号,由于我们指定了类型为NodePort,所以会将服务的端口映射到节点上,也就是32212端口,客户通过访问宿主机的IP和这个端口访问到我们Pod中容器的业务。

  1. [root@Master231 svc]# curl 10.0.0.231:32212
  2. <html>
  3. <head><title>403 Forbidden</title></head>
  4. <body>
  5. <center><h1>403 Forbidden</h1></center>
  6. <hr><center>nginx/1.25.1</center>
  7. </body>
  8. </html>
  9. [root@Master231 svc]# curl 10.0.0.232:32212
  10. <html>
  11. <head><title>403 Forbidden</title></head>
  12. <body>
  13. <center><h1>403 Forbidden</h1></center>
  14. <hr><center>nginx/1.25.1</center>
  15. </body>
  16. </html>
  17. [root@Master231 svc]# curl 10.0.0.233:32212
  18. <html>
  19. <head><title>403 Forbidden</title></head>
  20. <body>
  21. <center><h1>403 Forbidden</h1></center>
  22. <hr><center>nginx/1.25.1</center>
  23. </body>
  24. </html>

二、NodePort原理介绍 

但是我们查看节点端口并查不到,原因是因为底层是用iptables做了端口转发,svc底层是由kube-proxy实现路由规则编写的,默认基于iptables实现,生产环境中建议使用ipvs。

  1. [root@Master231 svc]# ss -ntl | grep 32212
  2. [root@Master231 svc]#

我们查看svc的详细信息,并通过筛选iptables的规则,筛选它是如何进行端口转发的

  1. [root@Master231 svc]# kubectl describe svc myweb-nodeport
  2. Name: myweb-nodeport
  3. Namespace: default
  4. Labels: <none>
  5. Annotations: <none>
  6. Selector: apps=web
  7. Type: NodePort
  8. IP Family Policy: SingleStack
  9. IP Families: IPv4
  10. IP: 10.200.98.50
  11. IPs: 10.200.98.50
  12. Port: <unset> 8888/TCP
  13. TargetPort: 80/TCP
  14. NodePort: <unset> 32212/TCP
  15. Endpoints: 10.100.1.142:80,10.100.1.143:80,10.100.1.144:80 + 4 more...
  16. Session Affinity: None
  17. External Traffic Policy: Cluster
  18. Events: <none>
  19. [root@Master231 svc]# iptables-save | grep 10.200.98.50
  20. -A KUBE-SERVICES -d 10.200.98.50/32 -p tcp -m comment --comment "default/myweb-nodeport cluster IP" -m tcp --dport 8888 -j KUBE-SVC-LX25QHSHDI4TEKI3
  21. -A KUBE-SVC-LX25QHSHDI4TEKI3 ! -s 10.100.0.0/16 -d 10.200.98.50/32 -p tcp -m comment --comment "default/myweb-nodeport cluster IP" -m tcp --dport 8888 -j KUBE-MARK-MASQ

我们去复制svc字样的继续筛选发现了其他不仅有端口转发的规则,还有分配到七个IP的规则

  1. [root@Master231 svc]# iptables-save | grep KUBE-SVC-LX25QHSHDI4TEKI3
  2. :KUBE-SVC-LX25QHSHDI4TEKI3 - [0:0]
  3. -A KUBE-NODEPORTS -p tcp -m comment --comment "default/myweb-nodeport" -m tcp --dport 32212 -j KUBE-SVC-LX25QHSHDI4TEKI3
  4. -A KUBE-SERVICES -d 10.200.98.50/32 -p tcp -m comment --comment "default/myweb-nodeport cluster IP" -m tcp --dport 8888 -j KUBE-SVC-LX25QHSHDI4TEKI
  5. -A KUBE-SVC-LX25QHSHDI4TEKI3 ! -s 10.100.0.0/16 -d 10.200.98.50/32 -p tcp -m comment --comment "default/myweb-nodeport cluster IP" -m tcp --dport 8888 -j KUBE-MARK-MASQ
  6. -A KUBE-SVC-LX25QHSHDI4TEKI3 -p tcp -m comment --comment "default/myweb-nodeport" -m tcp --dport 32212 -j KUBE-MARK-MASQ
  7. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.14285714272 -j KUBE-SEP-34YMX72M7Y4RMEBB
  8. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.16666666651 -j KUBE-SEP-VFCOKZYDGC4I5XA7
  9. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-ES2K7MWKTIV5L5XD
  10. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-5DAVBS243ETY24QA
  11. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-3NKFEKM2JTI6MOIL
  12. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-OGLMOFLEJIELE43F
  13. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -j KUBE-SEP-DLGA52B2M7I3YUU6

我们选择一个IP规则去筛选,果然发现了IP

  1. [root@Master231 svc]# iptables-save | grep KUBE-SEP-34YMX72M7Y4RMEBB
  2. :KUBE-SEP-34YMX72M7Y4RMEBB - [0:0]
  3. -A KUBE-SEP-34YMX72M7Y4RMEBB -s 10.100.1.142/32 -m comment --comment "default/myweb-nodeport" -j KUBE-MARK-MASQ
  4. -A KUBE-SEP-34YMX72M7Y4RMEBB -p tcp -m comment --comment "default/myweb-nodeport" -m tcp -j DNAT --to-destination 10.100.1.142:80
  5. -A KUBE-SVC-LX25QHSHDI4TEKI3 -m comment --comment "default/myweb-nodeport" -m statistic --mode random --probability 0.14285714272 -j KUBE-SEP-34YMX72M7Y4RMEBB

 查看svc的详细ep信息,发现是Addresses中的一个,其他对应的肯定也可以筛选出来,不再演示了

  1. [root@Master231 svc]# kubectl describe ep myweb-nodeport
  2. Name: myweb-nodeport
  3. Namespace: default
  4. Labels: <none>
  5. Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-06-20T11:42:33Z
  6. Subsets:
  7. Addresses: 10.100.1.142,10.100.1.143,10.100.1.144,10.100.1.145,10.100.2.63,10.100.2.64,10.100.2.65
  8. NotReadyAddresses: <none>
  9. Ports:
  10. Name Port Protocol
  11. ---- ---- --------
  12. <unset> 80 TCP
  13. Events: <none>

三、自定义svc端口

编写nodePort端口为30080,默认端口范围是30000-32767,这是官方规则,如果想要修改范围需要修改api-server启动时的参数

  1. [root@Master231 svc]# cat 05-svc-NodeIP.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: myweb-nodeport
  6. spec:
  7. # 指定svc的类型为NodePort,也就是在默认的ClusterIP基础之上多监听所有worker节点的端口而已。
  8. type: NodePort
  9. # 基于标签选择器关联Pod
  10. selector:
  11. apps: web
  12. # 配置端口映射
  13. ports:
  14. # 指定Service服务本身的端口号
  15. - port: 8888
  16. # 后端Pod提供服务的端口号
  17. targetPort: 80
  18. # 如果是NodePort类型,可以指定NodePort监听的端口号,若不指定,则随机生成。
  19. nodePort: 30080

启动svc,创建资源,发现节点提供服务的端口成功修改

  1. [root@Master231 svc]# kubectl apply -f 05-svc-NodeIP.yaml
  2. service/myweb-nodeport created
  3. [root@Master231 svc]# kubectl get pods -o wide --show-labels
  4. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES LABELS
  5. koten-rc-4dzmj 1/1 Running 0 118s 10.100.2.69 worker233 <none> <none> apps=web
  6. koten-rc-52snl 1/1 Running 0 118s 10.100.1.149 worker232 <none> <none> apps=web
  7. koten-rc-kh9wf 1/1 Running 0 118s 10.100.1.150 worker232 <none> <none> apps=web
  8. [root@Master231 svc]# kubectl get svc
  9. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  10. kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 3m18s
  11. myweb-nodeport NodePort 10.200.193.203 <none> 8888:30080/TCP 2m16s
  12. [root@Master231 svc]# curl 10.0.0.231:30080
  13. <html>
  14. <head><title>403 Forbidden</title></head>
  15. <body>
  16. <center><h1>403 Forbidden</h1></center>
  17. <hr><center>nginx/1.25.1</center>
  18. </body>
  19. </html>

名称空间

Namespaces(名称空间)是一种资源对象,用于对 Kubernetes 资源进行分组和隔离。使用 Namespaces,可以将不同的 Kubernetes 资源(如 Pod、Service、Deployment 等)划分到不同的逻辑组中,实现资源的隔离和管理。

一、查看

1、查看名称空间

  1. [root@Master231 svc]# kubectl get ns
  2. NAME STATUS AGE
  3. default Active 6d3h
  4. kube-flannel Active 6d3h
  5. kube-node-lease Active 6d3h
  6. kube-public Active 6d3h
  7. kube-system Active 6d3h

2、查看默认名称空间下的资源,若不指定,则默认为default

  1. [root@Master231 svc]# kubectl get pods
  2. NAME READY STATUS RESTARTS AGE
  3. koten-rc-j86lg 1/1 Running 0 3s
  4. koten-rc-pjnjq 1/1 Running 0 3s
  5. koten-rc-zcjck 1/1 Running 0 3s

3、查看指定的kube-system名称空间

  1. [root@Master231 svc]# kubectl get pods -n kube-system
  2. NAME READY STATUS RESTARTS AGE
  3. coredns-6d8c4cb4d-5cxh8 1/1 Running 162 (18h ago) 6d3h
  4. coredns-6d8c4cb4d-tkkmr 1/1 Running 164 (18h ago) 6d3h
  5. etcd-master231 1/1 Running 3 (18h ago) 6d3h
  6. kube-apiserver-master231 1/1 Running 3 (18h ago) 6d3h
  7. kube-controller-manager-master231 1/1 Running 3 (18h ago) 6d3h
  8. kube-proxy-cl72l 1/1 Running 4 (18h ago) 6d3h
  9. kube-proxy-fpfw6 1/1 Running 3 (18h ago) 6d3h
  10. kube-proxy-lq4jw 1/1 Running 3 (18h ago) 6d3h
  11. kube-scheduler-master231 1/1 Running 3 (18h ago) 6d3h

4、查看所有名称空间的pod,cm资源

[root@Master231 pod]# kubectl get pods,cm -A

二、创建

1、响应式创建名称空间

  1. [root@Master231 svc]# kubectl create namespace linux
  2. namespace/linux created

2、声明式创建名称空间

  1. [root@Master231 namespaces]# cat 01-ns-custom.yaml
  2. apiVersion: v1
  3. kind: Namespace
  4. metadata:
  5. name: koten-linux-custom
  6. labels:
  7. author: koten
  8. hobby: linux
  9. [root@Master231 namespaces]# kubectl apply -f 01-ns-custom.yaml
  10. namespace/koten-linux-custom created

3、使用名称空间

  1. [root@Master231 pod]# cat 31-pods-volumes-configMap-games-ns.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. name: koten-games-cm-ns-002
  6. # 将资源加入到指定的名称空间
  7. namespace: linux
  8. spec:
  9. nodeName: worker233
  10. volumes:
  11. - name: data01
  12. configMap:
  13. name: nginx.conf
  14. containers:
  15. - name: games
  16. image: harbor.koten.com/koten-games/games:v0.5
  17. volumeMounts:
  18. - name: data01
  19. mountPath: /etc/nginx/conf.d/
  20. ---
  21. apiVersion: v1
  22. kind: ConfigMap
  23. metadata:
  24. name: nginx.conf
  25. namespace: linux
  26. data:
  27. nginx.conf: |
  28. user nginx;
  29. worker_processes auto;
  30. error_log /var/log/nginx/error.log notice;
  31. pid /var/run/nginx.pid;
  32. events {
  33. worker_connections 1024;
  34. }
  35. http {
  36. include /etc/nginx/mime.types;
  37. default_type application/octet-stream;
  38. log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  39. '$status $body_bytes_sent "$http_referer" '
  40. '"$http_user_agent" "$http_x_forwarded_for"';
  41. access_log /var/log/nginx/access.log main;
  42. sendfile on;
  43. keepalive_timeout 65;
  44. include /etc/nginx/conf.d/*.conf;
  45. }
  46. [root@Master231 pod]# kubectl apply -f 31-pods-volumes-configMap-games-ns.yaml
  47. pod/koten-games-cm-ns-002 created
  48. configmap/nginx.conf created
  49. [root@Master231 pod]# kubectl -n linux get cm,po
  50. NAME DATA AGE
  51. configmap/kube-root-ca.crt 1 6m
  52. configmap/nginx.conf 1 11s
  53. NAME READY STATUS RESTARTS AGE
  54. pod/koten-games-cm-ns-002 1/1 Running 0 11s

三、删除

1、删除名称空间,一旦删除一个名称空间,则该名称空间下的所有资源都会被删除

  1. [root@Master231 pod]# kubectl delete namespaces linux
  2. namespace "linux" deleted
  3. [root@Master231 pod]# kubectl -n linux get cm,po
  4. No resources found in linux namespace.

切换kube-proxy工作模式为ipvs

1、查看kube-proxy默认的工作模式,可以看到是iptables

  1. [root@Master231 pod]# kubectl -n kube-system logs -f kube-proxy-cl72l
  2. I0620 00:32:58.145622 1 node.go:163] Successfully retrieved node IP: 10.0.0.232
  3. I0620 00:32:58.145952 1 server_others.go:138] "Detected node IP" address="10.0.0.232"
  4. I0620 00:32:58.146076 1 server_others.go:572] "Unknown proxy mode, assuming iptables proxy" proxyMode=""
  5. I0620 00:32:58.232023 1 server_others.go:206] "Using iptables Proxier"
  6. ......

2、修改默认的工作模式

  1. [root@Master231 pod]# kubectl -n kube-system edit cm kube-proxy
  2. ......
  3. configmap/kube-proxy edited
  4. [root@Master231 pod]# kubectl -n kube-system get cm kube-proxy -o yaml | grep mode
  5. mode: "ipvs"

3、所有节点安装ipvs相关模块管理工具

所有节点安装ipvs相关组件

yum -y install conntrack-tools ipvsadm.x86_64 

编写加载ipvs的配置文件

  1. cat > /etc/sysconfig/modules/ipvs.modules <<EOF
  2. #!/bin/bash
  3. modprobe -- ip_vs
  4. modprobe -- ip_vs_rr
  5. modprobe -- ip_vs_wrr
  6. modprobe -- ip_vs_sh
  7. modprobe -- nf_conntrack_ipv4
  8. EOF

加载ipvs相关模块并查看

chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod | grep -e ip_vs -e nf_conntrack_ipv4

4、重启Pod让cm的配置生效

  1. [root@Master231 pod]# kubectl -n kube-system get pods | grep kube-proxy
  2. kube-proxy-cl72l 1/1 Running 4 (19h ago) 6d3h
  3. kube-proxy-fpfw6 1/1 Running 3 (19h ago) 6d3h
  4. kube-proxy-lq4jw 1/1 Running 3 (19h ago) 6d4h
  5. [root@Master231 pod]# kubectl -n kube-system delete pod `kubectl -n kube-system get pods | grep kube-proxy | awk '{print $1}'`
  6. pod "kube-proxy-cl72l" deleted
  7. pod "kube-proxy-fpfw6" deleted
  8. pod "kube-proxy-lq4jw" deleted

5、验证是否修改了工作模式,发现成功修改

  1. [root@Master231 pod]# kubectl -n kube-system get pods | grep kube-proxy
  2. kube-proxy-8tk4w 1/1 Running 0 61s
  3. kube-proxy-dcblb 1/1 Running 0 62s
  4. kube-proxy-jpgmd 1/1 Running 0 62s
  5. [root@Master231 pod]# kubectl -n kube-system logs -f kube-proxy-8tk4w
  6. I0620 12:35:35.306840 1 node.go:163] Successfully retrieved node IP: 10.0.0.233
  7. I0620 12:35:35.307021 1 server_others.go:138] "Detected node IP" address="10.0.0.233"
  8. I0620 12:35:35.369096 1 server_others.go:269] "Using ipvs Proxier"

6、查看svc基于ipvs的映射

  1. #先启动下rc和svc
  2. [root@Master231 rc]# kubectl apply -f 03-rc-nginx.yaml
  3. configmap/nginx.conf unchanged
  4. replicationcontroller/koten-rc created
  5. [root@Master231 svc]# kubectl apply -f 05-svc-NodeIP.yaml
  6. service/myweb-nodeport created
  7. #可以看到svc的服务和端口指向Pod的IP和端口
  8. [root@Master231 rc]# kubectl get svc
  9. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  10. kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 46s
  11. myweb-nodeport NodePort 10.200.34.126 <none> 8888:30080/TCP 7s
  12. [root@Master231 rc]# ipvsadm -ln | grep 10.200.34.126 -A 3
  13. TCP 10.200.34.126:8888 rr
  14. -> 10.100.1.156:80 Masq 1 0 0
  15. -> 10.100.1.157:80 Masq 1 0 0
  16. -> 10.100.1.158:80 Masq 1 0 0
  17. #指向的Pod的IP与Endpoints一致
  18. [root@Master231 rc]# kubectl describe svc myweb-nodeport
  19. Name: myweb-nodeport
  20. Namespace: default
  21. Labels: <none>
  22. Annotations: <none>
  23. Selector: apps=web
  24. Type: NodePort
  25. IP Family Policy: SingleStack
  26. IP Families: IPv4
  27. IP: 10.200.34.126
  28. IPs: 10.200.34.126
  29. Port: <unset> 8888/TCP
  30. TargetPort: 80/TCP
  31. NodePort: <unset> 30080/TCP
  32. Endpoints: 10.100.1.156:80,10.100.1.157:80,10.100.1.158:80 + 4 more...
  33. Session Affinity: None
  34. External Traffic Policy: Cluster
  35. Events: <none>

可用性检查探针readinessProbe

  1. 可用性检查,周期性检查服务是否可用,从而判断容器是否就绪。
  2. 若检测Pod服务不可用,则会将Pod从svc的ep列表中移除。
  3. 若检测Pod服务可用,则会将Pod重新添加到svc的ep列表中。
  4. 如果容器没有提供可用性检查,则默认状态为Success。

一、案例展示

检查有exec、httpget、tcpSocket三种方式,我这边只展示httpget了

编写资源清单,有健康状态检查和可用性探针,观察参数是一直健康的状态,但是如果不干预会在第21秒检查出错,23秒Pod会从svc中移除

  1. [root@Master231 pod]# cat 32-pods-livenessProbe-readinessProbe-httGet.yaml
  2. kind: Pod
  3. apiVersion: v1
  4. metadata:
  5. name: probe-liveness-readiness-httpget
  6. labels:
  7. author: koten
  8. hobby: linux
  9. spec:
  10. containers:
  11. - name: web
  12. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  13. livenessProbe:
  14. httpGet:
  15. port: 80
  16. path: /index.html
  17. failureThreshold: 3
  18. initialDelaySeconds: 0
  19. periodSeconds: 10
  20. successThreshold: 1
  21. timeoutSeconds: 1
  22. readinessProbe:
  23. httpGet:
  24. port: 80
  25. path: /koten.html
  26. failureThreshold: 3
  27. initialDelaySeconds: 20
  28. periodSeconds: 1
  29. successThreshold: 1
  30. timeoutSeconds: 1
  31. ---
  32. apiVersion: v1
  33. kind: Service
  34. metadata:
  35. name: probe-readiness-liveness-httpget
  36. spec:
  37. type: ClusterIP
  38. selector:
  39. hobby: linux
  40. ports:
  41. - port: 8888
  42. targetPort: 80

运行测试,虽然status显示running,但是ready一直处于未就绪,在21秒的时候显示不可用,将在23秒的时候移除ep

  1. [root@Master231 pod]# kubectl apply -f 32-pods-livenessProbe-readinessProbe-httGet.yaml
  2. pod/probe-liveness-readiness-httpget created
  3. service/probe-readiness-liveness-httpget created
  4. [root@Master231 pod]# kubectl get pods
  5. NAME READY STATUS RESTARTS AGE
  6. probe-liveness-readiness-httpget 0/1 Running 0 15s
  7. [root@Master231 pod]# kubectl describe pods probe-liveness-readiness-httpget
  8. .....
  9. Events:
  10. Type Reason Age From Message
  11. ---- ------ ---- ---- -------
  12. Normal Scheduled 23s default-scheduler Successfully assigned default/probe-liveness-readiness-httpget to worker232
  13. Normal Pulled 23s kubelet Container image "harbor.koten.com/koten-web/nginx:1.25.1-alpine" already present on machine
  14. Normal Created 23s kubelet Created container web
  15. Normal Started 22s kubelet Started container web
  16. Warning Unhealthy 1s (x2 over 2s) kubelet Readine
  17. [root@Master231 pod]# kubectl describe ep probe-readiness-liveness-httpget
  18. Name: probe-readiness-liveness-httpget
  19. Namespace: default
  20. Labels: <none>
  21. Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-06-20T13:21:29Z
  22. Subsets:
  23. Addresses: <none>
  24. NotReadyAddresses: 10.100.1.164
  25. Ports:
  26. Name Port Protocol
  27. ---- ---- --------
  28. <unset> 80 TCP
  29. Events: <none>
  30. [root@Master231 pod]#

我们在20秒内增加/koten.html的代码文件,发现IP地址已经加上了,没有准备的IP地址消失了

  1. [root@Master231 pod]# kubectl describe ep probe-readiness-liveness-httpget
  2. Name: probe-readiness-liveness-httpget
  3. Namespace: default
  4. Labels: <none>
  5. Annotations: endpoints.kubernetes.io/last-change-trigger-time: 2023-06-20T13:26:36Z
  6. Subsets:
  7. Addresses: 10.100.1.167
  8. NotReadyAddresses: <none>
  9. Ports:
  10. Name Port Protocol
  11. ---- ---- --------
  12. <unset> 80 TCP
  13. Events: <none>
  14. [root@Master231 pod]#

启动探针startupProbe

  1. startupProbe探针是1.16后面的k8s版本才有的。
  2. 如果提供了启动探针,则所有其他探针都会被禁用,直到此探针成功为止。
  3. 如果启动探测失败,kubelet将杀死容器,而容器依其重启策略进行重启。 
  4. 如果容器没有提供启动探测,则默认状态为 Success。

一、案例展示

同样是只使用httpget的方式演示,其他方式同理

编写资源清单,观察探针,可以发现虽然规定了健康探针和可用性探针,但是他们在业务启动的同时进行检查的,与启动探针一起启动时,优先级比启动性探针小,可以发现启动探针会在41秒报错,43秒的时候杀死容器

  1. kind: Pod
  2. apiVersion: v1
  3. metadata:
  4. name: probe-startup-liveness-readiness-httpget
  5. labels:
  6. author: koten
  7. hobby: linux
  8. spec:
  9. containers:
  10. - name: web
  11. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  12. # 指定健康检查探针,若未指定默认成功,若定义该探针,检测失败则会重启容器,即重新创建容器。
  13. livenessProbe:
  14. httpGet:
  15. port: 80
  16. path: /linux.html
  17. failureThreshold: 3
  18. initialDelaySeconds: 10
  19. periodSeconds: 10
  20. successThreshold: 1
  21. timeoutSeconds: 1
  22. # 指定就绪探针,若未指定则默认成功,若定义该探针,检测失败则会将Pod标记为未就绪状态,而未就绪的Pod是不会记录到svc的ep列表中。
  23. readinessProbe:
  24. httpGet:
  25. port: 80
  26. path: /koten.html
  27. failureThreshold: 3
  28. initialDelaySeconds: 20
  29. periodSeconds: 1
  30. successThreshold: 1
  31. timeoutSeconds: 1
  32. # 指定启动探针,若定义了该探针,会优先通过该探针检测,若检测成功,再去执行readinessProbe和livenessProbe探针。
  33. # 若没有定义该探针,则默认为成功。
  34. startupProbe:
  35. httpGet:
  36. port: 80
  37. path: /start.html
  38. failureThreshold: 3
  39. initialDelaySeconds: 40
  40. periodSeconds: 1
  41. successThreshold: 1
  42. timeoutSeconds: 1
  43. ---
  44. apiVersion: v1
  45. kind: Service
  46. metadata:
  47. name: probe-startup-readiness-liveness-httpget
  48. spec:
  49. type: ClusterIP
  50. selector:
  51. hobby: linux
  52. ports:
  53. - port: 8888
  54. targetPort: 80

运行资源清单观察效果,启动后进入容器,啥也不干,发现自动退出了,查看详细信息,确实是43秒退出的

  1. [root@Master231 pod]# kubectl apply -f 33-pods-startupProbe-livenessProbe-readinessProbe-httGet.yaml
  2. pod/probe-startup-liveness-readiness-httpget created
  3. service/probe-startup-readiness-liveness-httpget created
  4. [root@Master231 pod]# kubectl exec -it probe-startup-liveness-readiness-httpget -- sh
  5. / # command terminated with exit code 137
  6. [root@Master231 pod]# kubectl describe pod probe-startup-liveness-readiness-httpget
  7. ......
  8. Events:
  9. Type Reason Age From Message
  10. ---- ------ ---- ---- -------
  11. Normal Scheduled 56s default-scheduler Successfully assigned default/probe-startup-liveness-readiness-httpget to worker232
  12. Normal Pulled 13s (x2 over 56s) kubelet Container image "harbor.koten.com/koten-web/nginx:1.25.1-alpine" already present on machine
  13. Normal Created 13s (x2 over 56s) kubelet Created container web
  14. Normal Started 13s (x2 over 55s) kubelet Started container web
  15. Warning Unhealthy 13s (x3 over 15s) kubelet Startup probe failed: HTTP probe failed with statuscode: 404
  16. Normal Killing 13s kubelet Container web failed startup probe, will be restarted

这次我们在40秒内echo进去\start.html,就不会报错了,发现启动探针没有报错,但是可用性探针报错了,说明可用性探针和健康状态检查探针在启动探针检查服务可用后开始工作了

  1. [root@Master231 pod]# kubectl apply -f 33-pods-startupProbe-livenessProbe-readinessProbe-httGet.yaml
  2. pod/probe-startup-liveness-readiness-httpget created
  3. service/probe-startup-readiness-liveness-httpget created
  4. [root@Master231 pod]# kubectl exec -it probe-startup-liveness-readiness-httpget -- sh
  5. / # echo 111 > /usr/share/nginx/html/start.html
  6. / #
  7. [root@Master231 pod]# kubectl describe pod probe-startup-liveness-readiness-httpget
  8. Events:
  9. Type Reason Age From Message
  10. ---- ------ ---- ---- -------
  11. Normal Scheduled 46s default-scheduler Successfully assigned default/probe-startup-liveness-readiness-httpget to worker232
  12. Normal Pulled 46s kubelet Container image "harbor.koten.com/koten-web/nginx:1.25.1-alpine" already present on machine
  13. Normal Created 46s kubelet Created container web
  14. Normal Started 45s kubelet Started container web
  15. Warning Unhealthy 1s (x5 over 5s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 404

静态Pod

静态pod是由kubelet进行管理的pod,不通过master节点的apiserver监管,可以有效预防kubectl误删除,用来保证k8s集群和核心业务功能的稳定性。

  1. 想要在特定节点上创建静态Pod,可以在该节点上创建静态Pod的清单文件,并将文件放在 /etc/kubernetes/manifests目录下。
  2. kubelet组件会监视该目录,并根据静态Pod的清单立即创建Pod。但是需要注意的是,这种方式只适用于在特定的Node节点上创建静态Pod,而不能用于在整个Kubernetes集群中创建静态Pod。

查看kubelet定义的默认静态Pod路径,可以看到里面有四个yaml文件,会根据yaml文件去自动创建Pod。

  1. [root@Master231 pod]# grep static /var/lib/kubelet/config.yaml
  2. staticPodPath: /etc/kubernetes/manifests
  3. [root@Master231 pod]# ls /etc/kubernetes/manifests/
  4. etcd.yaml kube-controller-manager.yaml
  5. kube-apiserver.yaml kube-scheduler.yaml

yaml文件内会有很多api-server的参数,看不懂没有关系,去官方文档看,参考链接如下

  1. https://kubernetes.io/zh-cn/docs/reference/command-line-tools-reference/kube-apiserver/
  2. https://kubernetes.io/zh-cn/docs/reference/command-line-tools-reference/kube-controller-manager/
  3. https://kubernetes.io/zh-cn/docs/reference/command-line-tools-reference/kube-proxy/
  4. https://kubernetes.io/zh-cn/docs/reference/command-line-tools-reference/kube-scheduler/

需要注意的是

  1. 1、静态Pod只能创建Pod,如果有其他的类型的文件不会创建,如果里面既有pod也有其他资源,会只创建pod;
  2. 2、如果更改了目录的文件,没有创建Pod,将其移动出去再移动进来即可;
  3. 3、修改Api Server非常容易报错,导致kubectl整体报错无法正常运行,所以提前做好快照!

修改svc的NodePort类型的端口范围

1、修改API-server静态Pod的资源清单

  1. [root@Master231 pod]# cat /etc/kubernetes/manifests/kube-apiserver.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. annotations:
  6. creationTimestamp: null
  7. labels:
  8. component: kube-apiserver
  9. tier: control-plane
  10. name: kube-apiserver
  11. namespace: kube-system
  12. spec:
  13. containers:
  14. - command:
  15. - kube-apiserver
  16. - --service-node-port-range=3000-50000 #在此处添加这一行即可
  17. - --advertise-address=10.0.0.231
  18. ......

2、移动资源清单

  1. [root@Master231 pod]# mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
  2. [root@Master231 pod]# mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/

3、 创建svc的NodePort类型,验证端口是否可以设置8080

  1. [root@Master231 pod]# cat /manifests/svc/05-svc-NodeIP.yaml
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: myweb-nodeport
  6. spec:
  7. # 指定svc的类型为NodePort,也就是在默认的ClusterIP基础之上多监听所有worker节点的端口而已。
  8. type: NodePort
  9. # 基于标签选择器关联Pod
  10. selector:
  11. apps: web
  12. # 配置端口映射
  13. ports:
  14. # 指定Service服务本身的端口号
  15. - port: 8888
  16. # 后端Pod提供服务的端口号
  17. targetPort: 80
  18. # 如果是NodePort类型,可以指定NodePort监听的端口号,若不指定,则随机生成。
  19. nodePort: 30080
  20. # 默认端口范围是"30000-32767",官方规则,如果想要修改该范围,需要修改api-server启动时的参数.
  21. nodePort: 8080

启动rc资源清单和上面写的svc指定端口的资源清单

  1. [root@Master231 pod]# kubectl apply -f ../rc/03-rc-nginx.yaml
  2. configmap/nginx.conf unchanged
  3. replicationcontroller/koten-rc created
  4. [root@Master231 pod]# kubectl apply -f /manifests/svc/05-svc-NodeIP.yaml
  5. service/myweb-nodeport created

 发现成功映射到了8080端口

  1. [root@Master231 pod]# kubectl get svc
  2. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  3. kubernetes ClusterIP 10.200.0.1 <none> 443/TCP 2m29s
  4. myweb-nodeport NodePort 10.200.21.72 <none> 8888:8080/TCP 83s
  5. [root@Master231 pod]# curl 10.0.0.231:8080
  6. <html>
  7. <head><title>403 Forbidden</title></head>
  8. <body>
  9. <center><h1>403 Forbidden</h1></center>
  10. <hr><center>nginx/1.25.1</center>
  11. </body>
  12. </html>

Pod优雅的终止

优雅的终止是指延迟结束Pod的时间,在这个时间内可以进行一些操作,例如备份等操作

一、案例展示

需要注意的是,优雅终止的延迟时间要大于Pod结束前做的事情的时间,不然Pod还没有处理完就已经终止Pod了

  1. [root@Master231 pod]# cat 34-pods-lifecycle-postStart-preStop.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. name: koten-pod-containers-lifecycle-001
  6. spec:
  7. volumes:
  8. - name: data
  9. hostPath:
  10. path: /koten-linux
  11. # 在pod优雅终止时,定义延迟发送kill信号的时间,此时间可用于pod处理完未处理的请求等状况。
  12. # 默认单位是秒,若不设置默认值为30s。
  13. terminationGracePeriodSeconds: 60
  14. # terminationGracePeriodSeconds: 5
  15. containers:
  16. - name: myweb
  17. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  18. volumeMounts:
  19. - name: data
  20. mountPath: /data
  21. # 定义Pod的生命周期。
  22. lifecycle:
  23. # Pod启动之后做的事情,若该步骤未完成,创建容器时一直处于"ContainerCreating"状态,此时无法访问业务。
  24. # 换句话说,若此节点为执行成功,是不会分配IP地址的,对于比较复杂的动作,建议放在initContianers中执行。
  25. postStart:
  26. exec:
  27. command:
  28. - "/bin/sh"
  29. - "-c"
  30. # - "echo \"postStart at $(date +%F_%T)\" >> /data/postStart.log"
  31. - "sleep 30"
  32. # Pod停止之前做的事情
  33. preStop:
  34. exec:
  35. command:
  36. - "/bin/sh"
  37. - "-c"
  38. - "echo \"preStop at $(date +%F_%T)\" >> /data/preStop.log"
  39. # - "sleep 30"

二、portstart与启动检查与初始化容器的优先级

不难推断,初始化容器的优先级一定比优雅终止和启动检查要高,经测试,portstart的优先级比启动检查高,所以在启动Pod时,以此经历初始化容器、portstart、启动检查,给大家提供可以测试的资源清单,感兴趣可以自行测试

  1. [root@Master231 pod]# cat 35-pods-lifecycle-postStart-preStop.yaml
  2. apiVersion: v1
  3. kind: Pod
  4. metadata:
  5. name: koten-pod-containers-lifecycle-002
  6. spec:
  7. volumes:
  8. - name: data
  9. hostPath:
  10. path: /koten-linux
  11. # 在pod优雅终止时,定义延迟发送kill信号的时间,此时间可用于pod处理完未处理的请求等状况。
  12. # 默认单位是秒,若不设置默认值为30s。
  13. terminationGracePeriodSeconds: 60
  14. # terminationGracePeriodSeconds: 5
  15. initContainers:
  16. - name: init
  17. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  18. command:
  19. - sleep
  20. - "10"
  21. containers:
  22. - name: myweb
  23. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  24. volumeMounts:
  25. - name: data
  26. mountPath: /data
  27. # 定义Pod的生命周期。
  28. lifecycle:
  29. # Pod启动之后做的事情,若该步骤未完成,创建容器时一直处于"ContainerCreating"状态,此时无法访问业务。
  30. # 换句话说,若此节点为执行成功,是不会分配IP地址的哟~对于比较复杂的动作,建议放在initContianers中执行。
  31. # 值得注意的是,postStart是高于startupProbe。
  32. postStart:
  33. exec:
  34. command:
  35. - "/bin/sh"
  36. - "-c"
  37. # - "echo \"postStart at $(date +%F_%T)\" >> /data/postStart.log"
  38. - "sleep 30"
  39. # Pod停止之前做的事情
  40. preStop:
  41. exec:
  42. command:
  43. - "/bin/sh"
  44. - "-c"
  45. - "echo \"preStop at $(date +%F_%T)\" >> /data/preStop.log"
  46. # - "sleep 30"
  47. livenessProbe:
  48. httpGet:
  49. port: 80
  50. path: /linux.html
  51. failureThreshold: 3
  52. initialDelaySeconds: 10
  53. periodSeconds: 10
  54. successThreshold: 1
  55. timeoutSeconds: 1
  56. readinessProbe:
  57. httpGet:
  58. port: 80
  59. path: /koten.html
  60. failureThreshold: 3
  61. initialDelaySeconds: 20
  62. periodSeconds: 1
  63. successThreshold: 1
  64. timeoutSeconds: 1
  65. startupProbe:
  66. httpGet:
  67. port: 80
  68. path: /start.html
  69. failureThreshold: 3
  70. initialDelaySeconds: 5
  71. periodSeconds: 1
  72. successThreshold: 1
  73. timeoutSeconds: 1

Pod的创建、删除流程(Pod生命周期)

一、创建Pod

  1. 1、完成Pod调度流程
  2. 2、initContainer初始化容器
  3. 3、容器启动并执行postStart
  4. 4、livessProbe
  5. 5、进入Running状态
  6. 6、livenessProbe与readinessProbe
  7. 7、service关联Pod
  8. 8、接收客户端请求

二、删除Pod

  1. 1、Pod被设置为Terminating状态,从service的endPoints列表中删除并不在接收客户端请求
  2. 2、执行PreStop(Pod停止之前做的事情)
  3. 3、k8s向pod中的容器发送SIGTERM信号(正常终止信号)终止Pod里面的主进程,这个信号让容器知道自己很快将会被关闭。
  4. 4、经过可选的配置参数terminationGracePeriodSeconds终止等待期,如果有设置宽限时间,则等待宽限时间到期,否则最多等待30秒。
  5. 5、k8S等待指定的时间称为优雅终止宽限期,默认情况下是30秒,值得注意的是等待期与preStop Hook和SIGTERM信号并行执行,即K8S可能不会等待preStop Hook完成(最长30秒之后主进程还没有介绍就强制终止Pod)。
  6. 6、SIGKILL信号被发送到Pod,并删除Pod。

利用rc和svc实现应用升级

要求用户访问svc的NodePort类型,升级过程中,应用不能停止,即升级过程中用户是可以访问的。

其实先前文章介绍过代码发布流程,有很多种,蓝绿发布、金丝雀发布、滚动发布等等,像蓝绿发布,比如我们的Pod是3个v1版本,我们可以再启动3个v2版本,将svc从v1指向v2即可;还有一种滚动式的发布是一个一个Pod去删除,反正rc会稳定在3个数量。

我这边演示后面滚动式发布的方法,用nginx1.14.0版本当做v1,用nginx1.15.1当做v2

这样方法的弊端是只能在rc修改镜像,如果修改了tag,rc那边就调度不上了,会影响客户体验。

先编写v1的rc和svc

  1. [root@Master231 rc]# cat 04-rc-apps.yaml
  2. apiVersion: v1
  3. kind: ReplicationController
  4. metadata:
  5. name: koten-rc-apps-v1
  6. spec:
  7. replicas: 3
  8. template:
  9. metadata:
  10. labels:
  11. apps: v1
  12. spec:
  13. containers:
  14. - name: v1
  15. image: harbor.koten.com/koten-web/nginx:1.24.0-alpine
  16. [root@Master231 svc]# cat 06-rc-apps-svc.yaml
  17. apiVersion: v1
  18. kind: Service
  19. metadata:
  20. name: koten-apps
  21. spec:
  22. type: NodePort
  23. selector:
  24. apps: v1
  25. ports:
  26. - port: 80
  27. targetPort: 80
  28. nodePort: 30080

启动v1版本的rc和svc,观察label,浏览器访问节点的IP

  1. [root@Master231 rc]# kubectl apply -f 04-rc-apps.yaml
  2. replicationcontroller/koten-rc-apps-v1 created
  3. [root@Master231 svc]# kubectl apply -f 06-rc-apps-svc.yaml
  4. service/koten-apps created
  5. [root@Master231 svc]# kubectl get pods --show-labels
  6. NAME READY STATUS RESTARTS AGE LABELS
  7. koten-rc-apps-v1-7crtc 1/1 Running 0 3m20s apps=v1
  8. koten-rc-apps-v1-pt4lm 1/1 Running 0 3m20s apps=v1
  9. koten-rc-apps-v1-sfsk8 1/1 Running 0 3m20s apps=v1
  10. [root@Master231 svc]# kubectl get svc koten-apps
  11. NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  12. koten-apps NodePort 10.200.167.49 <none> 80:30080/TCP 26s

修改svc调度,Pod不会直接发生变化

  1. [root@Master231 rc]# cat 04-rc-apps.yaml
  2. apiVersion: v1
  3. kind: ReplicationController
  4. metadata:
  5. name: koten-rc-apps-v1
  6. spec:
  7. replicas: 3
  8. template:
  9. metadata:
  10. labels:
  11. apps: v1
  12. spec:
  13. containers:
  14. - name: v1
  15. image: harbor.koten.com/koten-web/nginx:1.25.1-alpine
  16. [root@Master231 rc]# kubectl apply -f 04-rc-apps.yaml
  17. replicationcontroller/koten-rc-apps-v1 configured

一个一个删除Pod,发现Pod都变成了v2 

  1. [root@Master231 rc]# kubectl get pods -o wide
  2. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
  3. koten-rc-apps-v1-7crtc 1/1 Running 0 7m16s 10.100.1.179 worker232 <none> <none>
  4. koten-rc-apps-v1-pt4lm 1/1 Running 0 7m16s 10.100.2.86 worker233 <none> <none>
  5. koten-rc-apps-v1-sfsk8 1/1 Running 0 7m16s 10.100.1.178 worker232 <none> <none>
  6. [root@Master231 rc]# kubectl delete pods koten-rc-apps-v1-7crtc
  7. pod "koten-rc-apps-v1-7crtc" deleted
  8. [root@Master231 rc]# kubectl get pods -o wide
  9. NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
  10. koten-rc-apps-v1-h4l7x 1/1 Running 0 3s 10.100.1.180 worker232 <none> <none>
  11. koten-rc-apps-v1-pt4lm 1/1 Running 0 7m34s 10.100.2.86 worker233 <none> <none>
  12. koten-rc-apps-v1-sfsk8 1/1 Running 0 7m34s 10.100.1.178 worker232 <none> <none>
  13. [root@Master231 rc]# curl -I 10.100.1.180
  14. HTTP/1.1 200 OK
  15. Server: nginx/1.25.1
  16. Date: Tue, 20 Jun 2023 15:00:03 GMT
  17. Content-Type: text/html
  18. Content-Length: 615
  19. Last-Modified: Tue, 13 Jun 2023 17:34:28 GMT
  20. Connection: keep-alive
  21. ETag: "6488a8a4-267"
  22. Accept-Ranges: bytes
  23. [root@Master231 rc]# kubectl delete pod koten-rc-apps-v1-pt4lm koten-rc-apps-v1-sfsk8
  24. pod "koten-rc-apps-v1-pt4lm" deleted
  25. pod "koten-rc-apps-v1-sfsk8" deleted


我是koten,10年运维经验,持续分享运维干货,感谢大家的阅读和关注!

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号