Docker 的最佳实践提出了“一个容器内只运行一个进程”之后,是否需要将应用拆分到多个容器中就成了一个热门话题。本文以一个普通的 Java 应用作为讨论,从软件设计的角度来介绍为什么要将其拆分到多个容器中。
假设一个标准的 Java Web 应用包含以下两部分:
这两部分通常运行在同一个容器(如 Tomcat)中,相互之间基于 REST 接口进行交互。类似这种应用,我们应该将其拆分到不同的容器中运行吗?
简单的回答:我们应该将其拆分到不同的容器中,但这需要经过谨慎的考虑。
抛开一些“普适”的原则(如“一个容器只运行一个进程”),我们主要从应用本身和部署策略上进行考虑。
首先,我们抛开具体业务场景,做一些设计思考(Design Thinking):
- JVM 本身支持多线程,因此通常的 Java 应用程序都运行在一个进程中,以多线程的方式进行并行处理。类似 Tomcat 这样的容器本身也支持将多个 Java 应用程序运行在一个 Java 进程中。
- 事实上,很多已经容器化的应用,在容器中实际运行时也是多进程的, 例如 Apache 的 prefork 模块。同时,当前流行的网络应用(如 Nginx)大量使用事件驱动和反应器模式,这些设计的方向,都是将 IO 操作交给内核,将业务逻辑处理交给子进程(线程)。Linux 内核非常善于调度子进程,但是这不是诸如 Kubernetes、Swarm 等容器调度工具擅长的地方。进程(线程)侧重于内核资源的划分,而容器侧重于集群资源的划分。
- “一个容器只运行一个进程”的原则,更像一种哲学。作为工程师,我们应该从实际出发考虑技术架构和应用逻辑。同时这个“最佳实践”甚至没有获得广泛的赞同,它的传播可能是因为对 Unix 运作原理缺乏了解导致的。
- Linux 容器(LXC)历史上有很多种形式,其中很多是建议在一个容器中运行多个进程的。Linux 容器实质上是 clone 系统调用、SELinux、cgroup 等技术的组合,上层使用 LXC 或者 Docker(libcontainer)对于这些底层技术是不相关的,因为最终都依赖于内核的隔离技术。
- 进程间交互有多种形式,如套接字、文件、网络等,每种方式都有自己的优缺点。如果考虑将应用拆分到不同容器中,对应用程序内部组件之间的通信方式会有很大影响。
- 代码、配置和数据的独立性,也对是否能够拆分到不同容器的重要考量。如果应用程序的代码、配置和数据相互独立性比较强,就能够方便的将其拆分成独立的组件,放置到独立的容器中;反之,如果应用内部分层不清,拆分成本会非常高。值得注意的是,我们没有必要为了容器化而对应用做过多的改造,只要能够按照 Docker 镜像的格式制作成镜像,我们仍然能够享受到 Docker 带来的便利:方便的分发(利用 Docker 注册中心)和运行(使用 docker run 命令)。
上面总结了一些从系统层面看是否需要拆分到多个容器,回到我们的例子:
- 这两个 Java 组件功能上相互独立。一个是 web 前端,一个是 API 服务。由于它们是完全不同的服务,是否运行在一个 JVM(Java 进程)中对性能损耗非常小(当然,这只是相对的,因为它们将无法公用堆内存和垃圾回收)。
- 这两个组件之间使用 REST API 进行交互,没有使用传统的进程间交互方式(套接字、共享内存等)。
- web 前端和 API 服务的使用场景不同,针对不同的场景,在运维层面可能需要独立的进行扩容。例如除了 web 前端之外,API 服务还能提供给其他组件使用,因此 API 服务可能会因为接入方的变化需要独立扩容和缩容。这时候就是充分利用容器编排框架(如 Kubernetes、Mesos、Swarm 等)能力的时候了。
基于上述三点考虑,我会将这个 Java 应用的两个组件拆分到独立的容器中。同时建议配合使用诸如 Kubernetes 的容器编排框架,将两个服务关联到一起。
总结一下我们的“最佳实践”:
如果你的应用代码、配置和数据相对独立,并且有清晰的交互形式,将应用组件拆分到多个容器中是有意义的。 - - - - - -
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。
评论