基于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