본문 바로가기

책/데이터 중심 어플리케이션 설계

저장소와 검색 (1)

반응형

꿀꿀

 

3장 저장소와 검색 

명언부터 시작한다 

항상 주변을 단정히 정돈하는 사람은 단지 찾기를 너무 귀찮아하는 사람이다 

 

나는 찾는걸 좋아해서 일부러 그런거야!

 

 

 

 

 

가장 기본적인 수준에서 데이터베이스는 두 가지 작업을 수행한다 

  1. 어떤 데이터를 받으면 데이터를 저장하고 
  2. 나중에 그 데이터를 요청하면 다시 데이터를 제공한다 

 

데이터베이스가 저장과 검색을 내부적으로 처리하는 방법을 application 개발자가 주의해야 하는 이유는 뭘까? 

구현하기보단 자기가 현재 처한 상황에 따라서 적합한 저장소 엔진을 선택하는 방법이 좋다

(항상 정답은 없죠 우리 세계에서는 적합하냐 만 존재할뿐..)

 

 

 

많은 데이터베이스들은 내부적으로 추가 전용 데이터 파일인 로그를 사용한다

데이터베이스에서 특정 키의 값을 효율적으로 찾기 위해서 다른 데이터 구조가 필요한데 바로 색인(index)이다 

 

색인(index)

색인은 primary data에서 파생된 추가적인 구조다 많은 DB는 index를 추가 제거 가 가능하고

내용에 영향은 안미치며 찾는데만 영향을 준다 

 

오늘 내가 선택한 DB는 Mysql(돌고래)로 선택하였다! 집중적으로 파보자.. 

뀨?

 

 

Hash Index 

key-value 데이터가 index 할 수 있는 유일한 종류의 데이터는 아니지만 일반적이고 복잡한 색인을 위한 구성 요소로 유용하다..

 

컬럼의 값으로 해시값을 계산해서 인덱싱하는 알고리즘으로 매우 빠르다 1:1 기준으로 O(1) 이다 엄청 빠른거다

대충 python으로도 작성이 가능한데 python dictionary는 기본적으로 hash table로 동작해서 다음과 같이 작성히 가능하다

class HashIndex:
    def __init__(self):
        self.index = {}

    def add(self, key, value):
        hashed_key = self.hash_function(key)
        if hashed_key in self.index:
            self.index[hashed_key].append(value)
        else:
            self.index[hashed_key] = [value]

    def get(self, key):
        hashed_key = self.hash_function(key)
        return self.index.get(hashed_key, [])

    def hash_function(self, key):
        return hash(key)

 

값 자체를 변형해서 인덱싱 하므로 전방 일치와 같이 값의 일부만 검색하거나 범위를 검색할 때는 인덱스를 사용할 수 없다 

즉, 1:1 관계에서만 가능하다는것이다 주로 메모리 기반 (대표적으로 redis)에서 많이 사용한다 

 

요약을 해보면 
1. 해시 테이블 자료구조로 관리 
2. 특정 컬럼에 대해 hash index를 걸면 해시값을 얻고 
3. 이 해시 값에 해당하는 위치에 컬럼 값 원본과 로우가 저장된 위치 정보 저장 

요로케

 

 

 

비크 캐스크는 해시 맵 전부 메모리에 유지하기 떄문에 사용 가능한 램에 모든 키가 저장된다는 조건을 전제로 

고성능 쓰기가 읽기를 보장한다고 한다 그렇다는건 이미 값이 있으면 IO 가 필요하지 않다 그냥 가지고 오면 되서 

 

비크 캐스크 같이 저장소엔진은 각 키의 값이 자주 갱신되는 상황에 매우 적합하다  적절한 예로서는 

1. messag queue

2. 로그 수집기 

3. 캐시 시스템 

 

앞서 파일에 계속 저장된다면 결국 디스크 공간이 부족해지는데 이 상황을 피하는 방법은?

기여엉..

 

 

특정 크기의 세그먼트로 로그를 나누는 방식이 좋은 해결책이라고 되어 있다 

특정 크기에 도달하면 세그먼트를 닫고 새로운 세그먼트파일에 이후 쓰기를진행한다 .. 그렇다 떠오르는게 있다.. 카프카.. ㅋ

 

카프카 에서 segment는 partition 안에 실질적으로 저장되는 물리 파일이며 특정 (기본 1GB으로 알고 있음 ) 오프셋이 넘으면 

다름 segment를 생성하며 계속 데이터를 저장한다 

 

또한 중복된 segment를 합병하여 최신 값만 하는 로그 컴팩션도 존재한다 로그 컴팩션은 사실상 저장 공간을 효율적으로 사용하는것으로 사용한다 로그 컴팩션을사용하면 여러 segment를 병합하게 될꺼고 그러면 극단적으로 줄일 수 있다 

 

같은 키를 가진 데이터가 1천만개가 있다고 가정해보면 
log compection 한 순간 얼마나 넓은 공간 창출이 될지 뻔히 보인다 
# 로그 컴팩션을 흉내내는 간단한 예제

# 메시지가 추가될 로그(리스트 형태)
log = [
    {"key": "A", "value": "First message for A"},
    {"key": "B", "value": "First message for B"},
    {"key": "A", "value": "Second message for A"},
    {"key": "C", "value": "First message for C"},
    {"key": "B", "value": "Second message for B"},
    {"key": "A", "value": "Third message for A"},
]

# 컴팩션 결과를 저장할 딕셔너리
compacted_log = {}

# 로그를 순회하면서 가장 최신 값으로 키를 업데이트
for entry in log:
    key = entry["key"]
    value = entry["value"]
    # 같은 키에 대해 항상 최신값으로 덮어씀
    compacted_log[key] = value

# 최종 컴팩션 결과 출력
print("Compacted log:")
for key, value in compacted_log.items():
    print(f"Key: {key}, Value: {value}")

고정된 세그먼트의 병합과 컴팩션은 백그라운드에서 수행할 수 있다 

 

1. Kafka와 같은 로그 기반 시스템에서는 고정된 세그먼트(예: 파티션 내에서 일정 크기 이상의 데이터)가 생성된 후,

더 이상 쓰기 작업이 발생하지 않으면 해당 세그먼트를 대상으로 컴팩션을 수행할 수 있고.

컴팩션 작업은 실시간 데이터 처리와는 별개로 백그라운드에서 수행되어, 키에 대한 최신 값만 유지하고 나머지 중복된 값은 삭제한다 

 

백그라운드에서 수행하여 Python에서는 Thread를 이용해서 간단한게 원리를 맛보기를 구현할 수 있다 (말 그대로 느낌 전체를 매도하는건 아니다)

 

이러한것들을 구현하기 위해서 많은걸 고려해야한다 

 

Kafka 에서 레코드 삭제는 다음과 같이 이루어 진다 

1. 툼스톤 레코드 : 레코드를 삭제할때 해당 키에 값이 null인 레코드를생성하는데 이를 툼스톤 이라고 부르고 키가 삭제되었다고 나타냄 

2. 로그 컴팩션 : 일정 시간 지나면 해당 키의 이전 값은 삭제되지만 툼스톤은 남아 있음 이는 보존기간동안 존재하고 그 이상이면 삭제됨  이 기간 동안 레코드는 키의 삭제 상태를 다른 컨슈머가 인식할 수 있도록 보존함 

 

 

 

해시테이블의 단점

1. 메모리에 저장해야하기 때문에 키가너무 많으면 문제 

2. 키가 너무 많으면 무작위 I/O가 많이필요함 확장성 문제 

3. 해시 충돌 해소를 위해 로직 필요 

 

위에도 말했는데 1:1에 강한거지 범위 질의에서는 효율적이지 않음 아니 사용을 못함 1:1 대응일때가 O(1) 인거지 

 

 

계속...

반응형