5 步确认 TiDB 历史抽数的 Logstash 有没有真的写进 ES
先说结论
语法通过、进程在跑、日志干净,三样都齐了,也不等于数据写进了 ES。
判定一条历史抽数流水线真的在工作,要同时看到这四样:
- 9600 上能查到这条 pipeline,
workers/batch_size和配置对得上; events.out在涨;last_run水位文件的时间在往前;- 抽一条文档出来,和库里对得上。
少一样都不算。本文按这 5 步走一遍,每一步都写清看什么算过;前两步我踩的坑比较多,单独展开。
背景
上头派的任务:为了提查询效率,热表只留 3 个月,更早的查和改走 TiDB,访问 TiDB 的 ES 历史抽数要我来改。表已经是几千万甚至几亿行,索引再优化也没用。接口已经在查 ES。
真正麻烦的是旧单据------改了金额或发票,热表里没有这行,关联不上,历史数据就不能实时更新。近 3 个月的还能正常更新,再往前的就断在 ES 里。
拿到这份 Logstash 配置开始改。本地 Windows 上没有测试服的数据库环境配置,-t 验证卡住了,最后只能上测试服确认它到底有没有真的写进去。
一页速览
| 步 | 确认什么 | 看哪里 | 通过标准 |
|---|---|---|---|
| 1 | 配置语法本身没问题 | --config.test_and_exit |
输出 OK,且没有 No config files found |
| 2 | 这条流水线真被加载了 | 9600/_node/pipelines/<id> |
HTTP 200,正文含 id,workers: 2、batch_size: 400(9600 不通时,按日志里的 id 兜底) |
| 3 | 事件真写到 ES 了 | logstash-plain.log + /_node/stats/pipelines/<id> |
无失败关键字,events.out 在涨 |
| 4 | 水位在推进 | last_run_metadata_path 指向的文件 |
文件里的时间往前 |
| 5 | 写进去的内容对不对 | ES 文档 vs 库 | 抽一条,内容和库里的样本对得上 |
顺序不能跳。第 3 步 events.out 不涨,先回头查第 1、2 步,再考虑 SQL 本身。
#mermaid-svg-xV9XwuCcFJiJEXFN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xV9XwuCcFJiJEXFN .error-icon{fill:#552222;}#mermaid-svg-xV9XwuCcFJiJEXFN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xV9XwuCcFJiJEXFN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xV9XwuCcFJiJEXFN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xV9XwuCcFJiJEXFN .marker.cross{stroke:#333333;}#mermaid-svg-xV9XwuCcFJiJEXFN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xV9XwuCcFJiJEXFN p{margin:0;}#mermaid-svg-xV9XwuCcFJiJEXFN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster-label text{fill:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster-label span{color:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster-label span p{background-color:transparent;}#mermaid-svg-xV9XwuCcFJiJEXFN .label text,#mermaid-svg-xV9XwuCcFJiJEXFN span{fill:#333;color:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN .node rect,#mermaid-svg-xV9XwuCcFJiJEXFN .node circle,#mermaid-svg-xV9XwuCcFJiJEXFN .node ellipse,#mermaid-svg-xV9XwuCcFJiJEXFN .node polygon,#mermaid-svg-xV9XwuCcFJiJEXFN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xV9XwuCcFJiJEXFN .rough-node .label text,#mermaid-svg-xV9XwuCcFJiJEXFN .node .label text,#mermaid-svg-xV9XwuCcFJiJEXFN .image-shape .label,#mermaid-svg-xV9XwuCcFJiJEXFN .icon-shape .label{text-anchor:middle;}#mermaid-svg-xV9XwuCcFJiJEXFN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xV9XwuCcFJiJEXFN .rough-node .label,#mermaid-svg-xV9XwuCcFJiJEXFN .node .label,#mermaid-svg-xV9XwuCcFJiJEXFN .image-shape .label,#mermaid-svg-xV9XwuCcFJiJEXFN .icon-shape .label{text-align:center;}#mermaid-svg-xV9XwuCcFJiJEXFN .node.clickable{cursor:pointer;}#mermaid-svg-xV9XwuCcFJiJEXFN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xV9XwuCcFJiJEXFN .arrowheadPath{fill:#333333;}#mermaid-svg-xV9XwuCcFJiJEXFN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xV9XwuCcFJiJEXFN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xV9XwuCcFJiJEXFN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xV9XwuCcFJiJEXFN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xV9XwuCcFJiJEXFN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xV9XwuCcFJiJEXFN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster text{fill:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN .cluster span{color:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xV9XwuCcFJiJEXFN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xV9XwuCcFJiJEXFN rect.text{fill:none;stroke-width:0;}#mermaid-svg-xV9XwuCcFJiJEXFN .icon-shape,#mermaid-svg-xV9XwuCcFJiJEXFN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xV9XwuCcFJiJEXFN .icon-shape p,#mermaid-svg-xV9XwuCcFJiJEXFN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xV9XwuCcFJiJEXFN .icon-shape .label rect,#mermaid-svg-xV9XwuCcFJiJEXFN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xV9XwuCcFJiJEXFN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xV9XwuCcFJiJEXFN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xV9XwuCcFJiJEXFN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 语法检查:OK 且真读到了文件
9600 上查到这条 pipeline
日志和事件计数确认有写出
last_run 水位往前走
抽一条文档和库里对上
这次我的实际路线不是这条直线:第 2 步的 200 一直没拿到(先是 404,后来超时),而且 9600 整段时间都没恢复响应------不光 200,连 stats 都查不动。日志成了我唯一的观测手段。偏差集中在第 1、2、3 步,坑都写在各步里。
第 1 步:语法检查,OK 不一定算过
这份 conf 的语法,我是在测试服上验的。一开始想先在本地 Windows 校验通过,测试服遇到的麻烦会更少,但 Windows 的 -t 没走通(踩了三个坑,放在后面)。
命令:
bash
/opt/ysbapi/logstash/logstash-7.13.2/bin/logstash --config.test_and_exit -f /opt/ysbapi/logstash/config/ysbdd-saleout/v3/ysbddSaleOutHistory.conf
通过标准:输出 Configuration OK,并且输出里没有 No config files found in path。
下面是我第一次跑出来的截图:

我当时只盯着最后一行的 Configuration OK,觉得稳了。 写这篇文章回看截图才发现,OK 上面紧挨着两条:
text
[INFO ][logstash.config.source.local.configpathloader] No config files found in path
{:path=>"/opt/ysbapi/logstash/config/ysbdd-saleout/v3/..."}
[ERROR][logstash.config.source.local.configpathloader] No configuration found in the configured sources.
Configuration OK
-f 指向的路径下一个配置文件都没找到,Logstash 空跑一遍,照样返回 OK------它区分不了「验证通过」和「压根没文件可验」,两种情况的退出码都是 0。也就是说,这张截图不能证明语法没问题:它连文件都没读到。
写这篇文章回查时才想明白当时为什么找不到文件:大概率不是路径敲错,而是和后面的坑 2.1 同一个根因------当时这份 conf 还没合并到 develop,服务器上那个路径下根本还没有这个文件。
合并部署到位之后重新跑了一遍,这次的输出才作数:ls -l 先确认文件存在,Logstash 把配置里的插件全部展开(jdbc input、elasticsearch output 的参数都打印了出来),最后才是 Configuration OK。

这一步的教训 :
OK只说明「Logstash 认为没事」,不说明「它看过你的文件」。先确认输出里没有No config files found,再信那行 OK。
本地为什么走不通,下面三条是连着来的,按发生顺序排。
本地用的是 Logstash 7.13.2,Windows 绿色版,免安装。没有安装包可以到华为云下对应版本:https://repo.huaweicloud.com/logstash/
bat
D:\Java-Program\logstash-7.13.2\bin\logstash.bat -t -f D:\workspace\company-workspace\datacenter_logstash\config\ysbdd-saleout\v3\ysbddSaleOutHistory.conf
坑 1.1 绿色包解压漏了 jruby
现象 :could not find jruby,指向 logstash-7.13.2\vendor\jruby。
怎么查:
bat
dir D:\你的路径\logstash-7.13.2\vendor\jruby
dir D:\你的路径\logstash-7.13.2\vendor\jruby\bin\jruby.exe
处理 :没有 jruby.exe 就是解压漏了,重新解压。
坑 1.2 Logstash7.13.2 撞上 JDK 17,会在 bundler 上退出
现象 :我本机 JAVA_HOME 指向的是 JDK 17:
text
Using JAVA_HOME defined java: D:\Java-Program\Java\jdk-17.0.10_windows-x64_bin\jdk-17.0.10
7.13.2 只能用 JDK 8 和 JDK 11。目录补全之后,报错换成 (LoadError) no such file to load -- bundler,Logstash 就此停掉。
处理 :不想改全局 JAVA_HOME,只在当前会话指到 JDK 8。这一步要管理员终端,我在这上面耗了一会:
powershell
$env:JAVA_HOME="D:\Java-Program\Java\jdk1.8.0_212"
$env:PATH="$env:JAVA_HOME\bin;$env:PATH"
java -version
D:\Java-Program\logstash-7.13.2\bin\logstash.bat -t -f D:\workspace\company-workspace\datacenter_logstash\config\ysbdd-saleout\v3\ysbddSaleOutHistory.conf
坑 1.3 本地没有 TI_JDBC_CONNECTION_STRING配置,配置直接判非法
现象 :JDK 换对之后还有一条。ysbddSaleOutHistory.conf 里写了 ${TI_JDBC_CONNECTION_STRING},就是 JDBC 输入的连接串。Logstash 启动时按环境变量或 secret 去替换。当前 Windows 没定义这个变量,配置里也没默认值,就判配置非法:
text
Unable to configure plugins: Cannot evaluate `${TI_JDBC_CONNECTION_STRING}`. Replacement variable `TI_JDBC_CONNECTION_STRING` is not defined in a Logstash secret store or an environment entry and there is no default value given.
随后是 (SystemExit) exit。
处理:本地已经验证不下去了,语法检查回到测试服。
这一步的结论 :本地
-t走不通,不是配置写错了,是本机缺件(缺 jruby、JDK 版本不对、缺环境变量)。所以"语法过没过"这件事,只能以测试服为准。
第 2 步:进程在跑,不代表这条流水线挂上了
语法过了,要到正在跑 Logstash 的那台 Linux 上,确认这条流水线挂上了。
先确认 HTTP API 是不是 9600:
bash
grep -E '^http\.(host|port)' /opt/ysbapi/logstash/logstash-7.13.2/config/logstash.yml
再查我这条历史流水线:
bash
curl -sS -m 3 "localhost:9600/_node/pipelines/ysbdd-saleout-spfl-history-v3?pretty"
通过标准 :返回 HTTP 200,正文里同时有这个 id 和 "workers" : 2。同目录热库配置里 batch_size 是 400,这里也应是 400。
没拿到 200 的时候,按这张表分流:
| 结果 | 含义 | 先去查 |
|---|---|---|
Failed to connect / Connection refused |
当前会话不在 Logstash 机器上,或 API 不在 9600 | ssh、端口 |
| HTTP 404,或正文没有这个 id | 进程在跑,但加载的 pipelines.yml 里没有这条,或者 id 本身就写错了(见坑 2.3) |
分支、pipelines.yml、id 拼写、改完有没有重启/reload |
有 id,但 workers 不是 2 |
挂的是旧配置 | reload / 重启 |
curl: (28) Operation timed out |
进程在跑,但 9600 没响应------常见原因是某条流水线初始化失败,整进程卡在启动阶段 | 整个进程的日志里有没有报错 |
404 和超时我这次都遇到了,原因还不一样。
坑 2.1 先是 404:合到了 test,测试服跑的是 develop
现象 :curl 返回 404。进程在跑,加载的 pipelines.yml 里没有这条。
原因:我合并到了 test 分支,测试服用的是 develop。test 可能是历史测试分支,我不确定它是干什么的。
处理:合到正确分支再重启。
坑 2.2 接着是同一条 curl 超时:日志里有报错,但 id 对不上
现象:
text
curl: (28) Operation timed out after 3001 milliseconds with 0 out of -1 bytes received
怎么查:看进程日志,结果出现了错误日志👇
bash
tail -f /opt/ysbapi/logstash/logstash-7.13.2/logs/logstash-plain.log

原因 :日志把答案直接写了出来------有流水线初始化失败,解析不了 ${TI_JDBC_CONNECTION_STRING}。
为什么 curl 会超时 :Logstash 是整进程加载的------一条流水线起不来,整个进程都卡在启动阶段,9600 不响应,curl 自然超时。所以超时别先怀疑网络,先看日志。
这条报错有两处怪,各值一个教训:
怪一:报错的不是我在查的那条。 报错行里的 pipeline_id 是 ysbdd-saleout-saleOut-history-v3,我在查的却是 spfl-history-v3。我当时就断定:"别的流水线的锅。"事后拿 pipelines.yml 一对,还真猜对了------里面挂着五条兄弟流水线,配置一模一样、写同一个索引,报错的是出库单历史那条。但猜对不等于方法对 :id 对不上就甩锅,跟 id 对得上就认领,都是赌。真要归属,报错时间戳 + pipelines.yml 条目,两个都对上才算数。
怪二:日志是追加式的,旧报错会一直躺着。 写文章回查才发现,conf 早就改过数据库连接方式,这条报错还躺在文件里。日志里的错,不一定是现在的错------报错时间戳要和 conf 的改动时间对一遍,不然会拿着历史遗留的错,去修一份早就改好的配置。
变量解析不了,机器上到底有没有?看环境变量和 keystore:
bash
systemctl show logstash -p Environment
/opt/ysbapi/logstash/logstash-7.13.2/bin/logstash-keystore list
keystore 里有 TiDB 的域名 URL、用户名 USERNAME、密码 PASSWORD,没有 TI_JDBC_CONNECTION_STRING 这种整串。所以后面得自己拼,要么就找运维配置一个。不过这条历史流水线后来换了数据库连接方式(见怪二),这个变量也就不再需要了。
坑 2.3 一条流水线,三个名字
排查到后面我发现,这台机器上同一条流水线在不同地方叫三个名字,互相都对不上------以出库单历史这条为例:
| 出现位置 | 名字(举例) |
|---|---|
pipelines.yml 里的 pipeline.id |
ysbdd-saleout-saleOut-history-v3 |
conf 里 jdbc 插件的 id / type |
ysbdd-saleout-dt-v3-history(input 的标识,和 pipeline.id 无关) |
conf 里 last_run_metadata_path 的文件名 |
ysbdd-saleout-dtHistory-v3.txt |
后果就是:拿 conf 里的 jdbc id 去 curl 9600 是 404,拿 pipeline.id 去找水位文件是对不上号------三个名字谁也不挨着谁,必须靠一张对照表对齐(见下)。
另外别把「名字多」和「条数多」搞混。pipelines.yml 里光 ysbdd-saleout 的历史抽数一族就有五条独立条目 (出库单 / 返利 / 库存 / 补贴 / 归档增量),workers、batch 配得一模一样,还写同一个索引------兄弟流水线长得像,日志里报哪条就是哪条。坑 2.2 里那个"id 对不上",最后查明就是这族里的另一条兄弟,不是同一条流水线的别名。
记一张对照表,各查各的:
| 想确认什么 | 只认哪个名字 |
|---|---|
API(9600 的 /_node/...)和日志行前缀 |
pipelines.yml 里的 pipeline.id |
| 插件级报错定位(哪段 input/output 出错) | conf 里 jdbc / output 的 id |
| 水位推进 | conf 里 last_run_metadata_path 指向的文件 |
第 3 步:起来了,还要看事件有没有真的写出去
流水线能起来,只说明进程加载了它。还要看有没有写入失败,以及 events.out 有没有在涨。
curl 不通之后,我改看 logstash-plain.log------这次 9600 一直没恢复响应,按 id 搜日志是我唯一能做的。事件计数、stats 都要走 9600,先放一边。除了按 id 搜,另外用了两个关键字。
要盯的失败关键字 :JDBCException、rubyexception、_dateparsefailure、jdbc_batch_streaming,还有 ES bulk 的 mapper_parsing_exception。
bash
grep "ysbdd-saleout-spfl-history-v3" /opt/ysbapi/logstash/logstash-7.13.2/logs/logstash-plain.log | tail -20
grep "saleOut-history" /opt/ysbapi/logstash/logstash-7.13.2/logs/logstash-plain.log | tail -20
grep 'An unexpected error occurred!' /opt/ysbapi/logstash/logstash-7.13.2/logs/logstash-plain.log | tail -20
搜到内容了,说明它起来了。但别被"看着正常"骗了------这张截图第一行就有一条 [WARN] Restored connection to ES instance:连接曾经断开过又恢复。日志没报错不等于没出过事,jdbc_batch_streaming 这类报错也仍会让写入失败,所以关键字搜出来的结果要一条条看过,不能只看"有输出"。

事件计数这次没查成 :/_node/stats/pipelines/ysbdd-saleout-spfl-history-v3 同样走 9600,超时没恢复,这条检查只能空着。字段含义先记在这里,等哪天 9600 能用了照着对:
| 字段 | 含义 |
|---|---|
events.in |
JDBC 捞到的行,含 id=0 哨兵 |
events.filtered |
哨兵被 drop 的数量 |
events.out |
真正写入 ES 的明细 |
in在涨、out长期为 0 ------ JDBC 捞到了行,但要么被 filter 丢掉(返利日志和ti_stock出库记录对不上,这次就是),要么写入 ES 失败;- 队列
queue.events持续变大 ------ 一轮调度跑不完。
这次实际兜住我的,是后面的第 4、5 步:关键字没搜到写入失败,就接着看水位动不动、再抽文档和库对账------计数查不到,就用结果反推。
第 4 步:水位要看 last_run 有没有往前
历史库这条的水位文件必须和热库的 last_run 分开 。我看的是 last_run_metadata_path 指向的这个文件:
bash
tail -500f /opt/ysbapi/logstash/lastrun/ysbdd-saleout-spflHistory-v3.txt
为什么必须分开:这条和热库的进度不是一把尺------一个按归档库的跟踪列走,一个按线上热表的进度走。共用一份 last_run,只会互相把对方的进度冲掉。
坑 4.1 查询 0.02 秒就成功,水位也可以不动
现象:没有水位推进,日志上又是正常的查询、耗时很短。
原因 :这轮 SQL 没有返回任何带结果的行。JDBC 输入只有在结果里读到跟踪列时,才改写 last_run 文件。
这次查询 0.02 秒就成功了,所以日志看起来正常------快,恰恰是没捞到数据的信号。

解决 :让 Cursor 按水位线前面 造一小批数据------一条返利日志,加上对应的订单头和订单明细。跑完之后 last_run 里的跟踪值推到了这条新数据的 pk,说明「有数据就推进」成立:之前的 0.02 秒,确实只是没捞到行。
第 5 步:抽一条写成功的文档和库对上
前面都过了,最后还是抽一条写成功的文档。库里的测试数据和 ES 上的对得上就行。
这一步看着最没技术含量,但它是唯一能证明"内容对不对"的检查。前面四步证明的是"有没有在写",只有这一步证明"写对没写对"。
回头看:如果重来一次,排查顺序会这么排
这次我踩的顺序是"先撞坑、再回头补",效率不高。按故障层级重排一遍会顺很多:
| 优先级 | 层 | 先看什么 | 为什么排这 |
|---|---|---|---|
| 1 | 进程级 | logstash-plain.log 全局有没有报错 |
任何一条流水线挂了,后面全都起不来,所以这层要先看 |
| 2 | 加载级 | pipelines.yml 里有没有它、分支对不对、有没有重启 |
404 和"挂的是旧配置"都出在这层 |
| 3 | 语法级 | --config.test_and_exit |
便宜,几秒钟,但要在有环境的机器上跑 |
| 4 | 数据级 | events.in/filtered/out、queue.events |
定位是没捞到、被 drop、还是没写出去 |
| 5 | 水位级 | last_run 文件 |
水位不动,先看上游有没有返回带跟踪列的行(这次就是没有) |
| 6 | 内容级 | 抽一条文档对账 | 兜底,也是唯一验证正确性的步骤 |
一句话版本:语法通过不等于加载了,加载了不等于写出去了,写出去了不等于水位动了,水位动了不等于数据对。
写在最后
语法通过、进程在跑、日志看起来正常,只说明这条流水线能启动。历史数据写进 ES,要同时看到 9600 上的这条 id、events.out 在涨、last_run 往前,再抽一条和库对上。
你也遇到过 Logstash 日志看起来正常、水位文件却不往前走的情况吗?评论区聊聊你是怎么确认它真的在抽数的。