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 已有的安全功能,在每次注册、心跳和查询请求中都添加了验签鉴权的操作,保护了服务的安全性。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值