Kubernetes Loki 日志收集系统部署文档 (读写分离模式 + Ceph S3 + Nginx 日志分离)
本文档将指导你在自建的 Kubernetes 集群中,针对每天 TB 级别的海量日志场景,使用 Helm 部署 Loki 的读写分离模式(Simple Scalable)和 Promtail 日志收集系统。
其核心特点为:
- 高吞吐与高可用:采用读写分离架构(Write/Read/Backend),可水平扩展,轻松应对每天 1TB 以上的日志写入和并行查询。
- 对象存储:完全依赖 Ceph 提供的 S3 对象存储(RGW)来存储索引和日志块,摆脱本地磁盘容量和并发写入的限制。
- 日志分离 :文档支持通过多租户隔离 (Tenant) 和 分类标签 (Label) 两种方式,将 Nginx 日志与其他业务日志进行物理流隔离或逻辑分类,便于独立查询和告警。
1. 环境准备与 Ceph S3 (RGW) 配置
Loki 的读写分离模式强依赖对象存储。既然你已经部署了 Rook-Ceph,我们需要使用 Ceph 的 RADOS Gateway (RGW) 来提供 S3 服务,并为 Loki 创建专属的 Bucket 和账号。
1.1 确认并获取 Ceph S3 Endpoint
如果你的 Rook-Ceph 已经部署了对象存储(CephObjectStore),可以通过以下命令查看内部访问地址:
bash
# 获取 Rook-Ceph 命名空间下的 svc 列表
kubectl get svc -n rook-ceph | grep rgw
根据你的环境输出,Endpoint 为:
http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local:8080
(注:如果尚未部署 CephObjectStore,你需要先应用包含 CephObjectStore 的 YAML 文件来启动 RGW 服务。)
1.2 创建安全保留策略的 StorageClass (Retain)
为了防止未来误删 Loki 的 ObjectBucketClaim (OBC) 导致 Ceph 底层的日志数据被一并销毁,我们强烈建议创建一个 reclaimPolicy: Retain(保留策略)的 StorageClass。
首先,确认你的 CephObjectStore 的名称:
bash
kubectl get CephObjectStore -n rook-ceph
(假设输出的 NAME 为 my-store)
创建 sc-retain-bucket.yaml 文件:
yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rook-ceph-retain-bucket
provisioner: rook-ceph.ceph.rook.io/bucket # 必须匹配 Rook-Ceph 的 provisioner
reclaimPolicy: Retain # 关键配置:删除 OBC 时保留底层 Bucket 和数据
parameters:
objectStoreName: my-store # 替换为你的 CephObjectStore 名称
objectStoreNamespace: rook-ceph
应用该配置:
bash
kubectl apply -f sc-retain-bucket.yaml
1.3 创建 Loki 专属 Bucket 与凭证 (使用 OBC)
现在,我们可以使用刚刚创建的安全 StorageClass 来为 Loki 自动配置专属的 Bucket 和独立的 AK/SK 凭证。
创建一个专门的 namespace,并提交 OBC 请求 loki-obc.yaml:
yaml
apiVersion: v1
kind: Namespace
metadata:
name: loki-stack
---
apiVersion: objectbucket.io/v1alpha1
kind: ObjectBucketClaim
metadata:
name: loki-data-claim
namespace: loki-stack
spec:
bucketName: loki-data # 明确指定具体的 Bucket 名称,不使用随机后缀
storageClassName: rook-ceph-retain-bucket # 使用我们刚创建的安全 SC
应用该配置:
bash
kubectl apply -f loki-obc.yaml
获取自动生成的专属配置:
Rook-Ceph 会在 loki-stack 命名空间下自动创建一个 ConfigMap(包含 Bucket 名称和 Endpoint)和一个 Secret(包含专属的 AK/SK)。
bash
# 1. 获取专属 Access Key (AK)
kubectl get secret loki-data-claim -n loki-stack -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 --decode && echo
# 2. 获取专属 Secret Key (SK)
kubectl get secret loki-data-claim -n loki-stack -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 --decode && echo
至此,你已经获得了 Loki 所需的 Endpoint、专属 AK 以及专属 SK。Bucket 名称为你指定的 loki-data。请妥善保存这些值,稍后将它们填入 Loki 的配置中。
1.4 安装基础工具
-
安装 Helm:确保集群管理节点已安装 Helm v3。
-
添加 Grafana Helm 仓库 :
bashhelm repo add grafana https://grafana.github.io/helm-charts helm repo update
2. 部署 Loki (Simple Scalable 读写分离模式)
对于 TB 级日志,我们必须使用读写分离架构。这会将 Loki 拆分为三种角色的 Pod:
- Write: 负责接收日志并写入 S3(可横向扩容应对写入高峰)。
- Read: 负责从 S3 读取数据响应查询请求(支持分布式拆分查询)。
- Backend: 负责后台任务(如压缩、过期数据清理等)。
2.1 创建 Loki 配置文件
创建 values-loki-scalable.yaml 文件,请务必将 S3 的连接信息替换为你实际的 Ceph RGW 配置:
yaml
deploymentMode: SimpleScalable
loki:
# 是否开启多租户 (开启后需配置 X-Scope-OrgID Header,关闭则混租)
auth_enabled: true
commonConfig:
replication_factor: 1 # 因为我们将 write/read 副本数降为了 1,这里必须同步设置为 1,否则会导致 500 报错
# 配置统一的对象存储 (指向 Ceph S3)
storage:
bucketNames:
chunks: loki-data # 替换为你的 Bucket 名称
ruler: loki-data
admin: loki-data
type: s3
s3:
endpoint: http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local:8080 # 替换为实际的 Ceph RGW 地址 (注意必须保留 http:// 前缀)
region: default # Ceph 默认通常是 default 或者 us-east-1
accessKeyId: HR4UJM3NUCIBL7V1S4PZ # 替换为你的 AK
secretAccessKey: VBAOrEod0oIooXaGyDAlgvE5wYWELd0duWguaK2A # 替换为你的 SK
s3ForcePathStyle: true # Ceph S3 必须开启路径模式
insecure: true # 如果没有配置 https 则设为 true
# 存储 Schema 配置 (使用 TSDB 引擎)
schemaConfig:
configs:
- from: "2024-01-01" # 表示这个存储格式规则从这个日期开始生效。可以填一个早于当前的任意日期。
store: tsdb
object_store: s3
schema: v13
index:
prefix: index_
period: 24h
# 日志保留策略 (Retention) - 例如保留 30 天
compactor:
working_directory: /var/loki/compactor
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: s3 # 开启保留策略后必须配置此项,指定用于存放"删除请求记录"的后端存储
limits_config:
retention_period: 30d # 全局日志保留时间,超过该时间的日志将被 Compactor 自动清理
ingestion_rate_mb: 1024 # 全局每秒允许写入的日志量 (MB/s),调大至 1GB/s 避免初始化时大量历史日志导致 429 报错
ingestion_burst_size_mb: 1024 # 允许的全局瞬间突发写入量 (MB),应对突发的高并发日志
per_stream_rate_limit: 1024MB # 单个日志流 (相同的 Label 组合) 每秒允许的写入量
per_stream_rate_limit_burst: 1024MB # 单个日志流允许的瞬间突发写入量
# 禁用单体模式
singleBinary:
replicas: 0
# --- 读写分离组件扩容配置 ---
# 资源消耗优化 (关闭不必要的附加组件)
# 如果集群内存/CPU 资源紧张,强烈建议将以下组件关闭。
chunksCache:
enabled: false # 关闭用于缓存日志块(Chunks)的 Memcached 集群,节约大量内存
resultsCache:
enabled: false # 关闭用于缓存查询结果的 Memcached 集群,节约大量内存
test:
enabled: true # 开启 Helm test 框架,允许使用 helm test 命令来验证 Loki 集群是否正常工作
lokiCanary:
enabled: true # 开启 Canary 守护进程,它会持续向 Loki 写入和查询模拟日志,用于自我监控和告警
# 写入节点配置 (根据日志量调整副本数,可按需扩容)
write:
replicas: 1
persistence:
# 写入节点需要少量本地存储用于 WAL (预写日志),避免宕机丢失缓存的日志
size: 10Gi
storageClass: "rook-ceph-block"
# 读取节点配置 (根据查询并发量调整,支持横向扩容)
read:
replicas: 1
# 后端节点配置 (处理定时任务,通常 1 个或 2 个即可)
backend:
replicas: 1
persistence:
# Backend 需要本地存储用于 Compactor 的临时工作空间
size: 10Gi
storageClass: "rook-ceph-block"
2.2 执行 Loki 部署
创建一个独立的 namespace 并部署:
bash
helm upgrade --install loki grafana/loki \
--namespace loki-stack --create-namespace \
-f values-loki-scalable.yaml
验证 Loki Pod 和 Ceph PVC 状态:
bash
kubectl get pods -n loki-stack -l app.kubernetes.io/name=loki
kubectl get pvc -n loki-stack
# 确保 PVC 状态为 Bound,说明 Ceph 已成功分配存储卷
3. 部署 Promtail 并实现 Nginx 日志分离
Promtail 以 DaemonSet 的方式运行在每个节点上。为了将 Nginx 日志与其他业务日志分离,我们将使用 Promtail 的 pipeline_stages 和 match 机制。
分离逻辑:
- 识别标签中包含
nginx的 Pod(例如 Nginx Ingress 或你部署的 Nginx 业务)。 - 给 Nginx 日志打上
log_category: nginx标签,并提取出 Nginx 专有的字段(如状态码、请求路径)。 - 给非 Nginx 的日志打上
log_category: app标签。
3.1 创建 Promtail 配置文件
创建 values-promtail.yaml 文件:
yaml
config:
clients:
# 指向上面部署的 Loki 统一网关服务
# 读写分离模式下,所有请求都必须打给 loki-gateway
- url: http://loki-gateway.loki-stack.svc.cluster.local:80/loki/api/v1/push
snippets:
pipelineStages:
# 1. 容器运行时日志基础解析 (docker/cri)
- cri: {}
# 2. 匹配 Nginx 相关的日志
# 匹配 Ingress Nginx Controller
- match:
selector: '{app="ingress-nginx"}'
stages:
# 方案 A: 设置多租户 (与 loki.auth_enabled: true 配合使用,默认推荐)
- tenant:
value: "tenant-nginx"
# 方案 B: 添加分类标签 (若 loki.auth_enabled 为 false,则取消下方注释,查询时用 {log_category="nginx"})
# - static_labels:
# log_category: "nginx"
# 3. 匹配非 Nginx 的其他所有业务日志
# 排除 Ingress Nginx Controller
- match:
selector: '{app!="ingress-nginx"}'
stages:
# 方案 A: 设置多租户
- tenant:
value: "tenant-app"
# 方案 B: 添加分类标签
# - static_labels:
# log_category: "application"
# 容忍所有的污点,确保 Promtail 能在所有节点(包括 Master 节点)上收集日志
tolerations:
- operator: Exists
3.2 执行 Promtail 部署
bash
helm upgrade --install promtail grafana/promtail \
--namespace loki-stack --create-namespace \
-f values-promtail.yaml
检查 Promtail 运行状态:
bash
kubectl get pods -n loki-stack -l app.kubernetes.io/name=promtail
# 应该在集群的每个节点上都有一个处于 Running 状态的 Pod
4. 验证与 Grafana 集成
4.1 在 Grafana 中添加数据源
根据你在第 3 节中选择的方案,配置数据源的方法有所不同:
方案 A: 多租户隔离 (auth_enabled: true)
- 登录到你的 Grafana 面板。
- 进入 Connections -> Data Sources -> Add new data source ,选择 Loki。
- 在 HTTP URL 中填写 Loki Gateway 的内部服务地址:
http://loki-gateway.loki-stack.svc.cluster.local:80 - 在 HTTP headers 部分,点击 Add header ,填写:
- Header :
X-Scope-OrgID - Value :
tenant-nginx
- Header :
- 将这个数据源命名为 Loki-Nginx。
- 点击 Save & test,如果提示成功则说明连接正常。
- 重复以上步骤 ,再添加一个名为 Loki-App 的数据源,并将
X-Scope-OrgID的值设置为tenant-app。
方案 B: 标签分类 (auth_enabled: false)
- 登录到你的 Grafana 面板。
- 进入 Connections -> Data Sources -> Add new data source ,选择 Loki。
- 在 HTTP URL 中填写:
http://loki-gateway.loki-stack.svc.cluster.local:80 - 不需要配置任何 Header,直接将数据源命名为 Loki。
- 点击 Save & test 测试连接即可。
💡 获取 Loki 地址方法 :你可以通过执行
kubectl get svc -n loki-stack | grep gateway确认服务名称和端口。Kubernetes 集群内部服务地址的标准格式为http://<服务名>.<命名空间>.svc.cluster.local:<端口>。
4.2 查询与验证日志分离
进入 Grafana 的 Explore 页面。
如果你使用的是多租户方案 (auth_enabled: true):
- 在顶部的数据源下拉菜单中,选择 Loki-Nginx。
- 输入查询
{app="ingress-nginx"}即可查询 Nginx 租户下的日志。 - 切换为 Loki-App 数据源,由于查询需要至少包含一个明确的正向匹配标签,所以不能只输入
{app!="ingress-nginx"}。你可以输入{namespace=~".+"}或{job=~".+"}即可查询其他所有业务容器日志。
如果你使用的是分类标签方案 (auth_enabled: false):
-
只需要一个配置了 Loki 内部地址的普通数据源即可(无需 Header)。
-
在 Log browser 中输入以下 LogQL 查询 Nginx 日志:
logql{log_category="nginx"} -
查询其他业务日志:
logql{log_category="application"}
💡 排查查询不到 Nginx 日志的问题 :
如果查不到数据,通常是因为你的 Nginx Pod 的标签与 Promtail 配置中的选择器不匹配。
- 文档中 Promtail 的
match规则是selector: '{app="ingress-nginx"}'。但请注意,在 Promtail 的管道处理 (pipeline_stages) 中,match选择器使用的是经过 Promtail relabel 处理后最终保留的标签 。(查看最终标签的方法 :执行kubectl port-forward -n loki-stack daemonset/promtail --address 0.0.0.0 3101:3101,然后在浏览器访问http://<节点IP>:3101/targets,在页面中即可看到各 Pod 目标被 Promtail 最终保留的实际标签)。- 如果你在 Promtail 的 target 页面发现最终生成的标签不匹配,你需要将
match规则修改为匹配现有的标签。- 修改
values-promtail.yaml中的selector并重新执行helm upgrade。- 触发日志产生 :如果没有日志产生,查询结果也会为空。你可以通过访问你的 Ingress 域名或直接请求 Nginx 服务来产生访问日志,例如:
curl -I http://<你的Ingress节点IP或域名>(可通过curl -I http://<你的Ingress域名> | grep ingress-nginx搜索确认请求是否成功到达 Ingress Nginx),然后再去 Grafana 重新查询。- 确认 Nginx 自身是否打印了日志 :你可以直接通过
kubectl查看 Ingress Nginx 的原生日志,确认它是否记录了刚才的请求:kubectl logs -n <你的nginx命名空间> -l app.kubernetes.io/name=ingress-nginx --tail=10。如果这里也没有日志,说明是 Nginx 配置本身没有开启访问日志输出。- 排查 Promtail 采集问题 :如果 Nginx 本身有日志,但在 Grafana 中查不到,说明 Promtail 没有成功采集或发送。请执行
kubectl logs -n loki-stack -l app.kubernetes.io/name=promtail --tail=50查看 Promtail 的运行日志,检查是否有连接 Loki Gateway 失败的报错(如 401 认证失败、DNS 解析失败等)或是读取节点容器日志目录权限不足的问题。- 处理 Loki 写入限流 (429 错误) :如果在 Promtail 日志中发现
server returned HTTP status 429 Too Many Requests (429): ingestion rate limit exceeded,说明你的日志产生量超过了 Loki 默认的写入限制(默认通常是 4MB/s)。你需要修改values-loki-scalable.yaml,在loki.limits_config下增加或调大ingestion_rate_mb和ingestion_burst_size_mb(例如设置为 1024 即 1GB/s),然后执行helm upgrade应用新配置。- 查看 Promtail 采集目标状态 :如果以上都没问题,可以通过端口转发访问 Promtail 的内置 Web 页面,查看它到底抓取了哪些目标以及打上了什么标签:
kubectl port-forward -n loki-stack daemonset/promtail --address 0.0.0.0 3101:3101,然后在浏览器访问http://<任意节点IP>:3101/targets确认 Nginx 的 Pod 是否在列,以及它的标签是什么。
5. 维护与建议 (进阶)
- Write/Read 节点本地盘扩容 :如果日后发现 Write 节点的 WAL (预写日志) 空间不足(如 10Gi 不够),由于它们挂载的是 Rook-Ceph 的块存储,可以直接编辑对应的 PVC (
kubectl edit pvc ...) 修改大小实现在线扩容。 - 多租户隔离 :我们已经通过 Promtail 的
tenant机制和 Loki 的auth_enabled: true实现了多租户分离(已在本次部署中完成)。如果未来有新的业务线接入,只需在 Promtail 配置中增加相应的match规则,并为其分配新的tenant_id,然后在 Grafana 中添加对应的带X-Scope-OrgIDHeader 的数据源即可实现硬隔离。 - 数据清理监控:文档中我们已经配置了 Compactor(保留 30 天日志),请确保 Backend 节点正常运行,否则过期数据将不会被清理,会导致 S3 存储容量持续增长。
6. 进阶场景:使用 Sidecar 模式收集容器内文件日志 (支持 Java 多行异常)
文档前述部署的 DaemonSet 模式 Promtail 默认只收集容器的标准输出 (stdout/stderr)。如果业务应用将日志写到了容器内部的文件中(例如 /app/logs/app.log),我们需要使用 Sidecar(边车)模式 进行收集。
为了避免为每个业务硬编码标签,以下方案结合了 Kubernetes Downward API 自动注入 Pod 标签,并配置了 multiline 阶段来完美合并 Java 的多行异常堆栈。
6.1 创建通用的 Promtail ConfigMap
该 ConfigMap 可以被集群中所有需要 Sidecar 的业务复用。它通过读取环境变量动态生成日志标签。
步骤 1:创建配置文件 promtail-sidecar-config.yaml
将以下内容保存为 promtail-sidecar-config.yaml 文件:
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: promtail-sidecar-config-universal
namespace: default
data:
promtail.yaml: |
server:
http_listen_port: 9080
grpc_listen_port: 0
clients:
# 指向 Loki Gateway 地址。
# 注意:如果 Loki 开启了多租户 (auth_enabled: true),这里必须配置 tenant_id。
# 建议为 Sidecar 收集的日志配置独立的 tenant_id,以便与 DaemonSet 收集的容器标准输出隔离。
- url: http://loki-gateway.loki-stack.svc.cluster.local:80/loki/api/v1/push
tenant_id: "tenant-sidecar-app"
positions:
filename: /tmp/positions.yaml
scrape_configs:
- job_name: sidecar-local-logs
static_configs:
- targets:
- localhost
labels:
log_source: file
# 指定共享目录下的日志文件路径
__path__: /shared-logs/*.log
pipeline_stages:
# --- Java 多行异常合并 ---
- multiline:
# 匹配新日志的开头(例如以 2023-10-01 或 2023/10/01 开头)
firstline: '^\d{4}[-/]\d{2}[-/]\d{2}'
max_wait_time: 3s
max_lines: 500
# --- 通过环境变量展开写入静态标签 ---
- static_labels:
# 启动时自动将 ${XXX} 替换为真实的 Pod 环境变量值
pod: "${POD_NAME}"
namespace: "${POD_NAMESPACE}"
app: "${APP_LABEL}"
步骤 2:应用 ConfigMap 到集群
执行以下命令,将配置部署到 Kubernetes 中:
bash
kubectl apply -f promtail-sidecar-config.yaml
6.2 业务 Deployment 注入 Sidecar 示例
业务方只需在原有的 Deployment 中加入一个共享的 emptyDir 卷,并增加 promtail-sidecar 容器即可。
步骤 1:创建部署文件 java-multiline-demo.yaml
将以下内容保存为 java-multiline-demo.yaml 文件:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-multiline-demo
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: java-demo-app
template:
metadata:
labels:
app: java-demo-app
spec:
volumes:
- name: shared-logs
emptyDir: {}
- name: promtail-config
configMap:
name: promtail-sidecar-config-universal
containers:
# 1. 主业务容器
- name: main-app
image: busybox:latest
command: ["/bin/sh", "-c"]
args:
- |
mkdir -p /app/logs
while true; do
echo "$(date +'%Y-%m-%d %H:%M:%S') INFO com.example.MyApp - Starting application..." >> /app/logs/app.log
sleep 2
echo "$(date +'%Y-%m-%d %H:%M:%S') ERROR com.example.MyApp - An unexpected error occurred:" >> /app/logs/app.log
echo "java.lang.NullPointerException" >> /app/logs/app.log
echo " at com.example.MyApp.process(MyApp.java:42)" >> /app/logs/app.log
sleep 5
done
volumeMounts:
# 将共享卷挂载到业务代码写日志的目录
- name: shared-logs
mountPath: /app/logs
# 2. Promtail Sidecar 容器
- name: promtail-sidecar
image: grafana/promtail:2.9.2
args:
- -config.file=/etc/promtail/promtail.yaml
# 核心参数:允许解析配置文件中的 ${ENV_VAR}
- -config.expand-env=true
env:
# 通过 Downward API 自动注入元数据
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: APP_LABEL
valueFrom:
fieldRef:
fieldPath: metadata.labels['app']
volumeMounts:
# 必须和 ConfigMap 里的 __path__ 匹配
- name: shared-logs
mountPath: /shared-logs
- name: promtail-config
mountPath: /etc/promtail
步骤 2:执行部署并验证
执行以下命令,启动带有 Sidecar 的业务 Pod:
bash
kubectl apply -f java-multiline-demo.yaml
6.3 在 Grafana 中配置 Sidecar 专用数据源
由于我们在 ConfigMap 中为 Sidecar 指定了独立的租户 (tenant-sidecar-app),我们需要在 Grafana 中创建一个专门的数据源来查询这些容器内文件日志,以便与直接从容器终端获取的日志进行隔离。
配置步骤:
- 登录 Grafana 面板,进入 Connections -> Data Sources -> Add new data source ,选择 Loki。
- 在 HTTP URL 中填写 Loki Gateway 地址:
http://loki-gateway.loki-stack.svc.cluster.local:80 - 在 HTTP headers 部分,点击 Add header ,填写:
- Header :
X-Scope-OrgID - Value :
tenant-sidecar-app(必须与 Promtail ConfigMap 中的 tenant_id 保持一致)
- Header :
- 将该数据源命名为 Loki-Sidecar-File。
- 点击 Save & test 测试连接。
查询验证:
在 Grafana 的 Explore 页面中选择 Loki-Sidecar-File 数据源。由于我们注入了动态标签并合并了 Java 多行异常,你可以使用以下查询语句精准定位:
- 查询特定应用的日志:
{app="java-demo-app", log_source="file"} - 验证 Java 异常是否被正确合并:你会看到完整的包含多行
at com.example...的堆栈信息作为一条完整的日志条目展示,而不再是支离破碎的单行。