FastDFS大量請求時報ClientAbortException問題解決記錄

FastDFS大量請求時異常解決記錄

起因

最近做畢業設計,圖片文件的保存用到了FastDFS,普通地用發現並沒有什麼問題,但是當大量的圖片請求涌向FastDFS時,會報ClientAbortException,具體如下:

org.apache.catalina.connector.ClientAbortException: java.io.IOException: 你的主機中的軟件中止了一個已建立的連接。
	at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:356)
	at org.apache.catalina.connector.OutputBuffer.flushByteBuffer(OutputBuffer.java:808)
	at org.apache.catalina.connector.OutputBuffer.append(OutputBuffer.java:713)
	at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:391)
	at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:369)
	at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:96)
	at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1793)
	at org.apache.commons.io.IOUtils.copyLarge(IOUtils.java:1769)
	at org.apache.commons.io.IOUtils.copy(IOUtils.java:1744)
	at cn.dmall.common.util.FastDFSClient.downloadFile(FastDFSClient.java:146)
	at cn.dmall.manager.controller.ImageController.imageShow(ImageController.java:68)
	at sun.reflect.GeneratedMethodAccessor104.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:230)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:192)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:165)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80)
	at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:624)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:783)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:798)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1441)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:748)

原因

先提前說一下原因,是因爲FastDFS的server端設置最大連接數爲256,當超過這個連接數後,就會抱着個異常。

解決方案

方法1:最簡單粗暴的就是增加server端的max_connections參數。

提示:如果trackerServer是你自己創建的(不是FastDFS客戶端在內部創建的,具體可以“排查過程”),它被用完之後不會主動釋放。所以,如果每次來一個連接都去新建一個trackerServer,而這些trackerServer不被釋放,那麼當第256次,就會達到最大連接數,就會報異常,建議一定優化一下。我的解決方案是對trackerServer連接進行了“池化”,建立一個連接池,對創建完畢的trackerServer連接進行復用。

方法2:比較方便的是nginx+FastDFS,nginx配置緩存,然後注意一下上面的提示即可。

排查過程

問題復現與分析

使用大量數據進行訪問到報錯

在win10環境下,使用netstat -an | find "你的ip" \c命令查看到對FastDFS的連接數

發現連接到tracker的數量爲255,並且都爲established,猜測連接到tracker的連接達到了服務端設定的連接最大值了

追蹤源碼

爲了對剛剛的猜測進行檢驗,我去翻了FastDFS官方client的源碼,發現自己使用trackerClient.getConnection();獲取TrackerServer的時候,會給連接用的socket設置ReuseAddress和SoTimeout(注意soTimeout指的是read的超時時間)

該部分源碼如下:

public TrackerServer getConnection(int serverIndex) throws IOException {
        Socket sock = new Socket();
        sock.setReuseAddress(true);
        sock.setSoTimeout(ClientGlobal.g_network_timeout);
        sock.connect(this.tracker_servers[serverIndex], ClientGlobal.g_connect_timeout);
        return new TrackerServer(sock, this.tracker_servers[serverIndex]);
    }

然後去查看訪問一個文件的一整個流程的源碼,發現像這種我們自己trackerClient.getConnection()獲得的trackerServer不會被關閉。

使用連接池解決

由於一開始是trackerServer連接太多導致的異常,所以我準備對trackerServer連接進行池化,FastDfsConnectionPool實現如下所示:

package cn.dmall.common.util;

import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;

import java.io.IOException;
import java.net.Socket;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class FastDfsConnectionPool {

    private static final String CONFIG_FILENAME = "fdfs_client.conf";

    private static BlockingQueue<TrackerServer> trackerServerPool =
            new LinkedBlockingQueue<>();

    private static final int maxTrackerConn = Constant.getInt("fastdfs.tracker.maxConn", 255);

    private static AtomicInteger currentTrackerConn = new AtomicInteger(0);

    private static TrackerClient trackerClient = null;

    static {
        try {
            ClientGlobal.init(CONFIG_FILENAME);
            trackerClient = new TrackerClient();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static TrackerServer borrowTrackerServer() throws InterruptedException, IOException {
        TrackerServer trackerServer = trackerServerPool.poll(2, TimeUnit.SECONDS);
        if (trackerServer == null) {
            trackerServer = getNewTrackerConn();
        } else {
            if (!isUsable(trackerServer)) {
                trackerServer.close();
                trackerServer = getNewTrackerConn();
            }
        }
        return trackerServer;
    }

    /**
     * trackerConn獲取新連接
     * 如果當前連接數小於連接最大數
     * 則新建一個連接
     * 否則從pool中獲取或者等待超時
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    private static TrackerServer getNewTrackerConn() throws IOException, InterruptedException {
        TrackerServer trackerServer = null;
        int current = currentTrackerConn.get();
        if (current < maxTrackerConn) {
//                新建一個新連接
            trackerServer = trackerClient.getConnection();
        } else {
//                從pool中獲取,等待其他人把鏈接使用完畢後還回來
            trackerServer = trackerServerPool.poll(2,TimeUnit.SECONDS);
        }
        if (trackerServer == null) {
            throw new IllegalStateException("borrowTrackerServer failed!");
        }
        return trackerServer;
    }

    private static boolean isUsable(TrackerServer conn) throws IOException {
        if (conn == null) {
            return false;
        }
        Socket socket = conn.getSocket();
        return socket != null
                && socket.isBound()
                && !socket.isClosed()
                && socket.isConnected()
                && !socket.isInputShutdown()
                && !socket.isOutputShutdown();
    }

    public static void releaseTrackerServer(TrackerServer conn) throws IOException {
        if (!isUsable(conn)) {
            return;
        }
        trackerServerPool.add(conn);
    }

    public static void close() throws IOException {
        for (int i = 0; i < trackerServerPool.size(); i++) {
            TrackerServer trackerServer = trackerServerPool.poll();
            if (trackerServer != null) {
                trackerServer.close();
            }
        }
        currentTrackerConn.set(0);
    }
}

出現的問題

經過對trackerServer的池化後,對trackerServer連接可以進行復用了,實際使用過程中trackerServer連接從255個降到了6個。但是又出現了問題,也就是storageServer的連接出現了同樣的問題——請求數量大於了max_connections值導致報了同樣的異常。

這裏引發了思考:

這裏不同於trackerServer,因爲trackerServer的時候是有大量空閒連接但是沒用,我們用連接池解決了複用空閒連接的問題;而storageServer這邊是沒用空閒連接(storageServer是用完後就關閉的),所以問題就成了“storageServer機器最大能承受的連接數是多少”,而這個問題不是池化或者複用能解決的。

最後:我覺得可以對圖片進行緩存,這樣就不必去查詢FastDFS了,nginx+FastDFS就有這種功能,所以我們可以專門搭建一個圖片服務器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章