إعداد الإنتاج
انشر Potato في الإنتاج باستخدام gunicorn وnginx أو Apache، مع إنهاء TLS وDocker واستمرارية الجلسات والتحكم في الوصول الذي يحتاجه خادم مشترك.
انشر Potato لمهام التعليق التوضيحي الفعلية مع مستخدمين متعددين واستمرارية البيانات والأمان المناسب.
إعداد الخادم
إعداد الإنتاج الأساسي
server:
host: 0.0.0.0
port: 8000
debug: falseتقبل كتلة server المفاتيح host وport وdebug فقط. أما عدد العمال والمهلات الزمنية فتخص خادم WSGI الذي يشغّل Potato وليس ملف الإعداد. أبقِ debug معطلاً في الإنتاج، لأن وضع التصحيح يفتح صفحات المسؤول دون مفتاح للطلبات الآتية من جهاز الخادم نفسه، ومنها على المضيف المنشور طلبات وكيله العكسي.
استخدام HTTPS
لا يحمل ملف إعداد Potato أي إعدادات TLS، لذلك لا تُضبط الشهادات فيه. أنهِ TLS عند الوكيل العكسي الموضوع أمام Potato، وهو ما يُعدّه قسما nginx وApache أدناه، أو سلّم الشهادة إلى gunicorn مباشرة كما هو موضح في قسم «مع SSL».
التشغيل مع Gunicorn
شغّل Potato تحت gunicorn بعامل واحد بالضبط، وارفع عدد الخيوط عندما تحتاج إلى مزيد من التزامن:
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:8000 \
--timeout 120العامل الواحد متطلب لصحة البيانات وليس خياراً لضبط الأداء. يحتفظ Potato بمجموعة العناصر وطابور التعيين وحالة كل معلّق في الذاكرة داخل العملية. يحصل العامل الثاني على نسخته الخاصة من الثلاثة، فيوزّع عناصر سبق أن عيّنها العامل الأول، ولأن user_state.json يُعاد كتابته بالكامل عند كل حفظ، فإن العامل الذي يحفظ أخيراً يمحو تعليقات الآخر. لا شيء يُبلغ عن هذه الخسارة، والتعليقات تختفي ببساطة. تتشارك الخيوط نسخة واحدة من تلك الحالة، لذا يرفع --threads التزامن بأمان، وهو ما لا يفعله --workers.
مع SSL
gunicorn "potato.flask_server:create_app('config.yaml')" \
--workers 1 \
--threads 8 \
--bind 0.0.0.0:443 \
--certfile /path/to/cert.pem \
--keyfile /path/to/key.pemإعداد الوكيل العكسي
إعداد Nginx
upstream potato {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name annotation.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
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;
location / {
proxy_pass http://potato;
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;
# Long annotation sessions
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
}إعداد Apache
<VirtualHost *:443>
ServerName annotation.example.com
SSLEngine on
SSLCertificateFile /path/to/cert.pem
SSLCertificateKeyFile /path/to/key.pem
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
<Proxy *>
Order allow,deny
Allow from all
</Proxy>
</VirtualHost>التركيب تحت مسار URL
يتطلب تقديم Potato على https://host/app1/ بدلاً من جذر النطاق خطوة إضافية واحدة. تطلب صفحة التعليق التوضيحي /static/... و/updateinstance نسبةً إلى الجذر العام، لذلك تعيد الأصول الخطأ 404 تحت بادئة المسار ويفشل حفظ التعليقات، بينما يستمر عرض هيكل الصفحة.
أخبر Potato ببادئته العامة. عندما تتحكم في الوكيل ويستطيع إرسال ترويسات التمرير (forwarded headers):
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000location /app1/ {
proxy_pass http://127.0.0.1:8000/; # trailing slash strips /app1/
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /app1;
}فعّل POTATO_PROXY_FIX فقط عندما يكون الوصول إلى التطبيق حصرياً عبر ذلك الوكيل. فهو يثق بترويسات التمرير، لذا يستطيع عميل يصل إلى المنفذ الداخلي مباشرة أن يزيّف البادئة ويفسد كل عنوان URL يولّده التطبيق.
عندما لا تستطيع إضافة ترويسات لكنك تعرف مسار التركيب، اضبطه مباشرة. يجب أن يظل الوكيل يحذف البادئة قبل تمرير الطلب:
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000اختبر ذلك بتسجيل الدخول من متصفح بلا جلسة، لأن نموذج تسجيل الدخول هو أول صفحة تتعطل عند غياب البادئة.
يبث عارض الوكيل المباشر وعارض البرمجة المباشرة عبر Server-Sent Events، ويحجز الوكيل الذي يخزّن الاستجابات مؤقتاً (buffering) البث حتى ينتهي. أوقف التخزين المؤقت على تلك المسارات:
location /app1/api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}نشر Docker
ينشر Potato صورة مبنية مسبقاً على GitHub Container Registry، هي ghcr.io/davidjurgens/potato. تحتوي الصورة على Potato واعتمادياته ولا شيء غيرها. يُركَّب مجلد مشروعك، بما فيه config.yaml وبياناته، في /app، فتخدم صورة واحدة كل المهام:
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestيحمل الوسم latest ووسوم الإصدارات، مثل 2.12.1، الاعتماديات الأساسية فقط. تضيف وسوم -all (latest-all و2.12.1-all) حزم SDK الخاصة بالذكاء الاصطناعي وقارئات الصيغ وإضافات التصدير والمصادقة، وتستبعد نماذج الرؤية.
تقدّم الحاوية المهمة بعامل gunicorn واحد وثمانية خيوط، للسبب المذكور في قسم gunicorn أعلاه. ضبط GUNICORN_WORKERS على قيمة أكبر من 1 يوقف الحاوية عند بدء التشغيل ما لم يُضبط POTATO_ALLOW_MULTIWORKER=1 أيضاً، وهذا الإعداد يُفقد التعليقات في دراسة حقيقية. ارفع GUNICORN_THREADS لمزيد من التزامن. يحدد POTATO_CONFIG ملف إعداد غير config.yaml، ويغيّر PORT المنفذ داخل الحاوية من 7860.
تعمل الحاوية بالمعرّف uid 1000 وتكتب مخرجات التعليق وpotato.log وقواعد بيانات SQLite الخاصة بها في /app. على Linux، إذا كان مجلد المشروع المركَّب عبر bind mount مملوكاً لمستخدم آخر، يتوقف الخادم عند الإقلاع برسالة خطأ تسمّي المشكلة. انقل ملكية المجلد إلى uid 1000 باستخدام sudo chown -R 1000:1000 myproject. يتجاهل Docker Desktop على Mac ملكية bind mount، لذلك لا يظهر هذا الخطأ إلا على Linux.
الوسائط التي تلي اسم الصورة تحل محل أمر الخادم، وهذا مفيد للفحوصات التي تُجرى لمرة واحدة:
docker run -v "$PWD/myproject:/app" \
ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strictDocker Compose
services:
potato:
image: ghcr.io/davidjurgens/potato:latest
ports:
- "8000:7860"
volumes:
- ./myproject:/app
environment:
- POTATO_SECRET_KEY=${POTATO_SECRET_KEY}
- POTATO_ADMIN_API_KEY=${POTATO_ADMIN_API_KEY}
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/ssl/certs
depends_on:
- potato
restart: unless-stoppedداخل شبكة Compose، يصل nginx إلى Potato باسم الخدمة على منفذ الحاوية، لذا تصبح كتلة upstream في قسم الوكيل العكسي أعلاه server potato:7860; بدلاً من server 127.0.0.1:8000;.
لأن مجلد المشروع مركَّب وليس منسوخاً داخل صورة، تبقى التعليقات التي يكتبها Potato فيه على المضيف عند استبدال الحاوية.
التحكم في الوصول
تقييد من يمكنه التسجيل
يسمح Potato بالتسجيل الذاتي افتراضياً. على خادم عام، حدّد من يُسمح لهم بالتسجيل:
user_config:
allow_all_users: false
users:
- annotator1@example.com
- annotator2@example.comينتمي كلا المفتاحين إلى user_config. إذا كُتبا في المستوى الأعلى يُتجاهلان، فتبدو قائمة الوصول مقيِّدة بينما تقبل الجميع.
وصول المسؤول
المفتاح admin_api_key مفتاح مشترك يُرسَل في الترويسة X-API-Key. عندما لا تضبطه، يولّد Potato مفتاحاً ويكتبه في {task_dir}/admin_api_key.txt، لذا اضبطه بنفسك على خادم الإنتاج وأبقِه خارج المستودع:
admin_api_key: "your-shared-key"
rbac:
enabled: true
user_role_assignments:
lead@example.com: adminمع إعداد rbac، يصل أشخاص محددون بأسمائهم إلى /admin بتسجيل الدخول العادي بدلاً من تداول المفتاح المشترك. راجع إدارة المستخدمين للاطلاع على مجموعة الصلاحيات الكاملة.
الجلسات
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200يوقّع secret_key ملف تعريف الارتباط الخاص بالجلسة، ويمكن أن يأتي من POTATO_SECRET_KEY بدلاً من ذلك. من دونه يولّد Potato مفتاحاً جديداً عند كل بدء تشغيل، فتؤدي كل إعادة تشغيل إلى تسجيل خروج الجميع في منتصف المهمة. على خادم يُعاد تشغيله مع كل نشر، هذا هو الإعداد الذي يتسبب أكثر من غيره في فقدان التعليقات.
يمسح session_timeout_minutes الجلسة التي بقيت خاملة هذه المدة، ويبدأ العدّ من جديد مع كل طلب. وPotato 2.11 هو أول إصدار يطبّقه فعلياً، بقيمة افتراضية قدرها 480 دقيقة. وعند 60، كما في هذا المثال، يتعيّن على المعلّق الذي يبتعد ساعة أن يسجّل الدخول من جديد حين يعود.
مخرجات التعليق التوضيحي
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300يكتب Potato مجلداً لكل معلّق تحت output_annotation_dir، يحتوي كل منها على ملف user_state.json الخاص بذلك الشخص. يحدد export_annotation_format الصيغ التي يكتبها التصدير الدوري، والقيمة الفارغة تعني عدم وجود تصدير تلقائي إطلاقاً. أما auto_export_interval فهو الفاصل بين عمليات التصدير بالثواني، وقيمته الافتراضية 60.
قاعدة البيانات الخلفية
تُحفظ التعليقات على القرص افتراضياً. لعمليات النشر التي تحتاج إلى قاعدة بيانات، يقبل database.type القيمة mysql أو file:
database:
type: mysql
host: localhost
port: 3306
database: potato_db
username: potato
password: ${DB_PASSWORD}المفتاحان هما database وusername، وليسا name وuser. يمكن أن تأتي سلسلة الاتصال من POTATO_DB_CONNECTION بدلاً من ذلك، ما يُبقي بيانات الاعتماد خارج ملف الإعداد.
ثبّت برنامج التشغيل بالأمر pip install 'potato-annotation[mysql]'. قبل 2.10.2 لم تكن قاعدة بيانات MySQL الخلفية تعمل قط، وكان الإعداد الذي يذكرها يخزّن التعليقات على القرص دون أي تنبيه. ومنذ 2.10.2 يوقف غيابُ برنامج التشغيل أو تعذّرُ الوصول إلى الخادم بدءَ التشغيل، ويقرأ التصدير والنسخ الاحتياطي وpotato deploy pull من قاعدة البيانات.
فحوصات الصحة والسجلات
يقدّم Potato المسار GET /health في كل عملية نشر، ولا يحتاج هذا المسار إلى مصادقة، لذا يصلح فحصاً للجاهزية في موازن الحمل أو في منسِّق الحاويات:
curl -sf https://annotation.example.com/healthيُضبط مستوى تفصيل السجلات في ملف الإعداد أو في سطر الأوامر، وليس في كتلة خاصة بالسجلات:
verbose: true
debug_log: serverيقبل debug_log القيم all أو ui أو server أو none. ويحصر متغير البيئة POTATO_LOGGERS المخرجات في مسجِّلات محددة بالاسم عندما تتتبع نظاماً فرعياً واحداً.
التوسع
يعمل Potato كعملية واحدة تحتفظ بمجموعة العناصر كاملة في الذاكرة، لذا وسّع الجهاز بدلاً من زيادة عدد الخوادم. تشغيل عدة نسخ متماثلة خلف موازن حمل لا يقسّم العمل. تحتفظ كل نسخة بنسختها الخاصة من طابور التعيين وحالة المعلّقين، وهذا يعيد إنتاج فقدان البيانات الموصوف في قسم gunicorn أعلاه بالضبط.
الذاكرة هي المورد الذي ينمو مع حجم مجموعة البيانات. عند نحو مليون عنصر، يمكن نقل الحمولات إلى ذاكرة تخزين مؤقت في SQLite:
item_store:
backend: paged # default: memory
cache_size: 2048قِس قبل تفعيل هذا الخيار. يوفّر التقسيم إلى صفحات نحو 28% في عناصر الرؤية و51% في النصوص، وليس عشرة أضعاف، ويجعل تحميل مجموعة البيانات أبطأ بنحو 2.5 مرة. الملف ذاكرة تخزين مؤقت يُعاد بناؤها من ملفات البيانات عند كل إقلاع، لذا يمكن حذفه بأمان.
في المشاريع الكبيرة، الأدوات الفعالة هي تحديد سقف لما يُعيَّن لأي معلّق واحد باستخدام per_annotator_quota، وتقسيم مجموعة البيانات بين الأفواج باستخدام batch_assignment، وإضافة ملفات البيانات بمرور الوقت باستخدام data_directory بدلاً من تحميل كل شيء عند بدء التشغيل.
استراتيجية النسخ الاحتياطي
يجب أن يشمل النسخ الاحتياطي أكثر من مجلد مخرجات التعليقات. فمجلد المهمة يحتوي أيضاً على ما يصل إلى ثلاث قواعد بيانات SQLite، ولا يمكن إعادة بناء أي منها من ملفات البيانات: project.sqlite (المذكرات، ودليل الترميز، وسير عمل المراجعة)، وdatasets.sqlite، وengine.sqlite عند استخدام محرّك الوسم. ويخزّن Potato الحسابات التي أُنشئت عبر نموذج التسجيل في user_config.json. تعمل قواعد البيانات في وضع WAL أثناء كتابة الخادم فيها، لذا قد ينتج tar أو cp لملف قاعدة بيانات نسخة تالفة أو ينقصها عمل حديث، ولا يحذّرك أي من الأمرين.
النسخ الاحتياطي المدمج
منذ الإصدار 2.10، تنسخ كتلة backup في Potato مخرجات التعليقات ولقطات .backup من قواعد البيانات إلى خارج الخادم، وتستعيدها عندما يبدأ الخادم بقرص فارغ. وتعيد الاستعادة قائمة الحسابات مع التعليقات، ولا تكتب أبداً فوق تعليقات موجودة على القرص. ترسل الكتلة أدناه النسخة إلى حاوية S3 كل خمس دقائق، وهو الفاصل الافتراضي، فلا يكلّف فقدان القرص المعلّقين أكثر من خمس دقائق من العمل:
backup:
schedule_minutes: 5
restore_on_boot: true
sinks:
- type: s3
bucket: lab-potato-backups
prefix: pilot-study
endpoint_url: https://s3.example.edu # leave out for AWS S3يوجّه endpoint_url الوجهة إلى أي خدمة تتحدث واجهة S3 البرمجية، مثل مخزن كائنات جامعي أو MinIO أو Cloudflare R2 أو Backblaze B2. تأتي بيانات الاعتماد من POTATO_S3_ACCESS_KEY_ID وPOTATO_S3_SECRET_ACCESS_KEY في بيئة الخادم، أو من سلسلة boto3 المعتادة. والمفتاح المقصور على تلك الحاوية وحدها يحدّ مما يستطيع الخادم الوصول إليه. أما الوجهة type: huggingface مع repo_id فترسل النسخة إلى مجموعة بيانات خاصة على Hugging Face بدلاً من ذلك، باستخدام HF_TOKEN. وتحتاج الوجهتان كلتاهما إلى الإضافة hosting، pip install 'potato-annotation[hosting]'.
نسخة تبقى على تخزينك الخاص
عندما تُلزم اتفاقية استخدام البيانات بإبقاء البيانات على الخادم، يمكن لمهمة ليلية أن تنسخها إلى تخزين محلي أو مركَّب بدلاً من ذلك. يؤرشف السكربت أدناه مجلد المخرجات، ويأخذ لقطة من كل قاعدة بيانات باستخدام أمر .backup في SQLite بدلاً من نسخ الملف، ويحتفظ بنسخ سبعة أيام. ويحتاج إلى أداة سطر الأوامر sqlite3:
#!/bin/bash
# backup.sh
set -e
TASK_DIR=/var/potato
DEST=/var/backups/potato/$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEST"
tar -czf "$DEST/annotations.tar.gz" -C "$TASK_DIR" annotations
for db in project.sqlite datasets.sqlite engine.sqlite; do
if [ -f "$TASK_DIR/$db" ]; then
sqlite3 "$TASK_DIR/$db" ".backup '$DEST/$db'"
fi
done
cp "$TASK_DIR/config.yaml" "$DEST/"
if [ -f "$TASK_DIR/user_config.json" ]; then cp "$TASK_DIR/user_config.json" "$DEST/"; fi
find /var/backups/potato -mindepth 1 -maxdepth 1 -mtime +7 -exec rm -rf {} +شغّله من cron في الساعة 2 صباحاً كل ليلة:
0 2 * * * /var/potato/backup.shقد تفقد النسخة الليلية ما يصل إلى يوم من العمل، لذا شغّلها أكثر من ذلك أثناء دراسة فيها معلّقون كثيرون. وقبل أن تبدأ الدراسة، استعد نسخة واحدة على خادم اختبار وتحقق من وجود آخر تعليق فيها.
قائمة التحقق الأمنية
- أنهِ TLS عند الوكيل بشهادة صالحة
- اضبط
secret_keyأوPOTATO_SECRET_KEYلكي تبقى الجلسات بعد إعادة التشغيل - اضبط
admin_api_keyبدلاً من الاعتماد على الملف المولَّد - قيّد التسجيل باستخدام
user_config.users - أبقِ
debugمعطلاً، لأن وضع التصحيح يتيح لطلبات الوكيل العكسي الوصول إلى صفحات المسؤول دون مفتاح - شغّل gunicorn بعامل واحد
- احفظ بيانات الاعتماد في متغيرات البيئة، لا في ملف الإعداد
- فعّل
POTATO_PROXY_FIXفقط خلف وكيل موثوق - انسخ مجلد المخرجات وملف الحسابات وقواعد البيانات احتياطياً، واختبر الاستعادة
- راجع من يملك دور المسؤول
متغيرات البيئة
# .env file
POTATO_SECRET_KEY=your-super-secret-key
POTATO_ADMIN_API_KEY=your-admin-key
POTATO_DB_CONNECTION=mysql://potato:password@localhost:3306/potato_dbولّد مفتاح توقيع باستخدام:
python3 -c 'import secrets; print(secrets.token_hex(32))'التحميل في Docker Compose:
services:
potato:
env_file:
- .envاستكشاف الأخطاء وإصلاحها
فحص السجلات
# Gunicorn logs
journalctl -u potato -f
# Docker logs
docker-compose logs -f potatoالمشاكل الشائعة
502 Bad Gateway: تحقق من أن Potato قيد التشغيل ومرتبط بالمنفذ الذي يمرر إليه الوكيل الطلبات.
تسجيل خروج الجميع بعد النشر: لم يُضبط secret_key، لذا وُلِّد مفتاح توقيع جديد عند بدء التشغيل.
تعليقات مفقودة: تحقق من عدد العمال. تشغيل أكثر من عامل gunicorn واحد يمحو التعليقات دون الإبلاغ عن ذلك.
أخطاء 404 للأصول تحت بادئة مسار: اضبط POTATO_PROXY_FIX أو POTATO_URL_PREFIX، وتأكد من أن الوكيل يحذف البادئة قبل تمرير الطلب.
بطء مع مجموعة بيانات كبيرة: أضف ذاكرة قبل إضافة خوادم، وفكّر في item_store.backend: paged بعد نحو مليون عنصر.
الخطوات التالية
- إدارة المستخدمين - قوائم الوصول والأدوار والحصص
- التعهيد الجماعي - Prolific وSONA ولوحات مشاركين أخرى
- صيغ البيانات - التخزين والتصدير
للاطلاع على تفاصيل التنفيذ، راجع دليل الوكيل العكسي ودليل التوسع.