การทดสอบ A/B ที่ยืดหยุ่นด้วย AWS Lambda@Edge
เผยแพร่แล้ว: 2022-03-11เครือข่ายการจัดส่งเนื้อหา (CDN) เช่น Amazon CloudFront เป็นส่วนที่ค่อนข้างเรียบง่ายของโครงสร้างพื้นฐานเว็บ จนกระทั่งเมื่อไม่นานมานี้ ตามเนื้อผ้า เว็บแอปพลิเคชันได้รับการออกแบบโดยคำนึงถึงพวกเขา โดยส่วนใหญ่เป็นแคชแบบพาสซีฟแทนที่จะเป็นส่วนประกอบที่ทำงานอยู่
Lambda@Edge และเทคโนโลยีที่คล้ายคลึงกันได้เปลี่ยนแปลงสิ่งเหล่านั้นทั้งหมด และเปิดโลกทั้งใบของความเป็นไปได้โดยการแนะนำเลเยอร์ใหม่ของตรรกะระหว่างเว็บแอปพลิเคชันและผู้ใช้ พร้อมใช้งานตั้งแต่กลางปี 2017 Lambda@Edge เป็นคุณสมบัติใหม่ใน AWS ที่แนะนำแนวคิดของการรันโค้ดในรูปแบบของ Lambdas โดยตรงบนเซิร์ฟเวอร์ Edge ของ CloudFront
ความเป็นไปได้ใหม่ๆ ประการหนึ่งที่ Lambda@Edge นำเสนอคือโซลูชันที่สะอาดหมดจดสำหรับการทดสอบ A/B ฝั่งเซิร์ฟเวอร์ การทดสอบ A/B เป็นวิธีการทั่วไปในการทดสอบประสิทธิภาพของรูปแบบต่างๆ ของเว็บไซต์โดยแสดงให้กลุ่มผู้ชมต่างๆ ของเว็บไซต์ทราบพร้อมกัน
Lambda@Edge หมายถึงอะไรสำหรับการทดสอบ A/B
ความท้าทายทางเทคนิคหลักในการทดสอบ A/B คือการแบ่งกลุ่มการเข้าชมที่เข้ามาอย่างเหมาะสมโดยไม่กระทบต่อคุณภาพของข้อมูลการทดสอบหรือตัวเว็บไซต์เองแต่อย่างใด
มีสองเส้นทางหลักสำหรับการติดตั้ง: ฝั่งไคลเอ็นต์ และฝั่ง เซิร์ฟเวอร์
- ฝั่งไคลเอ็นต์ เกี่ยวข้องกับการเรียกใช้โค้ด JavaScript เล็กน้อยในเบราว์เซอร์ของผู้ใช้ปลายทาง ซึ่งจะเลือกรูปแบบที่จะแสดง มีข้อเสียที่สำคัญสองสามประการสำหรับแนวทางนี้ โดยเฉพาะอย่างยิ่ง มันสามารถทั้งทำให้การเรนเดอร์ช้าลงและทำให้เกิดการริบหรี่หรือปัญหาการเรนเดอร์อื่นๆ ซึ่งหมายความว่าเว็บไซต์ที่ต้องการเพิ่มประสิทธิภาพเวลาในการโหลดหรือมีมาตรฐานสูงสำหรับ UX มักจะหลีกเลี่ยงแนวทางนี้
- การทดสอบ A/B ฝั่งเซิร์ฟเวอร์ ช่วยขจัดปัญหาเหล่านี้ได้เกือบทั้งหมด เนื่องจากการตัดสินใจเลือกตัวแปรที่จะส่งคืนนั้นมาจากฝั่งโฮสต์ทั้งหมด เบราว์เซอร์จะแสดงแต่ละตัวแปรตามปกติราวกับว่าเป็นเวอร์ชันมาตรฐานของเว็บไซต์
ด้วยเหตุนี้ คุณอาจสงสัยว่าเหตุใดทุกคนจึงไม่ใช้การทดสอบ A/B ฝั่งเซิร์ฟเวอร์เพียงอย่างเดียว น่าเสียดายที่แนวทางฝั่งเซิร์ฟเวอร์นั้นไม่ง่ายที่จะนำไปใช้เหมือนแนวทางฝั่งไคลเอ็นต์ และการตั้งค่าการทดสอบมักจะต้องมีรูปแบบหนึ่งของการแทรกแซงในโค้ดฝั่งเซิร์ฟเวอร์หรือการกำหนดค่าเซิร์ฟเวอร์
เพื่อทำให้สิ่งต่าง ๆ ซับซ้อนยิ่งขึ้น เว็บแอปพลิเคชันสมัยใหม่ เช่น SPA มักจะทำหน้าที่เป็นกลุ่มของรหัสคงที่โดยตรงจากบัคเก็ต S3 โดยไม่ต้องเกี่ยวข้องกับเว็บเซิร์ฟเวอร์ แม้ว่าเว็บเซิร์ฟเวอร์จะมีส่วนเกี่ยวข้อง ก็มักจะไม่สามารถเปลี่ยนตรรกะฝั่งเซิร์ฟเวอร์เพื่อตั้งค่าการทดสอบ A/B การมีอยู่ของ CDN ยังเป็นอุปสรรคอีกประการหนึ่ง เนื่องจากการแคชอาจส่งผลต่อขนาดเซ็กเมนต์ หรือในทางกลับกัน การแบ่งส่วนการรับส่งข้อมูลประเภทนี้สามารถลดประสิทธิภาพของ CDN ได้
สิ่งที่ Lambda@Edge นำเสนอคือวิธีกำหนดเส้นทางคำขอของผู้ใช้ข้ามรุ่นต่างๆ ของการทดสอบ ก่อนที่พวกเขาจะไปถึงเซิร์ฟเวอร์ของคุณ ตัวอย่างพื้นฐานของกรณีการใช้งานนี้มีอยู่ในเอกสารของ AWS โดยตรง แม้ว่าจะมีประโยชน์ในการพิสูจน์แนวคิด แต่สภาพแวดล้อมการผลิตที่มีการทดลองพร้อมกันหลายครั้งอาจต้องการบางสิ่งที่ยืดหยุ่นและแข็งแกร่งกว่า
ยิ่งไปกว่านั้น หลังจากใช้งาน Lambda@Edge มาบ้างแล้ว คุณอาจพบว่ามีความแตกต่างบางอย่างที่ต้องระวังเมื่อสร้างสถาปัตยกรรมของคุณ
ตัวอย่างเช่น การปรับใช้ Edge Lambdas ต้องใช้เวลา และบันทึกจะกระจายไปทั่วภูมิภาค AWS พึงระลึกไว้เสมอว่าหากคุณต้องการดีบักการกำหนดค่าของคุณเพื่อหลีกเลี่ยงข้อผิดพลาด 502
บทช่วยสอนนี้จะแนะนำนักพัฒนาของ AWS ให้รู้จักวิธีใช้งานการทดสอบ A/B ฝั่งเซิร์ฟเวอร์โดยใช้ Lambda@Edge ในลักษณะที่สามารถนำมาใช้ซ้ำในการทดลองต่างๆ โดยไม่ต้องแก้ไขและปรับใช้ขอบ Lambdas อีกครั้ง สร้างขึ้นจากแนวทางของตัวอย่างในเอกสารประกอบของ AWS และบทช่วยสอนอื่นๆ ที่คล้ายคลึงกัน แต่แทนที่จะฮาร์ดโค้ดกฎการจัดสรรการรับส่งข้อมูลในแลมบ์ดาเอง กฎจะดึงกลับมาเป็นระยะจากไฟล์การกำหนดค่าบน S3 ซึ่งคุณสามารถเปลี่ยนได้ตลอดเวลา
ภาพรวมของแนวทางการทดสอบ A/B ของ Lambda@Edge
แนวคิดพื้นฐานเบื้องหลังวิธีนี้คือให้ CDN กำหนดผู้ใช้แต่ละรายให้กับกลุ่ม จากนั้นกำหนดเส้นทางผู้ใช้ไปยังการกำหนดค่าต้นทางที่เกี่ยวข้อง CloudFront อนุญาตให้การแจกจ่ายชี้ไปที่ S3 หรือต้นทางที่กำหนดเอง และในแนวทางนี้ เราสนับสนุนทั้งสองอย่าง
การแมปเซ็กเมนต์กับรูปแบบการทดสอบจะถูกเก็บไว้ในไฟล์ JSON บน S3 S3 ได้รับเลือกที่นี่เพื่อความง่าย แต่สามารถดึงข้อมูลจากฐานข้อมูลหรือรูปแบบการจัดเก็บอื่น ๆ ที่ Edge Lambda สามารถเข้าถึงได้
หมายเหตุ: มีข้อจำกัดบางประการ - ตรวจสอบบทความการใช้ประโยชน์จากข้อมูลภายนอกใน Lambda@Edge บนบล็อก AWS สำหรับข้อมูลเพิ่มเติม
การดำเนินการ
Lambda@Edge สามารถเรียกใช้งานได้จากเหตุการณ์ CloudFront สี่ประเภท:
ในกรณีนี้ เราจะใช้ Lambda กับสามเหตุการณ์ต่อไปนี้:
- คำขอดู
- คำขอต้นทาง
- คำตอบของผู้ชม
แต่ละเหตุการณ์จะใช้ขั้นตอนในกระบวนการต่อไปนี้:
- abtesting-lambda-vreq : ตรรกะส่วนใหญ่มีอยู่ในแลมบ์ดานี้ ขั้นแรก จะมีการอ่านหรือสร้างคุกกี้ ID ที่ไม่ซ้ำสำหรับคำขอที่เข้ามา จากนั้นจึงแฮชลงไปที่ช่วง [0, 1] จากนั้นดึงแผนที่การจัดสรรการรับส่งข้อมูลจาก S3 และแคชระหว่างการดำเนินการ และสุดท้าย ค่าแฮชดาวน์จะถูกใช้เพื่อเลือกการกำหนดค่าต้นทาง ซึ่งจะถูกส่งต่อเป็นส่วนหัวที่เข้ารหัส JSON ไปยัง Lambda ตัวถัดไป
- abtesting-lambda-oreq : สิ่งนี้จะอ่านการกำหนดค่าต้นทางจากแลมบ์ดาก่อนหน้าและกำหนดเส้นทางคำขอตามลำดับ
- abtesting-lambda-vres : นี่เป็นเพียงการเพิ่มส่วนหัว Set-Cookie เพื่อบันทึกคุกกี้ ID ที่ไม่ซ้ำบนเบราว์เซอร์ของผู้ใช้
เรามาตั้งค่าบัคเก็ต S3 สามอันกัน โดยสองอันจะมีเนื้อหาของตัวแปรของการทดสอบแต่ละรายการ ในขณะที่อันที่สามจะมีไฟล์ JSON พร้อมแผนที่การจัดสรรการเข้าชม
สำหรับบทช่วยสอนนี้ ที่เก็บข้อมูลจะมีลักษณะดังนี้:
- abtesting-ttblog -a สาธารณะ
- index.html
- abtesting-ttblog-b สาธารณะ
- index.html
- abtesting-ttblog-map ส่วนตัว
- map.json
รหัสแหล่งที่มา
อันดับแรก เริ่มจากแผนที่การจัดสรรการจราจร:
map.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" } } } ] }
แต่ละกลุ่มมีน้ำหนักการเข้าชม ซึ่งจะใช้ในการจัดสรรปริมาณการเข้าชมที่สอดคล้องกัน เรายังรวมการกำหนดค่าต้นทางและโฮสต์ด้วย รูปแบบการกำหนดค่าต้นทางได้อธิบายไว้ในเอกสารของ 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); };
ที่นี่ เราสร้าง ID เฉพาะสำหรับบทช่วยสอนนี้อย่างชัดเจน แต่เป็นเรื่องปกติที่เว็บไซต์ส่วนใหญ่จะมี ID ไคลเอ็นต์อื่นๆ อยู่รอบๆ ซึ่งสามารถใช้แทนได้ สิ่งนี้จะขจัดความจำเป็นในการตอบสนองของผู้ชมที่แลมบ์ดา

สำหรับการพิจารณาประสิทธิภาพ กฎการจัดสรรการรับส่งข้อมูลจะถูกแคชในการเรียกใช้ Lambda แทนที่จะดึงข้อมูลจาก S3 ในทุกคำขอ ในตัวอย่างนี้ เราตั้งค่าแคช TTL เป็น 1 ชั่วโมง
โปรดทราบว่าส่วนหัว X-ABTesting-Segment-Origin
จะต้องได้รับอนุญาตพิเศษใน CloudFront มิเช่นนั้นจะถูกลบออกจากคำขอก่อนที่จะถึงคำขอต้นทางของแลมบ์ดา
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); };
คำขอเริ่มต้นของ Lambda ค่อนข้างตรงไปตรงมา การกำหนดค่าต้นทางและโฮสต์ถูกอ่านจากส่วนหัว X-ABTesting-Origin
ที่สร้างขึ้นในขั้นตอนก่อนหน้าและแทรกลงในคำขอ ซึ่งจะสั่งให้ CloudFront กำหนดเส้นทางคำขอไปยังต้นทางที่เกี่ยวข้องในกรณีที่แคชพลาด
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); }
สุดท้าย การตอบสนองของผู้ดู Lambda มีหน้าที่ส่งคืนคุกกี้ ID เฉพาะที่สร้างขึ้นในส่วนหัว Set-Cookie
ดังที่กล่าวไว้ข้างต้น หากใช้รหัสไคลเอ็นต์ที่ไม่ซ้ำแล้ว Lambda นี้สามารถละเว้นได้โดยสิ้นเชิง
ในความเป็นจริง แม้ในกรณีนี้ คุกกี้สามารถตั้งค่าด้วยการเปลี่ยนเส้นทางโดยผู้ดูร้องขอ Lambda อย่างไรก็ตาม การทำเช่นนี้อาจเพิ่มเวลาในการตอบสนอง ดังนั้นในกรณีนี้ เราจึงชอบที่จะทำในรอบการตอบกลับคำขอเพียงครั้งเดียว
รหัสนี้ยังสามารถพบได้ใน GitHub
สิทธิ์แลมบ์ดา
เช่นเดียวกับ Edge Lambda คุณสามารถใช้พิมพ์เขียว CloudFront เมื่อสร้าง Lambda ไม่เช่นนั้น คุณจะต้องสร้างบทบาทที่กำหนดเองและแนบเทมเพลตนโยบาย "Basic Lambda@Edge Permissions"
สำหรับคำขอของผู้ดู Lambda คุณจะต้องอนุญาตการเข้าถึงบัคเก็ต S3 ที่มีไฟล์การจัดสรรการรับส่งข้อมูล
การปรับใช้แลมบ์ดาส
การตั้งค่า edge Lambdas ค่อนข้างแตกต่างจากเวิร์กโฟลว์ Lambda มาตรฐาน ในหน้าการกำหนดค่าของ Lamba คลิก "เพิ่มทริกเกอร์" และเลือก CloudFront ซึ่งจะเปิดกล่องโต้ตอบขนาดเล็กที่ให้คุณเชื่อมโยง Lambda นี้กับการกระจาย CloudFront
เลือกเหตุการณ์ที่เหมาะสมสำหรับ Lambdas แต่ละตัวและกด "Deploy" การดำเนินการนี้จะเริ่มกระบวนการปรับใช้โค้ดฟังก์ชันกับเซิร์ฟเวอร์ Edge ของ CloudFront
หมายเหตุ: หากคุณต้องการแก้ไข Edge Lambda และปรับใช้ใหม่ คุณต้องเผยแพร่เวอร์ชันใหม่ด้วยตนเองก่อน
การตั้งค่า CloudFront
เพื่อให้การกระจาย CloudFront สามารถกำหนดเส้นทางการรับส่งข้อมูลไปยังต้นทาง คุณจะต้องตั้งค่าแต่ละรายการแยกกันในแผงต้นทาง
การตั้งค่าการกำหนดค่าเดียวที่คุณต้องเปลี่ยนคือการอนุญาตส่วนหัว X-ABTesting-Segment-Origin
บน คอนโซล CloudFront เลือกการแจกจ่ายของคุณแล้วกดแก้ไขเพื่อเปลี่ยนการตั้งค่าของการแจกจ่าย
ในหน้า แก้ไขพฤติกรรม ให้เลือกรายการที่ อนุญาต พิเศษจากเมนูดรอปดาวน์บนตัวเลือก Cache Based on Selected Request Headers และเพิ่มส่วนหัว X-ABTesting-Segment-Origin ที่กำหนดเองลงในรายการ:
หากคุณปรับใช้ Edge Lambdas ตามที่อธิบายไว้ในส่วนก่อนหน้า สิ่งเหล่านี้ควรเชื่อมโยงกับการแจกจ่ายของคุณและแสดงอยู่ในส่วนสุดท้ายของหน้า แก้ไขพฤติกรรม
ทางออกที่ดีกับคำเตือนเล็กน้อย
การทดสอบ A/B ฝั่งเซิร์ฟเวอร์อาจเป็นเรื่องยากที่จะนำไปใช้อย่างเหมาะสมสำหรับเว็บไซต์ที่มีการเข้าชมสูงซึ่งถูกปรับใช้หลังบริการ CDN เช่น CloudFront ในบทความนี้ เราสาธิตวิธีการใช้ Lambda@Edge เป็นวิธีแก้ปัญหาแบบใหม่สำหรับปัญหานี้ โดยซ่อนรายละเอียดการนำไปใช้ใน CDN เอง ในขณะเดียวกันก็นำเสนอโซลูชันที่สะอาดและเชื่อถือได้เพื่อเรียกใช้การทดสอบ A/B
อย่างไรก็ตาม Lambda@Edge มีข้อเสียอยู่เล็กน้อย สิ่งสำคัญที่สุดคือ การเรียกใช้ Lambda เพิ่มเติมเหล่านี้ระหว่างเหตุการณ์ CloudFront สามารถเพิ่มขึ้นในแง่ของเวลาแฝงและค่าใช้จ่าย ดังนั้นควรวัดผลกระทบต่อการกระจาย CloudFront อย่างรอบคอบก่อน
นอกจากนี้ Lambda@Edge ยังเป็นคุณสมบัติที่ค่อนข้างใหม่และยังพัฒนาอยู่ของ AWS ดังนั้นจึงเป็นธรรมชาติที่ยังคงรู้สึกหยาบเล็กน้อยบริเวณขอบ ผู้ใช้ที่ระมัดระวังมากขึ้นอาจยังคงต้องการรอสักครู่ก่อนที่จะวางไว้ที่จุดสำคัญของโครงสร้างพื้นฐานของตน
ดังที่กล่าวไปแล้ว โซลูชันที่เป็นเอกลักษณ์ที่นำเสนอทำให้เป็นคุณลักษณะที่ขาดไม่ได้ของ CDN ดังนั้นจึงไม่ไม่มีเหตุผลที่จะคาดหวังให้มีการใช้กันอย่างแพร่หลายมากขึ้นในอนาคต