SpringCloud服务发现注册Eureka +Ribbon + Feign教程

  在本教程将学习以下知识点:

  • 什么是服务注册和发现?
  • 基于Eureka的注册服务器
  • 服务生产者
  • 结合Ribbon服务消费者
  • 结合Feign的服务生产者和消费者

什么是服务注册和发现

   假设有2个微服务A和B分别在端点http:// localhost:8181 /和http:// localhost:8282 /上运行,如果想要在A服务中调用B服务,那么我们需要在A服务中键入B服务的url,这个url是负载均衡器分配给我们的,包括负载平衡后的IP地址,那么很显然,B服务与这个URL硬编码耦合在一起了,如果我们使用了服务自动注册机制,就可以使用B服务的逻辑ID,而不是使用特定IP地址和端口号来调用服务。

   我们可以使用Netflix Eureka Server创建Service Registry服务器,并将我们的微服务同时作为Eureka客户端,这样一旦我们启动微服务,它将自动使用逻辑服务ID向Eureka Server注册。然后,其他微服务(同样也是Eureka客户端)就可以使用服逻辑务ID来调用REST端点服务了。

   Spring Cloud使用Load Balanced RestTemplate创建Service Registry并发现其他服务变得非常容易。

   除了使用Netflix Eureka Server作为服务发现,也可以使用Zookeeper,但是根据CAP定理,在需要P网络分区容忍性情况下,强一致性C和高可用性A只能选择一个,Zookeeper是属于CP,而Eureka是属于AP,在服务发现方面,高可用性才是更重要,否则无法完成服务之间调用,而服务信息是否一致则不是最重要,A服务发现B服务时,B服务信息没有及时更新,可能发生调用错误,但是调用错误总比无法连接到服务注册中心要强。否则,服务注册中心就成为整个系统的单点故障,存在极大的单点风险,这是我们为什么需要分布式系统的首要原因。

基于Eureka的注册服务器

  让我们使用Netflix Eureka创建一个Service Registry,它只是一个带有Eureka Server启动器的SpringBoot应用程序。

  使用Intellij的Idea开发工具是非常容易启动Spring cloud的:

可以从https://start.spring.io/网址,选择相应组件即可。

由于我们需要建立一个注册服务器,因此选择Eureka Server组件即可,通过这些自动工具实际上是能自动生成Maven的配置:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

我们需要给SpringBoot启动类添加@EnableEurekaServer注释,以使我们的SpringBoot应用程序成为基于Eureka Server的Service Registry。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
 
@EnableEurekaServer
@SpringBootApplication
public class ServiceRegistryApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(ServiceRegistryApplication.class, args);
    }
}

默认情况下,每个Eureka服务器也是Eureka客户端,客户端一定会需要一个服务器URL来定位,否则就会不断报错,由于我们只有一个Eureka Server节点(独立模式),我们将通过在application.properties文件中配置以下属性来禁用此客户端行为。

SpringCloud有properties和YAML两种配置方式,这两种配置方式其实只是形式不同,properties配置信息格式是a.b.c,而YAML则是a:b:c:,两者本质是一样的,只需要其中一个即可,这里以properties为案例:

spring.application.name=jdon-eureka-server
server.port=1111
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

现在运行ServiceRegistryApplication并访问http:// localhost:1111,如果不能访问,说明没有正常启动,请检查三个环节:pom.xml是否配置正确?需要Eureka和配置

SpringBoot的注释@EnableEurekaServer是否增加了?

最后,application.properties是否配置?

SpringCloud其实非常简单,约定大于配置,默认只要配置服务器端口就可以了,然后是一条注释@EnableEurekaServer,就能启动Eurek服务器了。

服务器准备好后,我们就要准备服务生产者,向服务器里面注册自己,服务消费者则是从服务器中发现注册的服务然后调用。

 

服务生产者

   服务生产者其实首先是Eureka的客户端,生产者将自己注册到前面启动的服务器当中,引如果是idea的导航,选择CloudDiscovery的EurekaDiscovery,如果是 Maven则引入包依赖是:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

这样,spring-cloud-starter-netflix-eureka-client这个jar包就放入我们系统的classpath,为了能够正常使用这个jar包,还需要配置,只需要在application.properties中配置eureka.client.service-url.defaultZone属性即可自动注册Eureka Server:

eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

当我们的服务在Eureka Server注册时,它会持续发送一定时间间隔的心跳。如果Eureka服务器没有从任何服务的实例接收到心跳,它将认为这个服务实例已经关闭并从自己的池中剔除它。

以上是服务生产者注册服务的过程,比较简单,为了使我们的服务生产者能的演示代码够运行起来,我们还需要新建一个服务生产者代码:

@RestController
public class ProducerService {

@GetMapping("/pengproducer")
public String sayHello(){
return "hello world";
}
}

这段代码是将服务暴露成RESTful接口,@RestController是声明Rest接口,/pengproducer是REST的访问url,通过get方式能够获得字符串:hello world

因为REST属于WEB的一种接口,因此需要在pom.xml中引入Web包:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后在application.properties中加入有关REST接口的配置:

spring.application.name=PengProducerService
server.port=2111

指定我们的生产者服务的名称是PengProducerService,REST端口开在2111。

现在可以在idea中启动我们的应用了,这样我们启动这个项目,就可以在http://127.0.0.1:2111/ 访问这个REST服务。同时,因为我们之前已经启动了注册服务器,访问http://localhost:1111/你会发现PengProducerService出现在服务列表中:

上面启动应用服务是在idea编辑器中,我们还可以通过命令行启动我们的服务生产者:

java -jar -Dserver.port=2112 producer-0.0.1-SNAPSHOT.jar

这个是在端口2112开启我们的服务端点了。现在再问http://localhost:1111/,你会看到可用节点Availability Zones下面已经从(1)变为(2),现在我们的服务生产者已经有两个实例在运行,当服务的消费者访问这个两个实例时,它可以根据负载平衡策略比如轮询访问其中一个服务生产者实例。

总结一下,为了让服务生产者注册到Euraka服务器中,只需要两个步骤:

1. 引入spring-cloud-starter-netflix-eureka-client包

2. 配置Eurake服务器的地址

请注意,spring-cloud-starter-netflix-eureka-client包是Spring Cloud升级后最新的包名,原来是spring-cloud-starter-eureka,里面没有netflix,这是过去版本,Spring Boot 1.5以后都是加入了netflix的,见Spring Cloud Edgware Release Notes

另外,这里不需要在SpringBoot主代码中再加入@enablediscoveryclient 或 @enableeurekaclient,只要eureka的client包在maven中配置,也就会出现在系统的classpath中,这样就会默认自动注册到eureka服务器中了。

这部分源码下载:百度网盘

下面我们准备访问这个服务生产者PengProducerService的消费者服务:

结合Ribbon的服务消费者

   上个章节我们已经启动了两个服务生产者实例,如何通过负载平衡从两个中选择一个访问呢?这时就需要Ribbon,为了使用Ribbon,我们需要使用@LoadBalanced元注解,那么这个注解放在哪里呢?一般有两个DiscoveryClient 和 RestTemplate,这两个的区别是:

1. DiscoveryClient可以获得服务提供者(生产者)的多个实例集合,能让你手工决定选择哪个实例,这里负载平衡的策略比如round robin轮询就不会派上,实际就没有使用Ribbon:

List<ServiceInstance> instances=discoveryClient.getInstances("PengProducerService");
ServiceInstance serviceInstance=instances.get(0);

2.RestTemplate则是使用Ribbon的负载平衡策略,使用@LoadBalanced注释resttemplate并使用zuul代理服务器作为边缘服务器。那么对zuul边缘服务器的任何请求将默认使用Ribbon进行负载平衡,而resttemplate将以循环方式路由请求。这部分代码如下:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerService {

@Autowired
private RestTemplate restTemplate;

public String callProducer() {
ResponseEntity<String> result =
this.restTemplate.getForEntity(
"http://PengProducerService/pengproducer",
String.class,
"");
if (result.getStatusCode() == HttpStatus.OK) {
System.out.printf(result.getBody() + " called in callProducer");
return result.getBody();
} else {
System.out.printf(" is it empty");
return " empty ";
}
}
}

RestTemplate是自动注射进这个控制器,在这控制器,我们调用了服务生产者http://PengProducerService/pengproducer,然后获得其结构。

这个控制器的调用我们可以在SpringBoot启动函数里调用:


@SpringBootApplication
public class ConsumerApplication {

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(ConsumerApplication
.class, args);
ConsumerService consumerService = ctx.getBean(ConsumerService.class);
System.out.printf("final result RestTemplate=" + consumerService
.callProducer() + " \n");
}
}

注意到@LoadBalanced是标注在RestTemplate上,而RestTemplate是被注入到ConsumerService中的,这样通过调用RestTemplate对象实际就是获得负载平衡后的服务实例。这个可以通过我们的服务提供者里面输出hashcode来分辨出来,启动两个服务提供者实例,每次运行ConsumerService,应该是依次打印出不同的hashcode:

hello world1246528978 called in callProducerfinal result RestTemplate=hello world1246528978

再次运行结果:

hello world1179769159 called in callProducerfinal result RestTemplate=hello world1179769159

hellow world后面的哈希值不同,可见是来自不同的服务提供者实例。

如果系统基于https进行负载平衡,那么只需要两个步骤:

1.application.properties中激活ribbon的https:

ribbon.IsSecure=true

2.代码中RestTemplate初始化时传入ClientHttpRequestFactory对象:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
HttpComponentsClientHttpRequestFactory clientrequestFactory = new HttpComponentsClientHttpRequestFactory();
clientrequestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(clientrequestFactory);
return restTemplate;
}

 

这部分源码下载:百度网盘

 

结合Feign的服务生产者和消费者

   上篇是使用Ribbon实现对多个服务生产者实例使用负载平衡的方式进行消费,在调用服务生产者时,返回的是字符串类型,如果返回是各种自己定义的对象,这些对象传递到消费端是通过JSON方式,那么我们的消费者需要使用Feign来访问各种Json对象。

   需要注意的是:Feign = Eureka +Ribbon + RestTemplate,也就是说,使用Feign访问服务生产者,无需前面章节那么关于负载平衡的代码了,前面我们使用RestTemplate进行负载平衡访问,代码还是挺复杂  

   现在我们开始Feign的实现:首先我们在服务的生产者那边进行修改,让我们生产者项目变得接近实战中项目,增加领域层、服务层和持久层。

   假设新增Article领域模型对象,我们就需要仓储保存,这里我们使用Spring默认约定,使用JPA访问h2数据库,将Article通过JPA保存到h2数据库中:

要启用JPA和h2数据库,首先只要配置pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

Article领域模型对象作为需要持久的实体对象:配置实体@Entity和@Id主键即可:


@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String body;
private Date startDate;

然后我们建立一个空的Article仓储接口即可:


@Repository
public interface ArticleRep extends JpaRepository<Article,Long> {
}

这样,关于Article的CRUD实现就已经有了,不需要自己再编写任何SQL语句。这样我们编写一个Service就可以提供Article对象的CRUD方法,这里只编写插入和查询批量两个方法:


@Service
public class ArticleService {

@Autowired
ArticleRep articleRep;

public List<Article> getAllArticles(){
return articleRep.findAll();
}

public void insertArticle(Article article){
articleRep.save(article);
}
}

我们在REST接口中暴露这两种方法:

1. get /articles是查询所有对象

2. post /article是新增


@RestController
public class ProducerService {

@Autowired
ArticleService articleService;

@GetMapping("/articles")
public List<Article> getAllArticles(){
return articleService.getAllArticles();
}

@GetMapping("/article")
public void publishArticle(@RequestBody Article article){
articleService.insertArticle(article);
}

 

上面服务的生产者提供了两个REST url,我们在消费者这边使用/articles以获得所有文章:


@FeignClient(name="PengProducerService")
public interface ConsumerService {

@GetMapping("/articles")
List<Article> getAllArticles();
}

这是我们消费者的服务,调用生产者 /articles,这是一个接口,无需实现,注意需要标注FeignClient,其中写入name或value微服务生产者的application.properties配置:

spring.application.name=PengProducerService

当然,这里会直接耦合PengProducerService这个名称,我们以后可以通过配置服务器更改,这是后话。

然后需要在应用Application代码加入@EnableFeignClients:

@SpringBootApplication
@EnableFeignClients
public class FeignconsumerApplication {

public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(FeignconsumerApplication
.class, args);
ConsumerService consumerService = context.getBean(ConsumerService
.class);
System.out.printf("#############all articles ok" + consumerService
.getAllArticles());
}

在FeignconsumerApplication我们调用了前面接口ConsumerService,而ConsumerService则通过负载平衡调用另外一个生产者微服务,如果我们给那个生产者服务加入一些Articles数据,则这里就能返回这些数据:

#############all articles ok[com.example.feignconsumer.domain.Article@62b475e2, com.example.feignconsumer.domain.Article@e9474f]

说明调用成功。

在调试过程中,曾经出现错误:

Load balancer does not have available server for client:PengProducerService

经常排查是由于生产者项目中pom.xml导入的是spring-cloud-starter-netflix-eureka-client,改为pring-cloud-starter-netflix-eureka-server就可以了,这是SpringBoot 2.0发现的一个问题。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

本章的代码下载:百度网盘

总结

   通过这个项目学习,我们如同蚕丝剥茧层层搞清楚了Spring Cloud的微服务之间同步调用方式,发现基于REST/JSON的调用代码最少,也是最方便,Feign封装了Ribbon负载平衡和Eureka服务器访问以及REST格式处理。

 

 

Spring Cloud专题