我们如何以及为何将 babylon.js 迁移到 Azure:CORS、gzip 和 IndexedDB





0/5 (0投票)
在本教程中,我想谦虚地以我们最近在 WebGL 开源游戏框架 babylon.js 及其网站 babylonjs.com 上取得的“成功案例”之一为例。
你正在一家初创公司工作。突然,辛勤的一年编码开始得到回报——成功带来了更多的增长和对你 वेब 应用程序扩展的需求。
在本教程中,我想谦虚地以我们最近在 WebGL 开源游戏框架 babylon.js 及其网站 babylonjs.com 上取得的“成功案例”之一为例。我们很高兴看到如此多的 Web 游戏开发者尝试使用它。但为了跟上需求,我们知道我们需要一个新的 Web 托管解决方案。虽然本教程专注于 Microsoft Azure,但许多概念也适用于你可能偏好的各种解决方案。我们还将看到我们采取的各种优化措施,以尽可能限制我们服务器到你的浏览器的输出带宽。
- 引言
- 第一步:迁移到 Azure Web Sites & Autoscale 服务
- 第二步:将资产移入 Azure Blob Storage,启用 CORS、gzip 支持 & CDN
- 第三步:使用 HTML5 IndexedDB 避免重新下载资产
引言
Babylon.js 是我们一年多来一直在开发的个人项目。由于是个人项目(即我们自己的时间和金钱),我们一直使用相对廉价的主机解决方案来托管网站、纹理和 3D 场景,使用的是一台小型专用 Windows/IIS 机器。该项目始于法国,但很快就引起了全球几位 3D 和 Web 专家的关注,以及一些游戏工作室的关注。我们对社区的反馈感到满意,但流量是可以管理的!
例如,在 2014 年 2 月至 2014 年 4 月期间,我们有平均每月 7000 多名用户,平均每月 16000 多页被浏览。我们参加的一些活动也产生了一些有趣的流量高峰。
但是网站的体验仍然足够好。加载我们的场景速度不算很快,但用户也没有抱怨太多。
然而,最近,一个 很棒的家伙 决定在 Hacker News 上分享我们的工作。我们对这样的新闻感到非常高兴!但看看网站连接发生了什么。
我们的小服务器完蛋了!它慢慢停止工作,用户体验非常糟糕。IIS 服务器忙于提供大型静态资产和图像,CPU 使用率过高。当我们即将启动基于 babylon.js 的《刺客信条:海盗 WebGL 体验》项目时,是时候通过使用云解决方案切换到更具可扩展性的专业托管了。
但在审查我们的托管选择之前,让我们简要谈谈我们引擎和网站的细节。
- 我们的网站上的一切都是静态的。我们目前没有任何服务器端代码在运行。
- 我们的场景(.babylon JSON 文件)和纹理(.png 或 .jpeg)文件可能非常大(高达 100 MB)。这意味着我们绝对需要激活“.babylon”场景文件的gzip 压缩。事实上,在我们的案例中,定价将很大程度上取决于传出带宽。
- WebGL 画布绘图需要特殊的安全检查。例如,你不能在没有启用 CORS 的情况下从另一个服务器加载我们的场景和纹理。
鸣谢:我想特别感谢Benjamin Talmard,他是一位法国 Azure 技术布道师,帮助我们迁移到 Azure。
第一步:迁移到 Azure Web Sites & Autoscale 服务
我们希望将大部分时间花在为我们的引擎编写代码和功能上,因此我们不想在基础架构上浪费时间。因此,我们立即决定选择 PaaS 方法而不是 IaaS 方法。
此外,我们喜欢 Visual Studio 与 Azure 的集成。我几乎可以在我最喜欢的 IDE 中完成所有事情。而且,尽管 babylon.js 托管在 Github 上,但我们使用 Visual Studio 2013、TypeScript 和 Visual Studio Online 来编写我们的引擎代码。作为你项目的提示,你可以免费获得 Visual Studio Community 和 Azure 试用版。
迁移到 Azure 大约花了 5 分钟:
- 我在管理页面:http://manage.windowsazure.com 上创建了一个新的 Web Site(也可以在 VS 中完成)。
- 我从我们的源代码存储库中提取了与当前在线版本匹配的正确更改集。
- 我在 Visual Studio 的解决方案资源管理器中右键单击了 Web 项目。
- 这就是工具的强大之处。由于我使用与我的 Azure 订阅关联的 Microsoft 帐户登录了 VS,因此向导让我可以直接选择我想部署的网站。
无需担心复杂的身份验证、连接字符串或其他任何东西。
“下一步,下一步,下一步 & 发布”,几分钟后,在上传完我们所有资产和文件后,网站就上线并运行了!
在配置方面,我们希望利用很棒的自动缩放服务。这在我们之前的 Hacker News 事件中会非常有帮助。
首先,你需要在“Scale”选项卡中将实例配置为“Standard”模式。
然后,你可以选择自动扩展到的最大实例数,以及在何种 CPU 条件下以及在何种预定时间进行扩展。在我们的案例中,我们决定最多使用 3 个小型实例(1 核,1.75 GB 内存),并在 CPU 使用率超过 80% 时自动启动一个新实例。当 CPU 使用率下降到 60% 以下时,我们会删除一个实例。自动缩放机制在我们的案例中始终处于开启状态,我们没有设置特定的预定时间。
这个想法是只为你在特定时间段和负载下需要的东西付费。我喜欢这个概念。有了它,我们就能在什么都不做的情况下处理之前的流量高峰,这要归功于这个 Azure 服务!这才是服务。
你还可以通过紫色图表快速查看自动缩放历史。在我们的案例中,自从我们迁移到 Azure 以来,我们至今从未超过 1 个实例。下面我们将看到如何尽量减少进入自动缩放的风险。
总结网站配置,我们希望启用自动 gzip 压缩来处理我们特定的 3D 引擎资源(.babylon & .babylonmeshdata 文件)。这对我们至关重要,因为它可以节省高达 3 倍的带宽,从而……降低成本。
Web Sites 是在 IIS 上运行的。要配置 IIS,你需要进入 web.config 文件。在我们的案例中,我们使用的是以下配置。
<system.webServer> <staticContent> <mimeMap fileExtension=".dds" mimeType="application/dds" /> <mimeMap fileExtension=".fx" mimeType="application/fx" /> <mimeMap fileExtension=".babylon" mimeType="application/babylon" /> <mimeMap fileExtension=".babylonmeshdata" mimeType="application/babylonmeshdata" /> <mimeMap fileExtension=".cache" mimeType="text/cache-manifest" /> <mimeMap fileExtension=".mp4" mimeType="video/mp4" /> </staticContent> <httpCompression> <dynamicTypes> <clear /> <add enabled="true" mimeType="text/*"/> <add enabled="true" mimeType="message/*"/> <add enabled="true" mimeType="application/x-javascript"/> <add enabled="true" mimeType="application/javascript"/> <add enabled="true" mimeType="application/json"/> <add enabled="true" mimeType="application/atom+xml"/> <add enabled="true" mimeType="application/atom+xml;charset=utf-8"/> <add enabled="true" mimeType="application/babylonmeshdata" /> <add enabled="true" mimeType="application/babylon"/> <add enabled="false" mimeType="*/*"/> </dynamicTypes> <staticTypes> <clear /> <add enabled="true" mimeType="text/*"/> <add enabled="true" mimeType="message/*"/> <add enabled="true" mimeType="application/javascript"/> <add enabled="true" mimeType="application/atom+xml"/> <add enabled="true" mimeType="application/xaml+xml"/> <add enabled="true" mimeType="application/json"/> <add enabled="true" mimeType="application/babylonmeshdata" /> <add enabled="true" mimeType="application/babylon"/> <add enabled="false" mimeType="*/*"/> </staticTypes> </httpCompression> </system.webServer>
这个解决方案效果很好,我们甚至注意到加载场景的时间比以前的主机有所缩短。我猜这是因为 Azure 数据中心使用了更好的基础架构和网络。
然而,我一直在考虑迁移到 Azure。我最初的想法不是让 Web Sites 实例来服务我的大型资产。从一开始,我一直对将我的资产存储在更适合此目的的 Blob Storage 中更感兴趣。这也可以为我们提供 CDN 方案。
第二步:将资产移入 Azure Blob Storage,启用 CORS、gzip 支持 & CDN
在我们的案例中使用 Blob Storage 的主要原因是为了避免加载我们的 Web Site 实例的 CPU 来服务它们。如果除了一些 HTML、JS 和 CSS 文件外,所有内容都通过 Blob Storage 提供服务,那么我们的 Web Site 实例将不太可能需要自动缩放。
但这会带来两个问题需要解决。
- 由于内容将托管在不同的域名上,我们将遇到跨域安全问题。要避免这种情况,你需要在远程域(Azure Blob Storage)上启用 CORS。
- Azure Blob Storage 不支持自动 gzip 压缩。我们不想在降低 Web Site CPU 使用率的同时,却因为带宽增加而支付 3 倍的费用!
在 Blob Storage 上启用 CORS
Blob Storage 的 CORS 支持已推出数月。本文 Windows Azure Storage: Introducing CORS 解释了如何使用 Azure API 配置 CORS。我自己不想写一个小应用程序来做这件事。我在网上找到了一个现成的工具:Cynapta Azure CORS Helper – Free Tool to Manage CORS Rules for Windows Azure Blob Storage。
然后我只需启用我的容器对 GET 请求和适当标头的支持。要检查一切是否按预期工作,只需打开 F12 开发者工具栏并查看控制台日志。
如你所见,绿色日志行表示一切正常。
这是一个会失败的示例。如果你尝试直接从你的本地机器(或其他任何域)从我们的 Blob Storage 加载场景,你将在日志中看到这些错误。
总而言之,如果你发现你的调用域未出现在“Access-Control-Allow-Origin”标头中,并且紧随其后出现“Access is denied”,那是因为你没有正确设置 CORS 规则。控制你的 CORS 规则非常重要;否则,任何人都可以使用你的资产,从而消耗你的带宽,让你毫无察觉地花费金钱!
在我们的 Blob Storage 上启用 gzip 支持
正如我之前告诉你的,Azure Blob Storage 不支持自动 gzip 压缩。竞争对手的解决方案(如 S3)似乎也是如此。你有两种选择来解决这个问题。
- 在上传之前在客户端对文件进行 gzip 压缩,使用经典工具将其上传到 Blob Storage,并将“content-encoding”标头设置为“gzip”。这个解决方案有效,但仅适用于支持 gzip 的浏览器(还有不支持 gzip 的浏览器吗?)
- 在客户端对文件进行 gzip 压缩,并在 Blob Storage 中上传两个版本:例如,一个带有默认的 .extension,另一个带有 .extension.gzip。在 IIS 端设置一个处理程序,该处理程序将捕获客户端的 HTTP 请求,检查“accept-encoding”标头是否设置为“gzip”,并根据这种支持提供相应的文件。你可以在本文中找到有关实现代码的更多详细信息:Serving GZip Compressed Content from the Azure CDN。
在我们的案例中,我不知道有任何支持 WebGL 但不支持 gzip 压缩的浏览器。因此,如果浏览器不支持 gzip,那么继续下去就没有多大意义了,因为这可能意味着 WebGL 也不受支持。
然后我选择了第一个解决方案。由于我们没有太多场景,而且我们不是每天都制作新的场景,所以我目前使用的是这种手动流程。
- 使用 7-zip,我在我的机器上使用 gzip 编码和“compression level”设置为“fastest”来压缩 .babylon 文件。其他压缩级别似乎在我的测试中会产生问题。
- 我使用 CloudBerry Explorer for Microsoft Azure Cloud Storage 上传文件。
- 我使用 CloudBerry 手动将 HTTP 标头“content-encoding”设置为“gzip”。
我知道你在想什么。我会为我所有的文件这样做吗?!?不,你可以构建一个工具或后期构建脚本来自动化这个过程。例如,这是一个我构建的小型命令行工具。
string accountName = "yoda"; string containerName = "wwwbabylonjs"; string accountKey = "yourmagickey"; string sceneTextContent; // First argument must be the directory into the Azure Blob Container targeted string directory = args[0]; try { StorageCredentials creds = new StorageCredentials(accountName, accountKey); CloudStorageAccount account = new CloudStorageAccount(creds, useHttps: true); CloudBlobClient client = account.CreateCloudBlobClient(); CloudBlobContainer blobContainer = client.GetContainerReference(containerName); blobContainer.CreateIfNotExists(); var sceneDirectory = blobContainer.GetDirectoryReference(directory); string[] filesArgs = args.Skip(1).ToArray(); foreach (string filespec in filesArgs) { string specdir = Path.GetDirectoryName(filespec); string specpart = Path.GetFileName(filespec); if (specdir.Length == 0) { specdir = Environment.CurrentDirectory; } foreach (string file in Directory.GetFiles(specdir, specpart)) { string path = Path.Combine(specdir, file); string sceneName = Path.GetFileName(path); Console.WriteLine("Working on " + sceneName + "..."); CloudBlockBlob blob = sceneDirectory.GetBlockBlobReference(sceneName); blob.Properties.ContentEncoding = "gzip"; blob.Properties.ContentType = "application/babylon"; sceneTextContent = System.IO.File.ReadAllText(path); var bytes = Encoding.UTF8.GetBytes(sceneTextContent); using (MemoryStream ms = new MemoryStream()) { using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true)) { gzip.Write(bytes, 0, bytes.Length); } ms.Position = 0; Console.WriteLine("Gzip done."); blob.UploadFromStream(ms); Console.WriteLine("Uploading in " + accountName + "/" + containerName + "/" + directory + " done."); } } } } catch (Exception ex) { Console.WriteLine(ex); }
要使用它,我可以这样做:
UploadAndGzipFilesToAzureBlobStorage Scenes/Espilit C:\Boulot\Babylon\Scenes\Espilit\*.babylon* 来推送包含多个文件的场景(我们的增量场景包含多个 .babylonmeshdata 文件)。
或者简单地:
UploadAndGzipFilesToAzureBlobStorage Scenes/Espilit C:\Boulot\Babylon\Scenes\Espilit\Espilit.babylon 来推送一个单个文件。
要使用此解决方案检查 gzip 是否按预期工作,我使用的是 Fiddler。从你的客户端机器加载你的内容,并在网络跟踪中检查返回的内容是否真的被压缩并且可以被解压缩。
启用 CDN
完成前面的两个步骤后,你只需在 Azure 管理页面上单击一个按钮即可启用 CDN 并将其映射到你的 Blob Storage。
就这么简单!在我的案例中,我只需将以下 URL 从:http://yoda.blob.core.windows.net/wwwbabylonjs/Scenes 更改为 http://az612410.vo.msecnd.net/wwwbabylonjs/Scenes。请注意,如果你愿意,可以将此 CDN 域名自定义为自己的域名。
有了这个,我们就能非常快速地为你提供 3D 资产,因为你将从此处列出的节点位置之一获得服务:Azure Content Delivery Network (CDN) Node Locations.
我们的网站目前托管在北欧 Azure 数据中心。但如果你来自西雅图,你只需要 ping 这个服务器来下载我们基本的 index.html、index.js、index.css 文件和几张截图。所有 3D 资产都将从你附近的西雅图节点提供!
注意:我们所有的演示都使用了完全优化的体验(使用 gzip 的 Blob Storage、CDN 和 DB 缓存)。
第三步:使用 HTML5 IndexedDB 避免重新下载资产
优化加载时间和控制输出带宽成本不仅仅是服务器端的事情。你也可以在客户端构建一些逻辑来优化事情。幸运的是,自 babylon.js 引擎 v1.4 起,我们已经这样做了。我在本文中详细解释了我如何实现对IndexedDB 的支持:Using IndexedDB to handle your 3D WebGL assets: sharing feedbacks & tips of Babylon.JS,你可以在我们的 wiki 上找到如何在 babylon.js 中激活它:Caching the resources in IndexedDB。
基本上,你只需创建一个与 .babylon 场景同名的 .babylon.manifest 文件,然后设置你想缓存的内容(纹理和/或 JSON 场景)。就是这样。
例如,看看 Hill Valley 演示场景的情况。第一次加载它时,发送的请求如下。
收到 153 个项目,43.33 MB。但如果你同意让 babylonjs.com“在你的计算机上使用更多存储空间”,那么当你第二次加载同一场景时,你会看到以下内容。
1 个项目,348 字节!我们只是检查清单文件是否已更改。如果没有,我们将从数据库加载所有内容,我们节省了 43 MB 以上的带宽。
例如,这种方法正在《刺客信条:海盗》游戏中被使用。
让我们来思考一下。
- 一旦游戏加载过一次,游戏几乎立即启动,因为资产直接从本地数据库提供。
- 你的 Web 存储压力更小,使用的带宽也更少——成本更低!
现在这将同时满足你的用户和你的老板!
本文是 Microsoft Web 开发技术系列的一部分。我们很乐意与你分享 Microsoft Edge 和新的 EdgeHTML 渲染引擎。免费获取虚拟机或在你的 Mac、iOS、Android 或 Windows 设备上远程测试 @ http://dev.modern.ie/。