2. Elasticsearch数据模型与可观测性数据建模
可观测性系统的数据底座最终落在 Elasticsearch 上,而 Elasticsearch 不是“把 JSON 丢进去就能查”的数据库。它要求我们在写入之前就想清楚三件事:数据以什么单位组织(文档)、这些单位如何被分组与分片(索引)、每个字段以什么类型和方式被解析与检索(映射)。这三者共同构成 Elasticsearch 的数据模型,也直接决定了后续日志检索、指标聚合、链路追踪的查询性能与存储成本。
文档、索引与映射:三层数据模型
文档(Document)是 Elasticsearch 中可被索引的最小 JSON 单元,一条日志、一个采样点、一个 span 都是一条文档。索引(Index)是文档的逻辑集合,也是分片、副本、刷新、合并等物理行为的载体。映射(Mapping)则定义了索引中文档的字段结构:字段名、字段类型、是否索引、是否存储、使用何种分析器。三者关系可以理解为:文档是“行”,索引是“表”,映射是“表结构”,但 Elasticsearch 的映射更灵活也更危险——它可以动态推断,而动态推断在可观测性场景中往往是性能事故的起点。
字段类型选择:keyword、text、数值与日期的分工
可观测性数据里最常见的字段类型有四类。第一类是 keyword,用于精确匹配、聚合和排序,例如 service.name、log.level、host.name、trace.id。第二类是 text,用于全文检索,例如日志正文 message;但 text 默认不能直接聚合,通常需要同时定义 keyword 子字段(multi-field)来支持按原文聚合。第三类是数值类型,如 long、double,用于指标值、耗时、状态码;其中状态码虽然看起来是数字,但若只做等值过滤和分组,用 keyword 反而更省空间。第四类是 date,用于 @timestamp,必须显式指定格式,否则跨时区解析会出错。
一个常见的错误是把所有字段都交给动态映射。动态映射会把首次出现的字符串推断为 text 并附加 keyword 子字段,把数字推断为 long 或 float。当某天某个字段突然出现非数字值,写入就会失败;当某个高基数字段(如 trace.id)被推断为 text,倒排索引会急剧膨胀。因此可观测性索引应当使用索引模板(Index Template)预先声明映射,只对确实需要全文检索的字段使用 text。
时序数据建模:索引滚动与 ILM
日志和指标都是典型的时序数据,共同特征是写入量大、按时间查询、旧数据价值递减。Elasticsearch 的标准做法是按时间切分索引,例如日志用 logs-<service>-YYYY.MM.dd,指标用 metrics-<dataset>-YYYY.MM.dd。这样做的收益是:查询可以只命中相关时间范围的索引,避免全量扫描;删除旧数据只需删除整个索引,而不是逐条删除文档;不同时间段的索引可以配置不同的分片数和副本数。
索引生命周期管理(ILM)把索引划分为 hot、warm、cold、delete 四个阶段。以日志为例:hot 阶段接收写入,refresh_interval 设为 5s 保证可检索延迟;当索引超过 50GB 或 1 天,滚动到新索引并进入 warm,副本降为 0、段合并为 1;7 天后进入 cold,可迁移到低成本节点;30 天后删除。指标数据通常保留更久,但采样粒度可以随阶段降低,例如原始 10s 精度保留 7 天,之后降采样为 1 分钟精度保留 90 天。
日志与指标的数据模型设计差异
日志模型强调“可检索的上下文”。一条日志文档至少应包含 @timestamp、service.name、host.name、log.level、message,以及可选的 trace.id、span.id 用于与链路关联。message 保留原文,同时通过 ingest pipeline 抽取结构化字段,例如从 Nginx 日志中提取 http.response.status_code、url.path、client.ip。抽取后的字段用 keyword 或数值类型,避免每次查询都做正则。
指标模型强调“可聚合的维度与数值”。一条指标文档通常包含 @timestamp、metricset.name、host.name、service.name 等维度字段,以及一个或多个数值字段如 system.cpu.total.pct、http.request.duration.ms。维度字段用 keyword,数值字段用 double 或 long。与日志不同,指标查询几乎总是聚合查询,因此映射中应关闭不需要检索的字段的 index,只保留 doc_values 以支持聚合和排序,从而降低倒排索引开销。
text、keyword 还是仅 doc_values。一个可参考的日志索引模板片段
下面给出一个面向应用日志的索引模板示例,字段命名遵循 Elastic Common Schema(ECS)风格,便于与 Filebeat、Logstash、OpenTelemetry 采集器对接。示例中的分片数、刷新间隔和 ILM 策略均为教学假设值,实际部署需按数据量和集群规模调整。
PUT _index_template/logs-app-template
{
"index_patterns": ["logs-app-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s",
"lifecycle.name": "logs-app-policy"
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"service.name": { "type": "keyword" },
"host.name": { "type": "keyword" },
"log.level": { "type": "keyword" },
"message": { "type": "text",
"fields": { "raw": { "type": "keyword", "ignore_above": 1024 } } },
"trace.id": { "type": "keyword" },
"span.id": { "type": "keyword" },
"http.response.status_code": { "type": "keyword" },
"url.path": { "type": "keyword" },
"event.duration": { "type": "long" }
}
}
}
}
这里把 dynamic 设为 strict,意味着任何未在映射中声明的字段写入都会报错。这在初期会增加调试成本,但能强制团队在采集端就统一字段命名,避免“同一个含义出现三种字段名”的混乱。若担心采集端字段不稳定,可先用 dynamic: false 让未知字段被存储但不索引,观察一段时间后再补全映射。
建模质量如何验证
映射写完后不能只看配置文件,要用真实查询验证。第一,用 _field_caps 检查关键字段的实际类型是否符合预期,尤其是被动态推断的字段。第二,用聚合查询验证维度字段是否可聚合,例如按 service.name 做 terms 聚合统计错误日志数量。第三,观察索引的 _stats,关注 segments.memory 和 store.size,若某个 keyword 字段基数异常高,应考虑是否误把 trace.id 这类字段用于聚合。第四,检查查询延迟,若日志检索出现明显慢查询,通常是因为 message 被当作 keyword 全量索引,或时间范围没有命中索引裁剪。
数据模型不是一次性的设计文档,而是随可观测性需求演进的契约。新增一个采集字段、调整一次日志格式、引入一种新的指标,都可能需要同步更新映射与索引模板。把映射纳入版本管理,并在采集端与查询端之间保持字段命名一致,是让可观测性系统长期可用的基础工作。