接上篇:https://www.cnblogs.com/Hleaves/p/11284316.html
環境:jdk1.8 + springboot 2.1.1.RELEASE + feign-hystrix 10.1.0,以下僅為個人理解,如果異議,歡迎指正。
上篇中,設置tomcat的max-connection=1
因為之前一直理解的這個參數是同一時刻可以處理的http請求的數量,比如說我用瀏覽器‘同時’發起2個http請求(可以通過debug在controller層斷點之后再發起另一個請求)A、B,此時A請求只要響應之后(忽略tcp連接是否釋放),B請求正常來說就可以立即被服務端處理,事實並不是這樣的,B請求一直在等待連接,但是再次發起一個請求C,C請求可以立即被處理,B請求還是一直在等待,只能串行執行,這個到底是為什么呢?后來查了一些資料,說是http1.1的請求頭中默認加了Connection:keep-alive
就是這個,使得在一個請求完成之后,不會馬上釋放tcp連接,發起其他請求(同一個url)時,會復用這個tcp連接(tcp的長連接),而且瀏覽器對同一個域名的tcp長連接最大數量有限制(具體自行查資料吧,參考:https://www.jianshu.com/p/1d535bd7fefb),所以建議不同服務使用多個域名部署,那tcp復用到底是怎么實現的呢?瀏覽器沒有辦法直觀的看到如何去選擇空閑的長連接的,feign的調用默認使用的也是http1.1,我們可以參考這個調用過程去探尋一下tcp連接和釋放,整個生命周期是怎樣的,feign使用的默認的Client.Default,請求方式HttpURLConnection,不是ApacheHttpClient,也不是OkHttpClient
-------------------下面是使用feign實驗的結果,均為debug出的結果,可能中間有些理解的不到位的地方--------------------
先說一下大致流程,從feign.Client.Default#execute開始,
public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = this.convertAndSend(request, options); return this.convertResponse(connection, request); }
1. this.convertAndSend(request, options);准備connection的基本信息,比如連接超時時間,讀取超時時間等等,此時並沒有建立tcp連接
2.this.convertResponse(connection, request);
2.1 先調用HttpClient.New(..)獲取一個可用的httpClient,這是一個靜態方法,這個方法中會先去KeepAliveCache中查找是否有可用的httpClient,如果有的話直接拿過來用
2.2 沒有的情況下,調用HttpClient的構造,新建一個httpClient對象,這個構造方法的最后一行調用了openServer()方法,這個時候才會去真正的建立tcp連接
2.3 有了連接,這個時候可以向server端寫數據了,這個時候會調用HttpURLConnection的writeRequests,此方法會判斷httpClient.isKeepAlive的,默認是true,所以在請求頭中加上了Connection:keep-alive
2.4 寫數據完成之后,會調用HttpClient.parseHTTP(..)方法,去解析服務端響應的數據,包括響應頭,此時如果響應頭中包含了Connection:keep-alive,並且還設置了Keep-Alive:timeout=xx,max=xxx(client端的‘空閑’超時時間,默認5s,最多處理多少個請求,默認5個),會將這個值覆蓋掉剛才的httpClient對象的keepAliveTimeout和keepAliveConnections屬性
2.5 讀取完數據之后,最終會調用到httpClient.finished方法,划重點 ,這個地方是實現tcp連接復用的關鍵
protected static KeepAliveCache kac = new KeepAliveCache();
private static boolean keepAliveProp = true;
......
public void finished() { if (!this.reuse) { --this.keepAliveConnections; this.poster = null; if (this.keepAliveConnections > 0 && this.isKeepingAlive() && !this.serverOutput.checkError()) { this.putInKeepAliveCache(); } else { this.closeServer(); } } } protected synchronized void putInKeepAliveCache() { if (this.inCache) { assert false : "Duplicate put to keep alive cache"; } else { this.inCache = true; kac.put(this.url, (Object)null, this); } }
如果條件不成立,則直接close掉當前連接,就不會出現復用的情況了;反之會將當前對象存到KeepAliveCache中,KeepAliveCache繼承了HashMap,本質上就是一個map,這里的key是host+port,跟前面說的瀏覽器是根據域名划分的好像是一致的(這個沒有做深入的了解),我們看下KeepAliveCache的put操作都做了什么
public synchronized void put(URL var1, Object var2, HttpClient var3) { boolean var4 = this.keepAliveTimer == null; if (!var4 && !this.keepAliveTimer.isAlive()) { var4 = true; } if (var4) { this.clear(); AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ThreadGroup var1 = Thread.currentThread().getThreadGroup(); for(ThreadGroup var2 = null; (var2 = var1.getParent()) != null; var1 = var2) { } KeepAliveCache.this.keepAliveTimer = new Thread(var1, KeepAliveCache.this, "Keep-Alive-Timer"); KeepAliveCache.this.keepAliveTimer.setDaemon(true); KeepAliveCache.this.keepAliveTimer.setPriority(8); KeepAliveCache.this.keepAliveTimer.setContextClassLoader((ClassLoader)null); KeepAliveCache.this.keepAliveTimer.start(); return null; } }); } KeepAliveKey var5 = new KeepAliveKey(var1, var2); ClientVector var6 = (ClientVector)super.get(var5); if (var6 == null) { int var7 = var3.getKeepAliveTimeout(); var6 = new ClientVector(var7 > 0 ? var7 * 1000 : 5000); var6.put(var3); super.put(var5, var6); } else { var6.put(var3); } }
這幾個類的關系和功能如下圖所示,到此為止,為什么會復用,以及什么條件下可以復用基本上都明了了
------------延伸問題--------------------
feign中默認使用的jdk1.8中的HttpClient,每個請求都會有一個httpClient,一個httpClient都持有一個tcp長連接,所以tcp長連接的復用,其實就是httpClient的復用
1.首先是為什么feign調用會默認在請求頭中加上Connection:keep-alive?
sun.net.www.http.HttpClient
static { String var0 = (String)AccessController.doPrivileged(new GetPropertyAction("http.keepAlive")); String var1 = (String)AccessController.doPrivileged(new GetPropertyAction("sun.net.http.retryPost")); String var2 = (String)AccessController.doPrivileged(new GetPropertyAction("jdk.ntlm.cache")); String var3 = (String)AccessController.doPrivileged(new GetPropertyAction("jdk.spnego.cache")); if (var0 != null) { keepAliveProp = Boolean.valueOf(var0); } else { keepAliveProp = true; } if (var1 != null) { retryPostProp = Boolean.valueOf(var1); } else { retryPostProp = true; } ....... }
可以看到這是取得一個系統配置http.keepAlive,如果沒有增加或修改這個配置,默認就是keepAliveProp=true的,這個就是2.3中用來判斷的其中一個條件
2.tcp連接復用的話,到底是誰先close的,server還是client端?
再上一篇中分析了springboot server 有一個connection-timeout的配置,默認是60s,就是client端請求完成之后,如果server端正常響應200,server端的org.apache.tomcat.util.net.NioEndpoint.Poller#timeout會判斷當前的socket是否已超時,判斷的依據是 (當前系統時間-最后一次讀寫的時間>connection-timeout時間),如果超時,就會close掉當前的socket,但是,在未達到超時時間時,通過命令行查看tcp的狀態,發現服務端的端口狀態是CLOSE_WAIT的,也就是說client已經主動關閉了連接,到底是什么時候在哪里關閉的連接?
在流程的2.5中,在緩存httpClient時會在KeepAliveEntry中記錄一下當前的系統時間,標記為idleStartTime,顧名思義,就是你開始閑着的時間,哈哈,2.5中的Keep-Alive-Timer線程的run方法會去判斷此httpClient是不是已經閑夠了,閑夠了就把它close掉,這個時間默認是5s,可以通過流程2.4,在response中修改這個值,不管是不是用的緩存的httpClient,每次請求完成都會調用2.5的finished方法,所以這個idleStartTime每次都會更新的。所以現在client端的‘超時’時間是5s,server端的超時時間是60s,所以就會出現client端先close掉,然后server端一直等到60s才去close的情況,所以server端的這個60s是不是有點多余了。。。。。。
ps: server端會判斷即將響應的結果,如果是異常的,比如是以下的狀態碼,則會將scoket的狀態標記為error,此時即使client設置了keepAlive,server也會自動close掉當前連接
return status == 400 /* SC_BAD_REQUEST */ || status == 408 /* SC_REQUEST_TIMEOUT */ || status == 411 /* SC_LENGTH_REQUIRED */ || status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ || status == 414 /* SC_REQUEST_URI_TOO_LONG */ || status == 500 /* SC_INTERNAL_SERVER_ERROR */ || status == 503 /* SC_SERVICE_UNAVAILABLE */ || status == 501 /* SC_NOT_IMPLEMENTED */;
3. tcp連接復用的之http1.1和http2.0
參考:https://blog.csdn.net/CrankZ/article/details/81239654
http1.1的復用,是串行的,一個tcp連接,只能等一個請求完成才可以給另一個請求使用
http2.0的復用,可以並行處理,增加了HttpStream,一個tcp連接中可以同時處理多個HttpStream(同步代碼塊實現的),但是只支持https,server端和client端都要做改動,只是目前了解到的一丟丟而已,后續再做補充