找回密码
立即注册
搜索
发回帖 发新帖

5061

积分

0

好友

647

主题
发表于 2 小时前 | 查看: 10| 回复: 0

周末我在自己笔记本上干了一件平时不敢在生产环境随便干的事:往 Easysearch(后简称 ES)里一口气写 1 亿条数据。

秒表停在 44 分 57 秒。

机器是 Intel Core Ultra 9 185H,内存 63.5 GB,硬盘是 NVMe。Easysearch 2.5.0 就跑在本机 http://127.0.0.1:9200。

写完我去 Head 里看,索引 logs-2026 是 1 亿条,占了 4.06 GiB。集群一直是绿的。

Elasticsearch Head管理界面显示logs-2026索引文档数1亿条

Easysearch索引概览页面显示logs-2026分片状态与索引吞吐趋势

CPU 大多时候就两成到三成,没红过。

我得先泼一盆冷水。这 1 亿条都是小不点,一条就一个 id,外加一句 user login 或者 order paid。文件本身大约 5.4 GB。你要是灌带堆栈、带正文的日志,别拿我这 45 分钟去排期,那是两回事。

开发工具界面展示match_all查询返回的日志记录

为啥不一条一条写

一条一条调用 index(),等于 1 亿次握手。笔记本再快也受不了这个来回。

Bulk 是把很多条捆成一包,一次请求送出去。我用的脚本是 es_bulk_load.py,命令就这一条:

$env:EASYSEARCH_HOSTS = "http://127.0.0.1:9200"
python es_bulk_load.py --index logs-2026 --file events.jsonl --shards 4 --id-field id --replicas 0

分片我给了 4 个。如果你对分片数量和容量规划没概念,可以看看这门 Elasticsearch 8.X 从入门到精通 课程,里面有专门讲分片机制和集群规划的章节。

这台机器只有一个数据节点,副本我设成 0。副本要是开着,等于每条写两份,单机还分配不出去,集群直接黄给你看。

文档 id 用的是 e-1、e-2 这种自己带的号。中途断了再跑,相同的号会盖掉旧的,不会变成 2 亿条。

还有一个开关特别容易忘:refresh_interval。

默认大概 1 秒刷一次,好让新数据马上能搜到。1 亿条要是跟着每秒刷,磁盘光合并段就够呛。所以灌的时候我把它关成 -1,写完再改回 1s,顺手 refresh 一次。Head 里那 1 亿,是这一下之后才稳稳显示出来的。

一批 2000 条,4 个线程一起发。官方的经验是每批先从 1000 到 5000 条、大概 5 到 15 MB 试。

批量处理大小建议示意图:1000到5000条与5到15MB对比

我这批太小,2000 条凑一起也就一百来 KB,离 10 MB 还远着。

代码其实不复杂,怕的是一口气读进内存

5.4 GB 的文件,要是 readlines() 一下全读进来,内存先给你个教训。脚本是读一行、吐一行。

吐出去的也不是原文那一行,而是批量接口认得的一个小字典。这里有个特别容易抄错的地方:普通写入用 _source。

你要是写成 update 那种 doc,这一条会失败,请求整体还可能回你一个 200,看着像成功。

action = {
    "_index": "logs-2026",
    "_op_type": "index",     # ← 已经有了就覆盖
    "_id": str(doc["id"]),   # ← 断了再跑,靠这个号对上
    "_source": doc,          # ← 正文在这,别写成 doc
}

客户端拿到它,会拆成两行再发出去。

第一行说「往哪写、id 是啥」,第二行才是内容。

最后一行也得换行,不然 Bulk 不认:

{"index":{"_index":"logs-2026","_id":"e-1"}}
{"id":"e-1","message":"order paid","level":"info"}

凑满再发。满 2000 条,或者这一包的字节能到大约 10 MB,就送走。

events.jsonl文件内容展示多行JSON格式日志事件

我这批小文档,先碰到的是条数。你要是灌大日志,往往条数还没到,体积先到了,那也正常。

if chunk and (len(chunk) >= batch_size or size + nbytes > max_bytes):
    yield chunk, last_line   # ← 这一包结束,记住最后读到文件第几行

并发我也卡死了。

最多 4 包在路上,再多主线程就歇一会,等有人回来。

内存里大概就这 4 小包,所以任务管理器那条内存线几乎是平的。文档从 4000 万涨到 1 亿,内存还是 29 GB 左右,没跟着文档数一起长。

任务管理器显示CPU和内存使用情况,ES索引文档数4000万

pending[pool.submit(send_chunk, client, seq, last_line, chunk)] = None
while len(pending) >= args.threads:   # ← 在飞的不超过 4 包
    take_done(wait_all=False)

4 个线程谁先跑完说不准。检查点要是谁先回来就记谁的行号,文件中间会空一截,下次续跑就把空的那截跳过了。所以每包有个序号,只按 0、1、2 往下记已经写稳的最后一行。

网断了、对方回 429 或者 502,这一包整包不算数,行号停在上一包,下次从这儿重来。某一条自己写坏了,比如字段类型不对,就把这条丢进错误文件,行号继续走。不能因为一条坏数据,把剩下的几千万条都卡死。

底下还有个小保险:失败了最多再试 3 次,先等 2 秒,越等越长,最多等到 60 秒。这次曲线很平,说明本机基本没走到重试。

我盯着屏幕看的那 45 分钟

秒表是从这次灌 1 亿条开始按的。下面这几行,都是我对着 Head 和任务管理器抄下来的。

秒表 文档数 索引大小 CPU 内存
18:02 40,132,964 1.87 GB 29%,2.44 GHz 28.7 / 63.5 GB
24:42 55,275,867 2.41 GB 21%,2.92 GHz 28.6 / 63.5 GB
27:34 61,914,310 2.64 GB 28%,3.12 GHz 28.7 / 63.5 GB
31:35 70,637,466 2.83 GB 33%,2.97 GHz 28.6 / 63.5 GB
36:38 81,144,437 3.17 GB 21%,2.53 GHz 28.7 / 63.5 GB
41:40 91,453,247 3.78 GB 28%,2.89 GHz 29.1 / 63.5 GB
44:57 100,000,000 4.06 GB — —

44 分 57 秒算下来是 2697 秒。1 亿除以 2697,大约 每秒 3.7 万条。

Elasticsearch Head界面显示7000万文档与任务管理器CPU占用33%

CPU 那条线我看了很久。这颗 U 是 16 核 22 线程,标称 2.30 GHz。灌的时候利用率就在 21% 到 33% 晃,频率偶尔到 3.12 GHz,大部分时候 2.5 到 3.0。没有一根刺戳到顶。

内存更没啥故事。63.5 GB 里一直用着 28.6 到 29.1 GB,百分之四十五上下。

磁盘是那块系统 NVMe,多数时候 10% 到 32%。写到 9100 多万的时候冲过一次 49%,下一眼又掉回去了。

4 个分片 0、1、2、3 都在这个节点上,健康值一直绿。

索引最后 4.06 GiB,4 个分片每个才 1 GB 出头。摊到每条,存储大概 44 字节。文件本身 5.4 GB,进了索引反而更小。小文档再压一压,就会出现这种索引比原文还瘦的情况。

这 45 分钟,别直接抄到生产

中间我还被 Windows 坑了一下,跟 Easysearch 没关系。

检查点要是每 2000 条就替换一次同一个文件,写到几千万行,events.jsonl.es_bulk.ckpt.tmp 会报 WinError 5,说拒绝访问。

多半是杀毒或者别的程序正抓着这个文件。程序退出了,已经写进去的数据还在,行号停在最后一次存成功的地方。加上 --resume 就能接着写,相同 id 会盖掉,不会重复插。

我后来改成大概每 10 万条才落一次盘,替换失败就再试几次。后面那一大段就没再被掐断。你要是在 Windows 上跑这种长任务,别那么勤快地换同一个小文件。

还有三句老实话。

四条ES写入实践建议卡片:先灌100万、分片按30GB、写完再开刷新、单机副本写0

  • 文档太小,所以才有每秒 3.7 万条。 换成一条 1 KB 的日志,1 亿条光原文就大约 100 GB,包变大,刷新和合并都更重。先用你自己的数据灌 100 万条,再去估 1 亿。

  • 分片也别照抄成 4。 官方说一个主分片大概扛 10 到 50 GB 比较舒服。我这 4 GB 拆成 4 片,每片 1 GB,是图测试省事。分片个数创建之后改不了,生产上先拿原文体积除以 30 GB,再看看你有几个节点。

  • 副本 0 只适合我这种单机瞎折腾。 节点要是挂了,这 1 亿条就没了。写完真要给人查,把副本加回去。单节点千万别设成 1,副本没地方去,集群会黄着不走。

refresh_interval=-1 灌的时候是对的,写完一定要改回来,不然新数据搜不到。

我改回的是 1s。日志要是能接受半分钟延迟,用 30s 更省磁盘。

你要是也想试

找一份不大的 JSON,在自己机器上跑一小批,看三眼就够:

  • 文档数和文件里的有效行数对不对得上
  • CPU 是不是早就满了。我这次没满,说明这批还能再加大
  • 用索引大小除以条数,算算你自己一条占多少字节

我这 1 亿条小文档,45 分钟,CPU 三成,索引 4.06 GiB。

你线上一条大概多大?拿同样的脚本先灌 100 万条,把耗时和索引大小算出来,就能把这 45 分钟换成你的数。这类海量数据写入与搜索性能优化的话题,在 云栈社区 上也有不少同行在讨论,换数据之前不妨上去翻翻实战案例。




上一篇:Maven 依赖冲突排查:3 个命令定位真凶,附 Spring Boot 4.1.1 实战
下一篇:ESP-Claw 芯片上的 AI Agent:ESP32 端侧聊天编程与烧录指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-18 12:16 , Processed in 0.081569 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表