看完這篇文章涨缚,再次遇到Jedis「Redis客戶端」異常相信你不再怕了轧粟!

本文導(dǎo)讀:

[1] 疫情當(dāng)前

[2] 應(yīng)用異常監(jiān)控

[3] Redis客戶端異常分析

[4] Redis客戶端問題引導(dǎo)分析

[5] 站在Redis客戶端視角分析

[6] 站在Redis服務(wù)端視角分析

[7] 資源池生產(chǎn)配置合理性分析

[8] 本文總結(jié)


[1] 疫情當(dāng)前

為響應(yīng)國家抗擊疫情的號召,全國有過億的企業(yè)職員選擇了遠(yuǎn)程辦公脓魏,IT科技大廠們也紛紛開啟了VPN模式兰吟,保障企業(yè)運(yùn)營。

既然這樣茂翔,我們該怎么做呢混蔼?苦逼的程序猿們只能加油干!來張圖看看老板們擔(dān)心的是什么珊燎?

BOSS擔(dān)心的是什么惭嚣?

不好意思,大BOSS們首先擔(dān)心的可不是員工身心健康悔政,而是工作效率的提升哦~

但是晚吞,據(jù)業(yè)內(nèi)人士預(yù)估,新冠肺炎疫情很有可能激發(fā)國內(nèi)企業(yè)信息化建設(shè)提速谋国。

對于企業(yè)而言槽地,值得期待的是,『遠(yuǎn)程辦公』讓企業(yè)看到辦公形式的更多可能性,有助于企業(yè)未來辦公形式的新嘗試捌蚊。

更為重要的是集畅,企業(yè)可以此為契機(jī),提升企業(yè)自身信息化建設(shè)缅糟,增強(qiáng)團(tuán)隊(duì)凝聚力與協(xié)同性挺智,在『危機(jī)』中平穩(wěn)運(yùn)行,甚至是發(fā)現(xiàn)機(jī)會溺拱。

筆者也不例外逃贝,本周已依照公司要求,開啟了遠(yuǎn)程辦公模式迫摔,本周的感受來說,工作效率上肯定會收到一些影響泥从。但因我們一季度目標(biāo)明確句占,所以每天可以按部就班按計(jì)劃如期進(jìn)行。

而且躯嫉,也因?yàn)橐咔榈挠绊懮春妫叽蠊冒舜笠潭急槐镌诩依锪耍竟灸扯说姆?wù) DAU 最近一段時間逆襲不斷上漲祈餐,付費(fèi)會員收入也隨之增長不少擂啥,對于我們來說算是個好消息。

在家遠(yuǎn)程辦公帆阳,不給國家添亂就好哇??哺壶!

接下來,我們繼續(xù)聊聊線上環(huán)境遇到的一個問題以及分析過程蜒谤。


[2] 應(yīng)用異常監(jiān)控

這不山宾,項(xiàng)目中有一個Redis客戶端的異常在疫情期間,出現(xiàn)在了你的面前鳍徽,雖然該異常是偶發(fā)资锰,有必要仔細(xì)分析下該異常出現(xiàn)的原由。

具體異常信息如下所示:

ISSUE異常
異常信息

大家看截圖展示的異常信息阶祭,是不是很想問绷杜,這個異常顯示怎么這么「友好」?

沒錯濒募,是通過一款非常好用的實(shí)時異常監(jiān)控工具:Sentry來監(jiān)控到的鞭盟,這款工具在我們的項(xiàng)目中已經(jīng)接入并使用了很長一段時間了,對異常的監(jiān)控非常到位萨咳。

比如針對發(fā)生的異常懊缺,將具體訪問的整個URL、客戶端上報(bào)的信息、設(shè)備型號等信息作為TAGS收集上來鹃两,盡情的展示給你遗座,讓你盡快結(jié)合這些信息快速定位問題。

該服務(wù)部署在k8s容器環(huán)境下俊扳,在截圖中TAGS中途蒋,也能夠看到 server_name 代表的是Pod的hostname,這樣便能快速知道是哪個Pod出現(xiàn)的問題馋记,進(jìn)入容器平臺直接進(jìn)入到Pod內(nèi)部進(jìn)一步詳細(xì)分析号坡。

強(qiáng)烈推薦大家項(xiàng)目中接入Sentry,因?yàn)樗坏泻芎糜玫漠惓V卫砥脚_梯醒,更為重要的是Sentry支持跨語言客戶端宽堆,比如支持Java、Andriod茸习、C++畜隶、Python、Go等大部分語言号胚,現(xiàn)成的客戶端易于接入和使用籽慢。

我想只要你的服務(wù)不卡死,如果出現(xiàn)問題猫胁,項(xiàng)目里輸出的日志中總會有一些 ERROR 級別的日志出現(xiàn)的箱亿,那么此時就交給Sentry,它會及時向你發(fā)出告警(郵件...)通知你弃秆。

[3] Redis客戶端異常分析

本項(xiàng)目中使用的Jedis(Redis的Java客戶端)届惋,提示異常信息 JedisConnectionException Unexpected end of stream,在使用Redis過程中我還很少遇到這個問題驾茴,既然遇到了盼樟,這是不是緣分啊 :)

其實(shí)異常棧中已經(jīng)給出了詳細(xì)的調(diào)用過程,在哪里出現(xiàn)的問題锈至,順藤摸瓜根據(jù)這個堆棧去查找線索晨缴。

file

如何找到更為詳細(xì)的堆棧?別擔(dān)心峡捡,在上圖中點(diǎn)擊下 raw 會出現(xiàn)完整的異常堆棧的文本信息击碗,也方便復(fù)制拷貝出來分析。

如下所示:

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
    at redis.clients.jedis.Protocol.read(Protocol.java:215)
    at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
    at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
    at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
    at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
    at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
...

根據(jù)以上信息们拙,發(fā)現(xiàn)是調(diào)用到 BinaryJedis.auth 驗(yàn)證Redis密碼時出錯的稍途,而且有 GenericObjectPool.borrowObject 表示借用對象的方法,GenericObjectPool是Apache開源項(xiàng)目的線程池砚婆,在很多開源項(xiàng)目中都能看到它的身影械拍。

說明是在伸手向資源池索要對象時突勇,在資源池里沒有拿到對象,那就只能創(chuàng)建一個坷虑,調(diào)用了 GenericObjectPool.create 甲馋,調(diào)用具體實(shí)現(xiàn)方法 JedisFactory.makeObject 創(chuàng)建Jedis對象時出錯的。

哦迄损?這么一看定躏,簡單一想猜測下,在創(chuàng)建新的對象時驗(yàn)證密碼時芹敌,可能因網(wǎng)絡(luò)不穩(wěn)定鸵熟,Redis-Server沒有正常返回異常信息導(dǎo)致的吹泡。

[4] Redis客戶端問題引導(dǎo)分析

在上文中陆盘,我們在異常堆棧中發(fā)現(xiàn)使用了線程池株汉,如果不使用資源池管理這些對象,會發(fā)生什么情況液茎?

如下所示矾削,每次使用Redis連接都會在客戶端重新創(chuàng)建Jedis對象,創(chuàng)建Jedis對象后豁护,連接Redis Server,這個過程會建立TCP連接(三次握手)欲间,完成操作后楚里,斷開TCP連接(四次揮手),當(dāng)遇到并發(fā)量稍大的請求猎贴,就會吃不消了班缎,消耗資源的同時,無法滿足應(yīng)用性能上的要求她渴。

不使用資源池

如果使用了線程池达址,如下圖所示的樣子:

使用了資源池

按需在資源池中初始化一定數(shù)量的對象,當(dāng)有客戶端請求到達(dá)時趁耗,從資源池里獲取對象沉唠,對象使用完成,再將對象丟回到資源池里苛败,給其他客戶端使用满葛。

這就是所謂的 「池化技術(shù)」,相信在你的項(xiàng)目中一定會用到的罢屈,比如數(shù)據(jù)庫連接池嘀韧、應(yīng)用服務(wù)器的線程池等等。

池化技術(shù)的優(yōu)勢就是能夠復(fù)用池中的對象缠捌,比如上述圖示中锄贷,避免了分配內(nèi)存和創(chuàng)建堆中對象的開銷;避免了因?qū)ο笾貜?fù)創(chuàng)建,進(jìn)而能避免了TCP連接的建立和斷開的資源開銷谊却;避免了釋放內(nèi)存和銷毀堆中對象的開銷柔昼,進(jìn)而減少垃圾收集器的負(fù)擔(dān);避免內(nèi)存抖動因惭,不必重復(fù)初始化對象狀態(tài)岳锁。

當(dāng)然,我們也可以自己來實(shí)現(xiàn)蹦魔,但是如果想寫出比較完善的對象池的資源管理功能激率,也需要花費(fèi)不少的精力,考慮的細(xì)節(jié)也是非常多的勿决。

站在巨人的肩膀上乒躺,在前文中提到的Jedis內(nèi)部是由 Apache Common Pool2 開源工具包來實(shí)現(xiàn)的,很多開源項(xiàng)目中應(yīng)用也是很廣泛的低缩。

而Jedis客戶端的很多參數(shù)都是來源于Apache Common Pool2的底層實(shí)現(xiàn)過程所需要的參數(shù)嘉冒。

這也是Jedis或者說一些Redis客戶端給用戶使用簡單的原因,但是簡單的同時咆繁,我們也要根據(jù)不同場景去合理配置好連接池的參數(shù)讳推,不合理的配置加上不合理的功能使用,可能會引起很多的問題玩般。

在回歸到前文的最開始的異常银觅,這些異常跟什么有關(guān)系呢?

從圖示中坏为,我們能知道客戶端使用了線程池究驴,可能跟線程池有關(guān)系;創(chuàng)建對象時匀伏,auth 驗(yàn)證密碼時出現(xiàn)了問題洒忧,而驗(yàn)證密碼前已經(jīng)發(fā)起了 connect 連接了,說明連接到了Redis Server够颠,所以 Redis Server 也脫離不了干系的熙侍。

跟 Redis Client 有關(guān)系,我們就要進(jìn)一步分析客戶端的參數(shù)摧找,連接池的參數(shù)是否合理核行。

跟 Redis Server 有關(guān)系,就要結(jié)合問題分析下服務(wù)端的參數(shù)蹬耘,相關(guān)配置參數(shù)是否合理芝雪。

[5] 站在Redis客戶端視角分析

既然講到了Redis客戶端,首先想到的是從客戶端配置的參數(shù)入手综苔。

直接從參數(shù)入手惩系,不如我們可以先接著對異常棧的分析位岔,從對象資源池入手去分析,看看這個對象池到底是怎樣管理的堡牡?

1抒抬、資源池對象管理

資源池對象

資源池中創(chuàng)建對象的過程如上圖所示。

Apache Common Pool2 既然是一個通用的資源池管理框架晤柄,內(nèi)部會定義好資源池的接口和規(guī)范擦剑,具體創(chuàng)建對象實(shí)現(xiàn)交由具體框架來實(shí)現(xiàn)。

1)從資源池獲取對象芥颈,會調(diào)用ObjectPool#borrowObject惠勒,如果沒有空閑對象,則調(diào)用PooledObjectFactory#makeObject創(chuàng)建對象爬坑,JedisFactory是具體的實(shí)現(xiàn)類纠屋。

2)創(chuàng)建完對象放到資源池中,返回給客戶端使用盾计。

3)使用完對象會調(diào)用ObjectPool#returnObject售担,其內(nèi)部會校驗(yàn)一些條件是否滿足,驗(yàn)證通過署辉,對象歸還給資源池族铆。

4)條件驗(yàn)證不通過,比如資源池已關(guān)閉哭尝、對象狀態(tài)不正確(Jedis連接失效)骑素、已超出最大空閑資源數(shù),則會調(diào)用 PooledObjectFactory#destoryObject從資源池中銷毀對象刚夺。

CommonPool2設(shè)計(jì)類圖

ObjectPool 和 KeyedObjectPool 是兩個基礎(chǔ)接口。
從定義的接口名上也能做下區(qū)分末捣,ObjectPool 接口資源池列表里存儲都是對象侠姑,默認(rèn)實(shí)現(xiàn)類GenericObjectPool,KeyedObjectPool 接口用鍵值對的方式維護(hù)對象箩做,默認(rèn)實(shí)現(xiàn)類是GenericKeyedObjectPool莽红。在實(shí)現(xiàn)過程會有很多公共的功能實(shí)現(xiàn),放在了BaseGenericObjectPool基礎(chǔ)實(shí)現(xiàn)類當(dāng)中邦邦。

SoftReferenceObjectPool 是一個比較特殊的實(shí)現(xiàn)安吁,在這個對象池實(shí)現(xiàn)中,每個對象都會被包裝到一個 SoftReference 中燃辖。SoftReference 軟引用鬼店,能夠在JVM GC過程中當(dāng)內(nèi)存不足時,允許垃圾回收機(jī)制在需要釋放內(nèi)存時回收對象池中的對象黔龟,避免內(nèi)存泄露的問題

PooledObject類設(shè)計(jì)圖

PooledObject 是池化對象的接口定義妇智,池化的對象都會封裝在這里滥玷。DefaultPooledObject 是PooledObject 接口缺省實(shí)現(xiàn)類,PooledSoftReference 使用 SoftReference 封裝了對象巍棱,供SoftReferenceObjectPool 使用惑畴。

2、對象池參數(shù)詳解

查看對象池的參數(shù)配置航徙,一種方式是直接查找代碼或者官網(wǎng)文檔中的說明去查看如贷,另外介紹一種更為直觀的方式,因?yàn)?Common Pool2 工具資源池的管理都接入到 JMX 中到踏,所以可以通過如 Jconsole 等工具去查看暴露的屬性和操作杠袱。

第一種方式:

查找對應(yīng)配置類:

配置類

在 GenericObjectPoolConfig 和 BaseObjectPoolConfig 配置類對外提供的 setter 方法便是配置參數(shù),并且代碼里都有詳細(xì)的注釋說明夭禽。

setter方法

第二種方式:

前提是你的應(yīng)用暴露了 JMX 的端口和IP霞掺,允許外部連接。

JVM 參數(shù)如下所示:

-Dcom.sun.management.jmxremote 
-Djava.rmi.server.hostname=IP地址
-Dcom.sun.management.jmxremote.port=端口
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.ssl=false
JMX工具查看屬性

以上使用的是 Jconsole 工具類讹躯,點(diǎn)擊 MBean 在左側(cè)找到 org.apache.commons.pool2#GenericObjectPool#pool2 點(diǎn)擊屬性可以看到該類的所有屬性信息菩彬,其中除包括核心的配置屬性之外,還包括一些資源池的統(tǒng)計(jì)屬性潮梯。

核心配置屬性:

這些都是重點(diǎn)關(guān)注的屬性骗灶,也是對外提供的可配置參數(shù)。

1)minIdle 資源池確保最少空閑的連接數(shù)秉馏,默認(rèn)值: 0

2)maxIdle 資源池允許最大空閑的連接數(shù)耙旦,默認(rèn)值: 8

3)maxTotal 資源池中最大連接數(shù),默認(rèn)值:8

4)maxWaitMillis 當(dāng)資源池連接用盡后萝究,調(diào)用者的最大等待時間免都,單位是毫秒,默認(rèn)值:-1帆竹,建議設(shè)置合理的值

5)testOnBorrow 向資源池借用連接時绕娘,是否做連接有效性檢測,無效連接會被移除栽连,默認(rèn)值:false 险领,業(yè)務(wù)量很大時建議為false,因?yàn)闀嘁淮蝡ing的開銷

6)testOnCreate 創(chuàng)建新的資源連接后秒紧,是否做連接有效性檢測绢陌,無效連接會被移除,默認(rèn)值:false 熔恢,業(yè)務(wù)量很大時建議為false脐湾,因?yàn)闀嘁淮蝡ing的開銷

7)testOnReturn 向資源池歸還連接時,是否做連接有效性檢測叙淌,無效連接會被移除沥割,默認(rèn)值:false耗啦,業(yè)務(wù)量很大時建議為false,因?yàn)闀嘁淮蝡ing的開銷

8) testWhileIdle 是否開啟空閑資源監(jiān)測机杜,默認(rèn)值:false

9)blockWhenExhausted 當(dāng)資源池用盡后帜讲,調(diào)用者是否要等待。默認(rèn)值:true椒拗,當(dāng)為true時似将,maxWaitMillis參數(shù)才會生效,建議使用默認(rèn)值

10)lifo 資源池里放池對象的方式蚀苛,LIFO Last In First Out 后進(jìn)先出在验,true(默認(rèn)值),表示放在空閑隊(duì)列最前面堵未,false:放在空閑隊(duì)列最后面

空閑資源監(jiān)測配置屬性

當(dāng)需要對空閑資源進(jìn)行監(jiān)測時腋舌, testWhileIdle 參數(shù)開啟后與下列幾個參數(shù)組合完成監(jiān)測任務(wù)。

1)timeBetweenEvictionRunsMillis 空閑資源的檢測周期渗蟹,單位為毫秒块饺,默認(rèn)值:-1,表示不檢測雌芽,建議設(shè)置一個合理的值授艰,周期性運(yùn)行監(jiān)測任務(wù)

2)minEvictableIdleTimeMillis 資源池中資源最小空閑時間,單位為毫秒世落,默認(rèn)值:30分鐘(1000 * 60L * 30L)淮腾,當(dāng)達(dá)到該值后空閑資源將被移除,建議根據(jù)業(yè)務(wù)自身設(shè)定

3)numTestsPerEvictionRun 做空閑資源檢測時屉佳,每次的采樣數(shù)谷朝,默認(rèn)值:3,可根據(jù)自身應(yīng)用連接數(shù)進(jìn)行微調(diào)武花,如果設(shè)置為 -1徘禁,表示對所有連接做空閑監(jiān)測

3、空閑資源監(jiān)測源碼剖析

在資源池初始化之后髓堪,有個空閑資源監(jiān)測任務(wù)流程如下:

空閑資源監(jiān)測任務(wù)

對應(yīng)源代碼:

創(chuàng)建資源池對象時,在構(gòu)造函數(shù)中初始化配合和任務(wù)的娘荡。

this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
public GenericObjectPool(final PooledObjectFactory<T> factory,
            final GenericObjectPoolConfig config) {

        super(config, ONAME_BASE, config.getJmxNamePrefix());

        if (factory == null) {
            jmxUnregister(); // tidy up
            throw new IllegalArgumentException("factory may not be null");
        }
        this.factory = factory;
        // 創(chuàng)建空閑資源鏈表
        idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());
        // 初始化配置
        setConfig(config);
        
                // 開啟資源監(jiān)測任務(wù)
        startEvictor(getTimeBetweenEvictionRunsMillis());
    }
        
final void startEvictor(final long delay) {
        synchronized (evictionLock) {
                    // 當(dāng)資源池關(guān)閉時會觸發(fā)干旁,取消evictor任務(wù)
            if (null != evictor) {
                EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
                evictor = null;
                evictionIterator = null;
            }
            if (delay > 0) {
                            // 啟動evictor任務(wù)
                evictor = new Evictor();
                                // 開啟定時任務(wù)
                EvictionTimer.schedule(evictor, delay, delay);
            }
        }
    }

Eviector 是個TimerTask,通過啟用的調(diào)度器炮沐,每間隔 timeBetweenEvictionRunsMillis 運(yùn)行一次争群。

class Evictor extends TimerTask {
@Override
public void run() {
   final ClassLoader savedClassLoader =
                   Thread.currentThread().getContextClassLoader();
   try {
        ...

       // Evict from the pool
       evict();

    // Ensure min idle num
    ensureMinIdle();

   } finally {
           // Restore the previous CCL
           Thread.currentThread().setContextClassLoader(savedClassLoader);
   }
}
}

evict() 移除方法源碼:

@Override
public void evict() throws Exception {
        assertOpen();

    if (idleObjects.size() > 0) {

        PooledObject<T> underTest = null;
        // 獲取清除策略
        final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();

        synchronized (evictionLock) {
            final EvictionConfig evictionConfig = new EvictionConfig(
                            getMinEvictableIdleTimeMillis(),
                            getSoftMinEvictableIdleTimeMillis(),
                            getMinIdle());

            final boolean testWhileIdle = getTestWhileIdle();

            for (int i = 0, m = getNumTests(); i < m; i++) {
                 // ... 省略部分代碼
                 // underTest 代表每一個資源
                    boolean evict;

                    evict = evictionPolicy.evict(evictionConfig, underTest,
                                    idleObjects.size());
                 // evict為true,銷毀對象
                    if (evict) {
                        destroy(underTest);
                        destroyedByEvictorCount.incrementAndGet();
                    } else {
                            // testWhileIdle為true校驗(yàn)資源有效性
                            if (testWhileIdle) {
                                boolean active = false;
                                try {
                                        factory.activateObject(underTest);
                                        active = true;
                                } catch (final Exception e) {
                                        destroy(underTest);
                                        destroyedByEvictorCount.incrementAndGet();
                                }
                                if (active) {
                                        if (!factory.validateObject(underTest)) {
                                                destroy(underTest);
                                                destroyedByEvictorCount.incrementAndGet();
                                        } else {
                                                try {
                                                        factory.passivateObject(underTest);
                                                } catch (final Exception e) {
                                                        destroy(underTest);
                                                        destroyedByEvictorCount.incrementAndGet();
                                                }
                                        }
                                    }
                            }
                         //...
                     }
                }
        }
    }
 // ...
}

代碼里的默認(rèn)策略 evictionPolicy大年,由 org.apache.commons.pool2.impl.DefaultEvictionPolicy 提供默認(rèn)實(shí)現(xiàn)换薄。

// DefaultEvictionPolicy#evict()
@Override
public boolean evict(final EvictionConfig config, final PooledObject<T> underTest,
                final int idleCount) {
  
if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
                config.getMinIdle() < idleCount) ||
                config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
        return true;
}
return false;
}

1)當(dāng)空閑資源列表大小超過 minIdle 最小空閑資源數(shù)時玉雾,并且資源配置的 idleSoftEvictTime 小于資源空閑時間,返回 true轻要。

EvictionConfig 配置初始化時复旬,idleSoftEvictTime 如果使用的默認(rèn)值 -1 < 0,則賦予值為 Long.MAX_VALUE冲泥。

2)當(dāng)檢測的資源空閑時間過期后驹碍,即大于資源池配置的最小空閑時間,返回true凡恍。表示這些資源處于空閑狀態(tài)志秃,該時間段內(nèi)一直未被使用到。

以上兩個滿足其中任一條件嚼酝,則會銷毀資源對象浮还。

ensureIdle() 方法源代碼:

private void ensureIdle(final int idleCount, final boolean always) throws Exception {
        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
                return;
        }
    // 資源池里保留idleCount(minIdle)最小資源數(shù)量
        while (idleObjects.size() < idleCount) {
                final PooledObject<T> p = create();
                if (p == null) {
                        // Can't create objects, no reason to think another call to
                        // create will work. Give up.
                        break;
                }
                if (getLifo()) {
                        idleObjects.addFirst(p);
                } else {
                        idleObjects.addLast(p);
                }
        }
        if (isClosed()) {
                // Pool closed while object was being added to idle objects.
                // Make sure the returned object is destroyed rather than left
                // in the idle object pool (which would effectively be a leak)
                clear();
        }
}

以上就是對線程池的基本原理和參數(shù)的分析。

4闽巩、線程池對象狀態(tài)

線程池對象的狀態(tài)定義在 PooledObjectState 钧舌,是個枚舉類型,有以下值:

IDLE 處于空閑狀態(tài)

ALLOCATED 被使用中

EVICTION 正在被Evictor驅(qū)逐器驗(yàn)證

VALIDATION 正在驗(yàn)證

INVALID 驅(qū)逐測試或驗(yàn)證失敗并將被銷毀

ABANDONED 被拋棄狀態(tài)又官,對象取出后延刘,很久未歸還

RETURNING 歸還到對象池中

一張圖來了解下線程池狀態(tài)機(jī)轉(zhuǎn)換:

線程池狀態(tài)

5、對象池初始化時機(jī)

思考個問題六敬,資源池里對象什么時候初始化進(jìn)去的碘赖?這里的資源池就是指上文圖中的 idleObjects 空閑資源對象緩存列表。是在創(chuàng)建對象時還是歸還對象時外构?

答案是歸還對象的時候普泡。

某些場景,啟動后可能會出現(xiàn)超時現(xiàn)象审编,因?yàn)槊看握埱蠖紩?chuàng)建新的資源撼班,這個過程會有一定的開銷。

應(yīng)用啟動后我們可以提前做下線程池資源的預(yù)熱垒酬,示例代碼如下:

List<Jedis> minIdleList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());

for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    Jedis jedis = null;
    try {
        jedis = pool.getResource();
        minIdleList.add(jedis);
        jedis.ping();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
    }
}

for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
    Jedis jedis = null;
    try {
        jedis = minIdleList.get(i);
        jedis.close();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    } finally {
    }
}

如果不了解原理砰嘁,可能以為上面的預(yù)熱代碼不大對吧,怎么獲取后又調(diào)用了 jedis.close() 呢勘究?字面上理解是把資源關(guān)閉了嘛矮湘。

一起看下線程池資源歸還對象的源碼就明白了。

GenericObjectPool#returnObject() 歸還對象方法源碼:

// GenericObjectPool#returnObject() 歸還方法
public void returnObject(final T obj) {
  // allObjects是存儲所有對象資源的地方
    final PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
  // ... 
    // 變更對象狀態(tài)
    synchronized(p) {
            final PooledObjectState state = p.getState();
            if (state != PooledObjectState.ALLOCATED) {
                    throw new IllegalStateException(
                                    "Object has already been returned to this pool or is invalid");
            }
            p.markReturning(); // Keep from being marked abandoned
    }

    final long activeTime = p.getActiveTimeMillis();
  // testOnReturn為true口糕,返還時驗(yàn)證資源有效性
    if (getTestOnReturn()) {
            if (!factory.validateObject(p)) {
                    try {
                            destroy(p);
                    } catch (final Exception e) {
                            swallowException(e);
                    }
                    try {
                            ensureIdle(1, false);
                    } catch (final Exception e) {
                            swallowException(e);
                    }
                    updateStatsReturn(activeTime);
                    return;
            }
    }
 // ...

    if (!p.deallocate()) {
            throw new IllegalStateException(
                            "Object has already been returned to this pool or is invalid");
    }
 // 獲取maxIdle缅阳,限制空閑資源保留的上限數(shù)量
    final int maxIdleSave = getMaxIdle();
    if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
            try {
                    destroy(p);
            } catch (final Exception e) {
                    swallowException(e);
            }
    } else {
        // 重點(diǎn)在這里,如果沒有超過maxIdle景描,則會將歸還的對象添加到 idleObjects 中
            if (getLifo()) {
                    idleObjects.addFirst(p);
            } else {
                    idleObjects.addLast(p);
            }
            if (isClosed()) {
                    // Pool closed while object was being added to idle objects.
                    // Make sure the returned object is destroyed rather than left
                    // in the idle object pool (which would effectively be a leak)
                    clear();
            }
    }
    updateStatsReturn(activeTime);
}

歸還對象時十办,首先會變更對象狀態(tài)從 ALLOCATED 到 RETURNING秀撇,如果 testOnReturn參數(shù) 為true,校驗(yàn)資源有效性(Jedis連接的有效性)向族,如果無效呵燕,則調(diào)用 destroy() 方法銷毀對象,當(dāng) maxIdle 未超過 idleObjects 資源列表大小時炸枣,則會將歸還的對象添加到 idleObjects 中虏等。

而在 borrorObject() 的借出對象方法中就是從 **idleObjects#pollFirst() **獲取對象的,沒有的話就會去創(chuàng)建适肠,對象最多不能超過 maxTotal 數(shù)量霍衫。

6、Jedis客戶端線程池參數(shù)

我們了解完 Apache Common Pool2 框架的線程池原理之后侯养,接下來看看 Jedis 里是如何包裝的敦跌。

線程池里的參數(shù)都是基于 JedisPoolConfig 來構(gòu)建的。

JedisPoolConfig Jedis資源池配置類默認(rèn)構(gòu)造函數(shù):

public class JedisPoolConfig extends GenericObjectPoolConfig {
   public JedisPoolConfig() {
       // defaults to make your life with connection pool easier :)
       setTestWhileIdle(true);
       setMinEvictableIdleTimeMillis(60000);
       setTimeBetweenEvictionRunsMillis(30000);
       setNumTestsPerEvictionRun(-1);
   }
}

JedisPoolConfig 繼承了 GenericObjectPoolConfig逛揩,JedisPoolConfig 默認(rèn)構(gòu)造函數(shù)中會將 testWhileIdle 參數(shù)設(shè)置為true(默認(rèn)為false)柠傍,minEvictableIdleTimeMillis設(shè)置為60秒(默認(rèn)為30分鐘),timeBetweenEvictionRunsMillis設(shè)置為30秒(默認(rèn)為-1)辩稽,numTestsPerEvictionRun設(shè)置為-1(默認(rèn)為3)惧笛。

每個30秒執(zhí)行一次空閑資源監(jiān)測,發(fā)現(xiàn)空閑資源超過60秒未被使用逞泄,從資源池中移除患整。

創(chuàng)建 JedisPoolConfig 對象后,設(shè)置一些參數(shù):

// 創(chuàng)建 JedisPoolConfig 對象喷众,設(shè)置參數(shù)
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig()
jedisPoolConfig.setMaxTotal(100);
jedisPoolConfig.setMaxIdle(60);
jedisPoolConfig.setMaxWaitMillis(1000);
jedisPoolConfig.setTestOnBorrow(false);
jedisPoolConfig.setTestOnReturn(true);

JedisPool 管理了Jedis 的線程池:

// JedisPool 構(gòu)造函數(shù)
public JedisPool(final GenericObjectPoolConfig poolConfig, final String host, int port,
     int timeout, final String password) {
this(poolConfig, host, port, timeout, password, Protocol.DEFAULT_DATABASE, null);
}
   
public abstract class Pool<T> implements Closeable {
protected GenericObjectPool<T> internalPool;

// 抽象 Pool 構(gòu)造函數(shù)
public Pool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {
   initPool(poolConfig, factory);
}
}

[6] 站在Redis服務(wù)端視角分析

既然猜測可能跟 Redis 服務(wù)端有關(guān)系各谚,就需要從跟客戶端的參數(shù)配置去分析下,是否會有所影響到千。

1昌渤、Redis客戶端緩沖區(qū)滿了

Redis有三種客戶端緩沖區(qū):

客戶端緩沖區(qū)

普通客戶端緩沖區(qū)(normal):

用于接受普通的命令,例如get憔四、set膀息、mset、hgetall等

slave客戶端緩沖區(qū)(slave):

用于同步master節(jié)點(diǎn)的寫命令了赵,完成復(fù)制潜支。

發(fā)布訂閱緩沖區(qū)(pubsub):

pubsub不是普通的命令,因此有單獨(dú)的緩沖區(qū)斟览。

Redis的客戶端緩沖區(qū)配置具體格式是:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

(1)class: 客戶端類型: normal、 slave辑奈、 pubsub

(2)hard limit: 如果客戶端使用的輸出緩沖區(qū)大于hard limit苛茂,客戶端會被立即關(guān)閉已烤。

(3)soft limit和soft seconds: 如果客戶端使用的輸出緩沖區(qū)超過了soft limit并且持續(xù)了soft limit秒,客戶端會被立即關(guān)閉

連接 Redis 查看 client-output-buffer-limit:

127.0.0.1:6379> config get client-output-buffer-limit
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 21474836480 16106127360 60 pubsub 33554432 8388608 60"

普通客戶端緩沖區(qū)normal類型的class妓羊、hard limit胯究、soft limit 都是 0,表示關(guān)閉緩沖區(qū)的限制躁绸。

如果緩沖期過小的裕循,就可能會導(dǎo)致的 Unexpected end of stream 異常。

2净刮、Redis服務(wù)器 timeout 設(shè)置不合理

Redis服務(wù)器會將超過 timeout 時間的閑置連接主動斷開剥哑。

查看服務(wù)器的timeout配置:

127.0.0.1:6379> config get timeout
1) "timeout"
2) "600"

timeout 配置為 600 秒,同一個連接等待閑置 10 分鐘后淹父,發(fā)現(xiàn)還沒有被使用株婴,Redis 就將該連接中斷掉了。

所以這里就會有個問題暑认,這里的 timeout 時間是要與上文中的 Jedis 線程池里的 空閑資源監(jiān)測任務(wù) 有關(guān)系的困介。

假設(shè) JedisPoolConfig 里的 timeBetweenEvictionRunsMillis 不設(shè)置,會使用默認(rèn)值 -1蘸际,不會啟動 Evictor 空閑監(jiān)測任務(wù)了座哩。

當(dāng)從資源池借出 Jedis 連接后,注意此時粮彤,如果過了 10 分鐘根穷,Redis 服務(wù)端已將這根連接給中斷了。

而客戶端還拿著這個 Jedis 連接去繼續(xù)操作 set驾诈、get 之類的命令缠诅,就會出現(xiàn) Unexpected end of stream 異常了。

示例演示:

為了方便演示乍迄,如下參數(shù)調(diào)整管引。

1)Redis服務(wù)器 timeout 初始化為 10秒

2)Java 測試代碼如下所示

new Thread(new Runnable() {
        public void run() {
                for (int i = 0; i < 5; i++) {
                        System.out.println(" jedis.get(\"foo\"): " +  jedis.get("foo"));
                        try {
                                Thread.sleep(12000);
                        } catch (InterruptedException e) {
                                e.printStackTrace();
                        }
                }
        }
}).start();

輸出結(jié)果:

// 第一次輸出
 jedis.get("foo"): bar
 
 // sleep 12秒,Redis 服務(wù)器 timeout 等待 10秒斷開 Jedis 連接
 
 // 再次執(zhí)行 jedis.get("foo") 闯两,異常出現(xiàn)了

Exception in thread "Thread-58" redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
    at redis.clients.jedis.Protocol.read(Protocol.java:215)
    at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
    at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:259)
    at redis.clients.jedis.Connection.getBulkReply(Connection.java:248)
    at redis.clients.jedis.Jedis.get(Jedis.java:153)
 

所以褥伴,JedisPoolConfig 缺省構(gòu)造函數(shù)里,直接啟動了 Evictor 任務(wù)漾狼,在客戶端線程池里自身來監(jiān)測空閑的連接重慢,發(fā)現(xiàn)超過了 minEvictableIdleTimeMillis 設(shè)置的時間,從資源池里剔除逊躁。

避免客戶端獲取到了連接似踱,但是無法正常使用,導(dǎo)致一些異常的出現(xiàn)。

Redis服務(wù)器里的 timeout 這個值是否合理核芽,還是要結(jié)合自身業(yè)務(wù)場景來定囚戚。

據(jù)說阿里云Redis(公司內(nèi)沒用過)中 timeout 設(shè)置為 0,也就是不會主動關(guān)閉空閑連接轧简;緩沖區(qū)設(shè)置為 0 0 0 驰坊,也就是不會對客戶端緩沖區(qū)進(jìn)行限制,一般不會有問題哮独。

3拳芙、網(wǎng)絡(luò)不穩(wěn)定因素

回到本文開頭提到的 Sentry 告警的 JedisConnectionException 異常棧信息。

回顧異常棧如下所示:

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
    at redis.clients.jedis.Protocol.process(Protocol.java:151)
    at redis.clients.jedis.Protocol.read(Protocol.java:215)
    at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
    at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
    at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
    at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
    at

是在創(chuàng)建新的資源連接時皮璧,connect 之后的 auth 驗(yàn)證密碼時拋出了 Unexpected end of stream舟扎。

經(jīng)過上述細(xì)致的分析,排除了 Redis 客戶端緩沖區(qū)滿 和 timeout 參數(shù)設(shè)置合理性之后恶导,剩下可能就跟網(wǎng)絡(luò)因素有關(guān)系了浆竭。此前,在容器外的虛擬機(jī)惨寿、物理機(jī)部署的應(yīng)用是沒有出現(xiàn)過此問題邦泄,當(dāng)前是在 k8s 容器內(nèi)偶爾出現(xiàn),需要運(yùn)維配合熟悉下容器的網(wǎng)絡(luò)架設(shè)裂垦,通過工具轉(zhuǎn)包來排查網(wǎng)絡(luò)問題顺囊,進(jìn)一步明確原因。

根據(jù)最終分析結(jié)果蕉拢,因「網(wǎng)絡(luò)抖動」之類偶發(fā)的問題特碳,可以在客戶端增加重試機(jī)制來解決。

另外晕换,我們也在 k8s 容器里對 Redis 集群做了多次測試午乓,暫時也未能發(fā)現(xiàn)性能問題。

性能測試

[7] 資源池生產(chǎn)配置合理性分析

如果你拿不準(zhǔn) Jedis 線程池參數(shù)設(shè)置的是否合理闸准,可以配置一些核心參數(shù)益愈,線上通過 JMX 工具去觀察。

再次看下 JMX 工具查看屬性:

JMX監(jiān)控

CreatedCount:已創(chuàng)建的資源池對象數(shù)量

DestoryedCount:已銷毀的資源池對象總數(shù)量

DestoryedByEvictorCount:通過 Evictor 空閑監(jiān)測任務(wù)銷毀的資源池對象數(shù)量

BorrowedCount:從資源池借出對象的次數(shù)

ReturnedCount:歸還給資源池對象的次數(shù)

通過監(jiān)控可以看到 CreatedCount 為 6393夷家, DestoryedByEvictorCount 為 6381蒸其,說明大部分對象剛剛創(chuàng)建之后,沒過多久库快,都被空閑資源監(jiān)測 Evictor 任務(wù)給銷毀了摸袁。

根據(jù)前文中 Evictor 配置的參數(shù)「每隔 30 秒執(zhí)行一次任務(wù),如果池中對象超過 60 秒未使用义屏,對象即被銷毀掉」靠汁。

而 Redis 服務(wù)器端 timeout 是 10 分鐘蜂大,如果我們不想讓對象被銷毀的那么快,盡量保留在資源池中蝶怔,減少因創(chuàng)建新連接的開銷時間县爬,可以優(yōu)化空閑監(jiān)測任務(wù)的參數(shù)。

參數(shù)優(yōu)化示例:

// defaults to make your life with connection pool easier :)
jedisPoolConfig.setTestWhileIdle(true);
// 增加連接最小空閑時間添谊,在資源池里多保留一段時間
jedisPoolConfig.setMinEvictableIdleTimeMillis(180000);
// 檢測任務(wù)執(zhí)行時間周期
jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
// 檢測任務(wù)執(zhí)行時,每次的采樣數(shù)察迟,比如設(shè)置為5
jedisPoolConfig.setNumTestsPerEvictionRun(-1);

根據(jù)參數(shù)分析斩狱,顯然 maxIdle 設(shè)置為 60, maxTotal 為 100過大了扎瓶,適當(dāng)調(diào)整該值所踊。

jedisPoolConfig.setMaxTotal(30);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(5);
jedisPoolConfig.setMaxWaitMillis(1000);

另外,根據(jù)空閑資源檢測任務(wù)中的驅(qū)逐策略分析概荷,可以利用 softMinEvictableIdleTimeMillisminIdle 兩個參數(shù)組合使用秕岛,比如 softMinEvictableIdleTimeMillis 設(shè)置為 180 秒,minIdle 設(shè)置為 5误证,當(dāng)資源空閑時間超過 180 秒继薛,并且 idleObjects 空閑列表大小超過了 minIdle 最小空閑資源數(shù),才會將資源從池中移除掉愈捅。

由此遏考,保證了資源池有一定數(shù)量(minIdle)的資源連接存在,不會導(dǎo)致頻繁創(chuàng)建新的資源連接蓝谨。

遇到某些異常灌具,你也可以去 Jedis github 的 ISSUE里去搜索下是否有了答案。

本文的主要分析思路也是源于ISSUE#932和ISSUE#1092展開分析的譬巫,但是每個人遇到的問題都不同咖楣,解決方式也不一樣。

比如ISSUE#1092最后回復(fù)給出的答案:

ISSUE#1092

在Redis服務(wù)端將 timeout 設(shè)置為0芦昔,這樣避免Redis主動斷開連接诱贿,然后客戶端 maxIdle 設(shè)置為 0。

這位仁兄對參數(shù)配置有些過于『暴力』烟零,這樣是不可取的瘪松,maxIdle 為 0,資源池沒有充分利用起來锨阿,每次請求都會新建資源連接宵睦,歸還后馬上就銷毀了。

不過他因此這么改墅诡,分析的原因是對的壳嚎,就是一根連接被Redis給斷開了桐智,客戶端還拿著在那使用呢,能不出問題嘛烟馅。

[8] 本文總結(jié)

本文由 Redis Java 客戶端的一個異常引出说庭,從監(jiān)控到的異常堆棧整個過程進(jìn)行了細(xì)致分析。

站在Jedis客戶端視角郑趁,對 Jedis 客戶端內(nèi)部使用的 Apache Common Pool2 開源框架線程池的基本原理刊驴,包括創(chuàng)建對象、銷毀對象寡润、空閑資源監(jiān)測任務(wù)機(jī)制做了具體分析捆憎。

由于線程池使用的配置參數(shù),通過工具或源碼分析 JedisPool 線程池里的參數(shù)合理性設(shè)置梭纹。

站在 Redis 服務(wù)端視角躲惰,分析了 Redis 服務(wù)器端的客戶端緩沖區(qū)參數(shù)和 timeout 參數(shù)設(shè)置是否合理,什么情況下會導(dǎo)致 Unexpected end of stream 異常的出現(xiàn)变抽。

通過本文了解到 Redis 客戶端產(chǎn)生的異常础拨,跟 Redis 客戶端和服務(wù)端都是有關(guān)系的,對于客戶端工具(框架)基本原理要有所了解绍载,才能更好的應(yīng)對各類異常诡宗,找到問題根源所在。

有時大部分應(yīng)用的性能問題都可以通過參數(shù)來調(diào)優(yōu)击儡,前提是你要對這些參數(shù)配置以及背后的原理深入分析僚焦,才能斗膽嘗試調(diào)優(yōu)。

本文僅提到了 Unexpected end of Stream 異常曙痘,除了該異常外芳悲,其他 Jedis 客戶端拋出的異常,本文的分析也是有幫助的边坤。

這里匯總了一些常見的 Jedis 異常:

1)blockWhenExhausted = true 當(dāng)?shù)却?maxWaitMillis 時間仍然無法獲取到連接名扛,會拋出:

Caused by: java.util.NoSuchElementException: Timeout waiting for idle object

2)blockWhenExhausted = false 當(dāng)無法獲得連接,會拋出

Caused by: java.util.NoSuchElementException: Pool exhausted

一般檢查Redis慢查詢阻塞是否存在茧痒;maxWaitMillis設(shè)置是否過短肮韧;

3)Redis 無法連接,連接時會被拒絕旺订,會拋出

Caused by: java.net.ConnectException: Connection refused

一般檢查 Redis 域名配置正確性弄企;排查該段時間網(wǎng)絡(luò)是否有問題。

4)客戶端讀寫超時区拳,會拋出

JedisConnectionException: java.net.SocketTimeoutException: Read timed out

5)連接超時拘领,會拋出

JedisConnectionException: java.net.SocketTimeoutException: connect timed out

4)、5)考慮讀寫超時設(shè)置的過短樱调;有慢查詢或者Redis發(fā)生阻塞约素;網(wǎng)絡(luò)不穩(wěn)定 方向去分析届良。

6)pipeline的錯誤使用,會拋出

JedisDataException: Please close pipeline or multi block before calling this method.

按照pipeline最佳實(shí)踐去使用圣猎,比如批量結(jié)果的解析士葫,建議使用pipeline.syncAndReturnAll()。

其他的異常送悔,你就見招拆招吧慢显。

文末了,碼字不易,如有疏漏箍铲,還請指正,希望對大家有所幫助,謝謝纱注。

參考資料:

https://yq.aliyun.com/articles/236384?spm=a2c4e.11155435.0.0.e21e2612uQAVoW#cc1

https://github.com/xetorthio/jedis/issues/932

https://github.com/xetorthio/jedis/issues/1029

https://www.cnblogs.com/benthal/p/10761868.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市呜叫,隨后出現(xiàn)的幾起案子帐要,更是在濱河造成了極大的恐慌,老刑警劉巖落塑,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纽疟,死亡現(xiàn)場離奇詭異,居然都是意外死亡憾赁,警方通過查閱死者的電腦和手機(jī)污朽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來龙考,“玉大人蟆肆,你說我怎么就攤上這事』蘅睿” “怎么了炎功?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缓溅。 經(jīng)常有香客問我蛇损,道長,這世上最難降的妖魔是什么坛怪? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任淤齐,我火速辦了婚禮,結(jié)果婚禮上袜匿,老公的妹妹穿的比我還像新娘更啄。我一直安慰自己,他們只是感情好居灯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布锈死。 她就那樣靜靜地躺著贫堰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪待牵。 梳的紋絲不亂的頭發(fā)上其屏,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音缨该,去河邊找鬼偎行。 笑死,一個胖子當(dāng)著我的面吹牛贰拿,可吹牛的內(nèi)容都是我干的蛤袒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼膨更,長吁一口氣:“原來是場噩夢啊……” “哼妙真!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荚守,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤珍德,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后矗漾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锈候,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年敞贡,在試婚紗的時候發(fā)現(xiàn)自己被綠了泵琳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡誊役,死狀恐怖获列,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛔垢,我是刑警寧澤蛛倦,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站啦桌,受9級特大地震影響溯壶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜甫男,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一且改、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧板驳,春花似錦又跛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽感混。三九已至,卻和暖如春礼烈,著一層夾襖步出監(jiān)牢的瞬間弧满,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工此熬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庭呜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓犀忱,卻偏偏與公主長得像募谎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子阴汇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

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