
1. 为什么要用 Helm 在 Kubernetes 上部署 Loki先看清架构再动手我知道很多人看到 Helm Kubernetes Loki 这三个词凑在一起脑子里第一反应就是“又来一个部署教程”。但我接触过太多用 ELK 被存储成本压垮、用单 Pod 跑 Loki 结果查询慢到怀疑人生的案例所以我先把一个最容易被忽略的架构前提掰开揉碎讲清楚。Loki 是一个日志聚合系统如果你没用过可以把它想象成“日志界的 Prometheus”它只对日志内容和标签做索引不全文索引原始数据所以存储成本能做到 Elasticsearch 的几十分之一。但这种低成本查询方案也对部署形态有硬性要求比如存储后端必须用对象存储兼容方案比如查询前端和写入后端需要分离扩容这些恰恰是裸写 YAML 最容易搞错的地方。选择 Helm 来部署 Loki核心原因不是因为它“用起来最时髦”而是因为它把 Loki 集群里各个组件的依赖关系、配置挂载、探针、Service 暴露方式都做成了模板参数。你在裸 Kubernetes 集群上手工部署 Loki至少需要维护 Deployment、StatefulSet、ConfigMap、Service、ServiceAccount、Role、RoleBinding 这些对象的至少十几个 YAML 文件而且这些文件之间还有参数联动。一旦集群环境变了比如从单机测试环境切到带持久化存储的多节点生产集群你要改的地方就不是一个文件能搞定的。Helm 的价值在于用一套 chart 参数化这些差异环境变了改 values 文件而不是重新写一遍 YAML。当然这里得插一句经验之谈Helm 只是帮你把编排复杂度封装起来了不等于说你不用理解底层架构。我见过有人用默认 values 一把梭部署完结果发现 pod 起不来查了半天才发现是对象存储 Endpoint 配置没对上。后面我会把部署链路里每个容易翻车的点都过一遍这篇博文基本可以当成一份避坑手册来用。2. Loki 的读写分离架构与 Helm Chart 组件拆解搞清楚每个 Pod 都是干嘛的2.1 Loki 不是“一个大 Pod”而是多个角色的协同在真正写 helm install 命令之前我要先带你看看 Loki chart 安装出来到底有哪些组件这样后面排错的时候你至少能一眼看出哪个 Pod 不正常。Loki 从 2.x 版本开始官方 Helm Chartgrafana/loki-stack 和 grafana/loki-distributed已经能部署完整集群。我们先聊最常用的单写多读部署方式。Loki 的读写链路可以粗略分成几个角色Distributor负责接收来自 Promtail 或任何 Loki 客户端的日志写入请求它会把日志流按照标签拆分成多个批次然后转发给 Ingester。注意Distributor 本身是无状态的可以随便伸缩。Ingester负责在内存中批量攒日志攒够一批或者到达时间阈值后写入到对象存储比如 MinIO、S3、OSS同时给日志所在的日志流建立索引。Ingester 是有状态的如果它挂了对应日志流的写入会短暂不可用所以生产环境通常要跑副本并开启 WAL预写日志。Querier负责执行 LogQL 查询请求它把请求下发到存储层也会读取 Ingester 内存中还没 flush 的数据。Querier 是无状态组件查询压力大就多开几个。Query Frontend一个查询加速层它把大的查询任务拆分、把相同的查询结果做缓存如果没有它高并发查询会把 Querier 打爆。Compactor负责把对象存储里的日志块做压缩和索引整理这个组件对单机部署来说不是必须的但生产环境如果长期不跑查询性能会越来越差。Index Gateway这是 Loki 2.x 之后引入的组件主要解决索引数据量太大、放不下内存的问题。它负责把索引读写请求路由到合适的后端。看到这里你应该明白了直接裸写一个 Deployment 去跑 Loki 是行不通的因为你最少也要写一个 Distributor、一个 Ingester、一个 Querier 的 StatefulSet/Deployment。用 Helm Chart 最大的好处是这些角色对应的 Deployment、HPA、Service 都是现成的你只要通过 values 控制每个角色的副本数和资源配置。2.2 官方 Chart 版本怎么选loki-stack 与 loki-distributed这里必须说明一个常见的认知误区。很多同学搜 Helm 部署 Loki一上来就跑到 Artifact Hub 搜“loki”然后找到了 grafana/loki-stack 和 grafana/loki-distributed 两个 chart就不知道该选哪个了。我个人的建议是如果你只是想在测试环境快速跑起来看日志、配告警用 loki-stack 就够了。它会把 Loki单二进制模式、Promtail、Grafana 打包在一起一条命令就能起一套完整的日志观测环境。但如果你面对的是日均日志量在 100GB 以上的生产集群或者你有明确的读写分离、水平扩容诉求那就应该选择 loki-distributed。这个 chart 会把上面说的 Distributor、Ingester、Querier、Query Frontend、Compactor 拆成独立的 Deployment/StatefulSet每个组件可以单独设置存配额。在写 values 之前我建议先确认一下你的 Helm 3 版本是不是足够新。早期 Helm 3.0 版本对某些 API 资源的支持还不太完善如果你在部署时遇到 “unable to recognize : no matches for kind” 这类报错可以先执行 helm version 检查版本如果版本太低直接升到 3.10 以上省得后面折腾。实际上我在自己环境里用的就是 Helm 3.10 版本部署 Loki 时没遇到 API 兼容性问题。3. 部署前置存储选型、命名空间准备和 values 文件里的关键开关3.1 对象存储才是 Loki 的最终归宿本地磁盘只适合破局我第一次部署 Loki 时犯过一个错误用默认 values 直接装结果所有数据都写到了 Pod 的临时目录里。临时目录意味着 Pod 一旦重启历史日志就全没了这对日志系统来说就是灾难。后来我发现Loki 的日志数据最终要落在对象存储里而且它的块存储chunk和索引index是可以分开配置存储后端的。如果你手头没有现成对象存储最简单的方案是在同一个 Kubernetes 集群里用 MinIO 自建一套 S3 兼容服务。这样你可以在内网用 Loki 需要的 S3 协议直接对接。如果是云上环境直接用云厂商的对象存储会更省事比如阿里云 OSS、AWS S3只要在 values 里配置好 endpoint、bucket、access key 和 secret key 即可。有一个细节值得强调Loki 的存储配置里有个 storage_config它有一个 boltdb_shipper 机制负责把索引写到对象存储或者本地共享磁盘。在分布式模式下Compactor 需要能访问同一个对象存储桶所以你在 values 里配置 bucket 名称时一定要全局统一。否则会出现 Ingester 写入成功Query 的时候却查不到索引的诡异问题。我整理了一份适合多数场景的存储配置思路你可以当成参考环境存储方案说明测试环境 / 学习用本地 emptyDir 或单个 PVC数据不保证持久化适合验证功能小规模生产50GB/天自建 MinIO 或轻量对象存储成本低S3 兼容一劳永逸中大规模生产100GB/天云厂商对象存储 独立索引存储读写分离、索引和块分别管理扩容灵活3.2 命名空间、ServiceAccount 和 PodSecurityPolicy 的准备Helm 部署前先建一个干净的命名空间我习惯用 monitoring 这个命名空间来部署所有可观测性组件。这样做的好处是后续用网络策略、RBAC 做权限隔离时边界清晰。创建命名空间和执行部署的命令很简单但有个坑在于 RBAC。Loki 的 chart 默认会创建 ServiceAccount并且会给它绑定读取 Pod 元数据的权限。如果你用的是比较严格的 RBAC 体系比如在集群里预先配置了 Pod Security Admission 之类的安全策略可能会导致 Loki 的 Pod 因为安全上下文不满足条件而启动失败。这时你需要在 values 里显式关闭或者调整相关的安全策略开关。可以在安装时加参数kubectl create namespace monitoring helm repo add grafana https://grafana.github.io/helm-charts helm repo update你可以先用 helm show values grafana/loki-distributed loki-values.yaml 把默认配置导出来在本地打开过一遍重点关注这几个字段loki.image.tag镜像版本不要用 latest要指定具体版本号。loki.configLoki 的核心配置文件里面包含存储、查询、限制等所有配置。loki.persistence是否启用 PVC 持久化。monitoring.selfMonitoringLoki chart 自带的监控配置它会创建 ServiceMonitor如果你没有安装 Prometheus Operator可以直接把这块关掉。singleBinary.replicas单二进制模式下副本数默认 1。3.3 values 文件里最容易被忽视的配置limits_config 和 schema_config很多人在部署完 Loki 以后发现日志明明写入了但界面查询不出来问题往往出在 limits_config 的 ingestion_rate_mb 和 max_streams_per_user 这两个参数上。Loki 对每个用户的日志写入速率和日志流数量默认是有限制的如果 Promtail 采集的文件过多、日志流数量超过默认限制就会出现写入被拒绝的情况。我习惯在一开始就把限制调高一些比如loki: config: limits_config: ingestion_rate_mb: 16 ingestion_burst_size_mb: 32 max_streams_per_user: 5000 max_global_streams_per_user: 10000 retention_period: 168h这里有个反直觉的地方max_streams_per_user 不是指容器数量而是指唯一标签组合的数量。举个例子如果你按 namespace、pod、container 三个标签来关联日志那么一个命名空间里哪怕只有一个 Pod只要它重启过就可能产生多个日志流。日志流数量一多默认的 5000 上限很容易触发。我一般先把 max_streams_per_user 调高到 10000等跑稳定后再根据实际情况收紧。至于 schema_config主要和你的存储 schema 版本相关。Loki 从 2.x 开始推荐使用 v12 或 v13 的 schema。Helm Chart 的默认 values 里通常已经写好了你不需要改但如果你是从旧版本手动迁移数据过来的schema 版本升级一定要熟练操作否则历史数据可能无法被新版本读取。4. 实战部署完整过程从 loki-distributed 到 Promtail 采集日志4.1 helm install 命令与可观测性联动的完整配置理论铺垫做完下面进入实操。假设你已经配好 Helm 仓库并且准备好了 values 文件下面这条命令就是正式部署helm install loki grafana/loki-distributed -n monitoring -f loki-values.yaml --wait --timeout 10m加 --wait 参数是为了让 Helm 等待所有 Pod 都进入 Ready 状态。如果你的对象存储还没就绪这条命令会一直卡到超时所以建议先确保 MinIO 或云对象存储已经可用。部署完成后用下面的命令确认 Pod 状态kubectl get pods -n monitoring | grep loki正常情况下你会看到类似这样的输出loki-chunks-cache-0 1/1 Running 0 3m loki-compactor-0 1/1 Running 0 3m loki-distributor-6fc8d9f8d7-2b6n9 1/1 Running 0 3m loki-ingester-0 1/1 Running 0 3m loki-querier-7f6d6d5b5f-lk8n2 1/1 Running 0 3m loki-query-frontend-6fffd6cbb7-vxq4d 1/1 Running 0 3m如果你的列表中缺少某个组件比如没有 compactor那很可能是你的 chart 版本默认配置里没启用可以在 values 里找到对应开关手动打开。还有一个容易误解的地方distributed chart 默认会部署一个叫做 loki-chunks-cache 的组件它本质是给块索引做缓存用的并不是存储后端不要把它当成 MinIO 来用。4.2 配置 Promtail把节点上所有 Pod 日志抓进 LokiLoki 本身不负责采集日志它只负责接收和存储。所以你还需要一个日志采集器官方默认搭配的是 Promtail。Promtail 会以 DaemonSet 方式跑在每个节点上读取节点上 /var/log/containers 目录下的日志文件并加上 pod、namespace、container 等标签后发送给 Loki 的 Distributor。在你的监控命名空间里通常可以再加装一个 loki-promtailhelm install promtail grafana/promtail -n monitoring -f promtail-values.yamlpromtail-values.yaml 里最重要的配置是 clients 部分它指定了日志要发送到哪个 Loki 地址config: clients: - url: http://loki-gateway.monitoring.svc.cluster.local:80/loki/api/v1/push如果你部署的是 loki-distributed那么 Loki 的网关 Service 一般是 loki-gatewayPromtail 往这个地址推数据即可。如果你用的是 loki-stack那么这个地址大概率是 http://loki:3100/loki/api/v1/push具体以你的 Service 名为准。拿到正确的地址其实不难在集群里执行 kubectl get svc -n monitoring | grep loki看哪个 Service 名映射到了 3100 或 80 端口。建议在 Promtail 配置里开启 kubernetes_sd_config 来自动发现 Pod 和节点标签这样后面在 Grafana 里你就能直接按 namespace、pod 来筛选日志。默认配置一般已经这样做了不用额外动太多。4.3 在 Grafana 里接入 Loki 数据源并验证日志链路部署完 Loki 和 Promtail接下来的验证环节非常关键。我推荐直接用 Grafana 配上 Loki 数据源用 LogQL 查询日志验证整条链路是否打通。如果你没有现成的 Grafana可以在同一个命名空间里部署一个helm install grafana grafana/grafana -n monitoring --set service.typeClusterIP --set adminPasswordadmin然后在 Grafana 的 Configuration Data Sources 里添加一个类型为 Loki 的数据源。URL 填 http://loki-gateway.monitoring.svc.cluster.local:80和 Promtail 推数据的地址一致。保存后进入 Explore 页面选好数据源输入一条最简单的 LogQL 查询{namespacemonitoring}如果能查到监控命名空间里的 Pod 日志说明整条链路已经通了。如果查不到先不要怀疑 Loki 的问题大概率是 Promtail 的标签或者地址配置有问题。这时可以到 Promtail 的 Pod 日志里看是否有推送失败的报错。顺便说一句调试时最常用的命令是kubectl logs -n monitoring -l app.kubernetes.io/namepromtail --tail50看到日志里没有 error并且有类似 “successfully sent” 的输出基本可以确定采集链路是健康的。5. 部署后的关键验证与压测确认日志写入、查询与存储状态5.1 用的一份检查清单从 Pod 状态到日志延迟部署完成不代表一切正常我习惯按顺序做一轮快速检查确保系统没有隐藏问题检查所有 Loki 组件 Pod 是否全部 Running且 Ready 数大于 0。检查 Ingester Pod 所在节点是否有足够磁盘空间因为 WAL 会占用额外的临时存储。用 kubectl exec 进入任意一个 Loki Pod执行 wget -qO- http://localhost:3100/ready 确认 HTTP 就绪探针返回 200。用 curl 调用 Loki 的 /loki/api/v1/labels 接口看能否返回 label 列表。确认 MinIO 或云对象存储的桶里确实有文件在产生比如查看对象存储的容量变化。在 Grafana 里用 LogQL 查询最近 5 分钟的日志确认日志延迟在可接受范围内。日志延迟也就是从 Pod 打印日志到能在 Grafana 里查到正常情况下应该在几秒到十几秒之间。如果延迟超过了 1 分钟那就需要关注 Ingester 的 flush 周期、Promtail 的 batch_wait 时间。Helm 部署的默认参数通常已经合理但你可以在配置里调小 batch_wait让日志更快被推送到 Loki。5.2 写入与查询压力的简单测试方法如果要验证系统扛不扛得住真实业务量简单的办法是批量制造一些日志比如在集群里创建一个循环打印日志的测试 Podcat EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: loggen namespace: monitoring spec: containers: - name: loggen image: busybox command: [/bin/sh, -c, i0; while true; do echo \log entry $i from test pod\; i$((i1)); sleep 0.1; done] EOF然后观察 Loki 的 Distributor 和 Ingester 有没有写入延迟或者限流报错。在这个测试结束后记得删掉这个 Podkubectl delete pod loggen -n monitoring我自己做压测时还会把 grep 查询与实时查询结合起来比如用 LogQL 里的 rate() 语句对某个日志流做速率统计验证聚合查询的响应时间。如果一切都在几秒内返回说明当前配置水平是达标的。如果出现超时就要考虑是不是 querier 副本数不够、或者存储后端的带宽存在瓶颈。这种情况下优先加 querier 副本数同时在 object storage 侧排查网络到存储的带宽和延迟。6. 上线后最容易踩的坑我用实际经历总结的高频故障与对策6.1 故障一Pod 一直 CrashLoopBackOff日志提示存储连接失败这是一个特别常见的初装故障症状是 Loki 组件反复重启错误日志里出现 AccessDenied、Connection refused 或者 timeout。大概率原因有三个对象存储的 endpoint 填错比如把 MinIO 的服务地址写成 http但实际要求 https。AccessKey 和 SecretKey 填错或者不具备 Bucket 的写入权限。Bucket 名称没有提前创建好。有些云厂商支持自动创建但自建 MinIO 的默认策略不一定允许自动建桶需要提前用 mc 命令或控制台手动建好。排查方法是进入对应 Pod查看它启动时候引用的 ConfigMap 内容。用命令kubectl get configmap -n monitoring loki -o yaml | grep -A 20 storage_config把配置里的 endpoint 和 bucket 跟实际对象存储服务比对。很多时候发现只是 bucket name 多写了一个空格或斜杠。6.2 故障二日志写入报 429提示 streams limit reached这个故障出现频率极高特别是在接入大量应用后。现象是 Promtail 日志里出现 rate limit exceeded核心原因是前面讲到的 max_streams_per_user 默认值太小。但我发现一个更隐蔽的原因标签基数的失控。Promtail 默认会采集一部分 Kubernetes 标签如果你在业务应用里故意塞了像 request_id、user_id 这种高基数标签日志流数量会指数级膨胀。429 报错其实是 Loki 在帮你兜底否则内存会先爆。解决思路分两步第一步在 limits_config 里提高 max_streams_per_user第二步去 Promtail 配置里删掉不必要的标签。标签应该严格限制在 namespace、pod、container、app、level 这些维度。像 trace_id 这种高基数信息应该放到日志内容里而不是做标签。6.3 故障三Grafana 查询超时querier 内存被打满日志系统跑了一段时间后查询变慢、甚至超时这是一个必然要面对的问题。如果查询的日志时间范围很大、并且在 Select 里没有加任何 label 过滤Loki 会把所有 chunk 都扫一遍对 Querier 的压力极大。优化手段有几个层面在 LogQL 查询里强制加上 namespace 或 app 的过滤缩小日志流。打开 Query Frontend 的分片缓存默认配置里查不到的话就在 values 里启用。调整 chunk_target_size 参数让 Loki 生成更大的 chunk减少扫描开销。增加 querier 的无状态水平副本配合 HPA 在高峰期自动扩容。从运维角度看日志查询超时并不一定说明系统容量不行很多时候是查询语句写得不够精细。我通常会建议业务方在 Grafana 里保存常用查询模板把 namespace、pod 这些过滤条件做成变量减少随意全表扫描的情况。6.4 升级要注意的兼容性问题Helm Chart 版本和 schema 迁移最后提一下升级。Helm 的 chart 升级很方便一条命令就能搞定但 Loki 的版本升级和 chart 升级不是一回事。如果你跨了大版本升级比如从 2.6 升到 2.9就要特别留意 schema_config 的变化。如果新的 schema 版本和旧的数据不兼容查询的时候可能遇到 “block is corrupt” 或者找不到数据的错误。稳妥的做法是先备份 values 文件然后查看当前 chart 版本和 Loki 镜像版本的对应关系再确认 Loki 官方 upgrade guide 里有没有提到 schema 迁移步骤。生产环境升级前一定要先在测试环境把同样的数据和版本复现一遍别直接拿生产环境练手。我在一次升级时因为偷懒没验证结果历史日志全部查不出来了最后只能从对象存储里手动刷索引代价非常大。7. 进阶思考日志系统之外还要考虑的监控告警与长期数据治理Loki 部署只是第一步一个真正能用的日志平台还应该配套告警规则与数据生命周期管理。Loki 自带的 ruler 组件可以执行基于 LogQL 的告警规则比如统计某个业务日志中的错误数量超过阈值就触发告警。你可以在 values 里配置 ruler 的 alertmanager 地址把告警对接上 Alertmanager进而推送到钉钉或邮件。数据生命周期管理这块前面提过 retention_period 参数。我建议在测试环境把保留周期设成 24h 或者 72h方便快速观察数据什么时候被清理。生产环境再根据合规要求设置 7 天、30 天或者更长时间。注意retention_period 是全局配置如果你想让不同租户或者不同应用的日志保留不同周期要额外配置 per_tenant_override 机制这块以后有机会单独写一篇。另外Loki 本身也是一个有状态且需要监控的系统。如果它挂了你的可观测性链路会变成睁眼瞎。所以我会把 Loki 组件自身的 Prometheus 指标接入 Prometheus 或 Grafana Cloud给 Distributor 和 Ingester 配上内存、JVM、请求错误率的告警。比如当 Ingester 的内存占用超过 80% 持续 5 分钟时就需要告警并考虑扩容。回到我用 Helm 部署 Loki 的初衷不是为了炫技而是为了在 Kubernetes 环境里快速得到一套成本可控、可弹性扩展、和云原生生态无缝集成的日志平台。Helm 让这套系统可复制、可版本化、可回滚。如果你之前还在手工维护 Loki 的 YAML 文件或者对 Loki 日志系统停留在“听说过”的阶段希望这篇文章能帮你避掉我当年踩过的坑。把存储、标签基数、查询模式这三件事搞定你的 Loki 大概率能稳定跑很久。