- 论坛徽章:
- 0
|
摘要
本文主要描述了对JNDI的DirContext实例进行连接有效性检查的常用方法以及如何正确实现一个DirContext Pool的连接有效性检查机制。最后通过该文我们将得出解决类似问题的一个有效思路。
本文讨论的以被广泛使用的Sun JNDI LDAP Service Provider为主要对象,对于其他企业,团体或个人提供的LDAP Service Provider,本文所提供的思路同样适用。
关于Connection Pool与DirContext Pool 在JDK1.4之前的版本,被广泛使用的Sun JNDI LDAP Service Provider没有提供对连接池的支持,对于一个以JNDI为基础的LDAP前端应用来说,没有连接池对性能来说是灾难,所以我相信绝大多数系统会自己实现DirContext Pool。
JDK 1.4之后Sun提供了对Connection Pool的支持,但是这是一个比Context更底层的一个LDAP连接共享机制。在对DirContext进行close()操作后,底层的连接资源并没有被真正释放,相反在其他线程进行new InitialDirContext()时,该连接会被重用。但是该池主要专注于连接的重用,它并不能替代一个高层的DirContext实例池。比如使用Persistent Search,如果每次关闭Context,那么相关的Listener会被注销,这一般不能满足应用的要求。这种情况下就需要一个Context池用于长久的保存DirContext实例;同时一个DirContext池也会避免多次重复的new InitialDirContext()动作;而且对于非Sun的Service Provider来说,由于未必会提供这种连接池,所以往往也需要一个DirContext Pool。
连接池的连接有效性检查机制
如果实现一个更上层的DirContext 池,那么对于被重用的DirContext必须要在使用前对连接有效性进行检查,即保证被服务的每个线程将取到一个有效的DirContext实例。即在线程从池中获取实例前,DirContext Pool负责检查该实例中的连接是否有效,如果无效,则创建新的DirContext;否则使用池中实例。
具体点说,由于池中的实例会被长时间保存,如果池中的实例长时间未用导致远程LDAP服务器主动关闭了该实例关联的LDAP连接,或者由于其他原因导致物理连接失效而LDAP服务器并未当机,那么池中的实例必须要经过检查后才能交给其他线程使用。从而避免LDAP服务器处于正常工作状态但在客户端却收到CommunicationException这样一个不友好的容易造成错误理解的底层通讯异常。
其他的比如DataSource中的DB Connection Pool往往也有同样的机制,这是实现一个连接池必须的一个机制。
对性能的影响
勿庸置疑,这种有效性的检查对性能可能会造成较大的影响,因为每次与LDAP的交换事先都会进行这样的检查操作。
如何检查DirContext实例中的LDAP连接是否有效
但是JNDI并没有提供类似isConnected或者 isValid这样的方法(JNDI是一个更高层次上的接口),那么如何判断一个DirContext内关联的LDAP连接是否有效呢?答案只有一个即必须通过一次LDAP操作来确定该Context实例是可用的。
我之所以有写这篇文章的想法也是因为前段时间在网上看到一个开源项目的代码,其中对DirContext实例是否有效,有如下的一段检查代码:
private void checkConnection() { if(!initialized) { init(); } // test connection try { if(initialized) { DirContext ctxTest = (DirContext) ctx.lookup(""); ctxTest.close(); } } catch (NamingException ne) { getLogService().info("connection to ldap server failed. Retrying. Info is: " + ne); try { reset(); init(); } catch (NamingException e) { getLogService().fatal("ldap communication cannot be established: " + e); getLogService().fatal("explanation: " + e.getExplanation() + "\nroot cause: " + e.getRootCause()); throw new RuntimeException(communicationFailureMsg); } } if(!initialized) { throw new RuntimeException(communicationFailureMsg); } }
该段代码实际并未应用到连接池中,因此对性能并不是太敏感;但它对DirContext实例进行了保留以便后续的使用。
对于类似的有效性检查,主要有一个原则,就是有尽可能高的效率。比如DB中的select 1 from dual。所以对于DirContext来说,由于我们必须要执行一次LDAP Operation来达到我们的目的,因此必须选择一个非常轻量级的高效率的操作。
什么样的操作是一个轻量级操作?一般来说客户端与服务器交换的数据越少,越是一个轻量级的操作。LDAP操作中Compare操作一般情况下对比一些返回冗长数据的LDAP Search来说是一种轻量级操作。当然还有很多条件下的LDAP Search,bind,unbind等也可以达到同样的效果。同时一个轻量级的操作并不能同时证明对特定的LDAP Server来说也一定是效率最高的(Server资源开销最少——注:本文没有描述对LDAP Server资源的监控结果,而是采用在客户端计算Server返回操作结果所需的时间来进行对比;因为对于服务器的资源,不同的环境下测试结果差异可能会比较大,读者如果有兴趣可以根据实际情况在自己的环境中进行测试并监控服务器端资源),尽管很多情况下它们有密切的联系。
因此我们必须要通过具体的测试来总结出哪些操作才是效率最高的轻量级操作,这样的操作才适合使用在有效性检查这样的环境中。我这里列举了几个操作,来比较它们之间的差异。
1. lookup Root DSE
这就是上面那个开源项目采用的办法。我们使用JNDI Trace(见我的另外一篇文章《跟踪Sun JNDI LDAP Service Provider底层通讯》)来看看客户端与服务器到底往返了多少数据?
-> localhost:389 LDAPMessage { messageID = 1, protocolOp = { bindRequest = { version = 3, name = cn=Directory Manager, authentication = { simple = directory } } } } <- localhost:389 LDAPMessage { messageID = 1, protocolOp = { bindResponse = { resultCode = 0, matchedDN = , errorMessage = } } }
-> localhost:389 LDAPMessage { messageID = 2, protocolOp = { searchRequest = { baseObject = , scope = 0, derefAliases = 3, sizeLimit = 0, timeLimit = 0, typesOnly = false, filter = { present = objectClass }, attributes = { } } } , controls = { { controlType = 2.16.840.1.113730.3.4.2, criticality = false } } } <- localhost:389 LDAPMessage { messageID = 2, protocolOp = { searchResEntry = { objectName = , attributes = { { type = objectClass, vals = { top } }, { type = namingContexts, vals = { dc=tannin.com, o=NetscapeRoot } }, { type = supportedExtension, vals = { 2.16.840.1.113730.3.5.6, 2.16.840.1.113730.3.5.8, 2.16.840.1.113730.3.5.3, 2.16.840.1.113730.3.5.4, 2.16.840.1.113730.3.5.5, 2.16.840.1.113730.3.5.7 } }, { type = supportedControl, vals = { 2.16.840.1.113730.3.4.13, 2.16.840.1.113730.3.4.15, 1.3.6.1.4.1.1466.29539.12, 2.16.840.1.113730.3.4.3, 1.2.840.113556.1.4.473, 2.16.840.1.113730.3.4.12, 2.16.840.1.113730.3.4.9, 2.16.840.1.113730.3.4.18, 2.16.840.1.113730.3.4.16, 2.16.840.1.113730.3.4.17, 2.16.840.1.113730.3.4.19, 2.16.840.1.113730.3.4.4, 2.16.840.1.113730.3.4.5, 2.16.840.1.113730.3.4.14, 2.16.840.1.113730.3.4.2 } }, { type = supportedSASLMechanisms, vals = { DIGEST-MD5, EXTERNAL } }, { type = supportedLDAPVersion, vals = { 3, 2 } }, { type = dataversion, vals = { 020041231144250020041231144250 } }, { type = netscapemdsuffix, vals = { cn=ldap://dc=TANNIN,dc=:389 } } } } } } <- localhost:389 LDAPMessage { messageID = 2, protocolOp = { searchResDone = { resultCode = 0, matchedDN = , errorMessage = } } } -> localhost:389 LDAPMessage { messageID = 3, protocolOp = { unbindRequest = NULL } , controls = { { controlType = 2.16.840.1.113730.3.4.2, criticality = false } } }
统计了一下输出内容的字节数:3186。这包括了一些空格,和注释,以及bind/unbind操作等,如果要取得精确的返回数据,那么需要具体解析LDAPMessage,我们这里只作粗略对比即可。
2. search entry dn 这次测试具体代码如下:
ctx.search( "o=NetscapeRoot", "(objectclass=*)", EMPTY_CONSTRAINT); (本次及以下测试输出的LDAPMessage被省略) 往返字节总数:1050 3. search Root DSE 代码如下:
ctx.search( "", "(objectclass=*)", EMPTY_CONSTRAINT); 往返字节总数:1208
4. compare
代码如下: NamingEnumeration answer = ctx.search( "o=NetscapeRoot", "(objectclass={0})", new Object[]{"top".getBytes()}, EMPTY_CONSTRAINT); answer.hasMore(); 往返字节总数:936
---
从上面的测试数据可以粗略看出Compare确实是名副其实的轻量级操作,而lookup("")其实是对Root DSE进行了一次搜索,根据Sun JNDI的文档,这个搜索还会返回Root DSE的全部属性,往返的数据量是最大的,因此性能往往是最差的(对于其他的LDAP Service Provider来说未必会有和Sun相同的实现,但是我们同样可以根据上面的方法看出各种方式间的差异)。
所以可能会从外表上感觉context.lookup(“”)比cntext.search()是一个效率更高的操作(不指定具体的dn,没有返回值等等),或者并没有关心这样一个重要的效率问题。但是在面临这样的问题时我们必须要经过思考和测试才能决定哪个方法才是最合适的方法。
下面是更进一步的验证测试,用于验证对于LDAP Server来说这些操作所需要花费的时间——循环进行0xFF次操作所花费时间的对比:(每个测试会进行0xFF次搜索,共进行5次测试)
-- org.adn.jndi.cache.util.ConnectionCheck$LookupDSE Time: 1312ms. Time: 1031ms. Time: 1022ms. Time: 1021ms. Time: 1002ms. -- org.adn.jndi.cache.util.ConnectionCheck$SearchDN Time: 550ms. Time: 401ms. Time: 401ms. Time: 340ms. Time: 331ms. -- org.adn.jndi.cache.util.ConnectionCheck$SearchDSE Time: 1031ms. Time: 961ms. Time: 971ms. Time: 911ms. Time: 932ms. -- org.adn.jndi.cache.util.ConnectionCheck$Compare Time: 1261ms. Time: 1012ms. Time: 901ms. Time: 771ms. Time: 771ms.
从上面的结果可以看出SearchDN是性能最好的。当然上面的测试不同环境中可能会有不同的结果,但是大体上应该不会偏离这个结论。因此我们可以看出lookup("")是所有方式中最不好的一个。由于对于池来说,几乎每次LDAP操作都会事先进行一次check,所以对于这些的性能是非常敏感的。
结束语
通过以上的了解和分析,我们知道如何给一个DirContext Pool设计一个合适的DirContext有效性的检查机制。对于类似的问题主要的思路就是:1. 确定我们需要一个轻量级的操作;2. 通过跟踪底层交换的数据确定哪些操作符合我们的要求;3. 通过最终测试找出最适合的操作。
因为这些东西往往无法通过已有的文档比如JNDI Tutorial来获取,所以“纸上得来终觉浅,绝知此事要躬行”。
1. 下面是测试的源程序: package org.adn.jndi.cache.util; import java.io.ByteArrayOutputStream; import java.util.*; import javax.naming.directory.*; import javax.naming.*; /** * */ public class ConnectionCheck { /** * */ private static Properties env= new Properties(); /** * */ private static ByteArrayOutputStream out= new ByteArrayOutputStream(); /** * */ static { env.put( DirContext.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put( DirContext.SECURITY_CREDENTIALS, "directory"); env.put( DirContext.SECURITY_PRINCIPAL, "cn=Directory Manager"); env.put( DirContext.PROVIDER_URL, "[url=]ldap://localhost:389[/url]"); // env.put( "com.sun.jndi.ldap.trace.ber", out); } /** * */ private static SearchControls EMPTY_CONSTRAINT= new SearchControls(); static { EMPTY_CONSTRAINT.setReturningAttributes( new String[0]); EMPTY_CONSTRAINT.setSearchScope( SearchControls.OBJECT_SCOPE); EMPTY_CONSTRAINT.setReturningObjFlag(false); } /** * */ public ConnectionCheck() { super(); // TODO Auto-generated constructor stub } /** * * @param args */ public static void main( String[] args) throws Exception { String m= args[0]; Runner r= (Runner)Class.forName( m).newInstance(); for ( int i=0; i<5; i++) { test(r); } } /** * * */ protected static void test( Runner r) { DirContext ctx= null; try { ctx= new InitialDirContext( env); long start= System.currentTimeMillis(); for ( int i=0; i<0xFF; i++) { r.run( ctx); } long end= System.currentTimeMillis(); System.out.println( "Time: "+ (end-start)+ "ms."); }catch ( NamingException ex) { ex.printStackTrace(); }finally { try { if (ctx!= null) ctx.close(); }catch ( Exception ex) { } } } /** * */ static interface Runner { public void run( DirContext ctx) throws NamingException; } /** * */ static class LookupDSE implements Runner { public void run( DirContext ctx) throws NamingException { ((DirContext)ctx.lookup("")).close(); } } /** * */ static class SearchDN implements Runner { public void run( DirContext ctx) throws NamingException { ctx.search( "o=NetscapeRoot", "(objectclass=*)", EMPTY_CONSTRAINT); } } /** * */ static class SearchDSE implements Runner { public void run( DirContext ctx) throws NamingException { ctx.search( "", "(objectclass=*)", EMPTY_CONSTRAINT); } } /** * */ static class Compare implements Runner { public void run( DirContext ctx) throws NamingException { NamingEnumeration answer = ctx.search( "o=NetscapeRoot", "(objectclass={0})", new Object[]{"tops".getBytes()}, EMPTY_CONSTRAINT); } } } 2. 下面是使用Netscape LDAP Service Provider 测试的结果: -- .cache.util.ConnectionCheck$LookupDSE Time: 1342ms. Time: 1072ms. Time: 1072ms. Time: 1011ms. Time: 1011ms. -- .cache.util.ConnectionCheck$LookupDSE Time: 1332ms. Time: 1101ms. Time: 1072ms. Time: 1032ms. Time: 1012ms. -- .cache.util.ConnectionCheck$SearchDN Time: 521ms. Time: 341ms. Time: 321ms. Time: 270ms. Time: 270ms. -- .cache.util.ConnectionCheck$SearchDSE Time: 1071ms. Time: 951ms. Time: 951ms. Time: 901ms. Time: 881ms. -- .cache.util.ConnectionCheck$Compare Time: 1211ms. Time: 911ms. Time: 831ms. Time: 721ms. Time: 721ms. |
|