简介
Infinispan 是 JBoss Cache 缓存框架的后续项目,它是一个开源的数据网格平台,用于访问分布式状态的群集节点。 GridFileSystem (网格文件系统) 是一个全新的实验性的 API,这些 API 使 Infinispan 后端的网格数据像“文件系统”一样展示出来。这一系列 API 继承了 JDK 的 File , InputStream 和 OutputStream 类,创建了相应的: GridFile , GridInputStream 和 GridOutputStream 类。还有一个帮助类 GridFilesystem ,也被包含在这个框架里面。这些 API 在 Infinispan 4.1.0 版本中就可以使用了 (从 4.1.0.ALPHA2 版起)。
GridFilesystem 包含两个 Infinispan 缓存器:一个用于元数据缓存 (通常是完全复制),另外一个是用于实际数据的缓存 (通常是分布式)。前一个复制缓存器使每个节点在本地都有元数据信息,像列出文件列表之类的任务就不必使用 RPC 远程过程调用了。后一个是分布式缓存器,当存储空间的容量用光的时候,就需要一种可扩展的机制来存储这些数据。所有的文件都被分块,每个块都存储为一个缓存项。
在这篇文章里面我们关注的特性是 Infinispan 的分布式模式。该模式增加了“分布式”特性,这是一种基于哈希一致性的技术。JBossCache 框架只支持“复制”模式 (就是在群集里面的每一个节点都向其它节点复制所有的数据)。
完全复制技术可以很好的用于小型群集,或者是在每个节点的存储数据量都相对较小的情况。在群集中,当每个节点都向其它节点复制数据的时候,每个节点的平均数据存储容量都与这个群集的大小以及数据的容量有关。这种复制的优点在于它通常只在本地节点读取数据,因为每个节点都拥有这些数据;另外,当群集中有新节点加入或者需要移除现存节点的时候,它也不需要重新进行负载均衡。
另一方面,当你需要快速访问大型数据集合,并且又无法忍受从磁盘(譬如:数据库)中检索数据时, 内存网格文件系统将是一种更好的解决方案。
在之前的文章中,我们讨论了ReplCache,它使用了基于哈希一致性的分布式技术实现了一个网格数据容器。从某种程度上说,ReplCache 就是Infinispan 分布模式的原型。
在Infinispan 中,不管有没有冗余备份,数据都可以存储在网格中。举个例子,只有将Infinispan 的配置项设置为 distributed cache mode (分布缓存模式), numOwners (所有者数量)设置为 1,数据 D 才会被存储到网格中。在这种情况下,基于哈希一致性算法,Infinispan 只会选择一台服务器节点来存储数据 D。如果我们设置 numOwners 为 2,那么 Infinispan 就会选择两台服务器来存储数据 D,以此类推。
Infinispan 的优势在于它提供了聚合的网格内存。例如,假设我们有 5 台主机,每台主机都有 1GB 的内存,然后我们将参数 numOwners 设置为 1,那么我们就总共有 5GB 内存容量 - 这显然降低了开支费用。即便使用冗余备份 - 比如,设置 numOwners 为 2- 我们的配置也有 2.5GB 的内存容量。
然而有一个问题:如果我们有很多 1K 大小的数据项,却只有少量的 200MB 大小的数据项,这将造成了数据分布不均衡。一些服务器因为存储那些 200MB 的数据项,差不多把内存堆消耗殆尽,而另外的服务器的内存有可能还没有使用。
还有一个问题是,如果当前数据项的大小超过了所给的单个服务器的可用内存堆:譬如,当我们试图存储 2GB 的数据项时,操作就会失败,因为数据项大于服务器节点的 1GB 内存堆,调用 Infinispan 的方法 Cache.put(K,V),就会引起 OutOfMemoryException(内存溢出)错误。
要解决这些问题,我们要将一个数据项分成 chunks(区块)并将它们分散存储到群集的节点中。我们将一个区块的大小设置为 8K:如果我们把 2GB 的数据项分为 8K 大小的区块,最终将在网格中得到 250,000 个 8K 的区块。在网格中存储 250,000 个相等的区块,当然比存储少量的 200MB 的数据项要更加均衡。
当然,我们不会加重应用程序员的负担,他们既不用在写入的时候把数据项切分成区块,也不用在读取的时候把区块合并恢复为数据项。但是,这种方法仍需要将整个完整的数据项保存在内存中,这是不可行的。
为了克服这点,我们使用了流:一个流(输入流或输出流)只处理单个项的数据子集。例如,与其将 2GB 的数据项写入网格,不如对输入文件迭代读取(譬如每次读取 50K),再将其每次读取的少量数据写入到网格中。这样,不管什么时候,只需要占用 50K 内存容量就够了。
现在应用程序员可以编写代码(伪代码)将 2GB 的数据项存入网格中,如下面的列表 1 所示:
列表 1:存储数据项到网格中
InputStream in=new FileInputStream("/home/bela/bond.iso"); OutputStream out=getGridOutputStream("/home/bela/bond.iso"); byte[] buffer=new byte[50000]; int len; while((len=in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, len); } in.close(); out.close();
我们也可以便携类似的代码用来读取网格中的数据。
使用流接口的好处是:
- 它只需要使用一个很小的缓冲区来读写数据。这要优于给 Infinispan 的 put()/get() 方法创建一个 2GB 的 byte[] 缓冲区。
- 通过网格分布存储数据将使群集更加均衡。使用良好的哈希一致性功能,所有的服务器节点存储的数据容量都是差不多大小。
- 我们能够存储大型文件,即使文件容量大于单个节点的 JVM 内存堆。
- 应用程序员无需编写区块操作的相关代码。
架构
这个网格文件系统不仅需要存储区块,还要存储元数据信息,像目录、文件名、文件大小、最后修改时间等。
因为元数据很关键,并且也很小,我们决定把它存储在网格的所有节点中。因此,我们需要一个 Infinispan 缓存器来保存元数据,我们不用分布模式而用复制模式(在每个节点都完全拷贝元数据);我们还需要一个 Infinispan 缓存配置项,指定分布模式下的数据区块所期望的 numOwners 参数值。
需要注意的是,Infinispan 并不会创建两个 JGroups 堆和通道,而是让同一个 CacheManager(缓存管理器)创建的元数据缓存器和区块缓存器实例共享单独的 JGroups 通道。
元数据缓存器
元数据缓存器将目录和文件的路径名称保存为键,将元数据保存为值(如文件长度、最后修改时间、标记该条目是文件还是目录等)。
不管是创建一个目录或文件,删除一个文件,或者是写入一个文件,元数据缓存器都要更新。例如,当写入一个文件的时候,这个文件的长度和最后修改时间都要在元数据缓存器里面更新。
元数据缓存器也被用于导航,比如列出目录“/home/bela/grid”下面的所有子目录。因为每次修改的元数据都被完全复制到每个服务器节点中,所以读取元数据通常只要在一个节点本地进行,而不需要网络通讯。
区块缓存器
区块缓存器中会保存一个个单独的数据块。里面的键存储的是区块名称,值存储的是 byte[] 缓冲值。构造区块名称的时候会在全路径后面跟上“.#
每个区块大小为 4000 bytes,当写入一个 14K 的文件“/home/bela/tmp.txt”,将产生如下区块:
- /home/bela/tmp.txt.#0 (4000 bytes) [0 - 3999]
- /home/bela/tmp.txt.#1 (4000 bytes) [4000 - 7999]
- /home/bela/tmp.txt.#2 (4000 bytes) [8000 - 11999]
- /home/bela/tmp.txt.#3 (2000 bytes) [12000 - 13999]
当前区块的计算与当前读写的指针以及区块大小相关。例如,需要在 7900 位置读取 1000 bytes 数据时,将从区块#1 读取 99bytes,再从区块#2 读取 901 bytes。
区块名称 (“/home/bela/tmp.txt.#2”) 会根据哈希一致性技术,来选择并定位读写的服务器节点。
API
GridFileSystem 框架由 4 个类实现:
- org.infinispan.io.GridFilesystem
- org.infinispan.io.GridFile (继承自 java.io.File )
- org.infinispan.io.GridOutputStream (继承自 java.io.OutputStream )
- org.infinispan.io.GridInputStream (继承自 java.io.InputStream )
Infinispan 系统的入口点是 GridFilesystem 类:它用于实例化 GridFile, GridOutputStream 和 GridInputStream 类。
列表 2:GridFileSystem 类的主要方法
public GridFilesystem(Cache<String, byte[]> data, Cache<String, GridFile.Metadata> metadata, int default_chunk_size) { ... } public File getFile(String pathname) { ... } public OutputStream getOutput(String pathname) throws IOException { ... } public InputStream getInput(String pathname) throws FileNotFoundException { ... }
GridFilesystem 的构造器需要传入两个 Infinispan 缓存器,第一个参数为数据区块缓存器(指完全创建并运行的构造),第二个参数为元数据缓存器。上面的第三个参数名称 default_chunk_size 用于设置默认区块大小值。
列表 3:创建 GridFileSystem 对象的代码
Cache<String,byte[]> data; Cache<String,GridFile.Metadata> metadata; GridFilesystem fs; data = cacheManager.getCache("distributed"); metadata = cacheManager.getCache("replicated"); data.start(); metadata.start(); fs = new GridFilesystem(data, metadata, 8000);
方法 getFile() 会获取一个文件的句柄,从而列出这个文件下面的文件列表,新建文件或新建目录。getFile() 方法返回的是 GridFile 类,GridFile 类继承了 java.io.File 类,并重写了 File 类的大部分方法(但不是全部方法)。列表 4 的代码片段演示了如何使用 getFile() 方法。
列表 4. 使用 GridFileSystem 类中 getFile() 方法的例子
// create directories File file=fs.getFile("/home/bela/grid/config"); fs.mkdirs(); // creates directories /home/bela/grid/config // List all files and directories under "/usr/local" file=fs.getFile("/usr/local"); File[] files=file.listFiles(); // Create a new file file=fs.getFile("/home/bela/grid/tmp.txt"); file.createNewFile();
方法 getOutput() 会返回 GridOutputStream 实例,它可以把数据写入网格中。下面列表 5 的代码,演示了如何从本地文件系统拷贝数据到网格系统中(省略了异常处理代码):
列表 5:使用 GridFileSystem 类中 getOutput() 方法的例子
InputStream in=new FileInputStream("/home/bela/bond.iso"); OutputStream out=fs.getOutput("/home/bela/bond.iso"); // same name in the grid byte[] buffer=new byte[20000]; int len; while((len=in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, len); } in.close(); out.close();
getInput() 方法创建了一个 GridInputStream 实例,它可以从网格系统中读取数据。下面的列表 6 演示了 getInput() 方法调用的例子。
列表 6:使用 GridFileSystem 类中 getInput() 方法的例子
InputStream in=in.getInput("/home/bela/bond.iso"); OutputStream out=new FileOutputStream("/home/bela/bond.iso"); // local name same as grid byte[] buffer=new byte[20000]; int len; while((len=in.read(buffer, 0, buffer.length)) != -1) { out.write(buffer, 0, len); } in.close(); out.close();
用 WebDav 展示网格文件系统
从 4.1.0.ALPHA2 版本起,Infinispan 将附带集成了 WebDAV 的演示包。这个演示包在网站的下载页面提供,从最新发布的Infinispan 分发包里面,我们可以找到已经编译好的完整的WAR 文件。将这个WAR 文件部署到你最喜欢的servlet 容器中,你就可以用WebDAV 协议把文件系统加载上去。地址是 http://YOUR_HOST/infinispan-gridfs-webdav/
这里还有另外一个视频演示,两个JBoss AS(JBoss 应用服务器)实例启动并运行infinispan-gridfs-webdav 的web 示例应用程序,WebDAV 实例作为远程驱动加载。文件已经拷贝到infinispan 系统中,当一个实例停止的时候,我们还可以从另外一个实例中继续读取文件,这也是高可用性的样例。
可监测性
网格文件系统提供的可监测性支持,它通过Infinispan 框架的基础实现,包括 JMX 和 / 或 JOPR 或 JON 。
JMX
JMX 的监测报告可以在两个不同级别上启用:缓存管理器和缓存器。缓存管理器级别管理了它创建的所有缓存器实例。缓存器级别的管理包含了每个缓存器实例生成的信息。
JOPR
另一种管理多个 Infinispan 实例的方法是使用 JOPR,它是 JBoss 的企业管理解决方案。JOPR 的代理和自动搜索功能可以同时监测缓存管理器和缓存器实例。使用 JOPR,管理员可以用可视化的方式访问一些重要的运行时参数或统计数据,同时也可以得知这些参数和统计数据是否超标或过低。
总结
本文讨论了在网格中如何通过 Infinispan 的一套新的流 API,来使用网格文件系统存储大容量的文件。另外还有一些新的功能,用于将数据区块化存储到网格中,使用不同级别的冗余,以及在内存中处理超大型的数据集。
网格文件系统是一个原型,它并没有实现所有的 java.io.* 包里面的功能,但是它当前已经包含了大部分重要的方法。欲了解更多信息,请参阅相关的 Infinispan 的 API 文档。
参考
网格文件系统: http://community.jboss.org/wiki/GridFileSystem
ReplCache 文档: http://www.jgroups.org/javagroupsnew/docs/replcache.html
Infinspan 主页: http://www.infinispan.org
启用分布模式: http://docs.jboss.org/infinispan/4.0/apidocs/config.html#ce_default_clustering
关于作者
Bela Ban 在瑞士的苏黎世大学完成了他的哲学博士学位。在 IBM 研究中心待了一段时间后,他到康奈尔大学研读博士后。然后,他在加利福尼亚州圣何塞市的 Fujitsu Network Communications 公司从事 NMS/EMS 工作。2003 年,他全职加入了 JBoss 的开源工作。Bela 在 JBoss 公司管理 Clustering 团队并负责 JGroups 项目。Bela 的兴趣包括网络协议,性能,群组通讯,越野跑,骑自行车和 Beerathlon(注:一项有关啤酒的游戏)。当不写代码的时候,他喜欢和家人共度时光。
Manik Surtani 是 Infinispan caching 项目的负责人。
查看英文原文: Infinispan’s GridFileSystem - An In-Memory Grid File System
注:缓存器(Cache)指分布式节点的内存缓存器,类似与组件。缓冲(Buffer)一般指缓冲区 byte[] 等,一般指内存区域。
感谢侯伯薇对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论