你的Go微服务还在用"笨重"的HTTP/JSON做内部通信吗?当网关成为瓶颈,我决定向它开炮!本文将以开源项目
easyms.golang为例,完整复盘一次从HTTP到gRPC的性能优化实战。我们将从定义.proto契约开始,手把手带你实现HTTP/gRPC双协议并存 ,并用gRPC-Gateway优雅地连接两个世界。这不只是一次技术升级,更是一次解决真实性能瓶颈的架构演进全过程!🚀
大家好,今天有点忙,太晚了才想起明天发布的文章还没有写,今天我们基于grpc的改造为题展示Easyms 是如何切入grpc的全过程。基于 Grpc 几个月前一篇文章就有详细的写,但是总感觉缺点什么,那今天来补上。
在上一篇关于日志模块的文章中,我们聊了如何避免日志系统成为服务的瓶颈。今天,我们来聊一个更"硬核"的话题:性能压榨。
在easyms.golang这个微服务项目中,API网关承担了所有入口流量的路由、认证和转发。随着业务量的增长,我们通过监控发现,网关在验证Token时对auth-svc(认证服务)的HTTP调用,成了整个系统的性能热点。
每一次请求,网关都要和auth-svc进行一次这样的交互:
- 建立HTTP连接。
- 网关将Token序列化为JSON。
auth-svc接收请求,反序列化JSON。auth-svc处理完毕,将结果序列化为JSON。- 网关接收响应,反序列化JSON。
在这个过程中,JSON的序列化/反序列化开销 和HTTP 1.1的连接管理在高并发下都显得非常"笨重"。
有没有一种更快、更高效的方式来进行内部服务间的通信?答案是肯定的:gRPC。
于是,我决定对auth-svc和网关进行一次"外科手术式"的升级。
第一步:立下契约,一切的基石 (.proto)
从HTTP迁移到gRPC,第一步不是写代码,而是定义契约。我们需要用Protocol Buffers (Protobuf)来精确地描述我们的服务、方法和数据结构。这份契约将成为后续所有工作的"单一事实来源"。
1. 定义.proto文件
我们在项目根目录下创建了api/proto/auth/auth.proto文件。这个文件将成为auth-svc所有接口的"单一事实来源"。
2. 定义服务与消息
我们将auth-svc最核心的VerifyToken功能,用Protobuf的格式重新定义。
protobuf
// api/proto/auth/auth.proto
syntax = "proto3";
package auth;
option go_package = "easyms.golang/api/proto/auth";
// --- 消息体定义 ---
message VerifyRequest {
string token = 1;
}
message VerifyResponse {
bool valid = 1;
// ... 可以包含用户信息等
}
// --- 服务定义 ---
service AuthService {
// 对应 /oauth2/verify 接口
rpc VerifyToken(VerifyRequest) returns (VerifyResponse);
}
3. 生成Go代码
定义好.proto文件后,我们使用protoc编译器来生成Go代码。
sh
protoc --proto_path=. \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
api/proto/auth/auth.proto
这个命令会在api/proto/auth/目录下生成auth.pb.go(包含了消息体的Go结构体)和auth_grpc.pb.go(包含了gRPC客户端和服务端接口)。
顺便这里讲一下,如果你也报错,可以试着这样处理。
这是grpc 环境的坑,可能在这里会出现,平时我是在 Macos 或者是 wsl 中一切顺利,而且还用了buf工具,感觉挺爽的,今天在windows 台式机上却遇到了坑,也许是过多语言环境,整得有些乱了,再加上今天wsl正好罢工了。好在自己还熟悉 protoc。最终在 windows 下的解决方案:
go
#下载地址:https://github.com/protocolbuffers/protobuf/releases/
#拷贝到 $(go env GOMODCACHE) 如:D:/app/go/
protoc -I . \
-I third_party/googleapis \
-I "D:/app/go/include" \
--go_out . --go_opt paths=source_relative \
--go-grpc_out . --go-grpc_opt paths=source_relative \
api/proto/auth/auth.proto
第二步:无痛升级auth-svc,支持双协议
直接用gRPC替换掉HTTP接口是危险的,因为可能还有其他服务或测试依赖于旧的接口。因此,我们的策略是让HTTP和gRPC并存。
1. 实现gRPC服务
在internal/services/auth/internal/service/目录下,我们创建了一个grpc_server.go。它会实现由protoc生成的AuthServiceServer接口。
最棒的是,它可以完全复用我们现有的TokenService业务逻辑。这就是为什么我要分层的原因。
go
// internal/services/auth/internal/service/grpc_server.go
package service
import (
pb "easyms.golang/api/proto/auth"
// ...
)
// grpcServer 实现了 AuthService gRPC 服务。
type grpcServer struct {
pb.UnimplementedAuthServiceServer
tokenService TokenService // <-- 复用已有的业务逻辑
}
func (s *grpcServer) VerifyToken(ctx context.Context, req *pb.VerifyRequest) (*pb.VerifyResponse, error) {
// 直接调用已有的 tokenService
_, err := s.tokenService.GetOAuth2DetailsByAccessToken(req.Token)
if err != nil {
return &pb.VerifyResponse{Valid: false}, nil
}
return &pb.VerifyResponse{Valid: true}, nil
}
2. 在main.go中同时启动两个服务
我们修改auth-svc的main.go,让它在一个新的goroutine中启动gRPC服务器,同时在主goroutine中启动原有的Gin HTTP服务器。
go
// internal/services/auth/cmd/authsvc/main.go
func main() {
// ... 原有的依赖注入 ...
// 启动 gRPC 服务器 (在goroutine中)
go func() {
grpcPort := appConfig.Server.Port + 10000 // gRPC端口
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort))
// ...
s := grpc.NewServer()
pb.RegisterAuthServiceServer(s, service.NewGrpcServer(...)) // 注册gRPC服务
s.Serve(lis)
}()
// 启动 HTTP 服务 (在主goroutine中)
g := gin.Default()
// ...
g.Run(...)
}
现在,auth-svc已经是一个强大的"双引擎"服务了!它既能响应旧的HTTP验证请求,也能处理新的gRPC验证请求。
第三步:升级网关,打通任督二脉
万事俱备,只欠东风。最后一步就是改造API网关(gateway),让它在验证Token时,使用gRPC。
1. 创建gRPC客户端
在gateway.go的NewGateway函数中,我们初始化一个到auth-svc的gRPC连接。
go
// internal/platform/gateway/gateway.go
type Gateway struct {
// ...
authSvcClient pb.AuthServiceClient // 新增gRPC客户端
}
func NewGateway(sd *discovery.ServiceDiscovery) *Gateway {
// ...
// 创建到 auth-svc 的 gRPC 连接
// 注意:这里的地址应该通过服务发现动态获取,并配置负载均衡
// 为了演示,我们先硬编码地址
authSvcAddr := "localhost:20000" // 假设 auth-svc 的 gRPC 端口是 20000
conn, err := grpc.Dial(authSvcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Failed to connect to auth-svc via gRPC: %v", err)
}
gateway := &Gateway{
// ...
authSvcClient: pb.NewAuthServiceClient(conn),
}
return gateway
}
2. 改造AuthMiddleware
这是最激动人心的一步!我们将AuthMiddleware中原来的httpClient.Do调用,替换为gRPC客户端调用。
go
// internal/platform/gateway/gateway.go
func (g *Gateway) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ... 省略获取tokenStr的逻辑 ...
if _, found := g.authCache.Get(tokenStr); found {
next.ServeHTTP(w, r)
return
}
// -----------------------------------------
// ↓↓↓↓↓↓↓↓ 核心改造点 ↓↓↓↓↓↓↓↓
// -----------------------------------------
// 旧的HTTP调用方式 (将被替换)
// verifyURL := fmt.Sprintf("http://%s/oauth2/verify", authTarget)
// req, _ := http.NewRequestWithContext(r.Context(), "POST", verifyURL, nil)
// ...
// resp, err := g.httpClient.Do(req)
// 新的gRPC调用方式
resp, err := g.authSvcClient.VerifyToken(r.Context(), &pb.VerifyRequest{Token: tokenStr})
// -----------------------------------------
// ↑↑↑↑↑↑↑↑ 核心改造点 ↑↑↑↑↑↑↑↑
// -----------------------------------------
if err != nil || !resp.Valid {
// ... 错误处理 ...
return
}
g.authCache.Set(tokenStr, true, 5*time.Minute)
next.ServeHTTP(w, r)
})
}
改造完成!现在,网关和认证服务之间最频繁的Token验证操作,已经从"笨重"的HTTP/JSON切换到了"轻快"的gRPC/Protobuf。
额外惊喜:用gRPC-Gateway统一API
在改造过程中,我们还顺便解决了另一个问题:如何为新的gRPC接口提供一个RESTful风格的HTTP端点?
答案就是 gRPC-Gateway 。通过在.proto文件中添加HTTP注解,并运行protoc的grpc-gateway插件,我们可以自动生成一个反向代理,将HTTP请求无缝转为gRPC调用。
protobuf
// api/proto/auth/auth.proto
import "google/api/annotations.proto";
service AuthService {
rpc GrantToken(TokenRequest) returns (TokenResponse) {
// 将 HTTP POST /v1/auth/token 映射到这个gRPC方法
option (google.api.http) = {
post: "/v1/auth/token"
body: "*"
};
}
}
然后在auth-svc的main.go中将它作为路由挂载到Gin上,我们就同时拥有了一个现代化的gRPC接口和一个符合RESTful规范的HTTP接口,而这两者背后都是同一套业务逻辑!
总结:我们得到了什么?
通过这次"外科手术式"的升级,easyms.golang项目获得了一个面向未来、可平滑演进的架构:
- 性能提升 🚀:内部服务间最核心的通信路径切换到gRPC,获得了更高的性能和更低的时延。
- 统一契约 📜: Protobuf成为了服务接口的"单一事实来源",API管理变得前所未有的清晰和规范。
- 向后兼容 🤝: 保留了原有的HTTP接口,对现有客户端完全透明,实现了无痛升级。
- 渐进迁移 👣: 我们可以按照这个模式,将gRPC逐步推广到项目的其他服务中,而无需一次性重构整个系统。
从一个纯HTTP架构,到一个HTTP/gRPC双协议并存的现代化微服务架构,这不仅仅是一次技术升级,更是一次解决真实性能瓶颈的架构演进。
如果你想深入了解完整的实现细节,或者对easyms.golang这个微服务框架感兴趣,欢迎访问项目的源码地址,给我一个 Star ⭐!
- GitHub : github.com/louis-xie-p...
- Gitee : gitee.com/louis_xie/e...
你的项目是否也面临着从HTTP迁移到RPC的挑战?你选择了gRPC还是其他框架?欢迎在评论区分享你的经验和看法!👇