高并发系统40问
通用设计方法:
- 横向扩展:例如数据库一主多从、分库分表、存储分片;
- 缓存
- 异步
架构分层思想
MVC:表现层、逻辑层、数据访问层。 网络分层:OSI七层模型、TCP/IP四层模型。 Linux文件系统分层设计:VFS, Ext3/Ext4, General Block Device Layer。 分层的架构可以为横向扩展提供便捷。
系统设计目标
三大目标:高性能、高可用、可扩展。 可扩展:流量分为平时流量和峰值流量,可扩展的系统能够在短时间完成扩容,支撑峰值流量。
高性能
性能度量指标:分位值
从用户使用体验的角度来看,200ms是第一个分界点:接口的响应时间在200ms之内,用户是感觉不到延迟的,就像是瞬时发生的一样。而1s是另外一个分界点:接口的响应时间在1s之内时,虽然用户可以感受到一些延迟,但却是可以接受的,超过1s之后用户就会有明显等待的感觉,等待时间越长,用户的使用体验就越差。所以,健康系统的99分位值的响应时间通常需要控制在200ms之内,而不超过1s的请求占比要在99.99%以上。
高并发的性能优化:
- 提升并发能力
- 减少单次响应时间
提升并发能力
性能测试中的拐点模型。性能测试的目的就是找到拐点。
减少单次响应时间
根据系统类型:
- CPU密集型:选用高效算法;
- IO密集型:掌握找瓶颈点的工具,不同原因采用不同的应对方法;
高可用(HA)
可用性度量指标
- MTBF(Mean Time Between Failure): 平均故障间隔,越大越好。
- MTTR(Mean Time To Reapir):故障的平均恢复时间,越小越好。
系统可用性 Availability = MTBF / (MTBF + MTTR). 这个公式计算出来是一个比例,我们会使用几个九来描述系统可用性。 一般来说,我们的核心业务系统的可用性,需要达到四个九,非核心系统的可用性最多容忍到三个九。在实际工作中,你可能听到过类似的说法,只是不同级别,不同业务场景的系统对于可用性要求是不一样的。
高可用系统设计思路
- 系统设计:Design for failure,设计时考虑failover、超时控制、降级和限流。
- 系统运维:灰度发布、故障演练。
可扩展
系统不同分层上存在一些瓶颈点,制约着系统横向扩展能力。比如:带宽、LB、DB、缓存、第三方等。我们要知道系统并发到达不同量级时,哪一个因素会成为瓶颈点。
高可扩展设计思路
拆分:把庞大的系统拆分成独立的,有单一职责的模块。
- 存储层拆分:首先按业务维度,其次按数据特征水平拆分(最好一次性增加足够节点避免后续拆分需要数据迁移)。
- 业务层拆分:业务维度、重要性维度、请求来源维度。
池化技术
空间换时间。比如数据库连接池、HTTP连接池、Redis连接池、线程池。 常见的一些故障:
- 数据库域名对应的IP发生变更,池子的连接使用的还是旧IP。
- MySQL Server端wait_timeout超时关闭连接,Client端感知不到,使用时才会发现出错。
解决:定期检测池中的连接是否可用。
主从读写分离
主从读写分离的两个关键点:
- 主从复制:一个主库最多挂3~5个从库(log dump线程资源消耗)。
- 如何访问多个节点的数据库。
只写主库,只读从库。即使写请求会锁表或者锁记录,也不会影响到读请求的执行。 带来的问题:主从延迟。把从库落后的时间作为一个重点的数据库指标做监控和报警
分库分表
垂直拆分、水平拆分。 分库分表引入的一个最大的问题就是引入了分区键,我们之后所有的查询都需要带上这个字段,才能找到数据所在的库和表。
案例,在用户库中我们使用ID作为分区键,这时如果需要按照昵称来查询用户。可以建立一个昵称和ID的映射表,在查询的时候要先通过昵称查询到ID,再通过ID查询完整的数据,这个表也可以是分库分表的。
分库分表引入的另外一个问题是一些数据库的特性在实现时可能变得很困难,比如连表join和count。
分库分表后ID的全局唯一性
- 使用业务字段作为主键(易变更,不推荐)。
- 使用生成的唯一ID作为主键,单调递增,提升DB写入性能。
基于雪花算法的唯一ID生成-发号器
两种实现方式:
- 嵌入到业务代码里:需要更多的机器ID位数来支持更多的业务服务器,另外,由于业务服务器的数量很多,我们很难保证机器ID的唯一性,所以就需要引入ZooKeeper等分布式一致性组件来保证每次机器重启时都能获得唯一的机器ID。
- 部署为独立服务。 问题:时钟回拨问题。
坑:如果请求发号器的QPS不高,比如说发号器每毫秒只发一个ID,就会造成生成ID的末位永远是1,那么在分库分表时如果使用ID作为分区键就会造成库表分配的不均匀。
使用NoSQL数据库加速
NoSQL数据库
- KV存储:Redis、LevelDB,极高的读写性能;
- 列式存储:Hbase、Cassendra,以列为单位来存储数据,适合离线分析;
- 文档型数据库:MongoDB、CouchDB,模式自由,字段可以任意扩展;
很多NoSQL数据库都在使用基于LSM树(Log-Structred Merge Tree)的存储索引,牺牲了一定的读性能来换取写入数据的高性能。
缓存
缓存分类:
- 静态缓存:缓存静态资源;
- 分布式缓存:缓存动态资源;
- 热点本地缓存:应对极端的热点查询,阻挡热点查询对分布式缓存节点或数据库的压力;
缓存的不足:
- 缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性,才能保证缓存命中率。
- 缓存会给整体系统带来复杂度,并且会有数据不一致风险。
- 缓存通常使用内存作为存储介质,内存是有限的,使用缓存是要评估量级。
- 缓存给运维带来成本。
缓存的读写策略:
- Cache Aside (旁路缓存)策略:更新数据库记录后删除缓存记录;先读取缓存,命中返回,不命中则查询数据库并种缓存。
- Read/Write Throught: 适合本地缓存使用的模式。
- Write Back:计算机体系结构中的策略。
分布式缓存的高可用方案:
- 客户端方案(smart client):客户端配置多个缓存节点。客户端使用分片算法,比如一致性哈希算法,缓存系统使用主从或多副本架构。
- 中间代理方案:应用代码和缓存节点之间增加代理层,请求会经过代理节点。比如codis。
- 服务端方案:比如Redis Sentinel。
应对缓存穿透:
- 种空缓存:会占用缓存空间,要先评估缓存容量能否支持。
- 使用布隆过滤器
应对缓存击穿:
- 加锁
CDN
静态资源如果放在Web服务器上会占用很高的带宽,影响动态请求。 静态资源访问的关键点是就近访问,才能达到性能最优。 DNS技术是CDN实现中使用的核心技术,可以将用户的请求映射到CDN节点上。 CDN的核心模块:
- 智能调度系统:(CDN专用DNS服务器)GSLB 全局负载均衡;
- 边缘节点集群
- 回源系统
- 源站与存储
MQ
MQ作用:异步、解耦、削峰填谷。对峰值写流量削峰填谷;对次要的业务做异步处理;对不同的业务模块做解耦合。
如何保证消息只被消费一次(不丢,不重)
消息丢失的场景和解决方案:
- 生产过程中丢失:网络抖动——消息重传
- 消息队列中丢失:异步刷盘,掉电数据丢失——同步刷盘,集群部署,配置acks=all
- 消费过程中丢失:消费异常也commit了offset——要等到消息接收和处理完成后才能commit
生产幂等: 给每个生产者和每个消息一个唯一ID,Kafka服务端会存储<生产者ID,最后一条消息ID>的映射,当某一个生产者产生新的消息时,消息队列服务端会比对消息ID是否与存储的最后一条ID一致,如果一致就认为是重复的消息,服务端会自动丢弃。
消费者幂等: 消费端生成唯一ID,处理消息前检查ID是否存在。 如果消费消息后且在存储ID之前宕机,则还是有可能重复消费,这时需要引入事务机制。but,对于消息重复没有特别严格的要求,就不用考虑引入事务机制了。
注册中心
全链路追踪
负载均衡
LVS适合在入口处承担大流量的请求分发,而Nginx要部署在业务服务器之前做更细维度的请求分发。在项目的架构中,我们一般会同时部署LVS和Nginx来做HTTP应用服务的负载均衡。也就是说,在入口处部署LVS将流量分发到多个Nginx服务器上,再由Nginx服务器分发到应用服务器上。
微服务之间使用客户端负载均衡。
负载均衡策略最好使用动态策略,结合服务的负载状态。
API网关
API网关是系统的边界,可以出入系统的流量做统一管控。
分类:
- 入口网关:请求路由,协议转换,限流熔断;
- 出口网关:审计,访问控制;
API网关中的线程池可以针对不同的接口或者服务做隔离和保护,这样可以提升网关的可用性。
监控体系
服务端监控 用户体验-客户端监控
全链路压测
压测:在高并发大流量下测试,找到系统的性能隐患。 注意点:
- 压测最好使用线上数据和线上环境;
- 压测不能使用模拟的请求,使用线上流量;
- 不要从一台服务器发起流量;
全链路压测平台包含的模块:
- 流量构造和产生模块 - 流量拷贝工具;
- 压测数据隔离模块 - 流量染色;读请求(下行流量):mock不能压测的服务或组件,写请求(上行流量):搭建影子库;
- 系统健康度检查和压测流量干预模块;
配置中心
我们一般会在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存。
熔断
为防止服务雪崩,要实现熔断机制。在访问第三方服务或资源的时候都要考虑增加降级开关或熔断机制。
开关降级:通过开关控制降级; 熔断降级: 限流降级
限流
通过限制到达系统的并发请求数量,保证系统能够正常响应部份用户请求,而对于超过限制的流量拒绝服务。
限流一般在API网关中对系统整体流量塑形,或RPC客户端中保证下游服务不被压垮。
限流算法:
- 固定窗口、滑动窗口:无法限制短时间之内的集中流量,实际项目中不会使用。
- 漏桶算法:面对突发流量的时候,采用的解决方式是缓存在漏桶中,这样流量的响应时间就会增长。
- 令牌桶算法(prefer):令牌数量,单机存变量,分布式环境存redis;每次取一批令牌。
系统设计
计数系统设计
Feed流
数据迁移
数据迁移一般有两种方案:
- 双写
- 级联同步
这两种方案其实不仅适用于mysql,也适用于redis。
双写
- 将新库配置为源库的从库来同步数据;如果需要将数据同步到多库多表,可以使用第三方工具获取binlog增量日志Canal,然后按照分库分表写入新库表。
- 改造业务代码,写入时不仅写入旧库也要写入新库。基于性能考虑,写旧库要保证成功,写入新库可以异步,打印失败日志后续补写。
- 抽样校验数据。
- 灰度切流,逐渐放量到100%。
- 切换有问题时切回旧库。
- 观察几天没问题可以切为写新库。
级联同步
- 将新库配置为旧库的从库,用作数据同步;
- 再将一个备库(和旧库在同一个环境)配置为新库的从库,作为数据备份;
- 等到三个库写入一致后,将读流量切到新库;
- 暂停写流量,将写操作切到新库;
回滚方案:切到备份库。
抵抗流量峰值
峰值流量到来的时候,迅速确定系统的瓶颈点,并制定相应的预案。高并发流量流经系统时,线路上的每个组件、设备、线路都有可能成为系统的瓶颈点。切流、扩容、降级、限流是几种常见的抵御高并发冲击的方案。
切流
把流量从一个机房切换到另一个机房。切流方式一般有两种:
- 一种是全部流量流经一个机房的入口,然后在入口下面的某一层LB转到另一个机房。流量从一个机房到另一个机房需要跨专线,依赖专线稳定性,rt也会增加一点。
- 另一种是从域名解析也就是流量的最前端切换。DNS生效时间在小时级别。
扩容
通过增加冗余或提升配置的方式,提升系统或组件的流量承载能力。包括容器扩容、中间件扩节点、专线扩容。
降级
暂时关闭次要功能来保障系统整体的稳定性。
限流
超过预期流量,限流。