技术分享 | 如何让上千容器同时"存活"

99 阅读4分钟

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

一、 简要思路

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

二、 实现流程

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

微信图片_20220121113109.jpg

三、 工具支持

docker-client


因为我要用java操作docker,启动、停止、删除、执行命令等操作,所以找了一个快捷使用的工具。
附上官网 github.com/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,要在请求里携带一些参数用于重定向)
附上官网 docs.konghq.com/

四、 具体实现

1、生成容器


用户在访问接口的时候,后台创建并启动容器,并生成 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();

2、停止容器

停止容器要保证下一次用户访问请求的时候容器可以再次启动,所以在停止容器后要把用户访问容器的请求,通过代理转到后台的接口,这里的代理 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());

微信图片_20220121113605.jpg

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

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

微信图片_20220121113853.png

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

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());

微信图片_20220121114053.jpg

3、重启容器

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

微信图片_20220121114611.jpg

@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,其实技术使用上没有什么难度,主要是分享思路。