
[ "하둡 완벽 가이드 4판" ] 공부한 후 정리한 내용 입니다
5. 하둡 I/O
하둡은 데이터 I/O를 위한 프라미티브( 내장기능 제공 )
1) 데이터 무결성
- 하둡에서 데이터 무결성은 데이터가 유실되거나 손상되지 않도록 보장하는 것을 의미합니다.
- 즉, 저장된 데이터와 읽어드린 데이터가 항상 동일함을 보장하는 기능
체크섬(Checksum)
- 체크섬: 데이터를 일정 규칙으로 계산한 검증용 숫자
- 데이터가 손상되면 체크섬 값이 달라짐 → 손상 판단 가능
- 하둡에서는 CRC-32 / CRC-32C 사용
체크섬은 오류 검출만 가능하며 데이터 복구는 하지 않습니다. >> 시스템 메모리는 ECC 메모리 사용 권장 ( 오류 정정 기능이 있는 메모리 )
1.1 HDFS의 데이터 무결성
- HDFS는 데이터를 저장할 때와 읽을 때 자동으로 체크섬(Checksum) 검증을 합니다.
- 체크섬 설정
- 기본 512바이트마다 체크섬 4바이트 생성
- 오버헤드: 전체 공간의 약 1% 미만
- 데이터 노드 역할
- 단순 저장만 하는 것이 아니라, 손상 여부 검증도 담당
[ 그럼 언제 검증?? ]
- 저장 시 : 클라이언트가 데이터 전송 → 데이터 노드가 받는 즉시 체크섬 계산 후 검증
- 복제 시 : 다른 노드로 블록 복사 → 수신 데이터 체크섬 검증
[ 데이터 전송( 파이프라인 구조 ) ]
클라이언트 → 데이터노드1 → 데이터노드2 → 데이터노드3
- 마지막 노드에서 체크섬 검증 후 저장
- 데이터 손상 시 → 클라이언트에 오류 신호
- 클라이언트는 IOException 처리 → 재처리 가능
- 읽을 때도 저장된 체크섬과 수신 데이터 체크섬을 비교하여 검증
[ 추가 안전 기능 ]
- 체크섬 검증 로그: 데이터 블록 별 마지막 검증 시간 기록 → 디스크 오류 감지에 활용
- DataBlockScanner: 백그라운드에서 주기적으로 모든 블록 재검사 → Bit rot 예방 ( 물리적 저장장치 문제 )
[ 손상 블록 복구 과정 ]
- 클라이언트가 데이터 읽다가 손상 블록 발견
- ChecksumException 발생 → 블록 + 노드 정보 네임노드 보고
- 네임노드가 손상 블록 표시 → 다른 노드/클라이언트 접근 금지
- 정상 복제본을 다른 노드에 생성
- 손상 블록 자동 삭제
[ 체크섬 검증 비활성화 ]
- 코드: FileSystem.setVerifyChecksum(false)
- 쉘: -get 또는 -copyToLocal 명령에 -ignoreCrc 옵션
손상된 파일 조사나 복구 가능 여부 확인 시 유용
[ 파일 체크섬 확인 ]
- 명령: hadoop fs -checksum <파일>
- 용도: 두 파일 내용 동일 여부 확인 (예: distcp 시)
1.2 LocalFileSystem의 데이터 무결성
- 하둡은 로컬 디스크에 파일 저장할 떄도 자동으로 체크섬을 계산해 데이터가 손상되지 않도록 관리함
[ 파일 저장 시 ]
- Hadoop은 원본 파일 옆에 숨김 체크섬 파일(.crc) 생성
- .crc 파일에는 512바이트마다 계산된 체크섬 정보 포함
- 청크 크기 정보도 저장 → 나중에 청크 크기 변경되어도 파일 정확히 읽을 수 있음
- 결과: 파일 1개 저장 → 데이터 파일 + 체크섬 → 파일 2개 생성
[ 파일 읽기 시 ]
- 읽을 때 .crc 파일 참고 → 체크섬 검증
- 파일 손상 발견 → ChecksumException 발생
[ 성능 영향 ]
- 체크섬 계산으로 읽기/쓰기 속도 약간 느려짐
- 전체 성능에는 거의 영향 없음
- 데이터 안전성을 위한 납득할 수 있는 비용
[ 체크섬 비활성화 ]
체크섬이 필요 없는 경우, LocalFileSystem 대신 RawLocalFileSystem 사용 가능
< 전역설정 ( core-site.xml )에 설정 >
<configuration>
<property>
<name>fs.file.impl</name>
<value>org.apache.hadoop.fs.RawLocalFileSystem</value>
</property>
</configuration>
< 코드에서 부분 적용 >
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.RawLocalFileSystem;
import org.apache.hadoop.conf.Configuration;
Configuration conf = new Configuration();
RawLocalFileSystem rawFs = new RawLocalFileSystem();
rawFs.initialize(null, conf);
1.3 ChecksumFileSystem
- 체크섬 기능이 없는 파일시스템에도 쉽게 체크섬 기능을 붙일 수 있게 해주는 래퍼 입니다.
## 예시
FileSystem rawFs = ...; // 원래 파일 시스템 (체크섬 없음)
FileSystem checksummedFs = new ChecksumFileSystem(rawFs); // 체크섬 기능 추가
rawFs >> 원시 파일 시스템
checksummedFs >> 체크섬 기능이 추가된 파일 시스템
즉, 파일 읽기/쓰기 자체는 원시 파일 시스템에서 하고, 체크섬 기능만 추가로 제공하는 구조
[ 내부 원리 ]
- 단순 래퍼 역할: 데이터 처리 자체는 원시 파일 시스템에서 수행
- 필요하면 getRawFileSystem() 메서드로 원시 파일 시스템 접근 가능
[ 유용한 메서드 ]
| 메서드 | 역할 |
| getChecksumFile(Path file) | 해당 파일의 체크섬 파일 경로 반환 |
| reportChecksumFailure(Path file, Path checksumFile) | 읽기 중 체크섬 오류 발생 시 호출 |
- 기본 구현은 특별히 동작하지 않음
- 하지만 LocalFileSystem에서는
- 손상된 파일 + 체크섬 파일 → bad_files 디렉터리로 이동
- 관리자는 주기적으로 bad_files 확인 후 삭제/복구
2) 압축
- 압축 목적 : 저장공간 절약, 전송 속도 향상
- 대용량 데이터를 다루는 하둡 환경에서는 필수!!
2.1 하둡에서 사용 가능한 압축 알고리즘
| 알고리즘 | 분할 가능 여부 | 특징 |
| Gzip | 불가 | 널리 사용, 속도·압축률 균형 |
| Bzip2 | 가능 | 압축률 높지만 속도 느림 |
| LZO | 기본 불가, 색인(index) 생성 시 가능 | 압축/해제 속도 빠름 |
| LZ4 / Snappy | 불가 | 속도 매우 빠름, 압축률 낮음 |
분할 가능 의미!!
- 큰 파일을 여러 조각으로 나눠 동시에 처리 가능
- MapReduce 작업에 적합
2.2 속도 vs 압축률
- 속도 빠르면 압축률 낮음
- 압축률 높으면 속도 느림
- 즉, 시간 vs 공간의 트레이드오프 관계
[ 압축 옵션 ] ( -1 ~ -9 )
- -1 : 속도 우선
- -9 : 공간 우선
## 예시
gzip -1 file # 가장 빠른 방식으로 file.gz 생성
2.3 압축 알고리즘 정리
- 빠른 순서: LZO / LZ4 / Snappy > Gzip > Bzip2
| 알고리즘 | 속도 | 압축률 | 분할 가능 | 비고 |
| LZO | 매우 빠름 | 낮음 | ❌ (색인 시 ✅) | 실시간 처리에 유리 |
| LZ4 | 매우 빠름 | 낮음 | ❌ | 최신 시스템에서 자주 사용 |
| Snappy | 매우 빠름 | 낮음 | ❌ | 구글에서 개발 |
| Gzip | 보통 | 보통 | ❌ | 범용적으로 많이 사용 |
| Bzip2 | 느림 | 높음 | ✅ | 고압축 필요 시 유용 |
2.4 코덱
- 코덱 = 데이터를 압축/해제하는 알고리즘 구현체
- 하둡에서는 "CompressionCodec" 인터페이스로 제공됨 → 파일 확장자를 보고 자동으로 압축/해제 가능
| 코덱명 | 클래스명 | 확장자 | 특징 |
| Gzip | org.apache.hadoop.io.compress.GzipCodec | .gz | 압축률 좋음, 속도 보통 |
| Bzip2 | org.apache.hadoop.io.compress.BZip2Codec | .bz2 | 압축률 매우 높지만 느림 |
| LZO | com.hadoop.compression.lzo.LzopCodec | .lzo | 압축/해제 매우 빠름, 실시간 처리에 유리 |
[ CompressionCodecFactory ]
- 파일 확장자를 기반으로 적절한 코덱 자동 탐지
→ 예: xxx.gz → GzipCodec 자동 적용
[ 원시 라이브러리 ]
- 하둡은 C/C++로 구현된 압축 라이브러리 사용 → Java 대비 해제 50% 빠름 / 압축 10% 빠름
- 자동으로 OS에 맞는 원시 라이브러리 찾아 사용
- 필요 시 비활성화
# 비활성화 시 >> java로 동작하는 기본 코덱 사용
io.native.lib.available = false
[ CodecPool ]
- 압축기/해제기 객체를 매번 새로 생성하는 것은 비효율적 → Pool 방식으로 재사용
- 객체 생성 비용 감소 → 지속 압축/해제 시 성능 향상
2.5 압축과 입력 스플릿 ( MR 성능 핵심 포인트!! )
- MapReduce는 입력 파일을 여러 스플릿으로 분리하여 병렬 처리 수행
→ 따라서 "압축 포맷이 분할 가능한가?" 가 매우 중요
## 이게 무슨 소리냐면!! ##
Gzip + MapReduce 동작 방식
- HDFS는 파일을 블록 단위로 나눠 저장
하지만.. Gzip은 중간에서 분할 위치를 인식할 수 없음..
→ 압축 해제하려면 파일 처음부처 순차적으로 읽어야됨
즉, MapReduce가 map task를 만들떄 블록 단위로 나눠져 있어도
[File 1 block]
[File 2 block]
[File 3 block]
실제 실행은
맵태스크 1개가 전체 gzip 파일을 처음부터 끝까지 처리 ( 순차적으로 처리 )
결론 : 여러 HDFS 블록에 나눠 저장돼 있어도 "쪼개서 병렬 처리할 수 없음!!"
| 상황 | 결과 |
| 분할 가능한 압축 포맷 사용 | 여러 Map Task 병렬 실행 → 성능 ↑ |
| 분할 불가능 압축 포맷 사용 | 1개의 Map Task가 전체 파일 처리 → 성능 ↓ |
- Gzip 압축 1GB 파일 → HDFS 블록 여러 개로 나뉘어 저장되더라도 Gzip은 분할 불가 → 결국 Map Task 1개가 전체 파일 처리 → 병렬성 손실
- Bzip2 파일 → 블록 단위 구조 + Marker 존재 → 분할 가능
- LZO 파일 → 색인 생성 시 분할 가능
즉, 하둡에서는 파일 크기가 클수록 분할 가능한 압축 포맷이 필수!!
2.6 맵리듀스에서 압축 사용하기
- 하둡은 확장자만 있어도 압축 자동 인식 → 개발자가 별도 압축 코드 작성 X
[ 출력 압축 설정 ]
mapreduce.output.fileoutputformat.compress=true
mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec
## 시퀀스 파일 출력 시 ##
RECORD : 개별 레코드 압축 ( default )
BLOCK : 여러 레코드를 묶어 압축 ( 압축률 / 성능 ↑ )
[ 명령줄에서 바로 설정 ]
hadoop jar job.jar MyJob \
-Dmapreduce.output.fileoutputformat.compress=true \
-Dmapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec \
(input) (output)
→ 코드 수정 없이 압축 설정 교체
[ 맵 출력 압축 (매우 중요!) ]
- 맵 출력 데이터는 디스크에 임시 저장 + 네트워크로 리듀스에 전송됨
→ 압축 시 디스크 I/O 감소 + 전송량 감소 → 전체 성능 향상
mapreduce.map.output.compress=true
mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec
→ 보통 맵 출력은 Bzip2 처럼 느릭 코덱은 비추합니다.. Snappy / LZO / LZ4 사용 (빠르고 CPU 부담 낮음)
3) 직렬화
- 직렬화 → 객체를 네트워크로 보낼 수 있는 바이트 스트림으로 변환하는 과정
- 역질렬화 → 바이트 스트림을 다시 원래 객체로 복원하는 과정
[ 왜 직렬화가 필요? ]
- 분산 시스템은 여러 노드가 네트워크로 데이터를 주고받음 → 이떄, RPC 방식 사용 ( 다른 노드의 메서드 호출 ) → 문제점!.. 객체는 그대로 네트워크 전송 불가
- 즉, 내부 통신 과정에서 [ 객체 → 직렬화 → 네트워크 전송 → 역직렬화 → 객체 복원 ] 과정을 거침
[ RPC 직렬화 포맷이 가져야할 4가지 장점 ]
- 간결성
- 포맷이 간결할수록 전송 데이터 크기 ↓
- 네트워크 대역폭 절약
- 고속화
- 직렬화 / 역질렬화 빠른 포맷 == 전체 시스템 성능 향상
- 직렬화 / 역질렬화가 느리면 → 전체 시스템 성능도 느려짐
- 확장성
- 기존 데이터 유지 + 새 필드 추가 가능 == 좋은 직렬화 포맷
- 버전 변화에 유연해야 함
- 상호운영성
- 클라이언트가 ( java, python, c.. ) 다양한 언어 사용
- 직렬화 포맷도 여러 언어에서 공통으로 사용 가능해야 함
[ RPC 직렬화 vs 영속적 저장소 포맷 차이 ]
| 구분 | RPC 직렬화 | 영속적 저장소 포맷 |
| 수명 | 짧음 (보통 1초 미만) | 매우 김 (수년 보관) |
| 목적 | 빠른 전송 | 안전한 저장 & 재사용 |
| 중점 | 속도, 효율 | 안정성, 호환성 |
- 차이는 있지만 → 간결성 / 고속화 / 확장성 / 상호운용성 4가지는 둘 다 중요
[ 하둡의 직렬화 방식 - Writable ]
- Writable은 하둡이 자체적으로 만든 직렬화 인터페이스
[ 장점 ]
- 구조 단순
- 매우 빠름
- 하둡 내부 통신에 최적화 ( 속도 우선 )
[ 단점 ]
- 자바 전용
- 타 언어 호환이 어려움
[ 요즘 트랜드? ]
- Writable 대신 범용 직렬화 포맷을 많이 사용 → Avro, Protocol Buffers, Thrift ( 다양한 언어 지원 + 스키마 기반 + 확장성 우수 )
- 하둡 기본 RPC 통신은 Writalbe이 디폴트 지만, 필요시 범용 직렬화 포맷을 RPC에 쓸 수 있음
# 범용 직렬화 포맷 장단점
[ 장점 ]
- 여러 언어 지원
- 스키마 기반이라 버전 업그레이드 시 호환성 좋음
[ 단점 ]
- Writable 보다 느림
- 구조 복잡하면 직렬화 / 역직렬화 비용 증가
3.1 Writable 인터페이스
[ Writable 기본 개념 ]
- 하둡 내부 통신에서 데이터를 빠르게 직렬화 / 역직렬화하기 위해 만든 인터페이스
객체 → [직렬화] → 바이트 스트림 → [역직렬화] → 객체
- 메서드
void write(DataOutput out) // 직렬화: 객체 → 바이트
void readFields(DataInput in) // 역직렬화: 바이트 → 객체
[ Writable 사용 예시 ]
- 직렬화
## 직렬화 ( 객체 → 바이트 )
IntWritable writable = new IntWritable(163);
byte[] bytes = serialize(writable); // 내부적으로 163이 4바이트로 변환
## 직렬화 과정
IntWritable(163)
↓ write()
ByteArrayOutputStream → DataOutputStream
↓
byte[4] = {00 00 0a 3} // 실제 바이트 배열
## 확인
assertThat(bytes.length, is(4)); // 4바이트 확인
- 역직렬화
## 역직렬화 ( 바이트 → 객체 )
IntWritable newWritable = new IntWritable();
deserialize(newWritable, bytes);
assertThat(newWritable.get(), is(163)); // 163이 잘 복원됨
## 역질렬화 과정
byte[4] = {00 00 0a 3}
↓ readFields()
IntWritable(163)
[ WritableComparable ]
- Writable + Comparable( 비교 ) 기능
- 맵리듀스 키 정렬 떄문에 필요
IntWritable extends WritableComparable<IntWritable>
[ RawComparator ]
- 바이트 배열 상태에서 바로 비교
- 객체 생성 없이 비교 가능 → 속도 빠름
int compare(byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2);
# b1, b2 → 비교할 바이트 배열
# s1~l1, s2~l2 → 시작 위치와 길이
[ WritableComparator ]
- 하둡 기본 사용
- 범용 비교기 + 속도 최적화
- 두 가지 비교 방법 지원
- 객체로 변환 후 비교 ( b1 → 객체1 → 값 추출 → 비교 )
- 바이트 상태에서 비교 ( b1, b2 → 바로 값 추출 → 비교 (객체 생성 오버헤드 없음) )
- 하둡 내부 정렬 단계에서는 자동으로 RawComparator 사용 → 없으면 compareTo()로 폴백
# MapReduce 정렬 단계: 키 비교
┌─────────────┐
│ RawComparator?│
└───────┬─────┘
│ 있으면
▼
[ 바이트 상태로 비교 ] ← 속도 빠름
│
│ 없으면
▼
[ 객체 생성 후 compareTo() ] ← 속도 느림
3.2 Writable 클래스
- 하둡 내부 통신시 반드시 직렬화가 필요
- 자바 기본 자료형( int, long, double... )는 바로 직렬화 불가 → Writable 래퍼 클래스 제공
기본 자료형 → Writable 래퍼 → 직렬화 → 바이트 스트림
[ 자바 기본 자료형 용 Writable 래퍼 ]
| 자바 자료형 | Writable 클래스 | 직렬화 크기 |
| boolean | BooleanWritable | 1바이트 |
| byte | ByteWritable | 1바이트 |
| short | ShortWritable | 2바이트 |
| int | IntWritable | 4바이트 |
| int | VIntWritable | 1~5바이트 (가변) |
| float | FloatWritable | 4바이트 |
| long | LongWritable | 8바이트 |
| long | VLongWritable | 1~9바이트 (가변) |
| double | DoubleWritable | 8바이트 |
- 고정길이 vs 가변길이
- 고정길이 : 항상 일정한 바이트 사용 → 연산 빠름
- 가변길이 : 값이 작으면 바이트 절약 → 저장 공간 효율 ↑
[ 문자열용 Writable - Text ]
- 하둡 문자열용 Writable 클래스
- UTF-8 인코딩 사용 ( 1~4 바이트 )
- 바이트 기준으로 인덱스 관리
String s = "가A"; // String 기준 인덱스
Text t = new Text("가A"); // Text 기준 = 바이트 기준
| 문자 | UTF-8 인코딩 | 바이트 수 |
| 가 | EA B0 80 | 3바이트 |
| A | 41 | 1바이트 |
- Text 내부는 ByteBuffer + bytesToCodePoint() 로 문자 접근
- 재활용 가능 → set()으로 문자열 바꿔도 내부 배열 재사용
- getBytes() → 내부 배열 전체
- getLength() → 실제 유효 데이터 길이
Text t = new Text("hadoop");
t.set("pig");
t.getBytes() → [p,i,g,o,o,p] (배열 재사용)
t.getLength() → 3 (실제 유효 데이터)
- 문자열 조작 필요 시 → toString()로 String 변환
Text text = new Text("hadoop");
String str = text.toString(); // "hadoop"
[ 바이트 배열용 Writable - BytesWritable ]
- 이미지, 압축파일 등 바이너리 데이터 전용 Writable 클래스
- 구조
길이 정보(4바이트) + 데이터
- set(), getBytes(), getLength() 지원
BytesWritable b = new BytesWritable(new byte[]{3,5});
b.set(new byte[]{1,2});
b.getBytes() → [1,2,7,9] (배열 재활용)
b.getLength() → 2 (유효 데이터)
[ NullWritable ]
- 값이 필요 없을 떄 사용하는 빈 데이터
- 길이 0, 직렬화 가능
값만 필요 → emit(NullWritable.get(), value)
키만 필요 → emit(key, NullWritable.get())
[ 범용 Writable - ObjectWritable & GenericWritable ]
## ObjectWritable
| ow1 | "org.apache.hadoop.io.Text" + "apple" |
| ow2 | "org.apache.hadoop.io.IntWritable" + 100 |
| ow3 | "org.apache.hadoop.io.BooleanWritable" + true |
| ow4 | "org.apache.hadoop.io.NullWritable" + null |
## GenericWritable
| gw1 | 0 | "banana" |
| gw2 | 1 | 200 |
| gw3 | 2 | false |
| 구분 | ObjectWritable | GenericWritable |
| 저장 가능 데이터 | 모든 타입 | 미리 정의된 타입만 |
| 저장 방식 | 클래스명 + 데이터 | 인덱스 번호 + 데이터 |
| 유연성 | 높음 | 낮음 |
| 공간 효율 | 낮음 | 높음 |
- ObjectWritable → 다양한 타입 썩어서 저장 가능
- GenericWritable → 저장 공간 절약, 타입 미리 정의 필요
[ Writable 컬렉션 ]
| 클래스 | 용도 |
| ArrayWritable | 1차원 배열 |
| TwoDArrayWritable | 2차원 배열 |
| ArrayPrimitiveWritable | int[], double[] 등 기본형 배열 |
| MapWritable | Map<K,V> 구조 |
| SortedMapWritable | 정렬된 Map |
| EnumSetWritable | enum 집합 |
## ArrayWritable
ArrayWritable arr = new ArrayWritable(Text.class);
arr.set(new Writable[]{new Text("sy"), new Text("jw"), new Text("sj")});
## MapWritable
MapWritable map = new MapWritable();
map.put(new Text("sy"), new IntWritable(28));
map.put(new Text("jw"), new IntWritable(31));
## 다른 타입 썩는 리스트 → ListWritable
public class ListWritable extends MapWritable {
public void add(Writable value) {
put(new IntWritable(size()), value); // index를 key로 사용
}
}
→ 이처럼 하둡에서는 모든 데이터가 Writable 형태로 직렬화되어 처리됨!!
'데이터 엔지니어( 실습 정리 ) > hadoop' 카테고리의 다른 글
| 4. Yarn ( 하둡 기초 ) (0) | 2025.10.11 |
|---|---|
| 3. 하둡 분산 파일 시스템 ( 하둡 기초 ) (2) | 2025.09.15 |
| 2. 맵 리듀스 ( 하둡 기초 ) (0) | 2025.09.06 |
| 1. 하둡과의 만남 ( 하둡 기초 ) (0) | 2025.09.02 |