AWS Lambda@Edge ile Esnek A/B Testi
Yayınlanan: 2022-03-11Amazon CloudFront gibi içerik dağıtım ağları (CDN'ler), yakın zamana kadar web altyapısının nispeten basit bir parçasıydı. Geleneksel olarak, web uygulamaları onların etrafında tasarlandı ve onlara aktif bir bileşen yerine çoğunlukla pasif bir önbellek olarak davranıldı.
Lambda@Edge ve benzeri teknolojiler, tüm bunları değiştirdi ve bir web uygulaması ile kullanıcıları arasında yeni bir mantık katmanı sunarak yepyeni bir olasılıklar dünyasının kapılarını açtı. 2017 yılının ortalarından beri kullanılabilen Lambda@Edge, AWS'de doğrudan CloudFront'un uç sunucularında Lambda biçiminde kod yürütme kavramını tanıtan yeni bir özelliktir.
Lambda@Edge'in sunduğu yeni olanaklardan biri, sunucu tarafı A/B testi için temiz bir çözümdür. A/B testi, bir web sitesinin birden çok varyasyonunun performansını, bunları aynı anda web sitesinin hedef kitlesinin farklı segmentlerine göstererek test etmenin yaygın bir yöntemidir.
A/B Testi için Lambda@Edge Ne Anlama Geliyor?
A/B testindeki ana teknik zorluk, deneme verilerinin kalitesini veya web sitesinin kendisini herhangi bir şekilde etkilemeden gelen trafiği uygun şekilde bölümlere ayırmaktır.
Bunu uygulamak için iki ana yol vardır: istemci tarafı ve sunucu tarafı .
- İstemci tarafı , hangi varyantın gösterileceğini seçen son kullanıcının tarayıcısında biraz JavaScript kodu çalıştırmayı içerir. Bu yaklaşımın birkaç önemli dezavantajı vardır; en önemlisi, hem oluşturmayı yavaşlatabilir hem de titremeye veya diğer oluşturma sorunlarına neden olabilir. Bu, yükleme sürelerini optimize etmeye çalışan veya UX'leri için yüksek standartlara sahip web sitelerinin bu yaklaşımdan kaçınma eğiliminde olacağı anlamına gelir.
- Sunucu tarafı A/B testi, hangi varyantın döndürüleceği kararı tamamen ana bilgisayar tarafında alındığından, bu sorunların çoğunu ortadan kaldırır. Tarayıcı, her bir varyantı normal olarak, web sitesinin standart versiyonuymuş gibi işler.
Bunu akılda tutarak, neden herkesin sunucu tarafı A/B testini kullanmadığını merak edebilirsiniz. Ne yazık ki, sunucu tarafı yaklaşımının uygulanması, istemci tarafı yaklaşımı kadar kolay değildir ve bir deney kurmak genellikle sunucu tarafı koduna veya sunucu yapılandırmasına bir tür müdahale gerektirir.
İşleri daha da karmaşık hale getirmek için, SPA'lar gibi modern web uygulamalarına genellikle bir web sunucusunu dahil etmeden doğrudan bir S3 kovasından statik kod demetleri olarak sunulur. Bir web sunucusu söz konusu olduğunda bile, bir A/B testi ayarlamak için sunucu tarafı mantığını değiştirmek çoğu zaman mümkün değildir. Önbelleğe alma, segment boyutlarını etkileyebileceğinden veya tersine, bu tür trafik segmentasyonu CDN'nin performansını düşürebileceğinden, bir CDN'nin varlığı başka bir engel teşkil eder.
Lambda@Edge'in sunduğu şey, kullanıcı isteklerini daha sunucularınıza ulaşmadan bir denemenin çeşitleri arasında yönlendirmenin bir yoludur. Bu kullanım durumunun temel bir örneği doğrudan AWS belgelerinde bulunabilir. Kavram kanıtı olarak faydalı olsa da, birden fazla eşzamanlı deney içeren bir üretim ortamı muhtemelen daha esnek ve sağlam bir şeye ihtiyaç duyacaktır.
Ayrıca, Lambda@Edge ile biraz çalıştıktan sonra, muhtemelen mimarinizi oluştururken dikkat etmeniz gereken bazı nüanslar olduğunu anlayacaksınız.
Örneğin, uç Lambda'ları dağıtmak zaman alır ve günlükleri AWS bölgelerine dağıtılır. 502 hatalarından kaçınmak için yapılandırmanızda hata ayıklamanız gerekiyorsa buna dikkat edin.
Bu eğitici, AWS geliştiricilerine, Lambda@Edge kullanarak, uç Lambda'ları değiştirmeden ve yeniden dağıtmadan deneyler arasında yeniden kullanılabilecek şekilde sunucu tarafı A/B testini uygulamanın bir yolunu tanıtacaktır. AWS belgelerindeki ve diğer benzer öğreticilerdeki örneğin yaklaşımını temel alır, ancak trafik ayırma kurallarını Lambda'nın kendisinde sabit kodlamak yerine kurallar, istediğiniz zaman değiştirebileceğiniz S3'teki bir yapılandırma dosyasından periyodik olarak alınır.
Lambda@Edge A/B Testi Yaklaşımımıza Genel Bakış
Bu yaklaşımın arkasındaki temel fikir, CDN'nin her kullanıcıyı bir segmente atamasını ve ardından kullanıcıyı ilişkili Origin konfigürasyonuna yönlendirmesini sağlamaktır. CloudFront, dağıtımın S3'e veya özel kaynaklara işaret etmesine izin verir ve bu yaklaşımda her ikisini de destekleriz.
Segmentlerin deneme varyantlarına eşlenmesi, S3'te bir JSON dosyasında depolanacaktır. Burada basitlik için S3 seçilmiştir, ancak bu aynı zamanda bir veritabanından veya Edge Lambda'nın erişebileceği başka bir depolama biçiminden de alınabilir.
Not: Bazı sınırlamalar vardır - daha fazla bilgi için AWS Blogunda Lambda@Edge'de harici verilerden yararlanma makalesine bakın.
uygulama
Lambda@Edge, dört farklı CloudFront olayı türü tarafından tetiklenebilir:
Bu durumda, aşağıdaki üç etkinliğin her birinde bir Lambda çalıştıracağız:
- izleyici isteği
- kaynak isteği
- İzleyici yanıtı
Her olay aşağıdaki süreçte bir adım uygulayacaktır:
- abtesting-lambda-vreq : Mantığın çoğu bu lambdada bulunur. İlk olarak, gelen istek için benzersiz bir kimlik tanımlama bilgisi okunur veya oluşturulur ve ardından [0, 1] aralığına indirilir. Trafik tahsis haritası daha sonra S3'ten alınır ve yürütmeler arasında önbelleğe alınır. Ve son olarak, karma değeri, bir sonraki Lambda'ya JSON kodlu bir başlık olarak geçirilen bir Origin konfigürasyonu seçmek için kullanılır.
- abtesting-lambda-oreq : Bu, önceki Lambda'dan başlangıç yapılandırmasını okur ve isteği buna göre yönlendirir.
- abtesting-lambda-vres : Bu, benzersiz kimlik tanımlama bilgisini kullanıcının tarayıcısına kaydetmek için Set-Cookie başlığını ekler.
Ayrıca, ikisi denemenin varyantlarının her birinin içeriğini içerecek, üçüncüsü ise trafik ayırma haritasına sahip JSON dosyasını içerecek olan üç S3 paketi oluşturalım.
Bu öğretici için, kovalar şöyle görünecek:
- abtesting-ttblog -bir kamu
- index.html
- abtesting-ttblog-b kamu
- index.html
- abtesting-ttblog-harita özel
- harita.json
Kaynak kodu
İlk olarak, trafik tahsis haritasıyla başlayalım:
harita.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" } } } ] }
Her segmentin, karşılık gelen miktarda trafik tahsis etmek için kullanılacak bir trafik ağırlığı vardır. Ayrıca kaynak yapılandırmasını ve ana bilgisayarı da dahil ediyoruz. Kaynak yapılandırma biçimi, AWS belgelerinde açıklanmıştır.
caydırıcı-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); };
Burada, bu eğitim için açıkça benzersiz bir kimlik oluşturuyoruz, ancak çoğu web sitesinde bunun yerine kullanılabilecek başka bir istemci kimliğinin bulunması oldukça yaygındır. Bu aynı zamanda izleyici yanıtı Lambda ihtiyacını da ortadan kaldıracaktır.

Performansla ilgili hususlar için, trafik ayırma kuralları, her istekte S3'ten getirilmek yerine Lambda çağrılarında önbelleğe alınır. Bu örnekte, 1 saatlik bir önbellek TTL'si ayarladık.
X-ABTesting-Segment-Origin
başlığının CloudFront'ta beyaz listeye alınması gerektiğini unutmayın; aksi takdirde, kaynak isteği Lambda'ya ulaşmadan istekten silinecektir.
özleyen-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); };
Origin isteği Lambda oldukça basittir. Origin konfigürasyonu ve ana bilgisayar, önceki adımda oluşturulan ve isteğe enjekte edilen X-ABTesting-Origin
başlığından okunur. Bu, CloudFront'a bir önbellek eksikliği durumunda isteği ilgili Köken'e yönlendirmesi talimatını verir.
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); }
Son olarak, Lambda izleyici yanıtı, Set-Cookie
başlığında oluşturulan benzersiz kimlik tanımlama bilgisini döndürmekten sorumludur. Yukarıda belirtildiği gibi, benzersiz bir istemci kimliği zaten kullanılıyorsa, bu Lambda tamamen atlanabilir.
Aslında, bu durumda bile, çerez, Lambda'nın izleyici isteği tarafından bir yönlendirme ile ayarlanabilir. Ancak, bu biraz gecikme ekleyebilir, bu nedenle bu durumda, bunu tek bir istek-yanıt döngüsünde yapmayı tercih ediyoruz.
Kod ayrıca GitHub'da da bulunabilir.
Lambda İzinleri
Herhangi bir uç Lambda'da olduğu gibi, Lambda'yı oluştururken CloudFront planını kullanabilirsiniz. Aksi takdirde, özel bir rol oluşturmanız ve "Temel Lambda@Edge İzinleri" ilke şablonunu eklemeniz gerekir.
Lambda izleyici isteği için, trafik ayırma dosyasını içeren S3 klasörüne erişime de izin vermeniz gerekir.
Lambda'ları Dağıtma
Edge Lambda'ları kurmak, standart Lambda iş akışından biraz farklıdır. Lamba'nın yapılandırma sayfasında "Tetikleyici ekle"ye tıklayın ve CloudFront'u seçin. Bu, bu Lambda'yı bir CloudFront dağıtımıyla ilişkilendirmenize izin veren küçük bir iletişim kutusu açacaktır.
Üç Lambda'nın her biri için uygun olayı seçin ve "Dağıt"a basın. Bu, işlev kodunu CloudFront'un uç sunucularına dağıtma sürecini başlatacaktır.
Not: Edge Lambda'yı değiştirmeniz ve yeniden dağıtmanız gerekirse, önce yeni bir sürümü manuel olarak yayınlamanız gerekir.
CloudFront Ayarları
Bir CloudFront dağıtımının trafiği bir kaynağa yönlendirebilmesi için, başlangıçlar panelinde her birini ayrı ayrı kurmanız gerekir.
Değiştirmeniz gereken tek yapılandırma ayarı, X-ABTesting-Segment-Origin
başlığını beyaz listeye almaktır. CloudFront konsolunda dağıtımınızı seçin ve ardından dağıtım ayarlarını değiştirmek için düzenle'ye basın.
Davranışı Düzenle sayfasında, Seçili İstek Başlıklarına Göre Önbellek seçeneğindeki açılır menüden Beyaz Liste'yi seçin ve listeye özel bir X-ABTesting-Segment-Origin başlığı ekleyin:
Edge Lambda'ları önceki bölümde açıklandığı gibi dağıttıysanız, bunların zaten dağıtımınızla ilişkilendirilmiş ve Davranışı Düzenle sayfasının son bölümünde listelenmiş olmaları gerekir.
Küçük Uyarılarla İyi Bir Çözüm
Sunucu tarafı A/B testinin, CloudFront gibi CDN hizmetlerinin arkasına dağıtılan yüksek trafikli web siteleri için düzgün bir şekilde uygulanması zor olabilir. Bu makalede, Lambda@Edge'in uygulama ayrıntılarını CDN'nin kendisinde saklayarak bu soruna yeni bir çözüm olarak nasıl kullanılabileceğini gösterdik ve aynı zamanda A/B deneylerini çalıştırmak için temiz ve güvenilir bir çözüm sunduk.
Ancak Lambda@Edge'in birkaç dezavantajı vardır. En önemlisi, CloudFront olayları arasındaki bu ek Lambda çağrıları, hem gecikme hem de maliyet açısından toplanabilir, bu nedenle bunların bir CloudFront dağıtımı üzerindeki etkileri önce dikkatlice ölçülmelidir.
Ayrıca, Lambda@Edge, AWS'nin nispeten yeni ve hala gelişmekte olan bir özelliğidir, bu nedenle doğal olarak, kenarlarda hala biraz pürüzlü hissediyor. Daha muhafazakar kullanıcılar, onu altyapılarının bu kadar kritik bir noktasına yerleştirmeden önce biraz beklemek isteyebilir.
Bununla birlikte, sunduğu benzersiz çözümler onu CDN'lerin vazgeçilmez bir özelliği haline getiriyor, bu nedenle gelecekte çok daha yaygın bir şekilde benimsenmesini beklemek mantıksız değil.