第05课:分布式微服务架构体系详解——服务发现和服务通信

前言

微服务架构的概念比容器技术早,但是却随着容器技术在 2013 年的兴起,基于容器技术的微服务架构越来越被广泛应用。容器的轻量级部署方式很适合为每个微服务提供基础运行环境。

本文会基于 Docker 容器,先介绍下服务发现的问题和可用的方案,然后对同步通信以及异步通信的技术做一些对比介绍,最后会结合 DDD(Domain-Driven Design)的思想来分析合理的通信模型。

Docker 容器的基础知识本文会略过,对 Docker 感兴趣的读者可以去看看 Docker 源码 ,Docker 是用 Go 语言写的,Go 语言很适合写网络以及并发编程的代码,Go 语言也很易学,基本跟着 go-tour 看源码是没有太大障碍的。下面先看一下可用的服务发现的方案。

重要: 因篇幅有限,微服务结合 SpringBoot、SpringCloud 的实践部分的代码大家可以直接去我的 GitHub 上 Checkout。为了方便初学者了解,简单写了两个示例工程。

Microservice 代码:microservice_coffeeshop;
Gateway 代码:gateway_coffeeshop。
这两个工程 Demo 中分别留了一些作业,在 README.md 中,希望大家可以在 GitHub 或者 读者圈 跟我一起互动和探讨。这4个问题的相关代码,会在文章全部发布之后 push 上去。

服务发现

服务发现其实涉及到一系列的复杂问题,在分布式环境下,尤其是微服务的情况。假设你有 10 个微服务以及 3 台机器,以最简单的模型就是每台机器分别运行着 10 个不同的微服务镜像,那么在服务间需要调用的时候,调用方的微服务实例必须知道其他 27(30-3)个服务实例的请求地址。一种简单粗暴的方式就是把所有的服务的 IP 端口都写在某个配置文件中。然而不幸的是,Docker 容器会在每次运行时随机分配一个 5 位数的端口号。再粗暴点就是将端口配置写死,然而更不幸的是,当集群规模扩大到 4 台机器,或者增加其他微服务实例的时候,头会很大,并且如果其中某个实例挂掉了,可能请求会发送到失败的节点。服务发现是分布式系统的一系列问题之一,作为应用的运维基础,还需要处理:服务注册、负载均衡、健康检查、监控平台、服务上线下线、蓝绿部署、部署回滚等一系列问题。解决了这些之后才能继续考虑用什么 RPC 框架,怎样调用服务间的接口。

本文以一个最容易实施的服务发现方案 Eureka 为例来介绍下服务发现的基础方案。基于 Docker 的服务发现,还可以用 Etcd、ZooKeeper 及 Consul。

Eureka

Eureka 提供的是一种客户端注册的方式进行服务注册和发现。Eureka 是 Netflix 公司的开源项目之一,和 Spring-cloud-netflix 项目集成可以快速实现 SpringBoot 项目的云化集成部署。部署方式可以选择普通虚拟机或者用 Docker。Netflix 除了提供 Eureka,还有成套的负载均衡 Ribbon、网关路由 Zuul、熔断机制 Hystrix,下面会逐一介绍。

先来看一下使用 EurekaServer 进行服务发现的示意图:

1-32

示例中有三个微服务 A、B、C 都部署在 Docker 容器中,并且都提供了 RestAPI 进行服务通信。图中的 EurekaServer 也是部署在 Docker 容器中的进程,可以按照 Dockerhub 指导直接部署到集群中。服务发现、注册的过程如下:

(1)服务注册

微服务实例(客户端实例)启动后,将服务信息发送给 EurekaServer,EurekaServer 会将这个注册信息(服务名、IP、Port、服务在线(UP)、离线(DOWN)等信息记录在自己的服务注册表中)。

注册后会间隔一段时间(由客户端配置)发送心跳信息,如果一段时间收不到心跳则会将该节点从服务列表中去除。如果客户端实例停止服务,会主动发消息给 EurekaServer 将其下线。

(2)客户端获取 EurekaServer 中的服务注册信息。

(3)EurekaServer 将服务注册表发给客户端实例。

(4)微服务 A 想要请求微服务 B 的某个 RestAPI,此时 Ribbon 会将列表中所有可用的微服务 B 的实例以 round robin 算法进行负载均衡,决定请求到哪个具体的微服务的容器实例,也即将 /microserviceB/xx/{xxid} 请求地址转化为: http://123.12.12.123:12345/xx{xxid}。

Zuul 是一种智能路由技术,可以做动态路由,将请求转发给在 Eureka 注册的服务实例,也包含了负载均衡。Zuul 类似于一个网关,还可以用作代理所有外部请求到内部服务的转发,同时 Zuul 也具有一些网关的特性,如可以进行身份认证、安全、动态路由等。而 Ribbon 是用作在 Eureka 注册中心中的服务间的负载均衡。

服务发现的其他方案

服务发现的实现,其实是一个很复杂的过程,尤其是对于共识算法的实现,关于共识问题会在后面的章节中专门介绍。对于实施微服务的企业来说,一般选择了一套服务发现技术后,很难再切换成另外一套方案,集群规模越大,整体的迁移成本会越高。所以在初期进行技术选型时,最好对所有可行的技术方案做好调研。以下简单整理了 Eureka、Consul、Etcd 以及 ZooKeeper(以下简称 ZK)的区别:

  • 在 CAP 理论中,Eureka 满足了 AP,Consul 是 CA,ZK、Etcd 是 CP。 在分布式场景下 Eureka 和 Consul 都能保证可用性。而搭建 Eureka 服务会相对更快速,因为不需要搭建额外的高可用服务注册中心,在小规模服务器实例时,使用 Eureka 可以节省一定成本。从共识问题来看,Eureka 舍弃了一致性、保证可用性。Consul 和 Etcd 都是基于 Raft 算法,ZK 是基于 Paxos 的一部分思想,实现了自己的 Zab(Zookeeper atomic broadcast)算法。
  • Eureka、Consul 都提供了可以查看服务注册数据的 WebUI 组件,Consul 还提供了 KV 存储,支持 Http 和 DNS 接口,对于创业公司最开始搭建微服务,比较推荐这两者。
  • 在多数据中心方面,Consul 自带数据中心的 WAN 方案。ZK 和 Etcd 均不提供多数据中心功能的支持,需要额外的开发。
  • 跨语言性上,ZooKeeper 需要使用其提供的客户端 API,跨语言支持较弱。Etcd、Eureka 都支持 Http,Etcd 还支持 Grpc。Consul 除了 Http 之外还提供了 DNS 的支持。
  • 安全方面,Consul、ZooKeeper 支持 ACL,另外 Consul、Etcd 支持安全通道 Https。
  • SpringCloud 目前对于 Eureka、Consul、Etcd、ZK 都有相应的支持。
  • Consul 和 Docker 一样,都是用 Go 语言实现,基于 Go 语言的微服务应用可以优先考虑用 Consul。Consul 在今年 6 月发布的 V 1.2 开始支持了 Service Mesh(服务网格),服务网格可以说是微服务的一个新趋势,对于已经有成熟容器集群的应用来说在做集群升级时可以考虑调研下 Consul 的新版本。
    对于规模不大、刚开始起步的初创型企业来说,其实 Eureka 以及 Consul 都是不错的选择,这两者都能做很好的扩展,Netflix 公司使用 Eureka 管理着上万台运行微服务的实例。它们跟 Docker 都可以很好的融合,基本不需要太多的额外开发工作,并且提供了可视化的 UI 管理界面来随时查看服务上下线状态。

服务通信

同步通信

Dubbo 是由阿里巴巴开源的 Java 客户端的 RPC 框架。Dubbo 基于 TCP 协议的长连接进行数据传输,传输格式是使用 Hessian 二进制序列化。服务注册中心可以通过 ZooKeeper 实现。

ApacheThrift 是由 Facebook 开发的 RPC 框架,其代码生成引擎可以在多种语言中,如 C++、 Java、Python、PHP、Ruby、Erlang、Perl 等创建高效的服务。传输数据采用二进制格式,其数据包要比使用 JSON 或者 XML 格式的 HTTP 协议小。高并发、大数据场景下更有优势。

Rest 基于 Http 协议进行通信,微服务架构也提倡使用轻量级的跨语言特性好的 Restful 风格的 API。随着 SpringBoot 社区的日渐成熟,基于 Rest API 的服务间通信已经成为 Java 实现微服务的主流。不过只是在用 SpringBoot 的 @RestController 并不完全意味着你的 Rest API 是真正的 Restful 风格的 API。如果团队要写出 Restful 风格的 API,应该读读 Restful 风格 API 成熟度的四层模型 资料。

在 SpringBoot 项目中,可以使用其自带的 RestTemplate 来实现基础的 Http 请求封装,示例代码如下:

ResponseEntity<RestResult> entity = restTemplate.exchange("http://service-user/users/{userId}/base", GET, httpEntity,RestResult.class);  

如果使用前文提到的 Netflix-Eureka 的话,可以使用 Netflix-Feign。Feign 是一个声明式的 Web Service 客户端,客户端的负载均衡使用前文介绍的 Netflix-Ribbon。对于服务调用的示例代码如下:

// 接口声明微服务名-资源路径   
@FeignClient("microservice-user/users")
public interface UserClient {

    @RequestMapping(value = "/{userId}/base", method = {RequestMethod.GET})
    ResponseEntity<ResponseResult> userBaseInfo(@PathVariable("userId") Long userId);

}

异步通信

在微服务架构中,排除纯粹的“事件驱动架构”,使用消息队列的场景一般是为了进行微服务之间的解耦。服务之间不需要了解是由哪个服务实例来消费或者发布消息,只要处理好自己领域范围的逻辑,然后通过消息通道来发布,或者订阅自己关注的消息就可以。目前开源的消息队列技术也很多,比如 Apache Kafka、RabbitMQ、Apache ActiveMQ 以及阿里巴巴的 RocketMQ 目前已经成为 Apache 项目之一。消息队列的模型中,主要的三个组成就是:

  • Producer:生产消息,将消息写入 channel。
  • Message Broker:消息代理,将写入 channel 的消息按队列的结构进行管理,负责存储/转发消息。Broker 一般是需要单独搭建、配置的集群,而且必须是高可用的。
  • Consumer:消息的消费者。目前大多数的消息队列都是保证消息至少被消费一次,所以根据使用的消息队列设施不同,消费者要做好幂等。
  • 不同的消息队列的实现,消息模型不同,各个框架的特性也不同:

Kafka

Kafka 是一个高性能的基于发布/订阅的跨语言分布式消息系统,其开发语言为 Scala,比较重要的特性如下:

  • 以时间复杂度为 O(1) 的方式快速消息持久化;
  • 高吞吐率;
  • 支持服务间的消息分区及分布式消费,同时保证消息顺序传输;
  • 支持在线水平扩展,自带负载均衡;
  • 支持只消费且仅消费一次(Exactly Once)模式等。
    说个缺点:管理界面比较鸡肋了一点儿,可以使用开源的 kafka-manager。

其高吞吐的特性,除了可以作为微服务之间的消息队列,也可以用于日志收集、离线分析、实时分析等。

Kafka 官方提供了 Java 版本的客户端 API,Kafka 社区目前也支持多种语言,包括 PHP、Python、Go、C/C++、Ruby、NodeJS 等。对于 Kafka 的一些实现细节,可以看一下这篇论文 Kafka:a Distributed Messaging System for Log Processing。

RocketMQ/ONS

RocketMQ 是由阿里巴巴研发开源的高可用分布式消息队列,RocketMQ 支持 pull/push 两种消息推送模式,支持 At least Once 消息投递,目前的版本还不支持 Exactly Only Once,需要 Consumer 做好幂等,但是保证了很好的性能。ONS 提供商业版的高可用集群,ONS 也支持 pull/push。可支持主动推送,可支持百亿级别消息堆积。ONS 还支持全局的顺序消息,以及有友好的管理页面,可以很好的监控消息队列的消费情况,并且支持手动触发消息多次重发。

如果企业为了快速上线,并且消息量级不大的情况,可以考虑用商用版本的,集群相对稳定,并且有很好的技术支持,综合成本会比自建集群低。如果是要做大数据量级的消息消费,考虑自建集群会相对节省成本,比如像用 Kafka 做日志采集,然后用其他服务做日志数据分析。目前在 Kafka 社区都有很多部署方案,当然也可以用 Docker 来部署 Kafka 的 Broker 集群。因为 Kafka 依赖 ZooKeeper 实现服务发现,所以还需要有 ZooKeeper 镜像部署的 Docker 集群。如果已有 ZooKeeper 集群,则可以省去一些机器成本。上述还提到了可视化的 Kafka 监控 kafka-manager,也可以用 Docker 部署。

Domain-Driven Design 思想 in 微服务

DDD 的概念提出比微服务更早,是 2004 年 Eric Evans 在一篇讲述复杂软件设计的论文中提出的。当时 DDD 的思想是很先进的,很多好的架构以及技术产品还未成型,而现在拥有 Docker 技术做支撑,有微服务架构以及周围的开源软件的优秀实践,业务型系统应该拥抱 DDD,不再让程序设计只是表结构和 CRUD。

因为 DDD 是一个体系化的概念,前文讲述了如何实现服务间的通信。这里简单讲述下,微服务怎样选择合适的通信模型。

微服务间的通信

使用微服务架构之后,资源以及服务被拆分成更小的独立的模块,相互之间尽量不产生依赖,原则是能不调用就不调用。如果确实需要同步调用,按照 DDD 的思想,需要设计防腐层,可以用适配器层来处理。

在大多数的情况下,提供给上层用户请求的,一般都是需要调动很多微服务的,如果微服务之间相互调用显然不行。假设实现一个“用户个人中心页面”的 Get 请求,需要有用户基础信息(service-user)、用户交易信息(service-trade)、交易订单中的商品信息(service-item),这个 Get 的请求接口放到哪个微服务都不太合适,于是就有了针对微服务整合、分配资源的 API Gateway 层方案。API Gateway 一般可以使用 NIO 框架,基于响应式编程的风格,不仅提升性能,还可以提高开发效率。所以建议 API Gateway 使用类似 RxJava 的响应式开发框架。关于 API Gateway 的详细说明可以参考之前写的这篇《微服务中 API Gateway 的使用》。通过文中的一些示意图和示例应该可以很好地理解如何实现一个基于微服务的 API Gateway。

对于微服务之间,如果有不得不进行进程通信的场景,建议在结合领域模型和领域服务思考时,可以先用分布式消息队列进行解耦。尽管这个微服务不是纯事件驱动 型领域服务,但也可以用事件对象来解耦,分布式消息队列的选型可以参考前文的 异步通信 介绍。

小结

本篇内容介绍了基于微服务架构的,服务发现的几种方案:Eureka、Consul、Etcd 及 ZooKeeper 的对比,并且以 Eureka 为例讲述了客户端服务发现的流程模型。服务发现还有如 Consul、ZooKeeper 的第三方发现模式。需要了解的可以看一下这篇 《微服务的服务发现》 。文章最后简单总结了微服务间通信的一些注意点,使用微服务架构,需要使用 API Gateway 来解耦、提升性能,让每个微服务只关注自己的领域模型和职责。尽量考虑减少微服务之间同步调用,服务间的解耦可以引入一个分布式消息队列的实现,文中也介绍了异步通信的方案,无论是哪个 MQ,总有一款适合你。

资料
本文代码示例:

microservice_coffeeshop
gateway_coffeeshop
其他资料:

Kafka: a Distributed Messaging System for Log Processing
building-microservices-with-spring-cloud-and-netflix-oss
service-discovery-6-questions-to-4-experts
基于 Docker 的微服务架构实践

(全文完)