끝까지 가져갈 만한 인프라 프로젝트는 처음부터 architecture로 시작하지 않는 경우가 많다. 처음에는 대개 불편함에 가깝다. 이 프로젝트도 그랬다. Kubernetes가 반응하는 모습은 볼 수 있었지만, 그 반응이 왜 그 timing에 일어나는지, 문제가 눈에 띄기 전에 어떤 신호가 있었는지는 충분히 설명하기 어려웠다.
당시 일하던 회사에서 나는 EKS, Kubernetes trace, GitHub change, kubectl output, HPA behavior, Karpenter behavior를 많이 다뤘다. 멀리서 보면 cluster는 조용해 보일 때가 많았지만, 실제 운영의 흐름은 그렇지 않았다. workload가 CPU를 밀어 올리고, HPA는 내가 원하는 것보다 늦게 반응하고, Karpenter는 자기 cadence로 capacity를 추가하고, pod는 Pending 상태로 남아 있고, 증거는 dashboard와 terminal output, event history 조각들 사이에 흩어져 있었다.
이 프로젝트의 첫 모양은 그 불편함을 측정 가능한 시스템으로 바꾸려는 시도였다. trace가 들어오고, failure 또는 demand risk가 나오고, agent들이 action을 제안하고, referee가 하나를 고르고, Kubernetes가 반응하고, dashboard가 그 과정을 불편한 부분까지 숨기지 않고 보여주는 구조를 만들고 싶었다.
아이디어의 첫 모양
프로젝트의 가장 초기 버전은 나중에 된 모습보다 훨씬 작았다. 나는 failure forecaster를 만들고 싶었다. resource usage와 terminal event information이 주어졌을 때, 미래 window 안에서 실패할 가능성이 높은 task에 score를 줄 수 있을까? 이 질문은 충분히 구체적일 만큼 기술적이면서도, Kubernetes operation과 맞닿아 있어서 의미가 있었다.
- Borg trace는 시간에 따른 task와 machine behavior를 제공한다.
- Feature window는 CPU, memory, request, priority, scheduling class, recent delta를 요약한다.
- Target label은 고정된 horizon 안의 terminal failure를 표시한다.
- Baseline model은 가장 먼저 확인할 수 있는 risk score를 드러낸다.
- 나중에는 그 risk score가 notebook metric에 머무르지 않고 orchestrator의 input이 될 수 있다.
내가 과소평가했던 부분은 later라는 단어였다. model 하나는 너무 쉽게 과대평가된다. classifier를 학습시키고 AUCPR을 출력하는 데서 끝냈다면, 프로젝트는 정적인 ML 실험으로 멈췄을 것이다. 작업을 계속할수록 model output에서 control decision으로 이어지는 경로가 더 중요해졌다. 그래서 프로젝트는 결국 dashboard 비중이 커졌다. dashboard는 장식이 아니었다. 시스템이 실제로 무엇을 하고 있는지 나 자신에게 숨기지 않기 위한 유일한 방법이었다.
모델링 전 directory layout
처음부터 큰 data는 repository 밖에 두었다. Borg data는 git에 넣기에는 너무 크고 너무 기계적인 데이터였고, generated parquet file이 source tree에 섞이는 것도 원하지 않았다. repo는 code와 documentation layer가 되었다. 외부 directory는 raw, processed, model, report layer가 되었다.
mkdir -p ~/Documents/borg_data
mkdir -p ~/Documents/borg_processed
mkdir -p ~/Documents/borg_xgboost_workspace/{raw,processed,models,reports,runtime,config}
export BORG_DATA_DIR="$HOME/Documents/borg_data"
export BORG_PROCESSED_DIR="$HOME/Documents/borg_processed"
export BORG_XGBOOST_WORKSPACE="$HOME/Documents/borg_xgboost_workspace"
작은 결정이었지만 나중에 큰 도움이 됐다. baseline forecaster data, advanced XGBoost feature, orchestrator runtime trace, Optuna report, dashboard screenshot이 생기고 나니 어떤 artifact가 어떤 track에 속하는지 알아야 했다. 그렇지 않으면 실패한 실행이 의미를 알 수 없는 파일만 잔뜩 남겼을 것이다.
Kubernetes metric만 쓰지 않고 Borg trace를 쓴 이유
live Kubernetes metric만으로 시작할 수도 있었다. pod CPU, memory, HPA desired replicas, Pending pod, node readiness처럼 더 즉각적으로 익숙한 것들이었다. 하지만 나는 reactive demo 이상의 것을 원했다. Borg trace는 machine/task behavior를 offline으로 처리하고, 수리하고, replay할 수 있는 dataset을 제공했다. 또한 terminal event와 prediction horizon 관점으로 생각하게 만들었다. 바로 그 부분이 Kubernetes가 사후에 반응하는 모습을 보며 빠져 있다고 느낀 것이었다.
첫 baseline command는 의도적으로 지루했다. 영리한 architecture보다 재현 가능한 CLI entrypoint를 먼저 원했다.
python scripts/make_dataset.py
python scripts/make_forecaster_dataset.py
python scripts/train_forecaster_baseline.py
첫 target label
첫 번째 중요한 label은 문서로 쓰면 단순했다. task에 prediction horizon 안의 failure terminal event가 있으면 row를 positive로 표시하는 것. 실제 구현에서는 처음으로 진짜 조심해야 했다. usage window가 끝나기 전에 이미 일어난 terminal event는 피해야 했고, time arithmetic은 명시적으로 유지해야 했다.
dataset = pl.scan_parquet(dataset_file(cluster_id))
frame = (
dataset
.sort(["collection_id", "instance_index", "end_time"])
.with_columns([
(pl.col("last_event_time") - pl.col("end_time")).alias("time_to_terminal_event_us"),
pl.col("final_event_type").is_in(failure_event_types).alias("is_failure_terminal_event"),
])
.with_columns([
(
pl.col("is_failure_terminal_event")
& pl.col("time_to_terminal_event_us").is_not_null()
& (pl.col("time_to_terminal_event_us") >= 0)
& (pl.col("time_to_terminal_event_us") <= horizon_us)
).alias("failure_within_horizon")
])
)
이런 코드는 평범해 보이지만, 프로젝트 전체가 의미 있는지 아닌지를 결정한다. label이 future state를 잘못 leak하면 이후의 model과 dashboard는 모두 그럴듯해 보이면서도 아무 의미가 없을 수 있다. 프로젝트의 가치는 지루한 label plumbing만큼만 나온다는 사실을 계속 스스로에게 상기해야 했다.
프로젝트가 되길 바랐던 모습
이 시점에는 아직 최종 dashboard를 구체적으로 생각하고 있지 않았다. 내가 생각한 것은 언젠가 이런 질문에 답할 수 있는 실험이었다. 눈에 보이는 failure 전에 risk가 올라간다면 controller는 무엇을 해야 할까? demand가 낮을 때 safety를 해치지 않고 efficiency action을 취할 수 있을까? queue health가 악화되면 admission control이 power saving보다 우선해야 할까? 그리고 이 모든 일이 벌어진다면, 나는 최종 숫자만이 아니라 실제 reasoning을 볼 수 있을까?
그래서 이 series의 첫 글은 dashboard screenshot과 multi-agent stack보다 앞선 여기서 시작한다. Kubernetes 운영에서 느낀 마찰이 먼저였다. model은 그 답답함을 측정 가능하게 만들려는 첫 시도였다. dashboard는 나중에 왔다. 보이지 않는 실험을 더 이상 믿지 않게 되었기 때문이다.
이 단계가 중요했던 이유는 운영 중 느끼던 감각을 공학적인 질문으로 바꿨기 때문이다. 이제 프로젝트는 public trace dataset으로 model을 하나 학습시키는 일이 아니었다. prediction이 관찰 가능하고 검토 가능한 control signal이 될 수 있는지 확인하는 일이었다. 그 차이가 이후 모든 설계 결정을 끌고 갔다.