Manual de incidentes¶
Qué hacer primero ante las averías más comunes de Evey, para el operador de guardia. Busca la sección que corresponde al síntoma, haz sus comprobaciones y aplica la solución que corresponda a lo que hayas encontrado. Todas las secciones tienen la misma forma: Síntoma, Diagnóstico, Solución.
Antes de empezar¶
Los comandos están pensados para la instalación estándar: un solo
contenedor, evey-server, con Postgres y Meilisearch dentro. Sus líneas
aparecen en el mismo registro que las del servidor (prefijo postgres: para
la base de datos). El docker-compose.yml del repositorio añade los
contenedores evey-postgres y evey-caddy; consulta
Despliegues avanzados.
Puertos. La API HTTP escucha en EVEY_SERVER_PORT + 1, 9091 por
defecto; /healthz responde ahí. La línea ports: del compose es la que lo
publica.
Shell SQL en la base de datos integrada:
docker exec -it evey-server sh -c \
'PGPASSWORD=$(cat /data/keys/postgres.password) psql -h 127.0.0.1 -U evey evey'
Registros. El mismo flujo existe dos veces: el archivo diario
./evey-data/logs/evey-YYYY-MM-DD.log (EVEY_LOG_DIR, /data/logs dentro
de la imagen; un archivo de más de 50 MB, EVEY_LOG_MAX_SIZE_MB, continúa en evey-YYYY-MM-DD-HH.log) y docker logs evey-server. La referencia es el archivo: el
driver json-file de Docker rota docker logs, y después de una rotación el
comando puede dejar de mostrar líneas horas antes de que se parara el
contenedor. Cuando los dos no coinciden, lee el archivo. La página
Registros y depuración del panel de administración busca en el mismo flujo
(GET /api/v1/admin/logs/search?q=<term>&limit=200); los bearers, los JWT y
los valores token= / password= aparecen ahí como [redacted], así que
busca un id de medio, nunca un token.
El servidor no responde¶
Síntoma: los clientes no consiguen conectarse;
curl http://localhost:9091/healthz agota el tiempo de espera o es
rechazado.
Diagnóstico:
docker ps --filter name=evey-server
docker inspect --format='{{.State.Health.Status}}' evey-server
docker logs --tail=200 evey-server
ss -ltnp | grep 9091 # ¿escucha el puerto?
curl -v http://localhost:9091/healthz
Solución:
- El contenedor se ha parado: el panic está al final de
docker logs evey-server. Corrige la causa y despuésdocker compose up -d evey. - La comprobación de salud está en rojo porque la base de datos o el motor de búsqueda no funcionan: consulta Errores de la base de datos y La búsqueda funciona peor.
- Conflicto de puertos en el equipo: cambia el lado del equipo en la línea
ports:del compose, o libera el puerto. - Caddy, delante, no enruta (organización del repositorio):
docker logs evey-caddy, compruebaEVEY_DOMAIN, y que la comprobación de salud deeveyesté en verde, porque Caddy la espera.
Todos los inicios de sesión devuelven 401¶
Síntoma: todos los usuarios, administradores incluidos, reciben 401 de
/api/v1/auth/*.
Diagnóstico:
docker logs --since 10m evey-server | grep -iE 'epoch|jwt|auth|signing|private key'
docker exec evey-server ls -l /data/jwt.key
Busca primero la línea de la época (epoch). Cada petición autenticada vuelve a comprobar la época del token del usuario en la base de datos, y esa comprobación aplica el cierre por defecto: una base de datos lenta, bloqueada o inalcanzable hace que todas las peticiones devuelvan el mismo 401 que un token caducado, lo que parece un problema de JWT y no lo es. El servidor lo dice:
auth rejected: the token epoch could not be read, so every authenticated request is failing - check the database, not the JWT key
Si esa línea aparece, ve a Errores de la base de datos.
Mientras tanto, /healthz puede seguir en verde: comprueba la conexión, no
ejecuta la consulta de la época. El contador de Prometheus es
evey_auth_failures_total{reason="epoch_lookup_failed"}.
Solución:
- Una clave JWT que falta o que ha cambiado rara vez es la causa. Un
archivo de clave ilegible (
EVEY_JWT_PRIVATE_KEY_PATH,/data/jwt.key) impide arrancar al servidor; si falta, se genera otra. Los tokens de acceso firmados con la clave antigua se rechazan entonces, pero las apps los renuevan con su token de renovación, que se comprueba en la base de datos y no con la clave: los usuarios siguen con la sesión iniciada. Comprueba que el volumen./evey-dataestá montado en/data; sin él, también falta la base de datos. - El limitador de peticiones se activa en cada petición:
EVEY_RATELIMIT_LOGIN_MAX(10por IP y por minuto) yEVEY_RATELIMIT_GLOBAL_MAX(300). Un proxy inverso que lo reenvía todo como una sola dirección lo activa enseguida: añade el proxy a Proxies de confianza en Administración → Acceso remoto (se aplica sin reiniciar;EVEY_TRUSTED_PROXIESes el valor de reserva). Eldocker-compose.override.ymldel repositorio sube los dos a1000/10000para el desarrollo. - Desfase de reloj entre el servidor y los clientes:
date -uen el equipo.
La transcodificación falla¶
Síntoma: la reproducción directa funciona, pero la reproducción que necesita transcodificar falla; el reproductor da error y el registro del servidor muestra códigos de salida de ffmpeg.
Diagnóstico:
docker exec evey-server which ffmpeg ffprobe
docker exec evey-server ffmpeg -version | head -1
docker logs --since 10m evey-server | grep -iE 'ffmpeg|transcode|hls'
docker exec evey-server ls -ld /cache/transcode /data/trickplay
docker exec evey-server df -h /cache
docker exec evey-server touch /cache/transcode/.write-test && \
docker exec evey-server rm /cache/transcode/.write-test
Solución:
- Falta ffmpeg (una imagen personalizada): vuelve a construirla a partir
del
Dockerfileoficial. - No se puede escribir en la carpeta de la caché: la imagen define
EVEY_TRANSCODING_CACHE_DIR=/cache/transcode(el volumenevey-cache) yEVEY_TRICKPLAY_CACHE_DIR=/data/trickplay; una entradaenvironment:del compose los sustituye. Fuera de la imagen, los valores por defecto de Go son/data/transcodey/cache/trickplay. - Caché llena: consulta El disco de la caché está lleno.
- La aceleración por hardware falla: pon Aceleración por hardware en Desactivada (solo software) en Administración → Transcodificación → Calibración de codificación (se aplica en el acto y vuelve a calibrar) para que todas las transcodificaciones se hagan con el procesador; vuelve a Auto (mejor disponible) cuando el hardware esté arreglado.
- «El servidor está ocupado transcodificando otros flujos»: la capacidad
en directo se mide, no se fija; consulta Repartir la
capacidad.
EVEY_TRANSCODING_MAX_CONCURRENT(Máximo simultáneo en Administración → Caché de transcodificación) solo cubre las tareas de transcodificación fuera de la reproducción en directo.
Se queda cargando o se corta y el registro no dice por qué¶
Síntoma: un espectador avisa de interrupciones en un título transcodificado; el
registro muestra hls-vod: segment timeout con err:"context canceled", lo
que significa que el cliente ha cerrado la conexión, no que el servidor haya agotado su
tiempo.
Diagnóstico: empieza por lo que el servidor ya ha registrado, no cuesta nada:
# Por qué una petición de segmento ha reutilizado un codificador o ha lanzado
# uno, y lo que le ha costado al reproductor. Solo se registra cuando cuesta
# algo. `over_client_patience: true` es la prueba definitiva.
docker exec evey-server grep '"software: reach decision"' /data/logs/evey-$(date +%F)*.log | tail -40
# Los propios codificadores: lanzamientos, cierres y su motivo.
docker exec evey-server grep -E '"software: session (started|killed)"|per-hash cap' \
/data/logs/evey-$(date +%F)*.log | tail -40
Esas líneas dicen lo que ha decidido el servidor. Para ver lo que ha
conseguido ffmpeg segundo a segundo, activa el registro de progreso de cada
codificador ($TOKEN es un JWT de administrador):
# ACTIVAR. Se aplica al siguiente codificador, sin reiniciar.
curl -sf -X PUT http://localhost:9091/api/v1/admin/server-config/transcode.ffmpeg_progress_log \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"value":"true"}'
# Reproduce el problema y lee una sesión: out_time = contenido producido hasta
# ahora, speed = cuánto se ha adelantado el codificador al tiempo real.
docker exec evey-server ls -t /data/logs/ffmpeg | head
docker exec evey-server tail -20 /data/logs/ffmpeg/<file>.log
# DESACTIVAR al terminar.
curl -sf -X PUT http://localhost:9091/api/v1/admin/server-config/transcode.ffmpeg_progress_log \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"value":"false"}'
Note
Deja el registro de progreso desactivado fuera de una investigación: un
archivo por codificador, unos pocos MB por sesión, para cada
reproducción en la máquina. El nombre del archivo lleva el hash de la
decisión, el escalón (-vN-) y el segmento de inicio (-sN-), así que
coincide con el registro JSON; un codificador que se vuelve a lanzar en
la misma posición tiene su propio archivo, y eso es lo que hace visible
que los codificadores se relanzan sin parar.
Solución:
- Saltos cortos hacia delante repetidos: cada uno puede lanzar un
codificador nuevo, y el límite por hash (
MaxSessionsPerHash, 2 por defecto, 1 con el perfil de poca memoria) cierra el que lleva más tiempo sin usarse:session killed reason:lru_capen el registro. Normal si no pasa demasiado. - El cliente tiene menos paciencia que el servidor: la ventana para
alcanzar un salto se calcula con
clientSegmentPatience(6 s), mientras que el propio margen de espera del servidor (WaitBudgetDur) es de 60 s. Una líneareach decisionconover_client_patience: truesignifica que el servidor estaba a punto de pedir una espera que ningún reproductor soporta.
Errores 410 stale_rendition: el reproductor vuelve a pedir su lista maestra en plena reproducción¶
Síntoma: el reproductor se para un momento y luego se recupera; el
registro muestra
hls-vod: stale rendition URL refused (the ladder moved under a live player)
y peticiones respondidas con 410 Gone y X-Evey-Error: stale_rendition.
Es una protección que se activa, no la avería en sí. Cada URI del manifiesto
lleva el hash de la decisión con la que se construyó (fp=). Un 410 significa
que un reproductor ha presentado una URL de una sesión cuya escala ABR ha
cambiado de forma desde entonces; servirla le daría al descodificador bytes
de otra calidad. El rechazo obliga a volver a pedir la lista maestra limpia.
Diagnóstico: el WARN lleva sealed_fp, current_fp y el id del medio.
- Una ráfaga justo después de un despliegue, un reinicio o una recalibración es normal: hay sesiones en curso que han sobrevivido a un cambio de capacidades. Los reproductores se ponen al día en una consulta.
- Un goteo constante durante el funcionamiento normal significa que algo
sigue cambiando las escalas mientras hay flujos en curso: busca
capacidades que cambian una y otra vez (
docker logs evey-server | grep -iE 'calibrat|capability'), un regulador de admisión que cambia sin parar bajo carga, o un límite CMCD que oscila. - Los reproductores que se quedan en bucle con 410 tienen una lista
maestra en caché (una versión antigua de la app, un proxy que guarda en
caché los
.m3u8). El proxy tiene que dejar pasarCache-Control: no-cache.
Errores de la base de datos¶
Síntoma: el registro contiene dial tcp ... connect: connection refused,
pq: ..., o las consultas agotan su tiempo.
Diagnóstico:
docker logs --tail=300 evey-server | grep -iE 'postgres|database'
docker exec evey-server sh -c 'PGPASSWORD=$(cat /data/keys/postgres.password) psql -h 127.0.0.1 -U evey evey -c "
SELECT count(*), state
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;"'
df -h ./evey-data
Solución:
- Disco lleno en
./evey-data(el clúster está en./evey-data/postgres): libera espacio, y despuésdocker compose restart evey. - Reserva de conexiones agotada (
too many connections, esperas largas):EVEY_DB_MAX_OPEN_CONNS(por defecto25). Cierra las sesiones bloqueadas: - Contenedor de Postgres aparte (organización del repositorio):
docker ps --filter name=evey-postgres,docker logs --tail=200 evey-postgres; losEVEY_DB_USER/EVEY_DB_PASSWORD/EVEY_DB_NAMEdel servidor tienen que coincidir con el serviciopostgres.
La búsqueda funciona peor¶
Síntoma: la búsqueda da peores resultados; el registro muestra
meilisearch unavailable, using SQL search fallback.
La búsqueda sigue funcionando por SQL mientras no se puede llegar a
Meilisearch; solo baja la calidad del orden de los resultados, nadie se queda
sin servicio. Meilisearch funciona dentro de evey-server
(EVEY_EMBEDDED_MEILI, activado por defecto) en la interfaz local del
contenedor, puerto 7700.
Diagnóstico:
docker exec evey-server curl -sf http://127.0.0.1:7700/health
docker logs --tail=300 evey-server | grep -i meili
Solución:
- No está sano:
docker compose restart evey. - Índice corrupto: para el contenedor, aparta
./evey-data/meili, vuelve a arrancar, y después reconstruye el índice a partir de la base de datos: La reconstrucción es un comando, no una acción del panel.
El disco de la caché está lleno¶
Síntoma: las transcodificaciones fallan con no space left on device;
la comprobación de salud cambia de estado una y otra vez.
Diagnóstico:
docker exec evey-server df -h /cache
docker exec evey-server du -sh /cache/transcode /data/trickplay 2>/dev/null
docker volume inspect evey_evey-cache # el prefijo del compose puede cambiar
Solución:
- Vacía la caché de transcodificación, se puede regenerar:
Esto también borra
photos/(las versiones redimensionadas de las fotos) ytrack-art/(las portadas extraídas de las etiquetas de audio) que hay dentro; las dos se reconstruyen en la siguiente petición. Borrar todo en Administración → Caché de transcodificación vacía la misma caché desde el panel. - Las miniaturas de la barra de avance (trickplay) cuestan más de regenerar; bórralas solo si hace falta:
- Baja el Presupuesto de caché de transcodificación (GB) en
Administración → Transcodificación (por defecto
10) si el volumen del equipo tiene poco espacio. Sea cual sea el presupuesto, la limpieza automática mantiene 2 GiB libres en el volumen de la caché.
Un plugin se ha bloqueado¶
Síntoma: un plugin deja de responder; el registro muestra panics del ejecutor de plugins.
Diagnóstico:
docker logs --since 30m evey-server 2>&1 | grep -iE 'plugin|pluginrunner|panic'
# busca: "panic in plugin runner", "plugin process crashed", "remove plugin sock on crash"
Solución:
- Vuelve a lanzar el plugin sin tocar el servidor: Activar otra vez en
Administración → Plugins, o
POST /api/v1/admin/plugins/<id>/enable. Funciona cuando el plugin está activado pero atascado enError: el arranque solo rechaza un plugin que ya estáRunningoInitializing. - Si con eso no se arregla, Desactivar el plugin desde el panel.
- Si el propio servidor está inestable:
docker compose restart evey. - Los archivos de los plugins están en
./evey-data/plugins/en el equipo (/data/pluginsen el contenedor). Borra la carpeta del plugin que falla y reinicia. - Envía la traza del panic y el id del plugin a su autor.
Un envío se ha congelado, se ha quedado sin sonido, o el teléfono no deja de cargar¶
Todo lo de aquí se puede leer en Administración → Registros y depuración; no
hace falta un shell en la máquina. Las horas están en UTC. Cada señal es una
línea player-client: playback event reported con type= y una cadena
details=.
- ¿Funcionaba el receptor, y hasta cuándo? Busca
cast_receiver_alive. El receptor envía una cada 30 s mientras reproduce;detailsllevabuf=<behind>/<ahead>/<ranges>(los segundos en búfer alrededor de la posición de lectura) ypos_sec. Una reproducción sana muestraaheaden decenas de segundos. La hora de la última señal es cuando el receptor se detuvo o fue terminado: si no hay nada después, la tele ya no está ejecutando el receptor, diga lo que diga el teléfono. - ¿Por qué se paró? Busca
cast_receiver_finished:reason=INTERRUPTEDes un LOAD nuevo que sustituye al que estaba en marcha (busca unStartStreamunos segundos antes),reason=ERRORlleva el error del reproductor. Si no hay ningúnfinisheddespués del últimoalive, el proceso del receptor ha desaparecido; casi siempre es un Chromecast HD que el sistema ha cerrado por falta de memoria con una lista maestra de copia de alto bitrate (según lo observado). - ¿Qué ha visto el teléfono? Busca
cast_sender_stall: el emisor envíastate=playing frozen=<n>s pos=<p>cuando el SDK de Cast sigue diciendo PLAYING pero la posición no se ha movido en 20 s, ystate=idle reason=<r>cuando el receptor ha indicado un motivo de inactividad. Son líneas WARN, así que el filtro de avisos de la página las muestra. Compara suposcon el últimopos_secdel receptor. - ¿Ha provocado la sesión fallos en cadena? Busca
refresh rejected: un token de renovaciónnot foundcierra la sesión de la app y la vuelve a abrir (con las credenciales guardadas), lo que vuelve a lanzar el flujo; la línea lleva eljti,issuedyagedel token, que permiten distinguir una copia antigua de una falsificada. BuscaStartStreampara ver los relanzamientos. - ¿Era el flujo demasiado pesado para el dispositivo? Busca
advertises a fallback rung(el bitrate máximo de la lista maestra de copia) yplan fitted to the client's line(la medidamaxKbpsdel receptor o su mecanismo de rescateabr=floor; la lista maestra transcodificada que viene después lleva el motivoclient_line_exceededoabr_floor_requested). Una sesión que empezó con una lista maestra de copia de alto bitrate y bajó a un escalón 1080p ajustado al cabo de unos segundos es el mecanismo de rescate funcionando como debe; un nuevo LOAD que vuelve a empezar con la lista maestra de copia es lo que hace fallar a un Chromecast HD. - ¿Le ha faltado algo al servidor? Busca
on-demand resource refused: una ráfaga de rechazoscontext canceleden segmentos seguidos es alguien moviéndose por la barra de avance (cada petición la cancela la siguiente), no un fallo del servidor; las líneasservedcon duraciones largas sí lo son.
Si los pasos 1 a 3 muestran el receptor activo y el emisor sin señales, el problema está en la sesión de Cast del teléfono (el SDK, la red hasta la tele). Si las señales del receptor se paran primero, mira la tele: la memoria, el códec, el bitrate del flujo.
Detener una sesión o un dispositivo comprometido¶
Síntoma: un dispositivo inesperado en Administración → Reproduciendo ahora, o un usuario avisa de un dispositivo desconocido en su lista de sesiones.
- Identifica la sesión:
- Detenla desde Administración → Reproduciendo ahora: el icono de
parar de la sesión (Detener sesión), y después Detener para
confirmar; o
AdminSessionService/KillSessionpor gRPC con el id de la sesión. El reproductor recibe primero una orden de parar y el flujo se termina 5 segundos después. Esto solo termina la reproducción; el usuario sigue con la sesión iniciada. - Si el flujo sigue funcionando de todos modos, reinicia el contenedor: todos los codificadores se paran con él.
- Si la propia cuenta está comprometida, Administración → Usuarios →
Desactivar: los tokens de renovación se borran y los tokens de acceso se
revocan en el acto, así que se cierra la sesión en todos los
dispositivos. Pon una contraseña nueva (
evey admin reset-password --user <email>, consulta Recuperación), y después vuelve a Activar la cuenta.
Un escaneo de la biblioteca se ha atascado¶
Síntoma: un escaneo aparece en curso durante horas sin ningún avance de archivos en el registro.
- Comprueba si el escaneo sigue escribiendo algo en el registro; un escaneo
colgado muestra el procesador sin actividad y ninguna línea
scan:reciente: - El bloqueo del escaneo está en memoria, así que un reinicio lo quita:
- Si un archivo provoca panics una y otra vez (las trazas lo nombran), sácalo de la carpeta de la biblioteca y vuelve a escanear.
- Si el vigilante de archivos registra
watcher: inotify failed for path, will use periodic scan, se ha agotado el límite de vigilancias del núcleo:
Resumen de comandos¶
# Estado de todo
docker compose ps
curl -sf http://localhost:9091/healthz && echo healthy
# Seguir todos los contenedores
docker compose logs --tail=100 -f
# Configuración efectiva
docker exec evey-server env | grep ^EVEY_ | sort
# Tamaño de la base de datos
docker exec evey-server sh -c 'PGPASSWORD=$(cat /data/keys/postgres.password) psql -h 127.0.0.1 -U evey evey -c "
SELECT pg_size_pretty(pg_database_size(current_database()));"'
# Disco del equipo
df -h ./evey-data