DuckDB

مسارات DuckDB مجدولة، دون وضع قاعدة بيانات أمام قاعدة بياناتك.

يعمل DuckDB داخل العملية ودون خادم، وهذا بالضبط سبب غياب المجدول وإعادة المحاولة وسجل التشغيل. يضيف Dagu هذه الطبقة التشغيلية من ملف تنفيذي واحد، فتبقى المنظومة ملفين تنفيذيين ودليل بيانات.

ملف تنفيذي واحد ينسّق ملفًا تنفيذيًا آخر، دون قاعدة بيانات للتشغيل
max_active_runs يفرض قاعدة الكاتب الواحد في DuckDB
المؤشرات تبقى بعد إعادة تشغيل العملية في التحميل التزايدي
يعمل بجوار البيانات، بما في ذلك داخل الشبكات المغلقة
01

كونه مضمَّنًا يعني غياب المجدول عن قصد

يعمل DuckDB داخل عمليتك. لا توجد خدمة خلفية ولا منفذ ولا أي شيء ينتظر لتنفيذ استعلام في الثالثة فجرًا. هذا هو غرض التصميم وليس إغفالًا، ومعناه أن الجدولة يجب أن تأتي من الخارج.

  • مسار DuckDB عادةً استدعاء لسطر الأوامر، ما يجعل cron الإجابة الافتراضية التي تتوقف عندها معظم الفرق
  • cron يمنح التنفيذ فقط: لا إعادة محاولة عند انتهاء مهلة S3، ولا سجل، ولا إشارة حين لا يحدث شيء بصمت في الليلة الماضية
  • امتداد cron المجتمعي يجدول داخل العملية، فتنتهي الجدولة بانتهائها ولا تترك سجل تشغيل
02

لا تنقض السبب الذي اخترت DuckDB من أجله

جاذبية DuckDB أنه لا يوجد عنقود ولا خادم ولا فاتورة مستودع بيانات. وضع منسّق ثقيل أمامه يعيد ذلك كله. يتطلب Airflow مجدولًا وقاعدة بيانات وصفية وإطار عمل DAG بلغة Python. ويتطلب Kestra قاعدة بيانات JDBC وتخزين كائنات وأربعة مكونات. أما Dagu فملف تنفيذي واحد بحالة محفوظة في ملفات.

  • إضافة Postgres لجدولة محرك تحليلات بلا خادم مقايضة غريبة
  • تبقى تعريفات سير العمل في git بجوار الاستعلامات التي تنفذها، لا داخل بيانات وصفية لمنصة أخرى
  • تتسع المنظومة كلها لمضيف واحد، وهو غالبًا المضيف نفسه الذي توجد عليه البيانات
03

الكاتب الواحد مسألة جدولة

النصيحة المعتادة في الإنتاج مع DuckDB هي الاحتراس من تداخل عمليات الكتابة بقفل على مستوى نظام التشغيل، لأن قاعدة البيانات تقبل كاتبًا واحدًا. وحين تكون تلك الحماية cron وملف قفل، فهي على بعد نص برمجي واحد من الخطأ. والإعلان عنها على مستوى سير العمل يزيل صنف الأخطاء هذا بأكمله.

  • ضبط max_active_runs على واحد يعني أن التشغيل البطيء يؤخر التالي بدل أن يفسد قاعدة البيانات
  • resources.limits.memory يحدّ التشغيل قبل أن يُسقط ضمٌّ كبير المضيف معه
  • retry_policy يغطي الإخفاقات العابرة فعلًا، مثل انتهاء مهلة تخزين الكائنات في منتصف المسح

هذا يقيّد عمليات التشغيل المتزامنة لسير العمل هذا، ولا يجعل DuckDB متعدد الكتّاب: منع عملية أخرى من الكتابة في الملف نفسه يبقى مسؤوليتك.

تجميع ليلي فوق Parquet في تخزين الكائنات
# duckdb-nightly-rollup.yaml
schedule: "0 3 * * *"
max_active_runs: 1

resources:
  limits:
    memory: "8Gi"

steps:
  - id: rollup
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        CREATE OR REPLACE TABLE daily_sales AS
          SELECT order_date, region, sum(amount) AS amount
          FROM read_parquet('s3://warehouse/raw/orders/*.parquet')
          GROUP BY 1, 2;
    retry_policy:
      limit: 2
      interval_sec: 300

  - id: export
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        COPY daily_sales TO '/data/export/daily_sales.parquet' (FORMAT parquet);
    depends: rollup

  - id: count_rows
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      readonly: true
      query: SELECT count(*) AS row_count FROM daily_sales;
    depends: export

  - id: verify
    env:
      - COUNT_JSON: ${steps.count_rows.outputs.result}
    run: test "$(printf '%s\n' "$COUNT_JSON" | jq -r '.[0].row_count')" -gt 0
    depends: count_rows

handler_on:
  failure:
    run: /opt/analytics/notify-failure.sh

mail_on:
  failure: true
04

التحميل التزايدي يحتاج مؤشرًا يعيش أطول من العملية

إعادة تحميل كل شيء في كل تشغيل هي ما يحوّل استعلامًا محليًا سريعًا إلى استعلام بطيء ومكلف. يحتاج التحميل التزايدي إلى تذكّر أين انتهى آخر تشغيل ناجح، ويجب أن تصمد هذه الذاكرة أمام إعادة التشغيل دون قاعدة بيانات تحفظها.

  • يحفظ Dagu مؤشر JSON صغيرًا عبر عمليات التشغيل، فيقرأ كل تشغيل النافذة التي لم تُحمَّل بعد فقط
  • يُحفظ المؤشر بعد نجاح خطوة التحميل، فيتركه التشغيل الفاشل كما هو ويعيد التشغيل التالي النافذة نفسها
  • تحديد النافذة من الطرفين يمنع التسابق مع صفوف تصل أثناء التشغيل
إلحاق تزايدي بعلامة مائية محفوظة
# duckdb-incremental-load.yaml
schedule: "*/15 * * * *"
max_active_runs: 1

steps:
  - id: load_cursor
    action: state.get
    output: CURSOR
    with:
      key: cursors/events-loaded-through
      default:
        loaded_through: "2026-01-01T00:00:00Z"

  - id: window
    run: |
      printf 'since=%s\n' "$(printf '%s\n' "$CURSOR" | jq -r .value.loaded_through)" >> "$DAGU_OUTPUT_FILE"
      printf 'until=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: since
      - name: until
    depends: load_cursor

  - id: append_new_events
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        INSERT INTO events
          SELECT * FROM read_parquet('s3://warehouse/events/*.parquet')
          WHERE ingested_at >  TIMESTAMP '${steps.window.outputs.since}'
            AND ingested_at <= TIMESTAMP '${steps.window.outputs.until}';
    depends: window
    retry_policy:
      limit: 3
      interval_sec: 60

  - id: save_cursor
    action: state.set
    with:
      key: cursors/events-loaded-through
      value:
        loaded_through: "${steps.window.outputs.until}"
    depends: append_new_events

mail_on:
  failure: true
05

أين يكون هذا الاقتران الخيار الأضعف

ينظّم Dagu جدولة DuckDB، لكنه لا يغيّر ماهية DuckDB، وهناك أحمال يكون فيها هذا الاقتران إجابة خاطئة.

  • DuckDB أحادي العقدة وأحادي الكاتب. إذا احتاجت عدة خدمات للكتابة في آن واحد، فلا منسّق يعالج ذلك
  • لم يُبنَ لعمليات كتابة صغيرة متكررة؛ تلك تبقى وظيفة PostgreSQL
  • إذا كنت تشغّل Airflow أو مستودعًا له مجدوله الخاص، فنادرًا ما يستحق إضافة منسّق ثانٍ من أجل مسار واحد

FAQ

Practical questions before adopting

هل يوجد منفّذ أو إضافة خاصة بـ DuckDB؟

لا، وهذا مقصود. يوفّر DuckDB واجهة سطر أوامر بملف تنفيذي واحد تؤدي العمل أصلًا، لذا ينفّذها Dagu كخطوة أوامر عادية. المنفّذ المخصص سيضيف طبقة يجب مواكبتها مع إصدارات DuckDB، ويقدّم أقل مما تقدّمه الواجهة نفسها.

كيف أمنع تشغيلين من إفساد ملف قاعدة البيانات؟

اضبط max_active_runs على واحد في سير العمل. التشغيل الجاري يحجب التشغيل المجدول التالي بدل فتح كاتب ثانٍ، وهو الصيغة التعريفية لملف القفل الذي يطلب توثيق DuckDB بناءه. ينطبق ذلك على عمليات تشغيل سير العمل هذا فقط، فأبعد العمليات الأخرى عن الملف نفسه.

ماذا يحدث حين تنفد الذاكرة في استعلام كبير؟

اضبط resources.limits.memory على سير العمل ليُحدّ التشغيل قبل أن يُسقط المضيف، وامنح الخطوة سياسة إعادة محاولة للإخفاقات العابرة لا البنيوية. الاستعلام الذي يحتاج ذاكرة أكبر مما لدى المضيف يُعاد كتابته لا تُعاد محاولته.

لدى DuckDB امتداد cron مجتمعي. لماذا لا نستخدمه؟

لأنه يجدول داخل عملية DuckDB، فالجدولة موجودة ما دامت تلك العملية حية. لا سجل تشغيل ولا إعادة محاولة ولا تنبيه عند الإخفاق ولا شيء تنظر إليه في الصباح التالي. يناسب ذلك تطبيقًا مضمّنًا طويل العمر، لا أعمالًا دفعية على خادم.

هل يستطيع DuckDB القراءة من S3 في تشغيل مجدول؟

نعم، عبر امتداد httpfs، وبهذه الطريقة تقرأ معظم أعمال DuckDB المجدولة ملفات Parquet دون خطوة استخراج منفصلة. ولأن تخزين الكائنات اعتماد شبكي، فهذه بالذات هي الخطوة التي تستحق سياسة إعادة محاولة.

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.