“12306”的架构,是如何抗住几亿日活、百万级高并发的? | 您所在的位置:网站首页 › 12306奔溃阿里救急 › “12306”的架构,是如何抗住几亿日活、百万级高并发的? |
点击“技术领导力”关注∆ 每天早上8:30推送 作者: 绘你一世倾城 来源:https://juejin.im/post/5d84e21f6fb9a06ac8248149 “每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票! 12306 抢票,极限并发带来的思考 虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。 尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。 “12306 服务”承受着这个世界上任何秒杀系统都无法超越的 QPS,上百万的并发再正常不过了! 笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在 100 万人同时抢 1 万张火车票时,系统提供正常、稳定的服务。 Github代码地址: https://github.com/GuoZhaoran/spikeSystem大型高并发系统架构 高并发的系统架构都会采用分布式集群部署,服务上层有着层层负载均衡,并提供各种容灾手段(双火机房、节点容错、服务器灾备等)保证系统的高可用,流量也会根据不同的负载能力和配置策略均衡到不同的服务器上。 下边是一个简单的示意图:负载均衡简介 上图中描述了用户请求到服务器经历了三层的负载均衡,下边分别简单介绍一下这三种负载均衡。 ①OSPF(开放式最短链路优先)是一个内部网关协议(Interior Gateway Protocol,简称 IGP) OSPF 通过路由器之间通告网络接口的状态来建立链路状态数据库,生成最短路径树,OSPF 会自动计算路由接口上的 Cost 值,但也可以通过手工指定该接口的 Cost 值,手工指定的优先于自动计算的值。 OSPF 计算的 Cost,同样是和接口带宽成反比,带宽越高,Cost 值越小。到达目标相同 Cost 值的路径,可以执行负载均衡,最多 6 条链路同时执行负载均衡。 ②LVS (Linux Virtual Server) 它是一种集群(Cluster)技术,采用 IP 负载均衡技术和基于内容请求分发技术。 调度器具有很好的吞吐率,将请求均衡地转移到不同的服务器上执行,且调度器自动屏蔽掉服务器的故障,从而将一组服务器构成一个高性能的、高可用的虚拟服务器。 ③Nginx 想必大家都很熟悉了,是一款非常高性能的 HTTP 代理/反向代理服务器,服务开发中也经常使用它来做负载均衡。 Nginx 实现负载均衡的方式主要有三种: 轮询 加权轮询 IP Hash 轮询 下面我们就针对 Nginx 的加权轮询做专门的配置和测试。 Nginx 加权轮询的演示 Nginx 实现负载均衡通过 Upstream 模块实现,其中加权轮询的配置是可以给相关的服务加上一个权重值,配置的时候可能根据服务器的性能、负载能力设置相应的负载。下面是一个加权轮询负载的配置,我将在本地的监听 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 端口监听服务,下面是监听在 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 中 Upstream 机制的负载均衡》: https://www.kancloud.cn/digest/understandingnginx/202607秒杀抢购系统选型 回到我们最初提到的问题中来:火车票秒杀系统如何在高并发情况下提供正常、稳定的服务呢? 从上面的介绍我们知道用户秒杀流量通过层层的负载均衡,均匀到了不同的服务器上,即使如此,集群中的单机所承受的 QPS 也是非常高的。如何将单机性能优化到极致呢? 要解决这个问题,我们就要想明白一件事: 通常订票系统要处理生成订单、减扣库存、用户支付这三个基本的阶段。 我们系统要做的事情是要保证火车票订单不超卖、不少卖,每张售卖的车票都必须支付才有效,还要保证系统承受极高的并发。 这三个阶段的先后顺序该怎么分配才更加合理呢?我们来分析一下:下单减库存 ![]() 在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的。 如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制 IP 和用户的购买订单数量,这也不算是一个好方法。 支付减库存 预扣库存 扣库存的艺术 从上面的分析可知,显然预扣库存的方案最合理。我们进一步分析扣库存的细节,这里还有很大的优化空间,库存存在哪里?怎样保证高并发下,正确的扣库存,还能快速的响应用户请求?在单机低并发情况下,我们实现扣库存通常是这样的: 然后我们每台机器本地库存 100 张火车票,100 台服务器上的总库存还是 1 万,这样保证了库存订单不超卖,下面是我们描述的集群架构: ![]() 我们结合下面架构图具体分析一下: 代码演示 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 |
CopyRight 2018-2019 实验室设备网 版权所有 |