You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

K8s部署的Spring Boot应用StandardManager会话堆积致堆内存升高排查

充电桩代理服务会话堆积问题排查

服务说明

该Spring Boot应用是基于WebSocket(搭载OCPP协议)的充电桩代理服务,采用多节点部署在负载均衡器后方,维持长连接,以便关联的RabbitMQ消息消费微服务可进行重新部署与维护。

堆转储分析

数万充电桩设备频繁连接断开,核心问题是org.apache.catalina.session.StandardManager中的sessions属性持续新增会话(7天内超70万)但未被清理。

对比其他会话存储:

  • org.apache.tomcat.websocket.server.WsServerContainer中的WebSocket会话数为523,符合实际活跃连接状态;
  • 自定义WebsocketServerService中存储节点活跃会话的并发映射内会话数为530,也与实际一致。
    预期所有会话属性的会话数应接近530,而非70万级别的堆积。

问题与怀疑点

导致StandardManager标准会话持续增长的原因可能是什么?推测WebSocket连接基于标准会话建立,若连接未被双方正常关闭,可能导致标准会话无法被GC回收,但不确定该推测是否准确。

目前怀疑原因是使用了常规的WebSecurityConfigurerAdapter配置(如下代码),而非Spring Security官方针对WebSocket的专属配置。当充电桩连接出现401等错误时,Socket未正常关闭,导致会话堆积无法被GC回收。

安全配置代码

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final WebsocketServerProperties websocketServerProperties;
    private final StationAuthenticationProvider stationAuthenticationProvider;
    private final AuthenticationEntryPoint authEntryPoint;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(stationAuthenticationProvider);
    }

    @Override
    @SuppressWarnings("squid:S4502")
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic()
                .authenticationEntryPoint(authEntryPoint)
                .and()
                .antMatcher(websocketServerProperties.getWebsocketPathVpn() + "/**").anonymous()
                .and()
                .antMatcher(websocketServerProperties.getWebsocketPathInternet() + "/**")
                .authorizeRequests()
                .anyRequest()
                .authenticated();
    }

}

WebSocket配置与处理器代码

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
@EnableConfigurationProperties(WebsocketServerProperties.class)
public class WebsocketServerConfig implements WebSocketConfigurer {
    public static final String[] SUPPORTED_PROTOCOLS = new String[]{.....};

    private final WebsocketServerProperties websocketServerProperties;
    private final WebsocketHandShakeInterceptor websocketHandShakeInterceptor;
    private final StationSocketHandler stationSocketHandler;

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(stationSocketHandler,
                        websocketServerProperties.getWebsocketPathVpn() + "/*",
                        websocketServerProperties.getWebsocketPathInternet() + "/*")
                .addInterceptors(websocketHandShakeInterceptor)
                .setHandshakeHandler(handshakeHandler())
                .setAllowedOrigins("*");
    }

    private HandshakeHandler handshakeHandler() {
        DefaultHandshakeHandler handler = new DefaultHandshakeHandler();
        handler.setSupportedProtocols(SUPPORTED_PROTOCOLS);
        return handler;
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(websocketServerProperties.getMaxTextMessageBufferSize());
        container.setMaxSessionIdleTimeout(websocketServerProperties.getMaxSessionIdleTimeout());
        return container;
    }

}
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor = @__({@Autowired}))
public class StationSocketHandler extends TextWebSocketHandler implements SubProtocolCapable {

    public static final String DEVICE_ID = "DEVICE_ID";
    public static final String CONNECTION_ROUTE = "CONNECTION_ROUTE";

    private final OcppMessageParser ocppMessageParser;
    private final WebsocketServerService websocketServerService;
    private final PingPongService pingPongService;

    @Override
    public void handleTextMessage(final WebSocketSession session, TextMessage textMessage) {
        OcppMessage message = ocppMessageParser.parsePayloadWithSessionId(session.getId(), textMessage.getPayload());
        websocketServerService.processMessageFromStation(message);
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        Map<String, Object> attributes = session.getAttributes();
        String deviceId = (String) attributes.get(DEVICE_ID);
        String connectionRoute = (String) attributes.get(CONNECTION_ROUTE);
        websocketServerService.connect(Route.toRoute(connectionRoute), deviceId, session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, @NonNull CloseStatus status) {
        log.info("WebSocketSession[{}][{}] closed with status {} and attributes {}",
                session.getId(), session.isOpen(), status, session.getAttributes());
        websocketServerService.disconnectSessionId(session.getId());
    }

    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        pingPongService.handlePong(session, message);
    }

    @Override
    public @NonNull
    List<String> getSubProtocols() {
        return Arrays.asList(WebsocketServerConfig.SUPPORTED_PROTOCOLS);
    }

}

排查思路与建议

原因分析方向

  • 未正常关闭的HTTP会话与WebSocket关联问题:WebSocket握手基于HTTP请求完成,Tomcat会为每个握手请求创建HTTP会话(StandardSession)。如果握手失败(如401认证失败),但HTTP会话未被正确销毁,就会导致会话堆积。常规WebSecurityConfigurerAdapter配置可能在处理认证失败时,未触发HTTP会话的销毁逻辑。
  • Spring Security对WebSocket的适配缺失:WebSocket的安全配置需要专门处理,因为握手后会切换到TCP长连接,常规HTTP安全配置可能无法正确清理握手阶段产生的HTTP会话。例如认证失败时,Security异常处理仅返回错误响应,未主动 invalidate 对应HTTP会话。
  • 会话超时配置未生效:检查Tomcat的StandardManager会话超时配置是否正确设置。若超时时间过长或清理线程未正常运行,会导致大量过期会话堆积。可通过server.servlet.session.timeout配置确认超时时间,同时查看Tomcat日志中是否有会话清理记录。

排查步骤

  • 堆转储分析会话详情:从堆转储中提取堆积的StandardSession对象,查看创建时间、最后访问时间、关联请求信息,确认这些会话是否为握手失败请求产生,以及是否存在强引用导致无法回收。
  • 模拟认证失败场景:手动模拟充电桩连接时的401情况,查看HTTP会话是否被正确销毁,检查日志中是否有会话销毁记录。
  • 切换WebSocket专属安全配置:将安全配置改为Spring Security针对WebSocket的配置方式,使用WebSocketSecurityConfigurer替代WebSecurityConfigurerAdapter处理WebSocket路径认证,观察会话堆积是否缓解。
  • 检查会话清理机制:通过JMX查看StandardManager的会话统计信息(活跃会话数、过期会话数等),验证Tomcat会话清理线程是否正常运行。
  • 拦截器中添加会话清理逻辑:在WebsocketHandShakeInterceptor的afterHandshake方法中,若握手失败,主动调用request.getSession().invalidate()销毁HTTP会话。

内容的提问来源于stack exchange,提问作者joe380

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 11:21:03