从程序员角度分析,到底“12306”的架构到底有多牛逼?
来源:https://juejin.im/post/5d84e21f6fb9a06ac8248149
每到节假日期间,一二线城市返乡、外出游玩的人们几乎都面临着一个问题:抢火车票!
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
...
总结回顾
总体来说,秒杀系统是非常复杂的。我们这里只是简单介绍模拟了一下单机如何优化到高性能,集群如何避免单点故障,保证订单不超卖、不少卖的一些策略
完整的订单系统还有订单进度的查看,每台服务器上都有一个任务,定时的从总库存同步余票和库存信息展示给用户,还有用户在订单有效期内不支付,释放订单,补充到库存等等。
欢迎大家关注Java之道公众号,也会定期发布原创的Java技术文章~
- MORE | 更多精彩文章 -
如果你喜欢本文,
请长按二维码,关注 Hollis.
转发至朋友圈,是对我最大的支持。
转发+在看,让更多看见。
从程序员角度分析,到底“12306”的架构到底有多牛逼?相关推荐
- [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径 - 草根的暂时胜利
如果你还不知道问题的起因,请首先移步到这两篇文章 1. [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径,以及修改Hosts文件,使用OPENDNS无效情况下的解决方案 2. ...
- [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径,以及修改Hosts文件,使用OPENDNS无效情况下的解决方案...
问题描述 新年刚过,我就发现使用的安徽电信E9套餐有HTTP劫持的情况(网上有人说DNS劫持,有人说网页劫持),我想大致就是这种情况. 重现非常简单,在地址栏输入一些不存在的网址(比如http: // ...
- [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径 – 之深度分析
如果你还不知道问题的起因,请首先移步到这篇文章<[原创]从程序员角度分析安徽电信HTTP劫持的无耻行径,以及修改Hosts文件,使用OPENDNS无效情况下的解决方案> 我所深恶痛绝事情 ...
- 庆祝自开博来首篇浏览数过万的随笔诞生 - [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径......
这是自从2009年创立本博客以来,首个浏览数过万的随笔.庆祝!庆祝! 1. [原创]从程序员角度分析安徽电信HTTP劫持的无耻行径,以及修改Hosts文件,使用OPENDNS无效情况下的解决方案(11 ...
- 做程序员10年了,复制粘贴是我最牛逼的技能,从菜鸟兑变成大牛,直到看了这些大佬的公众号...
今年越来越多的技术公众号如雨后春笋般冒出来,质量参差不齐,高质量号哪儿去了?通过我近一个月的观察及统计,发现还是这些长期保持更新的高质量公众号,在我的朋友圈出镜率最高!他们有着高质量的原创文章,整理出 ...
- 从程序员角度看ELF
从程序员角度看ELF 原文:< ELF:From The Programmer's Perspective> 作者:Hongjiu Lu <mailto: hjl@nynexst.c ...
- 2008年下半年软件水平考试之程序员试题分析
文章试读 不拘一个遍程序系列:编程序不能一个脑袋钻到底,有时要学会变通,即所谓的曲线救国.一.二.三.四 职场规划:一些杂七杂八的职场感悟吧.不值钱的软件人才 精力充沛与事业成功 让系分来得更猛烈 ...
- 从程序员角度看ELF----__do_global_dtors_aux and __do_global_ctors_aux
原文地址::http://blog.chinaunix.net/uid-20605433-id-1617450.html 相关文章 1.glibc全局构造与析构(解释了_do_global_ctors ...
- java elf_从程序员角度看ELF(转载)
http://www.xfocus.net/articles/200109/260.html) 原文:< ELF:From The Programmer's Perspective> 作者 ...
最新文章
- 和12岁小同志搞创客开发:如何驱动各类型传感器?
- pandas使用fillna函数将dataframe中缺失值替换为空字符串(replace missing value with blank string in dataframe)
- 12.MapReduce第2部分(WordCount词频统计、自然连接)
- devops相关书籍哪个好_您在DevOps周期中的哪个位置进行安全保护?
- 编程完数_初级编程C++题:11H1343: 完数
- alwayson高可用组_AlwaysOn可用性组–如何在集群实例和独立实例之间设置AG(第3部分)
- python自带的解释器和编辑器叫什么_(四)python自带解释器(LDIE)的使用
- 静态常量static和方法重载
- TCPClient例子(3)基于委托和事件的TcpHelper程序
- javacv使用详解
- 让XP系统也支持微软雅黑字体
- Power BI 数据分析基础
- 参考TLC5615基于verilog HDL实现SPI时序
- android 调用系统打印
- Golang - Mysql ang Http Basic fucntions
- AtCoder Beginner Contest 283 E - Don‘t Isolate Elements
- 海洋污染全球告急:AI 可能是最后的防线
- networkx计算边的重要性:边介数或者中介中心性edge_betweenness
- 民政部:汶川地震救灾困难比较多
- CMOS芯片cmos image sensor
热门文章
- python爬虫下载模块_python爬虫系列(4.5-使用urllib模块方式下载图片)
- fastboot no permission
- docker查询mysql 有哪些版本的镜像_运维有话说 | Mysql容器化主主从架构搭建
- python 白色怎么表示_python – 如何使用pil使用白色背景(透明?)的round_corner标识?...
- (计算机组成原理)第五章中央处理器-第五节1:指令流水线(定义和表示方法及性能指标)
- c++中的运算符重载---知识点:运算符重载函数,友元函数,函数重载
- Java 线程实例一(查看线程是否存活、获取当前线程名称、状态监测、线程优先级设置、死锁及解决方法、获取线程id、线程挂起)
- git submodule 子模块的管理和使用
- TCP/IP四层模型及各层协议首部详述(包含IOS7层)
- [Nowcoder] 大整数相乘(拼多多笔试题)