简易微服务集群搭建指南
从完成一个grpc服务开始逐步搭建微服务集群
简易微服务集群搭建指南
这篇文章是一篇偏操作的文章,跟随这篇文章,你可以学会:
- 搭建一个简单的
grpc服务; - 使用
Docker将服务打包; - 使用
k8s将服务以Deployment的形式部署,并以Service的形式对外开放; - 在
k8s中,区分内部服务与外部服务;
这篇文章主要面对基本什么都不会的hxd,所以比较细致,比较流水账,可以自行选择需要的部分来看。
同时,这篇文章只负责教怎么做,至于grpc、k8s都是啥可以自行百度。
完成一个简单的grpc服务并在本机运行
grpc安装
grpc是谷歌推出的一款rpc框架,它支持多种语言并且使用范围颇广,它的安装也可以参考grpc官方文档。在安装grpc前,需要先安装好golang,并且对GOPATH进行配置,并将$GOPATH/bin添加入PATH。
首先安装Protocol Buffer:
sudo apt install -y protobuf-compiler
安装完成后,需要使用protoc --version进行验证,确保工具可用。
接下来安装go语言的插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
如果没有意外的话,grpc就安装好了。
grpc创建简单服务并在本地运行
在这一小节中,我们使用grpc完成一个简单的ping-pong服务:
- 服务端开放一个远程调用方法,接收来自客户端的字符串
- 客户端发来的是
ping,那么服务端返回pong - 客户端发来的不是
ping,服务端返回错误。
- 客户端发来的是
完成这一服务后,我们将在本地运行该程序。
项目结构
在GOPATH下,先新建pingpong文件夹,并在进入文件夹后执行:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [17:35:56]
$ go mod init
go: creating new go.mod: module pingpong
在本项目中,所有文件都存储在一个项目中,其中gomod用于存储项目名称和依赖的包信息,项目的其他部分由三个文件夹组成:
client protobuf go.mod service
protobuf:用于定义服务端接口,生成中间代码;service:引用并实现protobuf中定义的接口,对外提供服务;client:通过protobuf生成的中间代码访问service,完成远程过程调用;
在项目编译完成后,将存在两个可执行文件:
service:运行service后,将开启服务,等待客户端访问;client:用于访问service;
protobuf的定义与中间代码的生成
在protobuf文件夹中新建pingpong.proto进行编辑,我们希望:
- 服务端对外提供
pingpong服务; - 服务端接收
pingpongRequest,并且返回pingpongResponse给客户端。
因此做出如下定义:
syntax = "proto3";
option go_package = "pingpong/protobuf";
service Ops {
// PingPong return pong if request.message euqal to ping.
rpc PingPong (PingPongRequest) returns (PingPongResponse) {}
}
message PingPongRequest {
string message = 1;
}
message PingPongResponse {
string message = 1;
}
- 通过
service Ops定义了一个Ops服务,其中有一个远程调用方法PingPong,该方法接收PingPongRequest,返回PingPongResposne。 - 通过
message PingPongRequest定义了消息,消息中包含一个string类型的成员,被称为message;
除此之外,还对包名等进行了定义:
- 通过
option go_package:定义包所在路径为pingpong/protobuf,pingpong/protobuf就是当前项目的包名(与gomod项目名一致);
接下来,我们生成中间代码:
protoc ./pingpong.proto --go_out=. --go-grpc_out=.
将生成的代码放到本目录下,然后运行go mod tidy即可。
运行完go mod tidy后,go.mod文件如下:
module pingpong/protobuf
go 1.17
require (
google.golang.org/grpc v1.42.0
google.golang.org/protobuf v1.27.1
)
require (
github.com/golang/protobuf v1.5.0 // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
golang.org/x/text v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
)
到此为止,protobuf已经编写完成。
实现service
接下来在service中实现服务接口,服务接口的实现与运行分为两部分:
- 首先:需要实现
protobuf中定义的接口; - 其次:需要将实现好的对象注册到服务中;
实现代码如下:
package main
import (
"context"
"fmt"
"log"
"net"
"pingpong/protobuf"
"google.golang.org/grpc"
)
// service impl protobuf.Ops to provide pingpong service.
type service struct {
protobuf.UnimplementedOpsServer
}
// PingPong check req.Message, for msg euqal to ping, return pong, else return error.
func (s *service) PingPong(ctx context.Context, req *protobuf.PingPongRequest) (*protobuf.PingPongResponse, error) {
if req.Message == "ping" {
return &protobuf.PingPongResponse{Message: "pong"}, nil
}
return nil, fmt.Errorf("expect message = ping, but get message = %v", req.Message)
}
func main() {
lis, err := net.Listen("tcp", "0.0.0.0:23333")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
protobuf.RegisterOpsServer(s, &service{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
在这里,我们定义了service结构体,它实现了protobuf中的OpsServer接口,定义如下:
// OpsServer is the server API for Ops service.
// All implementations must embed UnimplementedOpsServer
// for forward compatibility
type OpsServer interface {
// PingPong return pong if request.message euqal to ping.
PingPong(context.Context, *PingPongRequest) (*PingPongResponse, error)
mustEmbedUnimplementedOpsServer()
}
随后,在main函数中,我们监听了23333端口,并且使用protobuf.RegisterOpsServer方法注册服务,最终使用 s.Serve(lis)的方法运行服务。
到此为止,pingpong服务已经实现了,运行service后,会开启并长期保持grpc服务。
实现client
接下来我们实现client,在这里我们会利用protobuf生成的代码来访问service;访问的过程也是非常简单:
- 首先:指定目标服务的
ip:port,新建一个client; - 随后:通过
client封装好的方法直接访问即可;
实现如下:
package main
import (
"context"
"log"
"pingpong/protobuf"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial("localhost:23333", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := protobuf.NewOpsClient(conn)
pingReq := &protobuf.PingPongRequest{Message: "ping"}
pingResp, err := client.PingPong(context.Background(), pingReq)
log.Printf("send req = %v, get resp = %v, %v", pingReq, pingResp, err)
otherReq := &protobuf.PingPongRequest{Message: "not ping"}
otherResp, err := client.PingPong(context.Background(), otherReq)
log.Printf("send req = %v, get resp = %v, %v", otherReq, otherResp, err)
}
在这里:
- 首先:使用
grpc.Dial方法指定service地址为localhost:23333,建立与service的连接,并通过该链接生成client; - 随后:使用
client封装的PingPong方法,分别向service发送message为ping和not ping的两条消息,并分别打印其结果;
这里的client代码,会向service发送两个rpc请求,并输出其返回结果,运行结束后将直接退出。
编译并运行服务端与客户端
通过go build进行编译,编译时通过-o指定输出文件名称分别为client_exec、service_exec:
go build -o ./client_exec ./client/main.go
go build -o ./service_exec ./service/main.go
先运行service_exec,效果如下(没有输出任何提示信息,因为我没有Println):
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [17:58:37]
$ ./service_exec
再运行client_exec,效果如下:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [18:01:59]
$ ./client_exec
2021/12/06 18:02:04 send req = message:"ping", get resp = message:"pong", <nil>
2021/12/06 18:02:04 send req = message:"not ping", get resp = <nil>, rpc error: code = Unknown desc = expect message = ping, but get message = not ping
这里一共输出了两条记录:
- 第一条记录表示向服务端发送了
ping,并且收到了message = pong; - 第二条记录表示向服务端发送了
not ping,并且:- 收到的
resp = nil,表示消息体为空; - 同时收到了来自服务端的错误提示;
- 收到的
简单总结
在这一节中,你学习到了:如何安装、编写、运行grpc。并且在本机上运行了grpc服务,还通过自定义的client访问了其中的pingpong方法。
你已经完成了一个类似于helloworld的grpc项目!
接下来,我们会将这个项目通过docker打包成镜像,并且在kubernetes集群中以服务的方式运行。
将grpc服务打包为镜像并发布到DockerHub
编写Dockerfile
Dockerfile用于描述镜像的构建过程,我们写了如下的dockerfile:
FROM golang:1.16 as builder
WORKDIR /go/src/pingpong
COPY . .
RUN go env -w GO111MODULE=on && \
go env -w GOPROXY=https://goproxy.io && \
go build -tags netgo -o pingpong_service ./service/main.go
FROM busybox
COPY --from=builder /go/src/pingpong/pingpong_service /pingpong_service
EXPOSE 23333
ENTRYPOINT [ "/pingpong_service" ]
这里可以分为上下两部分理解:
- 第一部分中,这一部分将根据源码构建可执行文件,过程中使用
golang:1.16作为builder:- 通过
WORKDIR规定,所有指令在路径/go/src/pingpong中执行 - 同时,将本地
pingpong下的所有代码拷贝到容器中的工作目录中; - 最后,运行
go build,编译service代码,生成可执行文件到/go/src/pingpong/pingpong_service
- 通过
- 第二部分中,构建运行镜像,第二部分的构建结果将作为最终的结果:
- 本部分基于
busybox,相较于第一部分基于的镜像,这是一个非常轻量的linux环境; - 随后:使用
COPY指令,指定从builder的/go/src/pingpong/pingpong_service拷贝到/pingpong_service;这一步将刚才编译好的可执行文件拷贝到运行镜像中。 - 最后:通过
EXPOSE指令对外暴漏23333端口,并通过ENTRYPOINT指定,容器运行时,直接执行编译好的可执行文件pingpong_service;
- 本部分基于
上面这种写法的主要优势在于通过利用了golang:1.16这个非常完备的编译镜像进行编译,再利用alpine这个体积非常小的镜像进行执行,最终构建出的镜像体积非常小。
编译容器并在本地运行
执行命令:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [18:29:47] C:1
$ docker build -t pingpong_service .
这表示基于当前目录的Dockerfile构建镜像,并将构建结果命名为pingpong_service,等待指令运行结束后,可以观察到提示:

通过docker images来看一下本地是否有相应的镜像存在:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [18:39:06] C:130
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
pingpong_service latest 673a79eb4ef3 5 minutes ago 17.1MB
这个镜像的大小仅有17.1MB。
接下来,需要通过docker run指令来将容器跑起来,但是在跑容器之前,需要注意:
pingpong_service在启动时会绑定23333接口,将其封装到镜像内后,绑定的不是本机的23333接口;
因此需要将镜像的23333接口,映射到本地的23333接口,这个概念非常容易理解,因此在运行时,通过-p host_port:container_port方法进行映射,同时使用-d使得服务能够后台运行:
docker run -d -p 23333:23333 pingpong_service
运行起来后,使用指令docker ps来查看跑起来的镜像:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [20:31:39]
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3defb55eace9 pingpong_service "/pingpong_service" 4 minutes ago Up 4 minutes 0.0.0.0:23333->23333/tcp, :::23333->23333/tcp competent_mayer
接下来,继续使用上一节中编译出来的client_exec来验证是否可用,得到结果如下,证明服务可用:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [20:41:52] C:130
$ ./client_exec
2021/12/06 20:41:53 send req = message:"ping", get resp = message:"pong", <nil>
2021/12/06 20:41:53 send req = message:"not ping", get resp = <nil>, rpc error: code = Unknown desc = expect message = ping, but get message = not ping
将容器推送到DockerHub
DockerHub和GitHub有点相似,GitHub作为代码的存储仓库存在,而DockerHub作为docker镜像的存储仓库存在。
想要使用DockerHub,需要现在DockerHub官网注册账号,注册账号完毕后,在命令行中通过docker_id和密码登录:
docker login
首先使用docker tag为镜像命名,其中pcgvphonebackend是docker_id,需要根据自身情况修改:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [21:04:50] C:130
$ docker tag pingpong_service:latest pcgvphonebackend/pingpong_service:v1
在打好标签后就可以推送到远端了:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [21:08:16] C:130
$ docker push pcgvphonebackend/pingpong_service:v1
The push refers to repository [docker.io/pcgvphonebackend/pingpong_service]
4df9e337354e: Pushed
9f2549622fec: Mounted from library/busybox
v1: digest: sha256:e443f60b2bfa24ea89c984a815f1f753b810934a37d55c74c8ce3463b9276270 size: 738
经过推送后,在任何机器上都可以通过docker pull pcgvphonebackend/pingpong_service:v1来拉取该镜像。
简单总结
在这一节中,我们学会了将一个服务包装成镜像,并把它通过docker运行。在包装、部署的过程中:
- 我们通过区分
builder和运行容器的方法降低运行容器的体积; - 通过将容器推送到
DockerHub降低远程部署难度;
但是在实际开发过程中,一个服务会在集群中被部署多份,服务间的访问往往不能通过ip:port直接访问。在下一节中,将学习如何使用k8s部署容器到集群。
k8s集群搭建
k8s需要一个master节点和多个worker节点,这就意味着需要多台物理机或是虚拟机,对多台机器进行操作无疑是繁琐的。minikube将这个过程简化,直接敲两行命令就能起一个集群,非常适合我这种搭个玩具的需求。
因此,在这节中,我们会:使用minikube搭建3节点k8s集群,并且使用kubectl对集群进行管理。
安装kubectl
在安装minikube前需要先安装kubectl,从名字上就能看出来,kubectl是用于管理k8s集群的命令行工具。安装过程可以参考k8s官方文档。下面基本上就是把官方文档抄了一遍,没啥意思。
首先使用curl下载kubectl的可执行文件。
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
再下载校验和文件,并且进行校验
curl -LO "https://dl.k8s.io/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256"
echo "$(<kubectl.sha256) kubectl" | sha256sum --check
收到结果kubectl: OK说明校验通过,最后直接安装:
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
安装完成后,验证一下是否可用:
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"22", GitVersion:"v1.22.4", GitCommit:"b695d79d4f967c403a96986f1750a35eb75e75f1", GitTreeState:"clean", BuildDate:"2021-11-17T15:48:33Z", GoVersion:"go1.16.10", Compiler:"gc", Platform:"linux/amd64"}
此时k8s集群根本不存在,所以kubectl还没啥用。
安装minikube并搭建集群
接下来安装minikube并且用它搭个集群,这个过程也是跟着minikube官方文档进行操作即可,看二道贩子写的也没啥意思。
直接下载二进制文件,并安装即可。
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
安装完成后,验证一下是否安装成功:
$ minikube version
minikube version: v1.24.0
commit: 76b94fb3c4e8ac5062daf70d60cf03ddcc0a741b
接下来,使用minikube搭建一个集群,这个过程minikube官方文档中写的也是非常详细,建议直接看官方文档。
使用minikube start命令,新建一个k8s集群:

需要注意的是,在
start集群时需要使用minikube start --cni=flannel,minikube会自动安装flannel插件,这使得pod可以跨节点通信。
安装完成后,minikube会将集群的配置信息放到~/.kube/config中,kubectl可以通过config对集群进行访问和管理,我们使用kubectl查看节点:
# wangsaiyu @ SaiyuWangPC in ~/.kube [16:21:29] C:130
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane,master 11m v1.22.3
可以看到,当前的集群中存在一个 master节点,master节点对内进行管理、统计,对外和用户交互,根据用户的指令执行操作或是提供信息。除了master节点外,集群还需要worker节点,worker会被master管理,在其调度下,服务会被部署到worker节点中。
下面我们使用minikube node add指令向集群中添加worker节点:

如上图所示,经过两次node add后,我们的集群中已经有一个master节点和两个worker节点。
将grpc服务部署到集群
在这一节中,我们将学习:
k8s集群中服务的组织结构;- 如何在
k8s集群中部署服务;
k8s还是比较复杂的,在这一节中,只会讲解一小部分相关的概念,以支持
k8s中的组织结构
自下而上观察k8s集群,可以将集群中的容器分为三层:
- 最底层的是容器
container,每个容器就是一个运行中的docker镜像,就像我们上一节中构建出来的一样; - 中层的是一个
Pod,Pod就像是以往服务中的一台物理机一样,其中可能包含多个container。每个Pod在集群中都拥有独立的IP地址,供其他节点访问; - 最上层是
Deployment:每一个Deployment下会有多个完全一致的Pod;
可以看作一组Deployment是一组完全相同的Pod的集合(虽然这么说很死板),这往往难以理解, 你可以带着疑问继续阅读。
现在,我们可以通过Deployment在集群中批量部署Pod,但是外部用户无法访问具体的服务,于是Service就应运而生。Service对内或对外暴漏一组Pod,来供用户访问。换而言之:k8s中记录了Service和一组Pod的对应关系,在用户用户的视角中,只存在Service的概念,用户通过Service进行访问,k8s分配某一个具体的Pod进行响应。
在这一小节中,我们学习了k8s集群的简单组成,在下一小节中,将学习如何通过Deployment将之前开发的pingpong_service部署到集群中。
通过Deployment部署pingpong_service并通过Service访问
在这一节中,我们先将pingpong_service通过depolyment的形式部署到集群中,再通过service的形式对外暴露。
通过Deployment部署pingpong_service
k8s中存在着非常多种的对象,为了简化维护过程,k8s创造了一套使用yaml对象描述被管理对象的通用描述方法,你可以在官方文档中查看更多关于配置文件的描述:
新建一个描述文件pingpong_service_deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pingpong-service-backend-deployment
spec:
selector:
matchLabels:
app: pingpong-service-backend
replicas: 3
template:
metadata:
labels:
app: pingpong-service-backend
spec:
containers:
- name: pingpong-service-backend
image: pcgvphonebackend/pingpong_service:v1
ports:
- containerPort: 23333
这个配置文件描述了一个Deployment对象,其中记录了很多的信息:
metadata元数据,用于记录这个Deployment的名称,以及他的标签、namespace等等信息。在这里只记录了它的名称;spec字段定义了该Deployment的期望状态,我们拆解来看replicas记录该Deployment需要有多少份相同的Pod副本;template记录每一个Pod副本的内容,其中:containers以列表的形式记录Pod中的每一个容器;
在完成描述文件后,可以通过kubectl apply来将对象提交到集群:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [22:40:46]
$ kubectl apply -f ./pingpong_service_deployment.yaml
deployment.apps/pingpong-service-backend-deployment created
提交成功后,可以通过kubectl get deployment、kubectl get pods来观察部署的情况,部署成功后如图所示:

同时,可以通过kubectl describe pods来获取每一个pod的详细信息,以我们的deployment中的pod之一为例:

该命令可以获取到Pod的状态、描述信息,我们也可以发现,服务被部署在minikube-m03上。
经过统计,我们部署的Deployment中,有两个部署在minikue-m03,一个部署在minikube-m02,部署在哪一台机器上完全由k8s决定。当服务宕机或是节点缺失导致Pod副本数量降低时,k8s会自动将重新选择节点部署Pod,直到节点数量达到提交的replicas=3为止。
通过Service将deployment暴漏给外部用户
在上一节中,我们通过Deployment将服务部署在集群中,但是我们怎么去访问这个服务呢?在这一节中,我们将使用Service来将Deployment暴漏给外部用户进行访问。
与Deployment对象一样,Service对象也可以通过同样的方法进行描述:
新建文件pingpong_service.yaml:
apiVersion: v1
kind: Service
metadata:
name: pingpong-service-backend-service
labels:
app: pingpong-service-backend
spec:
ports:
- port: 23333
targetPort: 23333
protocol: TCP
selector:
app: pingpong-service-backend
type: NodePort
这个信息非常易懂,我们只讲解下半部分内容:
ports描述需要对外开放的端口,我们的服务在23333上,使用TCP协议;- 使用
selector选择需要开放的服务,在这里我们使用app: pingpong-service-backend作为选择器,将相应的pod暴漏; - 这里的
Service存在多种类型,我们选择了NodePort,这种方法会将服务映射到workNode物理机上;
关于NodePort类型的服务,其访问方式如下:

服务打到某一个Node后,Node会将请求转发到Service中的某一个Pod中。
我们使用kubectl apply -f ./pingpong_service.yaml提交修改:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [23:27:12] C:130
$ kubectl apply -f ./pingpong_service.yaml
service/pingpong-service-backend-service created
使用kubectl get service查看效果:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [0:21:32]
$ kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8h
pingpong-service-backend-service NodePort 10.102.10.54 <none> 23333:31485/TCP 59m
其中kubernetes是k8s默认对外提供的服务,pingpong-service-backend-service是我们新建的服务。
使用kubectl describe nodes | grep ip查看各个节点的IP:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [0:35:19]
$ kubectl describe nodes | grep IP
InternalIP: 192.168.49.2
InternalIP: 192.168.49.3
InternalIP: 192.168.49.4
集群中有三个节点,其中master节点的ip为192.168.49.3,剩下两个是worker节点的ip。
我们修改client中目标的ip:
conn, err := grpc.Dial("192.168.49.4:31485", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
重新编译、运行:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [0:39:43] C:130
$ go build -o ./client_exec ./client/main.go && ./client_exec
2021/12/07 00:39:44 send req = message:"ping", get resp = message:"pong", <nil>
2021/12/07 00:39:44 send req = message:"not ping", get resp = <nil>, rpc error: code = Unknown desc = expect message = ping, but get message = not ping
这样就可以通过节点的ip:port访问集群内的服务了。
简单总结
在这一节中,我们将grpc服务的镜像通过k8s deployment的形式进行部署,并通过NodePort service的形式对外开放访问。
但美中不足的是:Nodeport能够将服务暴漏在所有的Node节点上,但是用户应该选择哪一个Node节点呢?这是难以指定的,在此基础上,还需要在集群外添加一Nginx才能够对Node节点进行负载均衡。
除此之外,还有两个重要的任务没有完成:
- 如何控制服务是否暴漏给集群外访问?
- 在集群内访问时能否不通过动态的
port:ip,而是通过形如rpc.project.service的方式进行访问呢?
在接下来的小节中,我们会一一回答。
区分内部服务与外部服务
在上一节中,我们将pingpong_service以NodePort的服务类型部署到k8s集群中,利用多个副本对外提供服务。但并非所有服务在部署时都想被外部用户访问,因此区分内部服务与外部服务是很有必要的。
我们会将原有的pingpong_service转化为k8s集群中的内部服务,同时继续包装client,将其作为对外的http服务。
将client包装为http服务
原有的client会创建一个可以访问pingpong_service的client,并发送两条消息已验证服务的可用性。现在我们希望将client继续封装,将PingPong方法包装成一个HTTP GET请求,这样我们可以在集群外的机器上直接通过curl来检查服务是否可用,我们期望:
- 用户发送
HTTP GET请求到,http://host:port/pingpong?message=xxx; - 封装后的
client发送PingPongRequest到pingpong_service,其中message=xxx; - 封装后的
client接收pingpong_service的resposne,并以HTTP Response的形式返回给用户;
修改可以分为两部分讨论:
- 对外
http服务:需要使用go原生的http框架对外提供服务,为此需要实现其http.HandleFunc; - 对内由于集群内的
Pod地址不固定,因此希望通过服务名称进行访问;
根据以上需求,修改client/main.go:
package main
import (
"log"
"net/http"
"pingpong/protobuf"
"google.golang.org/grpc"
)
var serviceLocation = "pingpong-service-backend-service:23333"
func handlePingPong(rw http.ResponseWriter, r *http.Request) {
conn, err := grpc.Dial(serviceLocation, grpc.WithInsecure())
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
defer conn.Close()
message := r.URL.Query().Get("message")
client := protobuf.NewOpsClient(conn)
resp, err := client.PingPong(r.Context(), &protobuf.PingPongRequest{Message: message})
if err != nil {
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(resp.GetMessage()))
}
func main() {
http.Handle("/pingpong", http.HandlerFunc(handlePingPong))
if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil {
log.Println(err.Error())
}
}
在这个client中,我们将原有创建client、发送msg的逻辑封装到handlePingPong函数中,handlePingPong实现了go语言中通用的http请求处理函数。它接收http.Request,并将响应的结果写入rw http.ResponseWriter,该函数return后,go http框架会进行后续处理。在这里,收到请求后:
- 首先利用
serviceLocation创建client; - 再通过
r.URL.Query()提取出请求URL中的query部分,从其中获取message参数; - 最终将消息发送,无论收到什么,都将以字符串的形式返回;
完成http请求处理函数后,使用http.Handle方法,将该函数注册到/pingpong路径下,最终使用http.ListenAndServe("0.0.0.0:8080", nil)方法,指定监听机器的8080端口提供服务。
到此为止,client服务的包装已经完成,可以在本地运行该服务,并通过curl工具访问,访问结果如下图所示:

原因非常明显,是因为pingpong-service-backend-service这个域名不存在,这个问题需要在集群中解决。
我们将被包装后的client称为pingpong_client_server。
将pingpong_client_server包装为镜像并发布到k8s
这一节是纯流水账,自己能完成就不需要看。
修改dockerfile并编译镜像
修改dockerfile:
FROM golang:1.16 as builder
WORKDIR /go/src/pingpong
COPY . .
RUN go env -w GO111MODULE=on && \
go env -w GOPROXY=https://goproxy.io && \
go build -tags netgo -o pingpong_client_service ./client/main.go
FROM busybox
COPY --from=builder /go/src/pingpong/pingpong_client_service /pingpong_client_service
EXPOSE 8080
ENTRYPOINT [ "/pingpong_client_service" ]
编译docker镜像:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [20:27:38] C:130
$ docker build -t pcgvphonebackend/pingpong_client_service:v1 .
推送docker镜像到dockerhub:
# wangsaiyu @ SaiyuWangPC in ~/go/src/pingpong [20:28:48]
$ docker push pcgvphonebackend/pingpong_client_service:v1
将pingpong_client_service发布为对外服务
这一过程与之前的pingpong_service完全相同:
deployment描述文件pingpong_client_service_deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pingpong-client-service-deployment
spec:
selector:
matchLabels:
app: pingpong-client-service
replicas: 3
template:
metadata:
labels:
app: pingpong-client-service
spec:
containers:
- name: pingpong-client-service
image: pcgvphonebackend/pingpong_client_service:v1
ports:
- containerPort: 8080
使用kubectl apply -f ./pingpong_client_service_deployment.yaml将应用变更,并使用kubectl get pods观察部署情况,等待全部部署完成:
# ubuntu @ VM-0-10-ubuntu in ~ [17:12:59]
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
flask-pod 1/1 Running 0 16h
pingpong-client-service-deployment-6b97ccd969-6rg5p 1/1 Running 0 16h
pingpong-client-service-deployment-6b97ccd969-dqhxz 1/1 Running 0 16h
pingpong-client-service-deployment-6b97ccd969-ld4sb 1/1 Running 0 16h
pingpong-service-backend-service-84b86f765f-chxjz 1/1 Running 0 16h
pingpong-service-backend-service-84b86f765f-cr4tq 1/1 Running 0 16h
pingpong-service-backend-service-84b86f765f-gpjxt 1/1 Running 0 16h
最后,我们需要创建Service将Deployment暴漏给外部访问,因此还是选择NodePort形式,但这一次我们直接借助kubectl工具完成这一过程:
# ubuntu @ VM-0-10-ubuntu in ~/k8s [17:16:07]
$ kubectl expose --type='NodePort' deployment/pingpong-client-service-deployment
service/pingpong-client-service-deployment exposed
# ubuntu @ VM-0-10-ubuntu in ~/k8s [17:16:37]
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 17h
pingpong-client-service-deployment NodePort 10.104.99.251 <none> 8080:31598/TCP 3s
pingpong-service-backend-service NodePort 10.109.59.76 <none> 23333:32303/TCP 42s
这里直接使用了kubectl expose命令,将deployment/pingpong-client-service-deployment以服务的形式暴漏,并指定了NodePort类型。
部署完成后,需要使用curl试一试跑步跑得通:
# ubuntu @ VM-0-10-ubuntu in ~/k8s [17:16:42]
$ kubectl describe nodes | grep IP
InternalIP: 192.168.49.2
InternalIP: 192.168.49.3
InternalIP: 192.168.49.4
这里我们访问192.168.49.3这一Node节点的31598端口,就可以访问到pingpong-client-service:
# ubuntu @ VM-0-10-ubuntu in ~/k8s [17:16:57]
$ curl 192.168.49.3:31598/pingpong\?message=ping
pong
到此为止,我们将pingpong_client_service以NodePort的形式部署并供外部访问,在发送curl 192.168.49.3:31598/pingpong\?message=ping请求到达pingpong_client_service服务后,pingpong_service通过"pingpong-service-backend-service:23333"访问到pingpong-service完成调用。
但美中不足的是,pingpong-service仍然以NodePort形式提供服务,这代表外部用户仍然能够直接访问pingpong-service,这不符合我们的期望。
使用ClusterIP形式限制外部用户访问pingpong-service
这一小节中,我们将介绍如何将服务隔离,仅供集群内部访问。
k8s提供了多种Service类别,上一节中使用的NodePort形式,可以将服务映射到物理机端口上,供外部用户访问。在这一节中,我们将学习ClusterIP类型服务,首先将原有的服务删除,并重建一个ClusterIP类型服务:
# ubuntu @ VM-0-10-ubuntu in ~ [17:44:16]
$ kubectl delete svc pingpong-service-backend-service
service "pingpong-service-backend-service" deleted
# ubuntu @ VM-0-10-ubuntu in ~ [17:44:32]
$ kubectl expose deployment/pingpong-service-backend-service
service/pingpong-service-backend-service exposed
# ubuntu @ VM-0-10-ubuntu in ~ [17:44:57] C:1
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 17h
pingpong-client-service-deployment NodePort 10.104.99.251 <none> 8080:31598/TCP 28m
pingpong-service-backend-service ClusterIP 10.96.152.213 <none> 23333/TCP 7s
现在我们建立起了ClusterIP类型的pingpong-service-backend-service ,对于该服务,k8s为其分配了一个ClusterIP,集群内的节点可以使用ClusterIP访问该服务,其原理如下:

当pod-nginx需要访问pod-python时,会使用service-python的clusterIP进行访问,该IP是集群中的虚拟IP,在iptable模式下,kube-proxy会将ClusterIP到PodIP的映射关系记录到节点的iptable中,这样在请求时,会在本地将IP地址进行转换,ClusterIP会被随机转换为一个PodIP进行访问。换而言之,节点在通过CluserIP访问一个服务时,实质上是访问了服务背后的一个Pod。
回忆一下pingpong_client_service的源码,是通过服务名称进行访问的,这是因为k8s中存在一coreDNS服务,专门将serviceName解析到cluserIP。
到此为止,我们已经将pingpong-service转化为k8s集群内的内部服务,我们已经完成了一个简单集群的构建。