하둡 (Apache Hadoop) 4. Zookeeper로 HDFS의 고가용성(High Availability) 달성하기

2021. 2. 22. 13:55빅데이터 플랫폼 (Bigdata Platforms)/아파치 하둡 (Apache Hadoop)


시작하면서

 

이전에 HDFS를 설치한 포스트에서 저비용으로 높은 가용성을 가진다는 것을 설명했습니다.

HDFS는 빅데이터를 위해 설계되었기 때문에 높은 처리량(high throughput)을 보장합니다. 따라서 당연히 그만큼 데이터 교환 시에 다양한 실패 상황이 있을 수 있고, 이를 위해 고가용성(high availiablity)을 달성하는 것은 당연합니다.

 

이번 포스팅은 주키퍼와 연동하여 HDFS의 고가용성을 달성하는 QJM(quorum journal manager) 방식이기 때문에 먼저 주키퍼에 대한 개요를 이해하고 있어야 하며 주키퍼 클러스터를 미리 설치해야 됩니다.

 

마지막으로 이전에 작성했던 글을 인용하여 가볍게 청사진을 그리고 시작하겠습니다.

활성화된 네임노드(active stated namenode)는 본인의 진행 상황을 공유 저장소 내 에디트 로그(edit log) 파일에 작성해야 합니다. 그리고 준비 상태의 네임노드(secondary namenode)는 해당 파일을 감시하면서 동기화(발을 맞추고)하고, 만약 오류가 났을 때 해당 네임노드를 대체하여 작업을 수행합니다(failover).
oimbook.tistory.com/entry/아파치-하둡-Apache-Hadoop-1-HDFS

 

 


어떤 점을 고려해야 될까요?

 

편의상 활성화된 네임 노드A노드, 준비 상태의 네임노드S노드라고 부르겠습니다.

S노드가 A노드와 동기화하기 위해서는 두 노드가 소통할 장소, 저널 노드들(JournalNodes, JNs)이 필요합니다. 만약 A노드가 네임스페이스를 변경한다면 과반수의 저널 노드들에 해당 내용을 기록하고 S노드는 해당 기록을 보고 자신의 네임스페이스를 똑같이 변경합니다. 그래서 만약 A노드에 문제가 발생하면 이를 대체할 수 있습니다.

In order for the Standby node to keep its state synchronized with the Active node, both nodes communicate with a group of separate daemons called “JournalNodes” (JNs). When any namespace modification is performed by the Active node, it durably logs a record of the modification to a majority of these JNs. The Standby node is capable of reading the edits from the JNs, and is constantly watching them for changes to the edit log. As the Standby Node sees the edits, it applies them to its own namespace. In the event of a failover, the Standby will ensure that it has read all of the edits from the JournalNodes before promoting itself to the Active state. This ensures that the namespace state is fully synchronized before a failover occurs.
https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html#Deploying_ZooKeeper

 

자, 이제 고려해야 할 사항을 정리해보겠습니다.

  • S노드는 A노드의 최신 상태를 계속 유지해야 합니다.
  • 1개의 S노드만 A노드를 대체해야 합니다. 

S노드가 계속 최신 상태를 반영하는 것은 빠르게 장애 복구(failover)를 할 수 있도록 하기 위해서입니다. 이를 달성하기 위해서는 데이터 노드는 모든 네임 노드들에게 자신의 위치(location) 정보와 heartbeat 정보를 공유해야 됩니다.

또한 2개 이상의 S노드가 동시에 A노드를 대체할 경우 일관성 문제(inconsistency)가 발생할 수 있습니다. 이를 split-brain이라고 합니다. 이를 막기 위해 저널 노드에 쓰기 역할을 하는 S노드를 1개로 제한하고, 쓰기 역할을 하는 S노드만 A노드를 대체할 수 있도록 합니다. 

 

 


하드웨어 구분

 

여기를 참고해보면 준비해야 되는 하드웨어를 확인할 수 있습니다.

  • A노드와 S노드는 동일한 하드웨어에서 구현해야 합니다.
  • 저널 노드들은 비교적 가볍기 때문에 네임 노드와 자원 매니저(resource manager)들과 함께 실행할 수 있습니다. 다만 최소 3개를 구성해야 되는 데, 이는 (N-1)/2개의 장애에 대해 복구할 수 있기 때문입니다.

 

 

 


HDFS 설정하기

 

먼저 각 네임 노드를 지칭하는 논리적 이름인 네임 노드 아이디(NameNode ID)를 정의해야 합니다. 그리고 해당 네임노드를 묶는 클러스터에 대해 이름도 정해줘야 하는데, 이를 네임 서비스 아이디(nameservice ID)라고 합니다.

 

이전에 HDFS 완전 분산 모드를 구현한 글에서 이미 HDFS 클러스터를 구현했으므로 그대로 활용하겠습니다.

구현 설계는 다음과 같습니다.  

 

 

위 구현 설계를 바탕으로 아래 파라미터 값을 채울 수 있습니다.

 

파라미터 이름 설정값 설명
dfs.nameservices hacluster 네임 서비스 이름입니다.
dfs.ha.namenodes.[nameservice ID] dnn, onn, jnn 네임 서비스 내의 네임 노드에 대한 아이디입니다. 콤마(,)로 구분합니다.
dfs.namenode.rpc-address.[nameservice ID].[namenode ID] 이미지 참고 원격 프로시저 호출의 주소를 정의합니다.
dfs.namenode.http-address.[nameservice ID].[namenode ID] 이미지 참고 HTTP 주소를 정의합니다.
dfs.namenode.shared.edits.dir qjournal://dim:8485;oim:8485;jim:8485/hacluster S노드들을 업데이트하도록 A노드가 읽거나 기록할 저널 노드들에 대한 주소를 정의합니다. 설정값을 참고하면, url은 단 한 개만 구성되었음을 알 수 있습니다. 
dfs.client.failover.proxy.provider.[nameservice ID] org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider ConfiguredFailoverProxyProvider 또는 RequestHedgingProxyProvider 중에 선택할 수 있습니다.
dfs.ha.fencing.methods sshfence  장애 조치가 끝난 뒤에도 이전 A노드가 클라이언트의 읽기 요청을 수행할 가능성이 있습니다. 이를 막는 것이 펜싱(fencing)입니다. 여기서 sshfence 옵션은 SSH 통신을 통해 해당 프로세스를 종료해줍니다.
dfs.ha.fencing.ssh.private-key-files /home/[USER]/.ssh/id_rsa 위 sshfence 방법은 로그인 과정 없이 접속되는 환경이 필요하므로 개인키를 제공해야 합니다.
fs.defaultFS hdfs://hacluster 고가용성이 적용된 논리적 주소를 사용할 수 있으며, 위에서 정의한 네임 서비스 이름을 core-site.xml 에서 작성(수정)합니다.
dfs.journalnode.edits.dir /hdfs/journalnode 저널 노드에서 작성한 데이터가 저장될 위치입니다.
dfs.ha.nn.not-become-active-in-safemode  true 안전 모드의 네임 노드가 A노드로 되는 것을 방지합니다. 자동 장애조치 기능이 있을 경우에는 주키퍼에게 SERVICE_UNHEALTHY 메시지를 보내고, 아니라면 예외 메시지를 전달합니다.

 

- ConfiguredFailoverProxyProvider 는 장애가 발생했다는 첫 번째 요청에 대해 A노드를 결정하기 위해 모든 네임 노드를 호출하는 방식이고, RequestHedgingProxyProvider 는 미리 A노드에 대한 요청을 계속 보내 장애가 발생했을 때 빠르게 복구하는 방식입니다. 아마 후자의 경우 자원을 더 사용할 것으로 예상되므로 전자의 방법을 선택했습니다.

 

- dfs.ha.fencing.methods 중 쉘 스크립트(shell script) 파일을 사용하는 방법도 있습니다. 

 

 

 


주키퍼 연동하기

 

 

HDFS 장애조치에 주키퍼를 연동했을 때에는 자동 장애 조치 기능을 얻으며, 크게 다음 두 가지 이점을 얻을 수 있습니다.

  • 실패 감지 : 장치가 고장 났을 때 이에 대한 알림을 빠르게 받을 수 있습니다.
  • A노드 선출 : 이미 이전에 리더 선출 알고리즘에 대해 설명했습니다. 동일한 원리로 빠른 시간 내에 대체할 S노드를 선출합니다.

이러한 이점은 ZKFailoverController(이하 ZKFC)가 다음 기능을 제공함으로 성취됩니다.

  • 상태 모니터링 : 상태 확인 명령을 주기적으로 네임 노드에게 보내어 응답되는 정상적으로 응답하는지 확인합니다.
  • 주키퍼 기반 선거 : 장애 발생 시, 1개의 S노드에게 특별히 lock이라는 제트노드를 가질 수 있게 하여 기존의 A노드를 대체하도록 합니다. 

이미 이전에 주키퍼 클러스터를 구현했으므로 이를 바로 활용하겠습니다.

 

사실 주키퍼를 설정하는 건 의외로 단순합니다.

파라미터 이름 설정값 설명
dfs.ha.automatic-failover.enabled true 당연히 true로 설정합니다.
ha.zookeeper.quorum dim:2181,oim:2181,jim:2181 저널 노드에서 작성한 데이터가 저장될 위치입니다.

 

 

 


HDFS 설정 파일 공개

 

core-site.xml

<configuration>
  <property>
    <name>fs.defaultFS</name>
    <value>hdfs://hacluster</value>
  </property>
  <property>
    <name>io.file.buffer.size</name>
    <value>204800</value>
  </property>
  <property>
    <name>hadoop.tmp.data</name>
    <value>/tmp/hadoop</value>
  </property>
  <property>
    <name>dfs.ha.fencing.methods</name>  
    <value>sshfence</value>
  </property>
  <property>
    <name>dfs.ha.fencing.ssh.private-key-files</name>  
    <value>/home/denny/.ssh/id_rsa</value>
  </property>
  <property>
    <name>ha.zookeeper.quorum</name>
    <value>dim:2181,oim:2181,jim:2181</value>
  </property>
</configuration>

 

hdfs-site.xml

... (이전에 작성한 내용에 이어서 쓰기)
  <property>
    <name>dfs.nameservices</name>
    <value>hacluster</value>
  </property>
  <property>
    <name>dfs.ha.namenodes.hacluster</name>
    <value>dnn,onn,jnn</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.hacluster.dnn</name>
    <value>dim:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.hacluster.onn</name>
    <value>oim:8020</value>
  </property>
  <property>
    <name>dfs.namenode.rpc-address.hacluster.jnn</name>
    <value>jim:8020</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.hacluster.dnn</name>
    <value>dim:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.hacluster.onn</name>
    <value>oim:9870</value>
  </property>
  <property>
    <name>dfs.namenode.http-address.hacluster.jnn</name>
    <value>jim:9870</value>
  </property>
  <property>
    <name>dfs.namenode.shared.edits.dir</name>
    <value>qjournal://dim:8485;oim:8485;jim:8485/hacluster</value>
  </property>
  <property>
    <name>dfs.client.failover.proxy.provider.hacluster</name>  
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
  </property>
  <property>
    <name>dfs.journalnode.edits.dir</name>  
    <value>/hdfs/journalnode</value>
  </property>
  <property>
    <name>dfs.ha.nn.not-become-active-in-safemode</name>  
    <value>true</value>
  </property>
  <property>
    <name>dfs.ha.automatic-failover.enabled</name>
    <value>true</value>
  </property>
</configuration>

 

당연한 이야기이겠지만, 모든 컴퓨터에는 /hdfs/journalnode 디렉터리가 존재해야 되고

dfs.ha.fencing.ssh.private-key-files 파라미터의 경로에서 사용자 이름을 달리해야 됩니다.

~$ mkdir /hdfs/journalnode

 

 

 


HDFS with HA 실행하기

 

먼저 저널 노드를 모든 컴퓨터에서 실행해줍니다.

~$ hdfs --daemon start journalnode
~$ jps
2641 JournalNode
2689 Jps
2094 QuorumPeerMain

다음과 같이 JournalNode 프로세스가 모든 컴퓨터에서 실행된다면 성공입니다.

 

다음 마스터 노드(컴퓨터)에서 네임 노드를 포맷해주세요.

$ hdfs namenode -format

... (생략)

************************************************************/
2021-01-19 03:19:47,748 INFO namenode.NameNode: registered UNIX signal handlers for [TERM, HUP, INT]
2021-01-19 03:19:47,823 INFO namenode.NameNode: createNameNode [-format]
2021-01-19 03:19:48,179 INFO common.Util: Assuming 'file' scheme for path /hdfs/namenode in configuration.
2021-01-19 03:19:48,182 INFO common.Util: Assuming 'file' scheme for path /hdfs/namenode in configuration.
2021-01-19 03:19:48,188 INFO namenode.NameNode: Formatting using clusterid: CID-f287f2b1-6055-43b7-a955-c3b15f3c8544
2021-01-19 03:19:48,214 INFO namenode.FSEditLog: Edit logging is async:true
2021-01-19 03:19:48,236 INFO namenode.FSNamesystem: KeyProvider: null
2021-01-19 03:19:48,237 INFO namenode.FSNamesystem: fsLock is fair: true
2021-01-19 03:19:48,237 INFO namenode.FSNamesystem: Detailed lock hold time metrics enabled: false
2021-01-19 03:19:48,241 INFO namenode.FSNamesystem: fsOwner                = denny (auth:SIMPLE)
2021-01-19 03:19:48,241 INFO namenode.FSNamesystem: supergroup             = supergroup
2021-01-19 03:19:48,241 INFO namenode.FSNamesystem: isPermissionEnabled    = true
2021-01-19 03:19:48,241 INFO namenode.FSNamesystem: isStoragePolicyEnabled = true
2021-01-19 03:19:48,242 INFO namenode.FSNamesystem: Determined nameservice ID: hacluster
2021-01-19 03:19:48,242 INFO namenode.FSNamesystem: HA Enabled: true

... (생략)

2021-01-19 03:19:49,975 INFO namenode.FSImage: FSImageSaver clean checkpoint: txid=0 when meet shutdown.
2021-01-19 03:19:49,975 INFO namenode.NameNode: SHUTDOWN_MSG: 
/************************************************************
SHUTDOWN_MSG: Shutting down NameNode at dim/192.168.1.6
************************************************************/

이 경우 Determined nameservice IDhacluster 임을 확인하고 HA Enabledtrue임을 확인해야 됩니다.

또한 어떠한 에러도 있으면 안 됩니다. 만약 등장한다면 99% 설정 파일에 오타가 있는 경우니, 다시 잘 살펴보시길 바랍니다.

 

사실 위 경우는 깔끔하게 파일 시스템을 초기화하는 행위이고, 다음과 같이 경우에 따라 달리 포맷을 진행할 수 있습니다.

  • 이미 네임 노드를 가지고 있고 단지 클러스터에 고가용성 기능을 활성화하고 싶으면,  포맷되지 않은 네임 노드에서 'hdfs namenode -bootstrapStandby' 명령어를 통해 네임 노드의 메타 데이터 디렉터리의 내용을 다른 포맷되지 않은 NameNode (s)에 복사합니다.
  • 단지 어떤 네임 노드가 고가용성 기능을 가지도록 한다면, 'dfs namenode -initializeSharedEdits' 명령어를 실행하세요.
* If you are setting up a fresh HDFS cluster, you should first run the format command (hdfs namenode -format) on one of NameNodes.
* If you have already formatted the NameNode, or are converting a non-HA-enabled cluster to be HA-enabled, you should now copy over the contents of your NameNode metadata directories to the other, unformatted NameNode(s) by running the command “hdfs namenode -bootstrapStandby” on the unformatted NameNode(s). Running this command will also ensure that the JournalNodes (as configured by dfs.namenode.shared.edits.dir) contain sufficient edits transactions to be able to start both NameNodes.
* If you are converting a non-HA NameNode to be HA, you should run the command “hdfs namenode -initializeSharedEdits”, which will initialize the JournalNodes with the edits data from the local NameNode edits directories.
https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html#Deployment_details

 

다음 ZKFailoverController 또한 초기화해줍니다.

denny@dim:~$ hdfs zkfc -formatZK
... (생략)
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:user.name=denny
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:user.home=/home/denny
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:user.dir=/hdfs
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:os.memory.free=229MB
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:os.memory.max=3550MB
2021-01-19 03:23:28,339 INFO zookeeper.ZooKeeper: Client environment:os.memory.total=240MB
2021-01-19 03:23:28,341 INFO zookeeper.ZooKeeper: Initiating client connection, connectString=dim:2181,oim:2181,jim:2181 sessionTimeout=10000 watcher=org.apache.hadoop.ha.ActiveStandbyElector$WatcherWithClientRef@76505305
2021-01-19 03:23:28,344 INFO common.X509Util: Setting -D jdk.tls.rejectClientInitiatedRenegotiation=true to disable client-initiated TLS renegotiation
2021-01-19 03:23:28,348 INFO zookeeper.ClientCnxnSocket: jute.maxbuffer value is 4194304 Bytes
2021-01-19 03:23:28,353 INFO zookeeper.ClientCnxn: zookeeper.request.timeout value is 0. feature enabled=
2021-01-19 03:23:28,359 INFO zookeeper.ClientCnxn: Opening socket connection to server jim/192.168.1.11:2181. Will not attempt to authenticate using SASL (unknown error)
2021-01-19 03:23:28,364 INFO zookeeper.ClientCnxn: Socket connection established, initiating session, client: /192.168.1.6:33988, server: jim/192.168.1.11:2181
2021-01-19 03:23:28,390 INFO zookeeper.ClientCnxn: Session establishment complete on server jim/192.168.1.11:2181, sessionid = 0x30000044aaf0000, negotiated timeout = 10000
2021-01-19 03:23:28,393 INFO ha.ActiveStandbyElector: Session connected.
2021-01-19 03:23:28,422 INFO ha.ActiveStandbyElector: Successfully created /hadoop-ha/hacluster in ZK.
2021-01-19 03:23:28,529 INFO zookeeper.ZooKeeper: Session: 0x30000044aaf0000 closed
2021-01-19 03:23:28,530 WARN ha.ActiveStandbyElector: Ignoring stale result from old client with sessionId 0x30000044aaf0000
2021-01-19 03:23:28,530 INFO zookeeper.ClientCnxn: EventThread shut down for session: 0x30000044aaf0000
2021-01-19 03:23:28,533 INFO tools.DFSZKFailoverController: SHUTDOWN_MSG: 
/************************************************************
SHUTDOWN_MSG: Shutting down DFSZKFailoverController at dim/192.168.1.6
************************************************************/

위처럼 /hadoop-ha/hacluster가 잘 생성되었다면 성공입니다.

 

마지막으로 start-dfs.sh 명령어로 HDFS를 실행합니다. 해당 쉘 스크립트 파일은 내부적으로 zkfc를 실행하는 코드가 있으니 해당 명령어만 실행하면 됩니다.

denny@dim:/hdfs$ start-dfs.sh
Starting namenodes on [dim oim jim]
jim: Connection closed by 192.168.1.11 port 22
pdsh@dim: jim: ssh exited with exit code 255
oim: Connection closed by 192.168.1.132 port 22
pdsh@dim: oim: ssh exited with exit code 255
Starting datanodes
Starting journal nodes [oim dim jim]
jim: Connection closed by 192.168.1.11 port 22
pdsh@dim: jim: ssh exited with exit code 255
oim: Connection closed by 192.168.1.132 port 22
pdsh@dim: oim: ssh exited with exit code 255
Starting ZK Failover Controllers on NN hosts [dim oim jim]
jim: Connection closed by 192.168.1.11 port 22
pdsh@dim: jim: ssh exited with exit code 255
oim: Connection closed by 192.168.1.132 port 22
pdsh@dim: oim: ssh exited with exit code 255
denny@dim:~$ jps
2480 QuorumPeerMain
7057 Jps
2983 XMLServerLauncher
6392 DataNode
6712 JournalNode
6088 NameNode
6957 DFSZKFailoverController
denny@dim:~$ ssh jong@jim
... (생략)
jong@jim:~$ jps
2641 JournalNode
2929 Jps
2841 DataNode
2094 QuorumPeerMain

위처럼 마스터 노드에는 DataNode, NameNode, JournalNode, DFSZKFailoverController, QuorumPeerMain 으로 총 5개의 프로세스가 존재해야 되고, 세컨더리 노드에는 JournalNode, DataNode, QuorumPeerMain 으로 총 3개의 프로세스가 실행되고 있어야 합니다.

 

여담이지만 현재 실행되고 있는 네임 노드 프로세스가 dim이므로 dim:9870으로 접속하면 아래와 같이 웹 화면을 보실 수 있습니다.

 

 


끝내면서

 

이제 드디어 플랫폼 간의 연동이 시작되었습니다.

이전과 다르게 양방향으로 모든 과정을 기록하면서, 포스팅을 진행하다 보니 더욱 힘들었네요.

 

하지만 무언가 달성해나가는 자신을 보면 역시 그 성취감과 뿌듯함은 이루 말할 수 없는 것 같습니다.

다음 포스팅은 하둡 프로그래밍에 대해 잠깐 이야기하면서 쉬는 시간을 가져보려고 합니다.