在微服务架构中,API网关作为流量的统一入口,最核心的职责之一就是基于路径的路由(Path-based Routing) 。当客户端请求 https://api.example.com/order/v1/create 时,网关需要将请求精准分发到后端的订单微服务。
在这个看似简单的分发过程中,其实隐藏着一个极具争议的架构抉择:流量分发后,那个用来做路由区分的前缀(比如 /order),到底该由网关剥离,还是由后端微服务自行消化?
本文将深入对比这两种常见的落地架构方案,并结合 K8s Gateway API 与微服务框架(如 Quarkus/Spring Boot)给出具体的代码示例。
方案一:网关剥离前缀(URL Rewrite / Strip Path)
在这种架构下,网关承担了所有的"脏活累活"。它对外暴露带有特定前缀的 URL,但在将请求转发给后端微服务之前,会将这个前缀"无情剥离"。后端微服务完全处于黑盒状态,认为自己独占了整个域名,接口直接挂载在根路径 / 下。
1.1 Mermaid 流量时序图
微服务 (Order SVC) API网关 客户端 微服务 (Order SVC) API网关 客户端 #mermaid-svg-JJ6Ssu6yUVGa2w3C{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .error-icon{fill:#552222;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .marker.cross{stroke:#333333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JJ6Ssu6yUVGa2w3C p{margin:0;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JJ6Ssu6yUVGa2w3C text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JJ6Ssu6yUVGa2w3C .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .sequenceNumber{fill:white;}#mermaid-svg-JJ6Ssu6yUVGa2w3C #sequencenumber{fill:#333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .messageText{fill:#333;stroke:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .labelText,#mermaid-svg-JJ6Ssu6yUVGa2w3C .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .loopText,#mermaid-svg-JJ6Ssu6yUVGa2w3C .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JJ6Ssu6yUVGa2w3C .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .noteText,#mermaid-svg-JJ6Ssu6yUVGa2w3C .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actorPopupMenu{position:absolute;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JJ6Ssu6yUVGa2w3C .actor-man circle,#mermaid-svg-JJ6Ssu6yUVGa2w3C line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JJ6Ssu6yUVGa2w3C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 匹配前缀 /order触发 URL Rewrite (剥离 /order) 框架毫无感知,正常处理 /api/v1/list GET /order/api/v1/listGET /api/v1/list200 OK200 OK
1.2 典型的网关配置示例 (Kong Ingress)
在老旧的 Ingress 规范中,通常通过注解来实现剥离:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: order-svc-ingress
annotations:
konghq.com/strip-path: "true" # 核心:通知 Kong 剥离前缀
spec:
rules:
- http:
paths:
- path: /order
pathType: Prefix
backend:
service:
name: order-svc
port:
number: 8080
后端的代码(如 Spring Boot)非常纯粹:
java
@RestController
@RequestMapping("/api/v1") // 不需要知道 /order 的存在
public class OrderController {
@GetMapping("/list")
public List<Order> listOrders() { ... }
}
1.3 优缺点分析
- 优点 :后端代码极致纯粹,路由逻辑与业务逻辑完全解耦。未来无论网关层的对外路径怎么变(比如改成
/v2/order),后端代码一行都不用改。 - 致命缺点(瞎子问题) :一旦剥离路径,后端框架就成了"瞎子"。微服务自动生成的 OpenAPI/Swagger 文档中的测试路径会全部失效(Swagger UI 会让你去请求
/api/v1/list,但实际上外部调用必须加/order);代码中如果需要生成绝对路径链接(HATEOAS),也会丢失前缀。
方案二:后端配置 Root Path(网关透传)
这种方案反其道而行之。网关只做纯粹的流量分发,不修改任何 URL 路径,将带有前缀的原始请求原封不动地透传给后端。后端微服务则在框架层面配置全局的 Root Path(或 Context Path),主动去"认领"并消化掉这个前缀。
2.1 Mermaid 流量时序图
微服务 (Order SVC) API网关 客户端 微服务 (Order SVC) API网关 客户端 #mermaid-svg-FyM9WzL5R0gR4tnt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FyM9WzL5R0gR4tnt .error-icon{fill:#552222;}#mermaid-svg-FyM9WzL5R0gR4tnt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FyM9WzL5R0gR4tnt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FyM9WzL5R0gR4tnt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FyM9WzL5R0gR4tnt .marker.cross{stroke:#333333;}#mermaid-svg-FyM9WzL5R0gR4tnt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FyM9WzL5R0gR4tnt p{margin:0;}#mermaid-svg-FyM9WzL5R0gR4tnt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FyM9WzL5R0gR4tnt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FyM9WzL5R0gR4tnt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-FyM9WzL5R0gR4tnt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-FyM9WzL5R0gR4tnt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-FyM9WzL5R0gR4tnt .sequenceNumber{fill:white;}#mermaid-svg-FyM9WzL5R0gR4tnt #sequencenumber{fill:#333;}#mermaid-svg-FyM9WzL5R0gR4tnt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-FyM9WzL5R0gR4tnt .messageText{fill:#333;stroke:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FyM9WzL5R0gR4tnt .labelText,#mermaid-svg-FyM9WzL5R0gR4tnt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .loopText,#mermaid-svg-FyM9WzL5R0gR4tnt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-FyM9WzL5R0gR4tnt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FyM9WzL5R0gR4tnt .noteText,#mermaid-svg-FyM9WzL5R0gR4tnt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-FyM9WzL5R0gR4tnt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FyM9WzL5R0gR4tnt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FyM9WzL5R0gR4tnt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-FyM9WzL5R0gR4tnt .actorPopupMenu{position:absolute;}#mermaid-svg-FyM9WzL5R0gR4tnt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-FyM9WzL5R0gR4tnt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-FyM9WzL5R0gR4tnt .actor-man circle,#mermaid-svg-FyM9WzL5R0gR4tnt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-FyM9WzL5R0gR4tnt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 仅匹配路由 /order不做任何修改,直接透传 框架配置了 root-path=/order自动消化前缀,路由到 /api/v1/list GET /order/api/v1/listGET /order/api/v1/list200 OK200 OK
2.2 配置示例 (K8s Gateway API + Quarkus)
在现代的 K8s Gateway API 中,我们不再强制使用重写,直接定义匹配规则即可:
yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: order-route
spec:
parentRefs:
- name: main-gateway
rules:
- matches:
- path:
type: PathPrefix
value: /order
# 注意:这里没有任何 filters去执行 URLRewrite
backendRefs:
- name: order-svc
port: 8080
对应的,在后端的微服务配置文件中,我们需要显式声明这个上下文路径(以 Quarkus 为例):
properties
# src/main/resources/application.properties
quarkus.http.root-path=/order
java
@Path("/api/v1") // 配合全局 root-path,实际暴露为 /order/api/v1
public class OrderResource {
@GET
@Path("/list")
public Response listOrders() { ... }
}
2.3 优缺点分析
- 优点:所见即所得。完美解决了 Swagger 迷路和绝对路径生成的问题。微服务清晰地知道自己在整个架构中的真实位置。
- 缺点:后端的"环境感知"变强了。如果运维团队决定调整对外的 API 前缀,研发必须配合修改代码配置并重新部署,打破了纯粹的职责边界。
三、 现实工程中的妥协与抉择
理论上,方案一是微服务纯粹主义者的最爱,方案二则是务实主义者的首选。但在真实的工程实践中,我们往往会因为底层基础设施的限制而"被迫"做出选择。
以开源版 Kong Ingress Controller (KIC) 为例,在集成 K8s Gateway API 规范时,由于 KIC 默认采用 Traditional Router(传统路由引擎),它在解析 Gateway API 高级的正则 URLRewrite 过滤器时,会直接抛出 KongConfigurationTranslationFailed 的翻译崩溃错误。
如果为了追求方案一的解耦,强行在生产环境开启 Kong 尚未完全成熟的 Expression Router,无疑是给整个集群埋下一颗定时炸弹。因此,在我们的工程实践中,为了稳妥起见,我们主动放弃了网关层的 URL 重写,选择了方案二(后端配置 Root Path)。
这看似是架构上的退让,实则是工程实践中为追求系统最高可用性而做出的明智权衡。理解每一种方案背后的利弊与基础设施的边界,才是架构设计的核心魅力所在。