从零开始的RPC(九):微服务网关
参考资料来源
- 《Go语言高并发与微服务实战》书籍
实体书成书于20年之前,可能是在18年左右开始编写的
彼时Go的版本为1.12.x,尚未添加泛型,PRC和Protobuf协议的一些细节也与现在不同,因此理论部分仅供参考 - B站开源Kratos微服务框架 官方文档
- Ge老师的发言
重要
文章含AI量较高,请酌情阅读
前言
在单体应用程序架构时代,客户端(Web或移动端)通过向后端应用程序发起一次RESTful调用来获取数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个,然后应用程序会查询各种数据库表,并将响应返回给客户端。微服务架构下,单体应用被切割成多个微服务,如果将所有的微服务直接对外暴露,势必会出现一些问题。
客户端可以直接向每个微服务发送请求,但是会存在如下的问题:
- 客户端需求和每个微服务暴露的细粒度API不匹配
- 部分服务使用的协议不是Web友好协议;可能使用Thrift二进制RPC,也可能使用AMQP消息传递协议,这些API无法暴露出去
- 微服务难以重构。如果合并两个服务,或者将一个服务拆分成两个或更多服务,这类重构就非常困难了
如上问题,可以通过微服务网关解决。网关在微服务架构中的作用是保护、增强和控制外部请求对于API服务的访问
微服务网关介绍与功能特性
早期的软件架构基本上都是单体架构,系统之间往往不需要进行交互,这也导致数据孤岛和ETL工具的发展。随着企业应用越来多,相互的关系也越来密切。应用之间也迫切需要进行实时交互访问,随后异构系统集成和数据交互技术被越来越多的公司采用,SOA的概念被提了出来,Web Service逐渐流行起来
互联网时代,很多公司为了适应更加灵活的业务需求,采用基于HTTP协议和RESTful的架构风格,轻量级的通信成为企业开发的最佳实践,在SOA架构中,企业服务总线技术ESB所暴露的集中式架构的劣势让开发者明白基于注册和发现的分布式架构才是解决问题的关键办法。由此,微服务架构逐渐流行起来
如下图所示,在微服务架构中,网关位于接入层和业务服务层之间。微服务网关是微服务架构中的一个基础服务,从面向对象设计的角度看,它与外观模式类似。微服务网关封装了系统内部架构,为每个客户端提供定制的API用来保护、增强和控制对微服务的访问。微服务网关是一个处于应用程序或服务之前的系统,这样微服务就会被微服务网关保护起来,对所有的调用者透明

微服务网关作为连接服务消费方和提供方的中间件系统,将各自的业务系统的演进和发展做了天然的隔离(如下图所示),使业务系统更加专注于业务服务本身,同时微服务网关还可以为服务提供和沉淀更多的附加功能,下面我们总结一下微服务网关的主要作用

- 请求接入:管理所有接入请求,是所有API接口的请求入口。作为企业系统边界,隔离外网系统与内网系统
- 解耦:通过解耦,使得微服务系统的各方能够独立、自由、高效、灵活地调整,而不用担心给其他方面带来影响
- 拦截策略:提供了一个扩展点,方便通过扩展机制对请求进行一系列加工和处理。可以提供统一的安全、路由和流控等公共服务组件
- 统一管理:可以提供统一的监控工具、配置管理等基础设施
下面我们具体介绍这几类作用
请求接入
通过服务网关接入外部请求。企业为了保护内部系统的安全性,内网与外网都是隔离的,企业的服务端应用都是运行在内网环境中,为了安全的考量,一般都不允许外部直接访问。对外只会暴露指定的端口,内部系统只接受服务网关转发过来的请求。网关通过白名单或校验规则,对访问进行了初步的过滤。相比防火墙,这种软件实现的过滤规则,更加动态灵活。
多方的解耦
在微服务架构下,整个环境包括服务的提供者、服务的消费者、服务运维人员、安全管理人员等,每个角色的职责和关注点都不同。例如:服务消费方已经提出一些新的服务需求,以快速应对业务变化;而服务提供者作为业务服务的沉淀方,更希望保持服务的通用性与稳定性,这就很难应对快速的变化。但有了服务网关这一层,就可以很好地解耦各方的相互依赖关系,让各方更加专注自己的目标。具体来说包括如下几点:
- 解耦功能与非功能
企业在把服务提供给外部访问时,除了实现业务逻辑功能外,还面临许多非功能性的要求。例如:需要防范黑客攻击,需要应对突发的访问量,需要确认用户的权限,需要对访问进行监控等。这些非功能逻辑,不能与业务逻辑的开发混在一起,需要有专业的人员甚至专业的团队来处理 - 解耦客户端与服务提供层
客户端与服务提供者分属于不同的团队,工作性质和要求也不相同。对于服务提供者来说,主要的职责是对业务进行抽象,提供可复用的业务功能,因此他们需要对业务模型进行深入的思考和沉淀,不能轻易为了响应外部的需求而破坏业务模型的稳定性。而业务的快速变化,又要求企业快速提供接口来满足客户端需求。这就需要一个中间层(网关层),来对服务层的接口进行封装,以及时响应客户端的需求。通过解耦,服务层可以使用统一的接口、协议和报文格式来暴露服务,而不必考虑客户端的多种形态。
拦截策略
服务网关层除了请求的路由转发外,还需要负责安全审计、鉴权、限流和监控等。这些功能的实现方式,往往随着业务的变化不断调整。例如权限控制方面,早期可能只需要简单的“用户名+密码”方式,后续用户量大了后,可能会使用高性能的第三方解决方案
因此,这些能力不能一开始就固化在网关平台上,而应该是一种可配置的方式,便于修改和替换。这就要求网关层提供一套机制,可以很好地支持这种动态扩展
统一管理
服务可以提供统一的监控工具、配置管理和接口的API文档管理(比如Swagger)等基础设施。例如,针对不同的监控方案,记录对应的日志文件
理论
API网关选型
在 2026 年,微服务网关不再是单一的组件,而是分为**入口网关(Traffic Gateway)和业务网关(Business Gateway/BFF)**两层协同作战。
1. APISIX:动态路由的王者(云原生首选)
如果你觉得配置 Nginx 频繁 reload 很痛苦,那么 APISIX 就是你的救星。它基于 Nginx(OpenResty),但通过 Etcd 实现了全动态配置。
核心优势:修改路由、热插拔插件均在毫秒级生效,无需重启服务。
Go 友好性:支持使用 Go 编写插件,这让 Kratos 开发者可以无缝将其业务逻辑下沉到网关层。
2. Nginx:永远的流量基石(底层必修课)
尽管出现了许多高级网关,但 Nginx 依然是全球流量的守门人
定位变化:在 2026 年,Nginx 更多地承担四层负载均衡(L4 LB)、SSL 卸载和静态资源压缩。
学习意义:它是所有现代网关(如 Kong, APISIX)的祖师爷。理解了 Nginx 的
location匹配、upstream机制,你就能秒懂所有网关的路由逻辑。Nginx可以说是互联网应用的标配组件,主要的使用场景包括负载均衡、反向代理、代理缓存、限流等
Nginx由内核和模块组成,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件与客户端请求进行URL匹配,然后启动不同的模块去完成相应的工作
Nginx启动后,会有一个Master进程和多个Worker进程,Master进程和Worker进程之间通过进程间通信进行交互的。Worker工作进程的阻塞点在I/O多路复用函数调用处(如
Select()、Wait()等),以等待发生数据可读/写事件。Nginx采用了异步非阻塞的方式来处理请求,也就是说,Nginx是可以同时处理成千上万个请求的
Nginx处理请求的流程如下图所示
在开发阶段,还可以将Lua嵌入到Nginx上,将Nginx变成一个Web容器,从而使用Lua语言开发高性能Web应用。在开发的时候我们可以使用OpenResty来搭建开发环境,OpenResty将Nginx核心、Lua JIT、许多有用的Lua库和Nginx第三方模块打包在一起;这样只需要安装OpenResty,而不需要了解Nginx核心和写复杂的C/C++模块,就可以只使用Lua语言进行Web应用开发了
使用Nginx的反向代理和负载均衡可实现负载均衡及高可用,除此之外还需要我们解决自注册和网关本身的扩展性
3. Envoy:Service Mesh 的事实标准
作为 CNCF 的明星项目,Envoy 是为了“巨量并发”而生的。
特性:它原生支持 gRPC 转发和 HTTP/3。
实战地位:如果以后进阶到 Istio(服务网格),Envoy 就是必须打交道的底层组件。它的配置极其复杂(通常由控制平面生成),不建议初学者手撸。
4. KrakenD:高性能的“无状态”怪兽
KrakenD 是目前 Go 语言编写的网关中,单机吞吐量最顶级的选手之一。它的设计理念非常硬核。
真正的无状态(Stateless):它不需要数据库,所有配置都通过一个 JSON 文件完成。这意味着你可以像部署 Kratos 二进制文件一样部署它,扩容极快。
线性性能预测:它没有使用传统的插件系统(如 Lua),而是通过编译时的组件组合。这让它在处理高并发请求时,延迟几乎是平稳的直线。
BFF 聚合能力:它最强大的地方在于 Response Merging。比如前端需要一个页面数据,原本要调 3 个 Kratos 服务,KrakenD 可以在网关层并行请求这 3 个服务,把结果拼成一个大 JSON 返回给前端。
5. Kratos Gateway:Kratos 体系的“亲儿子”
这就是下面那个手搓网关 的最终进化版。它是基于 Kratos 框架开发的官方网关。
原生集成 Discovery:它完美继承了你已经学会的
Discovery接口。你只需要在配置里写consul://...,它就会自动去捞地址。中间件完全复用:这是最爽的一点。你在
biz或data层写的限流、鉴权、日志中间件,可以直接通过配置挂载到网关上。协议转码(Transcoding):它内置了强大的 HTTP to gRPC 转码器。外部发来 JSON,它自动转成 Protobuf 调下游,连代码都不用生成,全动态完成。
时代的眼泪:Zuul vs. Spring Cloud Gateway
在 Spring Cloud 的早期版图中,Zuul 是绝对的流量入口。但随着高并发需求的爆发,Zuul 的底层架构成了它被“鞭尸”的根本原因。
1. Zuul 1.0:那个被时代抛弃的“老古董”
Zuul 1.0 的本质是一个基于 Servlet 构建的同步阻塞网关。
“一线程一请求”模型:每进来一个请求,Zuul 就要分配一个线程去处理。如果下游服务卡了 1 秒,这个线程就得在那干等 1 秒。
高并发下的惨状:在高并发场景下,线程池会迅速耗尽。此时,新的请求进不来,网关直接瘫痪。
鞭尸点:Zuul 2.0 其实尝试过改为非阻塞架构,但因为 Netflix 内部迭代太慢,Spring 社区等不及了,直接自己动手写了 Spring Cloud Gateway。这就是著名的“开源社区嫌你慢,干脆把你踢了”的故事。
2. Spring Cloud Gateway:涅槃重生的新贵
为了解决 Zuul 的短板,SCG 基于 Spring 5、Spring Boot 2 和 Project Reactor 构建。
响应式编程(WebFlux):它不再使用传统的 Servlet 模型,而是基于 Netty 的事件循环机制。
“小线程办大事”:几个核心线程就能处理成千上万的并发请求。线程不再由于 I/O 等待而阻塞,而是通过“回调”和“事件驱动”来流转请求。
强大的谓词(Predicate)与过滤器(Filter):它的逻辑设计得非常精妙。你可以通过简单的配置实现“如果 Header 带了 X,就转发到 A 服务”这种复杂路由。
深度对比:为什么 SCG 赢了?
| 特性 | Zuul 1.0 (已淘汰) | Spring Cloud Gateway (当前主流) |
|---|---|---|
| 底层架构 | 阻塞式 Servlet I/O | 非阻塞式 Netty / WebFlux |
| 高并发表现 | 线程数等于并发数,易崩溃 | 极少量线程处理极高并发 |
| 动态路由 | 支持较差,需要额外开发 | 原生支持,配置极其灵活 |
| 功能丰富度 | 较简陋,基本只有路由 | 内置限流、断路器、路径重写等 |
其他
注
为什么 Kratos 不需要像 Java 那样专门搞一个“响应式模型”?
- Go 的优势:Go 原生就通过 Goroutine(轻量级协程) 解决了 Zuul 的痛点。
- 对比:Zuul 开启 1000 个线程可能就爆内存了,而你的 Kratos 服务开启 100,000 个 Goroutine 依然能跑得很稳。
- 结论:Java 费了九牛二虎之力搞出的 WebFlux 响应式架构,在 Go 语言里只是**“正常写代码”** 的默认状态
注
书里接下来会讲Nginx
请记住:SCG 再强,它也是应用层的网关。 在生产环境中,Common Practice是用 Nginx 做第一层(处理 HTTPS 证书、基础防攻击),然后再转发给 SCG(Java 侧) 或 Kratos(Go 侧) 去处理业务路由
API网关方案对比
| 组件/指标 | Nginx (基石) | APISIX (动态王者) | Envoy (云原生标准) | KrakenD (Go 性能怪兽) | Kratos Gateway (Go 体系整合) |
|---|---|---|---|---|---|
| API 注册/动态路由 | 静态配置,需 reload | 全动态,基于 Etcd,毫秒生效 | 动态 xDS 配置流 | 静态 JSON/YAML 配置 | 动态感知,原生支持 Consul/Etcd |
| 支持协议 | HTTP, HTTP/2 | HTTP, gRPC, MQTT, Dubbo | HTTP/3, gRPC, Redis, Thrift | HTTP, gRPC | HTTP, gRPC 原生转码 |
| 核心开发语言 | C | C + Lua | C++ | Go | Go |
| 插件机制 | C 模块/Lua | Lua, Java/Go (外部插件) | Wasm, C++, Lua | Go (编译时插件) | Go 中间件 (与 Kratos 通用) |
| 性能 | 极高 | 极高 (基于 OpenResty) | 极高 | 极高 (Go 阵营顶峰) | 高 (适合 BFF 场景) |
| 高可用集群 | 需配合 Keepalived | 原生支持,数据面无状态 | 云原生控制面统一管理 | 无状态,水平扩容极简 | 分布式实例,依赖 Discovery |
| BFF 能力 | 弱 | 中 (需写 Lua 脚本) | 中 | 强 (支持请求聚合/合并) | 强 (原生 Go 逻辑处理) |
| 管理便利性 | 无 GUI,全靠文件 | 提供 Dashboard 管理后台 | 依赖控制平面 (如 Istio) | 配置文件/设计器 | 配置驱动/Kratos 插件化 |
为什么“鞭尸”Zuul 后,我们要推崇 KrakenD 和 Kratos Gateway?
- 内存与并发模型:Zuul 1.x 倒在了 Java 线程的昂贵开销上。而 KrakenD 和 Kratos Gateway 利用 Go Goroutine,在处理相同并发量时,内存占用仅为 Zuul 的几十分之一。
- 开发效率:如果你是一个 Kratos 开发者,Kratos Gateway 允许你复用所有
biz层的 Error Reason 和中间件,这种“全栈 Go”的快感是切换到 Java 网关完全体会不到的。
APISIX 与 Nginx 的“父子”进化
- Nginx 是那个稳健的父亲,但他老派,改个路牌(路由)都要重启全家(Reload)。
- APISIX 是那个穿了“动态外壳”的儿子,他继承了父亲 C 语言的高性能血统,但通过 Etcd 和 Lua JIT 实现了“车跑着就能换轮胎”的动态性。
Envoy:云原生的“翻译官”
- 如果你的项目未来要上 Service Mesh(服务网格),Envoy 是唯一真神。它能把复杂的网络拓扑抽象成统一的配置,是目前处理 gRPC 流量 最专业的工业级网关。
实践
手搓一个网关
API网关最基础的功能是对请求的路由转发,根据配置的转发规则将请求动态地转发到指定的服务实例。动态是指与服务发现结合,如Consul、Zookeeper等组件,在服务注册与发现章节中已详细讲解过了。本节将会基于Go Kratos实现一个简易的API网关
API网关根据客户端HTTP请求,动态查询注册中心的服务实例,通过反向代理实现对后台服务的调用
这里我们简单介绍一下正向代理和反向代理
- 正向代理
是在用户端进行的代理。比如需要访问某些网站,我们可能需要使用代理服务器,代理是在我们的用户浏览器端进行设置的(并不是在远端的服务器设置)。浏览器先访问代理地址,代理服务器转发请求,并在最后将请求结果原路返回。 - 反向代理
服务器拿到Request以后,把它们转发给内网的服务器,而那些发送Request给代理的client并不知道这个内网的存在。反向代理可以在任意多个服务器需要被同一个IP同时访问的时候使用,服务网关的代理模式属于反向代理。
API网关会为符合规则的请求转发到对应的后端服务。这里的规则可以有很多种,如HTTP请求的资源路径、请求的方法、请求的头部和请求的参数等等。这里我们以最简单的请求路径方式为例,规则为:/{serviceName}/#。即:路径第一部分为注册中心服务实例名称,其余部分为服务实例的RESTful路径
实现思路
要实现的网关应该遵循如下的运行流程:
客户端向网关发起请求,网关解析请求资源路径中的信息,根据服务名称查询注册中心的服务实例,然后使用反向代理技术把客户端请求转发至后端真实的服务实例,请求执行完毕后,再把响应信息返回客户端
设计实现的网关需要能做到下面这些事:
- HTTP请求的规则遵循
/<SERVICE_NAME>/<METHOD_NAME>/<PATH_PARAMTERS>/?<QUERY_STRINGS>,否则API网关无法处理转发 - 使用Go提供的反向代理库
httputil.ReverseProxy实现一个简单的反向代理,它能够对请求实现负载均衡,把请求随机地发送给集群中的某一服务实例 - 使用consul API动态查询服务实例
编写反向代理方法
反向代理需要用到NewReverseProxy方法NewReverseProxy 方法接受两个参数:Consul 客户端对象 *api.Client和日志记录工具 *log.Helper(Kratos框架使用的日志记录器),返回反向代理对象 httputil.ReverseProxy。该方法的实现如下所述:
- 获取请求路径(并按
/进行分割,抛弃第一部分),检查第二部分(约定为服务实例名称)是否为空,不为空则直接返回(http.util会报错http scheme为空,但不需要理会) - 解析请求路径,获取服务名称(
parts[1]为服务实例名称) - 使用consul API的
Service()方法查询服务实例,若实例Map长度不为0,则使用rand.Intn随机选择一个作为目标实例 - 根据选定的目标实例,修改
req的参数,包括协议/Scheme、远程主机/Host和请求路径/Path
func NewReverseProxyV1(client *api.Client, log *log.Helper) *httputil.ReverseProxy {
var searchService = func(name string) (res []*api.CatalogService) {
result, _, err := client.Catalog().Service(name, "", &api.QueryOptions{})
if err != nil {
log.Error(err)
return res
}
res = result
return res
}
var randomService = func(services []*api.CatalogService) *api.CatalogService {
var randomID = rand.Intn(len(services))
return services[randomID]
}
var backendScheme = "http"
director := func(req *http.Request) {
// 获取原始URL并切割
var url = req.URL.String()
var parts = strings.Split(url, "/")
if len(parts) < 1 || parts[1] == "" {
log.Error("服务实例名称不可为空")
return
}
var serviceName = parts[1] // 约定第二个参数是服务实例名称; 第一个参数为请求来源
services := searchService(serviceName)
if len(services) == 0 {
log.Errorf("未找到服务实例[%v]", serviceName)
return
}
var methodName = parts[2] // 约定第三个参数是方法名
var pathParameter = parts[3:] // 剩余参数是路径参数
/*
for k, service := range services {
fmt.Printf("服务实例[%v] %v | %v | %v:%v\n",
k, service.ServiceID, service.ServiceName, service.ServiceAddress, service.ServicePort)
}
服务实例[0] srv-1 | helloworld-srv | 192.168.100.1:9001
服务实例[1] srv-2 | helloworld-srv | 192.168.100.1:9002
*/
/*
for k, service := range services {
fmt.Printf("服务实例[%v] %v | %v ===\n",
k, service.ServiceID, service.ServiceName)
for k, v := range service.ServiceTaggedAddresses {
fmt.Printf(" %v: %v\n", k, v)
}
fmt.Printf("===\n")
}
服务实例[0] srv-1 | helloworld-srv ===
wan_ipv4: {192.168.100.1 9001}
grpc: {grpc://192.168.100.1:9001 9001}
http: {http://192.168.100.1:8001 8001}
lan_ipv4: {192.168.100.1 9001}
===
服务实例[1] srv-2 | helloworld-srv ===
grpc: {grpc://192.168.100.1:9002 9002}
http: {http://192.168.100.1:8002 8002}
lan_ipv4: {192.168.100.1 9002}
wan_ipv4: {192.168.100.1 9002}
===
*/
var service = randomService(services)
_ = service
_ = backendScheme
var originalURL = req.URL.String()
// 组装服务地址
req.URL.Scheme = backendScheme
switch backendScheme {
case "http":
if v, exists := service.ServiceTaggedAddresses["http"]; exists {
req.URL.Host = v.Address[7:] // 忽略掉协议头
} else {
log.Error("%v不存在http服务", service.ServiceName)
return
}
/*
case "grpc":
if v, exists := service.ServiceTaggedAddresses["grpc"]; exists {
req.URL.Host = v.Address[7:] // 忽略掉协议头
} else {
log.Error("%v不存在grpc服务", service.ServiceName)
return
}
*/
default:
log.Errorf("不支持转发%v服务", backendScheme)
return
}
req.URL.Path = fmt.Sprintf("/%v/%v", methodName, strings.Join(pathParameter, "/"))
log.Infof("%v -> [SVC-NAME: %v| SVC-ID: %v]%v",
originalURL, service.ServiceName, service.ServiceID, req.URL.String())
}
return &httputil.ReverseProxy{
Director: director,
}
}- 把grpc Host那部分注释起来,因为grpc方法调用方式和http路由调用不一样,先注释起来以免出问题
在上述代码中,反向转发处理时,我们只是根据请求中的服务名直接转发;如果我们需要对外屏蔽服务名,这样的路由转发规则显然是不够的。我们需要增加路由配置的多样性,可以抽出路由配置层,根据指定的规则进行路由转发,如配置名称、头部的信息、请求的参数和请求的body等转发到指定的服务
编写main方法
main方法的主要任务是创建Consul连接对象、创建日志记录对象和开启反向代理HTTP服务
func main() {
consulConfig := api.DefaultConfig()
{
consulConfig.Address = consulAddr
consulConfig.Scheme = consulScheme
}
consulClient, err := api.NewClient(consulConfig)
if err != nil {
h.Error(err)
os.Exit(1)
}
// 创建反向代理
proxy := NewReverseProxyV1(consulClient, h)
var sig = make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
// 启动http监听
var addr = "127.0.0.1:9090"
var srv = &http.Server{Addr: addr, Handler: proxy}
go func() {
log.Infof("start http server listening %s", addr)
_ = srv.ListenAndServe()
}()
// 协程准备关闭服务
<-sig
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go func() {
_ = srv.Shutdown(ctx)
println("exit~")
os.Exit(0)
}()
<-ctx.Done()
println("exit deadline exceeded~")
os.Exit(1)
}完整实现代码
import (
"context"
"flag"
"fmt"
"math/rand"
"net/http"
"net/http/httputil"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-kratos/kratos/v2/log"
"github.com/hashicorp/consul/api"
)
var (
consulAddr = "127.0.0.1:8500"
consulScheme = "http"
logger = log.NewStdLogger(os.Stdout)
h *log.Helper
)
func init() {
// consul服务发现中心地址
flag.StringVar(&consulAddr, "addr", consulAddr, "eg: -addr 127.0.0.1:8500")
flag.StringVar(&consulScheme, "scheme", consulScheme, "eg: -scheme http")
// 配置日志器
logger = log.With(logger, "ts", log.DefaultTimestamp, "caller", log.DefaultCaller)
h = log.NewHelper(logger)
}
func main() {
flag.Parse()
consulConfig := api.DefaultConfig()
{
consulConfig.Address = consulAddr
consulConfig.Scheme = consulScheme
}
consulClient, err := api.NewClient(consulConfig)
if err != nil {
h.Error(err)
os.Exit(1)
}
// 创建反向代理
proxy := NewReverseProxyV1(consulClient, h)
var sig = make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
// 启动http监听
var addr = "127.0.0.1:9090"
var srv = &http.Server{Addr: addr, Handler: proxy}
go func() {
log.Infof("start http server listening %s", addr)
_ = srv.ListenAndServe()
}()
// 协程准备关闭服务
<-sig
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go func() {
_ = srv.Shutdown(ctx)
println("exit~")
os.Exit(0)
}()
<-ctx.Done()
println("exit deadline exceeded~")
os.Exit(1)
}
func NewReverseProxyV1(client *api.Client, log *log.Helper) *httputil.ReverseProxy {
var searchService = func(name string) (res []*api.CatalogService) {
result, _, err := client.Catalog().Service(name, "", &api.QueryOptions{})
if err != nil {
log.Error(err)
return res
}
res = result
return res
}
var randomService = func(services []*api.CatalogService) *api.CatalogService {
var randomID = rand.Intn(len(services))
return services[randomID]
}
var backendScheme = "http"
director := func(req *http.Request) {
// 获取原始URL并切割
var url = req.URL.String()
var parts = strings.Split(url, "/")
if len(parts) < 1 || parts[1] == "" {
log.Error("服务实例名称不可为空")
return
}
var serviceName = parts[1] // 约定第二个参数是服务实例名称; 第一个参数为请求来源
services := searchService(serviceName)
if len(services) == 0 {
log.Errorf("未找到服务实例[%v]", serviceName)
return
}
var methodName = parts[2] // 约定第三个参数是方法名
var pathParameter = parts[3:] // 剩余参数是路径参数
/*
for k, service := range services {
fmt.Printf("服务实例[%v] %v | %v | %v:%v\n",
k, service.ServiceID, service.ServiceName, service.ServiceAddress, service.ServicePort)
}
服务实例[0] srv-1 | helloworld-srv | 192.168.100.1:9001
服务实例[1] srv-2 | helloworld-srv | 192.168.100.1:9002
*/
/*
for k, service := range services {
fmt.Printf("服务实例[%v] %v | %v ===\n",
k, service.ServiceID, service.ServiceName)
for k, v := range service.ServiceTaggedAddresses {
fmt.Printf(" %v: %v\n", k, v)
}
fmt.Printf("===\n")
}
服务实例[0] srv-1 | helloworld-srv ===
wan_ipv4: {192.168.100.1 9001}
grpc: {grpc://192.168.100.1:9001 9001}
http: {http://192.168.100.1:8001 8001}
lan_ipv4: {192.168.100.1 9001}
===
服务实例[1] srv-2 | helloworld-srv ===
grpc: {grpc://192.168.100.1:9002 9002}
http: {http://192.168.100.1:8002 8002}
lan_ipv4: {192.168.100.1 9002}
wan_ipv4: {192.168.100.1 9002}
===
*/
var service = randomService(services)
_ = service
_ = backendScheme
var originalURL = req.URL.String()
// 组装服务地址
req.URL.Scheme = backendScheme
switch backendScheme {
case "http":
if v, exists := service.ServiceTaggedAddresses["http"]; exists {
req.URL.Host = v.Address[7:] // 忽略掉协议头
} else {
log.Error("%v不存在http服务", service.ServiceName)
return
}
/*
case "grpc":
if v, exists := service.ServiceTaggedAddresses["grpc"]; exists {
req.URL.Host = v.Address[7:] // 忽略掉协议头
} else {
log.Error("%v不存在grpc服务", service.ServiceName)
return
}
*/
default:
log.Errorf("不支持转发%v服务", backendScheme)
return
}
req.URL.Path = fmt.Sprintf("/%v/%v", methodName, strings.Join(pathParameter, "/"))
log.Infof("%v -> [SVC-NAME: %v| SVC-ID: %v]%v",
originalURL, service.ServiceName, service.ServiceID, req.URL.String())
}
return &httputil.ReverseProxy{
Director: director,
}
}

可以看到两边都会有反应
可以做的优化
细化传输层配置
// 这是一个典型的高并发优化补丁
var defaultTransport = &http.Transport{
MaxIdleConns: 1000, // 最大空闲连接数,防止频繁握手
MaxIdleConnsPerHost: 100, // 每个目标主机的最大空闲连接
IdleConnTimeout: 90 * time.Second,
}
proxy.Transport = defaultTransport使用Kratos自带的负载均衡发现
遇到的问题
错把HTTP/1.1请求发到HTTP/2服务上
INFO ts=2026-03-04T16:50:13+08:00 caller=reverse_proxy_demo/main.go:130 msg=/helloworld-srv/helloworld/kratos -> [SVC-NAME: helloworld-srv| SVC-ID: srv-1]http://192.168.100.1:9001/helloworld/kratos
2026/03/04 16:50:13 http: proxy error: net/http: HTTP/1.x transport connection broken: malformed HTTP response "\x00\x00\x06\x04\x00\x00\x00\x00\x00\x00\x05\x00\x00@\x00"Kratos的HTTP服务确实还是HTTP/1.1的,但consul API返回的service.ServiceAddress是RPC服务端口
有必要的话可以去service.ServiceTaggedAddresses里翻
Kratos的HTTP Server也支持直接注册中转逻辑
// 在网关服务中,直接把 helloworld 的 gRPC 客户端挂载到 HTTP 路由上
conn, _ := grpc.DialInsecure(
context.Background(),
grpc.WithEndpoint("discovery:///helloworld-srv"),
grpc.WithDiscovery(r), // r 是你的 Consul 实例
)
client := v1.NewGreeterClient(conn)
// Kratos 的 HTTP Server 支持直接注册这种“中转”逻辑
route := http.NewServer(http.Address(":9090"))
v1.RegisterGreeterHTTPServer(route, &proxyService{client: client})活用Kratos(本身就是一个网关)的模块
应用Kratos的认证模块
更新conf.proto定义
- 在
HTTP与GRPC定义中增加secret定义,用于JWT的密钥
message Server {
message HTTP {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
string secret = 4; // 用于JWT密钥
}
message GRPC {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
string secret = 4;
}
HTTP http = 1;
GRPC grpc = 2;
}更新HTTP与RPC Server定义
- 注册JWT中间件服务
- 需要引入JWT依赖;这里选用
jwtv5 "github.com/golang-jwt/jwt/v5"
// NewHTTPServer new an HTTP server.
func NewHTTPServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *http.Server {
// 中间件
var middlewares = []middleware.Middleware{recovery.Recovery(), logging.Server(logger)}
// JWT服务器配置
if c.Http.Secret != "" {
middlewares = append(middlewares, jwt.Server(func(token *jwtv5.Token) (any, error) {
return []byte(c.Http.Secret), nil
}))
}
var opts = []http.ServerOption{
http.Middleware(middlewares...),
}
// ...
srv := http.NewServer(opts...)
// ...
return srv
}// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, callItself *service.CallItselfService, logger log.Logger) *grpc.Server {
var middlewares = []middleware.Middleware{recovery.Recovery(), logging.Server(logger)}
if c.Grpc.Secret != "" {
middlewares = append(middlewares, jwt.Server(func(token *jwtv5.Token) (any, error) {
return []byte(c.Grpc.Secret), nil
}))
}
var opts = []grpc.ServerOption{
grpc.Middleware(middlewares...),
}
// ...
srv := grpc.NewServer(opts...)
// ...
return srv
}显式刷新依赖注入代码
wire添加认证API用于测试
添加user.proto定义和user_error定义
kratos proto add api/user/v1/user.proto
kratos proto client api/user/v1/user.proto
kratos proto server api/user/v1/user.proto -t internal/serviceerrors生成工具需要额外安装
# 如果电脑中没有protoc-gen-go需要先安装
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latestMake CLI工具也有概率没有errors这个参数,需要自己补上
.PHONY: errors
# generate errors proto
errors:
protoc --proto_path=. \
--proto_path=./third_party \
--go_out=paths=source_relative:. \
--go-errors_out=paths=source_relative:. \
$(API_PROTO_FILES)扩展conf.proto定义以使用自定义业务Error
添加Ent数据库依赖并创建user模型
参考资料
# 创建实体Scheme
ent new User
# 编辑好字段和关系之后, 生成数据库定义相关代码
go generate ./ent// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
// Ent 默认使用Int自增主键; 但也可以显式覆盖
field.UUID("id", uuid.UUID{}).Default(uuid.New),
field.String("username").NotEmpty().Unique(),
field.String("password").NotEmpty(),
field.String("note").Default("He has no note"),
field.Time("created_at").Default(time.Now),
field.Time("updated_at").Default(time.Now),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return nil
}创建数据库连接客户端并更新依赖注入
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo, NewGreeterRPCClient)
// Data .
type Data struct {
// wrapped database client
// DB
//
// gorm数据库实例
DB *gorm.DB
/*
Client
Ent创建的数据库客户端连接, 与DB互相独立
*/
Client *ent.Client
}
/*
newSqliteDataWithEnt
使用Ent框架创建Sqlite数据库连接, 并调用迁移工具创建表结构、索引、关系等等
采用与TestCreateUser函数相同的连接逻辑,使用标准库sql.Open和_pragma参数
*/
func newSqliteDataWithEnt(c *conf.Data) (data *Data, err error) {
switch strings.ToLower(c.Database.GetDriver()) {
case "sqlite":
// 使用标准库sql.Open创建数据库连接,添加_pragma参数启用外键
connStr := c.Database.GetSource()
if !strings.Contains(connStr, "_pragma=foreign_keys") {
// 如果连接字符串中没有外键设置,则添加默认配置
if strings.Contains(connStr, "?") {
connStr += "&_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)"
} else {
connStr += "?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)"
}
}
db, err := sql.Open("sqlite", connStr)
if err != nil {
return nil, fmt.Errorf("failed to open sqlite connection: %v", err)
}
// 验证外键是否启用
var fkEnabled int
err = db.QueryRow("PRAGMA foreign_keys;").Scan(&fkEnabled)
if err != nil {
log.Errorf("查询外键状态失败: %v", err)
} else {
// log.Infof("外键启用状态: %d", fkEnabled)
}
drv := entsql.OpenDB(dialect.SQLite, db)
client := ent.NewClient(ent.Driver(drv))
if err = client.Schema.Create(context.Background()); err != nil {
return nil, fmt.Errorf("failed creating schema resources: %v", err)
}
return &Data{Client: client}, nil
case "sqlite3":
client, err := ent.Open(dialect.SQLite, c.Database.GetSource())
if err != nil {
return nil, fmt.Errorf("failed to open sqlite3 connection: %v", err)
}
if err = client.Schema.Create(context.Background()); err != nil {
return nil, fmt.Errorf("failed creating schema resources: %v", err)
}
return &Data{Client: client}, nil
}
return nil, err
}# 在项目的main包中运行
wire编写user服务所需要的数据库逻辑
实现user服务
更新依赖注入
依次更新:
data层的ProviderSetservice层的ProviderSet- 在
server层中的http.go和grpc.go里显式注册服务- 为了签发JWT Token,需要添加
metadata.Server()中间件以启用元信息传递
使用github.com/go-kratos/kratos/v2/middleware/metadata库 - HTTP服务处理元信息的方式不太一样,需要自定义中间件
- 为了签发JWT Token,需要添加
- 在
main层中更新wire.go的func wire(...)函数参数,传入新的配置(如哈希配置c.Hash)- 下游HTTP和GRPC服务需要显式传入新的配置
- 在
main层中更新wire函数的调用,把配置传进去 - 在项目的
main包中运行wire刷新依赖注入代码
注意:
- Kratos日志中间件的默认实现会把请求参数全量打印出来,可能造成明文密码泄露
jwt.FromContext需要配合jwt.NewContext使用,而后者建议在中间件里使用,从metadata.FromServerContext里拿数据一个一个断言jwt.MapClaimsuser服务里耦合了JWT配置进去,其实有点过度设计了- Kratos的metadata server默认实现似乎会拦截任何请求
Kratos的JWT默认实现
- 默认使用HS256签名算法
- 携带Token的请求头需设置为
Authorization - 请求头内容需为
xxx <TOKEN>- 程序使用空格
作为分隔符,切割两段出来,取后一段为JWT
- 程序使用空格
- 默认拦截所有请求路径,拿不到Token的话就会abort掉请求
// kratos/middleware/auth/jwt/jwt.go
// Server is a server auth middleware. Check the token and extract the info from token.
func Server(keyFunc jwt.Keyfunc, opts ...Option) middleware.Middleware {
o := &options{
signingMethod: jwt.SigningMethodHS256,
}
for _, opt := range opts {
opt(o)
}
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req any) (any, error) {
if header, ok := transport.FromServerContext(ctx); ok {
if keyFunc == nil {
return nil, ErrMissingKeyFunc
}
auths := strings.SplitN(header.RequestHeader().Get(authorizationKey), " ", 2)
if len(auths) != 2 || !strings.EqualFold(auths[0], bearerWord) {
return nil, ErrMissingJwtToken
}
jwtToken := auths[1]
var (
tokenInfo *jwt.Token
err error
)
if o.claims != nil {
tokenInfo, err = jwt.ParseWithClaims(jwtToken, o.claims(), keyFunc)
} else {
tokenInfo, err = jwt.Parse(jwtToken, keyFunc)
}
if err != nil {
if errors.Is(err, jwt.ErrTokenMalformed) || errors.Is(err, jwt.ErrTokenUnverifiable) {
return nil, ErrTokenInvalid
}
if errors.Is(err, jwt.ErrTokenNotValidYet) || errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrTokenExpired
}
return nil, ErrTokenParseFail
}
if !tokenInfo.Valid {
return nil, ErrTokenInvalid
}
if tokenInfo.Method != o.signingMethod {
return nil, ErrUnSupportSigningMethod
}
ctx = NewContext(ctx, tokenInfo.Claims)
return handler(ctx, req)
}
return nil, ErrWrongContext
}
}
}``
Kratos的监控模块
Kratos的链路追踪模块/链路追踪中间件
Kratos Tracing 中间件使用 OpenTelemetry 实现了链路追踪
拓展阅读:
OpenTelemetry的组件构造
Proto (规范):定义了数据长什么样,确保不同的语言(Go, Java, Python)产生的数据能互相兼容。
SDK (代码库):你在 Kratos 项目里引入的依赖。它负责在内存中收集 Trace 数据、进行采样(Sampling),并处理数据的打包。
Collector (收集器):一个独立的进程。它像一个“中转站”,接收来自各个微服务的 OTel 数据,进行过滤、聚合,然后再推送到后端的存储系统(如 Jaeger 或 ClickHouse)。
Exporter (导出器):SDK 的一部分。它决定了数据发送到哪里。比如
otlptracegrpc会把数据发给 Collector,或者直接发给 Jaeger。
OpenTelemetry镜像拉取事宜
由于OpenTelemetry官方正在迁移镜像,Docker Desktop已无法拉取到旧镜像,请前往ghcr.io获取新的镜像源:
# 拉取 2026 年目前的稳定版本
docker pull ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.147.0在服务端配置链路采集
import (
"context"
v1 "helloworld/api/helloworld/v1"
userv1 "helloworld/api/user/v1"
"helloworld/internal/conf"
"helloworld/internal/service"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/middleware/logging"
"github.com/go-kratos/kratos/v2/middleware/metadata"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/transport/grpc"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
resource "go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
// Optl依赖
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
)
// 设置全局tracer
func initGlobalTracer(endpoint string) error {
log.Infof("正在初始化链路追踪组件")
// 创建Otlp Exporter
exporter, err := otlptracehttp.New(
context.Background(),
otlptracehttp.WithInsecure(),
otlptracehttp.WithEndpoint(endpoint),
)
if err != nil {
return err
}
// 设置Exporter参数
tp := trace.NewTracerProvider(
// 将父级span的采样倍率设置为100%
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(1.0))),
// 始终确保在生产中批量处理
trace.WithBatcher(exporter),
trace.WithResource(resource.NewSchemaless(
semconv.ServiceNameKey.String("kratos-trace"),
attribute.String("exporter", "otlp"),
// attribute.Float64("float", 312.23), // 这行只是演示用的
),
),
)
otel.SetTracerProvider(tp)
log.Infof("链路追踪组件初始化完成")
return nil
}
// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Server,
greeter *service.GreeterService, callItself *service.CallItselfService, user *service.UserService,
logger log.Logger) *grpc.Server {
var middlewares = []middleware.Middleware{
recovery.Recovery(), logging.Server(logger), metadata.Server(),
}
// 添加链路追踪组件
if err := initGlobalTracer("localhost:4318"); err == nil {
middlewares = append(middlewares, tracing.Server())
log.Infof("链路追踪组件注册成功")
} else {
log.Errorf("链路追踪组件注册失败")
}
var opts = []grpc.ServerOption{
grpc.Middleware(middlewares...),
}
/* Funcion Option Pattern调用 */
srv := grpc.NewServer(opts...)
/* grpc服务注册 */
return srv
}与otel-collector、Jaeger协同运行
# docker/optl/docker-compose.yml
services:
# 1. 链路追踪展示后端 (Jaeger)
jaeger:
image: jaegertracing/all-in-one:1.60
ports:
- "16686:16686" # UI 界面
- "4317:4317" # OTLP gRPC 接收端口 (如果 Collector 直连)
# 2. OTel 收集器 (中转站)
otel-collector:
image: ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.147.0
container_name: otel-collector
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ../../otel-collector-config.yaml:/etc/otel-collector-config.yaml
# 配置文件在根目录下
ports:
# - "4317:4317" # 接收 Kratos 发来的数据 (gRPC)
- "4318:4318" # 接收 Kratos 发来的数据 (HTTP)
depends_on:
- jaegerotel采集器配置:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
# gRPC 协议配置(如果需要)
# grpc:
# endpoint: 0.0.0.0:4317
# 其他常用接收器(可根据需要启用)
# prometheus:
# config:
# scrape_configs:
# - job_name: 'otel-collector'
# scrape_interval: 10s
# static_configs:
# - targets: ['localhost:8888']
# jaeger:
# protocols:
# grpc:
# endpoint: 0.0.0.0:14250
# thrift_http:
# endpoint: 0.0.0.0:14268
# zipkin:
# endpoint: 0.0.0.0:9411
processors:
batch:
timeout: 1s
send_batch_size: 1000
memory_limiter:
check_interval: 5s
limit_percentage: 75
spike_limit_percentage: 15
# 其他常用处理器
# attributes:
# actions:
# - key: environment
# value: production
# action: insert
# resource:
# attributes:
# - key: service.name
# value: "my-service"
# action: upsert
# filter:
# traces:
# span: "attributes['http.status_code'] != 200"
# tail_sampling:
# policies:
# - name: error-sampling
# type: status_code
# status_code:
# status_codes: [ERROR]
# k8sattributes:
# auth_type: serviceAccount
# passthrough: false
# extract:
# metadata:
# - k8s.pod.name
# - k8s.namespace.name
# datadog:
# env: production
# service: my-service
# version: 1.0.0
exporters:
# 修复:将 jaeger 导出器替换为 otlp 导出器
# 原因:在新版本的 OpenTelemetry Collector 中,jaeger 类型的导出器已被移除
# Jaeger 现在原生支持 OTLP 协议,所以使用 otlp 导出器连接到 Jaeger
otlp:
endpoint: jaeger:4317
tls:
insecure: true
# 修复:根据 2026 年的 OpenTelemetry Collector 规范,将 logging 导出器改为 debug 导出器
# 原因:logging 已过时,debug 导出器功能更强大,支持 verbosity 参数
debug:
verbosity: detailed # 原来的 loglevel: info 对应这里的 verbosity
# 其他常用导出器(可根据需要启用)
# prometheus:
# endpoint: 0.0.0.0:8889
# zipkin:
# endpoint: "http://zipkin:9411/api/v2/spans"
# format: proto
# datadog:
# api:
# key: "YOUR_DATADOG_API_KEY"
# site: "datadoghq.com"
# awsxray:
# region: us-east-1
# otlp:
# endpoint: "otel-collector:4317"
# tls:
# insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp, debug] # 修复:使用 debug 替代 logging
# 其他管道(可根据需要启用)
# metrics:
# receivers: [otlp, prometheus]
# processors: [memory_limiter, batch]
# exporters: [prometheus, debug] # 如果启用 metrics 管道,也需要改成 debug
# logs:
# receivers: [otlp]
# processors: [memory_limiter, batch]
# exporters: [debug] # 如果启用 logs 管道,也需要改成 debug
# 服务配置
# telemetry:
# metrics:
# level: detailed
# logs:
# level: info
# 扩展(可根据需要启用)
# extensions:
# health_check:
# pprof:
# endpoint: 0.0.0.0:1777
# zpages:
# endpoint: 0.0.0.0:55679Kratos代码不变,只需要和Collector对接暴露/采集端口即可
Collector配置文件变动
- 修改Collector监听配置为
0.0.0.0:xxx- 原因:
v0.104.0开始Collector默认只监听本地端口
- 原因:
- 替换 Jaeger 导出器为 OTLP 导出器
- 原因: 新版本 OpenTelemetry Collector 中 jaeger 类型导出器已被移除
- 方案: 使用 otlp 导出器连接到 Jaeger(Jaeger 原生支持 OTLP 协议)
- 将 logging 导出器改为 debug 导出器
- 原因: logging 已过时, debug 导出器功能更强大
- 变更: 将 loglevel: info 改为 verbosity: detailed
- 更新 pipelines 引用
- 位置: service.pipelines.traces.exporters
- 变更: 将 logging 改为 debug ,确保配置与导出器定义一致

【附录】Consul API操作
简单查询已注册的服务
var client = consul.NewClient(...)
result, _, err := client.Catalog().Services(&api.QueryOptions{})
if err != nil {
log.Error(err)
os.Exit(1)
}
for service := range result {
log.Infof("service: %s", service)
}- 测试的时候注册了同名同ID的服务,后注册的会覆盖之前注册的
INFO ts=2026-03-04T15:15:02+08:00 caller=reverse_proxy_demo/main.go:52 msg=service: consul
INFO ts=2026-03-04T15:15:02+08:00 caller=reverse_proxy_demo/main.go:52 msg=service: helloworld-srv查询具体的服务实例
var serviceName = "srv-1"
var searchService = func(name string) (res []*api.CatalogService) {
result, _, err := client.Catalog().Service(name, "", &api.QueryOptions{})
if err != nil {
log.Error(err)
return res
}
res = result
return res
}
_ = searchService(serviceName)