上一篇博文中,我们介绍了如何搭建一个Eureka服务的架构,但是服务提供者我们只用了一个单例,完全不能体现高并发高可用。本文我们尝试在上一篇文章示例Eureka项目的基础上继续完善,让它可以做到一个集群的部署。
Eureka集群架构
我们先看一下我们这次示例打算改造成的架构图:
在我们的Eureka服务器里面会启动两个实例,这两个实例会相互注册。
然后服务提供者也会启动两个实例,这两个实例都会注册到我们服务器的两个实例,是的,像图中那样一个服务提供者实例分别向两个服务器实例注册。
服务调用者也会注册到两个服务器实例上面。
最后我们会编写一个Rest客户端,去调用服务调用者的站点端口,来测试服务调用的过程。
我们还用上文的比方继续比喻,Eureka服务器相当于114电话查询平台,而两个Eureka服务器实例相当于两个114接线员。某某酒店现在要提供电话服务,于是在114注册,它就是服务提供者。由于业务扩大,十分繁忙,我们需要提供接听电话的效率和及时性,类似于服务提供者需要高并发高可用,于是酒店也弄了两个接线员,相当于两个服务提供者实例。而一个客户想请求酒店服务,首先回去114查询这家酒店的电话,但是114是哪位接线员提供的服务信息和电话号码,我们不关心;同样,客户拿到酒店电话后,自行打电话给酒店,至于酒店是哪位客服接电话的,客户也不关心,反正联系到酒店了。
Eureka集群项目配置
模拟准备
由于我们是在本地电脑上模拟有这么个集群,所以我们需要在host文件里做个配置:
我们打开C:\Windows\System32\drivers\etc\host,加入下面这行,假装我们有两个节点机器slave1和slave2。无论我们等会儿访问slave1或是slave2,其实访问的都是我本地的IP地址。
127.0.0.1 slave1 slave2
(本文出自oschina博主happyBKs的博文:https://my.oschina.net/happyBKs/blog/1631960)
Eureka服务器的集群配置
今天的架构中,需要两个Eureka实例,并且这两个Eureka服务器之间还相互注册了信息。所以我们需要修改一下上文中的Eureka服务器应用项目eurekaServer。
我们将eurekaServer项目的配置文件改成如下,注意我们配置了两个profile,当中用---分隔开,我们在启动springboot的应用时只要将对应的profile名称作为参数赋值给springboot程序就可以了。
之前的application.yml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
现在我们改成:
配置了两个eureka,计划通过profile来标识它们,然后以profile作为启动参数来启动不同配置的eureka实例应用。这两个eureka分别向对方注册自己的信息。注意,我这里设置了不同的端口号。因为虽然看似它们的IP域名为slave1和slave2,其实是我本地模拟只有一台PC,没办法修改host文件假装是在两个IP的机器上,其实还是在一台机器上,所以端口号必须不同。真实生产有两台服务器,端口号不妨一样都设置为8761.
server:
port: 8761
spring:
profiles: eureka1
eureka:
client:
service-url:
defaultZone: http://slave2:8762/eureka
---
server:
port: 8762
spring:
profiles: eureka2
eureka:
client:
service-url:
defaultZone: http://slave1:8761/eureka
注意,可以配置一个应用名称,通过配置项spring.application.name
启动类我们做个改动,我们希望输入一个profile参数,以按照配置中不同的profile来分别启动一个eureka实例。
package com.happybks.demo;
import java.util.Scanner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
final String profile = sc.nextLine();
new SpringApplicationBuilder(EurekaServerApplication.class).profiles(profile).run(args);
sc.close();
}
}
服务提供者客户端应用配置
application.yml配置文件
spring:
application:
name: first-service-app
eureka:
client:
service-url:
defaultZone: http://slave1:8761/eureka/,http://slave2:8761/eureka/
服务请求者first-service-app需要同时向两个eureka服务器发出注册信息,所以这里配置了两个eureka的地址信息,并且用逗号隔离开。
然后我们改写一下上一篇例子中的控制器和bean。
PetBean增加一个info字段。
package com.happybks.beans;
public class PetBean {
private String breedType;
private int age;
private double price;
private String info;
public String getBreedType() {
return breedType;
}
public void setBreedType(String breedType) {
this.breedType = breedType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
public PetBean() {
}
@Override
public String toString() {
return "PetBean [breedType=" + breedType + ", age=" + age + ", price=" + price + ", info=" + info + "]";
}
}
控制器的接口方法增加一个servlet的request参数。(如有其它参数,可以继续追加在一起不要紧)通过reuqest获取请求的url地址,就可以知道别人发给自己的请求时的url地址,也就知道了自己的IP服务地址。这个信息我们存入petBean的info字段返回json中给前端,我们计划看看在一个eureka集群中,一会儿服务调用者向同一个服务的发送请求时,该服务的两个服务提供者实例谁来应答。
package com.happybks.controllers;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.happybks.beans.PetBean;
@RestController
public class PetInfoController {
@GetMapping(value="/petinfo/findpet",produces=MediaType.APPLICATION_JSON_VALUE)
public PetBean findPet(HttpServletRequest request) {
PetBean petBean = new PetBean();
petBean.setBreedType("聪明机灵小柴犬");
petBean.setAge(1);
petBean.setPrice(50000);
petBean.setInfo(request.getRequestURL().toString());
return petBean;
}
}
启动类:
这里我们秀另一种区别参数,可以借助于赋予不同的端口,而不是profile来启动多个实例。
package com.happybks.providers;
import java.util.Scanner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@EnableEurekaClient
@ComponentScan(basePackages = { "com.happybks.controllers" })
public class ServiceProvider1Application {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
String port=sc.nextLine();
new SpringApplicationBuilder(ServiceProvider1Application.class).properties("server.port="+port).run(args);
sc.close();
}
}
配置服务调用者
这里我们沿用上次的控制器,一会儿我们请求这个控制器的方法url,它会向服务提供者发出一个请求。
package com.happybks.controllers;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Configuration
public class ShopperBehaviorController {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@GetMapping("/shopper/routine")
public String routine() {
RestTemplate restTemplate = getRestTemplate();
String json = restTemplate.getForObject("http://first-service-app/petinfo/findpet", String.class);
return json;
}
@GetMapping("/shopper/getservice/{id}")
public String getservice(@PathVariable("id") Integer id, HttpServletRequest request) {
RestTemplate restTemplate = getRestTemplate();
String json = restTemplate.getForObject("http://first-service-app/petinfo/findpet", String.class);
System.out.println(request.getRequestURL().toString());
return json;
}
}
启动类:
package com.happybks.callers;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = { "com.happybks.controllers" })
@EnableEurekaClient
public class ServiceCaller1Application {
public static void main(String[] args) {
SpringApplication.run(ServiceCaller1Application.class, args);
}
}
关于项目的其他代码和配置,请参见上一篇博客,我本文就不全部粘贴出来了。
启动集群
下面,我们来启动各个服务:
启动两个相互注册的EUREKA服务器实例
我们先启动profile为eureka1的服务器应用,控制台输入eureka1之后,eureka服务启动,启动好后,eureka1会按照配置主动向另一个eureka示例eureka2注册,但是此时eureka2的服务还没有启动,所以会报一个无法连接的错误。
这个不要紧。我们访问eureka1的服务查看主页:http://slave1:8761/
可以看到我们配置的它会向slave2注册信息,等同于一个服务副本,所以看到DS Replicaas有一个slave2.
但是因为eureka2还没有起来,所以下面的实例列表里面什么都没有。
之后,我们启动eureka2的服务。
我们会发现此次eureka2启动没有报错
我们此时再看http://slave1:8761/,发现eureka1等到eureka2也启动之后,完成既定配置的注册。
细心的你也许发现主页上的实例的application名称都是UNKOWN,这是因为我们没有再application.yml文件中配置spring.application.name的缘故,如果你配置了,那么它会作为实例的Application名称显示在实例列表里。
启动一个服务的提供者的两个实例
下面我们启动客户端服务提供者一个FIRST-SERVICE-APP,端口输入为8101。
我们刷新两个eureka的主页:http://slave1:8761/
之后我们再启动一个FIRST-SERVICE-APP,端口8102
再次刷新两个eureka服务器实例的主页
还有
我们发现,与刚才相比,FIRST-SERVICE-APP的那一行的Status字段多了一个IP地址,一看端口正是8102
启动一个实现了负载均衡请求的服务请求者
然后我们启动一个客户端服务请求者应用,请求其对应的url,其controller做的是向服务提供者FIRST-SERVICE-APP请求http://first-service-app/petinfo/findpet
这个我们着重看info这个属性字段,我们填入的是:服务提供者FIRST-SERVICE-APP的控制器处理接收到的请求时把请求的url保留,填入info字段。从这个字段可以看出这个FIRST-SERVICE-APP服务提供者是那台实例的IP。这个IP来自于服务请求者客户端的直接请求,也就是说服务请求者自己在获取了FIRST-SERVICE-APP的服务列表信息之后,得知了FIRST-SERVICE-APP有两台服务器,它自己借助于配置的Ribbon负载均衡框架自己选了请求那台FIRST-SERVICE-APP服务器。
当我们多次请求,发现其请求的服务器是变化的,实现了负载均衡。