Skip to main content

10 篇博文 含有标签「微服务

View All Tags

· 7 分钟阅读

Choerodon 猪齿鱼作为一个微服务框架需要解决微服务数据初始化本身具有的问题和复杂性,同时也需要满足框架本身特有的数据初始化需求,下面为大家介绍一下这方面的设计思想和实现。

微服务的数据初始化难题

先来看一下在微服务的数据初始化中常见的几个问题。

▍1.1 表结构的初始化和可平滑升级

表结构的定义在数据库初始化中是重中之重,它涉及到整个服务运行和利用数据库实现功能的方式,一般来说表结构定义和升级涉及到以下操作:创建表,创建字段,创建索引,修改索引,修改字段,重命名表,删除索引,删除字段,删除表。这些操作如果都需要对多种数据库进行兼容和可平滑升级,那么复杂度就会突然增加,基本不可能像传统应那样通 SQL 脚本进行管理,而猪齿鱼面临的就是这种情况。

▍1.2 跨服务数据的自动初始化

在微服务架构中,不可避免的会出现需要将数据初始化到其他服务的场景,比如猪齿鱼的大部分服务都需要初始化菜单数据到 IAM 服务,处理菜单列表的请求是由 IAM 服务处理的,然而对于微服务的部署而言,很多时候又不能运行初始化数据的时候连接多个数据源从而产生问题。而且微服务的部署可能不是全量的,存在这个部署不需要这个服务的情况,这种情况的初始化又需要修改初始化的脚本或者程序带来复杂性。

▍1.3 繁琐的编码化数据的自动发现

数据的初始化中有一类数据是可以从代码或者文档,或者其它地方收集提取出来的,并且这部分数据往往比较繁琐和庞大,比如在猪齿鱼中的权限鉴定需要 URL 与 Controller,Method 的映射关系,这部分数据如果手工进行初始化会产生很大的工作量,并且在实际代码修改后可能初始化数据没有更新产生问题。

猪齿鱼的本服务数据的初始化

认识到这些问题后接下来再来介绍一下猪齿鱼在多次迭代后对这些问题提出的解决方案。先来看对于本服务数据初始化的解决方案,这部分的具体实现可以参考开源代码:https://github.com/choerodon/choerodon-starters/tree/master/choerodon-liquibase

▍2.1 数据表结构的初始化

对于数据库表结构的初始化猪齿鱼采用 Liquibase 开源项目,具体为使用 Liquibase 的 Groovy DSL,增强了 Liquibase 的灵活性,并且 Liquibase 本身支持平滑升级和多数据库支持解决了表结构初始化的问题。

▍2.2 本服务预置数据的初始化

对于一些预置数据,包括预置的用户角色,以及自动化测试执行时候需要的预置数据,猪齿鱼使用 Excel 来辅助初始化的数据,方便操作,填充,关联。

猪齿鱼的跨服务数据的初始化

下面再看一下猪齿鱼关于跨服务数据初始化和自动发现数据的处理方式。

▍3.1 自动发现数据的初始化

服务启动后通过管理服务访问各个服务的通用接口从 ClassPath 中获取数据通过分布式事务进行初始化。

具体代码参考:https://github.com/choerodon/manager-service/blob/master/src/main/java/io/choerodon/manager/api/eventhandler/EurekaEventObserver.java

▍3.2 跨服务预置数据的初始化

使用与本服务预置数据一样格式的 Excel 进行填写数据,编译时将 Excel 转化为 Json 数据,最终和自动发现数据一同通过分布式事务初始化。 其中编译时将 Excel 生成 Json,并且通过 Maven 的依赖关系进行合并使用了猪齿鱼 Maven 插件,具体代码参考:https://github.com/choerodon/choerodon-starters/tree/master/choerodon-maven-plugin

结语

猪齿鱼数据初始化的方式从早期的 SQL 脚本,到 Liquibase,再加上为了满足菜单初始化需要而设计的独立 Python 初始化工具,在 0.17.0 版本中统一升级为 Liquibase Groovy + Excel 的形式,解决了目前遇到的所有问题。以上就是猪齿鱼数据初始化的整个迭代过程和实现思路,谢谢大家。

更多 Choerodon 猪齿鱼微服务相关文章 ▼

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 13 分钟阅读

作者 | Rebecca Pruess

编译 | 毛智伟

随着 DevOps 理念的普及与扩散,大家经常会看到持续集成(Continuous Integration)与持续交付(Continuous Delivery)这样的字眼,而怎样使用与选择这些方法成了大多数 IT 团队必须面对的问题。在讨论更加深入地讨论问题之前,首先需要清楚这两者之间的主要区别是什么,以及用什么方法可以更好改善工作流程,从而在更短的时间内为目标用户提供更高质量的软件。

devops

持续集成(CI)和持续交付(CD)都体现了如今快节奏市场中的文化和发展原则,旨在缩短开发周期、提高软件交付效率以及实现全流程的自动化。同时,两者都有着共同的目标:让软件开发更少地依赖于手动执行的任务,在此基础上使得软件的发布更加频繁、更加安全可靠。由于有着相同的目标,因此持续集成和持续交付并非相互排斥的。只是它们的应用范围有所不同。

那下面就来看下 CI 与 CD 之间的联系与区别。

什么是持续集成

如上所述,CI 和 CD 是相互关联的。持续集成是指软件个人研发的部分向软件整体部分交付,频繁进行集成以便更快地发现其中的错误。由此可见,CI 专注于定期地让开发人员构建小批量的代码。而对于更新或新增的代码,它们会被上传至统一的代码库,执行自动构建与自动化测试的步骤。 频繁地向主干提交代码,意味着可以针对整个软件执行所有的自动化测试,并且在应用或接口的某个部分出现问题时,及时收到告警信息。

由于合并问题能被及时发现,因此也能被及时解决。此外,由于测试过程采用的是自动化测试,因此最终的主干分支一直处于可发布的状态。而这对传统的瀑布式的开发流程来说就很棘手。遵循 CI 中定义的原则,有助于进一步提高代码的可测试性和可部署性。通过将代码保持在可部署状态,就能避免在项目后期才进行单独的测试和 Bug 的修复,由此使得开发人员避开了“集成地狱”。而这也是 Choerodon 猪齿鱼开发流水线模块的主要目的。

ci

什么是持续交付

持续集成包含了构建与自动化测试的阶段,而持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的“类生产环境”之中。持续交付优先于整个产品生命周期的软件部署,建立在高水平自动化持续集成之上。此外,持续交付同样遵循一个小型的构建周期,可以将一小批代码推送到多个环境:开发,测试或生产。

在此过程中,它结合了持续集成和持续部署的实践(即:让主干始终处于可部署状态)。而在 Choerodon 猪齿鱼平台中,当提交的代码完成以上步骤后,可以在“部署流水线-流水线管理”中创建对应的 CD 流水线将持续集成后产生的应用版本自动部署到对应的环境中去。此外,对于部署到正式环境的代码,可以在流水线中间添加一个人工卡点任务,只有通过人工审核后,才能执行后续的自动部署任务。

ci

理论上来说,CD 使得 IT 团队可以每天发布与更新应用程序,但大多数 IT 团队选择每月或每两个月发布更完整的更新。

持续集成与持续交付的区别

CI 和 CD 之间的区别在于使用的范围和主要的受益者。

(1)持续集成

持续集成对于加快编码和构建阶段的软件交付过程至关重要。因此,它的目标对象主要是开发人员,特别是那些处在复杂组织架构中的开发人员。通过自动构建和测试的流程,将对软件做的所有更改都集成到统一的代码库中,而无需进行手动任务。此外,由于 CI 是一个持续的过程,因此开发人员可以即时得到问题的反馈。他们可以实时获取到相关错误的信息,以便快速地定位与解决问题。显然这个过程可以大大地提高开发人员以及整个 IT 团队的工作效率。

(2)持续交付

持续交付涵盖了软件交付生命周期的绝大部分,能为目标用户和客户带来重大利益。CD 中包含了自动构建,打包,部署与测试的流程,以此来减少手动任务并加快软件交付速度。小批量的代码成功完成整个流程的每个阶段后,目标用户或客户便能在类生产环境中进行验收。因此目标用户可以在几天或几周内就收到修复后的功能与新增的功能,而无需等待数月后才更新。

CD 的部署频率也加快了整个流程中的反馈循环。最新版本真的解决了预期的问题吗?是否满足了用户的需求?在此用户就可以快速地验收并作出判断,而 IT 团队也可以在问题影响到开发周期之前就解决反馈的问题。持续的反馈循环使得用户与 IT 团队更紧密地合作,以确保能准确的理解与满足他们的需求。整个交付过程进度可视化,方便团队人员与客户了解项目的进度。

在当前快节奏的市场中,这无疑是一个重大的优势。当您将软件更快地推向市场时,您将获得更大的竞争优势。

CI 或 CD 适合您的业务场景吗

持续集成可确保代码库中始终保持最新的代码,同时可以快速集成来自多个开发人员的代码,并确保这些代码可在多个环境中协同工作。它通常有助于减少错误并通过自动化流程来减少手动任务。CI 可以实现代码的自动构建与测试,减少开发中的 Bug。因此,CI 适用于那些过度依赖手动任务和复杂构建过程的企业。

持续交付适用于需要缩短开发周期,更快地为目标用户提供软件的企业。CD 降低了部署新软件或升级已有软件的难度,且实现了全流程的自动化,因此您的团队无需手动执行复杂繁琐的任务,从而加快反馈速度,来确保您增加的功能真正地满足用户的需求。

总而言之,CI 和 CD 是相互补充的。CI 的统一代码库和自动化测试的方法可用于支持 CD 中更大规模的自动化和更频繁的部署。因此将 CI 和 CD 结合到您开发与交付的流程中,会使您的 IT 团队更加敏捷,更加快速地开发。

目前,大多数 CI / CD 的工具采用的方法都大同小异。 而一般的 DevOps 工具通常都会支持 CI 和 CD 方法,相应地还会提供相关的自动化测试框架。Choerodon 猪齿鱼平台中的 DevOps 模块便是结合了 CI 与 CD 的方法,并在此基础上实现了测试与部署的自动化。用户需要根据自己的实质需求来创建 CD 流水线,以此来实现不同环境不同版本类型的自动化部署;当然,您还可以在其中设置人工卡点任务,使得 CD 流水线随时处于人工的监控之下。

此外,也有不少人认为 CI 是 CD 的前提与基础,没有 CI 就不能实现 CD。这种说法也是比较流行的,其思路如下图。因此,不管是哪种说法,CI 与 CD 都是 DevOps 工具中不可或缺的理念与方法。

Devops Flow

原文地址: https://dzone.com/articles/continuous-integration-vs-continuous-delivery

更多 Choerodon 猪齿鱼持续交付相关文章 ▼

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 14 分钟阅读

本文是Choerodon 的微服务系列推文第五篇,上一篇《Choerodon 的微服务之路(四):深入理解微服务配置中心》介绍了配置中心在微服务架构中的作用,本篇将介绍微服务监控的重要性和必要性。

▌文章的主要内容包括:

  • 为什么要监控
  • 开发者需要监控哪些
  • 猪齿鱼的解决方案

在前面的几期的文章里,介绍了在 Choerodon 的微服务架构中,系统被拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持最低限度的集中式管理。

这样的架构决定了功能模块的部署是分布式的,不同的业务服务单独部署运行的,运行在独立的容器进程中,彼此之间通过网络进行服务调用交互。一次完整的业务流程会经过很多个微服务的处理和传递。

在这种情况下,如何监控服务的错误和异常,如何快速地定位和处理问题,以及如何在复杂的容器拓扑中筛选出用户所需要的指标,是 Choerodon 在监控中面临的首要问题。本文将会分享 Choerodon 在微服务下的监控思考,以及结合社区流行的 Spring Cloud、Kubernetes、Prometheus 等,打造的 Choerodon 的监控方案。

为什么要监控

在谈到 Choerodon 的监控之前,大家需要清楚为什么需要微服务下的监控。

传统的单体应用中,由于应用部署在具体的服务器上,开发者一般会从不同的层级对应用进行监控。比如猪齿鱼团队常用的方式是将监控分成基础设施、系统、应用、业务和用户端这几层,并对每一层分别进行监控。如下图所示。

而在微服务系统中,开发者对于监控的关心点是一样的,但是视角却发生了改变,从分层 + 机器的视角转化为以服务为中心的视角。在 Choerodon 中,传统的分层已经不太适用。服务是部署在 k8s 的 pod 中,而不是直接部署在服务器上。团队除了对服务器的监控之外,还需要考虑到 k8s 中容器的监控。同样,由于一个业务流程可能是通过一系列的业务服务而实现的,如何追踪业务流的处理也同样至关重要。

所以在微服务中,大家同样离不开监控,有效的监控能够帮开发者快速的定位故障,保护系统健康的运行。

平开发者需要监控哪些

在 Choerodon 中,将系统的使用人员分为应用的管理人员,开发人员,运维人员。而不同的人员在平台中所关心的问题则分别不同。

  • 作为应用的管理人员,需要查看到系统中各个节点实例的运行状态以及实例中应用的状态。
  • 作为开发人员,需要查看自己开发的服务在运行中的所有信息,也需要跟踪请求流的处理顺序和结果,并快速定位问题。
  • 作为运维人员,需要查看系统集群中服务器的 CPU、内存、堆栈等信息。需要查看K8S集群的运行状态,同时也需要查看各个服务的运行日志。

除了这些以外,还需要监控到如下的一些信息:

  • 服务的概览信息:服务的名称,相关的配置等基本信息。
  • 服务的拓扑关系:服务之间的调用关系。
  • 服务的调用链:服务之间的请求调用链。
  • 服务的性能指标:服务的CPU,内存等。
  • 接口的调用监控:接口的吞吐量,错误率,响应时间等。
  • 服务的日志数据:服务运行中产生的日志异常。

简而概之,对于 Choerodon 而言,开发者将监控聚焦在指标监控,调用监控和日志监控。

猪齿鱼的解决方案

在社区中,有很多对监控的解决方案,比如指标监控有 Prometheus,链路监控有 zipkin、pinpoint,skywalking,日志则有 elk。

Choerodon 具有多集群多环境管理能力,Choerodon 为需要监控的集群配置监控组件,并与Choerodon 所在集群的监控组件互通以及过滤多余数据,可以最大限度地减少多集群非同一局域网的外网带宽需求。在多集群环境中仍然可以感知所管理应用的运行状态和配置预警信息。

▌指标监控

Spring Boot 的执行器包含一系列的度量指标(Metrics)接口。当你请求 metrics 端点,你可能会看到类似以下的响应:

{
"counter.status.200.root": 20,
"counter.status.200.metrics": 3,
"counter.status.200.star-star": 5,
"counter.status.401.root": 4,
"gauge.response.star-star": 6,
"gauge.response.root": 2,
"gauge.response.metrics": 3,
"classes": 5808,
"classes.loaded": 5808,
"classes.unloaded": 0,
"heap": 3728384,
"heap.committed": 986624,
"heap.init": 262144,
"heap.used": 52765,
"nonheap": 0,
"nonheap.committed": 77568,
"nonheap.init": 2496,
"nonheap.used": 75826,
"mem": 986624,
"mem.free": 933858,
"processors": 8,
"threads": 15,
"threads.daemon": 11,
"threads.peak": 15,
"threads.totalStarted": 42,
"uptime": 494836,
"instance.uptime": 489782,
"datasource.primary.active": 5,
"datasource.primary.usage": 0.25
}

这些系统指标具体含义如下:

  • 系统内存总量(mem),单位:KB
  • 空闲内存数量(mem.free),单位:KB
  • 处理器数量(processors)
  • 系统正常运行时间(uptime),单位:毫秒
  • 应用上下文(应用实例)正常运行时间(instance.uptime),单位:毫秒
  • 系统平均负载(systemload.average)
  • 堆信息(heap,heap.committed,heap.init,heap.used),单位:KB
  • 线程信息(threads,thread.peak,thead.daemon)
  • 类加载信息(classes,classes.loaded,classes.unloaded)
  • 垃圾收集信息(gc.xxx.count, gc.xxx.time)

有了这些指标,我们只需要做简单的修改,就可以使这些指标被 Prometheus 所监测到。Prometheus 是一套开源的系统监控报警框架。默认情况下 Prometheus 暴露的metrics endpoint为/prometheus。

在项目的pom.xml文件中添加依赖,该依赖包含了 micrometer 和 prometheus 的依赖,并对监控的指标做了扩充。

<dependency>
<groupId>io.choerodon</groupId>
<artifactId>choerodon-starter-hitoa</artifactId>
<version>${choerodon.starters.version}</version>
</dependency>

Prometheus提供了4中不同的Metrics类型:CounterGaugeHistogramSummary。通过Gauge,Choerodon对程序的线程指标进行了扩充,添加了 NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED 这几种类型,具体代码如下。

@Override
public void bindTo(MeterRegistry registry) {

Gauge.builder("jvm.thread.NEW.sum", threadStateBean, ThreadStateBean::getThreadStatusNEWCount)
.tags(tags)
.description("thread state NEW count")
.register(registry);
Gauge.builder("jvm.thread.RUNNABLE.sum", threadStateBean, ThreadStateBean::getThreadStatusRUNNABLECount)
.tags(tags)
.description("thread state RUNNABLE count")
.register(registry);

Gauge.builder("jvm.thread.BLOCKED.sum", threadStateBean, ThreadStateBean::getThreadStatusBLOCKEDCount)
.tags(tags)
.description("thread state BLOCKED count")
.register(registry);

Gauge.builder("jvm.thread.WAITING.sum", threadStateBean, ThreadStateBean::getThreadStatusWAITINGCount)
.tags(tags)
.description("thread state WAITING count")
.register(registry);

Gauge.builder("jvm.thread.TIMEDWAITING.sum", threadStateBean, ThreadStateBean::getThreadStatusTIMEDWAITINGCount)
.tags(tags)
.description("thread state TIMED_WAITING count")
.register(registry);

Gauge.builder("jvm.thread.TERMINATED.sum", threadStateBean, ThreadStateBean::getThreadStatusTERMINATEDCount)
.tags(tags)
.description("thread state TERMINATED count")
.register(registry);
}

▌调用监控

在微服务架构中,一个请求可能会涉及到多个服务,请求的路径则可能构成一个网状的调用链。而如果其中的某一个节点发生异常,则整个链条都可能受到影响。

针对这种情况,团队需要有一款调用链监控的工具,来支撑系统监控分布式的请求追踪。目前社区中有一些工具:Zipkin、Pinpoint、SkyWalking。Choerodon 使用的是 SkyWalking,它是一款国产的 APM 工具,包括了分布式追踪、性能指标分析、应用和服务依赖分析等。

Skywalking 包含 Agent 和 Collecter,具体的部署和原理在这里不在做具体的介绍,Choerodon 的服务在每个服务的 DockerFile中,添加了对 Skywalking Agent 的支持。具体如下:

FROM registry.cn-hangzhou.aliyuncs.com/choerodon-tools/javabase:0.7.1
COPY app.jar /iam-service.jar
ENTRYPOINT exec java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap $JAVA_OPTS $SKYWALKING_OPTS -jar /iam-service.jar

部署时通过配置容器的环境变量 SKYWALKING_OPTS 来实现客户端的配置。

▌日志监控

日志是程序在运行中产生的遵循一定格式(通常包含时间戳)的文本数据,通常由Choerodon的服务生成,输出到不同的文件中,一般会有系统日志、应用日志、安全日志等等。这些日志分散地存储在不同的容器、机器中。当开发者在排查故障的时候,日志会帮助他们快速地定位到故障的原因。

Choerodon 采用了业界通用的日志数据管理解决方案,主要包括elasticsearchfluent-bitfluentdKibana。对于日志的采集分为如下几个步骤。

  • 日志收集:通过 fluent-bit 读取 k8s 集群中 cluster 的日志,并进行收集。
  • 日志过滤:通过 fluentd 将读取到的日志进行过滤,并进行缓存。
  • 日志存储:将过滤后的日志存储至 elasticsearch 集群中。
  • 日志展示:通过 kibana 查询 elasticsearch 中的日志数据,并用于展示。

通过端对端可视化的日志集中管理,给开发团队带来如下的一些好处:

  • 故障查找:通过检索日志信息,定位相应的 bug ,找出解决方案。
  • 服务分析:通过对日志信息进行统计、分析,了解服务器的负荷和服务运行状态。
  • 数据分析:分析数据,对用户进行行为分析。

写在最后

回顾一下这篇文章,介绍了微服务监控的重要性和必要性,以及 Choerodon 是如何应对指标监控,调用监控和日志监控这三种监控的。微服务架构下的服务规模大,系统相对复杂,也使得众多开发者成为了微服务的受害者。如何做好微服务下的监控,保障系统健康地运行,我们仍有许多需要继续努力的。

更多关于微服务系列的文章,点击蓝字即可阅读 ▼

总结

回顾一下这篇文章,整体介绍了配置中心在微服务架构中的作用,并提出了Choerodon对于配置管理的一些心得和规范。服务配置是服务运行起来的基础,而配置中心是整个微服务技术体系中的关键基础保障,如何做好服务配置规划,并推动项目的持续交付,则是Choerodon仍需持续考虑的问题。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 14 分钟阅读

▌文章的主要内容包括:

  • 配置是什么
  • 为什么需要微服务配置中心
  • Choerodon的配置中心

在早期单体应用的时代,监控等系统配置管理可能并不是什么困难的问题。但是在微服务架构中,和安全、日志、非功能需求一样,配置管理也是一种非功能需求。配置中心也是整个微服务架构体系中的一个重要组件,即使它的功能看上去并不起眼,无非就是简单配置管理和存取,但它是整个微服务架构中不可或缺的一环。

在Choerodon的微服务体系中,如何通过更加高效的配置管理方式,帮助微服务系统进行配置规划,推动项目的持续交付,动态调整和控制系统的运行状态,这些问题都值得深入思考和研究。

配置是什么

在讲配置中心之前,首先提到一个核心的概念,就是服务的“配置”。配置可能各位都不陌生,一个配置项大多是key-value的形式,value可能是一个有限值的集合。Choerodon通过这些配置项,来控制系统的运行状态,可以说,配置其实是独立于程序的可配变量,同一份程序在不同配置下会有不同的行为。

常见的配置有如下几种:

  • 程序内置的硬编码:在开发过程中将配置写死,这种形式几乎不具备任何的扩展性可动态修改的能力。通常是不建议的!
  • 程序配置文件:Springboot提供了一种方式将配置放在application.properties或者application.yaml文件中,系统运行时的大部分配置,都可以通过这种格式进行配置。不同环境会有各自的配置文件。
  • 环境变量:将配置预制在操作系统的环境变量中,在程序运行时读取。尤其在docker容器中,不同容器间相互隔离,环境变量不会受到影响。
  • 启动参数:可以在程序启动时制定参数,修改时需要重新启动。
  • 数据库存储:易变化的配置存储在数据库中,这样在运行期可以灵活的调整。

而微服务的配置中心,将散落在各处的应用系统配置信息收集起来,进行集中式的统一管理,并且提供额外功能,如动态刷新,版本控制,功能开关等。

举一个简单的例子,有这样的一个配置:logging.level。在生产环境上的日志级别通常为error级别,但是在一些测试环境,或者当系统出问题时,开发人员会期望日志的级别为debug,此时则需要一种机制来帮助实现。

为什么需要微服务配置中心

既然已经有了这么多种形式来管理配置,又为什么要引入配置中心呢?

在传统单体应用中,开发者将系统打成一个包,并在包里提供一些配置,当需要修改配置时,只需要登录到服务器上将配置修改,然后reload一下就可以了。

在微服务体系中,服务的数量以及配置信息日益增多,比如各种服务器参数配置、各种数据库访问参数配置、各种环境下配置信息的不同、配置信息修改之后实时生效等等,显然开发者已经不可能登录到一台台虚拟机中,对配置进行修改重启,并且,传统的配置管理可能会带来如下的一些问题:

  • 配置的格式不一致:有的使用xml,有的使用properties,还有一些存在数据库中,不同项目对于配置的管理方式五花八门。
  • 失效性低:配置大多使用静态文件的格式,再通过打包放入容器中运行。当实例过多时,需要修改完配置重新打包,周期较长。
  • 不安全:配置跟随源代码保存在代码库中,容易造成配置泄漏。同时不同环境的配置不同,稍不注意,可能会将测试环境的配置带入生产环境,引发事故。
  • 功能局限:不支持动态调整。

同时,随着“微服务+容器化+DevOps”落地,对于服务的配置管理也提出了更高的要求。

  • 代码与配置分离:遵循“一次打包,多处部署”的原则。将代码和配置分离。配置单独管理。
  • 配置抽象:屏蔽后台实现,提供页面管理功能。
  • 跨环境跨集群:支持对多环境和多集群应用配置的集中式管理。
  • 实时性:配置更新需要尽快通知到客户端。
  • 可治理:配置审计、版本管理、权限控制。

所以,一个能够方便用户进行自助式的配置管理,标准化的配置中心服务,是Choerodon社区里的开发者急切所需的。

Choerodon的配置中心

配置分类

在Choerodon猪齿鱼中,将服务的配置按照不同的功能,大致分为3类。

  • 静态配置:程序打包前一次配置好,一般不会修改。这些配置通常在服务启动之前生效。如:服务的端口,名称,配置中心的地址等。
  • 部署时配置:程序在部署时添加的配置,一般和环境相关,每个环境不同。如:数据库配置,其他中间件配置,或者一些服务相关的配置。
  • 运行时配置:程序运行时可能会修改的配置,一般用来调整应用的行为和功能。如:日志级别,功能开关,线程池大小等等。

服务配置设计原则

对配置进行分类之后,还需要一种设计原则,来指导开发者对服务的配置进行划分。在Choerodon猪齿鱼中,开发者遵循下面的一些原则。

  1. 所有可能要修改的配置,都不应该采取硬编码的方式。
  2. 程序中的配置文件,均使用yaml文件进行管理。
  3. 引入spring-cloud-config-client,将服务名,端口号,管理端口,等不会修改的配置,添加在src/main/resources/bootstrap.yml,如下所示:
# bootstrap.yml
server:
port: 8080
spring:
application:
name: iam-service
cloud:
config:
failFast: true
retry:
maxAttempts: 6
multiplier: 1.5
maxInterval: 2000
uri: localhost:8010
enabled: false
management:
port: 80381
security:
enabled: false
security:
basic:
enabled: false
  1. 将服务运行中需要的配置添加在src/main/resources/application.yml。包括注册中心的地址,数据库连接,并为这些配置添加默认值,如下所示:
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost/iam_service?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: choerodon
password: 123456
eureka:
instance:
preferIpAddress: true
leaseRenewalIntervalInSeconds: 10
leaseExpirationDurationInSeconds: 30
metadata-map:
VERSION: v1
client:
serviceUrl:
defaultZone: http://localhost:8000/eureka/
registryFetchIntervalSeconds: 10
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 15000
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
mybatis:
mapperLocations: classpath*:/mapper/*.xml
configuration: # 数据库下划线转驼峰配置
mapUnderscoreToCamelCase: true
  1. 开发阶段,每个开发人员自己的本地配置,添加在src/main/resources/application-default.yml文件中,并且该文件不应该上传到代码库中,例如本地的数据库连接配置。
# application-default.yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3307/iam_service?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: username
password: pwd
  1. 配置的优先级遵循:环境变量>配置文件。通过Chart在部署时根据环境的不同,修改具体的环境变量值,来实现不同环境配置的不同,如下图所示:

  1. 对于紧要的配置修改通过配置中心修改,动态生效。对于实时性要求不高的配置,通过修改部署时的values重新部署生效。

配置中心实现

首先来看Spring Cloud自带的配置中心是怎么样的,如下图所示:

Spring Cloud Config将不同环境的所有配置存放在git 仓库中,服务启动时通过接口拉取配置。遵循{ServiceID}-{profile}.properties的结构,按照profile拉取自己所需的配置。

当开发者修改了配置项之后,需要结合spring config bus将配置通知到对应的服务,实现配置的动态更新。

可以看到,Spring Cloud Config已经具备了一个配置中心的雏形,可以满足小型项目对配置的管理,但仍然有着很多局限性。配置使用git库进行管理,那么git库的权限如何来判断?不同环境的安全性也得不到保障。配置的添加和删除,配置项的汇总,也只能通过git命令来实现,对运维人员也并不友好。

Choerodon猪齿鱼在Spring Cloud Config的基础上,将配置由git库存储改为由db存储。并且添加单独的manager-service来对配置进行管理。config-server则专注于将配置传递给服务。

同时,服务在部署的时候,通过猪齿鱼提供的工具包choerodon-tool-config将服务中的application.yml文件初始化到数据库中。在服务运行时通过前端来对配置进行操作,及时更新配置到各服务上,流程如下图所示:

同时可以在页面上依据已有的配置创建新的配置,并将配置应用给正在运行的服务实例。对于失效的配置文件,也可以在页面中删除。如下图所示:

Choerodon猪齿鱼通过配置中心管理服务配置的创建,生效,乃至销毁,并结合Chart来管理服务部署时的配置,通过这样的一种机制,满足了对服务配置整个生命周期管理的基本要求和配置版本、动态生效等进一步的需求。

总结

回顾一下这篇文章,整体介绍了配置中心在微服务架构中的作用,并提出了Choerodon对于配置管理的一些心得和规范。服务配置是服务运行起来的基础,而配置中心是整个微服务技术体系中的关键基础保障,如何做好服务配置规划,并推动项目的持续交付,则是Choerodon仍需持续考虑的问题。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 15 分钟阅读

▌文章的主要内容包括:

  • 服务注册/发现
  • 服务注册表
  • 健康检查

在上一篇文章的开始,我们提到解决微服务架构中的通信问题,基本只要解决下面三个问题:

  • 服务网络通信能力
  • 服务间的数据交互格式
  • 服务间如何相互发现与调用

网络的互通保证了服务之间是可以通信的,通过对JSON 的序列化和反序列化来实现网络请求中的数据交互。Choerodon 的 API 网关则统一了所有来自客户端的请求,并将请求路由到具体的后端服务上。然而这里就会有一个疑问,API 网关是如何与后端服务保持通信的,后端服务之间又是如何来进行通信的?当然我们能想到最简单的方式就是通过 URL + 端口的形式直接访问(例如:http://127.0.0.1:8080/v1/hello)。

在实际的生产中,我们认为这种方式应该是被避免的。因为 Choerodon 的每个服务实例都部署在K8S的不同 pod中,每一个服务实例的 IP 地址和端口都可以改变。同时服务间相互调用的接口地址如何管理,服务本身集群化后又是如何进行负载均衡。这些都是我们需要考虑的。

为了解决这个问题,自然就想到了微服务架构中的注册中心。一个注册中心应该包含下面几个部分:

  • 服务注册/发现:服务注册是微服务启动时,将自己的信息注册到注册中心的过程。服务发现是注册中心监听所有可用微服务,查询列表及其网络地址。
  • 服务注册表:用来纪录各个微服务的信息。
  • 服务检查:注册中心使用一定的机制定时检测已注册的服务,如果发现某实例长时间无法访问,就会从服务注册表中移除该实例。

Choerodon 中服务注册的过程如下图所示:

服务注册/发现

当我们通过接口去调用其他服务时,调用方则需要知道对应服务实例的 IP 地址和端口。对于传统的应用而言,服务实例的网络地址是相对不变的,这样可以通过固定的配置文件来读取网络地址,很容易地使用 HTTP/REST 调用另一个服务的接口。

但是在微服务架构中,服务实例的网络地址是动态分配的。而且当服务进行自动扩展,更新等操作时,服务实例的网络地址则会经常变化。这样我们的客户端则需要一套精确地服务发现机制。

Eureka 是 Netflix 开源的服务发现组件,本身是一个基于REST的服务。它包含 Server 和 Client 两部分。

Eureka Server 用作服务注册服务器,提供服务发现的能力,当一个服务实例被启动时,会向 Eureka Server 注册自己的信息(例如IP、端口、微服务名称等)。这些信息会被写到注册表上;当服务实例终止时,再从注册表中删除。这个服务实例的注册表通过心跳机制动态刷新。这个过程就是服务注册,当服务实例注册到注册中心以后,也就相当于注册中心发现了服务实例,完成了服务注册/发现的过程。

阅读 Spring Cloud Eureka 的源码可以看到,在eureka-client-1.6.2.jar 的包中,com.netflix.discovery. DiscoveryClient 启动的时候,会初始化一个定时任务,定时的把本地的服务配置信息,即需要注册到远端的服务信息自动刷新到注册服务器上。该类包含了Eureka Client 向 Eureka Server 注册的相关方法。

在 DiscoveryClient 类有一个服务注册的方法 register(),该方法是通过 HTTP 请求向Eureka Server 注册。其代码如下:

boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}

对于 Choerodon 而言,客户端依旧采用 Eureka Client,而服务端采用 GoLang 编写,结合 K8S,通过主动监听 K8Spod 的启停,发现服务实例上线,Eureka Client 则通过 HTTP 请求获取注册表,来实现服务注册/发现过程。

注册中心启动时,会构造一个podController,用来监听pod 的生命周期。代码如下:

func Run(s *options.ServerRunOptions, stopCh <-chan struct{}) error {
... ...
podController := controller.NewController(kubeClient, kubeInformerFactory, appRepo)


go kubeInformerFactory.Start(stopCh)

go podController.Run(instance, stopCh, lockSingle)

return registerServer.PrepareRun().Run(appRepo, stopCh)
}

github.com/choerodon/go-register-server/controller/controller.go 中定义了 Controller,提供了 Run() 方法,该方法会启动两个进程,用来监听环境变量 REGISTER_SERVICE_NAMESPACE 中配置的对应 namespace 中的pod,然后在pod 启动时,将 pod 信息转化为自定义的服务注册信息,存储起来。在 pod 下线时,从存储中删除服务信息。其代码如下:

func (c *Controller) syncHandler(key string, instance chan apps.Instance, lockSingle apps.RefArray) (bool, error) {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return true, nil
}
pod, err := c.podsLister.Pods(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
if ins := c.appRepo.DeleteInstance(key); ins != nil {
ins.Status = apps.DOWN
if lockSingle[0] > 0 {
glog.Info("create down event for ", key)
instance <- *ins
}
}
runtime.HandleError(fmt.Errorf("pod '%s' in work queue no longer exists", key))
return true, nil
}
return false, err
}
_, isContainServiceLabel := pod.Labels[ChoerodonServiceLabel]
_, isContainVersionLabel := pod.Labels[ChoerodonVersionLabel]
_, isContainPortLabel := pod.Labels[ChoerodonPortLabel]
if !isContainServiceLabel || !isContainVersionLabel || !isContainPortLabel {
return true, nil
}
if pod.Status.ContainerStatuses == nil {
return true, nil
}
if container := pod.Status.ContainerStatuses[0]; container.Ready && container.State.Running != nil && len(pod.Spec.Containers) > 0 {
if in := convertor.ConvertPod2Instance(pod); c.appRepo.Register(in, key) {
ins := *in
ins.Status = apps.UP
if lockSingle[0] > 0 {
glog.Info("create up event for ", key)
instance <- ins
}
}
} else {
if ins := c.appRepo.DeleteInstance(key); ins != nil {
ins.Status = apps.DOWN
if lockSingle[0] > 0 {
glog.Info("create down event for ", key)
instance <- *ins
}
}
}
return true, nil
}

github.com/choerodon/go-register-server/eureka/repository/repository 中的 ApplicationRepository 提供了 Register() 方法,该方法手动将服务的信息作为注册表存储在注册中心中。

func (appRepo *ApplicationRepository) Register(instance *apps.Instance, key string) bool {

if _, ok := appRepo.namespaceStore.Load(key); ok {
return false
} else {
appRepo.namespaceStore.Store(key, instance.InstanceId)
}
appRepo.instanceStore.Store(instance.InstanceId, instance)
return true
}

通过上面的代码我们可以了解到Choerodon 注册中心是如何实现服务注册的。有了注册中心后,下面我们来介绍下服务发现中的服务注册表。

服务注册表

在微服务架构中,服务注册表是一个很关键的系统组件。当服务向注册中心的其他服务发出请求时,请求调用方需要获取注册中心的服务实例,知道所有服务实例的请求地址。

Choerodon 沿用 Spring Cloud Eureka 的模式,由注册中心保存服务注册表,同时客户端缓存一份服务注册表,每经过一段时间去注册中心拉取最新的注册表。

github.com/choerodon/go-register-server/eureka/apps/types 中定义了Instance 对象,声明了一个微服务实例包含的字段。代码如下:

type Instance struct {
InstanceId string `xml:"instanceId" json:"instanceId"`
HostName string `xml:"hostName" json:"hostName"`
App string `xml:"app" json:"app"`
IPAddr string `xml:"ipAddr" json:"ipAddr"`
Status StatusType `xml:"status" json:"status"`
OverriddenStatus StatusType `xml:"overriddenstatus" json:"overriddenstatus"`
Port Port `xml:"port" json:"port"`
SecurePort Port `xml:"securePort" json:"securePort"`
CountryId uint64 `xml:"countryId" json:"countryId"`
DataCenterInfo DataCenterInfo `xml:"dataCenterInfo" json:"dataCenterInfo"`
LeaseInfo LeaseInfo `xml:"leaseInfo" json:"leaseInfo"`
Metadata map[string]string `xml:"metadata" json:"metadata"`
HomePageUrl string `xml:"homePageUrl" json:"homePageUrl"`
StatusPageUrl string `xml:"statusPageUrl" json:"statusPageUrl"`
HealthCheckUrl string `xml:"healthCheckUrl" json:"healthCheckUrl"`
VipAddress string `xml:"vipAddress" json:"vipAddress"`
SecureVipAddress string `xml:"secureVipAddress" json:"secureVipAddress"`

IsCoordinatingDiscoveryServer bool `xml:"isCoordinatingDiscoveryServer" json:"isCoordinatingDiscoveryServer"`
LastUpdatedTimestamp uint64 `xml:"lastUpdatedTimestamp" json:"lastUpdatedTimestamp"`
LastDirtyTimestamp uint64 `xml:"lastDirtyTimestamp" json:"lastDirtyTimestamp"`
ActionType string `xml:"actionType" json:"actionType"`
}

客户端可以通过访问注册中心的/eureka/apps 接口获取对应的注册表信息。如下所示:

{
"name": "iam-service",
"instance": [
{
"instanceId": "10.233.73.39:iam-service:8030",
"hostName": "10.233.73.39",
"app": "iam-service",
"ipAddr": "10.233.73.39",
"status": "UP",
"overriddenstatus": "UNKNOWN",
"port": {
"@enabled": true,
"$": 8030
},
"securePort": {
"@enabled": false,
"$": 443
},
"countryId": 8,
"dataCenterInfo": {
"name": "MyOwn",
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"
},
"leaseInfo": {
"renewalIntervalInSecs": 10,
"durationInSecs": 90,
"registrationTimestamp": 1542002980,
"lastRenewalTimestamp": 1542002980,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1542002980
},
"metadata": {
"VERSION": "2018.11.12-113155-master"
},
"homePageUrl": "http://10.233.73.39:8030/",
"statusPageUrl": "http://10.233.73.39:8031/info",
"healthCheckUrl": "http://10.233.73.39:8031/health",
"vipAddress": "iam-service",
"secureVipAddress": "iam-service",
"isCoordinatingDiscoveryServer": true,
"lastUpdatedTimestamp": 1542002980,
"lastDirtyTimestamp": 1542002980,
"actionType": "ADDED"
}
]
}

我们可以在服务注册表中获取到所有服务的 IP 地址、端口以及服务的其他信息,通过这些信息,服务直接就可以通过 HTTP 来进行访问。有了注册中心和注册表之后,我们的注册中心又是如何来确保服务是健康可用的,则需要通过健康检查机制来实现。

健康检查

在我们提供了注册中心以及服务注册表之后,我们还需要确保我们的服务注册表中的信息,与服务实际的运行状态保持一致,需要提供一种机制来保证服务自身是可被访问的。在Choerodon微服务架构中处理此问题的方法是提供一个健康检查的端点。当我们通过HTTP 进行访问时,如果能够正常访问,则应该回复HTTP 状态码200,表示健康。

Spring Boot 提供了默认的健康检查端口。需要添加spring-boot-starter-actuator 依赖。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

访问/health 端点后,则会返回如下类似的信息表示服务的状态。可以看到 HealthEndPoint 给我们提供默认的监控结果,包含磁盘检测和数据库检测等其他信息。

{
"status": "UP",
"diskSpace": {
"status": "UP",
"total": 398458875904,
"free": 315106918400,
"threshold": 10485760
},
"db": {
"status": "UP",
"database": "MySQL",
"hello": 1
}
}
  • K8S 通过,/health 通过
  • K8S 通过,/health 未通过
  • K8S 未通过,/health 通过

第一种情况,当两种都通过的话,服务是可以被访问的。

第二种情况,K8S 认为服务是正常运行的,但注册中心认为服务是不健康的,注册表中不会记录该服务,这样其他服务则不能获取该服务的注册信息,也就不会通过接口进行服务调用。则服务间不能正常访问。如下图所示。

第三种情况,服务通过心跳告知注册中心自己是可用的,但是可能因为网络的原因,K8Spod 标识为不可访问,这样当其他服务来请求该服务时,则不可以访问。这种情况下服务间也是不能正常访问的。如下图所示。

同时,当我们配置了管理端口之后,该端点则需要通过管理端口进行访问。可以在配置文件中添加如下配置来修改管理端口。

management.port: 8081

但是在这种情况下,会使我们的健康检查变得更加复杂,健康检查并不能获取服务真正的健康状态。

在这种情况下,Choerodon 使用 K8S 来监听服务的健康端口,同时需要保证服务的端口与管理端口都能被正常访问,才算通过健康检查。可以在部署的 deploy 文件中添加readinessProbe 参数。

apiVersion: v1
kind: Pod
spec:
containers:
readinessProbe:
exec:
command:
- /bin/sh
- -c
- curl -s localhost:8081/health --fail && nc -z localhost 8080
failureThreshold: 3
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10

这样,当我们的服务启动之后,才会被注册中心正常的识别。当服务状态异常时,也可以尽快的从注册表中移除。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 3 分钟阅读

Choerodon猪齿鱼社区的架构师分别介绍了Choerodon猪齿鱼最新版本的各项核心功能,以及后续功能开发计划。比如在DevOps方面,会在已有部署、开发、构建相关的可视化图表基础上,增加新的报表,帮助用户直观了解开发情况。同时会增强分支管理与敏捷管理的任务可追溯性,更好地将开发过程融入项目管理。

双方在交流中,主要探讨了Choerodon使用过程中遇到的一些问题以及开发技巧。票易通团队从不同的角度提出了一些改进意见和需求,Choerodon猪齿鱼将从中汲取这些反馈,归纳出一些可以增加的功能和优化方向。

比如在部署实施方面,将权限进一步细化到单个应用、单个环境级别,使项目下的每个环境和应用可以单独授权给不同的成员;微服务方面,增加工具来进行K8S容器的监控和管理;基础组件应用方面,优化基础组件调度问题等。

本次两个团队的技术交流是Choerodon猪齿鱼社区首个团队面对面交流活动,Choerodon猪齿鱼社区致力于促进Choerodon猪齿鱼平台的创新与应用,提供分享与交流的平台,来帮助开发人员更好地了解和应用Choerodon猪齿鱼,使企业能够更快地构建出色的产品,最终实现敏捷化的应用交付和自动化的运营管理。

关于信息

欢迎通过我们的GitHub猪齿鱼社区进行反馈与贡献,帮助Choerodon猪齿鱼不断成长,我们将持续迭代优化,敬请期待。

· 18 分钟阅读

我们了解到在微服务架构中,一个完整的单体应用被拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持最低限度的集中式管理。本篇将介绍Choerodon在搭建微服务网关时考虑的一些问题以及两种常见的微服务网关。

▌文章的主要内容包括:

  • 为什么要使用API Gateway
  • 两种Gateway 模式
  • Choerodon 的网关

对于Choerodon 而言,前端通过ReactJs实现,后端服务则通过Java,GoLang等多种语言实现。我们通过将后端拆分成许多个单独的业务服务,选择不同的语言切实地帮助我们来实现系统功能,这种面向服务的模式给我们带来了开发的便捷性,但是也带来了新的问题。服务之间如何做到相互通信,前端与后端又是如何进行通信的,是我们需要去解决的问题。

回到微服务架构的领域,如果要解决基本的通信问题,基本上只要解决下面三个问题就可以了。

  • 服务网络通信能力
  • 服务间的数据交互格式
  • 服务间如何相互发现与调用

除了这些基本的问题以外,因为整个Choerodon是一个分布式的系统,开始时看似清晰的服务拆分,实则杂乱无章,有时候完成一个业务逻辑需要到不同的服务区调取接口,这是一件很痛苦的事,同时我们又不得不面对分布式的一些问题。包括负载均衡,链路追踪,限流,熔断,链路加密,服务鉴权等等一大堆的问题。于是一个面向服务治理、服务编排的组件——微服务网关,是我们首要考虑的解决方案。

为什么要使用API Gateway

我们回到文章开始时的三个问题,我们来考虑如何解决服务间的通信。

为什么使用HTTP?

HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),无论在哪种语言中几乎都有原生的支持,即使没有,也有第三方库来支持。通过HTTP 减少网络传输,来解决服务间网络的通信问题。

为什么选择JSON 作为数据交互格式?

因为 JSON 本身轻量、简洁,不管是编写,传输,还是解析都更加高效,而且相对来说,每个语言支持地都比较好。通过对JSON 的序列化和反序列化来实现网络请求中的数据交互。

为什么使用K8S 来进行服务发现?

对于负载均衡而言,业内已经有多种成熟的解决方案了,也大多是通过DNS 的方式去发现服务。不论是使用硬件F5 来解决,或者软件nginx,甚至Spring Cloud 也提供了对Eureka、Consul 等多种服务发现的支持。不过由于Choerodon 使用K8s 来作为服务编排引擎,基于K8s Client 来实现服务发现则更符合我们的切实需求。

当然对于Choerodon 而言,我们需要的不仅仅是一个简单的通信方式,而是一个完整的微服务解决方案。

API Gateway(API 网关)作为微服务体系里面的一部分,其需要解决的问题和 Choerodon 需要解决的问题非常类似。顾名思义,是企业 IT 在系统边界上提供给外部访问内部接口服务的统一入口。在微服务概念的流行之前,API网关的实体就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。

API 网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与 Facade 模式类似。API 网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。

如果没有 API 网关,大家可能想到的一贯做法是通过前端客户端与后端服务直接通信。这样会存在以下一些问题:

  • 客户端直连各个服务,耦合度太高
  • 所有的服务接口都需要暴露给客户端,会存在安全隐患
  • 当服务增多时,后端服务对于客户端而言则是一个噩梦
  • 多次访问不同的服务,请求数量过多,影响效率
  • 权限、身份校验等需要每个服务都实现,重复造轮子

Choerodon 通过使用 Spring Cloud Zuul,将所有的后端都通过统一的网关接入微服务体系中,并在网关层处理所有的非业务功能,同时提供统一的REST/HTTP 方式对外提供API。

单节点Gateway 模式

所谓的单节点Gateway 模式,也就是提供一个单一的Gateway 来支持不同的客户端访问。

这种模式下,大家会使用一个自定义 API 网关服务面对多个不同客户端应用程序。其中最大的好处就是所有的请求都受统一网关的控制,实现简单。对于请求的身份认证、负载均衡、监控等都可以在统一的网关中实现。

伴随这一好处的同时,也会带来一定的风险。因为随着后端服务的增多,网关的API 将针对不同客户端发展,越来越多。同时,由于接口权限、身份验证等都在网关中实现,统一网关也会变得越来越庞大,类似于一个单独的应用程序或者单体应用。

除此之外,也会引入一个新的问题,即资源隔离的问题。假设后端的一个服务突然变慢,由于所有的请求都使用同一个网关入口,可能会将网关拖垮,进而影响到其他服务接口的访问。

要解决这个问题,有两种方式可以去解决,一种是做线程池的隔离,可以给一些重要的业务一些单独的线程池,不重要的业务再放到一个大的单独的线程池里面。另一种就是给不同的业务设置不同的网关。

Spring Cloud 可以通过修改 ZUUL 和 hystrix 的配置,将信号量隔离修改为线程池隔离,提高性能。

zuul:
ribbonIsolationStrategy: THREAD

hystrix:
command:
default:
execution:
isolation:
strategy: THREAD #hystrix隔离策略,默认为THREAD
thread:
timeoutInMilliseconds: 20000 #hystrix超时时间
threadpool:
default:
coreSize: 100 #并发执行的最大线程数
maximumSize: 5000 #最大线程池大小
allowMaximumSizeToDivergeFromCoreSize: true #允许maximumSize 配置生效
maxQueueSize: -1 #设置最大队列大小,为-1时,使用SynchronousQueue

线程池隔离仅仅做到了线程池的隔离,但是 CPU 和 Memory 之类资源的隔离其实并没有做。如果想要更加彻底的隔离方式,可以采用和线程池隔离类似的方式,给重要的服务用独立的网关来为其服务,不重要的服务,再给一个独立的网关来服务。这也就是多节点的Gateway 模式。

多节点Gateway 模式

多节点的Gateway 模式本身是一种BFF架构,即为不同的设备提供不同的API接口,引申而来,也可以按照不同的业务类型划分为多种业务场景下的网关。

上面这张图显示了一个简化版本的多API 网关。在这种情况下,每个API 的边界是基于BFF 模式,因此只提供每个客户端应用所有要的API。

这种模式带来的好处是根据不同颗粒度的API网关,在性能上能够做到更精确的控制。但是在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,则需要在每个网关中重复实现。代码开发比较冗余。

可以说,这两种模式各有利弊,并不能单纯的比较其好坏,而应该根据实际的业务场景来选择适合自己的解决方案。

Choerodon 的网关

结合Choerodon 自身的核心业务,我们在不考虑多终端的情况下,最终选择了单一网关,并在此基础上,做了插件化的开发。

Choerodon 认为,一个网关应该包含两部分。

服务网关 = 路由转发 + 过滤器

路由转发:将外部的请求,转发到对应的微服务上 过滤器:包含一系列非功能的横切需求。例如权限校验、限流、监控等

我们在API 网关中保留了Spring Cloud Zuul 的路由转发,然后将权限校验等抽离到一个叫做gateway-helper 的服务中。如下图所示。

请求到达api-gateway 后,根据当前的HttpContext 上下文封装一个RibbonCommandContext 对象,该对象将包含了请求转发的gateway-helper 对应的信息。再由RibbonCommandFactory 根据RibbonCommandContext 对象生成一个RibbonCommand,由RibbonCommand 完成HTTP 请求的发送并的得到响应结果ClientHttpResponse。核心代码如下:

// GateWayHelperFilter.java
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
RibbonCommandContext commandContext = buildCommandContext(req);
try (ClientHttpResponse clientHttpResponse = forward(commandContext)) {
if (clientHttpResponse.getStatusCode().is2xxSuccessful()) {
request.setAttribute(HEADER_JWT, clientHttpResponse.getHeaders().getFirst(HEADER_JWT));
chain.doFilter(request, res);
} else {
setGatewayHelperFailureResponse(clientHttpResponse, res);
}
} catch (ZuulException e) {
res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
res.setCharacterEncoding("utf-8");
try (PrintWriter out = res.getWriter()) {
out.println(e.getMessage());
out.flush();
}
}
}

private RibbonCommandContext buildCommandContext(HttpServletRequest req) {
Boolean retryable = gatewayHelperProperties.isRetryable();
String verb = getVerb(req);
MultiValueMap<String, String> headers = buildZuulRequestHeaders(req);
MultiValueMap<String, String> params = buildZuulRequestQueryParams(req);
InputStream requestEntity;
long contentLength;
String requestService = gatewayHelperProperties.getServiceId();
requestEntity = new ByteArrayInputStream("".getBytes());
contentLength = 0L;
return new RibbonCommandContext(requestService, verb, req.getRequestURI(), retryable,
headers, params, requestEntity, this.requestCustomizers, contentLength);
}

private ClientHttpResponse forward(RibbonCommandContext context) throws ZuulException {
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
return command.execute();
} catch (HystrixRuntimeException ex) {
throw new ZuulException(ex, "Forwarding gateway helper error", 500, ex.getMessage());
}
}

可以看到,我们在api-gateway 服务中完成了对请求的首次转发。请求到达gateway-helper。在gateway-helper 中,针对配置进行判断,如果有自定义的helper,则会重定向到自定义的helper 上进行后续的处理。否则的话按照默认的逻辑进行权限校验。核心代码如下:

// RequestRootFilter.java
public boolean filter(final HttpServletRequest request) {
String uri = RequestRibbonForwardUtils.buildZuulRequestUri(request);
String service = RequestRibbonForwardUtils.getHelperServiceByUri(helperZuulRoutesProperties, uri);
if (StringUtils.isEmpty(service)) {
return requestPermissionFilter.permission(request) && requestRatelimitFilter.through(request);
}
return customGatewayHelperFilter(request, service, uri);
}

private boolean customGatewayHelperFilter(final HttpServletRequest request, final String service, final String uri) {
ClientHttpResponse clientHttpResponse = null;
try {
RibbonCommandContext commandContext = RequestRibbonForwardUtils.buildCommandContext(request, requestCustomizers, service, uri);
clientHttpResponse = RequestRibbonForwardUtils.forward(commandContext, ribbonCommandFactory);
return clientHttpResponse.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
LOGGER.warn("error.customGatewayHelperFilter");
return false;
} finally {
if (clientHttpResponse != null) {
clientHttpResponse.close();
}
}
}

RequestPermissionFilter 中,我们对请求进行权限校验,来判断该用户是否有对应资源的操作权限。核心代码如下:

//RequestPermissionFilterImpl.java
public boolean permission(final HttpServletRequest request) {
if (!permissionProperties.isEnabled()) {
return true;
}
//如果是文件上传的url,以/zuul/开否,则去除了/zuul再进行校验权限
String requestURI = request.getRequestURI();
if (requestURI.startsWith(ZUUL_SERVLET_PATH)) {
requestURI = requestURI.substring(5, requestURI.length());
}
//skipPath直接返回true
for (String skipPath : permissionProperties.getSkipPaths()) {
if (matcher.match(skipPath, requestURI)) {
return true;
}
}
//如果获取不到该服务的路由信息,则不允许通过
ZuulRoute route = ZuulPathUtils.getRoute(requestURI, helperZuulRoutesProperties.getRoutes());
if (route == null) {
LOGGER.info("error.permissionVerifier.permission, can't find request service route, "
+ "request uri {}, zuulRoutes {}", request.getRequestURI(), helperZuulRoutesProperties.getRoutes());
return false;
}
String requestTruePath = ZuulPathUtils.getRequestTruePath(requestURI, route.getPath());
final RequestInfo requestInfo = new RequestInfo(requestURI, requestTruePath,
route.getServiceId(), request.getMethod());
final CustomUserDetails details = DetailsHelper.getUserDetails();
//如果是超级管理员用户,且接口非内部接口,则跳过权限校验
if (details != null && details.getAdmin() != null && details.getAdmin()) {
return passWithinPermissionBySql(requestInfo);
}
//判断是不是public接口获取loginAccess接口
if (passPublicOrLoginAccessPermissionByMap(requestInfo, details)
|| passPublicOrLoginAccessPermissionBySql(requestInfo, details)) {
return true;
}
if (details == null || details.getUserId() == null) {
LOGGER.info("error.permissionVerifier.permission, can't find userDetail {}", requestInfo);
return false;
}
//其他接口权限权限审查
if (passSourcePermission(requestInfo, details.getUserId())) {
return true;
}
LOGGER.info("error.permissionVerifier.permission when passSourcePermission {}", requestInfo);
return false;
}

通过上述的代码片段可以看到。在Choerodon 中,可以自主实现自己的geteway-helper,来对请求进行更复杂的控制。

Choerodon 支持在页面上对路由信息进行配置和修改,控制路由的动态调整。如下图所示。

以看到,通过页面对路由进行修改后,路由动态更新到 api-gateway及gateway-heler。通过配置中心实时生效,避免了修改代码重新部署带来的麻烦。

总结

回顾一下这篇文章,我们介绍了Choerodon 在搭建微服务网关时考虑的一些问题以及两种常见的微服务网关,并且通过代码介绍了Choerodon 的网关时如何实现的。这些都是我们实践过程中的一些做法和体会,希望大家可以结合自己的业务来参考。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 6 分钟阅读

随着企业业务创新和应用复杂度的升高,传统的“瀑布式开发模型”面临着需求变更、过度开发、适应性不强等诸多问题,亟待改善。不仅如此,企业内部程序复杂,业务发展快,开发效率也逐渐变得愈发重要。

本次直播将介绍Choerodon猪齿鱼如何助力华润置地实现中台化转型,基于真实案例和实践经验,讲解Choerodon猪齿鱼如何帮助企业利用微服务和容器技术构建中台架构体系,打造以Choerodon猪齿鱼为核心的敏捷研发体系,聚焦业务,快速迭代,持续交付。

华润置地架构转型背景

华润置地有限公司是财富500强企业华润集团旗下的地产业务旗舰,是中国内地最具实力的综合型地产发展商之一,主营业务包括房地产开发、商业地产开发及运营、物业服务等。

华润置地一直重视企业的信息化建设,从最早期的采用ERP套件,到后面自主研发的一系列“烟囱式”应用,应用之间相互独立,系统功能重合,架构各异,伸缩扩展能力有限,服务器及人力资源浪费严重,产品交付周期长,运维工作繁重。

引入Choerodon猪齿鱼后,统一开发框架和平台,新的系统尽量采用微服务方式开发,基于敏捷迭代研发的思想,一般几周便可快速上线系统,部署周期从数周减少到几分钟,应用交付的效率提高了数十倍,容器平台由专门团队运维,项目组只需要专注于业务需求和交付,极大的降低了日常的运维成本,产品在设计、开发、运维等各个阶段均有改善。

Choerodon猪齿鱼是什么

Choerodon猪齿鱼 是一个全场景效能平台,基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理,并提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,来帮助企业聚焦于业务,加速数字化转型。

此次直播分享有什么?

本次直播主要介绍华润置地中台转型背景及落地过程。

主要内容包括:
  • 华润置地中台转型背景和架构体系介绍
  • Choerodon在华润置地的部署架构及业务架构
  • 转型过程
    • 单体应用架构向微服务应用架构转变
    • 传统部署架构向容器部署架构转变--DevOps落地实践
    • 瀑布式研发向敏捷迭代式研发方式转变
  • 实践经验总结
直播时间
10月15日(周一)下午 14:00
直播地址

欢迎各位提前报名

关于信息

欢迎通过我们的GitHub猪齿鱼社区进行反馈与贡献,帮助Choerodon猪齿鱼不断成长,我们将持续迭代优化,敬请期待。

· 29 分钟阅读

在Choerodon猪齿鱼设想之初,我们希望基于容器技术,整合DevOps工具链、微服务应用框架,开发一个企业级的PaaS平台,来帮助企业实现敏捷化的应用交付和自动化的运营管理。同时,也确定了技术堆栈的要求,即充分地使用主流成熟的组件,利用工具的扩展机制来构建平台,打造一个开放的技术平台和体系,让企业享受到社区的成果。

当然,罗马不是一天建造起来的。从初期确定技术栈到现在,除Choerodon猪齿鱼平台具体应用的实践与迭代,平台的技术栈也在不断进行着迭代。在社区中解决一个相同问题的有很多,而如何验证甄别哪些既能够满足现在的系统需求,在未来又有比较好的适应性,就给广大的架构师和软件设计师提出了挑战。根据Choerodon猪齿鱼实践经验,在使用时,可以从如下几个方面来进行考虑:

  • 语言 - 选择一个非常核心的考量是尽量使用应用比较广泛的开发技术,避免涉及相对来说的新技术、开发语言,这样可以进一步研发降低成本。例如Java的使用非常广泛。

  • 成熟度 - 的版本是否已经发布稳定版本或者更高版本是其成熟的重要标志,例如Istio在8月发布1.0版本,促使了很过公司和产品跟进使用;如果产品仍处于孕育阶段,则其技术变更的风险就比较大,例如 Apache 的 incubating 、 0.XXX版本或者beta版本等都有可能有各种技术缺陷或者没有经过较好的实际检验。

  • 社区 - 社区的活跃度在一定程度上反映了整个技术的生命力,例如是否有大量相关社区存在,K8s在国内就有Kubernetes中文社区,K8s中文社区等,以及定期还有各种关于K8s的Meetup或者论坛等;当然GitHub 上的 stars 的数量是一个参考指标,以及贡献者数量、代码更新频率等。

  • 生态圈 - 围绕是否有活跃健康的生态圈,是否有比较多的用户在使用,尤其是一些大公司,以及围绕产品是否有较多的文章、知识分享,或者围绕产品的某一块功能第三方增强工具,例如围绕Hadoop有Sqoop、Hbase、Hive等。

  • 文档 - 完备并及时更新的文档,非常有利于用户了解产品的设计思路、安装、使用等,方便产品在用户这边落地实践。想想 sourceforge.net 上如今已是代码的坟墓,没有文档的代码生命周期通常都不长。

  • 资源 - 如果在业界比较高的人气,应该有比较多的使用者,市场上相关人员资源也非常丰富,比较有利于团队技术人员的补充。

到目前为止,Choerodon猪齿鱼经过不断地迭代,逐渐形成了以 Spring Cloud + Kubernetes 为主体的微服务技术体系。

什么是微服务架构

在开始介绍之前,首先需要了解什么是微服务架构? 2014年初(该年可以称之为微服务的元年),微服务之父 Martin Fowler 在其博客上发表了"Microservices" 一文,文中正式提出了微服务架构风格,并指出了微服务架构的一些特点。

简单地说,微服务是系统架构上的一种设计风格,它的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通信协作。被拆分的每一个小型服务都围绕着系统中的某一项或一些耦合度较高的业务功能进行构建,并且每个服务都维护着自身的数据存储、业务开发、自动化测试案例以及独立部署机制。由于有了轻量级的通信协作基础,所以这些微服务可以使用不同的语言来编写。

-- 作者 James Lewis and Martin Fowler, 翻译自《Spring Cloud微服务实战》

在传统的单体应用系统架构中,一般分为三个部分,即数据库、服务应用端和前端展现,在业务发展初期,由于所有的业务逻辑在一个应用中,开发、测试、部署都比较容易。但是,随着业务的发展,系统为了应对不同的业务需求会不断为单体应用增加不同的业务模块。久而久之,不断扩充的业务需求导致单体应用的系统越来越庞大臃肿。此时,单体应用的问题也逐渐显现出来,由于单体应用是一个“整体”,往往修改一个小的功能,为了部署上线就会影响到其他功能的运行。而且,对于业务而言,往往不同模块对系统资源的要求不也尽相同,而单体应用各个功能模块因为无法分割,也就无法细化对系统资源的需求。所以,单体应用在初期是比较方便快捷,但是随着业务的发展,维护成本会越来越大,且难以控制。

Martin Fowler 认为微服务架构与单体应用最大的区别在于,微服务架构将一个完整的单体应用拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持低限度的集中式管理。下面这张图,很好的说明了单体架构和微服务架构的区别。

根据 Choerodon 猪齿鱼的开发实践和产品化经验,在互联网+、云计算和大数据、人工智能等的大背景下,构建软件系统产品,首先要把系统的基础框架搭好,方便后续的扩展。而微服务架的独立部署、松耦合等特点,与Choerodon猪齿鱼的想法和设计理念不谋而合, 所以 Choerodon 最终选择了微服务架构作为基础架构。

而在微服务基础框架中,有两个不得不提的微服务架构,分别是阿里巴巴的 Dubbo 和 Pivotal 公司开源的 Spring Cloud 。

Dubbo的诞生背景

Dubbo 是一个高性能、基于JAVA 的开源RPC 框架。阿里巴巴开源的 Dubbo 致力于提供高性能和透明化的RPC 远程服务调用方案,以及SOA 服务治理方案,使得应用可通过高性能RPC 实现服务的输出和输入功能,和 Spring 框架可以无缝集成。本质上而言,是一个服务框架。根据Dubbo 的官方 Roadmap 可以看到,Dubbo 的发展经历了如下几个过程:

  • 数据访问框架(ORM):早期的主流开发方式是面向对象的开发方式。只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。关系数据库是企业级应用环境中永久存放数据的主流数据存储系统,简化了增删改查工作量。

  • Web框架(MVC):随着访问量逐渐增大,单一应用已经无法满足业务需求,将应用拆分成互不相干的几个应用,分离视图层和业务逻辑层以提升效率。

  • 分布式服务框架(RPC):当垂直应用越来越多,应用之间交互不可避免,将业务抽取出来,作为独立的服务,逐渐形成稳定的分布式服务架构。

  • 面向服务的架构(SOA):当服务越来越多,业务和环境的变化越来越快时,对于资源的控制,性能的要求也就越发重要。SOA 将一个应用程序的业务逻辑或某些单独的功能模块化并作为服务呈现给消费者或客户端,使得业务IT 系统变得更加灵活。

Dubbo 按照分层来规划我们的系统,包含远程通讯、集群容错和自动发现三个核心部分。提供透明化的远程方法调用,使各个服务之间解耦合,并通过RPC的调用来实现服务的调用。

Dubbo 由于自身的设计使得服务之间的调用更加透明,网络消耗小,同时借助类似zookeeper 等分布式协调服务实现了服务注册。但是Dubbo 的缺点也是显而易见的,比如:

  • 只支持JAVA 使得Dubbo 在开发语言上受到了限制
  • 虽然RPC 相对于HTTP 而言性能更高,但是在网络通用性上却有着局限性
  • 而且对于一个微服务架构而言,包括服务网关,配置中心等很多东西都是缺失的,需要自己实现
  • 虽然Dubbo 很早就进行了开源,但是在很长一段时间官方都没有对开源版本进行维护

由于这些缺点,Choerodon 并没有选择 Dubbo 作为基础开发框架。

Spring Cloud 应运而生

在微服务架构的概念提出之后,很快 Netflix 公司将自家经过多年大规模生产验证的微服务架构,抽象落地形成 一整套开源的微服务基础组件 NetflixOSS 。2015年,Pivotal 将 NetflixOSS 开源微服务组件集成到其 Spring 体 系,并推出 Spring Cloud 微服务开发技术栈。自此,微服务技术迅猛普及,甚至 Spring Cloud一度成为了微服务的代名词。

可能更多了解微服务架构的读者都是从 Spring Cloud 入门,凭借之前 Spring Framework 的良好群众基础和Cloud 这个具有时代感的名字,Spring Cloud 的名字可以说是无人不知,无人不晓。结合Pivotal 公司的 Spring Boot,我们通过封装开源成熟的Spring Cloud 组件和一些基础的分布式基础服务,就可以简单快速的实现一个微服务框架,降低应用微服务化的门槛。

Spring Cloud 提出了一整套有关于微服务框架的解决方案。包括:

  • 服务注册发现:Spring Cloud Eureka
  • 负载均衡:Spring Cloud Netflix
  • 服务网关:Spring Cloud Zuul
  • 配置管理:Spring Cloud Config
  • 服务消费: Spring Cloud Ribbon/Feign
  • 分布式追踪:Spring Cloud Sleuth
  • 服务容错:Spring Cloud Hystrix
  • ...

下图说明了借助Spring Cloud 搭建的一套简单的微服务体系。

可以看到,Spring Cloud 集成了众多组件,从技术架构上降低了对大型系统构建的要求,使架构师以非常低的成本(技术或者硬件)搭建一套高效、分布式、容错的平台。但同时,在实际开发中也发现 Spring Cloud 存在着的一些问题:

  • 技术要求高:Spring Cloud 对于配置中心、熔断降级、分布式追踪、在权限认证、分布式事物等基本的功能,并没有提供一个成熟的组件,需要结合第三方的组件或自研实现。这对整个开发团队提出了非常高的技术要求。
  • 代码侵入性强:Spring Cloud 对业务代码有一定的侵入性,技术升级替换成本高,导致实施团队配合意愿低,微服务落地困难。
  • 服务运维困难:Spring Cloud 在服务调度和部署、服务日志、服务监控等仍有缺失。当服务的规模增大,对于微服务的管理有可能会增加运维的负担。
  • 多语言支持不足:对于大型公司而言,尤其是快速发展的互联网公司,企业的性质决定了多语言的技术栈、跨语言的服务调用也是常态,跨语言调用也恰恰是微服务概念诞生之初的要实现的一个重要特性之一。

Dubbo & Spring Cloud 对比

对比Dubbo 和Spring Cloud 可以发现,Dubbo 只是实现了服务治理,而Spring Cloud 的子项目则分别覆盖了微服务架构下的众多组件,而服务治理只是其中的一个方面。

通过谷歌和百度的搜索统计可以看到,自2015年起到现在,国内对于Spring Cloud 检索指数也在逐渐赶超Dubbo。

综合而言,Dubbo专注于服务治理;Spring Cloud关注于微服务架构生态。

虽然 Spring Cloud 降低了微服务化的门槛,但是除了基础的服务发现以外,Choerodon 团队在实际开发中也遇到了诸多的挑战。例如,各个组件并非完美无缺,很多组件在实际应用中都存在诸多不足和缺陷。 Spring Cloud 并不是银弹,微服务架构解决了单体系统变得庞大臃肿之后产生的难以维护的问题,但是也因为服务的拆分引发了诸多原本在单体应用中没有的问题,比如部署困难,监控困难,运维成本大,是选择 Spring Cloud 首先要面对的问题。

而容器化的普及,尤其是云原生技术生态的不断完善,比较好地解决了微服务架构的采用引发的诸多问题,使得微服务在普通传统企业的落地成为了可能。

Kubernetes + Docker 使微服务实施成为一种可能

当企业逐步接受微服务架构,享受着微服务带来好处的同时,也面临着微服务运维成本的增加, 环境的不一致,服务的编排、部署、迁移等诸多问题。Choerodon 平台经过不断地演进,从初期引入Docker,到Rancher + Jenkins,再到现在采用 Kubernetes 为容器编排和管理工具。

容器技术产生的主要原因,并不是因为资源浪费。主要是开发和运维人员环境不一致,导致开发效率大大降低。通过容器可以在一个完全隔离的环境中非常高效地运行代码,容器化天然适用于微服务,改善了引入Spring Cloud 微服务后开发效率大大降低的问题。但是单独使用Docker 并没有完整的解决微服务管理的痛点,服务的部署和运维仍然急需解决。

Kubernetes 是谷歌推出的容器编排引擎,是基于GoLang 实现的一个开源软件。K8s(Kubernetes)初源于谷歌内部的Borg,提供了面向应用的容器集群部署和管理系统,其目标旨在消除编排物理/虚拟计算、网络和存储基础设施的负担,并使应用程序运营商和开发人员完全将重点放在以容器为中心的原语上进行自助运营。

早期大家对于K8s 定位是容器编排引擎,同一时期流行的容器编排引擎还有MESOS、Docker Swarm 等。但是经过几年的发展,K8s 已经成为了云供应商的通用基础设施,我们熟悉的Google Cloud,AWS,Microsoft Azure,阿里云,华为云等等,都提供了对K8s 的支持。现在,K8s 已经不仅仅是一种工具,更多的是作为微服务架构的一种行业标准。

下图通过使用微服务架构的设计思想来看待K8s,并对K8s 中的一些功能进行了说明。

通过对比可以看到,在设计上,K8s本身就属于微服务架构的范畴。这里有的人可能就会有疑问,既然 K8s 功能这么强大,那么K8s 和 Spring Cloud 到底哪个更好?Choerodon 为什么不直接使用Kubernetes 作为基础的微服务架构呢?

为了区分Spring Cloud 和Kubernetes 两个项目的范围,下面这张图列出了几乎是端到端的微服务架构需求,从底层的硬件到上层的 DevOps 和自服务经验,并且列出了如何关联到Spring Cloud 和Kubernetes 平台。

可以看到:

  • Spring Cloud:自上而下,面向开发者,从应用代码到微服务架构的方方面面
  • Kubernetes:自下而上,面向基础设施,试图将微服务的问题在平台层解决,对开发者屏蔽复杂性

综上对比,K8s 遵循了微服务架构的基本核心要素,虽然在一些功能上有所欠缺,但不可否认,K8s 帮助补足了使用Spring Cloud 所缺失的一部分。

微服务“新秀”--Service Mesh

Choerodon 通过使用 Spring Cloud + Kubernetes 的模式,帮我们能够很容易的构建和部署微服务架构。但是在线上管理整个微服务体系的时候,仍然面临着一些难点。

一直以来都存在一个谬误,那就是在分布式系统中网络是可靠的。实际上网络是不可靠的,也是不安全的,微服务中大部分的故障都是出现在服务通信中。

K8s 帮我们实现了微服务的部署,但服务的网络调用、限流、熔断和监控这些问题,依旧让开发和运维人员都十分头痛。如何保证应用调用和事务的安全性与可靠性?Service Mesh 由此应运而生。

在过去几个月里,Service Mesh是行业内毋庸置疑的焦点。Service Mesh 译作“服务网格”或“服务栅格”,作为服务间通信的基础设施层。Service Mesh 是一种模式,而非技术。Buoyant 公司的 CEO Willian Morgan 在他的文章《WHAT’S A SERVICE MESH? AND WHY DO I NEED ONE?》中解释了什么是 Service Mesh。

A service mesh is a dedicated infrastructure layer for handling service-to-service communication It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.*

Service Mesh 本质上是一个轻量级的网络代理,好比应用程序或者说微服务间的 TCP/IP。

对于开发人员而言,在编写应用的时候无需关心网络这一层,使得服务回归到本质,专注于业务功能,服务中的交互则交给Service Mesh。Service Mesh 为服务提供了一个视图,提高了追踪能力,并提供了添加跟踪而不触及所有应用的能力,也就是所谓的Service Mesh 代码无侵入和透明性,能够帮助团队更好地管理服务。

Service Mesh 架构图:

可以看到,Service Mesh 通过一种Sidecar的模式。给每一个微服务实例部署一个Sidecar Proxy。该Sidecar Proxy 负责接管对应服务的入流量和出流量,并将微服务架构中的服务订阅、服务发现、熔断、限流、降级、 分布式跟踪等功能从服务中抽离到该Proxy 中。

Sidecar 以一个独立的进程启动,可以每台宿主机共用同一个Sidecar 进程,也可以每个应用独占一个Sidecar 进程。所有的服务治理功能,都由Sidecar 接管,应用的对外访问仅需要访问Sidecar 即可。当该Sidecar 在微服务中大量部署时,这些Sidecar 节点自然就形成了一个服务网格。

通过控制面组件对这些服务网格进行管理,这样也就提供了一种对于微服务进行高效而统一的管理方式。集中化的控制面板,同时仍然具有随心所欲的敏捷性和基于云的应用开发。这一特性,使得Service Mesh 成为大势所趋。

总 结

回顾微服务结构发展的这几年,微服务架构的逐渐普及,容器技术的兴起,云原生的趋势,微服务技术生态在不断地变化中,容器、Cloud Native、Serverless、Service Mesh,Knative 等新技术新理念你方唱罢我登场,使得整个以微服务为核心的生态越来越完善成熟。

像在前文中提到的Kubernetes、Service Mesh等都是解决微服务架构本身系统范围的问题,扎实可靠的基础框架,有利于后续的开发,这也是产品研发、系统实施的关键第一步。

除了系统本身的技术栈,工程落地实施是另一个要解决的问题。很多产品开始开发的时候,都不太注意规范化,待产品需求越来越复杂,人员越来越多时,整个项目会变得很难维护,甚至会影响产品的持续迭代。特别是微服务技术体系的引入,这个问题会更加明显。所以如果项目一开始,就以工程化的思想去组织代码,以规范化的流程去做构建发布,会给后续的发展打下坚实的基础。微服务的工程落地实施是Choerodon猪齿鱼一直关注和实践的方向,通过整合DevOps工具链和引入落地敏捷实施的方法论,让微服务架构的工程落地变得容易,这也成了Choerodon的PaaS平台能力,在这就不做赘述,感兴趣的读者可以到Choerodon的官网了解。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

· 26 分钟阅读

众所周知,微服务架构解决了很多问题,通过分解复杂的单体式应用,在功能不变的情况下,使应用被分解为多个可管理的服务,为采用单体式编码方式很难实现的功能提供了模块化的解决方案。同时,每个微服务独立部署、独立扩展,使得持续化集成成为可能。由此,单个服务很容易开发、理解和维护。

微服务架构为开发带来了诸多好处的同时,也引发了很多问题。比如服务运维变得更复杂,服务之间的依赖关系更复杂,数据一致性难以保证。

本篇文章将讨论和介绍Choerodon猪齿鱼是如何保障微服务架构的数据一致性的。

主要内容包括 :

  • 传统应用使用本地事务保持一致性
  • 多数据源下的分布式事务
  • 微服务架构中应满足数据最终一致性原则
  • 使用Event Sourcing保证微服务的最终一致性
  • 使用可靠事件模式保证微服务的最终一致性
  • 使用Saga保证微服务的最终一致性

下面将通过一个实例来分别介绍这几种模式。

在Choerodon 猪齿鱼的 DevOps流程中,有这样一个步骤。

  1. 用户在Choerodon 平台上创建一个项目;
  2. DevOps 服务对应创建一个项目;
  3. DevOps 为该项目 在 Gitlab 上创建对应的group。

传统应用使用本地事务保持一致性

在讲微服务架构的数据一致性之前,先介绍一下传统关系型数据库是如何保证一致性的,从关系型数据库中的ACID理论讲起。

ACID 即数据库事务正确执行的四个基本要素。分别是:

  • 原子性(Atomicity):要么全部完成,要么全部不完成,不存在中间状态
  • 一致性(Consistency):事务必须始终保持系统处于一致的状态
  • 隔离性(Isolation):事务之间相互隔离,同一时间仅有一个请求用于同一数据
  • 持久性(Durability):事务一旦提交,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚

可以通过使用数据库自身的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(分区容错性),并且三者不可得兼。

  • 一致性(Consistency),是指对于每一次读操作,要么都能够读到最新写入的数据,要么错误,所有数据变动都是同步的。
  • 可用性(Availability),是指对于每一次请求,都能够得到一个及时的、非错的响应,但是不保证请求的结果是基于最新写入的数据。即在可以接受的时间范围内正确地响应用户请求。
  • 分区容错性(Partition tolerance),是指由于节点之间的网络问题,即使一些消息丢包或者延迟,整个系统仍能够提供满足一致性和可用性的服务。

关系型数据库单节点保证了数据强一致性(C)和可用性(A),但是却无法保证分区容错性(P)。

然而在分布式系统下,为了保证模块的分区容错性(P),只能在数据强一致性(C)和可用性(A)之间做平衡。具体表现为在一定时间内,可能模块之间数据是不一致的,但是通过自动或手动补偿后能够达到最终的一致。

可用性一般是更好的选择,但是在服务和数据库之间维护事务一致性是非常根本的需求,微服务架构中应该选择满足最终一致性。

那么我们应该如何实现数据的最终一致性呢?

使用Event Sourcing保证微服务的最终一致性

什么是Event Sourcing(事件溯源)?

一个对象从创建开始到消亡会经历很多事件,传统的方式是保存这个业务对象当前的状态。但更多的时候,我们也许更关心这个业务对象是怎样达到这一状态的。Event Sourcing从根本上和传统的数据存储不同,它存储的不是业务对象的状态,而是有关该业务对象一系列的状态变化的事件。只要一个对象的状态发生变化,服务就需要自动发布事件来附加到事件的序列中。这个操作本质上是原子的。

现在将上面的订单过程用Event Sourcing 进行改造,将订单变动的一个个事件存储起来,服务监听事件,对订单的状态进行修改。

可以看到 Event Sourcing 完整的描述了对象的整个生命周期过程中所经历的所有事件。由于事件是只会增加不会修改,这种特性使得领域模型十分的稳定。

Event sourcing 为整体架构提供了一些可能性,但是将应用程序的每个变动都封装到事件保存下来,并不是每个人都能接受的风格,而且大多数人都认为这样很别扭。同时这一架构在实际应用实践中也不是特别的成熟

使用可靠事件模式保证微服务的最终一致性

可靠事件模式属于事件驱动架构,微服务完成操作后向消息代理发布事件,关联的微服务从消息代理订阅到该 事件从而完成相应的业务操作,关键在于可靠事件投递和避免事件重复消费。

可靠事件投递有两个特性:

  1. 每个服务原子性的完成业务操作和发布事件;
  2. 消息代理确保事件投递至少一次 (at least once)。避免重复消费要求消费事件的服务实现幂等性。

有两种实现方式:

1. 本地事件表

本地事件表方法将事件和业务数据保存在同一个数据库中,使用一个额外的“事件恢复”服务来恢 复事件,由本地事务保证更新业务和发布事件的原子性。考虑到事件恢复可能会有一定的延时,服务在完成本 地事务后可立即向消息代理发布一个事件。

使用本地事件表将事件和业务数据保存在同一个数据库中,会在每个服务存储一份数据,在一定程度上会造成代码的重复冗余。同时,这种模式下的业务系统和事件系统耦合比较紧密,额外增加的事件数据库操作也会给数据库带来额外的压力,可能成为瓶颈。

2. 外部事件表

针对本地事件表出现的问题,提出外部事件表方法,将事件持久化到外部的事件系统,事件系统 需提供实时事件服务以接收微服务发布的事件,同时事件系统还需要提供事件恢复服务来确认和恢复事件。

借助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保证微服务的最终一致性 - Choerodon的解决方案

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的补偿机制

Saga支持向前和向后恢复:

  • 向后恢复:如果任意一个子事务失败,则补偿所有已完成的事务
  • 向前恢复:如果子事务失败,则重试失败的事务

Choerodon 采用的是向前恢复,通过界面可以很方便的对事务的信息进行检索,当Saga发生失败时,也可以看到失败的原因,并且手动进行重试。

  通过Choerodon的事务实例界面,可以查询到系统中运行的所有Saga实例,掌握实例的运行状态,并对失败的实例进行手动的重试:

对于向前恢复而言,理论上我们的子事务最终总是会成功的。但是在实际的应用中,可能因为一些其他的因素,造成失败,那么就需要有对应的故障恢复回滚的机制。

使用Saga的要求 

Saga是一个简单易行的方案,使用Saga的两个要求: 

  • 幂等:幂等是每个Saga 多次执行所产生的影响应该和一次执行的影响相同。一个很简单的例子,上述流程中,如果在创建项目的时候因为网络问题导致超时,这时如果进行重试,请求恢复。如果没有幂等,就可能创建了两个项目。
  • 可交换:可交换是指在同一层级中,无论先执行那个Saga最终的结果都应该是一样的。

综合比较

2PC是一个阻塞式,严格满足ACID原则的方案,但是因为性能上的原因在微服务架构下并不是最佳 的方案。

Event sourcing 为整体架构提供了一些可能性。但是如果随着业务的变更,事件结构自身发生一定的变化时,需要通过额外的方式来进行补偿,而且当下并没有一个成熟完善的框架。

基于事件驱动的可靠事件是建立在消息队列基础上,每一个服务,除了自己的业务逻辑之外还需要额外的事件 表来保存当前的事件的状态,所以相当于是把集中式的事务状态分布到了每一个服务当中。虽然服务之间去中 心化,但是当服务增多,服务之间的分布式事务带来的应用复杂度也再提高,当事件发生问题时,难以定位。

而Saga降低了数据一致性的复杂度,简单易行,将所有的事务统一可视化管理,让运维更加简单,同时每一个 消费者可以进行快速的扩展,实现了事务的高可用。

关于猪齿鱼

Choerodon 猪齿鱼是一个全场景效能平台,基于 Kubernetes 的容器编排和管理能力,整合 DevOps 工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的平台,同时提供 IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献: