专栏首页CNCF花椒服务端 gRPC 开发实践

花椒服务端 gRPC 开发实践

背景

在移动端平台开发中,为了增加代码复用,降低开发成本,通常会需要采用跨平台的开发技术,花椒也不例外。本次新的单品开发,由于时间紧,人员有限,经过调研选型,最终确定了 flutter 方案(具体选型过程不在本文讨论之内)。

为了让客户端更专注业务实现,降低接口联调测试成本,我们选用了 gRPC 方案。gRPC是一个高性能、通用的开源 RPC 框架,由 Google 开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf(Protocol Buffers)序列化协议开发,且支持当前主流开发语言。gRPC通过定义一个服务并指定一个可以远程调用的带有参数和返回类型的的方法,使客户端可以直接调用不同机器上的服务应用的方法,就像是本地对象一样。在服务端,服务实现这个接口并且运行 gRPC 服务处理客户端调用。在客户端,有一个stub提供和服务端相同的方法。

gRPC

特点

  • 基于标准化的 IDL(ProtoBuf)来生成服务器端和客户端代码,支持多种主流开发语言。同时可以更好的支持团队与团队之间的接口设计,开发,测试,协作等。
  • 基于 HTTP/2 设计,支持双向流,多路复用,头部压缩。
  • 支持流式发送和响应,批量传输数据,提升性能。
  • ProtoBuf 序列化数据抓包、调试难度较大。我们使用服务端注入方式提供了用户或设备过滤,请求及返回值日志捕获,并开发对应后台模拟抓包展示。
  • 相比 JSON, 对前端不够友好。gRPC 生态 提供了 gateway 的方式为 gRPC 服务代理出 RESTful 接口。
  • ProtoBuf 提供了非常强的扩展性,可以为 protoc 开发定制插件,从而扩展 proto 文件的功能及描述性。

gRPC-Web

gRPC-Web 为前端浏览器提供了 Javascript 库用来访问 gRPC 服务,但是需要通过 Envoy 提供代理服务。相比 JSON 的方式对前端有够友好,同时也增加了服务端的部署成本。因此在这次项目中前端未使用 gRPC 服务,而是由 gRPC-Gateway 提供代理的 RESTful 接口。

gRPC-Gateway

grpc-gateway 是 protoc 的一个插件,它能读取 gRPC 的服务定义并生成反向代理服务器,将 RESTful 的 JSON 请求转换为 gRPC 的方式。这样无需太多工作即可实现一套基于 gRPC 服务的 RESTful 接口,方便前端使用调用接口,同时也方便开发过程中通过 Postman/Paw 之类的工具调试接口。

gateway -> gRPC 映射方式:

  • HTTP 源 IP 添加到 gRPC 的 X-Forwarded-For 请求头
  • HTTP 请求 Host 添加到 gRPC 的 X-Forwarded-Host 请求头
  • HTTP 请求头 Authorization 添加到 gRPC 的 authorization 请求头
  • HTTP 请求头带 Grpc-Metadata- 前缀的映射到 gRPC 的 metadata (key 名不带前缀)

例如,gRPC 接口要求的通用的 metadata 参数(如 platform, device_id 等)在 HTTP RESTful 的传递方式如下:

GET /index HTTP/1.1
grpc-metadata-platform: ios
grpc-metadata-device_id: xxxxxxxxx
grpc-metadata-timestamp: 1562641496
grpc-metadata-locale: en_US
grpc-metadata-version: 1.0.0
grpc-metadata-user_id: 
grpc-metadata-user_token: 
Host: gateway.hostame.com

基础库

dart

为了便于客户端调用,连接复用及通用参数传递,我们封装了 dart 的基础库。

BaseClient 维护了针对 HOST 缓存的连接池,同时也提供了接口需要传递的 metadata 信息。

var base = BaseClient(host: 'rpc.hostame.com', port: 443, secure: true);
final md = await base.metadata;
final stub = AuthClient(base.channel, options: CallOptions(metadata: md));

golang

golang 后端服务需要同时支持 gRPC 和 gateway 两种请求方式。为了简化部署和上线依赖,gateway 和 gRPC 的功能放在了一起,并通过拦截器注入对应的功能,主要包括 gRPC 统计,访问日志,接口鉴权,请求参数校验,gateway JSON 编码等。

    svrMux := &ServerMux{
      ...
        ServeMux: http.NewServeMux(),
    }
    svrMux.svr = grpc.NewServer(
        grpc.UnaryInterceptor(
            middleware.ChainUnaryServer(
                recovery.UnaryServerInterceptor(recovery.WithRecoveryHandler(svrMux.recoveryHandler)),
                prometheus.UnaryServerInterceptor, // prometheus 统计
                svrMux.UnaryResponseInterceptor(),
                proto.UnaryServerInterceptor(), // metadata 解析
                log.UnaryServerInterceptor(conf.GlobalCallback()), // 访问日志
                auth.UnaryServerInterceptor(conf.GlobalCallback()), // 接口鉴权
                validator.UnaryServerInterceptor(), // 请求参数校验
            )),
        grpc.StreamInterceptor(
            middleware.ChainStreamServer(
                ...
            )))
    svrMux.mux = runtime.NewServeMux(
        runtime.WithMarshalerOption(runtime.MIMEWildcard, json.Proto), // json 设置
        runtime.WithProtoErrorHandler(svrMux.protoErrorHandler), // 错误处理
        runtime.WithStreamErrorHandler(svrMux.streamErrorHandler),
    )
    // gRPC gateway mux
    svrMux.ServeMux.Handle("/", svrMux.mux)
  • 引用到的 package
github.com/golang/protobuf
github.com/grpc-ecosystem/go-grpc-middleware
github.com/grpc-ecosystem/go-grpc-prometheus
github.com/grpc-ecosystem/grpc-gateway
github.com/lnnujxxy/protoc-gen-validate
github.com/youlu-cn/grpc-gen
go.uber.org/zap
google.golang.org/grpc
  • 开发流程

为了提高开发效率,方便维护及模块复用,服务端按功能进行组件化开发。每个组件可以单独运行一个服务,也可以和其它组件共同组成一个服务。每个组件都需要实现 Component 接口:

type Component interface {
    // 组件名称
    Name() string
    // 初始化存储
    InitStorage() error
    // 初始化注册 gRPC
    InitGRPC(svc Service) error
    // 初始化注册 gateway
    InitGateway(svc Service) error
    // cron 回调
    StorageCron()
}

对应组件开发完成后,需要开发对应的服务容器,步骤如下。

  • 初始化 base package
base.Init(context.TODO(), cfg, &global.Callback{
    Authenticator: &auth.Callback{},
    LogCapture:    &log.Capture{},
})
  • 如需对外提供服务,需要提供端口及 TLS 证书

base.DefaultServer.AddPublicServer(rpcPort, gatewayPort, setting.TLSConfig)

  • 组件注册
base.DefaultServer.RegisterComponent(&user.Component{})
base.DefaultServer.RegisterComponent(&push.Component{})
...
  • 监听服务

base.DefaultServer.Serve()

接口定义及实现

proto 规范

gRPC 基于标准化的 IDL(ProtoBuf)来生成服务器端和客户端代码,我们决定将所有的接口描述及文档说明都放到 proto 文件中,便于查看及修改。对 proto 的接口描述及注释的规范如下:

// 消息类型注释,支持多行,
// 支持 markdown 语法:
//
// > blockquote
//
// | Syntax | Description |
// | ----------- | ----------- |
// | Header | Title |
// | Paragraph | Text |
message ExampleMessage {
    uint64 id = 1; // 字段注释,简洁
}

// 服务说明,支持 markdown 语法
//
// 1. First item
// 2. Second item
// 3. Third item
//
// ```json
// {
//   "firstName": "John",
//   "lastName": "Smith",
//   "age": 25
// }
// ```
service Example {
    option (auth.visible) = {
        scope: PUBLIC_SCOPE // Service scope:仅内网可见或对外可见
    };

    // 方法说明,支持 markdown
    //
    // - [x] Write the press release
    // - [ ] Update the website
    // - [ ] Contact the media
    rpc test (ExampleMessage) returns (ExampleMessage) {
        option (auth.access) = {
            level: LOW_ACCESS_LEVEL // 接口请求权限
        };
        option (google.api.http) = {
            post: "/example/test"
            body: "*"
        };
    }
}

代码生成

golang

gengo:
    @protoc -Iproto \
        -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
        -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \
        -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \
        --go_out=plugins=grpc:go/pb \
        --grpc-gateway_out=logtostderr=true:go/pb \
        --validate_out="lang=go:go/pb" \
        --auth_out="lang=go:go/pb" \
        proto/*.proto
  • SDK 引入

golang 使用 go mod 的方式直接引入 pb 生成的 .go 文件

dart

ifeq ($(shell uname), Darwin)
PROTO_ROOT_DIR = $(shell brew --prefix)/Cellar/protobuf/*
else
PROTO_ROOT_DIR = /usr/local
endif

gendart:
    @protoc --dart_out=dart/front_user/lib/src/generated $(PROTO_ROOT_DIR)/include/google/protobuf/*.proto
    @protoc -Iproto \
        -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
        -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \
        -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \
        --dart_out=grpc:dart/user/lib proto/*.proto
  • SDK 引入

修改 pubspec.yaml,执行 flutter packages get 或 flutter packages upgrade

dependencies:
  flutter:
    sdk: flutter

  protobuf: ^0.13.4
  grpc: ^1.0.1
  user:
    git:
      url: git@github.com:project/repo.git
      path: dart/user
  • 已知问题:
  1. dart 在对 protobuf 生成的类型做 json 编码时,json 中的 key 是字段号而非名字,导致无法与其它语言交互。ISSUE (https://github.com/dart-lang/protobuf/issues/220)

文档生成

gRPC gateway 提供了通过 proto 文件生成 swagger API 文档,缺点是只支持 gateway 的 RESTful 接口,并且默认的展示方式有点不符合我们的常规文档使用方式。

我们基于 protoc 插件开发了 protoc-gen-markdown 工具,可以由 proto 文件生成 markdown 文档,提供 gRPC 接口描述,以及 RESTful 接口描述及 JSON 示例,提供全文目录,支持锚点导航等。生成方式如下:

gendoc:
    @protoc -Iproto \
        -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
        -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \
        -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \
        --markdown_out=":doc" \
        proto/*.proto

文档会在对应路径生成接口列表 README.md,以及每个 protobuf 对应的接口文档。

调试

传统的 RESTful 接口在调试及问题排查时,可以通过抓包或者 MitM(中间人攻击)的方式,配置也比较容易。而 gRPC 因为使用了 HTTP2 及 protobuf 二进制流,抓包及数据流反解难度相对较高,调试及问题排查时会比较复杂。为了解决这个问题,我们通过服务端注入的方式,配合查询后台过滤对应的请求日志,从而实现如下类似抓包的效果。

后续计划

  1. gRPC Streaming
  2. 框架层集成全链路 Trace 支持
  3. 迭代优化框架,提供对应脚手架,简化新组件/服务创建及开发流程

文章转载自花椒技术。点击这里阅读原文了解更多

本文分享自微信公众号 - CNCF(lf_cncf)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android开发:Android Studio插件GsonFormat根据Json自动生成javabean的方法

    在Android开发过程中,会创建多个javabean,在App网络请求多的时候,一个列表数据就需要新建一个javabean。而且javabean中好多重复的方...

    三掌柜
  • 前端智能漫谈 - 写给前端的AI白皮书

    | 导语 最近几年,学术界、工业界、投资界各方一起发力,人工智能发展得如火如荼,硬件、算法与数据共同发展,带来了各行各业的深度应用。而我们前端er更像一个事不...

    腾讯大讲堂
  • Spring Cloud Security:Oauth2实现单点登录

    单点登录(Single Sign On)指的是当有多个系统需要登录时,用户只需登录一个系统,就可以访问其他需要登录的系统而无需登录。

    macrozheng
  • SpringBoot 注解大全,收藏一波!!!

    @SpringBootApplication:包含了@ComponentScan、@Configuration和@EnableAutoConfiguration...

    掌上编程
  • JWT介绍及其安全性分析

    JWT(JSON Web令牌)是REST API中经常使用的一种机制,可以在流行的标准(例如OpenID Connect)中找到它,但是有时也会使用OAuth2...

    FB客服
  • 来,科普一下JWT

    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信...

    好好学java
  • 2019-08-17 awesome-java,JAVA开发的武器库

    A curated list of awesome Java frameworks, libraries and software.

    Albert陈凯
  • Go组件学习——Web框架Gin

    以前学Java的时候,和Spring全家桶打好关系就行了,从Spring、Spring MVC到SpringBoot,一脉相承。

    JackieZheng
  • Spring Cloud Security:Oauth2实现单点登录

    单点登录(Single Sign On)指的是当有多个系统需要登录时,用户只需登录一个系统,就可以访问其他需要登录的系统而无需登录。

    JAVA葵花宝典
  • 程序员过关斩将--更加优雅的Token认证方式JWT

    通过上一篇你大体已经了解session和cookie认证了,session认证需要服务端做大量的工作来保证session信息的一致性以及session的存储,所...

    心莱科技雪雁

扫码关注云+社区

领取腾讯云代金券