假设有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服务信息没有及时更新,可能发生调用错误,但是调用错误总比无法连接到服务注册中心要强。否则,服务注册中心就成为整个系统的单点故障,存在极大的单点风险,这是我们为什么需要分布式系统的首要原因。
让我们使用Netflix Eureka创建一个Service Registry,它只是一个带有Eureka Server启动器的SpringBoot应用程序。
使用Intellij的Idea开发工具是非常容易启动Spring cloud的:
可以从https://start.spring.io/网址,选择相应组件即可。
由于我们需要建立一个注册服务器,因此选择Eureka Server组件即可,通过这些自动工具实际上是能自动生成Maven的配置:
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
我们需要给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则引入包依赖是:
org.springframework.cloud
ring-cloud-starter-netflix-eureka-client
这样,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包:
org.springframework.boot spring-boot-starter-web
然后在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服务器的地址
下面我们准备访问这个服务生产者PengProducerService的消费者服务:
上个章节我们已经启动了两个服务生产者实例,如何通过负载平衡从两个中选择一个访问呢?这时就需要Ribbon,为了使用Ribbon,我们需要使用@LoadBalanced元注解,那么这个注解放在哪里呢?一般有两个DiscoveryClient 和 RestTemplate,这两个的区别是:
1. DiscoveryClient可以获得服务提供者(生产者)的多个实例集合,能让你手工决定选择哪个实例,这里负载平衡的策略比如round robin轮询就不会派上,实际就没有使用Ribbon:
List
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() { ResponseEntityresult = 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; }
上篇是使用Ribbon实现对多个服务生产者实例使用负载平衡的方式进行消费,在调用服务生产者时,返回的是字符串类型,如果返回是各种自己定义的对象,这些对象传递到消费端是通过JSON方式,那么我们的消费者需要使用Feign来访问各种Json对象。
需要注意的是:Feign = Eureka +Ribbon + RestTemplate,也就是说,使用Feign访问服务生产者,无需前面章节那么关于负载平衡的代码了,前面我们使用RestTemplate进行负载平衡访问,代码还是挺复杂
现在我们开始Feign的实现:首先我们在服务的生产者那边进行修改,让我们生产者项目变得接近实战中项目,增加领域层、服务层和持久层。
假设新增Article领域模型对象,我们就需要仓储保存,这里我们使用Spring默认约定,使用JPA访问h2数据库,将Article通过JPA保存到h2数据库中:
要启用JPA和h2数据库,首先只要配置pom.xml:
org.springframework.boot spring-boot-starter-data-jpa com.h2database h2 runtime
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的CRUD实现就已经有了,不需要自己再编写任何SQL语句。这样我们编写一个Service就可以提供Article对象的CRUD方法,这里只编写插入和查询批量两个方法:
@Service public class ArticleService { @Autowired ArticleRep articleRep; public ListgetAllArticles(){ 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 ListgetAllArticles(){ 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") ListgetAllArticles(); }
这是我们消费者的服务,调用生产者 /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发现的一个问题。
org.springframework.cloud spring-cloud-starter-netflix-eureka-server
通过这个项目学习,我们如同蚕丝剥茧层层搞清楚了Spring Cloud的微服务之间同步调用方式,发现基于REST/JSON的调用代码最少,也是最方便,Feign封装了Ribbon负载平衡和Eureka服务器访问以及REST格式处理。