前面几篇我们分析了Elasticsearch的启动过程和线程池部分的源码,这里我们来分析一下Elasticsearch中的JNA使用和swap的那些事。
initializeNatives方法入参
来看一段org.elasticsearch.bootstrap.Bootstrap#setup中的代码:
代码语言:javascript复制 //初始化本地的一些配置,如创建临时文件等 initializeNatives( environment.tmpFile(), BootstrapSettings.MEMORY_LOCK_SETTING.get(settings), BootstrapSettings.SYSTEM_CALL_FILTER_SETTING.get(settings), BootstrapSettings.CTRLHANDLER_SETTING.get(settings));
在此之前我们先来看下方法的几个参数,org.elasticsearch.bootstrap.BootstrapSettings中源码如下:
代码语言:javascript复制 // TODO: remove this hack when insecure defaults are removed from java public static final Setting<Boolean> SECURITY_FILTER_BAD_DEFAULTS_SETTING = Setting.boolSetting("security.manager.filter_bad_defaults", true, Property.NodeScope);
public static final Setting<Boolean> MEMORY_LOCK_SETTING = Setting.boolSetting("bootstrap.memory_lock", false, Property.NodeScope); public static final Setting<Boolean> SYSTEM_CALL_FILTER_SETTING = Setting.boolSetting("bootstrap.system_call_filter", true, Property.NodeScope); public static final Setting<Boolean> CTRLHANDLER_SETTING = Setting.boolSetting("bootstrap.ctrlhandler", true, Property.NodeScope);
这里对initializeNatives传入了四个参数,参数说明如下:
- tmpFile:配置中传入的临时文件路径;
- bootstrap.memory_lock:内存锁的配置,默认为false;
- bootstrap.systemcallfilter:系统调用过滤器,布尔类型,默认为true;
- bootstrap.ctrlhandler:按ctrl键的处理器,布尔类型,默认为true。
进入org.elasticsearch.bootstrap.Bootstrap#initializeNatives方法内部,我们还是一步步分析。
org.elasticsearch.bootstrap.Bootstrap#initializeNatives方法内部分析
一步步来分析,先上第一段的代码:
代码语言:javascript复制 /** initialize native resources */ public static void initializeNatives(Path tmpFile, boolean mlockAll, boolean systemCallFilter, boolean ctrlHandler) { final Logger logger = LogManager.getLogger(Bootstrap.class);
// check if the user is running as root, and bail // 校验下是否是在root用户下运行 if (Natives.definitelyRunningAsRoot()) { throw new RuntimeException("can not run elasticsearch as root"); }
// enable system call filter if (systemCallFilter) { Natives.tryInstallSystemCallFilter(tmpFile); } // 是否mlockall 这个参数可由用户传入,默认为false // mlockall if requested if (mlockAll) { if (Constants.WINDOWS) { // windows下中虚拟锁,即JNAKernel32Library中的锁定内存方法 Natives.tryVirtualLock(); } else { // linux和mac中的锁 JNACLibrary中的方法 Natives.tryMlockall(); } }
// listener for windows close event if (ctrlHandler) { Natives.addConsoleCtrlHandler(new ConsoleCtrlHandler() { @Override public boolean handle(int code) { if (CTRL_CLOSE_EVENT == code) { logger.info("running graceful exit on windows"); try { Bootstrap.stop(); } catch (IOException e) { throw new ElasticsearchException("failed to stop node", e); } return true; } return false; } }); }
// force remainder of JNA to be loaded (if available). try { JNAKernel32Library.getInstance(); } catch (Exception ignored) { // we've already logged this. } // 设置线程最大数 Natives.trySetMaxNumberOfThreads(); // 设置虚拟内存的最大值 Natives.trySetMaxSizeVirtualMemory(); // 设置最大文件大小 Natives.trySetMaxFileSize();
// init lucene random seed. it will use /dev/urandom where available: // 初始化lucene的随机种子 StringHelper.randomId(); }
在分析这个方法之前,我们先看一下两个用于处理内存锁定等操作的类:
- org.elasticsearch.bootstrap.JNAKernel32Library,它的调用链是首先在上面方法中的当判断当前为windows系统并且需要锁定时,调用的是Natives.tryVirtualLock()方法,进入方法内部调用的是JNANatives.tryVirtualLock()方法,继续进入方法内部调用的是JNAKernel32Library中的方法。
- org.elasticsearch.bootstrap.JNACLibrary,它的调用链就是上面方法中当判断当前非windows系统时,会调用Natives.tryMlockall()方法,进入内部调用的是JNANatives.tryMlockall()方法,继续进入方法内部调用的是JNACLibrary中的方法
使用java调用dll和cpp文件方法很多,可以使用jni,jna,jnative等,其中jni使用步骤太麻烦,而且只能调用自己生成的dll文件,有局限性。Jnative存在32位和64位系统的问题,貌似64位系统不能使用,而且调用方法也很麻烦。所以,采用jna比较适合。
JNA(Java Native Access)框架是一个开源的Java框架,是SUN公司主导开发的,建立在经典的JNI的基础之上的一个框架。
JNA项目地址:https://jna.dev.java.net/
JNA的安装很简单,把从官网下载的jna.jar包导入工作路径就可以了。
JNACLibray是通过JNA来调用linux和mac中的cpp库文件的,JNAKernel32Libray是通过调用windows的kernel32.dll来执行相关操作的。
对于JNACLibray库和JNAKernel32Libray库的使用我们这里不再深究,有兴趣的可以自己down一份es源码来学习,这里我们主要关注下一个用户可以自定义的参数bootstrap.memory_lock在这里的作用,关于mlockAll的说明在网上找到了一份中英文对照版本(地址:https://www.cnblogs.com/bonelee/p/6207097.html):
代码语言:javascript复制mlock() and mlockall() respectively lock part or all of the calling process's virtual address space into RAM, preventing that memory from being paged to the swap area. munlock() andmunlockall() perform the converse operation, respectively unlocking part or all of the calling process's virtual address space, so that pages in the specified virtual address range may once more to be swapped out if required by the kernel memory manager. Memory locking and unlocking are performed in units of whole pages.
mlock系统调用的作用:mlock系统调用允许程序在物理内存上锁住它的部分或全部地址空间,这将阻止Linux将这个内存页调度到交换空间(即阻止系统将某个页面换出到交换分区),即使该程序已有一段时间没有访问这段空间。一个严格时间相关的程序可能会希望锁住物理内存,因为内存页面调出调入的时间延迟可能太长或过于不可预知。安全性要求较高的应用程序可能希望防止敏感数据被换出到交换文件中,因为这样在程序结束后,攻击者可能从交换文件中恢复出这些数据。锁定一个内存区间只需简单将指向区间开始的指针及区间长度作为参数调用mlock()。Linux分配内存到页且每次只能锁定整页内存,被指定的区间涉及到的每个内存页都将被锁定。
mlock参数的目的是当你无法关闭系统的swap的时候,建议把这个参数设为true。防止在内存不够用的时候,elasticsearch的内存被交换至交换区,导致性能骤降。
那么什么是swap呢?
在Linux下,SWAP的作用类似Windows系统下的“虚拟内存”。当物理内存不足时,拿出部分硬盘空间当SWAP分区(虚拟成内存)使用,从而解决内存容量不足的情况。
SWAP意思是交换,顾名思义,当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在SWAP分区中,这个过程称为SWAP OUT。当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把SWAP分区中的数据交换回物理内存中,这个过程称为SWAP IN。
当然,swap大小是有上限的,一旦swap使用完,操作系统会触发OOM-Killer机制,把消耗内存最多的进程kill掉以释放内存。
那么elasticsearch使用swap是好还是坏呢?
- es一般对响应延迟比较敏感,而swap场景下进程虽然不会被kill,但是会一直处理不可用状态。
- swap场景下进程会一直hang住,导致es集群请求阻塞。
所以,一般情况下还是不用swap为好。如果想了解更多swap的知识,可以参考下:https://www.jianshu.com/p/73847b688728。
最后我简单地贴一下linux和mac环境下jna的工作方法,也就是JNACLibrary中的方法,代码如下:
代码语言:javascript复制/** * java mapping to some libc functions */final class JNACLibrary {
private static final Logger logger = LogManager.getLogger(JNACLibrary.class);
public static final int MCL_CURRENT = 1; public static final int ENOMEM = 12; public static final int RLIMIT_MEMLOCK = Constants.MAC_OS_X ? 6 : 8; public static final int RLIMIT_AS = Constants.MAC_OS_X ? 5 : 9; public static final int RLIMIT_FSIZE = Constants.MAC_OS_X ? 1 : 1; public static final long RLIM_INFINITY = Constants.MAC_OS_X ? 9223372036854775807L : -1L;
static { try { Native.register("c"); } catch (UnsatisfiedLinkError e) { logger.warn("unable to link C library. native methods (mlockall) will be disabled.", e); } }
/** * 系统调用 mlock 家族允许程序在物理内存上锁住它的部分或全部地址空间。 * 这将阻止Linux 将这个内存页调度到交换空间(swap space),即使该程序已有一段时间没有访问这段空间。 * mlockall() 锁定调用进程所有映射到地址空间的分页。这包括代码、数据、栈片段分页,同时也包括共享库、 * 用户空间内核数据、共享内存以及内存映射的文件。调用成功返回后所有映射的分页都保证在 RAM 中: * 直到后来的解锁,这些分页都保证一直在 RAM 内。 * @see:https://blog.csdn.net/sinkou/article/details/75143875 * @param flags * @return */ static native int mlockall(int flags);
/** * geteuid():返回有效用户的ID。getuid():返回实际用户的ID。 * 实际用户:表示一开始执行程序的用户,比如用账号iceup登录shell,然后执行程序ls,那么实际用户就是iceup。 * 有效用户:有效用户是指在程序运行时,计算权限的用户。大多数情况下实际用户和有效用户相等, * 但是在执行拥有SUID权限的程序的时候,这两个用户通常会不一致。总结,有效用户ID(EUID)是你最初执行程序时所用的ID, * 表示该ID是程序的所有者。真实用户ID(UID)是程序执行过程中采用的ID,该ID表明当前运行位置程序的执行者。 * @see:https://blog.csdn.net/sinkou/article/details/75143875 * @return */ static native int geteuid();
/** corresponds to struct rlimit */ public static final class Rlimit extends Structure implements Structure.ByReference { public NativeLong rlim_cur = new NativeLong(0); public NativeLong rlim_max = new NativeLong(0);
@Override protected List<String> getFieldOrder() { return Arrays.asList("rlim_cur", "rlim_max"); } }
/** * getrlimit查询进程所受的系统限制。这些系统限制通过一对硬/软限制对来指定。 * 当一个软限制被超过时,进程还可以继续。另一方面,进程不可以超过它的硬限制。 * 软限制值可以被进程设置在位于0和最大硬限制间的任意值。硬限制值不能被任何进程降低,仅仅超级用户可以增加。 * * getrlimit和setrlimit都使用下面的数据结构: * * struct rlimit { * rlim_t rlim_cur; * rlim_t rlim_max; * }; * rlim_cur 为指定的资源指定当前的系统软限制。rlim_max 将为指定的资源指定当前的系统硬限制。 * @see:https://blog.csdn.net/sinkou/article/details/75143875 * @param resource * @param rlimit * @return */ static native int getrlimit(int resource, Rlimit rlimit); static native int setrlimit(int resource, Rlimit rlimit);
/** * 当linuc C api函数发生异常时,一般会将errno变量赋一个整数值,不同的值表示不同的含义,可以使用strerror()获取错误的信息。 * 例如errno等于12的话,它就会返回”Cannot allocate memory”。 * @param errno * @return */ static native String strerror(int errno);
private JNACLibrary() { }}
到这里,关于elasticsearch的mlockall配置及其与swap之间的关系就简单地介绍完了,更多关于JNA的使用请自行查阅相关类库或文档。