Golang中gRPC微服务间通信的最佳实践建议

Golang中gRPC微服务间通信的最佳实践建议 编辑:
我一直在纠结如何提出这个问题,但这里有一个更简短直接的版本……
是应该让每个客户端向单一服务发送请求,由该服务完成一次认证后与其他所有服务通信,还是应该让客户端直接向每个微服务发送请求,由它们各自完成请求认证?

大家好!我正在尝试基于微服务创建一个独立研究的应用程序,但在设计过程中遇到了困难,特别是通信方面。我不会详细说明每个服务的功能,因为我主要困惑的是通信和认证问题。

目前项目非常简单。有一个网关服务器,用户连接到这里,并对连接进行认证(通过访问认证服务)。连接认证后,用户可以调用游戏服务器执行操作。这基本上就是一个小游戏;游戏服务器每33毫秒(30帧/秒)进行一次计算,API允许用户执行简单操作,比如移动角色或放置物体。

以下是架构图:
Olam%20Arch%20Diagrams-AgentFlow

这就是我困惑的地方。让网关接收请求并将这些操作传达给其他服务的最佳方式是什么?
以下是我的想法……

想法1:重定向
每个服务都有自己的gRPC API,因此它们会有自己的protobuf服务。这意味着我无法让网关接受针对其他服务的gRPC调用并仅重定向它们并返回结果(据我所知)。但是否有一种方法可以让调用通过网关以确保认证(包括加密和用户识别),然后转到正确的服务?

想法2:通用调用先到达网关,然后由网关处理其余部分
我还考虑为网关调用创建某种通用格式:

{ servicename, methodName, params... }

然后让网关解析这些内容,并对指定的服务执行正确的调用。这比重定向消息更有意义吗?

我非常希望得到任何建议,因为我刚开始用Go开发服务,并且对整个微服务的概念还很陌生。


更多关于Golang中gRPC微服务间通信的最佳实践建议的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

更多关于Golang中gRPC微服务间通信的最佳实践建议的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你不需要这个吗?我认为通过查看这个代码库,你可以为你的项目获得一些不错的想法。

据我理解,这仅仅是帮助创建一个RESTful JSON端点,该端点可以转换为gRPC,对吗?

我觉得我的问题表述得不太清楚,当时我自己也在努力理解到底想问什么。

是应该让每个客户端都向单一服务发送请求,由该服务统一完成认证后再与其他所有服务通信,还是应该让客户端直接向各个微服务发送请求,由它们各自对请求进行认证?

请参阅 https://www.nginx.com/blog/deploying-nginx-plus-as-an-api-gateway-part-3-publishing-grpc-services/
https://www.envoyproxy.io/ - 任何能够代理 HTTP/2 的组件同样可以代理 gRPC。

身份验证应通过 HTTP 头信息实现,位于协议缓冲区之外。

GitHub

grpc-ecosystem/grpc-gateway

遵循gRPC HTTP规范的gRPC到JSON代理生成器 - grpc-ecosystem/grpc-gateway

这取决于具体情况。

API网关通常对客户端有帮助,因为所有服务都通过该网关调用,它也可以进行身份验证。 同时它还能为上游提供一些有用的服务:注册、发现、认证、速率限制等等。 API网关本身也可以向客户端提供这些服务:列出已注册的服务及其.proto文件、端点、速率限制等。

我找到的最接近的是Ambassador(getambassador.io),但对我来说太重量级了:它需要Kubernetes和Envoy。 https://github.com/mwitkow/grpc-proxy 是一个gRPC代理(库),但表明这种高级代理很麻烦:你必须费很大劲才能不了解底层数据类型,只代理HTTP/2字节。

这意味着我认为带有服务注册功能的HTTP/2代理将是最佳选择。

在微服务架构中,gRPC通信和认证的设计确实是一个关键问题。根据你的描述,我建议采用网关集中认证和路由的方案,而不是让客户端直接访问每个微服务。以下是具体实现方案和代码示例:

推荐架构:网关统一认证 + gRPC服务间通信

1. 网关认证和路由设计

// gateway/main.go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
    
    "your-project/gateway/proto"
    "your-project/auth/proto"
    "your-project/game/proto"
)

type GatewayServer struct {
    proto.UnimplementedGatewayServer
    authClient auth.AuthServiceClient
    gameClient game.GameServiceClient
}

// 统一的gRPC调用接口
func (s *GatewayServer) CallService(ctx context.Context, req *proto.ServiceRequest) (*proto.ServiceResponse, error) {
    // 从上下文中提取认证信息
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "missing metadata")
    }

    // 验证用户身份
    userID, err := s.authenticateUser(md)
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "authentication failed")
    }

    // 根据服务名路由到对应的微服务
    ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("user-id", userID))
    
    switch req.ServiceName {
    case "game":
        return s.routeToGameService(ctx, req)
    case "auth":
        return s.routeToAuthService(ctx, req)
    default:
        return nil, status.Error(codes.Unimplemented, "unknown service")
    }
}

func (s *GatewayServer) authenticateUser(md metadata.MD) (string, error) {
    // 调用认证服务验证token
    tokens := md.Get("authorization")
    if len(tokens) == 0 {
        return "", status.Error(codes.Unauthenticated, "missing authorization token")
    }

    authReq := &auth.ValidateTokenRequest{Token: tokens[0]}
    authResp, err := s.authClient.ValidateToken(context.Background(), authReq)
    if err != nil {
        return "", err
    }
    
    return authResp.UserId, nil
}

func (s *GatewayServer) routeToGameService(ctx context.Context, req *proto.ServiceRequest) (*proto.ServiceResponse, error) {
    // 将通用请求转换为游戏服务的具体请求
    gameReq := &game.PlayerActionRequest{
        Action:   req.MethodName,
        Params:   req.Params,
    }
    
    gameResp, err := s.gameClient.ExecuteAction(ctx, gameReq)
    if err != nil {
        return nil, err
    }
    
    return &proto.ServiceResponse{
        Data: gameResp.Result,
    }, nil
}

2. Protobuf定义

// proto/gateway.proto
syntax = "proto3";

package gateway;

service Gateway {
  rpc CallService(ServiceRequest) returns (ServiceResponse);
}

message ServiceRequest {
  string service_name = 1;
  string method_name = 2;
  bytes params = 3;  // 可以是JSON或其他序列化格式
}

message ServiceResponse {
  bytes data = 1;
  string error = 2;
}

// proto/game.proto
syntax = "proto3";

package game;

service GameService {
  rpc ExecuteAction(PlayerActionRequest) returns (PlayerActionResponse);
}

message PlayerActionRequest {
  string action = 1;
  bytes params = 2;
}

message PlayerActionResponse {
  bytes result = 1;
}

3. 游戏服务实现

// game/main.go
package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    
    "your-project/game/proto"
)

type GameServer struct {
    proto.UnimplementedGameServiceServer
}

func (s *GameServer) ExecuteAction(ctx context.Context, req *proto.PlayerActionRequest) (*proto.PlayerActionResponse, error) {
    // 从上下文中获取用户ID(由网关传递)
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Error(codes.Unauthenticated, "missing user context")
    }
    
    userIDs := md.Get("user-id")
    if len(userIDs) == 0 {
        return nil, status.Error(codes.Unauthenticated, "missing user id")
    }
    
    userID := userIDs[0]
    
    // 处理游戏逻辑
    result, err := s.handleGameAction(userID, req.Action, req.Params)
    if err != nil {
        return nil, err
    }
    
    return &proto.PlayerActionResponse{
        Result: result,
    }, nil
}

func (s *GameServer) handleGameAction(userID, action string, params []byte) ([]byte, error) {
    // 具体的游戏逻辑处理
    switch action {
    case "move":
        return s.handleMove(userID, params)
    case "place":
        return s.handlePlace(userID, params)
    default:
        return nil, status.Error(codes.InvalidArgument, "unknown action")
    }
}

4. 客户端调用示例

// 客户端代码
func callGameService() error {
    conn, err := grpc.Dial("gateway:50051", grpc.WithInsecure())
    if err != nil {
        return err
    }
    defer conn.Close()
    
    client := gateway.NewGatewayClient(conn)
    
    // 设置认证token
    ctx := metadata.NewOutgoingContext(context.Background(), 
        metadata.Pairs("authorization", "user-token-here"))
    
    // 构造通用请求
    params, _ := json.Marshal(map[string]interface{}{
        "x": 10,
        "y": 20,
    })
    
    req := &gateway.ServiceRequest{
        ServiceName: "game",
        MethodName:  "move",
        Params:      params,
    }
    
    resp, err := client.CallService(ctx, req)
    if err != nil {
        return err
    }
    
    // 处理响应
    var result map[string]interface{}
    json.Unmarshal(resp.Data, &result)
    
    return nil
}

优势分析

这种架构的优势:

  1. 统一认证:所有请求都经过网关认证,避免在每个服务中重复认证逻辑
  2. 服务隔离:内部服务不需要暴露给外部网络
  3. 可扩展性:新增服务只需在网关中添加路由逻辑
  4. 安全性:用户凭证只在网关处理,内部服务使用用户ID进行授权

这种设计符合你的"想法2",但提供了更具体的gRPC实现方案。网关作为统一的入口点,负责认证和请求路由,而各个微服务专注于业务逻辑实现。

回到顶部