系统设计原则
简介
好的系统是迭代出来的。先解决核心问题,预测未来可能出现的问题,对现有的问题有方案,对未来的问题有预案。不是一上来就按1亿用户量设计,也不要过度复杂化系统。
业务千变万化,技术层出不穷,设计理念也是百花齐放,看起来似乎很难有一套通用的规范来适用所有的架构设计场景。但是总是有一些原则是可以通用的。
在设计系统时,应该多思考墨菲定律:
- 任何事情都没有表面看起来那么简单
- 所有的事情都会比你预计的时间长
- 可能会出错的事一定会出错
- 如果你担心某种情况发生,那么它就更有可能发生
在系统划分时,也要思考康威定律:
- 系统架构是公司组织架构的反映
- 应该按照业务闭环进行系统拆分/组织架构划分,实现闭环/高内聚/低耦合,减少沟通成本
- 如果沟通出现问题,那么就应该考虑进行系统和组织架构的调整
- 在合适时机进行系统拆分,不要一开始就把系统/服务拆得非常细,虽然闭环,但是每个人维护的系统多,维护成本高该处使用的url网络请求的数据。
一、系统的技术设计原则
1.1.高并发原则
1.1.1.无状态
如果设计的是无状态的,那么应用比较容易进行水平扩展。
实际生产环境可能是这样的:应用无状态,配置文件有状态。比如,不同的机房需要读取不同的数据源,此时,就需要通过配置文件或配置中心指定。比如后台系统使用session共享机制保证分布式部署。
1.1.2.拆分
在系统设计时,要考虑到系统是否做拆分。如果资源有限,并且用户并没有那么多,可以做一个大而全的系统。
而高并发的应用,通常是要做拆分的。拆分可以依据多个维度:
- 系统维度 :如订单、库存、商品系统等
- 功能维度 :如,对登录系统再拆分,划分为 :验证码登录、微信登录、密码登录等功能。
- 读写维度 :针对读写再做分离,读服务可以使用缓存、写服务使用分库分表。
1.1.3.服务化
首先判断单点服务是否可以满足。如果不能满足,集群可以吗?使用Nginx做负载均衡是否可以解决?
服务越来越多,是否要使用服务自动注册与发现?某些服务访问量太大,导致整个系统不可用,要不要上服务降级和限流?哪些是主要服务?
1.1.4.消息队列
消息队列的作用有三个 :削峰、解耦、异步。
使用消息队列可以实现服务解耦(一对多消费)、异步处理、流量削峰/缓冲等。但是订阅者太多,那么订阅单个消息队列就会成为瓶颈,此时需要考虑对消息队列进行多个镜像复制。
使用消息队列时,需要注意消息丢失、重复接收的场景。这对于不能容忍生产失败的业务场景来说,一定要做好后续的数据处理工作,比如持久化数据同时要增加日志、报警等,或者在生产失败后发送http请求来保证成功。还有消息重复问题,特别是一些分布式消息队列,出于对性能和开销的考虑,在一些场景下会发送消息重复接收,需要在代码层面进行防重处理。
1.1.5.缓存
缓存对读服务来说,是扛流量的必选技术。不同的场景缓存不同的信息,以解决不同的问题:
- 浏览器端缓存
- 客户端缓存
- CDN缓存
- 接入层缓存: 使用Nginx做一层缓存
- 应用层缓存
- 分布式缓存
- 异步与并发:某些资源实时性没那么高,可以考虑使用异步加载,如用户评价、商品打分这种。获取多个资源时,采用并发的方式获取,可以大大的加快访问速度。
1.1.6.数据异构
所谓数据异构,是把数据按需(数据结构、存取方式、存取形式)异地构建存储。比如将mysql里面的数据缓存到redis里面去,就是一种数据异构的方式。
分库分表中有一个最为常见的场景,为了提升数据库的查询能力,我们都会对数据库做分库分表操作。比如订单库,开始的时候是按照订单ID维度去分库分表,那么后来的业务需求按照商家维度去查询。相同的数据需要做多种异构可以使用MQ机制接收数据的变更,然后存储到合适的存储引擎,如订单id纬度的分库分表、商家纬度的分库分表、用户纬度的分库分表、redis、Elasticsearch等。
另外,还需要考虑对历史订单数据进行归档处理,以提升服务的性能和稳定性。而有些数据异构的意义不大,如库存架构,可以考虑异步加载,或者合并并发请求。
总结起来大概有以下几种场景:
数据库镜像
数据库实时备份
多级索引
search build(比如分库分表后的多维度数据查询)
业务cache刷新
价格、库存变化等重要业务消息
常见的异构方式:完全克隆。做数据备份。将数据库A,全部拷贝一份到数据库B,这样的使用场景是离线统计跑任务脚本的时候可以。缺点也很突出,不适用于持续增长的数据。
binlog方式。比如使用比较广泛的canal是基于mysql数据库binlog的增量订阅和消费组件。订阅mysql的binlog日志,消费这些日志做主从同步、缓存更新。
MQ方式。业务数据写入DB的同时,也发送MQ一份,也就是业务里面实现双写,消费MQ的数据做各种异构处理。这种方式比较简单,但也很难保证数据一致性,对简单的业务场景可以采用这种方式。
1.2.高可用原则
1.2.1.降级
对于一个高可用服务,很重要的一个设计就是降级开关,提前写好降级逻辑。
可以手动降级,也可以自动降级。自动降级触发的条件可以使用:超时的请求数超过阈值、异常的请求数超过阈值时。阈值具体设置为多少,通过压测初步确认,上线观察后,再次调整。
降级的手段:停止读数据库、准确结果转为近似结果、使用静态结果、同步转异步、功能裁剪、禁止写(高峰期减少不必要的写)、分用户降级、工作量证明POW(
验证码、数学题、拼图、滑块)。
主要依据如下思路:
- 开关集中化管理:通过推送机制把开关推送到各个应用。
- 可降级的多级读服务:可以指定服务调用降级为只读本地缓存、只读分布式缓存、只读默认数据。
- 开关前置化: 如架构是Nginx—>Apache,可以将开关前置到Nginx接入层,在Nginx层做开关,请求流量汇源后端应用或者只是一小部分流量回源
- 业务降级:当高并发流量来袭,保证核心业务是正常的,并保障数据最终一致性即可。这样就把一些同步调用改成异步调用,优先处理高优先级数据或特殊特征的数据,合理分配进入系统的流量,以保障系统可用。
1.2.2.限流
当监控发现外部流量超过阈值或内部资源使用达到阈值(通过压测、上线观察、调整)时,告知各系统限流打开。
限流的目的是防止恶意请求流量,或者防止流量超出系统峰值。思路如下:
基于请求的限流:
- 限制请求总量。如腾讯会议最多500人。
- 限制时间量。如一个时间窗口内最多接受100个请求。
基于关键资源的限流:
- 统计连接数、线程数、cup等硬件参数。难点是如何确定哪些是关键资源、阈值是多少。
- 使用池化技术:线程池、连接池;使用队列排队;
相关限流的算法:
- 滑动时间窗口:有突刺
- 漏桶: 请求进入队列的速度不受限制,但是超过队列的大小就拒绝,请求出队列的速度固定。请求会匀速出队列。
- 令牌桶:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。有突刺。
有些大流量是正常的用户,这种是要临时的水平扩容。
原则是限制流量穿透到后端薄弱的应用层
1.2.3.熔断
熔断发生的三个必要条件,缺一不可,必须全部满足才能开启 hystrix 的熔断功能:
有一个统计的时间周期,滚动窗口;如1000毫秒
请求次数必须达到一定数量;如20次
失败率达到阈值;如50%
熔断器的三个状态:关闭状态。关闭状态时用户请求是可以到达服务提供方的。
开启状态。开启状态时用户请求是不能到达服务提供方的,直接会走降级方法。
半开状态。当熔断器开启时,过一段时间后,熔断器就会由开启状态变成半开状态。半开状态的熔断器是可以接受用户请求并把请求传递给服务提供方的,这时候如果远程调用返回成功,那么熔断器就会有半开状态变成关闭状态,反之,如果调用失败,熔断器就会有半开状态变成开启状态。
Hystrix功能建议在并发比较高的方法上使用,并不是所有方法都得使用的。
Sentinel的熔断策略是根据响应时间,响应时间超过阈值,熔断开关打开。
1.2.4.恢复
撤出限流、消除降级、关闭熔断
熔断使用半开状态,完成吞吐量爬升、缓存预热。
灰度发布,限流阈值逐步提升。
1.2.5.隔离
- 数据隔离:数据按照重要性排序、分库
- 机器隔离:给重要的用户单独配置服务器,用用户标识去路由
- 线程池隔离:线程池分配。hystrix
- 信号量隔离:计数器。hystrix
- 集群隔离:服务分组(注册中心)、秒杀
- 机房隔离:3个服务。局域网IP、路由。
- 读写隔离:主从
- 动静隔离:识别静态资源。nginx CDN
- 爬虫隔离:对IP的访问频率
- 冷热隔离:秒杀、抢购。读:缓存;写:缓存+队列
1.2.6.异地多活
异地机房部署相同的服务,同时对外提供服务(不是备份)。防止因为停电、火灾、水灾、地震、战争等问题导致服务不可用。
异地多活通常考虑RTT(round trip time):网络请求一个来回消耗的传输时间。光纤光速计算 300000 KM/s,两个机房如果一个在上海,一个在新疆,隔5000KM,
rtt = 300000 / 5000 * 2 = 120毫秒的往返时延。
多活要求:
请求任何一个节点,都能正常响应
某些系统故障,用户访问其他系统也能访问
分类:同城异区:距离较近,可以防止停电、机房起火
跨域异地:距离较近,可以防止停电、机房起火、火灾、水灾、地震。
跨国异地(隔离):延迟,已经无法让系统提供服务了。通常在异国开展业务,数据和服务就放在异国,和国内数据是隔离的。
异地多活容易出现数据不一致问题,要保证核心业务的多活。如用户系统有注册、登陆、修改用户信息等功能,保证正常注册、登陆多活,修改用户信息可以根据时间合并数据。
1.2.7.可回滚
灰度发布。版本化机制,当程序出错时,回滚到上一个版本。
二、业务设计原则
2.1.防重、幂等
重复提交业务,消息中间件重复消费消息。使用分布式锁、数据库唯一键等保证。
2.2.模块复用
相同的功能只开发一次,模块化。不要到处拷贝相同的代码。
2.3.可追溯
可以快速追踪到问题涉及的这个数据链路,快速定位问题。traceId
2.4.反馈原则
给出精确友好的结果反馈。如http接口调用异常时尽量给出精确的异常原因,降低内外部沟通成本。
2.5.流程可定义
相关工作有明确的流程规范
2.6.系统审批化
系统变更需要审批
2.7.文档和注释
完善文档和注释
2.8.备份
- 代码备份:git、分支
- 数据备份:运维备份,操作记录备份。
- 人员备份:不因个人离职导致项目停滞。
2.9.规范
制定规范,定期review。
三、系统性能常见指标
3.1.响应时间(Response time)
响应时间就是用户感受软件系统为其服务所耗费的时间,对于网站系统来说,响应时间就是从点击了一个页面计时开始,到这个页面完全在浏览器里展现计时结束的这一段时间间隔,看起来很简单,但其实在这段响应时间内,软件系统在幕后经过了一系列的处理工作,贯穿了整个系统节点。
根据“管辖区域”不同,响应时间可以细分为:
- 服务器端响应时间这个时间指的是服务器完成交易请求执行的时间,不包括客户端到服务器端的反应(请求和耗费在网络上的通信时间),这个服务器端响应时间可以度量服务器的处理能力。
- 网络响应时间,这是网络硬件传输交易请求和交易结果所耗费的时间。
- 客户端响应时间,这是客户端在构建请求和展现交易结果时所耗费的时间,对于普通的瘦客户端Web应用来说,这个时间很短,通常可以忽略不计;但是对于胖客户端Web应用来说,比如Java
applet、AJAX,由于客户端内嵌了大量的逻辑处理,耗费的时间有可能很长,从而成为系统的瓶颈,这是要注意的一个地方。
那么客户感受的响应时间其实是等于客户端响应时间+服务器端响应时间+网络响应时间。细分的目的是为了方便定位性能瓶颈出现在哪个节点上。
3.2.吞吐量(Throughput)
吞吐量是我们常见的一个软件性能指标,对于软件系统来说,“吞”进去的是请求,“吐”出来的是结果,而吞吐量反映的就是软件系统的“饭量”,也就是系统的处理能力,具体说来,就是指软件系统在每单位时间内能处理多少个事务/请求/单位数据等。但它的定义比较灵活,在不同的场景下有不同的诠释,比如数据库的吞吐量指的是单位时间内,不同SQL语句的执行数量;而网络的吞吐量指的是单位时间内在网络上传输的数据流量。吞吐量的大小由负载(如用户的数量)或行为方式来决定。举个例子,下载文件比浏览网页需要更高的网络吞吐量。
3.3.资源使用率(Resource utilization)
常见的资源有:CPU占用率、内存使用率、磁盘I/O、网络I/O。
3.4.点击数(Hits per second)
点击数是衡量Web Server处理能力的一个很有用的指标。需要明确的是:点击数不是我们通常理解的用户鼠标点击次数,而是按照客户端向Web
Server发起了多少次http请求计算的,一次鼠标可能触发多个http请求,这需要结合具体的Web系统实现来计算。
3.3.并发用户数(Concurrent users)
并发用户数用来度量服务器并发容量和同步协调能力。在客户端指一批用户同时执行一个操作。并发数反映了软件系统的并发处理能力,和吞吐量不同的是,它大多是占用套接字、句柄等操作系统资源。
另外,度量软件系统的性能指标还有系统恢复时间等,其实凡是用户有关资源和时间的要求都可以被视作性能指标,都可以作为软件系统的度量,而性能测试就是为了验证这些性能指标是否被满足。
四、总结
一个系统的设计,不仅需要考虑实现业务功能,还要保证系统高并发、高可用等。在系统容量规划(流量、容量等)、SLA制定(吞吐量、响应时间、可用性、降级方案等)、压测方案(线上、test等)、监控报警(机器负载、响应时间、可用率等)、应急预案(容灾、降级、限流、隔离、切流量、可回滚)等方面,也要有一些原则来进行设计。