作者简介
Aaron,携程移动开发专家,关注网络优化、移动端性能优化。
团队热招岗位:高级Android开发工程师、资深前端开发工程师、高级Java研发工程师
一、背景 二、技术方案 2.1 数据采集 2.2 数据处理 2.3 结果输出 三、落地效果 四、未来展望
自从2010年携程推出”无线战略“,并发布移动端APP以来,无线研发团队对于客户端网络性能的优化就一直没有停止过。经过这十几年持续不断的优化,目前携程的端到端网络性能已经处于一个相当不错的水平,大盘数据趋于稳定,优化也随之进入 ”深水区“,提升难度巨大。
结合线上的一系列客诉反馈,我们发现即使大盘的数据再优秀,用户网络表现不佳的个例case仍然层出不穷,排查后大部分被我们归因到"弱网”。这部分“弱网”长尾数据相比大盘均值仍有巨大的提升空间,如果可以针对性优化的话,对于提升整体用户体验和减少客诉都有非常明确的价值。
既然要优化“弱网”,那第一步一定是建立相应的“弱网识别模型”,准确识别出弱网场景,本文即探讨携程在弱网识别方面的技术探索,包含技术选型细节和关键的路径思考,欢迎沟通交流。
二、技术方案
携程弱网识别模型的整个工作流程由数据采集、数据处理、结果输出三部分组成,接下来我们顺着流程来逐个剖析相关细节。
2.1 数据采集
2.1.1 可以客观反映网络质量的指标有哪些
说到可以客观反映网络质量的指标,业内定义清晰且获得公认的有如下这些:
HttpRTT:Http请求一次网络往返的耗时,具体口径是客户端从开始发送RequestHeader到收到ResponseHeader第一个字节的时间差
TransportRTT:网络通道上一次数据往返的耗时,具体口径是客户端从开始发送数据到收到服务端返回数据第一个字节的时间差,要减去服务处理的耗时
ThroughPut:网络吞吐量,是指定位时间内网络通道上下行的数据量,具体口径是单位位时间内上行或者下行的数据量除以单位时间,由于上行数据量受到业务因素的影响较大,我们一般仅关注下行
BandwidthDelayProduct:带宽时延乘积,顾名思义是指网络带宽乘以网络时延的结果,即当前网络通道里正在传输的数据总量,是一个复合指标,可以客观反映当前网络承载数据的能力,计算方式是 ThroughPut * TransportRTT
SignalStrength:信号强度,移动互联网时代,设备依靠Wifi或者蜂窝数据接入互联网,信号强度会影响用户的网络表现
NetworkSuccessRate:网络成功率,剔除业务影响的纯网络行为成功率,与网络质量呈正相关,包含建联成功率、传输成功率等
对于网络质量识别,业内做的比较早的是Google的NQE(Network Quality Estimator),国内大多数网络质量识别方案也都参考了Google NQE,NQE中识别模型的输入主要是HttpRTT、TransportRTT、DownstreamThroughput这三个指标。
对于HttpRTT、TransportRTT,在应用层和传输层都有很多方式可以采集到,且口径清晰,所以这两个指标被我们纳入采集范围。
对于DownstreamThroughput,我们实践过程中发现,该指标受到用户行为的影响很大,当用户集中操作大量发送网络请求的时候,该指标就偏高,当用户停止操作阅读数据时,该指标就会偏低甚至长时间得不到更新,考虑到指标的波动性,我们不将此指标纳入采集范围。
既然DownstreamThroughput被排除在外,那由他参与计算的BandwidthDelayProduct也被我们排除。
SignalStrength信号强度由于iOS无法准确获取,考虑到多端一致性,也被我们Pass。
NetworkSuccessRate网络成功率这个指标,可能很少被其他方案提及到,我们提出这个指标并将他纳入采集范围的主要原因是,基于RTT的网络识别模型,在遇到网络波动导致的用户大面积请求失败时,无法获取到有效的RTT值,导致识别的准确性和实时性都收到影响,引入网络成功率可以很好的弥补这个缺陷,最终线上生产环境验证也证明了该指标的必要性。
最终,携程的网络质量识别模型采集HttpRTT、TransportRTT、NetworkSuccessRate作为输入指标。
2.1.3 输入的网络指标如何采集
携程的网络请求,主要有Tcp代理通道、Quic代理通道、Http通道三种网络通道。对于上述提到了三个输入指标,我们从如下网络行为中进行数据采集:
TransportRTT:通道心跳耗时、Tcp通道建联耗时、Http通道建联耗时(Tips:建联耗时不涉及业务处理,近似于纯网络传输耗时,所以我们把他作为TransportRTT;Quic建联约等于Tls握手耗时,且存在0RTT等特性干扰,所以不采用)
HttpRTT:标准Http请求的responseHeader开始接收时间减去RequestHeader的开始发送时间、自定义网络通道请求的开始接收时间减去开始发送时间
NetworkSuccessRate:Tcp建联成功状态、Quic建联成功状态、心跳成功状态、Http请求是否完整接收到Response
对于标准的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状态码
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
最小数量限制,当窗口内数据过少时,会放大单条数据的影响,导致结果毛刺增多,所以我们限制最小计算窗口的数据条数为5
最大时间限制,为了数据实时性的考虑,比较旧的数据不参与计算,目前采用的阈值为5mins
上文提到的各种阈值设置,均可通过配置系统更新。
2.2.2 动态权重计算
弱网识别模型的原理简单来说就是将窗口内的一组数据经过一系列处理后,得出一个最终值,再用这个最终值与对应的弱网阈值比较来得出是否是弱网。
出于实时性的考虑,我们希望距离当前时间越近的数据权重越高,所以要用到动态权重的算法,这里我们比较推荐的是”半衰期动态权重“和”反正切动态权重“两种算法。
半衰期动态权重
半衰期顾名思义,即每经过一个固定的时间,权重降低为之前的一半。这里衰减幅度和周期都是可以自定义的,计算公式如下:
每秒衰减因子 = pow(衰减幅度, 1.0 / 衰减周期);衰减幅度为浮点型,取值范围 0~1,衰减周期为整形,单位为秒
动态权重 = pow(每秒衰减因子, abs(now - 数据采集时间))
横坐标为数据采集时间距今的时间差,纵坐标为权重,从图上可以清晰看到,随着时间差增大,权重无限趋近于0。
半衰期动态权重也是Google NQE采用的权重计算方案,Google采用的周期是每60秒降低50%,相关代码详见附录3,部分核心代码如下:
double GetWeightMultiplierPerSecond(
const std::map<std::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,
std::vector<WeightedObservation>* weighted_observations,
double* total_weight) const {
base::TimeDelta time_since_sample_taken = now - observation.timestamp();
double time_weight =
pow(weight_multiplier_per_second_, time_since_sample_taken.InSeconds());
…
}
y=arctan(x)反正切函数在第一象限的取值范围为0~Pi/2,我们将arctan(x)取反,向上平移Pi/2,然后除以Pi/2,函数曲线即可在第一象限随着x增大y的取值从1趋近于0。我们还可以使用一个斜率系数来控制权重降低的趋势快慢,公式推导过程如下:
动态权重 = (Pi / 2 - arctan(abs(now - 数据采集时间) * 斜率系数)) / (Pi / 2) = 1 - arctan(abs(now - 数据采集时间) * 斜率系数) / Pi * 2;斜率系数为浮点型,取值范围为0~1,系数越小,权重降低的越缓慢。
以斜率系数为1/20为例,对应的函数曲线如下:
和前文的半衰期动态权重相同,横坐标为数据采集时间距今的时间差,纵坐标为权重,随着时间差增大,权重趋近于0,两种动态权重算法效果类似。
反正切动态权重的实现代码如下:
static double _nqe_getWeight(double targetTime) {
……
double interval = now - targetTime;
/// 曲率系数,数值越小权重降低的越缓慢
double rate = 20.0 / 1;
return 1.0 - atan(interval * rate) / M_PI_2;
}
携程最终采用的也是半衰期动态权重的方案,出于实时性考虑,最终线上验证后采用的衰减幅度为0.3,衰减幅度为60秒,供参考。
2.2.3 RTT指标加权中值计算
在确定了单条数据的权重之后,对于RTT的数值计算,我们第一个想到的是加权平均,但是加权平均很容易收到高权重脏数据的影响,准确性堪忧,所以我们改用了“加权中值”。
加权中值的计算方式是,将窗口内的数据按照数值大小升序排列,然后从头遍历数据,累加权重大于等于总权重的一半时,停止遍历,当前遍历到的数值即为最终的加权中值。
NQE对于TransportRTT和HttpRTT处理,也是使用的这种方式,相关代码详见附录4,部分核心代码如下:
std::optional<int32_t> ObservationBuffer::GetPercentile(
base::TimeTicks begin_timestamp,
int32_t current_signal_strength,
int percentile,
size_t* observations_count) const {
……
// 此处的percentile值为50,即取中值
double desired_weight = percentile / 100.0 * total_weight;
double cumulative_weight_seen_so_far = 0.0;
for (const auto& weighted_observation : weighted_observations) {
cumulative_weight_seen_so_far += weighted_observation.weight;
if (cumulative_weight_seen_so_far >= desired_weight)
return weighted_observation.value;
}
// Computation may reach here due to floating point errors. This may happen
// if |percentile| was 100 (or close to 100), and |desired_weight| was
// slightly larger than |total_weight| (due to floating point errors).
// In this case, we return the highest |value| among all observations.
// This is same as value of the last observation in the sorted vector.
return weighted_observations.at(weighted_observations.size() - 1).value;
}
对于成功率,我们的NQEMetrics结构体内定义了单次成功状态isSuccessed,单条数据的加权成功率为 (NQEMetrics.isSuccessed ? 1 : 0) * weight,整体的加权成功率为加权成功率总和除以总权重。
相关代码实现如下:
extern double _calculateSuccessRateByWeight(const vector<CTNQEMetrics> &metrics, uint64_t types, const shared_ptr<NQEConfig> config) {
……
uint64_t totalValidCount = 0;
double totalWeights = 0.0;
double totalSuccessRate = 0.0;
for (const auto& m : metrics) {
/// 过滤需要的数据
if ((m.source & types) == 0) {
continue;
}
/// 累计总权重和总成功率
totalValidCount++;
totalWeights += m.weight;
totalSuccessRate += (m.isSuccessed ? 1 : 0) * m.weight;
}
/// 数据不足
if (totalValidCount < config->minValidWindowSize) {
return NQE_INVALID_RATE_VALUE;
}
if (totalWeights <= 0.0) {
return NQE_INVALID_RATE_VALUE;
}
return totalSuccessRate / totalWeights;
}
网络质量识别不仅需要准确,实时性也非常重要,在网络质量切换时模型识别的时间越短越好。前文已经提到了TransportRTT、HttpRTT、NetworkSuccessRate三个核心指标的计算,但是在线上实际验证的过程中,我们发现在网络完全不可用成功率跌0后,识别模型对于网络状态的恢复感知很慢,原因是成功率的攀升需要较长的时间。
针对这个极端的case,我们引入了一个“成功率趋势”的新指标,来优化模型的实时性,在成功率未达阈值当时有明显趋势时,提前切换网络质量状态。成功率趋势是指一段时间内成功率连续上升或者下降的幅度,浮点类型,取值范围-1 ~ +1。
成功率趋势初始值为0,计算方式如下:
1)在每次更新成功率时,计算更新前后成功率的差值
如果差值为正,则成功率向好
如果当前成功率趋势值为正,则向好趋势持续,成功率趋势加上当前差值 如果当前成功率趋势值为负,则成功率趋势由坏转好,成功率趋势重置为当前差值
如果当前成功率趋势值为正,则成功率趋势由好转坏,成功率趋势重置为当前差值 如果当前成功率趋势值为负,则向坏趋势持续,成功率趋势加上当前差值(负值)
void NQE::_updateSuccessRateTrend() {
auto oldRate;
auto newRate;
if (oldRate < 0 || newRate < 0) {
_successRateContinuousDiff = 0;
return;
}
auto diff = newRate - oldRate;
/// 数据错误,不做处理
if (abs(diff) > 1) {
_successRateContinuousDiff = 0;
return;
}
/// diff小于0.01,作为毛刺处理,不影响趋势变化
if (abs(diff) < 0.01) {
_successRateContinuousDiff += diff;
return;
}
/// 计算连续diff
if (diff > 0 && _successRateContinuousDiff > 0) {
_successRateContinuousDiff += diff;
} else if (diff < 0 && _successRateContinuousDiff < 0) {
_successRateContinuousDiff += diff;
} else {
_successRateContinuousDiff = diff;
}
}
单RTT识别模型,网络质量切换后识别较慢,且存在连续切换场景识别不出弱网的情况
RTT+成功率模型,切换识别速度较单RTT模型提升约50%,成功率跌0后的Bad切Good识别明显较慢
RTT+成功率+成功率趋势模型,切换识别速度较单RTT模型提升约70%,Bad切Good识别速度明显提升
2.3 结果输出
2.3.1 网络质量定义
识别模型对外输出的是一个网络质量的枚举值,Google NQE对于网络质量的定义如下,源码详见附录5:
enum EffectiveConnectionType {
// Effective connection type reported when the network quality is unknown.
EFFECTIVE_CONNECTION_TYPE_UNKNOWN = 0,
// Effective connection type reported when the Internet is unreachable
// because the device does not have a connection (as reported by underlying
// platform APIs). Note that due to rare but potential bugs in the platform
// APIs, it is possible that effective connection type is reported as
// EFFECTIVE_CONNECTION_TYPE_OFFLINE. Callers must use caution when using
// acting on this.
EFFECTIVE_CONNECTION_TYPE_OFFLINE,
// Effective connection type reported when the network has the quality of a
// poor 2G connection.
EFFECTIVE_CONNECTION_TYPE_SLOW_2G,
// Effective connection type reported when the network has the quality of a
// faster 2G connection.
EFFECTIVE_CONNECTION_TYPE_2G,
// Effective connection type reported when the network has the quality of a 3G
// connection.
EFFECTIVE_CONNECTION_TYPE_3G,
// Effective connection type reported when the network has the quality of a 4G
// connection.
EFFECTIVE_CONNECTION_TYPE_4G,
// Last value of the effective connection type. This value is unused.
EFFECTIVE_CONNECTION_TYPE_LAST,
};
所以我们在定义接口的时候,对于枚举的设计考虑最多的就是理解成本,结合开发同学最想知道的“是不是弱网”,我们的接口定义如下:
typedef enum : int64_t {
/// 未知状态,初始状态或者无有效计算窗口时会进入此状态
NetworkQualityTypeUnknown = 0,
/// 离线状态,网络不可用
NetworkQualityTypeOffline = 1,
/// 弱网状态
NetworkQualityTypeBad = 2,
/// 正常网络状态
NetworkQualityTypeGood = 3
} NetworkQualityType;
2.3.2 网络质量计算方式
NetworkQualityTypeUnknown 是在初始化或者网络切换后的一段时间内,数据不足无法得出网络质量,会进入此状态。
NetworkQualityTypeOffline 的触发条件很单一,就是操作系统识别到无网络连接,具体的获取方式由各平台自行实现,例如iOS可以通过Reachability获取,官方Demo详见附录6。
NetworkQualityTypeBad 也就是我们最核心的“弱网”状态,计算方式是上文提到的TransportRTT、HttpRTT两个指标任一指标触发弱网阈值,或者NetworkSuccessRate和SuccessRateTrend同时满足弱网阈值。
NetworkQualityTypeGood 是指正常的网络质量状态,上述三种网络质量类型讲完后,这个类型就简单了,即非上述三种情况的场景,归类到Good,这也是设计上占比最高的网络质量类型。
识别模型的运转流程如下:
数据队列在初始化和网络连接状态变化两个时机会被重置,充值后网络质量类型进入 NetworkQualityTypeUnknown
网络连接状态进入无连接状态时,数据队列被清空,网络质量类型直接进入 NetworkQualityTypeOffline,在网络类型变为可用类型前,数据队列不接受数据注入,且不进行计算
数据队列的数据变化触发质量计算,出于资源开销考虑,要限制计算的频次,我们采用计算间隔和新增数据量两个阈值限制,计算间隔大于60s或者新增数据量超过10条才会触发计算;同时也暴露了对外接口,业务可按需强制刷新计算结果
关于主动网络探测,可结合自身的业务需求按需实现,目前携程的APP在使用时网络数据更新比较频繁,无需补充主动探测数据
模型核心的计算逻辑,就是将加工后得到的各网络指标与对应的弱网阈值进行对比,从而获得是否进入弱网的结果,关于弱网阈值的制定上,我们经历了如下两个阶段:
第一阶段,主要参考NQE EFFECTIVE_CONNECTION_TYPE_2G 的阈值定义
HttpRTT > 1726ms
TransportRTT > 1531ms
成功率按照内部讨论的预期设置为 NetworkSuccessRate < 90%
成功率趋势阈值设置为 SuccessRateTrend < 0.1,即成功率连续向好增加超过10pp,即使成功率小于90%,也从Bad切换为Good,这个指标主要是为了提升Bad切Good的速度
理论上,我们希望识别模型的入参与当下计算出的网络质量类型所匹配,例如当前注入NQEMetrics数据的HttpRTT <= 1726ms ,那我们预期当前计算出的网络质量类型就是Good。但是弱网的决策逻辑是相对复杂的,需要考虑到各种因素,以下两点会造成弱网状态下的入参数据不一定符合弱网阈值定义:
1)弱网的计算是对过去已经发生网络行为的分析,具有一定的滞后性,所以在识别结果切换附近,必然有部分的原数据已经满足下一阶段的网络质量定义
2)弱网的决策是对多个指标的复合计算,所以在识别到弱网状态时,不一定所有的指标原数据都符合弱网定义,比如由HttpRTT触发弱网时,当前的TransportRTT数据可能表现良好
终上两点原因,弱网分类下必然有一定的非弱网数据,这里的误差数据占比与识别准确率负相关,误差数据占比越低,识别准确率越高。所以想到这里,我们的模型识别准确率的指标计算口径就有了:
模型弱网识别准确性 = 100% - 弱网状态下不符合弱网阈值定义原数据占比
最终携程90%准确率的模型对应的弱网阈值如下(不同业务场景的网络请求差别较大,仅供大家参考):
HttpRTT > 1220ms;这个值是线上HttpRTT的TP98值,与弱网占比相近
TransportRTT > 520ms;同线上TP98值
NetworkSuccessRate < 90%
SuccessRateTrend < 0.2;之前的0.1导致模型的结果切换过于频繁,最终调整到了0.2
三、落地效果
考虑到识别模型要支持多平台(iOS、Android、Harmony等),所以我们在一开始实现方案时就采用了C++作为开发语言,天然支持了多平台,各平台只需要实现上层数据采集和注入模型的少量逻辑即可完成模型的接入。相同的代码实现和弱网标准,也方便我们在不同的平台间直接对标数据,发现各平台的问题针对性优化。
目前携程的网络质量识别模型,已经在iOS、Android平台完成接入并大面积投产,网络质量数据与集团的APM监控平台打通,形成了携程官方统一的网络质量标准,在网络排障、框架网络优化、业务网络优化等多种场景下扮演重要角色,弱网优化相关的内容我们会在后面相关的专题内继续分享,此处不再赘述。
最终网络质量相关的分布数据如下(数据为实验采集,不代表携程真实业务情况,仅参考):
网络质量分布:
各网络质量下对应的请求性能数据:
四、未来展望
网络质量识别模型的完成只是我们网络优化的开始,后续还有很多的工作需要我们继续努力,未来一段时间我们会从以下几个方面继续推进:
1)持续推进各平台、各独立APP的网络质量识别模型接入,完成携程终端全平台的网络质量模型覆盖
2)做好识别模型的防劣化工作,解决各业务场景的bad case,坚守现阶段识别准确率和实时性的标准水位
3)推出携程内部的“网络性能白皮书”,从APP、系统平台、网络质量、成功率、全链路耗时等各维度解析公司内部各业务线的网络表现,形成内部的网络性能数据基线,为业务优化提供参考
4)借助现有的弱网标准和识别能力,从网络框架侧和业务侧两个不同的角度进行弱网优化,提高整体网络表现;当下海外市场是业务发力的重点,海外场景的网络表现也明显弱于国内,我们会针对海外场景从弱网的角度进行重点优化。
携程目前已经针对弱网场景推出了一系列优化策略,部分策略已经取得非常不错的收益,后续我们会继续推进,也会持续分享输出。