4_AWS日志搜索方案

我们给一个跑在 AWS ECS Fargate 上的 Go 服务做了一套日志搜索方案。链路很短:CloudWatch Logs → Firehose → S3 → EC2 → EBS → ripgrep。日志最终落到一台小 EC2 的本地磁盘上,开发者 SSH 进去用 rg 直接全文搜索。

1. 背景:我们的需求

服务叫 my-service(化名),是一个 Go 写的实时 API:

  • 部署在 AWS ECS Fargate(AWS 托管的 Docker 调度平台)
  • 每天日志量约 200 MB(不大但不小)
  • 调试时主要靠 req_id、用户 ID、API 路径等关键词捞日志
  • 不要求实时(接受 5–10 分钟延迟)
  • 不要求超长历史(最近 7 天足够;更早的去 CloudWatch Logs 兜底)

需求清晰之后,要做的就是从一堆候选方案中挑出性价比最高的那一个。

2. 选型:10 个方案横评

我们系统地评估了 10 个候选方案,最终选了第 10 个。下面逐个讲为什么放弃前 9 个。

2.1 ELK / OpenSearch — 太重

业界经典的日志搜索栈。AWS 托管版叫 OpenSearch Service,最小可用集群(3 节点 t3.small.search)每月起步约 200 美元(含实例 + EBS 存储)。OpenSearch Serverless 更贵——最小配置 4 个 OCU(2 索引 + 2 搜索),每 OCU 0.24 美元/小时,月费约 700 美元起。

我们用不上全文索引、相关性评分、聚合分析、Kibana 仪表盘。杀鸡用牛刀。

2.2 Grafana Loki — 托管贵、自托管重

号称 “ 轻量级 ELK”,只索引标签不索引正文。Grafana Cloud 按摄入量计费:免费额度 50 GB/月,超出后 0.50 美元/GB。我们 6 GB/月刚好在免费额度内,但一旦日志量翻几倍就得掏钱。自托管需要 Kubernetes + 对象存储 + 一堆 YAML 配置。没简化反而更复杂。

2.3 Datadog / Splunk / New Relic — 商业 SaaS 太贵

按摄入 GB 收费。Datadog Log Management 摄入价 0.10 美元/GB,但要用搜索和分析功能需要 Indexed Logs,起步 1.70 美元/百万事件/月。Splunk Cloud 按摄入量 GB 计费,企业合同制。New Relic 日志摄入 0.30 美元/GB。

这些产品强在 APM + Logs + Metrics 三合一的关联体验,但我们只需要 grep 日志。对内部调试工具不划算。

2.4 CloudWatch Logs Insights — 慢、正则弱

AWS 原生的日志查询服务,不用搬数据。每次查询按扫描数据量收费 0.005 美元/GB。查询语法是 CW 自己的,不支持 PCRE 正则。每次 grep 一周日志要 5–10 秒等结果。够用但不爽。

2.5 CloudWatch Logs Live Tail — 只能看实时

2023 年推出的功能,类似 tail -f。支持按关键词过滤和高亮,近实时流式输出。按会话时长计费 0.01 美元/分钟。

致命限制:只能看当前正在写入的日志,不能搜索历史。适合实时盯着看,不适合事后排查。而且单会话最长 3 小时,超过 500 条/秒会采样。只能当辅助工具。

2.6 S3 + Athena — 启动慢,体验糟

把日志归档到 S3,用 Athena 跑 SQL 查询。Athena 按扫描数据量收费 5 美元/TB。启动延迟 5–10 秒(要编译查询计划、启动执行引擎)。

如果日志是 Parquet/ORC 格式且做了分区,Athena 的效率很高——但日志通常是 JSONL/纯文本,扫描效率低。适合分析、不适合调试时随手 grep。

2.7 S3 Select — 行级过滤但限制多

在 S3 侧直接做行级过滤,不启动 Athena。按扫描量 + 返回量收费(扫描 0.002 美元/GB,返回 0.0007 美元/GB)。

限制:只支持 CSV/JSON/Parquet 格式,不支持 gzip 内嵌多 member(我们的日志格式),单文件最大 128 MB(非压缩),SQL 子集极小(不支持 JOIN、子查询、正则)。对我们的场景几乎不可用。

2.8 Fluent Bit 直推 S3 — 绕过 CW Logs 省钱

用 Fluent Bit 替代 awslogs 驱动,从容器 stdout 直接推到 S3,省掉 CloudWatch Logs 的 0.50 美元/GB 摄入费。Fluent Bit 的 S3 output 插件支持 gzip 压缩、按时间分区、自动 multipart upload。

为什么没选:ECS Fargate 使用 Fluent Bit 需要配 FireLens(AWS 的 sidecar 日志路由器),多一个 sidecar 容器要额外分配 CPU/内存。而且我们的 CW Logs 已经存在(合规要求),再加一路 Fluent Bit 等于维护两套日志 pipeline。省下的钱不够覆盖运维复杂度。

2.9 Vector / Fluentd — 更重的 Pipeline

Vector(Rust 写)和 Fluentd(Ruby 写)是通用的日志 pipeline 工具。功能比 Fluent Bit 更丰富,但也更重。在 Fargate 上跑 sidecar 的问题和 Fluent Bit 一样。自建 aggregator 集群又回到了 “ 重基础设施 “ 的老路。不适合我们的极简需求。

2.10 最终方案:把日志投到一台小 EC2,本地 Grep

思路简单粗暴:

把生产日志同步一份到一台 EC2 的本地磁盘,用 ripgrep 直接 grep 文件。

ripgrep(rg)是 Rust 写的 grep 替代品,原生支持 gzip 解压、并行搜索,单核可达 200 MB/s。一台 2 vCPU 的 EC2 + 100 GB 本地磁盘,足够装 7 天热数据。

这种思路能跑通的前提有三个:日志规模在 GB 量级(不是 TB)、允许少量延迟、且只服务内部开发者。我们都满足。

2.11 结构化对比

方案月费估算(6 GB/月)查询延迟运维复杂度查询能力扩展上限
OpenSearch 托管~$200 起秒级中(集群管理)全文索引 + 聚合TB 级
OpenSearch Serverless~$700 起秒级全文索引 + 聚合TB 级
Grafana Loki 托管免费~$3秒级标签过滤 + LogQLTB 级
Datadog Log Mgmt~$10+秒级无(SaaS)全文 + facetTB 级
CW Logs Insights~$0.03/次5–10 秒CW 查询语法TB 级
CW Live Tail~$0.01/分实时关键词过滤仅实时流
S3 + Athena<$0.01/次5–10 秒SQLPB 级
S3 Select<$0.01/次秒级SQL 子集单文件 128 MB
Fluent Bit + S3~$20(EC2)分钟级中(sidecar)grepGB 级
EC2 + ripgrep(已选)$21亚秒正则全文GB 级

方案选定后,下一步是把整条数据流串起来。

3. 架构总览

flowchart LR
    A[ECS Task<br/>Go App stdout] -->|awslogs driver| B[CloudWatch Logs<br/>Log Group]
    B -->|Subscription Filter| C[Kinesis Data Firehose<br/>5-min buffer / GZIP]
    C -->|S3 PUT| D[S3 Transit Bucket<br/>1-day lifecycle]
    D -->|aws s3 cp<br/>via Gateway Endpoint| E[EC2 t4g.small<br/>parse-firehose.py]
    E -->|append per-hour gzip| F[EBS 100GB gp3<br/>/logs/yyyymmdd/]
    G[Developer<br/>laptop] -->|aws ssm start-session| E
    E -->|rg -za| F

3.1 第一站:App Stdout → CloudWatch Logs

Go 应用用 log/slog 写到 stdout:

1
slog.Info("api request", "method", "POST", "path", "/api/v1/heartbeat", "req_id", id)

ECS Fargate 的 task definition 配置 awslogs 日志驱动:

1
2
3
4
5
6
7
8
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/apps/my-project/prod/my-service",
"awslogs-region": "us-west-2",
"awslogs-create-group": "true"
}
}

ECS 容器引擎自动收集 stdout/stderr,分批推到 CloudWatch Logs(CW Logs)的对应 Log Group。这一步应用方本来就有——所有合规的容器化应用都会配,不是我们这次新加的。

CW Logs 计费:0.50 美元/GB 摄入费 + 0.03 美元/GB·月 存储费。retention 默认 30 天。

日志进了 CW Logs,下一步是把它 “ 分流 “ 出来——既要保留 CW Logs 原数据满足合规,又要能进入我们自己的搜索链路。

3.2 第二站:Subscription Filter → Firehose → S3

Subscription Filter

CW Logs 有个叫 subscription filter 的功能:可以把一个 Log Group 里的实时日志流旁路一份到下游(Lambda / Kinesis Streams / Firehose 三选一)。纯转发,不影响原始 CW Logs 数据。

为什么选 Firehose

Kinesis Data Firehose(现已更名为 Amazon Data Firehose)是托管的数据投递服务:你往里推数据,它替你做缓冲、压缩、格式转换、最终写到目标存储(S3 / Redshift / OpenSearch / 自定义 HTTP)。不用写消费者代码。

和 Kinesis Data Streams 的区别:

Kinesis Data StreamsKinesis Data Firehose
角色原始消息流投递管道
消费方式自己写 consumer,管 checkpointAWS 帮你写到目标存储
延迟毫秒级几十秒到几分钟
计费按 shard 小时数(固定开销)按摄入数据量(按需)

Firehose 计费简单:只按摄入数据量收费,0–500 TB/月 档位 0.029 美元/GB。没有 stream 数量费、没有小时费、没有最低消费。我们 200 MB/天 ≈ 6 GB/月 → 每月 0.18 美元。

CloudWatch Logs 本身有 “export to S3” 功能,但只能批量触发(最快每小时一次)、格式不可控、跨账号有 IAM 麻烦。Firehose 是流式的、5 分钟级延迟、文件格式可控。

Firehose 关键配置

1
2
3
4
5
6
7
8
9
10
11
12
extended_s3_configuration {
prefix = "!{timestamp:yyyy}!{timestamp:MM}!{timestamp:dd}/"
buffering_size = 64 # MB,攒满 64 MB 立即投递
buffering_interval = 300 # 秒,每 5 分钟强制投递
compression_format = "GZIP"

processing_configuration {
processors {
type = "AppendDelimiterToRecord" # 每条记录之间加 \n
}
}
}

buffering_sizebuffering_interval 是 “ 或 “ 关系——任意先到先投递。低流量服务的端到端延迟约等于 buffering_interval(5 分钟)。

CW Logs → Firehose 数据格式(最大坑)

CW Logs subscription filter 投给 Firehose 的数据,每条记录长这样:

flowchart TB
    subgraph Outer["Firehose S3 文件(外层 GZIP)"]
        direction TB
        R1["record_1<br/>(binary gzip)"]
        N1["\\n"]
        R2["record_2<br/>(binary gzip)"]
        N2["\\n"]
        R3["..."]
    end

    subgraph Inner["每条 record_i 解 gzip 后"]
        J["{<br/>  &quot;messageType&quot;: &quot;DATA_MESSAGE&quot;,<br/>  &quot;owner&quot;: &quot;...&quot;,<br/>  &quot;logGroup&quot;: &quot;...&quot;,<br/>  &quot;logEvents&quot;: [<br/>    {&quot;timestamp&quot;: ..., &quot;message&quot;: &quot;...&quot;},<br/>    ...<br/>  ]<br/>}"]
    end

    R1 -.解 gzip.-> Inner

外层 Firehose 给整批记录套了一层 gzip(来自 compression_format = "GZIP")。里面是若干二进制 gzip 记录用换行分隔(来自 AppendDelimiterToRecord)。每个 gzip 记录解开是一坨 JSON,包含一个 logEvents 数组。

⚠️ 踩坑:很多文章和半官方文档暗示 “CW Logs 投给 Firehose 的数据是 base64 编码 “,我们最初的解码 pipeline 就用 base64 -d 处理。结果生产数据里的第一字节是 1f 8b(gzip 魔数),不是 ASCII。实际是原始二进制 gzip,没有 base64 一层。这导致解码 pipeline 全部产出空文件。

修复:用 Python 按 1f 8b 切分记录边界,逐个 gzip.decompress() + json.loads() + 提取 logEvents[].message

数据格式搞清楚后,剩下的就是把 S3 文件搬到 EC2 并解码落盘。

3.3 第三站:S3 Transit Bucket → EC2

S3 transit bucket 配了 1 天的 lifecycle expire——文件被 EC2 处理完就 aws s3 rm 删掉,万一处理失败的兜底也只留 24 小时。这个 bucket 永远只有几十兆数据。

EC2 上一个每分钟跑的 cron 同步:

1
2
3
4
5
6
7
8
9
10
while IFS= read -r s3_key; do
filename=$(basename "$s3_key")
date_dir=$(dirname "$s3_key") # 比如 20260425
output_dir="/logs/$date_dir"
tmpfile=$(mktemp /tmp/sync-XXXXXXXX.gz)
aws s3 cp "s3://$BUCKET/$s3_key" "$tmpfile" --quiet \
&& python3 /usr/local/bin/parse-firehose.py "$tmpfile" "$output_dir" "$filename" \
&& aws s3 rm "s3://$BUCKET/$s3_key" --quiet
rm -f "$tmpfile"
done < <(aws s3 ls "s3://$BUCKET/" --recursive | awk '{print $4}')

parse-firehose.py 干两件事:

  1. 解码:按 gzip magic 切记录 → 每条 gzip.decompress → 提取 logEvents[].message
  2. 按小时折叠:根据文件名解析出年月日时(my-project-logs-prod-my-service-2-2026-04-25-15-05-00-uuid.gz2026-04-25-15),把消息 append 到 /logs/{yyyymmdd}/{prefix}-{yyyy-MM-dd-HH}.gz

💡 小知识:gzip 文件可拼接

cat a.gz b.gz > c.gzzcat c.gz 能完整读两份内容。这是 RFC 1952 规定的 multi-member gzip stream 行为。我们的 append 实现就是基于这个:先把旧 hour 文件复制到 tmp,再追加新 gzip 流,最后 os.rename(tmp, target) 原子替换。

3.4 第四站:用户怎么搜

开发者本地:

1
2
3
4
5
6
7
8
9
10
# 进实例(aws ssm session manager 协议,TLS 加密)
aws ssm start-session --target i-0xxxxxxxxxxxx --region us-west-2

# 我们封装了一个内部 CLI 命令
# 内部按 tag 找到实例自动进入
my-cli logs shell

# 进去后直接搜
rg -za "req_id=abc123" /logs/
rg -za "panic|ERROR" /logs/20260425/

⚠️ 踩坑 5:rg 默认会把含 null byte 的文件当二进制跳过。生产日志里偶尔有 binary 字节(base64 解码后的某些 padding 上下文),导致 rg -z keyword /logs/ 漏掉文件。

修复:加 -a(”as text”),变成 rg -za

链路全部跑通后,再看一眼账单——还能不能更便宜。

4. 成本优化历程

4.1 起点:56 美元/月

项目月费
EC2 c6g.medium(1 dedicated vCPU,2 GB)$25
EBS 100 GB gp3$8
3 × VPC Interface Endpoints(ssm/ssmmessages/ec2messages)$22
Firehose 摄入(6 GB/月)$0.18
S3 + CloudWatch Alarms 等<$1
$56

4.2 优化 A:去掉 3 个 SSM Interface Endpoints(省 22 美元)

⚠️ 踩坑:原方案为了 “ 完全私网部署 “,给 EC2 配了 3 个 VPC Interface Endpoint(让 SSM 的控制流和数据流不经公网)。每个 Interface Endpoint 收 0.01 美元/小时,三个一个月 ~22 美元。

但我们的 shared VPC 本来就有 NAT Gateway(ECS 应用们在用)。SSM 走 NAT 出公网到 ssm.us-west-2.amazonaws.com,全程 TLS,安全性没差别。NAT 的小时费已经被 ECS 应用摊掉了,我们只多消耗几 KB/分钟的心跳流量(按 0.045 美元/GB 算约半美分/月)。

关键判断:S3 Gateway Endpoint 必须保留——它免费、且让日志数据不经 NAT,未来日志量增长不会带来 NAT 数据处理费。

4.3 优化 B:c6g.medium → t4g.small(省 13 美元)

最初选 c6g.medium(compute 系列,1 个永久独占 vCPU)。但我们的 CPU 用量画像是:99% 的时间闲、偶尔 rg 跑几秒打满。这是突发型负载的教科书场景。

t4g.small:2 个突发型 vCPU、2 GB RAM,月费 12 美元。CPU 积分平时一直涨,搜索时短促爆发完全够用。反而比 c6g.medium 的单核更快(rg 自带多线程并行)。

4.4 终点:21 美元/月

项目月费
EC2 t4g.small(2 burstable vCPU,2 GB)$12
EBS 100 GB gp3$8
Firehose 摄入$0.18
周边<$1
$21

整体省了 35 美元/月,降幅 62%。回过头看,几个关键判断值得提炼。

5. 总结

  1. 现成方案不一定都得用。日志量在 GB 级、用户都是内部工程师时,” 一台 EC2 + ripgrep” 比 ELK / Loki / Datadog 简单一个数量级、便宜两个数量级。
  2. 选型要看需求不看功能。10 个方案里有 7 个功能比我们强,但我们不需要全文索引、不需要 SQL、不需要仪表盘——需要的只是 “ 用正则 grep 最近 7 天的日志 “。
  3. Firehose 是数据管道里的万金油。不用写 consumer、按摄入量计费、低延迟(5 分钟)。但 CW Logs 投递格式有暗坑——是二进制 gzip 不是 base64。
  4. gzip 可拼接这个特性能让 “ 按时间折叠 “ 做得极简:cat new.gz >> old.gz 就完了,配合原子 rename 保证读者永远看到完整文件。
  5. AWS” 最佳实践 “ 要看上下文。3 个 SSM Interface Endpoints 在 “ 完全私网无 NAT” 场景是必须的,但在 “ 已有 NAT” 的 VPC 里就是白花钱。盲目套 AWS 推荐架构容易做出过度设计。
  6. t- 系列实例对突发型负载是福音。compute 系列贵,但很多场景的 CPU 是 99% 闲 + 1% 爆——这正是 t- 系列的甜区。