Elasticsearch 클러스터는 하나 이상의 노드들로 이루어집니다.
이 중 하나의 노드는 인덱스의 메타 데이터, 샤드의 위치와 같은 클러스터 상태 정보를 관리하는 마스터 노드의 역할을 수행합니다.
만약 클러스터에 마스터 노드가 존재하지 않는다면 클러스터는 작동이 정지됩니다.
Elasticsearch를 Docker 환경에서 구동하는 경우, 공식 가이드에서 아래와 같이 클러스터의 마스터 노드 후보를 지정하도록 가이드하고있습니다.
cluster.initial_master_nodes=노드명,노드명,노드명
아래는 Docker 환경에서 클러스터의 마스터 노드가 선출되지 못한 오류와 그 해결 과정입니다.
오류 내용
master_not_discovered_exception
http://엘라스틱서치노드:9200/
접속 시 아래와 같이 cluster_uuid가 _na_라고 출력됩니다.
그리고 http://엘라스틱서치노드:9200/_cluster/health?pretty
접속 시 아래와 같이 에러 메시지가 출력됩니다.
각 노드들의 로그를 확인해보니 아래와 같이 completed handshake with ~~ but followup connection failed라는 메시지가 출력된 것을 확인할 수 있었습니다.
{"type": "server", "timestamp": "2022-03-08T01:29:56,097Z", "level": "WARN", "component": "o.e.d.HandshakingTransportAddressConnector", "cluster.name": "es-data-collector-cluster", "node.name": "es02", "message": "[connectToRemoteMasterNode[10.162.5.88:9300]] completed handshake with [{es03}{xsyfSbEIQ7ewRwOkt3DlEA}{ddZjbakPQgS6TXfRMpoYqA}{172.27.0.4}{172.27.0.4:9300}{cdfhilmrstw}{ml.machine_memory=16656789504, ml.max_open_jobs=512, xpack.installed=true, ml.max_jvm_size=536870912, transform.node=true}] but followup connection failed", "cluster.uuid": "FAxk_MBdRS6vWpkmguEWDA", "node.id": "bb7cpvkFRV60cn0ndxnuHg" ,
"stacktrace": ["org.elasticsearch.transport.ConnectTransportException: [es03][172.27.0.4:9300] connect_exception",
"at org.elasticsearch.transport.TcpTransport$ChannelsConnectedListener.onFailure(TcpTransport.java:1047) ~[elasticsearch-7.16.2.jar:7.16.2]",
"at org.elasticsearch.action.ActionListener.lambda$toBiConsumer$0(ActionListener.java:279) ~[elasticsearch-7.16.2.jar:7.16.2]",
"at org.elasticsearch.core.CompletableContext.lambda$addListener$0(CompletableContext.java:31) ~[elasticsearch-core-7.16.2.jar:7.16.2]",
"at java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863) ~[?:?]",
"at java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841) ~[?:?]",
"at java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[?:?]",
"at java.util.concurrent.CompletableFuture.completeExceptionally(CompletableFuture.java:2162) ~[?:?]",
"at org.elasticsearch.core.CompletableContext.completeExceptionally(CompletableContext.java:46) ~[elasticsearch-core-7.16.2.jar:7.16.2]",
"at org.elasticsearch.transport.netty4.Netty4TcpChannel.lambda$addListener$0(Netty4TcpChannel.java:58) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.notifyListener0(DefaultPromise.java:578) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.notifyListeners0(DefaultPromise.java:571) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.notifyListenersNow(DefaultPromise.java:550) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.notifyListeners(DefaultPromise.java:491) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.setValue0(DefaultPromise.java:616) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.setFailure0(DefaultPromise.java:609) ~[?:?]",
"at io.netty.util.concurrent.DefaultPromise.tryFailure(DefaultPromise.java:117) ~[?:?]",
"at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.fulfillConnectPromise(AbstractNioChannel.java:321) ~[?:?]",
"at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:337) ~[?:?]",
"at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:707) ~[?:?]",
"at io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain(NioEventLoop.java:620) ~[?:?]",
"at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:583) ~[?:?]",
"at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493) ~[?:?]",
"at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[?:?]",
"at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[?:?]",
"at java.lang.Thread.run(Thread.java:833) [?:?]",
"Caused by: io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection timed out: 172.27.0.4/172.27.0.4:9300",
"Caused by: java.net.ConnectException: Connection timed out",
"at sun.nio.ch.Net.pollConnect(Native Method) ~[?:?]",
"at sun.nio.ch.Net.pollConnectNow(Net.java:672) ~[?:?]",
"at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:946) ~[?:?]",
"at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:330) ~[?:?]",
"at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:334) ~[?:?]",
"... 7 more"] }
로그 내용을 살펴보면, 다른 노드를 찾는 통신(Discovery)은 성공했지만 그 이후의 통신은 실패한 것으로 보입니다.
오류 원인과 해결 과정
Elasticsearch Node 간의 통신??
Elasticsearch Discovery 공식 가이드에 따르면 클러스터를 구성하는 노드가 다른 노드를 찾는 과정을 Discovery라고 하며, 아래의 상황에 수행합니다.
- Elasticsearch 노드가 시작될 때
- 마스터 노드를 찾지 못할 경우 마스터 노드를 찾을 때까지
- 새로운 마스터 노드가 선출될 때
Discovery 과정
Discovery는 아래와 같은 순서로 수행됩니다.
- seed_hosts에 지정된 호스트들과 기존에 알고있던 클러스터의 마스터 후보 노드 호스트들과 시작합니다.
- 각 노드들은 서로 seed_hosts의 주소들을 연결 검사하고, 노드가 연결되었는지, 마스터 후보 노드인지 구분합니다.
- 앞선 과정이 성공하면 원격 마스터 노드 리스트를 공유합니다.
- 새로운 노드가 발견되면 이 과정을 반복합니다.
마스터 후보 노드 여부에 따른 Discovery
자신이 마스터 후보 노드가 아닐 경우
- 선출된 마스터 노드를 찾을 때까지 Discovery 과정을 계속 반복합니다.
자신이 마스터 후보 노드일 경우
- 마스터 노드를 찾거나 마스터 노드가 없는 경우 마스터 노드 선출을 위한 충분한 후보 노드를 찾을 때까지 Discovery 과정을 계속 반복합니다.
노드 Discovery가 실패한건가??
기존 설정에서는 이 노드와 통신하려면 다른 노드에서 이 노드에 어떤 포트로 Request를 해야하는지 명세가 되어있지 않아 기본 포트(9300)로 Request를 하다보니, 2번 노드(es02)에서 요청을 받지 못하고 있었습니다.
Elasticsearch 공식 가이드를 참조해 아래 내용을 확인했습니다.
Elasticsearch의 여러 포트와 호스트 설정들
- http.port
- HTTP Client와의 통신을 위한 포트
- transport.port
- 노드간의 통신을 위한 포트
- 마스터 후보 노드는 범위가 아닌 단일 포트를 세팅해야합니다.
- http.publish_port
- HTTP Publish Address를 위한 포트
- http.port 와 다르게 설정할 경우 세팅하며, 기본값은 http.port
- transport.publish_port
- Transport Publish Address를 위한 포트
- 다른 노드에게 자신과 transport 통신을 하려면 사용해야하는 포트를 알려주는 용도. 기본값은 transport.port
- network.bind_host
- incomming 연결을 받을 네트워크 주소
- IP, 호스트명을 지정할 수 있으며, 특별한 값도 가능합니다.
- _local_ : 루프백 주소로 설정됩니다. 예) 127.0.0.1
- _site_ : 로컬 네트워크 주소로 설정됩니다. 예) 192.168.0.1
- _global_ : 네트워크 외부에서 바라보는 주소로 설정합니다. 예) 8.8.8.8
- _[networkInterface]_: 네트워크 인터페이스의 주소로 설정됩니다. 예) en01
- 0.0.0.0 : 사용 가능한 모든 네트워크 인터페이스의 주소로 설정됩니다.
- 기본값 network.host
- network.publish_host
- 클라이언트와 다른 노드들에게 자신과 통신하기 위해 사용해야하는 네트워크 주소를 알려주는 용도. 기본값은 network.host
Discovery 실패의 이유
정리를 하자면, 1번 3번 노드는 기본값을 그대로 사용해도 문제 없었으나, 2번 노드의 경우 1번 노드와 같은 서버이기 때문에 Docker port binding으로 외부 포트를 다르게 지정했기 때문에 문제가 발생했던 것이었습니다.
아래는 현재 클러스터 구성을 간단하게 나타낸 것입니다.
🖥 다른 노드들 : 2번 노드야, 너랑 통신하려면 어떤 포트 써야하니?
🖥 2번 노드 : 내 transport.publish_port를 쓰면 돼! 지금 설정이 되어있지 않으니 9300번이겠구나.
🖥 다른 노드들 : (9300 포트로 통신을 시도합니다.)
🐋 2번 노드 서버 Docker : 9300 포트? 1번 노드에 바인딩 되어있네. 1번 노드로 가세요.
🖥 1번 노드 : 뭐여.
🖥 다른 노드들 : 뭐야, 2번 노드가 아닌데?
이와 같이 자신에게 어떻게 Request를 보내야하는지 명세하는 설정이 publish_* 설정이었고, 각 노드별로 아래와 같이 설정했습니다.
# 1번 노드(es01)
- network.bind_host=0.0.0.0
- network.publish_host=node1-elasticsearch.1hoon.io
- transport.publish_port=9300
- http.publish_port=9200
# 2번 노드(es02)
- network.bind_host=0.0.0.0
- network.publish_host=node2-elasticsearch.1hoon.io
- transport.publish_port=9301
- http.publish_port=9210
# 3번 노드(es03)
- network.bind_host=0.0.0.0
- network.publish_host=node3-elasticsearch.1hoon.io
- transport.publish_port=9300
- http.publish_port=9200
위와 같이 설정을 수정한 후 아래와 같은 로그가 출력되는 것을 확인했습니다.
{"type": "deprecation.elasticsearch", "timestamp": "2022-03-10T07:06:43,912Z", "level": "CRITICAL", "component": "o.e.d.t.TransportInfo", "cluster.name": "es-data-collector-cluster", "node.name": "es01", "message": "transport.publish_address was printed as [ip:port] instead of [hostname/ip:port]. This format is deprecated and will change to [hostname/ip:port] in a future version. Use -Des.transport.cname_in_publish_address=true to enforce non-deprecated formatting.", "key": "cname_in_publish_address", "category": "settings" }
오류 메시지에 나온대로 JVM 옵션에 -Des.transport.cname_in_publish_address=true
를 추가했습니다.
Discovery는 성공했지만 여전히 마스터 노드는 선출되지 않았습니다.
모든 수정 완료 후 재구동했으나, 여전히 마스터 노드 선출이 되지 않았습니다.
{"type": "server", "timestamp": "2022-03-10T07:01:26,783Z", "level": "WARN", "component": "o.e.c.c.ClusterFormationFailureHelper", "cluster.name": "es-data-collector-cluster", "node.name": "es01", "message": "master not discovered yet, this node has not previously joined a bootstrapped (v7+) cluster, and this node must discover master-eligible nodes [node1-elasticsearch.1hoon.id:9300, node2-elasticsearch.1hoon.io:9301, node3-elasticsearch.1hoon.io:9300] to bootstrap a cluster: have discovered [{es01}{gqCJo2m7RKS-s79_9C0l_w}{vRBsow15ToGsrVuR5GOw8w}{node1-elasticsearch.1hoon.io}{서버 아이피:9300}{cdfhilmrstw}, {es02}{E8JXHJM6Ru-JMNy8M8phIg}{Yx0_4nFpRy2TZ0Xoo5AVdQ}{node2-elasticsearch.1hoon.io}{서버 아이피:9301}{cdfhilmrstw}, {es03}{YBy5EPjqR_qVOSoXsUXMEA}{-Kxt4gc3QMu16iCbz4imqw}{node3-elasticsearch.1hoon.io}{서버 아이피:9300}{cdfhilmrstw}]; discovery will continue using [10.162.0.144:9301, 10.162.5.88:9300] from hosts providers and [{es01}{gqCJo2m7RKS-s79_9C0l_w}{vRBsow15ToGsrVuR5GOw8w}{node1-elasticsearch.1hoon.io}{서버 아이피:9300}{cdfhilmrstw}] from last-known cluster state; node term 0, last-accepted version 0 in term 0" }
로그를 자세히 확인해본 결과, 노드를 찾는 과정(discovery)은 완료되어 아래와 같이 노드 정보를 갖게되는데 cluster.initial_master_node
에 지정된 값이 노드 정보에는 없기 때문에 마스터 노드 후보를 찾지 못하게 되는것이었습니다.
노드 정보들
es01, node1-elasticsearch.1hoon.io, 서버 아이피:9300
es02, node2-elasticsearch.1hoon.io, 서버 아이피:9301
es03, node3-elasticsearch.1hoon.io, 서버 아이피:9300
설정된 마스터 노드 목록
- cluster.initial_master_nodes=node1-elasticsearch.1hoon.io:9300,node2-elasticsearch.1hoon.io:9301,node3-elasticsearch.1hoon.io:9300
현재 클러스터 구성에서 initial_master_nodes로 설정할 수 있는 값은 노드 정보들에 있는 값들입니다.
- es01,es02,es03
- node1-elasticsearch.1hoon.io, node2-elasticsearch.1hoon.io, node3-elasticsearch.1hoon.io
- 서버 아이피:9300,서버 아이피:9301,서버 아이피:9300
이 때, cluster.initial_master_nodes
를 IP:PORT 혹은 도메인으로 지정할 경우에도 노드 정보에 매칭되는 데이터가 있기 때문에 정상적으로 동작합니다.
하지만 IP와 도메인은 변경될 수도 있기 때문에 대신 앞서 설정한 노드명(node.name)으로 지정해주었습니다.
- cluster.initial_master_nodes=es01,es02,es03
그 결과, 마스터 노드가 정상적으로 선출되었고, 클러스터 구성이 완료되었습니다!
'📦 ETC' 카테고리의 다른 글
[Elasticsearch] X-Pack Security 활성화 시 오류 (0) | 2022.03.15 |
---|---|
[Elasticsearch] vm.max_map_count 변경 (0) | 2022.03.14 |
[Jenkins Blue Ocean] 젠킨스가 어려운 개발자를 위해 (0) | 2021.08.28 |
[Swagger] Swagger를 이용해 API 문서를 자동으로 만들어보자 (0) | 2020.11.05 |
[Swagger] API 명세도구를 사용해보자 (0) | 2020.10.31 |