玩轉Elasticsearch原始碼-一圖看懂ES啟動流程

weixin_33766168發表於2019-01-10

開篇

直接看圖
ES啟動流程.png

上圖中虛線表示進入具體流程,實線表示下一步,為了後面講解方便每個步驟都加了編號。
先簡單介紹下啟動流程主要涉及的類:

  • org.elasticsearch.bootstrap.Elasticsearch: 啟動入口,main方法就在這個類裡面,執行邏輯對應圖中綠色部分
  • org.elasticsearch.bootstrap.Bootstrap:包含主要啟動流程程式碼,執行邏輯對應圖中紅色部分
  • org.elasticsearch.node.Node:代表叢集中的節點,執行邏輯對應圖中藍色部分

流程講解

1. main方法

2. 設定了一個空的SecurityManager:

// we want the JVM to think there is a security manager installed so that if internal policy decisions that would be based on the
// presence of a security manager or lack thereof act as if there is a security manager present (e.g., DNS cache policy)
//我們希望JVM認為已經安裝了一個安全管理器,這樣,如果基於安全管理器的存在或缺少安全管理器的內部策略決策就會像有一個安全管理器一樣(e.g.、DNS快取策略)
// grant all permissions so that we can later set the security manager to the one that we want
//授予所有許可權,以便稍後可以將安全管理器設定為所需的許可權

新增StatusConsoleListener到STATUS_LOGGER:

We want to detect situations where we touch logging before the configuration is loaded . If we do this , Log 4 j will status log an error message at the error level . With this error listener , we can capture if this happens . More broadly , we can detect any error - level status log message which likely indicates that something is broken . The listener is installed immediately on startup , and then when we get around to configuring logging we check that no error - level log messages have been logged by the status logger . If they have we fail startup and any such messages can be seen on the console
我們希望檢測在載入配置之前進行日誌記錄的情況。如果這樣做,log4j將在錯誤級別記錄一條錯誤訊息。使用這個錯誤監聽器,我們可以捕捉到這種情況。更廣泛地說,我們可以檢測任何錯誤級別的狀態日誌訊息,這些訊息可能表示某個東西壞了。偵聽器在啟動時立即安裝,然後在配置日誌記錄時,我們檢查狀態日誌記錄器沒有記錄錯誤級別的日誌訊息。如果它們啟動失敗,我們可以在控制檯上看到任何此類訊息。

例項化Elasticsearch:

Elasticsearch() {
        super("starts elasticsearch", () -> {}); // () -> {} 是啟動前的回撥
        //下面解析version,daemonize,pidfile,quiet引數
        versionOption = parser.acceptsAll(Arrays.asList("V", "version"),
            "Prints elasticsearch version information and exits");
        daemonizeOption = parser.acceptsAll(Arrays.asList("d", "daemonize"),
            "Starts Elasticsearch in the background")
            .availableUnless(versionOption);
        pidfileOption = parser.acceptsAll(Arrays.asList("p", "pidfile"),
            "Creates a pid file in the specified path on start")
            .availableUnless(versionOption)
            .withRequiredArg()
            .withValuesConvertedBy(new PathConverter());
        quietOption = parser.acceptsAll(Arrays.asList("q", "quiet"),
            "Turns off standard output/error streams logging in console")
            .availableUnless(versionOption)
            .availableUnless(daemonizeOption);
    }

3.註冊ShutdownHook,用於關閉系統時捕獲IOException到terminal

            shutdownHookThread = new Thread(() -> {
                try {
                    this.close();
                } catch (final IOException e) {
                    try (
                        StringWriter sw = new StringWriter();
                        PrintWriter pw = new PrintWriter(sw)) {
                        e.printStackTrace(pw);
                        terminal.println(sw.toString());
                    } catch (final IOException impossible) {
                        // StringWriter#close declares a checked IOException from the Closeable interface but the Javadocs for StringWriter
                        // say that an exception here is impossible
                        throw new AssertionError(impossible);
                    }
                }
            });
            Runtime.getRuntime().addShutdownHook(shutdownHookThread);

然後呼叫beforeMain.run(),其實就是上面例項化Elasticsearch物件時建立的()->{} lambda表示式。

4.進入Command類的mainWithoutErrorHandling方法

 void mainWithoutErrorHandling(String[] args, Terminal terminal) throws Exception {
        final OptionSet options = parser.parse(args);//根據提供給解析器的選項規範解析給定的命令列引數

        if (options.has(helpOption)) {
            printHelp(terminal);
            return;
        }

        if (options.has(silentOption)) {//terminal列印最少內容
            terminal.setVerbosity(Terminal.Verbosity.SILENT);
        } else if (options.has(verboseOption)) {//terminal列印詳細內容
            terminal.setVerbosity(Terminal.Verbosity.VERBOSE);
        } else {
            terminal.setVerbosity(Terminal.Verbosity.NORMAL);
        }

        execute(terminal, options);
    }

5.進入EnvironmentAwareCommand的execute方法

protected void execute(Terminal terminal, OptionSet options) throws Exception {
        final Map<String, String> settings = new HashMap<>();
        for (final KeyValuePair kvp : settingOption.values(options)) {
            if (kvp.value.isEmpty()) {
                throw new UserException(ExitCodes.USAGE, "setting [" + kvp.key + "] must not be empty");
            }
            if (settings.containsKey(kvp.key)) {
                final String message = String.format(
                        Locale.ROOT,
                        "setting [%s] already set, saw [%s] and [%s]",
                        kvp.key,
                        settings.get(kvp.key),
                        kvp.value);
                throw new UserException(ExitCodes.USAGE, message);
            }
            settings.put(kvp.key, kvp.value);
        }

        //確保給定的設定存在,如果尚未設定,則從系統屬性中讀取它。
        putSystemPropertyIfSettingIsMissing(settings, "path.data", "es.path.data");
        putSystemPropertyIfSettingIsMissing(settings, "path.home", "es.path.home");
        putSystemPropertyIfSettingIsMissing(settings, "path.logs", "es.path.logs");

        execute(terminal, options, createEnv(terminal, settings));
    }

6.進入InternalSettingsPreparer的prepareEnvironment方法,讀取elasticsearch.yml並建立Environment。細節比較多,後面再細講。

Environment物件.png

7.判斷是否有-v引數,沒有則準備進入init流程

 protected void execute(Terminal terminal, OptionSet options, Environment env) throws UserException {
        if (options.nonOptionArguments().isEmpty() == false) {
            throw new UserException(ExitCodes.USAGE, "Positional arguments not allowed, found " + options.nonOptionArguments());
        }
        if (options.has(versionOption)) { //如果有 -v 引數,列印版本號後直接退出
            terminal.println("Version: " + Version.displayVersion(Version.CURRENT, Build.CURRENT.isSnapshot())
                    + ", Build: " + Build.CURRENT.shortHash() + "/" + Build.CURRENT.date()
                    + ", JVM: " + JvmInfo.jvmInfo().version());
            return;
        }

        final boolean daemonize = options.has(daemonizeOption);
        final Path pidFile = pidfileOption.value(options);
        final boolean quiet = options.has(quietOption);

        try {
            init(daemonize, pidFile, quiet, env);
        } catch (NodeValidationException e) {
            throw new UserException(ExitCodes.CONFIG, e.getMessage());
        }
    }

8.呼叫Bootstrap.init

9.例項化Boostrap。保持keepAliveThread存活,可能是用於監控

Bootstrap() {
        keepAliveThread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    keepAliveLatch.await();
                } catch (InterruptedException e) {
                    // bail out
                }
            }
        }, "elasticsearch[keepAlive/" + Version.CURRENT + "]");
        keepAliveThread.setDaemon(false);
        // keep this thread alive (non daemon thread) until we shutdown 保持這個執行緒存活(非守護程式執行緒),直到我們關機
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                keepAliveLatch.countDown();
            }
        });
    }

10.載入elasticsearch.keystore檔案,重新建立Environment,然後呼叫LogConfigurator的靜態方法configure,讀取config目錄下log4j2.properties然後配log4j屬性

11.建立pid檔案,檢查lucene版本,不對應則丟擲異常

 private static void checkLucene() {
        if (Version.CURRENT.luceneVersion.equals(org.apache.lucene.util.Version.LATEST) == false) {
            throw new AssertionError("Lucene version mismatch this version of Elasticsearch requires lucene version ["
                + Version.CURRENT.luceneVersion + "]  but the current lucene version is [" + org.apache.lucene.util.Version.LATEST + "]");
        }
    }

12.設定ElasticsearchUncaughtExceptionHandler用於列印fatal日誌

            // install the default uncaught exception handler; must be done before security is
            // initialized as we do not want to grant the runtime permission
            // 安裝預設未捕獲異常處理程式;必須在初始化security之前完成,因為我們不想授予執行時許可權
            // setDefaultUncaughtExceptionHandler
            Thread.setDefaultUncaughtExceptionHandler(
                new ElasticsearchUncaughtExceptionHandler(() -> Node.NODE_NAME_SETTING.get(environment.settings())));

13.進入Boostrap.setup

14.spawner.spawnNativePluginControllers(environment);嘗試為給定模組生成控制器(native Controller)守護程式。 生成的程式將通過其stdin,stdout和stderr流保持與此JVM的連線,但對此包之外的程式碼不能使用對這些流的引用。

15.初始化本地資源 initializeNatives():

檢查使用者是否作為根使用者執行,是的話拋異常;系統呼叫和mlockAll檢查;嘗試設定最大執行緒數,最大虛擬記憶體,最大FD等。
初始化探針initializeProbes(),用於作業系統,程式,jvm的監控。

16.又加一個ShutdownHook

        if (addShutdownHook) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    try {
                        IOUtils.close(node, spawner);
                        LoggerContext context = (LoggerContext) LogManager.getContext(false);
                        Configurator.shutdown(context);
                    } catch (IOException ex) {
                        throw new ElasticsearchException("failed to stop node", ex);
                    }
                }
            });
        }

17.比較簡單,直接看程式碼

 try {
            // look for jar hell
            JarHell.checkJarHell();
        } catch (IOException | URISyntaxException e) {
            throw new BootstrapException(e);
        }

        // Log ifconfig output before SecurityManager is installed
        IfConfig.logIfNecessary();

        // install SM after natives, shutdown hooks, etc.
        try {
            Security.configure(environment, BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING.get(settings));
        } catch (IOException | NoSuchAlgorithmException e) {
            throw new BootstrapException(e);
        }

18.例項化Node

重寫validateNodeBeforeAcceptingRequests方法。具體主要包括三部分,第一是啟動外掛服務(es提供了外掛功能來進行擴充套件功能,這也是它的一個亮點),載入需要的外掛,第二是配置node環境,最後就是通過guice載入各個模組。下面22~32就是具體步驟。

19.進入Boostrap.start

20.node.start啟動節點

21.keepAliveThread.start

22.Node例項化第一步,建立NodeEnvironment

NodeEnvironment.png

23.生成nodeId,列印nodeId,nodeName和jvmInfo和程式資訊

24.建立 PluginsService 物件,建立過程中會讀取並載入所有的模組和外掛

25.又建立Environment

            // create the environment based on the finalized (processed) view of the settings 根據設定的最終(處理)檢視建立環境
            // this is just to makes sure that people get the same settings, no matter where they ask them from 這只是為了確保人們得到相同的設定,無論他們從哪裡詢問
            this.environment = new Environment(this.settings, environment.configFile());

26.建立ThreadPool,然後給DeprecationLogger設定ThreadContext

27.建立NodeClient,用於執行actions

28.建立各個Service:

ResourceWatcherService、NetworkService、ClusterService、IngestService、ClusterInfoService、UsageService、MonitorService、CircuitBreakerService、MetaStateService、IndicesService、MetaDataIndexUpgradeService、TemplateUpgradeService、TransportService、ResponseCollectorService、SearchTransportService、NodeService、SearchService、PersistentTasksClusterService

29.建立並新增modules:

ScriptModule、AnalysisModule、SettingsModule、pluginModule、ClusterModule、IndicesModule、SearchModule、GatewayModule、RepositoriesModule、ActionModule、NetworkModule、DiscoveryModule

30.Guice繫結和注入物件

31.初始化NodeClient

            client.initialize(injector.getInstance(new Key<Map<GenericAction, TransportAction>>() {}),
                    () -> clusterService.localNode().getId());

32.初始化rest處理器,這個非常重要,後面會專門講解

if (NetworkModule.HTTP_ENABLED.get(settings)) {
                logger.debug("initializing HTTP handlers ..."); // 初始化http handler
                actionModule.initRestHandlers(() -> clusterService.state().nodes());
            }

33.修改狀態為State.STARTED

34.啟動pluginLifecycleComponents

35.通過 injector 獲取各個類的物件,呼叫 start() 方法啟動(實際進入各個類的中 doStart 方法)

LifecycleComponent、IndicesService、IndicesClusterStateService、SnapshotsService、SnapshotShardsService、RoutingService、SearchService、MonitorService、NodeConnectionsService、ResourceWatcherService、GatewayService、Discovery、TransportService

36.啟動HttpServerTransport和TransportService並繫結埠

if (WRITE_PORTS_FILE_SETTING.get(settings)) {
            if (NetworkModule.HTTP_ENABLED.get(settings)) {
                HttpServerTransport http = injector.getInstance(HttpServerTransport.class);
                writePortsFile("http", http.boundAddress());
            }
            TransportService transport = injector.getInstance(TransportService.class);
            writePortsFile("transport", transport.boundAddress());
        }

總結

  • 本文只是講解了ES啟動的整體流程,其中很多細節會在本系列繼續深入講解
  • ES的原始碼讀起來還是比較費勁的,流程比較長,沒有Spring原始碼讀起來體驗好,這也是開源軟體和開源框架的區別之一,前者會遇到大量的流程細節,注重具體功能的實現,後者有大量擴充套件點,更注重擴充套件性。
  • 為什麼要讀開源原始碼?

1.知道底層實現,能夠更好地使用,出問題能夠快速定位和解決。
2.學習別人優秀的程式碼和處理問題的方式,提高自己的系統設計能力。
3.有機會可以對其進行擴充套件和改造。

相關文章