Противоречит ли реализация WebSphere 7 HTTPSession спецификации J2EE?

Недавно я столкнулся с проблемой, когда все 200 потоков веб-контейнера зависли, то есть ни один из них не был доступен для обслуживания входящих запросов, и приложение замерло.

Вот простое веб-приложение и тест JMeter, который, как мне кажется, демонстрирует причину этой проблемы. Веб-приложение состоит из двух классов, следующего сервлета:

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 запросов:

<?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, приложение быстро перестает отвечать, и дамп ядра показывает следующее:

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/MemorySession@00000000A38EA0C8/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/MemorySession@00000000A14E22C0/00000000A14E22CC: 
3LKDEADLOCKOWN    which is owned by:
2LKDEADLOCKTHR  Thread "WebContainer : 2" (0x000000000225C600)
NULL 

Похоже, что когда поток (T1), выполняющий метод doGet() сервлета, вызывает setAttribute() на экземпляре реализации HttpSession (S1), он блокирует монитор S1. Удерживая эту блокировку, он переходит в итерацию allSessions внутри метода attributeAdded() слушателя и вызывает getAttribute(). Похоже, что внутри getAttribute() WebSphere блокирует монитор этого экземпляра (возможно, потому что он устанавливает поле lastUpdateTime?). Таким образом, T1 в свою очередь заблокирует мониторы S1, S2, S3, S4, S5... все это время удерживая блокировку S1 от вызова setAttribute() в сервлете.

Таким образом, если в это же время другой поток (T2) блокирует монитор другой сессии (S2) в сервлете, а затем переходит в цикл addAttribute(), потоки заходят в тупик на мониторах S1 и S2.

Я не смог найти ничего явного в спецификациях J2EE об этом, но эта часть спецификации Servlet 2.4 подразумевает, что контейнер не должен синхронизироваться на экземплярах реализаций HttpSession:

SRV.7.7.1 Threading Issues

Несколько сервлетов, выполняющих потоки запросов, могут иметь активный доступ к одному объекту сессии в одно и то же время. Разработчик несет ответственность за синхронизацию доступа к ресурсам сессии, как соответствующим образом.

JBoss не показывает никаких тупиковых ситуаций, когда мы запускаем тест на нем. Итак, мои вопросы:

  • Правильно ли я понимаю?
  • Если да, то является ли это ошибкой или нарушением спецификации J2EE в WebSphere?
  • Если нет, и это правильное поведение, о котором разработчик должен знать и писать код, то документировано ли это поведение где-нибудь?

Спасибо

12
задан Paul Medcraft 24 January 2012 в 15:21
поделиться