使用 mod_perl 加速数据库访问

作者:Reuven M. Lerner

上个月,我们开始研究 mod_perl,这是一个用于 Apache Web 服务器的模块,它将 Perl 的副本放入我们的 HTTP 服务器内部。mod_perl 不仅为我们节省了每次运行 CGI 程序时 fork 新进程和调用 Perl 的开销,而且它还缓存编译后的程序,进一步缩短了启动时间。

与生活中的其他一切事物一样,使用 mod_perl 也有其权衡之处。CGI 程序可以用任何语言编写,并且可以在任何平台上的任何 Web 服务器上运行。相比之下,mod_perl 仅适用于 Apache(它在大多数 UNIX 版本下运行,并且即将发布 Win32 版本),并且要求程序以 Perl 编写。如果您对最大的可移植性感兴趣,您可能会考虑坚持使用 CGI。但是,如果您像我一样,大部分时间都在使用 Apache 和 Perl,您可能会认真考虑转向 mod_perl。

然而,这还不是故事的结尾。即使 mod_perl 比 CGI 的 fork 和执行方法快无限倍,主要的瓶颈仍然存在:打开到数据库服务器的连接。

如果您的 Web 应用程序与关系数据库服务器通信,则必须先打开连接才能发送查询。您的程序和数据库服务器之间的初始对话可能需要相当长的时间,这既是因为必须建立初始 TCP 连接,也是因为数据库服务器并非为单查询连接而设计。许多服务器期望您连接一次,发送多个请求,接收多个响应,并在完成后断开连接。相比之下,Web 应用程序倾向于为每个查询打开一个连接,这可能会不必要地减慢您的程序速度。

对我们来说幸运的是,编写 mod_perl 的人员创建了一个旨在解决此问题的模块。Apache::DBI,正如其名称所示,它利用 mod_perl 的变量持久性(即,变量值从程序的一次调用保留到另一次调用)来保持数据库连接打开。每次调用使用 Apache::DBI 的程序只需向数据库发送查询并根据返回的响应采取行动即可。

本月,我们将总体上了解 DBI 规范,特别是 Apache::DBI 模块。我们还将快速了解 BenchmarkLWP 模块,它们可以帮助进行一般的代码性能分析,并且使我们能够了解 mod_perl 和 Apache::DBI 可以使我们的程序快多少。

配置

要使 mod_perl 与 Apache 一起工作,您需要编译并安装这两个程序的最新版本。(完整的说明在上个月的“At the Forge”中提供。)要利用 Apache::DBI 的所有功能,您需要使用超出默认选项的选项来编译 mod_perl。这些选项可以通过在与 Makefile.PL 的初始调用同一行添加 OPTIONNAME=1 来指定,Makefile.PL 是编译和安装过程的第一阶段。

我发现最简单(如果有点浪费)的编译方法是使用 EVERYTHING 标志,它会启用所有 mod_perl 选项,即使是 Apache::DBI 不需要的选项也是如此。要执行此操作,请在初始 mod_perl 目录中键入以下内容

perl Makefile.PL EVERYTHING=1

mod_perl 和 Apache 配置的其余部分与上个月描述的相同。请参阅 mod_perl 附带的 INSTALL 文档,了解如何仅启用您需要的功能,而不是使用 EVERYTHING=1

Apache::DBI 与使用 mod_perl 的程序自动配合使用。换句话说,任何使用 Apache::Registry(或多或少是 mod_perl 等效于 CGI)的程序都会自动获得 Apache::DBI 的好处,前提是后者在配置文件中指定(如下所述)。您可以配置一个目录以使用 Apache::Registry,其方式与配置它以使用 CGI 非常相似,在 srm.conf 文件中使用如下指令

# Deal with mod_perl
<Location /perl-bin>
SetHandler perl-script
PerlHandler Apache::Registry
Options ExecCGI
</Location>

然后,您可以使用以下指令插入到 srm.conf 中,指示 mod_perl 在所有使用 Apache::Registry 的目录中加载 Apache::DBI

PerlModule Apache::DBI
现在您需要重新启动服务器,很可能以 root 身份登录,方法是键入
# killall -v -1 httpd
服务器现在已准备好使用 mod_perl 与数据库通信。但是,在我们能够利用 Apache::DBI 之前,我们需要更仔细地研究一下 DBI。
什么是 DBI?

市场上有数十种(甚至数百种)数据库,其中一些在 Linux 下运行。我曾经在自己的咨询工作和本专栏中使用过的一种产品是 MySQL,这是一个由 TcX DataKonsult AB 分发的“大部分免费”数据库(用作者的话来说)。“资源”侧边栏包含有关在哪里可以下载 MySQL 的源代码和二进制文件的信息。

我们在之前与 MySQL 的交互中编写的 CGI 程序使用了 Perl 的 Mysql 模块,该模块使我们能够访问 MySQL 的所有功能。Mysql.pm 继续适用于大多数应用程序。

但是,如果您有兴趣跟上 Perl 社区内的最新标准和趋势,您应该切换(就像我一样)到 DBI,Perl 程序的通用数据库接口。DBI 允许您在任意数量的数据库上使用相同的代码。也就是说,您可以编写一个与 MySQL、Sybase、Oracle 或任何其他数据库产品通信的程序——并且您只需更改一个词即可将该程序移植到另一个数据库产品。这使得 Perl 成为一种非常强大且可移植的数据库访问语言。

DBI 分为两部分:通用的 DBI 模块引擎,可以从 CPAN(Comprehensive Perl Archive Network,综合 Perl 档案网络)下载,以及用于您希望访问的特定品牌数据库的 DBD(数据库驱动)。只有一个 DBI 模块,但对于您可能希望访问的每个数据库,都有一个不同的 DBD。(有关在哪里可以获取 DBI 和 DBD 的信息,请参阅“资源”。)如果您计划使用多个数据库服务器,则必须安装多个 DBD。您可以根据需要安装任意数量的 DBD;它们是并行安装的,因此不会冲突。

如果您使用的是 MySQL,那么您将必须下载用于 Msql 的驱动程序,MySQL API 的接口就是以该数据库为模型的。在编译和安装必要的模块之前,Msql-modules 包会询问您是否要为 Msql、MySQL 或两者都安装 DBD。

使用 DBI

一旦您安装了 DBI 和适当的 DBD,您将能够执行您通常期望从数据库执行的所有操作。语法与我们在之前的“At the Forge”专栏中看到的语法略有不同,但在概念上非常相似。一旦您看到一些示例,您应该很快就可以开始使用 DBI。

与数据库的连接保存在数据库句柄中,通常存储在名为 $dbh 的变量中。数据库句柄不仅为您提供了一种紧凑的、面向对象的方式来访问数据库方法,而且还意味着您可以同时连接到多个数据库,为每个连接提供自己的数据库句柄。(例如,当从 Sybase 移动信息到 Oracle 时,这可能很有用。)

基本语法相当简单

$dbh = DBI->connect($data_source, $username,
        $password);

如您所见,connect 方法接受三个参数。第一个参数 $data_source 定义了您希望访问的数据库,以及服务器所在的计算机名称和该计算机上的访问端口。后两个参数在理论上是可选的,但大多数配置将(并且应该)要求它们。

例如,我的家用计算机上的大多数测试程序都使用以下语法

$dbh = DBI->connect("DBI:mysql:test:localhost");

因为我使用未受保护的“test”数据库,所以不需要用户名或密码。需要用户名和密码的生产环境站点将使用如下语法

$dbh = DBI->connect("DBI:mysql:classifieds:dbserver",
        "classy" "51haf3");
在上面的代码行中,我们再次使用 MySQL DBD 进行连接。但这一次,我们连接到名为 classifieds 的数据库,该数据库位于名为 dbserver 的机器上,用户名为 classy,密码为 51haf3。请记住,数据库系统上的用户名和密码与 UNIX 系统上的用户名和密码无关。出于安全目的,您应该在数据库中使用与系统上实际使用的密码不同的密码(甚至可能不同的用户名)。

如果连接成功,则可以将 $dbh 用作进入数据库的入口点。如果连接失败,$dbh 保持未定义状态。这使我们能够使用以下错误检查代码

&log_and_die($DBI::errstr) unless $dbh;

&log_and_die 例程是我一直以来的最爱之一(并且可能为本专栏的长期读者所熟悉),它会在屏幕上打印错误消息,然后优雅地退出。包含子例程 &log_and_die 的完整代码清单可在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue52/2991.tgz 获取。

现在我们已连接到数据库服务器,我们可以向其馈送一个或多个 SQL 查询,即结构化查询语言。

如果我们要将值插入到数据库的表中,我们可以简单地说类似

$sql = "INSERT INTO test_insert (contents) VALUES
(\"$random\") ";

在将 SQL 查询放入标量变量之前使用它不是必需的,但如果您需要调试代码,则会有所帮助。(这样,您可以轻松地在程序中间添加“print”语句。)

设置好查询后,我们使用 do 方法告诉数据库执行请求的操作。这会返回一个变量,我将其称为 $successful_insert;与 $dbh 非常相似,只有在查询成功时才定义 $successful_insert

$successful_insert = $dbh->do($sql);
print "<P>Success!</P>\n" if $successful_insert;

最后,我们断开与数据库的连接。这并非完全必要,因为当我们不再使用连接时,Perl 会关闭连接。尽管如此,清理始终是一种良好的编程习惯

$dbh->disconnect;
上面的语法适用于任何我们不期望从中获得结果的 SQL 查询,即 INSERTDELETEUPDATE。(有关 SQL 的更多信息,请参阅“资源”中的一些书籍推荐。)
使用 SELECT 检索行

如果我们要从数据库中检索匹配的行,我们需要稍微修改一下语法。毕竟,我们不仅期望收到关于数据库是否能够执行我们请求的操作的报告,还要查看结果。

假设我们已连接到数据库,我们将 $sql 设置为我们的 SQL 查询

$sql = "SELECT id,contents FROM test_insert";

然后我们使用 prepare 方法发送我们的查询,如下所示

$sth = $dbh->prepare($sql);
$dbh->prepare 的结果称为“语句句柄”,传统上命名为 $sth。正如 $dbh 允许我们对我们已连接到的数据库执行操作一样,$sth 允许我们对我们刚刚发送的语句执行操作。并且正如 $dbh 在发生错误的情况下未定义一样,$sth 也是如此
&log_and_die($sth->errstr) unless $sth;
假设 $sth 已成功发送到数据库,我们告诉数据库执行我们的查询,并检查返回代码是否存在问题
$sth->execute || &log_and_die($sth->err);
现在到了有趣的部分,即遍历返回给我们的每一行。我们可以通过检查 $sth->rows 的值来找出作为查询结果返回了多少行。然后,我们可以使用 $sth->fetchrow 逐行检索返回的每一行(每行一个列值)。当没有更多行要检索时,$sth->fetchrow 返回 false,这意味着我们可以在“while”循环中使用它。实际上,这是 DBI 世界中相当标准的惯用法
# Loop through returned rows
while (@row = $sth->fetchrow)
{
# Grab the columns from the row
$id = $row[0];
$contents = $row[1];
# Print the ID and the contents
print "<P>$id:\"$contents\"</P>\n";
}
当我们完成此语句后,我们使用与语句关联的 finish 方法,这类似于数据库句柄的 disconnect 方法
$sth->finish;
现在我们已经在理论上回顾了所有这些内容,让我们将其付诸实践。首先,我们将在 MySQL 的“test”数据库中创建一个小表,方法是运行 mysql 客户端程序
mysql test
一旦我们看到 mysql> 提示符,我们就可以创建我们的小型测试表
CREATE TABLE test_insert
 (id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY
        KEY, contents VARCHAR(50) NOT NULL,
        UNIQUE (contents));
上面定义了我们的表 test_insert,使其具有两列。第一列 id 定义为包含无符号整数。整数的存在是强制性的 (NOT NULL),每次我们在表中插入一行时都会自动递增,并且可以用作表的唯一索引。第二列 contents 是可变长度的字符串,其存在是强制性的 (NOT NULL),并且不能在另一个记录中重复 (UNIQUE)。

代码清单 1 中的 CGI 程序演示了上述所有内容,首先将多行插入到表中,然后检索它们。大多数 DBI 程序都像这个程序一样简单,尽管许多程序要么存储信息,要么检索信息,而不是两者都做。

DBI 这样的标准存在的问题之一是接口遵循最小公分母。也就是说,除了管理和速度之外,数据库包之间还存在差异;几乎每个包都包含许多非标准的 SQL 命令和功能,以便将自己与竞争对手区分开来。如果您有兴趣使用此类功能,您可能必须使用 func DBI 方法,该方法启用专有数据库扩展。当然,这样做意味着您的程序不再可移植到其他数据库,如果您切换到另一个供应商,这可能会成为一个问题。

将程序移至 Apache::DBI

我们现在已经编译了 Apache 以使用 mod_perl,配置了一个 perl-bin 目录来服务 mod_perl 程序,并配置了 Apache 以为 perl-bin 目录中的所有程序插入 Apache::DBI 模块。我们已准备好采用我们的示例 DBI 程序并将其与 mod_perl 一起使用。

为了使其与 mod_perl 一起工作,我们必须如何修改程序?实际上,如果我们按照上述方式配置了 Apache 副本,我们根本不需要进行任何修改。我们所需要做的就是将我们的程序复制到 perl-bin 中,设置适当的权限,然后试用一下。例如,这是我在我的计算机上编写的内容

~httpd/cgi-bin% cp dbi-demo.pl ../perl-bin/
~httpd/cgi-bin% chmod ug+x ../perl-bin/dbi-demo.pl

我更改了打开的浏览器窗口中的 URL,使其指向 perl-bin 而不是 cgi-bin,——瞧——一切都正常工作了。

当我第一次开始使用 mod_perl 和 Apache::DBI 时,我不确定程序会运行得快多少。执行速度肯定看起来更快了,但我不确定我看到的改进有多大。我决定使用 Perl 的 Benchmark 模块,比较两个不同程序的执行速度。我将尝试将 100 个随机文本字符串插入到数据库中,首先使用 CGI 程序,然后使用相同程序的 Apache::DBI 版本(正如我们现在所知,这仅仅意味着放置在 perl-bin 目录中的程序版本)。

基准测试是一项棘手而微妙的工作,并且在计算这些结果时,无疑有一些因素被我忽略了。即便如此,它们似乎也证明了 CGI 和 mod_perl 之间惊人的性能差异。我确信,如果我花更多的时间来改进我的 Apache 和/或 MySQL 配置,我可以从我运行 Red Hat 4.2 的低端 75 MHz 奔腾中获得更好的性能。但是,相对数字应该是不言自明的。

首先,让我们检查一下我执行的测试。我使用了与之前看到的相同的 MySQL 中的 test_insert 表。然后,我编写了一个 CGI 程序,类似于我们之前看到的程序,该程序连接到数据库并将一个随机值插入到 contents 列中。生成的程序显示在代码清单 2中。

速度有多快?

现在我们有了测试程序,我们实际上应该如何测试它?我使用 Benchmark.pm 编写了一个简短的 Perl 程序,在代码清单 3中。timethese 函数由 Benchmark.pm 导入,我们在程序开头引入了它。我们还引入了 LWP::Simple,它是“Perl WWW 访问库”的一部分,可以轻松编写小型 Web 客户端。有多简单?好吧,以下单行命令返回 https://linuxjournal.cn/ 的 HTML 内容

perl -e 'use LWP::Simple;
print get "https://linuxjournal.cn";'

Perl 不会为您格式化输出。这就是 Web 浏览器和 Web 客户端之间的区别;前者旨在为人类检索信息,而后者旨在为程序检索信息。在这种特殊情况下,我们只想模拟通过 Web 对我们的每个程序进行 100 次检索。任何时间差异都将归因于服务器端的程序——由于它们是相同的,这意味着差异将归因于 mod_perl 和 Apache::DBI。

Apache::DBI 版本比其 CGI 版本快多少?以下是我运行 time-db.pl 得到的结果

[1086] ~% ./time-db.pl
Benchmark: timing 100 iterations ...
Apache::DBI: 24 secs (1.77 usr 0.67 sys = 2.44 cpu)
Plain CGI: 394 secs (1.10 usr 0.61 sys = 1.71 cpu)

这真是太大的差异了。当我第一次运行这个基准测试时,我确信普通的 CGI 程序不知何故卡住了。唉,事实并非如此;与 CGI 相关的开销实在太大了。

这是同一基准测试的第二次运行,仅供比较。

[1099] ~% ./time-db.pl
Benchmark: timing 100 iterations ...
Apache::DBI: 28 secs (1.89 usr 0.61 sys = 2.50 cpu)
Plain CGI: 355 secs (1.15 usr 0.62 sys = 1.77 cpu)

是的,看起来 CGI 确实慢得多。顺便说一句,您可以看到 Apache::DBI 使用了比普通 CGI 更多的 CPU 时间——这意味着时间花在了 fork 新的 Perl 进程上,而不是执行我们程序的计算。

如果我们删除 srm.conf 中的 Apache::DBI 指令并重新启动服务器会怎么样?这将指示打开数据库连接使用了多少开销。正如您所看到的,事情确实变慢了——尽管必须承认,减速幅度不大

[1104] ~% ./time-db.pl
Benchmark: timing 100 iterations ...
Apache::DBI: 34 secs (1.97 usr 0.63 sys = 2.60 cpu)
Plain CGI: 460 secs (1.19 usr 0.60 sys = 1.79 cpu)

因此,寓意似乎是,从 CGI 迁移到 mod_perl 可以带来巨大的性能提升,而从 DBI 迁移到 Apache::DBI 可以带来适度的性能提升。您的 Web 应用程序执行的数据库访问越多,这些技术可能在您的工作中就越有用。Perl 一直被认为是一种有用的语言,但很少被认为是一种可以帮助您编写快速软件的语言。现在,借助 mod_perl 和 Apache::DBI,您可以快速编写 Web 应用程序,并观看它们快速运行。

资源

Speeding up Database Access with mod_perl
Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。在他的业余时间,他做饭、阅读并为社区的教育项目做志愿者。您可以通过 reuven@netvision.net.il 与他联系。
加载 Disqus 评论