ElasticSearch 实践过程中遇到的几个小问题

公司项目最近要增加一个搜索的模块,虽然之前已经在项目中广泛的使用了 Lucene,但是这次这个模块再直接使用 Lucene 的话还得担心以后的扩展问题,所以就决定尝试用 ElasticSearch 来解决。(为什么不用 Solr 呢?其实 Solr 和 ES 的差别不大,它俩的对比评测网上也是一搜一大把,我主要是之前使用过 ELK 技术栈,所以还是更倾向于 ES)

这篇文章就简单的对我在使用 ES 的过程中遇到的问题做一个记录,权当做笔记吧。

ulimit “不生效”

有一台机器的在启动 ES 的时候始终报错

1
max file descriptors [65000] for elasticsearch process is too low

但是我已经在/etc/security/limits.conf里增加了如下配置,

1
2
3
4
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited
elasticsearch hard nofile 65536
elasticsearch soft nofile 65536

按理说ulimit -n的时候应该是会到看到 65536 的,但是依旧输出的是 65000。郁闷

最后受这篇文章ulimit到底谁说了算?的启发,我把排查目标放在了 profile 和 bashrc 文件上面。

最终发现原来还真是有人在这台机器的/etc/bashrc的文末加了一句ulimit -n 65000,把这行去掉后就正常了。

如果你也出现了这个问题,建议排查一下以下四个文件:

1
2
3
4
/etc/profile
/etc/bashrc
~/.profile
~/.bashrc

由 X-Pack 的认证机制引起的问题

X-Pack是一个Elastic Stack的扩展,将安全,警报,监视,报告和图形功能包含在一个易于安装的软件包中。在Elasticsearch 5.0.0之前,您必须安装单独的Shield,Watcher和Marvel插件才能获得在X-Pack中所有的功能。

ES、Kibana 和 X-Pack 的安装很简单,有需要的可以参看:安装Elasticsearch、Kibana和X-Pack

HTTP REST API

在终端里访问 REST API 的时候

1
curl -XGET 'localhost:9200/_cat/health?v&pretty'

会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"error" : {
"root_cause" : [
{
"type" : "security_exception",
"reason" : "missing authentication token for REST request [/_cat/health?v&pretty]",
"header" : {
"WWW-Authenticate" : "Basic realm=\"security\" charset=\"UTF-8\""
}
}
],
"type" : "security_exception",
"reason" : "missing authentication token for REST request [/_cat/health?v&pretty]",
"header" : {
"WWW-Authenticate" : "Basic realm=\"security\" charset=\"UTF-8\""
}
},
"status" : 401
}

解决办法是添加上账号密码就好了

1
curl --user elastic:changeme -XGET 'localhost:9200/_cat/health?v&pretty'

这样就能正常访问了:

1
2
epoch      timestamp cluster  status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
1487747024 15:03:44 myClusterName yellow 1 1 19 19 0 0 19 0 - 50.0%

这个账号密码实际上是 X-Pack 这个插件附带的认证功能

Java API

使用 X-Pack 后,使用 Java API 获取 Client 的时候会报一个类似这样的错:

1
2
3
4
5
...
NoNodeAvailableException[None of the configured nodes are available: [{#transport#-1}{pyXJL2PeTtGejUwbpycDUg}{127.0.0.1}{127.0.0.1:9300}]]
...
Caused by: org.elasticsearch.ElasticsearchSecurityException: missing authentication token for action [cluster:monitor/nodes/liveness]
...

其实看到Caused by的时候基本就已经能确定是因为 X-Pack 引入的认证机制而引起的错误。

既然知道了错误原因,那么就加上用户名和密码呗!但是事情却不是想的那么简单,ES 的官方文档里并没有提到在哪里可以加用户名和密码。

那么想必 Google 一下就能解决了吧,但是这个解决的过程其实并不顺利。首先 ES 的版本比较多,如果是 ES5 之前的版本是基本没见到这个问题的,其次也不是每个人都会装 X-Pack 的啊!最后 pass 掉了几种过气了的解决方案后,终于在 ES 的社区找到了个类似的问题,最终解决的办法其实 elastic 已经写到了它的文档中,只不过不是 ElasticSearch 的文档而是 X-Pack 的文档里(唉,其实我应该早想到的[捂脸])

解决的关键是要引入一个 jar 包 x-pack-transport-5.2.1.jar,在构造 TransportClient 时用这个 jar 包里的 PreBuiltXPackTransportClient 替换掉 PreBuiltTransportClient,然后就可以在 Settings 里定义xpack.security.user了。

解决步骤如下(以使用 Maven 来管理依赖为例):

  1. 首先在 pom.xml 文件里添加仓库和依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<project ...>

<repositories>
<!-- add the elasticsearch repo -->
<repository>
<id>elasticsearch-releases</id>
<url>https://artifacts.elastic.co/maven</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
...
</repositories>
...

<dependencies>
<!-- add the x-pack jar as a dependency -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>x-pack-transport</artifactId>
<version>{version}</version>
</dependency>
...
</dependencies>
...

</project>
  1. 然后在配置里加入xpack.security.user就好了
1
2
3
4
5
6
7
8
9
import org.elasticsearch.xpack.client.PreBuiltXPackTransportClient;
...

TransportClient client = new PreBuiltXPackTransportClient(Settings.builder()
.put("cluster.name", "myClusterName")
.put("xpack.security.user", "elastic:changeme")
...
.build())
.addTransportAddress(new InetSocketTransportAddress("localhost", 9300));

正常的话做完上边两步后代码就能顺利执行了。不过我因为用的 Maven 库是公司的私服,在第一步添加依赖的时候始终加不上,而 x-pack-transport-5.2.1.jar 这个包直接以及间接依赖的包多达二十多个,一个个手动添加是不现实的,这时候只能开始处理 Maven 的配置问题。由于 Maven 平时我也只是直接使用,settings.xml 配置文件都是从前辈那里传下来的,没有仔细研究过,这次算是补补课了。

我的 ${HOME}/.m2/settings.xml 里之前 mirrors 节点的配置如下:

1
2
3
4
5
6
7
8
9
10
<mirrors>
<mirror>
...
</mirror>
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://my.host.com/nexus/groups/public</url>
</mirror>
</mirrors>

问题是由<mirrorOf>*</mirrorOf>这个配置引起的,mirrorOf的含义如下:

mirrorOf:用来表示该mirror是关联的哪一个仓库,其值为其关联仓库的id。当要同时关联多个仓库时,这多个仓库之间可以用逗号隔开;当要关联所有的仓库时,可以使用“”表示;当要关联除某一个仓库以外的其他所有仓库时,可以表示为“,!repositoryId”;当要关联不是localhost或用file请求的仓库时,可以表示为“external:*”。

既然找到了问题所在,那么对应的改之即可,即 id 为 elasticsearch-releases 的这个 repository 不再会关联到 nexus 这个镜像上

1
2
3
4
5
6
7
8
9
10
<mirrors>
<mirror>
...
</mirror>
<mirror>
<id>nexus</id>
<mirrorOf>*,!elasticsearch-releases</mirrorOf>
<url>http://my.host.com/nexus/groups/public</url>
</mirror>
</mirrors>

P.S.

You can also add an Authorization header to each request. If you’ve configured global authorization credentials, the Authorization header overrides the global authentication credentials. This is useful when an application has multiple users who access Elasticsearch using the same client. You can set the global token to a user that only has the transport_client role, and add the transport_client role to the individual users.

代码片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import org.elasticsearch.xpack.client.PreBuiltXPackTransportClient;

import static org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
...

TransportClient client = new PreBuiltXPackTransportClient(Settings.builder()
.put("cluster.name", "myClusterName")
.put("xpack.security.user", "transport_client_user:changeme")
...
.build())
.build()
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300))
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9301))

String token = basicAuthHeaderValue("elastic", new SecuredString("changeme".toCharArray()));

client.filterWithHeader(Collections.singletonMap("Authorization", token))
.prepareSearch().get();

关于 SSL 可以参考Java Client and Security