随着项目不断扩大,你需要将其迁移到更大的虚拟机上。但如果新虚拟机环境超出了你的需求则会产生额外开支。
相比虚拟机,容器具有更小的粒度,并且无需重启运行中的实例即可垂直扩展。
单体应用和历史遗留应用无需更改配置,即可从虚拟机迁移至容器中。
Java 的垂直扩展需要适当配置 JVM 和清理垃圾回收器。
“即用即付”和“按需付费”这两种云收费模型的对比,及提高效率的正确选择。
云服务会很昂贵,而当你为不需要的资源付费时尤其如此;另一方面,资源不足又会导致宕机。身为程序员该如何应对?这篇文章将会讨论一些技术,来帮助你恰到好处地只支付实际消耗的资源,且不受应用体量增长的限制。
任何解决方案的第一步当然都是要承认问题的存在。以下详述大部分云用户都在面对的问题:
几乎所有的云服务供应商都提供一系列不同体量的虚拟机供选择。而如何选择合适的体量往往让人左右为难:选小了会有性能问题,负载上去搞不好还会挂。选多了?那常规负载和空闲时的资源就都浪费了。你自己的云托管应用是不是经历过类似的情形?
当项目开始横向扩展,资源使用低效的问题会重复出现在每个新加入的实例上。所以,这个问题也会相应地增长。
进一步说,如果你想在同一个虚拟机中增加一点点服务,目前大部分云服务供应商能提供的唯一方案是更换双倍体量的虚拟机。参考 AWS 的方案:
更糟糕的是,迁移过程会导致服务中断:需要停掉现在的虚拟机,一步步重新部署或迁移应用,其中不可避免会有相关的棘手问题需要解决。
可见虚拟机在资源使用方面不够灵活高效,并且针对动态负载的调整能力很有限。这种缺乏弹性的方式导致了额外的开支。
如果横向扩展无助于资源的高效利用,我们就需要深入虚拟机内部,理解垂直扩展是如何实现的。
垂直扩展可以在现有基础上优化任何应用实例的内存和 CPU 使用。如果配置得当,这种方法对单体应用和微服务都同样有效。
在虚拟机内通过动态增加或减少资源来进行垂直扩展同时又不中断服务是一项艰巨的任务。虚拟机提供了内存膨胀(memory balloning)的技术,但它并不是自动化的,需要用工具监控宿主系统和客户系统中的内存压力,然后进行相应的向上或向下扩展。而实际情况是这个方法并不太好用,自动化的内存共享才更实用。
得益于开创性的同宿主内容器间资源自动分享,基于 cgroups 的容器技术拥有更高级别的灵活性。在设定范围内,空闲的资源会自动分配给相同硬件节点内的其他容器。
不同于虚拟机,容器内的资源无需重启应用实例即可轻松实现扩展。
结果就是,同一容器实时更改体量大小要比迁移至更大的虚拟机更加容易、经济和迅速。
容器分为两种:应用容器和系统容器。应用容器(如 Docker 或 rkt)一般运行在单个进程中,而系统容器(LXD、OpenVZ)则更像是完整的操作系统,可以运行全功能的初始化系统如 systemd、SysVinit 和 openrc,这些初始化系统允许在单一容器中的进程孵化出其他进程如 openssh、crond 或 syslogd 等。这两种容器都支持垂直扩展来让资源分配更有效率。
理想情况下,在一个新项目中你可能想基于应用容器开始从头设计,因为应用容器可以使用 Docker 公开的模板较为容易地创建所需镜像。但关于容器的一个普遍存在的认知误区就是容器只适用于全新的应用(微服务和原生云)。已有的经验和用例可以证实,将已有的工作从虚拟机迁移至容器,而不重写或重新设计应用也是完全可能的。
对于单体应用和遗留应用来说,使用系统容器会更合适,因为你可以重用之前为虚拟机设计的架构和配置等:使用标准网络配置,如组播;在容器内运行多个进程,避免因不恰当的内存限制决策导致的问题;确保在容器重启过程写入本地文件系统是安全的;依照现有的方法排查问题和分析日志;使用大量基于 SSH 的配置工具;对其他老旧但重要的任务有较好兼容性。
为了从虚拟机中迁移出来,单体应用的拓扑结构需要拆解成更小的逻辑块,分布于互联的容器内。下图简单示意了拆解过程:
每个应用组件被置于隔离的容器内。通常这样可以简化应用的拓扑结构,因为项目中的某些部分可能在新架构中不是必需的。
例如,Jave EE WebLogic Server 在虚拟机环境中运行大致需要包含三种实例:管理服务器、节点管理(进程)和被管理服务器。拆解之后,我们可以去掉节点管理,因为它是作为虚拟机代理来添加或删除被管理服务器实例的。而现在,利用容器的编排(orchestration)平台以及一系列 WLST(WebLogic Server Scriting Tool)脚本,容器可以自动添加被管理服务器实例并将其直连到管理服务器上。
为继续迁移,你需要准备所需的容器镜像。对系统容器来说,这个过程比应用容器要复杂一点。你可以自己编译生成,或者使用像 Jelastic 之类的包含系统容器预设模板的编排工具来完成。
最后,部署项目并配置所需的互联接。
现在每个容器都可以动态向上向下扩展而无需停机。相比于虚拟机,容器更加轻量,所以扩展花费的时间比起虚拟机也要少得多。因为容器很容易从头再分配或克隆,所以横向扩展同样变得很顺畅。
对 y 于 Java 的垂直扩展来说,只用容器是不够的;JVM 同样需要合理配置。尤其值得注意的是,你所选择的垃圾收集器需要在运行时提供内存收缩机制。
这种垃圾收回收会将所有存活对象打包,去除垃圾对象,撤回(uncommit)和释放空闲内存。相较而言,如果是不支持内存紧缩的垃圾收集器或未经优化配置的 JVM,Java 应用会占用所有提交的内存,不能实现按应用负载变化的垂直扩展。很不幸,JDK 8 的默认并行垃圾收集器(-XX:+UseParallelGC)就不支持内存收缩,且没能解决 JVM 低效使用内存的问题。幸运的是,将垃圾收集器更换为 Garbage-First(-XX:+UseG1GC)可以很容易地解决这个问题。
请看下面的例子:即使你的应用内存占用很低(图中蓝色部分),未使用的资源还是不能分配给其他进程或其他容器,因为它们全部分给了 JVM(桔红色部分)。
好消息是,JDK 9 会默认启用支持内存收缩的 G1 垃圾回收器。主要的好处之一就是可以减少空闲内存碎片,同时缩短垃圾回收的停顿时间,且无需撤回未使用的堆内存。
如果你的 JDK 版本低于 9,可以使用以下参数启用 G1 垃圾收集器:
-XX:+UseG1GC
后面这两个参数用来配置内存资源的垂直扩展:
并且,在低负载或空闲期,应用应该定期调用 Full GC,如 System.gc()。此过程可在应用逻辑内部实现,或借助外部工具 Jelastic 垃圾回收代理实现。
在下图中,我们展示了启用以下 JVM 启动参数后 300 秒内的内存使用情况:
-XX:+UseG1GC -Xmx2g -Xms32m
如图所示,资源使用方面对比之前的例子有了巨大的提升。保留内存部分(桔红色)随着真实使用的部分(蓝色)缓慢增长,且空闲期内的资源没有浪费,所有最大堆内未使用的资源都可以被同一宿主内的其他容器或进程利用。
由此可证,容器技术和 G1 垃圾回收器的结合为 Java 云服务提供了最高效的资源利用。
最后(但很重要)的一步是选择“按需付费”收费模型的云服务,这样我们的花销就完全基于所消耗的部分。
云计算经常被拿来和电力使用相比较,(电力公司)按需求提供资源,属于“即用即付”。但他们之间有一个主要的区别——当你只多用了一点电时,电费账单并不会翻倍!
大部分云服务供应商采用“即用即付”的收费模型,这意味着可以一开始选一个小一点的服务器,然后随着项目增长再逐步增加服务器。但就像我之前描述的那样,你不可能简单地选择刚好适合你现在需求的服务体量,然后不用额外的人工操作,不用停止服务,就能按你的需求扩展。所以你会一直为这些限制买单——一开始是小机器,然后体量加倍,最后扩展为许多虚拟机,但每个都未被充分使用。
对比而言,得益于容器技术,“按需付费”的收费模型考虑的是当前应用实例的负载,并且实时供应或回收必需的资源。结果就是,你只需支付实际使用的那部分资源,并且不需要复杂的重新配置就可以扩展。
但如果你已经受制于某个供应商,虚拟机已经在工作了,你正在为那些限制买单,且没准备好改变它,那是否还有机会缓解状况,提高效率,节省开支?答案是,你可以用一个大一点的虚拟机,在上面安装一个容器引擎,然后把工作负载从那堆小虚拟机上迁移过来。这样你的应用就会运行在虚拟机中的容器内——像夹心蛋糕一样。但这样有助于巩固和精简已占用资源,同时释放和共享未占用资源。
认识到垂直扩展的好处有助于快速排除一系列性能问题,避免盲目横向扩展带来的不必要的复杂性,以及减少云端应用的开销——无论是单体应用还是微服务。
作者介绍
Ruslan Synytsky,Jelastic 的 CEO 和联合创始人。Jelastic 向开发者提供多云平台即服务(multi-cloud Platform-as-a-Service)。他设计了平台的核心技术,可在全世界范围内的数据中心部署和运行上百万个容器。Synytsky 致力于搭建高可用集群方案,为云端遗留应用和微服务应用的自动垂直扩展和水平扩展提供改进和增强方案。他在技术和商业方面都有丰富经验,并热衷参与各种程序员、主机提供商、集成商和企业相关的交流会议。
「细说云计算」是 InfoQ 旗下关注云计算技术的垂直社群,投稿请发邮件到 [email protected],注明“细说云计算投稿”即可。