نشر Potato على خادم
شغّل Potato في بيئة الإنتاج خلف nginx مع TLS، في Docker أو على آلة افتراضية، واعرف لماذا يعمل خادم Potato كعملية واحدة بدلًا من عدة عمليات worker.
شغّل عملية Potato واحدة لكل مشروع، وضع أمامها nginx وشهادة TLS، وعند الحاجة إلى مزيد من التزامن زِد عدد الخيوط (threads) بدلًا من عدد عمليات worker. فعملية worker ثانية تفقد التوصيفات دون أي تنبيه.
يحتفظ Potato في الذاكرة، داخل عملية الخادم، بمجموعة العناصر وطابور الإسناد وحالة كل مُوصِّف. هذه الحقيقة وحدها تحدد معظم قرارات النشر. فبسبب هذه الحالة الداخلية يتوسع خادم Potato بالخيوط لا بعمليات worker، ويكون مجلد مخرجات التوصيف هو ما يجب نسخه احتياطيًا، ويؤدي توزيع الحمل على عدة عمليات Potato إلى إفساد الدراسة بدلًا من تسريعها. يتناول هذا الدليل الطرق الثلاث لتشغيل خادم، والوكيل العكسي وTLS أمامه، ومفاتيح الإعداد المهمة حين يعمل عليه مُوصِّفون حقيقيون.
لماذا يعمل خادم Potato كعملية واحدة
تحصل عملية worker ثانية في gunicorn على نسختها الخاصة من مجموعة العناصر وطابور الإسناد وحالة المستخدمين. فتوزّع مثيلات سبق أن أسندتها العملية الأولى، ولأن user_state.json يُعاد كتابته بالكامل عند كل حفظ، فإن العملية التي تحفظ أخيرًا تمحو توصيفات الأخرى. ولا يظهر شيء من ذلك في السجلات. تختفي التوصيفات، وأول علامة على ذلك عادةً مُوصِّف يقول إنه وسم عنصرًا يبدو الآن كأنه لم يُمسّ.
تفرض صورة Docker المنشورة لـ Potato هذه القاعدة. فهي تشغّل عملية worker واحدة في gunicorn وترفض البدء بأكثر منها، ويتجاوز POTATO_ALLOW_MULTIWORKER=1 هذا الرفض لأي شيء ليس دراسة حقيقية. ويأتي التزامن بدلًا من ذلك من الخيوط، التي تتشارك نسخة واحدة من الحالة. تستخدم الصورة ثمانية خيوط افتراضيًا عبر GUNICORN_THREADS، وهذا يكفي لعشرات المُوصِّفين المتزامنين في دراسة نموذجية.
هذا القيد يحدد بقية هذا الدليل. شغّل عملية واحدة لكل مشروع، وامنحها ذاكرة كافية لمجموعة البيانات، واستخدم الوكيل العكسي لـ TLS والملفات الثابتة لا لتوزيع الحمل.
ثلاث طرق لتشغيل خادم
| الطريقة | ما تقدمه لك | متى تستخدمها |
|---|---|---|
potato deploy | تجهّز المضيف وTLS والصورة انطلاقًا من إعداداتك | دراسة تريد تشغيلها اليوم |
| صورة Docker | البايتات نفسها في كل مرة، دون بناء الاعتماديات | آلة تديرها بالفعل |
| آلة افتراضية مع systemd | تحكم كامل في المضيف وبيئة Python | خادم مؤسسي له قواعده الخاصة |
potato deploy للمسار السريع
يجهّز potato deploy مضيفًا، ويثبّت الصورة، ويضبط TLS انطلاقًا من ملف الإعداد الموجود لديك:
potato deploy up myproject/config.yaml --provider digitaloceanبعد بضع دقائق يصبح لديك عنوان HTTPS ترسله إلى المُوصِّفين، ويعيد potato deploy pull التوصيفات إليك. منذ الإصدار 2.10.0 صارت الوجهات المتاحة هي local وpotato share وثلاثة عشر مضيفاً سحابياً، منها AWS وJetstream2 وHetzner وFly وRailway، وتختلف في ما إذا كانت البيانات تبقى بعد إعادة التشغيل. وتجمع وجهات الأجهزة الافتراضية السبع بين القرص الدائم وبث السجلات وSSH، أما المضيف الذي يمسح قرصه فيحتاج إلى --backup، الذي ينسخ التوصيفات إلى مجموعة بيانات على HuggingFace أو إلى حاوية S3. وتقارن صفحة تشغيل Potato محليًا أو في السحابة بينها. ابدأ بـ local، فهي تشغّل الحزمة نفسها والصورة نفسها دون حساب ودون فاتورة، وتخبرك بما سيُنشر قبل أن يكلفك أي شيء.
صورة Docker المنشورة
ينشر Potato صورة مبنية مسبقًا على GitHub Container Registry. تحتوي الصورة على الخادم واعتمادياته، ويُركَّب مشروعك على /app وقت التشغيل، فتخدم صورة واحدة كل المهام:
docker run -p 8000:7860 \
-v "$PWD/myproject:/app" \
-e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
ghcr.io/davidjurgens/potato:latestوُجدت هذه الصورة لأن الاعتماديات الأساسية لـ Potato تشمل numpy وpandas وscipy وscikit-learn. وتثبيتها على آلة افتراضية صغيرة بطيء في أحسن الأحوال. فعلى آلة بذاكرة 1 GB تُقتل عملية البناء لنفاد الذاكرة، وعلى 2 GB تستغرق من خمس إلى عشر دقائق عند كل إعادة نشر. أما سحب الصورة فيستغرق أقل من دقيقة.
هناك وسمان مهمان. يحمل latest الاعتماديات الأساسية بحجم يقارب 840 MB، ويضيف latest-all حِزم SDK للذكاء الاصطناعي وصيغ المستندات والتصدير وتسجيل الدخول عبر OAuth بحجم يقارب 1.5 GB. وكلاهما مبني لـ linux/amd64 وlinux/arm64. ثبّت رقم إصدار بدلًا من latest لأي دراسة ستستمر شهورًا، لأن latest يتغير من تحتك.
اضبط POTATO_SECRET_KEY في كل ما يدوم أكثر من تشغيل واحد. فبدونه يكون مفتاح توقيع جلسات Flask عشوائيًا لكل عملية، فتؤدي كل إعادة تشغيل إلى تسجيل خروج جميع المُوصِّفين. يحدد POTATO_CONFIG مسار ملف الإعداد داخل الحاوية، ويضبط PORT منفذ الاستماع (7860 افتراضيًا)، ويضبط GUNICORN_TIMEOUT مهلة عملية worker بالثواني.
تعمل الصورة بالمعرّف uid 1000، ويحتفظ الـ bind mount بملكية المضيف، لذا كثيرًا ما تعجز الحاوية على Linux عن الكتابة في مجلد أنشأته أنت. يتحقق الخادم من ذلك عند البدء ويرفض العمل بدلًا من أن يتعطل في منتصف الإقلاع. إما أن تسلّم المجلد لمستخدم الحاوية بالأمر sudo chown -R 1000:1000 myproject، أو أن تشغّل الحاوية بهويتك باستخدام docker run --user "$(id -u):$(id -g)"، وهذا يُبقي التوصيفات مملوكة لك أيضًا. يتجاهل Docker Desktop على macOS وWindows الملكية في الـ bind mounts، لذا قد يفشل على خادم Linux إعدادٌ يعمل على جهاز Mac.
التثبيت على آلة افتراضية
على آلة تديرها مباشرة، ثبّت Potato في بيئة افتراضية ودع systemd يتولى العملية.
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 --versionشغّل التطبيق عبر gunicorn بعملية worker واحدة وعدة خيوط. توجد دالة إنشاء التطبيق في potato.flask_server:create_app وتأخذ مسار ملف الإعداد:
gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 \
--chdir /opt/potato/projects/my_project \
"potato.flask_server:create_app('config.yaml')"أنشئ /etc/systemd/system/potato.service كي يعود الخادم للعمل بعد إعادة تشغيل الجهاز أو بعد تعطله:
[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.targetفعّل الخدمة وشغّلها:
sudo systemctl enable potato
sudo systemctl start potato
sudo systemctl status potatoيحتوي EnvironmentFile على الأسرار التي لا ينبغي وضعها في ملف الإعداد:
# /opt/potato/.env
POTATO_SECRET_KEY=a-long-random-value
OPENAI_API_KEY=sk-...ولّد المفتاح السري مرة واحدة بالأمر openssl rand -hex 32 واحتفظ به طوال مدة الدراسة. فتغييره يُبطل كل الجلسات ويسجّل خروج جميع المُوصِّفين.
nginx وTLS
يرتبط Potato بـ localhost، ويُنهي nginx اتصال TLS أمامه. ويهم حد الرفع في مشاريع الصور والصوت، حيث يرفض الحد الافتراضي لجسم الطلب البالغ 1 MB ملفات الوسائط دون أن يرى المُوصِّف الفشل.
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;
}
}فعّل الموقع واحصل على شهادة:
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.timerأغلق كل ما عدا ذلك في الجدار الناري، لأن منفذ Potato يجب ألا يكون قابلًا للوصول مباشرة بعد وضع وكيل أمامه:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enableخدمة عدة مشاريع تحت اسم مضيف واحد
كثيرًا ما تعرض المؤسسات عدة خوادم Potato عبر نقطة وصول HTTPS عامة واحدة، بربط المسارات بمنافذ محلية. تطلب واجهة التوصيف مواردها وتحفظ عبر عناوين URL نسبية إلى الجذر، لذا يحتاج خادم مركّب على /app1 إلى معرفة بادئته، وإلا أعاد ملف CSS الخطأ 404 وفشل الحفظ التلقائي برسالة "annotations not saved". يقرأ Potato البادئة من SCRIPT_NAME في WSGI، وهناك طريقتان لضبطها.
استخدم POTATO_PROXY_FIX=1 حين تتحكم في الوكيل ويمكنه إرسال X-Forwarded-Prefix. واستخدم POTATO_URL_PREFIX=/app1 حين لا يكون تعديل إعداد الوكيل بيدك. ويجب أن يظل الوكيل يحذف البادئة قبل التمرير، كي يستمر Flask في تلقي مسارات بلا بادئة:
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 بالترويسات الممرَّرة، لذا لا تفعّل POTATO_PROXY_FIX إلا حين يكون التطبيق قابلًا للوصول حصريًا عبر الوكيل الموثوق. فإن كان المنفذ الداخلي قابلًا للوصول مباشرة أيضًا، أمكن لأي عميل تزييف البادئة وتسميم عناوين URL التي يولّدها Potato.
الإعداد الذي يحتاجه مشروع في بيئة الإنتاج
أربعة إعدادات تميّز إعداد الإنتاج عن الإعداد المحلي: مفتاح توقيع ثابت، واشتراط كلمة مرور، ومدة جلسة صريحة، ومجلد مخرجات لديك نسخة احتياطية منه.
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: trueيحدّ session_timeout_minutes من مدة الخمول لا من طول نوبة العمل، لأن العدّاد يبدأ من جديد مع كل طلب. تناسب القيمة الافتراضية 480 المُوصِّفين الذين يعملون يومًا كاملًا، فخفّضها حين يكون الجهاز مشتركًا. اضبط allow_all_users: false واذكر أسماء مُوصِّفيك كلما كان الخادم قابلًا للوصول من الإنترنت العام، حتى لا يتمكن غريب يعثر على العنوان من التسجيل والبدء في الكتابة في مجموعة بياناتك.
فحوص السلامة والسجلات
/health مسبار حيوية لا يتطلب مصادقة، مخصص للحاويات وموازنات الحمل، ولا يُبلغ عن شيء تقريبًا عن قصد، لأن كل ما يقوله علني:
curl http://localhost:8000/health
# {"status": "ok"} once the state managers exist
# {"status": "starting"} 503 while the server is still coming upيعتمد فحص سلامة الحاوية واستطلاع مزوّد النشر بعد الإنشاء كلاهما على هذا الانتقال. أما أسماء المهام وأعداد العناصر وأعداد المُوصِّفين فتأتي من /admin/health، الذي يتطلب مفتاح API الخاص بالمسؤول.
يمر التسجيل عبر العملية، فيلتقطه systemd:
sudo journalctl -u potato -fارفع مستوى التسجيل من ملف الإعداد بـ verbose: true، أو فعّل الأجزاء التي تحتاجها من تسجيل التصحيح بـ debug_log، الذي يقبل all أو ui أو server أو none. اترك الاثنين معطّلين أثناء دراسة حقيقية، لأن تسجيل التصحيح مُسهب إلى حد قد يملأ قرصًا صغيرًا خلال تشغيل طويل.
ما يجب نسخه احتياطيًا
ثلاثة أشياء في مجلد المشروع لا يمكن إعادة توليدها. مخرجات التوصيف تحت output_annotation_dir هي الدراسة نفسها. ويحتفظ project.sqlite بالمذكرات ودليل الترميز والحالات وفهرس البحث وسير عمل المراجعة. ويحتفظ datasets.sqlite بسجلات مجموعة البيانات. وكل ما عدا ذلك يمكن إعادة بنائه من ملفات بياناتك وملف الإعداد.
#!/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 -deleteشغّله كل ليلة من crontab، وانسخ الأرشيف إلى خارج الجهاز:
0 2 * * * /opt/potato/backup.shتحديد حجم الجهاز
الذاكرة هي المورد الذي يتناسب مع حجم مجموعة البيانات، لأن مجموعة العناصر محفوظة داخل العملية. أما كل ما عداها فصار يُحمَّل عند الحاجة، فلا تُستورد حزمة التضمينات وsentence-transformers عند الإقلاع ما لم تحتج إليها ميزة ما. وعلى مجموعة البيانات المرجعية في ملاحظات إصدار v2.6.0، انخفضت الذاكرة المقيمة من نحو 750 MB إلى نحو 365 MB، وانخفض زمن الإقلاع مع 50,000 عنصر من نحو 10 ثوانٍ إلى نحو 5.7 ثانية.
تكلفة العنصر الواحد صغيرة وقابلة للقياس. يشغل عنصر رؤية يحمل عنوان URL لصورة وحقلين نحو 932 بايت، ويشغل عنصر نصي يضم نحو 540 حرفًا فريدًا نحو 1430 بايت. لذا فإن مليون عنصر نصي يعني نحو 1.43 GB من الذاكرة المقيمة، وهذا هو الرقم الذي تبني عليه ميزانيتك.
وللمدونات التي يهم فيها ذلك، يمكن أن تُحفظ محتويات العناصر في ملف SQLite مع ذاكرة تخزين مؤقت صغيرة في الذاكرة:
item_store:
backend: paged
cache_size: 2048أضف هذه الكتلة إلى الإعداد أعلاه. واقرأ القياس قبل تفعيلها. فالتقسيم إلى صفحات يوفر 28% في عناصر الرؤية و51% في النص، لا توفيرًا بمقدار رتبة عددية كاملة، وفي المقابل يستغرق تحميل المدونة نحو 2.5 ضعف الوقت. وتحت المليون عنصر لا يمنحك إلا خادمًا أبطأ دون أي فائدة.
فضّل جهازًا بذاكرة RAM أكبر على تقسيم مشروع بين عدة خوادم، لأن التقسيم هو نفسه عطل العمليات المتعددة الموصوف في بداية هذا الدليل. وحين يعجز جهاز واحد فعلًا عن استيعاب المدونة، قسّم العمل بدلًا من ذلك. فالإسناد على دفعات يمنح كل مُوصِّف شريحة، ويضع per_annotator_quota حدًا أعلى لما يُسند إلى شخص واحد، ويتيح لك data_directory مع مراقبة المجلد إضافة ملفات أثناء عمل الخادم.
استكشاف الأخطاء وإصلاحها
المنفذ مستخدم بالفعل. ابحث عن العملية وأوقفها، بدلًا من تشغيل نسخة Potato ثانية على منفذ آخر مع مجلد المشروع نفسه:
sudo lsof -i :8000رفض الإذن على مجلد المشروع. يجب أن يكون مستخدم الخدمة هو مالك المجلد، وفي Docker يكون هذا المستخدم هو uid 1000:
sudo chown -R potato:potato /opt/potatoيُسجَّل خروج المُوصِّفين بعد كل إعادة تشغيل. إما أن POTATO_SECRET_KEY غير موجود أو أن قيمة الإعداد secret_key مفقودة، فيُعاد توليد مفتاح التوقيع عند كل إقلاع.
الموارد تعيد 404 والحفظ التلقائي يفشل خلف وكيل. الخادم لا يعرف بادئة عنوانه. اضبط POTATO_PROXY_FIX=1 أو POTATO_URL_PREFIX، وتحقق من أن الوكيل يحذف البادئة قبل التمرير.
قراءات إضافية
- إعداد بيئة الإنتاج والوكيل العكسي على هذا الموقع
- Running Potato in Docker مرجعًا للصورة
- Scaling and Large Datasets للاطلاع على القياسات التي تستند إليها نصائح تحديد الحجم
- تشغيل دراسات التوصيف على Prolific بعد أن يصبح الخادم قابلًا للوصول