| 元件 | 版本 |
|---|---|
| Kubernetes | v1.35 |
| Gateway API | v1.5(Experimental channel) |
| Envoy Gateway | v1.8.0 |
| 測試後端 | traefik/whoami:v1.10 × 4 |
沿用 Day 5 打通的 platform-gateway,本篇只新增 HTTPRoute 與後端。
Day 5 那條路由只有一條規則、一個 PathPrefix: /,所有請求都往同一個後端送。真實服務不是這樣——同一個網域下會有十幾條規則,而且它們會互相重疊。
於是有兩個問題必須回答:
第二題才是重點。Ingress 時代這題的答案是「看你用哪個 Controller」——nginx 有自己的排序、Traefik 有自己的權重欄位,換一家就得重新學。Gateway API 把它寫進規格,裁決順序是規格的一部分,不是實作的自由。
本篇會把這條裁決階梯逐階拆開,設計一組刻意衝突的規則實測,看 Envoy Gateway 的實際行為是否逐條符合規格。
先釐清一個最容易搞錯的結構。matches 是陣列,陣列裡每個元素又可以帶多個條件:
rules:
- matches:
- path: { type: PathPrefix, value: /foo } # ─┐ 同一個 match 內
headers: # │ 是 AND
- { name: version, value: v2 } # ─┘
- path: { type: Exact, value: /v2/foo } # ← 與上一個 match 之間是 OR
Day 3 裝好 CRD 之後就能直接向叢集查證這件事,不必翻文件:
kubectl explain httproute.spec.rules.matches
Matches define conditions used for matching the rule against incoming
HTTP requests. Each match is independent, i.e. this rule will be matched
if **any** one of the matches is satisfied.
...
HTTPRouteMatch defines the predicate used to match requests to a given
action. Multiple match types are ANDed together, i.e. the match will
evaluate to true only if all conditions are satisfied.
規則因此是:
還有一條預設值要記住:沒寫 path 就等於 PathPrefix: /,會吃下所有路徑。這是很多「為什麼我的規則把全部流量都攔走了」的根因。
| 類型 | 欄位 | 支援層級 |
|---|---|---|
| 路徑 | path.type:Exact / PathPrefix / RegularExpression |
前兩者 Core,正則為實作自訂 |
| 標頭 | headers[],多個之間 AND |
Core |
| 查詢參數 | queryParams[],多個之間 AND |
Core |
| 方法 | method:GET / POST / … |
Core |
RegularExpression 標的是 Implementation-specific——寫得出來,但換一個實作行為可能不同,這是可攜性的破口。
# day06/route-matches.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: match-demo
namespace: demo
spec:
parentRefs:
- name: platform-gateway
namespace: infra
hostnames:
- "match.localhost"
rules:
# ① method 與 header 寫在同一個 match 內 —— 兩者都成立才算中
- matches:
- method: POST
headers:
- name: X-Env
value: canary
backendRefs: [{ name: echo-a, port: 80 }]
# ② query 參數
- matches:
- queryParams:
- name: debug
value: "true"
backendRefs: [{ name: echo-b, port: 80 }]
# ③ 兩個 match 並列 —— 任一成立即算中
- matches:
- path: { type: Exact, value: /alpha }
- path: { type: Exact, value: /beta }
backendRefs: [{ name: echo-c, port: 80 }]
# ④ 保底
- matches:
- path: { type: PathPrefix, value: / }
backendRefs: [{ name: echo-d, port: 80 }]
後端是四個 traefik/whoami,各自用 --name=echo-a 之類的參數標記身分。whoami 會把 Name: 回吐,那一行就是裁決結果——不需要看日誌,看回應就知道哪條規則贏了。
kubectl apply -f day06/backends.yaml
kubectl apply -f day06/route-matches.yaml
curl -s -X POST -H "Host: match.localhost" -H "X-Env: canary" 127.0.0.1/
Name: echo-a
Hostname: echo-a-6cc496d7bd-qgpkw
RemoteAddr: 10.244.0.3:45178
POST / HTTP/1.1
Host: match.localhost
X-Env: canary
X-Envoy-External-Address: 172.18.0.1
X-Forwarded-For: 172.18.0.1
X-Request-Id: 5ac86223-add3-4c60-bb19-ea306574b2a8
Name: echo-a,規則①中了。把八種請求打完:
| 請求 | 命中 | 為什麼 |
|---|---|---|
POST / + X-Env: canary |
echo-a | 規則① AND 兩項都成立 |
POST /(不帶 header) |
echo-d | AND 缺一項,規則①不成立 |
GET / + X-Env: canary |
echo-d | method 不符,規則①不成立 |
GET /?debug=true |
echo-b | 規則② |
GET /alpha |
echo-c | 規則③第一個 match |
GET /beta |
echo-c | 規則③第二個 match |
GET /gamma |
echo-d | 都不中,落保底 |
GET /alpha?debug=true |
echo-c | 同時符合②和③,Exact 勝 |
最後一列已經踩到今天的主題了:規則②和規則③同時成立,誰贏?
答案同樣寫在 CRD 裡:
kubectl explain httproute.spec.rules.matches | sed -n '/MUST prioritize/,/implementation-specific/p'
Proxy or Load Balancer routing configuration generated from HTTPRoutes
MUST prioritize matches based on the following criteria, continuing on
ties. Across all rules specified on applicable Routes, precedence must be
given to the match having:
* "Exact" path match.
* "Prefix" path match with largest number of characters.
* Method match.
* Largest number of header matches.
* Largest number of query param matches.
五階,逐階比、平手才往下一階。注意 MUST 這個字——這不是建議。
平手到底還是分不出來的話,規格繼續往下裁:
If ties still exist across multiple Routes, matching precedence MUST be
determined in order of the following criteria, continuing on ties:
* The oldest Route based on creation timestamp.
* The Route appearing first in alphabetical order by "{namespace}/{name}".
If ties still exist within an HTTPRoute, matching precedence MUST be granted
to the FIRST matching rule (in list order)...
完整的裁決順序因此是七階:
① Exact path
② 最長的 PathPrefix
③ 有 method 匹配
④ header 匹配數量
⑤ query 匹配數量
──────────────── 跨 Route 才用得到 ────────────────
⑥ 較舊的 Route(creationTimestamp)
⑦ namespace/name 的字母序
──────────────── 同一條 Route 內 ────────────────
⑧ 列表順序
列表順序排在最後。這與大多數人的直覺相反——寫在前面不代表優先。
要驗證這條階梯,得讓每一階「單獨被觸發」。手法是:讓兩條規則在該階之前的所有條件完全相同,只在該階分出勝負。
還有一個關鍵佈局——把預期會輸的那條寫在前面。這樣一旦後面那條贏了,就證明勝負來自演算法,而不是列表順序。五組的贏家一律設成 echo-a、輸家一律 echo-b,答案卡只有一格。
# day06/route-precedence.yaml(節錄,完整檔含五組)
spec:
hostnames: ["prec.localhost"]
rules:
# ── 第 1 階:Exact 勝過同字數的 PathPrefix ──
- matches: [{ path: { type: PathPrefix, value: /exact } }]
backendRefs: [{ name: echo-b, port: 80 }]
- matches: [{ path: { type: Exact, value: /exact } }]
backendRefs: [{ name: echo-a, port: 80 }]
# ── 第 3 階:有 method 者勝過有兩個 header 者 ──
- matches:
- path: { type: PathPrefix, value: /mth }
headers:
- { name: X-One, value: "1" }
- { name: X-Two, value: "2" }
backendRefs: [{ name: echo-b, port: 80 }]
- matches:
- path: { type: PathPrefix, value: /mth }
method: GET
backendRefs: [{ name: echo-a, port: 80 }]
第 1 階這組值得多看一眼:Exact: /exact 與 PathPrefix: /exact 字數完全相同,第 2 階分不出勝負,所以勝負只可能來自第 1 階。這種「把變因鎖到只剩一個」的設計,是這組實驗能當證據用的原因。
for p in exact len/deep/x mth hdr; do
printf '%-12s → ' "/$p"
curl -s -H "Host: prec.localhost" -H "X-One: 1" -H "X-Two: 2" "127.0.0.1/$p" | grep -m1 '^Name:'
done
printf '%-12s → ' "/qry"
curl -s -H "Host: prec.localhost" "127.0.0.1/qry?q1=1&q2=2" | grep -m1 '^Name:'
/exact → Name: echo-a
/len/deep/x → Name: echo-a
/mth → Name: echo-b ← 正解應該是 echo-a
/hdr → Name: echo-a
/qry → Name: echo-a
四題對,第 3 階錯。
規格說「有 method 匹配」是獨立的一階,排在 header 數量之上,所以 /mth 應該由帶 method: GET 的規則勝出。實測是帶兩個 header 的那條贏了。
「不符規格」只是現象,要能寫進文章得先定性。做法是設計一組能區分兩種假說的對照:
兩組規則內容完全相同,只把列表順序對調:
# day06/route-method-weight.yaml(節錄)
# /w1:三個 header 的規則寫在前面
- matches: [{ path: { type: PathPrefix, value: /w1 },
headers: [ X-One, X-Two, X-Three ] }] # 權重 3
backendRefs: [{ name: echo-b, port: 80 }]
- matches: [{ path: { type: PathPrefix, value: /w1 },
method: GET, headers: [ X-One, X-Two ] }] # 權重 1+2?還是自成一階?
backendRefs: [{ name: echo-a, port: 80 }]
# /w2:同樣兩條,順序對調
兩個模型的預測不同:
/w1 與 /w2 都是 echo-a(method 那條無論排哪裡都贏)/w1 → Name: echo-b
/w2 → Name: echo-a
翻面了。 結論很明確:
Envoy Gateway v1.8.0 把
method匹配當成「多一個 header 匹配」併進第 4 階計數,而不是規格所定義的、獨立且高於 header 數量的第 3 階。
回頭驗算前面所有觀察都對得上:/mth 是「2 個 header」對上「method 折算 1」,2 > 1,所以 echo-b 贏;/w1、/w2 是 3 對 3,平手後由列表順序決定。
多數規則組不會踩到——要同時滿足「路徑相同、Exact/Prefix 相同、一邊用 method 一邊用 header、且 header 數量恰好跨過門檻」才會顯現。但一旦踩到,症狀是流量靜默地走錯後端:兩條規則都 Accepted=True、ResolvedRefs=True,狀態一片綠,沒有任何錯誤可查。
實務上的自保方式是不要讓 method 與 header 成為唯一的勝負手。把該優先的規則用更長的 path 或更多的 header 拉開差距,勝負就落在第 2 階或第 4 階,不會碰到這個落差。
這也正好說明了 Day 2 提的第六項要求——失效時能說明原因——在這裡是失效的:規格層無法表達「我的裁決結果與規格不同」,只有實測打得出來。
第 6 階「較舊的 Route 勝」也值得單獨驗,因為它與第 7 階(字母序)容易搞混。設計時刻意讓兩者互相矛盾:
# day06/route-tie.yaml
# tie-zulu :先建立(時間較舊),字母序在後 → 規格說它該贏
# tie-alpha:後建立(時間較新),字母序在前 → 若實作搞錯成字母序,它會贏
兩條 Route 宣告完全相同的 hostnames: ["tie.localhost"] 與 PathPrefix: /,只有後端不同。分兩次 apply、中間隔幾秒拉開 creationTimestamp:
kubectl -n demo get httproute tie-zulu tie-alpha \
-o custom-columns=NAME:.metadata.name,CREATED:.metadata.creationTimestamp
NAME CREATED
tie-zulu 2026-08-08T16:02:37Z
tie-alpha 2026-08-08T16:02:41Z
curl -s -H "Host: tie.localhost" 127.0.0.1/ | grep '^Name:'
Name: echo-a
echo-a 是 tie-zulu 的後端——較舊者勝,字母序沒有被拿來當第一順位,符合規格。
順帶一提,兩條 Route 的 Accepted 都是 True。規格沒有要求輸家被標記成錯誤,它只是不會被匹配到。這在除錯時很容易誤判:看到 Accepted=True 不代表這條 Route 真的有流量。