之前業(yè)務(wù)線上出現(xiàn)了es日表數(shù)據(jù)不一致的情況,我一開始一臉蒙蔽,后來請教同事也好,自己查閱資料也好,最后的問題其實(shí)是小到自己看不見的代碼問題。最近是個空檔期,記錄一下血案。
業(yè)務(wù)場景描述:
業(yè)務(wù)項目中用戶,股票,文章之間有著關(guān)聯(lián)關(guān)系,線上數(shù)據(jù)越來越多時,采取的方案是數(shù)據(jù)首先落庫,然后同步到es(elasticsearch)中,即是做緩存數(shù)據(jù)庫,也方便了搜索業(yè)務(wù)需要。
后來隨著用戶和文章之間的關(guān)聯(lián)關(guān)系數(shù)據(jù)量越來越大,決定對es按天分表,即每天會自動的產(chǎn)生一個天索引(這里的索引和mysql索引不一樣),es中索引的概念可以類比與mysql中數(shù)據(jù)庫db的概念,類似于庫,索引是具有某些相似特征的文檔的集合。我們用的es版本較高,所以一個索引下只有一個type(type好比關(guān)系型數(shù)據(jù)庫中的table),type下有著大量的document。那出現(xiàn)的數(shù)據(jù)問題是什么呢?我們的業(yè)務(wù)邏輯代碼是根據(jù)這個業(yè)務(wù)線中一條document中的publishTime字段來決定這條數(shù)據(jù)落到哪個索引中。比如這條數(shù)據(jù)中的publishTime為20191023,那它就會落到index_20191023這個索引中。但是現(xiàn)在居然在index_20191023這個索引中發(fā)現(xiàn)了其他天的數(shù)據(jù),而且并沒有規(guī)律可言,不同索引中有著很多不同日期的數(shù)據(jù),造成了es數(shù)據(jù)的混亂。
主要從下面幾個方面來排查:
1.es集群本身的問題,或許由于多節(jié)點(diǎn),多分片造成的。
? ? ? 一個完整的索引分成多個分片,這樣的好處是可以把一個大的索引拆分成多個,分布到不同的節(jié)點(diǎn)上。構(gòu)成分布式搜索。分片的數(shù)量只能在索引創(chuàng)建前指定,并且索引創(chuàng)建后不能更改。
2.線上環(huán)境數(shù)據(jù)量太大,當(dāng)代碼中有線程不安全的地方,會造成數(shù)據(jù)錯亂
3.關(guān)系型數(shù)據(jù)庫同步到es的方案機(jī)制是否有問題
不管是哪個方面問題,核心就是根據(jù)字段publishTime找到相對應(yīng)的es索引,然后存儲這個過程除了問題,我一開始一度懷疑是es研發(fā)部門的問題,,,,我在document中多加了一個字段index,index就是publishTime的一種拼接形式,如果所有數(shù)據(jù)中index和es索引名一致,那就不是人家es部門的問題,如果不一致,說明落到es庫時出了問題。但是我加了一條日志后,在線上看到的index和es索引是一樣的,于是第一種原因就排除了。
之后幾天反復(fù)的看預(yù)發(fā)環(huán)境和線上環(huán)境的數(shù)據(jù),發(fā)現(xiàn)新測試的數(shù)據(jù)在預(yù)發(fā)環(huán)境上是保持一致的,沒有錯誤。在線上就會出問題,看來就是數(shù)據(jù)量一上來,就會造成數(shù)據(jù)混亂,這顯然就是代碼中有危險的地方,有線程不安全的地方。
于是就回去仔細(xì)看代碼,當(dāng)然核心還是publishTime前前后后各種格式的轉(zhuǎn)換,終于發(fā)現(xiàn)了simpledateFormat這個類,它是線程不全的,如果非要用它,那就不要讓所有線程共享,應(yīng)把它設(shè)置成局部變量。當(dāng)然還有其他的解決辦法,比如設(shè)置成threadLocal類型,或者利用同步鎖。當(dāng)然這里如果要用threadLocal的話,也是有點(diǎn)不穩(wěn)妥的,如果線程數(shù)過多,threadlocal要是常駐內(nèi)存會有風(fēng)險,我當(dāng)時也不確定threadlocal會不會釋放回收,可能會造成內(nèi)存泄漏的問題。
經(jīng)過查閱資料,發(fā)現(xiàn)threadlocal內(nèi)部果然會有無法釋放的部分,如下圖,實(shí)現(xiàn)是強(qiáng)引用,虛線是弱引用
每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實(shí)例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當(dāng)把threadlocal實(shí)例置為null以后,沒有任何強(qiáng)引用指向threadlocal實(shí)例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強(qiáng)引用. 只有當(dāng)前thread結(jié)束以后, current thread就不會存在棧中,強(qiáng)引用斷開, Current Thread, Map, value將全部被GC回收.

所以最后修改bug就顯而易見了,即保證線程初始化的時候單獨(dú)調(diào)用simpledateFormat,各自享用空間,或者在jdk1.8之后有代替它的線程安全的類。
這次修復(fù)es線上數(shù)據(jù)優(yōu)化問題其實(shí)很多時候都是代碼中我們很難發(fā)現(xiàn)的一些不安全問題,但更重要的是排查問題的定位與分析。