文章 答疑

基于 Docker 的微服务架构实践

李静瑶,2011 年从中南大学毕业后入职阿里巴巴集团。现就职于赤金信息科技有限公司,担任 CTO/合伙人 ,从零搭建基于 Docker 容器技术的微服务分布式企业集群,深度的 DDD 思想践行者。CSDN 博客专家。
文章

基于 Docker 的微服务架构-分布式企业级实践

前言

基于 Docker 的容器技术是在2015年的时候开始接触的,两年多的时间,作为一名 Docker 的 DevOps,也见证了 Docker 的技术体系的快速发展。本文主要是结合在公司搭建的微服务架构的实践过程,做一个简单的总结。希望给在创业初期探索如何布局服务架构体系的 DevOps,或者想初步了解企业级架构的同学们一些参考。

Microservice 和 Docker

对于创业公司的技术布局,很多声音基本上是,创业公司就是要快速上线快速试错。用单应用或者前后台应用分离的方式快速集成,快速开发,快速发布。但其实这种结果造成的隐性成本会更高。当业务发展起来,开发人员多了之后,就会面临庞大系统的部署效率,开发协同效率问题。然后通过服务的拆分,数据的读写分离、分库分表等方式重新架构,而且这种方式如果要做的彻底,需要花费大量人力物力。

个人建议,DevOps 结合自己对于业务目前以及长期的发展判断,能够在项目初期使用微服务架构,多为后人谋福。

随着 Docker 周围开源社区的发展,让微服务架构的概念能有更好的一个落地实施的方案。并且在每一个微服务应用内部,都可以使用 DDD(Domain-Drive Design)的六边形架构来进行服务内的设计。关于 DDD 的一些概念也可以参考之前写的几篇文章:领域驱动设计整理——概念&架构领域驱动设计整理——实体和值对象设计领域服务、领域事件

清晰的微服务的领域划分,服务内部有架构层次的优雅的实现,服务间通过 RPC 或者事件驱动完成必要的 IPC,使用 API gateway 进行所有微服务的请求转发,非阻塞的请求结果合并。本文下面会具体介绍,如何在分布式环境下,也可以快速搭建起来具有以上几点特征的,微服务架构 with Docker。

服务发现模式

如果使用 Docker 技术来架构微服务体系,服务发现就是一个必然的课题。目前主流的服务发现模式有两种:客户端发现模式,以及服务端发现模式。

客户端发现模式

客户端发现模式的架构图如下:

微服务客户端服务发现-Eureka

客户端发现模式的典型实现是Netflix体系技术。客户端从一个服务注册服务中心查询所有可用服务实例。客户端使用负载均衡算法从多个可用的服务实例中选择出一个,然后发出请求。比较典型的一个开源实现就是 Netflix 的 Eureka。

Netflix-Eureka

Eureka 的客户端是采用自注册的模式,客户端需要负责处理服务实例的注册和注销,发送心跳。

在使用 SpringBoot 集成一个微服务时,结合 SpringCloud 项目可以很方便得实现自动注册。在服务启动类上添加@EnableEurekaClient即可在服务实例启动时,向配置好的 Eureka 服务端注册服务,并且定时发送以心跳。客户端的负载均衡由 Netflix Ribbon 实现。服务网关使用 Netflix Zuul,熔断器使用 Netflix Hystrix

除了服务发现的配套框架,SpringCloud 的 Netflix-Feign,提供了声明式的接口来处理服务的 Rest 请求。当然,除了使用 FeignClient,也可以使用 Spring RestTemplate。项目中如果使用@FeignClient可以使代码的可阅读性更好,Rest API 也一目了然。

服务实例的注册管理、查询,都是通过应用内调用 Eureka 提供的 REST API 接口(当然使用 SpringCloud-Eureka 不需要编写这部分代码)。由于服务注册、注销是通过客户端自身发出请求的,所以这种模式的一个主要问题是对于不同的编程语言会注册不同服务,需要为每种开发语言单独开发服务发现逻辑。另外,使用 Eureka 时需要显式配置健康检查支持。

服务端发现模式

服务端发现模式的架构图如下:

微服务-服务端服务发现-Consul,etcd,zk

客户端向负载均衡器发出请求,负载均衡器向服务注册表发出请求,将请求转发到注册表中可用的服务实例。服务实例也是在注册表中注册,注销的。负载均衡可以使用可以使用 Haproxy 或者 Nginx。服务端发现模式目前基于 Docker 的主流方案主要是 Consul、Etcd 以及 Zookeeper。

Consul

Consul 提供了一个 API 允许客户端注册和发现服务。其一致性上基于RAFT算法。通过 WAN 的 Gossip 协议,管理成员和广播消息,以完成跨数据中心的同步,且支持 ACL 访问控制。Consul 还提供了健康检查机制,支持 kv 存储服务(Eureka 不支持)。Consul 的一些更详细的介绍可以参考之前写的一篇:Docker 容器部署 Consul 集群

Etcd

Etcd 都是强一致的(满足 CAP 的 CP),高可用的。Etcd 也是基于 RAFT 算法实现强一致性的 KV 数据同步。Kubernetes 中使用 Etcd 的 KV 结构存储所有对象的生命周期。

关于 Etcd 的一些内部原理可以看下etcd v3原理分析

Zookeeper

ZK 最早应用于 Hadoop,其体系已经非常成熟,常被用于大公司。如果已经有自己的 ZK 集群,那么可以考虑用 ZK 来做自己的服务注册中心。
Zookeeper 同 Etcd 一样,强一致性,高可用性。一致性算法是基于 Paxos 的。对于微服务架构的初始阶段,没有必要用比较繁重的 ZK 来做服务发现。

服务注册

服务注册表是服务发现中的一个重要组件。除了 Kubernetes、Marathon 其服务发现是内置的模块之外。服务都是需要注册到注册表上。上文介绍的 Eureka、consul、etcd 以及 ZK 都是服务注册表的例子。

微服务如何注册到注册表也是有两种比较典型的注册方式:自注册模式,第三方注册模式。

自注册模式 Self-registration pattern

上文中的 Netflix-Eureka 客户端就是一个典型的自注册模式的例子。也即每个微服务的实例本身,需要负责注册以及注销服务。Eureka 还提供了心跳机制,来保证注册信息的准确,具体的心跳的发送间隔时间可以在微服务的 SpringBoot 中进行配置。

如下,就是使用 Eureka 做注册表时,在微服务(SpringBoot 应用)启动时会有一条服务注册的信息:

com.netflix.discovery.DiscoveryClient    : DiscoveryClient_SERVICE-USER/{your_ip}:service-user:{port}:cc9f93c54a0820c7a845422f9ecc73fb: registering service...

同样,在应用停用时,服务实例需要主动注销本实例信息:

2018-01-04 20:41:37.290  INFO 49244 --- [       Thread-8] c.n.e.EurekaDiscoveryClientConfiguration : Unregistering application service-user with eureka with status DOWN
2018-01-04 20:41:37.340  INFO 49244 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : Shutting down DiscoveryClient ...
2018-01-04 20:41:37.381  INFO 49244 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : Unregistering ...
2018-01-04 20:41:37.559  INFO 49244 --- [       Thread-8] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_SERVICE-USER/{your_ip}:service-user:{port}:cc9f93c54a0820c7a845422f9ecc73fb - deregister  status: 200

自注册方式是比较简单的服务注册方式,不需要额外的设施或代理,由微服务实例本身来管理服务注册。但是缺点也很明显,比如 Eureka 目前只提供了 Java 客户端,所以不方便多语言的微服务扩展。因为需要微服务自己去管理服务注册逻辑,所以微服务实现也耦合了服务注册和心跳机制。跨语言性比较差。

第三方注册模式 Third party registration pattern

第三方注册,也即服务注册的管理(注册、注销服务)通过一个专门的服务管理器(Registar)来负责。Registrator 就是一个开源的服务管理器的实现。Registrator 提供了对于 Etcd 以及 Consul 的注册表服务支持。 Registrator 作为一个代理服务,需要部署、运行在微服务所在的服务器或者虚拟机中。比较简单的安装方式就是通过 Docker,以容器的方式来运行。三方注册模式的架构图如下:

微服务-Registrator-服务端模式,服务发现

通过添加一个服务管理器,微服务实例不再直接向注册中心注册,注销。由服务管理器(Registar)通过订阅服务,跟踪心跳,来发现可用的服务实例,并向注册中心(consul、etcd 等)注册,注销实例,以及发送心跳。这样就可以实现服务发现组件和微服务架构的解耦。

Registrator 配合 Consul,以及 Consul Template 搭建服务发现中心,可以参考: Scalable Architecture DR CoN: Docker, Registrator, Consul, Consul Template and Nginx 。此文示例了 Nginx 来做负载均衡,在具体的实施过程中也可以用 Haproxy 或其他方案进行替代。

小结

除了以上几种做服务发现的技术,Kubernetes 自带了服务发现模块,负责处理服务实例的注册和注销。Kubernetes 也在每个集群节点上运行代理,来实现服务端发现路由器的功能。如果编排技术使用的 k8n,可以用 k8n 的一整套 Docker 微服务方案,对 k8n 感兴趣的可以阅读下Kubernetes 架构设计与核心原理

在实际的技术选型中,最主要还是要结合业务、系统的未来发展的特征进行合理判断。

  • 在 CAP 理论中。Eureka 满足了 AP,Consul 是 CA,ZK 和 Etcd 是 CP。 在分布式场景下 Eureka 和 Consul 都能保证可用性。而搭建 Eureka 服务会相对更快速,因为不需要搭建额外的高可用服务注册中心,在小规模服务器实例时,使用 Eureka 可以节省一定成本。
  • 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。

服务间的 IPC 机制

按照微服务的架构体系,解决了服务发现的问题之后。就需要选择合适的服务间通信的机制。如果是在 SpringBoot 应用中,使用基于 Http 协议的 REST API 是一种同步的解决方案。而且 Restful 风格的 API 可以使每个微服务应用更加趋于资源化,使用轻量级的协议也是微服务一直提倡的。

如果每个微服务是使用 DDD(Domain-Driven Design)思想的话,那么需要每个微服务尽量不使用同步的 RPC 机制。异步的基于消息的方式比如 AMQP 或者 STOMP,来松耦合微服务间的依赖会是很好的选择。目前基于消息的点对点的 pub/sub 的框架选择也比较多。下面具体介绍下两种 IPC 的一些方案。

同步

对于同步的请求/响应模式的通信方式。可以选择基于 Restful 风格的 Http 协议进行服务间通信,或者跨语言性很好的 Thrift 协议。如果是使用纯 Java 语言的微服务,也可以使用 Dubbo。如果是 SpringBoot 集成的微服务架构体系,建议选择跨语言性好、Spring 社区支持比较好的 RPC。

Dubbo

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

ApacheThrift

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

Rest

Rest 基于 HTTP 协议,HTTP 协议本身具有语义的丰富性。随着 Springboot 被广泛使用,越来越多的基于 Restful 风格的 API 流行起来。REST 是基于 HTTP 协议的,并且大多数开发者也是熟知 HTTP 的。

这里另外提一点,很多公司或者团队也是使用Springboot的,也在说自己是基于 Restful 风格的。但是事实其实往往是实施得并不到位。对于你的 Restful 是否是真的 Restful,可以参考这篇文章,对于 Restful 风格 API 的成熟度进行了四个层次的分析: Richardson Maturity Model steps toward the glory of REST

如果使用Springboot的话,无论使用什么服务发现机制,都可以通过 Spring 的RestTemplate来做基础的Http请求封装。

如果使用的前文提到的Netflix-Eureka的话,可以使用Netflix-FeignFeign是一个声明式 Web Service 客户端。客户端的负载均衡使用 Netflix-Ribbon

异步

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

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

不同的消息队列的实现,消息模型不同。各个框架的特性也不同:

RabbitMQ

RabbitMQ 是基于 AMQP 协议的开源实现,由以高性能、可伸缩性出名的 Erlang 写成。目前客户端支持 Java、.Net/C# 和 Erlang。在 AMQP(Advanced Message Queuing Protocol)的组件中,Broker 中可以包含多个Exchange(交换机)组件。Exchange 可以绑定多个 Queue 以及其他 Exchange。消息会按照 Exchange 中设置的 Routing 规则,发送到相应的 Message Queue。在 Consumer 消费了这个消息之后,会跟 Broker 建立连接。发送消费消息的通知。则 Message Queue 才会将这个消息移除。

Kafka

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

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

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

Kafka 官方提供了 Java 版本的客户端 API,Kafka 社区目前也支持多种语言,包括 PHP、Python、Go、C/C++、Ruby、NodeJS 等。

ActiveMQ

ActiveMQ 是基于 JMS(Java Messaging Service)实现的 JMSProvider。JMS主要提供了两种类型的消息:点对点(Point-to-Point)以及发布/订阅(Publish/Subscribe)。目前客户端支持 Java、C、C++、 C#、Ruby、Perl、Python、PHP。而且 ActiveMQ 支持多种协议:Stomp、AMQP、MQTT 以及 OpenWire。

RocketMQ/ONS

RocketMQ 是由阿里巴巴研发开源的高可用分布式消息队列。ONS是提供商业版的高可用集群。ONS 支持 pull/push。可支持主动推送,百亿级别消息堆积。ONS 支持全局的顺序消息,以及有友好的管理页面,可以很好的监控消息队列的消费情况,并且支持手动触发消息多次重发。

小结

通过上篇的微服务的服务发现机制,加上 Restful API,可以解决微服务间的同步方式的进程间通信。当然,既然使用了微服务,就希望所有的微服务能有合理的限界上下文(系统边界)。微服务之间的同步通信应尽量避免,以防止服务间的领域模型互相侵入。为了避免这种情况,就可以在微服务的架构中使用一层API gateway(会在下文介绍)。所有的微服务通过API gateway进行统一的请求的转发,合并。并且API gateway也需要支持同步请求,以及NIO的异步的请求(可以提高请求合并的效率以及性能)。

消息队列可以用于微服务间的解耦。在基于Docker的微服务的服务集群环境下,网络环境会比一般的分布式集群复杂。选择一种高可用的分布式消息队列实现即可。如果自己搭建诸如Kafka、RabbitMQ集群环境的话,那对于Broker设施的高可用性会要求很高。基于Springboot的微服务的话,比较推荐使用Kafka 或者ONS。虽然ONS是商用的,但是易于管理以及稳定性高,尤其对于必要场景才依赖于消息队列进行通信的微服务架构来说,会更适合。如果考虑到会存在日志收集,实时分析等场景,也可以搭建Kafka集群。目前阿里云也有了基于Kafka的商用集群设施。

使用 API Gateway 处理微服务请求转发、合并

前面主要介绍了如何解决微服务的服务发现和通信问题。在微服务的架构体系中,使用DDD思想划分服务间的限界上下文的时候,会尽量减少微服务之间的调用。为了解耦微服务,便有了基于API Gateway方式的优化方案。

解耦微服务的调用

比如,下面一个常见的需求场景——“用户订单列表”的一个聚合页面。需要请求”用户服务“获取基础用户信息,以及”订单服务“获取订单信息,再通过请求“商品服务”获取订单列表中的商品图片、标题等信息。如下图所示的场景 :

微服务Docker-GatewayAPI

如果让客户端(比如H5、Android、iOS)发出多个请求来解决多个信息聚合,则会增加客户端的复杂度。比较合理的方式就是增加API Gateway层。API Gateway跟微服务一样,也可以部署、运行在Docker容器中,也是一个Springboot应用。如下,通过Gateway API进行转发后:

Docker微服务-GatewayApi

所有的请求的信息,由Gateway进行聚合,Gateway也是进入系统的唯一节点。并且Gateway和所有微服务,以及提供给客户端的也是Restful风格API。Gateway层的引入可以很好的解决信息的聚合问题。而且可以更好得适配不同的客户端的请求,比如H5的页面不需要展示用户信息,而iOS客户端需要展示用户信息,则只需要添加一个Gateway API请求资源即可,微服务层的资源不需要进行变更。

API Gateway 的特点

API gateway除了可以进行请求的合并、转发。还需要有其他的特点,才能成为一个完整的Gateway。

响应式编程

Gateway是所有客户端请求的入口。类似Facade模式。为了提高请求的性能,最好选择一套非阻塞I/O的框架。在一些需要请求多个微服务的场景下,对于每个微服务的请求不一定需要同步。前文举例的“用户订单列表”的例子中,获取用户信息,以及获取订单列表,就是两个独立请求。只有获取订单的商品信息,需要等订单信息返回之后,根据订单的商品id列表再去请求商品微服务。为了减少整个请求的响应时间,需要Gateway能够并发处理相互独立的请求。一种解决方案就是采用响应式编程。

目前使用Java技术栈的响应式编程方式有,Java8的CompletableFuture,以及ReactiveX提供的基于JVM的实现-RxJava。

ReactiveX是一个使用可观察数据流进行异步编程的编程接口,ReactiveX结合了观察者模式、迭代器模式和函数式编程的精华。除了RxJava还有RxJS,RX"

"
即可畅读图文课全站文章
即可阅读全文

打开微信"扫一扫",将本文章分享到朋友圈

快给朋友分享吧!

收藏 收藏

1236人已收藏