PHP Web System Optimization(undone)
目录
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来说,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的解释执行,并将结果返回
在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协议的模块
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解释器发送解析请求
和 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
对于大流量高负载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