vector memory growth issue in argo-workflow

k8s 환경의 특정 노드에서 동작중인 vector pod 1개가 메모리가 계속증가하고 있었다.
이유는 argo workflow 노드1개에만 스케쥴링되어서다.

[스케쥴링 원인]
argo-workflow(controller) 가 직접 스케줄링 하는 게 아니라 k8s 기본 스케줄러 (kube-scheduler) 가 워크플로우 task 파드를 노드에 배치한다.
workflow spec 에 nodeSelector/affinity 가 없어 다음 스케줄러의 기본 점수 두 개로 결정된다.
1. NodeResourcesFit — LeastAllocated (디폴트)
- 각 노드의 요청(request) 점유율이 가장 낮은 노드에 가장 높은 점수.
- 당시 CPU request worker01 34% / worker02 35% / worker03 25% <- 최저 / worker04 30% / worker05 30%
- 그래서 새로 들어오는 워크플로우 task 가 매번 worker03 으로 떨어짐.

2. ImageLocality
- 노드에 이미 캐시된 이미지를 쓰는 파드 에 추가 점수.
- 첫 워크플로우가 worker03 으로 떨어지면 그 노드에 이미지가 캐시 -> 다음 워크플로우도 worker03 이 더 높은 점수 -> 눈덩이 효과.

결과적으로 워크플로우 task 생성되면 -> kube-scheduler 점수 계산 -> worker03 선택 -> 이미지 더 강하게 캐시됨 -> 다음 task 도 같은 결과 -> 누적

[vector 로그 대상]
vector 의 kubernetes_logs source 는 pod들을 watch 한다.
pod 가 살아있으면 vector 는 그 파드를 수집 대상으로 계속 인식하고 다음을 메모리에 들고 있는다.
- 파드 메타데이터 캐시: namespace / pod_name / container_name / labels / annotations / node_name 등 100여 개 누적되면 무시 못할 양.
- file watcher state: 각 컨테이너 로그 파일의 inode, 마지막 read offset(checkpoint), open file descriptor.
- glob rescanning: config 에 glob_minimum_cooldown_ms: 8000 이라 8초마다 /var/log/pods/** 를 다시 스캔. 완료된 파드 디렉토리가 매번 결과에 잡혀 비교 작업이 일어남.

[조치1]
ns:argo-workflow > cm:workflow-controller-configmap > 아래 내용을 설정한다.
data:
  config: |
    workflowDefaults: # 각 워크플로가 별도의 설정이 없다면 사용될 디폴트 설정
       spec:
         podGC: # 워크플로우 task 파드 정리 전략1 - 상태 기반
           strategy: OnWorkflowSuccess # 워크플로우 전체가 성공으로 끝났을 때 task 파드들을 일괄 삭제. 실패하면 파드를 남겨 두어 사후 디버깅.
           deleteDelayDuration: 1m # strategy 가 발동한 뒤 1분 대기 후 실제 삭제. 로그 수집기(vector 등) 가 마지막 라인을 읽어 Kafka 까지 보낼 시간 확보.
         ttlStrategy: # 워크플로우 task 파드 정리 전략2 - 시간 기반
           secondsAfterSuccess: 3600 # 성공한 Workflow CR 을 1시간 뒤 삭제.
           secondsAfterFailure: 86400 # 실패한 Workflow CR 은 24시간 보존 후 삭제.

podGC + ttlStrategy 로 스케줄링 노드 편중을 막진 않지만 누적을 끊어 영향 자체를 무력화시킨다.
podGC , ttlStrategy 는 둘중 하나만 만족되면 정리가 되네 일정기간 유지하고 싶다면 podGC는 빼고 ttlstrategy 만 사용해야 한다.

argo workflows v2.4+ 부터 controller 가 ConfigMap 변경을 watch 해서 자동 reload 해서 별도 재시작이 필요 없다.
workflowDefaults(podGC/ttlStrategy)는 신규 워크플로우 부터만 적용된다.

[조치2]
이미 진행 중이거나 완료된 워크플로우에는 적용 안되니 작업이 끝난 workflow pod들을 종료한다.
kubectl -n argo-workflow delete pod --field-selector=status.phase=Succeeded
kubectl -n argo-workflow delete pod --field-selector=status.phase=Failed

[조치3]
vector 는 rust 로 작성되어 있고 jemalloc 을 기본 할당자로 사용한다.
jemalloc 은 한번 so 로 부터 받은 메모리를 내부 free pool 로 두고 잘 OS 로 돌려주지 않는다.
즉 vector 가 내부적으로 이제 안 쓰는 메모리가 됐어도 프로세스 RSS / working set 은 그대로 보인다.
새로 들어오는 워크로드는 이 free pool 을 재활용하므로 메모리는 더 늘지 않지만, 줄어들지도 않습니다.
이런 이유로 메모리가 증가된 vector pod1개는 새로띄운다.(vector daemonset 이라 pod 삭제하면 해당노드새 새 vector pod 생성된다.)
kubectl -n vector delete pod vector-abc123

사실 개별 workflow 는 ttl 설정이 되어 있어 자동 정리(삭제)가 되는데, cronworkflow 부분이 빠져 있어 문제가 됐다.

Usage of Claude Code and Codex

claude-code를 사용할때는 하루에 입력토큰이 많아야 2M 로 limit 에 걸릴일이 없었는데,
codex 를 사용하고 부턴 질문 몇개에 금방 5h limit 에 걸려 보니 input token 이 2M가 금방찬다.

https://github.com/ryoppippi/ccusage 는 claude-code/codex 의 사용량 현황을 알려준다.
참고로 사용자 .jsonl 의 토큰 및 캐시 사용 기록 파악한다.
~/.claude/projects/<project>/*.jsonl
~/.codex/sessions/<year>/<month>/<day>/*.jsonl

# claude-code usage
bunx ccusage
bunx ccusage weekly
bunx ccusage monthly

# codex usage
#bunx @ccusage/codex@latest -> deprecatd
bunx ccusage@20/codex@latest
bunx ccusage@20/codex@latest weekly
bunx ccusage@20/codex@latest monthly

다음과 같이 둘을 비교해보니 한달치 input token 이 claude-code 가 훨씬 적다.

Claude Code: Input 적고 > Cache Read로 대부분 처리 > 저렴
Codex: Input이 크고 > Cache Read 비율이 낮음 > 비쌈

codex reasoning(내부 추론) 과정은 캐시 키를 매번 바꿔서 캐시를 무효화시키는 경향이 있다고 한다.
fast 모드도 비용에 영향을 준다고 함.

다음 설정을 해서 줄여볼 수 있다고 하는데 여전히 input token 이 팍팍 증가한다.
# ~/.codex/config.toml
model_reasoning_effort = "low"
model_reasoning_summary = "concise"
model_verbosity = "low"
[features]
fast_mode = false # /fast 자체가 숨김 처리된다.

automated version pull request

js package.json 이나 rust cargo.toml 등에 소스파일에 version 을 명시하는 언어가 있다.
참고로 golang 소스파일에 버전을 명시하지 않는것이 관례라고 한다.
기능 개발을 하고 버전 태깅전에 별도로 소스 버전(package.json 등)수정하기 위해 브랜치 체크아웃 > commit > push > pr > merge 과정을 거쳐야하는데 불편하다.

[해결방법]
메인 브랜치에 커밋이 쌓이면 도구가 자동으로 release pr 을 생성한다.
(주의) 커밋 메시지는 conventional-commits(https://www.conventionalcommits.org)이 아니면 파싱이 실패하고 release pr 이 생성이 안된다.

1. git tag에서 최신 버전 확인 (예: v1.2.3)
2. 그 tag 이후의 conventional-commits를 분석해 버전 자동 결정
- fix: v1.2.4 (patch)
- feat: v1.3.0 (minor)
- feat!: / BREAKING CHANGE v2.0.0 (major)
3. release pr 생성(package.json 버전 업데이트 + CHANGELOG.md 작성 포함)

.github/workflows/release-please.yml 에 다음과 같이 사용
on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: googleapis/release-please-action@v4
        with:
          # 프로젝트의 언어 타입을 지정 (node, rust, go, python 등)
          release-type: node

github action 을 사용할 수 없는 환경이라면 다음과 같이 cli 로 실행하면 된다.
# bunx: npm 레지스트리에서 패키지를 임시 다운로드 후 바로 실행하는 기능입니다.(npx와 동일)
# --release-type : 프로젝트의 언어 타입을 지정 (node, rust, go, python 등)
# --api-url : 기업 github 사용시 별도 설정 필요
# --graphql-url : 기업 github 사용시 별도 설정 필요
bunx release-please release-pr \
    --repo-url=ysoftman/aaa \
    --target-branch=main \
    --release-type=node \
    --token="$GITHUB_TOKEN"

[참고]
이건 rust 전용으로 머지된 커밋을 확인하여 어떤 crate가 변경되었는지 분석해서 cargo.toml의 버전을 업데이트하고 의존성 버전을 맞춘 pr 을 생성한다.
머지 시 crates.io 배포까지 할 수 있다.

이건 개발자가 태그 생성하면 자동으로 빌드 및 파일 업로드해준다.