樂優商場開發第三天筆記
0.學習目標
-
會配置Hystix熔斷
-
會使用Feign進行遠端呼叫
-
能獨立搭建Zuul閘道器
-
能編寫Zuul的攔截器
1.Hystrix
1.1.簡介
Hystix,即熔斷器。
主頁:https://github.com/Netflix/Hystrix/
Hystix是Netflix開源的一個延遲和容錯庫,用於隔離訪問遠端服務、第三方庫,防止出現級聯失敗。
1.2.熔斷器的工作機制:
正常工作的情況下,客戶端請求呼叫服務API介面:
當有服務出現異常時,直接進行失敗回滾,服務降級處理:
當服務繁忙時,如果服務出現異常,不是粗暴的直接報錯,而是返回一個友好的提示,雖然拒絕了使用者的訪問,但是會返回一個結果。
這就好比去買魚,平常超市買魚會額外贈送殺魚的服務。等到逢年過節,超時繁忙時,可能就不提供殺魚服務了,這就是服務的降級。
系統特別繁忙時,一些次要服務暫時中斷,優先保證主要服務的暢通,一切資源優先讓給主要服務來使用,在雙十一、618時,京東天貓都會採用這樣的策略。
1.3.動手實踐
1.3.1.引入依賴
首先在user-consumer中引入Hystix依賴:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
1.3.2.開啟熔斷
@SpringBootApplication @EnableDiscoveryClient @EnableHystrix public class ConsumerDemoApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { // 這次我們使用了OkHttp客戶端,只需要注入工廠即可 return new RestTemplate(new OkHttp3ClientHttpRequestFactory()); } public static void main(String[] args) { SpringApplication.run(ConsumerDemoApplication.class, args); } }
1.3.2.改造消費者
我們改造user-consumer,新增一個用來訪問的user服務的DAO,並且宣告一個失敗時的回滾處理函式:
@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.setName("使用者資訊查詢出現異常!"); return user; } }
-
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
:宣告一個失敗回滾處理函式queryUserByIdFallback,當queryUserById執行超時(預設是1000毫秒),就會執行fallback函式,返回錯誤提示。 -
為了方便檢視熔斷的觸發時機,我們記錄請求訪問時間。
在原來的業務邏輯中呼叫這個DAO:
@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.3.3.改造服務提供者
改造服務提供者,隨機休眠一段時間,以觸發熔斷:
@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.3.4.啟動測試
然後執行並檢視日誌:
id為9、10、11的訪問時間分別是:
id為12的訪問時間:
因此,只有12是正常訪問,其它都會觸發熔斷,我們來檢視結果:
1.3.5.優化
雖然熔斷實現了,但是我們的重試機制似乎沒有生效,是這樣嗎?
其實這裡是因為我們的Ribbon超時時間設定的是1000ms:
而Hystix的超時時間預設也是1000ms,因此重試機制沒有被觸發,而是先觸發了熔斷。
所以,Ribbon的超時時間一定要小於Hystix的超時時間。
我們可以通過hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
來設定Hystrix超時時間。
hystrix: command: default: execution: isolation: thread: timeoutInMillisecond: 6000 # 設定hystrix的超時時間為6000ms
2.Feign
在前面的學習中,我們使用了Ribbon的負載均衡功能,大大簡化了遠端呼叫時的程式碼:
String baseUrl = "http://user-service/user/"; User user = this.restTemplate.getForObject(baseUrl + id, User.class)
如果就學到這裡,你可能以後需要編寫類似的大量重複程式碼,格式基本相同,無非引數不一樣。有沒有更優雅的方式,來對這些程式碼再次優化呢?
這就是我們接下來要學的Feign的功能了。
2.1.簡介
有道詞典的英文解釋:
為什麼叫偽裝?
Feign可以把Rest的請求進行隱藏,偽裝成類似SpringMVC的Controller一樣。你不用再自己拼接url,拼接引數等等操作,一切都交給Feign去做。
專案主頁:https://github.com/OpenFeign/feign
2.2.快速入門
2.2.1.匯入依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2.2.2.Feign的客戶端
@FeignClient("user-service") public interface UserFeignClient { @GetMapping("/user/{id}") User queryUserById(@PathVariable("id") Long id); }
-
首先這是一個介面,Feign會通過動態代理,幫我們生成實現類。這點跟mybatis的mapper很像
-
@FeignClient
,宣告這是一個Feign客戶端,類似@Mapper
註解。同時通過value
屬性指定服務名稱 -
介面中的定義方法,完全採用SpringMVC的註解,Feign會根據註解幫我們生成URL,並訪問獲取結果
改造原來的呼叫邏輯,不再呼叫UserDao:
@Service public class UserService { @Autowired private UserFeignClient userFeignClient; public List<User> queryUserByIds(List<Long> ids) { List<User> users = new ArrayList<>(); ids.forEach(id -> { // 我們測試多次查詢, users.add(this.userFeignClient.queryUserById(id)); }); return users; } }
2.2.3.開啟Feign功能
我們在啟動類上,添加註解,開啟Feign功能
@SpringBootApplication @EnableDiscoveryClient @EnableHystrix @EnableFeignClients // 開啟Feign功能 public class UserConsumerDemoApplication { public static void main(String[] args) { SpringApplication.run(UserConsumerDemoApplication.class, args); } }
-
你會發現RestTemplate的註冊被我刪除了。Feign中已經自動集成了Ribbon負載均衡,因此我們不需要自己定義RestTemplate了
2.2.4.啟動測試:
訪問介面:
正常獲取到了結果。
2.3.負載均衡
Feign中本身已經集成了Ribbon依賴和自動配置:
因此我們不需要額外引入依賴,也不需要再註冊RestTemplate
物件。
另外,我們可以像上節課中講的那樣去配置Ribbon,可以通過ribbon.xx
來進行全域性配置。也可以通過服務名.ribbon.xx
來對指定服務配置:
user-service: ribbon: ConnectTimeout: 250 # 連線超時時間(ms) ReadTimeout: 1000 # 通訊超時時間(ms) OkToRetryOnAllOperations: true # 是否對所有操作重試 MaxAutoRetriesNextServer: 1 # 同一服務不同例項的重試次數 MaxAutoRetries: 1 # 同一例項的重試次數
全域性的負載均衡配置:
ribbon: ConnectTimeout: 250 # Ribbon的連線超時時間 ReadTimeout: 1000 # Ribbon的資料讀取超時時間 OkToRetryOnAllOperations: true # 是否對所有操作都進行重試 MaxAutoRetriesNextServer: 1 # 切換例項的重試次數 MaxAutoRetries: 1 # 對當前例項的重試次數
2.4.Hystix支援
Feign預設也有對Hystix的整合:
只不過,預設情況下是關閉的。我們需要通過下面的引數來開啟:
feign: hystrix: enabled: true # 開啟Feign的熔斷功能
但是,Feign中的Fallback配置不像Ribbon中那樣簡單了。
1)首先,我們要定義一個類,實現剛才編寫的UserFeignClient,作為fallback的處理類
@Component public class UserFeignClientFallback implements UserFeignClient { @Override public User queryUserById(Long id) { User user = new User(); user.setId(id); user.setName("使用者查詢出現異常!"); return user; } }
2)然後在UserFeignClient中,指定剛才編寫的實現類
@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class) public interface UserFeignClient { @GetMapping("/user/{id}") User queryUserById(@PathVariable("id") Long id); }
3)重啟測試:
我們關閉user-service服務,然後在頁面訪問:
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 # 設定觸發壓縮的大小下限
注:上面的資料型別、壓縮大小下限均為預設值。
2.6.日誌級別(瞭解)
前面講過,通過logging.level.xx=debug
來設定日誌級別。然而這個對Fegin客戶端而言不會產生效果。因為@FeignClient
註解修改的客戶端在被代理時,都會建立一個新的Fegin.Logger例項。我們需要額外指定這個日誌的級別才可以。
1)設定com.leyou包下的日誌級別都為debug
logging: level: com.leyou: debug
2)編寫配置類,定義日誌級別
@Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; } }
這裡指定的Level級別是FULL,Feign支援4種級別:
-
NONE:不記錄任何日誌資訊,這是預設值。
-
BASIC:僅記錄請求的方法,URL以及響應狀態碼和執行時間
-
HEADERS:在BASIC的基礎上,額外記錄了請求和響應的頭資訊
-
FULL:記錄所有請求和響應的明細,包括頭資訊、請求體、元資料。
3)在FeignClient中指定配置類:
@FeignClient(value = "user-service", fallback = UserFeignClientFallback.class, configuration = FeignConfig.class) public interface UserFeignClient { @GetMapping("/user/{id}") User queryUserById(@PathVariable("id") Long id); }
4)重啟專案,即可看到每次訪問的日誌:
3.Zuul閘道器
通過前面的學習,使用Spring Cloud實現微服務的架構基本成型,大致是這樣的:
我們使用Spring Cloud Netflix中的Eureka實現了服務註冊中心以及服務註冊與發現;而服務間通過Ribbon或Feign實現服務的消費以及均衡負載;通過Spring Cloud Config實現了應用多環境的外部化配置以及版本管理。為了使得服務叢集更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現異常時引起的故障蔓延。
在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。我們把焦點聚集在對外服務這塊,直接暴露我們的服務地址,這樣的實現是否合理,或者是否有更好的實現方式呢?
先來說說這樣架構需要做的一些事兒以及存在的不足:
-
首先,破壞了服務無狀態特點。
-
為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。
-
從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外考慮對介面訪問的控制處理。
-
-
其次,無法直接複用既有介面。
-
當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。
-
面對類似上面的問題,我們要如何解決呢?答案是:服務閘道器!
為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器的 服務閘道器。
服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制
等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。
3.1.簡介
官網:https://github.com/Netflix/zuul
Zuul:維基百科:
電影《捉鬼敢死隊》中的怪獸,Zuul,在紐約引發了巨大騷亂。
事實上,在微服務架構中,Zuul就是守門的大Boss!一夫當關,萬夫莫開!
3.2.Zuul加入後的架構
-
不管是來自於客戶端(PC或移動端)的請求,還是服務內部呼叫。一切對服務的請求都會經過Zuul這個閘道器,然後再由閘道器來實現 鑑權、動態路由等等操作。Zuul就是我們服務的統一入口。
3.3.快速入門
3.3.1.新建工程
填寫基本資訊:
新增Zuul依賴:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>cloud-demo</artifactId> <groupId>cn.itcast.demo</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>cn.itcast.demo</groupId> <artifactId>zuul-demo</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies> </project>
3.3.2.編寫啟動類
通過@EnableZuulProxy
註解開啟Zuul的功能:
@SpringBootApplication @EnableZuulProxy // 開啟Zuul的閘道器功能 public class ZuulDemoApplication { public static void main(String[] args) { SpringApplication.run(ZuulDemoApplication.class, args); } }
3.3.3.編寫配置
server: port: 10010 #服務埠 spring: application: name: api-gateway #指定服務名
3.3.4.編寫路由規則
我們需要用Zuul來代理user-service服務,先看一下控制面板中的服務狀態:
-
ip為:127.0.0.1
-
埠為:8081
對映規則:
zuul: routes: user-service: # 這裡是路由id,隨意寫 path: /user-service/** # 這裡是對映路徑 url: http://127.0.0.1:8081 # 對映路徑對應的實際url地址
我們將符合path
規則的一切請求,都代理到 url
引數指定的地址
本例中,我們將 /user-service/**
開頭的請求,代理到http://127.0.0.1:8081
3.3.5.啟動測試:
訪問的路徑中需要加上配置規則的對映路徑,我們訪問:http://127.0.0.1:8081/user-service/user/10
3.4.面向服務的路由
在剛才的路由規則中,我們把路徑對應的服務地址寫死了!如果同一服務有多個例項的話,這樣做顯然就不合理了。
我們應該根據服務的名稱,去Eureka註冊中心查詢 服務對應的所有例項列表,然後進行動態路由才對!
3.4.1.新增Eureka客戶端依賴
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
3.4.2.開啟Eureka客戶端發現功能
@SpringBootApplication @EnableZuulProxy // 開啟Zuul的閘道器功能 @EnableDiscoveryClient public class ZuulDemoApplication { public static void main(String[] args) { SpringApplication.run(ZuulDemoApplication.class, args); } }
3.4.3.新增Eureka配置,獲取服務資訊
eureka: client: registry-fetch-interval-seconds: 5 # 獲取服務列表的週期:5s service-url: defaultZone: http://127.0.0.1:10086/eureka instance: prefer-ip-address: true ip-address: 127.0.0.1
3.4.4.修改對映配置,通過服務名稱獲取
因為已經有了Eureka客戶端,我們可以從Eureka獲取服務的地址資訊,因此對映時無需指定IP地址,而是通過服務名稱來訪問,而且Zuul已經集成了Ribbon的負載均衡功能。
zuul: routes: user-service: # 這裡是路由id,隨意寫 path: /user-service/** # 這裡是對映路徑 serviceId: user-service # 指定服務名稱
3.4.5.啟動測試
再次啟動,這次Zuul進行代理時,會利用Ribbon進行負載均衡訪問:
日誌中可以看到使用了負載均衡器:
3.5.簡化的路由配置
在剛才的配置中,我們的規則是這樣的:
-
zuul.routes.<route>.path=/xxx/**
: 來指定對映路徑。<route>
是自定義的路由名 -
zuul.routes.<route>.serviceId=/user-service
:來指定服務名。
而大多數情況下,我們的<route>
路由名稱往往和 服務名會寫成一樣的。因此Zuul就提供了一種簡化的配置語法:zuul.routes.<serviceId>=<path>
比方說上面我們關於user-service的配置可以簡化為一條:
zuul: routes: user-service: /user-service/** # 這裡是對映路徑
省去了對服務名稱的配置。
3.6.預設的路由規則
在使用Zuul的過程中,上面講述的規則已經大大的簡化了配置項。但是當服務較多時,配置也是比較繁瑣的。因此Zuul就指定了預設的路由規則:
-
預設情況下,一切服務的對映路徑就是服務名本身。
-
例如服務名為:
user-service
,則預設的對映路徑就是:/user-service/**
-
也就是說,剛才的對映規則我們完全不配置也是OK的,不信就試試看。
3.7.路由字首
配置示例:
zuul: prefix: /api # 新增路由字首 routes: user-service: /user-service/** # 這裡是對映路徑
我們通過zuul.prefix=/api
來指定了路由的字首,這樣在發起請求時,路徑就要以/api開頭。
路徑/api/user-service/user/1
將會被代理到/user-service/user/1
3.8.過濾器
Zuul作為閘道器的其中一個重要功能,就是實現請求的鑑權。而這個動作我們往往是通過Zuul提供的過濾器來實現的。
3.8.1.ZuulFilter
ZuulFilter是過濾器的頂級父類。在這裡我們看一下其中定義的4個最重要的方法:
public abstract ZuulFilter implements IZuulFilter{ abstract public String filterType(); abstract public int filterOrder(); boolean shouldFilter();// 來自IZuulFilter Object run() throws ZuulException;// IZuulFilter }
-
shouldFilter
:返回一個Boolean
值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。 -
run
:過濾器的具體業務邏輯。 -
filterType
:返回字串,代表過濾器的型別。包含以下4種:-
pre
:請求在被路由之前執行 -
routing
:在路由請求時呼叫 -
post
:在routing和errror過濾器之後呼叫 -
error
:處理請求時發生錯誤呼叫
-
-
filterOrder
:通過返回的int值來定義過濾器的執行順序,數字越小優先順序越高。
3.8.2.過濾器執行生命週期:
這張是Zuul官網提供的請求生命週期圖,清晰的表現了一個請求在各個過濾器的執行順序。
-
正常流程:
-
請求到達首先會經過pre型別過濾器,而後到達routing型別,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
-
-
異常流程:
-
整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會將請求交給POST過濾器,最後返回給使用者。
-
如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後返回。
-
如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。
-
所有內建過濾器列表:
3.8.3.使用場景
場景非常多:
-
請求鑑權:一般放在pre型別,如果發現沒有訪問許可權,直接就攔截了
-
異常處理:一般會在error型別和post型別過濾器中結合來處理。
-
服務呼叫時長統計:pre和post結合使用。
3.9.自定義過濾器
接下來我們來自定義一個過濾器,模擬一個登入的校驗。基本邏輯:如果請求中有access-token引數,則認為請求有效,放行。
3.9.1.定義過濾器類
@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() throws ZuulException { // 登入校驗邏輯。 // 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; } }
3.9.2.測試
沒有token引數時,訪問失敗:
新增token引數後:
3.10.負載均衡和熔斷
Zuul中預設就已經集成了Ribbon負載均衡和Hystix熔斷機制。但是所有的超時策略都是走的預設值,比如熔斷超時時間只有1S,很容易就觸發了。因此建議我們手動進行配置:
zuul: retryable: true ribbon: ConnectTimeout: 250 # 連線超時時間(ms) ReadTimeout: 2000 # 通訊超時時間(ms) OkToRetryOnAllOperations: true # 是否對所有操作重試 MaxAutoRetriesNextServer: 2 # 同一服務不同例項的重試次數 MaxAutoRetries: 1 # 同一例項的重試次數 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 6000