微服务布置实践

一、前言

    之前我们公司部署服务,就是大家都懂的那一套(安装JDK、Tomcat —>
编译好文件或者打war包上传 —>
启动Tomcat),这种部署方式一直持续了很久,带来的问题也很多:

1、繁重的发布任务。微服务一多,就要每个服务都要重启一遍,而且要是集群的话,那要启动的服务就更多了。

2、环境迁移报错。经常发生的一件事,同样的一套代码,这台服务器上就是能跑起来,换个服务器就是报错了。

3、士气低落。小公司没有正经的运维,都是让开发兼并着做这方面的工作,然后负责这块的同事怨言很多(因为这种发布部署实在太无趣了)。

    所以领导决定引起 Docker
作为我们的部署方式,一来可以很好的解决目前项目部署存在的问题,二来为项目注入新鲜血液。

    从上个月15号开始接触
Docker,到现在把我们系统的微服务架构初步搭建好,折腾了好久,踩了很多坑。纪念一下小成就,写了这篇博客。为了避免涉嫌泄露公司机密,就小而全的做一些简单介绍哈,以下面这张最小微服务架构图为例,部署一套
Dubbo 微服务。

图片 1

热部署

pom.xml文件中添加spring-boot-devtools依赖即可实现页面和代码的热部署。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

二、服务镜像打包

常规部署

     1、Tomcat 基础环境搭建

    我们系统的每个微服务都部署运行在 Tomcat
上(听说这种方式很不好,对于一些不是web工程的,没必要搭建成 web
服务,增加复杂性,也浪费系统资源),所以我的想法是:先搭建一套 Tomcat
环境镜像,然后每个微服务都基于这个环境镜像去构建。所以写了一个
tomcat-env 的镜像,思路如下:

    — 基于 JDK 的 Tomcat
容器(主要参考官网 Tomcat 镜像的 Dockerfile)。

    — 在上下文目录存放项目编译文件,并重命名为
ROOT(不放 war
包的原因是考虑调试的时候方便,不用改一个文件,就打个war包)。

    — 删除原本 Tomcat 容器 webapps 目录下的 ROOT
文件,并将上下文目录中项目的 ROOT 文件夹上传到容器 webapps
目录下。

    — 启动服务。

图片 2图片 3

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-env

看起来很复杂,不要被吓到,其实都是抄的官网 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 上:https://hub.docker.com/r/jmcui/tomcat-env/

jar

  • 打包

mvn package
  • 运行

java -jar xxx.jar
  • war 转 jar

    • pom.xml文件中将<packaging>war</packaging>改为<packaging>jar</packaging>
    • 去掉ServletInitializer类
    • 去掉如下依赖,恢复默认内嵌Tomcat依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
          <scope>provided</scope>
      </dependency>
      
  • 注册Linux服务

    • 修改spring-boot-maven-plugin配置

      <build>
          <plugins>
              <plugin>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-maven-plugin</artifactId>
                  <configuration>
                      <executable>true</executable>
                  </configuration>
              <plugin>
          </plugins>
      </build>
      
    • 使用mvn package打包

    • 使用init.d或systemd注册服务

      • init.d部署
        注册服务

        sudo ln -s /var/apps/xxx.jar /etc/init.d/xxx
        

        启动服务

        service xxx start
        

        停止服务

        service xxx stop
        

        服务状态

        service xxx status
        

        开机启动

        chkconfig xxx on
        

        日志存放在/var/log/xxx.log。

      • systemd部署
        注册服务
        在/etc/systemd/system/目录下新建文件xxx.service,xxx.service内容如下:

        [Unit]
        Description=xxx
        After=syslog.target
        
        [Service]
        ExecStart= /usr/bin/java -jar /var/apps/xxx.jar
        
        [Install]
        WantedBy=multi-user.target
        

        启动服务

        systemctl start xxx
        
        systemctl start xxx.service
        

        停止服务

        systemctl stop xxx
        
        systemctl stop xxx.service
        

        服务状态

        systemctl status xxx
        
        systemctl status xxx.service
        

        开机启动

        systemctl enable xxx
        
        systemctl enable xxx.service
        

        项目日志

        journalctl -u xxx
        
        journalctl -u xxx.service
        

     2、微服务镜像打包

    有了基础环境镜像
tomcat-env,那么打包一个服务镜像就是一件再简单不过的事情了:

图片 4

FROM tomcat-env:1.0

    没错,就是这么简单,因为我们把所有的工作都放在 tomcat-env
中了,其实就是那个 ONBUILD 命令的效果啦~~ 

war

  • 打包

mvn package
  • 运行
    将war包丢到支持war文件的Servlet容器执行。
  • jar 转 war

    • pom.xml文件中将<packaging>jar</packaging>改为<packaging>war</packaging>
    • 增加ServletInitializer类

      import org.springframework.boot.builder.SpringApplicationBuilder;
      import org.springframework.boot.context.web.SpringBootServletInitializer;
      public class ServletInitializer extends SpringBootServletInitializer {
          @Override
          protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
              return application.source(XxxApplication.class)
          }
      }
      
    • 增加如下依赖,覆盖默认内嵌Tomcat依赖

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-tomcat</artifactId>
          <scope>provided</scope>
      </dependency>
      

三、编排文件 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配置文件和数据仓库。

图片 5

    在 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 实现)等等……路漫漫其修远兮,吾将上下而求索。共勉!

Docker

使用Dockerfile编译Docker镜像。

Dockerfile指令

  • FROM
    指明当前镜像继承的基镜像,编译当前镜像时会自动下载基镜像。

    FROM java:8
    
  • MAINTAINER
    指明当前镜像的作者。

    MAINTAINER linliangsheng
    
  • RUN
    当前镜像上执行Linux命令并形成一个新的层,编译时(build)动作。

    RUN /bin/bash -c "echo hello"
    
    RUN ["/bin/bash", "-c", "echo hello"]
    
  • CMD
    启动镜像容器时的默认行为,一个Dockerfile只能有一个CMD指令,可在运行镜像时使用参数覆盖,运行时(run)动作。

    CMD echo "hello"
    

    参数覆盖写法:

    docker run -d image_name echo "docker-hello"
    
  • EXPOSE
    指明镜像运行时的容器必需监听指定的端口。

    EXPOSE 8080
    
  • ENV
    设置环境变量。

    ENV name=linliangsheng
    
    ENV name linliangsheng
    
  • ADD
    从当前工作目录复制文件到镜像目录。

    ADD xxx.jar app.jar
    
  • ENTRYPOINT
    让容器像可执行程序一样运行,镜像运行时可接收参数,运行时(run)动作。

    ENTRYPOINT ["java"]
    

    参数接收写法:

    docker run -d image_name "-jar app.jar"
    

发表评论

电子邮件地址不会被公开。 必填项已用*标注