当前位置: 首页 > 科技观察

微服务架构设计实践的总结与思考

时间:2023-03-17 20:37:02 科技观察

今天继续聊聊微服务架构设计中的一些实践与思考。对于SOA和微服务,我之前的很多文章都有详细阐述。今天的文章主要关注架构设计和实践的一些关键点。微服务架构核心再次强调,微服务架构的核心是将传统的单体应用拆解成小的,同时拆分成小的微服务,通过轻量级的API接口相互通信。而这种分裂本身又分为多个方面。开发团队的拆分代码层的拆分可以独立构建和打包数据库的拆分拆分后引入DevOps和容器云技术,更加敏捷的开发和集成。同时考虑到SOA与中台思想的融合,考虑到API接口的复用性,也进行了前后端分离的个体微服务开发。从单体微服务的概念来看,微服务并不是指具体的HttpAPI接口服务,而是指拆分出来的微服务模块,所以微服务可以理解为:拆分DB+微服务模块+API接口提供。微服务架构的思想很符合现在分治复杂应用系统的思想。这与微服务出现之前组件化开发的思想是一致的,但是微服务思想之后,拆分出来的微服务更加高度解耦和独立。系统复杂性本身也分为功能和非功能级别。比如一个传统的大型业务系统,比如ERP、合同管理等,业务系统足够复杂,需要考虑分而治之,方便后期管理和扩展。二是非功能性需求带来的复杂性。比如一个业务系统,功能不多,但是文件存储和检索量很大,那么就需要把文件服务单独拆分成微服务。很早之前我就强调过,微服务的拆分虽然降低了单个微服务开发和实现的难度,但是却增加了集成的难度。拆分得越细,集成就越复杂。因此,如果不具备上述复杂度需求,业务系统就没有必要对微服务架构进行拆分改造。根据划分的子域拆分数据库在我们实际项目中,原来一个单一的业务系统在微服务之后实际上拆分成了20个微服务模块,所以按照标准的微服务原则,应该是后端也是分成20个数据库实例。但这会导致巨大的集成复杂度和大量的分布式事务处理问题。显然,在这个场景中,我们引入了业务域的概念,即数据库应该按照业务域或者子域进行拆分,多个微服务可以共享一个数据库。当多个微服务共享一个数据库实例时,微服务本身并没有完全解耦,但也可以实现代码层解耦。比如需求的变化导致微服务A发生变化,但是数据库没有变化。那么我们只需要不断的集成和发布微服务A模块即可。同时,在划分业务域之后再划分团队也更加方便,即开发团队也按照业务域进行划分,而不是一个开发团队只负责一个微服务模块。微服务和微服务API接口注意,微服务和微服务模块暴露的API接口是两个概念,它们本身就是微服务边界划分和微服务控制的两个粒度。在主流的微服务开发框架的实现中,类似于SpringCLoud的实现实际上已经达到了Eureka、CloudGateway等微服务的粒度,即使用微服务模块进行服务的注册和访问,而不是单独的API。接口服务。一旦微服务被注册和访问,消费者通过注册中心找到可用的微服务,微服务以声明方式暴露的所有API接口都是可用的。在微服务架构的发展下,团队其实应该有更清晰的边界和更粗粒度的接口暴露和交互,而不是简单的A团队发现B团队的微服务,里面的所有API接口都可以随意调用。这反而导致更多的内部规则和外包,也加强了两个微服务模块之间的耦合。简单来说,两个微服务并没有通过API接口调用实现真正的解耦,只是两个微服务之间的少量粗粒度API接口传递实现了真正的解耦。我们在实践中发现的一个关键问题是微服务也被拆分了,开发团队也被拆分了。但是,多个微服务之间仍然存在大量接口随意调用。这仍然是一个很难扩展的紧耦合架构。如上图所示,微服务A、D、E是由不同的开发团队开发的,所以它们之间的边界应该控制在具体的API接口粒度,而不是微服务粒度。比如微服务E只能消费微服务A暴露的第二个接口,而不能消费接口1。如果单纯采用微服务注册的方式,其实我们很难真正控制API接口的粒度。也就是说,我们需要自己编写相应的代码来做细粒度的安全控制。面向API的接口设计这是我过去一直在强调的一点,即大型项目或传统的大型单体应用微服务化时,架构设计必须是第一位的。架构设计的关键任务是:微服务模块拆分,包括微服务模块暴露的数据库拆分API接口标识定义。完成这两件事后,单个微服务就可以真正交付给不同的开发团队或组进行独立的设计和开发工作。同时,在微服务开发的过程中,需要针对API接口进行设计和开发。需要暴露给其他外部微服务的接口,首先要根据之前定义的接口契约来实现,再考虑内部功能逻辑的实现。接口优先的好处是大家遵循同一套接口契约,可以并行开展相关的设计开发工作。只要接口契约相同,后续集成多个微服务应该没有问题。API接口的治理控制需要提升到一个比较重要的位置。微服务架构实践中经常看到的是前期架构设计不足,相关边界划分不清晰,接口定义不明确,导致在设计开发过程中出现大量交互。后期的微服务,同时随意增加和定义新的API接口,在这种场景下,必然会导致后续接口交互和控制治理的混乱。比如后期做微服务变更的时候,我们很难快速分析出微服务或者API接口的变更会影响到哪些其他的微服务模块,完全不清楚微服务和API之间的交互依赖关系。这也是我很早就强调的一个观点。不要指望通过后期的APM或者服务链监控来解决微服务架构设计阶段的不足,前期应该按照自上而下的思路进行设计。在SOA分层架构中可以看到构建独立领域的复合微服务。最底层是原子服务,上面是组合服务,组合服务上面是流程服务。也就是说,服务本身也是分层的。虽然越高,组合服务的实际复用率会下降,但复用效率本身会加快。在微服务架构的实践中,将原本单一的应用拆分成不同的微服务,每个微服务都可以提供独立的API接口服务能力供前端使用。但是当前端需要组合多个微服务的能力时,这个能力应该放在哪里呢?比如我们前面举了一个例子。对于订单提交的操作,其实需要调用后端的订单中心、预算中心、库存中心。可以完成多个微服务接口。传统的方式,这种能力其实是在前端模块进行组合协调,但是你会发现,你开发的应用既有传统的BS端应用,也有APP应用,所以这种组合显然需要分两次重复实现地方。同时,这个组合规则本身也暴露给了不合理的前端。领域复合微服务实际上是一种特殊类型的微服务,即这类微服务本身完成多个微服务API接口的组合和编排,完成分布式事务的管理和协调,完成复合业务规则的实现和处理等。微服务本身没有自己独立的Owner数据库,即这类微服务不直接在数据库DB层进行数据访问和交互,而是直接复用已有的接口服务能力进行组合组装。在DDD领域驱动设计的架构分层中,领域层上有一个独立的应用层,对应于前面提到的领域组合微服务。在下域层,多个微服务提供粗粒度的API接口服务能力。微服务网关和API网关我之前写过一个微服务网关。API网关一般具有独立的服务注册接入、负载均衡和路由能力,而微服务网关一般通过与服务注册中心集成来实现服务注册发现、负载均衡和路由。简单的说,如果当前微服务A模块有100个接口服务。在服务注册发现中心的情况下,微服务A模块在部署后会被注册中心自动发现,并添加到可用集群列表中。所以微服务网关和注册中心集成后,所有的接口服务都会自动注册连接到微服务网关上。当用户访问网关提供的服务地址时,整体流程如下:在这个场景中可以看出,其实不需要在网关上一一注册API接口。但是,也不可能控制微服务的哪些特定接口连接到网关,哪些不连接。同时,这里的微服务网关实际上是整个微服务架构体系中的一个微服务模块,充当服务消费者的角色。也就是说,APP应用无法被整体的微服务框架所治理,所以相应的依赖包和代理SDK也无法分发给外部应用,所以这部分内容实际上是传递给微服务网关,帮助外部APP应用完成.对于一个相对独立的API网关来说,整个注册和访问过程都是在API网关上独立完成的,但是控制在API接口服务粒度上。当然也可以不使用微服务网关,直接使用Nginx之类的东西进行代理和路由转发,但是这时候需要手动配置微服务节点,实现心跳检测。你能看到的一个完整的微服务架构。比如有3个独立的开发团队开发自己的微服务,每个团队都采用前后端分离的开发模式。这时候每个团队其实可以启用自己的注册中心和微服务网关,但是多个团队之间的接口协作必须控制到API接口的粒度,即多个团队之间的接口协作采用API网关进行。此时的API网关就不是由单一的开发团队管理,而是属于整个平台层的集成能力。公共基础JAR包依赖微服务架构拆分后,各个微服务仍然会使用或依赖一些公共基础组件。这些组件本身是独立的工程项目,可以独立编译构建。同时,每个微服务本身都依赖于以黑盒Jar包形式存在的基础组件包。这类似于在每个微服务中都有一个基本的内置SDK包。这个SDK包实现了一些基本的通用可复用方法,或者统一封装了一些技术能力。在这种场景下,如果微服务B对Common包提出了新的需求,经过Common包分析后,仍然需要实现Common包,那么会重新编译构建Common包,升级版本.在这个场景下,其实微服务A和C这两个模块的代码都没有修改,那么此时A和C是否需要重新编译构建呢?可以明显看出此时A和C不需要编译Build,而只需要编译构建微服务B,B在构建时会自动获取最新的CommonJar包。所以在这种场景下,Common包的多个版本在实际的部署架构中并存。我们为什么要这样做?简单来说,微服务拆分后,需要做的就是尽量减少编译、构建和部署,以满足业务需求的变化。部署。只有这样才能更好地控制变更范围,也更容易分析版本部署后出现的问题。比如上图中,如果Common包升级后,微服务A也重新部署构建,那么此时很难立即判断出问题出在哪里。当然还有其他场景:比如Common包的版本升级,虽然接口没变,但是某个common方法的实现逻辑变了。这时必须触发三个微服务的部署目录下的jar包进行升级。在这种情况下,有两种方法可以做到这一点。一种是重新构建三个微服务获取新版本的JAR包,另一种是将新的JAR包自动分发到三个微服务部署环境或容器中。目前第一种方式比较难做,往往需要重新编译构建微服务,或者重新部署。也正是因为这个原因,可以看出在使用JAR包或者SDK代理包的时候,最大的问题就是版本变化情况下的升级问题。为解耦而设计前面说了,如果使用HttpAPI接口,就是松耦合的。如果两个微服务模块之间有大量的API接口交互,那么还是紧耦合关系。谈到微服务,你会发现,一个微服务要想成功正常运行,有大量的底层技术组件或微服务依赖,还有大量同层的其他微服务模块API接口。如果任何一个依赖的微服务出现问题,或者数据库出现问题,微服务将无法正常运行。不管是缓存,消息中间件,还是事件驱动架构,都可以看出要解耦微服务,把微服务和数据库解耦。上面已经提到了核心的解耦思想,即:对于查询,使用缓存来解耦;对于import或者CUD接口,使用消息中间件来解耦。其实上面的思路和经常提到的CQRScommandqueryresponsibility的思路是类似的。CQRS最初是为了更好的配合读写分离的数据库的使用。但是真正的CQRS解耦的重点还是两个。一种是将命令作为事件推送到消息中间件进行处理,避免长时间的分布式事务。二是启用单独的R读库,可以是数据库也可以是缓存库,实现查询功能的独立解耦和性能提升。在实际实践中,最好通过消息中间件或缓存,将不同开发团队之间的交互接口完全解耦,减少相互依赖和影响。比如微服务A需要向微服务B推送数据,同时需要从微服务C查询数据,那么向B推送数据库的接口可以实现为消息接口,先向微服务C推送数据消息中间件;并且可以在获取数据后缓存数据的查询。变更影响分析在微服务架构的实践中,由于很多接口都是通过HttpAPI接口调用的,所以很多接口的修改实际上并不会导致编译构建时出错。因此,某个微服务接口的修改会导致其他微服务模块的功能异常。当出现问题时,我们会在事后修复它。对于服务链监控和链接跟踪是事后才想到的,重点是发现性能问题而不是帮助您分析服务之间的依赖关系。因此,需要提前梳理好微服务之间的接口交互和依赖关系,如上图所示。通过上图中的接口交互矩阵,可以清楚的看到某个接口发生变化时,哪些微服务模块和功能会受到影响。那么这些影响点在支持变更或者在提交测试的时候,这些受影响的微服务模块或者功能也需要进行测试。当然,如果我们在微服务架构的实现过程中形成了完整的基于接口的单元测试和自动化测试,也可以更好的提前解决和发现问题。当你关注微服务模块的粒度时,很容易忽略微服务模块之间的交互和协作其实需要控制在API接口的粒度上。这是我们在实现微服务架构时需要重点关注的一点。