1. 程式人生 > 其它 >【ElasticSearch】service啟動流程

【ElasticSearch】service啟動流程

技術標籤:大資料相關elasticsearch

目錄

Es啟動流程

main

啟動方法是org.elasticsearch.bootstrap.Elasticsearch.main,程式碼如下:

public static void main(final String[] args) throws Exception {
        // 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) System.setSecurityManager(new SecurityManager() { @Override public void checkPermission(Permission perm) { // grant all permissions so that we can later set the security manager to the one that we want
} }); LogConfigurator.registerErrorListener(); final Elasticsearch elasticsearch = new Elasticsearch(); int status = main(args, elasticsearch, Terminal.DEFAULT); if (status != ExitCodes.OK) { exit(status); } }

做了三件事:

  1. 載入安全配置
  2. 註冊錯誤監聽器
  3. 建立Elasticsearch例項。如果建立失敗則exit

接下來細看一下其中的main()。

static int main(final String[] args, final Elasticsearch elasticsearch, final Terminal terminal) throws Exception {
        return elasticsearch.main(args, terminal);
    }

Elasticsearch的靜態main函式呼叫了一個非靜態的main。這個main的實現位於Command中。

Command::main步驟:

  1. 新增Shutdown Hook。

Shutdown Hook是jvm在關閉服務時用於清理關閉資源的機制。當jvm關閉時,會先呼叫addShutdownHook中新增的所有Hook,系統執行完畢後,jvm才會關閉。因此,Shutdown Hook可以在關閉jvm時進行記憶體清理、物件銷燬等操作。

  1. beforeMain.run()執行main之前的操作,如日誌配置的載入。
  2. 呼叫mainWithoutErrorHandling。該方法位於Command類中。
public final int main(String[] args, Terminal terminal) throws Exception {
        if (addShutdownHook()) {
            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();

        try {
            mainWithoutErrorHandling(args, terminal);
        } catch (OptionException e) {
            printHelp(terminal);
            terminal.println(Terminal.Verbosity.SILENT, "ERROR: " + e.getMessage());
            return ExitCodes.USAGE;
        } catch (UserException e) {
            if (e.exitCode == ExitCodes.USAGE) {
                printHelp(terminal);
            }
            terminal.println(Terminal.Verbosity.SILENT, "ERROR: " + e.getMessage());
            return e.exitCode;
        }
        return ExitCodes.OK;
    }

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.setVerbosity(Terminal.Verbosity.SILENT);
        } else if (options.has(verboseOption)) {
            terminal.setVerbosity(Terminal.Verbosity.VERBOSE);
        } else {
            terminal.setVerbosity(Terminal.Verbosity.NORMAL);
        }

        execute(terminal, options);
    }

核心程式碼是execute()。在Command類當中,該方法是個抽象方法:

protected abstract void execute(Terminal terminal, OptionSet options) throws Exception;

那麼具體執行的是哪個實現,就要看一下上文中涉及的兩個類的繼承關係。

execute

在這裡插入圖片描述

程式入口所在的Elasticsearch繼承了EnvironmentAwareCommand, EnvironmentAwareCommand繼承Command並實現execute方法。

因此在呼叫elasticSearch.main()時,實際執行的是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));
    }

上述方法先讀取命令列配置,並檢查其中三個關鍵配置es.path.data、es.path.home、es.path.logs。接下來在進行execute。

這裡的execute在EnvironmentAwareCommand中同樣也是抽象函式:

protected abstract void execute(Terminal terminal, OptionSet options, Environment env) throws Exception;

這裡執行的是Elasticsearch::execute的實現。

protected void execute(Terminal terminal, OptionSet options, Environment env) throws UserException {
        
    	...

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

上述程式碼省略了引數校驗的部分,我們直接關注init方法。

init

void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment initialEnv)
        throws NodeValidationException, UserException {
        try {
            Bootstrap.init(!daemonize, pidFile, quiet, initialEnv);
        } catch (BootstrapException | RuntimeException e) {
            // format exceptions to the console in a special way
            // to avoid 2MB stacktraces from guice, etc.
            throw new StartupException(e);
        }
    }

Elasticsearch::init呼叫了Bootstrap.init方法,es啟動的執行過程是在方法中。程式碼很長,但是主要做了以下幾個步驟:

  1. 建立Bootstrap例項
  2. 載入安全配置
  3. 載入環境配置
  4. 配置logger
  5. 建立pid檔案
  6. 呼叫Bootstrap::setup
  7. 呼叫Bootstrap::start
static void init(
            final boolean foreground,
            final Path pidFile,
            final boolean quiet,
            final Environment initialEnv) throws BootstrapException, NodeValidationException, UserException {
        // force the class initializer for BootstrapInfo to run before
        // the security manager is installed
        BootstrapInfo.init();

        INSTANCE = new Bootstrap();

        final SecureSettings keystore = loadSecureSettings(initialEnv);
        final Environment environment = createEnvironment(foreground, pidFile, keystore, initialEnv.settings(), initialEnv.configFile());
        try {
            LogConfigurator.configure(environment);
        } catch (IOException e) {
            throw new BootstrapException(e);
        }
        if (environment.pidFile() != null) {
            try {
                PidFile.create(environment.pidFile(), true);
            } catch (IOException e) {
                throw new BootstrapException(e);
            }
        }

        	...

            INSTANCE.setup(true, environment);

            ...

            INSTANCE.start();

            ...
        } catch (NodeValidationException | RuntimeException e) {
            ...
        }
    }

接下來細看一下setup和start方法。

setup方法用於初始化環境配置並建立Node例項。詳細步驟如下:

  1. environment.settings獲取環境配置

  2. spawnNativeControllers為每個模組(Plugin)建立native Controller,並通過ProcessBuilder建立子程序(Process)例項

  3. initializeNatives初始化本地資源:

    • 如果是root使用者,丟擲錯誤
    • 啟用SystemCallFilter(需要JNA)
    • 設定mlockAll

    mlockAll配置:jvm在gc時會將堆中的頁寫回磁碟,當再次需要讀取該頁面時再從磁碟讀取。如果Elasticsearch的頁被jvm通過gc寫入磁碟,再次進行讀取時會產生大量磁碟抖動從而影響Es效能。因此,可以通過開啟memory lock將Es的堆頁面進行鎖定,不允許gc將Es的堆頁面進行交換,防止上述情況產生。

    https://www.elastic.co/guide/en/elasticsearch/reference/6.3/_memory_lock_check.html

    • 建立Windows關閉事件的監聽器,再監聽器當中呼叫Bootstrap::stop關閉服務
    • 初始化Lucene隨機數種子
  4. initializeProbes初始化探測器。該方法例項化了兩種探測器:ProcessProbe和OsProbe。分別對程序情況和系統情況進行監控。

  5. 新增Shutdown Hook

  6. 檢查Jar Hell

  7. 格式化輸出ifconfig日誌

  8. 初始化SecurityManager

  9. 建立Node節點。Node的建立還不是這一部分的重點,放到Node相關內容細看。

Jar Hell:jar包衝突

private void setup(boolean addShutdownHook, Environment environment) throws BootstrapException {
    	// 1. 獲取環境配置
        Settings settings = environment.settings();

    	// 2. 建立native controllers並建立程序例項
        try {
            spawner.spawnNativeControllers(environment);
        } catch (IOException e) {
            throw new BootstrapException(e);
        }

    	// 3. 初始化本地資源
        initializeNatives(
                environment.tmpFile(),
                BootstrapSettings.MEMORY_LOCK_SETTING.get(settings),
                BootstrapSettings.SYSTEM_CALL_FILTER_SETTING.get(settings),
                BootstrapSettings.CTRLHANDLER_SETTING.get(settings));

    	// 4. 初始化程序和系統監控
        // initialize probes before the security manager is installed
        initializeProbes();

    	// 5. 新增shutdown hook
        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);
                    }
                }
            });
        }

    	// 6. 檢查jar包衝突
        try {
            // look for jar hell
            final Logger logger = ESLoggerFactory.getLogger(JarHell.class);
            JarHell.checkJarHell(logger::debug);
        } catch (IOException | URISyntaxException e) {
            throw new BootstrapException(e);
        }

    	// 7. 輸出ifconfig日誌
        // Log ifconfig output before SecurityManager is installed
        IfConfig.logIfNecessary();

    	// 8. 初始化SecurityManager
        // 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);
        }

    	// 9. 建立Node例項
        node = new Node(environment) {
            @Override
            protected void validateNodeBeforeAcceptingRequests(
                final BootstrapContext context,
                final BoundTransportAddress boundTransportAddress, List<BootstrapCheck> checks) throws NodeValidationException {
                BootstrapChecks.check(context, boundTransportAddress, checks);
            }
        };
    }

到這裡,節點的相關配置就設定完成了。接下來執行Bootstrap::start。

start方法的作用是啟用節點和KeepAliveThread。程式碼如下:

private void start() throws NodeValidationException {
        node.start();
        keepAliveThread.start();
    }

node.start()的內容不在這裡詳細說。我們來看看keepAliveThread的定義。

private final CountDownLatch keepAliveLatch = new CountDownLatch(1);
private final Thread keepAliveThread;

/** creates a new instance */
    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();
            }
        });
    }

在Bootstrap的建構函式中初始化了keepAliveThread。其中KeepAliveLatch的詳細介紹與應用見:

https://blog.csdn.net/u012637358/article/details/90288585

在建構函式中,keepAliveLatch在Shutdown Hook當中才會呼叫countDown,因此keepAlive執行緒會一直處於await直到服務被關閉。

為什麼要有一個KeepAlive線城呢?在Es的程式碼中,main函式執行完畢後會返回ExitCode.OK並退出,查詢請求的執行將由NodeClient來執行。而主執行緒退出後,需要維持一個使用者執行緒來保持程序不退出。