一、标签基数失控怎么就拖垮了Loki
你有没有过这种情况:用Loki查日志的时候,明明日志量不大,却等了好几秒甚至几十秒才出结果?或者系统突然卡得不行,运维查半天发现是Loki的CPU、内存占满了?大概率是标签基数出了问题。
先给你说个真实的小例子:之前有个做电商的朋友,为了追踪每个用户的操作日志,给Loki的日志加了个user_id的标签,每个用户的ID都是唯一的。结果大促当天,百万用户同时访问,这个标签的取值直接从几千涨到了几百万,Loki的存储瞬间占满,查询也直接超时了——这就是标签基数失控的典型表现。
那为什么标签基数这么重要?Loki是靠标签来组织日志的,相当于给每批日志贴分类标签,比如服务名、环境、日志级别这些,查询的时候就靠标签快速定位。但标签有个核心规则:每个标签的取值不能太多,不然Loki就得存超多的索引,就像你把一本字典的每个字都单独做个目录,字典本身会变得超大,翻找也会特别慢。
1.1 标签基数失控的核心影响
标签基数失控的影响主要体现在三个方面:
第一是存储爆炸。Loki的索引是按标签取值来存的,比如user_id有100万个不同的值,索引就得存100万条对应的记录,哪怕每条记录只有几KB,加起来也会占大量存储,甚至直接把磁盘撑爆。
第二是查询变慢。查询的时候,Loki得先查索引定位日志,标签取值太多的话,索引查询本身就会花很长时间,相当于你要从100万条目录里找你要的那本内容,速度肯定快不了。
第三是资源耗尽。Loki处理标签的时候会占用CPU和内存,标签基数突然暴涨,会让Loki的处理线程瞬间占满,甚至导致服务崩溃,影响整个日志系统的使用。
二、怎么判断自己的标签基数失控了
要解决问题,首先得能发现问题。你可以通过Loki自带的工具来查标签基数,不用自己写复杂的脚本。这里给你一个完整的Shell命令示例,技术栈是Shell,你直接复制就能用:
# 技术栈:Shell
# 功能:查询Loki中所有标签的基数(即每个标签的不同取值数量)
# 前提:已配置好Loki的访问地址和端口,并且有权限访问
curl -s http://your-loki-address:3100/api/v1/labels | jq '{"data":.data}' | jq 'for label in .data; do echo label; curl -s http://your-loki-address:3100/api/v1/label/$label/values | jq length; done'
这个命令的作用是先获取Loki里所有的标签,再逐个查每个标签有多少个不同的取值,最后输出每个标签对应的基数。
正常来说,标签基数应该控制在多少以内?一般来说,常见的标签比如service(服务名)、env(环境)、level(日志级别),基数一般在几十以内,最多不超过几百。如果某个标签的基数突然涨到几千甚至几万、几十万,那肯定是失控了。
比如刚才说的电商大促的例子,正常情况下service标签的基数只有十几个(对应十几个服务),env只有2个(测试和生产),但user_id的基数直接涨到了几百万,明显超出了合理范围。
2.1 常见的失控标签类型
哪些标签最容易出现基数失控?主要有这几类:
第一是用户相关的标签,比如user_id、user_ip、user_token,每个用户的ID、IP都是唯一的,只要用户量上来,基数就会暴涨。
第二是业务流水相关的标签,比如order_id、trade_id、request_id,每笔订单、每个请求的ID都是唯一的,业务量一大,基数就会快速增长。
第三是时间相关的标签,比如timestamp(精确到毫秒)、datetime(精确到秒),每一条日志的时间都不一样,基数自然会非常大。
这些标签本身不是不能用,而是不能直接当Loki的标签来用,不然就会出问题。
三、从源头遏制标签基数爆炸的治理方案
既然标签基数失控的危害这么大,那怎么从源头解决?核心思路就是:把高基数的标签从Loki的标签里去掉,要么不存,要么存在日志内容里,只留低基数的标签用来分类。
3.1 方案一:直接去掉不需要的高基数标签
最简单的方法就是去掉那些根本不需要的高基数标签。比如有些开发为了方便调试,会把user_id、request_id这些标签加上,但其实这些标签在日常运维中根本用不到,只是调试的时候偶尔用,那完全可以去掉。
怎么去掉?以最常用的日志收集工具Promtail为例,给你一个完整的配置示例,技术栈是YAML(Promtail的配置格式):
# 技术栈:YAML(Promtail配置)
# 功能:过滤掉高基数的标签user_id和request_id,只保留低基数的标签
# 配置位置:Promtail的config.yaml文件的scrape_configs部分
scrape_configs:
- job_name: 'your-service'
static_configs:
- targets:
- localhost
labels:
service: your-service # 低基数标签,保留
env: production # 低基数标签,保留
level: info # 低基数标签,保留
pipeline_stages:
# 第一步:从日志内容中提取user_id和request_id(如果需要的话)
- regex:
expression: 'user_id=(\w+).*request_id=(\w+)'
named_capture_groups:
user_id: 1
request_id: 2
# 第二步:过滤掉提取的user_id和request_id,不让它们成为标签
- labeldrop:
- user_id
- request_id
这个配置的作用是,先从日志内容里提取user_id和request_id,然后通过labeldrop配置把这两个标签去掉,不让它们进入Loki的标签体系。
3.2 方案二:把高基数标签转成日志内容
如果有些高基数标签确实需要用到,比如user_id,需要在查某个用户的日志的时候能定位到,那可以把这些标签转成日志内容,只在需要的时候通过日志内容来过滤。
怎么实现?还是以Promtail为例,给你一个示例,技术栈是YAML:
# 技术栈:YAML(Promtail配置)
# 功能:把高基数的标签转成日志内容,只保留低基数标签
# 配置位置:Promtail的config.yaml文件的pipeline_stages部分
pipeline_stages:
# 第一步:从日志中提取高基数标签user_id
- regex:
expression: 'user_id=(\w+)'
named_capture_groups:
user_id: 1
# 第二步:把提取的user_id转成日志内容的一部分,而不是标签
- template:
source: output
template: '{{ .entry }} user_id={{ .user_id }}'
# 第三步:过滤掉user_id标签,不让它进入Loki
- labeldrop:
- user_id
这样配置之后,user_id就会存在日志内容里,不会成为Loki的标签,基数就不会失控。如果需要查某个用户的日志,只需要用Loki的|~ "user_id=12345"命令来过滤日志内容就可以了。
3.3 方案三:对高基数标签做聚合处理
如果有些高基数标签必须要用来分类,那可以对这些标签做聚合处理,降低基数。比如user_id,可以按用户的注册时间、地域来聚合,把同一个地域、同一年注册的用户归为一类,这样标签的基数就会大大降低。
举个例子,把user_id转成user_region标签,比如用户来自北京的就归为user_region=beijing,来自上海的归为user_region=shanghai,这样标签的基数就从几百万降到了几十个。给你一个Promtail的配置示例,技术栈是YAML:
# 技术栈:YAML(Promtail配置)
# 功能:对高基数的user_id标签做地域聚合,降低基数
# 配置位置:Promtail的config.yaml文件的pipeline_stages部分
pipeline_stages:
# 第一步:从日志中提取user_id
- regex:
expression: 'user_id=(\w+)'
named_capture_groups:
user_id: 1
# 第二步:根据user_id的前缀判断地域(假设user_id前缀1开头是北京,2开头是上海)
- regex:
source: user_id
expression: '^1'
named_capture_groups:
user_region: beijing
- regex:
source: user_id
expression: '^2'
named_capture_groups:
user_region: shanghai
# 第三步:过滤掉原来的user_id标签,保留聚合后的user_region标签
- labeldrop:
- user_id
这样处理之后,标签的基数就会大大降低,既满足了分类的需求,又不会导致基数失控。
四、治理方案的应用场景、优缺点和注意事项
4.1 应用场景
这三种治理方案分别适用于不同的场景:
第一种方案(直接去掉标签)适用于那些根本不需要用到的高基数标签,比如开发为了调试临时加的request_id、trace_id等,日常运维中根本用不到,直接去掉就可以。
第二种方案(转成日志内容)适用于那些偶尔需要用到,但不需要经常按这个标签分类的高基数标签,比如user_id、order_id等,偶尔查一下某个用户的日志,只需要过滤内容就可以。
第三种方案(聚合处理)适用于那些必须要按高基数标签分类的场景,比如需要按用户地域、订单类型来统计日志,这时候聚合处理是最好的选择。
4.2 技术优缺点
三种方案的优缺点也很明显: 第一种方案的优点是最简单,直接去掉标签,不会有任何额外的处理,缺点是如果后续需要用到这个标签,就得重新加,比较麻烦。 第二种方案的优点是灵活,既不会导致基数失控,又能保留标签的内容,需要的时候可以过滤,缺点是查询的时候会比直接按标签分类慢一点,因为需要扫描日志内容。 第三种方案的优点是既能保留分类的功能,又能控制基数,查询速度也快,缺点是需要做聚合处理,配置相对复杂一点,而且聚合的粒度要控制好,不能太粗也不能太细。
4.3 注意事项
在实施治理方案的时候,有几个注意事项: 第一,不要随便加标签。加标签之前要先想清楚,这个标签是不是真的需要,是不是低基数的,不要为了方便随便加,不然很容易导致基数失控。 第二,定期检查标签基数。可以每周或者每月用之前的Shell命令查一下标签的基数,及时发现失控的标签,提前处理。 第三,聚合处理要合理。聚合的粒度不能太粗,不然会失去分类的意义,也不能太细,不然基数还是会失控,要根据实际的业务需求来调整。 第四,测试再上线。在正式上线治理方案之前,一定要在测试环境测试一下,看看配置是不是正确,会不会影响日志的收集和查询,避免上线之后出问题。
五、文章总结
标签基数失控是导致Loki性能下降的核心原因之一,很多人在使用Loki的时候,只关注日志的收集和查询,却忽略了标签的管理,结果导致性能问题,甚至影响整个系统的运行。
解决标签基数失控的核心思路是从源头控制,不要把高基数的标签当成Loki的标签,要么去掉,要么转成日志内容,要么做聚合处理。在实施治理方案的时候,要根据实际的业务场景选择合适的方案,同时要注意定期检查标签基数,避免再次出现失控的情况。
只要做好标签的管理,控制好标签的基数,Loki的性能就能得到很大的提升,日志系统也能稳定运行,为业务的发展提供有力的支持。
Comments