Alibaba Dubbo 是国内流行的微服务框架,而 SpringCloud 是国外流行的微服务框架。SpringCloud 为微服务提供一站式完整的解决方案,具有独特的优势和发展前景。本文讲述了 SpringCloud 的历史和版本号规则、与同类产品相比的优势,并基于 SpringBoot 演示了服务注册中心、服务提供者和消费者的实现方法,并说明了高可用注册中心的部署思路。
**作者:**王克锋
出处:https://kefeng.wang/2017/12/20/spring-cloud/
版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。
Spring Cloud 是一个基于 Spring Boot 实现的“微服务架构”开发工具,它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。
所谓“微服务架构”,就是将一个完整的应用,从数据存储开始垂直拆分成多个不同的服务,每个服务都能独立部署、独立维护、独立扩展,服务与服务间通过诸如 RESTful API 的方式互相调用。
Microservice Architecture
Martin Flower - 微服务
Spring Cloud 官网
Spring Cloud 中文网
Spring Cloud
《Spring Cloud微服务实战》 - 电子工业出版社 - 翟永超 著
Spring Cloud 是一个拥有众多子项目的大项目,每个子项目都有自己的版本号,每个 Spring Cloud 版本会包含不同版本的子项目。为避免主项目与子项目版本号混淆,Spring Cloud 版本不采用数字方式,而是通过命名方式。
这些版本名称采用了伦敦地铁站名,按字母表顺序使用,如 Angel / Brixton / Camden / Dalston / Edgware / Finchley(尚未发布正式版本),当 Spring Cloud 项目的发布内容积累的重要程度时,就会发布一个“Service Releases”版本,简称 SRn 版本,其中n是一个递增数字。
Spring Cloud 版本列表见这里:Spring Cloud Dependencies
各大版本及其最新 SR、首SR月份、最新SR月份,如下(截止 2018-01-18,只涉及正式版本GA):
建议使用最新的 SR 正式版本(当前为 Edgware.SR1
)。
注意 Edgware 不支持 SpringBoot 2.x,可用 1.5.10.RELEASE(1.x 最高版本)。
否则应用类启动时会报错:java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder.
与 Dubbo 相比,SpringCloud 提供了完整的微服务各个组件(核心仍然是服务治理),提供更灵活的 REST API 调用方式(Dubbo只有RPC,当当扩展的Dubbox优化了RPC并增加了REST API)。
Dubbo 采用 Zookeeper 作为服务注册中心,因此只需要开发 Productor 和Consumer 即可;而 Spring Cloud 需要我们开发一个服务治理服务(服务注册中心),不过这对于 Spring Boot 是很便捷的。
对比点
SpringCloud
Dubbo
出品者
Spring社区
阿里巴巴
活跃度
更新频繁
五年未更新,刚重启更新
流行地
国外
国内
功能
整个微服务架构的各方面,包括服务治理,像品牌机稳定
只是实现了服务治理(注册/发现/监控/调度等),其他环节要自己接入,像组装机有风险
注册中心
Netflix Eureka(遵循AP)
ZooKeeper(遵循CP,欠佳)
调用方式
RPC/REST API(供消双方没有代码级别的强依赖)
RPC(供消双方需要使用共同接口,必须对外封装出REST API),当当的dubbox是添加了REST API支持
编码侵入性
有侵入
通过Spring配置,无侵入
开发文档
英文、中文(SpringCloud中文网、SpringCloud中国社区)
中文、英文
国际上,SpringCloud是主流;而Dubbo在国内是主流,有一定局限性,现在Dubbo在积极适配SpringCloud。长远来看,SpringCloud是未来趋势。
Spring Cloud 为服务治理(服务注册中心、服务注册与发现)做了一层抽象接口,Spring Cloud 应用中支持多种不同的服务治理框架,比如 Eureka([ju’rik]推荐使用) / Consul / Zookeeper,这些选项可以在新建项目时 Cloud Discovery 分支下看到。本例也是使用 Eureka。
Eureka分为两部分:
本例中,各个模块都运行在本机上,端口规划如下:
创建 Maven 工程(不依赖于任何 Maven 框架),创建后删除 src 目录。
wang.kefeng/spring-cloud-demo
依赖选择: Cloud Discovery / Eureka Server(服务端)
其中的 spring-cloud-starter-eureka-server
已废弃(Deprecated),需要手工改为 spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
1.8
Edgware.SR1
UTF-8
UTF-8
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
指定端口号为 8001,注意必须指定注册中心URL(eureka.client.serviceUrl.defaultZone,不论是否需要向它注册):
如果只有一个注册中心: 指定自身URL,指定“不注册自身至注册中心”(eureka.client.register-with-eureka=false);
如果有多个注册中心:指定其他注册中心URL,指定“要注册自身至指定的其他注册中心”(eureka.client.register-with-eureka=true);
server.port=8001
spring.application.name=spring-cloud-registry
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
增加注解 @EnableEurekaServer
启用“服务注册中心(Eureka Server)”
运行 SpringCloudRegistryApplication.java
### spring-cloud-registry 日志
10:01:27.601 INFO [EurekaServerInitializerConfiguration.java:72] - Started Eureka Server
10:01:27.726 INFO [StartupInfoLogger.java:57] - Started SpringCloudRegistryApplication in 15.558 seconds (JVM running for 21.684)
进入 http://localhost:8001/
可见 “Instances currently registered with Eureka” 一栏,还没有注册到 Eureka 的服务提供者和消费者的实例。
依赖选择: Cloud Discovery / Eureka Discovery(客户端)
其中的 spring-cloud-starter-eureka
已废弃(Deprecated),要手工改为 spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
1.8
Edgware.SR1
UTF-8
UTF-8
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
指定“服务提供者”的端口号为 8061;
服务提供者的名称为 spring-cloud-provider,将来 spring-cloud-consumer 以该名称调用该服务;
必须指定“服务注册中心”的地址 eureka.client.serviceUrl.defaultZone
server.port=8061
spring.application.name=spring-cloud-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
实现了两个URL:
/add: 服务提供者的 add 方法的具体实现;
/info: 在“服务注册中心”控制台中有“服务提供者”的链接,用来查看服务提供者的信息。
@RestController // 指定数据格式为 JSON(Apache Jackson)
public class SpringCloudProviderController {
@Resource
private DiscoveryClient discoveryClient; // of org.springframework
@RequestMapping(value = "/add", method = RequestMethod.GET)
public Integer add(@RequestParam Integer a, @RequestParam Integer b) {
Integer result = a + b;
return result;
}
@GetMapping("/info")
public Map info() {
Map map = new LinkedHashMap<>();
List serviceNames = discoveryClient.getServices();
for (String serviceName : serviceNames) {
List uris = new LinkedList<>();
for (ServiceInstance instance : discoveryClient.getInstances(serviceName)) {
uris.add(instance.getUri().toASCIIString());
}
map.put(serviceName, uris);
}
return map;
}
}
增加注解 @EnableDiscoveryClient
激活 Eureka 中的 DiscoveryClient 实现(SpringCloudProviderController 要用)。
运行 SpringCloudProviderApplication.java
### spring-cloud-provider 日志(向注册中心注册服务)
10:13:36.707 INFO [DiscoveryClient.java:804] - DiscoveryClient_SPRING-CLOUD-PROVIDER/192.168.1.101:spring-cloud-provider:8061: registering service...
10:13:36.878 INFO [DiscoveryClient.java:813] - DiscoveryClient_SPRING-CLOUD-PROVIDER/192.168.1.101:spring-cloud-provider:8061 - registration status: 204
10:13:36.925 INFO [StartupInfoLogger.java:57] - Started SpringCloudProviderApplication in 8.455 seconds (JVM running for 9.204)
### spring-cloud-registry 日志(服务提供者 SPRING-CLOUD-PROVIDER 的注册信息)
10:13:36.909 INFO [AbstractInstanceRegistry.java:267] - Registered instance SPRING-CLOUD-PROVIDER/192.168.1.101:spring-cloud-provider:8061 with status UP (replication=false)
10:13:37.565 INFO [AbstractInstanceRegistry.java:267] - Registered instance SPRING-CLOUD-PROVIDER/192.168.1.101:spring-cloud-provider:8061 with status UP (replication=true)
进入注册中心控制台 http://localhost:8001/
可以看到服务提供者 spring-cloud-provider
也可以点击查看其信息: http://localhost:8061/info
依赖选择: Cloud Discovery / Eureka Discovery(客户端)
其中的 spring-cloud-starter-eureka
已废弃(Deprecated),要手工改为 spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
1.8
Edgware.SR1
UTF-8
UTF-8
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
指定“服务消费者”的端口号为 8081(面向 SpringCloud 外部的 API 端口)
指定“服务消费者”的名称为 spring-cloud-provider
指定“服务注册中心”的地址 eureka.client.serviceUrl.defaultZone
server.port=8081
spring.application.name=spring-cloud-consumer
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
实现了两个URL:
/add: 面向 SpringCloud 外部调用的 API 接口,其中调用了服务提供者的 add 方法,注意 URL 格式为 http://providerName/methodName?argName1=argValue1&argName2=argValue2;
/info: 在“服务注册中心”控制台中有“服务消费者”的链接,用来查看服务消费者的信息。
@RestController // 指定数据格式为 JSON(Apache Jackson)
public class SpringCloudConsumerController {
@Resource
RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient; // of org.springframework
@RequestMapping(value = "/add", method = RequestMethod.GET)
public Integer add() {
String url = "http://spring-cloud-provider/add?a=10&b=20";
return restTemplate.getForObject(url, Integer.class);
}
@GetMapping("/info")
public Map info() {
Map map = new LinkedHashMap<>();
List serviceNames = discoveryClient.getServices();
for (String serviceName : serviceNames) {
List uris = new LinkedList<>();
for (ServiceInstance instance : discoveryClient.getInstances(serviceName)) {
uris.add(instance.getUri().toASCIIString());
}
map.put(serviceName, uris);
}
return map;
}
}
@EnableDiscoveryClient // 激活 Eureka 中的 DiscoveryClient 实现(SpringCloudProviderController 要用)
@SpringBootApplication
public class SpringCloudConsumerApplication {
@Bean // 生成 Bean,SpringCloudConsumerController 中使用
@LoadBalanced // 开启均衡负载
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(SpringCloudConsumerApplication.class, args);
}
}
负载均衡分为两种:
运行 SpringCloudConsumerApplication.java
### spring-cloud-consumer 日志(向注册中心注册服务)
10:27:46.585 INFO [DiscoveryClient.java:804] - DiscoveryClient_SPRING-CLOUD-CONSUMER/192.168.1.101:spring-cloud-consumer:8081: registering service...
10:27:46.647 INFO [DiscoveryClient.java:813] - DiscoveryClient_SPRING-CLOUD-CONSUMER/192.168.1.101:spring-cloud-consumer:8081 - registration status: 204
10:27:46.771 INFO [StartupInfoLogger.java:57] - Started SpringCloudConsumerApplication in 8.371 seconds (JVM running for 10.392)
### spring-cloud-registry 日志(服务消费者 SPRING-CLOUD-CONSUMER 的注册信息)
10:27:46.631 INFO [AbstractInstanceRegistry.java:267] - Registered instance SPRING-CLOUD-CONSUMER/192.168.1.101:spring-cloud-consumer:8081 with status UP (replication=false)
10:27:47.158 INFO [AbstractInstanceRegistry.java:267] - Registered instance SPRING-CLOUD-CONSUMER/192.168.1.101:spring-cloud-consumer:8081 with status UP (replication=true)
进入注册中心控制台 http://localhost:8001/
可以看到服务消费者 spring-cloud-consumer
也可以点击查看其信息: http://localhost:8081/info
打开 SpringCloud 对外的 API 接口,可看到计算结果:http://localhost:8081/add
@echo off & cls
## 编译三个子模块
call mvn.cmd clean package -Dmaven.test.skip=true -f spring-cloud-registry/pom.xml
call mvn.cmd clean package -Dmaven.test.skip=true -f spring-cloud-provider/pom.xml
call mvn.cmd clean package -Dmaven.test.skip=true -f spring-cloud-consumer/pom.xml
start /b java -jar spring-cloud-registry/target/spring-cloud-registry-0.0.1-SNAPSHOT.jar
pause ## 等待 registry 启动完毕后,再按任意键继续
start /b java -jar spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --server.port=8061
start /b java -jar spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --server.port=8062
start /b java -jar spring-cloud-provider/target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --server.port=8063
pause ## 等待 provider 启动完毕后,再按任意键继续
start /b java -jar spring-cloud-consumer/target/spring-cloud-consumer-0.0.1-SNAPSHOT.jar
pause ## 任意键继续结束所有程序
编译中可能遇到警告:WARN [URLConfigurationSource.java:121] - No URLs will be polled as dynamic configuration sources.
无需理会,也可以增加空的配置文件来避免:resources/config.properties
前面的服务注册中心(Eureka Server)是单点的,可能导致单点故障,生产环境应该构建高可用的Eureka Server集群。下面以双节点注册中心为例来构建:
Eureka服务治理体系:
相关参数有:
关闭保护机制(配置在注册中心): 关闭后(建议采用此方式),严格以心跳断定服务提供者是否失效,认为失效时直接当作下线,不会采用一定算法的保护机制认为它可能还有效;
提供者列表的刷新间隔(配置在注册中心):默认值为 30(秒),注册中心缓存了一份服务提供者清单,消费者查询时的数据来自该缓存,该参数定义了最新实际有效的提供者列表向缓存更新的时间间隔;
服务失效时间(配置在注册中心): 默认值为 90(秒),注册中心每60秒检查一遍,在90s内没有收到某个服务提供者的心跳包,就把它看作下线,从提供给消费者的列表中剔除;
心跳发送间隔(配置在服务提供者): 默认值为 30(秒),服务提供者每隔这个时间段,就会向注册中心发送心跳包;
eureka.server.enable-self-preservation=false
eureka.client.registry-fetch-interval-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90
eureka.instance.lease-renewal-interval-in-seconds=30
当有多个注册中心(集群)时,各服务中心相互注册为服务,只要其中一个注册中心收到服务提供者的注册信息,就会同步给其他的注册中心,以保证任何注册中心的注册表都是完整的。以两个服务中心、两个服务提供者为例,构建方案如下:
指定自身端口为 8001,注册中心地址为第二个注册中心(localhost:8002);
开启 eureka.client.register-with-eureka(把自身注册到注册中心),第一个注册中心会注册到第二个注册中心;
server.port=8001
spring.application.name=spring-cloud-registry
eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=false
指定自身端口为 8002,注册中心地址为第一个注册中心(localhost:8001);
开启 eureka.client.register-with-eureka(把自身注册到注册中心),第二个注册中心会注册到第一个注册中心;
server.port=8002
spring.application.name=spring-cloud-registry
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=false
指定注册中心为第一个注册中心(也可以指定为所有注册中心URL列表,以逗号分隔)。
如果指定多个注册中心,那么会逐个尝试注册,一旦有一个注册成功,就不会在向后续注册中心注册。
server.port=8061
spring.application.name=spring-cloud-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
## eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/,http://localhost:8002/eureka/
指定注册中心为第二个注册中心(也可以指定为所有注册中心URL列表,以逗号分隔)。
server.port=8062
spring.application.name=spring-cloud-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/
## eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/,http://localhost:8002/eureka/
指定注册中心为第一个注册中心(也可以指定为所有注册中心URL列表,以逗号分隔)。
server.port=8081
spring.application.name=spring-cloud-consumer
eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/
## eureka.client.serviceUrl.defaultZone=http://localhost:8001/eureka/,http://localhost:8002/eureka/
java -jar target/spring-cloud-registry-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar target/spring-cloud-registry-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
java -jar target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar target/spring-cloud-provider-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
java -jar target/spring-cloud-consumer-0.0.1-SNAPSHOT.jar
发现消费者依次调用了两个服务提供者,注册中心同步机制有效,消费者也达到了负载均衡效果。
http://localhost:8001/
http://localhost:8002/
发现他们都有两个服务提供者,都是完整的。
。