【问题标题】:Does WebSphere 7 HTTPSession implementation contravene J2EE spec?WebSphere 7 HTTPSession 实现是否违反 J2EE 规范?
【发布时间】:2012-02-17 20:17:38
【问题描述】:

最近发现一个问题,所有 200 个 Web 容器线程都挂起,这意味着没有一个可用于处理传入请求,因此应用程序冻结了。

这是一个简单的 Web 应用程序和 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/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 

似乎当执行 servlet 的 doGet() 方法的线程 (T1) 在 HttpSession 实现 (S1) 的实例上调用 setAttribute() 时,它会锁定 S1 的监视器。在持有该锁的同时,它进入侦听器的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 规范?
  • 如果不是,并且这是开发人员应该了解和编写代码的有效行为,此行为是否记录在任何地方?

谢谢

【问题讨论】:

  • WAS 让我惊叹不已。不错的测试用例。我无法从经验或权威资源中回答,但当这确实是另一个 WAS 怪癖时,我不会感到惊讶。

标签: java jakarta-ee websphere deadlock


【解决方案1】:

Servlet 2.5 MR6 包含 clarification 到问题中引用的 Servlet 规范部分:

阐明 SRV 7.7.1“线程问题”(第 33 期)

更改当前的段落

"多个 servlet 执行请求线程可能 同时可以主动访问单个会话对象。这 开发者有责任同步访问会话 适当的资源。”

阅读

"多个 servlet 正在执行 请求线程可能在 同时。容器必须确保操纵内部 表示会话属性的数据结构在一个 线程安全的方式。开发人员负责线程安全 访问属性对象本身。这将保护 来自并发的 HttpSession 对象内的属性集合 访问,消除了应用程序导致的机会 集合损坏。”

这在 Servlet 3.0 MR1 中仍然是最新的,并使 WAS 的行为看起来更合理。但是,我认为 *set*Attribute 可能会同步,但 *get*Attribute 不会同步。

所以我认为答案是:

  • WAS 根据 2.5 MR6 中的说明遵守 Servlet 规范
  • 规范留下了误解的余地
  • WAS 对同步的热情比规范和 AFAIK 中合理预期的要热心,这种行为在任何地方都没有明确记录

(附带说明,更改测试用例以使 listener.attributeAdded() 调用 setAttribute 而不是 getAttribute 不会导致 JBoss 4 或 5 上的死锁。)

【讨论】:

    【解决方案2】:

    您可能在 IBM WebSphere 特定实现中发现了一个不受支持的 HttpSession 用例。为什么不向 IBM 报告?

    您在实现中遗漏了一点:如果服务器必须在负载下处理太多会话,JavaEE 容器可能会钝化HttpSession 对象(通过在磁盘或数据库上对其进行序列化)以释放内存。您的侦听器阻止垃圾收集器释放该会话。

    顺便说一句,HttpSession 对象应该只被与其自己的会话对应的线程使用。正如您在规范中发现的那样,如果同一会话中有多个并发线程,代码必须在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());
        }                
    }
    

    【讨论】:

    • 谢谢伊夫。 “HttpSession 对象应该只由与其自己的会话对应的线程使用”是准则或规则还是最佳实践?
    • 我想说,从 JavaEE 容器供应商的角度来看,规范 HttpSession 实现的是标准用例。任何其他用例仅与内部容器工具或监控/审计工具相关。顺便说一句,在没有容器特定实现细节的情况下,您仍然有访问钝化会话的限制。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-25
    • 2011-10-07
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    相关资源
    最近更新 更多