使用 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'