“老板,我的接口性能还能再快一倍!” — Go微服务gRPC升级实战

你的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进行一次这样的交互:

  1. 建立HTTP连接。
  2. 网关将Token序列化为JSON。
  3. auth-svc接收请求,反序列化JSON。
  4. auth-svc处理完毕,将结果序列化为JSON。
  5. 网关接收响应,反序列化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-svcmain.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.goNewGateway函数中,我们初始化一个到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注解,并运行protocgrpc-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-svcmain.go中将它作为路由挂载到Gin上,我们就同时拥有了一个现代化的gRPC接口和一个符合RESTful规范的HTTP接口,而这两者背后都是同一套业务逻辑!

总结:我们得到了什么?

通过这次"外科手术式"的升级,easyms.golang项目获得了一个面向未来、可平滑演进的架构:

  • 性能提升 🚀:内部服务间最核心的通信路径切换到gRPC,获得了更高的性能和更低的时延。
  • 统一契约 📜: Protobuf成为了服务接口的"单一事实来源",API管理变得前所未有的清晰和规范。
  • 向后兼容 🤝: 保留了原有的HTTP接口,对现有客户端完全透明,实现了无痛升级。
  • 渐进迁移 👣: 我们可以按照这个模式,将gRPC逐步推广到项目的其他服务中,而无需一次性重构整个系统。

从一个纯HTTP架构,到一个HTTP/gRPC双协议并存的现代化微服务架构,这不仅仅是一次技术升级,更是一次解决真实性能瓶颈的架构演进。

如果你想深入了解完整的实现细节,或者对easyms.golang这个微服务框架感兴趣,欢迎访问项目的源码地址,给我一个 Star ⭐!


你的项目是否也面临着从HTTP迁移到RPC的挑战?你选择了gRPC还是其他框架?欢迎在评论区分享你的经验和看法!👇

相关推荐
王中阳Go2 小时前
全面解析Go泛型:从1.18到最新版本的演进与实践
后端·面试·go
oak隔壁找我2 小时前
Java ThreadLocal详解:原理、应用与最佳实践
后端
woniu_maggie2 小时前
SAP暂估科目自动清账
后端
rannn_1112 小时前
【SQL题解】力扣高频 SQL 50题|DAY4
数据库·后端·sql·leetcode·题解
isyuah2 小时前
Miko v0.7 发布:我写的一个 Rust Web 框架,虽然还是个玩具
后端·rust
isyuah2 小时前
Miko 框架系列(十四):集成测试
后端·rust
代码笔耕2 小时前
我们这样设计消息中心,解决了业务反复折腾的顽疾
java·后端·架构
chenyuhao20242 小时前
Linux系统编程:多线程同步与单例模式
linux·服务器·c++·后端·单例模式
唐装鼠2 小时前
Rust Turbofish 语法详解(deepseek)
开发语言·后端·rust