前傳
小林求職記(五)上來就一連串的分布式快取提問,我有點上頭....
終于,在小林的努力下,獲得了王哥公司那邊的offer,但是因為薪水沒有談妥,小林又重新進入了求職的旅途,在經歷了多次求職程序之后,小林也大概地對求職的考點掌握地七七八八了,于是這次他重新書寫了簡歷,投遞了一家新的互聯網企業,
距離面試開始還有大約十分鐘,小林已經抵達了面試現場,并開始調整自己的狀態,
過了不久,一個稍顯消瘦,戴著黑色眼鏡框的男人走了過來,估計這家伙就是小林這次的面試官了,
面試官:你好,請簡單先做個自我介紹吧,
小林:嗯嗯,面試官你好,我是XXXX(此處省略200個字)
面試官:我看到你的專案里面有提及到dubbo,rpc技術這一技術堆疊正好和我們這邊的匹配,我先問你些關于dubbo和rpc的技術問題吧,首先你能講解下什么是rpc嗎?
小林:好的,rpc技術其實簡單地來理解就是不同計算機之間進行遠程通信實作資料互動的一種技術手段吧,一個合理的rpc應該要分為server, client, server stub,client stub四個模塊部分,
面試官:嗯嗯,你說的server stub,client stub該怎么理解呢?
小林:這個可以通過名字來識別進行理解,client stub就是將服務的請求的引數,請求方法,請求地址通過打包封裝給成一個物件統一發送給server端,server stub就是服務端接收到這些引數之后進行拆解得到最終資料的結果,

在以前的單機版架構里面,兩個方法進行相互呼叫的時候都是先通過記憶體地址查詢到對應的方法,然后呼叫執行,但是分布式環境下不同的行程是可能存在于不同的機器中的,因此在通過原先的尋址方式呼叫函式就不可行了,這個時候就需要結合網路io的手段來進行服務的”交流“,
面試官:了解,你對rpc本質還是有自己的理解,可以大致講解下dubbo在工程中啟動的時候的一些整體流程嗎?
小林:嗯嗯(猛地想起了之前寫的一些筆記內容)
在工程進行啟動的時候(假設使用spring容器進行bean的托管),首先會將bean注冊到spring容器中,然后再將對應的服務注冊到zk中,實作對外暴露服務,
面試官:可以說說在原始碼里面的核心設計嗎?假設說某個dubbo服務沒有對外暴露成功,你會如何去做分析呢?
小林:嗯嗯,其實可以先通過閱讀啟動日志進行分析,dubbo的啟動順序并不是直接就進行zk的連接,而是先校驗組態檔是否正確,然后是否已經將bean都成功注冊到了Spring的ioc容器中,接下來才是連接zk并且將服務進行注冊的環節,

如果確保服務的配置無誤,那么問題可能就是出在連接zk的程序了,
面試官:嗯嗯,有一定的邏輯依據,挺好的,你有了解過服務暴露的細節點嗎?例如說dubbo是如何將自己的服務提供者資訊寫入到注冊中心(zookeeper)的呢?
小林:我在閱讀dubbo對外進行服務暴露的源代碼時印象中對ServiceConfig這個類比較熟系,在實作對外做服務暴露的時候,這里面的有個加了鎖的export函式,內部會先對dubbo的配置進行校驗,首先判斷是否需要對外暴露,然后是是否需要延遲暴露,如果需要延遲暴露則會通過ScheduledExecutorService去做延遲暴露的操作,否則立即暴露,即執行doExport方法

在往原始碼里面分析,會看到一個叫做doExportUrls的函式,這里面寫明了關于注冊的細節點:
private void doExportUrls() { List<URL> registryURLs = loadRegistries(true); for (ProtocolConfig protocolConfig : protocols) { String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version); ProviderModel providerModel = new ProviderModel(pathKey, ref, interfaceClass); ApplicationModel.initProviderModel(pathKey, providerModel); //暴露對外的服務內容 核心 doExportUrlsFor1Protocol(protocolConfig, registryURLs); } }
實作注冊中心的服務暴露核心點:

doExportUrlsFor1Protocol內部的代碼 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString())); //這里有一個使用了委派模型的invoker DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this); //服務暴露的核心點 Exporter<?> exporter = protocol.export(wrapperInvoker); exporters.add(exporter);
最終在org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#doRegister函式里面會有一步熟系的操作,將dubbo的服務轉換為url寫入到zk中做持久化處理:
并且這里寫入的資料節點還是非持久化的節點

面試官: 看來你對服務注冊的這些原理還是有過一定深入的理解啊,你以前的作業中是有遇到過原始碼分析的情況嗎?對這塊還蠻清晰的,
小林:嗯嗯,之前在作業中有遇到過服務啟動例外,一直報錯,但是又沒人肯幫我,所以這塊只好硬著頭皮去學習,后來發現了解了原理以后,對于dubbo啟動報錯例外的分析還是蠻有思路的,
面試官:嘿嘿,挺好的,那你對于使用dubbo的時候又遇到過dubbo執行緒池溢位的情況嗎?
小林:嗯嗯,以前作業中在做這塊的時候有遇到過,
面試官: 嘿嘿,跟我講講你自己對于dubbo內部的執行緒池這塊的分析吧,
小林:嗯嗯,可以的,
接下來小林進行了一番壓測場景的講解:
其實dubbo的服務提供者端一共包含了兩類執行緒池,一類叫做io執行緒池,還有一類叫做業務執行緒池,它們各自有著自己的分工,如下圖所示:

dubbo在服務提供方中有io執行緒池和業務執行緒池之分,可以通過調整相關的dispatcher引數來控制將請求處理交給不同的執行緒池處理,(下邊列舉作業中常用的幾個引數:)
all:將請求全部交給業務執行緒池處理(這里面除了日常的消費者進行服務呼叫之外,還有關于服務的心跳校驗,連接事件,斷開服務,回應資料寫回等)
execution:會將請求處理進行分離,心跳檢測,連接等非業務核心模塊交給io執行緒池處理,核心的業務呼叫介面則交由業務執行緒池處理,
假設說我們的dubbo介面只是一些簡單的邏輯處理,例如說下方這類:
@Service(interfaceName = "msgService") public class MsgServiceImpl implements MsgService { @Override public Boolean sendMsg(int id, String msg) { System.out.println("msg is :"+msg); return true; } }
并沒有過多的繁瑣請求,并且我們手動設定執行緒池引數:
dubbo.protocol.threadpool=fixed dubbo.protocol.threads=10 dubbo.protocol.accepts=5
當執行緒池滿了的時候,服務會立馬進入失敗狀態,此時如果需要給provider設定等待佇列的話可以嘗試使用queues引數進行設定,
dubbo.protocol.queues=100
但是這個設定項雖然看似能夠增大服務提供者的承載能力,卻并不是特別建議開啟,因為當我們的provider承載能力達到原先預期的限度時,通過請求堆積的方式繼續請求指定的服務器并不是一個合理的方案,合理的做法應該是直接拋出執行緒池溢位例外,然后請求其他的服務提供者,
測驗環境:Mac筆記本,jvm:-xmx 256m -xms 256m
接著通過使用jmeter進行壓力測驗,發現一秒鐘呼叫100次(大于實際的業務執行緒數目下,執行緒池并沒有發生溢位情況),這是因為此時dubbo介面中的處理邏輯非常簡單,這么點并發量并不會造成過大影響,(幾乎所有請求都能正常抗住)

圖片
但是假設說我們的dubbo服務內部做了一定的業務處理,耗時較久,例如下方:
@Service(interfaceName = "msgService")
public class MsgServiceImpl implements MsgService {
@Override
public Boolean sendMsg(int id, String msg) throws InterruptedException {
System.out.println("msg is :"+msg);
Thread.sleep(500);
return true;
}
}
此時再做壓測,解果就會不一樣了,

此時大部分的請求都會因為業務執行緒池中的數目有限出現堵塞,因此導致大量的rpc呼叫出現例外,可以在console視窗看到呼叫出現大量例外:

將jmeter的壓測報告進行匯出之后,可以看到呼叫成功率大大降低,

也僅僅只有10%左右的請求能夠被成功處理,這樣的服務假設進行了執行緒池引數優化之后又會如何呢?
1秒鐘100個請求并發訪問dubbo服務,此時業務執行緒池專心只處理服務呼叫的請求,并且最大執行緒數為100,服務端最大可接納連接數也是100,按理來說應該所有請求都能正常處理
dubbo.protocol.threadpool=fixed dubbo.protocol.dispatcher=execution dubbo.protocol.threads=100 dubbo.protocol.accepts=100
還是之前的壓測引數,這回所有的請求都能正常回傳,

ps:提出一個小問題,從測驗報告中查看到平均介面的回應耗時為:502ms,也就是說其實dubbo介面的承載能力估計還能擴大個一倍左右,我又嘗試加大了壓測的力度,這次看看1秒鐘190次請求會如何?(假設執行緒池100連接中,每個連接對請求的處理耗時大約為500ms,那么一秒時長大約能處理2個請求,但是考慮到一些額外的耗時可能達不到理想狀態那么高,因此設定為每秒190次(190 <= 2*100)請求的壓測)
但是此時發現請求的回應結果似乎并沒有這么理想,這次請求回應的成功率大大降低了,
jmeter引數:
圖片
請求結果:
圖片
面試官:哦,看來你對執行緒池這塊的引數還是有一定的研究哈,
面試官:你剛剛提到了請求其他服務提供者,那么你對于dubbo的遠程呼叫程序以及負載均衡策略這塊可以講講嗎?最好能夠將dubbo的整個呼叫鏈路都講解一遍?
小林思考了一整子,在腦海中整理了一遍dubbo的呼叫鏈路,然后開始了自己的分析:
小林:這整個的呼叫鏈路其實是非常復雜的,但是我嘗試將其和你闡述清楚,
銜接我上邊的服務啟動流程,當dubbo將服務暴露成功之后,會在zk里面記錄相關的url資訊
圖片
此時我們切換視角回歸到consumer端來分析,假設此時consumer進行了啟動,啟動的程序中,會觸發一個叫做get的函式操作,這個操作位于ReferenceConfig中,
圖片
首先是檢查配置校驗,然后再是進行初始化操作,在init操作中通過斷點分析可以看到一個叫做createProxy的函式,在這里面會觸發創建dubbo的代理物件,可以通過idea工具分析,此時會傳遞一個包含了各種服務呼叫方的引數進入該函式中,
圖片
在createProxy這個方法的名字上邊可以分析出,這時候主要是創建了一個代理物件,并且還優先會判斷是否走jvm本地呼叫,如果不是的話,則會創建遠程呼叫的代理物件,并且是通過jdk的代理技術進行實作的,
最侄訓在org.apache.dubbo.registry.support.ProviderConsumerRegTable#registerConsumer里面看到consumer呼叫服務時候的一份map關系映射,這里面根據遠程呼叫的方法名稱來識別對應provider的invoker物件
圖片
最后當需要從consumer對provider端進行遠程呼叫的時候,會觸發一個叫做:DubboInvoker的物件,在這個物件內部有個叫做doInvoke的操作,這里面會將資料的格式進行封裝,最終通過netty進行通信傳輸給provider,并且服務資料的寫回主要也是依靠netty來處理,
ps:
dubbo的整體架構圖

面試官:嗯嗯,你大概講解了一遍服務的呼叫程序,雖然原始碼部分講解地挺細(面試官也聽得有點點暈~~),但是我還是想問你一些關于更加深入的問題,你對于netty這塊有過相關的了解嗎?
小林:好像在底層中是通過netty進行通信的,這塊的通信機制原理之前有簡單了解過一些,
面試官:能講解下netty里面的粘包和拆包有所了解嗎?
小林:哈哈,可以啊,其實粘包和拆包是一件我們在研發作業中經常可能遇到的一個問題,一般只有在TCP網路上通信時才會出現粘包與拆包的情況,
正常的一次網路通信:
客戶端和服務端進行網路通信的時候,client給server發送了兩個資料包,分別是msg1,msg2,理想狀態下可能資料的發送情況如下:

但是在網路傳輸中,tcp每次發送都會有一個叫做Nagle的演算法,當發送的資料包小于mss(最大分段報文體積)的時候,該演算法會盡可能將所有類似的資料包歸為同一個分組進行資料的發送,避免大量的小資料包發送,因為發送端通常都是收到前一個報文確認之后才會進行下個資料包的發送,因此有可能在網路傳輸資料程序中會出現粘包的情況,例如下圖:
兩個資料包合成一個資料包一并發送

某個資料包的資料丟失了一部分,缺失部分和其他資料包一并發送

為了防止這種情況發生,通常我們會在服務端制定一定的規則來防范,確保每次接收的資料包都是完整的資料資訊,
netty里面對于資料的粘包拆包處理機制主要是通過ByteToMessageDecoder這款編碼器來進行實作的,常見的手段有:定長協議處理,特殊分隔符,自定義協議方式,
面試官:哦,看來你對這塊還是有些了解的哈,行吧,那先這樣吧,后邊是二面,你先在這等一下吧,
小林長舒一口氣,瞬間感覺整個人都輕松多了,
(未完待續...)
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/51241.html
標籤:Java
