摘要:若使用連接池的方式,來管理連接對象,能極大地提高服務的吞吐量。另外每個對應一個連接池,實現了在級別的隔離,若下游的某臺提供服務的主機掛了,無效的連接最多只占用該對應的連接池,不會占用整個連接池,從而拖垮整個服務。
背景
在做服務化拆分的時候,若不是性能要求特別高的場景,我們一般對外暴露Http服務。Spring里提供了一個模板類RestTemplate,通過配置RestTemplate,我們可以快速地訪問外部的Http服務。Http底層是通過Tcp的三次握手建立連接的,若每個請求都要重新建立連接,那開銷是很大的,特別是對于消息體非常小的場景,開銷更大。
若使用連接池的方式,來管理連接對象,能極大地提高服務的吞吐量。
RestTemplate底層是封裝了HttpClient(筆者的版本是4.3.6),它提供了連接池機制來處理高并發網絡請求。
示例通常,我們采用如下的樣板代碼來構建HttpClient:
HttpClientBuilder builder = HttpClientBuilder.create(); builder.setMaxConnTotal(maxConnections).setMaxConnPerRoute(maxConnectionsPerRoute); if (!connectionReuse) { builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE); } if (!automaticRetry) { builder.disableAutomaticRetries(); } if (!compress) { builder.disableContentCompression(); } HttpClient httpClient = builder.build();
從上面的代碼可以看出,HttpClient使用建造者設計模式來構造對象,最后一行代碼構建對象,前面的代碼是用來設置客戶端的最大連接數、單路由最大連接數、是否使用長連接、壓縮等特性。
源碼分析我們進入HttpClientBuilder的build()方法,會看到如下代碼:
# 構造Http連接池管理器 final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager( RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslSocketFactory) .build()); if (defaultSocketConfig != null) { poolingmgr.setDefaultSocketConfig(defaultSocketConfig); } if (defaultConnectionConfig != null) { poolingmgr.setDefaultConnectionConfig(defaultConnectionConfig); } if (systemProperties) { String s = System.getProperty("http.keepAlive", "true"); if ("true".equalsIgnoreCase(s)) { s = System.getProperty("http.maxConnections", "5"); final int max = Integer.parseInt(s); poolingmgr.setDefaultMaxPerRoute(max); poolingmgr.setMaxTotal(2 * max); } } if (maxConnTotal > 0) { poolingmgr.setMaxTotal(maxConnTotal); } if (maxConnPerRoute > 0) { poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute); } # Http連接管理器采用連接池的方式實現 connManager = poolingmgr;
默認情況下構造出的Http連接管理器是采用連接池的方式實現的。
我們進入 PoolingHttpClientConnectionManager的代碼,其連接池的核心實現是依賴于 CPool類,而 CPool又繼承了抽象類AbstractConnPool, AbstractConnPool有@ThreadSafe的注解,說明它是線程安全類,所以 HttpClient線程安全地獲取、釋放連接都依賴于 AbstractConnPool。
接下來我來看最核心的AbstractConnPool類,以下是連接池的結構圖:
連接池最重要的兩個公有方法是 lease和release,即獲取連接和釋放連接的兩個方法。
lease 獲取連接@Override public Futurelease(final T route, final Object state, final FutureCallback callback) { Args.notNull(route, "Route"); Asserts.check(!this.isShutDown, "Connection pool shut down"); return new PoolEntryFuture (this.lock, callback) { @Override public E getPoolEntry( final long timeout, final TimeUnit tunit) throws InterruptedException, TimeoutException, IOException { final E entry = getPoolEntryBlocking(route, state, timeout, tunit, this); onLease(entry); return entry; } }; }
lease方法返回的是一個 Future對象,即需要調用 Future的get方法,才可以得到PoolEntry的對象,它包含了一個連接的具體信息。
而獲取連接是通過 getPoolEntryBlocking方法實現的,通過函數名可以知道,這是一個阻塞的方法,即該route所對應的連接池中的連接不夠用時,該方法就會阻塞,直到該 route所對應的連接池有連接釋放,方法才會被喚醒;或者方法一直等待,直到連接超時拋出異常。
private E getPoolEntryBlocking( final T route, final Object state, final long timeout, final TimeUnit tunit, final PoolEntryFuturerelease 釋放連接future) throws IOException, InterruptedException, TimeoutException { Date deadline = null; // 設置連接超時時間戳 if (timeout > 0) { deadline = new Date (System.currentTimeMillis() + tunit.toMillis(timeout)); } // 獲取連接,并修改修改連接池,所以加鎖--->線程安全 this.lock.lock(); try { // 從Map中獲取該route對應的連接池,若Map中沒有,則創建該route對應的連接池 final RouteSpecificPool pool = getPool(route); E entry = null; while (entry == null) { Asserts.check(!this.isShutDown, "Connection pool shut down"); for (;;) { // 獲取 同一狀態的 空閑連接,即從available鏈表的頭部中移除,添加到leased集合中 entry = pool.getFree(state); // 若返回連接為空,跳出循環 if (entry == null) { break; } // 若連接已過期,則關閉連接 if (entry.isExpired(System.currentTimeMillis())) { entry.close(); } else if (this.validateAfterInactivity > 0) { if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) { if (!validate(entry)) { entry.close(); } } } if (entry.isClosed()) { // 若該連接已關閉,則總的available鏈表中刪除該連接 this.available.remove(entry); // 從該route對應的連接池的leased集合中刪除該連接,并且不回收到available鏈表中 pool.free(entry, false); } else { break; } } // 跳出for循環 if (entry != null) { // 若獲取的連接不為空,將連接從總的available鏈表移除,并添加到leased集合中 // 獲取連接成功,直接返回 this.available.remove(entry); this.leased.add(entry); onReuse(entry); return entry; } // 計算該route的最大連接數 // New connection is needed final int maxPerRoute = getMax(route); // Shrink the pool prior to allocating a new connection // 計算該route連接池中的連接數 是否 大于等于 route最大連接數 final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute); // 若大于等于 route最大連接數,則收縮該route的連接池 if (excess > 0) { for (int i = 0; i < excess; i++) { // 獲取該route連接池中最不常用的空閑連接,即available鏈表末尾的連接 // 因為回收連接時,總是將連接添加到available鏈表的頭部,所以鏈表尾部的連接是最有可能過期的 final E lastUsed = pool.getLastUsed(); if (lastUsed == null) { break; } // 關閉連接,并從總的空閑鏈表以及route對應的連接池中刪除 lastUsed.close(); this.available.remove(lastUsed); pool.remove(lastUsed); } } // 該route的連接池大小 小于 route最大連接數 if (pool.getAllocatedCount() < maxPerRoute) { final int totalUsed = this.leased.size(); final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); if (freeCapacity > 0) { final int totalAvailable = this.available.size(); // 總的空閑連接數 大于等于 總的連接池剩余容量 if (totalAvailable > freeCapacity - 1) { if (!this.available.isEmpty()) { // 從總的available鏈表中 以及 route對應的連接池中 刪除連接,并關閉連接 final E lastUsed = this.available.removeLast(); lastUsed.close(); final RouteSpecificPool otherpool = getPool(lastUsed.getRoute()); otherpool.remove(lastUsed); } } // 創建新連接,并添加到總的leased集合以及route連接池的leased集合中,函數返回 final C conn = this.connFactory.create(route); entry = pool.add(conn); this.leased.add(entry); return entry; } } //route的連接池已滿,無法分配連接 boolean success = false; try { // 將該獲取連接的任務放入pending隊列 pool.queue(future); this.pending.add(future); // 阻塞等待,若在超時之前被喚醒,則返回true;若直到超時才返回,則返回false success = future.await(deadline); } finally { // In case of "success", we were woken up by the // connection pool and should now have a connection // waiting for us, or else we"re shutting down. // Just continue in the loop, both cases are checked. // 無論是 被喚醒返回、超時返回 還是被 中斷異常返回,都會進入finally代碼段 // 從pending隊列中移除 pool.unqueue(future); this.pending.remove(future); } // check for spurious wakeup vs. timeout // 判斷是偽喚醒 還是 連接超時 // 若是 連接超時,則跳出while循環,并拋出 連接超時的異常; // 若是 偽喚醒,則繼續循環獲取連接 if (!success && (deadline != null) && (deadline.getTime() <= System.currentTimeMillis())) { break; } } throw new TimeoutException("Timeout waiting for connection"); } finally { // 釋放鎖 this.lock.unlock(); } }
@Override public void release(final E entry, final boolean reusable) { // 獲取鎖 this.lock.lock(); try { // 從總的leased集合中移除連接 if (this.leased.remove(entry)) { final RouteSpecificPool總結pool = getPool(entry.getRoute()); // 回收連接 pool.free(entry, reusable); if (reusable && !this.isShutDown) { this.available.addFirst(entry); onRelease(entry); } else { entry.close(); } // 獲取pending隊列隊頭的任務(先進先出原則),喚醒該阻塞的任務 PoolEntryFuture future = pool.nextPending(); if (future != null) { this.pending.remove(future); } else { future = this.pending.poll(); } if (future != null) { future.wakeup(); } } } finally { // 釋放鎖 this.lock.unlock(); } }
AbstractConnPool其實就是通過在獲取連接、釋放連接時加鎖,來實現線程安全,思路非常簡單,但它沒有在route對應的連接池中加鎖對象,即 RouteSpecificPool的獲取連接、釋放連接操作是不加鎖的,因為已經在 AbstractConnPool的外部調用中加鎖,所以是線程安全的,簡化了設計。
另外每個route對應一個連接池,實現了在host級別的隔離,若下游的某臺提供服務的主機掛了,無效的連接最多只占用該route對應的連接池,不會占用整個連接池,從而拖垮整個服務。
以上。
原文鏈接https://segmentfault.com/a/11...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/68005.html
摘要:服務本身是一個,開起的線程數為,再加上一些其他線程,總的線程數不會超過服務內自己沒有顯示創建線程或者使用線程池。問題解決找到所在后,結局方案很簡單,只需將的通過單例的方式注入到服務中,即可解決堆外內存泄漏的問題。 內存泄漏Bug現場 一個做BI數據展示的服務在一個晚上重啟了5次,由于是通過k8s容器編排,服務掛了以后會自動重啟,所以服務還能繼續提供服務。 第一時間先上日志系統查看錯誤日...
摘要:對連接數的管理則有兩個維度,分別是全局最大數和單最大數。當請求一個連接時,會返回。而會維護與及存活時間等。最終用戶得到的是里封裝而成的連接對象。連接數達到閾值時對請求進行堵塞,并且將放入。 showImg(https://segmentfault.com/img/bVZW77?w=1217&h=886); 上圖時連接池類圖關系。PoolingHttpConnectionManager:...
摘要:壓縮和緩存機制可以有效地減少網絡訪問的流量,在提升速度和省電方面也起到了較大的作用。打開來分析一下,不了解和協議原理的請查看網絡編程一協議原理這篇文章。當然這次錯誤是正常的,百度沒理由處理我們的這次請求。 前言 上一篇我們了解了HTTP協議原理,這一篇我們來講講Apache的HttpClient和Java的HttpURLConnection,這兩種都是我們平常請求網絡會用到的。無論我們...
摘要:在中也可以直接使用返回的是,然后通過來獲取結果阻塞線程,從中獲取結果四一點嘮叨非常的年輕,網絡資料不多,且代碼非常精細和復雜,目前來看底層應該是使用了線程池搭配進行異步通訊。 零 前期準備 0 版本 JDK 版本 : OpenJDK 11.0.1 IDE : idea 2018.3 1 HttpClient 簡介 java.net.http.HttpClient 是 jdk11 中正式...
閱讀 3456·2023-04-26 00:39
閱讀 4059·2021-09-22 10:02
閱讀 2543·2021-08-09 13:46
閱讀 1102·2019-08-29 18:40
閱讀 1447·2019-08-29 18:33
閱讀 775·2019-08-29 17:14
閱讀 1517·2019-08-29 12:40
閱讀 2979·2019-08-28 18:07