路由条目的意义

路由的设计远比一般的理解要复杂的多。典型的路由条目包括了源IP,目的IP,网关IP,scope,dev和type六个要素。

网关IP就是在配置路由的时候指定的via后面的地址,在路由表中叫Gateway,这是说明这条路由的下一跳是这个IP地址。这个IP地址之所以出现,是因为目的地址不是当前自己出口可以直接可达的,需要经过网关路由到下个网络才能投递。

也就是因此,如果这个via域配置为0.0.0.0,或者是用*表示,总之是代表一定的通配,那么就意味着这个路由的目的地和自己在一个二层的网络,到达那个目的地并不需要网关转发,只需要配置MAC地址从端口上发出去即可。这个发送出去的过程显然是去查ARP表,通过IP地址查询目标的MAC地址。很容易理解网关在路由条目中的意义,如果到达一个目标地址是需要通过网关转发出去的,via就要指定网关。大部分的个人局域网中,都会指定一个默认网关,目的IP填写了0.0.0.0,也就是所有的目的地址(通常使用命令的时候,这个词语叫做default),via后面填写网关地址。这样在其他的更精确的路由条目都不命中的情况下,就一定会命中这个默认路由条目。因为这个条目的目的IP设置是通配。使用ip命令设置这样的默认路由是例如ip route add default via 10.0.0.1。

假设一个路由条目指定了gateway,那么决策还需要知道这个gateway到底是从哪个网口发出去可达的,这就是dev的作用。既然到一个gateway必然要从一个设备出去,而其他的地方并不能指定这个gateway和设备的对应关系,于是就在路由表这里就指定了。通过dev可以到达该gateway。

如果gateway不指定,也就是该路由在同一个二层,那么仍然需要指定dev,因为即使是发送出去,也需要查从哪里发送出去。因为在收到一个数据包的时候,进入系统的时候目的IP不是自己就需要根据目的IP来查找路由,这个路由会决定这个目的IP是要转发给哪个端口(通常通过目的IP和网关IP和dev来决定)。

Dev是相对于gateway的一个更小的约束。同样起到约束作用的还有scope。Scope是一个更小程度的约束,指明了该路由在什么场景下才有效。也是用于约束目的地址的。例如不指定网关的二层路由,通常对应的scope类型是scope link。scope link的意义就是说明在同一个二层。这个意义与网关不指定的效果是呼应的。

有四种scope,global是在任何的场景下都有效,link是在链路上才有效,这个链路是指同一个端口,也就是说接收和发送都是走的同一个端口的时候,这条路由才会生效(也就是说在同一个二层)。Global则可以转发,例如从一个端口收到的包,可以查询global的路由条目,如果目的地址在另外一个网卡,那么该路由条目可以匹配转发的要求,进行路由转发。Link的scope路由条目是不会转发任何匹配的数据包到其他的硬件网口的。还有就是host,host表示这是一条本地路由,典型的是回环端口,loopback设备使用这种路由条目,该路由条目比link类型的还要严格,约定了都是本机内部的转发,不可能转发到外部。Site则是ipv6专用的路由scope。

源IP是一个路由条目的重要组成部分,这个源IP的意义在于一个补充作用。匹配还是根据目的IP进行匹配,但是由于在查找路由条目的时候很可能源地址还没有指定。典型的就是没有进行bind的发送情况,通常是随机选择端口和按照一定的规则源地址。这个一定的规则就是在这里的路由条目的src域可以影响。也就是如果进程没有bind一个源地址,将会使用这里src域里面的源地址作为数据包的源地址进行发送。但是如果进程提前bind了,命中了这个条目,就仍然会使用进程bind的源地址作为数据包的源地址。所以说这里的src只是一个建议的作用。

# ip route
default via 115.238.122.129 dev eth1 
115.238.122.128/25 dev eth1  proto kernel  scope link  src 115.238.122.163 
192.168.0.160/24 dev dpdk0.kni  proto kernel  scope link  src 192.168.0.163 
192.168.1.160/24 dev dpdk1.kni  proto kernel  scope link  src 192.168.1.163

举一个例子,从本机发出的目的地址是192.168.0.160/24网段的数据包将匹配第三条路由,如果在查询路由表之前没有设置bind,这个查询路由表的操作就会把数据包的源地址设置为192.168.0.163 。如果设置了bind,就保留bind的结果(所以你可以很容易的在Linux的主机上伪造原地址发送数据)。

src域在处理转发的数据包的时候,由于数据包是从外部收到的,外部进来的数据包也会查找路由表,也能命中同一个路由条目。但是由于外部进来的数据包已经有了明确的源地址,这里的src源地址建议就不会起作用了。所以关键就是理解src只是一个源地址的一个建议的作用即可。

对于路由表,是一个匹配的过程。一个数据包去查找匹配自己最能够匹配哪条路由表,然后就使用该路由条目指定的路由方法进行路由转发。匹配的方法就是鼎鼎大名的LPM,简单的说,就是匹配最匹配的那一个。

所以整个过程可以看到,核心的是对目的地址的限制,其他的域都是用于辅助这个限制,甚至可以辅助决策。

我们看一个虚拟机里的默认路由表:

root@ubuntu:~/# ip route show

default via 192.168.142.2 dev ens33 proto static metric 100

169.254.0.0/16 dev ens33 scope link metric 1000

192.168.142.0/24 dev ens33 proto kernel scope link src 192.168.142.135 metric 100

跳过default,后面两条的第一个域都是目的地址,确切的说,这里指定的是目的网段,然后约束了设备,也就是ens33,这个路由条目是scope link的,也就是说当主机收到目标地址是169.254.0.0/16 这个网段的时候,通过ens33这个设备将包转发出去。

虽然理论上是如此,但是实际上,例如在linux中,这个dev ens33是没有在路由中起到任何作用的,也就是说你改了ens33的名字,而不改路由表,那这个路由表项一样命中,从改名后的网口发送出去。所以dev的这个限制相当于不存在,也就是只是一个命名的作用。但是并不确定在其他的实现中是否有限制的意义。

Default路由本质上就是目标地址填了0.0.0.0的路由。Default路由有两种添加方式,一种是约束网关地址,另外一种是约束源IP。因为要添加到网关地址的默认路由,是需要在添加的时候发一个arp请求到网络上,看这个网关的地址是否存在于二层的,但是这个arp请求也是需要首先经过路由的。也就是一个鸡生蛋,蛋生鸡的问题。所以一个空的路由表是不能直接配置默认路由是一个网关的。但是明明网关确实是和当前的主机在同一个网段的。

如果要配置默认网关,首先需要先让这个网络通。这个通的方法一个是配一个scope link的路由,也就是目的地址是该网段的发包,都可以匹配这个路由。因为是link scope的,所有的请求都会走二层的路由表,这就解决了arp不能到达网关的问题。Link scope的特点是所有的数据请求走二层arp,而不是走三层路由。所以在配置了这条路由之后,再配置网关就可以了。

但是还有一个思路是使用源地址约束,我们要的只是这个查询能命中一个可以出去的路由,当我们使用源地址约束,不指定目标地址,也就是源地址是自己设备的IP的地址的包全部走link scope,同样也可以匹配,由于是link scope,也就可以触发arp请求了。

所以我们看到,整个过程的关键在于区分同二层和三层转发。Link scope的作用是用在二层转发,命中该路由条目的可以触发arp查找,但是如果是网关式的,就是一个三层转发,虽然也会触发arp查找,但是目标MAC地址永远是网关的地址,这样下一跳就锁死了。

但是这里有一个问题是如果先添加了link scope的路由条目,然后又添加了gateway的路由,这个时候再把link scope的路由条目删除,那么gateway的路由条目仍然存在并且生效,这个时候,所有的转发都会匹配这个gateway的路由条目,包括本来应该走二层转发的数据包。也就是说,原本应该在同一个二层传输走arp的数据包,在这种情况下,也会直接走网关,网关回复一个icmp redirect,但是网关仍然会把这个数据包转发到同一个二层的目标地址。

理论上,收到icmp redirect的主机应当更新自己的arp表,但是并不会更新路由表,而arp表是要先经过路由表查询的,所以这个icmp redirect相当于没有意义。Arp表里面即使是有了IP到MAC的映射关系,但是由于路由没有命中link scope,所以永远不会查询ARP表。

另外linux下的路由条目还会有一个proto的域,一般有proto kernel和proto dhcp两种。Proto表明的是这个路由条目是由谁添加,例如给一个linux设备添加一个IP的时候会自动添加一条有这个源IP约束的Link scope的路由。前面说了,也正是有了这条路由才能够使得配置网关的路由条目可以进行。这个内核自动添加的路由就是proto kernel了。

需要理解的是,路由表和网络设备是两个实体,路由表在决策的时候,由路由表看到的网络设备是独立于路由表存在的。他们是并行的关系,先要查询了路由表,找到满足路由表的路由条目,才有可能按照条目约定的路由路径去找到对应的设备。路由过程并不是发生在设备逻辑的内部,而是外部。所以实际上给设备添加了一个IP地址的时候,同时生成的路由条目实际上是两个操作被在上层进行了组合。技术上,完全可以分别的添加IP地址和路由条目,上面也说了这个添加的路由条目可以被删除,然后再次添加回去

【转自】https://www.cnblogs.com/yyleeshine/p/15185911.html

发表在 未分类 | 留下评论

局域网网段列表

IPv4

RFC 1918规定的私有IP网段
A类:10.0.0.0/8, 10.0.0.0 ~ 10.255.255.255
B类:172.16.0.0/12, 172.16.0.0 ~ 172.31.255.255
C类: 192.168.0.0/16, 192.168.0.0 ~ 192.168.255.255

RFC 3330 loopback网段
127.0.0.0/8

RFC 6598规定的私有IP网段(供ISP使用)
100.64.0.0/10, 100.64.0.0 ~ 100.127.255.255

IPv6

RFC 4193规定的私有IP网段
fd00::/8 (掩码位数为8位)

RFC 4291规定的特定IP网段,用于链路地址(MAC地址映射)
fe80::/64

发表在 未分类 | 局域网网段列表已关闭评论

大规模分布式系统设计概览

build-large-scale-service

发表在 基础架构 | 大规模分布式系统设计概览已关闭评论

【转载】分布式事务实践

前言

不知道你是否遇到过这样的情况,去小卖铺买东西,付了钱,但是店主因为处理了一些其他事,居然忘记你付了钱,又叫你重新付。又或者在网上购物明明已经扣款,但是却告诉我没有发生交易。这一系列情况都是因为没有事务导致的。这说明了事务在生活中的一些重要性。有了事务,你去小卖铺买东西,那就是一手交钱一手交货。有了事务,你去网上购物,扣款即产生订单交易。

事务的具体定义

事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。

数据库本地事务

ACID

说到数据库事务就不得不说,数据库事务中的四大特性,ACID:

  • A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

就像你买东西要么交钱收货一起都执行,要么要是发不出货,就退钱。

  • C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其他人的。

  • D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

InnoDB实现原理

InnoDB是mysql的一个存储引擎,大部分人对mysql都比较熟悉,这里简单介绍一下数据库事务实现的一些基本原理,在本地事务中,服务和资源在事务的包裹下可以看做是一体的:

我们的本地事务由资源管理器进行管理: 而事务的ACID是通过InnoDB日志和锁来保证。事务的隔离性是通过数据库锁的机制实现的,持久性通过redo log(重做日志)来实现,原子性和一致性通过Undo log来实现。UndoLog的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。 和Undo Log相反,RedoLog记录的是新数据的备份。在事务提交前,只要将RedoLog持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是RedoLog已经持久化。系统可以根据RedoLog的内容,将所有数据恢复到最新的状态。 对具体实现过程有兴趣的同学可以去自行搜索扩展。

分布式事务

什么是分布式事务

分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

分布式事务产生的原因

从上面本地事务来看,我们可以看为两块,一个是service产生多个节点,另一个是resource产生多个节点。

service多个节点

随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护  这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。

resource多个节点

同样的,互联网发展得太快了,我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。 

分布式事务的基础

从上面来看分布式事务是随着互联网高速发展应运而生的,这是一个必然的我们之前说过数据库的ACID四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论:

CAP

CAP定理,又被叫作布鲁尔定理。对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP就是你的入门理论。

  • C (一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
  • A (可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。
  • P (分区容错性):当出现网络分区后,系统能够继续工作。打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。

熟悉CAP的人都知道,三者不能共有,如果感兴趣可以搜索CAP的证明,在分布式系统中,网络无法100%可靠,分区其实是一个必然现象,如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构。

对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。

对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。

顺便一提,CAP理论中是忽略网络延迟,也就是当事务提交时,从节点A复制到节点B,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。同时CAP中选择两个,比如你选择了CP,并不是叫你放弃A。因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA。就算分区出现了你也要为后来的A做准备,比如通过一些日志的手段,是其他机器回复至可用。

BASE

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展

  1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务解决方案

有了上面的理论基础后,这里介绍开始介绍几种常见的分布式事务的解决方案。

是否真的要分布式事务

在说方案之前,首先你一定要明确你是否真的需要分布式事务?

上面说过出现分布式事务的两个原因,其中有个原因是因为微服务过多。我见过太多团队一个人维护几个微服务,太多团队过度设计,搞得所有人疲劳不堪,而微服务过多就会引出分布式事务,这个时候我不会建议你去采用下面任何一种方案,而是请把需要事务的微服务聚合成一个单机服务,使用数据库的本地事务。因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了,千万不要因为追求某些设计,而引入不必要的成本和复杂度。

如果你确定需要引入分布式事务可以看看下面几种常见的方案。

2PC

说到2PC就不得不聊数据库分布式事务中的 XA Transactions。 在XA协议中分为两阶段:

第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.

第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。

优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,XA协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

TCC

关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC事务机制相比于上面介绍的XA,解决了其几个缺点: 1.解决了协调者单点,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。 2.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。 3.数据一致性,有了补偿机制之后,由业务活动管理器控制一致性 对于TCC的解释:

  • Try阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性)
  • Confirm阶段:确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作满足幂等性。要求具备幂等设计,Confirm失败后需要进行重试。
  • Cancel阶段:取消执行,释放Try阶段预留的业务资源 Cancel操作满足幂等性Cancel阶段的异常和Confirm阶段异常处理方案基本上一致。

举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。

如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。

如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)

对于TCC来说适合一些:

  • 强隔离性,严格一致性要求的活动业务。
  • 执行时间较短的业务

实现参考:ByteTCC:https://github.com/liuyangming/ByteTCC/

本地消息表

本地消息表这个方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。

1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。

2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。

3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。

4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

MQ事务

在RocketMQ中实现了分布式事务,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,下面简单介绍一下MQ事务,如果想对其详细了解可以参考: https://www.jianshu.com/p/453c6e7ff81c。  基本流程如下: 第一阶段Prepared消息,会拿到消息的地址。

第二阶段执行本地事务。

第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。

如果确认消息失败,在RocketMq Broker中提供了定时扫描没有更新状态的消息,如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在rocketmq中是以listener的形式给发送者,用来处理。 如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这个就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失

Saga事务

Saga是30年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 Saga的组成:

每个Saga由一系列sub-transaction Ti 组成 每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T,都是一个本地事务。 可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库。

Saga的执行顺序有两种:

T1, T2, T3, …, Tn

T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n Saga定义了两种恢复策略:

向后恢复,即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。 向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。

这里要注意的是,在saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。

还是拿100元买一瓶水的例子来说,这里定义

T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水

C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水

我们一次进行T1,T2,T3如果发生问题,就执行发生问题的C操作的反向。 上面说到的隔离性的问题会出现在,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题

可以看见saga模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。也可以在业务层面通过预先冻结资金的方式隔离这部分资源, 最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。

具体实例:可以参考华为的servicecomb

最后

还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。最后在总结一些问题,大家可以下来自己从文章找寻答案:

  1. ACID和CAP的 CA是一样的吗?
  2. 分布式事务常用的解决方案的优缺点是什么?适用于什么场景?
  3. 分布式事务出现的原因?用来解决什么痛点?

如果上面问题有什么疑问的话可以关注公众号,来和我一起讨论吧。

发表在 分布式系统理论 | 【转载】分布式事务实践已关闭评论

zookeeper使用场景

原理一、 zookeeper原理

zk service网络结构
zookeeper的工作集群可以简单分成两类,一个是Leader,唯一一个,其余的都是follower,如何确定Leader是通过内部选举确定的。

Leader和各个follower是互相通信的,对于zk系统的数据都是保存在内存里面的,同样也会备份一份在磁盘上。对于每个zk节点而言,可以看做每个zk节点的命名空间是一样的,也就是有同样的数据(下面的树结构)

  • 如果Leader挂了,zk集群会重新选举,在毫秒级别就会重新选举出一个Leaer
  • 集群中除非有一半以上的zk节点挂了,zk service才不可用

zk命名空间结构

zk的命名空间就是zk应用的文件系统,它和linux的文件系统很像,也是树状,这样就可以确定每个路径都是唯一的,对于命名空间的操作必须都是绝对路径操作。与linux文件系统不同的是,linux文件系统有目录和文件的区别,而zk统一叫做znode,一个znode节点可以包含子znode,同时也可以包含数据。

比如/Nginx/conf,/是一个znode,/Nginx是/的子znode,/Nginx还可以包含数据,数据内容就是所有安装Nginx的机器IP,/Nginx/conf是/Nginx子znode,它也可以包含内容,数据就是Nginx的配置文件内容。在应用中,我们可以通过这样一个路径就可以获得所有安装Nginx的机器IP列表,还可以获得这些机器上Nginx的配置文件。

zk读写数据

  • 写数据,一个客户端进行写数据请求时,会指定zk集群中节点,如果是follower接收到写请求,就会把请求转发给Leader,Leader通过内部的Zab协议进行原子广播,直到所有zk节点都成功写了数据后(内存同步以及磁盘更新),这次写请求算是完成,然后zk service就会给client发回响应

 

  • 读数据,因为集群中所有的zk节点都呈现一个同样的命名空间视图(就是结构数据),上面的写请求已经保证了写一次数据必须保证集群所有的zk节点都是同步命名空间的,所以读的时候可以在任意一台zk节点上

 

  • ps:其实写数据的时候不是要保证所有zk节点都写完才响应,而是保证一半以上的节点写完了就把这次变更更新到内存,并且当做最新命名空间的应用。所以在读数据的时候可能会读到不是最新的zk节点,这时候只能通过sync()解决。这里先不考虑了,假设整个zk service都是同步meta信息的,后面的文章再讨论。

zk znode类型
zk中znode的节点创建时候是可以指定类型的,主要有下面几种类型。

  • PERSISTENT:持久化znode节点,一旦创建这个znode点存储的数据不会主动消失,除非是客户端主动的delete。

 

  • SEQUENCE:顺序增加编号znode节点,比如ClientA去zk service上建立一个znode名字叫做/Nginx/conf,指定了这种类型的节点后zk会创建/Nginx/conf0000000000,ClientB再去创建就是创建/Nginx/conf0000000001,ClientC是创建/Nginx/conf0000000002,以后任意Client来创建这个znode都会得到一个比当前zk命名空间最大znode编号+1的znode,也就说任意一个Client去创建znode都是保证得到的znode是递增的,而且是唯一的。

 

  • EPHEMERAL:临时znode节点,Client连接到zk service的时候会建立一个session,之后用这个zk连接实例创建该类型的znode,一旦Client关闭了zk的连接,服务器就会清除session,然后这个session建立的znode节点都会从命名空间消失。总结就是,这个类型的znode的生命周期是和Client建立的连接一样的。比如ClientA创建了一个EPHEMERAL的/Nginx/conf0000000011的znode节点,一旦ClientA的zk连接关闭,这个znode节点就会消失。整个zk service命名空间里就会删除这个znode节点。

 

  • PERSISTENT|SEQUENTIAL:顺序自动编号的znode节点,这种znoe节点会根据当前已近存在的znode节点编号自动加 1,而且不会随session断开而消失。

 

  • EPHEMERAL|SEQUENTIAL:临时自动编号节点,znode节点编号会自动增加,但是会随session消失而消失

原理二、zookeeper中Watcher和Notifications

在阅读之前首先明确个概念:

1.什么是znode?


2.什么是客户端?

我们使用znode这个术语来表示ZooKeeper的数据节点

znode维持一个stat结构,它包含数据变化的版本号、ACL变化和时间戳,以允许cache校验和协调化的更新。每当znode的数据变化时,版本号将增加。一个客户端收到数据时,它也会收到数据的版本号。

保存在每个znode中的数据都是自动读写的。读操作获取znode的所有数据,写操作替换掉znode的所有数据。每个节点有一个访问控制表(ACL)来限制谁能做哪些操作。

 

Zookeeper中的角色主要有以下三类,如下表所示:
系统模型如图所示:

 

传统轮询远程service服务
传统远程的service往往是这样服务的,服务提供者在远程service注册自己的服务,服务调用者不断去远程service轮询看看是否服务提供者有没有提供服务或者更新服务。所以有弊端,就是延时比较高,而且因为很多不必要的空轮询带来高的负载和网络损耗,这种模式到zk里面就应该是这样。

 

 

zk中异步回调服务

zk实际上的实现是异步回调来代替polling,引入一种机制是event inotifications:客户端首先注册到zk service从而可以接收znode的改变事件,也就是说一旦watch的znode变更了,客户端就会得到相应的通知,然后处理自己的业务逻辑。

 

  • zk客户端可以通过exists,getChildren,getData可以注册观察者,观察者说白了就是指定一个callback
  • 那么观察者什么时候调用呢?一旦监听的znode变化了,zk service就会发送对应znode路径给客户端,客户端调用相应的之前注册的回掉函数处理。对于节点的create,delete,setData都会触发观察者,也就是这个callback()函数。

服务端只存储事件的信息,客户端存储事件的信息和Watcher的执行逻辑。客户端在注册watcher的时候,在客户端本地会维持对应znode path和callback()的对应关系。在服务端会维护对应连接session以及znode path和事件类型。服务端触发了对应的事件类型后,会发送给客户端事件类型和znode path,在客户端会根据映射关系调用相应的callback(),接下来的业务逻辑都是在客户端实现的。

zk中watcher单次触发问题

zk中的Watch是一次触发的,一次变更只会触发一个通知,要想下次还得到通知,就需要重新注册。为什么不是永久注册Watcher呢?这主要是考虑到性能上面的影响吧。看下面的情况

  • Client C1对于znode /Task 设置了一个watcher
  • Client C2来到然后对 /Task 增加znode
  • Client C1接收到了notifications,得知监控的znode变化了
  • Client C1在处理这个notifications,这时候Client C3又增加 /task 一个znode

在步骤3的时候C1已经触发了一次watcher,步骤四的时候没有watcher了,除非重新设置watcher,所以这个过过程中就会丢失一个notifications,这就是涉及到了CAP原理了。zookeeper只能保证最终一致性,不能保证强一致性,但是因为zk保证了顺序一致性,所以就能确保最终一致性。

  • 强一致性:分布式系统里面一个数据变更后,访问任一个服务都可以得到最新的数据
  • 弱一致性:一个数据变更后,其中部分服务可以得到最新数据,部分服务不能
  • 最终一致性:在更新某个数据后,可能在开始的时候得不到最新的数据,但是最终是可以呈现最新的数据
  • 顺序一致性:更新N份数据,能保证是服务是按照N份数据顺序更新提供服务的。

其实对于上面的case也是有办法解决的,具体就是每次在注册watcher之后都getData,保证数据版本是最新的,但相比较传统的polling优势还是很明显的。

zk中Versions

每个znode一旦数据变化,都会有一个递增的版本号,在zk API执行的时候都需要指定版本号,客户端提供的版本号只有和服务端匹配了才能进行znode操作。在多个客户端都要操作同一个znode的时候版本号就很重要了。看下面的情况。

  • 比如Client C1写了一个znode /Nginx/conf的数据,写了一些配置信息,这时候/Nginx/conf版本号就从version1变成version2
  • 在上面的同时,Client C2也想写/Nginx/conf,因为C2的客户端版本还是version1,而服务端已经是version2了,此刻就会冲突,这个操作就会以失败告终。所以必须要先更新C2上到version2,然后再提交操作。
zk上更新version2到version3,C2本地更新至version3

 


适用场景一、如何竞选Master
在zookeeper应用场景提出了对于Master节点管理的问题,如何保证集群中Master可用性和唯一性,下面就利用zookeeper来实现。
在确保zookeeper集群节点安装配置的前提下,假设zk已经对外提供了正常的服务,通过下面的步骤来实现Master竞选

  • Client连接到zk上,判断znode /Roles/workers是否存在,不存在则建立,znode的类型是PERSISTENT类型,保证不会随着C1的session断开而消失。
  • Client在/Roles/workers下面建立一个SEQUENCE|EPHEMERAL类型的znode,前缀可以是worker,由zk保证znode编号是递增而且是暂时的,EPHEMERAL在前文说了,一旦session断开创建的znode也会消失。
  • Client通过getChildren获取所有的/Roles/workers下znode列表,并且设置一个Watcher等待通知,返回值有多少个znode数量就对应Client来竞选。
  • 对于步骤4返回的节点列表进行排序,找到最小的worker编号,如果是和自己创建的一致(步骤2返回值),那么就代表自己的编号是最小的,自己就是Master。如果发现自己的编号不是最小,那么就等待通知,一旦Watcher触发,就在Watcher回到步骤3。

上面的机制主要利用了zk的几个特性

  • 对于N个客户端同时请求create一个znode,zk能保证顺序的一致性,并且保证每个客户端创建的znode节点是递增并且唯一。
  • 因为创建的znode是临时的,一旦session断开,那么znode就会从zk上消失,从而给每个设置Watcher的客户端发送通知,让每个客户端重新竞选Master,编号小的肯定是Master,保证了唯一性。

下图是上面的逻辑图

下面是实现的代码,默认是连接本地的zk服务,端口是2181,zkclient模块位于zookeeper python接口只需要运行多个下面的脚本就会能实现Master的竞选。

  • 先后在三个终端上面运行下面的脚本,模拟为c1,c2,c3三个client,创建的节点依次是/Roles/workers/worker0000000000,/Roles/workers/worker0000000001,依次是/Roles/workers/worker0000000002
  • 发现c1成功竞选了Master,然后c2和c3都是slave
  • 把c1关了从而导致依次是/Roles/workers/worker0000000000消失,一段时间后c2和c3会重新竞选,c2会成为master,c3是slave
  • 重新启动c1,发现c1立马加入集群,消息里面变更表示创建了新的znode依次是/Roles/workers/worker0000000003,重新竞选,c2还是master

PS:上面步骤3里面一个客户端关闭后经历了一段时间znode才会删除,原因是这段时间内zk的session还没有被清除,因为关闭是通过ctrl+c关闭的。但是加了一个客户端,znode里面创建,就会通知其余注册了watcher的客户端


适用场景二、分布式锁实现

在zookeeper应用场景有关于分布式集群配置文件同步问题的描述,设想一下如果有100台机器同时对同一台机器上某个文件进行修改,如何才能保证文本不会被写乱,这就是最简单的分布式锁,本文介绍利用zk实现分布式锁。下面是写锁的实现步骤

分布式写锁
create一个PERSISTENT类型的znode,/Locks/write_lock

  • 客户端创建SEQUENCE|EPHEMERAL类型的znode,名字是lockid开头,创建的znode是/Locks/write_lock/lockid0000000001
  • 调用getChildren()不要设置Watcher获取/Locks/write_lock下的znode列表
  • 判断自己步骤2创建znode是不是znode列表中最小的一个,如果是就代表获得了锁,如果不是往下走
  • 调用exists()判断步骤2自己创建的节点编号小1的znode节点(也就是获取的znode节点列表中最小的znode),并且设置Watcher,如果exists()返回false,执行步骤3
  • 如果exists()返回true,那么等待zk通知,从而在回掉函数里返回执行步骤3

释放锁就是删除znode节点或者断开连接就行

*注意:上面步骤2中getChildren()不设置Watcher的原因是,防止羊群效应,如果getChildren()设置了Watcher,那么集群一抖动都会收到通知。在整个分布式锁的竞争过程中,大量重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知—,这个显然看起来不怎么科学。客户端无端的接受到过多的和自己不相关的事件通知,这如果在集群规模大的时候,会对Server造成很大的性能影响,并且如果一旦同一时间有多个节点的客户端断开连接,这个时候,服务器就会像其余客户端发送大量的事件通知——这就是所谓的羊群效应。

发表在 分布式系统理论 | zookeeper使用场景已关闭评论