Ovler

Ovler

tg_channel
twitter
telegram
github

MongoDB 向 MeiliSearch 同步的踩坑記錄

使用 meilisync

感謝 yzqzss 的大力支持

tl;dr: fork 並大改了程序,參見

崩壞的前端#

先嘗試使用了他的 Admin Console,https://github.com/long2ice/meilisync-admin

只有 AMD64,沒有 ARM 的鏡像。誰還沒有 x86_64 的機器啊,嘗試在非數據庫的機器上使用。我當時還沒意識到從 A 機拉數據到 B 機再塞給 C 機是啥概念,但總之當時犯傻了。

下鏡像、運行…… 等等這個數據庫同步的 admin 為什麼還要 MySQL 和 Redis 的 DBurl 啊?不理解但是當時配置了。

隨後……

  1. 沒有初始賬戶 ref #7

    解決方案是手動寫郵箱密碼進數據庫,還要手搓 bcrypt hash

  2. 創建 MongoDB 數據源,報錯 Unknown option user #11

    原因竟然是,不同數據庫在後端配置文件需要的參數不同,網頁只按照 PostgreSQL 的 user 進行了傳參,而且後端沒處理直接塞,sync 程序原地爆炸。

    使用抓包改包重放解決。

    本來寫了 fix 但發現 PostgreSQL 的參就是對的,那愛咋咋地吧我感覺我管不了。也應該沒人看到這還想用吧…… 應該吧。

  3. 設置好一切了之後還是跑不起來…… 後端有如下報錯(刪除一噸內容):

    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。

解決方案有三種:

  1. 不引用這個 plugin

  2. 修改 plugin 內容

  3. 修改全局的 log 級別:

    Meilisync 使用 loguru,參考其文檔可以通過設置 level 實現,再根據其環境變量的相關文檔,可以設置 LOGURU_LEVEL,可採用值如下表:

    Level nameSeverity valueLogger method
    TRACE5logger.trace()
    DEBUG10logger.debug()
    INFO20logger.info()
    SUCCESS25logger.success()
    WARNING30logger.warning()
    ERROR40logger.error()
    CRITICAL50logger.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'
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。