HDFS Observer NameNode에서의 일관된 읽기
HDFS Observer NameNode에서의 일관된 읽기 (Consistent Reads from HDFS Observer NameNode)
HA 클러스터에 Observer NameNode라는 새 유형의 NameNode를 도입해 일관된 읽기를 제공하는 기능을 설명하는 문서예요. Active NameNode의 부하를 분산해 전체 처리량을 높이며, 배포·구성·새 관리 명령·클라이언트 구성을 다룹니다. 자세한 기술 설계 개요는 HDFS-12943에 첨부된 문서를 확인하세요.
출처: 문서
본문
목적 (Purpose)
이 가이드는 HDFS Observer NameNode 기능의 개요와 일반적인 HA 활성화 클러스터에서의 구성·설치 방법을 제공합니다.
배경 (Background)
HA 활성화 HDFS 클러스터(HDFSHighAvailabilityWithQJM 참고)에는 단일 Active NameNode와 하나 이상의 Standby NameNode가 있습니다. Active NameNode는 모든 클라이언트 요청을 서빙하고, Standby NameNode는 JournalNodes에서 편집 로그를 테일링해 네임스페이스에 대한 최신 정보를 유지하고 모든 DataNode에서 블록 리포트를 받아 블록 위치 정보를 유지합니다. 이 아키텍처의 단점은 Active NameNode가 단일 병목이 되어 클라이언트 요청으로 과부하될 수 있다는 것입니다. 특히 바쁜 클러스터에서 그렇죠.
Consistent Reads from HDFS Observer NameNode 기능은 Observer NameNode라는 새 유형의 NameNode를 도입해 이 문제를 해결합니다. Standby NameNode와 비슷하게 Observer NameNode는 네임스페이스와 블록 위치 정보를 최신으로 유지합니다. 추가로 Active NameNode처럼 일관된 읽기를 서빙할 수 있어요. 일반적인 환경에서 읽기 요청이 대다수이므로, 이는 NameNode 트래픽의 로드 밸런싱을 돕고 전체 처리량을 개선합니다.
아키텍처 (Architecture)
새 아키텍처에서 HA 클러스터는 active, standby, observer의 3가지 상태를 가진 네임노드들로 구성될 수 있습니다. 상태 전환은 active↔standby, standby↔observer 사이에서 일어날 수 있고, active↔observer 사이에서 직접 일어나지는 않아요.
단일 클라이언트 내에서 읽기-후-쓰기(read-after-write) 일관성을 보장하기 위해, RPC 헤더에 NameNode 내에서 트랜잭션 ID로 구현된 **상태 ID(state ID)**가 도입됩니다. 클라이언트가 Active NameNode를 통해 쓰기를 수행하면 NameNode의 최신 트랜잭션 ID로 상태 ID를 갱신합니다. 이후 읽기를 수행할 때 클라이언트는 이 상태 ID를 Observer NameNode에 전달하고, Observer는 자신의 트랜잭션 ID를 확인해 읽기 요청을 서빙하기 전에 자신의 트랜잭션 ID가 요청의 상태 ID를 따라잡았는지 보장합니다. 이는 단일 클라이언트의 "자신이 쓴 것을 읽는다(read your own writes)" 의미를 보장해요. 대역 외(out-of-band) 통신 상황에서 여러 클라이언트 간 일관성 유지는 아래 "클라이언트 일관성 유지" 절에서 논의합니다.
편집 로그 테일링은 Observer NameNode에 매우 중요합니다. 트랜잭션이 Active NameNode에 적용된 시점과 Observer NameNode에 적용된 시점 사이의 지연시간에 직접 영향을 주기 때문이에요. 이 지연을 크게 줄이는 "Edit Tailing Fast-Path" 라는 새 편집 로그 테일링 메커니즘이 도입됐습니다. 이는 기존 in-progress 편집 로그 테일링 기능 위에 구축되며, HTTP 대신 RPC 기반 테일링, JournalNode의 인메모리 캐시 같은 추가 개선을 포함해요. 자세한 내용은 HDFS-13150에 첨부된 설계 문서를 참고하세요.
새 클라이언트 측 프록시 프로바이더도 도입됩니다. 기존 ConfiguredFailoverProxyProvider를 상속하는 ObserverReadProxyProvider는 후자를 대체해 Observer NameNode에서 읽기를 활성화하는 데 사용해야 합니다. 클라이언트 읽기 요청을 제출할 때 프록시 프로바이더는 먼저 클러스터에서 사용 가능한 각 Observer NameNode를 시도하고, 모두 실패해야 Active NameNode로 폴백해요. 마찬가지로 IP failover 설정에서는 IPFailoverProxyProvider를 대체하는 ObserverReadProxyProviderWithIPFailover가 도입됩니다.
클라이언트 일관성 유지 (Maintaining Client Consistency)
위에서 논의했듯 클라이언트 'foo'는 Active NameNode에 대한 모든 요청(모든 쓰기 연산 포함) 시마다 상태 ID를 갱신합니다. Observer NameNode로 향하는 어떤 요청도 Observer가 그 트랜잭션 ID를 볼 때까지 기다리므로, 클라이언트가 자신의 모든 쓰기를 읽을 수 있게 보장됩니다. 하지만 'foo'가 대역 외(비-HDFS) 메시지를 클라이언트 'bar'에게 보내 쓰기가 수행됐다고 알리면, 'bar'의 이후 읽기는 'foo'의 최근 쓰기를 보지 못할 수 있어요. 이 불일치 동작을 막기 위해 새 msync()("metadata sync") 명령이 추가됐습니다. 클라이언트에서 msync()를 호출하면 Active NameNode를 기준으로 상태 ID를 갱신합니다(매우 가벼운 연산). 이후 읽기는 msync() 시점까지 일관성이 보장됩니다. 따라서 'bar'가 읽기 전에 msync()를 호출하기만 하면 'foo'가 만든 쓰기를 볼 수 있어요.
msync()를 사용하기 위해 애플리케이션이 코드를 바꿀 필요는 없습니다. 시작 시 클라이언트는 Observer에 대해 읽기를 수행하기 전에 자동으로 msync()를 호출해서, 클라이언트 초기화 이전에 수행된 모든 쓰기가 보이도록 합니다. 추가로 ObserverReadProxyProvider가 지원하는 구성 가능한 "auto-msync" 모드가 있어, 구성 가능한 간격으로 자동으로 msync()를 수행해 클라이언트가 특정 시간 한계보다 오래된 데이터를 보지 않도록 합니다. 각 갱신마다 Active NameNode에 RPC가 필요하므로 오버헤드가 있으며, 기본적으로 비활성화되어 있습니다.
배포 (Deployment)
구성 (Configurations)
Observer NameNode에서 일관된 읽기를 활성화하려면 hdfs-site.xml에 몇 가지 구성을 추가해야 합니다.
- dfs.namenode.state.context.enabled — NameNode가 서버 상태와 id를 유지·갱신하도록 활성화합니다.
이는 NameNode가 현재 서버 상태 id를 추적하는 정렬 컨텍스트(alignment context) 인스턴스를 만들게 해요. 서버 상태 id는 클라이언트로 다시 전달됩니다. Observer 읽기 경우 성능을 최적화하기 위해 기본적으로 비활성화되어 있지만, Observer NameNode 기능에는 켜는 것이 필수입니다.
<property>
<name>dfs.namenode.state.context.enabled</name>
<value>true</value>
</property>
- dfs.ha.tail-edits.in-progress — in-progress 편집 로그의 빠른 테일링을 활성화합니다.
이것은 in-progress 편집 로그를 통한 빠른 편집 로그 테일링과 RPC 기반 편집 로그 가져오기, JournalNodes의 인메모리 캐시 등 다른 메커니즘을 활성화합니다. 기본적으로 비활성화되어 있지만 Observer NameNode 기능에는 켜는 것이 필수입니다.
<property>
<name>dfs.ha.tail-edits.in-progress</name>
<value>true</value>
</property>
- dfs.ha.tail-edits.period — Standby/Observer NameNode가 JournalNodes에서 편집을 가져오는 주기.
이는 Observer NameNode의 Active에 대한 지연(staleness)을 결정합니다. 너무 크면 클라이언트 요청이 Observer가 편집 로그를 테일링해 Active의 최신 상태를 따라잡기 전까지 RPC 큐에서 더 오래 기다리므로 RPC 시간이 늘어납니다. 기본값은 1분입니다. 훨씬 낮은 값으로 구성하는 것이 강력히 권장됩니다. 낮은 값을 쓸 때는 backoff를 활성화하는 것도 권장됩니다(아래 참고).
<property>
<name>dfs.ha.tail-edits.period</name>
<value>0ms</value>
</property>
- dfs.ha.tail-edits.period.backoff-max — Standby/Observer NameNode가 편집 테일링 시 backoff를 수행할지 여부.
이는 Standby/Observer가 JournalNodes에서 편집을 테일링하려다 사용 가능한 편집이 없음을 발견할 때의 동작을 결정합니다. 편집 테일링 주기가 매우 낮지만 클러스터가 과부하되지 않은 일반적인 상황입니다. 이 구성이 없으면 Standby/Observer가 사용 가능한 편집이 없음에도 계속 편집을 읽으려 해서 활용도가 높아집니다. 이 구성을 활성화하면 편집 테일링 시도가 0개 편집을 반환할 때 지수 backoff가 수행됩니다. 이 구성은 편집 테일링 시도 사이의 최대 대기 시간을 지정합니다.
<property>
<name>dfs.ha.tail-edits.period.backoff-max</name>
<value>10s</value>
</property>
- dfs.journalnode.edit-cache-size.bytes — JournalNodes의 인메모리 캐시 크기(바이트).
이는 JournalNode 측에서 편집을 저장하는 인메모리 캐시의 크기(바이트)입니다. 캐시는 RPC 기반 테일링으로 편집을 서빙하는 데 사용됩니다. dfs.ha.tail-edits.in-progress가 켜져 있을 때만 효과가 있어요.
<property>
<name>dfs.journalnode.edit-cache-size.bytes</name>
<value>1048576</value>
</property>
- dfs.journalnode.edit-cache-size.fraction — JVM 최대 메모리 중 비율을 가리키는 분수.
JournalNode 메모리에 유지되는 편집 캐시 크기를 계산하는 데 사용됩니다. 이 구성은 dfs.journalnode.edit-cache-size.bytes의 대안입니다. RPC 기반 메커니즘으로 테일링용 편집을 서빙하는 데 사용되며, dfs.ha.tail-edits.in-progress가 true일 때만 활성화됩니다. 트랜잭션은 크기가 다양하지만 평균 약 200바이트이므로 기본 1MB는 약 5000개의 트랜잭션을 저장할 수 있어요. 따라서 최대 메모리를 기준으로 합리적인 값을 구성할 수 있습니다. 권장 값은 0.9보다 작습니다. dfs.journalnode.edit-cache-size.bytes를 설정하면 이 파라미터는 효과가 없어요.
<property>
<name>dfs.journalnode.edit-cache-size.fraction</name>
<value>0.5f</value>
</property>
- dfs.namenode.accesstime.precision — HDFS 파일의 접근 시간 활성화 여부.
이 구성을 비활성화하는 것이 강력히 권장됩니다. 활성화하면 열린 파일의 시간을 갱신하기 위해 쓰기 잠금을 잡아야 하므로 getBlockLocations 호출이 쓰기 호출이 됩니다. 따라서 요청이 모든 Observer NameNode에서 실패하고 결국 active로 폴백합니다. 결과적으로 RPC 성능이 저하돼요.
<property>
<name>dfs.namenode.accesstime.precision</name>
<value>0</value>
</property>
새 관리 명령 (New administrative command)
Standby NameNode를 observer 상태로 전환하는 새 HA 관리 명령이 도입됩니다:
haadmin -transitionToObserver
이것은 Standby NameNode에서만 실행할 수 있습니다. Active NameNode에서 호출하면 예외가 발생해요.
마찬가지로 기존 transitionToStandby도 Observer NameNode에서 실행할 수 있으며, observer를 standby 상태로 전환합니다.
참고: Observer NameNode가 failover에 참여하는 기능은 아직 구현되지 않았습니다. 따라서 다음 절에서 설명하듯 observer를 올리려면 transitionToObserver만 사용해야 해요. ZKFC는 Observer NameNode에서 켤 수 있지만, NameNode가 Observer 상태일 때는 아무것도 하지 않습니다. ZKFC는 NameNode가 standby 상태로 전환된 뒤 Active 선출에 참여합니다.
배포 세부 사항 (Deployment details)
observer 지원을 활성화하려면 먼저 2개 이상의 네임노드를 가진 HA 활성화 HDFS 클러스터가 필요합니다. 그런 다음 Standby NameNode를 observer 상태로 전환해야 합니다. 최소 구성은 클러스터에서 3개의 네임노드(active 1, standby 1, observer 1)를 실행하는 것입니다. 큰 HDFS 클러스터에서는 읽기 요청 강도와 HA 요구사항에 따라 Observer를 2개 이상 실행하는 것을 권장합니다.
현재 Observer NameNode는 자동 failover가 활성화될 때 완전히 통합되지 않습니다. dfs.ha.automatic-failover.enabled가 켜져 있으면 Observer NameNode에서 ZKFC를 실행하는 유일한 이점은 NameNode를 Standby로 전환한 뒤 자동으로 Active 선출에 참여한다는 것입니다. 이것이 바람직하지 않으면 Observer NameNode에서 ZKFC를 비활성화할 수 있어요. 추가로 transitionToObserver 명령에 forcemanual 플래그를 추가해야 합니다:
haadmin -transitionToObserver -forcemanual
미래에는 이 제한이 해제될 것입니다.
클라이언트 구성 (Client configuration)
읽기 접근에 Observer NameNode를 사용하려는 클라이언트는 클라이언트 측 hdfs-site.xml 구성 파일에서 프록시 프로바이더 구현으로 ObserverReadProxyProvider 클래스를 지정할 수 있습니다:
<property>
<name>dfs.client.failover.proxy.provider.<nameservice></name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider</value>
</property>
Observer NameNode를 사용하지 않으려는 클라이언트는 기존 ConfiguredFailoverProxyProvider를 그대로 쓰면 되고, 동작 변화를 보지 않아야 합니다.
"auto-msync" 기능을 사용하려는 클라이언트는 아래 구성을 조정해야 합니다. 이 구성은 일정 기간을 지정하며, 그 기간 후에 클라이언트의 상태 ID가 Active NameNode로부터 갱신되지 않았다면 msync()가 자동으로 수행됩니다. 0으로 지정하면 모든 읽기 연산 전에 msync()가 수행됩니다. 양수 지속시간이면 읽기 연산이 요청될 때마다 Active에 그 기간보다 오래 접촉하지 않았다면 msync()가 수행됩니다. 음수(기본값)이면 자동 msync()가 수행되지 않습니다.
<property>
<name>dfs.client.failover.observer.auto-msync-period.<nameservice></name>
<value>500ms</value>
</property>
더 알아보기 (Learn more)
- 원문: 문서