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

  • ~6.84K 字
  1. 1. 代码生成
    1. 1.1. API生成HTTP网关代码
    2. 1.2. proto生成RPC代码
  2. 2. 服务建立连接
    1. 2.1. payment-rpc启动注册服务
    2. 2.2. APP网关创建gRPC客户端
  3. 3. HTTP请求进入网关
    1. 3.1. 路由找到handler并解析
    2. 3.2. handler调用网关logic
  4. 4. HTTP转换为Protobuf类型
  5. 5. PaymentClient内部
  6. 6. 发送网络请求
  7. 7. 拦截器传递身份
  8. 8. RPC服务找到对应方法
  9. 9. goctl生成server适配层
  10. 10. 响应回到前端

下面以一个调用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.go,handler和types.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
赞助喵
非常感谢您的喜欢!
赞助喵