基于k8s service服务发现的微服务部署

808 阅读5分钟

基于kitex的微服务

场景构造和框架生成

首先构造一个应用场景:图书管理系统
一个Server的rpc服务提供图片管理的核心功能,一个Client的http服务提供面向网络的api能力,将Server的功能转化成http的api,Client通过rpc调用Server的能力。

rpc框架使用kitex,详情见kitex官网

首先是图书管理系统的核心实体类Book的定义,kitex默认使用thrift来定义idl

namespace go book

enum StoreStatus {
    Disable = 0,
    Enable = 1,
    Borrowed = 2
}
struct Book {
    1: i64 id,
    2: string name,
    3: StoreStatus status
}

再定义List接口来返回需要的图书列表,接口的Req有offset和size两个参数,offset表示要查询的位移,size表示需要的数据个数,返回的Resp包括一个Book的列表和一个more表示是否还有更多数据

struct ListRequest {
    1: optional i32 size,
    2: optional i32 offset
}
struct ListResponse {
    1: list<Book> books,
    2: bool more
}

service BookService {
    ListResponse List(1: required ListRequest req)
}

将上面的两段代码定义在lib_book.thrift文件中,使用命令kitex -service study.book.server book_lib.thrift来生成服务端代码和服务端脚手架代码。

服务实现

只是为了演示,这里在内存中用一个map来模拟Book的储存,用于实现List接口,在生成的脚手架代码的handler.go文件中实现如下代码功能

// BookServiceImpl implements the last service interface defined in the IDL.
type BookServiceImpl struct{}

var books = []*book.Book{}

func init() {
	for i := 0; i < 100; i++ {
		books = append(books, &book.Book{
			Id:     int64(i),
			Name:   fmt.Sprintf("book%d", i+1),
			Status: book.StoreStatus_Enable,
		})
	}
}

// List implements the BookServiceImpl interface.
func (s *BookServiceImpl) List(ctx context.Context, req *book.ListRequest) (resp *book.ListResponse, err error) {
	offset := int32(0)
	if req.Offset != nil {
		offset = *req.Offset
	}
	size := int32(20)
	if req.Size != nil {
		size = *req.Size
	}
	resp = &book.ListResponse{}
	resp.More = offset+size < int32(len(books))
	resp.Books = make([]*book.Book, 0)
	if int(offset) < len(books) {
		resp.Books = books[offset:math.MinInt(int(offset+size), len(books))]
	}
	return
}

接着是客户端的代码,同样用kitex工具生成客户端代码kitex lib_book.thrift,由于客户端是个http的服务,不用像服务端一样生成kitex的rpc脚手架代码,因此直接手写代码。和之前的show_pod_name服务一样,首先是定义http请求的resp处理工具类,在之前的基础上,用

{
    "err_no":0,
    "err_tips":"success"
    "data":xxx
}

这样的json结构来定义http的resp,具体的工具代码如下

package main

import (
	"encoding/json"
	"net/http"
)

func HttpError(resp http.ResponseWriter, err error) {
	resp.WriteHeader(http.StatusOK)
	resp.Write([]byte(err.Error()))
}
func HttpSuccess(resp http.ResponseWriter, ret interface{}) {
	resp.WriteHeader(http.StatusOK)
	retData, err := json.Marshal(ret)
	if err != nil {
		HttpError(resp, err)
		return
	}
	_, err = resp.Write(retData)
	if err != nil {
		HttpError(resp, err)
	}
}

type RespData struct {
	ErrNo   int32       `json:"err_no"`
	ErrTips string      `json:"err_tips"`
	Data    interface{} `json:"data"`
}

func HttpSuccessData(resp http.ResponseWriter, data interface{}) {
	HttpSuccess(resp, RespData{
		ErrNo:   0,
		ErrTips: "success",
		Data:    data,
	})
}

然后是真正的代码实现,创建一个http的server,并且在handler中创建之前生成的client,调用client的List函数,rpc调用server的服务。

func ListBook(w http.ResponseWriter, r *http.Request) {
	// client, err := bookservice.NewClient("lib-server", client.WithHostPorts("192.168.49.2:32004"))
	client, err := bookservice.NewClient("lib-server:8888", client.WithResolver(dns.NewDNSResolver()))
	if err != nil {
		panic(err)
	}
	req := &book.ListRequest{}
	if offset, ok := r.URL.Query()["offset"]; ok {
		i, err := strconv.ParseInt(offset[0], 10, 64)
		if err != nil {
			fmt.Printf("parse offset err=%v\n", err)
		} else {
			i32 := int32(i)
			req.Offset = &i32
		}
	}
	if size, ok := r.URL.Query()["size"]; ok {
		i, err := strconv.ParseInt(size[0], 10, 64)
		if err != nil {
			fmt.Printf("parse size err=%v\n", err)
		} else {
			i32 := int32(i)
			req.Size = &i32
		}
	}
	resp, err := client.List(context.Background(), req)
	if err != nil {
		HttpError(w, err)
		return
	}
	fmt.Printf("List books=%v,more=%t\n", resp.Books, resp.More)
	HttpSuccessData(w, struct {
		Books []*book.Book `json:"books"`
		More  bool         `json:"more"`
	}{
		Books: resp.Books,
		More:  resp.More,
	})

}

func main() {

	http.HandleFunc("/book/list", ListBook)
	fmt.Println("Server starting on 8080...")
	http.ListenAndServe(":8080", nil)
}

先将server在本地起起来,再用client代码用client.WithHostPorts的方式来调用server进行测试,发现可以测试通过,然后再在k8s中运行时才用client, err := bookservice.NewClient("lib-server:8888", client.WithResolver(dns.NewDNSResolver()))来创建client进行rpc调用

Docker镜像构建

还是参考之前的镜像构建方案,在项目根目录创建Dockerfile文件,在Dockerfile文件中实现镜像的构建过程

FROM golang:alpine as build
RUN apk update && apk add git
RUN apk --no-cache add tzdata
WORKDIR /app
ADD . .
RUN go env -w GOPROXY=https://goproxy.io,direct
RUN go env -w GO111MODULE=on
RUN CGO_ENABLE=0 GOOS=linux ./build.sh

FROM alpine as final
COPY --from=build /app/output .
# ENTRYPOINT ["./main"]
CMD ["sh", "bootstrap.sh"]

这里照样是先在build这层镜像中进行构建,调用server项目中的build.sh脚本,编译的结果会输出到output目录,再到final这层镜像中生成最后能部署的镜像,将构建镜像中生成的编译产物copy到运行镜像中,在镜像运行起来时执行bootstrap.sh脚本

参考# 将自己的应用程序部署到k8s上step by step的镜像build和储存方案,使用命令docker build -t 192.168.210.128:5000/lib_server .将镜像构造生成到本地,再使用命令docker push 192.168.210.128:5000/lib_server:latest将镜像推到本地起的registery docker镜像仓库中。

对于client的镜像构建也一样,只是Dockerfile不太一样,也是参考之前的show_pod_name

FROM golang:alpine as build
RUN apk update && apk add git
RUN apk --no-cache add tzdata
WORKDIR /app
ADD . .
RUN go env -w GOPROXY=https://goproxy.io,direct
RUN go env -w GO111MODULE=on
RUN CGO_ENABLE=0 GOOS=linux ./build.sh

FROM alpine as final
COPY --from=build /app/output .
# ENTRYPOINT ["./main"]
CMD ["sh", "bootstrap.sh"]

基于镜像的部署

还是像之前一样,先在本地的docker把server和client运行起来,记得需要在client中修改ip,在docker中部署时使用命令docker run -d --name lib_server -p 8888:8888 192.168.210.128:5000/lib_server将监听的端口映射到宿主机的8888端口,这样client用的ip:port就是localhost:8888,同样client也部署到docker中进行测试,测试通过后将client项目中的client创建改成基于k8s的cdn服务发现。

最简单的部署方案还是使用Rancher直接部署Deployment并创建Service,Service的name一定要是lib-server,这是因为在client工程中client的创建方式是基于k8s的服务发现。同时client也要创建nodePort类型的service,这样才能在k8s外部访问到client的http服务,将client的http监听的8080映射到nodePort的端口。

最后还是像show_pod_name一样,使用node的ip+nodePort的port的方式来访问client的http服务,或者使用minikube的minikube service list来获取lib-client的访问url