first commit
This commit is contained in:
416
docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md
Normal file
416
docs/regional/ARQUITETURA_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Arquitetura TIA Regional com Pool xAI Pré-Aquecido
|
||||
|
||||
## 1. Introdução
|
||||
|
||||
Este documento descreve a evolução do TIA para operar TTS xAI em alta disponibilidade e alta volumetria usando réplicas Kubernetes distribuídas por região. A solução parte da implementação atual do TIA/LiveKit e mantém seu contrato de TTS, suas métricas de underflow/TTFB e sua lógica de proteção contra repetição após áudio parcial.
|
||||
|
||||
A mudança principal é mover a manutenção do conjunto de conexões WebSocket xAI para um **pool persistente por Pod TIA**, implementado como sidecar. Cada Pod regional pode manter até `XAI_POOL_SIZE` conexões xAI pré-aquecidas e prontas para uso. O TIA continua enxergando um endpoint compatível com xAI, porém local (`127.0.0.1`).
|
||||
|
||||
## 2. Dificuldade do modelo anterior
|
||||
|
||||
Na versão anterior, cada instância `OraclexAITTS` mantinha uma única conexão reutilizável. Como o LiveKit executa chamadas em processos de job, essas conexões são naturalmente distribuídas por chamadas/processos e não formam um pool global do Pod.
|
||||
|
||||
Isso cria alguns riscos em alta volumetria:
|
||||
|
||||
1. **Burst de handshakes WebSocket.** Muitas chamadas podem abrir conexões ao xAI praticamente ao mesmo tempo.
|
||||
2. **Capacidade não compartilhada.** Uma chamada pode manter uma conexão ociosa enquanto outra precisa abrir uma nova.
|
||||
3. **Ausência de backpressure no balanceador.** O LB enxerga o TIA como saudável mesmo quando a capacidade de TTS daquele Pod está totalmente ocupada.
|
||||
4. **Acoplamento entre sessão e conexão xAI.** A quantidade de chamadas pode virar, indiretamente, a quantidade de conexões abertas, mesmo quando apenas uma parte das chamadas está sintetizando naquele instante.
|
||||
5. **Falha regional afeta novas chamadas.** Sem readiness orientada à saúde/capacidade do xAI, novas sessões podem continuar chegando a uma réplica cuja região está degradada.
|
||||
|
||||
O histórico de testes do TTS já mostrou que problemas de estabelecimento de WebSocket e jitter podem se manifestar de forma regional e em função de carga. A arquitetura proposta transforma esses sinais em capacidade operacional do Pod.
|
||||
|
||||
## 3. Objetivos
|
||||
|
||||
A solução tem os seguintes objetivos:
|
||||
|
||||
- manter conexões OCI xAI abertas e pré-aquecidas;
|
||||
- reutilizar uma conexão entre diferentes sínteses;
|
||||
- reservar conexão upstream apenas durante uma utterance;
|
||||
- liberar a conexão imediatamente após `audio.done`;
|
||||
- impedir bursts de abertura de WebSockets no caminho crítico da chamada;
|
||||
- permitir TIA ativo/ativo em múltiplas regiões;
|
||||
- retirar automaticamente uma réplica saturada do balanceamento de novas conexões;
|
||||
- manter chamadas existentes durante draining/rollout;
|
||||
- renovar sockets antes do TTL de forma escalonada, evitando reconexão simultânea;
|
||||
- preservar o protocolo atual do `OraclexAITTS` e minimizar mudanças no código de voz;
|
||||
- permitir escalabilidade horizontal em Kubernetes.
|
||||
|
||||
## 4. Arquitetura proposta
|
||||
|
||||
```text
|
||||
Clientes / Telefonia
|
||||
|
|
||||
v
|
||||
Load Balancer / Service
|
||||
(novas conexões WebSocket)
|
||||
|
|
||||
+--------------------+--------------------+
|
||||
| |
|
||||
v v
|
||||
Deployment ORD Deployment IAD
|
||||
| |
|
||||
+--------+--------+ +--------+--------+
|
||||
| Pod TIA ORD | | Pod TIA IAD |
|
||||
| | | |
|
||||
| bridge | | bridge |
|
||||
| agent/livekit | | agent/livekit |
|
||||
| | | | | |
|
||||
| v | | v |
|
||||
| xAI pool sidecar| | xAI pool sidecar|
|
||||
| 50 WS warm | | 50 WS warm |
|
||||
+-------+---------+ +-------+---------+
|
||||
| |
|
||||
v v
|
||||
OCI xAI ORD OCI xAI IAD
|
||||
```
|
||||
|
||||
O mesmo `Service` Kubernetes seleciona Pods ORD e IAD. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, mas ambos compartilham `app=${APP_NAME}-regional`.
|
||||
|
||||
## 5. Componentes
|
||||
|
||||
### 5.1 Bridge TIA
|
||||
|
||||
Mantém o WebSocket de entrada e o comportamento existente do TIA. O código principal não precisa conhecer o endpoint xAI regional.
|
||||
|
||||
### 5.2 LiveKit Agent
|
||||
|
||||
Continua instanciando `OraclexAITTS`, porém passa a usar:
|
||||
|
||||
```text
|
||||
XAI_WEBSOCKET_URL=ws://127.0.0.1:18100/xai/v1/tts
|
||||
```
|
||||
|
||||
O adapter continua enviando:
|
||||
|
||||
```text
|
||||
text.clear
|
||||
text.delta
|
||||
text.done
|
||||
```
|
||||
|
||||
e recebendo:
|
||||
|
||||
```text
|
||||
audio.clear
|
||||
audio.delta
|
||||
audio.done
|
||||
```
|
||||
|
||||
Portanto, a lógica atual de streaming, TTFB, gaps, underflow e prevenção de replay permanece válida.
|
||||
|
||||
### 5.3 Sidecar `xai-pool`
|
||||
|
||||
Implementado em:
|
||||
|
||||
```text
|
||||
src/app/livekit/adapters/xai_pool_proxy.py
|
||||
```
|
||||
|
||||
Responsabilidades:
|
||||
|
||||
- abrir `XAI_POOL_SIZE` WebSockets no startup;
|
||||
- usar abertura em ondas controladas (`XAI_POOL_PREWARM_CONCURRENCY`);
|
||||
- manter os sockets vivos;
|
||||
- recuperar automaticamente slots desconectados;
|
||||
- renovar sockets antes do TTL;
|
||||
- aplicar jitter no refresh para não reconectar todos simultaneamente;
|
||||
- emprestar uma conexão a uma utterance;
|
||||
- devolver a conexão ao pool depois de `audio.done`;
|
||||
- expor health, readiness, status, drain e métricas.
|
||||
|
||||
### 5.4 OCI xAI regional
|
||||
|
||||
Cada deployment recebe seu endpoint próprio por `XAI_POOL_UPSTREAM_URL`.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
ORD -> wss://...us-chicago-1.../xai/v1/tts
|
||||
IAD -> wss://...us-ashburn-1.../xai/v1/tts
|
||||
```
|
||||
|
||||
As credenciais reais ficam somente no sidecar de pool. O container `agent` usa uma credencial local dummy porque se conecta apenas a `localhost`.
|
||||
|
||||
### 5.5 Kubernetes Service / Load Balancer
|
||||
|
||||
O Service seleciona todas as réplicas regionais. O WebSocket funciona normalmente através do LB: o balanceador escolhe um Pod durante o HTTP Upgrade e aquela conexão permanece no mesmo backend durante sua vida.
|
||||
|
||||
A estratégia de capacidade não tenta migrar uma conexão existente. Ela afeta **novas conexões**.
|
||||
|
||||
## 6. Pool compartilhado por Pod
|
||||
|
||||
Uma chamada não reserva um socket xAI por toda sua duração.
|
||||
|
||||
```text
|
||||
Call A falando ---------- sem TTS
|
||||
Call B ouvindo ---------- sem nova síntese
|
||||
Call C precisa falar ---- acquire WS #17
|
||||
text.clear
|
||||
text.delta
|
||||
text.done
|
||||
audio.delta...
|
||||
audio.done
|
||||
release WS #17
|
||||
```
|
||||
|
||||
Portanto, 200 chamadas podem coexistir em um Pod com pool de 50, desde que não existam mais de 50 sínteses concorrentes naquele instante.
|
||||
|
||||
Essa é a principal diferença entre dimensionar por **chamadas simultâneas** e por **utterances TTS simultâneas**.
|
||||
|
||||
## 7. Readiness orientada à capacidade
|
||||
|
||||
O sidecar expõe:
|
||||
|
||||
```text
|
||||
GET /healthz
|
||||
GET /readyz
|
||||
GET /pool/status
|
||||
GET /metrics
|
||||
POST /drain
|
||||
```
|
||||
|
||||
`/healthz` responde se o processo está vivo.
|
||||
|
||||
`/readyz` responde se o Pod deve aceitar **novas chamadas**.
|
||||
|
||||
Exemplo padrão:
|
||||
|
||||
```text
|
||||
XAI_POOL_SIZE=50
|
||||
XAI_POOL_UNAVAILABLE_FREE=2
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Com o Pod inicialmente pronto:
|
||||
|
||||
```text
|
||||
free > 2 -> ready
|
||||
free <= 2 -> not ready (HTTP 503)
|
||||
```
|
||||
|
||||
Depois de sair da rotação, só retorna quando:
|
||||
|
||||
```text
|
||||
free >= 5 -> ready novamente
|
||||
```
|
||||
|
||||
Isso cria histerese e evita flapping de readiness em torno do limite.
|
||||
|
||||
Para a política estrita sugerida de somente sair quando todas estiverem ocupadas:
|
||||
|
||||
```text
|
||||
XAI_POOL_UNAVAILABLE_FREE=0
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Em produção recomenda-se uma pequena reserva (por exemplo 2 a 5 conexões), pois ela absorve rajadas e reduz a chance de uma sessão recém-chegada não encontrar capacidade.
|
||||
|
||||
## 8. Como o LB redireciona tráfego
|
||||
|
||||
Em Kubernetes, um Pod é Ready apenas quando todos os containers que possuem readiness probe estão Ready.
|
||||
|
||||
O sidecar `xai-pool` possui uma probe em `/readyz`.
|
||||
|
||||
Quando a capacidade acaba:
|
||||
|
||||
```text
|
||||
xai-pool /readyz -> 503
|
||||
|
|
||||
v
|
||||
Pod becomes NotReady
|
||||
|
|
||||
v
|
||||
Pod removed from Service Endpoints
|
||||
|
|
||||
v
|
||||
LB stops sending NEW connections
|
||||
```
|
||||
|
||||
As conexões WebSocket já estabelecidas não são redirecionadas e continuam no Pod enquanto o processo continuar disponível.
|
||||
|
||||
## 9. Renovação escalonada das conexões
|
||||
|
||||
Manter 50 sockets abertos indefinidamente sem renovação é arriscado porque serviços upstream normalmente aplicam TTL e renovação de autorização.
|
||||
|
||||
A configuração padrão usa:
|
||||
|
||||
```text
|
||||
XAI_POOL_CONNECTION_TTL_S=540
|
||||
XAI_POOL_REFRESH_JITTER_S=45
|
||||
```
|
||||
|
||||
Cada conexão recebe um refresh deadline diferente:
|
||||
|
||||
```text
|
||||
WS01 -> ~501s
|
||||
WS02 -> ~527s
|
||||
WS03 -> ~509s
|
||||
...
|
||||
```
|
||||
|
||||
Somente conexões livres são renovadas. Isso evita um evento no qual 50 conexões expiram e fazem handshake simultaneamente.
|
||||
|
||||
## 10. Alta disponibilidade regional
|
||||
|
||||
Operação normal:
|
||||
|
||||
```text
|
||||
LB
|
||||
|-- ORD Pod 1 -> ready
|
||||
|-- ORD Pod 2 -> ready
|
||||
|-- IAD Pod 1 -> ready
|
||||
`-- IAD Pod 2 -> ready
|
||||
```
|
||||
|
||||
Se ORD perder saúde/capacidade xAI, os slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods ORD saem dos endpoints e novas chamadas passam a ser atendidas pelos Pods IAD disponíveis.
|
||||
|
||||
Não há necessidade de alterar o cliente ou o LiveKit para escolher a região.
|
||||
|
||||
## 11. Escala horizontal
|
||||
|
||||
A capacidade teórica de pool é:
|
||||
|
||||
```text
|
||||
capacidade regional de sockets = replicas_region * XAI_POOL_SIZE
|
||||
```
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
ORD: 2 pods x 25 sockets = 50
|
||||
IAD: 2 pods x 25 sockets = 50
|
||||
TOTAL = 100 sockets prewarmed
|
||||
```
|
||||
|
||||
ou, se a OCI conceder capacidade independente suficiente:
|
||||
|
||||
```text
|
||||
ORD: 2 pods x 50 = 100
|
||||
IAD: 2 pods x 50 = 100
|
||||
TOTAL = 200
|
||||
```
|
||||
|
||||
### Restrição crítica
|
||||
|
||||
`replicas * XAI_POOL_SIZE` **não pode ultrapassar o limite real concedido pela OCI para o endpoint/tenancy/região**.
|
||||
|
||||
Se a OCI disser que o limite 50 é global por endpoint, então duas réplicas de 50 no mesmo endpoint seriam incorretas. Nesse caso use, por exemplo:
|
||||
|
||||
```text
|
||||
2 replicas x 25 = 50 total
|
||||
```
|
||||
|
||||
ou obtenha endpoints/capacidades independentes.
|
||||
|
||||
## 12. Escala visual
|
||||
|
||||
```text
|
||||
Carga baixa
|
||||
=========
|
||||
LB
|
||||
|-- ORD-1 [pool 50: 10 leased / 40 free]
|
||||
`-- IAD-1 [pool 50: 8 leased / 42 free]
|
||||
|
||||
Carga aumenta
|
||||
=============
|
||||
LB
|
||||
|-- ORD-1 [47 leased / 3 free] READY
|
||||
`-- IAD-1 [30 leased /20 free] READY
|
||||
|
||||
ORD satura
|
||||
===========
|
||||
LB
|
||||
|-- ORD-1 [48 leased /2 free] NOT READY -> sem novas chamadas
|
||||
`-- IAD-1 [31 leased /19 free] READY -> recebe novas chamadas
|
||||
|
||||
ORD recupera
|
||||
=============
|
||||
ORD-1 chega a 45 leased /5 free
|
||||
/readyz volta a 200
|
||||
LB volta a considerá-lo para novas conexões
|
||||
```
|
||||
|
||||
## 13. Draining e rollout
|
||||
|
||||
O deployment usa:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 0
|
||||
maxSurge: 1
|
||||
```
|
||||
|
||||
O sidecar executa no `preStop`:
|
||||
|
||||
```text
|
||||
POST /drain
|
||||
```
|
||||
|
||||
Isso torna `/readyz` imediatamente 503, removendo o Pod da entrada de novas conexões antes de sua finalização.
|
||||
|
||||
`terminationGracePeriodSeconds` deve ser compatível com a duração/grace desejada para as chamadas existentes.
|
||||
|
||||
## 14. Segurança
|
||||
|
||||
As credenciais OCI/xAI reais ficam no Secret indicado por `XAI_SECRET_NAME` e são montadas apenas no sidecar.
|
||||
|
||||
O agent conecta a localhost e não precisa conhecer a chave real do xAI.
|
||||
|
||||
Para produção recomenda-se evoluir para `OKE_WORKLOAD_IDENTITY` sempre que suportado pela política do ambiente, eliminando API keys estáticas.
|
||||
|
||||
## 15. Observabilidade
|
||||
|
||||
`GET /pool/status` retorna:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ready",
|
||||
"region": "ord",
|
||||
"configured": 50,
|
||||
"healthy": 50,
|
||||
"leased": 17,
|
||||
"free": 33,
|
||||
"total_acquires": 845,
|
||||
"total_acquire_timeouts": 0,
|
||||
"total_proxy_failures": 0
|
||||
}
|
||||
```
|
||||
|
||||
`GET /metrics` expõe métricas Prometheus simples:
|
||||
|
||||
- `tia_xai_pool_connections{state="healthy"}`
|
||||
- `tia_xai_pool_connections{state="leased"}`
|
||||
- `tia_xai_pool_connections{state="free"}`
|
||||
- `tia_xai_pool_acquires_total`
|
||||
- `tia_xai_pool_acquire_timeouts_total`
|
||||
- `tia_xai_pool_proxy_failures_total`
|
||||
|
||||
Estas métricas devem ser correlacionadas com as métricas já existentes no TIA, especialmente TTFB, gap e underflow.
|
||||
|
||||
## 16. O que esta versão resolve
|
||||
|
||||
| Problema | Solução |
|
||||
|---|---|
|
||||
| handshakes xAI no caminho crítico | pool pré-aquecido |
|
||||
| burst de WebSockets | prewarm em ondas + refresh com jitter |
|
||||
| conexão presa a uma chamada | lease somente durante utterance |
|
||||
| LB envia tráfego a Pod sem capacidade TTS | `/readyz` baseado no pool |
|
||||
| flapping de health | histerese unavailable/recover |
|
||||
| indisponibilidade regional | Deployments ORD/IAD no mesmo Service |
|
||||
| rollout derruba novas sessões | drain + RollingUpdate |
|
||||
| chave xAI em todos os processos | credencial real somente no sidecar |
|
||||
| observabilidade de capacidade | `/pool/status` + `/metrics` |
|
||||
|
||||
## 17. O que esta versão não resolve sozinha
|
||||
|
||||
A solução não cria capacidade de inferência no OCI xAI. Se todos os endpoints regionais terminarem no mesmo pool de inferência saturado, o TIA terá failover e melhor utilização de sockets, mas não multiplicará a capacidade real do modelo.
|
||||
|
||||
É necessário confirmar com a OCI:
|
||||
|
||||
1. limite de WebSockets por endpoint/região/tenancy;
|
||||
2. se endpoints dedicados possuem capacidade independente;
|
||||
3. se API servers/LB e inference workers são dedicados ou compartilhados;
|
||||
4. quais limites podem ser reservados/negociados para a TIM.
|
||||
32
docs/regional/CHANGELOG_IMPLEMENTACAO.md
Normal file
32
docs/regional/CHANGELOG_IMPLEMENTACAO.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Changelog — TIA Regional / xAI Pool
|
||||
|
||||
## Implementação adicionada
|
||||
|
||||
- sidecar `xai_pool_proxy` compatível com o protocolo WebSocket xAI usado pelo TIA;
|
||||
- pool pré-aquecido configurável por Pod (`XAI_POOL_SIZE`, default de exemplo 50);
|
||||
- acquire por utterance após `text.clear`;
|
||||
- release automático após `audio.done`;
|
||||
- recuperação de slots desconectados;
|
||||
- refresh escalonado por TTL + jitter;
|
||||
- prewarm em ondas para evitar burst de handshake;
|
||||
- `/healthz`, `/readyz`, `/pool/status`, `/metrics` e `/drain`;
|
||||
- readiness com histerese;
|
||||
- Deployments regionais ORD/IAD usando a mesma imagem;
|
||||
- credencial xAI real isolada no sidecar;
|
||||
- Service único para balancear Pods regionais;
|
||||
- RollingUpdate, PDB e HPA;
|
||||
- scripts de renderização, validação e deployment;
|
||||
- manuais de arquitetura, implantação e testes.
|
||||
|
||||
## Compatibilidade
|
||||
|
||||
O modo anterior permanece disponível. O deployment regional é opcional e não remove `XAI_WEBSOCKET_URL` direto para OCI xAI.
|
||||
|
||||
## Validações executadas na geração
|
||||
|
||||
- `py_compile` / `compileall` do novo sidecar: PASS;
|
||||
- parsing YAML dos templates Kubernetes: PASS;
|
||||
- renderização ORD/IAD via `envsubst`: PASS;
|
||||
- parsing YAML dos manifests renderizados: PASS.
|
||||
|
||||
Não foi executado teste real contra OCI xAI, pois depende das credenciais/endpoints do ambiente TIM/OCI. O manual descreve smoke, saturação, failover e stress test a executar em FQA.
|
||||
385
docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md
Normal file
385
docs/regional/DEPLOYMENT_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# Deployment — TIA Regional com Pool xAI
|
||||
|
||||
## 1. Pré-requisitos
|
||||
|
||||
- cluster Kubernetes/OKE funcional;
|
||||
- `kubectl` configurado;
|
||||
- `envsubst` (`gettext`) instalado na máquina de deployment;
|
||||
- imagem TIA construída a partir desta versão;
|
||||
- ConfigMap `${APP_NAME}-config` já existente;
|
||||
- Secrets `${APP_NAME}-api-secrets`, `${APP_NAME}-google-sa-secret` e `shared-tls-secret` já existentes conforme deployment atual;
|
||||
- endpoint e credencial xAI de cada região;
|
||||
- quota xAI validada para o número total de WebSockets configurado.
|
||||
|
||||
## 2. Arquivos adicionados
|
||||
|
||||
```text
|
||||
src/app/livekit/adapters/xai_pool_proxy.py
|
||||
|
||||
k8s/regional/
|
||||
deployment-region.yaml
|
||||
service.yaml
|
||||
pdb.yaml
|
||||
hpa-region.yaml
|
||||
xai-secret.example.yaml
|
||||
regions.env.example
|
||||
|
||||
scripts/
|
||||
render-regional-k8s.sh
|
||||
validate-regional-k8s.sh
|
||||
deploy-regional-k8s.sh
|
||||
```
|
||||
|
||||
## 3. Construir a imagem
|
||||
|
||||
Use o pipeline existente ou Dockerfile atual do TIA. O novo sidecar utiliza a mesma imagem e apenas muda o módulo executado:
|
||||
|
||||
```text
|
||||
python -m app.livekit.adapters.xai_pool_proxy
|
||||
```
|
||||
|
||||
Exemplo local:
|
||||
|
||||
```bash
|
||||
docker build -f k8s/tia/Dockerfile \
|
||||
-t iad.ocir.io/<namespace>/tia:regional-xai-pool-v1 .
|
||||
```
|
||||
|
||||
Faça push para o registry usado pelo cluster.
|
||||
|
||||
## 4. Criar arquivo de ambiente de deployment
|
||||
|
||||
```bash
|
||||
cp k8s/regional/regions.env.example k8s/regional/regions.env
|
||||
```
|
||||
|
||||
Edite no mínimo:
|
||||
|
||||
```bash
|
||||
APP_NAME=tim-ai-atend-agnt-integ-tia
|
||||
K8S_NAMESPACE=...
|
||||
IMAGE_REPOSITORY=...
|
||||
IMAGE_TAG=...
|
||||
|
||||
ORD_XAI_UPSTREAM_URL=wss://...us-chicago-1.../xai/v1/tts
|
||||
ORD_XAI_SECRET_NAME=xai-ord-credentials
|
||||
|
||||
IAD_XAI_UPSTREAM_URL=wss://...us-ashburn-1.../xai/v1/tts
|
||||
IAD_XAI_SECRET_NAME=xai-iad-credentials
|
||||
```
|
||||
|
||||
## 5. Definir tamanho do pool
|
||||
|
||||
Para um Pod com 50 sockets:
|
||||
|
||||
```bash
|
||||
XAI_POOL_SIZE=50
|
||||
```
|
||||
|
||||
Readiness recomendada:
|
||||
|
||||
```bash
|
||||
XAI_POOL_UNAVAILABLE_FREE=2
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Se desejar exatamente o comportamento "só sair quando os 50 estiverem ocupados":
|
||||
|
||||
```bash
|
||||
XAI_POOL_UNAVAILABLE_FREE=0
|
||||
XAI_POOL_RECOVER_FREE=5
|
||||
```
|
||||
|
||||
### Importante
|
||||
|
||||
Se houver `N` réplicas apontando para o mesmo endpoint:
|
||||
|
||||
```text
|
||||
sockets máximos = N * XAI_POOL_SIZE
|
||||
```
|
||||
|
||||
Nunca configure isso acima da quota xAI real.
|
||||
|
||||
## 6. Criar Secrets regionais
|
||||
|
||||
Não versione chaves reais.
|
||||
|
||||
Exemplo por linha de comando:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" create secret generic xai-ord-credentials \
|
||||
--from-literal=XAI_API_KEY='<ORD_KEY>' \
|
||||
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
|
||||
|
||||
kubectl -n "$K8S_NAMESPACE" create secret generic xai-iad-credentials \
|
||||
--from-literal=XAI_API_KEY='<IAD_KEY>' \
|
||||
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
|
||||
```
|
||||
|
||||
Para `API_KEY`, `OCI_COMPARTMENT_ID` pode ficar vazio se o fluxo upstream não o exigir.
|
||||
|
||||
Para produção, prefira OCI Vault/External Secrets/Workload Identity em vez de gravar chaves no repositório.
|
||||
|
||||
## 7. Renderizar manifests
|
||||
|
||||
```bash
|
||||
./scripts/render-regional-k8s.sh
|
||||
```
|
||||
|
||||
Saída:
|
||||
|
||||
```text
|
||||
k8s/regional/rendered/
|
||||
deployment-ord.yaml
|
||||
deployment-iad.yaml
|
||||
hpa-ord.yaml
|
||||
hpa-iad.yaml
|
||||
service.yaml
|
||||
pdb.yaml
|
||||
```
|
||||
|
||||
É possível escolher outro arquivo e diretório:
|
||||
|
||||
```bash
|
||||
./scripts/render-regional-k8s.sh ./minha-config.env /tmp/tia-regional
|
||||
```
|
||||
|
||||
## 8. Validar sem implantar
|
||||
|
||||
```bash
|
||||
./scripts/validate-regional-k8s.sh
|
||||
```
|
||||
|
||||
O script usa:
|
||||
|
||||
```bash
|
||||
kubectl apply --dry-run=client
|
||||
```
|
||||
|
||||
Revise também:
|
||||
|
||||
```bash
|
||||
kubectl diff -f k8s/regional/rendered/
|
||||
```
|
||||
|
||||
## 9. Implantar
|
||||
|
||||
```bash
|
||||
./scripts/deploy-regional-k8s.sh
|
||||
```
|
||||
|
||||
Ou manualmente:
|
||||
|
||||
```bash
|
||||
kubectl apply -f k8s/regional/rendered/service.yaml
|
||||
kubectl apply -f k8s/regional/rendered/pdb.yaml
|
||||
kubectl apply -f k8s/regional/rendered/deployment-ord.yaml
|
||||
kubectl apply -f k8s/regional/rendered/deployment-iad.yaml
|
||||
kubectl apply -f k8s/regional/rendered/hpa-ord.yaml
|
||||
kubectl apply -f k8s/regional/rendered/hpa-iad.yaml
|
||||
```
|
||||
|
||||
## 10. Verificar rollout
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get pods -l app=${APP_NAME}-regional -o wide
|
||||
```
|
||||
|
||||
Todos os containers devem ficar Ready:
|
||||
|
||||
```text
|
||||
bridge 1/1
|
||||
agent 1/1
|
||||
xai-pool 1/1
|
||||
```
|
||||
|
||||
Confira os deployments:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-ord
|
||||
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-iad
|
||||
```
|
||||
|
||||
## 11. Validar o pool
|
||||
|
||||
Escolha um Pod:
|
||||
|
||||
```bash
|
||||
POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
|
||||
-l app=${APP_NAME}-regional,tia-region=ord \
|
||||
-o jsonpath='{.items[0].metadata.name}')
|
||||
```
|
||||
|
||||
Port-forward:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" port-forward "$POD" 18100:18100
|
||||
```
|
||||
|
||||
Em outro terminal:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:18100/healthz | jq
|
||||
curl -s http://127.0.0.1:18100/readyz | jq
|
||||
curl -s http://127.0.0.1:18100/pool/status | jq
|
||||
curl -s http://127.0.0.1:18100/metrics
|
||||
```
|
||||
|
||||
Resultado esperado após prewarm:
|
||||
|
||||
```json
|
||||
{
|
||||
"region": "ord",
|
||||
"configured": 50,
|
||||
"healthy": 50,
|
||||
"leased": 0,
|
||||
"free": 50,
|
||||
"status": "ready"
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Logs
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" logs "$POD" -c xai-pool -f
|
||||
```
|
||||
|
||||
Eventos relevantes:
|
||||
|
||||
```text
|
||||
XAI_POOL_SLOT_OPENED
|
||||
XAI_POOL_PREWARM_FAILED
|
||||
XAI_POOL_SLOT_RECOVERY_FAILED
|
||||
XAI_POOL_STARTED
|
||||
```
|
||||
|
||||
## 13. Testar saturação/readiness
|
||||
|
||||
Objetivo: provar que o Pod sai da rotação para novas sessões quando o pool atinge o limite.
|
||||
|
||||
1. Gere sínteses concorrentes suficientes para ocupar o pool.
|
||||
2. Observe:
|
||||
|
||||
```bash
|
||||
watch -n 1 'curl -s http://127.0.0.1:18100/pool/status | jq'
|
||||
```
|
||||
|
||||
3. Quando `free <= XAI_POOL_UNAVAILABLE_FREE`, espere:
|
||||
|
||||
```bash
|
||||
curl -i http://127.0.0.1:18100/readyz
|
||||
```
|
||||
|
||||
Resultado:
|
||||
|
||||
```text
|
||||
HTTP/1.1 503 Service Unavailable
|
||||
```
|
||||
|
||||
4. Verifique o Pod:
|
||||
|
||||
```bash
|
||||
kubectl get pod "$POD"
|
||||
```
|
||||
|
||||
Ele deve ficar `NotReady` enquanto o sidecar estiver sem capacidade.
|
||||
|
||||
5. Quando conexões forem liberadas e `free >= XAI_POOL_RECOVER_FREE`, o `/readyz` volta a 200 e o Pod retorna aos endpoints.
|
||||
|
||||
## 14. Verificar endpoints do Service
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get endpoints ${APP_NAME}-regional -o wide
|
||||
```
|
||||
|
||||
ou, em clusters novos:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" get endpointslice \
|
||||
-l kubernetes.io/service-name=${APP_NAME}-regional -o yaml
|
||||
```
|
||||
|
||||
Um Pod NotReady não deve ser usado para novas conexões do Service.
|
||||
|
||||
## 15. Testar failover regional
|
||||
|
||||
### Teste controlado de ORD
|
||||
|
||||
Coloque ORD em drain:
|
||||
|
||||
```bash
|
||||
ORD_POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
|
||||
-l app=${APP_NAME}-regional,tia-region=ord \
|
||||
-o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
kubectl -n "$K8S_NAMESPACE" exec "$ORD_POD" -c xai-pool -- \
|
||||
python -c "import urllib.request; urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:18100/drain', method='POST')).read()"
|
||||
```
|
||||
|
||||
Confirme `/readyz=503` e gere **nova chamada**. Ela deve ser entregue a um Pod IAD ainda Ready.
|
||||
|
||||
Esse teste não deve ser interpretado como migração de uma chamada existente; somente novas conexões são rebalanceadas.
|
||||
|
||||
## 16. Testar recuperação
|
||||
|
||||
O drain manual é intencional e não é revertido. Para voltar o Pod, reinicie-o:
|
||||
|
||||
```bash
|
||||
kubectl -n "$K8S_NAMESPACE" delete pod "$ORD_POD"
|
||||
```
|
||||
|
||||
O novo Pod deve:
|
||||
|
||||
1. iniciar sidecar;
|
||||
2. pré-aquecer sockets;
|
||||
3. atingir `recover_free_threshold`;
|
||||
4. ficar Ready;
|
||||
5. entrar novamente nos endpoints.
|
||||
|
||||
## 17. Load Balancer e WebSocket
|
||||
|
||||
WebSocket é suportado porque a conexão começa como HTTP Upgrade. O LB escolhe o backend no handshake e mantém a conexão naquele Pod.
|
||||
|
||||
Não use sticky session como requisito de correção. A conexão WebSocket em si já é persistente ao backend selecionado. Em caso de perda do Pod, o cliente precisa reconectar.
|
||||
|
||||
Uma política equivalente a least-connections pode ser útil quando o LB permite configuração, mas o mecanismo primário de proteção desta arquitetura é a readiness baseada em capacidade real do TTS.
|
||||
|
||||
## 18. HPA
|
||||
|
||||
Os manifests incluem HPA por CPU como proteção inicial.
|
||||
|
||||
Atenção: aumentar réplicas também aumenta o número de sockets xAI pré-aquecidos. Portanto, HPA só deve ter `maxReplicas` maior que 1 se a quota xAI permitir:
|
||||
|
||||
```text
|
||||
HPA_MAX_REPLICAS * XAI_POOL_SIZE <= quota regional permitida
|
||||
```
|
||||
|
||||
Para evolução futura, prefira uma métrica customizada que considere chamadas/TTS ativos e também um controlador que respeite orçamento global de sockets.
|
||||
|
||||
## 19. Rollback
|
||||
|
||||
Para voltar ao modelo anterior:
|
||||
|
||||
1. redirecione o Service/LB para o deployment antigo;
|
||||
2. ou restaure o manifesto `k8s/tia/deployment.yaml` original;
|
||||
3. o código antigo `OraclexAITTS` continua presente e compatível com endpoint xAI direto.
|
||||
|
||||
A alteração não remove suporte ao modo anterior.
|
||||
|
||||
## 20. Checklist de produção
|
||||
|
||||
- [ ] confirmar quota real de WebSockets por endpoint/região/tenancy;
|
||||
- [ ] confirmar se ORD e IAD têm pools de capacidade independentes;
|
||||
- [ ] validar `XAI_POOL_SIZE * replicas` por região;
|
||||
- [ ] validar Secret/Vault;
|
||||
- [ ] medir tempo de prewarm dos 50 sockets;
|
||||
- [ ] observar taxa de erro de handshake;
|
||||
- [ ] validar refresh escalonado em janela > 10 minutos;
|
||||
- [ ] testar saturation -> `/readyz=503`;
|
||||
- [ ] testar recovery -> `/readyz=200`;
|
||||
- [ ] testar drain durante chamada ativa;
|
||||
- [ ] testar rollout sem perda de novas chamadas;
|
||||
- [ ] testar perda total de ORD e entrada em IAD;
|
||||
- [ ] correlacionar TTFB/gap/underflow com `pool_free`;
|
||||
- [ ] validar LB/Service com WebSocket real;
|
||||
- [ ] executar stress test com perfil semelhante ao tráfego de produção.
|
||||
151
docs/regional/TESTES_TIA_XAI_REGIONAL.md
Normal file
151
docs/regional/TESTES_TIA_XAI_REGIONAL.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# Plano de Testes — TIA Regional / Pool xAI
|
||||
|
||||
## Objetivo
|
||||
|
||||
Validar separadamente capacidade do pool, comportamento do Kubernetes, failover regional, impacto de latência e resiliência do xAI.
|
||||
|
||||
## Camada 1 — Smoke
|
||||
|
||||
1. subir 1 Pod ORD com pool pequeno (`XAI_POOL_SIZE=3`);
|
||||
2. confirmar 3 conexões healthy;
|
||||
3. realizar uma chamada e uma síntese;
|
||||
4. confirmar que `leased` vai 0 -> 1 -> 0;
|
||||
5. confirmar que o socket upstream continua healthy após `audio.done`.
|
||||
|
||||
## Camada 2 — Prewarm
|
||||
|
||||
Subir com `XAI_POOL_SIZE=50` e medir:
|
||||
|
||||
- tempo total até Ready;
|
||||
- taxa de sucesso de handshake;
|
||||
- número máximo de handshakes simultâneos;
|
||||
- impacto de `XAI_POOL_PREWARM_CONCURRENCY` 2, 5 e 10.
|
||||
|
||||
Critério inicial: nenhuma rajada deve reproduzir os erros de conexão observados no modelo burst.
|
||||
|
||||
## Camada 3 — Saturação
|
||||
|
||||
Gerar mais utterances concorrentes que slots.
|
||||
|
||||
Esperado:
|
||||
|
||||
- leases nunca superam `XAI_POOL_SIZE`;
|
||||
- acquire espera até `XAI_POOL_ACQUIRE_TIMEOUT_S`;
|
||||
- readiness fica 503 quando free cruza threshold;
|
||||
- novas chamadas deixam de entrar naquele Pod;
|
||||
- chamadas existentes continuam.
|
||||
|
||||
## Camada 4 — Histerese
|
||||
|
||||
Com size=10:
|
||||
|
||||
```text
|
||||
UNAVAILABLE_FREE=2
|
||||
RECOVER_FREE=5
|
||||
```
|
||||
|
||||
Esperado:
|
||||
|
||||
- free=2 -> NotReady;
|
||||
- free=3/4 -> continua NotReady;
|
||||
- free=5 -> Ready.
|
||||
|
||||
## Camada 5 — Refresh
|
||||
|
||||
Use TTL curto em FQA:
|
||||
|
||||
```text
|
||||
XAI_POOL_CONNECTION_TTL_S=60
|
||||
XAI_POOL_REFRESH_JITTER_S=20
|
||||
```
|
||||
|
||||
Observe por 5 minutos.
|
||||
|
||||
Esperado:
|
||||
|
||||
- sockets são renovados individualmente;
|
||||
- não existe burst de 50 reconnects;
|
||||
- slots ocupados não são renovados no meio da síntese;
|
||||
- pool retorna ao tamanho configurado.
|
||||
|
||||
## Camada 6 — Falha upstream
|
||||
|
||||
Bloqueie ORD ou aponte temporariamente para endpoint inválido.
|
||||
|
||||
Esperado:
|
||||
|
||||
- slots ORD tornam-se unhealthy;
|
||||
- maintenance tenta recuperação;
|
||||
- readiness ORD cai;
|
||||
- Service deixa de enviar novas chamadas a ORD;
|
||||
- IAD continua Ready.
|
||||
|
||||
## Camada 7 — Latência
|
||||
|
||||
Compare três cenários:
|
||||
|
||||
A. TIA -> xAI direto sem pool
|
||||
|
||||
B. TIA -> localhost pool -> xAI com socket já aquecido
|
||||
|
||||
C. TIA -> localhost pool -> xAI durante recuperação/abertura de socket
|
||||
|
||||
Meça:
|
||||
|
||||
- `provider_ttfb_ms`;
|
||||
- `end_to_end_ttfb_ms`;
|
||||
- `max_audio_delta_gap_ms`;
|
||||
- `xai_underrun_estimado_ms`;
|
||||
- connect time do upstream;
|
||||
- `pool_free` e `pool_leased`.
|
||||
|
||||
Hipótese: o hop localhost adiciona latência desprezível frente ao TTFB do provider, enquanto remove handshake xAI do caminho crítico na situação normal.
|
||||
|
||||
## Camada 8 — Carga semelhante a produção
|
||||
|
||||
Evite somente burst C=200. Use sockets persistentes e concorrência de síntese representativa da operação real, seguindo a metodologia que produziu resultados reprodutíveis nos testes anteriores.
|
||||
|
||||
Rodar pelo menos:
|
||||
|
||||
```text
|
||||
20% da carga alvo
|
||||
50%
|
||||
80%
|
||||
100%
|
||||
120% por janela curta
|
||||
```
|
||||
|
||||
## Camada 9 — Rollout
|
||||
|
||||
Com chamadas ativas:
|
||||
|
||||
```bash
|
||||
kubectl rollout restart deployment/<tia-ord>
|
||||
```
|
||||
|
||||
Validar:
|
||||
|
||||
- Pod antigo entra em drain;
|
||||
- não recebe novas conexões;
|
||||
- Pod novo preaquece antes de ficar Ready;
|
||||
- `maxUnavailable=0` preserva capacidade durante rollout.
|
||||
|
||||
## Camada 10 — Failover regional
|
||||
|
||||
1. ORD e IAD Ready;
|
||||
2. iniciar tráfego contínuo;
|
||||
3. tornar ORD NotReady;
|
||||
4. verificar novas chamadas em IAD;
|
||||
5. recuperar ORD;
|
||||
6. verificar reentrada progressiva.
|
||||
|
||||
## Evidências a guardar
|
||||
|
||||
- logs do sidecar;
|
||||
- `/pool/status` em intervalos de 1s;
|
||||
- métricas Prometheus;
|
||||
- logs TIA de TTFB/underflow;
|
||||
- quantidade de endpoints Ready por região;
|
||||
- distribuição de chamadas por Pod;
|
||||
- erros de WebSocket upstream;
|
||||
- timestamps de `audio.done`.
|
||||
Reference in New Issue
Block a user