在上一章中实现了微服务拆分,并且通过HTTP请求(RestTemplate)实现了跨微服务的远程调用,不过这种手动发起HTTP请求的方式存在问题:
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
//private final IItemService itemService;
private final RestTemplate restTemplate;
...
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
String itemUrl = "http://localhost:8081/items?ids={ids}";
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
itemUrl,//请求路径
HttpMethod.GET,//请求方式
null,//请求实体
new ParameterizedTypeReference<List<ItemDTO>>() {
},//响应数据类型
Map.of("ids", CollUtils.join(itemIds, ","))//请求参数
);
List<ItemDTO> items = null;
if (response.getStatusCode().is2xxSuccessful()) {
items = response.getBody();
}
if (CollUtils.isEmpty(items)) {
return;
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream()
.collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
...
}
在查询商品的时候,URL地址是写死的:String itemUrl = "http://localhost:8081/items?ids={ids}
如果商品服务是热点接口,为了应对更高的并发/避免单点故障,进行多实例部署:
每一个item-service实例的IP或者端口号是不同的,此时就会出现问题:
-
item-service多实例,服务调用者cart-service如何知道每一个实例的地址?
-
HTTP请求需要指定URL地址,此时服务调用者cart-service到底调用哪一个实例?
-
如果某一个item-service实例宕机,cart-service仍然在调用怎么办?
-
并发量过高,item-service临时部署多台,cart-service如何知道最新的实例地址?
为了解决这些问题,必须引入服务注册中心的概念。
注册中心原理
在微服务的远程调用中有两个角色:
- 服务提供者:item-service
- 服务调用者:cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些微服务引入了注册中心。注册中心、服务提供者、服务消费者之间的关系:
流程如下:
- 服务启动时将自己的服务信息(服务名称、IP、端口号、版本)写入服务注册中心的服务注册表中,服务注册表保存了所有的服务
任何一个微服务都可能是调用者/提供者
-
服务调用者需要调用微服务时,首先在注册中心订阅需要的服务,将服务注册表下载到本地
-
根据调用的微服务名称在服务注册表中获取对应的服务提供者集合
-
调用者自身对服务提供者负载均衡,根据调用者本地的负载均衡策略挑选一个实例进行调用
服务提供者实例宕机或启动新实例:
-
服务提供者定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
注册中心如果长时间收不到服务提供者的心跳请求,认为该实例宕机,将其从服务注册表中删除
-
每当服务有新实例启动时,会发送注册请求,信息会被记录在注册中心的服务注册表中。
-
注册中心的服务注册表变更后,主动通知微服务,更新本地的服务注册表
注册中心产品
目前开源的注册中心有很多,比较常见的有:
- Eureka:Netflix公司出品,目前集成在Spring Cloud应用中,一般用于Java语言
- Nacos:Alibaba公司出品,目前被集成在Spring Cloud Alibaba中,一般用于Java语言
- Consul:HashiCorp公司出品,目前集成在Spring Cloud中,不限制微服务语言
这三种注册中心都遵循Spring Cloud的API规范,在业务开发上没有太大差异,Spring Cloud Alibaba使用的注册中心是Nacos
Nacos简介
Nacos的两个重要功能:服务发现与配置管理
云原生:微服务 + DevOps + CI/CD + 容器化
Nacos架构:
Nacos client:Java语言编写的客户端
-
sidecar:多语言异构模块,通过这个模块可以让其他语言使用nacos
-
Name Server:命名服务,支持的服务:dns、vip、address-server
-
OpenAPI:客户端(Provider/Consumer)通过API接口就可以访问服务端(Nacos)的 Naming Service(服务发现)和 Config Service(配置管理),客户端和服务端通信协议支持http/dns/udp/tls
-
Consistency Protocol:集群之间的一致性协议,priv-raft(Naming Service)、sync renew、rdbms based(Config Service)
Nacos下载与配置
Windows环境
在Nacos官网找到最新稳定版本 ,目前的Spring Cloud Alibaba版本是 2022.0.0.0-RC2,对应的Nacos版本是2.2.1。
对于2.2.0.1和2.2.1版本,需要修改conf下application.properties文件:
设置其中的nacos.core.auth.plugin.nacos.token.secret.key
值(Authorization | Nacos 官网)
参数名 | 默认值 | 启止版本 | 说明 |
---|---|---|---|
nacos.core.auth.enabled | false | 1.2.0 ~ latest | 是否开启鉴权功能 |
nacos.core.auth.system.type | nacos | 1.2.0 ~ latest | 鉴权类型 |
nacos.core.auth.plugin.nacos.token.secret.key | SecretKey012345678901234567890123456789012345678901234567890123456789(2.2.0.1后无默认值) | 2.1.0 ~ latest | 默认鉴权插件用于生成用户登陆临时accessToken所使用的密钥,使用默认值有安全风险 |
nacos.core.auth.plugin.nacos.token.expire.seconds | 18000 | 2.1.0 ~ latest | 用户登陆临时accessToken的过期时间 |
nacos.core.auth.enable.userAgentAuthWhite | false | 1.4.1 ~ latest | 是否使用useragent白名单,主要用于适配老版本升级,置为true时有安全风险 |
nacos.core.auth.server.identity.key | serverIdentity(2.2.1后无默认值) | 1.4.1 ~ latest | 用于替换useragent白名单的身份识别key,使用默认值有安全风险 |
nacos.core.auth.server.identity.value | security(2.2.1后无默认值) | 1.4.1 ~ latest | 用于替换useragent白名单的身份识别value,使用默认值有安全风险 |
SecretKey012345678901234567890123456789012345678901234567890123456789 | 1.2.0 ~ 2.0.4 | 同nacos.core.auth.plugin.nacos.token.secret.key | |
18000 |
修改配置文件:
启动Nacos:
startup.cmd -m standalone
默认是集群启动,需要指定为单机启动
生态融合
参照Nacos官网-生态融合 -> 服务发现
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
官网说明的使用方式:
server.port=8081
spring.application.name=nacos-producer
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
management.endpoints.web.exposure.include=*
要求指定服务名称、nacos地址
注册provider
spring:
# 微服务名称
application:
name: depart-provider
#nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
在2.2.1版本中,不加username和password会报错:user not found,某些版本加了反而可能会报错
在2.1.0版本中,不需要指定
注册consumer
spring:
application:
name: depart-consumer
cloud:
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
注册完毕后在nacos控制台 127.0.0.1/nacos 中就可以看到这两个服务
改造案例一:简单consumer、provider
在案例一中,我们进行远程调用使用的是RestTemplate的简单直连方式:
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
@Resource
private RestTemplate restTemplate;
public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart";
@PostMapping
public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart){
ResponseResult<Boolean> responseResult = restTemplate.postForObject(SERVICE_PROVIDER, depart,
ResponseResult.class);
return responseResult;
}
@GetMapping("/{id}")
public ResponseResult<Depart> getHandler(@PathVariable Long id){
String url = SERVICE_PROVIDER + "/" + id;
ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);
return result;
}
@GetMapping("/list")
public ResponseResult<List<Depart>> listHandler(){
String url = SERVICE_PROVIDER + "/list";
ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);
return result;
}
}
在调用时使用的地址:public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart"
使用微服务方式:
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
@Resource
private RestTemplate restTemplate;
//直连方式
//public static final String SERVICE_PROVIDER = "http://localhost:8081/provider/depart";
//微服务方式
public static final String SERVICE_PROVIDER = "http://depart-provider/provider/depart";
@PostMapping
public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart){
ResponseResult<Boolean> responseResult = restTemplate.postForObject(SERVICE_PROVIDER, depart, ResponseResult.class);
return responseResult;
}
@GetMapping("/{id}")
public ResponseResult<Depart> getHandler(@PathVariable Long id){
String url = SERVICE_PROVIDER + "/" + id;
ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);
return result;
}
@GetMapping("/list")
public ResponseResult<List<Depart>> listHandler(){
String url = SERVICE_PROVIDER + "/list";
ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);
return result;
}
}
并且要指明RestTemplate以负载均衡方式调用:
@Configuration
public class RestTemplateConfig {
//以负载均衡的方式调用服务
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
但是这样启动会报错:
java.net.UnknownHostException: depart-provider
因为新版的Spring Cloud不再支持Ribbon了,老版本Spring Cloud默认使用Ribbon做负载均衡,需要增加依赖:
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
获取服务注册表信息
通过DiscoveryClient获取到服务注册表信息
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/discovery")
public ResponseResult<List<String>> discoveryHandler(){
//获取注册中心所有服务名称
List<String> services = discoveryClient.getServices();
for (String service : services) {
//获取指定服务名称对应的所有服务实例
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
//获取实例对应的信息
HashMap<String, Object> hashMap = new HashMap<>() {{
put("serviceName",service);
put("serviceID",instance.getServiceId());
put("serviceHost",instance.getHost());
put("servicePort",instance.getPort());
put("serviceURI",instance.getUri());
}};
System.out.println(hashMap);
}
}
return ResponseResult.success(services);
}
请求得到的服务名称列表:
{
"code": 200,
"data": [
"depart-consumer",
"depart-provider"
],
"message": null
}
服务对应的实例信息:
{
serviceHost=192.168.11.1,
serviceURI=http://192.168.11.1:8081,
servicePort=8081,
serviceName=depart-provider,
serviceID=depart-provider
}
改造案例二的RestTemplate调用方式
在案例二中,发起远程调用:
//微服务方式
public static final String SERVICE_PROVIDER = "http://depart-provider/provider/depart";
这种方式是通过RestTemplate自己的负载均衡策略调用,常用的负载均衡算法有:
- 随机
- 轮询
- IP的hash
- LRU
我们可以根据需要调用服务的名称通过DiscoveryClient获取到对应的服务实例列表,自定义负载均衡策略,获取到URL进行访问
@Autowired
private DiscoveryClient discoveryClient;
private String getLoadBalancedServerAddress() {
List<ServiceInstance> instances = discoveryClient.getInstances("depart-provider");
Collections.shuffle(instances);
String URL = instances.get(0).getUri().toString() + "/provider/depart";
System.out.println(URL);
return URL;
}
@PostMapping
public ResponseResult<Boolean> saveHandle(@RequestBody Depart depart) {
ResponseResult<Boolean> responseResult =
restTemplate.postForObject(getLoadBalancedServerAddress(), depart, ResponseResult.class);
return responseResult;
}
@GetMapping("/{id}")
public ResponseResult<Depart> getHandler(@PathVariable Long id) {
String url = getLoadBalancedServerAddress() + "/" + id;
ResponseResult<Depart> result = restTemplate.getForObject(url, ResponseResult.class, id);
return result;
}
@GetMapping("/list")
public ResponseResult<List<Depart>> listHandler() {
String url = getLoadBalancedServerAddress() + "/list";
ResponseResult<List<Depart>> result = restTemplate.getForObject(url, ResponseResult.class);
return result;
}
注册表缓存
如果此时停止Nacos,consumer还是可以正常访问provider的。
服务启动后,发生调用时会自动从Nacos注册中心下载并缓存注册表到本地。所以,即使Nacos宕机,consumer仍然可以调用provider,只是此时不能有新服务进行注册了,服务缓存中的注册表信息无法更新。
但是如果服务启动后,没有发生调用Nacos就宕机,consumer就获取不到服务注册表信息,自然也无法调用provider
临时实例与持久实例
临时实例和持久实例的存储位置和健康检测机制是不同的:
-
临时实例:默认情况下,仅会注册在Nacos内存,不会持久化到Nacos磁盘,其健康检测机制为Client模式,即Client主动向Server上报健康状态。默认心跳间隔为5秒,在15秒内Server未收到Client心跳,就会将其标记为 不健康 状态;30秒内收到了Client心跳,则重新恢复到 健康 状态,否则将Client从Server端内存清除。
-
持久实例:服务实例不仅会注册到Nacos内存,也会持久化到Nacos磁盘。健康检测机制为Server模式,Server主动检测Client的健康状态,默认20s检测一次,检测失败后标记为 不健康 状态,但不会被清除,因为这是持久化到磁盘的。
大多数情况下都是临时实例,因为互联网项目存在突发流量暴增的情况,Alibaba是云原生的,可以充分利用云端的弹性,流量下降就清除临时实例。持久实例销毁是很麻烦的。
创建持久实例
默认的实例是临时实例(ephemeral为true),设置为true后尝试重启:
spring:
# 微服务名称
application:
name: depart-provider
#nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
# 注册为持久实例
ephemeral: false
重启:
Caused by: com.alibaba.nacos.api.exception.NacosException:
failed to req API:/nacos/v1/ns/instance after all servers([127.0.0.1:8848]) tried: caused: errCode: 400, errMsg: Current service DEFAULT_GROUP@@depart-provider is ephemeral service, can't register persistent instance. ;
临时实例depart-provider不能被注册为持久实例,因为实例一旦被注册过为临时实例,就不能再次注册为持久实例了。
注册为持久实例:更改spring.application.name
spring:
# 微服务名称
application:
name: depart-provider-persistent
#nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
ephemeral: false
此时depart-provider-persistent就被注册为持久实例
注销持久实例
此时是无法删除临时实例和持久实例的:
停机后:
临时实例下线,持久实例还是不能删除。
删除持久实例需要参照Nacos官网-OpenAPI指南
请求方式:
DELETE
Content-Type:application/x-www-form-urlencoded
请求地址:
/nacos/v2/ns/instance
请求Body:
参数名 | 参数类型 | 是否必填 | 描述说明 |
---|---|---|---|
namespaceId | String | 否 | 命名空间Id ,默认为public |
groupName | String | 否 | 分组名,默认为DEFAULT_GROUP |
serviceName | String | 是 | 服务名 |
ip | String | 是 | IP 地址 |
port | int | 是 | 端口号 |
clusterName | String | 否 | 集群名称,默认为DEFAULT |
healthy | boolean | 否 | 是否只查找健康实例,默认为true |
weight | double | 否 | 实例权重,默认为1.0 |
enabled | boolean | 否 | 是否可用,默认为true |
metadata | JSON格式String | 否 | 实例元数据 |
ephemeral | boolean | 否 | 是否为临时实例 |
返回值
参数名 | 参数类型 | 描述 |
---|---|---|
data | boolean | 是否执行成功 |
请求实例
curl -d 'serviceName=test_service' \
-d 'ip=127.0.0.1' \
-d 'port=8090' \
-d 'weight=0.9' \
-d 'ephemeral=true' \
-X DELETE 'http://127.0.0.1:8848/nacos/v2/ns/instance'
IP和端口号在Nacos控制台可以看到,ephemeral为false:
curl -d 'serviceName=depart-provider-persistent' \
-d 'ip=192.168.11.1' \
-d 'port=8081' \
-d 'ephemeral=false' \
-d 'username=nacos' \
-d 'password=nacos' \
-X DELETE 'http://127.0.0.1:8848/nacos/v2/ns/instance'
http://localhost:8848/nacos/v2/ns/instance?serviceName=serviceName=depart-provider-persistent&ip=192.168.11.1&port=8081&weight=1&groupName=DEFAULT_GROUP&namespaceId=public&ephemeral=false&clusterName=DEFAULT&username=nacos&password=nacos
外置数据持久化
在启动Nacos时,控制台输出:
Nacos started successfully in stand alone mode. use embedded storage
使用了内嵌的数据库,在nacos/data/derby-data下可以看到。
Nacos官网-单机模式支持mysql中说明,在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
-
1.安装数据库,版本要求:5.6.5+
-
2.初始化mysql数据库,数据库初始化文件:mysql-schema.sql
-
3.修改nacos/conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow
再次以单机模式启动nacos,nacos将所有写入derby的数据都写入了mysql
在Nacos架构图中:
mysql-schema.sql
该sql脚本在nacos\conf\mysql-schema.sql位置。其中只有建表语句,没有创建数据库语句
其中建议数据库名为 nacos_config
运行脚本,创建了12张表。
修改application.properties
修改nacos/conf/application.properties:
启动nacos
控制台提示:
Nacos started successfully in stand alone mode. use external storage
Nacos集群
Nacos单节点宕机会导致系统故障,创建Nacos集群。
配置Nacos集群
复制nacos文件夹到nacos-cluster,在该文件夹下以集群方式启动nacos。
nacos8849的配置
修改conf/cluster.conf:
#it is ip
#example
192.168.11.1:8849
192.168.11.1:8851
192.168.11.1:8853
注意:此处必须填写真实ip地址,并且端口号必须间隔一个
同时修改conf/application.properties的端口号:
server.port=8849
nacos8851的配置
复制nacos8849文件夹,改名为nacos8851,只需要修改application.properties的端口号
server.port=8851
nacos8853的配置
和上文相同
启动nacos集群
在上文我们的启动是通过命令:startup.cmd -m standalone
因为我们是单机启动,而nacos默认以集群方式启动。
此时我们配置了nacos集群,直接双击启动即可。
Nacos started successfully in cluster mode. use external storage
连接Nacos集群
修改provider/consumer的application.yml配置文件:
# 微服务名称
application:
name: depart-provider
#nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8851,127.0.0.1:8853
username: nacos
password: nacos
此时在8851和8853控制台都能看到相同的内容:
但是这样做实际上是不安全的,nacos节点的地址都暴露给了客户端,可以使用nginx进行反向代理。
Nacos服务隔离
数据模型
Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。
Nacos中的服务是由三元组唯一确定的:namespace、group、serviceName。
namespace和group的作用是相同的,用于划分不同的区域范围,隔离服务。不同的是,namespace的范围更大,不同的namespace可以包含相同的group,不同的group可以包含相同的service。
namespace默认是public(空字符串),group默认是DEFAULT_GROUP
服务隔离
服务隔离:微服务只能在同一个三元组之间互相调用
启动三个 02-provider-8081实例,不同的是namespace、group、port:
- public + DEFAULT_GROUP + 8081
- public + MY_GROUP + 8082
- hello + MY_GROUP + 8083
自定义hello命名空间
配置服务注册的namespace、group
spring:
# 微服务名称
application:
name: depart-provider
#nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
# 可以在此处配置namespace和group
namespace:
group:
使用行内参数的方式启动3个服务:
-
默认启动的就是 public DEFAULT_GROUP 8081
-
启动public + MY_GROUP + 8083
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.group=MY_GROUP
public registering service depart-provider,nacos registry, MY_GROUP depart-provider 192.168.11.1:8083 register finished
- 启动hello + MY_GROUP + 8085
-Dserver.port=8085 -Dspring.cloud.nacos.discovery.group=MY_GROUP -Dspring.cloud.nacos.discovery.namespace=hello
hello registering service depart-provider,nacos registry, MY_GROUP depart-provider 192.168.11.1:8085 register finished
虽然不会报错,但是在nacos控制台是看不到这个服务的,需要指定为namespace的ID:
-Dserver.port=8085
-Dspring.cloud.nacos.discovery.group=MY_GROUP
-Dspring.cloud.nacos.discovery.namespace=679c3195-e967-47f7-9925-3c1aa6165a03
679c3195-e967-47f7-9925-3c1aa6165a03 registering service depart-provider
nacos registry, MY_GROUP depart-provider 192.168.11.1:8085 register finished
打开控制台就能看到三个provider实例,启动consumer,consumer当前的配置:
# consumer的配置文件
cloud:
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
consumer进行接口调用,当前的consumer在public DEFAULT_GROUP下,只能调用相同三元组中的服务,也就是8081
标签:depart,Nacos,nacos,实例,注册,provider,服务 From: https://blog.csdn.net/weixin_66129233/article/details/143580244