在閒魚,我們如何用Dart做高效後端開發?
背景
像阿里其他技術團隊以及業界的做法一樣,閒魚的大多數後端應用都是全部使用java來實現的。java易用、豐富的庫、結構容易設計的特性決定了它是進行業務開發的最好語言之一。後端應用中資料的儲存、訪問、轉換、輸出雖然都屬於後端的範疇,但是其中變更的頻率是不同的。通常領域物件確定之後,它的變化是很少的,但是客戶端展示的變化很多,導致介面層(或者叫粘連前臺和後臺的膠水層)的變化非常快。大多數web應用採用統一的技術棧來實現後端,膠水層跟領域層使用統一技術,這樣的做法仍然有可以優化的地方:
- 在預發環境中驗證除錯比較困難:一方面,每次提交程式碼、構建、部署、驗證的總時間相對較長;另一方面,多人共用一個部署環境,相互干擾(程式碼衝突和部署衝突),增加了成本。後端開發人員都渴望有一個獨立、高效的開發環境,就像開發一個前端頁面那樣
- 前臺(java、object-c,javascript)和後臺(java)的技術不同,導致前臺同學很難開發後端程式,閒魚技術團隊為了追求更高的開發效率,希望能夠跨越服務端開發與客戶端、前端的界限,讓前臺開發人員也能夠寫後端程式碼
- 膠水層通常依賴很多後端服務,計算比較簡單,是IO密集型的任務。我們理想中的程式設計框架是能夠像寫同步程式碼一樣簡單,但是享受非同步的好處。目前的方案還無法完全做到這一點。
為什麼選擇dart
閒魚技術團隊選擇使用dart作為膠水層的實現語言。
- dart是一種靜態型別語言,在編譯器就能完全確定變數的型別。它是支援泛型的面嚮物件語言,任何變數都是物件,不存在java中的原始型別。跟javascript類似,它是一種單執行緒語言,對非同步的支援非常好(async/await)。dart的語法與主流開發語言(java,python,c/c++,javascript)很類似, 在主流的語言語法基礎上,dart增加了很多語法結構,getter/setter、方法級聯、函數語言程式設計、閉包,這些語法讓允許開發人員更加容易地寫出簡潔的程式碼;全面易用的類庫也是dart能夠作為flutter開發語言的重要原因。
- flutter證明了dart在客戶端開發上的成功,閒魚不僅走在flutter開發的前列,也正在嘗試使用dart開發後端應用;語法跟javascript,java相近,有人形容這門語言是傻瓜式的簡單(stupid-simple to learn),無論是java後端開發人員,還是客戶端開發同學,亦或是前端開發同學,都能夠快速上手寫出生產級的程式碼。所有技術同學都能夠開發後端介面在閒魚是可以做到的。
- dart對非同步化的良好支援對業務開發是強大助力。後端應用膠水層程式碼大多數IO密集型的任務,使用非同步化技術可以把多個IO請求的總RT,從所有請求RT之和,降低為所有請求中最高RT。dart對非同步有良好的支援,開發同學使用dart可以以近乎同步的程式碼風格取得非同步的效能。我們以閒魚寶貝詳情頁的程式碼舉例,對比不同的編碼方式。
// java同步程式碼
ItemDetailDO queryItemDetail(Long itemId) {
ItemDetailDO data = new ItemDetailDO();
data.setBrowseCount(IdleItemBrowseService.count(itemId));// 多少人看過
data.setFavorCount(IdleItemFavorService.count(itemId));// 多少人點贊
return data;
}
// dart非同步程式碼
ItemDetailDO queryItemDetail(int itemId) async {
var data = new ItemDetailDO();
await Future.wait([
IdleItemBrowseService.count(itemId).then((count) => data.browseCount = count)
.catchError((exp, stackTrace) => logError('$exp $stackTrace')),
IdleItemFavorService.count(itemId).then((count) => data.favorCount = count)
.catchError((exp, stackTrace) => logError('$exp $stackTrace'))
]);
return data;
}
// rxjava非同步程式碼
ItemDetailDO queryItemDetail(Long itemId) {
ItemDetailDO data = new ItemDetailDO();
Flowable<Long> browseCountFlow = Flowable.fromCallable(
() => IdleItemBrowseService.count(itemId)
).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
Flowable<Long> favorCountFlow = Flowable.fromCallable(
() => IdleItemFavorService.count(itemId)
).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
Flowable.zip(browseCountFlow, favorCountFlow, (browseCount, favorCount) -> {
data.setBrowseCount(browseCount);
data.setFavorCount(favorCount);
}).blockingFirst();
}
在java中我們也廣泛使用RxJava這種強大的響應式擴充套件實施非同步操作:RxJava作為java的響應式程式設計擴充套件,功能非常強大全面,它使用流的概念封裝所有的非同步操作。需要注意的是這裡的兩個服務呼叫都被放到一個IO執行緒池中執行, 這個執行緒池是無界的,容易消耗執行緒這種系統稀缺的資源。這意味著當流量非常大的時候,系統的執行緒池很容易被打滿,需要設定合理的背壓策略。
從上面的程式碼中可以看到“資料獲取”,“資料組裝”的邏輯非常清晰,不像同步程式碼分散在各處;相比於同步操作,dart的非同步操作允許我們同時等待多個IO事件,降低總的響應時間。dart的非同步程式碼擁有同步程式碼的簡潔容易理解的優點,又具有非同步程式設計的效能優勢。
dart非同步的原理也是容易理解的。作為單執行緒語言,dart依靠事件迴圈執行程式碼。dart從main函式開始執行,我們在main函式裡面建立Future,相當於在一個dart內部維護的事件佇列(event queue)中新增計劃任務(新增的任務並不會立即執行)。main中的程式碼執行完之後,dart事件迴圈開始從事件佇列中依次獲取任務執行。async/await是dart的語法糖,它允許開發人員能夠以書寫同步程式碼的方式來實施非同步程式設計(在C#、javascript中也有類似實現)。被async修飾的方法返回一個Future,呼叫這樣的方法,相當於建立一個Future。await一個Future,相當於把await之後的程式碼打包放在Future.then()的程式碼塊裡,這樣就保證之後的程式碼在Future之後執行。由於任務儲存於事件佇列,dart在流量大的時候,記憶體消耗較大,也需要我們前期合理評估需求和分配系統資源。
dart後端開發實戰
為了提高開發效率,我們利用dart的特性構建了一套高效的隔離開發環境。在業務開發實踐中,我們總結出基本的開發架構和程式碼模式。在這些技術基礎上,開發了閒魚寶貝詳情頁的主幹業務。下面逐一介紹。
高效的隔離開發環境
我們以往的開發場景是:提交程式碼 -> 程式碼衝突(多人共用一個部署環境) -> 構建/部署 -> 通過介面驗證 -> 提交fix -> 構建/部署 -> 驗證 的迭代。在這個過程中,開發人員有可能需要親自解決程式碼衝突,或者依賴別人解決程式碼衝突,需要等待構建/部署的時間(少則5分鐘,多則十幾分鍾)。而且這個過程可能需要迭代多次,時間成本很高,如果因為其他開發人員的程式碼分支的問題導致部署失敗,那麼等待驗證的時間成倍增加。這樣的開發效率顯然不是特別理想。
在閒魚的dart應用中,這種問題會得到緩解。每個開發人員使用自己獨立的開發環境,開發環境使用每個人的工號唯一識別。在不需要提交程式碼的情況下,開發人員把程式碼部署到遠端預發環境中,並在本地呼叫預發服務,檢視服務的輸出,做到本地驗證除錯的效果,極大地提高了開發效率。因為只會有開發自己單一分支的程式碼部署,不會牽扯到程式碼衝突。整個過程,部署、服務呼叫過程十分快速,可以在10秒內完成。驗證和除錯的效率非常高。
每個開發人員的獨立開發環境對應預發機器上的一個isolate。dart的isolate相當於一個執行緒,但是不會和其他isolate共享記憶體,isolate之間的通訊通過傳送、接收訊息完成。閒魚技術團隊使用每個開發人員的程式碼建立一個isolate,使用工號作為標識,程式碼可以全量替換掉執行中的isolate,也可以使用熱部署增量替換掉isolate中更改的功能。整個過程非常快。在早期使用dart原生的編譯器,發現速度較慢(10多秒)後,我們對dart編譯器做了裁剪和優化,把編譯時間從10多秒降低到幾百毫秒(簡單來說就是,把dart原生的編譯器的附加功能,重新封裝,然後通過JIT/AOT生成新的編譯工具)。經過我們對dart開發環境的增強,現在開發dart膠水層介面,只需要點選開發工具上的一個按鈕,就可以把修改的程式碼,在幾秒內部署到遠端的預發環境,並呼叫當前的開發介面,在本地檢視輸出。獲得和在預發環境上驗證一樣的效果,但是體驗就像在開發一個完全不依賴外部的本地應用程式。
業務開發架構
業務開發中最重要的部分是分離出變化和不變的部分,變化的部分用最靈活、快捷的方式實現(變的最多的地方當然用最快的方式處理),不變的部分使用穩定、高效的方式實現。我們已經把dart建設成為一種能夠高效開發,並且適合客戶端、前端、後端技術人員共同使用的技術。這種技術最適合應用於發生快速變化的介面層,也就是客戶端和後端互動的地方,業務需求的變化導致這裡的資料結構快速變化,也稱之為膠水層。對於相對穩定的資料服務,我們使用java實現為領域服務。
上圖是服務之間的互動圖,實現方式如下圖所示:
膠水層dart應用以HTTP協議方式作為MTOP介面提供給客戶端呼叫,往下使用HSF從Java應用中獲取資料。
通常先定義並開發好領域服務,然後再與客戶端對接開發出介面,領域服務提供的介面,包含了獲取基礎資料的所有方法,開發好之後,很少發生變化;膠水層獲取領域服務提供的資料,對資料進行加工、裁剪、組裝,輸出為客戶端能夠解析的檢視資料,客戶端解析、渲染、展示為頁面。膠水層的程式碼大致可以分為:獲取資料,然後資料處理和組裝。抽象出程式碼模式如下所示:
// 資料處理和組裝
void putTiger(Zoo zoo, Tiger tiger) => zoo.tiger = tiger;
void putDophin(Zoo zoo, Dophin dophin) => zoo.dophin = dophin;
void putRatel(Zoo zoo, Ratel ratel) => zoo.ratel = ratel;
// 發起多個非同步請求,所有請求完成後返回所有資料
Future<T> catchError<T>(Future<T> future) {
return future.catchError((e, st) => LogUtils.error('$e $st'));
}
Future<List<T>> waitFutures<T>(List<Future<T>> futures) {
Future<List<T>> future = Future.wait(futures.map(catchError));
return catchError(future);
}
// 服務介面
Future<Zoo> process(Parameter param) async {
var zoo = new Zoo();
// 資料獲取
await waitFutures(
Service1.invoke(param).then((animal) -> putTiger(zoo, animal)),
Service2.invoke(param).then((animal) -> putLion(zoo, dophin)),
Service3.invoke(param).then((animal) -> putRatel(zoo, animal))
);
return finalData;
}
為了使用java的領域服務,我們首先解決了dart和java之間資料互動問題,主要是通過序列化對java類檔案和dart類檔案進行合理的轉換,保證dart能夠透明、簡潔地使用java的資料結構,呼叫java的遠端服務;在呼叫鏈路上設定全域性唯一的上下文id,跨越dart和java呼叫棧,支援全鏈路排查;對所有的服務的成功率,rt和額外業務引數有詳細的日誌,可以配置以日誌為資料來源的監控告警等等(後續的文章將詳細介紹我們對這些問題的詳細解決方案,請持續關注哦)。
服務化詳情頁主幹開發
閒魚寶貝詳情頁是我們使用dart開發的一個重要專案。最早的閒魚寶貝詳情頁把各個業務的程式碼邏輯耦合在一起,導致維護和變更困難,穩定性也難以保證。我們設計的swak框架(更多細節請檢視文章swak框架),能夠分離垂直業務的共性和差異性,把閒魚寶貝詳情頁的實現分割成主幹實現和垂直業務實現兩塊。我們使用自己開發的dart後端開發框架,對swak框架做了最小實現。專案完成了詳情頁主幹的完整功能和基礎優化:
- 垂直業務路由:我們使用dart中的zone儲存每個閒魚商品的業務標識,程式碼生成的靜態代理類依據業務標識呼叫相應的服務,在主幹資料裡填充各個業務的獨有資料。zone是dart非同步程式碼的執行環境,能夠快取一些可重用資料(業務程式碼裡除非非此不可,儘量不要多用)
- 作為遠端服務的提供方:在hsfcpp對hessian協議的實現基礎上做開發,dart也能成為遠端服務的提供方
- 服務呼叫的優化: 對java遠端服務的代理做了優化,隔離業務層面對框架層的感知,做到透明呼叫
-
解決快取呼叫的差異性:我們依賴快取的c++介面訪問快取,但是仍然需要處理java/c++快取讀寫不相容問題完成dart和java對同一快取的同時讀寫
專案流程圖可見下圖:![dart-detail-flow.png](http://gw.alicdn.com/mt/TB1Pyv6V9zqK1RjSZFjXXblCFXa-558-561.png)
實際效果
目前該專案已經上線超過6周,qps最高可達400,成功率在99.5%以上。整個呼叫鏈路的RT與同樣功能的java應用持平。由於前期的精心設計,領域服務很少改動,大部分變更發生在dart膠水層。從上線後經歷的若干次變更來看,dart膠水層從修改程式碼結束到提供給客戶端使用總耗時不超過2分鐘,而相同功能的java應用需要10分鐘以上。
總結
dart是一門簡潔、容易上手、對非同步支援良好的程式語言,在flutter的開發中大放異彩。在我們的努力下,dart用於後端開發的支援逐漸完善,前臺開發同學和後端開發人員快速高效地開發膠水層介面。我們在很多生產專案中使用了dart用於後端開發,效能、穩定性良好,開發效率大大提高。未來我們會著力於進一步改善dart開發體驗、與java專案的相容性、提升dart遠端服務的效能,挖掘dart在後端開發中更大的潛力。
作者:閒魚技術-臨耕
原文連結
本文為雲棲社群原創內容,未經