通用设计方法
Scale-out(横向扩展)
采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量
系统设计初期会考虑使用 Scale-up 的方式,因为这种方案足够简单,但是当系统并发超过了单机的极限时,就要使用 Scale-out 的方式缓存
使用缓存来提高系统的性能异步
在某些场景下,未处理完成之前,我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求
高并发系统的演进应该是循序渐进,以解决系统中存在的问题为目的和驱动力的
架构分层
MVC(Model-View-Controller)架构
它将整体的系统分成了 Model(模型),View(视图)和 Controller(控制器)三个层次
分层的优势
- 简化系统设计,让不同的人专注做某一层次的事情
- 分层之后可以做到很高的复用
- 分层架构可以更容易做横向扩展
分层的劣势
- 增加了代码的复杂度
- 每个层次独立部署,层次间通过网络来交互,在性能上会有损耗
系统设计目标:高性能
高并发系统设计的三大目标:高性能、高可用、可扩展
性能反应了系统的使用体验;可用性则表示系统可以正常服务用户的时间;易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担峰值流量
性能优化原则
- 问题导向
- 八二原则
- 数据支撑
- 过程持续
度量性能的指标是系统接口的响应时间,常见的特征值有平均值,最大值,分位值
设立性能优化的目标时通常会这样表述:在每秒 1 万次的请求量下,响应时间 99 分位值在 10ms 以下。
性能优化思路
提高系统的处理核心数(不可能无限制增加)
减少单次任务的响应时间
CPU密集型和IO密集型优化方式不一样
对于CPU密集型,选用更高效的算法,对于IO密集型,区分磁盘IO和网络IO
系统设计目标:高可用
两个指标
MTBF(Mean Time Between Failure)表示两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高
MTTR(Mean Time To Repair)表示平均故障时间。这个值越小,故障对于用户的影响越小
Availability = MTBF / (MTBF + MTTR)
设计思路
系统设计方面:failover(故障转移)、超时控制以及降级和限流等
系统运维方面:灰度发布、故障演练等
系统设计目标:高扩展
数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等都是系统扩展时需要考虑的因素
设计思路:拆分
- 存储层的扩展性
- 业务层的扩展性
池化技术
核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本
用连接池预先建立数据库连接
用线程池预先创建线程
数据库优化:主从分离
依据一些云厂商的 Benchmark 的结果,在 4 核 8G 的机器上运 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS
主从读写的两个技术关键点
主从复制
MySQL的主从复制
在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行
在读流量比较大的情况下可以部署多个从库共同承担读流量
从库也可以当成一个备库来使用,以避免主库故障导致数据丢失
主从同步的延迟会带一些问题
通过中间件来解决多个数据库的访问问题屏蔽主从分离带来的访问数据库方式的变化
数据库优化:分库分表
通过数据分片解决数据库的写入请求量大造成的性能和可用性方面的问题,分库分表是一种常见的将数据分片的方式
垂直拆分:垂直拆分的关注点在于业务相关性
水平拆分:水平拆分的关注点在于数据的特点
按照某一个字段的哈希值做拆分
按照某一个字段的区间来拆分
发号器:保证ID全局唯一
当面临高并发的查询数据请求时,可以使用主从读写分离的方式,部署多个从库分摊读压力;当存储的数据量达到瓶颈时,可以将数据分片存储在多个节点上,降低单个存储节点的存储压力
数据库主键的选择
使用业务字段作为主键
使用生成的唯一ID作为主键
在单库单表的场景下,可以使用数据库的自增字段作为 ID,但是当数据库分库分表后,使用自增字段就无法保证 ID 的全局唯一性了
基于 Snowflake 算法搭建发号器的实现方式
- 嵌入到业务代码里,也就是分布在业务服务器中
- 作为独立的服务部署,这也就是我们常说的发号器服务
缺点:依赖系统的时间戳;QPS不高的情况下生成ID不均匀
不使用UUID的原因
NoSQL:数据库和NoSQL互补
NoSQL分类
- Redis、LevelDB 这样的 KV 存储
- Hbase、Cassandra 这样的列式存储数据库
- MongoDB、CouchDB 这样的文档型数据库
提升性能
一般来说,随机IO的读写效率要比顺序IO小两到三个数量级,所以想要提升写入的性能就要尽量减少随机IO
NoSQL 数据库使用一些算法将对磁盘的随机写转换成顺序写,提升了写的性能
LSM 树(Log-Structured Merge Tree)牺牲了一定的读性能来换取写入数据的高性能
特定场景支持
全文搜索功能
提升扩展性
天生支持分布式
缓存:数据库成为瓶颈后,如何加速查询
位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存
内存是最常见的一种缓存数据的介质
静态缓存、分布式缓存和热点本地缓存
缓存:选择缓存的读写策略
Cache Aside(旁路缓存)策略
读策略的步骤是:
- 从缓存中读取数据
- 如果缓存命中,则直接返回数据
- 如果缓存不命中,则从数据库中查询数据
- 查询到数据后,将数据写入到缓存中,并且返回给用户
写策略的步骤是:
- 更新数据库中的记录
- 删除缓存记录
当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响
Read/Write Through(读穿 / 写穿)策略
Write Back(写回)策略
缓存:高可用
分布式缓存的高可用方案:客户端方案、中间代理层方案和服务端方案
- 客户端方案就是在客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性
- 中间代理层方案是在应用代码和缓存节点之间增加代理层,客户端所有的写入和读取的请求都通过代理层,而代理层中会内置高可用策略,帮助提升缓存系统的高可用
- 服务端方案就是 Redis 2.4 版本后提出的 Redis Sentinel 方案
缓存:缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况
回种空值以及使用布隆过滤器
CDN:加速静态资源
静态资源
- 图片,视频,流媒体
- JS文件,CSS文件,静态HTML文件等
静态资源访问的关键点是就近访问
CDN(Content Distribution NetWork,内容分发网络):将静态资源分发到位于多个地理位置机房的服务器上
如何让用户的请求到达CDN节点
DNS解决域名映射问题
如何找到离用户最近的CDN节点
GSLB(Glolbal Server Load Balance,全局负载均衡)
数据迁移
消息队列:秒杀如何处理大量请求
秒杀场景下处理读请求
- 缓存策略
- 静态化数据,CDN加速
- 限流策略
秒杀场景下处理写请求:消息队列
高并发的写请求只在几秒或者几十秒内存在
把秒杀请求暂存在消息队列中
削峰填谷,异步处理,解耦合
异步处理简化了业务流程 的步骤,提升系统性能
削峰填谷可以削去峰值流量,让业务逻辑的处理更加缓和
解耦合可以将秒杀系统和数据系统解耦开,彼此互不影响
消息投递:保证消息仅仅被消费一次
消息队列场景
消息从生产者写入到消息队列的过程
超时重传,可能有消息重复消息在消息队列的存储场景
异步刷盘造成数据丢失,考虑通过集群方式部署多个副本备份数据消息被消费者消费的过程
保证消息生产和消费的过程是幂等的
消息队列:提升消息队列系统性能
系统架构:系统服务化拆分
一体化架构的痛点
- 数据库连接数可能是系统的瓶颈
- 增加研发成本
- 影响系统运维
微服务架构
微服务拆分的原则
问题和思路
- 服务接口的调用,服务注册中心
- 多个服务间的依赖关系
- 分布式追踪,服务端监控报表
RPC框架:毫秒级服务调用
RPC(Remote Procedure Call,远程过程调用):通过网络,调用另一台计算机上部署服务的技术
过程
- 客户端将调用的类名,方法名,参数名,参数值等信息,序列化成二进制流
- 客户端将二进制流通过网络发送给服务端
- 服务端接收二进制流后进行反序列化,得到需要调用的类名,方法名,参数名和参数值,再通过动态代理的方式,调用对应的方法得到返回值
- 服务端序列化返回值,通过网络发送给客户端
- 客户端对结果反序列化后,得到调用结果
提升网络传输性能
非阻塞IO+IO多路复用
开启tcp_nodelay
选择序列化方式
JSON
Protobuf
注册中心:分布式系统寻址
注册中心
- 提供了服务地址的存储
- 当存储内容发生变化时,可以将变更的内容推送给客户端
服务节点的增加和减少对于客户端来说就是透明的
优雅关闭:将RPC服务从注册中心的服务列表删除掉,然后等RPC服务端没有流量后再将服务端停掉
服务状态管理
主动探测
心跳机制
服务治理
- 服务的注册和发现
- 服务的监控
- 熔断和引流
- 分布式追踪
- 负载均衡