了解一般的技术和设计,用于编写与 Oracle 数据库的使用直接相关的可管理、可伸缩的快速 PHP 代码。
在过去九年中,PHP 已经从组装个人网站的小型语言发展到为世界上某些最大和流量最高的网站提供动力。任何高流量网站的三个最重要的设计方面是可伸缩性、性能和可维护性。可伸缩性意味着您的应用程序流量负载可以不断增长,而不会从根本上破坏其工作方式。性能是快速为单个请求提供服务的能力。 可维护性是能够在不造成过多负担的情况下修复、重新调整、扩增或更改应用程序的品质。
利用 PHP 来实现这三个设计目标并不困难,但确实需要预先考虑如何设计和构建您的应用程序。关于编写可管理、可伸缩的快速 PHP 代码的论题范围很广;针对每个论题都有大量的技术和文章。在本文中,我们将讨论那些与使用 Oracle 及 PHP 直接相关的因素。有很多一般(非 Oracle 专用)技术和设计可能非常有用。
我喜欢以一个尖锐的警告作为任何与性能相关的谈话或文章的开始:始终要记住,最后总有一天,快速却不完善的应用程序将毫无价值。性能调整以及对应用程序不利因素的设计提取都很容易分散您的精力。Web 的性质就是这样,经常发布版本的方法非常有效。(发布网站“新版本”的成本很低,因为最终用户始终需要这些代码。)这就允许您延迟对代码的重大调整,直到需要这样做为止。因此,首要目标应该是创建便于重新调整的代码。
创建和管理连接
与 Oracle 数据库最基本的交互之一是连接。要了解连接如何影响您的应用程序的性能和可伸缩性,需要了解连接的生命周期,如图 1 所示。每个步骤所涉及的工作如下:
客户创建连接: 客户创建与 Oracle 监听器的网络连接,提供其认证证书,并请求会话。
服务器创建一个新会话:在认证之后,服务器为客户创建一个新会话。如果您没有通过 Oracle 多线程服务器(MTS ― 它在可伸缩性及性能问题上臭名昭著)使用共享会话,则此步骤包括服务器为会话创建一个专用进程。该进程通常称为影子进程。创建此进程需要不少工作量。除了创建进程的正常开销之外,影子进程在其创建期间还必须临时锁定某些共享系统资源。
客户端执行查询: 既然客户端已经具有开放的连接,就可以根据需要来执行查询。
客户端关闭连接: 当客户端完成工作后,关闭与服务器的连接。
服务器毁坏会话: 与用户会话相关的影子进程被毁坏,任何未提交的事务被回滚。
图 1:连接的生命周期
由于创建新的影子进程的成本相当大,我们应该在必要时努力避开它。达到此目的的最简单方法是使用持续连接。PHP 被设计为一种非会话状态的语言。这意味着在默认情况下,在请求期间创建的任何信息(或例程化的资源)都会在请求结束时被彻底清除并毁坏。对于 Oracle 客户连接,我们希望避免这种行为。
为了使连接能够从一个请求保留到下一个请求,您可以使用以下两种连接变通方法之一:
OCIPLogin($username, $password [, $tnsname])
或
OCINLogin($username, $password [, $tnsname])
这两个函数都创建持续的服务器连接,尽管 OCINLogin() 将为每个请求创建一个新会话句柄。如果您的应用程序要使用事务,并且您希望将同时发生的事务分散到多个会话中,则可以使用 OCINLogin()。
使用持续连接的一个副作用是您更容易出现进程不足的情况。基于专用 Oracle 数据库运行单个 Apache Web 服务器(子进程的最大默认数量为 256)时,可能从不会遇到问题。但是如果增加到 4 个 Web 服务器,每个服务器运行 256 个具有持续 Oracle 连接的子进程,现在则要创建 1024 个与 Oracle 数据库的连接,并且很快就会与 Oracle 实例的资源限制发生冲突。
在 Oracle 实例配置文件 (init.ora) 中,有两个可调整的参数:
sessions = NNNN 和 processes = NNNN。
这两个参数控制着实例可以支持的最大会话数和最大进程数。如果您需要支持 1024 个同时出现的连接,则需要至少 1024 个会话(因为 OCINLogin() 连接和某些递归查询可能在每个连接中需要多个会话),而需要的进程还会更多(因为我们还需要考虑 Oracle 后台进程)。不幸的是,不能任意将这些进程设得很高。Oracle 进程消耗不少的专用内存(在多数系统中每个进程需要 2 到 3MB)。从个体来说,这些进程很小,但当把它们作为一组并与服务器系统全局区 (SGA) 所需要的共享内存相结合时,很快就会让您因为数据库服务器的物理内存限制而感到烦恼。
来自 MySQL 环境的用户可能试图避开持续连接(这是 MySQL 环境中的建议)。由 Oracle 影子进程启动所导致的栓锁和文件争用使得非持续连接的使用效率极低。那么解决方案是什么呢?
应该确定我们的数据库能够支持多少个同时发生的会话,并相应地设置其限制。有些文章详细说明了如何完成此工作,但主要是一个计算过程:计算系统中物理内存的总量,并减去内核、支持程序和 Oracle 后台进程所使用的内存。然后减去所有被配置为 Oracle 共享内存的那些内存(共享池和缓冲区高速缓存)。剩下的内存可以用于影子进程。将该数量除以一个影子进程所使用的专用进程内存的平均数量(应该自己测出该数量,因为它根据您所运行的查询性质而变化),则我们得到可支持的进程数量。
对我们的 Web 服务器进行配置,以便使其永远不能创建超过您的进程设置允许数量的连接。其实现方法是将每个 Web 服务器的 MaxChildren 可调参数设置得足够低,使得所有 Web 服务器总共拥有的子进程数量低于可支持的 Oracle 连接数。这意味着每个 Apache 实例不再支持 256 个子进程。
重新设计我们的应用程序,使其不会遗漏额外的子进程。我们在后文中还会讨论这个话题。
这样有多重要?作为一个老板,我们即使启用了持续连接,也一直遇到栓锁问题。任何遇到过严重栓锁争用的人都能证实,这是一个极为严重的问题,服务器在很大程度上不响应,因为它花费过多时间来进行锁定操作。我们在调查中发现,尽管在终止前有大量进程为数以百计的请求提供服务,但很多进程只服务于单个请求。这一切是由于我们将 Apache 的 MaxSpareServers 设置得太低。我们所使用的负载均衡设备的一些问题导致了“突然爆发”的行为,此时 Web 服务器被多个同时发生的请求所冲击,然后闲置数秒时间。在 Apache 内部,这导致要创建额外的子进程,来为高请求等级提供服务;但当它一旦平息时(几乎立即平息),就会出现大部分目前处于闲置状态的子进程(终止进程并关闭其 Oracle 连接)。总体看来,这与运行非持续进程的效果相似。将 MaxSpareServers 设得较高就可消除此问题,并消除了栓锁争用。
执行 SQL
任何 Oracle 客户服务器关系的主要内容是执行查询。这里没有篇幅来谈论对查询的调整 ― 那是需要整本书来讨论的主题。相反,我们将集中讨论如何尽可能地使已经调整过的查询高效运行。
使用 PHP 编写良好的 Oracle 应用程序代码的第一步是始终使用绑定 SQL。当我们编写类似以下的查询时:
SELECT * FROM USERS WHERE USERNAME = 'george'
Oracle 必须对该查询进行软分析,查看以前是否曾编译过该查询。在默认情况下,"george" 值被作为文字项,这意味着如果我们使用不同的名字 ('bob') 执行此查询,则 Oracle 将把它看作完全不同的查询。Oracle 在其共享池中保留它执行的每个查询的分析副本,因此如果您使用数千个名字来执行此查询,则在您的共享池中将有数千个该查询的不同副本。即使在轻度活跃的网站上,这也会导致严重的内存碎片以及 ORA-4031 错误增殖。
对这个问题的解决方案是使用绑定 SQL。绑定 SQL 允许我们将 WHERE 子句中的文字值替换为占位符,如下所示:
SELECT * FROM USERS WHERE USERNAME = ':NAME'
在这里,查询只须被完全分析一次(硬分析);以后所有的分析都是所谓的软分析,此时引擎只是简单地从 SGA 中提取已编译的查询。此外,将只有一个单次分析的副本被存储,从而显著减少了不断执行的查询对内存的需求。
现在我们可以执行如下:
<?php
$db = OCIPLogin('scott', 'tiger', 'testdb');
$stmt = OCIParse("SELECT * FROM USERS WHERE USERNAME = ':NAME'");
OCIBindByName($stmt, ":NAME", "george");
OCIExecute($stmt);
?
在 Oracle8i 之前,我们必须手动绑定查询;从 8i 开始,通过设置 init.ora 的参数 "cursor_sharing = FORCE",我们可以指示优化器为我们完成这一工作。该设置通知优化器查找可以绑定的文字值,并手动执行绑定。自 9i 起,我们可以使用设置“cursor_sharing = SIMILAR”,该设置指示优化器深入查看基表的统计信息,了解自动绑定文字是否有好处(如果某个字段的分布极不均匀,则可能没有好处)。尽管应该启用这些设置(在 8i 中为 FORCE,在 9i 及更高版本中为 SIMILAR),但深入到查询中分析潜在绑定的这种操作对于优化器而言成本很高,因此在可能的情况下,应该手动绑定您的查询。
Oracle 客户服务器在 SQLNet 协议基础上运行,众所周知,这种协议的对话很多。例如,如果您执行一个返回 100 行的查询,则会有分析查询的对话交换、对每个绑定变量的交换、对执行的查询以及对每个提取行的查询。其中每次交换都包括客户端与服务器之间的网络数据包交换(称为一次往返)。减少往返次数可能具有深刻的性能影响。