阿里云分布式应用服务 关注
手机版
  1. 云栖社区>
  2. 阿里云分布式应用服务>
  3. 博客>
  4. 正文

Spring Cloud 接入 EDAS 之服务发现篇

flystar32 2017-11-17 17:51:20 浏览1356 评论2 发表于: 阿里云分布式应用服务 >> 微服务

RPC 开发框架与中间件 负载均衡 EDAS springcloud

摘要: EDAS 已经完全支持 Spring Cloud 应用的部署了,同时我们也对 Spring Cloud 中的一些组件进行了加强或替换的工作,将阿里中间件的功能以云服务的形式提供给大家。本文将详细介绍如何将 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 即可启动成功。
启动成功后,可以在 http://localhost:8761 页面查看详情。

5a7b6781-f069-46b0-8df0-ab07ae4ca490.png | center

页面打开成功,表明服务已经启动,目前 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 EchoController {
    @RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
    public String echo(@PathVariable String string) {
        return string;
    }
}

最后同样是配置,除去配置应用名与监听端口外,还需要配置一下 Eureka Server 的地址。

spring.application.name=service-provider
server.port=18081
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

启动 service-provider 服务,在 Eureka 页面查看服务是否已经注册成功。

ab52a743-9e67-4be7-8c75-aa1bce6c9471.png | center

可以看到 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 相比,pom.xml文件中的依赖增加了一个 spring-cloud-starter-feign。

配置好依赖后,首先在启动函数里完成三件事:
使用 @EnableDiscoveryClient 注解启用服务注册与发现;使用 @EnableFeignClients 注解激活 FeignClients;添加 @LoadBalanced 注解将 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 之前,我们还需要完善它的配置,配置服务名以及方法对应的HTTP请求,其中代码如下

@FeignClient(name = "service-provider")
public interface EchoService {
    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    String echo(@PathVariable("str") String str);
}

然后,我们就可以在 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("http://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("http://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=http://localhost:8761/eureka/

启动服务,分别进行调用,可以看到调用都成功了。

32b244d5-53c6-426c-9edd-13414c8fd91f.png | center

注意:AsyncRestTemplate 接入 服务发现的时间比较晚,需要在 Dalston 之后的版本才能使用,具体详情参见此 pull request

Eureka 的烦恼

前面的例子在本机工作起来是很方便的,但是很遗憾,这只是一个 demo ,实际部署中我们可能都踩过坑或者有这么一些不爽。

  • 只有一个服务注册中心,显然这不符合高可用的原则,高可用就得增加
    eureka server 的数量,维护成本太高了。
  • 实际生产中,不会将服务注册中心与业务服务部署在同一台机器上。实际部署中,当 eureka server 的地址发生变化时,还得修改配置文件里 eureka server的地址,太麻烦了。
  • 实际使用中,服务注册发现中心的安全性也是需要考虑的,应该对服务注册和发现的请求进行鉴权,来确保服务的安全性,安全也是急需解决的问题。
  • eureka 使用过程中,有可能出现注册上去的服务地址不是一个 ip ,而是一个 hostname 的情况,事实上又无法通过 hostname 进行服务调用。其实只是因为没有增加 eureka.instance.prefer-ip-address=true这个配置,依旧需要添加配置。
  • eureka 因为缓存设计的原因,使得服务注册上去之后,最迟需要两分钟后才能发现。

或许你希望有人提供一个 安全、稳定、高可用、高性能、简单易用的服务注册中心。
然后,你也不想配置那么一大堆地址了
最后,你甚至也不想修改原有已经接入 Eureka 的代码

是的,EDAS 服务注册中心,就是这样一个解决方案。
只需要修改两行代码以及 pom 依赖,无缝将服务注册中心从 Eureka 切换到 EDAS 服务注册中心。
你将得到

  • 稳定高可用的服务注册中心
  • 安全的服务注册、服务发现
  • 秒级的服务发现机制
  • 无需再关心服务注册中心的地址

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 的 dependencies 依赖如下。

<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 的修改方式完全一样。

注意 由于目前 spring cloud for aliware 尚未进入中央仓库,需要配置 maven 的私服地址,配置详情参考 私服配置

使用方式

使用方式?使用方式已经在接入方式里了,其他方式和原生的完全一样。
如果你习惯了 Eureka 页面查看服务状态的方式,EDAS 控制台同样也提供了相关的功能。

本地调试

本地调试时,需要下载轻量级配置中心,并将其启动,详情参见 轻量级配置中心

最后在应用的启动时,配置 JVM 参数,配置如下。

-Dvipserver.server.port=8080 

Demo 下载

server-demo

client-demo

工作原理

换了一个依赖就把 Eureka 替换成了 EDAS 服务注册中心,虽然方便,但是这对于你来说也许相当于是一个黑盒,黑盒总是让人很没有安全感。
下面我们将从 服务注册中心寻址、服务注册与下线、客户端结合、高可用、安全等多个方面来分析原理。

服务注册中心寻址

既然不需要在配置文件里配置服务注册中心的地址了,那么客户端是如何找到服务中心的呢?
其实是通过一个 http 请求来实现的,http://jmenv.tbsite.net/vipserver/serverlist
不仅仅是客户端,服务端也是通过这个地址来互相发现的。 在 EDAS 的机器上, jmenv.tbsite.net 是自动配置的。
如果是使用轻量级配置中心做本地的开发调试,还需要做一点额外配置,即 -Dvipserver.server.port=8080,如果你的轻量级配置中心与应用部署在不同的机器上,还需进行 hosts 绑定,详情见 轻量级配置中心

服务注册与下线

服务注册的通信协议是 HTTP 协议,默认注册的应用名是 spring.application.name ,如果有需要将某个应用发布成多个服务名的话,可以试试在 /resource/application.properties 中配置 vipserver.register.doms 的方式来实现,多个服务名中间用英文逗号 , 隔开。 如vipserver.register.doms=service1,service2

服务注册成功后,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 来维护实例的地址列表的。

目前的植入做的很简单,单纯地注入了上文中提到的两个 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));
    }

高可用实现

服务端高可用

  • eureka
    Eureka的多个server 是对等的实体,在 CAP 中选择了 AP。

节点间的数据使用的是最终一致性,eureka 会将注册的信息同步到 peer 节点,但是 peer 节点不会二次传播。
peer节点需要显示地在配置中设置。如果 peer 节点配置的不全,那么集群的概念也不存在了,节点之间的关系是通过 peer 节点的显示配置来维护的。

  • EDAS 服务注册发现组件
    EDAS 服务注册中心的多个 server,存在主从,各节点之间使用 raft 协议保证一致性。

server 之间的互相感知是通过访问 http://jmenv.tbsite.net/vipserver/serverlist 来获取其他 peer 节点地址来实现的。
然后通过自定义的端口和协议来进行选举和数据同步等操作。CAP 中选择的是 CP。

客户端高可用

  • eureka
    通过本地缓存来实现,当 server 连接不上时,直接使用本地缓存。每 30s 异步更新一次缓存,避免了每次请求都强依赖于服务注册中心。
  • EDAS 服务注册发现组件
    通过本地缓存来实现,当 server 连接不上时,直接使用本地缓存。异步更新缓存,避免了每次请求都强依赖于服务注册中心。同时,还提供了通过 UDP 主动 push 的方式在新服务节点加入时及时通知。

安全的实现

EDAS 服务注册发现组件,结合 EDAS 已有的安全功能,在每次注册、心跳和查询请求中都添加了验签鉴权的操作,保护了服务的安全性。

本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com;如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:yqgroup@service.aliyun.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

用云栖社区APP,舒服~

【云栖快讯】中办国办印发《推进互联网协议第六版(IPv6)规模部署行动计划》加快推进基于 IPv6 的下一代互联网规模部署,计划指出2025年末中国 IPv6 规模要达到世界第一,阿里云也第一时间宣布了将全面提供IPv6服务,那么在全面部署 IPV6 前,你需要了解都在这儿  详情请点击

网友评论

1F
银时

有概念有方法,还附有原理介绍,全面的指导!

2F
1340316491509409

spring-cloud-starter-vipclient这个包如何引入