优质博文IT-BLOG-CN一、问题描述生产环境偶尔(涉及到多线程处理)出现前端传递Cookie为空的告警导致前端请求丢失出现请求失败问题。告警内容如下前端传递Cookie为空 告警内容服务端获取request Cookie为空请尽快处理 AppIdxxxxxx ipxx.xx.xxx.xx 告警事件2024-03-15背景为什么要加Cookie告警项目出海需要保证多语言语言信息从Cookie中获取所以添加了Cookie告警告警后发到工作群中但是相关开发人员告知自己能够正常访问没有问题因为正好周五自己觉得偶发性肯定和并发相关所以周末研究了下代码发现和Tomcat Rquest复用机制和ThreadLocal的使用存在缺陷导致这个偶发性问题在分析原因前先需要搞懂一个概念request在tomcat里面是循环使用的二、Tomcat 中 Reqeust 复用机制Request对象的复用机制是为了提高性能和减少垃圾收集压力而设计的。Tomcat使用了一种对象池的机制来管理Request对象和Response对象。通过复用这些对象Tomcat可以避免频繁地创建和销毁对象从而提高系统的效率。复用机制的工作原理【1】对象池Tomcat维护一个对象池用于存储Request对象和Response对象。当一个新的HTTP请求到达时Tomcat从对象池中获取一个空闲的Request对象和Response对象。如果对象池中没有空闲的对象Tomcat会创建新的对象。简单看个案例public class RequestPool { private StackRequest pool new Stack(); // 获取对象getRequest 方法从对象池中获取一个 Request 对象。如果对象池为空则创建一个新的 Request 对象。 public Request getRequest() { if (pool.isEmpty()) { return new Request(); } else { return pool.pop(); } } // 释放对象releaseRequest 方法将 Request 对象重置调用 recycle 方法并放回对象池中。 public void releaseRequest(Request request) { request.recycle(); pool.push(request); } }【2】对象重置当一个请求处理完毕后Request对象会被重置通过调用recycle方法以清除上一次请求的状态使其可以安全地用于下一个请求。以下是org.apache.catalina.connector.Request类中recycle方法的简化源码和解释public class Request { // Various fields representing the state of the request private String protocol; private String method; private String requestURI; private String queryString; private String remoteAddr; private String remoteHost; private String serverName; private int serverPort; private boolean secure; private InputStream inputStream; private Reader reader; private ServletInputStream servletInputStream; private BufferedReader bufferedReader; private MapString, Object attributes; private MapString, String[] parameters; private Cookie[] cookies; private HttpSession session; // Other fields and methods... /** * Recycle this request object. */ public void recycle() { // Reset the state of the request object // 重置基本属性recycle 方法将 Request 对象的基本属性如 protocol、method、requestURI 等重置为初始状态通常为 null 或默认值。 // 清空集合和数组attributes 和 parameters 集合被清空以确保没有残留的请求数据。cookies 数组也被重置为 null。 // 重置流和读者inputStream、reader、servletInputStream 和 bufferedReader 被重置为 null以确保没有残留的输入流和读者对象。 // 重置会话session 被重置为 null以确保没有残留的会话信息。 protocol null; method null; requestURI null; queryString null; remoteAddr null; remoteHost null; serverName null; serverPort 0; secure false; inputStream null; reader null; servletInputStream null; bufferedReader null; attributes.clear(); parameters.clear(); cookies null; session null; // Other reset logic... } }recycle执行的时机recycle方法在Tomcat源码中的调用时机主要是在请求处理完毕之后Request对象被返回到对象池之前。具体来说recycle方法通常在以下几个场景中被调用【1】请求处理完毕后在Tomcat的org.apache.coyote.Request类中recycle方法通常在请求处理完毕后被调用。例如在AbstractProcessorLight类中处理请求和响应的逻辑中recycle方法被调用来重置Request对象。// org.apache.coyote.AbstractProcessorLight public class AbstractProcessorLightS implements Processor { // Various fields and methods... Override public SocketState process(SocketWrapperBaseS socketWrapper, SocketEvent status) throws IOException { // Process the request and response try { // Request processing logic... } finally { // Recycle the request and response objects request.recycle(); response.recycle(); } return SocketState.CLOSED; } }【2】连接关闭时在Tomcat的org.apache.coyote.http11.Http11Processor类中当连接关闭时recycle方法也会被调用。例如当处理完一个请求并决定关闭连接时会调用recycle方法。// org.apache.coyote.http11.Http11Processor public class Http11Processor extends AbstractProcessorLightSocketChannel { // Various fields and methods... Override public SocketState service(SocketWrapperBaseSocketChannel socketWrapper) throws IOException { // Service the request and response try { // Request servicing logic... } finally { // Recycle the request and response objects request.recycle(); response.recycle(); } return SocketState.CLOSED; } }【3】异常处理在处理请求的过程中如果发生异常Tomcat也会确保调用recycle方法来重置Request对象。例如// org.apache.coyote.http11.Http11Processor public class Http11Processor extends AbstractProcessorLightSocketChannel { // Various fields and methods... Override public SocketState service(SocketWrapperBaseSocketChannel socketWrapper) throws IOException { try { // Request servicing logic... } catch (Exception e) { // Handle exception and recycle request request.recycle(); response.recycle(); throw e; } } }后期原因分析中需要使用到RequestFacade这里解释下RequestFacade与Request之间的关系RequestFacade是一个包装类Facade用于保护底层的Request对象确保应用程序无法直接访问和修改内部实现细节。【1】Request类Request类是Tomcat内部用来表示HTTP请求的类包含了请求的所有详细信息。该类提供了许多方法来访问和操作请求的各个部分例如请求头、请求参数、输入流等。【2】RequestFacade类RequestFacade类是一个包装器用于保护Request对象。它实现了javax.servlet.http.HttpServletRequest接口并将方法调用委托给内部的Request对象。通过使用RequestFacadeTomcat确保了应用程序只能通过标准的HttpServletRequest接口访问请求数据而不能直接访问或修改Request对象的内部实现。具体实现在Tomcat中RequestFacade类通常包含一个Request对象的引用并将所有的接口方法调用委托给这个内部的Request对象。例如// org.apache.catalina.connector.RequestFacade public class RequestFacade implements HttpServletRequest { private final Request request; public RequestFacade(Request request) { this.request request; } Override public String getParameter(String name) { return request.getParameter(name); } // Other methods from HttpServletRequest interface // All methods delegate to the internal Request object }使用场景在Tomcat处理请求的过程中当需要将HttpServletRequest对象传递给应用程序时Tomcat会创建一个RequestFacade实例并将内部的Request对象传递给它。例如// org.apache.catalina.connector.CoyoteAdapter public class CoyoteAdapter implements Adapter { Override public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request (Request) req.getNote(ADAPTER_NOTES); Response response (Response) res.getNote(ADAPTER_NOTES); // Create a RequestFacade to pass to the application HttpServletRequest requestFacade request.getRequest(); // Pass the RequestFacade to the application context.getPipeline().getFirst().invoke(requestFacade, response); } }ThreadLocal的原理不清楚可以参考ThreadLocal 类三、原因分析【1】第一次请求由线程A正常执行执行完成后执行recycle方法将RequestFacade中的属性修改为null准备下次复用但是当前线程的ThreadLocal没有被清理。【2】第二次请求恰好也由线程A执行这也是偶发的原因通过ThreadLocal获取RequestFacade对象并通过getCookies获取Cookie因为第一次请求结束后将Cookie置为null并将cookiesParsed修改为了false但是这次请求再次调用getCookies的时候将cookiesParsed修改为了true。用来表示RequestFacade A的Cookies已经被解析过了。同时需要注意此时第一次请求的生命周期已经结束了所以重置cookiesParsed的操作就不复存在了Tomcat重新复用RequestFacade A的时候Cookies就会获取到一个null。Override public Cookie[] getCookies() { if (!cookiesParsed) { parseCookies(); } return cookies; } protected void parseCookies() { cookiesParsed true; Cookies serverCookies coyoteRequest.getCookies(); int count serverCookies.getCookieCount(); if (count 0) { returnl } cookies new Cookie[count]; }【3】第三次请求时Tomcat复用了RequestFacade A当正常解析Cookies的时候发现cookiesParsed为true就跳过了正确解析的环节当需要使用Cookie的时候发现为空本次请求直接被中止。灵异事件解决方案【1】ThreadLocal使用完后一定需要clean【2】不要在跨线程中使用request对象。可以使用-Dorg.apache.catalina.connector.RECYCLE_FACADEStrue禁止复用。在项目的extraenv.sh中设置参数后如果有访问已经被回收的request对象就会抛出The request object has been recycled and is no longer associated with this facade异常以此就能定位到问题