高并发的那些事

273 阅读36分钟

前言

本文主要讲解高并发场景下,从技术角度,设计系统需要注意点,以及每种场景下的优化方案。这里只介绍概念和思路,目的是高并发场景下的知识面扫盲,由于篇幅问题,不会深入提供每种技术的实战代码等。

1、基础知识点

1.1、高并发系统通用设计方法

  • 分布式:采用分布式部署的方式把流量分流开,让每个服务器都承担一部分并发和流量。
  • 缓存:使用缓存来提高系统的性能
  • 异步:在某些场景下,未处理完成之前,可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。

1.2、架构分层

软件架构分层在软件工程中是一种常见的设计方式,它是将整体系统拆分成N个层次,每个层次有独立的职责,多个层次协同提供完整的功能。比如:MVC架构(Model-View-Controller)、系统分层(Web、Service 和 Dao)、OSI网络模型(七层,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层)等。

1.2.1、优点

  1. 分层的设计可以简化系统设计,让不同的人专注做某一层次的事情。
  2. 分层之后可以做到很高的复用,比如common包的utils方法。
  3. 分层架构可以更容易做横向扩展,比如:项目中复杂计算抽取出来独立部署。

1.2.2、分层方法

1、层次的边界。 2、层次之间一定是相邻层互相依赖,数据的流转也只能在相邻的两层之间流转。比如:Controller不能调Dao。

1.2.3、缺点

1、增加了代码的复杂度。 2、每个层次独立部署,层次间通过网络来交互,那么多层的架构在性能上会有损耗。

1.3、系统设计目标

三点:高性能、高可用、易扩展。

1.3.1、高性能

1.3.1.1、原则

  1. 性能优化一定不能盲目,一定是问题导向的;
  2. 性能优化也遵循“八二原则”, 即可以用 20% 的精力解决 80% 的性能问题;
  3. 性能优化也要有数据支撑,时刻了解优化让响应时间减少了多少,提升了多少的吞吐量;
  4. 性能优化的过程是持续的。

1.3.1.2、度量指标

  1. 平均值:是把这段时间所有请求的响应时间数据相加,再除以总请求数。可以大致参考
  2. 最大值:是这段时间内所有请求响应时间最长的值。过于敏感,看看就行
  3. 分位值:有很多种,比如 90 分位、95 分位、75 分位。以 90 分位为例,把这段时间请求的响应时间从小到大排序,假如一共有 100 个请求,那么排在第 90 位的响应时间就是 90 分位值。分位值排除了偶发极慢请求对于数据的影响,能够很好地反应这段时间的性能情况,分位值越大,对于慢请求的影响就越敏感。

从用户使用体验的角度来看:

  1. 200ms是一个分界点,200ms之内感觉不到
  2. 1s是一个分界点,感受到一些延迟,但却是可以接受的,
  3. 超过1s之后,感觉明显,等待越长体验就越差。

所以,健康系统的99分位值的响应时间通常需要控制在200ms之内,而不超过1s的请求占比要在99.99%以上。

1.3.1.3、优化方法

  1. 提高系统的处理核心数,就是增加系统的并行处理能力,这个思路是优化性能最简单的途径。
  2. 减少单次任务响应时间。CPU 密集型系统中,需要处理大量的 CPU 运算,那么选用更高效的算法或者减少运算次数就是这类系统重要的优化手段。IO 密集型系统,比方说:如果是数据库访问慢,那么就要看是不是有锁表的情况、是不是有全表扫描、索引加得是否合适、是否有 JOIN 操作、需不需要加缓存,等等;如果是网络的问题,就要看网络的参数是否有优化的空间,抓包来看是否有大量的超时重传,网卡是否有大量丢包等。

1.3.2、系统怎样做到高可用

可用性是一个抽象的概念,与度量它相关的概念:MTBF和MTTR。

1.3.2.1、度量标准

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

可用性与MTBF和MTTR的值息息相关,可以用下面的公式表示它们之间的关系:

Availability = MTBF / (MTBF + MTTR)

上面公式的值Availability,就是下图的系统可用性这一列,如下图: image.png 一般来说,会使用几个九来描述系统的可用性,核心业务系统的可用性,需要达到四个九,非核心系统的可用性最多容忍到三个九。

从图中可以看出二个九之后,系统的年故障时间从3天锐减到8小时。四个九年故障时间缩减到1小时之内,需要建立完善的运维值班体系、故障处理流程和业务变更流程,在系统设计上有更多的考虑,比如,在开发中要考虑,如果发生故障,是否不用人工介入就能自动恢复,在工具建设方面,也需要多加完善,以便快速排查故障原因,让系统快速恢复。五个九故障就不能靠人力恢复了,靠系统的容灾和自动恢复的能力,让机器来处理故障,才会让可用性指标提升一个档次。

1.3.4、如何让系统易于扩展

指以通过增加机器的方式来线性提高系统的处理能力,从而承担更高的流量和并发。拆分是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块,将复杂的问题简单化。

  1. 存储层的扩展性,考虑的维度是根据各个业务读写的瓶颈拆分;
  2. 业务层的扩展性,从三个维度考虑业务层的拆分方案:业务纬度、重要性性纬度和请求来源纬度。

未做拆分的系统虽然可扩展性不强,但是却足够简单,无论是系统开发还是运行维护都不需要投入很大的精力。拆分之后,需求开发需要横跨多个系统多个小团队,排查问题也需要涉及多个系统,运行维护上,可能每个子系统都需要有专人来负责,对于团队是一个比较大的考验。这个考验是必须要经历的一个大坎,需要做好准备。

2、数据库

2.1、池化技术

  • 池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
  • 池子中的对象需要在使用之前预先初始化完成,这叫做池子的预热,比方说使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
  • 池化技术核心是一种空间换时间优化方法的实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄露或者频繁垃圾回收等问题。

2.2、主从分离

查询量增加时,通过主从分离和一主多从部署抵抗增加的数据库流量的,但也会有一些问题:

  1. 主从的一致性和写入性能的权衡,如果要保证所有从节点都写入成功,那么写入性能一定会受影响;如果只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,而在互联网的项目中,一般会优先考虑性能而不是数据的强一致性。
  2. 主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果遇到这类问题不妨先看看主从延迟的数据。
  3. 使用麻烦,业界有很多的方案可以屏蔽主从分离之后数据库访问的细节,让开发人员像是访问单一数据库一样,包括有像 TDDL、Sharding-JDBC 这样的嵌入应用内部的方案,也有像 Mycat 这样的独立部署的代理方案。

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

2.3、分库分表

在面对数据库容量瓶颈和写并发量大的问题时,可以采用垂直拆分和水平拆分来解决,带来的问题:

  1. 查询数据必须带上分区键;
  2. 列表总数需要单独冗余存储;
  3. 在实现分库分表过程中,数据从单库单表迁移多库多表,即繁杂又容易出错;
  4. 初期没有规划得当,后面要继续增加数据库数或者表数时,还要迁移。

分库分表的原则主要有以下几点:

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

2.4、分布式id

在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了,需要生成分布式ID了。

要求:

  • 全局唯一 :ID 的全局唯一性肯定是首先要满足的!
  • 高性能 : 分布式 ID 的生成速度要快,对本地资源消耗要小。
  • 高可用 :生成分布式 ID 的服务要保证可用性无限接近于 100%。
  • 方便易用 :拿来即用,使用方便,快速接入!

保证:

  • 安全 :ID 中不包含敏感信息。
  • 有序递增 :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候,还很有可能会直接通过 ID 来进行排序。
  • 有具体的业务含义 :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
  • 独立部署 :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。

常见解决方案:

  1. 数据库:
    • 数据库主键自增,支持的并发量不大不推荐
    • 数据库号段模式,滴滴开源的Tinyid
    • NoSQL,使用redis的原子性操作,实现id自增不推荐
  2. 算法
    • UUID不推荐
    • Snowflake(雪花算法),当机器时间不对的情况下,可能导致会产生重复 ID
  3. 开源框架
    • UidGenerator(百度)
    • Leaf(美团)

2.5、NoSQL

NoSQL数据库在性能、扩展性上的优势,以及它的一些特殊功能特性,主要有以下几点:

  1. 在性能方面,NoSQL 数据库使用一些算法将对磁盘的随机写转换成顺序写,提升了写的性能;
  2. 在某些场景下,比如全文搜索功能,关系型数据库并不能高效地支持,需要NoSQL数据库的支持;
  3. 在扩展性方面,NoSQL数据库天生支持分布式,支持数据冗余和数据分片的特性。

NoSQL 可供选型的种类很多,每一个组件都有各自的特点。在做选型的时候需要对它的实现原理有比较深入的了解,最好在运维方面对它有一定的熟悉,这样在出现问题时才能及时找到解决方案。

3、缓存

3.1、缓存介绍

缓存的定义,常见缓存的分类以及缓存的不足:

  • 缓存可以有多层,比如上面提到的静态缓存处在负载均衡层,分布式缓存处在应用层和数据库层之间,本地缓存处在应用层。需要将请求尽量挡在上层,因为越往下层,对于并发的承受能力越差;
  • 缓存命中率是对于缓存最重要的一个监控项,越是热点的数据,缓存的命中率就越高。

缓存不仅仅是一种组件的名字,更是一种设计思想,任何能够加速读请求的组件和设计方案都是缓存思想的体现。而这种加速通常是通过两种方式来实现:

  • 使用更快的介质,比方说内存;
  • 缓存复杂运算的结果。

3.2、缓存的读写策略

缓存使用的几种策略,以及每种策略适用的使用场景:

  1. Cache Aside(旁路缓存)最常用的策略,更新数据时不更新缓存,而是删除缓存中的数。问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响,解决方案:

    • 在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
    • 在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受。
  2. Read/Write Through(读穿/写穿)策略,核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。

image.png 3. Write Back(写回)策略是计算机体系结构中的策略,不过写入策略中的只写缓存,异步写入后端存储的策略倒是有很多的应用场景。和Read/Write Through(读穿/写穿)策略都需要缓存组件的支持,所以比较适合在实现本地缓存组件的时候使用; image.png image.png 在实际开发过程中,需要结合实际的业务特点灵活使用,甚至加以改造,这些业务特点包括但不仅限于:整体的数据量级情况,访问的读写比例的情况,对于数据的不一致时间的容忍度,对于缓存命中率的要求等等。

3.3、缓存高可用

分布式缓存的高可用方案主要有三种:

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

3.4、缓存穿透

在发现缓存系统命中率下降时,可能是穿透了,解决方案:

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

数据库是一个脆弱的资源,在扩展性、性能还是承担并发的能力上,相比缓存都处于绝对的劣势,所以解决缓存穿透问题的核心目标在于减少对于数据库的并发请求。 Redis击穿、穿透、雪崩

3.5、CDN

CDN是系统的门面,其缓存的静态数据,如图片和视频数据的请求量很可能是接口请求数据的几倍甚至更高,一旦发生故障,对于整体系统的影响是巨大的。另外CDN的带宽。是研发成本的大头,尤其是目前处于小视频和直播风口上,大量的小视频和直播研发团队都在绞尽脑汁地减少CDN的成本。 由此看出,CDN是整体系统至关重要的组成部分,而它作为一种特殊的缓存,其命中率和可用性也是服务端开发人员需要重点关注的指标。

CDN对静态资源进行加速的原理和使用的核心技术,重点:

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

4、消息队列

4.1、MQ作用

  1. 削峰填谷是消息队列最主要的作用,但是会造成请求处理的延迟。
  2. 异步处理是提升系统性能的神器,但是需要分清同步流程和异步流程的边界,同时消息存在着丢失的风险,需要考虑如何确保消息一定到达。
  3. 解耦合可以提升的整体系统的健壮性。

4.2、消费的可靠性和幂等性

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

4.3、降低消息的延迟

可以使⽤消息队列提供的⼯具,或者通过发送监控消息的⽅式,来监控消息的延迟情况;横向扩展消费者是提升消费处理能⼒的重要⽅式; 选择⾼性能的数据存储⽅式,配合零拷⻉技术,可以提升消息的消费性能。

5、分布式服务

5.1、拆分系统因素

微服务化拆分决定性的因素:

  1. 系统使用的资源出现扩展性问题,比如数据库的连接数出现瓶颈;
  2. 大团队共同维护一套代码,研发效率低,研发成本高;
  3. 部署成本高,里面用到了各种中间件。

5.2、改造成微服务架构

微服务化拆分的原则:

  1. 做到单一服务内部功能的高内聚,和低耦合。
  2. 关注服务拆分的粒度,先粗略拆分,再逐渐细化。
  3. 拆分的过程,要尽量避免影响产品的日常功能迭代,
  4. 服务接口的定义要具备可扩展性。

常识:

  1. 团队组织结构决定架构。如果团队分为服务端开发团队,DBA团队,运维团队,测试团队,那么架构就是一体化的;如果微服务化的架构,团队要按照业务边界拆分,每一个模块由一个自治的小团队负责,这个小团队里面有开发、测试、运维和 DBA。
  2. 微服务化的一个目标是减少研发的成本,比如沟通的成本。
  3. 可以优先做工程的拆分。依据业务的边界,将代码拆分到不同的子工程中,然后子工程之间以 jar 包的方式依赖,这样每个子工程代码量减少,可以减少打包时间;并且子工程代码内部,可以做到高内聚低耦合,一定程度上减少研发的成本。

5.3、RPC框架

改造成微服务架构,提升了系统的扩展性,但性能也会变差,比如一个接口查三个表,只会进行一次IO请求,现在变成一接口调三个服务,每个服务再查各自的库,一共6次网络请求。

RPC步骤:

  1. 客户端将调用的类名、方法名、参数名、参数值等信息,序列化成二进制流;
  2. 客户端将二进制流,通过网络发送给服务端;
  3. 服务端将返回值序列化,再通过网络发送给客户端;
  4. 客户端对结果反序列化之后,得到调用的结果。

从上面的步骤,可以看出,要提升RPC框架的性能,需要从网络传输和序列化两方面来优化。实现高并发RPC框架的要素:

  1. 选择高性能的I/O模型,推荐使用同步多路I/O复用模型;
  2. 调试网络参数,比如将tcp_nodelay设置为true,关闭Nagle算法,有数据发送时就马上发送;在运行中来调试一些参数,比如接受缓冲区和发送缓冲区的大小,客户端连接请求缓冲队列的大小(back log)等;
  3. 序列化协议依据具体业务来选择。如果对性能要求不高,可以选择JSON,否则可以从Thrift和Protobuf中选择其一,他俩缺点就是由于IDL存在带来一些使用上的不方便。

5.4、注册中心

老派的 ZooKeeper,Kubernetes 使用的 ETCD,阿里的微服务注册中心 Nacos,Spring Cloud 的 Eureka 等。基本功能:

  1. 提供了服务地址的存储;
  2. 当存储内容发生变化时,可以将变更的内容推送给客户端。

心跳机制:注册中心为每一个连接上来的 RPC 服务节点,记录最近续约的时间,RPC 服务节点在启动 注册到注册中心后,就按照一定的时间间隔(比如 30 秒),向注册中心发送心跳包。注册 中心在接受到心跳包之后,会更新这个节点的最近续约时间。然后,注册中心会启动一个定 时器,定期检测当前时间和节点,最近续约时间的差值,如果达到一个阈值(比如说 90 秒),那么认为这个服务节点不可用。

通知风暴:注册中心推送的消息很多,严重占用机器的带宽资源。解决方案:

  1. 控制一组注册中心管理的服务集群的规模,具体限制多少没有统一的标准,需要结合业务以及注册中心的选型来考虑,主要考察的指标就是注册中心服务器的峰值带宽;
  2. 通过扩容注册中心节点的方式来解决;
  3. 规范对于注册中心的使用方式,如果只是变更某一个节点,那么只需要通知这个节点的变更信息即可;
  4. 最后,如果是自建的注册中心,也可以在其中加入一些保护策略,比如说如果通知的消息量达到某一个阈值就停止变更通知

总结:

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

5.5、分布式追踪

SkyWalkingtlog

5.6、负载均衡

分类:

  1. 代理类的负载均衡服务,比如LVS和Nginx,在入口处部署 LVS,将流量分发到多个 Nginx 服务器上,再由 Nginx 服务器分发到应用服务器上;
  2. 客户端负载均衡服务,也就是把负载均衡的服务 内嵌在 RPC 客户端中。为微服务架构中的服务节点存储在注册中心里,使用 LVS 就很难和注册中心交互, 获取全量的服务节点列表。客户端负载均衡是在spring-cloud分布式框架组件Ribbon中定义的。(微服务推荐这种)

负载均衡策略:

  1. 静态策略:轮询策略、是带有权重的轮询策略;Nginx 提供了 ip_hash 和 url_hash 算法; LVS 提供了按照请求的源地址,和目的地址做 hash 的策略; Dubbo 也提供了随机选取策略,以及一致性 hash 的策略。
  2. 动态策略:Dubbo 提供的 LeastAcive 策略,就是优先选择活跃连接数最少的服务; Spring Cloud 全家桶中的 Ribbon 提供了 WeightedResponseTimeRule 是使用响应 时间,给每个服务节点计算一个权重,然后依据这个权重,来给调用方分配服务节点。(推荐)

总结:

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

5.7、API网关

API 网关(API Gateway)不是一个开源组件,而是一种架构模式,它是将一些服务共有的 功能整合在一起,独立部署为单独的一层,用来解决一些服务治理的问题。常见的有:

  1. Netfix 开源的 API 网关 Zuul,是 Spring Cloud 全家桶中的成员;
  2. Kong是在 Nginx 中运行的 Lua 程序;
  3. Tyk是一种 Go 语言实现的轻量级 API 网关。

作用:

  1. 将用户的请求动态路由到不同的业务服务上,并且做一些必要的协议转换工作。
  2. 植入一些服务治理的策略,比如服务的熔断、降 级,流量控制和分流等;
  3. 客户端的认证和授权的实现;
  4. 黑白名单相关的事情,比如针对设备 ID、用户 IP、用户 ID 等维度的黑白名单。

优点:

  1. API 网关分为入口网关和出口网关两类,入口网关作用很多,可以隔离客户端和微服务,从中提供协议转换、安全策略、认证、限流、熔断等功能。出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权、审计以及访问控制;
  2. API 网关的实现重点在于性能和扩展性,可以使用多路 I/O 复用模型和线程池并发处理,来提升网关性能,使用责任链模式来提升网关的扩展性;
  3. API 网关中的线程池可以针对不同的接口或者服务做隔离和保护,这样可以提升网关的可用性;
  4. API 网关可以替代原本系统中的 Web 层,将 Web 层中的协议转换、认证、限流等功能挪入到 API 网关中,将服务聚合的逻辑下沉到服务层。

缺点:性能上可能会有一些损耗,可以接受。

5.8、多机房部署

多机房部署是一个业务发展到一定规模,对于机房容灾有需求时才会考虑的方案,能不做则尽量不要做。重点:

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

一旦团队决定做多机房部署,那么同城双活已经能够满足需求了,这个方案相比异地多活要简单很多。而在业界,很少有公司能够搭建一套真正的异步多活架构,这是因为这套架构在实现时过于复杂,所以,轻易不要尝试。

5.9、ServiceMesh

Service Mesh 主要处理服务之间的通信,它的主要实现形式就是在应用程序同主机上部署 一个代理程序,一般来讲,将这个代理程序称为“Sidecar(边车)”,服务之间的通 信也从之前的,客户端和服务端直连,变成了A RPC->A Sidecar->B Sidecar-> B RPC。

在这种形式下,RPC 客户端将数据包先发送给,与自身同主机部署的 Sidecar,在 Sidecar 中经过服务发现、负载均衡、服务路由、流量控制之后,再将数据发往指定服务节点的 Sidecar,在服务节点的 Sidecar 中,经过记录访问日志、记录分布式追踪日志、限流之 后,再将数据发送给 RPC 服务端。 这种方式,可以把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立 的基础模块。这样一来,不仅可以实现跨语言,服务治理策略的复用,还能对这些 Sidecar 做统一的管理。目前,业界提及最多的 Service Mesh 方案当属istio。

总结:

  1. Service Mesh 分为数据平面和控制平面。数据平面主要负责数据的传输;控制平面用来控制服务治理策略的植入。出于性能的考虑,一般会把服务治理策略植入到数据平面中,控制平面负责服务治理策略数据的下发。
  2. Sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另一种是通过轻量级客户端来实现流量转发。

6、维护

6.1、服务端监控

SkyWalking

总结:

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

6.2、应用性能管理

SkyWalking 总结:

  1. 从客户端采集到的数据可以用通用的消息格式,上传到 APM 服务端,服务端将数据存入到Elasticsearch中,以提供原始日志的查询,也可以依据这些数据形成客户端的监控报表;
  2. 用户网络数据是排查客户端,和服务端交互过程的重要数据,可以通过代码的植入,来获取到这些数据;
  3. 无论是网络数据,还是异常数据,亦或是卡顿、崩溃、流量、耗电量等数据,都可以通过把它们封装成APM消息格式,上传到 APM 服务端,这些用户在客户端上留下的踪迹 可以更好地优化用户的使用体验。

6.3、压力测试

做压力测试常见的误区:

  1. 做压力测试时,最好使用线上的数据和线上的环境,因为无法确定自己搭建的测试环境与正式环境的差异,是否会影响到压力测试的结果;
  2. 压力测试时不能使用模拟的请求,而是要使用线上的流量。可以通过拷贝流量 的方式,把线上流量拷贝一份到压力测试环境。因为模拟流量的访问模型,和线上流量相差 很大,会对压力测试的结果产生比较大的影响;
  3. 不要从一台服务器发起流量,这样很容易达到这台服务器性能瓶颈,从而导致压力测试 的 QPS 上不去,最终影响压力测试的结果。而且,为了尽量真实地模拟用户请求,倾向于把流量产生的机器,放在离用户更近的位置,比如放在 CDN 节点上。如果没有这个条 件,那么可以放在不同的机房中,这样可以尽量保证压力测试结果的真实性。

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

全链路压测平台的搭建过程:

  1. 流量的隔离。由于压力测试是在正式环境进行,所以需要区分压力测试流量和正式流量,这样可以针对压力测试的流量做单独的处理。
  2. 风险的控制。也就是,尽量避免压力测试对于正常访问用户的影响,因此,一般来 说全链路压测平台需要包含以下几个模块: 流量构造和产生模块;压测数据隔离模块;系统健康度检查和压测流量干预模块。

全链路的压力测试系统价值:

  1. 发现系统中可能出现的性能瓶颈,方便提前准备预案来应对;
  2. 做容量评估,提供数据上的支撑;
  3. 在压测的时候做预案演练,因为压测一般会安排在流量的低峰期进行,可以降级一些服务来验证预案效果,并且可以尽量减少对线上用户的影响。

总结:

  1. 压力测试是一种发现系统性能隐患的重要手段,所以应该尽量使用正式的环境和数据;
  2. 对压测的流量需要增加标记,这样就可以通过 Mock 第三方依赖服务和影子库的方式来 实现压测数据和正式数据的隔离;
  3. 压测时,应该实时地对系统性能指标做监控和告警,及时地对出现瓶颈的资源或者服务扩容,避免对正式环境产生影响。
  4. 压力测试应该作为系统稳定性 保障的常规手段,周期性地进行。

6.4、配置管理

携程开源的 Apollo,百度开源的 Disconf,360 开源的 QConf,Spring Cloud 的组件 Spring Cloud Config 等等。高可用:增加两级缓存:第一级缓存是内存的缓存;另外一级缓 存是文件的缓存。热加载:一种是轮询查询的方式;一种是长连推送的方式。

总结:

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

6.5、降级熔断

熔断机制: 三种状态:关闭(调用远程服务)、半打开 (尝试调用远程服务)和打开(返回错误)。自定义redis熔断客户端;

降级机制: 放弃部分非核心功能或者服务,保证整体的可用性的方法,是一种有损的系统容错方 式。这样看来,熔断也是降级的一种,除此之外还有限流降级、开关降级等

总结:

  1. 在分布式环境下最怕的是服务或者组件慢,因为这样会导致调用者持有的资源无法释放,最终拖垮整体服务。
  2. 服务熔断的实现是一个有限状态机,关键是三种状态之间的转换过程。
  3. 开关降级的实现策略主要有返回降级数据、降频和异步三种方案。 其实,开关不仅仅应该在降级策略中使用,在项目中,只要上线新的功能必然要加 开关控制业务逻辑是运行新的功能还是运行旧的功能。这样,一旦新的功能上线后,出现未 知的问题(比如性能问题),那么可以通过切换开关的方式来实现快速地回滚,减少问题的 持续时间。 总之,熔断和降级是保证系统稳定性和可用性的重要手段,在访问第三方服务或者资源的 时候都需要考虑增加降级开关或者熔断机制,保证资源或者服务出现问题时,不会对整体系 统产生灾难性的影响

6.6、限流

限流指的是通过限制到达系统的并发请求数量,保证系统能够正常响应部分用户请求,而对 于超过限制的流量,则只能通过拒绝服务的方式保证整体系统的可用性。

作用:

  1. 可以对系统每分钟处理多少请求做限制;
  2. 可以针对单个接口设置每分钟请求流量的限制;
  3. 可以限制单个 IP、用户 ID 或者设备 ID 在一段时间内发送请求的数量;
  4. 对于服务于多个第三方应用的开放平台来说,每一个第三方应用对于平台方来说都有一 个唯一的 appkey 来标识,那么也可以限制单个 appkey 的访问接口的速率。

限流算法:

  1. 固定窗口与滑动窗口的算法;
  2. 漏桶算法与令牌筒算法;

总结:

  1. 限流是一种常见的服务保护策略,可以在整体服务、单个服务、单个接口、单个IP或者单个用户等多个维度进行流量的控制;
  2. 基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;
  3. 令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,它能够应对一定的突发流量,所以在实际项目中应用更多。 实际开发中,把限流的阈值放置在配置中心中方便动态调整;同时,可以通过定期地压力测试得到整体系统以及每个微服务的实际承载能力,然后再依据这个压测出来的值设置合适的阈值。