集群和负载平衡是改进系统内服务的可伸缩性和可用性的有效方法。在应用于 WebSphere 等应用服务器内托管的应用程序时,这些技术能处理更高的整体吞吐量,并使整个应用程序能够实现更高的可用性。它们利用直观的方法提供了可伸缩性和可用性的改进。您是否希望提高集群解决方案的吞吐量?没有问题:只要在集群中添加一个服务器的新实例,接入负载平衡器以包含新服务器,即可实现目标。
但可伸缩系统意味着您能够调整应用程序的所有 部分,包括会话持久性和其他本地缓存数据。如果您未解决数据可用性的问题,那么就会给系统造成又一个瓶颈问题。服务器集群和负载平衡不能处理后台使用的数据。如果这些数据是访问的瓶颈问题所在,无论是由于它处于网络中距离过远的位置处,还是由于过多的连接为这些数据提供服务,都会妨碍提高应用程序处理负载的努力。如果您尝试更为简化的方法,直接通过复制来制作 “本地” 副本,那么实际上只会进一步提高复杂性。这是由于对一个副本的任何更新都必须在所有其他副本中执行,只有这样才能保持所有副本同步。
解决数据可用性问题的方法之一就是利用 WebSphere eXtreme Scale Object Grid(下文简称为 Object Grid)中称为多主机的新特性。此特性允许将您的数据镜像到另一个远程位置。这就意味着,在远程位置缓存的数据可以按需保持较近的版本,同时也能保留一份数据的同步视图。通过这种方式,即可在增加服务器数量时增加数据副本的数量,而无需担忧如何维护这些数据的单一、事务性的视图。除此之外,您也不必依靠其他产品或机制来完成这一任务,所有任务都将使用一个简单、优雅的解决方案完成。
这篇文章介绍了如何配置多主机网格,并讨论了使用多主机 7.1 版本时现存的一些限制,特别是针对 HTTP 会话持久性的限制。HTTP 会话持久性数据是数据优化的一种重要特例,对于某些类型的应用程序来说,开箱即用的功能存在一些局限之处。这篇文章介绍了作者如何与 IBM Web Identity Team 中的同事协力打造绕开这种限制的解决方案。
利用多主机,您可以在 Object Grid 中存储数据,并镜像数据来确保数据处于距离需要它的服务器较近的位置。图 1 展示了一个全局集群应用程序,其 Web 服务器和应用服务器位于纽约和香港。在 Object Grid 的纽约节点副本中作出的更新将通过多主机特性传播到香港的节点。对于登录到香港节点的用户来说,如果因某种原因负载平衡器将其路由至纽约节点,用户不会发现数据展示存在任何差异。如果在纽约服务器中进行数据更新,那么用户在使用香港服务器时仍然可以使用这些更新,并且可将更新后的数据作为香港服务器中的本地数据。
图 1. 多主机数据镜像
Object Grid 使用经过优化的内部事务功能来执行镜像。您不需要为执行镜像的方式配置或维护内部组件。要利用镜像特性,只需使用标准 Object Grid 应用程序编程接口即可。产品将替您维护数据副本。对于一个网格镜像的更新将自动传播到其多主机副本。
配置多主机非常简单,只需要在 WebSphere eXtreme Scale 7.1 或更新版本的对象网格配置中使用一个简单的属性文件设置即可。安装了对象网格安装文件(一个 JSE 安装文件)之后,定义本地和域名,随后链接端点即可。就是这样简单。步骤如下:
- 确保服务器了解 server.properties
文件的位置。因此在产品提供的 Object Grid 启动脚本中,确定 serverProps 文件的路径,如下所示:
-serverProps /usr/WebSphere/eXtremeScale/ObjectGrid/bin/server.properties - 在 server.properties 文件中定义本地域名:
domainName = Local - 在 server.properties 文件中定义外部域:
foreignDomains= Foreign - 在 server.properties 文件中为外部域提供可选端点列表:
Foreign.endpoints=host1:2809,host2:2809
在使用多主机解决方案时,目前存在以下限制:
- 网格名称和映射集名称必须匹配
- 必须使用固定分区,必须有相同数量的分区
- 相同的数据类型和模板
- 部分加载器限制适用
与多主机带来的价值相比,大多数此类限制都是微不足道的。然而,目前有一项限制使多主机无法开箱即用地用于 HTTP 会话持久性:必须使用固定分区,每一个多主机副本中的分区数量都必须相同。
Object Grid 实际上可不做任何修改地用于提供会话持久性(下文将介绍更多内容)。它使用分区算法来执行一些内部管理功能,因此它以容器为单位提供优化的 HTTP 会话持久性,而不是以固定分区为单位。对于某些应用程序来说,这可能已经足够了,但在许多用例中,都值得编写一点代码来实现多主机网格的全部优势。这些优势包括:
- 您可以通过已知关键字直接访问数据
- 您可以预测希望在网格中保存多少数据,因此可以使用固定分区
对于 HTTP 会话数据,只要完成两项任务,即可满足获得这些优势。
- 使用 SessionID 和应用程序上下文作为关键字
- 理解峰值用户负载,以便预测最大网格大小(实际上,就是根据峰值负载确定网格的规模)
处理 HTTP 会话数据是许多需要数据镜像的应用程序的一项关键要求。本文的后续部分将讨论利用 Web 容器的标准 J2EE 扩展来绕开这些限制的一种方法。
请注意,您可以通过简单的编码来绕过这项限制,即直接利用网格 API 在多主机网格中存储数据。这需要检查使用 setAttribute 和 getAttribute 的位置,并重载它们来包含简单的网格 API。这种方法并无错误,也非常易于实现。
但如果您希望采用一种更加通用的方法。假设您希望使用应用程序现有的 getAttribute 和 setAttribute API,不希望做出任何修改(也就是说,不希望编写代码来覆盖这些方法),也不希望检查代码,将 Object API 添加到需要使用它的位置。在这样的时候,您可以通过一些简单的添加来扩展您的能力,而无需修改现有应用程序。需要编码的内容如下:
- 一个新的 Servlet 过滤器:
MySessionOverrideFilter - 一个新的 Servlet 请求包装器:
MyHttpServletRequestWrapper - 一个新的会话属性侦听器:
MySessionAttributeListener
可以轻而易举地连接这些内容,提供所需功能,而且无需进一步更改底层应用程序。所有这些功能都是一种标准 Java Enterprise Edition 产品的一部分 - 这种方法不存在根本性的更改,不存在不受支持的部分。实际上,您可以立即使用这些工具获得其他功能。
图 2 展示了模块彼此交互的流程。
图 2. 展示组件的设计概览
下面给出了这些模块彼此交互的流程:
- 浏览器发出应用程序请求
- WebSphere 容器在运行 servlet 或 JSP 之前调用过滤器类
MySessionOverrideFilter。过滤器例程使用MyHTTPServletRequestWrapper覆盖HTTPServletRequest,这将覆盖getSession()方法。 - 调用应用程序 servlet。通过
setAttribute()方法对属性做出的任何更改都将触发 WISessionAttributeListener listener.logic 内的 attribute 方法 - 如果会话是新会话,则覆盖
session.getSession()方法,使之使用数据的 Object Grid 版本
对于所有对象,网格中存储的类都必须可序列化,Object Grid 必须理解对象签名。可序列化接口的要求之一就是类签名必须为已知,对象必须实现无参数的构造函数。为实现这种应用程序类的序列化,您可能需要将应用程序 JAR 文件添加到一个对象网格服务器的 –classpath 参数中。
覆盖过滤器链:MySessionOverrideFilter
一个新过滤器将添加到应用程序的 sessionManager.war 文件中。其目的在于覆盖常规 HTTP 处理的过滤器链,转为调用一个新的 servlet 包装器。新 servlet 包装器将用于覆盖 HTTPSession.getSession() 方法。
可以使用标准 Rational® Application Developer 向导(图 3)添加过滤器。(突出显示 WAR 文件、右键单击并选择 New->Filter。)
图 3. 用于创建新 servlet 过滤器的 Rational 向导
清单 1 展示了新过滤器的主要方法。
清单 1. 过滤器覆盖的代码片段
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws
java.io.IOException, ServletException {
// Instantiate MyHttpServletRequest to override "getSession"
chain.doFilter(new
MyHttpServletRequestWrapper((HttpServletRequest)request),
response);
}
|
chain.doFilter 方法将覆盖常规过滤器链,转为调用客户包装器 WIHttpServletReqeuestWrapper。
为了确保过滤器已应用于整个 web 应用程序,可将过滤器映射添加到 web.xml 中,如清单 2 所示。
清单 2. 过滤器映射 XML
<filter-mapping> <filter-name>MySessionOverrideFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
使用包装器读取 Object Grid 中的属性:MyHttpServletRequestWrapper
作为 Servlet 2.3 规范的一部分,新增的 HTTPServletRequestWrapper
接口允许您覆盖某些 web 容器方法。在这样的设计中,HTTPServletRequest
getSession() 方法将被覆盖,以便读取 Object Grid 中的属性。
清单 3 包含强调了这种解决方案关键部分的代码片段。
清单 3. 覆盖 getSession() 方法的代码片段
public class MyHttpServletRequestWrapper extends
HttpServletRequestWrapper {
private static String username = "user";
private static String password = "";
@Override
public HttpSession getSession(boolean aCreateIfAbsent)
{
// Still get the session from the container…
httpSess = super.getSession();
sessionId = httpSess.getId();
try {
// Get a session with the grid
gridSess = grid.getSession();
CODE OMITTED…
// Get the ObjectMap
map1 = gridSess.getMap("test");
// ObjectMap map2 = sess2.getMap("test");
gridSess.begin();
System.out
gridStoreObj = (Hashtable)
map1.get(sessionId);
CODE OMITTED…
// set attributes for each object in the grid
Iterator iter = (Iterator)
gridStoreObj.keySet().iterator();
while (iter.hasNext()) {
String name = (String) iter.next();
Object value = gridStoreObj.get(name);
CODE OMITTED…
httpSess.setAttribute(name, value);
return httpSess;
}
|
新的 MyHTTPServletRequestWrapper 覆盖了
HTTPServletRequest,这就是按照 Servlet 2.3 规范覆盖默认行为的正确接口。
模块从超类中获取会话,并检查该会话的会话 ID 是否能在 Object Grid 的本地副本中找到。如果能,则使用标准 setAttribute 方法,将会话的 Object Grid 版本的全部属性应用于会话对象的内存版本(httpSess 变量)。
这里的一个要点是:如果 setAttribute 也被覆盖为实现网格持久性,则存在连续递归调用造成循环逻辑 bug 的风险。为了避免这种问题,我们将编写会话属性侦听器来避免此类调用。请参见清单 3 中的
MySessionAttributeListener 代码,了解如何编写这种侦听器。
另外一种替代性的解决方法就是确保这是唯一出现容器 setAttribute 方法的位置,而应用程序的其他部分使用新方法来设置属性。
添加一个会话属性侦听器:MySessionAttributeListener
可以通过 Rational 中的向导添加一个会话属性侦听器,以便侦听属性更改。可以使用标准 Rational 向导添加此类(图 4)。
图 4. 创建会话属性侦听器的 Rational 向导
清单 4 突出显示了 MySessionAttributeListener 的关键逻辑。
清单 4. 会话属性侦听器代码片段
public class MySessionAttributeListener implements
HttpSessionAttributeListener, HttpSessionBindingListener,
HttpSessionListener,
HttpSessionActivationListener {
/**
* @see
HttpSessionAttributeListener#attributeAdded(HttpSessionBindingEvent)
*/
public void attributeAdded(HttpSessionBindingEvent se) {
if (thisClassCalledMe("WIHttpServletRequestWrapper")) {
// do nothing if called by the wrapper to avoid recursive call
} else {
try {
// Get a session with the grid
gridSess = grid.getSession();
map1 = gridSess.getMap("test");
// ObjectMap map2 = sess2.getMap("test");
gridSess.begin();
storeObj = (Hashtable) map1.get(sessionId);
gridSess.commit();
} catch (Exception e) {
e.printStackTrace();
}
// if nothing in grid, stick regular session there..
// if (storedSession == null) {
if (storeObj == null) {
try {
storeObj = new Hashtable();
storeObj.put(se.getName(), se.getValue());
gridSess.begin();
// map1.insert(sessionId, currentSession);
//
// currentSession);
map1.insert(sessionId, storeObj);
// currentSession);
gridSess.commit();
} catch (Exception e) {
try {
gridSess.rollback();
} catch (Exception e2) {
e2.printStackTrace();
}
}
// if something in grid, use it...
} else {
storeObj.put(se.getName(), se.getValue());
try {
gridSess.begin();
// add the new attribute
map1.update(sessionId, storeObj); //
gridSess.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
|
会话属性侦听器提供了三种方法来检测 HTTP 会话名称/值对的更改。这些方法是:
attributeRemoved(HTTPSessionBindingEvent)attributeAdded(HTTPSessionBindingEvent)AttributeReplaced(HTTPSessionBindingEvent)
代码片段关注的是 attributeAdded(HTTPSessionBindingEvent)
方法。调用 API session.setAttribute(name,
value) 时将调用此方法。
在类中,使用 thisClassCalledMe 方法来评估调用堆栈。如果调用了特殊 WIHTTPServletRequestWrapper 类,则忽略在网格中保留属性的逻辑,因为此时网格中应该已经具备了这些属性。随后,逻辑将使用常规 Object Grid get/put API 来更新 Object Grid 中的数据。
清单 5 展示了完整的 thisClassCalledMe
方法。有关此方法的更多细节,请参见 参考资料 部分。
清单 5. 评估调用堆栈的代码片段
public static boolean thisClassCalledMe(String who) {
StackTraceElement[] stackTraceElements =
Thread.currentThread()
.getStackTrace();
for (int i = 5; i < stackTraceElements.length;
i++) {
StackTraceElement ste =
stackTraceElements[i];
String classname = ste.getClassName();
if (classname.equals(who)) {
return true;
}
String methodName = ste.getMethodName();
int lineNumber = ste.getLineNumber();
}
return false;
}
|
它使用线程上下文和 getStackTrace 方法确定调用方是否是特定方法,从而对递归调用进行有条件的覆盖调用。
毫无疑问,会话持久性的特殊情况要求已经存在了较长的时间,而其他方法确实提供了一些具有吸引力的特性。下面简要介绍了这些方法及其与上述方法的不同之处:
- 数据库会话持久性。在数据库会话持久性中,使用一个中央数据库来存储会话数据。数据库会话持久性是一种常用、可靠的方法。WebSphere Application Server 将其作为一种便捷的选项提供,可通过最少的配置加以利用。它提供了数据的单一视图,数据库高可用性技术消除了数据库成为单一故障点的问题。IBM DB2 数据库也提供了队列复制和高可用性数据库复制,从而提供数据复制特性。
- 内存到内存 (M2M) 会话持久性。WebSphere Application Server 还提供了内存到内存 (M2M) 会话持久性。在这种方法中,应用服务器将会话更新的副本推送到其他 WebSphere Application Server 实例。M2M 解决方案有着集群化的设计,因此提供了内置的容错能力。它们还能将数据存储在距离使用这些数据的服务器较近的位置。在这里,应该倍加谨慎,确保设置复制路径,避免单独一个服务器成为单一故障点,避免出现过多的跨网络流量。
- WebSphere eXtreme Scale 或 XC10 提供的会话持久性。当然,WebSphere eXtreme Scale 提供了自己的会话持久性功能。它能在通用计算机或采用专业设备技术的 XC10 设备上实现。它提供了内置的容错能力,并且易于安装。(它是作为最新版本的 WebSphere 的一项配置的一部分提供的。)但务必牢记,这种方法难以控制网格内部组件的数据放置。其内部算法控制数据放置,因此部分数据将仍然距离使用这些数据的服务器较远。当然,目前它还没有使用多主机技术。
可以看到,“上扩” Web 应用程序的常用技术可能会带来数据可用性方面的问题。通过其他方法可以解决该问题,但会很快造成复杂度升高。解决数据可用性问题的一种新方法就是 WebSphere eXtreme Scale 7.1 版本中的多主机特性。多主机复制提供了制作数据镜像副本的一种方法,且无需考虑制作镜像的机制。如果您需要扩展其功能,包含 HTTP 会话数据,也可以按部就班地完成该任务。eXtreme Scale 可靠、事务性的设计使解决方案切实可行,而其中包含的简单步骤又使之成为满足数据可用性需求的便捷选择。
除了 Web Identity Team 之外,作者还要感谢 Object Grid 团队的以下成员:Billy Newport 和 Doug Berg。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| 多主机解决方案示例代码 | Multi_Master_JEE_PI.zip | 22KB | HTTP |
学习
-
Gentle intro to DataGrid technology and customer use
cases
-
通过 WebSphere eXtreme Scale 实现可伸缩的 HTTP 会话管理
-
WebSphere eXtreme Scale InfoCenter
-
Infocenter 中有关固定分区与按容器分区的对比的参考资料
-
WebSphere eXtreme Scale XC10 Appliance 信息中心
-
WebSphere eXtreme Scale WIKI
-
有关 ThisClassCalledMe 方法的详细信息
-
IBM developerWorks 中国 WebSphere 专区:为使用 WebSphere 产品的开发人员准备的技术信息和资料。这里提供产品下载、how-to 信息、支持资源以及免费技术库,包含 2000 多份技术文章、教程、最佳实践、IBM Redbook 和在线产品手册。
获得产品和技术
- 最受欢迎的 WebSphere 试用软件下载:下载关键 WebSphere 产品的免费试用版。
- IBM developerWorks 软件下载资源中心:IBM deveperWorks 最新的软件下载。
- IBM developerWorks 工具包:下载关键 WebSphere 最新的产品工具包。
讨论
- 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。
- 加入 IBM 软件下载与技术交流群组,参与在线交流。