Fastly 全球规模边缘云计算实践

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GIPHY 提供大量的 GIF 媒体内容。事实上,每天有超过 100 亿条内容。除代表 GIF 实际下载的媒体请求外,我们还提供了公共 API 和 SDK 服务,让开发者可以在他们的产品中使用,从而使他们的用户能够访问我们庞大的库。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和很多每天都有大量流量的科技公司一样,我们面临着可扩展性的挑战。系统必须能够处理大量的请求 (在每秒 10000 个请求之内),并且响应延迟很小。最糟糕的事莫过于等待加载,特别是 GIF!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这就是边缘云平台(edge cloud platform)发挥作用的地方:边缘云平台并不是让我们的 AWS 服务器处理每个请求,而是尽可能多地缓存媒体内容和搜索结果 JSON 负载。这样做是非常有效的,因为媒体内容和 API 响应都不会频繁改变。边缘云平台服务器还将请求负载分配给不同的区域。我们使用 Fastly 驱动边缘云平台,为用户提供了数十亿条内容。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Fastly 解决方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fastly 提供多种功能,使我们能够大规模地交付内容。这些特性可以大致归类为:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缓存分层"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缓存管理"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"边缘计算"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"缓存分层"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基本边缘云平台的设置是将内容缓存在边缘。这些服务器节点分布在全球,向在其区域内发送请求的用户提供缓存内容。如果边缘节点没有任何内容,则会向原始服务器(origin server)发送请求,以便检索内容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/54\/546c0b0777fa75e610a62c41490f5ee2.jpeg","alt":"image.png","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样的单层设置存在缺陷。每一个边缘节点都根据其区域的请求维护自己的缓存。所以,一个新的内容片段可能不会在任何一个边缘节点上被缓存,这可能会导致当每个边缘节点都重复相同的内容请求时,到我们的原始服务器的流量会激增。由于病毒式内容的流行程度越来越高,这种行为常会出现。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fastly 提供名为 Origin Shield 的第二层缓存服务。现在,可以从 Origin Shield 层检索缓存中没有请求内容的边缘节点,请求只需要到达我们的原始服务器。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/31\/312c48b1ab8975bf2fb628fdeecc3f2e.jpeg","alt":"image.png","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"缓存管理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然内容被缓存在边缘和 Origin Shield,我们需要设法管理其缓存策略。并不是所有的内容都应该保持相同的缓存时间,或者说 TTL(Time to Live,生存时间)。例如,单个 GIF 的信息不会有太大的变化,所以它的 API 响应可以在一个相当长的一段时间内被缓存。而 "},{"type":"link","attrs":{"href":"https:\/\/developers.giphy.com\/docs\/api\/endpoint#trending","title":null,"type":null},"content":[{"type":"text","text":"Trending Endpoint"}]},{"type":"text","text":" 的 API 响应则返回当前趋势 GIF 的持续更新列表,由于趋势的性质,它需要在一个较短的 TTL 上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Fastly 是由 Varnish 驱动的,所以所有的配置都采用 Varnish 配置语言(VCL)代码的形式执行。边缘和 Origin Shield 都运行 VCL 代码,因此我们能够通过一些简单的 VCL 代码,基于 API 端点路径设置各种缓存 TTL:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_fetch\nif (req.url ~ \"^\/v1\/gifs\/trending\") {\n # set 5 minute ttl for trending responses\n set beresp.ttl = 600s;\n return(deliver);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"并不总是用 VCL 代码来设置缓存 TTL。发送到 Origin 的 API 请求,可以在 Origin 的响应中对缓存控制指令进行编码。仅需设置 VCL 代码即可使其被重写。在 Origin 中,我们可以通过在 API 响应中设置缓存控制头,将这一决定传递给 Fastly 的 Origin Shield 和边缘节点。尤其是 Surrogate-Control 头,因为这个头将仅用于 Fastly 节点。所以我们可以更新上述 VCL,使 Surrogate-Control 优先于端点缓存策略,如下所示:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_fetch\nif (beresp.http.Surrogate-Control ~ \"max-age\" || beresp.http.Cache-Control ~ \"(s-maxage|max-age)\"\n) {\n # upstream set some cache control headers, so Fastly will use its cache TTL\n return(deliver);\n} else {\n # no cache headers, so use cache policies for endpoints\n if (req.url ~ \"^\/v1\/gifs\/trending\") {\n # set 10 minute ttl for trending responses\n set beresp.ttl = 600s;\n return(deliver);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过这样的设置,我们可以让缓存内容通过动态 TTL 策略自动失效,从而满足我们的需求,但是如果我们不希望等待缓存自然过期,也需要显式地让缓存失效。只需通过缓存键(URL)就可以让缓存失效。对于媒体来说,这很有效,但是对 API 的响应有点复杂。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,我们的 API 搜索端点可以为不同的查询返回相同的 GIF,但是如果我们希望使其失效,则无法知道每个可能生成 GIF 的 URL:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# same GIF can appear in the response of all of these API calls\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=haha\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=hehe\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY2__&q=lol\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY3__&q=laugh"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于这种情况,我们利用了 Fastly 的代理键(Surrogate Key)!顾名思义,代理键能够唯一地识别缓存的内容,与缓存键的方式基本相同。与缓存键不同,每个存储结果可以有多个代理键,我们可以设置代理键。通过在每个 API 响应中显示的 GIF ID,使我们可以确定包含特定 GIF 的多个缓存内容:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# same GIF (gif_id_abc) can appear in the response of all of these API calls\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=haha\n Assign Surrogate Key: gif_id_abc\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=hehe\n Assign Surrogate Key: gif_id_abc\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY2__&q=lol\n Assign Surrogate Key: gif_id_abc\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY3__&q=laugh\n Assign Surrogate Key: gif_id_abc"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"还可以为同一内容添加多个代理键:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"https:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=haha\n Assign Surrogate Key: gif_id_abc gif_id_def key_KEY1 q_haha\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY1__&q=hehe\n Assign Surrogate Key: gif_id_abc gif_id_123 key_KEY1 q_hehe\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY2__q=lol\n Assign Surrogate Key: gif_id_abc, gif_id_321 gif_id_456 key_KEY2 q_lol\nhttps:\/\/api.giphy.com\/v1\/gifs\/search?api_key=__KEY3__&q=laugh\n Assign Surrogate Key: gif_id_abc key_KEY3 q_laugh"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代理键是一个强大的特性,可以让我们选择合适的缓存,非常精确而简单地使其失效。通过这种设置,我们可以在下列情况使缓存失效:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使所有包含特定 GIF 的缓存 API 响应失效;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使针对特定 API 键的所有缓存 API 响应无效;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使查询某些单词的所有缓存 API 响应无效。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"在边缘运行代码"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"VCL 为我们在边缘云平台的配置方面提供了大量功能。我们之前展示了配置如何为边缘和 Origin Shield 节点设置各种缓存 TTL 策略,但是我们还可以使用 VCL 设置请求信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以用代码来重写传入的请求 URL。如果我们需要修改我们的 API 端点,那么这样做会更方便,而不用麻烦我们的消费者来更新他们的调用。"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_recv\nif (req.url ~ “^\/some-old-endpoint”) {\n # rewrite to the new endpoint\nset req.url = regsub(req.url, “\/some-old-endpoint”, “\/new-and-improved-endpoint”);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"还可以选择一定比例的传入请求来测试实验特性。利用 Fastly 的"},{"type":"link","attrs":{"href":"https:\/\/developer.fastly.com\/reference\/vcl\/functions\/randomness\/","title":null,"type":null},"content":[{"type":"text","text":"随机性库"}]},{"type":"text","text":",我们可以为某些请求中添加一个特殊的头,以实现原始服务器上的新行为。"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_recv\nset req.http.new_feature = 0\nif (randombool(1,10000)) {\n # .01% of the traffic gets to see the new feature\nset req.http.new_feature = 1;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它结合了 Fastly 的边缘字典,使得我们可以用最少的代码建立不同的行为。"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# API keys that will have a percentage of their request use the new feature\ntable new_feature_access {\n “__API_KEY1__”: “1”,\n “__API_KEY2__”: “5”,\n “__API_KEY3__”: “1000”,\n}\nsub vcl_recv {\nset req.http.new_feature = 0\n# check if request has an api key that is setup to have a percentage of its requests use the new feature\nif (randombool(std.atoi(table.lookup(new_feature_access, subfield(req.url.qs, \"api_key\", \"&\"), \"0\"))\n,10000)) {\nset req.http.new_feature = 1;\n}\nreturn(lookup);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这只是触及了 VCL 实现的功能的皮毛。如果你想知道还有什么可以做的,可以在"},{"type":"link","attrs":{"href":"https:\/\/developer.fastly.com\/","title":null,"type":null},"content":[{"type":"text","text":"这里"}]},{"type":"text","text":"找到 Fastly 的文档!"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"技巧提示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们使用 Fastly 的很多特性来为世界提供 GIF 动画内容。但是,当你可以使用如此多的特性时,配置边缘云平台可能会变得非常复杂,因此,下面是一些我们推荐的技巧提示可以帮助你完成这个任务。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"在边缘和 Origin Shield 中执行 VCL"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于两层缓存设置,有一个需要记住的关键问题是,将在边缘和 Origin Shield 执行相同的 VCL 代码。这可能导致 VCL 代码在请求\/响应的状态信息更改时出现意外的结果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"举例来说,我们之前的 VCL 代码将根据由上游缓存控制头或 VCL 代码本身指定的缓存 TTL,为 Origin Shield 和边缘节点设置缓存 TTL:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_fetch\nif (beresp.http.Surrogate-Control ~ \"max-age\" || beresp.http.Cache-Control ~ \"(s-maxage|max-age)\"\n) {\n # upstream set some cache control headers, so Fastly will use its cache TTL\n return(deliver);\n} else {\n # no cache headers, so use cache policies for endpoints\n if (req.url ~ \"^\/v1\/gifs\/trending\") {\n # set 10 minute ttl for trending responses\n set beresp.ttl = 600s;\n return(deliver);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设对于 "},{"type":"link","attrs":{"href":"https:\/\/developers.giphy.com\/docs\/api\/endpoint#trending","title":null,"type":null},"content":[{"type":"text","text":"Trending Endpoint"}]},{"type":"text","text":",我们也设置了响应的 Cache-Control 头,这样我们就可以指示调用方将内容缓存到另一段的时间。这样做只需按以下步骤:"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_fetch\nif (beresp.http.Surrogate-Control ~ \"max-age\" || beresp.http.Cache-Control ~ \"(s-maxage|max-age)\"\n) {\n # upstream set some cache control headers, so Fastly will use its cache TTL\n return(deliver);\n} else {\n # no cache headers, so use cache policies for endpoints\n if (req.url ~ \"^\/v1\/gifs\/trending\") {\n # set 10 minute ttl for trending responses\n set beresp.ttl = 600s;\n # set 30 second ttl for callers\n set beresp.http.cache-control = \"max-age=30\";\n return(deliver);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Origin Shield 会执行这段 VCL 代码,向响应的头添加 Cache-Control 头,并将其返回到边缘。但是,在边缘处,它将看到响应中设置了 Cache-Control,并会执行 if 语句。这将导致边缘节点使用 30 秒的缓存 TTL,而不是预期的 10 分钟!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幸运的是,Fastly 提供了一种区分边缘和 Origin Shield 的方法,它在请求中设置了头("},{"type":"codeinline","content":[{"type":"text","text":"Fastly-FF"}]},{"type":"text","text":"):"}]},{"type":"codeblock","attrs":{"lang":"c"},"content":[{"type":"text","text":"# in vcl_fetch\nif (req.url ~ \"^\/v1\/gifs\/trending\") {\n # set 10 minute ttl for trending responses\n set beresp.ttl = 600s;\n return(deliver);\n}\n# in vcl_deliver\nif (!req.http.Fastly-FF) {\n # set 30 second ttl for callers\n set resp.http.cache-control = \"max-age=30\";\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过这个添加,Cache-Control 头将仅在边缘节点上设置,我们的缓存策略再次按预期运行!"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"调试和测试"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们刚才提到的陷阱可能难以发现和调试。VCL 代码只是运行在服务器上,并向你显示响应和响应头信息。只需将调试信息添加到自定义头信息中,并在响应中查看它们,但是这很快就会变得不方便了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所幸的是,"},{"type":"link","attrs":{"href":"https:\/\/developer.fastly.com\/learning\/vcl\/fiddle\/","title":null,"type":null},"content":[{"type":"text","text":"Fastly Fiddle 工具"}]},{"type":"text","text":"在执行 VCL 代码时能得到更好的信息。在这个工具中,我们可以模拟各种 VCL 代码部分,并了解 Fastly 的边缘以及 Origin Shield 服务器将如何处理 VCL 代码的信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是上述示例的 "},{"type":"link","attrs":{"href":"https:\/\/fiddle.fastlydemo.net\/fiddle\/4001c4c6","title":null,"type":null},"content":[{"type":"text","text":"fiddle"}]},{"type":"text","text":",显示双重执行 VCL 将影响缓存 TTL。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们在左边的适当部分设置了 VCL,然后执行它,查看 Fastly 将如何处理右边的请求:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/85\/85722e28bb404b338782d4f402cdb7eb.jpeg","alt":"image.png","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上图展示了在请求通过 edge 和 Origin Shield 节点时,关于它的生命周期的许多有用信息。实际环境中,VCL 代码可能会非常复杂,而这个工具在这种情况下非常出色。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者简介:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ethan Lu,API 团队技术领导。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"原文链接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/engineering.giphy.com\/how-giphy-uses-fastly-to-achieve-global-scale\/","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/engineering.giphy.com\/how-giphy-uses-fastly-to-achieve-global-scale\/"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章