一、URL 簡介

在闡述地址推送性能的具體優化之前,我們有必要先了解一下與之息息相關的內容—— URL。

1.定義

在不談及 Dubbo 時,我們大多數人對 URL 這個概念并不會感到陌生。統一資源定位器 (RFC1738――Uniform Resource Locators (URL))應該是最廣為人知的一個 RFC 規范,它的定義也非常簡單。

因特網上的可用資源可以用簡單字符串來表示,該文檔就是描述了這種字符串的語法和語義。而這些字符串則被稱為:“統一資源定位器”(URL)。

一個標準的 URL 格式至多可以包含如下的幾個部分:

protocol://username:password@host:port/path?key=value&key=value

一些典型 URL:

http://www.facebook.com/friends?param1=value1¶m2=value2https://username:password@10.20.130.230:8080/list?version=1.0.0ftp://username:password@192.168.1.7:21/1/read.txt

當然,也有一些不太符合常規的 URL,也被歸類到了 URL 之中:

192.168.1.3:20880url protocol = null, url host = 192.168.1.3, port = 20880, url path = nullfile:///home/user1/router.js?type=scripturl protocol = file, url host = null, url path = home/user1/router.jsfile://home/user1/router.js?type=script
url protocol = file, url host = home, url path = user1/router.jsfile:///D:/1/router.js?type=scripturl protocol = file, url host = null, url path = D:/1/router.jsfile:/D:/1/router.js?type=script同上 file:///D:/1/router.js?type=script/home/user1/router.js?type=scripturl protocol = null, url host = null, url path = home/user1/router.jshome/user1/router.js?type=scripturl protocol = null, url host = home, url path = user1/router.js2.Dubbo 中的 URL

在Dubbo 中,也使用了類似的 URL,主要用于在各個擴展點之間傳遞數據,組成此 URL 對象的具體參數如下:

protocol:一般是 Ddubbo 中的各種協議 如:Dubbo thrift http zk。username/password:用戶名/密碼。host/port:主機/端口。path:接口名稱。parameters:參數鍵值對。

一些典型的 Dubbo URL

dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000描述一個 dubbo 協議的服務zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333×tamp=1545721981946描述一個 zookeeper 注冊中心consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer×tamp=1545721827784描述一個消費者

可以說,任意的一個領域中的一個實現都可以認為是一類 URL,Dubbo 使用 URL 來統一描述了元數據,配置信息,貫穿在整個框架之中。

二、Dubbo 2.71.URL 結構

在 Dubbo 2.7 中,URL 的結構非常簡單,一個類就涵蓋了所有內容,如下圖所示。

2.地址推送模型

接下來我們再來看看 Dubbo 2.7 中的地址推送模型方案,主要性能問題由下列過程引起:

上圖中主要的流程為:

(1)用戶新增/刪除 DemoService 的某個具體 Provider 實例(常見于擴容縮容、網絡波動等原因);

(2)ZooKeeper 將 DemoService 下所有實例推送給 Consumer 端;

(3)Consumer 端根據 Zookeeper 推送的數據重新全量生成 URL。

根據該方案可以看出在 Provider 實例數量較小時,Consumer 端的影響比較小,但當某個接口有大量 Provider 實例時,便會有大量不必要的 URL 創建過程。

而 Dubbo 3.0 中則主要針對上述推送流程進行了一系列的優化,接下來我們便對其進行具體的講解。

三、Dubbo 3.01.URL 結構

當然,地址推送模型的優化依然離不開 URL 的優化,下圖是 Dubbo 3.0 中優化地址推送模型的過程中使用的新的 URL 結構。

根據上圖我們可以看出,在 Dubbo 2.7 的 URL 中的幾個重要屬性在 Dubbo 3.0 中已經不存在了,取而代之的是 URLAddress 和 URLParam 兩個類。原來的 parameters 屬性被移動到了 URLParam 中的 params,其他的屬性則移動到了 URLAddress 及其子類中。

再來介紹 URL 新增的 3 個子類,其中 InstanceAddressURL 屬于應用級接口地址,本篇章中不做介紹。

而 ServiceConfigURL 及 ServiceAddressURL 主要的差別就是,ServiceConfigURL 是程序讀取配置文件時生成的 URL。而 ServiceAddressURL 則是注冊中心推送一些信息(如 providers)過來時生成的 URL。

在這里我們順便提一下為什么會有 DubboServiceAddressURL 這個子類,按照目前的結構來看,ServiceAddressURL 只有這一個子類,所以完全可以將他們兩個的屬性全都放到 ServiceAddressURL 中,那么為什么還要有這個子類呢?其實是 Dubbo 3.0 為了兼容 HSF 框架所設計的,抽象出了一個 ServiceAddressURL,而 HSF 框架則可以繼承這個類,使用 HSFServiceAddressURL,當然,這個類目前沒有體現出來,所以此處我們簡單一提,不過多講解。

那么,我們接下來就討論一下 Dubbo 3.0 為什么要改為此種數據結構,并且該結構和地址推送模型的優化有何關聯性吧!

2.地址推送模型的優化URL 結構上的優化

我們在上小節中的類圖里看到雖然原來的屬性都被移到了 URLAddress 和 URLParam 里,但是 URL 的子類依然多了幾個屬性,這幾個屬性自然也是為了優化而新增的,那么這里就講講這幾個屬性的作用。

ServiceConfigURL:這個子類中新增了 attribute 這個屬性,這個屬性主要是針對 URLParam 的 params 做了冗余,僅僅只是將 value 的類型從 String 改為了 Object,減少了代碼中每次獲取 parameters 的格式轉換消耗。

ServiceAddressURL:這個子類及其對應的其他子類中則新增了 overrideURL 和 consumerURL 屬性。其中 consumerURL 是針對 consumer 端的配置信息,overrideURL 則是在 Dubbo Admin 上進行動態配置時寫入的值,當我們調用 URL 的 getParameter() 方法時,優先級為 overrideURL > consumerURL > urlParam。在 Dubbo 2.7 時,動態配置屬性會替換 URL 中的屬性,及當你有大量 URL 時消耗也是不可忽視的,而此處的 overrideURL 則避免了這種消耗,因為所有 URL 都會共同使用同一個對象。

多級緩存

緩存是 Dubbo 3.0 在 URL 上做的優化的重點,同時這部分也是直接針對地址推送模型所做的優化,那么接下來我們就開始來介紹一下多級緩存的具體實現。

首先,多級緩存主要體現在 CacheableFailbackRegistry 這個類之中,它直接繼承于 FailbackRegistry,以 Zookeeper 為例,我們看看 Dubbo 2.7 和 Dubbo 3.0 繼承結構的區別。

可以看到在 CacheableFailbackRegistry 緩存中,我們新增了 3 個緩存屬性 stringAddress,stringParam 和 stringUrls。我們通過下圖來描述這 3 個緩存的具體使用場景。

在該方案下,我們使用了 3 個緯度的緩存數據(URL 字符串緩存、URL 地址緩存、URL 參數緩存),這樣一來,在大部分情況下都能有效利用到緩存中的數據,減少了 Zookeeper 重復通知的消耗。

延遲通知

除了上面提到的優化之外,其實另外還有兩個小小的優化。

第一個是解析 URL 時可以直接使用編碼后的 URL 字符串字節進行解析,而在 Dubbo 2.7 中,所有編碼后的 URL 字符串都需要經過解碼才可以正常解析為 URL 對象。該方式也直接減少了 URL 解碼過程的開銷。

第二個則是 URL 變更后的通知機制增加了延遲,下圖以Zookeeper為例講解了實現細節。

在該方案中,當 Consumer 接收 Zookeeper 的變更通知后會主動休眠一段時間,而這段時間內的變更在休眠結束后只會保留最后一次變更,Consumer 便會使用最后一次變更來進行監聽實例的更新,以此方法來減少大量 URL 的創建開銷。

字符串重用

在舊版本實現中,不同的 URL 中屬性相同的字符串會存儲在堆內不同的地址中,如 protocol、path 等,當有大量 provider 的情況下,Consumer 端的堆內會存在大量的重復字符串,導致內存利用率低下,所以此處提供了另一個優化方式,即字符串重用。

而它的實現方式也非常的簡單,讓我們來看看對應的代碼片段。

public class URLItemCache { private static final Map PATH_CACHE = new LRUCache<>(10000); private static final Map PROTOCOL_CACHE = new ConcurrentHashMap<>(); // 省略無關代碼片段 public static String checkProtocol(String _protocol) { if (_protocol == null) { return _protocol; } String cachedProtocol = PROTOCOL_CACHE.putIfAbsent(_protocol, _protocol); if (cachedProtocol != null) { return cachedProtocol; } return _protocol; } public static String checkPath(String _path) { if (_path == null) { return _path; } String cachedPath = PATH_CACHE.putIfAbsent(_path, _path); if (cachedPath != null) { return cachedPath; } return _path; }}

由如上代碼片段可以得知,字符串重用即為簡單地使用了 Map 來存儲對應的緩存值,當你使用了相同的字符串時,便會從 Map 中獲取早已存在的對象返回給調用方,由此便可以減少堆內存中重復的字符串數以達到優化的效果。

3.優化結果

這里優化結果我引用了《Dubbo 3.0 前瞻:服務發現支持百萬集群,帶來可伸縮微服務架構》這篇文章中的兩副圖來說明,下圖模擬了在 220 萬個 Provider 接口的情況下,接口數據不斷變更導致的 Consumer 端的消耗,我們看到整個 Consumer 端幾乎被 Full GC 占滿了,嚴重影響了性能。

那么我們再來看看 Dubbo 3.0 中對 URL 進行優化后同一個環境下的壓測結果,如下圖所示。

我們明顯可以看到 Full GC 的頻率減少到了只有 3 次,大大提升了性能。當然,該文章中還有其他方面的對比,此處便不一一引用了,感興趣的讀者可以自行去閱讀該文章。

作者介紹:

吳治國,Apache Dubbo 社區活躍貢獻者

標簽: