前言
日志平台接入大规模日志场景时,最典型的挑战是:日增 30TB 日志数据。这个量级下,按天固定建索引、读写不分离、检索全索引扫一遍的传统做法全部会崩:要么分片数失控,要么写入把查询资源吃光,要么一次检索要触达几千个分片。
本文整理一套在这类场景下经过论证的架构设计思路,核心是三件事:可变索引粒度 + 动态索引路由、独立协调节点实现读写分离、面向场景的检索优化。先算容量账,再谈设计,最后给出可直接复用的 ILM 策略与索引模板。
一、先把账算清楚:容量与带宽评估
动手设计之前,先把数据量的账算清楚,所有后续决策都建立在这些数字上。
网络带宽:日增 30TB,平均下来是 30TB / 86400 秒 ≈347MB/s的持续写入;启用压缩后大约可以压到110MB/s,再考虑峰值放大和重传余量,接入链路至少要具备2.5Gb/s的带宽。很多方案失败不是 ES 扛不住,而是日志根本送不进来。
写入速率:等比缩小到 30GB/天的压测模型,假设单条日志 300Byte,可以推算出每秒写入速率:
| 单条日志大小(Byte) | 日志总量(GB) | 推算记录数 | 每小时记录数 | 每秒记录数 |
|---|---|---|---|---|
| 300 | 30 | 107,374,182 | 4,473,924 | 1,243 |
压测模型按此速率回放真实数据样例,再线性外推到目标量级,比拍脑袋估容量可靠得多。另外日志量评估要有方法论:区分高峰与低谷、统计各机器的日产日志量,做成表格持续跟踪,而不是只看一个日均值。
二、总体设计:三个核心决策
- 建立可变索引粒度(日/时/分),配合动态索引路由提升查询效率——高峰期索引更细,低谷期索引更粗,分片总数可控。
- 独立部署协调节点,实现检索与数据写入的路径分离。
- 针对应用场景,从部署架构、索引结构、检索方法三方面做匹配场景的优化,而不是孤立地调某个参数。
三、可变索引粒度:日/时/分平滑切换
为什么要可变粒度
按天建索引在 30TB/天场景下单索引过大,检索要扫的分片基数太高;统一按分钟建索引又会让分片数量爆炸(每天 1440 个索引)。正确做法是让粒度跟随数据量动态变化:高峰时段用分钟级索引,低谷时段回落到小时级或天级。
设计要点
- 配置驱动:在字典表/配置中心提供粒度配置项,默认为“天”,支持“时”和“分”,调整后动态生效,上层业务无感知。
- 索引预生成:定时线程每 30 分钟巡检一次,按当前粒度提前创建未来的索引——日粒度在每天换日前 30 分钟生成次日的索引;分钟粒度在每小时第 30 分钟预生成下一小时的 60 个分钟级索引。索引单独创建的性能消耗很低、单次数量少,不影响正常读写。
- 写入侧路由:管理端将粒度配置同步到 Redis,Flink 任务从 Redis 读取粒度信息并按粒度路由写入对应索引。这样切换粒度不需要重启数据处理任务。
- 两种切换方式:
- 平滑切换:在下一个索引预生成时点,自动按新粒度生成下一轮索引;
- 强制切换:立即同步 Redis 配置、立即按新粒度预生成索引,Flink 任务定时同步配置完成写入路由切换,程序再根据数据落点确认切换是否成功。
- 查询侧路由:检索接口根据查询时间条件 + 粒度配置,生成查询范围内的索引名列表,把无关索引直接从查询里排除掉——这是降低检索基数的钥匙。
四、分片规划:经验值与容量算例
两条被反复验证的经验值:
- 每 GB 堆内存管理的分片数不超过 20 个(1GB 堆对应 50 个分片是更早的老经验值,新版本集群建议更保守);
- 单个分片承载数据 30~50GB,配合 rollover 按大小滚动。
算例:日增 30GB、保留 180 天,总量约 5.4TB;按 50GB 一个滚动索引、3 主分片计算,约 108 个滚动索引 × 3 = 324 个主分片,再加 1 副本共 648 个分片;按每 GB 堆 20 分片倒推,需要约 33GB 堆内存——对应十余个 31GB 堆的数据节点。若集群确实需要放开分片上限,可通过下面的配置调整(配合前面的容量测算使用,不要盲调):
PUT/_cluster/settings{"persistent":{"cluster":{"max_shards_per_node":5000}}}五、ILM 生命周期与索引模板落地
用 ILM 管理滚动与过期:热阶段按 50GB 或 30 天 rollover,180 天后删除:
PUT_ilm/policy/logs_policy{"policy":{"phases":{"hot":{"min_age":"0ms","actions":{"rollover":{"max_primary_shard_size":"50gb","max_age":"30d"}}},"delete":{"min_age":"180d","actions":{"delete":{"delete_searchable_snapshot":true}}}}}}配套的索引模板(字段名按通用日志模型示意,重点是多格式日期与 keyword/text 的取舍):
PUT_template/logs_app{"order":0,"index_patterns":["logs_app-*"],"settings":{"index":{"lifecycle":{"name":"logs_policy","rollover_alias":"logs_app"},"number_of_shards":"3","number_of_replicas":"1"}},"mappings":{"properties":{"log_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||strict_date_optional_time||epoch_millis"},"collect_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||strict_date_optional_time||epoch_millis"},"receive_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss.SSS||strict_date_optional_time||epoch_millis"},"save_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss.SSS||strict_date_optional_time||epoch_millis"},"log_file_id":{"type":"keyword"},"log_record_number":{"type":"long"},"host_ip":{"type":"keyword"},"log_source":{"type":"keyword"},"tag":{"type":"keyword"},"raw_message":{"type":"text"},"result":{"properties":{"level":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"message":{"type":"text","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"log_time":{"type":"date","format":"yyyyMMdd HH:mm:ss||yyyy-MM-dd HH:mm:ss.SSS||strict_date_optional_time||epoch_millis||HH:mm:ss","null_value":"1970-01-01T00:00:00Z"}}}}}}注意raw_message用 text 支撑全文检索,过滤与聚合字段一律 keyword;日期字段声明多格式兼容,避免因上游时间格式不一致导致写入失败。
六、检索侧调优
并发查询的速度高度依赖缓存命中的分片:分片数据在文件系统缓存里就快,不在就慢。围绕这一点:
- 按时间条件路由索引,让查询只触达必要的时间范围内的分片,也保证有用的分片更有机会留在缓存中。
- 读写分离:独立部署协调节点,检索链路不被数据写入抢占资源;有副本分片时查询优先指定副本分片(如
preference设置),避免与写入节点争抢。 - 缓存容量规划:分钟级 30GB 的数据量,若期望较高命中率,一般需要 100~150GB 内存作为缓存(约为数据量的 30%~50%)。
- search 线程池大小是
(number_of_processors * 3) / 2,并发用户数要按这个上限评估,超出就该扩协调节点而不是硬扛。 - 定期执行保温查询,保持热点数据在缓存中的温度。
七、字段与数据规范(踩坑点)
这部分最容易被忽视,但对检索体验影响最大:
- 四个时间戳缺一不可:日志时间(log_time)、采集时间(collect_time)、接收时间(receive_time)、保存时间(save_time)。日志检索时用户最关注的是日志时间,应以日志时间 + 行号作为主检索关键字段。
- 无时间日志的兜底:没有时间字段的日志行(如堆栈的续行)用多行合并采集功能收录;日志时间为空时,将采集时间赋给日志时间。第三方推送的日志若日志时间、采集时间都没有,用接收时间兜底赋值,并且要在接入规范里明确要求报文携带日志时间与采集时间。
- 日志文件唯一 ID:同一主机上不同文件(应用日志/运行日志)、不同主机上的同一对象都要能区分,需要为每个日志文件设置一个不要太长的唯一 ID。K8s 场景要单独考虑:Pod 漂移后日志文件换了位置续写,逻辑上仍要视为同一个文件——而采集侧往往只能定位到 Service 级别,定位不到 Pod,这个矛盾要在 ID 设计上提前想清楚。
- 分词优化:通用分词对日志并不友好,需要补充中文分词,并针对日志类文本(路径、类名、异常栈)做专门的分词分析,否则关键字查询的召回会很难看。
八、典型分析场景对设计的要求
架构最终要服务场景,两个最高频的场景:
- 错误日志分析:错误关键字查询(依赖分词质量)、错误记录的上下文查询(按主机 + 文件 + 行号滚动取前后文)、模糊匹配与正则过滤、多条件组合(某系统某主机上某类 Error 日志)。这要求字段设计里保留好过滤维度。
- 跨系统关联分析:通过全局流水号或唯一请求 ID 关联不同系统的日志;固定时间范围内多系统日志按时间排序联动展示;A→B 之间有关键字 K1、B→C 之间有关键字 K2,把三个系统的日志串起来辅助定位。这要求日志时间规范统一,否则跨系统排序就是灾难。
结语
面对日增 30TB 的日志场景,真正的核心不是某个神奇参数,而是:先算清容量账,让索引粒度随数据量弹性变化,用路由把查询基数降下来,用读写分离把资源冲突隔离开,用严格的字段规范保住检索体验。这套思路同样适用于日增几百 GB 的中等规模场景,只是粒度切换的触发点不同。
你的日志平台单日数据量到什么量级了?高峰期有没有遇到检索被写入拖垮的情况,最后是怎么解决的?欢迎在评论区交流。