如何對DirContext進行連接有效性檢查
摘要
本文主要描述了對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, "ldap://localhost:389"); // env.put( "com.sun.jndi.ldap.trace.ber", out); } /** * */ private static SearchControls EMPTY_CONSTRAINT= new SearchControls(); static { EMPTY_CONSTRAINT.setReturningAttributes( new String); 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; 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.
《解決方案》
沒人頂嗎?汗~:em11: