大纲 Kubernetes 核心技术 部署集群性能监控平台 开源软件 cAdvisor(Container Advisor)可用于监控所在节点的容器运行状态,当前已经被默认集成到 Kubernetes 的 Kubelet 组件内,默认使用 TCP 4194 端口。在中小型规模容器集群中,通常使用 Prometheus + Grafana 来实现容器集群性能数据的采集、存储与展示。
集群可监控的指标 在 Kubernetes 集群中,可以监控的指标有以下这些:
节点级(Node Metrics):监控每个工作节点(Worker Node)的系统资源使用情况
CPU 使用率 / 空闲率 / 负载(Load Average) 内存使用量 / 可用内存 磁盘使用量 / I/O 吞吐 网络流量(带宽、收发包量、错误包) 节点状态(Ready / NotReady) 节点文件系统使用率(根分区、容器存储路径) Pod / 容器级(Pod & Container Metrics):监控集群中每个 Pod 或容器的资源使用情况
Pod 的 CPU 使用率 / 限额 / 请求 Pod 的内存使用量 / 限额 / 请求 容器重启次数 容器启动时间、运行时间 网络收发流量(in/out bytes) 文件系统使用(容器内存储卷) 容器状态(Running / Waiting / Terminated) Kubernetes 组件指标(Control Plane Metrics):监控 K8s 控制面(Control Plane)自身的健康状态
API Server 请求速率 / 延迟 / 错误率 Scheduler 调度延迟 / 排队任务数 Controller Manager 队列长度 / 事件处理速率 Etcd 存储延迟 / Leader 选举状态 / 写入吞吐 服务与网络(Service & Network Metrics):监控 Service、Ingress、DNS、网络插件等组件
Service 请求速率 / 成功率 / 延迟 Ingress 访问量 / 响应时间 / 4xx、5xx 错误率 DNS 查询速率 / 失败率(CoreDNS) CNI 插件流量、丢包、延迟 工作负载与资源对象状态(Workload Metrics):反映 K8s 资源对象(如 Deployment、DaemonSet、Job 等)的运行健康度
Deployment 可用副本数 / 期望副本数 ReplicaSet、StatefulSet 状态 Job 成功 / 失败次数 CronJob 最近执行时间 Namespace 级资源使用量 HPA(HorizontalPodAutoScaler)触发状态 存储指标(Storage Metrics):监控 PV、PVC、StorageClass 的使用与性能
PV 容量使用率 PVC 绑定状态 I/O 延迟、读写吞吐 挂载错误 / 超时 应用与业务指标(Application Metrics): 通过 Prometheus Exporter 或 SDK 自定义的应用性能指标
请求 QPS(请求数每秒) 错误率(Error Rate) 响应时间(Latency) 业务统计(订单数、任务完成数等) 自定义计数器 / 计时器 / 直方图(Histogram) 告警与事件(Events & Alerts):用于监控异常行为与故障
Pod CrashLoopBackOff 节点不可用 资源超限(CPU / 内存) Deployment 副本不足 PV 绑定失败 / 存储空间不足 API 请求超时 / 错误率过高 集群性能监控方案 常见的 Kubernetes 集群性能监控方案
Heapster + InfluxDB + Grafana
架构:Heapster + InfluxDB + Grafana 功能:Heapster 汇聚各 Node 上 cAdvisor 的监控数据,存入 InfluxDB 后通过 Grafana 展示。 状态:已被 Kubernetes 官方弃用,自 Kubernetes v1.13 起不再维护。 适用场景:早期集群监控,仅作学习了解。 Metrics Server
角色:Heapster 的官方替代品。 功能:提供实时的资源用量指标(CPU、内存)给 kubectl top、Horizontal Pod Autoscaler(HPA)等使用。 限制:不存储历史数据,不提供可视化界面或持久化存储。 状态:Kubernetes 官方的核心组件。 适用场景:HPA 自动扩缩容、轻量实时监控。 Prometheus + Grafana
架构:Prometheus + Grafana 功能:Prometheus = 数据收集 + 存储 + 告警。 Grafana = 数据展示 + 可视化分析界面。 优点:Prometheus 会周期性自动拉取 kubelet、cAdvisor、kube-state-metrics、node-exporter 等监控指标 监控维度丰富(节点、容器、Pod、Service、集群状态) 数据查询灵活(PromQL) 支持历史数据存储 状态:当前主流的开源监控方案。 适用场景:中小型或自建环境中的主力方案。 Prometheus Operator + kube-prometheus-stack
架构组件:Prometheus(采集与存储指标) Alertmanager(告警) Grafana(可视化) kube-state-metrics(集群资源状态) node-exporter(节点系统指标) 特点:Operator 自动化管理 Prometheus、Alertmanager、Grafana 等部署与配置。 社区维护的 “kube-prometheus-stack” Helm Chart 是生产级推荐方案。 状态:功能最全、生态最活跃。 适用场景:生产级集群的监控、告警、可视化一体化方案。 Weave Scope / Weave Cloud
功能:直观展示 Pod、容器、Service 之间的拓扑关系与状态。 支持查看部分性能指标(CPU、内存、网络流量)。 定位:更偏向于可视化与运维调试工具。 不属于严格意义上的 “性能监控方案”。 适用场景:集群拓扑观测、实时诊断、开发或测试环境。 可作为 Prometheus 或 Metrics Server 的补充。 不同 Kubernetes 集群性能监控方案的对比
性能监控方案 类型 是否官方推荐 是否可替代 Heapster 可视化展示 是否持久化存储 Heapster + InfluxDB + Grafana 旧版指标监控 ❌ 已弃用 ✔ ✔ ✔ Metrics Server 资源指标采集 ✔ 官方推荐 ✔ ❌ ❌ Prometheus + Grafana 指标监控 ✔ 官方推荐 ✔ ✔ ✔ Prometheus Operator Stack 生产级集群监控告警平台 ✔ 官方推荐 ✔ ✔ ✔ Weave Scope / Weave Cloud 拓扑与可视化 ⚙️ 可选 ❌ ✔ 部分
性能监控平台部署 本节将基于 Prometheus + Grafana 搭建 Kubernetes 集群的性能监控平台。
版本说明 组件 版本 Kubernetes v1.19.10Prometheus v2.0.0Grafana v4.4.3NodeExporter v1.10.2
准备工作 拉取镜像 在 Kubernetes 部署服务时,为了避免部署过程中出现镜像拉取超时(Image Pull Timeout)的问题,建议:
提前将相关镜像预拉取到所有节点里面,确保部署时无需从远程仓库重新下载镜像; 或者搭建本地镜像仓库(比如 Harbor),提高镜像拉取的速度与可靠性。 在 Kubernetes 集群的所有节点上(包括 Master 和 Worker),分别执行以下命令,提前将镜像拉取到本地
1 2 3 4 5 6 7 8 docker pull prom/node-exporter:v1.10.2 docker pull prom/prometheus:v2.0.0 docker pull grafana/grafana:4.2.0
安装 CoreDNS 安装 Ingress 特别注意
建议先安装 CoreDNS,然后再安装 Ingress,因为 Ingress 在启动时需要解析域名,有时候会依赖集群 DNS 组件(比如 CoreDNS)。
部署步骤 Prometheus 部署 创建 YAML 配置文件 prometheus-config.yml,用于部署 Prometheus 的 ConfigMap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 apiVersion: v1 kind: ConfigMap metadata: name: prometheus-config namespace: kube-system data: prometheus.yml: | global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'kubernetes-apiservers' kubernetes_sd_configs: - role: endpoints scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - source_labels: [__meta_kubernetes_namespace , __meta_kubernetes_service_name , __meta_kubernetes_endpoint_port_name ] action: keep regex: default;kubernetes;https - job_name: 'kubernetes-nodes' kubernetes_sd_configs: - role: node scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name ] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics - job_name: 'kubernetes-cadvisor' kubernetes_sd_configs: - role: node scheme: https tls_config: ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token relabel_configs: - action: labelmap regex: __meta_kubernetes_node_label_(.+) - target_label: __address__ replacement: kubernetes.default.svc:443 - source_labels: [__meta_kubernetes_node_name ] regex: (.+) target_label: __metrics_path__ replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor - job_name: 'kubernetes-service-endpoints' kubernetes_sd_configs: - role: endpoints relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape ] action: keep regex: true - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme ] action: replace target_label: __scheme__ regex: (https?) - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path ] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__ , __meta_kubernetes_service_annotation_prometheus_io_port ] action: replace target_label: __address__ regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace ] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name ] action: replace target_label: kubernetes_name - job_name: 'kubernetes-services' kubernetes_sd_configs: - role: service metrics_path: /probe params: module: [http_2xx ] relabel_configs: - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe ] action: keep regex: true - source_labels: [__address__ ] target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target ] target_label: instance - action: labelmap regex: __meta_kubernetes_service_label_(.+) - source_labels: [__meta_kubernetes_namespace ] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_service_name ] target_label: kubernetes_name - job_name: 'kubernetes-ingresses' kubernetes_sd_configs: - role: ingress relabel_configs: - source_labels: [__meta_kubernetes_ingress_annotation_prometheus_io_probe ] action: keep regex: true - source_labels: [__meta_kubernetes_ingress_scheme ,__address__ ,__meta_kubernetes_ingress_path ] regex: (.+);(.+);(.+) replacement: ${1}://${2}${3} target_label: __param_target - target_label: __address__ replacement: blackbox-exporter.example.com:9115 - source_labels: [__param_target ] target_label: instance - action: labelmap regex: __meta_kubernetes_ingress_label_(.+) - source_labels: [__meta_kubernetes_namespace ] target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_ingress_name ] target_label: kubernetes_name - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape ] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path ] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__ , __meta_kubernetes_pod_annotation_prometheus_io_port ] action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 target_label: __address__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - source_labels: [__meta_kubernetes_namespace ] action: replace target_label: kubernetes_namespace - source_labels: [__meta_kubernetes_pod_name ] action: replace target_label: kubernetes_pod_name
创建 YAML 配置文件 prometheus-deploy.yml,用于部署 Prometheus 的 Deployment 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 apiVersion: apps/v1 kind: Deployment metadata: labels: name: prometheus-deployment name: prometheus namespace: kube-system spec: replicas: 1 selector: matchLabels: app: prometheus template: metadata: labels: app: prometheus spec: containers: - image: prom/prometheus:v2.0.0 name: prometheus command: - "/bin/prometheus" args: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--storage.tsdb.retention=24h" ports: - containerPort: 9090 protocol: TCP volumeMounts: - mountPath: "/prometheus" name: data - mountPath: "/etc/prometheus" name: config-volume resources: requests: cpu: 100m memory: 100Mi limits: cpu: 500m memory: 2500Mi serviceAccountName: prometheus volumes: - name: data emptyDir: {} - name: config-volume configMap: name: prometheus-config
创建 YAML 配置文件 prometheus-svc.yml,用于部署 Prometheus 的 Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Service metadata: labels: app: prometheus name: prometheus namespace: kube-system spec: type: NodePort ports: - port: 9090 targetPort: 9090 nodePort: 30003 selector: app: prometheus
创建 YAML 配置文件 prometheus-rbac.yml,用于对 Prometheus 进行 RBAC 授权 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: prometheus rules: - apiGroups: ["" ] resources: - nodes - nodes/proxy - services - endpoints - pods verbs: ["get" , "list" , "watch" ] - apiGroups: - extensions resources: - ingresses verbs: ["get" , "list" , "watch" ] - nonResourceURLs: ["/metrics" ] verbs: ["get" ] --- apiVersion: v1 kind: ServiceAccount metadata: name: prometheus namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: prometheus roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: prometheus subjects: - kind: ServiceAccount name: prometheus namespace: kube-system
通过上述的 YAML 配置文件,快速部署 Prometheus(注意 K8s 资源对象的部署顺序) 1 2 3 4 5 6 7 8 9 10 11 kubectl apply -f prometheus-config.yml kubectl apply -f prometheus-deploy.yml kubectl apply -f prometheus-svc.yml # RBAC 授权kubectl apply -f prometheus-rbac.yml
1 kubectl get pods -n kube-system -l app=prometheus
1 2 NAME READY STATUS RESTARTS AGE prometheus-68546b8d9-bg69k 1/1 Running 0 96s
1 kubectl get svc -n kube-system -l app=prometheus
1 2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE prometheus NodePort 10.0.0.21 <none> 9090:30003/TCP 2m12s
Grafana 部署 创建 YAML 配置文件 grafana-deploy.yml,用于部署 Grafana 的 Deployment 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 apiVersion: apps/v1 kind: Deployment metadata: name: grafana-core namespace: kube-system labels: app: grafana component: core spec: replicas: 1 selector: matchLabels: app: grafana component: core template: metadata: labels: app: grafana component: core spec: containers: - image: grafana/grafana:4.2.0 name: grafana-core imagePullPolicy: IfNotPresent resources: limits: cpu: 100m memory: 100Mi requests: cpu: 100m memory: 100Mi env: - name: GF_AUTH_BASIC_ENABLED value: "true" - name: GF_AUTH_ANONYMOUS_ENABLED value: "false" readinessProbe: httpGet: path: /login port: 3000 volumeMounts: - name: grafana-persistent-storage mountPath: /var volumes: - name: grafana-persistent-storage emptyDir: {}
创建 YAML 配置文件 grafana-svc.yml,用于部署 Grafana 的 Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: Service metadata: name: grafana namespace: kube-system labels: app: grafana component: core spec: type: NodePort ports: - port: 3000 selector: app: grafana component: core
创建 YAML 配置文件 grafana-ingress.yml,用于部署 Ingress 的路由规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: grafana namespace: kube-system spec: rules: - host: k8s.grafana.com http: paths: - path: / pathType: Prefix backend: service: name: grafana port: number: 3000
通过上述的 YAML 配置文件,快速部署 Grafana(注意 K8s 资源对象的部署顺序) 1 2 3 4 5 6 7 8 kubectl apply -f grafana-deploy.yml kubectl apply -f grafana-svc.yml kubectl apply -f grafana-ingress.yml
1 kubectl get pods -n kube-system -l app=grafana -o wide
1 2 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES grafana-core-6d6fb7566-5tv6t 1/1 Running 0 13m 10.244.0.16 k8s-node1 <none> <none>
1 kubectl get svc -n kube-system -l app=grafana
1 2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana NodePort 10.0.0.242 <none> 3000:31671/TCP 3m41s
1 kubectl get ingress -n kube-system
1 2 NAME CLASS HOSTS ADDRESS PORTS AGE grafana <none> k8s.grafana.com 80 86m
对外暴露节点 创建 YAML 配置文件 node-exporter.yml,用于对外暴露节点(Node) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: kube-system labels: k8s-app: node-exporter spec: selector: matchLabels: k8s-app: node-exporter template: metadata: labels: k8s-app: node-exporter spec: containers: - image: prom/node-exporter:v1.10.2 name: node-exporter ports: - containerPort: 9100 protocol: TCP name: http --- apiVersion: v1 kind: Service metadata: labels: k8s-app: node-exporter name: node-exporter namespace: kube-system spec: ports: - name: http port: 9100 nodePort: 31672 protocol: TCP type: NodePort selector: k8s-app: node-exporter
通过上述的 YAML 配置文件,对外暴露节点(Node) 1 2 kubectl apply -f node-exporter.yml
1 kubectl get pods -n kube-system -l k8s-app=node-exporter
1 2 3 4 5 NAME READY STATUS RESTARTS AGE node-exporter-2xgh6 1/1 Running 0 37s node-exporter-6r2jz 1/1 Running 0 37s node-exporter-l5hjf 1/1 Running 0 37s node-exporter-q5zc2 1/1 Running 0 37s
1 kubectl get svc -n kube-system -l k8s-app=node-exporter
1 2 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE node-exporter NodePort 10.0.0.118 <none> 9100:31672/TCP 5m16s
1 kubectl get ingress -n kube-system
1 2 NAME CLASS HOSTS ADDRESS PORTS AGE grafana <none> k8s.grafana.com 80 86m
验证步骤 通过 Service 访问 Grafana 首先,查看 Grafana 相关的 Service 1 kubectl get svc -n kube-system
1 2 3 4 5 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana NodePort 10.0.0.242 <none> 3000:31671/TCP 20m kube-dns ClusterIP 10.0.0.2 <none> 53/UDP,53/TCP,9153/TCP 8d node-exporter NodePort 10.0.0.153 <none> 9100:31672/TCP 38m prometheus NodePort 10.0.0.21 <none> 9090:30003/TCP 31m
通过任意一个集群节点的 IP 与 Grafana 的 Service 对外暴露的端口(比如 http://192.168.2.191:31671),就可以在 Kubernetes 集群外部通过浏览器访问 Grafana 的控制台页面(如下图所示)
特别注意
在 Kubernetes 集群外部,浏览器通过任意集群节点的 IP + Service 对外暴露的端口(NodePort)来访问 Grafana 的控制台页面(比如 http://192.168.2.191:31671),这并没有使用到 Ingress。
通过 Ingress 访问 Grafana 查看 Grafana 相关的 Ingress 路由规则 1 kubectl get ingress -n kube-system
1 2 NAME CLASS HOSTS ADDRESS PORTS AGE grafana <none> k8s.grafana.com 80 86m
查看 Nginx Ingress Controller 所在的节点(Node) 1 kubectl get pods -n ingress-nginx -o wide
1 2 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-ingress-controller-5dc64b58f-s82x6 1/1 Running 0 6m51s 192.168.2.131 k8s-node2 <none> <none>
在 K8s 集群外部的操作系统中,编辑系统配置文件 /etc/hosts,添加域名映射记录,其中 192.168.2.131 是 Nginx Ingress Controller 所在节点的 IP 地址(请自行更改 IP 地址 ) 1 192.168.2.131 k8s.grafana.com
在 K8s 集群外部的操作系统中,浏览器通过域名 k8s.grafana.com 访问 Ingress。如果可以成功访问 Grafana 的控制台页面(如下图所示),则说明 Ingress + Service + Grafana 可以正常运行
Grafana 展示监控数据图表 首先,查看所有 Service,记住 Prometheus 的 ClusterIP(比如 10.0.0.21) 1 kubectl get svc -n kube-system
1 2 3 4 5 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana NodePort 10.0.0.242 <none> 3000:31671/TCP 20m kube-dns ClusterIP 10.0.0.2 <none> 53/UDP,53/TCP,9153/TCP 8d node-exporter NodePort 10.0.0.153 <none> 9100:31672/TCP 38m prometheus NodePort 10.0.0.21 <none> 9090:30003/TCP 31m
登录 Grafana 的控制台页面,默认的登录用户名和密码分别是 admin / admin,成功登录后的控制台页面如下:
在 Grafana 的控制台页面中,添加相应的数据源,这里数据源地址里的 IP 是 Prometheus 的 Pod 在 K8s 集群内部的 IP 地址(ClusterIP),比如 10.0.0.21,端口号是 9090
在 Grafana 的控制台页面中,导入 DashBoard 模板
在模板输入框中填写数字 315(Grafana 模板的编号),然后让模板输入框失去焦点(或者点击 Load 按钮),等待一会模板信息就会加载出来
模板信息加载出来后,手动选择之前添加的数据源,然后点击 Import 按钮就可以导入模板
模板导入成功后,Grafana 会自动跳转到 DashBoard 页面
在 DashBoard 页面中,如果可以看到 K8s 集群的监控图表,则说明基于 Prometheus + Grafana 的 K8s 集群性能监控平台搭建成功 实战部署 Java 应用程序 本节将介绍如何在 Kubernetes 环境中实现 Java 项目的 CI/CD(持续集成与持续交付),其中 Docker 镜像仓库采用阿里云容器镜像服务(ACR)。
容器交付的完整流程 Java 项目开发 / 部署的流程
Kubernetes 部署项目的流程
Kubernetes 实现 CI / CD 的流程
创建 Java 应用的镜像 制作 Java 应用的镜像 运行环境说明
制作镜像的所有操作,都可以在任意一台安装了 JDK + Maven + Docker 的机器上执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 demojenkins ├── demojenkins.iml ├── Dockerfile ├── pom.xml └── src ├── main │ ├── java │ │ └── com │ │ └── clay │ │ └── demojenkins │ │ ├── controller │ │ │ └── UserController.java │ │ └── DemoJenkinsApplication.java │ └── resources │ └── application.yml └── test └── java └── com └── clay └── demojenkins └── DemoJenkinsApplicationTests.java
(2) 使用 Maven 命令将 Java 项目打包成可执行的 Jar 包或者 War 包(运行依赖 Tomcat 等外部 Web 容器),比如 demojenkins.jar 1 2 3 4 5 # cd demojenkins# mvn clean package
1 2 3 4 5 # cd demojenkins# docker build -t demojenkins:0.0.1 .
1 2 3 4 REPOSITORY TAG IMAGE ID CREATED SIZE demojenkins 0.0.1 51ecb07ed574 48 seconds ago 122MB openjdk 8-jdk-alpine a3562aa0b991 6 years ago 105MB ...
1 2 # docker run -d --name demojenkins -p 8111:8111 demojenkins:0.0.1
1 2 3 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 85d0ce0e5edd demojenkins:0.0.1 "java -jar /demojenk…" 7 seconds ago Up 6 seconds 0.0.0.0:8111->8111/tcp demojenkins ...
1 2 # curl http://127.0.0.1:8111/user/findAll
1 # docker rm -f demojenkins
推送 Java 应用的镜像 运行环境说明
推送镜像的所有操作,都可以在任意一台安装了 JDK + Maven + Docker 的机器上执行。
(3) 创建镜像仓库(比如 demojenkins),需要选择上面创建的命名空间,且代码源必须选择本地仓库
(4) 在终端中使用命令登录阿里云镜像仓库(用于登录的用户名为阿里云账号全名,密码为开通容器镜像服务时设置的密码,镜像仓库域名可以从阿里云特定镜像仓库的详情页面获取 ) 1 2 3 4 5 # docker login --username =xxxxx [镜像仓库域名]# docker login --username =xxxxx registry.cn-beijing.aliyuncs.com
1 2 3 4 5 # docker tag [镜像ID] [镜像仓库域名]/[命名空间]/[镜像名称]:[镜像版本号]# docker tag 51ecb07ed574 registry.cn-beijing.aliyuncs.com/java-dev/demojenkins:0.0.1
1 2 3 4 5 # docker push [镜像仓库域名]/[命名空间]/[镜像名称]:[镜像版本号]# docker push registry.cn-beijing.aliyuncs.com/java-dev/demojenkins:0.0.1
部署 Java 应用的镜像 运行环境说明
部署镜像所有操作,必须在 Kubernetes 集群的任意一个 Master 节点上执行。
(1) 在 Kubernetes 集群中,创建镜像拉取凭据 Secret(用于登录的用户名为阿里云账号全名,密码为开通容器镜像服务时设置的密码,邮箱地址可以自定义,镜像仓库域名可以从阿里云特定镜像仓库的详情页面获取 ) 1 2 3 4 5 6 # kubectl create secret docker-registry aliyun-regcred \ --docker -server=<镜像仓库域名> \ --docker -username=<登录用户名> \ --docker -password=<登录密码> \ --docker -email=<邮箱地址>
1 2 # kubectl get secret aliyun-regcred
1 2 NAME TYPE DATA AGE aliyun-regcred kubernetes.io/dockerconfigjson 1 14s
(2) 创建一个 YAML 配置文件(比如 demojenkins-deploy.yaml),用于部署 Deployment 和 Service,使用了阿里云镜像仓库中的 Docker 镜像 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 apiVersion: v1 kind: Service metadata: name: demojenkins spec: type: NodePort selector: app: demojenkins ports: - port: 80 targetPort: 8111 --- apiVersion: apps/v1 kind: Deployment metadata: name: demojenkins spec: replicas: 3 selector: matchLabels: app: demojenkins template: metadata: labels: app: demojenkins spec: containers: - name: demojenkins image: <镜像仓库域名>/<命名空间>/<镜像名称>:<镜像版本号> ports: - containerPort: 8111 imagePullSecrets: - name: aliyun-regcred
(3) 应用 YAML 配置文件,创建 Deployment 和 Service 资源对象 1 2 # kubectl apply -f demojenkins-deploy.yaml
1 2 3 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demojenkins NodePort 10.0.0.112 <none> 80:30577/TCP 2m25s kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 30h
1 2 # kubectl get deployments
1 2 NAME READY UP-TO-DATE AVAILABLE AGE demojenkins 3/3 3 3 171m
1 2 # kubectl get pods -o wide
1 2 3 4 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-5486657b45-9ck68 1/1 Running 0 169m 10.244.4.10 k8s-node1 <none> <none> demojenkins-5486657b45-n6t88 1/1 Running 0 169m 10.244.2.6 k8s-node3 <none> <none> demojenkins-5486657b45-v6zsl 1/1 Running 0 169m 10.244.3.8 k8s-node2 <none> <none>
1 2 3 4 5 # kubectl describe pod <pod-name># kubectl delete pod <pod-name>
(7) 在 Kubernetes 集群外部,通过 curl 网络工具访问容器内的 Java 应用(IP 可以是 Kubernetes 集群任意一个节点的 IP 地址,端口号可以通过 kubectl get svc 命令获取得到 ) 1 2 # curl http://<node-ip>:30577/user/findAll
(8) 若希望删除上面创建的 Secret、Deployment、Service、Pod 所有资源对象,可以执行以下命令 1 2 3 4 5 # kubectl delete -f demojenkins-deploy.yaml# kubectl delete secret aliyun-regcred
扩容 / 缩容 Java 应用程序 运行环境说明
扩容和缩容操作,必须在 Kubernetes 集群的任意一个 Master 节点上执行。
扩容 Java 应用程序 (1) 当完成上述步骤并将 Java 应用部署到 Kubernetes 集群后,可以通过以下命令调整该应用对应 Pod 的副本数量,实现扩容操作 1 2 3 4 5 # kubectl scale deployment <Deployment名称> --replicas =5# kubectl scale deployment demojenkins --replicas =5
1 2 # kubectl get pods -o wide
1 2 3 4 5 6 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-5486657b45-9ck68 1/1 Running 0 177m 10.244.4.10 k8s-node2 <none> <none> demojenkins-5486657b45-bkc6s 1/1 Running 0 52s 10.244.1.8 k8s-node1 <none> <none> demojenkins-5486657b45-n6t88 1/1 Running 0 177m 10.244.2.6 k8s-node3 <none> <none> demojenkins-5486657b45-v6zsl 1/1 Running 0 177m 10.244.3.8 k8s-node2 <none> <none> demojenkins-5486657b45-vjpxq 1/1 Running 0 58s 10.244.0.8 k8s-node1 <none> <none>
缩容 Java 应用程序 (1) 当完成上述步骤并将 Java 应用部署到 Kubernetes 集群后,可以通过以下命令调整该应用对应 Pod 的副本数量,实现缩容操作 1 2 3 4 5 # kubectl scale deployment <Deployment名称> --replicas =2# kubectl scale deployment demojenkins --replicas =2
1 2 # kubectl get pods -o wide
1 2 3 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-5486657b45-9ck68 1/1 Running 0 3h4m 10.244.4.10 k8s-node2 <none> <none> demojenkins-5486657b45-n6t88 1/1 Running 0 3h4m 10.244.2.6 k8s-node3 <none> <none>
升级 / 回滚 Java 应用程序 运行环境说明
升级和回滚操作,必须在 Kubernetes 集群的任意一个 Master 节点上执行。值得注意的是,这里的升级和回滚操作针对的是 Pod 所使用的 Docker 镜像版本,也就意味着在实际执行时,相当于对 Java 应用的版本进行升级或回退。
升级 Java 应用程序 1 2 # kubectl get pods -o wide
1 2 3 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-5486657b45-9ck68 1/1 Running 0 3h4m 10.244.4.10 k8s-node2 <none> <none> demojenkins-5486657b45-n6t88 1/1 Running 0 3h4m 10.244.2.6 k8s-node3 <none> <none>
(3) 当新版本镜像推送到阿里云仓库后,可以通过以下命令来升级 Java 应用对应 Pod 的版本(比如,升级到 0.0.2 版本) 1 2 3 4 5 # kubectl set image deployment <Deployment名称> <容器名称>=<镜像仓库域名>/<命名空间>/<镜像名>:<版本号># kubectl set image deployment demojenkins demojenkins=registry.cn-beijing.aliyuncs.com/java-dev/demojenkins:0.0.2
(4) 查看 Java 应用升级版本的状态(过程) 1 2 3 4 5 # kubectl rollout status deployment <Deployment名称># kubectl rollout status deployment demojenkins
1 2 3 4 5 6 Waiting for deployment "demojenkins" rollout to finish: 1 out of 2 new replicas have been updated... Waiting for deployment "demojenkins" rollout to finish: 1 out of 2 new replicas have been updated... Waiting for deployment "demojenkins" rollout to finish: 1 out of 2 new replicas have been updated... Waiting for deployment "demojenkins" rollout to finish: 1 old replicas are pending termination... Waiting for deployment "demojenkins" rollout to finish: 1 old replicas are pending termination... deployment "demojenkins" successfully rolled out
(5) 再次查看所有 Pod 的运行状态,发现 Pod 名称已经发生变化 1 2 # kubectl get pods -o wide
1 2 3 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-6c58b8c7b-mt8hp 1/1 Running 0 6m50s 10.244.1.9 k8s-node1 <none> <none> demojenkins-6c58b8c7b-tng8r 1/1 Running 0 6m20s 10.244.3.9 k8s-node2 <none> <none>
回滚 Java 应用程序 (1) 为了更好观察 Java 应用的回滚过程,先查看所有 Pod 的运行状态 1 2 # kubectl get pods -o wide
1 2 3 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-6c58b8c7b-mt8hp 1/1 Running 0 10m 10.244.1.9 k8s-node1 <none> <none> demojenkins-6c58b8c7b-tng8r 1/1 Running 0 10m 10.244.3.9 k8s-node2 <none> <none>
(2) 查看指定 Deployment 的所有历史版本 1 2 3 4 5 # kubectl rollout history deployment <Deployment名称># kubectl rollout history deployment demojenkins
1 2 3 4 deployment.apps/demojenkins REVISION CHANGE-CAUSE 1 <none> 2 <none>
(3) 当执行完上述步骤将 Java 应用 的版本从 0.0.1 升级到 0.0.2 后,若希望回滚到旧的版本(即用旧版本的 Pod 替换掉所有新版本的 Pod),可以执行以下命令 1 2 3 4 5 # kubectl rollout undo deployment <Deployment名称># kubectl rollout undo deployment demojenkins
1 2 3 4 5 # kubectl rollout undo deployment <Deployment名称> --to -revision=<版本># kubectl rollout undo deployment demojenkins --to -revision=1
(4) 再次查看所有 Pod 的运行状态,发现 Pod 名称已经发生变化 1 2 # kubectl get pods -o wide
1 2 3 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES demojenkins-5486657b45-knbnp 1/1 Running 0 66s 10.244.4.11 k8s-node2 <none> <none> demojenkins-5486657b45-md6lh 1/1 Running 0 64s 10.244.2.7 k8s-node3 <none> <none>
平滑重启 Java 应用程序 运行环境说明
平滑重启操作,必须在 Kubernetes 集群的任意一个 Master 节点上执行。
什么是平滑重启 在 Kubernetes 中实现 Java 应用程序的平滑重启,本质上是通过 Deployment 滚动更新(Rolling Update)的方式实现,也就是逐个重建 Pod,而不是将全部 Pod 停掉再重建,这样可以保证服务在重启过程中尽量不丢失请求。 所谓 Java 应用平滑重启(Graceful Restart)有两个关键点:(1) 不中断现有请求:Java 应用在收到 SIGTERM 信号时,应该先停止接收新请求,但允许正在处理的请求完成,再退出。 (2) 保证服务可用性:Kubernetes 通过 Deployment 的滚动更新或 Pod 生命周期钩子来保证至少有一部分实例在运行,避免服务全部不可用。 扩展阅读
更多关于 Kubernetes 如何实现 Java 应用程序平滑重启的详细介绍,可以看 这里 。
平滑重启的实现 (1) 手动触发 Deployment 滚动更新(Rolling Update),也就是说,Kubernetes 会逐个重建 Pod,而不是将全部 Pod 停掉再重建 1 2 3 4 5 # kubectl rollout restart deployment <Deployment名称># kubectl rollout restart deployment demojenkins
(2) 特别注意,光是依靠 kubectl rollout restart deployment <Deployment名称> 命令,不一定就可以真正实现 Java 应用的平滑重启,它是有几个前提条件的,详细说明请看 这里 。 参考资料