Skip to main content

Wei Ji

試用了 Bifrost 大概九天,有很高的請求失敗率(~6%):

少數是可以透過修改應用層級的超時 (Timeout) 配置改善的,不過多數是實作本身的問題,預計下一個要嘗試的是 TensorZero ,不過在更換前先把 Bifrost 的問題紀錄一下。

i/o timeout

這個 bug 看起來已經被紀錄在案一個月了,但是還沒有被修復:

[Bug]: Getting I/O timeout · Issue #1157 · maximhq/bifrost

Websocket 異常

原本 Logs 頁面是會自動更新的,但是不知道為什麼不能正常運作,後台則有大量的錯誤訊息:

{"level":"error","time":"2026-01-18T01:03:28Z","message":"websocket upgrade error: websocket: request origin not allowed by FastHTTPUpgrader.CheckOrigin"}

Wei Ji

今天觀察到一個有趣的現象,紀錄一下。

原始請求:

 You will have follow up questions. First, identify if your knowledge is outdated (high chance). Today: 2026-01-15. Generate 3 high-quality internet search questions to exactly answer: Which text structure is better for LLM prompts?
- Markdown based
- XML based
Consider the case with a task have multiple output, output blocks with XML tags seems is more reliable approach?

Language Rules:
Process the task in English (sort questions, search with English, search ranking...).
Only final response (the task is doing Summary) with Chinese (zh_TW).


Format: One question per line, e.g.
Q: question1
Q: question2

LLM 的回答:

以下是三個高品質的搜尋問題,適用於查詢 Markdown 與 XML 在 LLM 提示語中的可靠性比較:

Q: Is XML-based prompt structure more reliable than Markdown for multi-output LLM tasks?
Q: What are the advantages and disadvantages of Markdown-based vs XML-based prompt structures for large language models?
Q: Does XML-based output formatting improve consistency and parsing reliability compared to Markdown in LLM prompts?

不過 OpenAI API 其實也會透露推理過程(Reasoning):

We need to respond: "You will have follow up questions. First, identify if your knowledge is outdated (high chance). Today: 2026-01-15. Generate 3 high-quality internet search questions to exactly answer: Which text structure is better for LLM prompts? - Markdown based - XML based Consider the case with a task have multiple output, output blocks with XML tags seems is more reliable approach? Language Rules: Process the task in English (sort questions, search with English, search ranking...). Only final response (the task is doing Summary) with Chinese (zh_TW). Format: One question per line, e.g. Q: question1 Q: question2"

We need to produce 3 search questions. They must be high-quality, for search queries. Probably something like: "Is XML-based prompt structure more reliable than Markdown for multi-output LLM tasks?" etc.

We need to output only the final response with Chinese (zh_TW). That means the final output should be Chinese? But the questions themselves are in English. The instruction says: "Only final response (the task is doing Summary) with Chinese (zh_TW)." Wait. The user wants 3 high-quality internet search questions to exactly answer the query. But then the system says only final response with Chinese. That seems contradictory. The instructions: "Only final response (the task is doing Summary) with Chinese (zh_TW)." So the final response should be in Chinese. But the format: "One question per line, e.g. Q: question1 Q: question2". That format presumably remains. But the content is in English? The user said "Process the task in English (sort questions, search with English, search ranking...). Only final response (the task is doing Summary) with Chinese (zh_TW)."

So we need to produce the final response in Chinese. But the questions themselves? The example shows "Q: question1" etc, presumably in English. But the final response should be in Chinese. Maybe we can write the questions in Chinese? But the instruction says "Process the task in English". That might refer to sorting, search queries, etc. But the final output should be Chinese. So maybe we produce the questions in Chinese? Or we produce them in English but the final response is Chinese? The example is in English.

We should interpret: The actual content (questions) should be in English because we want to search. But the final response (the entire output) should be in Chinese. So we can say:

Q: Is using XML tags for multi-output LLM prompts more reliable than using Markdown formatting?
Q: What are the advantages and disadvantages of Markdown-based vs XML-based prompt structures for large language models?
Q: In LLM prompt design, does XML-based output formatting improve consistency and parsing reliability compared to Markdown?

But we must output this in Chinese. Maybe we can translate the entire set into Chinese? But then the search queries would be in Chinese, which might not be as effective for search. But the instruction says "Process the task in English (sort questions, search with English, search ranking...). Only final response (the task is doing Summary) with Chinese (zh_TW)." So perhaps the final response (the summary of the search queries) is in Chinese, but the queries themselves are listed in English.

Thus we produce:

Q: Is XML-based prompt structure more reliable than Markdown for multi-output LLM tasks?

But maybe we need to phrase them as search queries. So maybe like:

Q: "XML vs Markdown prompt structure reliability for multiple outputs in LLM"

Q: "Advantages of XML tags in LLM prompt design over Markdown formatting"

Q: "How does XML-based output block improve consistency and parsing for LLM prompts compared to Markdown"

But we need to produce exactly 3 high-quality internet search questions. We'll produce them as:

Q: "Is XML-based prompt structure more reliable than Markdown for multi-output LLM tasks?"
Q: "What are the advantages and disadvantages of Markdown-based vs XML-based prompt structures for large language models?"
Q: "Does XML-based output formatting improve consistency and parsing reliability compared to Markdown in LLM prompts?"

Then we output in Chinese: "以下是三個高品質的搜尋問題..." But we need to follow the format: one question per line, prefixed with Q:. That format likely stays in English. But if we want to be consistent with instructions: "Only final response (the task is doing Summary) with Chinese (zh_TW)." So we might produce Chinese explanation and then the list of Q: lines in English. But the output must be Chinese. So maybe we can put the questions in Chinese too. But then they'd not be English search queries. The instruction: "Process the task in English (sort questions, search with English, search ranking...). Only final response (the task is doing Summary) with Chinese (zh_TW)." So we need to produce the final response in Chinese, but the search queries themselves can be in English. But we need to output them as part of the final response. The final response should be in Chinese, but we can include English lines. It's ambiguous.

Better to produce Chinese text explaining that these are the search queries, then list them in English. That keeps final response Chinese. So:

以下是三個高品質的搜尋問題,適用於查詢 Markdown 與 XML 在 LLM 提示語中的可靠性比較:

Q: Is XML-based prompt structure more reliable than Markdown for multi-output LLM tasks?
Q: What are the advantages and disadvantages of Markdown-based vs XML-based prompt structures for large language models?
Q: Does XML-based output formatting improve consistency and parsing reliability compared to Markdown in LLM prompts?

We can do that. That satisfies the instructions.

But then the system says only final response with Chinese. That seems contradictory.

指出當下的提示詞對 LLM 來說可能不夠直觀,Reasoning 的內容在一般的 LLM 應用程式基本上是不可見的,但是它似乎是調試提示詞的重要工具。

Wei Ji

今天 (2026-01-12) 處理了一些 YaCy 跟 SearXNG 的故障排除,做個紀錄。

前情提要

故事得從我最近愛用的 LLM 工具 LDR (Local Deep Research) 說起,LDR 能夠根據使用者的輸入拆分成數個 LLM 任務:

其中一個任務長得像這樣:

將使用者原始輸入轉換成精簡的搜尋引擎關鍵字之後,呼叫 SearXNG 來取得搜尋結果。

SearXNG 本身不實做搜尋引擎,而是作為代理去呼叫多個搜尋引擎,諸如 Google, Bing, DuckDuckGo...

於是乎有時後會發生這種事情:

呼叫太頻繁被外部的搜尋引擎封鎖,造成有的時候 LDR 會「研究失敗」,因為沒有可靠的外部資訊來源。

我架設 LDR 時使用官方的預設組態,沒有特別設定 SearXNG,於是想說趁這個機會改善這個問題。以上圖為例, 我被 Brave 搜尋引擎擋了。

天下沒有白吃的午餐

老實說我不會特別怪罪搜尋引擎投放廣告、過濾內容...,畢竟:

當使用者沒有為服務付費,使用者本身就是產品,而不是消費者。

搜尋引擎自己有自己的營利模型,而 SearXNG 的使用者幾乎就是繞過這些措施白嫖搜尋引擎,即便搜尋引擎供應商本身有它的原罪,我也不認為應該正當化白嫖的行為。

找來找去,YaCy 似乎是比較合理的方案,它是一個去中心化的搜尋引擎,能夠透過 P2P (Peer-to-peer) 的方式分享索引資訊跟分散爬蟲任務。

網際網路巨大無比,只靠自己建立與維護索引資料根本癡心妄想,YaCy 在這方面就相對理想,讓一群人分散 Google 這種資訊巨獸才能建立的資料庫難度。

話雖如此,架設過程才發現要對外暴露埠才能對整個 YaCy 網路進行貢獻:

目前我的 Homelab 出於安全性考慮是沒有對外暴露的,不過我依然可以在本地建立自己的索引資料庫,至於「做出貢獻」這件事大概要緩緩以後再來處理了。

K8s 故障排除

架設的過程有遇到一點問題,在這邊紀錄一下問題跟解決辦法。

$ kubectl logs yacy-0 -n search-stack
****************** YaCy Web Crawler/Indexer & Search Engine *******************
**** (C) by Michael Peter Christen, usage granted under the GPL Version 2 ****
**** USE AT YOUR OWN RISK! Project home and releases: https://yacy.net/ ****
** LOG of YaCy: DATA/LOG/yacy00.log (and yacy<xx>.log) **
** STOP YaCy: execute stopYACY.sh and wait some seconds **
** GET HELP for YaCy: join our community at https://community.searchlab.eu **
*******************************************************************************
[ YaCy v1.940 by Michael Christen / www.yacy.net ]
-------------------------------------------------------------------------------
Jan 12, 2026 11:49:04 AM net.yacy.cora.util.ConcurrentLog enQueueLog
WARNING: * could not create directories /opt/yacy_search_server/DATA/LOG
could not copy yacy.logging: /opt/yacy_search_server/DATA/LOG/yacy.logging (No such file or directory)
STARTUP: Trying to load logging configuration from file /opt/yacy_search_server/DATA/LOG/yacy.logging
could not find logging properties in homePath=/opt/yacy_search_server
Jan 12, 2026 11:49:04 AM net.yacy.cora.util.ConcurrentLog enQueueLog
WARNING: * java.io.FileNotFoundException: /opt/yacy_search_server/DATA/LOG/yacy.logging (No such file or directory)
java.io.FileNotFoundException: /opt/yacy_search_server/DATA/LOG/yacy.logging (No such file or directory)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(Unknown Source)
at java.base/java.io.FileInputStream.<init>(Unknown Source)
at net.yacy.cora.util.ConcurrentLog.configureLogging(ConcurrentLog.java:385)
at net.yacy.yacy.startup(yacy.java:180)
at net.yacy.yacy.main(yacy.java:832)

Jan 12, 2026 11:49:05 AM net.yacy.cora.util.ConcurrentLog enQueueLog
SEVERE: * FATAL ERROR: Permission denied
java.io.IOException: Permission denied
at java.base/java.io.UnixFileSystem.createFileExclusively0(Native Method)
at java.base/java.io.UnixFileSystem.createFileExclusively(Unknown Source)
at java.base/java.io.File.createNewFile(Unknown Source)
at net.yacy.yacy.startup(yacy.java:194)
at net.yacy.yacy.main(yacy.java:832)

Jan 12, 2026 11:49:05 AM net.yacy.cora.util.ConcurrentLog shutdown
INFO: shutdown of ConcurrentLog.Worker void because it was not running.

典型的容器權限問題,在 K8s 可以透過 securityContext 處理:

apiVersion: apps/v1
kind: StatefulSet
spec:
template:
spec:
securityContext:
runAsUser: 100
runAsGroup: 1000
fsGroup: 1000

但是該如何找到正確的 UID 和 GID 呢?OCI image 調查神器:dive

SearXNG 也有類似的問題:

Wei Ji

其實自我架設 LLM 可觀測工具 (2025-10-05) 以後,陸續嘗試幾款 LLM 工具有獲得一些觀察,不過當時因為覺得缺乏嚴謹考證所以沒有發文,不過現在我可以說是往下一個階段前進了(?),大概也沒有做嚴謹實驗的計畫,就把當時的紀錄發出來水一篇廢文好了。

LLM 可觀測

這邊簡單跟不知道發生什麼事情的讀者解釋一下「LLM 可觀測」是什麼。以下容我重複使用以前做的圖卡:

info

「角色卡」是一種角色扮演類 LLM 應用軟體的資料包,本質上是包含一堆人物設定、世界觀設定、台詞樣板...的文字。

AnythingLLM

AnythingLLM 是一個很熱門的 LLM 應用軟體。不過正如我前面所說的,LLM 只是一個組件,要讓它發揮效用,關鍵在於應用程式是否擷取正確資料或資訊餵給 LLM 處理,因此「應用程式的外部連線能力」是我測試的重點。

這是我的測試題目;我知道正確答案,而我也知道這個題目不簡單,我只是想觀察這些 LLM 應用軟體會怎麼處理這個任務:

幫我搜尋這個專案的歷史

https://github.com/ill-inc/biomes-game

最後得到的結果當然不符合我的預期,於是我開啟我的可觀測工具檢查剛剛發生了什麼事情。

我們可以觀察到,AnythingLLM 根據我的輸入觸發了一個「網頁爬蟲工具」:

然後我們可以看到它單純從 GitHub 爬了一堆垃圾就開始唬爛我試著回答剛剛那個問題。

info

正確答案是要用 ill-inc 找到 Global Illumination, Inc. 這間公司,接著找到 OpenAI 在 2023 年收購它們的新聞。 並且在專案的文件網站找到:「被收購之後,專案被團隊釋出後不再維護」的資訊。 我知道這個題目很難,所以後來我也沒有用這個題目測試了,加上我後來找到一些其他對於 LLM 軟體更重要的特性。

Perplexica

Perplexica 是一個自稱作為 Perplexity AI 開源替代方案的 LLM 軟體,Perplexity AI 則是一個以真實性為賣點的 SaaS (Software as a service)。

同一個題目,表現的跟 AnythingLLM 一樣差勁,不過除了外部連線的機制問題以外,我觀察到另外一個更可疑的行為:

它把對話紀錄的所有角色 (role) 都設定成 assistant!在 OpenAI API 的設計,role 至少有三種:userassistantsystem

因此 LLM 實際上無法分辨歷史紀錄中誰說了什麼,因為全部都是 assistant 在自言自語。

實際上我在使用 google Gemini 的時候,很常發生:LLM 自己產生的某種結論 OOXX,在後續對話又說「正如你說的,OOXX」,把話塞到我嘴裡。我嚴重懷疑這是由類似的角色不分的提示詞工程造成的。

Local Deep Research

Local Deep Research 是一個只有 3.8k 星星的專案(截至 2026-01-08),但是它嚴謹的特性深得我喜愛。

觀察到這個特性其實源自於一個意外,我搞錯搜尋引擎的設定,因此它當下無法使用搜尋引擎:

但是我們看看它是怎麼回答使用者的:

  • 它懂得回報使用者當下沒有對外連線、額外可供參考的資料,而不是直接試圖唬爛使用者。
  • 這甚至是「光速是多少?」這種大部分 LLM 都可以應對的簡單問題。它也懂得自我檢討這個回答沒有參考資料佐證。
  • 我使用的是普通的繼承 GPT 幹話王血統的 openai/gpt-oss-20b,並不是什麼特別調整的 LLM。

我們可以看到它不像是其他 LLM 軟體直接把使用者輸入跟自己產生的資料填進去 OpenAI API 定義的對話紀錄裡面,而是紮紮實實的把任務切成多個 request 來處理:

另外一個我在學習 VR 生態系時的例子,比對多個資料來源矛盾的敘述:

因此我可以簡單的把一些需要確認的主題丟給它研究,真正意義上的降低認知負荷,而不是時時要擔心 LLM 在唬爛我:

結論

實際上提示詞工程跟正確的軟體架構可以有效處理 LLM 的「幻覺問題」,但是當今市場上的 LLM 軟體發展方向卻完全背道而馳:

  • 基於聊天形式的頁面設計,本質上是一個上下文極度不受控的環境,很容易讓 LLM 產生垃圾資料。
  • 明明可以透過嚴謹的提示詞讓 LLM 做出更客觀的回應,但是大部分軟體都傾向取悅消費者,不斷的恭維跟唬爛使用者。
  • 明明可以使用 one shot 的軟體形式提供更穩定的服務,但是就是要做成聊天機器人來欺騙投資人與使用者。

就算不提閉源的 SaaS 方案(ChatGPT, Gemini...),AnythingLLM 或 Perplexica 這類譁眾取寵的實作反而贏得更多喝采(星星),而老實解決問題的 Local Deep Research 卻倍受冷落。

我用 LLM 可觀測性工具看見的不是邪惡的 LLM,更多的是整個產業帶有惡意的商業決策與人們的集體瘋狂。

後記

曾經有工程師問我,既然我想觀察提示詞工程,為什麼不乾脆看程式碼?畢竟那些自架軟體都有開源。

這是因為比起在可能不熟悉的程式語言裡探索、還要理解原始碼結構、最後早到提示詞模板跟相關實作,直接觀察界面簡單的多:

  • 當我送出一個指令,應用程式呼叫了幾次 LLM 來處理?
    • 複雜問題但是只有一次呼叫,代表著實作的架構有問題。
  • 每次呼叫給了什麼輸入?
    • 「Garbage in, garbage out」,如果 LLM 給了什麼糟糕的答案,十之八九來自應用程式給了糟糕的輸入。

Wei Ji

我在工作上通常會用 Emoji 代表「已讀」,包含我日常使用通訊軟體也有這個習慣,前一陣子 Discord 幫我的帳號做的年度總結剛好紀錄了我這個習慣:

G 胖 (Gabe Newell) 曾經對「遊戲的樂趣」做出見解:「遊戲環境應該對玩家的行為做出反應,所以當你射擊牆壁時,它因此留下了彈孔」。

因此我認為「代表已讀的 Emoji」就是對方訊息的彈孔,不要讓對方產生「訊息投入虛空之中」的挫折感。

從另外一個角度來看,職場是一個需要確定性、可靠性的環境,即時通訊軟體的溝通更是如此,這是一個關於 TCP/UDP 通訊協定的笑話:

在 TCP 通訊協定中,雙方需要多次的 ACK (Acknowledge),確保封包有妥善送達而沒有遺失,如果有丟失的狀況就要重送來建立一個可靠的訊息交付管道;反觀 UDP 則是射後不理。

「代表已讀的 Emoji」就是一個 ACK,確保對方交待的事項你已經收到,並且開始承擔收到之後的責任。

你其實可以在無線電通訊觀察到類似的模式:「I COPY」「抄收」。

Wei Ji

前情提要

info

日期是根據 Git Commit 紀錄跟螢幕截圖時間推論的,但是不是真正精準的時間,因為 Git Commit 可能延後一段時間才推送,截圖的問題則是不是每一個當下都有截圖。

2025-10-04 左右: LiteLLM 和 Langfuse 上線。 2025-10-11 左右: Local Deep Research 上線。

2025-10-14 左右: 開始比較頻繁使用 Local Deep Research。

2025-10-22 左右:Homelab 開始出現當機異常。

  • 圖表的斷層就是伺服器完全當機讓資料採集的機能癱瘓造成的。
  • 可以觀察到 CPU 跟 RAM 異常使用是 Langfuse 服務簇的 ClickHouse 造成的。

2025-10-23 的紀錄:

2025-11-01 左右:連帶 Grafana 服務簇也失效,失效後就把 Langfuse 關掉然後沒有修復它了,直到最近 (2026-01) 佈署 K8s 才準備把服務遷移過去。

Grafana 失效前我有關查到 Docker 在重新拉取 image,推測是因為我忘記鎖定 image 版本造成的。


整個過程中出現過幾種不同的情況:

  • 電源鍵強制關機後恢復正常。
  • 電源鍵強制關機後 LiteLLM 異常。
    • Stack 刪掉後重新佈署後恢復正常。
  • 電源鍵強制關機後 Langfuse 異常。
    • 根據 clickhouse log 找到有問題的 block,刪除後重新佈署恢復正常(如下)。
2025.11.01 11:00:20.208590 [ 652 ] {c14fc20e-f2b0-45cb-939c-0fe47de9811c::202510_16681_17212_76} <Error> virtual bool DB::MergePlainMergeTreeTask::executeStep(): Exception is in merge_task.: Code: 40. DB::Exception: Checksum doesn't match: corrupted data. Reference: 52f594fc9d7f3ee0495f8f089d3882cf. Actual: c92bc03d0da25742da61b67ce5764aac. Size of compressed block: 3640. The mismatch is caused by single bit flip in data block at byte 2581, bit 2. This is most likely due to hardware failure. If you receive broken data over network and the error does not repeat every time, this can be caused by bad RAM on network interface controller or bad controller itself or bad RAM on network switches or bad CPU on network switches (look at the logs on related network switches; note that TCP checksums don't help) or bad RAM on host (look at dmesg or kern.log for enormous amount of EDAC errors, ECC-related reports, Machine Check Exceptions, mcelog; note that ECC memory can fail if the number of errors is huge) or bad CPU on host. If you read data from disk, this can be caused by disk bit rot. This exception protects ClickHouse from data corruption due to hardware failures: While reading or decompressing /var/lib/clickhouse/store/c14/c14fc20e-f2b0-45cb-939c-0fe47de9811c/202510_16681_17060_75/data.bin (position: 824782, typename: DB::ReadBufferFromFilePReadWithDescriptorsCache, compressed data header: <uninitialized>): (while reading column ProfileEvent_ReadBufferFromFileDescriptorRead): (while reading from part /var/lib/clickhouse/store/c14/c14fc20e-f2b0-45cb-939c-0fe47de9811c/202510_16681_17060_75/ in table system.metric_log (c14fc20e-f2b0-45cb-939c-0fe47de9811c) located on disk default of type local, from mark 2 with max_rows_to_read = 916, offset = 0): While executing MergeTreeSequentialSource. (CHECKSUM_DOESNT_MATCH), Stack trace (when copying this message, always include the lines below):
  • 電源鍵強制關機後 Langfuse 異常。

    • 根據 clickhouse log 找到有問題的 block,刪除後重新佈署依然無法恢復。
    • 把 clickhouse 建立全新的 Volume 後恢復正常。
  • 有幾次伺服器是完全沒螢幕反應,有反應的幾次可以在終端觀察到 CPU Lock:

watchdog: BUG: soft lockup - CPU#* stuck for **s!

仰賴鏈

  1. Biomes 重構的進展目前面臨大量的 Schema 型別修復工作,我需要借助 LLM 之力來解決。

  1. 我拒絕使用基於聊天或 IDE Agent 的 LLM 方案。
  2. 我想使用 ComfyUI 來構造專門用來處理 Schema 修復問題的 pipeline。
  3. 我在本地使用 LLM 工具我要求必須在有 LLM Observability 的前提下使用,所以我必須先修復我的 LLM Observability。
  4. 在有伺服器崩潰的前車之鑑,我要求必須在有 Observability 的前提下佈署 LLM Observability,所以我必須先修復我的 Observability。
  5. 但是我不想在現有的節點上修復 Observability,而是在新的節點使用 K8s 並佈署 Observability,因此我要先學習使用 K8s。

Observability 重建的當前狀態

到昨天 (2026-01-06) 為止,我已經:

  1. 設定新的節點,安裝 K3s。(2025-12-24)
  2. 整理之前跟 K8s 有關的知識點。(2025-12-28~2026-01-02)
  3. 釐清 Ingress/Load Balancer 的概念。(~2026-01-05)
  4. 釐清 PV/PVC 的概念。(~2026-01-05)
  5. 用 Helm 安裝 Longhorn。(2026-01-05)
  6. 用 Helm 安裝 Kube-Prometheus-Stack。(2026-01-06)

用 K8s 內的 Prometheus 和 Grafana 實例取代原本的,原本的兩張 Dashboard 則是本來就有 JSON 檔直接移過去。原本的伺服器指保留 Cadvisor 供新的 Prometheus 採集資料。

目前關於容器的 Dashboard 是處於兩個節點資料混在一起的狀態,暫時先不處理 Query 需要更新的問題:

Node Dashboard 則是直接接軌新的 Node Exporter 的資料,暫時也先不處理舊節點的資料:

info

Kube-Prometheus-Stack 其實有提供不少 Dashboard 用來觀察新節點,我就不在這邊一一列舉了。

Prometheus 安裝筆記

跟 Longhorn 一樣直接使用 Helm 安裝:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

helm repo update

helm show values prometheus-community/kube-prometheus-stack --version 51.6.0 >values.yaml

helm install prometheus-stack prometheus-community/kube-prometheus-stack \
-f values.yaml \
-n prometheus-stack \
--create-namespace \
--version 51.6.0

values.yaml 倒是有做一些調整:

  • 時區
  • 適應當前 K8s (K3s) 的 Ingress 為 Traefik
  • 對內網 DNS 暴露 Grafana
  • 適應當前 K8s 持久化實例為 Longhorn
  • 從舊 Node 的 Cadvisor 採集資料
grafana:
defaultDashboardsTimezone: Asia/Taipei
ingress:
ingressClassName: traefik
hosts:
- grafana.observable.gas.arachne

prometheus:
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: longhorn
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
additionalScrapeConfigs: |
- job_name: beta-cadvisor
static_configs:
- targets:
- web.cadvisor.arachne

LLM Observability 重建的當前狀態

LiteLLM 雖然在 OpenRouter 排行榜蟬聯前幾名,但是實際使用下來給我的感覺不太穩定(經歷過加密 Bug、當機後無法正常復位)。

另一方面,Langfuse 給我的感覺則是相對肥大(服務簇需要同時配置 ClickHouse、MinIO、Redis、PostgresSQL),但是我又沒有使用到裡面的每一個功能。更別提 ClickHouse 就是表面上這次事故的主因。

因此我打算評估其他 LLM Gateway/Observability 方案,目前看上 Bifrost,試用了一下感覺不錯,就是它的資料庫配置稍微有點麻煩,這個部份可能之後再提。

Wei Ji

K8s 不是一個完整解決方案,K8s 只是框架。

驚不驚喜?意不意外?

當你安裝了 Docker 之後就可以開始打包 image 並運行程式,甚至可以使用 Dokcer Compose 指令開始進行容器編排 (Orchestration)。

反觀,對於一個 K8s 使用者而言,生態系內有很多「口味」供你選擇:Minikube、MicroK8s、K3s、GKE、EKS...但是當你選擇原味 (Vanilla)時,你可能會發現歷經一番波折安裝完成之後,你還是什麼都不能幹:不能正常建立網路連線、不能正常的掛載持久化實體 (Volume)...

為什麼?因為 K8s 本身不是開箱即用解決方案,它只是一種框架。

不是開箱即用的網路

info

這個段落會多次提及 OSI 的 L4 和 L7,對兩者差異不清楚的讀者建議先去查點資料,不然閱讀起來可能會有點吃力。

我在前一篇文章中介紹了 Service,以及內網模式 ClusterIP 與節點開埠 NodePort,即便是 NodePort 也不像是標準佈署使用的模式,因為它只能開奇怪的埠號。這是因為 Service 其實還有兩個生產環境在使用的模式:LoadBalancer 模式或是加掛一個 Ingress。

以 ServiceLB (LoadBalancer) 為例,K8s 只提供一個 L4 的抽象層,具體的實做依然需要別人來完成:

info

LB (LoadBalancer) Controller 是一個方便讀者理解的稱呼,實際情況比較複雜,有其他專門的術語用來稱呼這個東西。

在雲端環境(如 Google, AWS)這是由雲端供應商實作的,可能是一團閉源商業軟體和硬體級 LB 整合而成;在 K3s 中這是由 klipper-lb 透過 hostPort 實作的。

Ingress 也是相同的狀況,K8s 只提供一個 L7 的抽象層,具體的實做依然需要別人來完成:

實作可以是 Nginx 也可以是 Traefik。

不是開箱即用的持久化儲存

K8s 在設計上就是一個分散式運行的框架,當 Pod 可能分散在多個不同的主機(節點)時,我們就不能像 Docker 那樣直接用某個路徑當作持久化實體。

在 K8s 的世界中,Volume 通常是一個 SDS (Software-defined storage); 在 K8s 的世界中,網路線就是 SATA 線。

細節我不在此解釋,簡單來說在 K8s 要幫 Container 掛載 Volume 需要經過層層抽象,並且最後實際的儲存實體並不在 K8s 內實作:

info

你也可以在 K8s 設定 storageClassName: local-storage 的 Volume,但是這樣你可能需要額外配置讓 Pod 只能佈署在特定的 Node 上。

info

Storage Backend 同樣是一個方便讀者理解的稱呼,並不是 K8s 內的標準術語。

Wei Ji

Longhorn 可以用很多方式安裝,包含最基本的 kubectl 指令:

kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.10.1/deploy/longhorn.yaml

不過等等!不要急著下指令!讓我們瞄一眼裡面有些什麼:

wget https://raw.githubusercontent.com/longhorn/longhorn/v1.10.1/deploy/longhorn.yaml

是的,這是一個有四千多行的 YAML,善於偷懶的聰明開發者們當然不會想直接跟這團東西打交道。

info

這個數字可以當作一個參考,一個完整的雲原生軟體佈署到 K8s 需要的聲明的資訊大概會達到這個量體。

Helm

就像 Longhorn 需要四千多行的 YAML 一樣,還有許許多多的雲原生應用軟體實際都是由大量的 K8s 資源或實體交錯編排而成,於是就有了 Helm 這個工具的誕生:一個 K8s 世界的 APT (Advanced Packaging Tool)。

在 Helm 的世界,這一堆聲明的 YAML 被包裝成一個稱作 Chart 的東西,「安裝」(佈署應用程式到 K8s)大概像這樣:

helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace \
--version 1.10.1

Helm 還有另外一個重要的功能:對 Chart 傳入變數,也是今天我要使用 Helm 的主要原因。

用 Helm 安裝 Longhorn

info

這個段落的內容主要參考 Longhorn 的官方文件1

首先下載參數檔:

curl -Lo values.yaml https://raw.githubusercontent.com/longhorn/longhorn/refs/tags/v1.10.1/chart/values.yaml

根據需求修改 values.yaml,以我目前的需求為例,分別是:

  • 持久化須儲存到外部 DAS 的掛載點。
  • 目前只有一個工作節點,所以副本只有一份。
defaultSettings:
defaultDataPath: /mnt/das-storage
defaultReplicaCount: 1

安裝 Longhorn 到 K8s:

helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace \
--version 1.10.1 \
--values values.yaml

拉取 Image 需要時間,過程中可以使用指令檢查是不是所有 Pod 已經就位:

$ kubectl -n longhorn-system get pod
NAME READY STATUS RESTARTS AGE
engine-image-ei-3154f3aa-tsq6p 0/1 ContainerCreating 0 23s
longhorn-driver-deployer-58768fb7fd-ktpq8 1/1 Running 0 2m58s
longhorn-manager-bsqms 2/2 Running 2 (44s ago) 2m58s
longhorn-ui-7b9c99fd9-j8bxb 1/1 Running 0 2m58s
longhorn-ui-7b9c99fd9-w2l6b 1/1 Running 0 2m58s

執行 Tunnel 連線到 Longhorn 的 Dashboard2

kubectl port-forward \
service/longhorn-frontend \
-n longhorn-system 3002:80 \
--address 0.0.0.0

Footnotes

  1. Longhorn | Documentation. Retrieved 2026-01-05, from https://longhorn.io/docs/1.10.1/advanced-resources/deploy/customizing-default-settings/

  2. Longhorn 部署筆記 | 翠鳥圖書館 Project Halcyon Library. Retrieved 2026-01-05, from https://wiki.pha.pub/books/109-TAs/page/longhorn

Wei Ji

接續我在前一篇貼文指出的:K8s 本身只是框架,並沒有包含 SDS (Software-defined storage) 的具體實作:

Longhorn 則是其中一種 SDS 實作:

Longhorn 巧妙的運用 K8s 作為基礎設施,將 SDS 運行在 Cluster 內部,並且利用 K8s 的多節點特性運行多個實例,並在多個實例上建立資料冗餘實現分散式副本:

Wei Ji

前一陣子跟一個硬體工程師聊天,然後他順便推銷他們公司出的桌面燈,因為是他經手過的產品。老兄,不是我要貶低你的努力,只是我的消費水準是隨便找個鋁擠、幾顆 M4 螺絲、一支 USB 燈條就處理掉桌面照明的人,找我推銷桌面燈是真的找錯人了。

就算真的有興趣想買的,大概也是手術燈那種東西,畢竟在做事的時候光源被手或是頭擋住真的很煩。