Deploy gRPC microservice on Raspberrypi kubernetes
March 5, 2019今天带着大家如何在树莓派kubernetes集群中部署微服务,这次文章和上次文章的区别就是这个涉及两个微服务,如何使两个微服务在kubernetes中实现服务之间的调用可能是与上次的不同之处,这次也使用Google的开源框架gRPC框架来加快微服务的开发.
本次教程需要准备:
- 一个启动好的树莓派kubernetes集群(参考: 边缘智能-在树莓派上部署kubernetes集群)
- protobuf工具(参考 https://github.com/google/protobuf,从release页下载对应的操作系统的版本即可)
- Docker (参考: https://docs.docker.com/engine/installation/)
- kubectl工具 (参考: https://kubernetes.io/docs/tasks/tools/install-kubectl/)
本次文章主要参考:
代码:
- https://github.com/rh01/grpc-microservice-k8s
0x01 定义我们的protobuf文件
protobuf是google的一个序列化结构化数据工具,它可以让人们定义好相关的结构,使用protoc工具自动生成对应的代码.类似的结构化工具还有thrift.
微服务:这里我主要实现一个最大公约数的功能,输入两个数值,返回这两个数的最大公约数.
这里既然使用gRPC来做,那么主要使用rpc来实现服务调用,因为rpc实现的是服务之间的同步调用,即客户端调用服务并等待响应。gRPC是提供RPC功能的框架之一。此时我们需要使用Protocol Buffer的接口定义语言中编写消息类型和服务的代码并进行编译。
下面就是我们使用protobuf语言定义的消息类型和服务.(具体参考: https://grpc.io/docs/quickstart/go.html)
$ mkdir -pv ~/go/src/github.com/rh01/mini-deploy-app/pb
$ cd ~/go/src/github.com/rh01/mini-deploy-app/pb
$ vim pb.proto
syntax = "proto3"; // protobuf 版本
package pb; // 代码生成的package名字
// 定义的请求消息体
message GCDRequest {
uint64 a = 1;
uint64 b = 2;
}
// 定义的响应消息体
message GCDResponse {
uint64 result = 1;
}
// 调用的远程服务,这是client请求server端的远程计算服务
service GCDService {
rpc Compute (GCDRequest) returns (GCDResponse) {}
}
接下来我们需要使用 protoc 生成对应的服务代码
$ protoc -I . --go_out=plugins=grpc:. ./*.proto
提前须知:
执行上面的指令需要使用安装 grpc 和 proto-gen-go 工具,使用下面的命令:
$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go
将 $GOPATH/bin 目录添加到PATH环境变量中
$ export PATH=$PATH:$GOPATH/bin
这时应该生成了 gcd.pb.go 程序.
0x02 最大公约数服务
定义服务端
gcd 服务将会使用上一步生成的代码进行实现gcd计算服务.
$ cd ~/go/src/github.com/rh01/mini-deploy-app/
$ mkdir -p gcd
$ vim main.go
package main
import (
"log"
"net"
context "golang.org/x/net/context"
pb "github.com/rh01/mini-deploy-app/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
在main函数中主要定义server结构,并将其注册为server端,用于处理 gcd 计算的请求.然后启动grpc服务.
type server struct {
}
func main() {
lis, err := net.Listen("tcp", ":3000")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGCDServiceServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
实现GCDServiceServer接口的 Compute
方法, server 结构对象的指针作为方法接受者.
// gcd.pb.go
// GCDServiceServer is the server API for GCDService service.
type GCDServiceServer interface {
Compute(context.Context, *GCDRequest) (*GCDResponse, error)
}
func (s *server) Compute(ctx context.Context, r *pb.GCDRequest) (*pb.GCDResponse, error) {
a, b := r.A, r.B
for b != 0 {
a, b = b, a%b
}
return &pb.GCDResponse{Result: a}, nil
}
0x03 定义RESTFul客户端
前端使用 gin 框架,主要是提供一个REST风格的访问方式和调用我们定义的gcd服务端执行实际的计算任务.
$ cd ~/go/src/github.com/rh01/mini-deploy-app/
$ mkdir -p api
$ vim main.go
package main
import (
fmt "fmt"
"log"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
pb "github.com/rh01/mini-deploy-app/pb"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("gcd-service:3000", grpc.WithInsecure())
if err != nil {
log.Fatalf("Dial failed: %v", err)
}
gcdClient := pb.NewGCDServiceClient(conn)
}
上面的代码主要使用rpc的方式访问我们定义的服务端,此时的 gcd-service:3000 就是我们gcd服务端的 endpoint,这个就是服务地址,需要在kubernetes中定义.
r := gin.Default()
r.GET("/gcd/:a/:b", func(c *gin.Context) {
// Parse parameters
a, err := strconv.ParseUint(c.Param("a"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid parameter A"})
return
}
b, err := strconv.ParseUint(c.Param("b"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid parameter B"})
return
}
// Call GCD service
req := &pb.GCDRequest{A: a, B: b}
if res, err := gcdClient.Compute(c, req); err == nil {
c.JSON(http.StatusOK, gin.H{
"result": fmt.Sprint(res.Result),
})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
})
接下来处理 /gcd/:a/:b 请求,读取参数 A 和 B,然后调用GCD服务.
最后运行我们的REST API端.启动一个API server.
// Run HTTP server
if err := r.Run(":3000"); err != nil {
log.Fatalf("Failed to run server: %v", err)
}
0x04 构建Docker镜像
下面是我定义的Dockerfile文件,因为有两个服务,所以这里分成两个Docker镜像,一个是gcd服务,另外一个是提供RESTAPI访问的客户端api.
有关Docker的多阶段构建参考: [实战] 将golang编写的微服务部署在树莓派搭建的kubernetes集群
# Dockerfile.gcd
FROM golang AS build-env
WORKDIR /go/src/github.com/rh01/mini-deploy-app/gcd
COPY gcd .
COPY pb ../pb
COPY vendor ../vendor
ENV http_proxy http://192.168.1.9:12333
ENV https_proxy http://192.168.1.9:12333
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=arm GOARM=7 go build -v -o /go/src/github.com/rh01/mini-deploy-app/gcd-server
FROM armhf/alpine:latest
COPY --from=build-env /go/src/github.com/rh01/mini-deploy-app/gcd-server /usr/local/bin/gcd
EXPOSE 3000
CMD [ "gcd" ]
# Dockerfile.api
FROM golang AS build-env
WORKDIR /go/src/github.com/rh01/mini-deploy-app/api
COPY api .
COPY pb ../pb
COPY vendor ../vendor
ENV http_proxy http://192.168.1.9:12333
ENV https_proxy http://192.168.1.9:12333
RUN go get -u -v github.com/kardianos/govendor
RUN govendor sync
RUN GOOS=linux GOARCH=arm GOARM=7 go build -v -o /go/src/github.com/rh01/mini-deploy-app/api-server
FROM armhf/alpine:latest
COPY --from=build-env /go/src/github.com/rh01/mini-deploy-app/api-server /usr/local/bin/api
EXPOSE 3000
CMD [ "api" ]
然后构建
$ docker build -t rh02/apiserver:v1.0.0 -f Dockerfile.api .
$ docker build -t rh02/gcdserver:v1.0.0 -f Dockerfile.gcd .
0x05 部署到kubernetes
定义两个Deployment和对应的两个Service,并且将gcd服务的名字写成我们api服务调用的名字:gcd-service.
下面是gcd的deployment和service的定义: gcd.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: gcd-deployment
labels:
app: gcd
spec:
selector:
matchLabels:
app: gcd
replicas: 3
template:
metadata:
labels:
app: gcd
spec:
containers:
- name: gcd
image: rh02/gcdserver:v1.0.0
imagePullPolicy: Always
ports:
- name: gcd-service
containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: gcd-service
spec:
selector:
app: gcd
ports:
- port: 3000
targetPort: gcd-service
创建api.yaml, service类型设置为NodePort,从而可以在集群外部也可以访问,对于GCD服务,类型设置为ClusterIP即可,只需要在集群内部访问就可以.
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: api-deployment
labels:
app: api
spec:
selector:
matchLabels:
app: api
replicas: 1
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: rh02/apiserver:v1.0.0
imagePullPolicy: Always
ports:
- name: api-service
containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
type: NodePort
selector:
app: api
ports:
- port: 3000
targetPort: api-service
使用kubectl创建两个资源:
$ kubectl create -f api.yaml
$ kubectl create -f gcd.yaml
检查所有的Pod是否正在运行, 可以指定 -w
标记,查看启动的过程.
$ kubectl get pods -w
NAME READY STATUS RESTARTS AGE
api-deployment-778049682-3vd0z 1/1 Running 0 3s
gcd-deployment-544390878-0zgc8 1/1 Running 0 2s
gcd-deployment-544390878-p78g0 1/1 Running 0 2s
gcd-deployment-544390878-r26nx 1/1 Running 0 2s