众所周知,微服务架构解决了不少问题,经过分解复杂的单体式应用,在功能不变的状况下,使应用被分解为多个可管理的服务,为采用单体式编码方式很难实现的功能提供了模块化的解决方案。同时,每一个微服务独立部署、独立扩展,使得持续化集成成为可能。由此,单个服务很容易开发、理解和维护。git
微服务架构为开发带来了诸多好处的同时,也引起了不少问题。好比服务运维变得更复杂,服务之间的依赖关系更复杂,数据一致性难以保证。spring
本篇文章将讨论和介绍Choerodon猪齿鱼是如何保障微服务架构的数据一致性的。数据库
主要内容包括 :网络
下面将经过一个实例来分别介绍这几种模式。架构
在Choerodon 猪齿鱼的 DevOps流程中,有这样一个步骤。并发
在讲微服务架构的数据一致性以前,先介绍一下传统关系型数据库是如何保证一致性的,从关系型数据库中的ACID理论讲起。app
ACID 即数据库事务正确执行的四个基本要素。分别是:框架
能够经过使用数据库自身的ACID Transactions,将上述步骤简化为以下伪代码:运维
transaction.strat(); createProject(); devopsCreateProject(); gitlabCreateGroup(); transaction.commit();
这个过程能够说是十分简单,若是在这一过程当中发生失败,例如DevOps建立项目失败,那么该事务作回滚操做,使得最终平台建立项目失败。因为传统应用通常都会使用一个关系型数据库,因此能够直接使用 ACID transactions。 保证了数据自己不会出现不一致。为保证一致性只须要:开始一个事务,改变(插入,删除,更新)不少行,而后提交事务(若是有异常时回滚事务)。异步
随着业务量的不断增加,单数据库已经不足以支撑庞大的业务数据,此时就须要对应用和数据库进行拆分,于此同时,也就出现了一个应用须要同时访问两个或者两个以上的数据库或多个应用分别访问不一样的数据库的状况,数据库的本地事务则再也不适用。
为了解决这一问题,分布式事务应运而生。
想象一下,若是不少用户同时对Choerodon 平台进行建立项目的操做,应用接收的流量和业务数据剧增。一个数据库并不足以存储全部的业务数据,那么咱们能够将应用拆分红IAM服务和DevOps服务。其中两个服务分别使用各自的数据库,这样的状况下,咱们就减轻了请求的压力和数据库访问的压力,两个分别能够很明确的知道本身执行的事务是成功仍是失败。可是同时在这种状况下,每一个服务都不知道另外一个服务的状态。所以,在上面的例子中,若是当DevOps建立项目失败时,就没法直接使用数据库的事务。
那么若是当一个事务要跨越多个分布式服务的时候,咱们应该如何保证事务呢?
为了保证该事务能够知足ACID,通常采用2PC或者3PC。 2PC(Two Phase Commitment Protocol),实现分布式事务的经典表明就是两阶段提交协议。2PC包括准备阶段和提交阶段。在此协议中,一个或多个资源管理器的活动均由一个称为事务协调器的单独软件组件来控制。
咱们为DevOps服务分配一个事务管理器。那么上面的过程能够整理为以下两个阶段:
准备阶段:
提交/回滚阶段:
2PC 提供了一套完整的分布式事务的解决方案,遵循事务严格的 ACID 特性。
可是,当在准备阶段的时候,对应的业务数据会被锁定,直到整个过程结束才会释放锁。若是在高并发和涉及业务模块较多的状况下,会对数据库的性能影响较大。并且随着规模的增大,系统的可伸缩性越差。同时因为 2PC引入了事务管理器,若是事务管理器和执行的服务同时宕机,则会致使数据产生不一致。虽然又提出了3PC 将2PC中的准备阶段再次一分为二的来解决这一问题,可是一样可能会产生数据不一致的结果。
不能否认,2PC 和3PC 提供了解决分布式系统下事务一致性问题的思路,可是2PC同时又是一个很是耗时的复杂过程,会严重影响系统效率,在实践中咱们尽可能避免使用它。因此在分布式系统下没法直接使用此方案来保证事务。
对于分布式的微服务架构而言,传统数据库的ACID原则可能并不适用。首先微服务架构自身的全部数据都是通 过API 进行访问。这种数据访问方式使得微服务之间松耦合,而且彼此之间独立很是容易进行性能扩展。其次 不一样服务一般使用不一样的数据库,甚至并不必定会使用同一类数据库,反而使用非关系型数据库,而大部分的 非关系型数据库都不支持2PC。
一个最直接的办法就是考虑数据的强一致性。根据Eric Brewer提出的CAP理论,只能在数据强一致性(C)和可用性(A)之间作平衡。
CAP 是指在一个分布式系统下,包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),而且三者不可得兼。
关系型数据库单节点保证了数据强一致性(C)和可用性(A),可是却没法保证分区容错性(P)。
然而在分布式系统下,为了保证模块的分区容错性(P),只能在数据强一致性(C)和可用性(A)之间作平衡。具体表现为在必定时间内,可能模块之间数据是不一致的,可是经过自动或手动补偿后可以达到最终的一致。
可用性通常是更好的选择,可是在服务和数据库之间维护事务一致性是很是根本的需求,微服务架构中应该选择知足最终一致性。
那么咱们应该如何实现数据的最终一致性呢?
什么是Event Sourcing(事件溯源)?
一个对象从建立开始到消亡会经历不少事件,传统的方式是保存这个业务对象当前的状态。但更多的时候,咱们也许更关心这个业务对象是怎样达到这一状态的。Event Sourcing从根本上和传统的数据存储不一样,它存储的不是业务对象的状态,而是有关该业务对象一系列的状态变化的事件。只要一个对象的状态发生变化,服务就须要自动发布事件来附加到事件的序列中。这个操做本质上是原子的。
如今将上面的订单过程用Event Sourcing 进行改造,将订单变更的一个个事件存储起来,服务监听事件,对订单的状态进行修改。
能够看到 Event Sourcing 完整的描述了对象的整个生命周期过程当中所经历的全部事件。因为事件是只会增长不会修改,这种特性使得领域模型十分的稳定。
Event sourcing 为总体架构提供了一些可能性,可是将应用程序的每一个变更都封装到事件保存下来,并非每一个人都能接受的风格,并且大多数人都认为这样很别扭。同时这一架构在实际应用实践中也不是特别的成熟。
可靠事件模式属于事件驱动架构,微服务完成操做后向消息代理发布事件,关联的微服务从消息代理订阅到该 事件从而完成相应的业务操做,关键在于可靠事件投递和避免事件重复消费。
可靠事件投递有两个特性:
有两种实现方式:
本地事件表方法将事件和业务数据保存在同一个数据库中,使用一个额外的“事件恢复”服务来恢 复事件,由本地事务保证更新业务和发布事件的原子性。考虑到事件恢复可能会有必定的延时,服务在完成本 地事务后可当即向消息代理发布一个事件。
使用本地事件表将事件和业务数据保存在同一个数据库中,会在每一个服务存储一份数据,在必定程度上会形成代码的重复冗余。同时,这种模式下的业务系统和事件系统耦合比较紧密,额外增长的事件数据库操做也会给数据库带来额外的压力,可能成为瓶颈。
针对本地事件表出现的问题,提出外部事件表方法,将事件持久化到外部的事件系统,事件系统 需提供实时事件服务以接收微服务发布的事件,同时事件系统还须要提供事件恢复服务来确认和恢复事件。
借助Kafka和可靠事件,能够经过以下代码实现订单流程。
// IAM ProjectService @Service @RefreshScope public class ProjectServiceImpl implements ProjectService { private ProjectRepository projectRepository; private UserRepository userRepository; private OrganizationRepository organizationRepository; @Value("${choerodon.devops.message:false}") private boolean devopsMessage; @Value("${spring.application.name:default}") private String serviceName; private EventProducerTemplate eventProducerTemplate; public ProjectServiceImpl(ProjectRepository projectRepository, UserRepository userRepository, OrganizationRepository organizationRepository, EventProducerTemplate eventProducerTemplate) { this.projectRepository = projectRepository; this.userRepository = userRepository; this.organizationRepository = organizationRepository; this.eventProducerTemplate = eventProducerTemplate; } @Transactional(rollbackFor = CommonException.class) @Override public ProjectDTO update(ProjectDTO projectDTO) { ProjectDO project = ConvertHelper.convert(projectDTO, ProjectDO.class); if (devopsMessage) { ProjectDTO dto = new ProjectDTO(); CustomUserDetails details = DetailsHelper.getUserDetails(); UserE user = userRepository.selectByLoginName(details.getUsername()); ProjectDO projectDO = projectRepository.selectByPrimaryKey(projectDTO.getId()); OrganizationDO organizationDO = organizationRepository.selectByPrimaryKey(projectDO.getOrganizationId()); ProjectEventPayload projectEventMsg = new ProjectEventPayload(); projectEventMsg.setUserName(details.getUsername()); projectEventMsg.setUserId(user.getId()); if (organizationDO != null) { projectEventMsg.setOrganizationCode(organizationDO.getCode()); projectEventMsg.setOrganizationName(organizationDO.getName()); } projectEventMsg.setProjectId(projectDO.getId()); projectEventMsg.setProjectCode(projectDO.getCode()); Exception exception = eventProducerTemplate.execute("project", EVENT_TYPE_UPDATE_PROJECT, serviceName, projectEventMsg, (String uuid) -> { ProjectE projectE = projectRepository.updateSelective(project); projectEventMsg.setProjectName(project.getName()); BeanUtils.copyProperties(projectE, dto); }); if (exception != null) { throw new CommonException(exception.getMessage()); } return dto; } else { return ConvertHelper.convert( projectRepository.updateSelective(project), ProjectDTO.class); } } }
// DEVOPS DevopsEventHandler @Component public class DevopsEventHandler { private static final String DEVOPS_SERVICE = "devops-service"; private static final String IAM_SERVICE = "iam-service"; private static final Logger LOGGER = LoggerFactory.getLogger(DevopsEventHandler.class); @Autowired private ProjectService projectService; @Autowired private GitlabGroupService gitlabGroupService; private void loggerInfo(Object o) { LOGGER.info("data: {}", o); } /** * 建立项目事件 */ @EventListener(topic = IAM_SERVICE, businessType = "createProject") public void handleProjectCreateEvent(EventPayload<ProjectEvent> payload) { ProjectEvent projectEvent = payload.getData(); loggerInfo(projectEvent); projectService.createProject(projectEvent); } /** * 建立组事件 */ @EventListener(topic = DEVOPS_SERVICE, businessType = "GitlabGroup") public void handleGitlabGroupEvent(EventPayload<GitlabGroupPayload> payload) { GitlabGroupPayload gitlabGroupPayload = payload.getData(); loggerInfo(gitlabGroupPayload); gitlabGroupService.createGroup(gitlabGroupPayload); } }
Saga是来自于1987年Hector GM和Kenneth Salem论文。在他们的论文中提到,一个长活事务Long lived transactions (LLTs) 会相对较长的占用数据库资源。若是将它分解成多个事务,只要保证这些事务都执行成功, 或者经过补偿的机制,来保证事务的正常执行。这一个个的事务被他们称之为Saga。
Saga将一个跨服务的事务拆分红多个事务,每一个子事务都须要定义一个对应的补偿操做。经过异步的模式来完 成整个Saga流程。
在Choerodon中,将项目建立流程拆分红多个Saga。
// ProjectService @Transactional @Override @Saga(code = PROJECT_CREATE, description = "iam建立项目", inputSchemaClass = ProjectEventPayload.class) public ProjectDTO createProject(ProjectDTO projectDTO) { if (projectDTO.getEnabled() == null) { projectDTO.setEnabled(true); } final ProjectE projectE = ConvertHelper.convert(projectDTO, ProjectE.class); ProjectDTO dto; if (devopsMessage) { dto = createProjectBySaga(projectE); } else { ProjectE newProjectE = projectRepository.create(projectE); initMemberRole(newProjectE); dto = ConvertHelper.convert(newProjectE, ProjectDTO.class); } return dto; } private ProjectDTO createProjectBySaga(final ProjectE projectE) { ProjectEventPayload projectEventMsg = new ProjectEventPayload(); CustomUserDetails details = DetailsHelper.getUserDetails(); projectEventMsg.setUserName(details.getUsername()); projectEventMsg.setUserId(details.getUserId()); ProjectE newProjectE = projectRepository.create(projectE); projectEventMsg.setRoleLabels(initMemberRole(newProjectE)); projectEventMsg.setProjectId(newProjectE.getId()); projectEventMsg.setProjectCode(newProjectE.getCode()); projectEventMsg.setProjectName(newProjectE.getName()); OrganizationDO organizationDO = organizationRepository.selectByPrimaryKey(newProjectE.getOrganizationId()); projectEventMsg.setOrganizationCode(organizationDO.getCode()); projectEventMsg.setOrganizationName(organizationDO.getName()); try { String input = mapper.writeValueAsString(projectEventMsg); sagaClient.startSaga(PROJECT_CREATE, new StartInstanceDTO(input, "project", newProjectE.getId() + "")); } catch (Exception e) { throw new CommonException("error.organizationProjectService.createProject.event", e); } return ConvertHelper.convert(newProjectE, ProjectDTO.class); }
// DevopsSagaHandler @Component public class DevopsSagaHandler { private static final Logger LOGGER = LoggerFactory.getLogger(DevopsSagaHandler.class); private final Gson gson = new Gson(); @Autowired private ProjectService projectService; @Autowired private GitlabGroupService gitlabGroupService; private void loggerInfo(Object o) { LOGGER.info("data: {}", o); } /** * 建立项目saga */ @SagaTask(code = "devopsCreateProject", description = "devops建立项目", sagaCode = "iam-create-project", seq = 1) public String handleProjectCreateEvent(String msg) { ProjectEvent projectEvent = gson.fromJson(msg, ProjectEvent.class); loggerInfo(projectEvent); projectService.createProject(projectEvent); return msg; } /** * 建立组事件 */ @SagaTask(code = "devopsCreateGitLabGroup", description = "devops 建立 GitLab Group", sagaCode = "iam-create-project", seq = 2) public String handleGitlabGroupEvent(String msg) { ProjectEvent projectEvent = gson.fromJson(msg, ProjectEvent.class); GitlabGroupPayload gitlabGroupPayload = new GitlabGroupPayload(); BeanUtils.copyProperties(projectEvent, gitlabGroupPayload); loggerInfo(gitlabGroupPayload); gitlabGroupService.createGroup(gitlabGroupPayload, ""); return msg; } }
能够发现,Saga和可靠事件模式很类似,都是将微服务下的事务做为一个个体,而后经过有序序列来执行。可是在实现上,有很大的区别。
可靠事件依赖于Kafka,消费者属于被动监听Kafka的消息,鉴于Kafka自身的缘由,若是对消费者进行横向扩展,效果并不理想。
而在 Saga 中,咱们为 Saga 分配了一个orchestrator做为事务管理器,当服务启动时,将服务中全部的 SagaTask 注册到管理器中。当一个 Saga 实例经过sagaClient.startSaga启动时,服务消费者就能够经过轮询的方式主动拉取到该实例对应的Saga数据,并执行对应的业务逻辑。执行的状态能够经过事务管理器进行查看,展示在界面上。
经过Choerodon的事务定义界面,将不一样服务的SagaTask 收集展现,能够看到系统中的全部Saga 定义以及所属的微服务。同时,在每个Saga 定义的详情中,能够详细的了解到该Saga的详细信息:
在这种状况下,当并发量增多或者 SagaTask 的数量不少的时候,能够很便捷的对消费者进行扩展。
Saga支持向前和向后恢复:
Choerodon 采用的是向前恢复,经过界面能够很方便的对事务的信息进行检索,当Saga发生失败时,也能够看到失败的缘由,而且手动进行重试。
经过Choerodon的事务实例界面,能够查询到系统中运行的全部Saga实例,掌握实例的运行状态,并对失败的实例进行手动的重试:
对于向前恢复而言,理论上咱们的子事务最终老是会成功的。可是在实际的应用中,可能由于一些其余的因素,形成失败,那么就须要有对应的故障恢复回滚的机制。
Saga是一个简单易行的方案,使用Saga的两个要求:
2PC是一个阻塞式,严格知足ACID原则的方案,可是由于性能上的缘由在微服务架构下并非最佳 的方案。
Event sourcing 为总体架构提供了一些可能性。可是若是随着业务的变动,事件结构自身发生必定的变化时,须要经过额外的方式来进行补偿,并且当下并无一个成熟完善的框架。
基于事件驱动的可靠事件是创建在消息队列基础上,每个服务,除了本身的业务逻辑以外还须要额外的事件 表来保存当前的事件的状态,因此至关因而把集中式的事务状态分布到了每个服务当中。虽然服务之间去中 心化,可是当服务增多,服务之间的分布式事务带来的应用复杂度也再提升,当事件发生问题时,难以定位。
而Saga下降了数据一致性的复杂度,简单易行,将全部的事务统一可视化管理,让运维更加简单,同时每个 消费者能够进行快速的扩展,实现了事务的高可用。
本篇文章出自 Choerodon猪齿鱼社区董凡。