Hystrix原理與實(shí)戰(zhàn)

原文:https://my.oschina.net/7001/blog/1619842

摘要: Hystrix是Netflix開源的一款容錯(cuò)框架,提供線程池隔離、信號(hào)量隔離异剥、熔斷、降級(jí)回退等容錯(cuò)方法。Hystrix為幫助我們構(gòu)建穩(wěn)定蚕泽、可靠的分布式系統(tǒng)提供了一種解決方案。

背景

分布式系統(tǒng)環(huán)境下桥嗤,服務(wù)間類似依賴非常常見须妻,一個(gè)業(yè)務(wù)調(diào)用通常依賴多個(gè)基礎(chǔ)服務(wù)。如下圖泛领,對(duì)于同步調(diào)用荒吏,當(dāng)庫(kù)存服務(wù)不可用時(shí),商品服務(wù)請(qǐng)求線程被阻塞渊鞋,當(dāng)有大批量請(qǐng)求調(diào)用庫(kù)存服務(wù)時(shí)绰更,最終可能導(dǎo)致整個(gè)商品服務(wù)對(duì)外不可用, 并且這種可不用可能沿請(qǐng)求調(diào)用鏈向上傳遞锡宋,這種現(xiàn)象被稱為雪崩效應(yīng)儡湾。

image

雪崩效應(yīng)常見場(chǎng)景

  • 硬件故障:如服務(wù)器宕機(jī),機(jī)房斷電执俩,光纖被挖斷等徐钠。
  • 流量激增:如異常流量,重試加大流量等役首。
  • 緩存穿透:一般發(fā)生在應(yīng)用重啟尝丐,所有緩存失效時(shí)显拜,以及短時(shí)間內(nèi)大量緩存失效時(shí)。大量的緩存不命中爹袁,使請(qǐng)求直擊后端服務(wù)远荠,造成服務(wù)提供者超負(fù)荷運(yùn)行,引起服務(wù)不可用失息。
  • 程序BUG:如程序邏輯導(dǎo)致內(nèi)存泄漏譬淳,JVM長(zhǎng)時(shí)間FullGC等。
  • 同步等待:服務(wù)間采用同步調(diào)用模式根时,同步等待造成的資源耗盡瘦赫。

雪崩效應(yīng)應(yīng)對(duì)策略

針對(duì)造成雪崩效應(yīng)的不同場(chǎng)景,可以使用不同的應(yīng)對(duì)策略蛤迎,沒有一種通用所有場(chǎng)景的策略确虱,參考如下:

  • 硬件故障:多機(jī)房容災(zāi)、異地多活等替裆。
  • 流量激增:服務(wù)自動(dòng)擴(kuò)容校辩、流量控制(限流、關(guān)閉重試)等辆童。
  • 緩存穿透:緩存預(yù)加載宜咒、緩存異步加載等。
  • 程序BUG:修改程序bug把鉴、及時(shí)釋放資源等故黑。
  • 同步等待:資源隔離、MQ解耦庭砍、不可用服務(wù)調(diào)用快速失敗等场晶。資源隔離通常指不同服務(wù)調(diào)用采用不同的線程池;不可用服務(wù)調(diào)用快速失敗一般通過(guò)超時(shí)機(jī)制怠缸,熔斷器以及熔斷后降級(jí)方法等方案實(shí)現(xiàn)诗轻。

綜上所述,如果一個(gè)應(yīng)用不能對(duì)來(lái)自依賴的故障進(jìn)行隔離揭北,那該應(yīng)用本身就處在被拖垮的風(fēng)險(xiǎn)中扳炬。 因此,為了構(gòu)建穩(wěn)定搔体、可靠的分布式系統(tǒng)恨樟,我們的服務(wù)應(yīng)當(dāng)具有自我保護(hù)能力,當(dāng)依賴服務(wù)不可用時(shí)疚俱,當(dāng)前服務(wù)啟動(dòng)自我保護(hù)功能厌杜,從而避免發(fā)生雪崩效應(yīng)。本文將重點(diǎn)介紹使用Hystrix解決同步等待的雪崩問題计螺。

初探Hystrix

Hystrix [h?st'r?ks]夯尽,中文含義是豪豬,因其背上長(zhǎng)滿棘刺登馒,從而擁有了自我保護(hù)的能力匙握。而Hystrix是Netflix開源的一款容錯(cuò)框架,同樣具有自我保護(hù)能力陈轿。為了實(shí)現(xiàn)容錯(cuò)和自我保護(hù)圈纺,下面我們看看Hystrix如何設(shè)計(jì)和實(shí)現(xiàn)的。

Hystrix設(shè)計(jì)目標(biāo):

  • 對(duì)來(lái)自依賴的延遲和故障進(jìn)行防護(hù)和控制——這些依賴通常都是通過(guò)網(wǎng)絡(luò)訪問的
  • 阻止故障的連鎖反應(yīng)
  • 快速失敗并迅速恢復(fù)
  • 回退并優(yōu)雅降級(jí)
  • 提供近實(shí)時(shí)的監(jiān)控與告警

Hystrix遵循的設(shè)計(jì)原則:

  • 防止任何單獨(dú)的依賴耗盡資源(線程)
  • 過(guò)載立即切斷并快速失敗麦射,防止排隊(duì)
  • 盡可能提供回退以保護(hù)用戶免受故障
  • 使用隔離技術(shù)(例如隔板蛾娶,泳道和斷路器模式)來(lái)限制任何一個(gè)依賴的影響
  • 通過(guò)近實(shí)時(shí)的指標(biāo),監(jiān)控和告警潜秋,確保故障被及時(shí)發(fā)現(xiàn)
  • 通過(guò)動(dòng)態(tài)修改配置屬性蛔琅,確保故障及時(shí)恢復(fù)
  • 防止整個(gè)依賴客戶端執(zhí)行失敗,而不僅僅是網(wǎng)絡(luò)通信

Hystrix如何實(shí)現(xiàn)這些設(shè)計(jì)目標(biāo)峻呛?

  • 使用命令模式將所有對(duì)外部服務(wù)(或依賴關(guān)系)的調(diào)用包裝在HystrixCommand或HystrixObservableCommand對(duì)象中罗售,并將該對(duì)象放在單獨(dú)的線程中執(zhí)行;
  • 每個(gè)依賴都維護(hù)著一個(gè)線程池(或信號(hào)量)钩述,線程池被耗盡則拒絕請(qǐng)求(而不是讓請(qǐng)求排隊(duì))寨躁。
  • 記錄請(qǐng)求成功,失敗牙勘,超時(shí)和線程拒絕职恳。
  • 服務(wù)錯(cuò)誤百分比超過(guò)了閾值,熔斷器開關(guān)自動(dòng)打開方面,一段時(shí)間內(nèi)停止對(duì)該服務(wù)的所有請(qǐng)求放钦。
  • 請(qǐng)求失敗,被拒絕葡幸,超時(shí)或熔斷時(shí)執(zhí)行降級(jí)邏輯最筒。
  • 近實(shí)時(shí)地監(jiān)控指標(biāo)和配置的修改。

Hystrix入門

Hystrix簡(jiǎn)單示例

開始深入Hystrix原理之前蔚叨,我們先簡(jiǎn)單看一個(gè)示例床蜘。

第一步,繼承HystrixCommand實(shí)現(xiàn)自己的command蔑水,在command的構(gòu)造方法中需要配置請(qǐng)求被執(zhí)行需要的參數(shù)邢锯,并組合實(shí)際發(fā)送請(qǐng)求的對(duì)象,代碼如下:

public class QueryOrderIdCommand extends HystrixCommand<Integer> {
    private final static Logger logger = LoggerFactory.getLogger(QueryOrderIdCommand.class);
    private OrderServiceProvider orderServiceProvider;

    public QueryOrderIdCommand(OrderServiceProvider orderServiceProvider) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerRequestVolumeThreshold(10)//至少有10個(gè)請(qǐng)求搀别,熔斷器才進(jìn)行錯(cuò)誤率的計(jì)算
                        .withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請(qǐng)求5秒后會(huì)進(jìn)入半打開狀態(tài),放部分流量過(guò)去重試
                        .withCircuitBreakerErrorThresholdPercentage(50)//錯(cuò)誤率達(dá)到50開啟熔斷保護(hù)
                        .withExecutionTimeoutEnabled(true))
                .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties
                        .Setter().withCoreSize(10)));
        this.orderServiceProvider = orderServiceProvider;
    }

    @Override
    protected Integer run() {
        return orderServiceProvider.queryByOrderId();
    }

    @Override
    protected Integer getFallback() {
        return -1;
    }
}

第二步丹擎,調(diào)用HystrixCommand的執(zhí)行方法發(fā)起實(shí)際請(qǐng)求。

@Test
public void testQueryByOrderIdCommand() {
    Integer r = new QueryOrderIdCommand(orderServiceProvider).execute();
    logger.info("result:{}", r);
}

Hystrix處理流程

Hystrix流程圖如下:

image
                            圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)

Hystrix整個(gè)工作流如下:

  1. 構(gòu)造一個(gè) HystrixCommand或HystrixObservableCommand對(duì)象,用于封裝請(qǐng)求蒂培,并在構(gòu)造方法配置請(qǐng)求被執(zhí)行需要的參數(shù)再愈;
  2. 執(zhí)行命令,Hystrix提供了4種執(zhí)行命令的方法护戳,后面詳述翎冲;
  3. 判斷是否使用緩存響應(yīng)請(qǐng)求,若啟用了緩存媳荒,且緩存可用抗悍,直接使用緩存響應(yīng)請(qǐng)求。Hystrix支持請(qǐng)求緩存钳枕,但需要用戶自定義啟動(dòng)缴渊;
  4. 判斷熔斷器是否打開,如果打開鱼炒,跳到第8步衔沼;
  5. 判斷線程池/隊(duì)列/信號(hào)量是否已滿,已滿則跳到第8步田柔;
  6. 執(zhí)行HystrixObservableCommand.construct()或HystrixCommand.run()俐巴,如果執(zhí)行失敗或者超時(shí),跳到第8步硬爆;否則欣舵,跳到第9步;
  7. 統(tǒng)計(jì)熔斷器監(jiān)控指標(biāo)缀磕;
  8. 走Fallback備用邏輯
  9. 返回請(qǐng)求響應(yīng)

從流程圖上可知道缘圈,第5步線程池/隊(duì)列/信號(hào)量已滿時(shí),還會(huì)執(zhí)行第7步邏輯袜蚕,更新熔斷器統(tǒng)計(jì)信息糟把,而第6步無(wú)論成功與否,都會(huì)更新熔斷器統(tǒng)計(jì)信息牲剃。

執(zhí)行命令的幾種方法

Hystrix提供了4種執(zhí)行命令的方法遣疯,execute()和queue() 適用于HystrixCommand對(duì)象,而observe()和toObservable()適用于HystrixObservableCommand對(duì)象凿傅。

execute()

以同步堵塞方式執(zhí)行run()缠犀,只支持接收一個(gè)值對(duì)象。hystrix會(huì)從線程池中取一個(gè)線程來(lái)執(zhí)行run()聪舒,并等待返回值辨液。

queue()

以異步非阻塞方式執(zhí)行run(),只支持接收一個(gè)值對(duì)象箱残。調(diào)用queue()就直接返回一個(gè)Future對(duì)象滔迈≈褂酰可通過(guò) Future.get()拿到run()的返回結(jié)果,但Future.get()是阻塞執(zhí)行的燎悍。若執(zhí)行成功敬惦,F(xiàn)uture.get()返回單個(gè)返回值。當(dāng)執(zhí)行失敗時(shí)间涵,如果沒有重寫fallback仁热,F(xiàn)uture.get()拋出異常。

observe()

事件注冊(cè)前執(zhí)行run()/construct()勾哩,支持接收多個(gè)值對(duì)象,取決于發(fā)射源举哟。調(diào)用observe()會(huì)返回一個(gè)hot Observable思劳,也就是說(shuō),調(diào)用observe()自動(dòng)觸發(fā)執(zhí)行run()/construct()妨猩,無(wú)論是否存在訂閱者潜叛。

如果繼承的是HystrixCommand,hystrix會(huì)從線程池中取一個(gè)線程以非阻塞方式執(zhí)行run()壶硅;如果繼承的是HystrixObservableCommand威兜,將以調(diào)用線程阻塞執(zhí)行construct()。

observe()使用方法:

  1. 調(diào)用observe()會(huì)返回一個(gè)Observable對(duì)象
  2. 調(diào)用這個(gè)Observable對(duì)象的subscribe()方法完成事件注冊(cè)庐椒,從而獲取結(jié)果

toObservable()

事件注冊(cè)后執(zhí)行run()/construct()椒舵,支持接收多個(gè)值對(duì)象,取決于發(fā)射源约谈。調(diào)用toObservable()會(huì)返回一個(gè)cold Observable笔宿,也就是說(shuō),調(diào)用toObservable()不會(huì)立即觸發(fā)執(zhí)行run()/construct()棱诱,必須有訂閱者訂閱Observable時(shí)才會(huì)執(zhí)行泼橘。

如果繼承的是HystrixCommand,hystrix會(huì)從線程池中取一個(gè)線程以非阻塞方式執(zhí)行run()迈勋,調(diào)用線程不必等待run()炬灭;如果繼承的是HystrixObservableCommand,將以調(diào)用線程堵塞執(zhí)行construct()重归,調(diào)用線程需等待construct()執(zhí)行完才能繼續(xù)往下走。

toObservable()使用方法:

  1. 調(diào)用observe()會(huì)返回一個(gè)Observable對(duì)象
  2. 調(diào)用這個(gè)Observable對(duì)象的subscribe()方法完成事件注冊(cè),從而獲取結(jié)果

需注意的是烤礁,HystrixCommand也支持toObservable()和observe()鲤脏,但是即使將HystrixCommand轉(zhuǎn)換成Observable阻问,它也只能發(fā)射一個(gè)值對(duì)象彻桃。只有HystrixObservableCommand才支持發(fā)射多個(gè)值對(duì)象改衩。

幾種方法的關(guān)系

image
  • execute()實(shí)際是調(diào)用了queue().get()
  • queue()實(shí)際調(diào)用了toObservable().toBlocking().toFuture()
  • observe()實(shí)際調(diào)用toObservable()獲得一個(gè)cold Observable洽胶,再創(chuàng)建一個(gè)ReplaySubject對(duì)象訂閱Observable响逢,將源Observable轉(zhuǎn)化為hot Observable。因此調(diào)用observe()會(huì)自動(dòng)觸發(fā)執(zhí)行run()/construct()棕孙。

Hystrix總是以O(shè)bservable的形式作為響應(yīng)返回,不同執(zhí)行命令的方法只是進(jìn)行了相應(yīng)的轉(zhuǎn)換些膨。

Hystrix容錯(cuò)

Hystrix的容錯(cuò)主要是通過(guò)添加容許延遲和容錯(cuò)方法蟀俊,幫助控制這些分布式服務(wù)之間的交互。 還通過(guò)隔離服務(wù)之間的訪問點(diǎn)订雾,阻止它們之間的級(jí)聯(lián)故障以及提供回退選項(xiàng)來(lái)實(shí)現(xiàn)這一點(diǎn)肢预,從而提高系統(tǒng)的整體彈性。Hystrix主要提供了以下幾種容錯(cuò)方法:

  • 資源隔離
  • 熔斷
  • 降級(jí)

下面我們?cè)敿?xì)談?wù)勥@幾種容錯(cuò)機(jī)制洼哎。

資源隔離

資源隔離主要指對(duì)線程的隔離烫映。Hystrix提供了兩種線程隔離方式:線程池和信號(hào)量。

線程隔離-線程池

Hystrix通過(guò)命令模式對(duì)發(fā)送請(qǐng)求的對(duì)象和執(zhí)行請(qǐng)求的對(duì)象進(jìn)行解耦噩峦,將不同類型的業(yè)務(wù)請(qǐng)求封裝為對(duì)應(yīng)的命令請(qǐng)求锭沟。如訂單服務(wù)查詢商品,查詢商品請(qǐng)求->商品Command识补;商品服務(wù)查詢庫(kù)存族淮,查詢庫(kù)存請(qǐng)求->庫(kù)存Command。并且為每個(gè)類型的Command配置一個(gè)線程池凭涂,當(dāng)?shù)谝淮蝿?chuàng)建Command時(shí)祝辣,根據(jù)配置創(chuàng)建一個(gè)線程池,并放入ConcurrentHashMap切油,如商品Command:

final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
...
if (!threadPools.containsKey(key)) {
    threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}

后續(xù)查詢商品的請(qǐng)求創(chuàng)建Command時(shí)蝙斜,將會(huì)重用已創(chuàng)建的線程池。線程池隔離之后的服務(wù)依賴關(guān)系:

image

通過(guò)將發(fā)送請(qǐng)求線程與執(zhí)行請(qǐng)求的線程分離澎胡,可有效防止發(fā)生級(jí)聯(lián)故障孕荠。當(dāng)線程池或請(qǐng)求隊(duì)列飽和時(shí)娩鹉,Hystrix將拒絕服務(wù),使得請(qǐng)求線程可以快速失敗岛琼,從而避免依賴問題擴(kuò)散底循。

線程池隔離優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 保護(hù)應(yīng)用程序以免受來(lái)自依賴故障的影響,指定依賴線程池飽和不會(huì)影響應(yīng)用程序的其余部分槐瑞。
  • 當(dāng)引入新客戶端lib時(shí)熙涤,即使發(fā)生問題,也是在本lib中困檩,并不會(huì)影響到其他內(nèi)容祠挫。
  • 當(dāng)依賴從故障恢復(fù)正常時(shí),應(yīng)用程序會(huì)立即恢復(fù)正常的性能悼沿。
  • 當(dāng)應(yīng)用程序一些配置參數(shù)錯(cuò)誤時(shí)等舔,線程池的運(yùn)行狀況會(huì)很快檢測(cè)到這一點(diǎn)(通過(guò)增加錯(cuò)誤,延遲糟趾,超時(shí)归榕,拒絕等)锭碳,同時(shí)可以通過(guò)動(dòng)態(tài)屬性進(jìn)行實(shí)時(shí)糾正錯(cuò)誤的參數(shù)配置。
  • 如果服務(wù)的性能有變化,需要實(shí)時(shí)調(diào)整墩莫,比如增加或者減少超時(shí)時(shí)間陈惰,更改重試次數(shù)壕鹉,可以通過(guò)線程池指標(biāo)動(dòng)態(tài)屬性修改担扑,而且不會(huì)影響到其他調(diào)用請(qǐng)求。
  • 除了隔離優(yōu)勢(shì)外劫笙,hystrix擁有專門的線程池可提供內(nèi)置的并發(fā)功能芙扎,使得可以在同步調(diào)用之上構(gòu)建異步門面(外觀模式),為異步編程提供了支持(Hystrix引入了Rxjava異步框架)填大。

注意:盡管線程池提供了線程隔離戒洼,我們的客戶端底層代碼也必須要有超時(shí)設(shè)置或響應(yīng)線程中斷,不能無(wú)限制的阻塞以致線程池一直飽和栋盹。

缺點(diǎn):

線程池的主要缺點(diǎn)是增加了計(jì)算開銷施逾。每個(gè)命令的執(zhí)行都在單獨(dú)的線程完成,增加了排隊(duì)例获、調(diào)度和上下文切換的開銷汉额。因此,要使用Hystrix榨汤,就必須接受它帶來(lái)的開銷蠕搜,以換取它所提供的好處。

通常情況下收壕,線程池引入的開銷足夠小妓灌,不會(huì)有重大的成本或性能影響轨蛤。但對(duì)于一些訪問延遲極低的服務(wù),如只依賴內(nèi)存緩存虫埂,線程池引入的開銷就比較明顯了祥山,這時(shí)候使用線程池隔離技術(shù)就不適合了,我們需要考慮更輕量級(jí)的方式掉伏,如信號(hào)量隔離缝呕。

線程隔離-信號(hào)量

上面提到了線程池隔離的缺點(diǎn),當(dāng)依賴延遲極低的服務(wù)時(shí)斧散,線程池隔離技術(shù)引入的開銷超過(guò)了它所帶來(lái)的好處供常。這時(shí)候可以使用信號(hào)量隔離技術(shù)來(lái)代替,通過(guò)設(shè)置信號(hào)量來(lái)限制對(duì)任何給定依賴的并發(fā)調(diào)用量鸡捐。下圖說(shuō)明了線程池隔離和信號(hào)量隔離的主要區(qū)別:

image
                        圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)

使用線程池時(shí)栈暇,發(fā)送請(qǐng)求的線程和執(zhí)行依賴服務(wù)的線程不是同一個(gè),而使用信號(hào)量時(shí)箍镜,發(fā)送請(qǐng)求的線程和執(zhí)行依賴服務(wù)的線程是同一個(gè)源祈,都是發(fā)起請(qǐng)求的線程。先看一個(gè)使用信號(hào)量隔離線程的示例:

public class QueryByOrderIdCommandSemaphore extends HystrixCommand<Integer> {
    private final static Logger logger = LoggerFactory.getLogger(QueryByOrderIdCommandSemaphore.class);
    private OrderServiceProvider orderServiceProvider;

    public QueryByOrderIdCommandSemaphore(OrderServiceProvider orderServiceProvider) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                        .withCircuitBreakerRequestVolumeThreshold(10)////至少有10個(gè)請(qǐng)求色迂,熔斷器才進(jìn)行錯(cuò)誤率的計(jì)算
                        .withCircuitBreakerSleepWindowInMilliseconds(5000)//熔斷器中斷請(qǐng)求5秒后會(huì)進(jìn)入半打開狀態(tài),放部分流量過(guò)去重試
                        .withCircuitBreakerErrorThresholdPercentage(50)//錯(cuò)誤率達(dá)到50開啟熔斷保護(hù)
                        .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                        .withExecutionIsolationSemaphoreMaxConcurrentRequests(10)));//最大并發(fā)請(qǐng)求量
        this.orderServiceProvider = orderServiceProvider;
    }

    @Override
    protected Integer run() {
        return orderServiceProvider.queryByOrderId();
    }

    @Override
    protected Integer getFallback() {
        return -1;
    }
}

由于Hystrix默認(rèn)使用線程池做線程隔離新博,使用信號(hào)量隔離需要顯示地將屬性execution.isolation.strategy設(shè)置為ExecutionIsolationStrategy.SEMAPHORE,同時(shí)配置信號(hào)量個(gè)數(shù)脚草,默認(rèn)為10≡祝客戶端需向依賴服務(wù)發(fā)起請(qǐng)求時(shí)馏慨,首先要獲取一個(gè)信號(hào)量才能真正發(fā)起調(diào)用,由于信號(hào)量的數(shù)量有限姑隅,當(dāng)并發(fā)請(qǐng)求量超過(guò)信號(hào)量個(gè)數(shù)時(shí)写隶,后續(xù)的請(qǐng)求都會(huì)直接拒絕,進(jìn)入fallback流程讲仰。

信號(hào)量隔離主要是通過(guò)控制并發(fā)請(qǐng)求量慕趴,防止請(qǐng)求線程大面積阻塞,從而達(dá)到限流和防止雪崩的目的鄙陡。

線程隔離總結(jié)

線程池和信號(hào)量都可以做線程隔離冕房,但各有各的優(yōu)缺點(diǎn)和支持的場(chǎng)景,對(duì)比如下:

| | 線程切換 | 支持異步 | 支持超時(shí) | 支持熔斷 | 限流 | 開銷 |
| 信號(hào)量 | 否 | 否 | 否 | 是 | 是 | 小 |
| 線程池 | 是 | 是 | 是 | 是 | 是 | 大 |

線程池和信號(hào)量都支持熔斷和限流趁矾。相比線程池耙册,信號(hào)量不需要線程切換,因此避免了不必要的開銷毫捣。但是信號(hào)量不支持異步详拙,也不支持超時(shí)帝际,也就是說(shuō)當(dāng)所請(qǐng)求的服務(wù)不可用時(shí),信號(hào)量會(huì)控制超過(guò)限制的請(qǐng)求立即返回饶辙,但是已經(jīng)持有信號(hào)量的線程只能等待服務(wù)響應(yīng)或從超時(shí)中返回蹲诀,即可能出現(xiàn)長(zhǎng)時(shí)間等待。線程池模式下弃揽,當(dāng)超過(guò)指定時(shí)間未響應(yīng)的服務(wù)脯爪,Hystrix會(huì)通過(guò)響應(yīng)中斷的方式通知線程立即結(jié)束并返回。

熔斷

熔斷器簡(jiǎn)介

現(xiàn)實(shí)生活中蹋宦,可能大家都有注意到家庭電路中通常會(huì)安裝一個(gè)保險(xiǎn)盒披粟,當(dāng)負(fù)載過(guò)載時(shí),保險(xiǎn)盒中的保險(xiǎn)絲會(huì)自動(dòng)熔斷冷冗,以保護(hù)電路及家里的各種電器守屉,這就是熔斷器的一個(gè)常見例子。Hystrix中的熔斷器(Circuit Breaker)也是起類似作用蒿辙,Hystrix在運(yùn)行過(guò)程中會(huì)向每個(gè)commandKey對(duì)應(yīng)的熔斷器報(bào)告成功拇泛、失敗、超時(shí)和拒絕的狀態(tài)思灌,熔斷器維護(hù)并統(tǒng)計(jì)這些數(shù)據(jù)俺叭,并根據(jù)這些統(tǒng)計(jì)信息來(lái)決策熔斷開關(guān)是否打開。如果打開泰偿,熔斷后續(xù)請(qǐng)求熄守,快速返回。隔一段時(shí)間(默認(rèn)是5s)之后熔斷器嘗試半開耗跛,放入一部分流量請(qǐng)求進(jìn)來(lái)裕照,相當(dāng)于對(duì)依賴服務(wù)進(jìn)行一次健康檢查,如果請(qǐng)求成功调塌,熔斷器關(guān)閉晋南。

熔斷器配置

Circuit Breaker主要包括如下6個(gè)參數(shù):

1、circuitBreaker.enabled

是否啟用熔斷器羔砾,默認(rèn)是TRUE负间。
2 、circuitBreaker.forceOpen

熔斷器強(qiáng)制打開姜凄,始終保持打開狀態(tài)政溃,不關(guān)注熔斷開關(guān)的實(shí)際狀態(tài)。默認(rèn)值FLASE檀葛。
3玩祟、circuitBreaker.forceClosed
熔斷器強(qiáng)制關(guān)閉,始終保持關(guān)閉狀態(tài)屿聋,不關(guān)注熔斷開關(guān)的實(shí)際狀態(tài)空扎。默認(rèn)值FLASE藏鹊。

4、circuitBreaker.errorThresholdPercentage
錯(cuò)誤率转锈,默認(rèn)值50%盘寡,例如一段時(shí)間(10s)內(nèi)有100個(gè)請(qǐng)求,其中有54個(gè)超時(shí)或者異常撮慨,那么這段時(shí)間內(nèi)的錯(cuò)誤率是54%竿痰,大于了默認(rèn)值50%,這種情況下會(huì)觸發(fā)熔斷器打開砌溺。

5影涉、circuitBreaker.requestVolumeThreshold

默認(rèn)值20。含義是一段時(shí)間內(nèi)至少有20個(gè)請(qǐng)求才進(jìn)行errorThresholdPercentage計(jì)算规伐。比如一段時(shí)間了有19個(gè)請(qǐng)求蟹倾,且這些請(qǐng)求全部失敗了,錯(cuò)誤率是100%猖闪,但熔斷器不會(huì)打開鲜棠,總請(qǐng)求數(shù)不滿足20。

6培慌、circuitBreaker.sleepWindowInMilliseconds

半開狀態(tài)試探睡眠時(shí)間豁陆,默認(rèn)值5000ms。如:當(dāng)熔斷器開啟5000ms之后吵护,會(huì)嘗試放過(guò)去一部分流量進(jìn)行試探盒音,確定依賴服務(wù)是否恢復(fù)。

熔斷器工作原理

下圖展示了HystrixCircuitBreaker的工作原理:

image
                                圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)

熔斷器工作的詳細(xì)過(guò)程如下:

第一步馅而,調(diào)用allowRequest()判斷是否允許將請(qǐng)求提交到線程池

  1. 如果熔斷器強(qiáng)制打開里逆,circuitBreaker.forceOpen為true,不允許放行用爪,返回。
  2. 如果熔斷器強(qiáng)制關(guān)閉胁镐,circuitBreaker.forceClosed為true偎血,運(yùn)行放行。此外不必關(guān)注熔斷器實(shí)際狀態(tài)盯漂,也就是說(shuō)熔斷器仍然會(huì)維護(hù)統(tǒng)計(jì)數(shù)據(jù)和開關(guān)狀態(tài)颇玷,只是不生效而已。

第二步就缆,調(diào)用isOpen()判斷熔斷器開關(guān)是否打開

  1. 如果熔斷器開關(guān)打開帖渠,進(jìn)入第三步,否則繼續(xù)竭宰;
  2. 如果一個(gè)周期內(nèi)總的請(qǐng)求數(shù)小于circuitBreaker.requestVolumeThreshold的值空郊,允許請(qǐng)求放行份招,否則繼續(xù);
  3. 如果一個(gè)周期內(nèi)錯(cuò)誤率小于circuitBreaker.errorThresholdPercentage的值狞甚,允許請(qǐng)求放行锁摔。否則,打開熔斷器開關(guān)哼审,進(jìn)入第三步谐腰。

第三步,調(diào)用allowSingleTest()判斷是否允許單個(gè)請(qǐng)求通行涩盾,檢查依賴服務(wù)是否恢復(fù)

  1. 如果熔斷器打開十气,且距離熔斷器打開的時(shí)間或上一次試探請(qǐng)求放行的時(shí)間超過(guò)circuitBreaker.sleepWindowInMilliseconds的值時(shí),熔斷器器進(jìn)入半開狀態(tài)春霍,允許放行一個(gè)試探請(qǐng)求砸西;否則,不允許放行终畅。

此外籍胯,為了提供決策依據(jù),每個(gè)熔斷器默認(rèn)維護(hù)了10個(gè)bucket离福,每秒一個(gè)bucket杖狼,當(dāng)新的bucket被創(chuàng)建時(shí),最舊的bucket會(huì)被拋棄妖爷。其中每個(gè)blucket維護(hù)了請(qǐng)求成功蝶涩、失敗、超時(shí)絮识、拒絕的計(jì)數(shù)器绿聘,Hystrix負(fù)責(zé)收集并統(tǒng)計(jì)這些計(jì)數(shù)器。

熔斷器測(cè)試

1次舌、以QueryOrderIdCommand為測(cè)試command

2熄攘、配置orderServiceProvider不重試且500ms超時(shí)

<dubbo:reference id="orderServiceProvider" interface="com.huang.provider.OrderServiceProvider"
                    timeout="500" retries="0"/>

3、OrderServiceProviderImpl實(shí)現(xiàn)很簡(jiǎn)單彼念,前10個(gè)請(qǐng)求挪圾,服務(wù)端休眠600ms,使得客戶端調(diào)用超時(shí)逐沙。

@Service
public class OrderServiceProviderImpl implements OrderServiceProvider {
    private final static Logger logger = LoggerFactory.getLogger(OrderServiceProviderImpl.class);
    private AtomicInteger OrderIdCounter = new AtomicInteger(0);

    @Override
    public Integer queryByOrderId() {
        int c = OrderIdCounter.getAndIncrement();
        if (logger.isDebugEnabled()) {
            logger.debug("OrderIdCounter:{}", c);
        }
        if (c < 10) {
            try {
                Thread.sleep(600);
            } catch (InterruptedException e) {
            }
        }
        return c;
    }

    @Override
    public void reset() {
        OrderIdCounter.getAndSet(0);
    }
}

4哲思、單測(cè)代碼

@Test
public void testExecuteCommand() throws InterruptedException {
    orderServiceProvider.reset();
    int i = 1;
    for (; i < 15; i++) {
        HystrixCommand<Integer> command = new QueryByOrderIdCommand(orderServiceProvider);
        Integer r = command.execute();
        String method = r == -1 ? "fallback" : "run";
        logger.info("call {} times,result:{},method:{},isCircuitBreakerOpen:{}", i, r, method, command.isCircuitBreakerOpen());
    }
    //等待6s,使得熔斷器進(jìn)入半打開狀態(tài)
    Thread.sleep(6000);
    for (; i < 20; i++) {
        HystrixCommand<Integer> command = new QueryByOrderIdCommand(orderServiceProvider);
        Integer r = command.execute();
        String method = r == -1 ? "fallback" : "run";
        logger.info("call {} times,result:{},method:{},isCircuitBreakerOpen:{}", i, r, method, command.isCircuitBreakerOpen());
    }
}

5吩案、輸出結(jié)果

2018-02-07 11:38:36,056 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 1 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:36,564 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 2 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:37,074 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 3 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:37,580 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 4 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:38,089 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 5 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:38,599 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 6 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:39,109 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 7 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:39,622 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 8 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:40,138 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 9 times,result:-1,method:fallback,isCircuitBreakerOpen:false
2018-02-07 11:38:40,647 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 10 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,651 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 11 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,653 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 12 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,656 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 13 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:40,658 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:36 call 14 times,result:-1,method:fallback,isCircuitBreakerOpen:true
2018-02-07 11:38:46,671 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 15 times,result:10,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,675 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 16 times,result:11,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,680 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 17 times,result:12,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,685 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 18 times,result:13,method:run,isCircuitBreakerOpen:false
2018-02-07 11:38:46,691 INFO [main] com.huang.test.command.QueryByOrderIdCommandTest:testExecuteCommand:44 call 19 times,result:14,method:run,isCircuitBreakerOpen:false

前9個(gè)請(qǐng)求調(diào)用超時(shí)棚赔,走fallback邏輯;

10-14個(gè)請(qǐng)求,熔斷器開關(guān)打開靠益,直接快速失敗走fallback邏輯丧肴;

15-19個(gè)請(qǐng)求,熔斷器進(jìn)入半開狀態(tài)捆毫,放行一個(gè)試探請(qǐng)求調(diào)用成功闪湾,熔斷器關(guān)閉,后續(xù)請(qǐng)求恢復(fù)绩卤。

回退降級(jí)

降級(jí)途样,通常指務(wù)高峰期,為了保證核心服務(wù)正常運(yùn)行濒憋,需要停掉一些不太重要的業(yè)務(wù)何暇,或者某些服務(wù)不可用時(shí),執(zhí)行備用邏輯從故障服務(wù)中快速失敗或快速返回凛驮,以保障主體業(yè)務(wù)不受影響裆站。Hystrix提供的降級(jí)主要是為了容錯(cuò),保證當(dāng)前服務(wù)不受依賴服務(wù)故障的影響黔夭,從而提高服務(wù)的健壯性宏胯。要支持回退或降級(jí)處理,可以重寫HystrixCommand的getFallBack方法或HystrixObservableCommand的resumeWithFallback方法本姥。

Hystrix在以下幾種情況下會(huì)走降級(jí)邏輯:

  • 執(zhí)行construct()或run()拋出異常
  • 熔斷器打開導(dǎo)致命令短路
  • 命令的線程池和隊(duì)列或信號(hào)量的容量超額肩袍,命令被拒絕
  • 命令執(zhí)行超時(shí)

降級(jí)回退方式

Fail Fast 快速失敗

快速失敗是最普通的命令執(zhí)行方法,命令沒有重寫降級(jí)邏輯婚惫。 如果命令執(zhí)行發(fā)生任何類型的故障氛赐,它將直接拋出異常。

Fail Silent 無(wú)聲失敗

指在降級(jí)方法中通過(guò)返回null先舷,空Map艰管,空List或其他類似的響應(yīng)來(lái)完成。

@Override
protected Integer getFallback() {
   return null;
}

@Override
protected List<Integer> getFallback() {
   return Collections.emptyList();
}

@Override
protected Observable<Integer> resumeWithFallback() {
   return Observable.empty();
}

Fallback: Static

指在降級(jí)方法中返回靜態(tài)默認(rèn)值蒋川。 這不會(huì)導(dǎo)致服務(wù)以“無(wú)聲失敗”的方式被刪除牲芋,而是導(dǎo)致默認(rèn)行為發(fā)生。如:應(yīng)用根據(jù)命令執(zhí)行返回true / false執(zhí)行相應(yīng)邏輯捺球,但命令執(zhí)行失敗街图,則默認(rèn)為true

@Override
protected Boolean getFallback() {
    return true;
}
@Override
protected Observable<Boolean> resumeWithFallback() {
    return Observable.just( true );
}

Fallback: Stubbed

當(dāng)命令返回一個(gè)包含多個(gè)字段的復(fù)合對(duì)象時(shí),適合以Stubbed 的方式回退懒构。

@Override
protected MissionInfo getFallback() {
   return new MissionInfo("missionName","error");
}

Fallback: Cache via Network

有時(shí),如果調(diào)用依賴服務(wù)失敗耘擂,可以從緩存服務(wù)(如redis)中查詢舊數(shù)據(jù)版本胆剧。由于又會(huì)發(fā)起遠(yuǎn)程調(diào)用,所以建議重新封裝一個(gè)Command,使用不同的ThreadPoolKey秩霍,與主線程池進(jìn)行隔離篙悯。

@Override
protected Integer getFallback() {
   return new RedisServiceCommand(redisService).execute();
}

Primary + Secondary with Fallback

有時(shí)系統(tǒng)具有兩種行為- 主要和次要,或主要和故障轉(zhuǎn)移铃绒。主要和次要邏輯涉及到不同的網(wǎng)絡(luò)調(diào)用和業(yè)務(wù)邏輯鸽照,所以需要將主次邏輯封裝在不同的Command中,使用線程池進(jìn)行隔離颠悬。為了實(shí)現(xiàn)主從邏輯切換矮燎,可以將主次command封裝在外觀HystrixCommand的run方法中,并結(jié)合配置中心設(shè)置的開關(guān)切換主從邏輯赔癌。由于主次邏輯都是經(jīng)過(guò)線程池隔離的HystrixCommand诞外,因此外觀HystrixCommand可以使用信號(hào)量隔離,而沒有必要使用線程池隔離引入不必要的開銷灾票。原理圖如下:

image
                      圖片來(lái)源Hystrix官網(wǎng)[https://github.com/Netflix/Hystrix/wiki](https://github.com/Netflix/Hystrix/wiki)

主次模型的使用場(chǎng)景還是很多的峡谊。如當(dāng)系統(tǒng)升級(jí)新功能時(shí),如果新版本的功能出現(xiàn)問題刊苍,通過(guò)開關(guān)控制降級(jí)調(diào)用舊版本的功能既们。示例代碼如下:

public class CommandFacadeWithPrimarySecondary extends HystrixCommand<String> {

    private final static DynamicBooleanProperty usePrimary = DynamicPropertyFactory.getInstance().getBooleanProperty("primarySecondary.usePrimary", true);

    private final int id;

    public CommandFacadeWithPrimarySecondary(int id) {
        super(Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("PrimarySecondaryCommand"))
                .andCommandPropertiesDefaults(
                        // 由于主次command已經(jīng)使用線程池隔離,F(xiàn)acade Command使用信號(hào)量隔離即可
                        HystrixCommandProperties.Setter()
                                .withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));
        this.id = id;
    }

    @Override
    protected String run() {
        if (usePrimary.get()) {
            return new PrimaryCommand(id).execute();
        } else {
            return new SecondaryCommand(id).execute();
        }
    }

    @Override
    protected String getFallback() {
        return "static-fallback-" + id;
    }

    @Override
    protected String getCacheKey() {
        return String.valueOf(id);
    }

    private static class PrimaryCommand extends HystrixCommand<String> {

        private final int id;

        private PrimaryCommand(int id) {
            super(Setter
                    .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("PrimaryCommand"))
                    .andCommandPropertiesDefaults(                          HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(600)));
            this.id = id;
        }

        @Override
        protected String run() {
            return "responseFromPrimary-" + id;
        }

    }

    private static class SecondaryCommand extends HystrixCommand<String> {

        private final int id;

        private SecondaryCommand(int id) {
            super(Setter
                    .withGroupKey(HystrixCommandGroupKey.Factory.asKey("SystemX"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("SecondaryCommand"))
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("SecondaryCommand"))
                    .andCommandPropertiesDefaults(  HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(100)));
            this.id = id;
        }

        @Override
        protected String run() {
            return "responseFromSecondary-" + id;
        }

    }

    public static class UnitTest {

        @Test
        public void testPrimary() {
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            try {
                ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", true);
                assertEquals("responseFromPrimary-20", new CommandFacadeWithPrimarySecondary(20).execute());
            } finally {
                context.shutdown();
                ConfigurationManager.getConfigInstance().clear();
            }
        }

        @Test
        public void testSecondary() {
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
            try {
                ConfigurationManager.getConfigInstance().setProperty("primarySecondary.usePrimary", false);
                assertEquals("responseFromSecondary-20", new CommandFacadeWithPrimarySecondary(20).execute());
            } finally {
                context.shutdown();
                ConfigurationManager.getConfigInstance().clear();
            }
        }
    }
}

通常情況下正什,建議重寫getFallBack或resumeWithFallback提供自己的備用邏輯啥纸,但不建議在回退邏輯中執(zhí)行任何可能失敗的操作。

總結(jié)

本文介紹了Hystrix及其工作原理埠忘,還介紹了Hystrix線程池隔離脾拆、信號(hào)量隔離和熔斷器的工作原理,以及如何使用Hystrix的資源隔離莹妒,熔斷和降級(jí)等技術(shù)實(shí)現(xiàn)服務(wù)容錯(cuò)名船,從而提高系統(tǒng)的整體健壯性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末旨怠,一起剝皮案震驚了整個(gè)濱河市渠驼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鉴腻,老刑警劉巖迷扇,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爽哎,居然都是意外死亡蜓席,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門课锌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)厨内,“玉大人祈秕,你說(shuō)我怎么就攤上這事〕福” “怎么了请毛?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)瞭亮。 經(jīng)常有香客問我方仿,道長(zhǎng),這世上最難降的妖魔是什么统翩? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任仙蚜,我火速辦了婚禮,結(jié)果婚禮上唆缴,老公的妹妹穿的比我還像新娘鳍征。我一直安慰自己,他們只是感情好面徽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布艳丛。 她就那樣靜靜地躺著,像睡著了一般趟紊。 火紅的嫁衣襯著肌膚如雪氮双。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天霎匈,我揣著相機(jī)與錄音戴差,去河邊找鬼。 笑死铛嘱,一個(gè)胖子當(dāng)著我的面吹牛暖释,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播墨吓,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼球匕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了帖烘?” 一聲冷哼從身側(cè)響起亮曹,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎秘症,沒想到半個(gè)月后照卦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乡摹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年役耕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聪廉。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瞬痘,死狀恐怖氏义,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情图云,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布邻邮,位于F島的核電站竣况,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏筒严。R本人自食惡果不足惜丹泉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望鸭蛙。 院中可真熱鬧摹恨,春花似錦、人聲如沸娶视。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肪获。三九已至寝凌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孝赫,已是汗流浹背较木。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留青柄,地道東北人伐债。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像致开,于是被迫代替她去往敵國(guó)和親峰锁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353