服务高并发、高性能、高可用实现方案

  软件开发的三高指标:高并发、高性能、高可用。

  高并发方面要求QPS 大于 10万;高性能方面要求请求延迟小于 100 ms;高可用方面要高于 99.99%(4个9)

 

 

 一、高并发:

  高并发是现在互联网分布式框架设计必须要考虑的因素之一,它是可以保证系统能同时并发处理很多请求,对于高并发来说,它的指标有:

    1、响应时间:系统对进来的请求反应的时间,比如你打开一个页面需要1秒,那么这1秒就是响应时间

    2、吞吐量:吞吐量指每秒能处理多少请求数量

    3、每秒查询率(QPS,Queries Per Second):每秒响应请求数,和吞吐量差不多

    4、并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数

 

  提高QPS的架构策略

    Redis、MQ、多线程

    1、负载均衡:

      高并发首选方案就是集群化部署,一台服务器承载的QPS有限,多台服务器叠加效果就会有明显提升。

      集群化部署,就需要考虑如何将流量转发到服务器集群,这里就需要用到负载均衡,如LVS(Linux Virtual Server)和nginx。

      常用的负载均衡算法有轮询法、随机法、源地址哈希法、加权轮询法、加权随机法、最小连接法等。

      业务实战:对于千万级流量的秒杀业务,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析负载均衡。搭配高性能网卡,单台LVS能够提供百万以上并发能力。

        注意, LVS 负责网络四层协议转发,无法按 HTTP 协议中的请求路径做负载均衡,所以还需要 Nginx

      

    2、池化技术:

      复用单个连接无法承载高并发,如果每次请求都新建连接、关闭连接,考虑到TCP的三次握手、四次挥手,需要花费大量开销。

      池化技术的核心是资源的“预分配”和“循环利用”,常用的池化技术有线程池、连接池、进程池、对象池、内存池、协程池。

        连接池的几个重要参数:最小连接数、空闲连接数、最大连接数

      

    3、流量漏斗(风控拦截):

      以上的几种方式是正向方式提升系统QPS,我们也可以逆向思维,做减法,拦截非法请求,将核心能力留给正常业务请求。

      互联网高并发流量并不都是纯净的,也有很多恶意流量(比如黑客攻击、恶意爬虫、黄牛、秒杀器等),我们需要设计流量拦截器,将哪些非法的、无资格的、低优先级的流量过滤掉(风控掉),减轻系统的并发压力。

      拦截器分层:

        网关和 WAF(Web Application Firewall,Web 应用防火墙)

          采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法

        风控分析。借助大数据能力分析订单等历史业务数据,对同ip多个账号下单、或者下单后支付时间过快等行为有效识别,并给账号打标记,提供给业务团队使用

        下游的每个tomcat实例应用本地内存缓存化,将一些库存存储在本地一份,做前置校验。当然,为了尽量保持数据的一致性,有定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中

    

    4、直接读写缓存,不可直接读写关系型数据库

      MySQL即使使用分库分表,读写分离,完美的连接池配置等也无法抵挡qps大于10W带来的冲击。

      我们必须使用内存缓存,缓存预热读多级缓存(JVM缓存,其次Redis),写消息队列最后写入MySQL

    5、多级缓存

      Redis目前是缓存的第一首选.单机可达6-8万的qps,在面对高并发的情况下,我们可以手动的水平扩容,以达到应对qps可能无线增长的场景。但是这种做法也存在弊端,因为redis是单线程的,并且会存在热点问题

      虽然redis内部用crc16算法做了hash打散,但是同一个key还是会落到一个单独的机器上,就会使机器的负载增加,redis典型的存在缓存击穿和缓存穿透两个问题,尤其在秒杀这个场景中,如果要解决热点问题,就变的比较棘手

      这个时候多级缓存就必须要考虑了,典型的在秒杀的场景中,单sku商品在售卖开始的瞬间,qps会急剧上升.而我们这时候需要用memeryCache来挡一层,memeryCache是多线程的,比redis拥有更好的并发能力,并且它是天然可以解决热点问题的。有了memeryCache,我们还需要localCache,本地缓存,这是一种以内存换速度的方式。本地缓存会接入用户的第一层请求,如果它找不到,接下来走memeryCache,然后走redis,这套流程下来可以挡住百万的qps

    6、多线程

      多线程并发处理提高处理速度,CountDownLatch

    7、优化IO

      如将多次单个的请求,优化为一次批量请求,减少网络IO

      对应MySQL就是批量插入,批量查询

      因为每次建立连接,数据交互,释放连接都会消耗大量的资源,同时涉及到用户态到核心态的切换

    8、优雅打印日志

      高并发情况下,日志打印不当会占用程序的IO,增加响应耗时。如果日志量过大,会导致磁盘打满

      ①:异步打印日志,控制日志输出的长度

      ②:基于白名单的日志打印,线上配置了白名单用户请求才打印日志,避免大量的无效日志输出

 

 

  其他:  

    机器扩容:

      大流量到来之前,对服务机器进行扩容,分化消化流量。

      如Redis缓存单机可达6-8W的qps,在高并发到来之前,可以手动或配置自动伸缩扩容,以达到应对qps可能无限增长的场景。 

 

    高并发服务发散:

      假设qps为10W,每个请求读写数据为1KB,那么10W个请求每秒钟读写就达到1GB,1分钟则60GB,这对于底层的数据存储与访问都是巨大的压力。

      

 

 

 、高性能:

  高性能指程序处理速度非常快,所占内存少,且CPU占用率低。高性能的指标经常和高并发的指标紧密相关,想要提升性能,那么就要提高系统并发能力,两者互相捆绑在一起。 

  

  高性能指标要求:

指标 请求耗时
P50 50ms
P75 75ms
P90 90ms
P99(百分之99的请求) 100ms

 

  P50: 即中位数值。100个请求按照响应时间从小到大排列,位置为50的值,即为P50值

  P95:响应耗时从小到大排列,顺序处于95%位置的值即为P95值

  

 

 

  

  有哪些因素会影响系统的性能?

    业务代码的逻辑设计,算法实现是否高效、架构设计

    业务系统CPU、内存、磁盘等性能

    下游系统的性能

    业务链路的长度

    请求/响应数据包大小

    用户网络环境

 

  怎么样提高性能呢?

    1、避免因为IO阻塞让CPU闲置,导致CPU的浪费。

      当系统处理大量磁盘IO操作的时候,由于CPU和内存的速度远高于磁盘,可能导致CPU耗费太多时间等待磁盘返回处理的结果。对于这部分在IO上的开销,称为"iowait"。

      磁盘有个性能指标:IOPS,即每秒读写次数,性能较好的固态硬盘,IOPS 大概在 3 万左右。对于秒杀系统,如果单节点QPS在10万,每次请求产生3条日志,那么日志的写入QPS在 30W/s,磁盘根本扛不住

      Linux 有一种特殊的文件系统:tmpfs(临时文件系统),它是一种基于内存的文件系统,由操作系统管理。当我们写磁盘的时候实际是写到内存中,当日志文件达到我们的设置阈值,操作系统会将日志写到磁盘中,并将tmpfs中的日志文件删除

      这种批量化、顺序写,大大提升了磁盘的吞吐性能!

    2、避免多线程间增加锁来保证同步,导致并行系统串行化

    3、避免创建、销毁、维护太多进程、线程,导致操作系统浪费资源在调度上

    4、高性能缓存,如Redis。

      对热点数据从缓存中读取来提升热点数据的访问性能,避免热点数据每次都从数据库中读取,给数据库带来压力

 

  1、无锁化

    大多数情况下,多线程处理处理可以提高并发性能

    1):串行无锁

      无锁串行最简单的实现方式可能就是单线程模型了,如 redis/Nginx 都采用了这种方式。

      网络编程模型中,主线程负责处理IO事件,当主线程MainReactor accept一个新连接之后从众多的SubReactor选取一个进行注册,通过创建一个Channel与IO线程进行绑定,此后该连接的读写都在同一个线程执行,无需进行同步

      主从Reactor职责链模型:

        

    2):结构无锁

      利用硬件支持的原子操作可以实现无锁的数据结构,如CAS原子操作

 

 

  2、零拷贝

    零拷贝博客

    

 

  3、序列化

    当将数据写入文件、发送到网络时通常需要序列化技术,从其读取时需要进行反序列化。

    序列化作为传输数据的表示形式,与网络框架和通信协议是解耦的。如网络框架 taf 支持 jce、json 和自定义序列化,HTTP 协议支持 XML、JSON 和流媒体传输等

    1)序列化分类

      ①:内置类型

        指编程语言内置支持的类型,如 java 的 java.io.Serializable。这种类型由于与语言绑定,不具有通用性,而且一般性能不佳,一般只在局部范围内使用

      ②:文本类型

        一般是标准化的文本格式,如 XML、JSON。这种类型可读性较好,且支持跨平台,具有广泛的应用。主要缺点是比较臃肿,网络传输占用带宽大

      ③:二进制类型

        用二进制编码,数据组织更加紧凑,支持多语言和多平台。常见的有 Protocol Buffer/Thrift/MessagePack/FlatBuffer 等

    2)性能指标

      衡量序列化/反序列化主要有三个指标:

      ①:序列化之后的字节大小;

      ②:序列化/反序列化的速度;

      ③:CPU 和内存消耗

      其中性能最好的是FlatBuffer,其次是Protobuf

    3)选型考量  

      ①:性能

        CPU 和字节占用大小是序列化的主要开销。在基础的 RPC 通信、存储系统和高并发业务上应该选择高性能高压缩的二进制序列化。一些内部服务、请求较少 Web 的应用可以采用文本的 JSON,浏览器直接内置支持 JSON

      ②:易用性

        丰富数据结构和辅助工具能提高易用性,减少业务代码的开发量。现在很多序列化框架都支持 List、Map 等多种结构和可读的打印

      ③:通用性

        现代的服务往往涉及多语言、多平台,能否支持跨平台跨语言的互通是序列化选型的基本条件

      ④:兼容性

        现代的服务都是快速迭代和升级,一个好的序列化框架应该有良好的向前兼容性,支持字段的增减和修改等

      ⑤:扩展性  

        序列化框架能否低门槛的支持自定义的格式有时候也是一个比较重要的考虑因素

 

 

  4、池化

    其本质是通过创建池子提高对象复用,减少重复创建、销毁的开销。

    常见的池化技术有内存池、线程池、连接池、对象池等

    1)内存池

      我们都知道,在 C/C++中分别使用 malloc/free 和 new/delete 进行内存的分配,其底层调用系统调用 sbrk/brk。频繁的调用系统调用分配释放内存不但影响性能还容易造成内存碎片,内存池技术旨在解决这些问题。正是这些原因,C/C++中的内存操作并不是直接调用系统调用,而是已经实现了自己的一套内存管理。

        malloc 的实现主要有三大实现。
        ①、ptmalloc:glibc 的实现。
        ②、tcmalloc:Google 的实现。
        ③、jemalloc:Facebook 的实现。

        tcmalloc和jemalloc性能差不多,ptmalloc性能不如两者,redis和mysql可以指定使用哪个malloc,我们可以根据需要选择更适合的malloc。

      内存管理的三个层次:

        

        

    2)线程池

      线程池使应用能更加充分利用CPU、内存、网络、IO等系统资源。限制线程的创建数量并复用已创建的线程,从而提高系统的性能。

      线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间

      线程的销毁时需要回收这些系统资源。因此频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程风险。

      另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程本身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。线程池的作用包括:

        ①:利用线程池管理并复用线程、控制最大并发数等

        ②:实现任务线程队列缓存策略和拒绝机制

        ③:实现某些与时间相关的功能,如定时执行、周期执行

        ④:隔离线程环境(分类或者分组)

          分组:通过配置两个或多个线程池,不同的任务使用不同的线程池,如较慢的任务与其他任务分隔开,避免任务间互相影响

          分类:可以分为核心和非核心,核心线程池一直存在不会被回收,非核心可能对空闲一段时间后的线程进行回收,从而节省系统资源,等到需要时在按需创建放入池子中

      线程池总结

 

    3)连接池

      常见的连接池有数据库连接池、Redis连接池、TCP连接池等。

      其主要目的是通过复用连接来减少创建和释放连接的开销。连接池实现通常需要考虑以下几个问题:

      ①:初始化时机

        启动即初始化或惰性初始化,通常使用启动即初始化的方式

        启动初始化可以减少一些加锁操作和需要时可以直接使用,缺点是可能造成服务启动缓慢或者启动后没有任务处理,造成资源浪费

        惰性初始化是使用的时候再去创建,这种方式可能有助于减少资源占用,但是面对突发的任务请求,然后瞬间去创建一堆连接,可能会造成系统响应慢甚至响应失败。

      ②:连接数目

        权衡所需的连接数,连接数太少则可能造成任务处理缓慢,太多不但使任务处理慢还会过度消耗系统资源

      ③:连接取出

        当连接池已经无可用连接时,是一直等待直到有可用连接还是分配一个新的临时连接

      ④:连接归还

        当连接使用完毕且连接池未满时,将连接放入连接池(包括 3 中创建的临时连接),否则关闭

      ⑤:连接有效性检测

        长时间空闲连接和失效连接需要关闭并从连接池移除。常用的检测方法有:使用时检测和定期检测

      数据库连接池

 

    4)对象池

      严格来说,各种池都是对象池模式的应用。

      对象池跟上面其他池一样,也是缓存一些对象从而避免大量创建同一个类型的对象,同时限制了实例的个数,如:

        ①:Redis 中 0-9999 整数对象就通过采用对象池进行共享

        ②:在游戏开发中对象池模式经常使用,如进入地图时怪物和 NPC 的出现并不是每次都是重新创建,而是从对象池中取出         

        ③:mdm中RedisTemplate对象缓存、uvcas中TalosProducer对象缓存

        public static final Map<String, RedisTemplate<String, Object>> REDIS_TEMPLATE_MAP = new ConcurrentHashMap<>();

         // 当前线程本地变量
        RdmContext rdmContext = RdmContext.currentContext();
        RedisProperties redisProperties = rdmContext.getRedisProperties();
        String token = rdmContext.getToken();
        // 看是否有缓存的redisTemplate
        RedisTemplate<String, Object> redisTemplate = RdmCache.REDIS_TEMPLATE_MAP.get(token);
        if (redisTemplate != null) {
            rdmContext.setRedisTemplate(redisTemplate);
            return;
        }
        // 创建RedisTEmplate并缓存
        // 缓存起来
        RdmCache.REDIS_TEMPLATE_MAP.put(token, redisTemplate);

 

 

  5、并发化

    1)请求并发

      如果一个任务需要处理多个子任务,可以将没有依赖关系的子任务并发化,这种场景在后台开发很常见。如一个请求需要查询 3 个数据,分别耗时 T1、T2、T3,如果串行调用总耗时 T=T1+T2+T3。对三个任务执行并发,总耗时 T=max(T1,T 2,T3)。同理,写操作也如此。对于同种请求,还可以同时进行批量合并,减少 RPC 调用次数

    2)冗余请求

      冗余请求指的是同时向后端服务发送多个同样的请求,谁响应快就是使用谁,其他的则丢弃。这种策略缩短了客户端的等待时间,但也使整个系统调用量猛增,一般适用于初始化或者请求少的场景

 

 

  6、异步化

    对于处理耗时长的任务,如果采用同步等待的方式,会严重降低系统的吞吐量,可以采用异步化进行解决。

    1)调用异步化

      在进行一个耗时的RPC调用或者任务处理时,常用的异步化方式如下:

      ①:Callback

        异步回调通过注册一个回调函数,然后发起异步任务,当任务执行完毕时会回调用户注册的回调函数,从而减少调用端等待时间。这种方式会造成代码分散难以维护,定位问题也相对困难

      ②:Future

        当用户提交一个任务时会立刻先返回一个 Future,然后任务异步执行,后续可以通过 Future 获取执行结果

  //异步并发任务
  Future<Response> f1 = Executor.submit(query1);
  //处理其他事情
  doSomething();
  //获取结果
  Response res1 = f1.getResult();

      ③:CPS

        (Continuation-passing style)可以对多个异步编程进行编排,组成更复杂的异步处理,并以同步的代码调用形式实现异步效果

        CPS 将后续的处理逻辑当作参数传递给 Then 并可以最终捕获异常,解决了异步回调代码散乱和异常跟踪难的问题

        Java 中的 CompletableFuture 和 C++ PPL 基本支持这一特性。典型的调用形式如下:

void handleRequest(const Request &req)
{
  return req.Read().Then([](Buffer &inbuf){
      return handleData(inbuf);
  }).Then([](Buffer &outbuf){
      return handleWrite(outbuf);
  }).Finally(){
      return cleanUp();
  });
}

 

    2)流程异步化

      一个业务流程往往伴随着调用链路长、后置依赖多等特点,这会同时降低系统的可用性和并发处理能力

      可以采用对非关键依赖进行异步化解决,如MQ

 

  7、缓存

    从单核 CPU 到分布式系统,从前端到后台,缓存无处不在

    缓存是原始数据的一个复制集,其本质就是空间换时间,主要是为了解决高并发读

      1)缓存的使用场景

        缓存是空间换时间的艺术,使用缓存能提高系统的性能。

        注意不要为了所谓的提高性能不计成本的使用缓存,而是要看场景。

        ①:一旦生成后基本不会变化的数据

        ②:读密集型或存在热点的数据

        ③:计算代价大的数据

        ④:千人一面的数据

        不适合使用缓存的场景

        ①:写多读少,更新频繁

        ②:对数据一致性要求严格

 

      2)缓存的分类

        ①:进程级缓存

          缓存的数据直接在进程地址空间内,这可能是访问速度最快使用最简单的缓存方式了。

          主要的缺点是受制于进程空间大小,能缓存的数据量有限,进程重启缓存数据会丢失。一般用于缓存数据量不大的场景,如JVM缓存

        ②:集中式缓存

          缓存的数据集中在一台机器上,如共享内存。这类缓存容量主要受制于机器内存大小,而且进程重启后数据不丢失。常用的集中式缓存中间件有单机版 redis、memcache 等

        ③:分布式缓存

          缓存的数据分布在多台机器上,通常需要采用特定算法(如 Hash)进行数据分片,将海量的缓存数据均匀的分布在每个机器节点上。常用的组件有:Memcache(客户端分片)、Codis(代理分片)、Redis Cluster(集群分片)

        ④:多级缓存

          指在系统中的不同层级的进行数据缓存,以提高访问效率和减少对后端存储的冲击

          本地缓存:caffeine

          外部缓存:Redis

 

      3)缓存一些好的实践

        ①:动静分离

          对于一个缓存对象,可能分为很多种属性,这些属性中有的是静态的,有的是动态的。在缓存的时候最好采用动静分离的方式

        ②:慎用大对象

          如果缓存对象过大,每次读写开销非常大并且可能会卡住其他请求,特别是在 redis 这种单线程的架构中。典型的情况是将一堆列表挂在某个 value 的字段上或者存储一个没有边界的列表,这种情况下需要重新设计数据结构或者分割 value 再由客户端聚合

        ③:过期设置

          尽量设置过期时间减少脏数据和存储占用,但要注意过期时间不能集中在某个时间段

        ④:超时设置

          缓存作为加速数据访问的手段,通常需要设置超时时间而且超时时间不能过长(如 100ms 左右),否则会导致整个请求超时连回源访问的机会都没有

        ⑤:缓存隔离

          首先,不同的业务使用不同的 key,防止出现冲突或者互相覆盖。其次,核心和非核心业务进行通过不同的缓存实例进行物理上的隔离

        ⑥:失败降级

          用缓存需要有一定的降级预案,缓存通常不是关键逻辑,特别是对于核心服务,如果缓存部分失效或者失败,应该继续回源处理,不应该直接中断返回

        ⑦:容量控制

          使用缓存要进行容量控制,特别是本地缓存,缓存数量太多内存紧张时会频繁的 swap 存储空间或 GC 操作,从而降低响应速度

        ⑧:业务导向

          以业务为导向,不要为了缓存而缓存。对性能要求不高或请求量不大,分布式缓存甚至数据库都足以应对时,就不需要增加本地缓存,否则可能因为引入数据节点复制和幂等处理逻辑反而得不偿失

        ⑨:监控告警

          对大对象、慢查询、内存占用等进行监控

 

 

  8、分片

    分片,即将一个较大的部分分成多个较小的部分,在这里我们分为数据分片和任务分片。

    对于数据分片,不同系统的拆分技术术语(如 region、shard、vnode、partition)等统称为分片

    分片可以说是一箭三雕的技术,将一个大数据集分散在更多节点上,单点的读写负载随之也分散到了多个节点上,同时还提高了扩展性和可用性

    数据分片,小到编程语言标准库里的集合,大到分布式中间件,无所不在,如:

      Java线程安全的ConcurrentHashMap采取分段机制,按照哈希或者取模将对象放置到某个分段中,减少锁争用

      分布式消息中间件 Kafka 中对 topic 也分成了多个 partition,每个 partition 互相独立可以并发读写

    1)分片策略

      进行分片时,要尽量均匀的将数据分布在所有节点上以平摊负载。

      如果分布不均,会导致倾斜使得整个系统性能的下降,常见的分片策略如下:

      ①:区间分片

        基于一段连续关键字的分片,保持了排序,适合进行范围查找,减少了垮分片读写。区间分片的缺点是容易造成数据分布不均匀,导致热点。如根据ID范围进行分片

        常见的还有按时间范围分片,则最近时间段的读写操作通常比很久之前的时间段频繁

      ②:随机分片

        按照一定的方式(如哈希取模)进行分片,这种方式数据分布比较均匀,不容易出现热点和并发瓶颈

        缺点就是失去了有序相邻的特性,如进行范围查询时会向多个节点发起请求

      ③:组合

        对区间分片和随机分片的一种折中,采取了两种方式的组合。通过多个键组成复合键,其中第一个键用于做哈希随机,其余键用于进行区间排序

        社交场景,如微信朋友圈、QQ 说说、微博等以用户 id+发布时间(user_id,pub_time)的组合找到用户某段时间的发表记录

 

    2)二级索引

      二级索引通常用来加速特定值的查找,不能唯一标识一条记录,使用二级索引需要两次查找查找。

      关系型数据库和一些KV数据库都支持二级索引,如MySQL中的非聚簇索引,ES倒排索引通过term找到文档都是二级索引。

      ①:本地二级索引

        二级索引存储在与关键字相同的分区中,即索引和记录在同一个分区中。

        这样对于写操作时都在一个分区里进行,不需要跨分区操作。但是对于读操作,需要聚合其他分区上的数据。

      ②:全局二级索引

        按索引值本身进行分区,与关键字独立。

        这样对于读取某个索引的数据时,都在一个分区里进行,而对于写操作,需要跨多个分区。

 

    3)路由策略

      路由策略决定如何将数据请求发送到指定的节点,包括分片调整后的路由。

      路由策略通常有三种方式:客户端路由、代理路由、集群路由

      ①:客户端路由

        客户端直接操作分片逻辑,感知分片和节点的分配关系并直接连接到目标节点。

        Memcache就是采用这种方式实现的分布式

      ②:代理层路由

        客户端的请求发送到代理层,由其转发到对应的数据节点上。

        很多分布式系统都采用了这种方式,如业界的基于redis实现的分布式存储codis等

      ③:集群路由

        由集群实现分片路由,客户端连接任意节点,如果该节点存在请求的分片,则处理;否则将请求转发到合适的节点或者告诉客户端重定向到目标节点

        如redis cluster就采用了这种方式

      以上几种方式各有优缺点,客户端路由实现相对简单但对业务入侵较强。

      代理层路由对业务透明,但增加了一层网络传输。对性能有一定影响,同时在部署上也相对复杂。

      集群路由对业务透明,且比代理路由少了一层结构,节约成本,但实现更复杂,且不合理的策略会增加多次网络传输

 

    4)动态平衡

      在学习平衡二叉树和红黑树的时候我们都知道,由于数据的插入删除会破坏其平衡性。为了保持树的平衡,在插入删除后我们会通过左旋右旋动态调整树的高度以保持再平衡。在分布式数据存储也同样需要再平衡,只不过引起不平衡的因素更多了,主要有以下几个方面:

      ①:读写负载增加,需要更多CPU

      ②:数据规模增加,需要更多磁盘和内存

      ③:数据节点故障,需要其他节点接替

      业务有很多产品支持动态平衡调增,如redis cluster的resharding,HDFS/kafka的rebalance等。常见的方式如下:

      ①:固定分区

        创建远超节点数的分区数,为每个几点分配多个分区。

        如果新增节点,可从现有的节点上均匀移走几个分区从而达到平衡,删除节点反之。

      ②:动态分区

        自动增减分区数,当分区数据增长到一定阀值时,则对进行拆分。当分区数据缩小到一定阀值时,对分区进行合并。

        类似于B+树的分裂删除操作。很多存储组件都采用了这种方式,如 HBase Region 的拆分合并,TDSQL 的 Set Shard。

        这种方式的优点是自动适配数据量,扩展性好。使用这种分区需要注意的一点,如果初始化分区为一个,刚上线请求量就很大的话会造成单点负载高,通常采取预先初始化多个分区的方式解决,如 HBase 的预分裂。

 

    5)分库分表

       当数据库的单表/单机数据量很大时,会造成性能瓶颈,为了分散数据库的压力,提高读写性能,需要采取分而治之的策略进行分库分表。通常,在以下情况下需要进行分库分表:

        ①:单表的数据量达到了一定的量级(如 mysql 一般为千万级),读写的性能会下降。这时索引也会很大,性能不佳,需要分解单表

        ②:数据库吞吐量达到瓶颈,需要增加更多数据库实例来分担数据读写压力

      分库分表按照特定的条件将数据分散到多个数据库和表中,分为垂直切分和水平切分两种模式。

       ①:垂直切分

        按照一定规则,如业务或模块类型,将一个数据库中的多个表分布到不同的数据库上

        优点:

          ①:切分规则清晰,业务划分明确

          ②:可以按照业务的类型、重要程度进行成本管理,扩展也方便

          ③:数据维护简单

        缺点:

          ①:不同表分到了不同的库中,无法使用表连接 Join。不过在实际的业务设计中,也基本不会用到 join 操作,一般都会建立映射表通过两次查询或者写时构造好数据存到性能更高的存储系统中

          ②:事务处理复杂,原本在事务中操作同一个库的不同表不再支持。这时可以采用柔性事务或者其他分布式事物方案

      ②:水平切分

        按照一定规则,如哈希或取模,将同一个表中的数据拆分到多个数据库上

        可以简单理解为按行拆分,拆分后的表结构是一样的

        优点:

          ①:切分后表结构一样,业务代码不需要改动

          ②:能控制单表数据量,有利于性能提升

        缺点:

          ①:join、count、记录合并、排序、分页等问题需要跨节点处理

          ②:相对复杂,需要实现路由策略;

      综上所述,垂直切分和水平切分各有优缺点,通常情况下这两种模式会一起使用。

 

    6)任务分片

      任务分片将一个任务分成多个子任务并行处理,加速任务的执行,通常涉及到数据分片,如归并排序首先将数据分成多个子序列,先对每个子序列排序,最终合成一个有序序列。

      在大数据处理中,Map/Reduce 就是数据分片和任务分片的经典结合

 

 

  9、存储

    1)读写分离 

      大多数业务都是读多写少,为了提高系统处理能力(因为写时会加锁无法读),可以采用读写分离的方式将主节点用于写,从节点用于读

      读写分离架构有以下几个特点:

        ①:数据库服务为主从架构,可以为一主一从或者一主多从

        ②:主节点负责写操作,从节点负责读操作

        ③:主节点将数据复制到从节点;基于基本架构,可以变种出多种读写分离的架构,如主-主-从、主-从-从。主从节点也可以是不同的存储,如 mysql+redis

      读写分离的主从架构一般采用异步复制,会存在数据复制延迟的问题,适用于对数据一致性要求不高的业务。可采用以下几个方式尽量避免复制滞后带来的问题

        ①:写后读一致性

          即读自己的写,适用于用户写操作后要求实时看到更新。典型的场景是,用户注册账号或者修改账户密码后,紧接着登录,此时如果读请求发送到从节点,由于数据可能还没同步完成,用户登录失败,这是不可接受的。针对这种情况,可以将自己的读请求发送到主节点上,查看其他用户信息的请求依然发送到从节点

        ②:二次读取

          优先读取从节点,如果读取失败或者跟踪的更新时间小于某个阀值,则再从主节点读取

        ③:关键业务读写主节点,非关键业务读写分离

        ④:单调读

          保证用户的读请求都发到同一个从节点,避免出现回滚的现象。如用户在 M 主节点更新信息后,数据很快同步到了从节点 S1,用户查询时请求发往 S1,看到了更新的信息。接着用户再一次查询,此时请求发到数据同步没有完成的从节点 S2,用户看到的现象是刚才的更新的信息又消失了,即以为数据回滚了

    2)动静分离  

      动静分离将经常更新的数据和更新频率低的数据进行分离。最常见于 CDN,一个网页通常分为静态资源(图片/js/css 等)和动态资源(JSP、PHP 等),采取动静分离的方式将静态资源缓存在 CDN 边缘节点上,只需请求动态资源即可,减少网络传输和服务负载  

      在数据库和 KV 存储上也可以采取动态分离的方式:

        ①:在缓存中,将一个缓存对象中的静态字段和动态字段分开缓存

        ②:在数据库中,动静分离更像是一种垂直切分,将动态和静态的字段分别存储在不同的库表中,减小数据库锁的粒度,同时可以分配不同的数据库资源来合理提升利用率

    3)冷热分离

      冷热分离可以说是每个存储产品和海量业务的必备功能,Mysql、ElasticSearch、CMEM、Grocery 等都直接或间接支持冷热分离

      将热数据放到性能更好的存储设备上,冷数据下沉到廉价的磁盘,从而节约成本。

        如保留7天的热数据,超过7天的数据任务其为冷数据进行迁移

    4)重写轻读

      ①:关键写,降低读的关键性,如异步复制,保证主节点写成功即可,从节点的读可容忍同步延迟

      ②:写重逻辑,读轻逻辑,将计算的逻辑从读转移到写。适用于读请求的时候还要进行计算的场景,常见的如排行榜是在写的时候构建而不是在读请求的时候再构建

    5)数据异构

      数据异构主要是按照不同的维度建立索引关系以加速查询

      如京东、天猫等网上商城,一般按照订单号进行了分库分表。由于订单号不在同一个表中,要查询一个买家或者商家的订单列表,就需要查询所有分库然后进行数据聚合

      可以采取构建异构索引,在生成订单的时同时创建买家和商家到订单的索引表,这个表可以按照用户 id 进行分库分表

 

 

  10、队列

    在系统应用中,不是所有的任务和请求必须实时处理,很多时候数据也不需要强一致性而只需保持最终一致性,有时候我们也不需要知道系统模块间的依赖,在这些场景下队列技术大有可为

    1)应用场景

      ①:异步处理

        业务请求的处理流程通常很多,有些流程并不需要在本次请求中立即处理,这时就可以采用异步处理

      ②:流量削峰

        高并发系统的性能瓶颈一般在 I/O 操作上,如读写数据库。面对突发的流量,可以使用消息队列进行排队缓冲

      ③:系统解耦

        解决系统之间的强调用关系,去除阻塞调用,改为异步消息

      ④:数据同步

        消息队列可以起到数据总线的作用,特别是在跨系统进行数据同步时。如通过 RabbitMQ 在写 Mysql 时将数据同步到 Redis,从而实现一个最终一致性的分布式缓存

      ⑤:柔性事务

        传统的分布式事务采用两阶段协议或者其优化变种实现,当事务执行时都需要争抢锁资源和等待,在高并发场景下会严重降低系统的性能和吞吐量,甚至出现死锁

        互联网的核心是高并发和高可用,一般将传统的事务问题转换为柔性事务

    

        柔性事务的核心流程为:

        1):分布式事务发起方在执行第一个本地事务前,向 MQ 发送一条事务消息并保存到MQ服务端,MQ 消费者无法感知和消费该消息 ①②

        2):事务消息发送成功后开始进行单机事务操作 ③

          a. 如果本地事务执行成功,则将 MQ 服务端的事务消息更新为正常状态 ④

          b.如果本地事务执行时因为宕机或者网络问题没有及时向 MQ 服务端反馈,则之前的事务消息会一直保存在 MQ。MQ 服务端会对事务消息进行定期扫描,如果发现有消息保存时间超过了一定的时间阀值,则向 MQ 生产端发送检查事务执行状态的请求 ⑤

          c.检查本地事务结果后 ⑥,如果事务执行成功,则将之前保存的事务消息更新为正常状态,否则告知 MQ 服务端进行丢弃

        3):消费者获取到事务消息设置为正常状态后,则执行第二个本地事务 ⑧。如果执行失败则通知 MQ 发送方对第一个本地事务进行回滚或正向补偿

 

    2)应用分类

      ①:缓冲队列

        队列的基本功能就是缓冲排队,如 TCP 的发送缓冲区,网络框架通常还会再加上应用层的缓冲区。使用缓冲队列应对突发流量时,使处理更加平滑,从而保护系统

        在大数据日志系统中,通常需要在日志采集系统和日志解析系统之间增加日志缓冲队列,以防止解析系统高负载时阻塞采集系统甚至造成日志丢弃,同时便于各自升级维护。如数据采集系统中,采用 Kafka 作为日志缓冲队列

      ②:请求队列

        对用户的请求进行排队,网络框架一般都有请求队列,如 spp 在 proxy 进程和 work 进程之间有共享内存队列,taf 在网络线程和 Servant 线程之间也有队列,主要用于流量控制、过载保护和超时丢弃等

      ③:任务队列

        将任务提交到队列中异步执行,最常见的就是线程池的任务队列

      ④:消息队列

        用于消息投递,主要有点对点和发布订阅两种模式,常见的有 RabbitMQ、RocketMQ、Kafka 等

 

 

 

 

 、高可用:  

  可用性:指一个系统处在可用工作状态的时间的比例

  高可用:让系统趋近于100%的高度可用

 

  具体衡量指标:

    MTBF(Mean Time Between Failure):平均故障间隔时间,平均无故障工作时间,即系统可用时长,单位为小时

    MTTR(Mean Time To Repair):系统从故障到恢复正常所耗费的时间

    SLA(Service-Level Agreement):服务等级协议,用于评估服务可用性等级。计算公式是 MTBF/(MTBF+MTTR)

   我们常说的可用性高于99.99%(4个9),是指指标SLA高于99.99%。

可用性 年故障时间 日故障时间
90% (1个9) 36.5天 2.4小时
99% (2个9) 3.65天 14.4分钟
99.9% (3个9) 0.365天,8小时 1.44分钟
99.99% (4个9) 0.0365天,52分钟 8.6秒
99.999% (5个9) 0.00365天,5分钟 0.86秒

   可用性级别:

级别

可用性级别

通俗说法

年度停机时间

配套措施

基本可用性

99%

2 个 9

3d-15h-39m-29s

服务在一个数据中心里有冗余,简单基础的自动化运维

高可用性

99.9%

3 个 9

8h-45m-56s 

大量的自动化故障工具,以及各种控制调度系统等基础设施要做好

具有故障自动恢复

99.99%

4 个 9

52m-35s

本地多机房(像 AWS 一样每个地方都有三个可用区)

极高可用性

99.999%

5 个 9

5m-15s

远程多机房,异地多活

 

  技术架构,高可用有哪些策略:

    多云架构、异地多活、异地备份

    主备切换:如Redis缓存、MySQL数据库,主备节点会实时数据同步、备份。如果主节点不可用,自动切换到备用节点

    微服务,无状态化架构、业务集群化部署,有心跳检测,能最短时间检测到不可用的服务

    通过熔断、限流,解决流量过载问题,提供过载保护

    重视web安全,解决攻击和XSS问题

 

  1、系统拆分

    早前的系统都是单体系统,比如电商业务,会员、商品、订单、物流、营销等模块都堆积在一个系统。每到节假日搞个大促活动,系统扩容时,一扩全扩,一挂全挂。只要一个接口出了问题,整个系统都不可用。

    因此面对庞大的单体系统,我们要对其做系统拆分为微服务架构。按照DDD(领域驱动设计Domain-DrivenDesign)的思想,将一个复杂的业务拆分成若干个子系统,每个子系统负责专属的业务功能,做好垂直化建设,各个子系统之间做好边界隔离,降低风险蔓延。

 

  2、解耦

    软件开发有个重要原则“高内聚、低耦合”

    小到接口抽象、MVC分层,大到SOLID原则,23种设计模式。核心都是降低不同模块间的耦合度,避免一处错误改动影响到整个系统

    思路如,Spring AOP、事件驱动模型

 

  3、异步

    同步指一个线程在执行请求的时候,若该请求需要一段时间才能返回信息,那么这个线程将会阻塞一直等待下去,直到收到返回信息才继续执行下去。

    如果是非实时响应的动作可以采用异步来完成,线程不需要一直等待,而是继续执行后面的逻辑

    如:线程池、消息队列

    举例:下单操作,我们关心的是订单是否创建成功,能否进行后续的付款流程

      至于其他的业务动作,如短信通知、邮件通知、生成订单快照,超时取消任务这些非核心动作用户并不是很关心,这些操作我们可以采用消息队列异步执行。在下单成功在数据库插入订单记录之后,发送消息到MQ,然后返回用户成功,监听消息的线程来完成这些操作

 

  4、重试

    重试主要体现在远程的RPC调用,受网络抖动、线程资源阻塞等因素影响,请求无法及时响应。

    为了提升用户体验,调用方可以通过 重试 方式再次发送请求,尝试获取结果。

    接口重试是一把双刃剑,虽然客户端收到了响应超时结果,但是我们无法确定,服务端是否已经执行完成。如果盲目地重试,可能会带来严重后果。比如:银行转账。

    重试通常跟幂等组合使用,如果一个接口支持了 幂等,那你就可以随便重试。

    重试方案

      ①:sisyphus

      ②:spring retry

    幂等方案:

      ①:插入前先执行查询操作,看是否存在,再决定是否插入。
      ②:增加唯一索引。
      ③:建防重表。
      ④:引入状态机,比如付款后,订单状态调整为已付款,SQL 更新记录前增加条件判断。
      ⑥:增加分布式锁。
      ⑦:采用 Token 机制,服务端增加 token 校验,只有第一次请求是合法的

 

  5、补偿

    通过补偿,来实现数据的最终一致性

    注意:补偿操作有个重要前提,业务能接受短时间内的数据不一致

    业务补偿根据处理的方向分为两部分:

      ①:正向

        多个操作构成一个分布式事务,如果部分成功。部分失败,我们会通过最大努力机制将失败的任务推到成功状态

      ②:逆向

        通过反向操作,将部分成功任务恢复到初始状态

    补偿实现方式:

      ①:本地建表方式,存储相关数据,然后通过定时任务扫描提取,并借助反射机制触发执行

      ②:也可以采用简单的消息中间件,构建业务消息体,由下游的消费任务执行。如果失败,可以借助 MQ 的重试机制,多次重试

    

  6、故障转移

    故障转移:一般指主备切换、缩短故障时间

    当系统出现故障时,首要任务不是立马查找原因,考虑到故障的复杂性,定位排查要花些时间,等问题修复好,SLA也降了好几个档。更好的解决方案就是:故障转移

    故障转移:当发现故障节点的时候,不是尝试修复它,而是立即把它隔离,同时将流量转移到正常节点上。这样通过故障转移,不仅减少了MTTR提升了SLA,还为修复故障节点赢得了足够的时间。

      ①:对等节点可直接转移切换

      ②:节点分主备时,转移时需要进行主备切换

    如何发现故障并自动转移?

      一般采用某种故障检测机制,比如心跳机制,备份节点定期发送心跳包,当多数节点未收到主节点的心跳包,表示主节点故障,需要进行切换

    切换到哪个备节点?

      一般采用paxos、raft等分布式一致性算法,在多个备份节点中选出新主节点

    主备切换大致分为三步:

      1)故障自动侦测(auto-detect),采用健康检查,心跳等手段自动侦测故障节点

      2)自动转移(failover),当侦测到故障节点后,采用摘除流量、脱离集群等方式隔离故障节点,将流量转移到正常节点

      3)自动恢复(failback),当故障节点恢复正常后,自动将其加入集群中,确保集群资源与故障前一致

 

  7、多活策略

    容灾备份策略并不能保证万事大吉

    在一些极端情况,如:机房断电、机房火灾、地震、山洪等不可抗力因素,所有的服务器(主、备)可能都同时出现故障,全部无法对外提供服务,导致整体业务瘫痪。

    为了降低风险,保证服务的24小时可用性,我们可以采用多活策略

    常见的多活方案有:同城多活、两地三中心、三地五中心、异地双活、异地多活等

 

  8、隔离

    隔离属于物理层面的分割,将若干的系统低耦合设计,独立部署,从物理上隔开。

    每个子系统有自己独立的代码库,独立开发,独立发布。一旦出现故障,也不会相互干扰。当然如果不同子系统间有相互依赖,这种情况比较特殊,需要有默认值或者异常特殊处理,这属于业务层面解决方案。

    隔离属于分布式技术的衍生产物,我们最常见的微服务解决方案。

    将一个大型的复杂系统拆分成若干个微服务系统,这些微服务子系统通常由不同的团队开发、维护,独立部署,服务之间通过 RPC 远程调用。

    隔离使得系统间边界更加清晰,故障可以更加隔离开来,问题的发现与解决也更加快速,系统的可用性也更高

 

  9、限流,提供过载保护

    高并发系统,如果遇到流量洪峰,超过了当前系统的承载能力,要怎么办

    一种方案,如果照单全收,CPU、内存、Load负载飙的很高,最后处理不过来,所有请求都超时无法正常响应

    另一种方案,将多余的流量舍弃掉

    限流定义:

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

    限流的原理跟熔断有点类似,都是通过判断某个条件来确定是否执行某个策略。但是又有所区别,熔断触发过载保护,该节点会暂停服务,直到恢复。限流,则是只处理自己能力范围之内的请求,超量的请求会被限流

    根据作用范围:限流分为单机版限流、分布式限流

    1、单机版限流

      主要借助于本机内存来实现计数器,比如通过 AtomicLong#incrementAndGet(),但是要注意之前不用的 key 定期做清理,释放内存

      纯内存实现,无需和其他节点统计汇总,性能最高。但是优点也是缺点,无法做到全局统一化的限流

    2、分布式限流

      单机版限流仅能保护自身节点,但无法保护应用依赖的各种服务,并且在进行节点扩容、缩容时也无法准确控制整个服务的请求限制。

      而分布式限流,以集群为维度,可以方便的控制这个集群的请求限制,从而保护下游依赖的各种服务资源

    限流支持的多个维度:

      ①:整个系统一定时间内(比如每分钟)处理多少请求

      ②:单个接口一定时间内处理多少流量

      ③:单个 IP、城市、渠道、设备 id、用户 id 等在一定时间内发送的请求数

      ④:如果是开放平台,则为每个 appkey 设置独立的访问速率规则

    限流算法主要有:

      计数器限流、滑动窗口限流、令牌桶限流、漏桶限流

 

 

  10、熔断,提供过载保护

    所谓过载保护,是指负载超过系统的承载能力时,系统会自动采取保护措施,确保自身不被压垮。

    熔断,其实是对调用链路中某个资源出现不稳定状态时(如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致联机错误。

      例子:熔断触发条件往往跟系统节点的承载能力和服务质量有关,比如 CPU 的使用率超过 90%,请求错误率超过 5%,请求延迟超过 500ms, 它们中的任意一个满足条件就会出现熔断。

    熔断的主要方式是使用断路器阻断故障服务器的调用。

    断路器有三种状态:关闭、打开、半打开

    ①:关闭(Closed)状态:在这个状态下,请求都会被转发给后端服务。同时会记录请求失败的次数,当请求失败次数在一段时间超过一定次数就会进入打开状态

    ②:打开(Open)状态:在这个状态下,熔断器会直接拒绝请求,返回错误,而不去调用后端服务。同时,会有一个定时器,时间到的时候会变成半打开状态。目的是假设服务会在一段时间内恢复正常

    ③:半打开(Half Open)状态:在这个状态下,熔断器会尝试把部分请求转发给后端服务,目的是为了探测后端服务是否恢复。如果请求失败会进入打开状态,成功情况下会进入关闭状态,同时重置计数

 

 

  11、降级

    降级是系统保护的一种重要手段

    为了使有限资源发挥最大价值,我们会临时关闭一些非核心功能,减轻系统压力,并将有限资源留给核心业务

    比如电商大促,业务在峰值时刻,系统抵挡不住全部的流量时,系统的负载、CPU 的使用率都超过了预警水位,可以对一些非核心的功能进行降级,降低系统压力,比如把商品评价、成交记录等功能临时关掉。弃车保帅,保证 创建订单、支付 等核心功能的正常使用

    总结下来:降级是通过暂时关闭某些非核心服务或者组件从而保护核心系统的可用性。

 

 

  12、超时控制

    在分布式环境下,服务响应慢可能比宕机危害更大,失败只是暂时的,但调用延迟会导致占用的资源得不到释放,在高并发情况下会造成整个系统崩溃

    如何合理设置超时时间?

      收集系统之间的调用日志,统计比如说 99% 的响应时间是怎样的,然后依据这个时间来指定超时时间

    超时处理策略?

      ①:服务超时释放资源,响应失败

        如数据库配置超时时间,超时则终止操作,释放资源

        jdbc配置:

          connectTimeout:表示等待和MySQL建立socket连接的超时时间,默认值0,表示不超时,单位毫秒,建议30000

          socketTimeout:表示客户端和MySQL建立socket后,读写socket时的等待超时时间

      ②:由于网络波动、节点异常的原因导致的请求超时,可以采用服务降级的方式,为请求提供兜底的数据响应,避免用户界面处于长时间停顿

 

    

 

 

  高可用设计理论:

    CAP:Consistency、Availability、Partition tolerance,此理论人尽皆知,最终会在CP和AP中权衡,找到满足BASE(Basically Available、Soft state、Eventually consistent)的平衡结果

 

  高可用设计要素:

    冗余:确保对系统操作至关重要的任何元素都有一个额外的冗余组件,这些组件可以在发生故障时接管。

    监控:从正在运行的系统中收集数据,并检测组件何时发生故障或停止响应。

    故障转移:一种手动或自动机制。如果监控显示活动组件发生故障,该机制可以从当前活动的组件切换到冗余组件。

  上述三要素逻辑也很清晰:要实现高可用,不管是否存在状态,要先有冗余或备份;当真正出现故障的时候,要有监控手段监控到故障发生;故障发生后,可以通过故障转移组件快速转移到之前的冗余组件中,保证服务不中断。

 

   

  高可用方案设计需要从哪些角度讨论和思考?

    首先,应用侧、支撑侧、运维侧的设计方式方法不同。

    应用侧高可用除了可以通过上述提到的冗余、集群、负载均衡等做到快速的故障转移,还包括熔断、限流、容错、降级、应急等保障手段,框架组件的超时及重试策略、异步调用、幂等性设计来补充。

    支撑侧(或称基础设施平台)需要一整套高可用相关的监控指标,满足故障的提前预警、快速报警、可视化监控和分析。常见指标包括请求量、请求错误率、平均延时、HTTP状态,以及系统资源消耗相关指标等。

    运维侧中关键一点是DevOps,自动化发布、灰度发布、优雅发布、版本控制、健康检查等能力,可以在业务发生故障前和发生故障时,帮助应用最大程度减小服务不可用时长。

 

 

 

 



 

 

END.

posted @ 2021-05-13 11:34  杨岂  阅读(4783)  评论(0编辑  收藏  举报