1 Hystix
1.1 簡介
Hystix是Netflix開源的一個延遲和容錯庫,用於隔離訪問遠程服務、第三方庫,防止出現級聯失敗。
我感覺難以解釋清楚,鑒於接下來的demo項目基本不會用這個模塊,就過一遍代碼算了。
1.2 配置並測試
模擬一下熔斷:一旦請求的接口超過1s沒有響應就不再繼續請求,開始執行實現定義好的回滾函數,返回一個提示信息。
注意:Ribbon的重試機制和Hystrix的熔斷機制有一定關系。
1.2.1 引入依賴
往user-consume即服務調用者的pom文件里面添加hystrix依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
1.2.2 開啟熔斷
在UserConsumeApplication中添加@EnableCircuitBreaker
可以看到注解有點多了,可以用@SpringCloudApplication
代替上面三個注解
//@EnableDiscoveryClient // 開啟EurekaClient功能
//@EnableCircuitBreaker //開啟熔斷
//@SpringBootApplication
@SpringCloudApplication //三合一注解
1.2.3 改造consume
改造consume即消費者的調用方法,記錄調用接口耗費的時間,編寫調用超時的回滾方法。
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
:聲明一個失敗回滾處理函數queryUserByIdFallback,當queryUserById執行超時(默認是1000毫秒),就會執行fallback函數,返回錯誤提示。
@Component
public class UserDao {
@Autowired
private RestTemplate restTemplate;
private static final Logger logger = LoggerFactory.getLogger(UserDao.class);
@HystrixCommand(fallbackMethod = "queryUserByIdFallback")
public User queryUserById(Long id){
long begin = System.currentTimeMillis();
String url = "http://user-service/user/" + id;
User user = this.restTemplate.getForObject(url, User.class);
long end = System.currentTimeMillis();
// 記錄訪問用時:
logger.info("訪問用時:{}", end - begin);
return user;
}
public User queryUserByIdFallback(Long id){
User user = new User();
user.setId(id);
user.setUsername("用戶信息查詢出現異常!");
return user;
}
}
@Service
public class UserService {
@Autowired
private UserDao userDao;
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
// 我們測試多次查詢,
users.add(this.userDao.queryUserById(id));
});
return users;
}
}
1.2.4 改造service
即改造服務提供者,將方法隨機延遲一段時間,模擬超時。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id) throws InterruptedException {
// 為了演示超時現象,我們在這里然線程休眠,時間隨機 0~2000毫秒
Thread.sleep(new Random().nextInt(2000));
return this.userMapper.selectByPrimaryKey(id);
}
}
1.2.5 結果
很明顯,超過1000ms的請求被回滾方法處理掉了。
1.2.6 設置Hystrix超時時間
之前我們設置的Robbin的ReadTimeout
是1000ms,即請求一個接口超過1000ms未響應就請求另一個有相同功能的接口而Hystrix的超時時間默認也是1000ms,觀察現象可知,先觸發了熔斷。但這樣是不合理的,明明可以請求另一個接口得到結果,結果觸發了熔斷,返回的是找不到結果。
所以注意一點:Hystrix的超時時間一定要大於Robbin的重試時間。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 設置hystrix的超時時間為6000ms
2. Feign
2.1 簡介
Feign可以把Rest的請求進行隱藏,偽裝成類似SpringMVC的Controller一樣。你不用再自己拼接url,拼接參數等等操作,一切都交給Feign去做。
2.2 使用Feign
Feign的引入和配置全部都在consume中進行
2.2.1 引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.2.2 UserConsumeApplication添加注解
@EnableFeignClients
2.2.3 編寫UserClient接口
在項目中添加client包,下面新建UserClient接口
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
- 首先這是一個接口,Feign會通過動態代理,幫我們生成實現類。這點跟mybatis的mapper很像
@FeignClient
,聲明這是一個Feign客戶端,類似@Mapper
注解。同時通過value
屬性指定服務名稱- 接口中的定義方法,完全采用SpringMVC的注解,Feign會根據注解幫我們生成URL,並訪問獲取結果
2.2.4 使用UserClient請求數據
改造UserService,改為使用UserClient請求數據
@Service
public class UserService {
//@Autowired
//private UserDao userDao;
@Autowired
private UserClient userClient; //注入UserClient
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
ids.forEach(id -> {
//使用UserClient請求接口
users.add(userClient.queryUserById(id));
//users.add(this.userDao.queryUserById(id));
});
return users;
}
}
2.3 負載均衡
Feign是對請求的封裝,本身集成了Ribbon負載均衡。可以對其進行配置,和單獨使用Robbin的寫法一樣
user-service:
ribbon:
ConnectTimeout: 250 # 連接超時時間(ms)
ReadTimeout: 1000 # 通信超時時間(ms)
OkToRetryOnAllOperations: true # 是否對所有操作重試
MaxAutoRetriesNextServer: 1 # 同一服務不同實例的重試次數
MaxAutoRetries: 1 # 同一實例的重試次數
2.4 Hystrix支持
Feign也集成了Hystrix。
需要配置以開啟Hystrix
feign:
hystrix:
enabled: true # 開啟Feign的熔斷功能
回滾方法不像之前那樣寫了,要專門定義一個類。
@Component
public class UserFeignClientFallback implements UserClient {
@Override
public User queryUserById(Long id) {
User user = new User();
user.setId(id);
user.setUsername("用戶查詢出現異常");
return user;
}
}
在UserClient接口上配置回滾類
@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class)
public interface UserClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
2.5.請求壓縮
Spring Cloud Feign 支持對請求和響應進行GZIP壓縮,以減少通信過程中的性能損耗。通過下面的參數即可開啟請求與響應的壓縮功能:
feign:
compression:
request:
enabled: true # 開啟請求壓縮
response:
enabled: true # 開啟響應壓縮
同時,我們也可以對請求的數據類型,以及觸發壓縮的大小下限進行設置:
feign:
compression:
request:
enabled: true # 開啟請求壓縮
mime-types: text/html,application/xml,application/json # 設置壓縮的數據類型
min-request-size: 2048 # 設置觸發壓縮的大小下限
注:上面的數據類型、壓縮大小下限均為默認值。
3. Zuul網關
3.1 簡介
Zuul是Netflix開源的微服務網關,它可以和Eureka、Ribbon和Hystrix等組件配合使用。核心是一系列的過濾器,完成身份認證與權限管理、動態路由、壓力測試等功能。
工作流程如下所示
不管是來自於客戶端(PC或移動端)的請求,還是服務內部調用。一切對服務的請求都會經過Zuul這個網關,然后再由網關來實現 鑒權、動態路由等等操作。Zuul就是我們服務的統一入口。
3.2 搭建
3.2.1 建一個新的module
其他步驟前面已經說過,選擇模塊時注意選擇Zuul即可。
3.2.2 引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
3.2.3 添加注解
@SpringBootApplication
@EnableZuulProxy //開啟zuul網關
@EnableDiscoveryClient //開啟Eureka客戶端發現功能
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
3.3 路由功能
測試之前先把之前添加的user-service的隨機延遲給注釋掉
Zuul的路由功能:
- 配置
server:
port: 10010 #服務端口
spring:
application:
name: api-gateway #指定服務名
eureka:
client:
registry-fetch-interval-seconds: 5 # 獲取服務列表的周期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
zuul:
prefix: /api # 添加路由前綴
routes:
user-service: # 路由id,一般與服務名相同
path: /user-service/** # 映射路徑
serviceId: user-service
-
解釋:Zuul從Eureka中拉取服務列表,如果有人請求
/api/user-service/**
,將請求轉發給服務列表中的user-service
服務。
坑爹的一點:之前測試的時候Zuul一直報找不到user-service服務的錯誤,找了一段時間后來自己好了。
沒有再復現出來,不知道為啥。如果報錯了,可以嘗試着用maven的reimport,再等等試試。
- 由於路由id一般與服務名相同,因此zuul提供了一種簡化配置,與上面實現相同功能
zuul:
prefix: /api # 添加路由前綴
routes:
user-service: /user-service/** # 這里是映射路徑
訪問http://localhost:10010/api/user-service/user/20即可得到結果。
3.4 過濾功能
3.4.1 ZuulFilter
ZuulFilter是過濾器的頂級父類,我們自己實現的過濾器都要繼承自它。我們看看其中重要的四個方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();
Object run() throws ZuulException;
}
- shouldFilter:返回布爾值,判斷過濾器是否需要執行。true表示執行,false反之。
- run:表示具體的過濾邏輯。
- filterType:返回字符串,表示本過濾器的類型
- pre:請求在被路由之前執行。
- routing:在路由請求時調用
- post:在routing和errror過濾器之后調用
- error:處理請求時發生錯誤調用
- filterOrder:通過返回的int值來定義過濾器的執行順序,數字越小優先級越高。
3.4.2 生命周期
- 正常流程:
- 請求到達首先會經過pre類型過濾器,而后到達routing類型,進行路由,請求就到達真正的服務提供者,執行請求,返回結果后,會到達post過濾器。而后返回響應。
- 異常流程:
- 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢后,會將請求交給post過濾器,最后返回給用戶。
- 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而后返回。
- 如果是post過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達post過濾器了。
3.4.3 使用場景
- 請求鑒權:一般放在pre類型,如果發現沒有訪問權限,直接就攔截了
- 異常處理:一般會在error類型和post類型過濾器中結合來處理。
- 服務調用時長統計:pre和post結合使用。
3.4.4 模擬登錄校驗
添加登錄過濾器
@Component
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
// 登錄校驗,肯定是在前置攔截
return "pre";
}
@Override
public int filterOrder() {
// 順序設置為1
return 1;
}
@Override
public boolean shouldFilter() {
// 返回true,代表過濾器生效。
return true;
}
@Override
public Object run() {
// 登錄校驗邏輯。
// 1)獲取Zuul提供的請求上下文對象
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 從上下文中獲取request對象
HttpServletRequest req = ctx.getRequest();
// 3) 從請求中獲取token
String token = req.getParameter("access-token");
// 4) 判斷
if(token == null || "".equals(token.trim())){
// 沒有token,登錄校驗失敗,攔截
ctx.setSendZuulResponse(false);
// 返回401狀態碼。也可以考慮重定向到登錄頁。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校驗通過,可以考慮把用戶信息放入上下文,繼續向后執行
return null;
}
}
- 訪問http://localhost:10010/api/user-service/user/20,提示401未授權
- 訪問http://localhost:10010/api/user-service/user/20?access-token=123 ,能正常訪問
3.5 負載均衡和熔斷功能
Zuul內部集成了Ribbon負載均衡和Hystrix熔斷器,需要配置
zuul:
retryable: true
ribbon:
ConnectTimeout: 250 # 連接超時時間(ms)
ReadTimeout: 2000 # 通信超時時間(ms)
OkToRetryOnAllOperations: true # 是否對所有操作重試
MaxAutoRetriesNextServer: 2 # 同一服務不同實例的重試次數
MaxAutoRetries: 1 # 同一實例的重試次數
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 6000 # 熔斷超時時長:6000ms
總結
一直在想着如何配置,如何實現,容易犯一葉障目不見泰山的毛病,下面就概括一下。
Eureka 注冊中心
服務提供者在里面注冊,服務消費者拉取服務列表,根據服務列表去發請求並獲取數據。
這其中涉及到最重要的一個問題:如何保證服務列表里面的服務是有效的?
- 對於提供者,這個問題是:你怎么才能知道我什么時候可用,什么時候不可用?
Eureka和提供者就形成了約定:提供者准備好后在Eureka里面注冊,Eureka知道提供者可用了。注冊后提供者每隔一段時間向Eureka發個消息說我還活着,Eureka就知道提供者依舊可用。Eureka每隔一段時間就會掃描整個服務列表,把很長時間沒報告過的服務剔除出去。這樣注冊中心的服務列表就能始終更新,始終可用。
- 對於消費者,這個問題是:我怎么知道你不行了?
消費者每隔一段時間就從注冊中心拉取服務列表,注冊中心和服務提供者的約定保證了這個列表可以信任,我就根據這個列表發出請求,得到數據。
注意上面的三個時間段,這都是可以配置的,怎么配置就根據實際情況來了。
Robbin 負載均衡
服務的消費者在拉取服務列表后,針對同一業務可能有多個可選的服務提供者。
如果一直使用其中一個,那就是傳說中的"一核有難,八核圍觀",表現差勁,浪費資源,這就需要負載均衡了。
Ribbon提供了諸如隨機、輪詢等多種策略可選。
它還提供了重試機制:選擇了一個服務后,如果這個服務壞了(畢竟每隔一段時間才拉取服務列表,服務列表也是每隔一段時間才更新),或者說反應太慢,到了一定時間就考慮再換一個服務。
Hystrix 熔斷機制
為了避免一個模塊的錯誤拖垮整個系統,因此將其隔離起來。
不要讓一顆老鼠屎壞了一鍋湯?這個機制我不太能講清楚。
但是注意它和Ribbon的聯系,一個服務沒有反應,應該先用Ribbon進行重試,而不是先熔斷。然后這又引出來一個問題,一個錯誤如果每次都被重試機制掩蓋了,那系統還會被拖垮嗎?還是說這個業務的服務都壞了,試來試去找不到一個好用的,才觸發熔斷?想不清楚,真遇到了再說吧。
Feign 請求封裝
封裝了RestTemplate,讓請求的編碼更簡單。
請求必然涉及負載均衡和熔斷問題,所以Feign里面也可以配置Robbin和Hystrix。
但不光如此,Feign還提供了請求壓縮等小功能。
Zuul 網關
微服務提供者的接口是暴露在外的,要是誰都能用那就亂套了。
就像學校的器材室有各種器材,但是誰都能自由取用就亂了。因此我們需要一個管理器材的老師,拿器材的人來了,先判斷他有沒有老師的批准,再根據老師的批准決定他能拿什么器材,最后把他帶到對應的器材室拿相應的器材。
Zuul就像是這個老師,負責鑒權(判斷一個人能不能拿器材,能拿什么器材)、路由(領到地方拿器材)。
Zuul雖然不發請求,但是對請求有很強的干預能力,所以它也可以配置負載均衡和熔斷。