Подтвердить что ты не робот

Внедряет ли реализация 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/[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 

Похоже, что когда поток (T1), выполняющий метод servlet 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?
  • Если это не так, и это действительное поведение, о котором разработчик должен знать и код вокруг, является ли это поведение документированным где угодно?

Спасибо

4b9b3361

Ответ 1

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

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

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

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

читать

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

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

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

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

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

Ответ 2

Вероятно, вы нашли не поддерживаемый вариант использования 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());
    }                
}