博客行业干货文章详情
技术分享 | 如何让上千容器同时"存活"
showmebug2022年01月21日
行业干货

最近接到一个需求,用户可以申请创建一个或者多个 docker 容器,容器要一直存在,用户不管过了多长时间都可以访问,而且用户产生的数据一直存在,换而言之就是要做到容器持久化,也就是说我们要提供一个小型的服务器,但是久而久之服务器资源就会被容器占满,就只能扩容了,我要做的就是在尽可能节省服务器的情况下,提供尽可能多的 docker 容器。

简要思路

在用户使用一段时间后,把用户产生的差异数据持久化,然后停止容器,当用户再次访问,通过拦截访问地址去新建容器并把持久化数据放到新的容器中,然后重定向,用户就可以访问到与之前相同的容器,仿佛容器一直存在。

实现流程

1.当用户请求接口生成一个容器,返回域名 2.一段时间后,停止容器,并配置下次访问启动容器 3.用户访问,通过之前的配置重启容器,重定向返回用户之前的数据 具体如下图

工具支持

docker-client

因为我要用java操作docker,启动、停止、删除、执行命令等操作,所以找了一个快捷使用的工具。

附上官网 https://github.com/docker-java/docker-java

kong api

因为在用户访问容器内接口的时候,容器如果已经删了,那就没有办法访问了,所以我在外层做了一层代理,用的工具是 kong API,kong是基于nginx开发的 API Gateway,可以通过代码控制,而不像nginx那样去手动修改配置文件。

简单介绍一下我用到的几个功能:

Route:是请求的转发规则,按照 Hostname 和PATH,将请求转发给 Service。(我理解的就是 nginx 的 location)

Services:是多个 Upstream 的集合,是 Route的转发目标。(我理解的就是 nginx 的 server)

Plugin:是插件,plugin 可以是全局的,绑定到Service,绑定到 Router,绑定到 Consumer。(有鉴权,访问限制,监控,日志记录等插件,我这里用的是 request-transformer,要在请求里携带一些参数用于重定向)

附上官网 https://docs.konghq.com/

具体实现 一、生成容器

用户在访问接口的时候,后台创建并启动容器,并生成 kong 的配置,后续用户访问容器走的都是 kong 的代理,Route -> Services -> docker,设置心跳和失效时间,心跳是用户的操作产生的,失效时间是可以停止容器的时间,有心跳会更新失效时间。

代码示例

// 创建容器
CreateContainerCmd createContainerCmd = dockerClient
     .createContainerCmd(docker.getImage())
     .withTty(true)
     .withName(this.generateContainerName(docker))
     .withCmd(managerUrl + docker.getId())
     .withHostConfig(hostConfig);
CreateContainerResponse containerResponse = createContainerCmd.exec();
// 启动容器
dockerClient.startContainerCmd(containerResponse.getId()).exec();

二、停止容器

停止容器要保证下一次用户访问请求的时候容器可以再次启动,所以在停止容器后要把用户访问容器的请求,通过代理转到后台的接口,这里的代理 kong 。具体步骤就是后台会有一个job来控制让非活跃的容器持久化数据,然后停止容器,配置 kong,让下一次请求分发到后台服务。

具体实现:

1.增加 services 设置 http 接口请求地址

JKongAdmin admin = new JKongAdmin(kongAdminUrl);
ServiceResp resp = admin.addService(new ServiceReq.Builder()
                    .host(recoverHost)
                    .port(recoverPort)
                    .path(recoverPath)
                    .build());

2.增加 route 设置 serviceId, 用户访问域名

JKongAdmin admin = new JKongAdmin(kongAdminUrl);
RouteResp resp = admin.addRoute(new RouteReq.Builder()
                      .serviceId(serviceId)
                      .host(host)
                      .build());

3.增加 plugin 设置 serviceId,后台地址 主要用于重启容器是带入参数

```java
JKongAdmin admin = new JKongAdmin(kongAdminUrl);
Map<String, String> replaceConfigMap = new HashMap<>();
replaceConfigMap.put("uri", recoverPath);
PluginResp resp = admin.addPlugin(new PluginReq.Builder()
                       .name("request-transformer")
                       .serviceId(serviceId)
                       .config("replace",replaceConfigMap)
                       .enabled(true)
                       .build());

三、重启容器

用户通过域名访问到达 route,route 转发到对应的 service,在访问 service 配置的 http 接口,然后 http 接口启动容器,浏览器重定向,因为容器已经存在,只需要启动即可,速度可以达到秒级,以此达到容器“存活”的假象。

    @GetMapping("/recover")
    @ApiOperation(value = "资源恢复")
    public void recover(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String host = request.getHeader("x-forwarded-host");
        String path = request.getHeader("x-forwarded-path");
        this.playgroundService.recover(host);
        response.sendRedirect(host + ":" + kongClientPort + path);
    }

总结

最后再分析一下需求,其实难点主要在容器停止之后,页面访问,再把容器启动,因为用户访问的是容器绑定的域名,没有 http 接口,所以只能用 kong API拦截域名转发到后台服务器去启动 docker 和重定向,让空闲的容器停止,节省内存,需要的时候再启动,通过处理好像容器一直是启动状态,后台虽然转发的比较多,但是前台页面没有感知.

以上就是今天要讲的内容,技术上主要用到的是 kong API 和 docker-java,其实技术使用上没有什么难度,主要是分享思路。