分布式事务综述

理论知识 事务的四个特性:ACID Atomic 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成 Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义等约束不会被破坏 Isolation 隔离性:防止多个事务并发执行时由于交叉执行而导致数据的不一致 Durability 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失 分布式系统三个特性:CAP Consistency 一致性:集群执行某个操作后,所有副本节点的状态都相同,那么这样的系统就被认为具有强一致性 Available 可用性:集群一部分节点故障后,还能对外提供服务 Partition tolerance 分区容忍性:狭义上是集群节点之间是否能正常通信,更广义的是对通信的时限要求,系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况 对于互联网的场景来说,一般选择AP:因为现在集群规模越来越大,主机众多、部署分散,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9,即保证P和A,舍弃C。 分布式的BASE理论:柔性事务,对CAP的权衡 Basically Available 基本可用:系统在出现不可预知故障的时候,允许损失部分可用性,但这绝不等价于系统不可用 Soft state 软状态:允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性 Eventually consistent 最终一致性:经过一段时间的同步后,最终能够达到一个一致的状态 NewSQL的分布式事务: 以Spanner、TiDB为代表的NewSQL,在内部集群多节点间,实现了ACID的事务,即提供给用户的事务接口与普通本地事务无差别,但是在内部,一个事务是支持多个节点多条数据的写入,此时无法采用本地ACID的MVCC技术,而是会采用一套复杂的分布式MVCC来做到ACID。属于满足CP的系统,同时接近于满足A,称之为CP+HA。但是NewSQL和BASE的系统之间,性能上差异可能是巨大的。 跨库跨服务的分布式事务:这类分布式事务部分遵循 ACID 原子性:严格遵循 一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽 隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽 持久性:严格遵循 现有的分布式事务方案都无法做到强一致,但是有强弱之分:XA事务 > TCC > 二阶段消息 > SAGA(一般情况下)。具体为: XA:XA虽然不是强一致,但是XA的一致性是多种分布式事务中,一致性最好的,因为他处于不一致的状态时间很短,只有一部分分支开始commit,但还没有全部commit的这个时间窗口,数据是不一致的。因为数据库的commit操作耗时,通常是10ms内,因此不一致的窗口期很短。 TCC:理论上,TCC可以用XA来实现,例如Try-Prepare,Confirm-Commit,Cancel-Rollback。但绝大多数时候,TCC会在业务层自己实现Try|Confirm|Cancel,因此Confirm操作耗时,通常高于XA中的Commit,不一致的窗口时间比XA长 MSG:二阶段消息型事务在第一个操作完成后,在所有操作完成之前,这个时间窗口是不一致的,持续时长一般比前两者更久。 SAGA:SAGA的不一致窗口时长与消息接近,但是如果发生回滚,而子事务中正向操作修改的数据又会被用户看到,这部分数据就是错误数据,容易给用户带来较差的体验,因此一致性是最差的。 2PC 两阶段提交流程如下: 准备阶段:RM去开启事务并执行操作,但是不提交事务,此时相关资源都被锁定,第三方不能访问这些资源。返回操作结果给TM 提交阶段:若所有RM在准备阶段都操作成功,TM将发出请求让所有RM提交事务,否则发出请求让所有RM回滚 存在问题: 同步阻塞:从准备阶段的锁定资源,直到事务提交/回滚的过程中,资源都处于被RM锁定的状态,第三方访问会被阻塞。 单点故障:2PC的推进重度依赖TM,TM若发生故障,RM会被阻塞。 数据不一致:若提交阶段部分RM无法与TM正常通信,导致一部分子事务提交了,而发生异常的RM没提交,全局事务发生不一致。 3PC 3PC提交将2PC的准备阶段再次拆分,加入检查阶段,如果检查失败的话马上abort,减少了2PC在失败情况下白白浪费资源,并且引入RM的超时机制(2PC只有TM超时机制),如果在提交阶段TM超时,直接提交事务释放资源。3PC流程如下: 检查阶段:TM 询问 RM,是否具备执行事务的条件,RM 进行自身事务必要条件的检查 预提交阶段:TM 通知 RM 进行事务的预提交 提交阶段:TM 根据预提交阶段 RM 的反馈结果通知 RM 是否进行事务提交或是进行事务回滚 RM超时自动提交是因为此时已经进入了提交阶段,说明RM知道检查阶段已经成功(但是RM不知道预提交阶段是否成功),认为第三阶段很大概率也可以成功。但很明显,如果第二步存在RM失败了,即预提交失败,因此在第三步中,TM发出回滚请求,但此时又有另外一个RM超时并自动提交了事务(超时时间内收不到回滚请求),就会出现不一致。 ...

十月 28, 2024 · by NOSAE

字节RPC框架kitex源码阅读(二)

Note 基于kitex@v0.11.3 开篇 在上篇字节RPC框架kitex源码阅读(一)中,简单过了一遍从创建服务、监听端口、建立连接&派发、退出清理的流程,对于代码生成的回调如何在kitex内部得到调用也有了初步的认知。 这篇是(一)的续篇,深入分析remote.Server如何基于与客户端建立的连接做交互,包括传输、解码、编码等。 remote.ServerTransHandler 我们知道server.Server主要构建调用链、调用用户定义的回调。与远程传输有关的remote.Server提供了简单的几个接口方法给server.Server使用,相当于server.Server只需要关心调用链要怎么消费封装好的数据,不用管传输如何建立、数据如何封装: ...

十月 23, 2024 · by NOSAE

字节RPC框架kitex源码阅读(一)

Note 基于kitex@v0.11.3 开篇 随着分布式系统的发展,RPC(Remote Procedure Call,远程过程调用)已成为微服务架构中不可或缺的基础组件。RPC 通过让服务之间像调用本地方法一样发起远程调用,极大简化了跨进程、跨服务器的通信复杂度。对于开发者来说,选择一个性能稳定、易于使用的 RPC 框架至关重要。 ...

十月 22, 2024 · by NOSAE

clash规则配置

我一直在用的wmsxwd抑或是一元机场,这些厂商自带的代理规则都太少,对于没匹配上规则的网站,只能要么全部走代理或者全部直连,搞得我经常要手动切换。所以我需要一个相对更加全面的规则列表,让该走代理的网站走代理,该直连的直连!!! 无意中翻到clash-rules这个每日更新代理规则的仓库,便拿来用之。 使用方式如下: 去机场厂商那里拿到订阅链接,注意有的厂商不会直接给订阅链接而是托管链接,并且会给你一个网站自己去转换成订阅链接 创建一个.yaml文件(文件名随便),不同的客户端可能配置文件位置不同,比如我是放在~/.config/clash目录下 编辑文件内容,其中一些配置用原来默认的就行,比如我的是 mixed-port: 7890 allow-lan: true bind-address: '*' mode: rule log-level: info external-controller: '127.0.0.1:9090' 配置proxy-providers配置,顾名思义,配置代理 proxy-providers: CNIX: # 代理名称,自定义即可 type: http # 以远程下载的方式获取代理 url: "订阅链接" # 填写你的订阅链接 path: ./cnix.yaml # 从订阅链接下载存到本地的cnix.yaml文件 interval: 86400 # 更新间隔(s) 配置rule-providers,顾名思义,配置代理规则,同样以远程的方式获取,并定时更新,参考仓库的教程即可,比如加入一个名为google的远程规则(不知道为什么jsdelivr我经常访问不了,所以就直接不走这个cdn了): rule-providers: google: type: http behavior: domain url: "https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/google.txt" path: ./ruleset/google.yaml interval: 86400 配置proxy-groups,CNIX就是刚刚配置的proxy-providers,这里定义一个代理组去使用它 proxy-groups: - name: PROXY type: select use: - CNIX 配置rules,将规则和代理组关联起来。除了PROXY以外,DIRECT和REJECT可视为默认的代理组,即直连和拒绝,其中拒绝网站通过查看reject.txt就能看到,包含一些广告网站或者人家的内部网站、垃圾网站等。想访问某个网站一直被拒而关掉clash后又能访问的话,可以看看是不是reject.txt的锅 ...

十月 21, 2024 · by NOSAE

k8s服务发现

基础知识 了解服务发现之前,先明确k8s内部为什么需要服务发现,而在了解为什么需要服务发现之前,先看下pod、service他们的关系。 应用运行在k8s的容器之中,而容器运行在pod内,一般来说一个pod一个容器 每个pod都处于一个扁平的IP网段内,每个pod都有其唯一IP,在这个网段内pod之间可以直接进行通信 在这个网络内,会新增、删除pod,从而也会分配新的IP或者删除IP,对于我们在pod内的应用来说,就得手动维护一个应用的IP列表,以此知道我们要访问别的应用时,对应的是哪个IP,非常耦合以及痛苦。 好在k8s提供了service这个组件,从网络的层面上,一个service代表了一组pod。当需要访问pod内的应用时,访问service的ip即可,而service除非手动删除变更,否则他的ip是稳定的。因此外界从直接访问ip经常变化的pod,变成了访问ip稳定的service,再由service将流量负载均衡到这些pod上。 服务发现是什么 平时用浏览器上过网都知道,输入一个网址比如google.com就能访问内容,背后是DNS帮我们将google.com解析成IP地址,最终浏览器才能基于TCP协议,从本地连接到这个服务提供商的IP地址。所以DNS属于服务发现的其中一种方式。 对于k8s内部来说,如果一个service中的pod的应用,想访问处于另一个service中pod的应用,最简单的就是知道对方service的IP地址。但我们编写应用的时候往往更希望连接的是一个service的名字而不是service的IP,因为service的IP说到底也还是动态分配的,如果service经过销毁重建,IP变化了,应用代码也得跟着改。 因此k8s需要服务发现,将service的名字解析为service的IP供应用去正确访问。 服务发现实际上包含两个功能点: 服务注册 服务发现 服务注册 k8s使用的是DNS服务发现,每个k8s集群都会在kube-system命名空间中运行DNS服务(这个服务本质上也是pod内的应用),称为集群DNS。每个service都会自动注册到集群DNS中,注册过程如下: 向API server提交一个新的service定义请求,请求经过认证、鉴权等准入策略后放行 Service分配得到ClusterIP,保存到集群数据仓库 在集群范围内传播该Service配置 集群DNS感知到该Service创建,创建DNS A记录 可以看到最关键的是第4步,这一步创建了A记录指向ClusterIP,这个A记录名称就是service的metadata.name,并且之后会持续关注这个Service对象。 接着就是service管理的pods,k8s自动为每个service创建endpoints对象,其中保存的是匹配标签选择器的pod列表,service后续会将流量负载均衡到这些pod上。 服务发现 为了让应用使用上集群DNS提供的服务发现功能,每个pod中的每个容器的/etc/resolv.conf文件都被配置为使用集群DNS进行解析。 比如图中my-app中的应用想要访问your-app中的应用,就得拿着"your-app-svr"这个名称去查询DNS服务拿到IP 10.0.0.20,但显然容器的网关并不维护到这个IP的路由,最终会缺省地转发到pod所在节点的网卡上,随后转发到节点的缺省网关,经过节点内核。 在继续之前,有必要插播一下:k8s的每个节点上都会运行一个名为kube-proxy的服务,其会监控API Server上service的变化,这些变化包括endpoints的变化(对应pod的增删),并根据这些变化创建iptables或IPVS规则,目的是告知节点捕获目标为Service的网络报文并转发给pod。 因此my-app发送出来的这个报文,最终会到达节点的缺省网关,经过节点内核时根据kube-proxy写入的路由规则,报文的目标IP被改成目标pod的IP。 参考 浅谈 Kubernetes 中的服务发现

十月 20, 2024 · by NOSAE