本文概述
- Lambda@Edge对A/B测试意味着什么
- Lambda@Edge A/B测试方法概述
- 实作
- 源代码
- Lambda权限
- 部署Lambda
- CloudFront设置
- 小警告的好解决方案
Lambda@Edge和类似技术已经改变了一切, 并通过在Web应用程序及其用户之间引入新的逻辑层, 开辟了无限的可能性。 Lambda@Edge自2017年中开始提供, 是AWS的一项新功能, 它引入了以Lambdas形式直接在CloudFront的边缘服务器上执行代码的概念。
Lambda@Edge提供的新可能性之一是针对服务器端A/B测试的全新解决方案。 A/B测试是一种常见的测试网站多种变体的效果的方法, 可以同时向网站的不同细分受众群展示它们。
Lambda@Edge对A/B测试意味着什么 A/B测试的主要技术挑战是在不影响实验数据质量或网站本身的情况下, 正确划分传入流量。
实现它的主要途径有两个:客户端和服务器端。
- 客户端需要在最终用户的浏览器中运行一些JavaScript代码, 以选择要显示的变体。这种方法有两个重大缺点-最值得注意的是, 它既会减慢渲染速度, 又会导致闪烁或其他渲染问题。这意味着寻求优化其加载时间或对其UX制定高标准的网站将倾向于避免这种方法。
- 服务器端A/B测试消除了大多数此类问题, 因为返回哪种变体的决定完全在主机方面进行。浏览器简单地正常呈现每个变体, 就好像它是网站的标准版本一样。
更为复杂的是, SPA之类的现代Web应用程序通常直接作为S3存储桶中的静态代码包, 而无需使用Web服务器。即使涉及网络服务器, 更改服务器端逻辑来设置A/B测试通常也不可行。 CDN的存在又构成了另一个障碍, 因为缓存可能会影响网段的大小, 或者相反, 这种流量分段会降低CDN的性能。
Lambda@Edge所提供的一种方法可以在用户的??各种请求到达服务器之前就跨各种实验路由用户请求。该用例的基本示例可以直接在AWS文档中找到。尽管可以用作概念证明, 但具有多个并行实验的生产环境可能需要更灵活, 更强大的工具。
此外, 在使用Lambda@Edge进行了一些工作之后, 你可能会意识到在构建体系结构时需要注意一些细微差别。
例如, 部署边缘Lambda需要花费时间, 并且它们的日志分布在AWS区域中。如果需要调试配置以避免502错误, 请注意这一点。
本教程将向AWS开发人员介绍一种使用Lambda@Edge来实现服务器端A/B测试的方式, 该方式可以在整个实验中重复使用, 而无需修改和重新部署边缘Lambda。它以AWS文档和其他类似教程中的示例方法为基础, 但不是在Lambda本身中对流量分配规则进行硬编码, 而是定期从S3上的配置文件中检索规则, 你可以随时对其进行更改。
Lambda@Edge A/B测试方法概述 这种方法背后的基本思想是让CDN将每个用户分配给一个网段, 然后将用户路由到相关的原始配置。 CloudFront允许分发指向S3或自定义来源, 在这种方法中, 我们同时支持这两种方式。
段到实验变体的映射将存储在S3上的JSON文件中。为简单起见, 此处选择了S3, 但也可以从数据库或边缘Lambda可以访问的任何其他存储形式中检索到S3。
注意:有一些限制-请参阅AWS Blog上[利用电子邮件保护]中的利用外部数据一文, 以了解更多信息。
实作 可以通过四种不同类型的CloudFront事件来触发Lambda@Edge:
文章图片
在这种情况下, 我们将在以下三个事件中的每个事件上运行Lambda:
- 查看者要求
- 来源要求
- 观众回应
- abtesting-lambda-vreq:大多数逻辑都包含在此lambda中。首先, 为传入的请求读取或生成唯一的ID cookie, 然后将其散列为[0, 1]范围。然后, 从S3获取流量分配图, 并在执行过程中将其缓存。最后, 使用哈希值向下选择原始配置, 该配置将作为JSON编码的标头传递给下一个Lambda。
- abtesting-lambda-oreq:这将从先前的Lambda读取原始配置, 并相应地路由请求。
- abtesting-lambda-vres:只需添加Set-Cookie标头即可将唯一ID Cookie保存在用户的浏览器中。
在本教程中, 存储桶将如下所示:
- 令人反感的-ttblog-公开
- 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文档中描述了原始配置格式。
【使用AWS Lambda@Edge进行灵活的A/B测试】abtesting拉姆达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 hourconst 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 lateruniqueId = 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 = http://www.srcmini.com/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的需求。
出于性能方面的考虑, 流量分配规则会在Lambda调用之间进行缓存, 而不是针对每个请求从S3中获取它们。在此示例中, 我们将缓存TTL设置为1小时。
请注意, X-ABTesting-Segment-Origin标头需要在CloudFront中列入白名单;否则, 它将在到达原始请求Lambda之前从请求中删除。
abtesting拉姆达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 = http://www.srcmini.com/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在高速缓存未命中的情况下将请求路由到相应的来源。
令人讨厌的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负责在Set-Cookie标头中返回生成的唯一ID cookie。如上所述, 如果已经使用唯一的客户端ID, 则可以完全省略此Lambda。
实际上, 即使在这种情况下, 也可以通过查看器请求Lambda使用重定向设置cookie。但是, 这可能会增加一些延迟, 因此在这种情况下, 我们更喜欢在单个请求-响应周期中进行。
该代码也可以在GitHub上找到。
Lambda权限 与任何边缘Lambda一样, 你可以在创建Lambda时使用CloudFront蓝图。否则, 你将需要创建一个自定义角色并附加” 基本Lambda@Edge权限” 策略模板。
对于查看器请求Lambda, 你还需要允许访问包含流量分配文件的S3存储桶。
部署Lambda 设置边缘Lambda与标准Lambda工作流程有些不同。在Lamba的配置页面上, 单击” 添加触发器” , 然后选择CloudFront。这将打开一个小对话框, 可让你将此Lambda与CloudFront分配关联。
为三个Lambda分别选择适当的事件, 然后按” 部署” 。这将开始将功能代码部署到CloudFront的边缘服务器的过程。
注意:如果需要修改边缘Lambda并将其重新部署, 则需要先手动发布新版本。
CloudFront设置 为了使CloudFront分发能够将流量路由到某个来源, 你将需要在” 来源” 面板中分别设置每个端口。
你唯一需要更改的配置设置是将X-ABTesting-Segment-Origin标头列入白名单。在CloudFront控制台上, 选择你的分配, 然后按编辑以更改分配的设置。
在” 编辑行为” 页上, 从” 基于选定的请求头进行缓存” 选项的下拉菜单中选择” 白名单” , 然后将自定义X-ABTesting-Segment-Origin标头添加到列表中:
文章图片
如果你按照上一节中的说明部署了边缘Lambda, 则它们应该已经与你的发行版相关联, 并在” 编辑行为” 页面的最后一部分中列出。
小警告的好解决方案 对于部署在CloudFront等CDN服务后面的高流量网站, 要正确实施服务器端A/B测试可能是一项挑战。在本文中, 我们演示了如何通过将实现细节隐藏到CDN本身中来将Lambda@Edge用作解决此问题的新颖方法, 同时还提供了运行A/B实验的干净可靠的解决方案。
但是, Lambda@Edge有一些缺点。最重要的是, CloudFront事件之间的这些其他Lambda调用可能在延迟和成本方面加起来, 因此应首先仔细衡量它们对CloudFront分布的影响。
此外, Lambda@Edge是AWS的一个相对较新且仍在发展中的功能, 因此自然地, 它的边缘仍然有些粗糙。更为保守的用户可能仍需要等待一段时间, 然后再将其放置在基础架构的关键位置。
话虽这么说, 它提供的独特解决方案使其成为CDN不可或缺的功能, 因此可以期望它在将来被更广泛地采用而并非不合理。
文章图片
推荐阅读
- 时间戳截断(一个Ruby on Rails ActiveRecord的故事)
- 健全的逻辑和单调的人工智能模型
- 8种无服务器计算平台来运行你的应用程序代码
- 安卓基础之读取联系人的姓名和电话
- Server.MapPath是使用
- 安卓 通过www读取Application.persistentDataPath
- Android 如何将手机屏幕投屏到PC端
- 项目初始化以后出现(Unable to load script from assets 'index.android.bundle)
- Spring08-----IoC容器ApplicationContext