高併發解決方案

咱們一般衡量一個Web系統的吞吐率的指標是QPS(Query Per Second,每秒處理請求數),解決每秒數萬次的高併發場景,這個指標很是關鍵。舉個例子,咱們假設處理一個業務請求平均響應時間爲100ms,同時,系統內有20臺Apache的Web服務器,配置MaxClients爲500個(表示Apache的最大鏈接數目)。php

 

那麼,咱們的Web系統的理論峯值QPS爲(理想化的計算方式):前端

20*500/0.1 = 100000 (10萬QPS)java

咦?咱們的系統彷佛很強大,1秒鐘能夠處理完10萬的請求,5w/s的秒殺彷佛是「紙老虎」哈。實際狀況,固然沒有這麼理想。在高併發的實際場景下,機器都處於高負載的狀態,在這個時候平均響應時間會被大大增長。mysql

普通的一個p4的服務器天天最多能支持大約10萬左右的IP,若是訪問量超過10W那麼須要專用的服務器才能解決,若是硬件不給力 軟件怎麼優化都是於事無補的。主要影響服務器的速度android

有:網絡-硬盤讀寫速度-內存大小-cpu處理速度。nginx

就Web服務器而言,Apache打開了越多的鏈接進程,CPU須要處理的上下文切換也越多,額外增長了CPU的消耗,而後就直接致使平均響應時間增長。所以上述的MaxClient數目,要根據CPU、內存等硬件因素綜合考慮,絕對不是越多越好。能夠經過Apache自帶的abench來測試一下,取一個合適的值。而後,咱們選擇內存操做級別的存儲的Redis,在高併發的狀態下,存儲的響應時間相當重要。網絡帶寬雖然也是一個因素,不過,這種請求數據包通常比較小,通常不多成爲請求的瓶頸。負載均衡成爲系統瓶頸的狀況比較少,在這裏不作討論哈。程序員

那麼問題來了,假設咱們的系統,在5w/s的高併發狀態下,平均響應時間從100ms變爲250ms(實際狀況,甚至更多):web

20*500/0.25 = 40000 (4萬QPS)redis

因而,咱們的系統剩下了4w的QPS,面對5w每秒的請求,中間相差了1w。sql

舉個例子,高速路口,1秒鐘來5部車,每秒經過5部車,高速路口運做正常。忽然,這個路口1秒鐘只能經過4部車,車流量仍然依舊,結果一定出現大塞車。(5條車道突然變成4條車道的感受)

同理,某一個秒內,20*500個可用鏈接進程都在滿負荷工做中,卻仍然有1萬個新來請求,沒有鏈接進程可用,系統陷入到異常狀態也是預期以內。

14834077821.jpg

其實在正常的非高併發的業務場景中,也有相似的狀況出現,某個業務請求接口出現問題,響應時間極慢,將整個Web請求響應時間拉得很長,逐漸將Web服務器的可用鏈接數佔滿,其餘正常的業務請求,無鏈接進程可用。

更可怕的問題是,是用戶的行爲特色,系統越是不可用,用戶的點擊越頻繁,惡性循環最終致使「雪崩」(其中一臺Web機器掛了,致使流量分散到其餘正常工做的機器上,再致使正常的機器也掛,而後惡性循環),將整個Web系統拖垮。

3. 重啓與過載保護

若是系統發生「雪崩」,貿然重啓服務,是沒法解決問題的。最多見的現象是,啓動起來後,馬上掛掉。這個時候,最好在入口層將流量拒絕,而後再將重啓。若是是redis/memcache這種服務也掛了,重啓的時候須要注意「預熱」,而且極可能須要比較長的時間。

秒殺和搶購的場景,流量每每是超乎咱們系統的準備和想象的。這個時候,過載保護是必要的。若是檢測到系統滿負載狀態,拒絕請求也是一種保護措施。在前端設置過濾是最簡單的方式,可是,這種作法是被用戶「千夫所指」的行爲。更合適一點的是,將過載保護設置在CGI入口層,快速將客戶的直接請求返回

高併發下的數據安全

咱們知道在多線程寫入同一個文件的時候,會存現「線程安全」的問題(多個線程同時運行同一段代碼,若是每次運行結果和單線程運行的結果是同樣的,結果和預期相同,就是線程安全的)。若是是MySQL數據庫,可使用它自帶的鎖機制很好的解決問題,可是,在大規模併發的場景中,是不推薦使用MySQL的。秒殺和搶購的場景中,還有另一個問題,就是「超發」,若是在這方面控制不慎,會產生髮送過多的狀況。咱們也曾經據說過,某些電商搞搶購活動,買家成功拍下後,商家卻不認可訂單有效,拒絕發貨。這裏的問題,也許並不必定是商家奸詐,而是系統技術層面存在超發風險致使的。

1. 超發的緣由

假設某個搶購場景中,咱們一共只有100個商品,在最後一刻,咱們已經消耗了99個商品,僅剩最後一個。這個時候,系統發來多個併發請求,這批請求讀取到的商品餘量都是99個,而後都經過了這一個餘量判斷,最終致使超發。(同文章前面說的場景)

14834077822.jpg

在上面的這個圖中,就致使了併發用戶B也「搶購成功」,多讓一我的得到了商品。這種場景,在高併發的狀況下很是容易出現。

優化方案1:將庫存字段number字段設爲unsigned,當庫存爲0時,由於字段不能爲負數,將會返回false

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

<?php

//優化方案1:將庫存字段number字段設爲unsigned,當庫存爲0時,由於字段不能爲負數,將會返回false

include('./mysql.php');

$username 'wang'.rand(0,1000);

//生成惟一訂單

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//記錄日誌

function insertLog($event,$type=0,$username){

    global $conn;

    $sql="insert into ih_log(event,type,usernma)

    values('$event','$type','$username')";

    return mysqli_query($conn,$sql);

}

function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)

{

      global $conn;

      $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)

      values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";

     return  mysqli_query($conn,$sql);

}

//模擬下單操做

//庫存是否大於0

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";

$rs=mysqli_query($conn,$sql);

$row $rs->fetch_assoc();

  if($row['number']>0){//高併發下會致使超賣

      if($row['number']<$number){

        return insertLog('庫存不夠',3,$username);

      }

      $order_sn=build_order_no();

      //庫存減小

      $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";

      $store_rs=mysqli_query($conn,$sql);

      if($store_rs){

          //生成訂單

          insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);

          insertLog('庫存減小成功',1,$username);

      }else{

          insertLog('庫存減小失敗',2,$username);

      }

  }else{

      insertLog('庫存不夠',3,$username);

  }

?>

2. 悲觀鎖思路

解決線程安全的思路不少,能夠從「悲觀鎖」的方向開始討論。

悲觀鎖,也就是在修改數據的時候,採用鎖定狀態,排斥外部請求的修改。遇到加鎖的狀態,就必須等待。

14834077833.jpg

雖然上述的方案的確解決了線程安全的問題,可是,別忘記,咱們的場景是「高併發」。也就是說,會不少這樣的修改請求,每一個請求都須要等待「鎖」,某些線程可能永遠都沒有機會搶到這個「鎖」,這種請求就會死在那裏。同時,這種請求會不少,瞬間增大系統的平均響應時間,結果是可用鏈接數被耗盡,系統陷入異常。

優化方案2:使用MySQL的事務,鎖住操做的行

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

<?php

//優化方案2:使用MySQL的事務,鎖住操做的行

include('./mysql.php');

//生成惟一訂單號

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//記錄日誌

function insertLog($event,$type=0){

    global $conn;

    $sql="insert into ih_log(event,type)

    values('$event','$type')";

    mysqli_query($conn,$sql);

}

//模擬下單操做

//庫存是否大於0

mysqli_query($conn,"BEGIN");  //開始事務

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此時這條記錄被鎖住,其它事務必須等待這次事務提交後才能執行

$rs=mysqli_query($conn,$sql);

$row=$rs->fetch_assoc();

if($row['number']>0){

    //生成訂單

    $order_sn=build_order_no();

    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)

    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";

    $order_rs=mysqli_query($conn,$sql);

    //庫存減小

    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";

    $store_rs=mysqli_query($conn,$sql);

    if($store_rs){

      echo '庫存減小成功';

        insertLog('庫存減小成功');

        mysqli_query($conn,"COMMIT");//事務提交即解鎖

    }else{

      echo '庫存減小失敗';

        insertLog('庫存減小失敗');

    }

}else{

  echo '庫存不夠';

    insertLog('庫存不夠');

    mysqli_query($conn,"ROLLBACK");

}

?>

3. FIFO隊列思路

那好,那麼咱們稍微修改一下上面的場景,咱們直接將請求放入隊列中的,採用FIFO(First Input First Output,先進先出),這樣的話,咱們就不會致使某些請求永遠獲取不到鎖。看到這裏,是否是有點強行將多線程變成單線程的感受哈。

14834077834.jpg

而後,咱們如今解決了鎖的問題,所有請求採用「先進先出」的隊列方式來處理。那麼新的問題來了,高併發的場景下,由於請求不少,極可能一瞬間將隊列內存「撐爆」,而後系統又陷入到了異常狀態。或者設計一個極大的內存隊列,也是一種方案,可是,系統處理完一個隊列內請求的速度根本沒法和瘋狂涌入隊列中的數目相比。也就是說,隊列內的請求會越積累越多,最終Web系統平均響應時候仍是會大幅降低,系統仍是陷入異常。

4. 文件鎖的思路

對於日IP不高或者說併發數不是很大的應用,通常不用考慮這些!用通常的文件操做方法徹底沒有問題。但若是併發高,在咱們對文件進行讀寫操做時,頗有可能多個進程對進一文件進行操做,若是這時不對文件的訪問進行相應的獨佔,就容易形成數據丟失

優化方案4:使用非阻塞的文件排他鎖

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

<?php

//優化方案4:使用非阻塞的文件排他鎖

include ('./mysql.php');

//生成惟一訂單號

function build_order_no(){

  return date('ymd').substr(implode(NULL, array_map('ord'str_split(substr(uniqid(), 7, 13), 1))), 0, 8);

}

//記錄日誌

function insertLog($event,$type=0){

    global $conn;

    $sql="insert into ih_log(event,type)

    values('$event','$type')";

    mysqli_query($conn,$sql);

}

$fp fopen("lock.txt""w+");

if(!flock($fp,LOCK_EX | LOCK_NB)){

    echo "系統繁忙,請稍後再試";

    return;

}

//下單

$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";

$rs =  mysqli_query($conn,$sql);

$row $rs->fetch_assoc();

if($row['number']>0){//庫存是否大於0

    //模擬下單操做

    $order_sn=build_order_no();

    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)

    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";

    $order_rs =  mysqli_query($conn,$sql);

    //庫存減小

    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";

    $store_rs =  mysqli_query($conn,$sql);

    if($store_rs){

      echo '庫存減小成功';

        insertLog('庫存減小成功');

        flock($fp,LOCK_UN);//釋放鎖

    }else{

      echo '庫存減小失敗';

        insertLog('庫存減小失敗');

    }

}else{

  echo '庫存不夠';

    insertLog('庫存不夠');

}

fclose($fp);

 ?>

5. 樂觀鎖思路

這個時候,咱們就能夠討論一下「樂觀鎖」的思路了。樂觀鎖,是相對於「悲觀鎖」採用更爲寬鬆的加鎖機制,大都是採用帶版本號(Version)更新。實現就是,這個數據全部請求都有資格去修改,但會得到一個該數據的版本號,只有版本號符合的才能更新成功,其餘的返回搶購失敗。這樣的話,咱們就不須要考慮隊列的問題,不過,它會增大CPU的計算開銷。可是,綜合來講,這是一個比較好的解決方案。

14834077835.jpg

有不少軟件和服務都「樂觀鎖」功能的支持,例如Redis中的watch就是其中之一。經過這個實現,咱們保證了數據的安全。

優化方案5:Redis中的watch

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

<?php

$redis new redis();

 $result $redis->connect('127.0.0.1', 6379);

 echo $mywatchkey $redis->get("mywatchkey");

/*

  //插入搶購數據

 if($mywatchkey>0)

 {

     $redis->watch("mywatchkey");

  //啓動一個新的事務。

    $redis->multi();

   $redis->set("mywatchkey",$mywatchkey-1);

   $result = $redis->exec();

   if($result) {

      $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());

      $watchkeylist = $redis->hGetAll("watchkeylist");

        echo "搶購成功!<br/>";

        $re = $mywatchkey - 1;  

        echo "剩餘數量:".$re."<br/>";

        echo "用戶列表:<pre>";

        print_r($watchkeylist);

   }else{

      echo "手氣很差,再搶購!";exit;

   

 }else{

     // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");

     //  $watchkeylist = $redis->hGetAll("watchkeylist");

        echo "fail!<br/>";   

        echo ".no result<br/>";

        echo "用戶列表:<pre>";

      //  var_dump($watchkeylist); 

 }*/

$rob_total = 100;   //搶購數量

if($mywatchkey<=$rob_total){

    $redis->watch("mywatchkey");

    $redis->multi(); //在當前鏈接上啓動一個新的事務。

    //插入搶購數據

    $redis->set("mywatchkey",$mywatchkey+1);

    $rob_result $redis->exec();

    if($rob_result){

         $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);

        $mywatchlist $redis->hGetAll("watchkeylist");

        echo "搶購成功!<br/>";

      

        echo "剩餘數量:".($rob_total-$mywatchkey-1)."<br/>";

        echo "用戶列表:<pre>";

        var_dump($mywatchlist);

    }else{

          $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');

        echo "手氣很差,再搶購!";exit;

    }

}

?>

PHP解決網站大數據大流量與高併發

第一個要說的就是數據庫,首先要有一個很好的架構,查詢儘可能不用* 避免相關子查詢 給常常查詢的添加索引 用排序來取代非順序存取,若是條件容許 ,通常MySQL服務器最好安裝在Linux操做系統中 。關於apache和nginx在高併發的狀況下推薦使用nginx,ginx是Apache服務器不錯的替代品。nginx內存消耗少 官方測試可以支撐5萬併發鏈接,在實際生產環境中跑到2~3萬併發鏈接數。php方面不須要的模塊儘可能關閉,使用memcached,Memcached 是一個高性能的分佈式內存對象緩存系統,不使用數據庫直接從內存當中調數據,這樣大大提高了速度,iiS或Apache啓用GZIP壓縮優化網站,壓縮網站內容大大節省網站流量。

第二,禁止外部的盜鏈。

外部網站的圖片或者文件盜鏈每每會帶來大量的負載壓力,所以應該嚴格限制外部對
於自身的圖片或者文件盜鏈,好在目前能夠簡單地經過refer來控制盜鏈,Apache自
己就能夠經過配置來禁止盜鏈,IIS也有一些第三方的ISAPI能夠實現一樣的功能。當
然,僞造refer也能夠經過代碼來實現盜鏈,不過目前蓄意僞造refer盜鏈的還很少,
能夠先不去考慮,或者使用非技術手段來解決,好比在圖片上增長水印。

第三,控制大文件的下載。

大文件的下載會佔用很大的流量,而且對於非SCSI硬盤來講,大量文件下載會消耗
CPU,使得網站響應能力降低。所以,儘可能不要提供超過2M的大文件下載,若是須要
提供,建議將大文件放在另一臺服務器上。

第四,使用不一樣主機分流主要流量

將文件放在不一樣的主機上,提供不一樣的鏡像供用戶下載。好比若是以爲RSS文件佔用
流量大,那麼使用FeedBurner或者FeedSky等服務將RSS輸出放在其餘主機上,這
樣別人訪問的流量壓力就大多集中在FeedBurner的主機上,RSS就不佔用太多資源了

第五,使用不一樣主機分流主要流量
將文件放在不一樣的主機上,提供不一樣的鏡像供用戶下載。好比若是以爲RSS文件佔用流量大,那麼使用FeedBurner或者FeedSky等服務將RSS輸出放在其餘主機上,這樣別人訪問的流量壓力就大多集中在FeedBurner的主機上,RSS就不佔用太多資源了。

第六,使用流量分析統計軟件。
在網站上安裝一個流量分析統計軟件,能夠即時知道哪些地方耗費了大量流量,哪些頁面須要再進行優化,所以,解決流量問題還須要進行精確的統計分析才能夠。好比:Google Analytics(Google分析)。

高併發和高負載的約束條件:硬件、部署、操做系統、Web 服務器、PHP、MySQL、測試

部署:服務器分離、數據庫集羣和庫表散列、鏡像、負載均衡

負載均衡分類: 1)、DNS輪循 2)代理服務器負載均衡 3)地址轉換網關負載均衡 4)NAT負載均衡 5)反向代理負載均衡 6)混合型負載均衡

部署方案1:

適用範圍:靜態內容爲主體的網站和應用系統;對系統安全要求較高的網站和應用系統。

Main Server:主服務器

承載程序的主體運行壓力,處理網站或應用系統中的動態請求;

將靜態頁面推送至多個發佈服務器;

將附件文件推送至文件服務器;

安全要求較高,以靜態爲主的網站,可將服務器置於內網屏蔽外網的訪問。

DB Server:數據庫服務器

承載數據庫讀寫壓力;

只與主服務器進行數據量交換,屏蔽外網訪問。

File/Video Server:文件/視頻服務器

承載系統中佔用系統資源和帶寬資源較大的數據流;

做爲大附件的存儲和讀寫倉庫;

做爲視頻服務器將具有視頻自動處理能力。

發佈服務器組:

只負責靜態頁面的發佈,承載絕大多數的Web請求;

經過Nginx進行負載均衡部署。

部署方案2:

適用範圍:以動態交互內容爲主體的網站或應用系統;負載壓力較大,且預算比較充足的網站或應用系統;

Web服務器組:

Web服務無主從關係,屬平行冗餘設計;

經過前端負載均衡設備或Nginx反向代理實現負載均衡;

劃分專用文件服務器/視頻服務器有效分離輕/重總線;

每臺Web服務器可經過DEC可實現鏈接全部數據庫,同時劃分主從。

數據庫服務器組:

相對均衡的承載數據庫讀寫壓力;

經過數據庫物理文件的映射實現多數據庫的數據同步。

共享磁盤/磁盤陣列

將用於數據物理文件的統一讀寫

用於大型附件的存儲倉庫

經過自身物理磁盤的均衡和冗餘,確保總體系統的IO效率和數據安全;

方案特性:

經過前端負載均衡,合理分配Web壓力;

經過文件/視頻服務器與常規Web服務器的分離,合理分配輕重數據流;

經過數據庫服務器組,合理分配數據庫IO壓力;

每臺Web服務器一般只鏈接一臺數據庫服務器,經過DEC的心跳檢測,可在極短期內自動切換至冗餘數據庫服務器;

磁盤陣列的引入,大幅提高系統IO效率的同時,極大加強了數據安全性。

Web服務器:

Web服務器很大一部分資源佔用來自於處理Web請求,一般狀況下這也就是Apache產生的壓力,在高併發鏈接的狀況下,Nginx是Apache服務器不錯的替代品。Nginx (「engine x」) 是俄羅斯人編寫的一款高性能的 HTTP 和反向代理服務器。在國內,已經有新浪、搜狐通行證、網易新聞、網易博客、金山逍遙網、金山愛詞霸、校內網、YUPOO相冊、豆瓣、迅雷看看等多家網站、 頻道使用 Nginx 服務器。

Nginx的優點:

高併發鏈接:官方測試可以支撐5萬併發鏈接,在實際生產環境中跑到2~3萬併發鏈接數。

內存消耗少:在3萬併發鏈接下,開啓的10個Nginx 進程才消耗150M內存(15M*10=150M)。

內置的健康檢查功能:若是 Nginx Proxy 後端的某臺 Web 服務器宕機了,不會影響前端訪問。

策略:相對於老牌的Apache,咱們選擇Lighttpd和Nginx這些具備更小的資源佔用率和更高的負載能力的web服務器。

Mysql:

MySQL自己具有了很強的負載能力,MySQL優化是一項很複雜的工做,由於這最終須要對系統優化的很好理解。你們都知道數據庫工做就是大量的、 短時的查詢和讀寫,除了程序開發時須要注意創建索引、提升查詢效率等軟件開發技巧以外,從硬件設施的角度影響MySQL執行效率最主要來自於磁盤搜索、磁盤IO水平、CPU週期、內存帶寬。

  根據服務器上的硬件和軟件條件進行MySQl優化。MySQL優化的核心在於系統資源的分配,這不等於無限制的給MySQL分配更多的資源。在MySQL配置文件中咱們介紹幾個最值得關注的參數:

改變索引緩衝區長度(key_buffer)

改變表長(read_buffer_size)

設定打開表的數目的最大值(table_cache)

對緩長查詢設定一個時間限制(long_query_time)

若是條件容許 ,通常MySQL服務器最好安裝在Linux操做系統中,而不是安裝在FreeBSD中。
策略: MySQL優化須要根據業務系統的數據庫讀寫特性和服務器硬件配置,制定不一樣的優化方案,而且能夠根據須要部署MySQL的主從結構。

PHP:

一、加載儘量少的模塊;

二、若是是在windows平臺下,儘量使用IIS或者Nginx來替代咱們日常用的Apache;

三、安裝加速器(都是經過緩存php代碼預編譯的結果和數據庫結果來提升php代碼的執行速度)
eAccelerator,eAccelerator是一個自由開放源碼php加速器,優化和動態內容緩存,提升了性能php腳本的緩存性能,使得PHP腳本在編譯的狀態下,對服務器的開銷幾乎徹底消除。

Apc:Alternative PHP Cache(APC)是 PHP 的一個免費公開的優化代碼緩存。它用來提供免費,公開而且強健的架構來緩存和優化 PHP 的中間代碼。

memcache:memcache是由Danga Interactive開發的,高性能的,分佈式的內存對象緩存系統,用於在動態應用中減小數據庫負載,提高訪問速度。主要機制是經過在內存裏維護一個統 一的巨大的hash表,Memcache可以用來存儲各類格式的數據,包括圖像、視頻、文件以及數據庫檢索的結果等

Xcache:國人開發的緩存器,

策略: 爲PHP安裝加速器。

代理服務器(緩存服務器):

Squid Cache(簡稱爲Squid)是一個流行的自由軟件(GNU通用公共許可證)的代理服務器和Web緩存服務器。Squid有普遍的用途,從做爲網頁服務器的前置cache服務器緩存相關請求來提升Web服務器的速度,到爲一組人共享網絡資源而緩存萬維網,域名系統和其餘網絡搜索,到經過過濾流量幫助網絡安全,到局域網經過代理網。Squid主要設計用於在Unix一類系統運行。

策略:安裝Squid 反向代理服務器,可以大幅度提升服務器效率。

壓力測試:壓力測試是一種基本的質量保證行爲,它是每一個重要軟件測試工做的一部分。壓力測試的基本思路很簡單:不是在常規條件下運行手動或自動測試,而是在計算機數量較少或系統資源匱乏的條件下運行測試。一般要進行壓力測試的資源包括內部內存、CPU 可用性、磁盤空間和網絡帶寬等。通常用併發來作壓力測試。
壓力測試工具:webbench,ApacheBench等

漏洞測試:在咱們的系統中漏洞主要包括:sql注入漏洞,xss跨站腳本攻擊等。安全方面還包括系統軟件,如操做系統漏洞,mysql、apache等的漏洞,通常能夠經過升級來解決。

漏洞測試工具:Acunetix Web Vulnerability Scanner

緩存是優化系統性能最經常使用的方式之一,經過在耗時部件(如數據庫)以前添加緩存,能夠減小實際調用次數,下降響應時間。可是在引入緩存以前,務必三思然後行。

 

 

經過Internet獲取資源既緩慢,成本又高。爲此,Http協議裏包含了控制緩存的部分,以使Http客戶端能夠緩存和重用之前獲取的資源,從而優化性能,提高體驗。雖然Http中關於緩存控制的部分,隨着協議演進,有一些變化。但我覺着,做爲後端程序員,在開發Web服務時,只須要關注請求頭If-None-Match、響應頭ETag、響應頭Cache-Control就足夠了。由於這三個Http頭就能夠知足你的需求,而且,當今絕大多數的瀏覽器,都支持這三個Http頭。咱們所要作的就是,確保每一個服務器響應都提供正確的 HTTP 頭指令,以指導瀏覽器什麼時候能夠緩存響應以及能夠緩存多久。

緩存在哪兒?

cbd8f4eaaa6db9d6087aaea4351b469a.png

上圖中有三個角色,瀏覽器、Web代理和服務器,如圖所示HTTP緩存存在於瀏覽器和Web代理中。固然在服務器內部,也存在着各類緩存,但這已經不是本文要討論的Http緩存了。所謂的Http緩存控制,就是一種約定,經過設置不一樣的響應頭Cache-Control來控制瀏覽器和Web代理對緩存的使用策略,經過設置請求頭If-None-Match和響應頭ETag,來對緩存的有效性進行驗證。

響應頭ETag

ETag全稱Entity Tag,用來標識一個資源。在具體的實現中,ETag能夠是資源的hash值,也能夠是一個內部維護的版本號。但無論怎樣,ETag應該能反映出資源內容的變化,這是Http緩存能夠正常工做的基礎。

4cdb7042a2a63b4b2d05c797ca5fcda6.png

如上例中所展現的,服務器在返回響應時,一般會在Http頭中包含一些關於響應的元數據信息,其中,ETag就是其中一個,本例中返回了值爲x1323ddx的ETag。當資源/file的內容發生變化時,服務器應當返回不一樣的ETag。

請求頭If-None-Match

對於同一個資源,好比上一例中的/file,在進行了一次請求以後,瀏覽器就已經有了/file的一個版本的內容,和這個版本的ETag,當下次用戶再須要這個資源,瀏覽器再次向服務器請求的時候,能夠利用請求頭If-None-Match來告訴服務器本身已經有個ETag爲x1323ddx的/file,這樣,若是服務器上的/file沒有變化,也就是說服務器上的/file的ETag也是x1323ddx的話,服務器就不會再返回/file的內容,而是返回一個304的響應,告訴瀏覽器該資源沒有變化,緩存有效。

641453ab0085aa7fa0edce7a8812ae94.png

如上例中所示,在使用了If-None-Match以後,服務器只須要很小的響應就能夠達到相同的結果,從而優化了性能。

響應頭Cache-Control

每一個資源均可以經過Http頭Cache-Control來定義本身的緩存策略,Cache-Control控制誰在什麼條件下能夠緩存響應以及能夠緩存多久。 最快的請求是沒必要與服務器進行通訊的請求:經過響應的本地副本,咱們能夠避免全部的網絡延遲以及數據傳輸的數據成本。爲此,HTTP 規範容許服務器返回一系列不一樣的 Cache-Control 指令,控制瀏覽器或者其餘中繼緩存如何緩存某個響應以及緩存多長時間。

Cache-Control 頭在 HTTP/1.1 規範中定義,取代了以前用來定義響應緩存策略的頭(例如 Expires)。當前的全部瀏覽器都支持 Cache-Control,所以,使用它就夠了。

如下我來介紹能夠再Cache-Control中設置的經常使用指令。

max-age

該指令指定從當前請求開始,容許獲取的響應被重用的最長時間(單位爲秒。例如:Cache-Control:max-age=60表示響應能夠再緩存和重用 60 秒。須要注意的是,在max-age指定的時間以內,瀏覽器不會向服務器發送任何請求,包括驗證緩存是否有效的請求,也就是說,若是在這段時間以內,服務器上的資源發生了變化,那麼瀏覽器將不能獲得通知,而使用老版本的資源。因此在設置緩存時間的長度時,須要慎重。

public和private

若是設置了public,表示該響應能夠再瀏覽器或者任何中繼的Web代理中緩存,public是默認值,即Cache-Control:max-age=60等同於Cache-Control:public, max-age=60。

在服務器設置了private好比Cache-Control:private, max-age=60的狀況下,表示只有用戶的瀏覽器能夠緩存private響應,不容許任何中繼Web代理對其進行緩存 – 例如,用戶瀏覽器能夠緩存包含用戶私人信息的 HTML 網頁,可是 CDN 不能緩存。

no-cache

若是服務器在響應中設置了no-cache即Cache-Control:no-cache,那麼瀏覽器在使用緩存的資源以前,必須先與服務器確認返回的響應是否被更改,若是資源未被更改,能夠避免下載。這個驗證以前的響應是否被修改,就是經過上面介紹的請求頭If-None-match和響應頭ETag來實現的。

須要注意的是,no-cache這個名字有一點誤導。設置了no-cache以後,並非說瀏覽器就再也不緩存數據,只是瀏覽器在使用緩存數據時,須要先確認一下數據是否還跟服務器保持一致。若是設置了no-cache,而ETag的實現沒有反應出資源的變化,那就會致使瀏覽器的緩存數據一直得不到更新的狀況。

no-store

若是服務器在響應中設置了no-store即Cache-Control:no-store,那麼瀏覽器和任何中繼的Web代理,都不會存儲此次相應的數據。當下次請求該資源時,瀏覽器只能從新請求服務器,從新從服務器讀取資源。

怎樣決定一個資源的Cache-Control策略呢?

下面這個流程圖,能夠幫到你。

44aee8f6311b1256922b40a2d45063cd.png

常見錯誤

啓動時緩存

有時候,咱們會發現應用程序啓動很慢,最終發現是其中一個依賴的服務響應時間很長,這時該怎麼辦?

一般來講,遇到這類問題,說明這個依賴服務沒法知足需求。若是這是一個第三方服務,控制權不在本身手上,這時咱們可能會引入緩存。

此時引入緩存的問題,是緩存失效策略難以生效,由於緩存設計的本意就是儘量少的請求依賴的服務。

過早緩存

這裏提到「早」,不是應用程序的生命週期,而是開發的週期。有的時候咱們會看見,一些開發者在開發初期就已經估算出系統瓶頸,並引入緩存。

事實上,這樣的作法掩蓋了可能進行性能優化的點。反正到時候這個服務的返回值會被緩存住,我幹嗎還要花時間去優化這部分代碼呢?

集成緩存

SOLID原則中的「S」表明——單一功能原則(Single responsibility principle)。當應用程序集成緩存模塊以後,緩存模塊和服務層就有了強耦合,沒法在沒有緩存模塊的參與下單獨運行。

緩存全部內容

有的時候爲了下降響應延遲,可能會盲目的對外部調用都加上緩存。事實上,這樣的行爲很容易讓開發者和維護者沒法意識到緩存模塊的存在,最終對底層依賴模塊的可靠性作出了錯誤的評估。

級聯緩存

緩存全部內容,或者只是緩存了大部份內容,可能會致使緩存數據中包含其餘緩存數據。

若是應用程序中包含這種級聯的緩存結構,可能致使的狀況是緩存失效時間不可控。最上層的緩存須要等每一級緩存都失效更新以後,最終返回的數據纔會完全更新。

不可刷新緩存

一般狀況下,緩存中間件會提供一個刷新緩存的工具。例如Redis,維護人員能夠經過其提供的工具,刪除部分數據,甚至刷新整個緩存。

可是,一些臨時緩存,可能不會包含這樣的工具。例如簡單的將數據保存在內容中的緩存,一般不會容許外部工具來修改或者刪除緩存內容。這時,若是發現緩存數據異常,維護人員只能採起重啓服務的方式,這將大大增長運維成本和響應時間。更有甚者,一些緩存可能會將緩存內容寫在文件系統中進行備份。此時除了重啓服務,還須要確保應用程序啓動以前刪除文件系統上的緩存備份。

緩存帶來的影響

上面提到了引入緩存可能致使的常見錯誤,這些問題在無緩存系統中經過不會考慮。

部署一個重度依賴緩存的系統,可能會由於等待緩存失效而花費大量時間。例如經過CDN緩存內容,系統發佈以後去刷新CDN配置、CDN緩存的內容,可能須要幾個小時。

另外,出現性能瓶頸優先考慮緩存,會致使性能問題被掩蓋,得不到真正的解決。事實上,不少時候調優代碼花費的時間,和引入緩存組件不會相差太多。

最後,對於包含緩存組件的系統,調試成本會大大增長。常常會發生追蹤半天代碼,結果數據來自緩存,和實際邏輯上應該依賴的組件沒有任何關係。一樣的問題也可能出如今執行了全部相關測試用例以後,修改到的代碼實際沒有被測試到。

如何用好緩存?

放棄緩存!

好吧,不少時候緩存是沒法避免的。基於互聯網的系統,很難徹底避免使用緩存,甚至連http協議頭,都包含緩存配置:Cache-Control: max-age=xxx。

瞭解數據

若是要將數據訪問緩存,首先須要瞭解數據更新策略。只有明確瞭解數據什麼時候須要更新,才能經過If-Modified-Since頭來判斷客戶端請求的數據是否須要更新,是簡單返回304 Not Modified響應讓客戶端複用以前的本地緩存數據,仍是返回最新數據。另外,爲了更好利用http協議中的緩存,建議給數據區分版本,或者利用eTag來標記緩存數據的版本。

優化性能而不是使用緩存

前文提到過,使用緩存每每會將潛在性能問題掩蓋。儘量利用性能分析工具,找到應用程序響應緩慢的真實緣由而且修復它。例如減小無效代碼調用,根據SQL執行計劃優化SQL等。

下面是清除應用程序全部緩存的代碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

/*

 * 文 件 名:  DataCleanManager.java

 * 描   述:  主要功能有清除內/外緩存,清除數據庫,清除sharedPreference,清除files和清除自定義目錄

 */ 

package com.test.DataClean; 

   

import java.io.File; 

   

import android.content.Context; 

import android.os.Environment; 

   

/**

 * 本應用數據清除管理器

 */ 

public class DataCleanManager { 

    /**

     * 清除本應用內部緩存(/data/data/com.xxx.xxx/cache)

     

     * @param context

     */ 

    public static void cleanInternalCache(Context context) { 

        deleteFilesByDirectory(context.getCacheDir()); 

    

   

    /**

     * 清除本應用全部數據庫(/data/data/com.xxx.xxx/databases)

     

     * @param context

     */ 

    public static void cleanDatabases(Context context) { 

        deleteFilesByDirectory(new File("/data/data/" 

                + context.getPackageName() + "/databases")); 

    

   

    /**

     * 清除本應用SharedPreference(/data/data/com.xxx.xxx/shared_prefs)

     

     * @param context

     */ 

    public static void cleanSharedPreference(Context context) { 

        deleteFilesByDirectory(new File("/data/data/" 

                + context.getPackageName() + "/shared_prefs")); 

    

   

    /**

     * 按名字清除本應用數據庫

     

     * @param context

     * @param dbName

     */ 

    public static void cleanDatabaseByName(Context context, String dbName) { 

        context.deleteDatabase(dbName); 

    

   

    /**

     * 清除/data/data/com.xxx.xxx/files下的內容

     

     * @param context

     */ 

    public static void cleanFiles(Context context) { 

        deleteFilesByDirectory(context.getFilesDir()); 

    

   

    /**

     * 清除外部cache下的內容(/mnt/sdcard/android/data/com.xxx.xxx/cache)

     

     * @param context

     */ 

    public static void cleanExternalCache(Context context) { 

        if (Environment.getExternalStorageState().equals( 

                Environment.MEDIA_MOUNTED)) { 

            deleteFilesByDirectory(context.getExternalCacheDir()); 

        

    

   

    /**

     * 清除自定義路徑下的文件,使用需當心,請不要誤刪。並且只支持目錄下的文件刪除

     

     * @param filePath

     */ 

    public static void cleanCustomCache(String filePath) { 

        deleteFilesByDirectory(new File(filePath)); 

    

   

    /**

     * 清除本應用全部的數據

     

     * @param context

     * @param filepath

     */ 

    public static void cleanApplicationData(Context context, String... filepath) { 

        cleanInternalCache(context); 

        cleanExternalCache(context); 

        cleanDatabases(context); 

        cleanSharedPreference(context); 

        cleanFiles(context); 

        for (String filePath : filepath) { 

            cleanCustomCache(filePath); 

        

    

   

    /**

     * 刪除方法 這裏只會刪除某個文件夾下的文件,若是傳入的directory是個文件,將不作處理

     

     * @param directory

     */ 

    private static void deleteFilesByDirectory(File directory) { 

        if (directory != null && directory.exists() && directory.isDirectory()) { 

            for (File item : directory.listFiles()) { 

                item.delete(); 

            

        

    

}

總結

緩存是很是有用的工具,但極易被濫用。不到最後一刻不要使用緩存,優先考慮使用其餘方式優化應用程序性能。

相關文章
相關標籤/搜索