Sentinel 是如何做限流的

限流是保障服務(wù)高可用的方式之一,尤其是在微服務(wù)架構(gòu)中,對接口或資源進(jìn)行限流可以有效地保障服務(wù)的可用性和穩(wěn)定性。

之前的項目中使用的限流措施主要是Guava的RateLimiter。RateLimiter是基于令牌桶流控算法,使用非常簡單,但是功能相對比較少。

而現(xiàn)在,我們有了一種新的選擇,阿里提供的Sentinel。

Sentinel 是阿里巴巴提供的一種限流、熔斷中間件,與RateLimiter相比,Sentinel提供了豐富的限流、熔斷功能。它支持控制臺配置限流、熔斷規(guī)則,支持集群限流,并可以將相應(yīng)服務(wù)調(diào)用情況可視化。

目前已經(jīng)有很多項目接入了Sentinel,而本文主要是對Sentinel的限流功能做一次詳細(xì)的分析,至于Sentinel的其他能力,則不作深究。


一:總體流程:


從設(shè)計模式上來看,典型的的責(zé)任鏈模式。外部請求進(jìn)來后,要經(jīng)過責(zé)任鏈上各個節(jié)點(diǎn)的處理,而Sentinel的限流、熔斷就是通過責(zé)任鏈上的這些節(jié)點(diǎn)實(shí)現(xiàn)的。

從限流算法來看,Sentinel使用滑動窗口算法來進(jìn)行限流。要想深入了解原理,還是得從源碼上入手,下面,直接進(jìn)入Sentinel的源碼閱讀。


二:源碼解讀:

1,總體流程:

讀源碼先得找到源碼入口。我們經(jīng)常使用@ SentinelResource來標(biāo)記一個方法,可以將這個被@ SentinelResource標(biāo)記的方法看成是一個Sentinel資源。因此,我們以@ SentinelResource為入口,找到其切面,看看切面攔截后所做的工作,就可以明確Sentinel的工作原理了。直接看注解@SentinelResource的切面代碼(SentinelResourceAspect)。


可以清晰的看到Sentinel的行為方式。進(jìn)入SentinelResource切面后,會執(zhí)行SphU.entry方法,在這個方法中會對被攔截方法做限流和熔斷的邏輯處理。

如果觸發(fā)熔斷和限流,會拋出BlockException,我們可以指定blockHandler方法來處理BlockException。而對于業(yè)務(wù)上的異常,我們也可以配置fallback方法來處理被攔截方法調(diào)用產(chǎn)生的異常。

所以,Sentinel熔斷限流的處理主要是在SphU.entry方法中,其主要處理邏輯見下圖源碼。


可見,在SphU.entry方法中,Sentinel實(shí)現(xiàn)限流、熔斷等功能的流程可以總結(jié)如下:

獲取Sentinel上下文(Context);

獲取資源對應(yīng)的責(zé)任鏈;

生成資源調(diào)用憑證(Entry);

執(zhí)行責(zé)任鏈中各個節(jié)點(diǎn)。

接下來,圍繞這幾個方面,對Sentinel的服務(wù)機(jī)制做一個系統(tǒng)的闡述。

2,獲取Sentinel上下文(Context)

Context,顧名思義,就是Sentinel熔斷限流執(zhí)行的上下文,包含資源調(diào)用的節(jié)點(diǎn)和Entry信息。

來看看Context的特征:

Context是線程持有的,利用ThreadLocal與當(dāng)前線程綁定


Context包含的內(nèi)容



這里就引出了Sentinel的三個比較重要的概念:Conetxt,Node,Entry。這三個類是Sentinel的核心類,提供了資源調(diào)用路徑、資源調(diào)用統(tǒng)計等信息。

Context

Context是當(dāng)前線程所持有的Sentinel上下文。

進(jìn)入Sentinel的邏輯時,會首先獲取當(dāng)前線程的Context,如果沒有則新建。當(dāng)任務(wù)執(zhí)行完畢后,會清除當(dāng)前線程的context。Context 代表調(diào)用鏈路上下文,貫穿一次調(diào)用鏈路中的所有 Entry。

Context 維持著入口節(jié)點(diǎn)(entranceNode)、本次調(diào)用鏈路的 當(dāng)前節(jié)點(diǎn)(curNode)、調(diào)用來源(origin)等信息。Context 名稱即為調(diào)用鏈路入口名稱。

Node

Node是對一個@SentinelResource標(biāo)記的資源的統(tǒng)計包裝。

Context中記錄本當(dāng)前線程資源調(diào)用的入口節(jié)點(diǎn)。

我們可以通過入口節(jié)點(diǎn)的childList,可以追溯資源的調(diào)用情況。而每個節(jié)點(diǎn)都對應(yīng)一個@SentinelResource標(biāo)記的資源及其統(tǒng)計數(shù)據(jù),例如:passQps,blockQps,rt等數(shù)據(jù)。


Entry

Entry是Sentinel中用來表示是否通過限流的一個憑證,如果能正常返回,則說明你可以訪問被Sentinel保護(hù)的后方服務(wù),否則Sentinel會拋出一個BlockException。

另外,它保存了本次執(zhí)行entry()方法的一些基本信息,包括資源的Context、Node、對應(yīng)的責(zé)任鏈等信息,后續(xù)完成資源調(diào)用后,還需要更具獲得的這個Entry去執(zhí)行一些善后操作,包括退出Entry對應(yīng)的責(zé)任鏈,完成節(jié)點(diǎn)的一些統(tǒng)計信息更新,清除當(dāng)前線程的Context信息等。


3.? 獲取@SentinelResource標(biāo)記資源對應(yīng)的責(zé)任鏈

資源對應(yīng)的責(zé)任鏈?zhǔn)窍蘖鬟壿嬀唧w執(zhí)行的地方,采用的是典型的責(zé)任鏈模式。


默認(rèn)的責(zé)任鏈中的處理節(jié)點(diǎn)包括NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot、FlowSlot、DegradeSlot等。調(diào)用鏈(ProcessorSlotChain)和其中包含的所有Slot都實(shí)現(xiàn)了ProcessorSlot接口,采用責(zé)任鏈的模式執(zhí)行各個節(jié)點(diǎn)的處理邏輯,并調(diào)用下一個節(jié)點(diǎn)。

此外,相同資源(@SentinelResource標(biāo)記的方法)對應(yīng)的責(zé)任鏈?zhǔn)且恢碌摹R簿褪钦f,每個資源對應(yīng)一條單獨(dú)的責(zé)任鏈,可以看下源碼中資源責(zé)任鏈的獲取邏輯:先從緩存獲取,沒有則新建。


4. 生成調(diào)用憑證Entry

生成的Entry是CtEntry。其構(gòu)造參數(shù)包括資源包裝(ResourceWrapper)、資源對應(yīng)的責(zé)任鏈以及當(dāng)前線程的Context。

可以看到,新建CtEntry記錄了當(dāng)前資源的責(zé)任鏈和Context,同時更新Context,將Context的當(dāng)前Entry設(shè)置為自己。可以看到,CtEntry是一個雙向鏈表,構(gòu)建了Sentinel資源的調(diào)用鏈路。


5,責(zé)任鏈的執(zhí)行

接下來就進(jìn)入了責(zé)任鏈的執(zhí)行。責(zé)任鏈和其中的Slot都實(shí)現(xiàn)了ProcessorSlot,責(zé)任鏈的entry方法會依次執(zhí)行責(zé)任鏈各個slot,所以下面就進(jìn)入了責(zé)任鏈中的各個Slot。為了突出重點(diǎn),這次本文只研究與限流功能有關(guān)的Slot。


5.1??????NodeSelectorSlot -- 獲取當(dāng)前資源對應(yīng)Node,構(gòu)建節(jié)點(diǎn)調(diào)用樹

此節(jié)點(diǎn)負(fù)責(zé)獲取或者構(gòu)建當(dāng)前資源對應(yīng)的Node,這個Node被用于后續(xù)資源調(diào)用的統(tǒng)計及限流和熔斷條件的判斷。同時,NodeSelectorSlot還會完成調(diào)用鏈路構(gòu)建。來看源碼:


熟悉的代碼風(fēng)格。我們知道一個資源對應(yīng)一個責(zé)任鏈。每個調(diào)用鏈中都有NodeSelectorSlot。NodeSelectSlot中的node緩存map是非靜態(tài)變量,所以map只對當(dāng)前這個資源共用,不同的資源對應(yīng)的NodeSelectSlot及Node的緩存都是不一樣的,資源和Node緩存map的關(guān)系可見下圖。


所以NodeSelectorSlot的的作用是:

在資源對應(yīng)的調(diào)用鏈執(zhí)行時,獲取當(dāng)前context對應(yīng)的Node,這個Node代表著這個資源的調(diào)用情況。

將獲取到的node設(shè)為當(dāng)前node,添加到之前的node后面,形成樹狀的調(diào)用路徑。(通過Context中的當(dāng)前Entry進(jìn)行)

觸發(fā)下一個Slot的執(zhí)行。

這里有個很有趣的問題,就是我們在責(zé)任鏈的NodeSelectorSlot中獲取資源對應(yīng)的Node時,為什么用的是Context的name,而不是SentinelResource的name呢?

首先,我們知道一個資源對應(yīng)一條責(zé)任鏈。但是進(jìn)入一個資源調(diào)用的Context卻可能是不同的。如果使用資源名來作為key,獲取對應(yīng)的Node,那么通過不同context進(jìn)來的調(diào)用方法獲取到的Node就都是同一個了。所以通過這種方式,可以將相同resource對應(yīng)的node按Context區(qū)分開。

舉個例子,Sentinel功能的實(shí)現(xiàn)不僅僅可以通過@SentinelResource注解方法來實(shí)現(xiàn),也可以通過引入相關(guān)依賴(sentinel-dubbo-adapter),利用Dubbo的Filter機(jī)制直接對DUBBO接口進(jìn)行保護(hù)。我們來比較@SentinelResource和Dubbo方式生成Context的區(qū)別:

Dubbo Filter方式

生成的context的name是Dubbo的接口限定名或者方法限定名。

如果出現(xiàn)嵌套在Dubbo Filter方式下面的其他SentinelResource的資源調(diào)用,那么這些資源調(diào)用的就會就會出現(xiàn)不同的Context。

所以有這樣一種情況,不同的dubbo接口進(jìn)來,這些dubbo接口都調(diào)用了同一個@SentinelResource標(biāo)記的方法,那么這個方法對應(yīng)的SentinelReource的在執(zhí)行時對應(yīng)的Context就是不同的。

另一個問題是,既然資源按Context分出了不同的node,那我們想看資源總數(shù)統(tǒng)計是怎么辦呢?這就涉及到ClusterNode了。詳細(xì)可見ClusterBuilderSlot。


5.2? ?ClusterBuilderSlot -- 聚合相同資源不同Context的Node

此節(jié)點(diǎn)負(fù)責(zé)聚合相同資源不同Context對應(yīng)的Node,以供后續(xù)限流判斷使用。

可以看到,ClusterNode的獲取是以資源名為key。ClusterNode將會成為當(dāng)前node的一個屬性,主要目的是為了聚合同一個資源不同Context情況下的多個node。默認(rèn)的限流條件判斷就是依據(jù)ClusterNode中的統(tǒng)計信息來進(jìn)行的



5.3 StatisticSlot -- 資源調(diào)用統(tǒng)計

此節(jié)點(diǎn)主要負(fù)責(zé)資源調(diào)用的統(tǒng)計信息的計算和更新。與前面以及后面的slot不同,StatisticSlot的執(zhí)行時先觸發(fā)下一個slot的執(zhí)行,等下面的slot執(zhí)行完才會執(zhí)行自己的邏輯。


這也很好理解,作為統(tǒng)計組件,總要等熔斷或者限流處理完之后才能做統(tǒng)計吧。下面看一下具體的統(tǒng)計過程。



上面這張圖已經(jīng)很清晰的描述了StatisticSlot的數(shù)據(jù)統(tǒng)計的過程。可以注意一下無異常和阻塞異常的情況,主要是更新線程數(shù)、通過請求數(shù)量和阻塞請求數(shù)量。不管是DefaultNode,還是ClusterNode,都繼承自StatisticNode。所以Node的數(shù)據(jù)更新要來到StatisticNode。

參考Sentinel數(shù)據(jù)統(tǒng)計框圖,描述了Node統(tǒng)計數(shù)據(jù)更新的大體流程如下:



我們從StatisticNode.addPassRequest()方法入手,以passQps為例,探究StatisticNode是如何更新通過請求的QPS計數(shù)的。


從源碼可見,計數(shù)變量rollingCounterInSecond和rollingCounterInMinute都是Metric,兩個變量的時間維度分別是秒和分鐘。rollingCounterInSecond和rollingCounterInMinute用的是Metric的實(shí)現(xiàn)類ArrayMetric。



從ArrayMetric追溯下去:

統(tǒng)計信息都是保存到ArrayMetric的data,也就是LeapArray<MertricBucket>中的。


LeapArray是時間窗口數(shù)組。基本信息包括:時間窗口長度(ms,windowLengthInMs),取樣數(shù)(也就是時間窗口的數(shù)量,sampleCount),時間間隔(ms,intervalInMs),以及時間窗口數(shù)組(array)。時間窗口長度、取樣數(shù)及時間間隔有下面的關(guān)系:

windowLengthInMs = intervalInMs / sampleCount

代碼中rollingCounterInSecond使用的intervalInMs 是1000(ms),也就是1s,sampleCount=2。所以,窗口時長就是windowLengthInMs = 500ms。rollingCounterInMinute使用的intervalInMs 是60 * 1000(ms),也就是60s。sampleCount=60,所以,windowLengthInMs = 1000ms,也就是1s。

時間窗口數(shù)組(array)是類型是AtomicReferenceArray,可見這是一個原子操作的的數(shù)組引用。數(shù)組元素類型是WindowWrap<MetricBucket>。windowWrap是對時間窗口的一個包裝,包括窗口的開始時間(windowStart)及窗口的長度(windowLengthInMs),以及本窗口的計數(shù)器(value,類型為MetricBucket)。窗口實(shí)際的計數(shù)是由MetricBucket進(jìn)行的,計數(shù)信息是保存在MetricBucket里計數(shù)器counters(類型為(LongAdder))。可以看一下下圖計數(shù)組件的組成框圖:


回到StatisticNode.addPassRequest方法,以rollingCounterInSecond.addPass(count)為例,探究Sentinel如何進(jìn)行滑動窗口計數(shù)的。

5.3.1?獲取當(dāng)前時間窗口

(1)取當(dāng)前時間戳對應(yīng)的數(shù)組下標(biāo)

long timeId = time / windowLength

int idx = (int)(timeId % array.length());

time為當(dāng)前時間,windowLength為時間窗口長度,rollingCounterInSecond的時間窗口長度是500ms。array 是單位時間內(nèi)時間窗口的數(shù)量,rollingCounterInSecond的單位時間(1s)時間窗口數(shù)是2。timeId是當(dāng)前時間對時間窗口的整除。time每增加一個windowLength的長度,timeId就會增加1,時間窗口就會往前滑動一個。

(2)計算窗口開始時間

窗口開始時間 = 當(dāng)前時間(ms)-當(dāng)前時間(ms)%時間窗口長度(ms)

獲取的窗口開始時間均為時間窗口的整數(shù)倍。

(3)獲取時間窗口

首先,根據(jù)數(shù)組下標(biāo)從LeapArray的數(shù)組中獲取時間窗口。

如果獲取到的時間窗口自為空,則新建時間窗口(CAS)。

如果獲取到的時間窗口非空,且時間窗口的開始時間等于我們計算的開始時間,說明當(dāng)前時間正好在這個時間窗口里,直接返回該時間窗口。

?如果獲取到的時間窗口非空,且時間窗口的開始時間小于我們計算的開始時間,說明時間窗口已經(jīng)過期(距離上次獲取時間窗口已經(jīng)過去比較久的場景),需要更新時間窗口(加鎖操作),將時間窗口的開始時間設(shè)為計算出來的開始時間,將時間窗口里的計數(shù)器重置為0。

?如果獲取到的時間窗口非空,且時間窗口的開始時間大于我們計算的開始時間,創(chuàng)建新的時間窗口。這個一般不會走進(jìn)這個分支,因為說明當(dāng)前時間已經(jīng)落后于時間窗口了,獲取到的時間窗口是將來的時間,那就沒有意義了。


5.3.2?對時間窗口的計數(shù)器進(jìn)行累加

時間窗口計數(shù)器是一個LongAdder數(shù)組,這個數(shù)組用于存放通過請求數(shù)、異常請求數(shù)、阻塞請求數(shù)等數(shù)據(jù)。如下圖:


其中,通過計數(shù)、阻塞計數(shù)、異常計數(shù)為執(zhí)行StatisticSlot的entry方法時更新。成功計數(shù)及響應(yīng)時間是執(zhí)行StatisticSlot的exit方法時更新。其實(shí)就是分別在被攔截方法執(zhí)行前和執(zhí)行后進(jìn)行相應(yīng)計數(shù)的更新。當(dāng)然,addPass就是在計數(shù)數(shù)組的第一個元素上進(jìn)行累加。

計數(shù)數(shù)組元素類型是LongAdder。LongAdder是JDK8添加到JUC中的。它是一個線程安全的、比Atomic*系工具性能更好的"計數(shù)器"。


5.4 FlowSlot -- 限流判斷

FlowSlot是進(jìn)行限流條件判斷的節(jié)點(diǎn)。之前在StatisticSlot對相關(guān)資源調(diào)用做的統(tǒng)計,在FlowSlot限流判斷時將會得到使用。

直接來到限流操作的核心邏輯–限流規(guī)則檢查器(FlowRuleChecker):


主要的流程包括:

獲取資源對應(yīng)的限流規(guī)則

根據(jù)限流規(guī)則檢查是否被限流

如果被限流,則拋出限流異常FlowException。FlowException繼承自BlockException。

那么FlowSlot檢查是否限流的過程是怎么樣的?

默認(rèn)情況下,限流使用的節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)的cluster node。主要分析的限流方式是QPS限流。來看一下限流的關(guān)鍵代碼(DefaultController):


獲取節(jié)點(diǎn)的當(dāng)前qps計數(shù);

判斷獲取新的計數(shù)后是否超過閾值

超過閾值單返回false,表示被限流,后面會拋出FlowException。否則返回true,不被限流。

可以看到限流判斷非常簡單,只需要對qps計數(shù)進(jìn)行檢查就可以了。這歸功于StatisticSlot做的數(shù)據(jù)統(tǒng)計。



NodeSelectorSlot用于獲取資源對應(yīng)的Node,并構(gòu)建Node調(diào)用樹,將SentinelSource的調(diào)用鏈路以Node Tree的形式組起來。ClusterBuilderSlot為當(dāng)前Node創(chuàng)建對應(yīng)的ClusterNode,聚合相同資源對應(yīng)的不同Context的Node,后續(xù)的限流依據(jù)就是這個ClusterNode。

ClusterNode繼承自StatisticNode,記錄著相應(yīng)資源處理的一些統(tǒng)計數(shù)據(jù)。StatisticSlot用于更新資源調(diào)用的相關(guān)計數(shù),用于后續(xù)的限流判斷使用。FlowSlot根據(jù)資源對應(yīng)Node的調(diào)用計數(shù),判斷是否進(jìn)行限流。至此,Sentinel的責(zé)任鏈執(zhí)行邏輯就完整了。



6,Sentienl 的收尾工作

無論執(zhí)行成功還是失敗,或者是阻塞,都會執(zhí)行Entry.exit()方法,來看一下這個方法。

判斷要退出的entry是否是當(dāng)前context的當(dāng)前entry;

如果要退出的entry不是當(dāng)前context的當(dāng)前entry,則不退出此entry,而是退出context的的當(dāng)前entry及其所有父entry,并拋出異常;

如果要退出的entry是當(dāng)前context的當(dāng)前entry(這種是正常情況),先退出當(dāng)前entry對應(yīng)的責(zé)任鏈的所有slot。在這一步,StatisticSlot會更新node的success計數(shù)和RT計數(shù);

將context的當(dāng)前entry置為被退出的entry的父entry;

如果被退出entry的父entry為空,且context為默認(rèn)context,自動退出默認(rèn)context(清除ThreadLocal)。

清除被退出entry的context引用





總結(jié)

通過閱讀Sentinel的源碼,可以很清晰的理解Sentinel的限流過程了,而對上面的源碼閱讀,總結(jié)如下:

三大組件Context、Entry、Node,是Sentinel的核心組件,各類信息及資源調(diào)用情況都由這三大類持有;

采用責(zé)任鏈模式完成Sentinel的信息統(tǒng)計、熔斷、限流等操作;

責(zé)任鏈中NodeSelectSlot負(fù)責(zé)選擇當(dāng)前資源對應(yīng)的Node,同時構(gòu)建node調(diào)用樹;

責(zé)任鏈中ClusterBuilderSlot負(fù)責(zé)構(gòu)建當(dāng)前Node對應(yīng)的ClusterNode,用于聚合同一資源對應(yīng)不同Context的Node;

責(zé)任鏈中的StatisticSlot用于統(tǒng)計當(dāng)前資源的調(diào)用情況,更新Node與其對用的ClusterNode的各種統(tǒng)計數(shù)據(jù);

責(zé)任鏈中的FlowSlot根據(jù)當(dāng)前Node對應(yīng)的ClusterNode(默認(rèn))的統(tǒng)計信息進(jìn)行限流;

資源調(diào)用統(tǒng)計數(shù)據(jù)(例如PassQps)使用滑動時間窗口進(jìn)行統(tǒng)計;

所有工作執(zhí)行完畢后,執(zhí)行退出流程,補(bǔ)充一些統(tǒng)計數(shù)據(jù),清理Context。

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

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