[系统性能优化实践]JVM进阶实战之监控工具(Prometheus)
Posted 千千寰宇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[系统性能优化实践]JVM进阶实战之监控工具(Prometheus)相关的知识,希望对你有一定的参考价值。
0 Prometheus概述
0.1 简介
Prometheus
是一个开源的系统监控和报警系统;- 现在已加入到CNCF基金会,成为继k8s之后第二个在CNCF托管的项目
- 在kubernetes容器管理系统中,通常会搭配prometheus进行监控,同时也支持多种exporter采集数据,还支持pushgateway进行数据上报,Prometheus性能足够支撑上万台规模的集群。
0.2 特点
- 1)多维度数据模型
每一个时间序列数据都由metric度量指标名称和它的标签labels键值对集合唯一确定:这个metric度量指标名称指定监控目标系统的测量特征(如:http_requests_total- 接收http请求的总计数)。labels开启了Prometheus的多维数据模型:对于相同的度量名称,通过不同标签列表的结合, 会形成特定的度量维度实例。(例如:所有包含度量名称为/api/tracks的http请求,打上method=POST的标签,则形成了具体的http请求)。这个查询语言在这些度量和标签列表的基础上进行过滤和聚合。改变任何度量上的任何标签值,则会形成新的时间序列图。
-
2)灵活的查询语言(PromQL):可以对采集的metrics指标进行加法,乘法,连接等操作;
-
3)可以直接在本地部署,不依赖其他分布式存储;
-
4)通过基于HTTP的pull方式采集时序数据;
-
5)可以通过中间网关pushgateway的方式把时间序列数据推送到prometheus server端;
-
6)可通过服务发现或者静态配置来发现目标服务对象(targets)。
-
7)有多种可视化图像界面,如Grafana等。
-
8)高效的存储,每个采样数据占3.5 bytes左右,300万的时间序列,30s间隔,保留60天,消耗磁盘大概200G。
-
9)做高可用,可以对数据做异地备份,联邦集群,部署多套prometheus,pushgateway上报数据
0.3 样本
样本
:在时间序列中的每一个点称为一个样本
(sample
)
样本
由以下3部分组成:
指标
(metric):指标名称和描述当前样本特征的 labelsets;时间戳
(timestamp):一个精确到毫秒的时间戳;样本值
(value): 一个 folat64 的浮点型数据表示当前样本的值。表示方式
:通过如下表达方式表示指定指标名称和指定标签集合的时间序列:
例如,指标名称为 api_http_requests_total,标签为 method="POST" 和 handler="/messages" 的时间序列可以表示为:api_http_requests_total
0.4 架构与组件
从上图可发现,Prometheus整个生态圈组成主要包括prometheus server,Exporter,pushgateway,alertmanager,grafana,Web ui界面,Prometheus server由三个部分组成,Retrieval,Storage,PromQL
Retrieval
负责在活跃的target主机上抓取监控指标数据Storage
存储主要是把采集到的数据存储到磁盘中PromQL
是Prometheus
提供的查询语言模块。
-
Prometheus Server
: 用于收集和存储时间序列数据。 -
Client Library
: 客户端库,检测应用程序代码,当Prometheus抓取实例的HTTP端点时,客户端库会将所有跟踪的metrics指标的当前状态发送到prometheus server端。 -
Exporters
: prometheus支持多种exporter,通过exporter可以采集metrics数据,然后发送到prometheus server端,所有向promtheus server提供监控数据的程序都可以被称为exporter -
Alertmanager
: 从 Prometheus server 端接收到 alerts 后,会进行去重,分组,并路由到相应的接收方,发出报警,常见的接收方式有:电子邮件,微信,钉钉, slack等。 -
Grafana
:监控仪表盘,可视化监控数据 -
pushgateway
: 各个目标主机可上报数据到pushgateway,然后prometheus server统一从pushgateway拉取数据。
0.5 工作流程
-
1)Prometheus server可定期从活跃的(up)目标主机上(target)拉取监控指标数据,目标主机的监控数据可通过配置静态job或者服务发现的方式被prometheus server采集到,这种方式默认的pull方式拉取指标;也可通过pushgateway把采集的数据上报到prometheus server中;还可通过一些组件自带的exporter采集相应组件的数据;
-
2)Prometheus server把采集到的监控指标数据保存到本地磁盘或者数据库;
-
3)Prometheus采集的监控指标数据按时间序列存储,通过配置报警规则,把触发的报警发送到alertmanager
-
4)Alertmanager通过配置报警接收方,发送报警到邮件,微信或者钉钉等
-
5)Prometheus 自带的web ui界面提供PromQL查询语言,可查询监控数据
-
6)Grafana可接入prometheus数据源,把监控数据以图形化形式展示出
0.6 竞品分析:Prometheus vs zabbix
0.7 Prometheus的部署模式
0.7.1 基本高可用模式
基本的HA模式只能确保Promthues服务的可用性问题,但是不解决Prometheus Server之间的数据一致性问题以及持久化问题(数据丢失后无法恢复),也无法进行动态的扩展。
因此,这种部署方式适合监控规模不大,Promthues Server
也不会频繁发生迁移的情况,并且只需要保存短周期监控数据的场景。
0.7.2 基本高可用+远程存储
在解决了Promthues服务可用性的基础上,同时确保了数据的持久化,当Promthues Server发生宕机或者数据丢失的情况下,可以快速的恢复。
同时Promthues Server可能很好的进行迁移。因此,该方案适用于用户监控规模不大,但是希望能够将监控数据持久化,同时能够确保Promthues Server的可迁移性的场景。
0.7.3 基本HA + 远程存储 + 联邦集群方案
Promthues的性能瓶颈主要在于大量的采集任务,因此用户需要利用Prometheus联邦集群的特性,将不同类型的采集任务划分到不同的Promthues子服务中,从而实现功能分区。
例如一个Promthues Server负责采集基础设施相关的监控指标,另外一个Prometheus Server负责采集应用监控指标。再有上层Prometheus Server实现对数据的汇聚。
0.8 Prometheus的数据类型(x4)
0.8.1 Counter(计数器类型)
- Counter 用于累计值,例如记录请求次数、任务完成数、错误发生次数。
- 一直增加,不会减少。
- 重启进程后,会被重置。
# Counter类型示例
http_response_totalmethod="GET",endpoint="/api/tracks" 100
http_response_totalmethod="GET",endpoint="/api/tracks" 160
Counter 类型数据可以让用户方便的了解事件产生的速率的变化,在PromQL内置的相关操作函数可以提供相应的分析,比如以HTTP应用请求量来进行说明
- 1)通过rate()函数获取HTTP请求量的增长率:rate(http_requests_total[5m])
- 2)查询当前系统中,访问量前10的HTTP地址:topk(10, http_requests_total)
0.8.2 Gauge(测量器类型)
- Gauge是常规数值,例如温度变化、内存使用变化。
- 可变大,可变小。
- 重启进程后,会被重置
# Gauge类型示例
memory_usage_byteshost="master-01" 100
memory_usage_byteshost="master-01" 30
memory_usage_byteshost="master-01" 50
memory_usage_byteshost="master-01" 80
对于 Gauge 类型的监控指标,通过 PromQL 内置函数 delta() 可以获取样本在一段时间内的变化情况,例如,计算 CPU 温度在两小时内的差异:
dalta(cpu_temp_celsiushost="zeus"[2h])
你还可以通过PromQL 内置函数 predict_linear() 基于简单线性回归的方式,对样本数据的变化趋势做出预测。例如,基于 2 小时的样本数据,来预测主机可用磁盘空间在 4 个小时之后的剩余情况:predict_linear(node_filesystem_freejob="node"[2h], 4 * 3600) < 0
0.8.3 Histogram(柱状图)
histogram是柱状图,在Prometheus系统的查询语言中,有三种作用:
- 1)在一段时间范围内对数据进行采样(通常是请求持续时间或响应大小等),并将其计入可配置的存储桶(bucket)中. 后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。
- 2)对每个采样点值累计和(sum)
- 3)对采样点的次数累计和(count)
度量指标名称: [basename]上面三类的作用度量指标名称
1)[basename]bucketle="上边界", 这个值为小于等于上边界的所有采样点数量
2)[basename]_sum_
3)[basename]_count
小结:如果定义一个度量类型为Histogram,则Prometheus会自动生成三个对应的指标
为什需要用histogram柱状图?
在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 WEB 页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。
为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0~10ms 之间的请求数有多少,而 10~20ms 之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。Histogram 和 Summary 都是为了能够解决这样问题的存在,通过 Histogram 和 Summary 类型的监控指标,我们可以快速了解监控样本的分布情况。
Histogram 类型的样本会提供三种指标(假设指标名称为 ):
1)样本的值分布在 bucket 中的数量,命名为 _bucketle="<上边界>"。解释的更通俗易懂一点,这个值表示指标值小于等于上边界的所有样本数量。
# 1、在总共2次请求当中。http 请求响应时间 <=0.005 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.005", 0.0
# 2、在总共2次请求当中。http 请求响应时间 <=0.01 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.01", 0.0
# 3、在总共2次请求当中。http 请求响应时间 <=0.025 秒 的请求次数为0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.025", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.05", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.075", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.1", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.25", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.5", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="0.75", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="1.0", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="2.5", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="5.0", 0.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="7.5", 2.0
# 4、在总共2次请求当中。http 请求响应时间 <=10 秒 的请求次数为 2
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="10.0", 2.0
io_namespace_http_requests_latency_seconds_histogram_bucketpath="/",method="GET",code="200",le="+Inf", 2.0
2)所有样本值的大小总和,命名为 _sum
# 实际含义: 发生的2次 http 请求总的响应时间为 13.107670803000001 秒
io_namespace_http_requests_latency_seconds_histogram_sumpath="/",method="GET",code="200", 13.107670803000001
3)样本总数,命名为 _count,值和 _bucketle="+Inf" 相同
# 实际含义: 当前一共发生了 2 次 http 请求
io_namespace_http_requests_latency_seconds_histogram_countpath="/",method="GET",code="200", 2.0
注意:
1)bucket 可以理解为是对数据指标值域的一个划分,划分的依据应该基于数据值的分布。注意后面的采样点是包含前面的采样点的,假设 xxx_bucket...,le="0.01" 的值为 10,而 xxx_bucket...,le="0.05" 的值为 30,那么意味着这 30 个采样点中,有 10 个是小于 0.01s的,其余 20 个采样点的响应时间是介于0.01s 和 0.05s之间的。
2)可以通过 histogram_quantile() 函数来计算 Histogram 类型样本的分位数。分位数可能不太好理解,你可以理解为分割数据的点。我举个例子,假设样本的 9 分位数(quantile=0.9)的值为 x,即表示小于 x 的采样值的数量占总体采样值的 90%。Histogram 还可以用来计算应用性能指标值(Apdex score)。
0.8.4 Summary(汇总)
与 Histogram 类型类似,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间来计算。它也有三种作用:
1)对于每个采样点进行统计,并形成分位图。(如:正态分布一样,统计低于60分不及格的同学比例,统计低于80分的同学比例,统计低于95分的同学比例)
2)统计班上所有同学的总成绩(sum)
3)统计班上同学的考试总人数(count)
带有度量指标的[basename]的summary 在抓取时间序列数据有如命名。
1、观察时间的φ-quantiles (0 ≤ φ ≤ 1), 显示为[basename]分位数="[φ]"
2、[basename]sum, 是指所有观察值的总和
3、[basename]_count, 是指已观察到的事件计数值
样本值的分位数分布情况,命名为 quantile="<φ>"。
# 1、含义:这 12 次 http 请求中有 50% 的请求响应时间是 3.052404983s
io_namespace_http_requests_latency_seconds_summarypath="/",method="GET",code="200",quantile="0.5", 3.052404983
# 2、含义:这 12 次 http 请求中有 90% 的请求响应时间是 8.003261666s
io_namespace_http_requests_latency_seconds_summarypath="/",method="GET",code="200",quantile="0.9", 8.003261666
所有样本值的大小总和,命名为 _sum。
# 1、含义:这12次 http 请求的总响应时间为 51.029495508s
io_namespace_http_requests_latency_seconds_summary_sumpath="/",method="GET",code="200", 51.029495508
样本总数,命名为 _count。
# 1、含义:当前一共发生了 12 次 http 请求
io_namespace_http_requests_latency_seconds_summary_countpath="/",method="GET",code="200", 12.0
Histogram 与 Summary 的异同:
它们都包含了 _sum 和 _count 指标,Histogram 需要通过 _bucket 来计算分位数,而 Summary 则直接存储了分位数的值。
prometheus_tsdb_wal_fsync_duration_secondsquantile="0.5" 0.012352463
prometheus_tsdb_wal_fsync_duration_secondsquantile="0.9" 0.014458005
prometheus_tsdb_wal_fsync_duration_secondsquantile="0.99" 0.017316173
prometheus_tsdb_wal_fsync_duration_seconds_sum 2.888716127000002
prometheus_tsdb_wal_fsync_duration_seconds_count 216
# 从上面的样本中可以得知当前Promtheus Server进行wal_fsync操作的总次数为216次,耗时2.888716127000002s。其中中位数(quantile=0.5)的耗时为0.012352463,9分位数(quantile=0.9)的耗时为0.014458005s。
0.9 监控内容:Prometheus能监控什么?
# Databases---数据库
Aerospike exporter
ClickHouse exporter
Consul exporter (official)
Couchbase exporter
CouchDB exporter
ElasticSearch exporter
EventStore exporter
Memcached exporter (official)
MongoDB exporter
MSSQL server exporter
MySQL server exporter (official)
OpenTSDB Exporter
Oracle DB Exporter
PgBouncer exporter
PostgreSQL exporter
ProxySQL exporter
RavenDB exporter
Redis exporter
RethinkDB exporter
SQL exporter
Tarantool metric library
Twemproxy
# Hardware related---硬件相关
apcupsd exporter
Collins exporter
IBM Z HMC exporter
IoT Edison exporter
IPMI exporter
knxd exporter
Netgear Cable Modem Exporter
Node/system metrics exporter (official)
NVIDIA GPU exporter
ProSAFE exporter
Ubiquiti UniFi exporter
# Messaging systems---消息服务
Beanstalkd exporter
Gearman exporter
Kafka exporter
NATS exporter
NSQ exporter
Mirth Connect exporter
MQTT blackbox exporter
RabbitMQ exporter
RabbitMQ Management Plugin exporter
# Storage---存储
Ceph exporter
Ceph RADOSGW exporter
Gluster exporter
Hadoop HDFS FSImage exporter
Lustre exporter
ScaleIO exporter
# HTTP---网站服务
Apache exporter
HAProxy exporter (official)
Nginx metric library
Nginx VTS exporter
Passenger exporter
Squid exporter
Tinyproxy exporter
Varnish exporter
WebDriver exporter
# APIs
AWS ECS exporter
AWS Health exporter
AWS SQS exporter
Cloudflare exporter
DigitalOcean exporter
Docker Cloud exporter
Docker Hub exporter
GitHub exporter
InstaClustr exporter
Mozilla Observatory exporter
OpenWeatherMap exporter
Pagespeed exporter
Rancher exporter
Speedtest exporter
# Logging---日志
Fluentd exporter
Google\'s mtail log data extractor
Grok exporter
# Other monitoring systems
Akamai Cloudmonitor exporter
Alibaba Cloudmonitor exporter
AWS CloudWatch exporter (official)
Cloud Foundry Firehose exporter
Collectd exporter (official)
Google Stackdriver exporter
Graphite exporter (official)
Heka dashboard exporter
Heka exporter
InfluxDB exporter (official)
JavaMelody exporter
JMX exporter (official)
Munin exporter
Nagios / Naemon exporter
New Relic exporter
NRPE exporter
Osquery exporter
OTC CloudEye exporter
Pingdom exporter
scollector exporter
Sensu exporter
SNMP exporter (official)
StatsD exporter (official)
# Miscellaneous---其他
ACT Fibernet Exporter
Bamboo exporter
BIG-IP exporter
BIND exporter
Bitbucket exporter
Blackbox exporter (official)
BOSH exporter
cAdvisor
Cachet exporter
ccache exporter
Confluence exporter
Dovecot exporter
eBPF exporter
Ethereum Client exporter
Jenkins exporter
JIRA exporter
Kannel exporter
Kemp LoadBalancer exporter
Kibana Exporter
Meteor JS web framework exporter
Minecraft exporter module
PHP-FPM exporter
PowerDNS exporter
Presto exporter
Process exporter
rTorrent exporter
SABnzbd exporter
Script exporter
Shield exporter
SMTP/Maildir MDA blackbox prober
SoftEther exporter
Transmission exporter
Unbound exporter
Xen exporter
# Software exposing Prometheus metrics---Prometheus度量指标
App Connect Enterprise
Ballerina
Ceph
Collectd
Concourse
CRG Roller Derby Scoreboard (direct)
Docker Daemon
Doorman (direct)
Etcd (direct)
Flink
FreeBSD Kernel
Grafana
JavaMelody
Kubernetes (direct)
Linkerd
0.9 Prometheus对kubernetes的监控
对于Kubernetes而言,我们可以把当中所有的资源分为几类:
- 基础设施层(Node):集群节点,为整个集群和应用提供运行时资源
- 容器基础设施(Container):为应用提供运行时环境
- 用户应用(Pod):Pod中会包含一组容器,它们一起工作,并且对外提供一个(或者一组)功能
- 内部服务负载均衡(Service):在集群内,通过Service在集群暴露应用功能,集群内应用和应用之间访问时提供内部的负载均衡
- 外部访问入口(Ingress):通过Ingress提供集群外的访问入口,从而可以使外部客户端能够访问到部署在Kubernetes集群内的服务
因此,如果要构建一个完整的监控体系,我们应该考虑,以下5个方面:
- 集群节点状态监控:从集群中各节点的kubelet服务获取节点的基本运行状态;
- 集群节点资源用量监控:通过Daemonset的形式在集群中各个节点部署Node Exporter采集节点的资源使用情况;
- 节点中运行的容器监控:通过各个节点中kubelet内置的cAdvisor中获取个节点中所有容器的运行状态和资源使用情况;
- 如果在集群中部署的应用程序本身内置了对Prometheus的监控支持,那么我们还应该找到相应的Pod实例,并从该Pod实例中获取其内部运行状态的监控指标。
- 对k8s本身的组件做监控:apiserver、scheduler、controller-manager、kubelet、kube-proxy
1 Prometheus 监控 Spring Cloud Gateway
1.1 简述
- API网关作为应用服务与外部交互的入口,通过对API网关的监控,可以清晰的知道应用整体的请求量,以便根据不同的并发情况进行扩容处理。
对API网关的监控也是相当必要的。
- 通过Prometheus监控Gateway与监控普通Springboot项目几乎没有区别。基本步骤都是引入pom依赖,然后修改端点暴露metrics接口即可。
1.2 配置步骤
1.2.1 Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>$springboot.version</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
<!-- <version>1.9.6</version> 1.9.6 / 1.5.14 -->
</dependency>
- 需要注意的是
micrometer-registry-prometheus
的版本号需要跟spring-boot-dependencies
中定义的保持一致。Springboot
较高版本的定义统一在micrometer-bom
中,低版本的直接在spring-boot-dependencies
中定义。
1.2.2 配置文件 application.yaml [local or nacos]
--- # 暴露监控端点 配置
management:
endpoints:
# web端点配置属性
web:
# 默认端点前缀为/actuator,可修改
base-path: /actuator
exposure:
# 包含端点,全用直接使用\'*\'即可,多个场景[\'prometheus\',\'health\']
include: [ \'prometheus\',\'health\' ]
# 排除端点
exclude: [ \'shutdown\' ]
# JMX 端点配置属性
jmx:
exposure:
include: [ \'prometheus\' ]
exclude: [ \'shutdown\' ]
metrics:
tags:
application: $spring.application.name
export:
prometheus:
descriptions: true
enabled: true
按照实际使用情况,开放对应监控端点即可,为了保护应用安全,不使用的不开启
spring-cloud-gateway (embed : netty server)
spring-boot (embed : tomcat server)
1.2.3 Prometheus 相关配置
prometheus.yml
配置
# consul服务发现配置
- job_name: \'api_gatway\'
consul_sd_configs:
- server: \'10.0.107.55:8500\' #consul的服务地址
services: ["api_gateway"]
relabel_configs:
- source_labels: ["__meta_consul_tags"]
regex: .*api_gateway.*
action: keep
- regex: __meta_consul_service_metadata_(.+)
action: labelmap
# 指标标签兼容,spring cloud gateway 3.x版本前缀加了spring_cloud_
metric_relabel_configs:
- source_labels: [__name__]
regex: \'gateway(.*)\'
target_label: \'__name__\'
replacement: \'spring_cloud_gateway$1\'
# file_sd服务发现配置
- job_name: \'api_gateway\'
file_sd_configs:
- files:
- \'./api_gateway_config/*.json\'
refresh_interval: 15s
# 指标标签兼容,spring cloud gateway 3.x版本前缀加了spring_cloud_
metric_relabel_configs:
- source_labels: [__name__]
regex: \'gateway(.*)\'
target_label: \'__name__\'
replacement: \'spring_cloud_gateway$1\'
spring cloud gateway
在不同的版本中指标名称不一致,在3.X版本中指标名称加了前缀spring_cloud_
所以,在
prometheus
配置文件中使用metric_relabel_configs
对指标进行统一处理
1.2.4 调用 Actuator API
- curl -X GET http://127.0.0.1/actuator/prometheus
1.2.5 Grafana面板
官方面板:
Grafana中的面板:
grafana
官方提供的仅支持2.x
的gateway
,对于3.x的gateway存在问题。因此,我们在使用面板的时候同时兼容了2.x和3.x版本,需要根据gateway官方的面板进行自定义。
自定义面板:
1.2.6 指标选取
指标 | PromQL |
---|---|
运行状态 | up |
近5分钟QPS | sum by(instance) (rate(spring_cloud_gateway_requests_seconds_counturi!~“.actuator.”[5m])) |
近5分钟请求失败次数 | sum by(instance) (increase(spring_cloud_gateway_requests_seconds_countoutcome!=“SUCCESSFUL”[5m])) |
X 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
Android 进阶——性能优化之电量优化全攻略及实战小结
文章大纲
引言
电池续航时间是移动用户体验中最重要的一个方面。没电的设备完全无法使用。因此,对于应用来说,尽可能地考虑电池续航时间是至关重要的。为使应用保持节能,前一篇Android 进阶——性能优化之电量优化全攻略及实战小结(一)对于一些概念进行了总结。
一、在低电耗模式和应用待机模式下进行测试
为确保用户获得良好的体验,您应在低电耗模式和应用待机模式下全面测试您的应用。
1、在低电耗模式下测试您的应用
您可以按以下步骤在低电耗模式下测试您的应用:
-
使用 Android 6.0(API 级别 23)或更高版本的系统映像配置硬件设备或虚拟设备。
-
将设备连接到开发计算机并安装您的应用。
-
运行您的应用并使其保持活动状态。
-
运行以下命令,强制系统进入闲置模式:
$ adb shell dumpsys deviceidle force-idle
-
准备就绪后,运行以下命令,使系统退出闲置模式:
$ adb shell dumpsys deviceidle unforce
-
执行以下命令,重新激活设备:
$ adb shell dumpsys battery reset
-
在重新激活设备后观察应用的行为。确保应用在设备退出低电耗模式时正常恢复。
2、在应用待机模式下测试您的应用
如需在应用待机模式下测试您的应用,请执行以下操作:
-
使用 Android 6.0(API 级别 23)或更高版本的系统映像配置硬件设备或虚拟设备。
-
将设备连接到开发计算机并安装您的应用。
-
运行您的应用并使其保持活动状态。
-
运行以下命令,强制应用进入应用待机模式:
$ adb shell dumpsys battery unplug $ adb shell am set-inactive <packageName> true
-
使用以下命令模拟唤醒您的应用:
$ adb shell am set-inactive <packageName> false $ adb shell am get-inactive <packageName>
-
在唤醒应用后观察它的行为。确保应用从待机模式正常恢复。您应特别检查应用的通知和后台作业是否继续按预期运行。
3、列入白名单的可接受用例
下表重点介绍了请求将应用列入电池优化豁免白名单或应用目前在该白名单中的可接受用例。一般来说,除非低电耗模式或应用待机模式破坏了应用的核心功能,或者由于技术方面的原因而导致您的应用无法使用高优先级 FCM 消息,否则您的应用不应在白名单中。
4、确定当前充电状态
首先,确定当前充电状态。BatteryManager
会在一个包含充电状态的粘性 Intent
中广播所有电池和充电详情。
由于它是一个粘性 Intent,因此您并不需要如下一代码段中所示的那样通过简单地调用 registerReceiver
传入 null
作为接收器来注册 BroadcastReceiver
,便可返回当前电池状态 Intent。您可以在此处传入实际 BroadcastReceiver
对象,但由于稍后我们将会处理更新,因此并不需要这样做。
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter);
您可以提取当前充电状态,并且如果设备正在充电,则还可以提取设备是通过 USB 还是交流充电器进行充电。
// Are we charging / charged?
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL;
// How are we charging?
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
通常,如果设备连接到交流充电器,您应最大限度地提高后台更新的频率;如果设备是通过 USB 充电,则应降低更新频率;如果电池正在放电,则应进一步降低更新频率。
5、监控充电状态变化
就像设备可以轻松地插入电源,充电状态也很容易发生变化,因此必须监控充电状态的变化并相应地改变刷新频率。
每当设备连接或断开电源时,BatteryManager
都会广播一项操作。请务必接收这些事件,即便您的应用并未运行 - 尤其要考虑到这些事件可能会影响您启动应用以便发起后台更新的频率 - 因此您应在清单中注册一个 BroadcastReceiver
,通过在一个 Intent 过滤器内定义 ACTION_POWER_CONNECTED
和 ACTION_POWER_DISCONNECTED
来同时监听这两种事件。
<receiver android:name=".PowerConnectionReceiver">
<intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
</intent-filter>
</receiver>
6、确定当前电池电量
在某些情况下,确定当前电池电量也很有用处。您可以选择在电池电量低于某一水平时降低后台更新的频率。
您可以通过从电池状态 intent 提取当前电池电量和刻度来了解当前电池电量,如下所示:
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
float batteryPct = level * 100 / (float)scale;
7、监控显著的电池电量变化
您无法轻松地持续监控电池状态,您也不必如此。
一般而言,持续监控电池电量对电池的影响大于应用正常行为造成的影响,因此最好只监控显著的电池电量变化 - 特别是在设备进入或退出电量不足状态时。
以下清单代码段摘自某个广播接收器内的 Intent 过滤器元素。通过监听 ACTION_BATTERY_LOW
和 ACTION_BATTERY_OKAY
,每当设备电池电量不足或退出不足状态时,便会触发该接收器。
<receiver android:name=".BatteryLevelReceiver">
<intent-filter>
<action android:name="android.intent.action.BATTERY_LOW"/>
<action android:name="android.intent.action.BATTERY_OKAY"/>
</intent-filter>
</receiver>
一般而言,建议您在电量极低时停用所有后台更新。如果手机自行关机,您就无法利用相关数据,数据的新鲜度也就无关紧要。在许多情况下,为设备充电与将设备插入基座是同一操作。下一课将为您介绍如何确定当前基座状态以及如何监控设备插接状态的变化。
二、Wakelock 机制
Android为了确保应用程序中关键代码的正确执行,提供了Wake Lock(一种锁的机制, 只要有人拿着这个锁,系统就无法进入休眠)可以被用户态程序和内核获得。使得应用程序有权限通过代码阻止AP进入休眠状态。WakeLock是Android中为应用层及框架层提供的用来保证CPU处于唤醒状态的一种锁机制。PMS中为应用及框架层其他组件提供了接口,进行WakeLock的申请和释放。应用在申请WakeLock时,需要在清单文件中配置android.Manifest.permission.WAKE_LOCK
权限。如果没有锁了或者超时了, 内核就会启动休眠的那套机制来进入休眠。WakeLock阻止应用处理器(ApplicationProcessor)挂起,确保关键代码的运行,通过中断唤起应用处理器(ApplicationProcessor),可以阻止屏幕变暗。所有的WakeLock被释放后,系统会挂起。
1、WakeLock分类
根据作用时间,WakeLock可以分为永久锁和超时锁:
- 永久锁:只要获取了WakeLock锁,必须显式进行释放,否则系统会一直持有该锁;
- 超时锁:在到达给定时间后,自动释放WakeLock锁,其实现原理为方法内部维护了一个Handler进行。
根据释放原则,WakeLock可以分为计数锁和非计数锁:
- 计数锁:一次申请必须对应一次释放;
- 非计数锁:不管申请多少次,只需要一次就可以释放该WakeLock。
默认为计数锁。WakeLock机制从上到下架构如下:
WakeLock有三种表现形式:
- PowerManger.WakeLock:PMS暴露给应用层和其他组件用来申请WakeLock的接口;
- PowerManagerService.WakeLock: PowerManager.WakeLock在PMS中的表现形式;
- SuspendBlocker: PowerManagerService.WakeLock在向底层节点操作时的表现形式。
2、申请WakeLock锁
// 获取PowerManager对象
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// 创建WakeLock锁实例
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
// 申请WakeLock
wl.acquire();
// 释放WakeLock
wl.release();
在PowerManager中,共定义了以下七种WakeLock:
WakeLock | 说明 |
---|---|
PARTIAL_WAKE_LOCK | 0x00000001,保证CPU处于唤醒状态,但屏幕和键盘灯有可能是关闭的。 |
PROXIMITY_SCREEN_OFF_WAKE_LOCK | 0x00000020,通过PSensor进行亮灭屏工作,PSensor检测到有物体靠近时关闭屏幕,远离时又亮屏 |
DOZE_WAKE_LOCK | 0x00000040,仅用于PMS唤醒状态为Doze时,进入Doze状态后,DreamMangerService会申请该锁,允许CPU挂起 |
DRAW_WAKE_LOCK | 0x00000080,仅用于PMS唤醒状态为Doze时,保证CPU处于运行状态,以进行Doze状态下屏幕的绘制,如AOD、防烧屏显示 |
0x0000001a,保证屏幕、键盘灯都保持常亮状态,按Power键灭屏后,会忽略该锁 | |
0x0000000a,保证屏幕一直保持常亮状态,按Power键灭屏后,会忽略该锁(不推荐) | |
0x00000006,保持CPU运转,保证屏幕一直保持Dim状态但有可能是灰的,允许关闭键盘灯,按Power键灭屏后,会忽略该锁(不推荐) |
还定义了三个Flag,可以在创建、申请及释放WakeLock时和以上几类搭配使用:
Flag | 说明 |
---|---|
ACQUIRE_CAUSES_WAKEUP | 不会唤醒设备,强制屏幕马上高亮显示,键盘灯开启。有一个例外,如果有notification弹出的话,会唤醒设备。,不能和PARTIAL_WAKE_LOCK一起使用 |
ON_AFTER_RELEASE | 在释放有该Flag的WakeLock时,会稍微延长自动休眠时间一小会儿,但不能和PARTIAL_WAKE_LOCK一起使用,WakeLock被释放后,维持屏幕亮度一小段时间,减少WakeLock循环时的闪烁情况。 |
PROXIMITY_SCREEN_OFF_WAKE_LOCK | 在释放PROXIMITY_SCREEN_OFF_WAKE_LOCK锁时,不会立即解除PSensor监听,而是在PSensor上报远离后,才会亮屏并解除Psensor监听,仅用于释放PROXIMITY_SCREEN_OFF_WAKE_LOCK锁 |
3、获取WakeLock对象
PowerManager中提供了接口newWakeLock()
来创建WakeLock对象:
// frameworks/base/core/java/android/os/PowerManager.java
public WakeLock newWakeLock(int levelAndFlags, String tag)
validateWakeLockParameters(levelAndFlags, tag); // 校验Flag
// 创建WakeLock对象
return new WakeLock(levelAndFlags, tag, mContext.getOpPackageName());
首先进行了参数的校验,然后调用WakeLock构造方法创建WakeLock对象:
// frameworks/base/core/java/android/os/PowerManager.java
WakeLock(int flags, String tag, String packageName)
mFlags = flags; //表示wakelock类型
mTag = tag; //一个tag,一般为当前类名
mPackageName = packageName; //申请wakelock的应用包名
mToken = new Binder(); //一个Binder标记
mTraceName = "WakeLock (" + mTag + ")";
除以上几个属性之外,WakeLock中还有如下几个属性:
// frameworks/base/core/java/android/os/PowerManager.java
//表示内部计数
private int mInternalCount;
//表示外部计数
private int mExternalCount;
//表示是否是计数锁,默认true
private boolean mRefCounted = true;
//表示是否已经持有该锁
private boolean mHeld;
//表示和该wakelock相关联的工作源,这在一些服务获取wakelock时很有用,以便计算工作成本
private WorkSource mWorkSource;
//表示一个历史标签
private String mHistoryTag;
4、Wake Lock 申请流程
当创建好WakeLock对象以后,就可以申请WakeLock锁了。不管是永久锁还是超时锁,都是通过acquire()
方法来申请:
mWakeLock.acquire(); //申请一个永久锁
mWakeLock.acquire(int timeout); //申请一个超时锁,指定作用时间
复制代码
PowerManager#acquire()方法如下:
// frameworks/base/core/java/android/os/PowerManager.java
// 申请永久锁
public void acquire()
synchronized (mToken)
acquireLocked();
// 申请超时锁
public void acquire(long timeout)
synchronized (mToken)
acquireLocked();
//通过Handler设置一个延迟消息自动释放锁
mHandler.postDelayed(mReleaser, timeout);
复制代码
这两种申请方式完全一样,只不过如果是申请一个超时锁,会通过Handler发送一个延时消息,到达时间后去自动释放锁。继续看acquireLocked()
方法:
// frameworks/base/core/java/android/os/PowerManager.java
private void acquireLocked()
// 计数器+1
mInternalCount++;
mExternalCount++;
//如果是非计数锁或者内部计数值为1,即第一次申请该锁,才会真正去申请
if (!mRefCounted || mInternalCount == 1)
// 移除释放超时锁的Msg
mHandler.removeCallbacks(mReleaser);
try
// 通过Binder进入PMS中
mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource,
mHistoryTag);
catch (RemoteException e)
throw e.rethrowFromSystemServer();
mHeld = true; // 表示已持有该锁,申请成功
复制代码
对同一个WakeLock每申请一次,属性值mInternalCount和mExternalCount都会+1,这两个值都用来表示引用计数,前者相对于PowerManager内部,后者则相对于用户操作,之所以有两个引用计数器,主要是为了针对超时锁的释放,如果一个超时锁在已自动释放的情况下,用户手动再释放一次,相当于释放两次。这种情况下由于mExternalCount的存在,就不会导致crash。
mRefCounted用来表示计数锁或非计数锁,默认为true(计数锁),可以通过setReferenceCount()来设置:
public void setReferenceCounted(boolean value)
synchronized (mToken)
mRefCounted = value;
复制代码
mHeld
表示是否已经持有锁,可以通过调用isHeld()
来判断是已申请WakeLock。
以上逻辑都是在APP进程执行的,接下来通过mService进入到system_server,开始执行了PMS中的流程。直接来看PMS#acquireWakeLockInternal()方法:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void acquireWakeLockInternal(IBinder lock, int flags, String tag, String packageName,
WorkSource ws, String historyTag, int uid, int pid)
synchronized (mLock)
// 这是PMS中的WakeLock类
WakeLock wakeLock;
// 通过IBinder标记确认是否已申请该WakeLock
int index = findWakeLockIndexLocked(lock);
boolean notifyAcquire;
// 说明已申请过该WakeLock,则更新下该WakeLock即可
if (index >= 0)
wakeLock = mWakeLocks.get(index);
if (!wakeLock.hasSameProperties(flags, tag, ws, uid, pid))
notifyWakeLockChangingLocked(wakeLock, flags, tag, packageName,
uid, pid, ws, historyTag);
wakeLock.updateProperties(flags, tag, packageName, ws, historyTag, uid, pid);
notifyAcquire = false;
else // 说明没有申请过该WakeLock
......
// 创建一个WakeLock
wakeLock = new WakeLock(lock, flags, tag, packageName, ws, historyTag, uid, pid,
state);
// .......
// 添加到保存系统所有WakeLock的list中
mWakeLocks.add(wakeLock);
// 对于PowerManager.PARTIAL_WAKE_LOCK类型锁,省电机制Doze模式会对其进行disable处理
setWakeLockDisabledStateLocked(wakeLock);
notifyAcquire = true;
// 处理PowerManager.ACQUIRE_CAUSES_WAKEUP标记,带有此标记,进行亮屏处理
applyWakeLockFlagsOnAcquireLocked(wakeLock, uid);
mDirty |= DIRTY_WAKE_LOCKS; // 设置DIRTY_WAKE_LOCKS标记位
updatePowerStateLocked(); // 更新状态
if (notifyAcquire)
// 将申请WakeLock动作通知其他组件
notifyWakeLockAcquiredLocked(wakeLock);
复制代码
首先,通过传入的第一个参数IBinder进行查找WakeLock是否已经存在,若存在,则在原有的WakeLock上更新其属性值;若不存在,则创建一个WakeLock对象,同时将该WakeLock保存到List中,并将相关数据保存到UidState中。
4.1、setWakeLockDisabledStateLocked()
创建WakeLock实例后,接下来调用setWakeLockDisabledStateLocked()
方法,这个方法会对PARTIAL_WAKE_LOCK类型的WakeLock进行disable。系统如果持有该类型锁,会导致CPU一直保持唤醒状态而无法休眠,因此在省电策略DeviceIdle模块中,会在某些特定状态下将该类型的锁进行diable:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private boolean setWakeLockDisabledStateLocked(WakeLock wakeLock)
if ((wakeLock.mFlags & PowerManager.WAKE_LOCK_LEVEL_MASK)
== PowerManager.PARTIAL_WAKE_LOCK)
boolean disabled = false;
final int appid = UserHandle.getAppId(wakeLock.mOwnerUid);
// 非系统进程
if (appid >= Process.FIRST_APPLICATION_UID)
// Cached inactive processes are never allowed to hold wake locks.
if (mConstants.NO_CACHED_WAKE_LOCKS)
// 强制进入suspend、对应uid进程没有处于active且进程adj大于PROCESS_STATE_RECEIVER
disabled = mForceSuspendActive // 强制进入suspend
|| (!wakeLock.mUidState.mActive && wakeLock.mUidState.mProcState
!= ActivityManager.PROCESS_STATE_NONEXISTENT &&
wakeLock.mUidState.mProcState > ActivityManager.PROCESS_STATE_RECEIVER);
if (mDeviceIdleMode) //处于idle状态时,将非白名单应用wakeLock 禁用
final UidState state = wakeLock.mUidState;
if (Arrays.binarySearch(mDeviceIdleWhitelist, appid) < 0 &&
Arrays.binarySearch(mDeviceIdleTempWhitelist, appid) < 0 &&
state.mProcState != ActivityManager.PROCESS_STATE_NONEXISTENT &&
state.mProcState >
ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE)
disabled = true;
// 更新mDisabled属性
if (wakeLock.mDisabled != disabled)
wakeLock.mDisabled = disabled;
return true;
return false;
复制代码
主要有三种情况下会禁用Partical WakeLock:
- 强制进入suspend;
- WakeLock所属进程不处于active状态,且进程adj大于PROCESS_STATE_RECEIVER;
- DeviceIdle处于IDLE状态,且所属进程不在doze白名单中;
4.2、applyWakeLockFlagsOnAcquireLocked()处理亮屏标记
接下来调用applyWakeLockFlagsOnAcquireLocked()
方法,对ACQUIRE_CAUSES_WAKEUP
标记进行处理。如果WakeLock带有标志,并且WakeLock类型为FULL_WAKE_LOCK、SCREEN_BRIGHT_WAKE_LOCK、SCREEN_DIM_WAKE_LOCK这三种其中之一,则会点亮屏幕:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void applyWakeLockFlagsOnAcquireLocked(WakeLock wakeLock, int uid)
// 如果持有ACQUIRE_CAUSES_WAKEUP标记,且为亮屏相关三类锁之一
if ((wakeLock.mFlags & PowerManager.ACQUIRE_CAUSES_WAKEUP) != 0
&& isScreenLock(wakeLock))
......
// 亮屏流程
wakeUpNoUpdateLocked(SystemClock.uptimeMillis(),
PowerManager.WAKE_REASON_APPLICATION, wakeLock.mTag,
opUid, opPackageName, opUid);
复制代码
wakeUpNoUpdateLocked()
方法是点亮屏幕的主要方法,会在后面部分分析。
4.3、updatePowerStateLocked()更新全局状态
这个方法在PowerManagerService模块(一) 启动流程和核心方法中进行了分析,其中涉及到WakeLock流程的有两个方法:updateWakeLockSummaryLocked()和updateSuspendBlockerLocked()方法,前者已经分析过了,用来将所有的WakeLock统计到mWakeLockSummary全局变量中,这里对后一个方法进行分析。
4.4、updateSuspendBlockerLocked()更新SuspendBlocker
在这个方法中,将会根据系统所有WakeLock的状态,获得一个SuspendBlocker锁:
// frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private void updateSuspendBlockerLocked()
// 是否因持有WakeLock锁而需要CPU保持唤醒
final boolean needWakeLockSuspendBlocker = ((mWakeLockSummary & WAKE_LOCK_CPU) != 0);
// 是否因Display状态而需要CPU保持唤醒
final boolean needDisplaySuspendBlocker = needDisplaySuspendBlockerLocked();
// 是否开启auto_suspend模式
final boolean autoSuspend = !needDisplaySuspendBlocker;
// 是否处于交互状态
final boolean interactive = mDisplayPowerRequest.isBrightOrDim();
// 如果持有Display SuspendBlocker,则关闭auto-suspend模式
if (!autoSuspend && mDecoupleHalAutoSuspendModeFromDisplayConfig)
setHalAutoSuspendModeLocked(false);
// 申请mWakeLockSuspendBlocker锁
if (needWakeLockSuspendBlocker && !mHoldingWakeLockSuspendBlocker)
mWakeLockSuspendBlocker.acquire();
mHoldingWakeLockSuspendBlocker = true;
// 申请mDisplaySuspendBlocker锁
if (needDisplaySuspendBlocker && !mHoldingDisplaySuspendBlocker)
mDisplaySuspendBlocker.acquire();
mHoldingDisplaySuspendBlocker = true;
// 设置交互状态
if (mDecoupleHalInteractiveModeFromDisplayConfig)
if (interactive || mDisplayReady) Android 进阶——性能优化之电量优化全攻略及实战小结
程序员进阶架构师必备架构基础技能:并发编程+JVM+网络+Tomcat等