Python 和 C++ 中 ORM 层的性能测试





5.00/5 (7投票s)
比较两个相似的CRUD服务器应用程序的性能,一个使用Python和SQLAlchemy编写,另一个使用C++和YB.ORM编写。
引言
Martin Fowler在其著名著作《企业应用模式》(Patterns of Enterprise Application Architecture, 2002) 中解释的设计模式,对现代应用程序与关系数据库的互操作方式产生了深远影响。在该领域最富有成效的方法之一被称为对象关系映射(ORM)。乍一看,这种机制负责将业务逻辑对象映射到数据库表。尽管实现细节可能大相径庭,但所有ORM工具都会创建一个新的抽象层,承诺简化应用程序业务逻辑中对象和关系的操作。
使用对象关系映射可以真正将所谓的在线事务处理(OLTP)应用程序的开发提升到一个更高的水平。ORM框架的选择极大地影响着整个项目的架构。这些框架宣传的好处甚至可能是切换现有项目平台的理由。
但是,使用不同的ORM解决方案会带来什么开销呢?正如SQLAlchemy的作者Michael Bayer所说,这些性能损失是否真的无关紧要,而且随着支持JIT的PyPy技术被更广泛地接受,它们将消失?这并非易事,因为我们需要使用一套单一的测试来对许多不同的ORM解决方案进行基准测试。
在本文中,我们将比较使用两种不同的ORM解决方案所带来的开销:Python的SQLAlchemy与C++的YB.ORM [参见此处的快速入门]。为此,我们开发了两个特殊的测试应用程序,它们都实现了相同的OLTP逻辑,以及一个用于测试它们的测试套件。
测试平台
对于测试服务器应用程序,我们选择了以下主题:汽车停车自动化。典型操作包括:发放停车票,计算停车时间,支付服务费用,以及带着票离开停车场。这是所谓的后付费模式。还实现了预付费模式,用户预先支付一定时间费用,并可以选择随时延长停车会话。当用户离开停车场时,未使用的金额会退还到其停车账户,之后可以用于支付另一次停车会话。所有这些操作都很好地契合了CRUD(创建、读取、更新和删除)语义。
测试平台的源代码可在GitHub上找到:https://github.com/vnaydionov/teststand-parking。您可以在其中找到测试套件以及为两个不同平台实现的两个独立应用程序。
-
文件夹
parking
– 使用Python语言,SQLAlchemy(下称sa); -
文件夹
parkingxx
– 使用C++,YB.ORM(下称yborm)。
这两个应用程序都由一个单一的测试套件驱动,该套件依次重现应用程序实现的测试用例集。测试套件是用Python编写的,使用了标准的unittest
模块。两个应用程序都在测试中显示“绿灯”。
为了运行相同的测试套件来测试不同平台上的应用程序,我们需要某种形式的IPC(进程间通信)。最常见的IPC是Socket API,它允许运行在同一主机或不同主机上的进程之间进行通信。因此,测试平台API可以通过TCP套接字访问,使用HTTP处理请求/响应,并使用JSON序列化数据结构。在Python测试服务器应用程序(sa)中,我们使用SimpleHTTPServer
模块来实现HTTP服务器。在C++测试服务器应用程序(yborm)中,我们为此目的使用了HttpServer
类(文件parkingxx/src/micro_http.h
,parkingxx/src/micro_http.cpp
),该类是从YB.ORM项目的examples/auth
文件夹中借用的。为了获得更好的性能,我们在具有多核CPU的同一台主机上运行测试和应用程序。
在实际环境中,几乎总是需要并行处理传入的请求,无论是通过线程还是进程。CPython由于GIL(全局解释器锁)而在多线程方面存在已知的瓶颈。因此,Python驱动的服务器应用程序倾向于使用多进程。另一方面,C++没有这方面的顾虑。由于基准测试结果不应受到线程/进程池实现质量的影响,因此本次基准测试是以一系列顺序发送到服务器的请求来运行的。
我们使用MySQL数据库服务器及其InnoDB事务存储引擎。两个服务器应用程序都运行在同一个数据库实例上。
日志配置方式确保为两种实现生成大致相同的数据量。记录所有来自WEB服务器和数据库查询的消息。此外,还进行了禁用日志功能的额外测试。
两种实现方式基本上工作方式相同。为了进一步简化应用程序之间的比较,它们的业务逻辑代码库在结构上也很相似。在本文中,我们不讨论ORM工具的表达能力,尽管这是此类工具的一个重要考量因素。我们只比较一些数字,代码库的体积也很接近:业务逻辑代码为20.4KB(sa)对比22.5KB(yborm),数据模型描述为3.4KB(sa)对比5.7KB(yborm)。以下是使用SQLAlchemy和Python编写的业务逻辑代码示例。
def check_plate_number(session, version=None, registration_plate=None): plate_number = str(registration_plate or '') assert plate_number active_orders_count = session.query(Order).filter( (Order.plate_number == str(plate_number)) & (Order.paid_until_ts > datetime.datetime.now()) & (Order.finish_ts == None)).count() if active_orders_count >= 1: raise ApiResult(mk_resp('success', paid='true')) raise ApiResult(mk_resp('success', paid='false'))
使用YB.ORM和C++的类似示例
ElementTree::ElementPtr check_plate_number(Session &session, ILogger &logger, const StringDict ¶ms) { string plate_number = params["registration_plate"]; YB_ASSERT(!plate_number.empty()); LongInt active_orders_count = query(session).filter_by( (Order::c.plate_number == plate_number) && (Order::c.paid_until_ts > now()) && (Order::c.finish_ts == Value())).count(); ElementTree::ElementPtr res = mk_resp("success"); res->add_json_string("paid", active_orders_count >= 1? "true": "false"); throw ApiResult(res); }
硬件配置和软件套件
我们使用的测试系统是Ubuntu Linux,这是部署服务器端应用程序非常常见的选择。测试是在一台配备以下设备的台式电脑上进行的:Intel(R) Core(TM) i5 760 @2.80GHz,4GB RAM,运行64位Ubuntu 12.04。
除以下项目外,所有软件套件版本均从Ubuntu官方仓库安装:PyPy、PyMySQL、SOCI和YB.ORM。
- 操作系统内核:Linux 3.8.0-39-generic #58~precise1-Ubuntu SMP x86_64
- RDBMS服务器:MySQL 5.5.37-0ubuntu0.12.04.1
- C语言的MySQL客户端:libmysqlclient18 5.5.37-0ubuntu0.12.04.1
- 参考Python解释器:CPython 2.7.3
- CPython DBAPIv2的MySQL客户端:MySQLdb 1.2.3-1ubuntu0.1
- 支持JIT的Python解释器:PyPy 2.3.1 (https://pypy.pythonlang.cn/)
- PyPy DBAPIv2的MySQL客户端:PyMySQL 0.6.2 (https://github.com/PyMySQL/PyMySQL)
- C++编译器:GCC 4.6.3版 (Ubuntu/Linaro 4.6.3-1ubuntu5)
- ODBC驱动管理器:UnixODBC 2.2.14p2-5ubuntu3
- MySQL的ODBC驱动:MyODBC 5.1.10-1
- C++数据库连接库:SOCI 3.2.0 (http://soci.sourceforge.net/)
- C++ ORM框架:YB.ORM 0.4.5 (https://github.com/vnaydionov/yb-orm)
- Python ORM框架:SQLAlchemy 0.7.4-1ubuntu0.1 (https://sqlalchemy.org.cn/)
测试套件结构
要了解在测试套件运行时调用了哪些函数,请参见表1。修改数据库的函数是加粗显示的。它们的总时间贡献是根据使用YB.ORM和SOCI后端构建的配置日志大致计算的。
-
函数名
调用次数
单次测试套件运行对运行总时间的贡献,%
get_service_info
22
8.95
create_reservation
10
26.67
pay_reservation
8
19.30
get_user_account
7
1.24
stop_service
6
13.81
account_transfer
6
13.14
cancel_reservation
4
10.69
check_plate_number
3
0.60
leave_parking
1
2.69
issue_ticket
1
2.91
总计
68
100
表1. 按函数调用划分的测试套件结构
让我们看看测试套件在SQL语句方面的结构,参见表2。这有助于理解在完全不同的平台上构建的这两个实现的可比性。对于这两种(使用SQLAlchemy或YB.ORM构建的)版本,表中的数字略有不同,因为库的设计并非完全匹配。但是,正如测试套件中的断言所示,两种应用程序中的逻辑工作方式相同。
-
语句
来自SQLAlchemy应用程序的调用次数
来自YB.ORM应用程序的调用次数
SELECT
78
106
SELECT FOR UPDATE
88
89
更新
45
42
INSERT
27
27
删除
0
0
总计
238
264
表2. 按SQL语句划分的测试套件结构
当然,测试套件产生的这种负载与实际环境中的负载相去甚远。尽管如此,这个测试套件还是让我们有机会了解ORM层的性能水平如何相互比较。
测试方法
我们的测试套件以连续的批次运行,每批20次迭代。对于每种服务器配置,我们进行了5次计时测量,以尽量减少误差。总共对一个配置运行了100次测试套件。
计时是使用标准的Unix命令time进行的,该命令输出进程在用户空间(user)和内核(sys)模式下花费的时间,以及命令执行的实际时间(real)。后者极大地影响用户体验。
对于客户端,只考虑real计时。标准输出和标准错误输出都重定向到/dev/null。
启动测试批次的命令行示例
$ time ( for x in `yes | head -n 20`; do python parking_http_tests.py -apu https://:8111/ &> /dev/null ; done ) real 0m24.762s user 0m3.312s sys 0m1.192s
对于正在运行的服务器进程,user和sys计时具有实际意义,因为它们与CPU资源的物理消耗相关。real计时被省略,因为它会包含服务器等待传入请求或其他I/O时的空闲时间。此外,要查看测试运行期间的测量计时,必须停止服务器,因为它通常在一个无限循环中工作。
基准测试结果
以下四种配置进行了测试:
-
PyPy + SQLAlchemy
-
CPython + SQLAlchemy
-
YB.ORM + SOCI
-
YB.ORM + ODBC
对于每种配置,进行了两种类型的基准测试:启用日志和禁用日志。总共8种组合。对5次测量的结果进行了平均。图1显示了每种组合20次连续测试套件运行的测量计时。
图1. 客户端测试套件运行计时,数值越小越好
从部署和维护的角度来看,了解服务器应用程序消耗多少CPU时间非常重要。这个数字越高,服务相同数量的传入请求所需的CPU核心就越多,功耗和发热量也越高。消耗的CPU时间被计算为进程在用户模式下花费的时间加上在内核模式下花费的时间。图2显示了测量到的计时。
图2. 服务器端CPU消耗,数值越小越好
关闭日志
在寻找可能的性能改进方法时,一个经常考虑的选项是关闭调试功能。在使用ORM进行软件开发和测试时,通常会启用日志功能——需要一种方式来控制幕后发生的事情。但是,在生产环境中,日志会成为性能上的一个问题。日志可以被最小化或关闭,特别是当应用程序功能成熟后。
但是日志文件到底有多大呢?在这两个测试应用程序中,HTTP服务器的每条消息以及执行的每个SQL语句及其输入和输出数据都会被记录下来。如上所述,每个服务器配置的测试套件都运行了100次。表3显示了生成日志文件的大小。
|
pypy sa |
cpython sa |
yborm soci |
yborm odbc |
日志文件大小,MB |
19.02 |
18.97 |
20.78 |
20.95 |
行数,千 |
138.5 |
138.5 |
180.0 |
180.0 |
表3. 运行100次测试套件后日志文件的大小
进行的测量表明(表4),在禁用日志后,计时提高了多少百分比。第一行是关于服务器响应时间,第二行是关于服务器端的CPU消耗。
|
pypy sa |
cpython sa |
yborm soci |
yborm odbc |
客户端测试 |
10.0 |
12.4 |
11.5 |
12.6 |
CPU消耗 |
11.0 |
26.0 |
19.2 |
21.4 |
表4. 关闭日志后性能提升百分比
结论
基于这次基准测试,我们得出以下结论:
-
在这四种服务器实现中,最快的是使用
C++(yborm odbc)实现的。它的性能是Python中最快的实现(cpython sa)的三倍。而C++的代码量仅增加了20%。 -
在某些情况下,服务器响应时间,即用户看到响应后的时间,如果使用YB.ORM,也可以缩短。在本例中,改进幅度约为38%。
-
PyPy在运行SQLAlchemy这类多层框架时所承诺的性能尚未实现。
-
出于尚未发现的原因,使用带SOCI后端的YB.ORM的性能比使用ODBC后端的性能稍差。
-
禁用服务器端的日志功能并没有像预期的那样产生巨大影响。特别是,在CPython+SQLAlchemy中观察到服务器端最显著的改进:26%。对于YB.ORM,这一改进约为20%。
Python编程语言长期以来一直被认为是高生产力的语言。原因之一是“编码”-“运行”周期更短。并且拥有SQLAlchemy这样优秀的工具,使得该平台更具吸引力。同时,性能有时可能会受到影响。在某些情况下,最好使用Python进行原型设计,然后将最终实现交给C++,而C++今天已经拥有了可与之媲美的框架和工具。
历史
- 2015-01-28:发布初始版本