天天看點

用戶端動态降級系統

作者:閃念基因

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