笔记——从0-1做业务服务架构
序言
引子
毕玄曾在QCon
的围炉夜话中分享——09
年之前,阿里的技术团队都处于陪(业务)跑状态。
大型互联网公司都一定经历了业务初创阶段,再到业务稳定阶段的过程。公司在不同阶段,技术团队的重心会有所不同。初创阶段更多重视业务,业务稳定阶段则会更重视技术本身。
业务初创时期,面临的主要问题是把业务转变成真正的系统工程实现,这时的系统架构并不需要设计的非常复杂,因为还没有外部真实流量,仅仅为了快速迭代和内测实验,系统由“APP
–>业务服务(单体)–>单库单表”组成,服务并未按照不同业务拆分,只是从工程角度出发,将应用进行工程包的拆分。
业务开始趋于成熟,需要将产品推向C
端用户,考虑到业务复杂度(业务起步阶段并没有C
端大流量高并发的特征,但高性能、高可用是基础),开始按照业务进行简单地微服务拆分(垂直拆分)。从用户规模、成本控制、系统稳定性、可扩展性,以及快速迭代等角度考虑,系统使用“App
–>网关–>下游业务服务–>MySQL
单库单表(按业务服务分库,但每个业务服务依然是单库单表)”的简单架构模式来实现,初创阶段更侧重于业务功能实现。此时团队也开始相应地扩张和组织架构调整,组织架构仅粗略地分为业务产研、基础架构研发和运维,其中业务产研按照不同的业务分别负责不同的业务服务,比如账号、商城、消息、内容、活动、权益、金融等。经过一系列重构和扩展,整个系统就形成了以APP为中心的一套轻量级微服务体系架构。
业务复杂度、业务容量和用户体量不断膨胀,组织架构不得不开始演变以适应当前阶段(分工越来越细化),比如从业务产研分离出业务网关、业务中台(比如将商城中的订单、交易、支付等能力抽象成中台能力,使得商城业务能更好地聚焦业务,原本的业务服务逐渐演变为业务网关),单独组建数据中台等。微服务的流量和容量要求也逐渐变大,各个团队都开始从三高架构(高并发、高性能、高可用)出发考虑系统重构,系统逐步演变为“App
–>网关–>业务接入层–>业务中台层–>MySQL
分库分表+
Nosql
”的三高架构模式(包含服务注册发现、分布式缓存、消息队列、熔断限流降级、流量控制(限流/削峰)、无状态设计、弹性伸缩机制、监控告警体系等),理论上系统里边最早能感知到三高需求的业务应该是账号/商品/订单。
三高应用架构可以归纳为
3
个维度——高并发、高可用、高性能,这3
个维度并不是从开始就同等重要。
- 高性能,怎么释放硬件的性能是开发人员(不管是初级、中级,还是高级)都要掌握的知识,比如高性能
IO
、线程池等。- 高可用,是第二个进入我们视野的,高并发是在事物发展的后续阶段。
- 只要系统有上线,就会有一个可用性的目标,量化高可用目标可不是拍脑袋决定的。
- 目标是什么呢?比如
5
个9
,4
个9
。大厂细化出来了3-5-2
这三个数字,出现问题的时候要求3
分钟定位问题,5
分钟恢复业务,平均2
个月最多发生一次问题,也就是一年最多发生6
次这样的问题,6*8=48
分钟,刚好满足4
个9
。假设一次线程问题耗费150
分钟(3
个小时),那就得保证以后3
年都不出问题,才能达到4
个9
的目标。- 高可用手段都有哪些?分层之后的每一层,方案都不一样,每一层都有每一层的方案。接入层,比如异地多活、智能
DNS
、KeepAlive
保活(LVS
、Nginx
)等;服务层,比如服务注册/发现、自动扩缩容等;缓存层,比如Redis
代理Proxy
模式、哨兵模式、集群模式等;数据库层,比如代理/配置/客户端高可用、数据定期备份等;其他中间件高可用架构,比如ES
、RockerMQ
、反向代理等;容错设计,比如HTTPDNS
客户端容错、错误重试、备用服务重试等;运维和监控,比如接口自动化巡检、定期容量压测、监控告警(基础指标、业务指标)、分布式链路追踪等。- 高并发,有很多维度,比如分布式资源安全(分布式锁)、缓存、限流、降级(流控、熔断)、缩短访问距离、数据一致性、数据库的高并发(数据库集群、分库分表)、服务的高并发(
K8S
弹性伸缩、公私结合)等。
- 除了上面这些,高并发还需要考虑业务隔离,在支撑能力不够的时候,怎么保障核心业务,防止系统雪崩;怎么引入消息队列进行削峰等等。
- 数据一致性,按照物理距离的远近,分为跨地域(
IDC
之间异地多活的两个机房之间的数据库或缓存同步)、跨库/跨系统(分布式事务)、跨集群(缓存与DB
)、跨节点(ES
集群、缓存集群、DB
集群、ZK
集群)、跨CPU
内核(MESI
协议)、数据分片(Redis Cluster
)。
重构是在一定阶段后作出的重要决策,不仅仅是重新拆分,也是对业务系统的重新塑造,所以重构时一定要有前瞻性,设计合理的系统架构和成本,切不可盲目。务必对必须完成的不同任务进行优先级排序,找出在哪个阶段应该处理哪些任务,避免过早地进行优化。比如MySQL
分库分表+
Nosql
的方案,虽然带来了高性能,但同时也引入了系统复杂度,除非系统不得不重构(Trade-off Balance
,比如现有架构不能满足当前或未来几年的业务发展)。
引用
Donald Knuth
老爷子在《计算机编程艺术》这本书中说的一句话——Premature optimization is the root of all evil
(过早优化是万恶之源)。
释义:我们应该忘记细枝末节的优化,在97%
时间,过早的优化是所有邪恶的根源。然而,我们不应该在那个关键的3%
中放弃我们的机会。
架构考量维度
如果给某一个专业领域的业务服务做系统架构,应该怎么入手?拿到一个项目,要怎么从
0
到1
进行前瞻性的架构呢?
架构师的能力就是一个自己归纳、总结、梳理的能力,一切都要理论化、数据化。架构是会有一个架构演进的过程,开始的时候,资源可以少一点,但要明白为什么现在资源要少点,将来需要多少资源,心里要有底,体现出专业素质和专业水平。
在架构过程中,要学会系统分层,怎么拆分合适,如何做业务分级等等。总结下系统架构的结构化思维,我们经常需要考量以下这几个维度,
- 业务维度,做业务梳理和功能分离;
- 模块维度,考虑模块解耦和系统分层;
- 存储维度,对数据存储做技术选型;
- 流量维度,做流量规划,规划每一层用哪些组件;
- 部署维度,根据每个阶段的量去做实际的部署。
以消息中台服务为例,
业务维度
上面这张架构图是经过全面梳理业务之后,精心分析总结得到的,一开始肯定是没有这张图的。
架构师要具备前瞻性,做业务架构要尽可能地全面,该规划的就得规划,落实还是另外一回事。前期设计和后期实施允许有所偏差,但大致主线得差不多,不能偏离太多,否则架构就没什么意义了。
功能分离
为什么做功能分离?确保核心功能的高可用和高并发。
通过对系统业务的仔细分析,进行功能分离,划分核心池和非核心池功能,将核心和非核心功能物理隔离。
功能分离是划分一个个业务模块,业务功能模块很多,做工程时肯定不可能一蹴而就,一般都得分阶段架构,比如可以分一期、二期开发。模块划分后,从重要性、高可用层面出发做业务分级(核心池/非核心池),核心池要重点保障高性能、高可用。
业务分级可以从以下两个角度出发,
按照功能的重要程度
这里有两个关键点,首先要区分核心/非核心功能。举个例子,在亿级规模的用户中台系统中有注册、登录、用户信息、日志、行为分析等功能,哪些功能更重要(核心)?用户中台系统拥有亿级规模的用户,业务日活
2000
万,平均每天注册用户可能是10w
左右(假设2
年),修改用户信息的可能还不到1w
,但登录功能是2000w
,很明显登录更核心。登录是核心功能,注册、用户信息是非核心功能。登录功能一旦有问题,其他的业务系统就不能登录了;而非核心功能即使有问题,暂时也不会立刻影响业务系统的使用,因此优先保障核心功能是我们首要的目标。其次,核心/非核心功能有不同的应对策略,比如隔离策略、重试策略、功能降级策略。
功能的重要程度只是功能分离的一种维度,还可以按照其他维度进行划分,比如流量特点。
按照功能的流量特点
首先也要区分流量突发型、流量平缓型的功能,不同的类型需要有不同的应对策略。首先做好隔离策略;另外对突发流量的功能做好独立的伸缩扩展策略。举个例子,在电商业务的秒杀系统中,按照流量特点可以把功能分为流量突发型(秒杀功能)、流量平缓型(电商功能),对突发流量的功能做好隔离。
功能分离后的应对策略
功能分离之后,不同功能有不同的应对策略,可以从功能隔离和功能降级两个基础维度来看。
功能隔离
如何隔离?对于核心/非核心池,走单独的域名,还有单独的接入层、隔离的服务层、单独的缓存、单独的数据库。换一种说法就是域名隔离、代理隔离、微服务隔离、缓存隔离、数据库隔离。隔离可以是逻辑上的,也可以是物理上的(完全不在一台物理机上)。
只要核心/非核心池存在共享的资源,就有可能因为非核心池影响核心池。举个例子,如果数据库共用一套,那么非核心池如果出现了大量的整表查询(慢sql
),核心池同样受到影响。再比如核心/非核心池共享了缓存服务器,就可能会由于非核心池的操作影响了缓存的性能,甚至出现问题。读高可用/高并发,缓存很重要,缓存物理隔离,就能保证核心池的安全。
功能降级
流程有正常的流程,也有异常的流程,简单的异常流程可以直接返回一些静态信息作为兜底结果,当出现瓶颈或故障时,甚至可以将非核心池直接降级,保护核心池不受影响。拆分为核心/非核心池后,虽然物理上两者隔离了,但有的业务还是需要核心/非核心功能配合才能完成,这就存在了一定的风险。比如大量用户登录时,可以停止行为分析、登录日志等非核心功能,以保证核心功能不受影响。
秒杀系统中有正常的电商功能的秒杀、也有用消息队列异步秒杀实现,平时不用消息队列削峰,当秒杀进行时响应太慢了,可以把降级开关打开,通过消息队列削峰走降级流程。
再比如,微博热点场景下,服务会遭到热点流量洪峰(比如百万qps
)的冲击,在那个时候,区分保障核心/非核心池业务非常必要,先对非核心池的业务进行熔断/限流/降级,高优保障核心池业务正常提供服务(弹性扩容),防止系统服务发生雪崩。比如高优保障用户登录、信息流、正文页等业务,对评论、发微博等写业务临时降级处理,等到流量洪峰过去后,再逐步恢复之前被降级或熔断的非核心池业务。
降级的实现方式,通常有手动和自动。
- 自动方式是程序调用发生问题时,根据指标计算(比如
rt
、错误次数等)进行自动降级,比如调用某服务时,响应时间超过预订阀值,自动降级。微服务的熔断(Hystrix
、Sentinel
)就属于自动降级。 - 手动方式是使用配置中心,对系统中可降级的服务都设置好开关项(根据
rt
或者其他指标参数的表现手动打开开关),当需要降级时,在配置中心中进行操作,配置中心进行下发变更通知,可以开发一个后台运维管理,当需要停用某个功能时,只需要在后台上点击一个按钮就能够完成,花费时间只需要几秒钟。
模块维度
模块解耦
模块解耦和功能分块差不多,但实际上也不一样。一个业务模块在具体实施时可能分为很多子模块。这里关注的是区分模块外部边界和内部边界,分离变化的和不变的部分,最终形成一个个系统模块。
以消息服务为例,解耦成两条主线开发,分别是消息下行的主流程、运营后台。
- 消息下行的主流程去繁就简,大致就是上游业务调用消息服务进行消息下行推送,流量打进来后,先经过接入层(外部
Nginx
网关/K8S CoreDNS
+ 内部微服务网关),之后再到消息服务层,由服务层进行消息的存储和分发,不同通道的消息会分发到不同的下行通道服务,最终通过具体的下行通道完成一次消息下行的过程。- 消息触达运营后台,提供的功能是面向内部运营人员的,所以和消息主流程,是完全不同的一条线。
针对主流程,怎么做模块设计呢?
- 在分布式微服务架构开发层面,肯定要有比如微服务注册中心、内部微服务网关(当然运维部署层面还要有四七层代理、
K8S Service
等基础设施); - 网关路由消息到消息分发层,这里流量打过来有两种模式——同步做接口调用;消息队列异步做消息发送;
- 消息分发层有两个工作——消息持久化,消息分发(路由);
- 下边就是不同渠道的发送服务,可以把不同渠道分别做一个微服务去对接外部通道;
- 分离变与不变,在主流程中还穿插着一些重要的公共技术功能模块,比如业务接入流量的认证授权、风控策略、失败补偿、通道调度,把这些公共的功能识别出来,做成公共基础模块。
系统分层
在微服务架构体系下,高并发、高可用、高性能是围绕着每一层展开的,系统分层一般包含接入层、服务层、缓存层、数据库层、中间件层、监控预警、基础设施等。
接入层
主要作用是反向代理(业务网关、四七层网关、K8S
等)、流量分发、负载均衡,按照用户规模和流量(吞吐量)规模,接入层的架构方案不一样。
接入层组件介绍
Nginx
:一个高性能的Web-Server
和实施反向代理的软件;LVS
:Linux Virtual Server
,使用集群技术,实现在Linux
操作系统层面的一个高性能、高可用、负载均衡服务器;Keepalived
:一款用来检测服务状态存活性的软件,常用来做高可用;F5
:一个高性能、高可用、负载均衡的硬件设备;DNS
轮询:通过在DNS-Server
上对一个域名设置多个IP
解析,来扩充Web-Server
性能及实施负载均衡的技术。
服务层
微服务全家桶生态链,从微服务网关进来后,到微服务实例,包含高可用的注册中心。
- 服务层:提供公用服务,比如用户服务,订单服务,支付服务等;
- 公共基础能力:服务治理,统一配置,统一监控。
缓存层
请求分为两类,分别是读请求、写请求。大流量架构中,缓存不只是分布式缓存那么简单,而是复杂的多级缓存(整个系统架构的不同系统层级进行数据缓存,以提升读取的效率),理论上除了数据库以外,每一层都能设计缓存。
Tomcat
堆缓存(一级)、分布式缓存(二级)、Nginx
本地缓存(三级)。对于Tomcat
缓存、Nginx
缓存都是进程内的本地缓存,分布式缓存则是大体积、大容量的。HTTP
缓存:根据服务器端返回的缓存设置响应头将响应内容缓存到浏览器,减少浏览器端和服务器端之间来回传输数据量,节省带宽。
数据层
- 结构化数据(关系型)数据库集群(支持读写分离)
- 异构数据(非关系型
NoSQL
)集群 - 分布式文件系统集群
- 大数据存储层,支持应用层和服务层的日志数据收集,关系数据库和
NoSQL
数据库的结构化和半结构化数据收集; - 大数据处理层,通过
MapReduce
进行离线数据分析或Storm
实时数据分析,并将处理后的数据存入关系型数据库。
- 大数据存储层,支持应用层和服务层的日志数据收集,关系数据库和
在秒杀服务架构中,不管是订单,还是库存,都访问的是结构化数据(MySQL
),如果MySQL
数据规模比较大,可以做异地多活,分库分表架构,不同库表间有数据同步方案,这是写。对于读,比如搜索业务可以引入ES
集群;对于风控或者用户行为、商品特征分析等做离线计算,可以引入HBase
集群,通过Spark/Hive
计算,计算结果可以放到ES
或MySQL
。
中间件
Apollo
(动态配置)Rocketmq
、Kafka
等(消息队列)XxlJob
(定时调度集群)ZooKeeper
(协调集群)ELK
、GPE
(服务监控集群)
番外——请求分层过滤模型(亿级用户流量)
模型经过分层处理后,请求处理模型分两种——直筒型、漏斗型。
直筒型
用户请求1:1
的洞穿到db
层,存在于传统的低并发、低性能、低可用项目中。
直筒型请求处理模型的适用场景——企业级应用。
特点:
- 用户规模较小
- 请求峰值和平均值相差不大
- 请求峰值不会超过数据层的处理能力
漏斗型
用户的请求,从客户端到db
层,层层递减,递减的程度视业务而定。例如当10w
人去抢100
个物品时,db
层的请求在个位数量级1000
以内,这就是比较理想的模型。
漏斗型请求处理模型的适用场景——互联网应用(如秒杀)。
特点:
- 用户规模大
- 请求峰值和平均值相差巨大
- 请求峰值远远超出最后一层(数据层)的处理能力
怎么做到漏斗型中的请求在落地到最后一层时变少了?
分层过滤策略不是简单的把请求砍掉,针对漏斗型请求处理模型(如秒杀场景),一种核心策略就是对请求进行分层过滤,过滤掉一些无效的请求。
举个例子
以秒杀系统为例,来具体看下分层过滤的玩法。在秒杀系统中,请求分别经过CDN
、Nginx
、微服务(如库存、秒杀)和数据库这几层,
那么,
- 大部分数据和流量在用户浏览器或者
CDN
上获取,这一层可以拦截大部分静态资源的读取; - 经过第二层
Nginx
(获取商品详情)时,尽量得走Nginx Cache
,过滤一些可以直接访问Nginx
缓存的请求;经过Nginx
这一层时,也可以进行流控,还可以进行黑名单过滤,拦截掉一些无效的流量; - 再到服务层,进入微服务网关时,可以做用户的授权检验,对系统做好保护和限流,这样数据量和请求又进一步减少;
- 业务层,还可以进行数据的有效性、一致性过滤,这里又减少了一些流量。
就像漏斗一样,尽量把数据量和请求量一层一层地做过滤和减少。
分层过滤的核心思想
在不同的层次尽可能地过滤掉无效请求,让漏斗最末端的才是有效请求。要达到这种效果,就必须对数据做分层校验。
分层过滤的基本原则
- 通过在不同的层次尽可能地过滤掉无效请求,尽早处理掉请求;
- 通过
CDN
过滤掉大量的图片,静态资源的请求;- 读请求尽量命中缓存,不要穿透到数据库;
- 尽量将动态读数据请求,命中在三级缓存,或者二级缓存,过滤掉无效的数据读;
- 对写入操作进行削峰,争取批量写入,提高写入的吞吐量;
- 分层限流,防止系统雪崩。
在流量确实很大时,削峰、分层限流是两个常用措施。
- 写操作的流量削峰方案(降级方案)
- 正常的时候,写请求没有超出数据库的能力,可以直接落地到数据库。如果写操作超出了数据库的能力,就可以削峰;
- 从本质上来说,削峰就是更多地延缓用户请求,以及层层过滤用户的访问,遵从最后落地到数据库的请求数要尽量少的原则;
- 要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去;
- 在这里,消息队列就像是水库一样,拦截上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的;
- 消息队列中间件主要解决应用耦合、异步消息、流量削峰等问题。常用的消息队列系统有
ActiveMQ
、RabbitMQ
、ZeroMQ
、Kafka
、MetaMQ
和RocketMQ
等。
- 分层限流(防止系统雪崩的无奈策略)
- 分层限流,比如接入层、服务层限流,根据各层的能力对系统进行保护;
- 限流是一种无奈的策略,它会拒绝一部分请求,不是那么友善。
番外——幂等设计原则
什么是幂等性
对于一次请求和多次请求,同一个资源产生的副作用是相同的。通俗点说,以相同的请求调用这个接口一次或多次,需要给调用方返回一致的结果时,就要考虑把这个接口设计成幂等的,用数学公式表示,f(x)=f(f(x))
。
为什么要幂等
在高并发的系统环境下,很可能会因为网络、阻塞等问题,导致客户端不能及时地收到服务端的响应,甚至是调用超时了。比如订单场景下可能会遇到如下的几个问题,
- 创建订单时,第一次调用服务超时,再次调用是否产生两笔订单?
- 订单创建成功去减库存时,第一次减库存超时,是否会多扣一次?
做了幂等后,肯定就不会出现上述问题。
分布式微服务中的幂等性概念:用户对于同一操作发起的一次或多次请求的结果是一致的,不会因为多次点击而产生了副作用。
在分布式微服务架构中,可能会发生重复请求或消费的场景,随处可见。
- 网络波动:因网络波动,可能会引起重复请求;
- 分布式消息消费:任务发布后,使用分布式消息服务来进行消费;
- 用户重复操作:用户在使用产品时,可能无意地触发多笔交易,甚至没有响应而有意触发多笔交易;
- 未关闭的重试机制:因开发人员、测试人员或运维人员没有检查出来,而开启的重试机制(如
Nginx
重试、RPC
通信重试或业务层重试等)。
幂等Case
服务方提供一个查询操作是否成功的api
,第一次超时之后,调用方调用查询接口,如果查到了就走成功的流程,失败了就走失败的流程。
- 用户礼包领取
- 用户注册时,系统一般会送用户一份新用户大礼包(比如积分、权益等),当点击领取这个礼包之后,就相当于收下了这份礼包,以后无论怎么点击领取,结果还是一样,系统只会提示你已经领取过礼包了,不会再让你重复领取一次。
- 抢红包
- 抢红包时,点击了抢,抢到就有,没抢到就没有。无论重复点击多少次,红包都会提示已经抢过该红包了。
- 账单付款
- 结账时,支付平台会生成唯一的支付连接,不会再次生成另外的支付连接,不能因为这个支付接口被调了两次就创建两个一样的订单。
可以看到,针对一个微服务架构,如果不支持幂等操作,将会出现以下情况,
- 电商超卖现象
- 重复转账、扣款或付款
- 重复增加金币、积分或优惠券
CRUD操作的天然幂等性
- 新增类请求:不具备幂等性
- 查询类动作:重复查询不会产生或变更新的数据,查询具有天然幂等性
- 更新类请求:
- 基于主键的计算式
Update
,不具备幂等性,即UPDATE goods SET number=number-1 WHERE id=1
- 基于主键的非计算式
Update
:具备幂等性,即UPDATE goods SET number=newNumber WHERE id=1
- 基于条件查询的更新,不一定具备幂等性(需要根据实际情况进行分析判断)
- 删除类请求:
- 基于主键的
Delete
具备幂等性- 一般业务层面都是逻辑删除(即
update
操作),而基于主键的逻辑删除操作也是具有幂等性的
幂等性解决方案
全局唯一
ID
根据业务操作和内容生成一个全局唯一
ID
,在执行操作前,先根据这个ID
是否存在,来判断这个操作是否已经执行。如果不存在则把全局ID
存下来,比如数据库、Redis
等。如果存在则表示该操作已经执行了。使用全局唯一ID
是一个通用方案,可以支持插入、更新、删除业务操作。但是这个方案看起来很美但是实现起来比较麻烦,下面的方案适用于特定的场景,但是实现起来比较简单。唯一索引(去重表)
这种方法适用于在业务中有唯一标识的插入场景中,比如支付场景,如果一个订单只会支付一次,所以订单
ID
可以作为唯一标识。这时,我们就可以建一张去重表,并且把唯一标识作为唯一索引,在实现时,把创建支付单据写入去重表,放在一个事务中,如果重复创建,数据库会抛出唯一约束异常,操作就会回滚。插入或更新(
upsert
)这种方法插入并且有唯一索引的情况,比如我们要关联商品品类,其中商品
ID
和品类ID
可以构成唯一索引,并且在数据表中也增加了唯一索引,这时就可以使用InsertOrUpdate
操作。多版本控制
这种方法适合在更新的场景中,比如要更新商品名字,这时就可以在更新接口中增加一个版本号来做幂等,
1
boolean updateGoodsName(int id,String newName,int version);
在实现时可以如下,
1
update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
状态机控制
这种方法适合在有状态机流转的情况下,比如订单的创建和付款,订单的付款肯定是在之前,这时我们可以通过在设计状态字段时,使用
int
类型,并且通过值类型的大小来做幂等,比如订单的创建为0
,付款成功为100
,付款失败为99
。在做状态机更新时,我们就这可以这样控制,1
update goods_order set status=#{status} where id=#{id} and status<#{status}
幂等性设计不能脱离业务来讨论,它和业务相关性更大些,是一个强业务性的方案设计。
存储维度
技术选型要考虑哪些维度?
- 充分调研业务并抽象出业务模型,看数据模型是否结构化,数据结构是否固定;
- 数据规模是否很大(海量),这个是按时间段来的,比如一两年内会达到什么样的量级;这里的规模很大,指的是海量
PB
级,如果规模很大,即使用sql
,需要考虑把历史数据备份,放到Nosql
里边。- 团队是否具备
Nosql
人才,是否能支撑Nosql
的开发和维护工作。
技术两大方案
- 结构化(
MySQL
) - 结构化+非结构化混合(
MySQL + ES + HBase
) 纯NoSQL?完全抛弃结构化,目前不太现实(比如`Nosql`的事务支持问题),所以这种暂时不考虑。
存储数据规模要从流量和容量两方面考虑,流量和容量规模决定了存储架构的技术选型。
流量规模
首先是数据的吞吐量(数据流量规模),根据不同阶段的流量,做不同阶段的流量架构。数据访问量指标一般有Qps
、Tps
,我们需要预估访问数据库的峰值。
服务的峰值预估多少才合理?
一般系统的性质不一样,预估的方式也不一样。比如秒杀场景,可以参考Toc
的系统预估方式。
输入参数,系统大致的用户量,但消息服务并不是Toc
的,而是作为公共服务,提供给内部的第三方系统来使用,这时Toc
的方式就失效了。
可以简单做个调查,逆向的方式定一个流量规模。跟内部的第三方系统的owner
做一个调研,基本上可以确定当时服务的流量需求。但不排除特殊情况,比如业务临时排进一个需求,需要1s
推出100w
条短信,这时qps
非常大。所以没办法做很准确的流量预估。
这种高水位值和低水位值相差太大的场景,只能先按照平常预估一个qps
来做,比如10w
。100w
用10s
发完,消息系统一般允许有些延迟。大部分公司1w
qps
就能满足绝大多数场景,但作为基础中台,流量峰值还是比较高的。
容量规模
其次是数据容量(数量存储规模),不同阶段存储记录条数多少,数据规模大小多少TB
(比如一年或两年内会数据存储规模达10TB
或100TB
),需要提前考虑100TB
或者500TB
要怎么分表?
预估下每天消息量是
10w
,100w
,还是1000w
。
- 按照每天
1000w
消息量,2
年内保持稳定的要求,表数据量规划如下,
- 两年的消息总量:
1000w
*730
天=73
亿;73
亿,约为100
亿,约为10G
,一条消息20k
,则为200T
的规模。- 按照每天
100w
消息量,2
年内保持稳定的要求,
- 两年的消息总量:
100w
*730
天=7.3
亿;7.3
亿,约为10
亿,约为1G
,一条消息20k
,则为20T
的规模。
规划MySQL库表数量的Case
按照每天
1000w
消息量,2
年内保持稳定的要求,表数据量规划怎么规划?
两年的消息总量为73
亿,假设每张表的条数上限规定为500w
,就得需要1460
张表,比较接近2
的幂——1024
。73
亿/1024
=692
w,这个也是在接受范围内的,所以可以按照1024
张表来规划。
不过考虑到成本控制,刚开始一般表的数量很少,后边会按需扩容。比如刚开始4
张表,后边翻倍扩容就好了。扩容涉及到数据迁移,具体实施一般由DBA
来做,专业的事还得交给专业的人来做,就算是系统架构师做数据迁移,一般也不是特别熟悉。实际工作中也有安全责任制,DBA
也不会允许其他职能的人做这些事情。
按照
5w
qps
的流量峰值要求,库的数量怎么规划?
假设每个库正常承载的写入并发量为1500
qps
,32
个库就能承载32*1500=4.8w
写并发。
如果可以的话,悲观预估下,每秒写入超过5w
qps
,可以使用MQ
削峰、批量写入等降级策略。MQ
的写入吞吐量可以轻松到达10w
级别。
自然,按照10w
qps
的流量峰值要求,进行库的规划,需要64
个库。
综合规划
- 一个数据库实例假如
1T
,20T
就得20
个数据库实例,变成2
的倍数,就是16
;- 一个数据库实例
qps
峰值1500
,5w
qps
需要32
个库;- 容量和流量一起评估出来,最后取个最大值。无非就是
32
个库2s
推完10w
;16
个库4s
推完10w
。
流量维度
这里说的是整个系统的流量架构,它是单独的。整个分层的流量架构在设计时也是一层一层来的。比如接入层做流量规划,需要哪些组件,而上面说的数据存储维度中的流量规划实际上是数据层的。
流量架构怎么从
0-1
开始?
- 按照未来一段时间(比如
2
年之内)的用户规模,做系统的流量(吞吐量)规模预估;- 按照流量预估值和系统各层组件的性能基线值(参考值),做各层组件的部署架构规划,确保系统的高性能、高可用。
流量架构的类型
老系统
老系统做流量架构时,可以参考现有的监控数据、服务能力、流量指标。对着老的架构版本,计算偏离指标,折算成冗余系数,完成流量架构的工作。
- 做出系统在不同用户量、不同场景(高峰、平峰、低峰)下的流量(吞吐量)预估,含未来一段时期(如
2
年); - 做出系统在不同用户量、不同场景下的各层组件的部署架构。
- 做出系统在不同用户量、不同场景(高峰、平峰、低峰)下的流量(吞吐量)预估,含未来一段时期(如
新系统
新系统是在实际工作中遇到最多的情况,如果没有业务监控、没有中间件日志,也没有日活数据,那怎么评估预期指标?线上没有任何的历史监控数据和日志数据,之前介绍的方法就不适用了,这时需要用另一种方法来评估性能指标,那就是
2-8
法则。对新系统来说,完成流量架构,要做的的工作,
- 根据
2-8
法则,做出系统在不同用户量、不同场景下的流量(吞吐量)预估,含未来一段时期(如2
年); - 做出系统在不同用户量、不同场景下的各层组件的部署架构。
2-8
法则,又叫80/20
定律、帕累托法则(Pareto‘s principle
)、巴莱特定律、朱伦法则(Juran’s Principle
)、关键少数法则(Vital FeRule
)、不重要多数法则(Trivial Many Rule
)、最省力的法则、不平衡原则等,被广泛应用于社会学及企业管理学等。它是19
世纪末20
世纪初意大利经济学家帕累托发现的。
在任何一种事物中,最重要的只占其中一小部分,约20%
,其余80%
尽管是多数,却是次要的。心理学场景
- 从心理学来说,人类80%的智慧,都集中在20%人身上;
2-8
法则是一种社会准则,符合大多数社会现象的规律,同样也适用于互联网领域。
互联网行为场景
- 一个网站有成千上万的用户,但是
80%
的用户请求是发生在20%
的时间内,比如大家去网上购物,基本也都集中在中午休息或晚上下班后; 2-8
法则的核心原则是关注重要部分,忽略次要部分。系统性能如果能支撑发生在20%
时间内的高并发请求,必然也能支持非高峰期的访问。
新系统可能在建立之初只有
1w
用户,甚至还没有用户,但未来2
年可能会达到10w
、100w
,甚至10000w
,做架构时做好亿级架构规划有时是必要的。跟规划相配套,必须有对应的分层架构、缓存架构、限流架构等落地方案,就算未来没落地,但不影响预研(提前做好架构规划)。没有提前做好架构规划,等到后期性能跟不上,又要进行升级改造,甚至推倒重来。
新项目还没有上线,在上线前希望先进行一轮压测,评估项目性能是否能支撑当前的用户,这个时候性能预期指标更为重要。- 根据
流量规模预估(流量规划)
要考虑前瞻性,一年或两年之内会增长到多少用户?预估一般会分为多个阶段去估。
消息服务作为一个基础中间件系统,流量峰值不太好预估,只能先按额定量的模式来做,比如先定10w
qps
。
ToC场景,可以使用2-8法则,预估吞吐量规模
2-8
法则:80%
的请求 /20%
的时间 * 冗余系数
假设用户量有10w
,
- 平均
20%
是活跃用户(每天来访问网站的用户占到20%
,也就是2w
用户每天会来访问),每个用户平均30
次点击,计算pv
20% * 10w * 30 = 60w pv
80%
的点击量发生在20%
的时间里,也就是5
小时内会有48w
点击,计算qps
60w * 80% / 5 * 3600 = 27 qps
- 设置一个偏离系数,算出来
qps/tps
27 * 4 = 108 qps
- 假设一个请求
20k
,算出每秒的流量
108 * 20k = 2M
通过用户量来预估QPS
,先预估系统的每日总请求数,这个没有固定的方法,如果没有任何历史数据参考,一般是通过用户量或者其他关联系统来评估。
- 通过用户量来推算
PV
- (总用户数 *
20%
) * 每天的大致点击次数(参考淘宝,大概30-50
次)=pv
数 20%
就是一个经验值,这里是日活。
- (总用户数 *
PV
推算QPS
的公式- (总
PV
数 *80%
) / (每天秒数 *20%
) = 峰值时间每秒请求数(QPS
)` 80%
的点击量发生在20%
的时间里边
- (总
- 乘上冗余系数
- 评估出指标后,为了更保险,最好再乘以一个冗余系数(偏离系数),提高预期指标,防止人为评估造成预期指标偏低的情况(指标和预期结果有偏离),使得预算的值和实际的值更接近。
- 如果没有线上的指标做参考,根据行业经验,冗余系数一般定为
2-5
之间。 - 冗余系数的迭代,等将来项目上线后,可以通过对项目接口的峰值监控,来对比之前评估的算法结果,调整冗余系数,最终随着不断的数据积累,将会形成一套本项目的性能模型。
注意,在ToC
是这样的,这里有很多是经验值,在实际使用时,要根据实际项目的情况进行调整。ToB
后台就不一定是这样,中台也不能按照面向用户这么规划,内部消息有时会很频繁,这时就得按照实际场景去做实际的流量规划理论分析。
将来项目上线后,接口的访问量真的和计算的一模一样吗?这个肯定不会,实际与理论是有差距的,性能测试从来都不是一门非常精确的技术。
2-8
法则也并不是100%
适用于所有业务场景。在没有任何历史数据参考的背景下,2-8
法则是一种相对来说靠谱的算法,最起码有一定的理论依据,比拍脑袋猜的值靠谱多了。
了解服务中各个组件的服务吞吐能力
这个指标,一般得参考组件的硬件和系统资源,这里只做一个大致的预估,这里的数据仅供参考,仅用这里的估值来预算中间件的规模。
悲观地预估,各组件的并发能力基线值(参考值)
- 接入层
LVS
10w-17w
qps
Nginx
5w-10w
qps
- 服务层
SpringCloud gateway
5k
qps
- 异步削峰
MQ
10w
qps
- Java服务
Tomcat
1k
qps
- 存储
MySQL
1.5k
qps
- NoSQL集群,不是单机的指标
Redis
5w+
qps
Hbase
7w+
qps
ES
7w+
qps
Tomcat
Tomcat
默认配置的最大请求数是150
,也就是说同时支持150
个并发。具体能承载多少并发,需要看硬件的配置,CPU
越多性能越高,分配给JVM
的内存越多性能也就越高,但也会加重GC
的负担。当某个应用拥有250
个以上并发的时候,应考虑应用服务器的集群。
一般来说,虽然Tomcat
的IO
线程最多控制在400
以内,如果每个请求要300ms
,一个线程3 qps
,那么一个Tomcat
到达1000 qps
,还是可以的。所以Tomcat
参考的并发能力为1000qps
。
Nginx
的并发能力
看到这里,这个框架的弱点仍然是Nginx
结点的并发问题和单点故障。对于Nginx
的抗并发能力,官方给出的是5w
并发量,即轻轻松松处理5w
的并发访问。
对比Tomcat
,之所以Nginx
有这么大抗并发,主要原因有两个,
Nginx
只做请求和响应的转发而没有业务逻辑处理,大部分的时间花在与其他计算的IO
上;Nginx
的IO
采用的是单线程或少线程、异步非阻塞的模式(Tomcat
是一个连接一个线程,同步阻塞的模式),避免了打开IO
通道等待数据传输的过程(仅仅是在数据传到了,再来接收即可),极大的缩短了线程调度和IO
处理的时间。
MySQL
MySQL
数据库查询能力
- 主键查询:千万级别数据
= 1~10 ms
,4
核心8
线程为1000qps* 8=8000qps
- 唯一索引查询:千万级别数据
= 10~100 ms
,4
核心8
线程为100qps* 8=800qps
- 非唯一索引查询:千万级别数据
= 100~1000 ms
,4
核心8
线程为10qps* 8=80qps
- 无索引:百万条数据
= 1000 ms+
综合来说,MySQL
的并发能力大概在1500qps
左右
MySQL
数据库事务能力
- 更新删除(与查询相同)
- 插入操作,依赖于配置优化,比查询的效率低
MySQL
的连接数限制
MySQL
默认配置的最大连接数是151
。可以将最大连接数设置的最大值为100000
。一般情况在Linux
系统中建议设置为500-1000
。- 参见server-system-variables.html
mysql max connections
Redis
单机性能测试
Redis
单机为5w qps
左右。在生产环境中,测试单实例redis
的读写list
数据结构性能,结果如下,
本地/局域网 | 读写类型 | 测试命令 | client连接数 | qps | 延迟响应 <=1ms比例 | 延迟响应 <=2ms比例 | 延迟响应 <=4ms比例 | 延迟响应 <=8ms比例 |
---|---|---|---|---|---|---|---|---|
本地 | 写 | lpush | 1 | 35714 | 100% | 100% | 100% | 100% |
本地 | 写 | lpush | 2 | 63000 | 100% | 100% | 100% | 100% |
本地 | 写 | lpush | 4 | 153846 | 100% | 100% | 100% | 100% |
本地 | 写 | lpush | 8 | 155914 | 100% | 100% | 100% | 100% |
本地 | 写 | lpush | 16 | 151788 | 99.99% | 100% | 100% | 100% |
本地 | 读 | rpop | 1 | 38971 | 100% | 100% | 100% | 100% |
本地 | 读 | rpop | 2 | 65832 | 100% | 100% | 100% | 100% |
本地 | 读 | rpop | 4 | 162469 | 100% | 100% | 100% | 100% |
本地 | 读 | rpop | 8 | 184928 | 100% | 100% | 100% | 100% |
本地 | 读 | rpop | 16 | 171452 | 100% | 100% | 100% | 100% |
局域网 | 写 | lpush | 1 | 10009 | 99.99% | 99.99% | 100% | 100% |
局域网 | 写 | lpush | 2 | 13563 | 99.98% | 99.98% | 99.99% | 100% |
局域网 | 写 | lpush | 4 | 25601 | 99.98% | 99.99% | 100% | 100% |
局域网 | 写 | lpush | 8 | 43830 | 99.98% | 99.98% | 99.99% | 100% |
局域网 | 写 | lpush | 16 | 68820 | 99.94% | 99.97% | 99.99% | 100% |
局域网 | 读 | rpop | 1 | 6214 | 99.97% | 99.98% | 99.99% | 100% |
局域网 | 读 | rpop | 2 | 13289 | 99.94% | 99.97% | 99.99% | 100% |
局域网 | 读 | rpop | 4 | 24900 | 99.98% | 99.98% | 99.99% | 100% |
局域网 | 读 | rpop | 8 | 37118 | 99.96% | 99.98% | 99.99% | 100% |
局域网 | 读 | rpop | 16 | 69492 | 99.93% | 99.96% | 99.99% | 100% |
测试环境和测试工具
CPU
:8
核- 内存:
8G
Redis
版本:3.2.6
- 测试工具:
redis
官方基准测试工具redis-benchmark
SpringCloud Gateway
性能比
zuul1
高一些,zuul2
是基于netty
的,性能高一些。
SpringCloud Gateway
默认也是基于netty
的。
一次8
核8G
压测结果如下,
- 并发数:
300
; netty
工作线程数(reactor.netty.ioWorkerCount
):8
(默认)- 样本数据:返回
1.5k
大小 - 服务端响应时间:
10ms
左右 - 测试时长:
5
分钟 JVM
内存:2G
netty
工作线程数调整为(reactor.netty.ioWorkerCount
):12
,压测结果如下,
消息队列MQ
消息队列的吞吐量非常大,它是纯IO
型的,IO
+零复制,性能非常高。
一般可以理解,像Kafka
的QPS
在10w
左右,RocketMQ
稍微低一点,吞吐量在5-10w
之间。
NoSQL异构查询存储
ES
,线上有较多的节点,并发量和节点呈正相关。
HBase
也是,7w+ qps
没问题
真实按照每一层的流量预估,做流量架构
10w
qps
,刚开始肯定不会那么高,有可能一年达到,有可能两年。
Java
服务这里根据时效、性能要求,估计Tomcat
数量。收到消息,到发出去,rt
时间去规划。
10w / 1k = 100
到Redis
集群这,经过服务削峰这一层,流量已经下来了。
最终的流量架构就是系统在不同用户量、不同场景下(如平时、战时)的分层规划、组件规划。
部署维度
部署的行为,就和规划没有那么强关联了,需要做的就是根据每个阶段的量去做实际的部署。比如考虑高可用,看资源怎么规划,网络结构是怎么样的,每一层有多少组件,分别部署在哪个网段。如果你的网络结构比较简单,搞一下两个跨域的可用区,每个可用区各一套,做一下每个可用区的规划。
这里就是一个五花八门、变化多样的架构设计,跟网络环境有很大的关系。各个公司的架构师技术栈不一样,解决方案多种多样,没有标准解决方案,但是目标是一样的。有的部署在公有云,有的部署在私有云,有的是自建的IDC
机房。部署架构要有一个分而治之的思路(分层的部署架构方案,一层一层剥洋葱,一层一层来设计),有两个非常重要的考量要素,首当其冲的是高可用(很复杂的目标),另一个是单元化,每一层都要做单元化和高可用的考量。
高可用
- 接入层高可用
- 常见的方案有异地多活、
HTTPDNS
、KeepAlive
保活、防DDOS
攻击等。 - 异地多活,避免机房级别,甚至是地域级别的灾难,也就是常说的跨国多活、跨域多活、同城多活。
- 常见的方案有异地多活、
- 服务层高可用
- 注册中心高可用,微服务实例数
>=2
- 注册中心高可用,微服务实例数
- 缓存层高可用
- 代理模式、哨兵模式、
Cluster
模式
- 代理模式、哨兵模式、
- 数据库层高可用
- 代理型高可用(
MyCat
)、客户端高可用(Shardingjdbc
)、配置型高可用(进行高可用地址的更换)
- 代理型高可用(
- 其他中间件高可用
- 保证服务总线、反向代理、
ES
、MQ
高可用
- 保证服务总线、反向代理、
- 容错方案
- 重试机制,比如客户端容错(
HTTPDNS
)、错误重试机制、备用服务地址重试
- 重试机制,比如客户端容错(
- 运维与监控
- 立体化监控预警
- 基础设施的监控,比如虚拟机、容器、
Nginx
、数据库、缓存等 - 应用层监控,比如核心业务指标监控、分布式链路跟踪、慢查询、慢调用等
- 接口自动巡检,假死自启动等
异地多活
- 通过
dns + gslb
(智能ldns
)做负载均衡,将请求路由到最近的可用服务的机房。gslb
,全局负载均衡,策略很多,可以根据物理位置路由到最近的一个机房。
- 服务全部机房内自治,不进行跨机房访问。
- 不同的机房,进行数据异步双向复制(为了数据最终一致性)。
单元化
单元化,是在做异地多活时,一个地域形成完整的业务闭环(即在一个地域上,这个单元是可以独立可运行的),地域之间不依赖另外一个地域存活,脱离另一个单元,这个单元不会受到任何影响。单元化是垂直拆分的,根据用户id
划分单元即可。在高可用场景下,系统做跨地域的部署(一般系统用不到)。
虽然要满足单元化,但是也要满足单元之间具备故障转移机制,比如北京的某个服务挂了,流量要可以转移到上海机房的相应服务上。
机房之间既要独立,又要进行数据同步,还要考虑同步的延迟问题,考虑数据不一致的时间问题。如果出现数据同步的时间太长,或者数据不一致的时间太长,一旦出现故障的时候,就要考虑切换。数据不一致的周期也要考虑。
Case
下面基于私有云做部署架构。私有云不叫机房,叫可用区zone
。假设有两个可用区在两个城市,比如北京、上海。
上游业务实际上会通过内网域名或者K8S ServiceName
方式访问消息服务,这里我们只画了内网域名的接入层。
业务系统通过域名访问消息服务,用智能ldns
(本地dns
)解析正确的vip
,当一个可用区的dns
挂掉后,可以转移至另一个可用区的dns
解析vip
。
接入层通过vip
访问LVS
,vip
由keepalive
提供,也就是通过keepalive
来做故障转移。
接下来是整个微服务治理体系,RocketMQ
最好单独做到两个可用区高可用。
最后是缓存和数据层。
扩容维度
为什么在架构设计时,要考虑扩容预案?
做架构要避免返工,重新推倒重来。怎么避免返工,就要提前做好规划,这种规划会影响很多算法的选择,比如ID
生成、ID
分片算法怎么分等。这就是前瞻性。
梳理分库分表扩容的场景,有两大场景,
流量瓶颈后,扩容的目标库数量计算,
2
个库约为2000-3000
qps
,可以实现10w
消息,50s
内完成。目标:
10w
消息要在10s
内完成扩容到
4
个库够吗,不行就80
个库。容量瓶颈后,扩容的目标库数量计算,
2
库4
表,共8
个表,目前有4000w
记录需要扩容到
16
个表
从架构师视角来看,扩容的工作大致包括哪些步骤?
扩容工作,是以DBA
为主导,架构师是打配合的。DBA
不懂或者不管上面说的规划,那是架构师规划的,一般落地得DBA
来做。
配合dba
做好存量复制,和增量数据复制,
- 建好要扩展的库表;
- 开启增量数据的复制,可以使用
Canal
这样的Binlog
增量订阅和消费工具, 把增量数据进行二次路由,插入到新的库表中; - 开启存量数据的复制, 使用
select into
临时表from
原始表where
id%2
导出数据,复制到新表。
更换的方式,
- 冷更换:准备好数据后,在一个没有人烟的深夜,去停服重启,更换数据源;
- 热更换:准备好数据后,在访问的低峰时段,开启更换按钮,更换数据源。
总结
架构的工作,就是Trade-off Balance
,从宏观到微观,由粗到细,千万不要一开始就掉到细节的汪洋大海里边去。做一个合格的架构师,就得万事万物都得形成自己的理论,理论化、数据化支撑起来,万事万物要学会量化。
Trade-off Balance,怎么理解?
Trade-off Balance
,衡量工作怎么做才能更好,这需要有宽广的知识面和一定的技术深度来支撑。
架构师做一件工作,要随时对这件工作需要耗费的成本和所要达到的成果之间进行权衡评估。
2025
年 于 北京 记