You need to enable JavaScript to run this app. 使用Okhttp RealWebSocket出现OOM,RealCall$AsyncCall实例量过高问题求助
优惠活动
大模型
产品
解决方案
定价
更多

使用Okhttp RealWebSocket出现OOM,RealCall$AsyncCall实例量过高问题求助

问题描述

项目使用okhttp创建WebSocket连接,生产环境近期触发OOM。经IBM HeapAnalyzer分析堆内存,故障由okhttp3/RealCall$AsyncCall实例过多导致,实例数高达1115417个,暂无法定位根因。

运行环境

  • jdk1.8.0_181
  • okhttp3.12.1
相关代码实现

WebSocketConnection类

public class WebSocketConnection extends WebSocketListener {

    private static final Logger log = LoggerFactory.getLogger(WebSocketConnection.class);

    private static int connectionCounter = 0;

    public enum ConnectionState {
        IDLE, DELAY_CONNECT, CONNECTED, CLOSED_ON_ERROR
    }

    private WebSocket webSocket = null;

    private volatile long lastReceivedTime = 0;

    private volatile ConnectionState state = ConnectionState.IDLE;
    private int delayInSecond = 0;

    private final WebsocketRequest request;
    private final Request okhttpRequest;
    private final IWebSocketWatchDog watchDog;
    private final int connectionId;
    private final boolean autoClose;
    private  String listenKey;

    private String subscriptionUrl = BinanceApiConstants.WS_API_BASE_URL;

    WebSocketConnection(String apiKey, String secretKey, SubscriptionOptions options, WebsocketRequest request,
                        WebSocketWatchDog watchDog) {
        this(apiKey, secretKey, options, request, watchDog, false);
    }

    WebSocketConnection(String apiKey, String secretKey, SubscriptionOptions options, WebsocketRequest request,
                        IWebSocketWatchDog watchDog, boolean autoClose) {
        this.connectionId = WebSocketConnection.connectionCounter++;
        this.request = request;
        this.autoClose = autoClose;

        this.okhttpRequest = StringUtils.isNotBlank(options.getUri()) ? new Request.Builder().url(options.getUri()).build()
                : new Request.Builder().url(subscriptionUrl).build();
        this.watchDog = watchDog;
        log.info("[Sub] Connection [id: " + this.connectionId + "] created for " + request.name);
    }
    WebSocketConnection(String apiKey, String secretKey,String listenKey, SubscriptionOptions options, WebsocketRequest request,
                        IWebSocketWatchDog watchDog, boolean autoClose) {
        this.connectionId = WebSocketConnection.connectionCounter++;
        this.request = request;
        this.autoClose = autoClose;
        this.listenKey = listenKey;

        this.okhttpRequest = StringUtils.isNotBlank(options.getUri()) ? new Request.Builder().url(options.getUri()).build()
                : new Request.Builder().url(subscriptionUrl).build();
        this.watchDog = watchDog;
        log.info("[Sub] Connection [id: " + this.connectionId + "] created for " + request.name);
    }
    int getConnectionId() {
        return this.connectionId;
    }

    void connect() {
        if (state == ConnectionState.CONNECTED) {
            log.info("[Sub][" + this.connectionId + "] Already connected");
            return;
        }
        webSocket = RestApiInvoker.createWebSocket(okhttpRequest, this);
    }

    void reConnect(int delayInSecond) {
        log.warn("[Sub][" + this.connectionId + "] Reconnecting after " + delayInSecond + " seconds later");
        if (webSocket != null) {
            webSocket.cancel();
            webSocket = null;
        }
        this.delayInSecond = delayInSecond;
        state = ConnectionState.DELAY_CONNECT;
    }

    void reConnect() {
        if (delayInSecond != 0) {
            delayInSecond--;
        } else {
            connect();
        }
    }

    long getLastReceivedTime() {
        return this.lastReceivedTime;
    }
    public void setLastReceivedTime(long currentTimeMillis) {
        this.lastReceivedTime=currentTimeMillis;
    }

    void send(String str) {
        boolean result = false;
        log.debug("[Send]{}", str);
        if (webSocket != null) {
            result = webSocket.send(str);
        }
        if (!result) {
            log.error("[Sub][" + this.connectionId + "] Failed to send message");
            closeOnError();
        }
    }

    @Override
    public void onMessage(WebSocket webSocket, String text) {
        super.onMessage(webSocket, text);
        lastReceivedTime = System.currentTimeMillis();

        log.debug("[On Message]:{}", text);
        try {
            JsonWrapper jsonWrapper = JsonWrapper.parseFromString(text);

            if (jsonWrapper.containKey("result") || jsonWrapper.containKey("id")) {
                // onReceiveAndClose(jsonWrapper);
            } else {
                onReceiveAndClose(jsonWrapper);
            }

        } catch (Exception e) {
            log.error("[On Message][{}]: catch exception:", connectionId, e);
            closeOnError();
        }
    }

    private void onError(String errorMessage, Throwable e) {
        if (request.errorHandler != null) {
            BinanceApiException exception = new BinanceApiException(BinanceApiException.SUBSCRIPTION_ERROR, errorMessage, e);
            request.errorHandler.onError(exception);
        }
        log.error("[Sub][" + this.connectionId + "] " + errorMessage);
    }

    private void onReceiveAndClose(JsonWrapper jsonWrapper) {
        onReceive(jsonWrapper);
        if (autoClose) {
            close();
        }
    }

    @SuppressWarnings("unchecked")
    private void onReceive(JsonWrapper jsonWrapper) {
        Object obj = null;
        try {
            obj = request.jsonParser.parseJson(jsonWrapper);
        } catch (Exception e) {
            onError("Failed to parse server's response: " + e.getMessage(), e);
        }
        request.updateCallback.onReceive(obj);
    }

    public ConnectionState getState() {
        return state;
    }

    public void close() {
        log.error("[Sub][" + this.connectionId + "] Closing normally");
        webSocket.cancel();
        webSocket = null;
        watchDog.onClosedNormally(this);
    }

    @Override
    public void onClosed(WebSocket webSocket, int code, String reason) {
        super.onClosed(webSocket, code, reason);
        if (state == ConnectionState.CONNECTED) {
            state = ConnectionState.IDLE;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void onOpen(WebSocket webSocket, Response response) {
        super.onOpen(webSocket, response);
        this.webSocket = webSocket;
        log.info("[Sub][" + this.connectionId + "] Connected to server");
        if (watchDog instanceof SpotWebSocketWatchDog) {
            System.out.println();
        }
        watchDog.onConnectionCreated(this);
        if (request.connectionHandler != null) {
            request.connectionHandler.handle(this);
        }
        state = ConnectionState.CONNECTED;
        lastReceivedTime = System.currentTimeMillis();
    }

    @Override
    public void onFailure(WebSocket webSocket, Throwable t, Response response) {
        onError("Unexpected error: " + t.getMessage(), t);
        closeOnError();
    }

    private void closeOnError() {
        if (webSocket != null) {
            this.webSocket.cancel();
            state = ConnectionState.CLOSED_ON_ERROR;
            log.error("[Sub][" + this.connectionId + "] Connection is closing due to error");
        }
    }

    public String getListenKey() {
        return listenKey;
    }
}

SpotWebSocketWatchDog类

class SpotWebSocketWatchDog implements IWebSocketWatchDog{

    private final CopyOnWriteArrayList<WebSocketConnection> TIME_HELPER = new CopyOnWriteArrayList<>();
    private final SubscriptionOptions options;
    SpotWebSocketWatchDog(SubscriptionOptions subscriptionOptions) {
        this.options = Objects.requireNonNull(subscriptionOptions);
        long t = 1_000;
        ScheduledExecutorService exec = Executors.newScheduledThreadPool(1);
        exec.scheduleAtFixedRate(() -> {
            TIME_HELPER.forEach(connection -> {
                if (connection.getState() == WebSocketConnection.ConnectionState.CONNECTED) {
                    // Check response
                    if (options.isAutoReconnect()) {
                        long ts = System.currentTimeMillis() - connection.getLastReceivedTime();
                        if (ts > options.getReceiveLimitMs()) {
                            log.warn("[Sub SpotWebSocketWatchDog][" + connection.getConnectionId() + "] No response from server, \n send PONG to server, and update LastReceivedTime to now, jsut test!!!");
                            connection.send(Channels.userDataChannel(connection.getListenKey()));
                            connection.setLastReceivedTime(System.currentTimeMillis());
                        }
                    }
                } else if (connection.getState() == WebSocketConnection.ConnectionState.DELAY_CONNECT) {
                    connection.reConnect();
                } else if (connection.getState() == WebSocketConnection.ConnectionState.CLOSED_ON_ERROR) {
                    if (options.isAutoReconnect()) {
                        connection.reConnect(options.getConnectionDelayOnFailure());
                    }
                }
            });
        }, t, t, TimeUnit.MILLISECONDS);
        Runtime.getRuntime().addShutdownHook(new Thread(exec::shutdown));
    }

    @Override
    public void onConnectionCreated(WebSocketConnection connection) {
        TIME_HELPER.addIfAbsent(connection);
    }
    @Override
    public void onClosedNormally(WebSocketConnection connection) {
        TIME_HELPER.remove(connection);
    }

    private static volatile SpotWebSocketWatchDog instance;
    public static SpotWebSocketWatchDog getInstance(SubscriptionOptions options) {
        if (null == instance) {
            synchronized (SpotWebSocketWatchDog.class) {
                if (null == instance) {
                    instance = new SpotWebSocketWatchDog(options);
                }
            }
        }
        return SpotWebSocketWatchDog.instance;
    }

}

WebSocketStreamClientImpl类

public class WebSocketStreamClientImpl implements SubscriptionClient {

    private final SubscriptionOptions options;

    private IWebSocketWatchDog watchDog;
    private IWebSocketWatchDog watchSpotDog;
    private IWebSocketWatchDog watchSpotQuotesDog;

    private final WebsocketRequestImpl requestImpl;

    private final List<WebSocketConnection> connections = new LinkedList<>();

    private final String apiKey;

    private final String secretKey;

    WebSocketStreamClientImpl(String apiKey, String secretKey, SubscriptionOptions options) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.watchDog = null;
        this.watchSpotDog = null;
        this.watchSpotQuotesDog = null;
        this.options = Objects.requireNonNull(options);

        this.requestImpl = new WebsocketRequestImpl();
    }

    private <T> void createConnectionSpot(WebsocketRequest<T> request, boolean autoClose, String listenKey) {
        if (watchSpotDog == null) {
            watchSpotDog = SpotWebSocketWatchDog.getInstance(options);
        }
        WebSocketConnection connection = new WebSocketConnection(apiKey, secretKey, listenKey, options, request, watchSpotDog,
                autoClose);
        if (autoClose == false) {
            connections.add(connection);
        }
        connection.connect();
    }

    private <T> void createConnectionSpot(WebsocketRequest<T> request, boolean autoClose) {
        if (watchSpotQuotesDog == null) {
            watchSpotQuotesDog =  SpotQuotesWebSocketWatchDog.getInstance(options);
        }
        WebSocketConnection connection = new WebSocketConnection(apiKey, secretKey, options, request, watchSpotQuotesDog,
                autoClose);
        if (autoClose == false) {
            connections.add(connection);
        }
        connection.connect();
    }

    private <T> void createConnection(WebsocketRequest<T> request, boolean autoClose) {
        if (watchDog == null) {
            watchDog = new WebSocketWatchDog(options);
        }
        WebSocketConnection connection = new WebSocketConnection(apiKey, secretKey, options, request, watchDog,
                autoClose);
        if (autoClose == false) {
            connections.add(connection);
        }
        connection.connect();
    }

    private <T> void createConnection(WebsocketRequest<T> request) {
        createConnection(request, false);
    }

    private <T> void createConnectionSpot(WebsocketRequest<T> request, String listenKey) {
        createConnectionSpot(request, false, listenKey);
    }

    private <T> void createConnectionSpot(WebsocketRequest<T> request) {
        createConnectionSpot(request, false);
    }

    @Override
    public void unsubscribeAll() {
        for (WebSocketConnection connection : connections) {
            watchDog.onClosedNormally(connection);
            watchSpotDog.onClosedNormally(connection);
            watchSpotQuotesDog.onClosedNormally(connection);
            connection.close();
        }
        connections.clear();
    }
 @Override
    public void subCashBalanceUpdateEvent(String cashListenKey,
                                          SubscriptionListener<CashBalanceUpdate> subscriptionListener,
                                          SubscriptionErrorHandler errorHandler) {
        createConnection(
                requestImpl.subCashBalanceUpdateEvent(cashListenKey, subscriptionListener, errorHandler));
    }

   
    @Override
    public void subscribeSpotDataEvent(String listenKey, SubscriptionListener<SpotDataUpdateEvent> callback, SubscriptionErrorHandler errorHandler) {
        createConnectionSpot(
                requestImpl.subscribeSpotDataEvent(listenKey, callback, errorHandler), listenKey);
    }

    @Override
    public void subscribeSpotBookDepthEvent(String symbol, Integer limit, SubscriptionListener<SpotOrderBookEvent> subscriptionListener, SubscriptionErrorHandler errorHandler) {
        createConnectionSpot(
                requestImpl.subscribeSpotBookDepthEvent(symbol, limit, subscriptionListener, errorHandler));
    }

}
根因定位
  • 无限重连导致请求爆炸:WatchDog每秒调度一次,当网络异常/服务端不可用导致连接连续失败时,每秒会创建一个新的WebSocket连接请求,每个请求对应一个RealCall$AsyncCall实例,没有重试上限的情况下实例数会无限累加。
  • OkHttpClient未复用:若RestApiInvoker.createWebSocket内部每次创建新的OkHttpClient实例,每个实例持有的请求队列、线程池资源不会被主动回收,会加剧内存泄漏。
  • 状态流转无原子性保障:ConnectionState仅用volatile修饰,多线程(WatchDog调度线程、okhttp回调线程)并发修改状态时会出现错乱,可能重复触发connect()调用,重复创建请求实例。
  • 失效连接未清理:仅正常关闭时才会将连接从WatchDog的连接列表中移除,异常状态的连接会一直留在列表中被反复调度重连,持续创建新的请求实例。
修复方案
  • 新增重连次数上限:给每个连接增加重试次数统计,超过阈值后将连接从WatchDog列表中移除,停止重试,避免无限创建请求。
  • 复用OkHttpClient单例:全局仅初始化一个OkHttpClient实例,所有WebSocket请求复用该实例,避免重复创建资源。
  • 保障状态流转原子性:给连接状态修改逻辑加锁,避免并发修改导致的状态错乱,connect()方法执行前增加状态二次校验,避免重复调用。
  • 增加异常连接清理逻辑:连接连续失败达到阈值、或长期处于非活跃状态时,主动从WatchDog列表和客户端连接列表中移除,释放关联资源。
  • 新建连接前强制清理旧资源:每次调用connect()前,先强制cancel旧的WebSocket实例,清空关联的请求资源。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 05:12:01