作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Georgios Boutsioukis's profile image

Georgios Boutsioukis

Georgios是一名拥有超过8年经验的全栈开发人员. 他曾在欧洲核子研究中心工作,也是预订移动API团队的一员.com.

专业知识

Previously At

预订.com
分享

Content delivery networks (CDNs) like Amazon CloudFront 直到最近,它们还是网络基础设施中相对简单的一部分. Traditionally, web applications were designed around them, 将它们主要视为被动缓存,而不是主动组件.

Lambda@Edge 类似的技术已经改变了这一切,并通过在web应用程序和用户之间引入一层新的逻辑,开辟了一个全新的可能性世界. Available since mid-2017, Lambda@Edge是AWS的一个新功能,它引入了直接在CloudFront的边缘服务器上以Lambdas的形式执行代码的概念.

Lambda@Edge提供的一种新的可能性是为服务器端a /B测试提供一个干净的解决方案. A/B测试是一种测试网站多个变体性能的常用方法,通过同时向不同的网站受众展示这些变体.

What Lambda@Edge Means for A/B Testing

A/B测试的主要技术挑战是在不影响实验数据质量或网站本身的情况下,适当地分割进入的流量.

的re are two main routes for implementing it: 客户端服务器端.

  • 客户端 包括在最终用户的浏览器中运行一些JavaScript代码,该代码选择将显示哪个变体. 这种方法有几个明显的缺点——最明显的, 它既会减慢渲染速度,又会导致闪烁或其他渲染问题. 这意味着寻求优化加载时间或对用户体验有高标准的网站将倾向于避免这种方法.
  • 服务器端 A/B testing does away with most of these issues, 因为返回哪一种变体的决定完全由主机一方决定. 浏览器只是正常地呈现每个变体,就好像它是网站的标准版本一样.

考虑到这一点,你可能想知道为什么每个人都不简单地使用服务器端A/B测试. Unfortunately, 服务器端方法不像客户端方法那样容易实现, 建立一个实验通常需要对服务器端代码或服务器配置进行某种形式的干预.

To complicate things even further, 像spa这样的现代web应用程序通常是直接从S3存储桶中提供静态代码包,甚至不涉及web服务器. Even when a web server is involved, 更改服务器端逻辑来设置A/B测试通常是不可行的. 的 presence of a CDN poses yet another obstacle, as caching might affect segment sizes or, 相反, this kind of traffic segmentation can lower the CDN’s performance.

Lambda@Edge提供的是一种方法,在用户请求到达服务器之前,将它们路由到不同的实验中. A basic example of this use case can be found directly in the AWS documentation. While useful as a proof of concept, 具有多个并发实验的生产环境可能需要更加灵活和健壮的东西.

此外, after working a bit with Lambda@Edge, 您可能会意识到,在构建体系结构时需要注意一些细微差别.

For example, deploying the edge Lambdas takes time, 和 their logs are distributed across AWS regions. 如果需要调试配置以避免502错误,请注意这一点.

This tutorial will introduce AWS developers 到使用Lambda@Edge实现服务器端a /B测试的方式,这种方式可以在实验中重用,而无需修改和重新部署边缘lambda. 它以AWS文档和其他类似教程中的示例方法为基础, 而是在Lambda本身中硬编码流量分配规则, 规则定期从S3上的配置文件中检索,您可以随时更改该文件.

Overview of Our Lambda@Edge A/B Testing Approach

这种方法背后的基本思想是让CDN将每个用户分配到一个段,然后将用户路由到相关的源配置. CloudFront允许分发指向S3或自定义源, 和 in this approach, we support both.

片段到实验变量的映射将存储在S3上的JSON文件中. S3 is chosen here for simplicity, 但这也可以从边缘Lambda可以访问的数据库或任何其他形式的存储中检索.

注意: 的re are some limitations - check the article Leveraging external data in Lambda@Edge on the AWS 博客 for more info.

Implementation

Lambda@Edge可以由四种不同类型的CloudFront事件触发:

Lambda@Edge可以由四种不同类型的CloudFront事件触发

在本例中,我们将对以下三个事件分别运行Lambda:

  • Viewer 请求
  • Origin 请求
  • Viewer 响应

Each event will implement a step in the following process:

  • abtesting-lambda-vreq: Most of the logic is contained in this lambda. 第一个, a unique ID 饼干 is read or generated for the incoming 请求, 和 it is then hashed down to a [0, 1)范围. 然后从S3获取流量分配映射,并在执行期间缓存. And finally, the hashed down 价值 is used to choose an 起源 configuration, which is passed as a JSON-encoded header to the next Lambda.
  • abtesting-lambda-oreq:这将从前面的Lambda中读取原始配置,并相应地路由请求.
  • abtesting-lambda-vres: This just adds the set - 饼干 header to save the unique ID 饼干 on the user’s browser.

Let’s also set up three S3 buckets, 其中两个将包含每个实验变体的内容, 而第三个将包含带有流量分配映射的JSON文件.

For this tutorial, the buckets will look like this:

  • abtesting-ttblog-a 公共
    • 指数.html
  • abtesting-ttblog-b 公共
    • 指数.html
  • abtesting-ttblog-map 私人
    • map.json

Source Code

第一个, let’s start with the traffic allocation map:

map.json

{
    "segments": [
        {
            “体重”:0.7,
            "host": "abtesting-ttblog-a.s3.amazonaws.com”,
            “起源”:{
                "s3": {
                    "authMethod": "none",
                    "domainName": "abtesting-ttblog-a.s3.amazonaws.com”,
                    “路径”:“”,
                    "region": "eu-west-1"
                }
            }
        },
        {
            “体重”:0.3,
            "host": "abtesting-ttblog-b.s3.amazonaws.com”,
            “起源”:{
                "s3": {
                    "authMethod": "none",
                    "domainName": "abtesting-ttblog-b.s3.amazonaws.com”,
                    “路径”:“”,
                    "region": "eu-west-1"
                }
            }
        }
    ]
}

每个网段都有一个流量权重,用来分配相应的流量. We also include the 起源 configuration 和 host. 的 起源 configuration format is described in the AWS documentation.

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',
    关键:“地图.json’,
};
const SEGMENT_MAP_TTL = 3600000; // TTL of 1 hour

const fetchSegmentMapFromS3 = async () => {
    const 响应 = await s3.getObject(s3Params).承诺();
    返回JSON.parse(响应.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.现在();
    }

    return _segmentMap;
}

// Just generate a r和om UUID
const getR和omId = () => {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.r和om() * 16 | 0,
            v = c == 'x' ? r : (r & 0x3 | 0x8);
        返回v.toString(16);
    });
};

//该函数将散列任何字符串(在本例中是我们的随机UUID)
// to a [0, 1) range
const hashToInterval = (s) => {
    let hash = 0,
        i = 0;

    while (i < s.长度){
        hash = ((hash << 5) - hash + s.charCodeAt(i++)) << 0;
    }
    return (hash + 2147483647) % 100 / 100;
}

const getCookie = (头, 饼干Key) => {
    if (头.饼干){
        for (let 饼干Header of 头.饼干){
            const 饼干s = 饼干Header.价值.split(';');
            for (let 饼干 of 饼干s) {
                const [key, val] = 饼干.split('=');
                if (key === 饼干Key) {
                    返回val;
                }
            }
        }
    }
    return null;
}

const getSegment = async (p) => {
    const segmentMap = await fetchSegmentMap();
    let weight = 0;
    for (const segment of segmentMap.段){
        weight += segment.重量;
        if (p < weight) {
            return segment;
        }
    }
    控制台.error(`No segment for 价值 ${p}. Check the segment map.`);
}

出口.h和ler = async (event, context, callback) => {
    const 请求 = event.记录[0].cf.请求;
    const 头 = 请求.头;

    let uniqueId = getCookie(头, COOKIE_KEY);
    if (uniqueId === null) {
        // This is what happens on the first visit: we'll generate a new
        // unique ID, then leave it the 饼干 header for the
        // viewer 响应 lambda to set permanently later
    
        uniqueId = getR和omId();
        const 饼干 = `${COOKIE_KEY}=${uniqueId}`;
        头.饼干 = 头.饼干 || [];
        头.饼干.push({ key: 'Cookie', 价值: 饼干 });
    }
    
    // Get a 价值 between 0 和 1 和 use it to
    // resolve the traffic segment
    const p = hashToInterval(uniqueId);
    const segment = await getSegment(p);
    
    // Pass the 起源 data as a header to the 起源 请求 lambda
    // 的 header key below is whitelisted in Cloudfront
    const headerValue = JSON.stringify ({
        host: segment.主人
        起源: segment.起源
    });
    头['x-abtesting-segment-起源'] = [{
        key: 'X-ABTesting-Segment-Origin',
        价值: headerValue
    }];
    
    callback(null, 请求);
};

在这里, we explicitly generate a unique ID for this tutorial, 但对于大多数网站来说,使用其他客户端ID是很常见的. This would also eliminate the need for the viewer 响应 Lambda.

For performance considerations, 流量分配规则跨Lambda调用缓存,而不是在每个请求时从S3获取. In this example, we set up a cache TTL of 1 hour.

Note that the X-ABTesting-Segment-Origin header needs to be whitelisted in CloudFront; otherwise, 它将在到达原始请求Lambda之前从请求中删除.

abtesting-lambda-oreq

'use strict';

const HEADER_KEY = 'x-abtesting-segment-起源';

// Origin Request h和ler
出口.h和ler = (event, context, callback) => {
    const 请求 = event.记录[0].cf.请求;
    const 头 = 请求.头;

    const headerValue = 头[HEADER_KEY]
                        && 头[HEADER_KEY][0]
                        && 头[HEADER_KEY][0].价值;
    
    if (headerValue) {
        const segment = JSON.parse(headerValue);
        头['host'] = [{ key: 'host', 价值: segment.主机}];
        请求.起源 = segment.起源;
    }
    
    callback(null, 请求);
};

的 起源 请求 Lambda is pretty straightforward. 的 起源 configuration 和 host is read from the X-ABTesting-Origin 头,该头在前一步中生成并注入到请求中. 这指示CloudFront在缓存丢失的情况下将请求路由到相应的源.

abtesting-lambda-vres

'use strict';

const COOKIE_KEY = 'abtesting-unique-id';

const getCookie = (头, 饼干Key) => {
    if (头.饼干){
        for (let 饼干Header of 头.饼干){
            const 饼干s = 饼干Header.价值.split(';');
            for (let 饼干 of 饼干s) {
                const [key, val] = 饼干.split('=');
                if (key === 饼干Key) {
                    返回val;
                }
            }
        }
    }
    return null;
}

const setCookie = function (响应, 饼干){
    控制台.log(`Setting 饼干 ${饼干}`);
    响应.头['set-饼干'] = 响应.头['set-饼干'] || [];
    响应.头['set-饼干'] = [{
        key: "set - 饼干",
        价值: 饼干
    }];
}

出口.h和ler = (event, context, callback) => {
    const 请求 = event.记录[0].cf.请求;
    const 头 = 请求.头;
    const 响应 = event.记录[0].cf.反应;

    const 饼干Val = getCookie(头, COOKIE_KEY);
    if (饼干Val != null) {
        setCookie(响应, `${COOKIE_KEY}=${饼干Val}`);
        callback(null, 响应);
        返回;
    }
    
    控制台.log(`no ${COOKIE_KEY} 饼干`);
    callback(null, 响应);
}

最后, 查看器响应Lambda负责返回生成的惟一ID 饼干 set - 饼干 header. 如上所述,如果已经使用了唯一的客户机ID,则可以完全省略该Lambda.

实际上,即使在这种情况下,也可以通过查看器请求Lambda重定向来设置饼干. 然而, this could add some latency, so in this case, we prefer to do it in a single 请求-响应 cycle.

的 code can also be found on GitHub.

Lambda Permissions

与任何边缘Lambda一样,您可以在创建Lambda时使用CloudFront蓝图. 否则, 您需要创建一个自定义角色并附加“Basic Lambda@Edge Permissions”策略模板.

For the viewer 请求 Lambda, 您还需要允许访问包含流量分配文件的S3存储桶.

Deploying the Lambdas

设置边缘Lambda与标准Lambda工作流有些不同. 在Lamba的配置页面,单击“添加触发器”,选择CloudFront. 这将打开一个小对话框,允许您将这个Lambda与CloudFront发行版关联起来.

为三个Lambdas中的每一个选择适当的事件,然后按“Deploy”.这将启动将功能代码部署到CloudFront边缘服务器的过程.

注意: If you need to modify an edge Lambda 和 redeploy it, you need to manually publish a new version first.

CloudFront Settings

为了使CloudFront分发能够将流量路由到一个原点, you will need to set up each one separately in the 起源s panel.

您需要更改的唯一配置设置是将 X-ABTesting-Segment-Origin header. 在 CloudFront 控制台,选择您的发行版,然后按编辑键更改发行版的设置.

Edit Behavior 页面, select 白名单 from the dropdown menu on the Cache Based on Selected Request Headers option 和 add a custom X-ABTesting-Segment-Origin header to the list:

CloudFront Settings

如果您按照前一节中描述的方式部署了边缘Lambdas, 它们应该已经与您的发行版相关联,并在文档的最后一部分中列出 Edit Behavior 页面.

A Good Solution with Minor Caveats

对于部署在CDN服务(如CloudFront)后面的高流量网站,服务器端A/B测试可能具有挑战性. In this article, 我们演示了如何通过将实现细节隐藏在CDN本身中,将Lambda@Edge作为解决此问题的新方案, 同时也为运行a /B实验提供了一个干净可靠的解决方案.

然而, Lambda@Edge has a few drawbacks. Most importantly, CloudFront事件之间这些额外的Lambda调用会增加延迟和成本, 所以它们对CloudFront发行版的影响应该首先仔细衡量.

此外, Lambda@Edge是AWS的一个相对较新的、仍在发展中的特性, so naturally, it still feels a bit rough around the edges. 更保守的用户可能仍然希望等待一段时间,然后再将其放置在基础设施的关键点上.

That being said, 它提供的独特解决方案使其成为cdn不可或缺的功能, 因此,期望它在未来得到更广泛的采用并不是不合理的.


AWS Advanced Consulting Partner badge

Underst和ing the basics

  • What is Lambda@Edge?

    Lambda@Edge是CloudFront的一个特性,它允许您以AWS Lambda函数的形式运行自定义代码来响应CloudFront事件, which are executed close to CloudFront’s edge servers. 这些函数可用于在请求-响应周期的任何点修改CloudFront的行为.

  • How does Lambda@Edge work?

    Lambda@Edge使用附加到CloudFront事件触发器的标准AWS Lambda函数, 这对应于CloudFront在响应HTTP请求时可能进入的四个阶段. During each triggered execution, Lambda可以在将请求或响应对象传递到下一阶段之前读取和修改它.

  • When should I use AWS Lambda@Edge?

    AWS Lambda@Edge可用于在请求到达CloudFront原点之前决定如何处理请求. A popular use case is 起源 selection for A/B testing, 但是也可以以类似的方式使用它来提供针对特定设备类型或用户区域进行优化的自定义内容. 其他用例包括通过CloudFront进行用户身份验证.g., 在提供静态资产时验证用户凭据)以及某些形式的细粒度缓存优化.

  • Is something similar available outside AWS?

    At the time of writing, 在微软Azure或谷歌云平台上都没有类似的Lambda@Edge. A current competing alternative is CloudFlare’s Workers feature, 在原则上,这应该使实现a /B测试的类似解决方案成为可能. 然而, 这种情况可能很快就会改变,因为这两种云服务将来都可能开始提供类似的服务.

  • Would this increase CloudFront costs?

    As CloudFront is charged based on volume, 适当实施的任何形式的A/B测试都不会对CDN流量产生重大影响. Lambda@Edge invocations are, 然而, charged separately in the same manner as AWS Lambda functions - i.e.,基于调用的次数和每个Lambda执行的总持续时间.

Hire a Toptal expert on this topic.
现在雇佣
Georgios Boutsioukis's profile image
Georgios Boutsioukis

位于 Athens, Central Athens, Greece

Member since January 17, 2018

关于 the author

Georgios是一名拥有超过8年经验的全栈开发人员. 他曾在欧洲核子研究中心工作,也是预订移动API团队的一员.com.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

Previously At

预订.com

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal 开发人员

Join the Toptal® 社区.