“12306”是如何支撑百万QPS的?
作者:绘你一世倾城
编辑:陶家龙、孙淑娟,51CTO技术栈编辑整理
每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!
图片来自 Pexels
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
...
总结回顾
正文结束
1.
2.
3.
4.
5.
6.
7.
8.
一个人学习、工作很迷茫?
点击「阅读原文」加入我们的小圈子!
“12306”是如何支撑百万QPS的?相关推荐
- “12306” 是如何支撑百万 QPS 的?
点击关注公众号,Java干货及时送达 12306抢票,极限并发带来的思考? 每到节假日期间,一二线城市返乡.外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票,但是放票瞬间即 ...
- 五一回家的票买没?来看看“12306” 是如何支撑百万 QPS 的?
来源:https://juejin.cn/post/6844903949632274445 每到节假日期间,一二线城市返乡.外出游玩的人们几乎都面临着一个问题:抢火车票!虽然现在大多数情况下都能订到票 ...
- “12306”,为什么现在又不崩了?系统架构是如何支撑百万QPS的
点击上方" 开发者技术前线 ",选择"星标" 13:21 在看 真爱 来源:https://juejin.im/post/5d84e2 ...
- 新浪微博是如何支撑百万 QPS 的?
随着互联网从门户/搜索时代进入移动社交时代,互联网产品也从满足用户单向浏览的需求,发展为满足用户个性信息获取及社交的需求.这就要求产品做到以用户和关系为基础,对海量数据进行实时分析计算.也就意味着,用 ...
- 12306抢票带来的启示:看我如何用Go实现百万QPS的秒杀系统
本文为开源实验性工程:"github.com/GuoZhaoran/spikeSystem"的配套文章,原作者:"绘你一世倾城",现为:猎豹移动php开发工程师 ...
- 如何在几百万qps的网关服务中实现灵活调度策略
作者 | 加纳斯 导读 说起百度的BFE可能不少人都听说过,但是其实在百度内部还有一个几百万qps的通用网关服务:Janus.截止当前,Janus服务不仅覆盖了百度内部FEED.评论.点赞.关注.直播 ...
- 乐视云如何炼成弹性支撑百万级别直播流
在观看视频直播中,难免因打断错过精彩片刻.乐视云 "月光宝盒"可以完美解决直播过程中任意时间段时移回看,并在直播结束后提供瞬时秒回功能,快速将直播信号转为点播信号进行分发,大幅提升 ...
- 一台mysql并发能力_mysql怎么支撑百万级并发-对于同一个表,MySQL支持多少个并发操作...
到服务器的SQL最大并发连接数为16384.mysql百万级数据查询. 受服务器配置和网络环境的限制,实际服务器支持的并发连接数量会更小. MySQL流量大,并发问题高 因为mysql是一个线程的连接 ...
- 打造支撑百万用户的分布式代码托管平台
在2017在线技术峰会--首届阿里巴巴研发效能嘉年华上,来自研发效能事业部的杨再新分享了<打造支撑百万用户的分布式代码托管平台>.他主要介绍了GIT和SVN思想差异.开源的代码托管平台的挑 ...
- RDC如何打造支撑百万用户的分布式代码托管平台
一.背景介绍 毋庸置疑,代码是DevOps流程的起点,是所有研发流程的基础:代码托管为代码"保驾护航",确保代码的安全性.可用性,同时提供围绕代码的一些基础服务,如MR.Issue ...
最新文章
- win10 uwp 按下等待按钮
- CodeBlocks: 生成的exe文件自定义一个图标
- 为利用 QT 制作的项目设置图标
- 个人博客域名迁移说明 www.paincker.com
- 浅谈RSocket与响应式编程
- HTTP GET 和 POST 的区别
- 静电场里非常有用的公式
- 纽约出租车计费问题:数据清洗与初探
- iOS开发中通知(Notification)快速入门及推送通知实现教程
- html css 简历底纹,有了这些CSS套路,你也可以做一个漂亮的简历
- ios系统gps测试软件,GPS工具箱
- php 上传图片 裁剪,thinkphp_图片上传裁剪功能
- 50 个实用小工具(图片处理、截屏录屏、格式转化、下载工具、浏览器、工具箱等)
- hdu_2670Girl Love Value(dp)
- JavaWeb正则表达式2-不情愿模式
- uiautomator2输入中文的问题
- 『Linux』CentOS 7下一些有趣的配置
- Python 将MP3音频文件转换成MIDI乐谱文件
- 史上最难oracle数据库练习题(附答案)
- 计算机中ram和rom的区别是什么意思,ram和rom的区别 ram和rom有什么区别
热门文章
- Linux内核提供了三种不同形式的中断底半部实现机制:软中断、tasklet和工作队列。...
- FDQuery 怎么能插入NULL参数
- 请检查 PHP 或网站服务器日志,并正确配置 PHP 安装
- Acrobat Pro DC 教程,如何编辑 PDF 文件中的文本和图片?
- MacBook高效优先设置,你都开启了吗?
- 在 Mac 上的登录窗口中如何打开辅助功能选项?
- Mac一体化数据库管理和迁移工具Navicat Premium
- Sketch 80 for mac(矢量绘图设计软件)
- PDF to Word Document Converter for Mac(PDF格式转换必备)
- 如何通过系统信息查看 Mac 上的显示刷新率?