白渐 分布式实验室 

一种新的进入容器的方式: WebSocket + Docker Remote API_Java

众所周知,容器是基于操作系统内核的一种轻量级的虚拟化技术。其可以类比于虚拟机,但其本身并不是虚拟机。在传统的虚拟机使用场景中,每个用户都会通过堡垒机,根据自己被分配的权限,登录某些机器的某些账号。当应用部署逐渐转移到基于容器技术的PaaS平台上后,让用户进入容器进行观察、调试应用已经成为了PaaS平台的一个重要且必备的功能。

远程进入容器功能的传统实现方式是基于虚拟机的思想,在每个容器中启动一个sshd进程。由于容器PID为1的进程的特殊性,为了保证容器不停,容器的ENTRYPOINT需要设置为类似于Supervisord这样的进程管理程序。在这种多进程容器的使用场景中,用户通过ssh-client指定容器的IP远程连接到容器,让用户感觉到自己好像就在使用虚拟机。但是,这种方案会带来以下问题:

  1. 权限管理。如何控制哪些用户能够登录哪些容器?如何和平台已有的权限管理系统集成?这种情况往往都需要通过堡垒机系统控制。而在PaaS中,引入单独的堡垒机系统会增加PaaS的复杂度以及维护成本。

  2. 登录方式选择。无论使用密码还是私钥验证登录,容器内的密码或者authorized_keys的管理都需要通过加入额外的程序解决,无疑会增加容器的复杂度。同时还要面对同权限容器的密码或authorized_keys的一致性问题。

基于以上问题,在我们的LAIN平台中,设计出了基于WebSocket协议与Docker Remote API的远程登录方案。LAIN(https://laincloud.com)是一个基于Docker的PaaS。其面向技术栈多样寻求高效运维方案的高速发展中的组织,DevOps人力缺乏的startup以及个人开发者。LAIN通过统一高效的开发工作流,降低应用运维复杂度;在IaaS / 私有IDC裸机的基础上直接提供应用开发,集成,部署,运维的一揽子解决方案。

该方案的整体架构图如下:

一种新的进入容器的方式: WebSocket + Docker Remote API_Java_02

从图中可以看出,在LAIN中实现容器远程登录支持需要以下两个组件:

  1.  Entry应用。负责如下工作:

  • 调用Docker Remote API

  • 通过WebSocket 传递stdin,stdout和stderr。

  • 根据protobuf3协议对各类消息进行序列化与反序列化。

  • 对用户登录的鉴权。

    Entry是基于Go语言开发的,并依赖如下代码库:

  • github.com/gorilla/websocket:WebSocket的服务端实现。

  • github.com/fsouza/go-dockerclient:Go语言的Docker客户端。

  • github.com/golang/protobuf/proto:protobuf协议的支持库。

  2.  基于命令行的客户端。负责如下工作:

  • WebSocket连接请求的发送。

  • 监听键盘输入、窗口变化事件以及WebSocket返回的stream。

  • 将远端的stdout,stderr输出到本地终端的标准输出和标准错误。

一种新的进入容器的方式: WebSocket + Docker Remote API_Java_03


通过命令行客户端远程登录容器的过程及其实现如下:

  1. 用户通过客户端命令向Entry应用发送WebSocket连接请求。

  2. Entry应用接收到用户请求,得到请求Header中的access_token以及要进入的容器信息,通过调用LAIN的console接口判断该用户是否有权限进入容器。如果没有权限,则直接通知客户端鉴权失败,本次连接结束。

  3. 如果通过了权限验证,则WebSocket连接会被建立。紧接着Entry会去调用 execCreate 这个Docker Remote API。在调用时,需要指定Tty,AttachStdin、AttachStdout和AttachStderr参数均为true,Cmd参数为bash,这样才能获得bash进程的标准输入输出和错误。

    如果调用execCreate成功,调用请求会返回该Exec的ID,Entry会继续根据这个ID调用execStart接口。在调用时,需要指定Detach和Tty为false,这样才能连接到bash进程的标准输入输出和错误。调用execCreate成功后,会返回一个HTTP的stream。在Entry中则通过3个goroutine分别处理stdin,stdout和stderr。

  4. 客户端会同时监听WebSocket连接与键盘输入,对于WebSocket返回的Message,客户端会通过Entry制定的protobuf3消息格式反序列化出消息结构,并根据消息的类型,将数据发送到本地终端的stdout或stderr。对于键盘输入,客户端会将输入内容封装,经过protobuf3序列化后,通过WebSocket发送给Entry应用,Entry应用经过反序列化后,将输入发送给bash的stdin。

以上就是Entry的工作原理。从中我们可以看出,Entry已经很好地解决了传统ssh-client登录所遇到的问题:

  • Entry通过调用console的接口完成了身份验证工作,由于所有的权限都被console统一管理,因此Entry不需要自己维护权限信息,即Entry本身是无状态应用。这种应用的优势在于可以低成本扩容,用以应对多并发的场景。

  • Entry通过Docker Remote API连接容器,这样只要被连接的容器内可以启动bash进程,用户就可以通过客户端连接到该容器。容器无需启动sshd进程,也就无需再以supervisord等进程作为entrypoint。更多的容器就可以以单进程的形式运行,降低了容器本身的维护成本。

一种新的进入容器的方式: WebSocket + Docker Remote API_Java_04


俗话说,细节决定成败。为了提高使用体验,Entry应用在设计与实现时考虑到了很多细节,在这里拿出来与大家分享。

1. 连接保持:当WebSocket连接在一段时间内没有数据传输后,会自动断开。这给用户的使用带来了极大的不便。Entry在设计时,对每一个建立的WebSocket连接,会有一个单独的goroutine每隔10秒发送一个PING类型的Message(不是WebSocket协议中的PingMessage),这样保证了在不主动断开的情况下,用户和容器可以一直保持连接。

2. 使用protobuf3制定消息格式并实现序列化与反序列化:使用protobuf3可以方便地定义与扩展自己的消息格式,同时在传输时能减小一定的带宽占用。

Entry的消息格式有两类,RequestMessage和ResponseMessage。客户端发送的请求都属于RequestMessage,服务端返回的数据都封装在ResponseMessage中。其中:

  • RequestMessage类型包括:PLAIN和WINCH。PLAIN就是用户通过键盘的输入。WINCH则是终端窗口大小改变的消息,内容中会携带新窗口的rows和cols。

  • ResponseMessage类型包括:STDOUT, STDERR,PING和CLOSE。STDOUT和STDERR代表了该消息内容是来自于标准输出还是标准错误。PING则代表是连接保持专用的信息。CLOSE则是连接将要断开前Entry返回的信息,会包含错误原因或者正常退出的信息。

3. 监听终端窗口大小改变:默认的终端窗口大小都是80 * 24,但该标准在当前的日常使用中早已过时。如果在一个全屏的terminal中仍然使用该大小显然是不合理的。因此客户端在成功连接到容器后,客户端会首先根据当前的terminal大小发送一个WINCH类型的RequestMessage,Entry收到后会调用ExecResize接口,这样之后所有的stdout和stderr都会按照新的终端大小显示。客户端还会监听窗口大小改变的事件,如果发生改变,同样还会发送WINCH到Entry。

4. UTF-8编码检查:客户端和服务端在发送消息内容时,都会对缓冲区内要发送的数据做UTF-8编码检查。如果发送数据不符合编码规则,则会先发送最长符合的缓冲区前缀,后面剩余的数据则被移到缓冲区的开始,待下次发送。这种设计是为了处理中文等非latin1字符的显示问题。避免因为非法的UTF-8编码造成终端显示乱码。

一种新的进入容器的方式: WebSocket + Docker Remote API_Java_05


  1. 非正常退出时,bash进程不会结束,而是会以sleep的状态残留于容器中。如果一个容器有过多的bash进程,很可能因为cgroup的内存限制导致容器退出。目前官方并没有给出类似execKill的API,只能期待在以后版本的docker中能解决这个问题。

  2. Entry应用依赖特定的LAIN客户端。之前用户只能通过lain enter命令进入容器。但是12月份后我们升级了console的前端,增加了web terminal功能。用户只需要通过点击容器的ID就可以打开一个含有terminal的web页面,然后通过该web页面与容器进行交互,不需要再安装任何客户端。在这里要十分感谢开源项目xterm.js(https://github.com/sourcelair/xterm.js),该项目基于JavaScript与CSS实现了一个近乎完美模拟xterm终端的插件。目前,console的web terminal可以支持Firefox和Chrome,但是无法支持IE和Safari。

一种新的进入容器的方式: WebSocket + Docker Remote API_Java_06


Entry是LAIN中一款设计较为精巧、技术含量较高的应用。其利用了WebSocket全双工传输的特点,在单进程容器的场景下实现了对容器的远程登录,同时保证了登录权限的控制。本文希望通过分享LAIN中Entry的设计与实现,为需要开发远程登录容器功能的PaaS同行提供技术方案参考。Entry已经开源,地址在(https://github.com/laincloud/entry),欢迎一起讨论交流学习。

文章来源:宜信大数据创新中心