Day 13 / 共 20 天 · 第 3 周 HTTP 与外部调用

Cluster 集群抽象

Day 12 学的 HttpContext 遥控器里有个 RouteCall,能调后端;明天(Day 14)要正式在插件里调外部服务,第一个问题是"调哪个后端"。wasm-go 用 8 种 Cluster 类型表达不同来源的后端(当前路由、K8s 服务、Nacos、固定 IP、域名…),今天把它们讲清——它是明天 HttpCall 的入参。

📍 你在整门课的位置(wasm-go 20 天 · 第 3 周 HTTP 与外部调用)
HttpContext D12 Cluster 8 种 D13 HttpCall 异步 D14 Redis D15 高级特性 W4
L01

为什么要 Cluster

🤔 痛点:Envoy 的"后端地址"格式反人类 你想让插件调一个 K8s 里的鉴权服务。Envoy 认的不是 http://auth-svc:8080 这么简单的地址,而是一串 outbound|8080||auth-svc.default.svc.cluster.local——竖线、端口、版本、FQDN 的固定拼法,Nacos 来源的格式又完全不同。让你手写、还得记住每种来源的拼法,极易写错。
💡 本质:Cluster = 填表式的"地址翻译器" Cluster 就像寄快递时的电子面单:你只填"收件人、公司、楼层"这些人话字段,系统自动生成快递公司内部认的那串编码地址。你填 ServiceName/Namespace/Port,K8sCluster 帮你拼成 Envoy 认的 outbound|...。不同来源(K8s/Nacos/域名/IP)= 不同的"面单模板",但填法都一样简单。
"集群"= Envoy 眼里的一组后端 回想 Envoy/Istio 课:Envoy 把后端服务组织成"Cluster"(集群),每个集群有个名字(如 outbound|8080||foo.default.svc.cluster.local),里面是一组实例。插件要调后端,得告诉 Envoy"调哪个集群"——但集群名格式很复杂、不同来源(K8s/Nacos/固定IP)格式还不一样。wasm-go 的 Cluster 类型就是"帮你拼对集群名"的工具:你填服务名/端口,它生成 Envoy 认识的集群名。
L02

Cluster 接口

cluster_wrapper.go:24-27:极简接口,只有两个方法:

type Cluster interface {
    ClusterName() string   // Envoy 集群名(调用时用)
    HostName() string      // 请求的 :authority 头
}
读法:8 种 Cluster 都实现这个接口,各自用不同规则生成 ClusterNameHostName下游的 HttpCall(Day 14)只依赖这个接口——不管你用哪种 Cluster,调用方式一样。这是"面向接口编程"的典范:新增一种后端来源,加个实现即可。
interface Cluster ClusterName() · HostName() RouteCluster当前路由后端 K8sClusteroutbound|...svc NacosClustergroup/namespace FQDNCluster域名+端口 StaticIp固定 IP Dns/Consul/Target其余来源 HttpCall(Day 14)只认接口 → 换任意实现,调用代码一行不改
图注:一个 Cluster 接口,8 种实现"填空"出各自的集群名——面向接口编程,新增后端来源只需加一个实现类。
L03

RouteCluster:当前路由后端

cluster_wrapper.go:29-46

type RouteCluster struct { Host string }
func (c RouteCluster) ClusterName() string {
    routeName, _ := proxywasm.GetProperty([]string{"cluster_name"})  // 用当前请求的集群
    return string(routeName)
}
func (c RouteCluster) HostName() string {
    if c.Host != "" { return c.Host }
    return GetRequestHost()   // 默认用当前请求的 Host
}
最省事的一种 RouteCluster = "调当前请求本来要去的后端"。它直接读 Envoy 属性 cluster_name(当前请求匹配到的集群)。典型用途:插件想"预调一次当前后端"(比如先探一下),或改写后重新发。你什么都不用填,它自动用当前路由的目标。(对应 Day 12 HttpContext 的 RouteCall。)
L04

K8sCluster:K8s 服务

cluster_wrapper.go:61-83

type K8sCluster struct { ServiceName, Namespace string; Port int64; Version, Host string }
func (c K8sCluster) ClusterName() string {
    namespace := "default"; if c.Namespace != "" { namespace = c.Namespace }
    return fmt.Sprintf("outbound|%d|%s|%s.%s.svc.cluster.local",
        c.Port, c.Version, c.ServiceName, namespace)
}
📝 举个例子:填字段 → 自动生成集群名 你写 wrapper.K8sCluster{ServiceName:"auth", Namespace:"prod", Port:8080}
ClusterName() 自动生成 → outbound|8080||auth.prod.svc.cluster.local
换成 FQDN 外部服务:wrapper.FQDNCluster{FQDN:"api.example.com", Port:443}outbound|443||api.example.com。你从头到尾没碰过那串竖线格式。
这个集群名格式眼熟吗? outbound|端口|版本|服务.命名空间.svc.cluster.local——这正是 Istio 的集群命名约定!(回想 Istio 课)你只填服务名/命名空间/端口,K8sCluster 帮你拼成这个格式。用途:插件要调集群内另一个 K8s 服务(比如调个鉴权微服务)。不用记那套复杂格式,填字段即可。
L05

NacosCluster:Nacos 服务

cluster_wrapper.go:85-116:字段更多——ServiceName / Group(默认 DEFAULT-GROUP)/ NamespaceID / Port / IsExtRegistry / Version / Host。集群名按 Nacos 的命名规则拼(含 group、namespace)。

读法:回想上一站 Higress/Console:服务来源支持 Nacos(Day 14)。如果你的后端是从 Nacos 注册中心发现的,就用 NacosCluster——它按 Nacos 的服务标识规则拼集群名(group 里的 _ 会替换成 -)。IsExtRegistry 区分 edas/sae 等外部注册。
L06

FQDN / Static / Dns / Consul

Cluster 类型用途
FQDNCluster用完整域名 + 端口(:168,最常用于调外部 HTTP 服务,昨天 http-call 例子就用它)
StaticIpCluster固定 IP 后端(:117
DnsClusterDNS 解析的后端(:134
ConsulClusterConsul 注册中心的服务(:148
TargetCluster直接指定集群名 + Host(:48,最底层,你自己拼好集群名)
对应上一站的服务来源类型 还记得上一站 ServiceSource 的 8 种类型吗(nacos/consul/static/dns…)?这里的 Cluster 类型几乎一一对应——因为它们都在表达"后端从哪来"。控制面(Console)配服务来源、数据面(wasm-go 插件)用对应 Cluster 调用,两端概念统一。http-call 例子(Day 14)用的 FQDNCluster 是调外部 API 最常用的。
L07

集群名的含义(ClusterName vs HostName)

两个方法的区别很关键:

  • ClusterName:Envoy 内部的集群标识——告诉 Envoy"把请求发到这组后端"。
  • HostName:请求的 :authority 头(Host)——后端服务看到的域名,影响后端的虚拟主机路由。
读法:调用时两者都要:ClusterName 决定"物理上发给谁",HostName 决定"HTTP 语义上访问哪个域名"。比如调一个 K8s 服务:ClusterName 是那串 outbound|...,HostName 是 svc.ns.svc.cluster.localDay 14 的 HttpCall 会同时用到这两个值。
⚠️ 常见误解:小白常以为"填了 ClusterName 就够了,HostName 可有可无"。其实少了 HostName,后端拿到的 :authority 头是空/错的,很多后端会按虚拟主机路由拒绝或返回错误页——两个都得对。类比:ClusterName 是"把包裹送到哪栋楼",HostName 是"包裹上写给哪家公司收",缺一不可。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么要 Cluster 抽象?它帮你解决什么?
  • Cluster 接口的两个方法各是什么?
  • K8sCluster 的集群名格式和谁的约定一致?
  • 8 种 Cluster 和上一站的服务来源类型什么关系?
🎯 记忆口诀 "调外部域名用 FQDN,调集群内服务用 K8s,注册中心用 Nacos/Consul,就地转发用 Route——填字段,别记那串竖线。"

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
grep -n "type.*Cluster struct" pkg/wrapper/cluster_wrapper.go
sed -n '61,116p' pkg/wrapper/cluster_wrapper.go
grep -n "FQDNCluster" examples/http-call/main.go
明天预告 · Day 14HTTP 外部调用——用 Cluster + HttpCall 异步调后端,看 ClusterClient/DispatchHttpCall、回调式编程、以及 ActionPause → Resume 的完整节奏(含 http-call 例子)。
← Day 12 Day 14 · HTTP 调用 →