DockOne微信分享(一一一):LAIN 平台远程进入容器功能设计与实现

本文讲的是DockOne微信分享(一一一):LAIN 平台远程进入容器功能设计与实现【编者的话】本次分享主要介绍在宜信大数据创新中心的开源 PaaS 平台LAIN中,基于 WebSocket 和 Docker Remote API 远程进入单进程容器功能的设计与实现。

【上海站|3天烧脑式微服务架构训练营】培训内容包括:DevOps、微服务、Spring Cloud、Eureka、Ribbon、Feign、Hystrix、Zuul、Spring Cloud Config、Spring Cloud Sleuth 等。

LAIN 平台介绍

LAIN 是宜信大数据创新中心基于 Docker 开发的 PaaS 平台。目前已经开源至 GitHub 并已经在内部生产化。

LAIN 提供了裸机之上、应用开发之下的 DevOps 问题的整体解决方案;规范了应用开发、测试、上线工作流,辅以 SCM 支持;提高了整体资源使用率,优化冗余资源池。

LAIN 通过 Swarm 实现 Docker 集群化,用 etcd 作为服务发现与底层元数据存储。同时还集成了我们自己开发的容器调度器、控制器以及权限管理系统等。

远程进入容器方案调研

相信开发过 PaaS 平台的同学都面对过平台用户远程进入容器调试的要求。

在多进程容器的场景下,这种需求似乎很容易实现,只需要在内部启动一个 sshd 进程即可。但是,这种设计会带来一些问题。

首先是权限管理。如何控制用户和容器的权限对应关系?在传统的虚拟机或物理服务器的使用场景中,公司都会为这些基础设施配备相应的堡垒机管理系统。但是对于一个 PaaS 平台配备这种系统会使得平台变得复杂且臃肿。

其次是配置管理。用户如果需要修改密码或者信任公钥,如何实现所有容器的 sshd 配置同步修改并保证一致?

最后则是容器进程管理。容器的 1 号进程不仅需要管理 sshd,而且 sshd 需要暴露端口给客户端连接。由于不同宿主机的环境的差异,很可能出现暴露端口不一致的情况,这也会严重影响用户的使用体验。

在单进程容器的场景下,上面的方案就行不通了。让用户直接登录到宿主机执行 docker exec -it 显然也不是一个可行的办法。

我们通过调研 Docker 的 API 及其实现,探究出了一种解决容器远程登录需求的方案。

Entry 应用

Entry 应用分为服务端和客户端部分,整体结构如下图。

其中,Entry 的服务端负责和 Docker 进程通信,获取容器内终端的输出并发送用户输入到终端。Entry 的服务端还负责对用户鉴权、接收用户来自终端的输入并将容器终端的输出返回给用户。

Entry 的客户端则分为命令行客户端和 Web 客户端,二者功能类似,均是负责连接服务端,并发送用户的键盘输入,接收并仿真出容器的终端输出。

Entry 服务端和 Docker 通信实现

在介绍服务端和 Docker 通信的实现之前,让我们先看一下 Docker 的一个 API。

通过介绍可以看出,该 API 通过 WebSocket 实现输入和输出的全双工传输。

受到这个 API 的启发,我们认为既然通过 WebSocket 可以传输容器的输入输出,那自然也可以传输用户的输入输出。最后只要将这两部分的输入输出对接即可实现。

使用 WebSocket 的好处是,因为 WebSocket 是通过 HTTP 协议建立连接的,因此数据传输可以直接利用统一的 Nginx 入口,不需要额外开放端口。在开发过程中,我们可以在 WebSocket 协议的基础上定义自己的消息格式,这样使我们的应用设计更为灵活。

go 语言中已经有很多优秀的 Docker 客户端库和 WebSocket 库。我们使用的是 github.com/fsouza/go-dockerclient 和 github.com/gorilla/websocket。

和 docker exec 相关的 API 有两个,第一个是下图中的 createExec。

其对应的参数结构体如下图:

在参数中,设置 AttachStdout,AttachStderr,AttachStdin 和 Tty 均为 true ,即我们需要打开终端并且获得其输入输出流。cmd 参数则设置为我们经常使用的 “docker exec -it xxx bash” 后面的 bash 进程。由于 Docker 自带的终端类型(dumb)太挫,这里在执行 bash 命令时设置临时环境变量 TERM=xterm-256 以提升使用体验。

第二个 API 则是 startExec。

其对应的参数的结构体如下图。

CreateExec 执行成功后,会产生对应的 ExecID 。参数中还需要给 stdOut/stdErr 和 stdIn 分别传入 writer 和 reader 用来写入或读取。而我们则需要从另一边读到或写入数据,这就像是管道的两端。

恰好,Go 语言提供了类似管道的 PipeReader 和 PipeWriter。通过 io.Pipe() 即可获得一对 writer 和 reader。然后,启动3个 goroutine 去处理这三类数据的输入输出,这样服务端和 Docker 的通信就基本完成了。

Entry 服务端和客户端通信实现

Entry 服务端本质上是一个 WebSocket 服务端,接受来自客户端的 WebSocket 请求并通过 WebSocket 传输数据。在高级语言中,对 WebSocket 的操作是以消息为单位的。我们可以定义每条消息内数据的协议格式。

Entry 中,使用 protobuf3 定义数据的序列化协议。如下图。

RequestMessage 代表客户端发送的请求信息,包括 PLAIN 和 WINCH 两种类型。

PLAIN 代表用户一般的输入信息。WINCH 则代表终端窗口的大小改变请求。

因为不同大小的终端窗口,显示的结果不一样,因此我们需要根据用户的实际情况改变终端的显示方式。幸运的是,Docker 提供了 ResizeExecTTY 这种改变窗口大小的 API,恰好满足了我们的需求。

PLAIN 消息中,content 是用户输入的字节数组。WINCH 中,content 则是一个包含终端长和宽的一个 JSON 串的字节数组。

服务端在接收到 PLAIN 消息后,会原封不动地发给 Docker。在接收到 WINCH 消息后,会解析该 JSON ,然后调用改变窗口大小的接口。在这里需要注意一下,在 Docker 1.12 以后,只有 StartExec 成功执行后,才能执行 ResizeExecTTY 。

ResponseMessage 代表服务端返回的信息,包括 STDOUT,STDERR, CLOSE 和 PING 四种。

  • STDOUT:携带的是容器终端的标准输出数据。
  • STDERR:携带的是容器终端的标准错误数据。
  • CLOSE:是服务端连接关闭的消息。
  • PING:则是服务端为保持 WebSocket 连接存活发送的信息。

终端显示的效果,例如颜色、进度条等,都是通过一系列终端控制字符实现的。这些本质上也是 ASCII字符。因此在客户端输出的时候,不需要考虑数据如何,只需要直接 print 即可。

下面介绍一下客户端和服务端通信的工作流程。

首先,客户端向服务端发送 WebSocket 连接请求。对于命令行客户端,在请求头中会携带客户端的 token 信息(用于身份验证),以及要登录的容器信息。对于 Web 客户端,由于浏览器中的 WebSocket 客户端不支持自定义 header,因此需要通过别的方式发送身份验证信息。这个后面会讲到。

服务端收到连接建立请求后,判断如果是命令行客户端,则从头中拿出token和容器信息,在权限系统中判断该用户是否有权限登录容器。如果没有权限,则关闭连接并返回 CLOSE 信息,信息中会携带错误原因。如果是 web 客户端,则服务端会等待客户端发送的第一条 PLAIN 信息,这个信息中会包含要登录的容器信息和 token。之后服务端会做同样的鉴权工作。

鉴权完成后,服务端会保持和客户端的连接,并负责传输客户端和 Docker 的输入输出。同时每隔一定时间会向客户端发送一条 PING 信息,保证连接的存活。

当客户端先断开连接时,服务端会断开和 Docker 的连接,并回收相应的 IO 资源。

当 Docker 先断开连接时,服务端会向客户端发送 CLOSE 信息,并关闭 websocket 连接,同时回收相应的 IO 资源。

在数据的传输过程中,考虑到传输的数据有可能包含中文等非 ASCII 字符,因此我们统一字符编码均为 UTF-8。然而,传输给客户端时使用的发送缓冲区大小是有限的。

为了保证每次发送的数据都符合 UTF-8 编码规则,Entry 使用如下算法计算一个字节数组中,最长的合法 UTF-8 编码的前缀位置。

由于一个合法 UTF-8 编码最长4个字节,因此最多循环4次就可以找到最后一个 UTF-8 的开始字节。

得到最长的合法 UTF-8 编码子数组后,将该子数组的数据放到消息中发送给客户端,剩下的字节则放到缓冲区开始,待下次发送。

从这个流程中我们可以看出,Entry 实际上就是一个终端和 Docker 的高级代理。

Entry 的命令行客户端实现

Entry 命令行客户端集成至 LAIN 的命令行工具 lain-cli 中,通过 lain enter 命令指定集群名称、应用名称以及容器编号即可远程登录,使用体验和 SSH 登录是一样的。

Entry 的命令行客户端使用 Python 实现,protobuf3 的定义可以用 protobuf3 自动生成对应的 Python 代码。

在命令行的实现中,客户端在建立 WebSocket 连接后,会首先得到当前终端窗口的大小,并发送一个 WINCH 信息,这样可以在一开始就适配当前的终端大小。在交互过程中,除了要处理键盘输入、WebSocket 的返回,客户端还需要监听窗口大小改变的事件,这个在程序中体现为 signal.SIGWINCH 系统信号。当收到该信号时,客户端需要再次获得更新后的窗口大小,并发送 WINCH 消息给服务端。

Entry 的 Web 客户端实现

在 Web 页面中完全模拟命令行终端是一件非常困难的事情,如果自己开发插件,需要处理各种终端控制字符。

在这里向大家推荐一个十分好用的模拟终端的 js 库,xterm.js。这个库的安装与使用不需要任何第三方依赖,既可以以 js 文件的形式引入到页面中,也可以通过 npm 安装。

Web 客户端的实现和命令行类似,也是要监听键盘输入、WebSocket 返回和窗口改变。在监听键盘输入和窗口改变时,xterm.js 提供了 data 和 resize 两个事件,因此只需要实现这两个事件的 handler 就可以了。
在浏览器中,按 esc 键会失去当前组件的焦点。但是在命令行中,我们肯定会遇到需要使用 esc 键的情况,例如 vim 中从 INSERT 切换为 NORMAL。因此实现时需要特殊处理 esc 按键事件。

在序列化和反序列化数据时需要注意的是,因为之前在 protobuf3 中定义的 content 字段是字节数组类型。而 web 客户端是无法使用 protobuf3 生成代码的。因此这里需要使用 JSON 替代 protobuf3 序列化消息。而 go 语言中,对字节数组的 JSON 序列化则是将其转化为 Base64 编码的字符串。因此 Web 客户端在发送数据时,也需要将输入转化为 Base64 编码字符串,这样在服务端才能正确反序列化消息。同理,在接收到 WebSocket 的返回消息时,也需要用 Base64 解码以获得正确的输出。

由于服务端需要处理两种不同客户端的请求,因此我们将序列化和反序列化的逻辑抽象出来,定义了自己的序列化,反序列化的函数类型。Go 语言中 json 天生就满足该类型。

然后包装了 protobuf 的实现。

以上是 Web 客户端的实现细节。

Entry 的后续工作

目前的 Entry 还存在一些问题。

首先是浏览器兼容性问题。目前测试 Web 客户端在 Chrome 和 Firefox 浏览器是工作正常的。但是在 Safari 浏览器中,会出现卡住的情况,这个原因目前还在定位中。

其次是非正常退出的问题。如果客户端不是通过 exit 命令,而是直接断掉 websocket 连接,就会有 bash 进程残留在容器中。而一个 bash 进程多多少少还是会占一些资源的。如果 Docker 将来能提供 StopExec 类似的接口,这个问题应该可以解决。

目前,我们在开发 Entry 的新功能 attach,即attach到容器的标准输出,提供了一个远程获得进程标准输出的功能。依赖的接口是 AttachToContainerNonBlocking ,其他的实现则可以复用enter的部分。

总结

Entry 作为远程终端和Docker的高级通信代理,提供了身份验证、输入输出数据传输等功能,是一种远程登录容器的解决方案。该方案同时适用于单容器和多容器场景。

但是,远程登录容器也应该只用于线上调试,而不能等同使用虚拟机。更为完善的日志收集系统、监控报警机制才是 PaaS 平台调试和监控线上服务运行情况的关键。

以上就是我的分享内容,欢迎大家就该问题一起讨论交流。谢谢。

问题与回答

Q:服务端迭代升级的过程怎么保证和旧版本的客户端兼容?

A:服务端升级时,不修改已有协议,只会增加协议。因为协议定义已经一致,因此协议上层的业务逻辑的修改不会影响到客户端使用。

Q:是否允许多用户同时访问,是否有可能互相干扰?

A:不同的用户和服务端连接时肯定是建立不同的 WebSocket 连接,而对于同一个容器执行两次 exec 产生的也是 bash 进程。但是 gorilla/websocket 这个库,对于同一个 WebSocket 连接,不能有两个以上的 goroutine 同时读或者同时写,因此实现时要注意并发控制。

Q:不同宿主机的容器是怎么实现通信的?

A:Entry 是用作平台用户(应用开发运维人员)从自己的机器上远程登录的。这个问题应该属于平台的网络设计部分,我们平台用的 Calico 实现的容器网络,这里就不展开讨论了。

Q:连接时 token 在传输过程中会有被篡改的可能吗?

A:即使客户端伪造 token,如果 token 和我们的身份系统中记录的不符,也无法登录容器。而在传输过程中,我们使用 WSS(基于 HTTPS ),连接信息是加密的,token不会被窃取。

Q: 除了 CLI 和 Web 接口,以后可能有 SDK 接口么?这样可能第三方会使用。

A:客户端的实现关键就是对消息的序列化反序列化,而我们是使用的 protobuf3定义的,protobuf3 可以生成各种主流语言的代码,因此不需要单独开发 SDK。

以上内容根据2017年03月21日晚微信群分享内容整理。分享人白渐,宜信大数据创新中心研发工程师,主要负责开源平台LAIN相关组建的开发与维护,以及部门基础设施的运维工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesz,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

原文发布时间为:2017-03-23

本文作者:白渐

原文标题:DockOne微信分享(一一一):LAIN 平台远程进入容器功能设计与实现

时间: 2024-10-31 06:55:01

DockOne微信分享(一一一):LAIN 平台远程进入容器功能设计与实现的相关文章

DockOne微信分享(一一七):沪江容器化运维实践

本文讲的是DockOne微信分享(一一七):沪江容器化运维实践[编者的话]沪江目前容器技术主要应用场景:OCS课件业务无状态应用:基于Apache Mesos+Marathon实现沪江容器系统调度管理:Consul + Consul Template + Nginx实现服务自动发现和注册:Prometheus + Grafana + Alertmanager报警实现容器监控报警.本次分享将从以下几方面来讲解: 选择容器技术缘由 容器技术选型 容器存储 容器网络 监控报警 镜像管理 调度管理 服务

DockOne微信分享(一一八):容器技术在企业级服务里的实践

本文讲的是DockOne微信分享(一一八):容器技术在企业级服务里的实践[编者的话]邻盛在做面向中小微企业做服务的时候, 实际遇到很多情况, 比如对方IT基础过于薄弱, 比如基础设施过于简陋, 比如产品要解决行业需求, 企业个性需求等等,经过几年积累目前摸索出了一套完整的产品方案.目前产品是以容器为核心的一套完整的PaaS平台+全新的微服务架构+底层能力构成的完整解决方案, 目前也进入到了几家传统大型制造企业协助他们完成新一代的信息升级. [深圳站|3天烧脑式Kubernetes训练营]培训内容

DockOne微信分享(一一三):从一个实际案例来谈容器落地的问题

本文讲的是DockOne微信分享(一一三):从一个实际案例来谈容器落地的问题[编者的话]容器是这两年最热的一个话题,去年大家都在谈Mesos.Kubernetes.Swarm,究竟哪家的挖掘技术强,今年容器技术的进一步普及,更多的人更关心容器技术如何落地,下面我们就基于一个实际的案例来聊一下容器落地遇到的问题. [深圳站|3天烧脑式Kubernetes训练营]培训内容包括:Kubernetes概述和架构.部署和核心机制分析.进阶篇--Kubernetes调工作原理及源码分析等. 背景:某银行数据

DockOne微信分享(一一六):某股份制商业银行定制化PaaS介绍

本文讲的是DockOne微信分享(一一六):某股份制商业银行定制化PaaS介绍[编者的话]某股份制商业银行的PaaS平台是由Wise2C与Rancher合作,基于Rancher做的定制化开发.基于业务场景和银行业的特殊需求,同时为了兼顾能够实现对以后Rancher版本的平滑升级,我们在Rancher之上做了一层逻辑抽象. [深圳站|3天烧脑式Kubernetes训练营]培训内容包括:Kubernetes概述.架构.日志和监控,部署.自动驾驶.服务发现.网络方案等核心机制分析,进阶篇--Kuber

DockOne微信分享(一一九):Elastic-Job-Cloud作业云在当当的SRE实践

本文讲的是DockOne微信分享(一一九):Elastic-Job-Cloud作业云在当当的SRE实践[编者的话]本次分享面向对Mesos与SRE感兴趣的听众.随着容器技术在国内的持续流行,关注点已经由容器技术本身向运维方面逐渐过渡,Google一直安利的SRE经验正好契合了这个时代的运维节奏,由此契合SRE概念而衍生的Mesos,Kubernete服务也持续推动着相关理念落地.当当正是在这种背景下研发并推广作业云平台,该平台借助Mesos平台打造高自动化云平台.本次分享将重点为大家带来我们是如

DockOne微信分享(一一零):Docker在沪江落地的实践

本文讲的是DockOne微信分享(一一零):Docker在沪江落地的实践[编者的话]容器化是很多公司技术层向往又惧怕的一项热门技术,它的高效性,封装性能给开发.运维带来许多便利,但其本身也需要较强的技术能力去控制,否则会变成一个无法落地的概念.沪江作为教育界的独角兽,随着业务的增长,在开发.测试.运维上的成本增加日益显著.经过我们一年的探索,终于使Docker技术在沪江落地,不但成功的降低了成本,并吸引了其他部门的关注与试用,取得良好的成效. [上海站|3天烧脑式微服务架构训练营]培训内容包括:

DockOne微信分享( 一零一):构建容器服务平台(CaaS)

本文讲的是DockOne微信分享( 一零一):构建容器服务平台(CaaS)[编者的话]容器技术作为这两年最令人瞩目的技术,在各个行业无论是互联网还是传统行业都得到广大的应用.作为致力于打造金融行业领先的平安云,于今年引进容器技术,研发平安云容器服务平台,吧容器技术应用到业务中,推动业务和技术快速发展.本次分享的核心内容即是从用户 痛点及特征分析想如何构建平安云容器平台.分为4个部分: 容器平台定位 容器平台设计 容器平台架构 容器平台设计技术 一.定位 用户 首先平安集团旗下的子公司包含了金融行

DockOne微信分享( 一零二):基于容器的日志管理实践

本文讲的是DockOne微信分享( 一零二):基于容器的日志管理实践[编者的话]业务平台每天产生大量日志数据,为了实现数据分析,需要将生产服务器上的所有日志收集后进行大数据分析处理,Docker提供了日志驱动,然而并不能满足不同场景需求,本次将结合实例分享日志采集.存储以及告警等方面的实践经验. 2013年以来Docker迅速火了起来,它的理念带来了非常大的便利性,不过实际应用中会发现还有监控.日志.网络等问题尚待解决,本文会结合实例分享数人云做容器日志系统的经验. 基于ELK的日志管理系统架构

DockOne微信分享(一四二):容器云在万达的落地经验

本文讲的是DockOne微信分享(一四二):容器云在万达的落地经验[编者的话]容器生态是现在非常火热的技术生态之一,个人认为它主要囊括着四个方面的技术栈:一是容器核心技术栈(包括 Docker.rkt 及第三方公司自主研发的容器 Engine 等):二是容器基础技术栈(包括容器网络.存储.安全及服务发现等):三是容器编排技术栈(包括 Mesos/Marathon.Swarm.Kubernetes 及 OpenShift 等):四是容器应用技术栈(主要包括 CI/CD.监控.日志及微服务框架等).