线上答题系统,微服务架构的小小实践,项目代码

一、概述

  微服务的部署有下列几种方式:单主机多服务实例模式、每个主机一个服务实例模式、Serverless部署。这里选择的是在docker容器中部署每一个模块的微服务,因为使用容器可以实现快速启动,方便增加和删除某个模块服务实例,且受操作系统的影响比较小。当然,众多容器的管理也有些挑战。

  最终我们的系统架构图如下,其中每个圆圈就是一个独立的docker容器,结合docker-compose进行服务编排,实现一键启动整个系统。最终会有web应用、event service、user service、union service、problem service、participant service、credit service,7个容器服务。答题模块会有两个服务,participant service和credit service。

  base image、web image、event image、user image、union image、problem image、participant image、credit image等8个镜像,其他7个镜像都继承自base image。

  以及一个answer_net docker网络,各个容器都加入该网络。

docker 多服务器部署 docker部署多个微服务_项目架构

二、DOCKER

  docker的一些基础概念,容器、镜像操作等相关文档很多,可以直接在网上找。进行docker部署,有三点概念需要好好了解,那就是:
数据卷volume–解决如何存放代码
网络docker network–解决各个容器间如何通信,以及对外暴露端口
docker-compose–实现服务编排及自动化运行

2.1 数据卷volume

2.1.1 volume特性

  docker本身提供了一种机制volume,可以将主机上的某个目录与容器的某个目录(称为挂载点、或者叫卷)关联起来,容器上的挂载点下的内容就是主机的这个目录下的内容,这类似linux系统下mount的机制。当我们修改主机上该目录的内容时,不需要同步容器,容器对应的内容的修改也是立即生效。
volume具有如下特点:

  • volume在容器创建时就初始化,在容器运行时就可以使用其中的文件
  • volume能在不同的容器之间共享和重用
  • 对volume中的数据的操作会马上生效
  • 对volume中数据操作不会影响到镜像本身
  • volume的生存周期独立于容器的生存周期,即使删除容器,volume仍然会存在,没有任何容器使用的volume也不会被Docker删除

  由于volume的这些特性,我们为每个模块服务的代码都创建对应的volume,这样在宿主机中修改了代码后,再配合热加载机制,容器中的应用也能实时更改。

2.1.2 volume使用

  1. docker volume创建数据卷并与容器中目录绑定
docker volume create —name blog_background_code

  在主机上生成的挂载目录是在 /var/lib/docker/volumes/blog_background_code/_data
可使用docker volume inspect blog_background_code查看详情

docker volume inspect blog_background_code
[
    {
        "Name": "blog_background_code",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/blog_background_code/_data",
        "Labels": {},
        "Scope": "local"
    }
]

(该方式创建volume只能在/var/lib/docker/volumes/下,而不能指定主机上目录作为挂在点)

docker run --name test -it -v blog_background_code:/data ubuntu /bin/bash

  这个命令将容器的/data和数据卷blog_background_code绑定在了一起,即容器中的/data目录指向宿主机中的/var/lib/docker/volumes/blog_background_code/_data。在容器上挂载指定的主机目录:只能使用-v参数+宿主机指定目录。此外-v参数只能指定目录绑定,不能指定单个文件绑定。

  1. docker run -v创建数据卷并与容器中目录绑定
docker run --name test -it -v /home/xqh/myimage:/data ubuntu /bin/bash

  其中的 -v 标记 在容器中设置了一个挂载点 /data(就是容器中的一个目录),并将宿主机上的 /home/xqh/myimage 目录中的内容关联到容器中的 /data下(宿主机和容器都必须使用绝对路径)。这样不管是在容器中对/data目录下的操作,还是在主机上对/home/xqh/myimage的操作,都是完全实时同步的,因为这两个目录实际都是指向主机同一个目录。

docker run --name test1 -it -v /data ubuntu /bin/bash

  上面-v的标记只设置了容器的挂载点,并没有指定关联的主机目录。这时docker会自动绑定主机上的一个目录。通过docker inspect 命令可以查看到。

docker inspect test1
  1. 在Dockerfile中创建数据卷并与容器中目录绑定
VOLUME /data
VOLUME [“/data1”,”/data2”] 添加多个volume

  在使用docker build命令生成镜像并且以该镜像启动容器时,会挂载一个volume到容器中的/data。与docker volume create的方式类似,这种方式的volume不能挂载宿主机中指定的目录。需要注意的是,Dockerfile中VOLUME指令之后的代码,如果尝试对这个volume进行修改,这些修改都不会生效(CMD、ENTRYPOINT指令指定的操作除外)。如在创建volume后,/data目录中RUN touch /data/file,file并不会存在。但可将对数据卷目录的修改移到volume之前,即先RUN touch /data/file,再VOLUME /data。

  1. 删除
docker volume rm <volume_name>

  只有当没有任何容器使用该volume时,才能成功删除。若volume指定了宿主机中的特定目录进行挂载,删除该volume并不会删除宿主机中对应的目录

  1. –volumes-from
docker run —rm -it —name vol_use --volumes-from vol_simple ubuntu /bin/bash

  可以使容器之间共享volumes,新创建的容器vol_use与之前创建的容器vol_simple共享volume,这个volume目的目录与vol_simple相同(如容器中的/data)。如果给共享的容器有多个volume,新容器也将有多个volume,并且其挂载的目的目录相同。可以使用多个–volumes-from标签,是的容器与多个已有容器共享volume。这在多个容器间使用相同的配置文件、通用方法时十分方便。

  由于只有docker run -v的方式能指定挂载的宿主机中的目录,因此我们在Dockerfile中创建容器中存放代码的目录A,然后在运行镜像时使用docker run -v,将宿主机中存放代码的目录B和A关联起来。至于前端应用web应用,也是在创建容器时将宿主机中web的代码目录,与容器中beego的目录关联起来。而像conf(配置)、service/protoc(protoc定义)、service/common(公共方法)这些公共的内容,也要与每个容器相关联,最终目录结构与容器的关联关系如下。base service是基础镜像,其他镜像都从它继承。

docker 多服务器部署 docker部署多个微服务_golang_02

2.2 docker网络与通信

2.2.1 docker容器间通信

docker 多服务器部署 docker部署多个微服务_项目架构_03


  docker网络有上图这些模式,比较复杂,在此不详细解释了。简单来说docker容器间互相通信有三种方式:

(1)️每个容器都是用docker run -p公开端口并绑定到本地网络接口。这样可以把容器里的服务在本地docker宿主机所在的外部网络上公开,但这种方式应用暴露太多并不安全

(2)️Docker Networking用户创建自己的网络,容器可以通过这个网上互相通信,这是docker1.9版本中的一个新特性

(3)️docker run —link 容器A:别名 链接连接容器,新容器可以访问容器A的任意公开端口,而容器A不需要向宿主机公开这个端口。

  在docker1.9及之后的版本中推荐使用Docker Networking,之前的版本推荐链接。因此我们选择的是Docker Networking方式。

2.2.2 docker network使用

  1. 创建网络
docker network create answer_net

使用docker network inspect可以查看新创建的网络,一个容器可以同时隶属于多个不同网络

  1. 容器加入/断开网络
docker run --net=answer_net

正在运行的容器连接到网络中

docker network connect answer_net 容器名

容器与指定网络断开连接

docker network disconnect answer_net 容器名
  1. web应用对外提供公开端口
docker run --net=answer_net -p 8081:8081

web前端应用对外提供8081端口,即在宿主机http://localhost:8081/index仍就能访问到页面。也可在Dockerfile用EXPOSE 8081指令暴露端口

  1. 代码中IP、端口等的动态设置

最终我们创建了用户、事件、答题、题目、积分、联合、web应用这7个容器并加入answer_net网络中,通过/etc/hosts文件可以查看网络信息。如在user service容器中

cat /etc/hosts
[root@b4519d4bae7e /]# cat /etc/hosts
127.0.0.1    localhost
……
172.19.0.8    b4519d4bae7e
……
172.19.0.8    problem_service
……

  除了服务自身的信息,还有其他模块服务容器的IP地址,每个容器有两条,一条时容器的主机名+IP地址,另一条时将src_answer_net网络名作为域名后缀添加到主机名后。如果任何一个容器重启了,那么他的IP地址信息会自动在/etc/hosts中更新。

docker 多服务器部署 docker部署多个微服务_docker_04

2.2.3 从外部访问容器

  要能够从宿主机访问到容器的某个端口,容器使用-p参数向外暴露该端口,并和宿主机的一个端口绑定起来即可。
  执行docker run时使用-P(大写)参数,Docker 会随机映射一个 49000~49900 的端口到内部容器开放的网络端口。使用加-p(小写)参数,可以指定容器要映射的宿主机IP和端口,但是在一个指定端口上只可以绑定一个容器。支持的格式有 hostPort:containerPort、ip:hostPort:containerPort、 ip::containerPort、* hostPort:containerPort(映射所有接口地址)
  将本地的 8081 端口映射到容器的 8081 端口,可以执行如下命令:

docker run -p 8081:8081 web_image

PS

  • 使用 docker port <容器id或容器名>来查看当前映射的端口配置,也可以查看到绑定的地址
  • 容器有自己的内部网络和 ip 地址(使用 docker inspect 可以获取所有的变量,Docker 还可以有一个可变的网络配置。)
  • -p 标记可以多次使用来绑定多个端口
    例如
docker run -d -p 5000:5000 -p 3000:80 web_image

2.2.4 从容器内部访问宿主机–与consul、mysql连接

  consul、mysql不进行容器化,因此仍是宿主机中的应用,需要在容器中访问。

  docker容器和宿主机其实是通过docker0这个网桥进行通信的,所以要在docker容器中访问宿主机某个端口的应用,直接访问docker0的地址即可。在宿主机上执行ifconfig,会有docker0这个网桥信息。而在容器中,直接访问http://host.docker.internal:对应的端口即可。如在user的容器中 ping host.docker.internal成功,修改config.yaml中consul、mysql配置的地址为host.docker.internal即可。

docker 多服务器部署 docker部署多个微服务_分布式_05

三、Dockerfile

  使用Dockerfile创建镜像会更方便,直接在文件中设置好volume、网络等。user模块的Dockerfile如下,其他几个服务也类似。

  1. 基础镜像service_base_image
FROM centos

MAINTAINER xxxx@qq.com

RUN yum install -y epel-release
RUN yum install -y gcc
RUN yum install -y go

ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin
#ADD conf /go/src/conf
ADD github.com /go/src/github.com
ADD golang.org /go/src/golang.org
ADD gopkg.in /go/src/gopkg.in
ADD build.sh /build.sh
RUN chmod +x /build.sh
RUN /build.sh

build.sh

#/bin/bash 

cd /go/src/github.com/beego/bee
go install

构建镜像

cd /Users/gan/Documents/GitHub/AnswerSystem_go/src
docker build . -t service_base_image
  1. user镜像
FROM service_base_image:latest

RUN mkdir -p /go/src/service/user
CMD cd /go/src/service/user && go run UserManage.go

构建镜像

cd /Users/gan/Documents/GitHub/AnswerSystem_go/src/service/user
docker build . -t user_image

  运行user_image镜像,其中还需要使用-v参数将容器中的目录与service/user(服务代码)、conf(配置)、service/protoc(protoc定义)、service/common(公共方法)宿主机上的目录关联起来。

docker run -it --net=answer_net \
-v /Users/gan/Documents/GitHub/AnswerSystem_go/src/service/user:/go/src/service/user \
-v /Users/gan/Documents/GitHub/AnswerSystem_go/src/conf:/go/src/conf \
-v /Users/gan/Documents/GitHub/AnswerSystem_go/src/service/protoc:/go/src/service/protoc \
-v /Users/gan/Documents/GitHub/AnswerSystem_go/src/service/common:/go/src/service/common \
--name userContainer \
user_image

四、服务编排docker-compose

  我们可以按上述方法将写好每个模块的Dockerfile,再一一启动,但这样会非常的麻烦,每个模块有多个容器时也会非常不好管理。这时候就可以引入服务编排的概念了。
  服务编排,即根据被部署的对象之间的耦合关系,以及被部署对象对环境的依赖,制定部署流程中各个动作的执行顺序。这方面比较成熟的项目是docker-compose,他采用YAML格式。根据他的语法,制定好容器顺序以及启动参数等,即可按顺序启动相应的容器。且当某个容器A重新创建时,与其-link或-volume-from关联的容器B也能更新相应的信息。
语法及例子 编写启动的docker-compose.yaml文件如下,按格式写好网络、数据卷、容器启动参数。

version: '3.3'
services:
  base_image:
    build: .
    container_name: base_service
  user_image:
    build: ./service/user/
    container_name: user_service
    networks:
      - answer_net
    volumes:
      - "/Users/gan/Documents/GitHub/AnswerSystem_go/src/service/user:/go/src/service/user"
      - conf_volume:/go/src/conf
      - protoc_volume:/go/src/service/protoc
      - common_volume:/go/src/service/common

..........web_image、union_image、problem_image、event_image、credit_image、participant_image等模块省略……………………..

networks:
  answer_net:

volumes:
  conf_volume:
    driver: local
    driver_opts:
      type: none
      device: $PWD/conf
      o: bind
  protoc_volume:
    driver: local
    driver_opts:
      type: none
      device: $PWD/service/protoc
      o: bind
  common_volume:
    driver: local
    driver_opts:
      type: none
      device: $PWD/service/common
      o: bind

PS

  • 在docker-compose-v2版本中volumes_from是 可以正常使用的,但在v3版本却需要创建一个全局的volumes,各个镜像再引用该全局volumes
  • 各个服务的配置大体相同,只是web_image需要再对外暴露8081端口

  执行docker-compose up构建容器。经过测试,各个模块能正常运行。虽然从前端页面看起来,我们的答题系统没有什么变化,但我们已经将其部署到docker上了。这样根据各个模块不同的调用使用情况,能方便地增加或删除该模块的容器,架构的灵活性大大增加。

  最终创建的镜像(docker-compose默认使用所在目录作为前缀docker-compose)

docker 多服务器部署 docker部署多个微服务_golang_06

系统中容器

docker 多服务器部署 docker部署多个微服务_golang_07


创建的几个数据卷

docker 多服务器部署 docker部署多个微服务_分布式_08


创建的网络answer_net

docker 多服务器部署 docker部署多个微服务_分布式_09

五、运行项目

  1. 安装并启动mysql数据库,运行项目中的web/problem.sql建表
cd /usr/local/mysql/support-files  
sudo /usr/local/mysql/support-files/mysql.server start
  1. 安装并开启consul
cd /Users/gan/Documents/software(consul安装目录)
consul agent -server -node=answer_system -bind=127.0.0.1 -data-dir /tmp/consul -bootstrap-expect 1 -ui
  1. 安装运行docker
cd AnswerSystem_go(docker-compose.yaml所在目录)
docker-compose up

  我们可以修改docker-compose.yaml某个服务的mode参数,启动多个基于该服务镜像的容器

event_image:
  build: ./service/event/
    ……
  deploy:
    mode: replicated
    replicas: 3

  在consul上可以看到注册了多个可用服务,系统正常运行。

六、总结

  目前我们已经将系统系统进行了容器化,能很方便地增减服务,配合docker-compose也能方便地自动化部署。但目前这个部署方案仍旧是不完善的,它目前只部署在了一台机器上。docker-compose也是有缺陷的,它的部署是面向单个宿主机的,不能面向服务器集群编排和部署。要实现完善的面向服务器集群的Docker编排和部署方案,还需要同Machine和Swarm联动,这就是下一步的改造了。