| JSON | protobuf | |
|---|---|---|
| 스키마 | 없어도 된다 | 반드시 먼저 적는다 |
| bytes 안의 필드 이름 | 매번 싣는다 | 번호만 싣는다 |
| 사람이 읽기 | 그대로 읽힌다 | 스키마가 있어야 읽힌다 |
| 이름을 바꾸면 | 받는 쪽이 깨진다 | 번호가 같으면 괜찮다 |
// .proto message Item { int32 id = 1; string name = 2; repeated int32 tags = 3; }
// JSON, 35 bytes
{"id":150,"name":"hi","tags":[1,2]}
| 필드 | key | 값 | bytes |
|---|---|---|---|
| id = 150 | 08 | 96 01 | 3 |
| name = "hi" | 12 | 02 68 69 | 4 |
| tags = [1, 2] | 1a | 02 01 02 | 4 |
| 합계 | 11 |
| 읽은 bytes | key 풀기 | 스키마에서 찾기 | 값 읽기 |
|---|---|---|---|
| 08 | 번호 1, wire type 0 | 1번은 id, int32 | varint 96 01 → 150 |
| 12 | 번호 2, wire type 2 | 2번은 name, string | 길이 02, 다음 2바이트 68 69 → "hi" |
| 1a | 번호 3, wire type 2 | 3번은 tags, repeated int32 | 길이 02, varint 둘 → [1, 2] |
| 60 | 번호 12, wire type 0 | 스키마에 없음 | wire type이 varint이니 varint 하나를 건너뛴다 |
// proto3 언어 명세의 생성 규칙 field = [ "repeated" | "optional" ] type fieldName "=" fieldNumber ";" type = "double" | ... | messageType repeated Ring rings = 1; // 됨 repeated repeated double c = 1; // 문법에 자리가 없다
| 형식 | 반복을 어디에 두나 | 배열의 배열 | 이름은 누가 짓나 |
|---|---|---|---|
| protobuf, BigQuery | 필드의 라벨(repeated, REPEATED mode) | 직접은 안 됨 | 사용자가 struct로 직접 |
| Parquet | repeated group에 LIST 주석을 달아 세 단으로 고정 | 됨 | 포맷이 list, element를 대신 지음 |
| Avro | array가 타입이고, items에 아무 스키마나 넣음 | 됨 | 이름이 필요 없음 |
| 낱말 | 여기서 뜻 |
|---|---|
| fleet | 한 회사가 운영하는 서버 전체. "fleet 사이클의 9.6%"는 전체 서버 CPU 시간의 9.6%다. |
| 가속기 | 특정 일 하나만 빠르게 하도록 만든 전용 회로. GPU가 행렬 곱 가속기인 것과 같은 뜻이다. |
| RTL | 회로를 코드로 적은 설계도. 실제 칩을 만들기 전 단계다. |
| RISC-V, BOOM | 공개된 CPU 명령어 체계와, 그걸 따르는 공개 CPU 설계. 성능은 스마트폰 코어(ARM A72) 수준이다. |
| RoCC | CPU가 옆에 붙은 가속기에게 전용 명령어로 일을 넘기는 연결 방식. |
| FireSim | FPGA로 설계도를 실제 속도에 가깝게 돌리는 시뮬레이터. 이 위에서 Linux를 띄웠다. |
| arena | 메모리를 큰 덩어리로 미리 받아 두고, 포인터만 밀면서 나눠 쓰는 할당 방식. |
| hasbits | 객체 안에서 "이 필드에 값이 있다"를 필드마다 1비트로 적어 두는 칸. |
| 어디서 읽나 | 한 번 읽는 데 |
|---|---|
| L1 | 몇 사이클 |
| L2 | 십여 사이클 |
| LLC | 수십 사이클 |
| DRAM | 수백 사이클 |
| PCIe 너머 장치 | 수천 사이클 (마이크로초) |
| 붙이는 자리 | 일을 넘기는 방법 | 데이터는 | 잘 맞는 일 |
|---|---|---|---|
| NIC 같은 칩 밖 장치 | 드라이버가 장치 큐에 명령을 쓰고, 끝나면 인터럽트로 알린다. 한 번 오가는 데 마이크로초 단위다. | DRAM에서 장치 메모리로 PCIe를 건너 복사해 간다. | 크고 독립적인 덩어리. 네트워크로 막 들어온 패킷. |
| 코어 바로 옆 (이 논문) | 코어가 실행하는 명령어 한 줄이 그대로 가속기로 간다(RoCC). 몇 사이클이다. | 복사하지 않는다. 코어와 같은 캐시와 메모리를 그 자리에서 읽는다. | 작고 잦은 일. 포인터를 따라가며 객체를 읽고 쓰는 일. |
| 사이클 중 RPC 스택에서 시작한 몫 | RPC | RPC 아님 |
|---|---|---|
| deserialize | 16.3% | 83.7% |
| serialize | 35.2% | 64.8% |
| 무엇으로 셌나 | 결과 | 읽는 법 |
|---|---|---|
| 필드 개수 | 56% 넘게 varint 계열 (int32, int64, enum, bool) | 작은 필드가 압도적으로 많다 |
| 메시지 bytes | 92% 넘게 string, bytes와 그 repeated 필드 | memcpy만 빠르면 될 것처럼 보인다 |
| 추정 deserialize 시간 | 1GB/s 넘게 처리되는 구간은 시간의 14%뿐 | 큰 bytes 필드는 작은 필드보다 바이트당 100~500배 빠르다 |
| 벤치마크 | 기준 RISC-V 대비 | Xeon 대비 |
|---|---|---|
| 마이크로벤치마크 (필드 종류별, 기하평균) | 11.2배 | 3.8배 |
| HyperProtoBench (서비스를 본뜬 합성 6종) | 6.2배 | 3.8배 |
| 가장 좋았던 경우 | 15.5배 | 6.9배 |
// OTLP 요청 하나를 행 여러 개로 request.resourceSpansList.forEach { rs -> rs.scopeSpansList.forEach { ss -> ss.spansList.forEach { span -> rows += toRow(span, rs.resource, ss.scope) } } } // 예: attribute 값(AnyValue oneof)은 스칼라만 받는다 when (valueCase) { STRING_VALUE, INT_VALUE, DOUBLE_VALUE, BOOL_VALUE -> 값 else -> null // array, kvlist, bytes는 버림 }
| protobuf를 쓰면 얻는 것 | 웹에서는 |
|---|---|
| bytes가 작다 | 필드 이름이 반복되는 JSON은 gzip이 잘 줄인다. 이벤트 하나가 몇 KB 수준이면 크기가 결정 근거가 되기 어렵다. |
| 스키마로 타입을 검증한다 | 측정값을 자유 형식 map으로 받는다면 검증 이득은 id, 시각 같은 공통 필드 몇 개에만 남는다. |
| 모바일 경로를 재사용한다 | 가장 의미 있는 이점이다. 대신 브라우저 번들에 인코더가 들어가고, 페이지가 열릴 때 인코딩 비용을 낸다. |
| CPU를 아낀다 | 논문의 이득은 fleet 규모 서버 이야기다. 초당 몇 건 수준의 이벤트에서는 서버 CPU가 쟁점이 아니다. |