wenmo8 发布的文章

01 背景

企业为了能够提升线上服务的可靠性和竞争力,需要从代码端的视角来监控自己线上应用的质量和性能,因此,APM系统(应用性能监控系统)应运而生。APM系统是互联网公司最重要的基础设施之一,它可以帮助发现并解决生产环境中遇到的各种问题。

APM系统为了能够实现帮助企业确保自身的IT支撑系统高效地运行,它需要建立一个强大的IT运维管理体系,用来时刻监控IT环境各组件的性能和质量,并且通过多维度实时分析监控指标的异常,快速定位并解决线上应用的问题。其中,网络环境质量是线上服务最基本的性能指标之一,网络耗时,网络请求错误率等指标极大地影响着线上应用的服务质量,APM系统应系统地建立起网络性能的监控。

在爱奇艺移动端APM系统上线之前,网络监控只是针对后端服务,因用户手机性能和网络环境复杂性相关,用户的真实使用体验和后台服务监控之间存在很大差异。因此,我们建立了一套基于用户层面的、实时、多维度的网络监控系统,并建立了针对端上的错误率、劫持率和网络性能的评估标准。经过多年的积累,爱奇艺移动端APM系统已经建立包括崩溃、网络、卡顿、日志、内存、图片等多项监控功能。

本文主要介绍移动端网络监控系统的建立及优化。

02 系统设计

根据移动端网络错误的特点,计算错误率时,将其划分为三类:

网络层错误

(错误发生在三次握手,SSL校验,数据传输过程中,如ConnectException)

HTTP响应错误

(服务端已明确返回错误码,如404)

解析错误

(服务端返回200,但数据格式有问题,导致前端无法正确解析)

由于重试机制的存在,我们只按照一组数据中的最后一次请求的结果,评判该次用户网络行为成功或者失败。

维度信息设计:


在后端存储的字段里,除了上述网络字段,还包括APM平台和用户设备的基础字段信息,这里就不一一赘述。

2.1 SDK设计

因端上网络请求频繁,数据采样需要考虑对手机性能影响的同时,也要保证数据的准确可分析,采样有如下措施可参考:

  ·  云控采样:云控下发采样指令,动态控制数据量;

  ·  合并压缩:缓存数据在达到时间或者数量的阈值时,进行批量压缩投递;

  ·  失败重投:投递失败的数据,会进行本地存储,下次启动尝试投递。

由于数据投递可能滞后,分析时可使用:端上发生时间和投递到达后端时间,利用两个时间戳分别进行分析。

2.2 后端设计

在后端系统设计中,需要支持高数据实时性、较大的存储数据量、灵活的查询和准确及时的报警。具体措施有:

  ·  实时性:秒级别

  ·  存储:支持千万级别数据量的搜索,聚合,TOP等

  ·  报警:分钟级别多维度报警, 通过邮件、短信、热聊、电话等形式通知责任人。


2.3 Web设计

Web展现错误率监控时,主要考虑是:

1. 快速查看整体状态

2. 查看所有业务错误数/错误率/请求数的top列表

3. 可分析具体原因

基于上述三个基本出发点,我们设计了首页,错误分布,占比详情,详情信息,耗时分析,劫持分析等页面

排查问题时,如果错误码集中,可以判断具体错误类型; 如果服务IP集中,可以判断出问题的服务器信息。

03 优化

网络监控和标准建立完成后,为了降低了网络错误率、劫持率和请求耗时等问题,又引入一些优化方案。

3.1 DNS优化

在HTTP协议里,DNS作用是解析出正确的IP地址,网络请求首先进行DNS解析,然后根据IP进行后续流程,所以,DNS的优化很关键。DNS优化主要从以下几个点考虑:

DNS劫持:在移动请求里,劫持指的是运营商DNS服务器被攻击,域名解析时返回了错误的IP地址,导致请求失败。

DNS TTL过长:TTL时间过长,可能导致两个问题:

1. 正常下线了某些服务器,运营商可能长时间未更新缓存。
2. 后台服务故障,需要及时更新服务器列表。

跨运营商:跨运营商可能导致访问慢。

LocalDNS获取失败:在网络较差或者特殊情况下,获取失败或者时间过长。

3.1.1 三层缓存

建立内存+网络请求+本地持久化的三层缓存机制。缓存都设置有一定的时效,内存缓存过期,使用网络请求;网络不通时,会使用本地的持久化缓存。

好处在于:

1. 快速获取域名DNS解析信息。

2. 网络较差或者获取LocalDNS失败时,可以从本地获取结果,进行尝试。

3. 可记录上次请求时,成功的IP,会记录其成功率和性能数据,下次重建连接,优先使用上次成功率较高,性能较好的IP。

3.1.2 HTTPDNS

HTTPDNS是业内比较完善DNS优化方案,通过HTTPS请求,主动去自己的基础平台拉取域名对应的IP信息,好处也显而易见。

防劫持:由于接口存在于自己后台,不存在运营商劫持问题。

高时效:如果后台服务IP有更新,可以立刻更新上线,解决了运营商TTL问题。

结果最优:HTTPDNS可以根据当前后台服务状况,选择最优的IP排序返回。

上线后,观察到劫持率下降了80%。


在使用HTTPDNS时,设计了降级方案,如果HTTPDNS不可用,会降级为LocalDNS,在时效性方面,设计有超时机制,并且在网络环境改变时,会主动更新。

3.2 弱网优化

移动端上网络环境复杂,用户随时可能处于弱网环境下,为了保证用户体验,需要在弱网环境下进行特殊优化。

3.2.1 弱网模型

首先,需要进行弱网判断,甄别出用户当前所处的网络状况。

计算标准:

网络流畅分级模型,参考了多个维度数据,包括上行/下行网速、请求失败率、网络延迟等。针对不同维度的因素,设置不同的阈值,综合多个网络因素等级,最终确认整体网络等级。目前策略里,只要有一个因素落在VERY_POOR,POOR区间,就认为是弱网环境。

等级划分:


根据网速值,划分为六个等级,网络状态会投递至后端,判断时,在value<=1,表示弱网。

3.2.2 弱网优化

在建立好弱网模型后,我们又进行了一系列的优化,包括:

Brotli压缩:Brotli相对Gzip压缩效率提高17-25%,不过由于其更耗服务端资源,前后台约定,会在弱网下使用Brotli压缩。

减小并发:网络库中,存在网络请求线程池,弱网下,减小并发数。

建立优先级:重要域名使用优先级更高线程池处理。

减少响应数据:对返回数据大小进行缩减,比如,页面相关的,减少分页数,减少card数。

3.3 网关方案

网关方案——通过后台一个中转服务,分发请求,端上所有请求都复用现有的长连接,相当于间接实现了域名收敛。

相对正常HTTP请求,其优势有:

1. 多域名连接共享,实现0RTT多路复用。

2. 避免了DNS解析,防止DNS劫持。

3. protobuf编码私有协议,节省流量。

在一个接口上A/B实验的结果看,平均请求耗时从595ms降低至371ms,成功率也有所提升。

网络架构图:


3.4 超级管道兜底

这是爱奇艺自研的一种基于HTTP/HTTPS IP直连的低成本高通用性的业务代理服务。能够赋予业务异地多活功能。

前端:通过一个统一的域名进行请求,使用该服务的请求,可以将其请求信息,作为参数拼接在统一的请求里。

后端:通过中转服务,将不同的请求分发,并且,会进行异地容灾分发,减小单个地区服务器故障造成的影响。

在一次线上故障中,使用超级管道的接口错误率是3.95%,未使用超级管道的错误率高达28.96%。

3.5 合理重试

对于用户而言,需要最终的成功结果,所以,请求失败后,内部进行重试是十分有必要的,合理的重试,可以有效降低错误率。

我们使用的重试策略有:

原样重试:业务方可配置重试次数,防止网络波动引起的请求失败。

HTTPS降级重试:如HTTPS降级为HTTP重试,防止SSLException导致的请求失败。

HTTP2.0降级重试:网络状况较好时,HTTP2.0多路复用,带来了性能上的优势,但在网络不稳定时,HTTP1.1错误率低于HTTP2.0。

IP直连重试:直接进行IP直连,防止域名解析导致的错误。

超级管道重试:利用超级管道,进行兜底重试,进行异地容灾,防止单个机房故障导致服务中断。

3.6 其他

竞速连接:对于重要域名,使用TCP连接进行测试,选择最优的IP。

TLS1.3:TLS1.3相较之前版本,由2RTT降低为1RTT,降低了SSL握手失败的概率。

连接优化:预建连接,连接重建,备用/复用连接。

04 成果

错误率下降趋势显著

爱奇艺APP的网络错误率,随着版本的持续优化,Android端错误率从最初5.3%,目前稳定在0.48%; IOS端错误率从最初4.63%,目前稳定在0.35%。


05 结尾

网络监控系统上线后,已在爱奇艺APP各端,随刻、电影票、奇秀等独立APP落地。经过多个部门的合作,使爱奇艺APP的网络错误率有了较大幅度下降,并建立了网络性能监控体系,为爱奇艺APP网络服务质量保驾护航。

网络监控解决了APP整体网络请求质量的监控问题,对于单个业务网络请求质量监控的问题,又引入了全链路监控,网络诊断,日志聚合等功能,同时和用户反馈平台打通,方便排查单用户反馈的问题。后续会继续完善全链路监控,以及引入QUIC等其他方案,优化爱奇艺APP网络请求质量,提高问题排查效率。


批处理打开文件或文件夹
问题:
安装了robotframework+selenium+Python3.7自动化测试框架后,由于是自带路径,所以在电脑中很难去找ride.py文件所在路径,所以使用批处理命令打开最为方便。
处理:
1、桌面新建python.txt文件,如图:

2、找到ride.py文件所在路径。(所需打开的文件所在路径),如图:

3、双击打开python.txt文件;
方法一:
Start “” “C:\Users\Administrator\AppData\Local\Programs\Python\Python37\Scripts\ride.py”

方法二:
C:
cd \Users\Administrator\AppData\Local\Programs\Python\Python37\Scripts
Start ride.py

4、保存时,更改后缀为.bat,后如图:

5、双击执行即可。

SPU = Standard Product Unit (标准产品单位)SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。例如:iphone4就是一个SPU,与商家,与颜色、款式、套餐都无关。(可以理解成规格单位)

SKU=stock keeping unit(库存量单位)SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。例如:纺织品中一个SKU通常表示:规格、颜色、款式。(库存或销售单位)


什么是SPU?什么是SKU

SPU(Standard Product Unit) 标准产品单元,是一组可复用、易检索的标准化信息的集合。 该集合描述了一个产品的特性,通俗一点讲: 属性值、特性相同的商品就是一组SPU

(╯‵□′)╯︵┻━┻

SKU(Stock Keeping Unit) 库存保有单位,是对每一个产品的唯一标识符;比如说:比如说同一件产品:衣服, 衣服的颜色、尺码、款式就是一组SKU, 颜色、尺码也是一组SKU, 颜色、款式也是一组SKU.


他们两个之间的关系呢就是. 我们所说的商品是一个泛化的东西,比如说shoppingmode 苹果手机,他是有很多种的, shoppingmode iphone4, 5, 6等等, 我们可以将Iphone 看做是手机中的一个商品, 然后 shoppingmode iphone 5s, 确定一个产品,即SPU, 然后在从产品具体细化到, 颜色、尺码等具体的属性就是 一个SKU了,

即: SPU + 具体属性1 + 具体属性2 + 具体属性n = SKU,


一般来说,咱们库存,都是要按照具体属性来的(即,SKU为最小库存单位) 不然店里面缺货了,都不知道该补哪些哪些? 我要一个shoppingmode 苹果手机???5s ???? 嗯?那店长就会说,请问是什么颜色的呢? 请问是xxxxx

闭嘴,我用SKU存库存了…


自从我开始探索 Python 中惊人的语言功能已经有一段时间了。一开始,我给自己一个挑战,目的是让我练习更多的 Python 语言功能,而不是使用其他编程语言的编程经验。这让事情变得越来越有趣!代码变得越来越简洁,代码看起来更加结构化和规范化。下面我将会介绍这些好处。

通常如下使用场景中会用到 for 循环:

  • 在一个序列来提取一些信息。
  • 从一个序列生成另一个序列。
  • 写 for 已成习惯。

幸运的是,Python 已经有很多工具可以帮助你完成这些工作,你只需要转移你的思路,并以不同的角度来思考它。

通过避免编写 for 循环,你可以获得什么好处:

  • 较少的代码量
  • 更好的代码可读性
  • 更少的缩进(对 Python 还是很有意义的)

我们来看一下下面的代码结构:

# 1
with ...:
    for ...:
        if ...:
            try:
            except:
        else:

在这个例子中,我们正在处理多层嵌套的代码,这很难阅读。这个例子使用了多层嵌套的代码。我在这段代码中发现它无差别使用缩进把管理逻辑(with, try-except)和业务逻辑(for, if)混在一起。如果你遵守只对管理逻辑使用缩进的规范,那么核心业务逻辑应该立刻脱离出来。

"扁平结构比嵌套结构更好" - The Zen of Python

可以使用的已有的工具来替换 for 循环

1. List Comprehension / Generator 表达式

我们来看一个简单的例子。如果你想将一个数组转换为另一个数组:

result = []
for item in item_list:
    new_item = do_something_with(item)
    result.append(item)

如果你喜欢 MapReduce,你也可以使用 map,或者 Python 中的 List Comprehension:

result = [do_something_with(item) for item in item_list]

同样,如果您只想迭代数组中的元素,也可以使用一样的代码 Generator Expression。

result = (do_something_with(item) for item in item_list)

2. 函数

如果您想要将一个数组映射成另外数组,只需调用 map 函数,就可以用一个更高级、更实用的编程方式解决这个问题。

doubled_list = map(lambda x: x * 2, old_list)

如果要将序列减少为单个,请使用 reduce

from functools import reduce
summation = reduce(lambda x, y: x + y, numbers)

另外,许多 Python 内置函数都会使用 iterables:

>>> a = list(range(10))
>>> a
[0123456789]
>>> all(a)
False
>>> any(a)
True
>>> max(a)
9
>>> min(a)
0
>>> list(filter(bool, a))
[123456789]
>>> set(a)
{0123456789}
>>> dict(zip(a,a))
{00112233445566778899}
>>> sorted(a, reverse=True)
[9876543210]
>>> str(a)
'[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'
>>> sum(a)
45

3. Extract Functions or Generators

上述两种方法是很好的处理更简单的逻辑。更复杂的逻辑怎么样?作为程序员,我们编写函数来抽离出复杂的业务。相同的想法适用于此。如果你是这样写的:

results = []
for item in item_list:
    # setups
    # condition
    # processing
    # calculation
    results.append(result)

显然你对一个代码块添加了太多的责任。相反,我建议你做:

def process_item(item):
    # setups
    # condition
    # processing
    # calculation
    return result

results = [process_item(item) for item in item_list]

如果换成嵌套函数会如何

results = []
for i in range(10):
    for j in range(i):
        results.append((i, j))

换成 List Comprehension 来实现是这样的:

results = [(i, j)
           for i in range(10)
           for j in range(i)]

如果你的代码块需要记录一些内部状态

# finding the max prior to the current item
a = [3462190758]
results = []
current_max = 0
for i in a:
    current_max = max(i, current_max)
    results.append(current_max)

# results = [3, 4, 6, 6, 6, 9, 9, 9, 9, 9]

我们使用 generator 来实现这一点:

def max_generator(numbers):
    current_max = 0
    for i in numbers:
        current_max = max(i, current_max)
        yield current_max

a = [3462190758]
results = list(max_generator(a))

读者可能要问 “等等!你在 generator 中用到 for 循环,作弊啊!别急,再看看下面的代码。

不要自己写。itertools 会帮你实现了

这个模块很简单。我相信这个模块在大多数场景中可以替换你原先的 for 循环。例如,最后一个例子可以重写为:

from itertools import accumulate
a = [3462190758]
resutls = list(accumulate(a, max))

另外,如果要迭代组合序列,则需要使用product(), permutations(), combinations()。

结论

  • 在大多数情况下,您都不需要编写 for 循环。
  • 你应该避免编写 for 循环,这样会有更好的代码可读性。