Java实现基于docker-java用Swarm管理service及启动容器的相关API介绍

大神讲解Swarm如何使用相关基础知识,不熟悉的小伙伴可以学习一下,传送门
https://blog.csdn.net/liuzhang6966/article/details/90665503

使用场景

首先说一下对应的业务需求,用户通过Nginx服务器访问分布式服务器(Nginx不做集群,只有一个),Docker根据用户信息启动容器,要求每个用户对应自己专属的容器,每个容器互不相同,容器间也没有通讯,同时回调该用户专属容器相关信息,如IP,对外暴露的端口等。

实现原理

  1. 这里我们选择的集群管理工具是Docker官方提供的Swarm,该工具属于轻量级工具,使用与安装都极其简单,根据需求,在不同服务器上为用户启动专属容器,那么需要保证的是所有容器“同IP不同Port,同Port不同IP”。因此,我们需要用Swarm建立管理节点,通过管理节点服务器管理其他服务器。
  2. 我们将复杂的负载均衡问题交给Swarm管理,同时用Swarm启动Service服务,通过Service再拉起容器,容器端口号采用随机映射的方式,这样一个步骤来达到我们的目的,所以此时容器的IP和端口获取就成为了难点。

废话少说,上代码!

代码实现

准备工作:
需要用到的核心jar包坐标,低版本docker-java有较多坑,尽量选用高版本的。(这里小部分包是我项目中用到的,没有导入相关坐标,核心坐标就下面前三个,slf4j也可以不导入)

		<dependency>
            <groupId>com.github.docker-java</groupId>
            <artifactId>docker-java</artifactId>
            <version>3.2.0-rc2</version>
        </dependency>
        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
            <version>2.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
            <version>2.27</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.2</version>
        </dependency>

首先来准备一下固定参数,你可以根据自己的项目情况来修改,包括这里导入一些包,有些也是用不到的。

package com.tianshui.demo.utils;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.DockerCmdExecFactory;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.*;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import com.google.common.collect.Lists;
import com.tianshui.demo.common.Constants;
import com.tianshui.demo.dao.mapper.UserEntityMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.ExecutionException;

/**
 * @Author: wolf
 * @Version: 1.0
 * @Date: 2020/4/9 23:16
 * @Descripition:
 **/
@RestController
@RequestMapping
public class DockerJavaUtils {

    //docker远程服务器访问地址
    private static String SERVERURL = "tcp://XX.XX.XX.XX:2375";
    //镜像名称(docker中启动容器需要用到的镜像名称)
    private static String IMAGE_NAME = "XXX";
    //参数配置
    private static int REPLICAS = 1;//service实例化个数,对应容器启动个数
    private static int TARGETPORT = 7000;//指定target,根据target随机映射端口
    //配置容器内部所需环境
    private static String DOCKER_ROLE_TYPE = "DOCKER_ROLE_TYPE=";
    private static String DOCKER_USER_NAME = "DOCKER_USER_NAME=";
    private static String DOCKER_ACTIVEMQ_MQTT = "DOCKER_ACTIVEMQ_MQTT=XX.XX.XX.XX:1883";
    private static String DOCKER_ACTIVEMQ_WS = "DOCKER_ACTIVEMQ_WS=XX.XX.XX.XX:61614";

    //凌晨1点开始清除多余容器(这里我写了个定时器清除用户非法退出导致未关闭的容器,不需要的可以不写)
    private static final String TIMING_TASK = " 0 0 1 * * ? ";
    @Resource
    RedisUtils redisUtils;//自己封装的Redis工具包
    @Autowired
    UserEntityMapper userEntityMapper;//项目中的用户Mapper
	//日志系统
    private Logger log = LoggerFactory.getLogger(DockerJavaUtils.class);
	//配置dockerclient
    public static DockerClient getDockerClient() {
        DockerCmdExecFactory factory = new NettyDockerCmdExecFactory().withConnectTimeout(10000);
        return DockerClientBuilder.getInstance(SERVERURL).withDockerCmdExecFactory(factory).build();
    }

以下是我项目中用到的流程,以及一些参数,我保留了有些我用不到的参数配置如何书写我注释掉了,如果你需要可以解开这些注释,并调整相关参数。

这段可以不看,或者你可以根据自己的业务调整剩余方法的调用规则。

    /**
     * @Descripition: 根据用户worknumber启动服务
     * @Author: wolf
     * @Date: 2020/4/9 23:44
     * @Param: [worknumber]
     * @Return: boolean
     **/
    @RequestMapping("startDocker")
    public boolean dockerServiceStart(String worknumber) {
        //获取用户角色(1、学员,2、教员)(这里是我项目中的业务需求,可忽略)
        Integer role = userEntityMapper.getRole(worknumber);
        try {
            boolean b = removeService(worknumber);//先移除服务,防止服务重复
            //创建docker-service
            log.info("用户:" + worknumber + "开始创建service!");
            createServivce(worknumber, role);
            //查询用户对应的container地址信息
            String addr = servicInspectPort(worknumber);
            //将该信息存入redis
            if (StringUtils.isEmpty(addr)) {
                log.info("用户:" + worknumber + "数据存入redis失败!");
            } else {
                redisUtils.hset(worknumber, Constants.POWER_SYSTEM, addr);
                log.info("用户:" + worknumber + "数据存入redis成功!存入的数据为:"+worknumber+"  "+Constants.POWER_SYSTEM+"  "+addr);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }


    }

以下进入正题:

创建服务

service的创建需要用到swarm,所以这里可以初始化一个swam,但是要记得做判断,如果不存在swarm则初始化,如果已存在要跳过,由于我是在服务器上用指令已经初始化了swarm,所以这里我不需要用代码执行了,另外就是service的创建需要指定很多参数,比如最基本的你的镜像名称,这个镜像文件是pull的还是自己dockerfile做的看个人,这里不对镜像做过多讲解,需要提示的就是ImageName这种参数是一定要传的,其他参数需要什么就传什么

    /**
     * @param worknumber
     * @param role       角色
     * @Return: void
     * @Description: 创建服务
     * @Date: 2020/4/26 13:48
     * @Author: zse
     **/
    @RequestMapping("/create")
    public synchronized void createServivce(String worknumber, Integer role) throws ExecutionException, InterruptedException {
        DockerClient dockerClient = DockerJavaUtils.getDockerClient();
        //初始化swarm(我在管理节点以及启动swarm,这里就不再启动了)
//        dockerClient.initializeSwarmCmd(new SwarmSpec())
//                .withListenAddr("XX.XX.XX.XX")
//                .withAdvertiseAddr("XX.XX.XX.XX")
//                .exec();

        //设置network
//        String networkId = dockerClient.createNetworkCmd().withName(worknumber)
//                .withDriver("overlay")
//                .withIpam(new Network.Ipam()
//                        .withDriver("default"))
//                .exec().getId();
        //创建服务
        ServiceSpec spec = new ServiceSpec()
                //服务名称(serviceName)
                .withName(worknumber)
                //环境变量(env)
                .withTaskTemplate(new TaskSpec()
                        .withForceUpdate(0)
                        .withContainerSpec(new ContainerSpec()
                                .withEnv(Lists.newArrayList(
                                        new String(DOCKER_ROLE_TYPE + role),
                                        new String(DOCKER_USER_NAME + worknumber),
                                        new String(DOCKER_ACTIVEMQ_MQTT),
                                        new String(DOCKER_ACTIVEMQ_WS)
                                ))
                                //镜像名称
                                .withImage(IMAGE_NAME))
                )
                //网络(network)
//                .withNetworks(Lists.newArrayList(
//                        new NetworkAttachmentConfig()
//                                .withTarget(networkId)
                                .withAliases(Lists.<String>newArrayList("alias1", "alias2"))
//                ))
                //标签
//                .withLabels(ImmutableMap.of(worknumber, worknumber))
                //启动数量
                .withMode(new ServiceModeConfig().withReplicated(
                        new ServiceReplicatedModeOptions()
                                .withReplicas(REPLICAS)
                )).withEndpointSpec(new EndpointSpec()
                        .withMode(EndpointResolutionMode.VIP)
                        .withPorts(Lists.<PortConfig>newArrayList(new PortConfig()
                                        //设置主机模式(mode=host)
                                        .withPublishMode(PortConfig.PublishMode.host)
                                        //设置目标端口号(targetport=7000)
                                        .withTargetPort(TARGETPORT)
//                                .withProtocol(PortConfigProtocol.TCP)
                        )));
        //执行服务创建指令
        dockerClient.createServiceCmd(spec).exec();
        System.out.println("用户:" + worknumber + " 服务启动完毕!");
    }

删除服务

这里呢其实很简单,只需要查询出用户对应的服务名称再直接调用removeServiceCmd()这个方法就OK了,但是我在做这个地方的时候遇到一个问题,就是由于我的镜像和容器启动后比较大,导致这个删除服务方法调用后,service是删掉了,但是container还没有删掉,也就是说service是瞬间删完的,但是container还需要几秒钟,因为我的逻辑是删掉该用户专属container后重新启动新的container,所以一定要保证这个同名称的container不存在了,才能创建新的容器,不然就会在查询的时候出错。

但是这个地方理论上来讲应该有回调函数来提醒我们服务删除成功了,包括上面的容器启动也应该有相关API提醒我们容器创建成功了,但是我在翻源码的时候没有找到相关API,有可能是没有这样的API(因为我这个情况比较特殊嘛,正常container应该是和service一并清除的),也或许是我英文不太好的缘故没认出来,如果有大神了解这方面的回调信息API,欢迎指点,小弟一定虚心学习!

因为没找到相关回调API,所以我就用了个比较笨的方法,我让程序sleep了一下,哈哈,给它时间来等它删除完毕。


    /**
     * @Descripition: 删除服务
     * @Author: zse
     * @Date: 2020/4/9 23:55
     * @Param: [worknumber]
     * @Return: void
     **/
    @RequestMapping("/removeService")
    public boolean removeService(String worknumber) {
        try {
            //获取dockerClient
            DockerClient dockerClient = DockerJavaUtils.getDockerClient();
            //根据worknumber删除服务
            List<Service> services = dockerClient.listServicesCmd().withNameFilter(Lists.newArrayList(worknumber)).exec();
            //清除Redis缓存数据(这个也是我项目中的需求,不需要的话可忽略)
            redisUtils.del(worknumber);
            log.info("用户:" + worknumber + "的redis数据清除数据成功!");
            for (Service service : services) {//循环所有服务列表,找出符合条件的,这个地方只有模糊查询,没有精确查询的相关api
                if (worknumber.equals(service.getSpec().getName())) {
                    dockerClient.removeServiceCmd(worknumber).exec();
                    try {
                        System.out.println("用户:" + worknumber + " 删除处开始sleep");
                        Thread.sleep(5000);
                        System.out.println("用户:" + worknumber + " sleep结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("用户:" + worknumber + " 成功移除服务!");
                    return true;
                }
            }
            log.info("用户:" + worknumber + " 无此服务!");
            return false;
        } catch (NotFoundException e) {
            e.printStackTrace();
            return false;
        }
    }

查询容器信息

这里为了拿到服务所在节点的IP以及Swarm随机映射出的端口号可以说是煞费苦心,尝试了很多方法都不行,包括上面提到的因为启动容器没有回调函数提示启动成功,我这边也不知道什么时候启动好,所以就又采取了一个比较笨拙的方法,轮询…

我保留这些对我来说暂时无用的注释,因为这些也都是docker-java中提供的,如果你需要,可以解开注释修改使用,如果不需要可以直接删掉。


    /**
     * @Descripition: 查询并存入服务的ip及port
     * @Author: wolf
     * @Date: 2020/4/12 0:18
     * @Param: [worknumber]
     * @Return: void
     **/
    @RequestMapping("/inspect")
    public String servicInspectPort(String worknumber) {
        //获取dockerClient
        DockerClient dockerClient = DockerJavaUtils.getDockerClient();

        //根据服务名称查询服务
//        List<Service> services = dockerClient.listServicesCmd()
//                .withNameFilter(Lists.newArrayList(worknumber))
//                .exec();
//        InspectContainerResponse response = dockerClient.inspectContainerCmd("b287a1bb508e20e523391e5261bd463f26568d5e31035cf1d4fe26c5e812f6e9").exec();
//        System.out.println(response);


//        for (Service service : services) {
//            service.getSpec().getName();
//            service.getSpec().getEndpointSpec().g
//        }
//
//        WaitContainerCmd waitContainerCmd = dockerClient.waitContainerCmd("");
//        WaitResponse waitResponse= new WaitResponse();
//        waitContainerCmd.exec(waitResponse.getStatusCode()).;

        //获取服务id
//        String serviceId = services.get(0).getId();
//
//        Service exec = dockerClient.inspectServiceCmd(serviceId).exec();
//
//        System.out.println(exec);


        //根据服务标签获取对应容器信息
//        List<Container> containers = dockerClient.listContainersCmd().withLabelFilter(ImmutableMap.of(worknumber, worknumber)).exec();
        List<Container> containers = dockerClient.listContainersCmd().withNameFilter(Lists.newArrayList(worknumber)).exec();
        if (containers.size() == 0) {
            System.out.println("containers数据为空!==============");
        }
//        Service service = dockerClient.listServicesCmd().withNameFilter(Lists.newArrayList(worknumber)).exec().get(0);
//
//        Task task = dockerClient.listTasksCmd().withServiceFilter(service.getId()).exec().get(0);
//        String nodeId = task.getNodeId();

//        List<SwarmNode> swarmNodes = dockerClient.listSwarmNodesCmd().withIdFilter(Lists.newArrayList(nodeId)).exec();
//        for (SwarmNode swarmNode : swarmNodes) {
//
//        }
//        String addr = swarmNodes.get(0).getManagerStatus().getAddr();

//        List<Task> exec = dockerClient.listTasksCmd().withNameFilter(worknumber).exec();
//        for (Task task : exec) {
//            String username = task.getSpec().getContainerSpec().getEnv().get(1);
//            if ((DOCKER_USER_NAME + worknumber).equals(username)) {
//                String nodeId = task.getNodeId();
//                List<SwarmNode> swarmNodes = dockerClient.listSwarmNodesCmd().withIdFilter(Lists.newArrayList(nodeId)).exec();
//                String addr;
//                for (SwarmNode swarmNode : swarmNodes) {
//                    addr = swarmNode.getManagerStatus().getAddr();
//                }
//                String containerID = task.getStatus().getContainerStatus().getContainerID();
//                InspectContainerResponse containerResponse = dockerClient.inspectContainerCmd(containerID).exec();
//        String[] env = containerResponse.getConfig().getEnv();
//        String dd = env[0];
//        String status = containerResponse.getState().getStatus();
//
//            }


//            WaitContainerCmd waitContainerCmd = dockerClient.waitContainerCmd(containerID);
//            WaitContainerResultCallback resultCallback = waitContainerCmd.exec(new WaitContainerResultCallback());

//            Integer integer = dockerClient.waitContainerCmd(containerID).exec(new WaitContainerResultCallback()).awaitStatusCode();
//            try {
//                resultCallback.onComplete();
//                resultCallback.awaitStatusCode(10, TimeUnit.SECONDS);
//                throw new AssertionError("启动超时,请重新启动!");
//            } catch (Exception e) {
//                e.printStackTrace();
//            }
//        }

        System.out.println("准备轮询--------------------");
        for (int i = 0; i < 120; i++) {
            System.out.println("轮询开始-----------------" + i);
            containers = dockerClient.listContainersCmd().withNameFilter(Lists.newArrayList(worknumber)).exec();
            //获取容器对外暴露的publicPort
            for (Container container : containers) {
//                InspectContainerResponse containerResponse = dockerClient.inspectContainerCmd(container.getId()).exec();
//                String[] env = containerResponse.getConfig().getEnv();
//
//                String[] split = env[1].split("=");
//                String username = split[1];
                String message = worknumber.equals(container.getLabels().get("com.docker.swarm.service.name")) ? "容器暂未启动=========":"容器启动成功==========";
                System.out.println(message);
//            container.getLabels().equals(ImmutableMap.of(worknumber, worknumber));
                if (worknumber.equals(container.getLabels().get("com.docker.swarm.service.name"))) {
//                if (worknumber.equals(username)) {
                    System.out.println("容器相关信息:===================");
                    System.out.println("容器"+worknumber+"的所有端口"+Lists.newArrayList(container.getPorts()));
//                    ContainerPort[] ports = container.getPorts();
//                    for (ContainerPort port : ports) {
//                        System.out.println("容器"+worknumber+"的所有对外端口"+port);
//                    }
                    Integer publicPort = container.getPorts()[0].getPublicPort();
                    //获取节点IP
                    Info info = dockerClient.infoCmd().exec();
                    String ipAddr = info.getSwarm().getNodeAddr();


//        InspectContainerResponse.Node node = containerResponse.getNode();
                    log.info("用户:" + worknumber + "节点IP:" + ipAddr);

                    log.info("用户:" + worknumber + "容器对外端口号:" + publicPort);
                    //将容器端口存入redis
                    String str = ":tcp -h " + ipAddr + " -p " + publicPort + " -t 1000";

                    return str;
                }
            }

            try {
                Thread.sleep(1000);
//                System.out.println("沉睡开始");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("无此容器!");
        return null;

    }

最后是这个定时任务,可以不需的话可以不写。

    /**
     * @param
     * @Return: void
     * @Description: 凌晨一点清除docker中多余容器
     * @Date: 2020/4/30 11:11
     * @Author: zse
     **/
    @Scheduled(cron = TIMING_TASK)
    public void scheduledDeletion() {
        DockerClient dockerClient = DockerJavaUtils.getDockerClient();
        List<String> workNumbers = userEntityMapper.getWorkNumbers();
        List<Container> containers = dockerClient.listContainersCmd().withNameFilter(workNumbers).exec();
        if (containers.size() == 0) {
            System.out.println("containers数据为空!==============");
        } else {
            for (Container container : containers) {
                System.out.println("定时任务启动,开始清除下线用户" + (container.getLabels().get("com.docker.swarm.service.name")) + "的容器!");
                removeService((container.getLabels().get("com.docker.swarm.service.name")));
            }
        }
    }

}

以上代码没有做细化的格式,相信这个也难不倒各位Java老司机,Swarm这个东西呢感觉在国内用的不多,网上的中文资料聊聊无几,尤其是踩到坑的时候关于排雷的论坛基本都是老外的帖子,另外就是获取节点IP和容器端口这个问题Swarm确实很鸡肋,不如k8s做得好。

学无止境,也欢迎大神能指出不足,相互进步。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐