原來有這麼多時間
六月的那麼一天,天氣比以往時候都更涼爽,媳婦邊收拾桌子,邊漫不經心的對我說:你最近好像都沒怎麼閱讀了。 正刷著新聞我,如同被一記響亮的晴空霹靂擊中一般,不知所措。是了,最近幾月諸事湊一起,加之兩大專案接踵而至,確實有些許糟心,於是總是在空閒的時間泡在新聞裡聊以解憂,再回首,隱隱有些恍如隔世之感。於是收拾好心情,翻開了躺在書架良久的整潔三步曲。也許是太久沒有閱讀了, 一口氣,Bob大叔 Clean 系列三本都讀完了,重點推薦Clear Architecture,部分章節建議重複讀,比如第5部分-軟體架構,可以讓你有真正的提升,對程式碼,對程式設計,對軟體都會有不一樣的認識。
Clean Code 次之,基本寫了一些常見的規約,大部分也是大家熟知,資料結構與物件導向的看法,是少有的讓我 哇喔的點,如果真是在碼路上摸跋滾打過的,快速翻閱即可。
The Clean Coder 對個人而言可能作用最小。 確實寫人最難,無法聚焦。講了很多,但是感覺都不深入,或者作者是在寫自己,很難對映到自己身上。 當然,第二章說不,與第14章輔導,學徒與技藝,還是值得一看的。
閱讀技術書之餘,又戰戰兢兢的翻開了敬畏已久的朱生豪先生翻譯的《莎士比亞》, 不看則已,因為看了根本停不來。其華麗的辭職,幽默的比喻,真的會讓人情不自禁的開懷朗讀起來。
。。。
再看從6月到現在,電子書閱讀時間超過120小時,平均每天原來有1個多小時的空餘時間,簡直超乎想像。
看了整潔架構一書,就想寫程式碼,於是有了這篇文章。
靈魂拷問 - 當機怎麼辦
為了解決系統中大量規則配置的問題,與同事一起構建了一個視覺化表示式引擎 RuleLink《非全自研視覺化表達引擎-RuleLinK》,解決了公司內部幾乎所有配置問題。尤為重要的一點,所有配置業務同學即可自助完成。隨著業務深入又增加了一些自定義函式,增加了公式及計算功能,增加元件無縫嵌入其他業務...我一度以為現在的功能已經可以滿足絕大部分場景了。真到Wsin強同學說了一句:業財專案是深度依賴RuleLink的,流水打標,關聯科目。。。我知道他看了資料,10分RuleLink執行了5萬+次。這也就意味著,如果RuleLink當機了,業財服務也就當機了,也就意味著巨大的事故。這卻是是一個問題,公司業務確實屬於非常低頻,架不住財務資料這麼多。如果才能讓RuleLink更穩定成了當前的首要問題。
高可用VS少依賴
要提升服務的可用性,增加服務的例項是最快的方式。 但是考慮到我們自己的業務屬性,以及業財只是在每天固定的幾個時間點短時高頻呼叫。 增加節點似乎不是最經濟的方式。看 Bob大叔的《Clear Architecture》書中,對架構的穩定性有這樣一個公式:不穩定性,I=Fan-out/(Fan-in+Fan-out)
Fan-in:入向依賴,這個指標指代了元件外部類依賴於元件內部類的數量。
Fan-out:出向依賴,這個指標指代了元件內部類依賴於元件外部類的數量。
這個想法,對於各個微服務的穩定性同時適用,少一個外部依賴,穩定性就增加一些。站在業財系統來說,如果我能減少呼叫次數,其穩定性就在提升,批次介面可以一定程度上減少依賴,但並未解決根本問題。那麼呼叫次數減少到極限會是什麼樣的呢?答案是:一次。如果規則不變的話,我只需要啟動時載入遠端規則,並在本地容器執行規則的解析。如果有變動,我們只需要監聽變化即可。這樣極大減少了業財對RuleLink的依賴,也不用增RuleLink的節點。實際上大部分配置中心都是這樣的設計的,比如apollo,nacos。 當然,本文的實現方式也有非常多借鑑(copy)了apollo的思想與實現。
服務端設計
模型比較比較簡單,應用訂閱場景,場景及其規則變化時,或者訂閱關係變化時,生成應用與場景變更記錄。類似於生成者-消費都模型,使用DB做儲存。
”推送”原理
整體邏輯參考apollo實現方式。 服務端啟動後 建立Bean ReleaseMessageScanner 注入變更監聽器 NotificationController。
ReleaseMessageScanner 一個執行緒定時掃碼變更,如果有變化 通知到所有監聽器。
NotificationController在得知有配置釋出後是如何通知到客戶端的呢?
實現方式如下:
1,客戶端會發起一個Http請求到RuleLink的介面,NotificationController
2,NotificationController不會立即返回結果,而是透過Spring DeferredResult把請求掛起
3,如果在60秒內沒有該客戶端關心的配置釋出,那麼會返回Http狀態碼304給客戶端
4,如果有該客戶端關心的配置釋出,NotificationController會呼叫DeferredResult的setResult方法,傳入有變化的場景列表,同時該請求會立即返回。客戶端從返回的結果中獲取到有變化的場景後,會直接更新快取中場景,並更新重新整理時間
ReleaseMessageScanner 比較簡單,如下。NotificationController 程式碼也簡單,就是收到更新訊息,setResult返回(如果有請求正在等待的話)
public class ReleaseMessageScanner implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageScanner.class); private final AppSceneChangeLogRepository changeLogRepository; private int databaseScanInterval; private final List<ReleaseMessageListener> listeners; private final ScheduledExecutorService executorService; public ReleaseMessageScanner(final AppSceneChangeLogRepository changeLogRepository) { this.changeLogRepository = changeLogRepository; databaseScanInterval = 5000; listeners = Lists.newCopyOnWriteArrayList(); executorService = Executors.newScheduledThreadPool(1, RuleThreadFactory .create("ReleaseMessageScanner", true)); } @Override public void afterPropertiesSet() throws Exception { executorService.scheduleWithFixedDelay(() -> { try { scanMessages(); } catch (Throwable ex) { logger.error("Scan and send message failed", ex); } finally { } }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS); } /** * add message listeners for release message * @param listener */ public void addMessageListener(ReleaseMessageListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } /** * Scan messages, continue scanning until there is no more messages */ private void scanMessages() { boolean hasMoreMessages = true; while (hasMoreMessages && !Thread.currentThread().isInterrupted()) { hasMoreMessages = scanAndSendMessages(); } } /** * scan messages and send * * @return whether there are more messages */ private boolean scanAndSendMessages() { //current batch is 500 List<AppSceneChangeLogEntity> releaseMessages = changeLogRepository.findUnSyncAppList(); if (CollectionUtils.isEmpty(releaseMessages)) { return false; } fireMessageScanned(releaseMessages); return false; } /** * Notify listeners with messages loaded * @param messages */ private void fireMessageScanned(Iterable<AppSceneChangeLogEntity> messages) { for (AppSceneChangeLogEntity message : messages) { for (ReleaseMessageListener listener : listeners) { try { listener.handleMessage(message.getAppId(), ""); } catch (Throwable ex) { logger.error("Failed to invoke message listener {}", listener.getClass(), ex); } } } } }
客戶端設計
上圖簡要描述了客戶端的實現原理:
- 客戶端和服務端保持了一個長連線,從而能第一時間獲得配置更新的推送。(透過Http Long Polling實現)
- 客戶端還會定時從RuleLink配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,為了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率預設為每5分鐘拉取一次,客戶端也可以透過在執行時指定配置項: rule.refreshInterval來覆蓋,單位為分鐘。
- 客戶端從RuleLink配置中心服務端獲取到應用的最新配置後,會寫入記憶體儲存到SceneHolder中,
- 可以透過RuleLinkMonitor 檢視client 配置重新整理時間,以及記憶體中的規則是否遠端相同
客戶端工程
客戶端以starter的形式,透過註解EnableRuleLinkClient 開始初始化。
1 /** 2 * @author JJ 3 */ 4 @Retention(RetentionPolicy.RUNTIME) 5 @Target(ElementType.TYPE) 6 @Documented 7 @Import({EnableRuleLinkClientImportSelector.class}) 8 public @interface EnableRuleLinkClient { 9 10 /** 11 * The order of the client config, default is {@link Ordered#LOWEST_PRECEDENCE}, which is Integer.MAX_VALUE. 12 * @return 13 */ 14 int order() default Ordered.LOWEST_PRECEDENCE; 15 }
在最需求的地方應用起來
花了大概3個周的業餘時間,搭建了client工程,經過一番鬥爭後,決定直接用到了最迫切的專案 - 業財。當然,也做了完全準備,可以隨時切換到RPC版本。 得益於DeferredResult的應用,變更總會在60s內同步,也有兜底方案:每300s主動查詢變更,即便是啟動後RuleLink當機了,也不影響其執行。這樣的準備之下,上線後幾乎沒有任何波瀾。當然,也就沒有人會擔心當機了。這真可以算得上一次愉快的程式設計之旅。
成為一名優秀的程式設計師!