本文原載于Elastic中文社區(qū): https://elasticsearch.cn/publish/article/273
本文是針對(duì)社區(qū)問題question#2352的分析和總結(jié)
現(xiàn)在很多公司(包括我們自己)將ES用作數(shù)據(jù)庫數(shù)據(jù)的索引,將多個(gè)數(shù)據(jù)庫的數(shù)據(jù)同步到ES是非常常見的應(yīng)用場(chǎng)景。所以感覺這個(gè)問題可能會(huì)困擾不止一個(gè)用戶闸英,而官方的文檔也沒有對(duì)update的底層機(jī)制及局限做特別說明,特將該問題的討論和結(jié)論整理成文连锯,供社區(qū)用戶參考。
問題描述
在ES5.x里通過bulk update將數(shù)據(jù)從數(shù)據(jù)庫同步到ES,如果短時(shí)間更新的一批數(shù)據(jù)里存在相同的文檔ID画舌,例如一個(gè)bulk update里大量寫入下面類型的數(shù)據(jù):
{id:1,name:aaa}
{id:1,name:bbb}
{id:1,name:ccc}
{id:2,name:aaa}
{id:2,name:bbb}
{id:2,name:ccc}
.......
則更新的速度非常慢奠货。 而在ES 1.x和2.x里同樣的操作快得多
根源追溯
update操作是分為兩個(gè)步驟進(jìn)行介褥,即先根據(jù)文檔ID做一次GET,得到最新版本的文檔递惋,然后在內(nèi)存里做好更新后呻顽,再寫回去。問題就出在這個(gè)GET操作上面丹墨。
在core/src/main/java/org/elasticsearch/index/engine/InternalEngine.java
這個(gè)類里面廊遍,get函數(shù)會(huì)根據(jù)一個(gè)realtime
參數(shù)(默認(rèn)是true
),決定如何獲取原始文檔贩挣。
public GetResult get(Get get, Function<String, Searcher> searcherFactory, LongConsumer onRefresh) throws EngineException {
assert Objects.equals(get.uid().field(), uidField) : get.uid().field();
try (ReleasableLock lock = readLock.acquire()) {
ensureOpen();
if (get.realtime()) {
VersionValue versionValue = versionMap.getUnderLock(get.uid());
if (versionValue != null) {
if (versionValue.isDelete()) {
return GetResult.NOT_EXISTS;
}
if (get.versionType().isVersionConflictForReads(versionValue.getVersion(), get.version())) {
throw new VersionConflictEngineException(shardId, get.type(), get.id(),
get.versionType().explainConflictForReads(versionValue.getVersion(), get.version()));
}
long time = System.nanoTime();
refresh("realtime_get");
onRefresh.accept(System.nanoTime() - time);
}
}
// no version, get the version from the index, we know that we refresh on flush
return getFromSearcher(get, searcherFactory);
}
可以看到realtime
參數(shù)決定了是否以實(shí)時(shí)的方式獲取數(shù)據(jù)喉前。 如果設(shè)置為false
,意味著不關(guān)心實(shí)時(shí)性王财,此時(shí)直接從searcher
對(duì)象里面拿數(shù)據(jù)卵迂。因?yàn)?code>searcher只能訪問refresh過的數(shù)據(jù),那些剛寫入到indexing writter buffer里绒净,還未經(jīng)歷過refresh的數(shù)據(jù)不會(huì)被訪問到见咒,故而該讀取方式是準(zhǔn)實(shí)時(shí)(Near Real Time)。 而這個(gè)realtime
參數(shù)默認(rèn)設(shè)置是true
挂疆,說明需要以實(shí)時(shí)的方式訪問數(shù)據(jù)改览,也就是說writter buffer里未經(jīng)refresh的數(shù)據(jù)也要能被檢索到,如何保證這塊數(shù)據(jù)也能被實(shí)時(shí)訪問呢缤言?
從代碼里可以看到宝当,其中存在一個(gè)refresh("realtime_get")
的函數(shù)調(diào)用。這個(gè)函數(shù)調(diào)用會(huì)檢查胆萧,GET的doc id是否都是可以被搜索到庆揩。 如果已經(jīng)寫入了但無法搜索到,也就是剛剛寫入到writter buffer里還未refresh這種情況跌穗,就會(huì)強(qiáng)制執(zhí)行一次refresh操作订晌,讓數(shù)據(jù)對(duì)searcher可見,保證getFromSearcher
調(diào)用拿的是完全實(shí)時(shí)的數(shù)據(jù)蚌吸。
實(shí)際上測(cè)試下來锈拨,正是這樣的結(jié)果: 在關(guān)閉索引的自動(dòng)刷新的情況下(設(shè)置refresh_interval: -1
,只寫入一條文檔套利,然后對(duì)該文檔ID執(zhí)行一個(gè)GET操作推励,就會(huì)看到有一個(gè)新的segment生成鹤耍。 說明GET的過程觸發(fā)了refresh。
查了下文檔验辞,如果僅僅是做GET API調(diào)用稿黄,這個(gè)實(shí)時(shí)性可以認(rèn)為控制,只需要在url里帶可選參數(shù)realtime=[true/|false]
跌造。 參考: reference/5.6/docs-get.html#realtime杆怕。
然而,不幸的是壳贪,update API的文檔和源碼都沒有提供一個(gè)禁用實(shí)時(shí)性的參數(shù)陵珍。 update對(duì)GET的調(diào)用,傳入的realtime參數(shù)是在代碼里寫死為true的违施,意味著update的時(shí)候互纯,必須強(qiáng)制執(zhí)行一次realtime GET.
為什么是這樣的代碼邏輯,仔細(xì)想一下就也就了然了磕蒲。因?yàn)閡pdate允許對(duì)文檔做部分字段更新留潦,如果有2個(gè)請(qǐng)求分別更新了同一個(gè)文檔的不同字段, 可能先更新的數(shù)據(jù)還在writter buffer里辣往,沒來得及refresh兔院,因而對(duì)searcher不可見。如果后續(xù)更新不做一次refresh站削,前面的更新可能就丟失了坊萝。
另外一個(gè)問題,為啥5.x之前的版本沒有這個(gè)性能問題许起? 看了下2.4的GET方法源碼十偶,其的確沒有采用refresh的方式來保障數(shù)據(jù)的實(shí)時(shí)性,而是通過訪問translog來達(dá)到同樣的目的街氢。官方在這個(gè)變更里pull#20102將機(jī)制從訪問translog改為了refresh哨啃。理由是之前ES里有很多地方利用translog來維護(hù)數(shù)據(jù)的位置界酒,使得很多操作變得很慢,去掉對(duì)translog的依賴可以全面提高性能绒极。
很遺憾馅笙,這個(gè)更改對(duì)于短時(shí)間反復(fù)大量更新相同doc id的操作伦乔,會(huì)因?yàn)檫^于頻繁的強(qiáng)制refresh,短時(shí)間生成很多小segment董习,繼而不斷觸發(fā)segment合并烈和,產(chǎn)生顯著的性能損耗。 從上面鏈接里的討論看皿淋,官方認(rèn)為招刹,在提升大多數(shù)應(yīng)用場(chǎng)景性能的前提下恬试,對(duì)于這種較少見的場(chǎng)景下的性能損失是值得付出的。所以疯暑,建議從應(yīng)用層面去解決训柴。
因此,如果實(shí)際應(yīng)用場(chǎng)景里遇到類似的數(shù)據(jù)更新問題妇拯, 只能是優(yōu)化應(yīng)用數(shù)據(jù)架構(gòu)幻馁,在應(yīng)用層面合并相同doc id的數(shù)據(jù)更新后再寫入ES,或者只能使用ES 2.x這樣的老版本了越锈。