Distribuire Potato su un server
Eseguire Potato in produzione dietro nginx con TLS, in Docker o su una VM, e perché un server Potato gira come un unico processo invece che con più worker.
Eseguite un processo Potato per progetto, mettete davanti nginx e un certificato TLS e, quando serve più concorrenza, aumentate i thread invece dei processi worker. Un secondo processo worker perde annotazioni senza segnalarlo.
Potato tiene in memoria, dentro il processo server, il pool degli item, la coda di assegnazione e lo stato di ogni annotatore. Questo solo fatto decide gran parte di un deployment. Lo stato interno al processo è il motivo per cui un server Potato scala con i thread e non con i worker, per cui la directory di output delle annotazioni è ciò di cui fare il backup, e per cui il bilanciamento del carico tra più processi Potato corrompe uno studio invece di accelerarlo. Questa guida tratta i tre modi di eseguire un server, il reverse proxy e TLS che gli stanno davanti e le chiavi di configurazione che contano quando ci lavorano annotatori reali.
Perché un server Potato gira come un unico processo
Un secondo worker gunicorn riceve una propria copia del pool degli item, della coda di assegnazione e dello stato degli utenti. Distribuisce istanze che il primo worker ha già assegnato e, poiché user_state.json viene riscritto per intero a ogni salvataggio, il worker che salva per ultimo scarta le annotazioni dell'altro. Nei log non compare nulla. Le annotazioni spariscono, e il primo segnale di solito è un annotatore che dice di aver etichettato un item che ora sembra intatto.
L'immagine Docker pubblicata di Potato impone questa regola. Esegue un solo worker gunicorn e rifiuta di avviarsi con più worker, e POTATO_ALLOW_MULTIWORKER=1 scavalca il rifiuto per tutto ciò che non è uno studio reale. La concorrenza viene invece dai thread, che condividono un'unica copia dello stato. L'immagine ne usa otto per impostazione predefinita tramite GUNICORN_THREADS, abbastanza per le decine di annotatori simultanei di uno studio tipico.
Questo vincolo delimita il resto della guida. Eseguite un processo per progetto, dategli memoria sufficiente per il dataset e usate il reverse proxy per TLS e i file statici, non per distribuire il carico.
Tre modi di eseguire un server
| Metodo | Cosa vi offre | Quando usarlo |
|---|---|---|
potato deploy | Predispone host, TLS e immagine a partire dalla vostra configurazione | Uno studio che volete avviare oggi |
| Immagine Docker | Gli stessi byte ogni volta, senza compilare dipendenze | Una macchina che già amministrate |
| VM con systemd | Controllo completo dell'host e dell'ambiente Python | Un server istituzionale con regole proprie |
potato deploy per la via rapida
potato deploy predispone un host, installa l'immagine e configura TLS a partire dal file di configurazione che avete già:
potato deploy up myproject/config.yaml --provider digitaloceanQualche minuto dopo avete un URL HTTPS da inviare agli annotatori, e potato deploy pull riporta in locale le annotazioni. Dalla 2.10.0 le destinazioni sono local, potato share e tredici host cloud, tra cui AWS, Jetstream2, Hetzner, Fly e Railway, e differiscono per il fatto che i dati sopravvivano o meno a un riavvio. Le sette destinazioni VM offrono insieme disco persistente, streaming dei log e SSH, e un host che cancella il proprio disco richiede --backup, che copia le annotazioni su un dataset HuggingFace o su un bucket S3. Eseguire Potato in locale o nel cloud le mette a confronto. Iniziate con local, che esegue lo stesso bundle e la stessa immagine senza account e senza fattura, e vi dice cosa verrebbe distribuito prima che costi qualcosa.
L'immagine Docker pubblicata
Potato pubblica un'immagine precompilata su GitHub Container Registry. Contiene il server e le sue dipendenze, e il vostro progetto viene montato su /app in fase di esecuzione, quindi una sola immagine serve per ogni task:
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestL'immagine esiste perché le dipendenze di base di Potato includono numpy, pandas, scipy e scikit-learn. Installarle su una piccola VM è lento nel migliore dei casi. Su una macchina da 1 GB la compilazione viene terminata per memoria esaurita, e su una da 2 GB richiede da cinque a dieci minuti a ogni nuovo deployment. Scaricare l'immagine richiede meno di un minuto.
Contano due tag. latest contiene le dipendenze di base e pesa circa 840 MB, e latest-all aggiunge gli SDK di IA, i formati di documento, l'esportazione e gli accessi OAuth, per circa 1,5 GB. Entrambi sono compilati per linux/amd64 e linux/arm64. Per uno studio che durerà mesi fissate una versione invece di latest, perché latest cambia senza avvisarvi.
Impostate POTATO_SECRET_KEY su tutto ciò che dura più di una singola esecuzione. Senza di essa la chiave di firma delle sessioni Flask è casuale per ogni processo, quindi ogni riavvio disconnette tutti gli annotatori. POTATO_CONFIG sceglie un percorso di configurazione dentro il container, PORT imposta la porta di ascolto (7860 per impostazione predefinita) e GUNICORN_TIMEOUT imposta il timeout del worker in secondi.
L'immagine gira come uid 1000, e un bind mount mantiene la proprietà dell'host, quindi su Linux il container spesso non può scrivere in una directory creata da voi. Il server lo verifica all'avvio e si rifiuta di partire invece di bloccarsi a metà dell'avvio. Assegnate la directory all'utente del container con sudo chown -R 1000:1000 myproject, oppure eseguite il container con la vostra identità tramite docker run --user "$(id -u):$(id -g)", che lascia anche le annotazioni di vostra proprietà. Docker Desktop su macOS e Windows ignora la proprietà nei bind mount, quindi una configurazione che funziona su un Mac può comunque fallire su un server Linux.
Installazione su una VM
Su una macchina che amministrate direttamente, installate Potato in un ambiente virtuale e lasciate che sia systemd a gestire il processo.
sudo apt update
sudo apt install python3 python3-venv python3-pip nginx certbot python3-certbot-nginx
sudo mkdir -p /opt/potato/projects
cd /opt/potato
python3 -m venv venv
source venv/bin/activate
pip install potato-annotation gunicorn
potato --versionEseguite l'app con gunicorn con un worker e più thread. La factory dell'applicazione si trova in potato.flask_server:create_app e riceve il percorso della configurazione:
gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
--chdir /opt/potato/projects/my_project \
"potato.flask_server:create_app('config.yaml')"Create /etc/systemd/system/potato.service in modo che il server riparta dopo un riavvio o un crash:
[Unit]
Description=Potato Annotation Server
After=network.target
[Service]
Type=simple
User=potato
Group=potato
WorkingDirectory=/opt/potato/projects/my_project
Environment="PATH=/opt/potato/venv/bin"
EnvironmentFile=/opt/potato/.env
ExecStart=/opt/potato/venv/bin/gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
"potato.flask_server:create_app('config.yaml')"
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetAbilitatelo e avviatelo:
sudo systemctl enable potato
sudo systemctl start potato
sudo systemctl status potatoL'EnvironmentFile contiene i segreti che non devono stare nella configurazione:
# /opt/potato/.env
POTATO_SECRET_KEY=a-long-random-value
OPENAI_API_KEY=sk-...Generate la chiave segreta una sola volta con openssl rand -hex 32 e conservatela per tutta la durata dello studio. Cambiarla invalida tutte le sessioni e disconnette tutti gli annotatori.
nginx e TLS
Potato ascolta su localhost e nginx termina TLS davanti a esso. Il limite di upload conta nei progetti con immagini e audio, dove il limite predefinito di 1 MB sul corpo della richiesta rifiuta file multimediali senza che l'annotatore veda l'errore.
server {
listen 443 ssl;
http2 on;
server_name annotation.example.com;
ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Abilitate il sito e ottenete un certificato:
sudo ln -s /etc/nginx/sites-available/potato /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d annotation.example.com
sudo systemctl enable certbot.timerChiudete tutto il resto sul firewall, perché la porta di Potato non deve essere raggiungibile direttamente una volta che c'è un proxy davanti:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enableServire più progetti sotto un unico nome host
Le istituzioni espongono spesso più server Potato attraverso un unico endpoint HTTPS pubblico, associando percorsi a porte locali. L'interfaccia di annotazione richiede le risorse e salva tramite URL relativi alla radice, quindi un server montato su /app1 deve conoscere il proprio prefisso, altrimenti il CSS restituisce 404 e i salvataggi automatici falliscono con "annotations not saved". Potato legge il prefisso dal SCRIPT_NAME WSGI, e ci sono due modi per impostarlo.
Usate POTATO_PROXY_FIX=1 quando controllate il proxy e questo può inviare X-Forwarded-Prefix. Usate POTATO_URL_PREFIX=/app1 quando la configurazione del proxy non dipende da voi. Il proxy deve comunque rimuovere il prefisso prima di inoltrare, così Flask continua a ricevere percorsi senza prefisso:
location /app1/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app1;
}ProxyFix si fida delle intestazioni inoltrate, quindi attivate POTATO_PROXY_FIX solo quando l'app è raggiungibile esclusivamente attraverso il proxy fidato. Se anche la porta interna è raggiungibile direttamente, un client può falsificare il prefisso e avvelenare gli URL generati da Potato.
La configurazione di un progetto in produzione
Quattro impostazioni distinguono una configurazione di produzione da una locale: una chiave di firma stabile, l'obbligo di password, una durata di sessione esplicita e una directory di output di cui avete un backup.
annotation_task_name: "Production Annotation Task"
task_description: "Classify each passage."
host: 127.0.0.1
port: 8000
# Sessions survive a restart only when the signing key is stable.
# Set it here, or leave it out and supply POTATO_SECRET_KEY in the environment.
require_password: true
persist_sessions: true
session_timeout_minutes: 480
data_files:
- data/items.jsonl
item_properties:
id_key: id
text_key: text
output_annotation_dir: annotation_output/production/
export_annotation_format: jsonl
user_config:
allow_all_users: false
users:
- annotator1
- annotator2
annotation_schemes:
- annotation_type: radio
name: sentiment
description: "What is the sentiment expressed in this text?"
labels:
- name: positive
- name: neutral
- name: negative
label_requirement:
required: truesession_timeout_minutes limita il tempo di inattività e non la durata del turno, perché il conteggio riparte a ogni richiesta. Il valore predefinito di 480 va bene per annotatori che lavorano una giornata intera; abbassatelo quando la macchina è condivisa. Impostate allow_all_users: false ed elencate i vostri annotatori ogni volta che il server è raggiungibile da internet, così uno sconosciuto che trova l'URL non può registrarsi e iniziare a scrivere nel vostro dataset.
Controlli di salute e log
/health è una sonda di liveness non autenticata per container e bilanciatori di carico, e di proposito non riporta quasi nulla, perché tutto ciò che dice è pubblico:
curl http://localhost:8000/health
# {"status": "ok"} once the state managers exist
# {"status": "starting"} 503 while the server is still coming upL'healthcheck di un container e il polling successivo alla creazione di un provider di deployment si basano entrambi su questa transizione. Nomi dei task, numero di item e numero di annotatori vengono invece da /admin/health, che richiede la chiave API di amministrazione.
Il logging passa dal processo, quindi systemd lo raccoglie:
sudo journalctl -u potato -fAlzate il livello dalla configurazione con verbose: true, oppure attivate le parti del logging di debug che vi servono con debug_log, che accetta all, ui, server o none. Lasciateli entrambi disattivati durante uno studio reale, perché il logging di debug è abbastanza prolisso da riempire un disco piccolo nel corso di un'esecuzione lunga.
Cosa includere nel backup
Tre elementi nella directory del progetto non sono rigenerabili. L'output delle annotazioni sotto output_annotation_dir è lo studio. project.sqlite contiene i memo, il codebook, i casi, l'indice di ricerca e il workflow di revisione. datasets.sqlite contiene i record del dataset. Tutto il resto si può ricostruire dai vostri file di dati e dalla configurazione.
#!/bin/bash
# /opt/potato/backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/opt/potato/backups
tar -czf $BACKUP_DIR/annotations_$DATE.tar.gz \
/opt/potato/projects/*/annotation_output/ \
/opt/potato/projects/*/project.sqlite \
/opt/potato/projects/*/datasets.sqlite \
/opt/potato/projects/*/config.yaml
find $BACKUP_DIR -mtime +7 -deleteEseguitelo ogni notte da crontab e copiate l'archivio fuori dalla macchina:
0 2 * * * /opt/potato/backup.shDimensionare la macchina
La memoria è la risorsa che cresce con il dataset, perché il pool degli item è tenuto nel processo. Tutto il resto viene caricato solo quando serve, quindi lo stack di embedding e sentence-transformers non viene importato all'avvio a meno che una funzionalità non ne abbia bisogno. Sul dataset di riferimento delle note di rilascio della v2.6.0, la memoria residente è scesa da circa 750 MB a circa 365 MB e l'avvio con 50.000 item da circa 10 secondi a circa 5,7 secondi.
Il costo per item è piccolo e misurabile. Un item di visione con un URL di immagine e due campi occupa circa 932 byte, e un item di testo di circa 540 caratteri unici circa 1430 byte. Un milione di item di testo corrisponde quindi a circa 1,43 GB residenti, ed è la cifra su cui fare il budget.
Per i corpora in cui questo conta, i contenuti degli item possono risiedere in un file SQLite con una piccola cache in memoria:
item_store:
backend: paged
cache_size: 2048Aggiungete questo blocco alla configurazione precedente. Leggete la misurazione prima di attivarlo. La paginazione risparmia il 28% sugli item di visione e il 51% sul testo, non un ordine di grandezza, e in cambio il caricamento del corpus richiede circa 2,5 volte il tempo. Sotto il milione di item vi dà solo un server più lento e nessun vantaggio.
Preferite una macchina con più RAM piuttosto che dividere un progetto su più server, perché la divisione è esattamente il guasto multiprocesso descritto all'inizio di questa guida. Quando una macchina davvero non riesce a contenere il corpus, suddividete invece il lavoro. L'assegnazione a lotti dà a ogni annotatore una porzione, per_annotator_quota limita quanto viene assegnato a una singola persona e una data_directory con monitoraggio della directory vi permette di aggiungere file mentre il server è in esecuzione.
Risoluzione dei problemi
La porta è già in uso. Trovate il processo e fermatelo, invece di avviare un secondo Potato su un'altra porta con la stessa directory di progetto:
sudo lsof -i :8000Permesso negato sulla directory del progetto. L'utente del servizio deve esserne il proprietario, e in Docker quell'utente è uid 1000:
sudo chown -R potato:potato /opt/potatoGli annotatori vengono disconnessi dopo ogni riavvio. Manca POTATO_SECRET_KEY o il valore di configurazione secret_key, quindi la chiave di firma viene rigenerata a ogni avvio.
Le risorse restituiscono 404 e i salvataggi automatici falliscono dietro un proxy. Il server non conosce il proprio prefisso URL. Impostate POTATO_PROXY_FIX=1 o POTATO_URL_PREFIX e verificate che il proxy rimuova il prefisso prima di inoltrare.
Letture consigliate
- Configurazione di produzione e Reverse proxy su questo sito
- Running Potato in Docker per il riferimento sull'immagine
- Scaling and Large Datasets per le misurazioni alla base dei consigli di dimensionamento
- Condurre studi di annotazione su Prolific una volta che il server è raggiungibile