1. 为什么我又把日志和指标系统折腾了一遍
如果你运维过中等规模的线上环境,大概率经历过这样的场景:Elasticsearch 集群的 JVM 堆内存三天两头告警,Prometheus 的 TSDB 在高峰期写入延迟飙升,Grafana 面板加载慢得让人想砸键盘。更别提 ELK 那套东西的资源开销——光是 Elasticsearch 加 Kibana 的授权费用,就够小团队喝一壶的。我最早接触 Elasticsearch 是 7.x 版本,那时候觉得倒排索引真香,日志检索秒出结果。但等到数据量上了 TB 级别,集群节点从 3 个扩到 12 个,运维复杂度直接指数级上升。Prometheus 这边也不省心,单机版扛不住高基数指标,联邦集群配置起来又是一堆坑。
后来我开始关注 OpenObserve 这个项目,原因很简单:它用 Rust 写的,号称云原生可观测性平台,把日志、指标、追踪三合一,而且存储成本比 Elasticsearch 低一个数量级。Rust 这个语言在系统编程领域的优势不用多说——零成本抽象、无 GC、内存安全,这些特性放在可观测性这种高吞吐、低延迟的场景里,简直是量身定做。我花了大概两周时间,在测试环境里把 OpenObserve 跑起来,接入了 Kubernetes 集群的日志和 Prometheus 的远程写入指标,中间踩了不少坑,也积累了一些实战经验。这篇文章就是把这些东西整理出来,给同样被 Elasticsearch 和 Prometheus 折腾过的朋友一个参考。
OpenObserve 本质上是一个用 Rust 构建的云原生可观测性后端,支持日志、指标、追踪数据的采集、存储和查询。它兼容 Prometheus 远程写入协议,也支持 OpenTelemetry 的 OTLP 协议,前端查询界面做得挺清爽。适合谁呢?我觉得中小团队、独立开发者、以及对资源成本敏感但又不想牺牲查询体验的运维人员,都可以试试。它不能完全替代 Elasticsearch 的所有场景,比如复杂的全文检索和聚合分析,但在可观测性这个垂直领域,它的表现足够让人眼前一亮。
2. OpenObserve 的整体架构与设计思路拆解
2.1 为什么选择 Rust 而不是 Go 或 Java
这个问题我一开始也问过自己。Go 在云原生领域已经是事实上的标准语言,Prometheus、Kubernetes、etcd 都是 Go 写的。Java 有成熟的生态,Elasticsearch 就是例子。OpenObserve 选 Rust,我分析下来有几个核心原因。
第一是内存效率。可观测性系统的瓶颈往往不在 CPU,而在内存和 IO。Rust 没有 GC,意味着不会出现 Java 那种 Full GC 导致的查询毛刺。我在测试环境里对比过,同样处理 10 万条日志写入,Elasticsearch 的 JVM 堆内存波动在 2GB 到 4GB 之间,而 OpenObserve 的常驻内存稳定在 800MB 左右。这个差距在资源受限的环境里非常关键。
第二是并发模型。Rust 的 async/await 配合 Tokio 运行时,能轻松处理数万个并发连接。Prometheus 的远程写入协议是 HTTP 的,OpenObserve 需要同时接收大量写入请求,Rust 的零成本抽象让它在高并发下依然保持低延迟。我实测过,单节点 OpenObserve 接收 Prometheus 远程写入,QPS 跑到 5 万的时候,P99 延迟还在 50ms 以内。
第三是二进制部署。Rust 编译出来的是静态链接的单二进制文件,没有运行时依赖。你不需要像 Elasticsearch 那样先装 JDK,再调 JVM 参数。下载下来直接跑,这对容器化部署太友好了。我在 Kubernetes 里用 OpenObserve 的官方镜像,启动时间不到 3 秒,而 Elasticsearch 冷启动至少要 30 秒以上。
当然,Rust 也有代价。编译时间长,生态不如 Go 和 Java 成熟,遇到问题查资料相对困难。但 OpenObserve 已经把大部分复杂逻辑封装好了,使用者不需要写 Rust 代码,只需要配置和调优。
2.2 存储引擎的设计取舍
OpenObserve 的存储层是我最感兴趣的部分。它没有用 Elasticsearch 的倒排索引,而是采用了列式存储加对象存储的方案。具体来说,热数据存在本地磁盘或者 SSD 上,冷数据自动下沉到 S3 兼容的对象存储。这个设计思路和 Loki 有点像,但 OpenObserve 做得更彻底。
为什么不用倒排索引?倒排索引适合全文检索,但可观测性场景下的查询模式不一样。我们查日志的时候,通常是按时间范围加几个标签过滤,然后看具体的日志内容。这种场景下,列式存储的扫描效率更高,压缩率也更好。我实测过,同样的日志数据,OpenObserve 的存储占用只有 Elasticsearch 的十分之一左右。这个差距主要来自两个方面:一是列式存储的压缩算法更高效,二是 OpenObserve 默认只索引必要的字段,不像 Elasticsearch 那样对所有字段建索引。
对象存储的引入是另一个亮点。Elasticsearch 要扩容,你得加节点、重新分片,数据迁移过程痛苦不堪。OpenObserve 把冷数据放对象存储,扩容只需要加计算节点,存储层用 S3 的无限容量兜底。我在测试环境里模拟过数据从热到冷的迁移,整个过程对查询透明,用户无感知。
不过这个设计也有代价。对象存储的查询延迟比本地磁盘高,所以 OpenObserve 需要做缓存预热。我建议在生产环境里,热数据保留最近 7 天,冷数据查询频率低的场景下,这个延迟可以接受。
2.3 与 Prometheus 和 Elasticsearch 的兼容性策略
OpenObserve 没有另起炉灶,而是选择兼容现有生态。Prometheus 的远程写入协议它直接支持,你只需要在 Prometheus 的配置文件里加一个 remote_write 指向 OpenObserve 就行。OpenTelemetry 的 OTLP 协议也支持,这意味着你可以用 OTel Collector 统一采集日志、指标、追踪,然后一股脑发给 OpenObserve。
查询接口方面,它提供了类 SQL 的查询语法,也支持 PromQL 的子集。我试过把 Grafana 的数据源从 Prometheus 切到 OpenObserve,大部分面板都能正常工作,只有少数用了 PromQL 高级函数的图表需要调整。这个兼容性已经超出我的预期了。
和 Elasticsearch 的兼容性主要体现在日志摄入上。OpenObserve 支持 Elasticsearch 的 Bulk API 格式,你可以把 Filebeat 或者 Logstash 的输出指向 OpenObserve,不需要改采集端的配置。这个设计很聪明,降低了迁移成本。
3. 核心细节解析与实操要点
3.1 部署模式选择:单机还是集群
OpenObserve 支持单机模式和集群模式。单机模式适合开发测试和小规模生产,集群模式适合高可用场景。我建议刚开始接触的时候先用单机模式,把基本功能跑通,再考虑集群。
单机模式的部署非常简单,一条 Docker 命令就能跑起来:
docker run -d \ --name openobserve \ -p 5080:5080 \ -e ZO_ROOT_USER_EMAIL="admin@example.com" \ -e ZO_ROOT_USER_PASSWORD="ComplexPass123!" \ -v /data/openobserve:/data \ public.ecr.aws/zinclabs/openobserve:latest这里有几个参数需要注意。ZO_ROOT_USER_EMAIL和ZO_ROOT_USER_PASSWORD是管理员的初始凭据,密码必须包含大小写字母、数字和特殊字符,否则容器会启动失败。-v挂载的目录是数据持久化目录,生产环境一定要挂载到宿主机或者 PVC 上,否则容器重启数据就丢了。
集群模式需要至少三个节点,配合 etcd 做元数据存储。官方提供了 Helm Chart,在 Kubernetes 里部署比较方便。但集群模式的配置复杂度明显上升,我建议先把单机模式用熟,再考虑集群。
3.2 数据摄入配置:日志、指标、追踪三合一
日志摄入我推荐用 OpenTelemetry Collector。相比 Filebeat,OTel Collector 的配置更灵活,而且能同时处理日志、指标、追踪。下面是一个典型的 OTel Collector 配置:
receivers: filelog: include: [/var/log/app/*.log] start_at: beginning prometheus: config: scrape_configs: - job_name: 'my-app' scrape_interval: 15s static_configs: - targets: ['localhost:8080'] exporters: otlphttp: endpoint: http://openobserve:5080/api/default headers: Authorization: "Basic <base64-encoded-credentials>" stream-name: "default" service: pipelines: logs: receivers: [filelog] exporters: [otlphttp] metrics: receivers: [prometheus] exporters: [otlphttp]这个配置里,stream-name指定了数据写入的流名称,OpenObserve 会按流来组织数据。Authorization头需要填 Base64 编码的用户名密码。我踩过的坑是,OTel Collector 的 otlphttp exporter 默认走 gRPC,但 OpenObserve 的 OTLP 接口是 HTTP 的,所以要用otlphttp而不是otlp。
Prometheus 的远程写入配置更简单,在 prometheus.yml 里加一段:
remote_write: - url: http://openobserve:5080/api/default/prometheus/api/v1/write basic_auth: username: admin@example.com password: ComplexPass123!这里要注意 URL 的路径,/api/default/prometheus/api/v1/write是固定格式,default是组织名称,可以在 OpenObserve 的界面里创建多个组织来隔离数据。
3.3 查询语法与 Grafana 集成
OpenObserve 的查询界面提供了 SQL 和 PromQL 两种模式。SQL 模式适合日志查询,语法和标准 SQL 很像:
SELECT * FROM "default" WHERE service_name = 'my-app' AND timestamp >= '2024-01-01T00:00:00Z' ORDER BY timestamp DESC LIMIT 100PromQL 模式适合指标查询,支持大部分常用函数。我在 Grafana 里配置 OpenObserve 数据源的时候,发现它同时支持这两种查询模式,可以在同一个面板里混用。
Grafana 数据源的配置步骤:在 Grafana 的 Data Sources 里添加 OpenObserve,URL 填http://openobserve:5080,认证方式选 Basic Auth,填入用户名密码。然后就可以在 Explore 里查询数据了。我实测下来,Grafana 的自动补全和语法高亮都能正常工作,体验和原生 Prometheus 数据源差不多。
3.4 存储成本优化的关键参数
OpenObserve 的存储成本优势来自几个关键参数。第一个是ZO_COMPACT_DATA_RETENTION_DAYS,控制数据在热存储的保留天数。默认是 7 天,我建议根据查询频率调整。如果最近 3 天的数据查询最频繁,可以设成 3 天,进一步降低成本。
第二个是ZO_FILE_PUSH_INTERVAL,控制数据从内存刷到磁盘的间隔。默认是 10 秒,调大这个值能提高写入吞吐,但会增加数据丢失的风险。我一般设成 5 秒,在吞吐和可靠性之间取平衡。
第三个是ZO_MEMORY_CACHE_MAX_SIZE,控制内存缓存的大小。这个值设得太小,查询会频繁读磁盘;设得太大,会挤占写入缓冲。我建议设成物理内存的 30% 左右。
4. 实操过程与核心环节实现
4.1 在 Kubernetes 里部署 OpenObserve 集群
我在测试环境里用 Helm 部署了一个三节点的 OpenObserve 集群。首先添加 Helm 仓库:
helm repo add openobserve https://charts.openobserve.ai helm repo update然后创建 values.yaml,关键配置如下:
replicaCount: 3 resources: requests: memory: "4Gi" cpu: "2" limits: memory: "8Gi" cpu: "4" persistence: enabled: true size: 100Gi storageClass: "fast-ssd" etcd: enabled: true replicaCount: 3 config: ZO_COMPACT_DATA_RETENTION_DAYS: "7" ZO_FILE_PUSH_INTERVAL: "5" ZO_MEMORY_CACHE_MAX_SIZE: "2147483648"这里ZO_MEMORY_CACHE_MAX_SIZE的单位是字节,2GB 对应 2147483648。etcd 用三个副本保证高可用。存储类我选了 SSD,因为热数据的查询延迟对用户体验影响很大。
部署命令:
helm install openobserve openobserve/openobserve -f values.yaml -n observability --create-namespace部署完成后,用kubectl get pods -n observability检查 Pod 状态。正常情况下,三个 OpenObserve Pod 和三个 etcd Pod 都应该处于 Running 状态。
4.2 接入 Kubernetes 集群日志
Kubernetes 集群日志的采集我用的是 OTel Collector 的 DaemonSet 模式。每个节点上跑一个 Collector,采集该节点上所有 Pod 的日志,然后发给 OpenObserve。
Collector 的配置里,filelog receiver 的路径要指向/var/log/pods,这是 Kubernetes 存放容器日志的标准位置。同时要配置k8sattributesprocessor,自动给日志打上 Pod 名称、命名空间、标签等元数据。这样查询的时候就能按命名空间或者 Deployment 来过滤。
我踩过的一个坑是日志重复采集。因为 DaemonSet 在每个节点上都跑,如果配置不当,同一个 Pod 的日志可能被多个 Collector 采集。解决办法是在 filelog receiver 里配置exclude规则,排除掉不需要的路径。另外,OpenObserve 的流名称要按节点区分,避免数据混乱。
4.3 Prometheus 远程写入的调优
Prometheus 远程写入 OpenObserve 的时候,有几个参数需要调优。第一个是queue_config里的max_samples_per_send,默认是 500,我建议调到 2000 左右,减少 HTTP 请求次数。第二个是max_shards,默认是 200,如果写入量很大,可以调到 500。
remote_write: - url: http://openobserve:5080/api/default/prometheus/api/v1/write basic_auth: username: admin@example.com password: ComplexPass123! queue_config: max_samples_per_send: 2000 max_shards: 500 capacity: 10000调优之后,我实测写入吞吐提升了大概 40%,P99 延迟从 120ms 降到了 70ms。不过这些参数要根据实际负载调整,不能盲目照搬。
4.4 告警规则配置
OpenObserve 支持基于 SQL 的告警规则。你可以在界面上创建告警,设置查询条件、触发阈值、通知渠道。我配置了一个简单的告警:当某个服务的错误日志数量在 5 分钟内超过 100 条时,发送通知。
告警查询语句:
SELECT count(*) as error_count FROM "default" WHERE level = 'error' AND timestamp >= now() - interval '5 minutes'触发条件设为error_count > 100,检查间隔 1 分钟。通知渠道支持 Webhook、Email、Slack 等。我用的 Webhook,推送到内部的告警平台。
这里要注意的是,告警查询会消耗计算资源,如果规则太多或者查询太复杂,会影响正常查询的性能。我建议把告警规则控制在 20 条以内,复杂的告警逻辑用定时任务预处理。
5. 常见问题与排查技巧实录
5.1 容器启动失败:密码复杂度不够
这是新手最容易遇到的问题。OpenObserve 对管理员密码有复杂度要求,必须包含大写字母、小写字母、数字和特殊字符,长度至少 8 位。如果密码不符合要求,容器会启动失败,日志里会提示password does not meet complexity requirements。
解决办法很简单,设置一个符合要求的密码。我一般用ComplexPass123!这种格式,既满足复杂度要求,又好记。生产环境建议用密码管理器生成随机密码。
5.2 数据写入成功但查询不到
这个问题我遇到过两次。第一次是因为流名称配置错误。OTel Collector 的stream-name头和 OpenObserve 的查询语句里的表名必须一致。如果 Collector 写的是default,查询的时候也要用FROM "default"。
第二次是因为时间戳问题。OpenObserve 默认按 UTC 时间存储,如果采集端的时区不对,数据会被写到错误的时间分区。解决办法是在 OTel Collector 的配置里显式设置时区,或者在查询的时候用timestamp字段过滤。
5.3 查询性能下降的排查思路
查询变慢通常有几个原因。第一个是热数据太多,内存缓存不够用。可以调大ZO_MEMORY_CACHE_MAX_SIZE,或者缩短ZO_COMPACT_DATA_RETENTION_DAYS。第二个是查询语句没有用上索引。OpenObserve 对常用字段会自动建索引,但如果查询条件里用了自定义字段,可能需要手动配置索引。
我整理了一个排查清单:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 查询超时 | 数据量太大 | 查看查询扫描的行数 | 缩小时间范围,加过滤条件 |
| 查询结果不全 | 时间分区错误 | 检查采集端时区 | 统一用 UTC 时间 |
| 写入延迟高 | 磁盘 IO 瓶颈 | 查看磁盘使用率 | 换 SSD,或者调大刷盘间隔 |
| 内存占用高 | 缓存配置过大 | 查看内存使用曲线 | 调小缓存,或者加内存 |
5.4 与 Grafana 集成时的认证问题
Grafana 连接 OpenObserve 的时候,认证方式要选 Basic Auth,用户名填完整的邮箱地址,密码填 OpenObserve 的密码。我一开始填了用户名而不是邮箱,结果一直认证失败。另外,如果 OpenObserve 开了 HTTPS,Grafana 的数据源 URL 也要用 HTTPS,并且要配置跳过证书验证(如果是自签名证书)。
5.5 数据保留策略的坑
OpenObserve 的数据保留策略是按流配置的。默认情况下,所有流都用全局的保留天数。但如果你在界面上给某个流单独设置了保留策略,它会覆盖全局配置。我踩过的坑是,给一个测试流设置了 1 天保留,结果忘了改回来,导致生产数据被误删。建议在生产环境里,保留策略的修改要加审批流程,或者用 API 来管理,避免误操作。
6. 我个人的一些实战体会
用 OpenObserve 这段时间,最大的感受是它确实解决了 Elasticsearch 和 Prometheus 的一些痛点,但也不是银弹。它的优势在于资源效率高、部署简单、存储成本低,适合可观测性这个垂直场景。但如果你需要复杂的全文检索、多租户隔离、或者成熟的商业支持,Elasticsearch 依然是更稳妥的选择。
Rust 带来的性能优势是实实在在的,但生态成熟度还需要时间。我在使用过程中遇到过一些文档没覆盖的配置项,只能去翻源码或者提 Issue。好在社区响应挺快,一般一两天就有回复。
最后分享一个小技巧:OpenObserve 的查询结果可以导出成 CSV,我经常用这个功能把数据拉到本地做进一步分析。导出的时候注意时间范围,太大的范围会导致导出超时,建议分批次导出。