Skip to main content

Wei Ji

最近在整理腦中對於儲存的觀念,試著用「如果儲存是 RPG 等級制」的概念寫了一段虛構的過程,不完全嚴謹,但是應該有助於初學者建立觀念。

Level 1

你安裝了 Linux 作業系統,在安裝過程把硬碟格式化成 ext4 並切割了磁區:

  • /boot/efi: fat32
  • /boot: ext4
  • /home: ext4
  • 剩下的給 /: ext4

安裝給系統的檔案會到 /,使用者各自囤積的檔案則會儲存在 /home

但是用著用著,你發現 /home 快滿了但是 / 還很空。

Level 2

你安裝了 Linux 作業系統,這次你在安裝過程先對硬碟建立 LVM(Logical Volume Manager):

  • /boot/efi: fat32
  • /boot: ext4
  • VG (Volume Group)
    • /home LV (Logical Volume): ext4
    • 剩下的給 / LV: ext4

但是用著用著,你發現 /home 快滿了但是 / 還很空,於是你利用 LVM 的特性修改 /home/ 這兩個 LV 的大小。

但是用著用著,你發現硬碟滿了,於是你買了另外一個硬碟接上電腦,卻發現現在你有 sdasdb 兩顆硬碟了,資料夾的安排變得很不方便。

Level 3

你安裝了 Linux 作業系統,這次你在安裝過程先對硬碟建立 LVM(Logical Volume Manager):

  • /boot/efi: fat32
  • /boot: ext4
  • VG (硬碟A + 硬碟B)
    • /home LV (Logical Volume): ext4
    • 剩下的給 / LV: ext4

你把多個硬碟當成一個硬碟使用,用得很愉快,但是好景不常,外面發生了警匪槍戰,一顆子彈剛好打在你的其中一顆硬碟上,現在你所有的資料都沒辦法讀取了。

Level 4

你根據教學 (https://std.rocks/gnulinux_mdadm_uefi.html) 折騰了一番,

  1. 先在每一個硬碟切出 EFI 和普通磁區
  2. 再用 mdadm 將磁區建立 raid 1 (或是 raid 1/0 或是 raid 5)
  3. 再把 mdadm 陣列格式化成 LVM
  4. 把 LVM 切成 //home 要用的邏輯磁區
  5. 最後再把 Linux 安裝進 /

用著用著,外面又發生了槍戰,你的其中一個硬碟又被打壞了,這次你有恃無恐的更換掉一顆硬碟。

但是重建速度太慢,重建完成之前第二顆硬碟又被子彈打中了,現在你的所有資料又丟失了。

Level 5

你買了一張硬碟陣列卡,這次你直接在一張 SSD 安裝系統,陣列卡模擬的硬碟直接掛在 /home 下,你心想系統壞了就直接重灌就是了。

用著用著,外面又發生了槍戰,你的其中一個硬碟又被打壞了,這次你有恃無恐的更換掉一顆硬碟。

過了幾年外面又發生了槍戰,這次換你的硬碟陣列卡被打中,買不到相同型號的陣列卡,你的資料又報廢了。

Level 5-1

在 Level 5 陣列卡被打壞後,你意識到 RAID 不是備份 (RAID is not Backup)。你開始實行 3-2-1 原則:

  • 3 份資料拷貝。
  • 2 種不同的存儲媒介。
  • 1 個異地存放
info

本段落由 Gemini fast 補充。

Level 6

這次你決定安裝 ZFS 把多顆硬碟組成陣列,享受它帶來的冗餘與快照功能。

然後你發現你的硬碟數量受到主機板的 I/O 數量限制,雖然你可以用兩台電腦組合成更大的帳面容量,但是你面臨 Level 2 類似的情況,你有兩個檔案目錄不方便管理。

Level 6-1

在使用 ZFS 後,你發現了「靜態資料損壞」的可怕。你開始定期進行 zpool scrub,確保即便子彈沒打中,背景輻射或硬體老化也不會悄悄吃掉你的位元。

info

本段落由 Gemini fast 補充。

Level 7

你決定使用 GlusterFS,現在多個節點的儲存空間可以被視作一個資源池使用了。

Level 8

你發現在 GlusterFS 的架構下還要為每個節點配置軟體陣列太麻煩了,SDS (Software-defined storage) 本身就有實作冗餘機制,於是你直接使用 JBOD (Just a Bunch Of Disks)。

但是你發現你依然需要管理「儲存空間」這個概念,為什麼我不能有一個「接近無限」的實體而無須在意有多少空間?

Level 9

你配置了 MinIO,現在你要儲存資料不再關心磁區的概念,而是單純對檔案 CRUD。

你意識到你經歷三種儲存類型:

  • File
  • Block
  • Object

Level 9-1

在進入分散式存儲後,你發現資料不再是「寫進去就在那裡」。你開始研究 CAP 定理:在網路斷開時,你的系統要選擇「一致性 (Consistency)」還是「可用性 (Availability)」。

info

本段落由 Gemini fast 補充。

Level 10

你配置了 Ceph 處理了所有分散式儲存的需求,但是你發現你需要在每一個節點維護 Ceph 實例有點麻煩。

Level 11

你使用了 Rook,現在透過 Kubernetes,不論是應用程式還是儲存都透過它管理,只需要維護並運行 K8s 節點/叢集即可。

Wei Ji

這篇筆記已經放在我的 CodiMD 很久了 (2024-11-02),想說整理一下發一篇廢文。

info

關於我如何獲得模型檔案,請見稍早的文章:從 Blender 開始的解剖學筆記 - 骨骼篇 - 啟程

warning

本人非醫學背景,以下不專業筆記如有錯誤歡迎指出。

腳掌

腳掌可以分成三個區塊1

  • 趾骨骨群 (Phalanx)
  • 蹠骨骨群 (Metatarsus)
  • 跗骨骨群 (Tarsus)
info

留意用詞區分了兩種概念:

  • 某個骨群(單數)
  • 某骨頭們(複數)

例如:Tarsus 是指「腳跟那個骨群」和 Tarsals 是指「腳跟那些骨頭」。

跗骨 (Tarsals)

跗骨 (Tarsals) 則由多個骨頭構成2

  • A: 跟骨 (Calcaneus)
  • B: 距骨 (Talus bone)
  • C: Cuboid bone
  • D: 足舟骨 (Navicular bone)
  • E, F, G: 楔形骨 (Cuneiform bones)
    • Medial
    • Intermediate
    • Lateral

距骨 (Talus)

"anklebone," 1690s, from Latin talus "ankle, anklebone, knucklebone" (plural tali), related to or a derivative of Latin taxillus "a small die, cube" (they originally were made from the knucklebones of animals), which is of obscure origin.3

talus 在拉丁文中有腳踝之意。

跟骨 (Calcaneus)

足舟骨 (Navicular Bone)

楔形骨 (Cuneiform)

楔形骨從腳掌剖面看過去很像三角形的楔子:

Medial Cuneiform

Intermediate Cuneiform

Lateral Cuneiform

骰骨 (Cuboid Bone)

跟其他骨頭比起來,它的確蠻方的:

蹠骨(Metatarsus)

Metatarsal 1

Metatarsal 2

Metatarsal 3

Metatarsal 4

Metatarsal 5

近節趾骨 (Proximal phalanx)

Proximal Phalange 1

Proximal Phalange 2

Proximal Phalange 3

Proximal Phalange 4

Proximal Phalange 5

Phalanx

有趣的是,英文 Phalanx 有方陣之意,趾骨排列的方式是不是很像方陣呢?

Middle phalanx (Intermediate phalanx)

拇指沒有中節趾骨,手指也是一樣的情況。

Intermediate Phalange 2

Intermediate Phalange 3

Intermediate Phalange 4

Intermediate Phalange 5

Distal phalanx

Distal Phalange 1

Distal Phalange 2

Distal Phalange 3

Distal Phalange 4

Distal Phalange 5

Footnotes

  1. File:Ospied-en.svg - Wikipedia. Mario modesto. Retrieved 2026-02-25, from https://en.wikipedia.org/wiki/File:Ospied-en.svg

  2. Tarsus (skeleton) - Wikipedia. Retrieved 2024-11-04, from https://en.wikipedia.org/wiki/Tarsus_(skeleton)

  3. talus | Etymology of talus by etymonline. Retrieved 2024-11-04, from https://www.etymonline.com/word/talus

Wei Ji

最近在陸續把服務從運行在一台主機 Docker Swarm 遷移到另外一台主機的 Kubernetes 中,

一直到回老家過年前 (2026-02-15) 已經完成大部分服務的遷移,這裡紀錄一下剩餘的服務以及還沒完成遷移的原因。

簡單遷移流程

因為我使用 Longhorn 作為 Volume Provider,資料並不是直接寫在 host 的檔案系統內的,而是寫在類似虛擬機硬碟映像檔的東西內,

$ ll
total 14945644
drwx------ 2 root root 4096 Feb 20 07:01 ./
drwxr-xr-x 22 root root 4096 Feb 24 12:16 ../
-rw-r--r-- 1 root root 21474836480 Feb 25 00:56 volume-head-000.img
-rw-r--r-- 1 root root 126 Jan 24 11:55 volume-head-000.img.meta
-rw-r--r-- 1 root root 143 Feb 20 07:01 volume.meta

因此不能直接單純的把資料從一個主機的硬碟複製到另外一個主機硬碟,而是需要經過一層 K8s 把資料寫入 Volume 內。目前使用的遷移步驟大致如下。

1. 起草 K8s YAML

使用 Kompose 將 Docker Swarm 的 YAML 轉換成 K8s 資源,並且進行適當的修飾(例如:有狀態的服務從 Deployment 改成 StatefulSet)。

並且在目標 Pod 掛上臨時的 container,用於提供 PVC 寫入的 runtime,同時先註解掉真正的服務,如下:

K8s YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
io.kompose.service: pinry
name: pinry
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: pinry
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
template:
metadata:
labels:
io.kompose.service: pinry
spec:
containers:
# - image: docker.io/getpinry/pinry:2.1.13
# name: pinry
# ports:
# - containerPort: 80
# protocol: TCP
# volumeMounts:
# - mountPath: /data
# name: pinry-data

# Used to do data migration
- image: docker.io/library/busybox:latest
name: busybox
command:
- sleep
- "3600"
volumeMounts:
- mountPath: /data
name: pinry-data
restartPolicy: Always
volumes:
- name: pinry-data
persistentVolumeClaim:
claimName: pinry-data

2. 確認原始 Volume 大小

du -s -h

3. 遷移

3.1 直接 cp,適用小量遷移

先遷移到本地:

rsync -avh --info=progress2 --info=name0 --delete \
root@arachne-node-beta:/mnt/das-storage/volumes/pinry_data/ \
./pinry_data/

之後上傳到 Pod:

kubectl cp -n pinry-stack ./pinry_data/ pinry-0:/pinry_data

移動檔案到 PV 掛載的路徑:

kubectl exec -n pinry-stack --stdin --tty pinry-0 -- /bin/sh
cp -rf  /pinry_data/* /data/.
info

需要拆分兩個步驟是因為 kubectl cp 指令只能複製資料夾,不能在兩個資料夾之間直接同步內容。

3.2 tar 打包後 cp,適用小量遷移

步驟同上,只是多了一個打包/解包的步驟1

# 打包壓縮
tar -czf gitea.tar.gz <PATH>

# 解壓縮解包
tar -xzf gitea.tar.gz

3.3 hostPath,適用中量遷移

上述方法對於容量小的遷移尚可處理,但是我的 ArchiveBox 有 12 GB 的資料,kubectl cp 傳輸過程會遇到以下問題:

error: unexpected EOF

於是我在 Deployment 上加掛一個 hostPath Volume:

K8s YAML
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
io.kompose.service: archivebox
name: archivebox
spec:
replicas: 1
selector:
matchLabels:
io.kompose.service: archivebox
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
template:
metadata:
labels:
io.kompose.service: archivebox
spec:
containers:
# - name: archivebox
# image: docker.io/archivebox/archivebox:0.7.3
# ports:
# - containerPort: 8000
# protocol: TCP
# volumeMounts:
# - mountPath: /data
# name: archivebox-data

# Used to do data migration
- image: docker.io/library/busybox:latest
name: busybox
command:
- sleep
- "3600"
volumeMounts:
- mountPath: /data
name: archivebox-data
- mountPath: /archivebox_data
name: tmp-archivebox-data
restartPolicy: Always
volumes:
- name: archivebox-data
persistentVolumeClaim:
claimName: archivebox-data

# Used to do data migration
- name: tmp-archivebox-data
hostPath:
path: /mnt/archivebox_data

先把資料從一台主機移到另外一台主機,再進入容器中把資料從 hostPath Volume 移到 Longhorn 去。

4. 切換 container

將遷移用暫時性的 container 註解並且把真實服務掛回。

剩餘服務

以下是過年前尚未完成遷移的服務,主要是因為有額外的雜務需要處理,不適用上述簡單遷移方法。

MinIO

MinIO 的遷移比較特別,一來是已經停止維護了2,雖然作為封閉的地端使用情況並不需要太過擔心安全性問題, 可以繼續使用已經存在的 OCI (Open Container Initiative) 映像檔,不過依然可能要物色一下其他替代方案。

二來是 MinIO 是一個 S3 (Simple Storage Service) 實例,本身就有 CRUD (Create, read, update and delete) 的 API,因此資料遷移時無須考慮 Volume 層級的問題,只要用 mc 指令同步兩個在不同實例上的 Bucket 即可。

Harbor

Harbor 本身就有 Helm 可以使用,但是因為我是客製化 docker-compose.yaml 的情況,考慮資料遷移的複雜性可能不能直接使用 Helm。

加上 Harbor 的微服務結構跟我 selfhosted 的其他服務相比複雜得多,翻譯成 K8s 的過程會比較麻煩。

Jellyfin

資料比較多 (1.3TB),即便是 hostPath 方案也必須在新的主機上消費兩倍的硬碟空間,因此 hostPath 不適合用來遷移這種規模的資料。

我腦海浮現兩種解決方法,第一個是直接在舊將資料封裝成 SDS (Software-defined storage),然後在新節點上作為 Volume 掛載後進行資料轉移。

第二個方法是把 Volume 在新節點上掛給一個 SSH 容器,由 SSH 完成資料轉移。

Gitea

Gitea 因為需要使用 SSH (Git over SSH),無法透過 Ingress 處理,而必須設定 Load Balancer。

info

Ingress 是 L7 的 HTTP 反向代理,SSH 是建立在 L4 的 TCP 連線上,因此需要 L4 的 Gateway (即 Load Balancer) 處理。

AptCacherNg

AptCacherNg 雖然是使用 HTTP,但是 apt 指令的實做似乎不會帶上 hostname 之類的資訊,因此服務不能運行在反向代理之後,在 Docker Swarm 的舊節點我是直接找個 port 暴露出去。

在 K8s 則是類似於 Gitea 的情況,必須要用 Load Balancer 額外設定。

Dashy

Dashy 的運作方式是每次容器啟動時,都會根據配置檔「編譯」一份靜態網站。我有點懷疑在 K8s 這種「容器是經濟動物;隨便新增隨便刪除」的哲學下,這種運作是否恰當,視情況可能需要找其他的 homepage 替代方案。

Footnotes

  1. Remco Kersten - Importing Data into Longhorn. aspberry Pi 5 for 4K Gaming - Jeff Geerling. Retrieved 2026-02-25, from https://www.remcokersten.nl/posts/import-data-into-longhorn/

  2. MinIO 已死,MinIO 復生 - 知乎. Retrieved 2026-02-25, from https://zhuanlan.zhihu.com/p/2008215929461445776

Wei Ji

在 Homelab 的 S3 中我有幾個 Bucket:

3D 模型

理想上我是希望有一個「開源自架的 Sketchfab」,來儲存這類檔案,不過目前還沒找到合適的方案因此就先放在 S3 內,具體是哪種檔案呢?例如:

素材

跟 3D 模型類似,理想上我是希望有一個「開源自架的 itch.io/opengameart.org」但是因為目前沒有所以先找個地方塞。

資料集

可能跟機器學習有關的資料集,這種資料集通常動輒數 GB,為了節省網路流量,我收錄了幾個有興趣的在 homelab 裡,以備不時之需(例如:COCO dataset val2014cv-corpus-15.0-2023-09-08...)。

等距長方投影 (Equirectangular)

之前經手過處理 Equirectangular 相關的專案,跟資料集的情況差不多,8K 影片的話動輒數 GB,手邊存幾份樣本方便日後處理類似題目的時候有檔案可以用。

機器學習模型

之前隨手開的 Bucket,目前已經有 Huggingface 的鏡像站 (Olah)了,之後用途可能不大了。或許可以用來儲存那種沒有被上傳到 Huggingface 的野雞模型。

作業系統映像檔

這應該不用解釋吧...?安裝 Linux 的時候手邊存一份備著。

SDK

不少軟體的 SDK 非常的肥大,為了避免日後需要花時間重複下載,手邊備一份。

Windows 應用程式

我開始使用 Linux 以前囤積的軟體。

遷移過程

整個 S3 的遷移過程大致如下:

  1. 安裝 mc 指令:
curl https://dl.min.io/client/mc/release/linux-amd64/mc \
--create-dirs \
-o $HOME/.local/bin/mc

chmod +x $HOME/.local/bin/mc
  1. 分別設定新/舊的 S3 實例:
$ mc config host add minio-server http://localhost:9000
Enter Access Key:
Enter Secret Key:
  1. 搬遷檔案:
mc mirror minio-server/3d-models rustfs-server/3d-models

RustFS

程式碼https://github.com/rustfs/rustfs
星數22.3k

因為 MinIO 官方不再維護1,RustFS 是一個倍受推崇的替代方案,於是我便嘗試了一下。

然後遷移過程遇到以下問題:

mc: <ERROR> Failed to copy `http://s3.minio.arachne/sdk/cuda-repo-ubuntu2204-13-0-local_13.0.1-580.82.07-1_amd64.deb`. Put "http://s3.apps.liquid.arachne/sdk/cuda-repo-ubuntu2204-13-0-local_13.0.1-580.82.07-1_amd64.deb?partNumber=6&uploadId=YzRiMmE0YTgtN2JlOC00ZjY4LTlmZjUtYmVkYzY0NGI4NTg4LjZhNTMzZDU2LWZiZGMtNDk3OS05ZDI5LTY4ZjVhZDllMGNjYngxNzcxOTI2ODgzMDU2MzY3MTc3": http: ContentLength=16777216 with Body length 14680064
mc: <ERROR> Failed to copy `http://s3.minio.arachne/sdk/cuda-repo-ubuntu2204-13-0-local_13.0.1-580.82.07-1_amd64.deb`. Put "http://s3.apps.liquid.arachne/sdk/cuda-repo-ubuntu2204-13-0-local_13.0.1-580.82.07-1_amd64.deb?partNumber=6&uploadId=YzRiMmE0YTgtN2JlOC00ZjY4LTlmZjUtYmVkYzY0NGI4NTg4LmM5MWU3NjMzLTk2ZTYtNDNkYy1iMDg0LWNlYzI5YjgyNzMzZHgxNzcxOTI3MzIwNjk0MjE5MjYy": http: ContentLength=16777216 with Body length 14680064
mc: <ERROR> Unable to list comparison retrying.. context canceled

不過我對 CUDA SDK 沒什麼留念,刪除之後剩下的檔案都順利完成遷移了。

接著映入眼簾的是永遠在轉圈圈的 Bucket 大小:

不過還好,這也只是錦上添花的功能,但是 Bucket 的設定頁面也一直在轉圈圈是怎麼回事?

後來近一步調查:

Be ware of the recent RustFS CVE2 because a static key was vibe coded into the product… even though they mitigated the issue, my confidence dropped severely because of this. 3

This project looks mostly vibecoded, after a quick review I have found a dozen of obvious problems4

很好,RustFS 的嘗試到此為止,繼續使用 MinIO;推移更新 S3 實作的計畫。

info

我使用的 RustFS 是 docker.io/rustfs/rustfs:1.0.0-alpha.83,供參考。

info

另外我有評估過 Garage,不過它有兩個問題:

  • 設定稍微複雜一點,它需要設定 domain name,似乎難以在單純的環境 (localhost) 中測試。
  • 不像 MinIO 有開箱即用 Web UI。

MinIO

不幸的是即便我使用 mc 進行 MinIO 到 MinIO 的遷移依然遇到諸如以下的錯誤:

mc: <ERROR> Failed to copy `http://s3.minio.arachne/os-image/lubuntu-24.04.4-desktop-amd64.iso`. You did not provide the number of bytes specified by the Content-Length HTTP header.
mc: <ERROR> Failed to copy `http://s3.minio.arachne/os-image/2018-11-13-raspbian-stretch-full.img`. Resource requested is unreadable, please reduce your request rate

最後是使用 Rclone 解決。

Footnotes

  1. MinIO 已死,MinIO 復生 - 知乎. Retrieved 2026-02-25, from https://zhuanlan.zhihu.com/p/2008215929461445776

  2. Update your RustFS immediately - Hardcoded token with privileged access (CVE-2025-68926) : r/selfhosted. Retrieved 2026-02-25, from https://www.reddit.com/r/selfhosted/comments/1q432iz/update_your_rustfs_immediately_hardcoded_token/

  3. What is the Best MiniO Alternative Right Now, RustFS, Garage or SeaweedFS ? : r/selfhosted. Retrieved 2026-02-25, from https://www.reddit.com/r/selfhosted/comments/1qcm5r5/comment/nzjbtcc/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

  4. What is the Best MiniO Alternative Right Now, RustFS, Garage or SeaweedFS ? : r/selfhosted. Retrieved 2026-02-25, from https://www.reddit.com/r/selfhosted/comments/1qcm5r5/comment/nzo0ez8/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

Wei Ji

家庭、公司、政府...是常見構成國家的「最小協作單元」,當政府要買東西的時,往往受到預算法、採購法...等等法律的約制,以及制度構成的嚴謹紀錄;公司(大型企業)要買東西時的情況也差不多,受到內部的採購制度約束,以及透過 ERP、POS 等資通訊系統追蹤。

家庭或個人在「消費行為」這件事情上相較之下就顯得缺乏紀律,可能隨意購買路邊攤的小吃、短期承租店面的「出清小店」,缺乏審計制度。如果買了一個不合用的拖把,可能也不會特意去紀錄品牌跟製造商,避免下一次再購買它。換言之,企業會極力的紀錄、分析消費者,但是消費者往往不會追蹤企業的供應鍊、仔細比對不同廠商之間的差異。

這種不對稱性使得自然人在面對巨靈時,往往處於弱勢的地位。


「舉證之所在,敗訴之所在」是一句法律圈的俗語,這點出了一個奇怪的現象,法律似乎假設了一種「超人」的存在,這個超人無所不知(不會出現記憶模糊)、無所不能(總是有證據),政府或企業透過嚴謹的制度與完善的基礎建設,能夠調閱報表、單據、POS、ERP 紀錄...作為證據,使其代表的法人能夠無限逼近這個「超人」,反觀憑感覺生活的家庭或個人在法律活動中往往處於弱勢。

Wei Ji

背景知識

要明白我要表達什麼,需要具備一些先驗知識,包含:Dyason Swarm, Starlink 和 樹莓派叢集,我會先做一些簡單的解釋幫對這方面概念比較缺乏的讀者能夠搭上我的思路。

戴森雲 (Dyason Swarm)

戴森雲是一種科幻構想,簡單來說就是:

用大量的人造衛星圍繞並運行在太陽附近來採集太陽能源。

更具體的介紹可以觀看 Kurzgesagt 的影片,我就不在這邊細談了:

info

Kurzgesagt 的影片算是高度簡化版本,例如有人指出「把人造衛星降到靠近太陽的軌道這件事本身就需要消費不少能源」1

然後我的立場是:

太酷了!我想蓋!

不過我看完 Kurzgesagt 的第一個想法是,它一定需要有一個資料中心來遙測與控制這麼多的衛星。

星鏈是由接近一萬顆人造衛星構成的網路系統,讓消費者不用透過海底電纜就能使用高速的無線網路。尤其適合有線網路基礎設施不夠發達的國家,其他使用情境包含空運、海運等位於海洋中間或在深山之類缺乏基地台地方。

請特別留意 Starlink 佈署時堆疊在火箭內的樣子:

https://www.space.com/spacex-starlink-falcon-9-rocket-fairing-video

樹莓派叢集

樹莓派 (Raspberry Pi) 是原先出於教育目的而被設計出來的單板電腦,它就像一個手長大小的電路板,卻具備電腦的完整功能。

預留腳位的設計讓它能夠作為開發板被客製化裝上各種感測器或擴充電路。其中一個例子是把它跟獨立顯示卡裝在一起2

樹莓派叢集則是將多個樹莓派透過網路線連接在一起構成一個運算叢集:

https://dev.to/fredinono/from-nas-to-kubernetes-setting-up-a-raspberry-pi-cluster-with-synology-nas-57f4

是不是跟堆疊的衛星有幾分神似?

現實

現在讓我把眼光從戴森雲、星鏈拉回我那 4.3 坪的租屋處,身處於寸土寸金的大台北,五年網頁前端開發經驗,混雜一些 3D、後端、Docker...的經驗。

這是我手上有的牌,接下來我該如何玩這個名為現實的遊戲呢?

Kubernetes

操作著數以百計的容器

閱讀 K8s (Kubernetes) 相關的文章很常會看到類似的描述。

「它一定需要有一個資料中心來遙測與控制這麼多的衛星」

它跟 Dyason Swarm 的影子在我面前重疊,「Dyason Swarm 的科技樹上必然有 K8s 的存在」我如此確信。

Longhorn

Longhorn 是基於 K8s 的分散式儲存,它可以和 K8s 以類似共生的關係運作在一起:透過 K8s 的叢集架構 Longhorn 可以運作在每一個節點上實現多節點冗餘,而當 K8s 內佈署的服務請求儲存區 (Persistent Volumes) 時,則由 Longhorn 直接向應用程式提供儲存空間。

因此一顆硬碟只要掛上一個堪用的電腦(比如一塊樹莓派),丟進 K8s 內就能讓它成為儲存資源池的一部分,多餘的運算能力則可以「順便」運行一些服務分攤叢集的負載。

基於筆電的 K8s 叢集

筆電有一些工程特性是一般桌機以及伺服器運算不會考量的:省電,即能源效率以及體積和輕量化,為了方便攜帶,可靠性也不會做得太脆弱。我認為這在台灣這種地狹人綢以及寸土寸金的都市中是十分重要的因素,我最為租屋族不太可能負擔得起充足的空間建設伺服器機櫃,遷移的頻率也遠比居住在大陸上的歐美人士來得高。

並且根據我的主觀體驗,很容易低成本獲得二手筆電,加上 K8s 和 Longhorn 的架構下,數個無須十分強大的節點就能提供儲存冗餘,從而解決基於筆電的設備無法使用硬碟陣列卡的問題;又或是避免管理軟體陣列帶來額外的麻煩,只要是能夠安裝並運行 K8s 節點的設備都能對這個叢集進行水平拓展。拓展叢集的關鍵零件,交換器本身又不會太昂貴。

筆電或是單板電腦的尺寸較小,比起機架伺服器或是一般的桌機來得更有空間彈性,必要時甚至可以把叢集拆成數個不同大小的單元分散在空間之中。

當然,要把二手筆電當作一個儲存節點,可能需要透過 USB 外掛硬碟,二手筆電低階的 CPU、低階的 USB 埠口、低階的乙太網路埠口都會成為系統的瓶頸。不過這種事情等遇到再說吧,瓶頸就是用來感受的,畢竟我都經歷過直接把 OS 灌在 DAS (Direct-attached storage) 外部儲存、最後 I/O await 高到會讓 Docker 不穩定才把系統裝回 SSD 了。

Footnotes

  1. The Dyson Swarm – A Personal Perspective - JMORE. Retrieved 2026-02-04, from https://jmoreliving.com/2020/06/08/the-dyson-swarm-a-personal-perspective/

  2. Use an External GPU on Raspberry Pi 5 for 4K Gaming - Jeff Geerling. Retrieved 2026-02-04, from https://www.jeffgeerling.com/blog/2024/use-external-gpu-on-raspberry-pi-5-4k-gaming/

Wei Ji

G 胖

G 胖 (Gabe Newell) 並且是 Valve 公司的擁有者,Valve 則是 Steam 這個最大的遊戲平台背後的開發與維護者。G 胖在玩家圈中有著很高的聲望,這裡挑幾個論點或例子。

G 胖曾經表達他對於盜版遊戲的看法:盜版源自於缺乏服務,玩家都會花錢買電腦,他們不會單純為了省那一點遊戲的錢而刻意去使用盜版軟體,盜版通常猖獗在遊戲軟體的服務觸及不到的地區,你只要把服務做好,盜版就會自然消失。那些針對盜版機制努力的公司根本搞錯重點,他們這樣做反而在降低玩家的消費體驗。

G 胖稱呼玩家為客戶 (Our Client) 而不是消費者 (Consumer)。

Valve 不是上市公司的特性,讓它能夠在 G 胖的領導下真正提高產品的品質,而無須迎合股東。

資本主義下的王者

關於上市公司與私人公司的比較我認為是一個很有趣的切入點,這也是很多人詬病的問題。在股份公司的架構下,權力集中在董事會上,但是股東通常不具備公司業務相關的知識背景,只是單純的出資者,並且要求金錢收益作為回報。結果就是造成「外行人領導內行人」的問題,經常讓公司推出一些「理論上有利於股東,但是不利於消費者」的策略。

反之,Valve 與 G 胖之間的關係是私人公司與唯一的資本持有者,G 胖本身又是具有遊戲相關背景的人,因此公司前進的方向通常是為了解決玩家(即消費者)的問題,而不是為了迎合股東的利益。

有趣的是,從權力結構上去觀察兩者,董事會反而是有分權的體系,私人公司則是完全獨裁的體系,G 胖的例子剛好成為「民主優於獨裁」的反向案例。不過有一點概念的人都知道,民主並沒有比獨裁才好,只是多了一些分散風險的機制,避免昏君帶領群體走向毀滅,而 G 胖剛好是「獨裁體制下的明君」。

權利與義務的失衡

一個權利背後往往伴隨著對應的義務,這是社會契約運作的基本原理。上市公司的結構反而呈現微妙的權責分配方式:股東有獲得公司收益的權利,卻沒有理解公司業務邏輯的義務。

我認為這個權利義務的不對稱、失衡正是造成上市公司在資本主義的遊戲規則下變成一種罪惡之源的根本性原因。

巨靈尺度下的集體惡意

霍布斯的巨靈論主要是一個論述透過匯集人民的權利義務到君主身上而構成君權合理性的理論,一個建立在人本上君權理論;而不是基於神學的君權神授。

我認為巨靈論可以描述一種集體惡意的狀態,例如:第一世界的公民購買巧克力這件事本身是非常中性的消費行為,但是無數的消費行為累積在巨靈尺度很有可能變成壓榨第三世界巧克力勞工的一種集體惡意,就像是一個第一世界巨靈在欺壓另外一個第三世界巨靈。

當我們用相同的概念來看帶「買股票、買基金」這種購買金融性產品的行為,會發現它在巨靈尺度同樣構成了一種惡意:無數人持有獲利的權利卻沒有理解基本底層邏輯(公司業務邏輯)的義務,其結果就是資本的代理人往往會做出很多傷害終端消費者的決策,完全有違資本運作最原始的目的:一個人幫助另外一個人,並用金錢衡量這個幫助。

Wei Ji

前情提要

前一陣子和人聊天,剛好提到了一個我六年前 (2020) 寫的一個小玩具,我似乎沒有把跟它相關的想法寫下來,於是試著在這邊文章做個整理。

有限像素影像辨識謎題

謎題規則如下:

  • 謎題本身為一張點陣圖
  • 初始狀態為 1 pixel
  • 點擊一次 1 pixel 會被切割成四等分
  • 換句話說每點一次(探勘)就會多揭露 3px 的資訊
  • 玩家必須用最少的探勘次數獲得的「馬賽克圖像」辨識出謎題

舉例來說,這是初始狀態:

這是第一次點擊:

這是點擊了 55 次之後:

經過 453 次之後,玩家可以辨識出這是「貓」:

453 就是這個玩家的量化成績,數字越小越好。

這個謎題的程式碼在此:

https://github.com/FlySkyPie/dequadtree-puzzle-game

CNN 問題

這個謎題啟發自卷積類神經網路 (CNN, Convolution Neural Network) 的問題。

CNN 會掃描整張圖片,並且偵測諸如邊界之類的微小特徵1

接著再透過這些微小特徵組合成比較高級的特徵,例如:臉、手...之類的。

這個特性為 CNN 帶來了一些問題:

  • 高頻下毒
  • 重組可辨識性

高頻下毒

攻擊者可以在影像內參入人類肉眼不可見的資訊,讓圖片無法被 CNN 正確的識別2

重組可辨識性

當圖片被重組打亂,人類將會難以辨識,但是 CNN 仍然能夠維持辨識能力3

模擬注意力

人類的視覺成像其實並不是均勻的,受到視網膜視錐細胞分佈的影響,實際上只有中間的區域具有比較多的成像資訊。另一方面任意瞬間視網膜接受的資訊其實是模糊的,是透過大腦在時間序上累積的資訊腦補成一個比較清晰的影像。

前面的謎題就是為了模擬這種特性而設計的,人類會將注意力放在最可疑、最有價值的區域去提高解析度,其他地方就算不管也沒關係。經過探索圖片本質上是一個四元樹 (Quadtree) 結構。

Footnotes

  1. Convolutional Neural Networks Explained. Retrieved 2026-01-25, from https://tvirdi.github.io/2017-10-29/cnn/

  2. How we might protect ourselves from malicious AI | MIT Technology Review. Retrieved 2026-01-25, from https://www.technologyreview.com/2019/05/19/135299/how-we-might-protect-ourselves-from-malicious-ai/

  3. Neural Networks seem to follow a puzzlingly simple strategy to classify images | by Wieland Brendel | Bethgelab | Medium. Retrieved 2026-01-25, from https://medium.com/bethgelab/neural-networks-seem-to-follow-a-puzzlingly-simple-strategy-to-classify-images-f4229317261f

Wei Ji

背景

機戰傭兵 (Armored Core) 是一款我沒玩過的遊戲,但是很享受於它的衍生作品,包含遊戲預告、實機影片之類的。其中一個讓我感興趣的是機甲在遊戲中機動的方式:

撇除誇張的瞬間噴射以及懸浮飛行以外,麥卡納姆輪 (Mecanum wheel) 在可控的室內平地同樣可以做出炫泡的機動:

因為很炫泡,所以我想做,於是一個構想就在我腦海成型:

  • 以 50cm 的迷你尺度實現。
  • 運行在草皮上,比室內平地粗曠,但是又保留考量可行性的平面。
  • 四足機甲造型,四足設計用來作為主動式懸吊系統。
  • 使用類似 ZIL-29061 螺旋輪結構,實現一般輪型或履帶型載具無法達成的漂移機動。
  • 使用 360 攝影機實時進行 3D 點雲掃描與場景重建,因此可以在一個渲染的 3D 空間實現第三人稱操作(近似遊戲本身的體驗)。

最近隨便用 Nano Banana 搓了一張概念圖,順便把這個想法做個筆記:

Nano Banana 使用筆記

info

過程我省略原圖,所有使用過得原圖我放在文章最後面。

第一次合成嘗試

提示詞:

將第一章圖(水道橋重工機甲)的底盤改成第二張圖(ZiL-2906)的螺旋輪。

保持原本的四足設計、將原本的兩個螺旋輪設計改成四個較小的尺寸的螺旋輪、對應原本的四足位置。

結果:

左邊的東西很明顯是這個上下文讓模型推論出 bug 了,不能期望它 one shot 搞定了,於是後面採用不同的策略。

提高解析度

第一次嘗試除了組裝失敗以外,底圖的解析度明顯太低,但是因為這張圖的角度剛剛好,所以我沒有再去尋找其他解析度比較高的圖片。

提示詞:

提高畫質、提高解析度 

第一次預組裝

將提昇過解析度的圖與第一次生成的螺旋輪做簡單的合成:

之後再用 Nano Banana 進行揉合,提示詞:

這是一個簡易合成的圖片,內容為四足人形機甲配上螺旋輪 (marsh screw),
有以下缺陷:
- 不完整的去背
- 缺乏正確透視
- 機甲與螺旋輪缺乏真實的機械連接
你必須修復上述缺陷。

結果:

提示詞:

移除機甲的左右手,保留上半身與頭部

結果:

這個步驟是因為我不太喜歡人形這種高重心的設計,但是手部有部份遮擋到一些腿部,自行移除再修復有失敗的風險,乾脆一開始就用比較明確的語意讓 Banana 移除手部。

info

我知道最後的圖還是重心偏高,但是我也說了:「隨便搓的」=3=

第二次預組裝

提示詞:

這是一個簡易合成的圖片,內容為四足機甲配上螺旋輪 (marsh screw),配上砲塔以及格林機關槍,,
有以下缺陷:
- 機甲、砲塔、機關槍色彩不一致,將砲塔改成與機甲相同的德軍灰。
- 缺乏正確透視
- 機甲-砲塔、砲塔-機關槍,兩個連接處缺乏真實的機械連接

你必須修復上述缺陷。

結果:

使用素材

水道橋重工機甲1

ZiL-29062:

科幻戰車3

格林機關槍4

後記

後來稍微搜尋一下,發現 ZIL-29061 遙控車已經有人做了:

所以除了四足機甲以外,其他部份的可行性算是已經被驗證過了(?)

Footnotes

  1. Suidobashi Heavy Industry – KURATAS – Retrenders. Retrieved 2026-01-25, from https://retrenders.com/2012/07/31/suidobashi-heavy-industry-kuratas/

  2. ZIL Amphibious Screw Vehicles: a Cool Soviet Era Invention - autoevolution. Retrieved 2026-01-25, from https://www.autoevolution.com/news/zil-amphibious-screw-vehicles-a-cool-soviet-era-invention-79250.html

  3. Sci-fi Art: Mountain Urban Tank V07 | Coolvibe – Digital Art. Retrieved 2026-01-25, from https://coolvibe.com/2013/sci-fi-art-mountain-urban-tank-v07/

  4. GAU-8/A 30mm Gatling Gun. Retrieved 2026-01-25, from https://www.gdots.com/armaments/aircraft-guns-gun-systems/gau8a/

Wei Ji

前情提要

嘗試過 LiteLLM 和 Langfuse 的組合,LiteLLM 本身被我遇到錯誤實作的加密造成沒辦法設定 Langfuse Callback 的問題,Langfuse 則是仰賴的 ClickHouse 在缺乏設定的情況下在低端設備運作不太穩定,不只造成 CPU Lock,更在當機之後產生資料庫損毀無法正常運作的情況。

後來使用 Maxim AI 的 Bifrost,確有很高的代理失敗率(~6%),我用 Local Deep Research 跑一次搜尋要 4 次 LLM 請求,失敗一次就會造成整個研究失敗,換句話說有 22% 的機率會研究失敗,這蠻惱人的。

Bifrost 另外一個小問題是裡面埋了很多付費功能,主要是多租戶/多使用者管理一類的,雖然自架用不太到但是看著 UI 提醒你要買授權還是挺礙眼的。

最後決定再換一個 LLM 可觀測方案。

TensorZero

它使用前後端分離的架構,有 gateway 本體就能進行 LLM Gateway 了,如果需要使用可觀測 (LLM Observability) 的部份則必須設定 ClickHouse。

services:
gateway:
image: docker.io/tensorzero/gateway:2026.1.2
command: --config-file /app/config/tensorzero.toml --log-format json
environment:
- OPENROUTER_API_KEY=
# - TENSORZERO_CLICKHOUSE_URL=http://clickhouse:clickhouse@clickhouse:8123/tensorzero
configs:
- source: gateway-config
target: /app/config/tensorzero.toml
ports:
- "3000:3000"
restart: always

ui:
image: docker.io/tensorzero/ui:2026.1.2
environment:
- TENSORZERO_GATEWAY_URL=http://gateway:3000
ports:
- "4000:4000"
restart: always

ClickHouse

TensorZero 依然仰賴 ClickHouse 作為可觀測資料庫,不過既然有了前車之鑒,這次稍微在設定上做點功課。

As an example, when using M-type CPUs, we recommend provisioning 100GB of memory per 25 CPU cores. 1

Fortune 500 B2B SaaS 1

Storage
Monthly new data volume30TB
Total Storage (compressed)540TB
Data retention18 months
Disk per node25TB
CPU
Concurrency200+ concurrent queries
# of replicas (including HA pair)44
vCPU per node62
Total vCPU2700
Memory
Total RAM11TB
RAM per replica256GB
RAM-to-vCPU ratio4 GB:1
RAM-to-disk ratio1:50

For small amounts of data (up to ~200 GB compressed), it is best to use as much memory as the volume of data.2

The recommended amount of RAM is 32 GB or more.2

種種跡象都顯示 ClickHouse 是為了大型業務需求設計的,對於 Homelab 這種小型使用者確實需要額外的設定。

於是我直接從網路上檢了別人的設定來用3

config.xmlusers.xml
<!-- config.xml -->
<!-- These settinsg should allow to run clickhouse in nodes with 4GB/8GB RAM -->
<clickhouse>
<!-- disable some optional components/tables -->
<mysql_port remove="1" />
<postgresql_port remove="1" />
<query_thread_log remove="1" />
<opentelemetry_span_log remove="1" />
<processors_profile_log remove="1" />

<!-- disable mlock, allowing binary pages to be unloaded from RAM, relying on Linux defaults -->
<mlock_executable>false</mlock_executable>

<!-- decrease the cache sizes -->
<mark_cache_size>268435456</mark_cache_size> <!-- 256 MB -->
<index_mark_cache_size>67108864</index_mark_cache_size> <!-- 64 MB -->
<uncompressed_cache_size>16777216</uncompressed_cache_size> <!-- 16 MB -->

<!-- control the concurrency -->
<max_thread_pool_size>2000</max_thread_pool_size>
<max_connections>64</max_connections>
<max_concurrent_queries>8</max_concurrent_queries>
<max_server_memory_usage_to_ram_ratio>0.75</max_server_memory_usage_to_ram_ratio> <!-- 75% of the RAM, leave more for the system -->
<max_server_memory_usage>0</max_server_memory_usage> <!-- We leave the overcommiter to manage available ram for queries-->

<!-- reconfigure the main pool to limit the merges (those can create problems if the insert pressure is high) -->
<background_pool_size>2</background_pool_size>
<background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
<merge_tree>
<merge_max_block_size>1024</merge_max_block_size>
<max_bytes_to_merge_at_max_space_in_pool>1073741824</max_bytes_to_merge_at_max_space_in_pool> <!-- 1 GB max part-->
<number_of_free_entries_in_pool_to_lower_max_size_of_merge>2</number_of_free_entries_in_pool_to_lower_max_size_of_merge>
<number_of_free_entries_in_pool_to_execute_mutation>2</number_of_free_entries_in_pool_to_execute_mutation>
<number_of_free_entries_in_pool_to_execute_optimize_entire_partition>2</number_of_free_entries_in_pool_to_execute_optimize_entire_partition>
<!-- Reduces memory usage during merges in system.metric_log table (enabled by default) by setting min_bytes_for_wide_part and vertical_merge_algorithm_min_bytes_to_activate to 128MB -->
<min_bytes_for_wide_part>134217728</min_bytes_for_wide_part>
<vertical_merge_algorithm_min_bytes_to_activate>134217728</vertical_merge_algorithm_min_bytes_to_activate>
</merge_tree>

<!-- shrink all pools to minimum-->
<background_buffer_flush_schedule_pool_size>1</background_buffer_flush_schedule_pool_size>
<background_merges_mutations_scheduling_policy>round_robin</background_merges_mutations_scheduling_policy>
<background_move_pool_size>1</background_move_pool_size>
<background_fetches_pool_size>1</background_fetches_pool_size>
<background_common_pool_size>2</background_common_pool_size>
<background_schedule_pool_size>8</background_schedule_pool_size>
<background_message_broker_schedule_pool_size>1</background_message_broker_schedule_pool_size>
<background_distributed_schedule_pool_size>1</background_distributed_schedule_pool_size>
<tables_loader_foreground_pool_size>0</tables_loader_foreground_pool_size>
<tables_loader_background_pool_size>0</tables_loader_background_pool_size>
</clickhouse>
<!-- users.xml -->
<clickhouse>
<profiles>
<default>
<max_threads>2</max_threads>
<max_block_size>8192</max_block_size>
<queue_max_wait_ms>1000</queue_max_wait_ms>
<max_execution_time>600</max_execution_time>
<input_format_parallel_parsing>0</input_format_parallel_parsing>
<output_format_parallel_formatting>0</output_format_parallel_formatting>
<max_bytes_before_external_group_by>3221225472</max_bytes_before_external_group_by> <!-- 3 GB -->
<max_bytes_before_external_sort>3221225472</max_bytes_before_external_sort> <!-- 3 GB -->
</default>
</profiles>
</clickhouse>

不過 max_concurrent_queries 設成 8 在我的用例有點太少了,TensorZero 那邊會噴錯誤,所以我稍微調高到 100。(預設值是 0,沒有限制)

基本使用

TensorZero 比較討厭的是它的官方文件會一直找機會推銷自己設計的 API:

curl -X POST "http://localhost:3000/inference" \
-H "Content-Type: application/json" \
-d '{
"function_name": "generate_haiku",
"input": {
"messages": [
{
"role": "user",
"content": "Write a haiku about TensorZero."
}
]
}
}'

不然就是使用自己特殊的 function_name 設計(等等會解釋這個東西):

import OpenAI from "openai";

const client = new OpenAI({
baseURL: "http://localhost:3000/openai/v1",
});

const response = await client.chat.completions.create({
model: "tensorzero::function_name::generate_haiku",
messages: [
{
role: "user",
content: "Write a haiku about TensorZero.",
},
],
});

console.log(JSON.stringify(response, null, 2));

但是 TensorZero 本身是 OpenAI API 兼容的:

const url = "http://awesome-tensorzero-service:8123/openai/v1/chat/completions"
const headers = {
"Authorization": `Bearer ANY`,
"Content-Type": "application/json"
}
const payload = {
"model": "tensorzero::model_name::openrouter::openai/gpt-oss-20b",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Hi" },
],
"modalities": ["text"],
}

const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(payload),
});

只要在環境變數針對特定的供應商 (Provider) 設定好 API key (例如:OPENROUTER_API_KEY),使用以下格式就可以直接調用對應的模型:

tensorzero::model_name::<PROVIDER>::<MODEL_NAME>

PROVIDER 是 TensorZero 有實作支援的部份,具體清單請見官方文件4

缺乏 Reasoning

使用一陣子之後第一個觀察到的問題是缺少 Reasoning,不論是可觀測紀錄本身,或是 client 的 request 有帶上 Reasoning 的參數,都沒辦法獲得。

發現問題的當下,剛好被開發人員標記沒多久:

Add reasoning support to OpenRouter model provider · Issue #5703 · tensorzero/tensorzero

幾天後撰寫本文的時候(2026-01-22),再次查看似乎已經被解決了。

進階使用

info

除了基本使用,其他功能我並沒有親自實測(使用),單純是根據我閱讀文件的理解整理的。

info

以下皆以 OpenAI API 舉例,畢竟已經是實質產業標準了。TensorZero 自己的 API 格式我完全沒興趣研究。

第一種是剛剛提過得,使用內建的 Provider 直接呼叫模型:

自定義模型

第二種是在組態檔裡配置自定義的模型:

一個自訂模型可以有多個 Provider/Routing,用來當作備援 (Fallbacks),當一個 Provider 失敗會自動使用其他設定的 Routing,例如:

[models.my_gpt_5]
routing = ["my_openai_provider", "my_azure_provider"]

[models.my_gpt_5.providers.my_openai_provider]
type = "openai"
model_name = "gpt-5"

[models.my_gpt_5.providers.my_azure_provider]
type = "azure"
deployment_id = "gpt-5"
endpoint = "https://your-resource.openai.azure.com"

自定義函數

第三種是在組態檔裡配置自定義的函數:

可以使用標準的 Provider 和 Model;或是前一種自定義模型。另外除了備援之外,函數可以用來建立實驗 (Experimentation) 來進行 A/B 測試。

Footnotes

  1. Sizing and hardware recommendations | ClickHouse Docs. Retrieved 2026-01-22, from https://clickhouse.com/docs/guides/sizing-and-hardware-recommendations 2

  2. OSS usage recommendations | ClickHouse Docs. Retrieved 2026-01-22, from https://clickhouse.com/docs/operations/tips 2

  3. Configure ClickHouse® for low memory environments | Altinity® Knowledge Base for ClickHouse®. Retrieved 2026-01-22, from https://kb.altinity.com/altinity-kb-setup-and-maintenance/configure_clickhouse_for_low_mem_envs/

  4. Overview - TensorZero Docs. Retrieved 2026-01-22, from https://www.tensorzero.com/docs/integrations/model-providers