Table of Contents:

01 | 高并发系统:它的通用设计方法是什么?

四种方法:
* Scale-up(纵向扩展):升配置,当系统并发超过了单机的极限时,我们就要使用 Scale-out 的方式。
* Scale-out(横向扩展):分而治之是一种常见的高并发系统设计方法,采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量。
* 缓存:使用缓存来提高系统的性能,就好比用“拓宽河道”的方式抵抗高并发大流量的冲击。
* 异步:在某些场景下,未处理完成之前,我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。

02 | 架构分层:我们为什么一定要这么做?

分层有什么好处
* 分层的设计可以简化系统设计,让不同的人专注做某一层次的事情。
* 再有,分层之后可以做到很高的复用。比如,我们在设计系统 A 的时候,发现某一层具有一定的通用性,那么我们可以把它抽取独立出来,在设计系统 B 的时候使用起来,这样可以减少研发周期,提升研发的效率。
* 最后一点,分层架构可以让我们更容易做横向扩展。如果系统没有分层,当流量增加时我们需要针对整体系统来做扩展。但是,如果我们按照上面提到的三层架构将系统分层后,那么我们就可以针对具体的问题来做细致的扩展。

横向扩展是高并发系统设计的常用方法之一,既然分层的架构可以为横向扩展提供便捷, 那么支撑高并发的系统一定是分层的系统。

03 | 系统设计目标(一):如何提升系统性能?

高并发系统设计的三大目标:
* 高性能:相应快,服务器资源消耗少
* 高可用:指的是系统具备较高的无故障运行的能力。指标:停机次数、故障率、故障出现后恢复时间
* 可扩展:1.流量分为平时流量和峰值流量,如何在峰值流量时应对,这属于应急扩容。2.如何在未来业务量大时对服务进行扩展

如何提升系统的性能?
性能的度量指标
度量性能的指标是系统接口的响应时间,但是单次的响应时间是没有意义的,你需要知道一段时间的性能情况是什么样的
* 平均值响应时间:
平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数
* 最大值响应时间:
就是这段时间内所有请求响应时间最长的值
* 分位值响应时间:
分位值是最适合作为时间段内,响应时间统计值来使用的,在实际工作中也应用最多。比如在每秒 1 万次的请求量下,响应时间 99 分位值在 100ms 以下。

从用户使用体验的角度来看,200ms 是第一个分界点:接口的响应时间在 200ms 之内,用户是感觉不到延迟的,就像是瞬时发生的一样。而 1s 是另外一个分界点:接口的响应时间在 1s 之内时,虽然用户可以感受到一些延迟,但却是可以接受的,超过 1s 之后用户就会有明显等待的感觉,等待时间越长,用户的使用体验就越差。所以,健康系统的 99 分位值的响应时间通常需要控制在 200ms 之内,而不超过 1s 的请求占比要在 99.99% 以上。

04 | 系统设计目标(二):系统怎样做到高可用?

可用性的度量
* MTBF(Mean Time Between Failure)是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
* MTTR(Mean Time To Repair)表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。

一个成熟系统的可用性需要从系统设计系统运维两方面来做保障
* 系统设计注重的是如何处理故障,关键词是冗余和取舍。冗余指的是有备用节点,集群来顶替出故障的服务,比如文中提到的故障转移,还有多活架构等等;取舍指的是丢卒保车,保障主体服务的安全。
* 从运维角度来看则更偏保守,注重的是如何避免故障的发生,比如更关注变更管理以及如何做故障演练

1.系统设计

failover(故障转移)

一般来说,发生 failover 的节点可能有两种情况:
1. 是在完全对等的节点之间做 failover。一般就是不在对fail节点访问即可。
2. 是在不对等的节点之间,即系统中存在主节点也存在备节点。
面临问题:1.如何检测节点失效。使用最广泛的故障检测机制是“心跳”。 2.确认主节点已经发生故障,触发选主的操作,如何确定备选节点中哪个作为主节点。所以会使用某一种分布式一致性算法,比方说 Paxos,Raft。

系统间调用超时

超时引发问题的例子:
模块之间通过 RPC 框架来调用,超时时间是默认的 30 秒。平时系统运行得非常稳定,可是一旦遇到比较大的流量,RPC 服务端出现一定数量慢请求的时候,RPC 客户端线程就会大量阻塞在这些慢请求上长达 30 秒,造成 RPC 客户端用尽调用线程而挂掉。
解决:
控制超时时间。超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作用。故可以通过收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间。

降级

降级是为了保证核心服务的稳定而牺牲非核心服务的做法。比方说我们发一条微博会先经过反垃圾服务检测,检测内容是否是广告,通过后才会完成诸如写数据库等逻辑。而反垃圾的检测是一个相对比较重的操作,在高流量的特殊情况下可以关掉。

限流

限流完全是另外一种思路,它通过对并发的请求进行限速来保护系统。
比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

2. 系统运维

我们可以从灰度发布故障演练两个方面来考虑如何提升系统的可用性。

在业务平稳运行过程中,系统是很少发生故障的,90% 的故障是发生在上线变更阶段的。比方说,你上了一个新的功能,由于设计方案的问题,数据库的慢请求数翻了一倍,导致系统请求被拖慢而产生故障。

灰度发布

灰度发布指的是系统的变更不是一次性地推到线上的,而是按照一定比例逐步推进的。一般情况下,灰度发布是以机器维度进行的。比方说,我们先在 10% 的机器上进行变更,同时观察 Dashboard 上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志,那么再推动全量变更。
灰度发布给了开发和运维同学绝佳的机会,让他们能在线上流量上观察变更带来的影响,是保证系统高可用的重要关卡。

故障演练

故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是怎样的,从而发现系统中存在的,潜在的可用性问题。

故障演练和时下比较流行的“混沌工程”的思路如出一辙,作为混沌工程的鼻祖,Netfix 在 2010 年推出的“Chaos Monkey”工具就是故障演练绝佳的工具。它通过在线上系统上随机地关闭线上节点来模拟故障,让工程师可以了解,在出现此类故障时会有什么样的影响。

05 | 系统设计目标(三):如何让系统易于扩展?

集群系统中,不同的系统分层上可能存在一些“瓶颈点”,这些瓶颈点制约着系统的横线扩展能力。

无状态的服务和组件更易于扩展,而像 MySQL 这种存储服务是有状态的,就比较难以扩展。因为向存储集群中增加或者减少机器时,会涉及大量数据的迁移,而一般传统的关系型数据库都不支持。

高可扩展性的设计思路
拆分是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。将复杂的问题简单化,这就是我们的思路。

  1. 存储层的扩展性
    无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟社区中,关系的数据量是远远大于用户数据量的。
    所以存储拆分首先考虑的维度是业务维度。根据业务的不同把数据库拆分成多个。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。
    按照业务拆分,在一定程度上提升了系统的扩展性,但系统运行时间长了之后,单一的业务数据库在容量和并发请求量上仍然会超过单机的限制。这时,我们就需要针对数据库做第二次拆分。这次拆分是按照数据特征做水平的拆分,比如说我们可以给用户库增加两个节点,然后按照某些算法将用户的数据拆分到这三个库里面。但这里要注意,我们不能随意地增加节点,因为一旦增加节点就需要手动地迁移数据,成本还是很高的。所以基于长远的考虑,我们最好一次性增加足够的节点以避免频繁地扩容。
    注意:当数据库按照业务和数据维度拆分之后,我们尽量不要使用事务。

  2. 业务层的扩展性
    我们一般会从三个维度考虑业务层的拆分方案,它们分别是:业务纬度,重要性纬度和请求来源纬度。

06 | 面试现场第一期:当问到组件实现原理时,面试官是在刁难你吗?

07 | 池化技术:如何减少频繁创建数据库连接的性能损耗?

那么为什么频繁创建连接会造成响应时间慢呢?
用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上 MySQL 建立连接的网络包来做分析,从抓包结果来看,整个 MySQL 的连接过程可以分为两部分:

第一部分是前三个数据包。
第一个数据包是客户端向服务端发送的一个“SYN”包,第二个包是服务端回给客户端的“ACK”包以及一个“SYN”包,第三个包是客户端回给服务端的“ACK”包,熟悉 TCP 协议的同学可以看出这是一个 TCP 的三次握手过程。
第二部分是 MySQL 服务端校验客户端密码的过程。
其中第一个包是服务端发给客户端要求认证的报文,第二和第三个包是客户端将加密后的密码发送给服务端的包,最后两个包是服务端回给客户端认证 OK 的报文。从图中,你可以看到整个连接过程大概消耗了 4ms。而SQL 的平均执行时间大概是 也就是1ms,建立连接就用了4ms。

用连接池预先建立数据库连接
* 数据库连接池有两个最重要的配置:最小连接数最大连接数,它们控制着从连接池中获取连接的流程:
* 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
* 如果连接池中有空闲连接则复用空闲连接;
* 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
* 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
* 如果等待超过了这个设定时间则向用户抛出错误。

这就是一种常见的软件设计思想,叫做池化技术。

需考虑的问题:
1. 数据库的域名对应的 IP 发生了变更,池子的连接还是使用旧的 IP,当旧的 IP 下的数据库服务关闭后,再使用这个连接查询就会发生错误;
2. MySQL 有个参数是“wait_timeout”,控制着当数据库连接闲置多长时间后,数据库会主动的关闭这条连接。这个机制对于数据库使用方是无感知的,所以当我们使用这个被关闭的连接时就会发生错误。

解决:
1. 启动一个线程来定期检测连接池中的连接是否可用,比如使用连接发送“select 1”的命令给数据库看是否会抛出异常,如果抛出异常则将这个连接从连接池中移除,并且尝试关闭。目前 C3P0 连接池可以采用这种方式来检测连接是否可用,也是我比较推荐的方式。

  1. 在获取到连接之后,先校验连接是否可用,如果可用才会执行 SQL 语句。比如 DBCP 连接池的 testOnBorrow 配置项,就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销,在线上系统中还是尽量不要开启,在测试服务上可以使用。

08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?

主从复制的过程
MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。
主从复制的过程是这样的:首先从库在连接到主节点时会创建一个 IO 线程,用以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中,而主库也会创建一个 log dump 线程来发送 binlog 给从库;同时,从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。

  1. 主从读写分离以及部署一主多从可以解决突发的数据库读流量,是一种数据库横向扩展的方法;
  2. 读写分离后,主从的延迟是一个关键的监控指标,可能会造成写入数据之后立刻读的时候读取不到的情况;
  3. 业界有很多的方案可以屏蔽主从分离之后数据库访问的细节,让开发人员像是访问单一数据库一样,包括有像 TDDL、Sharding-JDBC 这样的嵌入应用内部的方案,也有像 Mycat 这样的独立部署的代理方案。

其实,我们可以把主从复制引申为存储节点之间互相复制存储数据的技术,它可以实现数据的冗余,以达到备份提升横向扩展能力的作用。在使用主从复制这个技术点时,你一般会考虑两个问题:
1. 主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。
2. 主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。

我们采用的很多组件都会使用到这个技术,比如,Redis 也是通过主从复制实现读写分离;Elasticsearch 中存储的索引分片也可以被复制到多个节点中;写入到 HDFS 中文件也会被复制到多个 DataNode 中。只是不同的组件对于复制的一致性、延迟要求不同,采用的方案也不同。但是这种设计的思想是通用的。

问题:主从延迟导致从从库中获取数据没获取到
比如:在发微博的过程中会有些同步的操作,像是更新数据库的操作,也有一些异步的操作,比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队列,再由队列处理机依据 ID 在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。

解决方案:
1. 数据的冗余。你可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。
2. 使用缓存。我可以在同步写数据库的同时,也把微博的数据写入到 Memcached 缓存里面,这样队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。
3. 查询主库。我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。

09 | 数据库优化方案(二):写入数据量增加时,如何实现分库分表?

mysql中当单表超过亿量级后的问题:
1. 单个表的数据量超过了千万甚至到了亿级别。这时即使你使用了索引,索引占用的空间也随着数据量的增长而增大,数据库就无法缓存全量的索引信息,那么就需要从磁盘上读取索引数据,就会影响到查询的性能了。那么这时你要如何提升查询性能呢?
2. 数据量的增加也占据了磁盘的空间,数据库在备份和恢复的时间变长,你如何让数据库系统支持如此大的数据量呢?
3. 不同模块的数据,比如用户数据和用户关系数据,全都存储在一个主库中,一旦主库发生故障,所有的模块儿都会受到影响,那么如何做到不同模块的故障隔离呢?

分库分表实际上是分布式存储中一种数据分片的解决方案
数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。
* 垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的不同的表拆分到多个不同的数据库中。
* 水平拆分,和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。

水平拆分的规则有下面这两种:
1. 按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表
2. 另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。

从分库分表规则中你可以看到,无论是哈希拆分还是区间段的拆分,我们首先都需要选取一个数据库字段,这带来一个问题是:我们之后所有的查询都需要带上这个字段,才能找到数据所在的库和表,否则就只能向所有的数据库和数据表发送查询命令。
解决:
建立一个查询字段和拆分字段的映射表,专门用来作为查询用。即相当于根据这个表先查询到id,然后再根据id查询到相应库和相应表中的数据。
分库分表引入的另外一个问题是一些数据库的特性在实现时可能变得很困难。比如sql中的join,count等,这个可能就需要在程序里面实现了。

对于分库分表的原则主要有以下几点:
1. 如果在性能上没有瓶颈点那么就尽量不做分库分表;
2. 如果要做,就尽量一次到位,比如说 16 库 64 表就基本能够满足为了几年内你的业务的需求。
3. 很多的 NoSQL 数据库,例如 Hbase,MongoDB 都提供 auto sharding 的特性,如果你的团队内部对于这些组件比较熟悉,有较强的运维能力,那么也可以考虑使用这些 NoSQL 数据库替代传统的关系型数据库。

10 | 发号器:如何保证分库分表后ID的全局唯一性?

用户id如何生成
* 随机生成+普通查重模式
* 经典表ID自增
* 号池模式,生成一批UID存放到号池内, 注册一个取走一个。关键点在于号池的维护。
* 类Snowflake模式,缺点是位数太长
* UUID

如何生成数据库表中的唯一id?
UUID(Universally Unique Identifier,通用唯一标识码)不依赖于任何第三方系统,所以在性能和可用性上都比较好,我一般会使用它生成 Request ID 来标记单次请求,但是如果用它来作为数据库主键,它会存在以下几点问题。
1. 生成的 ID 做好具有单调递增性,也就是有序的,而 UUID 不具备这个特点
2. ID 有序也会提升数据的写入性能,而且UUID比较长,占的空间也多。
3. UUID 不能作为 ID 的另一个原因是它不具备业务含义

故可以使用Snowflake算法,Snowflake 的核心思想是将 64bit 的二进制数字分成若干部分,每一部分都存储有特定含义的数据,比如说时间戳、机器 ID、序列号等等,最终生成全局唯一的有序 ID。它的标准算法是这样的:

了解了 Snowflake 算法的原理之后,我们如何把它工程化,来为业务生成全局唯一的 ID 呢?一般来说我们会有两种算法的实现方式:
1. 一种是嵌入到业务代码里,也就是分布在业务服务器中。这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些
2. 另外一个部署方式是作为独立的服务部署,这也就是我们常说的发号器服务

11 | NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?

NoSQL 数据库发展到现在,十几年间,出现了多种类型,我来给你举几个例子:
* Redis、LevelDB 这样的 KV 存储。这类存储相比于传统的数据库的优势是极高的读写性能,一般对性能有比较高的要求的场景会使用。
* Hbase、Cassandra 这样的列式存储数据库。这种数据库的特点是数据不像传统数据库以行为单位来存储,而是以列来存储,适用于一些离线数据统计的场景。
* MongoDB、CouchDB 这样的文档型数据库。这种数据库的特点是 Schema Free(模式自由),数据表中的字段可以任意扩展,比如说电商系统中的商品有非常多的字段,并且不同品类的商品的字段也都不尽相同,使用关系型数据库就需要不断增加字段支持,而用文档型数据库就简单很多了。

在 NoSQL 数据库刚刚被应用时,它被认为是可以替代关系型数据库的银弹,在我看来,也许因为以下几个方面的原因:
* 弥补了传统数据库在性能方面的不足;
* 数据库变更方便,不需要更改原先的数据结构;
* 适合互联网项目常见的大数据量的场景;

不过,这种看法是个误区,因为慢慢地我们发现在业务开发的场景下还是需要利用 SQL 语句的强大的查询功能以及传统数据库事务和灵活的索引等功能,NoSQL 只能作为一些场景的补充。

使用 NoSQL 提升写入性能
数据库系统大多使用的是传统的机械磁盘,对于机械磁盘的访问方式有两种:一种是随机 IO;另一种是顺序 IO。随机 IO 就需要花费时间做昂贵的磁盘寻道,一般来说,它的读写效率要比顺序 IO 小两到三个数量级,所以我们想要提升写入的性能就要尽量减少随机 IO。NoSQL 数据库通过使用不同的算法来避免随机的写入磁盘。

场景补充
使用Elasticsearch解决模糊查询的问题
sql只有后模糊匹配的语句才能使用索引。比如语句“select * from product where name like ‘% 电冰箱’” 就没有使用到字段“name”上的索引,而“select * from product where name like ‘索尼 %’”可以使用到索引。
而 Elasticsearch 作为一种常见的 NoSQL 数据库,就以倒排索引作为核心技术原理,为你提供了分布式的全文搜索服务,这在传统的关系型数据库中使用 SQL 语句是很难实现的。

倒排索引是指将记录中的某些列做分词,然后形成的分词与记录 ID 之间的映射关系。

本节课我带你了解了 NoSQL 数据库在性能、扩展性上的优势,以及它的一些特殊功能特性,主要有以下几点:
1. 在性能方面,NoSQL 数据库使用一些算法将对磁盘的随机写转换成顺序写,提升了写的性能;
2. 在某些场景下,比如全文搜索功能,关系型数据库并不能高效地支持,需要 NoSQL 数据库的支持;
3. 在扩展性方面,NoSQL 数据库天生支持分布式,支持数据冗余和数据分片的特性。

NoSQL 可供选型的种类很多,每一个组件都有各自的特点。你在做选型的时候需要对它的实现原理有比较深入的了解,最好在运维方面对它有一定的熟悉,这样在出现问题时才能及时找到解决方案。否则,盲目跟从地上了一个新的 NoSQL 数据库,最终可能导致会出了故障无法解决,反而成为整体系统的拖累。
对于开源组件的使用,不能只停留在只会“hello world”的阶段,而应该对它有足够的运维上的把控能力。

12 | 缓存:数据库成为瓶颈后,动态数据的查询要如何加速?

什么是缓存

我们经常会把缓存放在内存中来存储, 所以有人就把内存和缓存画上了等号,这完全是外行人的见解。作为业内人士,你要知道在某些场景下我们可能还会使用 SSD 作为冷数据的缓存。比如说 360 开源的 Pika 就是使用 SSD 存储数据解决 Redis 的容量瓶颈的。

实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。

缓存作为一种常见的空间换时间的性能优化手段,在很多地方都有应用

缓存案例
1. Linux 内存管理是通过一个叫做 MMU(Memory Management Unit)的硬件,来实现从虚拟地址到物理地址的转换的,但是如果每次转换都要做这么复杂计算的话,无疑会造成性能的损耗,所以我们会借助一个叫做 TLB(Translation Lookaside Buffer)的组件来缓存最近转换过的虚拟地址,和物理地址的映射。

再想一下你平时经常刷的抖音。平台上的短视频实际上是使用内置的网络播放器来完成的。网络播放器接收的是数据流,将数据下载下来之后经过分离音视频流,解码等流程后输出到外设设备上播放。
如果我们在打开一个视频的时候才开始下载数据的话,无疑会增加视频的打开速度(我们叫首播时间),并且播放过程中会有卡顿。所以我们的播放器中通常会设计一些缓存的组件,在未打开视频时缓存一部分视频数据,比如我们打开抖音,服务端可能一次会返回三个视频信息,我们在播放第一个视频的时候,播放器已经帮我们缓存了第二、三个视频的部分数据,这样在看第二个视频的时候就可以给用户“秒开”的感觉。

  1. 缓存与缓冲区
    缓冲区则是一块临时存储数据的区域,这些数据后面会被传输到其他设备上。缓冲区更像“消息队列篇”中即将提到的消息队列,用以弥补高速设备和低速设备通信时的速度差。比如,我们将数据写入磁盘时并不是直接刷盘,而是写到一块缓冲区里面,内核会标识这个缓冲区为脏。当经过一定时间或者脏缓冲区比例到达一定阈值时,由单独的线程把脏块刷新到硬盘上。这样避免了每次写数据都要刷盘带来的性能问题。

缓存分类

在我们日常开发中,常见的缓存主要就是静态缓存、分布式缓存和热点本地缓存这三种。
1. 静态缓存
用模板生成静态页面
2. 分布式缓存
Memcached、Redis,通过一些分布式的方案组成集群可以突破单机的限制
3.热点本地缓存
当我们遇到极端的热点数据查询的时候。热点本地缓存主要部署在应用服务器的代码中,用于阻挡热点查询对于分布式缓存节点或者数据库的压力
如 HashMap,Guava Cache 或者是 Ehcache 等,它们和应用程序部署在同一个进程中,优势是不需要跨网络调度,速度极快,所以可以来阻挡短时间内的热点查询。
例如 Guava 的 Loading Cache:

CacheBuilder<String, List<Product>> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize).recordStats(); // 设置缓存最大值
cacheBuilder = cacheBuilder.refreshAfterWrite(30, TimeUnit.Seconds); // 设置刷新间隔
LoadingCache<String, List<Product>> cache = cacheBuilder.build(new CacheLoader<String, List<Product>>() {
    @Override
    public List<Product> load(String k) throws Exception {
        return productService.loadAll(); // 获取所有商品
    }
});

缓存的不足

  1. 缓存比较适合于读多写少的业务场景,并且数据最好带有一定的热点属性。
  2. 缓存会给整体系统带来复杂度,并且会有数据不一致的风险。当更新数据库成功,更新缓存失败的场景下,缓存中就会存在脏数据。对于这种场景,我们可以考虑使用较短的过期时间或者手动清理的方式来解决。
  3. 之前提到缓存通常使用内存作为存储介质,但是内存并不是无限的
  4. 缓存会给运维也带来一定的成本,运维需要对缓存组件有一定的了解,在排查问题的时候也多了一个组件需要考虑在内。

缓存可以有多层,比如上面提到的静态缓存处在负载均衡层,分布式缓存处在应用层和数据库层之间,本地缓存处在应用层。我们需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差;

缓存命中率是我们对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高。

缓存不仅仅是一种组件的名字,更是一种设计思想,你可以认为任何能够加速读请求的组件和设计方案都是缓存思想的体现。而这种加速通常是通过两种方式来实现:
* 使用更快的介质,比方说课程中提到的内存;
* 缓存复杂运算的结果,比方说前面 TLB 的例子就是缓存地址转换的结果。

13 | 缓存的使用姿势(一):如何选择缓存的读写策略?

Cache Aside(旁路缓存)策略

这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策略,其中读策略的步骤是:
* 从缓存中读取数据;
* 如果缓存命中,则直接返回数据;
* 如果缓存不命中,则从数据库中查询数据;
* 查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略的步骤是:
* 更新数据库中的记录;
* 删除缓存记录,注意缓存要在更新后删除,不然会存在数据不一致的情况

Cache Aside 策略是我们日常开发中最经常使用的缓存策略,不过我们在使用时也要学会依情况而变。比如说当新注册一个用户,按照这个更新策略,你要写数据库,然后清理缓存(当然缓存中没有数据给你清理)。可当我注册用户后立即读取用户信息,并且数据库主从分离时,会出现因为主从延迟所以读不到用户信息的情况。

而解决这个问题的办法恰恰是在插入新数据到数据库之后写入缓存,这样后续的读请求就会从缓存中读到数据了。并且因为是新注册的用户,所以不会出现并发更新用户信息的情况。
Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
1. 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
2. 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受。

Read/Write Through(读穿 / 写穿)策略

这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。这就好比你在汇报工作的时候只对你的直接上级汇报,再由你的直接上级汇报给他的上级,你是不能越级汇报的。

Write Back(写回)策略

这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中。

课程小结
1.Cache Aside 是我们在使用分布式缓存时最常用的策略,你可以在实际工作中直接拿来使用。
2.Read/Write Through 和 Write Back 策略需要缓存组件的支持,所以比较适合你在实现本地缓存组件的时候使用;
3.Write Back 策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。

14 | 缓存的使用姿势(二):缓存如何做到高可用?

分布式缓存的高可用方案主要有三种,
1.首先是客户端方案,一般也称为 Smart Client。我们通过制定一些数据分片和数据读写的策略,可以实现缓存高可用。这种方案的好处是性能没有损耗,缺点是客户端逻辑复杂且在多语言环境下不能复用。
2.其次,中间代理方案在客户端和缓存节点之间增加了中间层,在性能上会有一些损耗,在代理层会有一些内置的高可用方案,比如 Codis 会使用 Codis Ha 或者 Sentinel。
3.最后,服务端方案依赖于组件的实现,Memcached 就只支持单机版没有分布式和 HA 的方案,而 Redis 在 2.4 版本提供了 Sentinel 方案可以自动进行主从切换。服务端方案会在运维上增加一些复杂度。

15 | 缓存的使用姿势(三):缓存穿透了怎么办?

对于缓存来说,命中率是它的生命线
我们的核心缓存的命中率要保持在 99% 以上,非核心缓存的命中率也要尽量保证在 90%,如果低于这个标准,那么你可能就需要优化缓存的使用方式了

缓存穿透
缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况。
那么什么样的缓存穿透对系统有害呢?答案是大量的穿透请求超过了后端系统的承受范围,造成了后端系统的崩溃。

解决缓存穿透的方案
1. 回种空值是一种最常见的解决思路,实现起来也最简单,如果评估空值缓存占据的缓存空间可以接受,那么可以优先使用这种方案;
2. 布隆过滤器会引入一个新的组件,也会引入一些开发上的复杂度和运维上的成本。所以只有在存在海量查询数据库中,不存在数据的请求时才会使用,在使用时也要关注布隆过滤器对内存空间的消耗;
3. 对于极热点缓存数据穿透造成的“狗桩效应”,可以通过设置分布式锁或者后台线程定时加载的方式来解决。

除此之外,你还需要了解的是,数据库是一个脆弱的资源,它无论是在扩展性、性能还是承担并发的能力上,相比缓存都处于绝对的劣势,所以我们解决缓存穿透问题的核心目标在于减少对于数据库的并发请求。了解了这个核心的思想,也许你还会在日常工作中找到其他更好的解决缓存穿透问题的方案。

16 | CDN:静态资源如何加速?


1.DNS 技术是 CDN 实现中使用的核心技术,可以将用户的请求映射到 CDN 节点上;
2.DNS 解析结果需要做本地缓存,降低 DNS 解析过程的响应时间;
3.GSLB 可以给用户返回一个离着他更近的节点,加快静态资源的访问速度。

加餐 | 数据的迁移应该如何做?

两种方案可以做数据库的迁移。

“双写”方案

  1. 将新的库配置为源库的从库,用来同步数据;如果需要将数据同步到多库多表,那么可以使用一些第三方工具获取 Binlog 的增量日志(比如开源工具 Canal),在获取增量日志之后就可以按照分库分表的逻辑写入到新的库表中了。
  2. 同时,我们需要改造业务代码,在数据写入的时候,不仅要写入旧库,也要写入新库。当然,基于性能的考虑,我们可以异步地写入新库,只要保证旧库写入成功即可。但是,我们需要注意的是,需要将写入新库失败的数据记录在单独的日志中,这样方便后续对这些数据补写,保证新库和旧库的数据一致性。
  3. 然后,我们就可以开始校验数据了。由于数据库中数据量很大,做全量的数据校验不太现实。你可以抽取部分数据,具体数据量依据总体数据量而定,只要保证这些数据是一致的就可以。
  4. 如果一切顺利,我们就可以将读流量切换到新库了。由于担心一次切换全量读流量可能会对系统产生未知的影响,所以这里最好采用灰度的方式来切换,比如开始切换 10% 的流量,如果没有问题再切换到 50% 的流量,最后再切换到 100%。
  5. 由于有双写的存在,所以在切换的过程中出现任何的问题,都可以将读写流量随时切换到旧库去,保障系统的性能。
  6. 在观察了几天发现数据的迁移没有问题之后,就可以将数据库的双写改造成只写新库,数据的迁移也就完成了。

这种方式的好处是:迁移的过程可以随时回滚,将迁移的风险降到了最低。劣势是:时间周期比较长,应用有改造的成本。

级联同步方案

这种方案优势是简单易实施,在业务上基本没有改造的成本;缺点是在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。

数据迁移时如何预热缓存

如果部署一个空的缓存,那么所有的请求就都穿透到数据库,数据库可能因为承受不了这么大的压力而宕机,这样你的服务就会不可用了。所以,缓存迁移的重点是保持缓存的热度。
Redis 的数据迁移可以使用双写的方案或者级联同步的方案,所以在这里我就不考虑 Redis 缓存的同步了,而是以 Memcached 为例来说明。

使用以下这种方式,我们可以实现缓存数据的迁移,又可以尽量控制专线的带宽和请求的延迟情况,你也可以直接在项目中使用。

以上我提到的数据迁移的方案,都是我在实际项目中,经常用到的、经受过实战考验的方案,希望你能通过这节课的学习,将这些方案运用到你的项目中,解决实际的问题。与此同时,我想再次跟你强调一下本节课的重点内容:
* 双写的方案是数据库、Redis 迁移的通用方案,你可以在实际工作中直接加以使用。双写方案中最重要的,是通过数据校验来保证数据的一致性,这样就可以在迁移过程中随时回滚;
* 如果你需要将自建机房的数据迁移到云上,那么也可以考虑使用级联复制的方案,这种方案会造成数据的短暂停写,需要在业务低峰期执行;
* 缓存的迁移重点,是保证云上缓存的命中率,你可以使用改进版的副本组方式来迁移,在缓存写入的时候,异步写入云上的副本组,在读取时放少量流量到云上副本组,从而又可以迁移部分数据到云上副本组,又能尽量减少穿透给自建机房造成专线延迟的问题。

如果你作为项目的负责人,那么在迁移的过程中,你一定要制定周密的计划:如果是数据库的迁移,那么数据的校验应该是你最需要花费时间来解决的问题。
如果是自建机房迁移到云上,那么专线的带宽一定是你迁移过程中的一个瓶颈点,你需要在迁移之前梳理清楚,有哪些调用需要经过专线,占用带宽的情况是怎样的,带宽的延时是否能够满足要求。你的方案中也需要尽量做到在迁移过程中,同机房的服务,调用同机房的缓存和数据库,尽量减少对于专线带宽资源的占用。

17 | 消息队列:秒杀时如何处理每秒上万次的下单请求?

在我历年的工作经历中,我一直把消息队列看作暂时存储数据的一个容器,认为消息队列是一个平衡低速系统和高速系统处理任务时间差的工具,我给你举个形象的例子。

异步处理、解耦合和削峰填谷是消息队列在秒杀系统设计中起到的主要作用,其中,
异步处理可以简化业务流程中的步骤,提升系统性能;
削峰填谷可以削去到达秒杀系统的峰值流量,让业务逻辑的处理更加缓和;
解耦合可以将秒杀系统和数据系统解耦开,这样两个系统的任何变更都不会影响到另一个系统

在使用消息队列之后虽然可以解决现有的问题,但是系统的复杂度也会上升。比如上面提到的业务流程中,同步流程和异步流程的边界在哪里?消息是否会丢失,是否会重复?请求的延迟如何能够减少?消息接收的顺序是否会影响到业务流程的正常执行?如果消息处理流程失败了之后是否需要补发?这些问题都是我们需要考虑的。我会利用接下来的两节课,针对最主要的两个问题来讲讲解决思路:一个是如何处理消息的丢失和重复,另一个是如何减少消息的延迟。


可以增加多个消息队列,第一个是削峰填谷,第二个是异步处理耗时的任务,第三个是解耦合其他功能模块

18 | 消息投递:如何保证消息仅仅被消费一次?

消息丢失

消息为什么会丢失
如果要保证消息只被消费一次,首先就要保证消息不会丢失。那么消息从被写入到消息队列,到被消费者消费完成,这个链路上会有哪些地方存在丢失消息的可能呢?其实,主要存在三个场景:
* 消息从生产者写入到消息队列的过程。
消息的生产者一般是我们的业务服务器,消息队列是独立部署在单独的服务器上的。两者之间的网络虽然是内网,但是也会存在抖动的可能,而一旦发生抖动,消息就有可能因为网络的错误而丢失
* 消息在消息队列中的存储场景。
拿 Kafka 举例,消息在 Kafka 中是存储在本地磁盘上的,而为了减少消息存储时对磁盘的随机 I/O,我们一般会将消息先写入到操作系统的 Page Cache 中,然后再找合适的时机刷新到磁盘上。
* 消息被消费者消费的过程。
消息取出标记完成,处理程序挂掉,会造成遗漏

消息取出,处理完毕后标记完成,若在标记处完成后,程序挂掉,就会造成重复。

消息重复

消息在生产和消费的过程中都可能会产生重复,所以你要做的是,在生产过程和消费过程中增加消息幂等性的保证,这样就可以认为从“最终结果上来看”,消息实际上是只被消费了一次的。

生产端
在消息生产过程中,在 Kafka0.11 版本和 Pulsar 中都支持“producer idempotency”的特性,翻译过来就是生产过程的幂等性,这种特性保证消息虽然可能在生产端产生重复,但是最终在消息队列存储时只会存储一份。

消费端
而在消费端,幂等性的保证会稍微复杂一些,你可以从通用层和业务层两个层面来考虑。
通用层面,你可以在消息被生产的时候,使用发号器给它生成一个全局唯一的消息 ID,消息被处理之后,把这个 ID 存储在数据库中,在处理下一条消息之前,先从数据库里面查询这个全局 ID 是否被消费过,如果被消费过就放弃消费。

业务层面怎么处理呢?这里有很多种处理方式,其中有一种是增加乐观锁的方式。比如,你的消息处理程序需要给一个人的账号加钱,那么你可以通过乐观锁的方式来解决。
具体的操作方式是这样的:你给每个人的账号数据中增加一个版本号的字段,在生产消息时先查询这个账户的版本号,并且将版本号连同消息一起发送给消息队列。消费端在拿到消息和版本号后,在执行更新账户金额 SQL 的时候带上版本号,类似于执行:

update user set amount = amount + 20, version=version+1 where userId=1 and version=1;

你看,我们在更新数据时给数据加了乐观锁,这样在消费第一条消息时,version 值为 1,SQL 可以执行成功,并且同时把 version 值改为了 2;在执行第二条相同的消息时,由于 version 值不再是 1,所以这条 SQL 不能执行成功,也就保证了消息的幂等性。

课程小结:
* 消息的丢失可以通过生产端的重试、消息队列配置集群模式,以及消费端合理处理消费进度三个方式来解决。
* 为了解决消息的丢失通常会造成性能上的问题以及消息的重复问题。
* 通过保证消息处理的幂等性可以解决消息的重复问题。

19 | 消息队列:如何降低消息队列系统中消息的延迟?

消息堆积的太多延迟了,这其实是消费性能的问题,那么你要如何提升消费性能,保证更短的消息延迟呢?在我看来,你首先需要掌握如何来监控消息的延迟,因为有了数据之后,你才可以知道目前的延迟数据是否满足要求,也可以评估优化之后的效果。

监控消息延迟

在消费队列中数据的时候还需要注意消费线程空转的问题,取不到的时候可以sleep一下。

21 | 系统架构:每秒1万次请求的系统要做服务化拆分吗?

在什么情况下对一体化架构系统进行微服务化拆分
* 系统中,使用的资源出现扩展性问题,尤其是数据库的连接数出现瓶颈;
* 大团队共同维护一套代码,带来研发效率的降低,和研发成本的提升;
* 系统部署成本越来越高。

从中你应该有所感悟:在架构演进的初期和中期,性能、可用性、可扩展性是我们追求的主要目标,高性能和高可用给用户带来更好的使用体验,扩展性可以方便我们支撑更大量级的并发。但是当系统做的越来越大,团队成员越来越多,我们就不得不考虑成本了。
这里面的“成本”有着复杂的含义,它不仅代表购买服务器的费用,还包括研发团队,内部的开发成本,沟通成本以及运维成本等等,甚至有些时候,成本会成为架构设计中的决定性因素。
比方说,你做一个直播系统,在架构设计时除了要关注起播速度,还需要关注 CDN 成本;再比如作为团队 Leader,你在日常开发中除了要推进正常的功能需求开发,也要考虑完善工具链建设,提高工程师的研发效率,降低研发成本。
这很好理解,如果在一个 100 个人的团队中,你的工具为每个人每天节省了 10 分钟,那么加起来就是接近 17 小时,差不多增加了 2 个人工作时间。而正是基于提升扩展性和降低成本的考虑,我们最终走上了微服务化的道路。

22 | 微服务架构:微服务化后,系统架构要如何改造?

原则一,做到单一服务内部功能的高内聚,和低耦合。
原则二,你需要关注服务拆分的粒度,先粗略拆分,再逐渐细化。在服务拆分的初期,你其实很难确定,服务究竟要拆分成什么样。服务多了会带来问题,像是服务个数的增加会增加运维的成本。再比如,原本一次请求只需要调用进程内的多个方法,现在则需要跨网络调用多个 RPC 服务,在性能上肯定会有所下降。
所以推荐的做法是:拆分初期可以把服务粒度拆的粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。
原则三,拆分的过程,要尽量避免影响产品的日常功能迭代,也就是说,要一边做产品功能迭代,一边完成服务化拆分。
原则四,服务接口的定义要具备可扩展性(调用参数要做到兼容)。服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信,就不再是进程内部的方法调用,而是跨进程的网络通信了。在这种通信模型下需要注意,服务接口的定义要具备可扩展性,否则在服务变更时,会造成意想不到的错误。

微服务化带来的问题和解决思路

  1. 服务接口的调用,不再是同一进程内的方法调用,而是跨进程的网络调用,这会增加接口响应时间的增加。此时,我们就要选择高效的服务调用框架,同时,接口调用方需要知道服务部署在哪些机器的哪个端口上,这些信息需要存储在一个分布式一致性的存储中,于是就需要引入服务注册中心,这一点,是我在 24 讲会提到的内容。不过在这里我想强调的是,注册中心管理的是服务完整的生命周期,包括对于服务存活状态的检测。
  2. 多个服务之间有着错综复杂的依赖关系。一个服务会依赖多个其它服务,也会被多个服务所依赖,那么一旦被依赖的服务的性能出现问题,产生大量的慢请求,就会导致依赖服务的工作线程池中的线程被占满,那么依赖的服务也会出现性能问题。接下来,问题就会沿着依赖网,逐步向上蔓延,直到整个系统出现故障为止。
    为了避免这种情况的发生,我们需要引入服务治理体系,针对出问题的服务,采用熔断、降级、限流、超时控制的方法,使得问题被限制在单一服务中,保护服务网络中的其它服务不受影响。
  3. 服务拆分到多个进程后,一条请求的调用链路上,涉及多个服务,那么一旦这个请求的响应时间增长,或者是出现错误,我们就很难知道,是哪一个服务出现的问题。另外,整体系统一旦出现故障,很可能外在的表现是所有服务在同一时间都出现了问题,你在问题定位时,很难确认哪一个服务是源头,这就需要引入分布式追踪工具,以及更细致的服务端监控报表
    监控报表关注的是,依赖服务和资源的宏观性能表现;
    分布式追踪关注的是,单一慢请求中的性能瓶颈分析,两者需要结合起来帮助你来排查问题。

1.“康威定律”提到,设计系统的组织,其产生的设计等同于组织间的沟通结构。通俗一点说,就是你的团队组织结构是什么样的,你的架构就会长成什么样
如果你的团队分为服务端开发团队,DBA 团队,运维团队,测试团队,那么你的架构就是一体化的,所有的团队共同为一个大系统负责,团队内成员众多,沟通成本就会很高;
而如果你想实现微服务化的架构,那么你的团队也要按照业务边界拆分,每一个模块由一个自治的小团队负责,这个小团队里面有开发、测试、运维和 DBA,这样沟通就只发生在这个小团队内部,沟通的成本就会明显降低。

23 | RPC框架:10万QPS下如何实现毫秒级的服务调用?

说到 RPC(Remote Procedure Call,远程过程调用),你不会陌生,它指的是通过网络,调用另一台计算机上部署服务的技术。
而 RPC 框架就封装了网络调用的细节,让你像调用本地服务一样,调用远程部署的服务。

RPC 的调用都经过了哪些步骤
* 在一次 RPC 调用过程中,客户端首先会将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流;
* 然后客户端将二进制流,通过网络发送给服务端;
* 服务端接收到二进制流之后,将它反序列化,得到需要调用的类名、方法名、参数名和参数值,再通过动态代理的方式,调用对应的方法得到返回值;
* 服务端将返回值序列化,再通过网络发送给客户端;
* 客户端对结果反序列化之后,就可以得到调用的结果了。

为了优化 RPC 框架的性能,本节课,我带你了解了网络 I/O 模型序列化方式的选择,它们是实现高并发 RPC 框架的要素,总结起来有三个要点:
1. 选择高性能的 I/O 模型,这里我推荐使用同步多路 I/O 复用模型;
2. 调试网络参数,这里面有一些经验值的推荐。比如将 tcp_nodelay 设置为 true,也有一些参数需要在运行中来调试,比如接受缓冲区和发送缓冲区的大小,客户端连接请求缓冲队列的大小(back log)等等;
3. 序列化协议依据具体业务来选择。如果对性能要求不高,可以选择 JSON,否则可以从 Thrift 和 Protobuf 中选择其一。

24 | 注册中心:分布式系统如何寻址?

在服务拆分之后,你需要维护更多的细粒度的服务,而你需要面对的第一个问题就是,如何让 RPC 客户端知道服务端部署的地址。
服务注册和发现不是一个新的概念,你在之前的实际项目中也一定了解过,只是你可能没怎么注意罢了。比如说,你知道 Nginx 是一个反向代理组件,那么 Nginx 需要知道,应用服务器的地址是什么,这样才能够将流量透传到应用服务器上,这就是服务发现的过程。
那么 Nginx 是怎么实现的呢?它是把应用服务器的地址配置在了文件中。

目前业界有很多可供你来选择的注册中心组件,比如说老派的 ZooKeeper,Kubernetes 使用的 ETCD,阿里的微服务注册中心 Nacos,Spring Cloud 的 Eureka 等等。
这些注册中心的基本功能有两点:
* 其一是提供了服务地址的存储;
* 其二是当存储内容发生变化时,可以将变更的内容推送给客户端。


从图中,你可以看到一个完整的,服务注册和发现的过程:
1. 客户端会与注册中心建立连接,并且告诉注册中心,它对哪一组服务感兴趣;
2. 服务端向注册中心注册服务后,注册中心会将最新的服务注册信息通知给客户端;
3. 客户端拿到服务端的地址之后就可以向服务端发起调用请求了。

课程小结:
注册中心可以让我们动态地,变更 RPC 服务的节点信息,对于动态扩缩容,故障快速恢复,以及服务的优雅关闭都有重要的意义;
心跳机制是一种常见的探测服务状态的方式,你在实际的项目中也可以考虑使用;
我们需要对注册中心中管理的节点提供一些保护策略,避免节点被过度摘除导致的服务不可用。

25 | 分布式Trace:横跨几十个分布式组件的慢请求要如何排查?

一体化架构中的慢请求排查如何做

从经验出发来说,一个接口响应时间慢,一般是出在跨网络的调用上,比如说请求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类,做切面编程,通过插入一些代码打印它们的耗时就好了。

一般来说,切面编程的实现分为两类:
* 一类是静态代理,典型的代表是 AspectJ,它的特点是在编译期做切面代码注入;
* 另一类是动态代理,典型的代表是 Spring AOP,它的特点是在运行期做切面代码注入。

日志的采样频率:
一次请求可能要打印十几条日志,如果你的电商系统的 QPS 是 10000 的话,就是每秒钟会产生十几万条日志,对于磁盘 I/O 的负载是巨大的,那么这时,你就要考虑如何减少日志的数量。
你可以考虑对请求做采样,采样的方式也简单,比如你想采样 10% 的日志,那么你可以只打印“requestId%10==0”的请求。

有了这些日志之后,当给你一个 requestId 的时候,你发现自己并不能确定这个请求到了哪一台服务器上,所以你不得不登陆所有的服务器,去搜索这个 requestId 才能定位请求。这样无疑会增加问题排查的时间。
你可以考虑的解决思路是:把日志不打印到本地文件中,而是发送到消息队列里,再由消息处理程序写入到集中存储中,比如 Elasticsearch。这样,你在排查问题的时候,只需要拿着 requestId 到 Elasticsearch 中查找相关的记录就好了。在加入消息队列和 Elasticsearch 之后,我们这个排查程序的架构图也会有所改变:

单台排查步骤:
1. 在记录打点日志时,我们使用 requestId 将日志串起来,这样方便比较一次请求中的多个步骤的耗时情况;
2. 我们使用静态代理的方式做切面编程,避免在业务代码中,加入大量打印耗时的日志的代码,减少了对于代码的侵入性,同时编译期的代码注入可以减少;
3. 我们增加了日志采样率,避免全量日志的打印;
4. 最后为了避免在排查问题时,需要到多台服务器上搜索日志,我们使用消息队列,将日志集中起来放在了 Elasticsearch 中。

如何来做分布式 Trace

在一体化架构中,单次请求的所有的耗时日志,都被记录在一台服务器上,而在微服务的场景下,单次请求可能跨越多个 RPC 服务,这就造成了,单次的请求的日志会分布在多个服务器上。通过 requestId 将多个服务器上的日志串起来,但是仅仅依靠 requestId 很难表达清楚服务之间的调用关系,所以从日志中,就无法了解服务之间是谁在调用谁。因此,我们采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系

那么 spanId 是何时生成的,又是如何传递的呢?
首先,A 服务在发起 RPC 请求服务 B 前,先从线程上下文中获取当前的 traceId 和 spanId,然后,依据上面的逻辑生成本次 RPC 调用的 spanId,再将 spanId 和 traceId 序列化后,装配到请求体中,发送给服务方 B。
服务方 B 获取请求后,从请求体中反序列化出 spanId 和 traceId,同时设置到线程上下文中,以便给下次 RPC 调用使用。在服务 B 调用完成返回响应前,计算出服务 B 的执行时间发送给消息队列。

课程小结:
* 服务的追踪的需求主要有两点,一点对代码要无侵入,你可以使用切面编程的方式来解决;另一点是性能上要低损耗,我建议你采用静态代理和日志采样的方式,来尽量减少追踪日志对于系统性能的影响;
* 无论是单体系统还是服务化架构,无论是服务追踪还是业务问题排查,你都需要在日志中增加 requestId,这样可以将你的日志串起来,给你呈现一个完整的问题场景。如果 requestId 可以在客户端上生成,在请求业务接口的时候传递给服务端,那么就可以把客户端的日志体系也整合进来,对于问题的排查帮助更大。

26 | 负载均衡:怎样提升系统的横向扩展能力?

LVS 在 OSI 网络模型中的第四层,传输层工作,所以 LVS 又可以称为四层负载;而 Nginx 运行在 OSI 网络模型中的第七层,应用层,所以又可以称它为七层负载
在项目的架构中,我们一般会同时部署 LVS 和 Nginx 来做 HTTP 应用服务的负载均衡。也就是说,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上,为什么这么做呢?
主要和 LVS 和 Nginx 的特点有关,LVS 是在网络栈的四层做请求包的转发,请求包转发之后,由客户端和后端服务直接建立连接,后续的响应包不会再经过 LVS 服务器,所以相比 Nginx,性能会更高,也能够承担更高的并发。
因此,LVS 适合在入口处,承担大流量的请求分发,而 Nginx 要部署在业务服务器之前做更细维度的请求分发。我给你的建议是,如果你的 QPS 在十万以内,那么可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器,这样少维护一个组件,也会减少系统的维护成本。

不过这两个负载均衡服务适用于普通的 Web 服务,对于微服务架构来说,它们是不合适的。
因为微服务架构中的服务节点存储在注册中心里,使用 LVS 就很难和注册中心交互,获取全量的服务节点列表。另外,一般微服务架构中,使用的是 RPC 协议而不是 HTTP 协议,所以 Nginx 也不能满足要求。
所以,我们会使用另一类的负载均衡服务,客户端负载均衡服务,也就是把负载均衡的服务内嵌在 RPC 客户端中。
它一般和客户端应用,部署在一个进程中,提供多种选择节点的策略,最终为客户端应用提供一个最佳的,可用的服务端节点。这类服务一般会结合注册中心来使用,注册中心提供服务节点的完整列表,客户端拿到列表之后使用负载均衡服务的策略选取一个合适的节点,然后将请求发到这个节点上。

常见的负载均衡策略有哪些

负载均衡策略从大体上来看可以分为两类:
* 一类是静态策略,也就是说负载均衡服务器在选择服务节点时,不会参考后端服务的实际运行的状态。
* 一类是动态策略,也就是说负载均衡服务器会依据后端服务的一些负载特性,来决定要选择哪一个服务节点。

常见的静态策略有几种,其中使用最广泛的是轮询的策略(RoundRobin,RR),这种策略会记录上次请求后端服务的地址或者序号,然后在请求时,按照服务列表的顺序,请求下一个后端服务节点。
带权重的轮询策略,比如给 8 核 8G 的机器配置权重为 2,那么就会给它分配双倍的流量。

课程小结:
* 网站负载均衡服务的部署,是以 LVS 承接入口流量,在应用服务器之前,部署 Nginx 做细化的流量分发,和故障节点检测。当然,如果你的网站的并发不高,也可以考虑不引入 LVS。
* 负载均衡的策略可以优先选择动态策略,保证请求发送到性能最优的节点上;如果没有合适的动态策略,那么可以选择轮询的策略,让请求平均分配到所有的服务节点上。
* Nginx 可以引入 nginx_upstream_check_module,对后端服务做定期的存活检测,后端的服务节点在重启时,也要秉承着“先切流量后重启”的原则,尽量减少节点重启对于整体系统的影响。

27 | API网关:系统的门面要如何做呢?

课程小结
本节课我带你了解了 API 网关在系统中的作用,在实现中的一些关键的点,以及如何将 API 网关引入你的系统,重点如下:
1. API 网关分为入口网关和出口网关两类,入口网关作用很多,可以隔离客户端和微服务,从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权,审计以及访问控制;
2. API 网关的实现重点在于性能和扩展性,你可以使用多路 I/O 复用模型和线程池并发处理,来提升网关性能,使用责任链模式来提升网关的扩展性;
3. API 网关中的线程池,可以针对不同的接口或者服务做隔离和保护,这样可以提升网关的可用性;
4. API 网关可以替代原本系统中的 Web 层,将 Web 层中的协议转换、认证、限流等功能挪入到 API 网关中,将服务聚合的逻辑下沉到服务层。
5. API 网关可以为 API 的调用提供便捷,也可以为将一些服务治理的功能独立出来,达到复用的目的,虽然在性能上可能会有一些损耗,但是一般来说,使用成熟的开源 API 网关组件,这些损耗都是可以接受的。所以,当你的微服务系统越来越复杂时,你可以考虑使用 API 网关作为整体系统的门面。

28 | 多机房部署:跨地域的分布式系统如何做?

课程小结:
* 不同机房的数据传输延迟,是造成多机房部署困难的主要原因,你需要知道,同城多机房的延迟一般在 1ms~3ms,异地机房的延迟在 50ms 以下,而跨国机房的延迟在 200ms 以下。
* 同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取,和服务的调用应该尽量保证在同一个机房中。
* 异地多活方案则应该避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房。

多机房部署是一个业务发展到一定规模,对于机房容灾有需求时,才会考虑的方案,能不做则尽量不要做。一旦你的团队决定做多机房部署,那么同城双活已经能够满足你的需求了,这个方案相比异地多活要简单很多。而在业界,很少有公司,能够搭建一套真正的异步多活架构,这是因为这套架构在实现时过于复杂,所以,轻易不要尝试。
总之,架构需要依据系统的量级和对可用性、性能、扩展性的要求,不断演进和调整,盲目地追求架构的“先进性”只能造成方案的复杂,增加运维成本,从而给你的系统维护带来不便。

29 | Service Mesh:如何屏蔽服务化系统的服务治理细节?

一个公司的不同团队,使用不同的开发语言是比较常见的。比如,微博的主要开发语言是 Java 和 PHP,近几年也有一些使用 Go 开发的系统。而使用不同的语言开发出来的微服务,在相互调用时会存在两方面的挑战:
一方面,服务之间的通信协议上,要对多语言友好,要想实现跨语言调用,关键点是选择合适的序列化方式。我给你举一个例子。
比如,你用 Java 开发一个 RPC 服务,使用的是 Java 原生的序列化方式,这种序列化方式对于其它语言并不友好,所以在选择序列化协议时,考虑序列化协议是否对多语言友好,比如,你可以选择 Protobuf、Thrift,这样一来,跨语言服务调用的问题,就可以很容易地解决了。

如何让服务治理的策略在多语言之间复用呢?

课程小结
本节课,为了解决跨语言场景下,服务治理策略的复用问题,我带你了解了什么是 Service Mesh 以及如何在实际项目中落地,你需要的重点内容如下:
1.Service Mesh 分为数据平面和控制平面。数据平面主要负责数据的传输;控制平面用来控制服务治理策略的植入。出于性能的考虑,一般会把服务治理策略植入到数据平面中,控制平面负责服务治理策略数据的下发。
2.Sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另一种是通过轻量级客户端来实现流量转发。
目前,在一些大厂中,比如微博、蚂蚁金服,Service Mesh 已经开始在实际项目中大量的落地实践,而我建议你持续关注这项技术。它本身是一种将业务与通信基础设施分离的技术,如果你的业务上遇到多语言环境下,服务治理的困境,如果你的遗留服务,需要快速植入服务治理策略,如果你想要将你在服务治理方面积累的经验,快速地与其他团队共享,那么 Service Mesh 就是你的一个不错的选择。

30 | 给系统加上眼睛:服务端监控要怎么做?

监控指标如何选择?

谷歌针对分布式系统监控的经验总结,四个黄金信号(Four Golden Signals)。它指的是,在服务层面一般需要监控四个指标,分别是延迟,通信量、错误和饱和度。
* 延迟指的是请求的响应时间。比如,接口的响应时间、访问数据库和缓存的响应时间。
* 通信量可以理解为吞吐量,也就是单位时间内,请求量的大小。比如,访问第三方服务的请求量,访问消息队列的请求量。
* 错误表示当前系统发生的错误数量。这里需要注意的是, 我们需要监控的错误既有显示的,比如在监控 Web 服务时,出现 4 * * 和 5 * * 的响应码;也有隐示的,比如,Web 服务虽然返回的响应码是 200,但是却发生了一些和业务相关的错误(出现了数组越界的异常或者空指针异常等),这些都是错误的范畴。
* 饱和度指的是服务或者资源到达上限的程度(也可以说是服务或者资源的利用率),比如说 CPU 的使用率,内存使用率,磁盘使用率,缓存数据库的连接数等等。

当然,一些组件或者服务还有独特的指标,这些指标也是需要你特殊关注的。比如,课程中提到的数据库主从延迟数据、消息队列的堆积情况、缓存的命中率等等。

如何采集数据指标

课程小结
本节课,我带你了解了,服务端监控搭建的过程,在这里,你需要了解以下几个重点:
1. 耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指标,你在搭建自己的监控系统的时候可以直接使用;
2. Agent、埋点和日志是三种最常见的数据采集方式;
3. 访问趋势报表用来展示服务的整体运行情况,性能报表用来分析资源或者依赖的服务是否出现问题,资源报表用来追查资源问题的根本原因。这三个报表共同构成了你的服务端监控体系。

31 | 应用性能管理:用户的使用体验应该如何监控?

应用性能管理(Application Performance Management,简称 APM),它的含义是:对应用各个层面做全方位的监测,期望及时发现可能存在的问题,并加以解决,从而提升系统的性能和可用性。
你是不是觉得和之前讲到的服务端监控很相似?其实,服务端监控的核心关注点是后端服务的性能和可用性,而应用性能管理的核心关注点是终端用户的使用体验,也就是你需要衡量,从客户端请求发出开始,到响应数据返回到客户端为止,这个端到端的整体链路上的性能情况。

课程小结
以上就是本节课的全部内容了。本节课,我主要带你了解了,如何搭建一个端到端的 APM 监控系统,你需要了解的重点是:
1. 从客户端采集到的数据可以用通用的消息格式,上传到 APM 服务端,服务端将数据存入到 Elasticsearch 中,以提供原始日志的查询,也可以依据这些数据形成客户端的监控报表;
2. 用户网络数据是我们排查客户端,和服务端交互过程的重要数据,你可以通过代码的植入,来获取到这些数据;
3. 无论是网络数据,还是异常数据,亦或是卡顿、崩溃、流量、耗电量等数据,你都可以通过把它们封装成 APM 消息格式,上传到 APM 服务端,这些用户在客户端上留下的踪迹可以帮助你更好地优化用户的使用体验。
总而言之,监测和优化用户的使用体验是应用性能管理的最终目标。然而,服务端的开发人员往往会陷入一个误区,认为我们将服务端的监控做好,保证接口性能和可用性足够好就好了。事实上,接口的响应时间只是我们监控系统中很小的一部分,搭建一套端到端的全链路的监控体系,才是你的监控系统的最终形态。

32 | 压力测试:怎样设计全链路压力测试平台?

你需要了解在流量增长若干倍的时候,系统的哪些组件或者服务会成为整体系统的瓶颈点,这时你就需要做一次全链路的压力测试。

与监控一样,压力测试是一种常见的,发现系统中存在问题的方式,也是保障系统可用性和稳定性的重要手段。而在压力测试的过程中,我们不能只针对某一个核心模块来做压测,而需要将接入层、所有后端服务、数据库、缓存、消息队列、中间件以及依赖的第三方服务系统及其资源,都纳入压力测试的目标之中。因为,一旦用户的访问行为增加,包含上述组件服务的整个链路都会受到不确定的大流量的冲击,因此,它们都需要依赖压力测试来发现可能存在的性能瓶颈,这种针对整个调用链路执行的压力测试也称为“全链路压测”。

通常做一次全链路压力测试,需要联合 DBA、运维、依赖服务方、中间件架构等多个团队,一起协调进行,无论是人力成本还是沟通协调的成本都比较高。同时,在压力测试的过程中,如果没有很好的监控机制,那么还会对线上系统造成不利的影响。为了解决这些问题,我们需要搭建一套自动化的全链路压测平台,来降低成本和风险。

课程小结
本节课,我带你了解了做压力测试常见的误区,以及自动化的全链路压测平台的搭建过程,这里你需要了解的重点是:
1. 压力测试是一种发现系统性能隐患的重要手段,所以应该尽量使用正式的环境和数据;
2. 对压测的流量需要增加标记,这样就可以通过 Mock 第三方依赖服务和影子库的方式来实现压测数据和正式数据的隔离;
3. 压测时,应该实时地对系统性能指标做监控和告警,及时地对出现瓶颈的资源或者服务扩容,避免对正式环境产生影响。
这套全链路的压力测试系统对于我们来说有三方面的价值:其一,它可以帮助我们发现系统中可能出现的性能瓶颈,方便我们提前准备预案来应对;其次,它也可以为我们做容量评估,提供数据上的支撑;最后,我们也可以在压测的时候做预案演练,因为压测一般会安排在流量的低峰期进行,这样我们可以降级一些服务来验证预案效果,并且可以尽量减少对线上用户的影响。

33 | 配置管理:成千上万的配置项要如何管理?

课程小结
1. 配置存储是分级的,有公共配置,有个性的配置,一般个性配置会覆盖公共配置,这样可以减少存储配置项的数量;

  1. 配置中心可以提供配置变更通知的功能,可以实现配置的热更新;
  2. 配置中心关注的性能指标中,可用性的优先级是高于性能的,一般我们会要求配置中心的可用性达到 99.999%,甚至会是 99.9999%。
    这里你需要注意的是,并不是所有的配置项都需要使用配置中心来存储,如果你的项目还是使用文件方式来管理配置,那么你只需要,将类似超时时间等,需要动态调整的配置,迁移到配置中心就可以了。对于像是数据库地址,依赖第三方请求的地址,这些基本不会发生变化的配置项,可以依然使用文件的方式来管理,这样可以大大地减少配置迁移的成本。

34 | 降级熔断:如何屏蔽非核心系统故障的影响?

在分布式环境下,系统最怕的反而不是某一个服务或者组件宕机,而是最怕它响应缓慢,因为,某一个服务或者组件宕机也许只会影响系统的部分功能,但它响应一慢,就会出现雪崩拖垮整个系统。
而我们在部门内部讨论方案的时候,会格外注意这个问题,解决的思路就是在检测到某一个服务的响应时间出现异常时,切断调用它的服务与它之间的联系,让服务的调用快速返回失败,从而释放这次请求持有的资源。这个思路也就是我们经常提到的降级和熔断机制。

课程小结:
以上就是本节课的全部内容了。本节课我带你了解了雪崩产生的原因,服务熔断的实现方式以及服务降级的策略,这里你需要了解的重点是:
1. 在分布式环境下最怕的是服务或者组件慢,因为这样会导致调用者持有的资源无法释放,最终拖垮整体服务。
2. 服务熔断的实现是一个有限状态机,关键是三种状态之间的转换过程。
3. 开关降级的实现策略主要有返回降级数据、降频和异步三种方案。

其实,开关不仅仅应该在你的降级策略中使用,在我的项目中,只要上线新的功能必然要加开关控制业务逻辑是运行新的功能还是运行旧的功能。这样,一旦新的功能上线后,出现未知的问题(比如性能问题),那么可以通过切换开关的方式来实现快速地回滚,减少问题的持续时间。
总之,熔断和降级是保证系统稳定性和可用性的重要手段,在你访问第三方服务或者资源的时候都需要考虑增加降级开关或者熔断机制,保证资源或者服务出现问题时,不会对整体系统产生灾难性的影响。

35 | 流量控制:高并发系统中我们如何操纵流量?

限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。限流策略一般部署在服务的入口层,比如 API 网关中,这样可以对系统整体流量做塑形。而在微服务架构中,你也可以在 RPC 客户端中引入限流的策略,来保证单个服务不会被过大的流量压垮。

课程小结
1. 限流是一种常见的服务保护策略,你可以在整体服务、单个服务、单个接口、单个 IP 或者单个用户等多个维度进行流量的控制;

  1. 基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;
  2. 令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一定的突发流量,所以在实际项目中应用更多。

限流策略是微服务治理中的标配策略,只是你很难在实际中确认限流的阈值是多少,设置的小了容易误伤正常的请求,设置的大了则达不到限流的目的。所以,一般在实际项目中,我们会把阈值放置在配置中心中方便动态调整;同时,我们可以通过定期地压力测试得到整体系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值。

37 | 计数系统设计(一):面对海量数据的计数器要如何做?

课程小结
1. 数据库 + 缓存的方案是计数系统的初级阶段,完全可以支撑中小访问量和存储量的存储服务。如果你的项目还处在初级阶段,量级还不是很大,那么你一开始可以考虑使用这种方案。
2. 通过对原生 Redis 组件的改造,我们可以极大地减小存储数据的内存开销。
3. 使用 SSD+ 内存的方案可以最终解决存储计数数据的成本问题。这个方式适用于冷热数据明显的场景,你在使用时需要考虑如何将内存中的数据做换入换出。

38 | 计数系统设计(二):50万QPS下如何设计未读数系统?

课程小结
1. 评论未读、@未读、赞未读等一对一关系的未读数可以使用上节课讲到的通用计数方案来解决;

  1. 在系统通知未读、全量用户打点等存在有限的共享存储的场景下,可以通过记录用户上次操作的时间或者偏移量,来实现未读方案;
  2. 最后,信息流未读方案最为复杂,采用的是记录用户博文数快照的方式。

这里你可以看到,这三类需求虽然都和未读数有关,但是需求场景不同、对于量级的要求不同,设计出来的方案也就不同。因此,就像我刚刚提到的样子,你在做方案设计的时候,要分析需求的场景,比如说数据的量级是怎样的,请求的量级是怎样的,有没有一些可以利用的特点(比如系统通知未读场景下的有限共享存储、信息流未读场景下关注人数是有限的等等),然后再制定针对性的方案,切忌盲目使用之前的经验套用不同的场景,否则就可能造成性能的下降,甚至危害系统的稳定性。

39 | 信息流设计(一):通用信息流系统的推模式要如何做?

课程小结
1. 推模式就是在用户发送微博时,主动将微博写入到他的粉丝的收件箱中;

  1. 推送信息是否延迟、存储的成本、方案的可扩展性以及针对取消关注和微博删除的特殊处理是推模式的主要问题;
  2. 推模式比较适合粉丝数有限的场景。

你可以看到,其实推模式并不适合微博这种动辄就有上千万粉丝的业务,因为这种业务特性带来的超高的推送消息延迟以及存储成本是难以接受的,因此,我们要么会使用基于拉模式的实现,要么会使用基于推拉结合模式的实现。

40 | 信息流设计(二):通用信息流系统的拉模式要如何做?

如何使用拉模式设计信息流系统
所谓拉模式,就是指用户主动拉取他关注的所有人的微博,将这些微博按照发布时间的倒序进行排序和聚合之后,生成信息流数据的方法。