在高负载网站中使用PHP的策略

253

在你回答之前,我从未开发过任何流行到足以达到高服务器负载的东西。把我当作(叹气)刚降落在这个星球上的外星人,尽管我知道PHP和一些优化技巧。


我正在开发一个PHP工具,如果能够正确运行,它可以吸引相当多的用户。然而,虽然我完全有能力开发程序,但当涉及到处理大量流量时,我几乎一无所知。因此,以下是一些关于此的问题(也可以将这个问题转化为资源线程)。

数据库

目前,我计划在PHP5中使用MySQLi功能。然而,我应该如何设置与用户和内容相关的数据库?我真的需要多个数据库吗?目前,所有东西都混杂在一个数据库中 - 尽管我一直在考虑将用户数据分散到一个数据库中,实际内容分散到另一个数据库中,最后将核心站点内容(模板主控等)分散到另一个数据库中。我的理由是向不同的数据库发送查询将减轻它们的负载,因为一个数据库= 3个负载源。如果它们都在同一台服务器上,这仍然有效吗?

缓存

我有一个模板系统,用于构建页面并交换变量。母版模板存储在数据库中,每次调用模板时都会调用其缓存副本(html文档)。目前,这些模板中有两种类型的变量——静态变量和动态变量。静态变量通常是页面名称、站点名称之类不经常更改的内容;动态变量是每次页面加载时都会更改的内容。

我的问题是:

假设我在不同的文章上有评论。哪种解决方案更好:存储简单的评论模板,并在每次加载页面时呈现评论(从DB调用),或者将评论页面的缓存副本存储为html页面—每次添加/编辑/删除评论时重新缓存页面。

最后

有没有人有关于在PHP上运行高负载网站的技巧/指针。我相信使用它是可行的语言-Facebook和Yahoo!都给予了很大的优先权-但是我应该注意什么经验教训?


12
3.5年过去了,我甚至都不记得当时在做什么了。我想知道当时我认为那件事有多酷 :) - Ross
9
这可以成为一个教训,提醒你不要过早地进行优化 :) - Rimu Atkinson
24个回答

94

没有两个网站是完全一样的。你真的需要使用像jmeter这样的工具来进行基准测试,以查看哪些地方存在问题。你可以花费很多时间猜测和改进,但直到你对变化进行度量和比较,才会看到真正的结果。

例如,多年来,MySQL查询缓存一直是我们解决性能问题的方法。如果你的网站运行缓慢,MySQL专家建议开启查询缓存。事实证明,如果写入负载高,缓存实际上是有害的。如果你没有测试就打开了它,你将永远不会知道。

还要记住,你永远不会停止扩展规模。处理10req/s的网站需要调整才能支持1000req/s。如果你需要支持10,000req/s,你的架构可能也会完全不同。

数据库

  • 不要使用MySQLi——PDO是“现代”的面向对象数据库访问层。在你的查询中使用占位符是最重要的特性。它足够聪明,为你使用服务器端准备和其他优化。
  • 目前你可能不想将数据库分开。如果你发现一个数据库无法胜任,根据你的应用程序,有几种扩展技术。如果你有更多的读操作,将数据复制到其他服务器通常是有效的。分片是将数据分割到多台机器上的技术。

缓存

  • 你可能不想在数据库中进行缓存。数据库通常是瓶颈,因此增加更多IO通常是不好的。有一些PHP缓存可以完成类似的事情,比如APC和Zend。
  • 使用缓存打开和关闭测量你的系统,我敢打赌你的缓存比直接服务页面更重。
  • 如果从数据库中构建您的注释和文章数据需要很长时间,请集成Memcache到您的系统中。您可以将查询结果缓存并存储在Memcached实例中。重要的是要记住,从Memcache检索数据必须比从数据库中组装数据更快才能获得任何好处。
  • 如果您的文章不是动态的,或者在生成后有简单的动态更改,请考虑将HTML或PHP写入磁盘。您可以有一个index.php页面,在磁盘上查找文章,如果它在那里,将其流式传输到客户端。如果没有,它将生成文章,将其写入磁盘并将其发送到客户端。从磁盘中删除文件将导致页面被重新写入。如果向文章添加评论,请删除缓存的副本--它会重新生成。

  • 11
    将内容写入磁盘。您甚至可以放弃index.php并让Apache为您完成工作,这样只有在路径不存在时才会调用index.php。您需要使用mode_rewrite实现此功能。 - troelskn
    PDO 不足智能,无法自动使用 MySQL 的服务器端准备语句 - 您必须明确要求它们。 - user42092
    5
    PDO比MySQLi甚至是MySQL扩展慢得多。 - Alix Axel
    4
    PDO 比 mysqli 慢得多,对于嵌套查询也不能正常工作。Mysqli 也支持像 PDO 一样的服务器端准备和绑定参数。 - Daren Schwenke
    5
    我无法相信这个答案被接受了。它并不是很好。 - symcbean
    1
    关于缓存 - 图像、CSS、HTML和JS将有所帮助,还要关闭图像上的Cookie! - Talvi Watia

    69

    我是一个负责超过1500万用户的网站的首席开发人员。由于我们早期进行了规划并谨慎扩展,因此我们几乎没有遇到缩放问题。以下是我从经验中可以建议的一些策略。

    模式 首先,对您的模式进行去规范化。这意味着您应该选择一个大表而不是多个关系表。总的来说,联接是一种浪费宝贵数据库资源的方式,因为多个准备和校对操作会消耗磁盘I/O。尽可能避免使用联接。

    在这里的权衡是,您将存储/提取冗余数据,但是这是可以接受的,因为数据和内部网络带宽非常便宜(更大的磁盘),而多个准备I/O的成本则是数量级更高的(更多服务器)。

    索引 确保您的查询至少使用一个索引。但是要注意,如果您频繁编写或更新,索引将花费更多成本。有一些实验性技巧可避免这种情况。

    您可以尝试添加未索引的其他列,并行运行与已索引列相同的列。然后,您可以拥有一个离线过程,批量地将未索引的列写入已索引的列。这样,您可以更好地控制MySQL何时需要重新计算索引。

    像瘟疫一样避免计算查询。如果必须计算查询,请尝试在编写时只计算一次。

    缓存 我强烈推荐使用Memcached。它已被PHP堆栈上的最大玩家(Facebook)证明,并且非常灵活。有两种方法可以实现,一种是在您的数据库层中进行缓存,另一种是在业务逻辑层中进行缓存。

    数据库层选项需要对从数据库检索的查询结果进行缓存。您可以使用md5()散列您的SQL查询,并将其用作查找键,然后再到达数据库。这样做的好处在于它相当容易实现。缺点(取决于实现方式)是,由于在缓存到期方面将所有缓存视为相同,因此会失去灵活性。

    我工作的店铺使用业务层缓存,这意味着我们系统中的每个具体类都控制着自己的缓存模式和缓存超时时间。 这对我们来说效果还不错,但请注意,从数据库检索到的项可能与从缓存中检索到的项不同,因此您需要同时更新缓存和数据库。

    数据分片 仅有复制操作无法解决所有问题。您很快就会遇到写入瓶颈问题。为了弥补这一点,请尽早支持数据分片。如果不这样做,您很可能会后悔。

    实现起来非常简单。基本上,您需要将密钥管理权与数据存储分开。使用全局数据库来存储主键和集群ID之间的映射关系。您可以查询此映射以获取一个集群,然后查询该集群以获取数据。您可以大量缓存此查找操作,使其成为可忽略的操作。

    这种方法的缺点是可能难以从多个分片中组合数据。但是,您也可以通过工程方式解决这个问题。

    离线处理 如果用户没有必要等待后端处理,请不要让他们等待。构建作业队列,并将任何可以离线处理的进程与用户请求分开。


    10
    毫无疑问,这应该是被采纳的答案。有趣的是,我读过的所有关于构建数据库的文章都强调“尽可能将数据规范化”,却没有提到执行连接操作对性能的影响。我一直觉得连接操作(特别是多个连接)会增加很多开销,但直到现在我才听到有人明确地说出来。我希望我更好地理解你所说的控制MySQL计算索引的方法,这听起来像一个非常有趣的技巧。 - Evan Plaice
    数据分片对于不断增长的数据库来说是必不可少的。Google(公司而非搜索引擎)在实施分片方案方面有很多有趣的见解。离线处理也是限制数据库写入数量(以及限制表索引重新计算数量)的重要手段。我看到很多博客(甚至包括堆栈溢出)都在他们的用户生成评论/反馈系统中使用这种技术。 - Evan Plaice
    1
    感谢您的评论。令人惊奇的是,有些人主张对中间层代码进行分析,而实际上绝大部分执行时间都花在数据输入/输出或客户端-服务器输入/输出上。相比于简单地节省一个耗时40毫秒的PHP进程的20%执行时间,对一个耗时1秒的数据库查询进行5%的优化才是有意义的。 - thesmart

    43

    我曾在使用PHP和MySQL作为后端的一些网站上工作过,这里是一些基础知识:

    1. 缓存,缓存,再缓存。缓存是减轻Web服务器和数据库负荷的最简单和最有效的方法之一。缓存页面内容、查询、昂贵的计算等任何I/O绑定的内容。Memcache非常简单而且有效。
    2. 一旦达到极限,请使用多个服务器。您可以拥有多个Web服务器和多个数据库服务器(具备复制功能)。
    3. 减少对Web服务器的总请求数。这包括使用过期标头缓存JS、CSS和图像。您还可以将静态内容移动到CDN上,从而加速用户体验。
    4. 测量和基准测试。在生产机器上运行Nagios,并在开发/ QA服务器上进行负载测试。您需要知道何时您的服务器会出现问题,以便预防它。

    我建议阅读Building Scalable Websites,它是由Flickr的一位工程师撰写的优秀参考资料。

    同时查看我的博客文章关于可伸缩性,它有许多关于使用多种语言和平台进行扩展的演示文稿链接: http://www.ryandoherty.net/2008/07/13/unicorns-and-scalability/


    1
    +1 这里有很多好的信息。我最近一直在研究这个话题,你的答案与我所读到的一切都是一致的。Memcache、缓存、CDN用于静态内容、减少请求等都是好东西。我还要补充一点,如果您在CDN/缓存后面,请在服务器端对静态内容文件生成哈希值,以便更新的文件在缓存中具有唯一签名。此外,动态地组合静态源文件(CSS、JavaScript)(并使用文件名哈希进行缓存)以减少请求。同时,动态生成缩略图(并将它们存储在缓存中)。 - Evan Plaice
    Google已经创建了一个名为mod_pagespeed的apache模块,可以处理所有静态内容的文件合并、缩小、文件重命名以包含哈希等。它只会在服务器最初添加一点处理开销,直到缓存(和CDN)填充了大部分内容。此外,出于安全考虑,将公开访问的表格(用户)放在与处理后端的表格相同的数据库中通常是不明智的(如果由于某种原因其中一个表格被黑客攻击)。 - Evan Plaice

    40

    关于PDO / MySQLi / MySQLND的回复

    @gary

    你不能简单地说“不要使用MySQLi”,因为它们有不同的目标。PDO几乎像一个抽象层(尽管实际上不是),旨在使使用多个数据库产品变得容易,而MySQLi则专门用于MySQL连接。在比较与MySQLi相比时,错误地说PDO是现代访问层,因为你的声明暗示了进展是mysql -> mysqli -> PDO,这并不是事实。

    在MySQLi和PDO之间进行选择很简单-如果您需要支持多个数据库产品,则使用PDO。如果只使用MySQL,则可以在PDO和MySQLi之间进行选择。

    那么为什么会选择MySQLi而不是PDO?请参见下文...

    @ross

    关于MySQLnd,你是正确的,它是最新的MySQL核心语言级库,但它不是MySQLi的替代品。MySQLi(以及PDO)仍然是通过PHP代码与MySQL交互的方式。这两者都使用libmysql作为PHP代码背后的C客户端。问题在于libmysql在核心PHP引擎之外,这就是mysqlnd发挥作用的地方,即它是一种原生驱动程序,利用核心PHP内部来最大化效率,特别是在内存使用方面。

    MySQLnd正在由MySQL自己开发,并最近已经登陆到PHP 5.3分支中,该版本处于RC测试阶段,准备在今年晚些时候发布。然后,您将能够使用MySQLnd与MySQLi一起使用...但无法与PDO一起使用。这将使MySQLi在许多区域(不是所有区域)获得性能提升,并且如果您不需要像PDO那样的抽象功能,则使其成为与MySQL交互的最佳选择。
    话虽如此,MySQLnd 现在可用于PDO,因此您可以从ND中获得性能增强的优势,但是,PDO仍然是一个通用的数据库层,因此不太可能像MySQLi那样从ND的增强中受益这里可以找到一些有用的基准测试,尽管它们是来自2006年的。您还需要注意诸如此选项之类的事情。
    在决定使用MySQLi还是PDO时,需要考虑很多因素。实际上,在处理非常高的请求量之前,这并不重要。在这种情况下,最好使用专门为MySQL设计的扩展,而不是抽象化事物并提供MySQL驱动程序的扩展。
    选择哪个更好并不是一个简单的问题,因为每个扩展都有优点和缺点。您需要阅读我提供的链接,做出自己的决定,然后进行测试并找出结果。我过去在一些项目中使用了PDO,它是一个很好的扩展,但如果纯粹考虑性能,我的选择将是MySQLi,并编译新的MySQLND选项(当PHP 5.3发布时)。

    6
    我从 PDO 转向 mysqli 后,常规查询的执行速度明显提高了两倍。 - serg
    5
    @serg: 你能否发布一些测试来确认这一点?因为我严重怀疑仅仅从PDO切换到mysqli就会给你带来如此大的速度提升。 - Stann

    23

    概述

    • 不要试图在真实负载之前进行优化。你可能猜对了,但如果你没有,你就浪费了时间。
    • 使用jmeterxdebug或其他工具来测试网站性能。
    • 如果负载开始成为问题,通常涉及到对象或数据缓存,因此一般需要了解缓存选项(memcached、MySQL缓存选项)。

    代码

    • 对代码进行分析,以便知道瓶颈在哪里,以及是在代码还是数据库中。

    数据库

    • 如果不需要移植到其他数据库,则使用MYSQLi,否则使用PDO
    • 如果基准测试显示数据库是问题所在,请在缓存之前检查查询。使用EXPLAIN查看查询的哪些部分正在减慢速度。
    • 在优化查询并对数据库进行某种形式的缓存后,您可能希望使用多个数据库。根据数据、查询和读/写行为的类型,可以复制到多个服务器或分片(将数据分散到多个数据库/服务器)。

    缓存

    • 已经有很多关于缓存代码、对象和数据的文章了。查找APC, Zend Optimizer, memcached, QuickCache, JPCache的文章。在真正需要之前就进行一些学习,这样你就不用担心开始时无法优化。
    • APC和Zend Optimizer是opcode缓存,它们通过避免重新解析和编译代码来加速PHP代码。通常很容易安装,值得早期使用。
    • Memcached是一个通用缓存,可以用来缓存查询、PHP函数或对象,或整个页面。必须专门编写代码才能使用它,如果没有集中处理缓存对象的创建、更新和删除,则可能会涉及到一个复杂的过程。
    • QuickCache和JPCache是文件缓存,与Memcached类似。基本概念很简单,但也需要代码,并且有集中的创建、更新和删除点更容易实现。

    其他

    • 考虑使用替代的 Web 服务器来处理高负载。像 lighthttpnginx 这样的服务器可以在比 Apache 更少的内存情况下处理大量流量,如果您可以牺牲 Apache 的功能和灵活性(或者您不需要这些功能,通常情况下确实如此)。
    • 请记住,现在硬件价格惊人便宜,因此一定要计算优化大块代码所需的成本与“让我们购买一个巨型服务器”的成本之间的差异。
    • 考虑将“MySQL”和“scaling”标签添加到此问题中。

    9

    APC是必不可少的。它不仅可以作为一个很好的缓存系统,而且自动缓存PHP文件所带来的收益是一个福音。至于多个数据库的想法,我认为在同一台服务器上使用不同的数据库并不能获得太多的优势。虽然在查询时可能会提高一点速度,但我怀疑为了维护所有三个数据库并确保它们保持同步而部署和维护代码所需要的工作量是否值得。

    我还强烈推荐运行Xdebug来查找程序中的瓶颈。它让我的优化变得轻而易举。


    9
    首先,我认为Knuth曾经说过,“过早优化是万恶之源”。如果你现在不必处理这些问题,那就不要着急,首先专注于交付正确的工作。话虽如此,如果优化不能等待。
    尝试对数据库查询进行分析,找出哪些查询较慢且频繁发生,并从中制定优化策略。
    我建议调查Memcached,因为许多高负载网站都使用它来高效缓存各种类型的内容,而且它的PHP对象接口非常好用。
    将数据库分割到多台服务器上,并使用某种负载平衡技术(例如,在必要数据的冗余数据库数量之间生成1到#的随机数,并使用该数字确定要连接的数据库服务器),也可以是提高效率的绝佳方法。
    这些方法在过去为一些相当高负载的网站表现良好。希望这能帮助你入门 :-)

    1
    "我们应该忘记小效率问题,大约有97%的时间:过早优化是万恶之源。" - Alister Bulman
    程序员浪费了大量时间思考或担心程序中非关键部分的速度,而这些效率的尝试实际上在调试和维护时会产生强烈的负面影响。我们应该忘记小的效率问题,大约97%的时间:过早的优化是万恶之源。然而,在关键的3%机会中,我们也不应该放弃。 - cHao

    7
    值得一提的是,即使没有像memcached这样的扩展/帮助程序包,PHP中的缓存也非常简单。只需使用ob_start()创建输出缓冲区即可。
    创建一个全局缓存函数。调用ob_start,将函数作为回调传递。在函数中,查找页面的缓存版本。如果存在,则提供并结束。
    如果不存在,脚本将继续处理。当它到达匹配的ob_end()时,它将调用您指定的函数。此时,您只需获取输出缓冲区的内容,将其放入文件中,保存文件并结束。
    添加一些过期/垃圾收集。
    许多人不知道您可以嵌套ob_start()/ob_end()调用。因此,如果您已经使用输出缓冲区来解析广告或执行语法高亮或其他操作,您可以嵌套另一个ob_start/ob_end调用。

    +1 因为它看起来是一个有趣的想法。我不知道它的性能表现如何。 - Sylverdrag
    +1 因为这是一个有趣的想法。那些回调函数可以为我调用我的缓存类! - Xeoncross

    6
    使用类似Xdebug这样的工具来分析您的应用程序(如tj9991所建议的)绝对是必要的。盲目优化没有太多意义。Xdebug将帮助您找到代码中真正的瓶颈,以便您可以明智地花费优化时间,并修复实际导致减速的代码块。
    如果您正在使用Apache,则另一个可帮助测试的实用程序是Siege。它将通过真正地使其经过严格的测试来帮助您预测服务器和应用程序在高负载下的反应情况。
    任何一种PHP的opcode缓存(如APC或其他许多缓存)也会有很大帮助。

    6
    我运营一个每月有七八百万页面浏览量的网站。虽然不算太多,但足以让我们的服务器感受到了负载。我们选择的解决方案很简单:在数据库层面使用Memcache。如果数据库负载是您的主要问题,这个解决方案是有效的。
    我们最初使用Memcache来缓存整个对象和最常用的数据库结果。它确实起到了作用,但也引入了一些错误(如果我们更加小心,可能会避免其中的一些错误)。
    所以我们改变了方法。我们构建了一个数据库包装器(具有与旧数据库完全相同的方法,因此很容易切换),然后我们对其进行了子类化,以提供基于Memcached的数据库访问方法。
    现在你只需要决定查询是否可以使用缓存(可能已经过期)的结果。现在用户运行的大多数查询都直接从Memcache中获取。例外情况是更新和插入,仅因为日志记录而发生在主网站上。这个相当简单的措施将我们的服务器负载减少了约80%。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接