前言
在微服务系统前期,服务并不多,所以可以通过静态配置来维护并完成服务间的调用。但随着服务越来越多,静态配置就会变得难以维护(集群规模、服务的位置、服务的命名等都有可能发生变化)。
为解决服务实例的维护问题,由此衍生出服务治理的框架和产品
服务治理
服务治理又分为服务注册、服务发现
服务注册
在服务治理框架中,通常都会构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
简要理解为一份服务的清单即可
服务发现
在服务治理框架下运作, 服务间的调用不再通过指定具体的实例地址来实现, 而是通过向服务名发起请求调用实现。 所以, 服务调用方在调用服务提供方接口的时候, 并不知道具体的服务实例位置。 因此, 调用方需要向服务注册中心咨询服务, 并获取所有服务的实例清单, 以实现对具体服务实例的访问。
如果发现有多个合适的服务实例,那便涉及到负载均衡的问题
简单的可以理解为
在学校有一份各科任老师的名单(服务注册),学生有问题的话,只要拿到这份名单,就能找到他能找
的科任老师有那些(是数学还是语文……),找到的同一科任老师可能有多个,那么学生就按一定的策略来
选择一个老师解决问题即可(负载均衡)
Netflix Eureka
Spring Cloud Eureka, 使用Netflix Eureka来实现服务注册与发现, 它既包含了服务端组件,也包含了客户端组件。
Eureka服务端,服务注册中心,支持高可用配置。
Eureka客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。
搭建Eureka服务注册中心
1、导包
先建立一个空的maven项目,将src文件夹删掉
导入依赖
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring.cloud-version>2021.0.3</spring.cloud-version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
依赖导入版本如果不合适会报错,如下图
SpringCloud对应的SpringBoot版本可以在SpringCloud官网查看
https://spring.io/projects/spring-cloud
建立两个子moudle:
分别作为Eureka服务端和Eureka客户端
服务端依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
//继承父工程pom依赖
<parent>
<groupId>org.example</groupId>
<artifactId>Eureka</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-service</name>
<description>eureka-service</description>
//eureka服务端依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
eureka客户端依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
//继承父工程pom文件
<parent>
<groupId>org.example</groupId>
<artifactId>Eureka</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-client</name>
<description>eureka-client</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//eureka客户端依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、写配置
eureka-server
application.yaml(用properties也行)
#服务端口号
server:
port: 8081
#域名
eureka:
instance:
hostname: localhost
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
eureka.client.register-with-eureka: 由于该应用为注册中心,所以设置为 false, 代表不向注册中心注册自己。
eureka.client.fetch-registry: 由于注册中心的职责就是维护服务实例,它并不需要去检索服务,
所以也设置为 false。
eureka.client. serviceUrl.defaultZone:指定服务注册中心的地址
在启动类上加上@EnableEurekaServer注解
eureka-client
application.yaml
server:
port: 8082
eureka:
instance:
hostname: localhost
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8081/eureka/
spring:
application:
name: eureka-client
在启动类上加上@EnableEurekaClient注解
3、测试
启动两个项目,在eureka-client控制台会打印以下输入,表示客户端已向服务端注册
[nfoReplicator-0] com.netflix.discovery.DiscoveryClient :
DiscoveryClient_EUREKA-CLIENT/localhost:eureka-client:8082 - registration status: 204
打开eureka-server服务注册中心地址:即上面配置的localhost:8081
显示已有一个实例被注册,且状态是UP.
在eureka-client中编写一个控制器进行测试:
@RestController
@RequestMapping("/v1")
public class HelloController {
//获取当前应用的端口号
@Value("${server.port}")
String port ;
@GetMapping("/hello")
public String hello(@RequestBody String name){
String str = "hello " + name + "this service port is:" + port;
return str;
}
}
测试结果:
注册中心高可用
在微服务架构这样的分布式环境中, 需要充分考虑发生故障的情况, 所以在生产环境中必须对各个组件进行高可用部署, 对于微服务如此, 对于服务注册中心也一样。
Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心, 以实现服务清单的互相同步, 达到高可用的效果。
1、写配置
直接在上面的基础至上进行改造
新建两个新的配置文件:
application-jq1.yaml
server:
port: 8083
//eureka会自动拉取设备host,各节点在同一机器下时请务必添加,注意各节点配置自己节点的host
//不要一样
//不想使用主机名来定义注册中心的地址也可以使用IP地址的形式, 但是需要在 配置文件中增加配置参数
//eureka.instance.prefer-ip-address= true, 该值默认为false。
//使用ip如果解析出来的ip是一样的话,也会报错,所以建议使用命名
eureka:
instance:
hostname: jq1
//注册url指向jq2
client:
service-url:
defaultZone: http://jq2:8084/eureka/
//各个配置文件里这个必须一样,也可以不写,默认一样
spring:
application:
name: eureka-server
application-jq2.yaml
server:
port: 8084
eureka:
instance:
hostname: jq2
client:
service-url:
defaultZone: http://jq1:8083/eureka/
spring:
application:
name: eureka-server
与之前的配置文件不同:
因为要相互发现和注册,所以这两个要设置为true,但默认就是true,所以可以不用配
eureka.client.register-with-eureka
eureka.client.fetch-registry
因为是在个人电脑本地进行测试,为了保证两个配置能正确访问,要在hosts文件下进行配置
C:\Windows\System32\drivers\etc
127.0.0.1 jq1
127.0.0.1 jq2
2、测试
将项目进行打包,在idea的控制台进入到target目录下,以命令行的方式执行jar
java -jar .\eureka-service-0.0.1-SNAPSHOT.jar --spring.profiles.active=jq1
java -jar .\eureka-service-0.0.1-SNAPSHOT.jar --spring.profiles.active=jq2
打开8083和8084的页面
显示up两个,并且另外的节点在可用分片(available-replicase)之中
关掉一个服务
节点变为了不可用分片(unavailable-replicas)
但如果打开页面就直接是这样的,在网上那些都是说什么配置的问题的,不现实,因为配置都是一样的,我出现这样的情况是因为eureka-server中最开始的application.yaml没注释掉,而应用一启动会先加载application.yaml文件导致的,注释掉就好了
解决问题链接:http://t.csdn.cn/tc4Zf
高可用配合客户端使用
将之前的客户端里的注册url改一下
client:
service-url:
defaultZone: http://jq1:8083/eureka/,http://jq2:8084/eureka/
打开任意一个服务端的地址
此时进行测试,即使有其中一个服务端失效了,也能继续提供服务
服务发现与消费
以上只是说到了服务注册中心的注册和高可用以及eureka客户端的使用。但只有服务的注册和发现。没有消费。
什么是消费?如果只有上面的点东西,那么客户端的服务注册到注册中心有什么意义呢是吧?
所以,有别的应用要在注册中心调用某个客户端的服务才行,这就是消费。
所以被调用的客户端称为服务提供者,调用的客户端称为消费者
1、导包
因为服务的调用会涉及到负载均衡,这里先用,不深入。
新建一个Springboot工程
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>Eureka</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>eureka-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-consumer</name>
<description>eureka-consumer</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
//负载均衡依赖包不用导
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>-->
<!-- <version>2.2.10.RELEASE</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
微服务实战这书里导入的包是
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-eureka</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.cloud</groupid>
<artifactid>spring-cloud-starter-ribbon</artifactid>
</dependency>
</dependencies>
我使用的版本是没有spring-cloud-starter-eureka的,所以导spring-cloud-starter-netflix-eureka-client
由于netflix-eureka-client中已经集成了ribbon,所以导包的时候只要netflix-eureka-client这个就可以了。
不要导ribbon的包了
如果导了会出现异常
java.lang.IllegalStateException: No instances available for xxxx
就是 eureka-server服务不可用
另外出现这样的异常:
添加负载均衡Ribbon后,启动springboot时
[org/springframework/boot/autoconfigure/web/ServerPropertiesAutoConfiguration.class] cannot be opened because it does not exist
看这里:http://t.csdn.cn/XXkC0
2、写配置
application.yaml
server:
port: 8085
#注册的地址,随便写一个之前的注册中心地址即可
eureka:
client:
service-url:
defaultZone: http://jq1:8083/eureka/
spring:
application:
name: eureka-consumer
在启动类上加入@EnableDiscoveryClient注解
3、测试
编写一个测试类
package com.example.eurekaconsumer.Controller;
import com.example.eurekaconsumer.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/v1")
public class ConsumerController {
@Bean
@LoadBalanced //开启负载均衡
RestTemplate restTemplate() {
return new RestTemplate();
}
@Lazy
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/eureka-consumer",method = RequestMethod.GET)
public String helloConsumer() {
User user = new User();
user.setId("1");
user.setName("world");
String str = "Hello World";
ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity("http://EUREKA-CLIENT/v1/hellouser", user,String.class);
ResponseEntity<String> string = restTemplate.postForEntity("http://EUREKA-CLIENT/v1/hello", str,String.class);
return stringResponseEntity.getBody() + "\n"+string.getBody();
}
}
服务提供者的方法
@PostMapping("/hello")
public String hello(@RequestBody String name) {
String str = "hello " + name + ",this service port is:" + port;
return str;
}
@PostMapping("/hellouser")
public String hello(@RequestBody User user) {
String str = "hello " + user.getName() + ",this service port is:" + port;
return str;
}
在idea上将服务提供者即eureka-client使用两个端口号启动
测试结果显示,两个服务消费经负载均衡后可能会调用一个服务,也可能不会
某一个服务提供者实例挂掉后,在本地服务注册列表更新之前会继续请求之前的实例,所以会报错,过一会恢复正常。
Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新 一次。
eureka.instance.lease-renewal-interval-in-seconds 参数用于定义服务续约任务的调用间隔时间,默认为30
秒。 若希望修改缓存清单的 更新时间,可修改。
eureka.instance.lease-expiration-duration-in-seconds参数用于定义服务失效的时间,默认为90秒。
2023-02-18 10:57:50.668 ERROR 21144 --- [nio-8085-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://EUREKA-CLIENT/v1/hello": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect] with root cause
java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.connect0(Native Method) ~[na:1.8.0_341]
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:75) ~[na:1.8.0_341]
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476) ~[na:1.8.0_341]
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218) ~[na:1.8.0_341]
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200) ~[na:1.8.0_341]
补充restTemplate使用
restTemplate使用什么样的api出不出问题,和服务提供者标注的注解也有关系的,比如标注了@GetMapping
的服务方,你使用postForEntity等post方法调,会报错。具体的细节可以再深究
贴一个restTemplate的使用:https://www.jianshu.com/p/e87595f7fffd
最后
只是一个简单的入门配置demo,至于源码分析、具体配置啥的先不涉及。以后再说。