小白零基础--gRPC整合Kubernetes

共 15388字,需浏览 31分钟

 ·

2021-05-18 22:13

上一篇,我们简单介绍了下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为你的服务写一个简单的客户端和服务端

安装软件和撸一个工程

先决条件

  1. golang 安装 关于这部分网上大把文章Google之
  2. Protocol Buffer compiler,protoc 关于protocol buffers的玩法可以参考往期文章。

选择适合你平台的预编译好的二进制文件(https://github.com/google/protobuf/releases),解压并将可执行文件protoc放到你的环境变了中

  1. 使用以下命令为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
  1. 更新你的PATH,以便protoc可以找到插件:
$ export PATH="$PATH:$(go env GOPATH)/bin"
  1. 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协议消息类型定义-例如,

// 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服务中定义的方法。

也可以简单点儿

 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:%s\n", 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)
 }
}

要构建和启动服务器,我们:

  1. 使用以下命令指定我们要用于侦听客户端请求的端口:lis,err:= net.Listen(...)
  2. 使用grpc.NewServer(...)创建gRPC服务器的实例。
  3. 在gRPC服务器上注册我们的服务实现。
  4. 使用我们的端口详细信息在服务器上调用Serve()进行阻塞等待,直到进程被杀死或调用Stop()为止。

创建客户端

客户端代码主要是调用服务方法,我们首先需要

  1. 创建一个gRPC通道来与服务器通信。我们通过将服务器地址和端口号传递给grpc.Dial()来创建它,当服务需要它们时,可以使用DialOptions在grpc.Dial中设置身份验证凭据(例如TLS,GCE凭据或JWT凭据)。Greeter服务不需要任何凭据。
  2. 设置gRPC通道后,我们需要一个客户端存根来执行RPC。例如,我们使用从.proto文件生成的pb包提供的NewGreeterClient方法获取它。
client := hello.NewGreeterClient(conn)
  1. 调用服务方法:在gRPC-Go中,RPC在阻塞/同步模式下运行,这意味着RPC调用等待服务器响应,并且将返回响应或错误。

整体代码如下:

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:%v\n", 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/)的演示。

推送仓库前你需要进行登录注册。

docker login #使用注册的用户名密码登陆
docker push leoshus/grpc-demo:v1 # 完成镜像的推送

编写k8s资源文件

首先是服务端在k8s上部署的资源文件编写

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

然后你可以只需下面命令来进行服务部署

kubectl apply -f grpc_server.yaml

有了服务端集群,自然需要有客户端来访问,接下来我们需要编写客户端的资源文件并部署在k8s上

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

部署客户端

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的负载均衡已经起作用了。

对于服务的扩缩容,也只需要一行命令:

#缩容到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)


浏览 60
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报