上一篇,我们简单介绍了下mac下单节点Kubernetes的安装,今天我们乘热打铁,感受下grpc整合Kubernetes的魅力。好了Talk is cheap,Show me the graph
我们要做的是下面这么一个架构的小demo。
grpc-k8s
后续计划写个gRPC的专题,我们先来简单认识下gRPC的基本玩法。
gRPC
在微服务盛行的今天,如果你不会个RPC框架你都不好意思出门跟人打招呼。业界流行的目前主要有dubbo、motan、rpcx、gRPC、thrift,排名不分先后,各有千秋。今天主要带来的是go中gRPC的使用。
正如其官方介绍的那样,gRPC
是一个高性能、通用的开源RPC框架,由Google开发主要面向移动应用开发并基于HTTP/2协议标准而设计,基于Protocol Buffers序列化协议开发,且支持众多开发语言。至于gRPC优势不再赘述,合适才是最好的。
零基础在Go中使用gRPC大致分为四个步骤:
- 安装golang以及Protocol Buffer compiler,go module撸一个工程引用下gRPC库
- 创建
.proto
文件,并定义一个service - 使用protocol buffer compiler生成服务端和客户端代码
- 使用Go gRPC API为你的服务写一个简单的客户端和服务端
安装软件和撸一个工程
先决条件
- golang 安装 关于这部分网上大把文章Google之
- Protocol Buffer compiler,
protoc
关于protocol buffers的玩法可以参考往期文章。
选择适合你平台的预编译好的二进制文件(https://github.com/google/protobuf/releases),解压并将可执行文件protoc
放到你的环境变了中
- 使用以下命令为Go安装protobuf协议编译器插件:
$ export GO111MODULE=on # Enable module mode
$ go get google.golang.org/protobuf/cmd/protoc-gen-go
google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 更新你的PATH,以便
protoc
可以找到插件:
$ export PATH="$PATH:$(go env GOPATH)/bin"
Go module
撸一个工程
cd ${your workspace}
mkdir -p grpc-k8s-demo && cd grpc-k8s-demo
go mod init github.com/xxx/grpc-k8s-demo
#引入gRPC库
go get -u google.golang.org/grpc
定义服务
在gRPC中,我们是使用protocol buffers定义gRPC服务以及方法请求和响应类型。首先要定义服务,需要在.proto文件中指定一个命名service
:
service Greeter {
...
}
然后,你可以在服务定义中定义rpc方法,并指定它们的请求和响应类型。gRPC允许您定义四种服务方法,所有这些方法都在 Greeter服务中使用:
- 一个简单的RPC,客户端使用存根将请求发送到服务器,然后等待响应返回,就像正常的函数调用一样。
rpc SayHello (HelloRequest) returns (HelloReply) {}
- 服务器端流式RPC,客户端向服务器发送请求,并获取流来读取后续的一系列消息。客户端从返回的流中读取数据,直到没有更多消息为止。如下你可以通过在响应类型之前放置stream关键字来指定服务器端流方法。
rpc SayHello (HelloRequest) returns (stream HelloReply) {}
- 客户端流式RPC,客户端编写消息序列,然后使用提供的流将消息发送到服务器。一旦客户端写完消息后,它将等待服务器读取所有消息并返回其响应。你可以通过将stream关键字放在请求类型之前指定客户端流方法。
rpc SayHello (stream HelloRequest) returns (HelloReply) {}
- 双向流式RPC,双方都使用读写流发送一系列消息。这两个流是独立运行的,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以在写响应之前等待接收所有客户端消息,或者可以先读取消息再写入消息,或读写的其他组合。每个流中的消息顺序都会保留。你可以通过在请求和响应之前都放置stream关键字来指定这种类型的方法。
rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
.proto文件还包含用于服务方法中所有请求和响应类型的protobuf协议消息类型定义-例如,
代码语言:javascript复制// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
map<string, HelloRequest> maps = 2;
}
本文为了简单演示,使用第一种简单RPC的方式。
生成客户端和服务器代码
接下来,我们需要根据.proto服务定义生成gRPC客户端和服务器接口。可以使用带有特殊gRPC Go插件的protocol buffer compiler protoc
进行此操作。运行以下命令:(关于protoc 原理可见往期《搞定protocol buffers 下-原理篇》)
$ protoc --go_out=. --go_opt=paths=source_relative
--go-grpc_out=. --go-grpc_opt=paths=source_relative
pb/hello.proto
运行此命令将在pb目录中生成以下文件:
- hello.pb.go,其中包含用于填充,序列化和检索请求和响应消息类型的所有protocol buffers代码。
- hello_grpc.pb.go,其中包含以下内容:
- 客户端使用Greeter服务中定义的方法调用的接口类型(或存根)。
- 服务器要实现的接口类型,也具有Greeter服务中定义的方法。
也可以简单点儿
代码语言:javascript复制 protoc hello/service.proto --go_out=hello/ --go-grpc_out=hello/
这样只会生成一个文件,大同小异。
创建服务器
要使我们的Greeter服务发挥作用,服务端编写分为两个部分:
- 实现根据我们的服务定义生成的服务接口:完成我们服务的实际工作。
- 运行gRPC服务器以侦听来自客户端的请求,并将其分发到正确的服务实现。
package main
import (
"context"
"fmt"
hello "github.com/leoshus/proto-demo/pb"
"google.golang.org/grpc"
"log"
"net"
"os"
"time"
)
type HelloServer struct {
}
func (h *HelloServer) SayHello(ctx context.Context, req *hello.HelloRequest) (*hello.HelloReply, error) {
now := time.Now().Format("2006-01-02 15:04:05")
hostname, _ := os.Hostname()
log.Printf("%s say hello:%sn", hostname, now)
return &hello.HelloReply{
Message: fmt.Sprintf("%s say hello %s :%s", hostname, req.Name, now),
}, nil
}
func main() {
server := grpc.NewServer()
hello.RegisterGreeterServer(server, &HelloServer{})
listener, err := net.Listen("tcp", ":8088")
if err != nil {
log.Printf("start server listen error:%v", err)
return
}
log.Println("start server...")
if err := server.Serve(listener); err != nil {
log.Printf("start server error:%v", err)
}
}
要构建和启动服务器,我们:
- 使用以下命令指定我们要用于侦听客户端请求的端口:
lis,err:= net.Listen(...)
。 - 使用grpc.NewServer(...)创建gRPC服务器的实例。
- 在gRPC服务器上注册我们的服务实现。
- 使用我们的端口详细信息在服务器上调用Serve()进行阻塞等待,直到进程被杀死或调用Stop()为止。
创建客户端
客户端代码主要是调用服务方法,我们首先需要
- 创建一个gRPC通道来与服务器通信。我们通过将服务器地址和端口号传递给grpc.Dial()来创建它,当服务需要它们时,可以使用DialOptions在grpc.Dial中设置身份验证凭据(例如TLS,GCE凭据或JWT凭据)。Greeter服务不需要任何凭据。
- 设置gRPC通道后,我们需要一个客户端存根来执行RPC。例如,我们使用从.proto文件生成的pb包提供的NewGreeterClient方法获取它。
client := hello.NewGreeterClient(conn)
- 调用服务方法:在gRPC-Go中,RPC在阻塞/同步模式下运行,这意味着RPC调用等待服务器响应,并且将返回响应或错误。
整体代码如下:
代码语言:javascript复制package main
import (
"context"
"flag"
"fmt"
hello "github.com/leoshus/proto-demo/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/backoff"
"google.golang.org/grpc/balancer/roundrobin"
"log"
"strings"
"time"
)
func main() {
log.SetFlags(log.Lshortfile | log.Ldate)
var address string
flag.StringVar(&address, "address", "localhost:8088", "grpc server address")
flag.Parse()
conn, err := grpc.Dial(strings.Join([]string{"dns:///", address}, ""), grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"LoadBalancingPolicy":"%s"}`, roundrobin.Name)),
//grpc.WithBlock(),
grpc.WithConnectParams(grpc.ConnectParams{
Backoff: backoff.Config{
MaxDelay: 2 * time.Second,
},
MinConnectTimeout: 2 * time.Second,
}))
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
client := hello.NewGreeterClient(conn)
for range time.Tick(time.Second) {
resp, err := client.SayHello(context.TODO(), &hello.HelloRequest{
Name: "tom",
})
if err != nil {
fmt.Println(err)
log.Printf("say hello occur error:%vn", err)
return
}
log.Printf("say hello : %s n", resp)
}
}
制作镜像
通过Dockfile
定义一个镜像。
FROM golang:1.16.3
COPY . /app/src/grpc-demo
WORKDIR /app/src/grpc-demo
RUN go get -d -v ./...
RUN go install -gcflags=all="-N -l " ./...
这样只需cd到Dockerfile
所在目录执行docker build -t leoshus/grpc-demo:v1 .
即可构建一个镜像。
push镜像
因为构建的镜像要被之后的Kubernetes使用,所以需要讲镜像push到远端仓库。当然你也可以搭建私有仓库来管理镜像,这里我们使用官方的镜像仓库(https://hub.docker.com/)的演示。
推送仓库前你需要进行登录注册。
代码语言:javascript复制docker login #使用注册的用户名密码登陆
docker push leoshus/grpc-demo:v1 # 完成镜像的推送
编写k8s资源文件
首先是服务端在k8s上部署的资源文件编写
代码语言:javascript复制apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-server
labels:
app-name: grpc-server
spec:
replicas: 3
selector:
matchLabels:
app-name: grpc-server
template:
metadata:
labels:
app-name: grpc-server
name: grpc-server
spec:
containers:
- command:
- server
image: docker.io/leoshus/grpc-demo:v6
imagePullPolicy: Always
name: server
resources:
limits:
cpu: "0.5"
memory: 100Mi
requests:
cpu: "0.5"
memory: 100Mi
restartPolicy: Always
这里我们为了方便演示gRPC
的负载均衡机制,我们使用了Headless Service
,保证该服务不会分配Cluster IP
,也不通过kube-proxy
做反向代理和负载均衡。而是通过DNS
提供稳定的网络ID来访问,将headless service
的后端直接解析为pod ip
列表,然后由gRPC
的负载均衡机制来选择使用哪台server
。
apiVersion: v1
kind: Service
metadata:
labels:
app-name: grpc-server
name: grpc-server-service
spec:
clusterIP: None
ports:
- name: grpc
port: 31250
protocol: TCP
targetPort: 8088
selector:
app-name: grpc-server
然后你可以只需下面命令来进行服务部署
代码语言:javascript复制kubectl apply -f grpc_server.yaml
有了服务端集群,自然需要有客户端来访问,接下来我们需要编写客户端的资源文件并部署在k8s上
代码语言:javascript复制apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc-client
labels:
app-name: grpc-client
spec:
selector:
matchLabels:
app-name: grpc-client
template:
metadata:
labels:
app-name: grpc-client
name: grpc-client
spec:
containers:
- command:
- client
args:
- --address
- grpc-server-service.default.svc.cluster.local:8088"
env:
- name: GRPC_GO_RETRY
value: "on"
image: docker.io/leoshus/grpc-demo:v6
imagePullPolicy: Always
name: client
resources:
limits:
cpu: "0.5"
memory: 100Mi
requests:
cpu: "0.5"
memory: 100Mi
restartPolicy: Always
部署客户端
代码语言:javascript复制kubectl apply -f grpc_client.yaml
执行kubectl get po -o wide
可以看到当前启动的pod
的情况
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
grpc-client-5b595ff864-kqw5g 1/1 Running 0 2m40s 10.1.0.104 docker-desktop <none> <none>
grpc-server-6f579c4f88-cd8tj 1/1 Running 0 4m20s 10.1.0.101 docker-desktop <none> <none>
grpc-server-6f579c4f88-kkqkm 1/1 Running 0 4m20s 10.1.0.102 docker-desktop <none> <none>
grpc-server-6f579c4f88-p9zxp 1/1 Running 0 4m20s 10.1.0.103 docker-desktop <none> <none>
看到STATUS
一栏所有pod都处于Running
状态了,那么可以看下grpc_client
打印的日志:
$ kubectl logs -f grpc-client-5b595ff864-kqw5g
2021/05/16 client.go:45: say hello : message:"grpc-server-6f579c4f88-p9zxp say hello tom :2021-05-16 05:45:26"
2021/05/16 client.go:45: say hello : message:"grpc-server-6f579c4f88-cd8tj say hello tom :2021-05-16 05:45:27"
2021/05/16 client.go:45: say hello : message:"grpc-server-6f579c4f88-kkqkm say hello tom :2021-05-16 05:45:28"
2021/05/16 client.go:45: say hello : message:"grpc-server-6f579c4f88-p9zxp say hello tom :2021-05-16 05:45:29"
2021/05/16 client.go:45: say hello : message:"grpc-server-6f579c4f88-cd8tj say hello tom :2021-05-16 05:45:30"
日志从输出可见,此时的grpc
的负载均衡已经起作用了。
对于服务的扩缩容,也只需要一行命令:
代码语言:javascript复制#缩容到1台
kubectl scale --replicas=1 deployment grpc-server
#扩容到5台
kubectl scale --replicas=5 deployment grpc-server
当然具体的扩缩容的细节策略可以在yaml
资源文件进行配置
至此一个简单的grpc整合k8s的工程就完成了。详细代码可见(https://github.com/leoshus/grpc-k8s-demo)