01
背景
無論是iOS還是Android系統的裝置,線上上運作時受硬體、網絡環境、代碼品質等多方面因素影響,可能會導緻性能問題,這一類問題有些在開發階段是發現不了的。如何線上上始終為使用者提供一個相對順暢的使用者體驗,是用戶端開發需要考慮的一個問題。
02
服務降級、熔斷
服務端有降級機制和熔斷機制,在設計用戶端降級系統時可以參照服務端現有方案。例如發生性能問題或網絡擁堵的情況,需要減少裝置和網絡的負擔,等恢複後再進行政策更新。
服務端降級機制,當服務端出現整體負載較大,或因為特殊原因出現資料錯誤,則會觸發降級。不同的情況對應不同的降級政策。例如資料原因導緻的,可以不去讀DB資料庫,直接傳回緩存資料。從使用者的角度來看,可能是資料更新不及時,但可以正常顯示。
服務端熔斷機制,熔斷機制是比降級更嚴重的情況,當服務端中某個微服務不可用,或響應時間過長,則會觸發熔斷,不再調用這個服務。從使用者的角度來看,可能是頭像不能顯示,或者頁面部分模版未顯示,購物車商品結算不能使用優惠券等。
03
方案簡述
首先,我們需要捋清楚,用戶端需要處理的問題都有哪些。我将其分為兩類,性能和網速,性能又可以細化為CPU、記憶體、電量三類,這三類都會對App的運作造成影響。同樣的,我們不能直接把性能分為好和壞兩種,而是需要通過枚舉的方式,将其細化為不同等級。
這裡以iOS系統為例,我們需要對iOS裝置CPU、記憶體、電量、網速進行實時監控,可以設定一個合理的間隔區間。在發生前面的性能問題時,通過對不同類型的問題進行門檻值計算,進而得出對應的等級。如果級别發生變化,則通過通知的方式,告訴業務方降級或更新。
當發生降級時,業務方進行對應的降級處理,例如降低網絡請求的圖檔尺寸。通過業務降級處理,降低系統性能消耗,讓CPU、記憶體逐漸恢複到正常區間,再進行業務更新,恢複原有業務處理規則。
通過上述方式,來保證發生性能或網絡問題時,使用者依然可以較為流暢的使用App,并且App内功能的正常使用不受影響。
04
整體設計
動态降級系統的設計,主要分為三個部分,職責劃分如下。
DynamicLevelManager:調用monitor和decision完成分級計算,當級别發生變化時,通過通知的方式告知業務方。
DynamicLevelMonitor:監控關鍵性能名額,由manager定時調用。
DynamicLevelDecision:由manager将收集到的性能名額交給decision,desicion對于名額進行統一計算,并決定性能級别,并傳回給manager。
下面是脫敏後的僞代碼,主要是表達清楚設計思路。demo代碼也可以直接跑起來,如有需要可以直接copy拿去用。
05
DynamicLevelManager
DynamicLevelManager為動态降級系統的核心類,後面都稱為manager,當App啟動時通過openLevelAnalyze方法注冊監聽,進而開啟一個由dispatch_source_t實作的loop,每隔1.5秒執行一次,執行時會觸發dispatch_source_set_event_handler的回調方法。dispatch_source_t由手機硬體時鐘觸發,不受主線程卡頓影響,監聽相對精确很多。
/// 開啟動态降級監控系統
- (void)openLevelAnalyze {
self.sourceHandle = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(self.sourceHandle, dispatch_time(DISPATCH_TIME_NOW, 0), 1.5 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.sourceHandle, ^{
/// 計算綜合性能級别
CGFloat cpuUsageValue = [[DynamicLevelMonitor sharedInstance] cpuUsageForApp];
NSInteger memoryUsageValue = [[DynamicLevelMonitor sharedInstance] useMemoryForApp];
CGFloat batteryUsageValue = [[DynamicLevelMonitor sharedInstance] batteryUsageForApp];
[[DynamicLevelDecision sharedInstance] calculatePerformanceLevelWithMemoryValue:memoryUsageValue
cpuValue:cpuUsageValue
batteryValue:batteryUsageValue
completionBlock:^(MemoryUsageLevel memoryLevel, CPUUsageLevel cpuLevel, BatteryUsageLevel batteryLevel, MultiplePerformanceLevel performanceLevel) {
/// 判斷級别是否發生變化,發送性能降級或恢複原有等級的通知
if (performanceLevel != self.currentPerformanceLevel) {
[self postPerformanceNotifiWithPerformanceLevel:performanceLevel
memoryLevel:memoryLevel
cpuLevel:cpuLevel
batteryLevel:batteryLevel];
}
}];
/// 計算網絡性能級别
CGFloat networkSpeed = [[QUICManager shareQUICManager] currentNetworkSpeed];
[[DynamicLevelDecision sharedInstance] calculateNetworkLevelWithNetworkSpeed:networkSpeed completionBlock:^(NetworkSpeedLevel speedLevel) {
/// 判斷級别是否發生變化,發送網絡降級或恢複原有等級的通知
if (speedLevel != self.currentNetworkSpeedLevel) {
[self postPerformanceNotifiWithNetworkSpeedLevel:speedLevel];
}
}];
});
dispatch_resume(self.sourceHandle);
}
- (void)closeLevelAnalyze {
dispatch_source_cancel(self.sourceHandle);
}
/// 發送性能降級或恢複原有等級的通知
- (void)postPerformanceNotifiWithPerformanceLevel:(MultiplePerformanceLevel)performanceLevel
memoryLevel:(MemoryUsageLevel)memoryLevel
cpuLevel:(CPUUsageLevel)cpuLevel
batteryLevel:(BatteryUsageLevel)batteryLevel {
[[NSNotificationCenter defaultCenter] postNotificationName:@"PerformanceLevelChanged"
object:nil
userInfo:@{@"performanceLevel": @(performanceLevel),
@"memoryLevel": @(memoryLevel),
@"cpuLevel": @(cpuLevel),
@"batteryLevel": @(batteryLevel)}];
}
/// 發送網絡降級或恢複原有等級的通知
- (void)postPerformanceNotifiWithNetworkSpeedLevel:(NetworkSpeedLevel)networkSpeedLevel {
[[NSNotificationCenter defaultCenter] postNotificationName:@"NetworkSpeedLevelChanged"
object:nil
userInfo:@{@"networkSpeedLevel": @(networkSpeedLevel)}];
}
manager對外界提供的消息回調分為兩類,一個是由CPU、記憶體、電量,綜合計算出的性能分級performanceLevel,另一個是網速分級networkSpeedLevel。
5.1 performanceLevel
在handler方法中會調用monitor的cpuUsageForApp方法擷取CPU的使用率,取值範圍是0-1,當CPU發生超頻,也有超過1的情況。調用monitor的useMemoryForApp方法擷取記憶體使用率,取值範圍是0-1。調用monitor的batteryUsageForApp方法擷取剩餘電量,取值範圍是0-100。
擷取這些資訊後,調用decision的calculatePerformanceLevel方法,将資訊交由decision進行綜合計算,計算後傳回結果為四個值。
1、performanceLevel:綜合性能分級
2、memoryLevel:記憶體占用率分級
3、cpuLevel:CPU使用率分級
4、batteryLevel:電量使用分級
這裡的核心就是performanceLevel綜合分級,類型為MultiplePerformanceLevel,這是根據記憶體、電量、CPU綜合計算出來的結果。上述的四個值通過枚舉定義,具體定義如下。
/// 綜合性能枚舉
typedef NS_ENUM(NSUInteger, MultiplePerformanceLevel) {
MultiplePerformanceLevelNormal,
MultiplePerformanceLevelLow,
MultiplePerformanceLevelVeryLow,
};
/// cpu使用率枚舉,overclock表示cpu已超頻
typedef NS_ENUM(NSUInteger, CPUUsageLevel) {
CPUUsageLevelLow,
CPUUsageLevelHigh,
CPUUsageLevelOverclock,
};
/// 記憶體使用級别枚舉
typedef NS_ENUM(NSUInteger, MemoryUsageLevel) {
MemoryUsageLevelLow,
MemoryUsageLevelMiddle,
MemoryUsageLevelHigh,
};
/// 電量使用枚舉,high表示使用較多,電量剩餘1%
typedef NS_ENUM(NSUInteger, BatteryUsageLevel) {
BatteryUsageLevelLow,
BatteryUsageLevelMiddle,
BatteryUsageLevelHigh,
};
拿到這些性能level後,會判斷performanceLevel是否發生變化,如果低于目前level,則發生降級。如果高于目前level,則表示性能恢複。随後會調用NSNotificationCenter以通知的形式進行消息通知,通知名為PerformanceLevelChanged,并這四個分級參數傳遞過去。如果level沒有發生改變,則不會發出消息通知。
5.2 speedLevel
另一個是網速分級,這個名額并沒有歸類于性能分級中,因為和性能分級并不是一類。
在handler方法中會調用網絡庫QUICManager的currentNetworkSpeed方法,獲得目前網速,機關是kb每秒。這裡的QUICManager是公司自研的網絡庫,提供目前實時網速。
拿到網速資料後,會調用decision的calculateNetworkLevel方法,交給decision進行計算。decision會傳回一個speedLevel目前網速級别,其類型是NetworkSpeedLevel,分為三個級别。
/// 目前網速枚舉
typedef NS_ENUM(NSUInteger, NetworkSpeedLevel) {
NetworkSpeedLevelNormal,
NetworkSpeedLevelLow,
NetworkSpeedLevelVeryLow,
};
拿到這些資訊後,會判斷speedLevel是否發生改變,如果低于目前level,則表示網速發生劣化。如果高于目前level,則表示網速恢複。随後會調用NSNotificationCenter以通知的形式進行消息通知,通知名為NetworkSpeedLevelChanged,并将speedLevel參數傳遞過去。如果level沒有發生改變,則不會發出消息通知。
06
DynamicLevelDecision
Decision負責接收manager傳入的資料資訊,傳回對應的性能級别。在計算時,會先對傳入的參數進行計算,計算出對應單個性能參數的level分級,再計算performanceLevel分級。
/// 進行綜合性能計算
- (void)calculatePerformanceLevelWithMemoryValue:(NSInteger)memoryValue
cpuValue:(CGFloat)cpuValue
batteryValue:(CGFloat)batteryValue
completionBlock:(DynamicPerformanceLevelBlock)completionBlock {
MemoryUsageLevel memoryLevel = [self calculateMemoryUsageLevelWithMemoryValue:memoryValue];
CPUUsageLevel cpuLevel = [self calculateCPUUsageLevelWithCpuValue:cpuValue];
BatteryUsageLevel batteryLevel = [self calculateBatteryUsageLevelWithBatteryValue:batteryValue];
MultiplePerformanceLevel performanceLevel = MultiplePerformanceLevelNormal;
if (batteryLevel == BatteryUsageLevelHigh) {
performanceLevel = MultiplePerformanceLevelVeryLow;
}
else if (cpuLevel == CPUUsageLevelOverclock && memoryLevel == MemoryUsageLevelHigh) {
performanceLevel = MultiplePerformanceLevelVeryLow;
}
else if (batteryLevel >= 1 && memoryLevel >= 1) {
performanceLevel = MultiplePerformanceLevelLow;
}
else if (batteryLevel >= 1 && cpuLevel >= 1) {
performanceLevel = MultiplePerformanceLevelLow;
}
else if (memoryLevel >= 1 && cpuLevel >= 1) {
performanceLevel = MultiplePerformanceLevelLow;
}
if (completionBlock) {
completionBlock(memoryLevel, cpuLevel, batteryLevel, performanceLevel);
}
}
/// 進行網速級别計算
- (void)calculateNetworkLevelWithNetworkSpeed:(CGFloat)networkSpeed
completionBlock:(DynamicNetworkSpeedLevelBlock)completionBlock {
[self.networkSpeedArray addObject:@(networkSpeed)];
if (self.networkSpeedArray.count > 5) {
[self.networkSpeedArray removeObjectsInRange:NSMakeRange(0, self.networkSpeedArray.count - 5)];
}
__block NSInteger middleCount = 0;
__block NSInteger highCount = 0;
[self.networkSpeedArray enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.floatValue <= 200) {
middleCount++;
}
if (obj.floatValue <= 50) {
highCount++;
}
}];
NetworkSpeedLevel networkThreshold = NetworkSpeedLevelNormal;
if (highCount >= 3) {
networkThreshold = NetworkSpeedLevelVeryLow;
} else if (middleCount >= 3) {
networkThreshold = NetworkSpeedLevelLow;
}
if (completionBlock) {
completionBlock(networkThreshold);
}
}
/// 計算記憶體使用級别
- (MemoryUsageLevel)calculateMemoryUsageLevelWithMemoryValue:(NSInteger)memoryValue {
[self.memoryUsageArray addObject:@(memoryValue)];
if (self.memoryUsageArray.count > 5) {
[self.memoryUsageArray removeObjectsInRange:NSMakeRange(0, self.memoryUsageArray.count - 5)];
}
__block NSInteger middleCount = 0;
__block NSInteger highCount = 0;
[self.memoryUsageArray enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.floatValue > 0.45) {
highCount++;
}
if (obj.floatValue > 0.4) {
middleCount++;
}
}];
MemoryUsageLevel memoryThreshold = MemoryUsageLevelLow;
if (highCount >= 3) {
memoryThreshold = MemoryUsageLevelHigh;
} else if (middleCount >= 3) {
memoryThreshold = MemoryUsageLevelMiddle;
}
return memoryThreshold;
}
/// 計算CPU使用級别
- (CPUUsageLevel)calculateCPUUsageLevelWithCpuValue:(CGFloat)cpuValue {
[self.cpuUsageArray addObject:@(cpuValue)];
/// cpu level calculate
return CPUUsageLevelLow;
}
/// 計算電量使用級别
- (BatteryUsageLevel)calculateBatteryUsageLevelWithBatteryValue:(CGFloat)batteryValue {
[self.batteryUsageArray addObject:@(batteryValue)];
/// battery level calculate
return BatteryUsageLevelLow;
}
6.1 單個性能參數level計算
CPU:傳入數值>0.8,也就是CPU使用率超過80%,CPUUsageLevel等于levelMiddle,如果CPU使用率超過100%,則發生CPU超頻,CPUUsageLevel等于levelHigh。
記憶體:因為在iOS系統中,App最多可以使用裝置總記憶體的50%,記憶體使用率超過40%,MemoryUsageLevel等于levelMiddle,如果記憶體使用率超過45%,MemoryUsageLevel等于levelHigh。
電量:傳入數值<6%,則表示低電量,BatteryUsageLevel等于levelMiddle,傳入數值<1%,則表示到達臨界值,BatteryUsageLevel等于levelHigh。
6.2 performanceLevel計算
得到上述三個性能參數的level後,manager會調用decision的calculatePerformanceLevel方法,通過方法傳回值獲得performanceLevel,其類型為MultiplePerformanceLevel。計算performanceLevel時,根據先後順序會有如下條件,條件之間彼此互斥。
1、判斷batteryLevel是否等于levelHigh,如果是的話表示電量接近臨界值,則直接将performanceLevel設定為veryLow;
2、cpuLevel等于overclock,memoryLevel等于high,則表示CPU處于超頻狀态,并且記憶體占用也處于非常高的狀态,此時很容易被系統強殺造成OOM,直接将performanceLevel設定為veryLow;
3、batteryLevel、cpuLevel、memoryLevel,任意兩者構成middle或high,則将performanceLevel設定為low。
6.3 speedLevel計算
Manager調用decision的calculateNetworkLevel方法,擷取網絡變化名額。在計算speedLevel時,傳入的網速小于200kb/s,則表示網速較低,将speedLevel設定為low,傳入的網速小于50kb/s,則表示網速非常慢,将speedLevel設定為veryLow。
6.3.1 性能計算視窗
在擷取性能參數時,不能以某一個時間點的性能資料作為計算依據,而是以一個時間視窗的多條性能資料作為計算依據,這樣更能反映這個時間段的綜合性能。
性能計算視窗是基于handler的回調,收集從目前次到前四次,這連續五次的資料,綜合進行計算。例如NetworkSpeedLevel的計算,如果超過三次網速都小于50kb/s,則NetworkSpeedLevel等于veryLow,如果超過三次網速都小于200kb/s,則NetworkSpeedLevel等于low。
從實作的角度,性能計算視窗時通過NSMutableArray實作的,通過FIFO政策進行淘汰,始終保留相鄰的五條資料。
07
DynamicLevelMonitor
Monitor的作用是提供擷取系統性能資訊的方法,在handler中調用的三個monitor的方法,内部實作如下。
/// 目前app記憶體使用量,傳回機關百分比
- (NSInteger)useMemoryForApp {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (kernelReturn == KERN_SUCCESS) {
int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
int64_t totalMemory = [[NSProcessInfo processInfo] physicalMemory];
return memoryUsageInByte / totalMemory;
} else {
return -1;
}
}
/// 目前app的CPU使用率
- (CGFloat)cpuUsageForApp {
kern_return_t kr;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;
thread_basic_info_t basic_info_th;
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS)
return -1;
float total_cpu_usage = 0;
for (int i = 0; i < thread_count; i++) {
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[i], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS) {
return -1;
}
basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
total_cpu_usage += basic_info_th->cpu_usage / (float)TH_USAGE_SCALE;
}
}
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
return total_cpu_usage;
}
UseMemoryForApp方法實作,通過系統task_info函數擷取到目前App已使用的記憶體,通過NSProcessInfo的physicalMemory方法獲得裝置的實體記憶體,二者的機關都是bytes,通過計算task_info占physicalMemory的百分比,得到App已使用的記憶體的百分比。
CpuUsageForApp方法實作,通過系統task_threads函數獲得所有線程的資訊thread_list,thread_list是一個數組,周遊thread_list得到thread_info_t單個線程的資訊,累加thread_info_t的cpu_usage屬性(cpu_usage屬性表示目前線程使用CPU的百分比),得到總的CPU使用占比。
BatteryUsageForApp方法實作,設定系統UIDevice的batteryMonitoringEnabled為true,開啟電量監聽。并通過通知接收電量變化的回調,回調的機關是0~1,再乘以100傳回給manager。
08
業務方
業務方收到PerformanceLevelChanged的消息後,可以基于performanceLevel的綜合性能進行判斷,如果是veryLow,可以暫停流内秒播處理,也就是在視訊流中,滑動到下一條視訊不會自動播放。
也可以基于單個性能level進行判斷,例如batteryLevel名額為middle或low,也就是電量低于6%時,可以提示使用者先不進行視訊檔案緩存等非常消耗性能的操作,以避免因為消耗性能的操作,導緻手機自動關機。
業務方收到NetworkSpeedLevelChanged的消息後,可以根據通知傳過來的speedLevel參數,low和veryLow可以有不同的處理。例如可以降低向服務端擷取圖檔的尺寸,low可以将圖檔尺寸壓縮80%,如果是veryLow可以将圖檔尺寸壓縮60%,可以明顯提升弱網下,向伺服器擷取圖檔的速度。壓縮比率在請求圖檔URL時,在URL中拼接發送給服務端,服務端會傳回對應壓縮比率的圖檔。
作者:劉壯
來源-微信公衆号:搜狐技術産品
出處:https://mp.weixin.qq.com/s/bBmmnUyU3KwJwgvaybleTg