Welcome 微信登录

首页 / 软件开发 / JAVA / 高密度Java应用部署的实践

高密度Java应用部署的实践2015-02-09 infoq 李三红传统的Java应用部署模式,一般遵循“硬件->操作系统->JVM->Java应用”这种自底向上的部署结构,其中JEE应用可以细化为“硬件->操作系统->JVM->JEE容器->JEE应用”的部署结构。这种部署结构往往比较重,操作系统、JVM和JEE容器造成的overhead很高,而很多时候一个Java应用并不需要跑满整个硬件的资源,导致这种模式的资源利用率是比较低的。

而另一方面,硬件虚拟化技术逐渐成熟:VMware Hypervisor、Xen、KVM、Power LPAR等技术能够帮助我们在同一个硬件上部署多个操作系统实例,而时下流行的OS Container技术如LXC、Docker等,则是把操作系统虚拟化为多个实例,实现更轻量级的虚拟化。无论哪个层面的虚拟化,其目的都是对资源利用率更加高效的追求,从而成为如今构建云计算平台底层架构的基础技术。

Java应用也可以通过同样的思路来实现高密度的部署。JVM虚拟化是比OS虚拟化更高一层的做法,可以更大程度的提高资源利用率,降低平均应用的部署成本。本文将介绍Multi-tenant JVM这一方案实现高密度Java应用部署的一些特点和思路。

背景介绍

早在2004年,Sun公司就提出过Java应用虚拟化这方面的想法。当时Grzegorz Czajkowski领导了一个叫做巴塞罗那的研究项目,该项目基于Java HotSpot虚拟1.5版本开发了Multi-Tasking Virtual Machine(MVM)。MVM的目的旨在提高Java程序的启动速度,节省内存开销。不过自从Sun被甲骨文收购后,我们没有听到关于该项目的任何新的进展。

尽管我们没有看到MVM成功产品化,不过它却留下两个JSR规范:和。对于JSR284,目前在java.net上有一个实现它的孵化项目。

从2009年开始,我所在的IBM Java团队开始研究Java应用的SaaS化方案,即让一个应用实例服务于多个租户。为了保证多个租户在使用同一个应用实例时候数据的隔离,该方案在应用这个层面做了一些Bytecode Instrument(BCI)的工作,主要通过改写getstatic/putstatic使每个租户有独立的类的静态数据拷贝而没有相互影响。但是,该方案在Bytecode层面更改带来的额外性能开销, 以及Java Reflection等访问带来的安全性/正确性的问题。 而且,除了数据上的隔离,也需要针对关键性的资源譬如CPU、Heap、IO等资源的使用进行管理,于是该方案下沉到了JVM层面,形成现在的。

Multi-tenant JVM是JVM层面的虚拟化,其思路是把多个Java应用部署在同一个JVM上,让这些应用共享底层的GC、JIT、Java运行时库等基础组件。除了IBM的团队之外,爱尔兰的也实现了多租户的JVM。和IBM Multi-tenant JVM类似,Waratek允许多个应用运行在同一个CloudVM上,每一个应用运行在一个叫Java Virtual Container(JVC)的容器里。从现有公开的资料开看,IBM Multi-tenant JVM是基于Java 7的,而Waratek是基于Java 6的,两者支持的CPU架构和平台也有所不同。

此外,JEE方面在两年前也有讨论,这项提议旨在定义PaaS环境下如何使得JEE应用支持多租户,保证不同租户在使用这些应用时相互隔离,以及资源方面的管理(如JMS资源),不过该项提议已经推迟到JEE 8。

除了提升部署密度之外,多租户的另一项好处在于应用启动的加速。快速的程序启动受益于不同的应用共享同一个JVM,我们称之为javad。Java核心的类库在javad运行后,不再需要被重新装载和定义。你也许可以用Nailgun来加速你的启动时间,但Nailgun的问题是没有安全的数据隔离,这包括类的静态数据以及Java属性值,而且Nailgun在易用性等方面也不如Multi-tenent JVM。

多租户JVM的实现思路

跟传统JVM相比,多租户JVM的主要工作围绕隔离而进行,其针对JVM/JDK的改动主要实现三个方面的目标:

租户之间的数据隔离

Java类库支持多租户语境

资源管理隔离

租户之间的数据隔离

让每个租户应用拥有独立的类静态数据拷贝,这个目标主要通过修改getstatic/putstatic字节码指令实现。下面是一个简单的例子:

public class HelloWorld {public static void main(String[] args) {System.out.println("Hello World!");}}
每一个运行在Multi-tenant JVM上的程序都有不同System.out实例。就java.lnag.System内部实现来说,out是其类静态变量:

public final class System {// The standard input, output, and error streams.// Typically, these are connected to the shell which// ran the Java program./** * Default input stream */public static final InputStream in = null;/** * Default output stream */public static final PrintStream out = null;/** * Default error output stream */public static final PrintStream err = null;……}
Multi-tenant JVM对于标准的JVM行为进行的更改如下:

每一个租户第一次使用java/lang/System时,都会触发它的初始化,也就是<clinit>。而一般的JVM,java/lang/System只会被初始化一次。

<clinit>的执行,对于每一个静态成员变量存取,都被重新定向到了具体的租户存贮空间。比如对于out = null赋值,putstatic执行时实际上会找到当前的租户,然后把值存到该租户的空间去 ,getstatic有着类似的道理。