[8]elasticsearch原始碼深入分析——Node與NodeEnvironment的例項化

飛來來發表於2019-03-01

本篇為elasticsearch原始碼分析系列文章的第八篇,又到了我們深扒ElasticSearch原始碼的時候了:)

本篇開始將會詳細解釋Node例項化的過程,從Node例項化這個操作為源點,瞭解ElasticSearch的編碼思想,由於Node內容眾多,所以會分篇敘述。

Node概覽

前不久的分析中說到了,Node是ElasticSearch啟動的重中之重,一個Node代表在一個叢集(cluster.name)中的一個節點。為了使用客戶端對叢集進行操作,客戶端可以使用Node中的client()來取得org.elasticsearch.client.Client的例項。

任何時候,啟動一個elasticsearch例項都是啟動Node的一個例項,多個Node例項的集合叫做Cluster

叢集中的節點預設都可以使用HTTP和Transport兩種方法通訊。transport的通訊可以使用Java TransportClient,而HTTP就只能使用Rest Client了。

叢集中的Node都能相互發現,並轉發請求到合適節點。而且每個Node會有以下的一個或多個作用:

  • 通過設定node.master屬性值為true(true為預設值)被選舉為Master節點
  • 通過設定node.Data屬性值為true(true為預設值)來充當資料節點,顧名思義,這種節點持有資料且能做資料的關聯操作
  • 通過設定node.ingest屬性值為true(true為預設值)來充當ingest node。ingest node是5.0新增的特性,簡單點說就是elasticsearch內建的資料處理器,目前提供了convert,grok之類的操作,相信用過Logstash的同學一定不會陌生。
  • 通過設定tribe.屬性來使node成為Tribe node*,它是一個特殊的客戶端,它可以連線多個叢集,在所有連線的叢集上執行搜尋和其他操作

Node類首先構造了三個Setting屬性,分別是:

屬性名 key值 作用
WRITE_PORTS_FILE_SETTING node.portsfile 用於控制是否將檔案寫入到包含給定傳輸型別埠的日誌目錄中
NODE_DATA_SETTING node.data 使該node被選舉為data節點
NODE_MASTER_SETTING node.master 使該node被選舉為master節點
NODE_INGEST_SETTING node.ingest 使該node被選舉為ingest節點
NODE_LOCAL_STORAGE_SETTING local_storage 控制節點是否需要持久化後設資料到磁碟,這和data node沒有必然聯絡,但是如果local_storage為false,node.data和node.master的值必須為false
NODE_NAME_SETTING node.name 節點名稱
NODE_ATTRIBUTES node.attr. 新增gateway,zone,rack_id等引數key
BREAKER_TYPE_KEY indices.breaker.type 斷路器型別,提供引數有hierarchy,none兩種,主要是防止記憶體溢位後elasticsearch當機

Node例項化

三個Node的構造引數:

Node的構造引數

最重要的構造方法是:

protected Node(final Environment environment, Collection<Class<? extends Plugin>> classpathPlugins)
複製程式碼

該構造方法所做的工作:

  • 用當前節點名稱設定臨時Logger(因為後續可能節點名稱會變動所以設定成臨時Logger)
  • 根據引數environment中的settings變數構造新的settings例項,新增預設的CLIENT_TYPE=”node”值。
  • 用生成的新的settings例項和environment引數構建新的節點環境(NodeEnvironment
  • 構造plugins
  • 載入LocalNodeFactory
  • 構造ThreadPool,接收引數為setting和plugins的builder
  • 構造scriptModule,analysisModule,settingsModule
  • 通過pluginsService構造NetworkService
  • 通過pluginsService構造ClusterPugins
  • 構造IngestService
  • 構造DiskThresholdMonitor
  • 構造ClusterInfoService
  • 構造UsageService
  • 例項化ModulesBuilder
  • 通過pluginsService構造SearchModule
  • 通過settingsModule構造CircuitBreakerService
  • 構造ActionModule
  • 構造NamedXContentRegistry
  • 構造MetaStateService
  • 構造IndicesService
  • 構造RestController
  • 構造NetworkModule
  • 構造MetaDataUpgrader
  • 構造TransportService
  • 構造ResponseCollectorService
  • 構造SearchTransportService
  • 構造DiscoveryModule
  • 構造NodeService
  • 向構造好的ModuleBuilder中新增所有需要的服務
  • 通過ModuleBuilder得到Guice注入類
  • 構件LifecycleComponent集合
  • 初始化NodeClient

我們的原始碼解析也會按照這個流程來開展。

構建預設的Setting

在Node剛開始構造的時候,這個時候Node物件中還沒有存在Setting例項的,有的配置只有在BootStrap方法中傳過來的Environment例項,這個Envi的例項(environment)其實就是解析了啟動環境中若干的配置路徑(lib路徑,module路徑,logs路徑),在對environment的setting化後(呼叫Environment的settings()方法,就是對初始的環境變數標準化為Settings型別的物件),如下圖:

Environment的settings()方法

在構造完這個最初始版本的Settings後,程式碼檢視取得配置中的node.name,為什麼會在Node剛開始初始化的時候就去查詢node的name呢?在跟進原始碼後會知道,ElasticSearch這麼做是為了給Logger的例項增加marker這個引數,相信對log4j熟悉的同學會對這個引數很熟悉,merker是log4j中LayoutPattern的引數之一,作用是event元素中的標記元素,這種標記元素僅在日誌訊息中使用標記時出現,且具有繼承性。如下圖:

logger中的marker元素

當然如果配置了node.name,且在log4j.properties中配置了屬性appender.console.layout.pattern包含元素**%marker**,那麼在控制檯中會很容易看到形如下圖中的日誌列印,這就能很容易區分出日誌的歸屬Node。

logger中的marker

當然到這裡我們都還沒給Node設定名稱。

接下來給Node設定了client.type的值為node,這個也是寫在程式碼裡的配置。

private static final String CLIENT_TYPE = "node";
複製程式碼

接下來開始就開始構建NodeEnvironment例項了。

NodeEnvironment的例項化

首先說明EnvironmentNodeEnvironment是沒有任何繼承關係的,只是在NodeEnvironment的例項化過程中,Environment作為了構建所必需的引數。NodeEnvironment主要是針對單個節點的包含所有資料路徑的構件物件,說白了這個類就是xxx,直接看NodeEnvironment建構函式。建構函式中通過累加possibleLockId的值來新增資料儲存的路徑,這個值是從0開始的,所以才會在ElasticSearch的資料儲存頁面生成如下圖的資料夾:

資料儲存路徑

接下來使用FSDirectory.open(dir, NativeFSLockFactory.INSTANCE)獲取儲存索引的目錄,FSDirectory是對檔案系統目錄的操作

  • 第一個引數java.nio.file.Path:dir這個引數是NIO的一個類Path,接收字串引數建立的。
  • 第二個引數org.apache.lucene.store.LockFactory:這個引數是Lucene中的索引鎖。因為Lucene必須知道一份索引是否已經被某個IndexWriter開啟,所以必須使用鎖的機制來保證寫索引的同步性。首先大家要明確一個問題,在ElasticSearch異常退出,或是JVM異常關閉的情況下,在下次重啟ElasticSearch,索引依然能夠正確讀寫,就是這麼神奇。這是怎麼實現的呢?祕密就在這個NativeFSLockFactory.INSTANCE引數中,他是FSDirectory提供的預設鎖,他的最大優勢就是當程式異常退出後,可以由作業系統負責解除索引的鎖,作業系統會釋放檔案上所有的引用,以確保索引可以正確讀寫。LockFactory還提供了其他型別的鎖,由於涉及到Lucene的深層次知識點,這裡就不展開敘述。

通過locks[dirIndex] = luceneDir.obtainLock(NODE_LOCK_FILENAME);取得鎖後生成一個內部類NodePath的例項,到這裡鎖就持久化到磁碟上了。

node.lock

補充一句,這個地方涉及到了ElasticSearch的引數max_local_storage_nodes,這個配置限制了單節點上可以開啟的ES儲存例項的個數,如果我們需要開多個例項,就要把這個配置寫到配置檔案中,併為這個配置賦值為2或者更高,這樣的話ElasticSearch就會用for迴圈建立多個NodePath,而不只是建立唯一的那個ID為0的例項。

在NodeEnvironment中載入或建立Node後設資料

接下類是構造NodeMetaData節點後設資料,這個後設資料有個關鍵資料叫nodeId,構造出來後是形如D2_COg3LTUeQcrYjcj_fQQ這樣的字串。

程式執行到這個地方,其內部類NodePath的物件裡已經儲存了節點目錄xxxxdata
odes
和節點索引目錄xxxxdata
odes indices
,如下圖所示:

NodePath例項

程式首先通過DirectoryStream<Path> paths = Files.newDirectoryStream(stateDir)遍歷data
odes _state
資料夾下的狀態檔案,再通過匹配正規表示式Qnode-E(d+)(.st)?,查詢到狀態檔案node-xxx.st

注意,如果有多個資料儲存路徑,那麼狀態資料夾下可能會有多個最新狀態版本。這種情況下,只會取最高的版本。如果至少有一個狀態檔案使用了新的格式(format,也就是編碼中的legacy==false),那麼最新的狀態檔案肯定是最新的的格式(format)。如果不是使用最新的狀態檔案,那編碼中的pathAndStateIds值是空的,且會在日誌中報載入狀態檔案失敗的錯誤。

狀態檔案

最後從node-xxx.st檔案中讀出ID,至此NodeMetaData物件的nodeId欄位就被賦值了。而這個ID的字首也被作為Logger的marker值被注入。

至此nodeEnvironment = new NodeEnvironment(tmpSettings, environment);的工作就結束了,總而言之就是載入了狀態引數到記憶體中。

下一篇會講述pluginsService相關的內容,希望大家持續關注哦^ _ ^。

相關文章