溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

12306的架構有哪些優(yōu)點

發(fā)布時間:2021-10-20 15:48:58 來源:億速云 閱讀:184 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“12306的架構有哪些優(yōu)點”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“12306的架構有哪些優(yōu)點”吧!

如何在 100 萬人同時搶 1 萬張火車票時,系統(tǒng)提供正常、穩(wěn)定的服務。

Github代碼地址:

https://github.com/GuoZhaoran/spikeSystem
大型高并發(fā)系統(tǒng)架構

高并發(fā)的系統(tǒng)架構都會采用分布式集群部署,服務上層有著層層負載均衡,并提供各種容災手段(雙火機房、節(jié)點容錯、服務器災備等)保證系統(tǒng)的高可用,流量也會根據(jù)不同的負載能力和配置策略均衡到不同的服務器上。

下邊是一個簡單的示意圖:

12306的架構有哪些優(yōu)點

負載均衡簡介

上圖中描述了用戶請求到服務器經(jīng)歷了三層的負載均衡,下邊分別簡單介紹一下這三種負載均衡。

①OSPF(開放式最短鏈路優(yōu)先)是一個內(nèi)部網(wǎng)關協(xié)議(Interior Gateway Protocol,簡稱 IGP)

OSPF 通過路由器之間通告網(wǎng)絡接口的狀態(tài)來建立鏈路狀態(tài)數(shù)據(jù)庫,生成最短路徑樹,OSPF 會自動計算路由接口上的 Cost 值。

但也可以通過手工指定該接口的 Cost 值,手工指定的優(yōu)先于自動計算的值。

OSPF 計算的 Cost,同樣是和接口帶寬成反比,帶寬越高,Cost 值越小。

到達目標相同 Cost 值的路徑,可以執(zhí)行負載均衡,最多 6 條鏈路同時執(zhí)行負載均衡。

②LVS (Linux Virtual Server)

它是一種集群(Cluster)技術,采用 IP 負載均衡技術和基于內(nèi)容請求分發(fā)技術。

調(diào)度器具有很好的吞吐率,將請求均衡地轉(zhuǎn)移到不同的服務器上執(zhí)行,且調(diào)度器自動屏蔽掉服務器的故障,從而將一組服務器構成一個高性能的、高可用的虛擬服務器。

③Nginx

想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服務器,服務開發(fā)中也經(jīng)常使用它來做負載均衡。

Nginx 實現(xiàn)負載均衡的方式主要有三種:

  • 輪詢

  • 加權輪詢

  • IP Hash 輪詢

下面我們就針對 Nginx 的加權輪詢做專門的配置和測試。

Nginx 加權輪詢的演示

Nginx 實現(xiàn)負載均衡通過 Upstream 模塊實現(xiàn),其中加權輪詢的配置是可以給相關的服務加上一個權重值,配置的時候可能根據(jù)服務器的性能、負載能力設置相應的負載。

下面是一個加權輪詢負載的配置,我將在本地的監(jiān)聽 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 語言開啟四個 HTTP 端口監(jiān)聽服務,下面是監(jiān)聽在 3001 端口的 Go 程序,其他幾個只需要修改端口即可:

package mainimport (
    "net/http"    "os"    "strings")func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3001", nil)
}//處理請求函數(shù),根據(jù)請求將響應結果信息寫入日志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, "rn"}, "3001")
    buf := []byte(content)
    fd.Write(buf)
}

我將請求的端口日志信息寫到了 ./stat.log 文件當中,然后使用 AB 壓測工具做壓測:

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

統(tǒng)計日志中的結果,3001-3004 端口分別得到了 100、200、300、400 的請求量。

這和我在 Nginx 中配置的權重占比很好的吻合在了一起,并且負載后的流量非常的均勻、隨機。

秒殺搶購系統(tǒng)選型

回到我們最初提到的問題中來:火車票秒殺系統(tǒng)如何在高并發(fā)情況下提供正常、穩(wěn)定的服務呢?

從上面的介紹我們知道用戶秒殺流量通過層層的負載均衡,均勻到了不同的服務器上,即使如此,集群中的單機所承受的 QPS 也是非常高的。

如何將單機性能優(yōu)化到極致呢?

要解決這個問題,我們就要想明白一件事:通常訂票系統(tǒng)要處理生成訂單、減扣庫存、用戶支付這三個基本的階段。

我們系統(tǒng)要做的事情是要保證火車票訂單不超賣、不少賣,每張售賣的車票都必須支付才有效,還要保證系統(tǒng)承受極高的并發(fā)。

這三個階段的先后順序該怎么分配才更加合理呢?我們來分析一下:

下單減庫存

12306的架構有哪些優(yōu)點

如果等待用戶支付了訂單在減庫存,第一感覺就是不會少賣。

但是這是并發(fā)架構的大忌,因為在極限并發(fā)情況下,用戶可能會創(chuàng)建很多訂單。

當庫存減為零的時候很多用戶發(fā)現(xiàn)搶到的訂單支付不了了,這也就是所謂的“超賣”。也不能避免并發(fā)操作數(shù)據(jù)庫磁盤 IO。

預扣庫存

12306的架構有哪些優(yōu)點

為了保證扣庫存和生成訂單的原子性,需要采用事務處理,然后取庫存判斷、減庫存,最后提交事務,整個流程有很多 IO,對數(shù)據(jù)庫的操作又是阻塞的。

這種方式根本不適合高并發(fā)的秒殺系統(tǒng)。接下來我們對單機扣庫存的方案做優(yōu)化:本地扣庫存。

我們把一定的庫存量分配到本地機器,直接在內(nèi)存中減庫存,然后按照之前的邏輯異步創(chuàng)建訂單。

改進過之后的單機系統(tǒng)是這樣的:

12306的架構有哪些優(yōu)點

問題接踵而至,在高并發(fā)情況下,現(xiàn)在我們還無法保證系統(tǒng)的高可用,假如這 100 臺服務器上有兩三臺機器因為扛不住并發(fā)的流量或者其他的原因宕機了。

那么這些服務器上的訂單就賣不出去了,這就造成了訂單的少賣。

要解決這個問題,我們需要對總訂單量做統(tǒng)一的管理,這就是接下來的容錯方案。

服務器不僅要在本地減庫存,另外要遠程統(tǒng)一減庫存。

有了遠程統(tǒng)一減庫存的操作,我們就可以根據(jù)機器負載情況,為每臺機器分配一些多余的“Buffer 庫存”用來防止機器中有機器宕機的情況。

我們結合下面架構圖具體分析一下:

image

我們采用 Redis 存儲統(tǒng)一庫存,因為 Redis 的性能非常高,號稱單機 QPS 能抗 10W 的并發(fā)。

在本地減庫存以后,如果本地有訂單,我們再去請求 Redis 遠程減庫存,本地減庫存和遠程減庫存都成功了,才返回給用戶搶票成功的提示,這樣也能有效的保證訂單不會超賣。

當機器中有機器宕機時,因為每個機器上有預留的 Buffer 余票,所以宕機機器上的余票依然能夠在其他機器上得到彌補,保證了不少賣。

Buffer 余票設置多少合適呢,理論上 Buffer 設置的越多,系統(tǒng)容忍宕機的機器數(shù)量就越多,但是 Buffer 設置的太大也會對 Redis 造成一定的影響。

雖然 Redis 內(nèi)存數(shù)據(jù)庫抗并發(fā)能力非常高,請求依然會走一次網(wǎng)絡 IO,其實搶票過程中對 Redis 的請求次數(shù)是本地庫存和 Buffer 庫存的總量。

因為當本地庫存不足時,系統(tǒng)直接返回用戶“已售罄”的信息提示,就不會再走統(tǒng)一扣庫存的邏輯。

這在一定程度上也避免了巨大的網(wǎng)絡請求量把 Redis 壓跨,所以 Buffer 值設置多少,需要架構師對系統(tǒng)的負載能力做認真的考量。

代碼演示:

Go 語言原生為并發(fā)設計,我采用 Go 語言給大家演示一下單機搶票的具體流程。

初始化工作

Go 包中的 Init 函數(shù)先于 Main 函數(shù)執(zhí)行,在這個階段主要做一些準備性工作。

我們系統(tǒng)需要做的準備工作有:初始化本地庫存、初始化遠程 Redis 存儲統(tǒng)一庫存的 Hash 鍵值、初始化 Redis 連接池。

另外還需要初始化一個大小為 1 的 Int 類型 Chan,目的是實現(xiàn)分布式鎖的功能。

也可以直接使用讀寫鎖或者使用 Redis 等其他的方式避免資源競爭,但使用 Channel 更加高效,這就是 Go 語言的哲學:不要通過共享內(nèi)存來通信,而要通過通信來共享內(nèi)存。

Redis 庫使用的是 Redigo,下面是代碼實現(xiàn):

...//localSpike包結構體定義package localSpiketype 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結構中已有訂單數(shù)量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}

本地扣庫存和統(tǒng)一扣庫存

本地扣庫存邏輯非常簡單,用戶請求過來,添加銷量,然后對比銷量是否大于本地庫存,返回 Bool 值:

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

注意這里對共享數(shù)據(jù) LocalSalesVolume 的操作是要使用鎖來實現(xiàn)的,但是因為本地扣庫存和統(tǒng)一扣庫存是一個原子性操作,所以在最上層使用 Channel 來實現(xiàn),這塊后邊會講。

統(tǒng)一扣庫存操作 Redis,因為 Redis 是單線程的,而我們要實現(xiàn)從中取數(shù)據(jù),寫數(shù)據(jù)并計算一些列步驟,我們要配合 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))
        -- 查看是否還有余票,增加訂單數(shù)量,返回結果值
       if(ticket_total_nums >= ticket_sold_nums) then            return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
        end
        return 0`
//遠端統(tǒng)一扣庫存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

響應用戶信息

我們開啟一個 HTTP 服務,監(jiān)聽在一個端口上:

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

上面我們做完了所有的初始化工作,接下來 handleReq 的邏輯非常清晰,判斷是否搶票成功,返回給用戶信息就可以了。

package main//處理請求函數(shù),根據(jù)請求將響應結果信息寫入日志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    //將搶票狀態(tài)寫入到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, "rn"}, "")
    buf := []byte(content)
    fd.Write(buf)
}

前邊提到我們扣庫存時要考慮競態(tài)條件,我們這里是使用 Channel 避免并發(fā)的讀寫,保證了請求的高效順序執(zhí)行。

我們將接口的返回信息寫入到了 ./stat.log 文件方便做壓測統(tǒng)計。

單機服務壓測

開啟服務,我們使用 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 requestsCompleted 2000 requestsCompleted 3000 requestsCompleted 4000 requestsCompleted 5000 requestsCompleted 6000 requestsCompleted 7000 requestsCompleted 8000 requestsCompleted 9000 requestsCompleted 10000 requestsFinished 10000 requestsServer Software:Server Hostname:        127.0.0.1Server Port:            3005Document Path:          /buy/ticketDocument Length:        29 bytesConcurrency Level:      100Time taken for tests:   2.339 secondsComplete requests:      10000Failed requests:        0Total transferred:      1370000 bytesHTML transferred:       290000 bytesRequests 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)

根據(jù)指標顯示,我單機每秒就能處理 4000+ 的請求,正常服務器都是多核配置,處理 1W+ 的請求根本沒有問題。

而且查看日志發(fā)現(xiàn)整個服務過程中,請求都很正常,流量均勻,Redis 也很正常:

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

總結回顧

總體來說,秒殺系統(tǒng)是非常復雜的。

我們這里只是簡單介紹模擬了一下單機如何優(yōu)化到高性能,集群如何避免單點故障,保證訂單不超賣、不少賣的一些策略。

完整的訂單系統(tǒng)還有訂單進度的查看,每臺服務器上都有一個任務,定時的從總庫存同步余票和庫存信息展示給用戶,還有用戶在訂單有效期內(nèi)不支付,釋放訂單,補充到庫存等等。

我們實現(xiàn)了高并發(fā)搶票的核心邏輯,可以說系統(tǒng)設計的非常的巧妙,巧妙的避開了對 DB 數(shù)據(jù)庫 IO 的操作。

對 Redis 網(wǎng)絡 IO 的高并發(fā)請求,幾乎所有的計算都是在內(nèi)存中完成的,而且有效的保證了不超賣、不少賣,還能夠容忍部分機器的宕機。

我覺得其中有兩點特別值得學習總結:

①負載均衡,分而治之

通過負載均衡,將不同的流量劃分到不同的機器上,每臺機器處理好自己的請求,將自己的性能發(fā)揮到極致。

這樣系統(tǒng)的整體也就能承受極高的并發(fā)了,就像工作的一個團隊,每個人都將自己的價值發(fā)揮到了極致,團隊成長自然是很大的。

②合理的使用并發(fā)和異步

自 Epoll 網(wǎng)絡架構模型解決了 c10k 問題以來,異步越來越被服務端開發(fā)人員所接受,能夠用異步來做的工作,就用異步來做,在功能拆解上能達到意想不到的效果。

這點在 Nginx、Node.JS、Redis 上都能體現(xiàn),他們處理網(wǎng)絡請求使用的 Epoll 模型,用實踐告訴了我們單線程依然可以發(fā)揮強大的威力。

服務器已經(jīng)進入了多核時代,Go 語言這種天生為并發(fā)而生的語言,完美的發(fā)揮了服務器多核優(yōu)勢,很多可以并發(fā)處理的任務都可以使用并發(fā)來解決,比如 Go 處理 HTTP 請求時每個請求都會在一個 Goroutine 中執(zhí)行。

到此,相信大家對“12306的架構有哪些優(yōu)點”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關內(nèi)容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI