hdfs의 file write protocol에 대한 이해
- 개발관련팁/Hadoop
- 2012. 1. 5.
의외로 자료가 별로 없어서.. 직접 소스코드와 로그를 뒤지면서 정리를 해 보았다.
출처 : Hadoop: The Definitive Guide, Second Edition
일단 책에 나와 있는 내용은 저 정도이고, 실제 소스코드를 찾아보면, 책에는 생략된 block management에 대한 내용도 있다. (사실 요 부분이 궁금해서..)
테스트 환경은 hadoop 0.20.5 / pseudo-distribute 모드이며, protocol 자체는 최근에 릴리즈된 hadoop 1.0 버전과 크게 차이는 없을 것이라고 생각된다. 아래는 local에 있는 102 MB (정확히는 106,168,320 byte) 샘플 파일을 hdfs에 upload하는 과정에서 발생한 log (debug log 포함)들을 모아서 정리한 것이다.
node |
time |
message |
comment |
Client |
21:50:56 |
hdfs.DFSClient: /test.txt: masked=rwxr-xr-x |
Client가 hdfs://test.txt 라는 파일을 생성하고자 함 |
Client |
21:50:56 |
hdfs.DFSClient: Allocating new block |
Client가 NameNode에게 block 요청 |
NameNode |
21:50:56,717 |
*DIR* NameNode.create: file /test.txt for DFSClient_439896788 at 127.0.0.1 |
Client가 file을 요청하면, NameNode는 namespace (메모리)에서 빈 file entry 생성 |
NameNode |
21:50:56,717 |
DIR* NameSystem.startFile: src=/test.txt, holder=DFSClient_439896788, ClientMachine=127.0.0.1, replication=1, overwrite=false, append=false |
FSNameSystem.startFileInternal() |
NameNode |
21:50:56,718 |
DIR* FSDirectory.addFile: /test.txt is added to the file system |
|
NameNode |
21:50:56,718 |
DIR* NameSystem.startFile: add /test.txt to namespace for DFSClient_439896788 |
|
NameNode |
21:50:56,724 |
ugi=chaehyun ip=/127.0.0.1 cmd=create src=/test.txt dst=null perm=chaehyun:supergroup:rw-r--r-- |
FSEditLog.logSync()가 완료 된 뒤 출력된 메시지. |
NameNode |
21:50:56,729 |
*BLOCK* NameNode.addBlock: file /test.txt for DFSClient_439896788 |
/test.txt 파일을 위한 block을 요청 |
NameNode |
21:50:56,729 |
BLOCK*NameSystem.getAdditionalBlock: file /test.txt for DFSClient_439896788 |
해당 file의 내용을 기록할 block과 이 block을 저장할 machine list를 반환. |
NameNode |
21:50:56,729 |
DIR* FSDirectory.addFile: /test.txt with blk_-1208142459293778377_1012block is added to the in-memory file system |
메모리에 저장된 file entry에 할당받은 block 정보들을 추가 |
NameNode |
21:50:56,729 |
BLOCK* NameSystem.allocateBlock: /test.txt. blk_-1208142459293778377_1012 |
FSNameSystem.allocateBlock() |
Client |
21:50:56 |
hdfs.DFSClient: pipeline = 127.0.0.1:50010 |
|
Client |
21:50:56 |
hdfs.DFSClient: Connecting to 127.0.0.1:50010 |
Client가 DataNode로 접속 |
Client |
21:50:56 |
hdfs.DFSClient: Send buf size 131072 |
Client가 해당 DataNode로 data를 packet 단위로 쪼개서 전송 시작 |
DataNode |
21:50:56,750 |
Receiving block blk_-1208142459293778377_1012 src: /127.0.0.1:6047 dest: /127.0.0.1:50010 |
DataNode가 Client로 부터 첫 번째 block을 받기 시작 |
DataNode |
21:50:57,606 |
PacketResponder 0 for block blk_-1208142459293778377_1012Closing down. |
DataNode가 Client로 부터 첫 번째 block 받기를 끝냄 |
Nadenode |
21:50:57,609 |
*BLOCK* NameNode.blockReceived: from 127.0.0.1:50010 1 blocks. |
DataNode가 NameNode에게 보고. |
Nadenode |
21:50:57,609 |
BLOCK* NameSystem.blockReceived: blk_-1208142459293778377_1012 is received from 127.0.0.1:50010 |
해당 block들을 정상적으로 수신했음을 알림. |
Nadenode |
21:50:57,609 |
BLOCK* NameSystem.addStoredBlock: blockMap updated: 127.0.0.1:50010 is added to blk_-1208142459293778377_1012 size 67108864 |
NameNode가 block들을 실제로 저장한 DataNode들의 정보를 저장 |
Client |
21:50:57 |
Allocating new block |
Client가 두 번째 block을 요청 |
Nadenode |
21:50:57,610 |
*BLOCK* NameNode.addBlock: file /test.txt for DFSClient_439896788 |
|
Nadenode |
21:50:57,610 |
BLOCK* NameSystem.getAdditionalBlock: file /test.txt for DFSClient_439896788 |
NameNode에서 추가 block 할당 |
Nadenode |
21:50:57,610 |
DIR* FSDirectory.addFile: /test.txt with blk_5548041259556473375_1012 block is added to the in-memory file system |
|
Nadenode |
21:50:57,610 |
BLOCK* NameSystem.allocateBlock: /test.txt. blk_5548041259556473375_1012 |
|
Client |
21:50:57 |
pipeline = 127.0.0.1:50010 |
|
Client |
21:50:57 |
Connecting to 127.0.0.1:50010 |
Client가 DataNode로 packet 단위로 쪼개진 data 전송 시작 |
DataNode |
21:50:57,611 |
Receiving block blk_5548041259556473375_1012 src: /127.0.0.1:6050 dest: /127.0.0.1:50010 |
DataNode가 Client로 부터 두 번째block을 받기 시작 |
DataNode |
21:50:58,034 |
PacketResponder 0 for block blk_5548041259556473375_1012 Closing down. |
DataNode가 Client로 부터 두 번 째 block 받기를 끝냄 |
Nadenode |
21:50:58,037 |
*BLOCK* NameNode.blockReceived: from 127.0.0.1:50010 1 blocks. |
DataNode가 NameNode에게 보고 |
Nadenode |
21:50:58,037 |
BLOCK* NameSystem.blockReceived: blk_5548041259556473375_1012 is received from 127.0.0.1:50010 |
해당 block들을 정상적으로 수신했음을 알림 |
Nadenode |
21:50:58,037 |
BLOCK* NameSystem.addStoredBlock: blockMap updated: 127.0.0.1:50010 is added to blk_5548041259556473375_1012 size 39059456 |
NameNode가 block들을 실제로 저장한 DataNode들을 저장 |
Nadenode |
21:50:58,038 |
*DIR* NameNode.complete: /test.txt for DFSClient_439896788 |
Client가 파일을 다 썼음 |
Nadenode |
21:50:58,038 |
DIR* NameSystem.completeFile: /test.txt for DFSClient_439896788 |
file을 구성하는 모든 block들이 최소 replication 이상 복사 되었음을 확인 |
Nadenode |
21:50:58,038 |
Removing lease on file /test.txt from Client DFSClient_439896788 |
|
Nadenode |
21:50:58,038 |
DIR* FSDirectory.closeFile: /test.txt with 2 blocks is persisted to the file system |
|
Nadenode |
21:50:58,038 |
DIR* NameSystem.completeFile: file /test.txt is closed by DFSClient_439896788 |
파일 쓰기 완료 |
복잡해 보이니 간단히 요약해보자.
- Client에서 파일을 쓰기 위해 NameNode에게 file 생성을 요청을 하면, NameNode는 먼저 메모리에 해당 path에 이미 파일이 존재하는지, Client가 적절한 권한을 가지고 있는지 확인한 다음, 문제가 없으면, namespace 상에서 file entry를 생성한다.
- 그런 다음, 실제 file을 저장할 block을 할당하고, Client의 위치와 DataNode들의 저장 상황, 위치 등을 고려하려, 해당 block을 저장할 DataNode 들을 선정한다.
- Client는 NameNode로 부터 받은 block 정보를 이용하여, 첫 번째 DataNode에 data를 전송하고, 전송이 정상적으로 완료되면, NameNode에게 다음 block을 요청한다.
- NameNode가 다시 block을 할당하면, Client가 file의 다음 내용을 DataNode에게 전송하고, 이 과정을 반복한다.
- Client에 있는 file의 모든 data가 DataNode들에게 정상적으로 전송이 끝나고, NameNode가 해당 파일에 속하는 모든 block들이 최소한의 복제 계수만큼 복사가 완료되었다고 판단하면 file write가 종료된다.
그 외 내가 궁금해서 찾아본 내 맘대로 FAQ)
- Q) NameNode는 block을 어떻게 관리하나?
A) block은 long으로 구분되며, 이론 상 2^64 개의 block을 생성할 수 있다. block은 계속 생성되고 소멸되므로, block id (long)을 관리하는 일이 중요한데, NameNode에서는 현재 굉장히 simple하게-_- 관리를 한다. 랜덤하게 block id값을 고른 다음, 아직 사용하지 않은 숫자가 나올 때 까지 계속한다. (허무할 정도로 간단하게..)
참고로, 사용 중인 block들에 대한 정보는 BlocksMap 이라는 class에서 관리하며, 내부에서는 LightWeightGSet 이라는 자체 set class를 만들어서 저장한다.
- Q) block을 저장할 DataNode들은 어떻게 선발하나요?
A) hadoop 0.20.5 버전에서는 ReplicationTargetChooser 라는 class가 해당 역할을 수행한다. 현재 버전에서의 복사 전략은 Client가 DataNode일 경우에는 먼저 해당 DataNode가 첫 번째 노드로 선정된다. (이럴 경우, 첫 번째 복사는 network을 타지 않기 때문에 빠르다) 만약 Client가 DataNode가 아니면, 그냥 랜덤하게 고른다.
그리고 두 번째 node는 첫 번째 DataNode와 다른 rack에 위치한 DataNode들을 고르고, 세 번째 DataNode는 첫 번째 node와 같은 rack에 있는 node를 고른다. 그리고 네 번째 부터는 그냥 랜덤하게 고른다.
이렇게 함으로써, data 전송을 위한 network traffic을 줄이고, data 안정성을 최대한으로 높일 수 있다. (복제계수가 2이상이면, 최소 하나 이상의 block은 다른 rack에 저장되므로, 한 rack 전체가 날아가더라도, file은 살아 있을 것이다)
그리고 ReplicationTargetChooser에게 DataNode를 요청할 때는 exclude list를 함께 줘서, 장애가 발생한 DataNode나, 이미 해당 block을 저장하고 있는 DataNode, decommission 중인 DataNode들은 제외되도록 설계되어 있다.