2012-01-24 7 views
12

Vi un problema recientemente donde los 200 hilos del contenedor web se colgaron, lo que significa que no había ninguno disponible para atender las solicitudes entrantes y la aplicación se congeló.¿La implementación de WebSphere 7 HTTPSession contraviene la especificación J2EE?

Aquí hay una sencilla aplicación web y una prueba de JMeter que creo que demuestran la causa de este problema. La aplicación web se compone de dos clases, la siguiente 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(); 
    } 
} 

y la siguiente implementación de HttpSessionListener y 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()); 
    }     
} 

La prueba JMeter ha 100 solicitudes golpear el servlet cada segundo:

<?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> 

Cuando esta prueba se ejecuta contra la aplicación web de prueba implementada en WebSphere 7, la aplicación deja de responder rápidamente y un volcado del núcleo muestra esto:

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 

Parece ser que cuando un hilo (T1) que ejecuta el método del servlet doGet() llama setAttribute() en la instancia de la aplicación HttpSession (S1), que encaje en el monitor de S1. Mientras mantiene ese bloqueo entra en la iteración de allSessions dentro del método attributeded() del oyente y llama a getAttribute(). Parece que dentro de getAttribute(), WebSphere se bloquea en el monitor de esa instancia (posiblemente porque está configurando un campo lastUpdateTime?). Entonces, T1 a su vez bloqueará los monitores de S1, S2, S3, S4, S5 ... mientras mantiene el bloqueo en S1 desde la llamada setAttribute() en el servlet.

Así que si al mismo tiempo otra secuencia (T2) se bloquea en el monitor de otra sesión (S2) en el servlet y luego entra en el bucle en addAttribute(), el punto muerto de subprocesos en los monitores S1 y S2.

he podido encontrar nada explícito en las especificaciones J2EE sobre esto, pero esta parte de la especificación Servlet 2.4 implica que el envase no debe ser Sincronización en casos de implementaciones HttpSession:

SRV.7.7 .1 Temas de subprocesamiento

Los servlets múltiples que ejecutan subprocesos de solicitud pueden tener acceso activo a un solo objeto de sesión al mismo tiempo. El Desarrollador tiene la responsabilidad de para sincronizar el acceso a los recursos de la sesión como apropiado.

JBoss no muestra bloqueos cuando ejecutamos la prueba en su contra. Entonces mis preguntas son:

  • ¿Es correcto mi entendimiento?
  • En caso afirmativo, ¿se trata de un error o una contravención de la especificación J2EE en WebSphere?
  • Si no, y es un comportamiento válido que el desarrollador debe conocer y codificar, ¿está documentado este comportamiento en algún lugar?

Gracias

+3

WAS me sigue sorprendiendo. Buen testcase. No puedo responder por experiencia o por recursos fidedignos, pero no me sorprendería que esto sea realmente otra rareza de WAS. – BalusC

Respuesta

3

El Servlet 2.5 MR6 contiene un clarification a la parte de la especificación Servlet citado en la pregunta:

Aclarar SRV 7.7.1 "Cuestiones Threading" (número 33)

Cambie el párrafo que actualmente es

"Varios servlets que ejecutan subprocesos de solicitud pueden tener acceso activo a un objeto de sesión única al mismo tiempo. El desarrollador tiene la responsabilidad para sincronizar el acceso a la sesión de recursos según sea apropiado. "

para leer

" múltiples servlets ejecución hilos de solicitud puede tener acceso activo para el mismo objeto de sesión en al mismo tiempo. El contenedor debe garantizar que la manipulación de las estructuras de datos internas que representan los atributos de la sesión se realice de manera segura para hilos . El Desarrollador tiene la responsabilidad del acceso de hilos a los objetos de los atributos mismos. Esto protegerá a la colección atributo dentro del objeto HttpSession del concurrente acceso, eliminando la posibilidad de una aplicación para causar que colección que se dañe."

Esto sigue siendo actual en Servlet 3.0 MR1 y marcas ERA de comportamiento parece más razonable. Sin embargo, tomaría de ello que * establece * El atributo puede estar sincronizado, pero no es que * obtener * El atributo sería.

Así que creo que la respuesta es:

  • ERA es cumpliendo con la especificación Servlet acuerdo con la aclaración en 2,5 MR6
  • La especificación deja lugar a malentendidos
  • era es más celoso con su sincronización es razonablemente esperada de la especificación y AFAIK este comportamiento no está claramente documentado en ningún lugar

(Como nota al margen, cambiar el caso de prueba para que listener.attributeAdded() llame a setAttribute en lugar de getAttribute no causa bloqueos en JBoss 4 o 5.)

1

es probable que haya encontrado un caso de uso no compatible de HttpSession en la implementación específica de IBM WebSphere. ¿Por qué no informarlo a IBM?

Un punto que se ha perdido para su implementación: el contenedor JavaEE puede pasivar objetos HttpSession (serializándolo en un disco o base de datos) para liberar memoria si el servidor tiene que manejar demasiadas sesiones bajo carga. Su oyente evita que el recolector de basura libere esas sesiones.

Por cierto, se supone que el objeto HttpSession solo es utilizado por el hilo que corresponde a su propia sesión. Como encontró en la especificación, en el caso de múltiples hilos concurrentes de la misma sesión, el código debe usar un mecanismo de sincronización en el objeto HttpSession.

Los detectores de sesión están basados ​​en eventos con toda la información necesaria en el evento, dicho diseño es suficiente para evitar que el oyente guarde todas las referencias a los objetos vivos HttpSession de la forma en que lo hace.

Consultar desde un hilo todas las sesiones de vida en el contenedor son extrañas e inesperadas. No es el trabajo de una aplicación web, sino de una herramienta de monitoreo o auditoría. En ese caso, se deben usar otros medios como la consulta JMX o la interfaz PMI en el contexto específico de WebSphere.

Para ayudarte, aquí hay una implementación alternativa de tu oyente para lograr el mismo recuento de atributo de sesión pero sin guardar ninguna referencia en HttpSession. Cuidado: no fue compilado ni probado.

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

Gracias Yves. ¿Se supone que "el objeto HttpSession solo debe ser utilizado por el hilo que corresponde a su propia sesión", una pauta, regla o práctica recomendada? –

+0

Diría que es el caso de uso estándar que gobierna la implementación de HttpSession desde el punto de vista de los proveedores de contenedores JavaEE. Cualquier otro caso de uso solo es relevante para herramientas de contenedor interno o herramientas de monitoreo/auditoría. Por cierto, todavía tiene la limitación de acceder a sesiones pasivadas sin detalles de implementación específicos del contenedor. –

Cuestiones relacionadas