Heim  >  Artikel  >  Backend-Entwicklung  >  PHP Nginx MySQL 高并发调优 小试

PHP Nginx MySQL 高并发调优 小试

WBOY
WBOYOriginal
2016-06-23 13:41:561046Durchsuche

项目要求实现一个免费抢券的功能,涉及到高并发的问题,研究了几天,记录下来,欢迎工友们扔砖头~~

整个项目是PHP+Nginx+Mysql的架构,由于PHP是阻塞的单线程模型,不支持多线程,因此也没有Java那么好用的同步机制,我想到的办法就是在数据库级别做相应的同步互斥的控制,Mysql的锁机制我放在了Mysql数据库锁机制这篇博文当中。通过查看Mysql官方文档,我想到了两种解决方案:一、使用LOCK TABLE 或START TRANSACTION 写SQL 语句; 二、使用CREATE PROCEDURE 直接在数据库中创建存储过程,接下来我就分别试了这两种方法。

一、 使用锁机制

SET autocommit=0;LOCK TABLE test;select count(*) from test where value=1;COMMIT;
这是 查询当天中奖的用户(为了示意简化了业务逻辑),然后我用PHP做一个判断:是否中奖用户超过了当天的限额,没超过则该用户中奖,那么此时要UPDATE 一下数据库,若两个用户同时读取中奖用户总数,其中一个update了数据库,另一个用户读到的自然是脏数据,这也就是为什么我没有释放刚才那张表的锁,按照业务逻辑,是要跳出mysql用程序判断一下,然后update数据库再释放锁。

update test(name,value) values('Tomcat',1);COMMIT;UNLOCK TABLE;

这种方法的缺点在于使用了两次数据库连接,中间插入了PHP判断,必定会造成性能上的损失,好处是数据库不必插入业务逻辑,松耦合。


二、 使用存储过程

DELIMITER //DROP PROCEDURE IF EXISTS proc;CREATE PROCEDURE proc(IN cnt INT,IN user VARCHAR(32))BEGIN	DECLARE num INT;	DECLARE success INT;	select count(*) INTO num from test where value=1;	IF num<cnt then insert into test values set success="1;" else end if select>  <br> 稍微解释一下代码(熟悉的工友请pass):1. 将mysql默认的分隔符分号重定义为// 避免mysql 只执行其中一句话;2. 创建存储过程传入参数cnt (中奖用户限额), user (此次抢票的用户); 3. 定义两个临时变量num (目前中奖用户数), success(是否中奖);4.查询当前中奖用户数目,未超额则插入用户状态1,反之0 ; 5. 返回中奖与否标志,恢复mysql的sql分隔符.  <p></p>  <p>在php中调用此存储过程: $db->query("call proc(100,'hehe')");</p>  <p>此方法的缺点是在数据库引入了业务逻辑,程序修改不易,优点是只使用一次数据库连接,表的锁定时间大大减少,并发效率很高。</p>  <p><br> </p>  <p>三、 奇葩windows环境下的PHP</p>  <p>在我满怀欣喜的开始模拟高并发用户访问的时候,问题来了。。。</p>  <p>先贴 java 写的多线程并发访问程序(php不支持多线程。。)</p>  <p></p>  <pre name="code" class="sycode">import java.util.concurrent.CyclicBarrier;import com.test.run.ThreadTest;public class Test {	public static void main(String[] args) {		CyclicBarrier cb=new CyclicBarrier(100);<span style="white-space:pre">	</span>//fork 100个线程		ThreadTest[] ttarray=new ThreadTest[100];<span style="white-space:pre">	</span>//待这些线程fork完毕,同时发起http请求		for (int i = 0; i 	//必须写上await 方法等待其他线程创建完毕,再统一发送			System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"\tbegin ");		//	long l1= ;			InputStreamReader isr =new InputStreamReader(huc.getInputStream(),"UTF-8");			BufferedReader bf=new BufferedReader(isr);			String str=bf.readLine();			while(str!=null)			{				System.out.println(str);				str=bf.readLine();			}			long l2= System.currentTimeMillis();			//System.out.println(l2-l1+"   "+Thread.currentThread().getName());			//System.out.println(Thread.currentThread().getName()+"\t"+System.currentTimeMillis()+"   end");		} catch (MalformedURLException e) {			e.printStackTrace();		} catch (IOException e) {			e.printStackTrace();		} catch (InterruptedException e) {			// TODO Auto-generated catch block			e.printStackTrace();		} catch (BrokenBarrierException e) {			// TODO Auto-generated catch block			e.printStackTrace();		}	}}

满怀信心地跑程序,结果发现控制台1秒1秒地给我蹦出结果,也就是1个用户服务器需要大约1秒的处理时间,这简直是一坨翔!! 没办法赶紧做测试查原因,测试方案及结果:

1. 单独测试所有线程的产生和发出请求是否符合要求。

结果:通过打印线程名和时间,发现线程随机地被fork出来,在几乎同一时间点开始run, run的顺序跟fork的顺序不一样,更显出其随机性,因此不是多线程的问题。

2. 分别使用锁机制和存储过程的方式访问数据库,比较二者差异。 

结果:锁机制第一个用户耗时1078 ms, 第二个2146ms, 第三个3199ms ;存储过程 1023ms, 2084ms; 3115ms; 不相伯仲,这说明一个问题:PHP似乎是串行地处理这些请求的,就算这些线程几乎是同时到达服务器的。

3. 直接使用PHP向mysql 中插入一条数据,是否插入就需要1s;

结果:插入一条数据时间真TM是1s左右!!!这PHP跟mysql连接也忒慢了!

4. 使用linux 服务器测试,是否是系统影响

结果:插入数据30ms左右,100并发在300ms左右搞定!!!


从第三个方案想到第四个花了老长时间了,根本没想到居然是系统的原因,google上说这TM是 PHP 的bug

“The problem is that the PHP_FCGI_CHILDREN environment variable is ignored under windows, therefore php-cgidoes not spawn children, and when PHP_FCGI_MAX_REQUESTS is reached the process terminates.So, php with fast-cgi will **never** work on Windows.”From  https://bugs.php.net/bug.php?id=49859

我只想说WTF, windows看来真不适合做服务器,或许php的缔造者压根不想使用windows。在windows下,php-cgi是默认在监听9000端口,只有唯一一个进程在服务于用户,纵使nginx多么的高并发,转发给php-cgi的时候只能串行执行了。有一个非常机智的哥们直接fork了好几个php-cgi进程来处理请求,膜拜一下:

http {#window 不能派生子进程,只能人工配 PHP_FCGI_CHILDREN 在window不起作用的upstream fastcgi_backend {server 127.0.0.1:9000;server 127.0.0.1:9001;server 127.0.0.1:9002;server 127.0.0.1:9003;}server {listen       80;server_name  q.qq;access_log ./../log/q.qq.access.txt;root d:/web/www;location ~ \.php$ {fastcgi_pass   fastcgi_backend;}}

他在nginx 的配置文件中使用upstream 建立4个进程来处理请求,然后将php请求转发到这个类似与负载均衡器的东西上,就可以一下提高并发的处理能力了。

回想了一下我在Linux下启动Php 的方式:命令行输入 spawn-fcgi -a 127.0.0.1 -p 9000 -C10-u www-data -f /usr/bin/php-cgi ,spawn出10个子进程来处理9000端口的并发的请求,因此100个请求的时间几乎是单线程的10倍,因此快乐不少~~

在查资料优化的过程中,也学到了一些调优的小技巧:

  • Nginx 配置调优:
  • worker_processes  4;//开启4个工作进程,数目不多于CPU的核数。Nginx是非阻塞IO & IO复用模型,适合高并发

    events {
          worker_connections  1024;//提高每个工作进程最多可接受请求的连接数
        multi_accept on;//开启接受多请求
    }

    关于上文提到的nginx upstream 可以通过ip_hash, 将不同的IP请求转发到相应的服务器做负载均衡,

    #定义负载均衡设备的Ip及设备状态

    upstream resinserver{
    ip_hash;
    server 127.0.0.1:8000 down;
    server 127.0.0.1:8080 weight=2;
    server 127.0.0.1:6801;
    server 127.0.0.1:6802 backup;
    }

    在需要使用负载均衡的server中增加均衡器地址 proxy_pass http://resinserver/;


    每个设备的状态设置为:
    1.down 表示单前的server暂时不参与负载
    2.weight 代表负载权重,默认为1。weight越大,负载的权重就越大。
    3.max_fails :允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误
    4.fail_timeout:max_fails次失败后,暂停的时间。
    5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。

    nginx支持同时设置多组的负载均衡,用来给不用的server来使用。

    client_body_in_file_only 设置为On 可以讲client post过来的数据记录到文件中用来做debug
    client_body_temp_path 设置记录文件的目录 可以设置最多3层目录
    location 对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡


  • PHP-fpm 调优:开启process.max = 128
  • 关于Mysql调优可以参考这两篇文章:
  • LAMP 系统性能调优,第 3 部分: MySQL 服务器调优
    论MySQL的监控和调优

    plus: 对于PHP中无法存储全局变量在服务器中,类似于Java的application变量,我采用了一种共享内存的方法暂时解决这个问题,总感觉哪里不好,欢迎工友们多多指教~~

    //读取共享内存中的变量,输入内存ID,访问模式READ/WRITE,权限,块大小
    function readMemory($systemid,$mode,$permissions,$size){		$shmid = shmop_open($systemid, $mode, $permissions, $size);		$size = shmop_size($shmid);	$res = shmop_read($shmid,0,$size);	shmop_close($shmid);	//close shared memory is a must in case of dead lock		return $res;}//写入变量,function writeMemory($systemid, $mode, $permissions, $size,$content){	$shmid = shmop_open($systemid, $mode, $permissions, $size);	shmop_write($shmid, $content, 0);	shmop_close($shmid);}
    writeMemory(1024, 'c', 0755, 1024,$content);
    readMemory(1024, 'a', 0755, 1024);



    分享促进社会进步~~



    参考文献:

    nginx upstream的分配方式;

    window+nginx+php-cgi的php-cgi线程/子进程问题;

    PHP内核探索;

    探讨nginx与php-fpm是不是以多进程多线程方式运行的



    Stellungnahme:
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn