Heim > Artikel > Backend-Entwicklung > Wie großartig ist die Architektur von „12306“?
An jedem Feiertag stehen Menschen, die nach Hause zurückkehren oder zum Spaß in Städten der ersten und zweiten Reihe unterwegs sind, fast immer vor einem Problem: Bahntickets zu ergattern!
12306 Ticketraub, Gedanken hervorgerufen durch extreme Parallelität
Obwohl Tickets jetzt in den meisten Fällen gebucht werden können, sind zum Zeitpunkt der Ticketbestellung keine Tickets verfügbar Ich glaube, dass jeder ein tiefes Verständnis für die Szene hat.
Besonders während des Frühlingsfestes nutzen die Menschen nicht nur 12306, sondern ziehen auch „Zhixing“ und andere Ticket-Grabber-Software in Betracht. Hunderte Millionen Menschen im ganzen Land schnappen sich in dieser Zeit Tickets.
„12306 Service“ verfügt über einen QPS, der von keinem Instant-Kill-System der Welt übertroffen werden kann. Millionen von Parallelität sind nichts anderes als normal!
Der Autor hat speziell die Serverarchitektur von „12306“ untersucht und viele Highlights seines Systemdesigns gelernt. Hier werde ich ein Beispiel mit Ihnen teilen und simulieren: Wie man 10.000 Bahntickets erhält, wenn 1 Million Menschen gleichzeitig greifen Das System bietet normale und stabile Dienste.
Github-Codeadresse:
https://github.com/GuoZhaoran/spikeSystem
Groß angelegte Hoch- Parallelitätssystemarchitektur
Eine Systemarchitektur mit hoher Parallelität übernimmt die verteilte Clusterbereitstellung. Die obere Schicht des Dienstes verfügt über einen schichtweisen Lastausgleich und bietet verschiedene Möglichkeiten zur Notfallwiederherstellung (Dual-Fire-Computerraum, Knotenfehler). Toleranz, Server-Disaster-Recovery usw.), um die hohe Verfügbarkeit des Systems sicherzustellen, wird der Datenverkehr auch auf verschiedene Server basierend auf unterschiedlichen Lastfähigkeiten und Konfigurationsstrategien verteilt.
Das Folgende ist ein einfaches schematisches Diagramm:
Lastausgleich einfach
Die obige Abbildung beschreibt den dreischichtigen Lastausgleich, den der Benutzer anfordert Im Folgenden finden Sie eine kurze Einführung in diese drei Arten des Lastausgleichs.
①OSPF (Open Shortest Link First) ist ein Interior Gateway Protocol (kurz IGP)
OSPF gibt den Status von Netzwerkschnittstellen zwischen Routern bekannt, um die Link-Status-Datenbank einzurichten und den kürzesten Pfadbaum generieren, berechnet OSPF automatisch den Kostenwert auf der Routing-Schnittstelle, Sie können den Kostenwert der Schnittstelle jedoch auch manuell angeben. Der manuell angegebene Wert hat Vorrang vor dem automatisch berechneten Wert.
Die von OSPF berechneten Kosten sind auch umgekehrt proportional zur Schnittstellenbandbreite. Je höher die Bandbreite, desto kleiner ist der Kostenwert. Pfade mit demselben Kostenwert zum Ziel können einen Lastausgleich durchführen, und bis zu 6 Verbindungen können gleichzeitig einen Lastausgleich durchführen.
②LVS (Linux Virtual Server)
Es handelt sich um eine Cluster-Technologie (Cluster), die IP-Lastausgleichstechnologie und inhaltsbasierte Anforderungsverteilungstechnologie verwendet.
Der Scheduler verfügt über eine sehr gute Durchsatzrate und überträgt Anforderungen gleichmäßig zur Ausführung an verschiedene Server. Der Scheduler schirmt Serverausfälle automatisch ab und bildet so eine Gruppe von Servern zu einem leistungsstarken, hochverfügbaren virtuellen Server.
③Nginx
Jeder muss damit vertraut sein. Es handelt sich um einen sehr leistungsstarken HTTP-Proxy-/Reverse-Proxy-Server, der häufig in der Dienstentwicklung verwendet wird.
Es gibt drei Hauptmethoden für Nginx, um den Lastausgleich zu implementieren: Polling, gewichtetes Polling, IP-Hash-Polling
Im Folgenden führen wir spezielle Konfigurationen und Tests für das gewichtete Polling von Nginx durch.
Demonstration der gewichteten Abfrage von Nginx
Nginx implementiert den Lastausgleich über das Upstream-Modul, in dem die Konfiguration der gewichteten Abfrage einen Gewichtungswert zu verwandten Diensten hinzufügen kann Die Last kann während der Konfiguration entsprechend der Leistung und Lastkapazität des Servers eingestellt werden.
Das Folgende ist eine gewichtete Polling-Lastkonfiguration. Ich werde die Ports 3001-3004 lokal abhören und die Gewichtungen von 1, 2, 3 bzw. 4 konfigurieren:
#配置负载均衡 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; } }
Ich bin lokal / Die virtuelle Domänennamenadresse von www.load_balance.com wird im Verzeichnis etc/hosts konfiguriert.
Als nächstes öffnen Sie mit der Go-Sprache vier HTTP-Port-Listening-Dienste. Das Folgende ist das Go-Programm, das Port 3001 überwacht. Die anderen müssen nur den Port ändern:
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) }
Ich werde anfordern Die Portprotokollinformationen werden in die Datei ./stat.log geschrieben und anschließend wird das AB-Stresstesttool für den Stresstest verwendet: ab -n 1000 -
c 100 http://www.load_balance.com/buy/ticket
Gemäß den Ergebnissen im statistischen Protokoll, Die Ports 3001–3004 haben 100, das Anforderungsvolumen beträgt 200, 300, 400.
Dies stimmt mit dem Gewichtsverhältnis überein, das ich in Nginx konfiguriert habe, und der Datenverkehr nach dem Laden ist sehr gleichmäßig und zufällig.
Informationen zur spezifischen Implementierung finden Sie im Quellcode des Upsteam-Moduls von Nginx. Hier ist ein empfohlener Artikel „Load Balancing of the Upstream Mechanism in Nginx“: https://www.kancloud.cn/digest/. Verständnisnginx/202607
Auswahl des Flash-Sale-Systems
Zurück zur ursprünglichen Frage, die wir erwähnt haben: Wie kann das Flash-Sale-System für Bahntickets eine normale und stabile Leistung bieten? unter Bedingungen hoher Parallelität? Was ist mit dem Dienst?
Aus der obigen Einführung wissen wir, dass der Benutzer-Flash-Sale-Verkehr durch Lastausgleichsebenen gleichmäßig auf verschiedene Server verteilt wird. Dennoch ist die von einer einzelnen Maschine im Cluster ausgehaltene QPS ebenfalls sehr hoch. Wie kann die Standalone-Leistung aufs Äußerste optimiert werden?
Um dieses Problem zu lösen, müssen wir eines verstehen: Normalerweise muss das Buchungssystem drei grundlegende Phasen verarbeiten: Auftragsgenerierung, Bestandsreduzierung und Benutzerzahlung.
Was unser System tun muss, ist sicherzustellen, dass Zugticketbestellungen nicht überverkauft oder überverkauft werden. Jedes verkaufte Ticket muss bezahlt werden, um gültig zu sein. Wir müssen auch sicherstellen, dass das System einer extrem hohen Parallelität standhält.
Wie sollte die Reihenfolge dieser drei Phasen sinnvoller zugeordnet werden? Lassen Sie es uns analysieren:
Eine Bestellung aufgeben, um den Lagerbestand zu reduzieren
Wenn gleichzeitige Benutzeranfragen bei der eingehen Server: Erstellen Sie zunächst eine Bestellung, ziehen Sie dann den Lagerbestand ab und warten Sie auf die Zahlung durch den Benutzer.
Diese Bestellung ist die erste Lösung, die den meisten von uns einfällt. In diesem Fall kann sie auch sicherstellen, dass die Bestellung nicht überverkauft wird, da der Lagerbestand nach der Erstellung der Bestellung reduziert wird eine atomare Operation.
Aber das wird auch einige Probleme verursachen:
Im Falle extremer Parallelität wirken sich die Details jeder Speicheroperation erheblich auf die Leistung aus, insbesondere Dinge wie das Erstellen von Bestellungen. Logik muss im Allgemeinen in einer Festplattendatenbank gespeichert werden, und der Druck auf die Datenbank ist denkbar.
Wenn ein Benutzer böswillig eine Bestellung aufgibt und nur eine Bestellung aufgibt, ohne zu bezahlen, wird der Lagerbestand reduziert und viele Bestellungen werden weniger verkauft, obwohl der Server die IP und die Anzahl der Benutzerbestellungen begrenzen kann , das zählt nicht.
Bezahlen, um den Lagerbestand zu reduzieren
Wenn Sie darauf warten, dass der Benutzer für die Bestellung bezahlt und den Lagerbestand reduziert, ist das erste Gefühl, dass es nicht weniger geben wird Verkäufe. Dies ist jedoch ein Tabu der gleichzeitigen Architektur, da Benutzer bei extremer Parallelität möglicherweise viele Aufträge erstellen.
Wenn der Lagerbestand auf Null reduziert wird, stellen viele Benutzer fest, dass sie die Bestellungen, die sie erhalten haben, nicht bezahlen können. Dies ist der sogenannte „Überverkauf“. Gleichzeitige E/A-Vorgänge auf der Datenbankfestplatte können ebenfalls nicht vermieden werden.
Einbehalten des Lagerbestands
Aus den Überlegungen der beiden oben genannten Lösungen können wir schließen, dass die Datenbank-E/A häufig betrieben werden muss, solange eine Bestellung erstellt wird.
Gibt es also eine Lösung, die keinen direkten Betrieb der Datenbank-E/A erfordert? Das ist die Zurückhaltung des Inventars. Zuerst wird der Lagerbestand abgezogen, um sicherzustellen, dass er nicht überverkauft wird, und dann wird die Benutzerbestellung asynchron generiert, sodass die Antwort an den Benutzer viel schneller erfolgt. Wie kann also sichergestellt werden, dass es viele Verkäufe gibt? Was sollte der Benutzer tun, wenn er nach Erhalt der Bestellung nicht bezahlt?
Wir alle wissen, dass Bestellungen jetzt eine Gültigkeitsdauer haben. Wenn der Benutzer beispielsweise nicht innerhalb von fünf Minuten bezahlt, wird die Bestellung ungültig. Aus diesem Grund wird die Bestellung ungültig Viele Online-Handelsunternehmen garantieren mittlerweile, dass die Ware nicht abläuft. Weniger angenommene Lösungen werden verkauft.
Bestellungen werden asynchron generiert und im Allgemeinen in Sofortverbrauchswarteschlangen wie MQ und Kafka verarbeitet. Wenn das Bestellvolumen relativ klein ist, werden Bestellungen sehr schnell generiert und Benutzer müssen kaum anstehen.
Die Kunst, Lagerbestände einzubehalten
Aus der obigen Analyse geht hervor, dass die Lösung, Lagerbestände einzubehalten, die vernünftigste ist. Lassen Sie uns die Details des Bestandsabzugs weiter analysieren. Es gibt noch viel Raum für Optimierung. Wie kann bei hoher Parallelität ein korrekter Bestandsabzug und eine schnelle Reaktion auf Benutzeranfragen sichergestellt werden?
Im Falle einer geringen Parallelität auf einer einzelnen Maschine implementieren wir den Bestandsabzug normalerweise wie folgt:
Um sicherzustellen Bestandsabzug und Auftragsgenerierung Die Atomizität erfordert eine Transaktionsverarbeitung, dann einen Bestandsabruf, eine Bestandsreduzierung und schließlich eine Transaktionsübermittlung. Der gesamte Prozess erfordert viele E/A-Vorgänge und blockiert den Betrieb der Datenbank.
Diese Methode ist überhaupt nicht für Flash-Sales-Systeme mit hoher Parallelität geeignet. Als nächstes optimieren wir den Bestandsabzugsplan für eine einzelne Maschine: lokaler Bestandsabzug.
Wir weisen der lokalen Maschine eine bestimmte Menge an Lagerbestand zu, reduzieren den Lagerbestand direkt im Speicher und erstellen dann asynchron eine Bestellung gemäß der vorherigen Logik.
Das verbesserte Standalone-System sieht so aus:
Dadurch werden häufige IO-Vorgänge in der Datenbank vermieden und Vorgänge werden nur im Speicher ausgeführt, was äußerst effizient ist . Verbessert die Fähigkeit einer einzelnen Maschine, Parallelität zu widerstehen, erheblich.
Allerdings kann eine einzelne Maschine Millionen von Benutzeranfragen nicht standhalten. Obwohl Nginx das Epoll-Modell zur Verarbeitung von Netzwerkanfragen verwendet, ist das c10k-Problem in der Branche längst gelöst.
Unter dem Linux-System sind jedoch alle Ressourcen Dateien, und das Gleiche gilt für Netzwerkanfragen. Eine große Anzahl von Dateideskriptoren führt dazu, dass das Betriebssystem sofort nicht mehr reagiert.
Wir haben oben die gewichtete Ausgleichsstrategie von Nginx erwähnt. Wir können genauso gut davon ausgehen, dass 1 Million Benutzeranfragen im Durchschnitt auf 100 Server verteilt werden, sodass die Menge an Parallelität, die eine einzelne Maschine ertragen muss, viel geringer ist.
Dann speichern wir 100 Bahntickets lokal auf jedem Automaten, und der Gesamtbestand auf den 100 Servern beträgt immer noch 10.000. Dadurch wird sichergestellt, dass die Inventarbestellungen nicht überverkauft werden :
问题接踵而至,在高并发情况下,现在我们还无法保证系统的高可用,假如这 100 台服务器上有两三台机器因为扛不住并发的流量或者其他的原因宕机了。那么这些服务器上的订单就卖不出去了,这就造成了订单的少卖。
要解决这个问题,我们需要对总订单量做统一的管理,这就是接下来的容错方案。服务器不仅要在本地减库存,另外要远程统一减库存。
有了远程统一减库存的操作,我们就可以根据机器负载情况,为每台机器分配一些多余的“Buffer 库存”用来防止机器中有机器宕机的情况。
我们结合下面架构图具体分析一下:
我们采用 Redis 存储统一库存,因为 Redis 的性能非常高,号称单机 QPS 能抗 10W 的并发。
在本地减库存以后,如果本地有订单,我们再去请求 Redis 远程减库存,本地减库存和远程减库存都成功了,才返回给用户抢票成功的提示,这样也能有效的保证订单不会超卖。
当机器中有机器宕机时,因为每个机器上有预留的 Buffer 余票,所以宕机机器上的余票依然能够在其他机器上得到弥补,保证了不少卖。
Buffer 余票设置多少合适呢,理论上 Buffer 设置的越多,系统容忍宕机的机器数量就越多,但是 Buffer 设置的太大也会对 Redis 造成一定的影响。
虽然 Redis 内存数据库抗并发能力非常高,请求依然会走一次网络 IO,其实抢票过程中对 Redis 的请求次数是本地库存和 Buffer 库存的总量。
因为当本地库存不足时,系统直接返回用户“已售罄”的信息提示,就不会再走统一扣库存的逻辑。
这在一定程度上也避免了巨大的网络请求量把 Redis 压跨,所以 Buffer 值设置多少,需要架构师对系统的负载能力做认真的考量。
代码演示
Go 语言原生为并发设计,我采用 Go 语言给大家演示一下单机抢票的具体流程。
初始化工作
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 }
本地扣库存和统一扣库存
本地扣库存逻辑非常简单,用户请求过来,添加销量,然后对比销量是否大于本地库存,返回 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
响应用户信息
我们开启一个 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 文件方便做压测统计。
单机服务压测
开启服务,我们使用 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 ...
总结回顾
总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略
完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。
我们实现了高并发抢票的核心逻辑,可以说系统设计的非常的巧妙,巧妙的避开了对 DB 数据库 IO 的操作。
对 Redis 网络 IO 的高并发请求,几乎所有的计算都是在内存中完成的,而且有效的保证了不超卖、不少卖,还能够容忍部分机器的宕机。
我觉得其中有两点特别值得学习总结:
①负载均衡,分而治之
通过负载均衡,将不同的流量划分到不同的机器上,每台机器处理好自己的请求,将自己的性能发挥到极致。
这样系统的整体也就能承受极高的并发了,就像工作的一个团队,每个人都将自己的价值发挥到了极致,团队成长自然是很大的。
②合理的使用并发和异步
自 Epoll 网络架构模型解决了 c10k 问题以来,异步越来越被服务端开发人员所接受,能够用异步来做的工作,就用异步来做,在功能拆解上能达到意想不到的效果。
这点在 Nginx、Node.JS、Redis 上都能体现,他们处理网络请求使用的 Epoll 模型,用实践告诉了我们单线程依然可以发挥强大的威力。
服务器已经进入了多核时代,Go 语言这种天生为并发而生的语言,完美的发挥了服务器多核优势,很多可以并发处理的任务都可以使用并发来解决,比如 Go 处理 HTTP 请求时每个请求都会在一个 Goroutine 中执行。
总之,怎样合理的压榨 CPU,让其发挥出应有的价值,是我们一直需要探索学习的方向。
Das obige ist der detaillierte Inhalt vonWie großartig ist die Architektur von „12306“?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!