微服务架构技术浅析

微服务与微服务架构

微服务的概述

微服务理论的提出者马丁。福勒(Martin Fowler) 在其博客中详细描述了什么是微服务。微服务强调的是服务的大小,它关注的是某一个点,是具体解决某一个问题 / 提供落地对应服务的一个服务应用;狭意的看,可以看作 Eclipse 里面的一个个微服务工程 / 或者 Module。

微服务架构的概述

微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小服务,每个服务运行在自己的独立进程中,服务间通信采用轻量级通信机制 (通常是基于 HTTP 的 RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立地部署到生产环境、类生产环境等。另外,应该尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储技术。

微服务架构的优缺点

  • 优点:

    • 易于开发和维护:一个微服务只会关注一个特定的业务功能,所以它业务清晰,代码量较少
    • 单个微服务启动较快:单个微服务代码量较少,所以启动会比较快
    • 业务之间松耦合,无论是在开发阶段或者部署阶段,不同的服务都是互相独立的
    • 局部修改容易部署:单体应用只要有修改,就得重新部署整个应用,微服务解决了这样的问题
    • 技术栈不受限:在微服务架构中,可以结合项目业务及团队的特点,合理地选择技术栈
    • 按需伸缩:可根据需求,实现细粒度的扩展
  • 缺点:

    • 运维要求高:更多的服务意味着更多的运维投入
    • 技术开发难度高:涉及到网络通信延迟、服务容错、数据一致性、系统集成测试、性能监控等
    • 分布式固有的复杂性:使用微服务架构的是分布式系统,对于一个分布式系统,系统容错,网络延迟,分布式事务等都会带来巨大的挑战
    • 接口调整成本高:微服务之间通过接口进行通信。如果修改某一个微服务的 API,可能所有使用了该接口的微服务都需要做调整
    • 重复劳动:很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复

微服务项目的模块拆分示例

  • edu-common-parent(Maven 父配置)
  • edu-common(公共模块)
  • edu-common-config(公共 Config 模块)
  • edu-common-core(公共 Core 模块)
  • edu-common-web(公共 Web 模块)
  • edu-facade-user(用户服务接口)
  • edu-service-user(用户服务提供者)
  • edu-web-boss(用户服务消费者)

传统项目与微服务项目的区别

  • 传统的 Maven 单模块项目,最终会打包成单个 Java 或 Web 应用
  • 传统的 Maven 多模块项目,各模块之间直接通过 Maven 依赖来实现 Java 代码的互相调用与代码重用,最终会打包成单个或多个 Java 或 Web 应用,若多个应用之间需要互相通信,则采用 TCP/HTTP 等协议,可扩展为集群架构。
  • 微服务的 Maven 多模块项目,部分模块之间通过 Maven 依赖来实现 Java 代码的互相调用与代码重用,一般会引入注册中心来实现服务的自动注册与发现,最终会打包成多个 Java 或 Web 应用,多个应用之间通过 RPC/RESTful API 进行调用。

微服务解决方案选型

服务治理框架对比

micro-service-tech

基于 Dubbo 的微服务解决方案

Dubbo 未来的定位并不是要成为一个微服务的全面解决方案,而是专注于 RPC 领域,成为微服务生态体系中的一个重要组件。至于微服务化衍生出的服务治理需求,Dubbo 正在积极适配开源解决方案,并且已经启动独立的开源项目予以支持。因此基于 Dubbo 的微服务解决方案是:Dubbo + Nacos + Sentinel + 其他。

基于 Spring Cloud 的微服务解决方案

SpringCloud 的技术选择性是中立的,因此可以随需更换搭配使用,基于 SpringCloud 的微服务落地解决方案大致可以分为以下三种:

micro-service-spring-cloud

服务注册与发现

注册中心对比

register-center-vs

ZooKeeper 在分布式 / 微服务系统中的角色

ZooKeeper 是一种分布式 / 微服务协调服务,用于管理大型主机,包括分布式锁、服务注册与发现。其中 Zookeeper 是为读多写少的场景所设计,并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点的数据最大不能超过 1MB。

统一配置中心

Spring Cloud Config 配置中心架构

Spring Cloud Config 基于消息总线的架构图如下,该架构需要依赖外部的 MQ 组件,如 Rabbit、Kafka 实现远程环境事件变更通知,客户端实时配置变更可以基于 Git Hook 功能实现。当依赖的消息组件出现问题时,此时如果 Git 仓库 配置发生了变更,会导致部分或所有客户端可能无法获取到最新配置,这样就造成了客户端应用配置数据无法达到最终一致性,进而引起线上问题。架构图中的 Self scheduleing refresher 就是为了解决该问题,它是一个定时任务,执行时会判断本地的 Git 仓库版本与远程 Git 仓库版本是否一致,若不一致则会从配置中心获取最新配置进行加载,保障了配置最终一致性。

spring-cloud-config-system

路由网关

路由网关的性能对比

  • Spring Cloud Gateway ~ Zuul 2 << OpenResty ~< Kong << Direct(直连)
  • Spring Cloud Gateway、Zuul 2 的性能差不多,大概是直连的 35%-40%
  • OpenResty、Kong 差不多,大概是直连的 60%-70%

值得一提的是,在大并发场景下,例如模拟 200 并发用户、1000 并发用户时,Zuul 2 会有很大概率返回出错,这也说明 Zuul 2 目前还不成熟。Kong 的性能非常不错,非常适合做流量网关,并且对于 service、route、upstream、consumer、plugins 的抽象,也是自研网关值得借鉴的。但对于复杂系统,不建议业务网关用 Kong,或者更明确的说是不建议在 Java 技术栈的系统深度定制 Kong 或 OpenResty,主要是工程性方面的考虑。举个例子:假如有很多个不同业务线,鉴权方式五花八门,都是与业务多少有点相关的;这时如果把鉴权在网关实现,就需要维护大量的 Lua 脚本,引入一个新的复杂技术栈是一个成本不低的事情。Spring Cloud Gateway、Zuul2 对于 Java 技术栈来说比较方便,可以依赖业务系统的一些 Common 组件;而使用 Lua 开发不方便,不光是语言的问题,更是复用基础设施的问题。另外,对于网关系统来说,性能不是差一个数量级,问题不是很大,多加机器就可以搞定。

Spring Cloud Gateway 与 Zuul 1.x 对比

  • 底层实现:Zuul 1.x 基于 Servlet 2.5 构建,使用的是阻塞的 I/O 模型。Gateway 是基于 Spring 5.x、Spring Boot 2.x、Spring WebFlux 和 Project Reactor 等技术,底层使用 Netty 的非阻塞 I/O 模型。
  • 长连接:Gateway 支持长连接,而 Zuul 1.x 不支持长连接(如 WebSocket),不适用后端服务响应慢或者高并发场景下,因为线程数量是固定(有限)的,线程容易被耗尽,导致新请求被拒绝处理。
  • 限流:Zuul 1.x 需要通过 Filter 实现限流扩展,Gateway 内置了限流过滤器。
  • 性能:根据官方提供的基准测试,Spring Cloud Gateway 的 RPS(每秒请求数)是 Zuul 1.x 的 1.6 倍,平均延迟是 Zuul 1.x 的一半。
  • 技术栈沉淀:Zuul 1.x 开源近七年,经受考验,稳定成熟,Gateway 未见实际落地案例,Github 统计如下:
仓库数量 issues 数量说明
Zuul1.x 1007 repositoriesZuul1.x 88 Open / 2736 Closed 统计时间截止 2019/8/26
Gateway 102 repositoriesGateway 135 Open / 850 Closed 统计时间截止 2019/8/26

杂项

分布式锁的实现方案

  • Redisson:Redis 官方的分布式锁实现
  • Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列
  • Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法
  • Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令,此命令同样是原子性操作,只有在 key 不存在的情况下才能 set 成功
  • Memcached:利用 Memcached 的 add 命令,此命令是原子性操作,只有在 key 不存在的情况下才能 add 成功,也就意味着线程得到了锁

Nginx 动态更新 upstream 的方案

在不使用服务发现中间件(如 Zookeeper、Eureka、Consul)的场景下,使用传统的基于代理的负载均衡解决方案(如 Nginx),此时可以考虑使用以下方案动态更新服务提供者列表。

  • 手工或者通过脚本方式,在部署的时候去更新 Nginx 的配置文件,然后 reload
  • 使用 ngx_http_dyups_module 模块,通过 REST API 来在运行时直接更新 upstream 而不需要 reload
  • consul-template + Nginx 的方案一,通过 consul 监听服务实例的变化,然后更新 Nginx 的配置文件,通过 reload 实现服务列表的更新
  • consul-template + Nginx 的方案二,Nginx 在运行时通过 consul 获取服务列表来实现动态 upstream 的路由
  • OpenResty + Lua 的方案,通过 Lua 脚本实现动态 upstream 的路由