你好!我是 Lewis,這是我在 Tangled 的第一篇文章。 在 Tangled,我的工作涉及大量研究與開發,也就是所謂的「亂搞與找出答案」。讓我來告訴你其中一個發現,這個發現最終變成了這個新的 Tangled API。

Bobbin 是 Tangled 的唯讀、僅 API AppView。 它透過 XRPC 提供 sh.tangled.* lexicons 的讀取端,讓用戶端能夠呈現 git 儲存庫、issues、pulls、 comments、stars 與 follows,而無需向 N 個 PDSes 查詢資料。它沒有永久儲存,所有資料都只存在於 RAM 中,並在每次重新啟動時從上游重新回填。

我和其他許多人一樣,覺得「AppView」這個詞太過抽象,甚至可能命名不當。在 atproto 中,資料集分散在每個用戶擁有的 Personal Data Servers (PDSes) 上,因此 AppView 是一個單一 API,透過它可以查詢資料集,而不必讓下游的每個用戶端都與每個 PDS 維持連線。AppView 是許多用來減輕 PDS 負擔的程式之一,讓 PDS 相對便宜且資源使用效率高。

讓我列出我認為 Tangled 的 AppView 存在的一些問題,這樣才能理解我發現 Bobbin 的探索歷程:

  1. Tangled 的 AppView 沒有 API;它透過 SSR 同時擔任網頁用戶端。
  2. 它很脆弱,無法輕易回填 atproto 資料。
  3. 它的架構從根本上就是為假設自己是資料來源的網頁程式而建構的。
  4. 由於上述原因,加上確實存在一些私有資料,例如 email 與使用者 DID 的對應關係,因此 Tangled 應該只有一個 AppView。這意味著到 Tangled 的延遲大致取決於與斯德哥爾摩伺服器的距離。
  5. 它在 RAM 危機期間相當耗用記憶體。

將上述所有問題反轉,就會得出我理想中的 AppView:一個僅 API、易於回填、易於分散部署、節省 RAM 的程式。

為什麼要「僅 API」?一旦能輕易透過通用 XRPC API 取得 Tangled 資料,就能在其上建構更多程式;例如替代前端、第三方用戶端、個人專用儀表板,在我的案例中則是為了打造良好的 CLI(雖然這也屬於替代前端的範疇)。現有的 Tangled AppView 同時也是網頁用戶端,意味著沒有任何可擴充的基礎可供建構,除非你是爬蟲高手;否則必須自己想辦法從野生的 PDSes 世界中抓取 Tangled 資料集。

在我思考如何打造這樣一個有趣程式的同時,我也對狀態儲存產生了危機感,尤其在半夜驚醒,擔心 Tranquil PDS 的自製嵌入式資料庫可能出現新的神秘問題。我如何避免這個新程式需要寫入任何持久且可能出錯的狀態?此外,AppViews 位於 PDSes 的下游,我們在 AppView 中保留的狀態越多,就越有可能與真實情況產生分歧。只要遺漏一筆記錄,AppView 就會嚴重錯誤,因此讓 AppView 能夠與真實情況保持一致至關重要。

身為一個簡單的人,我認為要打造一個只負責快取與提供記錄的程式,最好的方法就是讓它永遠不要碰磁碟。我希望它在每次重新啟動時都能從頭回填所有資料。這樣一來,與真實情況的調和只要重新啟動就能自動完成!這可行嗎?如果每次回填都需要一整天,那會讓部署變得很慢,這樣值得嗎?

我對這個概念的初步測試結果令人振奮,我快速組裝了一個小型伺服器,實際上可以在約 15 分鐘內回填 Tangled 的所有 atproto 資料集,並使用 Hydrant 作為聚合器。我決定可接受的回填時間應在 5 分鐘以內,這樣才能在合理的暖機時間內切換藍綠部署的執行中實例。

Hydrant 是一個用於任意 atproto 資料集的聚合器,它會追蹤即時的 firehose, 透過拉取每個 repoCARs 來回填網路上的每個 repo, 並透過 websocket 提供服務。Bobbin 只需要開啟連線、要求游標 0,然後從產生的資料流中(重新)建立索引即可。

非常感謝 Dawn 製作了 Hydrant!之後又聽到我的困難,主動幫忙優化它!

當我把回填時間縮短到 5 分鐘後,我又把目標往前推進,現在的可接受回填時間應該在 90 秒以內。這也是我們今天正在接近的數字,視上游 Hydrant 實例的暖機程度而定;最佳情況下約 30 秒,最糟情況(Hydrant 也必須從頭從 PDSes 拉取記錄)則可能需要 20 分鐘。

是的,你也可以在 20 分鐘內啟動這些實例並取得整個 Tangled 的資料,或是獲得退款。(€0 的退款。)

我遇到了一個小難題:在回填還在進行時,程式應該提供什麼服務?如果聚合或連結資料可能不正確,我是否應該讓程式繼續提供請求?我不這麼認為。那麼,在最糟情況下可能需要 20 分鐘的回填期間,我是否應該讓程式什麼都不提供?我原本可以這麼做,但我們改為連線到 Slingshot 進行點查詢,至少能立即取得準確資料,即使程式本身尚未正式接收到這些資料。

Slingshot 是一個很棒的 atproto 記錄與身份邊緣快取。Bobbin 會向它查詢單一記錄與身份,以便新誕生的 Bobbin 在自己的索引還沒暖機前,就能準確回答個別查詢。Slingshot 是 microcosm 專案的一部分,這個專案是一組由社群運行的 atproto 基礎設施。感謝 Fig 製作它!

這個正在變成 Bobbin 的程式,能夠將 Tangled 的所有 atproto 資料集存放在約 200MB 的 RAM 中,這已經很不錯了,但經過一些壓縮後降到 100MB。還有更多可以壓縮的地方。「哇,整個平台的社交層只需要這麼少的資料!」你可能會這麼說,但恰恰相反,請記住這是從今天開始,資料集最可能永遠保持最小的狀態。節省空間至關重要。

Bobbin 使用 Slingshot 作為即時記錄/身份解析器,用於 RAM 中尚未持有或尚未從 Hydrant 接收的資料。我很欣賞 Fig 的工作,但我依賴 Slingshot 而非自己撰寫查詢,主要是為了節省時間;如果 Bobbin 未來有一項重大的架構變更,那就是將這部分內嵌進去,讓 Bobbin 永遠不需要查詢 Slingshot,而是能更快地取得相同資料。

有人可能會想在自己的 AppView 程式中嵌入 Hydrant 實例,讓特定 atproto 資料集的提供只是 Hydrant 資料的薄包裝;但我希望能輕鬆啟動數百個實例。如果有數百個 Hydrant 實例不斷查詢 PDSes,那會相當浪費資源。一個 Hydrant 實例就能毫不費力地服務數百個 Bobbin。

由於我們沒有針對 Knot 相關資料的 Hydrant/relay 等效方案,Bobbin 會將 Knot 相關請求直接代理到指定的 knot。我認為需要一個 knotstream 聚合器,甚至可以只是 Hydrant 的一個外掛,讓它同時提供這兩種服務。我認為 Bobbin 與 Knot Mirror 應該合併為一個服務,但這只是我的個人意見。

有了「無狀態」的 AppView,我們就不必擔心資料庫及其問題:遷移、當資料與「真實」資料集略有偏差時的生產環境調整等等。每當我們想要變更或新增功能時,只需重新啟動 Bobbin 即可完成操作。另一個好處是 Bobbin 非常適合「雲端原生部署」,如同企業所說的那樣。Tangled 自己的旗艦實例就運行在 Cloudflare Containers 上。

話雖如此,我並不完全執著於 Bobbin 永遠不碰磁碟的想法,我認為一個有趣的改進是每小時將 RAM 儲存的資料內容傾印為快照,啟動時只需回填差異;但目前我認為還有空間繼續走這條路,因為 Bobbin 在撰寫本文時只需分配 200MB 就能健康運行。此外,我原本對 RAM 友善的要求目前仍然滿足,但當它變得太重時,Bobbin 就會在執行期間動態將部分資料卸載到磁碟。我如何定義「太重」?我看著自己的心,它告訴我 1GB RAM 是浪費,10GB 是不可接受的。Bobbin 還必須服務使用者,每個 API 請求都會對 RAM 造成額外壓力,而不僅僅是儲存 Tangled 資料集。

就是這樣!現在有了這個有趣的 AppView,它沒有磁碟儲存、回填時間低於 90 秒,作為一個小小的引擎來提供 API 請求。有了它,任何人都能更輕鬆地使用 Tangled 資料集來建構程式。

api.tangled.org 上有一個 Bobbin 實例,希望你會覺得自行託管也很容易。Bobbin 的原始碼位於 我們的單體儲存庫