首页  >  文章  >  后端开发  >  优化 PHP_PHP 的指南

优化 PHP_PHP 的指南

WBOY
WBOY原创
2016-06-01 12:40:521373浏览

来源:http://phplens.com/ 作者:John Lim

PHP 是一种非常快的编程语言,但优化 PHP 的不仅仅是代码执行速度。

在此本章中,我们解释了为什么优化 PHP 涉及许多与代码无关的因素,以及为什么调整 PHP 需要了解 PHP 相对于服务器上所有其他子系统的执行方式,然后识别由这些子系统引起的瓶颈并修复它们。我们还介绍了如何调整和优化 PHP 脚本,使其运行得更快。



实现高性能

当我们谈论良好的性能时,我们并不是在谈论良好的性能PHP 脚本的运行速度有多快。性能是可扩展性和速度之间的一系列权衡。调整为使用较少资源的脚本可能比执行缓存的脚本慢,但同一脚本的更多副本可以在 Web 服务器上同时运行。

在下面的示例中,A.php 是一位可以跑得很快的短跑运动员,B.php 是一位马拉松运动员,可以以几乎相同的速度永远慢跑。对于轻负载,A.php 的速度要快得多,但随着网络流量的增加,B.php 的性能只会下降一点点,而 A.php 就会失去动力。



让我们举一个更现实的例子来进一步澄清问题。假设我们需要编写一个 PHP 脚本来读取一个 250K 文件并生成该文件的 HTML 摘要。我们编写了两个执行相同操作的脚本:hare.php 一次性将整个文件读入内存并一次性处理它,而 tortoise.php 读取文件,一次一行,永远不会保留超过最长的行记忆中。由于发出多次读取,Tortoise.php 会变慢,需要更多的系统调用。

Hare.php 需要 0.04 秒的 CPU 和 10 Mb RAM,而 tortoise.php 需要 0.06 秒的 CPU 和 5 Mb RAM。服务器有 100 Mb 的可用实际 RAM,其 CPU 空闲率为 99%。假设没有发生内存碎片以简化事情。

运行 10 个并发脚本时,hare.php 将耗尽内存 (10 x 10 = 100)。此时,tortoise.php 仍将有 50 Mb 的可用内存。第 11 个并发脚本运行将使 hare.php 崩溃,因为它开始使用虚拟内存,使其速度减慢到原始速度的一半;每次调用 hare.php 现在需要 0.08 秒的 CPU 时间。同时,tortoise.php 仍将以其正常的 0.06 秒 CPU 时间运行。

在下表中,针对不同负载的更快的 php 脚本以粗体显示:




如上面的示例所示,获得良好的性能不仅仅是编写快速 PHP 脚本。高性能 PHP 需要对底层硬件、操作系统以及支持软件(如 Web 服务器和数据库)有很好的了解。

瓶颈

兔子和乌龟的例子向我们展示了瓶颈导致的原因放缓。有了无限的 RAM,hare.php 总是比 tortoise.php 更快。不幸的是,上述模型有点简单化,除了 RAM 之外,还有许多其他性能瓶颈:

(a) 网络

您的网络可能是最大的瓶颈。假设您有一个 10 Mbit 的互联网链路,每秒可以传输 1 MB 的数据。如果每个网页为 30k,那么每秒仅 33 个网页就会使线路饱和。

更微妙的网络瓶颈包括频繁访问 DNS 等慢速网络服务,或为网络软件分配足够的内存。

(b) CPU

如果您监控 CPU负载,通过网络发送纯 HTML 页面根本不会加重 CPU 负担,因为正如我们之前提到的,瓶颈将是网络。然而,对于 PHP 生成的复杂动态网页,CPU 速度通常会成为限制因素。拥有具有多个处理器的服务器或拥有服务器场可以缓解这种情况。

(c) 共享内存

共享内存用于进程间通信,并存储在进程之间共享的资源多个进程,例如缓存数据和代码。如果分配的共享内存不足,任何尝试访问使用共享内存的资源(例如数据库连接或可执行代码)的性能都会很差。

(d) 文件系统

访问硬盘可以是 50比从 RAM 读取数据慢 100 倍。使用 RAM 的文件缓存可以缓解这种情况。然而,内存不足的情况会减少文件系统缓存的可用内存量,从而减慢速度。文件系统也可能变得严重碎片化,从而减慢磁盘访问速度。在 Unix 系统上大量使用符号链接也会减慢磁盘访问速度。

默认 Linux 安装也因设置硬盘默认设置而臭名昭著,这些默认设置是为了兼容性而不是速度而调整的。使用命令 hdparm 调整 Linux 硬盘设置。

(e) 进程管理

在某些操作系统(例如 Windows)上,创建新进程是一个缓慢的操作。这意味着每次调用时都派生一个新进程的 CGI 应用程序在这些操作系统上的运行速度会显着变慢。在多线程模式下运行 PHP 应该可以缩短响应时间(注意:旧版本的 PHP 在多线程模式下不稳定)。

避免过多不需要的进程使您的 Web 服务器过度拥挤。例如,如果您的服务器纯粹用于 Web 服务,请避免在计算机上运行(甚至安装)X-Windows。在 Windows 上,避免运行 Microsoft Find Fast(Office 的一部分)和 3 维屏幕保护程序,这会导致 CPU 利用率达到 100%。

您可以考虑删除的一些程序包括未使用的网络协议、邮件服务器、防病毒扫描仪、鼠标硬件驱动程序、红外端口等。在 Unix 上,我假设您正在使用 SSH 访问服务器。那么你可以考虑删除:

连接数
满足 1 个 HTTP 请求所需的 CPU 秒
满足 10 个 HTTP 请求所需的 CPU 秒
满足 11 个 HTTP 请求所需的 CPU 秒

hare.php
0.04
0.40
0.88
(内存不足)

tortoise.php
0.06
0.60
0.66


守护进程,例如 telnetd、inetd、atd、ftpd、lpd、sambad
用于传入邮件的 sendmail
NFS 的端口映射
xfs、fvwm、xinit、X

您还可以通过修改启动文件来禁用启动时的各种程序,这些启动文件通常存储在 /etc/init* 或 /etc/rc*/init* 目录中。

还要检查您的 cron 作业,看看是否可以删除它们或在非高峰时段重新安排它们。

(f) 连接到其他服务器

如果您的 Web 服务器需要在其他服务器上运行服务,这些服务器有可能成为瓶颈。最常见的示例是缓慢的数据库服务器,它正在为来自多个 Web 服务器的过多复杂 SQL 请求提供服务。




何时开始优化?

有人说最好推迟到编码完成后再进行调整。仅当您的编程团队的编码一开始就具有高质量,并且您已经对应用程序的性能参数有了很好的了解时,此建议才有意义。否则,您将面临在测试后必须重写大部分代码的风险。

我的建议是,在设计软件应用程序之前,您应该对硬件和软件进行一些基本基准测试,以获得感受您可能达到的最大性能。然后,在设计和编码应用程序时,请牢记所需的性能参数,因为在每一步中都会在性能、可用​​性、安全性和灵活性之间进行权衡。

还要选择良好的测试数据。如果您的数据库预计可容纳 100,000 条记录,请避免仅使用 100 条记录的数据库进行测试 – 您会后悔的。我公司的一位程序员曾经遇到过这种情况;我们直到很久以后才检测到缓慢的代码,导致浪费了大量时间,因为我们必须重写许多可以工作但无法扩展的代码。


为 PHP 调整您的 Web 服务器

我们将介绍如何为当今使用的两种最常见的 Web 服务器(Apache 1.3 和 IIS)获得最佳 PHP 性能。这里的很多建议也与服务 HTML 相关。

PHP 的作者表示,与 Apache 1.3 和 PHP 相比,使用 Apache 2.0 没有性能和可扩展性优势,尤其是在多线程模式下。在预分叉模式下运行 Apache 2.0 时,以下讨论仍然相关(2003 年 10 月 21 日)。

(a) Apache 1.3/2.0

Apache 在 Unix 和 Windows 上都可用。它是世界上最流行的网络服务器。 Apache 1.3 使用预分叉模型进行 Web 服务。当 Apache 启动时,它会创建多个处理 HTTP 请求的子进程。初始父进程就像一个守护天使,确保所有子进程正常工作并协调一切。随着越来越多的 HTTP 请求进来,会产生更多的子进程来处理它们。当 HTTP 请求减慢时,父进程将终止空闲子进程,为其他进程释放资源。这个方案的优点在于它使 Apache 变得非常健壮。即使子进程崩溃,父进程和其他子进程也不会受到崩溃子进程的影响。

预分叉模型并不像其他一些可能的设计那么快,但对我来说,它“非常快”在服务 PHP 脚本的服务器上“无事生非”,因为早在 Apache 性能问题变得严重之前,其他瓶颈就会出现。 Apache的健壮性和可靠性更为重要。

Apache 2.0 提供多线程模式下的操作。我的基准测试表明这种模式几乎没有性能优势。另请注意,许多 PHP 扩展不兼容(例如 GD 和 IMAP)。使用 Apache 2.0.47 进行测试(2003 年 10 月 21 日)。

Apache 是使用 httpd.conf 文件配置的。以下参数在配置子进程时特别重要:


对于大型站点,接近以下值可能会更好:

指令
默认
说明

MaxClients
256
要创建的子进程的最大数量。默认值意味着最多可以同时处理 256 个 HTTP 请求。任何进一步的连接请求都会排队。

StartServers
5
启动时创建的子进程数。

MinSpareServers
5
应创建的空闲子进程数。如果空闲子进程的数量低于此数量,则最初创建 1 个子进程,然后再过一秒创建 2 个子进程,然后再过一秒创建 4 个子进程,依此类推,直到每秒创建 32 个子进程。

MaxSpareServers
10
如果超过此数量的子进程处于活动状态,那么这些额外的进程将被终止。

MaxRequestsPerChild
0
设置子进程在终止之前可以处理的 HTTP 请求数。设置为 0 表示永不终止。如果您怀疑发生内存泄漏,或者为了释放未充分利用的资源,请将其设置为 100 到 10000 之间的值。


MinSpareServers 32

MaxSpareServers 64

Windows 上的 Apache 表现不同。 Apache 不使用子进程,而是使用线程。上述参数均未使用。相反,我们有一个参数:ThreadsPerChild,默认为 50。该参数设置 Apache 可以生成的线程数。由于Windows版本只有1个子进程,默认设置50意味着只能处理50个并发HTTP请求。对于流量较高的 Web 服务器,请将此值增加到 256 到 1024 之间。

您可以更改的其他有用的性能参数包括:


如果您不需要 DNS 查找,并且您不使用 htaccess 文件为单个目录配置 Apache 设置,您可以设置:

指令
默认
描述

SendBufferSize
设置为操作系统默认值
确定TCP/IP 连接中使用的输出缓冲区的大小(以字节为单位)。当数据包需要缓冲时,这主要适用于拥塞或缓慢的网络;然后将此参数设置为接近正常下载的最大文件的大小。每个客户端连接都会创建一个 TCP/IP 缓冲区。

KeepAlive [on|off]
On
在最初的 HTTP 规范中,每个 HTTP 请求都必须与服务器建立单独的连接。为了减少频繁连接的开销,开发了 keep-alive 标头。 Keep-alives 告诉服务器为多个 HTTP 请求重用同一个套接字连接。

如果单独的专用 Web 服务器提供所有图像,您可以禁用此选项。该技术可以显着提高资源利用率。

KeepAliveTimeout
15
保持套接字连接处于活动状态的秒数。该时间包括服务器生成内容和客户端确认。如果客户端没有及时响应,则必须建立新连接。

该值应保持较低,否则套接字将长时间处于空闲状态。

MaxKeepAliveRequests
100
当达到 MaxKeepAliveRequests 设置的请求数时,Socket 连接将被终止。将此值保持为低于 MaxClients 或 ThreadsPerChild 的较高值。

TimeOut
300
当空闲时间超过此值时断开连接。如果您的客户端延迟较低,您可以将此值设置得较低。

LimitRequestBody
0
PUT 或 POST 的最大大小。 O表示没有限制。


# 禁用 DNS 查找:PHP 脚本仅获取 IP 地址

关闭 HostnameLookups

# 禁用 htaccess 检查



AllowOverride none



如果您在访问符号链接时不担心目录安全,请打开 FollowSymLinks 并关闭 SymLinksIfOwnerMatch 以防止额外的 lstat() 系统调用正在制作:

Options FollowSymLinks

#Options SymLinksIfOwnerMatch

(b) IIS 调整

IIS 是 Windows NT 上提供的多线程 Web 服务器和 2000。从 Internet 服务管理器中,可以调整以下参数:


您还可以配置网站的默认隔离级别。在应用程序保护下的主目录选项卡中,您可以定义隔离级别。高度隔离的网站运行速度会较慢,因为它作为独立于 IIS 的进程运行,而在 IIS 进程中运行网站速度最快,但如果网站代码中存在严重错误,则会导致服务器瘫痪。目前,我建议使用 CGI 运行 PHP 网站,或使用 ISAPI 将应用程序保护设置为高。

您还可以使用 regedit.exe 修改存储在以下位置的以下 IIS 5 注册表设置:

根据每天的点击次数进行性能调整。
确定为 IIS 预分配多少内存。 (性能选项卡)。

带宽限制
控制每个网站每秒分配的带宽。 (“性能”选项卡)。

进程限制
控制每个网站可用的 CPU%。 (性能选项卡)。

超时
默认值为 900 秒。在局域网上设置为较低的值。 (网站选项卡)

HTTP 压缩
在 IIS 5 中,您可以压缩动态页面、html 和图像。可以配置为缓存压缩的静态 html 和图像。默认情况下压缩是关闭的。

必须为整个物理服务器启用 HTTP 压缩。要打开它,请打开 IIS 控制台,右键单击服务器(不是任何子站点,而是左侧窗格中的服务器),然后获取“属性”。单击“服务”选项卡,选择“压缩应用程序文件”以压缩动态内容,选择“压缩静态文件”以压缩静态内容。


HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesInetinfoParameters


如果此注册表位置缺少设置,正在使用默认设置。

Windows 上的高性能:IIS 和 FastCGI

经过大量测试,我发现 Windows 上最佳的 PHP 性能由将 IIS 与 FastCGI 结合使用。 CGI 是一种从 Web 服务器调用外部程序的协议。它不是很快,因为 CGI 程序在每次页面请求后都会终止。 FastCGI 修改了此协议以获得高性能,方法是使 CGI 程序在页面请求后持续存在,并在新的页面请求到来时重用相同的 CGI 程序。

由于使用 IIS 安装 FastCGI 很复杂,您应使用 EasyWindows PHP 安装程序。这将安装 PHP、FastCGI 和 Turck MMCache 以获得最佳性能。此安装程序还可以安装适用于 Apache 1.3/2.0 的 PHP。

有关 FastCGI 的部分于 2003 年 10 月 21 日添加。

PHP4 的 Zend 引擎

Zend 引擎是 PHP4 使用的内部编译器和运行时引擎。 Zend Engine 由 Zeev Suraski 和 Andi Gutmans 开发,是他们名字的缩写。在 PHP4 的早期,它以以下方式工作:

MemCacheSize
设置 IIS 将用于其文件缓存的内存量。默认情况下,IIS 将使用 50% 的可用内存。如果 IIS 是服务器上的唯一应用程序,则增加。值以兆字节为单位。

MaxCachedFileSize
确定文件缓存中缓存的文件的最大大小(以字节为单位)。默认值为 262,144 (256K)。

ObjectCacheTTL
设置缓存中的对象在内存中保留的时间长度(以毫秒为单位)。默认值为 30,000 毫秒(30 秒)。

MaxPoolThreads
设置每个处理器创建的池线程数。确定可以同时运行多少个 CGI 应用程序。默认值为 4。如果您在 CGI 模式下使用 PHP,请增加此值。

ListenBackLog
指定 IIS 在连接队列中维护的活动保持活动连接的最大数量。默认值为 15,应增加到您想要支持的并发连接数。最大值为 250。




PHP 脚本由 Zend 引擎加载并编译为 Zend 操作码。操作码是操作码的缩写,是低级二进制指令。然后执行操作码并将生成的 HTML 发送到客户端。操作码在执行后从内存中清除。

如今,有多种产品和技术可以帮助您加快此过程。在下图中,我们展示了现代 PHP 脚本的工作原理;所有阴影框都是可选的。



PHP 脚本加载到内存中并编译成 Zend 操作码。现在可以使用称为 Zend Optimizer 的可选窥孔优化器来优化这些操作码。根据脚本的不同,它可以将 PHP 代码的速度提高 0-50%。

以前执行后,操作码被丢弃。现在,可以选择使用多种替代开源产品和 Zend Accelerator(以前称为 Zend Cache)(一种商业闭源产品)将操作码缓存在内存中。唯一与 Zend Optimizer 兼容的操作码缓存是 Zend Accelerator。操作码缓存通过删除脚本加载和编译步骤来加速执行。使用操作码缓存,执行时间可以提高 10-200%。




高性能的秘密之一不是编写更快的 PHP 代码,而是避免执行 PHP 代码通过将生成的 HTML 缓存在文件或共享内存中。 PHP 脚本仅运行一次并捕获 HTML,以后调用该脚本将加载缓存的 HTML。如果需要定期更新数据,则为缓存的 HTML 设置过期值。 HTML 缓存不是 PHP 语言或 Zend Engine 的一部分,而是使用 PHP 代码实现的。有许多类库可以执行此操作。其中之一是 PEAR 缓存,我们将在下一节中介绍它。另一个是Smarty模板库。

最后,发送到Web客户端的HTML可以被压缩。这是通过将以下代码放在 PHP 脚本的开头来启用的:

在哪里可以找到操作码缓存

Zend Accelerator:由 Zend Engine 团队开发的商业操作码缓存。非常可靠和坚固。请访问 http://zend.com 了解更多信息。

在生产服务器上使用以下开源操作码缓存之前,您需要测试它们,因为它们的性能和可靠性很大程度上取决于您运行的 PHP 脚本。

Turcke MMCache:http://turck-mmcache.sourceforge.net/
(2003 年 10 月 21 日添加 - 强烈推荐 - 我自己使用这个)

替代 PHP 缓存:http: //apc.communityconnect.com/

PHP 加速器:http://www.php-accelerator.co.uk/

AfterBurner 缓存:http://www.bwcache.bware。它/



ob_start("ob_gzhandler");

:
:

?>

如果您的 HTML 具有高度可压缩性,则可以将 HTML 文件的大小减少 50-80%,从而减少网络带宽要求和延迟。缺点是您需要有一些 CPU 能力来进行压缩。

使用 PEAR 缓存进行 HTML 缓存

PEAR 缓存是一组缓存类,允许您缓存多种类型的数据,包括 HTML 和图像。

PEAR 缓存最常见的用途是缓存 HTML 文本。为此,我们使用输出缓冲类来缓存 start() 和 end() 函数之间打印或回显的所有文本:

require_once("Cache/Output.php");

$cache = new Cache_Output("file", array("cache_dir" => "cache/") );

if ($contents = $cache->start(md5("这是一个唯一的键) !"))) {

#
# 啊哈,已返回缓存数据
#

print $contents;
print "

缓存命中

" ;

} else {

#
# 无缓存数据,或缓存已过期
#

print "

出门前不要带它...

”; # 放入​​缓存
print "

站立并交付

"; # 放入缓存
print $cache->end(10);

}

Cache 构造函数将要使用的存储驱动程序作为第一个参数。文件、数据库和共享内存存储驱动程序可用;请参阅 Ulf Wendel 的基准测试,第二个参数是存储驱动程序选项,即“cache_dir”。缓存目录的位置,以及“filename_prefix”,这是用于所有缓存文件的前缀。奇怪的是,选项参数中没有设置缓存到期时间。

要缓存一些数据,您需要生成一个使用键的缓存数据的唯一 ID 在上面的示例中,我们使用了 md5(“这是一个唯一的键!”)。

start() 函数使用键来查找缓存的副本。如果内容没有被缓存,则 start() 返回一个空字符串,并且所有以后的 echo() 和 print() 语句将被缓冲在输出缓存中,直到调用 end() 为止。

end() 函数返回缓冲区的内容,并结束输出缓冲。 end() 函数将缓存的到期时间作为其第一个参数。此参数可以是缓存数据的秒数,也可以是给出数据过期日期和时间的 Unix 整数时间戳,也可以是零(默认为 24​​ 小时)。

使用 PEAR 缓存的另一种方法是存储变量或其他数据。为此,您可以使用 Cache 基类:


require_once("Cache.php");

$cache = new Cache("file ", array("cache_dir" => "cache/") );
$id = $cache->generateID("这是一个唯一的键");

if ($data = $cache ->get($id)) {

print "缓存命中。
数据: $data";

} else {

$data = "质量慈悲不紧张...";
$cache->save($id, $data, $expires = 60);
print "缓存未命中。
";

}

?>

为了保存数据,我们使用 save()。如果您的唯一密钥已经是合法的文件名,则可以绕过generateID()步骤。可以保存对象和数组,因为 save() 将为您序列化数据。最后一个参数控制数据何时过期;这可以是缓存数据的秒数,也可以是给出数据过期日期和时间的 Unix 整数时间戳,也可以是零以使用默认值 24 小时。要检索缓存数据,我们使用 get()。

您可以使用 $cache->delete($id) 删除缓存数据项,并使用 $cache->flush() 删除所有缓存项。

新功能:更快的缓存类是 Cache-Lite。强烈推荐。

使用基准

在前面的部分中,我们已经讨论了许多性能问题。现在我们来讨论重点,如何对代码进行测量和基准测试,以便您可以获得有关调整内容的体面信息。

如果您想在 Web 服务器上执行实际基准测试,您将需要一个工具来向服务器发送 HTTP 请求。在 Unix 上,执行基准测试的常用工具包括 ab(apachebench 的缩写),它是 Apache 版本的一部分,以及较新的洪水 (httpd.apache.org/test/flood)。在 Windows NT/2000 上,您可以使用 Microsoft 的免费 Web 应用程序压力工具 (webtool.rte.microsoft.com)。

这些程序可以发出多个并发 HTTP 请求,模拟多个 Web 客户端,并向您提供详细的统计数据完成测试后。

您可以使用“vmstat 1”在 Unix 上进行基准测试时监控服务器的行为。这会每秒打印一份有关磁盘 I/O、虚拟内存和 CPU 负载性能的状态报告。或者,您可以使用“top d 1”,它每 1 秒为您提供按 CPU 负载排序的所有正在运行的进程的全屏更新。

在 Windows 2000 上,您可以使用性能监视器或任务管理器来查看系统统计信息。

如果您想测试代码的特定方面而不必担心 HTTP开销,您可以使用 microtime() 进行基准测试,它以字符串形式返回精确到微秒的当前时间。以下函数会将其转换为适合计算的数字。

function getmicrotime()
{
list($usec, $sec) =explode(" ",microtime());
return ((float)$usec (float)$sec);
}

$time = getmicrotime();

#
# 基准代码
#

echo "

已用时间: ",getmicrotime() - $time, "秒";

或者,您可以使用 APD 或 XDebug 等分析工具。另请参阅我的文章使用 xdebug 压缩代码。

基准测试案例研究

此案例研究详细介绍了我们为客户所做的真实基准测试。在本例中,客户希望所有不涉及运行长 SQL 查询的 PHP 页面的响应时间保证为 5 秒。使用以下服务器配置:在 Red Hat 7.2 Linux 上运行 PHP 4.0.6 的 Apache 1.3.20 服务器。硬件是双 Pentium III 933 MHz 处理器和 1 Gb RAM。 HTTP 请求将针对 PHP 脚本“testmysql.php”。该脚本从另一台服务器上运行的 MySQL 数据库读取并处理大约 20 条记录。为了简单起见,我们假设所有图形都是从另一个网络服务器下载的。

我们使用“ab”作为基准测试工具。我们将“ab”设置为使用 10 个并发连接 (-c10) 执行 1000 个请求 (-n1000)。以下是结果:

# ab -n1000 -c10 http://192.168.0.99/php/testmysql.php
这是 ApacheBench,版本 1.3
版权所有 (c) 1996 Adam Twiss, Zeus Technology Ltd,http://www.zeustech.net/
版权所有 (c) 1998-1999 Apache Group,http://www.apache.org/

服务器软件:Apache/1.3 .20
服务器主机名:192.168.0.99
服务器端口:80

文档路径:/php/testmysql.php
文档长度:25970 字节

并发级别: 10
测试时间:128.672 秒
完成请求:1000
失败请求:0
传输总量:26382000 字节
传输 HTML:25970000 字节
每秒请求数:7.77
接收传输速率:205.03 kb/s

连接时间(毫秒)
最小平均最大值
连接:0 9 114
处理:698 1274 2071
总计:698 1283 2185

运行基准测试时,我们在服务器端使用命令“top d 1”监控资源利用率。参数“d 1”表示更新之间延迟 1 秒。输出如下所示。

10:58pm up 3:36,2 个用户,平均负载:9.07, 3.29, 1.79
74 个进程:63 个正在睡眠,11 个正在运行,0 个僵尸,0 个已停止
CPU0 状态:92.0% 用户、7.0% 系统、0.0% 正常、0.0% 空闲
CPU1 状态:95.0% 用户、4.0% 系统、0.0% 正常、0.0% 空闲
内存:1028484K av、230324K 使用, 798160K 空闲, 64K 碎片, 27196K buff
交换: 2040244K av, 0K 使用, 2040244K 空闲 30360K 缓存

PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND
11 42 阿帕奇 20 0 7280 7280 3780 R 21.2 0.7 0:20 httpd
1154 阿帕奇 17 0 8044 8044 3788 S 19.3 0.7 0:20 httpd
1155 阿帕奇 20 0 8052 8052 3796 R 19.3 0.7 0:20 httpd
1141 阿帕奇15 0 6764 6764 3780 S 14.7 0.6 0:20 httpd
1174 阿帕奇 14 0 6848 6848 3788 S 12.9 0.6 0:20 httpd
1178 阿帕奇 13 0 6864 6864 3804 S 1 2.9 0.6 0:19 httpd
1157阿帕奇 15 0 7536 7536 3788 R 11.0 0.7 0:19 httpd
1159 阿帕奇 15 0 7540 7540 3788 R 11.0 0.7 0:19 httpd
1148 阿帕奇 11 0 6672 6672 3784 S 10.1 0.6 0:20 httpd
1158 阿帕奇 14 0 7400 7400 3788 R 10.1 0.7 0:19 httpd
1163 阿帕奇 20 0 7540 7540 3788 R 10.1 0.7 0:19 httpd
1169 阿帕奇 12 0 6856 6856 3796 S 10.1 0.6 0:20 httpd
1176 阿帕奇 16 0 8052 8052 3796 R 10.1 0.7 0:19 httpd
1171 阿帕奇 15 0 7984 7984 3780 S 9.2 0.7 0:18 httpd
1170 阿帕奇 16 0 7204 7204 3796 R 6.4 0.7 0:20 httpd
1168 阿帕奇 10 0 6856 6856 3796 S 4.6 0.6 0:20 httpd
1377 natsoft 11 0 1104 1104 856 R 2.7 0.1 0:02 顶部
1152 阿帕奇 9 0 6752 675 2 3788 S 1.8 0.6 0:20 httpd
1167 apache 9 0 6848 6848 3788 S 0.9 0.6 0:19 httpd
1 root 8 0 520 520 452 S 0.0 0.0 0:04 init
2 root 9 0 0 0 0 SW 0.0 0.0 0:00 keventd

查看“top”的输出,双 CPU Apache 服务器正在全力运行,空闲时间为 0%。更糟糕的是,过去一分钟的平均负载为 9.07(过去 5 分钟为 3.29,过去 15 分钟为 1.79)。平均负载是准备运行的进程的平均数量。对于双处理器服务器,任何高于 2.0 的负载都意味着系统过载。您可能会注意到,负载 (9.07) 和我们用 ab 定义的并发连接数 (10) 之间存在密切关系。

幸运的是,我们有足够的物理内存,大约有 798,160 Mb 的可用内存和没有使用虚拟内存。

再往下我们可以看到按 CPU 利用率排序的进程。最活跃的是 Apache httpd 进程。第一个 httpd 任务使用 7280K 内存,平均占用 21.2% 的 CPU 和 0.7% 的物理内存。 STAT 列指示状态:R 是可运行的,S 是睡眠的,W 表示进程已换出。

鉴于上面的数字,并假设这是一个典型的峰值负载,我们可以执行一些规划。如果双 CPU 服务器的平均负载为 9.0,并且假设每个任务完成所需的时间大致相同,则轻负载服务器的速度应为 9.0 / 2 个 CPU = 4.5 倍。因此,过去在峰值负载下需要 1.283 秒才能满足的 HTTP 请求将需要大约 1.283 / 4.5 = 0.285 秒才能完成。

为了验证这一点,我们使用 2 个并发客户端连接(而不是之前的基准测试中的 10 个)进行基准测试,得到平均 0.281 秒,非常接近 0.285 秒的预测!

# ab -n100 -c2 http://192.168.0.99/php/testmysql.php

[为简洁起见省略了一些行]

每秒请求数:7.10
接收传输速率:187.37 kb/s

连接时间(毫秒)
最小平均最大值
连接:0 2 40
处理:255 279 292
总计:255 281 332

相反,将连接数加倍,我们可以预测平均连接时间应加倍,从 1.283 秒增加到 2.566 秒。在基准测试中,实际时间为 2.570 秒。

40 个连接过载

当我们将基准推至使用 40 个连接时,服务器因 35% 的失败请求而过载。进一步调查,这是因为MySQL服务器持久连接由于“连接太多”而失败。

该基准测试还演示了 Apache 子进程的延迟行为。每个 PHP 脚本使用 2 个持久连接,因此在 40 个连接时,我们最多只能使用 80 个持久连接,远低于默认的 MySQL max_connections 100。但是,由于延迟,Apache 空闲子进程不会立即分配给新请求,请保持- 生命和其他技术原因;这些挥之不去的子进程持有剩余的 20 个持久连接,这些连接是“压垮骆驼的最后一根稻草”。

修复

通过切换到非持久数据库连接,我们能够修复这个问题得到了5.340秒的结果。另一种解决方案是增加 MySQL max_connections 参数的默认值 100。

结论

上述案例研究再次向我们表明,优化性能是极其复杂的。它需要了解多个软件子系统,包括网络路由、TCP/IP 堆栈、物理和虚拟内存量、CPU 数量、Apache 子进程的行为、PHP 脚本和数据库配置。

在这种情况下,PHP 代码调整得相当好,因此第一个瓶颈是 CPU,这导致响应时间变慢。随着负载的增加,系统以接近线性的方式减慢速度(这是一个好兆头),直到我们遇到更严重的 MySQL 客户端连接瓶颈。这导致我们的 PHP 页面出现多个错误,直到我们通过切换到非持久连接来修复它。

根据上图,我们可以计算出在给定的所需响应时间下,我们可以处理多少个并发 HTTP 连接。假设互联网上双向网络延迟为 0.5 秒(单向 0.25 秒),我们可以预测:



由于我们的客户端希望最大响应时间为 5 秒,服务器可以每秒最多可处理 34 个同时连接。计算得出的峰值容量为每秒 34/5 = 6.8 页面浏览量。

要获得服务器每天可以处理的最大页面浏览量,请将每秒峰值容量乘以 50,000(此技术是由大型网络托管公司pair.com 的网站管理员建议的),每天提供 340,000 次页面浏览量。

代码优化

仍在想知道为什么如此强调讨论非 PHP 问题的耐心读者,请注意 PHP 是一种快速语言,许多可能导致速度缓慢的瓶颈都位于 PHP 之外。

大多数 PHP 脚本都很简单。它们涉及读取一些会话信息、从内容管理系统或数据库加载一些数据、格式化适当的 HTML 并将结果回显给 HTTP 客户端。假设典型的 PHP 脚本在 0.1 秒内完成,并且 Internet 延迟为 0.2 秒,则 HTTP 客户端看到的 0.3 秒响应时间中只有 33% 是实际的 PHP 计算。因此,如果将脚本的速度提高 20%,HTTP 客户端的响应时间将下降到 0.28 秒,这是一个微不足道的改进。当然,服务器可能可以处理同一页面20%以上的请求,因此可扩展性得到了提高。

上面的例子并不意味着我们应该举手放弃。这意味着我们不应该为调整代码的最后 1% 的速度而感到自豪,但我们应该花时间优化代码中有价值的区域以获得更高的回报。

高回报代码优化

可以实现如此高回报的地方是我们代码中的 while 和 for 循环,其中代码中的每次减慢都会因我们迭代它们的次数而放大。了解可优化内容的最佳方法是使用几个示例:

示例 1

这是一个打印数组的简单示例:

for ($j =0; $j echo $arr[$j]."
";

将代码更改为:

for ($j=0, $max = sizeof($arr), $s = '; $j $s .= $arr[$j]。 "
";

echo $s;

首先我们需要了解表达式 $j
第二个问题是,在 PHP 4 中,多次回显比将所有内容存储在字符串中并在一次调用中回显要慢。这是因为 echo 是一项昂贵的操作,可能涉及将 TCP/IP 数据包发送到 HTTP 客户端。当然,在 $s 中累积字符串会存在一些可扩展性问题,因为它会占用更多内存,因此您可以看到这里涉及到权衡。

加速上述代码的另一种方法是使用输出缓冲。这将在内部累积输出字符串,并在脚本末尾一次性发送输出。这大大减少了网络开销,但代价是更多的内存和延迟的增加。在我的一些完全由 echo 语句组成的代码中,观察到性能提高了 15%。

ob_start();
for ($j=0, $max = sizeof($arr), $s = '; $j echo $arr[ $j]."
";

请注意,ob_start() 的输出缓冲可用作所有 PHP 脚本的全局优化。在长时间运行的脚本中,您还需要定期刷新输出缓冲区,以便将一些反馈发送到 HTTP 客户端。这可以通过 ob_end_flush() 来完成。此函数还会关闭输出缓冲,因此您可能需要在刷新后立即再次调用 ob_start()。

总之,这个示例向我们展示了如何优化循环不变量以及如何使用输出缓冲来加速

示例 2

在下面的代码中,我们迭代 PEAR DB 记录集,使用特殊的格式化函数来格式化行,然后回显结果。这次,我对执行时间进行了基准测试,为 10.2 毫秒(不包括数据库连接和 SQL 执行时间):

function FormatRow(&$recordSet)
{
$arr = $recordSet-> fetchRow();
return ''.$arr[0].''.$arr[1].'';
}

for ($j = 0; $j numRows(); $j ) {
print FormatRow($rs);
}

从示例 1 中,我们了解到我们可以通过将代码更改为以下代码来优化代码(执行时间:8.7 ms):

function FormatRow(&$recordSet)
{
$arr = $recordSet->fetchRow();
return ''.$arr[0].''.$arr[1].'';
}

ob_start();

for ($j = 0, $max = $rs->numRows(); $j print FormatRow($rs);
}

我的基准测试显示,使用 $max 贡献了 0.5 毫秒,而 ob_start 贡献了 1 毫秒,从而实现了 1.5 毫秒的加速。

但是,通过更改循环算法,我们可以简化并加速代码。在这种情况下,执行时间减少到 8.5 毫秒:

function FormatRow($arr)
{
return ''.$arr[0].''.$arr[1].';
}

ob_start();

while ($arr = $rs->fetchRow()) {
print FormatRow($arr);
}

这里可以进行最后一个优化。我们可以消除函数调用的开销(可能会牺牲速度的可维护性),从而再减少 0.1 毫秒(执行时间:8.4 毫秒):

ob_start();

while ($arr = $rs->fetchRow()) {
print ''.$arr[0].''.$arr[1].'';
}

通过切换到 PEAR 缓存,缓存数据的执行时间再次降至 3.5 毫秒:

require_once("Cache/Output.php");

ob_start();

$cache = new Cache_Output("file", array("cache_dir" => "cache/") );

$t = getmicrotime();

if ($contents = $cache->start(md5("这是一个独特的 kexy!"))) {
print "

缓存命中

";
print $contents;
} else {
print "

缓存未命中

";

##
## 连接和查询数据库的代码省略
##

while ($arr = $rs->fetchRow()) {
print ''.$arr[0].''.$arr[1].'';
}

print $cache->end (100);
}

print (getmicrotime()-$t);

我们总结了以下优化方法:


从上图可以看出,速度提升最大的是不是通过调整代码获得的,而是通过简单的全局优化(例如 ob_start())或使用完全不同的算法(例如 HTML 缓存)获得的。




调整摘要

执行时间(毫秒)
优化方法

9.9
初始代码,无优化,不包括数据库连接和SQL执行时间。

9.2
使用ob_start

8.7
优化循环不变量 ($max) 并使用 ob_start

8.5
从 for 循环更改为 while 循环,并将数组传递给 FormatRow() 并使用 ob_start

8.4
删除 FormatRow() 并使用 ob_start

3.5
使用 PEAR 缓存并使用 ob_start

优化面向对象编程

中2001 年 3 月,我对 PHP 4.0.4pl1 上的类进行了一些非正式的基准测试,并从结果中得出了一些建议。三个要点是:

1.使用前初始化所有变量。

2。如果您打算访问该值两次以上,请取消引用方法中经常使用的所有全局/属性变量,并将这些值放入局部变量中。

3.尝试将常用的方法放在派生类中。

警告:由于 PHP 正在经历一个持续改进的过程,未来情况可能会发生变化。

更多详细信息

我发现调用对象方法(在一个类)大约是普通函数调用的两倍。对我来说,这是完全可以接受的,并且可以与其他 OOP 语言相媲美。

在方法内部(以下比率仅为近似值):

在方法中递增局部变量是最快的。与在函数中调用局部变量几乎相同。
增加全局变量比局部变量慢 2 倍。
增加对象属性(例如 $this->prop )比局部变量慢 3 倍。
递增未定义的局部变量比预初始化的变量慢 9-10 倍。
仅声明全局变量而不在函数中使用它也会减慢速度(与增加局部变量的速度大致相同)。 PHP 可能会检查全局是否存在。
方法调用似乎与类中定义的方法数量无关,因为我向测试类添加了 10 个以上的方法(在测试方法之前和之后),而性能没有变化。
派生类中的方法比基类中定义的方法运行得更快。
具有一个参数和一个空函数体的函数调用大约需要与执行 7-8 个 $localvar 操作相同的时间。类似的方法调用当然是大约 15 个 $localvar 操作。
更新:2004 年 7 月 11 日:上述测试是在 PHP 4.0.4 上进行的,大约 3 年前。我在 PHP4.3.3 中再次测试了这一点,现在调用一个函数大约需要 20 个 $localvar 操作,调用一个方法大约需要 30 个 $localvar 操作。这可能是因为 $localvar 现在运行得更快,或者函数更慢。



您对所使用的软件(Apache、PHP、IIS、数据库)了解得越多,对操作系统、网络和服务器硬件的了解越深入,您就能做得越好对您的代码和系统执行全局优化。

对于 PHP 脚本,最昂贵的瓶颈通常是 CPU。双 CPU 可能比 2 GB RAM 更有用。

使用“configure --enable-inline-optimization”选项编译 PHP,以生成尽可能快的 PHP 可执行文件。

调整数据库并为 SQL WHERE 条件中常用的字段建立索引。 ADOdb 是非常流行的数据库抽象库,它提供了 SQL 调优模式,您可以在其中查看无效的、昂贵的和可疑的 SQL、它们的执行计划以及在哪个 PHP 脚本中执行 SQL。

如果您的数据很少更改,请使用 HTML 缓存。即使数据每分钟都在变化,只要数据与缓存同步,缓存也可以提供帮助。根据您的代码复杂性,它可以将您的性能提高 10 倍。

尽早对最复杂的代码(或至少是原型)进行基准测试,以便您在预期性能太之前就了解它迟到修复。尝试使用实际数量的测试数据以确保其适当扩展。
2004 年 7 月 11 日更新:要使用所有函数调用的执行配置文件进行基准测试,您可以尝试 xdebug 扩展。有关如何使用 xdebug 的简短教程,请参阅使用 xdebug 压缩代码。也有商业产品可以做到这一点,例如。禅德工作室。


考虑使用操作码缓存。这可提供 10-200% 的加速,具体取决于代码的复杂性。确保在安装缓存之前进行一些压力测试,因为有些测试比其他测试更可靠。

在代码开头使用 ob_start() 。这可以让您在 Apache 上免费提升 5-15% 的速度。您还可以使用 gzip 压缩来实现超快速下载(这需要空闲的 CPU 周期)。

考虑安装 Zend Optimizer。这是免费的,并且做了一些优化,但请注意,安装 Zend Optimizer 后,某些脚本实际上会变慢。共识是,当您的代码有大量循环时,Zend Optimizer 是很好的选择。如今,许多操作码加速器都具有类似的功能(2003 年 10 月 21 日添加了这句话)。

首先优化你的循环。将循环不变量(常量)移到循环之外。


尽可能使用数组和字符串函数。它们比用 PHP 编写等效代码更快。

将多个小字符串连接成一个大字符串的最快方法是创建一个输出缓冲区(ob_start)并回显到缓冲区中。最后使用 ob_get_contents 获取内容。这是可行的,因为内存分配通常是字符串连接中的杀手,而输出缓冲会分配一个 40K 的大初始缓冲区,并以 10K 块的形式增长。 2004 年 6 月 22 日添加。

使用函数中的引用传递对象和数组。也尽可能返回对象和数组作为引用。如果这是一个简短的脚本,并且代码维护不是问题,您可以考虑使用全局变量来保存对象或数组。

如果您有许多使用会话变量的 PHP 脚本,请考虑使用会话共享内存模块重新编译 PHP,或使用 RAM 磁盘。使用“configure -–with-mm”启用此功能,然后重新编译 PHP,并在 php.ini 中设置 session.save_handler=mm。

对于搜索子字符串,最快的代码是使用 strpos(),其次是 preg_match(),最后是 ereg()。同样,str_replace() 比 preg_replace() 更快,而 preg_replace() 又比 ereg_replace() 更快。

于 2004 年 7 月 11 日添加:对大型 switch 语句进行排序,最常出现的情况位于顶部。如果一些最常见的情况位于默认部分,请考虑在 switch 语句的顶部显式定义这些情况。

对于处理 XML,使用正则表达式进行解析比使用 DOM 或 SAX 快得多。

Unset() 不再使用的变量以减少内存使用。这对于资源和大型数组非常有用。


对于具有深层层次结构的类,派生类(子类)中定义的函数比基类(父类)中定义的函数调用得更快。考虑将基类中最常用的代码也复制到派生类中。

如果




作者
strayly
(海外文摘),请考虑将代码编写为PHP扩展或Java类或COM对象
::
评论
(0)



声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn