分布式架构是分布式计算技术的应用和工具,目前成熟的技术包括J2EE, CORBA和.NET(DCOM),这些技术牵扯的内容非常广,相关的书籍也非常多,也没有涉及这些技术的细节,只是从各种分布式系统平台产生的背景和在软件开发中应用的情况来探讨它们的主要异同。
微服务架构是一项在云中部署应用和服务的新技术。大部分围绕微服务的争论都集中在容器或其他技术是否能很好的实施微服务,而红帽说API应该是重点。(推荐学习:Java视频教程)
微服务可以在“自己的程序”中运行,并通过“轻量级设备与HTTP型API进行沟通”。关键在于该服务可以在自己的程序中运行。通过这一点我们就可以将服务公开与微服务架构(在现有系统中分布一个API)区分开来。在服务公开中,许多服务都可以被内部独立进程所限制。如果其中任何一个服务需要增加某种功能,那么就必须缩小进程范围。在微服务架构中,只需要在特定的某种服务中增加所需功能,而不影响整体进程的架构。
从概念理解,分布式服务架构强调的是服务化以及服务的分散化,微服务则更强调服务的专业化和精细分工;从实践的角度来看,微服务架构通常是分布式服务架构,反之则未必成立。所以,选择微服务通常意味着需要解决分布式架构的各种难题。
区别分布式的方式是根据不同机器不同业务。
将一个大的系统划分为多个业务模块,业务模块分别部署到不同的机器上,各个业务模块之间通过接口进行数据交互。区别分布式的方式是根据不同机器不同业务。
微服务更加强调单一职责、轻量级通信(HTTP)、独立性并且进程隔离。
微服务与分布式的细微差别是,微服务的应用不一定是分散在多个服务器上,他也可以是同一个服务器。
分布式是否属于微服务?
不一定,如果一个很大应用,拆分成三个应用,但还是很庞大,虽然分布式了,但不是微服务。。微服务核心要素是微小。。
微服务架构是分布式服务架构的子集。
微服务架构通过更细粒度的服务切分,使得整个系统的迭代速度并行程度更高,但是运维的复杂度和性能会随着服务的粒度更细而增加。
微服务重在解耦合,使每个模块都独立。分布式重在资源共享与加快计算机计算速度。
分布式:分散压力。微服务:分散能力。
课程介绍
微服务架构的技术体系、社区目前已经越来越成熟。在最初系统架构的搭建,或者当现有架构已到达瓶颈需要进行架构演进时,很多架构师、运维工程师会考虑是否需要搭建微服务架构体系。虽然很多文章都说微服务架构是复杂的、会带来很多分布式的问题,但只要我们了解这些问题,并找到解法,就会有种拨开云雾的感觉。
微服务架构也不是完美的,世上没有完美的架构,微服务架构也是随着业务、团队成长而不断演进的。最开始可能就几个、十几个微服务,每个服务是分库的,通过 API Gateway 并行进行服务数据合并、转发。随着业务扩大、不断地加入搜索引擎、缓存技术、分布式消息队列、数据存储层的数据复制、分区、分表等。
本课程会一一解开微服务架构下分布式场景的问题,以及通过对于一些分布式技术的原理、模型和算法的介绍,来帮助想要实施微服务架构的工程师们知其然并知其所以然。
课程内容
微服务架构的演变
微服务是一种服务间松耦合的、每个服务之间高度自治并且使用轻量级协议进行通信的可持续集成部署的分布式架构体系。这一句包含了微服务的特点,微服务架构和其他架构有什么区别?以下对比一些常见的架构。
单体架构
单体架构是最简单的软件架构,常用于传统的应用软件开发以及传统 Web 应用。传统 Web 应用,一般是将所有功能模块都打包(jar、war)在一个 Web 容器(JBoss、Tomcate)中部署、运行。随着业务复杂度增加、技术团队规模扩大,在一个单体应用中维护代码,会降低开发效率,即使是处理一个小需求,也需要将所有机器上的应用全部部署一遍,增加了运维的复杂度。
SOA 架构
当某一天使用单体架构发现很难推进需求的开发、以及日积月累的技术债时,很多企业会开始做单体服务的拆分,拆分的方式一般有水平拆分和垂直拆分。垂直拆分是把一个应用拆成松耦合的多个独立的应用,让应用可以独立部署,有独立的团队进行维护;水平拆分是把一些通用的,会被很多上层服务调用的模块独立拆分出去,形成一个共享的基础服务,这样拆分可以对一些性能瓶颈的应用进行单独的优化和运维管理,也在一定程度上防止了垂直拆分的重复造轮子。
SOA 也叫面向服务的架构,从单体服务到 SOA 的演进,需要结合水平拆分及垂直拆分。SOA 强调用统一的协议进行服务间的通信,服务间运行在彼此独立的硬件平台但是需通过统一的协议接口相互协作,也即将应用系统服务化。举个易懂的例子,单体服务如果相当于一个快餐店,所有的服务员职责都是一样的,又要负责收银结算,又要负责做汉堡,又要负责端盘子,又要负责打扫,服务员之间不需要有交流,用户来了后,服务员从前到后负责到底。SOA 相当于让服务员有职责分工,收银员负责收银,厨师负责做汉堡,保洁阿姨负责打扫等,所有服务员需要用同一种语言交流,方便工作协调。
微服务和 SOA
微服务也是一种服务化,不过其和 SOA 架构的服务化概念也是有区别的,可以从以下几个关键字来理解:
松耦合:每个微服务内部都可以使用 DDD(领域驱动设计)的思想进行设计领域模型,服务间尽量减少同步的调用,多使用消息的方式让服务间的领域事件来进行解耦。
轻量级协议:Dubbo 是 SOA 的开源的标准实现之一,类似的还有像 gRPC、Thrift 等。微服务更倾向于使用 Restful 风格的 API,轻量级的协议可以很好得支持跨语言开发的服务,可能有的微服务用 Java 语言实现,有的用 Go 语言,有的用 C++,但所有的语言都可以支持 Http 协议通信,所有的开发人员都能理解 Restful 风格 API 的含义。
高度自治和持续集成:从底层的角度来说,SOA 更加倾向于基于虚拟机或者服务器的部署,每个应用都部署在不同的机器上,一般持续集成工具更多是由运维团队写一些 Shell 脚本以及提供基于共同协议(比如 Dubbo 管理页面)的开发部署页面。微服务可以很好得和容器技术结合,容器技术比微服务出现得晚,但是容器技术的出现让微服务的实施更加简便,目前 Docker 已经成为很多微服务实践的基础容器。因为容器的特色,所以一台机器上可以部署几十个、几百个不同的微服务。如果某个微服务流量压力比其他微服务大,可以在不增加机器的情况下,在一台机器上多分配一些该微服务的容器实例。同时,因为 Docker 的容器编排社区日渐成熟,类似 Mesos、Kubernetes 及 Docker 官方提供的 Swarm 都可以作为持续集成部署的技术选择。
其实从架构的演进的角度来看,整体的演进都是朝着越来越轻量级、越来越灵活的应用方向发展,甚至到近两年日渐成熟起来的 Serverless(无服务)架构。从单体服务到分层的服务,再到面向服务、再到微服务甚至无服务,对于架构的挑战是越来越大。
微服务架构和分布式
微服务架构属于分布式系统吗?答案是肯定的。微服务和 SOA 都是典型的分布式架构,只不过微服务的部署粒度更细,服务扩展更灵活。
理解微服务中的分布式
怎样理解微服务中的分布式?举一个招聘时一个同学来面试的例子。A 同学说,目前所在公司在做从单应用到微服务架构迁移,已经差不多完成了。提到微服务感觉就有话题聊了,于是便问“是否可以简单描述下服务拆分后的部署结构、底层存储的拆分、迁移方案?”。于是 A 同学说,只是做了代码工程结构的拆分,还是原来的部署方式,数据库还是那个库,所有的微服务都用一个库,分布式事务处理方式是“避免”,尽量都同步调用……于是我就跟这位同学友好地微笑说再见了。
微服务的分布式不仅仅是容器应用层面的分布式,其为了高度自治,底层的存储体系也应该互相独立,并且也不是所有的微服务都需要持久化的存储服务。一个“手机验证码”微服务可能底层存储只用一个 Redis;一个“营销活动搭建页面”微服务可能底层存储只需要一个 MongoDB。
微服务中的分布式场景除了服务本身需要有服务发现、负载均衡,微服务依赖的底层存储也会有分布式的场景:为了高可用性和性能需要处理数据库的复制、分区,并且在存储的分库情况下,微服务需要能保证分布式事务的一致性。
课程背景
微服务架构的技术体系、社区目前已经越来越成熟,所以在初期选择使用或者企业技术体系转型微服务的时候,需要了解微服务架构中的分布式的问题:
1.在所有服务都是更小单元的部署结构时,一个请求需要调动更多的服务资源,怎样获得更好的性能?
2.当业务规模增大,需要有地理分布不同的微服务集群时,其底层的数据存储集群是多数据中心还是单数据集群?
3.数据存储如何进行数据复制?
4.业务数据达到大数据量时怎样进行数据的分区?
5.分布式事务怎样保证一致性?
6.不同程度的一致性有什么差别?
7.基于容器技术的服务发现怎么处理?
8.应该用哪些 RPC 技术,用哪些分布式消息队列来完成服务通信和解耦?
9.那么多的分布式技术框架、算法、服务应该选哪个才适合企业的业务场景?
本课程从微服务不得不面对和解决的分布式问题出发,包含分布式技术的一系列理论以及架构模型、算法的介绍,同时结合技术选型和实践应用,提供一系列解决方案的梳理。相信阅读完整个课程,你会对微服务的分布式问题有个系统地理解。本课程会对微服务的分布式场景问题一一击破,为你提供解决思路。
课程内容
本课程示例代码地址如下:
1.Microservice
2.API-gateway
分布式系统的问题
引出分布式系统的可能问题:节点故障、网络延迟,结合错误检测的可行方案进行介绍;
分布式中的时间和顺序的问题,以及标量时钟和向量时钟的实现。
分布式数据存储
分布式数据存储的技术选型、关系型数据库以及一些流行的 NoSQL 技术介绍(MongoDB、Redis、Neo4j 及 Cassandra 等);
分布式存储技术使用的数据结构,了解底层数据存储原理(HashTable、SSTable、LSMTree、BTree 等);
各个存储方案使用的场景以及对比。
数据复制
对于大规模存储集群,需要进行数据库的复制、排除单点故障;
数据复制的模型和实现以及几种复制日志的实现方式;
主备同步、主主复制、多数据中心的数据复制方案;
数据复制中的读写一致性问题以及写冲突问题的解决;
介绍以 MySQL 为例延伸集群数据复制方案。
数据分区
当单个领域模型维度的数据已经到一定规模时,需要进行数据分区,减轻单库压力。数据分区和分表又有哪些不同?数据分区可以如何实现?
以 MySQL 的分区策略为例介绍不同分区策略的实现。
数据分区后,请求的路由有哪些解决方案?会展开介绍不同的方案又有什么差别。
服务发现和服务通信
基于容器技术的微服务体系,怎样选择服务发现、负载均衡的技术实现?不同的服务发现的技术有什么区别,如何选型?
为了达到松耦合的目的以及基于 DDD 思想,微服务之间减少服务调用可以通过哪些技术实现?API Gateway 可以用哪些技术框架实现?远程调用可以有哪些技术框架?怎样提高同步通信的性能?
分布式的消息队列都有哪些开源、商业实现?应该怎样选择适合的消息队列?
使用 DDD 思想应该如何应对服务通信,如何在实践中应用 DDD?
分布式存储集群的事务
理解分布式中的事务以及本地事务的基础概念;
分布式存储的隔离级别以及各个 DB 支持的隔离方案的实现原理;
以 MySQL InnoDB 中的 MVCC 为例看并发控制的在 MySQL 的实现,学习存储系统对于分布式事务的实现思想。
分布式一致性
了解分布式系统的一致性有哪些问题以及一致性的几种实现程度的模型:线性一致性(强一致性)、顺序一致性及因果一致性、最终一致性;
分布式一致性相关的理论 CAP(CA、CP、AP 的相关算法)的介绍以及适合用于哪些实践;
介绍 FLP 不可能结果,以及 BASE 理论。
分布式事务实践
了解微服务中分布式事务的问题;
1.介绍强一致性的实践:二阶段、三阶段。2PC、3PC 的优缺点和限制,XA 协议的介绍和实践方案,以及最终一致性实践:TCC 模型和实践方案;
2.分布式锁的实现模型和实践方案;
3.基于微服务下的分布式事务实践案例分析。
共识问题
了解为什么分布式场景下有共识问题;
介绍共识算法和全局消息广播的实现,公式算法的基础:leader 选举和 quorum 算法,以及一些已实现的算法的介绍和对比:VSR、Raft、Paxos、ZAB;
共识算法在微服务体系的应用场景介绍:服务发现、一致性 kv 存储(Etcd、Zk)以及技术的选型如何权衡一致性的追求和性能。
架构设计
了解了很多分布式的问题和解决方案之后,回归微服务架构模型、技术选型、再回顾下微服务的弊端和特点;
微服务体系的架构要素分析:安全、伸缩性、性能、可用性、扩展性;
结合团队、业务场景的 DDD 实践和总结。
课程寄语
如果你是一位开发工程师,相信阅读完本系列课程,将会了解很多分布式系统的理论知识,同时也会理解一些分布式存储、中间件技术的原理,对工作中的分布式架构会有体系化的清晰认知。
如果你是一位架构师,本系列课程提供了对于分布式系统问题的全面梳理,以及一些技术背后的理论,结合实践和目前业界先进的方案,对于技术选型和架构搭建提供了参考。
第01课:分布式系统的问题
前言
无论是 SOA 或者微服务架构,都是必须要面对和解决一些分布式场景下的问题。如果只是单服务、做个简单的主备,那么编程则会成为一件简单幸福的事,只要没有 bug,一切都会按照你的预期进行。然而在分布式系统中,如果想当然的去按照单服务思想编程和架构,那可能会收获很多意想不到的“惊喜”:网络延迟导致的重复提交、数据不一致、部分节点挂掉但是任务处理了一半等。在分布式系统环境下编程和在单机器系统上写软件最大的差别就是,分布式环境下会有很多很“诡异”的方式出错,所以我们需要理解哪些是不能依靠的,以及如何处理分布式系统的各种问题。
理想和现实
微服务架构跟 SOA 一样,也是服务化的思想。在微服务中,我们倾向于使用 RESTful 风格的接口进行通信,使用 Docker 来管理服务实例。我们的理想是希望分布式系统能像在单个机器中运行一样,就像客户端应用,再坏的情况,用户只要一键重启就可以重新恢复,然而现实是我们必须面对分布式环境下的由网络延迟、节点崩溃等导致的各种突发情况。
在决定使用分布式系统,或者微服务架构的时候,往往是因为我们希望获得更好的伸缩性、更好的性能、高可用性(容错)。虽然分布式系统环境更加复杂,但只要能了解分布式系统的问题以及找到适合自己应用场景的方案,便能更接近理想的开发环境,同时也能获得伸缩性、性能、可用性。
分布式系统的可能问题
分布式系统从结构上来看,是由多台机器节点,以及保证节点间通信的网络组成,所以需要关注节点、网络的特征。
(1)部分失败
在分布式环境下,有可能是节点挂了,或者是网络断了,如下图:
如果系统中的某个节点挂掉了,但是其他节点可以正常提供服务,这种部分失败,不像单台机器或者本地服务那样好处理。单机的多线程对象可以通过机器的资源进行协调和同步,以及决定如何进行错误恢复。但在分布式环境下,没有一个可以来进行协调同步、资源分配以及进行故障恢复的节点,部分失败一般是无法预测的,有时甚至无法知道请求任务是否有被成功处理。
所以在开发需要进行网络通讯的接口时(RPC 或者异步消息),需要考虑到部分失败,让整个系统接受部分失败并做好容错机制,比如在网络传输失败时要能在服务层处理好,并且给用户一个好的反馈。
(2)网络延迟
网络是机器间通信的唯一路径,但这条唯一路径并不是可靠的,而且分布式系统中一定会存在网络延迟,网络延迟会影响系统对于“超时时间”、“心跳机制”的判断。如果是使用异步的系统模型,还会有各种环节可能出错:可能请求没有成功发出去、可能远程节点收到请求但是处理请求的进程突然挂掉、可能请求已经处理了但是在 Response,可能在网络上传输失败(如数据包丢失)或者延迟,而且网络延迟一般无法辨别。
即使是 TCP 能够建立可靠的连接,不丢失数据并且按照顺序传输,但是也处理不了网络延迟。对于网络延迟敏感的应用,使用 UDP 会更好,UDP 不保证可靠传输,也不会丢失重发,但是可以避免一些网络延迟,适合处理音频和视频的应用。
(3)没有共享内存、锁、时钟
分布式系统的节点间没有共享的内存,不应理所当然认为本地对象和远程对象是同一个对象。分布式系统也不像单机器的情况,可以共享同一个 CPU 的信号量以及并发操作的控制;也没有共享的物理时钟,无法保证所有机器的时间是绝对一致的。时间的顺序是很重要的,夸张一点说,假如对于一个人来说,从一个时钟来看是 7 点起床、8 点出门,但可能因为不同时钟的时间不一致,从不同节点上来看可能是 7 点出门、8 点起床。
在分布式环境下开发,需要我们能够有意识地进行问题识别,以上只是举例了一部分场景和问题,不同的接口实现,会在分布式环境下有不同的性能、扩展性、可靠性的表现。下面会继续上述的问题进行探讨,如何实现一个更可靠的系统。
概念和处理模型
对于上述分布式系统中的一些问题,可以针对不同的特征做一些容错和处理,下面主要看一下错误检测以及时间和顺序的处理模型。在实际处理中,一般是综合多个方案以及应用的特点。
错误检测
对于部分失败,需要一分为二的看待。
节点的部分失败,可以通过增加错误检测的机制,自动检测问题节点。在实际的应用中,比如有通过 Load Balancer,自动排除问题节点,只将请求发送给有效的节点。对于需要有 Leader 选举的服务集群来说,可以引入实现 Leader 选举的算法,如果 Leader 节点挂掉了,其余节点能选举出新的 Leader。实现选举算法也属于共识问题,在后续文章中会再涉及到几种算法的实现和应用。
网络问题:由于网络的不确定性,比较难说一个节点是否真正的“在工作”(有可能是网络延迟导致的错误),通过添加一些反馈机制可以在一定程度确定节点是否正常运行,比如:
健康检查机制,一般是通过心跳检测来实现的,比如使用 Docker 的话,Consul、Eureka 都有健康检查机制,当发送心跳请求发现容器实例已经无法回应时,可以认为服务挂掉了,但是却很难确认在这个 Node/Container 中有多少数据被正确的处理了。
如果一个节点的某个进程挂了,但是整个节点还可以正常运行。在使用微服务体系中是比较常见的,一台机器上部署着很多容器实例,其中个容器实例(即相当于刚才描述挂掉的进程)挂掉了,可以有一个方式去通知其他容器来快速接管,而不用等待执行超时。比如 Consul 通过 Gossip 协议进行多播,关于 Consul,可以参考这篇 Docker 容器部署 Consul 集群 内容。在批处理系统中,HBase 也有故障转移机制。
在实际做错误检测处理时,除了需要 节点、容器 做出积极的反馈,还要有一定的重试机制。重试的实现可以基于网络传输协议,如使用 TCP 的 RTT;也可以在应用层实现,如 Kafka 的 at-least-once 的实现。基于 Docker 体系的应用,可以使用 SpringCloud 的 Retry,结合 Hytrix、Ribbon 等。对于需要给用户反馈的应用,不太建议使用过多重试,根据不同的场景进行判断,更多的时候需要应用做出积极的响应即可,比如用户的“个人中心页面”,当 User 微服务挂了,可以给个默认头像、默认昵称,然后正确展示其他信息,而不是反复请求 User 微服务。
时间和顺序
在分布式系统中,时间可以作为所有执行操作的顺序先后的判定标准,也可以作为一些算法的边界条件。在分布式系统中决定操作的顺序是很重要的,比如对于提供分布式存储服务的系统来说,Repeated Read 以及 Serializable 的隔离级别,需要确定事务的顺序,以及一些事件的因果关系等。
物理时钟
每个机器都有两个不同的时钟,一个是 time-of-day,即常用的关于当前的日期、时间的信息,例如,此时是 2018 年 6 月 23 日 23:08:00,在 Java 中可以用 System.currentTimeMillis() 获取;另一个是 Monotonic 时钟,代表着单调递增的时间,一般是测量时间间距,在 Java 中调用 System.nanoTime() 可以获得 Monotonic 时间,常常用于测量一个本地请求的返回时间,比如 Apache commons 中的 StopWatch 的实现。
在分布式环境中,一般不会使用 Monotonic,测量两台不同的机器的 Monotonic 的时间差是无意义的。
不同机器的 time-of-day 一般也不同,就算使用 NTP 同步所有机器时间,也会存在毫秒级的差,NTP 本身也允许存在前后 0.05% 的误差。如果需要同步所有机器的时间,还需要对所有机器时间值进行监控,如果一个机器的时间和其他的有很大差异,需要移除不一致的节点。因为能改变机器时间的因素比较多,比如无法判断是否有人登上某台机器改变了其本地时间。
虽然全局时钟很难实现,并且有一定的限制,但基于全局时钟的假设还是有一些实践上的应用。比如 Facebook Cassandra 使用 NTP 同步时间来实现 LWW(Last Write Win)。Cassandra 假设有一个全局时钟,并基于这个时钟的值,用最新的写入覆盖旧值。当然时钟上的最新不代表顺序的最新,LWW 区分不了实际顺序;另外还有如 Google Spanner 使用 GPS 和原子时钟进行时间同步,但节点之间还是会存在时间误差。
逻辑时钟
在分布式系统中,因为全局时钟很难实现,并且像 NTP 同步过程,也会受到网络传输时间的影响,一般不会使用刚才所述的全局同步时间,当然也肯定不能使用各个机器的本地时间。对于需要确认操作执行顺序的时候,不能简单依赖一个基于 time-of-day 的 timestamps,所以需要一个逻辑时钟,来标记一些事件顺序、操作顺序的序列号。常见的方式是给所有操作加上递增的计数器。
这种所有操作都添加一个全局唯一的序列号的方式,提供了一种全局顺序的保证,全局顺序也包含了因果顺序一致的概念。关于分布式一致性的概念和实现会在后续文章详细介绍,我们先将关注点回归到时间和顺序上。下面看两种典型的逻辑时钟实现。
(1)Lamport Timestamps
Lamport timestamps 是 Leslie Lamport 在 1978 年提出的一种逻辑时钟的实现方法。Lamport Timestamps 的算法实现,可以理解为基于每个节点的一对值(NodeId,Counter)的全局顺序的同步。在集群中的每个节点(Node)都有一个唯一标识,并且每个 Node 都持有一个本地的对于所有操作顺序的一个 Counter(计数器)。
Lamport 实现的核心思想就是把事件分成三类(节点内处理的事件、发送事件、接收事件):
1.如果一个节点处理一个事件,节点 counter +1。
2.如果是发送一个消息事件,则在消息中带上 counter 值。
3.如果是接收一个消息事件,则更新 counter = max(本地 counter,接收的消息中带的 counter) +1。
简单画了个示例如下图:
初始的 counter 都是 0,在 Node1 接收到请求,处理事件时 counter+1(C:1表示),并且再发送消息带上 C:1。
在 Node1 接受 ClientA 请求时,本地的 Counter=1 > ClientA 请求的值,所以处理事件时 max(1,0)+1=2(C:2),然后再发送消息,带上 Counter 值,ClientA 更新请求的最大 Counter 值 =2,并在下一次对 Node2 的事件发送时会带上这个值。
这种序列号的全局顺序的递增,需要每次 Client 请求持续跟踪 Node 返回的 Counter,并且再下一次请求时带上这个 Counter。lamport 维护了全局顺序,但是却不能更好的处理并发。在并发的情况下,因为网络延迟,可能导致先发生的事件被认为是后发生的事件。如图中红色的两个事件属于并发事件,虽然 ClientB 的事件先发出,但是因为延迟,所以在 Node 1 中会先处理 ClientA,也即在 Lamport 的算法中,认为 Node1(C:4) happens before Node1(C:5)。
Lamport Timestamps 还有另一种并发冲突事件:不同的 NodeId,但 Counter 值相同,这种冲突会通过 Node 的编号的比较进行并发处理。比如 Node2(C:10)、Node1(C:10) 是两个并发事件,则认为 Node2 的时间顺序值 > Node1 的序列值,也就认为 Node1(C:10) happens before Node2(C:10)。
所以可见,Lamport 时间戳是一种逻辑的时间戳,其可以表示全局的执行顺序,但是无法识别并发,以及因果顺序,并发情况无法很好地处理 偏序。
(2)Vector Clock
Vector Clock 又叫向量时钟,跟 Lamport Timestamps 比较类似,也是使用 SequenceNo 实现逻辑时钟,但是最主要的区别是向量时钟有因果关系,可以区分两个并发操作,是否一个操作依赖于另外一个。
Lamport Timestamps 通过不断把本地的 counter 更新成公共的 MaxCounter 来维持事件的全局顺序。Vector Clock 则各个节点维持自己本地的一个递增的 Counter,并且会多记录其他节点事件的 Counter。通过维护了一组 [NodeId,Counter] 值来记录事件的因果顺序,能更好得识别并发事件,也即,Vector Clock 的 [NodeId,Counter] 不仅记录了本地的,还记录了其他 Node 的 Counter 信息。
Vector Clock 的 [NodeId,Counter] 更新规则:
1.如果一个节点处理一个事件,节点本地的逻辑时钟的 counter +1。
2.当节点发送一个消息,需要包含所有本地逻辑时钟的一组 [NodeId,Counter] 记录值。
3.接受一个事件消息时, 更新本地逻辑时钟的这组 [NodeId,Counter] 值:
4.让这组 [NodeId,Counter] 值中每个值都是 max(本地 counter,接收的消息中的counter)。
本地逻辑时钟 counter+1。
如下图简单示意了 Vector Clock 的时间顺序记录:
三个 Node,初始 counter 都是 0。NodeB 在处理 NodeC 的请求时,记录了 NodeC 的 Counter=1,并且处理事件时,本地逻辑时钟的 counter=0+1,所以 NodeB 处理事件时更新了本地逻辑时钟为 [B:1,C:1]。在事件处理时,通过不断更新本地的这组 Counter,就可以根据一组 [NodeId,Counter] 值来确定请求的因果顺序了,比如对于 NodeB,第二个处理事件 [A:1,B:2,C:1] 早于第三个事件:[A:1,B:3,C:1]。
在数值冲突的时候,如图中粉色剪头标记的。NodeC 的 [A:2,B:2,C:3] 和 NodeB[A:3,B:4,C:1]。C:3 > C:1、B:2 < B:4,种情况认为是没有因果关系,属于同时发生。
Vector Clock 可以通过各个节点的时间序列值的一组值,识别两个事件的先后顺序。Vector 提供了发现数据冲突的方式,但是具体怎样解决冲突需要由发现冲突的节点决定,比如可以将并发冲突抛给 Client 决定,或者用 Quorum-NRW 算法进行读取修复(Read Repair)。
Amazon Dynamo 就是通过 Vector Clock 来做并发检测的一个很好的分布式存储系统的例子。对于复制节点的数据冲突使用了 Quorum NRW 决议,以及读修复(Read Repair)处理最新更新数据的丢失,详细实现可以参考这篇论文 Dynamo: Amazon’s Highly Available Key-value Store ,Dynamo 是典型的高可用、可扩展的,提供弱一致性(最终一致性)保证的分布式 K-V 数据存储服务。后续文章再介绍 Quorums 算法时,也会再次提到。Vector Clock 在实际应用中,还会有一些问题需要处理,比如如果一个上千节点的集群,那么随着时间的推移,每个 Node 将需要记录大量 [NodeId,Counter] 数据。Dynamo 的解决方案是通过添加一个 timestamp 记录每个 Node 更新 [NodeId,Counter] 值的时间,以及一个设定好的阀值,比如说阀值是 10,那么本地只保存最新的 10 个 [NodeId,Counter] 组合数据。
小结
本文引出了一些分布式系统的常见问题以及一些基础的分布式系统模型概念,微服务的架构目前已经被更广泛得应用,但微服务面临的问题其实也都是经典的分布式场景的问题。本文在分布式系统的问题中,主要介绍了关于错误检测以及时间和顺序的处理模型。
关于时间和顺序的问题处理中,没有一个绝对最优的方案,Cassandra 使用了全局时钟以及 LWW 处理顺序判定;Dynamo 使用了 Vector clock 发现冲突,加上 Quorum 算法处理事件并发。这两个存储系统都有很多优秀的分布式系统设计和思想,在后续文章中会更详细的介绍数据复制、一致性、共识算法等。