在 AIX 上通过 pollset 接口实现高效的 I/O

本文解释如何使用 pollset 接口提高在 AIX® 平台上运行的包含大量异步 I/O 操作的 Java™ 应用程序的性能。这会消除用户和内核空间之间重复的文件描述符复制操作,通过 poll 缓存减少 poll 时间。

Liang Jiang, 资深软件工程师, IBM

Liang Jiang 于 2000 年加入 IBM,最初属于 AIX 后端技术支持团队。后来,他转到 AIX 基本内核开发团队,从事在 POWER 系统上支持 AIX 以及 AIX 内核中的其他低层组件。



Moriyoshi Ohara, 资深研究员, IBM

Moriyoshi Ohara 是 IBM Tokyo Research Laboratory 的资深研究员。他于 1996 年从 Stanford University 获得了电子工程博士学位。他当前的研究方向包括微处理器体系结构和商用服务器的工作负载特征。



Sathiskumar Palaniappan, 系统软件工程师, IBM

Sathiskumar Palaniappan 是 Bangalore 的 IBM India Labs 的软件工程师。他于 2007 年加入 IBM Java Technology Center,从事 net 和 nio 库开发。他曾经参与 WebSphere Real Time 的功能性测试,喜欢研究运行时技术。



2009 年 9 月 02 日

简介

JDK 1.4 引入的 new input/output (NIO) 库为标准 Java 程序提供高速、非阻塞的异步 I/O 功能。异步 I/O 让应用程序能够在不阻塞的情况下读写数据。一般情况下,当应用程序调用 read () 时,代码会阻塞,直到有数据可读。同样,write () 调用会阻塞,直到可以写数据。

另一方面,异步 I/O 不会阻塞。应用程序注册 I/O 事件(可读数据的到达、新的套接字连接等等),当出现这种事件时系统会发出通知。

异步 I/O 的优点之一是,允许应用程序同时处理来自许多输入和输出的 I/O 操作。它还让应用程序能够在执行 I/O 的同时利用空闲的 CPU 时间执行其他处理。本文先讨论传统 poll 机制的开销,说明 pollset 接口如何改进性能。然后,展示在一个 JEE5 应用服务器上测量到的性能改进。


忙 - 等待 (Busy-wait)polling:传统的异步 I/O 方式

在处理多个文件描述符时,应用程序通常把每个文件描述符设置为非阻塞(见 清单 1),每次对一个文件描述符执行 read 调用。

清单 1. 如何把 I/O 通道配置为非阻塞
 DatagramChannel channel1 = DatagramChannel.open(); 

 channel1.configureBlocking(false);

如果有数据,就读取并处理数据。如果没有数据可读,read 调用立即返回。然后,对另一个文件描述符重复这个过程。在等待一段时间之后,重新开始,重复读取每个文件描述符。这种方法称为忙 - 等待 poll。


poll 方法:传统的 I/O 多路复用方式

由于以下两个问题,忙 - 等待 polling 方法会对效率造成严重影响:

  • 当给定的文件描述符上没有数据可读时,它会反复执行 read () 系统调用,这会浪费大量 CPU 周期。
  • 当一个文件描述符上数据准备好时,它无法立即响应。

在多任务系统上尤其应该避免这种忙 - 等待 polling 方法。

为了解决这些问题,UNIX® System SVR3 上引入了一个 poll() API,它已经成为 POSIX 标准的一部分。简单地说,应用程序向内核提供一个文件描述符列表和一个超时值,它需要监视这些文件描述符的读 / 写 / 错误状态。内核向相关设备的选择函数注册这个进程 / 线程,并让这个进程 / 线程睡眠。当相关设备准备好或计时器过期时,内核唤醒注册的进程 / 线程。这种方法会显著降低 I/O 开销;它消除了内核和用户空间之间的大量系统调用和数据复制。另外,应用程序可以立即响应 I/O 事件。

Java NIO 库引入了一个 “选择器” 类,用于从 Java 应用程序支持这个 API。任何 Java 应用程序都可以通过 open() 调用打开一个选择器以获取相关联的数据结构,见 清单 2

清单 2. 如何获取选择器对象
 Selector selector = Selector.open();

然后,应用程序用 register () 调用向选择器注册通道(文件描述符)和感兴趣的操作。例如,如果应用程序希望知道某一通道什么时候准备好读取,可以向选择器注册这个文件描述符并指定读操作,见 清单 3

清单 3. 向选择器注册通道
 channel.register(selector, SelectionKey.OP_READ);
图 1. 传统的 poll() 方式
poll 方式

图 1 所示,选择器在内部保存这些详细信息,直到应用程序在选择器上调用 select() 方法,见 清单 4。然后,选择器把通道和感兴趣的操作复制到内核空间,让内核为这个应用程序执行实际的 poll。

清单 4. 如何启动 poll
 channel.register(selector, SelectionKey.OP_READ);

select() 调用返回一个文件描述符列表,对于每个文件描述符至少注册了一个事件。然后,应用程序只能对这些文件描述符执行 I/O 操作。这种方法会消除内核和用户空间之间的大量系统调用和数据复制,从而显著降低开销。

选择器在内部调用一个本机 poll() 函数(见 清单 5),这个函数支持在一组文件描述符上多路复用输入和输出:

清单 5. poll() API 的签名
 int poll(struct pollfd fds[], nfds_t nfds, int timeout);

pollset 方法:AIX 解决 poll () /select () 可伸缩性问题的方法

但是,传统的 poll 方法有可伸缩性问题;它不太适合处理大量文件描述符。根本问题是,随着文件描述符数量的增加,每个 poll 操作所需的工作量会线性增加。为了提高可伸缩性,已经提出了许多新的 API,比如 /dev/poll、实时信号、I/O 完成端口、/dev/epoll 和内核队列。对于哪个 API 是最好的长期解决方案,有许多争论(见 [POLLCMP])。

poll() 的哪些方面影响可伸缩性?

  • 每个 poll() 调用都提供要 poll 的文件描述符列表。对于每个调用,都要把这个列表复制到内核空间中。图 1 中红色的事件表示这些重复的复制。
  • poll 一个对象需要两步:首先在文件描述符上建立一个持有计数,然后调用与这个文件描述符相关联的操作。
  • 异步和同步 poll 之间的主要路径长度差异是,分配和最终清除控制块。
  • 作为 poll 操作的最后一步,要清除所有控制块。必须从与块相关联的对象中删除每个控制块。这要求 poll 方法锁住对象。

在监视大量文件描述符时,如果在循环中调用 poll(),poll 所涉及的这些高成本的系统调用会严重影响总体性能。

为了让 poll() 能够处理大量文件描述符,AIX pollset 接口提供了两个优化措施。第一个措施减少每个 poll 操作在内核和用户空间之间传输的信息量,见 图 2。pollset 接口在本机(内核)pollset 层中创建并维护一个文件描述符集和感兴趣的事件。应用程序直接向本机 pollset 层注册文件描述符和感兴趣的事件。与 poll() 不同,pollset 接口并不要求选择器在每次调用 select() 时复制整个文件描述符集。相反,它只复制在前一次 select() 调用之后新注册的事件。

图 2. Pollset() 方法
pollset 方法

第二个优化措施是在内核中使用 poll 缓存机制。它跨系统调用在请求的文件描述符集上维护文件描述符状态。在每个 poll 操作的开头,通过 poll 繁忙的文件描述符跟踪状态。空闲文件描述符的状态是已知的,因为在状态改变时会通知 poll 缓存服务。

图 3. poll 缓存内部结构
poll 缓存内部结构

图 3 显示 poll 缓存中的组件及其关系。poll 缓存管理一个可能很大的文件描述符集。其中的每个文件描述符由一个 poll 缓存控制块(pccb)表示。每个 pccb 根据文件描述符散列值放在 poll 缓存中。维护一个未处理列表,它标识最近有状态转换的 pccb。支持选择和 poll 的每个子系统都向 poll 缓存注册。当文件描述符的状态发生变化时,子系统通知 poll 缓存,这会在 poll 缓存中触发状态转换。传统的 poll()/select() 需要检查选择的所有文件描述符,为了解决由此产生的可伸缩性问题,poll 缓存通过状态转换只把 ‘繁忙的’ pccb 转移到事件列表中。这样,poll 操作就不需要访问 pollset 中的所有 pccb。只为已经添加到事件列表中的控制块提供服务。如果繁忙的文件描述符数量接近选择的文件描述符总数,而且文件描述符数量非常大,就会出现最糟糕的情况。在这种情况下,pollset 方法与传统的选择 /poll 方法相比并不能显著提高性能。

IBM® JDK 从 6.0 Service Refresh 5 开始支持 pollset 接口。应用程序不需要为了启用 pollset 接口进行修改。如果操作系统支持 pollset 接口,Java.nio.SelectorProvider 方法在默认情况下会打开 pollset 选择器。NIO 的 pollset 选择器使用下面的本机 pollset API(见 清单 6)提高应用程序性能。

清单 6. NIO 库使用的本机 pollset 接口
 pollset_t ps = pollset_create(int maxfd); 

 int rc = pollset_destroy(pollset_t ps); 

 int rc = pollset_ctl(pollset_t ps, struct poll_ctl *pollctl_array, int array_length); 

 int nfound = pollset_poll(pollset_t ps, 	 struct pollfd *polldata_array, 
						 int array_length, int timeout);

正如前面提到的,当应用程序打开 poll 选择器时,poll 选择器会创建一个本机 pollset 结构。当应用程序注册通道时,选择器在本机 pollset 结构中注册文件描述符和感兴趣的事件。这意味着,对于每个注册,选择器调用必须执行两个模式切换。第一个切换是从 Java API 层到 Java native Interface (JNI) 层。第二个切换是从 JNI 层到内核空间。如果应用程序注册大量通道,这些切换会影响性能。

图 4. Pollset() - 批量更新
pollset - 批量更新

为了避免过多的模式切换,选择器方法在内部维护一个数据结构(见 图 4),在其中临时存储注册的文件描述符,直到它们的数量达到 java.nio.pollset.RegistrationPerCall(默认值为 50)。注意,当应用程序在选择器上调用 select() 时,即使注册数量没有达到 java.nio.pollset.RegistrationPerCall,选择器也会在本机 pollset 层中注册所有文件描述符。应用程序可以在启动时通过设置正整数值调整 registrationPerCall 数量,见 清单 7

清单 7. 如何设置每次注册的文件描述符数量
 java -Djava.nio.pollset.RegistrationPerCall=30 "class name"

实验环境:Pet Store 2.0

我们使用 Java Pet Store 2.0(见 [PETSTORE])评估性能改进。这是 Sun 公司提供的示例应用程序。它支持虚拟宠物商店中的网上购物场景,比如列出出售的宠物、发贴出售自己的宠物、通过 PayPal 购买宠物和搜索某一地理区域的宠物。这个应用程序演示 JEE5 (Java Platform Enterprise Edition 5) 技术,尤其是与 Web 2.0 特性相关的技术,比如 Ajax (Asynchronous JavaScript and XML),提供响应性的用户界面并通过 Web 2.0 mashup 组合来自多个源的信息。mashup 场景之一使用非常流行的 Google Maps 地图服务在浏览器中显示的地图上标出每个宠物的位置。

我们的实验集中于一个 Ajax 场景,这个场景允许客户机浏览器与服务器异步地通信。这种技术通常在浏览器中显示和更新一个小的弹出窗口,这比需要下载和更新整个网页的传统网站快得多,可以提供响应性的用户界面。因此,尽管应用程序的主要代码在服务器上运行,但是 Ajax 技术可以让用户觉得应用程序就像是在客户机浏览器中运行一样。

在 Pet Store 2.0 的这个场景中,当用户把鼠标光标放在浏览器中一个宠物条目上时,在浏览器中运行的 JavaScript 代码向服务器发送一个 Ajax 请求,请求获取关于这个条目的信息。服务器获取这个条目的数据库记录并以 XML (Extensible Markup Language) 格式把记录返回给客户机浏览器。客户机浏览器显示一个小的弹出窗口,在其中显示收到的信息。

因为每个 Ajax 请求都需要在服务器上执行一个非常简单的数据库查询事务,服务器会在操作系统方面花费相当多的 CPU 周期(见 [ISPASS])。这个特点促使我们使用 pollset API 减少系统时间,从而增加吞吐量性能。Ajax 请求的吞吐量性能是一个重要的指标,因为在服务器负载重时它会直接影响用户体验。

图 5. Petstore 环境
Petstore - 实验环境

图 5 说明我们的实验环境,它包含三层:模拟的客户机、应用服务器和后端数据库服务器。对于模拟客户机,我们使用 8 台基于 Linux® 的刀片服务器,它们运行一个基于开放源码 Grinder 工具的客户机模拟程序(见 [GRIDER])。对于我们的 Ajax 实验,模拟总共 1280 个客户机,每个客户机在循环中随机选择一个宠物条目并向应用服务器发送 Ajax 请求,请求获取关于这个宠物的信息。对于应用服务器,使用在 IBM BladeCenter® JS22 服务器上运行的 Glassfish 应用服务器(见 [GLASSFISH]),这台服务器使用 4GHz 的 4 核 POWER6™ 处理器。另外,使用在 IBM BladeCenter HS21 上运行的 MySQL 数据库,这台机器使用 8 核 Intel Xeon E5320 处理器。


实验结果

我们通过在一个 Java 驱动程序中使用 pollset API 来评估性能改进。在实验中,我们主要关注前面介绍的 Ajax 请求,因为这个场景代表 Web 2.0 应用程序中常见的客户机 - 服务器交互模式。图 6 显示对于两个驱动程序测得的吞吐量性能结果:一个使用 poll(),另一个使用 pollset()。结果表明,与使用 poll API 的老驱动程序相比,使用 pollset API 的驱动程序的吞吐量性能提高了 13.3%。

图 6. 两个驱动程序的吞吐量性能,一个使用 poll(),另一个使用 pollset()
Petstore - 性能对比

我们还使用 curt 命令进一步分析了系统时间。这个命令是 AIX 跟踪工具的一部分(见 [AIX TOOL]),我们通过它了解 pollset API 可以减少多少系统时间。图 7 显示在修改 java.nio.pollset.RegistrationPerCall 的值时,每毫秒两个系统调用(pollset_ctl() 和 pollset_poll())的数量。随着 java.nio.pollset.RegistrationPerCall 值的增加,pollset_ctl() 的 CPU 时间减少,这是因为每个 pollset_ctl() 处理的套接字数量增加了。

图 7. 每毫秒系统调用数量
每毫秒系统调用数量

图 8 显示 poll()、pollset_ctl() 和 pollset_poll() API 的 CPU 时间比例。原来的驱动程序在调用 poll() 方面花费 5.3% 的 CPU 时间(见 图 8 中最左边的条),新的驱动程序在调用 pollset_ctl() 和 pollset_poll() 方面只花费 3% 的 CPU 时间。因此我们的实验结果表明,pollset API 有助于减少系统时间,而且我们的实现对于 java.nio.pollset.RegistrationPerCall 参数不敏感,修改这个参数对系统时间和吞吐量性能影响不大。

图 8. CPU 时间花费
每个调用的 CPU 时间

结束语

本文通过一个宠物商店应用程序展示了使用 pollset 接口对性能的益处。另外,pollset 接口只查询繁忙的文件描述符,这减少了内核和用户空间之间传输的数据量。在文件描述符集不需要频繁更新的情况下,最适合使用 pollset 接口。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=424967
ArticleTitle=在 AIX 上通过 pollset 接口实现高效的 I/O
publish-date=09022009