PHP Web System Optimization(undone)

时间:2022-12-20 20:00:37

PHP Optimization

目录

0. 引言
1. PHP Pool
2. listen
3. Process Manage(PM)
4. pm.max_children
5. PHP DB Connection Pool(数据库连接池)

 

0. 引言

0x1: WEB系统的性能优化需要考虑哪些方面

对于一个WEB系统来说,从client发起请求,到服务器端处理,最后到返回到client显示结果,在大多数情况下这是一个较长的链路,其中的每一个环节都存在可以优化性能的热点,例如

1. 将多台web server配置成集群模式,在集群的前端(逻辑)部署Load Balance Server,将客户端的访问请求"平均地"负载到集群的子节点server中
2. 在Load Balance Server和集群之间部署Cache Server,例如对于典型的DB驱动型的应用,使用Redis代替直接Mysql连接就是一个很好的实践
1) 对于insert操作,在向mysql插入的同时,向redis中也保存一份
2) 对于select操作,优先从redis中查询,如果命中失败,再从mysql中查询
3. 集群中的web server采用nginx+php-fpm的组合,nginx在高并发情况下的表现比apache要更好
4. 针对web server上的业务代码进行逻辑优化
5. 将单点的DB,例如mysql,改为读写分离的主从集群架构
6. 使用DB Connection Pool(数据库连接池)代替每次请求都重新创建并销毁DB连接的操作

0x2: PHP解析工作方式

PHP Web System Optimization(undone)

首先要明白的是,对于PHP来说,SAPI是PHP必由的和外部交互的通道,所谓的PHP和外部不同的通信交互方式的实现差别,都在SAPI这个层面实现,函数sapi_cgibin_ub_write告诉zend如何输出数据

SAPI提供了一个和外部通信的接口,使得PHP可以和其他应用进行交互数据,PHP默认提供了很多种SAPI

1. Apache:mod_php5
2. IIS:ISAPI
3. SHELL:CLI

PHP从本质上讲是一个脚本代码解释执行容器,而这个解析请求是由WEB容器主动触发的,一般来说,WEB server和PHP的配合方式有以下几种

1. Apache + mod_php 
2. Apache + mod_fastcgi
3. nginx + php-fpm

1. Apache + mod_php

这是在LAMP架构中最常用的组合方式,它把PHP编译为APACHE的一个"内置模块",让apache http server本身能够支持php语言,不需要每一个请求都要启动一次"php解释器"来解释执行php
当有一个php请求过来时,直接在httpd进程里完成php的解释执行,并将结果返回

PHP Web System Optimization(undone)

在mod_php的源码中,函数sapi_cgibin_ub_write()直接调用了apache的ap_write函数,所以,用mod_php,我们可以把php和apache看成一个模块,两者绑定在一起

2. Apache + mod_fastcgi

fastcgi是http server和第三方程序的交互方式,它可以接收web server发起的请求,解释输入信息,并将处理后的结果返回给服务器。mod_fastcgi是在apache支持下fastcgi协议的模块

PHP Web System Optimization(undone)

apache启动后,mod_fastcgi会在启动多个cgi程序,即php-cgi脚本,具体的数目通过配置来指定,当有http请求到来后,httpd进程会选择其中一个当前空闲的php-cgi程序来执行,执行的方式和mod_php类似,也是通过php-cgi提供的sapi完成交互

从源码中可以看到,对于cgi的sapi它是把结果输出到fastcgi提供的stdout上,fastcgi再将数据返回给httpd完成交互

3. nginx + php-fpm 

php-fpm的出现是为了解决在fastcgi模式下cgi管理的问题,php-fpm是一个类似于spwn-cgi的管理工具,可以和任何支持远端fastcgi的web server工作
FPM(FastCGI 进程管理器)用于替换 PHP FastCGI 的大部分附加功能,对于高负载网站是非常有用的

1. 支持平滑停止/启动的高级进程管理功能 
2. 可以工作于不同的 uid/gid/chroot 环境下,并监听不同的端口和使用不同的 php.ini 配置文件(可取代 safe_mode 的设置)
3. stdout 和 stderr 日志记录
4. 在发生意外情况的时候能够重新启动并缓存被破坏的 opcode
5. 文件上传优化支持
6. "慢日志" - 记录脚本(不仅记录文件名,还记录 PHP backtrace 信息,可以使用 ptrace或者类似工具读取和分析远程进程的运行数据)运行所导致的异常缓慢
7. fastcgi_finish_request() - 特殊功能:用于在请求完成和刷新数据后,继续在后台执行耗时的工作(录入视频转换、统计处理等)
8. 动态/静态子进程产生
9. 基本 SAPI 运行状态信息(类似Apache的 mod_status)
10. 基于 php.ini 的配置文件

Relevant Link:

http://huoding.com/2014/12/25/398
http://php-fpm.org/
http://php.net/manual/zh/install.fpm.php
http://wenku.baidu.com/view/887de969561252d380eb6e92.html
http://baike.baidu.com/view/4168033.htm

 

1. PHP Pool

这里的池指的是"PHP进程池",PHP允许同时启动多个池,每个池使用不同的配置,各个池之间尊重彼此的主权领土完整,互不干涉内政

"Pool对象(线程池)"是多个 Worker 对象的容器,同时也是它们的控制器。它是对 Worker 功能的高层抽象,包括按照 pthreads 需要的方式来管理应用的功能

<?php
class Config extends Threaded
{ // shared global object
protected $val = 0, $val2 = 0;
protected function inc(){++$this->val;} // protected synchronizes by-object
public function inc2(){++$this->val2;} // no synchronization
}
class WorkerClass extends Worker
{
protected static $worker_id_next = -1;
protected $worker_id;
protected $config;
public function __construct($config)
{
$this->worker_id = ++static::$worker_id_next; // static members are not avalable in thread but are in 'main thread'
$this->config = $config;
}
public function run()
{
global $config;
$config = $this->config; // NOTE: setting by reference WON'T work
global $worker_id;
$worker_id = $this->worker_id;
echo "working context {$worker_id} is created!\n";
//$this->say_config(); // globally synchronized function.
}
protected function say_config()
{ // 'protected' is synchronized by-object so WON'T work between multiple instances
global $config; // you can use the shared $config object as synchronization source.
$config->synchronized(function() use (&$config)
{ // NOTE: you can use Closures here, but if you attach a Closure to a Threaded object it will be destroyed as can't be serialized
var_dump($config);
});
}
}
class Task extends Stackable
{
// Stackable still exists, it's just somehow dissappeared from docs (probably by mistake). See older version's docs for more details.
protected $set;
public function __construct($set)
{
$this->set = $set;
}
public function run()
{
global $worker_id;
echo "task is running in {$worker_id}!\n";
usleep(mt_rand(1,100)*100);
$config = $this->getConfig();
$val = $config->arr->shift();
$config->arr[] = $this->set;
for ($i = 0 ; $i < 1000; ++$i)
{
$config->inc();
$config->inc2();
}
}
public function getConfig(){
global $config; // WorkerClass set this on thread's scope, can be reused by Tasks for additional asynch data source. (ie: connection pool or taskqueue to demultiplexer)
return $config;
}
}

$config = new Config;
$config->arr = new Threaded();
$config->arr->merge(array(1,2,3,4,5,6));

class PoolClass extends Pool
{
public function worker_list()
{
if ($this->workers !== null)
return array_keys($this->workers);
return null;
}
}
$pool = new PoolClass(3, 'WorkerClass', [$config] );
$pool->worker_list();
//$pool->submitTo(0,new Task(-10)); // submitTo DOES NOT try to create worker

$spammed_id = -1;
for ($i = 1; $i <= 100; ++$i)
{
// add some jobs
if ($spammed_id == -1 && ($x = $pool->worker_list())!= null && @$x[2])
{
$spammed_id = $x[2];
echo "spamming worker {$spammed_id} with lots of tasks from now on\n";
}
if ($spammed_id != -1 && ($i % 5) == 0) // every 5th job is routed to one worker, so it has 20% of the total jobs (with 3 workers it should do ~33%, not it has (33+20)%, so only delegate to worker if you plan to do balancing as well... )
$pool->submitTo($spammed_id,new Task(10*$i));
else
$pool->submit(new Task(10*$i));
}
$pool->shutdown();
var_dump($config); // "val" is exactly 100000, "val2" is probably a bit less
// also: if you disable the spammer, you'll that the order of the "arr" is random.

?>

默认情况下,PHP 只启用了一个池,所有请求均在这个池中执行。一旦某些请求出现拥堵之类的情况,那么很可能会连累整个池出现火烧赤壁的结局;如果启用多个池,那么可以把请求分门别类放到不同的池中执行,此时如果某些请求出现拥堵之类的情况,那么只会影响自己所在的池,从而控制故障的波及范围

Relevant Link:

http://php.net/manual/zh/class.pool.php
http://netkiller.github.io/journal/thread.php.html

 

2. listen

从本质上来讲,PHP是一个脚本语言的解释服务器,它接收来自web容器的解析请求,并返回动态解析的结果

虽然 Nginx 和 PHP 可以部署在不同的服务器上,但是实际应用中,多数人都习惯把它们部署在同一台服务器上,如此就有两个选择

1. TCP:通过TCP连接的方式向PHP解释器发送解析请求
2. Unix Socket:通过Unix Socket方式向PHP解释器发送解析请求

PHP Web System Optimization(undone)

和 TCP 比较,Unix Socket 省略了一些诸如 TCP 三次握手之类的环节,所以相对更高效,不过需要注意的是,在使用 Unix Socket 时,因为没有 TCP 对应的可靠性保证机制,所以最好把 backlog 和 somaxconn 设置大些,否则面对高并发时会不稳定

Relevant Link:

https://blog.linuxeye.com/364.html
http://www.cnxct.com/default-configuration-and-performance-of-nginx-phpfpm-and-tcp-socket-or-unix-domain-socket/

 

3. Process Manage(PM)

进程管理有动态和静态之分

1. 动态模式
一般先启动少量进程,再按照请求数的多少实时调整进程数。如此的优点很明显:节省资源;当然它的缺点也很明显:一旦出现高并发请求,系统将不得不忙着 FORK 新进程,必然会影响性能
2. 静态模式
一次性 FORK 足量的进程,之后不管请求量如何均保持不变。和动态模式相比,静态模式虽然消耗了更多的资源,但是面对高并发请求,它不需要执行高昂的 FORK

PHP Web System Optimization(undone)

对于大流量高负载WEB应用来说,使用静态模式进行进程预分配是一个很好的最佳实践

 

4. pm.max_children

一个 CPU 在某一个时刻只能处理一个请求。当请求数大于 CPU 个数时,CPU 会划分时间片,轮流执行各个请求,既然涉及多个任务的调度,那么上下文切换必然会消耗一部分性能,从这个意义上讲,进程数应该等于 CPU 个数,如此一来每个进程都对应一个专属的 CPU,可以把上下文切换损失的效率降到最低。不过这个结论仅在请求是 CPU 密集型时才是正确的,而对于一般的 Web 请求而言,多半是 IO 密集型的,此时这个结论就值得商榷了,因为数据库查询等 IO 的存在,必然会导致 CPU 有相当一部分时间处于 WAIT 状态,也就是被浪费的状态。此时如果进程数多于 CPU 个数的话,那么当发生 IO 时,CPU 就有机会切换到别的请求继续执行,虽然这会带来一定上下文切换的开销,但是总比卡在 WAIT 状态好多了。

Relevant Link:

http://www.guangla.com/post/2014-03-14/40061238121
http://forum.nginx.org/read.php?3,222702

 

5. PHP DB Connection Pool(数据库连接池)

Relevant Link:

​http://gonzalo123.com/2010/11/01/database-connection-pooling-with-php-and-gearman/​

 

Copyright (c) 2014 LittleHann All rights reserved