01 | 高并发系统:它的通用设计方法是什么?
三种方法:
Scale-out(横向扩展):分而治之是一种常见的高并发系统设计方法,采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量。
缓存:使用缓存来提高系统的性能,就好比用“拓宽河道”的方式抵抗高并发大流量的冲击。
异步:在某些场景下,未处理完成之前我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。
Scale-up vs Scale-out
Scale-up是提升机器性能,比如4核8G换8核16G,在系统初期是个不错的思路,因为方案简单;当达到单机极限时,采用Scale-out的方式,即加机器,但多节点会引入一些复杂问题。比如,如何无感知增加、删除节点等。
使用缓存提升性能
数据是持久化到磁盘中的,盘片是磁盘的存储介质,被划分为多个同心圆,也就是磁道,数据就存储在磁道中。
普通磁盘的寻道时间是10ms左右,内存寻址在ns级别。所以磁盘是整个计算机体系最慢的一环,因此选用内存作为存储介质来提升性能。
异步处理
什么是同步,什么是异步?
以方法调用为例,
同步调用代表调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在高并发下会造成整体系统性能下降甚至发生雪崩。
异步调用代表调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。
02 | 架构分层:我们为什么一定要这么做?
什么是分层架构
常见的MVC、七层OSI网络模型、四层TCP/IP协议等。
分层有什么好处
简化系统设计,让不同的人专注做某一层次的事情。比如,我们开发的程序就属于应用层,不用数据传输的细节。
再有,分层之后可以做到很高的复用。意思是,某一层如果通用,可以抽出来在多个系统中复用。
最后一点,分层架构可以让我们更容易做横向扩展。业务逻辑里面包含有比较复杂的计算,导致CPU成为性能的瓶颈,那这样就可以把逻辑层单独抽取出来独立部署,然后只对逻辑层来做扩展。
03 | 系统设计目标(一):如何提升系统性能?
三大目标:高性能、高可用、可扩展
性能优化原则
问题导向:盲目提早优化会增加复杂度,浪费精力,毕竟问题还没有发生
遵循八二原则:20%精力解决80%性能问题
数据支撑:时刻了解优化后响应时间减少了多少,提升了多少吞吐量
可持续:未达到某一目标,需要不断寻找并解决性能瓶颈
高并发下的性能优化
假设现有一个系统,只有一个处理核心,任务响应时间10ms,吞吐量每秒100次。如何提高系统性能?
一是提高系统处理核心数,另一种是降低单次任务响应时间。
04 | 系统设计目标(二):系统怎样做到高可用?
可用性的度量
MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
Availability(可用性) = MTBF / (MTBF + MTTR)
07 | 池化技术:如何减少频繁创建数据库连接的性能损耗?
数据库连接池有两个最重要的配置:最小连接数和最大连接数,它们控制着从连接池中获取连接的流程:
小于最小连接数,创建新的连接处理请求
如果有空闲连接直接复用
如果空闲池中无可用且当前连接数小于最大连接数,继续创建新的连接
如果当前连接数大于等于最大连接数,则等待一定时间来等待旧连接可用
如果超过了这个时间就抛异常
池化技术,核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,总之是好处多多。
池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
池子中的对象需要在使用之前预先初始化完成,这叫做池子的预热,比方说使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
线程池:
CPU密集型任务只需要创建和CPU核数相当的线程就好了,多了反而会造成线程上下文切换,降低任务执行效率。所以当前线程数超过核心线程数时,线程池不会增加线程,而是放在队列里等待核心线程空闲下来。
IO密集型任务在执行IO操作的时候CPU就空闲了下来,这时如果增加执行任务的线程数而不是把任务暂存在队列中,就可以在单位时间内执行更多的任务,大大提高了任务执行的吞吐量。Tomcat使用的线程池就不是JDK原生的线程池,而是做了一些改造,当线程数超过coreThreadCount之后会优先创建线程,直到线程数到达maxThreadCount,这样就比较适合于Web系统大量IO操作的场景了。
08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?
主从读写分离
大部分场景是读多写少,比如刷朋友圈比发朋友圈多。
需要现将度写流量区分开,才能针对性地对读流量做扩展。
主从读写的两个技术关键点
1. 主从复制
MySQL的主从复制是依赖于binlog的,也就是记录MySQL上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将binlog中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待binlog同步的完成。
主从复制的过程是这样的:首先从库在连接到主节点时会创建一个IO线程,用以请求主库更新的binlog,并且把接收到的binlog信息写入一个叫做relay log的日志文件中,而主库也会创建一个log dump线程来发送binlog给从库;同时,从库还会创建一个SQL线程读取relay log中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。
随着从库数量增加,从库连接上来的IO线程比较多,主库也需要创建同样多的log dump线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用中,一般一个主库最多挂3~5个从库。
2. 如何访问数据库
为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。
一种是以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将SQL语句发给某一个指定的数据源来处理,然后将处理结果返回。
另一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对SQL语句做必要的改写,然后发往指定的数据源。
09 | 数据库优化方案(二):写入数据量增加时,如何实现分库分表?
分库分表后,每个节点只保存部分的数据,这样可以有效地减少单个数据库节点和单个数据表中存储的数据量,在解决了数据存储瓶颈的同时也能有效地提升数据查询的性能。同时,因为数据被分配到多个数据库节点上,那么数据的写入请求也从请求单一主库变成了请求多个数据分片节点,在一定程度上也会提升并发写入的性能。
数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。
垂直拆分的原则一般是按照业务类型来拆分,核心思想是专库专用。
数据库垂直拆分后依然不能解决某一个业务模块的数据大量膨胀的问题,比如你的系统遭遇某一个业务库的数据量暴增。
如何对数据库做水平拆分
水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。
拆分的规则有下面这两种:
按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表,我们一般按照这些实体表的ID字段来拆分。比如说我们想把用户表拆分成16个库,每个库是64张表,那么可以先对用户ID做哈希,哈希的目的是将ID尽量打散,然后再对16取余,这样就得到了分库后的索引值;对64取余,就得到了分表后的索引值。
另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。你知道在内容表里面有“创建时间”的字段,而我们也是按照时间来查看一个人发布的内容。我们可能会要看昨天的内容,也可能会看一个月前发布的内容,这时就可以按照创建时间的区间来分库分表,比如说可以把一个月的数据放入一张表中,这样在查询时就可以根据创建时间先定位数据存储在哪个表里面,再按照查询条件来查询。
解决分库分表引入的问题
最大的问题就是引入了分库分表键,也叫做分区键,也就是我们对数据库做分库分表所依据的字段,查询时必须带着分区键。
另外一个问题是一些数据库的特性在实现时可能变得很困难。比如说在未分库分表之前查询数据总数时只需要在SQL中执行count()即可,现在数据被分散到多个库表中,我们可能要考虑其他的方案,比方说将计数的数据单独存储在一张表中或者记录在Redis里面。
10 | 发号器:如何保证分库分表后ID的全局唯一性?
分库分表还有一个问题是主键的全局唯一性的问题。
在单库单表的场景下,我们可以使用数据库的自增字段作为ID,因为这样最简单,对于开发人员来说也是透明的。但是当数据库分库分表后,使用自增字段就无法保证ID的全局唯一性了。
基于Snowflake算法搭建发号器
为什么不用UUID做主键?
UUID(Universally Unique Identifier,通用唯一标识码)不依赖于任何第三方系统,所以在性能和可用性上都比较好,一般会使用它生成Request ID来标记单次请求,但是如果用它来作为数据库主键,它会存在以下几点问题。
首先,生成的ID最好具有单调递增性,也就是有序的,而UUID不具备这个特点。为什么ID要是有序的呢?因为在系统设计时,ID有可能成为排序的字段。
另一个原因在于ID有序也会提升数据的写入性能。MySQL InnoDB存储引擎使用B+树存储索引数据,而主键也是一种索引。索引数据在B+树中是有序排列的,当插入的下一条记录的ID是递增的时候,数据库只需要把它追加到后面就好了,但是如果插入的数据是无序的,需要先找到插入位置,再挪动后面数据,就造成了多余的数据移动的开销。
另外,磁盘的顺序写比随机写性能要好得多,因为省去了“寻道”这一步骤。
UUID不能作为ID的另一个原因是它不具备业务含义。
Snowflake的核心思想是将64bit的二进制数字分成若干部分,每一部分都存储有特定含义的数据,比如说时间戳、机器ID、序列号等等,最终生成全局唯一的有序ID。它的标准算法是这样的:
41位的时间戳大概可以支撑pow(2,41)/1000/60/60/24/365年,约等于69年,对于一个系统是足够了。10位的机器ID可以继续划分为2~3位的IDC标示(可以支撑4个或者8个IDC机房)和7~8位的机器ID(支持128-256台机器);12位的序列号代表着每个节点每毫秒最多可以生成4096的ID。
不同公司也会依据自身业务的特点对Snowflake算法做一些改造,比如说减少序列号的位数增加机器ID的位数以支持单IDC更多的机器,也可以在其中加入业务ID字段来区分不同的业务。
比方说我现在使用的发号器的组成规则就是:1位兼容位恒为0 + 41位时间信息 + 6位IDC信息(支持64个IDC)+ 6位业务信息(支持64个业务)+ 10位自增信息(每毫秒支持1024个号)
它也有一些缺点,其中最大的缺点就是它依赖于系统的时间戳,一旦系统时间不准,就有可能生成重复的ID。所以如果我们发现系统时钟不准,就可以让发号器暂时拒绝发号,直到时钟准确为止。
另外,如果请求发号器的QPS不高,比如说发号器每毫秒只发一个ID,就会造成生成ID的末位永远是1,那么在分库分表时如果使用ID作为分区键就会造成库表分配的不均匀。这一点,也是我在实际项目中踩过的坑,而解决办法主要有两个:
1.时间戳不记录毫秒而是记录秒,这样在一个时间区间里可以多发出几个号,避免出现分库分表时数据分配不均。
生成的序列号的起始号可以做一下随机,这一秒是21,下一秒是30,这样就会尽量地均衡了。
11 | NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?
使用NoSQL提升写入性能
很多NoSQL数据库都在使用的基于LSM树的存储引擎,牺牲了一定的读性能来换取写入数据的高性能。
数据首先会写入到一个叫做MemTable的内存结构中,在MemTable中数据是按照写入的Key来排序的。
MemTable在累积到一定规模时,它会被刷新生成一个新的文件,我们把这个文件叫做SSTable(Sorted String Table)。当SSTable达到一定数量时,我们会将这些SSTable合并,减少文件的数量,因为SSTable都是有序的,所以合并的速度也很快。
当从LSM树里面读数据时,我们首先从MemTable中查找数据,如果数据没有找到,再从SSTable中查找数据。因为存储的数据都是有序的,所以查找的效率是很高的,只是因为数据被拆分成多个SSTable,所以读取的效率会低于B+树索引。
场景补充
利用ES的倒排索引做搜索。
提升扩展性
NoSQL数据库天生支持分布式,支持数据冗余和数据分片的特性。
其一是Replica,也叫做副本集,你可以理解为主从分离。
其二是Shard,也叫做分片,你可以理解为分库分表。
12 | 缓存:数据库成为瓶颈后,动态数据的查询要如何加速?
凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。
缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。
缓存分类
常见的缓存主要就是静态缓存、分布式缓存和热点本地缓存这三种。
静态的资源的缓存你可以选择静态缓存,动态的请求你可以选择分布式缓存,那么什么时候要考虑热点本地缓存呢?
是当我们遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力。
13 | 缓存的使用姿势(一):如何选择缓存的读写策略?
Cache Aside(旁路缓存)策略
在使用缓存时,可能会想到,先写DB,再更新缓存。
这个思路会造成缓存和数据库中的数据不一致。例如,多个线程并发更新数据时。
要想解决这个问题,可以采用Cache Aside策略(也叫旁路缓存策略)。这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略,
其中读策略的步骤是:
从缓存中读取数据;
如果缓存命中,则直接返回数据;
如果缓存不命中,则从数据库中查询数据;
查询到数据后,将数据写入到缓存中,并且返回给用户。
写策略的步骤是:
更新数据库中的记录;
删除缓存记录。
实际这个策略仍有缺陷,见下图
当请求A写缓存晚于请求B更新DB并删除缓存操作时会出现。不过这种问题出现的几率并不高,原因是缓存的写入通常远远快于数据库的写入。
Read/Write Through(读穿/写穿)策略
这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。
Write Through的策略是这样的:先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)”。
一般来说,我们可以选择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。
在Write Through策略中,我们一般选择“No-write allocate”方式,原因是无论采用哪种“Write Miss”方式,我们都需要同步将数据更新到数据库中,而“No-write allocate”方式相比“Write Allocate”还减少了一次缓存的写入,能够提升写入的性能。
Read Through策略就简单一些,它的步骤是这样的:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。
Read Through/Write Through策略的特点是由缓存节点而非用户来和数据库打交道,在我们开发过程中相比Cache Aside策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是Memcached还是Redis都不提供写入数据库,或者自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略,比如说在上一节中提到的本地缓存Guava Cache中的Loading Cache就有Read Through策略的影子。
总结
1.Cache Aside是我们在使用分布式缓存时最常用的策略,你可以在实际工作中直接拿来使用。
2.Read/Write Through和Write Back策略需要缓存组件的支持,所以比较适合你在实现本地缓存组件的时候使用;
3.Write Back策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。
15 | 缓存的使用姿势(三):缓存穿透了怎么办?
什么是缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况。
缓存穿透的解决方案
一般来说我们会有两种解决方案:回种空值以及使用布隆过滤器。
回种空值
从数据库中查询到空值或者发生异常时,我们可以向缓存中回种一个空值。但是因为空值并不是准确的业务数据,并且会占用缓存的空间,所以我们会给这个空值加一个比较短的过期时间,让空值在短时间之内能够快速过期淘汰。
回种空值虽然能够阻挡大量穿透的请求,但如果有大量获取不到信息的请求,缓存内就会有有大量的空值缓存,也就会浪费缓存的存储空间,如果缓存空间被占满了,还会剔除掉一些已经被缓存的用户信息反而会造成缓存命中率的下降。
如果需要大量的缓存节点来支持,那么就无法通过通过回种空值的方式来解决,这时你可以考虑使用布隆过滤器。
布隆过滤器
布隆过滤器来解决缓存穿透的问题,是在缓存前面的。
这种算法用来判断一个元素是否在一个集合中,由一个二进制数组和一个Hash算法组成。
把集合中的每一个值按照提供的Hash算法算出对应的Hash值,然后将Hash值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从0改成1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为1就认为这个元素在集合中,否则则认为不在集合中。
A、B、C等元素组成了一个集合,元素D计算出的Hash值所对应的的数组中值是1,所以可以认为D也在集合中。而F在数组中的值是0,所以F不在数组中。
查询时,先判断在布隆过滤器中是否存在,如果不在,直接返回空值;如果在,进一步查询缓存甚至数据库。
它主要有两个缺陷:
1.它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中;(哈希碰撞)。解决方案是:使用多个Hash算法为元素计算出多个Hash值,只有所有Hash值对应的数组中的值都为1时,才会认为这个元素在集合中。
2.不支持删除元素。
17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?
削去秒杀场景下的峰值写流量
在秒杀场景下短时间之内数据库的写流量会很高,将秒杀请求暂存在消息队列中,然后业务服务器会响应用户“秒杀结果正在计算中”,释放了系统资源之后再处理其它用户的请求。
这就是消息队列在秒杀系统中最主要的作用:削峰填谷。
通过异步处理简化秒杀请求中的业务流程
秒杀场景下整个的购买流程,发现这里面会有主要的业务逻辑,也会有次要的业务逻辑:比如说,主要的流程是生成订单、扣减库存;次要的流程可能是我们在下单购买成功之后会给用户发放优惠券,会增加用户的积分。
假如发放优惠券的耗时是50ms,增加用户积分的耗时也是50ms,那么如果我们将发放优惠券、增加积分的操作放在另外一个队列处理机中执行,那么整个流程就缩短了100ms。
解耦实现秒杀系统模块之间松耦合
如果数据团队对你说,在秒杀活动之后想要统计活动的数据,借此来分析活动商品的受欢迎程度、购买者人群的特点以及用户对于秒杀互动的满意程度等等指标。而我们需要将大量的数据发送给数据团队,那么要怎么做呢?
一个思路是:使用HTTP或者RPC的方式来同步地调用,也就是数据团队这边提供一个接口,我们实时将秒杀的数据推送给它,但是这样调用会有两个问题:
整体系统的耦合性比较强,当数据团队的接口发生故障时,会影响到秒杀系统的可用性。
当数据系统需要新的字段,就要变更接口的参数,那么秒杀系统也要随着一起变更。
秒杀系统产生一条购买数据后,我们可以先把全部数据发送给消息队列,然后数据团队再订阅这个消息队列的话题,这样它们就可以接收到数据,然后再做过滤和处理了。
秒杀系统在这样解耦合之后,数据系统的故障就不会影响到秒杀系统了,同时当数据系统需要新的字段时,只需要解析消息队列中的消息,拿到需要的数据就好了。
18 | 消息投递:如何保证消息仅仅被消费一次?
19 | 消息队列:如何降低消息队列系统中消息的延迟?
减少消息延迟的正确姿势
在消费端和消息队列两个层面来完成。
在消费端的目标是提升消费者的消息处理能力,你能做的是:
优化消费代码提升性能;
增加消费者的数量(这个方式比较简单)。
不过第二种方式会受限于消息队列的实现。如果消息队列使用的是Kafka就无法通过增加消费者数量的方式来提升消息处理能力。
因为在Kafka中,一个Topic(话题)可以配置多个Partition(分区),数据会被平均或者按照生产者指定的方式写入到多个分区中,那么在消费的时候,Kafka约定一个分区只能被一个消费者消费,为什么要这么设计呢?在我看来,如果有多个consumer(消费者)可以消费一个分区的数据,那么在操作这个消费进度的时候就需要加锁,可能会对性能有一定的影响。
所以说,话题的分区数量决定了消费的并行度,增加多余的消费者也是没有用处的,你可以通过增加分区来提高消费者的处理能力。
Kafka一个topic下可以有一个或多个partition。而消费一个partition是以消费组为单位的,一个消费组中如果有多个实例,只能有一个实例能消费该partition。但是一个消费实例却可以同时消费多个partition。
如果是不同消费组的两个实例,则可以对同一个partition进行消费,且他们之间互不影响。
虽然不能增加consumer,但你可以在一个consumer中提升处理消息的并行度,所以可以考虑使用多线程的方式来增加处理能力:你可以预先创建一个或者多个线程池,在接收到消息之后把消息丢到线程池中来异步地处理,这样,原本串行的消费消息的流程就变成了并行的消费,可以提高消息消费的吞吐量。
20 | 面试现场第二期:当问到项目经历时,面试官究竟想要了解什么?
可以介绍一下你参与的这个项目吗?
你在这个项目中的主要职责是什么?
你提到的这个项目中,XX模块QPS很高,使用了缓存,缓存命中率是多少?
你在这个项目中,遇到过哪些问题?又是怎么排查的呢?
那你有没有遇到过一些性能相关的问题?你又是怎么调优的?
分页避免GC
深分页慢查优化
batchInsert
三大要点:
针对复杂的需求设计了哪些方案,方案中的技术难点是什么,你是如何解决的?
在项目中遇到过哪些诡异的问题,排查问题的思路是怎样的?
在项目运维过程中出现过哪些性能问题,你是怎么样优化的?
在介绍项目时,要突出两点。比如QPS很高是多少多少,数据量很大是什么级别,使用布隆过滤器解决缓存穿透问题等。
如果项目没那么高的流量,可以结合高并发知识聊聊可做的改造和优化。
如何通过压测评估系统承载能力以便制定扩容计划。
面试要扬长避短,将面试官引到自己擅长的领域。
23 | RPC框架:10万QPS下如何实现毫秒级的服务调用?
RPC步骤:
在一次RPC调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流;
然后客户端将二进制流通过网络发送给服务端;
服务端接收到二进制流之后将它反序列化,得到需要调用的类名、方法名、参数名和参数值,再通过动态代理的方式调用对应的方法得到返回值;
服务端将返回值序列化,再通过网络发送给客户端;
客户端对结果反序列化之后,就可以得到调用的结果了。
如果要提升RPC框架的性能,需要从网络传输和序列化两方面来优化。
如何提升网络传输性能
一般单次I/O请求会分为两个阶段,每个阶段对于I/O的处理方式是不同的。
首先是等待资源的阶段,比如等待网络传输数据,在这个过程中我们对I/O会有两种处理方式:
阻塞。指的是在数据不可用时I/O请求一直阻塞,直到数据返回;
非阻塞。指的是数据不可用时I/O请求立即返回,直到被通知资源可用为止。
然后是使用资源的阶段,比如将从网络接收的数据拷贝到程序的缓冲区,在这个阶段也有两种方式:
同步处理。指的是I/O请求在读取或者写入数据时会阻塞,直到读取或者写入数据完成;
异步处理。指的是I/O请求在读取或者写入数据时立即返回,当操作系统处理完成I/O请求并且将数据拷贝到用户提供的缓冲区后,再通知应用I/O请求执行完成。
选择高性能的I/O模型,这里我推荐使用同步多路I/O复用模型;
选择合适的序列化方式
通常所说的序列化是将传输对象转换成二进制串的过程,而反序列化则是相反的动作,是将二进制串转换成对象的过程。
对性能要求不高可以选择JSON,否则可以从Thrift和Protobuf中选择其一。
32 | 压力测试:怎样设计全链路压力测试平台?
1.首先,做压力测试时,最好使用线上的数据和线上的环境。因为,你无法确定自己搭建的测试环境与正式环境的差异,是否会影响到压力测试的结果。
2.其次,压力测试时不能使用模拟的请求而是要使用线上的流量。你可以通过拷贝流量的方式,把线上流量拷贝一份到压力测试环境。因为模拟流量的访问模型和线上流量相差很大,会对压力测试的结果产生比较大的影响。
比如,你在获取商品信息的时候,线上的流量会获取不同商品的数据,这些商品的数据有的命中了缓存,有的没有命中缓存。如果使用同一个商品ID来做压力测试,那么只有第一次请求没有命中缓存,而在请求之后会将数据库中的数据回种到缓存,后续的请求就一定会命中缓存了,这种压力测试的数据就不具备参考性了。
3.不要从一台服务器发起流量,这样很容易达到这台服务器性能瓶颈,从而导致压力测试的QPS上不去,最终影响压力测试的结果。
34 | 降级熔断:如何屏蔽非核心系统故障的影响?
熔断机制指的是在发起服务调用的时候,如果返回错误或者超时的次数超过一定阈值,则后续的请求不再发向远程服务而是暂时返回错误。
熔断机制是如何做的
服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中会有三种状态:关闭(调用远程服务)、半打开(尝试调用远程服务)和打开(返回错误)。这三种状态之间切换的过程是下面这个样子。
降级机制要如何做
相比熔断来说,降级是一个更大的概念。熔断也是降级的一种,除此之外还有限流降级、开关降级等等。
可以抛异常、返回固定值。
35 | 流量控制:高并发系统中我们如何操纵流量?
限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。限流策略一般部署在服务的入口层,比如买菜是交易限流。
在TCP协议中有一个滑动窗口的概念,可以实现对网络传输流量的控制。
在接收方回复发送方的ACK消息中,会带上这个窗口的大小。这样,发送方就可以通过这个滑动窗口的大小决定发送数据的速率了。如果接收方处理了一些缓冲区的数据,那么这个滑动窗口就会变大,发送方发送数据的速率就会提升;反之,如果接收方接收了一些数据还没有来得及处理,那么这个滑动窗口就会减小,发送方发送数据的速率就会减慢。
你应该知道的限流算法
限流的目的是限制一段时间内发向系统的总体请求量,比如,限制一分钟之内系统只能承接1万次请求,那么最暴力的一种方式就是记录这一分钟之内访问系统的请求量有多少,如果超过了1万次的限制,那么就触发限流的策略返回请求失败的错误。如果这一分钟的请求量没有达到限制,那么在下一分钟到来的时候先重置请求量的计数,再统计这一分钟的请求量是否超过限制,这种算法叫做固定窗口算法。
这种算法虽然实现非常简单,但是却有一个很大的缺陷 :无法限制短时间之内的集中流量。
假如我们需要限制每秒钟只能处理10次请求,如果前一秒钟产生了10次请求,这10次请求全部集中在最后的10毫秒中,而下一秒钟的前10毫秒也产生了10次请求,那么在这20毫秒中就产生了20次请求,超过了限流的阈值。但是因为这20次请求分布在两个时间窗口内,所以没有触发限流,这就造成了限流的策略并没有生效。
为了解决这个缺陷,就有了基于滑动窗口的算法。 这个算法的原理是将时间的窗口划分为多个小窗口,每个小窗口中都有单独的请求计数。比如下面这张图,我们将1s的时间窗口划分为5份,每一份就是200ms;那么当在1s和1.2s之间来了一次新的请求时,我们就需要统计之前的一秒钟内的请求量,也就是0.2s~1.2s这个区间的总请求量,如果请求量超过了限流阈值那么就执行限流策略。
还是无法限制短时间之内的集中流量,也就是说无法控制流量让它们更加平滑。
令牌筒算法
如果我们需要在一秒内限制访问次数为N次,那么就每隔1/N的时间,往桶内放入一个令牌;
在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;
桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题。
令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量,所以一般我会使用令牌桶算法来实现限流方案,而Guava中的限流方案就是使用令牌桶算法来实现的。
使用令牌桶算法就需要存储令牌的数量,如果是单机上实现限流的话,可以在进程中使用一个变量来存储;但是如果在分布式环境下,不同的机器之间无法共享进程中的变量,我们就一般会使用Redis来存储这个令牌的数量。
基础知识、结合项目考察设计能力、排查问题能力、系统优化能力等。