参考:https://www.jianshu.com/p/730d86030a41
目录
在微服务架构的系统中,我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线。在总线上的各个实例都可以方便地广播- - 些需要让其他连接在该主题上的实例都知道的消息,例如配置信息的变更或者其他一些管理操作等。
由于消息总线在微服务架构系统中被广泛使用,所以它同配置中心一样,几乎是微服务架构中的必备组件。Spring Cloud作为微服务架构综合性的解决方案,对此自然也有自己的实现,这就是本章我们将要具体介绍的Spring Cloud Bus。通过使用Spring Cloud Bus,可以非常容易地搭建起消息总线,同时实现了一些消息总线中的常用功能,比如,配合Spring Cloud Config实现微服务应用配置信息的动态更新等。
RabbitMQ实现消息总线
spring boot 整合 RabbitMQ
- 新建一个Spring Boot工程,命名为:“rabbitmq-hello”。
- 在
pom.xml
中引入如下依赖内容,其中spring-boot-starter-amqp
用于支持RabbitMQ。
|
- 在
application.properties
中配置关于RabbitMQ的连接和用户信息,
|
- 创建消息生产者
Sender
。通过注入AmqpTemplate
接口的实例来实现消息的发送,AmqpTemplate
接口定义了一套针对AMQP协议的基础操作。在Spring Boot中会根据配置来注入其具体实现。在该生产者,我们会产生一个字符串,并发送到名为hello
的队列中。
|
- 创建消息消费者
Receiver
。通过@RabbitListener
注解定义该类对hello
队列的监听,并用@RabbitHandler
注解来指定对消息的处理方法。所以,该消费者实现了对hello
队列的消费,消费操作为输出消息的字符串内容。
|
- 创建RabbitMQ的配置类
RabbitConfig
,用来配置队列、交换器、路由等高级信息。这里我们以入门为主,先以最小化的配置来定义,以完成一个基本的生产和消费过程。
|
- 创建应用主类:
|
- 创建单元测试类,用来调用消息生产:
|
完成程序编写之后,下面开始尝试运行。首先确保RabbitMQ Server已经开始,然后进行下面的操作:
- 启动应用主类,从控制台中,我们看到如下内容,程序创建了一个访问
127.0.0.1:5672
中springcloud
的连接。
|
同时,我们通过RabbitMQ的控制面板,可以看到Connection和Channels中包含当前连接的条目。
- 运行单元测试类,我们可以看到控制台中输出下面的内容,消息被发送到了RabbitMQ Server的
hello
队列中。
|
- 切换到应用主类的控制台,我们可以看到类似如下输出,消费者对
hello
队列的监听程序执行了,并输出了接受到的消息信息。
|
通过上面的示例,我们在Spring Boot应用中引入spring-boot-starter-amqp
模块,进行简单配置就完成了对RabbitMQ的消息生产和消费的开发内容。
RabbitMQ实现消息总线
- 准备工作:这里我们不做新的应用,但需要用到上一章中,我们已经实现的关于Spring Cloud Config的几个工程
- config-repo:定义在Git仓库中的一个目录,其中存储了应用名为didispace的多环境配置文件,配置文件中有一个from参数。
- config-server-eureka:配置了Git仓库,并注册到了Eureka的服务端。
- config-client-eureka:通过Eureka发现Config Server的客户端,应用名为didispace,用来访问配置服务器以获取配置信息。该应用中提供了一个
/from
接口,它会获取config-repo/didispace-dev.properties
中的from属性返回。
- 扩展config-client-eureka应用
- 修改
pom.xml
增加spring-cloud-starter-bus-amqp
模块(注意spring-boot-starter-actuator
模块也是必须的)。
- 修改
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 在配置文件中增加关于RabbitMQ的连接和用户信息
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=springcloud
spring.rabbitmq.password=123456
spring.cloud.bus.enabled=true
spring.cloud.bus.trace.enabled=true
#开放这个更新接口,可以在client、server端开放
management.endpoints.web.exposure.include=bus-refresh
-
这里面还有一个坑,我们还需要加一个注解 @RefreshScope 在需要刷新的地方
下面我们修改下 spring-cloud-config-client 的接口类,然后重启应用
@SpringBootApplication
@RestController
@EnableEurekaClient
@RefreshScope
public class ConfigClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApplication.class, args);
}
@Value("${hello}")
private String hello;
@RequestMapping("hello")
public String hello() {
return hello;
}
}
- 启动config-server-eureka,再启动两个config-client-eureka(分别在不同的端口上,比如7002、7003),我们可以在config-client-eureka中的控制台中看到如下内容,在启动时候,客户端程序多了一个
/bus/refresh
请求。
o.s.b.a.e.mvc.EndpointHandlerMapping : Mapped "{[/bus/refresh],methods=[POST]}" onto public void org.springframework.cloud.bus.endpoint.RefreshBusEndpoint.refresh(java.lang.String)
- 先访问两个config-client-eureka的
/from
请求,会返回当前config-repo/didispace-dev.properties
中的from属性。 - 接着,我们修改
config-repo/didispace-dev.properties
中的from属性值,并发送POST请求到其中的一个/bus/refresh
。 - 最后,我们再分别访问启动的两个config-client-eureka的
/from
请求,此时这两个请求都会返回最新的config-repo/didispace-dev.properties
中的from属性。
到这里,我们已经能够通过Spring Cloud Bus来实时更新总线上的属性配置了。
原理分析
我们通过使用Spring Cloud Bus与Spring Cloud Config的整合,并以RabbitMQ作为消息代理,实现了应用配置的动态更新。
整个方案的架构如上图所示,其中包含了Git仓库、Config Server、以及微服务“Service A”的三个实例,这三个实例中都引入了Spring Cloud Bus,所以他们都连接到了RabbitMQ的消息总线上。
当我们将系统启动起来之后,“Service A”的三个实例会请求Config Server以获取配置信息,Config Server根据应用配置的规则从Git仓库中获取配置信息并返回。
此时,若我们需要修改“Service A”的属性。首先,通过Git管理工具去仓库中修改对应的属性值,但是这个修改并不会触发“Service A”实例的属性更新。我们向“Service A”的实例3发送POST请求,访问/bus/refresh
接口。此时,“Service A”的实例3就会将刷新请求发送到消息总线中,该消息事件会被“Service A”的实例1和实例2从总线中获取到,并重新从Config Server中获取他们的配置信息,从而实现配置信息的动态更新。
而从Git仓库中配置的修改到发起/bus/refresh
的POST请求这一步可以通过Git仓库的Web Hook来自动触发。由于所有连接到消息总线上的应用都会接受到更新请求,所以在Web Hook中就不需要维护所有节点内容来进行更新,从而解决了通过Web Hook来逐个进行刷新的问题。
指定刷新范围
上面的例子中,我们通过向服务实例请求Spring Cloud Bus的/bus/refresh
接口,从而触发总线上其他服务实例的/refresh
。但是有些特殊场景下(比如:灰度发布),我们希望可以刷新微服务中某个具体实例的配置。
Spring Cloud Bus对这种场景也有很好的支持:/bus/refresh
接口还提供了destination
参数,用来定位具体要刷新的应用程序。比如,我们可以请求/bus/refresh?destination=customers:9000
,此时总线上的各应用实例会根据destination
属性的值来判断是否为自己的实例名,若符合才进行配置刷新,若不符合就忽略该消息。
destination
参数除了可以定位具体的实例之外,还可以用来定位具体的服务。定位服务的原理是通过使用Spring的PathMatecher(路径匹配)来实现,比如:/bus/refresh?destination=customers:**
,该请求会触发customers
服务的所有实例进行刷新。
架构优化
既然Spring Cloud Bus的/bus/refresh
接口提供了针对服务和实例进行配置更新的参数,那么我们的架构也相应的可以做出一些调整。在之前的架构中,服务的配置更新需要通过向具体服务中的某个实例发送请求,再触发对整个服务集群的配置更新。虽然能实现功能,但是这样的结果是,我们指定的应用实例就会不同于集群中的其他应用实例,这样会增加集群内部的复杂度,不利于将来的运维工作,比如:我们需要对服务实例进行迁移,那么我们不得不修改Web Hook中的配置等。所以我们要尽可能的让服务集群中的各个节点是对等的。
因此,我们将之前的架构做了一些调整,如下图所示:
我们主要做了这些改动:
- 在Config Server中也引入Spring Cloud Bus,将配置服务端也加入到消息总线中来。
/bus/refresh
请求不在发送到具体服务实例上,而是发送给Config Server,并通过destination
参数来指定需要更新配置的服务或实例。
通过上面的改动,我们的服务实例就不需要再承担触发配置更新的职责。同时,对于Git的触发等配置都只需要针对Config Server即可,从而简化了集群上的一些维护工作。
kafka实现消息总线
Spring Cloud Bus除了支持RabbitMQ的自动化配置之外,还支持现在被广泛应用的Kafka。在本文中,我们将搭建一个Kafka的本地环境,并通过它来尝试使用Spring Cloud Bus对Kafka的支持,实现消息总线的功能。
Kafka简介
Kafka是一个由LinkedIn开发的分布式消息系统,它于2011年初开源,现在由著名的Apache基金会维护与开发。Kafka使用Scala实现,被用作LinkedIn的活动流和运营数据处理的管道,现在也被诸多互联网企业广泛地用作为数据流管道和消息系统。
Kafka是基于消息发布/订阅模式实现的消息系统,其主要设计目标如下:
- 消息持久化:以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。
- 高吞吐:在廉价的商用机器上也能支持单机每秒100K条以上的吞吐量
- 分布式:支持消息分区以及分布式消费,并保证分区内的消息顺序
- 跨平台:支持不同技术平台的客户端(如:Java、PHP、Python等)
- 实时性:支持实时数据处理和离线数据处理
- 伸缩性:支持水平扩展
Kafka中涉及的一些基本概念:
- Broker:Kafka集群包含一个或多个服务器,这些服务器被称为Broker。
- Topic:逻辑上同Rabbit的Queue队列相似,每条发布到Kafka集群的消息都必须有一个Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个Broker上,但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
- Partition:Partition是物理概念上的分区,为了提供系统吞吐率,在物理上每个Topic会分成一个或多个Partition,每个Partition对应一个文件夹(存储对应分区的消息内容和索引文件)。
- Producer:消息生产者,负责生产消息并发送到Kafka Broker。
- Consumer:消息消费者,向Kafka Broker读取消息并处理的客户端。
- Consumer Group:每个Consumer属于一个特定的组(可为每个Consumer指定属于一个组,若不指定则属于默认组),组可以用来实现一条消息被组内多个成员消费等功能。
快速入门
在对Kafka有了一些基本了解之后,下面我们来尝试构建一个Kafka服务端,并体验一下基于Kafka的消息生产与消费。
环境安装
首先,我们需要从官网上下载安装介质。下载地址为:http://kafka.apache.org/downloads.html
。本例中采用的版本为:Kafka-0.10.0.1
在解压Kafka的安装包之后,可以看到其目录结构如下:
kafka
+-bin
+-windows
+-config
+-libs
+-logs
+-site-docs
由于Kafka的设计中依赖了ZooKeeper,所以我们可以在bin
和config
目录中除了看到Kafka相关的内容之外,还有ZooKeeper相关的内容。其中bin
目录存放了Kafka和ZooKeeper的命令行工具,bin
根目录下是适用于Linux/Unix的shell,而bin/windows
下的则是适用于windows下的bat。我们可以根据实际的系统来设置环境变量,以方便后续的使用和操作。而在config
目录中,则是用来存放了关于Kafka与ZooKeeper的配置信息。
启动测试
下面我们来尝试启动ZooKeeper和Kafka来进行消息的生产和消费。示例中所有的命令均已配置了Kafka的环境变量为例。
- 启动ZooKeeper,执行命令:
zookeeper-server-start config/zookeeper.properties
,该命令需要指定zookeeper的配置文件位置才能正确启动,kafka的压缩包中包含了其默认配置,开发与测试环境不需要修改。
[2016-09-28 08:05:34,849] INFO Reading configuration from: config\zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
[2016-09-28 08:05:34,850] INFO autopurge.snapRetainCount set to 3 (org.apache.zookeeper.server.DatadirCleanupManager)
[2016-09-28 08:05:34,851] INFO autopurge.purgeInterval set to 0 (org.apache.zookeeper.server.DatadirCleanupManager)
[2016-09-28 08:05:34,851] INFO Purge task is not scheduled. (org.apache.zookeeper.server.DatadirCleanupManager)
[2016-09-28 08:05:34,852] WARN Either no config or no quorum defined in config, running in standalone mode (org.apache.zookeeper.server.quorum.QuorumPeerMain)
[2016-09-28 08:05:34,868] INFO Reading configuration from: config\zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
[2016-09-28 08:05:34,869] INFO Starting server (org.apache.zookeeper.server.ZooKeeperServerMain)
...
[2016-09-28 08:05:34,940] INFO binding to port 0.0.0.0/0.0.0.0:2181 (org.apache.zookeeper.server.NIOServerCnxnFactory)
从控制台信息中,我们可以看到ZooKeeper从指定的config/zookeeper.properties
配置文件中读取信息并绑定2181端口启动服务。有时候启动失败,可查看一下端口是否被占用,可以杀掉占用进程或通过修改config/zookeeper.properties
配置文件中的clientPort
内容以绑定其他端口号来启动ZooKeeper。
-
启动Kafka,执行命令:
kafka-server-start config/server.properties
,该命令也需要指定Kafka配置文件的正确位置,如上命令中指向了解压目录包含的默认配置。若在测试时,使用外部集中环境的ZooKeeper的话,我们可以在该配置文件中通过zookeeper.connect
参数来设置ZooKeeper的地址和端口,它默认会连接本地2181端口的ZooKeeper;如果需要设置多个ZooKeeper节点,可以为这个参数配置多个ZooKeeper地址,并用逗号分割。比如:zookeeper.connect=127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002
。 -
创建Topic,执行命令:
kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
,通过该命令,创建一个名为“test”的Topic,该Topic包含一个分区一个Replica。在创建完成后,可以使用kafka-topics --list --zookeeper localhost:2181
命令来查看当前的Topic。
另外,如果我们不使用kafka-topics
命令来手工创建,直接进行下面的内容进行消息创建时也会自动创建Topics来使用。
-
创建消息生产者,执行命令:
kafka-console-producer --broker-list localhost:9092 --topic test
。kafka-console-producer
命令可以启动Kafka基于命令行的消息生产客户端,启动后可以直接在控制台中输入消息来发送,控制台中的每一行数据都会被视为一条消息来发送。我们可以尝试输入几行消息,由于此时并没有消费者,所以这些输入的消息都会被阻塞在名为test的Topics中,直到有消费者将其消费掉位置。 -
创建消息消费者,执行命令:
kafka-console-consumer --zookeeper localhost:2181 --topic test --from-beginning
。kafka-console-consumer
命令启动的是Kafka基于命令行的消息消费客户端,在启动之后,我们马上可以在控制台中看到输出了之前我们在消息生产客户端中发送的消息。我们可以再次打开之前的消息生产客户端来发送消息,并观察消费者这边对消息的输出来体验Kafka对消息的基础处理。
整合Spring Cloud Bus
在上一篇使用Rabbit实现消息总线的案例中,我们已经通过引入spring-cloud-starter-bus-amqp
模块,完成了使用RabbitMQ来实现的消息总线。若我们要使用Kafka来实现消息总线时,只需要把spring-cloud-starter-bus-amqp
替换成spring-cloud-starter-bus-kafka
模块,在pom.xml
的dependenies节点中进行修改,具体如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-kafka</artifactId>
</dependency>
如果我们在启动Kafka时均采用了默认配置,那么我们不需要再做任何其他配置就能在本地实现从RabbitMQ到Kafka的切换。我们可以尝试把刚刚搭建的ZooKeeper、Kafka启动起来,并将修改为spring-cloud-starter-bus-kafka
模块的config-server和config-client启动起来。
在config-server启动时,我们可以在控制台中看到如下输出:
2016-09-28 22:11:29.627 INFO 15144 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder : Using kafka topic for outbound: springCloudBus
2016-09-28 22:11:29.642 INFO 15144 --- [-localhost:2181] org.I0Itec.zkclient.ZkEventThread : Starting ZkClient event thread.
...
016-09-28 22:11:30.290 INFO 15144 --- [ main] o.s.i.kafka.support.ProducerFactoryBean : Using producer properties => {bootstrap.servers=localhost:9092, linger.ms=0, acks=1, compression.type=none, batch.size=16384}
2016-09-28 22:11:30.298 INFO 15144 --- [ main] o.a.k.clients.producer.ProducerConfig : ProducerConfig values:
...
2016-09-28 22:11:30.322 INFO 15144 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$1 : Adding {message-handler:outbound.springCloudBus} as a subscriber to the 'springCloudBusOutput' channel
2016-09-28 22:11:30.322 INFO 15144 --- [ main] o.s.integration.channel.DirectChannel : Channel 'config-server:7001.springCloudBusOutput' has 1 subscriber(s).
2016-09-28 22:11:30.322 INFO 15144 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$1 : started outbound.springCloudBus
...
2016-09-28 22:11:31.465 INFO 15144 --- [ main] s.i.k.i.KafkaMessageDrivenChannelAdapter : started org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter@4178cb34
2016-09-28 22:11:31.467 INFO 15144 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$7 : Adding {message-handler:inbound.springCloudBus.anonymous.8b9e6c7b-6a50-48c5-b981-8282a0d5a30b} as a subscriber to the 'bridge.springCloudBus' channel
2016-09-28 22:11:31.467 INFO 15144 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$7 : started inbound.springCloudBus.anonymous.8b9e6c7b-6a50-48c5-b981-8282a0d5a30b
从控制台的输出内容,我们可以看到config-server连接到了Kafka中,并使用了名为springCloudBus
的Topic。
此时,我们可以使用kafka-topics --list --zookeeper localhost:2181
命令来查看当前Kafka中的Topic,若已成功启动了config-server并配置正确,我们就可以在Kafka中看到已经多了一个名为springCloudBus
的Topic。
我们再启动配置了spring-cloud-starter-bus-kafka
模块的config-client,可以看到控制台中输出如下内容:
2016-09-28 22:43:55.067 INFO 6136 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder : Using kafka topic for outbound: springCloudBus
2016-09-28 22:43:55.078 INFO 6136 --- [-localhost:2181] org.I0Itec.zkclient.ZkEventThread : Starting ZkClient event thread.
...
2016-09-28 22:50:38.584 INFO 828 --- [ main] o.s.i.kafka.support.ProducerFactoryBean : Using producer properties => {bootstrap.servers=localhost:9092, linger.ms=0, acks=1, compression.type=none, batch.size=16384}
2016-09-28 22:50:38.592 INFO 828 --- [ main] o.a.k.clients.producer.ProducerConfig : ProducerConfig values:
...
2016-09-28 22:50:38.615 INFO 828 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$1 : Adding {message-handler:outbound.springCloudBus} as a subscriber to the 'springCloudBusOutput' channel
2016-09-28 22:50:38.616 INFO 828 --- [ main] o.s.integration.channel.DirectChannel : Channel 'didispace:7002.springCloudBusOutput' has 1 subscriber(s).
2016-09-28 22:50:38.616 INFO 828 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$1 : started outbound.springCloudBus
...
2016-09-28 22:50:39.162 INFO 828 --- [ main] s.i.k.i.KafkaMessageDrivenChannelAdapter : started org.springframework.integration.kafka.inbound.KafkaMessageDrivenChannelAdapter@60cf855e
2016-09-28 22:50:39.162 INFO 828 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$7 : Adding {message-handler:inbound.springCloudBus.anonymous.f8fc9c0c-ccd3-46dd-9537-07198f4ee216} as a subscriber to the 'bridge.springCloudBus' channel
2016-09-28 22:50:39.163 INFO 828 --- [ main] o.s.c.s.b.k.KafkaMessageChannelBinder$7 : started inbound.springCloudBus.anonymous.f8fc9c0c-ccd3-46dd-9537-07198f4ee216
可以看到,config-client启动时输出了类似的内容,他们都订阅了名为springCloudBus
的Topic。
在启动了config-server和config-client之后,为了更明显地观察消息总线刷新配置的效果,我们可以在本地启动多个不同端口的config-client。此时,我们的config-server以及多个config-client都已经连接到了由Kafka实现的消息总线上。我们可以先访问各个config-client上的/from
请求,查看他获取到的配置内容。然后,修改Git中对应的参数内容,再访问各个config-client上的/from
请求,可以看到配置内容并没有改变。最后,我们向config-server发送POST请求:/bus/refresh
,此时我们再去访问各个config-client上的/from
请求,就能获得到最新的配置信息,各客户端上的配置都已经加载为最新的Git配置内容。
从config-client的控制台中,我们可以看到如下内容:
2016-09-29 08:20:34.361 INFO 21256 --- [ kafka-binder-1] o.s.cloud.bus.event.RefreshListener : Received remote refresh request. Keys refreshed [from]
RefreshListener
监听类记录了收到远程刷新请求,并刷新了from
属性的日志。
Kafka配置
在上面的例子中,由于Kafka、ZooKeeper均运行于本地,所以我们没有在测试程序中通过配置信息来指定Kafka和ZooKeeper的配置信息,就完成了本地消息总线的试验。但是我们实际应用中,Kafka和ZooKeeper一般都会独立部署,所以在应用中都需要来为Kafka和ZooKeeper配置一些连接信息等。Kafka的整合与RabbitMQ不同,在Spring Boot 1.3.7中并没有直接提供的Starter模块,而是采用了Spring Cloud Stream的Kafka模块,所以对于Kafka的配置均采用了spring.cloud.stream.kafka
的前缀,比如:
属性名 | 说明 | 默认值 |
---|---|---|
spring.cloud.stream.kafka.binder.brokers | Kafka的服务端列表 | localhost |
spring.cloud.stream.kafka.binder.defaultBrokerPort | Kafka服务端的默认端口,当brokers 属性中没有配置端口信息时,就会使用这个默认端口 | 9092 |
spring.cloud.stream.kafka.binder.zkNodes | Kafka服务端连接的ZooKeeper节点列表 | localhost |
spring.cloud.stream.kafka.binder.defaultZkPort | ZooKeeper节点的默认端口,当zkNodes 属性中没有配置端口信息时,就会使用这个默认端口 | 2181 |
更多配置参数请参考官方文档