正文
对于自定义的Tcp、Quic代理通道,按照上述口径在网络通道相关状态回调内统计数据即可,自定义实现参考价值不大,这里就不过多赘述。
对于标准的Http请求,我们可以通过获取系统网络框架返回的Metric信息或者监听请求的状态流转来获取网络指标。
对于iOS
,iOS 10之后NSURLSession支持通过NSURLSessionTaskDelegate的协议方法URLSession:task:didFinishCollectingMetrics:获取到请求的Metric信息,详细信息见附录1,单次请求的Metric定义如下图:
-
TransportRTT = connectEnd - connectStart - secureConnectionEnd + secureConnectionStart;建联耗时要减去Tls的耗时,连接复用时,相关字段为空值,不纳入计算
-
HttpRTT = responseStart - requestStart
-
NetworkSuccessStatus = responseEnd 且没有传输错误;网络成功率只关心传输是否成功,不需要关注Response的http状态码
对于Android
,系统网络框架OkHttp支持添加EventListener来获取Http请求的状态流转信息,可以在各状态回调内记录时间戳来计算RTT,详细信息见附录2,单次请求的Events定义如下图:
-
TransportRTT = connectEnd - connectStart - secureConnectEnd + secureConnectStart
-
HttpRTT = responseHeadersStart - requestHeadersStart
-
NetworkSuccessStatus = responseBodyEnd 且没有传输错误
依照上述方法收集到网络数据后,我们把数据封装成对应的结构体,注入识别模型,携程对于网络数据结构体的定义如下,方便大家参考:
typedef enum : int64_t {
NQEMetricsSourceTypeInvalid = 0, // 0
NQEMetricsSourceTypeTcpConnect = 1 << 0, // 1
NQEMetricsSourceTypeQuicConnect = 1 << 1, // 2
NQEMetricsSourceTypeHttpRequest = 1 << 2, // 4
NQEMetricsSourceTypeQuicRequest = 1 << 3, // 8
NQEMetricsSourceTypeHeartBeat = 1 << 4, // 16
......
} NQEMetricsSourceType;
struct NQEMetrics {
// 本次采集到的数据来源,可以是多个枚举值的或值
// 例如一次没有连接复用http请求,source = TcpConnect|HttpRequest,同时存在transportRTT和httpRTT NQEMetricsSourceType source;
// 本次数据的成功状态,用作成功率计算
bool isSuccessed;
// httpRTT,可为空
double httpRTTInSec;
// transportRTT,可为空
double transportRTTInSec;
// 数据采集时间
double occurrenceTimeInSec;
};
2.2.1 数据过滤和滑动窗口
网络数据采集后,注入到识别模型内,需要一个数据结构来承载,我们采用的是队列。
进入队列前,我们需要先进行数据过滤,筛选掉一些无效的数据,目前采用的筛选策略有如下这些:
-
单条NQEMetrics数据,在isSuccessed=true的情况下,httpRTT、transportRTT至少有一条不为空,否则为无效数据
-
RTT必须大于最小阈值,用来过滤一些类似LocalHost请求的脏数据,目前采用的阈值为10ms
-
RTT必须小于最大阈值,用来过滤前后台切换进程挂起导致的RTT数值偏大,目前采用的阈值为5mins
数据过滤后加入队列,为了实时性和结果准确性,我们处理数据时,会根据两个限制逻辑来确定一个具体的滑动窗口,只让窗口内的数据参与计算,具体窗口限制逻辑如下:
每次计算网络质量时,可以根据这两个限制来确定计算窗口,窗口外的数据可以实时清理出队列,减少内存占用。
上文提到的各种阈值设置,均可通过配置系统更新。
2.2.2 动态权重计算
弱网识别模型的原理简单来说就是将窗口内的一组数据经过一系列处理后,得出一个最终值,再用这个最终值与对应的弱网阈值比较来得出是否是弱网。
出于实时性的考虑,我们希望距离当前时间越近的数据权重越高,所以要用到动态权重的算法,这里我们比较推荐的是”半衰期动态权重“和”反正切动态权重“两种算法。
半衰期动态权重
半衰期顾名思义,即每经过一个固定的时间,权重降低为之前的一半。这里衰减幅度和周期都是可以自定义的,计算公式如下:
-
每秒衰减因子 = pow(衰减幅度, 1.0 / 衰减周期);衰减幅度为浮点型,取值范围 0~1,衰减周期为整形,单位为秒
-
动态权重 = pow(每秒衰减因子, abs(now - 数据采集时间))
以衰减幅度为0.5,衰减周期为60秒为例,对应的函数曲线如下:
横坐标为数据采集时间距今的时间差,纵坐标为权重,从图上可以清晰看到,随着时间差增大,权重无限趋近于0。
半衰期动态权重也是Google NQE采用的权重计算方案,Google采用的周期是每60秒降低50%,相关代码详见附录3,部分核心代码如下:
double GetWeightMultiplierPerSecond(
const std::map<:string std::string>& params) {
// Default value of the half life (in seconds) for computing time weighted
// percentiles. Every half life, the weight of all observations reduces by
// half. Lowering the half life would reduce the weight of older values
// faster.
int half_life_seconds = 60;
int32_t variations_value = 0;
auto it = params.find("HalfLifeSeconds");
if (it != params.end() && base::StringToInt(it->second, &variations_value) &&
variations_value >= 1) {
half_life_seconds = variations_value;
}
DCHECK_GT(half_life_seconds, 0);
return pow(0.5, 1.0 / half_life_seconds);
}
void ObservationBuffer::ComputeWeightedObservations(
const base::TimeTicks& begin_timestamp,
int32_t current_signal_strength,