做运维这几年,我对K8s最深的感受不是它有多强,而是"默认状态太裸了"。裸到什么程度?想知道集群整体情况,得把kubectl get node、kubectl get po --all-namespaces一个个敲过去;线上流量突然涨起来,得人肉盯着再手动kubectl scale;部署一套应用,明明只是改了镜像版本,却要把十几个YAML从头到尾贴一遍。这三件事恰恰就是Dashboard、HPA、Helm分别解决的。这篇是我K8s系列的第十篇,前面的基础篇已经把Pod、Deployment、Service这些核心概念都过完了,这一篇开始进入运维工具层:装Dashboard做可视化,配HPA让应用自己感知负载,用Helm把YAML做成能打包、能回滚的Chart,最后用一个完整部署示例把它们串起来。适合已经能熟练操作kubectl、开始想把运维能力真正落地的读者。
1. 先说说这三个工具在我日常运维里的分工
很多人容易把这三种工具混在一起,觉得都是"管理K8s的",其实它们的职责边界非常清楚。我自己的理解是:Dashboard是眼睛,让人能"看清"集群;HPA是手,让应用能"自动干活";Helm是大脑加仓库,让一堆散装YAML变成可复用、有版本、能回滚的成品。它们配合起来,刚好覆盖了日常发布、观察、伸缩三条主线。
1.1 从复制粘贴YAML到组件化部署,我踩过的弯路
我刚接触K8s的时候,部署一个服务全靠复制别人的YAML,改改名字和镜像就往上扔。每天最常干的活儿是打开编辑器翻之前的文件,找image:那一行改版本号,然后kubectl apply -f。坏了就再改,改完再apply。这种模式在环境少、服务少的时候勉强能用,一旦你有一个开发环境、一个测试环境、一个生产环境,每个环境还独立命名空间,纯手动维护十几个YAML,早晚要出事。
出过的最大一次事故,是我给生产环境的某个服务升级镜像版本,改完image:之后忘了同步改资源配额,结果新版本Pod带着不合理的limits直接起不来,整个服务断了将近二十分钟。后来我复盘这件事,发现问题的根源不在于"操作失误",而在于YAML根本没有结构化管理:版本号、资源量、副本数全散落在不同文件里,没有一个统一的"参数入口"。这就是后面为什么Helm对我冲击那么大——它不改变K8s资源本身,但它改变了人管理资源的方式。
1.2 三件工具覆盖的运维场景图谱
先说Dashboard。它解决的问题很朴素:你不是随时都能记住几十个资源的状态,尤其是非技术同事或者临时顶班的同学,他们用命令行去看复杂资源列表很不现实。Dashboard把节点状态、工作负载、存储、日志这些信息汇总成可视化的界面,操作还能直接落到API Server上。
再说HPA。很多应用并非一直平稳,早晚高峰流量差异很大。传统做法是人盯监控、手动扩容,但半夜三点流量涨了,靠人肉去执行kubectl scale根本不现实。HPA就是让K8s自己盯着指标,超过阈值加Pod,低于阈值收Pod。
最后是Helm。它解决的是K8s资源怎么打包、怎么复用、怎么发布的问题。比如一套应用由Deployment、Service、ConfigMap、HPA五个资源组成,这五个文件放一起打成一个包,这个包就能在不同环境里用不同参数反复安装——这种能力,无论kubectl还是Dashboard都给不了你。
2. Dashboard上手指南:部署、安全登录与常见故障
这一章我直接讲实操。Dashboard的部署在官方仓库里有整合好的YAML,但实际落地时要注意几个点,不然很容易装完打不开或者打开了有各种毛病。
2.1 两行命令装好面板,但镜像源要提前想清楚
我用的是v2.7.0版本的推荐部署文件,命令很简单:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml这条命令会在kubernetes-dashboard命名空间下部署好所有相关资源,包括Deployment、Service、ServiceAccount和一些默认的RBAC授权。装完检查一下:
kubectl -n kubernetes-dashboard get pods如果状态显示Running,说明控制面已经起来了。这里要提醒的是镜像拉取问题。Dashboard的镜像仓库在kubernetes.gcr.io,国内网络环境经常拉不动,你可能会看到Pod卡在ImagePullBackOff状态。这种情况我一般有三种处理方式:
- 登录到能访问外网的机器,用
docker pull把镜像拉下来再docker tag改成目标仓库地址,推送到自己内网仓库; - 直接把Deployment镜像地址改到可用的国内镜像源,很多云厂商有同步镜像;
- 如果集群本来就有代理能力,配置好节点的
/etc/docker/daemon.json中的镜像代理。
我自己的习惯是第二种最省事,改一下kubectl -n kubernetes-dashboard edit deployment kubernetes-dashboard里的image:字段即可。
2.2 创建只读或管理员账号,用Token登录而不是绕过登录
Dashboard默认不对外开放,也不会自动创建带权限的用户,这是好事,安全上不建议图省事直接开--enable-skip-login或者是匿名登录。正确做法是创建专门的服务账号。
我日常用的账号分两种:一种是管理员账号,方便全集群排查问题;一种是只读账号,给过来观察环境的同事用。管理员账号的创建方式如下:
kubectl -n kubernetes-dashboard create serviceaccount admin-user kubectl -n kubernetes-dashboard create clusterrolebinding admin-user --clusterrole=cluster-admin --serviceaccount=kubernetes-dashboard:admin-user然后获取登录Token:
kubectl -n kubernetes-dashboard create token admin-user这里注意,新版本Kubernetes里kubectl -n kubernetes-dashboard get secret那套老办法在很多环境已经不适用了,因为create token是更受推荐的方式。拿到那一长串Token,在Dashboard登录页选"Token"方式粘贴进去就能进。
访问地址方面,由于Dashboard的Service默认是ClusterIP类型,我通常用三种方式之一访问:临时用kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard 8443:443;或者在自己维护的Ingress网关上加一个域名;或者改Service为NodePort。个人建议是开发环境用port-forward最干净,不往集群上多开端口。
2.3 CPU和内存图表为空:多半是集群里没有metrics-server
第一次打开Dashboard的人经常发现节点和Pod的CPU、内存图表全是空的,页面右上角还会显示"Metrics未提供"相关的提示。这个坑的原因很明确:集群里没有部署metrics-server,Dashboard拿不到监控数据。
解决办法是装metrics-server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml装好之后用kubectl top nodes验证,如果能看到每个节点的CPU和内存使用量,Dashboard图表也就会跟着有数据了。这一步不仅Dashboard需要,第三章讲HPA也完全依赖它,建议提前装好。
3. HPA的完整链路:指标怎么来、副本数怎么算、为什么不能乱配参数
HPA(HorizontalPodAutoscaler)是K8s里最容易被"照着文档配置但没吃透"的功能之一。很多人配完,压测时发现半天不扩容,或者扩容了过一会儿又疯狂缩容,就是因为没理解它背后那条完整链路。
3.1 HPA不是玄学,它只是不断执行一个公式的控制器
HPA从本质上看是一个循环控制器,每15秒做一次下面的流程:通过Metrics API拿到当前Pod的指标数据;根据当前副本数、当前指标值、目标指标值算出期望副本数;如果期望副本数和当前副本数不同,就更新Deployment的replicas字段,剩下的活由ReplicaSet去干。
期望副本数的核心公式是:
desiredReplicas = ceil(currentReplicas * (currentMetric / desiredMetric))举个例子:当前有2个Pod,每个Pod CPU使用率为80%,目标阈值是50%,那么期望副本数就是ceil(2 * (80 / 50)) = ceil(3.2) = 4。这个公式看起来很简单,但实际生产环境里,指标数据的时延、默认采样窗口、缩容稳定窗口等参数都会影响最终结果,这也是我们说"配置参数不能乱来"的原因。
3.2 从零到一配置HPA,注意requests是前提条件
要让HPA的CPU指标生效,有一个常被忽略的前提:Deployment里的容器必须设置requests.cpu。没有requests,HPA拿不到百分比数据,TARGETS列会显示<unknown>。我见过不少人只设了limits忘了requests,结果HPA配了等于没配。
先部署一个带requests的测试服务:
apiVersion: apps/v1 kind: Deployment metadata: name: php-apache spec: selector: matchLabels: app: php-apache template: metadata: labels: app: php-apache spec: containers: - name: php-apache image: registry.cn-hangzhou.aliyuncs.com/xxx/php-apache:latest ports: - containerPort: 80 resources: requests: cpu: 200m memory: 128Mi然后创建HPA:
kubectl autoscale deployment php-apache --cpu-percent=60 --min=1 --max=5查看状态:
kubectl get hpa上面版本默认走的是autoscaling/v2,显示信息更全,包括目标值、当前值、最小/最大副本数。
3.3 压测验证扩容和缩容,注意冷却窗口
创建完HPA后我习惯用kubectl get hpa -w盯实时变化,同时起一个压测工具打流量。这里以ab为例:
ab -n 500000 -c 300 http://<service-ip>/压测起来后,HPA的TARGETS列会逐渐显示CPU百分比超过阈值,比如260%/60%,然后REPLICAS从1变成2、3、4、5。这个过程不是瞬时的,一个是因为HPA控制器默认同步周期是15秒,另外每次变更副本数后要等指标重新采集、计算,K8s还加了一个扩容稳定窗口避免抖动。
说到缩容,大家更要心里有数:HPA默认的缩容稳定窗口是5分钟。意思是说,就算你的CPU使用率已经降到0,它也会先观察5分钟,确认持续低负载才逐步减少副本数,而且每次缩容也是逐步收。这样做是因为如果流量是周期性的,快速缩下去又快速扩上来,对系统来说比多留着几个Pod成本更高。所以当你看到CPU已经很低但Pod数量迟迟没降下来,别慌,那是正常设计。
3.4 自定义指标与ClusterAutoscaler的边界
CPU和内存指标是HPA最常用的,但有些场景下CPU并不能反映真实压力,比如网关类服务主要看QPS、连接数,这时候就需要自定义指标了。做法一般是在集群里部署Prometheus Adapter,让它把Prometheus里的业务指标暴露成custom.metrics.k8s.io接口,HPA就能通过type: Object或type: Pods引用这些指标。
这里必须提一个容易搞混的边界:HPA只管应用层的Pod副本数量,不管集群节点数量。如果你的业务高峰期需要40个Pod,但集群总共只有3台节点,每台节点只能跑8个Pod,那么HPA最多把Pod扩到上限24个就扩不动了。想增加节点,这是ClusterAutoscaler的职责。两者一个管Pod,一个管Node,互为补充,但没有替换关系。
4. Helm的Chart骨架与模板渲染机制拆解
Helm对没接触过包管理概念的K8s使用者来说,刚开始会觉得"这不就是多了一层封装吗"。对,它确实是一层封装,但这层封装解决的问题恰好是原生kubectl最薄弱的环节。
4.1 Chart就是"应用说明书加可执行模板"
一个Helm Chart从结果来看就是一个安装包,里面包含了描述应用该用什么镜像、几个副本、什么端口、什么环境变量的"说明书",也包含把这些说明变成K8s真实资源YAML的模板。你拿到一个Chart,不管内部多复杂,执行helm install就自动把整套资源创建好。不同环境之间的差异通过values.yaml动态注入,不需要你手动去改模板代码。
我类比一下:kubectl apply像是拿着一堆散装零件清单去仓库自己拼一台机器,Helm则是一个完整的"产品安装包",里面带安装脚本、配置文件和回滚工具。生产环境中,没有人会愿意手工维护两三百个YAML而没有版本管理能力。
4.2 Chart目录结构:十个文件里我们真正需要的几个
helm create生成的Chart结构里有一些默认文件,刚上手容易看懵。拆解来看:
my-chart/ ├── Chart.yaml ├── values.yaml ├── charts/ ├── templates/ │ ├── deployment.yaml │ ├── service.yaml │ ├── hpa.yaml │ ├── _helpers.tpl │ └── NOTES.txt └── .helmignoreChart.yaml是Chart的身份证,声明名字、版本、依赖等;values.yaml是所有可变参数的默认值,比如镜像地址、副本数、资源配额;templates/是资源模板真正所在的地方;_helpers.tpl是模板辅助函数,用于生成统一的标签、名字之类的公共部分;NOTES.txt是安装完成后打印给用户看的提示信息,比如怎么访问服务。
初学阶段最需要操作的就是Chart.yaml、values.yaml和templates下的三个文件。charts/目录是放置子Chart依赖用的,不是直接用。
4.3 templates目录里的语法:values、with、default、quote
很多人学Helm卡在模板语法上。我的建议是别背,理解几个核心动作就够了。
最常用的是{{ .Values.xxx }},这表示把values.yaml里的xxx字段的值插入到这个位置。例如:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Values.name }} spec: replicas: {{ .Values.replicaCount }} template: spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"再看几个常用函数。default用于提供默认值,比如{{ .Values.image.tag | default "latest" }},意思是如果values.yaml里没定义tag就用latest。quote用于强制把值渲染成字符串,比如{{ .Values.nodeSelector.zone | quote }}。with用来缩小变量作用域,让后面一段内部不用反复写前缀。管道符|类似于命令行里的管道,把前面的值传给后面的函数处理。
调试模板最常用的命令是这套组合:
helm template ./my-chart helm template ./my-chart --debug helm install my-release ./my-chart --dry-run --debughelm template直接渲染出最终YAML但不执行部署;加--debug能看到渲染过程里的详细信息;--dry-run则会在API Server上做一次模拟,能检查出很多纯本地渲染发现不了的校验问题。我编写任何Chart都会先跑一遍这三条,确认无误之后再真的install。
4.4 Release、Revision、Rollback:Helm比kubectl多出来的能力
Helm把一次安装称作一次Release,同一个Chart可以装成很多个Release,比如web-app-prod、web-app-test。每次helm upgrade都会生成一个新的修订版本,也就是Revision。这些历史版本可以查看,也可以一键回滚。这是原生kubectl做不到的,因为它根本没有"发布单元"的概念。
实际操作里,我常用这套命令:
helm install web-app ./my-chart --namespace default helm upgrade web-app ./my-chart --set image.tag=v2 helm history web-app helm rollback web-app 1 helm uninstall web-apphelm history能看到每次发布的时间、版本号、状态和说明;helm rollback web-app 1直接把整个Release回退到第一次安装时的状态,比手动改YAML再apply快得多,也安全得多。
5. 实战:把一个Web应用和HPA一起打包成Chart并完成发布回滚
前面对原理做了拆解,这一章我用一个完整的过程演示怎么从零做一个Chart,把Deployment、Service、HPA一起参数化,然后走一遍安装、升级、回滚。这里选的应用只是一个简单的Web服务,你替换成自己的业务镜像即可。
5.1 用helm create生成骨架,先删掉默认模板
helm create demo-app这条命令会创建demo-app目录,里面自带了nginx相关的默认模板。我一般会清掉templates/里用不到的文件,只保留_helpers.tpl、hpa.yaml、deployment.yaml、service.yaml,再改Chart.yaml:
apiVersion: v2 name: demo-app description: A demo web application with HPA enabled type: application version: 0.1.0 appVersion: "1.0.0"version是Chart的版本,appVersion是业务应用的版本,两者可以不同。
5.2 values.yaml:把镜像、副本数、资源、伸缩参数全部抽出来
我把所有可能要按环境变化的参数都放进values.yaml,这样不同环境部署时只需要维护一份配置值,而不需要改模板代码:
replicaCount: 1 image: repository: nginx tag: "1.25" pullPolicy: IfNotPresent name: demo-app service: type: ClusterIP port: 80 targetPort: 80 resources: requests: cpu: 200m memory: 128Mi autoscaling: enabled: true minReplicas: 1 maxReplicas: 5 targetCPUUtilizationPercentage: 60这里把HPA的开关、上下限、CPU阈值都参数化了。某些环境如果你不想启用自动伸缩,把autoscaling.enabled设为false,HPA模板就不会被渲染出来。
5.3 编写deployment.yaml、service.yaml和hpa.yaml模板
templates/deployment.yaml的关键部分这样写:
apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Values.name }} labels: app: {{ .Values.name }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: app: {{ .Values.name }} template: metadata: labels: app: {{ .Values.name }} spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: {{ .Values.service.targetPort }} resources: requests: cpu: {{ .Values.resources.requests.cpu }} memory: {{ .Values.resources.requests.memory }}Service模板:
apiVersion: v1 kind: Service metadata: name: {{ .Values.name }} spec: type: {{ .Values.service.type }} selector: app: {{ .Values.name }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }}HPA模板:
{{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ .Values.name }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ .Values.name }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }}这里值得强调的是{{- if ... }}和{{- end }}的语法,横杠的作用是去掉左右两侧的空白行,避免渲染出无意义的空行。HPA的开关逻辑就靠这个条件块。
5.4 本地渲染检查,再正式安装
写完后先检查再安装:
helm lint ./demo-app helm template ./demo-app helm install demo ./demo-app --namespace demo --create-namespace --dry-run --debughelm lint会检查Chart里的格式和常见问题;helm template直接看渲染的YAML有没有错误;第三行是在API Server上做模拟安装。确认没问题后去掉--dry-run真正执行:
helm install demo ./demo-app --namespace demo --create-namespace查看结果:
helm list -n demo kubectl -n demo get pods,svc,hpa正常情况下Pods运行,Service创建好,HPA自动绑定到Deployment上。
5.5 发布v2镜像,发现故障后快速回滚
现在模拟一次升级。假设业务新版本镜像tag是1.26,我用--set直接覆盖参数,不动文件:
helm upgrade demo ./demo-app --namespace demo --set image.tag=1.26升级完成后我故意查看Pod状态,发现新版本镜像启动失败,Pod处于CrashLoopBackOff状态。这时候回滚:
helm history demo -n demo helm rollback demo 1 -n demohelm rollback demo 1会把Release回退到第一次安装的修订版本。执行后等十几秒再看kubectl -n demo get pods,Pod恢复Running,服务恢复正常。整个发布、发现故障、回滚的过程,不超过两分钟。这在手动维护YAML的工作流里是难以想象的。
6. 三个工具混合使用的经验提醒
这套组合我实际用下来有个体会:Dashboard、HPA、Helm不是孤立的,它们之间有很多隐性的配合关系。比如Dashboard上的数据来自metrics-server,HPA的CPU指标也来自metrics-server,所以如果你在Dashboard上看到的Pod CPU为0,而HPA却显示CPU超标,多半不是两边的数据矛盾,而是采样窗口不同导致的,先看集群里有没有多套指标服务,再排查具体应用。
再比如,用Helm管理Dashboard本身也很常见。你可以把Dashboard作为一个Chart装到集群里,这样升级、回滚都和业务应用用同一套Helm流程。我个人在管理多个集群时,会把所有基础组件,包括metrics-server、Dashboard、Ingress Controller,都做成Helm Chart放进统一的Git仓库,用版本号追踪每次变更。这样配合HPA的自动伸缩策略,再加上Dashboard的可视化排障,K8s集群的日常运维压力会小很多。
最后分享一个我踩过的坑:HPA阈值设得太低,应用高峰时副本数直接冲上最大值,每个Pod又设置了很高的requests.cpu,结果节点总CPU容量打满,Pod调度不上去,页面反而更慢。后来我把阈值调整到60%到70%,同时给HPA的maxReplicas设置了一个合理的业务上界,观察了几轮高峰期,系统才稳定下来。所以不要照搬文档参数,阈值和副本数上下限都要结合自己业务的真实流量曲线去调,调完记得用压测或者自然流量验证一个完整周期。