抖音大项目开发部署全流程<叁>(微服务的改造|docker镜像制作|docker-compose部署)|青训营笔记

186 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第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程序的镜像,又可以分成两条路:

  1. Golang本身支持跨平台编译,使用CGOGOOSGOARCH等flag,编译好后的二进制文件放到一个最小运行时环境,如alpine等。
  2. 使用两步编译,为了处理包冲突环境隔离等问题,可以将Go源码放到一个对应Golang版本的Docker镜像中,使用go build指令,再把编译完成后的二进制文件放到一个更小的运行时环境中(alpine)去运行。在这里我选择了第二种方式,因为主要编写一个docker file后就可以适合多个场景。

首先,上面提到的alpine是一个社区开发的面向安全应用的轻量级Linux发行版。主要其docker镜像大小之后5MB左右,非常轻量。

image.png

正题来了,我们使用两步来制作我们的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来解析对应服务地址的。

运行起来的状态如下:

image.png

这一篇差不多结束了,最后一篇是关于k8s上的部署,说实话k8s上的部署看起来就是简单的写yaml,但是最痛苦的就是yaml写错了,但是它不报错,你得找半天,找了半天也不知道哪个东西写错了。不摆的话,下一节写写吧。