微服务其实是系统架构上的一种设计风格,它的主旨是讲一个系统拆分成多个小型服务,这些小型服务在各自独立的进程中运行,服务之间通过基于HTTP的RESTful
API
进行通信协作。被拆分成的每一个小型服务都围绕这系统中的某一项或者某一些耦合度较高的业务功能进行构建,并且每个服务都维护这自身的数据存储、业务开发、自动化测试案例以及独立部署机制。由于有了轻量级的通信协作基础,所以这些微服务可以使用不同的语言进行编写。
- 在项目中通常将需求分为三个重要部分:数据库,服务端处理,前端展现。
- 随着企业的发展,系统为了不同的业务需求会不断地为该单体服务增加不同的业务模块;单体应用由于面对的业务需求更加宽泛,不断扩大的需求会使单体应用变得越来越臃肿。
- 由于单体系统部署在一个进程内,往往我们修改了一个很小的功能,为了部署上线会影响其他功能的运行。
- 单体应用中的这些功能模块的使用场景、并发量、消耗的资源类型都各有不同,对于资源的利用又互相影响,使得我们对于各个业务模块的系统容量很难给出较为准确的评估。
- 单体系统在初期可以很方便的进行开发和使用,但是随着系统的发展,维护成本会变得越来越大,且难以控制。
为了解决单体系统变得庞大臃肿后产生的难以维护的问题,微服务架构诞生了。
微服务的优点:
- 我们可以将系统中的不同功能模块拆分成多个不同的服务,这些服务可以独立的部署和扩展;
- 由于每个服务都运行在自己的进程内,在部署上有稳固的边界,这样每个服务的更新并不会影响其他服务的运行。
- 配合服务间的协作流程还可以更加容易的发现系统的瓶颈位置,给出较为准确的系统级性能容量评估。
微服务虽然有许多优点,但也会引发许多单体服务中并不存在的问题:
虽然拆分了业务,但是业务逻辑上的依赖并不会消除,只是从单体应用中的代码依赖转变成了服务之间的通信依赖。而当我们对原有接口进行了一些修改,那么交互方也需要协调这样的改变来进行发布,以保证接口的正确调用。我们需要更晚上的接口和版本管理,或是严格的遵循开闭原则。
由于拆分后的各个微服务都是独立部署并运行在各自的进程内,他们只能通过通讯来进行协作,所以分布式坏境的问题都将是微服务架构系统设计时需要考虑的重要因素:如网络延迟,分布式事务,异步消息等。
微服务架构的九大特性:
在微服务的架构中,通常会使用一下两种服务调用方式:
- 使用HTTP的RESTful API 或轻量级的消息发送协议,实现信息传递与服务调用的触发。
- 通过轻量级消息总线上传递消息,类似RabbitMQ 等一些提供可靠一部交换的中间件。
Spring Cloud微服务构建基于Spring Boot的实现
重点:
- 如何构建Spring Boot项目
- 如何实现 RESTful API接口
- 如何实现多环境的Spring Boot应用配置
- 深入理解Spring Boot配置的启动机制
- Spring Boot应用的监控与管理
1.通过官方的Spring InitialLizr工具来产生基础项目
2.访问https://start.spring.io/,该页面提供了以Maven或Gradle构建SpringBoot项目的功能
3.选择构建工具Maven Project,SpringBoot版本选择1.3.7,填写Group和Artifact信息,在Search for dependencies中可以搜索需要的其他依赖包,这里我们需要实现RESTful API,所以要添加web依赖。
4.单击Generate Project按钮下载项目压缩包。
5.解压项目包,并用IDE以Maven项目导入,File-New-Project from Existing Sources。
6,。选择解压后的项目文件夹,单击OK
Spring Boot的基础结构有三大块:
<?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.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<!--Spring Boot默认将项目打包成jar包的形式,因为默认的web模块依赖会包含嵌入式的Tomcat
所以,这就使得我们的应用jar自身就具备了提供web服务的能力-->
<name>springboot-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<!--<java.version>1.8</java.version>-->
<!--父项目parent配置指定为spring-boot-starter-parent 的1.3.7版本
定义了Spring Boot版本的基础依赖以及一些默认配置内容,比如配置文件application.properties的位置等
-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-start-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</properties>
<dependencies>
<dependency>
<!--全栈web开发模块,包含嵌入式Tomcat,Spring MVC-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<!--通用测试模块,包含JUnit、Hamcrest、Mockito-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>springboot-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里所引用的web和test模块在Spring Boot生态中被称为Starter POMs,Starter POMs是一系列轻便的依赖包,是一套一站式的Spring相关技术的解决方案。开发者在使用和整合模块的时候,不需要再去搜索样例式代码中的以来配置来复制使用,只需要引入对应的模块包即可。
比如在开发Web应用的时候,就映入spring-boot-starter-web;希望应用具有访问数据库能力的时候,则需要引入spring-boot-starter-jdbc或者更好用的spring-boot-starter-data-jpa。
创建web包,创建HelloController
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index(){ //通过访问http://localhost:8080/hello
return "HELLO world";
}
}
访问http://localhost:8080/hello,则可以看到效果。
启动Spring Cloud应用
编写单元测试
package com.example.springbootdemo;
import com.example.springbootdemo.pojo.Book;
import com.example.springbootdemo.web.HelloController;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.hamcrest.Matchers.equalTo;
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)//引入spring对于JUnit4的支持
//@SpringApplicationConFiguration(Classes = HelloApplication.class)
@WebAppConfiguration//开启web应用的配置,用于模拟servletContext
class SpringbootDemoApplicationTests {
//用于模拟调用Contriller的接口发起请求,在@Test定义的hello测试用例中
//perform执行一次请求调用,accept用于执行接受的数据类型
//accept用于执行接收的数据类型
//andExpect用于判断接口返回的期望值
private MockMvc mvc;
@Autowired
Book b;
@Before//JUnit中定义在测试用例@Test内容执行前预加载的内容,这里用来初始化HelloController的模拟
public void setUp() throws Exception{
// mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
}
@Test
public void hello() throws Exception{
mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("HELLO world")));
}
@Test
void contextLoads() {
}
}
Spring Boot的默认配置文件位置为:src/main/resources/application.properties。
#定义web模块的服务端口号
server.port=8888
#指定应用名,这个名字在后续SpringCloud中会被注册为服务名
spring.application.name=hello
book.name=SpringCloudInAction
book.author=ZhaiYongchao
book.desc=${book.author} is writing 《${book.name}}》
package com.example.springbootdemo.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author huwenhao
* @create 2020-10-14 10:03
*/
@Component
public class Book {
@Value("${book.name}")
private String name;
@Value("${book.author}")
private String author;
@Value("${book.desc}")
private String desc;
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", desc='" + desc + '\'' +
'}';
}
public String getName() {
return name;
}
public String getAuthor() {
return author;
}
public String getDesc() {
return desc;
}
public void setName(String name) {
this.name = name;
}
public void setAuthor(String author) {
this.author = author;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
输出结果:
Book{name='SpringCloudInAction', author='ZhaiYongchao', desc='ZhaiYongchao is writing 《SpringCloudInAction}》'}
在一些特殊的情况下,我们会希望有些参数每次被加载的时候不是一个固定的值,比如密钥、服务端口等。
#${random}的配置方式主要有以及几种
#随机字符串
com.example.springbootdemo.blog.value=${random.value}
#随机int
com.example.springbootdemo.blog.number=${random.int}
#随机long
com.example.springbootdemo.blog.bignumber=${random.long}
#10以内的随机数
com.example.springbootdemo.blog.test1=${random.int(10)}
#10-20内的随机数
com.example.springbootdemo.blog.test2=${random.int(10,20)}
@Component
public class Random {
@Value("${com.example.springbootdemo.blog.value}")
private String value;
@Value("${com.example.springbootdemo.blog.number}")
private int number;
@Value("${com.example.springbootdemo.blog.bignumber}")
private long bignumber;
@Value("${com.example.springbootdemo.blog.test1}")
private int test1;
@Value("${com.example.springbootdemo.blog.test2}")
private int test2;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public long getBignumber() {
return bignumber;
}
public void setBignumber(long bignumber) {
this.bignumber = bignumber;
}
public int getTest1() {
return test1;
}
public void setTest1(int test1) {
this.test1 = test1;
}
public int getTest2() {
return test2;
}
public void setTest2(int test2) {
this.test2 = test2;
}
@Override
public String toString() {
return "Random{" +
"value='" + value + '\'' +
", number=" + number +
", bignumber=" + bignumber +
", test1=" + test1 +
", test2=" + test2 +
'}';
}
}
@Test
public void test(){
System.out.println(random.toString());
}
输出结果:
Random{value='725a6a878a6ee8fed0c6e2388c09934f', number=-783760119, bignumber=-4484994478512857702, test1=8, test2=15}
java -jar xxx.jar --server.port=8888
在使用命令行方式启动Spring Boot应用时,连续的两个减号 “–”就是对application。properties 中的属
性值进行赋值的标识。
在实际的工作中,通常一套程序会被应用和安装到几个不同的环境中,比如开发、测试、生产等…
在Spring Boot中,多环境配置的文件名需要满足application-{profile}.properties的格式,其中{profile}对应环境标识:
application.properties文件中通过spring.profiles.active属性来设置,对应{profile}值
application.properties
#定义web模块的服务端口号
server.port=8888
#指定应用名,这个名字在后续SpringCloud中会被注册为服务名
spring.application.name=hello
spring.profiles.active=test
application-test.properties
#定义web模块的服务端口号
server.port=3333
#指定应用名,这个名字在后续SpringCloud中会被注册为服务名
spring.application.name=hello
book.name=SpringCloudInAction
book.author=ZhaiYongchao
book.desc=${book.author} is writing 《${book.name}}》
#${random}的配置方式主要有以及几种
#随机字符串
com.example.springbootdemo.blog.value=${random.value}
#随机int
com.example.springbootdemo.blog.number=${random.int}
#随机long
com.example.springbootdemo.blog.bignumber=${random.long}
#10以内的随机数
com.example.springbootdemo.blog.test1=${random.int(10)}
#10-20内的随机数
com.example.springbootdemo.blog.test2=${random.int(10,20)}
run–>访问:http://localhost:3333/hello
或
总结:
Spring Boot对数据文件的加载顺序
1.在命令行中传入的参数。
2. SPRING_APPLICATION_JSON中的属性。SPRING_APPLICATION_JSON是以JSON格式配置在系统环境变量中的内容。
3. java: comp/env中的JNDI属性。
4. Java的系统属性,可以通过system.getProperties()获得的内容。 5.操作系统的环境变量。 6.通过random .*配置的随机属性。
7.位于当前应用jar包之外,针对不同{profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。
8.位于当前应用jar包之内,针对不同{profile}环境的配置文件内容,例如application-{profile}.properties或是YAML定义的配置文件。
9.位于当前应用jar包之外的application.properties和YAML配置内容。
10.位于当前应用jar包之内的application.properties和YAML配置内容。
11.在@Configuration注解修改的类中,通过@PropertySource注解定义的属性。
12.应用默认属性,使用SpringApplication.setDefaultProperties定义的内容。
优先级按上面的顺序由高到低,数字越小优先级越高。
其中第7项和第9项都是从应用jar包之外读取配置文件,所以,实现外部化配置的原理就是从此切入,为其指定外部配置文件的加载位置来取代jar包之内的配置内容。通过这样的实现,我们的工程在配置中就变得非常干净,只需在本地放置开发需要的配置即可,而不用关心其他环境的配置,由其对应环境的负责人去维护即可。
初识actuator
<!--该模块能够自动的为Spring Boot构建的应用提供一系列用于监控的端点-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
重新启动,访问http://localhost:3333/actuator,效果如下:
// 20201014165312
// http://localhost:3333/actuator
{
"_links": {
"self": {
"href": "http://localhost:3333/actuator",
"templated": false
},
"health": {
"href": "http://localhost:3333/actuator/health",
"templated": false
},
"health-path": {
"href": "http://localhost:3333/actuator/health/{*path}",
"templated": true
},
"info": {
"href": "http://localhost:3333/actuator/info",
"templated": false
}
}
}
访问http://localhost:3333/actuator/health
// 20201014165907
// http://localhost:3333/actuator/health
{
"status": "UP"
}
根据端点的作用,可以将原生端点分为以下三大类。
在服务治理框架中,通常会构建一个注册中心,每个服务单元想注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告诉注册中心,注册中心按照服务名分类组织服务清单。
服务调用方在调用服务提供方接口的时候并不知道具体的服务实例位置。服务调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,一实现对具体服务实例的访问。
比如:现有服务C希望调用服务A,服务C就需要想注册中心发起咨询服务请求,服务注册中心就会将服务A的位置清单返回给服务C,当服务C想要发起调用的时候,便会向该清单中以某种轮询策略去除一个位置来进行服务调用(负载均衡)(这里只是举了一个简单的服务治理逻辑,实际的框架为了性能并不会采用每次都向服务注册中心获取服务的方式,并且不同的应用场景在缓存上和服务剔除等机制上也会有一些不同的实现策略。)
Spring Cloud Eureka使用NetFlix Eureka来实现服务注册与发现,它既包含服务端组件,也包含客户端组件。
Eureka服务端,也称为服务注册中心,
Eureka客户端,主要处理服务的注册与发现,客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性的刷新服务状态。
Eureka Server的高可用实际就是将自己作为服务向其他注册中心注册自己,这样就可以形成一组相互注册的服务注册中心,以实现服务清单的相互同步,达到高可用的效果。
尝试搭建高可用服务注册中心的集群,构建一个双节点的服务注册中心集群。
pom.xml如下:
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>eureka-serverartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-servername>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR8spring-cloud.version>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-start-parentartifactId>
<version>1.3.7.RELEASEversion>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用来进行对话。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
#server.port=1111
#eureka.instance.hostname=localhost
#
##该应用为注册中心,所以设置为false,代表不向注册中心注册自己
#eureka.client.register-with-eureka=false
#
#eureka.client.fetch-registry=false
#eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#
#
#
##eureka服务注册中心名单
#spring.application.name=eureka-server
application-peer1.properties:
spring.application.name=eureka-server
server.port=1111
eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
application-peer2.properties:
spring.application.name=eureka-server
server.port=1112
eureka.instance.hostname=peer2
eureka.client.serviceUrl.defaultZone=http://peer2:1111/eureka/
在/etc/hosts文件中添加对peer1和peer2的转换,让上面配置的host形式serviceUrl能在本地正确的访问到:Windows系统路径为C:\Windows\System32\drivers\etc\hosts
127.0.0.1 peer1
127.0.0.1 peer2
通过spring.profiles.active属性来分别启动peer1和peer2
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer1
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer2
注册服务提供者:
pom.xml
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.4.RELEASEversion>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>hello-serverartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>hello-servername>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR8spring-cloud.version>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-start-parentartifactId>
<version>1.3.7.RELEASEversion>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-eurekaartifactId>
<version>1.4.6.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
package com.example.helloserver.controller;
import com.sun.istack.internal.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.cloud.client.discovery.DiscoveryClient;
@RestController
public class HelloController {
private final Logger logger = Logger.getLogger(getClass());
@Autowired
private DiscoveryClient client;
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public String index(){
// ServiceInstance instance = client.getLocalServiceInstance();
// logger.info("/hello,host:"+instance.getHost()+", service_id:"+
// instance.getServiceId());
logger.info("---------------------------------"+client.getServices());
return "Hello world";
}
}
package com.example.helloserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class HelloServerApplication {
public static void main(String[] args) {
SpringApplication.run(HelloServerApplication.class, args);
}
}
application.properties:
server.port=8080
spring.application.name=hello-service
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka,http://peer2:1112/eureka/
到这里通过简单地配置,我们已经使得该程序注册到Eureka注册中心上,成为服务治理体系下的一个服务,我们已经有了服务注册中心和服务提供者,下面就来尝试构建一个服务消费者,服务消费者主要完成“发现服务”以及“消费服务”。
服务的发现任务是由Eureka的客户端完成的,而服务消费的任务是由Ribbon完成。
Riboon是一个基于HTTP和TCP的客户端负载均衡器,它可以通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到负载均衡的作用。当Ribbon和Eureka联合使用的时候,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心获取服务端列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。
服务消费者:
//通过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,
// 以获得服务发现的能力,同时在该主类中创建RestTemplate的Springbean实例
//通过@LoadBalanced注解开启客户端负载均衡
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
package com.example.consumer.controller;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @author huwenhao
* @create 2020-10-19 15:01
*/
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping(value = "/ribbon-consumer",method = RequestMethod.GET)
public String helloConsumer(){
/**
* getForEntity的第一个参数为我要调用的服务的地址,这里我调用了服务提供者提供的/hello接口,注意这里是通过服务名调用而不是服务地址,如果写成服务地址就没法实现客户端负载均衡了。
* getForEntity第二个参数String.class表示我希望返回的body类型是String
* 拿到返回结果之后,将返回结果遍历打印出来
*/
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",//注意:此处访问的是服务名HELLO-SERVICE,而不是一个具体的地址
String.class).getBody();
}
}
application.properties
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-start-parent</artifactId>
<version>1.3.7.RELEASE</version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<!--ribbon-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
<version>RELEASE</version>
</dependency>
<!--<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
服务注册中心:Eureka提供的服务端,提供服务注册与发现的功能,也就是我们实现的eureka-server。
服务提供者:提供服务的应用,可以使SpringBoot应用,也可以是其他技术平台且遵循Eureka通讯机制的应用,它将自己提供的服务注册到Eureka,以供其他应用发现,也就是我们实现的hello-service应用。
服务消费者:消费在从服务注册中心获取服务列表,从而使消费者可以知道去何处调用其所需要的服务,在上一节中使用了Ribbon来实现服务消费,后续还会介绍Feign的消费方式。
很多时候,客户端及时服务提供者也是服务消费者。
“服务提供者”在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。EurekaServer 接收到这个Rest请求后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key是具体实例名。
在服务注册时,需要确认一下eureka.client.register-with-eureka=true参数是否正确,该值默认为true,如果是false将不会启动注册操作。
如图所示,两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说,它们的信息分别被两个服务注册中心所维护,此时,由于服务注册中心之间互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将fail请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步,通过服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到。
在注册完服务后,服务提供者会维护一个心跳用来持续告诉Eureka Server:“我还活着”,以防止Eureka server 的“剔除任务”将该服务实例从服务列表中排除出去,我们称为“服务续约”
eureka.instance.lease-renewal-interval-in-seconds=30
参数用于定义服务续约任务的调用间隔时间,默认为30s;
eureka.instance.lease-expiration-duration-in-seconds=90
参数用于定义服务失效的时间,默认为90s;
到这里服务注册中心已经注册了一个服务,并且该服务有两个实例,当我们启动服务消费者的时候,它会发送一个Rest请求给服务注册中心,来获取上面注册的服务清单。为了性能考虑,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30s更新一次。
可以通过eureka.client.registry-fetch-interval-seconds=30进行修改。默认为30s;
服务消费者在获取服务清单后,通过服务名可以获取具体提供服务的实例名和该实例的元数据信息。正式因为有这些服务实例的详细信息,所以客户端可以根据自己的需要决定具体调用哪个实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
对于访问实例的选择,Eureka中有Region和Zone的概念,一个Region中可以包含多个Zone,每个服务客户端需要被注册到一个Zone中,所以一个客户端对应一个Region和一个Zone。在进行服务调用的时候,优先访问同一个Zone中的服务提供方,若访问不到,再访问其他的Zone。
在系统运行过程中必然会面临关闭或重启服务的某个实例的情况,在服务关闭期间,我们自然不希望客户端会调用关闭了的实例。所以在客户端程序中,当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务端在接收到请求后,将该服务状态置为下线(DOWN),并把该下线时间传播出去。
有的时候,服务实例并不一定会正常下线,可能由于内存溢出,网络故障等原因使得服务不能正常工作,而服务注册中心并未收到服务下线的请求,为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认60s)将当前清单中超时(默认为90s)没有续约的服务剔除出去。
服务注册到Eureka Server之后,会维护一个心跳连接,告诉Eureka Server自己还活着,Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内低于85%,如果出现低于的情况,Eureka Server 会将当前的实例注册信息保护起来,让实例不会过期,尽可能的保护这些注册信息。但是,在这段保护期间内实例若出现问题,客户端拿到实际已经不存在的服务实例,会出现调用失败的情况。所以客户端必须要有容错机制,比如可以使用请求重试、断路器等机制。
由于本地调试很容易触发注册中心的保护机制,这会使得注册中心维护的服务实例不那么准确,所以我们在本地进行开发时,可以通过eureka.server.enable-self-preservation=false参数来关闭保护机制,以确保注册中心可以将不可用的实例正确剔除。