大型互联网应用系统使用技术方案和手段

用户头像
关注
发布于: 2020 年 06 月 28 日



一、单体架构性能极致:



1.数据库  :池化技术

                大量查询,主从分离

                写入数据量大,  分库分表(发号器设计)

                 高并发下,引入nosql



2.数据库达到瓶颈后,引入缓存

缓存:      CDN加速静态资源

                热点数据,做本地应用缓存

                本地应用与数据之间加分布式缓存



3.短暂高并发写场景应对(秒杀),引入消息队列

        削峰填谷

        异步

        解耦

        确保消息被消费一次

        降低消息队列系统中消息的延迟



4.负载均衡(代理类负载均衡模式)

   1.LVS( 四层负载) 适合在入口处承担大流量的请求分发,而 Nginx ( 七层负载)要部署在业务服务器之前做更细维度的请求分发 

   2. QPS 在十万以内,可以考虑不引入 LVS 而直接使用 Nginx 作为唯一的负载均衡服务器



二、分布式演化(依赖资源可扩展性出现瓶颈;一体化架构带来研发成本、部署成本难以接受后,进行演化)

                

       1.服务拆分和架构改造

              1.1 服务拆分原则:做到单一服务内部功能的高内聚和低耦合;

                                      关注服务拆分的粒度,先粗略拆分再逐渐细化;

                                      拆分的过程,要尽量避免影响产品的日常功能迭代

                                                    先剥离比较独立的边界服务(非核心的比如短信、位置服务)

                                                    梳理服务依赖关系,优先剥离被依赖的服务

                                      服务接口的定义具备可扩展性(由本地调用变成了跨网络通信,一定要考虑扩展性)

              1.2 服务化后带来什么问题:

                        服务调用: 同一进程的方法调用变成跨进程的网络调用,性能降低。  解决手段:高效RPC框架

                                             需要清楚被调用服务信息。   解决手段:服务注册中心

                        服务治理: 被依赖服务出现性能问题蔓延,导致系统故障。 解决方案:熔断、降级、限流、超时控制等

                                          调用链路上涉及多个服务,如何确定问题源头。 解决方案:分布式追踪工具

       2.设计RPC框架

                2.1 选择合适的网络模型

                         常见IO模型:  同步阻塞I/O、同步非阻塞I/O、同步多路I/O复用、信号驱动I/O、异步I/O

                          选择IO模型:  同步多路复用I/O (linux中的select、epoll, java中的netty)

                2.2有针对性地调整网络参数优化网络传输性能

                          调优参数:开启tcp_nodelay. (关闭nagle's算法)

                                           发送缓冲区和接收缓冲区大小

                                            客户端连接请求缓冲队列的大小(back log)

                2.3选择合适的序列化方式,以提升封包、解包的性能

                          性能: 时间上(序列化和反序列化速度)、空间上(序列化后二进制串大小,影响传输效率)

                          是否可以跨语言、跨平台

                          可扩展性

                        JSON、Thrift、Protobuf

       3.设计注册中心(常见的注册中心:zookeeper、 etcd、Nacos、eureka

            3.1服务优雅关闭设计

                  先从注册中心服务列表剔除服务,再看是否有流量进入,如果没有,再关闭服务。

            3.2 心跳机制设计( 服务状态管理)

                   服务故障感知

            3.3 增加服务自动摘除的保护机制

                   防止网络抖动,导致摘除所有服务实例

            3.4 防止通知风暴

                    通知消息量达到一定阈值,停止变更通知;

                   只变更一个节点的时候,只通知这个节点的变更信息;

                   变更节点的时候,只通知订阅这个节点的服务

       4.分布式trace设计

                1.对代码要无侵入,你可以使用切面编程的方式来解决;

                2.采用静态代理和日志采样的方式,来尽量减少追踪日志对于系统性能的影响

                3. 提供一个开关,方便在线上随时将日志打印关闭

                4. 采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系, traceId 串起单次请求,spanId 记录每一次 RPC 调用

      5.搭建服务端监控体系



              1.监控指标:、耗时、请求量和错误数是三种最通用的监控指标,不同的组件还有一些特殊的监控指标;

                    





            2.指标数据采集:Agent、埋点和日志是三种最常见的数据采集方式;

                   agent代理

                   客户端埋点

                   日志数据采集

           3.数据处理

             



           4.报表展示      

                     访问趋势报表用来展示服务的整体运行情况, 这类报表接入的是 Web 服务器,和应用服务器的访问日志,展示了服务整体的访问量、响应时间情况、错误数量、带宽                            等信息  

                   性能报表用来分析资源或者依赖的服务是否出现问题,  这类报表对接的是资源和依赖服务的埋点数据,展示了被埋点资源的访问量和响应时间情况

                   资源报表用来追查资源问题的根本原因, 这类报表主要对接的是,使用 Agent 采集的资源的运行情况数据。

三、微服务架构

        1.API网关设计和使用( 在 API 网关中植入服务熔断、降级和流控等服务治理的策略。

            1.1API网关设计考虑点:

              性能考虑:选择多路复用IO模型

              扩展性:责任链模式(热插拔)

              并行处理能力:线程池隔离

                        针对不同的服务,采用不同的线程池

                        在线程池内部针对不同的接口设置配额

           1.2API网关使用:

                  流量网关: 先将 API 网关从 Web 层中独立出来,将协议转换、限流、黑白名单等事情挪到 API 网关中来处理,形成独立的入口网关层

                  业务网关: 再独立出一组网关专门做服务聚合、超时控制方面的事情(这一层可以用服务层代替, 把原来的服务层分成服务聚合层和服务原子层)

                  出口网关: 出口网关主要是为调用第三方服务提供统一的出口,在其中可以对调用外部的 API 做统一的认证、授权、审计以及访问控制;

           1.3常用网关:

                   Kong是在 Nginx 中运行的 Lua 程序。得益于 Nginx 的性能优势,Kong 相比于其它的开源 API 网关来说,性能方面是最好的。由于大中型公司对于 Nginx 运维能力                            都比较强,所以选择 Kong 作为 API 网关,无论是在性能还是在运维的把控力上,都是比较好的选择;

                 Zuul是 Spring Cloud 全家桶中的成员,如果你已经使用了 Spring Cloud 中的其他组件,那么也可以考虑使用 Zuul 和它们无缝集成。不过,Zuul1 因为采用同步阻塞                           模型,所以在性能上并不是很高效,而 Zuul2 推出时间不长,难免会有坑。但是 Zuul 的代码简单易懂,可以很好地把控,并且你的系统的量级很可能达不到                                   Netfix 这样的级别,所以对于 Java 技术栈的团队,使用 Zuul 也是一个不错的选择;

                 Tyk是一种 Go 语言实现的轻量级 API 网关,有着丰富的插件资源,对于 Go 语言栈的团队来说,也是一种不错的选择。

        2.配置中心设计

            开源配置中心:携程开源的 Apollo、百度开源的 Disconf、360 开源的 QConf、Spring Cloud 的组件 Spring Cloud Config

                  2.1 配置中心存储设计

                            Disconf、Apollo 使用的是 MySQL;QConf 使用的是 ZooKeeper, 微博的配置中心使用 Redis 来存储信息,而美图用的是 Etcd

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

                             支持存储全局配置、机房配置和节点配置。其中,节点配置优先级高于机房配置,机房配置优先级高于全局配置。也就是说,我们会优先读取节点的配置,如果                              节点配置不存在,再读取机房配置,最后读取全局配置

                           

/confs/global/{env}/{project}/{service}/{version}/{module}/{key} //全局配置

/confs/regions/{env}/{project}/{service}/{version}/{region}/{module}/{key}   //机房配置

/confs/nodes/{env}/{project}/{service}/{version}/{region}/{node}/{module}/{key}     //节点配置



                 2.2 变更推送如何实现

                          1. 应用程序向配置中心客户端注册一个监听器,配置中心的客户端,定期地(比如 1 分钟)查询所需要的配置是否有变化,如果有变化则通知触发监听器,让应                                      用程序得到变更通知

                          2. 给配置中心的每一个配置项多存储一个根据配置项计算出来的 MD5 值, 配置项一旦变化,这个 MD5 值也会随之改变。配置中心客户端在获取到配置的同时,                                 也会获取到配置的 MD5 值,并且存储起来

                         3. 轮询查询的时候,需要先确认存储的 MD5 值和配置中心的 MD5 是不是一致的。如果不一致,这就说明配置中心里存储的配置项有变化,然后才会从配置中心                                  拉取最新的配置

 



              2.3 如何保证配置中心高可用

                        让配置中心“旁路化”: 在配置中心的客户端上,增加两级缓存:第一级缓存是内存的缓存;另外一级缓存是文件的缓存

                        配置中心客户端在获取到配置信息后,会同时把配置信息同步地写入到内存缓存,并且异步地写入到文件缓存中

                        内存缓存的作用是降低客户端和配置中心的交互频率,提升配置获取的性能;

                        文件的缓存的作用就是灾备,当应用程序重启时,一旦配置中心发生故障,那么应用程序就会优先使用文件中的配置

       3.服务治理方案

                3.1 由于依赖的资源或者服务不可用,最终导致整体服务宕机( 熔断)

                           雪崩”: 局部故障最终导致全局故障

                    断路器模式,在这种模式下,服务调用方为每一个调用的服务维护一个有限状态机,在这个状态机中会有三种状态:关闭(调用远程服务)、半打开(尝试调用远程服                       务)和打开(返回错误)

                

                     当调用失败的次数累积到一定的阈值时,熔断状态从关闭态切换到打开态。一般在实现时,如果调用成功一次,就会重置调用失败次数。



                     当熔断处于打开状态时,我们会启动一个超时计时器,当计时器超时后,状态切换到半打开态。你也可以通过设置一个定时器,定期地探测服务是否恢复



                    在熔断处于半打开状态时,请求可以达到后端服务,如果累计一定的成功次数后,状态切换到关闭态;如果出现调用失败的情况,则切换到打开态

                  

                redis熔断器:

                          在系统初始化的时候,我们定义了一个定时器,当熔断器处于 Open 状态时,定期地检测 Redis 组件是否可用

                        



new Timer("RedisPort-Recover", true).scheduleAtFixedRate(new TimerTask() {

    @Override

    public void run() {

        if (breaker.isOpen()) {

            Jedis jedis = null;

            try {

                jedis = connPool.getResource();

                jedis.ping(); //验证redis是否可用

                successCount.set(0); //重置连续成功的计数

                breaker.setHalfOpen(); //设置为半打开态

            } catch (Exception ignored) {

            } finally {

                if (jedis != null) {

                    jedis.close();

                }

            }

        }

    }

}, 0, recoverInterval); //初始化定时器定期检测redis是否可用



             Redis 客户端操作 Redis 中的数据时,我们会在其中加入熔断器的逻辑:

              



if (breaker.isOpen()) {

    return null;  // 断路器打开则直接返回空值

}



K value = null;

Jedis jedis = null;



try {

     jedis = connPool.getResource();

     value = callback.call(jedis);

     if(breaker.isHalfOpen()) { //如果是半打开状态

          if(successCount.incrementAndGet() >= SUCCESS_THRESHOLD) {//成功次数超过阈值

                failCount.set(0);  //清空失败数

                breaker.setClose(); //设置为关闭态

          }

     }

     return value;

} catch (JedisException je) {

     if(breaker.isClose()){  //如果是关闭态

         if(failCount.incrementAndGet() >= FAILS_THRESHOLD){ //失败次数超过阈值

            breaker.setOpen();  //设置为打开态

         }

     } else if(breaker.isHalfOpen()) {  //如果是半打开态

         breaker.setOpen();    //直接设置为打开态

     }

     throw  je;

} finally {

     if (jedis != null) {

           jedis.close();

     }

}



                3.2 当有超过系统承载能力的流量到来时,系统不堪重负,从而出现拒绝服务的情况(降级)

                       开关降级:代码中预先埋设一些“开关”,用来控制服务调用的返回值。比方说,开关关闭的时候正常调用远程服务,开关打开时则执行降级的策略。这些开关的值                                    可以存 储在配置中心中,当系统出现问题需要降级时,只需要通过配置中心动态更改开关的值,就可以实现不重启服务快速地降级远程服务了

                         

                        实现策略主要有返回降级数据、降频和异步三种方案

                        首先要区分哪些是核心服务,哪些是非核心服务, 只能针对非核心服务来做降级处理,针对具体的业务,制定不同的降级策略:

                              降级数据:针对读取数据的场景,我们一般采用的策略是直接返回降级数据。比如,如果数据库的压力比较大,我们在降级的时候,可以考虑只读取缓存的数                                                   据,而不再读 取数据库中的数据;如果非核心接口出现问题,可以直接返回服务繁忙或者返回固定的降级数据

                              降频:        对于一些轮询查询数据的场景,比如每隔 30 秒轮询获取未读数,可以降低获取数据的频率(将获取频率下降到 10 分钟一次)

                              异步 :       而对于写数据的场景,一般会考虑把同步写转换成异步写,这样可以牺牲一些数据一致性和实效性来保证系统的可用性



                3.3 系统的峰值流量会超过了预估的峰值,对于核心服务也产生了比较大的影响(限流)



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

                     基于时间窗口维度的算法有固定窗口算法和滑动窗口算法,两者虽然能一定程度上实现限流的目的,但是都无法让流量变得更平滑;

                    令牌桶算法和漏桶算法则能够塑形流量,让流量更加平滑,但是令牌桶算法能够应对一定的突发流量,所以在实际项目中应用更多。



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

                              对系统每分钟处理多少请求做限制

                              针对单个接口设置每分钟请求流量的限制

                              限制单个 IP、用户 ID 或者设备 ID 在一段时间内发送请求的数量

                              对于服务于多个第三方应用的开放平台来说,每一个第三方应用对于平台方来说都有一个唯一的 appkey 来标识,可以限制单个 appkey 的访问接口的速率

       

                        固定窗口与算法: 首先要启动一个定时器定期重置计数,比如限制每秒钟访问次数。

                                

private AtomicInteger counter;

ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();

timer.scheduleAtFixedRate(new Runnable(){

    @Override

    public void run() {

        counter.set(0);

    }

}, 0, 1, TimeUnit.SECONDS);



public boolena isRateLimit() {

  return counter.incrementAndGet() >= allowedLimit;

}



                          缺陷: 无法限制短时间之内的集中流量

                         



                    滑动窗口的算法: 将时间的窗口划分为多个小窗口,每个小窗口中都有单独的请求计数;

                              将 1s 的时间窗口划分为 5 份,每一份就是 200ms;那么当在 1s 和 1.2s 之间来了一次新的请求时,我们就需要统计之前的一秒钟内的请求量,也就是 0.2s~                                     1.2s 这个区间的总请求量,如果请求量超过了限流阈值那么就执行限流策略

                          



 

                  漏桶算法与令牌筒算法漏桶算法:

                         漏桶算法的原理很简单,它就像在流量产生端和接收端之间增加一个漏桶,流量会进入和暂存到漏桶里面,而漏桶的出口处会按照一个固定的速率将流量漏出到接                            收端(也就是服务接口)。 如果流入的流量在某一段时间内大增,超过了漏桶的承受极限,那么多余的流量就会触发限流策略,被拒绝服务

                                





                     令牌桶算法的基本算法:

                                 如果我们需要在一秒内限制访问次数为 N 次,那么就每隔 1/N 的时间,往桶内放入一个令牌;

                                 在处理请求之前先要从桶中获得一个令牌,如果桶中已经没有了令牌,那么就需要等待新的令牌或者直接拒绝服务;

                                 桶中的令牌总数也要有一个限制,如果超过了限制就不能向桶中再增加新的令牌了。这样可以限制令牌的总数,一定程度上可以避免瞬时流量高峰的问题

                                       





                      漏桶算法在面对突发流量的时候,采用的解决方式是缓存在漏桶中, 这样流量的响应时间就会增长,这就与互联网业务低延迟的要求不符;

                      而令牌桶算法可以在令牌中暂存一定量的令牌,能够应对一定的突发流量, Guava 中的限流方案就是使用令牌桶算法来实现的(优先)



4.ServiceMesh ( 让服务治理的策略在多语言之间复用

              1.什么是ServiceMesh

               Service Mesh 主要处理服务之间的通信,它的主要实现形式就是在应用程序同主机上部署一个代理程序

               把业务代码和服务治理的策略隔离开,将服务治理策略下沉,让它成为独立的基础模块

               sidecar 的植入方式目前主要有两种实现方式,一种是使用 iptables 实现流量的劫持;另一种是通过轻量级客户端来实现流量转发

               服务之间的通信也从之前的客户端和服务端直连,变成了下面这种形式

               



               2. Istio

                        它将组件分为数据平面和控制平面,数据平面就是我提到的 Sidecar(Istio 使用Envoy作为 Sidecar 的实现)。控制平面主要负责服务治理策略的执行,在 Istio                              中,主要分为 Mixer、Pilot 和 Istio-auth 三部分

                        





 四.多机房部署规划

        1. 跨机房的数据传输

          1.1 同地双机房之间的专线延迟一般在 1ms~3ms。

          1. 2 国内异地双机房之间的专线延迟会在 50ms 之内

          1. 3 如果业务是国际化的服务,需要部署跨国的双机房。 依据各大云厂商的数据来看,从国内想要访问部署在美国西海岸的服务,这个延迟会在 100ms~200ms 左右

        2. 同城双活

            核心思想是尽量避免跨机房的调用。同城多机房方案可以允许有跨机房数据写入的发生,但是数据的读取和服务的调用应该尽量保证在同一个机房中

                  首先,数据库的主库可以部署在一个机房中,比如部署在 A 机房中,那么 A 和 B 机房数据都会被写入到 A 机房中。然后,在 A、B 两个机房中各部署一个从库,通过                              主从复制的方式,从主库中同步数据,这样双机房的查询请求可以查询本机房的从库。一旦 A 机房发生故障,可以通过主从切换的方式将 B 机房的从库提升为主                              库,达到容灾的目的

                  其次,不同机房的 RPC 服务会向注册中心注册不同的服务组,而不同机房的 RPC 客户端,也就是 Web 服务,只订阅同机房的 RPC 服务组,这样就可以实现 RPC 调                              用尽量发生在本机房内,避免跨机房的 RPC 调用 

        3. 异地多活

                 避免跨机房同步的数据写入和读取,而是采取异步的方式,将数据从一个机房同步到另一个机房

                 在数据写入时,要保证只写本机房的数据存储服务再采取数据同步的方案,将数据同步到异地机房中

                    数据同步的方案有两种:

                         基于存储系统的主从复制,比如 MySQL 和 Redis。也就是在一个机房部署主库,在异地机房部署从库,两者同步主从复制实现数据的同步。

                         基于消息队列的方式。一个机房产生写入请求后,会写一条消息到消息队列,另一个机房的应用消费这条消息后再执行业务处理逻辑,写入到存储服务中

                   一般两种结合用:基于消息的方式,同步缓存的数据、HBase 数据等。然后基于存储,主从复制同步 MySQL、Redis 等数据



用户头像

关注

还未添加个人签名 2018.05.04 加入

还未添加个人简介

评论

发布
暂无评论
大型互联网应用系统使用技术方案和手段