或许是这5件事导致你的web性能低下

我们都知道服务器的负载能力的重要性,本文从5个可能影响负载能力的点上进行讨论。

首先,有必要了解提高服务端PHP代码效率所需的关键操作。

最重要的是对性能数据的收集,如果你想对某个地方进行优化,那么你需要测量优化前后的数据以进行对比。一般来说,程序的响应时间以及对内存的使用是比较重要的。对于PHP来说,大多数情况下,页面的加载时间是影响用户体验最大的一个环节。当然,还有其他的各种问题同样对性能有很大的影响,如:网络延迟、I/O等。

提示: 对于日志输出,需要极为谨慎,因为日志系统本身就会对性能有所影响,如果滥用日志,很可能会成为你的系统中的一块短板。当然,也不能完全没有日志,往往日志是你发现问题最关键的信息。至于如果合理的使用日志,就得根据你的业务场景来定了。

下面是一个简单的获取内存使用情况的代码:

$time = microtime(true);
$mem = memory_get_usage();

// 需要测试的代码
for ($i = 0; $i < 10000000000; $i ++) {  
  $b = $i + $i;
  $c = $b * $i;

  for ($k = 0; $k < 999; $k ++) {

    $d = $k * $i;
    $e = $k * $b * $c;
  }
}


print_r([  
  'momory' => (memory_get_usage() - $mem) / (1024 * 1024)
  'seconds' => microtime(true) - $time;
]);

1. 缓存

这个建议可能会出现在所有的性能清单上,这表明了它的重要性。有很多不错的工具可以帮你完成缓存的工作,比如:Memcache或者强大的Varnish。从本质上来说,你必须知道你的程序是否真的需要一遍遍的执行。如果你的信息是不变的或者不需要实时的变化,使用缓存可以节省CPU的执行周期,提高程序的速度。

下面是使用Memcache来缓存数据的示例:

function showAndHeavyOperation() {  
  sleep(1);
  return date('Y-m-d H:i:s');
}

$item1 = showAndHeavyOperation();
echo $item1;  

上面的代码利用sleep(1)让程序睡眠1秒钟来模拟一个慢操作。结下来就让我们用缓存来重构上面的代码:

$memcache = new Memcache;
$memcache->connect('localhost', 11211);

function showAndHeavyOperation() {  
  sleep(1);
  return data('Y-m-d H:i:s');
}

$item1 = $memcache->get('item');

if ($item1 === false) {  
  $item1 = showAndHeavyOperation();
  $memcache->set('item', $item1);
}

echo $item1;  

现在脚本在第一次的时候,showAndHeavyOperation执行一次。当你再次执行的时候,就不会再去执行showAndHeavyOperation,而是从缓存中获取数据。但是,你肯定发现一个问题,从Memcache中获取到的数据总是老的数据,但是Memcache允许你设置存储的数据的TTL(存活时间)。有了这个功能,你可以设置一个刷新策略来缓存数据,虽然还是无法做到真正的实时数据,但是为服务器节省了大量的资源,特别是在高负载和高并发的业务下,作用尤其明显。对于变化少或者实时性要求低的数据就可将其放入到缓存中来提高程序效率。更多关于Memcache的信息,请参见这里

提示:Memcache中的数据不是持久化的,当你重启Memcache后,存储在Memcache中的数据将不再存在。所有你的应用程序必须能够在缓存数据为空的时候,重建缓存。换句话说,你的应用程序不应该依赖于数据的存在,特别是在云环境中。当然你可以使用 Redis 来替代Memcache。

Memcache为你提供了一个简单而强大的机制来创建缓存。如果你还想想创建更加高级的高速缓存,使网站的不同部分拥有不同的TTL,例如:你可能希望网页标题缓存两个小时,侧边栏缓存十分钟,这种情况下,你可以使用Varnish

Varnish 是缓存和HTTP反向代理的混合。有些人把它称为 HTTP 加速器。Varnish 非常的灵活,且具有高可定制性。目前主流的PHP框架,如:Symfony2,已经集成了Varnish。

回顾一下,缓存可以帮助我们解决三个问题:

  • 降低对CPU和内存的使用
  • 提高页面的加载时间
  • 利于搜索引擎优化(谷歌Analytics认为任何网页加载时间超过1.5秒都属于慢网页,慢网页对于SEO有着不少的弊端)。

2. 请密切关注循环

我们总是习惯性的使用循环,它们是个强大的编程工具,但是往往循环会造成性能瓶颈。执行一个慢操作本身就是一个问题,但是如果这个慢操作在循环中执行,就会将问题放大。那么,循环到底好不好呢?循环当然是个好东西。就好比菜刀,用于切菜是个很好的东西,但是用来伤人,就不对了。所以需要将循环利用好,且需要仔细评估你的循环,特别是在嵌套循环中,防止出现问题。

以下面的代码为例:

// 错误使用循环的例子
function expexiveOperation() {  
  sleep(1);
  return "Hello";
}

for ($i = 0; $i < 100; $i ++) {  
  $value = expexiveOperation();
  echo $value;
}

上面代码的问题很明显,每循环一次都设置相同的变量,做了很多的无用功,下面我们优化下上面的代码:

// 正确的使用案例
function expexiveOperation() {  
  sleep(1);
  return "Hello";
}

$value = expexiveOperation();

for ($i = 0; $i < 100; $i ++) {  
  echo $value;
}

这段代码输出的内容和上一段代码完全一致,但是这里就不需要每次循环都去调用慢操作方法,很大程度上的提高了代码的执行效率。

但是,上面给的案例很简单,所以你能给很容易的定位到问题的所在。在现实的开发中,往往没有这么简单。为了检测性能问题,你需要考虑如下几点:

  • 检测大循环(for, foreach, ...)
  • 它们是否会大量的遍历数据
  • 对他们的执行速度进行测量
  • 是否能够利用缓存
    • 如果是的话,那你还在等啥?
    • 如果不能,将它们标记为可能存在危险,并专注于它们的检查。因为它们可能会无限放大你的小问题。

基本上,你必须清楚的知道,你写这个循环是为什么。你很难记住程序的所有代码,但是你必须意识到,循环往往需要昂贵的性能。有时候我需要对以前的代码进行重构和优化,我往往是先用剖析器查找循环并重构可优化的。

我们可以使用性能分析工具帮助我们完成这个工作。Xdebug 和 Zend Debugger 允许我们创建概要分析报告。如果我们选择Xdebug,我们可以使用Webgrind,它可以帮助我们检查瓶颈。请记住,一个瓶颈是一个问题,而一个瓶颈迭代10000次是将问题放大10000倍。

3. 使用队列

我们真的需要执行用户请求中的所有任务吗?有时候是必要的,但并非总是如此。想象一下,例如,你需要在用户提交一个操作时发送一个电子邮件给用户,你可以使用简单的php脚本发送此邮件,但这个操作可能需要一秒钟。如果你等到脚本执行完最后一句,你可以确保邮件已经发送成功。但是我们真的有必要等待这一秒钟呢?你可以使用一个队列,将操作放到队列中,而不需要在此等待一秒。邮件稍后将被发送,用户不需要等到发送成功。

Gearman是一个框架,允许你创建队列和并行处理任务,你可以阅读Gearman文档来获得更多关于Gearman的信息。Gearman的主要思想很简单,你可以定义主角本调用Worker,而不是在主脚本中执行操作。

下面是一个Gearman的简单案例:

$filename = '/path/to/img.jpg';
if (realpath(__FILE__) == realpath($filename)) {  
  exit();
}

$stringSize = 3;
$footerSize = ($stringSize == 1) ? 12 : 15;
$footer = date('d/m/Y H:i:s');

list($width, $heigth, $image_type) = getimagesize($filename);  
$im = imagecreatefromjpeg($filename);
imagefilledrectangle(  
  $im,
  0,
  $height,
  $width,
  $height - $footerSize,
  imagecolorallocate($im, 49, 49, 156)
);

imagestring(  
  $im,
  $stringSize,
  $width - (imagefontwidth($stringSize) * strlen($footer)) -2,
  $height - $footerSize,
  $footer,
  imagecolorallocate($im, 255, 255, 255);
);

header('Content-Type: image/jpeg');

下面代码将上面的操作重写为为Gearman的Worker

$gmw = new GreamanWorker();
$gmw->addServer();
$gmw->addFunction('watermark', function ($job) {
  $workload = $job->workload();
  $workload_size = $job->workloadSize();

  list($filename, $footer) = json_encode($workload);

  $footerSize = 15;
  list($width, $height, $image_type) = getimagesize($filename);

  $im = imagecreateformjpeg($filename);

  imagefilledrectangle(
    $im,
    0,
    $height,
    $width,
    $height - $footerSize, 
    imagecolorallocate($im, 49, 49, 156)
  );

  imagestring(
    $stringSize,
    $width - (imagefontwidth($stringSize) * strlen($footer)) - 2,
    $height->$footerSize,
    $footer,
    imagecolorallocate($im, 255, 255, 255)
  );

  ob_start();
  ob_implicit_flush(0);
  imagepng($im);
  $image = ob_get_content();
  ob_end_clean();

  return $img;
});

while(1) {  
  $gmw->work();
}

在客户端脚本上调用:

$filename = '/path/to/img.jpg';
$footer = date('d/m/Y H:i:s');

$gmclient = new GearmanClient();
$gmclient->addServer();

$handle = $gmclient->do('watermark',json_encode([$filename, $footer]));

if ($gmclient->requestOpc() != GEARMAN_SUCCESS){  
  echo "Ups someting wrong happen";
} else {
  headr('Content-Type: image/jpeg');
  echo $handle;
}

关于Gearman最酷的事情,就是可以平行的增加Worker,而不需要对客户端代码进行修改。这样当用户量上升后,你只需要多布置几个Gearman节点就好了。很简单吧

可能使用Gearman的一些场景:

  • 海量邮件系统
  • 生成PDF
  • 图像处理
  • Logs
  • ...

Gearman在web应用程序中广泛被使用,例如Grooveshark和Instagram就使用了Gearman。Instagram大概有200多个使用Python编写的Worker。也就是说,它是语言无关的。你可以用任何语言来编写。

其他队列系统还有ZeroMQRabbitMQ等。

4. 谨慎的访问数据库

一般在海量数据的时候,数据库往往都是一个大的性能问题来源。数据库的连接是昂贵的操作,特别是对于PHP这种缺少连接池的语言来说。

此外,一个简单的查询是否使用索引的差异也是令人难以置信的。强烈建议你检查数据库索引,因为使用错误的索引的SQL查询会大幅的降低程序的性能。

对索引的检查不能只检查一次,因为随着数据的增长,索引可能会有所改变。

另外一个建议是,使用预处理语句,为什么?让我们从例子中看看吧:

$dbh = new PDO('pgsql:dbname=pg1;host=localhost', 'user', 'password');
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$field1 = uniqid();
$dbh->beginTransaction();
foreach (range(1, 5000, 1) as $i) {  
  $stmt = $dbh->prepare("UPDATE test.tbl1 set field1 = '{$field1}' where id = 1");
  $field1 = $i;
  $stmt->execute();
}
$dbh->commit();

另外一个:

$dbh = new PDO('pgsql:dbname=pg1;host=localhost', 'user', 'password');
$dbh->setAttribute(PDO_ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$field1 = uniqid();
$dbh->beginTransaction();
$stmt = $dbh->prepare('UPDATE test.tbl1 set field1 = :F1 where id = 1');
foreach (range(1, 5000, 1) as $i) {  
  $field1 = $i;
  $stmt->execute(array('F1' => $field1));
}
$dbh->commit();

第一个例子在循环中,使用了一个新的SQL语句,并执行5000次, 数据库需要解析每条SQL并执行它。第二个例子中,使用预处理语句,只是在循环中,绑定了5000次不同的参数而已,而不需要把SQL解析5000次。而且,使用预处理语句,可以有效的防范SQL注入。

5. 大流量

如果你的应用瞬间增加了数以万计的并发,会发生什么?你的服务器能够处理好这些并发吗?这个问题并不容易回答。所以在开发过程中,就应该模拟大并发对程序进行压力测试。

类似的测试用具有不少,我平时用的是Apache AB来对应用进行性能测试。

Apache AB的使用非常简单,我们看看它的基本操作:

ab -n 100 -c http://www.baidu.com/  

执行上面的命令会直接在终端中打印出结果,当然,你也可以结果输出到文件中:

ab -n 100 -c 10 -e test.csv http://www.baidu.com  

总结

如果你想要提高你的WEB性能,你需要回答下面这些问题:

  • 我的应用程序有多少个数据库连接?
  • 每个select语句花费多少时间?
  • 应用程序有多少个select语句?
  • 它们是在循环内吗?
  • 是否真的需要每次都执行它们,是否可以将它们放入缓存中?
  • 是否真的有必要在主线程中执行用户的所有请求?
  • 可以将它们放入队列中吗?
  • 我的服务器是否支持大负载和大并发?
  • 每个请求使用多少CPU?
  • 每个请求使用多少内存?

正如你所看到的,有很多你必须回答的问题,获取你开始阅读这篇文章寻找完美的解决方案。但是很抱歉,没有什么灵丹妙药。你必须根据你的情况来回答上面的问题,并作出相应的调整。

还有最后一点,对前端的优化也不可忽视,毕竟每个请求,不是只有后端消耗了时间。响应时间 = 后端 + 前端。

猜你喜欢

转载自blog.csdn.net/yangyang_01/article/details/80665673