-
Avro Serialization 알아보기.BigData 2023. 10. 5. 13:18728x90반응형
- 목차
관련된 글
https://westlife0615.tistory.com/332
소개.
Avro 는 대표적인 Serialization Format 입니다.
Avro 가 다른 Serialization Format 과 다른 특징은 아래의 기술한 내용들인데요.
1. Binary Format 을 사용한다.2. Schema 를 필요로한다.
3. 프로그래밍 언어나 환경에 영향을 받지 않는다
등등 이 있습니다.
특히 Avro 가 Schema 를 중심으로 어떻게 활용되고 어떤 장단점이 있는지 알아보는 시간을 가지려고 합니다.Schema.
Avro 는 필수적으로 Schema 를 필요로 합니다.
흔히 아는 JSON 이나 XML 그리고 MessagePack 과 같은 Serialization Format 은 외부의 Schema 를 필요로 하지 않습니다.
이러한 상태를 Self-Describing 이라고 하는데요. ( * MessagePack 은 유니티에서 주로 활용됩니다. )
데이터 자체가 칼럼 정보와 데이터 타입을 가지는 데이터 형식입니다.
Avro 는 별도의 Schema 를 정의하여 사용합니다.
Self-Describing.
Self-Describing 의 예시를 들어보면,
문자열 타입의 name 칼럼과 숫자 타입의 age 칼럼를 가지는 데이터들은 아래와 같습니다.
<JSON>
{ "name" : "westlife", "age" : 30 }
<XML>
<?xml version="1.0" encoding="UTF-8" ?> <user> <name>westlife</name> <age>30</age> </user>
예시는 Text 기반의 포맷인 JSON 과 XML 로 작성하였습니다. (MessagePack 은 Binary 기반이라서 제외하였습니다.)
JSON 과 XML 모두 데이터에 name 과 age 라는 칼럼이 포함됩니다.
그리고 JSON 은 "" 의 유무로 문자열임을 식별하고, Number, Boolean 에 대한 식별 또한 가능합니다.
이렇게 데이터 자체가 스키마와 데이터 타입을 가지는 형태를 Self-Describing 이라고 합니다.
Schema 기반으로 직렬화.
각 데이터 타입별로 어떻게 직렬화되는지 알아보려고 합니다.
int 는 어떻게 직렬화될까 ?
int 타입의 데이터는 zigzag 인코딩을 사용합니다.
zigzag 인코딩은 모든 정수를 0 을 포함한 자연수로 변환하는 인코딩 기법입니다.
양의 정수는 짝수로 변환을 하고, 음의 정수는 홀수로 변환됩니다.
이러한 방식을 통해서 음의 값을 표현하지 않아도 되기 때문에 숫자 타입의 데이터를 표현함에 있어 크기의 제한이 없어집니다.
보통 4 바이트나 8 바이트로 숫자를 표현하죠.
이때, 첫번째 한 비트를 signed bit 로 주어집니다.
java 의 integer 타입의 1 이 00000000000000000000000000000001 으로 표현되고,
java 의 integer 타입의 -1 이 11111111111111111111111111111111 로 표현되듯이
첫번째 bit 는 signed bit 로써 사용되며, signed bit 의 존재가 숫자 크기를 제한하게 됩니다.
ZigZag Encoding 을 통해서 모든 정수는 자연수로 변환되며,
낭비되는 signed bit 없앰으로써 모든 크기의 정수를 표현할 수 있게 됩니다.
<zigzag encoding>
def zigzag_encode(n): # Map positive and negative integers to non-negative integers if n >= 0: return n * 2 else: return -n * 2 - 1 def zigzag_decode(encoded): # Decode ZigZag-encoded value to the original signed integer if encoded % 2 == 0: return encoded // 2 else: return -((encoded + 1) // 2)
아래의 코드 예시는 int 타입의 데이터를 write 하는 코드입니다.
30 이라는 int 가 6개의 Record 로 생성되죠.
import fastavro schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "num1", "type": "int"}, ] } avro_file_path = 'users.avro' with open(avro_file_path, "wb") as new_file: fastavro.writer(new_file, schema, [ {"num1": 30}, {"num1": 30}, {"num1": 30}, {"num1": 30}, {"num1": 30}, {"num1": 30}, ], metadata={"owner": "westlife0615"}) with open(avro_file_path, 'rb') as avro_file: block_readers = fastavro.block_reader(avro_file) for block_reader in block_readers: print(f"num_records : {block_reader.num_records}") for record in block_reader: print(record)
생성된 users.avro 파일을 읽어보도록 하겠습니다.
30 을 저장하였는데, 30은 ZigZag Encoding 을 거치면 60이 됩니다.
그리고 60은 16진수로 표현하면 3c 가 됩니다.
실재로 3c 값이 연이어 6개가 존재함을 알 수 있습니다.
hexdump -C users.avro 00000000 4f 62 6a 01 06 0a 6f 77 6e 65 72 18 77 65 73 74 |Obj...owner.west| 00000010 6c 69 66 65 30 36 31 35 14 61 76 72 6f 2e 63 6f |life0615.avro.co| 00000020 64 65 63 08 6e 75 6c 6c 16 61 76 72 6f 2e 73 63 |dec.null.avro.sc| 00000030 68 65 6d 61 d8 01 7b 22 6e 61 6d 65 73 70 61 63 |hema..{"namespac| 00000040 65 22 3a 20 22 65 78 61 6d 70 6c 65 2e 61 76 72 |e": "example.avr| 00000050 6f 22 2c 20 22 74 79 70 65 22 3a 20 22 72 65 63 |o", "type": "rec| 00000060 6f 72 64 22 2c 20 22 6e 61 6d 65 22 3a 20 22 55 |ord", "name": "U| 00000070 73 65 72 22 2c 20 22 66 69 65 6c 64 73 22 3a 20 |ser", "fields": | 00000080 5b 7b 22 6e 61 6d 65 22 3a 20 22 6e 75 6d 31 22 |[{"name": "num1"| 00000090 2c 20 22 74 79 70 65 22 3a 20 22 69 6e 74 22 7d |, "type": "int"}| 000000a0 5d 7d 00 7c f6 64 8c 87 0d f6 29 e9 74 84 4e a1 |]}.|.d....).t.N.| 000000b0 5e ea 80 0c 0c 3c 3c 3c 3c 3c 3c 7c f6 64 8c 87 |^....<<<<<<|.d..| 000000c0 0d f6 29 e9 74 84 4e a1 5e ea 80 |..).t.N.^..|
double 은 어떻게 직렬화될까 ?
Double Type 은 부동소수점으로 표현되기 때문에
부호 비트, 지수, 가수 세 가지 데이터로 표현이 되는데요.
일반적인 프로그래밍 언어에서 사용하는 8 바이트 형식이 Avro 에서도 동일하게 적용됩니다.
예를 들어,
1.0 의 Double Type 은 16진수로 3ff0000000000000 로 표현됩니다.
import fastavro schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "num1", "type": "double"}, ] } avro_file_path = 'users.avro' with open(avro_file_path, "wb") as new_file: fastavro.writer(new_file, schema, [ {"num1": 1.0}, {"num1": 1.0}, {"num1": 1.0}, {"num1": 1.0}, {"num1": 1.0}, {"num1": 1.0}, ], metadata={"owner": "westlife0615"}) with open(avro_file_path, 'rb') as avro_file: block_readers = fastavro.block_reader(avro_file) for block_reader in block_readers: print(f"num_records : {block_reader.num_records}") for record in block_reader: print(record)
그리고 실제 avro 파일을 조회해보면
거꾸로긴 하지만 00 00 00 00 00 00 f0 3f 의 값이 존재합니다.
hexdump -C users.avro 00000000 4f 62 6a 01 06 0a 6f 77 6e 65 72 18 77 65 73 74 |Obj...owner.west| 00000010 6c 69 66 65 30 36 31 35 14 61 76 72 6f 2e 63 6f |life0615.avro.co| 00000020 64 65 63 08 6e 75 6c 6c 16 61 76 72 6f 2e 73 63 |dec.null.avro.sc| 00000030 68 65 6d 61 de 01 7b 22 6e 61 6d 65 73 70 61 63 |hema..{"namespac| 00000040 65 22 3a 20 22 65 78 61 6d 70 6c 65 2e 61 76 72 |e": "example.avr| 00000050 6f 22 2c 20 22 74 79 70 65 22 3a 20 22 72 65 63 |o", "type": "rec| 00000060 6f 72 64 22 2c 20 22 6e 61 6d 65 22 3a 20 22 55 |ord", "name": "U| 00000070 73 65 72 22 2c 20 22 66 69 65 6c 64 73 22 3a 20 |ser", "fields": | 00000080 5b 7b 22 6e 61 6d 65 22 3a 20 22 6e 75 6d 31 22 |[{"name": "num1"| 00000090 2c 20 22 74 79 70 65 22 3a 20 22 64 6f 75 62 6c |, "type": "doubl| 000000a0 65 22 7d 5d 7d 00 35 1f cf 78 bc e9 1a 19 d3 c1 |e"}]}.5..x......| 000000b0 01 8d 61 52 ad 1d 0c 60 00 00 00 00 00 00 f0 3f |..aR...`.......?| 000000c0 00 00 00 00 00 00 f0 3f 00 00 00 00 00 00 f0 3f |.......?.......?| * 000000e0 00 00 00 00 00 00 f0 3f 35 1f cf 78 bc e9 1a 19 |.......?5..x....| 000000f0 d3 c1 01 8d 61 52 ad 1d |....aR..| 000000f8
boolean 는 어떻게 직렬화될까 ?
Avro 는 Boolean 데이터 타입을 가집니다.
이는 단순히 True 와 False 가 1 과 0 으로 표현됩니다.
import fastavro schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "num1", "type": "boolean"}, ] } avro_file_path = 'users.avro' with open(avro_file_path, "wb") as new_file: fastavro.writer(new_file, schema, [ { "num1": False}, { "num1": True}, { "num1": False}, { "num1": True}, { "num1": False}, { "num1": True}, ], metadata={"owner": "westlife0615"}) with open(avro_file_path, 'rb') as avro_file: block_readers = fastavro.block_reader(avro_file) for block_reader in block_readers: print(f"num_records : {block_reader.num_records}") for record in block_reader: print(record)
000000b0 라인에서 00 과 01 이 연이어 등장하는 것을 확인할 수 있습니다.
hexdump -C users.avro 00000000 4f 62 6a 01 06 0a 6f 77 6e 65 72 18 77 65 73 74 |Obj...owner.west| 00000010 6c 69 66 65 30 36 31 35 14 61 76 72 6f 2e 63 6f |life0615.avro.co| 00000020 64 65 63 08 6e 75 6c 6c 16 61 76 72 6f 2e 73 63 |dec.null.avro.sc| 00000030 68 65 6d 61 e0 01 7b 22 6e 61 6d 65 73 70 61 63 |hema..{"namespac| 00000040 65 22 3a 20 22 65 78 61 6d 70 6c 65 2e 61 76 72 |e": "example.avr| 00000050 6f 22 2c 20 22 74 79 70 65 22 3a 20 22 72 65 63 |o", "type": "rec| 00000060 6f 72 64 22 2c 20 22 6e 61 6d 65 22 3a 20 22 55 |ord", "name": "U| 00000070 73 65 72 22 2c 20 22 66 69 65 6c 64 73 22 3a 20 |ser", "fields": | 00000080 5b 7b 22 6e 61 6d 65 22 3a 20 22 6e 75 6d 31 22 |[{"name": "num1"| 00000090 2c 20 22 74 79 70 65 22 3a 20 22 62 6f 6f 6c 65 |, "type": "boole| 000000a0 61 6e 22 7d 5d 7d 00 bd e2 02 13 0c be 19 01 13 |an"}]}..........| 000000b0 22 2d e3 74 e7 74 e4 0c 0c 00 01 00 01 00 01 bd |"-.t.t..........| 000000c0 e2 02 13 0c be 19 01 13 22 2d e3 74 e7 74 e4 |........"-.t.t.|
int 와 long 의 크기 제한.
주목할 점은 int 와 long 의 크기 제한입니다.
int 와 long 의 데이터는 저장하는 관점에서 크기의 제한이 없습니다.
값이 커질수록, 저장 공간 또한 비례하여 커지는 방식으로 저장됩니다.
하지만 Avro 는 모든 프로그래밍 언어와 제약없이 통신해야하므로
일반적인 크기 제한 방식을 따릅니다.
따라서 int 인 경우에는 4 bytes , long 인 경우에는 8 bytes 를 권장합니다.
특히, Avro 라이브러리를 통해서 Avro 데이터를 직렬화하는 경우에는 해당 크기 제한이 적용됩니다.
하지만 실질적인 파일의 관점에서볼 때, 어떠한 크기의 제약사항은 없습니다.
Data Types.
Primitive Type.
Avro 의 Data Type 은 아래와 같습니다.
일반적인 프로그래밍 언어에서 사용하는 Primitive Data Type 과 유사합니다.
null: no value boolean: a binary value int: 32-bit signed integer long: 64-bit signed integer float: single precision (32-bit) IEEE 754 floating-point number double: double precision (64-bit) IEEE 754 floating-point number bytes: sequence of 8-bit unsigned bytes string: unicode character sequence
실제 적용사례와 함께 Primitive Type 에 대해서 설명 이어나가도록 하겠습니다.
string, int, long, boolean, float, double 타입을 가지는 User 라는 Schema 를 생성하였습니다.
import fastavro schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "name", "type": "string"}, {"name": "age", "type": "int"}, {"name": "hair", "type": "long"}, {"name": "marriage", "type": "boolean"}, {"name": "leftEyeVision", "type": "float"}, {"name": "rightEyeVision", "type": "double"}, ] } avro_file_path = 'users.avro' with open(avro_file_path, "wb") as new_file: fastavro.writer(new_file, schema, [ { "name": "John", "age": 11, "hair": 1299559951923, "marriage": True, "leftEyeVision": 1.0, "rightEyeVision": 1.5 }, { "name": "Smith", "age": 40, "hair": -102955634, "marriage": False, "leftEyeVision": 0.02, "rightEyeVision": 0.35 }, ], metadata={"owner": "westlife0615"}) with open(avro_file_path, 'rb') as avro_file: block_readers = fastavro.block_reader(avro_file) for block_reader in block_readers: print(f"num_records : {block_reader.num_records}") for record in block_reader: print(record)
<실행 결과>
num_records : 2 {'name': 'John', 'age': 11, 'hair': 1299559951923, 'marriage': True, 'leftEyeVision': 1.0, 'rightEyeVision': 1.5} {'name': 'Smith', 'age': 40, 'hair': -102955634, 'marriage': False, 'leftEyeVision': 0.019999999552965164, 'rightEyeVision': 0.35}
Null Type.
Null Type 에 대해서 알아보겠습니다.
Null 은 다른 프로그래밍 언어에서 null 또는 None 으로 표현되는 비어있는 상태을 의미합니다.
Null Data Type 인 칼럼에 데이터 쓰기.
아래 예시는 Null 타입 칼럼을 가지는 스키마와 Null Data Type 에 다른 형식의 데이터를 쓰는 코드 예시입니다.
Null 타입의 칼럼에 string, int, boolean 등 다른 타입으로 write 를 시도하는 경우에는 Null 로 write 됩니다.
즉, string, int, boolean 등의 쓰기 시도는 모두 무시됩니다.
아래는 Null 타입에 String 데이터를 write 하는 예시입니다.
import fastavro schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "nullable", "type": "null"}, ] } avro_file_path = 'users.avro' with open(avro_file_path, "wb") as new_file: fastavro.writer(new_file, schema, [ { "nullable": "this is string" } for _ in range(0, 1000000) ], metadata={"owner": "westlife0615"}) with open(avro_file_path, 'rb') as avro_file: block_readers = fastavro.block_reader(avro_file) for block_reader in block_readers: block_reader.reader_schema = {"namespace": "example.avro", "type": "record", "name": "User", "fields": [ {"name": "nullable", "type": "null"}, ] } print(f"num_records : {block_reader.num_records}") for record in block_reader: print(record)
Avro 에서 Null 타입은 어떠한 메모리나 파일의 공간도 차지하지 않는 방식으로 직렬화됩니다.
1000000 번 Record 를 추가하였으나 공간의 증가는 하나도 없습니다.
그래서 데이터가 아무리 많이 추가되더라도 빈 상태가 유지됩니다.
hexdump -C users.avro 00000000 4f 62 6a 01 06 0a 6f 77 6e 65 72 18 77 65 73 74 |Obj...owner.west| 00000010 6c 69 66 65 30 36 31 35 14 61 76 72 6f 2e 63 6f |life0615.avro.co| 00000020 64 65 63 08 6e 75 6c 6c 16 61 76 72 6f 2e 73 63 |dec.null.avro.sc| 00000030 68 65 6d 61 e2 01 7b 22 6e 61 6d 65 73 70 61 63 |hema..{"namespac| 00000040 65 22 3a 20 22 65 78 61 6d 70 6c 65 2e 61 76 72 |e": "example.avr| 00000050 6f 22 2c 20 22 74 79 70 65 22 3a 20 22 72 65 63 |o", "type": "rec| 00000060 6f 72 64 22 2c 20 22 6e 61 6d 65 22 3a 20 22 55 |ord", "name": "U| 00000070 73 65 72 22 2c 20 22 66 69 65 6c 64 73 22 3a 20 |ser", "fields": | 00000080 5b 7b 22 6e 61 6d 65 22 3a 20 22 6e 75 6c 6c 61 |[{"name": "nulla| 00000090 62 6c 65 22 2c 20 22 74 79 70 65 22 3a 20 22 6e |ble", "type": "n| 000000a0 75 6c 6c 22 7d 5d 7d 00 a2 39 f1 98 78 17 2e f0 |ull"}]}..9..x...| 000000b0 8d a5 e6 41 be 56 9a 23 18 00 a2 39 f1 98 78 17 |...A.V.#...9..x.| 000000c0 2e f0 8d a5 e6 41 be 56 9a 23 |.....A.V.#|
반응형'BigData' 카테고리의 다른 글
Trino 도커로 따라하기 (0) 2023.12.03 Thrift 알아보기 (0) 2023.11.04 Avro File 알아보기 (0) 2023.10.04 RabbitMQ 에 대해서 (0) 2023.04.09 apache spark 란 (0) 2023.01.12