2. Elasticsearch数据模型与可观测性数据建模

可观测性系统的数据底座最终落在 Elasticsearch 上,而 Elasticsearch 不是“把 JSON 丢进去就能查”的数据库。它要求我们在写入之前就想清楚三件事:数据以什么单位组织(文档)、这些单位如何被分组与分片(索引)、每个字段以什么类型和方式被解析与检索(映射)。这三者共同构成 Elasticsearch 的数据模型,也直接决定了后续日志检索、指标聚合、链路追踪的查询性能与存储成本。

文档、索引与映射:三层数据模型

文档(Document)是 Elasticsearch 中可被索引的最小 JSON 单元,一条日志、一个采样点、一个 span 都是一条文档。索引(Index)是文档的逻辑集合,也是分片、副本、刷新、合并等物理行为的载体。映射(Mapping)则定义了索引中文档的字段结构:字段名、字段类型、是否索引、是否存储、使用何种分析器。三者关系可以理解为:文档是“行”,索引是“表”,映射是“表结构”,但 Elasticsearch 的映射更灵活也更危险——它可以动态推断,而动态推断在可观测性场景中往往是性能事故的起点。

Elasticsearch 可观测性数据模型三层结构 文档 Document {"@timestamp":"...", "service":"order", "level":"ERROR", ...} 最小可索引 JSON 单元 一条日志 / 一个采样点 / 一个 span 写入时生成 _id 与 _source 不可变,更新即重建 示例:单条应用错误日志 索引 Index logs-order-2025.06.01 主分片 3 / 副本 1 refresh_interval 5s 文档的逻辑集合 分片 / 副本 / 刷新的载体 按时间滚动:logs-* ILM 管理生命周期 示例:按天滚动的日志索引 映射 Mapping "level": keyword "message": text+keyword "duration": long 字段类型与索引方式 决定能否聚合 / 排序 dynamic 推断风险点 显式定义优于自动推断 示例:日志索引字段定义 组成 约束

字段类型选择: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 全量索引,或时间范围没有命中索引裁剪。

数据模型不是一次性的设计文档,而是随可观测性需求演进的契约。新增一个采集字段、调整一次日志格式、引入一种新的指标,都可能需要同步更新映射与索引模板。把映射纳入版本管理,并在采集端与查询端之间保持字段命名一致,是让可观测性系统长期可用的基础工作。