SpringCloud微服务~面试题

1. SpringCloud常见组件有哪些?

**问题说明:**这个题目主要考察对SpringCloud的组件基本了解

**难易程度:**简单

参考话术:

SpringCloud包含的组件很多,有很多功能是重复的。其中最常用组件包括:

  • 注册中心组件:Eureka、Nacos等
  • 负载均衡组件:Ribbon
  • 远程调用组件:OpenFeign
  • 网关组件:Zuul、GateWay
  • 服务保护组件:Hystrix、Sentinel
  • 服务配置管理组件:SpringCloudConfig、Nacos

2. Nacos的服务注册表结构是怎样的?

​ 什么叫做注册表结构啊,我们知道Naocs首先它是一个注册中心,我们所有的微服务在启动时,就会提交自己的信息到Nacos当中,我们Nacos保存当前服务的一些信息。那么保持在哪里呢?就是保存在这个注册表当中,只有把所有的服务信息都保存下来了,将来消费者想要去获取服务时,才能够从这个注册表里得到对应服务的信息、IP端口才能进行访问。那么问题就来了,Nacos它是如何创建这样一个表,那么这个注册表结构是什么样子呢?面试官问这个问题其实就是考查两个东西,第一,就是你对于他的这个服务分级存储的一个模型是否了解,第二呢,就是Nacos的源码

image-20230210005624585

Nacos采用了数据的分级存储模型,最外层是Namespace,用来隔离环境。然后是Group,用来对服务分组。接下来就是服务(Service)了,一个服务包含多个实例,但是可能处于不同机房,因此Service下有多个集群(Cluster),Cluster下是不同的实例(Instance)。

对应到Java代码中,Nacos采用了一个多层的Map来表示。结构为Map<String, Map<String, Service>>,其中最外层Map的key就是namespaceId,值是一个Map。内层Map的key是group拼接serviceName,值是Service对象。Service对象内部又是一个Map,key是集群名称,值是Cluster对象。而Cluster对象内部维护了Instance的集合。

3. Nacos如何支撑数十万服务注册压力?

Nacos内部接收到注册的请求时,不会立即写数据,而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。

4. Nacos如何避免并发读写冲突问题?

Nacos在更新实例列表时,会采用CopyOnWrite技术,首先将旧的实例列表拷贝一份,然后更新拷贝的实例列表,再用更新后的实例列表来覆盖旧的实例列表。

这样在更新的过程中,就不会对读实例列表的请求产生影响,也不会出现脏读问题了。

5. Nacos与Eureka的区别有哪些?

Nacos与Eureka有相同点,也有不同之处,可以从以下几点来描述:

  • 接口方式:Nacos与Eureka都对外暴露了Rest风格的API接口,用来实现服务注册、发现等功能
  • 实例类型:Nacos的实例有永久和临时实例之分;而Eureka只支持临时实例
  • 健康检测:Nacos对临时实例采用心跳模式检测,对永久实例采用主动请求来检测;Eureka只支持心跳模式
  • 服务发现:Nacos支持定时拉取和订阅推送两种模式;Eureka只支持定时拉取模式

6. Sentinel的限流与Gateway的限流有什么差别?

考察对限流算法的掌握情况

限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法

而Sentinel内部却比较复杂:

  • 默认限流模式是基于滑动时间窗口算法
  • 排队等待的限流模式则基于漏桶算法
  • 而热点参数限流则是基于令牌桶算法

7. Sentinel的线程隔离与Hystix的线程隔离有什么差别?

Hystix默认是基于线程池实现的线程隔离,每一个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强。

Sentinel是基于信号量(计数器)实现的线程隔离,不用创建线程池,性能较好,但是隔离性一般。

image-20230210011501205

Feign的介绍和使用

一、Feign介绍和简单基本使用流程

image-20230207213746760

image-20230207213755922

​ Feign是一种负载均衡的HTTP客户端, 使用Feign调用API就像调用本地方法一样,从避免了调用目标微服务时,需要不断的解析/封装json 数据的繁琐。Feign集成了Ribbon。Ribbon+eureka是面向微服务编程,而Feign是面向接口编程。

	Fegin是一个声明似的web服务客户端,它使得编写web服务客户端变得更加容易。使用Fegin创建一个接口并对它进行注解。它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解,Spring Web 默认使用了HttpMessageConverters, Spring Cloud 集成 Ribbon 和 Eureka 提供的负载均衡的HTTP客户端 Feign。

image-20230207213838171

image-20230207213949255

image-20230207215026899

Feign内部集成了ribbon,自动实现了负载均衡

image-20230207215002005

二、Feign自定义配置

image-20230207215233270

image-20230207215305103

image-20230207215415792

image-20230207215550021

如果要调试的时候可以使用FULL,但平时建议一般使用BASIC或NONE,因为记录日志还是会消耗一定的性能的

设置超时:

  • 设置openFeign的超时时间
  • 设置Ribbon的超时时间

1、设置Ribbon的超时时间(不推荐)

设置很简单,在配置文件中添加如下设置:

1
2
3
4
5
ribbon:
# 值的是建立链接所用的时间,适用于网络状况正常的情况下, 两端链接所用的时间
ReadTimeout: 5000
# 指的是建立链接后从服务器读取可用资源所用的时间
ConectTimeout: 5000

2、设置openFeign的超时时间(推荐)

openFeign设置超时时间非常简单,只需要在配置文件中配置,如下:

1
2
3
4
5
6
7
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000

default设置的是全局超时时间,对所有的openFeign接口服务都生效

但是正常的业务逻辑中可能涉及到多个openFeign接口的调用,如下图:

img

上图中的伪代码如下:

1
2
3
4
5
6
7
8
9
10
public T invoke(){
//1. 调用serviceA
serviceA();

//2. 调用serviceA
serviceB();

//3. 调用serviceA
serviceC();
}

那么上面配置的全局超时时间能不能通过呢?很显然是serviceAserviceB能够成功调用,但是serviceC并不能成功执行,肯定报超时。

此时我们可以给serviceC这个服务单独配置一个超时时间,配置如下:

1
2
3
4
5
6
7
8
9
10
11
feign:
client:
config:
## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
default:
connectTimeout: 5000
readTimeout: 5000
## 为serviceC这个服务单独配置超时时间
serviceC:
connectTimeout: 30000
readTimeout: 30000

注意:单个配置的超时时间将会覆盖全局配置。

三、Feign的性能调优

Feign是一个声明式客户端,它只是把我们的声明变成http请求,最后发送http请求时还是会应用到一些别的客户端

image-20230207220231333

image-20230207220445237

image-20230207220949115

四、Feign的最佳实践

image-20230207221619077

image-20230207221552791

image-20230207221538398

演示Feign的最佳实践二:抽取FeignClient

教程视频:11-Feign-实现Feign最佳实践_哔哩哔哩_bilibili

image-20230207221754929

Sentinel-线程隔离和熔断降级

一、线程隔离

image-20230209154610011

image-20230209154514001

image-20230209154637624

总结

  • 线程隔离的两种手段是?

    • 信号量隔离

    • 线程池隔离

  • 信号量隔离的特点是?

    • 基于计数器模式,简单,开销小
  • 线程池隔离的特点是?

    • 基于线程池模式,有额外开销,但隔离控制更强

二、熔断降级

​ **Sentinel如何去实现熔断降级?**融合降级,它其实就是用一个断路器去统计服务调用时的一个异常比例、慢请求比例,如果说在做服务调用的时候,异常的比例过高,触发了阈值,就会熔断该服务,拦截访问该服务的一切请求。那这样呢,就会把这个故障服务隔离开了,不会让它影响到我们正常的服务。(就像古代啊,这个武侠人士是吧,这手被毒蛇咬了,赶紧手起刀落,把这个手砍掉,那脚还中毒了呢,卡,把脚砍掉,防止这个毒扩散到全身啊。所以壮士断腕就是一种自我保护)。当然,你把手砍了不算本事啊,你要是能接回来才算本事。所以呢,服务熔断很好做,将来服务如果恢复了,还应该去恢复对该服务的访问。那这个我们的断路器怎么去做呢?

​ 它是由内部的一个状态机来实现的,这个状态机,包含三个状态,分别是Close、Open、Half-Open。Open绿色代表走,这种状态下,断路器不会拦截任何请求,不管请求是正常的还是异常的,都可以访问,但是我们的断路器会统计这个调用的异常比例,如果统计过程中发现异常的比例过高,达到了阈值,它就会从Closed状态切换到Open状态,红色代表停止,那这个时候他就会拦截进入该服务的一些请求了,也就相当于是垄断,但是你不能一直是垄断状态(因为万一这个服务它又恢复了),那因此我们这个熔断的状态会有一个持续的时间,当这个熔断时间结束时,它会从Open状态切换到Half-Open状态,在这个状态下它会放行一次请求,然后根据这次请求的结果来判断接下来干嘛,比如说Half-Open放行了一次请求,结果发现这个请求依然是失败的,那会再次进入Open状态,拦截一切请求,进入熔断,当然同样是持续一段熔断时间,然后再进入Half-Open,那如果放行的这个请求它执行完了,发现是成功的,那么就会从Half-Open切换到Cloesd,这个时候,就等于我们的登录器又开始放行了,那大家随便,然后又开始做数据统计了。那么这三个状态啊,就可以按照这种方式进行一个切换,因此呢,我们不仅能够熔断,还能恢复,就是靠这个来实现的。那在这里面啊,比较关键的两个东西。第一,就是熔断的持续时间(这个将来肯定由我们去配置),第二,失败的阈值,什么情况下你要去熔断。而这个达成熔断的条件啊,在Sentinel里面就叫做熔断的策略

image-20230209161221875

熔断策略

熔断策略到底有哪些呢?

断路器熔断策略有三种:慢调用、异常比例、异常数

慢调用
  • 慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。例如:

image-20230209164421742

上面降级规则配置的解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。

案例:

image-20230209164715829

image-20230209165230617

异常比例、异常数

image-20230209165608275

总结

Sentinel熔断降级的策略有哪些?

  • 慢调用比例:超过指定时长的调用为慢调用,统计单位时长内慢调用的比例,超过阈值则熔断

  • 异常比例:统计单位时长内异常调用的比例,超过阈值则熔断

  • 异常数:统计单位时长内异常调用的次数,超过阈值则熔断

Nacos-集群搭建

Nacos集群搭建

1.集群结构图

官方给出的Nacos集群图:

image-20210409210621117

其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。

我们计划的集群结构:

image-20210409211355037

三个nacos节点的地址:

节点 ip port
nacos1 192.168.150.1 8845
nacos2 192.168.150.1 8846
nacos3 192.168.150.1 8847

2.搭建集群

搭建集群的基本步骤:

  • 搭建数据库,初始化数据库表结构
  • 下载nacos安装包
  • 配置nacos
  • 启动nacos集群
  • nginx反向代理

2.1.初始化数据库

Nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。

官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库可以参考传智教育的后续高手课程。

这里我们以单点的数据库为例来讲解。

首先新建一个数据库,命名为nacos,而后导入下面的SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
CREATE TABLE `config_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) DEFAULT NULL,
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
`c_desc` varchar(256) DEFAULT NULL,
`c_use` varchar(64) DEFAULT NULL,
`effect` varchar(64) DEFAULT NULL,
`type` varchar(64) DEFAULT NULL,
`c_schema` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_aggr */
/******************************************/
CREATE TABLE `config_info_aggr` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(255) NOT NULL COMMENT 'group_id',
`datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
`content` longtext NOT NULL COMMENT '内容',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`app_name` varchar(128) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_beta */
/******************************************/
CREATE TABLE `config_info_beta` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_info_tag */
/******************************************/
CREATE TABLE `config_info_tag` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL COMMENT 'content',
`md5` varchar(32) DEFAULT NULL COMMENT 'md5',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
`src_user` text COMMENT 'source user',
`src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = config_tags_relation */
/******************************************/
CREATE TABLE `config_tags_relation` (
`id` bigint(20) NOT NULL COMMENT 'id',
`tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
`tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
`data_id` varchar(255) NOT NULL COMMENT 'data_id',
`group_id` varchar(128) NOT NULL COMMENT 'group_id',
`tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
`nid` bigint(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`nid`),
UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = group_capacity */
/******************************************/
CREATE TABLE `group_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';

/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = his_config_info */
/******************************************/
CREATE TABLE `his_config_info` (
`id` bigint(64) unsigned NOT NULL,
`nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`data_id` varchar(255) NOT NULL,
`group_id` varchar(128) NOT NULL,
`app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
`content` longtext NOT NULL,
`md5` varchar(32) DEFAULT NULL,
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`src_user` text,
`src_ip` varchar(50) DEFAULT NULL,
`op_type` char(10) DEFAULT NULL,
`tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
PRIMARY KEY (`nid`),
KEY `idx_gmt_create` (`gmt_create`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';


/******************************************/
/* 数据库全名 = nacos_config */
/* 表名称 = tenant_capacity */
/******************************************/
CREATE TABLE `tenant_capacity` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
`quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
`usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
`max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
`max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
`max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
`max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';


CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`kp` varchar(128) NOT NULL COMMENT 'kp',
`tenant_id` varchar(128) default '' COMMENT 'tenant_id',
`tenant_name` varchar(128) default '' COMMENT 'tenant_name',
`tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
`create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
`gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
`gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';

CREATE TABLE `users` (
`username` varchar(50) NOT NULL PRIMARY KEY,
`password` varchar(500) NOT NULL,
`enabled` boolean NOT NULL
);

CREATE TABLE `roles` (
`username` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);

CREATE TABLE `permissions` (
`role` varchar(50) NOT NULL,
`resource` varchar(255) NOT NULL,
`action` varchar(8) NOT NULL,
UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);

INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);

INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');

2.2.下载nacos

nacos在GitHub上有下载地址:https://github.com/alibaba/nacos/tags,可以选择任意版本下载。

本例中才用1.4.1版本:

image-20210409212119411

2.3.配置Nacos

将这个包解压到任意非中文目录下,如图:

image-20210402161843337

目录说明:

  • bin:启动脚本
  • conf:配置文件

进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

image-20210409212459292

然后添加内容:

1
2
3
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847

然后修改application.properties文件,添加数据库配置

1
2
3
4
5
6
7
spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123

2.4.启动

将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3

image-20210409213335538

然后分别修改三个文件夹中的application.properties,

nacos1:

1
server.port=8845

nacos2:

1
server.port=8846

nacos3:

1
server.port=8847

然后分别启动三个nacos节点:

1
startup.cmd

2.5.nginx反向代理

找到课前资料提供的nginx安装包:

image-20210410103253355

解压到任意非中文目录下:

image-20210410103322874

修改conf/nginx.conf文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}

server {
listen 80;
server_name localhost;

location /nacos {
proxy_pass http://nacos-cluster;
}
}

而后在浏览器访问:http://localhost/nacos即可。

代码中application.yml文件配置如下:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:80 # Nacos地址

2.6.优化

  • 实际部署时,需要给做反向代理的nginx服务器设置一个域名,这样后续如果有服务器迁移nacos的客户端也无需更改配置.

  • Nacos的各个节点应该部署到多个不同服务器,做好容灾和隔离

Nacos-注册细节以及与Eureka的比较

Nacos注册细节

image-20230208175614453

相比Eureka,Nacos可以主动推送变更消息push,nacos注册中心一当发现有服务挂了,会立即向服务消费者发送服务变更消息,让服务消费者能及时跟新,跟新的效率更高些,时效性好些。

img

img

不是临时实例,即便服务程序关闭之后,还在这里,只是标红表示不是健康状态,等待着我们启动它

img

再重新启动服务,上面的服务就会恢复健康状态了

image-20230208180456332

  1. Nacos与eureka的共同点
    1. 都支持服务注册和服务拉取
    2. 都支持服务提供者心跳方式做健康检测
      总结
  2. Nacos与Eureka的区别
    1. Nacos支持服务端主动检测提供者状态 : 临时实例采用心跳模式,非临时实例采用主动检测模式(但是主动检测对服务器压力会比较大,所以更建议使用临时实例)
    2. 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    3. Naco支持主动服务列表变更的消息推送模式,服务列表更新更及时<
    4. Nacos集群默认采用AP(强调数据的可用性)方式,当集群中存在非临时实例时,采用CP(强调数据可靠性和一致性)模式; Eureka采用AP方式

Nacos-实现配置管理

image-20230209095809931

image-20230209095828746

img

配置好后就会出现在配置列表中

img

上面就是已经把部分的配置放到了Nacos的服务器了,下面介绍微服务怎样得到Nacos上那些配置

首先项目要知道,去哪读取,读取谁?

bootstrap.yml的优先级会比application.yml的优先级高很多

与Nacos的地址和配置文件有关的信息都应该放到bootstrap.yml文件中

image-20230209104626994

image-20230209105137315

image-20230209105741565

image-20230209105801342

配置热更新

image-20230209111159685

在开发中也比较建议用方式二

image-20230209111901087

image-20230209111637920

image-20230209111420871

多环境配置共享

image-20230209112532846

添加多环境共享配置

image-20230209112740490

多种配置的优先级:

image-20230209113541179

总结:

微服务会从nacos读取的配置文件:

  • [服义名]-[spring.profile.active].yaml,环境配置
  • [服务名].yaml,默认配置,多环境共享

优先级:

  • [服务名]-[环境].yaml > [服务名].yaml > 本地配置

Nacos-namespace环境隔离

image-20230208173305495

  • Nacos环境隔离

    • namespace用来做环境隔离

    • 每个namespace都有唯一id

    • 不同namespace下的服务不可见不可互相访问

img

img

img

不同namespace(命名空间)下的服务是不可见的,要想访问,必须放到同一个namespace下

img

img

Nacos-服务实例的权重设置

先分析为何要用权重设置:

​ 集群优先的负载均衡,不过呢,我们部署的时候啊,可能会存在这么一种情况,因为企业里服务器设备啊,会更新迭代,有一些机器呢,性能比较好,还有一些属于是祖传设备了,性能非常的差,可以说是老弱病残,这个时候呢,我们肯定是希望这些性能好的机器,它承担更多的用户请求,而那些性能差一点的,自然是承担少一点的请求,正所谓能者多劳嘛。但是我们目前看来,NacosRule做到的是集群优先,而后做随机,当用户请求来了以后,它可不管你是性格好的还是差,这个身强力壮的还是老弱病残拉过来就一顿造,那这个时候那些性能差的肯定就会出问题。那么我们该怎样去控制不同服务它的一个请求量呢?哎,Nacos,给我们提供了一个权重的配置,通过修改服务实力的权重,可以控制访问频率,权重越大,访问到的频率就越高,那我们就可以把性能好的机器全都设得大一点,性能差一些呢,设置的小一点。

image-20230208162844861

权重设置为0时,该实例就不会被访问了,也就是说权重调整0时,它压根儿就不会被访问。

那设置权重为0有什么作用?

​ 我们以前一个服务,我们想要对它做一个版本的升级,我们该怎么办?我们是不是要把它重启啊,但是你光天化日之下,你去重启个服务器,是不好的,因为用户都还在访问,你一重启别人就反应不了“哎,你这服务怎么挂了?”,就有问题了!对不对,所以说呢,我们是不能随便重启的。往往呀,每次版本升级都是搞得跟那个什么谍战片一样,要找一个月黑风高无人之夜是吧,然后等用户都下线儿了,我们偷偷的把服务停机,然后呢,去做版本的一个升级。但是你想看,如果现在有了这个权重,我可不可以这么做?我有多个服务器,8081、8082、8083各自部署,我先将8081这个服务的权重调成零。然后呢,大白天也没事儿啊,这个时候呢,渐渐的8081就不承担用户请求了,那这个时候我对它做停机,用户就不会有感知了,那么这个时候对8081停机完了以后,就可以去做一些这个版本的升级,升级完成以后我再重启,我给它权重先不着急调太大,先调小一点,调到零点多,0.01什么之类的,这个时候呢,我们放出少数用户进来做个测试,看看行不行,如果没什么问题,我们就可以逐渐扩大比例,一次升级,这个时候用户是无感知的,你可以做到平滑升级。非常优雅,那么这样呢,这种升级方式啊,其实就是比较比较顺滑这种方式了,你就不用去大半夜去加班的去搞了。所以呢,这是我们这个权重的一些作用。

image-20230208163704237

Nacos-服务多级存储模式 & 集群 & 优先本地集群访问的负载均衡的实现

​ 之前我们有服务的概念,我们提供用户查询的userservice服务,还有提供订单查询的orderservice服务。然后,userservice还部署了多个实例,我们有8081 、8082、8083,这三个呢,都是userservice的实例,所以我们之前是有两层概念的,一层是服务,而第二层就是实例。一个服务,可以包含多个实例。不过呀,随着我们这个业务规模越来越扩大,那么我们就会考虑更多的问题了。比如说我们现在的,我们把所有的实力都部署在一个地方,就像你把鸡蛋都放在一个篮子里,哪天你不小心篮子翻了,那所有的鸡蛋不就都打碎了吗?**那你的机房放在那里同一个地方,哪天天灾人祸出了问题,整个服务不就完了?**所以呀,为了解决这个问l题,我们会将一个服务的多个实例部署到多个机房,特别是像阿里京东这些财大气粗的,哎,我给全国各地,上海、杭州、北京都整一个,这俩送到杭州,这俩到上海,再来几个给它扔到北京,这就像我们把鸡蛋分散开了,一个打了是不是还有好几栏儿呢,对不对?那这样呢,就能够做到一种叫容灾。而我们的那个服务分级存储模型,就是引入了这样一个机房的概念,或者叫地域的概念,把同在一个机房的多个实例称为一个集群。比如,杭州机房的两个userservice实例就称之为杭州的userservice集群,那北京的userservice实例就称为北京的userservice集群。所以呢,在Nacos的服务分级模型中啊,一级是服务,往下是集群,再往下是实例。

​ **那为什么Nacos要引入这样的一个服务分级的?**我原来直接服务找实例不好吗?为什么要多加一个地域划分集群的一个概念?我们设想这么一种情况,比方说我有一个杭州的地方,里面有orderservice的集群,还有一个userservice集群,然后呢,我还有一个上海的地方,也是一样的配置,将来还有广东地方,这个北京地方等等。现在呢,我的orderservice需要访问userservice,那么它有两种选择,一种是在自己本地局域网内访问,另一种是去外边的机房访问。那你觉得他该选哪个呀?那不用说呀,肯定选本地啊,因为外边的野花不要采嘛。当然啦,还有更重要的原因,我们局域网内的访问呢,它的距离比较短,所以呢,速度比较快,延迟比较低,而你跨越了这种集群的访问,比如说你从杭州访问到广州或者北京,那么达到数百公里上千公里,那么这个时候延迟是非常高的,所以啊,在服务调用时,应该尽可能的去访问本地集群,只有在本地集群不可用的情况下,我们才会考虑去访问其集群。我们的Nacos引入这个集训概念,其实就是为了防止出现跨集群调用啊,尽可能的避免。

image-20230208114620776

image-20230208114536926

image-20230208114842391

演示:

image-20230208115409758

image-20230208115731341

然后去Nacos控制台就可以看到有两个集群,以及集群各自有的实例

image-20230208115947584

image-20230208115826327

orderservice想访问本地集群HZ中的userservice的那两个实例服务,那么首先就需要将orderservice也注册到HZ集群

image-20230208120609913

image-20230208120553443

到这个时候,如果orderservice去访问3次userservice服务时,userservice的三个实例都会被访问到,因为依然采用的是轮询的方式。服务在选择实例时的规则都是由负载均衡的规则来决定的,也就是Irule,因为还没有配置Irule,所以还是默认的轮询方式。所以要想实现优先同集群访问的这种负载均衡的规则,我们必须去修改负载均衡。

image-20230208121356082

在orderservice的yml配置文件中:

1
2
3
userservice: # 要做配置的微服务名称
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规矩(优先同集群访问)

这样之后,orderservice去访问多次userservice服务,就会发现都是访问的是8081、8082的实例(在idea的控制台可以看各实例日志,8083是没有日志输出的)。

  • 那么有一个问题,本地同集群的8081、8082两个实例的被访问的比例是怎么的呢?

    • 其实是随机的,NacosRule的一个特点,优先使用本地同集群,在集群内的多个服务当中再采用随机方式来负载均衡
  • 那么又有一个问题,如果我们把orderservice的同集群的userservice服务都停了,这样就只剩下不同集群的SH内的userservice实例,这个时候再去通过oerservice访问userservice时会怎样呢?

image-20230208122809011

image-20230208122900770

这是orderservice去访问userservice就会去跨集群访问了,即访问到8083端口的实例。idea,看一下日志。我们发现,这次请求是8083给承担了,那在order当中会出现一个警告信息什么,显示一次跨集群访问发生了,谁呀?就是userservice,那么你想访问的是杭州,但是呢,实际上你访问的是上海。那通过这个啊,我们就知道了,NacosRule其实优先访问本地,本地没有,它才会跨境访问,跨域访问时,它会有一个警告,将来我们的运维人员如果看到了这样的警告信息,就能够清楚的知道发生了什么问题了,那么他就会及时的去重新启动我们的挂掉的服务,这样问题就能得到解决了

image-20230208123816670

5-分析GC日志

[toc]

5. 分析 GC 日志

5.1. GC 分类

针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)

  • 部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:

    • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0, S1)的垃圾收集
    • 老年代收集(Major GC / Old GC):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。注意,很多时候 Major GC 会和 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有 G1 GC 会有这种行为

  • 整堆收集(Full GC):收集整个 java 堆和方法区的垃圾收集。

    • 哪些情况会触发 Full GC ?
      • 老年代空间不足
      • 方法区空间不足
      • 显示调用System.gc()
      • Minor GC进入老年代的数据的平均大小 大于 老年代的可用内存
      • 大对象直接进入老年代,而老年代的可用空间不足

5.2. GC 日志分类

MinorGC

MinorGC(或 young GC 或 YGC)日志:

1
[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]

image-20210506202126562

image-20210506202156090

FullGC

1
[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]

image-20210506202330868

image-20210506202349072

5.3. GC 日志结构剖析

透过日志看垃圾收集器

  • Serial 收集器:新生代显示 “[DefNew”,即 Default New Generation

  • ParNew 收集器:新生代显示 “[ParNew”,即 Parallel New Generation

  • Parallel Scavenge 收集器:新生代显示"[PSYoungGen",JDK1.7 使用的即 PSYoungGen

  • Parallel Old 收集器:老年代显示"[ParoldGen"

  • G1 收集器:显示”garbage-first heap“

透过日志看 GC 原因

  • Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据
  • Metadata GCThreshold:Metaspace 区不够用了
  • FErgonomics:JVM 自适应调整导致的 GC
  • System:调用了 System.gc()方法

透过日志看 GC 前后情况

通过图示,我们可以发现 GC 日志格式的规律一般都是:GC 前内存占用-> GC 后内存占用(该区域内存总大小)

1
[PSYoungGen: 5986K->696K (8704K) ] 5986K->704K (9216K)
  • 中括号内:GC 回收前年轻代堆大小,回收后大小,(年轻代堆总大小)

  • 括号外:GC 回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)

注意:Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系,Eden:S0:S1=8:1:1。

透过日志看 GC 时间

GC 日志中有三个时间:user,sys 和 real

  • user:进程执行用户态代码(核心之外)所使用的时间。这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
  • sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间
  • real:程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成)。对于并行 gc,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数。

由于多核的原因,一般的 GC 事件中,real time 是小于 sys time + user time 的,因为一般是多个线程并发的去做 GC,所以 real time 是要小于 sys + user time 的。如果 real > sys + user 的话,则你的应用可能存在下列问题:IO 负载非常重或 CPU 不够用。

5.4. GC 日志分析工具

GCEasy

GCEasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的。

官网地址:https://gceasy.io/

GCViewer

GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:<file>。还可以计算与垃圾回收相关的性能指标(吞吐量、累积的暂停、最长的暂停等)。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用。

源码下载:https://github.com/chewiebug/GCViewer

运行版本下载:https://github.com/chewiebug/GCViewer/wiki/Changelog

GChisto

  • 官网上没有下载的地方,需要自己从 SVN 上拉下来编译
  • 不过这个工具似乎没怎么维护了,存在不少 bug

HPjmeter

  • 工具很强大,但是只能打开由以下参数生成的 GC log,-verbose:gc -Xloggc:gc.log。添加其他参数生成的 gc.log 无法打开
  • HPjmeter 集成了以前的 HPjtune 功能,可以分析在 HP 机器上产生的垃圾回收日志文件