分布式系统中一次操作由多个服务协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。
分布式事务一般满足CAP原则
CAP 是 Consistency、Availability、Partition tolerance 三个单词的缩写,分别表示一致性、可用性、分区容忍性。
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)的缩写。
在分布式系统中,CAP理论是指导思维,而BASE理论是CAP理论中AP的延伸,是对 CAP 中的一致性和可用性进行一个权衡的结果,核心思想是:即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。
TCC 的全称是:Try
、Confirm
、Cancel
。
这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写逻辑来实现回滚和补偿,会造成巨大的补偿代码量。
或者基于本地消息表
实现流程:
这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
XA是把所有数据执行成功的通知发送给协调器(第一阶段),协调器在执行commit(第二阶段),并且数据源还要保证XA协议
TCC是第一阶段就把事务commit了(try接口),TCC的第二阶段是一个确认(Confirm)的阶段,也就是说只需要调用各个子系统里的confirm逻辑,所以在执行confirm逻辑的时候,并不会持有数据库的锁,所以不会产生性能问题。
大多数情况下,多线程处理可以提高并发性能,但如果对共享资源的处理不当,严重的锁竞争也会导致性能的下降。面对这种情况,有些场景采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,串行无锁和数据结构无锁。
无锁串行最简单的实现方式可能就是单线程模型了,如 redis/Nginx 都采用了这种方式。在网络编程模型中,常规的方式是主线程负责处理 I/O 事件,并将读到的数据压入队列,工作线程则从队列中取出数据进行处理,这种半同步/半异步模型需要对队列进行加锁,如下图所示:
上图的模式可以改成无锁串行的形式,当 MainReactor accept 一个新连接之后从众多的 SubReactor 选取一个进行注册,通过创建一个 Channel 与 I/O 线程进行绑定,此后该连接的读写都在同一个线程执行,无需进行同步。
利用硬件支持的原子操作可以实现无锁的数据结构,很多语言都提供 CAS 原子操作(如 go 中的 atomic 包和 C++11 中的 atomic 库),可以用于实现无锁队列。
当将数据写入文件、发送到网络、写入到存储时通常需要序列化(serialization)技术,从其读取时需要进行反序列化(deserialization),又称编码(encode)和解码(decode)。序列化作为传输数据的表示形式,与网络框架和通信协议是解耦的。如网络框架 taf 支持 jce、json 和自定义序列化,HTTP 协议支持 XML、JSON 和流媒体传输等。
序列化的方式很多,作为数据传输和存储的基础,如何选择合适的序列化方式尤其重要。
池化恐怕是最常用的一种技术了,其本质就是通过创建池子来提高对象复用,减少重复创建、销毁的开销。常用的池化技术有内存池、线程池、连接池、对象池等。
如果一个任务需要处理多个子任务,可以将没有依赖关系的子任务并发化,这种场景在后台开发很常见。如一个请求需要查询 3 个数据,分别耗时 T1、T2、T3,如果串行调用总耗时 T=T1+T2+T3。对三个任务执行并发,总耗时 T=max(T1,T 2,T3)。同理,写操作也如此。对于同种请求,还可以同时进行批量合并,减少 RPC 调用次数。
冗余请求指的是同时向后端服务发送多个同样的请求,谁响应快就是使用谁,其他的则丢弃。这种策略缩短了客户端的等待时间,但也使整个系统调用量猛增,一般适用于初始化或者请求少的场景。公司 WNS 的跑马模块其实就是这种机制,跑马模块为了快速建立长连接同时向后台多个 ip/port 发起请求,谁快就用谁,这在弱网的移动设备上特别有用,如果使用等待超时再重试的机制,无疑将大大增加用户的等待时间。
对于处理耗时的任务,如果采用同步等待的方式,会严重降低系统的吞吐量,可以通过异步化进行解决。异步在不同层面概念是有一些差异的,在这里我们不讨论异步 I/O。
在进行一个耗时的 RPC 调用或者任务处理时,常用的异步化方式如下:
Callback:异步回调通过注册一个回调函数,然后发起异步任务,当任务执行完毕时会回调用户注册的回调函数,从而减少调用端等待时间。这种方式会造成代码分散难以维护,定位问题也相对困难。
Future:当用户提交一个任务时会立刻先返回一个 Future,然后任务异步执行,后续可以通过 Future 获取执行结果。对 1.4.1 中请求并发,我们可以使用 Future 实现,伪代码如下:
//异步并发任务
Future<Response> f1 = Executor.submit(query1);
Future<Response> f2 = Executor.submit(query2);
Future<Response> f3 = Executor.submit(query3);
//处理其他事情
doSomething();
//获取结果
Response res1 = f1.getResult();
Response res2 = f2.getResult();
Response res3 = f3.getResult();
(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();
});
}
一个业务流程往往伴随着调用链路长、后置依赖多等特点,这会同时降低系统的可用性和并发处理能力。可以采用对非关键依赖进行异步化解决。如企鹅电竞开播服务,除了开播写节目存储以外,还需要将节目信息同步到神盾推荐平台、App 首页和二级页等。由于同步到外部都不是开播的关键逻辑且对一致性要求不是很高,可以对这些后置的同步操作进行异步化,写完存储即向 App 返回响应,如下图所示:
从单核 CPU 到分布式系统,从前端到后台,缓存无处不在。古有朱元璋“缓称王”而终得天下,今有不论是芯片制造商还是互联网公司都同样采取了“缓称王”(缓存称王)的政策才能占据一席之地。缓存是原始数据的一个复制集,其本质就是空间换时间,主要是为了解决高并发读。
分片即将一个较大的部分分成多个较小的部分,在这里我们分为数据分片和任务分片。对于数据分片,在本文将不同系统的拆分技术术语(如 region、shard、vnode、partition)等统称为分片。分片可以说是一箭三雕的技术,将一个大数据集分散在更多节点上,单点的读写负载随之也分散到了多个节点上,同时还提高了扩展性和可用性。
数据分片,小到编程语言标准库里的集合,大到分布式中间件,无所不在。如我曾经写过一个线程安全的容器以放置各种对象时,为了减少锁争用,对容器进行了分段,每个分段一个锁,按照哈希或者取模将对象放置到某个分段中,如 Java 中的 ConcurrentHashMap 也采取了分段的机制。分布式消息中间件 Kafka 中对 topic 也分成了多个 partition,每个 partition 互相独立可以比并发读写。
任何一个系统,从单核 CPU 到分布式,从前端到后台,要实现各式各样的功能和逻辑,只有读和写两种操作。而每个系统的业务特性可能都不一样,有的侧重读、有的侧重写,有的两者兼备,本节主要探讨在不同业务场景下存储读写的一些方法论。
在系统应用中,不是所有的任务和请求必须实时处理,很多时候数据也不需要强一致性而只需保持最终一致性,有时候我们也不需要知道系统模块间的依赖,在这些场景下队列技术大有可为。
在慢SQL的优化过程中,可以从以下五个角度去进行思考优化:SQL优化、资源占用、业务改造、数据减少、源头替换。
SQL语句的优化方式主要是通过选择合适的索引、优化查询语句、避免全表扫描等提高查询效率,减少慢SQL的出现
in执行流程:查询子查询的表且内外表有关联时,先执行内层表的子查询,然后将内表和外表做一个笛卡尔积,然后按照条件进行筛选,得到结果集。所以相对内表比较小的时候,in的速度较快
exists执行流程:指定一个子查询,检测行的存在。遍历循环外表,然后看外表中的记录有没有和内表的数据一样的,匹配上就将结果放入结果集中
在高并发、高流量下,数据库所在机器的负载load过高也会导致SQL整体执行时间过长,这时可能需要从机器和实例的分配,分布式部署,分库分表,读写分离等角度进行优化
Mysql并不是任何的查询场景都是适合的,如需要支持全模糊搜索时,全模糊的like是无法走到索引的。同时结合数据本身的生命周期,对于热点数据,可以考虑存储到缓存解决。因此针对不适合mysql数据源的情况,我们需要替代新的存储介质
QL本身的性能已经到达极限了,但是耗时仍然很长,可能由于数据量或索引数据都比较大了。因此需要从数据量级减少的角度去处理
第 1 层:客户端层 -> 反向代理层 的负载均衡。通过 DNS 轮询
第 2 层:反向代理层 -> Web 层 的负载均衡。通过 Nginx 的负载均衡模块
第 3 层:Web 层 -> 业务服务层 的负载均衡。通过服务治理框架的负载均衡模块
第 4 层:业务服务层 -> 数据存储层 的负载均衡。通过数据的水平分布,数据均匀了,理论上请求也会均匀。比如通过买家ID分片类似
限制到达系统的并发请求数,使得系统能够正常的处理 部分 用户的请求,来保证系统的稳定性。
最简单粗暴的一种算法,比如说一个接口一分钟内的请求不能超过60次
应用场景:短信限流,比如一个用户一分钟内一条短信,可以使用计数器限流
问题:临界问题,两个时间窗口交界处,可能会执行任务,近似于通知执行两倍量的任务
在固定窗口划分多个小窗口,分别在小时间窗口里记录访问次数,随着时间推动窗口滑动并删除过期窗口,即去除之前的访问次数,最终统计所有小窗口的可访问总数。
demo:将1min分为4个小窗口,每个小窗口能处理25个请求。通过虚线表示窗口大小(当前窗口大小为2,所以能处理50个请求)。同时窗口可以随时间滑动,比如15s后,窗口滑动到(15s-45s)这个范围内,然后在新的窗口里进行统计
当滑动窗口的小时间窗口划分越多,那么滑动窗口的滑动就越平滑,限流的统计就更精确。
内部维护一个容器,以恒定的速度出水,不管上游的速度多快。请求到达时直接放入到漏桶中,如果容量达到上限(阈值),则进行丢弃(执行限流策略)。
漏桶算法无法短时间处理突发流量,是一种恒定的限流算法。
令牌桶是网络流量整形(Traffic Shaping)和速度限制(Rate Limting)中最常使用的算法,对于每个请求,都需要从令牌桶中获得一个令牌,如果没有令牌则触发限流策略。
系统以一个恒定的速度往固定容量的容器放入令牌,如果有请求,则需要从令牌桶中取出令牌才能放行。
由于令牌桶有固定大小,当请求速度<生成令牌速度,令牌桶会被填满,因此令牌桶可以处理突发流量。
argparse
模块来解析命令行参数,包括文件路径、行数、是否实时监视等选项。open
函数打开用户指定的文件。我会使用 with
语句来确保在使用完文件后正确关闭它。file.seek()
方法将文件指针定位到文件末尾。这样,我就可以从末尾开始逐行读取。time.sleep()
等方法来实现定期检查,并在发现新行时输出到终端。如果是大文件直接读取inode文件内容
定位到末尾的块: 文件的 inode 包含了文件数据块的指针。你可以首先通过 inode 获取文件的大小,然后确定需要读取的块数量。这可以帮助你跳过文件的开头部分。
常见的容错模式主要包含以下几种方式
由于爆炸性的流量冲击,对一些服务进行有策略的放弃,以此缓解系统压力,保证目前主要业务的正常运行。它主要是针对非正常情况下的应急服务措施:当此时一些业务服务无法执行时,给出一个统一的返回结果。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
在学习服务熔断时,有必要区分下如下几个相关的概念。
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务,如果整个链路上某个微服务的调用响应式过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”
“断路器”本身是一种开关装置,当某个服务单元发生故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延。乃至雪崩。
熔断机制是应对雪崩效应的一种微服务链路保护机制,当整个链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。
Hystrix是一个用于分布式系统的延迟和容错的开源库。在分布式系统里,许多依赖不可避免的调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个服务失败,避免级联故障,以提高分布式系统的弹性。
它有两个基本状态(close和open)和一个基本trip动作:
基本的断路器模式下,保证了断路器在open状态时,保护supplier不会被调用, 但我们还需要额外的措施可以在supplier恢复服务后,可以重置断路器。一种可行的办法是断路器定期探测supplier的服务是否恢复, 一但恢复, 就将状态设置成close。断路器进行重试时的状态为半开(half-open)状态。