模拟“12306”服务端架构:100万人同时抢1万张火车票
▼数据猿年度征集评选正在进行中▼
大数据产业创新服务媒体—聚焦数据·改变商业
数据猿官网 | www.datayuan.cn
今日头条丨一点资讯丨腾讯丨搜狐丨网易丨凤凰丨阿里UC大鱼丨新浪微博丨新浪看点丨百度百家丨博客中国丨趣头条丨腾讯云·云+社区
每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!
12306 抢票,极限并发带来的思考
虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会。
尤其是春节期间,大家不仅使用 12306,还会考虑“智行”和其他的抢票软件,全国上下几亿人在这段时间都在抢票。
“12306 服务”承受着这个世界上任何秒杀系统都无法超越的 QPS,上百万的并发再正常不过了!
笔者专门研究了一下“12306”的服务端架构,学习到了其系统设计上很多亮点,在这里和大家分享一下并模拟一个例子:如何在 100 万人同时抢 1 万张火车票时,系统提供正常、稳定的服务。
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 加权轮询的演示
下面是一个加权轮询负载的配置,我将在本地的监听 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;}
}
接下来使用 Go 语言开启四个 HTTP 端口监听服务,下面是监听在 3001 端口的 Go 程序,其他几个只需要修改端口即可:
package mainimport ("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
具体的实现大家可以参考 Nginx 的 Upsteam 模块实现源码,这里推荐一篇文章《Nginx 中 Upstream 机制的负载均衡》:
https://www.kancloud.cn/digest/understandingnginx/202607
秒杀抢购系统选型
下单减库存
在极限并发情况下,任何一个内存操作的细节都至关影响性能,尤其像创建订单这种逻辑,一般都需要存储到磁盘数据库的,对数据库的压力是可想而知的。
如果用户存在恶意下单的情况,只下单不支付这样库存就会变少,会少卖很多订单,虽然服务端可以限制 IP 和用户的购买订单数量,这也不算是一个好方法。
支付减库存
预扣库存
扣库存的艺术
在单机低并发情况下,我们实现扣库存通常是这样的:
然后我们每台机器本地库存 100 张火车票,100 台服务器上的总库存还是 1 万,这样保证了库存订单不超卖,下面是我们描述的集群架构:
我们结合下面架构图具体分析一下:
代码演示
初始化工作
Redis 库使用的是 Redigo,下面是代码实现:
...
//localSpike包结构体定义
package localSpiketype LocalSpike struct {LocalInStock int64LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和redis连接池
package remoteSpike
//远程订单存储健值
type RemoteSpikeKeys struct {SpikeOrderHashKey string //redis中秒杀订单hash结构keyTotalInventoryKey string //hash结构中总订单库存keyQuantityOfOrderKey string //hash结构中已有订单数量key
}//初始化redis连接池
func NewPool() *redis.Pool {return &redis.Pool{MaxIdle: 10000,MaxActive: 12000, // max number of connectionsDial: 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 + 1return spike.LocalSalesVolume < spike.LocalInStock
}
统一扣库存操作 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) thenreturn redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)endreturn 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
}
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)
}
单机服务压测
开启服务,我们使用 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 requestsServer Software:
Server Hostname: 127.0.0.1
Server Port: 3005Document Path: /buy/ticket
Document Length: 29 bytesConcurrency 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] receivedConnection 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 239Percentage of the requests served within a certain time (ms)50% 1866% 2475% 2680% 2890% 3395% 3998% 4599% 54100% 239 (longest request)
而且查看日志发现整个服务过程中,请求都很正常,流量均匀,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
...
总结回顾
总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略
完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。
——END——
模拟“12306”服务端架构:100万人同时抢1万张火车票相关推荐
- 12306系统的秒杀“艺术”:如何抗住100万人同时抢1万张票?
作者 | IT牧场 编辑 | 阿秃 每到节假日期间,一二线城市返乡.外出游玩的人们几乎都面临着一个问题--抢火车票.虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场景,相信大家都深有体会. 尤其 ...
- 12306 的技术扒光在你面前,100 万人如何抢 1 万张票
戳蓝字"CSDN云计算"关注我们哦! 作者 | IT牧场 责编 | 阿秃 12306 抢票,极限并发带来的思考 每到节假日期间,一二线城市返乡.外出游玩的人们几乎都面临着一个问 ...
- 100万人同时抢1万张火车票,极限并发带来的思考
欢迎关注方志朋的博客,回复"666"获面试宝典 每到节假日期间,一二线城市返乡.外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票,但是放票瞬间即无票的场 ...
- 1年内4次架构调整,谈Nice的服务端架构变迁之路--转
原文地址:http://mp.weixin.qq.com/s?__biz=MzA5Nzc4OTA1Mw==&mid=410775314&idx=1&sn=7c7cc94f8f4 ...
- [知乎] 端游、手游服务端架构演变
本文版权归 知乎 韦易笑 所有,此处为转载+收藏,如有再转,请于作者处获得授权并于文章篇头处标明原创作者及出处,以示尊重! 作者:韦易笑 链接:https://www.zhihu.com/questi ...
- 各类游戏对应服务端架构
卡牌.跑酷等弱交互服务端 卡牌跑酷类因为交互弱,玩家和玩家之间不需要实时面对面PK,打一下对方的离线数据,计算下排行榜,买卖下道具即可,所以实现往往使用简单的 HTTP服务器: 登录时可以使用非对称加 ...
- 各类游戏对应的服务端架构
卡牌.跑酷等弱交互服务端 卡牌跑酷类因为交互弱,玩家和玩家之间不需要实时面对面PK,打一下对方的离线数据,计算下排行榜,买卖下道具即可,所以实现往往使用简单的 HTTP服务器: 登录时可以使用非对称加 ...
- 【精品】服务端 架构演进 历程
概述 以淘宝作为例,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则. 单机架构 ...
- 京东无线服务端架构演进历程
京东无线服务端 首先,在大会上统计一下数据,不知道多少人的手机上安装了京东手机APP,从举手的比例来看,大家也能猜到.就像这个图画的一样,京东无线服务端的流量像这个瀑布一样,汹涌不绝.服务端就是APP ...
最新文章
- CVPR 2021 | MI-AOD: 少量样本实现高检测性能
- iPhone拍人像,人头直接不见了,什么情况?
- C#实验报告 类与对象的访问性:银行账户存取款、新建账户、查询余额
- mysql8出现1045报错+常用的加密plugin汇总
- ibatis使用心得——返回Map的Map
- 全国计算机等级考试题库二级C操作题100套(第82套)
- php给客户端发广播,phpsocket.io如何向一个组广播?
- 新中国成立60周年重要科技成果知识竞赛试题
- 我的世界超能物质纳米机器人_我的世界超能物质攻略
- 印度有个 “扎克伯格庙”?
- Unity UGUI Rect
- 合作创新,携手共赢 共筑坚实数字底座
- 《袁老师访谈录》第一期|吴恩柏教授/香港科大协理副校长:【成功,说到底要非常专注!】...
- 用TinySpider进行网页抓取实例
- 网易患病员工被保安赶出公司,项目经理该如何应对中年危机?
- win10找不到oracle11g客户端,win10操做系统下oracle11g客户端/服务端的下载安装配置卸载总结...
- 80端口web服务攻击痕迹
- 145.如何评价个性化推荐系统的效果-2
- 最强大脑魏郭撕X,理科男与文科生的…
- 程序员的表白浪漫HTML生日礼物