天天看點

第二部分:實戰二

第二部分:實戰二

文中舉例,設計開發一個小的架構,能夠擷取接口調用的各種統計資訊,并且支援将統計結果以各種顯示格式輸出到各種終端,以友善檢視。

性能計數器作為一個跟業務無關的功能,我們完全可以把它開發成一個獨立的架構或者類庫,內建到很多業務系統中。

作為可被複用的架構,除了功能性需求之外,非功能性需求也非常重要。

接口統計資訊:包括接口響應時間的統計資訊,以及接口調用次數的統計資訊等。

統計資訊的類型:max、min、avg、percentile、count、tps 等。

統計資訊顯示格式:Json、Html、自定義顯示格式。

統計資訊顯示終端:Console、Email、HTTP 網頁、日志、自定義顯示終端。

統計觸發方式:包括主動和被動兩種。

統計時間區間:架構需要支援自定義統計時間區間。

統計時間間隔:對于主動觸發統計,我們還要支援指定統計時間間隔,也就是多久觸發一次統計顯示。

第二部分:實戰二

易用性:

架構是否易內建、易插拔、跟業務代碼是否松耦合、提供的接口是否夠靈活等等,都是我們應該花心思去思考和設計的。

有的時候,文檔寫得好壞甚至都有可能決定一個架構是否受歡迎。

性能:

對于需要內建到業務系統的架構來說,我們不希望架構本身的代碼執行效率,對業務系統有太多性能上的影響。

擴充性:

在不修改或盡量少修改代碼的情況下添加新的功能。

使用者可以在不修改架構源碼,甚至不拿到架構源碼的情況下,為架構擴充新的功能。這就有點類似給架構開發插件。

比如,在不修改架構源碼的情況下,以繼承架構中方法的方式來達到修改架構源碼中方法的目的。

容錯性:

不能因為架構本身的異常導緻接口請求出錯。

對外暴露的接口抛出的所有運作時、非運作時異常都進行捕獲處理。

通用性:

為了提高架構的複用性,能夠靈活應用到各種場景中。架構在設計的時候,要盡可能通用。

資料采集:

負責打點采集原始資料,包括記錄每次接口請求的響應時間和請求時間。

資料采集過程要高度容錯,不能影響到接口本身的可用性。

暴露給架構的使用者,也要盡量考慮其易用性。

存儲:

負責将采集的原始資料儲存下來,以便後面做聚合統計。

資料存儲比較耗時,為了盡量地減少對接口性能(比如響應時間)的影響,采集和存儲的過程異步完成。

聚合統計:

負責将原始資料聚合為統計資料。

為了支援更多的聚合統計規則,代碼希望盡可能靈活、可擴充。

顯示:

負責将統計資料以某種格式顯示到終端。

第二部分:實戰二

解決一個簡單應用場景的性能計數器:統計使用者注冊、登入這兩個接口的響應時間的最大值和平均值、接口調用次數,并且将統計結果以 JSON 的格式輸出到指令行中。

應用場景的代碼,具體如下所示:

最小原型實作如下所示:recordResponseTime() 和 recordTimestamp() 兩個函數分别用來記錄接口請求的響應時間和通路時間,startRepeatedReport() 函數以指定的頻率統計資料并輸出結果

如何用它來統計注冊、登入接口的響應時間和通路次數,具體的代碼如下所示:

資料采集:負責打點采集原始資料,包括記錄每次接口請求的響應時間。

存儲:負責将采集的原始資料儲存下來,以便之後做聚合統計。資料的存儲方式有很多種,我們暫時隻支援 Redis 這一種存儲方式,并且,采集與存儲兩個過程同步執行。

聚合統計:負責将原始資料聚合為統計資料,包括響應時間的最大值、最小值、平均值、99.9 百分位值、99 百分位值,以及接口請求的次數和 tps。

顯示:負責将統計資料以某種格式顯示到終端,暫時隻支援主動推送給指令行和郵件。指令行間隔 n 秒統計顯示上 m 秒的資料(比如,間隔 60s 統計上 60s 的資料)。郵件每日統計上日的資料。

劃分職責進而識别出有哪些類

MetricsCollector 類負責提供 API,來采集接口請求的原始資料。我們可以為 MetricsCollector 抽象出一個接口,但這并不是必須的,因為暫時我們隻能想到一個 MetricsCollector 的實作方式。

MetricsStorage 接口負責原始資料存儲,RedisMetricsStorage 類實作 MetricsStorage 接口。這樣做是為了今後靈活地擴充新的存儲方法,比如用 HBase 來存儲。

Aggregator 類負責根據原始資料計算統計資料。

ConsoleReporter 類、EmailReporter 類分别負責以一定頻率統計并發送統計資料到指令行和郵件。至于ConsoleReporter 和 EmailReporter 是否可以抽象出可複用的抽象類,或者抽象出一個公共的接口,我們暫時還不能确定。

定義類及類與類之間的關系

接下來就是定義類及屬性和方法,定義類與類之間的關系。這兩步沒法分得很開。

MetricsStorage 接口定義存取資料相關的屬性和方法。RedisMetricsStorage 類實作 MetricsStorage 接口,填充具體的方法和屬性。

MetricsCollector 類在構造函數中,以依賴注入的方式引入 MetricsStorage 接口,并在類内部的方法中得以調用資料存取的方法。

統計顯示所要完成的功能邏輯細分位下面 4 點:

根據給定的時間區間,從資料庫中拉取資料

根據原始資料,計算得到統計資料

将統計資料顯示到終端(指令行或郵件)

定時觸發以上 3 個過程的執行

面向對象設計和實作要做的事情,就是把合适的代碼放到合适的類中。讓代碼盡量地滿足低耦合、高内聚、單一職責、對擴充開放對修改關閉等之前講到的各種設計原則和思想,盡量地讓設計滿足代碼易複用、易讀、易擴充、易維護。

我們暫時選擇把第 1、3、4 邏輯放到 ConsoleReporter 或 EmailReporter 類中,把第 2 個邏輯放到 Aggregator 類中。

Aggregator 類負責的邏輯比較簡單,我們把它設計成隻包含靜态方法的工具類。靜态方法中有統計方式,比如加和、取最大最小等,使用RequestStat 類中的 set 方法指派給 RequestStat 類定義的這些統計屬性。

ConsoleReporter 類相當于一個上帝類,定時根據給定的時間區間,從資料庫中取出資料,借助 Aggregator 類完成統計工作,并将統計結果輸出到指令行。也就是在 4 中定時觸發 1、2、3 代碼的執行。

MetricsCollector 代碼實作:

RequestInfo 代碼實作:

MetricsStorage 代碼實作:

RedisMetricsStorage 代碼實作:

Aggregator 代碼實作:

RequestStat 代碼實作:

ConsoleReporter 代碼實作:

EmailReporter 代碼實作:

Demo 類實作:

将類組裝起來并提供執行入口

一個是 MetricsCollector 類,提供了一組 API 來采集原始資料。

另一個是 ConsoleReporter 類和 EmailReporter 類,用來觸發統計顯示。

Review 設計與實作

MetricsCollector 負責采集和存儲資料,職責相對來說還算比較單一。它基于接口而非實作程式設計,通過依賴注入的方式來傳遞 MetricsStorage 對象,可以在不需要修改代碼的情況下,靈活地替換不同的存儲方式,滿足開閉原則。

RedisMetricsStorage 和 MetricsStorage 的設計比較簡單。當我們需要實作新的存儲方式的時候,隻需要實作 MetricsStorage 接口即可。其他接口函數調用的地方都不需要改動,滿足開閉原則。

Aggregator 類是一個工具類,裡面隻有一個靜态函數,有 50 行左右的代碼量,負責各種統計資料的計算。一旦越來越多的統計功能添加進來之後,這個函數的代碼量會持續增加,可讀性、可維護性就變差了。這個類的設計可能存在職責不夠單一、不易擴充等問題,需要在之後的版本中,對其結構做優化。

ConsoleReporter和EmailReporter中存在問題較多:

從資料庫中取資料、做統計的邏輯都是相同的,可以抽取出來複用,違反了 DRY 原則。

整個類負責的事情比較多,職責不單一。特别是顯示部分的代碼,可能會比較複雜(比如 Email 的展示方式),最好是将顯示部分的代碼邏輯拆分成獨立的類。

因為代碼中涉及線程操作,并且調用了 Aggregator 的靜态函數,是以代碼的可測試性不好。