Hystrix服務容錯處理(一)


在微服務架構中存在多個可直接調用的服務,這些服務若在調用時出現故障會導致連鎖效應,也就是可能會讓整個系統變得不可用,這種情況我們稱之為服務雪崩效應。

如何避免服務雪崩效應?通過Hystrix就能夠解決。

1.Hystrix

Hystrix是Netflix針對微服務分布式系統采用的熔斷保護中間件, 相當於電路中的保險 絲。在微服務架構下,很多服務都相互依賴,如果不能對依賴的服務進行隔離,那么服務 本身也有可能發生故障, Hystrix通過Hystrix Command對調用進行隔離, 這樣可以阻止故 障的連鎖反應,能夠讓接口調用快速失敗並迅速恢復正常,或者回退並優雅降級。

1.1 Hystrix的簡單實用

創建一個空的Maven項目,增加Hystrix的依賴:

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.18</version>
</dependency>

編寫第一個HystrixCommand,如代碼所示:

public class MyHystrixCommand extends HystrixCommand<String>{
    private final String name;
    public MyHystrixCommand(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
        this.name=name;
    }
    @Override
    protected String run() throws Exception {
        // TODO Auto-generated method stub
        return this.name+":"+Thread.currentThread().getName();
    }

}

首先需要繼承HystrixCommand,通過構造函數設置一個Groupkey。具體的邏輯在run方法中,我們返回了一個當前線程名稱的值,寫一個main方法來調用上面編寫的MyHystrixCommand程序,如代碼所示:

public static void main(String[] args) throws InterruptedException,ExecutionException{
        String result=new MyHystrixCommand("yinjihuan").execute();
        System.out.println(result);
    }

輸出結果是:yinjihuan:hystrix-MyGroup-1.由此可以看出,構造函數中設置的組名變成了線程的名字。

上面是同步調用,如果需要異步調用可以使用代碼如下:

public static void main(String[] args) throws InterruptedException,ExecutionException{

        Future<String> future=new MyHystrixCommand("yinjihuan").queue();
        System.out.println(future.get());
    }

1.2 回退支持

下面我們通過增加執行時間模擬調用失敗的情況,首先改造MyHystrixCommand,增加getFallback方法返回回退內容,如代碼所示:

public class MyHystrixCommand extends HystrixCommand<String>{
    private final String name;
    public MyHystrixCommand(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
        this.name=name;
    }
    @Override
    protected String run() throws Exception {
        // TODO Auto-generated method stub
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return this.name+":"+Thread.currentThread().getName();
    }
    protected String getFallback() {
        return "失敗了";
    }
    
    
    public static void main(String[] args) throws InterruptedException,ExecutionException{
//        String result=new MyHystrixCommand("yinjihuan").execute();
//        System.out.println(result);
        Future<String> future=new MyHystrixCommand("yinjihuan").queue();
        System.out.println(future.get());
    }

}

執行代碼,返回內容是”失敗了”,證明觸發了回退。

1.3 信號量策略配置

信號量策略配置方法如代碼所示:

public MyHystrixCommand(String name) {
        super(HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionIsolationStrategy(
                                HystrixCommandProperties
                                .ExecutionIsolationStrategy.SEMAPHORE)));
        this.name=name;
    }

之前在run方法中特意輸出了線程名稱,通過這個名稱就可以確定當前是線程隔離還是信號量隔離。

1.4 線程隔離策略配置

系統默認采用線程隔離策略,我們可以通過andThreadPoolPropertiesDefaults配置線程池的一些參數,如代碼所示:

public MyHystrixCommand(String name) {
        super(HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withExecutionIsolationStrategy(
                                HystrixCommandProperties
                                .ExecutionIsolationStrategy.THREAD)
                        ).andThreadPoolPropertiesDefaults(
                                HystrixThreadPoolProperties.Setter()
                                .withCoreSize(10)
                                .withMaxQueueSize(100)
                                .withMaximumSize(100)
                                )
                );
        this.name=name;
    }

1.5 結果緩存

緩存在開發中經常用到,我們常用Redis這種第三方的緩存數據庫來對數據進行緩存處理,Hystrix中也為我們提供了方法級別的緩存。通過重寫getCacheKey來判斷是否返回緩存的數據,getCacheKey可以根據參數來生成,這樣同樣的參數就可以都用到緩存了。

改寫之前的MyHystrixCommand,在其中增加getCacheKey的重寫實現,如代碼所示:

//返回緩存key
protected String getCacheKey() {
        return String.valueOf(this.name);
    }

上面代碼中我們創建對象傳進來的name參數作為緩存的key。

為了證明能夠用到緩存,在run方法中加一行輸出,在調用多次的情況下,如果控制台只輸出了一次,那么可以知道后面的都是走的緩存邏輯,如代碼所示:

protected String run()  {
        // TODO Auto-generated method stub
        System.err.println("gat data");
        return this.name+":"+Thread.currentThread().getName();
    }

執行main方法,發現報錯: Request caching is not available. Maybe you need to initialize the HystrixRequestContext?

根據錯誤提示可以知道,緩存的處理請求取決於請求的上下文,我們必須初始化Hystrix-RequestContext.

改造main方法中的調用代碼,初始化HystrixRequestContext,如代碼所示:

public static void main(String[] args) throws InterruptedException,ExecutionException{
        HystrixRequestContext context=HystrixRequestContext.initializeContext();
        String result=new MyHystrixCommand("yinjihuan").execute();
        System.out.println(result);
        Future<String> future=new MyHystrixCommand("yinjihuan").queue();
        System.out.println(future.get());
        context.shutdown();
    }

改造完之后重新執行main方法,可以正常運行了,輸出結構如下:

get data
yinjihuan:hystrix-MyGroup-1
yinjihuan:hystrix-MyGroup-1

可以看到只輸出一次get data,緩存生效。

1.6 緩存清除

在上節學習了如何使用hystrix來實現數據緩存功能。有緩存必然就有清除緩存的動作,當數據發生變動時,必須將緩存中的數據也更新掉,不然就會產生臟數據的問題。同樣,Hystrix也有清除緩存的功能。

增加一個支持清除緩存的類,如代碼所示:

public class ClearCacheHystrixCommand extends HystrixCommand<String>{
    private final String name;
    private static final HystrixCommandKey GETTER_KEY=HystrixCommandKey.Factory.asKey("MyKey");
    public ClearCacheHystrixCommand(String name) {
        super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.
                Factory.asKey("MyGroup")).andCommandKey(GETTER_KEY));
        this.name=name;
    }
    
    public static void flushCache(String name) {
        HystrixRequestCache.getInstance(
                GETTER_KEY, HystrixConcurrencyStrategyDefault.getInstance()).clear(name);
    }
    
    
    protected String getCacheKey() {
        return String.valueOf(this.name);
    }
    @Override
    protected String run() throws Exception {
        System.err.println("get data");
        return this.name+":"+Thread.currentThread().getName();
    }
    
    protected String getFallback() {
        return "失敗了";
    }

}
flushCache方法就是清除緩存的方法,通過HystrixRequestCache來執行清除操作,根據getCacheKey返回的key來清除。

修改調用代碼來驗證清除是否有效果,如代碼所示:

HystrixRequestContext context=HystrixRequestContext.initializeContext();
        String result=new ClearCacheHystrixCommand("yinjihuan").execute();
        System.out.println(result);
        ClearCacheHystrixCommand.flushCache("yinjihuan");
        Future<String> future=new ClearCacheHystrixCommand("yinjihuan").queue();
        System.out.println(future.get());

執行兩次相同的key,在第二次執行之前調用緩存清除的方法,也就是說第二次用不到緩存,輸出結果如下:

get data
yinjihuan:hystrix-MyGroup-1
get data
yinjihuan:hystrix-MyGroup-2
1.7 合並請求

Hystrix支持將多個請求自動合並為一個請求,利用這個功能可以節省網絡開銷,比如每個請求都有通過網絡訪問遠程資源。如果把多個請求合並為一個一起執行,將多次網絡交互變成一次,則會極大節省開銷。如代碼所示:

public class MyHystrixCollapser extends HystrixCollapser<List<String>,String,String>{

    private final String name;
    public MyHystrixCollapser(String name) {
        this.name=name;
    }
    @Override
    public String getRequestArgument() {
        // TODO Auto-generated method stub
        return name;
    }

    @Override
    protected HystrixCommand<List<String>> createCommand(final Collection<CollapsedRequest<String, String>> requests) {
        // TODO Auto-generated method stub
        return new BatchCommand(requests);
    }

    @Override
    protected void mapResponseToRequests(List<String> batchResponse,
            Collection<CollapsedRequest<String, String>> requests) {
        int count=0;
        for(CollapsedRequest<String, String>request:requests) {
            request.setResponse(batchResponse.get(count++));
        }
    }
    
    private static final class BatchCommand extends HystrixCommand<List<String>>{
        private final Collection<CollapsedRequest<String,String>> requests;
        private BatchCommand(Collection<CollapsedRequest<String, String>>requests) {
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("GetValueForKey")));
                this.requests=requests;
        }
        @Override
        protected List<String> run() throws Exception {
            System.out.println("真正執行請求......");
            ArrayList<String> response=new ArrayList<String>();
            for(CollapsedRequest<String, String>request:requests) {
                response.add("返回結果:"+request.getArgument());
            }
            return response;
        }
    }

}

接下來編寫測試代碼,如代碼所示:

HystrixRequestContext context=HystrixRequestContext.initializeContext();
        Future<String> f1=new MyHystrixCollapser("yinjihuan").queue();
        Future<String> f2=new MyHystrixCollapser("yinjihuan333").queue();
        System.out.println(f1.get()+"="+f2.get());
        context.shutdown();
通過

MyHystrixCollapser創建兩個執行任務,按照正常的邏輯肯定是分別執行這兩個任務,通過HystrixCollapser可以將多個任務合並到一起執行。從輸出結果可以看出,任務的執行是在run方法中去做的,輸出結果如下:

真正執行請求......
返回結果:yinjihuan=返回結果:yinjihuan333


2.在Spring Cloud中使用Hystrix

2.1 簡單使用

在fsh-substitution服務中增加Hystrix的依賴:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
        </dependency>

在啟動類上添加@EnableHystrix或者@EnableCircuitBreaker。注意,@EnableHystrix中包含了@EnableCircuitBreaker。

然后改造之前的callHello方法,在上面增加一個@HystrixCommand注解,用於指定依賴服務調用延遲或失敗時調用的方法,如代碼所示:

@GetMapping("/callHello")
    @HystrixCommand(fallbackMethod="defaultCallHello")
    public String callHello() {

String result=restTemplate.getForObject("http://fsh-house/house/hello", String.class);

        return result;
    }

調用失敗觸發熔斷時會用defaultCallHello方法來回退具體的內容,定義defaultCallHello方法的代碼如下所示:

public String defaultCallHello() {
        return "fail";
    }

只啟動fsh-substitution服務而不啟動fsh-house服務, 調用/callHello接口, 可以看到返回的內容是“fail”。

將啟動類上的@EnableHystrix去掉, 重啟服務, 再次調用/callHello接口可以看到返 回的是500錯誤信息,這個時候就沒有用到回退功能了。

2.2 配置詳解

HystrixCommand中除了fallbackMethod還有很多的配置,下面來看看這些配置:

官方的配置信息文檔請參考:https://github.com/Netflix/Hystrix/wiki/Configuration


2.3 Feign整合Hystrix服務容錯

首先需要執行2.1節中的集成步驟,然后在屬性文件中開啟Feign對Hystrix的支持:

feign.hystrix.enabled=true

1.Fallback方式

在Feign的客戶端類上的@FeignClient注解中指定fallback進行回退,改造fsh-housed客戶端類HouseRemoteClient,為其配置fallback。代碼如下:

@FeignClient(value = "fsh-house", path = "/house", configuration = FeignConfiguration.class
        , fallback = HouseRemoteClientHystrix.class)
public interface HouseRemoteClient {
    
    @GetMapping("/{houseId}")
    HouseInfoDto hosueInfo@PathVariable("houseId")Long houseId);
}

HouseRemoteClientHystrix類需要實現HouseRemoteClient類中所有的方法,返回回退時的內容,如代碼所示:

@Component
public class HouseRemoteClientHystrix implements HouseRemoteClient{
    
    public HouseInfoDto houseInfo(Long houseId){
        return new HouseInfoDto();
}

}

啟動fsh-substitution服務,停掉所有fsh-house服務,然后訪問http://localhost:8082/substitution/1接口,這時候fsh-house服務是不可用的,必然觸發回退,返回的內容是正常的格式,只是house對象是空的,這證明回退生效了。在這種情況下,如果你的接口調用了多個服務的接口,那么只有fsh-house服務會沒數據,不會影響別的服務,如果不用Hystrix回退處理,整個請求都將失敗。

2.FallbackFactory方式

通過fallback已經可以實現服務不可用時回退的功能,如果你想知道觸發回退的原因,可以使用FallbackFactory來實現回退功能,如代碼所示:

@Component
public class HouseRemoteClientFallbackFactory implements FallbackFactory<HouseRemoteClient> {
    @Override
    public HouseRemoteClient create(final Throwable cause) {
        return new HouseRemoteClient() {

            @Override
            public HouseInfoDto hosueInfo(Long houseId) {
                HouseInfoDto info = new HouseInfoDto();
                info.setData(new HouseInfo(1L, "", "", cause.getMessage()));
                return info;
            }
};
}
}

FallbackFactory的使用就是在@FeignClient中用fallbackFactory指定回退處理類,如代碼所示:

@FeignClient(value = "fsh-house", path = "/house", configuration = FeignConfiguration.class
        , fallbackFactory = HouseRemoteClientFallbackFactory.class)

在這個回退處理的時候,將異常信息設置到HouseInfo中的name屬性中了。我們重啟fsh-substitution,調用接口,可以看到異常信息返回在結果里面了,FallbackFactory和Fallback唯一的區別就在這里。

2.4 Feign中禁用Hystrix

禁用Hystrix還是比較簡單的,目前有兩種方式可以禁用,其一是在屬性文件中進行全部禁用:

feign.hystrix.enabled=false

另一種是通過代碼的方式禁用某個客戶端,在Feign的配置類中增加代碼如下所示:

@Configuration
public class FeignConfiguration{

    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder(){
        return Feign.builder();
    }
}
3.Hystrix監控

在微服務架構中,Hystrix除了實現容錯外,還提供了實時監控功能。在服務調用時,Hystrix會實時累積關於HystrixCommand的執行信息,比如每秒的請求數、成功數等。更多的指標信息請查看官方文檔地址:

https://github.com/Netflix/Hystrix/wiki/Metrics-and-Monitoring.

Hystrix監控需要兩個必備條件:

(1)必須有Actuator的依賴,如代碼所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artigactId>
</dependency>

(2)必須有Hystrix的依賴,Spring Cloud中必須在啟動類中添加@EnableHystrix開啟Hystrix,如代碼所示:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

我們訪問fsh-substitution中的地址(http://localhost:8082/hystrix.sream)中可以看到一直在輸出“ping:”,這種情況是因為還沒有數據,等到HystrixCommand執行了之后就可以看到具體數據了。

4.整合Dashboard查看監控數據

上面講了通過hystrix.stream端點來獲取監控數據,但是這些數據是以字符串的形式展現的,實際使用中不方便查看,我們可以借助Hystrix-dashboard對監控進行圖形化展示。

源碼參考:https://github.com/yinjihuan/spring-cloud/tree/master/fangjia-hystrix-dashboard

創建一個maven項目,在pom.xml中添加dashboard的依賴,如代碼:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
        </dependency>

創建啟動類,在啟動類上添加@EnableHystrixDashboard注解,如代碼所示:

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication{
    public static void main(String[] args){
        SpringApplication.run(HystrixDashboardApplication.class,args);
}
}

在屬性配置文件中只需要配置服務名稱和服務端口:

spring.application.name=fangjia-hystrix-dashboard
server.port=9011

5.Turbine聚合集群數據

5.1 Turbine使用

Turbine是聚合服務器發送事件流數據的一個工具。Hystrix只能監控單個節點, 然后通 過dashboard進行展示。實際生產中都為集群, 這個時候我們可以通過Turbine來監控集群 下Hystrix的metrics情況, 通過Eureka來發現Hystrix服務。

首先增加turbine的依賴:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-turbine</artifactId>
        </dependency>
在啟動上增加@EnableTurbine和@EnableDiscoveryClient。在屬性文件中配置如下:
eureka.client.serviceUrl.defaultZone=http://yinjihuan:123456@master:8761/eureka/
#配置需要聚合的服務名稱
turbine.appConfig=fsh-substitution,fsh-house
#Turbine需要聚合的集群名稱
turbine.aggregator.clusterConfig=default
#集群名表達式
turbine.clusterNameExpression=new String("default")

重啟服務,就可以使用http://localhost:9011/turbine.stream來訪問集群的監控數據了。

5.2 context-path導致監控失敗

如果被監控的服務中設置了context-path,則會導致Turbine無法獲取監控數據。這時需要在Turbine中指定turbine.instanceUrlSuffix來解決這個問題:turbine.instanceUrlSuffix=/sub/hystrix.stream

sub用於監控服務的context-path。這種方式是全局配置,如果每個服務的context-path都不一樣,這個時候有一些就會出問題,那么就需要對每個服務做一個集群,然后配置集群對應的context-path:

turbine.instanceUrlSuffix.集群名稱=/sub/hystrix.stream

更多內容可參見官方文檔:https://github.com/Netflix/Turbine/wiki/Configuration-(1.x).


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM