Springcloud基础及使用

spring cloud

本文档示例采用spring boot 的 1.5.9版本

spring cloud用于spring boot项目的分布式管理,它的部分功能同dubbo很类似,如eureka跟zookeeper功能类似,zuul跟nginx类似,同时它集成了hystrix断路器、spring cloud config统一配置等实用功能组件。

spring boot项目通信的直连方式:

服务之间互相调用可以用直连的方式,直连不需要依赖spring cloud,使用spring boot的依赖就够了。

provider:

没有特殊配置,直接通过controller提供http接口:

@RestController
@RequestMapping("user")
public class UserController {

@RequestMapping("list")
public String list() {
System.out.println("服务被调用");
return "user_list";
}
}

 

consumer:

注入RestTemplate,然后通过它调用远程接口:

@RequestMapping("test")
@RestController
public class TestController {

@Autowired
public RestTemplate restTemplate;

@RequestMapping("testConsumer")
public String testConsumer() {
String a = restTemplate.getForObject("http://localhost:90/user/list", String.class);
return a;
}
}

RestTemplate在主程序中通过@Bean标签配置:

@SpringBootApplication
public class ConsumerApplication {

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

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

测试访问:


				

 

使用Eureka:

Eureka是一个服务发现组件,类似于zookeeper用来做注册中心的功能。可以将服务注册到Eureka上。

服务注册表记录服务的ip、端口等信息。

服务注册到Eureka的服务注册表,每30秒发送一次心跳(续约),连续3次(90秒)没有接收到心跳,就会将服务从服务注册表移除掉。

客户端缓存提供者ip,即使Eureka挂掉,也可以继续调用服务。

Eureka通过心跳检测、健康检查、客户端缓存确保高可用性、灵活性、可伸缩性。

创建Eureka的server服务发现组件工程

Eureka服务发现组件作为一个spring boot工程创建。

1、pom.xml加入依赖


<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

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

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

<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>Edgware.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

它里面定了各种依赖的版本,使用了它之后,以后的eureka、ribbon、feign、hystrix等依赖加入时都不用再写版本号。

2、启动类:

EurekaserverApplication:

 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaserverApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaserverApplication.class, args);
}
}

3、启动启动类后浏览器访问:


				

上面列出了eureka服务启动时间,服务器内存、Cpu等信息,注册到上面的实例、状态等信息。

 

服务端注册到Eureka

pom.xml加入依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

 

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

</dependency>

启动类加注解:


@EnableDiscoveryClient

或者
@EnableEurekaClient


				

 

application.yml加入配置:

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/

启动服务,观察Eureka服务器:


				

可以看到服务列表多了一条记录。

 

设置服务名称

provider方的yml文件增加配置:

spring:
application:
name:
user-server

效果:


				

 

自定义状态status格式

最后一个参数可以通过instance_id自定义名字,如:


				

效果:


				

 

消费端注册到Eureka

pom.xml加入依赖

<dependencyManagement>

<dependencies>

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-dependencies</artifactId>

<version>Edgware.SR2</version>

<type>pom</type>

<scope>import</scope>

</dependency>

</dependencies>

</dependencyManagement>

 

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

</dependency>

启动类加注解:


@EnableDiscoveryClient

或者
@EnableEurekaClient


				

 

application.yml加入配置:

eureka:
client:
service-url:
#defaultZone: http://localhost:8761/eureka/
defaultZone: http://kkk:kkk123@localhost:8761/eureka/

spring:
application:
name:
consumer-test

 

启动服务,观察Eureka服务器:


				

eurekaserver服务器配置登录验证

配置登录账号密码,正确才能进入管理界面。

pom.xml引入依赖


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

 

yml文件增加配置:

security:
basic:
enabled:
true
user:
name:
kkk
password: kkk123

效果:


				

输入kkk    kkk123登录以后才能进服务管理界面。

 

 

provider端配置用户名密码注册服务

只需要通过curl的方式将用户名密码写入链接地址即可:

如:


defaultZone: http://kkk:kkk123@localhost:8761/eureka/

 


				

监控和管理生产环境

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

 

这个包提供了状态查看、健康检查等功能,可以用在provider、consumer、eurekaserver任何一个工程里。

服务状态查看

启动时可以看到很多url地址:


				

服务启动后可访问,如:


				

 

如果要看更多信息,则需要配置登录验证账号和密码,provider、consumer端配置方法跟eurekaserver端配置方法一样,见上面"eurekaserver服务器配置登录验证"。

 

也可以通过禁用验证查看更多信息,application.yml中加入配置:

management:
security:
enabled:
false

 

效果:


				

查询服务提供者节点信息

可以通过服务名查询节点

在provider中配置:



import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Created by Administrator on 2018/1/26.
*/
@RestController
@RequestMapping("user")
public class UserController {

@Autowired
private EurekaClient eurekaClient;

@Autowired
private DiscoveryClient discoveryClient;


@GetMapping("aaa")
public String serviceUrl() {
InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("user-server", false);
return instanceInfo.getHomePageUrl();
}

@GetMapping("bbb")
public ServiceInstance showInfo() {
ServiceInstance serviceInstance = discoveryClient.getLocalServiceInstance();
return serviceInstance;
}

@RequestMapping("list")
public User list() {
System.out.println("服务被调用");
User user = new User();
user.setName("kkk");
user.setAge(99L);
return user;
}
}

效果:


				

使用ribbon:

服务器端负载均衡和客户端负载均衡

服务器端负载均衡组件如nginx、tengine等,在服务器端做反向代理实现负载均衡。

客户端负载均衡是在客户端记录服务器节点列表,然后在客户端决定访问哪个节点实现负载均衡,如dubbo也是使用客户端负载均衡。ribbon就是一种客户端负载均衡组件。

 

基本使用(consumer端配置)

由于ribbon是客户端负载均衡组件,因此配置到了consumer端。

使用ribbon需要引入ribbon的依赖,由于cloud-starter已经引入了ribbon的依赖, 所以不需要再引入。

启动类加入@LoadBalanced注解:

ConsumerApplication.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {

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

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

加入了@LoadBalanced注解表示它使用ribbon

 

通过服务名调用服务:

TestController:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RequestMapping("test")
@RestController
public class TestController {

@Autowired
public RestTemplate restTemplate;

@RequestMapping("testConsumer")
public User testConsumer() {
// 通过服务名调用服务
User user = restTemplate.getForObject("http://user-server/user/list", User.class);
return user;
}
}

注意这里的user-server是服务名,没有区分大小写。

 

 

application.yml配置文件不变

spring:
application:
name:
consumer-test
server:
port:
91

eureka:
client:
service-url:
#defaultZone: http://localhost:8761/eureka/
defaultZone: http://kkk:kkk123@localhost:8761/eureka/

 

效果:


				

 

List参数的坑(只能接收数组类型,不能是List类型)

返回List时,不能用List接收,而要用数组,如:

服务端方法:


@RequestMapping("list")
public List<User> list() {
System.out.println("服务被调用");
User user1 = new User();
user1.setName("kkk");
user1.setAge(99L);
User user2 = new User();
user2.setName("kkk2");
user2.setAge(100L);

List<User> list = new ArrayList<>();
list.add(user1);
list.add(user2);
return list;
}

 

 

客户端方法(只能接收为数组):
@RequestMapping("testConsumer")
public List<User> testConsumer() {
// 通过服务名调用服务
User[] userArray = restTemplate.getForObject("http://user-server/user/list", User[].class);
List<User> users = Arrays.asList(userArray);
for (User user : users) {
System.out.println(user.getName());
}
return users;
}

 

如果写成:
@RequestMapping("testConsumer")
public List<User> testConsumer() {
// 通过服务名调用服务
List<User> users = restTemplate.getForObject("http://user-server/user/list", List.class);
for (User user : users) {
System.out.println(user.getName());
}
return users;
}

 

则访问会报错


				

负载均衡测试

ribbon的负载均衡默认采用轮询的方式。

复制一个provider工程为provider2,使用92端口,服务名也为user-server,实例id(instance-id)为user-server92。


				

启动后在eureka上可以看到。


				

 

然后使用客户端访问多次,效果:


				

2个服务节点被轮流调用。

 

 

代码自定义使用ribbon负载均衡规则

ribbon默认采用轮询方式使用服务节点,可以进行自定义配置。

配置
编写配置类TestConfiguration:

package com.example.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestConfiguration {

@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}

这里定义了一个随机的ribbonRule,它会随机访问服务节点。ribbonRule这个方法会被自动回调。

注意这个类不要放到启动类及其子包下,也就是不能让@ComponentScan扫描到它。否则会对所有服务都生效,而不只是对user-server生效了。

 

启动类增加@RibbonClient注解:

ConsumerApplication:

package com.example.demo;

import com.example.config.TestConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
@RibbonClient(name="user-server", configuration = TestConfiguration.class)
public class ConsumerApplication {

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

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}

 

其中:name="user-server", configuration = TestConfiguration.class

表示对某个服务采用哪种负载均衡规则。

说明:

没有配置Configuration时,spring cloud默认采用RibbonClientConfiguration
它也实现了ribbonRule方法。


				

默认使用的ZoneAvoidanceRule

 

自定义配置负载均衡规则测试
2个服务分别使用不同规则

新建user-server服务,提供者为2个节点user90、user92,同理新建shop-server服务,提供者为2个节点shop95、shop96。eureka上显示如下:


				

 

consumer端仅对user-server服务配置随机访问规则,而shop-server采用默认的轮询规则:


				

 


				

 

编写Controller的测试访问类:

TestController:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("test")
@RestController
public class TestController {

@Autowired
public LoadBalancerClient loadBalancerClient;

@GetMapping("testUserServer")
public String testUserServer() {
ServiceInstance userServiceInstance = loadBalancerClient.choose("user-server");
System.out.println(String.format("%s:%s:%s", userServiceInstance.getServiceId(), userServiceInstance.getHost(), userServiceInstance.getPort()));
return "finished";
}
@GetMapping("testShopServer")
public String testShopServer() {
ServiceInstance shopServiceInstance = loadBalancerClient.choose("shop-server");
System.out.println(String.format("%s:%s:%s", shopServiceInstance.getServiceId(), shopServiceInstance.getHost(), shopServiceInstance.getPort()));
return "finished";
}
}

 

其中loadBalancerClient可以访问节点,并且取到当前使用节点的ip、端口等信息。

 

分别多次访问

http://localhost:91/test/testUserServer和http://localhost:91/test/testShopServer

控制台打印如下:

 

可以看出user-server是随机的,而shop-server是轮询的。

 

Configuration放到启动类或其子包下时的情况

Configuration放到启动类或其子包下时的会对所有的服务都生效,而不只是RibbonClient里name配置的服务,而且会在服务启动时就回调ribbonRule
				

方法(而不放在启动类及其子包下时,需要第一次访问到时才会回调ribbonRule)。

效果:

 

user-server和shop-server都使用了自定义的随机访问规则。

排除Configuration到扫描包之外

可以通过自定义注解,将自定义Configuration排除到扫描包之外:

ExcludeScan.java:

public @interface ExcludeScan {
}

 

启动类加注解:
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = ExcludeScan.class)})

 

表示将带有@ ExcludeScan注解的类排除到扫描范围之外。

 

TestConfiguration加@ExcludeScan注解

效果:

 

随机只对user-server生效,shop-server还是采用默认的轮询。

配置文件使用ribbon负载均衡规则

前提是需要Spring Cloud1.2.0及以上的版本。

这里用的是1.4.3


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>

 

 

application.yml增加配置:

user-server:
ribbon:
NFLoadBalancerRuleClassName:
com.netflix.loadbalancer.RandomRule

shop-server:
ribbon:
NFLoadBalancerRuleClassName:
com.netflix.loadbalancer.RandomRule

表示user-server和shop-server都用随机的规则,效果:

脱离eureka使用ribbon

ribbon的服务节点列表可以不从eureka获取,而将其配置到yml文件中。

 

如:

shop-server:
ribbon:
listOfServers:
localhost:96
ribbon:
eureka:
enabled:
false

这里禁用了eureka,同时配置了shop-server服务的节点列表为localhost:96

 

效果:

使用feign:

feign是一个声明式的web service client。springcloud整合了feign,让它支持spring mvc的注解。

使用feign可以不用将访问url的具体路径写到代码里(因此可以不使用RestTemplate去手动调用远程服务),而是将它抽象成client接口。通过client接口的方法调用远程服务。

 

consumer端配置:

pom.xml加入依赖


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

 

启动类加注解:

@EnableFeignClients

 

编写接口

UserFeignClient:

 

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient("user-server")
public interface UserFeignClient {

@GetMapping(value = "/user/list")
User list(@RequestParam("id") Long id);

@PostMapping(value = "/user/postList")
User postList(@RequestBody Long id);
}

 

注意这里如果是get请求,则参数必须加@RequestParam注解,否则会发送post请求。并且括号内的参数"id"不能省略,否则报错。

 

Controller里调用:

TestController:

 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("test")
@RestController
public class TestController {

@Autowired
private UserFeignClient userFeignClient;

@GetMapping("testGetUserList")
public User testGetUserList() {
return userFeignClient.list(5L);
}

@GetMapping("testPostUserList")
public User testPostUserList() {
return userFeignClient.postList(6L);
}
}

 

 

provider端controller提供的接口:


@GetMapping("list")
public User list(@RequestParam Long id) {
System.out.println("服务被调用");
User user = new User();
user.setName("kkk");
user.setAge(99L);
return user;
}

@PostMapping("postList")
public User postList(@RequestBody Long id) {
System.out.println("服务被调用");
User user = new User();
user.setName("kkk");
user.setAge(100L);
return user;
}

 

访问测试:

 

 

说明:Feign通信都是采用字符串的方式,参数中定义的User类其实是转成json字符串传输的,返回时发现需要User,就自动创建User对象,再设置属性,也可以用String类型接收返回的json串,还可以把User改成其他的名字,如Dog.java,也是可以转换成功的:

public class Dog {

public String name;

public Long age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getAge() {
return age;
}

public void setAge(Long age) {
this.age = age;
}
}

 

设置属性时需要能够转换,比如将name定义成Long型:

public Long name

则返回的"kkk"转换不了会报错:

 

与dubbo类似,服务端多出的属性会被消费端设置时忽略,消费端多出的属性会默认为null、0等,而不会报错。

 

 

 

自定义配置feign

类似于ribbon的自定义配置,feign也可以用写代码或者配置文件的方式进行自定义配置。

手动写代码配置:

编写Configuration类:

TestFeignConfiguration

package com.example.config;

import feign.Contract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestFeignConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}

这个类有一个feignContract()方法,返回一个Contract对象(契约对象),这个方法会自动被回调,类似于ribbon的ribbonRule()方法回调。

 

这里用feign默认的Contract覆盖了原来的SpringMvcContract ,而Contract会影响Client接口中方法的定义方式等。

因此这里覆盖之后就只能用feign的方式定义方法了,否则会报错:

@FeignClient(name= "user-server", configuration = TestFeignConfiguration.class)
public interface UserFeignClient {

@RequestLine("GET /user/list")
Dog list(@Param("id") Long id);
}

 

服务提供端provider:
@RequestMapping("list")
public User list(@RequestParam(value = "id", defaultValue = "0") Long id) {
System.out.println("服务被调用");
User user = new User();
user.setName("kkk");
user.setAge(99L);
return user;
}

 

配置类的路径规则同Ribbon的类似,也是不能放在启动类及其子包下,否则会被所有FeignClient使用到,也可以用自定义注解让@ComponentScan扫描不到。

name属性必须唯一,在没有url属性时它是指服务名,有url属性时它只是一个唯一标识。

 

 

通过url属性从eureka服务器获取服务信息

加了url属性以后,name属性就不再是服务名了,而只是一个唯一标识。

 

TestFeignConfiguration2:

import feign.auth.BasicAuthRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestFeignConfiguration2 {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("kkk", "kkk123");
}
}

 

自定义了一个Configuration,它使用默认配置,只是加了一个用户名密码,便于访问eureka。

 

FeignUrlClient.java:

import com.example.config.TestFeignConfiguration2;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name= "aaa", url="http://localhost:8761/", configuration = TestFeignConfiguration2.class)
public interface FeignUrlClient {
@RequestMapping(value = "/eureka/apps/user-server")
String aaa();
}

 

 

定义了一个FeignClient,它的url是eureka的地址,name是唯一标识。

@RequestMapping(value = "/eureka/apps/user-server")是eureka上的user-server服务描述路径。

 

Controller中调用:


@GetMapping("testGetUserServieFromEureka")
public String testGetUserServieFromEureka() {
return feignUrlClient.aaa();
}

 

 

测试:

设置日志打印

feign默认在调用远程服务时是没有日志打印的,可以通过配置设置,可以对单个FeignClient设置不同的日志级别,如对UserFeignClient设置DEBUG级别:

 

1、application.yml增加配置:

logging:
level:
com.example.demo.UserFeignClient:
DEBUG

 

2、在UserFeignClient使用的TestFeignConfiguration增加Bean配置:

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}


				

 

3、测试效果:


				

解决超时问题

设置断路器的超时时间、禁用超时、禁用断路器都可以:

application.yml

hystrix.command.default.execution.isolation.timeoutInMilliseconds: 5000

或者
hystrix.command.default.execution.timeout.enabled: false

或者
feign.hystrix.enabled: false

 

设置客户端元数据

客户端注册到eureka上时,可以设置元数据参数,通过instance.metadata-map设置,如user服务提供端增加配置:

metadata-map:
zone:
aaa
kkk: bbb

 

 

然后到eureka上看:

http://localhost:8761/eureka/apps/user-server

 

元数据分为eureka可以识别的和用户自定义的,可以识别的元数据会影响客户端的行为,不能识别的元数据相当于自定义参数。

 

设置客户端心跳检测间隔时间

leaseRenewalIntervalInSeconds:5

默认是30秒

这个参数主要用在测试时,而在生成环境最好不要改,用默认值。

Eureka集群

多个eurekaserver集群高可用时,每个eurekaserver同时也是eureka客户端,它会注册到至少其他一个eurekaserver上去。如A、B、C三个eurekaserver节点组成高可用,则A会作为一个客户端注册到B或C上(也可以B和C上都注册)。

 

eureka服务器配置

这里配置3个eurekaserver服务器peer1、peer2、peer3,做高可用。

1、将peer1、peer2、peer3加入hosts:

127.0.0.1    peer1 peer2 peer3

 

2、创建3个eurekaserver服务器工程peer1、peer2、peer3

peer1的application.yml:

spring:
application:
name:
eureka-ha
eureka:
instance:
hostname:
peer1
client:
serviceUrl:

defaultZone: http://peer2:92/eureka/,http://peer3:93/eureka/

server:
port:
91

 

peer2的application.yml:

spring:
application:
name:
eureka-ha

eureka:
instance:
hostname:
peer2
client:
serviceUrl:

defaultZone: http://peer1:91/eureka/,http://peer3:93/eureka/

server:
port:
92

 

peer3的application.yml:

spring:
application:
name:
eureka-ha

eureka:
instance:
hostname:
peer3
client:
serviceUrl:

defaultZone: http://peer1:91/eureka/,http://peer2:92/eureka/

server:
port:
93

 

这里peer1注册到了peer2、pee3上,peer2注册到了peer1、peer3上,peer3注册到了peer1、peer2上。

 

3、浏览器访问:


				

 


				

 


				

可以看到eureka-ha有了3个可用节点。

 

 

为了方便测试,可以用springboot的profiles在一个工程中启动3个:

application.yml:
spring:
application:
name:
eureka-ha
---
spring:
profiles:
peer1
eureka:
instance:
hostname:
peer1
client:
serviceUrl:
defaultZone:
http://peer2:92/eureka/,http://peer3:93/eureka/
server:
port:
91

---
spring:
profiles:
peer2
eureka:
instance:
hostname:
peer2
client:
serviceUrl:
defaultZone:
http://peer1:91/eureka/,http://peer3:93/eureka/
server:
port:
92

---
spring:
profiles:
peer3
eureka:
instance:
hostname:
peer3
client:
serviceUrl:
defaultZone:
http://peer1:91/eureka/,http://peer2:92/eureka/
server:
port:
93


				

 

 

客户端注册到eureka集群

如user注册到eureka

再application.yml中配置:
eureka:
client:
service-url:
defaultZone:
http://kkk:kkk123@peer1:91/eureka/

可以只注册到其中一个节点,它会自动同步到其他节点


				


				


				

 

不过如果只注册到一个节点,那个节点挂了之后,服务如果此时重启,则注册不上去了,因此一般都注册到多个节点上:
eureka:
client:
service-url:
defaultZone:
http://kkk:kkk123@peer1:91/eureka/,http://kkk:kkk123@peer2:92/eureka/,http://kkk:kkk123@peer3:93/eureka/

 

常用配置

eureka.client.enabled    true

表明eureka client是否可用

 

eureka.client.fetch-registry    true

表明eureka客户端是否从eureka服务器上拉取注册信息

 

eureka.client.g-zip-content    true

内容传输时是否采用g-zip压缩

 

eureka.client.prefer-same-zone-eureka    true

客户端优先到相同的zone里面找eureka server,如客户端在zone1,就优先找zone1里面eureka服务器,而不是zone2里面的。


				

 

eureka.client.region    us-east-1

默认使用的region名字

 

eureka.client.register-with-eureka    true

指示此实例是否应该将其信息注册到eureka服务器以供其他人发现。

在某些情况下,不希望您的实例被发现,而您只是想发现其他实例。

 

eureka.client.registry-fetch-interval-seconds    30

客户端到eureka服务端抓取注册信息的间隔时间,默认30秒。

 

eureka.client.service-url

它是一个map,如:

service-url:
defaultZone: http://kkk:kkk123@peer1:91

 

eureka.dashboard.enabled    true

是否启用eureka的界面(网页查看首页)

 

eureka.dashboard.path    /

界面首页路径,默认是/,如:

http://localhost:8761/

 


				

 

eureka.instance.appname    unknown

客户端配置的服务名字,配置了它,它就会覆盖spring.application.name,如:


				


				

使用场景是当eureka与其他软件结合使用可能会产生冲突,导致spring.application.name配置不生效时,此时再配置eureka.instance.appname即可。

 

eureka.instance.hostname

指定eureka服务器的主机名,高可用eureka服务器时使用很多。

 

eureka.instance.instance-id

节点的唯一id

 

eureka.instance.lease-expiration-duration-in-seconds    90

指定时间内节点无响应就移除,默认90秒

 

eureka.instance.metadata-map

客户端发送给eurekaserver的键值对参数,有一些会改变客户端的行为(eureka可理解的参数),见"设置客户端元数据"

 

 

eureka.instance.prefer-ip-address    false

客户端在eureka服务器中是否注册ip地址,如;

为true时:

鼠标移动到链接上。链接到了ip地址。


				


				

 

为false时:

链接地址是电脑的机器名字。


				


				


				

 

修改系统状态信息


				

可以修改默认的Environment和Data center信息

application.yml中配置:

eureka:
datacenter:
aaaaa
environment: bbbbb

 

 


				

 

超时机制和自我保护机制

超时机制

客户端可配置该节点发送一次心跳给eureka服务器,以及多久不发送心跳(超时时间)就自动变为无效节点(也就是摘除该instance,但在界面列表中依然能看到,需要清理无效节点后才会从列表删掉)
					

客户端参数:
					

eureka.instance.lease-renewal-interval-in-seconds:

表示eureka client发送心跳给server端的频率,默认30秒

 

eureka.instance.lease-expiration-duration-in-seconds:

客户端多久不发送心跳(超时时间)就自动变为无效节点,默认90秒。

 

eureka服务端参数:
					

eureka.server.eviction-interval-timer-in-ms:

eureka server清理无效节点的时间间隔,默认60000(毫秒),即60秒.

 

 

因此想要客户端失去连接后,很快就能从列表中移除,可以这样配置:
					

客户端:
eureka:
instance:
lease-expiration-duration-in-seconds:
4
lease-renewal-interval-in-seconds: 2

eureka服务器端:
eureka:
eviction-interval-timer-in-ms:
2000

 

 

表示每隔2秒向eureka发送一次心跳,如果4秒没有发送心跳就成为无效节点,而服务端每隔2秒扫描一次无效节点,将它从列表删除。
					

 

 

自我保护机制:

引用:
					

默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
					

Eureka通过"自我保护模式"来解决这个问题——Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
						

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
						

 

 

自我保护机制要在短时间内丢失1个或多个客户端节点才有可能触发。
					

 

通过eureka.server.enable-self-preservation参数配置,默认为true,配置为false时会禁用,建议采用默认。

hystrix

雪崩效应:分布式应用中当某个服务不可用时,导致其他服务调用等到,进而扩大到很多服务不可用的情况。

 

解决方案:

超时机制:调用超时自动降级。

断路器:服务健康状态不佳时,不调用服务而调用降级函数(短路机制),防止等待线程积压。

hytrix概念

hystrix断路器只是作用在服务调用这一端,只需改consumer端。

断路器模式:当一个服务不可用时,其他服务不再调用它,避免出现等待。

断路器的三种状态:关闭、打开、半开:

 

服务的健康状况 = 请求失败数 / 请求总数。

 

断路器关闭时,允许服务被调用。

 

当一个服务的健康状态很低时,断路器打开,不允许服务被调用,调用者直接返回失败(快速返回结果, 避免大量的同步等待)。

 

处于打开状态的断路器经过一段时间后,自动进入半开状态,此时只允许一个请求通过,当该请求调用成功时, 断路器恢复到关闭状态. 若该请求失败, 断路器继续保持打开状态, 接下来的请求被禁止通过(在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能)。

 

B服务不可用且断路器打开时,调用Fallback,而不去调用服务B

 

 

配置(consumer端,ribbon):

1、pom.xml引入依赖:


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

 

2、启动类上加注解@EnableCircuitBreaker


				

 

3、编写断路器方法,指定到目标方法:


import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RequestMapping("test")
@RestController
public class TestController {

@Autowired
public RestTemplate restTemplate;

@RequestMapping("testUserConsumer")
@HystrixCommand(fallbackMethod = "userFallback")
public String testUserConsumer() {
// 通过服务名调用服务
String user = restTemplate.getForObject("http://user-server/user/list", String.class);
return user;
}

@RequestMapping("testMovieConsumer")
@HystrixCommand(fallbackMethod = "movieFallback")
public String testMovieConsumer() {
// 通过服务名调用服务
String movie = restTemplate.getForObject("http://movie-server/movie/list", String.class);
return movie;
}

// testUserConsumer的hystrix短路回调方法
public String userFallback() {
System.out.println("testUserConsumer断路器方法被调用");
return "default user fallback";
}

// testMovieConsumer的hystrix短路回调方法
public String movieFallback() {
System.out.println("testMovieConsumer断路器方法被调用");
return "default movie fallback";
}
}

 

这里将断路器方法userFallback ()指定给了目标方法testUserConsumer (),

断路器方法movieFallback() 指定给了目标方法testMovieConsumer()。

当服务请求失败或者是断路器打开时,会自动调用断路器方法返回结果。

 

注意:断路器方法的参数、返回值必须跟目标方法一致。

否则报错,如:

返回值不一致:


				

参数不一致:

4、访问测试

user服务正常时:

movie服务正常时:

 

 

user服务挂掉时:

movie服务挂掉时:

 

流程:

1、初始状态断路器关闭:先进目标方法,发现服务不可用,转而调用断路器方法返回结果。

2、多次请求服务都不可用时,断路器自动打开,此时不再调用目标方法,而直接调断路器方法返回

3、等一段时间后半开,只有单个请求去调用目标方法,其他请求还是直接调用断路器方法返回。

4、如发现服务健康后,关闭断路器,如服务还不健康,保持打开状态,过一段时间再去试。

日志:

断路器打开后,隔一段时间才去尝试一次目标方法调用。

 

设置线程隔离

默认hystrimCommand修饰的方法执行时会单独放到一个线程池里,因此ThreadLocal里的变量带不过来,可以进行配置,如:


@HystrixCommand(fallbackMethod = "dddTestList", commandProperties = {@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")})

 

此时就使用同一个线程。

 

health页面查看hystrix状态

前提:

1、加入依赖:

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

 

2、禁用验证或者指定账号密码:

management:
security:
enabled:
false

 

查看:

刚开始访问时,断路器关闭,status:UP表示关闭,CIRCUIT_OPEN才是打开

 

 

将movie服务停掉,然后多次刷新http://localhost:9030/test/testMovieConsumer页面,断路器打开。

 

再将user服务也停掉,多次刷新http://localhost:9030/test/testUserConsumer ,此时user的断路器也打开了

 

只要有一个服务断路器打开,hystrix的状态就是CIRCUIT_OPEN。

 

hystrix监控:

通过网页查看

http://localhost:91/hystrix.stream

第一次会一直显示ping,访问一次目标地址

http://localhost:91/test/testConsumer

 

然后就能自动ping出数据来:

 

feign使用hystrix

配置

在consumer的feign工程的基础上配置:

application.yml增加配置:

feign:
hystrix:
enabled:
true

 

UserFeignClient.java


import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@FeignClient(name = "user-server", fallback = HystrixClientFallback.class)
public interface UserFeignClient {

@RequestMapping(value = "/user/list")
List<User> list();
}

@Component
class HystrixClientFallback implements UserFeignClient {
@Override
public List<User> list() {
System.out.println("feign的断路器方法被调用");
List<User> defaultList = new ArrayList<>();
User defaultUser = new User();
defaultUser.setName("defaultUser");
defaultUser.setAge(777L);
defaultList.add(defaultUser);
return defaultList;
}
}

这里定义了回调类HystrixClientFallback,它实现了UserFeignClient接口,服务调用失败或者断路器打开时会自动回调实现方法。

效果:

 

禁用单个feignClient的hystrix

通过单个feignClient的Configuration配置即可,如:

 

FooConfiguration.java:


@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}

 

UserFeignClient.java:

 

这样它就会去找fallback方法了,其实也可以把fallback = HystrixClientFallback.class
						

去掉,它就会提示找不到fallback方法。

使用fallbackFactory打印异常信息

与fallback很类似,只是fallbackFactory可以获取异常对象,然后打印异常信息等。

配置:

配置方式同fallback很类似

 

UserFeignClient.java


import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.ArrayList;
import java.util.List;

@FeignClient(name = "user-server", /*fallback = HystrixClientFallback.class,*/ fallbackFactory = HystrixClientFallbackFactory.class)
public interface UserFeignClient {

@RequestMapping(value = "/user/list")
List<User> list();
}
@Component
class HystrixClientFallbackFactory implements FallbackFactory<UserFeignClient> {

static final Logger LOGGER = LoggerFactory.getLogger(HystrixClientFallbackFactory.class);

@Override
public UserFeignClient create(Throwable cause) {
LOGGER.info("factory打印异常信息:" + cause.getMessage());
cause.printStackTrace();
return new UserFeignClient() {
@Override
public List<User> list() {
System.out.println("feign的断路器方法Factory被调用");
List<User> defaultList = new ArrayList<>();
User defaultUser = new User();
defaultUser.setName("defaultFactoryUser");
defaultUser.setAge(888L);
defaultList.add(defaultUser);
return defaultList;
}
};
}
}

注意:fallback和fallbackFactory同时使用时fallbackFactory会不生效,而fallback优先级更高。

 

服务调用失败或者断路器打开时,打印:

 

 

hystrix dashboard查看断路器状态

可以创建一个单独的工程用于监控各应用的hystrix情况。它可以读取应用的/hystrix.stream结尾地址的数据出来显示。

默认数据是这样的:

 

使用hystrix dashboard可以图形化方式查看断路器状态。

配置:

1、创建hystrix-dashboard工程,pom.xml加入依赖:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

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

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

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

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

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>

2、启动类加入@EnableHystrixDashboard注解
3、访问dashboard主页:


			

4、输入需要监控的应用的hystrix.stream地址,点击监控:

如:http://localhost:91/hystrix.stream


			

它就会自动刷新应用的hystrix状态。

如:


			

各字段含义:

turbine

hystrix dashboard默认一次只能监控一个接入了hystrix的项目的断路情况,因为填项目地址的stream时一次只能填一个,使用turbine可以将项目的服务名填到参数里(如:http://localhost:9019/turbine.stream?cluster=RIBBON-WITH-HYSTIRX-USER表示监控服务名为RIBBON-WITH-HYSTIRX-USER的项目,可以是集群),同时监控多个接入了接入hystrix的项目。

turbine.stream可以看成是一个聚合的数据,交给hystrix dashboard同时监控多个项目。

配置:

加依赖


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>

启动类加注解:

@EnableTurbine

 

 

3、application.yml配置:

server:
port:
9019

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
spring:
application:
name:
turbine

turbine:
aggregator:
cluster-config:
RIBBON-WITH-HYSTIRX-USER
app-config: ribbon-with-hystirx-user

注册到eureka,配置监控的hystrix项目。

 

与hystrix dashboard类似,turbine也有stream端点,可以查看hystrix状态数据,如:

http://localhost:9019/turbine.stream?cluster=RIBBON-WITH-HYSTIRX-USER

 

将这个地址放到hystrix dashboard监控,可以看到结果:

这里RIBBON-WITH-HYSTIRX-USER服务用了2个节点(1个9081端口,1个9181端口)做测试,因此hosts数量为2.

 

停掉user服务后,多次刷新9181端口的服务http://localhost:9181/test/testUserConsumer使它的断路器打开,观察dashboard可以看到Circuit状态的变化。

 

 

再多次刷新9081端口的服务http://localhost:9081/test/testUserConsumer使它的断路器也打开。

断路器状态变化如下,2个全都打开了:

 

 

监控多个不同服务名的项目:

修改配置即可,将服务名用逗号分隔写到app-config属性中。然后cluster-config配置成default,这样dashboard不需要加参数,默认就监控这些配置的服务。

application.yml配置:

server:
port:
9019

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
spring:
application:
name:
turbine

turbine:
aggregator:
cluster-config:
default
app-config: ribbon-with-hystirx-user,ribbon-with-hystirx-movie
cluster-name-expression: "'default'"

dashboard地址去掉参数:

http://localhost:9019/turbine.stream

 

效果:

 

上下文路径配置

hystrix项目有上下文路径时,默认主页会带上上下文,如:

 

其中context-path配置了上下文,home-page-url-path指定了主页地址,可通过eureka的apps节点查看:

同时,hystrix.stream的地址也要加上上下文才可以访问:

http://localhost:9081/ribbonuser/hystrix.stream

 

 

因此turbine里面也需要加上该项目的上下文路径配置,通过instanceUrlSuffix配置。

 

turbine项目的application.yml配置:

server:
port:
9019

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
spring:
application:
name:
turbine

turbine:
aggregator:
cluster-config:
RIBBON-WITH-HYSTIRX-USER
app-config: ribbon-with-hystirx-user

turbine.instanceUrlSuffix.RIBBON-WITH-HYSTIRX-USER: ribbonuser/hystrix.stream

logging:
level:
root:
com.netflix.turbine.monitor:
DEBUG

 

 

这里加了日志打印:可以从控制台看到如下日志:

管理端口配置

管理端口默认跟应用端口使用同一个,可以单独配置管理端口,这样即使应用有上下文也不影响了。

其实就是把监控依赖的端点单独配置了端口。

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

 

 

 

zuul

zuul是spring cloud常用的一个网关(API Gateway)服务器。zuul本身也是一个eureka的客户端,可以将eureka中所有的服务通过zuul的入口来访问。类似于nginx的作用。

 

配置:

新建spring boot工程。

1、pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>

<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>zuul1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>zuul1</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

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

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

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

 

 

其中引入了zuul的依赖

<artifactId>spring-cloud-starter-zuul</artifactId>

和eureka客户端的依赖

<artifactId>spring-cloud-starter-eureka</artifactId>

 

2、application.yml配置:

server:
port:
8040
eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
spring:
application:
name:
zuul1

 

 

3、启动类加@EnableZuulProxy注解:

@SpringBootApplication
@EnableZuulProxy
public class Zuul1Application {

public static void main(String[] args) {
SpringApplication.run(Zuul1Application.class, args);
}
}

 

4、启动:

可以从启动日志看到它把user-service工程提供的服务映射到了mapping:

zuul1注册到了eureka上,

 

 

 

5、访问测试:

http://localhost:8040/user-server/user/list

 

其中的user-server对应服务提供者的name(注意访问时要用小写)。

 

这样就通过访问zuul1这个工程的端口调用到了user-server的服务,zuul1起了网关的作用。类似于nginx的反向代理。zuul默认会代理所有注册到eureka上的服务。

 

zuul可以先于或后于服务启动,zuul会自动到eureka找服务。

 

以下再加入一个movie-server服务方便测试:

服务的路径(路由)匹配:

默认使用服务名作为访问路径映射到指定服务,也可以使用匹配路径映射到指定服务。如:

zuul:
routes:
user-server:
/myusers/**

或者:

zuul:
routes:
aaa:
path:
/myusers/**
serviceId: user-server


也可以使用url指定服务,代替服务名指定服务, 如:
zuul:
routes:
aaa:
path:
/myusers/**
#serviceId: user-server
url: http://localhost:90

其中aaa是自定义取的名字,指定一对映射关系。

此时用

http://localhost:8040/user-server/user/list

或者

http://localhost:8040/myusers/user/list

都可以访问user-server服务。

但movie-server服务不受影响还是只能用
http://localhost:8040/movie-server/movie/list

访问。

 

 

 

注意:使用服务名匹配时如果有多个节点服务名是相同的,则会轮询访问节点(如果某个节点下没有该路径则会返回404),而用url匹配的只会访问到url指定的节点。

排除服务与路径:

在指定了服务与路径的匹配后,可以用ignored-servicesignoredPatterns指定其中哪些服务于路径排除映射,被排除的映射如果访问到会提示找不到页面。

排除服务

ignored-services指定,如

ignored-services: 'movie-server'

表示排除movie-server服务映射

ignored-services: 'user-server,movie-server'

表示排除user-server和movie-server服务映射。

 

ignored-services: '*'

表示排除所有服务映射

 

排除服务不会影响配置的路径映射,如:

zuul:
ignored-services: '*'
routes:
user-server:
/myusers/**

此时user-server服务只能用路径配置方式myusers访问,而不能再用服务名user-server访问。

 

排除路径:

ignoredPatterns指定排除路径

如:

zuul:
ignoredPatterns:
/user-server/**

排除user-server/开头的路径映射,如果有服务名是user-server的,则相当于该服务被排除了。

 

routes如果也有相同路径,则ignoredPatterns的优先级高于routes,如:

zuul:
routes:
user-server:
/myusers/**
ignored-patterns: /myusers/**

此时/myusers开头路径不可访问:

 

ignoredPatterns通常用在子路径匹配中,表示排除某个子路径下的匹配,如:

zuul:
routes:
user-server:
/myusers/**
ignored-patterns: /myusers/aaa/**

 

表示myusers/aaa子路径下的不匹配,myusers下其他的子路径匹配。

 

负载均衡:

默认方式(ribbon轮询)

如果zuul所调用的服务有多个相同的提供者,它默认使用的是ribbon的的轮询方式调用。

如配置了2个id都为user-server的服务,一个返回user_list一个返回user_list_1_2。

 

zuul配置:

 

测试访问:

交替出现。

 

 

自定义服务提供者节点:

zuul定义:

zuul:
routes:
aaa:
path:
/myusers/**
serviceId: user-server

ribbon:
eureka:
enabled:
false

user-server:
ribbon:
listOfServers:
http://localhost:90

 

 

它表示禁用ribbon的默认方式。为user-server这个serviceId自定义服务提供者节点,此时访问http://localhost:8040/myusers/user/list或者http://localhost:8040/user-server/user/list

都只会返回user-list,说明http://localhost:93这个节点不会被请求到。

 

而如果通过zuul访问movie-server则会报错,因为它没有配置自定义的负载均衡节点:

 

对于配置多个节点,也会采用轮询的方式访问,如:
user-server:
ribbon:
listOfServers:
http://localhost:90,http://localhost:93

正则表达式映射服务名和路由路径

在zuul中配置,如:

public class Zuul1Application {

public static void main(String[] args) {
SpringApplication.run(Zuul1Application.class, args);
}

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
}

 

此时符合user-server-v1这种服务名的会映射成v1/user-server的形式访问,如:

但使用服务名就不能访问了

 

相当于修改了默认的用服务id访问方式。yml中配置的映射不受影响,如:

zuul:
routes:
aaa:
path:
/myusers/**
serviceId: user-server-v1

测试访问:

如果匹配不成功,则还是按原样访问(服务id)。

strip-prefix

strip-prefix默认为true,表示去掉前缀。

单个服务中的strip-prefix

zuul单个服务中的strip-prefix需要配合path使用,否则没有任何作用。

strip-prefix表示zuul在转发服务时是否去掉前缀。如:

zuul:
routes:
aaa:
path:
/user/**
serviceId: user-server
strip-prefix: false

 

此时如果访问:http://localhost:8040/user/user/list,本来是想访问/user/list路径,然而由于strip-prefix: false,表示不去掉前缀,它会把前缀/user再加上去,因此zuul会去访问服务的/user/user/list路径,显然user_server中不存在这个地址。

而用http://localhost:8040/user/list访问,它会把"user"前缀加到"/list"前面。

这种strip-prefix: false通常用在服务有上下文路径的情况中,不用每次访问都带上上下文。

 

zuul全局配置中的strip-prefix

zuul全局的strip-prefix需要配合prefix使用,否则没有任何作用。

如:

zuul:
routes:
aaa:
path:
/user/**
serviceId: user-server
prefix: /user
strip-prefix: false

 

首先prefix表示访问zuul的所有服务都要带上这个前缀,其次strip-prefix: false表示这个前缀会自动加到所有访问到的服务的前面。如:

 

此时用http://localhost:8040/user/user/list 或者http://localhost:8040/user-server/user/list都可以访问。

它会去查找前缀之后的服务名或路径路由(user和user-server),找到以后再把前缀拼接到后面的地址(/list)前面,形成/user/list

 

而用http://localhost:8040/user-server/user/list则不能访问,因为没有带上/user前缀。

通常全局的strip-prefix使用很少,除非所有服务都用同一个上下文全局的strip-prefix和单个strip-prefix同时使用的情况也很少。

如:
zuul:
routes:
aaa:
path:
/user/**
serviceId: user-server
strip-prefix: false
prefix: /user
strip-prefix: false

这种配置时使用http://localhost:8040/user/user/list无法访问,因为它找到user路由匹配后会再把user拼接到/user/list前面形成/user/user/list。

http://localhost:8040/user/list也无法访问,因为user后面是list,而list既不是服务名也不是可匹配的path路径。

日志打印(访问的服务路径等)

 

如:

logging:
level:
com.netflix:
debug

后台查看:

zuul的文件上传

默认的文件上传

user工程:static/testUser.html

 

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>user 上传测试</title>
</head>
<body>
<form method="post" action="user/upload" enctype="multipart/form-data">
<input type="file" name="attach"/>
<br/>
<input type="submit" value="上传"/>
</form>
</body>
</html>

 

 

UserController.java添加方法:

@RequestMapping(value="/upload", method= RequestMethod.POST)
public String upload(MultipartFile attach) throws IOException {
// 保存文件
FileCopyUtils.copy(attach.getBytes(), new File("d:/" + attach.getOriginalFilename()));
return "upload success";
}

 

user工程的application.yml配置文件大小:

server:
port:
9010

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/
spring:
application:
name:
user-server
http:
multipart:
max-file-size:
100Mb #默认1M
max-request-size: 120Mb #默认10M

 

此时使用http://localhost:9010/testUser.html可以上传100Mb以下的任何文件

 

而使用http://localhost:8040/user-server/testUser.html的zuul转发,还是只能上传1M以下的文件,且文件名不能带有中文。因为zuul那里也需要配置支持的文件大小。

 

使用/zuul前缀替代默认的DispatchServlet

zuul默认代理请求路径是采用DispatchServlet转发,添加zuul前缀可以使用zuul的方式替代它。

如:

此时,上传路径变成了

http://localhost:8040/zuul/user-server/user/upload

 

此时上传文件大小取决于user端的配置,中文名也没问题了。

 

如果提示超时,可在zuul工程的application.yml里配置超时时间设置,如:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout:
3000
ReadTimeout: 60000

zuul的hystrix的fallback回退

zuul默认集成了hystrix,查看如下:

 

 

使用hystrix的dashboard查看:

 

 

短路测试

停掉user-server服务,然后不停刷新http://localhost:8040/user-server/user/list

多次刷新以后断路器自动开启:

            

自定义fallback

实现ZuulFallbackProvider即可:

 

package com.example.demo.fallback;

import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

@Component
class MyFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return "user-server";
}

@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}

@Override
public int getRawStatusCode() throws IOException {
return 200;
}

@Override
public String getStatusText() throws IOException {
return "OK";
}

@Override
public void close() {

}

@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}

@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}

其中getRoute()方法返回的是服务名。

此时user-server挂掉时会调用getHeaders()、getBody()、getStatusText()等方法封装返回头和返回信息:

 

sidecar

sidecar用于代理异构语言,将其他语言编写的服务桥接到spring boot的java工程,被zuul代理之后,可以像操作java的服务一样操作其他语言的服务,并实现健康检查等监控。

异构语言需要实现健康检查/health.json的url。

 

node语言编写的服务如下:

node-server.js内容

var http = require('http');

var url = require('url');

var path = require('path');

 

// 创建server

var server = http.createServer(function(req, res) {

    var pathname = url.parse(req.url).pathname;

    res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8'});

 

    if (pathname === '/') {

        res.end(JSON.stringify({ "index": "欢迎来到首页"}));

    }

    else if (pathname === '/health.json') {

        res.end(JSON.stringify({ "status": "UP"}));

    }

    else {

        res.end("404");

    }

});

 

// 创建监听

server.listen(8060, function() {

    console.log('listen localhost:8060');

});

 

使用node运行服务

访问测试:

 

创建sidecar工程:

加依赖:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>

启动类加注解

@EnableSidecar

它是一个组合注解,包含了zuul和hystrix的断路器注解

 

application.yml加配置:

sidecar:
port:
8060
health-uri: http://localhost:8060/health.json

 

访问测试:

使用zuul访问:

http://localhost:8040/sidecartest

 

http://localhost:8040/health.json

 

直接访问sidecar的应用,出现几个链接

 

依次点进去可以看到:

 

修改health状态测试:

将node服务的health.json的UP改为DOWN

重启之后观察eureka

sidecar的状态也变成了DOWN,说明sidecar的状态同步了被桥接服务的状态。

此时代理服务也没法使用了:

 

再改为UP再重启,要过一会服务才可用。

 

zuul过滤器(Filter)

zuul根据请求的生命周期分为4中过滤器:

PRE:请求路由到微服务之前调用

ROUTING:这种过滤器是请求路由到微服务过程中调用

POST:路由到微服务之后调用,可以用来添加Header,进行信息统计等。

ERROR:发生错误时调用。

 

配置:

创建Filter继承ZuulFilter即可,这里创建2个Filter,一个pre一个post(通过filterType()方法返回决定)。

TestPreZuulFilter.java

package com.example.demo.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

import javax.servlet.http.HttpServletRequest;

public class TestPreZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
System.out.println("pre:host=" + request.getRemoteHost());
return null;
}
}

 

TestPostZuulFilter.java:

package com.example.demo.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

import javax.servlet.http.HttpServletRequest;

public class TestPostZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "post";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
System.out.println("post:host=" + request.getRemoteHost());
return null;
}
}

 

 

其中shouldFilter()返回false时不走该filter。

filterOrder()表示顺序,多个filter时该值越小越先执行

filterType()返回filter的类型,如"pre"、"post"等。

启动类中用@Bean将Filter配置成Bean

package com.example.demo;

import com.example.demo.filter.TestPostZuulFilter;
import com.example.demo.filter.TestPreZuulFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableZuulProxy
public class Zuul1Application {

public static void main(String[] args) {
SpringApplication.run(Zuul1Application.class, args);
}

@Bean
public TestPreZuulFilter testPreZuulFilter() {
return new TestPreZuulFilter();
}
@Bean
public TestPostZuulFilter testPostZuulFilter() {
return new TestPostZuulFilter();
}

}

 

测试:

请求http://localhost:8040/user-server/user/list,观察控制台打印:

 

禁用zuul的filter

zuul默认使用了多个filter,可以根据情况禁用,如配置:

zuul.SendResponseFilter.post.disable=true

则可以禁用默认的SendResponseFilter

spring cloud config

spring cloud config可以实现集中配置、分环境配置(如dev、test、product等)、动态调整刷新配置的功能。将配置信息放在git、svn等服务端,然后工程中配上配置服务端地址,自动去获取配置。

比如有20个项目都有相同配置,就可以将这些相同的配置抽出来放到服务端,不用每个项目都去配,修改的时候只需要改服务端一个地方即可动态刷新,不需要每个项目去改,然后重启。

 

也可以用zookeeper或者consul,也有很多第三方的,如百度的disconf,淘宝的diamond,携程的apollo

 

cloud config包括Config Server和Config Client两部分。默认使用Git存储,也可以使用SVN、本地File等,方便实现配置的版本控制。

 

config server配置:

1、pom加入config server依赖:


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

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

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>


</dependencies>

 

2、启动类加@EnableConfigServer注解

@SpringBootApplication
@EnableConfigServer
public class Cloud1Application {

public static void main(String[] args) {
SpringApplication.run(Cloud1Application.class, args);
}

}

加了@EnableConfigServer注解

 

3、application.yml加配置:

spring:
cloud:
config:
server:
git:
uri:
https://gitee.com/wukejing2/cloudtest
username: xxxxxxx@qq.com
password: xxxxxxxx

这里配置了git的地址,账号和密码

git地址本来是

https://gitee.com/wukejing2/cloudtest.git

这里去掉了".git"后缀

 

匹配规则:

/{application}/{profile}[/{label}]

/{application}-{profile}.yml

/{label}/{application}-{profile}.yml

/{application}-{profile}.properties

/{label}/{application}-{profile}.properties

有这几种匹配方式,传入的路径必须跟它匹配上。否则返回404,如:

其中{application}表示配置项名。

{profile}一般是dev、test、product等,表示环境名称。

{label}一般是dev、test、master,表示分支名称

 

 

正确的格式,如:

/aaa/bbb/master

/aaa-bbb.yml

/master/aaa-bbb.yml

/aaa-bbb.properties

/master/aaa-bbb.properties

 

匹配逻辑:

1、解析出{application}和{profile}。

2、然后去匹配,如果没有匹配到对应的{application}+{profile}的组合,则去找{application}.yml

3、如果没有找到,则将{application}置为默认的"application",再去匹配application+{profile}。

4、如果还没有匹配到,则去掉{profile},直接取application.xml.

5、同名的properties文件和yml文件同时存在时,如application.yml和application.properties。properties文件优先抓取。

 

 

4、测试:

服务器文件:

 

例1:

没有找到ggg-dev.yml,则去找ggg.yml,没有找到,然后将ggg置为application,找application-dev。找到了。

 

例2:

没有找到foobar-bbb.yml,则去找foobar.yml,找到了。

 

 

例3:

没有找到ggg-dev.yml,,则去找ggg.yml,没有找到,然后将ggg置为application,找application-aaa.yml。也没有找到,去掉-aaa,直接找application.yml。

 

 

config client配置:

client连接server,然后获取配置信息。当然也可以手动写代码用http连接server获取配置然后解析,不过client有封装好的方式,直接用@Value通过key获取配置的值。

1、pom加入client的依赖:


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

 

 

2、创建bootstrap.yml文件,内容:

spring:
cloud:
config:
uri:
http://localhost:8080
profile: dev
label: master
application:
name:
foobar

 

其中uri是server的地址,profile是${application}-${profile}中的profile。

label是分支,当仓库为git时,默认是master

foobar是${application}部分,同理如果没有找到会默认为"application"

 

ps:spring:cloud:config的部分不能直接在application.yml中配置。

 

下面的

application:
name:
foobar

可以放到application.yml中

 

3、使用@Value注入使用:

ClientController.java:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("client")
@RestController
public class ClientController {

@Value("${kkk}")
private String tk;

@RequestMapping("test")
public String test() {
return tk;
}
}

 

4、测试访问:

配置文件测试内容如下:

效果:

 

ps:如果本地有相同的配置,也会优先取远程服务端的。

 

通配符{application}匹配仓库地址

git仓库的地址在server可以用{application}通配符配置,请求的时候传入仓库名,自动动态定位到对应仓库,这样可以做到一个微服务对应一个仓库。

1、创建仓库及application.yml文件

创建2个仓库分别是:

https://gitee.com/wukejing2/simple.git

https://gitee.com/wukejing2/special.git

 

仓库文件

 

2、server的application.yml配置

spring:
cloud:
config:
server:
git:
uri:
https://gitee.com/wukejing2/{application}
username: xxxxxx
password: xxxxxxxx

使用{application}通配符

 

3、访问测试:

3.1、访问的方式需要是{application}-{profile}的形式才可以:

 

3.2默认访问master分支。

 

3.3找到profile为dev时的情况:

找不到profile时默认访问application.yml:

 

模式匹配仓库

可以对每个仓库单独设置匹配的方式,如:


spring:
cloud:
config:
server:
git:
uri:
https://gitee.com/wukejing2/cloudtest
username: 397710200@qq.com
password: 3071828183a

repos:
simple_test:
pattern:
- 'simple*'
uri:
https://gitee.com/wukejing2/simple
special_test:
pattern:
- 'special*/dev*'
- '*special*/test*'
uri:
https://gitee.com/wukejing2/special

 

测试访问:

全都可以用{application}/{profile}或者{application}-{profile}访问

1、simple*的访问方式是{application}-{profile}或者{application}/{profile}

 

 

2、special*/dev*这种模式的访问方式也是{application}-{profile}或者{application}/{profile},

 

匹配不到时取默认(配置中只匹配了dev和test)

 

3、无法命中时,取上面uri的公用配置

 

加密

将密码等配置信息放到git仓库会带来安全问题,因此最好对其进行加密,将密文放到git仓库,使用时由server端获取后进行解密,再传给client。

对称加密

配置:

1、下载替换jdk的jce安全策略文件

jce下载地址:

https://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html

 

 

这2个文件是jce安全策略文件,将local_policy.jar和US_export_policy.jar替换jdk的jdk1.8\jre\lib\security目录同名文件。

 

2、server端的bootstrap.yml配置密钥

如:

encrypt:
key:
foo

 

启动server,从启动日志可以看到加密端点和解密端点的路径。

 

用postman或者curl工具访问:

curl使用如:

curl http://localhost:8080/encrypt -d foobar

 

postman:

 

其中-d表示后面的参数是提交的数据,也就是foobar是提交的body数据。

可以看到同一个body文本(foobar)每次加密生成的密文都不一样,而解密的结果都是foobar。

可以推断出加密时可能加入一个随机字符串或者时间戳作为salt,解密后将salt去掉就得到明文。

 

3、客户端使用(上传密文到git仓库)

将密文前面加上{cipher}上传到git仓库,如:

注意:如果是properties文件,则不能加单引号,而yml文件里的要加单引号。

 

client不用做任何修改,在server端解密后传给client

4、测试访问:

 

 

非对称加密

非对称加解密是指用公钥加密,私钥解密。这里由keytool工具生成公钥+私钥。

 

1、用keytool工具生成密钥对(公钥+私钥)文件

keytool是jdk自带的密钥工具。

cd到D盘,执行:

keytool -genkeypair -alias mytestkey -keyalg RSA -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" -keypass changeme -keystore server.jks -storepass letmein

生成的文件server.jks;

 

命令含义,使用keytool -genkeypair –help查看

 

其中-dname可以用向导方式创建,如:

 

 

2、server的bootstrap.yml配置:

encrypt:
keyStore:
location:
classpath:/server.jks
password: letmein
alias: mytestkey
secret: changeme

其中location指定密钥对文件路径,其他几个参数跟创建密钥对文件时的参数对应。

 

使用curl命令测试

ps:同spring cloud的对称加密一样,spring cloud的非对称加密每次生成的密文也都不同。

 

3、客户端使用(上传密文到git仓库)

同对称加密使用方式相同,也是将密文上传到git仓库:

 

4、测试访问:

 

 

 

安全认证

配置方式:

1、加依赖:


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

 

 

重启访问,要求输入密码

用户名user

密码打印在了启动日志中。

2、配置手动设置用户名密码

同eureka一样的方式手动设置用户名密码。

security:
basic:
enabled:
true
user:
name:
kkk
password: kkk123

 

 

client没有配置正确的账号密码时的日志:

 

 

client配置了正确的账号密码时的日志:

 

3、client配置使用

bootstrap.yml里配置上用户名密码即可,如:

spring:
cloud:
config:
uri:
http://kkk:kkk123@localhost:8080
profile: b
label: master
# username: kkk
# password: kkk123
application:
name:
a

配到uri里面,或者单独配置username和password属性都可以。属性比uri里面的优先级高。

 

集成eureka

client端直接硬编码配置server的地址不利于server的迁移、扩容、负载均衡等方式,如果要实现高可用还得借助nginx等。

将server注册到eureka可以解决此问题,并可以做到server多节点的高可用。

 

server端配置:

配置方式同eureka的client端一样

1、加依赖:

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

 

2、启动类加@EnableDiscoveryClient或者@EnableEurekaClient注解

 

 

3、application.yml加入配置:

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/

 

4、访问eureka服务器查看

可以看到server已经在eureka上注册成功了。

 

client端配置:

配置方式同server端类似,只是bootstrap.yml的配置需要调整。

1、加依赖:

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

 

2、启动类加@EnableDiscoveryClient或者@EnableEurekaClient注解

 

 

3、application.yml加入配置:

eureka:
client:
service-url:
defaultZone:
http://localhost:8761/eureka/

 

4、bootrap.yml配置调整

spring:
cloud:
config:
# uri: http://kkk:kkk123@localhost:8080
# profile: b
# label: master
discovery:
enabled:
true
service-id: cloud1-config-server
username: kkk
password: kkk123
name: a

去掉了server地址的硬编码配置,改为服务发现配置,配置的service-id对应server的服务名application-name

5、访问eureka服务器查看,网页测试

 

配置刷新

git中存储的配置更新以后,client默认是需要重启服务重新获取配置,它也支持不重启的情况下重新拉取配置。

client单节点刷新

1、client的Controller上加@RefreshScope注解。

使用@RefreshScope注解标注的bean在配置发生更新时会进行特殊处理。

 

2、加监控依赖


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

它提供了/refresh端点,用于刷新配置。

 

 

3、关闭监控默认认证

由于监控的端点如refresh默认需要用户名密码认证,需要将其关闭才能用curl访问.

application.yml中增加配置:

management:
security:
enabled:
false

4、手动访问链接刷新/refresh

修改配置信息上传git后,使用curl手动刷新client配置:

curl -X POST http://localhost/refresh

日志滚动:

配置刷新成功。

 

如果没有做上述第3步关闭默认监控,则可能出现如下报错:

多个client节点刷新(使用spring cloud bus)

如果对每个client节点都去访问/refresh刷新很麻烦,尤其是在client节点很多的时候。可以采用spring cloud bus,在一个节点刷新时发送mq消息通知其他节点刷新。它依赖了mq组件,这里采用rabbitmq做例子,也可以用kafka等其他mq消息组件。

rabbitmq安装
1、安装erlang

http://www.erlang.org/downloads

2、安装rabbitmq

https://www.rabbitmq.com/install-windows.html

 

rabbitmq默认会安装为系统服务

 

3、安装management插件

management插件提供了界面查看mq运行情况。

进入到rabbitmq安装目录下的sbin目录执行rabbitmq-plugins enable rabbitmq_management

然后重启rabbitmq服务,就可以访问了:

http://localhost:15672

账号密码都是guest

 

 

 

 

client端使用spring cloud bus
使用步骤

1、pom加spring cloud bus依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

 

 

2、bootstrap.yml加配置

配置mq的连接地址和账号密码。

spring:
rabbitmq:
host:
localhost
port: 5672
username: guest
password: guest

 

 

3、配置更新时刷新链接:/bus/refresh

如;

curl -X POST http://localhost/bus/refresh

 

访问一个client的/bus/refresh时会触发一个事件,该事件会通过mq广播发送到其他client节点

对应的topic名字是springCloudBus。

 

设置webhooks(可选操作)

通过在git仓库配置webhooks,可以设置在代码push等操作以后自动访问特定链接,触发事件。

 

 

测试:

复制一个client端将端口改为8083

然后启动原client和8083的client

此时2个client的配置都相同

 

修改git文件内容,改为hellworld-refresh,然后提交

 

client不刷新也没有重启的情况下还是原来的helloworld

执行刷新操作:

curl -X POST http://localhost/bus/refresh

 

可以看到client的日志滚动了:

同时,2个client的配置都更新了:

 

在rabbitMQ可以看到topic名字是springCloudBus

指定节点刷新

刷新时可以指定刷新哪些节点,可以用在切流量更新配置等情况。

如:

curl -X POST http://localhost/bus/refresh?destination=cloud1-config-client:80

效果:

只刷新了cloud1-config-client:80这个节点

 

ps:被访问链接的节点默认会被刷新,不管是否配置到了destination.

spring.application.index的坑

cloud bus在刷新配置时通过spring.application.index属性的值来区分不同的client节点,spring.application.index属性的值默认是"应用名:端口",如果client部署到多台机器的相同端口上,则"应用名+端口"都相同,因此这种情况下cloud bus在刷新配置时只会刷新其中一台。

解决办法让每个client节点的spring.application.index都配成不同的值,cloud bus就能区分了,不过比较麻烦。

 

 

trace查看访问记录

trace端点可查看访问的历史记录

 

开启bus的trace可以看到收到MQ消息的记录:

spring:
cloud:
bus:
trace:
enabled:
true

 

 

 

这是接收方收到MQ消息时的trace记录。client节点会解析destination属性中是否包含当前节点,包含的话就刷新配置。

 

刷新节点放到server

由于client节点变动的可能性,比如服务器ip端口都可能经常变,如果作为bus请求节点,则在webhooks上需要经常改地址,很不方便,而server的ip、端口相对稳定,因此通常将server端配置上mq的接入,作为client刷新/bus/refresh。

配置fail-fast

默认client获取远程配置失败时,采用本地配置,可以设置成远程获取失败就报错启动失败,不用本地配置。

spring:
cloud:
config:
fail-fast:
true

 

 

效果:

 

 

 

 

posted @ 2020-12-16 10:52  吴克兢  阅读(139)  评论(0)    收藏  举报