友情提示:全文6000多文字,预计阅读时间16分钟
前言
从2010年A版至2019年T版,OpenStack现在已经进入了商用成熟期,在各大厂商都有了广泛的应用。然而,OpenStack性能一直困扰着OpenStack的使用者们。笔者在对OpenStack性能分析中发现,除去python执行效率低的问题之外,OpenStack本身代码存在较多不合理之处,也对性能造成了较大的影响。
为此,笔者通过Pyflame工具对OpenStack组件——neutron的性能进行量化分析,找出其cpu耗时较多的代码片段,并对其进行优化工作,从而在代码层面优化OpenStack存在的性能问题。经以下实践,代码优化工作减少了port资源创建过程中40%的cpu耗时占比。
Pyflame介绍
Pyflame[1]是由uber工程师编写的、一个为Python程序生成cpu耗时火焰图[2]的程序效率分析工具。它是唯一一个基于Linux ptrace系统调用的Python分析器,这使得Pyflame能够获取到Python调用堆栈的快照,这意味着可以在不修改源代码(像cProfile/memory_profile都需要修改代码)的情况下分析整个程序。另外,Pyflame能够通过加载嵌入式Python解释器,比如WSGI,完全支持分析多线程Python程序。
优化前
使用OpenStack创建虚拟机流程中,与neutron直接交互的是创建端口(create-port)操作。为此,我们选取create-port操作、使用Pyflame工具生成neutron-server的火焰图,分析该操作的cpu耗时所在,并确定后续的优化思路。
注:火焰图中调用栈越深,火焰就越高。占据的宽度越宽,就表示执行时间长。所以火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。
1.1 创建port流程
使用neutron创建port,分为三个步骤:预处理、创建port的db和后处理。其中,预处理涉及默认安全组的创建;port db涉及将port写入db和通知其它插件有port创建完成;后处理涉及将port创建完成事件告知dhcp agent。
1.2 实验和数据
使用Pyflame监控neutron-server进程进行50个port并发创建,选取其中一个进程进行分析。Port创建占用cpu时间为总过程的67.65%,根据创建port的流程,我们将其分为三个部分,对其中占用cpu时间过长的部分进行梳理如下:
步骤 |
方法 |
cpu耗时(%) |
描述 |
优化? |
#port创建前的预处理 self._before_create_port |
_ensure_default_security_group |
5.38% |
1) 为租户创建默认安全组 |
Y |
# 创建port db self._create_port_db
|
_create_port_db:self.create_port_db -> create_port_db: self._get_network |
14.90% |
2)创建port的db |
Y |
_ipam_get_subnets: self._find_candidate_subnets |
6.92% |
3)选取候选的subnet |
||
allocate: ipam_subnet.allocate -> allocate: self._generate_ip |
15.68% |
4)分配ip地址 |
||
_create_port_db: self.get_network |
5.06% |
5) 与2)中调用的get_network方法相同。 |
Y |
|
create_port_db: registry.notify(resources.PORT, events.PRECOMMIT_CREATE, self, **kwargs) |
7.80% |
Y |
||
# port创建完成后处理 create_port: self._after_create_port |
5.67% |
6) 发送消息告知dhcp agents有port创建完成 |
表(1)创建port流程中各方法的CPU耗时
图(1) 创建port流程中各方法的CPU耗时
1.3 优化思路
从创建port各调用的cpu耗时占比表格中,我们可以看出以下几个问题:
存在重复的方法调用。如:neutron/db/db_base_plugin_common.py:get_network方法,就在db操作中被调用了2次,二者的总共耗时约占总cpu耗时的30%;
回调函数较多,主要用于通知其它插件或服务来对port创建过程不同状态时做相应的处理,如安全组、qos、dhcp等,cpu耗时约占30%;
创建port的大部分cpu耗时花费在db上,约占80%。
针对上述问题,我们根据从简单到复杂的优化目标进行下述优化工作。
优化后
结合上一节中的优化思路,我们进行代码优化工作,并使用Pyflame对neutron-server进行性能优化的验证。
2.1 减少流程中存在的重复调用
通过对“创建port资源”的代码流程进行梳理和分析,整理出重复调用的方法、模块以及其耗时。
表(2) 创建port流程中重复的方法调用
通过对表2的分析,我们的优化思路就在于消除重复方法调用的CPU耗时。
2.1.1 减少get_network()方法调用
从表2中,根据代码的结构和功能,保留表2中的1-2,消除其它部分get_network方法的调用。在做了代码的调整后,我们重新生成火焰图,如下所示。
2.1.1.1 验证network是否存在(1-1)
图(2) 创建port db(优化前)
图(3)创建port db(优化后)
从优化前后的火焰图可以看出,优化后的火焰图更加稀疏(实际上为图2去掉蓝框——get_network后的剩余部分),所以CPU耗时减少。
2.1.1.2 获取network信息、并验证qos_policy_id是否存在(1-3)
图(4)验证qos_policy_id(优化前)
图(5)验证qos_policy_id(优化后)
从优化前后的火焰图可以看出,优化后的火焰图更加稀疏(实际上为图4去掉蓝框——get_network后的剩余部分),所以该部分CPU耗时大大减少。
2.1.1.3 dhcp agent获取network全量信息(1-4)
create_port: self._after_create_port中存在get_network方法获取network信息,为此,我们可以消除该部分的cpu耗时。
图(6) dhcp agent获取network全量信息(优化前)
图(7) dhcp agent获取network全量信息(优化后)
从优化前后的火焰图可以看出,优化后的火焰图更加稀疏(实际上为图6去掉蓝框——get_network后的剩余部分),所以CPU耗时减少。
2.1.2减少_ensure_default_security_group()的方法调用
从表2中,根据代码的结构和功能,保留2-1,消除其它部分_ensure_default_security_group方法的调用。在做了代码的调整后,我们重新生成火焰图,如下所示。
2.1.2.1 租户的默认安全组创建(2-2)
在create_port: self._before_create_port中有存在回调函数重复调用” _ensure_default_security_group(self, context, tenant_id)”方法来为租户创建默认安全组。
图(8)租户的默认安全组创建(优化前)
图(9)租户的默认安全组创建(优化后)
从优化前后的火焰图可以看出,优化后的火焰图更加稀疏(实际上为图9去掉蓝框——_ensure_default_security_group后的剩余部分),所以CPU耗时减少。
2.1.3 CPU耗时数据分析
2.1.3.1 创建port三大主要模块CPU耗时情况
优化前后,创建port中各模块CPU耗时占比统计如下:
优化前 |
优化后 |
|||
模块 |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
create_port: self._before_create_port |
5.38% |
8.95% |
3.89% |
11.68% |
create_port: self._create_port_db |
49.05% |
81.6% |
29.05% |
87.23% |
create_port: self._after_create_port |
5.67% |
9.43% |
0.36% |
1.08%↓ |
合计 |
60.10% |
100% |
33.30% |
100% |
表(3) 创建port的三个主模块cpu耗时情况
从表3中,可以看出,neutron-server在创建port时的cpu耗时占比缩短了40%(经过多次测试统计,优化后cpu耗时占比减少了25%-40%,取决并发数目大小);且“create_port: self._after_create_port”模块的CPU耗时占比减幅最大,从9.43%减到1.08%。(当然,这也就造成了其它两个模块的CPU占比升高)。
为此,我们对另外两个模块的子模块CPU耗时占比进行计算,确定其CPU耗时优化效果。
2.1.3.2 子模块self._create_port_db
1)我们优化了create_port: self._create_port_db模块中的get_network方法,针对该模块,我们统计优化前后,其主要子模块的cpu耗时占比如下:
优化前 |
优化后 |
|||
子模块 |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
_create_port_db: self.create_port_db |
30.99% |
77.37% |
12.72% |
43.78% ↓ |
_create_port_db: self.get_network |
5.06% |
10.31% |
10.10% |
34.76% |
_create_port_db: registry.notify(resources.PORT, events.PRECOMMIT_CREATE, self, **kwargs) |
7.80% |
15.90% |
2.27% |
7.81% ↓ |
… |
… |
… |
… |
… |
合计 |
49.05% |
100% |
29.05% |
100% |
表(4) self._create_port_db主要子模块的CPU耗时占比
从表(4)中我们可以看出,优化后_create_port_db: self.create_port_db子模块cpu耗时占比减少了约30%,回调函数的cpu耗时占比减少了约8%(其中,self.get_network的耗时是不变的)。
2.1.3.3 子模块self._after_create_port
优化前 |
优化后 |
|||
子模块 |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
比值(cpu消耗时间/统计时长) |
比值(cpu消耗时间/cpu总共消耗时间) |
_notify_agents: self.plugin.get_network |
4.25% |
76.02% |
0 |
0↓ |
_notify_agents: self.plugin.get_dhcp_agents_hosting_networks |
0.93% |
16.63% |
0.06% |
17.64% |
_notify_agents: self._schedule_network |
0.23% |
4.11% |
0.14% |
41.1% |
_notify_agents: self._cast_message |
0.17% |
3.04% |
0.14% |
41.1% |
合计 |
5.59% |
100% |
0.34% |
100% |
表(5) self._after_create_port主要子模块的CPU耗时占比
从表(5)中我们可以看出,优化后_notify_agents: self.plugin.get_network的cpu耗时比为0,回调函数的cpu耗时占比减少了约8%(其中,self.get_network的耗时是不变的)。
2.2减少回调函数的耗时
上述分析中,我们通过在回调函数中减少重复调用,减少了回调函数的CPU耗时。其实,在实际应用中,我们可以通过裁减插件来减少回调函数的调用,这需要根据实际情况进行调整。
2.3 减少db操作的时间
由于篇幅限制,暂不在此文中体现。这里,提供两点思路:
1) 加速数据表和neutron obj的转化时间;
2) 引入缓存机制。由于大部分的数据库操作涉及查询,因此,可以将port的信息加入缓存中,以减少查询db的时间。
总结
本文旨在使用Pyflame对python代码进行性能分析、为OpenStack或其它python程序的性能优化提供思路。通过Pyflame为neutron创建port生成火焰图、并结合对neutron“创建port”的代码流程进行分析,提出优化思路;根据优化思路,对neutron代码进行修改,通过实验验证了优化思路的正确性,降低了neutron-server创建port的cpu耗时。实验结果表明,优化后的代码减少了创建port时资源处理流程40%的CPU资源消耗(在rally api测试时,减少cpu耗时约20%),从而提升了neutron-server创建port API的性能。
本文给出了OpenStack代码层面性能优化的三个思路:
1) 减少流程中存在的重复调用。对于重复调用的方法、尤其涉及数据库操作的方法,要尽量减少其调用次数;
2) 减少回调函数中插件(qos、security_group、l3等)的不必要耗时。该部分需要结合实际的应用场景;
3) 减少db操作的耗时。对于反复查询数据库的操作、要尽量缩减到最小。
三个思路并不是完全割裂,三者之间存在着交叉、重叠部分。
值得注意的是,在代码的性能优化中,删减或重构任何代码都需要经过深思熟虑、考虑组件或函数之间的影响,以免引出其它问题。
问题
1、为什么并发数选取在50,而不是其它数目?
A:其实在实验验证中,笔者也进行了并发100、200的实验,其实验结果如总结所述。Rally测试结果见附录。
2、并发数由什么控制?
A:起初,我们通过脚本的方式进行50/100/200并发的“创建port”;之后,也使用rally进行了api并发测试。
3、表格中分析的结果准确程度如何?总结中为何rally api测试结果cpu耗时减少为20%?
A:其实,笔者的测试针对的是“创建port”在neutron plugin中这段代码本身,经验证,这段代码的cpu耗时确实降低了不少,如总结所述为40%;但是,真正使用neutron去创建一个资源,除了plugin去处理,wsgi和evenlet服务存在cpu耗时,而且这两个服务自身耗时较长,所以,在rally api测试中,发现cpu耗时减少为20%。
参考
[1] Pyflame, https://Pyflame.readthedocs.io/en/stable/
[2] Flame graph, http://www.brendangregg.com/flamegraphs.html
注释
火焰图就是看顶层的哪个函数占据的宽度最大。只要有“平顶”(plateaus),就表示该函数可能存在性能问题。
附录
Rally api测试结果
50 并发
优化效果
优化后cpu耗时降低了22%
优化前
优化后
100 并发
优化效果
优化后cpu耗时降低了20%
优化前
优化后
-End:)
往
期
精
选
1、干货分享 | 基于RocketMQ构建MQTT集群系列(1)—从MQTT协议和MQTT集群架构说起
3、干货分享 | 时序数据库Graphite在BC-DeepWatch中的设计与使用