springcloud-單個服務無法處理新的請求

前言:
最近生產上出現一個問題,描述如下:
springcloud分佈式環境下,服務B無法處理新進來的請求,且zuul服務一直在走降級邏輯。
服務調用情況:zuul->服務A->服務B

問題分析:
1.首先查看tomcat連接是不是滿了,通過netstat -nat|grep -i “服務B端口號”|grep ESTABLISHED|wc -l,連接數爲300多,tomcat默認最大線程數配置是200,說明所有tomcat線程可能已經被使用。
2.通過jvisualvm工具,查看服務B的線程數確實在200以上,證明了第一點分析,dump內存線程,發現問題的原因是線程卡在了一段代碼執行上。

問題已經改了,但是感覺還是沒有完全解決完:
1.是什麼原因導致zuul服務走降級邏輯?
2.爲什麼zuul服務已經走降級邏輯了,但是內部服務B的tomcat連接卻還佔滿着不釋放?

針對問題1
1.根據debug了下,發現是報了hystrix超時異常導致走的降級邏輯,查看超時配置是30秒:

hystrix:
  command:
    A:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 30000

針對問題2
1.查看ribbon讀取超時配置:ribbon.ReadTimeout: 60000
2.當服務之間建立連接之後調用超過了60秒,就會報ribbon的socket read timeout異常,結合問題1分析,發現zuul請求服務A的hystrix超時配置小於ribbon的讀取超時配置,在這種情況下,當前請求超過20秒,該請求就會走降級邏輯,但是服務A調用服務B,還沒有到達ribbon的讀取超時時間,所以連接還保持着。
結論:這種配置是有點不太合理,建議服務之間調用超時配置小於hystrix超時配置。

這個問題的分析過程中涉及到了tomcat連接數及線程數概念,後來我就想:
那什麼時候tomcat連接數纔算真的滿了?如果不能建立新的請求,那這時會報錯麼?報什麼錯?

帶着疑惑寫代碼做了下驗證:
1.自定義tomcat容器配置:

@Component
public class MyEmbeddedServletContainerFactory extends TomcatEmbeddedServletContainerFactory {
    @Override
    public EmbeddedServletContainer getEmbeddedServletContainer(ServletContextInitializer... initializers){
        //設置端口
        this.setPort(8081);
        return super.getEmbeddedServletContainer(initializers);
    }

    @Override
    protected void customizeConnector(Connector connector){
        super.customizeConnector(connector);
        Http11NioProtocol protocol = (Http11NioProtocol)connector.getProtocolHandler();
        //設置最大連接數,默認10000
        protocol.setMaxConnections(50);
        //設置全連接隊列大小,默認100
        protocol.setAcceptCount(200);
        //設置最大線程數,默認200
        protocol.setMaxThreads(99);
        //設置連接超時,默認20000ms,tomcat讀取超時配置也是使用的這個值
        protocol.setConnectionTimeout(100);
    }
}

2.編寫controller:

@RestController
public class ConnectTest {
    private AtomicInteger count = new AtomicInteger();
    @GetMapping("/test")
    public void test(){
        System.out.println(count.decrementAndGet());
        try {
            //休息較長時間,讓線程阻塞掛起
            Thread.sleep(1000* 10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.編寫測試代碼:

public class ControllerTest {
	/**線程數**/
    private static final int THREAD_SIZE = 100;
    public static void main(String[] args){
        String url = "http://127.0.0.1:8081/test";
        for (int i =0; i < THREAD_SIZE; i++){
            Thread t = new Thread(()->{
                RestTemplate template = new RestTemplate();
                template.getForEntity(url , Object.class);
            });
            t.start();
        }
    }
}

測試開始:
1.設置THREAD_SIZE=149(tomcat最大連接數+tomcat最大處理線程數)
預期:全部建連成功
結果:所有請求全部建立連接,符合預期
2.設置THREAD_SIZE=150(tomcat最大連接數+tomcat最大處理線程數)+1
預期:最後一個執行線程會建連失敗
結果:所有請求全部建立連接,不符合預期,百度了下網上也有提到accpetCount也會影響連接的上限
3.設置THREAD_SIZE=250(tomcat最大連接數+tomcat全連接隊列大小)
預期:全部建連成功
結果:所有請求全部建立連接,符合預期
4.設置THREAD_SIZE=251(tomcat最大連接數+tomcat全連接隊列大小)+1
預期:最後一個執行線程會建連失敗
結果:最後一個線程請求報錯,服務端返回connect refused錯誤,符合預期

經過反覆測試,不斷修改配置以及測試代碼最終得到一些結論:
1.tomcat容器可以接受的最大連接數計算:最大連接數+(全對列大小/最大線程數 其中較大的那個值)
2.當連接滿了之後,服務端會拒絕客戶端的請求,客戶端報:connect refused錯誤。

擴展學習:

socket各種超時
1.socket連接超時

摘自java.Socket.connect方法的一段源碼:
 * Connects this socket to the server with a specified timeout value.
 * A timeout of zero is interpreted as an infinite timeout. The connection
 * will then block until established or an error occurs.

public void connect(SocketAddress endpoint, int timeout) throws IOException

2.socket讀超時

摘自java.Socket.setSoTimeout方法的一段源碼:
 *  Enable/disable {@link SocketOptions#SO_TIMEOUT SO_TIMEOUT}
 *  with the specified timeout, in milliseconds. With this option set
 *  to a non-zero timeout, a read() call on the InputStream associated with
 *  this Socket will block for only this amount of time.  If the timeout
 *  expires, a <B>java.net.SocketTimeoutException</B> is raised, though the
 *  Socket is still valid. The option <B>must</B> be enabled
 *  prior to entering the blocking operation to have effect. The
 *  timeout must be {@code > 0}.
 *  A timeout of zero is interpreted as an infinite timeout.

public synchronized void setSoTimeout(int timeout) throws SocketException

3.socket寫超時

socket沒有寫入超時這個概念,可以自己封裝實現,例如tomcat1.8使用Nio2SocketWrapper自己封裝了寫超時。

留下問題(方便自己回過頭看時想一想):

1.tcp長連接和短連接區別?
2.怎麼理解無狀態性?
3.什麼是全連接隊列?
4.全連接隊列大小取值?
全連接隊列的大小未必是backlog的值,它是backlog與somaxconn(一個os級別的系統參數)的較小值

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