終于,在小林的努力下,獲得了王哥公司那邊的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的函數,這裡面寫明了關于注冊的細節點:
privatevoiddoExportUrls(){
List<URL>registryURLs=loadRegistries(true);
for(ProtocolConfigprotocolConfig:protocols){
StringpathKey=URL.buildKey(getContextPath(protocolConfig).map(p->p "/" path).orElse(path),group,version);
ProviderModelproviderModel=newProviderModel(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
DelegateProviderMetaDataInvokerwrapperInvoker=newDelegateProviderMetaDataInvoker(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")
publicclassMsgServiceImplimplementsMsgService{
@Override
publicBooleansendMsg(intid,Stringmsg){
System.out.println("msgis:" msg);
returntrue;
}
}
并沒有過多的繁瑣請求,并且我們手動設置線程池參數:
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這款編碼器來進行實現的。常見的手段有:定長協議處理,特殊分隔符,自定義協議方式。
面試官:哦,看來你對這塊還是有些了解的哈。行吧,那先這樣吧,後邊是二面,你先在這等一下吧。
小林長舒一口氣,瞬間感覺整個人都輕松多了。
(未完待續...)
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!