如何使用Go建開發(fā)高負(fù)載WebSocket服務(wù)器

嗨血巍,大家好! 我的名字是Sergey Kamardin纸肉,我是Mail.Ru的工程師。

介紹

首先介紹我們的故事的上下文股毫,應(yīng)該介紹幾點(diǎn)我們?yōu)槭裁葱枰@個(gè)服務(wù)器。

Mail.Ru有很多有狀態(tài)的系統(tǒng)召衔。 用戶電子郵件存儲(chǔ)是其中之一铃诬。 跟蹤系統(tǒng)中的狀態(tài)變化和系統(tǒng)事件有幾種方法。 這主要是通過定期系統(tǒng)輪詢或關(guān)于其狀態(tài)變化的系統(tǒng)通知。

兩種方式都有利弊趣席。 但是當(dāng)涉及郵件時(shí)兵志,用戶收到新郵件的速度越快越好。

郵件輪詢涉及每秒大約50,000個(gè)HTTP查詢宣肚,其中60%返回304狀態(tài)想罕,這意味著郵箱沒有變化。

因此霉涨,為了減少服務(wù)器上的負(fù)載并加快郵件傳遞給用戶按价,決定通過編寫發(fā)布-訂閱服務(wù)器,一方面將接收有關(guān)狀態(tài)更改的通知嵌纲,另一方面則會(huì)收到這種通知的訂閱俘枫。

先前

現(xiàn)在

第一個(gè)方案顯示了以前的樣子腥沽。 瀏覽器定期輪詢API逮走,并查詢有關(guān)Storage(郵箱服務(wù))的更改。

第二個(gè)方案描述了新架構(gòu)今阳。 瀏覽器與通知API建立WebSocket連接师溅,通知API是Bus服務(wù)器的客戶端。收到新的電子郵件后盾舌,Storage會(huì)向Bus(1)發(fā)送一條通知墓臭,由Bus發(fā)送到訂閱者。 API確定連接以發(fā)送接收到的通知妖谴,并將其發(fā)送到用戶的瀏覽器(3)窿锉。

所以今天我們將討論API或WebSocket服務(wù)器。 我們的服務(wù)器將有大約300萬個(gè)在線連接膝舅。

實(shí)現(xiàn)方式

讓我們看看如何使用Go函數(shù)實(shí)現(xiàn)服務(wù)器的某些部分嗡载,而無需任何優(yōu)化。

在進(jìn)行net/http 仍稀,我們來談?wù)勎覀內(nèi)绾伟l(fā)送和接收數(shù)據(jù)洼滚。 站在WebSocket協(xié)議(例如JSON對象) 之上的數(shù)據(jù)在下文中將被稱為分組 。

我們開始實(shí)現(xiàn)包含通過WebSocket連接發(fā)送和接收這些數(shù)據(jù)包的Channel結(jié)構(gòu)技潘。

channel 結(jié)構(gòu)

//?Packet?represents?applicationleveldata.

type?Packet?struct?{

...

}

//?Channel?wrapsuserconnection.

type?Channel?struct?{

conn?net.Conn????//?WebSocketconnection.

send?chan?Packet?//?Outgoing?packets?queue.

}

func?NewChannel(conn?net.Conn)?*Channel?{

c?:=?&Channel{

conn:?conn,

send:?make(chan?Packet,?N),

}

go?c.reader()

go?c.writer()

returnc

}

注意這里有reader和writer連個(gè)goroutines遥巴。 每個(gè)goroutine都需要自己的內(nèi)存棧, 根據(jù)操作系統(tǒng)和Go版本可能具有2到8 KB的初始大小享幽。

在300萬個(gè)在線連接的時(shí)候铲掐,我們將需要24 GB的內(nèi)存 (堆棧為4 KB)用于維持所有連接。 這還沒有計(jì)算為Channel結(jié)構(gòu)分配的內(nèi)存值桩,傳出的數(shù)據(jù)包c(diǎn)h.send和其他內(nèi)部字段消耗的內(nèi)存摆霉。

I/O?goroutines

我們來看看“reader”的實(shí)現(xiàn):

func?(c?*Channel)?reader()?{

//?We?make?a?bufferedreadtoreducereadsyscalls.

buf?:=?bufio.NewReader(c.conn)

for{

pkt,?_?:=?readPacket(buf)

c.handle(pkt)

}

}

這里我們使用bufio.Reader來減少read() syscalls的數(shù)量,并讀取與buf緩沖區(qū)大小一樣的數(shù)量。 在無限循環(huán)中斯入,我們期待新數(shù)據(jù)的到來砂碉。 請記住: 預(yù)計(jì)新數(shù)據(jù)將會(huì)來臨刻两。 我們稍后會(huì)回來增蹭。

我們將離開傳入數(shù)據(jù)包的解析和處理,因?yàn)閷ξ覀儗⒁懻摰膬?yōu)化不重要磅摹。 但是滋迈, buf現(xiàn)在值得我們注意:默認(rèn)情況下,它是4 KB户誓,這意味著我們需要另外12 GB內(nèi)存饼灿。 “writer”有類似的情況:

func?(c?*Channel)?writer()?{

//?We?make?buffered?writetoreduce?write?syscalls.

buf?:=?bufio.NewWriter(c.conn)

forpkt?:=?range?c.send?{

_?:=?writePacket(buf,?pkt)

buf.Flush()

}

}

我們遍歷c.send ,并將它們寫入緩沖區(qū)帝美。細(xì)心讀者已經(jīng)猜到的碍彭,我們的300萬個(gè)連接還將消耗12 GB的內(nèi)存。

HTTP

我們已經(jīng)有一個(gè)簡單的Channel實(shí)現(xiàn)悼潭,現(xiàn)在我們需要一個(gè)WebSocket連接才能使用庇忌。

注意:如果您不知道WebSocket如何工作〗⑼剩客戶端通過稱為升級的特殊HTTP機(jī)制切換到WebSocket協(xié)議皆疹。 在成功處理升級請求后,服務(wù)器和客戶端使用TCP連接來交換二進(jìn)制WebSocket幀占拍。 這是連接中的框架結(jié)構(gòu)的描述略就。

import?(

"net/http"

"some/websocket"

)

http.HandleFunc("/v1/ws",?func(w?http.ResponseWriter,?r?*http.Request)?{

conn,?_?:=?websocket.Upgrade(r,?w)

ch?:=?NewChannel(conn)

//...

})

請注意, http.ResponseWriter為bufio.Reader和bufio.Writer (使用4 KB緩沖區(qū))進(jìn)行內(nèi)存分配晃酒,用于*http.Request初始化和進(jìn)一步的響應(yīng)寫入表牢。

無論使用什么WebSocket庫,在成功響應(yīng)升級請求后掖疮, 服務(wù)器在responseWriter.Hijack()調(diào)用之后初茶,連同TCP連接一起接收 I/O緩沖區(qū)。

提示:在某些情況下浊闪, go:linkname 可用于 通過調(diào)用 net/http.putBufio{Reader,Writer} 將緩沖區(qū)返回到 net/http 內(nèi) 的 sync.Pool 恼布。

因此,我們需要另外24 GB的內(nèi)存來維持300萬個(gè)鏈接搁宾。

所以折汞,我們的程序即使什么都沒做,也需要72G內(nèi)存盖腿。

優(yōu)化

我們來回顧介紹部分中談到的內(nèi)容爽待,并記住用戶連接的行為损同。 切換到WebSocket之后,客戶端發(fā)送一個(gè)包含相關(guān)事件的數(shù)據(jù)包鸟款,換句話說就是訂閱事件膏燃。 然后(不考慮諸如ping/pong等技術(shù)信息),客戶端可能在整個(gè)連接壽命中不發(fā)送任何其他信息何什。

連接壽命可能是幾秒到幾天组哩。

所以在最多的時(shí)候,我們的Channel.reader()和Channel.writer()正在等待接收或發(fā)送數(shù)據(jù)的處理处渣。 每個(gè)都有4 KB的I/O緩沖區(qū)伶贰。

現(xiàn)在很明顯,某些事情可以做得更好罐栈,不是嗎?

Netpoll

你還記得bufio.Reader.Read()內(nèi)部黍衙,Channel.reader()實(shí)現(xiàn)了在沒有新數(shù)據(jù)的時(shí)候conn.read()會(huì)被鎖。如果連接中有數(shù)據(jù)荠诬,Go運(yùn)行時(shí)“喚醒”我們的goroutine并允許它讀取下一個(gè)數(shù)據(jù)包琅翻。 之后,goroutine再次鎖定浅妆,期待新的數(shù)據(jù)望迎。 讓我們看看Go運(yùn)行時(shí)如何理解goroutine必須被“喚醒”障癌。 如果我們看看conn.Read()實(shí)現(xiàn) 凌外,我們將在其中看到net.netFD.Read()調(diào)用 :

//?net/fd_unix.go

func?(fd?*netFD)Read(p?[]byte)?(nint,?err?error)?{

//...

for{

n,?err?=?syscall.Read(fd.sysfd,?p)

if?err?!=?nil?{

n?=?0

if?err?==?syscall.EAGAIN?{

if?err?=?fd.pd.waitRead();?err?==?nil?{

continue

}

}

}

//...

break

}

//...

}

Go在非阻塞模式下使用套接字。 EAGAIN表示涛浙,套接字中沒有數(shù)據(jù)康辑,并且在從空套接字讀取時(shí)不會(huì)被鎖定,操作系統(tǒng)將控制權(quán)返還給我們轿亮。

我們從連接文件描述符中看到一個(gè)read()系統(tǒng)調(diào)用疮薇。 如果讀取返回EAGAIN錯(cuò)誤 ,則運(yùn)行時(shí)會(huì)使pollDesc.waitRead()調(diào)用 :

//?net/fd_poll_runtime.go

func?(pd?*pollDesc)?waitRead()?error?{

returnpd.wait('r')

}

func?(pd?*pollDesc)?wait(modeint)?error?{

res?:=?runtime_pollWait(pd.runtimeCtx,?mode)

//...

}

如果我們深入挖掘 我注,我們將看到netpoll是使用Linux中的epoll和BSD中的kqueue來實(shí)現(xiàn)的按咒。 為什么不使用相同的方法來進(jìn)行連接? 我們可以分配一個(gè)讀緩沖區(qū),只有在真正有必要時(shí)才使用goroutine:當(dāng)套接字中有真實(shí)可讀的數(shù)據(jù)時(shí)但骨。

在github.com/golang/go上励七, 導(dǎo)出netpoll函數(shù)有問題 。

擺脫goroutines

假設(shè)我們有Go的netpoll實(shí)現(xiàn) 奔缠。 現(xiàn)在我們可以避免使用內(nèi)部緩沖區(qū)啟動(dòng)Channel.reader() goroutine掠抬,并在連接中訂閱可讀數(shù)據(jù)的事件:

ch?:=?NewChannel(conn)

//?Make?conntobe?observedbynetpoll?instance.

poller.Start(conn,?netpoll.EventRead,?func()?{

//?We?spawn?goroutine?heretoprevent?poller?wait?loop

//tobecome?locked?during?receiving?packetfromch.

go?Receive(ch)

})

//?Receive?reads?a?packetfromconnandhandles?it?somehow.

func?(ch?*Channel)?Receive()?{

buf?:=?bufio.NewReader(ch.conn)

pkt?:=?readPacket(buf)

c.handle(pkt)

}

使用Channel.writer()更容易,因?yàn)橹挥挟?dāng)我們要發(fā)送數(shù)據(jù)包時(shí)校哎,我們才能運(yùn)行g(shù)oroutine并分配緩沖區(qū):

func?(ch?*Channel)?Send(p?Packet)?{

if?c.noWriterYet()?{

go?ch.writer()

}

ch.send?<-?p

}

請注意两波,當(dāng)操作系統(tǒng)在 write() 系統(tǒng)調(diào)用時(shí)返回 EAGAIN 時(shí)瞳步,我們不處理這種情況 。 對于這種情況腰奋,我們傾向于Go運(yùn)行時(shí)那樣處理单起。 如果需要,它可以以相同的方式來處理劣坊。

從ch.send (一個(gè)或幾個(gè))讀出傳出的數(shù)據(jù)包后馏臭,writer將完成其操作并釋放goroutine棧和發(fā)送緩沖區(qū)。

完美! 通過擺脫兩個(gè)連續(xù)運(yùn)行的goroutine中的堆棧和I/O緩沖區(qū)讼稚,我們節(jié)省了48 GB 括儒。

資源控制

大量的連接不僅涉及高內(nèi)存消耗。 在開發(fā)服務(wù)器時(shí)锐想,我們會(huì)經(jīng)歷重復(fù)的競爭條件和死鎖帮寻,常常是所謂的自動(dòng)DDoS,這種情況是當(dāng)應(yīng)用程序客戶端肆意嘗試連接到服務(wù)器赠摇,從而破壞服務(wù)器固逗。

例如,如果由于某些原因我們突然無法處理ping/pong消息,但是空閑連接的處理程序會(huì)關(guān)閉這樣的連接(假設(shè)連接斷開置媳,因此沒有提供數(shù)據(jù))洪己,客戶端會(huì)不斷嘗試連接,而不是等待事件贝攒。

如果鎖定或超載的服務(wù)器剛剛停止接受新連接,并且負(fù)載均衡器(例如时甚,nginx)將請求都傳遞給下一個(gè)服務(wù)器實(shí)例隘弊,那壓力將是巨大的。

此外荒适,無論服務(wù)器負(fù)載如何梨熙,如果所有客戶端突然想要以任何原因發(fā)送數(shù)據(jù)包(大概是由于錯(cuò)誤原因),則先前節(jié)省的48 GB將再次使用刀诬,因?yàn)槲覀儗?shí)際恢復(fù)到初始狀態(tài)goroutine和并對每個(gè)連接分配緩沖區(qū)咽扇。

Goroutine池

我們可以使用goroutine池來限制同時(shí)處理的數(shù)據(jù)包數(shù)量。 這是一個(gè)go routine池的簡單實(shí)現(xiàn):

package?gopool

func?New(sizeint)?*Pool?{

return&Pool{

work:?make(chan?func()),

sem:??make(chan?struct{},size),

}

}

func?(p?*Pool)?Schedule(task?func())?error?{

select{

casep.work<-?task:

casep.sem?<-?struct{}{}:

go?p.worker(task)

}

}

func?(p?*Pool)?worker(task?func())?{

defer?func()?{?<-p.sem?}

for{

task()

task?=?<-p.work

}

}

現(xiàn)在我們的netpoll代碼如下:

pool?:=?gopool.New(128)

poller.Start(conn,?netpoll.EventRead,?func()?{

//?We?will?block?poller?wait?loopwhen

//allpool?workers?are?busy.

pool.Schedule(func()?{

Receive(ch)

})

})

所以現(xiàn)在我們讀取數(shù)據(jù)包可以在池中使用了空閑的goroutine陕壹。

同樣质欲,我們將更改Send() :

pool?:=?gopool.New(128)

func?(ch?*Channel)?Send(p?Packet)?{

if?c.noWriterYet()?{

pool.Schedule(ch.writer)

}

ch.send?<-?p

}

而不是go ch.writer() ,我們想寫一個(gè)重用的goroutine帐要。 因此把敞,對于N goroutines池,我們可以保證在N請求同時(shí)處理并且到達(dá)N + 1我們不會(huì)分配N + 1緩沖區(qū)進(jìn)行讀取榨惠。 goroutine池還允許我們限制新連接的Accept()和Upgrade() 奋早,并避免大多數(shù)情況下被DDoS打垮盛霎。

零拷貝升級

讓我們從WebSocket協(xié)議中偏離一點(diǎn)。 如前所述耽装,客戶端使用HTTP升級請求切換到WebSocket協(xié)議愤炸。 協(xié)議是樣子:

GET?/ws?HTTP/1.1

Host:?mail.ru

Connection:?Upgrade

Sec-Websocket-Key:?A3xNe7sEB9HixkmBhVrYaA==

Sec-Websocket-Version:?13

Upgrade:?websocket

HTTP/1.1?101?Switching?Protocols

Connection:?Upgrade

Sec-Websocket-Accept:?ksu0wXWG+YmkVx+KQR2agP0cQn4=

Upgrade:?websocket

也就是說,在我們的例子中掉奄,我們需要HTTP請求和header才能切換到WebSocket協(xié)議规个。 這個(gè)知識(shí)點(diǎn)和http.Request的內(nèi)部實(shí)現(xiàn)表明我們可以做優(yōu)化。我們會(huì)在處理HTTP請求時(shí)拋棄不必要的內(nèi)存分配和復(fù)制姓建,并放棄標(biāo)準(zhǔn)的net/http服務(wù)器诞仓。

例如, http.Request 包含一個(gè)具有相同名稱的頭文件類型的字段速兔,它通過將數(shù)據(jù)從連接復(fù)制到值字符串而無條件填充所有請求頭墅拭。 想像一下這個(gè)字段中可以保留多少額外的數(shù)據(jù),例如大型Cookie頭涣狗。

但是要做什么呢?

WebSocket實(shí)現(xiàn)

不幸的是谍婉,在我們的服務(wù)器優(yōu)化時(shí)存在的所有庫都允許我們對標(biāo)準(zhǔn)的net/http服務(wù)器進(jìn)行升級。 此外镀钓,所有庫都不能使用所有上述讀寫優(yōu)化穗熬。 為使這些優(yōu)化能夠正常工作,我們必須使用一個(gè)相當(dāng)?shù)图墑e的API來處理WebSocket丁溅。 要重用緩沖區(qū)唤蔗,我們需要procotol函數(shù)看起來像這樣:

func?ReadFrame(io.Reader)?(Frame,?error)

func?WriteFrame(io.Writer,?Frame)?error

如果我們有一個(gè)這樣的API的庫,我們可以從連接中讀取數(shù)據(jù)包唧瘾,如下所示(數(shù)據(jù)包寫入看起來差不多):

//?getReadBuf,?putReadBuf?are?intendedto

//?reuse?*bufio.Reader?(withsync.Poolforexample).

func?getReadBuf(io.Reader)?*bufio.Reader

func?putReadBuf(*bufio.Reader)

//?readPacket?must?be?calledwhendata?could?bereadfromconn.

func?readPacket(conn?io.Reader)?error?{

buf?:=?getReadBuf()

defer?putReadBuf(buf)

buf.Reset(conn)

frame,?_?:=?ReadFrame(buf)

parsePacket(frame.Payload)

//...

}

簡而言之措译,現(xiàn)在是制作我們自己庫的時(shí)候了。

github.com/gobwas/ws

為了避免將協(xié)議操作邏輯強(qiáng)加給用戶饰序,我們編寫了WS庫。 所有讀寫方法都接受標(biāo)準(zhǔn)的io.Reader和io.Writer接口规哪,可以使用或不使用緩沖或任何其他I/O包裝器求豫。

除了來自標(biāo)準(zhǔn)net/http升級請求之外, ws支持零拷貝升級 诉稍,升級請求的處理和切換到WebSocket蝠嘉,而無需內(nèi)存分配或復(fù)制。 ws.Upgrade()接受io.ReadWriter ( net.Conn實(shí)現(xiàn)了這個(gè)接口)杯巨。 換句話說蚤告,我們可以使用標(biāo)準(zhǔn)的net.Listen()并將接收到的連接從ln.Accept()立即傳遞給ws.Upgrade() 。 該庫可以復(fù)制任何請求數(shù)據(jù)以供將來在應(yīng)用程序中使用(例如服爷, Cookie以驗(yàn)證會(huì)話)杜恰。

以下是升級請求處理的基準(zhǔn) :標(biāo)準(zhǔn)net/http服務(wù)器與net.Listen()加零拷貝升級:

BenchmarkUpgradeHTTP?5156?ns/op?8576?B/op?9?allocs/op

BenchmarkUpgradeTCP?973?ns/op?0?B/op?0?allocs/op

切換到ws和零拷貝升級節(jié)省了另外24 GB內(nèi)存 - 這是由net/http處理程序請求處理時(shí)為I/O緩沖區(qū)分配的空間获诈。

概要

讓我們結(jié)合代碼告訴你我們做的優(yōu)化。

讀取內(nèi)部緩沖區(qū)的goroutine是非常昂貴的心褐。 解決方案 :netpoll(epoll舔涎,kqueue); 重用緩沖區(qū)。

寫入內(nèi)部緩沖區(qū)的goroutine是非常昂貴的逗爹。 解決方案 :必要時(shí)啟動(dòng)goroutine; 重用緩沖區(qū)亡嫌。

DDOS,netpoll將無法工作掘而。 解決方案 :重新使用數(shù)量限制的goroutines挟冠。

net/http不是處理升級到WebSocket的最快方法。 解決方案 :在連接上使用零拷貝升級袍睡。

這就是服務(wù)器代碼的樣子:

import?(

"net"

"github.com/gobwas/ws"

)

ln,?_?:=?net.Listen("tcp",":8080")

for{

//?Trytoaccept?incomingconnectioninsidefreepool?worker.

//?If?therenofreeworkersfor1ms,?donotaccept?anythingandtry?later.

//?This?will?help?ustoprevent?many?self-ddosoroutofresource?limit?cases.

err?:=?pool.ScheduleTimeout(time.Millisecond,?func()?{

conn?:=?ln.Accept()

_?=?ws.Upgrade(conn)

//?Wrap?WebSocketconnectionwithour?Channel?struct.

//?This?will?help?ustohandle/send?our?app's?packets.

ch?:=?NewChannel(conn)

//?Waitforincoming?bytesfromconnection.

poller.Start(conn,?netpoll.EventRead,?func()?{

//?Donotcrossthe?resource?limits.

pool.Schedule(func()?{

//Readandhandle?incoming?packet(s).

ch.Recevie()

})

})

})

if?err?!=?nil?{

time.Sleep(time.Millisecond)

}

}

結(jié)論

過早優(yōu)化是萬惡之源圃郊。 Donald Knuth

當(dāng)然,上述優(yōu)化是有意義的女蜈,但并非所有情況都如此持舆。 例如,如果可用資源(內(nèi)存伪窖,CPU)和在線連接數(shù)之間的比例相當(dāng)高(服務(wù)器很閑)逸寓,則優(yōu)化可能沒有任何意義。 但是覆山,您可以從哪里需要改進(jìn)以及改進(jìn)內(nèi)容中受益匪淺竹伸。

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市簇宽,隨后出現(xiàn)的幾起案子勋篓,更是在濱河造成了極大的恐慌,老刑警劉巖魏割,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件譬嚣,死亡現(xiàn)場離奇詭異,居然都是意外死亡钞它,警方通過查閱死者的電腦和手機(jī)拜银,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遭垛,“玉大人尼桶,你說我怎么就攤上這事【庖牵” “怎么了泵督?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長庶喜。 經(jīng)常有香客問我小腊,道長救鲤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任溢豆,我火速辦了婚禮蜒简,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘漩仙。我一直安慰自己搓茬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布队他。 她就那樣靜靜地躺著卷仑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麸折。 梳的紋絲不亂的頭發(fā)上锡凝,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機(jī)與錄音垢啼,去河邊找鬼窜锯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛芭析,可吹牛的內(nèi)容都是我干的锚扎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼馁启,長吁一口氣:“原來是場噩夢啊……” “哼驾孔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起惯疙,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤翠勉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后霉颠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體对碌,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年掉分,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俭缓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酥郭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出愿吹,到底是詐尸還是另有隱情不从,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布犁跪,位于F島的核電站椿息,受9級特大地震影響歹袁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜寝优,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一条舔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乏矾,春花似錦孟抗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捷沸,卻和暖如春摊沉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痒给。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工说墨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苍柏。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓尼斧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親序仙。 傳聞我的和親對象是個(gè)殘疾皇子突颊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

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