IT_AI_DeepLearning/Hadoop Eco

[펌] 멀티테넌트 Hadoop 클러스터 운영 경험기

JJun ™ 2016. 9. 20. 07:33



 출처: http://d2.naver.com/helloworld/0475200




네이버의 검색 서비스는 사용자에게 좋은 검색 결과를 제공하기 위해 매일 수억 건 이상의 문서를 처리합니다.
웹 로봇이 수집한 문서나 네이버 사용자가 작성한 문서를 저장하고, 저장한 문서를 검색에 적합하도록 변환하며,
부가적인 정보를 추가하기도 합니다. 이런 일련의 작업을 위해 다수의 장비를 이용하는 분산 환경을 구성하는데,
네이버는 오픈소스인 Hadoop을 이용해 분산 환경을 구성합니다.

Hadoop은 대용량 데이터의 보관과 처리를 수월하게 하는 기능을 제공합니다. 하지만 이런 기능을 잘 사용하려면 상황에 맞는 적절한 설정이 필요합니다. Hadoop의 설정에는 성능을 최적화하는 설정도 있고, 적절한 기능 실행을 위해 필요한 설정도 있습니다. 클러스터의 규모가 작고 사용자가 적을 때는 문제가 없더라도, 사용자가 늘고 실행되는 애플리케이션이 늘어 클러스터 장비의 개수가 많아지면 운영 중에 다양한 문제가 발생하게 됩니다.

이 글에서는 네이버에서 몇 년간 Hadoop 클러스터를 운영하면서 겪었던 경험을 몇 가지 공유하려 합니다. 클러스터 운영에 사용하고 있는 Hadoop 2.4.0 버전과 Hadoop 2.7.1 버전으로 나누어 이야기하겠습니다.

Hadoop 2.4.0 클러스터 사례

Hadoop 2.4.0 사례에서는 rack awareness를 설정하지 않아 특정 노드로 요청이 몰리면서 문제가 발생한 사례를 살펴보겠다.
그리고 데이터를 저장하는 HDFS(Hadoop Distributed File System)의 모니터링 지표 알림의 중요성과 디스크 볼륨의 장애에
대응하는 DataNode 관리 방법도 살펴본다.

rack awareness를 설정한다

HBase의 테이블을 삭제하고 싶은데 drop 명령어를 실행한 이후에 오랫동안 응답이 없고 테이블이 지워지지 않는다는
문제가 있어 확인해 달라는 요청이 있었다.

테이블에 속한 특정 region이 계속 not deployed로 표시돼 테이블 삭제 명령이 제대로 실행되지 않고 있었다.
우선 다음과 같이 HBase 셸에서 수동으로 region을 할당하고 테이블을 삭제했다.

$ hbase shell
hbase(main):004:0> enable 'TABLENAME'  
hbase(main):005:0> assign 'REGIONNAME'  # 'not deployed'로 표시되는 region 이름  
hbase(main):006:0> disable 'TABLENAME'  
hbase(main):007:0> drop 'TABLENAME'  

HBase에 요청이 많은 것도 아니고, HBase master의 웹 UI로 확인한 지표도 문제가 없어 보여 region이 할당된 RegionServer의 지표를 전체적으로 확인했다. 그런데 특이하게도 다음과 같이 HDFS 읽기 요청이 문제가 발생한 장비에서만 유난히 많았다.


그림 1 장비별 HDFS 읽기 요청

이렇게 장비 하나에 요청이 몰리면서 그 여파로 해당 장비의 네트워크 자원이 모두 소비돼 HDFS 읽기 외의 요청을 처리하지 못하고 있었다.


그림 2 네트워크 자원이 모두 사용되고 있는 장비

HDFS 읽기 요청이 많은 원인을 찾아 보니 해당 장비에 있는 특정 파일의 블록을 많은 노드에서 읽고 있었다. 이와 같은 요청은 MapReduce에서 필요한 파일을 분산 캐시 기능을 이용해서 사용하면 발생할 수 있다. 이런 경우 파일의 replication factor(dfs.replication 설정)를 크게 늘려(10에서 20사이) 여러 노드로 읽기 요청이 분산되도록 처리하라고 안내하고 있다.

replication factor를 늘려서 읽기 요청을 분산시킬 수 있는 이유는 다음과 같이 HDFS에서는 파일을 블록 단위로 나누어 노드별로 블록을 복제해서 가지고 있기 때문이다.


그림 3 파일 블록의 복제본 보관(원본 출처: https://www.packtpub.com/books/content/hbase-administration-performance-tuning)

블록이 복제될 때는 다음과 같은 순서로 노드를 할당해서 복제한다.

Node Local -> Rack Local -> Off Switch  

Node Local은 HDFS에 파일을 올리는 작업을 실행하는 노드가 DataNode의 일부라면 해당 노드에 블록을 저장하는 것을 의미한다. Rack Local은 작업 수행 노드와 같은 랙(rack)에 존재하는 노드 중 임의의 노드 하나를 선택해서 저장하는 것을 의미한다. Off Switch는 Rack Local이 아닌 랙에 포함된 노드 중 임의의 노드에 저장하는 것을 의미한다.

이렇게 블록을 나누어 저장하는 이유는 노드나 랙의 물리적인 장애에 대비하고 네트워크를 통한 데이터 전송 시간 지연을 최소화하기 위해서다. 그래서 클러스터에 속한 노드의 네트워크 구성에 대해 잘 알고 있어야 한다. Hadoop의 rack awareness 설정이 노드에 대한 랙 정보를 제공한다.

하지만 물리적인 노드의 구성 상황을 클러스터에 그대로 반영하기 어려운 경우도 있고 모든 노드가 하나의 랙에 모두 존재할 수도 있기에, 현실적인 상황에서 rack awareness 설정이 꼭 필요한가 고민이 생겼다. 실제 노드의 구성을 반영할 수 없다면 rack awareness 설정으로 Node Local과 Rack Local을 구분하는 것이 특별히 의미가 없다고 판단했다. 파일이 보관될 때 최종적으로는 클러스터의 모든 노드에 무작위로 블록이 존재하는 것이 보장되고, 파일의 블록에 접근할 때는 블록이 존재하는 임의 노드로 읽기 요청이 분산될 것으로 생각했다.

다음 그림은 처음에 클러스터를 구성했을 때의 네트워크 토폴로지다.


그림 4 하나의 랙으로 구성된 네트워크 토폴로지

하지만 실제 HDFS 컴포넌트는 생각대로 동작하지 않고 있었다. 파일 쓰기는 생각과 같이 잘 분포가 나누어지고 있었으나, 읽기는 생각했던 것과 전혀 다르게 동작하고 있었다. Rack Local의 경우 하나의 랙에 존재하는 임의의 노드가 선택되지 않고 랙에 속한 노드 중 맨 처음 노드만 선택되어 읽기 요청을 처리하고 있었다. 결국 파일의 블록을 요청하는 노드 자신이 가지고 있지 않으면, 블록이 존재하는 첫 번째 노드로 모든 읽기 요청이 몰려 문제가 발생했다.


그림 5 Rack Local로 선택되는 하나의 노드

HDFS 읽기 요청 시 Rack Local의 노드 선택 방식은 Hadoop 2.4의 버그(HDFS-6268과 HDFS-6840 참고)였으며, Hadoop 2.5부터는 임의의 노드가 선택되도록 개선됐다.

Hadoop 2.4에서 이 문제를 해결하기 위해서 rack awareness를 위한 설정인 net.topology.script.file.name에 Python 스크립트(참고 예제)를 작성해서 추가했다.


그림 6 rack awareness 설정을 추가한 네트워크 토폴로지 모습

rack awareness 설정을 추가한 이후 장비별 HDFS 읽기 요청이 다음 그래프처럼 균일하게 이루어졌고, 해당 장비의 네트워크 자원도 여유가 생겼다.


그림 7 균일한 HDFS 읽기 요청

HDFS 용량과 파일 수에 대한 알림은 필수다

어느 날 급격하게 파일 수가 증가하면서 NameNode의 heap 메모리가 모자라게 돼 HDFS 서비스가 중단되는 상황이 발생했다. 파일 수는 증가했지만 특이하게도 파일 용량에는 변화가 거의 없었다.


그림 8 급격한 파일 수 증가

급격히 늘어난 파일은 모두 HBase의 archive 디렉터리에 있어서 무엇 때문에 이런 현상이 있는지 검색해 봤다. HBase의 archive 디렉터리의 파일 수가 급증한 이유는, TTL이 설정된 테이블을 대상으로 컴팩션(compaction)을 실행할 때 archive 디렉터리에 컴팩션 대상 파일이 쌓이는 HBase 버그(HBASE-10371 참고) 때문이었다.

NameNode가 종료되어 클러스터가 중단된 상태라 우선 NameNode를 다시 시작했다. NameNode의 heap이 부족해 데몬이 종료된 것이므로 기존보다 heap의 크기를 늘리고 NameNode를 시작해야 한다. heap의 크기를 늘려 NameNode는 실행됐지만, DataNode의 block report 처리 때문에 safe mode가 'on'인 상태가 지속되고 가비지 컬렉션도 실행되면서 HDFS 서비스가 불가능한 상태가 지속됐다. 결국 block report의 양을 줄이기 위해서 DataNode를 모두 종료시키고, safe mode의 상태도 강제로 'off'로 변경했다(safe mode 조정). 그렇게 하고 나서 문제가 됐던 HBase archive 디렉터리를 삭제하고 DataNode를 한 대씩 구동시켜 복구에 성공했다.

HDFS는 파일 용량을 주로 모니터링한다. 현재 사용량이 얼마이며, 얼마의 여유가 있는지 확인하고, 불필요한 파일 정리도 한다. 하지만 파일 용량에 못지않게 파일 수와 블록 수에 대한 모니터링이 꼭 필요하며, NameNode의 heap 크기 내에서 HDFS 서비스 제공에 문제가 없을 나름의 기준을 갖고 있는 것이 중요하다는 걸 경험으로 알게 됐다.

파일과 블록의 크기, 개수에 따라 NameNode의 적절한 heap 크기를 정하는 것은 Cloudera의 "Sizing NameNode Heap Memory" 문서를 참고하면 좋다. 간략히 정리하면 백만 개 블록마다 1GB의 heap을 설정하라는 것이다. 예를 들어 지금 HDFS에 저장된 블록 수가 5백만 개라면(블록 수는 NameNode UI에서 확인할 수 있다) NameNode의 heap 크기는 5GB가 적당하다는 것이다. 그리고 (파일 개수 + 블록 수) x 150으로 계산한 크기(NameNode가 차지하는 대략적인 메모리량)보다는 크게 설정한다.

NameNode 장비의 최대 메모리 크기까지 heap을 설정하는 것도 좋진 않다. heap 크기가 크면 가비지 컬렉션으로 인한 stop the world 시간도 고려해야 하고, 위의 복구 상황처럼 기존 heap 크기가 작을 때 heap 크기를 늘려 다시 시작할 수 있는 약간의 여유분을 갖고 있는 것도 중요하다.

NameNode의 heap 크기를 적절하게 설정하면서 HDFS의 name quota도 함께 설정하는 것이 좋다. name quota는 디렉터리별로 설정할 수 있으므로, HDFS 사용 정책에 따라 최상위 디렉터리(/ 디렉터리)나 사용자별 디렉터리에 설정할 수 있다.

DataNode 볼륨 설정 변경은 천천히 진행한다

Hadoop 클러스터 구성 시 디스크는 RAID로 구성하지 않고 사용하고 있다. 클러스터를 구성한 초기에는 장비 장애가 적었으나 시간이 지날수록 장비 장애 발생 빈도가 늘어났다. 특히 디스크 장애는 아주 빈번히 일어났다. 디스크 장애가 발생하면 일부 디스크 볼륨만 사용할 수 없더라도 DataNode 데몬을 종료해서 디스크를 교체해야 하기 때문에 전체 디스크 볼륨을 모두 사용하지 못하는 상황이 된다.

DataNode는 주기적으로 block report를 NameNode로 보내고, NameNode는 DataNode로부터 받은 block report를 이용해서 HDFS에 저장된 파일에 대한 최신 정보를 유지한다.


그림 9 디스크 볼륨의 block report

dfs.datanode.data.dir 설정에서 장애가 발생한 볼륨을 제외하고 다시 시작하면 DataNode는 삭제된 볼륨의 블록이 없다는 내용을 NameNode로 보낼 것이고, NameNode도 블록 정보에서 삭제된 블록을 없애면 replication monitor가 부족한 복제본을 새롭게 생성할 것이라 기대했다. 그러면 디스크 볼륨 교체를 자주 하지 않고 한 번에 많은 장비의 디스크 교체를 진행해도 서비스 영향을 최소화할 수 있을 것이라 생각했다.


그림 10 디스크 볼륨 장애에 대한 block report 및 NameNode 블록 정보 제거

그래서 다음과 같은 과정으로 디스크 볼륨 장애에 대처했다.

디스크 볼륨 장애 발생 -> dfs.datanode.data.dir 설정에서 장애 볼륨 제거 -> DataNode 재시작

얼마 동안은 볼륨 설정을 바꾸고 재시작해도 문제가 없었고 DataNode의 디스크 볼륨 장애 대처가 수월해졌다고 생각했다. 하지만 어느 날 실행 중인 일부 애플리케이션에서 ReplicationNotFoundException이 발생한다는 문의가 왔다. NameNode UI에서는 missing block도 없고, 파일의 블록 정보를 보면 replication factor 만큼 복제도 잘 되어 있었지만 계속 ReplicationNotFoundException이 발생했다.

블록이 잘 존재하는데 자꾸 ReplicationNotFoundException이 발생하는 것이 이상해 블록이 존재하는 노드에서 실제 블록을 찾아보니 블록이 존재하지 않았다. NameNode에서 알려 주는 블록 정보와 실제 DataNode에 보관된 블록 정보가 다른 상황이 발생한 것이다.

DataNode 볼륨 설정을 변경하고 다시 시작하면 DataNode는 block report를 주기적으로 보내지만 NameNode의 블록 정보는 오랜 시간이 지나도 갱신되지 않았다. 결국 NameNode의 블록 정보가 갱신되지 않으니 DataNode에서 제거된 볼륨에 있던 블록이 존재하는 것으로 간주되어 HDFS 정보가 불일치하는 상황이 발생했다.


그림 11 NameNode가 DataNode의 block report를 갱신하지 않음

이 문제는 Hadoop의 버그(HDFS-7575HDFS-7596 참고)로, 2.4 버전과 2.5 버전, 2.6 버전까지 영향을 준다는 것을 확인했다.

문제는 확인했지만 클러스터를 업그레이드할 수 없어서 다음과 같이 운영 절차를 바꿨다.

  1. 설정을 변경할 DataNode를 종료시키고, NameNode가 DataNode를 dead로 인지할 때까지 기다린다.
  2. dfs.datanode.data.dir 설정을 변경하고 DataNode를 다시 시작한다.

NameNode가 DataNode를 dead로 인지하는 시간은 다음과 같이 계산한다.

2 x dfs.NameNode.heartbeat.recheck-interval + 10 x 1000 x dfs.heartbeat.interval  

기본값으로 계산하면 10분 30초(2 x 300000 + 10 x 1000 x 3)다. 즉, DataNode를 종료하고 10분 30초를 기다렸다 설정을 변경하고 DataNode를 다시 시작하는 것이다.

영향을 받는 버전을 사용하고 있다면 이와 같은 문제가 있다는 점을 인지하고 클러스터를 운영하는 것이 좋겠다.

Hadoop 2.7.0부터는 DataNode hot swap drive를 지원하니 참고하기 바란다.

hdfs dfsadmin -reconfig datanode HOST:PORT start  

그리고 클러스터를 구성하고 시간이 지날수록 하드 디스크 드라이브에 볼륨 장애가 자주 발생하니, 일부 볼륨에 장애가 발생해도 DataNode는 종료되지 않도록 dfs.datanode.failed.volumes.tolerated 설정을 상황에 맞게 변경하는 것도 좋다.

기본값은 0이라 볼륨이 하나라도 장애가 발생하면 DataNode가 종료되는데, 지금은 2개까지는 볼륨 장애가 나도 DataNode를 종료시키지 않게 했다.

Hadoop 2.7.1 클러스터 사례

Hadoop 2.7.1 사례에서는 멀티 클러스터를 고려하지 않고 rack awareness를 설정해 리소스 할당이 지연된 사례를 살펴보겠다. 그리고 ResourceManager의 HA(high availability)를 위한 기본 설정이 부적절해 발생한 문제를 살펴보겠다.

멀티 클러스터를 고려한 rack awareness를 설정한다

특정 서비스에서 큐에 충분한 리소스가 있는데 작업 할당이 잘 안되고, 전체적인 실행 시간이 느려서 서비스 주기를 맞추지 못하고 있다는 의견을 전달받았다. 클러스터의 모든 작업의 할당이 느린 것도 아니고 특정 작업의 할당만 제대로 되지 않고 있었다. 작업 실행 시 할당된 컨테이너를 확인해 보니 일부 노드에서만 작업이 실행이 됐다. 작업이 실행되는 노드는 모두 /default-rack에 포함된 노드였다.

/default-rack은 rack awareness 설정을 통해 랙 정보가 할당되지 않으면 기본으로 설정되는 랙이다. 신규로 추가된 노드에 대해 net.topology.script.file.name 스크립트를 최신으로 업데이트하지 않아 /default-rack에 작업이 할당된 상태였다.


그림 12 컨테이너가 일부 장비로만 할당

클러스터의 리소스 여유분이 충분하기 때문에 여러 노드에서 작업이 나뉘어 실행돼야 하는데, 왜 일부 노드에서만,
그것도 /default-rack에 포함된 노드에서 유독 많은 작업이 할당돼 실행되는지 확인해야 했다.

Hadoop에서는 애플리케이션 실행을 위한 컨테이너를 할당받기 위해 필요한 리소스를 ResourceManager에 요청한다.
리소스를 요청할 때는 컨테이너의 용량과 ResourceName(location 정보), 컨테이너 수를 넘겨준다.

표 1 리소스 요청 예제

TypeResourceNameCapabilityNumContainers
ApplicationMaster*1
Mapper*2
/rack-12
/rack-22
host1.com1
host2.com1
Reduce*4

ApplicationMaster와 Reducer는 작업 실행 시 locality라는 것이 없기 때문에 ResourceName의 location 정보는 표에서 볼 수 있듯이 *(Off Switch)가 된다. 하지만 Mapper는 데이터가 존재하는 노드에서 Map 작업이 실행될 수 있도록 location 정보인 노드 이름과 랙 정보를 준다. host1.com과 host2.com은 블록이 존재하는 노드이며, /rack-1과 /rack-2는 host1.com과 host2.com의 랙 정보다.

이와 같이 ResourceName에 location 정보가 주어지면 ResourceManager는 'Node Local -> Rack Local -> Off Switch' 순서로 컨테이너를 할당한다. Node Local은 NodeManager의 heartbeat가 오면 바로 리소스를 확인해서 할당하지만, Rack Local과 Off Switch는 일부 heartbeat를 무시하고 천천히 할당한다. Rack Local은 'min(node 수, yarn.scheduler.capacity.node-locality-delay)' 만큼의 heartbeat를 무시하고 할당되며, Off Switch는 클러스터의 노드 수와 요청된 컨테이너 수를 기반으로 계산해서 할당한다.

예를 들어 host1.com의 NodeManager가 heartbeat를 보내면 ResourceManager는 바로 컨테이너를 할당하지만, /rack-3에 포함된 host3.com의 NodeManager가 보낸 heartbeat는 무시되어 바로 컨테이너가 할당되지 않을 수 있다.

작업 실행 시간이 지연되는 애플리케이션의 리소스 요청을 보니 다음처럼 ResourceName에 관리하지 않는 다른 클러스터에 속한 노드('other'로 시작하는 노드)가 있었다. 표 1에서 Mapper의 리소스 요청에서는 location 정보를 준다고 했는데, 해당 작업의 Inputformat 클래스가 다른 클러스터에서 데이터를 가져오도록 작성되어 있어서 다른 클러스터의 노드가 ResourceName에 보인 것이다.


그림 13 대기 중인 리소스 요청의 ResourceName

다른 클러스터의 노드이기 때문에 Node Local은 아니고, 랙 정보가 없어서 /default-rack이 ResourceName에 포함된다.
결국 /default-rack에 포함된 일부 노드가 보낸 heartbeat를 받아 리소스가 할당됐기 때문에 클러스터에 여유가 있어도
일부 노드로 작업이 몰리는 현상이 발생한 것이었다.

클러스터의 노드가 /default-rack에 포함되지 않았거나 다른 클러스터의 노드가 관리하고 있는 노드와 같은 rack 정보를 갖고 있다면 이런 문제가 없을 것이라 생각했다. 그래서 기존에는 클러스터에 포함된 노드에 대해서만 랙 정보를 알 수 있었던 것을 어떠한 노드에 대해서도 랙 정보를 알 수 있도록 net.topology.script.file.name에 설정한 rack awareness 스크립트를 개선했다. 개선 후에는 특정 작업으로 작업이 몰리지 않게 되어 실행 시간을 단축하게 됐다.

ResourceManager가 HA라고 안심하지 않는다

멀티테넌트 클러스터를 구성하면서 각종 데몬의 HA 구성은 반드시 필요하다. NameNode와 ResourceManager도 HA 기능을
제공한다. ResourceManager HA 설정을 사용하면, 클러스터 사용자가 작업을 실행하고 있어도 중단 없이 ResourceManager를
다시 시작할 수 있어 클러스터 관리가 상당히 수월해진다.


그림 14 ResourceManager의 state 전환 모습(원본 출처: http://hortonworks.com/blog/apache-hadoop-yarn-resilience-hadoop-yarn-applications-across-resourcemanager-restart-phase-1/)

하루는 ResourceManager의 HA 기능을 믿고(HA 동작에 대한 테스트는 이미 마친 상태), 설정 변경 후 액티브 상태의 ResourceManager를 다시 시작했다. 하지만 스탠바이 ResourceManager는 오랜 시간 동안 액티브 ResourceManager로 전환되지 못했고 클러스터에서 실행되던 모든 작업이 실패하는 상황이 발생했다. 스탠바이 ResourceManager의 액티브 ResourceManager로 전환이 늦은 이유는 너무 많은 애플리케이션에 대한 state 데이터를 가져오느라 전환 시간이 오래 걸렸기 때문이다.

ResourceManager는 현재 실행되는 애플리케이션에 대한 정보만 있으면 되는데, 기본 설정(10,000)이 크다 보니 이미 완료된 애플리케이션에 대한 정보도 같이 저장되고 있었다. ResourceManager의 상태 저장 개수(yarn.ResourceManager.state-store.max-completed-applications 설정)의 기본 설정이 너무 크게 되어 있다고 생각하고 소스코드를 확인해 보니 다음과 같이 yarn.resourcemanager.max-completed-applications 설정과 동일한 값을 사용하고 있었다.

  /** The maximum number of completed applications RM keeps. */ 
  public static final String RM_MAX_COMPLETED_APPLICATIONS =
    RM_PREFIX + "max-completed-applications";
  public static final int DEFAULT_RM_MAX_COMPLETED_APPLICATIONS = 10000;
  /**
   * The maximum number of completed applications RM state store keeps, by
   * default equals to DEFAULT_RM_MAX_COMPLETED_APPLICATIONS
   */
  public static final String RM_STATE_STORE_MAX_COMPLETED_APPLICATIONS =
      RM_PREFIX + "state-store.max-completed-applications";
  public static final int DEFAULT_RM_STATE_STORE_MAX_COMPLETED_APPLICATIONS =
      DEFAULT_RM_MAX_COMPLETED_APPLICATIONS;

두 설정값은 사용 방식이 다른데 동일한 설정값을 공유하고 있는 문제가 있었다. 그 외에 state를 저장하는 설정의 기본값이 큰 것에 대한 문제점은 YARN-4464에서 논의가 진행됐다. 지금은 이 값을 '0'으로 설정해서 사용하고 있다.

마치며

지금까지 네이버 검색 서비스를 위한 데이터를 만드는 Hadoop 클러스터를 운영하면서 경험했던 몇 가지 사례를 소개했다.
소개한 내용이 Hadoop 클러스터 운영에서 언제나 발생하는 문제도 아니고, 이 문제가 해결된다고 완벽한 클러스터가
되는 것도 아니다. 그래도 비슷한 문제를 겪고 있는 Hadoop 클러스터 운영자에게 조금이나마 도움이 됐으면 한다.



   정재부|Naver Search
   네이버에서 다양한 분산 오픈 소스 솔루션들을 이용해 구축한 검색 데이터 정제 플랫폼을 운영하고 있습니다.
   Hadoop, HBase 기반의 멀티 테넌트 분산 플랫폼 구축과 Storm, Spark을 이용한 문서 정제 프레임워크 개발에 관심이
   많습니다.