PHP 8.2 وjson_decode على استجابات 40 ميغابايت: أين يختنق الذاكرة

这篇内容暂时只有 العربية 版本,以下是原文。

خادم Ubuntu 22.04، PHP 8.2.18 FPM، memory_limit مضبوط على 256M. نقطة نهاية تستقبل ردًا JSON من مزوّد خارجي بحجم 40–55 ميغابايت (قائمة أحداث، متوسط 120 ألف عنصر). العَرَض: الطلب ينجح أحيانًا ويرمي Allowed memory size exhausted أحيانًا أخرى، مع أن حجم الملف على القرص لا يتجاوز 55M. استهلاك RSS للعامل يقفز إلى 600M قبل السقوط.

أول ما فعلته هو قياس الفرق بين حجم النص وارتفاع الذاكرة، لأن json_decode يبني شجرة كاملة من كائنات stdClass ومصفوفات قبل أن تعود إليك:

$raw = file_get_contents($path);
echo memory_get_usage(true) / 1048576, " MB after read\n";
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
echo memory_get_usage(true) / 1048576, " MB after decode\n";

النتيجة على عيّنة 42M: حوالي 60M بعد القراءة، و470M بعد الفك. أي أن معامل التضخّم يقارب 7–10×، وهذا ليس تسريبًا بل طبيعة البنية. الحل الذي اعتمدته هو التدفّق بدل الفك الكامل: أقرأ الملف عبر fopen وأمرّر قطعًا إلى محلّل تدفّقي مبني على JsonMachine، وأعالج كل عنصر فورًا ثم أحرّره:

$items = \JsonMachine\Items::fromStream(
    fopen($path, 'rb'),
    ['pointer' => '/events', 'decoder' => new \JsonMachine\ItemDecoder()]
);
foreach ($items as $event) {
    $this->handle($event); // لا نحتفظ بالمرجع بعد هذه النقطة
}

السبب في نجاح هذا: الذروة تصبح مرتبطة بحجم أكبر عنصر واحد لا بحجم المستند، فتنزل من 470M إلى أقل من 90M على نفس العيّنة. الثمن واضح: تفقد إمكانية الوصول العشوائي، ولا يمكنك التراجع للخلف في التدفّق، وأي منطق يحتاج العنصر السابق يجب أن تحتفظ به يدويًا. كذلك JSON_THROW_ON_ERROR لا يعمل مع التدفّق، فأخطاء البنية تظهر متأخرة عند العنصر المعطوب لا في البداية، وهذا يغيّر شكل معالجة الأخطاء عندك.

متى يفشل هذا؟ إذا كان المسار المطلوب داخل المستند متأخرًا جدًا (مثلًا /meta/summary في آخر الملف) فستقرأ كل شيء للوصول إليه؛ وإذا كان العنصر الواحد نفسه ضخمًا (تسلسل واحد بحجم 30M) فلا فائدة من التدفّق. جربت أيضًا رفع memory_limit إلى 1G كحل مؤقت، ونجح، لكنه جعل أربعة عمّال FPM متزامنين يستهلكون الذاكرة كلها ويقتلون الخادم عند أول موجة طلبات — الارتفاع لم يختفِ، انتقل فقط إلى مكان أسوأ. نقطة أخرى: json_decode مع true للمصفوفات يستهلك ذاكرة أقل بقليل من الكائنات، لكن الفرق لا يعوّل عليه أمام معامل 7×.

السؤال الذي يهمني: من جرّب json_validate() في 8.3 قبل التدفّق لتقصير زمن الفشل — هل غطّى عندكم حالة المستند الصحيح بنيويًا لكن العنصر الواحد أكبر من الحد، أم أنكم تعاملتم معها بقاعدة حد أقصى لحجم العنصر قبل المحلّل؟

0
赞同度 100% · 0 条评论 · 6 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。