Docker 系列七(Duubo 微服务部署实践).
一、前言
之前我们公司部署服务,就是大家都懂的那一套(安装jdk、tomcat —> 编译好文件或者打war包上传 —> 启动tomcat),这种部署方式一直持续了很久,带来的问题也很多:
1、繁重的发布任务。微服务一多,就要每个服务都要重启一遍,而且要是集群的话,那要启动的服务就更多了。
2、环境迁移报错。经常发生的一件事,同样的一套代码,这台服务器上就是能跑起来,换个服务器就是报错了。
3、士气低落。小公司没有正经的运维,都是让开发兼并着做这方面的工作,然后负责这块的同事怨言很多(因为这种发布部署实在太无趣了)。
所以领导决定引起 docker 作为我们的部署方式,一来可以很好的解决目前项目部署存在的问题,二来为项目注入新鲜血液。
从上个月15号开始接触 docker,到现在把我们系统的微服务架构初步搭建好,折腾了好久,踩了很多坑。纪念一下小成就,写了这篇博客。为了避免涉嫌泄露公司机密,就小而全的做一些简单介绍哈,以下面这张最小微服务架构图为例,部署一套 dubbo 微服务。
二、服务镜像打包
1、tomcat 基础环境搭建
我们系统的每个微服务都部署运行在 tomcat 上(听说这种方式很不好,对于一些不是web工程的,没必要搭建成 web 服务,增加复杂性,也浪费系统资源),所以我的想法是:先搭建一套 tomcat 环境镜像,然后每个微服务都基于这个环境镜像去构建。所以写了一个 tomcat-env 的镜像,思路如下:
-- 基于 jdk 的 tomcat 容器(主要参考官网 tomcat 镜像的 dockerfile)。
-- 在上下文目录存放项目编译文件,并重命名为 root(不放 war 包的原因是考虑调试的时候方便,不用改一个文件,就打个war包)。
-- 删除原本 tomcat 容器 webapps 目录下的 root 文件,并将上下文目录中项目的 root 文件夹上传到容器 webapps 目录下。
-- 启动服务。
from openjdk:8-jre env catalina_home /usr/local/tomcat env path $catalina_home/bin:$path run mkdir -p "$catalina_home" workdir $catalina_home # let "tomcat native" live somewhere isolated env tomcat_native_libdir $catalina_home/native-jni-lib env ld_library_path ${ld_library_path:+$ld_library_path:}$tomcat_native_libdir # runtime dependencies for tomcat native libraries # tomcat native 1.2+ requires a newer version of openssl than debian:jessie has available # > checking openssl library version >= 1.0.2... # > configure: error: your version of openssl is not compatible with this version of tcnative # see http://tomcat.10.x6.nabble.com/vote-release-apache-tomcat-8-0-32-tp5046007p5046024.html (and following discussion) # and https://github.com/docker-library/tomcat/pull/31 env openssl_version 1.1.0f-3+deb9u2 run set -ex; \ currentversion="$(dpkg-query --show --showformat '${version}\n' openssl)"; \ if dpkg --compare-versions "$currentversion" '<<' "$openssl_version"; then \ if ! grep -q stretch /etc/apt/sources.list; then \ # only add stretch if we're not already building from within stretch { \ echo 'deb http://deb.debian.org/debian stretch main'; \ echo 'deb http://security.debian.org stretch/updates main'; \ echo 'deb http://deb.debian.org/debian stretch-updates main'; \ } > /etc/apt/sources.list.d/stretch.list; \ { \ # add a negative "pin-priority" so that we never ever get packages from stretch unless we explicitly request them echo 'package: *'; \ echo 'pin: release n=stretch*'; \ echo 'pin-priority: -10'; \ echo; \ # ... except openssl, which is the reason we're here echo 'package: openssl libssl*'; \ echo "pin: version $openssl_version"; \ echo 'pin-priority: 990'; \ } > /etc/apt/preferences.d/stretch-openssl; \ fi; \ apt-get update; \ apt-get install -y --no-install-recommends openssl="$openssl_version"; \ rm -rf /var/lib/apt/lists/*; \ fi run apt-get update && apt-get install -y --no-install-recommends \ libapr1 \ && rm -rf /var/lib/apt/lists/* # see https://www.apache.org/dist/tomcat/tomcat-$tomcat_major/keys # see also "update.sh" (https://github.com/docker-library/tomcat/blob/master/update.sh) env gpg_keys 05ab33110949707c93a279e3d3efe6b686867ba6 07e48665a34dcafae522e5e6266191c37c037d42 47309207d818ffd8dcd3f83f1931d684307a10a5 541fbe7d8f78b25e055ddee13c370389288584e7 61b832ac2f1c5a90f0f9b00a1c506407564c17a3 713da88be50911535fe716f5208b0ab1d63011c7 79f7026c690baa50b92cd8b66a3ad3f4f22c4fed 9ba44c2621385cb966eba586f72c284d731fabee a27677289986db50844682f8acb77fc2e86e29ac a9c5df4d22e99998d9875a5110c01c5a2f6059e7 dcfd35e0bf8ca7344752de8b6fb21e8933c60243 f3a04c595db5b6a5f1eca43e3b7bbb100d811bbe f7da48bb64bcb84ecba7ee6935cd23c10d498e23 env tomcat_major 8 env tomcat_version 8.0.53 env tomcat_sha512 cd8a4e48a629a2f2bb4ce6b101ebcce41da52b506064396ec1b2915c0b0d8d82123091242f2929a649bcd8b65ecf6cd1ab9c7d90ac0e261821097ab6fbe22df9 env tomcat_tgz_urls \ # https://issues.apache.org/jira/browse/infra-8753?focusedcommentid=14735394#comment-14735394 https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz \ # if the version is outdated, we might have to pull from the dist/archive :/ https://www-us.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz \ https://www.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz \ https://archive.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz env tomcat_asc_urls \ https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz.asc \ # not all the mirrors actually carry the .asc files :'( https://www-us.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz.asc \ https://www.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz.asc \ https://archive.apache.org/dist/tomcat/tomcat-$tomcat_major/v$tomcat_version/bin/apache-tomcat-$tomcat_version.tar.gz.asc run set -eux; \ \ savedaptmark="$(apt-mark showmanual)"; \ apt-get update; \ \ apt-get install -y --no-install-recommends gnupg dirmngr; \ \ export gnupghome="$(mktemp -d)"; \ for key in $gpg_keys; do \ gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ done; \ \ apt-get install -y --no-install-recommends wget ca-certificates; \ \ success=; \ for url in $tomcat_tgz_urls; do \ if wget -o tomcat.tar.gz "$url"; then \ success=1; \ break; \ fi; \ done; \ [ -n "$success" ]; \ \ echo "$tomcat_sha512 *tomcat.tar.gz" | sha512sum -c -; \ \ success=; \ for url in $tomcat_asc_urls; do \ if wget -o tomcat.tar.gz.asc "$url"; then \ success=1; \ break; \ fi; \ done; \ [ -n "$success" ]; \ \ gpg --batch --verify tomcat.tar.gz.asc tomcat.tar.gz; \ tar -xvf tomcat.tar.gz --strip-components=1; \ rm bin/*.bat; \ rm tomcat.tar.gz*; \ command -v gpgconf && gpgconf --kill all || :; \ rm -rf "$gnupghome"; \ \ nativebuilddir="$(mktemp -d)"; \ tar -xvf bin/tomcat-native.tar.gz -c "$nativebuilddir" --strip-components=1; \ apt-get install -y --no-install-recommends \ dpkg-dev \ gcc \ libapr1-dev \ libssl-dev \ make \ "openjdk-${java_version%%[.~bu-]*}-jdk=$java_debian_version" \ ; \ ( \ export catalina_home="$pwd"; \ cd "$nativebuilddir/native"; \ gnuarch="$(dpkg-architecture --query deb_build_gnu_type)"; \ ./configure \ --build="$gnuarch" \ --libdir="$tomcat_native_libdir" \ --prefix="$catalina_home" \ --with-apr="$(which apr-1-config)" \ --with-java-home="$(docker-java-home)" \ --with-ssl=yes; \ make -j "$(nproc)"; \ make install; \ ); \ rm -rf "$nativebuilddir"; \ rm bin/tomcat-native.tar.gz; \ \ # reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies apt-mark auto '.*' > /dev/null; \ [ -z "$savedaptmark" ] || apt-mark manual $savedaptmark; \ apt-get purge -y --auto-remove -o apt::autoremove::recommendsimportant=false; \ rm -rf /var/lib/apt/lists/*; \ \ # sh removes env vars it doesn't support (ones with periods) # https://github.com/docker-library/tomcat/issues/77 find ./bin/ -name '*.sh' -exec sed -ri 's|^#!/bin/sh$|#!/usr/bin/env bash|' '{}' + # verify tomcat native is working properly run set -e \ && nativelines="$(catalina.sh configtest 2>&1)" \ && nativelines="$(echo "$nativelines" | grep 'apache tomcat native')" \ && nativelines="$(echo "$nativelines" | sort -u)" \ && if ! echo "$nativelines" | grep 'info: loaded apr based apache tomcat native library' >&2; then \ echo >&2 "$nativelines"; \ exit 1; \ fi expose 8080 run rm -rf /usr/local/tomcat/webapps/root/ onbuild copy root /usr/local/tomcat/webapps/root/ onbuild entrypoint ["/usr/local/tomcat/bin/catalina.sh","run"]
看起来很复杂,不要被吓到,其实都是抄的官网 tomcat 镜像的dockerfile,然后改动了一点,主要是后面三句:删除容器 root 文件夹,拷贝上下文目录的 root 文件夹到 wenapps 目录下,重启服务。
run rm -rf /usr/local/tomcat/webapps/root/ onbuild copy root /usr/local/tomcat/webapps/root/ onbuild entrypoint ["/usr/local/tomcat/bin/catalina.sh","run"]
tips:1、onbuild 命令本次镜像不会被执行,只有以这个镜像为基础镜像的时候才会被执行。
2、上下文目录指的是 dockerfile 文件所在的目录。
3、该镜像已上传到 dockerhub 上:
2、微服务镜像打包
有了基础环境镜像 tomcat-env,那么打包一个服务镜像就是一件再简单不过的事情了:
from tomcat-env:1.0
没错,就是这么简单,因为我们把所有的工作都放在 tomcat-env 中了,其实就是那个 onbuild 命令的效果啦~~
三、编排文件 docker-compose.yml
微服务项目要部署起来,主要是靠 docker-compose.yml 文件进行编排,规定服务之间的关联以及先后启动顺序,然后把几十个零散的微服务当成一个整体来统一管理。
首先,困扰我的是网络问题。做过开发的都知道,要在项目中指定(spring 在 applicationcontext.xml)数据库地址和 zookeeper 地址,那么我怎么知道容器的 ip 地址是多少呢?先来了解下 docker 的网络模式?
docker 的默认网络配置是 "bridge",当 docker 启动时,会自动在主机上创建一个 docker0 虚拟网桥,实际上是 linux 的一个 bridge,可以理解为一个软件交换机。docker 会随机分配一个本地未占用的私有网段(在 rfc1918 中定义)中的一个地址给 docker0 接口,它会在挂载到它的网口之间进行转发。当创建一个 docker 容器的时候,同时会创建了一对 veth pair 接口。这对接口一端在容器内,即 eth0;另一端在本地并被挂载到 docker0 网桥,名称以 veth 开头(例如 vethaqi2qt)。通过这种方式,主机可以跟容器通信,容器之间也可以相互通信。
也就是说,每次容器启动以后的 ip 地址是不固定的,这该怎么办呢?当然可以写死 ip 地址,规定局域网网段,给每个服务编排 ip 地址;当然也可以把network_mode="host",统一用宿主机的网络地址。当然!这些都不是最好的办法:
version: '3.7' #服务列表 services: #基础组件 zookeeper zookeeper: image: zookeeper restart: always ports: - 4181:2181 #基础组件 mysql db: image: mysql:5.7.17 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --init-connect='set names utf8mb4;' ports: - "3636:3306" volumes: - /var/mysqldb:/var/lib/mysql - /docker/mysql/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf restart: always environment: mysql_root_password: password #消费者服务1 admin admin: image: "admin:2.3.1" ports: - "7575:8080" depends_on: - zookeeper restart: always environment: zookeeper.host: zookeeper://zookeeper:2181 #提供者服务1 system system: image: "system:2.3.1" depends_on: - db - zookeeper restart: always environment: zookeeper.host: zookeeper://zookeeper:2181 mysql.address: db:3306
看到了吗?ip 地址直接由 服务名 指定就可以了。另外, docker 中设置的环境变量,竟然能被 applicationcontext.xml 中读取,我也是蛮诧异的!(在代码和 docker 中都配置了mysql.address 的话,以 docker 中设置的生效)。
然后 docker-compose up -d 启动微服务项目就可以了~~
容器部署的一个原则:尽量不要在容器内部做文件的修改,要修改的内容用数据卷的方式映射到宿主机上,比如上面的mysql配置文件和数据仓库。
在 docker 上部署 mysql 遇到了几个问题,简单罗列下:
1、navicat 连接的时候: client does not support authentication protocol requested by server ?
解决:进入 mysql 容器,运行
alter user 'root'@'%' identified with mysql_native_password by 'password';
2、expression #1 of select list is not in group by clause and contains nonaggre 的问题?
原因:mysql 5.7.5及以上功能依赖检测功能。如果启用了only_full_group_by sql模式(默认情况下),mysql将拒绝选择列表,having条件或order by列表的查询引用在group by子句中既未命名的非集合列,也不在功能上依赖于它们。
解决:在mysql的配置文件中加上:
sql_mode=strict_trans_tables,no_zero_in_date,no_zero_date,error_for_division_by_zero,no_auto_create_user,no_engine_substitution
3、mysql 连接参数usessl=true 和 usessl=false 的区别?
建议不要在没有服务器身份验证的情况下建立ssl连接(同一个 docker-compose 中是内网环境)。根据 mysql 5.5.45 +,5.6.26 +和5.7.6+ 要求如果未设置显式选项,则必须默认建立ssl连接。为了符合不使用ssl的现有应用程序。您需要通过设置usessl = false显式禁用ssl,或者设置usessl = true并为服务器证书验证提供信任库。
四、结语
总算是把一个微服务项目部署运行起来了,几乎是用了最少的 docker-compose 模板文件,所以还是有很多地方可以完善的,比如说 mysql 密码没有加密处理、服务没有做健康检查、集群方面还没怎么考虑(用 docker swarm 实现)等等......路漫漫其修远兮,吾将上下而求索。共勉!
下一篇: 智能测温人脸识别系统项目