《从Paxos到Zookper分布式一致性原理与实践》读书笔记
笔记主要是关于《从Paxos到Zookeeper分布式一致性原理与实践》的学习笔记,从而对分布式有个大概的了解。对 Zookeeper有个基本认识。
写在前面
嗯,一直听人家说分布式,奈何这个概念一直不清晰,不知道和微服务有啥区别,而且问大佬,也总是听的一知半解的,一直听人家讲Zookeeper,奈何工作中遇不到,很早就想系统的学习一下,奈何时间挤不出来,除了工作就是不开心,没时间学习。现在离职准备找工作,留了点时间系统的学习一下。而且,忍受不了一知半解。要不就不学,要学就形成一个大概的知识体系,和已有的体系建立连接。至于学了有用没有,那就不重要了,也不考虑时间成本问题,要是这点任性都没有的,那活着多憋屈呀。哈哈…
嗯,时间原因,好多复杂的东西我都没看,先整体熟悉下,以后如果会用到在深入研究研究。
笔记还在更新中…..
傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。——–王小波
第1章分布式架构
随着计算机系统规模变得越来越大,将所有的业务单元集中部署在一个或若干个大型机上的体系结构,已经越来越不能满足当今计算机系统.
1.1从集中式到分布式
伴随着大型主机时代的到来,集中式的计算机系统架构也成为了主流。在那个时候,由于大型主机卓越的性能和良好的稳定性,其在单机处理能力方面的优势非常明显,使得IT系统快速进入了集中式处理阶段,其对应的计算机系统称为集中式系统。但从20世纪80年代以来,计算机系统向网络化和微型化的发展日趋明显,传统的集中式处理模式越来越不能适应人们的需求。(找重点,计算机系统架构,即分布式是一种计算机系统架构方式)
- 学习成本高
- 大型主机贵
- 容错性差,扩容困难
为了解决业务快速发展给IT系统带来的巨大挑战,从2009年开始,阿里集团启动了去IOE计划,其电商系统开始正式迈入分布式系统时代。
1.1.1集中式的特点
集中式系统:指由一台或多台主计算机组成中心节点,数据集中存储于这个中心节点中,并且整个系统的所有业务单元都集中部署在这个中心节点上,系统的所有功能均由其集中处理。
在集中式系统中:每个终端或客户端机器仅仅负责数据的录入和输出,而数据的存储与控制处理完全交由主机来完成。集中式系统最大的特点就是部署结构简单。由于集中式系统往往基于底层性能卓越的大型主机,因此无须考虑如何对服务进行多个节点的部署,也就不用考虑多个节点之间的分布式协作问题。
1.1.2分布式的特点
在《分布式系统概念与设计》生一书中,对分布式系统做了如下定义:
分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。 (找重点,硬件或软件组件,个人理解 ,硬件组件分布我们可以结合HarmonyOS理解,音画同步,应用跨设备流转,软总线等硬件抽象的分布式,软件组件分布这里结合我们常说微服务,类比Web分布式系统。)
一个标准的分布式系统在没有任何特定业务逻辑约束的情况下,都会有如下几个特征。
分布性:多台计算机都会在
空间上随意分布对等性:计算机
没有主/从之分- 副本(Replica)最常见的概念之一,对数据和服务提供的一种冗余方式。为了对外提供高可用的服务,我们往往会对数据和服务进行副本处理。
- 数据副本是指在不同的节点上持久化同一份数据,当某一个节点上存储的数据丢失时,可以从副本上读取到该数据,这是解决分布式系统数据丢失问题最为有效的手段。
- 服务副本指多个节点提供同样的服务,每个节点都有能力接收来自外部的请求并进行相应的处理。
并发性:在一个计算机网络中,程序运行过程中的
并发性操作是非常常见的行为,例如同一个分布式系统中的多个节点,可能会并发地操作一些共享的资源,诸如数据库或分布式存储等.缺乏全局时钟一个典型的分布式系统是由一系列在
空间上随意分布的多个进程组成的,具有明显的分布性,这些进程之间通过交换消息来进行相互通信。因此,在分布式系统中,很难定义两个事件究竟谁先谁后,原因就是因为分布式系统缺乏一个全局的时钟序列控制。故障总是会发生:任何在
设计阶段考虑到的异常情况,一定会在系统实际运行中发生,并且,在系统实际运行过程中还会遇到很多在设计时未能考虑到的异常故障。所以,除非需求指标允许,在系统设计时不能放过任何异常情况。
1.1.3分布式环境的各种问题
通信异常
分布式系统需要在各个节点之间进行网络通信,因此每次网络通信都会伴随着网络不可用的风险,网络光纤、路由器或是DNS等硬件设备或是系统不可用都会导致最终分布式系统无法顺利完成一次网络通信。
即使分布式系统各节点之间的网络通信能够正常进行,其延时也会远大于单机操作。通常我们认为在现代计算机体系结构中,单机内存访问的延时在纳秒数量级(通常是10ns左右),而正常的一次网络通信的延迟在0.1~1ms左右(相当于内存访问延时的105-106倍),如此巨大的延时差别,也会影响消息的收发的过程,因此消息丢失和消息延迟变得非常普遍。
网络分区
当网络由于发生异常情况,导致分布式系统中部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够进行正常通信,而另一些节点则不能–我们将这个现象称为网络分区,就是俗称的脑裂。当网络分区出现时,分布式系统会出现局部小集群,在极端情况下,这些局部小集群会独立完成原本需要整个分布式系统才能完成的功能,包括对数据的事务处理等。
三态
分布式系统的每一次请求与响应,存在特有的三态概念,即成功、失败与超时。在传统的单机系统中,应用程序在调用一个函数之后,能够得到一个非常明确的响应:成功或失败
分布式系统中当网络出现异常的情况下,就可能会出现超时现象,通常有以下两种情况:
- 由于网络原因,该请求(消息)并没有被成功地发送到接收方,而是在发送过程就发生了消息丢失现象。
- 该请求(消息)成功的被接收方接收后,并进行了处理,但是在将响应反馈给发送方的过程中,发生了消息丢失现象。
当出现这样的超时现象时,网络通信的发起方是无法确定当前请求是否被成功处理的。
节点故障
节点故障则是比较常见的问题,组成分布式系统的服务器节点出现的宕机或“僵死”现象。通常根据经验来说,每个节点都有可能会出现故障,并且每天都在发生。
1.2从ACID到CAP/BASE
1.2.1 ACID
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元(Unit),狭义上的事务特指数据库事务。
- 一方面,当多个应用程序并发访,问数据库时,事务可以在这些应用程序之间提供一个
隔离方法,以防止彼此的操作互相干扰。 - 另一方面,事务为数据库操作序列提供了一个
从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。
事务具有四个特征,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称为事务的ACID特性。
- 原子性: 事务的原子性是指
事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现以下两种状态之一。全部成功执行。全部不执行。任何一项操作失败都将导致整个事务失败,同时其他已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。 - 一致性: 事务的一致性是指
事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。也就是说,事务执行的结果必须是**使数据库从一个一致性状态转变到另一个一致性状态,**因此当数据库只包含成功事务提交的结果时,就能说数据库处于一致性状态。而如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。 - 持久性: 事务的
持久性也被称为永久性,是指一个事务一旦提交,它对数据库中对应数据的状态变更就应该是永久性的。换句话说,一旦某个事务成功结束,那么它对数据库所做的更新就必须被永久保存下来-即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束时的状态。 - 隔离性: 事务的隔离性是指在并发环境中,并发的事务是相互隔离的,
一个事务的执行不能被其他事务干扰。也就是说,不同的事务并发操纵相同的数据时,每个事务都有各自完整的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同:
| 隔离级别 | 描述 |
|---|---|
未授权读取 |
未授权读取也被称为读未提交(Read Uncommitted)该隔离级别允许脏读取,其隔离级别最低。换句话说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此还没有进行事务提交;而与此同时,允许另一个事务也能够访问该数据。举个例子来说,事务A和事务B同时进行,事务A在整个执行阶段,会将某数据项的值从1开始,做一系列加法操作(比如说加1操作)直到变成10之后进行事务提交,此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2、2变成3等),而对这一系列的中间值的读取就是未授权读取。 |
授权读取 |
授权读取也被称为读已提交(Read Committed)它和未授权读取非常相近,唯一的区别就是授权读取只允许获取已经被提交的数据。同样以上面的例子来说,事务A和事务B同时进行,事务A进行与上述同样的操作,此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的10,另外,如果说有一个事务C,和事务A进行非常类似的操作,只是事务C是将数据项从10加到20,此时事务B也同样可以读取到20,即授权读取允许不可重复读取。 |
可重复读取(Repeatable Read) |
简单地说,就是保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的。因此该事务级别禁止了不可重复读取和脏读取,但是有可能出现幻影数据。所谓幻影数据,就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。在上面的例子,可重复读取隔离级别能够保证事务B在第一次事务操作过程中,始终对数据项读取到1,但是在下一次事务操作中,即使事务B(注意,事务名字虽然相同,但是指的是另一次事务操作)采用同样的查询方式,就可能会读取到10或20. |
串行化(Serializable) |
是最严格的事务隔离级别。它要求所有事务都被串行执行,即事务只能一个接一个地进行处理,不能并发执行。类似于java的同步块同步方法 |
| 隔离级别 | 脏读 | 可重复读 | 幻读 |
|---|---|---|---|
| 未授权读取 | 存在 | 不可以 | 存在 |
| 授权读取 | 不存在 | 不可以 | 存在 |
| 可重复读取 | 不存在 | 可以 | 存在 |
| 串行化 | 不存在 | 不存在 | 不存在 |
事务隔离级别越高,就越能保证数据的完整性和一致性,但同时对并发性能的影响也越大。通常,对于绝大多数的应用程序来说,可以优先考虑将数据库系统的隔离级别设置为授权读取,这能够在避免脏读取的同时保证较好的并发性能。
尽管这种事务隔离级别会导致不可重复读、虚读和第二类丢失更新等并发问题,但较为科学的做法是在可能出现这类问题的个别场合中,由应用程序主动采用悲观锁或乐观锁来进行事务控制。
1.2.2分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。
1.2.3 CAP和BASE理论
在可用性和一致性之间永远无法存在一个两全其美的方案,于是如何构建一个兼顾可用性和一致性”的分布式系统成为了无数工程师探讨的难题,出现了诸如CAP和BASE这样的分布式系统经典理论。
CAP定理
CAP定理:CAP理论告诉我们,一个分布式系统不可能同时满足一致性(C: Consistency)、可用性(A: Availability)和分区容错性(P: Partition tolerance)这三个基本需求,最多只能同时满足其中的两项。
一致性:在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。那么这样的系统就被认为具有强一致性(或严格的一致性)。
可用性:可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。对于用户的一个操作请求,系统必须能够在指定的时间(即响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为不可用。
分区容错性:分区容错性 约束 了一个分布式系统需要具有如下特性:
- 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足
一致性和可用性的服务,除非是整个网络环境都发生了故障。 网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络等)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。
| 放弃CAP定理 | 说明 |
|---|---|
放弃P(分区容错性) |
如果希望能够避免系统出现分区容错性问题,一种较为简单的做法是将所有的数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上。这样的做法虽然无法100%地保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响。但同时需要注意的是,放弃P的同时也就意味着放弃了系统的可扩展性,分区容错性可以说是一个最基本的要求 |
放弃A(可用性) |
相对于放弃“分区容错性”来说,放弃可用性则正好相反,其做法是一旦系统遇到网络分区或其他故障时,那么受到影响的服务需要等待一定的时间,因.此在等待期间系统无法对外提供正常的服务,即不可用 |
放弃C(一致性) |
这里所说的放弃一致性,并不是完全不需要数据一致性,如果真是这样的话,那么系统的数据都是没有意义的,整个系统也是没有价值的。事实上,放弃一致性指的是放弃數据的强一致性,而保留數据的最终一致性。这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。这就引入了一个时间窗口的概念,具体多久能够达到数据一致取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短 |
而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构设计师往往需要把精力花在如何根据业务特点在C(一致性)和A (可用性)之间寻求平衡。
BASE理论
BASE是Basically Available (基本可用), Soft state (软状态)和Eventually consistent(最终一致性)三个短语的简写.
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency),
BASE中的三要素:
基本可用:是指分布式系统在出现不可预知故障的时候,允许损失部分可用性-但请注意,这绝不等价于系统不可用。以下两个就是“基本可用”的典型例子。响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障) ,查询结果的响应时间增加到了1-2秒。功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
弱状态:弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。最终一致性:最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
亚马逊首席技术官Werner Vogels在于2008年发表的一篇经典文章:对最终一致性进行了非常详细的介绍。他认为最终一致性是一种特殊的弱一致性:系统能够保证在没有其他新的更新操作的情况下,数据最终一定能够达到一致的状态,因此所有客户端对系统的数据访问都能够获取到最新的值。同时,在没有发生故障的前提下,数据达到一致状态的时间延迟,取决于网络延迟、系统负载和数据复制方案设计等
最终一致性变种:
因果一致性( Causal consistency )
因果一致性是指,如果进程A在更新完某个数据项后通知了进程B,那么进程B之后对该数据项的访问都应该能够获取到进程A更新后的最新值,并且如果进程B要对该数据项进行更新操作的话,务必基于进程A更新后的最新值,即不能发生丢失更新情况。与此同时,与进程A无因果关系的进程C的数据访问则没有这样的限制。
读已之所写( Read your writes)
读己之所写是指,进程A更新一个数据项之后,它自己总是能够访问到更新过的最新值,而不会看到旧值。也就是说,对于单个数据获取者来说,其读取到的数据,一定不会比自己上次写入的值旧。因此,读己之所写也可以看作是一种特殊的因果一致性。
会话一致性(Session consistency )
会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。单调读一致性( Monotonic read consistency )
单调读一致性是指如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。**单调写一致性是指,一个系统需要能够保证来自同一个进程的写操作被顺序地执行。**
总的来说, BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性是相反的,它完全不同于ACID的强一致性模型,而是提出通过牺牲强一致性来获,得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中, ACID特性与BASE理论往往又会结合在一起使用。
第2章一致性协议
为了解决分布式一致性问题,在长期的探索研究过程中,涌现出了一大批经典的一致性协议和算法,其中最著名的就是二阶段提交协议、三阶段提交协议和Paxos算法了。
2.1 2PC3PC
在分布式系统中,每一个机器节点虽然都能够明确地知道自己在进行事务操作过程中的结果是成功或失败,但却无法直接获取到其他分布式节点的操作结果。因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个称为“协调者(Coordinator)”的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为“参与者” (Participant),协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正进行提交。基于这个思想,衍生出了二阶段提交和三阶段提交两种协议。
2.1.1 2PC
2PC,是Two-Phase Commit的缩写,即二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法。通常,二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前,绝大部分的关系型数据库都是采用二阶段提交协议·来完成分布式事务处理的,利用该协议能够非常方便地完成所有分布式事务参与者的协调,统一决定事务的提交或回滚,从而能够有效地保证分布式数据一致性.
协议说明
顾名思义,二阶段提交协议是将事务的提交过程分成了两个阶段
阶段一:提交事务请求
- 事务询问。
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 - 执行事务。各
参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中。 - 各参与者向协调者反馈事务询问的响应。
如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行。
由于上面讲述的内容在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程,因此二阶段提交协议的阶段一也被称为“投票阶段”,即各参与者投票表明是否要继续执行接下的事务提交操作。
阶段二:执行事务提交
阶段二中,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下,包含以下两种可能:
执行事务提交:假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交。发送提交请求。协调者向所有参与者节点发出Commit请求。事务提交。参与者接收到Commit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。反馈事务提交结果。参与者在完成事务提交之后,向协调者发送Ack消息。完成事务。协调者接收到所有参与者反馈的Ack消息后,完成事务。
中断事务:假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。发送回滚请求。协调者向所有参与者节点发出Rollback请求。事务回滚。参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。反馈事务回滚结果。参与者在完成事务回滚之后,向协调者发送Ack消息。中断事务。协调者接收到所有参与者反馈的Ack消息后,完成事务中断。
二阶段提交将一个事务的处理过程分为了投票和执行两个阶段,其核心是对每个事务都采用先尝试后提交的处理方式,因此也可以将二阶段提交看作一个强一致性的算法.
优缺点:
- 优点:
原理简单,实现方便。 - 缺点:
同步阻塞、单点问题、脑裂、太过保守。
同步阻塞:所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
单点问题:如果协调者是在阶段二中出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
数据不一致:在二阶段提交协议的阶段二,即执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit请求的参与者则无法进行事务提交,于是整个分布式系统便出现了数据不一致性现象。
太过保守:如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
2.1.2 3PC
二阶段提交协议的基础上进行了改进,提出了三阶段提交协议。
3PC,是Three-Phase Commit的缩写,即三阶段提交,是2PC的改进版,其将二阶段提交协议的“提交事务请求”过程一分为二,形成了由CanCommit, PreCommit和do Commit三个阶段组成的事务处理协议.
阶段一: CanCommit1
1. 事务询问。协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
2. 各参与者向协调者反馈事务询问的响应。参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应。
阶段二: PreCommit
在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能。
- 执行事务预提交假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。
发送预提交请求。协调者向所有参与者节点发出preCommit的请求,并进入Prepared阶段。事务预提交。参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。各参与者向协调者反馈事务执行的响应。如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)中断事务
- 中断事务假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
发送中断请求。协调者向所有参与者节点发出abort请求。中断事务。无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务。
阶段三: doCommit
该阶段将进行真正的事务提交,会存在以下两种可能的情况。
- 执行提交.
发送提交请求。进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求。事务提交。参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。反馈事务提交结果。参与者在完成事务提交之后,向协调者发送Ack消息。完成事务。协调者接收到所有参与者反馈的Ack消息后,完成事务。
- 中断事务进入这一阶段,假设
协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。发送中断请求。协调者向所有的参与者节点发送abort请求。事务回滚。参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
需要注意的是,一旦进入阶段三,可能会存在以下两种故障。
- 协调者出现问题。
- 协调者和参与者之间的网络出现故障。
无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。
感觉这里说的有点问题,感觉和第二阶段的中断事务是有冲突的
优缺点
三阶段提交协议的优点:
相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
三阶段提交协议的缺点:
三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
2.2 Paxos算法
嗯,这部分有点深,简单了解一下。
2.2.1追本湖源
- 拜占廷将军问题
- Paxos算法名称的由来也是取自Lamport论文(The Par-Time Parliament)中提到的Paxos小岛。
2.2.2 Paxos理论的诞生
由于Lamport个人自负固执的性格,使得Paxos理论的诞生可谓一波三折。
2.2.3 Paxos算法详解
嗯。。。这个以后在学习。
第3章Paxos的工程实践
嗯。。。这个以后在学习。
第4章Zookeeper与Paxos
Apache ZooKeeper是由Apache Hadoop的子项目发展而来,于2010年11月正式成为了Apache的顶级项目。ZooKeeper为分布式应用提供了高效且可靠的分布式协调服务,提供了诸如统一命名服务、配置管理和分布式锁等分布式的基础服务。在解决分布式数据一致性方面, ZooKeeper并没有直接采用Paxos算法,而是采用了一种被称为ZAB(Zookeeper Atomic Broadcast)的一致性协议。
4.1初识ZooKeeper
4.1.1 ZooKeeper介绍
ZooKeeper是一个开放源代码的分布式协调服务,由知名互联网公司雅虎创建,是Google.Chubby的开源实现。ZooKeeper的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
Zookeeper是什么
Zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能. ZooKeeper可以保证如下分布式一致性特性.
- 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去.
- 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况
- 单一视围( Single System Image ):无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的。
- 可靠性:一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
- 实时性:Zookeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
ZooKeeper的设计目标
Zookeeper致力于提供一个高性能、高可用,且具有严格的顺序访问控制能力(主要是写操作的严格顺序性)的分布式协调服务。高性能使得ZooKeeper能够应用于那些对系统吞吐有明确要求的大型分布式系统中,高可用使得分布式的单点问题得到了很好的解决,而严格的顺序访问控制使得客户端能够基于ZooKeeper实现一些复杂的同步原语。下面我们来具体看一下Zookeeper的四个设计目标。
- 目标一:简单的数据模型:
ZooKeeper使得分布式程序能够通过一个共享的、树型结构的名字空间来进行相互协调。 - 目标二:可以构建集群:一个
ZooKeeper集群通常由一组机器组成,一般3-5台机器就可以组成一个可用的ZooKeeper集群了。 - 目标三:顺序访问:对于来自客户端的每个更新请求, ZooKeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序可以使用ZooKeeper的这个特性来实现更高层次的同步原语。关于ZooKeeper的事务请求处理和事务ID的生成,
- 目标四:高性能“由于ZooKeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景。
4.1.2 Zookeeper从何而来
关于”ZooKeeper”这个项目的名字,其实也有一段趣闻。在立项初期,考虑到之前内部很多项目都是使用动物的名字来命名的(例如著名的Pig项目),雅虎的工程师希望给这个项目也取一个动物的名字。时任研究院的首席科学家Raghu Ramakrishnan开玩笑地说: “在这样下去,我们这儿就变成动物园了!”此话一出,大家纷纷表示就叫动物园管理员吧-因为各个以动物命名的分布式组件放在一起,雅虎的整个分布式系统看上去就像一个大型的动物园了,而ZooKeeper正好要用来进行分布式环境的协调-于是,ZooKeeper的名字也就由此诞生了。
4.1.3 ZooKeeper的基本概念
集群角色
通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是Master/Slave模式(主备模式)。在这种模式中,我们把能够处理所有写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。
而在ZooKeeper中,这些概念被颠覆了。它没有沿用传统的Master/Slave概念,而是引入了Leader, Follower和Observer三种角色。ZooKeeper集群中的所有机器通过一个Leader选举过程来选定一台被称为”Leader“的机器, Leader服务器为客户端提供读和写服务。除Leader外,其他机器包括Follower``和Observer, Follower和Observer都能够提供读服务,唯一的区别在于, Observer机器不参与Leader选举过程,也不参与写操作的“过半写成功”策略,因此Observer可以在不影响写性能的情况下提升集群的读性能。
会话(Session)
Session是指客户端会话,在讲解会话之前,我们首先来了解一下客户端连接。在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接.ZooKeeper对外的服务端口默认是2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知.
数据节点(Znode)
在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器。然而,在ZooKeeper中, “节点”分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点-ZNode,.
ZooKeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割的路径,就是一个Znode,例如/oo/pathl,每个ZNode上都会保存自己的数据内容,同时,还会保存一系列属性信息。在ZooKeeper中, ZNode可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
Zookeeper还允许用户为每个节点添加一个特殊的属性: SEQUENTIAL,一旦节点被标记上这个属性,那么在这个节点被创建的时候, ZooKeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。
版本
在前面我们已经提到, ZooKeeper的每个ZNode上都会存储数据,对应于每个ZNode,ZooKeeper都会为其维护一个叫作Stat的数据结构, Stat中记录了这个ZNode的三个数据版本,分别是version (当前ZNode的版本), cversion (当前ZNode子节点的版本)和aversion (当前ZNode的ACL版本)
Watcher
Watcher (事件监听器),是ZooKeeper中的一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候, ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。
ACL
ZooKeeper采用ACL (Access Control Lists)策略来进行权限控制,类似于UNIX文件系统的权限控制。ZooKkeeper定义了如下5种权限。
- CREATE;创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIN:设置节点ACL的权限。其中尤其需要注意的是.
CREATE和DELETE这两种权限都是针对子节点的权限控制。
4.1.4为什么选择Zookeeper
- 达到了一个工业级产品的标准。其次, ZooKeeper是开放源代码的.
- ZooKeeper是免费的,你无须为它支付任何费用。
- ZooKeeper已经得到了广泛的应用。诸如Hadoop, HBase, Storm和Solr等.
4.2 Zookeeper的ZAB协议
嗯,时间紧张,这部分以后在看
4.2.1 ZAB协议
4.2.2协议介绍
4.2.3深入ZAB协议
4.2.4 ZAB与Paxos算法的联系与区别
第5章使用Zookeeper
5.1部署与运行
如何部署一个ZooKeeper集群。
5.1.1系统环境
- 操作系统: GNU/Linux, Sun Solaris, Win32以及MacoSX等
- Java环境: JDK1.6 以上。
5.1.2集群与单机
ZooKeeper有两种运行模式:集群模式和单机模式。涉及的部署与配置操作都是针对GNU/Linux系统的。
集群模式
- zookeeper安装
下载网站: https://zookeeper.apache.org/releases.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38[root@liruilong opt]# wget https://downloads.apache.org/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz
--2021-07-01 16:47:48-- https://downloads.apache.org/zookeeper/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin.tar.gz
Resolving downloads.apache.org (downloads.apache.org)... 135.181.214.104, 88.99.95.219, 135.181.209.10, ...
Connecting to downloads.apache.org (downloads.apache.org)|135.181.214.104|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 9623007 (9.2M) [application/x-gzip]
Saving to: ‘apache-zookeeper-3.5.9-bin.tar.gz’
100%[===========================================================================>] 9,623,007 7.57KB/s in 22m 35s
2021-07-01 17:10:25 (6.93 KB/s) - ‘apache-zookeeper-3.5.9-bin.tar.gz’ saved [9623007/9623007]
[root@liruilong opt]# ls
apache-zookeeper-3.5.9-bin.tar.gz
[root@liruilong opt]#
[root@liruilong opt]# mkdir zookeeper-3.5.9
[root@liruilong opt]# tar -xf apache-zookeeper-3.5.9-bin.tar.gz -C ./zookeeper-3.5.9/
[root@liruilong opt]# cd zookeeper-3.5.9/
[root@liruilong zookeeper-3.5.9]# ls
apache-zookeeper-3.5.9-bin
[root@liruilong zookeeper-3.5.9]#
[root@liruilong zookeeper-3.5.9]# ls
apache-zookeeper-3.5.9-bin
[root@liruilong zookeeper-3.5.9]# cd apache-zookeeper-3.5.9-bin/
[root@liruilong apache-zookeeper-3.5.9-bin]# ls
bin conf docs lib LICENSE.txt NOTICE.txt README.md README_packaging.txt
[root@liruilong apache-zookeeper-3.5.9-bin]# ls -l
total 40
drwxr-xr-x 2 502 games 4096 Jan 7 02:56 bin
drwxr-xr-x 2 502 games 4096 Jan 7 02:56 conf
drwxr-xr-x 5 502 games 4096 Jan 7 03:48 docs
drwxr-xr-x 2 root root 4096 Jul 1 19:12 lib
-rw-r--r-- 1 502 games 11358 Oct 5 2020 LICENSE.txt
-rw-r--r-- 1 502 games 432 Jan 7 00:12 NOTICE.txt
-rw-r--r-- 1 502 games 1560 Jan 7 02:56 README.md
-rw-r--r-- 1 502 games 1347 Jan 7 02:56 README_packaging.txt
[root@liruilong apache-zookeeper-3.5.9-bin]# - 配置配置文件初次使用
1
2
3
4
5
6
7[root@liruilong apache-zookeeper-3.5.9-bin]# ls conf/
configuration.xsl log4j.properties zoo_sample.cfg
[root@liruilong apache-zookeeper-3.5.9-bin]# cp ./conf/zoo_sample.cfg ./conf/zoo.cfg
[root@liruilong apache-zookeeper-3.5.9-bin]# ls conf/
configuration.xsl log4j.properties zoo.cfg zoo_sample.cfg
[root@liruilong apache-zookeeper-3.5.9-bin]# vim ./conf/zoo.cfgZooKeeper,需要将%ZK-HOME%conf目录下的zoo-sample.cfg文件重命名为zoo.cfg,并且按照如下代码进行简单配置即可:在1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/tmp/zookeeper
# the port at which the clients will connect
clientPort=2181
server.1=IP1: 2888:3888
server.2=IP2: 2888:3888
server.3-IP3: 2888:3888集群模式下,集群中的每台机器都需要感知到整个集群是由哪几台机器组成的,在配置文件中,可以按照这样的格式进行配置,每一行都代表一个机器配置:server.id-host:port:port其中,id被称为Server ID,用来标识该机器在集群中的机器序号。同时,在每台ZooKeeper机器上,我们都需要在数据目录(即dataDir参数指定的那个目录)下创建一个myid文件,该文件只有一行内容,并且是一个数字,即对应于每台机器的Server ID数字。在ZooKeeper的设计中,集群中所有机器上zoo.cfg文件的内容都应该是一致内。
myid文件中只有一个数字,即一个Server ID,例如, server.1的myid文件内容就是“1” 。注意,请确保每个服务器的myid文件中的数字不同,并且和自己所在机器的zoo.cfg中server.id-host:port: port的d值一致。另外, id的范围是1-255
- 创建 myid文件
1
2
3
4
5
6
7
8
9
10
11[root@liruilong ~]# touch /tmp/zookeeper/myid ;echo "1" > myid
```
5. 按照相同的步骤,为其他机器都配置上zoo.cfg和myid文件。
6. 启动服务器。 `zkServer.sh`脚本进行服务器的启动
```bash
[root@liruilong bin]# sh zkServer.sh start
/usr/bin/java
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper-3.5.9/apache-zookeeper-3.5.9-bin/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 6580.
[root@liruilong bin]# - 验证服务器。启动完成后,可以使用如下命令来检查服务器启动是否正常:
$telnet 127.0.0.1 2181
1 | [root@liruilong bin]# telnet 127.0.0.1 2181 |
报错了,指令不在白名单里,百度解决一下 : 修改启动指令 zkServer.sh ,往里面添加 :ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}" #L77
1 | ...... |
在试一下,这里需要 注意的是 服务先stop 然后在start,因为没有restart命令。
1 | [root@liruilong bin]# sh zkServer.sh start |
服务正常启动了。嗯,这个是单机版的,阿里云上搞得,笔记本内存太小,多跑机器卡的不行,所以集群不试了。
单机模式
单机模式只有一个Server
1 | [root@liruilong bin]# cat ../conf/zoo.cfg |
集群模式和单机模式下输出的服务器验证信息基本一致,只有Mode属性不一样。在集群模式中, Mode显示的是leader,其实还有可能是follower,对于Leader和Follower角色的概念,而在单机模式中, Mode显示的是standalone.
所谓的伪集群,用一句话说就是,集群所有的机器都在一台机器上,但是还是以集群的特性来对外提供服务。这种模式和集群模式非常类似,只是把zoo.cfg做了如下修改:
1 | server.1=IP:2888:3888 |
嗯,这里的myid文件如何写,书里没有明确说明。百度下需要跑三个端口不同的服务。myid还是原来的写法。
5.1.3运行服务
Java命令行
这是Java语言中通常使用的方式。使用Java命令来运行JAR包,具体方法是在ZooKeeper 发行版本的%ZK HOME%目录下执行如下命令:
1 | $java -cp zookeeper-3.4.3.jar:lib/ slf4j-api-1.6. 1.jar:Lib/slf4j- log4j121.6.1.iar: lib/loq4i-1.2.15.jar:conf org.apache.zookeeper.server.quorum.QuorumPeerMain conf/zoo.cfg |
通过运行上面这个命令, ZooKeeper的主入口QuorumPeerMain类就会启动ZooKeeper服务器,同时,随着ZooKeeper服务的启动,其内部的JMX也会被启动,方便管理员在JMX管理控制台上进行一些对ZooKeeper的监控与操作。
使用ZooKeeper自带的启动脚本来启动ZooKeeper
1 | [root@liruilong bin]# ls | grep .sh |
常见异常
在启动的时候,通常会碰到一些异常,下面将对这些常见的异常进行讲解。
- 端口被占用在启动ZooKeeper的时候,可能出现如下“端口被占用”的异常,导致服务器无法正常启动:
1
java.net.BindException: Address already in use
- 磁盘没有剩余空间无论是在Zookeeper启动还是正常运行过程中,都有可能出现如下“磁盘没有剩余空间”的异常,一旦遇到这个异常, ZooKeeper会立即执行Failover策略,从而退出进程:
1
java.io.IOException: No space Left on device
- 无法找到myid文件在数据目录下创建一个myid文件。这里说的“无法找到myid文件”就是因为没有找到这个配置文件而导致的如下异常:
1
ERROR Imain:OuorumPeerMaina851 . Invalid config, exiting abnormally
- 集群中其他机器Leader选举端口未开在集群模式部署下服务器逐台启动的过程中,会碰到类似于下面这样的异常:
1
WARN [QuorumPeer[myid-11/0:0:0:0:0:0:0:0:2181:0uorumCnxManagera368]Cannot open channel to 2 at election address /122.228.242.241:3888
5.2客户端脚本
带的一些命令行工具,在本节,我们重点要看下zkCli这个脚本。进入ZooKeeper的bin目录之后,直接执行如下命令:当看到如下输出信息时,表示已经成功连接上本地的ZooKeeper服务器了:
1 | [root@liruilong bin]# sh zkCli.sh |
命令没有显式地指定ZooKeeper服务器地址,那么默认是连接本地的ZooKeeper服务器。如果希望连接指定的ZooKeeper服务器,可以通过如下方式实现:sh zkCli.sh -server ip:port
5.2.1创建
使用create命令,可以创建一个ZooKeeper节点。用法如下:create [-5] [-e] path data act
其中, -s或-e分别指定节点特性:顺序或临时节点。默认情况下,即不添加-s或-e参数的,创建的是持久节点。执行完下面的命令,就在ZooKeeper的根节点下创建了一个叫作/k-book的节点,并且节点的数据内容是“123”。另外, create命令的最后一个参数是acl,它是用来进行权限控制的,缺省情况下,不做任何权限控制。
1 | [zk: localhost:2181(CONNECTED) 0] |
5.2.2读取
与读取相关的命令包括1s命令和get命令。
1s
使用1s命令,可以列出ZooKeeper指定节点下的所有子节点。当然,这个命令只能看到指定节点下第一级的所有子节点。用法如下:Is path [watch],
其中, path表示的是指定数据节点的节点路径。执行如下命令:第一次部署的ZooKeeper集群,默认在根节点“/"下面有一个叫作/zookeeper的保留节点。
1 | [zk: localhost:2181(CONNECTED) 1] ls / |
** get**
使用get命令,可以获取ZooKeeper指定节点的数据内容和属性信息。用法如下:get path [watch]
执行如下命令:
1 | [zk: localhost:2181(CONNECTED) 2] get /zk-book |
5.2.3更新
使用set命令,可以更新指定节点的数据内容。用法如下:set path data [version]
其中, data就是要更新的新内容。注意, set命令后面还有一个version参数,在ZooKeeper中,节点的数据是有版本概念的,这个参数用于指定本次更新操作是基于ZNode的哪一个数据版本进行的。执行如下命令:
1 | [zk: localhost:2181(CONNECTED) 3] set /zk-book 456 |
5.2.4删除
使用delete命令,可以删除ZooKeeepr上的指定节点。用法如下:delete path [version]
此命令中的version参数和set命令中的version参数的作用是一致的。执行如下命令:
1 | [zk: localhost:2181(CONNECTED) 5] delete /zk-book |
执行完以上命令后,就可以把/zk-book这个节点成功删除了。但是这里要注意的一点是,要想删除某一个指定节点,该节点必须没有子节点存在。
5.3 Java户端AP使用
ZooKeeper作为一个分布式服务框架,主要用来解决分布式数据一致性问题,它提供了简单的分布式原语,并且对多种编程语言提供了API,下面我们重点来看下Zookeeper的Java客户端API使用方式。
5.3.1创建会话
这里自己用SpringBoot集成了Zookeeper,代码还是书里的,只是加了一些Java8 的写法。
配置文件https://gitee.com/liruilonger/zookeeper_demo/blob/master/src/main/resources/application.yml
1 | zookeeper: |
1 |
|
Zookeeper构造方法
1 | public ZooKeeper(String, int, Watcher) throws java.io.IOException |

注意, ZooKeeper客户端和服务端会话的建立是一个异步的过程,也就是说在程序中,”构造方法会在处理完客户端初始化工作后立即返回,在大多数情况下,此时并没有真正建立好一个可用的会话,在会话的生命周期中处于”CONNECTING“的状态。当该会话真正创建完毕后, Zookeeper服务端会向会话对应的客户端发送一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话。该构造方法内部实现了与ZooKeeper服务器之间的TCP连接创建,负责维护客户端会话的生命周期。
创建一个复用sessionId和sessionPasswd的ZooKeeper对象实例
ZooKeeper客户端构造方法中,我们看到Zookeeper构造方法允许传入sessionId和sessionPasswd-客户端传入sessionId和sessionPasswd的目的是为了复用会话,以维持之前会话的有效性。(这个输出结果和书里不一样,原因以后遇到这细究)
1 | //Chapter: 5.3.1 Java API -> 创建连接 -> 创建一个最基本的ZooKeeper对象实例,复用sessionId和 |
5.3.2创建节点
客户端可以通过ZooKeeper的API来创建一个数据节点,有如下6个接口:
1 | public java.lang.String org.apache.zookeeper.ZooKeeper.create(java.lang.String,byte[],java.util.List,org.apache.zookeeper.CreateMode) throws org.apache.zookeeper.KeeperException,java.lang.InterruptedException |
返回String的为同步创建节点,返回void的为异步创建节点。
1 | public void org.apache.zookeeper.ZooKeeper.create(java.lang.String,byte[],java.util.List,org.apache.zookeeper.CreateMode,org.apache.zookeeper.AsyncCallback$Create2Callback,java.lang.Object) |

1 |
|
1 | Success create znode: /zk-test-ephemeral-sync |
- 使用了同步的节点创建接口中,我们分别创建了两种类型的节点:
临时节点和临时顺序节点。 - 如果创建了
临时节点,那么API的返回值就是当时传入的path参数 - 如果创建了
临时顺序节点,那么ZooKeeper会自动在节点后缀加上一个数字,并且在API接口的返回值中返回该数据节点的一个完整的节点路径。
1 | zookeeper.create("/zk-test-ephemeral-async", "123".getBytes(), |
1 | Create path result: [0, /zk-test-ephemeral-, I am context., real path name: /zk-test-ephemeral- |
- 使用异步方式创建接口用户需要实现
AsyncCallback.StringCallback()接口即可。AsyncCallback包含了StatCallback, Datacallback, ACLCallback, ChildrenCallbackhildren2Callback, stringCallback和VoidCalLback七种不同的回调接口,用户可以在不同的异步接口中实现不同的接口。 - 和同步接口方法最大的区别在于,节点的创建过程(包括网络通信和服务端的节点创建过程)是异步的。并且,在同步接口调用过程中,我们需要关注接口抛出异常的可能;但是在异步接口中,接口本身是不会抛出异常的,所有的异常都会在回调函数中通过Result Code (响应码)来体现。下面来重点看下回调方法:
void processResult(int rc, String path,object ctx, String name),这个方法的几个参数:

5.3.3删除节点
在Zookeeper中,只允许删除叶子节点。也就是说,如果一个节点存在至少一个子节点的话,那么该节点将无法被直接删除,必须先删除掉其所有子节点。
客户端可以通过ZooKeeper的API来删除一个节点,有如下两个接口:
1 | public void org.apache.zookeeper.ZooKeeper.delete(java.lang.String,int,org.apache.zookeeper.AsyncCallback$VoidCallback,java.lang.Object) |

1 | String path1 = zookeeper.create("/zk-test-ephemeral-sync", |
5.3.4读取数据
读取数据,包括子节点列表的获取和节点数据的获取。Zookeeper分别提供了不同的API来获取数据。
- 注册Watcher,如果
ZooKeeper客户端在获取到指定节点的子节点列表后,还需要订阅这个子节点列表的变化通知,那么就可以通过注册一个Watcher来实现。当有子节点被添加或是删除时,服务端就会向客户端发送一个NodeChildrenChanged (EventType.NodeChildrenChanged)类型的事件通知。需要注意的是,在服务端发送给客户端的事件通知中,是不包含最新的节点列表的,客户端必须主动重新进行获取。通常客户端在收到这个事件通知后,就可以再次获取最新的子节点列表了。

- stat, stat对象中记录了一个节点的
基本属性信息,例如节点创建时的事务ID (cZxid)、最后一次修改的事务ID (mZxid)和节点数据内容的长度(dataLength)等。有时候,我们不仅需要获取节点最新的子节点列表,还要获取这个节点最新的节点状态信息。对于这种情况,我们可以将一个旧的stat变量传入API接口,该stat变量会在方法执行过程中,被来自服务端响应的新stat对象替换。
使用同步API获取子节点列表
1 | public java.util.List org.apache.zookeeper.ZooKeeper.getChildren(java.lang.String,boolean,org.apache.zookeeper.data.Stat) throws org.apache.zookeeper.KeeperException,java.lang.InterruptedException |
1 | public static void main(String[] args) throws Exception{ |
1 | [c1] |
关于Watcher,ZooKeeper服务端在向客户端发送Watcher”NodeChildrenChanged”事件通知的时候,仅仅只会发出一个通知,而不会把节点的变化情况发送给客户端,需要客户端自己重新获取。另外,由于Watcher通知是一次性的,即一旦触发一次通知后,该Watcher就失效了,因此客户端需要反复注册Watcher.
使用异步AP1获取子节点列表
1 | public void org.apache.zookeeper.ZooKeeper.getChildren(java.lang.String,org.apache.zookeeper.Watcher,org.apache.zookeeper.AsyncCallback$ChildrenCallback,java.lang.Object) |
1 | public static void main(String[] args) throws Exception{ |
1 | Get Children znode result: [response code: 0, param path: /zk-book, ctx: null, children list: [c1], stat: 141,141,1625245212016,1625245212016,0,1,0,0,0,1,142 |
getData
客户端可以通过Zookeeper的API来获取一个节点的数据内容,有如下4个接口:

getData接口和上文中的getChildren接口的用法基本相同
同步方法:https://gitee.com/liruilonger/zookeeper_demo/blob/master/src/main/java/com/liruilong/zookeeper_demo/book_demo/GetData_API_Sync_Usage.java
1 | public static void main(String[] args) throws Exception { |
1 | 123 |
1 | public static void main(String[] args) throws Exception { |
1 | 0, /zk-book, 123 |
5.3.5更新数据
客户端可以通过ZooKeeper的API来更新一个节点的数据内容:

指定数据版本的意义:
在讲解这个问题之前,我们首先来看下CAS (Compare and Swap)理论的相关知识。CAS的意思就是: “对于值v,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B.” ZooKeeper的setData接口中的version参数正是由CAS原理衍化而来的。
从前面的介绍中,我们已经了解到, ZooKeeper每个节点都有数据版本的概念,在调用更新操作的时候,就可以添加version这个参数,该参数可以对应于CAS原理中的“预期值”,表明是针对该数据版本进行更新的。具体来说,假如一个客户端试图进行更新操作,它会携带上次获取到的version值进行更新。而如果在这段时间内, ZooKeeper服务器上该节点的数据恰好已经被其他客户端更新了,那么其数据版本一定也发生了变化,因此肯定与客户端携带的version无法匹配,于是便无法更新成功-因此可以有效地避免一些分布式更新的并发问题,
1 | public static void main(String[] args) throws Exception { |
1 | public static void main(String[] args) throws Exception { |
版本“-1”代表了什么:在ZooKeeper中,数据版本都是从0开始计数的,所以严格地讲, “-1”并不是一个合法的数据版本,它仅仅是一个标识符,如果客户端传入的版本参数是“-1”,就是告诉ZooKeeper服务器,客户端需要基于数据的最新版本进行更新操作。如果对ZooKeeper数据节点的更新操作没有原子性要求,那么就可以使用“-1”
5.3.6检测节点是否存在
1 | public static void main(String[] args) throws Exception { |
1 | Node(/zk-book)Created |
5.3.7枚限控制
ZooKeeper提供了多种权限控制模式(Scheme),分别是world, auth, digest, ip和super,在本节中,我们主要讲解在digest模式下如何进行ZooKeeper的权限控制。开发人员如果要使用Zookeeper的权限控制功能,需要在完成Zookeeper会话创建后,给该会话添加上相关的权限信息(AuthInfo), ZooKeeper客户端提供了相应的API接口来进行权限信息的设置,如下:

这里书里的代码有问题,以仓库代码为准。
全部代碼見:https://gitee.com/liruilonger/zookeeper_demo/tree/master/src/main/java/com/liruilong/zookeeper_demo/book_demo/auth
5.4开源客户端
5.4.1 ZkClient
ZkClient是Github上一个开源的ZooKeeper客户端,是由Datameer的工程师StefanGroschupf和Peter Voss一起开发的。ZkClient在ZooKeeper原生API接口之上进行了包装,是一个更易用的ZooKeeper客户端。同时, ZkClient在内部实现了诸如Session超时重连、Watcher反复注册等功能,使得ZooKeeper客户端的这些繁琐的细节工作对开发人员透明。
這裏先不看了,以後用到在學習:
5.4.2 Curator
Curator是Netlix公司开源的一套ZooKeeper客户端框架,作者是Jordan Zimmermans和ZkClient一样, Curator解决了很多ZooKeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等, 目前已经成为了Apache的顶级项目,是全世界范围内使用最广泛的ZooKeeper客户端之一, Curator还在ZooKeeper原生API的基础上进行了包装,提供了一套易用性和可读性更强的Fluent风格的客户端API框架。除此之外, Curator中还提供了ZooKeeper各种应用场景(Recipe,如共享锁服务、Master选举机制和分布式计数器等)的抽象封装。
第6章Zookeeper的典型应用场景
ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布与订阅。
通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、负载均衡,命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等。
6.1 典型应用场景及实现
ZooKeeper是一个高可用的分布式数据管理与协调框架。基于对ZAB算法的实现,该框架能够很好地保证分布式环境中数据的一致性。也正是基于这样的特性,使得ZooKeeper成为了解决分布式一致性问题的利器
。
6.1.1数据发布/订阅
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中心,顾名思义就是 发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。
- 在推模式中,服务端主动将数据更新发送给所有订阅的客户端,
- 拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。
ZooKeeper采用的是推拉相结合的方式:
客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
配置管理的实际案例:
展示ZooKeeper在“数据发布/订阅”场景下的使用方式。在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性。
| 全局配置信息通常具备以下3个特性 |
|---|
| 数据量通常比较小。 |
| 数据内容在运行时会发生动态变化。 |
| 集群中各机器共享,配置一致。 |
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。
| 配置信息存储位置 | 描述 |
|---|---|
| 本地配置文件 | 如果采用本地配置文件的方式,那么通常系统可以在应用启动的时候读取到本地磁盘的一个文件来进行初始化. |
| 内存变量 | 借助内存变量来实现配置管理的方式也非常简单,以Java系统为例,通常可以采用JMX方式来实现对系统运行时内存变量的更新 |
接下去我们就以一个“数据库切换”的应用场景展开,看看如何使用Zookeeper来实现配置管理。
| 步骤 | 描述 |
|---|---|
| 配置存储: | 在进行配置管理之前,首先我们需要将初始化配置存储到ZooKeeper上去。一般情,况下,我们可以在Zookeeper上选取一个数据节点用于配置的存储. |
| 配置获取: | 集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。 |
| 配置变更: | 在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。 |
6.1.2负载均衡
负载均衡可以分为硬件和软件负载均衡两类,Zookeeper在“软”负载均衡中的应用场景
分布式系统具有对等性,为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。而对于服务消费者而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务逻辑,其中此较典型的就是DNS服务。
通常情况下,我们可以向域名注册服务商申请域名注册,但是这种方式最大的缺陷在于只能注册有限的域名:
因此,在实际开发中,往往使用本地HOST绑定来实现域名解析的工作。
1 | [root@liruilong ~]# more /etc/hosts |
缺点:我们在应用上线的时候,需要在应用的每台机器上去绑定域名,但是在机器规模相当庞大的情况下,这种做法就相当不方便。另外,如果想要临时更新城名,还需要到每个机器上去逐个进行变更,要消耗大量时间,因此完全无法保证实时性。其实可以使用Ansible等运自动化维工具解决。
基于ZooKeeper实现的动态DNS方案(以下简称该方案为”DDNS” , Dynamic DNS)。
| 步骤 | 描述 |
|---|---|
| 域名配置 | 和配置管理一样,我们首先需要在ZooKeeper上创建一个节点来进行域名配置 |
| 域名解析 | DDNS方案和传统的域名解析有很大的区别–在DDNS中,域名的解析过程都是由每一个应用自己负责的。通常应用都会首先从域名节点中获取一份IP地址和端口的配置,进行自行解析。同时,每个应用还会在域名节点上注册一个数据变更Watcher监听,以便及时收到域名变更的通知。 |
| 域名变更 | 在DDNS中,我们只需要对指定的域名节点进行更新操作, ZooKeeper就会向订阅的客户端发送这个事件通知,应用在接收到这个事件通知后,就会再次进行域名配置的获取。 |
通过ZooKeeper来实现动态DNS服务:
- 可以避免域名数量无限增长带来的
集中式维护的成本; - 在
域名变更的情况下,也能够避免因逐台机器更新本地HOST而带来的繁琐工作。

首先来介绍整个动态DNS系统的架构体系中几个比较重要的组件及其职责。
Register集群负责域名的动态注册。Dispatcher集群负责域名解析。Seanner集群负责检测以及维护服务状态(探测服务的可用性、屏蔽异常服务节点等)。SDK提供各种语言的系统接入协议,提供服务注册以及查询接口。Monitor负责收集服务信息以及对DDNS自身状态的监控。ontroller是一个后台管理的Console,负责授权管理、流量控制、静态配置服务和手动屏蔽服务等功能,另外,系统的运维人员也可以在上面管理RegisterDispatcher和Scanner等集群。
整个系统的核心当然是ZooKeeper集群,负责数据的存储以及一系列分布式协调。下面我们再来详细地看下整个系统是如何运行的。在这个架构模型中,我们将那些目标IP地址和端口抽象为服务的提供者,而那些需要使用域名解析的客户端则被抽象成服务的消费者。
域名注册
域名注册主要是针对服务提供者来说的。域名注册过程可以简单地概括为:每个服务提供者在启动的过程中,都会把自己的域名信息注册到Register集群中去。
- 服务提供者通过SDK提供的API接口,将域名、IP地址和端口发送给Register
集群。例如, A机器用于提供serviceA.xxx.com,于是它就向Register发送一个“域名->IP:PORT”的映射: “serviceA.xx.com-192.168.0.1:8080” - Register获取到域名、IP地址和端口配置后,根据城名将信息写入相对应的Zookeeper域名节点中。
域名解析
域名解析是针对服务消费者来说的,正好和域名注册过程相反:服务消费者在使用域名的时候,会向Dispatcher发出域名解析请求。Dispatcher收到请求后,会从ZooKeeper上的指定域名节点读取相应的1P:PORT列表,通过一定的策略选取其中一个返回给前端应用。
域名探测
域名探测是指DDNS系统需要对域名下所有注册的IP地址和端口的可用性进行检测,俗称“健康度检测”。健康度检测一般有两种方式,
- 第一种是
服务端主动发起健康度心跳检测,这种方式一般需要在服务端和客户端之间建立起一个TCP长链接; - 第二种则是
客户端主动向服务端发起健康度心跳检测。在DDNS架构中的域名探测,使用的是服务提供者主动向Scanner进行状态汇报(即第二种健康度检测方式)的模式,即每个服务提供者都会定时向Scanner汇报自己的状态。Scanner会负责记录每个服务提供者最近一次的状态汇报时间,一旦超过5秒没有收到状态汇报,那么就认为该IP地址和端口已经不可用,于是开始进行域名清理过程。在域名清理过程中,Scanner会在ZooKeeper中找到该域名对应的域名节点,然后将该IP地址和端口配置从节点内容中移除。
6.1.3命名服务
Java语言中的JNDI便是一种典型的命名服务。JNDI是Java命名与目录接口(JavaNaming and Directory Interface)的缩写,是J2EE体系中重要的规范之一,标准的J2EE容器都提供了对JNDI规范的实现。
Zookeeper提供的命名服务功能与JNDI技术有相似的地方,都能够帮助应用系统通过一个资源引用的方式来实现对资源的定位与使用。另外,广义上命名服务的资源定位都不是真正意义的实体资源-在分布式环境中,上层应用仅仅需要一个全局唯一的名字,类似于数据库中的唯一主键。下面我们来看看如何使用ZooKeeper来实现一套分布式全局唯一ID的分配机制。
一说起全局唯一ID,相信读者都会联想到UUID, UUID是通用唯一识别码(Universally Unique Identifier)的简称,是一种在分布式系统中广泛使用的用于唯一标识元素的标准,最典型的实现是GUID (Globally Unique Identifier,全局唯一标识符),缺点:
- 长度过长:UUID最大的问题就在于生成的字符串过长。显然,和数据库中的INT类型相比,存储一个UUID需要花费更多的空间。
- 含义不明:一个典型的UUID是类似于”e70f1357-f260-46ff-a32d53a086c57ade”的一个字符串。
一个分布式任务调度系统来看看如何使用 ZooKeeper来实现这类全局唯一ID的生成。
通过调用Zookeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。
1 | [zk: localhost:2181(CONNECTED) 1] create -s -e /zk-liruilong |
6.1.4分布式协调/通知
ZooKeeper中特有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。基于ZooKeeper实现分布式协调与通知功能,通常的做法是 **不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发生变化,那么所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理**。
MySQL数据复制总线: Mysq-Replicator
嗯,这个以后在看… :)
6.1.5集群管理
嗯,这个以后在看… :)
6.1.6 Master选举
嗯,这个以后在看… :)
6.1.7 分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。
在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是一种非常简便且被广泛使用的分布式锁实现方式。然而有一个不争的事实是,目前绝大多数大型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给数据库添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,]会让数据库更加不堪重负
我们来看看使用ZooKeeper如何实现分布式锁,这里主要讲解排他锁和共享锁两类分布式锁。
排他锁
排他锁(Exclusive Locks,简称x锁),又称为写锁或独占锁,是一种基本的锁类型。
如果事务T,对数据对象0,加上了排他锁,那么在整个加锁期间,只允许事务T,对0进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作直到T释放了排他锁。
从上面讲解的排他锁的基本概念中,我们可以看到,排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。
定义锁
如何借助ZooKeeper实现排他锁。定义锁在通常的Java开发编程中,有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock,然而,在ZooKeeper中,没有类似于这样的API可以直接使用,而是通过ZooKeeper上的数据节点来表示一个锁,例如/exclusivelock/lock节点就可以被定义为一个锁。
1 |
获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在lexclusive lock节点下创建临时子节点/exclusive lockllock,在前面几节中我们也介绍了,Zookeeper会保证在所有的客户端中,最终只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusivelock节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。
在“定义锁”部分,我们已经提到, /exclusivelock/lock 是一个临时节点,因此在以下两种情况下,都有可能释放锁。
- 当前获取锁的客户端机器发生
宕机,那么Zookeeper上的这个临时节点就会被移除。 - 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除。
无论在什么情况下移除了lock节点, ZooKeeper都会通知所有在/exclusive lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。整个排他锁的获取和释放流程,可以用图6-15来表示。
6.1.8 分布式队列
6.2 ZooKeeper在大型分布式系统中的应用
6.2.1 Hadoop
6.2.2 HBase
6.2.3 Kafka
6.3 ZooKeeper在阿里巴巴的实践与应用小结
6.3.1案例一消息中间件: Metamorphosis
6.3.2案例二RPC服务框架: Dubbo
6.3.3案例三基于MySQL Binlog的增量订阅和消费组件:Canal
6.3.4案例四分布式数据库同步系统: Otter
6.3.5案例伍轻量级分布式通用搜索平台:终搜
6.3.6案例六实时计算引擎: JStorm
第7章Zookeeper技术内幕
第8章Zookeeper运维
《从Paxos到Zookper分布式一致性原理与实践》读书笔记
https://liruilongs.github.io/2021/06/30/Java/《从Paxos到Zookper分布式一致性原理与实践》读书笔记/



