Spring Cloud 接入 EDAS 之服务发现篇
目前 EDAS 已经完全支持 Spring Cloud 应用的部署了,你可以将你的Spring Cloud 应用部署到 EDAS 中。
同时,为了更好地将阿里中间件的功能以云服务的方式提供给大家,我们也对 Spring Cloud 中的一些组件进行了加强或替换的工作。
让我们先来聊聊服务发现。
我们知道原生的 Spring Cloud 支持多种服务注册与发现的方式,Eureka 、 Consul 、 Zookeeper 等,目前使用最多最广的就是 Eureka了,那我们就先从一个简单的 Eureka Demo 说起。
Eureka Demo
创建服务注册中心
创建一个基础的 Spring Cloud 工程,命名为 eureka-server,并在 pom.xml 中引入需要的依赖内容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
通过 @EnableEurekaServer 注解来启动一个服务注册中心。只需要在一个普通的 Spring Boot 应用中添加这个注解就能开启此功能,代码如下:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
这样启动时,应用将完全只用默认配置,如果我们想给服务命名,或者是修改监听端口,可以在 resource/application.properties 中进行如下配置。
由于我们自己就是唯一的一个 EurekaServer ,这里就不向自己注册自己了,将 register-with-eureka 设置成 false。
spring.application.name=eureka-server
server.port=8761
eureka.client.register-with-eureka=false
只需要直接运行 EurekaServerApplication 的 main 函数,eureka server 即可启动成功。
启动成功后,可以在 https://localhost:8761 页面查看详情。
页面打开成功,表明服务已经启动,目前 instances 为空,表明还没有服务注册上来。
创建服务提供者
创建一个 Spring Cloud 工程,命名为 service-provider。同样,首先在 pom.xml 中引入需要的依赖内容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
接着是服务提供端的代码,其中 @EnableDiscoveryClient 注解表明此应用需开启服务注册与发现功能。
@SpringBootApplication
@EnableDiscoveryClient
public class ServerApplication {
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}
既然是服务提供者,所以我们还需要提供一个简单的服务
@RestController
public class PersonController {
@RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
public String echo(@PathVariable String string) {
return string;
}
}
最后同样是配置,出去配置应用名与监听端口外,我们还得告诉应用,服务注册中心的地址。
spring.application.name=service-provider
server.port=18081
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/
启动 service-provider 服务,在 Eureka 页面查看服务是否已经注册成功.
可以看到 instances 中已经存在的实例有了,service-provider,端口是 18081。
创建服务消费者
这个例子中,我们将不仅仅是演示服务发现的功能,同时还将演示 Eureka 服务发现 与 RestTemplate、AsyncRestTemplate、FeignClient这三个客户端是如何结合的。因为实际使用中,我们更多使用的是用这三个客户端进行服务调用。
创建一个 Spring Cloud 工程,命名为 service-consumer。同样,首先在 pom.xml 中引入需要的依赖内容:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
因为在这里我们要演示 FeignClient 的使用,所以与 service-provider 相比,我们的依赖增加了一个 spring-cloud-starter-feign。
配置好依赖后,我们首先在启动函数里完成三件事:启用服务注册与发现,激活 FeignClients,将 RestTemplate 与 AsyncRestTemplate 与服务发现结合。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@LoadBalanced
@Bean
public AsyncRestTemplate asyncRestTemplate(){
return new AsyncRestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
在使用 FeignClient 之前,我们还需要完善它的配置,其中代码如下
@FeignClient(name = "service-provider")
public interface EchoService {
@RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
String echo(@PathVariable("str") String str);
}
在这三个客户端都配置好,并与 Eureka Client 结合后,我们可以在 Controller 中直接使用他们。
@RestController
public class Controller {
@Autowired
private RestTemplate restTemplate;
@Autowired
private AsyncRestTemplate asyncRestTemplate;
@Autowired
private EchoService echoService;
@RequestMapping(value = "/echo-rest/{str}", method = RequestMethod.GET)
public String rest(@PathVariable String str) {
return restTemplate.getForObject("https://service-provider/echo/" + str, String.class);
}
@RequestMapping(value = "/echo-async-rest/{str}", method = RequestMethod.GET)
public String asyncRest(@PathVariable String str) throws Exception{
ListenableFuture<ResponseEntity<String>> future = asyncRestTemplate.
getForEntity("https://service-provider/echo/"+str, String.class);
return future.get().getBody();
}
@RequestMapping(value = "/echo-feign/{str}", method = RequestMethod.GET)
public String feign(@PathVariable String str) {
return echoService.echo(str);
}
}
最后,还是不能忘了配置,特别是服务注册中心的地址
spring.application.name=service-consumer
server.port=18082
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/
启动服务,开始我们的调用吧,可以看到调用都成功了,
注意:AsyncRestTemplate 接入 服务发现的时间比较晚,需要在 Dalston 之后的版本才能使用,具体详情参见此 pull request
Eureka 的烦恼
前面的例子在本机工作起来是很方便的,但是很遗憾,这只是一个 demo ,实际部署中我们可能都踩过坑或者有这么一些不爽。
- 只有一个服务注册中心,显然这不符合高可用的原则,高可用就得加机器,维护成本太高了。
- 实际生产中,不会将服务注册中心与业务服务部署在同一台机器上。如果上云的时候,我们 eureka server 的地址变化了,我还得修改配置文件里 eureka 服务的地址,太麻烦了。
- 实际使用中,我的服务注册中心肯定是需要加密的,总不能随随便便一个人就来随便注册和看看我都有哪些服务吧,我还得自己实现服务注册的安全。
- 为什么我注册上去的服务,不是一个 ip ,而是一个奇怪的名字,通过这个名字我怎么能调通服务。其实只是因为没有增加 eureka.instance.prefer-ip-address=true这个配置。依旧需要添加配置。
- eureka 因为缓存设计的原因,使得服务注册上去之后,最迟需要两分钟后才能发现。
或许你希望有人提供一个 安全、稳定、高可用、高性能、简单易用的服务注册中心。
然后,我不想配置那么一大堆地址了
最后,我也不想修改我的代码
是的,EDAS 服务注册中心,就是这样一个解决方案。
只需要修改两行代码以及 pom 依赖,无缝将服务注册中心从 Eureka 切换到 EDAS 服务注册中心。
你将得到
- 稳定高可用的服务注册中心
- 安全的服务注册、服务发现
- 秒级的服务发现机制
- 无需再关心服务注册中心的地址
EDAS 服务注册中心
如何接入
源码的修改,只有两行,需要在 main 函数中添加两行,修改之后的 service-provider 的 main 函数如下。
public static void main(String[] args) {
PandoraBootstrap.run(args);
SpringApplication.run(ServerApplication.class, args);
PandoraBootstrap.markStartupAndWait();
}
pom.xml 的修改有两点
首先就是将原来的 eureka的starter 替换成 EDAS 服务注册中心的starter,并加入 pandora 的依赖。
修改之后的 service-provider 的 dependences 依赖如下。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vipclient</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-pandora</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
在 build 的 plugins 中,也需要修改成 EDAS 的方式,修改后的内容如下,版本号后续可能会升级。
<build>
<plugins>
<plugin>
<groupId>com.taobao.pandora</groupId>
<artifactId>pandora-boot-maven-plugin</artifactId>
<version>2.1.7.8</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
service-consumer 的修改方式与 service-provider 的修改方式完全一样。
使用方式
使用方式?使用方式已经在接入方式里了,其他方式和原生的完全一样。
如果你习惯了 Eureka 页面查看服务状态的方式,EDAS 控制台同样也提供了相关的功能。
本地调试
本地调试时,需要下载轻量级配置中心,并将其启动,详情参见 轻量级配置中心
最后在应用的启动时,配置 JVM 参数,配置如下。
-Dvipserver.client.port=8080
Demo 下载
工作原理
换了一个依赖就把 Eureka 替换成了 EDAS 服务注册中心,虽然方便,但是这对于我来说相当于是一个黑盒啊,黑盒总是让人很没有安全感。
下面我们将从 服务注册中心寻址、服务注册与下线、客户端结合、高可用、安全等多个方面来分析原理。
服务注册中心寻址
既然不需要在配置文件里配置服务注册中心的地址了,那么客户端是如何找到服务中心的呢?
其实是通过一个 http 请求来实现的,https://jmenv.tbsite.net/vipserver/serverlist。
不仅仅是客户端,服务端也是通过这个地址来互相发现的。
而在 EDAS 的机器上, jmenv.tbsite.net 是自动配置的。如果在结合我们的轻量级配置中心做本地的开发调试时,还是需要做一点额外配置。
服务注册与下线
服务注册的通信协议是 HTTP 协议,默认注册的应用名是 spring.application.name ,如果有需要将某个应用发布成多个服务名的话,可以试试在 /resource/application.properties
中配置 vipserver.register.doms
的方式来实现,多个服务名中间用英文逗号 ,
隔开。
服务注册成功后,client 端将会与 server 端维持长连接,通过心跳来检查服务的可用性,当超过一定时间内 server 端没有收到 client 端的心跳时,会将服务标记成不可用,这样其他 client 在查询时就能发现此服务当前处于不可用的状态。
如果短时间内,大量 client 与 server 心跳失败,则会出发降级保护机制,这时候,这些服务会暂时不被标记成不可用的状态。
客户端结合
与客户端结合的方式, EDAS 服务发现组件与 Eureka 是完全一致的。
对于 RestTemplate 和 AsyncRestTemplate 来说,添加上 @LoadBalanced 注解,即可直接接入服务发现以及负载均衡。
添加了此注解后,他们将分别会被添加 LoadBalancerInterceptor 和 AsyncLoadBalancerInterceptor 这两个拦截器。
执行的过程中,所有请求都会分别被这两个 Interceptor 所拦截,通过其所持有的 LoadBalancerClient 对象去执行这个请求。
而这个 LoadBalancerClient 对象,其实就是 RibbonLoadBalancerClient 的一个实例,在其源码中,execute 方法的执行逻辑如下
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
首先我们会拿到一个 ILoadBalancer 对象,然后再通过这个 loadBalancer 对象去拿到我们真正需要调用的服务的地址。
ILoadBalancer有这么几个实现类,BaseLoadBalancer、DynamicServerListLoadBalancer、ZoneAwareLoadBalancer等。
看看这个类RibbonClientConfiguration,默认注入的是这个,ZoneAwareLoadBalancer
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
关于ServerList的来源,我们看这个类 EurekaRibbonClientConfiguration
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
可以看到,是通过 EurekeClient 来维护实例的地址列表的。
负载均衡选择策略?ZoneAware,字面上可以看出是和 zone 有关的,不过 zone 都是默认的情况下,退化成 RoundRobin。
目前的植入做的很简单,单纯地重新抢先注入了上文中提到的两个 Bean。
@Bean
@ConditionalOnMissingBean
public ServerList<Server> ribbonServerList(IClientConfig config) {
return new VipserverList(config.getClientName());
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping) {
return new DynamicServerListLoadBalancer<Server>(config, rule, ping, serverList, serverListFilter,
new PollingServerListUpdater(1000L, 1000L));
}
负载均衡使用的也是默认的RoundRobin。后续我们会把权重等支持添加进来。
高可用实现
服务端高可用
- eureka
Eureka的多个server 是对等的实体,在 CAP 中选择了 AP。
节点间的数据使用的是最终一致性,eureka 会将注册的信息同步到 peer 节点,但是 peer 节点不会二次传播。
peer节点需要显示地在配置中设置。如果 peer 节点配置的不全,那么集群的概念也不存在了,节点之间的关系是通过 peer 节点的显示配置来维护的。 - EDAS 服务注册发现组件
EDAS 服务注册中心的多个 server,存在主从,各节点之间使用 raft 协议保证一致性。
server 之间的互相感知是通过访问 https://jmenv.tbsite.net/vipserver/serverlist 来获取其他 peer 节点地址来实现的。
然后通过自定义的端口和协议来进行选举和数据同步等操作。CAP 中选择的是 CP。
客户端高可用
- eureka
通过本地缓存来实现,当 server 连接不上时,直接使用本地缓存。每 30s 异步更新一次缓存,避免了每次请求都强依赖于服务注册中心。 - EDAS 服务注册发现组件
通过本地缓存来实现,当 server 连接不上时,直接使用本地缓存。异步更新缓存,避免了每次请求都强依赖于服务注册中心。同时,还提供了通过 UDP 主动 push 的方式在新服务节点加入时及时通知。
安全的实现
EDAS 服务注册发现组件,结合 EDAS 已有的安全功能,在每次注册、心跳和查询请求中都添加了验签鉴权的操作,保护了服务的安全性。
最后更新:2017-11-17 18:04:42