SpringCloud微服務治理二(Robbin,Hystix,Feign)

QLQ發表於2019-05-15

SpringCloud微服務治理一(介紹,環境搭建,Eureka)
SpringCloud微服務治理二(Robbin,Hystix,Feign)
SpringCloud微服務治理三(Zuul閘道器)

6.負載均衡Ribbon

在剛才的案例中,我們啟動了一個user-service,然後通過DiscoveryClient來獲取服務例項資訊,然後獲取ip和埠來訪問。

但是實際環境中,我們往往會開啟很多個user-service的叢集。此時我們獲取的服務列表中就會有多個,到底該訪問哪一個呢?

一般這種情況下我們就需要編寫負載均衡演算法,在多個例項列表中進行選擇。

接下來,我們就來使用Ribbon實現負載均衡。

6.1.啟動兩個服務例項

首先我們啟動兩個user-service例項,一個8081,一個8082。

Eureka監控皮膚:

1525619546904

6.2.開啟負載均衡

因為Eureka中已經整合了Ribbon,所以我們無需引入新的依賴。直接修改程式碼:

在RestTemplate的配置方法上新增@LoadBalanced註解:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
複製程式碼

修改呼叫方式,不再手動獲取ip和埠,而是直接通過服務名稱呼叫:

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // 地址直接寫服務名稱即可
        String baseUrl = "http://user-service/user/";
        ids.forEach(id -> {
            // 我們測試多次查詢,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次間隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}
複製程式碼

6.3.負載均衡策略

Ribbon預設的負載均衡策略是簡單的輪詢,我們可以測試一下:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UserConsumerDemoApplication.class)
public class LoadBalanceTest {

    @Autowired
    RibbonLoadBalancerClient client;

    @Test
    public void test(){
        for (int i = 0; i < 100; i++) {
            ServiceInstance instance = this.client.choose("user-service");
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}

複製程式碼

結果:

1525622357371

符合了我們的預期推測,確實是輪詢方式。

SpringBoot也幫我們提供了修改負載均衡規則的配置入口:

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
複製程式碼

格式是:{服務名稱}.ribbon.NFLoadBalancerRuleClassName

6.4.重試機制

Eureka的服務治理強調了CAP原則中的AP,即可用性和可靠性。它與Zookeeper這一類強調CP(一致性,可靠性)的服務治理框架最大的區別在於:Eureka為了實現更高的服務可用性,犧牲了一定的一致性,極端情況下它寧願接收故障例項也不願丟掉健康例項,正如我們上面所說的自我保護機制。

但是,此時如果我們呼叫了這些不正常的服務,呼叫就會失敗,從而導致其它服務不能正常工作!這顯然不是我們願意看到的。

我們現在關閉一個user-service例項:

1525653565855

因為服務剔除的延遲,consumer並不會立即得到最新的服務列表,此時再次訪問你會得到錯誤提示:

1525653715488

但是此時,8081服務其實是正常的。

因此Spring Cloud 整合了Spring Retry 來增強RestTemplate的重試能力,當一次服務呼叫失敗後,不會立即丟擲一次,而是再次重試另一個服務。

只需要簡單配置即可實現Ribbon的重試:

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true # 開啟Spring Cloud的重試功能
user-service:
  ribbon:
    ConnectTimeout: 250 # Ribbon的連線超時時間
    ReadTimeout: 1000 # Ribbon的資料讀取超時時間
    OkToRetryOnAllOperations: true # 是否對所有操作都進行重試
    MaxAutoRetriesNextServer: 1 # 切換例項的重試次數
    MaxAutoRetries: 1 # 對當前例項的重試次數
複製程式碼

根據如上配置,當訪問到某個服務超時後,它會再次嘗試訪問下一個服務例項,如果不行就再換一個例項,如果不行,則返回失敗。切換次數取決於MaxAutoRetriesNextServer引數的值

引入spring-retry依賴

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
複製程式碼

我們重啟user-consumer-demo,測試,發現即使user-service2當機,也能通過另一臺服務例項獲取到結果!

1525658269456

7.Hystix

7.1.簡介

Hystix是Netflix開源的一個延遲和容錯庫,用於隔離訪問遠端服務、第三方庫,防止出現級聯失敗。

7.2.熔斷器的工作機制:

正常工作的情況下,客戶端請求呼叫服務API介面:

當有服務出現異常時,直接進行失敗回滾,服務降級處理:

當服務繁忙時,如果服務出現異常,不是粗暴的直接報錯,而是返回一個友好的提示,雖然拒絕了使用者的訪問,但是會返回一個結果。

7.3.動手實踐

7.3.1.引入依賴

首先在user-consumer中引入Hystix依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
複製程式碼

7.3.2.開啟熔斷

7.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;
    }
}
複製程式碼

7.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);
    }
}

複製程式碼

7.3.4.啟動測試

然後執行並檢視日誌:

id為9、10、11的訪問時間分別是:

1525661641660

id為12的訪問時間:

1525661669136

因此,只有12是正常訪問,其它都會觸發熔斷,我們來檢視結果:

1525661720656

7.3.5.優化

雖然熔斷實現了,但是我們的重試機制似乎沒有生效,是這樣嗎?

其實這裡是因為我們的Ribbon超時時間設定的是1000ms:

1525666632542

而Hystix的超時時間預設也是1000ms,因此重試機制沒有被觸發,而是先觸發了熔斷。

所以,Ribbon的超時時間一定要小於Hystix的超時時間。

我們可以通過hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds來設定Hystrix超時時間。

hystrix:
  command:
  	default:
        execution:
          isolation:
            thread:
              timeoutInMillisecond: 6000 # 設定hystrix的超時時間為6000ms
複製程式碼

8.Feign

8.1.簡介

Feign可以把Rest的請求進行隱藏,偽裝成類似SpringMVC的Controller一樣。你不用再自己拼接url,拼接引數等等操作,一切都交給Feign去做。

8.2.快速入門

8.2.1.匯入依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
複製程式碼

8.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;
    }
}
複製程式碼

8.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了

8.3.負載均衡

Feign中本身已經整合了Ribbon依賴和自動配置:

1525672070679

因此我們不需要額外引入依賴,也不需要再註冊RestTemplate物件。

另外,我們可以像上節課中講的那樣去配置Ribbon,可以通過ribbon.xx來進行全域性配置。也可以通過服務名.ribbon.xx來對指定服務配置:

user-service:
  ribbon:
    ConnectTimeout: 250 # 連線超時時間(ms)
    ReadTimeout: 1000 # 通訊超時時間(ms)
    OkToRetryOnAllOperations: true # 是否對所有操作重試
    MaxAutoRetriesNextServer: 1 # 同一服務不同例項的重試次數
    MaxAutoRetries: 1 # 同一例項的重試次數
複製程式碼

8.4.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);
}

複製程式碼

8.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 # 設定觸發壓縮的大小下限
複製程式碼

注:上面的資料型別、壓縮大小下限均為預設值。

8.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)重啟專案,即可看到每次訪問的日誌:

1525674544569

相關文章