1. 从手工YAML到包管理:Helm到底解决了什么问题
如果你维护过超过三个微服务的Kubernetes集群,大概率经历过这样的场景:一套Redis高可用集群,StatefulSet、Headless Service、ConfigMap、Secret、PDB、ServiceMonitor加起来七八个YAML文件,每个文件里还有一堆需要根据环境改动的镜像地址、存储类名、资源限制。开发环境跑通了,推到测试环境改一遍参数,上生产再改一遍,改到最后自己都记不清哪个值对应哪个环境。更别提版本回滚——kubectl apply了一堆文件之后想退回上一版,只能靠翻Git记录手动比对。
Helm就是冲着这个痛点来的。一句话概括:Helm是Kubernetes的包管理器,它把一组相关的K8s资源清单打包成一个可版本化、可参数化、可复用、可回滚的"Chart"。你可以把它类比成Linux世界的apt或yum,只不过安装的不是单个二进制,而是一整套协同工作的集群资源。
这篇文章我打算把Helm的核心概念、模板机制、依赖管理讲透,然后手把手用Bitnami的Redis Chart部署一套生产可用的高可用集群,包括哨兵模式、持久化、密码管理、资源限制、Pod反亲和这些关键配置。适合已经会用kubectl但还没系统用过Helm的运维和开发,也适合用过Helm但一直停留在helm install层面、没深挖过模板和依赖的同学。
先给个心理预期:Helm 3和Helm 2的差异非常大,网上很多老教程还在讲Tiller,那套东西早就废弃了。本文所有内容基于Helm 3,如果你还在用Helm 2,建议先升级,否则下面很多操作对不上。
2. Helm核心概念拆解:Chart、Release、Repository到底是什么关系
2.1 三个核心名词的通俗解释
刚接触Helm的人最容易被Chart、Release、Repository这三个词绕晕。我用做菜打个比方:
- Chart是菜谱。它描述了做一道菜需要哪些食材(K8s资源)、每种食材放多少(默认参数)、哪些步骤可以调整(可配置项)。菜谱本身是静态的,放在那里不会自己变成菜。
- Release是按菜谱实际做出来的一份菜。同一个菜谱你可以做十份,每份可以微调咸淡(通过values覆盖),每份都是独立的Release,互不干扰。Release是Helm管理的真正单位,安装、升级、回滚都是针对Release的。
- Repository是菜谱合集,就是一个HTTP服务器,上面托管了一堆打包好的Chart,你可以
helm repo add把它加进来,然后搜索、安装里面的Chart。
理解这三者的关系,后面所有操作就顺了。helm install的本质是:从Repository拉取Chart,用你提供的values渲染成最终的K8s YAML,提交给API Server,同时记录一个Release。
2.2 Helm 3的架构变化:为什么去掉了Tiller
Helm 2时代有个叫Tiller的服务端组件跑在集群里,helm客户端通过gRPC跟它通信,由它来执行实际的资源操作。这套设计带来两个大问题:一是Tiller默认拥有cluster-admin权限,安全风险极高;二是多租户场景下权限隔离很难做。
Helm 3直接把Tiller砍了,客户端直接用你本地的kubeconfig跟API Server通信,权限模型完全交给K8s的RBAC。这个变化的意义在于:你在集群里有什么权限,Helm就能做什么,不多不少。同时Release的元数据从ConfigMap迁移到了Secret(默认),因为Secret里可以放敏感信息且支持加密。
还有一个变化是Release的作用域从全局变成了命名空间级别。Helm 2里Release名字全局唯一,Helm 3里同一个名字可以在不同namespace共存,这更符合K8s的隔离理念。
2.3 Chart的目录结构长什么样
一个标准Chart的目录结构是这样的:
my-chart/ ├── Chart.yaml # Chart的元数据:名称、版本、appVersion、依赖 ├── values.yaml # 默认配置值 ├── values.schema.json # 可选,values的JSON Schema校验 ├── charts/ # 子Chart(依赖) ├── templates/ # 模板目录,渲染成K8s YAML │ ├── deployment.yaml │ ├── service.yaml │ ├── _helpers.tpl # 可复用的模板片段 │ ├── NOTES.txt # 安装后显示的提示信息 │ └── tests/ └── .helmignore # 打包时忽略的文件Chart.yaml里几个字段要特别注意:version是Chart本身的版本,遵循SemVer;appVersion是里面打包的应用版本,跟Chart版本是两回事。比如Bitnami的Redis Chart可能version是18.0.0,appVersion是7.2.4,意思是这个Chart模板结构是第18版,里面装的Redis是7.2.4。升级Chart和升级应用是两码事,这个区分在排查问题时很关键。
templates/目录下的文件不是合法的YAML,而是Go template。渲染引擎会把{{ }}里的内容替换掉,生成最终YAML。_helpers.tpl以下划线开头,不会被渲染成独立资源,只用来定义可复用的命名模板。
3. 模板引擎与values覆盖机制:Helm真正的威力所在
3.1 Go template基础语法速通
Helm的模板基于Go的text/template,加上Sprig函数库和Helm自己的扩展。核心语法就几个:
{{ .Values.xxx }}:引用values里的值{{ .Release.Name }}:内置对象,当前Release的名字{{ .Chart.Name }}:Chart名称{{ include "chart.fullname" . }}:调用命名模板{{ if ... }}...{{ else }}...{{ end }}:条件判断{{ range ... }}...{{ end }}:循环{{ default "值" .Values.xxx }}:默认值{{ quote .Values.xxx }}:加引号{{ toYaml .Values.xxx | nindent 4 }}:把结构体转成YAML并缩进
举个实际例子,一个Deployment的镜像字段通常这么写:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"这行的意思是:镜像仓库从values取,tag如果没指定就用Chart的appVersion兜底。这种写法让Chart在默认情况下能跑,同时允许用户覆盖。
3.2 values的优先级与覆盖规则
Helm的values来源有多个,优先级从低到高是:
- Chart自带的
values.yaml - 父Chart通过
dependencies传给子Chart的values - 用户通过
-f指定的values文件(多个文件按顺序覆盖) - 用户通过
--set指定的单个值
--set的语法要小心,点号表示层级,逗号分隔多个,数组用花括号:
helm install my-redis bitnami/redis \ --set architecture=replication \ --set auth.password=MySecretPass \ --set master.persistence.size=20Gi但--set有个坑:它会把所有值当字符串处理,遇到布尔值和数字容易出问题。比如--set master.persistence.enabled=false,Helm会尝试解析成布尔,但复杂嵌套时经常翻车。我的经验是:超过五个参数就别用--set了,老老实实写values文件,可读、可版本控制、可复用。
3.3 命名模板与_helpers.tpl的复用技巧
_helpers.tpl是Chart作者写给自己用的工具库。最常见的几个命名模板:
{{- define "redis.fullname" -}} {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}} {{- end -}}这个模板生成一个不超过63字符的资源名(K8s对name长度有限制)。用include调用:
metadata: name: {{ include "redis.fullname" . }}注意include和template的区别:template是Go原生指令,不能用在管道里;include是Helm扩展的,可以接管道做后续处理,比如{{ include "redis.fullname" . | quote }}。写Chart时一律用include,别用template,这是社区共识。
4. 依赖管理:一个Chart如何优雅地引用另一个Chart
4.1 dependencies声明与Chart.yaml
复杂应用的Chart往往依赖其他Chart。比如你要部署一个带监控的Redis,可能依赖Redis Chart和Exporter Chart。依赖在Chart.yaml里声明:
apiVersion: v2 name: my-app version: 1.0.0 dependencies: - name: redis version: "18.x.x" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled - name: common version: "2.x.x" repository: "https://charts.bitnami.com/bitnami"condition字段很实用,它指向values里的一个布尔值,用户可以通过redis.enabled=false来禁用这个依赖。tags字段也能做类似的事,用于批量控制一组依赖。
声明完依赖后要执行helm dependency update,它会把依赖Chart下载到charts/目录,并生成Chart.lock锁定版本。Chart.lock一定要提交到Git,它保证团队每个人构建时用的依赖版本一致,跟package-lock.json是一个道理。
4.2 子Chart的values传递
父Chart的values里,可以用子Chart的名字作为key来传值:
redis: architecture: replication auth: password: xxx这段values会自动传给名为redis的子Chart。但要注意:子Chart只能看到自己那部分values,看不到全局。如果子Chart需要全局值,得用global字段:
global: imageRegistry: my-registry.comglobal下的值对所有Chart可见,适合放镜像仓库、环境标签这类全局配置。
4.3 依赖的别名与多实例
有时候你需要同一个Chart的多个实例,比如两套Redis集群。这时用alias:
dependencies: - name: redis alias: cache-redis version: "18.x.x" repository: "https://charts.bitnami.com/bitnami" - name: redis alias: session-redis version: "18.x.x" repository: "https://charts.bitnami.com/bitnami"然后在values里分别用cache-redis和session-redis作为key传值。这个技巧在多环境、多租户场景下非常有用。
5. 实战:用Bitnami Chart部署Redis高可用集群
5.1 环境准备与仓库添加
先确认Helm版本,建议3.12以上:
helm version添加Bitnami仓库并更新索引:
helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update搜索Redis Chart确认可用:
helm search repo bitnami/redis --versions | head -20你会看到一长串版本,选最新的稳定版即可。注意别选带-debian-12之类后缀的,那是镜像变体,不是Chart版本。
5.2 Redis高可用的架构选型:主从+哨兵 vs Cluster
Bitnami的Redis Chart支持三种架构,通过architecture参数控制:
| architecture值 | 架构 | 适用场景 | 最小节点数 |
|---|---|---|---|
| standalone | 单节点 | 开发测试 | 1 |
| replication | 主从+哨兵 | 生产读写分离、自动故障转移 | 3 |
| cluster | Redis Cluster分片 | 超大数据量、水平扩展 | 6 |
生产环境如果数据量在几十GB以内、QPS不是特别夸张,replication模式(主从+哨兵)是性价比最高的选择。它提供自动故障转移,读可以走从节点,运维复杂度远低于Cluster。Cluster虽然能水平扩展,但跨slot操作受限、客户端兼容性要求高,不是所有业务都适合。
下面我以replication模式为例,部署一套3节点(1主2从)+3哨兵的高可用集群。
5.3 编写生产级values文件
新建redis-ha-values.yaml:
architecture: replication auth: enabled: true password: "ChangeMe_Str0ng_P@ss" sentinel: true master: count: 1 persistence: enabled: true size: 20Gi storageClass: "fast-ssd" resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2000m memory: 4Gi podAntiAffinity: enabled: true replica: replicaCount: 2 persistence: enabled: true size: 20Gi storageClass: "fast-ssd" resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2000m memory: 4Gi podAntiAffinity: enabled: true sentinel: enabled: true quorum: 2 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi metrics: enabled: true serviceMonitor: enabled: true podDisruptionBudget: enabled: true minAvailable: 2 persistence: enabled: true storageClass: "fast-ssd"逐项解释关键参数:
auth.sentinel: true让哨兵也用密码认证,否则哨兵和Redis之间的通信会失败。这个坑我踩过,当时哨兵一直报NOAUTH,排查了半天。
master.count: 1和replica.replicaCount: 2构成1主2从。哨兵需要奇数个节点做quorum投票,默认3个哨兵,quorum: 2表示至少2个哨兵认为主挂了才触发故障转移。
podAntiAffinity.enabled: true让Pod尽量分散到不同节点,避免单节点故障导致整个集群挂掉。生产环境这个必须开,否则三个Pod可能全调度到同一台机器上。
podDisruptionBudget.minAvailable: 2保证滚动更新或节点维护时至少2个Pod可用,防止集群整体不可用。
5.4 安装与验证
安装Release:
kubectl create namespace redis-ha helm install redis-ha bitnami/redis \ -n redis-ha \ -f redis-ha-values.yaml \ --timeout 10m--timeout 10m是因为拉镜像和初始化可能比较慢,默认5分钟有时不够。
安装完成后查看Release状态:
helm list -n redis-ha helm status redis-ha -n redis-ha查看Pod:
kubectl get pods -n redis-ha -o wide正常应该看到1个master、2个replica、3个sentinel,共6个Pod,且分布在不同的节点上。
验证主从复制:
# 获取密码 export REDIS_PASSWORD=$(kubectl get secret redis-ha -n redis-ha -o jsonpath="{.data.redis-password}" | base64 -d) # 连到master写入 kubectl exec -it redis-ha-master-0 -n redis-ha -- redis-cli -a $REDIS_PASSWORD SET testkey "hello" # 连到replica读取 kubectl exec -it redis-ha-replicas-0 -n redis-ha -- redis-cli -a $REDIS_PASSWORD GET testkey如果replica能读到master写入的值,说明复制正常。
验证哨兵:
kubectl exec -it redis-ha-sentinel-0 -n redis-ha -- redis-cli -p 26379 -a $REDIS_PASSWORD SENTINEL get-master-addr-by-name mymaster应该返回当前master的IP和端口。
5.5 故障转移演练
光看状态不够,得实际演练一次故障转移。手动删掉master Pod:
kubectl delete pod redis-ha-master-0 -n redis-ha观察哨兵日志:
kubectl logs -f redis-ha-sentinel-0 -n redis-ha你会看到+switch-master事件,哨兵在几十秒内选出新的master。再次查询master地址,应该已经变了。原来的master Pod重建后会作为replica重新加入。
这个演练很重要,很多团队部署完Redis HA就以为万事大吉,结果真出故障时才发现哨兵配置有问题、客户端没适配。建议上线前至少演练三次,确认故障转移时间和客户端重连逻辑符合预期。
6. 升级、回滚与调试:Helm日常运维三板斧
6.1 升级Release的正确姿势
改完values文件后升级:
helm upgrade redis-ha bitnami/redis \ -n redis-ha \ -f redis-ha-values.yaml \ --atomic \ --timeout 10m--atomic是个好东西:如果升级失败,Helm会自动回滚到上一个版本。没有这个参数的话,升级失败会留下一个半死不活的Release,还得手动处理。
升级前建议先--dry-run看看渲染出来的YAML:
helm upgrade redis-ha bitnami/redis \ -n redis-ha \ -f redis-ha-values.yaml \ --dry-run --debug > rendered.yaml--debug会输出完整的渲染结果,方便你检查模板有没有问题。这个习惯能帮你避免90%的升级事故。
6.2 回滚与历史查看
查看Release历史:
helm history redis-ha -n redis-ha回滚到指定版本:
helm rollback redis-ha 2 -n redis-ha回滚本质上是把旧版本的values和Chart重新渲染一遍再apply,所以它不会恢复数据。如果升级过程中数据被迁移或删除了,回滚救不回来,这点要清楚。
6.3 调试模板渲染问题
模板渲染出错时,helm template是最直接的排查工具:
helm template redis-ha bitnami/redis -n redis-ha -f redis-ha-values.yaml它不连接集群,只在本地渲染,报错信息比helm install清晰得多。如果某个字段渲染不对,可以配合--show-only只看某个文件:
helm template redis-ha bitnami/redis -n redis-ha -f redis-ha-values.yaml \ --show-only templates/master/statefulset.yaml另一个常用技巧是查看Release的实际values(合并了默认值和你的覆盖):
helm get values redis-ha -n redis-ha helm get values redis-ha -n redis-ha --all--all会显示所有值,包括Chart默认值,方便你确认某个参数到底生效了没有。
7. 常见问题与避坑经验实录
7.1 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| Pod一直Pending | 存储类不存在或资源不足 | kubectl describe pod看Events |
| 哨兵报NOAUTH | sentinel未配密码 | 检查auth.sentinel |
| 主从复制失败 | 网络策略或密码不一致 | 检查Secret和NetworkPolicy |
| 升级卡住超时 | 镜像拉取慢或探针失败 | 看Pod Events和日志 |
| 回滚后配置没变 | 回滚的是Chart不是数据 | 确认回滚版本号 |
| Release名冲突 | 同namespace已有同名 | helm list -A查全局 |
7.2 几个我踩过的坑
第一个坑:values文件里的密码明文提交到了Git。后来改成用--set从环境变量注入,或者用Sealed Secrets、External Secrets这类方案管理。生产环境密码绝对不能进Git。
第二个坑:storageClass写错导致Pod一直Pending。不同云厂商的存储类名字不一样,部署前一定先kubectl get sc确认。而且有些存储类不支持ReadWriteOnce以外的模式,StatefulSet用RWO没问题,但如果你改成Deployment共享PVC就会出问题。
第三个坑:哨兵quorum配置错误。3个哨兵quorum设成3,结果挂一个哨兵就无法故障转移了。quorum应该是(n/2)+1,3个哨兵就是2。这个值设大了会导致故障转移永远触发不了,设小了会脑裂。
第四个坑:没配PDB,节点维护时整个集群被驱逐。K8s做节点drain时默认会驱逐所有Pod,如果没有PDB,可能一瞬间所有Redis Pod都被赶走,服务直接不可用。PDB的minAvailable要结合副本数设置,1主2从的场景设2比较合理。
7.3 性能调优的几个建议
Redis的maxmemory-policy要根据业务选。纯缓存场景用allkeys-lru,持久化数据用noeviction。Bitnami Chart可以通过master.extraFlags和replica.extraFlags传入:
master: extraFlags: - "--maxmemory 3gb" - "--maxmemory-policy allkeys-lru"maxmemory要留出约30%的余量给RDB/AOF重写和复制缓冲区,别设成容器内存上限,否则OOM Killer会找上门。比如容器limit是4Gi,maxmemory设3gb比较稳妥。
持久化方面,如果对数据丢失容忍度低,开启AOF:
master: persistence: enabled: true extraFlags: - "--appendonly yes"AOF的fsync策略默认everysec,兼顾性能和安全。要求极高可以设always,但性能会明显下降。
8. 写在最后的一点个人体会
Helm这东西,入门半小时,用好得半年。我见过太多团队把Helm当成"批量apply YAML的工具",values文件里塞几百行硬编码,Chart改得面目全非,最后比不用Helm还乱。真正发挥Helm价值的关键在于:把环境差异收敛到values,把通用逻辑沉淀到Chart模板,把版本管理交给Release。
用Bitnami这类成熟Chart时,别急着改它的模板,先花时间读它的values.yaml和README,你会发现90%的需求它都支持,只是你没找到对应的参数。实在要定制,优先用extraDeploy、extraFlags这类扩展点,而不是fork整个Chart。fork的代价是后续升级要手动merge,维护成本极高。
最后分享一个我常用的技巧:把helm get manifest的输出存下来,跟helm template的渲染结果做diff,能快速定位"为什么集群里的资源和我想的不一样"。这个命令在排查线上问题时屡试不爽。