Files
workshop/aula-13/benchmarks/benchmark.sh
ArgoCD Setup f5cb6f0581 fix(aula-13): reescrever benchmarks eStargz vs GZIP
Benchmarks antigos eram falhos: node hardcoded, imagens diferentes,
sem verificação de snapshotter, sem controle de cache, 1 iteração.

Novos scripts:
- prepare-images.sh: constrói mesma imagem em gzip e estargz
- benchmark.sh: múltiplas iterações, detecção de cache hits,
  verificação de snapshotter, 3 tipos de imagem (nginx/node/postgres)

Requer worker node sem cache (node fresh via cluster autoscaler).
2026-03-14 07:48:51 -03:00

486 lines
20 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# =============================================================================
# Benchmark: eStargz vs GZIP — Cold Pull Performance
# =============================================================================
#
# Metodologia:
#
# 1. VERIFICAÇÃO — confirma stargz-snapshotter ativo no worker
# 2. PREPARAÇÃO — namespace, registry secret, identificação do worker
# 3. EXECUÇÃO — para cada imagem × formato × iteração:
# a) Remove imagem do cache do worker (via crictl em pod privilegiado)
# b) Cria pod pinado no worker
# c) Mede tempo de pull (evento Pulling→Pulled) e total (até Ready)
# d) Remove pod
# 4. RELATÓRIO — médias por imagem/formato, tabela comparativa
#
# Controles de validade:
# - Mesma imagem base, mesmo Dockerfile, só a compressão muda
# - Pod pinado no mesmo worker (elimina variação de rede entre nodes)
# - Evento "Pulled" checado para descartar cache hits silenciosos
# - Múltiplas iterações com média
#
# IMPORTANTE — Cache de imagens:
# O benchmark precisa de um worker node SEM as imagens em cache.
# Não é possível limpar cache de forma confiável no Talos (sem crictl/ctr).
# A forma mais segura é escalar um worker novo via cluster autoscaler:
#
# kubectl apply -f ../test-autoscaler.yaml # Força scale-up
# kubectl get nodes -w # Esperar novo node
# ./benchmark.sh # Rodar no node limpo
#
# O script detecta cache e avisa antes de rodar.
#
# Pré-requisitos:
# - kubectl configurado (KUBECONFIG)
# - Imagens preparadas com ./prepare-images.sh
# - stargz-snapshotter ativo nos workers (extensão Talos)
# - Worker node SEM cache das imagens de benchmark
#
# Uso:
# ./benchmark.sh # 3 iterações (padrão)
# BENCH_ITERATIONS=5 ./benchmark.sh # 5 iterações
#
# =============================================================================
set -euo pipefail
# ─────────────────────────────────────────────────────────────
# Configuração
# ─────────────────────────────────────────────────────────────
REGISTRY="${REGISTRY:-gitea.kube.quest}"
ORG="bench"
NAMESPACE="benchmark"
ITERATIONS="${BENCH_ITERATIONS:-3}"
TALOSCONFIG="${TALOSCONFIG:-$(cd "$(dirname "$0")/../.." && pwd)/aula-08/talosconfig}"
WORKER_NODE=""
WORKER_IP=""
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERRO]${NC} $1"; }
# Timestamp em milissegundos (compatível com macOS + Linux)
ts_ms() { python3 -c "import time; print(int(time.time() * 1000))"; }
# Converter timestamp ISO 8601 → epoch ms
iso_to_ms() {
python3 -c "
from datetime import datetime, timezone
s = '${1}'.rstrip('Z')
try:
dt = datetime.fromisoformat(s).replace(tzinfo=timezone.utc)
except:
dt = datetime.strptime(s, '%Y-%m-%dT%H:%M:%S').replace(tzinfo=timezone.utc)
print(int(dt.timestamp() * 1000))
"
}
# ─────────────────────────────────────────────────────────────
# 1. PRÉ-FLIGHT
# ─────────────────────────────────────────────────────────────
preflight() {
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Benchmark: eStargz vs GZIP — Cold Pull Performance${NC}"
echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}"
echo ""
echo " Registry: ${REGISTRY}/${ORG}"
echo " Iterações: ${ITERATIONS}"
echo ""
# Detectar worker
WORKER_NODE=$(kubectl get nodes \
-l '!node-role.kubernetes.io/control-plane' \
-o jsonpath='{.items[0].metadata.name}')
WORKER_IP=$(kubectl get node "$WORKER_NODE" \
-o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}')
if [[ -z "$WORKER_NODE" ]]; then
log_error "Nenhum worker node encontrado"
exit 1
fi
log_info "Worker: ${WORKER_NODE} (${WORKER_IP})"
# Verificar stargz-snapshotter
log_info "Verificando stargz-snapshotter..."
local snapshotter_ok=false
if [[ -f "$TALOSCONFIG" ]]; then
local snap_conf
snap_conf=$(TALOSCONFIG="$TALOSCONFIG" talosctl -n "$WORKER_IP" \
read /etc/cri/conf.d/10-stargz-snapshotter.part 2>/dev/null || echo "")
if echo "$snap_conf" | grep -q "stargz"; then
snapshotter_ok=true
fi
if [[ "$snapshotter_ok" == "false" ]]; then
local ext
ext=$(TALOSCONFIG="$TALOSCONFIG" talosctl -n "$WORKER_IP" \
get extensionstatuses 2>/dev/null || echo "")
if echo "$ext" | grep -qi "stargz"; then
snapshotter_ok=true
fi
fi
fi
if [[ "$snapshotter_ok" == "true" ]]; then
log_success "stargz-snapshotter ativo"
else
echo ""
log_warn "stargz-snapshotter NÃO detectado em ${WORKER_NODE}"
log_warn "Sem ele, imagens eStargz são tratadas como gzip — resultados serão idênticos."
echo ""
echo -n " Continuar mesmo assim? [s/N]: "
read -r choice
[[ "$choice" == "s" || "$choice" == "S" ]] || exit 1
fi
# Verificar imagens no registry
log_info "Verificando imagens no registry..."
for img in nginx node postgres; do
for fmt in gzip estargz; do
local ref="${REGISTRY}/${ORG}/${img}:${fmt}"
if ! docker manifest inspect "$ref" &>/dev/null; then
log_error "Imagem não encontrada: ${ref}"
log_error "Execute ./prepare-images.sh primeiro"
exit 1
fi
done
done
log_success "Todas as imagens encontradas"
# Verificar se o node tem cache (invalida resultados)
verify_clean_node
}
# ─────────────────────────────────────────────────────────────
# 2. SETUP DO NAMESPACE
# ─────────────────────────────────────────────────────────────
setup_namespace() {
log_info "Preparando namespace ${NAMESPACE}..."
kubectl delete namespace "$NAMESPACE" --ignore-not-found --wait 2>/dev/null || true
kubectl create namespace "$NAMESPACE"
kubectl label namespace "$NAMESPACE" \
pod-security.kubernetes.io/enforce=privileged --overwrite
# Registry secret — usar credenciais do docker config
local docker_cfg="${HOME}/.docker/config.json"
if [[ -f "$docker_cfg" ]]; then
kubectl create secret generic bench-registry \
--from-file=.dockerconfigjson="$docker_cfg" \
--type=kubernetes.io/dockerconfigjson \
-n "$NAMESPACE" 2>/dev/null || true
else
log_warn "~/.docker/config.json não encontrado — pull de imagens privadas pode falhar"
fi
log_success "Namespace pronto"
}
# ─────────────────────────────────────────────────────────────
# 3. LIMPEZA DE CACHE
# ─────────────────────────────────────────────────────────────
verify_clean_node() {
# Verifica se o worker node tem as imagens de benchmark em cache.
# Se tiver, o benchmark não é válido — precisa de node fresh.
log_info "Verificando cache de imagens no worker..."
local cached_count=0
local cached_images=""
# Usa talosctl para listar imagens no node
if [[ -f "$TALOSCONFIG" ]]; then
for img in nginx node postgres; do
for fmt in gzip estargz; do
local ref="${REGISTRY}/${ORG}/${img}:${fmt}"
if TALOSCONFIG="$TALOSCONFIG" talosctl -n "$WORKER_IP" \
image list 2>/dev/null | grep -q "$ref"; then
cached_count=$((cached_count + 1))
cached_images="${cached_images} ${ref}\n"
fi
done
done
fi
if [[ "$cached_count" -gt 0 ]]; then
echo ""
log_warn "O worker ${WORKER_NODE} tem ${cached_count} imagens de benchmark em cache:"
echo -e "$cached_images"
log_warn "Para resultados válidos, use um worker SEM cache."
echo ""
echo " Opções:"
echo " 1. Escalar um worker novo (cluster autoscaler cria node limpo):"
echo " kubectl apply -f ../test-autoscaler.yaml"
echo " # Aguardar novo node ficar Ready"
echo " # Re-rodar benchmark (vai usar o novo node)"
echo ""
echo " 2. Continuar mesmo assim (resultados podem ter cache hits)"
echo ""
echo -n " Continuar com cache? [s/N]: "
read -r choice
[[ "$choice" == "s" || "$choice" == "S" ]] || exit 1
else
log_success "Worker sem cache de benchmark — resultados serão válidos"
fi
}
# ─────────────────────────────────────────────────────────────
# 4. EXECUTAR UM TESTE
# Saída: pull_ms total_ms cached(yes/no)
# ─────────────────────────────────────────────────────────────
run_single_test() {
local image=$1 # nginx, node, postgres
local format=$2 # gzip, estargz
local run=$3 # número da iteração
local image_ref="${REGISTRY}/${ORG}/${image}:${format}"
local pod_name="b-${image}-${format}-${run}"
# Configuração específica por imagem
local env_yaml=""
local cmd_yaml=""
case $image in
postgres)
env_yaml=" env:
- name: POSTGRES_PASSWORD
value: benchtest"
;;
*)
# nginx e node precisam de um command para não sair imediatamente
cmd_yaml=" command: [\"sleep\", \"infinity\"]"
;;
esac
# Registrar tempo de parede
local t_start
t_start=$(ts_ms)
# Criar pod
cat <<EOF | kubectl apply -f - -n "$NAMESPACE" >/dev/null 2>&1
apiVersion: v1
kind: Pod
metadata:
name: ${pod_name}
spec:
nodeName: ${WORKER_NODE}
restartPolicy: Never
terminationGracePeriodSeconds: 0
imagePullSecrets:
- name: bench-registry
containers:
- name: app
image: ${image_ref}
${cmd_yaml}
${env_yaml}
EOF
# Esperar Ready
kubectl wait --for=condition=Ready "pod/${pod_name}" \
-n "$NAMESPACE" --timeout=300s >/dev/null 2>&1
local t_ready
t_ready=$(ts_ms)
local total_ms=$(( t_ready - t_start ))
# Extrair tempo de pull dos eventos do Kubernetes
sleep 1 # dar tempo pro event ser registrado
local pull_ms=-1
local cached="no"
local pull_msg
pull_msg=$(kubectl get events -n "$NAMESPACE" \
--field-selector "involvedObject.name=${pod_name},reason=Pulled" \
-o jsonpath='{.items[0].message}' 2>/dev/null || echo "")
if echo "$pull_msg" | grep -qi "already present"; then
cached="yes"
pull_ms=0
log_warn " ⚠ CACHE HIT — resultado inválido" >&2
else
# Tentar extrair timestamps dos eventos
local ts_pulling ts_pulled
ts_pulling=$(kubectl get events -n "$NAMESPACE" \
--field-selector "involvedObject.name=${pod_name},reason=Pulling" \
-o jsonpath='{.items[0].firstTimestamp}' 2>/dev/null || echo "")
ts_pulled=$(kubectl get events -n "$NAMESPACE" \
--field-selector "involvedObject.name=${pod_name},reason=Pulled" \
-o jsonpath='{.items[0].firstTimestamp}' 2>/dev/null || echo "")
if [[ -n "$ts_pulling" && -n "$ts_pulled" ]]; then
local ms_start ms_end
ms_start=$(iso_to_ms "$ts_pulling")
ms_end=$(iso_to_ms "$ts_pulled")
pull_ms=$(( ms_end - ms_start ))
fi
fi
# Limpar pod
kubectl delete pod "$pod_name" -n "$NAMESPACE" \
--grace-period=0 --force >/dev/null 2>&1 || true
# Esperar pod ser removido antes da próxima iteração
kubectl wait --for=delete "pod/${pod_name}" \
-n "$NAMESPACE" --timeout=30s >/dev/null 2>&1 || true
echo "${pull_ms} ${total_ms} ${cached}"
}
# ─────────────────────────────────────────────────────────────
# 5. RELATÓRIO (lê resultados do RESULTS_DIR)
# ─────────────────────────────────────────────────────────────
# Helpers para ler/escrever resultados em arquivos (compatível com bash 3.2)
rset() { echo "$2" > "${RESULTS_DIR}/$1"; }
rget() { cat "${RESULTS_DIR}/$1" 2>/dev/null || echo "${2:-0}"; }
radd() { local cur; cur=$(rget "$1" 0); local val="${2:-0}"; rset "$1" $(( cur + val )); }
print_report() {
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD} RESULTADOS — eStargz vs GZIP Cold Pull${NC}"
echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
echo " Worker: ${WORKER_NODE}"
echo " Iterações: ${ITERATIONS}"
echo ""
printf " %-12s │ %-8s │ %10s │ %10s │ %10s │ %7s\n" \
"IMAGEM" "FORMATO" "PULL (ms)" "TOTAL (ms)" "PULL (s)" "CACHE?"
echo " ─────────────┼──────────┼────────────┼────────────┼────────────┼────────"
for img in nginx node postgres; do
for fmt in gzip estargz; do
local pull_avg; pull_avg=$(rget "${img}_${fmt}_pull_avg" "?")
local total_avg; total_avg=$(rget "${img}_${fmt}_total_avg" "?")
local cache_hits; cache_hits=$(rget "${img}_${fmt}_cache" 0)
local pull_sec="?"
if [[ "$pull_avg" != "?" ]] && [[ "$pull_avg" -ge 0 ]] 2>/dev/null; then
pull_sec=$(python3 -c "print(f'{${pull_avg}/1000:.1f}')")
fi
printf " %-12s │ %-8s │ %10s │ %10s │ %10s │ %d/%d\n" \
"$img" "$fmt" "$pull_avg" "$total_avg" "$pull_sec" \
"$cache_hits" "$ITERATIONS"
done
echo " ─────────────┼──────────┼────────────┼────────────┼────────────┼────────"
done
echo ""
# Comparação resumida
echo -e " ${BOLD}Comparação (tempo médio de pull):${NC}"
echo ""
for img in nginx node postgres; do
local gzip_pull; gzip_pull=$(rget "${img}_gzip_pull_avg" 0)
local estargz_pull; estargz_pull=$(rget "${img}_estargz_pull_avg" 0)
if [[ "$gzip_pull" != "?" ]] && [[ "$estargz_pull" != "?" ]] && [[ "$gzip_pull" -gt 0 ]] && [[ "$estargz_pull" -gt 0 ]]; then
local diff_ms=$(( gzip_pull - estargz_pull ))
local ratio
ratio=$(python3 -c "print(f'{${gzip_pull}/${estargz_pull}:.1f}')" 2>/dev/null || echo "?")
if [[ "$diff_ms" -gt 0 ]]; then
echo -e " ${img}: eStargz ${GREEN}${ratio}x mais rápido${NC} (${diff_ms}ms menos)"
elif [[ "$diff_ms" -lt 0 ]]; then
local abs_diff=$(( -diff_ms ))
echo -e " ${img}: eStargz ${RED}${abs_diff}ms mais lento${NC}"
else
echo " ${img}: resultados idênticos"
fi
else
echo " ${img}: dados insuficientes para comparação"
fi
done
echo ""
echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════${NC}"
echo ""
echo " Notas:"
echo " - PULL (ms): tempo entre eventos Pulling→Pulled (resolução ~1s)"
echo " - TOTAL (ms): tempo de parede até pod Ready (inclui pull + startup)"
echo " - CACHE? mostra quantos testes tiveram cache hit (deveria ser 0/N)"
echo " - Se CACHE? > 0, a limpeza de cache falhou e o resultado é inválido"
echo ""
}
# ─────────────────────────────────────────────────────────────
# MAIN
# ─────────────────────────────────────────────────────────────
# Diretório temporário para acumular resultados (substitui declare -A)
RESULTS_DIR=$(mktemp -d)
trap "rm -rf $RESULTS_DIR" EXIT
preflight
setup_namespace
for img in nginx node postgres; do
echo ""
echo -e "${CYAN}────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}${img}${NC}"
echo -e "${CYAN}────────────────────────────────────────────────${NC}"
for fmt in gzip estargz; do
key="${img}_${fmt}"
rset "${key}_pull_sum" 0
rset "${key}_total_sum" 0
rset "${key}_count" 0
rset "${key}_cache" 0
for run in $(seq 1 "$ITERATIONS"); do
echo -n " ${fmt} #${run}/${ITERATIONS} ... "
result=$(run_single_test "$img" "$fmt" "$run")
pull_ms=$(echo "$result" | awk '{print $1}')
total_ms=$(echo "$result" | awk '{print $2}')
cached=$(echo "$result" | awk '{print $3}')
if [[ "$cached" == "yes" ]]; then
radd "${key}_cache" 1
echo -e "${YELLOW}cache hit${NC} (total: ${total_ms}ms)"
else
radd "${key}_pull_sum" "$pull_ms"
radd "${key}_total_sum" "$total_ms"
radd "${key}_count" 1
echo "pull: ${pull_ms}ms total: ${total_ms}ms"
fi
done
# Calcular médias
count=$(rget "${key}_count" 0)
if [[ "$count" -gt 0 ]]; then
rset "${key}_pull_avg" $(( $(rget "${key}_pull_sum") / count ))
rset "${key}_total_avg" $(( $(rget "${key}_total_sum") / count ))
else
rset "${key}_pull_avg" "?"
rset "${key}_total_avg" "?"
fi
done
done
print_report
# Limpeza
log_info "Limpando namespace de benchmark..."
kubectl delete namespace "$NAMESPACE" --ignore-not-found >/dev/null 2>&1 || true
log_success "Benchmark concluído"