optional label과 비용효과적인 Protobuf 처리

게시일 : 2024년 04월 24일    
# Protobuf # Python # Firehose

optional label과 비용효과적인 Protobuf 처리에 대해 알아본다.

이전 상황

센서 데이터를 protobuf로 형식으로 받아 Kinesis Firehose를 통해 데이터 레이크로 전송하고 있다.

proto에는 많은 필드의 데이터와 repeated, bytes 등 다양한 타입이 포함되어 있다.

protobuf의 json_format.MessageToDict 함수가 존재하지만 복잡한 형식을 처리하기 위해 stackoverflow에서 알게 된 코드를 활용했다.

MessageToDict function - in stackoverflow

def MessageToDict(message):
    message_dict = {}
    
    for descriptor in message.DESCRIPTOR.fields:
        key = descriptor.name
        value = getattr(message, descriptor.name)
    
    ...

이 함수는 모든 필드를 Dict 형식으로 변환해주지만 단점은 client 쪽에서 보내지 않는 필드 데이터 마저 default value로 변환한다.

예를 들어 특정 상황에서만 client에서 필드 값을 1로 보내는데 그 전에 수신자는 항상 0으로 변환한다.

하지만 모든 default value를 제외하는 것은 실제 데이터까지 제외되는 문제가 생길 수 있었다.

Application Note: Field Presence - protobuf

protobuf 3는 기본적으로 implicit presence가 적용되어 default value를 포함한 경우 메시지에 포함하지 않는다. 이로 인해

예를 들어, 수신자 입장에서는 온도 필드가 0인 경우 실제 0도인지 오류로 인해 생긴 0인지 알 수가 없다.

Firehose는 JSON 포맷을 input으로 받고 그 양에 따라 비용이 청구된다. 의미없는 default value와 필드 이름이 비용으로 연결되고 있었다.

protobuf 3.15부터 optional을 명시한 경우, explicit presence를 알 수 있다는 것을 알게 되었다.

ListFields() - protobuf 이 함수를 활용하면 client가 명시적으로 보내는 optional 필드 데이터를 확인할 수 있다.

필드 값 입력 여부는 HasField(field_name) - protobuf를 통해 확인할 수 있다.

optional 적용 테스트

proto 정의

sensor.proto

syntax = "proto3";

message sensorMsg {
  string sensorId = 1;
  int32 state = 2;
  float value = 3;
  string msg = 4;
}

optional_sensor.proto

syntax = "proto3";

message sensorMsg {
  optional string sensorId = 1;
  optional int32 state = 2;
  optional float value = 3;
  optional string msg = 4;
}

protobuf 생성

sensor

import sensor_pb2

sensor_msg = sensor_pb2.sensorMsg()

sensor_msg.sensorId = 'sensorA'
sensor_msg.value = 10
sensor_msg.state = 0

optional_sensor

import optional_sensor_pb2

optional_sensor_msg = optional_sensor_pb2.sensorMsg()

optional_sensor_msg.sensorId = 'sensorB'
optional_sensor_msg.value = 10
optional_sensor_msg.state = 0
optional_sensor_msg.msg = ''

field presence 확인

optional이 지정되지 않은 경우 필드의 존재를 확인할 수 없다.

sensor_msg.HasField('value')

=> ValueError: Field sensorMsg.value does not have presence.

optional_sensor_msg.HasField('value')

=> True

ListFields 확인

optional이 아닌 경우, default value를 입력했어도 수신자는 확인할 수 없다.

sensor_msg.ListFields()

[(<google._upb._message.FieldDescriptor at 0x10567d570>, 'sensorA'),
 (<google._upb._message.FieldDescriptor at 0x109d6d470>, 10.0)]
optional_sensor_msg.ListFields()

[(<google._upb._message.FieldDescriptor at 0x1085d5430>, 'sensorB'),
 (<google._upb._message.FieldDescriptor at 0x1085d5d30>, 0),
 (<google._upb._message.FieldDescriptor at 0x1085d6230>, 10.0),
 (<google._upb._message.FieldDescriptor at 0x1085d6bf0>, '')]

ListFields 활용

맨 처음 확인했던 MessageToDict를 ListFields를 활용하여 다음처럼 변경하여 사용가능하다.

def MessageToDict(message):
    message_dict = {}
    
    for desc, value in message.ListFields():
        key = desc.name
        # value는 그대로 사용
    ...

이 함수를 적용해서 client에서 default 값을 포함하여 명시적으로 보내는 경우만 변환이 가능하였다.

실제로 Firehose 비용이 약 20% 절감되는 효과가 있었다.

References :