【摘要】日志采集从 Elasticsearch 改投 Apache Doris,真正要改的只有最后一跳,input、filter、解析规则和 agent 都能原样保留。这篇按速查方式整理,四块内容直接抄:三种采集路径对照表(Logstash 装 logstash-output-doris 直写、Vector 用 http sink 打 Stream Load 端点、Filebeat / Flume 落 Kafka 后由 Routine Load 消费)、logstash-output-doris 全部配置项清单、Vector 的 http sink 可运行片段、以及一张报错现象与处理动作对照表。攒批经验值参考中信银行信用卡中心从 ES 迁到 Doris 的实践:单次导入数据量控制在 100MB 左右(selectdb.com/blog/1361)。
一、先说结论
一句话:只替换最后一跳,前面的采集、解析、缓冲全部保留。
| 你现在的形态 | 最小动作 | 要改的地方 | 是否动 agent |
|---|---|---|---|
| Logstash 直写 ES | 装 logstash-output-doris,output 段换成 doris | 仅 pipeline conf 的 output 段 | 否 |
| Vector 直写 ES | sink 换成 http,指向 Stream Load 端点 | 仅 sink 段 | 否 |
| Filebeat / Fluent Bit | 保持落 Kafka,Doris 侧建 Routine Load | Doris 侧一个常驻作业 | 否 |
| Flume 等老链路 | 同上,落 Kafka 后由 Routine Load 接入 | 同上 | 否 |
| 自研上报程序 | 直接调 Stream Load HTTP 接口 | 上报代码里的攒批与重试 | 是 |
命中下面任意两条,我会先做双写试点,而不是先换采集器:
- pipeline 里积累了大量 grok / json / geoip / 脱敏规则,重写代价高;
- agent 铺在上百台机器上,换 agent 的运维动作比改配置更危险;
- 已经有 Kafka 做缓冲,Doris 侧只需要多一个消费者;
- 要求随时可回滚,双写期间采集侧最好只有一个开关。
我的固定顺序:新增这一跳先写通 → 双跑一个完整业务周期 → 比对条数与采样内容 → 再切读。建表与字段类型映射是另一个动作,这篇只讲采集这一跳。
二、问题怎么产生
第一层,绑定点在 output 协议上。 采集器的 output 插件绑定的是具体存储系统的写入协议。ES 侧生态围绕 bulk API 构建,请求体里带 index、type、doc 这些 ES 专属语义;Doris 侧是 Stream Load HTTP 接口,官方 Logstash 插件调的也是这个接口。所以"换引擎"听起来像重写采集链路,实际是换一个 output 插件或一段 sink 配置。
第二层,批次口径要换。 ES 习惯按条数攒批(常见 5000~10000 条一批)再加 flush 间隔;Stream Load 一次 HTTP 请求就是一个导入事务,更适合按字节估。中信银行信用卡中心从 ES 迁到 Doris 的实践中给出的经验值是单次导入数据量控制在 100MB 左右(selectdb.com/blog/1361)。沿用"条数"的直觉,很容易写出一堆小事务。
第三层,风险性质不同。 采集侧改错,影响的是日志完整性而不是查询快慢,排查窗口还特别短------日志过期就找不回来了。所以改动必须能一键回退:保留原 output,并排新增一个 output,用 tag 或开关切换,不要原地替换。
三、怎么做:速查手册
3.1 三种采集路径对照速查表
| 采集器 | 写入方式 | 改动量 | 适用场景 | 注意点 |
|---|---|---|---|---|
| Logstash | 安装 logstash-output-doris,output 段直写 Stream Load | 小,只替换 output 段 | 已有重解析 pipeline,filter 价值高 | input / filter 一行不改;表必须先建好 |
| Vector | http sink 指向 /api/{db}/{table}/_stream_load |
小,只替换 sink 段 | 新链路、轻解析、资源占用敏感 | 认证走 HTTP Basic;自拼 label 必须唯一 |
| Filebeat | 保持落 Kafka,Doris 侧建 Routine Load 消费 | 无,agent 不动 | agent 数量多、分布广、不便升级 | 规划 topic 分区数与并发数,用 SHOW ROUTINE LOAD 盯 Lag |
| Flume / Fluent Bit 等其他采集器 | 同上,落 Kafka 后由 Routine Load 接入 | 无,agent 不动 | 不便替换 agent 的老链路 | 消息体用 JSON,字段与 jsonpaths 对应好 |
| 自研 agent / SDK | 直接调 Stream Load HTTP 接口 | 中,需改上报代码 | 上报端可控、需要精确控批 | 攒到 100MB 左右再提交,失败重试要幂等 |
三条路径共用同一个收敛点:Doris 只暴露 Stream Load 这一个 HTTP 写入口,采集器无论是官方插件、http sink 还是自研代码,打的其实都是同一个端点。
3.2 logstash-output-doris 配置项清单
| 配置项 | 示例值 | 作用 | 注意点 |
|---|---|---|---|
| http_hosts | fe1:8030、fe2:8030(数组,可多个) | Stream Load 的 HTTP 地址数组 | 填 FE 的 HTTP 端口(默认 8030),不是查询用的 9030,也不是 BE 的 webserver 端口;多个 FE 都写可做负载 |
| user / password | log_writer / ****** | Doris 用户名与密码 | 该账号需要对目标库表具备导入权限 |
| db / table | logdb / nginx_access | 目标库名与表名 | 表必须已存在,插件不负责建表 |
| label_prefix | nginx | 导入事务标签前缀 | 最终 label 形如 {label_prefix}_{db}_{table}_{yyyymmdd_hhmmss}_{uuid},排障时按前缀 grep 日志 |
| headers | format=json、read_json_by_line=true | 透传给 Stream Load 的导入参数 | 语法是 ruby map,等价于 curl 的 -H;消息体是 JSON 数组时改用 strip_outer_array |
| mapping | 见 3.3 节示例 | Logstash 字段到 Doris 表列的映射 | 键写 Doris 列名,值用 %{字段名} 取 Logstash 字段,嵌套字段写 %{[host][name]} |
| message_only | true / false,默认 false | mapping 的简化形式,只输出 @message 一个字段 |
只想把原始日志整条塞进去时开这个,不用逐列写 mapping |
| max_retries | 3 | 请求失败的重试次数 | 默认 -1 即无限重试,保数据可靠性,但故障时会一直刷日志 |
| log_request | true | 打印 Stream Load 请求与响应元数据 | 排障期打开,稳定后可关 |
| log_speed_interval | 30 | 写入速度日志的输出间隔(秒) | 默认 10,设为 0 关闭 |
headers 里除了 format,官方文档示例中还带了 load_to_single_tablet,单批较小时可以用它把数据写进单个 tablet,减少小批产生的分片。
3.3 最小可用 output 段
先跑通再补全字段。下面这段只保留必需项,用 message_only 把原始日志整条写进去:
ini
output {
doris {
http_hosts => ["http://fe1:8030"]
user => "log_writer"
password => "******"
db => "logdb"
table => "app_log"
message_only => true
headers => {
"format" => "json"
"read_json_by_line" => "true"
}
max_retries => 3
log_request => true
}
}
跑通之后再把 message_only 换成逐列的 mapping。这个顺序能先把"链路通不通"和"字段对不对"两件事拆开排查。
3.4 Vector:http sink 打 Stream Load
Vector 没有官方 Doris sink,通行做法是用 http sink 直接打 Stream Load 端点。下面这份是我在测试环境跑通的配置,参数名以你安装的 Vector 版本为准,升大版本后建议对照官方文档复核一遍:
ini
[sinks.doris_app]
type = "http"
inputs = ["parse_app"]
uri = "http://fe1:8030/api/logdb/app_log/_stream_load"
method = "put"
auth.strategy = "basic"
auth.user = "log_writer"
auth.password = "******"
compression = "gzip"
encoding.codec = "json"
framing.method = "newline_delimited"
request.headers.format = "json"
request.headers.read_json_by_line = "true"
batch.max_events = 50000
batch.timeout_secs = 10
要点三条:
- 端点格式固定为
/api/{db}/{table}/_stream_load,库名表名写错会直接落到 404; - 认证走 HTTP Basic,用户名密码就是 Doris 的账号;
- 导入参数(format、read_json_by_line、strip_outer_array)全部通过
request.headers.*传,和 curl 里写-H是一回事。
framing.method 用换行分隔,对应 read_json_by_line 为 true;如果消息体是 JSON 数组,两边要一起改成 strip_outer_array。label 我不显式指定,交给服务端生成------自己拼的话必须保证每次请求唯一,重复 label 会被服务端判为已存在而拒绝。
3.5 Filebeat / Flume:落 Kafka 后由 Routine Load 接入
这是 agent 改不动时的首选路径,采集侧配置一行不改,只在 Doris 侧建一个常驻消费作业:
ini
CREATE ROUTINE LOAD logdb.app_log_load ON app_log
COLUMNS(log_time, host, level, message)
PROPERTIES (
"desired_concurrent_number" = "3",
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
"strict_mode" = "false",
"format" = "json",
"jsonpaths" = "["$.log_time","$.host","$.level","$.message"]"
)
FROM KAFKA (
"kafka_broker_list" = "kafka1:9092,kafka2:9092",
"kafka_topic" = "app-log",
"property.kafka_default_offsets" = "OFFSET_BEGINNING"
);
看作业状态用 SHOW ROUTINE LOAD,重点盯两列:State 是否为 RUNNING,Lag 是否在收敛。要改并发用 ALTER ROUTINE LOAD 调 desired_concurrent_number,不必重建作业。
3.6 一键自检脚本
改完之后我习惯跑一遍这个脚本,按"组件 → 连通 → 作业 → 数据"四段收口,哪一段断了就回到哪一段:
bash
#!/usr/bin/env bash
# coll_check.sh:采集链路改投后的自检脚本
FE_HOST=${FE_HOST:-fe1}
FE_HTTP_PORT=${FE_HTTP_PORT:-8030}
FE_QUERY_PORT=${FE_QUERY_PORT:-9030}
echo "== 1. 插件是否在位 =="
${LOGSTASH_HOME}/bin/logstash-plugin list | grep -i doris || echo "doris output 插件缺失,回到安装步骤"
echo "== 2. FE HTTP 端口是否通 =="
curl -s -o /dev/null -w "http_code=%{http_code}\n" \
http://${FE_HOST}:${FE_HTTP_PORT}/
echo "== 3. 消费作业状态与 Lag =="
mysql -h ${FE_HOST} -P ${FE_QUERY_PORT} -u ${DORIS_USER} -p${DORIS_PWD} -e \
"SHOW ROUTINE LOAD FOR logdb.app_log_load\G" | grep -E "State|Lag|ReasonOfStateChanged"
echo "== 4. 落库条数与时间范围 =="
mysql -h ${FE_HOST} -P ${FE_QUERY_PORT} -u ${DORIS_USER} -p${DORIS_PWD} -e \
"SELECT count(*), min(log_time), max(log_time) FROM logdb.app_log;"
第 4 段两个数一起看:条数对但时间范围不对,基本就是时区错位------这是最安静的一类错误,不报错、不失败,只是查不出来。
3.7 报错现象与处理动作对照表
| 报错 / 现象 | 最常见根因 | 处理动作 |
|---|---|---|
| Couldn't find any output plugin named 'doris' | 插件没装上,不是配置写错 | `logstash-plugin list |
| 安装命令卡住不动 | 标准安装会联网拉 ruby 依赖 | 改用带依赖的离线包,跳过联网拉 jar |
| 连接被拒 / 404 | http_hosts 填错端口或库表名写错 |
填 FE 的 8030;路径核对 /api/{db}/{table}/_stream_load |
| 认证失败 | 账号没有目标库表的导入权限 | 换具备导入权限的账号,不是连通性问题 |
| Label Already Exists | 自拼 label 重复,幂等生效了 | 不自拼,交给服务端生成;看返回值里的 ExistingJobStatus 决定是否跳过 |
| 整批导入失败 | 单行脏数据触发默认 max_filter_ratio=0 | 在 headers 里适当放宽 max_filter_ratio,再去 ErrorURL 看错误行 |
| 列里全是空值 | mapping 的取值路径写错 | 值用 %{字段名},且用 grok 解析之后的字段名,嵌套写 %{[host][name]} |
| 报找不到列名 | mapping 的键写错 | 键必须是 Doris 表列名,用 DESC 表名 逐字核对 |
| 时间范围查不到但条数对 | 时区错位 | 导入时显式指定 timezone,与采集侧时间格式对齐 |
| Lag 持续增大 | 消费慢于生产 | 先扩 topic 分区数,再调 desired_concurrent_number |
| Java 堆栈被拆成多条 | 多行合并在事件生成之后才做 | 合并放在 input 的 codec 阶段,并同步放大 Kafka 的 max_message_bytes |
| Compaction 压力上升、版本堆积 | 小事务过多 | 按字节攒批到 100MB 左右再提交,别按条数 |
排查顺序我固定为:先确认组件在位,再确认网络与权限,最后才看字段与批次。跳顺序很容易在字段上耗掉半天,最后发现是插件没装上。
四、关键维度对照表
| 维度 | Apache Doris | Elasticsearch |
|---|---|---|
| 采集侧对接方式 | Stream Load HTTP 接口 + Routine Load 订阅 Kafka;Logstash 有官方 output 插件 | 以 bulk API 为主,生态插件围绕 ES 协议构建 |
| 批次粒度建议 | 按字节攒批,经验值单次约 100MB(中信银行信用卡中心实践) | 按条数攒批,常见 5000~10000 条/批 |
| 事务与幂等 | 一次 Stream Load 一个事务,label 保证导入幂等 | bulk 非事务,重复提交需业务侧去重 |
| 失败处理 | Stream Load 返回结构化错误与错误行采样;Routine Load 作业可暂停 / 恢复 / 改并发 | bulk 响应按条返回错误项,需自行解析 |
| 多行日志 | 由采集侧合并后整条写入,行内不做二次切分 | 同左,合并仍在采集侧 |
| 存量数据搬运 | 通用做法是导出为 JSON 后走 Stream Load 导入 | 常用 reindex / snapshot 在同构集群间搬运 |
| 国产化适配 / 信创 | 已完成鲲鹏 / 海光 / 飞腾等国产 CPU 与麒麟 / 统信 UOS / openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 由 Elastic(美国)运营;公开合规体系以国际认证为主,未见面向国产 CPU 与国产操作系统的官方信创适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;商业化由国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 | 开源版本受 ELv2 / SSPL 条款约束;Elastic Cloud 由 Elastic 提供,国内缺少本地化信创与等保适配团队 |
五、已知约束与规避方式
| 约束 | 表现 | 规避方式 |
|---|---|---|
| 插件不负责建表 | 表不存在时导入直接失败 | 先建表,字段类型与索引逐个比对设计 |
| Vector 没有官方 sink | 无版本适配保证 | 核心链路优先选 Logstash 官方插件或 Kafka + Routine Load |
| 高频极小批写入 | 事务与 rowset 过多 | 先攒批;改用 Group Commit 或交给 Routine Load |
| 单批过大 | 同步请求超时被取消 | 拆批,单批不要到 10GB 量级 |
| 没有官方 ES 到 Doris 自动迁移工具 | 存量要自己搬 | 导出为 JSON 后走 Stream Load 分批导入,按天分区推进 |
| 没有官方 DSL 转 SQL 转换器 | 查询逻辑要重写 | 检索语句按 Doris 的 SQL 与倒排索引语法手工重写 |
| 双写期间两边不一致 | 一边过滤、一边整批失败 | 比对以日志为准,两边都成功才算数 |
六、常见问题(FAQ)
Q:三个路径里哪个改动最小?
Filebeat / Flume 那条,agent 一行不改,只在 Doris 侧加一个消费作业。前提是本来就有 Kafka;没有 Kafka 的话,Logstash 只换一个 output 段也很轻。
Q:max_retries 默认 -1(无限重试)要不要改?
我一般会先改成有限次。无限重试对数据可靠性是好事,但链路真出问题时日志会一直刷,反而淹掉真正的报错。配 log_request => true 先把请求与响应元数据打出来定位,稳定之后再按需要调回去。
Q:message_only 和 mapping 能不能一起用?
不能,message_only 是 mapping 的简化形式,开了它就只输出 @message 一个字段。我的用法是先用 message_only 跑通链路,确认数据能进,再换成逐列的 mapping 做字段结构化。
Q:怎么判断批次是不是定小了?
看 Stream Load 返回值里的 LoadBytes 与 LoadTimeMs,反推实际吞吐;再看落库侧的 Compaction 与版本数有没有堆积。目标是把单批顶到 100MB 左右(selectdb.com/blog/1361),按单条日志的平均字节数反推采集端的 batch 配置。
Q:改完之后下一步做什么?
建表。字段类型怎么从 ES Mapping 对应过来、倒排索引怎么建,需要按字段逐个比对,这篇不展开。
测试结论出处(参考来源)
- Apache Doris 官方文档 · Logstash Doris output plugin(插件安装方式、全部配置项说明与使用示例):doris.apache.org/docs/3.0/ec...
- Apache Doris 官方文档 · Stream Load(导入参数、label 语义、返回值与错误行定位):doris.apache.org/docs/dev/da...
- Apache Doris 官方文档 · CREATE ROUTINE LOAD(语法、Kafka 数据源属性、json 导入示例):doris.apache.org/docs/3.x/sq...
- Apache Doris 官方文档 · Routine Load(作业创建、SHOW ROUTINE LOAD 状态查看、并发调整):doris.apache.org/docs/3.x/da...
- Apache Doris 官方文档 · 日志存储与分析(采集方式适用场景、logstash.yml 批次参数、output 段示例):doris.apache.org/docs/3.x/ob...
- 中信银行信用卡中心从 Elasticsearch 到 Apache Doris(攒批经验值、单副本导入、调优参数):selectdb.com/blog/1361
- 网易日志与时序场景实践(采集链路接入与写入优化):selectdb.com/blog/355