在微服务和 API 网关后面,访问日志(Request Log / Access Log)通常是产生最快、体积最大的一类数据。

每天 10 亿到 20 亿条时,峰值写入在每秒 3 万到 7 万条,一周的热数据有 16.8 到 33.6 TB。写入跟不上时,bulk 请求会被拒绝,报 OpenSearchRejectedExecutionException(见 源码)。

规模推导与指标基线#

先把「每天 1B~2B 条日志」换算成写入速率和存储量。假设单条请求日志(方法、路径、状态码、响应时间、客户端信息、TraceID 等)原始大小在 800 B~1.5 KB 之间,写入索引后的磁盘占用按每条 1.2 KB 计:

指标 1B(10 亿条 / 天) 2B(20 亿条 / 天) 算法 / 说明
平均写入速率 约 11,600 docs/s 约 23,100 docs/s 每天条数 ÷ 86,400
峰值写入速率 约 30,000~35,000 docs/s 约 60,000~70,000 docs/s 按均值的 2.5~3 倍,考虑白天高峰和突发流量
单日主分片增量 约 1.2 TB 约 2.4 TB 每天条数 × 1.2 KB
单日总增量(一主一副) 约 2.4 TB 约 4.8 TB 主分片增量 × 2
7 天热数据 约 16.8 TB 约 33.6 TB 放在热节点的本地 NVMe 上
30 天快照(只含主分片) 约 36 TB 约 72 TB 每天主分片增量 × 30,放在 S3

按原始大小 800 B~1.5 KB 算,峰值 60,000~70,000 docs/s 对应约 48~105 MB/s 的写入流量。每个 bulk 按 3,000 条算,峰值时每秒约 20~23 个 bulk 请求。

端到端写入链路#

业务 Pod 和日志收集器不直接写 OpenSearch,中间隔一层消息队列和一层批量写入。AWS 的建议是在上游做流量控制,并让聚合层留出足够的缓冲空间,应对突发流量和集群的短时维护(见 Operational best practices):

flowchart LR Apps["Kubernetes Pods / API 网关"] --> Kafka["Kafka / AWS Kinesis
(削峰填谷,至少缓冲 24h)"] Kafka --> Ingest["Vector / Data Prepper
(消费并组装 bulk)"] Ingest --> Coord["Coordinating 节点
(接收 bulk,按分片路由)"] Coord --> HotData["Hot data 节点
(本地 NVMe,负责建索引)"] HotData -->|"每天快照,保留约 30 天"| Warm["对象存储(S3)"]

Kafka / Kinesis 缓冲层#

OpenSearch 滚动重启、段合并或短时压力大的时候,积压的日志留在 Kafka 里,上游不会因此超时或丢数据。Topic 的分区数要和下游消费者的并行度匹配,Kafka 按分区并行读写(见 Kafka 文档)。以 60,000 docs/s、24~48 个分区计,每个分区约 1,250~2,500 docs/s。

Vector / Data Prepper 消费层#

消费层可以用 Vector(Rust 实现,见 GitHub)或 OpenSearch 的 Data Prepper(Java 实现,以 jar 包运行,见 Getting started with OpenSearch Data Prepper)。bulk 大小可以按 AWS 的建议从每个请求 3~5 MiB 起步,逐步加大,直到索引吞吐不再提升;一次 bulk 发几千条文档,比几千次单条请求更高效(见 Operational best practices)。requestBody、stacktrace 这类字段可能带着敏感信息,适合在消费层写入之前脱敏:Data Prepper 有 obfuscate processor,Vector 的 VRL 有 redact 函数。

节点角色分离#

这个规模下,不同职责分给不同的节点(节点角色见 Creating a cluster):

  • Cluster manager 节点 3 台,node.roles: [ cluster_manager ],只维护 cluster state 和节点状态,不承担数据和客户端请求。OpenSearch 文档的建议是三台专职 cluster manager 分布在三个可用区。
  • Coordinating 节点 4~6 台,node.roles: [ ],接收 Vector 或 Data Prepper 发来的 bulk 请求,按文档路由到对应的分片。AWS 的说法是专职协调节点把请求协调从数据节点上卸下来,按负载不同,索引吞吐最多能提升约 15%(见 Dedicated coordinator nodes)。
  • Hot data 节点,node.roles: [ data ],负责建索引、写段和 merge。

写入调优#

下面四项都写在索引模板里,彼此独立:segment replication 让副本不再重复建索引,refresh 和 translog 设置减少刷新和 fsync,zstd 压缩存储字段,mapping 控制字段数量和倒排内容。

用 segment replication 代替 document replication#

默认的 document replication 下,主分片把文档转发给副本,副本要把分词和建索引重做一遍,索引 CPU 随副本数线性增加。Segment replication 在 OpenSearch 2.7 正式可用:只有主分片做文本分析和建索引,副本直接复制主分片生成的段文件(见 Segment replication)。

OpenSearch 博客给的基准是 stackoverflow 数据集、10 个主分片加 1 个副本,写入吞吐最多提升 25%;试用版用户反馈的数字是最多 40%(见 这篇博客)。代价落在网络和主分片上:主分片要把段文件推给所有副本,副本越多,主分片越容易成为瓶颈,复制延迟也越长,副本上看到新数据会更晚。文档还提到,主分片数越多,segment replication 相对 document replication 的优势越小,并建议打开 cluster.routing.allocation.balance.prefer_primary,让主分片在节点间分布均匀。

在索引模板中配置:

{
  "settings": {
    "index.replication.type": "SEGMENT"
  }
}

refresh 与 translog 设置#

OpenSearch 默认每秒 refresh 一次,写入后约 1 秒就能搜到。日志通常不需要秒级可见,AWS 建议把所有索引的 refresh_interval 设到 30 秒或更长;refresh 越少,索引性能越好,代价是新数据要等更久才能搜到(见 Operational best practices)。

在索引模板中调整:

{
  "settings": {
    "index.refresh_interval": "30s",
    "index.translog.durability": "async",
    "index.translog.sync_interval": "30s",
    "index.translog.flush_threshold_size": "1gb"
  }
}
  • refresh_interval: 30s:从默认的 1 秒放宽到 30 秒(默认值见 index settings)。
  • translog.durability: async:默认是 request,每个写请求确认之前都要 fsync translog;改成 async 后按 sync_interval 在后台 fsync(默认 5 秒,这里设成 30 秒),节点故障时会丢掉上次 fsync 之后已经确认的写入,最多约 30 秒(同上文档)。这部分数据还在 Kafka 里,但如果消费端在 bulk 成功后就提交了 offset,回放要手动把消费位点往回拨(at-least-once 下消费端先处理、再保存位点,见 Confluent 文档)。回放会把一部分文档写两遍:写入时不指定 _id,OpenSearch 为每条自动生成 ID,重放的都是新文档;要去掉重复,可以用 Kafka 的 topic、分区和 offset 拼出确定的 _id,同一个 _id 再写一次只会更新原来那条文档(见 Index document)。
  • flush_threshold_size: 1gb:默认 512 MB。OpenSearch 的 Tuning for indexing speed 里的例子是内存大于 32 GB 的实例设成 1024 MB;阈值越大,flush 越少,但分片故障后要重放的 translog 越大,恢复越慢。

zstd 压缩#

OpenSearch 2.9 起,index.codec 可以设成 zstd(带字典压缩)或 zstd_no_dict,index.codec.compression_level 取 1~6。codec 只影响存储字段(stored fields)的压缩。文档里的基准用的是 nyc_taxi 数据集:和默认的 LZ4 相比,zstd 磁盘占用减少 35%、写入吞吐提升 7%,zstd_no_dict 磁盘占用减少 30%、吞吐提升 14%(见 Index codecs)。日志数据的效果要用自己的样本测。

{
  "settings": {
    "index.codec": "zstd",
    "index.codec.compression_level": 3
  }
}

Mapping 控制#

  • 关掉自动的 dynamic mapping,在 mapping 里设 "dynamic": "strict",新字段不会被意外加进来(见 Operational best practices)。
  • URL、requestBody、stacktrace 这类长文本只映射成 text,不加 .keyword 子字段。dynamic mapping 默认会给字符串同时建 text 和 keyword 子字段(见 Mappings)。
  • 不需要评分和短语查询的日志正文,可以设 "index_options": "docs",只记录词出现在哪些文档里,不存词频和位置,这样也就做不了短语查询(见 index_options)。OpenSearch 2.12 起也可以直接用 match_only_text 字段类型,它不存位置、词频和 norms,所有命中文档的得分都是常数(见 match_only_text)。

分片与生命周期#

单分片 30~50 GiB#

AWS 的分片建议是:搜索延迟敏感的负载单分片 10~30 GiB,日志这类写多的负载 30~50 GiB。分片太大,故障恢复困难;分片太多太小,每个分片都要占 CPU 和内存,容易出现性能问题甚至内存不足(见 Choosing the number of shards)。

Data stream 与 ISM rollover#

按自然天切索引(如 logs-YYYY.MM.dd)时,各天流量不同,分片大小会差很多。改用 data stream 以后,写请求只进当前的 write index,查询覆盖所有 backing index(见 Data streams),滚动时机交给 ISM 策略。策略里的 ism_template 负责把策略自动套到新建的 backing index 上,对 data stream 的 backing index,ISM 用所属 data stream 的名字去匹配 index_patterns(见 Policies 和 ManagedIndexCoordinator.kt)。下面的 logs-* 按实际的 data stream 名字改:

{
  "policy": {
    "description": "Billion scale logging ISM policy",
    "default_state": "hot",
    "ism_template": [
      {
        "index_patterns": ["logs-*"],
        "priority": 100
      }
    ],
    "states": [
      {
        "name": "hot",
        "actions": [
          {
            "rollover": {
              "min_index_age": "1d",
              "min_primary_shard_size": "40gb"
            }
          }
        ],
        "transitions": [
          {
            "state_name": "delete",
            "conditions": {
              "min_index_age": "7d"
            }
          }
        ]
      },
      {
        "name": "delete",
        "actions": [
          {
            "delete": {}
          }
        ]
      }
    ]
  }
}

本地索引 7 天后由这条策略删除,30 天的保留交给快照。Snapshot Management(SM)每天把 logs-* 快照到 S3 仓库;快照是增量的,只包含主分片,删除快照要走 API,OpenSearch 只清掉其他快照不再引用的数据(见 Snapshot management 和 Take and restore snapshots,S3 仓库要先在所有节点上装 repository-s3 插件再注册,步骤也在 Take and restore snapshots 里)。一个索引在本地的最后一天也会被当天的快照带上,快照再保留 23 天,合起来约 30 天;要查 7 天前的日志,从快照里恢复对应的索引:

POST _plugins/_sm/policies/logs-daily
{
  "creation": {
    "schedule": {
      "cron": { "expression": "0 1 * * *", "timezone": "UTC" }
    }
  },
  "deletion": {
    "schedule": {
      "cron": { "expression": "0 3 * * *", "timezone": "UTC" }
    },
    "condition": { "max_age": "23d" }
  },
  "snapshot_config": {
    "indices": "logs-*",
    "repository": "s3-logs",
    "include_global_state": "false",
    "date_format": "yyyy-MM-dd-HH:mm"
  }
}

按单分片 40~50 GB 算,1B 规模每天约 1.2 TB 主分片数据,对应 24~30 个主分片(加上副本 48~60 个);2B 规模每天约 2.4 TB,对应 48~60 个主分片(加上副本 96~120 个)。同一时刻承担写入的只有当前 write index 的主分片。AWS 建议主分片数取数据节点数的整数倍,让分片均匀落在各个节点上(见 Operational best practices);热节点按下一节定为 14 台,索引模板里设 index.number_of_shards: 14。按 min_primary_shard_size: 40gb 滚动,一个 backing index 约 560 GB,2B 规模每天滚动 4 次左右;峰值 70,000 docs/s 摊到 14 个主分片上,每个约 5,000 docs/s。

容量规划与硬件选型#

按 2B 规模、峰值 60,000~70,000 docs/s,用 AWS Graviton 实例估算:

节点类型 实例规格 数量 配置要点
Cluster manager 4 vCPU / 8 GiB(c6g.xlarge,见 C6g 实例) 3 heap 4 GB,只维护集群元数据
Coordinating 8 vCPU / 16 GiB(c6g.2xlarge) 4~6 heap 8 GB,接收 bulk 请求并路由
Hot data 16 vCPU / 128 GiB,1 × 3,750 GB 本地 NVMe(i4g.4xlarge,见 I4g 实例) 14 heap 31 GB,其余约 90 GB 留给操作系统和 page cache。OpenSearch 建议 heap 取内存的一半(见 安装文档),但 heap 到 32 GB 就用不上压缩指针(见 Oracle 文档),所以取 31 GB
快照仓库 对象存储(S3) 按需 SM 每天快照,保留约 30 天;要在线查 7 天前的数据,自建集群可以用 searchable snapshot,Amazon OpenSearch Service 上可以用 UltraWarm

热节点数量的推导#

节点数先按存储定,再把写入能力当作上线前要压测的指标:

  • 存储:7 天热数据约 33.6 TB(含副本)。内存优化型的 r6gd.4xlarge 只有 1 × 950 GB 本地盘(见 R6g 实例),12 台合计约 11.4 TB,放不下;换成 vCPU 和内存相同、本地盘 3,750 GB 的 i4g.4xlarge,14 台合计 52.5 TB,平均每台 2.4 TB,使用率约 64%,在 60%~70% 之间。OpenSearch 的磁盘水位默认在 85% 时停止往节点分配分片,90% 时开始把分片迁走(见 Cluster settings)。
  • 写入:峰值 70,000 docs/s 摊到 14 台,每台要稳定写入 5,000 docs/s。这个数要用 OpenSearch Benchmark 的 http_logs workload,换成自己的日志样本压测确认;单台达不到,就按实测吞吐加节点。
  • 节点故障:本地 NVMe 跟着实例走,一台热节点坏了,它上面约 2.4 TB 的分片要在其余节点上重建副本,这段时间相关分片只剩一份。重建速度受 indices.recovery.max_bytes_per_sec 限制,默认每个节点 40 MB/s,是动态设置,重建时可以临时调高(见 RecoverySettings.java)。其余 13 台接住这部分数据后,平均使用率约 69%,仍低于 85% 的低水位。

小结#

这套规划从峰值 60,000~70,000 docs/s 和每天约 2.4 TB 主分片数据出发:Kafka 缓冲上游流量,Vector 或 Data Prepper 组装 bulk,cluster manager、coordinating 和热数据节点分开部署。写入侧用 segment replication、放宽 refresh 与 translog、zstd 和收紧 mapping 降低开销,分片由 data stream 和 ISM 滚动,控制在 30~50 GiB。热节点按 7 天热数据的存储定为 14 台 i4g.4xlarge,磁盘使用率约 64%;每台 5,000 docs/s 的写入能力要在上线前压测确认。7 天之后本地索引由 ISM 删除,SM 每天把快照写进 S3,保留约 30 天,需要时再恢复。

参考资料#