Pengujian A/B Fleksibel dengan AWS Lambda@Edge
Diterbitkan: 2022-03-11Jaringan pengiriman konten (CDN) seperti Amazon CloudFront, hingga saat ini, merupakan bagian yang relatif sederhana dari infrastruktur web. Secara tradisional, aplikasi web dirancang di sekitarnya, memperlakukannya sebagian besar sebagai cache pasif daripada komponen aktif.
Lambda@Edge dan teknologi serupa telah mengubah semua itu dan membuka seluruh dunia kemungkinan dengan memperkenalkan lapisan logika baru antara aplikasi web dan penggunanya. Tersedia sejak pertengahan 2017, Lambda@Edge adalah fitur baru di AWS yang memperkenalkan konsep mengeksekusi kode dalam bentuk Lambdas langsung di server edge CloudFront.
Salah satu kemungkinan baru yang ditawarkan Lambda@Edge adalah solusi bersih untuk pengujian A/B sisi server. Pengujian A/B adalah metode umum untuk menguji kinerja berbagai variasi situs web dengan menunjukkannya secara bersamaan kepada segmen audiens situs web yang berbeda.
Apa Arti Lambda@Edge untuk Pengujian A/B
Tantangan teknis utama dalam pengujian A/B adalah mengelompokkan lalu lintas masuk dengan benar tanpa memengaruhi kualitas data eksperimen atau situs web itu sendiri dengan cara apa pun.
Ada dua rute utama untuk mengimplementasikannya: sisi klien dan sisi server .
- Sisi klien melibatkan menjalankan sedikit kode JavaScript di browser pengguna akhir yang memilih varian mana yang akan ditampilkan. Ada beberapa kelemahan signifikan dari pendekatan ini—terutama, ini dapat memperlambat rendering dan menyebabkan kedipan atau masalah rendering lainnya. Ini berarti situs web yang berusaha mengoptimalkan waktu pemuatannya atau memiliki standar tinggi untuk UX mereka akan cenderung menghindari pendekatan ini.
- Pengujian A/B sisi server menghilangkan sebagian besar masalah ini, karena keputusan varian mana yang akan dikembalikan sepenuhnya berada di pihak host. Peramban hanya merender setiap varian secara normal seolah-olah itu adalah versi standar situs web.
Dengan mengingat hal itu, Anda mungkin bertanya-tanya mengapa semua orang tidak hanya menggunakan pengujian A/B sisi server. Sayangnya, pendekatan sisi server tidak mudah diterapkan seperti pendekatan sisi klien, dan menyiapkan eksperimen seringkali memerlukan beberapa bentuk intervensi pada kode sisi server atau konfigurasi server.
Lebih rumit lagi, aplikasi web modern seperti SPA sering disajikan sebagai kumpulan kode statis langsung dari bucket S3 bahkan tanpa melibatkan server web. Bahkan ketika server web terlibat, seringkali tidak layak untuk mengubah logika sisi server untuk menyiapkan pengujian A/B. Kehadiran CDN menimbulkan kendala lain, karena caching dapat mempengaruhi ukuran segmen atau, sebaliknya, segmentasi lalu lintas semacam ini dapat menurunkan kinerja CDN.
Apa yang ditawarkan Lambda@Edge adalah cara untuk mengarahkan permintaan pengguna di seluruh varian eksperimen bahkan sebelum permintaan itu mengenai server Anda. Contoh dasar dari kasus penggunaan ini dapat ditemukan langsung di dokumentasi AWS. Meskipun berguna sebagai bukti konsep, lingkungan produksi dengan beberapa eksperimen bersamaan mungkin memerlukan sesuatu yang lebih fleksibel dan kuat.
Selain itu, setelah bekerja sedikit dengan Lambda@Edge, Anda mungkin akan menyadari bahwa ada beberapa nuansa yang harus diperhatikan saat membangun arsitektur Anda.
Misalnya, penerapan edge Lambdas membutuhkan waktu, dan lognya didistribusikan di seluruh wilayah AWS. Perhatikan hal ini jika Anda perlu men-debug konfigurasi Anda untuk menghindari kesalahan 502.
Tutorial ini akan memperkenalkan pengembang AWS cara menerapkan pengujian A/B sisi server menggunakan Lambda@Edge dengan cara yang dapat digunakan kembali di seluruh eksperimen tanpa memodifikasi dan menerapkan ulang Lambdas edge. Itu dibangun berdasarkan pendekatan contoh dalam dokumentasi AWS dan tutorial serupa lainnya, tetapi alih-alih mengkodekan aturan alokasi lalu lintas di Lambda itu sendiri, aturan diambil secara berkala dari file konfigurasi di S3 yang dapat Anda ubah kapan saja.
Ikhtisar Pendekatan Pengujian A/B Lambda@Edge Kami
Ide dasar di balik pendekatan ini adalah agar CDN menetapkan setiap pengguna ke segmen dan kemudian mengarahkan pengguna ke konfigurasi Origin terkait. CloudFront memungkinkan distribusi mengarah ke S3 atau asal kustom, dan dalam pendekatan ini, kami mendukung keduanya.
Pemetaan segmen untuk varian eksperimen akan disimpan dalam file JSON di S3. S3 dipilih di sini untuk kesederhanaan, tetapi ini juga dapat diambil dari database atau bentuk penyimpanan lainnya yang dapat diakses oleh tepi Lambda.
Catatan: Ada beberapa batasan - lihat artikel Memanfaatkan data eksternal di Lambda@Edge di Blog AWS untuk info selengkapnya.
Penerapan
Lambda@Edge dapat dipicu oleh empat jenis peristiwa CloudFront yang berbeda:
Dalam hal ini, kami akan menjalankan Lambda pada masing-masing dari tiga peristiwa berikut:
- Permintaan pemirsa
- Permintaan asal
- Tanggapan pemirsa
Setiap acara akan menerapkan langkah dalam proses berikut:
- abtesting-lambda-vreq : Sebagian besar logika terkandung dalam lambda ini. Pertama, cookie ID unik dibaca atau dibuat untuk permintaan yang masuk, dan kemudian di-hash ke kisaran [0, 1]. Peta alokasi lalu lintas kemudian diambil dari S3 dan di-cache di seluruh eksekusi. Dan terakhir, nilai hash down digunakan untuk memilih konfigurasi asal, yang diteruskan sebagai header yang disandikan JSON ke Lambda berikutnya.
- abtesting-lambda-oreq : Ini membaca konfigurasi asal dari Lambda sebelumnya dan merutekan permintaan yang sesuai.
- abtesting-lambda-vres : Ini hanya menambahkan header Set-Cookie untuk menyimpan cookie ID unik di browser pengguna.
Mari kita juga menyiapkan tiga keranjang S3, dua di antaranya akan berisi konten dari setiap varian eksperimen, sedangkan yang ketiga akan berisi file JSON dengan peta alokasi lalu lintas.
Untuk tutorial ini, ember akan terlihat seperti ini:
- abtesting- ttblog -publik
- index.html
- abtesting-ttblog-b publik
- index.html
- abtesting-ttblog-peta pribadi
- peta.json
Kode sumber
Pertama, mari kita mulai dengan peta alokasi lalu lintas:
peta.json
{ "segments": [ { "weight": 0.7, "host": "abtesting-ttblog-a.s3.amazonaws.com", "origin": { "s3": { "authMethod": "none", "domainName": "abtesting-ttblog-a.s3.amazonaws.com", "path": "", "region": "eu-west-1" } } }, { "weight": 0.3, "host": "abtesting-ttblog-b.s3.amazonaws.com", "origin": { "s3": { "authMethod": "none", "domainName": "abtesting-ttblog-b.s3.amazonaws.com", "path": "", "region": "eu-west-1" } } } ] }
Setiap segmen memiliki bobot lalu lintas, yang akan digunakan untuk mengalokasikan jumlah lalu lintas yang sesuai. Kami juga menyertakan konfigurasi asal dan host. Format konfigurasi asal dijelaskan dalam dokumentasi AWS.
abtesting-lambda-vreq
'use strict'; const aws = require('aws-sdk'); const COOKIE_KEY = 'abtesting-unique-id'; const s3 = new aws.S3({ region: 'eu-west-1' }); const s3Params = { Bucket: 'abtesting-ttblog-map', Key: 'map.json', }; const SEGMENT_MAP_TTL = 3600000; // TTL of 1 hour const fetchSegmentMapFromS3 = async () => { const response = await s3.getObject(s3Params).promise(); return JSON.parse(response.Body.toString('utf-8')); } // Cache the segment map across Lambda invocations let _segmentMap; let _lastFetchedSegmentMap = 0; const fetchSegmentMap = async () => { if (!_segmentMap || (Date.now() - _lastFetchedSegmentMap) > SEGMENT_MAP_TTL) { _segmentMap = await fetchSegmentMapFromS3(); _lastFetchedSegmentMap = Date.now(); } return _segmentMap; } // Just generate a random UUID const getRandomId = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }; // This function will hash any string (our random UUID in this case) // to a [0, 1) range const hashToInterval = (s) => { let hash = 0, i = 0; while (i < s.length) { hash = ((hash << 5) - hash + s.charCodeAt(i++)) << 0; } return (hash + 2147483647) % 100 / 100; } const getCookie = (headers, cookieKey) => { if (headers.cookie) { for (let cookieHeader of headers.cookie) { const cookies = cookieHeader.value.split(';'); for (let cookie of cookies) { const [key, val] = cookie.split('='); if (key === cookieKey) { return val; } } } } return null; } const getSegment = async (p) => { const segmentMap = await fetchSegmentMap(); let weight = 0; for (const segment of segmentMap.segments) { weight += segment.weight; if (p < weight) { return segment; } } console.error(`No segment for value ${p}. Check the segment map.`); } exports.handler = async (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; let uniqueId = getCookie(headers, COOKIE_KEY); if (uniqueId === null) { // This is what happens on the first visit: we'll generate a new // unique ID, then leave it the cookie header for the // viewer response lambda to set permanently later uniqueId = getRandomId(); const cookie = `${COOKIE_KEY}=${uniqueId}`; headers.cookie = headers.cookie || []; headers.cookie.push({ key: 'Cookie', value: cookie }); } // Get a value between 0 and 1 and use it to // resolve the traffic segment const p = hashToInterval(uniqueId); const segment = await getSegment(p); // Pass the origin data as a header to the origin request lambda // The header key below is whitelisted in Cloudfront const headerValue = JSON.stringify({ host: segment.host, origin: segment.origin }); headers['x-abtesting-segment-origin'] = [{ key: 'X-ABTesting-Segment-Origin', value: headerValue }]; callback(null, request); };
Di sini, kami secara eksplisit membuat ID unik untuk tutorial ini, tetapi cukup umum bagi sebagian besar situs web untuk memiliki beberapa ID klien lain yang dapat digunakan sebagai gantinya. Ini juga akan menghilangkan kebutuhan akan respons pemirsa Lambda.

Untuk pertimbangan kinerja, aturan alokasi lalu lintas di-cache di seluruh pemanggilan Lambda alih-alih mengambilnya dari S3 pada setiap permintaan. Dalam contoh ini, kami menyiapkan cache TTL 1 jam.
Perhatikan bahwa header X-ABTesting-Segment-Origin
perlu masuk daftar putih di CloudFront; jika tidak, itu akan dihapus dari permintaan sebelum mencapai permintaan asal Lambda.
abtesting-lambda-oreq
'use strict'; const HEADER_KEY = 'x-abtesting-segment-origin'; // Origin Request handler exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; const headerValue = headers[HEADER_KEY] && headers[HEADER_KEY][0] && headers[HEADER_KEY][0].value; if (headerValue) { const segment = JSON.parse(headerValue); headers['host'] = [{ key: 'host', value: segment.host }]; request.origin = segment.origin; } callback(null, request); };
Permintaan asal Lambda cukup mudah. Konfigurasi asal dan host dibaca dari header X-ABTesting-Origin
yang dihasilkan pada langkah sebelumnya dan disuntikkan ke dalam permintaan. Ini menginstruksikan CloudFront untuk merutekan permintaan ke asal yang sesuai jika terjadi kesalahan cache.
abtesting-lambda-vres
'use strict'; const COOKIE_KEY = 'abtesting-unique-id'; const getCookie = (headers, cookieKey) => { if (headers.cookie) { for (let cookieHeader of headers.cookie) { const cookies = cookieHeader.value.split(';'); for (let cookie of cookies) { const [key, val] = cookie.split('='); if (key === cookieKey) { return val; } } } } return null; } const setCookie = function (response, cookie) { console.log(`Setting cookie ${cookie}`); response.headers['set-cookie'] = response.headers['set-cookie'] || []; response.headers['set-cookie'] = [{ key: "Set-Cookie", value: cookie }]; } exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; const headers = request.headers; const response = event.Records[0].cf.response; const cookieVal = getCookie(headers, COOKIE_KEY); if (cookieVal != null) { setCookie(response, `${COOKIE_KEY}=${cookieVal}`); callback(null, response); return; } console.log(`no ${COOKIE_KEY} cookie`); callback(null, response); }
Terakhir, respons pemirsa Lambda bertanggung jawab untuk mengembalikan cookie ID unik yang dihasilkan di header Set-Cookie
. Seperti disebutkan di atas, jika ID klien unik sudah digunakan, Lambda ini dapat dihilangkan sepenuhnya.
Bahkan, bahkan dalam kasus ini, cookie dapat disetel dengan pengalihan oleh pengunjung yang meminta Lambda. Namun, ini dapat menambahkan beberapa latensi, jadi dalam kasus ini, kami lebih suka melakukannya dalam satu siklus permintaan-tanggapan.
Kode juga dapat ditemukan di GitHub.
Izin Lambda
Seperti halnya tepi Lambda, Anda dapat menggunakan cetak biru CloudFront saat membuat Lambda. Jika tidak, Anda harus membuat peran khusus dan melampirkan templat kebijakan "Izin Lambda@Edge Dasar".
Untuk permintaan penampil Lambda, Anda juga harus mengizinkan akses ke bucket S3 yang berisi file alokasi lalu lintas.
Menyebarkan Lambdas
Menyiapkan tepi Lambdas agak berbeda dari alur kerja Lambda standar. Pada halaman konfigurasi Lamba, klik “Tambahkan pemicu” dan pilih CloudFront. Ini akan membuka dialog kecil yang memungkinkan Anda untuk mengaitkan Lambda ini dengan distribusi CloudFront.
Pilih acara yang sesuai untuk masing-masing dari tiga Lambdas dan tekan "Sebarkan." Ini akan memulai proses penerapan kode fungsi ke server edge CloudFront.
Catatan: Jika Anda perlu memodifikasi tepi Lambda dan menerapkannya kembali, Anda harus menerbitkan versi baru terlebih dahulu secara manual.
Pengaturan CloudFront
Agar distribusi CloudFront dapat merutekan lalu lintas ke asal, Anda harus menyiapkan masing-masing secara terpisah di panel asal.
Satu-satunya pengaturan konfigurasi yang perlu Anda ubah adalah memasukkan header X-ABTesting-Segment-Origin
ke daftar putih. Di konsol CloudFront , pilih distribusi Anda lalu tekan edit untuk mengubah pengaturan distribusi.
Pada halaman Edit Perilaku , pilih Daftar Putih dari menu tarik-turun pada opsi Cache Based on Selected Request Headers dan tambahkan header X-ABtesting-Segment-Origin kustom ke daftar:
Jika Anda menerapkan tepi Lambdas seperti yang dijelaskan di bagian sebelumnya, mereka seharusnya sudah dikaitkan dengan distribusi Anda dan terdaftar di bagian terakhir halaman Edit Perilaku .
Solusi Bagus dengan Peringatan Kecil
Pengujian A/B sisi server dapat menjadi tantangan untuk diterapkan dengan benar untuk situs web dengan lalu lintas tinggi yang diterapkan di belakang layanan CDN seperti CloudFront. Dalam artikel ini, kami menunjukkan bagaimana Lambda@Edge dapat digunakan sebagai solusi baru untuk masalah ini dengan menyembunyikan detail implementasi ke dalam CDN itu sendiri, sementara juga menawarkan solusi yang bersih dan andal untuk menjalankan eksperimen A/B.
Namun, Lambda@Edge memiliki beberapa kelemahan. Yang terpenting, pemanggilan Lambda tambahan antara peristiwa CloudFront ini dapat bertambah baik dari segi latensi maupun biaya, sehingga dampaknya pada distribusi CloudFront harus diukur terlebih dahulu dengan cermat.
Selain itu, Lambda@Edge adalah fitur AWS yang relatif baru dan masih berkembang, jadi tentu saja, masih terasa agak kasar di tepinya. Pengguna yang lebih konservatif mungkin masih ingin menunggu beberapa saat sebelum menempatkannya pada titik kritis infrastruktur mereka.
Meskipun demikian, solusi unik yang ditawarkannya menjadikannya fitur yang tak terpisahkan dari CDN, jadi tidak masuk akal untuk mengharapkannya menjadi lebih banyak diadopsi di masa depan.