内容


如何提高系统性能指标

MMF 在 Java 中的应用

Comments

前言

当我写下这个标题后,我被自己吓啦一大跳,怎么取个这么大的题目。要知道这可是无数人一生的舞台!我在这里不想也不可能针对提高系统性能的方方面面一一讲解,只是想结合具体的个案来谈谈我们在进行系统移植过程中对提高 J2EE 系统性能指标的一些思考和设计方案,主要是关于数据缓存技术的应用,但愿不会给大家带来太大的误会。

说明:

本文所有的讨论都是基于 Windows 平台,至于其他主流平台比如 Unix、Linux 等也有类似于 MMF 的实现机制,在此不再赘述。

概念 -- 什么是 MMF ? (1)

从现在开始,MMF 一词将在本文中大量出现。所以,我在此先对 MMF 做一个简单的描述。MMF,全称 Memory Mapped Files,从宏观上看,它是一种数据内存映射的技术或者说管理动态内存的一种方法,Randy Kath 这样定义到 MMF:Memory-mapped files(MMFs) offer a unique memory management feature that allows applications to access files on disk in the same way they access dynamic memory-through pointers。从微观的角度,它主要具有以下几个特性:

  • 概念:MMF 是一个 Windows 对象,你可以通过 Windows API 创建和访问它。
  • 本质:你可以把 MMF 当成一个普通的文件,只不过它贮存在系统内存中。
    图一:MMF 在各个进程间实现共享(来自 MSDN Online)
    图一:MMF 在各个进程间实现共享(来自 MSDN Online)
    图一:MMF 在各个进程间实现共享(来自 MSDN Online)
  • 特性:MMF 可以被任何进程、线程所访问,这说明 MMF 具有可在进程间共享的特性,这也正是它的最大"魅力"所在。当然,因为所有的存取操作都在内存中进行,它也同时具备快速的特点。
  • 实现原理:MMF 是基于现代操作系统都普遍采用的虚拟内存(virtual memory)技术,而虚拟内存是基于一种被称作 Paging 的机制之上的(2)。所以可以这样认为,只要某个操作系统采用了基于 Page 的虚拟内存管理系统,它就可以实现 MMF 这种功能特性。
  • 生存周期:MMF 一直存在直到对它的最后一个引用被断开。

MMF 其实是 Windows 平台下的一个基本特性,所有关于它的操作都可以通过 Windows API 获得,它使得 DNA 架构下 COM 跨进程访问数据成为可能。利用它,可以将数据库端的业务数据缓存到应用服务器端或者客户端的 MMFs 中,省去频繁访问数据库的开销,极大地提高系统访问性能。对于 Java,我们也在 Jdk1.4 的 NIO 规范下找到了利用 MMF 的类集合,虽然在 Jdk1.4 的 API 文档中并没有明确地提出这样一个概念,但是我们在 FileChannel 和 ByteBuffer 类的文档中了解到 FileChannel 对象具有映射文件至内存的功能,从上面的介绍中我们可以看出这实际上就是创建了 MMF。

背景 -- 我们遇到什么困难?

我们希望对某个购买系统进行升级开发,自然就会涉及到平台的选型。原来的系统是基于微软的 DNA 架构,我们现在倾向于将之移植到 J2EE 平台。在此之前自然要进行必要的可行性分析,除去其他方面的考虑之外,我们最关心的自然落到关键技术的可行性上面,因为我们希望最大限度地利用原有系统的架构设计。

由于该系统基于微软的 DNA 架构,采用 DCOM 远程访问组件的方式,系统性能自然成为一个非常重要的考虑。所以,在原有系统中最大的亮丽之处在于花费大量工作来提高整个系统的性能指标,使得整个系统无论在系统响应速度,还是大数据量并发操作方面都有很杰出的表现。在这其中尤以数据缓存技术 MMF 的应用最为关键,通过服务器端和客户端的数据缓存,有效地提升了整个系统的性能。

图二:应用 MMF 后的系统图
图二:应用 MMF 后的系统图
图二:应用 MMF 后的系统图

图示说明:

  1. 图中的"Server Cache(Business Rules)"部分即为利用 MMF 进行的数据缓存;
  2. 另外,在客户端也大量利用到 MMF,在图中并未标出。

整个系统沿着这样一个思路来利用 MMF:每次系统启动的时候,程序访问数据库,获取表中数据,通过一系列步骤将之缓存至应用服务器端 MMFs,见下图中黑线所示。以后客户端每次请求数据,将直接访问应用服务器端 MMFs,见图中红线所示,并且同时将数据缓存到客户端。此后,如果有任何配置数据的改变,可以重新装载数据到 MMFs。当然,与之配套的还有一套比较合理的定时数据比较机制。

图三、系统与 MMF 的交互图
图三、系统与 MMF 的交互图
图三、系统与 MMF 的交互图

以上这些就是我们所要实现的 MMF 缓存机制,简单地说,我们就是要在 Java 中找出与之对应的缓存机制解决方案。

解决方案 -- 我们想到了什么?

明确了目标之后,我们就开始了在 Java 中寻找的征程。可以说几乎涵盖了现有的所有可行的方案,下面就是我们探索和思考的点点滴滴。

一、 利用 JNI

我们首先想到的就是 JNI(Java Native Interface,Java 本地接口 ),毕竟这是最直观和最省事的解决方案。在 Java 中利用 JNI 直接调用已有的 VC 或者 VB 代码,不需要重新编写这些代码,节省了时间,而且程序执行效率也相当不错。但是,利用 JNI 也存在着诸多的问题:不同程序代码之间的兼容性和可协调性,不易维护性。总之,对于这种夹生饭可以作为一时的权宜之计,在项目时间紧迫的情况下可以考虑使用,但是从长远考虑还是不宜采用。(3)

二、 利用 XML

这其中我们也想到了利用 XML,作为时下非常流行和实用的一门技术,Jdk1.4 中提供了一整套比较完整的 XML API,使得产生以及解析 XML 文件变得非常的容易。但是,个人觉得 XML 最大的优势在于为不同系统间的数据交换提供一种通用的格式,在于数据存储、解析和转换方面,作为数据缓存的候选虽然也未尝不可,但是从最优系统性能和充分继承原有系统架构考虑,还不是最优解决方案。

三、 利用 MMF

因为原有系统是使用的 MMF,所以我们也自然而然想到了 JAVA 中是否也存在 MMF。经过对 Jdk1.4 的仔细研究,我们也如愿找到了我们希望的功能。经过各方面的讨论,我们决定在新系统中采用该技术。

解决方案 -- 我们做了些什么?

在做出决定之后,我们就需要对 Java 中的 MMF 做一个详细的研究。在 Jdk1.4 中,关于 MMF 的 API 主要位于 java.nio 和 java.nio.channels 包下。在新的 JAVA NIO 中着重提到两个概念 Buffer 和 Channel,MMF 其实是作为它们的一个附属品被提出来的。其中的 FileChannel 类的 map 方法能够完成这样一种功能"Maps a region of this channel's file directly into memory",返回一个 MappedByteBuffer 对象。由此我们可见在 Jdk1.4 中,MMF 的表现形式为 MappedByteBuffer 类及其父类 ByteBuffer,你可以通过这些类提供的一些方法来操纵 MMF 对象,而创建 MMF 的功能主要由 FileChannel 类来完成。(4)

在使用类 MappedByteBuffer 之前,你必须弄清楚这样几个概念:capacity, limit, position,这在所有 Buffer 类中都是非常关键的。这里我直接引用 Jdk1.4 文档中的解释:

A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.

A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.

A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.

也许这样一个数学公式更加直观:0 <= position <= limit <= capacity。

在进行大规模的系统应用之前,我们建立个简单的应用模型。今天,我们介绍一下这其中关于 MMF 最简单的一些操作。

1、 创建 MMF

上面我们已经提到,调用 FileChannel 类的 map() 方法可以创建 MMF,详细的方法说明如下:

 abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size)

通过设置不同的 MapMode 类型,可以分别得到只读的、可读写的和私有的 MMF,因此可以视情况而定创建不同的 MMF。同时通过设置参数 position 和 size 可以指定文件的某一部分映射至内存,该特点对于大文件是非常有用的。

 // 清单一:创建不同类型的 MMF 
 try 
 { 
        File file = new File("filename"); 
    
        // 创建一个只读的 memory-mapped file 
        FileChannel roChannel = new RandomAccessFile(file, "r").getChannel(); 
        ByteBuffer roBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0, 
			(int)roChannel.size()); 
    
        // 创建一个可读写的 memory-mapped file 
        FileChannel rwChannel = new RandomAccessFile(file, "rw").getChannel(); 
        ByteBuffer wrBuf = rwChannel.map(FileChannel.MapMode.READ_WRITE, 0, 
			(int)rwChannel.size()); 
    
        // 创建一个私有的 (copy-on-write) memory-mapped file. 
        // Any write to this channel results in a private copy of the data. 
        FileChannel pvChannel = new RandomAccessFile(file, "rw").getChannel(); 
        ByteBuffer pvBuf = roChannel.map(FileChannel.MapMode.PRIVATE, 0, 
			(int)rwChannel.size()); 
 } 
 catch (IOException e) 
 {}

2、 向 MMF 中插入数据

你可以利用类 MappedByteBuffer 的 capacity 来得到它里面包含的字节数,这是个常量。你可以利用方法 put() 来向 MMF 中插入数据,它有两种不同的版本:绝对位置插入 put(int index, byte b),为此你必须指定 index(0<=index<=capacity-1);相对位置插入 put(byte b),它是利用了 position 和 limit 属性。利用相对位置插入数值后,position 也相应地加 1,直至达到 limit 的限制。而且,针对不同的数据类型,有各自相对应的 put 方法,比如 putChar, putDouble 之类。

    // Create an empty MappedByteBuffer with a 10 byte capacity 
    ByteBuffer bbuf = MappedByteBuffer.allocate(10); 
    
    // Get the buffer's capacity 
    int capacity = bbuf.capacity(); // 10 
    
    // Use the absolute put(). 
    // This method does not affect the position. 
    bbuf.put(1,(byte)0xFF); // position=0 
    
    // Set the position 
    bbuf.position(5); 
    
    // Use the relative put() 
    bbuf.put((byte)0xFF); 
    
    // Get the new position 
    int pos = bbuf.position(); // 6 
    
    // Get remaining byte count 
    int rem = bbuf.remaining(); // 4 
    
    // Set the limit 
    bbuf.limit(7); // remaining=1 
    
    // This convenience method sets the position to 0 
    bbuf.rewind(); // remaining=7

3、 从 MMF 中获得数据

与上述的过程相反,你可以通过不同的 get 方法来从 MMF 中获得数据。

    // Create an empty MappedByteBuffer with a 10 byte capacity 
    ByteBuffer bbuf = MappedByteBuffer.allocate(10); 
    
    // Get the MappedByteBuffer's capacity 
    int capacity = bbuf.capacity(); // 10 
    
    // Use the absolute get(). 
    // This method does not affect the position. 
    byte b = bbuf.get(5); // position=0 
    
    // Set the position 
    bbuf.position(5); 
    
    // Use the relative get() 
    b = bbuf.get(); 
    
    // Get the new position 
    int pos = bbuf.position(); // 6 
    
    // Get remaining byte count 
    int rem = bbuf.remaining(); // 4 
    
    // Set the limit 
    bbuf.limit(7); // remaining=1 
    
    // This convenience method sets the position to 0 
    bbuf.rewind(); // remaining=7

解决方案 -- 需要注意的地方

上面我们给出的只是一个非常简单的读写 MMF 的例子,在实际的使用过程中会复杂得多,下面几个因素可能是你要好好考虑的:

1、 数据与 MMF 的对应关系

既然是要将数据缓存到 MMF 中,那我们就必须确立数据库表与 MMF 的对应关系。我们推荐使用的方式是每一张表对应一个 MMF 文件。

2、 MMF 文件长度的设计

确立了对应关系之后,我们需要分析一下如何设定 MMF 文件的初始长度。文件长度不能太小,否则就不能容纳所有的数据,同时文件也不能太长,那样一来浪费系统内存,二来也会使创建 MMF 的开销急剧增大。那刚好能容纳所有的记录呢?听起来是个不错的主意,但是如果这个时候需要添加一条记录呢?麻烦就来啦,由于原有长度不够。系统需要重新 re-map MMF 文件,造成系统内频繁地创建 MMF,反而使性能下降。经过我们研究后得出,这个比例在 1.1-1.3 之间比较合适,也就是 MMF 文件略大于表中现有记录的总和。

3、 针对不同性质的数据进行不同的处理

明确以上两点,我们还需要对数据本身做一番研究。有些数据趋于固化,一般不会有什么改变,比如国家、省份等,而有些数据则会经常变化,比如产品等,对于这两种不同类型的数据,你可以采取不同的处理方式,以达到最优的系统性能。

可能存在的问题 -- 我们需要预防些什么?

1、 MMF 不是万能灵药

千万不要以为有了 MMF,你就可以高枕无忧,可以轻轻松松搞定系统的缓存机制。事实远非如此,MMF 只不过是一把利刃,更重要的是你自己要仔细认真地设计好系统的缓存机制。要知道,解决交通堵塞问题的关键不是把路修得多么宽,而是要合理地规划整个交通路线。要知道在某些操作系统中,使用 MMF 的代价是非常昂贵的,失去好的规划,你可能会适得其反,系统反而会更加的拥挤不堪。况且,使用 MMF 还会带来很多的副作用。

2、 性能与数据差错容忍度之间的平衡

我们知道,随着数据缓存的大量使用,不可避免地会产生某种程度上的数据不一致,也就可能会产生某些数据差错。所以说,数据缓存使用的力度决定于系统客户对这些错误的容忍程度有多大。在某些非常关键的业务数据应用数据缓存技术时,必须格外地小心。

3、 需要额外的 MMF 支持代码

如上所述,为了最大限度地减少数据的不统一,我们必须提供一套非常合理和有效的数据同步机制,某种程度上甚至可以认为数据同步机制的好坏决定了数据缓存技术的成败。而这些是我们在使用 MMF 的过程中需要额外提供的代码。

4、 MMF 与平台的相关性

现在大部分编程语言中使用 MMF 的方法都是,提供相应的接口创建和操作 MMF 或者系统 API,而底层的具体 MMF 细节则由相应的操作系统去决定。这样每种操作系统中 MMF 不同的实现细节也在某种程度上影响着我们对 MMF 的使用。

5、 使用 MMF 必须十分的小心

既然 MMF 是贮存在系统内存中,所以对于某些错误必须时刻警惕,比如"Array Out of Bound"等。要是您的系统没有很好地捕获这些错误,您的系统可能会彻底崩溃。每当你编写这些 MMF 代码的时候,你必须时刻牢记在心:我是在与系统内存打交道,这家伙可是娇贵的很。

6、 由于 Jdkl1.4 的推出时间不长,基于 MMF 的现有应用几乎没有,所以没有真正能够在现实环境中检验 MMF 的使用情况,可能会存在一些不可预知的风险。

总结

通过以上的介绍,相信大家对 MMF 在 Java 中的应用都有了一个初步的印象。实际上,提高应用系统的性能一直是所有应用系统开发人员追求的目标。除去本文谈到的缓存技术之外,在 J2EE 中,你还可以通过各种池技术的应用,EJB 组件的优化来提高系统性能(5)。但愿,本文能够给你带来这方面的一些启示。


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=52835
ArticleTitle=如何提高系统性能指标
publish-date=12272002