تجربة مليوني جلسة بدون رأس

تم نشره في 4 يونيو 2018 على مدونة الشركات بدون متصفح.

يسرنا أن نعلن أننا تجاوزنا مؤخرًا خط مليوني جلسة عمل ! هذه الملايين من لقطات الشاشة المولدة وملفات PDF المطبوعة والمواقع التي تم اختبارها. لقد فعلنا كل شيء تقريبًا يمكنك التفكير فيه باستخدام متصفح بلا رأس.

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

إليك بعض النصائح.

1. لا تستخدم متصفح بدون رأس على الإطلاق




استهلاك موارد الكروم بدون رأس


بأي حال من الأحوال ، إذا كان ذلك ممكنًا ، لا تبدأ المتصفح على الإطلاق في وضع بلا رأس . خاصة على نفس البنية التحتية للتطبيق الخاص بك (انظر أعلاه). المتصفح بدون رأس ، لا يمكن التنبؤ به ، وشراهة وتكاثر مثل السيد ميسيكس من ريك ومورتي. يمكن تنفيذ كل ما يمكن أن يفعله المتصفح تقريبًا (باستثناء إقحام JavaScript وتشغيله) باستخدام أدوات Linux البسيطة. تقدم مكتبات Cheerio وغيرها واجهة Node API أنيقة لاسترداد البيانات باستخدام طلبات HTTP والقص ، إذا كان هذا هو هدفك.

على سبيل المثال ، يمكنك اختيار صفحة (بافتراض أنها نوع من HTML) ونسخها باستخدام أوامر بسيطة مثل هذه:

import cheerio from 'cheerio'; import fetch from 'node-fetch'; async function getPrice(url) { const res = await fetch(url); const html = await res.test(); const $ = cheerio.load(html); return $('buy-now.price').text(); } getPrice('https://my-cool-website.com/'); 

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

2. لا تقم بتشغيل متصفح بدون رأس


لقد واجهنا العديد من المستخدمين الذين يحاولون الحفاظ على تشغيل المتصفح حتى إذا لم يكن قيد الاستخدام (مع اتصالات مفتوحة). على الرغم من أن هذه قد تكون إستراتيجية جيدة لتسريع الجلسة ، إلا أنها ستنهار في غضون ساعات قليلة. إلى حد كبير لأن المتصفحات تحب تخزين كل شيء على التوالي وتخزين الذاكرة بشكل تدريجي. بمجرد التوقف عن استخدام المتصفح بشكل مكثف ، قم بإغلاقه على الفور!

 import puppeteer from 'puppeteer'; async function run() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://www.example.com/'); // More stuff ...page.click() page.type() browser.close(); // <- Always do this! } 

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

3. صفحة صديقك


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

لدى Puppeteer العديد من الطرق الرائعة ، مثل تخزين محددات DOM وأشياء أخرى في بيئة العقدة. على الرغم من أنها مريحة للغاية ، يمكنك بسهولة تصوير نفسك في القدم إذا كان هناك شيء على الصفحة يجبر عقدة DOM على التحور . قد لا يكون هذا رائعًا ، ولكن في الواقع من الأفضل القيام بكل العمل على جانب المتصفح في سياق المتصفح . هذا يعني عادة تحميل الصفحة. page.evaulate على كل العمل الذي يجب القيام به.

على سبيل المثال ، بدلاً من شيء من هذا القبيل ( ثلاثة إجراءات غير متزامنة):

 const $anchor = await page.$('a.buy-now'); const link = await $anchor.getProperty('href'); await $anchor.click(); return link; 

من الأفضل القيام بذلك (إجراء غير متزامن):

 await page.evaluate(() => { const $anchor = document.querySelector('a.buy-now'); const text = $anchor.href; $anchor.click(); }); 

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

القاعدة الأساسية البسيطة هي حساب عدد await أو في الشفرة. إذا كان هناك أكثر من واحد ، فمن الأفضل على الأرجح تشغيل الكود داخل page.evaluate . والسبب هو أن جميع الإجراءات غير المتزامنة تتحرك ذهابًا وإيابًا بين وقت تشغيل العقدة والمتصفح ، مما يعني تسلسل JSON المستمر وإلغاء التسلسل. على الرغم من عدم وجود مثل هذا المبلغ الضخم من التحليل (لأن كل شيء مدعوم بواسطة WebSockets) ، فإنه لا يزال يستغرق وقتًا ، والذي يُنفق بشكل أفضل على شيء آخر.

4. موازية المتصفحات ، وليس صفحات الويب


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

بدلاً من ذلك:

 import puppeteer from 'puppeteer'; // Launch one browser and capture the promise const launch = puppeteer.launch(); const runJob = async (url) { // Re-use the browser here const browser = await launch; const page = await browser.newPage(); await page.goto(url); const title = await page.title(); browser.close(); return title; }; 

من الأفضل القيام بذلك:

 import puppeteer from 'puppeteer'; const runJob = async (url) { // Launch a clean browser for every "job" const browser = puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); const title = await page.title(); browser.close(); return title; }; 

يحصل كل مثيل متصفح جديد على نظيف --user-data-dir ( ما لم يذكر خلاف ذلك ). أي أنه يتم معالجتها بالكامل كجلسة جديدة جديدة. إذا تعطل Chrome لسبب ما ، فلن يسحب جلسات أخرى معه.

5. حدود الطابور والتزامن


واحدة من الميزات الرئيسية للمتصفح هي القدرة على الحد بدقة من التوازي والانتظار. لذا فإن تطبيقات العميل تعمل فقط على puppeteer.connect ، لكنها لا تفكر في تنفيذ قائمة الانتظار. يمنع هذا قدرًا كبيرًا من المشاكل ، بشكل أساسي مع مثيلات Chrome المتزامنة التي تلتهم جميع الموارد المتاحة لتطبيقك.

الطريقة الأفضل والأسهل هي التقاط صورة Docker وتشغيلها بالمعلمات الضرورية:

 # Pull in Puppeteer@1.4.0 support $ docker pull browserless/chrome:release-puppeteer-1.4.0 $ docker run -e "MAX_CONCURRENT_SESSIONS=10" browserless/chrome:release-puppeteer-1.4.0 

هذا يحد من عدد الطلبات المتزامنة إلى عشرة (بما في ذلك جلسات التصحيح والمزيد). تم تكوين قائمة الانتظار بواسطة المتغير MAX_QUEUE_LENGTH . عادةً ، يمكنك تنفيذ ما يقرب من 10 طلبات متزامنة لكل غيغابايت من الذاكرة. يمكن أن تختلف النسبة المئوية لاستخدام وحدة المعالجة المركزية للمهام المختلفة ، ولكن في الأساس ستحتاج إلى الكثير والكثير من ذاكرة الوصول العشوائي.

6. لا تنسى page.waitForNavigation


واحدة من أكثر المشاكل الشائعة التي واجهناها هي الإجراءات التي تبدأ في تحميل الصفحات مع الإنهاء المفاجئ للبرامج النصية. وذلك لأن الإجراءات التي تؤدي إلى pageload غالبًا ما تسبب ابتلاع العمل اللاحق. للتغلب على المشكلة ، تحتاج عادةً إلى استدعاء إجراء تحميل الصفحة - وبعد انتظار التحميل مباشرة.

على سبيل المثال ، لا تعمل هذه console.log في مكان واحد ( انظر العرض ):

 await page.goto('https://example.com'); await page.click('a'); const title = await page.title(); console.log(title); 

لكنها تعمل في مكان آخر ( انظر العرض ).

 await page.goto('https://example.com'); page.click('a'); await page.waitForNavigation(); const title = await page.title(); console.log(title); 

يمكنك قراءة المزيد عن waitForNavigation هنا . تحتوي هذه الوظيفة تقريبًا على نفس معلمات الواجهة مثل page.goto ، ولكن فقط مع جزء "الانتظار".

7. استخدم Docker لكل ما تحتاجه.


يحتاج Chrome إلى الكثير من التبعيات للعمل بشكل صحيح. حقا الكثير. حتى بعد تثبيت كل شيء ، يجب أن تقلق بشأن أشياء مثل الخطوط والعمليات الوهمية. لذلك ، من الأفضل استخدام نوع من الحاويات لوضع كل شيء هناك. تم تصميم Docker خصيصًا لهذه المهمة ، لأنه يمكنك تحديد كمية الموارد المتاحة وعزلها. إذا كنت ترغب في إنشاء Dockerfile الخاص بك ، فراجع أدناه جميع التبعيات اللازمة:

 # Dependencies needed for packages downstream RUN apt-get update && apt-get install -y \ unzip \ fontconfig \ locales \ gconf-service \ libasound2 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgcc1 \ libgconf-2-4 \ libgdk-pixbuf2.0-0 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ ca-certificates \ fonts-liberation \ libappindicator1 \ libnss3 \ lsb-release \ xdg-utils \ wget 

ولتجنب عمليات الزومبي (وهو أمر شائع في Chrome) ، من الأفضل استخدام شيء مثل dumb-init للتشغيل بشكل صحيح:

 ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init RUN chmod +x /usr/local/bin/dumb-init 

إذا كنت تريد معرفة المزيد ، ألق نظرة على ملف Dockerfile الخاص بنا .

8. تذكر اثنين من أوقات التشغيل المختلفة.


من المفيد أن نتذكر أن هناك وقتين لتشغيل جافا سكريبت (العقدة والمتصفح). يعد هذا أمرًا رائعًا لفصل المهام ، ولكن يحدث الالتباس حتمًا لأن بعض الطرق تتطلب تمرير ارتباط صريحًا بدلاً من الرفع.

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

وبالتالي ، بدلاً من الرجوع إلى selector عبر عمليات الإغلاق:

 const anchor = 'a'; await page.goto('https://example.com/'); // `selector` here is `undefined` since we're in the browser context const clicked = await page.evaluate(() => document.querySelector(anchor).click()); 

معلمة تمرير أفضل:

 const anchor = 'a'; await page.goto('https://example.com/'); // Here we add a `selector` arg and pass in the reference in `evaluate` const clicked = await page.evaluate((selector) => document.querySelector(selector).click(), anchor); 

page.evaluate إضافة وسيطة أو أكثر إلى دالة page.evaluate ، لأنها متغيرة هنا. تأكد من الاستفادة من هذا!

المستقبل


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

Source: https://habr.com/ru/post/ar413547/


All Articles