四個緯度 學習新技術 思路
-
是什么?
-
能干什么?
-
去哪下載?
-
怎么玩
HelloWorld 入門案例
前言
一、Eureka 服務注冊與發現
1、什么是注冊中心
注冊中心可以說是微服務架構中的“通訊錄”,它記錄了服務和服務地址的映射關系。在分布式架構中,服務會注冊到這里,當服務需要調用其他服務時,就到這里找到服務的地址,進行調用。
我們現在給張三打電話,我們要打開通訊錄,查找張三的電話號碼,然后撥出電話。這樣一個過程,在微服務注冊中心當中,可以理解為服務發現的過程。
現在給李四打電話,那么前提李四的電話是哪來的?我們要先把李四的電話先收錄到通訊錄中,將李四的姓名電話號碼添加到通訊錄中,這樣添加的過程就是服務注冊的過程。
這個通訊錄扮演的什么角色?扮演的是注冊中心的角色
服務注冊中心的作用就是 服務的注冊 和 服務的發現
Eureka 包含兩個組件 :Eureka Server 和 Eureka Client
Eureka Server 提供服務注冊服務
Eureka Client 通過注冊中心進行訪問
2、為什么需要注冊中心
在分布式系統中,不僅僅是需要在注冊中心找到 服務和服務地址的映射關系,還需要考慮更多更復雜的問題 :
- 服務注冊后,如何被及時發現
- 服務宕機后,如何即使下線
- 服務如何有效的水平擴展
- 服務發現時,如何進行路由
- 服務異常時,如何進行降級
- 注冊中心如何實現自身的高可用
注冊中心 解決了什么問題 ?
- 服務管理
- 服務的依賴關系管理
3、Eureka注冊中心三種角色
Eureka Server
通過 Register、Get、Renew 等接口提供服務的注冊和發現。
Service Provider
服務提供方,把自身的服務實例注冊到 Eureka Server 中 。
Service Consumer
服務調用方,通過 Eureka Server 獲取服務列表,消費服務。
Eureka 入門案例
配置
server:
port: 8080
spring:
application:
name: eureka-server # 應用名稱
# 配置 Eureka Server 注冊中心
eureka:
instance:
hostname: localhost # 主機名
client:
register-with-eureka: false # 是否將自己注冊到注冊中心 默認true
fetch-register: false # 是否從注冊中心獲取服務注冊信息 默認true
service-url: # 注冊中心對外暴露的注冊地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
如果應用的角色是注冊中心並且是單節點的話 ,需要關閉兩個配置項 register-with-eureka
、fetch-register
設置為 false
省略 App.java
在啟動類加上注解 @EnableEurekaServer
高可用 Eureka 注冊中心
注冊中心 - server
導入的依賴 :spring-cloud-starter-netflix-eureka-server
配置文件
server:
port: 8080
spring:
application:
name: eureka-server # 應用名稱 (集群下相同)
# 配置 Eureka Server 注冊中心
eureka:
instance:
hostname: eureka01 # 主機名
client:
# 設置服務注冊中心地址,指向另一個注冊中心
service-url: # 注冊中心對外暴露的注冊地址
defaultZone: http://192.168.0.102:8081/eureka/
server:
port: 8081
spring:
application:
name: eureka-server # 應用名稱 (集群下相同)
# 配置 Eureka Server 注冊中心
eureka:
instance:
hostname: eureka02 # 主機名
client:
# 設置服務注冊中心地址,指向另一個注冊中心
service-url: # 注冊中心對外暴露的注冊地址
defaultZone: http://192.168.0.102:8080/eureka/
使用 ip+端口方式訪問
server:
port: 8080
spring:
application:
name: eureka-server # 應用名稱 (集群下相同)
# 配置 Eureka Server 注冊中心
eureka:
instance:
hostname: eureka02 # 主機名
prefer-ip-address: true # 是否使用ip地址注冊
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
# 設置服務注冊中心地址,指向另一個注冊中心
service-url: # 注冊中心對外暴露的注冊地址
defaultZone: http://192.168.0.102:8081/eureka/
server:
port: 8081
spring:
application:
name: eureka-server # 應用名稱 (集群下相同)
# 配置 Eureka Server 注冊中心
eureka:
instance:
hostname: eureka02 # 主機名
prefer-ip-address: true # 是否使用ip地址注冊
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
# 設置服務注冊中心地址,指向另一個注冊中心
service-url: # 注冊中心對外暴露的注冊地址
defaultZone: http://192.168.0.102:8080/eureka/
注冊中心構建集群是相互注冊
服務提供者 -service-provider
導入的依賴 :spring-cloud-starter-netflix-eureka-client
環境搭建
配置文件
server:
port: 7070
spring:
application:
name: service-provider # 應用名稱 集群下相同
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url: # 設置服務注冊中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
啟動類加上注解 @EnableEurekaClient
如果配置了 Eureka 注冊中心,默認會開啟該注解
服務消費者 service -consumer
導入的依賴 :spring-cloud-starter-netflix-eureka-client
環境搭建
server:
port: 9090
spring:
application:
name: service-consumer # 應用名稱
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
registerr-with-eureka: false # 是否將自己注冊到注冊中心 默認true
register-fetch-interval-seconds: 10 # 標識 Eureka Client 間隔多久取服務器拉取注冊信息 默認30s
service-url: # 設置服務注冊中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
消費服務 - 方式
- DiscoveryClient :通過元數據獲取服務信息
- LoadBalancerClient :Ribbon 的負載均衡器
- @LoadBalanced :通過注解開啟 Ribbon 的負載均衡器
方式一 : DiscoveryClient
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@Override
public Order selectOrderbyId(Integer id) {
return new Order(id, "order-001", "中國", 319D, selectProductsByDiscoveryClient());
}
private List<Product> selectProductsByDiscoveryClient() {
StringBuffer sb = null;
// 獲取服務列表
List<String> serviceIds = discoveryClient.getServices();
if (CollectionUtils.isEmpty(serviceIds)) {
return null;
}
// 根據服務名稱獲取服務
List<ServiceInstance> serviceInstance = discoveryClient.getInstances("service-provider");
if (CollectionUtils.isEmpty(serviceInstance)) {
return null;
}
ServiceInstance si = serviceInstance.get(0);
sb = new StringBuffer();
sb.append("http://").append(si.getHost()).append(si.getPort()).append("/product/list");
// ResponseEntity 封裝返回數據
ResponseEntity<List<Product>> response = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {}
);
return response.getBody();
}
}
啟動類 App.java
RestTemplate SpringBoot默認不會集成 需要手動注入Bean
注解 @EnableEurekaClient
```
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
```
方式二 :LoadBalancerClient
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowiired
private LoadBalancerClient loadBalancerClient; // Ribbon 負載均衡器
@Override
public Order selectOrderbyId(Integer id) {
return new Order(id, "order-001", "中國", 319D, selectProductListByLoadBalancerClient());
}
private List<Prodct> selectProductListByLoadBalancerClient() {
StringBuffer sb = null;
// 根據服務名稱獲取服務
ServiceInstance si = loadBalancerClient.choose("service-provider");
if (null == si) {
return null;
}
sb = new StringBuffer();
sb.append("http://").append(si.getHost()).append(si.getPort()).append("/product/list");
// ResponseEntity 封裝返回數據
ResponseEntity<List<Product>> response = restTemplate.exchange(
sb.toString(),
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {}
);
return response.getBody();
}
}
方式三 :@LoadBalanced
在啟動類注入 RestTemplate
同時添加 @LoadBalanced
負載均衡器注解, 表示這個 RestTemplate
在請求時擁有客戶端負載均衡的能力
```
@Bean
@LoadBalanced // 負載均衡注解
public RestTemplate restTemplate() {
return new RestTemplate();
}
```
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Override
public Order selectOrderbyId(Integer id) {
return new Order(id, "order-001", "中國", 319D, selectProductListByLoadBalancerClient());
}
private List<Product> selectProductListByLoadBalancerAnnotation() {
ResponseEntity<List<Product>> response = restTemplate.exchange(
"http://service-provider/product/list",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {}
);
return response.getBody();
}
}
Eureka 架構原理
- Register(服務注冊):把自己的 IP 和 端口 注冊到 Eureka。
- Renew(服務續約):發送心跳包,每 30 s發送一次,告訴 Eureka 自己還在運行,如果 90 s還未發送心跳,將其宕機。
- Cancel(服務下線):當 Provider 關閉時會向 Eureka 發送消息,讓 Eureka 把自己從服務列表中刪除,防止 Consumer 調用到不存的服務。
- Get Register(獲取服務注冊列表):獲取其他服務列表。
- Replicate(集群中數據同步):Eureka 集群中的數據復制與同步。
- Make Remote Call(遠程調用):完成服務的遠程調用
服務發現 DiscoveryClient
對於注冊進 Eureka 里面的微服務,可以通過服務發現來獲取某個服務的信息
啟動類上加上注解
@EnableDiscoveryClient
Controller 類中 注入 DiscoveryClient 類
@Resource
private DiscoveryClient discoveryClient;
controller
@RestController
@Slf4j
public class PaymentController
{
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/payment/discovery")
public Object discovery()
{
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("*****element: "+element);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}
}
打印日志情況
element: cloud-payment-service
element: cloud-order-service
CLOUD-PAYMENT-SERVICE 192.168.56.1 8001 http://192.168.56.1:8001
CLOUD-PAYMENT-SERVICE 192.168.56.1 8002 http://192.168.56.1:8002
Eureka 自我保護
故障現象
保護模式主要用於一組客戶端 和 Eureka Server 之間存在網絡分區場景下的保護。
一旦進入保護模式,Eureka Server 將會嘗試保護其服務注冊表中的信息,不再刪除服務注冊表中的數據,也就是說不會注銷任何微服務。
如果在 Eureka Server 的首頁看到以下這段提示,則說明 Eureka 進入到保護模式 。
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
導致原因
一句話 :某時刻某一個微服務不可用了,Eureka 不會馬上清理,依舊會對該微服務的信息進行保存。
屬於 CAP 原則里面的 AP 分支
為什么會產生 Eureka 自我保護機制 ?
為了防止 EurekaClient可以正常運行,但是與 EurekaServer 網絡不通情況下,EurekaServer 不會馬上將 EurekaClient 服務剔除
什么是自我保護模式 ?
默認情況下,如果 EurekaServer 在一定時間內沒有接收到某個微服務實例的心跳,EurekaServer 將會注銷該實例(默認90s)。但是當網絡分區故障發生時(延時、卡頓、擁擠),微服務與 EurekaServer 之間無法正常通信,以上行為可能變得非常危險。因為微服務本身其實是健康的,此時本不應該注銷這個微服務。
Eureka 通過 “自我保護模式” 來解決這個問題 —— 當 EurekaServer 節點在短時間內丟失過多客戶端時(可能發生了網絡分區故障),那么這個節點就會進入自我保護模式。
它的設計就是 寧可保留錯誤的服務注冊信息,也不盲目注銷任何可能健康的服務實例。
綜上,自我保護模式是一種應對網絡異常的安全保護措施。
禁止自我保護機制
默認是開啟的
服務注冊中心 EurekaServer
eureka
server:
#關閉自我保護機制,保證不可用服務被及時踢除
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000
服務消費者 EurekaClient
eureka:
#Eureka客戶端向服務端發送心跳的時間間隔,單位為秒(默認是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服務端在收到最后一次心跳后等待時間上限,單位為秒(默認是90秒),超時將剔除服務
lease-expiration-duration-in-seconds: 2
zookeeper 注冊與發現
zookeeper 的服務節點是臨時節點
入門
https://blog.csdn.net/qq_41112238/article/details/105240421
首先在 centos 7 系統安裝 zookeeper ,zookeeper 依賴 jdk ,需要先安裝 jdk
依賴
<!-- SpringBoot整合zookeeper客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自帶的zookeeper-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.4.9版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
application.yaml
#8004表示注冊到zookeeper服務器的支付服務提供者端口號
server:
port: 8004
#服務別名----注冊zookeeper到注冊中心名稱
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
# zookeeper 服務器ip+端口
connect-string: 192.168.0.199:2181
App.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
// 該注解用於向使用consul或者zookeeper作為注冊中心時注冊服務
@EnableDiscoveryClient
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
將服務注冊進 zookeeper
如圖,服務名 與 配置文件配置的相同,說明注冊成功
Consul
Consul 是什么 ?
Consul 是一套開源的分布式服務發現和配置管理系統, Go 語言開發
Consul 提供了微服務系統中的 服務治理、配置中心、控制總線等功能。這些功能每一個都可以根據需要單獨使用,也可以一起使用用以構建全方位的服務網格,Consul 提供了一種完整的服務網格解決方案 。
默認是安裝好的
使用開發模式啟動 consul agent -dev
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
服務提供者
配置
application.yaml
###consul服務端口號
server:
port: 8006
spring:
application:
name: consul-provider-payment
####consul注冊中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
App.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006
{
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class, args);
}
}
controller
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/consul")
public String paymentConsul()
{
return "springcloud with consul: "+serverPort+"\t "+ UUID.randomUUID().toString();
}
}
服務消費者
配置
application.yaml
###consul服務端口號
server:
port: 80
spring:
application:
name: cloud-consumer-order
####consul注冊中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
controller
import org.springframework.web.client.RestTemplate;
public class OrderConsulController
{
public static final String INVOKE_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping(value = "/consumer/payment/consul")
public String paymentInfo()
{
String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
return result;
}
}
注入RestTemplate
@Configuration
public class ApplicationContextConfig
{
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
App.java
@SpringBootApplication
@EnableDiscoveryClient //該注解用於向使用consul或者zookeeper作為注冊中心時注冊服務
public class OrderConsulMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
三個注冊中心異同點
-
C : Consistency(強一致性)
-
A : Availability(可用性)
-
P : Partition tolerance(分區容錯性)
CAP理論關注粒度是數據,而不是整體系統設計的策略
最多只能同時較好的滿足兩個。
CAP理論的核心是:一個分布式系統不可能同時很好的滿足一致性,可用性和分區容錯性這三個需求,
因此,根據 CAP 原理將 NoSQL 數據庫分成了滿足 CA 原則、滿足 CP 原則和滿足 AP 原則三 大類:
CA - 單點集群,滿足一致性,可用性的系統,通常在可擴展性上不太強大。
CP - 滿足一致性,分區容忍必的系統,通常性能不是特別高。
AP - 滿足可用性,分區容忍性的系統,通常可能對一致性要求低一些。
Ribbon 入門介紹
Spring Cloud Ribbon是基於Netflix Ribbon實現的一套客戶端 負載均衡的工具。
簡單的說,Ribbon是Netflix發布的開源項目,主要功能是提供客戶端的軟件負載均衡算法和服務調用。Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等。簡單的說,就是在配置文件中列出Load Balancer(簡稱LB)后面所有的機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連接等)去連接這些機器。我們很容易使用Ribbon實現自定義的負載均衡算法。
LB負載均衡
負載均衡 + RestTemplate 調用
LB負載均衡(Load Balance)是什么
簡單的說就是將用戶的請求平攤的分配到多個服務上,從而達到系統的HA(高可用)。
常見的負載均衡有軟件Nginx,LVS,硬件 F5等。
Ribbon本地負載均衡客戶端 VS Nginx服務端負載均衡區別
Nginx是服務器負載均衡,客戶端所有請求都會交給nginx,然后由nginx實現轉發請求。即負載均衡是由服務端實現的。
Ribbon本地負載均衡,在調用微服務接口時候,會在注冊中心上獲取注冊信息服務列表之后緩存到JVM本地,從而在本地實現RPC遠程服務調用技術。
集中式LB
即在服務的消費方和提供方之間使用獨立的LB設施(可以是硬件,如F5, 也可以是軟件,如nginx), 由該設施負責把訪問請求通過某種策略轉發至服務的提供方;
進程內LB
將LB邏輯集成到消費方,消費方從服務注冊中心獲知有哪些地址可用,然后自己再從這些地址中選擇出一個合適的服務器。
Ribbon就屬於進程內LB,它只是一個類庫,集成於消費方進程,消費方通過它來獲取到服務提供方的地址。
Ribbon 核心組件 IRule
IRule:根據特定算法中從服務列表中選取一個要訪問的服務
- com.netflix.loadbalancer.RoundRobinRule
- 輪詢
- com.netflix.loadbalancer.RandomRule
- 隨機
- com.netflix.loadbalancer.RetryRule
- 先按照RoundRobinRule的策略獲取服務,如果獲取服務失敗則在指定時間內會進行重試,獲取可用的服務
- WeightedResponseTimeRule
- 對RoundRobinRule的擴展,響應速度越快的實例選擇權重越大,越容易被選擇
- BestAvailableRule
- 會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然后選擇一個並發量最小的服務
- AvailabilityFilteringRule
- 先過濾掉故障實例,再選擇並發較小的實例
- ZoneAvoidanceRule
- 默認規則,復合判斷server所在區域的性能和server的可用性選擇服務器
Ribbon負載均衡替換
官方文檔明確給出了警告
自定義配置類不能放在
@ComponentScan
所掃描的當前包下及其子包下,否則 自定義的配置類就會被 Ribbon 客戶端所共享,達不到特殊化定制的目的
配置 自定義 負載均衡 規則類
MySelfRule.java
@Configuration
public class MySelfRule
{
@Bean
public IRule myRule()
{
return new RandomRule();//定義為隨機
}
}
主啟動類 App.java
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)
public class OrderMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MySelfRule.class)
當前服務消費者訪問 支付微服務,配置
configuration
不要用默認配置,改為自定義的隨機 負載均衡
Ribbon負載均衡算法
原理
負載均衡算法:
rest接口第幾次請求數 % 服務器集群總數量 = 實際調用服務器位置下標 ,每次服務重啟動后rest接口計數從1開始。
List instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
如: List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
- 8001+ 8002 組合成為集群,它們共計2台機器,集群總數為2,
按照輪詢算法原理:
當總請求數為1時: 1 % 2 =1 對應下標位置為1 ,則獲得服務地址為127.0.0.1:8001
當總請求數位2時: 2 % 2 =0 對應下標位置為0 ,則獲得服務地址為127.0.0.1:8002
當總請求數位3時: 3 % 2 =1 對應下標位置為1 ,則獲得服務地址為127.0.0.1:8001
當總請求數位4時: 4 % 2 =0 對應下標位置為0 ,則獲得服務地址為127.0.0.1:8002
如此類推......
12 - 15 29
RoundRobinRule 源碼
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
手寫一個負載的算法
原理 + JUC (CAS + 自旋鎖)
接口
public interface LoadBalancer
{
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
實現類
@Component
public class MyLB implements LoadBalancer
{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement()
{
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= 2147483647 ? 0 : current + 1;
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("*****第幾次訪問,次數next: "+next);
return next;
}
//負載均衡算法:rest接口第幾次請求數 % 服務器集群總數量 = 實際調用服務器位置下標 ,每次服務重啟動后rest接口計數從1開始。
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances)
{
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
controller
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() <= 0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
8001端口 微服務 controller
@GetMapping(value = "/payment/lb")
public String getPaymentLB()
{
return serverPort;
}
運行
OpenFeign 服務調用
Feign是一個聲明式的Web服務客戶端,讓編寫Web服務客戶端變得非常容易,只需創建一個接口並在接口上添加注解即可
Feign 在消費端使用
Feign 和 OpenFeign 兩者區別
FeignOpenFeignFeign是Spring Cloud組件中的一個輕量級RESTful的HTTP服務客戶端Feign內置了Ribbon,用來做客戶端負載均衡,去調用服務注冊中心的服務。Feign的使用方式是:使用Feign的注解定義接口,調用這個接口,就可以調用服務注冊中心的服務
OpenFeign是Spring Cloud 在Feign的基礎上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,並通過動態代理的方式產生實現類,實現類中做負載均衡並調用其他服務。
org.springframework.cloud spring-cloud-starter-feign
org.springframework.cloud spring-cloud-starter-openfeign
入門案例
依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
application.yaml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
主啟動類
加上注解 開啟
@EnableFeignClients
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80
{
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
業務邏輯接口+@FeignClient配置調用provider服務
新建PaymentFeignService接口並新增注解@FeignClient
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
Eureka 注冊中心中注冊的服務名稱 (要調用的服務名)
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService
{
@GetMapping(value = "/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
controller
public class OrderFeignController
{
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping(value = "/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
{
return paymentFeignService.getPaymentById(id);
}
}
客戶端的服務接口使用用
@FeignClient
注解根據服務名稱去調用服務提供方的具體服務微服務調用接口+@FeignClient
OpenFeign超時控制
默認Feign客戶端只等待一秒鍾,但是服務端處理需要超過1秒鍾,導致Feign客戶端不想等待了,直接返回報錯。為了避免這樣的情況,有時候我們需要設置Feign客戶端的超時控制。yml文件中開啟配置
在 application.yaml 配置文件中添加 如下配置
#設置feign客戶端超時時間(OpenFeign默認支持ribbon)
ribbon:
#指的是建立連接所用的時間,適用於網絡狀況正常的情況下,兩端連接所用的時間
ReadTimeout: 5000
#指的是建立連接后從服務器讀取到可用資源所用的時間
ConnectTimeout: 5000
OpenFeign日志打印功能
使用步驟
配置日志bean
注入 OpenFeign 日志 Bean
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL;
}
}
YML文件里需要開啟日志的Feign客戶端,在yaml配置文件中 添加如下配置
logging:
level:
# feign日志以什么級別監控哪個接口
com.atguigu.springcloud.service.PaymentFeignService: debug
Hystrix斷路器
前言
服務雪崩
分布式系統面臨的問題復雜分布式體系結構中的應用程序有數十個依賴關系,每個依賴關系在某些時候將不可避免地失敗。 服務雪崩多個微服務之間調用的時候,假設微服務A調用微服務B和微服務C,微服務B和微服務C又調用其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的調用響應時間過長或者不可用,對微服務A的調用就會占用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”.
對於高流量的應用來說,單一的后端依賴可能會導致所有服務器上的所有資源都在幾秒鍾內飽和。比失敗更糟糕的是,這些應用程序還可能導致服務之間的延遲增加,備份隊列,線程和其他系統資源緊張,導致整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關系的失敗,不能取消整個應用程序或系統。
所以,通常當你發現一個模塊下的某個實例失敗后,這時候這個模塊依然還會接收流量,然后這個有問題的模塊還調用了其他的模塊,這樣就會發生級聯故障,或者叫雪崩。
Hystrix 是什么 ?
Hystrix是一個用於處理分布式系統的延遲和容錯的開源庫,在分布式系統里,許多依賴不可避免的會調用失敗,比如超時、異常等,Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分布式系統的彈性。
“斷路器”本身是一種開關裝置,當某個服務單元發生故障之后,通過斷路器的故障監控(類似熔斷保險絲),向調用方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常,這樣就保證了服務調用方的線程不會被長時間、不必要地占用,從而避免了故障在分布式系統中的蔓延,乃至雪崩。
Hystrix能做什么?
主要有服務降級、服務熔斷、接近實時的監控、限流、隔離等等,其官方文檔參考。當然Hystrix現在已經停更了,雖然有一些替代品,但是學習Hystrix及其里面的思想還是非常重要的!
Hystrix重要概念
1、服務降級 —— Fall Back
假設微服務A要調用的服務B不可用了,需要服務B提供一個兜底的解決方法,而不是讓服務A在那里傻等,耗死。不讓客戶端等待並立刻返回一個友好圖示,比如像客戶端提示服務器忙,請稍后再試等。
哪些情況會觸發服務降級呢?
- 比如程序運行異常、超時、服務熔斷觸發服務降級、線程池/信號量打滿也會導致服務降級。
2、服務熔斷 —— Break
可以理解為保險絲,首先是服務的降級 -> 進而熔斷 -> 再恢復調用鏈路
服務熔斷就相當於物理上的熔斷保險絲。類比保險絲達到最大服務訪問后,直接拒絕訪問,拉閘斷點,然后調用服務降級的方法並返回友好提示。
熔斷 -> 降級
3、服務限流 —— Flow Limit
秒殺高並發等操作,嚴禁一窩蜂的過來擁擠,大家排隊,一秒鍾N個,有序進行。
Hystrix 微服務構建
依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
application.yaml
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
defaultZone: http://eureka7001.com:7001/eureka
主啟動類
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001
{
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
@Service
public class PaymentService
{
/**
* 正常訪問,肯定OK
* @param id
* @return
*/
public String paymentInfo_OK(Integer id)
{
return "線程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
public String paymentInfo_TimeOut(Integer id)
{
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "線程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗時(秒): ";
}
}
@RestController
@Slf4j
public class PaymentController
{
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_OK(id);
log.info("*****result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*****result: "+result);
return result;
}
}
不模擬高並發下 訪問接口 測試
訪問服務超時或服務宕機如何解決?
服務降級 Fall Back
服務端 - 服務提供方服務降級
首先在服務提供方的業務類上啟用
@HystrixCommand
實現報異常后如何處理,也就是一旦調用服務方法失敗並拋出了錯誤信息后,會自動調用@HystrixCommand
標注好的fallbackMethod服務降級方法。然后在主啟動類上添加
@EnableCircuitBreaker
注解對熔斷器進行激活
在 服務接口方法上 添加
@HystrixCommand
注解 ,如下文
{
// fallbackMethod = "paymentInfo_TimeOutHandler" 超時走 paymentInfo_TimeOutHandler() 方法
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
})
// 五秒以內走正常業務邏輯
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
}
}
@Service
public class PaymentService
{
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
})
public String paymentInfo_TimeOut(Integer id)
{
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "線程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗時(秒): ";
}
public String paymentInfo_TimeOutHandler(Integer id)
{
return "線程池: "+Thread.currentThread().getName()+" 8001系統繁忙或者運行報錯,請稍后再試,id: "+id+"\t"+"o(╥﹏╥)o";
}
}
主啟動類激活 在App.java 類上加上
@EnableCircuitBreaker
注解
@EnableCircuitBreaker
fallbackMethod
后面跟的什么方法名,就是誰來處理接口服務異常.
程序運行異常,超時異常等服務不可用了,做服務降級,兜底的方案都是 paymentInfo_TimeOutHandler()
FallBack 服務降級,一般是放在客戶端 服務消費方
客戶端 - 服務消費方的服務降級
服務的提供方可以進行降級保護,那么服務的消費方,也可以更好的保護自己,也可以對自己進行降級保護,也就是說Hystrix服務降級既可以放在服務端(服務提供方),也可以放在客戶端(服務消費方),但是!!!通常是用客戶端做服務降級,
開啟 消費方 支持 Hystrix 配置
feign:
hystrix:
enabled: true
主啟動 App.java 類上 添加 @EnableHystrix
激活 注解 ,然后在 80端口的 Controller 中 加入 @HystrixCommand
注解 實現服務降級
public class PaymentHystirxController{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id); r
eturn result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消費者80,對方支付系統繁忙請10秒鍾后再試或者自己運行出錯請檢查自己,o(╥﹏╥)o";
}
}
解釋 :也就是說,如果消費方調用服務提供者接口的調用時間超過了 1.5 s,那么就會訪問消費方自己的服務降級方法 。
而當前的這種處理方式是有問題的,也就是每個業務方法都對應了一個服務降級犯法,這會導致代碼膨脹,所以我們應該定義一個統一的服務降級方法,統一的方法和自定義的方法分開。而且我們將服務降級方法和業務邏輯混合在了一起,這會導致代碼混亂,業務邏輯不清晰
1、每個方法配置一個???膨脹 ? 2、和業務邏輯混一起???混亂?
全局服務降級 DefaultProperties
對於第一個問題,我們可以使用 feign 接口的
@DefaultProperties(defaultFallback = "")
注解來配置全局的服務降級方法 。配置過
@HystrixCommand(fallbackMethod = "")
注解的方法采用自己配置的服務降級方法,沒有配置鍋自己的服務降級處理就采用@DefaultProperties(defaultFallback = "")
配置的全局服務降級方法,這樣的話通用的服務降級方法和獨享的服務降級方法分開,避免了代碼膨脹,合理減少了代碼量
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController
{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
/*@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})*/
//@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id)
{
return "我是消費者80,對方支付系統繁忙請10秒鍾后再試或者自己運行出錯請檢查自己,o(╥﹏╥)o";
}
// 下面是全局fallback方法
public String payment_Global_FallbackMethod()
{
return "Global異常處理信息,請稍后再試,/(ㄒoㄒ)/~~";
}
}
對於第二個問題,可以為 Feign 客戶端定義的調用服務提供方接口 添加一個服務降級處理的實現類實現解耦,新建一個類並實現該接口,重寫接口方法。為接口內的方法進行異常處理,並且在 其 實現的接口內聲明服務降級方法所在的類 。
application.yaml
feign:
hystrix:
enabled: true #在Feign中開啟Hystrix
接口
@Component
//當出現錯誤時,到PaymentFallbackService類中找服務降級方法
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,fallback = PaymentFallbackService.class)
public interface PaymentHystrixService
{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
實現類
@Component
public class PaymentFallbackService implements PaymentHystrixService
{
@Override
public String paymentInfo_OK(Integer id)
{
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}
@Override
public String paymentInfo_TimeOut(Integer id)
{
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
}
移除controller 中的耦合代碼,並測試
@RestController
@Slf4j
// @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController
{
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
/*@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})*/
//@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
服務熔斷 Break
https://martinfowler.com/bliki/CircuitBreaker.html
熔斷機制概述
熔斷機制是應對雪崩效應的一種微服務鏈路保護機制。當扇出鏈路的某個微服務出錯不可用或者響應時間太長時,會進行服務的降級,進而熔斷該節點微服務的調用,快速返回錯誤的響應信息。當檢測到該節點微服務調用響應正常后,恢復調用鏈路。
在Spring Cloud框架里,熔斷機制通過Hystrix實現。Hystrix會監控微服務間調用的狀況,當失敗的調用到一定閾值,缺省是5秒內20次調用失敗,就會啟動熔斷機制。熔斷機制的注解是@HystrixCommand。
這個簡單的斷路器避免在電路打開時進行受保護的呼叫,但當一切恢復正常時需要外部干預來重置它。對於建築物中的電路斷路器,這是一種合理的方法,但對於軟件斷路器,我們可以讓斷路器本身檢測底層調用是否再次工作。我們可以通過在合適的時間間隔后再次嘗試受保護的調用來實現這種自重置行為,並在成功時重置斷路器
服務熔斷案例
在服務提供方的 Service 中添加如下代碼
@Service
public class PaymentService{
...
//=====服務熔斷
/**
* fallbackMethod 服務降級方法
* circuitBreaker.enabled 是否開啟斷路器
* circuitBreaker.requestVolumeThreshold 請求次數
* circuitBreaker.sleepWindowInMilliseconds 時間窗口期
* circuitBreaker.errorThresholdPercentage 失敗率達到多少后跳閘
* 以下配置意思是在10秒時間內請求10次,如果有6此是失敗的,就觸發熔斷器
* 注解@HystrixProperty中的屬性在com.netflix.hystrix.HystrixCommandProperties類中查看
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否開啟斷路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 請求次數
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 時間窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失敗率達到多少后跳閘
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
if(id < 0)
{
throw new RuntimeException("******id 不能負數");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"調用成功,流水號: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
return "id 不能負數,請稍后再試,/(ㄒoㄒ)/~~ id: " +id;
}
}
在 controller 加入如下代碼
//====服務熔斷
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}
@HystrixCommand
請求次數+時間窗口期+失敗率多少后跳閘 -> 在10s時間內 訪問接口次數10次 達到60%失敗率 跳閘。
@HystrixCommand
注解中 @HystrixCommand
配置熔斷機制的參數 如下
屬性 | 含義 | 默認值 |
---|---|---|
circuitBreaker.enabled | 是否開啟斷路器 | true |
circuitBreaker.requestVolumeThreshold | 請求次數 | 20 |
circuitBreaker.sleepWindowInMilliseconds | 時間窗口期 | 5000 |
circuitBreaker.errorThresholdPercentage | 失敗率達到多少后跳閘 | 50 |
這些屬性名的具體含義一級其默認值可以在 com.netflix.hystrix.HystrixCommandProperties
類中進行查看。而我們在service中配置的意思就是在10秒時間內請求10次,如果有6次是失敗的,就觸發熔斷器
觸發了服務熔斷,即使再進行正確的訪問也無法進行,但是一定時間后,正確的服務訪問又可以順利進行,這就是服務熔斷的整體過程:在觸發了服務熔斷后,先進行服務的降級,再逐漸恢復調用鏈路
總結
熔斷類型
熔斷器打開 OPEN | 請求不再進行調用當前服務,內部設置時鍾一般為MTTR(平均故障處理時間),當打開時長達到所設時鍾則進入半熔斷狀態 |
---|---|
熔斷器關閉 CLOSED | 熔斷關閉不會對服務進行熔斷 |
熔斷器半開 HALF-OPEN | 部分請求根據規則調用當前服務,如果請求成功且符合規則則認為當前服務恢復正常,關閉熔斷 |
熔斷器打開和關閉方式
- 1、假設電路上的訪問達到某個閾值(
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
) - 2、並假設誤差百分比超過閾值誤差百分比(
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
) - 3、然后,斷路器從
CLOSED
為OPEN
,觸發熔斷機制 - 4、當它斷開時,它會使針對該斷路器的所有請求短路
- 5、經過一段時間(
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
)后,會讓其中一個請求被允許通過(這是HALF-OPEN
狀態)。如果請求失敗,繼續開啟。重復4和5,斷路器將OPEN
在睡眠窗口期間返回到該狀態,如果請求成功,則斷路器切換到,CLOSED
並且由 步驟 1的邏輯接管 。
官網的熔斷器流程圖
斷路器在什么情況下開始起作用
- 涉及到斷路器的三個重要參數:快照時間窗、請求總數閥值、錯誤百分比閥值。
1:快照時間窗
circuitBreaker.sleepWindowInMilliseconds
:斷路器確定是否打開需要統計一些請求和錯誤數據,而統計的時間范圍就是快照時間窗,默認為最近的10秒。2:請求總數閥值
circuitBreaker.requestVolumeThreshold
:在快照時間窗內,必須滿足請求總數閥值才有資格熔斷。默認為20,意味着在10秒內,如果該hystrix命令的調用次數不足20次,即使所有的請求都超時或其他原因失敗,斷路器都不會打開。3:錯誤百分比閥值
circuitBreaker.errorThresholdPercentage
:當請求總數在快照時間窗內超過了閥值,比如發生了30次調用,如果在這30次調用中,有15次發生了超時異常,也就是超過50%的錯誤百分比,在默認設定50%閥值情況下,這時候就會將斷路器打開。
斷路器打開之后
1:再有請求調用的時候,將不會調用主邏輯,而是直接調用降級fallback。通過斷路器,實現了自動地發現錯誤並將降級邏輯切換為主邏輯,減少響應延遲的效果。
2:原來的主邏輯要如何恢復呢?
- 當斷路器打開,對主邏輯進行熔斷之后,hystrix會啟動一個休眠時間窗,在這個時間窗內,降級邏輯是臨時的成為主邏輯,當休眠時間窗到期,斷路器將進入半開狀態,釋放一次請求到原來的主邏輯上,如果此次請求正常返回,那么斷路器將繼續閉合,主邏輯恢復,如果這次請求依然有問題,斷路器繼續進入打開狀態,休眠時間窗重新計時。
HyStrix 工作流程圖
步驟說明
1創建 HystrixCommand(用在依賴的服務返回單個操作結果的時候) 或 HystrixObserableCommand(用在依賴的服務返回多個操作結果的時候) 對象。
2命令執行。其中 HystrixComand 實現了下面前兩種執行方式;而 HystrixObservableCommand 實現了后兩種執行方式:execute():同步執行,從依賴的服務返回一個單一的結果對象, 或是在發生錯誤的時候拋出異常。queue():異步執行, 直接返回 一個Future對象, 其中包含了服務執行結束時要返回的單一結果對象。observe():返回 Observable 對象,它代表了操作的多個結果,它是一個 Hot Obserable(不論 "事件源" 是否有 "訂閱者",都會在創建后對事件進行發布,所以對於 Hot Observable 的每一個 "訂閱者" 都有可能是從 "事件源" 的中途開始的,並可能只是看到了整個操作的局部過程)。toObservable(): 同樣會返回 Observable 對象,也代表了操作的多個結果,但它返回的是一個Cold Observable(沒有 "訂閱者" 的時候並不會發布事件,而是進行等待,直到有 "訂閱者" 之后才發布事件,所以對於 Cold Observable 的訂閱者,它可以保證從一開始看到整個操作的全部過程)。
3若當前命令的請求緩存功能是被啟用的, 並且該命令緩存命中, 那么緩存的結果會立即以 Observable 對象的形式 返回。
4檢查斷路器是否為打開狀態。如果斷路器是打開的,那么Hystrix不會執行命令,而是轉接到 fallback 處理邏輯(第 8 步);如果斷路器是關閉的,檢查是否有可用資源來執行命令(第 5 步)。
5線程池/請求隊列/信號量是否占滿。如果命令依賴服務的專有線程池和請求隊列,或者信號量(不使用線程池的時候)已經被占滿, 那么 Hystrix 也不會執行命令, 而是轉接到 fallback 處理邏輯(第8步)。
6Hystrix 會根據我們編寫的方法來決定采取什么樣的方式去請求依賴服務。HystrixCommand.run() :返回一個單一的結果,或者拋出異常。HystrixObservableCommand.construct(): 返回一個Observable 對象來發射多個結果,或通過 onError 發送錯誤通知。
7Hystrix會將 "成功"、"失敗"、"拒絕"、"超時" 等信息報告給斷路器, 而斷路器會維護一組計數器來統計這些數據。斷路器會使用這些統計數據來決定是否要將斷路器打開,來對某個依賴服務的請求進行 "熔斷/短路"。
8當命令執行失敗的時候, Hystrix 會進入 fallback 嘗試回退處理, 我們通常也稱該操作為 "服務降級"。而能夠引起服務降級處理的情況有下面幾種:第4步: 當前命令處於"熔斷/短路"狀態,斷路器是打開的時候。第5步: 當前命令的線程池、 請求隊列或 者信號量被占滿的時候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 拋出異常的時候。
9當Hystrix命令執行成功之后, 它會將處理結果直接返回或是以Observable 的形式返回。tips:如果我們沒有為命令實現降級邏輯或者在降級處理邏輯中拋出了異常, Hystrix 依然會返回一個 Observable 對象, 但是它不會發射任何結果數據, 而是通過 onError 方法通知命令立即中斷請求,並通過onError()方法將引起命令失敗的異常發送給調用者。
服務監控hystrixDashboard
除了隔離依賴服務的調用以外,Hystrix還提供了准實時的調用監控(Hystrix Dashboard),Hystrix會持續地記錄所有通過Hystrix發起的請求的執行信息,並以統計報表和圖形的形式展示給用戶,包括每秒執行多少請求多少成功,多少失敗等。Netflix通過hystrix-metrics-event-stream項目實現了對以上指標的監控。Spring Cloud也提供了Hystrix Dashboard的整合,對監控內容轉化成可視化界面。
-
新建Module:cloud-consumer-hystrix-dashboard9001作為Hystrix Dashboard服務
-
添加依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>
- application.yaml
server:
port: 9001
- 主程序 在主程序類上 添加
@EnableHystrixDashboard
注解 開啟 HystrixDashboard 功能
package cn.sher6j.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* @author sher6j
* @create 2020-05-21-23:47
*/
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class);
}
}
- 所有的服務提供方微服務(如我們的8001/8002)都需要監控依賴配置
<!--actuator監控信息完善-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在服務提供方的主程序類中添加如下配置
/**
*此配置是為了服務監控而配置,與服務容錯本身無關,springcloud升級后的坑
*ServletRegistrationBean因為springboot的默認路徑不是"/hystrix.stream",
*只要在自己的項目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
如何觀察監控窗口
七種顏色,狀態值,
曲線:用來記錄2分鍾內流量的相對變化,可以通過它來觀察到流量的上升和下降趨勢。
實心圓:共有兩種含義。它通過顏色的變化代表了實例的健康程度,它的健康度從綠色<黃色<橙色<紅色遞減。該實心圓除了顏色的變化之外,它的大小也會根據實例的請求流量發生變化,流量越大該實心圓就越大。所以通過該實心圓的展示,就可以在大量的實例中快速的發現故障實例和高壓力實例。
服務網關 GateWay
概述
SpringCloud Gateway是什么?
服務網關還可以用Zuul網關,但是Zuul網關由於一些維護問題,所以這里我們學習Gateway網關,SpringCloud全家桶里有個很重要的組件就是網關, 在1.x的版本中都是采用Zuul網關;但在2.x版本中,Zuul的升級一直跳票,SpringCloud最后自己研發了一個網關代替Zuul,也就是說SpringCloud Gateway是原Zuul1.x版的替代品。SpringCloud Gateway是在Spring生態系統之上構建的API網關服務,基於Spring5,SpringBoot2和Project Reactor等技術。Gateway旨在提供一種簡單而有效的方式來對API進行路由,以及提供一些強大的過濾功能,例如熔斷、限流、重試等。
SpringCloud Gateway作為SpringCloud生態系統中的網關,目的是替代Zuul,在SpringCloud2.0以上版本中,沒有對新版本的Zuul2.0以上最新高新能版本進行集成,仍然使用的是Zuul 1.x非Reactor模式的老版本。而為了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
SpringCloud Gateway的目標是提供統一的路由方式且基於Filter鏈的方式提供了網關基本的功能,例如:安全、監控/指標、限流等。
SpringCloud Gateway 使用的Webflux中的reactor-netty響應式編程組件,底層使用了Netty通訊框架。
SpringCloud Gateway能做什么?
反向代理、鑒權、流量控制、熔斷、日志監控等等。SpringCloud Gateway具有如下特性:
- 基於Spring Framework 5, Project Reactor和SpringBoot 2.x進行構建;
- 動態路由:能夠匹配任何請求屬性;
- 可以對路由指定Predicate(斷言)和Filter(過濾器);
- 集成Hystrix的熔斷器功能;
- 集成SpringCloud服務發現功能;
- 請求限流功能;
- 支持路徑重寫等待。
在整個微服務架構中,網關的位置
網關是所有微服務的入口。
SpringCloud Gateway 與 Zuul的區別
在SpringCloud Finchley正式版以前,SpringCloud推薦的網關是Netflix提供的Zuul:
- Zuul 1.x 是一個基於阻塞I/O的API Gateway;
- Zuul 1.x 基於Servlet 2.5使用阻塞架構,它不支持任何長連接(如WebSocket),Zuul的設計模式和Nginx比較像,每次I/O操作都是從工作線程中選擇一個執行,請求線程被阻塞到工作線程完成,但是差別是Nginx是用C++實現的,Zuul用Java實現,而JVM本身會有第一次加載較慢的情況,使得Zuul的性能相對較差;
- Zuul 2.x 理念更先進,想基於Netty非阻塞和支持長連接,但是SpringCloud目前還沒有整合。Zuul 2.x 的性能較Zull 1.x有很大提升。根據官方提供的基准測試,SpringCloud Gateway的RPS(每秒請求數)是Zuul的1.6倍。
- SpringCloud Gateway基於Spring Framework 5, Project Reactor和SpringBoot 2.x進行構建,使用非阻塞API,還支持WebSocket。
核心概念
路由 Route
- 路由是構建網關的基本模塊,它由ID,目標URI,一系列的斷言和過濾器組成,如果斷言為true則匹配該路由
斷言 Predicate
- 參考的是Java8的
java.util.function.Predicate
開發人員可以匹配HTTP請求中的所有內容(例如請求頭或請求參數),如果請求與斷言相匹配則進行路由
過濾器 Filter
- 指的是Spring框架中GatewayFilter的實例,使用過濾器,可以在請求被路由前或者之后對請求進行修改。
總結
web請求,通過一些匹配條件,定位到真正的服務節點。並在這個轉發過程的前后,進行一些精細化控制。predicate就是我們的匹配條件;而filter,就可以理解為一個無所不能的攔截器。有了這兩個元素,再加上目標uri,就可以實現一個具體的路由了
網關工作流程
客戶端向Spring Cloud Gateway發出請求。如果網關處理程序映射(Gateway Handler Mapping)確定請求與路由匹配,則將其發送到網關Web處理程序(Gateway Web Handler)。(Hanndler 再通過指定的過濾器鏈來將請求發送到實際的服務執行業務邏輯,然后返回。)該處理程序通過特定於請求的過濾器鏈來運行請求。過濾器器由虛線分隔的原因是,過濾器可以在發送代理請求之前和之后運行邏輯。所有“前置”過濾器邏輯均被執行。然后發出代理請求。發出代理請求后,將運行“后置”過濾器邏輯。圖中虛線左邊的對應於前置過濾器,虛線右邊的對應於后置過濾器。
前置過濾器可以做參數校驗、權限校驗、流量監控、日志輸出、協議轉換等;
后置過濾器可以做響應內容、響應頭的修改、日志的輸出、流量監控等。SpringCloud Gateway的核心邏輯其實就是路由轉發和執行過濾器鏈
核心邏輯 > 路由轉發 + 執行過濾鏈
入門配置
依賴 需要注意的是,網關微服務不需要引入Web啟動器
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
主啟動類
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
修改 application.yaml 配置
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,沒有固定規則但要求唯一,建議配合服務名
uri: http://localhost:8001 #匹配后提供服務的路由地址
predicates:
- Path=/payment/get/** # 斷言,路徑相匹配的進行路由
- id: payment_routh2 #payment_route #路由的ID,沒有固定規則但要求唯一,建議配合服務名
uri: http://localhost:8001 #匹配后提供服務的路由地址
predicates:
- Path=/payment/lb/** # 斷言,路徑相匹配的進行路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服務提供者provider注冊進eureka服務列表內
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka, # http://eureka7002.com:7002/eureka
運行
SpringCloud Gateway的網關路由有兩種配置方式,
一種就是上面通過配置文件application.yml進行網關路由配置,
還可以在代碼中注入
RouteLocator
的Bean進行配置
非配置文件,編碼方式實現 網關路由配置
編寫如下配置類
@Configuration
public class GateWayConfig {
/**
* 配置了一個id為 path_route_atguigu 的路由規則,
* 當訪問地址 http://localhost:9527/guonei時會自動轉發到地址:http://news.baidu.com/guonei
* @param builder
* @return
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
兩者 對應關系
通過微服務名實現動態路由
默認情況下Gateway會根據注冊中心注冊的服務列表,以注冊中心上微服務名為路徑創建動態路由進行轉發,從而實現動態路由的功能
在 application.yaml 添加如下配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #開啟從注冊中心動態創建路由的功能,利用微服務名進行路由
將原 域名+端口的 uri 替換成如下配置
需要注意的是uri的協議為lb,表示啟用Gateway的負載均衡功能。
lb://serviceName是spring cloud gateway在微服務中自動為我們創建的負載均衡uri
uri: lb://cloud-payment-service #匹配后提供服務的路由地址
運行
- 可以看出其默認的負載均衡算法也是輪詢負載均衡
Route Predicate Factories這個是什么?
Spring Cloud Gateway將路由匹配作為Spring WebFlux HandlerMapping基礎架構的一部分。
Spring Cloud Gateway包括許多內置的Route Predicate工廠。所有這些Predicate都與HTTP請求的不同屬性匹配。多個Route Predicate工廠可以進行組合
Spring Cloud Gateway 創建 Route 對象時, 使用 RoutePredicateFactory 創建 Predicate 對象,Predicate 對象可以賦值給 Route。 Spring Cloud Gateway 包含許多內置的Route Predicate Factories。
所有這些謂詞都匹配HTTP請求的不同屬性。多種謂詞工廠可以組合,並通過邏輯and。
http://localhost:9527/payment/lb --cookie "username=zzyy"
說白了,Predicate就是為了實現一組匹配規則,讓請求過來找到對應的Route進行處理。
自定義過濾器
SpringCloud Gateway中自定義過濾器要是實現兩個接口
org.springframework.cloud.gateway.filter.GlobalFilter
和org.springframework.core.Ordered
。前者實現了全局過濾器,后者規定了過濾器的執行順序,該順序數字越小,過濾器會優先被執行 。
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null) {
log.info("*******用戶名為null,非法用戶,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
Config 分布式配置中心
將公共配置 匯總到 一個中里面 ,三台服務器中的配置有一半是相同,難不成要寫三份部分相同的配置?那相同的配置發生變更了,也修改三次?哭了,所以需要一個統一管理公共的配置文件的地方。相當於碼頭的角色,——SpringCloud Config 配置中心。
是什么?能干嘛?怎么玩?
微服務意味着要將單體應用中的業務拆分成一個一個子服務,每個服務的粒度相對較小,因此系統中會出現大量的 服務。由於每個服務都需要必要的配置才能運行,所以一套集中式的、動態的配置管理設施是必不可少的。
SpringCloud提供了Config Server來解決這個問題,
SpringCloud Config為微服務架構中的微服務提供集中化的外部配置支持,配置服務器為各個不同微服務應用的所有環境提供了一個中心化的外部配置。
SpringCloud Config分為服務端和客戶端兩部分,
- 服務端也稱為分布式配置中心,它是一個獨立的微服務應用,用來連接配置服務器並為客戶端提供獲取配置信息,加密/解密信息等訪問接口,將配置信息以REST接口的形式暴露給客戶端(客戶端可以用REST風格方式讀取到該配置信息)。
- 客戶端則是通過指定的配置中心來管理應用資源,以及與業務相關的配置內容,並在啟動的時候從配置中心獲取和加載配置信息。配置服務器默認采用git來存儲配置信息,這樣就有助於對環境配置進行版本管理,並且可以通過git客戶端工具來方便的管理和訪問配置內容
上圖 ,服務配置中心從遠端讀取配置文件,然后客戶端再通過服務配置中心讀取配置
config 服務端配置
在 git 上新建一個用作配置中心的倉庫,(github、gitlab),並克隆到本地
新建Module:cloud-config-center-3344作為配置中心微服務
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
application.yml配置
server:
port: 3344
spring:
application:
name: cloud-config-center #注冊進Eureka服務器的微服務名
cloud:
config:
server:
git:
uri: https://gitee.com/yuanwu233/spring-cloud.git #GitHub上面的git倉庫名字
username: xxx
password: xxx
skip-ssl-validation: true
####搜索目錄
search-paths:
- spring-cloud
####讀取分支
label: main
#服務注冊到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
主啟動類 添加
@EnableConfigServer
注解 ,使其具有配置中心功能。
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344{
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
啟動 eureka 注冊中心,在啟動 config 配置中心
值得注意的是,我配置了 本地 ip 映射 域名 config-3344.com 但用域名訪問訪問不到
config 客戶端配置
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
配置文件 bootstrap.yml
application.yml是用戶級的資源配置項,
bootstrap.yml是系統級的資源配置項,bootstrap.yml的優先級更高,
SpringCloud會創建一個"Bootstrap Context",作為Spring應用的“Application Context"的父上下文。初始化的時候,“Bootstrap Context"負責從外部源加載配置屬性並解析配置,這兩個上下文共享一個從外部獲取的"Environment”。
”Bootstrap“屬性有高優先級,默認情況系,它們不會被本地配置覆蓋。"Bootstrap Context"和"Application Context"這兩個上下文有不同的約定,所以新增一個bootstrap.yml文件,保證這兩個上下文的配置分離
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客戶端配置
config:
label: main #分支名稱
name: config #配置文件名稱
profile: dev #讀取后綴名稱 上述3個綜合:master分支上config-dev.yml的配置文件被讀取http://config-3344.com:3344/main/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服務注冊到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
主啟動類
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
controller
@RestController
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
如果一切正常 , 3355 端口 是可以拿到 3344 配置中心中的 配置信息,
此時,存在問題
我們在GitHub上修改配置文件內容,刷新3344配置中心服務端,發現Config Server配置中心立刻響應並刷新了配置信息,但是!!我們刷新3355客戶端Config Client,發現沒有任何響應,配置信息仍然是原來的配置信息
難道每次遠端修改了配置文件后,客戶端都需要重啟來進行對配置信息的重新加載嗎?
config 客戶端的動態刷新
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置文件 botstrap.yml
# 暴露監控端點
management:
endpoints:
web:
exposure:
include: "*"
在業務類 controller 添加如下 注解 使其客戶端具有刷新功能
@RefreshScope
@RestController
@RefreshScope
public class ConfigClientController{
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
在 windows 命令窗口 執行 curl -X POST "http://localhost:3355/actuator/refresh" 命令 刷新 客戶端 3355
當出現以上信息時激活刷新客戶端3355成功,再次訪問客戶端,發現已經可以得到刷新后的配置信息。
但是假設如果我們有多個微服務客戶端呢?難道每個微服務都需要執行一次POST請求進行手動刷新嗎?
事實上我們可以通過廣播的方式進行一次通知,處處生效,這里就要學習消息總線——SpringCloud Bus
Bus消息總線
簡述
用SpringCloud Config時,我們可以實現配置信息手動的動態刷新,也就是遠端配置信息發生改變后,需要告訴服務端配置信息發生變化后,服務端才會更新配置信息,而現在我們想要實現分布式自動刷新配置信息功能,
這就需要我們使用SpringCloud Bus消息總線配合SpringCloud Config實現配置信息的動態刷新。
SpringCloud Bus是用來將分布式系統的節點與輕量級消息系統連接起來的框架,整合了Java的事件處理機制和消息中間件的功能,SpringCloud Bus目前支持兩種消息代理:RabbitMQ和Kafka。
SpringCloud Bus能管理和傳播分布式系統間的消息,就像一個分布式執行器,可用於廣播狀態更改、事件推送等, 也可以當做微服務間的通信通道
是什么?能干嘛、怎么用?
在微服務架構的系統中,通常會使用輕量級的消息代理來構建一個共用的消息主題,並讓系統中所有微服務實例都連接上來,由於該主題中產生的消息會被所有實例監聽和消費,所以稱它為消息總線。在總線上的各個實例,都可以方便地廣播一些需要讓其他連接在該主題上的實例都知道的消息。
基本原理
- SpringCloud Config客戶端的實例都監聽消息隊列中的同一個主題(topic)(默認是SpringCloud Bus),當一個服務器刷新數據的時候,它會把這個信息放入到 topic 中,這樣其他監聽了同一 Topic 的服務就能得到通知,然后去更新自身的配置。
設計思想
- 1)利用消息總線觸發一個客戶端/bus/refresh,而刷新所有客戶端的配置
- 2)利用消息總線觸發一個服務端ConfigServer的/bus/refresh端點,而刷新所有客戶端的配置
明顯第二種架構更加合適,第一種架構不合適的原因主要有:
-
打破了微服務的職責單一性,因為微服務本身是業務模塊,它本不應該承擔配置刷新的職責;
-
破壞了微服務各節點的對等性;
-
有一定的局限性,比如在微服務遷移時,它的網絡地址常常會發生變化,此時如果想要做到自動刷新,那就會增加更多的修改。
動態刷新全局廣播的設計
**配置 **
依賴
<!--添加消息總線RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
bootstrap.yml
# RabbitMQ相關配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
# 暴露總線刷新配置的端點
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
客戶端 配置 服務端地址
然后給其客戶端添加消息總線支持
<!--添加消息總線RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
bootstrap.yml
# RabbitMQ相關配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
修改 git 倉庫的配之后 ,然后對服務配置中心發送 POST 請求,即可刷新服務端關聯的客戶端全局配置信息
curl -X POST "http://localhost:3344/actuator/bus-refresh"
動態刷新定點通知
在全局廣播中,更新配置文件的信息對所有服務都進行了通知,但是我只想通知兩個服務,此時需要用到定點通知去指定具體的某個實例。
進行消息通知時只指定具體某個實例生效而不是全部生效。
/bus/refresh請求不再發送到具體的服務實例上,而是發給config server並通過destination參數類指定需要更新配置的服務或實例
curl -X POST "http://{配置中心的地址}/actuator/bus-refresh/{destination}"
destination 具體的配置為 微服務名 + 端口號
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
MQ 消息中間件
ActivityMQ 、RabbitMQ、RocketMQ、Kafka
SpringCloud Stream 消息驅動
是什么?能干嘛?怎么用 ?
消息驅動,它能屏蔽底層消息中間件的差異,降低切換成本,統一消息的編程模型。
官方定義 Spring Cloud Stream 是一個構建消息驅動微服務的框架。
應用程序通過 inputs (消息消費者) 或者 outputs ( 消息發送者 ) 來與 Spring Cloud Stream中binder (綁定器) 對象交互。通過我們配置來binding(綁定) ,而 Spring Cloud Stream 的 binder對象負責與消息中間件交互。所以,我們只需要搞清楚如何與 Spring Cloud Stream 交互就可以方便使用消息驅動的方式。
通過使用Spring Integration來連接消息代理中間件以實現消息事件驅動。Spring Cloud Stream 為一些供應商的消息中間件產品提供了個性化的自動化配置實現,引用了發布-訂閱、消費組、分區的三個核心概念。
目前僅支持RabbitMQ、Kafka。
設計思想
在經典的消息隊列中,生產者/消費者之間靠消息媒介傳遞信息內容,消息必須走特定的通道Message Channel,消息通道里的子接口Subscribable Channel消費消息,然后MessageHandler負責收發處理。
在SpringCloud Stream中,通過定義綁定器(binder)作為中間層,實現了應用程序與消息中間件細節之間的隔離。在消息綁定器中,INPUT對應於消費者,OUTPUT對應於生產者,
Stream中的消息通信方式遵循了發布—訂閱模式:用Topic(主題)進行廣播(RabbitMQ中對應於Exchange交換機,Kafka中就是Topic)。
Stream 編碼API和常用注解
API / 注解 | 說明 |
---|---|
Middleware | 中間件,目前僅支持 RabbitMQ、Kafka |
Binder | Binder是應用與消息中間件之間的封裝,目前實行了RabbitMQ和Kafka的Binder,通過Binder可以很方便的連接中間件,可以動態的改變消息類型(對應於Kafka的topic,RabbitMQ的exchange),這些都可以通過配置文件來實現 |
@Input | 注解標識輸入通道,通過該輸入通道接收到的信息進入應用程序 |
@Output | 注解標識輸出通過,發布的消息將通過該通道離開應用程序 |
@StreamListner | 監聽隊列,用於消費者隊列的消息接收 |
@EnableBinding | 使信道 Channel 和交換機/主題 ( Exchange/Topic ) 綁定在一起 |
新建三個子模塊分別對應於消息的生產者和消費者:
模塊名 | 微服務功能 |
---|---|
cloud-stream-rabbitmq-provider8801 | 生產者,發送消息模塊 |
cloud-stream-rabbitmq-consumer8802 | 消費者,接收消息模塊 |
cloud-stream-rabbitmq-consumer8803 | 消費者,接收消息模塊 |
Stream 消息驅動之 生產者
依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
配置文件 application.yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此處配置要綁定的rabbitmq的服務信息;
defaultRabbit: # 表示定義的名稱,用於於binding整合
type: rabbit # 消息組件類型
environment: # 設置rabbitmq的相關的環境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服務的整合處理
output: # 這個名字是一個通道的名稱,OUTPUT表示這是消息的發送方
destination: testExchange # 表示要使用的Exchange名稱定義
content-type: application/json # 設置消息類型,本次為json,文本則設置“text/plain”
binder: defaultRabbit # 設置要綁定的消息服務的具體設置
eureka:
client: # 客戶端進行Eureka注冊的配置
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 設置心跳的時間間隔(默認是30秒)
lease-expiration-duration-in-seconds: 5 # 如果現在超過了5秒的間隔(默認是90秒)
instance-id: send-8801.com # 在信息列表時顯示主機名稱
prefer-ip-address: true # 訪問的路徑變為IP地址
主啟動類 App.java
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
業務類 ( 發送消息接口、發送消息接口實現類、controller 入口程序 )
在發送消息接口的實現類中添加
@EnableBinding(Source.class)
注解用來綁定消息的推送管道,消息生產者綁定的消息推送管道定義為org.springframework.cloud.stream.messaging.Source
並且此時 實現類中 不再需要
@Service
注解 ,而是需要跟綁定器打交道的 service , 是跟消息中間件打交道的邏輯
// 接口
public interface IMessageProvider {
public String send();
}
// 實現類 import com.atguigu.springcloud.service.IMessageProvider;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.MessageChannel;
import org.springframework.integration.support.MessageBuilder;
import javax.annotation.Resource;
import org.springframework.cloud.stream.messaging.Source;
import java.util.UUID;
@EnableBinding(Source.class) //定義消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息發送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: "+serial);
return null;
}
}
// 入口程序 controller
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
在RabbitMQ的控制面板中我們也看到了確實發送了消息
Stream 消息驅動之 消費者
特別需要注意的是,消息生產者微服務用到的通道為OUTPUT,而消息消費者微服務用到的通道為INPUT
這是從隊列中獲取到的消息,
application.yml
spring:
cloud:
bindings:
input: # 這個名字是一個通道的名稱,INPUT表示消息消費者
省略主啟動類 ,同上 生產者主啟動類
業務類
controller 同樣需要添加注解
@EnableBinding
用來綁定消息的推送管道,消息消費者綁定的消息推送管道為org.springframework.cloud.stream.messaging.Sink
,在需要接收消息的方法上使用@StreamListener
注解來監聽其綁定的消息推送管道
package cn.sher6j.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消費者" + serverPort + "號,收到消息:" + message.getPayload());
}
}
生產者發送消息 ,消費者接收消息
分組消費與持久化
重復消費問題
當生產者發送消息后,此時的我們的消費者都接受了消息並進行了消費,也就是說同一條消息被多個消息消費者所消費
其實重復消費這個問題本身不可怕,可怕的是沒考慮到重復消費之后,怎么保證冪等性 ?( 冪等性 通俗的說,就是一條數據,或者一個請求,重復很多次,需要確保對應的數據是不會改變的,不能出錯)。
分布式微服務應用為了實現高可用和負載均衡,實際上同一功能的服務都會部署多個具體的服務實例。舉個例子,假設有一個系統,有一條消息要求往數據庫里插入一條數據,要是這個消息重復消費兩次,結果就是向數據庫里插入了兩條數據,這樣數據就錯了,就違背了冪等性原則,但是要是該消息消費到第二次的時候,可以判斷一下已經消費過了,然后直接將該消息丟棄,這就實現了只插入一條數據,一條消息重復出現了兩次,但是只有第一次真正被消費了,數據庫里也就只插入了一條數據,這就保證了系統的冪等性。
如何解決重復消費問題呢 ?
- 進行分組和持久化屬性組操作 ,利用 SpringCloud Stream 中的消息分組來解決這個問題。
- 注意在Stream中處於同一個group中的多個消費者是競爭關系,就能夠保證消息只會被其中一個應用消費一次。也就是保證生產者所發送的同一個消息只會被其中一個消費者消費一次。不同組是可以全面消費的(重復消費),同一組內會發生競爭關系,只有其中一個可以消費。
在 RabbitMQ 中,默認分組是不同的。
兩條隊列同時綁定交換器,當發消息時兩條隊列都會有消息,只要讓兩個消費者監聽同一個隊列就不會有重復消費
解決消息重復消費問題
在 消費者 配置文件中 添加如下配置 application.yml
spring:
cloud:
stream:
bindings:
input:
group: atguiguA / atguiguB ## 分組名稱
此時由於 8802/8803 位於兩個不同分組下,所以沒有競爭關系,消息生產者發送消息后,仍然可以重復消費。
在Spring Cloud Stream中提供了消費組的概念。
解決重復消費就是把 MQ 的工作模式從 廣播模式 改成 工作隊列模式
重啟 8083 服務 並運行 localhost:8801/sendMessage
輪詢分組
同一個組的多個微服務實例,每次只會有一個拿到
持久化
有分組屬性配置 ,組下的某一個實例重啟后,RabbitMQ 上面還沒有消費的消息,可以重新撿起來消費。 后台打出來了MQ上的消息
無分組屬性配置,啟動的時候后台沒有打出來消息
spring:
cloud:
stream:
bindings:
input:
group: atguiguA / atguiguB ## 分組名稱
Sleuth 分布式請求鏈路跟蹤
是什么?能干嘛?怎么用?
在微服務框架中,一個由客戶端發起的請求在后端系統中會經過多個不同的的服務節點調用來協同產生最后的請求結果,每一個前端請求都會形成一條復雜的分布式服務調用鏈路,鏈路中的任何一環出現高延時或錯誤都會引起整個請求最后的失敗。所以在較復雜的系統中,一個調用鏈路中會有很多個微服務,無疑我們需要對鏈路上的微服務進行跟蹤。
Spring Cloud Sleuth提供了一套完整的服務跟蹤的解決方案。在分布式系統中提供追蹤解決方案並且兼容支持了zipkin
Sleuth 負責對微服務調用鏈路的收集整理,而 Zipkin 負責對鏈路的展現。
Zikpin 搭建
SpringCloud從F版之后就不需要自己構建Zipkin Server了,只需要調用相關jar包即可。下載 jar 包
java -jar zipkin-server-2.12.9-exec.jar
http://localhost:9411/zipkin/ 進入 zipkin 監控平台 。
術語
一條鏈路通過 Trace Id 唯一標識,Span 標識發起的請求信息,各 span 通過 Parent Id 關聯起來
A 是 父服務 即第一個服務,ParentId 為 null,A 調 B服務 ,B 調 C 服務, C 調 D、F 服務 ........
Trace 類似樹結構的 Span 集合 ,標識一條鏈路調用,存在唯一標識。span 標識調用鏈路來源,通俗理解 span 就是一次請求信息 。
鏈路 我個人理解為 :一次請求 ,比如 > @GetMapping("/xxx/getId") 該請求處理方法下的服務調用
整個來南路的依賴關系如下 。
Sleuth 鏈路監控展現
在 服務提供方 和 服務消費方添加如下 配置。
依賴
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
application.yml
spring:
zipkin:
base-url: http://localhost:9411 # 監控地址
sleuth:
sampler:
# 采樣率值介於0到1之間,1表示全部采集
probability: 1
啟動Eureka服務注冊中心、8001服務提供方服務、80服務消費方服務
http://localhost/consumer/payment/zipkin
在zipkin面板中即可查看服務調用鏈路
原理
SpringCloud Alibaba簡介
是什么?能干嘛?去哪下?怎么玩?
Spring Cloud Alibaba 致力於提供微服務開發的一站式解決方案。此項目包含開發分布式應用微服務的必需組件,方便開發者通過 Spring Cloud 編程模型輕松使用這些組件來開發分布式應用服務。
依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以將 Spring Cloud 應用接入阿里微服務解決方案,通過阿里中間件來迅速搭建分布式應用系統。
服務限流降級:默認支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降級功能的接入,可以在運行時通過控制台實時修改限流降級規則,還支持查看限流降級 Metrics 監控。
服務注冊與發現:適配 Spring Cloud 服務注冊與發現標准,默認集成了 Ribbon 的支持。
分布式配置管理:支持分布式系統中的外部化配置,配置更改時自動刷新。
消息驅動能力:基於 Spring Cloud Stream 為微服務應用構建消息驅動能力。
阿里雲對象存儲:阿里雲提供的海量、安全、低成本、高可靠的雲存儲服務。支持在任何應用、任何時間、任何地點存儲和訪問任意類型的數據。
分布式任務調度:提供秒級、精准、高可靠、高可用的定時(基於 Cron 表達式)任務調度服務。同時提供分布式的任務執行模型,如網格任務。網格任務支持海量子任務均勻分配到所有 Worker(schedulerx-client)上執行。
官網
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html
Nacos服務注冊和配置中心
全稱 :Nacos: Dynamic Naming and Configuration Service
是什么?能干嘛?在哪下?怎么玩?
前四個字母分別為Naming和Configuration的前兩個字母,最后的s為Service。
一個更易於構建雲原生應用的動態服務發現、配置管理和服務管理平台。
Nacos就是注冊中心 + 配置中心的組合,等價於 -> Nacos = Eureka+Config +Bus
替代Eureka做服務注冊中心、替代Config做服務配置中心
https://github.com/alibaba/Nacos
官網文檔 : https://nacos.io/zh-cn/index.html 、https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_nacos_discovery
主流注冊中心比較
安裝並運行 Nacos
https://github.com/alibaba/nacos/releases 從官網下載Nacos
解壓安裝包,直接運行bin目錄下的startup.cmd
命令運行成功后直接訪問http://localhost:8848/nacos
賬號密碼 都是 nacos ,頁面左上角出現版本號即是成功啟動 。
Nacos 服務注冊中心
Nacos 服務提供者
值得注意的是,nacos 自帶負載均衡功能
環境搭建
父 POM 引入依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在需要用到 nacos 的模塊 引入依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置文件 YML application.yml
server:
port: 9001
spring:
application:
name: nacos-payment-provider # 服務提供者名字
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*' # 暴露監控
主啟動類 App.java 添加
@EnableDiscoveryClient
注解 開啟 nacos 支持
@SpringBootApplication
@EnableDiscoveryClient
public class NacosProviderDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderDemoApplication.class, args);
}
}
業務類 controller
@RestController
public class PaymentController{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
新建cloudalibaba-provider-payment9002
除了端口需要更改 ,其他代碼可以直接 copy 參照同上配置
並啟動 9001 9002 服務
點擊 服務列表 -> 操作列的 詳情 ,會出現下圖
Nacos 服務消費者
環境配置
引入依賴
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
application.yml 配置文件
server:
port: 83
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消費者將要去訪問的微服務名稱(注冊成功進nacos的微服務提供者) 可寫可不寫
service-url:
nacos-user-service: http://nacos-payment-provider
主啟動類 添加注解
@EnableDiscoveryClient
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain83 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain83.class,args);
}
}
config 配置類
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate()
{
return new RestTemplate();
}
}
業務類 controller
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}")
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
}
}
啟動 http://localhost:83/consumer/payment/nacos/100 出現下圖 ,成功。83 訪問 9001、9002,輪詢負載成功。
83 (服務消費者) 服務 調用 9001、9002 ( 服務生產者 )
為什么 nacos 支持負載均衡
nacos 內部整合了 Ribbon
Nacos 服務注冊中心對比提升
nacos 自動支持 AP + CP 的切換 。
Nacos 支持 AP 和 CP 模式的切換
C 是所有節點在同一時間看到的數據是一致的;而A的定義是所有的請求都會收到響應。
何時選擇使用何種模式?一般來說,
- 如果不需要存儲服務級別的信息且服務實例是通過nacos-client注冊,並能夠保持心跳上報,那么就可以選擇AP模式。當前主流的服務如 Spring cloud 和 Dubbo 服務,都適用於AP模式,AP模式為了服務的可能性而減弱了一致性,因此AP模式下只支持注冊臨時實例。
- 如果需要在服務級別編輯或者存儲配置信息,那么 CP 是必須,K8S服務和DNS服務則適用於CP模式。CP模式下則支持注冊持久化實例,此時則是以 Raft 協議為集群運行模式,該模式下注冊實例之前必須先注冊服務,如果服務不存在,則會返回錯誤。
切換模式 命令
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
Nacos 服務配置中心
環境搭建
新建 model cloudalibaba-config-nacos-client3377
引入依賴
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
YML 配置兩個 yml 文件 1、bootstrap.yml 2、application.yml
為什么要配置兩個 yml 文件,Nacos 同 springcloud-config 一樣,在 項目初始化時,要保證先從配置中心進行配置拉取,拉取配置之后,才能保證項目的正常啟動。先有共性,在有個性。
springboot 中配置文件的加載時存在優先級順序的,bootstrap 優先級高於 application
bootstrap.yml
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務注冊中心地址
config:
server-addr: localhost:8848 #Nacos作為配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
# nacos-config-client-test.yaml ----> config.info
application.yml
spring:
profiles:
active: dev # 表示開發環境
主啟動類
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClientMain3377{
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
業務類 controller ,在controller 類上 添加 注解
@RefreshScope
通過 springcloud 原生注解 實現配置自動更新 。
@RestController
@RefreshScope //支持Nacos的動態刷新功能。
public class ConfigClientController{
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
在 Nacos 中添加配置信息 ,如下時 Nacos 中的匹配規則 ,需要注意的是:應該嚴格按照官網格式,否則會出問題。
之所以需要配置 spring.application.name ,是因為它是構成 Nacos 配置管理 dataId 字段的一部分 。
在 Nacos Spring Cloud 中,dataId
的完整格式如下:
${prefix}-${spring.profiles.active}.${file-extension}
prefix
默認為spring.application.name
的值,也可以通過配置項spring.cloud.nacos.config.prefix
來配置。spring.profiles.active
即為當前環境對應的 profile,詳情可以參考 Spring Boot文檔。 注意:當spring.profiles.active
為空時,對應的連接符-
也將不存在,dataId 的拼接格式變成${prefix}.${file-extension}
file-exetension
為配置內容的數據格式,可以通過配置項spring.cloud.nacos.config.file-extension
來配置。目前只支持properties
和yaml
類型。- 通過 Spring Cloud 原生注解
@RefreshScope
實現配置自動更新:
最后公式 :${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
實操
一切准備就緒,調用接口查看配置信息 http://localhost:3377/config/info
修改 Nacos 中的yaml配置文件,再次調用接口,就會發現配置已經刷新
Nacos 命名空間、分組和DataId三者關系
引入問題
實際開發中,一個系統通常會准備 dev、test、prod、等環境,如何保證指定環境啟動時,服務能正確讀取到 Nacos 上的相應環境的配置文件呢 ?
一個大型分布式微服務系統會有很多微服務子項目,每個微服務項目都會有相應的開發環境。那怎么對這些微服務配置進行管理呢?
開發環境
指的是 :dev-開發環境、test-測試環境、prod-生產環境
Namespace+Group+Data ID三者關系?為什么這么設計?
1 是什么
- 類似Java里面的package名和類名 最外層的namespace是可以用於區分部署環境的,Group和DataID邏輯上區分兩個目標對象。
2 三者情況
- 默認情況:Namespace=public,Group=DEFAULT_GROUP, 默認Cluster是DEFAULT
- Nacos默認的命名空間是public,Namespace主要用來實現隔離。
- 比方說我們現在有三個環境:開發、測試、生產環境,我們就可以創建三個Namespace,不同的Namespace之間是隔離的。
- Group默認是DEFAULT_GROUP,Group可以把不同的微服務划分到同一個分組里面去 [ SpringCloud Stream 也有 group 分組 ]
- Service就是微服務;一個Service可以包含多個Cluster(集群),Nacos默認Cluster是DEFAULT,Cluster是對指定微服務的一個虛擬划分。比方說為了容災,將Service微服務分別部署在了杭州機房和廣州機房,這時就可以給杭州機房的Service微服務起一個集群名稱(HZ),給廣州機房的Service微服務起一個集群名稱(GZ),還可以盡量讓同一個機房的微服務互相調用,以提升性能。
- 最后是Instance,就是微服務的實例。
實操 - 三種方案加載配置
這里不做實操演示
- DataID方案
指定spring.profile.active和配置文件的DataID來使不同環境下讀取不同的配置
最常用的
默認空間+默認分組+新建dev和test兩個DataID ,通過spring.profile.active屬性就能進行多環境下配置文件的讀取
- Group方案
通過Group實現環境區分
在nacos圖形界面控制台上面新建配置文件DataID 並修改 Group ,bootstrap+application ,在config下增加一條group的配置即可。
可配置為DEV_GROUP或TEST_GROUP,通過修改spring.cloud.nacos.config.group
和spring.profiles.active
屬性來進行多環節下配置文件的讀取 。
- Namespace方案
- 新建dev/test的Namespace
回到服務管理-服務列表查看
按照域名配置填寫
spring.cloud.nacos.config.namespace:名稱空間id
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務注冊中心地址
config:
server-addr: localhost:8848 #Nacos作為配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP
namespace: 7d8f0f5a-6a53-4785-9686-dd460158e5d4
namespace 包着 group ,group 包着 DataId
Nacos 集群架構說明
默認Nacos使用嵌入式數據庫實現數據的存儲。所以,如果啟動多個默認配置下的Nacos節點,數據存儲是存在一致性問題的。為了解決這個問題,Nacos采用了集中式存儲的方式來支持集群化部署,目前只支持MySQL的存儲。
Nacos 持久化切換配置
Nacos默認自帶的是嵌入式數據庫derby
derby到mysql切換配置步驟
- 1、nacos-server-1.1.4\nacos\conf目錄下找到sql腳本 nacos-mysql.sql
先創建表
nacos_config
,然后在自己本機數據庫 或者 linux 數據庫中 執行 nacos-mysql.sql 腳本, 復制 粘貼 即可,
- 2、nacos-server-1.1.4\nacos\conf目錄下找到application.properties,nacos1.1.4 版本粘貼 如下配置 ,更改為自己的數據庫,nacos 1.3 版本 已經寫好了,不用復制下面配置 ,直接把配置放開就可以了,去掉前面的
#
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
啟動Nacos,可以看到是個全新的空記錄界面,以前是記錄進derby
如果重啟報錯 :
- 報錯的可能是最新版本的nacos里面的proprieties自帶數據庫連接信息,不需要粘貼進去直接去掉前面的#號然后修改即可
- 報錯可以降一下mysql級 降到5.7
如果你的mysql是8.0以上的需要下載1.2以上的nacos和修改數據庫驅動
用mysql8.0然后改完配置Nacos進不去的
- 第一步:在application.properties增加jdbc.DriverClassName=com.mysql.cj.jdbc.Driver
- 第二步:在 db.url.0那一行加上參數useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
- 第三步:在你nacos的安裝目錄新建plugins/mysql文件夾,加入一個8.0+版本的mysql-connector-java...jar
然后重啟 nacos 即可 。
Nacos Linux版本安裝
三個 或 三個以上 Nacos 節點才能構成集群 。
預計需要,1個Nginx+3個nacos注冊中心+1個mysql
Nacos下載Linux版
官網 :https://github.com/alibaba/nacos/releases/tag/1.1.4 nacos-server-1.1.4.tar.gz
解壓后安裝
這里不做詳細介紹
搭建 nacos 集群環境 https://www.cnblogs.com/linchenguang/p/12827582.html
安裝 nginx https://blog.csdn.net/qq_37345604/article/details/90034424
新建 Model ,model 配置過很多次了,這里不再贅述了。參數
spring.cloud.naco.discovery.server-addr
設置為 服務器 nginx 地址 即可 。
server:
port: 9002
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
# server-addr: localhost:8848 #配置Nacos地址
# 換成nginx的1111端口,做集群
server-addr: 192.168.0.188:1111
management:
endpoints:
web:
exposure:
include: '*'
Sentinel
官網 :https://github.com/alibaba/Sentinel 中文 :https://github.com/alibaba/Sentinel/wiki/介紹
A powerful flow control component enabling reliability, resilience and monitoring for microservices. (面向雲原生微服務的高可用流控防護組件)
The Sentinel of Your Microservices --- 微服務的哨兵 能夠監控你的微服務
是什么?去哪下?能干嘛?怎么玩?
同 Hystrix 功能一樣 ,服務降級、服務熔斷
能干嘛
下載好 sentinel-dashboard-1.8.1.jar 之后,cmd ,java -jar sentinel-dashboard-1.8.1.jar 啟動
運行 http://localhost:8080/ 賬號密碼都是 sentinel
初始化監控
-
1、啟動 Nacos 8848
-
2、新建 Module
-
cloudalibaba-sentinel-service8401
-
POM 引入依賴
<!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel-datasource-nacos 后續做持久化用到--> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
- YML
server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 #Nacos服務注冊中心地址 sentinel: transport: dashboard: localhost:8080 #配置Sentinel dashboard地址 # 默認8719端口, 假如被占用會自動從8719開始依次+1掃描,直到找到未被占用的端口. port: 8719 management: endpoints: web: exposure: include: '*'
- 主啟動類
@EnableDiscoveryClient @SpringBootApplication public class MainApp8401{ public static void main(String[] args) { SpringApplication.run(MainApp8401.class, args); } }
- 業務類 controller
@RestController @Slf4j public class FlowLimitController{ @GetMapping("/testA") public String testA(){ return "------testA"; } @GetMapping("/testB") public String testB(){ log.info(Thread.currentThread().getName()+"\t"+"...testB"); return "------testB"; } }
-
-
3、啟動 Sentinel 8080
-
4、啟動微服務 8401
-
5、查看 sentienl 控制台
Sentinel采用的懶加載,需先運行 8401 http://localhost:8401/testA http://localhost:8401/testB ,再次查看 sentienl 控制台 即可 。
流控規則
名詞解釋說明
流控模式-QPS -直接失敗
QPS 每秒鍾請求數 ,此時我設置的是 ,一秒鍾一個請求,如果操作這個規則,假設一秒鍾兩個請求,那么就直接報錯,直接失敗
會出現下圖的默認錯誤
流控模式-線程數直接失敗
同一時間線程數達到閥值后直接失敗。
流控模式-關聯
當關聯的資源達到閾值時,就限流自己,當與A關聯的資源B達到閥值后,就限流A自己,一句話 :B惹事,A掛了
測試 http://localhost:8401/testB 一秒鍾多次請求 。
此時 請求 /testB
一秒鍾只允許一次請求,我們可以使用 jMeter 測試,一秒鍾多次請求。超過了 /testB
的閥值。那么此時會造成/testA
直接掛了
流控效果 -預熱
直接->快速失敗(默認的流控處理)
結果就是 直接失敗,拋出異常
Blocked by Sentinel (flow limiting)
源碼
com.alibaba.csp.sentinel.slots.block.flow.controllerDefaulstController
默認coldFactor為3,即請求 QPS 從 threshold / 3 開始,經預熱時長逐漸升至設定的 QPS 閾值。
上圖解釋 :
/testA
我希望每秒鍾能承受 10 QPS,一開始 單機閾值就是 3,但是給系統 5 s 的預熱時間(緩沖時間),5 s 以后 從 3 慢慢過渡到 10 QPS系統初始化的閥值為10 / 3 約等於3,即閥值剛開始為3;然后過了5秒后閥值才慢慢升高恢復到10
何以證明 默認 coldFactor 是3呢 ?看源碼
源碼 : com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
應用場景
如:秒殺系統在開啟的瞬間,會有很多流量上來,很有可能把系統打死,預熱方式就是把為了保護系統,可慢慢的把流量放進來,慢慢的把閥值增長到設置的閥值。
排隊等待
勻速排隊,讓請求以均勻的速度通過,閥值類型必須設成QPS,否則無效。
設置含義:/testA每秒1次請求,超過的話就排隊等待,等待的超時時間為20000毫秒。
原理
勻速排隊,閾值必須設置為QPS
源碼
com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
降級
是什么?去哪下?能干嘛?怎么玩?
熔斷降級
官網
- RT(平均響應時間,秒級)
- 平均響應時間 超出閾值 且 在時間窗口內通過的請求>=5,兩個條件同時滿足后觸發降級
- 熔斷時長(過后關閉斷路器
- RT最大4900(更大的需要通過-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
- 異常比列(秒級)
- QPS >= 5 且異常比例(秒級統計)超過閾值時,觸發降級;熔斷時長(結束后,關閉降級 異常數(分鍾級)
- 異常數(分鍾統計)
- 超過閾值時,觸發降級;熔斷時長(結束后,關閉降級
說明
Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。
當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)。
值得注意的是 :
Sentinel的斷路器是沒有半開狀態的
半開的狀態系統自動去檢測是否請求有異常,沒有異常就關閉斷路器恢復使用,有異常則繼續打開斷路器不可用。具體可以參考Hystrix
RT
慢調用比例 (
SLOW_REQUEST_RATIO
):選擇以慢調用比例作為閾值,需要設置允許的慢調用 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢調用。當單位統計時長(statIntervalMs
)內請求數目大於設置的最小請求數目,並且慢調用的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設置的慢調用 RT 則結束熔斷,若大於設置的慢調用 RT 則會再次被熔斷
一秒中進來 5 個請求,並且 200 毫秒處理一次任務。
如果一秒鍾持續進入 10 個請求, 且 200 毫秒還沒處理完,在未來 1 秒鍾的熔斷時長內,斷路器打開,微服務不可用 。
使用 jMeter 壓測
異常比例
- 異常比例 (
ERROR_RATIO
):當單位統計時長(statIntervalMs
)內請求數目大於設置的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值范圍是[0.0, 1.0]
,代表 0% - 100%。
正確率 在 80%,10個請求,最多出現兩個異常數,低於 80% ,熔斷時長內該服務不可用
- 業務類 controller
@GetMapping("/testD")
public String testD()
{
log.info("testD 異常比例");
int age = 10/0;
return "------testD";
}
jMeter 測試
停掉 jMeter ,再次請求
/testD
,會發現會拋出異常 ,RunntimeException
原因是 ,不滿足熔斷條件 ,一秒鍾只有一個請求,沒有超過閥值 ,系統拋出 RunntimeException
異常
異常數
- 異常數 (
ERROR_COUNT
):當單位統計時長內的異常數目超過閾值之后會自動進行熔斷。經過熔斷時長后熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。
異常數 達到 5 次后 ,先熔斷后降級 。
熱點規則
相當於 Hystrix 兜底方法規則。HystrixCommand
何為熱點。熱點即經常訪問的數據,很多時候我們希望統計或者限制某個熱點數據中訪問頻次最高的TopN數據,並對其訪問進行限流或者其它操作
官網: https://github.com/alibaba/Sentinel/wiki/熱點參數限流
源碼
com.alibaba.csp.sentinel.slots.block.BlockException
業務實現
public class FlowLimitController{
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "------testHotKey";
}
// 兜底的方法,
public String deal_testHotKey (String p1, String p2, BlockException exception){
return "------deal_testHotKey,o(╥﹏╥)o"; //sentinel系統默認的提示:Blocked by Sentinel (flow limiting)
}
}
blockHandler="" 指定兜底的方法名
配置
方法testHotKey里面第一個參數只要QPS超過每秒1次,馬上降級處理 。一秒鍾只允許一個請求訪問,如果超過一個請求,則執行 deal_testHotkey() 兜底方法 。
注意 :
- 如果用到了熱點規則注解,請一定要加上 blockHandler="" 指定兜底處理方法,如果不加 blockHandler="",error Page會打印到頁面,不友好
參數例外項
添加配置
結合上述配置解釋 ,p1 QPS 超過每秒一個則限流,(參數例外項)但是如果 p1=5 時,允許p1 QPS 為 200
如果出現 Java RunntimeException 異常
@SentinelResource
- 處理的是Sentinel控制台配置的違規情況,有blockHandler方法配置的兜底處理;
RuntimeException
- int age = 10/0,這個是java運行時報出的運行時異常RunTimeException,@SentinelResource不管
總結
- @SentinelResource主管配置出錯,運行出錯該走異常走異常
系統規則
官網 : https://github.com/alibaba/Sentinel/wiki/系統自適應限流
Sentinel 系統自適應限流從整體維度對應用入口流量進行控制,結合應用的 Load、CPU 使用率、總體平均 RT、入口 QPS 和並發線程數等幾個維度的監控指標,通過自適應的流控策略,讓系統的入口流量和系統的負載達到一個平衡,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性
各項配置參數說明
SentinelResource
引入 pom 相關依賴
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<dependency><!-- 引入自己定義的api通用包,可以使用Payment支付Entity -->
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
業務類 RateLimitController
public class RateLimitController
{
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200,"按資源名稱限流測試OK",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t 服務不可用");
}
}
可以按照 url 地址進行限流,也可以按照資源名稱進行限流 。
不過上訴方案也面臨的 跟 Hystrix 同樣的問題
1 系統默認的,沒有體現我們自己的業務要求。
2 依照現有條件,我們自定義的處理方法又和業務代碼耦合在一塊,不直觀。
3 每個業務方法都添加一個兜底的,那代碼膨脹加劇。
4 全局統一的處理方法沒有體現。
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200,"按客戶自定義",new Payment(2020L,"serial003"));
}
value=""唯一名稱 . blockHandlerClass="" 指定哪個類兜底處理. blockHandler="" 指定兜底類的哪個方法處理異常
注意 : 兜底類的處理方法 必須是 靜態方法 也就是 static 修飾的方法 。
更多注解屬性說明