Rocks DB 스토리지 구현 5)

Tasker_Jang·2024년 9월 21일
0

Rust 프로젝트에서 Raft 관련 코드 실행 중 Result::unwrap() 호출 시 Err로 인해 패닉이 발생했습니다. 오류는 아래와 같은 메시지를 나타냅니다.

백트레이스(Backtrace)는 프로그램이 패닉이나 예외 상황에서 중단되었을 때, 그 중단 시점까지의 함수 호출 경로를 추적해 보여주는 디버깅 정보입니다. 이를 통해 어디에서 문제가 발생했는지, 그리고 그 문제에 이르기까지 어떤 함수들이 호출되었는지 알 수 있습니다. Rust에서는 패닉이 발생할 때 자동으로 백트레이스를 생성해주며, 이를 이용해 코드를 디버깅할 수 있습니다.

1. 백트레이스 활성화 방법

Rust에서 백트레이스를 활성화하려면 환경 변수 RUST_BACKTRACE를 설정한 후 실행해야 합니다.

  1. 짧은 백트레이스 출력 (간단한 호출 스택 정보만 표시):

    RUST_BACKTRACE=1 ./target/debug/memstore-static-members.exe --raft-addr=127.0.0.1:60061 --web-server=127.0.0.1:8001
  2. 전체 백트레이스 출력 (호출된 모든 함수와 각 소스 코드 라인까지 표시):

    RUST_BACKTRACE=full ./target/debug/memstore-static-members.exe --raft-addr=127.0.0.1:60061 --web-server=127.0.0.1:8001

2. 백트레이스를 활용한 디버깅

백트레이스는 프로그램의 충돌 원인을 찾는 데 유용합니다. 호출 경로를 따라가며 어떤 함수에서 문제가 발생했는지, 구체적인 에러가 어디에서 유발되었는지를 알 수 있습니다. 백트레이스 출력이 활성화되면, 패닉이 발생한 지점뿐 아니라 그 문제를 발생시키는 함수 호출 체인을 확인할 수 있습니다.

백트레이스를 분석하여, 어디에서 unwrap()이 호출되어 에러가 발생했는지, 그리고 그 전의 함수 호출 흐름을 파악하면 문제를 디버깅하는 데 큰 도움이 됩니다.

Windows 환경에서 RUST_BACKTRACE=1 환경 변수를 설정하는 방식이 Linux나 macOS와는 조금 다릅니다. PowerShell 또는 명령 프롬프트에서 환경 변수를 설정하고 프로그램을 실행하려면 다음과 같은 방식으로 해야 합니다.

3. PowerShell에서 환경 변수 설정 및 실행

PowerShell을 사용하는 경우에는 환경 변수를 다음과 같이 설정할 수 있습니다:

$env:RUST_BACKTRACE = 1
./target/debug/memstore-static-members.exe --raft-addr=127.0.0.1:60061 --web-server=127.0.0.1:8001

이 명령은 PowerShell에서 일시적으로 RUST_BACKTRACE 변수를 설정하고, 그 뒤에 프로그램을 실행합니다.

4. 명령 프롬프트(cmd)에서 환경 변수 설정 및 실행

명령 프롬프트에서는 아래와 같은 방식으로 환경 변수를 설정할 수 있습니다:

set RUST_BACKTRACE=1
./target/debug/memstore-static-members.exe --raft-addr=127.0.0.1:60061 --web-server=127.0.0.1:8001

이 방법으로도 동일하게 백트레이스를 활성화한 상태에서 프로그램을 실행할 수 있습니다.

이 중 하나를 선택하여 시도해 보시면 백트레이스 정보와 함께 프로그램의 충돌 원인을 확인할 수 있습니다.

5. 에러 분석

첫 번째 에러: 로그 항목 저장 실패

  • 에러 메시지: Error reading key from DB
  • 원인: RocksDB에서 키를 읽는 중 오류가 발생한 것으로 보입니다. 이는 로그 인덱스가 제대로 처리되지 않거나, 키-값 쌍이 손상되었을 가능성을 시사합니다.
  • 분석 방법: 이 에러는 로그가 제대로 저장되지 않거나, 인덱스가 연속적이지 않아서 발생할 수 있습니다. RocksDB에서 키를 읽는 동안 발생한 에러 메시지로, 데이터를 제대로 쓰지 못했거나 키 생성에 문제가 있을 수 있습니다.
  • 해결 방법: debug_rocksdb_keys 함수를 통해 RocksDB에 저장된 키를 출력하여 잘못된 키가 있는지 확인해야 합니다. 또한, 로그 인덱스 생성 로직을 다시 검토해 인덱스가 연속적으로 생성되고 있는지 확인해야 합니다.

두 번째 에러: 키 디코딩 오류

  • 에러 메시지: Error decoding entry for key
  • 원인: RocksDB에 저장된 값이 올바르게 디코딩되지 않았습니다. 이는 데이터를 저장하는 과정에서 문제가 생겨 손상된 값이 저장되었을 가능성이 큽니다.
  • 분석 방법: 디코딩 과정에서 prost::Message를 사용해 엔트리를 디코딩하는데, 잘못된 포맷으로 저장되었거나, 디코딩을 시도한 값이 유효하지 않을 때 발생할 수 있습니다. 주로 데이터 타입 간의 불일치나, 인덱스 또는 포맷 오류로 인해 발생합니다.
  • 해결 방법: 디버깅 출력을 통해 어떤 키에서 문제가 발생했는지 확인한 후, 그 키에 대응하는 값이 올바르게 인코딩되었는지 확인해야 합니다. 또한, 인코딩 및 디코딩 프로세스에서 데이터 타입의 일관성을 유지하고 있는지 다시 검토해야 합니다.

세 번째 에러: 스냅샷 처리 중 오류

  • 에러 메시지: Error fetching snapshot
  • 원인: 스냅샷을 가져오는 과정에서 문제가 발생했습니다. 이는 스냅샷 데이터가 존재하지 않거나, 잘못된 형식으로 저장된 경우에 발생할 수 있습니다.
  • 분석 방법: 스냅샷이 RocksDB에 제대로 저장되었는지 확인하고, 스냅샷을 가져오는 메서드에서 해당 키가 존재하는지 여부를 확인해야 합니다. RocksDB의 스냅샷 처리 메커니즘이 제대로 작동하고 있는지 검토해야 합니다.
  • 해결 방법: 스냅샷 조회 시 발생한 오류 메시지를 통해 스냅샷이 존재하지 않거나 잘못된 데이터를 읽으려는 경우 기본 스냅샷을 반환하는 로직을 추가해야 합니다. 또한, 스냅샷을 저장할 때 제대로 저장되었는지 확인하는 과정도 필요합니다.

6. 해결방법

1) RocksDB의 내부 키 및 값 디버깅

RocksDB는 키-값 저장소로 작동하며, Raft의 로그 항목을 저장하는 데 사용됩니다. 하지만 특정 로그 항목이 제대로 저장되지 않거나, 데이터베이스 내부 상태가 의심스러울 때, RocksDB 내부의 키와 값을 출력하여 현재 상태를 파악할 수 있습니다. 이를 위해 debug_rocksdb_keys라는 디버깅 함수를 추가하여 RocksDB에 저장된 모든 키와 값을 출력하도록 하였습니다.

해결 방법

pub fn debug_rocksdb_keys(&self) {
    let iter = self.db.iterator(rocksdb::IteratorMode::Start);
    for item in iter {
        match item {
            Ok((key, value)) => {
                println!("Key: {:?}, Value: {:?}", key, value);
            }
            Err(e) => {
                println!("Error reading key from DB: {:?}", e);
            }
        }
    }
}
  • 사용 시점: 로그가 제대로 저장되지 않거나, 특정 키를 조회할 때 오류가 발생할 경우 이 함수를 호출하여 RocksDB의 현재 상태를 출력할 수 있습니다.
  • 효과: 현재 데이터베이스에 저장된 로그 항목과 메타데이터 키를 직접 확인할 수 있어 문제를 추적하는 데 큰 도움이 됩니다.

2) 엔트리 조회 시 추가적인 디버깅 정보 출력

Raft에서 로그 항목을 RocksDB로부터 조회하는 과정에서 디버깅을 위해 추가적인 println! 출력문을 사용하여 로그 항목의 상태를 실시간으로 확인할 수 있습니다. 이를 통해 어떤 단계에서 문제가 발생하는지 확인할 수 있습니다.

해결 방법

fn entries(
    &self,
    low: u64,
    high: u64,
    max_size: impl Into<Option<u64>>,
    context: GetEntriesContext,
) -> crate::raft::Result<Vec<Entry>> {
    let mut entries = Vec::new();
    for i in low..high {
        let key = format!("{:020}", i);
        match self.db.get(key).unwrap() {
            Some(value) => {
                println!("Entry found for index {}: {:?}", i, value);
                let entry = Entry::decode(&*value).unwrap();
                entries.push(entry);
            }
            None => continue,
        }
    }

    println!("Total entries retrieved: {:?}", entries.len());
    Ok(entries)
}
  • 사용 시점: 엔트리 조회 시 로그가 예상보다 적거나 로그 항목이 정상적으로 조회되지 않는 경우, 각 인덱스에 해당하는 키가 RocksDB에서 어떻게 처리되고 있는지 실시간으로 확인할 수 있습니다.
  • 효과: 어떤 인덱스에서 문제가 발생하는지 구체적으로 확인 가능하며, 데이터베이스와의 상호작용을 보다 명확히 파악할 수 있습니다.

3) 메타데이터 필터링을 통한 로그 인덱스 처리 개선

first_indexlast_index 함수는 RocksDB에서 첫 번째와 마지막 로그 항목을 조회하는 데 사용됩니다. 하지만 RocksDB에는 로그 인덱스 외에도 다양한 메타데이터(예: conf_state, hard_state, snapshot)가 저장되기 때문에, 이 메타데이터를 로그 인덱스와 혼동하지 않도록 필터링이 필요합니다.

해결 방법

fn first_index(&self) -> crate::raft::Result<u64> {
    let mut iter = self.db.iterator(rocksdb::IteratorMode::Start);
    for item in iter {
        match item {
            Ok((key, _)) => {
                if key.starts_with(b"last_") || key.starts_with(b"conf_") || key.starts_with(b"hard_") || key.starts_with(b"snapshot") {
                    continue;
                }
                if key.len() == 8 {
                    let idx = u64::from_be_bytes(key[..8].try_into().unwrap());
                    return Ok(idx);
                }
            }
            Err(_) => return Err(crate::raft::Error::Store(crate::raft::StorageError::Unavailable)),
        }
    }
    Ok(1) // No valid log entries found
}
  • 사용 시점: RocksDB에서 첫 번째 혹은 마지막 인덱스를 조회할 때, 메타데이터 키를 제외하고 실제 로그 인덱스만 처리할 필요가 있을 때 사용합니다.
  • 효과: 메타데이터와 로그 인덱스가 혼동되지 않으며, 정확한 첫 번째와 마지막 로그 인덱스를 가져올 수 있습니다.

4) 스냅샷 조회 시 오류 처리 강화

스냅샷은 Raft에서 중요한 역할을 하며, 특정 시점의 상태를 저장하고 복구하는 데 사용됩니다. 하지만 스냅샷 조회 과정에서 스냅샷이 없거나, 읽기 오류가 발생하는 경우가 있을 수 있습니다. 이를 해결하기 위해 스냅샷 조회 시 오류 메시지를 추가하여 문제를 명확히 파악할 수 있도록 개선했습니다.

해결 방법

fn snapshot(&self, request_index: u64, to: u64) -> crate::raft::Result<Snapshot> {
    match self.db.get("snapshot") {
        Ok(Some(value)) => {
            let snapshot = Snapshot::decode(&*value).unwrap();
            Ok(snapshot)
        }
        Ok(None) => {
            println!("Warning: No snapshot found. Returning default snapshot for initial state.");
            Ok(Snapshot::default())
        }
        Err(e) => {
            println!("Error fetching snapshot: {:?}", e);
            Err(crate::raft::Error::Store(crate::raft::StorageError::SnapshotTemporarilyUnavailable))
        }
    }
}
  • 사용 시점: 스냅샷이 조회되지 않거나, 스냅샷 조회 과정에서 오류가 발생할 때 유용합니다.
  • 효과: 스냅샷이 없는 경우 기본값을 반환하면서 경고 메시지를 출력하고, 오류 발생 시 명확한 오류 메시지를 출력하여 문제 해결을 돕습니다.

7. 결론

RocksDB와 Raft를 연동하는 과정에서 발생할 수 있는 다양한 문제를 해결하기 위해, 디버깅용 출력 추가, 메타데이터 필터링, 그리고 오류 메시지 출력 강화와 같은 개선을 통해 문제를 보다 명확히 파악하고 해결할 수 있었습니다. 이러한 접근법들은 데이터 저장과 관리에서 발생하는 불확실성을 줄이고, 시스템의 안정성을 높이는 데 중요한 역할을 합니다.

Raft와 같은 분산 시스템 알고리즘을 RocksDB와 결합하여 사용하려면, 데이터베이스 상태를 명확하게 확인할 수 있는 디버깅 기능과 오류 처리가 매우 중요합니다. 이를 통해 복잡한 시스템에서도 신뢰성을 확보할 수 있습니다.

profile
터널을 지나고 있을 뿐, 길은 여전히 열려 있다.

0개의 댓글