• jar方式部署
  • Docker方式部署

1. 互联网 Web 应用与分布式

应用的开发流程:

  • 产品调研
  • 产品设计
  • 系统设计-前后端文档等
  • 开发
  • 测试
  • 部署

Web 应用中:

  • 每个 HTTP 请求的背后都是一台主机
  • 主机上的某个进程监听着某个端口,可能是 Java/Go/Python/Node.js 等

分布式:

  • 当访问的用户太多时
    • 垂直扩展:买更好的机器
    • 水平扩展:加机器(能用加机器解决的问题都不是问题)
  • 如何保证数据的唯一性?
    • 单一数据源,但数据库挂了怎么办?数据库背后,分库分表,读写分离等
    • TDDL (Taobao Distributed Database Layer)

2. 发布和部署中要解决的问题

所谓部署,其实就是将开发好的程序放在服务器中运行,使之可以:

  • 监听端口
  • 响应 HTTP 请求
  • 处理预定的业务逻辑
  • 产品在不断迭代,部署的版本也要不停更新

发布和部署要解决的问题:

  • 环境问题
    • 开发环境(测试环境,测试数据库)
    • 预发布环境(预生产环境,生产数据库,或者生产数据库的副本)
    • 生产环境(正式环境,生产数据库)
  • 环境的兼容性问题
    • 硬件/软件
    • 数据库等
  • 升级和回滚

分布式更新:每次只更新部分服务,另外的部分继续提供服务,直至更新完全部的服务,所谓的不停服更新。

3. 使用 Maven Exec 插件部署

带有各种 jar 拼接出的命令太长了,手动输入是噩梦,可以使用 maven exec plugin,让程序来拼接 java 命令。

优点:简单
缺点:
不太适合自动化部署,需要下载所有源代码,单独执行 mvn exec:exec 时并不会自动编译,除非插件绑定到 mvn 生命周期中尽量靠后(在 compile 之后)的构建阶段中,然后运行 mvn 相应的生命周期。
或者运行 mvn compile exec:exec

  1. <plugin>
  2. <groupId>org.codehaus.mojo</groupId>
  3. <artifactId>exec-maven-plugin</artifactId>
  4. <version>1.6.0</version>
  5. <configuration>
  6. <executable>java</executable>
  7. <arguments>
  8. <argument>-classpath</argument>
  9. <classpath/>
  10. <argument>hello.Application</argument>
  11. </arguments>
  12. </configuration>
  13. </plugin>

4. 使用 jar 包的方式部署

现代的 SpringBoot 项目一般都是使用 jar 包的方式进行部署,使用 mvn package (或者跳过测试 mvn package -DskipTests )进行打包:
image.png
java 命令启动程序有两种方式:

  • java -classpath xxx.xxx.Main
  • java -jar target/my-spring-boot-0.0.1-SNAPSHOT.jar(本质上和第一种一样)

SpringBoot 项目的 jar 包中,包含所有依赖和内嵌的 Tomcat 服务器。
jar 中有个 META-INF/MANIFEST.MF 清单文件:

  1. Manifest-Version: 1.0
  2. Implementation-Title: spring-boot
  3. Implementation-Version: 0.0.1-SNAPSHOT
  4. Start-Class: hello.Application
  5. Spring-Boot-Classes: BOOT-INF/classes/
  6. Spring-Boot-Lib: BOOT-INF/lib/
  7. Build-Jdk-Spec: 1.8
  8. Spring-Boot-Version: 2.2.2.RELEASE
  9. Created-By: Maven Archiver 3.4.0
  10. Main-Class: org.springframework.boot.loader.JarLauncher

5. 实战:SpringBoot/Redis/MySQL 分布式部署与 Nginx 负载均衡

image.png

使用之前练习 AOP与Spring 时的项目代码进行部署练习。
用 docker 先把 mysql 和 redis 启动。

方式1:Maven Exec 插件

在 pom.xml 中配置好插件之后,使用命令 mvn compile exec:exec 运行 SpringBoot 项目。

找一个目录创建一个 nginx.conf 配置文件:

events {}

http {
    upstream myapp1 {
            # 要注意宿主和容器是隔离的,这里指明宿主的局域网IP地址(mac中ifconfig,windows中ipconfig)
        server 192.168.31.83:8080;
        server 192.168.31.83:8081;
        server 192.168.31.83:8082;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
        }
    }
}

使用 docker 启动一个 nginx 容器,切换到刚才存放 nginx.conf 的目录,然后映射给容器内部的 nginx 使用,并映射 80 端口:

$ docker run --restart=always --name my-nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf -d -p 80:80 nginx

或者启动 docker 时使用 --add-host 参数,会添加到容器的 /etc/hosts 文件中,从而 nginx.conf 文件中直接使用域名+端口号即可:

$ docker run --restart=always --name my-nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf --add-host example.com:192.168.31.83 -d -p 80:80 nginx

实现了外部用户访问宿主机上的 80 端口时,被映射到了 docker 中的 nginx 的 80 端口,并且将请求转发到了上游服务(即在宿主机中通过 maven exec 插件运行的 SpringBoot 项目,该项目的服务端口是 8080)。

接下来采用别的部署方式将同样的 SpringBoot 项目继续至 8081 和 8082 端口,而刚才的 nginx 容器服务保持不变,继续运行。

方式2:jar 包

使用 mvn package 打 jar 包,然后运行 java 命令,由于 8080 端口已被占用,这里启动时指定了 8081 端口, -D 用来在启动一个 java 程序时设置系统属性值(即JVM参数):

$ java -Dserver.port=8081 -jar target/spring-aop-redis-mysql-0.0.1.jar

现在访问 http://localhost/rankData(http协议默认就是80端口)就能各自服务的输出中看到 Nginx 把请求轮询交替转发到了 8080 和 8081 中,也可以访问 http://localhost/rank.htm 静态页面,但是 chrome 浏览器中可能要强制刷新才能后台看到请求被轮询转发,否则可能每次都是请求同一个端口,具体可能和浏览器及 Nginx 对于静态页面的缓存策略有关。

关于 Nginx 负载均衡中的请求转发策略如下,即 轮询(默认)、最闲、相同用户 IP 固定请求同一台服务,这三种策略:

The following load balancing mechanisms (or methods) are supported in nginx:

  • round-robin — requests to the application servers are distributed in a round-robin fashion,
  • least-connected — next request is assigned to the server with the least number of active connections,
  • ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client’s IP address).

方式3:Docker 镜像

使用 docker 部署,也是先打 jar 包,然后制作 docker 镜像,简单起见,直接在项目根目录下创建 Dockerfile 文件:

# 基础镜像(专用于docker的微型jdk发行版)
FROM java:openjdk-8u111-alpine
# 创建目录(缺省时WORKDIR也会自动创建)
RUN mkdir /app
# 将目录设置为工作目录
WORKDIR /app
# 将构建时外部上下文中的jar包复制到镜像的工作目录中
COPY target/spring-aop-redis-mysql-0.0.1.jar /app
# 容器向外暴露出8080端口
EXPOSE 8080
# 在工作目录下用java执行jar包
CMD [ "java", "-jar", "spring-aop-redis-mysql-0.0.1.jar"]

然后继续根目录下执行构建命令,顺便添加了标签:

$ docker build . -t goods-rank:0.0.1

接下来在使用 docker 运行镜像之前,还需要注意当前 jar 包中的 SpringBoot 项目所采用的配置文件 application.properties 中的 mysql 和 redis 主机地址都是代表宿主的 localhost,项目在容器中运行时,容器内部并没有运行 mysql 和 redis 服务,mysql 和 redis 是在宿主机上通过另外的 docker 容器运行的。

因此,刚才 jar 包中的 application.properties 中的部分配置需要修改。参考末尾提供的文档链接,有多种方式可以将配置文件外部化,这里继续使用 application.properties 配置文件的方式,根据加载的优先级高低进行覆盖。

SpringBoot 加载 application.properties时的优先级(从高到低):

SpringApplication will load properties from application.properties files in the following locations and add them to the Spring Environment:

  1. A /config subdir of the current directory.
  2. The current directory
  3. A classpath /config package
  4. The classpath root

The list is ordered by precedence (locations higher in the list override lower items).

jar 中是最低的第 4 种优先级:
image.png
为了覆盖 jar 包中的第 4 种优先级,接下来使用第 1 种优先级。

在宿主机中找个地方重新创建一个 application.properties 文件,内容如下(其他不变,只是把 mysql 和 redis 的 host 改为宿主机的局域网 IP):

# 请勿改变数据库名、端口、用户名及密码
spring.datasource.url=jdbc:mysql://192.168.31.83:3306/mall?characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.config-location=classpath:db/mybatis/config.xml
# 请勿改变Redis端口号
spring.redis.host=192.168.31.83
spring.redis.port=6379

然后运行 docker run 时,采用 -v 将文件映射到容器中的工作目录(即之前 Dockerfile 文件中定义的 /app 目录),同时套一层 config 子文件夹,这样优先级最高!:

# 注意这里没有使用 -d,是为了方便在当前 shell 中打印 SpringBoot 的日志
$ docker run -p 8082:8080 -v `pwd`/application.properties:/app/config/application.properties <镜像ID>

如果一切 OK,访问 http://localhost/rankData,刷新,刷新,再刷新,应该可以看到三种部署方式在交替输出日志,说明来自 80 端口的请求被 Nginx 成功地以轮询的方式转发给了 8080、8081、8082!

6. 参考

  1. nginx负载均衡 http://nginx.org/en/docs/http/load_balancing.html
  2. Externalized Configuration