本文是《Docker下,两分钟极速体验Nacos》的续篇,前文我们极速体验了Nacos注册中心、服务提供者、服务消费者,这些应用都对应着不同的Docker容器,今天就来细说这些Docker容器的镜像。

回顾上一章的业务流程

先来回顾一下上一章,整个Docker环境中有哪些容器,提供了什么服务,如下图,请顺着橙色提示框的数字顺序来看请整个流程::

docker nacos 内部ip docker运行nacos_docker

Nacos环境背后对应的Docker技术

在Docker下搭建一个包含Nacos注册中心、服务提供者、服务消费者的环境,总的来说需要做下面这些事情:

  1. 制作Nacos镜像;
  2. 制作服务提供者镜像;
  3. 制作服务消费者镜像;
  4. 制作docker-compose.yml文件,将容器编排在一起,然后一次性启动;
    接下来我们逐个开发上面提到的内容;

源码下载

如果您不打算写代码,也可以从GitHub上下载本次实战的源码,地址和链接信息如下表所示:

名称

链接

备注

项目主页

https://github.com/zq2599/blog_demos

该项目在GitHub上的主页

git仓库地址(https)

https://github.com/zq2599/blog_demos.git

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

这个git项目中有多个文件夹,本章的应用在nacosdemo文件夹下,如下图所示:

docker nacos 内部ip docker运行nacos_docker_02

制作Nacos镜像

对Nacos镜像的功能要求有以下三点:

  1. 包含Nacos server应用;
  2. 暴露web管理服务的端口;
  3. 容器启动时,Nacos服务能够自动启动;

    为了满足以上要求,除了编写Dockerfile文件,还要编写docker-entrypoint.sh文件,在容器创建时执行该文件用于启动Nacos服务;

首先是Dockerfile文件,该文件用于制作Nacos镜像:

# Docker image for Anaconda3-2019.03
# VERSION 0.0.1
# Author: bolingcavalry

### 基础镜像,使用alpine操作系统,openjkd使用8u201
FROM openjdk:8u201-jdk-alpine3.9

#作者
MAINTAINER BolingCavalry <zq2599@gmail.com>

#系统编码
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
#path
ENV PATH /opt/conda/bin:$PATH

#安装必要的软件
#RUN apt-get update --fix-missing && apt-get install -y wget
RUN apk update && apk add wget

#下载下来的压缩文件名称
ENV NACOS_FILE_NAME nacos-server-1.1.0.tar.gz

#把启动时用到的文件准备好
COPY ./docker-entrypoint.sh /docker-entrypoint.sh

#解压后的文件夹名称
ENV NACOS_FOLDER_NAME nacos

RUN wget https://github.com/alibaba/nacos/releases/download/1.1.0/nacos-server-1.1.0.tar.gz -O ~/$NACOS_FILE_NAME && \
    tar -zxf ~/$NACOS_FILE_NAME -C ~/ && \
    rm ~/$NACOS_FILE_NAME && \
    chmod a+x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 8848

从Dockerfile内容中可见,先去nacos的github下载安装包,然后解压,再复制docker-entrypoint.sh到镜像中,最后将nacos的8848端口暴露出来;
再来看看docker-entrypoint.sh文件的内容,该文件在容器启动时会被执行,内容很简单,就是进入nacos的bin目录,执行启动文件,再将start.out输出到控制台:

#!/bin/sh

echo "Starting nacos"n && \
     cd ~/nacos/bin && \
     ./startup.sh -m standalone && \
     cd ../logs && \
     tail -f start.out

有两个重要信息需要注意:

  1. nacos的官方参考启动命令是./startup.sh -m standalone,这个命令会将jvm的输出重定向到start.out文件,也就是说nacos的JVM进程是在后台运行的,不会占用控制台(相比之下,spring boot应用使用java -jar启动时会占用控制台),对于docker来说,容器内的进程如果不占用控制台,docker就认为该容器已经结束工作,就会停止该容器,所以,为了避免nacos在docker刚刚启动就退出,需要用tail -f start.out来占领控制台;
  2. 用tail -f start.out来占领控制台可以避免容器刚刚启动就退出,但也有个弊端,就是容器中有了多个进程,并且nacos进程的PID不是1,所以在执行docker stop命令时,结束进程的信号量不会到nacos进程,而是去了PID等于1的进程,所以nacos进程不会立即退出,只能等到30秒后被强制kill,这个问题最好的解法是修改nacos的startup.sh,让nacos进程始终保持在控制台,不要重定向到后台,但这样就导致Dockerfile不好处理了,每次下载和解压了nacos安装包后,都要用本地的startup.sh去替换原有的,这样做的话,如果nacos升级版本,这边本地的startup.sh也要随之更新,很是麻烦…

构建镜像

  1. Dockerfile和docker-entrypoint.sh文件准备好之后放在同一个目录,执行以下命令构建镜像:
docker build -t bolingcavalry/nacossimpleprovider:1.0-SNAPSHOT
  1. 如果您在hub.docker.com已经注册,可以执行以下命令将本地镜像上传到 hub.docker.com ,这样任何人都可以下载使用该镜像了:
docker push bolingcavalry/nacossimpleprovider:1.0-SNAPSHOT

Nacos镜像的制作已经完成,接下来制作一个java应用的镜像:服务提供者;

java应用的父工程

接下来要开发的simple-provider和simple-consumer两个应用都是java应用,为了管理方便,做一个基于maven的父工程,再将simple-provider和simple-consumer以module的形式加入到这个父工程中;

  1. 基于maven创建父工程,名为nacosdemo,其pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <modules>
        <module>simpleconsumer</module>
        <module>simpleprovider</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.bolingcavalry</groupId>
    <artifactId>nacosdemo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring.boot.version>2.0.5.RELEASE</spring.boot.version>
        <spring.cloud.version>Finchley.SR1</spring.cloud.version>
        <spring.cloud.alibaba.version>0.2.2.RELEASE</spring.cloud.alibaba.version>
    </properties>

    <!-- 依赖声明 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring.cloud.alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!--编译管理 jdk版本和字符集编码-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

可见这是个普通的父工程,里面对spring cloud和spring cloud alibaba的版本做了控制,避免子工程还要各种指定版本的繁琐操作;

制作服务提供者镜像

simple-provider是个java web应用,使用了spring cloud alibaba的依赖库之后可以使用Nacos的注册发现服务,整个工程的开发步骤如下:

  1. 基于maven创建工程,其pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>nacosdemo</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>simpleprovider</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--使用jib插件-->
            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>1.3.0</version>
                <configuration>
                    <!--from节点用来设置镜像的基础镜像,相当于Docerkfile中的FROM关键字-->
                    <from>
                        <!--使用openjdk官方镜像,tag是8u201-jdk-alpine3.9,表示镜像的操作系统是alpine3.9,装好了jdk8u201-->
                        <image>openjdk:8u201-jdk-alpine3.9</image>
                    </from>
                    <to>
                        <!--镜像名称和tag,使用了mvn内置变量${project.artifactId}和${project.version}-->
                        <image>bolingcavalry/nacos${project.artifactId}:${project.version}</image>
                    </to>
                    <!--容器相关的属性-->
                    <container>
                        <!--jvm内存参数-->
                        <jvmFlags>
                            <jvmFlag>-Xms1g</jvmFlag>
                            <jvmFlag>-Xmx1g</jvmFlag>
                        </jvmFlags>
                    </container>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

上述内容有两点需要注意:
a. 依赖spring-cloud-starter-alibaba-nacos-discovery,这样可以用上spring cloud nacos的服务;
b. 使用了maven插件jib-maven-plugin,用于将应用构建成docker镜像,此插件相关的详情请参考《Docker与Jib(maven插件版)实战》;
2. 配置文件application.properties,配置应用名称和nacos地址,注意这里nacos地址配置的是nacoshost,对应的是后面docker-compose.yml中的link参数:

spring.application.name=simple-provider
spring.cloud.nacos.discovery.server-addr=nacoshost:8848
  1. 应用启动类SimpleProviderApplication ,配置了注解EnableDiscoveryClient,用于启动注册发现服务:
package simpleprovider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class SimpleProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(SimpleProviderApplication.class, args);
    }
}
  1. 增加一个提供http服务的controller类ProviderController:
package simpleprovider.controller;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Description: 提供web服务的controller
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2019/7/28 11:08
 */
@RestController
public class ProviderController {

    @RequestMapping(value = "/hello/{name}", method = RequestMethod.GET)
    public String hello(@PathVariable("name") String name){
        return "hello " + name + ", " + new SimpleDateFormat("yyyy-mm-dd  HH:mm:ss").format(new Date());
    }
}
  1. 以上就是simple-provider的所有源码了,在pom.xml所在目录执行以下命令,即可构建docker镜像,存入本地仓库:
mvn compile jib:dockerBuild

制作服务消费者镜像

simple-consumer是个java web应用,启动后对外提供http服务,响应的时候,通过nacos取得simple-provider的地址,然后向simple-provider发请求,将响应返回给浏览器:

  1. 基于maven创建工程,其pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>nacosdemo</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>simpleconsumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--使用jib插件-->
            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
                <version>1.3.0</version>
                <configuration>
                    <!--from节点用来设置镜像的基础镜像,相当于Docerkfile中的FROM关键字-->
                    <from>
                        <!--使用openjdk官方镜像,tag是8u201-jdk-alpine3.9,表示镜像的操作系统是alpine3.9,装好了jdk8u201-->
                        <image>openjdk:8u201-jdk-alpine3.9</image>
                    </from>
                    <to>
                        <!--镜像名称和tag,使用了mvn内置变量${project.artifactId}和${project.version}-->
                        <image>bolingcavalry/nacos${project.artifactId}:${project.version}</image>
                    </to>
                    <!--容器相关的属性-->
                    <container>
                        <!--jvm内存参数-->
                        <jvmFlags>
                            <jvmFlag>-Xms1g</jvmFlag>
                            <jvmFlag>-Xmx1g</jvmFlag>
                        </jvmFlags>
                        <!--要暴露的端口-->
                        <ports>
                            <port>8080</port>
                        </ports>
                    </container>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. 配置文件application.properties,配置应用名称和nacos地址,注意这里nacos地址配置的是nacoshost,对应的是后面docker-compose.yml中的link参数:
spring.application.name=simple-consumer
spring.cloud.nacos.discovery.server-addr=nacoshost:8848
  1. 应用启动类SimpleConsumerApplication,配置了注解EnableDiscoveryClient,用于启动注册发现服务:
package com.bolingcavalry.simpleconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class SimpleConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SimpleConsumerApplication.class, args);
    }
}
  1. 增加一个提供http服务的controller类ConsumerController,通过LoadBalancerClient取得simple-provider的服务地址,然后发请求过去 :
package com.bolingcavalry.simpleconsumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Description: 提供web服务的controller
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2019/7/28 11:08
 */
@RestController
public class ConsumerController {

    @Autowired
    LoadBalancerClient loadBalancerClient;

    @RequestMapping("/test")
    public String test(){
        //根据应用名称取得实例对象
        ServiceInstance serviceInstance = loadBalancerClient.choose("simple-provider");
        //根据实例对象取得地址
        String uri = serviceInstance.getUri().toString();
        String result = new RestTemplate().getForObject(uri + "/hello/bolingcavalry", String.class);
        return "provider uri : " + uri + "<br>" + "response :" + result;
    }
}
  1. 以上就是simple-consumer的所有源码了,在pom.xml所在目录执行以下命令,即可构建docker镜像,存入本地仓库:
mvn compile jib:dockerBuild

编写docker-compose.yml

三个镜像都准备好了,接下来是做容器编排,docker-compose.yml内容如下:

version: '2'
services:
  nacos:
    image: bolingcavalry/nacosserver:0.0.1
    container_name: nacos
    restart: unless-stopped
    ports:
      - '8848:8848'
  provider: 
    image: bolingcavalry/nacossimpleprovider:1.0-SNAPSHOT
    links:
      - nacos:nacoshost
    depends_on:
      - nacos
    restart: unless-stopped
  consumer: 
    image: bolingcavalry/nacossimpleconsumer:1.0-SNAPSHOT
    links:
      - nacos:nacoshost
    container_name: consumer
    depends_on:
      - nacos
    ports:
      - '8080:8080'
    restart: unless-stopped

上述编排文件,有几点需要注意:
a. 可以用container_name设置容器的名字,但是不要给provider的容器设置名字,因为按照规划provider会启动多个容器,指定名字会导致第二个容器启动时报名字冲突的错误;
b. provider和consumer通过depends_on参数,将自己的启动时间放在了nacos后面;
c. links的作用是将nacos的IP地址,写入provider和consumer的/etc/hosts文件中,这样这两个容器内容的应用可以通过nacoshost来访问nacos了;

启动多个容器

在docker-compose.yml所在目录执行以下命令,即可启动所有容器,并且provider容器会启动6个:

docker-compose up --scale provider=6 -d

至此,整个nacos的docker环境搭建过程已经回顾完毕,在您搭建自己的容器环境时,希望本文能给您一些参考;