红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【杂项】一个微服务是怎么跑起来的

下面以一个调用CreatePayment创建支付的情景为例,描述一整个微服务在项目中的工作过程

一次微服务的调用的完整链路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
HTTP请求

 routes.go

 CreatePaymentHandler

 CreatePaymentLogic

 svcCtx.PaymentClient

 goctl生成的paymentservice客户端包装

 protobuf生成的gRPC Client

 gRPC序列化 + HTTP/2网络传输

 payment-rpc的gRPC Server

 protobuf生成的Server Handler

 goctl生成的PaymentServiceServer

 CreatePaymentLogic

 数据库、支付宝

 响应沿原路返回

代码生成

一般项目中会有两套接口:

定义文件 作用 通信方向
.api 定义对前端暴露的HTTP接口 前端→App网关
.proto 定义内部gRPC服务 App网关→paymentRPC

CreatePayment的HTTP接口定义:

1
2
3
@handler CreatePayment
  post /orders/:id/pay (MallCreatePaymentReq) returns
  (MallCreatePaymentResp)

描述:POST /api/mall/orders/:id/pay

protobuf定义:

1
2
3
service PaymentService {
    rpc CreatePayment(CreatePaymentReq) returns (CreatePaymentResp);
  }

描述:/payment.PaymentService/CreatePayment

把这两套接口真正连接起来的是网关logic中的代码:

l.svcCtx.PaymentClient.CreatePayment(...)

API生成HTTP网关代码

HTTP接口定义在.api中,执行代码后,goctl会根据文件生成routes.gohandlertypes.go

路由结构类似:

1
2
3
4
5
{
  Method:  http.MethodPost,
  Path:    "/orders/:id/pay",
  Handler: mall.CreatePaymentHandler(serverCtx),
}

再加上统一前缀:rest.WithPrefix("/api/mall")

得到最终接口:POST /api/mall/orders/:id/pay

proto生成RPC代码

RPC定义放在.proto文件中,goctl会生成四类重要的rpc代码:

  • payment.pb.go:protobuf消息结构体

  • payment_grpc.pb.go:gRPC客户端、服务端接口、方法注册信息

  • payment_service.go:go-zero提供给其他服务使用的客户端包装

  • payment_service_server.go:go-zero服务端适配层

服务建立连接

payment-rpc启动注册服务

payment-rpc入口在services下对应的main.go,负责创建gRPC Server:

1
2
3
4
5
6
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
  pb.RegisterPaymentServiceServer(
      grpcServer,
      paymentserviceServer.NewPaymentServiceServer(ctx),
  )
})

pb.RegisterPaymentServiceServer是protobuf生成的代码,用于告诉gRPC Server,收到对应的gRPC接口时(/payment.PaymentService/CreatePayment),调用对应的PaymentServiceServer.CreatePayment方法

APP网关创建gRPC客户端

APP网关配置中包含PaymentRpc zrpc.RpcClientConf,用于根据配置文件创建客户端,在service_context.go中创建客户端:

1
2
3
paymentClient := paymentservice.NewPaymentService(
  zrpc.MustNewClient(c.PaymentRpc, tokenPropagator),
)

有两层逻辑:

1
rpcClient := zrpc.MustNewClient(c.PaymentRpc, tokenPropagator)

负责根据配置或etcd找到paymentrpc,并连接gRPC,同时处理超时、状态和服务发现,以及执行拦截器

1
paymentClient := paymentservice.NewPaymentService(rpcClient)

将底层连接包装成强类型接口

1
2
3
4
5
6
7
type PaymentService interface {
  CreatePayment(
      ctx context.Context,
      in *CreatePaymentReq,
      opts ...grpc.CallOption,
  ) (*CreatePaymentResp, error)
}

最后把客户端保存到依赖容器

1
2
3
type ServiceContext struct {
  PaymentClient paymentservice.PaymentService
}

这样所有网关logic都可以通过l.svcCtx.PaymentClient调用支付服务,不用每次重新连接

HTTP请求进入网关

假设用户请求:

1
2
POST /api/mall/orders/order-001/pay
Authorization: Bearer <token>

路由找到handler并解析

routes.go会将该URL映射到对应handler,位于cmd层的handler中

handler负责解析HTTP参数,通过httpx.Parse()将路径中的参数解析为types文件中之前定义好的请求体(通过api文件定义的)

1
2
3
types.MallCreatePaymentReq{
  Id: "order-001",
}

handler调用网关logic

handler通过

1
2
l := mall.NewCreatePaymentLogic(ctx, svcCtx)
resp, err := l.CreatePayment(in)

进入网关执行CreatePayment逻辑的函数func (l *CreatePaymentLogic) CreatePayment(req *types.MallCreatePaymentReq,) (*types.MallCreatePaymentResp, error)进而执行

  1. 读取当前登录用户;
  2. 调用mall-rpc查询订单;
  3. 校验订单归属;
  4. 校验订单金额;
  5. 校验订单状态。

等操作

HTTP转换为Protobuf类型

网关完成校验后调用:

1
2
3
4
5
6
7
8
rpcResp, err := l.svcCtx.PaymentClient.CreatePayment(
  l.ctx,
  &paymentpb.CreatePaymentReq{
      OrderId: order.Id,
      UserId:  userID,
      Amount:  order.PayAmount,
  },
)

这里由HTTP请求类型,通过网关读取订单补充数据转为RPC请求类型

因为请求中只携带了order_id,而发给rpc的数据又添加了userid和amount,前者来自登录状态,后者来自mall-rpc,而不是从前端获取数据

PaymentClient内部

PaymentClient实现于payment_service.go

l.svcCtx.PaymentClient.CreatePayment进入了该Client的实现:

1
2
3
4
5
6
7
8
func (m *defaultPaymentService) CreatePayment(
  ctx context.Context,
  in *CreatePaymentReq,
  opts ...grpc.CallOption,
) (*CreatePaymentResp, error) {
  client := pb.NewPaymentServiceClient(m.cli.Conn())
  return client.CreatePayment(ctx, in, opts...)
}

这里又包装了一层gozero的paymentService客户端,用于统一调用方式、对接zrpc连接、提供更方便的接口和集成服务发现、超时、拦截器等功能,执行RPC请求的是pb.NewPaymentServiceClient(m.cli.Conn())

发送网络请求

protobuf生成的_grpc.pb.go中的CreatePayment方法用于发送网络请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (c *paymentServiceClient) CreatePayment(
  ctx context.Context,
  in *CreatePaymentReq,
  opts ...grpc.CallOption,
) (*CreatePaymentResp, error) {
  out := new(CreatePaymentResp)

  err := c.cc.Invoke(
      ctx,
      "/payment.PaymentService/CreatePayment",
      in,
      out,
      opts...,
  )
  if err != nil {
      return nil, err
  }

  return out, nil
}
1
2
3
4
5
6
c.cc.Invoke(
      ctx,
      "/payment.PaymentService/CreatePayment",
      in,
      out,
)

用于拼成gRPC方法,接下来gRPC会根据protobuf定义序列化req,通过gRPC连接发送到payment-rpc,等待服务端响应后将响应反序列化为resp

我们的请求对象如下:

1
2
3
4
5
&paymentpb.CreatePaymentReq{
  OrderId: "order-001",
  UserId:  "user-001",
  Amount:  480000,
}

protobuf在网络传输时主要依赖字段编号,而不是字段名称,因此字段编号一旦发布就不能随意更改含义

拦截器传递身份

创建客户端时调用了

1
2
3
tokenPropagator := zrpc.WithUnaryClientInterceptor(
  interceptor.UnaryClientInterceptor(),
)

会从当前HTTP请求的context中读取JWT等身份信息,写入gRPC的metadata,链路如下:

1
2
3
4
5
6
7
8
9
10
11
HTTP Authorization Header

AuthMiddleware解析JWT

user_id/token放入context

gRPC客户端拦截器读取context

写入gRPC metadata

RPC服务端认证拦截器校验

因此调用RPC时需要传递当前请求的context:PaymentClient.CreatePayment(l.ctx, req)

RPC服务找到对应方法

服务器收到/payment.PaymentService/CreatePayment后,通过protobuf生成代码中注册的服务描述:

1
2
3
4
5
6
7
8
9
var PaymentService_ServiceDesc = grpc.ServiceDesc{
  ServiceName: "payment.PaymentService",
  Methods: []grpc.MethodDesc{
      {
          MethodName: "CreatePayment",
          Handler: _PaymentService_CreatePayment_Handler,
      },
  },
}

找到对应handler:_PaymentService_CreatePayment_Handler,负责将protobuf中的二进制数据反序列化为*pb.CreatePaymentReq并调用srv.(PaymentServiceServer).CreatePayment(ctx, in)

注意:正式进入业务方法之前,请求会通过AddUnaryInterceptors执行各种中间件和拦截器,负责业务前的日志记录、链路追踪、jwt验证、身份信息注入metadata等,其中支付宝异步被排除认证,因为没有jwt

goctl生成server适配层

gRPC最终调用PaymentServiceServer.CreatePayment(ctx, in),由payment_service_server.go

1
2
3
4
5
6
7
func (s *PaymentServiceServer) CreatePayment(
  ctx context.Context,
  in *pb.CreatePaymentReq,
) (*pb.CreatePaymentResp, error) {
  l := paymentservicelogic.NewCreatePaymentLogic(ctx, s.svcCtx)
  return l.CreatePayment(in)
}

它本身不写具体业务,主要负责通过gRPC请求构造和调用logic,使得业务进入l.CreatePayment,进而执行具体逻辑

响应回到前端

payment-rpc返回resp后,响应沿着反方向返回:

1
2
3
4
5
6
7
8
9
10
11
payment-rpc logic

PaymentServiceServer

protobuf序列化

gRPC网络

网关PaymentClient

rpcResp

网关再把protobuf响应转换为HTTP响应

1
2
3
4
5
return &types.MallCreatePaymentResp{
  OutTradeNo: rpcResp.OutTradeNo,
  QrCode:     rpcResp.QrCode,
  Status:     rpcResp.Status,
}, nil

最后执行

1
2
3
"out_trade_no": "abc123",
"qr_code": "https://qr.alipay.com/...",
"status": 0

完整的数据类型转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP JSON

types.MallCreatePaymentReq

pb.CreatePaymentReq

protobuf二进制

pb.CreatePaymentReq

payment-rpc业务处理

pb.CreatePaymentResp

protobuf二进制

pb.CreatePaymentResp

types.MallCreatePaymentResp

HTTP JSON