服務(wù)容錯保護——Spring Cloud Hystrix

(git上的源碼:https://gitee.com/rain7564/spring_microservices_study/tree/master/forth-spring-cloud-hystrix)

幾乎每一個系統(tǒng),特別是分布式系統(tǒng),都會有調(diào)用失敗的情況,最有效的辦法是通過提升代碼的質(zhì)量減少錯誤的次數(shù),但有些情況確實無法避免的,比如遠(yuǎn)程服務(wù)不可用了,從而影響接下來的業(yè)務(wù),所以在應(yīng)用出現(xiàn)錯誤的時候,如何更好的應(yīng)對這些錯誤是整個應(yīng)用中非常重要的部分。然而,在搭建一個彈性系統(tǒng)時,大多數(shù)軟件工程師只在整個架構(gòu)中的一部分或一些關(guān)鍵服務(wù)有實現(xiàn)這些邏輯;他們會把注意力集中在應(yīng)用各層中做數(shù)據(jù)冗余,比如使用集群服務(wù)器、負(fù)載均衡或者將架構(gòu)中的多個部分隔離到多個地方。這些方法雖然考慮到了將系統(tǒng)組件化,但也只考慮了搭建彈性系統(tǒng)需要考慮的一部分。這些實現(xiàn),當(dāng)一個服務(wù)宕掉,可以發(fā)現(xiàn)該服務(wù)并避開,但當(dāng)一個只是運行慢而還沒有宕掉,則很難發(fā)現(xiàn)并避開這個服務(wù),直到該服務(wù)徹底宕掉。因為:

  1. 一般情況下,只要服務(wù)并沒有完全不可用,其它服務(wù)還是會繼續(xù)消費該服務(wù)。
  2. 遠(yuǎn)程調(diào)用一般都是同步的,并且很難中斷。開發(fā)者一般都是調(diào)用一個遠(yuǎn)程調(diào)用然后等待遠(yuǎn)程服務(wù)的返回,并沒有設(shè)置timeout來避免調(diào)用因長時間沒有返回被掛起。
  3. 服務(wù)處理能力弱化,有很大概率會出現(xiàn)雪崩效應(yīng)。假設(shè)有ServiceA、ServiceB、ServiceC,ServiceA調(diào)用ServiceB,ServiceB調(diào)用ServiceC,當(dāng)ServiceC處理能力降低,而ServiceB照常一直發(fā)送請求給ServiceC,但一直得不到返回,所以被掛起的遠(yuǎn)程調(diào)用越來越多,直到ServiceB所在容器的資源被大量消耗,ServiceB處理能力也變?nèi)趿耍琒erviceB處理能力變?nèi)跻矔绊懙絊erviceA以及其他消費ServiceB的服務(wù),直到這些服務(wù)也因為資源被消耗變得不可用,最終整個系統(tǒng)癱瘓。

綜上,性能差的遠(yuǎn)程服務(wù)帶來的潛在問題是它們不僅難以檢測,還能引發(fā)連鎖反應(yīng)。在整個應(yīng)用程序生態(tài)系統(tǒng)中,若沒有保障措施,一個單一的性能差的服務(wù)可能會迅速影響到多個應(yīng)用。基于云計算或微服務(wù)的應(yīng)用程序特別容易受到這種問題的影響,因為這些應(yīng)用程序是由大量的細(xì)粒度的,使用不同的服務(wù)來完成用戶的服務(wù)。

什么是客戶端彈性機制(client-side resiliency patterns)

客戶端彈性機制的作用主要是,當(dāng)遠(yuǎn)程資源(遠(yuǎn)程調(diào)用或者數(shù)據(jù)庫訪問)宕掉或處理能力弱時,能降低消費方受到的影響,避免也因此宕掉。這些機制的目標(biāo)是讓消費方能“快速結(jié)束調(diào)用(fail fast)”,不會大量占用可用資源,如數(shù)據(jù)庫連接、線程池等,并避免受到上游服務(wù)的影響。客戶端彈性機制包括:

  • Client-side load balancing:客戶端負(fù)載均衡
  • Circuit breaker:熔斷器
  • FallBack:回退機制
  • Bulkhead:艙壁機制
    下圖展示了這些模式在微服務(wù)中起的作用:


    客戶端彈性機制

這些機制都是在服務(wù)客戶端實現(xiàn)的,邏輯上,這些實現(xiàn)是處于資源消費方與資源之間。

Client-side load balancing——客戶端負(fù)載均衡

關(guān)于客戶端負(fù)載均衡,前兩章介紹服務(wù)發(fā)現(xiàn)(Netflix Eureka)時已經(jīng)介紹過。
客戶端負(fù)載均衡的機制是:客戶端從服務(wù)發(fā)現(xiàn)代理(如Netflix Eureka)獲取其他服務(wù)實例的IP等位置信息,然后緩存起來并定時刷新。當(dāng)服務(wù)消費端需要調(diào)用其他服務(wù),客戶端負(fù)載均衡器會從維護的可用服務(wù)實例池返回目標(biāo)服務(wù)的一個可用實例的具體位置,從而消費端能夠準(zhǔn)確定位并訪問。

因為客戶端負(fù)載均衡器處在服務(wù)客戶端和服務(wù)消費端之間(客戶端可以理解為服務(wù);消費端則為服務(wù)的某個方法,該方法中調(diào)用了遠(yuǎn)程接口,即該方法消費了遠(yuǎn)程資源),所以負(fù)載均衡器能檢測到哪個遠(yuǎn)程服務(wù)實例拋異常或者處理能力弱,若檢測到,則會將該可能已經(jīng)出問題的服務(wù)實例從可用服務(wù)實例池中移除,避免再次訪問。

Netflix的Ribbon庫就實現(xiàn)了這樣的機制,能開箱即用而不用額外的配置。因為在服務(wù)發(fā)現(xiàn)已經(jīng)介紹過Netflix Ribbon的基本使用方法,這里就不過多贅述。

Circuit breaker——熔斷器

Circuit breaker機制的作用與電氣領(lǐng)域中的熔斷器原理類似。在電氣系統(tǒng)中,熔斷器能檢測線路中的電流是否過載,若已過載熔斷器則能夠切斷線路連接,從而保證熔斷器下游的線路不會因電流過高被損壞甚至燒毀。

而這里所說的Circuit breaker機制的作用是,當(dāng)訪問遠(yuǎn)程服務(wù)時,熔斷器能監(jiān)控這些訪問。如果這些訪問在規(guī)定的timeout內(nèi)沒有得到響應(yīng),熔斷器會強制打斷此次訪問。另外,熔斷器還會監(jiān)控所有訪問某一資源的遠(yuǎn)程訪問(這里的"所有",可以是相同服務(wù)的不同實例,也可以是不同服務(wù)的不同實例,只要調(diào)用了同一個接口;重點在訪問的是同一資源),如果在規(guī)定的時間內(nèi)失敗次數(shù)超過一個閾值,熔斷器就會起作用,即會阻止該資源的消費者繼續(xù)消費該可能已經(jīng)不可用的資源,轉(zhuǎn)而去消費其它可用資源。

當(dāng)然熔斷器不可能這么簡單,這里只簡單介紹它的大致原理,詳細(xì)的原理,下文會給出。

Fallback processing——回退處理

回退機制的作用是,當(dāng)一個遠(yuǎn)程訪問失敗后,服務(wù)消費端會執(zhí)行另一段代碼,這段代碼可以返回一個已定義好的結(jié)果,當(dāng)然也可以做其它邏輯,而不是硬生生地拋異常。比如現(xiàn)在調(diào)用遠(yuǎn)程接口獲取用戶數(shù)據(jù),而且失敗了,那么此時可以選擇返回一個用戶對象,其中的數(shù)據(jù)為空,而不是拋出異常,畢竟拋異常的用戶體驗很不好。

Bulkheads——艙壁機制

艙壁機制的作用與船只的艙壁作用類似。船的艙壁能將船體內(nèi)部分隔成若干艙室,當(dāng)發(fā)生海損事故船只進水時,只有那些外板受損的艙室進水,而其它的艙室由于有了艙壁的阻擋而不受影響,船只整體浮力損失減小,所以不易沉沒,提高船只的生存力。

同樣,可以將同一遠(yuǎn)程調(diào)用分到同一線程池中,這樣能降低因為一個響應(yīng)緩慢的調(diào)用不斷地消耗可用資源而拖垮整個應(yīng)用的風(fēng)險。這些線程成就好比船只的一個個艙室,當(dāng)一個響應(yīng)緩慢的調(diào)用所屬的線程池滿了,那么接下來的調(diào)用只能進入一個隊列排隊等待,而不會去占用其它可用資源,也就不會影響到其它模塊的正常運行。

上面已經(jīng)抽象說明了幾種客戶端彈性機制的作用,接下來用一個簡單的場景來更深入理解這幾種機制,如熔斷器。該場景中的應(yīng)用和服務(wù)之間的聯(lián)系如下圖:
image.png

上圖中,應(yīng)用A和B直接與服務(wù)A通信;服務(wù)A從數(shù)據(jù)庫查詢獲取數(shù)據(jù)、調(diào)用服務(wù)B的接口;服務(wù)B從另一個數(shù)據(jù)庫查詢獲取數(shù)據(jù)、調(diào)用第三方服務(wù)C提供的接口,且服務(wù)C極其依賴于一個內(nèi)網(wǎng)存儲設(shè)備(NAS)來將數(shù)據(jù)寫入文件共享系統(tǒng);另外,應(yīng)用C調(diào)用服務(wù)C的接口。
在周末,一個網(wǎng)絡(luò)管理員對NAS的配置做了在他看來無關(guān)緊要的變更,變更后,服務(wù)C看起來還是運行得很好;但是在星期一早上,個別磁盤子系統(tǒng)的出現(xiàn)寫入極其緩慢的情況。
開發(fā)服務(wù)B的開發(fā)者沒有事先預(yù)想到調(diào)用服務(wù)C的接口會出問題,比如服務(wù)C處理速度慢長時間未響應(yīng),所以將操作數(shù)據(jù)源B與調(diào)用服務(wù)C接口的邏輯都編寫在同一個事務(wù)中。當(dāng)服務(wù)C執(zhí)行效率變低,服務(wù)B的連接池中的數(shù)據(jù)庫連接數(shù)量爆增,因為服務(wù)C接口的訪問并沒有得到及時響應(yīng),所以這些數(shù)據(jù)庫連接必須一直保持連接狀態(tài)。
最后,因為服務(wù)B的可用資源被迅速消耗,沒有足夠的資源處理來自服務(wù)A的訪問,所以導(dǎo)致服務(wù)A也會消耗服務(wù)A所處容器的可用資源。最終,應(yīng)用A、B、C都會被迫停止響應(yīng)。

在上面的場景中,在訪問分布式資源時都可以加入熔斷器的實現(xiàn)。比如,當(dāng)服務(wù)C處理效率急劇下降時,如果在訪問服務(wù)C的時候有熔斷器的實現(xiàn),那么這些訪問在長時間(相對正常響應(yīng)時間)未得到響應(yīng)時,熔斷器會將其打斷,而不會一直占用系統(tǒng)資源。如果服務(wù)B暴露了許多端點(endpoints),那么只有那一個或多個需要與服務(wù)C通信的端點不可用,而剩余其他端點則能繼續(xù)響應(yīng)用戶的請求。
熔斷器的角色就好比是處于應(yīng)用和遠(yuǎn)程服務(wù)的中間人。在上面提到的場景中,熔斷器的實現(xiàn)能保護應(yīng)用A、B、C不會因為可用資源不足而變得完全不可用。

再來看一個場景,如下圖:
image.png

上圖中,服務(wù)B并不是直接訪問服務(wù)C,服務(wù)B和服務(wù)C之前實現(xiàn)了熔斷器,服務(wù)B將真正的接口調(diào)用委托給熔斷器,熔斷器將其包裝在一個線程中。所以服務(wù)B不再一直等待訪問得到響應(yīng),而是熔斷器監(jiān)控這個線程并且在線程運行時間過長時可以終止此次訪問。

上圖的三個場景,第一個場景是理想情況,熔斷器會維護一個定時器,遠(yuǎn)程服務(wù)調(diào)用能在規(guī)定時間內(nèi)得到響應(yīng),服務(wù)B能繼續(xù)正常運作。
第二個場景,服務(wù)C性能變低,服務(wù)B對服務(wù)C的訪問無法在定時器結(jié)束之前得到響應(yīng),熔斷器會將此次訪問切斷。因此服務(wù)B會得到一個錯誤返回,不再繼續(xù)等待服務(wù)C的響應(yīng),也就會釋放之前占用的資源。另外,當(dāng)熔斷器監(jiān)控到某一個遠(yuǎn)程調(diào)用(如端點P)因timeout而得到錯誤返回,那么熔斷器會開始對所有訪問這一服務(wù)實例的指定端點(端點P)的失敗次數(shù)進行監(jiān)控,當(dāng)在規(guī)定的時間內(nèi),失敗次數(shù)達到一個指定的閾值,熔斷器會將其標(biāo)記為不可用。
第三個場景,訪問某一服務(wù)C實例的端點P在規(guī)定的時間內(nèi)的失敗次數(shù)超過閾值,該端點被標(biāo)記為不可用。所以當(dāng)服務(wù)B在此訪問了該端點,會立即得到一個錯誤結(jié)果,而沒有發(fā)出訪問端點P的請求,又因為有回退機制,那么會返回一個預(yù)定的結(jié)果。最后,熔斷器在規(guī)定的時間后,默認(rèn)5s,會放行部分請求進行嘗試,根據(jù)結(jié)果確認(rèn)服務(wù)C是否已經(jīng)恢復(fù)正常。

綜上,熔斷器機制讓應(yīng)用在遠(yuǎn)程調(diào)用上能具備如下能力:

  • Fail fast:當(dāng)一個遠(yuǎn)程服務(wù)退化,處理能力下降,應(yīng)用訪問該服務(wù)時能讓遠(yuǎn)程調(diào)用迅速失敗返回,這樣能有效防止等待遠(yuǎn)程調(diào)用的響應(yīng)而一直占有資源,也就提升了應(yīng)用的魯棒性。
  • Fail gracefully:因為有了定時器和迅速失敗,熔斷器提供了讓應(yīng)用擁有優(yōu)雅返回錯誤結(jié)果的能力。即在迅速失敗后,執(zhí)行了預(yù)先定義好的邏輯,該邏輯能產(chǎn)生一個友好的結(jié)果并返回,而不是返回一個硬生生的錯誤結(jié)果。
  • Recover seamlessly:熔斷器作為一個“中間人”,它可以在不需要人為干預(yù)的情況下,定期檢查遠(yuǎn)程服務(wù)/資源是否已恢復(fù)正常,能夠再次被訪問。

一個云應(yīng)用,特別是擁有幾百上千微服務(wù)的大型云應(yīng)用,這種不需要人為干預(yù)的服務(wù)恢復(fù),是一個極為關(guān)鍵的技術(shù),因為它能明顯減少服務(wù)重啟的次數(shù),由于不需要人為的操作,也能降低運維員工或系統(tǒng)工程師的錯誤操作導(dǎo)致更大的故障的風(fēng)險。

Spring Cloud Hystrix

熔斷器、回退機制、艙壁機制的實現(xiàn),需要有非常豐富的多線程開發(fā)經(jīng)驗。不過Netflix的Hystrix庫已經(jīng)幫我們實現(xiàn),而且Spring Cloud已將Hystrix集成到Spring Cloud Hystrix中,所以可以使用它來讓我們的應(yīng)用變得更加健壯。

接下來,我們會學(xué)習(xí)如何:

  • 在pom文件引入Spring Cloud Hystrix的啟動依賴
  • 使用Hystrix提供的注解將遠(yuǎn)程調(diào)用通過熔斷器包裝起來
  • 自定義熔斷器來應(yīng)對不同的需求
  • 實現(xiàn)遠(yuǎn)程調(diào)用失敗后回退策略
  • 自定義線程池來實現(xiàn)艙壁機制

license服務(wù)引入Spring Cloud Hystrix

在上一節(jié)的license服務(wù)的pom文件中加入如下依賴:

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

引入該依賴后,還無法讓你的應(yīng)用具備熔斷器能力,必須在服務(wù)啟動類再加一個注解——@EnableCircuitBreaker。該注解告訴Spring Cloud,我們將在服務(wù)中使用Hystrix實現(xiàn)熔斷器。

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

如果忘記在啟動類加上該注解,那么應(yīng)用中的所有Hystrix熔斷器都將無法起作用,而且在服務(wù)啟動時,控制臺會出現(xiàn)警告或錯誤信息。所有第一件事就是加上該注解。

使用Hystrix實現(xiàn)熔斷器

下面,我們會使用Hystrix實現(xiàn)兩種不同類型的熔斷器。第一種,在服務(wù)license、organization中的所有數(shù)據(jù)庫操作,都使用Hystrix熔斷器包裝;第二種是,包裝license服務(wù)對organization服務(wù)的調(diào)用。但是這兩種情況,Hystrix熔斷器的實現(xiàn)是一樣的。下圖展示了這兩種類型:


image.png
Hystrix熔斷器的簡單使用

首先,license服務(wù)從數(shù)據(jù)庫獲取數(shù)據(jù)的訪問,使用同步的Hystrix熔斷器包裝。這些同步調(diào)用,在SQL語句執(zhí)行完后會將結(jié)果返回,或者在定時器超時之后強制返回。

Spring Cloud Hystrix使用@HystrixCommand注解標(biāo)記一個方法,然后這些方法會被Hystrix熔斷器管理。當(dāng)Spring framework看到這個注解,它會動態(tài)生成一個代理將方法包裝起來,并添加到指定的線程池中,由線程池統(tǒng)一管理。

在LicenseService添加getLicensesByOrg()方法,并使用注解@HystrixCommand:

@HystrixCommand
public List<License> getLicensesByOrg(String organizationId){
    return licenseRepository.findByOrganizationId(organizationId);
}

這看似不多的代碼,較之前可能會編寫的邏輯,只是多了一個@HystrixCommand注解,但是在該注解下,卻有很多邏輯包含在里邊。每一次getLicenseByOrg方法調(diào)用,都會被包裝在一個Hystrix熔斷器中。若一次調(diào)用花費的時間超過1000毫秒(默認(rèn)),則該調(diào)用會被打斷。

在數(shù)據(jù)庫能正常工作的情況下,上面的示例的運行結(jié)果會很尷尬,根本看不出熔斷器的效果。所以,在驗證熔斷器效果之前,將代碼略做修改,創(chuàng)建讓熔斷器“生效”的環(huán)境。如下:

@HystrixCommand
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

private void randomlyRunLong(){
    Random rand = new Random();
    int randomNum = rand.nextInt((3 - 1) + 1) + 1;
    if (randomNum==3) sleep();
}

private void sleep(){
    try {
        Thread.sleep(11000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

然后在LicenseController加入一個端點:

@RequestMapping(value="/",method = RequestMethod.GET)
public List<License> getLicenses(@PathVariable("organizationId") String organizationId) {
    return licenseService.getLicensesByOrg(organizationId);
}

最后,啟動eureka服務(wù),config-server服務(wù),license服務(wù),organization服務(wù)無所謂,暫時用不到;然后用postman訪問:http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/。當(dāng)你訪問多次時,中間可能會出現(xiàn)類似如下圖的結(jié)果:

image.png

出現(xiàn)這種結(jié)果是因為調(diào)用getLicensesByOrg方法花費的時間過長,所以Hystrix熔斷器強制切斷此次調(diào)用,并得到一個錯誤返回,觀察上圖,可以看到,拋的異常是:com.netflix.hystrix.exception.HystrixRuntimeException,所以該異常是Hystrix熔斷器在timeout后拋出的,而不是getLicensesByOrg中的邏輯執(zhí)行錯誤拋出的。

使用默認(rèn)的Hystrix熔斷器包裝服務(wù)間的調(diào)用的使用方法與上面展示一樣,只要在遠(yuǎn)程調(diào)用的業(yè)務(wù)方法上加上注解@HystrixCommand即可。在LicenseService添加方法getOrganization,如下:

@HystrixCommand
private Organization getOrganization(String organizationId) {
    return organizationRestClient.getOrganization(organizationId);
}

然而,雖然注解@HystrixCommand很容易使用,但在使用時還需要特別注意,因為上面所列舉的兩段代碼,@HystrixCommand注解都是使用的默認(rèn)配置。若都使用默認(rèn)配置,會有很多弊端。比如,所有遠(yuǎn)程調(diào)用都會由同一個線程池,這樣會引起許多問題。在后文講解艙壁機制時會說明如何讓不同的遠(yuǎn)程調(diào)用由不同的線程池進行管理。所以在開發(fā)過程中,一般會根據(jù)實際情況使用自定義的配置。

自定義熔斷器的超時時間

修改getLicensesByOrg方法的@HystrixCommand注解,如下:

@HystrixCommand(
    commandProperties= {
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="12000")
    }
)
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

Hystrix允許我們通過注解@HystrixCommand的屬性commandProperties來自定義熔斷器,commandProperties可以包含一個@HystrixProperty數(shù)組。上面的代碼中,配置execution.isolation.thread.timeoutInMilliseconds屬性來自定義Hystrix熔斷器的超時時間為12s。重啟license服務(wù)后,如果再次多次訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,都會得到正確的查詢結(jié)果。因為熔斷器的超時時間為12s,而程序的睡眠時間為11s,所以正常情況下,不會因定時器超時,數(shù)據(jù)庫訪問被強制中斷。

很明顯,上面把超時時間設(shè)置成12s是為了驗證的需要。而在分布式環(huán)境中,一般不需要去該這個配置,因為微服務(wù)間的調(diào)用都是很快的。如果出現(xiàn)需要增大該超時時間才能讓應(yīng)用正常運行,這就代表該遠(yuǎn)程調(diào)用存在潛在的性能問題,此時我們要把持住,不要受增大超時時間的誘惑,轉(zhuǎn)而去進行性能優(yōu)化。如果實在是無法進行優(yōu)化,比如訪問的是第三方提供的接口,那就只能增大超時時間了,但是增大多少也要多加注意。

回退處理

熔斷器機制設(shè)計得最巧妙的地方就是,熔斷器作為遠(yuǎn)程資源消費者與遠(yuǎn)程資源本身的一個中間人,這樣的設(shè)計有利于開發(fā)者能夠中斷遠(yuǎn)程調(diào)用甚至可以在中斷后執(zhí)行另一種預(yù)定義的邏輯并返回一個友好的結(jié)果。

在Hystrix中,回退機制與熔斷器可以結(jié)合在一起使用,即在熔斷器中斷遠(yuǎn)程訪問后,可以選擇進行回退處理而不是得到一個錯誤返回。使用Hystrix,這種強制中斷后進行的回退策略的實現(xiàn)也是極為簡單的。下面我們來實現(xiàn)一個簡單的回退處理策略,即license服務(wù)訪問數(shù)據(jù)庫失敗后,返回一個不攜帶license可用信息的License對象。如下:

@HystrixCommand(fallbackMethod = "buildFallbackLicenseList")
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

private List<License> buildFallbackLicenseList(String organizationId){
    List<License> fallbackList = new ArrayList<>();
    License license = new License()
            .withId("0000000-00-00000")
            .withOrganizationId( organizationId )
            .withProductName(
                    "Sorry no licensing information currently available");
    fallbackList.add(license);
    return fallbackList;
}

上面的代碼中,首先將之前@HystrixCommand配置的超時時間去掉,再配置一個新的屬性fallbackMethod,該屬性代表在熔斷器中斷遠(yuǎn)程訪問后進行回退處理,可以看到fallbackMethod的值為"buildFallbackLicenseList",所以處理的邏輯(方法)是:與getLicensesByOrg方法處在同一個類中的方法buildFallbackLicenseList()。

實際上,要使用Hystrix實現(xiàn)一個回退策略,需要兩個步驟。第一個是在注解@HystrixCommand中加入一個屬性fallbackMethod,該屬性的值是一個方法名,代表熔斷器在中斷遠(yuǎn)程訪問后會調(diào)用的方法。
第二個是定義一個方法,該方法的簽名與fallbackMethod屬性的值相同,并且該方法必須與添加了@HystrixCommand注解的方法處在相同的類中。另外,該方法的參數(shù)列表必須與添加了@HystrixCommand注解的方法一致,在執(zhí)行回退處理時,熔斷器會傳入相應(yīng)的值。

上面的代碼中,回退處理的邏輯是只返回一個“空”的License對象,這是最簡單的做法。當(dāng)然,在生產(chǎn)環(huán)境中,可以將邏輯改成從另一個數(shù)據(jù)源獲取數(shù)據(jù)。如果是這樣,那就要注意了,從另一個數(shù)據(jù)源獲取數(shù)據(jù),也屬于遠(yuǎn)程調(diào)用,所以也要加上@HystrixCommand注解來“保護”該二級方案。

在生產(chǎn)環(huán)境中,回退處理的實現(xiàn)是一件值得花費時間和精力去做好的事情。比如,用戶獲取最新數(shù)據(jù),但查詢超時了,此時,即使返回的是舊數(shù)據(jù)也比返回一個錯誤給人的體驗更好。

現(xiàn)在,我們已經(jīng)實現(xiàn)一個簡單的回退處理策略,重啟license服務(wù),然后多次訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,直到出現(xiàn)類似如下圖的結(jié)果:

image.png

出現(xiàn)這個結(jié)果,證明熔斷器在遠(yuǎn)程調(diào)用超時后中斷了調(diào)用并進行了預(yù)定義的回退處理。回退處理就介紹到這里,接下來介紹艙壁機制。

艙壁機制的實現(xiàn)

在一個基于微服務(wù)的應(yīng)用程序中,您通常需要調(diào)用多個微服務(wù)完成一個特定任務(wù)。不使用艙壁模式,這些調(diào)用默認(rèn)是使用相同的線程來執(zhí)行調(diào)用的,這些線程Java容器為處理所有請求預(yù)留的。在高服務(wù)器請求的情況下,一個性能較低的服務(wù)會“霸占”java容器中絕大多數(shù)線程,而其它性能正常的服務(wù)的請求則需要等待線程資源的釋放。最后,整個java容器會崩潰。艙壁模式能將遠(yuǎn)程調(diào)用隔離在各個遠(yuǎn)程調(diào)用自己的線程池中,因此單個性能出問題的服務(wù)能得到控制,java容器也不會崩潰。

Hystrix將遠(yuǎn)程服務(wù)的請求托管在一個線程池中。即默認(rèn)情況下,所有Hystrix命令(@HystrixCommand)共享同一個線程池來處理這些請求。該線程池中持有10個線程來處理各種遠(yuǎn)程服務(wù)請求,可以是REST服務(wù)調(diào)用、數(shù)據(jù)庫訪問等。如下圖所示:


image.png

@HystrixCommand的默認(rèn)配置適用于只有少量遠(yuǎn)程調(diào)用的應(yīng)用。幸運的是,Hystrix提供了簡單易用的方法實現(xiàn)艙壁來隔離不同的遠(yuǎn)程資源調(diào)用。下圖說明了Hystrix將不同的遠(yuǎn)程調(diào)用隔離在不同的“艙室”(線程池)中:


image.png

實現(xiàn)這種隔離的線程池,需要使用到@HystrixCommand注解提供的其他屬性。接下來,我們會:

  1. 為方法getLicensesByOrg()設(shè)置一個隔離的線程池
  2. 設(shè)置該線程池的線程數(shù)
  3. 設(shè)置隊列的容量,該隊列的作用是當(dāng)線程池中的線程都處于工作狀態(tài),接下來的請求會進入該隊列。
    下面的代碼演示了如何自定義艙壁:
@HystrixCommand(
        fallbackMethod = "buildFallbackLicenseList",
        threadPoolKey = "licenseByOrgThreadPool",
        threadPoolProperties = {
            @HystrixProperty(name = "coreSize",value="30"),
            @HystrixProperty(name="maxQueueSize", value="10")
        }
)
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

上面代碼中涉及到幾個新的@HystrixCommand暴露的屬性。第一個是threadPoolKey,這對于Hystrix來說是新建一個線程池的信號,threadPoolKey的值則是線程池的標(biāo)識。如果只是配置了threadPoolKey,那么Hystrix會使用默認(rèn)配置來初始化該線程池。

而若要自定義新建的線程池,則需要使用另一個屬性:threadPoolProperties。該屬性接收一個@HystrixProperty數(shù)組,這些HystrixProperty就是用來配置新建的線程池。比如,可以使用coreSize來配置線程池的容量。
當(dāng)然也可以設(shè)置一個隊列,來應(yīng)對當(dāng)線程池繁忙的情況。通過maxQueueSize來設(shè)置該隊列的容量。當(dāng)請求的數(shù)量超過隊列的容量,其它的請求會迅速失敗返回,直到隊列又有空閑的“位置”。

對于屬性maxQueueSize,有亮點需要注意。第一,如果value值設(shè)置為-1,Hystrix會使用SynchronousQueue來實現(xiàn)該隊列。同步隊列意味著,當(dāng)線程池繁忙時,就不再接收其它請求,直接迅速失敗返回,可以粗略理解為該隊列不存在。當(dāng)設(shè)置一個大于1的值時,Hystrix會創(chuàng)建一個LinkedBlockingQueue,這樣會讓后到的請求排隊等候線程池的線程完成請求處理。
第二,Hystrix允許我們使用SizeRejectionThreshold屬性來動態(tài)變更隊列的容量,但該屬性只有在maxQueueSize的value值大于0的時候才能生效。而maxQueueSize屬性的值只能在線程池初始化時設(shè)置,所以當(dāng)maxQueueSize為-1時,將無法再變更隊列的容量,因為隊列是同步隊列。

最后,我們應(yīng)該如何設(shè)置一個合適的線程池容量呢?Netflix建議:
每秒處理請求的峰值 × 99%平均響應(yīng)時間 + 緩沖線程數(shù)

然而,在服務(wù)正式部署之前,我們是無法知道服務(wù)的性能為幾何。這里有一個指標(biāo)可以作為參考,當(dāng)目標(biāo)遠(yuǎn)程資源正常的情況下,調(diào)用還會出現(xiàn)超時,那么線程池的容量就需要調(diào)整了。

更多@HystrixCommand的配置可參考https://github.com/Netflix/Hystrix/wiki/Configuration

深入了解Hystrix;微調(diào)Hystrix

到目前為止,我們只是了解了如何使用Hystrix配置最簡單的熔斷器、艙壁實現(xiàn)。接下來我們會深入了解Hystrix并學(xué)習(xí)如何真正自定義一個Hystrix熔斷器。在此之前,需要牢記的是,Hystrix做的遠(yuǎn)比中斷一個超時調(diào)用多。Hystrix還會監(jiān)控調(diào)用失敗的次數(shù),如果失敗率超過一個閾值,Hystrix會自動讓以后的遠(yuǎn)程調(diào)用請求在到達遠(yuǎn)程資源之前迅速失敗返回;也就是說,只要是該請求訪問的是不可用資源,Hystrix會直接迅速失敗返回,連發(fā)起遠(yuǎn)程調(diào)用的機會都不給你,誰叫你托管在Hystrix呢。

Hystrix這樣實現(xiàn)由兩個原因。第一,如果一個遠(yuǎn)程資源存在性能問題,迅速失敗返回能避免遠(yuǎn)程調(diào)用在超時后熔斷器再去中斷它。這樣能有效降低發(fā)起調(diào)用的應(yīng)用程序或服務(wù)耗盡可用資源而崩潰。
第二,這樣有利于遠(yuǎn)程服務(wù)性能的恢復(fù)。設(shè)想一下,現(xiàn)在一個遠(yuǎn)程資源服務(wù)的請求量突然劇增,出現(xiàn)短暫性的性能問題,導(dǎo)致發(fā)起遠(yuǎn)程調(diào)用的服務(wù)發(fā)起的大量請求都超時,失敗請求的比例超過閾值,這時熔斷器打開,接下來的請求都會迅速失敗返回,即不再訪問遠(yuǎn)程服務(wù)而是直接失敗返回,這時遠(yuǎn)程服務(wù)就有足夠的“喘息”時間來處理劇增的請求,當(dāng)處理完后,遠(yuǎn)程服務(wù)性能恢復(fù),Hystrix熔斷器的無縫恢復(fù)機制感知到后,會將熔斷器關(guān)閉(無縫恢復(fù)如何實現(xiàn)的下文會說明)。

在學(xué)習(xí)如何配置熔斷器之前,必須了解Hystrix熔斷器是在何時開啟、何時關(guān)閉的。下圖展示了當(dāng)Hystrix熔斷器監(jiān)聽到第一個失敗調(diào)用后開啟、關(guān)閉的判定流程:


image.png

當(dāng)Hystrix命令遇到一個遠(yuǎn)程資源調(diào)用失敗,它會開啟一個10s的計時器,計時器被用來檢測該服務(wù)調(diào)用失敗的頻率。該時間窗(計時器)是可以配置的,默認(rèn)是10s。在時間窗開啟后結(jié)束前,會統(tǒng)計接下來的每一個失敗調(diào)用。如果失敗調(diào)用的頻數(shù)小于一個預(yù)設(shè)的值,Hystrix不會采取進一步措施,認(rèn)為之前的失敗調(diào)用屬于正常可控的。比如,設(shè)置這個預(yù)設(shè)值為20,在10s內(nèi),Hystrix統(tǒng)計到的失敗調(diào)用數(shù)為15,那么Hystrix會放行接下來的遠(yuǎn)程調(diào)用請求。

在時間窗結(jié)束前,失敗調(diào)用頻數(shù)達到預(yù)設(shè)的值,Hystrix會開始統(tǒng)計遠(yuǎn)程調(diào)用整體失敗率。如果失敗率超過閾值,該閾值默認(rèn)為50%,Hystrix會觸發(fā)熔斷器,讓接下來的請求都迅速失敗返回,防止繼續(xù)訪問可能已出現(xiàn)故障的遠(yuǎn)程資源。如果,10s時間窗結(jié)束,總體失敗率未達到或超過閾值,那么Hystrix會重置只統(tǒng)計的數(shù)據(jù)。直到監(jiān)聽到又一個失敗請求,Hystrix會再次開啟一個時間窗。稍后還會講解Hystrix通過放行部分遠(yuǎn)程調(diào)用請求嘗試,確認(rèn)遠(yuǎn)程資源故障是否已修復(fù),實現(xiàn)“無縫恢復(fù)”。

熔斷器有打開,肯定就會有關(guān)閉,不然只要熔斷器一打開,那么對應(yīng)的那個遠(yuǎn)程資源豈不是直接被判死刑,永遠(yuǎn)拉入黑名單,就算遠(yuǎn)程資源已恢復(fù)也不會放行,人家跟你什么仇什么怨。很明顯,這肯定是不合理的。在說明Hystrix何時關(guān)閉熔斷器時,會涉及到一個叫熔斷器“半開”狀態(tài)的概念。

何為“半開”狀態(tài)?熔斷器被觸發(fā)后,Hystrix會開啟另一個默認(rèn)為5s長度也可設(shè)置的休眠時間窗,當(dāng)時間窗結(jié)束后,熔斷器會從開啟狀態(tài)轉(zhuǎn)換為半開狀態(tài)。熔斷器半開狀態(tài)下,Hystrix會允許請求嘗試訪問,若此時訪問繼續(xù)失敗,熔斷器有進入開啟狀態(tài),并繼續(xù)等待下一個休眠時間窗結(jié)束后,會再進入半開狀態(tài),一直循環(huán)重復(fù),知道請求成功;若請求成功,熔斷器會被重置為關(guān)閉狀態(tài)。

Hystrix就是這樣來控制熔斷器的開啟與關(guān)閉的。

下面,開始自定義熔斷器的配置,代碼如下:

@HystrixCommand(
    fallbackMethod = "buildFallbackLicenseList",
    threadPoolKey = "licenseByOrgThreadPool",
    threadPoolProperties = {
        @HystrixProperty(name = "coreSize",value="30"),
        @HystrixProperty(name="maxQueueSize", value="10")
    },
    commandProperties = {
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value = "10"),
        @HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"),
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),
        @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds",value="15000"),
        @HystrixProperty(name="metrics.rollingStats.numBuckets",value="5")
    }
)
public List<License> getLicensesByOrg(String organizationId){
    randomlyRunLong();
    return licenseRepository.findByOrganizationId(organizationId);
}

可以看到,Hystrix支持通過配置commandProperties屬性來自定義熔斷器,該屬性接收一個HystrixProperty對象數(shù)組,數(shù)組的元素可通過配置注解@HystrixProperty實現(xiàn)。上面的代碼,總共配置了5個@HystrixProperty,作用分別如下:

  • circuitBreaker.requestVolumeTheshold:控制10s(默認(rèn))時間窗內(nèi)失敗頻數(shù)達到的閾值,若達到或超過,會進行總體失敗率統(tǒng)計;
  • circuitBreaker.errorThresholdPercentage:總體失敗率閾值。在失敗調(diào)用頻數(shù)超過circuitBreaker.requestVolumeTheshold設(shè)置的值后,若統(tǒng)計的總體失敗率達到或超過該閾值,熔斷器會開啟;
  • circuitBreaker.sleepWindowInMilliseconds:休眠時間窗長度。該屬性控制熔斷器打開后,會開啟多長的休眠時間窗。休眠時間窗結(jié)束后,Hystrix會允許部分遠(yuǎn)程訪問嘗試。
  • metrics.rollingStats.timeInMilliseconds:該屬性是Hystrix用來控制監(jiān)聽到第一個失敗調(diào)用后打開的時間窗長度,默認(rèn)是10000ms,即10s。
  • metrics.rollingStats.numBuckets:設(shè)置了將時間窗劃分成桶的數(shù)量。該時間窗指上一個屬性設(shè)置的時間窗,而不是休眠時間窗。Hystrix在該時間窗內(nèi),會收集各個桶的度量指標(biāo),根據(jù)收集到的統(tǒng)計分析,最后確定遠(yuǎn)程資源是否不可用。另外,timeInMilliseconds參數(shù)的設(shè)置必須能被numBuckets參數(shù)整除,不然會拋出異常。比如,當(dāng)timeInMilliseconds為10000,numBuckets可以是10,頁可以是20,但不能是7;若numBuckets為10,那么每個桶的時間長度為1s。

不同粒度的Hystrix配置

實際上,Hystrix為我們提供的配置內(nèi)容和配置方式遠(yuǎn)不止這些,它提供了非常豐富和靈活的配置方法。Hystrix屬性的配置有四個不同的優(yōu)先級別(優(yōu)先級由低到高):

  • Hystrix提供的默認(rèn)值
  • 整個應(yīng)用的的全局配置
  • 類級別的局部配置
  • 線程池級別的特殊配置

這四個級別的優(yōu)先級是由低到高,優(yōu)先級高的會覆蓋優(yōu)先級低的。

Hystrix的每一個屬性配置都會有一個默認(rèn)值,并應(yīng)用到每一個@HystrixCommand注解。除非在類級別或線程池級別對部分配置進行覆蓋,否則都會使用默認(rèn)值。

Hystrix允許我們設(shè)置類級別的默認(rèn)配置,這些配置會被該類中的所有@HystrixCommand共享。類級別配置通過一個類級別注解@DefaultProperties進行設(shè)置,比如,你想配置類MyService中的所有托管給Hystrix的遠(yuǎn)程調(diào)用的超時時間為10s,你可以這樣配置:

@DefaultProperties(
    commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10000")
    }
)    
class MyService{...}

除非在線程池級別對配置顯式覆蓋,否則所有線程池的配置都會繼承應(yīng)用級別的默認(rèn)值或類級別的默認(rèn)值。

注:在上面所有演示代碼中,都使用硬編碼的方式對Hystrix屬性進行配置,但在生產(chǎn)環(huán)境中,Hystrix屬性的value值有很大概率需要微調(diào),比如超時時間、線程池數(shù)量等,所以Hystrix的配置會托管在Spring Cloud Config服務(wù)器。使用這樣的解決方案,當(dāng)你需要微調(diào)某些屬性值時,可以在修改后刷新配置、重啟服務(wù)實例,而不用重新編譯、重新部署應(yīng)用。

線程上下文和Hystrix

當(dāng)執(zhí)行一個@HystrixCommand,Hystrix允許以兩種不同的隔離策略運行,分別為:THREAD(線程)和SEMAPHORE(信號量)。Hystrix默認(rèn)選擇THREAD,此時Hystrix command(命令)會保護運行在被隔離起來的線程池中的線程,這些線程與父線程之間不會共享上下文。這意味著Hystrix可以在有必要的情況(如超時)下隨時中斷由Hystrix控制的線程。

當(dāng)使用基于SEMAPHORE的隔離策略,Hystrix同樣會管理被注解@HystrixCommand保護的分布式調(diào)用,但不會創(chuàng)建一個新的線程;當(dāng)調(diào)用超時,Hystrix將會中斷父線程。在同步容器服務(wù)器環(huán)境(Tomcat),中斷父線程會產(chǎn)生一個無法捕獲的異常,這樣會導(dǎo)致意想不到的后果,因為開發(fā)者無法捕獲異常并處理。

我們可以通過設(shè)置注解@HystrixCommand的屬性commandProperties來定制不同的隔離策略。比如,如果想設(shè)置Hystrix 命令的隔離策略為SEMAPHORE,那么可以這樣配置:

@HystrixCommand(
    commandProperties = {
        @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
    }
)
...

大多數(shù)情況下,Hystrix團隊建議我們使用默認(rèn)的隔離策略——THREAD,這樣能讓@HystrixCommand保護的線程與父線程保持較高的隔離級別。而SEMAPHORE隔離模式屬于輕量級,因此,當(dāng)服務(wù)會有較高訪問量且服務(wù)是運行在異步I/O容器中,如Netty,則應(yīng)當(dāng)選擇SEMAPHORE。

Hystrix儀表盤——Hystrix Dashboard

Hystrix儀表盤的相關(guān)內(nèi)容,請看Hystrix儀表盤——Hystrix dashboard

ThreadLocal和Hystrix

上文已經(jīng)提到,默認(rèn)情況下(THREAD隔離策略),Hystrix線程池中的線程與父線程間不會共享上下文,換句話說,父線程的一些請求數(shù)據(jù),如token,不會傳遞給Hystrix命令管理的線程。由于本教程屬于入門教程,所以暫時先不將太多,等以后的進階教程會詳講。不過在Git上的源碼已經(jīng)有解決方法,有興趣的童鞋可以看看,當(dāng)然Git上的相關(guān)代碼只是解決方案的一種,還有其他的,因涉及到Hystrix的高級編程,會在以后給出。以下的內(nèi)容可以先跳過。

需要添加Git上的相關(guān)代碼為:

  1. 在LicenseServiceController.getLicenses(String organizationId)、LicenseService.getLicensesByOrg(String organizationId)這兩個方法加入日志打印語句;
  2. 然后添加/utils下的三個類,UserContext、UserContextFilter、UserContextHolder;

到這里,添加代碼后,啟動license服務(wù),訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/,且攜帶一個請求頭:tmx-correlation-id:TEST-CORRELATION-ID,如下:

image.png
。請求完成后,可以看到控制臺的輸出如下:
image.png

到這里添加的代碼,只是驗證請求頭tmx-correlation-id沒有被傳遞到被@HystrixCommand注解的getLicensesByOrg()方法中,因為getLicensesByOrg被Hystrix新建的線程包裹著。

接下來,就是真正的解決方案。在license的目錄添加hystrix包,包中有三個類,分別為:DelegatingUserContextCallable、ThreadLocalAwareStrategy、ThreadLocalConfiguration。最后重啟服務(wù)。再次訪問,可以看到控制臺的輸出為:


image.png

可以看到getLicensesByOrg()方法也能拿到請求頭的內(nèi)容了。

上面的代碼,涉及到HystrixConcurrencyStrategy的使用,Hystrix的高級編程還包括HystrixRequestContext等的使用。通過這些可以實現(xiàn)請求緩存、請求合并等提高性能的功能。有興趣的朋友可以自行先了解。

下一節(jié)將繼續(xù)講解:服務(wù)網(wǎng)關(guān)——Spring Cloud Zuul。

完!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容