在 Nomad 上以微服务模式部署 Loki:job.nomad.hcl 与 config.yml 全解析
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
本文以 examples/nomad/loki-microservices/README.md 及其配套的 job.nomad.hcl 与 config.yml 为主体,系统讲解如何在 HashiCorp Nomad 集群中以微服务(Microservices)模式部署 Loki:包括作业定义结构、统一配置文件的每个关键参数、运行与扩缩容操作,以及面向生产的 HTTP 端点安全(Consul Connect + Traefik 认证)与 GRPC 通信加密(Vault PKI + mTLS)方案。读完本文,你将能够基于这套示例搭建一套可横向伸缩、具备健康检查与安全防护的分布式 Loki 集群。
微服务模式:为什么需要它
Loki 本质上是一个由众多微服务组成的分布式系统,但它采用了一种独特的构建模型:所有微服务都打包在同一个二进制中,通过-target命令行参数决定启动时运行哪些组件(参见 docs/sources/get-started/deployment-modes.md 中的说明)。微服务(分布式)部署模式正是把每个组件作为独立进程运行,每个进程通过-target指定自己的职责。
与单机模式(-target=all)和高可用单机模式相比,微服务模式的特点是:
| 维度 | 单机模式 | HA 单机模式 | 微服务模式 |
|---|---|---|---|
| 持久性 | ✅ | ✅ | ✅ |
| 高可用 | ❌ | ✅ | ✅ |
| 执行路径分离 | ❌ | ❌ | ✅ |
| 运维复杂度 | 低 | 中 | 高 |
| 可扩展性 | 低 | 中 | 高 |
微服务模式以最高的运维复杂度换取最细粒度的扩展能力,官方建议用于规模很大的集群,或需要精确控制各组件伸缩与集群运维的场景。本文示例在 Nomad 中实际部署了 8 个组件(compactor、ruler、distributor、ingester、querier、query-scheduler、query-frontend、index-gateway),覆盖了读写链路的全部核心进程。
Loki 微服务模式架构图
前置条件与文件结构
根据 examples/nomad/README.md 的说明,运行这套示例需要满足以下条件:
硬性要求
- 安装了较新版本的 Nomad,且 Docker driver 健康可用;
- Nomad 启用了 Consul 集成(服务发现与健康检查依赖 Consul);
- 可访问 S3 存储(示例默认使用 S3 兼容对象存储,如 MinIO)。
可选要求
- Vault 集成:用于安全地提供 S3 凭据;
- 配置了 Consul provider 的 Traefik:用于在多个 Loki 实例之间负载均衡。
示例目录包含三个文件:
| 文件 | 作用 |
|---|---|
| README.md | 使用说明与生产建议 |
| job.nomad.hcl | Nomad 作业定义:8 个组件组、任务、服务注册、健康检查 |
| config.yml | 所有组件共用的 Loki 配置文件(通过 Consul Template 注入) |
一个值得注意的细节:README 开篇提到使用 boltdb-shipper,但仓库内实际的 config.yml 已采用TSDB 索引 + S3 对象存储 + index-gateway的组合(store: tsdb、schema: v13、tsdb_shipper配置块),并以 job.nomad.hcl 中的index-gateway组与之配套。实际部署时应以配置文件为准:TSDB 是当前推荐的索引后端,index-gateway 负责为 querier 提供统一的索引读取入口。
作业定义全览:job.nomad.hcl 的 8 个组件组
job.nomad.hcl 定义了一个名为loki的作业,内含 8 个 group,每个 group 对应一个 Loki 组件,每个组内部都以-target=<组件名>启动同一个grafana/loki:${var.version}镜像:
| 组 | 默认副本数 | 启动 target | 说明 |
|---|---|---|---|
| compactor | 1 | compactor | 索引压缩、删除请求与保留策略执行 |
| ruler | 1 | ruler | 告警与记录规则评估 |
| distributor | 2 | distributor | 写入入口,校验并转发日志流 |
| ingester | 2 | ingester | 内存写入、WAL 与 flush |
| querier | 2 | querier | 执行查询(经 scheduler 调度) |
| query-scheduler | 2 | query-scheduler | 查询队列与调度 |
| query-frontend | 2 | query-frontend | 查询入口、缓存与拆分 |
| index-gateway | 1 | index-gateway | TSDB 索引读取网关 |
作业文件顶部通过 HCL 变量控制版本:
variable "version" { type = string description = "Loki version" default = "3.7.2" }所有任务统一使用 Docker driver、以nobody用户运行、kill_timeout = "90s"(给 ingester 等组件足够的优雅退出时间以 flush WAL 数据),并追加三个标准启动参数:
args = [ "-target=distributor", "-config.file=/local/config.yml", "-config.expand-env=true", ]其中-config.expand-env=true允许配置文件中使用${ENV_VAR}语法展开环境变量(S3 凭据即通过此机制注入)。
有状态组件的关键配置
ingester(写路径核心)使用ephemeral_disk的 sticky 模式,让/alloc数据在作业更新时尽量保留(WAL 就存放在NOMAD_ALLOC_DIR/data/wal):
ephemeral_disk { size = 4000 sticky = true }同时用 constraint 保证副本分布在不同故障域(必须与配置文件中的instance_availability_zone保持一致):
constraint { distinct_property = node.unique.name # distinct_property = node.datacenter # distinct_property = attr.platform.aws.placement.availability-zone }compactor、ruler、index-gateway同样配置了 sticky 的 ephemeral_disk,用于存放索引目录、规则文件与压缩工作目录。
服务注册与健康检查
每个组都注册了 Consul 服务,并通过meta携带两个标签——alloc_id(取 Nomad 环境变量NOMAD_ALLOC_ID)和component(组件名),这两者会被后文 Prometheus 的 relabel 规则消费:
service { name = "loki-distributor" port = "http" meta { alloc_id = NOMAD_ALLOC_ID component = "distributor" } ... }健康检查统一指向 Loki 内置的/ready端点,例如:
check { name = "Loki distributor" port = "http" type = "http" path = "/ready" interval = "20s" timeout = "1s" }query-scheduler与index-gateway还通过network块固定了 GRPC 端口映射(static = 9096/static = 9097),以便其他组件通过 Consul DNS 稳定寻址(如配置中的loki-query-scheduler.service.consul:9096)。
S3 凭据注入
需要访问对象存储的组(compactor、ruler、ingester、querier、index-gateway)通过 template 生成环境变量文件:
template { data = <<-EOH S3_ACCESS_KEY_ID=<access_key> S3_SECRET_ACCESS_KEY=<secret_access_key> EOH destination = "secrets/s3.env" env = true }生产环境建议把<access_key>占位符替换为 Vault 模板(参见 examples/nomad/README.md 中基于secret/minio/loki或 AWS secret engine 的写法),避免明文凭据入库。
运行与扩缩容
在loki-microservices目录内直接运行:
nomad run job.nomad.hcl新版 Nomad 也支持显式的作业命令形式:
nomad job run job.nomad.hcl如需部署指定版本,可覆盖variable.version默认值,或从命令行传入:
nomad job run -var="version=3.7.2" job.nomad.hcl扩缩容两种方式
方式一:修改作业文件中的count。注意本示例的作业由 8 个 group 组成(README 中group "loki"是泛指),需要按组分别调整各自group块内的count,然后重新运行nomad run job.nomad.hcl应用变更。
方式二:使用 Nomad CLI 动态扩缩容(推荐,无需改文件):
nomad job scale loki distributor <count>其中loki是作业名,distributor是组名,<count>为目标副本数。得益于分布式部署模式,你可以独立、精确地伸缩每个组件——例如只增加 querier 以提升查询吞吐,或只增加 ingester 以承接写入洪峰,这正是微服务模式相比单机模式的核心价值。
统一配置解析:config.yml
所有组件共享同一份 config.yml,它大量使用 Nomad 环境变量模板占位符({{ env "..." }}),由 Consul Template 在渲染时替换为实际值。逐块解读如下。
server:监听端口与 TLS 预留
server: log_level: info http_listen_port: {{ env "NOMAD_PORT_http" }} grpc_listen_port: {{ env "NOMAD_PORT_grpc" }} # grpc_tls_config: # client_auth_type: "RequireAndVerifyClientCert" # client_ca_file: "/secrets/certs/CA.pem" # cert_file: "/secrets/certs/cert.pem" # key_file: "/secrets/certs/key.pem"HTTP/GRPC 端口直接取自 Nomad 动态分配的端口环境变量。注释掉的grpc_tls_config是为后文 mTLS 方案预留的开关,启用后要求客户端证书必须通过 CA 校验(RequireAndVerifyClientCert)。
common:复制因子、ring 与故障域
common: replication_factor: 2 # Tell Loki which address to advertise instance_addr: {{ env "NOMAD_IP_grpc" }} # Failure domain # Must be the same as specified in job constraints instance_availability_zone: {{ env "node.unique.name" }} zone_awareness_enabled: true ring: # Tell Loki which address to advertise in ring instance_addr: {{ env "NOMAD_IP_grpc" }} kvstore: store: consul prefix: loki/ consul: host: {{ env "attr.unique.network.ip-address" }}:8500replication_factor: 2:每个日志流被复制到 2 个 ingester;instance_addr与ring.instance_addr:向 ring 宣告的地址取自NOMAD_IP_grpc,解决多网卡/动态 IP 场景下的地址误宣告问题(这也是 examples/nomad/README.md 特意强调不用 memberlist 而改用 Consul ring 的原因);instance_availability_zone取自node.unique.name,配合zone_awareness_enabled: true启用感知故障域的复制,必须与作业文件中 ingester 组的 constraint 对应;- ring 状态存储使用Consul(
kvstore.store: consul,前缀loki/),由 distributor、ingester、compactor、ruler 等组件共享,这也是各组件 ring 端点(/distributor/ring、/ingester/ring等)的数据来源。对应的 Ring 配置在源码中定义于 pkg/loki/common/common.go 的RingConfig与InstanceAddr字段。
ingester:WAL 与回放控制
ingester: wal: dir: {{ env "NOMAD_ALLOC_DIR" }}/data/wal flush_on_shutdown: true replay_memory_ceiling: "1G"WAL 存放在 sticky 的/alloc/data/wal,保证重启后可回放未 flush 的数据;flush_on_shutdown: true让进程在退出时主动 flush;replay_memory_ceiling: "1G"限制 WAL 回放期间的内存占用上限。
frontend 与 frontend_worker:查询链路接入
frontend: scheduler_address: loki-query-scheduler.service.consul:9096 compress_responses: true log_queries_longer_than: 5s frontend_worker: scheduler_address: loki-query-scheduler.service.consul:9096query-frontend 与 querier 的 worker 都通过 Consul DNS 连接到 query-scheduler 的固定端口 9096,实现查询请求的排队、调度与结果缓存;compress_responses: true压缩响应减少带宽,log_queries_longer_than: 5s记录慢查询。
schema_config 与 storage_config:TSDB + S3
schema_config: configs: - from: 2022-05-15 store: tsdb object_store: s3 schema: v13 index: prefix: index_ period: 24h storage_config: tsdb_shipper: active_index_directory: {{ env "NOMAD_ALLOC_DIR" }}/data/index cache_location: {{ env "NOMAD_ALLOC_DIR" }}/data/index-cache index_gateway_client: server_address: loki-index-gateway.service.consul:9097 aws: endpoint: https://minio.service.consul bucketnames: loki region: us-west-1 access_key_id: ${S3_ACCESS_KEY_ID} secret_access_key: ${S3_SECRET_ACCESS_KEY} s3forcepathstyle: true- 索引使用 TSDB 格式(schema v13),每日一个索引周期;
tsdb_shipper把活跃索引放在/alloc/data/index(Nomad 更新作业时尽量保留),并通过index_gateway_client指向 index-gateway 的 9097 端口读取共享索引; - 对象存储指向 S3 兼容端点(示例为
https://minio.service.consul),bucketnames: loki、s3forcepathstyle: true(MinIO 等兼容存储必需),凭据来自-config.expand-env=true展开的${S3_ACCESS_KEY_ID}/${S3_SECRET_ACCESS_KEY}。
compactor、ruler 与 limits_config
compactor: working_directory: {{ env "NOMAD_ALLOC_DIR" }}/compactor delete_request_store: s3 compaction_interval: 24h retention_enabled: true ruler: alertmanager_url: https://alertmanager.service.consul enable_alertmanager_v2: true enable_api: true external_url: https://loki-ruler.service.consul rule_path: {{ env "NOMAD_ALLOC_DIR" }}/tmp/rules storage: type: local local: directory: {{ env "NOMAD_TASK_DIR" }}/rules wal: dir: {{ env "NOMAD_ALLOC_DIR" }}/data/ruler limits_config: reject_old_samples: true reject_old_samples_max_age: 168hcompactor 每 24h 压缩一次索引并将删除请求存储到 S3,同时开启保留策略;ruler 使用local规则存储(规则文件通过 job.nomad.hcl 中 ruler 组的dynamic "template"从rules/**目录注入容器,详见 examples/nomad/README.md);reject_old_samples拒绝超过 168h 的陈旧写入,防止乱序数据污染。
生产建议一:统一采集各组件指标
将 config.yml 中server.http_listen_port暴露的/metrics端点接入 Prometheus。由于所有组件都注册了 Consul 服务,可以直接用consul_sd_configs一次性发现全部 8 个组件:
- job_name: "loki" consul_sd_configs: - services: - "loki-compactor" - "loki-ruler" - "loki-distributor" - "loki-ingestor" - "loki-querier" - "loki-index-gateway" - "loki-query-frontend" - "loki-query-scheduler" relabel_configs: - source_labels: ["__meta_consul_service_metadata_alloc_id"] target_label: "instance" - source_labels: ["__meta_consul_service_metadata_component"] target_label: "component"relabel 规则把 job 中service.meta里声明的alloc_id和component映射为 Prometheus 的instance与component标签,从而能在告警与面板中区分同一组件的不同副本及组件类型。
生产建议二:用 Consul Connect 保护 HTTP 端点
微服务模式下 HTTP 端点(push/query 接口、ring 状态页)不应直接暴露。README 给出了一套结合 Nomad bridge 网络、Consul Connect 与 Traefik 的加固方案。
第一步:网络设为bridge模式并显式声明三个端口——http、grpc以及专供健康检查使用的health:
network { mode = "bridge" port "http" {} port "health" {} port "grpc" {} }第二步:任务中发布全部三个端口(不要漏掉health),并在 Loki 配置中将 HTTP 绑定到回环地址 127.0.0.1,使外部无法直接访问:
task "distributor" { driver = "docker" user = "nobody" kill_timeout = "90s" config { image = "grafana/loki:${var.versions.loki}" ports = [ "http", "health", # do not forget to publish health port "grpc", ]server: http_listen_address: 127.0.0.1 http_listen_port: 80第三步:注册服务时开启 Consul Connect sidecar proxy,把本地 80 端口包起来,仅通过expose开放/metrics与/ready两个路径给健康检查使用;同时为 Traefik 打上路由与 basicauth 标签:
service { name = "loki-distributor" port = "http" meta { alloc_id = NOMAD_ALLOC_ID component = "distributor" } tags = [ "traefik.enable=true", "traefik.consulcatalog.connect=true", "traefik.http.routers.loki-distributor.entrypoints=https", "traefik.http.routers.loki-distributor.rule=Host(`loki-distributor.service.consul`)", "traefik.http.middlewares.loki-distributor.basicauth.users=promtail:$$apr1$$wnih40yf$$vcxJYiqcEQLknQAZcpy/I1", "traefik.http.routers.loki-distirbutor.middlewares=loki-distributor@consulcatalog", "traefik.http.routers.loki-distributor-ring.entrypoints=https", "traefik.http.routers.loki-distributor-ring.rule=Host(`loki-distributor.service.consul`) && Path(`/distributor/ring`)", "traefik.http.middlewares.loki-distributor-ring.basicauth.users=devops:$apr1$bNIZL02A$QrOgT3NAOx.koXWnqfXbo0", "traefik.http.routers.loki-distributor-ring.middlewares=loki-distributor-ring@consulcatalog", ] check { name = "Loki distributor" port = "health" type = "http" path = "/ready" interval = "20s" timeout = "1s" } connect { sidecar_service { proxy { local_service_port = 80 expose { path { path = "/metrics" protocol = "http" local_path_port = 80 listener_port = "http" } path { path = "/ready" protocol = "http" local_path_port = 80 listener_port = "health" } } } } } }注意:Traefik 标签中的 basicauth 密码以$$转义$,防止 Nomad 模板引擎提前展开;/distributor/ring这类 ring 状态页单独路由并配置不同的凭据。其余组件(compactor、ingester、ruler、query-frontend 等)按相同模式套用即可,作业文件中已为各组件预置了对应的 Traefik 路由标签(见 job.nomad.hcl)。basicauth 凭据可用如下命令生成:
docker run --rm httpd:alpine htpasswd -nb promtail password123生产建议三:用 Vault PKI 与 mTLS 保护 GRPC 通信
Consul Connect 无法用于组件间的 GRPC 通信——因为部分组件需要连接其他组件的所有实例(如 ingester 与 querier 之间),而 Connect 面向固定拓扑。README 给出的替代方案是用 Vault 的 PKI secret engine 为每个实例签发证书,实现 mTLS。
步骤 1:在locals中定义证书文件与 Vault 返回字段的映射,让后续模板更简洁:
locals { certs = { "CA" = "issuing_ca", "cert" = "certificate", "key" = "private_key", } }步骤 2:为每个任务添加dynamic "template",遍历local.certs生成三个文件(CA、证书、私钥),每份证书 TTL 10 天、包含服务通用名与实例 IP 的ip_sans:
dynamic "template" { for_each = local.certs content { data = <<-EOH {{- with secret "pki/issue/internal" "ttl=10d" "common_name=loki-<component_name>.service.consul" (env "attr.unique.network.ip-address" | printf "ip_sans=%s") -}} {{ .Data.${template.value} }} {{- end -}} EOH destination = "secrets/certs/${template.key}.pem" change_mode = "restart" splay = "5m" } }证书续期时(change_mode = "restart")任务会自动重启以加载新证书,splay = "5m"则避免所有实例同时重启。
步骤 3:启用配置文件中预留的 TLS 开关——服务端开启server.grpc_tls_config(要求校验客户端证书),各客户端启用grpc_client_config并指定 CA/证书/私钥路径与 snappy 压缩,例如:
ingester_client: grpc_client_config: grpc_compression: snappy tls_enabled: true tls_ca_path: "/secrets/certs/CA.pem" tls_cert_path: "/secrets/certs/cert.pem" tls_key_path: "/secrets/certs/key.pem"config.yml 中query_scheduler、frontend、frontend_worker以及storage_config.tsdb_shipper.index_gateway_client均已预留同结构的注释块,按需逐一取消注释并填写证书路径即可。
从源码看配置背后的机制
这套配置的每一处都对应 Loki 源码中的真实实现:
- Ring 与地址宣告:
common.ring、instance_addr、instance_availability_zone等字段定义于 pkg/loki/common/common.go 的CommonConfig/RingConfig结构(含InstanceAddr、InstanceInterfaceNames等字段),ring 状态最终通过 Consul KV 共享,各组件 ring 端点(如/distributor/ring、/ingester/ring、/compactor/ring、/ruler/ring)即由此驱动; - 健康检查:所有组的
check都依赖 Loki 内置的/ready端点,它反映组件是否完成启动与 ring 注册,是 Consul 健康检查与 Consul Connectexpose的共同依据; -config.expand-env=true:配合 template 生成的s3.env环境变量,实现 S3 凭据与配置的运行时注入,避免在配置文件中硬编码密钥。
小结
通过 job.nomad.hcl 与 config.yml 的组合,可以快速在 Nomad 上拉起一套由 8 个可独立伸缩组件构成的分布式 Loki:TSDB 索引 + S3 对象存储、Consul 支撑的 ring 与服务发现、基于/ready的全面健康检查,以及面向生产的 HTTP(Consul Connect + Traefik basicauth)与 GRPC(Vault PKI + mTLS)双重加固方案。
如果需要复杂度更低的起点,可以参考仓库内的另外两套 Nomad 示例:examples/nomad/loki-monolithic/README.md(单机模式)与 examples/nomad/loki-ha-monolithic/README.md(高可用单机模式);更多 Nomad 通用技巧(负载均衡、Vault 凭据、ruler 规则注入)可查阅 examples/nomad/README.md。根据集群规模与运维能力在三者之间选择,才是部署 Loki 的正确姿势。
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考