旁白
很久没有写技术文章了,最近不是写水文就是写小说。说到底,还是最近很少研究技术的缘故,已经到了江郎才尽的地步了。
不过,LZ无意间看到自己团队的小伙伴写的一些文章,觉得还是不错的,于是便动了心思,准备把这些文章拿来,也算填补一下最近技术文章缺乏的空白。
而这些文章,不光涉及到一些技术干货,也算是变相的给自己团队的产品做了宣传,这也算是一石俩鸟了吧。
引言
好了,接下来咱们进入正题。可以看到,本篇文章的标题里有三个关键字,SpringCloud、EDAS以及服务发现。关于SpringCloud和EDAS咱们接下来再说,LZ想首先简单介绍一下“服务发现”这个概念。
说到服务发现,就不得不提到很火的一个概念“微服务”,究竟什么是“微服务”?
微服务是一个新兴的软件架构,就是把一个大型的单个应用程序和服务拆分为数十个的支持微服务。一个微服务的策略可以让工作变得更为简便,它可扩展单个组件而不是整个的应用程序堆栈,从而满足服务等级协议。
以上是从百度上抄来的解释,从字面上粗暴的理解,微服务其实就是把一个大的应用拆分成很多的小应用,而这每一个所谓的小应用,就是所谓的微服务了。
拆分成一个个微服务以后,可以得到不小的好处,最显而易见的就是,可以节省掉很多计算资源,因为你可以针对其中某一个模块进行扩缩容,而不再是只能对整个应用。
举个例子,就拿淘宝来说,双11大促的时候,每个模块的压力都是不同的,比如订单系统、商品搜索、评价系统等等,其中显而易见的是,订单系统这部分的压力肯定很大,但是评价系统的压力就不会那么大。
所以,大促期间,就可以把更多的计算资源往订单系统倾斜,而如果这些还都是集中在一个应用中的话,那就无法做到这样把资源充分利用了。
不过,微服务虽然有诸多好处,但也会同样引入一些问题。
最典型的问题,就是所谓的“服务发现”,那究竟什么叫服务发现?
简单地说,一个应用改成微服务以后,大家都在这个池子里,谁发布了什么服务,这个事情是需要让池子里的各个微服务知道的,否则的话,服务之间调用就不知道该找谁了。
所以,一个服务发布了以后,怎么“发现”它,就成了微服务体系中一个重要的事情。
那么怎么解决这个事情,思路也很简单,拿淘宝来举例,我现在要买一个娃娃,怎么才能买到呢?
首先第一件事是,卖娃娃的商家要把自己的店开到淘宝上,然后我去淘宝上搜索“娃娃”这个关键字,然后淘宝就告诉我一堆卖娃娃的商家,我再从这些商家里选出来一家,最终就可以买到我想要的娃娃了。
那么结合以上买娃娃的过程,放到微服务这个“市场”里,也是类似的。首先得现有一个淘宝这样的平台,一般我们叫它“服务注册中心”,然后,每一个微服务模块发布了一个服务的时候,都需要到这个“服务注册中心”去注册一下,类似于卖家去淘宝上开店的操作。
接下来,调用者一方在调用的时候,就先去“服务注册中心”查询一下,这个就相当于去淘宝上搜索“娃娃”的操作,最终,调用者从“服务注册中心”返回的服务提供者列表里选取一个,最终调用成功。
当然了,在调用者选取服务提供者的时候,“服务注册中心”又或者是调用者自己,也可以有一定的排序算法,比如同机房的优先调用,或者压力小的优先调用等等。
这其实在淘宝那个例子中也同样适用,比如你搜索“娃娃”,淘宝也会给你按销量或者是按评价,对卖娃娃的商家进行综合排序,以便给你筛选出最适合你的娃娃。
SpringCloud的服务发现
好了,LZ先简单介绍了一下微服务和服务发现的概念,那么接下来,就来谈谈SpringCloud吧。
SpringCloud是什么玩意呢?
Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems.
上面这一段英文,是SpringCloud官网的解释,翻译过来的意思就是SpringCloud提供了一堆工具给开发者,让开发者可以很快的,构建一套通用模式的分布式系统。
而这些SpringCloud提供的工具中,其中有一个,就是“服务发现”。
在SpringCloud的支持体系中,有Eureka、Consul、Zookeeper等几种服务发现的支持,但是目前使用最广的,无疑就是Eureka了。接下来咱们就通过一个demo,来看看Eureka怎么使用的。
1. 启动一个服务注册中心(相当于上面例子中的淘宝网站)
创建一个基础的 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 查看eureka server的详情。
当然,页面打开成功,只是表明服务已经启动,目前 instances 为空,表明还没有服务注册上来。
2.创建服务提供者(相当于上面例子中的卖家)
创建一个 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 页面查看服务是否已经注册成功,可以看到 instances 中已经存在的实例 service-provider,端口是 18081。
3.创建服务消费者(相当于上面例子中的买家)
这个例子中,我们将不仅仅是演示服务发现的功能,同时还将演示 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/
启动服务,分别进行调用,可以看到调用都成功了。不过需要注意,AsyncRestTemplate 接入服务发现的时间比较晚,需要在 Dalston 之后的版本才能使用,具体详情参见此 pull request
Eureka 的烦恼
前面的例子在本机工作起来是很方便的,但是很遗憾,这只是一个 demo ,实际部署中我们可能都踩过坑或者有这么一些不爽。
- 只有一个服务注册中心,显然这不符合高可用的原则,高可用就得增加
eureka server 的数量,维护成本太高了。 - 实际生产中,不会将服务注册中心与业务服务部署在同一台机器上。实际部署中,当 eureka server 的地址发生变化时,还得修改配置文件里 eureka server的地址,太麻烦了。
- 实际使用中,服务注册发现中心的安全性也是需要考虑的,应该对服务注册和发现的请求进行鉴权,来确保服务的安全性,安全也是急需解决的问题。
- eureka 使用过程中,有可能出现注册上去的服务地址不是一个 ip ,而是一个 hostname 的情况,事实上又无法通过 hostname 进行服务调用。其实只是因为没有增加 eureka.instance.prefer-ip-address=true这个配置,依旧需要添加配置。
- eureka 因为缓存设计的原因,使得服务注册上去之后,最迟需要两分钟后才能发现。
或许你希望有人提供一个安全、稳定、高可用、高性能、简单易用的服务注册中心。它的配置更加简单,而且又与原来写好的eureka代码完全兼容。
这个怎么办呢?
重点来了,LZ要插广告了,注意!
EDAS就是你理想中的选择,你只需要简单修改几行代码,即可得到以下好处。
- 稳定高可用的服务注册中心
- 安全的服务注册、服务发现
- 秒级的服务发现机制
- 无需再关心服务注册中心的地址
EDAS 服务注册中心
1.安装轻量版配置中心(相当于Eureka的服务注册中心)
这个安装过程很简单,这里就不再赘述了,详情可以参考 轻量级配置中心 。唯一需要注意的是,最后在应用启动时,需要配置一个 JVM 参数,配置如下。
//windows中修改startup.bat如下这一行 %_EXECJAVA% -Daddress.server.ip=%SERVER_IP% -Dvipserver.server.port=8080 -jar edas-config-center.jar //linux中修改startup.sh如下这一行 nohup $JAVA -Daddress.server.ip=$SERVER_IP -Dvipserver.server.port=8080 -jar edas-config-center.jar >/dev/null 2>&1 &
然后按照文档所描述,windows下直接执行startup.bat,linux下执行startup.sh即可。
最终,你也会看到一个和Eureka类似的页面,当然,现在这个页面当中是没有服务的。
另外,EDAS的轻量版配置中心启动以后,不光内嵌了一个简单的开发版服务注册中心,还包含了一个简单的分布式配置服务,以下这个页面可以进行配置的维护操作。
这个分布式配置服务相当于SpringCloud体系中的Spring Cloud Config,不过这个和本文主线没有什么关系,这里就暂且不详谈了。
2.服务提供者和消费者代码修改
关于Java源码的修改,只有两行,需要在 main 函数中添加两行,修改之后的 service-provider 的 main 函数如下。
public static void main(String[] args) { PandoraBootstrap.run(args);//引入Pandora SpringApplication.run(ServerApplication.class, args); PandoraBootstrap.markStartupAndWait();//引入Pandora }
pom.xml 的修改有两点,一个是将原来的 eureka 的 starter 替换成 EDAS 服务注册中心的starter,并加入 pandora 的依赖。修改之后的service-provider 的pom文件如下:
<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>
最后一步,由于目前以上jar包尚未进入中央仓库,所以还需要配置下 maven 的私服地址,修改settings.xml配置如下。
<settings> <localRepository>/Users/../.m2/repository</localRepository> <profiles> <profile> <id>nexus</id> <repositories> <repository> <id>central</id> <url>http://repo1.maven.org/maven2</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <url>http://repo1.maven.org/maven2</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> <profile> <id>edas.oss.repo</id> <repositories> <repository> <id>edas-oss-central</id> <name>taobao mirror central</name> <url> http://edas-public.oss-cn-hangzhou.aliyuncs.com/repository </url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>edas-oss-plugin-central</id> <url> http://edas-public.oss-cn-hangzhou.aliyuncs.com/repository </url> <snapshots> <enabled>true</enabled> </snapshots> <releases> <enabled>true</enabled> </releases> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <activeProfile>nexus</activeProfile> <activeProfile>edas.oss.repo</activeProfile> </activeProfiles> </settings>
OK,到这里所有的都已经修改好了,service-consumer 的修改方式与 service-provider 的修改方式完全一样,这里就不再赘述了。
当你将service-provider和service-consumer启动以后,便可以在轻量版配置中心的页面当中看到对应的服务提供者和调用者。
当然,你也可以直接从官网上直接下载写好的demo。
EDAS轻量版配置中心原理简介
换了一个pom依赖,加了两行代码就把 Eureka 替换成了 EDAS 服务注册中心,虽然方便,但是这对于你来说也许相当于是一个黑盒,黑盒总是让人很没有安全感。下面LZ将从服务注册中心寻址、服务注册和下线、客户端结合、高可用和安全多个方面,来简单介绍一下。
服务注册中心寻址
既然不需要在配置文件里配置服务注册中心的地址了,那么客户端是如何找到服务中心的呢?
其实是通过一个http请求来实现的,地址为http://jmenv.tbsite.net/vipserver/serverlist。无论是服务端还是客户端,都会利用这个地址来进行服务发现,这个地址就相当于在Eureka中的http://localhost:8761/eureka这个地址的作用。
唯一的区别就是,当你使用EDAS时,这个配置是默认自带的,所以省去了这部分工作。
服务注册与下线
服务注册的通信协议是http协议,默认注册的应用名是 spring.application.name 的value值,如果有需要将某个应用发布成多个服务名的话,也可以在application.properties文件里添加以下配置,来把当前应用发布成多个服务名。
vipserver.register.doms=applicationServiceName1,applicationServiceName2
举个实例的场景,比如有一个应用,集成了订单、商品管理等功能,那可能你会希望给这个应用起两个服务名,比如order和product,这个时候,这个应用就可以独自提供两个服务给外部使用。
关于服务下线,其实是利用的典型的心跳机制,由服务提供者向服务注册中心发送心跳,当心跳超过一定时长不正常的情况下,服务注册中心便会将该服务提供者从列表中移出,如果此时没有其它服务提供者提供同样的服务,那么这个服务就会被下线掉。
EDAS服务发现客户端原理简介
上面更多的讲的是服务注册中心的原理解释,接下来咱们来看看,客户端这一块,EDAS是如何与SpringCloud做兼容的。
简单地说,EDAS其实对SpringCloud的三个http客户端做了兼容,分别是FeignClient、RestTemplate、AsyncRestTemplate,那么接下来,咱们就以RestTemplate、AsyncRestTemplate为例,简单了解下EDAS如何做的兼容。
对于这两个rest客户端的Bean来说,只要加上@LoadBalanced 注解,就可以接入EDAS服务发现。
具体是怎么实现的呢?
其实在SpringCloud当中,当给RestTemplate、AsyncRestTemplate添加了LoadBalanced注解以后,他们会被两个拦截器所拦截,即LoadBalancerInterceptor 和 AsyncLoadBalancerInterceptor。
而这两个拦截器其实是通过LoadBalancerClient这个对象是处理的,这个接口只有一个实现,叫做RibbonLoadBalancerClient。
我们分析一下这个类的代码,不难发现,这个类在做负载均衡的时候,是依靠一个叫做ILoadBalancer的接口来处理的,代码如下。
ILoadBalancer loadBalancer = getLoadBalancer(serviceId); Server server = getServer(loadBalancer);
这个接口有几个实现,DynamicServerListLoadBalancer、NoOpLoadBalancer、ZoneAwareLoadBalancer等。其中NoOpLoadBalancer这个实现只是一个空实现,而最主要的就是DynamicServerListLoadBalancer和ZoneAwareLoadBalancer。
而其中ZoneAwareLoadBalancer又是DynamicServerListLoadBalancer的子类,我们观察DynamicServerListLoadBalancer这个类的逻辑可以发现,它的服务者列表其实是由一个叫做ServerList的接口所实现的。
这个接口只有两个方法,一个是获取初始列表,一个是更新服务者列表。
public interface ServerList<T extends Server> { public List<T> getInitialListOfServers(); /** * Return updated list of servers. This is called say every 30 secs * (configurable) by the Loadbalancer‘s Ping cycle * */ public List<T> getUpdatedListOfServers(); }
而EDAS所做的,就是实现了一个自己的ServerList,名为VipserverList,并且抢先把这个Bean给注入了进去,就像下面这样。
@Bean @ConditionalOnMissingBean public ServerList<Server> ribbonServerList(IClientConfig config) { return new VipserverList(config.getClientName()); }
这个VipserverList,其实就是连接的轻量版配置中心,因此如此一来,客户端所获取的服务者列表将会由轻量版配置中心来维护,并根据负载均衡策略,提供给客户端选择。
EDAS服务发现高可用
服务端高可用:
- 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 服务注册发现组件,结合 EDAS 已有的安全功能,在每次注册、心跳和查询请求中都添加了验签鉴权的操作,确保了服务的安全性。
小结
好了,本文主要就是简单介绍下SpringCloud的服务发现,顺便和EDAS的服务发现简单对比下,还囫囵吞枣的介绍了下EDAS服务发现的部分实现原理。
当然,顺便......真的是顺便......再打上一波广告。
现在微服务的概念基本已经普及到了大大小小的公司,很多公司,特别是一些笨重的传统企业IT系统,都在往微服务的方向靠拢。
那么靠拢的过程中,肯定要选择一个合适的方案。
这其实就涉及到一个选择问题,到底是选择开源版产品,还是选择商业版产品?
从LZ个人的感受来说,如果是五年前的LZ,一定会选择开源,但现在的LZ,一定会选择商业。
因为使用开源的过程往往是踩坑的过程,而对于五年前的LZ来说,踩坑可以很快的提高LZ的技术。但五年过去了,如果现在再让LZ选择,LZ更加看重的是高效的解决问题,而踩坑的代价往往是很大的。
不管怎么说,这就是LZ的真实想法,这绝不是在诱导你掏腰包,相信LZ!
参考文章链接:https://yq.aliyun.com/articles/255160
EDAS产品主页:https://www.aliyun.com/product/edas