整个项目是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
稍微解释一下代码(熟悉的工友请pass):1. 将mysql默认的分隔符分号重定义为// 避免mysql 只执行其中一句话;2. 创建存储过程传入参数cnt (中奖用户限额), user (此次抢票的用户); 3. 定义两个临时变量num (目前中奖用户数), success(是否中奖);4.查询当前中奖用户数目,未超额则插入用户状态1,反之0 ; 5. 返回中奖与否标志,恢复mysql的sql分隔符.
在php中调用此存储过程: $db->query(call proc(100,'hehe'));
此方法的缺点是在数据库引入了业务逻辑,程序修改不易,优点是只使用一次数据库连接,表的锁定时间大大减少,并发效率很高。
三、 奇葩windows环境下的php
在我满怀欣喜的开始模拟高并发用户访问的时候,问题来了。。。
先贴 java 写的多线程并发访问程序(php不支持多线程。。)
import java.util.concurrent.cyclicbarrier;import com.test.run.threadtest;public class test { public static void main(string[] args) { cyclicbarrier cb=new cyclicbarrier(100); //fork 100个线程 threadtest[] ttarray=new threadtest[100]; //待这些线程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是不是以多进程多线程方式运行的