Testare A/B flexibilă cu AWS Lambda@Edge
Publicat: 2022-03-11Rețelele de livrare de conținut (CDN) precum Amazon CloudFront au fost, până de curând, o parte relativ simplă a infrastructurii web. În mod tradițional, aplicațiile web au fost concepute în jurul lor, tratându-le mai ales ca un cache pasiv în loc de o componentă activă.
Lambda@Edge și tehnologiile similare au schimbat toate acestea și au deschis o lume întreagă de posibilități prin introducerea unui nou strat de logică între o aplicație web și utilizatorii săi. Disponibil de la jumătatea anului 2017, Lambda@Edge este o nouă caracteristică în AWS care introduce conceptul de execuție a codului sub formă de Lambda direct pe serverele edge ale CloudFront.
Una dintre noile posibilități pe care Lambda@Edge le oferă este o soluție curată pentru testarea A/B pe server. Testarea A/B este o metodă comună de testare a performanței mai multor variante ale unui site web, arătându-le în același timp diferitelor segmente ale publicului site-ului.
Ce înseamnă Lambda@Edge pentru testarea A/B
Principala provocare tehnică în testarea A/B este segmentarea corectă a traficului de intrare fără a afecta în niciun fel calitatea datelor experimentului sau site-ul în sine.
Există două căi principale de implementare: partea client și partea serverului .
- Partea client implică rularea unui pic de cod JavaScript în browserul utilizatorului final care alege varianta care va fi afișată. Există câteva dezavantaje semnificative ale acestei abordări - mai ales, poate încetini randarea și poate provoca pâlpâirea sau alte probleme de redare. Aceasta înseamnă că site-urile web care încearcă să-și optimizeze timpul de încărcare sau care au standarde înalte pentru UX-ul lor vor tinde să evite această abordare.
- Testarea A/B pe server elimină majoritatea acestor probleme, deoarece decizia asupra variantei de returnat este luată în întregime de partea gazdei. Browserul pur și simplu redă fiecare variantă în mod normal, ca și cum ar fi versiunea standard a site-ului web.
Având în vedere acest lucru, s-ar putea să vă întrebați de ce toată lumea nu folosește pur și simplu testarea A/B pe server. Din păcate, abordarea pe partea serverului nu este la fel de ușor de implementat ca abordarea pe partea clientului, iar configurarea unui experiment necesită adesea o anumită formă de intervenție asupra codului serverului sau a configurației serverului.
Pentru a complica lucrurile și mai mult, aplicațiile web moderne, cum ar fi SPA-urile, sunt adesea servite ca pachete de cod static direct dintr-o găleată S3, fără a implica măcar un server web. Chiar și atunci când este implicat un server web, de multe ori nu este fezabil să schimbați logica serverului pentru a configura un test A/B. Prezența unui CDN reprezintă încă un obstacol, deoarece memorarea în cache poate afecta dimensiunile segmentelor sau, dimpotrivă, acest tip de segmentare a traficului poate reduce performanța CDN-ului.
Ceea ce oferă Lambda@Edge este o modalitate de a direcționa solicitările utilizatorilor peste variantele unui experiment înainte ca acestea să ajungă chiar pe serverele dvs. Un exemplu de bază al acestui caz de utilizare poate fi găsit direct în documentația AWS. Deși util ca dovadă de concept, un mediu de producție cu mai multe experimente concurente ar avea probabil nevoie de ceva mai flexibil și mai robust.
Mai mult decât atât, după ce ai lucrat puțin cu Lambda@Edge, probabil vei realiza că există câteva nuanțe de care trebuie să fii conștient atunci când construiești arhitectura.
De exemplu, implementarea Lambda edge necesită timp, iar jurnalele lor sunt distribuite în regiunile AWS. Fiți atenți la acest lucru dacă trebuie să vă depanați configurația pentru a evita erorile 502.
Acest tutorial va prezenta dezvoltatorilor AWS o modalitate de implementare a testării A/B pe server folosind Lambda@Edge într-o manieră care poate fi reutilizată în experimente fără modificarea și redistribuirea Lambdas-ului edge. Se bazează pe abordarea exemplului din documentația AWS și din alte tutoriale similare, dar în loc să codifice regulile de alocare a traficului în Lambda în sine, regulile sunt preluate periodic dintr-un fișier de configurare pe S3 pe care îl poți schimba oricând.
Prezentare generală a abordării noastre de testare Lambda@Edge A/B
Ideea de bază din spatele acestei abordări este ca CDN-ul să atribuie fiecărui utilizator un segment și apoi să îl direcționeze către configurația de origine asociată. CloudFront permite distribuției să indice fie S3, fie origini personalizate și, în această abordare, le sprijinim pe ambele.
Maparea segmentelor la variantele de experiment va fi stocată într-un fișier JSON pe S3. S3 este ales aici pentru simplitate, dar acesta ar putea fi preluat și dintr-o bază de date sau orice altă formă de stocare pe care o poate accesa Edge Lambda.
Notă: Există unele limitări - consultați articolul Utilizarea datelor externe în Lambda@Edge de pe blogul AWS pentru mai multe informații.
Implementarea
Lambda@Edge poate fi declanșat de patru tipuri diferite de evenimente CloudFront:
În acest caz, vom rula un Lambda pentru fiecare dintre următoarele trei evenimente:
- Solicitarea spectatorului
- Cerere de origine
- Răspunsul spectatorului
Fiecare eveniment va implementa un pas în următorul proces:
- abtesting-lambda-vreq : Cea mai mare parte a logicii este conținută în acest lambda. În primul rând, un cookie ID unic este citit sau generat pentru cererea de intrare și apoi este codificat în jos la un interval [0, 1]. Harta de alocare a traficului este apoi preluată de la S3 și stocată în cache pe parcursul execuțiilor. Și, în cele din urmă, valoarea hashed în jos este utilizată pentru a alege o configurație de origine, care este transmisă ca antet codificat JSON următorului Lambda.
- abtesting-lambda-oreq : Aceasta citește configurația de origine din Lambda anterioară și direcționează cererea în consecință.
- abtesting-lambda-vres : Aceasta adaugă doar antetul Set-Cookie pentru a salva cookie-ul ID unic în browserul utilizatorului.
De asemenea, să setăm trei compartimente S3, dintre care două vor conține conținutul fiecăreia dintre variantele experimentului, în timp ce al treilea va conține fișierul JSON cu harta de alocare a traficului.
Pentru acest tutorial, gălețile vor arăta astfel:
- abtesting-ttblog -a public
- index.html
- abtesting-ttblog-b public
- index.html
- abtesting-ttblog-map privat
- map.json
Cod sursa
În primul rând, să începem cu harta de alocare a traficului:
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" } } } ] }Fiecare segment are o pondere de trafic, care va fi utilizată pentru a aloca o cantitate corespunzătoare de trafic. Includem, de asemenea, configurația de origine și gazda. Formatul de configurare a originii este descris în documentația 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); };Aici, generăm în mod explicit un ID unic pentru acest tutorial, dar este destul de comun ca majoritatea site-urilor web să aibă un alt ID de client care ar putea fi folosit în schimb. Acest lucru ar elimina, de asemenea, nevoia de răspunsul spectatorului Lambda.

Pentru considerente de performanță, regulile de alocare a traficului sunt stocate în cache prin invocări Lambda în loc să le preia de la S3 la fiecare solicitare. În acest exemplu, am configurat un TTL cache de 1 oră.
Rețineți că X-ABTesting-Segment-Origin trebuie să fie inclus în lista albă în CloudFront; în caz contrar, va fi șters din cerere înainte de a ajunge la cererea de origine 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); }; Solicitarea de origine Lambda este destul de simplă. Configurația de origine și gazda sunt citite din X-ABTesting-Origin care a fost generat în pasul anterior și injectat în cerere. Acest lucru indică CloudFront să direcționeze cererea către originea corespunzătoare în caz de pierdere a memoriei 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); } În cele din urmă, răspunsul vizualizatorului Lambda este responsabil pentru returnarea cookie-ului ID unic generat în antetul Set-Cookie . După cum sa menționat mai sus, dacă este deja utilizat un ID de client unic, acest Lambda poate fi omis complet.
De fapt, chiar și în acest caz, cookie-ul poate fi setat cu o redirecționare de către solicitarea vizualizatorului Lambda. Cu toate acestea, acest lucru ar putea adăuga o anumită latență, așa că în acest caz, preferăm să o facem într-un singur ciclu cerere-răspuns.
Codul poate fi găsit și pe GitHub.
Permisiuni Lambda
Ca și în cazul oricărui Lambda de margine, puteți utiliza planul CloudFront atunci când creați Lambda. În caz contrar, va trebui să creați un rol personalizat și să atașați șablonul de politică „Permisiuni de bază Lambda@Edge”.
Pentru solicitarea vizualizatorului Lambda, va trebui, de asemenea, să permiteți accesul la compartimentul S3 care conține fișierul de alocare a traficului.
Desfășurarea Lambda
Configurarea edge Lambda este oarecum diferită de fluxul de lucru standard Lambda. Pe pagina de configurare a lui Lamba, faceți clic pe „Adăugați declanșator” și selectați CloudFront. Aceasta va deschide un mic dialog care vă permite să asociați acest Lambda cu o distribuție CloudFront.
Selectați evenimentul potrivit pentru fiecare dintre cele trei Lambda și apăsați pe „Deploy”. Aceasta va începe procesul de implementare a codului funcției pe serverele edge CloudFront.
Notă: Dacă trebuie să modificați un Lambda edge și să îl redistribuiți, mai întâi trebuie să publicați manual o nouă versiune.
Setări CloudFront
Pentru ca o distribuție CloudFront să poată direcționa traficul către o origine, va trebui să le configurați pe fiecare separat în panoul de origini.
Singura setare de configurare pe care va trebui să o modificați este să X-ABTesting-Segment-Origin în lista albă. Pe consola CloudFront , alegeți distribuția dvs. și apoi apăsați Editați pentru a modifica setările distribuției.
Pe pagina Editați comportamentul , selectați Lista albă din meniul drop-down din opțiunea Cache pe baza antetelor de solicitare selectate și adăugați un antet personalizat X-ABTesting-Segment-Origin la listă:
Dacă ați implementat edge Lambda așa cum este descris în secțiunea anterioară, acestea ar trebui să fie deja asociate cu distribuția dvs. și listate în ultima secțiune a paginii Editați comportamentul .
O soluție bună cu avertismente minore
Testarea A/B la nivelul serverului poate fi o provocare de implementat corect pentru site-urile web cu trafic ridicat care sunt implementate în spatele serviciilor CDN precum CloudFront. În acest articol, am demonstrat cum Lambda@Edge poate fi folosit ca o soluție nouă la această problemă, ascunzând detaliile de implementare în CDN-ul însuși, oferind în același timp o soluție curată și fiabilă pentru a rula experimente A/B.
Cu toate acestea, Lambda@Edge are câteva dezavantaje. Cel mai important, aceste invocări suplimentare Lambda între evenimentele CloudFront se pot adăuga atât în termeni de latență, cât și de cost, astfel încât impactul lor asupra unei distribuții CloudFront ar trebui mai întâi măsurat cu atenție.
Mai mult decât atât, Lambda@Edge este o caracteristică relativ recentă și încă în evoluție a AWS, așa că, firește, se simte încă puțin cam dur în jurul marginilor. Utilizatorii mai conservatori ar putea dori totuși să aștepte ceva timp înainte de a-l plasa într-un punct atât de critic al infrastructurii lor.
Acestea fiind spuse, soluțiile unice pe care le oferă îl fac o caracteristică indispensabilă a CDN-urilor, așa că nu este nerezonabil să ne așteptăm să devină mult mai larg adoptat în viitor.
