使用 meilisync
感谢 yzqzss 的大力支持
tl;dr: fork 并大改了程序,参见
崩坏的前端#
先尝试使用了他的 Admin Console,https://github.com/long2ice/meilisync-admin
只有 AMD64,没有 ARM 的镜像。谁还没有 x86_64 的机器啊,尝试在非数据库的机器上使用。我当时还没意识到从 A 机拉数据到 B 机再塞给 C 机是啥概念,但总之当时犯傻了。
下镜像、运行…… 等等这个数据库同步的 admin 为什么还要 MySQL 和 Redis 的 DBurl 啊?不理解但是当时配置了。
随后……
-
没有初始账户 ref #7
解决方案是手动写邮箱密码进数据库,还要手搓 bcrypt hash
-
创建 MongoDB 数据源,报错 Unknown option user #11
原因竟然是,不同数据库在后端配置文件需要的参数不同,网页只按照 PostgreSQL 的 user 进行了传参,而且后端没处理直接塞,sync 程序原地爆炸。
使用抓包改包重放解决。
本来写了 fix 但发现 PostgreSQL 的参就是对的,那爱咋咋地吧我感觉我管不了。也应该没人看到这还想用吧…… 应该吧。
-
设置好一切了之后还是跑不起来…… 后端有如下报错 (删除一吨内容):
2025-04-11 21:38:42.156 | INFO | uvicorn.protocols.http.httptools_impl:send:496 - 10.0.1.1:64000 - "POST /api/sync HTTP/1.1" 500 ERROR: Exception in ASGI application Traceback (most recent call last): File "/meilisync_admin/meilisync_admin/models.py", line 64, in meili_client self.meilisearch.api_url, ^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'QuerySet' object has no attribute 'api_url'
额…… 看起来不是简单配置文件的问题了……
于是尝试使用 cli,避免是那鬼畜的 Admin Console 导致的问题以为马上就是终结的开始,原来只是开始的终结。
滞后的 docker#
找到实际进行同步的程序,https://github.com/long2ice/meilisync
我还是尝试了 Docker,毕竟是”Recommended“的方法。但是按照他 Readme 写的 compose,
version: "3"
services:
meilisync:
image: long2ice/meilisync
volumes:
- ./config.yml:/meilisync/config.yml
restart: always
拉下来的镜像有问题。
我遇到的是 TypeError: 'async for' requires an object with aiter method, got list #94
但对应还有 TypeError: 'async for' requires an object with aiter method, got coroutine #76
嗯,得用 dev 呢。
啊对和上文一样,他 MongoDB 的 user 的字段是 username,和模板不一样。鬼知道当时我怎么能灵光一现想出是 username 的。
后面配置文件来回几次之后不想搞 docker 了,故转为本地 cli。
本地 Python#
本地 cli 阶段,一切似乎向着好的方向好起来了。
为数不多的几个问题就是,虽然有 pip install meilisync[mongo]
for MongoDB,但是只安装这个是不够的。实际运行任何命令都会狠狠的告诉你,缺这缺那。你只能 pip install meilisync[all]
for all.
还有小 bug 要打 zsh。
$ pip install meilisync[mongo]
zsh: no matches found: meilisync[mongo]
终于配置好 config,一切似乎在向好发展…… 或者是么?
爆炸的进度#
参考 #17 中的回复,当以 MongoDB 作为数据源时,progress.json
可能不会自动生成,导致一堆一堆的TypeError: meilisync.progress.file.File.set() argument after ** must be a mapping, not NoneType
。
解决方案,先 touch 一下progress.json
,再写进案例……
{"resume_token": {"_data": "8267FBA647000000022B042C0100296E5A10046F963A9EB7AB4D14B8CF191E8E5E8D67463C6F7065726174696F6E54797065003C696E736572740046646F63756D656E744B65790046645F6964006467FBA6470D168B18625CC73E000004"}}#
狠狠的 log#
测试没发现大问题之后,改配置文件关了 debug,使用 nohup 运行起来之后我就去干别的事了,直到硬盘告警把我拉回到 shell。同步数据库到新地方的确很耗空间,我也准备好了。但我实在没想到是中间的这位硬盘先爆炸了 —— 增速甚至大于 Meili 数据库的机器 —— 人畜无害的同步器给我摔了 6 个 G 的日志到我脸上。
这不可能啊,我明明在配置文件中写了 debug=false 啊 ——
tail 了一下巨大的 log,发现他把每一条同步的内容全纯文本的记录下来了……
其实是默认的插件实例导致的。
在配置文件中存在如下的内容:
debug: false
plugins:
- meilisync.plugin.Plugin
其中 plugin 部分实际引用的是 https://github.com/long2ice/meilisync/blob/dev/meilisync/plugin.py
class Plugin:
is_global = False
async def pre_event(self, event: Event):
logger.debug(f"pre_event: {event}, is_global: {self.is_global}")
return event
async def post_event(self, event: Event):
logger.debug(f"post_event: {event}, is_global: {self.is_global}")
return event
在这个 plugin 中,不管配置文件中 debug 的设置为何值,都会写入 debug。
解决方案有三种:
-
不引用这个 plugin
-
修改 plugin 内容
-
修改全局的 log 级别:
Meilisync 使用 loguru,参考其文档可以通过设置 level 实现,再根据其环境变量的相关文档,可以设置
LOGURU_LEVEL
,可采用值如下表:Level name Severity value Logger method TRACE
5 logger.trace()
DEBUG
10 logger.debug()
INFO
20 logger.info()
SUCCESS
25 logger.success()
WARNING
30 logger.warning()
ERROR
40 logger.error()
CRITICAL
50 logger.critical()
然后设置环境变量:
Unix 下:
export LOGURU_LEVEL=INFO
Windows 下:
PowerShell
$env:LOGURU_LEVEL="INFO"
CMD
set LOGURU_LEVEL=INFO
后续来看节省了一吨的空间……
在各种 debug 中,把这个服务从 B 迁移到了 C,也就是 Meili Search 所在的地方。这在后来被证明提升了非常多的速度。
Index 的疑虑#
我的数据中有 id 一项,但实际使用中会冒出各种问题,还是使用_id 作为主键。
动手改脚本#
矫正了类型#
似乎一切正常的运行了一阵子之后,程序自己就死掉了。几次检查之后发现是 TypeError: Object of type ObjectId is not JSON serializable
。此时的进度大概都是 1,140,000 条数据。
同样有 GitHub Issue,#16,说是”fixed“。检查了一下本地代码,的确包含了 fix 的内容。但似乎还有出现在 #102,这次就没有任何回复了。
最难搞的不是修代码,而是让它再出错。由于出错之后进度就 g 了,每次都是从头开始,而每次跑到错误地方需要 20 分钟,消耗了几个小时在这个上面…… 还有,运行期间 CPU 全都拉满…… 我还应该庆幸不是用的小服务商机器会被拉闸……
对了,这段时间,用上了 sentry.io。很奇怪作者在这个同步工具中特别留了 sentry.io 的口子,但它真的非常有用。也许作者知道会在各种地方出 bug?
本地的修改#
于是再实现了一下检测与修复。一开始尝试改造那个 plugin,但一开始实在没理顺内容,最后决定硬改源码!添加了额外的类型检查。不想一次次的 pip install,直接进site-packages
改文件,又快又好。
修复完 ObjectId
不久,看超过半小时都没问题,进度也在一点点的走,正准备去睡觉,结果又有报错,这次是Object of type datetime is not JSON serializable
,类似 #31 。同样的,加检查。这次是在 5,270,000 的位置,大概三分之一。修好之后能接着同步,也过了一半,于是我放心的去睡觉了。
然后早上醒来又是晴天霹雳,在差不多三分之二的时候,就会冒出Client error '408 Request Timeout' for url 'http://127.0.0.1:7700/tasks/xxxx
。实在没办法,索引的东西太多太多,算不过来也就越积压越多,直至彻底 boom。但不正常的是,这个问题也被修过,在 #13 有提起过,但不知道啥原因,还是爆炸了。此时我检查了一下积压了多少,发现运行一个小时就会积压半个小时…… 我应该庆幸没有发生 Too many open files 的问题……
索引慢是没办法的…… 欸等等,说到底这索引就不应该这么快加才对吧!
延迟的索引#
我决定尝试延迟,然后发现可怕的事实:在创建 index 的时候,meilisync 没有指定任何的字段索引选项。所以文档中的每个字段都会显示并可搜索,这耗费了超级多的资源,也造成可怕的浪费。我们完全没必要一开始就索引全部的内容。相反,我们在从远端同步数据的时候,不应该建立任何的索引,而应该等到直到所有来自源的内容全都都成功被插入了之后,再做索引的处理,而且应该能在 config file 中指定哪些字段被索引,包括索引的类型(searchable, sortable, filterable, none )
于是写了。目前写了从远端同步数据的时候,不应该建立任何的索引的逻辑,同步速度提升了一万倍。
然后写了在同步完之后通过改 setting 设置索引的功能,一切看似非常正常,直到一觉睡醒还是没有任何索引。
这不应该啊。检查之后发现,虽然提交了修改索引的任务,但跑到一大半爆炸了:
Index `nmbxd`: internal: MDB_TXN_FULL: Transaction has too many dirty pages - transaction too big.
终于不是 meilisync 的问题了!
优化的内存#
仔细确认之后发现,其实不是跑了几个小时。它跑了几十分钟就 transaction too big 了,然后缩小 batch 重试,直到咋都试不出来。
锻炼!
经 @yzqzss 提醒了最佳实现是先设定 attribution,再同步 index,遵循最佳实现,但那样的话可能再出现 408。要解决 408 得实现队列,我比较懒不想再写代码了。
随后找了下竟然找到了能减少 index 时内存占用的 flag,参考 https://github.com/meilisearch/meilisearch/issues/3603 ,--experimental-reduce-indexing-memory-usage
,的确一用就灵,一次成功。
以及关于更新,运行 meilisync refresh 即可。
再之后就是写 systemd 来定时进行同步了,如下。
# /etc/systemd/system/meilisync.timer
[Unit]
Description=Run meilisync refresh nmbxd weekly on Monday at 5 AM
[Timer]
# Run every Monday at 5:00 AM local time
OnCalendar=Mon *-*-* 05:00:00
Persistent=false
[Install]
WantedBy=timers.target
# /etc/systemd/system/meilisync.service
[Unit]
Description=Meilisync Refresh nmbxd
#After=network.target
[Service]
Type=oneshot
WorkingDirectory=/path/to/meilisync/config/
Environment='LOGURU_LEVEL=DEBUG'
ExecStart=/etc/meilisync/meilisync/bin/meilisync refresh
配置文件/path/to/meilisync/config/config.yml
debug: false
progress:
type: file
source:
type: mongo
host: REDACTED
port: REDACTED
username: 'REDACTED'
password: 'REDACTED'
database: REDACTED
meilisearch:
api_url: http://127.0.0.1:REDACTED
api_key: REDACTED
insert_size: 10000
insert_interval: 10
sync:
- table: REDACTED
index: REDACTED
full: true
pk: _id
attributes:
id: [filterable, sortable]
fid: [filterable]
img: [filterable]
ext: [filterable, sortable]
now: [filterable, sortable]
name: [searchable]
title: [searchable]
content: [searchable]
parent: [filterable, sortable]
type: [filterable]
userid: [filterable]
sentry:
dsn: ''
environment: 'production'