通常、Web システムのスループット レートは QPS (Query Per Second、1 秒あたりに処理されるリクエストの数) によって測定されます。この指標は、1 秒あたり数万回の同時実行性の高いシナリオを解決するために非常に重要です。たとえば、ビジネス リクエストを処理するための平均応答時間が 100 ミリ秒であると仮定します。同時に、システム内に 20 台の Apache Web サーバーがあり、MaxClients が 500 (Apache 接続の最大数を示す) に設定されているとします。
つまり、私たちの Web システムの理論上のピーク QPS は (理想的な計算方法):
20*500/0.1 = 100000 (100,000 QPS)
え?私たちのシステムは非常に強力なようで、1 秒間に 100,000 件のリクエストを処理できます。5w/s のフラッシュセールは「紙の虎」のようです。もちろん、実際の状況はそれほど理想的ではありません。実際の同時実行性の高いシナリオでは、マシンは高負荷にさらされており、この時点で平均応答時間は大幅に増加します。
Web サーバーに関する限り、Apache が開く接続プロセスが増えるほど、CPU が処理する必要があるコンテキスト スイッチが増え、CPU 消費量が増加し、平均応答時間の増加に直接つながります。したがって、上記の MaxClient の数は、CPU やメモリなどのハードウェア要因に基づいて考慮する必要があります。多いほど良いというわけではありません。 Apache 独自のアベンチを通じてテストし、適切な値を取得できます。次に、メモリ操作レベルでのストレージとして Redis を選択します。同時実行性が高い状態では、ストレージの応答時間が重要になります。ネットワーク帯域幅も要因ですが、このような要求パケットは一般に比較的小さいため、要求のボトルネックになることはほとんどありません。負荷分散がシステムのボトルネックになることはほとんどないため、ここでは説明しません。
次に問題は、私たちのシステムが 5w/s の高い同時実行状態にあると仮定すると、平均応答時間が 100 ミリ秒から 250 ミリ秒に変化する (実際の状況ではさらに多くなります):
20*500/0.25 = 40000 (4) 10,000 QPS)
つまり、1 秒あたり 50,000 のリクエストに直面すると、システムには 40,000 QPS が残ることになります。その差は 10,000 QPS です。
例えば、高速道路の交差点では、1秒間に5台の車が行き来し、高速道路の交差点は通常通り動作します。突然、この交差点は1秒間に4台しか通過できなくなり、交通量は変わらないため、間違いなく渋滞が発生します。 (5レーンが突然4レーンになったような感じです)
同様に、ある秒間では20*500の利用可能な接続プロセスがフル稼働していますが、まだ10,000の新規リクエストがあり、利用可能な接続プロセスが存在しないことが予想されます。システムが異常な状態に陥ります。
実際、同時実行性が高くない通常のビジネス シナリオでも、特定のビジネス リクエスト インターフェイスに問題があり、Web リクエスト全体の応答時間が非常に遅くなることがあります。が非常に長く、Web は徐々に減少し、サーバー上で使用可能な接続の数がいっぱいになり、他の通常のビジネス リクエストに使用できる接続プロセスがなくなりました。
さらに恐ろしい問題は、ユーザーの行動特性です。システムが利用できなくなると、ユーザーがクリックする頻度が高まり、最終的には「雪崩」が発生します (Web マシンの 1 つがハングアップし、トラフィックが停止します)。他の正常に動作しているマシンに分散され、通常のマシンもハングアップし、Web システム全体がダウンするという悪循環が発生します。
3. 再起動と過負荷保護
システムで「雪崩」が発生した場合、むやみにサービスを再起動しても問題は解決しません。最も多い現象は、起動してもすぐにハングアップしてしまうことです。現時点では、イングレス層でトラフィックを拒否してから再起動することをお勧めします。 redis/memcacheなどのサービスもダウンしている場合は、再起動時の「ウォームアップ」に注意が必要で、時間がかかる場合があります。
フラッシュ セールやラッシュ セールのシナリオでは、トラフィックがシステムの準備や想像を超えることがよくあります。このとき、過負荷保護が必要です。リクエストの拒否は、システムのフル負荷状態が検出された場合の保護手段でもあります。フロントエンドでフィルタリングを設定するのが最も簡単な方法ですが、このアプローチはユーザーから「批判」される動作です。より適切なのは、顧客からの直接リクエストを迅速に返すために、CGI エントリー層で過負荷保護を設定することです
高い同時実行下でのデータセキュリティ
複数のスレッドが同じファイルに書き込む場合、「スレッド」は「安全」として表示されることがわかっています。問題 (複数のスレッドが同じコード部分を同時に実行します。各実行の結果が単一スレッドの実行の結果と同じで、結果が期待どおりであれば、スレッドセーフです)。 MySQL データベースの場合は、独自のロック メカニズムを使用して問題を解決できます。ただし、大規模な同時実行シナリオでは、MySQL はお勧めできません。フラッシュ セールやラッシュ セールのシナリオでは、「過剰送信」という別の問題が発生します。この点を慎重に制御しないと、過剰な送信が発生します。一部の電子商取引会社では、購入者が写真撮影に成功した後、販売者が注文が有効であると認識せず、商品の配送を拒否する行為を行っていると聞いています。ここでの問題は、必ずしも販売業者が不正であるということではなく、システムの技術レベルでの過剰発行のリスクによって引き起こされる可能性があります。
1. 過剰な毛の成長の理由
ある急ぎ購入シナリオで、合計 100 個の製品しかなく、最後の瞬間に 99 個の製品を消費し、最後の 1 個だけが残ったとします。このとき、システムは複数のリクエストを同時に送信し、それらのリクエストによって読み取られた商品残高はすべて 99 であり、すべてがこの残高判定を通過し、最終的に過剰発行につながりました。 (記事内で前述したシーンと同じです)
上の図では、同時ユーザー B も「購入に成功」し、さらに 1 人が商品を入手できるようになりました。このシナリオは、同時実行性が高い状況で非常に発生しやすくなります。
最適化計画 1: 在庫フィールド番号フィールドを unsigned に設定します。在庫が 0 の場合、フィールドは負の数値にすることができないため、false が返されます
<?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。解決すべきアイデアはたくさんあります。スレッド セーフティ、から始めることができます。 「悲観的ロック」の方向性が議論され始めました。
悲観的ロック、つまり、データを変更するときに、外部リクエストからの変更を除外するためにロック状態が採用されます。ロック状態になった場合は、待機する必要があります。
上記の解決策はスレッドの安全性の問題を解決しますが、私たちのシナリオは「高い同時実行性」であることを忘れないでください。言い換えれば、そのような変更リクエストが多数あり、各リクエストは「ロック」を待つ必要があるため、一部のスレッドはこの「ロック」を取得する機会がなく、そのようなリクエストはそこで終了します。同時に、そのようなリクエストが多数発生し、システムの平均応答時間が即座に増加します。その結果、使用可能な接続の数が枯渇し、システムが例外に陥ります。
最適化計画 2: MySQL トランザクションを使用して操作の行をロックする
<?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) を使用してみましょう。出力、先入れ先出し)、この場合、一部のリクエストがロックを取得しないことはありません。これを見ると、マルチスレッドを強制的にシングルスレッドにするような感じでしょうか?
これでロックの問題は解決され、すべてのリクエストは「先入れ先出し」キューで処理されます。さらに、同時実行性が高いシナリオでは、リクエストが多数あるため、キュー メモリが瞬時に「爆発」し、システムが再び異常状態に陥る可能性があります。または、巨大なメモリ キューを設計することも解決策です。ただし、システムがキュー内のリクエストを処理する速度は、キューへの異常な流入の数と比較することはできません。つまり、キュー内のリクエストの数はますます蓄積され、最終的には Web システムの平均応答時間は依然として大幅に低下し、システムは依然として例外に陥ってしまいます。
4. ファイルロックの考え方
1 日あたりの IP が高くない、または同時実行数がそれほど多くないアプリケーションの場合、通常、これを考慮する必要はありません。通常のファイル操作方法では全く問題ありません。しかし、同時実行性が高い場合、ファイルの読み書き時に複数のプロセスが次のファイルに対して動作する可能性が高く、その時点でファイルへのアクセスが排他的でない場合、データ損失が発生しやすくなります
。プラン 4: ノンブロッキング ファイル排他ロックを使用する
<?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. 楽観的ロックのアイデア
この時点で、「楽観的ロック」のアイデアについて議論できます。楽観的ロックは「悲観的ロック」に比べて緩いロック機構を採用しており、その多くはバージョンアップを利用します。実装では、このデータに対するすべてのリクエストが変更可能ですが、データのバージョン番号は一貫したバージョン番号を持つもののみが正常に取得され、その他のリクエストは失敗に返されます。この場合、キューの問題を考慮する必要はありませんが、CPU の計算オーバーヘッドが増加します。ただし、全体的には、これはより良い解決策です。
「楽観的ロック」機能をサポートするソフトウェアやサービスは数多くあり、Redis の watch もその 1 つです。この実装により、データのセキュリティが確保されます。
最適化計画 5: Redis で見る
<?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 class="brush:php;toolbar:false">"; 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 class="brush:php;toolbar:false">"; // 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 class="brush:php;toolbar:false">"; var_dump($mywatchlist); }else{ $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao'); echo "手气不好,再抢购!";exit; } } ?>
以上がフラッシュセールのアイデアと高同時実行下でのデータセキュリティを分析するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。