这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记。
本次笔记主要记录的是微服务的改造,将所有配置参数通过flag库来暴露,使其能够通过命令行或者环境变量获得运行所需的配置参数。然后通过两步编译将源码编译成二进制文件,运行在alpine-linux上,最终将其制作成为docker image,并上传到白嫖来的镜像仓库中。最后使用docker compose,通过编写简单的yaml,完成docker compose部署。
一、配置参数暴露的改造
在上一版中,我使用了Nacos作为了服务注册和配置中心。但是如果之后需要在k8s上部署的话,关于是否需要额外的服务注册中心这件事讨论的也很广泛,所以我考虑在之后的两个部署中先放弃Nacos的服务发现和配置中心,所需的所有参数从环境变量和命令行中获取。
原生flag库在平常的使用中也很频繁,但是原生库只能从命令行中获得参数,很多容器编排过程中,运行是隔离的;所以更愿意把参数写到环境变量中,所以我使用了“github.com/namsral/flag” 库,这个库是Go的flag 包的替代品,增加了解析文件和环境变量。使用方式与Go原生flag库相似,但是功能更强大了。
六个微服务中主要需要的参数是Mysql数据库的配置,如下:(注意必须调用,Parse())
var (
MysqlInfo cfg.MysqlConfig
DB *gorm.DB
ServicePort int
)
func init() {
InitParse()
InitDB()
}
func InitParse() {
flag.StringVar(&MysqlInfo.User, "mysql_user", "root", "mysql user")
flag.StringVar(&MysqlInfo.Password, "mysql_password", "root", "mysql password")
flag.StringVar(&MysqlInfo.Host, "mysql_host", "127.0.0.1", "mysql host")
flag.IntVar(&MysqlInfo.Port, "mysql_port", 3306, "mysql port")
flag.StringVar(&MysqlInfo.Name, "mysql_name", "douyin_user", "mysql name")
flag.IntVar(&ServicePort, "service_port", 8080, "service port")
flag.Parse()
}
在Gin网关中需要获取的是六个微服务的Resolver地址,这里的resolver地址包括ip:port,之前的nacos:///xxx形式,dns:///xxx形式。但是具体resolver的解析方式需要自己实现,但是获得resolver的方式是一样的:(下面的默认形式是ip:port,但后面的服务发现和自动扩缩容就需要更复杂的revolver)
var (
ConnMap = make(map[string]proto.ServerClient)
ServicePort int
userService string
relationService string
feedService string
commentService string
favoriteService string
publishService string
localAddr string
connetTimeout int
)
func InitParse() {
flag.IntVar(&ServicePort, "service_port", 8080, "service port")
flag.StringVar(&userService, "user_service_resolver", "dnsss:///user-balance-svc:8080", "user service resolver")
flag.StringVar(&relationService, "relation_service_resolver", "127.0.0.1:8082", "relation service resolver")
flag.StringVar(&feedService, "feed_service_resolver", "127.0.0.1:8083", "feed service resolver")
flag.StringVar(&commentService, "comment_service_resolver", "127.0.0.1:8084", "comment service resolver")
flag.StringVar(&favoriteService, "favorite_service_resolver", "127.0.0.1:8085", "favorite service resolver")
flag.StringVar(&publishService, "publish_service_resolver", "127.0.0.1:8086", "publish service resolver")
flag.StringVar(&localAddr, "local_addr", "", "local addr")
flag.IntVar(&connetTimeout, "connet_timeout", 10, "connet timeout")
flag.Parse()
}
二、Docker镜像的两步制作(编译和运行分开)
在云原生和容器部署火热的当前,如何把自己的服务打包成可执行文件,连同运行环境一并提供给其他人这件事是非常基础和关键的。
总所周知,Golang是一个需要编译运行的语言,也就是Golang写出来的东西只需要一些基础的OS运行时就可以运行,与Java不同,Java需要更大的运行时支撑(JVM)。
因此制作Go程序的镜像,又可以分成两条路:
- Golang本身支持跨平台编译,使用
CGO、GOOS、GOARCH等flag,编译好后的二进制文件放到一个最小运行时环境,如alpine等。 - 使用两步编译,为了处理
包冲突、环境隔离等问题,可以将Go源码放到一个对应Golang版本的Docker镜像中,使用go build指令,再把编译完成后的二进制文件放到一个更小的运行时环境中(alpine)去运行。在这里我选择了第二种方式,因为主要编写一个docker file后就可以适合多个场景。
首先,上面提到的alpine是一个社区开发的面向安全应用的轻量级Linux发行版。主要其docker镜像大小之后5MB左右,非常轻量。
正题来了,我们使用两步来制作我们的docker镜像,如:
# 基准镜像 使用golang:1.18.2-alpine作为编译镜像
FROM golang:1.18.2-alpine AS builder
# 配置 打开gomod 和 配置代理
RUN go env -w GO111MODULE=on
RUN go env -w GOPROXY=https://goproxy.cn,direct
# 拷贝源代码 将本地源代码拷贝到镜像容器中
COPY . /go/src/service
# 编译 设置workdir以及编译
WORKDIR /go/src/service
RUN go install main.go
# 生产镜像 将alpine作为生产镜像,并把编译容器中产生的二进制文件拷贝到当前镜像容器中
FROM alpine:3.15
COPY --from=builder /go/bin/main /bin/main
# 声明暴露端口
EXPOSE 8080
# 服务入口
ENTRYPOINT ["/bin/main"]
运行以下命令启动镜像制作:docker build -t <your_service_name> -f Dockerfile .
这就是在本地成功制作完成镜像了。这时候需要去腾讯,阿里白嫖一个镜像仓库。
根据文档创建自己的镜像仓库,本地登陆上对应的服务docker log后,然后根据指导将镜像打上tagdocker tag,最后使用docker push将镜像推到镜像仓库。
到这里就完成了镜像的制作与上传。
三、使用docker-compose完成多镜像编排部署
完成镜像制作上传后,我们可以通过docker pull/run来拉取和运行我们的单个镜像。但是如果多个镜像需要一起启动,这时候一个个拉取启动就比较麻烦了。这时候就需要使用docker-compose来进行多镜像编排部署了,虽然我们最终目的是在k8s上部署,但是不妨碍我们使用一个docker-compose来快速地拉起一个整体服务(因为k8s部署确实很繁琐)。
接下来的所有工作就是一个yaml工程师了。
首先需要写一个docker-compose.yaml来部署多镜像。如下:
version: "3"
services:
user-srv:
image: "ccr.ccs.tencentyun.com/douyin-test/user-srv:v2.0"
ports:
- "8081:8080"
environment:
- MYSQL_HOST=127.0.0.1
- MYSQL_PORT=3307
networks:
- douyin-net
restart: always
gateway:
image: "ccr.ccs.tencentyun.com/douyin-test/gateway-gin:v2.1"
ports:
- "8080:8080"
networks:
- douyin-net
environment:
- USER_SERVICE_RESOLVER=user-srv:8080
- RELATION_SERVICE_RESOLVER=relation-srv:8080
- FEED_SERVICE_RESOLVER=feed-srv:8080
- COMMENT_SERVICE_RESOLVER=comment-srv:8080
- FAVORITE_SERVICE_RESOLVER=favorite-srv:8080
- PUBLISH_SERVICE_RESOLVER=publish-srv:8080
restart: always
networks:
douyin-net:
external: true
这里我只放了一部分,完整文件在github下。
其中services声明了下面的多个服务需要一起启动。
第一行user-srv是服务的名称;image是对应的镜像名称,本地找不到的话就会去docker.io或其他地方拉取;ports是端口暴露与映射;networks是声明使用的网络,这样的话就可以使用服务名称来找到当前网络下的服务地址;environment是环境变量设置。
在gateway服务中,在环境变量中设置的resolver就是通过服务名称:port来解析对应服务地址的。
运行起来的状态如下:
这一篇差不多结束了,最后一篇是关于k8s上的部署,说实话k8s上的部署看起来就是简单的写yaml,但是最痛苦的就是yaml写错了,但是它不报错,你得找半天,找了半天也不知道哪个东西写错了。不摆的话,下一节写写吧。