接着上一篇:SpringCloud容錯保護Hystrix(一)
請求合併
上一篇寫到可以使用請求緩存來減輕高併發時的請求線程消耗、降低請求相應時間。請求合併又是什麼東西呢?在微服務架構中,我們將項目拆分成多個模塊,每個模塊間通過遠程調用進行通信。遠程調用最常見的問題是通信消耗與連接數佔用。在高併發情況下。隨着通信次數的增加,通信時間會增加;因爲依賴服務的線程池資源有限,將出現排隊等待與響應延遲的情況。請求合併正是Hystrix爲解決這兩個問題而開發的,以減少通信消耗和線程數的佔用。
Hystrix提供HystrixCollapser來實現請求轉發,在HystrixCommand之前放置一個合併處理器,將處於一個很短的時間窗(默認爲10毫秒)內對同一個依賴服務的多個請求進行整合並以批量方式發起請求,
HystrixCollapser是一個抽象類,進入源碼可以看到,它指定了三個不同的類。
- BatchReturnType: 合併後批量請求的返回類型;
- ResponseType: 單個請求返回的類型;
- RequestArgumentType: 請求參數類型。
對於這三個類型的使用:
//用來定義獲取請求參數的方法
public abstract RequestArgumentType getRequestArgument();
//合併請求產生批量命令的具體實現
protected abstract HystrixCommand<BatchReturnType> createCommand(Collection<com.netflix.hystrix.HystrixCollapser.CollapsedRequest<ResponseType, RequestArgumentType>> requests);
//批量命令結果返回後的處理,這裏需要實現將批量命令結果拆分並傳遞給合併前各個原子請求命令的邏輯
protected abstract void mapResponseToRequests(BatchReturnType batchResponse, Collection<com.netflix.hystrix.HystrixCollapser.CollapsedRequest<ResponseType, RequestArgumentType>> requests);
繼承方式
修改服務提供者
在之前的代碼基礎上,在USER-SERVICE中添加一個接口,這裏使用到的兩個接口:
- /user/{id}:根據id查詢用戶
- /users/ids?ids={ids}:查詢多個用戶,這裏id以逗號隔開。
@GetMapping("/user/{id}")
public User findById(@PathVariable("id") Long id){
return userRepository.findOne(id);
}
@GetMapping("/users/ids")
public List<User> findUserByIds(String ids){
System.out.println(">>>>>>>>>>"+ids);
String[] split = ids.split(",");
List<User> result = new ArrayList<>();
for (String s : split){
Long id = Long.valueOf(s);
User user = userRepository.findOne(id);
result.add(user);
}
return result;
}
服務消費者
1. 這裏我是通過ArticleService調用USER-SERVICE服務,在ArticleService中添加方法
public User getUserById(@CacheKey("id") Long id) {
return restTemplate.getForObject("http://USER-SERVICE/user/{1}", User.class, id);
}
public List<User> findUserByIds(List<Long> ids){
System.out.println("findUserByIds---------"+ids+"Thread.currentThread().getName():" + Thread.currentThread().getName());
String str = StringUtils.join(ids,",");
User[] users = restTemplate.getForObject("http://USER-SERVICE/users/ids?ids={1}", User[].class,str);
return Arrays.asList(users);
}
- 實現一個批量請求命令
public class UserBatchCommand extends HystrixCommand<List<User>> {
private final Logger logger = LoggerFactory.getLogger(UserCommand.class);
private List<Long> ids;
private ArticleService articleService;
public UserBatchCommand(ArticleService articleService,List<Long> ids){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup")));
this.ids = ids;
this.articleService = articleService;
}
@Override
protected List<User> run() throws Exception {
return articleService.findUserByIds(ids);
}
}
- 通過繼承HystrixCollapser實現請求合併器
public class UserCollapdeCommand extends HystrixCollapser<List<User>,User,Long> {
private ArticleService articleService;
private Long id;
public UserCollapdeCommand(ArticleService articleService,Long id){
//設置時間延遲屬性,延遲時間窗爲100毫秒
super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("userCollapdeCommand")).andCollapserPropertiesDefaults(
HystrixCollapserProperties.Setter().withTimerDelayInMilliseconds(100)
));
this.articleService = articleService;
this.id = id;
}
/**
* 返回單個請求參數id
* @return
*/
@Override
public Long getRequestArgument() {
return id;
}
/**
* 這裏通過獲取單個請求的參數來組織批量請求命令UserBatchCommand的實例
* @param collapsedRequests 保存了延遲時間窗中收集到的所有獲取單個User的請求。
* @return
*/
@Override
protected HystrixCommand<List<User>> createCommand(Collection<CollapsedRequest<User, Long>> collapsedRequests) {
ArrayList<Long> userIds = new ArrayList<>(collapsedRequests.size());
userIds.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
return new UserBatchCommand(articleService,userIds);
}
/**
* 該方法在批量請求命令UserBatchCommand執行完成之後執行
* 通過遍歷batchResponse來爲collapsedRequests設置請求結果。
* @param batchResponse 保存了createCommand中組織的批量請求返回結果
* @param collapsedRequests 每個被合併的請求,
*/
@Override
protected void mapResponseToRequests(List<User> batchResponse, Collection<CollapsedRequest<User, Long>> collapsedRequests) {
int count = 0;
for (CollapsedRequest<User,Long> collapsedRequest : collapsedRequests){
User user = batchResponse.get(count++);
collapsedRequest.setResponse(user);
}
}
}
- 測試接口,因爲要將請求合併是合併100毫秒時間窗的請求,所以這裏使用異步請求的方式。
@GetMapping("/testBathCommand")
public List<User> testBathCommand() throws ExecutionException, InterruptedException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
UserCollapdeCommand u1 = new UserCollapdeCommand(articleService, 1L);
UserCollapdeCommand u2 = new UserCollapdeCommand(articleService, 2L);
UserCollapdeCommand u3 = new UserCollapdeCommand(articleService, 3L);
UserCollapdeCommand u4 = new UserCollapdeCommand(articleService, 4L);
Future<User> q1 = u1.queue();
Future<User> q2 = u2.queue();
Future<User> q3 = u3.queue();
Future<User> q4 = u4.queue();
User e1 = q1.get();
User e2 = q2.get();
User e3 = q3.get();
User e4 = q4.get();
List<User> res = new ArrayList<>();
res.add(e1);
res.add(e2);
res.add(e3);
res.add(e4);
System.out.println(res);
return res;
}
註解方式
上面使用繼承類的方式可能會有些繁瑣,在Hystrix中同樣提供了註解來優雅的實現請求合併。
@HystrixCollapser(batchMethod = "findAll",collapserProperties = {
@HystrixProperty(name = "DelayInMilliseconds",value = "100")
})
public User findOne(Long id){
return null;
}
@HystrixCommand
public List<User> findAll(List<Long> ids){
System.out.println("findUserByIds---------"+ids+"Thread.currentThread().getName():" + Thread.currentThread().getName());
String str = StringUtils.join(ids,",");
User[] users = restTemplate.getForObject("http://USER-SERVICE/users/ids?ids={1}", User[].class,str);
return Arrays.asList(users);
}
這裏通過@HystrixCollapser註解創建合併請求器,通過batchMethod屬性指定實現批量請求的findAll方法,通過HystrixProperty屬性爲合併請求器設置相關屬性。 @HystrixProperty(name = "DelayInMilliseconds",value = "100")
設置時間窗爲100毫秒。這裏直接調用findOne方法即可,使用註解確實是簡單。
測試接口
@GetMapping("/testBathCommandAnn")
public List<User> testBathCommandAnn() throws ExecutionException, InterruptedException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
Future<User> q1 = articleService.findOne(1L);
Future<User> q2 = articleService.findOne(2L);
Future<User> q3 = articleService.findOne(3L);
Future<User> q4 = articleService.findOne(4L);
User e1 = q1.get();
User e2 = q2.get();
User e3 = q3.get();
User e4 = q4.get();
List<User> res = new ArrayList<>();
res.add(e1);
res.add(e2);
res.add(e3);
res.add(e4);
System.out.println(res);
return res;
}
Hystrix屬性
Hystrix提供了非常靈活的配置方式,所有屬性存在下面四個優先級的配置(優先級由低到高):
- 全局默認配置:如果沒有下面三個級別的屬性,那麼該屬性就是默認的;
- 全局配置屬性:通過配置文件中定義;
- 實例默認值:通過代碼爲實例定義默認值;
- 實例配置屬性:通過配置文件來指定的實例進行屬性配置。
Hystrix中主要的三個屬性:
- Command屬性:主要用來控制HystrixCommand命令行爲;
- Collapser屬性:主要用來控制命令合併相關的行爲;
- ThreadPool屬性:用來控制Hystrix命令所屬線程池的配置。
關於屬性參數的更多詳解可以查看《SpringCloud微服務實戰》
Hystrix儀表盤
單機監控
儀表盤是Hystrix Dashboard提供的用來實時監控Hystrix的指標信息的組件。通過該組件反饋的實時信息,可以幫助我們快速的發現系統存在的問題。項目結構圖
1. 創建一個名爲hystrix-dashborad的SpringBoot項目,然後修改pom文件,添加一下依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 然後再主類上使用註解
@EnableHystrixDashboard
開啓HystrixDashboard功能。 - 修改配置文件:
spring:
application:
name: hystrix-dashboard
server:
port: 9999
- 啓動項目,這裏的監控方式是根據指定的url開啓。前兩個是對集羣的監控,需要整合Turbin才能實現
- 修改需要監控的服務實例,這裏監控ARTICLE-SERVICE。添加
spring-boot-starter-actuator
監控模塊的以開啓監控相關的端點。還有hystrix依賴是一定要的。並且確保服務以及使用@EnableCircuitBreaker
開啓了斷路器功能。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 重啓服務實例
- 將需要監控的服務地址輸入上面的輸入框,這裏是:http://localhost:30000/hystrix.stream。然後點擊Monitor Stream按鈕。說明:這裏要訪問/hystrix.stream,需要先訪問被監控服務的任意其他接口,否則將不會無法獲取到相應的數據。
集羣監控
可以使用Turbine實現集羣監控,該端點爲/trubine.stream。和上面一樣,新建一個SpringBoot項目,這裏命名爲hystrix-turbine。添加以下依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
</dependencies>
在主類使用@EnableTurbine
註解開啓Trubine
@SpringBootApplication
@EnableDiscoveryClient
@EnableTurbine
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
修改配置文件
spring.application.name=hystrix-turbine
server.port=9998
management.port=9000
eureka.client.service-url.defaultZone=http://localhost:8888/eureka/
turbine.app-config=article-service
turbine.cluster-name-expression="default"
turbine.combine-host-port=true
- turbine.app-config=ribbon-consumer指定了要監控的應用名字爲ribbon-consumer
- turbine.cluster-name-expression=”default”,表示集羣的名字爲default
- turbine.combine-host-port=true表示同一主機上的服務通過host和port的組合來進行區分,默認情況下是使用host來區分,這樣會使本地調試有問題
最後啓動項目,並啓動兩個article-service,然後添加對 http://localhost:9998/turbine.stream的監控。
作爲SpringCloud學習筆記,有很多地方不好。望指出!!!