12306に隠された制限の同時実行性についての工夫があることがわかりましたか?

12306チケットを手に入れて、極端な同時実行性について考えていますか?

休暇期間ごとに、故郷に戻って一次および二次都市に出かける人々は、ほとんど問題に直面しています。列車の切符を手に入れる!ほとんどの場合、切符は予約できますが、リリースの時点では切符はありません。 、私は皆が深く経験したと信じています。特に春節の期間中は、誰もが12306を使用するだけでなく、「Chixing」やその他のチケット取得ソフトウェアも検討します。この期間中、全国で何億人もの人々がチケットを取得しています。「12306サービス」は、世界中のインスタントキリングシステムが超えることのできないQPSを備えており、何百万もの同時実行が正常です。著者は「12306」のサーバー側アーキテクチャを特別に研究し、そのシステム設計の多くのハイライトを学びました。ここでは、例を共有してシミュレートします。100万人が10,000の列車のチケットを取得するときに通常のシステムを提供する方法同時に、安定したサービス。githubコードアドレス

1.大規模な同時実行性の高いシステムアーキテクチャ

同時実行性の高いシステムアーキテクチャは分散クラスターに展開されます。サービスの上位層にはレイヤーごとの負荷分散があり、さまざまな災害復旧方法(デュアルファイアコンピュータールーム、ノード障害耐性、サーバー災害復旧など)を提供します。 。)システムの高可用性を確保するため、トラフィックもさまざまな負荷容量に基づいており、構成戦略はさまざまなサーバーにバランスがとられています。以下は簡単な図です。

1.1ロードバランシングの概要

上の図は、サーバーへのユーザー要求が3層の負荷分散を経たことを示しています。3種類の負荷分散を以下に簡単に紹介します。

  • OSPF(Open Shortest Link First)は、Interior Gateway Protocol(IGP)です。OSPFは、ルーター間のネットワークインターフェイスの状態をアドバタイズすることでリンク状態データベースを確立し、最短パスツリーを生成します。OSPFはルーティングインターフェイスのコスト値を自動的に計算しますが、インターフェイスのコスト値を手動で指定することもできます。自動的に計算値。OSPFによって計算されるコストも、インターフェイスの帯域幅に反比例します。帯域幅が大きいほど、コスト値は小さくなります。同じコスト値でターゲットに到達するパスはロードバランシングを実行でき、最大6つのリンクが同時にロードバランシングを実行できます。
  • クラスター技術であるLVS(Linux Virtual Server)は、IP負荷分散技術とコンテンツベースの要求分散技術を採用しています。スケジューラーはスループットが高く、要求を異なるサーバーに転送してバランスの取れた方法で実行します。スケジューラーはサーバーの障害を自動的に保護し、サーバーのグループを高性能で可用性の高い仮想サーバーに形成します。
  • Nginxは誰もが知っている必要があり、非常に高性能なhttpプロキシ/リバースプロキシサーバーであり、サービス開発の負荷分散によく使用されます。Nginxが負荷分散を実現するには、ポーリング、加重ポーリング、IPハッシュポーリングの3つの主な方法があります。次に、Nginxの加重ポーリングの特別な構成とテストを行います。

1.2Nginx加重ポーリングのデモンストレーション

Nginxは、アップストリームモジュールを介して負荷分散を実装します。加重ポーリングの構成により、関連するサービスに重み値を追加できます。構成時に、サーバーのパフォーマンスと負荷容量に応じて、対応する負荷を設定できます。以下は、重み付きポーリング負荷の構成です。ポート3001〜3004をローカルでリッスンし、それぞれ1、2、3、および4の重みを構成します。

#配置负载均衡
    upstream load_rule {
       server 127.0.0.1:3001 weight=1;
       server 127.0.0.1:3002 weight=2;
       server 127.0.0.1:3003 weight=3;
       server 127.0.0.1:3004 weight=4;
    }
    ...
    server {
    listen       80;
    server_name  load_balance.com www.load_balance.com;
    location / {
       proxy_pass http://load_rule;
    }
}


复制代码

ローカルの/etc/hostsディレクトリにwww.load\_balance.comの仮想ドメイン名アドレスを設定しました。次に、Go言語を使用して4つのhttpポート監視サービスを開きました。以下はポート3001でリッスンしているGoプログラムです。ポートを変更する必要があります。

package main

import (
 "net/http"
 "os"
 "strings"
)

func main() {
 http.HandleFunc("/buy/ticket", handleReq)
 http.ListenAndServe(":3001", nil)
}

//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
 failedMsg :=  "handle in port:"
 writeLog(failedMsg, "./stat.log")
}

//写入日志
func writeLog(msg string, logPath string) {
 fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
 defer fd.Close()
 content := strings.Join([]string{msg, "\r\n"}, "3001")
 buf := []byte(content)
 fd.Write(buf)
}


复制代码

要求されたポートログ情報を./stat.logファイルに書き込み、abストレステストツールを使用してストレステストを実行しました。

ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket


复制代码

統計ログの結果によると、ポート3001〜3004はそれぞれ100、200、300、400のリクエストを受信しました。これらは、nginxで構成した重み比とよく一致しており、負荷後のトラフィックは非常に高くなっています。均一でランダム。具体的な実装については、nginxのupsteamモジュール実装のソースコードを参照できます。推奨される記事は次のとおりです。Nginxのアップストリームメカニズムの負荷分散

2.スパイク購入システムのタイプ選択

最初に述べた質問に戻ります。列車の切符のseckillシステムは、高い同時実行性の条件下で、通常の安定したサービスをどのように提供しますか?

上記の紹介から、ユーザースパイクトラフィックは、レイヤーごとの負荷分散によってさまざまなサーバーに均等に分散されることがわかります。それでも、クラスター内の単一のマシンが負担するQPSは非常に高くなります。スタンドアロンのパフォーマンスを極端に最適化するにはどうすればよいですか?この問題を解決するには、1つのことを理解する必要があります。通常、チケット予約システムは、注文の生成、在庫の差し引き、ユーザーへの支払いの3つの基本段階を処理する必要があります。システムが行う必要があるのは、列車のチケット注文を確実にすることです。販売を超えない、大量に販売する、販売された各チケットは有効であるために支払われる必要があり、システムは非常に高い同時実行性にも耐える必要があります。これらの3つの段階のシーケンスをより合理的に割り当てるにはどうすればよいですか?分析してみましょう。

2.1在庫を減らすための注文

ユーザーの同時リクエストがサーバーに到着すると、最初に注文が作成され、次に在庫が差し引かれ、ユーザーは支払いを待っています。この注文は、ほとんどの人が考える最初の解決策です。この場合、不可分操作である注文の作成後に在庫が減少するため、注文が売られ過ぎないようにすることもできます。ただし、これにはいくつかの問題も発生します。1つは、極端な同時実行の場合、メモリ操作の詳細がパフォーマンスに影響することです。特に、注文を作成するロジックは、通常、ディスクデータベースに保存する必要があります。これはデータベースに圧力をかけると考えられます。2つ目は、ユーザーが悪意を持って注文した場合、サーバーはIPの数を制限できますが、支払いを行わずに注文するだけで在庫が減り、大量の注文が販売されることです。ユーザーの発注書、これも良い方法ではありません。

2.2支払いから在庫を差し引いたもの

ユーザーが注文の支払いを待ってから在庫を減らすと、最初の感覚は売上が減ることはないということです。しかし、これは並行アーキテクチャにとって大きなタブーです。極端な同時実行の場合、ユーザーは大量の注文を作成する可能性があるためです。在庫がゼロになると、多くのユーザーは、取得した注文を支払うことができないことに気付きます。 「売られ過ぎ」と呼ばれます。また、データベースディスクIOの同時動作を回避することもできません。

2.3源泉徴収在庫

上記の2つのスキームを考慮すると、注文が作成されている限り、データベースIOを頻繁に操作する必要があると結論付けることができます。それで、在庫を差し控えているデータベースIOの直接操作を必要としないソリューションがあります。在庫が売られ過ぎないように最初に差し引かれ、次にユーザーの注文が非同期で生成されるため、ユーザーへの応答がはるかに速くなります。それでは、多くの売上を保証するにはどうすればよいでしょうか。ユーザーが注文を受け取り、支払いをしない場合はどうなりますか?注文には有効期限があることは誰もが知っています。たとえば、ユーザーが5分以内に支払いを行わないと、注文は無効になります。注文の有効期限が切れると、新しい在庫が追加されます。これは、多くのオンライン小売店で採用されているソリューションでもあります。多くの製品が販売されることを保証する会社。注文の生成は非同期であり、通常、MQやKafkaなどのリアルタイムの消費キューで処理されます。注文量が比較的少ない場合、注文の生成は非常に高速であり、ユーザーはほとんどキューに入れる必要がありません。

3.在庫を差し引く技術

上記の分析から、在庫を源泉徴収する計画が最も合理的であることが明らかです。在庫控除の詳細をさらに分析しますが、まだまだ最適化の余地はありますが、在庫はどこにありますか?高い同時実行性の下で在庫を正しく控除し、ユーザーの要求に迅速に対応するにはどうすればよいですか?

単一のマシンの同時実行性が低い場合、通常、次のように在庫を差し引きます。

在庫の差し引きと注文の生成の原子性を確保するには、トランザクション処理を使用し、在庫を判断し、在庫を減らし、最後にトランザクションを送信する必要があります。プロセス全体には多くのIOがあり、データベースの操作が必要です。ブロックされています。この方法は、同時実行性の高いスパイクシステムにはまったく適していません。

次に、単一のマシンの在庫を差し引くスキームを最適化します。つまり、在庫のローカル控除です。ローカルマシンに一定量の在庫を割り当て、メモリ内の在庫を直接減らしてから、前のロジックに従って非同期で注文を作成します。改善されたスタンドアロンシステムは次のとおりです。

このようにして、データベースでの頻繁なIO操作が回避され、操作はメモリ内でのみ実行されるため、単一のマシンの同時実行防止機能が大幅に向上します。ただし、数百万のユーザー要求がある単一のマシンは、とにかくそれに耐えることができません。nginxはepollモデルを使用してネットワーク要求を処理しますが、c10kの問題は業界ですでに解決されています。ただし、Linuxシステムでは、すべてのリソースがファイルであり、ネットワーク要求についても同じことが言えます。ファイル記述子が多数あると、オペレーティングシステムはすぐに応答を失います。上記でnginxの加重バランシング戦略について説明しましたが、100Wのユーザーリクエストボリュームは100台のサーバーに均等にバランシングされるため、1台のマシンが負担する同時実行量ははるかに少なくなります。次に、各マシンにローカルで100枚の列車の切符をストックしますが、100台のサーバーの合計在庫は10,000のままであるため、在庫注文が売られ過ぎないようになっています。説明するクラスターアーキテクチャは次のとおりです。

問題が続きます。同時実行性が高い場合、システムの高可用性を保証することはできません。100台のサーバー上の2台または3台のマシンが、同時トラフィックを処理できないなどの理由でダウンした場合。その場合、これらのサーバーでの注文は販売できなくなり、販売される注文が少なくなります。この問題を解決するには、注文量全体を統一的に管理する必要があります。これが次のフォールトトレラントソリューションです。サーバーは、ローカルで在庫を減らすだけでなく、リモートで在庫を減らす必要があります。リモート統合在庫削減操作により、マシンのダウンタイムを防ぐために、マシンの負荷に応じて各マシンに余分な「バッファインベントリ」を割り当てることができます。次のアーキテクチャ図を使用して、詳細に分析してみましょう。

Redisのパフォーマンスは非常に高く、単一マシンのQPSは10Wの同時実行に耐えることができると主張されているため、Redisを使用して統合インベントリを保存します。ローカル在庫削減後、ローカルで注文があった場合は、リモートで在庫を削減するようにredisにリクエストします。ローカル在庫削減とリモート在庫削減の両方が成功した後、チケット取得の成功を求めるプロンプトをユーザーに返します。これにより、注文が減らされないようにすることもできます。売られ過ぎ。

いずれかのマシンがダウンした場合、各マシンにはバッファの残りのチケットが予約されているため、ダウンしたマシンの残りのチケットは他のマシンで補うことができ、多くの売上を確保できます。バッファの残りの投票の適切な設定は何ですか?理論的には、設定されるバッファが多いほど、システムはダウンタイムを許容できるマシンが多くなります。ただし、バッファの設定が大きすぎると、redisにも一定の影響があります。redisインメモリデータベースの同時実行防止機能は非常に高いですが、リクエストは引き続きネットワークIOを通過します。実際、チケット取得プロセス中のredisリクエストの数は、ローカルインベントリとバッファの合計量です。ローカルインベントリが不十分な場合、システムはユーザーに直接戻るため、「売り切れ」メッセージプロンプトが表示され、インベントリの均一な控除のロジックが実行されなくなります。これにより、大量のネットワークもある程度回避されます。 redisを圧倒する要求があるため、バッファ値がどの程度設定されるかは、システムに対するアーキテクトの負荷によって異なります。真剣に検討する能力。

4.コードデモ

Go言語は、並行性のためにネイティブに設計されています。私は、go言語を使用して、単一マシンのチケット取得の特定のプロセスを示します。

4.1初期化作業

goパッケージのinit関数は、main関数の前に実行され、いくつかの準備作業は主にこの段階で行われます。システムが行う必要のある準備は、ローカルインベントリの初期化、リモートredisストレージ統合インベントリのハッシュキー値の初期化、redis接続プールの初期化です。さらに、サイズ1のint型chanを初期化する必要があります。 、目的は分散ロックの機能を実現することです。読み取り/書き込みロックを直接使用することも、redisなどの他の方法を使用してリソースの競合を回避することもできますが、チャネルを使用する方が効率的です。これがGo言語の哲学です。 :共有メモリを介して通信するのではなく、通信を介してメモリを共有します。redisライブラリはredigoを使用し、コードの実装は次のとおりです。

...
//localSpike包结构体定义
package localSpike

type LocalSpike struct {
 LocalInStock     int64
 LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和redis连接池
package remoteSpike
//远程订单存储健值
type RemoteSpikeKeys struct {
 SpikeOrderHashKey string //redis中秒杀订单hash结构key
 TotalInventoryKey string //hash结构中总订单库存key
 QuantityOfOrderKey string //hash结构中已有订单数量key
}

//初始化redis连接池
func NewPool() *redis.Pool {
 return &redis.Pool{
  MaxIdle:   10000,
  MaxActive: 12000, // max number of connections
  Dial: func() (redis.Conn, error) {
   c, err := redis.Dial("tcp", ":6379")
   if err != nil {
    panic(err.Error())
   }
   return c, err
  },
 }
}
...
func init() {
 localSpike = localSpike2.LocalSpike{
  LocalInStock:     150,
  LocalSalesVolume: 0,
 }
 remoteSpike = remoteSpike2.RemoteSpikeKeys{
  SpikeOrderHashKey:  "ticket_hash_key",
  TotalInventoryKey:  "ticket_total_nums",
  QuantityOfOrderKey: "ticket_sold_nums",
 }
 redisPool = remoteSpike2.NewPool()
 done = make(chan int, 1)
 done <- 1
}


复制代码

4.2在庫のローカル控除と在庫の統一控除

ローカル在庫控除のロジックは非常に単純です。ユーザーはそれを要求し、販売量を追加してから、販売量がローカル在庫よりも多いかどうかを比較し、ブール値を返します。

package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
 spike.LocalSalesVolume = spike.LocalSalesVolume + 1
 return spike.LocalSalesVolume < spike.LocalInStock
}


复制代码

ここでの共有データLocalSalesVolumeの操作はロックを使用して実装されますが、ローカル在庫控除と統合在庫控除は不可分操作であるため、チャネルは実装の最上位層で使用されます。これについては後で説明します。在庫操作redisの統一された控除。redisはシングルスレッドであり、そこからデータをフェッチし、データを書き込み、一連のステップを計算するプロセスを実現する必要があるため、luaスクリプトと協力してコマンドをパッケージ化する必要があります。操作のアトミック性を確認します。

package remoteSpike
......
const LuaScript = `
        local ticket_key = KEYS[1]
        local ticket_total_key = ARGV[1]
        local ticket_sold_key = ARGV[2]
        local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
        local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
  -- 查看是否还有余票,增加订单数量,返回结果值
       if(ticket_total_nums >= ticket_sold_nums) then
            return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
        end
        return 0
`
//远端统一扣库存
func (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
 lua := redis.NewScript(1, LuaScript)
 result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
 if err != nil {
  return false
 }
 return result != 0
}


复制代码

ハッシュ構造を使用して、総在庫と総売上の情報を格納します。ユーザーが要求すると、総売上が在庫よりも大きいかどうかを判断し、関連するブール値を返します。サービスを開始する前に、redisの初期インベントリ情報を初期化する必要があります。

 hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0


复制代码

4.3ユーザー情報への対応

ポートでリッスンするhttpサービスを開始します。

package main
...
func main() {
 http.HandleFunc("/buy/ticket", handleReq)
 http.ListenAndServe(":3005", nil)
}


复制代码

上記のすべての初期化作業を完了しました。次に、handleReqのロジックは非常に明確です。チケットの取得が成功したかどうかを判断し、情報をユーザーに返すだけで十分です。

package main
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
 redisConn := redisPool.Get()
 LogMsg := ""
 <-done
 //全局读写锁
 if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
  util.RespJson(w, 1,  "抢票成功", nil)
  LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
 } else {
  util.RespJson(w, -1, "已售罄", nil)
  LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
 }
 done <- 1

 //将抢票状态写入到log中
 writeLog(LogMsg, "./stat.log")
}

func writeLog(msg string, logPath string) {
 fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
 defer fd.Close()
 content := strings.Join([]string{msg, "\r\n"}, "")
 buf := []byte(content)
 fd.Write(buf)
}


复制代码

前述のように、在庫を差し引く際には競合状態を考慮する必要があります。ここでは、チャネルを使用して読み取りと書き込みの同時実行を回避し、要求の効率的かつ順次的な実行を保証します。ストレス測定の統計を容易にするために、インターフェースの戻り情報を./stat.logファイルに書き込みました。

4.4スタンドアロンサービスのストレステスト

サービスを開始するには、ab圧力テストツールを使用して以下をテストします。

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket


复制代码

以下は私のローカルローエンドマックの圧力テスト情報です

This is ApacheBench, Version 2.3 <$Revision: 1826891 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:
Server Hostname:        127.0.0.1
Server Port:            3005

Document Path:          /buy/ticket
Document Length:        29 bytes

Concurrency Level:      100
Time taken for tests:   2.339 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1370000 bytes
HTML transferred:       290000 bytes
Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
Time per request:       0.234 [ms] (mean, across all concurrent requests)
Transfer rate:          572.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8  14.7      6     223
Processing:     2   15  17.6     11     232
Waiting:        1   11  13.5      8     225
Total:          7   23  22.8     18     239

Percentage of the requests served within a certain time (ms)
  50%     18
  66%     24
  75%     26
  80%     28
  90%     33
  95%     39
  98%     45
  99%     54
 100%    239 (longest request)


复制代码

指標によると、私の1台のマシンは1秒あたり4000以上のリクエストを処理できます。通常のサーバーはすべてマルチコア構成であり、1W以上のリクエストを処理するのに問題はありません。また、ログを見ると、サービスプロセス全体を通じて、リクエストは正常であり、トラフィックは均一であり、redisも正常であることがわかります。

//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...


复制代码

5.要約レビュー

全体として、seckillシステムは非常に複雑です。ここでは、単一のマシンを高性能に最適化する方法、クラスターが単一障害点を回避する方法、および注文が売られ過ぎたり大量に売られたりしないようにする方法を簡単に紹介してシミュレートします。残りのマシンを定期的に同期するタスクがあります。総在庫からの請求書と在庫情報をユーザーに表示します。ユーザーは注文の有効期間内に支払いを行わず、注文をリリースし、在庫を補充します。

同時実行性の高いチケットグラブのコアロジックを実装しました。システム設計は非常に巧妙であると言えます。DBデータベースIOの操作や、RedisネットワークIOの同時要求が多いことを巧みに回避します。ほとんどすべての計算はメモリ内にあります。完成し、売られ過ぎや大量販売がないことを効果的に保証し、一部のマシンのダウンタイムにも耐えることができます。そのうちの2つは、特に学習して要約する価値があると思います。

  • 負荷分散、分割統治。負荷分散により、さまざまなトラフィックがさまざまなマシンに分割されます。各マシンは独自のリクエストを処理し、パフォーマンスを最大化するため、システム全体が、チームで作業するのと同じように、非常に高い同時実行性に耐えることができます。極端であり、チームの成長は当然素晴らしいです。
  • 並行性と非同期性の合理的な使用。epollネットワークアーキテクチャモデルがc10k問題を解決して以来、非同期はサーバー開発者にますます受け入れられるようになりました。非同期で実行できる作業は非同期で実行できるため、機能の分解で予期しない結果が生じる可能性があります。nginxに反映できます。 node.jsとredis。ネットワークリクエストの処理に使用するepollモデルは、単一のスレッドが依然として強力な力を発揮できることを示しています。サーバーはマルチコア時代に突入しました。並行性のために生まれた言語であるgo言語は、サーバーのマルチコアの利点を完全に活用しています。同時に処理できる多くのタスクは、並行性によって解決できます。 goはhttpリクエストを処理し、各リクエストはゴルーチンで実行されます。つまり、CPUを合理的にスクイーズし、その正当な価値を再生させる方法は、常に調査および学習する必要のある方向です。


著者:
公式Account_ITブラザーリンク:https://juejin.cn/post/7020215813969805343
出典: RareEarthNuggets
著作権は著者に帰属します。商用の再版については、著者に連絡して許可を求め、非商用の再版については、出典を示してください。

おすすめ

転載: blog.csdn.net/wdjnb/article/details/124363781