警惕Dubbo因超时和重试引起的系统雪崩
- 超时时间一定要设置,要根据业务场景而定,设置太短容易引发重试,设置太长容易引发请求堆积。
- 超时设置过短的后果:Dubbo默认失败重试2次,所以假设有1000个并发,都超时了,则会重试2次,变成3000个请求,导致请求流量翻了3倍。
- 重试设置:写服务一般要考虑幂等性,所以失败后不进行重试。
- Dubbo重试的集中容错方案:
- failover(失败重试其他服务节点,通常用于读操作)。
- failfast(失败立即报错,通常用于幂等性的写操作)。
- failsafe(失败直接忽略,通常用于记录日志)。
- failback(失败定时重发,通产惯用于消息通知)。
- forking(并行调用多个节点,成功1个即返回,通常用于实时性要求高的操作)。
服务治理的三个基础要素
- 服务的动态注册与发现。
- 服务的扩容实现。
- 服务的升/降级处理。
- Dubbo的管理控制台中服务治理包含的功能:路由规则、动态配置、服务降级、访问控制、权重调节以及负载均衡等。
JVM性能调优的矛盾:吞吐量和低延迟
如果吞吐量优先,那么GC就必然会比较少进行垃圾回收,会达到一定程度才进行垃圾回收,相对的就需要花费更长的暂停时间来执行内存回收;反之,如果低延迟优先,则会频繁地执行垃圾回收,又会导致程序吞吐量的下降。
分布式调用跟踪系统的实现原理(依据Google的Dapper论文)
- Trace表示一次请求的完整调用链追踪。
- Span用于体现服务与服务之间的一次调用,所以多个服务之间调用,Span就能体现具体的依赖关系。
- 每一次请求都会分配一个全局唯一的TraceID,整个调用链中所有的Span都会获取到同一个TraceID。
- 一般采集的数据信息包括TraceID、SpanID和ParentSpanID、RpcContext中包含的数据信息、服务执行异常时的堆栈信息、以及Trace中各个Span过程的开始和结束时间,这些数据信息都是需要在Trace的上下文信息中进行传递的。
基于Dubbo实现分布式调用跟踪系统方案
- 目前主流的跟踪系统有:淘宝的EagleEye,嵌入HSF框架、Twitter的Zipkin,嵌入Finagle框架。
- Dubbo提供了专门用于拦截RPC请求的Filter接口,不过目前Dubbo的Filter并没有纳入Spring的IOC容器管理,所以配置可以查看文档或书pg26。
- 主要的类:由Invocation派生可以传递当前Trace上下文信息的RpcInvocation和临时状态记录器RpcContext
- 执行流程:
- 当服务调用方向服务提供方发起RPC请求时,Filter会对服务调用方法进行拦截,然后试图从ThreadLocal中获取当前线程的Trace上下文信息,如果不存在则说明是根调用,需要生成TraceID,然后将生成的TraceID、SpanID设置在Invocation中传递给服务提供方,接着执行前置数据上报(开始时间维度为Client Send Time)。当调用Invoker接口的invoke()方法执行完远程服务方法后,再执行后置数据收集上报(结束时间维度为Clinet Receive Time)。
- 当RPC请求到达服务提供方后,Filter会对其拦截,然后从Invocation中获取由服务调用方传递过来的Trace上下文信息,并将其存储到当前线程的ThreadLocal中,然后执行前置数据收集上报(开始时间维度为Server Receive Time)。当调用Invoker接口的invoke()方法执行完服务方法后,再执行后置数据收集上报(结束时间维度为Server Send Time),最后还要删除存储在ThreadLocal中当前线程的Trace上下文信息。
- 收集到的信息通过发送消息队列,再异步持久化到数据库中。数据量大的放Hbase,量不大的可以放mysql即可,不过需要定时清除。
- 当并发量太大时,全跟踪记录损耗较大,可以只对其中一些请求进行采样跟踪(比如1/1000,1000个请求才记录1个),采样率可以配置在配置中心动态更改。
大流量和高并发的常规应对手段
扩容、动静分离、缓存、服务降级和限流。
限流的常用算法和实践思路
- 目前主流的算法主要有三种:令牌桶算法、漏桶算法和计数器算法。
- 令牌桶算法:主要限制流量的流入速率,允许出现一定程度的突发流量。Nginx的限流模块就是使用的这种算法实现的。
- 每秒会有r个令牌按照固定速率放入桶中。
- 桶的容量是固定不变的,如果桶满了再放入令牌,则溢出。
- 若桶中的可用令牌不足,则改请求会被进行限流处理(被抛弃或缓存)。
- 漏桶算法:主要限制流量的流出速率,并且流出速率是固定不变的
- 可以以任意速率向桶中流入水滴。
- 桶的容量是固定不变的,如果桶满了则溢出。
- 按照固定的速率从桶中流出水滴。
- Google的Guava也实现了基于令牌桶算法那样的平均速率限流,RateLimiter抽象类。
- Nginx可以使用限流模块在接入层实现令桶牌算法限流,
- limit_zone 定义每个IP的session空间大小。
- limit_zeq_zone定义每个IP每秒允许发起的请求数。
- limit_conn 定义每个IP能够发起的并发连接数。
- limit_req 等待处理的请求队列数量。
- 生产环境中的商品抢购可以使用计数器算法,具体不同的sku限流规则配置在配置中心内,支持动态更改。可抢购次数的扣减操作,既可以用redis,也可以用JVM。如果是集群并且选择用JVM,则要根据总并发数量除以集群数量,得出单台机器的并发数。(比如总并发数5000,集群机器10台,则每台机器的并发为5000/10=500)。
ActiveMQ和RocketMQ的对比
- 两者都能用于分布式系统的解耦,ActiveMQ使用于企业级内部系统,RocketMQ使用大流量、高并发的互联网项目,所以还起到了流量削峰的作用。
- ActiveMQ定位用于企业级应用服务,所以使用于用户规模较小的场景。遵循JMS规范,由JMS Provider、Provider和Consumer三者构成,其中JMS Provider负责消息路由和消息传递。主要支持的消息模型:点对点和发布/订阅两种模型。
- RocketMQ的基本流程参考pg63,由NameServer(注册中心)、Boker(消息服务端)、Producer和Consumer构成。具有的基本特点:
- 支持顺序消息。
- 支持事务消息。
- 支持集群(点对点)与广播(发布/订阅)模式。
- 亿级消息堆积能力。
- 完善的分布式特性。
- 支持Push与Pull两种消息订阅模式。
用Zookeeper实现配置中心
- Zookeeper提供了配置管理(发布/订阅)、分布式协调/通知、分布式锁以及命名等服务,利用Zookeeper提供的一系列接口,能够实现一致性、Leader选举以及分布式配置管理平台等功能。
- Zookeeper的数据模型中,每个子节点成为Znode,每个Znode都有一个全局唯一的路径作为标识,进行数据的读写操作。每个Znode都会维护一个版本号,版本号会随着每次数据的变更自动递增,这个特性可以避免并发带来的不一致性问题。
- 实现方案:将配置信息发布到Znode目录上后,有客户端负责信息订阅,利用长连接进行Watch,一旦配置信息发生变更,实现Watchr接口的process()方法将会感知到相应的时间,并进行相应的处理。Zookeeper采用的是类似于UNIX的文件系统目录,每个Znode都可以存储数据信息,并且还可以包含子节点(瞬时节点除外)。
堆外内存off-heap的使用
NIO网络通信框架Netty和消息中间件RocketMQ等都使用了ByteBuffer.allocateDirect()方法来操作实际的物理内存资源。
Redis集群
- 吞吐量:Redis单点TPS达到8万/秒,QPS达到10万/秒。
- 集群原理:Redis集群节点(主节点)中一共包含16384个Slot,不同的Redis节点内各自维护其中一小段Slot用于存储不同区间的数据。当根据key进行读/写操作时,redis会根据crc16的算法得出一个结果然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
- 集群投票机制:所有master参与,如果半数以上master节点与master节点通信超时,认为当前master节点挂掉。每一个节点都存有这个集群所有主节点以及从节点的信息。节点与节点之间通过ping-pong来进行心跳连接。
- 集群不可用的情况:
- 集群中任意master挂掉,且当前master没有slave。
- 集群中超过半数以上master挂掉。
抢购商品高并发读需求
对于一件抢购商品的流量来说,因为key是同一个,所以流量必然会都引入到同一个redis缓存节点中,这时就容易出现单点故障。因此有下面两种解决方式:
1. 在每个master节点都挂slave从节点,当主节点挂了可以自动顶上。
2. 多级Cache方案,多用LocalCache来过滤掉一部分流量。
- 本地缓存一般只缓存一些热点商品数据,缓存内容一般是商品详情和商品库存。
- 本地缓存跟分布式缓存的同步一般有两种方式:一种是定时主动拉取更新策略。这种会存在一定时间的不一致,要视业务情况而定,例如库存,暂时的不一致导致超卖,单到真正下单的时候还会再进行库存的判断,所以影响较小,可以接受。这种方式要注意关掉缓存的定时失效,防止当用户流量突然过大,都到分布式缓存中拉取数据;第二种方式是每次商品更新,都发布一个消息,订阅此消息的节点监听到后再更新本地缓存的内容。
实时热点自动发现方案
可以将交易系统产生的相关数据,以及在上游系统中埋点上报的相关数据异步写入日志系统中,然后通过实时热点自动发现平台对收集到的日志数据做调用次数统计和热点分析。数据符合热点条件后,就立即通知交易系统做好热点保护。
redis使用watch命令实现高并发抢购需求
- 一般高并发这里,不用悲观锁,会迅速增加系统资源;而使用队列,容易造成请求堆积,内存效果过快。所以一般使用乐观锁,可以用redis的watch命令实现。
- watch命令会监视给定的key,当exec时,如果监视的key从调用watch后发生过变化,则事务会失败。注意watch的可以是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令都会清除连接中的所有监视。
mysql主从切换过程中保证数据一致性方案
- 在Master的TPS较高的情况下,主从同步的延迟肯定是非常大的,因此为了避免数据库读写分离后应用层无法从Slave拉到实时数据,通常可以在写入Master之前也将数据落到缓存中。
- 在TPS不高的情况下,可以开启mysql5.5版本后开启的半同步复制功能。当事务提交到Master后,Master会等待Slave的回应,待Slave回应收到Binlog后,Master才会响应请求方已经完成事务。但是当Master实例宕机后,Slave成为新的Master时,主从数据库之间数据肯定还是会出现不一致,这时为了避免手动比对Binlog来确保主从数据的一致性,可以是用mysql5.6版本开始提供的GRID(全局事务ID)特性。由于新Master是之前的Slave,而宕机后的Master在重启后可以作为Slave存在,可以依靠GTID特性来保证主从之间数据的最终一致性。
mysql单表在500W数据量的读写性能
- 读会受到慢慢变慢,即使有索引。
- 写是顺序写,跟表数据一般没什么关系,主要是跟表的索引多少有关,所以还是差别不大。
对于分布式事务或者异构表的数据一致性方案:
- 分布式事务中,要求最终一致性即可,不用强一致性。
- 保障最终一致性的方案一般有监测补偿机制(线上监测补偿和线下监测补偿两种)和异步同步(跟Mysql的主从同步同理),以数据写入表A和表B为例:
- 线上监测补偿:当数据成功写入表A后,立即将消息1写入消息队列中。然后再继续将数据写入表B,写入成功后也马上将消息2写入消息队列中。然后有一个消费者监听这两个消息1和2,如果消费者消费到消息1后在指定时间范围内没有收到消息2,则认为数据已经产生不一致,需要执行补偿操作。不一致的窗口期相对较短。
- 线下监测补偿:当数据成功写入表A后,立即将消息1写入到log1中。然后再继续将数据写入表B,写入成功后也马上将消息2写入log2中。然后有一个Job程序不停地增量对比log1和log2,如果出现数据不一致的情况,则进行数据补偿。不一致的窗口期相对较长。
- 参照mysql的主从同步机制,利用Canal订阅mysql的Binlog日志。