Arsitektur Pyodide pada Background Worker
Menjalankan kode Python di dalam WebAssembly (WASM) via Pyodide menawarkan lingkungan eksekusi tersandbox tanpa overhead virtual machine atau container terpisah. Pola ini populer untuk multi-tenant job queue, evaluasi plugin, dan data processing dinamis. Namun, deployment Pyodide di level produksi memunculkan dua masalah operasional utama: latensi jaringan akibat resolusi wheel package berulang dan state bleed (kebocoran variabel/memori) antar-task pada instance runtime yang dipakai bersama.
Eliminasi Cold Start: Strategi Wheel Pre-Caching
Secara default, instalasi package via micropip.install() mengunduh wheel WebAssembly dari remote PyPI atau CDN Pyodide setiap kali runtime diinisialisasi. Operasi I/O jaringan ini membuat throughput worker anjlok dan memperkenalkan single point of failure jika registry mengalami rate limiting atau downtime.
1. Local Filesystem Wheel Store
Simpan wheel yang dibutuhkan (baik pure-Python maupun build WASM/Emscripten) langsung ke layer kontainer worker saat fase build Docker:
# Dockerfile: Cache dependencies ke filesystem lokal
RUN mkdir -p /opt/pyodide-cache && \
pip download \
--only-binary=:all: \
--dest /opt/pyodide-cache \
pandas numpy pydantic2. Injeksi Wheel Tanpa Jaringan
Di runtime Node.js, mapping direktori lokal tersebut ke file path internal Pyodide atau sajikan via web server loopback lokal (http://127.0.0.1:8080). Pyodide dapat langsung memuat wheel dari path disk tanpa DNS lookup atau negosiasi TLS:
// Inisialisasi package langsung dari local file buffer
const fs = require('fs');
const wheelBuffer = fs.readFileSync('/opt/pyodide-cache/pydantic-2.0-py3-none-any.whl');
await pyodide.unpackArchive(wheelBuffer.buffer, 'wheel');
// Alternatif via micropip lokal:
await pyodide.runPythonAsync(`
import micropip
await micropip.install('file:///opt/pyodide-cache/pydantic-2.0-py3-none-any.whl')
`);Mitigasi State Bleed dan Memory Leaks
Pyodide mengeksekusi interpreter CPython di dalam satu linear memory buffer WebAssembly (WebAssembly.Memory). Menggunakan satu instance runtime untuk ribuan job secara beruntun memicu dua risiko:
- State Bleed: Job A mendefinisikan variabel global, memodifikasi
sys.modules, atau mengubah environment variable. Job B yang berjalan setelahnya dapat membaca atau terpengaruh oleh state Job A tersebut. - Memory Bloat: CPython garbage collection mengembalikan objek ke allocator internal, namun runtime WebAssembly tidak otomatis melepaskan alokasi heap linear memory kembali ke sistem operasi host.
Solusi: Fresh Dict Scope dan Scheduled Recycling
Isolasi total hanya bisa dijamin dengan me-recreate instance Pyodide. Namun, inisialisasi WASM baru pada setiap job memakan waktu sekitar 100-300 ms. Solusi optimalnya adalah pendekatan hibrida: gunakan dictionary scope yang bersih untuk setiap eksekusi task, dipadukan dengan worker recycling setelah ambang batas tertentu.
Implementasi Queue Runner Node.js
Contoh runner background queue berikut mengimplementasikan eksekusi task terisolasi, injeksi wheel lokal, proteksi timeout, dan automatic worker recycling:
const { loadPyodide } = require('pyodide');
const { Worker } = require('bullmq');
const MAX_JOBS_BEFORE_RECYCLE = 100;
const TASK_TIMEOUT_MS = 5000;
let pyodideInstance = null;
let jobCounter = 0;
async function getPyodide() {
if (!pyodideInstance || jobCounter >= MAX_JOBS_BEFORE_RECYCLE) {
// Recycle instance untuk mengosongkan WASM linear memory
pyodideInstance = await loadPyodide({
packageCacheDir: '/opt/pyodide-cache'
});
jobCounter = 0;
}
return pyodideInstance;
}
const worker = new Worker('python-tasks', async (job) => {
const py = await getPyodide();
jobCounter++;
const { code, params } = job.data;
// Buat isolated global dict agar tidak mencemari scope default
const cleanDict = py.toPy({});
const pyParams = py.toPy(params);
cleanDict.set('params', pyParams);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Pyodide execution timeout')), TASK_TIMEOUT_MS)
);
try {
const executionPromise = py.runPythonAsync(code, { globals: cleanDict });
const result = await Promise.race([executionPromise, timeoutPromise]);
return result ? (typeof result.toJs === 'function' ? result.toJs() : result) : null;
} finally {
// Destroy PyProxy handles untuk mencegah memory leak di bridge JS-WASM
cleanDict.destroy();
pyParams.destroy();
}
}, { connection: { host: 'localhost', port: 6379 } });Evaluasi Trade-off: Throughput vs Isolasi
| Strategi Eksekusi | Throughput | Isolasi State | Skenario Ideal |
|---|---|---|---|
| Full Sandbox (New Pyodide per Task) | Rendah (~3-8 job/detik) | Sempurna | Eksekusi untrusted user code dengan payload arbitrer. |
| Warm Runtime + Clean Scope + Periodic Recycle | Tinggi (~50-200 job/detik) | Tinggi (Terbatas pada single context) | Pipeline data internal, transformasi JSON, evaluasi rule engine. |
| Persistent Shared Scope | Sangat Tinggi | Tidak Ada (Rentan State Bleed) | Tidak disarankan untuk background job queue. |
Kombinasi bundling wheel lokal dan rotasi instance berkala memberikan keseimbangan optimal: menghilangkan dependensi eksternal saat runtime serta membatasi dampak memory fragmentation WebAssembly tanpa mengorbankan kecepatan throughput eksekusi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!