小白零基礎(chǔ)--gRPC整合Kubernetes
上一篇,我們簡單介紹了下mac下單節(jié)點Kubernetes的安裝,今天我們乘熱打鐵,感受下grpc整合Kubernetes的魅力。好了Talk is cheap,Show me the graph 我們要做的是下面這么一個架構(gòu)的小demo。

后續(xù)計劃寫個gRPC的專題,我們先來簡單認(rèn)識下gRPC的基本玩法。
gRPC
在微服務(wù)盛行的今天,如果你不會個RPC框架你都不好意思出門跟人打招呼。業(yè)界流行的目前主要有dubbo、motan、rpcx、gRPC、thrift,排名不分先后,各有千秋。今天主要帶來的是go中g(shù)RPC的使用。
正如其官方介紹的那樣,gRPC是一個高性能、通用的開源RPC框架,由Google開發(fā)主要面向移動應(yīng)用開發(fā)并基于HTTP/2協(xié)議標(biāo)準(zhǔn)而設(shè)計,基于Protocol Buffers序列化協(xié)議開發(fā),且支持眾多開發(fā)語言。至于gRPC優(yōu)勢不再贅述,合適才是最好的。
零基礎(chǔ)在Go中使用gRPC大致分為四個步驟:
安裝golang以及Protocol Buffer compiler,go module擼一個工程引用下gRPC庫 創(chuàng)建 .proto文件,并定義一個service使用protocol buffer compiler生成服務(wù)端和客戶端代碼 使用Go gRPC API為你的服務(wù)寫一個簡單的客戶端和服務(wù)端
安裝軟件和擼一個工程
先決條件
golang 安裝 關(guān)于這部分網(wǎng)上大把文章Google之 Protocol Buffer compiler, protoc關(guān)于protocol buffers的玩法可以參考往期文章。
選擇適合你平臺的預(yù)編譯好的二進(jìn)制文件(https://github.com/google/protobuf/releases),解壓并將可執(zhí)行文件protoc放到你的環(huán)境變了中
使用以下命令為Go安裝protobuf協(xié)議編譯器插件:
$ 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
定義服務(wù)
在gRPC中,我們是使用protocol buffers定義gRPC服務(wù)以及方法請求和響應(yīng)類型。首先要定義服務(wù),需要在.proto文件中指定一個命名service:
service Greeter {
...
}
然后,你可以在服務(wù)定義中定義rpc方法,并指定它們的請求和響應(yīng)類型。gRPC允許您定義四種服務(wù)方法,所有這些方法都在 Greeter服務(wù)中使用:
一個簡單的RPC,客戶端使用存根將請求發(fā)送到服務(wù)器,然后等待響應(yīng)返回,就像正常的函數(shù)調(diào)用一樣。
rpc SayHello (HelloRequest) returns (HelloReply) {}
服務(wù)器端流式RPC,客戶端向服務(wù)器發(fā)送請求,并獲取流來讀取后續(xù)的一系列消息。客戶端從返回的流中讀取數(shù)據(jù),直到?jīng)]有更多消息為止。如下你可以通過在響應(yīng)類型之前放置stream關(guān)鍵字來指定服務(wù)器端流方法。
rpc SayHello (HelloRequest) returns (stream HelloReply) {}
客戶端流式RPC,客戶端編寫消息序列,然后使用提供的流將消息發(fā)送到服務(wù)器。一旦客戶端寫完消息后,它將等待服務(wù)器讀取所有消息并返回其響應(yīng)。你可以通過將stream關(guān)鍵字放在請求類型之前指定客戶端流方法。
rpc SayHello (stream HelloRequest) returns (HelloReply) {}
雙向流式RPC,雙方都使用讀寫流發(fā)送一系列消息。這兩個流是獨立運行的,因此客戶端和服務(wù)器可以按照自己喜歡的順序進(jìn)行讀寫:例如,服務(wù)器可以在寫響應(yīng)之前等待接收所有客戶端消息,或者可以先讀取消息再寫入消息,或讀寫的其他組合。每個流中的消息順序都會保留。你可以通過在請求和響應(yīng)之前都放置stream關(guān)鍵字來指定這種類型的方法。
rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
.proto文件還包含用于服務(wù)方法中所有請求和響應(yīng)類型的protobuf協(xié)議消息類型定義-例如,
// 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的方式。
生成客戶端和服務(wù)器代碼
接下來,我們需要根據(jù).proto服務(wù)定義生成gRPC客戶端和服務(wù)器接口。可以使用帶有特殊gRPC Go插件的protocol buffer compiler protoc 進(jìn)行此操作。運行以下命令:(關(guān)于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,其中包含用于填充,序列化和檢索請求和響應(yīng)消息類型的所有protocol buffers代碼。 hello_grpc.pb.go,其中包含以下內(nèi)容: 客戶端使用Greeter服務(wù)中定義的方法調(diào)用的接口類型(或存根)。 服務(wù)器要實現(xiàn)的接口類型,也具有Greeter服務(wù)中定義的方法。
也可以簡單點兒
protoc hello/service.proto --go_out=hello/ --go-grpc_out=hello/
這樣只會生成一個文件,大同小異。
創(chuàng)建服務(wù)器
要使我們的Greeter服務(wù)發(fā)揮作用,服務(wù)端編寫分為兩個部分:
實現(xiàn)根據(jù)我們的服務(wù)定義生成的服務(wù)接口:完成我們服務(wù)的實際工作。 運行g(shù)RPC服務(wù)器以偵聽來自客戶端的請求,并將其分發(fā)到正確的服務(wù)實現(xiàn)。
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)
}
}
要構(gòu)建和啟動服務(wù)器,我們:
使用以下命令指定我們要用于偵聽客戶端請求的端口: lis,err:= net.Listen(...)。使用grpc.NewServer(...)創(chuàng)建gRPC服務(wù)器的實例。 在gRPC服務(wù)器上注冊我們的服務(wù)實現(xiàn)。 使用我們的端口詳細(xì)信息在服務(wù)器上調(diào)用Serve()進(jìn)行阻塞等待,直到進(jìn)程被殺死或調(diào)用Stop()為止。
創(chuàng)建客戶端
客戶端代碼主要是調(diào)用服務(wù)方法,我們首先需要
創(chuàng)建一個gRPC通道來與服務(wù)器通信。我們通過將服務(wù)器地址和端口號傳遞給grpc.Dial()來創(chuàng)建它,當(dāng)服務(wù)需要它們時,可以使用DialOptions在grpc.Dial中設(shè)置身份驗證憑據(jù)(例如TLS,GCE憑據(jù)或JWT憑據(jù))。Greeter服務(wù)不需要任何憑據(jù)。 設(shè)置gRPC通道后,我們需要一個客戶端存根來執(zhí)行RPC。例如,我們使用從.proto文件生成的pb包提供的NewGreeterClient方法獲取它。
client := hello.NewGreeterClient(conn)
調(diào)用服務(wù)方法:在gRPC-Go中,RPC在阻塞/同步模式下運行,這意味著RPC調(diào)用等待服務(wù)器響應(yīng),并且將返回響應(yīng)或錯誤。
整體代碼如下:
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所在目錄執(zhí)行docker build -t leoshus/grpc-demo:v1 .即可構(gòu)建一個鏡像。
push鏡像
因為構(gòu)建的鏡像要被之后的Kubernetes使用,所以需要講鏡像push到遠(yuǎn)端倉庫。當(dāng)然你也可以搭建私有倉庫來管理鏡像,這里我們使用官方的鏡像倉庫(https://hub.docker.com/)的演示。
推送倉庫前你需要進(jìn)行登錄注冊。
docker login #使用注冊的用戶名密碼登陸
docker push leoshus/grpc-demo:v1 # 完成鏡像的推送
編寫k8s資源文件
首先是服務(wù)端在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
這里我們?yōu)榱朔奖阊菔?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">gRPC的負(fù)載均衡機制,我們使用了Headless Service,保證該服務(wù)不會分配Cluster IP,也不通過kube-proxy做反向代理和負(fù)載均衡。而是通過DNS提供穩(wěn)定的網(wǎng)絡(luò)ID來訪問,將headless service的后端直接解析為pod ip列表,然后由gRPC的負(fù)載均衡機制來選擇使用哪臺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
然后你可以只需下面命令來進(jìn)行服務(wù)部署
kubectl apply -f grpc_server.yaml
有了服務(wù)端集群,自然需要有客戶端來訪問,接下來我們需要編寫客戶端的資源文件并部署在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
執(zhí)行kubectl get po -o wide可以看到當(dāng)前啟動的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狀態(tài)了,那么可以看下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的負(fù)載均衡已經(jīng)起作用了。
對于服務(wù)的擴(kuò)縮容,也只需要一行命令:
#縮容到1臺
kubectl scale --replicas=1 deployment grpc-server
#擴(kuò)容到5臺
kubectl scale --replicas=5 deployment grpc-server
當(dāng)然具體的擴(kuò)縮容的細(xì)節(jié)策略可以在yaml資源文件進(jìn)行配置
至此一個簡單的grpc整合k8s的工程就完成了。詳細(xì)代碼可見(https://github.com/leoshus/grpc-k8s-demo)
