「12306」はどのようにQPSの百万をサポートすることですか?

出典:ナゲッツ

著者:塗装あなたがI、アリュール

リンク:https://juejin.im/post/5d84e21f6fb9a06ac8248149

 

 

アートスパイクシステム

 

12306件のグラブの投票は、制限の同時実行性をもたらすことを考えて?

帰宅するすべての休暇期間、第二層の都市、人々が遊びに行く、ほとんどすべての問題に直面:グラブのチケットを今ではほとんどの状況下でチケットの予約が、チケットはすぐにチケットなしで場面を置くことができるけれども、私はみんなを信じて!すべてあまりにもよく。特に、春祭りの間に、我々は唯一、票をつかむために、「カイ」と他のソフトウェアとみなされます12306を使用して、全国の何億人もの人々が、この時点でのグラブの投票です。「12306サービス」この世界で任意のスパイクシステム卓越したQPS、同時より通常の数百万人に苦しみます!私はここで、自分のシステムの設計上の明るいスポットの多くを学ぶ、少し特別な「12306」サーバー側のアーキテクチャを研究し、あなたと共有し、例をシミュレート:億で万枚の券ながらつかむためにどのように、システムは通常の提供します、安定したサービス。githubのコードアドレス

1.大高い並行処理システムのアーキテクチャ

並行性の高いシステム・アーキテクチャは、サービスが負荷分散、災害復旧の上位層を有している、分散型クラスタ配置を採用し、様々な手段(ツインエンジンルームの火災、フォールトトレラントノード、サーバー、災害復旧など)、システムの高可用性を確保するために提供されます、トラフィックは上のベースとなります別のサーバーにバランスが異なる負荷容量および構成の方針。以下は、簡単な図です。

負荷分散1.1はじめに

上の図は、サーバーへのユーザー要求は負荷分散の3層を貫通して、次の3つのロードバランシングを簡単に紹介されてしまったについて説明します。

  • OSPF(オープンショー優先リンク)はIGP(インテリアゲートウェイプロトコル、IGPと呼ばれる)です。OSPFは、ルータ広告リンク状態データベースとの間のネットワークインタフェースの状態によって確立され、最短経路ツリーを生成するために、OSPFは自動的に値Costルーティングインタフェースを計算し、それが手でインターフェイスの値のコストを指定することができる、手動で割り当てられた優先順位値が自動的に計算されます。コスト計算OSPF、同じインターフェイス、より高い帯域幅、小さい値コストの帯域幅に反比例します。目標値への同じパスをコスト、あなたは、同時に最大6つのリンクがロードバランシングを実行、負荷分散を行うことができます。
  • IP負荷分散技術とコンテンツベースの要求配信技術を使用してクラスタ(クラスタ)技術であり、LVS(Linuxの仮想サーバー)、。スケジューラは、良好なスループット、実行する別のサーバーにバランス転送要求を有し、スケジューラは自動的にマスクされ、サーバに失敗し、それによってサーバのグループを構成する高性能、高可用性の仮想サーバです。
  • nginxのは確かに我々はすべてに精通している、リバース・プロキシ・サーバー/非常に高性能のHTTPプロキシで、サービス開発、多くの場合、負荷分散のためにそれを使用。nginxのは、バランスの取れた負荷を達成三つの主要な方法がありますポーリング、加重ラウンドロビン、IPハッシュ世論調査では、ここではnginxのための特別な設定や重み付けラウンドロビンテストを行う必要があります

1.2 nginxの加重ポーリングデモ

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で構成され、4つのHTTPポートリスニングサービスを開くために行くの言語を使用して、以下に続くが、リスナーポートは3001でプログラムを行くである、いくつかは他のみ変更する必要がありますポート:

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) } 复制代码

ポートIは、要求されたログ情報を./stat.logがそれらを提出した後、AB圧力測定圧力測定を行うためのツールを使用して書きました:

ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket
复制代码

トラフィック負荷が非常にされた後、統計ログの結果、3001から3004個のまでのポートが要求されている金額は、権利である400、300だった、と私は一緒によくフィット比例してnginxの重い構成され、ランダム均一。私たちは、nginxのソースのupsteamモジュールの特定の実装を参照することができ、私はここの記事をお勧めします。負荷が上流のメカニズムにnginxのバランスをとります

2.スパイクラッシュシステムの選択

戻る過去に言及した私達の元の質問に:高い同時実行で、通常の列車のチケットスパイクシステム、安定したサービスを提供するために、どのように?

上記の説明から、我々は、ロードバランシングの層を介してユーザスパイクの流れを知って、でも別のサーバーに、そうであっても、QPSを負担するスタンドアローンのクラスターが非常に高いです。それを最大化するために、スタンドアローンのパフォーマンスを最適化するには?この問題を解決するために、我々は一つのことを理解しなければならない:通常生成注文を処理するために、予約システム、在庫控除を、あなたはそのチケットの注文を確保するために行うには、当社のシステムの3つの基本的な段階をさ支払う超えません販売し、多くを販売し、各販売チケットは、唯一の有効な支払わなければならないが、また、非常に高い同時実行に耐えられるようにシステムを確保するために。?我々はそれがより合理的に分析するために割り当てませんどのようにこれらの三つの段階の順序を変更します。

2.1受注マイナス在庫

ユーザーの同時要求がサーバーに到達すると、まず、ユーザーが支払うことを待って、在庫を差し引く、注文を作成します。この順序は、我々が原因アトミック操作で、在庫を削減するために、作成した後、売られ過ぎれることはありません順序を保証することができ、この場合には、我々は一般的に解決策を考える最初の人、です。それはまた、問題の原因となり、最初は、メモリ動作のいずれかの詳細は、特に、この論理順序を作成するように、一般的に、ディスクのデータベースに格納されるデータベースの圧力を必要とする、性能に重大な影響である限界同時場合であります想像することができ、2番目は、悪意のあるユーザの存在である、そのような株式を支払うだけでなく、受注が少なくなり、単一の場合ならば、それは購入注文の数と、サーバがユーザのIPを制限することができますが少なく、受注の多くを販売するだろう、これは良い方法とはみなされません。

2.2賃金カットの在庫

あなたは、ユーザーの注文マイナス在庫を支払うために待っていれば、第一印象はあまり売っていません。しかし、これは「売られ過ぎ」と呼ばれる、限界同時の場合には、ユーザーが注文の多くを作成する可能性があるため、多くのユーザーが注文をつかむために見つけたときにゼロに減少し、在庫が支払うことができない、タブーのアーキテクチャによって複雑になります。あなたは、データベースのディスクIOの同時動作を回避することはできません

2.3源泉徴収株式

上から二つのプログラムを考えてみましょう、我々はそれを締結することができます限り、順序が作成されると、頻繁にデータベースIOを動作させる必要があります。在庫を源泉徴収されたIOプログラムがそれを必要としないデータベースの直接操作があります。在庫の控除前、その後、非同期顧客の注文を生成し、売られ過ぎではない保証するために、そんなに速く、ユーザーへの応答率は、その後、どのようにそれを販売する多くのことを確実にするために?ユーザーは、オーダーを取得し、あなたが行う方法を払っていませんか?私たちはすべての注文が有効であることを知って、例えば、ユーザが、5分以内に支払わない、順序が無効になり、かつて過ごした注文は、オンライン小売業者の多くは、プログラムが使用することを確実にするために品物をたくさん売って、今で新株を追加します。注文を生成することは非常に迅速に注文を生成する、それはほとんど並んでいない、通常MQキューカフカ即時消費このような処理、受注よりも少ない場合に、非同期です。

在庫3.アートバックル

上記の分析から明らかに在庫プログラムに最も合理的な源泉徴収ことを示しています。当社株式が存在し、最適化の余地がまだある分析バックル在庫、の詳細?高い同時実行、正しい控除の在庫だけでなく、ユーザの要求に迅速な応答を確保するには?

スタンドアロンの低並行処理では、我々はバックル在庫は通常、このようなものです実現します。

アトミックバックル在庫や生産受注を確保するために、我々はトランザクションを必要とし、その後、在庫裁判官、在庫削減を取り、最後にトランザクションをコミットし、全体のプロセスは、IOをたくさん持っている、データベースの操作がブロックされています。この方法では、高度に並行システムのスパイクには適していません。

:次は、単一のバックル原液の最適化を行うローカルバックルの株式を私たちは、ローカルマシンに在庫の一定量を割り当てるメモリに直接、在庫を削減し、その後、前の非同期論理に従って順序を作成します。スタンドアローンシステムは、このような何かの後に改善しました。

这样就避免了对数据库频繁的IO操作,只在内存中做运算,极大的提高了单机抗并发的能力。但是百万的用户请求量单机是无论如何也抗不住的,虽然nginx处理网络请求使用epoll模型,c10k的问题在业界早已得到了解决。但是linux系统下,一切资源皆文件,网络请求也是这样,大量的文件描述符会使操作系统瞬间失去响应。上面我们提到了nginx的加权均衡策略,我们不妨假设将100W的用户请求量平均均衡到100台服务器上,这样单机所承受的并发量就小了很多。然后我们每台机器本地库存100张火车票,100台服务器上的总库存还是1万,这样保证了库存订单不超卖,下面是我们描述的集群架构:

问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这100台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“buffer库存”用来防止机器中有机器宕机的情况。我们结合下面架构图具体分析一下:

我们采用Redis存储统一库存,因为Redis的性能非常高,号称单机QPS能抗10W的并发。在本地减库存以后,如果本地有订单,我们再去请求redis远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。当机器中有机器宕机时,因为每个机器上有预留的buffer余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。buffer余票设置多少合适呢,理论上buffer设置的越多,系统容忍宕机的机器数量就越多,但是buffer设置的太大也会对redis造成一定的影响。虽然redis内存数据库抗并发能力非常高,请求依然会走一次网络IO,其实抢票过程中对redis的请求次数是本地库存和buffer库存的总量,因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑,这在一定程度上也避免了巨大的网络请求量把redis压跨,所以buffer值设置多少,需要架构师对系统的负载能力做认真的考量。

4. 代码演示

Go语言原生为并发设计,我采用go语言给大家演示一下单机抢票的具体流程。

4.1 初始化工作

go包中的init函数先于main函数执行,在这个阶段主要做一些准备性工作。我们系统需要做的准备工作有:初始化本地库存、初始化远程redis存储统一库存的hash键值、初始化redis连接池;另外还需要初始化一个大小为1的int类型chan,目的是实现分布式锁的功能,也可以直接使用读写锁或者使用redis等其他的方式避免资源竞争,但使用channel更加高效,这就是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 本地扣库存和统一扣库存

本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回bool值:

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

注意这里对共享数据LocalSalesVolume的操作是要使用锁来实现的,但是因为本地扣库存和统一扣库存是一个原子性操作,所以在最上层使用channel来实现,这块后边会讲。统一扣库存操作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 } 复制代码

我们使用hash结构存储总库存和总销量的信息,用户请求过来时,判断总销量是否大于库存,然后返回相关的bool值。在启动服务之前,我们需要初始化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) } 复制代码

前边提到我们扣库存时要考虑竞态条件,我们这里是使用channel避免并发的读写,保证了请求的高效顺序执行。我们将接口的返回信息写入到了./stat.log文件方便做压测统计。

4.4 单机服务压测

开启服务,我们使用ab压测工具进行测试:

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket
复制代码

下面是我本地低配mac的压测信息

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)
复制代码

根据指标显示,我单机每秒就能处理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.总结回顾

总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略,完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。

我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对DB数据库IO的操作,对Redis网络IO的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。我觉得其中有两点特别值得学习总结:

  • 负载均衡,分而治之。通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致,这样系统的整体也就能承受极高的并发了,就像工作的的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。
  • 合理的使用并发和异步。自epoll网络架构模型解决了c10k问题以来,异步越来被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果,这点在nginx、node.js、redis上都能体现,他们处理网络请求使用的epoll模型,用实践告诉了我们单线程依然可以发挥强大的威力。服务器已经进入了多核时代,go语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如go处理http请求时每个请求都会在一个goroutine中执行,总之:怎样合理的压榨CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。

 

おすすめ

転載: www.cnblogs.com/xiang--liu/p/11670887.html