Противоречит ли реализация WebSphere 7 HTTPSession спецификации J2EE? - PullRequest
12 голосов
/ 24 января 2012

Недавно увидела проблему, когда все 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 Проблемы с потоками

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

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

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

Спасибо

Ответы [ 2 ]

3 голосов
/ 27 января 2012

Сервлет 2.5 MR6 содержит пояснение к части спецификации сервлета, указанной в вопросе:

Уточнение SRV 7.7.1 «Проблемы с потоками» (выпуск 33)

Изменить абзац, который в настоящее время

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

читать

"Выполнение нескольких сервлетов потоки запроса могут иметь активный доступ к тому же объекту сеанса в в то же время. Контейнер должен обеспечивать манипулирование внутренними Структуры данных, представляющие атрибуты сеанса, выполняются в виде Безопасный способ. Разработчик несет ответственность за безопасность потоков доступ к объектам атрибута сами. Это защитит коллекция атрибутов внутри объекта HttpSession из одновременного доступ, исключая возможность для приложения, чтобы вызвать это коллекция повреждена. "

Это все еще актуально в Servlet 3.0 MR1 и делает поведение WAS более разумным. Однако я бы взял из этого, что * set * Attribute может быть синхронизирован, но это не то, что * get * Attribute будет.

Так что я думаю, что ответ:

  • WAS соответствует в соответствии со спецификацией сервлета в соответствии с пояснением в 2.5 MR6
  • Спецификация оставляет место для недоразумений
  • WAS более ревностен в своей синхронизации, чем можно было бы ожидать от спецификации, и AFAIK это поведение нигде четко не задокументировано

(Как примечание: изменение контрольного примера таким образом, чтобы listener.attributeAdded () вызывал setAttribute вместо getAttribute, чтобы не вызывать взаимоблокировки в JBoss 4 или 5.)

1 голос
/ 27 января 2012

Возможно, вы обнаружили неподдерживаемый вариант использования HttpSession в конкретной реализации IBM WebSphere.Почему бы не сообщить об этом в IBM?

Точка, которую вы упустили в своей реализации: контейнер JavaEE может пассивировать HttpSession объекты (путем сериализации его на диске или в базе данных) для освобождения памяти, если сервер должен обрабатывать слишкоммного сессий под нагрузкой.Ваш слушатель не позволяет сборщику мусора освободить эти сеансы.

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

Слушатели сеанса основаны на событиях со всей необходимой информацией в событии, напримердизайна достаточно, чтобы слушатель не поддерживал все ссылки на живые HttpSession объекты так, как вы это делаете.

Запрос из одного потока всех живых сессий в контейнере является странным и неожиданным.Это работа не веб-приложения, а инструмента мониторинга или аудита.В этом случае следует использовать другие средства, такие как запрос JMX или интерфейс PMI в определенном контексте WebSphere.

Чтобы помочь вам, здесь есть альтернативная реализация вашего слушателя для достижения того же количества атрибутов сеанса, но без сохранения каких-либоссылка на 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());
    }                
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...