ELK专题:使用Elastic APM对Python项目实施性能监控

前言

在运维的技术栈里面,说到监控系统,大多数人可能首先想到的对后台服务器的CPU、内存、网络流量、磁盘IO等性能指标监控起来。要是再深入一点,可能会再做一个日志监控,把各个事务的日志统计起来,可以跟踪各个事务的时间和数量的分布等等。

那假如,我们想更深入一层,想要知道各个API在处理事务的过程中在每个步骤上都花了多少时间呢?比方说,有一个接口GET /user_info/<user_id>,服务器在处理这个接口的时候需要从数据库上使用user_id查询用户的昵称,同时请求其他API得到玩家的排行榜信息。我们自然可以使用日志打点的方式把这些步骤都记录下来,再通过ELK的日志监控去完成,但这样无疑会增加大量的开发成本,而且很有可能需要反复打磨才能获得一个满足各个场景的通用标准。

而今天要说的Elastic APM(application performance monitoring)就是专门针对这种场景的通用解决方案。

title

实现原理

Elastic APM的技术栈由四个组件组成:APM agent、APM Server、Elasticsearch、Kibana。

arch

  • APM agent: 植入到业务应用的代码中,采集应用运行期间的性能数据,各个主流的开发框架都有现成的APM库可以直接使用。

  • APM Server: 所有应用的性能数据最后都会汇聚到APM Server中,可以在APM Server按需进行数据转换、校验或者过滤后再输出到Elasticsearch。

  • Elasticsearch(ES):和日志监控一样,APM Server采集回来的性能数据最后会以文档的形式存放到ES的索引中,同时ES会提供强大的存储和查询功能。

  • Kibana: 提供数据可视化功能,在最新的版本中还可以提供ELK Stack集群的管理功能。

总体而言,Elastic APM的实现原理和本专题之前讨论的ELK日志分析系统类似,只是数据源的采集方式不同。

测试环境搭建

在之前的实践中,在笔者的实验环境已经有一套现成的ES和Kibana,今天我们重点放在APM的搭建。

版本信息

根据官方的兼容矩阵,本实验环境选用的组件版本如下:

参考:

https://www.elastic.co/support/matrix#matrix_compatibility

https://www.elastic.co/guide/en/apm/get-started/current/agent-server-compatibility.html

https://www.elastic.co/guide/en/apm/agent/python/6.x/supported-technologies.html

组件 IP地址 版本
Kibana 192.168.0.213 7.13.4
ES 192.168.0.212 7.13.4
APM Server 192.168.0.214 7.15.2
APM Agent 192.168.0.129 6.x
Python 192.168.0.129 3.9
Flask 192.168.0.129 1.1

APM Server 安装

笔者使用的是Ubuntu 20.04系统,官方文档或者Kibana的页面里面都有详细的指引,部署过程如下:

1
2
curl -L -O https://artifacts.elastic.co/downloads/apm-server/apm-server-7.15.2-amd64.deb
sudo dpkg -i apm-server-7.15.2-amd64.deb

更多详细安装步骤可以参考:https://www.elastic.co/guide/en/apm/server/7.15/installing.html

APM Agent 安装

在本测试环境中,使用Python Flask搭建一个demo。在开始测试代码之前,需要先安装如下Python库:

参考文档:https://www.elastic.co/guide/en/apm/agent/python/6.x/flask-support.html

1
pip install "elastic-apm[flask]"

Server与Agent联调

在APM Server使用的测试配置文件apm-test.yaml内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apm-server:
host: "0.0.0.0:8200"
rum:
enabled: true

# 运行日志直接通过sdtout输出
output.console:
codec.json:
pretty: true
escape_html: false

queue.mem.events: 4096

max_procs: 4

使用命令apm-server -c /root/apm-test.yaml启动APM Server服务,屏幕输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# apm-server -c /root/apm-test.yaml 
{
"@timestamp": "2022-03-13T09:41:16.726Z",
"@metadata": {
"beat": "apm-server",
"type": "_doc",
"version": "7.15.2",
"pipeline": "apm"
},
"processor": {
"name": "onboarding",
"event": "onboarding"
},
"observer": {
"ephemeral_id": "ba3423ad-6249-494a-8c36-26cbb873d5f6",
"hostname": "apm-server",
"id": "fe30a887-a251-4e3d-b353-1809f87ff49a",
"type": "apm-server",
"version": "7.15.2",
"version_major": 7,
"listening": "[::]:8200"
},
"ecs": {
"version": "1.11.0"
}
}

Python测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask
from elasticapm.contrib.flask import ElasticAPM

app = Flask(__name__)

app.config['ELASTIC_APM'] = {
'SERVICE_NAME': 'my-app',
'SERVER_URL': 'http://192.168.0.214:8200',
'ENABLED': True,
'LOG_LEVEL': "debug",
}

apm = ElasticAPM(app)

@app.route('/')
def hello():
return 'hello world!'

if __name__ == "__main__":
app.run(host='0.0.0.0')

启动上面的flask代码后,当flask应用接收到请求后,APM Server会收到应用上报的性能数据,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
"@timestamp": "2022-03-13T09:53:31.999Z",
"@metadata": {
"beat": "apm-server",
"type": "_doc",
"version": "7.15.2",
"pipeline": "apm"
},
"user_agent": {
"original": "curl/7.61.1"
},
"event": {
"outcome": "success"
},
"http": {
"request": {
"method": "GET",
"headers": {
"Accept": [
"*/*"
],
"Host": [
"192.168.0.129:5000"
],
"User-Agent": [
"curl/7.61.1"
]
},
"env": {
"REMOTE_ADDR": "192.168.0.108",
"SERVER_NAME": "0.0.0.0",
"SERVER_PORT": "5000"
},
"socket": {
"remote_address": "192.168.0.108"
}
},
"response": {
"status_code": 200,
"headers": {
"Content-Type": [
"text/html; charset=utf-8"
],
"Content-Length": [
"12"
]
}
}
},
...省略...
}

到这里,我们已经完成了APM Server和APM Agent的联调了。

实战演练

示例代码

下面的示例代码,会模拟一个现实API的工作场景:

  • 服务启动后,功能演示如下:
    1
    2
    $ curl http://192.168.0.129:5000             
    i hava a uuid: beeca785-0022-4313-a2d4-0b26154e10af! I have been seen 1210 times.
  • 当接到请求时,查询并返回Redis中的数据(一个随机uuid)
  • 在特定的场景下(用了一个随机数模拟),需要向另一个API请求数据,最后再存储到数据库中
  • 通过redis计算页面的访问次数并显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from flask import Flask
from elasticapm.contrib.flask import ElasticAPM
import redis
import requests
from random import randint

app = Flask(__name__)
app.config['ELASTIC_APM'] = {
'SERVICE_NAME': 'my-app',
'SERVER_URL': 'http://192.168.0.214:8200',
'ENABLED': True,
'LOG_LEVEL': "debug",
}
apm = ElasticAPM(app)
cache = redis.Redis(host='192.168.0.251', port=6379, db=0, password='aaaa1111')

@app.route('/')
def hello():
count = cache.incr('hits')
if randint(1,10) > 5:
url = 'https://httpbin.org/uuid'
get_uuid = requests.get(url).json()['uuid']
cache.sadd('uuid',get_uuid)
else:
get_uuid = cache.srandmember('uuid')
return 'i hava a uuid: {}! I have been seen {} times.\n'.format(str(get_uuid),count)

if __name__ == "__main__":
app.run(host='0.0.0.0')

APM Server 输出到ES

为了更好地实现性能数据的存储和可视化,我们还需要借助ES和Kibana,笔者在之前的文章中已经完成了Kibana和ES的联调,这里就不多赘述。在本测试环境中,APM Server使用的配置文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apm-server:
host: "0.0.0.0:8200"
rum:
enabled: true
logging.level: debug
logging.to_files: true
logging.files:
path: /var/log/apm-server
name: apm-server
keepfiles: 7
permissions: 0644
# 指定ES中使用的索引模板
setup.template.name: "apm-template"
setup.template.pattern: "apm-*"
output:
elasticsearch:
hosts: ["https://192.168.0.212:9200"]
username: "elastic"
password: "xxxxxx"
index: "apm-%{+yyyy.MM}"
# 本测试环境中对ES进行了SSL安全加固, 需要使用证书连接
ssl:
enabled: true
certificate_authorities: "/etc/apm-server/elasticsearch-ca.pem"
queue.mem.events: 4096
max_procs: 4

API测试

使用ab工具,对测试API进行60秒压力测试,并发连接数10,测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ab -c 10 -t 60 'http://192.168.0.129:5000/'
...省略...
Requests per second: 19.82 [#/sec] (mean)
Time per request: 504.653 [ms] (mean)
Time per request: 50.465 [ms] (mean, across all concurrent requests)
Transfer rate: 4.56 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 1
Processing: 3 494 490.0 953 1671
Waiting: 2 493 490.0 952 1664
Total: 3 494 490.0 953 1671
...省略...

在上面的Python代码中通过随机数去控制概率,会有一半的请求需要通过httpbin的接口获得UUID,所以最后返回时间的中位数是953ms。

在Kibana上的可视化

服务概况

我们可以在Kibana页面的”APM -> Service Name -> Overview”中查看业务的概况:

overview

▲ Overview页面中可以直观地看到以服务器节点或者API路径为维度的延时、访问量的变化。

Metrics

metrics

▲ 也可以查看服务进程的CPU和内存使用情况。

Transactions

这是笔者最常用的一个功能, 查看特定API路径的运行情况,本文的demo只有一个GET /:

transaction

timeline-1

▲ 在trace sample栏目可以看到APM Agent的采样,在timeline会显示该样本在处理这个请求时候每一个细分步骤和花费的时间。

timeline-2

▲ 这是返回时间958ms的一个样本,我们可以看到有954ms都花在了请求httpbin上。

What’s Next

笔者目前在一个手游项目上担任运维工程师,在项目筹备阶段就先后完善了系统和日志的监控,再到后来通过服务拨测功能也覆盖了部分关键游戏场景的业务监控。在前一段时间,为了在压测或者是日常运营的时候能更有效地对各个API接口进行优化,才引入了本文的介绍到的APM工具。

而当初在给开发团队推荐介绍Elastic APM的时候,也是从类似这样的demo开始的,当然在会议室里面那个demo是结合项目的实际API做的,功能和场景也会更丰富。

尽管已经说了很多,但搭建一个demo往往只是开始,到后面的调研、使用试点服务在测试环境反复打磨乃至上线到生产环境,期间也经历了更多深入的研究。比如说:使用现成的云产品还是自己搭建、各个模块之间的认证方式、APM Agent带来的性能损耗、对不同代码语言的改造方案、集群容量、APM Agent采集回来的数据类型、ES的索引模板、Kibana可视化方案等,都需要反复斟酌。

和本系列的文章类似,本文旨在提供一个思路,希望能给读者带去一点参考经验。

推荐阅读

本站文章:ELK专题

APM Data Model