服務降級是一種常見的應對系統過載或部分系統故障的策略,旨在保證系統的整體可用性和穩定性。
在面對高流量或部分組件故障時,服務降級可以通過減少服務的一部分功能來減輕服務器的負載,從而避免整個系統崩潰,保證核心服務的正常運行。
服務降常見的方式如下:
在以下場景下可考慮是使用服務降級:
在促銷、大型活動或者突發新聞事件期間,網站或應用可能會經歷流量激增。
可以在這種情況下通過臨時關閉一些非關鍵功能,如評論、推薦系統等,來減輕服務器負載,確保核心交易或內容訪問的流暢。
當系統的某個組件或服務因故障而無法正常工作時,可以通過降低對該組件的依賴,切換到備用流程或提供簡化服務來維持系統整體的可用性,如使用緩存數據代替實時數據處理。
在服務器資源緊張(如CPU、內存使用率高)或依賴的外部服務響應緩慢時,可以通過降低服務質量,減少資源消耗來防止系統過載。
如:對非核心服務返回緩存內容或者靜態內容以減少對數據庫的查詢和對網服務的訪問。
在系統維護或部署新版本期間,可能需要臨時減少服務負載以避免給正在進行的操作帶來影響。
此時,可以實施服務降級策略,如暫停非核心服務,確保關鍵服務的平穩運行。
在網絡延遲高或網絡連接不穩定時,服務降級可以通過降低對實時數據的依賴,使用本地緩存或降低數據同步頻率來改善用戶體驗。
當系統依賴的外部服務(如第三方API、數據庫服務等)限流或暫時不可用時,服務降級可以通過切換到備選方案或提供有限的服務來減少對用戶體驗的影響。
對于不同級別的用戶,可以實施差異化的服務降級策略,比如在系統負載高時,優先保障VIP用戶的服務,對普通用戶實施一定程度的服務降級。
服務降級的功能性要求主要圍繞確保系統的可用性、穩定性以及在高負載和部分服務故障情況下的恢復能力。
實現服務降級策略時,需要考慮以下這些功能點:
常用的一些框架提供了服務降級的功能,包括:Hystrix、Sentinel 等。
Hystrix 工作原理的相關內容參考自 https://github.com/Netflix/Hystrix/wiki/How-it-Works
Hystrix的工作原理
Hystrix的核心工作機制,包括命令模式、熔斷器、資源隔離、降級機制和性能監控。
構造一個HystrixCommand 或者 HystrixObservableCommand對象。該對象代碼客戶端來執行真正的調用請求,調用請求所需的參數作為Command對象構造函數的入參。
如果調用的服務只返回一個響應結果,可以使用HystrixCommand。
HystrixCommand command=new HystrixCommand(arg1, arg2);
如果通過Observable來返回多個響應,可以使用HystrixObservableCommand
HystrixObservableCommand command=new HystrixObservableCommand(arg1, arg2);
有四個方法可以執行具體命名,分別是HystrixCommand對象的execute()和queue()方法,和 HystrixObservableCommand對象的observe()和toObservable() 方法。
K value=command.execute();
Future<K> fValue=command.queue();
Observable<K> ohValue=command.observe(); //hot observable
Observable<K> ocValue=command.toObservable(); //cold observable
同步調用execute()時,在execute中會調用queue().get(),在queue()中依次調用toObservable().toBlocking().toFuture()。 也就是說,最終每個命令HystrixCommand都由實現O`bservable接口的對象返回,即使是返回單個單個值的命令。
如下圖所示
執行命令
ystrixCommand 和 HystrixObservableCommand 的實現可以定義一個緩存鍵,然后在請求中使用該鍵來去重調用,如果命中緩存健,則直接返回緩存健對應的結果。
以下是涉及 HTTP 請求生命周期的一個示例流程,以及在該請求中進行工作的兩個線程:
請求緩存
請求緩存的好處包括:
這在大型項目中特別有益,因為許多開發者會在同一個項目中實現不同的功能模塊。
例如,所有需要獲取用戶的 Account 對象的代碼路徑可以像這樣請求它:
Account account=new UserGetAccount(accountId).execute();
// 或者
Observable<Account> accountObservable=new UserGetAccount(accountId).observe();
Hystrix RequestCache 將執行底層的 run(), run()方法只會被執行一次, 盡管實例化了不同的實例,執行 HystrixCommand 的兩個線程將接收到相同的數據返回。
由于第一次響應被緩存,所以同一請求在下次調用時,會返相同的結果。
由于請求的結果 在 construct() 或 run() 方法調用之前緩存,Hystrix 可以在啟動新的執行線程前,將結果返回。這樣可以避免產生新的線程,減少線程調用的開銷。
如果 Hystrix 沒有實現請求緩存功能,那么每個命令都需要在 construct 或 run 方法內部自行實現它,緩存結果的查詢要放到新線程創建后,所有無法避免新的線程的創建。
當執行該命令時,Hystrix 會檢查斷路器的開關是否已打開。
如果開關已打開,則 Hystrix 將不會執行該命令,而是快速Fallback(第8步)。
如果開關已打開,則流程跳到 第5步,檢查是否有可用線程或者信號量來運行命令。
如果與該命令關聯的線程池和隊列(或信號量,如果不在線程中運行)已滿,則 Hystrix 將不會執行該命令,但會跳轉 第8步,執行 Fallback。
Hystrix 向依賴的服務觸發具體的業務請求,在下面的兩個方法中編寫具體的業務調用邏輯
如果 run() 或 construct() 方法執行工程中超時了,線程將拋出 TimeoutException(或者如果命令本身沒有在其自己的線程中運行,則由單獨的定時器線程拋出)。 在這種情況下,Hystrix 會跳轉到 第 8 步,調用Fallback 降級來返回響應。
Hystrix 向斷路器報告成功、失敗、拒絕和超時,斷路器維護一組用于計算統計數據的滾動計數器。
它使用這些統計數據來確定電路何時應該將開關打開(“跳閘”),如果開關打開了,它會拒絕任何后續請求,直到恢復期結束。
恢復期結束后,在首次檢查某些運行狀況時,如果新請求通過了,Hystrix將關閉開關。
Hystrix會在以下情況下嘗試降級(Fallback):
編寫降級方法(Fallback)時,在降級方法中中返回一個通用響應,最好不要依賴任何網絡請求,這個請求的響應結果可以來自內存的緩存或者某個靜態邏輯。 如果必須依賴于網絡低啊用,則應該通過另外一個HystrixCommand或ystrixObservableCommand來實現。
Hystrix 將把降級方法返回的結果直接返回給調用者。HystrixCommand.getFallback() 和 HystrixObservableCommand.resumeWithFallback() 都返回同一個 Observable。
如果未實現降級方法,將破除一個異常,通過 Observable 的 onError 通知調用者。
如果 Hystrix 命令執行成功,將以Observable的方式將結果返回給調用者。
返回成功響應
熔斷器的時序說明
斷路器開啟和關閉的具體方式如下:
1.在一個滑動窗口內請求數不低于最小請求數(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
2.并且錯誤百分比超過了閾值(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
3.熔斷器從 CLOSED 狀態轉變為 OPEN 狀態。
4.當它處于開啟狀態時,它將直接拒絕該熔斷的所有請求。
5.經過一定時間(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds())后,允許通過下一個單一請求(這是 HALF-OPEN 狀態)。 如果請求失敗,熔斷器將在睡眠窗口期間返回 OPEN 狀態。如果請求成功,熔斷器轉變為 CLOSED 狀態,然后第1點中的邏輯再次接管。
Hystrix通過實施資源隔離策略來保護系統免受單點故障的影響,增強整個系統的健壯性。 隔離策略可以有效避免某一部分的問題擴散整個系統,影響其他部分的功能。
隔離策略
Hystrix主要提供有兩種資源隔離策略:線程池隔離和信號量隔離。
線程池隔離是Hystrix的默認隔離策略,也是最常用的一種。 它為每個依賴服務創建一個獨立的線程池。這樣,即使一個服務的線程池因請求過多或服務延遲而飽和,也不會影響到其他服務的線程池。 具體工作機制如下:
信號量隔離提供了一種輕量級的隔離方式,不同于線程池隔離,它并不通過創建線程來實現隔離,而是通過限制并發訪問的數量。 信號量隔離通常用于不涉及網絡調用的本地或內存中的操作。具體工作機制如下:
選擇哪種隔離策略取決于具體應用場景:
Hystrix 請求緩存的原理是基于減少對依賴服務的重復調用來提高性能和減輕負載。
這在高并發環境下尤其重要,可以顯著減少延遲和外部系統的壓力。
Hystrix通過在一個請求上下文中緩存請求結果來實現這一目標。當多個相同的請求在同一個請求上下文中發生時,Hystrix會返回緩存的結果而不是重新執行命令。
假設:有一個根據用戶ID獲取用戶信息的服務。 創建一個Hystrix命令來封裝獲取用戶信息的邏輯,并實現請求緩存:
public class GetUserCommand extends HystrixCommand<User> {
private final int userId;
public GetUserCommand(int userId) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.userId=userId;
}
@Override
protected User run() throws Exception {
// 模擬獲取用戶信息的邏輯
return new User(userId, "Name" + userId);
}
@Override
protected String getCacheKey() {
// 使用用戶ID作為緩存鍵
return String.valueOf(userId);
}
public static void main(String[] args) {
HystrixRequestContext context=HystrixRequestContext.initializeContext();
try {
int userId=1;
GetUserCommand command1=new GetUserCommand(userId);
User user1=command1.execute();
System.out.println("User 1: " + user1.getName());
GetUserCommand command2=new GetUserCommand(userId);
User user2=command2.execute();
System.out.println("User 2: " + user2.getName());
// user2 should be retrieved from cache
} finally {
context.shutdown();
}
}
}
class User {
private int id;
private String name;
public User(int id, String name) {
this.id=id;
this.name=name;
}
public String getName() {
return name;
}
}
定義一個GetUserCommand Hystrix命令,它通過用戶ID獲取用戶信息。 每次調用都會檢查是否有緩存的結果可用,如果有,則直接返回緩存的結果,從而避免重復執行相同的業務邏輯。 這個例子中,當我們第二次執行帶有相同用戶ID的命令時,結果會從緩存中獲取,減少了對后端資源的調用。
以下文章參考Sentinel官網
流量防護與容錯是服務流量治理中關鍵的一環,以流量為切入點,通過流量控制、流量平滑、熔斷降級、自適應過載保護等手段來保障服務的穩定性。
一個容錯治理規則 (FaultToleranceRule) 由以下三部分組成:
Sentinel降級規則
Target 定義該規則針對什么樣的請求,如某個 key(可以類比 Sentinel 中的資源名的概念),或者包含某類參數的 HTTP 請求等等。v1alpha1 版本中,Target 先通過 targetResourceName 的方式直接配置資源 key。
Strategy 定義該規則對應的容錯或控制策略。在
Strategy 支持流控、勻速排隊、并發控制、熔斷、系統過載保護等策略。在后續版本中,Strategy 還會支持過載實例摘除/調度、參數流控等能力。
流量控制策略 (RateLimitStrategy),即控制單位時長內的請求量在一定范圍內。
多適用于激增流量下保護服務承載能力在容量之內,避免過多流量將服務打垮。RateLimitStrategy 包含以下要素:
字段名 | 是否必填 | 類型 | 描述 |
metricType | required | string (enum) | 指標類型,取值范圍 RequestAmount |
limitMode | required | string (enum) | 控制模式,單機 Local, 集群總體 Global, 集群按實例數轉單機 GlobalToLocal |
threshold | required | double | 閾值,單位統計時長內最多允許的量 |
statDurationSeconds | required | int32 | 統計時長(秒),如 1 代表 1s |
以下示例定義了一個集群流控的策略,集群總體維度每秒不超過 10個請求。示例 CR YAML:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: RateLimitStrategy
metadata:
name: rate-limit-foo
spec:
metricType: RequestAmount
limitMode: Global
threshold: 10
statDurationSeconds: 1
流量平滑策略 (ThrottlingStrategy),以勻速+排隊等待控制效果,對并發請求進行平滑。
多適用于異步后臺任務(如消息 consumer 批量處理)或對延時不敏感的請求場景。ThrottlingStrategy 包含以下要素:
字段名 | 是否必填 | 類型 | 描述 |
minIntervalOfRequests | required | string (int+timeUnit) | 相鄰兩個并發請求之間的最短時間間隔 |
queueTimeout | required | string (int+timeUnit) | 最大排隊等待時長 |
以下示例定義了一個勻速排隊的策略,相鄰兩個并發請求的時間間隔不小于 20ms,同時排隊平滑的等待時長不超過 500ms。示例 CR YAML:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: ThrottlingStrategy
metadata:
name: throttling-foo
spec:
minIntervalOfRequests: '20ms'
queueTimeout: '500ms'
并發控制 (ConcurrencyLimitStrategy),即控制同時并發調用請求的數目。多適用于慢調用場景下的軟隔離保護,避免調用端線程池被某些慢調用占滿,導致服務不可用甚至鏈路不可用。ConcurrencyLimitStrategy 包含以下要素:
字段名 | 是否必填 | 類型 | 描述 |
maxConcurrency | required | int | 最大并發 |
limitMode | required | string (enum) | 控制模式,單機 Local, 集群總體 Global |
示例 CR YAML:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: ConcurrencyLimitStrategy
metadata:
name: concurrency-limit-foo
spec:
maxConcurrency: 8
limitMode: 'Local'
CircuitBreakerStrategy 對應微服務設計中標準的斷路器模式,單機維度生效。CircuitBreakerStrategy 包含以下要素:
以下示例定義了一個慢調用比例熔斷策略(在 30s 內請求超過 500ms 的比例達到 60% 時,且請求數達到5個,則會自動觸發熔斷,熔斷恢復時長為 5s),示例 CR YAML:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: CircuitBreakerStrategy
metadata:
name: circuit-breaker-slow-foo
spec:
strategy: SlowRequestRatio
triggerRatio: '60%'
statDuration: '30s'
recoveryTimeout: '5s'
minRequestAmount: 5
slowConditions:
maxAllowedRt: '500ms'
實例維度自適應過載保護策略 (AdaptiveOverloadProtectionStrategy),基于某些系統指標與自適應策略結合來對實例維度的穩定性進行整體兜底保護。注意該策略的維度為某個服務的每個 pod 維度,分別生效,不區分具體條件。
AdaptiveOverloadProtectionStrategy 包含以下要素:
示例 CR YAML:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: AdaptiveOverloadProtectionStrategy
metadata:
name: system-overload-foo
spec:
metricType: 'CpuPercentage'
triggerThreshold: '70%'
adaptiveStrategy: 'BBR
針對 HTTP 請求的 fallbackAction 可以參考下面的示例。
一個 YAML 示例:
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: RateLimitStrategy
metadata:
name: rate-limit-foo
spec:
metricType: RequestAmount
limitMode: Global
threshold: 10
statDurationSeconds: 1
---
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: HttpRequestFallbackAction
metadata:
name: fallback-foo
spec:
behavior: ReturnProvidedResponse
behaviorDesc:
# 觸發策略控制后,HTTP 請求返回 429 狀態碼,同時攜帶指定的內容和 header.
responseStatusCode: 429
responseContentBody: "Blocked by Sentinel"
responseAdditionalHeaders:
- key: X-Sentinel-Limit
value: "foo"
---
apiVersion: fault-tolerance.opensergo.io/v1alpha1
kind: FaultToleranceRule
metadata:
name: my-rule
namespace: prod
labels:
app: my-app
spec:
selector:
app: my-app # 規則配置生效的應用名
targets:
- targetResourceName: '/foo'
strategies:
- name: rate-limit-foo
fallbackAction: fallback-foo
這個規則相當于為 key 為 /foo 的請求配置了一個策略(以下假定該資源對應 HTTP 請求),這個策略對應流控策略,全局不超過 10 QPS。
當策略觸發時,被拒絕的請求將根據配置的 fallback 返回 429 狀態碼,返回信息為 Blocked by Sentinel,同時返回 header 中增加一個 header,key 為 X-Sentinel-Limit, value 為 foo。
Resilience4j是一個專為Java 8及更高版本設計的輕量級故障恢復庫,它是由Netflix的Hystrix啟發而來的。
Resilience4j提供了一套豐富的功能,旨在使Java應用更加健壯,包括但不限于熔斷器、限流器、重試機制、服務降級和響應式編程支持。
不同于Hystrix的是,Resilience4j更加輕量級,且專注于使用函數式編程風格,并完全依賴Java 8的函數式接口、Lambda表達式和CompletableFuture等特性。
1. 模塊化設計 Resilience4j采用模塊化的設計,每一個故障恢復策略(如熔斷、重試、限流等)都被設計為一個獨立的模塊。這樣做的好處是用戶可以根據需要選擇性地使用這些模塊,不必引入整個庫。
2. 輕量級 與Hystrix相比,Resilience4j更加輕量級,沒有引入任何額外的線程池管理,而是利用了Java 8的CompletableFuture來支持異步編程。這減少了運行時的資源消耗,同時簡化了線程管理。
3. 函數式編程 Resilience4j充分利用Java 8的函數式編程特性,例如,所有的故障恢復操作(如重試、熔斷)都可以通過Lambda表達式來定義。這使得代碼更加簡潔,并提高了代碼的可讀性和可維護性。
4. 異步和響應式支持 Resilience4j提供了對異步編程的支持,允許開發者在CompletableFuture的幫助下非阻塞地執行操作。此外,它也提供了與響應式編程庫如Reactor和RxJava的集成,使得Resilience4j可以很容易地被整合到響應式流中。
5. 靈活的配置 Resilience4j提供了基于代碼配置和外部配置(如配置文件)的方式。這為應用的配置提供了極大的靈活性,使得在不同環境下調整策略變得更加方便。
6. 監控和度量 Resilience4j內建了監控和度量功能,與Prometheus和Micrometer等監控系統集成。這讓開發者可以實時監控應用的狀態,評估故障恢復策略的效果。
熔斷器的目的是在確定調用鏈某部分失敗的情況下自動停止當前的操作,防止進一步的故障擴散,改善響應時間。
代碼示例:
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import java.time.Duration;
public class CircuitBreakerExample {
public static void main(String[] args) {
CircuitBreakerConfig config=CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.ringBufferSizeInHalfOpenState(2)
.ringBufferSizeInClosedState(2)
.build();
CircuitBreaker circuitBreaker=CircuitBreaker.of("myService", config);
circuitBreaker.onError(Duration.ofMillis(1000), new RuntimeException("test"));
}
}
限流器控制了對資源的訪問頻率,避免因過高的并發訪問導致系統過載。
代碼示例:
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import java.time.Duration;
public class RateLimiterExample {
public static void main(String[] args) {
RateLimiterConfig config=RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(10)
.timeoutDuration(Duration.ofMillis(25))
.build();
RateLimiter rateLimiter=RateLimiter.of("myServiceRateLimiter", config);
// Use rateLimiter
}
}
通過配置重試機制,可以在操作失敗后自動重試預定義次數,可以有效地處理暫時性故障。
代碼示例:
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import java.time.Duration;
public class RetryExample {
public static void main(String[] args) {
RetryConfig retryConfig=RetryConfig.custom()
.maxAttempts(4)
.waitDuration(Duration.ofMillis(100))
.build();
Retry retry=Retry.of("myServiceRetry", retryConfig);
// Use retry
}
}
服務降級處理通過在調用失敗時提供一個默認的、預備的響應或行為,從而確保系統的整體可用性和穩定性。
代碼示例:
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
public class FallbackExample {
public static void main(String[] args) {
CircuitBreaker circuitBreaker=CircuitBreaker.ofDefaults("myService");
String result=circuitBreaker.executeSupplier(() -> {
throw new RuntimeException("Bam!");
}, throwable -> "Recovered value");
System.out.println(result); // 輸出: Recovered value
}
}
用于限制可能長時間運行的操作的執行時間,配合CompletableFuture使用,以實現異步操作的時間控制。 代碼示例:
import io.github.resilience4j.timelimiter.TimeLimiter;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import java.time.Duration;
import java.util.concurrent.*;
public class TimeLimiterExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
TimeLimiterConfig config=TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build();
TimeLimiter timeLimiter=TimeLimiter.of(config);
ExecutorService executor
Service=Executors.newSingleThreadExecutor();
Callable<String> slowCallable=() -> {
Thread.sleep(5000);
return "Hello World from Callable";
};
ScheduledExecutorService scheduler=Executors.newScheduledThreadPool(1);
ScheduledFuture<String> scheduledFuture=scheduler.schedule(slowCallable, 1, TimeUnit.SECONDS);
Callable<String> limitedCall=TimeLimiter.decorateFutureSupplier(timeLimiter, () -> scheduledFuture);
Future<String> future=executorService.submit(limitedCall);
executorService.shutdown();
try {
System.out.println(future.get());
} catch (ExecutionException e) {
System.out.println("The call was timed out.");
}
}
}
Resilience4j通過這些組件提供了一個全面的故障恢復機制,使得Java應用能夠更加健壯地處理外部調用和內部執行時可能遇到的問題。
Spring Cloud Circuit Breaker
IT之家 8 月 6 日消息,蘋果公司今天發布公告,宣布停止對 iOS 17.5.1 系統的簽名,意味著已經升級更高版本的用戶后續無法再自行降級。
蘋果通常會阻止用戶安裝舊版本的 iOS,以鼓勵客戶保持其操作系統的最新狀態,并防止降級到較舊、安全性較低的 iPhone 操作系統版本。
IT之家注:蘋果公司最初于 5 月 21 發布了 iOS 17.5.1 正式版更新,后于 6 月 4 日重新為 iPad 10 用戶發布了 iPadOS 17.5.1 更新。
這一更新只是修復了部分重要錯誤,并解決了極少數情況下,數據庫損壞導致已刪除的照片可能重新在“照片”圖庫中出現的問題。