前言

用 gRPC 很久了,它使用 Protocol buffer 数据传输协议来进行通讯,底层是 HTTP2,它没有任何服务治理能力,是一个纯纯的跨语言的 RPC 框架。

我们使用它来开发微服务,看重它的便捷性,因为定义了 pb 后就不用写文档了,然后用 k8s 来部署它,至于服务治理相关的用其他的工具来辅助。

准备

团队协作中,要保证编译器和相关工具是版本一致的,避免互相污染。

参考:https://www.grpc.io/docs/languages/go/quickstart 来准备 gRPC 开发的基础环境。

Pb编译器安装

https://github.com/protocolbuffers/protobuf/releases 下载你的操作系统需要的 pb 编译工具。 下载规则是 protoc-<version>-<os><arch>.zip

比如我要下载 protoc-3.17.3-osx-x86_64.zip-osx-x86_64 表示苹果操作系统。

PB_REL="https://github.com/protocolbuffers/protobuf/releases"
curl -LO $PB_REL/download/v3.17.3/protoc-3.17.3-osx-x86_64.zip

当然,你可以通过浏览器下载, 下载后解压到任意一个文件夹中:

unzip protoc-3.17.3-osx-x86_64.zip -d $HOME/.local

我们解压到了本地目录下的 $HOME/.local,我们将这个路径加入环境变量:

vim ~/.bash_profile

export PATH="$PATH:$HOME/.local/bin"

检验:

source ~/.bash_profile

protoc --version

我们就安装了 3.17.3protoc

安装 Golang 插件

将 protoc 文件生成 go 文件的插件工具: https://github.com/protocolbuffers/protobuf-go

辅助生成 gRPC 代码的插件工具,同时也是 golang gRPC SDK: https://github.com/grpc/grpc-go

辅助生成 http 网关代码的插件工具: https://github.com/grpc-ecosystem/grpc-gateway

开始安装:

# 编辑环境变量
vim ~/.bash_profile

# 配置 golang 环境
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
export GOROOT=/Users/pika/Documents/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOROOT/bin

# 使环境变量生效
source ~/.bash_profile

# 安装将 protoc 文件生成 go 文件的插件工具,这个插件要指定版本
# 这个工具是 protobuf 提供的,开源地址:https://github.com/protocolbuffers/protobuf-go
go get -u -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1

# 安装辅助生成 gRPC 代码的插件工具
# 这个工具是 grpc-go 客户端体提供的,开源地址: https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc
go get -u -v google.golang.org/grpc/cmd/protoc-gen-go-grpc

# 安装辅助生成 http 网关代码的插件工具
go get -u -v  github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go get -u -v  github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

查看:

ls $GOBIN/

测试

新建 hello.proto

mkdir proto
cd proto 

vim hello.proto

内容如下:

syntax = "proto3";

package hello;

import "google/api/annotations.proto";

// 包名必须是全名
option go_package = "github.com/hunterhug/hello";

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/say"
      body: "*"
    };
  }
  // Sends another greeting
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

编译可以参考 https://developers.google.com/protocol-buffers/docs/reference/go-generated

mkdir hello

protoc --proto_path=./proto \
-I./googleapis -I/usr/local/include -I. \
--go_out=./hello --go_opt=paths=source_relative \
--go-grpc_out=./hello --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=./${build} --grpc-gateway_opt=paths=source_relative \
hello.proto

其中当前文件夹下的 googleapis 请到 https://github.com/googleapis/googleapis/tree/master/google/api 下载。

编译 /proto 文件夹里的 pb 文件,生成相应的 go 代码到 ./hello 文件夹中。

多文件夹编译脚本贡献:

#!/bin/bash -e

BUILDS=("account" "admin" "file" "gateway" "common")

for build in ${BUILDS[@]}; do
  echo "compile ${build} pb"
  protoc --proto_path=./${build} \
  -I./googleapis -I/usr/local/include -I. \
  --go_out=./${build} --go_opt=paths=source_relative \
  --go-grpc_out=./${build} --go-grpc_opt=paths=source_relative \
  --grpc-gateway_out=./${build} --grpc-gateway_opt=paths=source_relative \
  ./${build}/*.proto
done

Golang SDK 客户端库

接下来就是实战了:

go get -u -v google.golang.org/grpc@v1.39.0

可以参考官方示例。差不多 go.mod 如下:

require (
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0
	google.golang.org/genproto v0.0.0-20210729151513-df9385d47c1b
	google.golang.org/grpc v1.39.0
	google.golang.org/protobuf v1.27.1
)

升级的时候改版本即可。

部署到 k8s 负载均衡

k8s 默认的负载均衡是四层的,无法负载均衡保持 TCP 长连接的 gRPC,因为一旦 gRPC 开了一个 TCP 长连接,那么 k8s 会接住这个长连接,然后流量就会一直打到那个长连接,其他的 pod 就在那里白费了。

除非在代码里实现,每次请求 gRPC 后马上关闭 TCP 连接,我在某 Saas 公司,就见识到每次获取连接后 defer conn.Close,这样请求完毕后连接就会被释放,下一次 k8s 就会去找其他的 pod 连一条新的 TCP 长连接。

还有一种方式,在客户端处维持多个 TCP 长连接来均衡,这种官方库已经提供了内置插件,我们可以建一个无头的 k8s service,然后连这个 service name,这样每次请求都会获取所有 pod 的 IP,然后可能同时 hold 住好几条长连接。

但是,我们还是使用代理模式来进行负载均衡,这种方式客户端和服务端都无感知,特别好。

七层代理,老牌有 Nginx/Haproxy,新的有 Kong/Envoy,它们都有针对 k8s 提出了解决方案。

由于服务网格 Istio 相对出名,数据平面使用了 Envoy,它会以边车模式 Sidecar 进行贴身守护,所有流量会被边车守护捕获,然后进行流控、遥测和相关数据的收集。

最后我们使用 Envoy 来进行负载均衡。

Todo …