ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多台服务器日志分散难查?用Promtail+Loki+Grafana搭建集中检索平台

多台服务器日志分散难查?用Promtail+Loki+Grafana搭建集中检索平台 线上服务一旦拆到多台机器日志查询就会变成一件很烦的事。我维护的几个后端服务分布在四台服务器上平时排查问题基本靠ssh登上去再tail -f或者grep。单机还好一旦某个请求跨了多个服务或者要对比几台机器同一时间段的报错就得开好几个终端来回切效率低到让人怀疑人生。之前也考虑过 ELK但 Elasticsearch 对内存要求比较高我的机器上还要跑业务进程不太想为了查日志再吃掉几个 G 内存。后来选了 Loki 这套方案它的思路和 ES 不一样只对标签建立索引日志正文压缩后直接存对象存储或本地磁盘资源占用小很多。配合 Promtail 采集、Grafana 查询整套跑起来内存占用比我预期低。这篇文章记录一下我从零搭这套平台的过程包括配置、标签设计、查询语法以及跑起来之后真实遇到过的问题。为什么是 Loki 而不是 ELK先说选型。这不是说 Loki 比 ELK 好而是场景不同。方案优点缺点适用场景ELKElasticsearch Logstash Kibana全文检索强聚合分析能力完整生态成熟资源占用高ES 对内存和磁盘 IO 要求大运维成本高日志量大、需要复杂全文检索和聚合分析的团队Loki Promtail Grafana资源占用低和 Prometheus/Grafana 体系天然契合部署简单不支持全文索引查询依赖标签过滤聚合能力弱于 ES中小规模、已有 Grafana、主要靠标签定位问题的场景直接 ssh grep零部署成本多机、跨服务、历史回溯场景基本不可用单机、临时排查我这边的情况是服务数量不算多日志量每天大概几个 G查询诉求主要是某台机器某个服务在某段时间的报错而不是全文搜索某个关键词出现在哪些日志里。这种以标签定位为主的查询正好是 Loki 的强项。【关键结论】如果你的查询主要是按服务、主机、时间范围过滤Loki 性价比很高如果经常要做全文关键词检索和复杂聚合ES 更合适。组件分工三个组件各管一段Promtail部署在每台被采集的机器上负责读本地日志文件打上标签推送到 Loki。Loki接收日志按标签建索引把日志内容压缩存储。Grafana连到 Loki 作为数据源提供查询界面和仪表盘。数据流向大致是日志文件 → Promtail采集打标签→ Loki索引存储→ Grafana查询展示。部署方式我用的是 docker-compose把 Loki 和 Grafana 放在一台机器上下面叫它日志服务器Promtail 单独部署在每台业务机上。这样业务机只需要跑一个很轻的 Promtail 容器。先看 Loki 和 Grafana 这一侧。我用的版本是 Loki 2.9.x 和 Grafana 10.x这两个版本当前比较稳定配置写法也是现在主流的。# docker-compose.yml —— 部署在日志服务器version:3.8services:loki:image:grafana/loki:2.9.6container_name:lokiports:-3100:3100volumes:-./loki-config.yml:/etc/loki/local-config.yaml-./loki-data:/lokicommand:-config.file/etc/loki/local-config.yamlrestart:unless-stoppedgrafana:image:grafana/grafana:10.4.2container_name:grafanaports:-3000:3000volumes:-./grafana-data:/var/lib/grafanaenvironment:-GF_SECURITY_ADMIN_PASSWORDchange_medepends_on:-lokirestart:unless-stoppedLoki 的配置文件我用了接近单机默认的写法主要确认存储路径和监听端口# loki-config.ymlauth_enabled:falseserver:http_listen_port:3100common:path_prefix:/lokistorage:filesystem:chunks_directory:/loki/chunksrules_directory:/loki/rulesreplication_factor:1ring:kvstore:store:inmemoryschema_config:configs:-from:2024-01-01store:tsdbobject_store:filesystemschema:v13index:prefix:index_period:24hlimits_config:reject_old_samples:truereject_old_samples_max_age:168h这里有几个点值得说一下。auth_enabled: false是关掉多租户单机自己用没必要开。schema: v13是当前 Loki 推荐的 schema 版本和 tsdb 存储配合。reject_old_samples_max_age: 168h是拒绝超过 7 天的旧日志防止客户端时钟不对导致乱序写入。【注意】schema_config里的from日期一旦定下来后续如果要改 schema需要新增一条配置项而不是改这一条Loki 是按时间段选择 schema 的。这一点官方文档有说明改错了会导致旧数据读不出来。filesystem存储适合单机或者挂载了共享存储的场景。如果日志量再大、想上对象存储比如 S3、MinIO把object_store换成对应类型即可这个我没实际配过就不展开了。Promtail 采集配置业务机上的 Promtail 配置是重点标签怎么打直接决定了后面查询好不好用。# promtail-config.yml —— 部署在每台业务机server:http_listen_port:9080grpc_listen_port:0positions:filename:/tmp/positions.yamlclients:-url:http://日志服务器IP:3100/loki/api/v1/pushscrape_configs:-job_name:app-logsstatic_configs:-targets:-localhostlabels:job:apphost:server-01__path__:/var/log/myapp/*.logpositions.yaml记录每个文件读到哪一行了Promtail 重启后从断点继续不会重复推送。这个文件要保证可写。clients指向 Loki 的 push 接口。scrape_configs里__path__是匹配日志文件的路径labels里定义的标签会附加到这批日志上。上面这个配置的意思是采集/var/log/myapp/下所有.log文件给它们打上jobapp、hostserver-01两个标签。每台机器上的配置只有host不一样其他都一样。实际部署时我用了一个模板部署脚本里替换 host 值。标签设计的坑这里是我踩得比较实的一个问题。一开始我想当然地把一些动态字段也做成了标签比如把日志里的request_id、user_id提取出来当标签。结果 Loki 很快就报警说标签基数太高查询也变得很慢。原因是 Loki 的索引是基于标签组合的。标签的唯一组合数量基数一旦爆炸索引就撑不住了。request_id这种几乎每条都不一样的值做成标签等于给每条日志建一个索引完全违背了 Loki 的设计初衷。【踩坑提醒】标签只放基数低、可枚举的维度比如host、job、env、level。像request_id、user_id、trace_id这种高基数字段要么放在日志正文里用查询语法过滤要么用 Loki 的 structured metadata需要较新版本且行为有差异我没有深入验证。调整之后我的标签就固定在host、job、env这几个上labels:job:apphost:server-01env:prod这样标签组合数量就是机器数 × 服务数 × 环境数非常可控。日志格式建议Loki 查询时如果日志是结构化 JSON过滤会方便很多。我这边后端服务用的是 Python日志输出改成了 JSON 格式方便后续按字段过滤。# logging_config.pyimportjsonimportloggingimportsysclassJsonFormatter(logging.Formatter):defformat(self,record:logging.LogRecord)-str:payload{ts:self.formatTime(record,%Y-%m-%dT%H:%M:%S%z),level:record.levelname,logger:record.name,message:record.getMessage(),}# 如果有额外字段比如 request_id一并带上ifhasattr(record,request_id):payload[request_id]record.request_idifrecord.exc_info:payload[exc]self.formatException(record.exc_info)returnjson.dumps(payload,ensure_asciiFalse)defget_logger(name:str)-logging.Logger:loggerlogging.getLogger(name)logger.setLevel(logging.INFO)handlerlogging.StreamHandler(sys.stdout)handler.setFormatter(JsonFormatter())logger.addHandler(handler)returnlogger这样每条日志就是一行 JSON。注意request_id是放在正文里的不是标签查询时用 Loki 的 JSON 解析语法过滤。在 Grafana 里查日志Grafana 加 Loki 数据源很简单Configuration → Data Sources → Add data source → 选 LokiURL 填http://loki:3100如果 Grafana 和 Loki 在同一个 compose 网络里或者日志服务器 IP。配好之后在 Explore 页面就能查了。Loki 的查询分两部分标签选择器和过滤器。标签选择器必须至少有一个非空的匹配条件比如{hostserver-01, jobapp}这会查出这台机器上 app 服务的所有日志。然后可以加过滤器{hostserver-01, jobapp} | ERROR|是包含某个字符串!是不包含|~是正则匹配。这些都是对日志正文做过滤配合标签先缩小范围效率会好很多。如果日志是 JSON可以用| json解析后按字段过滤{hostserver-01, jobapp} | json | levelERROR还可以按 request_id 查一条请求的完整链路即使它跨了多个服务{jobapp} | json | request_idabc-123这比挨个 ssh 上去 grep 快太多了。常用查询整理几个我平时用得比较多的写法查某台机器最近的报错{hostserver-01, jobapp} | ERROR查某个服务所有机器上的 500 错误{jobapp} | json | status500统计每分钟错误日志条数用于做告警sum(count_over_time({jobapp} | ERROR [1m]))排除健康检查这类噪音{jobapp} ! healthcheck第 3 条这种是 metric 查询可以直接配成 Grafana 的告警规则。实际遇到的问题标签基数报警前面提过一开始把 request_id 做成标签Loki 日志里出现max label cardinality类似的告警查询也明显变慢。把高基数字段从标签里去掉之后恢复正常。这是 Loki 使用中最容易犯的错没有之一。时间范围查询慢Loki 查询性能和时间范围强相关。查最近 15 分钟很快查最近 7 天就会慢因为它要扫过整个时间段的数据。我的做法是日常排查控制在几小时内需要长期回溯的场景单独处理而不是一上来就查一周。limits_config里的max_query_length可以限制单次查询的最大时间跨度我设了一个相对保守的值防止有人误查超大范围把 Loki 拖垮。这个参数具体取值要结合自己的数据量和机器配置我没有一个通用数字。磁盘增长日志是持续写入的磁盘会一直涨。Loki 本身有 retention 配置但我用的是 filesystem 存储实际清理行为需要结合compactor相关配置一起看这块我配得比较简单靠reject_old_samples_max_age控制写入定期手动清理旧 chunks。如果对保留策略有严格要求建议用对象存储配合 retention 配置或者接一个定时清理脚本。这一点我没有做完整的自动化验证属于待完善的部分。是否值得搭回到最开始的问题。这套方案适合什么样的场景服务分布在多台机器ssh 逐台查已经明显影响效率查询以标签时间范围为主不是重度全文检索机器资源有限不想为日志系统投入太多内存已经在用 Grafana希望日志和指标在同一个界面看。如果这几点都符合Loki 这套组合的性价比很高。如果日志量特别大、或者需要 ES 那种全文检索和复杂聚合那就老老实实上 ELK别硬套。我搭完之后最直观的变化是排查一个跨服务问题的时间从开四个终端来回切变成了在 Grafana 里一条查询搞定。尤其是按 request_id 追链路之前基本靠手动拼现在直接查。后续我打算再补两块一是把错误日志的 metric 查询配上 Grafana 告警出问题主动通知而不是等人来查二是把 retention 策略做成自动化的省得手动清磁盘。这两块落地之后这套平台才算真正完整。
返回列表