بناء أداة التحقق من المستلمين لشركة Bird بشكل غير متزامن وبالجملة

زاكاري صامويلز

26‏/05‏/2022

البريد الإلكتروني

1 min read

بناء أداة التحقق من المستلمين لشركة Bird بشكل غير متزامن وبالجملة

زاكاري صامويلز

26‏/05‏/2022

البريد الإلكتروني

1 min read

بناء أداة التحقق من المستلمين لشركة Bird بشكل غير متزامن وبالجملة

بالنسبة للشخص الذي يبحث عن برنامج بسيط وسريع يقبل ملف بصيغة CSV، ويستدعي واجهة برمجة تطبيقات التحقق من المستلم، ويُخرج ملف CSV، فهذا البرنامج مناسب لك.

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

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

يعمل الخيار الأول بشكل رائع ولكنه يقتصر عند حد 20 ميغابايت (حوالي 500,000 عنوان). ماذا لو كان لدى شخص ما قائمة بريد إلكتروني تحتوي على ملايين العناوين؟ قد يعني ذلك تقسيمه إلى 1,000 تحميل لملفات CSV.

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

يمكن العثور على النسخة النهائية من هذا البرنامج هنا.

أول خطأ لي: استخدام Python

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

"قفل المفسر العالمي للبايثون أو GIL، ببساطة، هو عبارة عن مغلاق يسمح فقط لخط واحد بالتحكم في مفسر بايثون.

هذا يعني أن خطًا واحدًا فقط يمكن أن يكون في حالة تنفيذ في أي وقت. تأثير GIL غير مرئي للمطورين الذين ينفذون برامج ذات خيط واحد، لكنه يمكن أن يكون عنق الزجاجة للأداء في الأكواد التي تعتمد على المعالج المتعدد الخيوط.

نظرًا لأن GIL يسمح فقط لخط واحد بالتنفيذ في أي وقت حتى في بنية متعددة الخيوط مع أكثر من نواة CPU واحدة، فقد اكتسب GIL سمعة باعتباره ميزة "سيئة السمعة" للبايثون." (https://realpython.com/python-gil/)

في البداية، لم أكن على علم بـ GIL، لذلك بدأت البرمجة في بايثون. في النهاية، على الرغم من أن برنامجي كان غير متزامن، إلا أنه كان محبوسًا، وبغض النظر عن عدد الخيوط التي أضفتها، حصلت فقط على حوالي 12-15 تكرارًا في الثانية.

يمكن رؤية الجزء الرئيسي من الوظيفة غير المتزامنة في البايثون أدناه:

async def validateRecipients(f, fh, apiKey, snooze, count): h = {'Authorization': apiKey, 'Accept': 'application/json'} with tqdm(total=count) as pbar: async with aiohttp.ClientSession() as session: for address in f: for i in address: thisReq = requests.compat.urljoin(url, i) async with session.get(thisReq, headers=h, ssl=False) as resp: content = await resp.json() row = content['results'] row['email'] = i fh.writerow(row) pbar.update(1)

لذلك توقفت عن استخدام البايثون وعدت للتخطيط من جديد...

قررت استخدام NodeJS نظرًا لقدرتها على أداء عمليات الإدخال/الإخراج غير المتزامنة بشكل جيد جداً. خيار آخر ممتاز لمعالجة API غير المتزامنة هو بناء مستهلكات webhook خالية من الخوادم باستخدام وظائف Azure، والتي يمكن أن تتعامل بكفاءة مع أحمال العمل المتغيرة. أنا أيضًا معتاد إلى حد ما على البرمجة في NodeJS.

باستخدام جوانب غير متزامنة من NodeJS، انتهى الأمر بالعمل بشكل جيد. للحصول على مزيد من التفاصيل حول البرمجة غير المتزامنة في NodeJS، راجع https://blog.risingstack.com/node-hero-async-programming-in-node-js/

خطأي الثاني: محاولة قراءة الملف في الذاكرة

كانت فكرتي الأولية كما يلي:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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

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

كانت فكرتي الأولية كما يلي:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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

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

كانت فكرتي الأولية كما يلي:

Flowchart illustrating the process of validating a CSV list of emails, starting with ingestion, format checking, asynchronous API validation, result aggregation, and concluding with outputting to a CSV file.


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

عمل هذا بشكل جيد جدًا للملفات الصغيرة. أصبحت المشكلة عندما حاولت تشغيل 100,000 بريد إلكتروني. توقف البرنامج عند حوالي 12,000 تحقق. بمساعدة أحد مطوري الواجهة الأمامية، رأيت أن المشكلة كانت في تحميل جميع النتائج في متغير (وبالتالي نفاد الذاكرة بسرعة). إذا كنت ترغب في رؤية النسخة الأولى من هذا البرنامج، فقد قمت بربطها هنا: الإصدار 1 (غير موصى به).


Flowchart illustrating an email processing workflow, showing steps from ingesting a CSV list of emails to outputting results to a CSV file, with asynchronous validation via an API.


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

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

تحليل الكود النهائي

بعد قراءة وفحص معطيات الجهاز الطرفي، أقوم بتشغيل الكود التالي. أولاً، أقوم بقراءة ملف CSV من الرسائل الإلكترونية وعد كل سطر. هناك غرضان لهذه الوظيفة: 1) يمكنني من خلالها الإبلاغ بدقة عن تقدم الملف [كما سنرى لاحقًا]، و2) يمكنني التوقف عن المؤقت عندما يساوي عدد الرسائل الإلكترونية في الملف عدد الفحوصات المكتملة. أضفت مؤقتًا حتى أتمكن من تشغيل اختبارات الأداء وضمان الحصول على نتائج جيدة.

let count = 0; //Line count require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Reads the infile and increases the count for each line .on("close", function () { //At the end of the infile, after all lines have been counted, run the recipient validation function validateRecipients.validateRecipients(count, myArgs); });


 ثم أدعو وظيفة validateRecipients. لاحظ أن هذه الوظيفة غير متزامنة. بعد التأكد من أن infile وoutfile هما بتنسيق CSV، أكتب صفًا للرأس وأبدأ مؤقت البرنامج باستخدام مكتبة JSDOM.

async function validateRecipients(email_count, myArgs) { if ( //If both the infile and outfile are in .csv format extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Counter for each API call email_count++; //Line counter returns #lines - 1, this is done to correct the number of lines //Start a timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //Outfile output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Write the headers in the outfile


النص البرمجي التالي هو الجزء الأكبر من البرنامج لذلك سأقوم بتقسيمه وشرح ما يحدث. لكل سطر من infile:

fs.createReadStream(myArgs[1]) .pipe(csv.parse({ headers: false })) .on("data", async (email) => { let url = SPARKPOST_HOST + "/api/v1/recipient-validation/single/" + email; await axios .get(url, { headers: { Authorization: SPARKPOST_API_KEY, }, }) //For each row read in from the infile, call the SparkPost Recipient Validation API


ثم، على الرد

  • أضف البريد الإلكتروني إلى JSON (لتمكين طباعة البريد الإلكتروني في CSV)

  • تحقق مما إذا كان السبب فارغًا، وإذا كان كذلك، املأ بقيمة فارغة (وذلك لضمان تنسيق CSV بشكل متسق، حيث يتم تقديم السبب في بعض الحالات في الرد)

  • حدد الخيارات والمفاتيح للوحدة json2csv.

  • قم بتحويل JSON إلى CSV وأخرج (باستخدام json2csv)

  • اكتب التقدم في الجهاز الطرفي

  • أخيرًا، إذا كان عدد الرسائل الإلكترونية في الملف = الفحوصات المكتملة، أوقف المؤقت واطبع النتائج


.then(function (response) { response.data.results.email = String(email); //Adds the email as a value/key pair to the response JSON to be used for output response.data.results.reason ? null : (response.data.results.reason = ""); //If reason is null, set it to blank so the CSV is uniform //Utilizes json-2-csv to convert the JSON to CSV format and output let options = { prependHeader: false, //Disables JSON values from being added as header rows for every line keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Sets the order of keys }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Increase the API counter process.stdout.write(`Done with ${completed} / ${email_count}\r`); //Output status of Completed / Total to the console without showing new lines //If all emails have completed validation if (completed == email_count) { const stop = window.performance.now(); //Stop the timer console.log( `All emails successfully validated in ${ (stop - start) / 1000 } seconds` ); } })

 

كان هناك مشكلة نهائية واجهتها وهي أثناء أن هذا يعمل بشكل رائع على Mac، واجهت الخطأ التالي عند استخدام Windows بعد حوالي 10,000 تحقق:

خطأ: connect ENOBUFS XX.XX.XXX.XXX:443 – Local (undefined:undefined) مع البريد الإلكتروني XXXXXXX@XXXXXXXXXX.XXX

بعد إجراء بعض الأبحاث الإضافية، يبدو أنه مشكلة مع مجموعة اتصال العميل HTTP الخاصة بـ NodeJS التي لا تعيد استخدام الاتصالات. وجدت مقالة على Stackoverflow حول المشكلة، وبعد المزيد من البحث، وجدت إعداد افتراضي جيد لمكتبة axios الذي حل هذه المشكلة. لا أزال غير متأكد من سبب حدوث هذه المشكلة فقط على Windows وليس على Mac.

الخطوات التالية

لشخص يبحث عن برنامج بسيط وسريع يستقبل ملف CSV، ويستدعي API التحقق من المستلم، ويُصدر ملف CSV، هذا البرنامج مناسب لك.

بعض الإضافات لهذا البرنامج ستكون كما يلي:

  • إنشاء واجهة أمامية أو واجهة مستخدم أسهل للاستخدام

  • تحسين معالجة الأخطاء والمحاولات لأن البرنامج حالياً لا يعيد المحاولة إذا حدث خطأ في استدعاء API لسبب ما

  • اعتبر التنفيذ كـ وظيفة Azure بدون خادم لتحقيق التوسع التلقائي وتقليل إدارة البنية التحتية


سأكون فضولياً أيضًا لمعرفة ما إذا كان يمكن تحقيق نتائج أسرع باستخدام لغة أخرى مثل Golang أو Erlang/Elixir. إلى جانب اختيار اللغة، يمكن أن تؤثر قيود البنية التحتية أيضًا على الأداء - لقد تعلمنا ذلك بأنفسنا عندما واجهنا قيود DNS غير موثقة في AWS التي أثرت على أنظمة معالجة البريد الإلكتروني ذات الحجم الكبير لدينا.

للمطورين المهتمين بدمج معالجة API مع أدوات سير العمل البصرية، اطلع على كيفية دمج Flow Builder مع Google Cloud Functions لأتمتة سير العمل بدون كود.

يرجى عدم التردد في تقديم أي ملاحظات أو اقتراحات لتوسيع هذا المشروع.

دعنا نوصلك بخبير من Bird.
رؤية القوة الكاملة لـ Bird في 30 دقيقة.

بتقديمك طلبًا، فإنك توافق على أن تقوم Bird بالاتصال بك بشأن منتجاتنا وخدماتنا.

يمكنك إلغاء الاشتراك في أي وقت. انظر بيان الخصوصية الخاص بـ Bird للتفاصيل حول معالجة البيانات.

دعنا نوصلك بخبير من Bird.
رؤية القوة الكاملة لـ Bird في 30 دقيقة.

بتقديمك طلبًا، فإنك توافق على أن تقوم Bird بالاتصال بك بشأن منتجاتنا وخدماتنا.

يمكنك إلغاء الاشتراك في أي وقت. انظر بيان الخصوصية الخاص بـ Bird للتفاصيل حول معالجة البيانات.

دعنا نوصلك بخبير من Bird.
رؤية القوة الكاملة لـ Bird في 30 دقيقة.

بتقديمك طلبًا، فإنك توافق على أن تقوم Bird بالاتصال بك بشأن منتجاتنا وخدماتنا.

يمكنك إلغاء الاشتراك في أي وقت. انظر بيان الخصوصية الخاص بـ Bird للتفاصيل حول معالجة البيانات.

R

وصول

G

نمو

م

إدارة

A

أتمتة

النشرة الإخبارية

ابقَ على اطلاع مع Bird من خلال التحديثات الأسبوعية إلى بريدك الوارد.