데이터 보존 | Splunk Indexer Cluster에서 coldToFrozenScript로 S3 아카이빙 구성하기 (2/2)
본문 바로가기

Splunk/Splunk Project

데이터 보존 | Splunk Indexer Cluster에서 coldToFrozenScript로 S3 아카이빙 구성하기 (2/2)

728x90
반응형

1. 개요

Splunk 인덱서 클러스터 환경에서 일정 기간이 지난 cold bucket을 S3로 아카이빙하는 구성을 테스트했다.

이번 구성의 목적은 다음과 같다.

  • Splunk bucket이 frozen으로 전환될 때 S3로 자동 아카이빙
  • Indexer Cluster 환경에서 db_, rb_ bucket 중복 업로드 방지
  • S3 lock / done marker 기반 dedupe 처리
  • lock을 획득한 peer가 장애 나더라도 fallback 경로로 데이터 유실 방지
  • Splunk 실행 환경에서 AWS CLI가 정상 동작하도록 환경변수 충돌 제거

테스트 환경은 다음과 같다.

Splunk Enterprise Indexer Cluster
RF = 2
SF = 2
S3 Bucket = archive-splunk-data-123456789-ap-northeast-2
Archive Root Prefix = splunk-frozen
AWS Region = ap-northeast-2

2. Splunk bucket lifecycle 정리

Splunk 인덱스 데이터는 일반적으로 다음 흐름으로 이동한다.

hot → warm → cold → frozen

경로 기준으로 보면 보통 다음과 같다.

hot / warm : $SPLUNK_DB/<index>/db
cold       : $SPLUNK_DB/<index>/colddb
thawed     : $SPLUNK_DB/<index>/thaweddb

이번 테스트에서는 frozen 전환 시점에 coldToFrozenScript를 실행하여 cold bucket을 S3에 보관하도록 구성했다.

예상 bucket 경로는 다음과 같다.

/opt/splunk/var/lib/splunk/<index>/colddb/<bucket>

예시:

/opt/splunk/var/lib/splunk/s3_archive_test/colddb/db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A

3. S3 아카이빙 설계

Indexer Cluster 환경에서는 동일 logical bucket이 여러 peer에 복제될 수 있다.

예를 들어 다음 두 bucket은 같은 logical bucket의 원본/복제본일 수 있다.

db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A
rb_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A

여기서 rb_는 replicated bucket이다.
S3에 그대로 올리면 동일 데이터가 중복 저장될 수 있으므로, 스크립트에서는 rb_를 db_로 정규화하여 canonical bucket name을 만든다.

CANONICAL_BUCKET_NAME="${BUCKET_NAME/#rb_/db_}"

S3 저장 구조는 다음과 같이 설계했다.

s3://<bucket>/splunk-frozen/data/<index>/<canonical_bucket>/
s3://<bucket>/splunk-frozen/_locks/<index>/<canonical_bucket>.lock
s3://<bucket>/splunk-frozen/_manifests/<index>/<canonical_bucket>.done
s3://<bucket>/splunk-frozen/duplicates/<index>/<peer>/<canonical_bucket>/
s3://<bucket>/splunk-frozen/_fallback/<index>/<peer>/<canonical_bucket>.done

각 경로의 의미는 다음과 같다.

경로설명

data/ 실제 bucket 데이터 저장 위치
_locks/ 중복 업로드 방지를 위한 lock marker
_manifests/ 정상 아카이빙 완료 marker
duplicates/ lock peer 장애 시 fallback 업로드 위치
_fallback/ fallback 업로드 완료 marker

4. indexes.conf 테스트 설정

테스트 인덱스는 s3_archive_test로 구성했다.

[s3_archive_test]
homePath   = $SPLUNK_DB/s3_archive_test/db
coldPath   = $SPLUNK_DB/s3_archive_test/colddb
thawedPath = $SPLUNK_DB/s3_archive_test/thaweddb

maxDataSize = 1
maxHotBuckets = 1
maxHotIdleSecs = 60
maxWarmDBCount = 0

frozenTimePeriodInSecs = 1800

coldToFrozenScript = /bin/bash /opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh

테스트 목적은 bucket을 빠르게 cold로 내린 뒤 frozen 처리하는 것이었다.

중요한 설정은 다음과 같다.

maxWarmDBCount = 0

이 설정을 통해 warm bucket을 오래 유지하지 않고 cold path인 colddb로 빠르게 이동시키도록 했다.

다만 운영 인덱스에서는 위 설정을 그대로 사용하면 안 된다.
maxDataSize = 1, maxWarmDBCount = 0, 짧은 frozenTimePeriodInSecs는 작은 bucket을 많이 만들 수 있고 검색 성능이나 bucket 관리 측면에서 비효율적일 수 있다.


5. 디버깅 과정

5.1 첫 번째 오류: exit code 127

초기에는 다음과 같은 오류가 발생했다.

coldToFrozenScript cmd='"/bin/bash" ".../coldToFrozenS3.sh" ...'
exited with non-zero status='PID ... exited with code 127'

Linux에서 exit code 127은 보통 command not found 계열이다.

처음에는 AWS CLI 경로 문제를 의심했다.

확인한 항목은 다음과 같다.

ls -l /opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh
sudo -u splunk which aws
sudo -u splunk /usr/bin/aws --version

이후 AWS CLI 자체는 존재하는 것을 확인했다.


5.2 두 번째 오류: expected=colddb actual=db

다음으로 발생한 오류는 다음과 같았다.

ERROR: unexpected bucket parent directory. expected=colddb actual=db
path=/opt/splunk/var/lib/splunk/s3_archive_test/db/db_...

스크립트는 bucket이 반드시 colddb 아래에 있어야 한다고 검증하고 있었다.

[[ "$PARENT_DIR" == "colddb" ]] || fail "unexpected bucket parent directory..."

그런데 테스트 환경에서 frozenTimePeriodInSecs를 너무 짧게 잡으면 bucket이 colddb로 이동하기 전에 db 경로에서 바로 frozen 대상이 될 수 있다.

이를 해결하기 위해 테스트 인덱스의 lifecycle 설정을 조정했다.

maxDataSize = 1
maxHotBuckets = 1
maxHotIdleSecs = 60
maxWarmDBCount = 0
frozenTimePeriodInSecs = 1800

이후 s3_archive_test bucket이 colddb 경로에서 coldToFrozenScript를 실행하는 것을 확인했다.


5.3 세 번째 오류: DONE marker timeout

이후에는 다음 로그가 반복됐다.

waiting for DONE marker. attempt=38/60
waiting for DONE marker. attempt=39/60
...
waiting for DONE marker. attempt=60/60

이는 lock은 생성되었거나 lock이 있다고 판단했지만, 제한 시간 내에 done marker를 확인하지 못했다는 뜻이다.

원인 후보는 다음과 같았다.

1. lock 생성 peer가 업로드 중 실패
2. DONE marker 생성 실패
3. DONE marker를 다른 S3 prefix에 생성
4. S3 권한 문제
5. AWS CLI 실행 자체 실패

기존 스크립트는 AWS CLI 실패 원인을 대부분 /dev/null로 버리고 있었기 때문에 원인을 알기 어려웠다.

예를 들어 기존 함수는 다음과 같았다.

s3_head_object() {
  "$AWS_BIN" s3api head-object \
    --bucket "$S3_BUCKET" \
    --key "$key" >/dev/null 2>&1
}

이 방식은 404 Not Found, 403 AccessDenied, AWS CLI 실행 실패를 모두 동일하게 false로 처리한다.
따라서 디버깅 로그를 강화했다.


5.4 네 번째 오류: AWS CLI ImportError

디버깅 로그를 강화한 뒤 실제 원인이 드러났다.

ImportError: /usr/lib64/python3.9/lib-dynload/_sqlite3.cpython-39-x86_64-linux-gnu.so:
undefined symbol: sqlite3_enable_load_extension

즉, S3 권한 문제가 아니라 AWS CLI가 실행 과정에서 Python sqlite3 모듈을 로딩하다가 실패하고 있었다.

흥미로운 점은 root shell에서 splunk 계정으로 실행하면 AWS CLI가 정상이었다는 것이다.

sudo -u splunk /usr/bin/aws --version

결과:

aws-cli/2.27.57 Python/3.9.23 Linux/6.12.40-63.107.amzn2023.x86_64 source/x86_64.amzn.2023

하지만 Splunk 프로세스의 환경변수를 주입해서 실행하면 동일한 ImportError가 재현됐다.

pid=$(pgrep -xo splunkd)

sudo -u splunk env \
  "$(tr '\0' '\n' < /proc/$pid/environ | grep '^LD_LIBRARY_PATH=')" \
  /usr/bin/aws --version

결과:

ImportError: /usr/lib64/python3.9/lib-dynload/_sqlite3.cpython-39-x86_64-linux-gnu.so:
undefined symbol: sqlite3_enable_load_extension

결론적으로 원인은 다음과 같았다.

Splunkd의 LD_LIBRARY_PATH 때문에 /usr/bin/aws 실행 시 잘못된 sqlite 라이브러리를 참조
→ AWS CLI 내부 Python sqlite3 import 실패
→ s3 head-object / put-object / sync 실패
→ DONE marker 확인/생성 실패
→ timeout 발생

해결은 AWS CLI 실행 시 Splunk의 LD_LIBRARY_PATH, PYTHONPATH, PYTHONHOME 등을 제거하는 것이다.

aws_cmd() {
  env \
    -u LD_LIBRARY_PATH \
    -u PYTHONPATH \
    -u PYTHONHOME \
    -u PYTHONSTARTUP \
    "$AWS_BIN" "$@"
}

이후 모든 AWS CLI 호출을 "$AWS_BIN" 직접 호출이 아니라 aws_cmd를 통해 실행하도록 수정했다.


6. 최종 성공 로그

수정 후 다음과 같은 로그가 발생했다.

archive completed. done=s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/_manifests/s3_archive_test/db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A.done

그리고 done marker 생성도 정상 확인됐다.

s3 put-object created. key=s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/_manifests/s3_archive_test/db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A.done

즉, 다음 단계가 모두 정상 완료됐다.

1. colddb bucket 감지
2. coldToFrozenScript 실행
3. S3 data prefix로 bucket 업로드
4. S3 done marker 생성
5. archive completed 로그 출력

7. 완성 스크립트

아래는 최종 적용한 coldToFrozenS3.sh 예시다.

경로 예시:

/opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh
#!/bin/bash
set -Eeuo pipefail

###############################################################################
# Splunk coldToFrozen S3 Archive Script with Dedupe
#
# Environment:
#   - Splunk Indexer Cluster
#   - RF=2, SF=2
#
# Purpose:
#   - frozen 전환되는 cold bucket을 S3로 아카이빙
#   - db_ / rb_ 복제 bucket 중복 업로드 방지
#   - S3 lock / done marker 기반 dedupe
#   - lock 획득 peer 장애 시 fallback duplicate 경로로 데이터 유실 방지
#   - Splunk LD_LIBRARY_PATH 영향으로 AWS CLI가 깨지는 문제 방지
#
# Expected bucket path:
#   /opt/splunk/var/lib/splunk/<index>/colddb/<bucket>
#
# Bucket name examples:
#   db_1760000000_1759000000_123_GUID
#   rb_1760000000_1759000000_123_GUID
#
# Dedupe rule:
#   rb_... -> db_... 로 canonical bucket name 정규화
#
# S3 structure:
#   s3://<bucket>/splunk-frozen/data/<index>/<canonical_bucket>/
#   s3://<bucket>/splunk-frozen/_locks/<index>/<canonical_bucket>.lock
#   s3://<bucket>/splunk-frozen/_manifests/<index>/<canonical_bucket>.done
#   s3://<bucket>/splunk-frozen/duplicates/<index>/<peer>/<canonical_bucket>/
#   s3://<bucket>/splunk-frozen/_fallback/<index>/<peer>/<canonical_bucket>.done
###############################################################################

export AWS_DEFAULT_REGION="ap-northeast-2"

SPLUNK_HOME="${SPLUNK_HOME:-/opt/splunk}"

S3_BUCKET="worxphere-splunk-data-361809382823-ap-northeast-2"
ROOT_PREFIX="splunk-frozen"

AWS_BIN="${AWS_BIN:-/usr/bin/aws}"

if [[ ! -x "$AWS_BIN" ]]; then
  AWS_BIN="$(command -v aws || true)"
fi

if [[ -z "${AWS_BIN}" || ! -x "$AWS_BIN" ]]; then
  echo "ERROR: aws cli not found" >&2
  exit 1
fi

###############################################################################
# AWS CLI wrapper
#
# Splunkd가 가진 LD_LIBRARY_PATH 때문에 /usr/bin/aws 실행 시 Python sqlite3
# ImportError가 발생할 수 있다.
#
# 예:
#   ImportError: _sqlite3... undefined symbol: sqlite3_enable_load_extension
#
# 따라서 AWS CLI 실행 시 Splunk 관련 Python/library 환경변수를 제거한다.
###############################################################################

aws_cmd() {
  env \
    -u LD_LIBRARY_PATH \
    -u PYTHONPATH \
    -u PYTHONHOME \
    -u PYTHONSTARTUP \
    "$AWS_BIN" "$@"
}

BUCKET_PATH="${1:?Usage: $0 <bucket_path>}"

if command -v realpath >/dev/null 2>&1; then
  BUCKET_PATH="$(realpath "$BUCKET_PATH")"
fi

BUCKET_NAME="$(basename "$BUCKET_PATH")"

# RF=2/SF=2 indexer cluster dedupe 처리
# db_ = originating bucket
# rb_ = replicated bucket
# 같은 logical bucket copy이므로 S3 dedupe key는 db_ 기준으로 정규화
CANONICAL_BUCKET_NAME="${BUCKET_NAME/#rb_/db_}"

PARENT_DIR="$(basename "$(dirname "$BUCKET_PATH")")"
INDEX_NAME="$(basename "$(dirname "$(dirname "$BUCKET_PATH")")")"
PEER_NAME="$(hostname -s)"

LOGFILE="${SPLUNK_HOME}/var/log/splunk/coldtofrozen_s3.log"

DATA_PREFIX="${ROOT_PREFIX}/data/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}"
LOCK_KEY="${ROOT_PREFIX}/_locks/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}.lock"
DONE_KEY="${ROOT_PREFIX}/_manifests/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}.done"

FALLBACK_PREFIX="${ROOT_PREFIX}/duplicates/${INDEX_NAME}/${PEER_NAME}/${CANONICAL_BUCKET_NAME}"
FALLBACK_DONE_KEY="${ROOT_PREFIX}/_fallback/${INDEX_NAME}/${PEER_NAME}/${CANONICAL_BUCKET_NAME}.done"

TMP_META="$(mktemp /tmp/splunk_coldtofrozen_meta.XXXXXX)"

cleanup() {
  rm -f "$TMP_META"
}

trap cleanup EXIT

single_line() {
  tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | cut -c1-2000
}

log() {
  local msg="$*"
  local line

  line="$(date -Is) peer=${PEER_NAME} index=${INDEX_NAME} bucket=${BUCKET_NAME} canonical=${CANONICAL_BUCKET_NAME} ${msg}"

  printf '%s\n' "$line" >> "$LOGFILE" 2>/dev/null || true
  logger -t splunk-coldtofrozen-s3 "$line" || true
}

fail() {
  log "ERROR: $*"
  echo "ERROR: $*" >&2
  exit 1
}

trap 'rc=$?; log "ERROR: unexpected failure line=${LINENO} rc=${rc} cmd=[$BASH_COMMAND]"; exit "$rc"' ERR

bucket_size_bytes() {
  local size

  size="$(du -sb "$BUCKET_PATH" 2>/dev/null | awk '{print $1}' || true)"
  echo "${size:-0}"
}

bucket_file_count() {
  local cnt

  cnt="$(find "$BUCKET_PATH" -type f 2>/dev/null | wc -l | tr -d ' ' || true)"
  echo "${cnt:-0}"
}

write_meta() {
  local status="$1"
  local now_utc

  now_utc="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

  cat > "$TMP_META" <<EOF
{
  "status": "${status}",
  "index": "${INDEX_NAME}",
  "bucket": "${BUCKET_NAME}",
  "canonical_bucket": "${CANONICAL_BUCKET_NAME}",
  "bucket_path": "${BUCKET_PATH}",
  "peer": "${PEER_NAME}",
  "timestamp_utc": "${now_utc}",
  "size_bytes": "$(bucket_size_bytes)",
  "file_count": "$(bucket_file_count)"
}
EOF
}

###############################################################################
# AWS helper functions
###############################################################################

s3_object_exists() {
  local key="$1"
  local mode="${2:-quiet}"
  local output
  local rc

  if output="$(aws_cmd s3api head-object \
      --bucket "$S3_BUCKET" \
      --key "$key" \
      --query '{LastModified:LastModified,ContentLength:ContentLength,ETag:ETag}' \
      --output json 2>&1)"; then

    if [[ "$mode" != "quiet" ]]; then
      log "s3 head-object exists. key=s3://${S3_BUCKET}/${key} meta=$(printf '%s' "$output" | single_line)"
    fi

    return 0
  else
    rc=$?
  fi

  if printf '%s' "$output" | grep -qiE '\(404\)|Not Found|NoSuchKey|NotFound|404'; then
    if [[ "$mode" != "quiet" ]]; then
      log "s3 head-object not found. key=s3://${S3_BUCKET}/${key}"
    fi
    return 1
  fi

  fail "s3 head-object failed. key=s3://${S3_BUCKET}/${key} rc=${rc} error=$(printf '%s' "$output" | single_line)"
}

s3_put_object_if_absent() {
  local key="$1"
  local body="$2"
  local output
  local rc
  local attempt

  for attempt in 1 2 3; do
    if output="$(aws_cmd s3api put-object \
        --bucket "$S3_BUCKET" \
        --key "$key" \
        --body "$body" \
        --if-none-match "*" \
        --output json 2>&1)"; then

      log "s3 put-object created. key=s3://${S3_BUCKET}/${key} result=$(printf '%s' "$output" | single_line)"
      return 0
    else
      rc=$?
    fi

    if printf '%s' "$output" | grep -qiE 'PreconditionFailed|Precondition Failed|412'; then
      log "s3 put-object skipped because object already exists. key=s3://${S3_BUCKET}/${key} rc=${rc}"
      return 1
    fi

    if printf '%s' "$output" | grep -qiE 'ConditionalRequestConflict|409'; then
      log "WARN: s3 conditional write conflict. retrying. attempt=${attempt}/3 key=s3://${S3_BUCKET}/${key} rc=${rc} error=$(printf '%s' "$output" | single_line)"
      sleep "$attempt"
      continue
    fi

    fail "s3 put-object failed. key=s3://${S3_BUCKET}/${key} rc=${rc} error=$(printf '%s' "$output" | single_line)"
  done

  fail "s3 put-object failed after retries. key=s3://${S3_BUCKET}/${key}"
}

archive_to_s3() {
  local source_path="$1"
  local target_prefix="$2"
  local output
  local rc

  log "archive upload command started. source=${source_path}/ target=s3://${S3_BUCKET}/${target_prefix}/ size_bytes=$(bucket_size_bytes) file_count=$(bucket_file_count)"

  if output="$(aws_cmd s3 sync \
      "${source_path}/" \
      "s3://${S3_BUCKET}/${target_prefix}/" \
      --only-show-errors 2>&1)"; then

    log "archive upload command completed. target=s3://${S3_BUCKET}/${target_prefix}/"
    return 0
  else
    rc=$?
  fi

  fail "archive upload command failed. source=${source_path}/ target=s3://${S3_BUCKET}/${target_prefix}/ rc=${rc} error=$(printf '%s' "$output" | single_line)"
}

log_runtime_info() {
  local aws_version
  local caller

  aws_version="$(aws_cmd --version 2>&1 | single_line || true)"
  log "runtime aws_bin=${AWS_BIN} aws_version=${aws_version} region=${AWS_DEFAULT_REGION}"

  caller="$(aws_cmd sts get-caller-identity --query Arn --output text 2>&1 || true)"
  log "runtime aws_caller=$(printf '%s' "$caller" | single_line)"
}

###############################################################################
# Validation
###############################################################################

[[ -d "$BUCKET_PATH" ]] || fail "bucket path does not exist or is not directory: ${BUCKET_PATH}"

[[ "$PARENT_DIR" == "colddb" ]] || fail "unexpected bucket parent directory. expected=colddb actual=${PARENT_DIR} path=${BUCKET_PATH}"

[[ -n "$INDEX_NAME" ]] || fail "failed to parse index name from path: ${BUCKET_PATH}"

[[ -n "$BUCKET_NAME" ]] || fail "failed to parse bucket name from path: ${BUCKET_PATH}"

if [[ "$BUCKET_NAME" != db_* && "$BUCKET_NAME" != rb_* ]]; then
  fail "unexpected bucket name. expected db_* or rb_* actual=${BUCKET_NAME}"
fi

log "start coldToFrozen archive. path=${BUCKET_PATH} parent=${PARENT_DIR} data_prefix=s3://${S3_BUCKET}/${DATA_PREFIX}/ lock=s3://${S3_BUCKET}/${LOCK_KEY} done=s3://${S3_BUCKET}/${DONE_KEY}"

log_runtime_info

###############################################################################
# Step 1. 이미 아카이빙 완료된 logical bucket이면 skip
###############################################################################

if s3_object_exists "$DONE_KEY" verbose; then
  log "DONE marker already exists. skip archive. done=s3://${S3_BUCKET}/${DONE_KEY}"
  exit 0
fi

###############################################################################
# Step 2. S3 lock 생성 시도
#         lock 생성에 성공한 peer만 정상 data prefix에 업로드
###############################################################################

write_meta "LOCKED"

if s3_put_object_if_absent "$LOCK_KEY" "$TMP_META"; then
  log "lock acquired. lock=s3://${S3_BUCKET}/${LOCK_KEY}"

  # lock 획득 후에도 혹시 done marker가 생겼는지 재확인
  if s3_object_exists "$DONE_KEY" verbose; then
    log "DONE marker appeared after lock acquisition. skip archive."
    exit 0
  fi

  log "archive upload started. target=s3://${S3_BUCKET}/${DATA_PREFIX}/"

  archive_to_s3 "$BUCKET_PATH" "$DATA_PREFIX"

  write_meta "DONE"

  if s3_put_object_if_absent "$DONE_KEY" "$TMP_META"; then
    log "archive completed. done=s3://${S3_BUCKET}/${DONE_KEY}"
    exit 0
  else
    # 업로드는 완료됐지만 done marker 생성이 실패한 경우
    # 이미 다른 peer가 done을 만들었을 가능성이 있으므로 확인
    if s3_object_exists "$DONE_KEY" verbose; then
      log "archive upload completed, DONE marker already exists."
      exit 0
    fi

    fail "archive uploaded but failed to create DONE marker: s3://${S3_BUCKET}/${DONE_KEY}"
  fi

else
  log "lock already exists. another peer may be archiving this logical bucket. lock=s3://${S3_BUCKET}/${LOCK_KEY}"

  if s3_object_exists "$LOCK_KEY" verbose; then
    log "confirmed existing lock marker."
  else
    fail "lock put reported object exists, but lock marker is not readable or not found. lock=s3://${S3_BUCKET}/${LOCK_KEY}"
  fi

  #############################################################################
  # Step 3. 다른 peer가 업로드 중이면 DONE marker 대기
  #         최대 10분 대기: 60회 x 10초
  #############################################################################

  for i in $(seq 1 60); do
    sleep 10

    if s3_object_exists "$DONE_KEY"; then
      log "DONE marker found after waiting. another peer archived this bucket. skip. attempt=${i}/60"
      exit 0
    fi

    if (( i % 6 == 0 )); then
      log "waiting for DONE marker. attempt=${i}/60 done=s3://${S3_BUCKET}/${DONE_KEY}"
      s3_object_exists "$LOCK_KEY" verbose || fail "lock marker disappeared while waiting. lock=s3://${S3_BUCKET}/${LOCK_KEY}"
    else
      log "waiting for DONE marker. attempt=${i}/60"
    fi
  done

  #############################################################################
  # Step 4. lock은 있는데 DONE이 없는 비정상 상황
  #         첫 번째 peer가 업로드 중 장애났을 수 있음
  #         데이터 유실 방지를 위해 peer별 fallback duplicate 경로에 업로드
  #############################################################################

  if s3_object_exists "$DONE_KEY" verbose; then
    log "DONE marker found just before fallback. skip fallback."
    exit 0
  fi

  log "DONE marker not found after timeout. fallback duplicate archive started. target=s3://${S3_BUCKET}/${FALLBACK_PREFIX}/"

  archive_to_s3 "$BUCKET_PATH" "$FALLBACK_PREFIX"

  write_meta "FALLBACK_DONE"

  if s3_put_object_if_absent "$FALLBACK_DONE_KEY" "$TMP_META"; then
    log "fallback marker created. marker=s3://${S3_BUCKET}/${FALLBACK_DONE_KEY}"
  else
    log "fallback marker already exists. marker=s3://${S3_BUCKET}/${FALLBACK_DONE_KEY}"
  fi

  log "fallback duplicate archive completed. marker=s3://${S3_BUCKET}/${FALLBACK_DONE_KEY}"
  exit 0
fi

8. 배포 및 권한 설정

스크립트 파일을 배포한 뒤 실행 권한을 부여한다.

chmod 755 /opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh
chown splunk:splunk /opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh

Cluster Manager에서 app을 관리한다면 manager-apps에 반영 후 cluster bundle을 배포한다.

/opt/splunk/bin/splunk apply cluster-bundle

필요 시 rolling restart를 수행한다.

/opt/splunk/bin/splunk rolling-restart cluster-peers

9. 테스트 방법

9.1 AWS CLI 환경변수 충돌 재현

Splunkd의 LD_LIBRARY_PATH가 AWS CLI에 영향을 주는지 확인하려면 다음 명령을 사용한다.

pid=$(pgrep -xo splunkd)

sudo -u splunk env \
  "$(tr '\0' '\n' < /proc/$pid/environ | grep '^LD_LIBRARY_PATH=')" \
  /usr/bin/aws --version

만약 아래와 같은 오류가 발생하면 Splunk 환경변수 충돌이다.

ImportError: _sqlite3... undefined symbol: sqlite3_enable_load_extension

스크립트의 aws_cmd() 함수처럼 LD_LIBRARY_PATH를 제거하고 실행하면 정상이어야 한다.

pid=$(pgrep -xo splunkd)

sudo -u splunk env \
  "$(tr '\0' '\n' < /proc/$pid/environ | grep '^LD_LIBRARY_PATH=')" \
  bash -c 'env -u LD_LIBRARY_PATH -u PYTHONPATH -u PYTHONHOME -u PYTHONSTARTUP /usr/bin/aws --version'

9.2 수동 실행 테스트

이미 존재하는 cold bucket을 대상으로 수동 실행한다.

sudo -u splunk /bin/bash -x /opt/splunk/etc/peer-apps/<app_name>/bin/coldToFrozenS3.sh \
/opt/splunk/var/lib/splunk/s3_archive_test/colddb/db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A

로그 확인:

tail -100 /opt/splunk/var/log/splunk/coldtofrozen_s3.log

정상 로그 예시:

archive upload command completed. target=s3://...
s3 put-object created. key=s3://.../_manifests/...done
archive completed. done=s3://.../_manifests/...done

9.3 S3 데이터 확인

manifest 확인:

aws s3 ls s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/_manifests/s3_archive_test/ --recursive

data prefix 확인:

aws s3 ls s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/data/s3_archive_test/ --recursive --summarize

특정 bucket 확인:

aws s3 ls s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/data/s3_archive_test/db_1780886615_1780886615_0_F67B7772-2FAF-4B33-9E5F-0BC82892677A/ --recursive --summarize

10. Splunk 검색으로 로그 확인

스크립트 로그는 파일에도 남기고 syslog logger로도 남기도록 구성했다.

파일 로그:

tail -f /opt/splunk/var/log/splunk/coldtofrozen_s3.log

Splunk _internal 검색 예시:

index=_internal "splunk-coldtofrozen-s3"
| rex field=_raw "index=(?<archive_index>\S+) bucket=(?<bucket>\S+) canonical=(?<canonical>\S+)"
| table _time host archive_index bucket canonical _raw
| sort _time

오류만 확인:

index=_internal "splunk-coldtofrozen-s3" ("ERROR" OR "failed" OR "timeout" OR "fallback")
| rex field=_raw "index=(?<archive_index>\S+) bucket=(?<bucket>\S+) canonical=(?<canonical>\S+)"
| table _time host archive_index bucket canonical _raw
| sort _time

11. 운영 적용 시 주의사항

11.1 테스트용 lifecycle 설정을 운영에 그대로 쓰지 않기

아래 설정은 테스트용으로만 사용하는 것이 좋다.

maxDataSize = 1
maxWarmDBCount = 0
frozenTimePeriodInSecs = 1800

운영에서는 bucket 개수가 과도하게 많아지지 않도록 인덱스별 수집량과 보관정책에 맞게 조정해야 한다.


11.2 rb_ bucket 처리 정책 결정

현재 스크립트는 db_와 rb_ 중 먼저 lock을 획득한 peer가 대표로 S3 업로드를 수행한다.

즉, 로컬 bucket 이름이 rb_라도 S3에는 canonical 이름인 db_로 저장한다.

local bucket : rb_...
S3 key       : db_...

이 방식의 장점은 다음과 같다.

- db_ / rb_ 중복 업로드 방지
- 어떤 peer가 먼저 frozen 처리되든 하나만 대표 업로드
- done marker 기준으로 다른 peer는 skip 가능

다만 운영 정책상 반드시 원본 db_만 업로드하고 싶다면, rb_ bucket은 skip하도록 별도 로직을 넣어야 한다.

예시:

if [[ "$BUCKET_NAME" == rb_* ]]; then
  log "replicated bucket skipped by policy. path=${BUCKET_PATH}"
  exit 0
fi

단, 이 정책을 사용할 때는 원본 db_ bucket이 반드시 frozen 처리되어 S3에 올라간다는 보장이 있어야 한다.
그렇지 않으면 데이터 유실 가능성이 있다.


11.3 stale lock 관리

스크립트는 lock이 있는데 done marker가 생성되지 않는 경우 fallback duplicate 경로에 데이터를 업로드하도록 구성했다.

s3://<bucket>/splunk-frozen/duplicates/<index>/<peer>/<canonical_bucket>/

운영에서는 fallback이 자주 발생하는지 모니터링해야 한다.

확인 명령:

aws s3 ls s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/_fallback/ --recursive
aws s3 ls s3://archive-splunk-data-123456789-ap-northeast-2/splunk-frozen/duplicates/ --recursive --summarize

fallback이 발생했다면 다음을 확인한다.

1. lock 획득 peer가 중간에 장애났는지
2. aws s3 sync가 실패했는지
3. done marker 생성 권한이 있는지
4. S3 prefix가 잘못 조립되지 않았는지
5. peer 간 시간 차이나 네트워크 문제가 있었는지

12. 정리

이번 문제의 핵심은 단순한 S3 권한 문제가 아니었다.

최종 원인은 다음과 같았다.

Splunkd의 LD_LIBRARY_PATH가 AWS CLI 실행에 영향을 줌
→ AWS CLI 내부 Python sqlite3 import 실패
→ S3 head-object / put-object / sync 실패
→ done marker 확인 실패
→ coldToFrozenScript timeout 발생

해결 방법은 다음과 같았다.

1. cold bucket이 colddb로 먼저 이동하도록 indexes.conf 테스트 설정 조정
2. S3 lock / done marker 기반 dedupe 로직 구현
3. AWS CLI 실패 원인을 로그에 남기도록 스크립트 개선
4. AWS CLI 실행 시 LD_LIBRARY_PATH, PYTHONPATH, PYTHONHOME 제거
5. done marker 생성 확인 후 archive completed 로그로 성공 검증

최종적으로 아래 로그가 확인되면 성공으로 판단할 수 있다.

archive completed. done=s3://.../splunk-frozen/_manifests/<index>/<bucket>.done

이 로그는 bucket 데이터가 S3에 업로드되었고, 해당 logical bucket의 아카이빙 완료 marker가 생성되었다는 의미다.

728x90
반응형