Salah satu pertanyaan yang kadang kami terima adalah, bagaimana saya dapat memvalidasi daftar email secara massal dengan validasi penerima? Ada dua opsi di sini, satu adalah mengunggah file melalui SparkPost UI untuk validasi, dan yang lainnya adalah melakukan panggilan individu per email ke API (karena API adalah validasi email tunggal).
Opsi pertama bekerja dengan baik tetapi memiliki batasan 20Mb (sekitar 500.000 alamat). Bagaimana jika seseorang memiliki daftar email yang berisi jutaan alamat? Itu bisa berarti membaginya menjadi ribuan unggahan file CSV.
Karena mengunggah ribuan file CSV tampaknya sedikit tidak masuk akal, saya mengambil kasus penggunaan itu dan mulai bertanya-tanya seberapa cepat saya bisa membuat API berjalan. Dalam posting blog ini, saya akan menjelaskan apa yang saya coba dan bagaimana saya akhirnya sampai pada program yang dapat melakukan 100.000 validasi dalam 55 detik (sedangkan di UI saya mendapatkan sekitar 100.000 validasi dalam 1 menit 10 detik). Dan meskipun ini masih akan memakan waktu sekitar 100 jam untuk menyelesaikan 654 juta validasi, skrip ini dapat berjalan di latar belakang menghemat waktu yang signifikan.
Versi akhir dari program ini dapat ditemukan di sini.
Kesalahan pertama saya: menggunakan Python
Python adalah salah satu bahasa pemrograman favorit saya. Ia unggul dalam banyak bidang dan sangat mudah dipahami. Namun, satu bidang di mana ia tidak unggul adalah proses bersamaan. Meskipun Python memiliki kemampuan untuk menjalankan fungsi asinkron, ia memiliki apa yang dikenal sebagai Python Global Interpreter Lock atau GIL.
“Python Global Interpreter Lock atau GIL, dalam kata-kata sederhana, adalah mutex (atau kunci) yang hanya memungkinkan satu thread memegang kendali interpreter Python.
Ini berarti bahwa hanya satu thread yang dapat berada dalam keadaan eksekusi pada titik mana pun dalam waktu. Dampak GIL tidak terlihat bagi pengembang yang menjalankan program thread tunggal, tetapi ini dapat menjadi bottleneck kinerja dalam kode yang bergantung pada CPU dan multi-threaded.
Karena GIL hanya memungkinkan satu thread untuk dieksekusi pada satu waktu bahkan dalam arsitektur multi-threaded dengan lebih dari satu inti CPU, GIL telah mendapatkan reputasi sebagai fitur Python yang "terkenal".” (https://realpython.com/python-gil/)”
Pada awalnya, saya tidak menyadari GIL, jadi saya mulai memprogram dalam Python. Pada akhirnya, meskipun program saya asinkron, ia terjebak, dan tidak peduli berapa banyak thread yang saya tambahkan, saya tetap hanya mendapatkan sekitar 12-15 iterasi per detik.
Bagian utama dari fungsi asinkron dalam Python dapat dilihat di bawah:
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)
Jadi saya membuang penggunaan Python dan kembali ke papan gambar…
Saya memutuskan untuk menggunakan NodeJS karena kemampuannya untuk melakukan operasi I/O non-blok dengan sangat baik. Saya juga sudah cukup familiar dengan pemrograman di NodeJS.
Dengan memanfaatkan aspek asinkron dari NodeJS, ini berjalan dengan baik. Untuk lebih jelasnya tentang pemrograman asinkron di NodeJS, lihat https://blog.risingstack.com/node-hero-async-programming-in-node-js/
Kesalahan kedua saya: mencoba membaca file ke dalam memori
Ide awal saya adalah sebagai berikut:
Pertama, mengonsumsi daftar email CSV. Kedua, memuat email ke dalam array dan memeriksa apakah mereka dalam format yang benar. Ketiga, memanggil API validasi penerima secara asinkron. Keempat, menunggu hasil dan memuatnya ke dalam variabel. Dan akhirnya, mengeluarkan variabel ini ke dalam file CSV.
Ini berjalan dengan sangat baik untuk file yang lebih kecil. Masalah muncul ketika saya mencoba menjalankan 100.000 email. Program terhenti pada sekitar 12.000 validasi. Dengan bantuan salah satu pengembang front-end kami, saya melihat bahwa masalahnya adalah dengan memuat semua hasil ke dalam variabel (dan karena itu kehabisan memori dengan cepat). Jika Anda ingin melihat iterasi pertama dari program ini, saya telah menautkannya di sini: Versi 1 (TIDAK DIREKOMENDASIKAN).
Pertama, mengonsumsi daftar email CSV. Kedua, menghitung jumlah email dalam file untuk tujuan pelaporan. Ketiga, saat setiap baris dibaca secara asinkron, panggil API validasi penerima dan keluarkan hasilnya ke file CSV.
Dengan demikian, untuk setiap baris dibaca, saya memanggil API dan menulis hasilnya secara asinkron agar tidak menyimpan data ini dalam memori jangka panjang. Saya juga menghapus pemeriksaan sintaks email setelah berbicara dengan tim validasi penerima, karena mereka memberi tahu saya validasi penerima sudah memiliki pemeriksaan yang tertanam untuk memeriksa apakah email valid atau tidak.
Membongkar kode akhir
Setelah membaca dan memvalidasi argumen terminal, saya menjalankan kode berikut. Pertama, saya membaca file CSV email dan menghitung setiap baris. Ada dua tujuan dari fungsi ini, 1) ini memungkinkan saya untuk melaporkan kemajuan file dengan akurat [seperti yang akan kita lihat nanti], dan 2) ini memungkinkan saya untuk menghentikan timer ketika jumlah email dalam file sama dengan validasi yang selesai. Saya menambahkan timer agar saya dapat menjalankan benchmark dan memastikan saya mendapatkan hasil yang baik.
let count = 0; //Penghitungan baris require("fs") .createReadStream(myArgs[1]) .on("data", function (chunk) { for (let i = 0; i < chunk.length; ++i) if (chunk[i] == 10) count++; }) //Membaca infile dan meningkatkan hitungan untuk setiap baris .on("close", function () { //Pada akhir infile, setelah semua baris dihitung, jalankan fungsi validasi penerima validateRecipients.validateRecipients(count, myArgs); });
Saya kemudian memanggil fungsi validateRecipients. Perhatikan bahwa fungsi ini adalah asinkron. Setelah memvalidasi bahwa infile dan outfile adalah CSV, saya menulis baris judul, dan memulai timer program menggunakan pustaka JSDOM.
async function validateRecipients(email_count, myArgs) { if ( //Jika baik infile dan outfile dalam format .csv extname(myArgs[1]).toLowerCase() == ".csv" && extname(myArgs[3]).toLowerCase() == ".csv" ) { let completed = 0; //Penghitung untuk setiap panggilan API email_count++; //Penghitung garis mengembalikan #garis - 1, ini dilakukan untuk memperbaiki jumlah garis //Mulai timer const { window } = new JSDOM(); const start = window.performance.now(); const output = fs.createWriteStream(myArgs[3]); //File output output.write( "Email,Valid,Result,Reason,Is_Role,Is_Disposable,Is_Free,Delivery_Confidence\n" ); //Tulis header di outfile
Skrip berikut benar-benar merupakan bagian besar dari program jadi saya akan membaginya dan menjelaskan apa yang terjadi. Untuk setiap baris dari infile:
Secara asinkron ambil baris itu dan panggil API validasi penerima.
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, }, }) //Untuk setiap baris yang dibaca dari infile, panggil API Validasi Penerima SparkPost
Kemudian, pada respons
Tambahkan email ke JSON (agar dapat mencetak email dalam CSV)
Memvalidasi jika alasan adalah null, dan jika demikian, isi nilai kosong (ini agar format CSV konsisten, karena dalam beberapa kasus alasan diberikan dalam respons)
Atur opsi dan kunci untuk modul json2csv.
Konversi JSON ke CSV dan keluarkan (menggunakan json2csv)
Tulis kemajuan di terminal
Akhirnya, jika jumlah email dalam file = validasi yang telah selesai, hentikan timer dan cetak hasilnya
.then(function (response) { response.data.results.email = String(email); //Menambahkan email sebagai pasangan nilai/kunci ke JSON respons yang akan digunakan untuk keluaran response.data.results.reason ? null : (response.data.results.reason = ""); //Jika alasan adalah null, set ke kosong agar CSV seragam //Menggunakan json-2-csv untuk mengonversi JSON ke format CSV dan keluarkan let options = { prependHeader: false, //Menonaktifkan nilai JSON dari ditambahkan sebagai baris header untuk setiap baris keys: [ "results.email", "results.valid", "results.result", "results.reason", "results.is_role", "results.is_disposable", "results.is_free", "results.delivery_confidence", ], //Mengatur urutan kunci }; let json2csvCallback = function (err, csv) { if (err) throw err; output.write(`${csv}\n`); }; converter.json2csv(response.data, json2csvCallback, options); completed++; //Tingkatkan penghitung API process.stdout.write(`Selesai dengan ${completed} / ${email_count}\r`); //Keluarkan status Selesai / Total ke konsol tanpa menunjukkan baris baru //Jika semua email telah selesai divalidasi if (completed == email_count) { const stop = window.performance.now(); //Hentikan timer console.log( `Semua email berhasil divalidasi dalam ${ (stop - start) / 1000 } detik` ); } })
Satu masalah terakhir yang saya temukan adalah saat ini berjalan dengan baik di Mac, saya mengalami kesalahan berikut menggunakan Windows setelah sekitar 10.000 validasi:
Kesalahan: connect ENOBUFS XX.XX.XXX.XXX:443 – Lokal (tidak terdefinisi:tidak terdefinisi) dengan email XXXXXXX@XXXXXXXXXX.XXX
Setelah melakukan penelitian lebih lanjut, tampaknya ini adalah masalah dengan kolam koneksi HTTP NodeJS yang tidak menggunakan kembali koneksi. Saya menemukan artikel Stackoverflow tentang masalah ini, dan setelah menggali lebih dalam, menemukan konfigurasi default yang baik untuk pustaka axios yang menyelesaikan masalah ini. Saya masih tidak yakin mengapa masalah ini hanya terjadi di Windows dan bukan di Mac.
Langkah Selanjutnya
Untuk seseorang yang mencari program sederhana cepat yang mengambil CSV, memanggil API validasi penerima, dan mengeluarkan CSV, program ini untuk Anda.
Beberapa tambahan untuk program ini adalah sebagai berikut:
Membangun antarmuka depan atau UI yang lebih mudah untuk digunakan
Pemrosesan kesalahan dan percobaan yang lebih baik karena jika suatu alasan API melempar kesalahan, program saat ini tidak mencoba ulang panggilan tersebut
Saya juga penasaran untuk melihat apakah hasil yang lebih cepat dapat dicapai dengan bahasa lain seperti Golang atau Erlang/Elixir.
Silakan beri saya umpan balik atau saran untuk memperluas proyek ini.