PHP Nginx MySQL 高並發調優 小試

來源:互聯網
上載者:User

標籤:php   並發   多線程   效能   

項目要求實現一個免費搶券的功能,涉及到高並發的問題,研究了幾天,記錄下來,歡迎工友們扔磚頭~~

整個項目是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))BEGINDECLARE num INT;DECLARE success INT;select count(*) INTO num from test where value=1;IF num<cnt THENinsert into test(user,value) values(user,1);SET success=1;ELSEinsert into test(user,value) values(user,0);SET success=0;END IF;SELECT success;END//DELIMITER ;

稍微解釋一下代碼(熟悉的工友請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);<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 < ttarray.length; i++) {ttarray[i]=new ThreadTest(cb);ttarray[i].start();}}}import java.io.BufferedReader;import java.io.DataInputStream;import java.io.IOException;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.util.concurrent.BrokenBarrierException;import java.util.concurrent.CyclicBarrier;public class ThreadTest extends Thread {private  CyclicBarrier cb;public ThreadTest(CyclicBarrier cb) {super();this.cb = cb;}@Overridepublic void run() {String path="http://127.16.0.57/concurrent/index.php?user="+Thread.currentThread().getName();try {URL url=new URL(path);HttpURLConnection huc=(HttpURLConnection) url.openConnection();huc.setRequestMethod("GET");huc.setDoInput(true);huc.setDoOutput(true);huc.setUseCaches(false);huc.connect();cb.await();<span style="white-space:pre"></span>//必須寫上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 blocke.printStackTrace();} catch (BrokenBarrierException e) {// TODO Auto-generated catch blocke.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 lockreturn $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是不是以多進程多線程方式啟動並執行



PHP Nginx MySQL 高並發調優 小試

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.