微服务架构是由很多小而自治的微服务组合而成,每个服务都提供一套独立自洽的业务能力。从应用的层面,我们首先对业务进行建模,业务域定义的边界也自然而然作为每个微服务的职能边界。
什么是微服务?
微服务的定义比较直白,但只有理解它的设计理念和原则,才能真正用好它。从单个服务/服务间关系来看,有以下特征:
- 微服务通常很小、提供很少但独立的功能,服务间耦合度很低。一个很小的研发团队就可以开发和维护一个微服务。
- 每个服务都是一个单独的代码库,可以由一个很小的研发团队来管理。
- 服务可以独立部署,而不需要重新构建和部署整个应用。
- 每个服务负责维护自己的数据和状态,数据所有权归本服务所有,其他服务必须通过该服务访问数据和状态。
- 服务之间通过定义好的API进行通信,对外隐藏API的内部实现细节。
- 支持多语言编程,不同服务可以使用独立的技术栈进行开发,自由选择编程语言、框架和库。
如果从更全局的视角来看,微服务架构还必须下面几个组件:
- 服务管理和编排。该组件负责把服务部署在物理机/虚拟机节点上、做失败检查、在多个节点上负责对服务的多个实例做负载均衡等。我们通常可以使用现成的开源技术来充当这个组件,比如k8s。
- API Gateway。API Gateway给所有Client提供了访问入口。Client通常不会直接访问提供业务支持的服务,而是调用API Gateway,由Gateway负责把请求转发给后端对应的服务。
使用 API Gateway 有很多优势:
- 解耦Client和服务,我们可以单独对服务进行更新或重构,client端可能不需要更新甚至不需要感知。
- 服务间通信可以使用更灵活的方式,比如不走HTTP协议,而是走rpc等二进制协议、消息队列等,某些超大数据量的场景甚至会走p2p协议。
- API Gateway可以承担一些跨业务的职能,比如权限认证、日志、SSL校验、负载均衡等。
- API Gateway一般会提供一些开箱即用的能力,比如限流、缓存、请求篡改、权限校验等。
架构优势
- 支持敏捷迭代。由于微服务可以独立部署,所以修复bug和上线新特性也更容易。你可以独立更新特定的服务,而不是重新部署整个应用,如果更新出现问题,也支持快速回滚。在传统应用中,一旦在应用的某个环节发现问题,整个发布流程都要被阻塞。新特性也可以等着bug修复后再去测试和发布。
- 小而专的研发团队。微服务可以很小,小到一个独立的小团队iu可以构建、测试和阿布。小团队非常适合推行敏捷迭代流程。往往大型团队的生产力更弱一些,因为沟通满,管理成本上升,由此也不适合再推行敏捷迭代。
- 代码库小。在大型单体应用中,随着时间推移,代码依赖越来越复杂。增加新特性意味着要改很多地方的代码。由于不共享代码和数据,微服务架构将依赖最小化,部署新特性也更容易。
- 可以融合使用多种技术。团队成员可以选择最适合服务的技术,并在适当的情况下混合使用多个技术栈。
- 故障隔离。如果一个特定的微服务挂掉了,只要上游微服务在设计时考虑到容灾处理,通常不会影响整个应用的正常功能。比如,采用适当的熔断机制,或者服务间通信走异步消息队列。
- 架构扩展性好。每个服务都可以单独进行扩容,这样的话,我们就能独立扩容需要更多资源的子系统,而不是整个应用。使用K8s等对服务进行编排,我们可以把多个服务的实例部署在同一台机器上,以提高资源利用率。
- 原生支持服务间的数据隔离。如果要更新数据的schema,只有单个服务受到影响。在单体应用中,更新schema往往不太容易,因为应用的多个模块都会通过库依赖的形式访问数据,在更新时,很难追踪到所有使用的地方和使用方式,导致schema的迭代有很多未知的风险。
有哪些挑战
微服务的架构优势是有代价的,在采用微服务架构之前,我们要认识到未来可能面临的挑战:
- 架构复杂度高。相对于单体应用,微服务应用有很多可变的组件。每个服务都更简单了,但整个系统变得更复杂了。
- 开发和测试麻烦,尤其是集成测试。在实现一个微服务时,如果它依赖了其他服务,编写和测试过程都会比单体应用更复杂。现存的工具在设计时可能并未考虑到服务依赖。如果我们要对相互依赖的多个服务进行重构,难度也会比重构单体应用高。在测试时,涉及到服务依赖,需要单独维护多套与线上相似的测试环境。考虑到每个服务都有多个测试版本,迭代比较快时,很容易出问题。
- 互相独立的服务,很难做治理。由于微服务以区中心化的方式去构建各个服务,在服务治理上增加了额外的难度。最终整个应用中可能包含多个编程语言,使用了多种框架,开发人员可以更自由地选择小众的技术,导致后期维护成本反而更高。
- 容易产生网络拥塞和延迟。很多小且功能独立的服务需要通过API或消息队列继续频繁的通信。由于底层走网卡进行通信,对比单体架构的代码库依赖,网络通信的代价可能很高。当服务的依赖链很长时,比如 A调用B,B调用C,C调用...,额外的网络延迟会高到无法接受。所以在设计服务时,需要慎重考虑API的设计,应避免过度繁琐的API,考虑到编码格式(二进制协议优于文本协议),优先使用异步通信模式或消息队列模式;
- 跨服务的数据一致性问题。每个微服务负责自身的数据持久化,导致多个服务的数据可能存在不一致。通常我们使用最终一致性原则解决这些问题;
- 服务管理难度高。微服务架构的成功采用离不开成熟的DevOps文化。处理跨服务的日志追踪,尤其是一个用户操作触发整个链路的微服务调用,日志都要记录下来,并且在发生错误时,能够把整个调用链的日志拉取出来;
- 服务的发布版本管理。在更新服务时,不能对API做破坏性的更新,否则依赖它的服务可能会挂掉。由于多个开发团队在同一时间段可能都要对服务做更新,所以接口设计必须保持向前或向后兼容。一个典型的低级错误是:删除了API定义的某些字段,或修改了某些字段的业务含义;
- 要求研发团队懂分布式系统。微服务通常是分布式的,需要认真评估团队是否具备对应的能力和经验;
最佳实践
- 基于业务场景对服务进行建模;
- 尽量去中心化。每个团队都要负责设计和构建服务,避免分享代码或底层数据结构。
- 数据存储只允许单个服务访问,每个服务根据自己的业务需求设计选择存储方案和数据格式。
- 服务间通过设计良好的API进行通信,应尽量避免透露实现细节。API的定义应当基于业务场景进行设计,而不是根据内部的技术实现进行设计。
- 避免服务间的耦合。服务间耦合的原因通常是把数据库schema透传给外部,或通信协议弹性比较差。
- 将跨业务域的需求交给API Gateway来处理,比如身份认证、SSL验证。
- 保持API Gateway的纯粹性,不掺入业务逻辑。API Gateway只处理和转发client侧的请求,不感知任何业务逻辑。否则API Gateway将成为一个依赖,导致服务间的耦合。
- 服务间应该高内聚、低耦合。应该一起变化的逻辑应该被打包到一个服务里,统一部署。如果他们分散在不同的服务里,这些服务将高度耦合在一起,一个服务的更新也将连锁导致另一个服务的更新。两个服务间繁琐的通信是“高耦合低内聚”的体现。
- 故障隔离。应当采取一定的恢复策略,以避免一个服务的故障引发连锁反应。