SpringCloud微服務治理一(介紹,環境搭建,Eureka)
SpringCloud微服務治理二(Robbin,Hystix,Feign)
SpringCloud微服務治理三(Zuul閘道器)
6.負載均衡Ribbon
在剛才的案例中,我們啟動了一個user-service,然後通過DiscoveryClient來獲取服務例項資訊,然後獲取ip和埠來訪問。
但是實際環境中,我們往往會開啟很多個user-service的叢集。此時我們獲取的服務列表中就會有多個,到底該訪問哪一個呢?
一般這種情況下我們就需要編寫負載均衡演算法,在多個例項列表中進行選擇。
接下來,我們就來使用Ribbon實現負載均衡。
6.1.啟動兩個服務例項
首先我們啟動兩個user-service例項,一個8081,一個8082。
Eureka監控皮膚:
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());
}
}
}
複製程式碼
結果:
符合了我們的預期推測,確實是輪詢方式。
SpringBoot也幫我們提供了修改負載均衡規則的配置入口:
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
複製程式碼
格式是:{服務名稱}.ribbon.NFLoadBalancerRuleClassName
6.4.重試機制
Eureka的服務治理強調了CAP原則中的AP,即可用性和可靠性。它與Zookeeper這一類強調CP(一致性,可靠性)的服務治理框架最大的區別在於:Eureka為了實現更高的服務可用性,犧牲了一定的一致性,極端情況下它寧願接收故障例項也不願丟掉健康例項,正如我們上面所說的自我保護機制。
但是,此時如果我們呼叫了這些不正常的服務,呼叫就會失敗,從而導致其它服務不能正常工作!這顯然不是我們願意看到的。
我們現在關閉一個user-service例項:
因為服務剔除的延遲,consumer並不會立即得到最新的服務列表,此時再次訪問你會得到錯誤提示:
但是此時,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當機,也能通過另一臺服務例項獲取到結果!
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的訪問時間分別是:
id為12的訪問時間:
因此,只有12是正常訪問,其它都會觸發熔斷,我們來檢視結果:
7.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
複製程式碼
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依賴和自動配置:
因此我們不需要額外引入依賴,也不需要再註冊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)重啟專案,即可看到每次訪問的日誌: