为性能而设计, 第三部分: 远程接口
学习怎样在设计 java 类的时候避免性能冒险.
概述
许多 Java 的通常性能问题来源于设计过程早期的类设计想法中, 早在开发者开始考虑
性能问题之前. 在这个系列中, Brian Goetz 讨论了一些通常的 Java 性能的冒险, 解
释了怎样在设计时间避免它们. 在这篇文章中, 它检验了远程应用程序中的特定的性能
问题.
By Brian Goetz
翻译 by SuperMMX
这个系列探索一些早期的设计思想对应用程序的性能产生影响的方法. 第一部分, 检查了
一个类的对象创建行为是如何嵌入它的接口中的. 特定的接口实际上要求一个类创建临时
对象, 或者需要它的调用者来创建临时对象, 才能使用这个类. 因为临时对象的创建对 Java
程序来说是一个性能指标, 当你在设计时候, 能再对你的类接口做一些回顾来检查性能冒?
这是值得的.
在第一和第二部分我集中于对象的创建, 因为对许多 Java 程序来说这是一个很大的性能侍?
但是, 在分布式的应用程序中, 象建立在 RMI, CORBA, 或者 COM 之上的程序, 一个完全煌
的性能问题就在眼前了. 这篇文章探索一些针对远程程序的性能问题, 演示你怎样才能通
单地检查一个类的接口来猜测到分布式应用程序中的性能问题.
阅读这个 "为性能而设计" 系列:
第一部分: 接口事宜
第二部分: 减少对象创建
第三部分: 远程接口
远程调用的概观
在分布式的应用程序中, 一个运行在一个系统中的对象可以调用另一个系统中的一个对象
的方法. 这个通过很多使远程对象表现为本地的结构的帮助而实现. 要访问一个远程对象,
你首先要找到它, 可以通过使用目录或者命名服务来实现, 象 RMI 注册, JNDI, 或者 CORBA
命名服务.
当你通过目录服务得到一个远程对象的引用时, 你并没有得到那个对象的实际的引用, 而
是一个实现了和远程对象同样接口的stub对象的引用. 当你调用一个stub对象的方法时, 飧
对象把方法的所有参数汇集起来 -- 把它们转化成一个字节流的表现形式, 类似于序列化
过程. 这个stub对象把汇集的参数通过网络传递给一个skeleton对象, 把参数分解出来, 饔
你想调用的实际的对象的方法. 然后这个方法向skeleton对象返回一个值, skeleton对象逊祷刂祷慵
起来, 把它传送给stub对象, stub对象把它分解出来, 传递给调用者. Phew! 一个单独的椒
调用要做这么多的工作. 很明显, 除去表面的相似性, 一个远程方法调用比本地方法调用
更大.
以上描述浏览了一些对于程序性能非常重要的细节. 当一个远程方法返回的不是一个原类?
而是一个对象时, 会发生什么? 不一定. 假如返回的对象是一种支持远程方法调用的类型,
它就创建一个中stub对象和一个skeleton对象, 在这种情况下需要在注册表中查找一个远潭韵?
这显然是一个高代价的操作. (远程对象支持一种分布式的垃圾回收的形式, 包括了每一个
参与的 JVM 维护一个线程来和其他 JVM 的维护线程进行通讯, 往返传递引用信息). 假如
返回的对象不支持远程调用, 这个对象所有的域和引用的对象都要汇集起来, 这也是一个
代价的操作.
远程和本地方法调用的性能比较
远程对象访问的性能特征和本地的不一样:
远程对象的创建比本地对象创建代价要高. 不仅仅是当它不存在时要创建它, 而且stub对
和skeleton对象也要创建, 还要互相感知.
远程方法调用还包括网络的传递 -- 汇集起来的参数必须发送到远程系统, 而且响应也需
汇集起来, 在调用程序重新得到控制权之前发送回来. 汇集, 分解, 网络延时, 实际的远
调用所导致的延迟都加在一起; 客户端通常是等待所有这些而步骤完成. 一个远程调用也
大地依靠于底层网络的延时.
不同的数据类型有不同的汇集开支. 汇集原类型相对来说花费少一些; 汇集简单的对象,
Point 或者 String 要多一些; 汇集远程对象要多得多, 而汇集那些引用非常多的对象的
对象(象 collection 等)要更多. 这和本地调用完全矛盾, 因为传递一个简单对象的引用槐
一个复杂对象的引用花费多.
接口设计是要害
设计不好的远程接口可能完全消除一个程序的性能. 不幸的是, 对本地对象来说好的接口
的特性对远程对象可能不适合. 大量的临时对象创建, 就象在本系列的第一, 二部分讨论?
也能阻碍分布式的应用程序, 但是大量的传递更是一个性能问题. 所以, 调用一个在一个
时对象(比如一个 Point)中返回多个值的方法比多次调用来分别得到它们可能更有效. (注
意, 这和在第二部分给本地对象岢龅慕ㄒ?I 完全相反.)
实际远程应用程序的一些重要的性能指导:
提防不必要的数据传递. 假如一个对象要同时得到几个相关的项, 假如可能的话, 在一个
远程调用中实现可能轻易一些.
当调用者可能不必要保持一个远程对象的引用时, 提防返回远程的对象.
当远程对象不需要一个对象的拷贝时, 提防传递复杂对象.
幸运的是, 你可以通过简单查看远程对象的接口来找出所有的问题. 要求做任何高层动作
的方法调用序列可以从类接口中明显看到. 假如你看到一个通常的高层操作需要许多连续
的远程方法调用, 这就是一个警告信号, 可能你需要重新查看一下类接口.
减少远程调用代价的技巧
一个例子, 考虑下面假定的治理一个组织目录的应用程序: 一个远程的 Directory 对象包
含了 DirectoryEntry 对象的引用, 表现了电话簿的入口.
![以下内容为程序代码](https://i-blog.csdnimg.cn/blog_migrate/690414f8c3ff79a77e5b529dd90732d7.gif)
public interface Directory extends Remote {
DirectoryEntry[] getEntries();
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
public interface DirectoryEntry extends Remote {
String getName();
String getPhoneNumber();
String getEmailAddress();
}
现在假设你想在一个 GUI email 程序中使用 Directory 的东西. 程序首先调用
getEntries() 来得到入口的列表, 接着在每个入口中调用 getName(), 计算结果的列表,
当用户选择一个时, 应用程序在相应的入口调用 getEmailAdress() 来得到 email 地址.
在你能够写一封 email 之前有多少远程方法调用必须发生? 你必须调用 getEntries() 一
次, 地址簿中每个入口调用一次 getName(), 一次 getEmailAddress(). 所以假如在地址
中有 N 个入口, 你必须进行 N + 2 次远程调用. 注重你也需要创建 N + 1 个远程对象引
用, 也是一个代价很高的操作. 假如你的地址簿有许多入口的话, 不仅仅是打开 email 窗
口的时候非常慢, 也造成了网络阻塞, 给你的目录服务程序造成高负载, 导致可扩展性的
问题.
现在考虑增强的 Directory 接口:
![以下内容为程序代码](https://i-blog.csdnimg.cn/blog_migrate/690414f8c3ff79a77e5b529dd90732d7.gif)
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
这将减少多少你的 email 程序所造成的花费呢? 现在你可以调用 Directory.getNames()
一次就可以同时得到所有的名字, 只需要给你想要发送 email 的容器调用 getEntryByName() .
这个过程需要 3 个远程方法调用, 而不是 N + 2, 和两个远程对象, 而不是 N + 1 个.
假如地址簿有再多一点的名字, 这个调用的减少在程序的响应和网络负载和系统负载有很
大的不同.
用来减少远程调用和引用传递的代价的技术叫做使用次要对象标识符. 使用一个对象的标
属性, -- 在这个例子中, 是 name -- 而不是传回一个远程对象, 作为对象的一个轻量级晔斗?
次要标识符包含了它描述的对象足够的信息, 这样你只需要获取你实际需要的远程对象.
在这个目录系统的例子中, 一个人的名字是一个好的次要标识符. 在另一个例子中, 一个
安全皮包治理系统, 一个采购标识号可能是一个好的次要标识符.
另一个减少远程调用数量的技巧是块获取. 你可以进一步给 Directory 接口加个方法, 来一
次获取多个需要的 DirectoryEntry 对象:
![以下内容为程序代码](https://i-blog.csdnimg.cn/blog_migrate/690414f8c3ff79a77e5b529dd90732d7.gif)
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
DirectoryEntry getEntryByName(String name);
DirectoryEntry[] getEntriesByName(String names[]);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
现在你不仅可以得到需要的远程 DirectoryEntry , 也可以用单独一个远程方法调用得到
要的所有的入口. 虽然这并不减少汇集的代价, 但极大地较少了网络往返的次数. 如果网
延迟很重要的话, 就可以产生一个响应更快的系统(也能减少这个网络的使用).
照亮去向 RMI 层次的路径的第三的技巧是不把 DirectoryEntry 作为一个远程对象, 而把它
定义为一个通常的对象, 带有访问 name, address, email address 和其他域的访问函数.
(在 CORBA 系统中, 我可能要使用类似的 object-by-value 机制.) 然后, 当 email 应用程
序调用 getEntryName() 时, 它会获取一个 entry 对象的值 -- 不需要创建一个stub对象或
者skeleton对象, getEmailAddress() 的调用也是一个本地的调用而不是一个远程的.
当然, 所有这些技巧都都依赖于对远程对象实际上是怎样使用的理解上的, 但是对于这个
理解, 你甚至不需要看一看远程类的实现就可以找出一些潜在的严重性能问题.
结论
分布式的应用程序的性能特性本质上和本地程序不同. 许多对于本地程序代价很小的操作
对于远程应用程序来说代价非常高, 设计不好的远程接口导致一个程序有严重的扩展性和
能问题.
幸运的是, 很容易在设计时候, 为那些高代价的操作(象远程调用和远程对象创建), 通过觳橥
常的用例和分析它们, 确定和解决许多通常的分布式的性能问题, 正确使用这里提到的技?
次要的对象标识符, 块获取和 return-by-value -- 可以本质上提高用户响应时间和整个
统的吞吐量.