微服务入门权威指南


做者:大闲人柴毛毛
连接:https://juejin.im/post/5ab0d1a3f265da23731448e0html

 

本文将介绍以下内容:程序员

  1. 微服务概述web

    1.1 易于扩展算法

    1.2 部署简单sql

    1.3 技术异构性docker

  2. 数据库的服务化切分数据库

    2.1 什么是“分库分表”?编程

    2.2 数据库扩展的几种方式缓存

    2.3 分库分表的几种方式安全

    2.4 引入分库分表中间件后面临的问题

    2.5 现有分库分表中间件的横向对比

  3. 微服务架构中的分布式事务

    3.1 什么是事务?

    3.2 事务的四大特性 ACID

    3.3 事务的隔离级别

    3.4 什么是分布式事务?

    3.5 CAP理论

    3.6 BASE理论

    3.7 酸碱平衡

    3.8 分布式事务协议

    3.9 分布式事务的解决方案

  4. 服务部署

    4.1 持续集成、持续部署、持续交付

    4.2 微服务与持续集成

    4.3 微服务构建物

  5. 参考文献


1. 什么是微服务?

咱们首先给出微服务的定义,而后再对该定义给出详细的解释。

微服务就是一些可独立运行、可协同工做的小的服务。

从概念中咱们能够提取三个关键词:可独立运行、可协同工做、小。这三个词高度归纳了微服务的核心特性。下面咱们就对这三个词做详细解释。

  1. 可独立运行

    微服务是一个个能够独立开发、独立部署、独立运行的系统或者进程。

  2. 可协同工做

    采用了微服务架构后,整个系统被拆分红多个微服务,这些服务之间每每不是彻底独立的,在业务上存在必定的耦合,即一个服务可能须要使用另外一个服务所提供的功能。这就是所谓的“可协同工做”。与单服务应用不一样的是,多个微服务之间的调用时经过RPC通讯来实现,而非单服务的本地调用,因此通讯的成本相对要高一些,但带来的好处也是可观的。

  3. 小而美

    微服务的思想是,将一个拥有复杂功能的庞大系统,按照业务功能,拆分红多个相互独立的子系统,这些子系统则被称为“微服务”。每一个微服务只承担某一项职责,从而相对于单服务应用来讲,微服务的体积是“小”的。小也就意味着每一个服务承担的职责变少,根据单一职责原则,咱们在系统设计时,要尽可能使得每一项服务只承担一项职责,从而实现系统的“高内聚”。

2. 微服务的优势

1. 易于扩展

在单服务应用中,若是目前性能到达瓶颈,没法支撑目前的业务量,此时通常采用集群模式,即增长服务器集群的节点,并将这个单服务应用“复制”到全部的节点上,从而提高总体性能。然而这种扩展的粒度是比较粗糙的。若是只是系统中某一小部分存在性能问题,在单服务应用中,也要将整个应用进行扩展,这种方式简单粗暴,没法对症下药。而当咱们使用了微服务架构后,若是某一项服务的性能到达瓶颈,那么咱们只须要增长该服务的节点数便可,其余服务无需变化。这种扩展更加具备针对性,可以充分利用计算机硬件/软件资源。并且只扩展单个服务影响的范围较小,从而系统出错的几率也就越低。

2. 部署简单

对于单服务应用而言,全部代码均在一个项目中,从而致使任何微小的改变都须要将整个项目打包、发布、部署,而这一系列操做的代价是高昂的。久而久之,团队为了下降发布的频率,会使得每次发布都伴随着大量的修改,修改越多也就意味着出错的几率也越大。 当咱们采用微服务架构之后,每一个服务只承担少数职责,从而每次只须要发布发生修改的系统,其余系统依然可以正常运行,波及范围较小。此外,相对于单服务应用而言,每一个微服务系统修改的代码相对较少,从而部署后出现错误的几率也相对较低。

3. 技术异构性

对于单服务应用而言,一个系统的全部模块均整合在一个项目中,因此这些模块只能选择相同的技术。但有些时候,单一技术没办法知足不一样的业务需求。如对于项目的算法团队而言,函数试编程语言可能更适合算法的开发,而对于业务开发团队而言,相似于Java的强类型语言具备更高的稳定性。然而在单服务应用中只能互相权衡,选择同一种语言,而当咱们使用微服务结构后,这个问题就可以引刃而解。咱们将一个完整的系统拆分红了多个独立的服务,从而每一个服务均可以根据各自不一样的特色,选择最为合适的技术体系。

固然,并非全部的微服务系统都具有技术异构性,要实现技术异构性,必须保证全部服务都提供通用接口。咱们知道,在微服务系统中,服务之间采用RPC接口通讯,而实现RPC通讯的方式有不少。有一些RPC通讯方式与语言强耦合,如Java的RMI技术,它就要求通讯的双方都必须采用Java语言开发。固然,也有一些RPC通讯方式与语言无关,如基于HTTP协议的REST。这种通讯方式对通讯双方所采用的语言没有作任何限制,只要通讯过程当中传输的数据遵循REST规范便可。固然,与语言无关也就意味着通讯双方没有类型检查,从而会提升出错的几率。因此,究竟选择与语言无关的RPC通讯方式,仍是选择与语言强耦合的RPC通讯方式,须要咱们根据实际的业务场景合理地分析。

2. 数据库的服务化切分

2.1 什么是“分库分表”?

随着大数据时代的到来,业务系统的数据量日益增大,数据存储能力逐渐成为影响系统性能的瓶颈。目前主流的关系型数据库单表存储上限为1000万条记录,而这一存储能力显然已经没法知足大数据背景下的业务系统存储要求了。随着微服务架构、分布式存储等概念的出现,数据存储问题也渐渐迎来了起色。而数据分片是目前解决海量数据持久化存储与高效查询的一种重要手段。数据分库分表的过程在系统设计阶段完成,要求系统设计人员根据系统预期的业务量,将将来可能出现瓶颈的数据库、数据表按照必定规则拆分红多个库、多张表。这些数据库和数据表须要部署在不一样的服务器上,从而将数据读写压力分摊至集群中的各个节点,提高数据库总体处理能力,避免出现读写瓶颈的现象。

目前数据分片的方式一共有两种:离散分片和连续分片。

离散分片是按照数据的某一字段哈希取模后进行分片存储。只要哈希算法选择得当,数据就会均匀地分布在不一样的分片中,从而将读写压力平均分配给全部分片,总体上提高数据的读写能力。然而,离散存储要求数据之间有较强的独立性,但实际业务系统并不是如此,不一样分片之间的数据每每存在必定的关联性,所以在某些场景下须要跨分片链接查询。因为目前全部的关系型数据库出于安全性考虑,均不支持跨库链接。所以,跨库操做须要由数据分库分表中间件来完成,这极大影响数据的查询效率。此外,当数据存储能力出现瓶颈须要扩容时,离散分片规则须要将全部数据从新进行哈希取模运算,这无疑成为限制系统可扩展性的一个重要因素。虽然,一致性哈希能在必定程度上减小系统扩容时的数据迁移,但数据迁移问题仍然不可避免。对于一个已经上线运行的系统而言,系统中止对外服务进行数据迁移的代价太大。

第二种数据分片的方式即为连续分片,它能解决系统扩容时产生的数据迁移问题。这种方式要求数据按照时间或连续自增主键连续存储。从而一段时间内的数据或相邻主键的数据会被存储在同一个分片中。当须要增长分片时,不会影响现有的分片。所以,连续分片能解决扩容所带来的数据迁移问题。可是,数据的存储时间和读写频率每每呈正比,也就是大量的读写每每都集中在最新存储的那一部分数据,这就会致使热点问题,并不能起到分摊读写压力的初衷。

2.2 数据库扩展的几种方式

数据库扩展一共有四种分配方式,分别是:垂直分库、垂直分表、水平分表、水平数据分片。每一种策略都有各自的适用场景。

  1. 垂直分库

    垂直分库便是将一个完整的数据库根据业务功能拆分红多个独立的数据库,这些数据库能够运行在不一样的服务器上,从而提高数据库总体的数据读写性能。这种方式在微服务架构中很是经常使用。微服务架构的核心思想是将一个完整的应用按照业务功能拆分红多个可独立运行的子系统,这些子系统称为“微服务”,各个服务之间经过RPC接口通讯,这样的结构使得系统耦合度更低、更易于扩展。垂直分库的理念与微服务的理念不谋而合,能够将本来完整的数据按照微服务拆分系统的方式,拆分红多个独立的数据库,使得每一个微服务系统都有各自独立的数据库,从而能够避免单个数据库节点压力过大,影响系统的总体性能,以下图所示。

    title

     

  2. 垂直分表

    垂直分表若是一张表的字段很是多,那么颇有可能会引发数据的跨页存储,这会形成数据库额外的性能开销,而垂直分表能够解决这个问题。垂直分表就是将一张表中不经常使用的字段拆分到另外一张表中,从而保证第一章表中的字段较少,避免出现数据库跨页存储的问题,从而提高查询效率。而另外一张表中的数据经过外键与第一张表进行关联,以下图所示。

    title

     

  3. 水平分表

    若是一张表中的记录数过多(超过1000万条记录),那么会对数据库的读写性能产生较大的影响,虽然此时仍然可以正确地读写,但读写的速度已经到了业务没法忍受的地步,此时就须要使用水平分表来解决这个问题。水平分表是将一张含有不少记录数的表水平切分,拆分红几张结构相同的表。举个例子,假设一张订单表目前存储了2000万条订单的数据,致使数据读写效率极低。此时能够采用水平分表的方式,将订单表拆分红100张结构相同的订单表,分别叫作order_一、order_2……、order_100。而后能够根据订单所属用户的id进行哈希取模后均匀地存储在这100张表中,从而每张表中只存储了20万条订单记录,极大提高了订单的读写效率,以下图所示。 固然,若是拆分出来的表都存储在同一个数据库节点上,那么当请求量过大的时候,毕竟单台服务器的处理能力是有限的,数据库仍然会成为系统的瓶颈,因此为了解决这个问题,就出现了水平数据分片的解决方案。

    title

     

  4. 水平分库分表

    水平数据分片与数据分片区别在于:水平数据分片首先将数据表进行水平拆分,而后按照某一分片规则存储在多台数据库服务器上。从而将单库的压力分摊到了多库上,从而避免由于数据库硬件资源有限致使的数据库性能瓶颈,以下图所示。

    title

     

2.3 分库分表的几种方式

目前经常使用的数据分片策略有两种,分别是连续分片和离散分片。

  1. 离散分片

    离散分片是指将数据打散以后均匀地存储在逻辑表的各个分片中,从而使的对同一张逻辑表的数据读取操做均匀地落在不一样库的不一样表上,从而提升读写速度。离散分片通常以哈希取模的方式实现。好比:一张逻辑表有4个分片,那么在读写数据的时候,中间件首先会取得分片字段的哈希值,而后再模以4,从而计算出该条记录所在的分片。在这种方法中,只要哈希算法选的好,那么数据分片将会比较均匀,从而数据读写就会比较均匀地落在各个分片上,从而就有较高的读写效率。可是,这种方式也存在一个最大的缺陷——数据库扩容成本较高。采用这种方式,若是须要再增长分片,原先的分片算法将失效,而且全部记录都须要从新计算所在分片的位置。对于一个已经上线的系统来讲,行级别的数据迁移成本至关高,并且因为数据迁移期间系统仍在运行,仍有新数据产生,从而没法保证迁移过程数据的一致性。若是为了不这个问题而停机迁移,那必然会对业务形成巨大影响。固然,若是为了不数据迁移,在一开始的时候就分片较多的分片,那须要承担较高的费用,这对于中小公司来讲是没法承受的。

  2. 连续分片

    连续分片指的是按照某一种分片规则,将某一个区间内的数据存储在同一个分片上。好比按照时间分片,每月生成一张物理表。那么在读写数据时,直接根据当前时间就能够找到数据所在的分片。再好比能够按照记录ID分片,这种分片方式要求ID须要连续递增。因为Mysql数据库单表支持最大的记录数约为1000万,所以咱们能够根据记录的ID,使得每一个分片存储1000万条记录,当目前的记录数即将到达存储上限时,咱们只需增长分片便可,原有的数据无需迁移。连续分片的一个最大好处就是方便扩容,由于它不须要任何的数据迁移。可是,连续分片有个最大的缺点就是热点问题。连续分片使得新插入的数据集中在同一个分片上,而每每新插入的数据读写频率较高,所以,读写操做都会集中在最新的分片上,从而没法体现数据分片的优点。

2.4 引入分库分表中间件后面临的问题

  1. 跨库操做

    在关系型数据库中,多张表之间每每存在关联,咱们在开发过程当中须要使用JOIN操做进行多表链接。可是当咱们使用了分库分表模式后,因为数据库厂商处于安全考虑,不容许跨库JOIN操做,从而若是须要链接的两张表被分到不一样的库中后,就没法使用SQL提供的JOIN关键字来实现表链接,咱们可能须要在业务系统层面,经过屡次SQL查询,完成数据的组装和拼接。这一方面会增长业务系统的复杂度,另外一方面会增长业务系统的负载。 所以,当咱们使用分库分表模式时,须要根据具体的业务场景,合理地设置分片策略、设置分片字段,这将会在本文的后续章节中介绍。

  2. 分布式事务

    咱们知道,数据库提供了事务的功能,以保证数据一致性。然而,这种事务只是针对单数据库而言的,数据库厂商并未提供跨库事务。所以,当咱们使用了分库分表以后,就须要咱们在业务系统层面实现分布式事务。关于分布式事务的详细内容,能够参考笔者的另外一篇文章《经常使用的分布式事务解决方案》

2.5 现有分库分表中间件的横向对比

  • Cobar实现数据库的透明分库,让开发人员可以在无感知的状况下操纵数据库集群,从而简化数据库的编程模型。然而Cobar仅实现了分库功能,并未实现分表功能。分库能够解决单库IO、CPU、内存的瓶颈,但没法解决单表数据量过大的问题。此外,Cobar是一个独立运行的系统,它处在应用系统与数据库系统之间,所以增长了额外的部署复杂度,增长了运维成本。

  • 为了解决上述问题,Cobar还推出了一个Cobar-Client项目,它只是一个安装在应用程序的Jar包,并非一个独立运行的系统,必定程度上下降了系统的复杂度。但和Cobar同样,仍然只支持分库,并不支持分表,也不支持读写分离。

  • MyCat是基于Cobar二次开发的数据库中间件,和Cobar相比,它增长了读写分离的功能,并修复了Cobar的一些bug。可是,MyCat和Cobar同样,都是一套须要独立部署的系统,所以会增长部署的复杂度,提升了后期系统运维的成本。

3. 微服务架构中的分布式事务

众所周知,数据库能实现本地事务,也就是在同一个数据库中,你能够容许一组操做要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。但如今的系统每每采用微服务架构,业务系统拥有独立的数据库,所以就出现了跨多个数据库的事务需求,这种事务即为“分布式事务”。那么在目前数据库不支持跨库事务的状况下,咱们应该如何实现分布式事务呢?本文首先会为你们梳理分布式事务的基本概念和理论基础,而后介绍几种目前经常使用的分布式事务解决方案。废话很少说,那就开始吧~

3.1 什么是事务?

事务由一组操做构成,咱们但愿这组操做可以所有正确执行,若是这一组操做中的任意一个步骤发生错误,那么就须要回滚以前已经完成的操做。也就是同一个事务中的全部操做,要么全都正确执行,要么全都不要执行。

3.2 事务的四大特性 ACID

说到事务,就不得不提一下事务著名的四大特性。

  • 原子性

    原子性要求,事务是一个不可分割的执行单元,事务中的全部操做要么全都执行,要么全都不执行。

  • 一致性

    一致性要求,事务在开始前和结束后,数据库的完整性约束没有被破坏。

  • 隔离性

    事务的执行是相互独立的,它们不会相互干扰,一个事务不会看到另外一个正在运行过程当中的事务的数据。

  • 持久性

    持久性要求,一个事务完成以后,事务的执行结果必须是持久化保存的。即便数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失。

注意:事务只能保证数据库的高可靠性,即数据库自己发生问题后,事务提交后的数据仍然能恢复;而若是不是数据库自己的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。所以,事务只能保证数据库的『高可靠性』,而『高可用性』须要整个系统共同配合实现。

3.3 事务的隔离级别

这里扩展一下,对事务的隔离性作一个详细的解释。

在事务的四大特性ACID中,要求的隔离性是一种严格意义上的隔离,也就是多个事务是串行执行的,彼此之间不会受到任何干扰。这确实可以彻底保证数据的安全性,但在实际业务系统中,这种方式性能不高。所以,数据库定义了四种隔离级别,隔离级别和数据库的性能是呈反比的,隔离级别越低,数据库性能越高,而隔离级别越高,数据库性能越差。

3.3.1 事务并发执行会出现的问题

咱们先来看一下在不一样的隔离级别下,数据库可能会出现的问题:

  1. 更新丢失

    当有两个并发执行的事务,更新同一行数据,那么有可能一个事务会把另外一个事务的更新覆盖掉。 当数据库没有加任何锁操做的状况下会发生。

  2. 脏读

    一个事务读到另外一个还没有提交的事务中的数据。 该数据可能会被回滚从而失效。 若是第一个事务拿着失效的数据去处理那就发生错误了。

  3. 不可重复读

    不可重复度的含义:一个事务对同一行数据读了两次,却获得了不一样的结果。它具体分为以下两种状况:

    • 虚读:在事务1两次读取同一记录的过程当中,事务2对该记录进行了修改,从而事务1第二次读到了不同的记录。
    • 幻读:事务1在两次查询的过程当中,事务2对该表进行了插入、删除操做,从而事务1第二次查询的结果发生了变化。

不可重复读 与 脏读 的区别? 脏读读到的是还没有提交的数据,而不可重复读读到的是已经提交的数据,只不过在两次读的过程当中数据被另外一个事务改过了。

3.3.2 数据库的四种隔离级别

数据库一共有以下四种隔离级别:

  1. Read uncommitted 读未提交

    在该级别下,一个事务对一行数据修改的过程当中,不容许另外一个事务对该行数据进行修改,但容许另外一个事务对该行数据读。 所以本级别下,不会出现更新丢失,但会出现脏读、不可重复读。

  2. Read committed 读提交

    在该级别下,未提交的写事务不容许其余事务访问该行,所以不会出现脏读;可是读取数据的事务容许其余事务的访问该行数据,所以会出现不可重复读的状况。

  3. Repeatable read 重复读

    在该级别下,读事务禁止写事务,但容许读事务,所以不会出现同一事务两次读到不一样的数据的状况(不可重复读),且写事务禁止其余一切事务。

  4. Serializable 序列化

    该级别要求全部事务都必须串行执行,所以能避免一切因并发引发的问题,但效率很低。

隔离级别越高,越能保证数据的完整性和一致性,可是对并发性能的影响也越大。对于多数应用程序,能够优先考虑把数据库系统的隔离级别设为Read Committed。它可以避免脏读取,并且具备较好的并发性能。尽管它会致使不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,能够由应用程序采用悲观锁或乐观锁来控制。

3.4 什么是分布式事务?

到此为止,所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统每每由若干个子系统构成,这些子系统又拥有各自独立的数据库。每每一个业务流程须要由多个子系统共同完成,并且这些操做可能须要在一个事务中完成。在微服务系统中,这些业务场景是广泛存在的。此时,咱们就须要在数据库之上经过某种手段,实现支持跨数据库的事务支持,这也就是你们常说的“分布式事务”。

这里举一个分布式事务的典型例子——用户下单过程。 当咱们的系统采用了微服务架构后,一个电商系统每每被拆分红以下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程以下:

  1. 用户经过商品系统浏览商品,他看中了某一项商品,便点击下单
  2. 此时订单系统会生成一条订单
  3. 订单建立成功后,支付系统提供支付功能
  4. 当支付完成后,由积分系统为该用户增长积分

上述步骤二、三、4须要在一个事务中完成。对于传统单体应用而言,实现事务很是简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法便可。Spring经过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时咱们必须在数据库和应用系统之间,经过某项黑科技,实现分布式事务的支持。

3.5 CAP理论

CAP理论说的是:在一个分布式系统中,最多只能知足C、A、P中的两个需求。

CAP的含义:

  • C:Consistency 一致性

    同一数据的多个副本是否实时相同。

  • A:Availability 可用性

    可用性:必定时间内 & 系统返回一个明确的结果 则称为该系统可用。

  • P:Partition tolerance 分区容错性

    将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其余系统提供相同的服务。

CAP理论告诉咱们,在分布式系统中,C、A、P三个条件中咱们最多只能选择两个。那么问题来了,究竟选择哪两个条件较为合适呢?

对于一个业务系统来讲,可用性和分区容错性是必需要知足的两个条件,而且这二者是相辅相成的。业务系统之因此使用分布式系统,主要缘由有两个:

  • 提高总体性能

    当业务量猛增,单个服务器已经没法知足咱们的业务需求的时候,就须要使用分布式系统,使用多个节点提供相同的功能,从而总体上提高系统的性能,这就是使用分布式系统的第一个缘由。

  • 实现分区容错性

    单一节点 或 多个节点处于相同的网络环境下,那么会存在必定的风险,万一该机房断电、该地区发生天然灾害,那么业务系统就全面瘫痪了。为了防止这一问题,采用分布式系统,将多个子系统分布在不一样的地域、不一样的机房中,从而保证系统高可用性。

这说明分区容错性是分布式系统的根本,若是分区容错性不能知足,那使用分布式系统将失去意义。

此外,可用性对业务系统也尤其重要。在大谈用户体验的今天,若是业务系统时常出现“系统异常”、响应时间过长等状况,这使得用户对系统的好感度大打折扣,在互联网行业竞争激烈的今天,相同领域的竞争者不甚枚举,系统的间歇性不可用会立马致使用户流向竞争对手。所以,咱们只能经过牺牲一致性来换取系统的可用性分区容错性。这也就是下面要介绍的BASE理论。

3.6 BASE理论

CAP理论告诉咱们一个悲惨但不得不接受的事实——咱们只能在C、A、P中选择两个条件。而对于业务系统而言,咱们每每选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并非彻底放弃数据一致性,而是牺牲强一致性换取弱一致性。下面来介绍下BASE理论。

  • BA:Basic Available 基本可用
    • 整个系统在某些不可抗力的状况下,仍然可以保证“可用性”,即必定时间内仍然可以返回一个明确的结果。只不过“基本可用”和“高可用”的区别是:
      • “必定时间”能够适当延长 当举行大促时,响应时间能够适当延长
      • 给部分用户返回一个降级页面 给部分用户直接返回一个降级页面,从而缓解服务器压力。但要注意,返回降级页面仍然是返回明确结果。
  • S:Soft State:柔性状态 同一数据的不一样副本的状态,能够不须要实时一致。
  • E:Eventual Consisstency:最终一致性 同一数据的不一样副本的状态,能够不须要实时一致,但必定要保证通过必定时间后仍然是一致的。

3.7 酸碱平衡

ACID可以保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,所以分布式系统中遵循BASE理论便可。但分布式系统的不一样业务场景对一致性的要求也不一样。如交易场景下,就要求强一致性,此时就须要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不须要实时一致,所以遵循BASE理论便可。所以要根据具体业务场景,在ACID和BASE之间寻求平衡。

3.8 分布式事务协议

下面介绍几种实现分布式事务的协议。

3.8.1 两阶段提交协议 2PC

分布式系统的一个难点是如何保证架构下多个节点在进行事务性操做的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于如下假设:

  • 该分布式系统中,存在一个节点做为协调者(Coordinator),其余节点做为参与者(Cohorts)。且节点之间能够进行网络通讯。
  • 全部节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即便节点损坏不会致使日志数据的消失。
  • 全部节点不会永久性损坏,即便损坏后仍然能够恢复。

1. 第一阶段(投票阶段):

  1. 协调者节点向全部参与者节点询问是否能够执行提交操做(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的全部事务操做,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每一个参与者已经执行了事务操做)
  3. 各参与者节点响应协调者节点发起的询问。若是参与者节点的事务操做实际执行成功,则它返回一个"赞成"消息;若是参与者节点的事务操做实际执行失败,则它返回一个"停止"消息。

2. 第二阶段(提交执行阶段):

当协调者节点从全部参与者节点得到的相应消息都为"赞成"时:

  1. 协调者节点向全部参与者节点发出"正式提交(commit)"的请求。
  2. 参与者节点正式完成操做,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"完成"消息。
  4. 协调者节点受到全部参与者节点反馈的"完成"消息后,完成事务。

若是任一参与者节点在第一阶段返回的响应消息为"停止",或者 协调者节点在第一阶段的询问超时以前没法获取全部参与者节点的响应消息时:

  1. 协调者节点向全部参与者节点发出"回滚操做(rollback)"的请求。
  2. 参与者节点利用以前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送"回滚完成"消息。
  4. 协调者节点受到全部参与者节点反馈的"回滚完成"消息后,取消事务。

无论最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实可以提供原子性的操做,可是不幸的事,二阶段提交仍是有几个缺点的:

  1. 执行过程当中,全部参与节点都是事务阻塞型的。当参与者占有公共资源时,其余第三方节点访问公共资源不得不处于阻塞状态。
  2. 参与者发生故障。协调者须要给每一个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
  3. 协调者发生故障。参与者会一直阻塞下去。须要额外的备机进行容错。(这个能够依赖后面要讲的Paxos协议实现HA)
  4. 二阶段没法解决的问题:协调者再发出commit消息以后宕机,而惟一接收到这条消息的参与者同时也宕机了。那么即便协调者经过选举协议产生了新的协调者,这条事务的状态也是不肯定的,没人知道事务是否被已经提交。

为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。

3.8.2 三阶段提交协议 3PC

与两阶段提交不一样的是,三阶段提交有两个改动点。

  • 引入超时机制。同时在协调者和参与者中都引入超时机制。
  • 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段以前各参与节点的状态是一致的。

也就是说,除了引入超时机制以外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

1. CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者若是能够提交就返回Yes响应,不然返回No响应。

  1. 事务询问

    协调者向参与者发送CanCommit请求。询问是否能够执行事务提交操做。而后开始等待参与者的响应。

  2. 响应反馈

    参与者接到CanCommit请求以后,正常状况下,若是其自身认为能够顺利执行事务,则返回Yes响应,并进入预备状态。不然反馈No

2. PreCommit阶段

协调者根据参与者的反应状况来决定是否能够记性事务的PreCommit操做。根据响应状况,有如下两种可能。 假如协调者从全部的参与者得到的反馈都是Yes响应,那么就会执行事务的预执行。

  1. 发送预提交请求

    协调者向参与者发送PreCommit请求,并进入Prepared阶段。

  2. 事务预提交

    参与者接收到PreCommit请求后,会执行事务操做,并将undo和redo信息记录到事务日志中。

  3. 响应反馈

    若是参与者成功的执行了事务操做,则返回ACK响应,同时开始等待最终指令。

假若有任何一个参与者向协调者发送了No响应,或者等待超时以后,协调者都没有接到参与者的响应,那么就执行事务的中断。

  1. 发送中断请求

    协调者向全部参与者发送abort请求。

  2. 中断事务

    参与者收到来自协调者的abort请求以后(或超时以后,仍未收到协调者的请求),执行事务的中断。

3. doCommit阶段 该阶段进行真正的事务提交,也能够分为如下两种状况。

该阶段进行真正的事务提交,也能够分为如下两种状况。

3.1 执行提交

  1. 发送提交请求

    协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向全部参与者发送doCommit请求。

  2. 事务提交

    参与者接收到doCommit请求以后,执行正式的事务提交。并在完成事务提交以后释放全部事务资源。

  3. 响应反馈

    事务提交完以后,向协调者发送Ack响应。

  4. 完成事务

    协调者接收到全部参与者的ack响应以后,完成事务。

3.2 中断事务 协调者没有接收到参与者发送的ACK响应(多是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

  1. 发送中断请求

    协调者向全部参与者发送abort请求

  2. 事务回滚

    参与者接收到abort请求以后,利用其在阶段二记录的undo信息来执行事务的回滚操做,并在完成回滚以后释放全部的事务资源。

  3. 反馈结果

    参与者完成事务回滚以后,向协调者发送ACK消息

  4. 中断事务

    协调者接收到参与者反馈的ACK消息以后,执行事务的中断。

3.9 分布式事务的解决方案

分布式事务的解决方案有以下几种:

  • 全局消息
  • 基于可靠消息服务的分布式事务
  • TCC
  • 最大努力通知

3.9.1 方案1:全局事务(DTP模型)

全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,须要三种角色:

  • AP:Application 应用系统

    它就是咱们开发的业务系统,在咱们开发的过程当中,可使用资源管理器提供的事务接口来实现分布式事务。

  • TM:Transaction Manager 事务管理器

    • 分布式事务的实现由事务管理器来完成,它会提供分布式事务的操做接口供咱们的业务系统调用。这些接口称为TX接口。
    • 事务管理器还管理着全部的资源管理器,经过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。
    • DTP只是一套实现分布式事务的规范,并无定义具体如何实现分布式事务,TM能够采用2PC、3PC、Paxos等协议实现分布式事务。
  • RM:Resource Manager 资源管理器

    • 可以提供数据服务的对象均可以是资源管理器,好比:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。
    • 资源管理器可以提供单数据库的事务能力,它们经过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。
    • XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。
    • DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。
  1. 有没有基于DTP模型的分布式事务中间件?
  1. DTP模型有啥优缺点?

3.9.2 方案2:基于可靠消息服务的分布式事务

这种实现分布式事务的方式须要经过消息中间件来实现。假设有A和B两个系统,分别能够处理任务A和任务B。此时系统A中存在一个业务流程,须要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。

 

title

 

  • 在系统A处理任务A前,首先向消息中间件发送一条消息
  • 消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
  • 消息中间件持久化成功后,便向系统A返回一个确认应答;
  • 系统A收到确认应答后,则能够开始处理任务A;
  • 任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它能够处理别的任务了。 但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
  • 消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
  • 当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。

上述过程能够得出以下几个结论:

  1. 消息中间件扮演者分布式事务协调者的角色。
  2. 系统A完成任务A后,到任务B执行完成之间,会存在必定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是能够接受的,由于通过短暂的时间后,系统又能够保持数据一致性,知足BASE理论。

上述过程当中,若是任务A处理失败,那么须要进入回滚流程,以下图所示:

title

 

  • 若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求同样,系统A发完以后即可以认为回滚已经完成,它即可以去作其余的事情。
  • 消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。

此时系统又处于一致性状态,由于任务A和任务B都没有执行。

上面所介绍的Commit和Rollback都属于理想状况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种状况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。

 

title

 

系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,若是到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:

  • 提交

    若得到的状态是“提交”,则将该消息投递给系统B。

  • 回滚

    若得到的状态是“回滚”,则直接将条消息丢弃。

  • 处理中

    若得到的状态是“处理中”,则继续等待。

消息中间件的超时询问机制可以防止上游系统因在传输过程当中丢失Commit/Rollback指令而致使的系统不一致状况,并且能下降上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后即可以处理其余任务,无需等待确认应答。而Commit/Rollback指令丢失的状况经过超时询问机制来弥补,这样大大下降上游系统的阻塞时间,提高系统的并发度。

下面来讲一说消息投递过程的可靠性保证。 当上游系统执行完任务并向消息中间件提交了Commit指令后,即可以处理其余任务了,此时它能够认为事务已经完成,接下来消息中间件**必定会保证消息被下游系统成功消费掉!**那么这是怎么作到的呢?这由消息中间件的投递流程来保证。

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便当即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!

若是消息在投递过程当中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时以后就会从新投递,直到下游消费者返回消费成功响应为止。固然,通常消息中间件能够设置消息重试的次数和时间间隔,好比:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。若是重试3次以后仍然投递失败,那么这条消息就须要人工干预。

title

 

 

title

 

有的同窗可能要问:消息投递失败后为何不回滚消息,而是不断尝试从新投递?

这就涉及到整套分布式事务系统的实现成本问题。 咱们知道,当系统A将向消息中间件发送Commit指令后,它便去作别的事情了。若是此时消息投递失败,须要回滚的话,就须要让系统A事先提供回滚接口,这无疑增长了额外的开发成本,业务系统的复杂度也将提升。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地下降系统复杂度,从而可以下降系统的运维成本。

不知你们是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后即可以去作别的事情,接下来提交、回滚就彻底交给消息中间件来完成,而且彻底信任消息中间件,认为它必定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为何这二者在设计上是不一致的呢?

首先,上游系统和消息中间件之间采用异步通讯是为了提升系统并发度。业务系统直接和用户打交道,用户体验尤其重要,所以这种异步通讯方式可以极大程度地下降用户等待时间。此外,异步通讯相对于同步通讯而言,没有了长时间的阻塞等待,所以系统的并发性也大大增长。但异步通讯可能会引发Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。

那么,消息中间件和下游系统之间为何要采用同步通讯呢?

异步能提高系统性能,但随之会增长系统复杂度;而同步虽然下降系统并发度,但实现成本较低。所以,在对并发度要求不是很高的状况下,或者服务器资源较为充裕的状况下,咱们能够选择同步来下降系统的复杂度。 咱们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它通常部署在独立的服务器集群上,具备良好的可扩展性,因此没必要太过于担忧它的性能,若是处理速度没法知足咱们的要求,能够增长机器来解决。并且,即便消息中间件处理速度有必定的延迟那也是能够接受的,由于前面所介绍的BASE理论就告诉咱们了,咱们追求的是最终一致性,而非实时一致性,所以消息中间件产生的时延致使事务短暂的不一致是能够接受的。

3.9.3 方案3:最大努力通知(按期校对)

最大努力通知也被称为按期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也须要消息中间件的参与,其过程以下:

 

title

 

  • 上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,而后上游系统能够去作别的事情了;
  • 消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;
  • 当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件即可以将该条消息删除,从而该事务完成。

上面是一个理想化的过程,但在实际场景中,每每会出现以下几种意外状况:

  1. 消息中间件向下游系统投递消息失败
  2. 上游系统向消息中间件发送消息失败

对于第一种状况,消息中间件具备重试机制,咱们能够在消息中间件中设置消息的重试次数和重试时间间隔,对于网络不稳定致使的消息投递失败的状况,每每重试几回后消息即可以成功投递,若是超过了重试的上限仍然投递失败,那么消息中间件再也不投递该消息,而是记录在失败消息表中,消息中间件须要提供失败消息的查询接口,下游系统会按期查询失败消息,并将其消费,这就是所谓的“按期校对”。

若是重复投递和按期校对都不能解决问题,每每是由于下游系统出现了严重的错误,此时就须要人工干预。

对于第二种状况,须要在上游系统中创建消息重发机制。能够在上游系统创建一张本地消息表,并将 任务处理过程向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。若是向本地消息表插入消息失败,那么就会触发回滚,以前的任务处理结果就会被取消。若是这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,若是发送失败它会返回重试。固然,也要给消息发送者设置重试的上限,通常而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题。

对于不支持事务型消息的消息中间件,若是要实现分布式事务的话,就能够采用这种方式。它可以经过重试机制+按期校对实现分布式事务,但相比于第二种方案,它达到数据一致性的周期较长,并且还须要在上游系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,这无疑增长了业务系统的开发成本,使得业务系统不够纯粹,而且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。

所以,尽可能选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ。

3.9.4 方案4:TCC(两阶段型、补偿型)

TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有三个步骤:

  • Try:尝试待执行的业务
    • 这个过程并未执行业务,只是完成全部业务的一致性检查,并预留好执行所需的所有资源
  • Confirm:执行业务
    • 这个过程真正开始执行业务,因为Try阶段已经完成了一致性检查,所以本过程直接执行,而不作任何检查。而且在执行的过程当中,会使用到Try阶段预留的业务资源。
  • Cancel:取消执行的业务
    • 若业务执行失败,则进入Cancel阶段,它会释放全部占用的业务资源,并回滚Confirm阶段执行的操做。

下面以一个转帐的例子来解释下TCC实现分布式事务的过程。

假设用户A用他的帐户余额给用户B发一个100元的红包,而且余额系统和红包系统是两个独立的系统。

  • Try

    • 建立一条转帐流水,并将流水的状态设为交易中
    • 将用户A的帐户中扣除100元(预留业务资源)
    • Try成功以后,便进入Confirm阶段
    • Try过程发生任何异常,均进入Cancel阶段
  • Confirm

    • 向B用户的红包帐户中增长100元
    • 将流水的状态设为交易已完成
    • Confirm过程发生任何异常,均进入Cancel阶段
    • Confirm过程执行成功,则该事务结束
  • Cancel

    • 将用户A的帐户增长100元
    • 将流水的状态设为交易失败

在传统事务机制中,业务逻辑的执行和事务的处理,是在不一样的阶段由不一样的部件来完成的:业务逻辑部分访问资源实现数据存储,其处理是由业务系统负责;事务处理部分经过协调资源管理器以实现事务管理,其处理由事务管理器来负责。两者没有太多交互的地方,因此,传统事务管理器的事务处理逻辑,仅须要着眼于事务完成(commit/rollback)阶段,而没必要关注业务执行阶段。

TCC全局事务必须基于RM本地事务来实现全局事务

TCC服务是由Try/Confirm/Cancel业务构成的, 其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操做,必需要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。

这一点不难理解,考虑一下以下场景:

 

title

 

假设图中的服务B没有基于RM本地事务(以RDBS为例,可经过设置auto-commit为true来模拟),那么一旦[B:Try]操做中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则须要判断[B:Try]中哪些操做已经写到DB、哪些操做尚未写到DB:假设[B:Try]业务有5个写库操做,[B:Cancel]业务则须要逐个判断这5个操做是否生效,并将生效的操做执行反向操做。

不幸的是,因为[B:Cancel]业务也有n(0<=n<=5)个反向的写库操做,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。由于,相比第一次[B:Cancel]操做,后续的[B:Cancel]操做还须要判断先前的[B:Cancel]操做的n(0<=n<=5)个写库中哪几个已经执行、哪几个尚未执行,这就涉及到了幂等性问题。而对幂等性的保障,又极可能还须要涉及额外的写库操做,该写库操做又会由于没有RM本地事务的支持而存在相似问题。。。可想而知,若是不基于RM本地事务,TCC事务框架是没法有效的管理TCC全局事务的。

反之,基于RM本地事务的TCC事务,这种状况则会很容易处理:[B:Try]操做中途执行失败,TCC事务框架将其参与RM本地事务直接rollback便可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操做涉及的RM本地事务已经rollback”的状况下,根本无需执行[B:Cancel]操做。

换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不须要考虑部分执行的状况。

TCC事务框架应该提供Confirm/Cancel服务的幂等性保障

通常认为,服务的幂等性,是指针对同一个服务的屡次(n>1)请求和对它的单次(n=1)请求,两者具备相同的反作用。

在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其缘由不少。好比,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。所以,故障恢复机制后续仍然会从新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。

既然Confirm/Cancel业务可能会被屡次调用,就须要保障其幂等性。 那么,应该由TCC事务框架来提供幂等性保障?仍是应该由业务系统自行来保障幂等性呢? 我的认为,应该是由TCC事务框架来提供幂等性保障。若是仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是能够的;然而,这是一类公共问题,毫无疑问,全部TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;并且,考虑一下由业务系统来负责幂等性须要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

4. 服务部署

当咱们完成业务代码的开发后,就须要进入部署阶段。在部署过程当中,咱们将会引入持续集成、持续交付、持续部署,而且阐述如何在微服务中使用他们。

4.1 持续集成、持续部署、持续交付

在介绍这三个概念以前,咱们首先来了解下使用了这三个概念以后的软件开发流程,以下图所示:

 

首先是代码的开发阶段,当代码完成开发后须要提交至代码仓库,此时须要对代码进行编译、打包,打包后的产物被称为“构建物”,如:对Web项目打包以后生成的war包、jar包就是一种构建物。此时的构建物虽然没有语法错误,但其质量是没法保证的,必须通过一系列严格的测试以后才能具备部署到生产环境的资格。咱们通常会给系统分配多套环境,如开发环境、测试环境、预发环境、生产环境。每套环境都有它测试标准,当构建物完成了一套环境的测试,并达到交付标准时,就会自动进入下一个环境。构建物依次会通过这四套环境,构建物每完成一套环境的验证,就具有交付给下一套环境的资格。当完成预发环境的验证后,就具有的上线的资格。

测试和交付过程是相互伴随的,每一套环境都有各自的测试标准。如在开发环境中,当代码提交后须要经过编译、打包生成构建物,在编译的过程当中会对代码进行单元测试,若是有任何测试用例没经过,整个构建流程就会被停止。此时开发人员须要当即修复问题,并从新提交代码、从新编译打包。

当单元测试经过以后,构建物就具有了进入测试环境的资格,此时它会被自动部署到测试环境,进行新一轮的测试。在测试环境中,通常须要完成接口测试和人工测试。接口测试由自动化脚本完成,这个过程完成后还须要人工进行功能性测试。人工测试完成后,须要手动触发进入下一个阶段。

此时构建物将会被部署到预发环境。预发环境是一种“类生产环境”,它和生产环境的服务器配置须要保持高度一致。在预发环境中,通常须要对构建物进行性能测试,了解其性能指标是否能知足上线的要求。当经过预发验证后,构建物已经具有了上线的资格,此时它能够随时上线。

上述过程涵盖了持续集成、持续交付、持续部署,那么下面咱们就从理论角度来介绍这三个概念。

4.1.1 持续集成

“集成”指的是修改后/新增的代码向代码仓库合并的过程,而“持续集成”指的是代码高频率合并。这样有什么好处呢?你们不妨想想,若是咱们集成代码的频率变高了,那么每次集成的代码量就会变少,因为每次集成的时候都会进行单元测试,从而当出现问题的时候问题出现的范围就被缩小的,这样就能快速定位到出错的地方,寻找问题就更容易了。此外,频繁集成可以使问题尽早地暴露,这样解决问题的成本也就越低。由于在软件测试中有这样一条定律,时间和bug修复的成本成正比,也就是时间越长,bug修复的成本也就越大。因此持续集成可以尽早发现问题,并可以及时修复问题,这对于软件的质量是很是重要的。

4.1.2 持续部署

“持续部署”指的是当存在多套环境时,当构建物成完上一套环境的测试后,自动部署到下一套环境并进行一系列的测试,直到构建物知足上线的要求为止。

4.1.3 持续交付

当系统经过了全部的测试以后,就具有了部署到生产环境的资格,这个过程也就被称为“交付”。“持续交付”指的是每一个版本的构建物都具备上线的资格,这就要求每当代码库中有新的版本后,都须要自动触发构建、测试、部署、交付等一系列流程,当构建物在某个阶段的测试未经过时,就须要开发人员当即解决这个问题,并从新构建,从而保证每一个版本的构建物都具有上线的资格,能够随时部署到生产环境中。

4.2 微服务与持续集成

当咱们了解了持续集成后,下面来介绍微服务如何与持续集成相整合。当咱们对系统进行了微服务化后,本来单一的系统被拆分红多个课独立运行的微服务。单服务系统的持续集成较为简单,代码库、构建和构建物之间都是一对一的关系。然而,当咱们将系统微服务化后,持续集成就变得复杂了。下面介绍两种在微服务中使用持续集成的方法,分别是单库多构建和多库多构建,并依次介绍这两种方式的优缺点及使用场景。

4.2.1 单库多构建

“单库”指的是单个代码仓库,即整个系统的多个模块的代码均由一个代码仓库维护。“多构建”指的是持续集成平台中的构建项目会有多个,每一个构建都会生成一个构建物,以下如所示:

 

 

在这种持续集成的模式中,整个项目的全部代码均在同一个代码仓库中维护。但在持续集成平台中,每一项服务都有各自独立的构建,从而持续集成平台可以为每一项服务产出各自的构建物。

这种持续集成的模式在微服务架构中显然是不合理的。首先,一个系统的可能会有不少服务构成,若是将这些服务的代码均在同一个代码仓库中维护,那么一个程序员在开发服务A代码的时候颇有可能会由于疏忽,修改了服务B的代码,此时服务B构建以后就会存在安全隐患,若是这个问题在服务B上线前被发现,那么还好,但无疑增长了额外的工做量;但若是这个问题及其隐讳,致使以前的测试用例没有覆盖到,从而服务B会带着这个问题进入生产环境,这可能会给企业带来巨大的损失。因此,在微服务架构中,尽可能选择多库多构建模式来实现持续集成,它将带来更大的安全性。

虽然这种模式不合理,但它也有存在的必要性,当咱们在项目建设初期的时候,这种模式会给咱们带来更多的便利性。由于项目在建设初期,服务之间的边界每每是比较模糊的,并且须要通过一段时间的演化才可以构建出稳定的边界。因此若是在项目建设初期直接使用微服务架构,那么服务边界频繁地调整会极大增长系统开发的复杂度,你要知道,在多个系统之间调整边界比在单个系统的多个模块之间调整边界的成本要高不少。因此在项目建设初期,咱们可使用单服务结构,服务内部采用模块做为将来各个微服务的边界,当系统演化出较为清晰、稳定的边界后再将系统拆分红多个微服务。此时代码在同一个代码仓库中维护是合理的,这也符合敏捷开发中快速迭代的理念。

4.2.2 多库多构建

 

 

当系咱们的系统拥有了稳定、清晰的边界后,就能够将系统向微服务架构演进。与此同时,持续集成模式也能够从单库多构建向多库多构建演进。

在多库多构建模式中,每项服务都有各自独立的代码仓库,代码仓库之间互不干扰。开发团队只需关注属于本身的某几项服务的代码仓库便可。每一项服务都有各自独立的构建。这种方式逻辑清晰,维护成本较低,并且能避免单库多构建模式中出现的影响其余服务的问题。

4.3 微服务构建物

持续集成平台对源码编译、大包后生成的产物称为“构建物”。根据打包的粒度不一样,能够将构建物分为以下三种:平台构建物、操做系统构建物和镜像构建物。

4.3.1 平台构建物

平台构建物指的是由某一特定平台生成的构建物,好比JVM平台生成的Jar包、War包,Python生成的egg等都属于平台构建物。但平台构建物运行须要部署在特定的容器中,如war须要运行在Servlet容器中,而Servlet容器又依赖的JVM环境。因此若要部署平台构建物,则须要先给它们提供好运行所需的环境。

4.3.2 操做系统构建物

操做系统构建物是将系统打包成一个操做系统可执行程序,,如CentOS的RPM包、Windows的MSI包等。这些安装包能够在操做系统上直接安装运行。但和平台构建物相同的是,操做系统构建物每每也须要依赖于其余环境,因此也须要在部署以前搭建好安装包所需的依赖。此外,配置操做系统构建物的复杂度较大,构建的成本较高,因此通常不使用这种方式,这里仅做介绍。

4.3.3 镜像构建物

平台构建物和操做系统构建物都有一个共同的缺点就是须要安装构建物运行的额外依赖,增长部署复杂度,而镜像构建物能很好地解决这个问题。

咱们能够把镜像理解成一个小型操做系统,这个操做系统中包含了系统运行所需的全部依赖,并将系统也部署在这个“操做系统”中。这样当持续集成平台构建完这个镜像后,就能够直接运行它,无需任何依赖的安装,从而极大简化了构建的复杂度。可是,镜像每每比较庞大,构建镜像的过程也较长,从而当咱们将生成的镜像从持续集成服务器发布到部署服务器的时间将会很长,这无疑下降了部署的效率。不过好在Docker的出现解决了这一问题。持续集成平台在构建过程当中并不须要生成一个镜像,而只需生成一个镜像的Dockerfile文件便可。Dockerfile文件用命令定义了镜像所包含的内容,以及镜像建立的过程。从而持续集成服务器只需将这个体积较小的镜像文件发布到部署服务器上便可。而后部署服务器会经过docker build命令基于这个Dockerfile文件建立镜像,并建立该镜像的容器,从而完成服务的部署。

相对于平台构建物和操做系统构建物而言,镜像构建物在部署时不须要安装额外的环境依赖,它把环境依赖的配置都在持续集成平台构建Dockerfile文件时完成,从而简化了部署的过程。

5. 参考文献

大规模SOA系统中的分布事务处理_程立

Life beyond Distributed Transactions: an Apostate’s Opinion

关于如何实现一个TCC分布式事务框架的一点思考

How can a requestor ensure a consistent outcome across multiple, independent providers

关于分布式事务、两阶段提交协议、三阶提交协议

Three-phase commit protocol

微服务架构技术栈选型手册

微服务架构适用场景分析

分库分表的几种常见形式以及可能遇到的难

相关文章
相关标签/搜索