1. 程式人生 > >Java小菜求職記-以前在Dubbo踩的坑,這次全被問到了,這下舒服了

Java小菜求職記-以前在Dubbo踩的坑,這次全被問到了,這下舒服了

前傳

小林求職記(五)上來就一連串的分散式快取提問,我有點上頭....

終於,在小林的努力下,獲得了王哥公司那邊的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這款編碼器來進行實現的。常見的手段有:定長協議處理,特殊分隔符,自定義協議方式。

面試官:哦,看來你對這塊還是有些瞭解的哈。行吧,那先這樣吧,後邊是二面,你先在這等一下吧。

小林長舒一口氣,瞬間感覺整個人都輕鬆多了。

(未完待續..