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就有這種功能,所以我們可以專門搭建一個圖片服務器。