RuleLinKClient - 再也不擔心表達引擎當機了

2J發表於2024-09-05

原來有這麼多時間

六月的那麼一天,天氣比以往時候都更涼爽,媳婦邊收拾桌子,邊漫不經心的對我說:你最近好像都沒怎麼閱讀了。 正刷著新聞我,如同被一記響亮的晴空霹靂擊中一般,不知所措。是了,最近幾月諸事湊一起,加之兩大專案接踵而至,確實有些許糟心,於是總是在空閒的時間泡在新聞裡聊以解憂,再回首,隱隱有些恍如隔世之感。於是收拾好心情,翻開了躺在書架良久的整潔三步曲。也許是太久沒有閱讀了, 一口氣,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當機了,也不影響其執行。這樣的準備之下,上線後幾乎沒有任何波瀾。當然,也就沒有人會擔心當機了。這真可以算得上一次愉快的程式設計之旅。

成為一名優秀的程式設計師!

相關文章