2012-01-24 56 views
12

最近發生了一個問題,即所有200個Web容器線程都掛起,這意味着沒有可用的服務來傳入請求,因此應用程序凍結。WebSphere 7 HTTPSession的實現是否違反J2EE規範?

這裏是一個簡單的網絡應用程序和JMeter測試,我認爲證明了這個問題的原因。 Web應用程序包括兩個類,下面的servlet:

public class SessionTestServlet extends HttpServlet { 

    protected static final String SESSION_KEY = "session_key"; 

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
      throws ServletException, IOException { 
     // set data on session so the listener is invoked 
     String sessionData = new String("Session data"); 
     request.getSession().setAttribute(SESSION_KEY, sessionData); 
     PrintWriter writer = response.getWriter();       
     writer.println("<html><body>OK</body></html>");      
     writer.flush(); 
     writer.close(); 
    } 
} 

及以下實施HttpSessionListener和HTTPSessionAttributeListener:

public class SessionTestListener implements 
     HttpSessionListener, HttpSessionAttributeListener { 

    private static final ConcurrentMap<String, HttpSession> allSessions 
     = new ConcurrentHashMap<String, HttpSession>(); 

    public void attributeRemoved(HttpSessionBindingEvent hsbe) {} 

    public void attributeAdded(HttpSessionBindingEvent hsbe) { 
     System.out.println("Attribute added, " + hsbe.getName() 
      + "=" + hsbe.getValue()); 

     int count = 0; 
     for (HttpSession session : allSessions.values()) { 
      if (session.getAttribute(SessionTestServlet.SESSION_KEY) != null) { 
       count++; 
      } 
     } 
     System.out.println(count + " of " + allSessions.size() 
      + " sessions have attribute set."); 
    } 

    public void attributeReplaced(HttpSessionBindingEvent hsbe) {} 

    public void sessionCreated(HttpSessionEvent hse) { 
     allSessions.put(hse.getSession().getId(), session);        
    } 

    public void sessionDestroyed(HttpSessionEvent hse) { 
     allSessions.remove(hse.getSession().getId()); 
    }     
} 

JMeter的測試已經100個請求命中servlet的每秒:

<?xml version="1.0" encoding="UTF-8"?> 
<jmeterTestPlan version="1.2" properties="2.1"> 
    <hashTree> 
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true"> 
     <stringProp name="TestPlan.comments"></stringProp> 
     <boolProp name="TestPlan.functional_mode">false</boolProp> 
     <boolProp name="TestPlan.serialize_threadgroups">false</boolProp> 
     <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"> 
     <collectionProp name="Arguments.arguments"/> 
     </elementProp> 
     <stringProp name="TestPlan.user_define_classpath"></stringProp> 
    </TestPlan> 
    <hashTree> 
     <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true"> 
     <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> 
     <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true"> 
      <boolProp name="LoopController.continue_forever">false</boolProp> 
      <intProp name="LoopController.loops">-1</intProp> 
     </elementProp> 
     <stringProp name="ThreadGroup.num_threads">100</stringProp> 
     <stringProp name="ThreadGroup.ramp_time">1</stringProp> 
     <longProp name="ThreadGroup.start_time">1327193422000</longProp> 
     <longProp name="ThreadGroup.end_time">1327193422000</longProp> 
     <boolProp name="ThreadGroup.scheduler">false</boolProp> 
     <stringProp name="ThreadGroup.duration"></stringProp> 
     <stringProp name="ThreadGroup.delay"></stringProp> 
     </ThreadGroup> 
     <hashTree> 
     <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP Request" enabled="true"> 
      <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"> 
      <collectionProp name="Arguments.arguments"/> 
      </elementProp> 
      <stringProp name="HTTPSampler.domain">localhost</stringProp> 
      <stringProp name="HTTPSampler.port">9080</stringProp> 
      <stringProp name="HTTPSampler.connect_timeout"></stringProp> 
      <stringProp name="HTTPSampler.response_timeout"></stringProp> 
      <stringProp name="HTTPSampler.protocol">http</stringProp> 
      <stringProp name="HTTPSampler.contentEncoding"></stringProp> 
      <stringProp name="HTTPSampler.path">/SESSION_TESTWeb/SessionTestServlet</stringProp> 
      <stringProp name="HTTPSampler.method">GET</stringProp> 
      <boolProp name="HTTPSampler.follow_redirects">true</boolProp> 
      <boolProp name="HTTPSampler.auto_redirects">false</boolProp> 
      <boolProp name="HTTPSampler.use_keepalive">true</boolProp> 
      <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> 
      <boolProp name="HTTPSampler.monitor">false</boolProp> 
      <stringProp name="HTTPSampler.embedded_url_re"></stringProp> 
     </HTTPSamplerProxy> 
     <hashTree> 
      <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true"> 
      <stringProp name="ConstantTimer.delay">1000</stringProp> 
      </ConstantTimer> 
      <hashTree/> 
     </hashTree> 
     </hashTree> 
     <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true"> 
     <boolProp name="ResultCollector.error_logging">false</boolProp> 
     <objProp> 
      <name>saveConfig</name> 
      <value class="SampleSaveConfiguration"> 
      <time>true</time> 
      <latency>true</latency> 
      <timestamp>true</timestamp> 
      <success>true</success> 
      <label>true</label> 
      <code>true</code> 
      <message>true</message> 
      <threadName>true</threadName> 
      <dataType>true</dataType> 
      <encoding>false</encoding> 
      <assertions>true</assertions> 
      <subresults>true</subresults> 
      <responseData>false</responseData> 
      <samplerData>false</samplerData> 
      <xml>true</xml> 
      <fieldNames>false</fieldNames> 
      <responseHeaders>false</responseHeaders> 
      <requestHeaders>false</requestHeaders> 
      <responseDataOnError>false</responseDataOnError> 
      <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage> 
      <assertionsResultsToSave>0</assertionsResultsToSave> 
      <bytes>true</bytes> 
      </value> 
     </objProp> 
     <stringProp name="filename"></stringProp> 
     </ResultCollector> 
     <hashTree/> 
     <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true"> 
     <boolProp name="ResultCollector.error_logging">false</boolProp> 
     <objProp> 
      <name>saveConfig</name> 
      <value class="SampleSaveConfiguration"> 
      <time>true</time> 
      <latency>true</latency> 
      <timestamp>true</timestamp> 
      <success>true</success> 
      <label>true</label> 
      <code>true</code> 
      <message>true</message> 
      <threadName>true</threadName> 
      <dataType>true</dataType> 
      <encoding>false</encoding> 
      <assertions>true</assertions> 
      <subresults>true</subresults> 
      <responseData>false</responseData> 
      <samplerData>false</samplerData> 
      <xml>true</xml> 
      <fieldNames>false</fieldNames> 
      <responseHeaders>false</responseHeaders> 
      <requestHeaders>false</requestHeaders> 
      <responseDataOnError>false</responseDataOnError> 
      <saveAssertionResultsFailureMessage>false</saveAssertionResultsFailureMessage> 
      <assertionsResultsToSave>0</assertionsResultsToSave> 
      <bytes>true</bytes> 
      </value> 
     </objProp> 
     <stringProp name="filename"></stringProp> 
     </ResultCollector> 
     <hashTree/> 
    </hashTree> 
    </hashTree> 
</jmeterTestPlan> 

當針對在WebSphere 7上部署的測試Web應用運行此測試時,應用程序會迅速停止響應,並且核心轉儲會顯示此消息:

1LKDEADLOCK Deadlock detected !!! 
NULL   --------------------- 
NULL   
2LKDEADLOCKTHR Thread "WebContainer : 2" (0x000000000225C600) 
3LKDEADLOCKWTR is waiting for: 
4LKDEADLOCKMON  sys_mon_t:0x00000000151938C0 infl_mon_t: 0x0000000015193930: 
4LKDEADLOCKOBJ  com/ibm/ws/session/store/memory/[email protected]/00000000A38EA0D4: 
3LKDEADLOCKOWN which is owned by: 
2LKDEADLOCKTHR Thread "WebContainer : 1" (0x00000000021FB500) 
3LKDEADLOCKWTR which is waiting for: 
4LKDEADLOCKMON  sys_mon_t:0x0000000015193820 infl_mon_t: 0x0000000015193890: 
4LKDEADLOCKOBJ  com/ibm/ws/session/store/memory/[email protected]/00000000A14E22CC: 
3LKDEADLOCKOWN which is owned by: 
2LKDEADLOCKTHR Thread "WebContainer : 2" (0x000000000225C600) 
NULL 

看來,當執行servlet的doGet()方法的線程(T1)調用的setAttribute()對HttpSession的執行情況(S1)的情況下,它鎖定S1的監視器上。在持有該鎖的同時,它進入listener的attributeAdded()方法內的allSessions迭代並調用getAttribute()。它看起來像在getAttribute()裏面,WebSphere在該實例的監視器上鎖定(可能是因爲它設置了lastUpdateTime字段?)。因此,T1將依次鎖定S1,S2,S3,S4,S5 ...的監視器,同時在servlet中從setAttribute()調用中鎖定S1。

因此,如果同時另一個線程(T2)在servlet中的另一個會話(S2)的監視器上進行鎖定,然後進入addAttribute()中的循環,則S1和S2上的線程死鎖將監視。

我一直無法找到任何東西在這個J2EE的規範明確,但在Servlet 2.4規範,這部分意味着容器不應該在HttpSession中實現的實例來進行同步:

SRV.7.7 .1線程問題

執行請求線程的多個servlet可能同時具有對單個會話對象的主動訪問權限 。開發人員具有 職責,用於同步訪問會話資源,如 合適。

當我們對它運行測試時,JBoss不會顯示任何死鎖。所以我的問題是:

  • 我的理解是否正確?
  • 如果是這樣,這是WebSphere中的J2EE規範的缺陷還是違反了?
  • 如果沒有,並且開發人員應該瞭解並編碼的有效行爲,這種行爲在任何地方都有記錄嗎?

感謝

+3

WAS讓我驚歎不已。好的測試用例。我不能從經驗或權威資源回答,但當這確實是另一個WAS怪癖時,我不會感到驚訝。 – BalusC

回答

3

在Servlet 2.5 MR6包含clarification給Servlet規範的問題所引用的部分:

澄清SRV 7.7.1 「線程問題」(第33期)

更改當前爲

的段落「執行請求線程的多個servlet可能會有 的活動訪問權限單一會話對象。該 開發具有同步訪問會話 適當的資源的責任。在 同時「

閱讀

」執行 請求的線程可能在同一會話對象主動訪問多個servlet。容器必須確保以 線程安全方式執行表示會話屬性的內部數據結構的操作。開發人員有責任通過線程安全 訪問屬性對象本身。這將保護 屬性集合從並行 訪問HttpSession對象內,消除了機會,爲應用程序會導致 收集被破壞。」

這仍然是在的Servlet 3.0 MR1電流,使WAS的行爲看起來比較合理,但是我會從中得知* 集合 *屬性可能是同步的,但不是那* 得到 *屬性會是。

所以我想答案是:

  • WAS 根據澄清與Servlet規範遵守在2.5 MR6
  • 該規範餘地誤解
  • WAS與更多熱心它的同步不是會合理地從規範預期,據我所知這種行爲是不明確記載的任何地方

(作爲一個方面說明,更改測試用例,以便listener.attributeAdded()調用setAttribute而不是getAttribute不會導致JBoss 4或5上的死鎖。)

1

你可能已經發現的IBM WebSphere具體實施HttpSession一個不支持使用情況。爲什麼不向IBM報告?

您錯過了一個實現的觀點:如果服務器必須處理負載過重的會話,JavaEE容器可能會鈍化HttpSession對象(通過在磁盤或數據庫上序列化)來釋放內存。你的監聽器阻止垃圾收集器釋放該會話。

順便說一下,HttpSession對象應該只被對應於自己的會話的線程使用。正如您在規範中發現的那樣,如果來自同一會話的多個併發線程,代碼必須在對象上使用同步機制。

會話偵聽器是基於事件的事件中的所有必要信息,這樣的設計足以避免收聽者保持對所有對象的引用。

從一個線程查詢容器中的所有活動會話都是奇怪的和意外的。這不是Web應用程序的工作,而是監控或審計工具。在這種情況下,應該使用特定WebSphere上下文中的其他方法,如JMX查詢或PMI接口。

爲了幫助您,以下是您的聽衆的替代實現方式,以實現相同的會話屬性計數,但不保留對HttpSession的任何參考。注意:它既沒有編譯也沒有測試。

public class SessionTestListener implements 
     HttpSessionListener, HttpSessionAttributeListener { 

    private static final Set<String> sessionsIds 
     = new ConcurrentSkipListSet<String>(); 

    private static final ConcurrentMap<String, Object> sessionsKeys 
     = new ConcurrentHashMap<String, Object>(); 

    public void attributeRemoved(HttpSessionBindingEvent hsbe) { 
     System.out.println("Attribute removed, " + hsbe.getName() 
      + "=" + hsbe.getValue()); 
     if (SessionTestServlet.SESSION_KEY.equals(hsbe.getName())) { 
      sessionsKeys.remove(hsbe.getSession().getId()); 
     } 
    } 

    public void attributeAdded(HttpSessionBindingEvent hsbe) { 
     System.out.println("Attribute added, " + hsbe.getName() 
      + "=" + hsbe.getValue()); 

     if (SessionTestServlet.SESSION_KEY.equals(hsbe.getName())) { 
      if (hsbe.getValue() == null) { 
       sessionsKeys.remove(hsbe.getSession().getId()); 
      } else { 
       sessionsKeys.put(hsbe.getSession().getId(), hsbe.getValue()); 
      } 
     } 
     System.out.println(sessionsKeys.size() + " of " + sessionsIds.size() 
      + " sessions have attribute set."); 
    } 

    public void attributeReplaced(HttpSessionBindingEvent hsbe) {} 

    public void sessionCreated(HttpSessionEvent hse) { 
     sessionsIds.add(hse.getSession().getId()); 
    } 

    public void sessionDestroyed(HttpSessionEvent hse) { 
     sessionsIds.remove(hse.getSession().getId()); 
     sessionsKeys.remove(hse.getSession().getId()); 
    }     
} 
+0

謝謝伊夫。 「HttpSession對象應該只被與自己的會話相對應的線程使用」是指導原則還是規則或最佳實踐? –

+0

我想說這是從JavaEE容器供應商的角度來規範HttpSession實現的標準用例。任何其他用例只與內部容器工具或監控/審計工具相關。順便說一句,你仍然有限制訪問鈍化會話沒有容器具體實施細節。 –

相關問題