最近接到一个需求,用户可以申请创建一个或者多个 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,其实技术使用上没有什么难度,主要是分享思路。