1. HDFS 쓰기 프로세스
1.1 핵심 개념
파이프라인(Pipeline) 전송 방식
HDFS는 파일 업로드 시 데이터를 전송하는 독특한 방식을采用합니다. 클라이언트가 데이터를 첫 번째 데이터노드에 기록하면, 해당 노드는 데이터를 저장한 후 두 번째 노드로 블록을 복제합니다. 두 번째 노드도 저장 후 세 번째 노드로 최종 복제합니다.
파이프라인 방식이Topology 구조로 여러 노드에 동시에 전송하지 않는 이유는 대역폭 활용도를 극대화하고 네트워크 병목 현상을 방지하기 위함입니다. 순차적 전송 방식은 각 시스템의 모든 출력 대역폭을 활용하여 데이터를 가장 빠르게 전송할 수 있습니다.
ACK 확인 응답 메커니즘
ACK(Acknowledge Character)는 수신 측이 송신 측에게 데이터 수신을 확인하는 전송 제어 문자를 의미합니다. HDFS 파이프라인에서는 데이터 전송과 역방향으로 ACK 검증이 수행되어数据传输의 무결성을 보장합니다.
기본 3중 복제 정책
- 첫 번째 복제본: 클라이언트 로컬에 우선 배치, 불가능할 경우 무작위 선택
- 두 번째 복제본: 첫 번째 복제본과 다른 랙(Rack)에 위치
- 세 번째 복제본: 두 번째 복제본과 동일한 랙 내 다른 머신에 배치
1.2 데이터 쓰기 절차
1단계: FileSystem 객체 생성
HDFS 클라이언트는 DistributedFileSystem 객체를 생성합니다. 이 객체는 파일시스템 관련 모든 연산을 캡슐화합니다.
2단계: 파일 생성 요청
클라이언트가 create() 메서드를 호출하면 RPC를 통해 NameNode에 파일 생성을 요청합니다. NameNode는 다음 항목을 검증합니다:
- 대상 파일이 이미 존재하는지 확인
- 부모 디렉토리가 유효한지 확인
- 클라이언트에 파일 생성 권한이 있는지 확인
검증 통과 시 NameNode는 해당 요청을 기록하고 FSDataOutputStream 출력 스트림을 클라이언트에게 반환합니다.
3단계: 데이터 쓰기 시작
클라이언트는 FSDataOutputStream을 통해 데이터 쓰기를 시작합니다. 이 스트림은 DFSOutputStream을 래핑한 클래스입니다.
4단계: 데이터 파이프라인 전송
// 데이터 처리 로직 예시
class DataWriter {
private static final int PACKET_SIZE = 64 * 1024;
private Queue<DataPacket> writeBuffer = new LinkedList<>();
private List<DataNode> replicaNodes;
public void writeData(byte[] data) {
// 데이터를 패킷으로 분할
List<DataPacket> packets = splitIntoPackets(data);
// NameNode에 복제 노드 요청
replicaNodes = nameNode.requestReplicaLocations();
// 파이프라인을 통한 전송
for (DataPacket packet : packets) {
sendThroughPipeline(packet, replicaNodes);
}
}
private void sendThroughPipeline(DataPacket packet, List<DataNode> nodes) {
for (int i = 0; i < nodes.size(); i++) {
DataNode currentNode = nodes.get(i);
currentNode.receiveData(packet);
if (i < nodes.size() - 1) {
currentNode.forwardToNext(nodes.get(i + 1), packet);
}
}
}
}
데이터를 패킷(기본 64KB)으로 분할하여 내부 큐에 저장합니다. DataStreamer 클래스가 NameNode에 블록 저장을 위한 DataNode 목록을 요청하고, 첫 번째 노드로 데이터를 전송합니다. 각 노드는 데이터를 저장 후 다음 노드로 순차 전송합니다.
5단계: 확인 응답 대기
DFSOutputStream은 ACK 큐를 관리합니다. 파이프라인의 모든 DataNode가 패킷 수신을 확인하면 해당 패킷이 ACK 큐에서 제거됩니다. 이를 통해 데이터 전송의 신뢰성을 보장합니다.
6단계: 스트림 종료
클라이언트가 데이터 쓰기를 완료하면 close() 메서드를 호출하여 출력 스트림을 닫습니다.
7단계: NameNode에 완료 알림
DistributedFileSystem이 NameNode에게 파일 쓰기 완료를 알립니다. NameNode는 이미 블록 할당 정보를 알고 있으므로, 최소 복제 수(df.namenode.replication.min, 기본값 1)만 만족하면 성공 응답을 반환합니다.
2. HDFS 읽기 프로세스
1단계: FileSystem 객체 생성
읽기 操作도 쓰기와 동일하게 DistributedFileSystem 객체를 생성하고, open() 메서드를 호출하여 파일을 엽니다.
2단계: 블록 위치 정보 조회
DistributedFileSystem은 RPC를 통해 NameNode에 첫 번째 블록 그룹의 위치 정보를 요청합니다. NameNode는 클라이언트의 네트워크 토폴로지 기준 가까운 순서로 정렬된 DataNode 목록을 반환합니다.
3단계: 입력 스트림 반환
DistributedFileSystem은 FSDataInputStream 입력 스트림을 클라이언트에게 반환합니다. 이 클래스는 DFSInputStream을 래핑합니다.
4단계: 데이터 읽기 실행
// 데이터 읽기 로직 예시
class DataReader {
private List<DataNode> blockLocations;
private int currentNodeIndex = 0;
private FSDataInputStream inputStream;
public void readFile(String filePath) {
// 파일 열기
DistributedFileSystem fs = new DistributedFileSystem();
inputStream = fs.open(filePath);
// 블록 위치 조회
blockLocations = nameNode.getBlockLocations(filePath, 0, fileLength);
// 최적 노드 선택 (네트워크 거리 기반 정렬됨)
DataNode bestNode = selectOptimalNode(blockLocations.get(0));
// 데이터 읽기
byte[] buffer = new byte[BUFFER_SIZE];
while (inputStream.read(buffer) != -1) {
processData(buffer);
// 블록 종료 시 다음 최적 노드로 연결 전환
if (isBlockFinished()) {
bestNode = selectOptimalNode(getNextBlockLocations());
reconnect(bestNode);
}
}
}
private DataNode selectOptimalNode(BlockInfo block) {
return block.getNodes().get(0); // 정렬된 목록의 첫 번째 노드
}
}
클라이언트가 read() 메서드를 호출하면 DFSInputStream이 가장 가까운 DataNode에 연결하여 데이터를 읽습니다. 블록 종료 시 다음 블록의 최적 DataNode로 자동 전환됩니다.
5단계: 스트림 종료
읽기 操作 완료 후 FSDataInputStream의 close() 메서드를 호출하여 리소스를 해제합니다.
3. 핵심 정리
HDFS의 읽기/쓰기 프로세스는 NameNode의 메타데이터 관리와 DataNode의 실제 데이터 저장을 분리하여 확장성과 안정성을 보장합니다. 파이프라인 방식의 데이터 전송과 ACK 확인 메커니즘은 대규모 클러스터에서 효율적이고 안전한 데이터 操作을 가능하게 합니다.