一.服务注册中心Nacos最佳实践
1.Nacos注册中心简介
背景分析
在微服务中,首先需要面对的问题就是如何查找服务(软件即服务),其次,就是如何在不同的服务之间进行通信?如何更好更方便的管理应用中的每一个服务,如何建立各个服务之间联系的纽带,由此注册中心诞生(例如淘宝网卖家提供服务,买家调用服务)。
市面上常用注册中心有Zookeeper(雅虎Apache),Eureka(Netfix),Nacos(Alibaba),Consul(Google),那他们分别都有什么特点,我们如何进行选型呢?我们主要从社区活跃度,稳定性,功能,性能等方面进行考虑即可.本次微服务的学习,我们选择Nacos,它很好的支持了阿里的双11活动,不仅可以做注册中心,还可以作为配置中心,稳定性和性能都很好.
Nacos概述
Nacos(DynamicNaming and Configuration Service)是一个应用于服务注册与发现、配置管理的平台。它孵化于阿里巴巴,成长于十年双十一的洪峰考验,沉淀了简单易用、稳定可靠、性能卓越的核心竞争力。其官网地址如下:
https://nacos.io/zh-cn/docs/quick-start.html
构建Nacos服务
准备工作
第一:确保你电脑已配置JAVA_HOME环境变量(Nacos启动时需要),例如:
第二:确保你的MySQL版本为5.7以上(MariaDB10.5以上),例如
在这里插入图片描述
下载与安装
第一步:Nacos下载,可在浏览器直接输入如下地址:
https://github.com/alibaba/nacos/releases
第二步:选择对应版本,直接下载,如图所示:
在这里插入图片描述
第三步:解压Nacos(最好不要解压到中文目录下),其目录结构如下:
初始化配置
第一步:登陆mysql,执行老师发给同学们的sql脚本。例如,我们可以使用mysql自带客户端,在命令行首先登录mysql,然后执行如下指令:
source d:/nacos-mysql.sql
执行成功以后,会创建一个nacos_config数据库,打开数据库会看到一些表,例如;
说明:在执行此文件时,要求mysql的版本大于5.7版本(MariaDB最好10.5.11),否则会出现如下错误:
第二步:打开/conf/application.properties里打开默认配置,并基于你当前环境配置要连接的数据库,连接数据库时使用的用户名和密码(假如前面有"#"要将其去掉):
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
服务启动与访问
第一步:启动Nacos服务。
Linux/Unix/Mac启动命令(standalone代表着单机模式运行,非集群模式):
./startup.sh -m standalone
Windows启动命令(standalone代表着单机模式运行,非集群模式):
startup.cmd -m standalone
说明:
1)执行执行令时要么配置环境变量,要么直接在nacos/bin目录下去执行.
2)nacos启动时需要本地环境变量中配置了JAVA_HOME(对应jdk的安装目录),
3)一定要确保你连接的数据库(nacos_config)是存在的.
4)假如所有的配置都正确,还连不上,检查一下你有几个数据库(mysql,…)
Idea中启动Nacos
第一步:打开服务编辑配置,例如:
第二步:添加Shell Script,例如:
第三步:添加nacos相关信息,例如:
第二步:访问Nacos服务。
打开浏览器,输入http://localhost:8848/nacos地址,出现如下登陆页面:
其中,默认账号密码为nacos/nacos.
2.服务注册与调用入门(重点)
业务描述
创建两个项目Module分别为服务提供者和服务消费者,两者都要注册到NacosServer中(这个server本质上就是一个web服务,端口默认为8848),然后服务提供者可以为服务消费者提供远端调用服务(例如支付服务为服务提供方,订单服务为服务消费方),如图所示:
生产者服务创建及注册
第一步:创建服务提供者工程(module名为sca-provider),继承parent工程(01-sca),其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">
<!--继承父工程-->
<parent>
<artifactId>01-sca</artifactId>
<groupId>com.cy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!--工程坐标-->
<artifactId>sca-provider</artifactId>
<!--创建maven模块时自带的,用于指定maven的中java类的编译和运行版本
你的工程中是否默认有这个maven compiler由你的maven本决定,有些版本没有-->
<!--<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>-->
<dependencies>
<!--web服务依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos服务的注册和发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
第二步:创建并修改配置文件application.yml(或者application.properties),实现服务注册,关键代码如下:
#服务端口
#server.port=8081
#服务名
#spring.application.name=service-provider
#服务注册和发现地址
#spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server:
port: 8081 #服务端口
spring:
application:
name: sca-provider #服务名
cloud:
nacos: #服务启动时会向nacos发送心跳包(5秒发送一次)15秒没收到,会变成非健康状态,(503就是服务不可用) 30秒没收到 ,变成死亡状态,key为服务名,只能有一个,v是一个实例list集合,
discovery: #服务的注册和发展
server-addr: localhost:8848 #nacos server
注意:服务名不要使用下划线(“_”),应使用横杠(“-”),这是规则。
第三步:创建启动类,并定义处理请求的控制层对象和方法,关键代码如下:
package com.cy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class,args);
}
/**定义Controller对象(这个对象在spring mvc中给他的定义是handler),
* 基于此对象处理客户端的请求*/
@RestController
public class ProviderController{
//@value用于读取application.yml配置的内容
//要读取的配置需要写在${}这个表达式中,
// 8080为没有读到server.port的值时,给定的默认值
@Value("${server.port:8080}")
private String provider;
/**
* 基于此方法实现一个字符串的问题
* echo:回显的意思
* rest:一种软件架构编码风格,可以基于这种风格定义url
* 访问:http://localhost:8081/provider/echo/nacos
*/
@GetMapping("/provider/echo/{msg}")//{msg}和@PathVariable("msg")中的名字相同
public String doRestEcho1(
// @PathVariable String msg){ //jdk8 之后的写法
@PathVariable("msg") String msg){//考虑兼容性,jdk5和6 所以@PathVariable加了("msg")
return provider + "Say Hello" + msg;
}
}
}
第四步:启动启动类,然后刷新nacos服务,检测是否服务注册成功,如图所示:
第六步:打开浏览器,输入http://localhost:8081/provider/echo/msaioicdcd (msg写什么都可以),然后进行访问。
消费者服务发现及调用
第一步:创建服务消费者(module名为sca-consumer),继承parent工程(01-sca),其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">
<parent>
<artifactId>01-sca</artifactId>
<groupId>com.cy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sca-consumer</artifactId>
<dependencies>
<!--Spring Boot Web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring Cloud Alibaba nacos 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
</project>
第二步:修改配置文件application.yml,关键代码如下:
server:
port: 8090 #默认为8080
spring:
application:
name: sca-consumer #假如做服务注册,必须写
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos server地址,从哪里去查找服务
第三步:创建启动类并实现服务消费,关键代码如下:
package com.cy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.sql.SQLOutput;
/**
* 服务消费方对象的启动类
* 业务描述:
* 当客户端(当浏览器、手机app)向服务消费方发起请求时,服务消费方调用服务提供方的api,进而获取服务服务提供方的数据
* 例如:
* 我们访问一个订单模块数据(例如我的订单),订单模块中还要呈现商品信息
*/
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Autowired
private RestTemplate restTemplate;
/**
* 构建restTemplate对象,并将此对象交给spring管理
* 后续我们会通过此对象进行远程服务调用
* @return
*/
@Bean//默认是方法名
//@Bean("rt")起名
public RestTemplate restTemplate(){
System.out.println("=================restTemplate===");
return new RestTemplate();
}
@RestController
public class ConsumerController{
@Value("${spring.application.name}")
private String appName;
//http://ip:port/consumer/doRestEcho1
@GetMapping("/consumer/doRestEcho1")
public String doRestEcho1(){
System.out.println("======================doRestEcho1=================");
//调用服务提供方API(ip:port/path)
//1.定义要调用API
//restTemplate
String url =
"http://localhost:8081/provider/echo/"+appName;
//2.谁去访问这个API? restTemplate
return restTemplate.getForObject(url,String.class);
}
//封装
//1)狭义:属性私有化,方法能公开则公开
//2)广义:一个系统系统有哪些服务构成,一个服务有哪些模块构成,一个模块由哪些对象构成,一个对象由哪些属性构成
//API网关
}
}
第四步:启动消费者服务,并在浏览器输入http://localhost:8090/consumer/doRestEcho1地址进行访问,假如访问成功会出现,如图所示效果:
小节面试分析
为什么要将服务注册到nacos?(为了更好的查找这些服务)
在Nacos中服务提供者是如何向Nacos注册中心(Registry)续约的?(5秒心跳)
对于Nacos服务来讲它是如何判定服务实例的状态?(检测心跳包,15,30)
#服务启动时会向nacos发送心跳包(5秒发送一次),15秒没收到,会变成非健康状态,(503就是服务不可用) 30秒没收到 ,变成死亡状态,key为服务名,只能有一个,v是一个实例list集合,
服务启动时如何找到服务启动注册配置类?(NacosNamingService)
服务消费方是如何调用服务提供方的服务的?(RestTemplate)
3.服务负载均衡设计及实现(重点)
入门案例
一个服务实例可以处理请求是有限的,假如服务实例的并发访问比较大,我们会启动多个服务实例,让这些服务实例采用一定策略均衡(轮询,权重,随机,hash等)的处理并发请求,在Nacos中服务的负载均衡(Nacos客户端负载均衡)是如何应用的?
第一步:修改ConsumerController类,注入LoadBalancerClient对象,并添加doRestLoadBalancerClientEcho2方法,然后进行服务访问.
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/consumer/doRestEcho2")
public String doRestEcho2(){
//1.从注册中心获取服务实例
ServiceInstance instance =
loadBalancerClient
.choose("sca-provider");
//2.基于RestTemplate进行服务实例对象调用
String ip = instance.getHost();//ip
int port = instance.getPort();//port
//String url = "http://"+ip+":"+port+"/provider/echo/"+appName;
String url = String.format("http://%s:%s/provider/echo/%s",
ip,port,appName);
return restTemplate.getForObject(
url,
String.class);
}
第二步:打开Idea服务启动配置,如图所示:
第三步:修改并发运行选项(假如没有找到这个选项我们需要通过搜索引擎基于组合查询的方法,去找到对应的解决方案,例如搜索 idea allow parallel run),如图所示:
第四步:修改sca-provider的配置文件端口,分别以8081,8082端口方式进行启动。
server:
port: 8081
spring:
application:
name: sca-provider
cloud:
nacos:
server-addr: localhost:8848
启动成功以后,访问nacos的服务列表,检测服务是否成功注册,如图所示:
第五步:启动sca-consumer项目模块,打开浏览器,输入如下网址进行反复服务访问:
http://localhost:8090/consumer/doRestEcho02
然后会发现sca-provider的两个服务都可以处理sca-consumer的请求。
这里多个实例并发提供服务的方式为负载均衡,这里的负载均衡实现默认是因为Nacos集成了Ribbon来实现的,Ribbon配合RestTemplate,可以非常容易的实现服务之间的访问。Ribbon是Spring Cloud核心组件之一,它提供的最重要的功能就是客户端的负载均衡(客户端可以采用一定算法,例如轮询访问,访问服务端实例信息),这个功能可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡方式的服务调用。
@LoadBalanced
当使用RestTemplate进行远程服务调用时,假如需要负载均衡,可以在RestTemplate对象构建时,使用@LoadBalanced对构建RestTemplate的方法进行修饰,例如在ConsumerApplication中构建RestTemplate对象:
@Bean
@LoadBalanced
public RestTemplate loadBalancerRestTemplate(){
return new RestTemplate();
}
在需要RestTemplate实现负载均衡调用的地方进行依赖注入.例如在ConsumerController类中添加loadBalancedRestTemplate属性
@Autowired
private RestTemplate loadBalancerRestTemplate;
接下来,可以在对应的服务端调用方的方法内,基于RestTemplate借助服务名进行服务调用, 例如:
public String doRestEcho3(){
//定义url
String url =
String.format("http://%s/provider/echo/%s","sca-provider",appName);
//服务调用
return loadBalancerRestTemplate.getForObject(
url,
String.class);
}
RestTemplate在发送请求的时候会被LoadBalancerInterceptor拦截,它的作用就是用于RestTemplate的负载均衡,LoadBalancerInterceptor将负载均衡的核心逻辑交给了loadBalancer,核心代码如下所示(了解):
public ClientHttpResponse intercept(final HttpRequest request,
final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
return this.loadBalancer.execute(serviceName,
requestFactory.createRequest(request, body, execution));
}
@LoadBalanced注解是属于Spring,而不是Ribbon的,Spring在初始化容器的时候,如果检测到Bean被@LoadBalanced注解,Spring会为其设置LoadBalancerInterceptor的拦截器。
Ribbon负载均衡策略(了解)
基于Ribbon方式的负载均衡,Netflix默认提供了七种负载均衡策略,对于SpringCloud Alibaba解决方案中又提供了NacosRule策略,默认的负载均衡策略是轮训策略。如图所示:
当系统提供的负载均衡策略不能满足我们需求时,我们还可以基于IRule接口自己定义策略.
小节面试分析
@Bean注解的作用?(一般用于配置类内部,描述相关方法,用于告诉spring此方法的返回值要交给spring管理,bean的名字默认为方法名,假如需要指定名字可以@Bean(“bean的名字”),最多的应用场景是整合第三方的资源-对象)
@Autowired注解的作用?(此注解用于描述属性,构造方法,set方法等,用于告诉spring框架,按找一定的规则为属性进行DI操作,默认按属性,方法参数类型查找对应的对象,假如只找到一个,则直接注入,类型多个时还会按照属性名或方法参数名进行值的注入,假如名字也不同,就出报错.)
Nacos中的负责均衡底层是如何实现的?(通过Ribbon实现,Ribbon中定义了一些负载均衡算法,然后基于这些算法从服务实例中获取一个实例为消费方法提供服务)
Ribbon 是什么?(Netflix公司提供的负载均衡客户端,一般应用于服务的消费方法)
Ribbon 可以解决什么问题? (基于负载均衡策略进行服务调用, 所有策略都会实现IRule接口)
Ribbon 内置的负载策略都有哪些?(8种,可以通过查看IRule接口的实现类进行分析)
@LoadBalanced的作用是什么?(描述RestTemplate对象,用于告诉Spring框架,在使用RestTempalte进行服务调用时,这个调用过程会被一个拦截器进行拦截,然后在拦截器内部,启动负载均衡策略。)
我们可以自己定义负载均衡策略吗?(可以,基于IRule接口进行策略定义,也可以参考NacosRule进行实现)
4.基于Feign的远程服务调用(重点)
背景分析
服务消费方基于rest方式请求服务提供方的服务时,一种直接的方式就是自己拼接url,拼接参数然后实现服务调用,但每次服务调用都需要这样拼接,代码量复杂且不易维护,此时Feign诞生。
Feign是什么
Feign 是一种声明式Web服务客户端,底层封装了对Rest技术的应用,通过Feign可以简化服务消费方对远程服务提供方法的调用实现。如图所示:
在这里插入图片描述
Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由一些社区进行维护,更名为 OpenFeign。
Feign应用实践(掌握)
第一步:在服务消费方pom.xml,添加项目依赖(SpringCloud团队基于OpenFeign研发了starter),代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:在启动类上添加@EnableFeignClients注解,代码如下:
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {…}
第三步:定义Http请求API,基于此API借助OpenFeign访问远端服务,代码如下:
@FeignClient(name="sca-provider")
public interface RemoteProviderService {
@GetMapping("/provider/echo/{msg}")
public String echoMessage(
@PathVariable("msg") String msg);
}//consumer.controller--->Fegin interface-->remote call
其中,@FeignClient描述的接口底层会为其创建实现类。
第四步:创建FeignConsumerController中并添加feign访问,代码如下:
package com.cy.controller;
import com.cy.service.RemoteProviderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/consumer")
public class FeginConsumerController {
@Autowired
private RemoteProviderService remoteProviderService;
/**
* 执行远程服务调用
* 访问:browser-->consumer-->provider
* 1)浏览器(browser)访问:
* http://localhost:8090/consumer/echo/cgb
* 2)consumer
* http://sca-provider/provider/echo/cgb
* @param msg
* @return
*/
@GetMapping("/echo/{msg}")
public String doRestEcho(@PathVariable("msg") String msg){
return remoteProviderService.echoMessage(msg);
}
}
第五步:启动消费者服务,在浏览器中直接通过feign客户端进行访问,如图所示(反复刷新检测其响应结果):
Feign配置进阶实践
一个服务提供方通常会提供很多资源服务,服务消费方基于同一个服务提供方写了很多服务调用接口,此时假如没有指定contextId,服务
启动就会失败,例如假如在服务消费方再添加一个如下接口,消费方启动时就会启动失败,例如:
@FeignClient(name="sca-provider")
public interface RemoteProviderService {
@GetMapping("/provider/echo/{msg}")
String echoMessage(
@PathVariable("msg") String msg);
}//consumer.controller--->Fegin interface-->remote call
其启动异常:
在这里插入代码片
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
此时我们需要为远程调用服务接口指定一个contextId,作为远程调用服务的唯一标识即可,例如:
@FeignClient(name="sca-provider",
contextId = "remoteProviderService")
public interface RemoteProviderService {
@GetMapping("/provider/echo/{msg}")
String echoMessage(
@PathVariable("msg") String msg);
}//consumer.controller--->Fegin interface-->remote call
还有,当我们在进行远程服务调用时,假如调用的服务突然不可用了或者调用过程超时了,怎么办呢?一般服务消费端会给出具体的容错方案,例如:
第一步:定义FallbackFactory接口的实现,代码如下:
package com.cy.service.factory;
import com.cy.service.RemoteProviderService;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ProviderFallbackFactory
implements FallbackFactory<RemoteProviderService> {
/**
* org.slf4j.Logge是java中的日志规范,定义了一组接口
* org.slf4j.Logger这个接口的实现有log4j,logback
*/
// private static Logger log =
// LoggerFactory.getLogger(ProviderFallbackFactory.class);
@Override
public RemoteProviderService create(Throwable throwable) {
log.error("用户调用服务失败:{}",throwable.getMessage());
/*return new RemoteProviderService() {
@Override
public String echoMessage(String msg) {
//...给运维人员发送消息
return "服务忙,稍等片刻再访问";
}
};*/
//如上写法的简化形式如下:借助了jak8中的lambda表达式
/*return (msg) -> {//lambda(jdk中一个表达式)
//..给运维人员发消息
return "服务忙,稍等片刻再访问";
};*/
return msg->"服务忙,稍等片刻再访问";
}
}
第二步:在Feign访问接口中应用FallbackFactory对象,例如:
package com.cy.service;
import com.cy.service.factory.ProviderFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 只写方法的声明的调用
*
* 定义用于实现远程provider服务调用的service接口
* 1)@FeignClient用于描述远程服务调用接口
* 2)name="sca-provider"为你远程要调用的服务名
* 3)contextId为当前bean的名称,假如没有指定contextId,
* 默认会采用@FeignClient注解中name属性指定的名字作为bean的名字
* 如果有多个调用这个,必须写contextId
* 4)
*/
@FeignClient(name="sca-provider",
contextId = "remoteProviderService",
fallbackFactory = ProviderFallbackFactory.class)
public interface RemoteProviderService {
@GetMapping("/provider/echo/{msg}")
String echoMessage(
@PathVariable("msg") String msg);
}//consumer.controller--->Fegin interface-->remote call
第三步:在配置文件application.yml中添加如下配置,启动feign方式调用时的服务中断处理机制.
feign:
hystrix:
enabled: true #开启fegin方式调用,服务超时等问题,默认值为false
第四步:在服务提供方对应的方法中添加Thread.sleep(500000)模拟耗时操作,然后启动服务进行访问测试.
Feign 调用过程分析(了解)
Feign应用过程分析(底层逻辑先了解):
1)通过 @EnableFeignCleints 注解告诉springcloud,启动 Feign Starter 组件。
2) Feign Starter 在项目启动过程中注册全局配置,扫描包下所由@FeignClient注解描述的接口,然后由系统底层创建接口实现类(JDK代理类),并构建类的对象,然后交给spring管理(注册 IOC 容器)。
3) 接口被调用时被动态代理类逻辑拦截,将 @FeignClient 请求信息通过编码器生成 Request对象,基于此对象进行远程过程调用。
4) 请求对象经Ribbon进行负载均衡,挑选出一个健康的 Server 实例(instance)。
5) 通过 Client 携带 Request 调用远端服务返回请求响应。
6) 通过解码器生成 Response 返回客户端,将信息流解析成为接口返回数据。
小节面试分析
为什么使用feign?(基于Feign可以更加友好的实现服务调用,简化服务消费方对服务提供方方法的调用)。
@FeignClient注解的作用是什么?(告诉Feign Starter,在项目启动时,为此注解描述的接口创建实现类-代理类)
Feign方式的调用,底层负载均衡是如何实现的?(Ribbon)
@EnableFeignCleints 注解的作用是什么?(描述配置类,例如启动类)
5.总结(Summary)
重难点分析
何为注册中心?(用于记录服务信息的一个web服务,例如淘宝平台,滴滴平台,美团外卖平台,……)
注册中心的核心对象?(服务提供方,服务消费方,注册中心-Registry)
市面上常用注册中心?(Google-Consul,Alibaba-Nacos,…)
微服务架构下项目的构建过程?(聚合工程)
Nacos安装、启动、服务的注册、发现机制以及实现过程?
Feign的基本应用以及底层底层调用原理?
FAQ分析
Nacos是什么,提供了什么特性(服务的注册、发现、配置)?
你为什么会选择Nacos?(活跃度、稳定、性能、学习成本)
Nacos的官网?(nacos.io)
Nacos在github的源码?(github.com/alibaba/nacos)
Nacos在windows环境下安装?(解压即可使用)
Nacos在windows中的的初步配置?(application.properties访问数据库的数据源)
Nacos服务注册的基本过程?(服务启动时发送web请求)
Nacos服务消费的基本过程?(服务启动时获取服务实例,然后调用服务)
Nacos服务负载均衡逻辑及设计实现?(Ribbon)
注册中心的核心数据是什么?(服务的名字和它对应的网络地址)
注册中心中心核心数据的存取为什么会采用读写锁?(底层安全和性能)
Nacos健康检查的方式?(基于心跳包机制进行实现)
Nacos是如何保证高可用的?(重试,本地缓存、集群)
Feign是什么,它的应用是怎样的,feign应用过程中的代理对象是如何创建的(JDK)?
Feign方式的调用过程,其负载均衡是如何实现?(Ribbon)
Bug分析
404
400
405
415
500
问题处理
项目克隆问题
克隆下的项目导入以后是如下问题:
这个问题,可以右键项目的pom.xml文件,然后执行如下操作,将项目添加到maven区,例如:
JDK 配置问题
打开项目代码,是如下问题,例如
如上问题,可以打开项目结构,然后配置JDK,例如
Maven配置编译问题
我们使用的maven一般默认编译版本会采用JDK1.5,假如你希望为JDK8,可以在当前工程或父工程中添加如下,代码:
<build>
<plugins>
<!--修改maven编译版本-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
我们也可以在每个工程的properties元素中设置指定当前工程的编译版本,例如:
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
服务注册异常(NacosException)
启动服务时,出现如下问题,例如:
服务配置读取问题
配置文件格式问题
无法访问此网站
pom.xml有删除横线
访问404异常
访问500异常
例如,打开后台,假如出现了如下问题,就是你远程调用的服务url应该是写错了
例如:假如你的Consumer服务调用Provider服务,但是Provider服务没有启动就会出现如下问题:
端口占用问题
服务调用案例分析
服务ID问题分析
项目配置文件中的spring.application.name属性值为服务id,服务注册到nacos以后的服务名就是这个id,后续可以基于这个服务id找到具体的服务实例,然后进行调用.
二.Nacos服务配置中心应用实践
1.配置中心简介
背景分析
我们知道,除了代码之外,软件还有一些配置信息,比如数据库的用户名和密码,还有一些我们不想写死在代码里的东西,例如像线程池大小、队列长度等运行参数,以及日志级别、算法策略等, 还有一些是软件运行环境的参数,如Java 的内存大小,应用启动的参数,包括操作系统的一些 参数配置…… 所有这些东西,我们都叫做软件配置。以前,我们把软件配置写在一个配置文件中,就像 Windows 下的 ini 文件,或是 Linux 下的 conf 文件。然而,在分布式系统下,这样的方式就变得非常不好管理,并容易出错。假如生产环境下,项目现在正在运行,此时修改了配置文件,我们需要让这些配置生效,通常的做法是不是要重启服务。但重启是不是会带来系统服务短时间的暂停,从而影响用户体验呢,还有可能会带来经济上的很大损失(例如双11重启下服务)。基于这样的背景,配置中心诞生了。
配置中心概述
配置中心最基础的功能就是存储一个键值对,用户发布一个配置(configKey),然后客户端获取这个配置项(configValue);进阶的功能就是当某个配置项发生变更时,不停机就可以动态刷新服务内部的配置项,例如,在生产环境上我们可能把我们的日志级别调整为 error 级别,但是,在系统出问题我们希望对它 debug 的时候,我们需要动态的调整系统的行为的能力,把日志级别调整为 debug 级别。还有,当你设计一个电商系统时,设计大促预案一定会考虑,同时涌进来超过一亿人并发访问的时候,假如系统是扛不住的,你会怎么办,在这个过程中我们一般会采用限流,降级。系统的限流和降级本质上来讲就是从日常的运行态切换到大促态的一个行为的动态调整,这个本身天然就是配置起到作用的一个相应的场景。
配置中心的选型
在面向分布式的微服务系统中,如何通过更高效的配置管理方式,实现微服务系统架构持续“无痛”的演进,并动态调整和控制系统的运行时态,配置中心的选型和设计起着举足轻重的作用。市场上主流配置中心有Apollo(携程开源),nacos(阿里开源),Spring Cloud Config(Spring Cloud 全家桶成员)。我们在对这些配置中心进行选型时重点要从产品功能、使用体验、实施过程和性能等方面进行综合考量。本次课程我们选择nacos,此组件不仅提供了注册中心,还具备配置中心的功能。
小节面试分析
什么是配置中心?(存储项目配置信息的一个服务)
为什么要使用配置中心?(集中管理配置信息,动态发布配置信息)
市场上有哪些主流的配置中心?(Apollo,nacos,……)
配置文件加载顺序
拉取(Pull)配置信息
Nacos配置中心模型
2.Nacos配置快速入门
业务描述
基于nacos配置中心,实现日志级别的动态配置,然后基于日志级别动态调整服务中日志信息的输出.例如:
@GetMapping("/provider/echo/{msg}")
public String doRestEcho1(
@PathVariable("msg") String msg) throws InterruptedException {
//需求:通过配置中心中动态日志级别的配置,控制日志信息的输出
log.info("doRestEcho1 start {}",//{} 表示占位符
System.currentTimeMillis());
return server+" say hello "+msg;
}
其中:log对象可以自己基于SLF4J API创建,也可以基于@Slf4j对象指定类进行描述,来创建日志对象.
添加依赖
在已有的sca-provider项目中添加如配置依赖,例如:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
修改配置文件
将项目中的application.yml的名字修改为bootstrap.yml配置文件(启动优先级最高),代码如下:
#服务端口
#server.port=8081
#服务名
#spring.application.name=service-provider
#服务注册和发现地址
#spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
server:
port: 8081 #服务端口
spring:
application:
name: sca-provider #服务名
cloud:
nacos: #服务启动时会向nacos发送心跳包(5秒发送一次),15秒没收到,会变成非健康状态,(503就是服务不可用) 30秒没收到 ,变成死亡状态,key为服务名,只能有一个,v是一个实例list集合,
discovery: #服务的注册和发展
server-addr: localhost:8848 #nacos server
config: #服务配置(将可能经常变化的数据写到配置中心)
server-addr: localhost:8848
file-extension: yml #配置中心指定的配置格式扩展名
#日志配置
#logging:
# level:
# com.cy: eroor
新建Nacos配置
打开nacos配置中心,新建配置,如图所示:
其中Data ID的值要与bootstrap.yml中定义的spring.application.name的值相同(服务名-假如有多个服务一般会创建多个配置实例,不同服务对应不同的配置实例)。
配置中心动态发布测试
启动sca-provider服务,然后打开浏览器,最/provider/echo/{msg}这个路径代表的资源进行访问,检测idea控制台日志输出.
访问成功后,打开idea控制台,检测日志输出.然后再打开nacos控制台动态更新日志级别,再访问资源并检测后台日志输出.
Nacos 动态刷新机制分析
对于nacos配置中心而言,有系统内部对配置变化的感知,还有外部系统对配置的感知,假如我们系统在浏览器中能看到日志级别的变化,该如何实现呢?我们现在来实现一个案例.
第一步:创建ProviderLogController中添加一个获取日志级别(debug<info<warn<error)的方法,代码如下:
@RestController
public class ProviderLogController{
private static final Logger log= LoggerFactory.getLogger(ProviderLogController.class);
@Value("${logging.level.com.jt:error}")
private String logLevel;
@GetMapping("/provider/doGetLogLevel")
public String doGetLogLevel(){
log.trace("==log.trace==");//跟踪
log.debug("==log.debug==");//调试
log.info("==log.info==");//常规信息
log.warn("==log.warn==");//警告
log.error("==log.error==");//错误信息
return "log level is "+logLevel;
}
}
第二步:在类的上面添加一个@RefreshScope注解
@RefreshScope
@RestController
public class ProviderLogController{
//.....
}
打开nacos配置中心,新建配置,如图所示:
其中,@RefreshScope的作用是在配置中心的相关配置发生变化以后,能够及时看到类中属性值的更新(底层是通过重新创建Controller对象的方式,对属性进行了重新初始化),Controller编写好以后,启动配置中心服务,然后进行访问测试。,打开浏览器直接在地址栏输入http://localhost:8081/provider/doGetLogLevel,检测输出结果是否为我们配置中配置的信息,如图所示。
说明,假如对配置的信息访问不到,请检测项目配置文件的名字是否为bootstrap.yml,检查配置文件中spring.application.name属性的值是否与配置中心的data-id名相同,还有你读取的配置信息缩进以及空格写的格式是否正确.
小节面试分析
- 配置中心一般都会配置什么内容?(可能会经常变化的配置信息,例如连接池,日志、线程池、限流熔断规则)
- 什么信息一般不会写到配置中心?(服务端口,服务名,服务的注册地址,配置中心)
- 项目中为什么要定义bootstrap.yml文件?(此文件被读取的优先级比较高,可以在服务启动时读取配置中心的数据)
- Nacos配置中心宕机了,我们的服务还可以读取到配置信息吗?(可以从内存,客户端获取了配置中心的配置信息以后,会将配置信息在本地内存中存储一份.)
- 微服务应用中我们的客户端如何获取配置中心的信息?(我们的服务一般首先会从内存读取配置信息,同时我们的微服务还可以定时向nacos配置中心发请求拉取(pull)更新的配置信息)
- 微服务应用中客户端如何感知配置中心数据变化?(当数据发生变化时,nacos找到它维护的客户端,然后通知客户端去获取更新的数据,客户端获取数据以后更新本地内存,并在下次访问资源时,刷新@Value注解描述的属性值,但是需要借助@RefreshScope注解对属性所在的类进行描述)
- 服务启动后没有从配置中心获取我们的配置数据是什么原因?(依赖,配置文件名字bootstrap.yml,配置中心的dataId名字是否正确,分组是否正确,配置的名字是否正确,缩进关系是否正确,假如是动态发布,类上是否有@RefreshScope注解)
- 你项目中使用的日志规范是什么?(SLF4J)
- 你了解项目中的日志级别吗?(debug,info,error,…,可以基于日志级别控制日志的输出)
三.Nacos配置管理模型
概述
Nacos 配置管理模型由三部分构成,如图所示:
其中:
Namespace:命名空间,对不同的环境进⾏隔离,⽐如隔离开发环境和⽣产环境。
Group:分组,将若⼲个服务或者若⼲个配置集归为⼀组。
Service/DataId:某⼀个服务或配置集,一般对应一个配置文件。
命名空间设计
Nacos中的命名空间一般用于配置隔离,这种命名空间的定义一般会按照环境(开发,生产等环境)进行设计和实现.我们默认创建的配置都存储到了public命名空间,如图所示:
创建新的开发环境并定义其配置,然后从开发环境的配置中读取配置信息,该如何实现呢?
第一步:创建新命名空间,如图所示:
命名空间成功创建以后,会在如下列表进行呈现。
在指定命名空间下添加配置,也可以直接取配置列表中克隆,例如:
克隆成功以后,我们会发现在指定的命名空间中有了我们克隆的配置,如图所示:
此时我们修改dev命名空间中Data Id的sca-provider配置,如图所示:
修改项目module中的配置文件bootstrap.yml,添加如下配置,关键代码如下:
spring:
cloud:
nacos:
config:
namespace: 6058fd3f-0d4d-44f2-85d6-5fc7d2348046
其中,namespace后面的字符串为命名空间的id,可直接从命名空间列表中进行拷贝.
重启服务,继续刷新http://localhost:8081/config/doGetLogLevel地址。检测输出,看看输出的内容是什么,是否为dev命名空间下配置的内容,如图所示:
我们还可以创建生产环境,依次类推进行设计和实现即可。
分组设计及实现
当我们在指定命名空间下,按环境或服务做好了配置以后,有时还需要基于服务做分组配置,例如,一个服务在不同时间节点(节假日,活动等)切换不同的配置,可以在新建配置时指定分组名称,如图所示:
配置发布以后,修改boostrap.yml配置类,在其内部指定我们刚刚创建的分组,代码如下:
server:
port: 8070
spring:
application:
name: nacos-config
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
group: DEV_GROUP_51 # Group, default is DEFAULT_GROUP
file-extension: yml # Configure the data format of the content, default to properties
namespace: 5c27fe4a-1141-4836-a14e-cbac77fb2130
在ProviderThreadController类中添加属性和方法用于获取和输出DEV_GROUP_ZQ配置中设置的线程数,代码如下:
DEV_GROUP_ZQ代码:
server:
tomcat:
threads:
max: 500
ProviderThreadController代码:
package com.cy.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope
@RestController
public class ProviderThreadController {
@Value("${server.tomcat.threads.max:200}")
private Integer maxThread;
@RequestMapping("/provider/doGetMaxThread")
public String doGetMaxThread(){
return "server.threads.max is "+maxThread;
}
}
然后重启服务,进行测试,检测内容输出,如图所示:
共享配置设计及读取
当同一个namespace的多个配置文件中都有相同配置时,可以对这些配置进行提取,然后存储到nacos配置中心的一个或多个指定配置文件,哪个微服务需要,就在服务的配置中设置读取即可。例如:
第一步:在nacos中创建一个共享配置文件,例如:
第二步:在指定的微服务配置文件(bootstrap.yml)中设置对共享配置文件的读
取,例如:
# 共享配置
shared-configs[0]:
data-id: app-public-dev.yml
group: DEFAULT_GROUP
refresh: true #默认false
第三步:在ProviderPageController业务类中读取和应用共享配置即可,例如:
package com.cy.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RefreshScope
@RestController
public class ProviderPageController {
@Value("${page.pageSize:10}")
private Integer pageSize;
@GetMapping("/provider/doGetPageSize")
public String doGetPageSize(){
//return String.format()
return "page size is "+pageSize;
}
}
第四步:启动服务进行访问测试。
小节面试分析
Nacos配置管理模型的背景?(环境不同配置不同)
Nacos配置中的管理模型是怎样的?(namespace,group,service/data-id)
Nacos客户端(微服务)是否可以读取共享配置?(可以)
总结(Summary)
重难点分析
配置中心的选型。(市场活跃度、稳定性、性能、易用)
Nacos配置中心基本应用。(新建,修改、删除配置以后,在Nacos客户端应用配置)
配置管理模型应用。(namespace,group,service/dataId)
Nacos配置变更的动态感知。(底层原理分析)
FAQ分析
为什么需要配置中心?(动态管理发布配置,无需重启服务,更好保证服务的可用)
配置中一般要配置什么内容?(经常变化的配置数据-日志级别,线程池、连接池、…)
市面上有哪些主流配置中心?(Nacos,….)
配置中心选型时要重点考虑哪些因素?(市场活跃度、稳定性、性能、易用)
Nacos客户端(微服务业务)如何动态感知配置中心数据变化的?(nacos2.0之前nacos客户端采用长轮询机制每隔30秒拉取nacos配置信息.)
Nacos配置管理模型是怎样的?(命名空间-namespace,分组-group,服务实例-dataId)
Bug分析
问题处理
Tomcat 请求处理分析
Java线程池构成分析
线程池任务执行过程
线程拒绝执行异常
Maven Helper插件应用
客户端调用异常(ClientException)
**@PathVariable 注解在@FeignClient中应用
**
依赖注入失败(Bean Not Found)
请求映射路径重复定义
@FeignClient注解名字问题
Feign远程调用超时
图解服务调用方案
这张图描述了远程服务调用的几中方式:
第一种:服务比较少,例如就两个服务,一个服务消费,一个服务提供,就不需要注册中心,不需要负载均衡.
第二种:并发比较大,服务服务比较多,我们需要管理服务,就需要注册中心,我们还需要服务间的负载均衡.但代码编写的复杂多相对高一些,我们需要自己获取ip,获取端口,拼接字符串等.
第三种:我们要基于第二种进行代码简化,底层提供了一种拦截器,把基于服务名获取服务实例的过程在拦截器中做了封装,简化了代码的开发.但是加了拦截器多少会在性能少有一点损耗.
第四种方式主要是从代码结构上做一个挑战,我们前面三种基于RestTemplate进行服务调用,本身属于一种远程服务调用业务,能够将这种业务写到一个业务对象中,Feign方式就诞生了,它主要对代码结构的一种优化.