解读 Windows Azure 存储服务的账单 – 带宽、事务数量,以及容量
经常有人询问我们,如何估算 Windows Azure 存储服务的成本,以便了解如何更好地构建一个经济有效的应用程序。本文我们将从带宽、事务数量,以及容量这三种存储成本的角度探讨这一问题。
在使用 Windows Azure Blob、表,以及队列时,存储成本是由下列因素决定的:
- 带宽– 从承载存储帐户的位置传入和传出的数据总量
- 事务– 针对您的存储帐户所执行的请求数量
- 存储容量 – 持久存储的数据总容量
请注意,随着存储系统增加新的功能,本文所涉及内容可能会有变化。 本文将作为指导原则,使服务能够在应用程序运行于生产环境之前估算其存储带宽、事务和容量使用情况。
下文将概括介绍账单中的这三项内容:
- 带宽 – 因为应用程序的运算过程需要用到已存储的数据,因此我们可以将托管服务与相应的存储放置在同一个位置。这样即可在同一位置的计算和存储服务之间提供免费带宽,而只需要为访问当前位置之外的存储服务所产生的带宽付费。
- 事务 – 每一个针对存储服务产生的 Blob、表,以及队列 REST 请求都会被视作可计费的事务。因此为了控制事务成本,应用程序可以控制发送至存储服务的请求的频率和数量。我们会分析接获的每一个请求,随后,我们会根据这些请求的处理情况,以及请求的来源,确定是否需要对该请求计费。
- 容量 – 为了统计需要计费的存储容量,我们会将存储的对象(Blob、实体,以及消息),以及相关应用程序和系统元数据的总容量进行累加。
下文中,我们将向您介绍如何解读您的应用程序所产生的这三项指标。
带宽何时会被计算在内
为了访问 Blob、表,以及队列,首先您需要访问 Windows Azure 开发者门户,并创建存储帐户。在创建存储帐户时,您可以选择您的存储帐户保存的位置。目前我们为您提供下列六个位置:
- 美国中北部
- 美国中南部
- 欧洲北部
- 欧洲西部
- 亚洲东部
- 亚洲东南部
存储帐户内所有数据的存储和访问都要通过创建时所选择的位置进行。为了尽量降低客户访问时遇到的延迟,一些应用程序会尽可能选择距离主要客户最近的位置。这里一个最重要的问题在于,对需要访问该存储帐户的托管服务,您也需要在开发者门户中选择与存储帐户相同的区域。这是因为同一个位置内部的数据传输带宽是免费的。相反,如果要从不同于存储帐户位置的其他位置传入或传出数据,将需要根据本文开头处列出的费率收取带宽费用。
另外还要注意,同一位置内“访问”操作产生的带宽是免费的,但是所产生的事务并不免费。对存储系统的每次访问都会被视作一个需要计费的事务。此外,只有被视作可计费事务所产生的带宽才需要收费,关于可计费事务的详情请参阅下文。
如果您在 Windows Azure 内容传送网络(CDN)中使用了 Blob,那么关于带宽还有一个需要注意的问题。如果 CDN 节点中没找到所需 Blob(或者该 Blob 的驻留时间(TTL)已过期),而需要从(来源)存储帐户重新读取并将其缓存起来,在发生这种情况时,缓存 Blob (将 Blob 从来源位置传输至 CDN 节点)所产生的带宽(以及所产生的一笔事务)需要向存储帐户收费。因此建议您对于 CDN 所用的 Blob,要设法让缓存在由于 TTL 而过期之前,尽量提高缓存的命中率,以便尽可能抵消将 Blob 从您的存储帐户传输至 CDN 额外花费的时间和成本。
这里有几个例子:
- 您的存储帐户和托管服务都位于“美国中北部”。托管服务从存储帐户中访问数据产生的全部带宽都是免费的,因为它们位于同一个位置。
- 您的存储帐户位于“美国中北部”,您的托管服务位于“美国中南部”。托管服务从存储帐户中访问数据产生的全部带宽都需要按照本文开头处列出的费率收费。
- 您的存储帐户位于“美国中北部”,您的 Blob 被 Windows Azure CDN 位于欧洲的边缘位置所缓存并提供访问。因为 Windows Azure CDN 边缘位置与存储帐户不在同一个位置,因此 Windows Azure CDN 读取您的存储帐户并缓存 Blob 时,将产生上文提到的带宽费用。
- 您的存储帐户位于“美国中北部”,但被位于全球的网站和服务所访问。由于这些访问并非同一位置的 Windows Azure 托管服务产生的,因此需要收取标准带宽费用。
事务数量是如何统计的
关于事务数量,首先需要明确对于 Windows Azure 存储来说,什么才算是 1 笔事务。针对 Windows Azure Blob、表,以及队列所进行的每个 REST 调用都会被视作 1 笔事务(但这笔事务是否需要计费,取决于上文提到的记账分类方式)。REST 调用的详情请参阅:
上述每种 REST 调用每一次操作都会被算作 1 笔事务。其中可能包含下列类型的请求:
- 查询 / 列出请求和持续符 – 表查询,以及 Blob 容器、表,以及队列的列出操作可以返回持续符。这意味着查询/列出操作在真正完成之前必须持续执行。正如上文所述,对存储服务帐户的每个 REST 请求都会被视作 1 笔事务。因此查询/列出操作的每次持续都会额外产生 1 笔事务,因为每次持续操作都是对存储服务执行的另一个 REST 请求。
- 批处理操作– 目前我们支持两种类型的批处理操作:
- Get Messages – 通过队列,一次最多可以获得 32 条消息的能力。
- Entity Group Transactions – 通过对 Azure 表任意搭配使用 Insert、Update,或 Delete 操作,最多对 100 个实体执行原子事务的能力。该操作要求所有目标实体必须位于同一个表中,具备相同的 PartitionKey 值,并且请求的总尺寸不能超过 4Mbyte。
上述批处理操作都会对存储服务执行一个 REST 请求。因此每次操作可视作 1 笔事务。
在使用 Storage Client Library 时,少数函数的调用可能会对您的存储帐户执行多个 REST 请求。
- Uploading Blobs– 在上传容量大于 32Mbyte 的 Blob 时,存储客户端库默认会将其拆分为多个 4Mbyte 的块。通过设置CloudBlobClient.WriteBlockSizeInBytes字段可更改块的大小。在上传大于 32MB 的 Blob 时,客户端库会使用单独的 PutBlock REST 请求上传每个块,并在全部上传完毕后使用一个 PutBlockList 将所有块合并在一起。每个 PutBlock 都算作 1 笔事务,最后执行的 PutBlockList 也算作 1 笔事务。
- Table Queries – 当您使用 CloudTableQuery 查询时,该操作会自行处理持续符,这样即可使用上一个查询请求获得的持续符重新发起查询,并获得剩余的实体。如上文所述,对服务重新发起的每个持续符查询都会视作 1 笔事务。
- Table SaveChanges – 对于表服务,当您执行 AddObject、UpdateObject 或 DeleteObject 操作时,获得的实体会被加入数据上下文,这样当程序执行 SaveChangesWithRetries 操作时,后续执行的一些改动就可以写入到您的存储帐户中。在执行 SaveChangesWithRetries 时,所有挂起的改动会逐个使用自己的 REST 调用合并到表服务。因此每个挂起的改动都会被视作 1 笔事务。
这种情况有一个例外:Entity Group Transaction。如果您对具备相同 PartitionKey 值的一系列实体安排了批处理操作(AddObject、UpdateObject、DeleteObject),则只需要执行一个 SaveChangesWithRetries(SaveChangesOptions.Batch),随后即可使用一个 REST 请求将所有改动写入表服务,此时将只产生 1 笔事务。这里的关键在于确保将 SaveChangesOptions.Batch 选项传递至 SaveChangesWithRetries 方法。我们发现很多服务忘了使用 SaveChangesOptions.Batch,导致每个挂起的改动都通过单独的请求发送,进而客户会质疑为什么 SaveChanges 操作所需的时间远远超过预期,为什么该操作无法作为 1 笔事务以原子的方式执行(意味着所有改动或者全部提交,或者全部没能提交),以及为什么事务数量也远远超出预期。
这里有几个例子:
- 对 Blob 服务执行的 1 个 GetBlob 请求 = 1 笔事务
- 对 Blob 服务执行 1 个 PutBlob 操作 = 1 笔事务
- 上传大容量 Blob,通过 PutBlock 产生 100 个请求,最后使用 1 个 PutBlockList 进行提交 = 101 笔事务
- 总共使用 5 个请求(因为有 4 个持续符)列出大量 Blob 的内容 = 5 笔事务
- 表单一实体 AddObject 请求 = 1 笔事务
- 针对 100 个实体执行表 Save Changes(不使用 SaveChangesOptions.Batch) = 100 笔事务
- 针对 100 个实体执行表 Save Changes(使用 SaveChangesOptions.Batch) = 1 笔事务
- 指定精确匹配 PartitionKey 和 RowKey 的表查询(返回 1 个实体) = 1 笔事务
- 表查询执行一个存储请求,返回 500 个实体(没有遇到持续符) = 1 笔事务
- 表查询对表存储产生 5 个请求(由于有 4 个持续符) = 5 笔事务
- 队列存储消息 = 1 笔事务
- 队列获得 1 条消息 = 1 笔事务
- 队列通过空队列获得消息 = 1 笔事务
- 队列批处理获得 32 条消息 = 1 笔事务
- 队列删除消息 = 1 笔事务
至此我们已经了解事务到底是什么,随后看看哪些事务需要计费,哪些不需要计费。
当我们的服务接获一笔事务后,如果该事务符合下列任何一种类别,我们 不会 将其视作可计费事务,这些事务也不会产生带宽费用:
- 身份验证前失败 – 如果我们没有机会对该事务进行身份验证,那么该事务就不会被算作可计费事务。例如因为 http 头错误、URL 格式错误,请求的时间范围无效等原因导致的错误请求。
- 身份验证失败 – 如果事务身份验证失败,我们不会将其视作存储帐户的可计费事务。
- 配额权限失败 – 我们的每个存储帐户有 100TB 的配额。如果某一存储帐户已达到该上限,那么我们会将该存储帐户置于一种只提供“读取+删除”权限,不提供“写入”权限的模式下。此时该存储帐户可以执行 Get 和 Delete 操作,但无法执行 put/post 操作。处于这种模式后,包含写权限的请求将会失败,此时这样的事务不会被视作可计费事务。
- 共享访问签名( SAS )有误的 HTTP 动作 / 权限– 如果发送的签名验证无误(身份验证),但 REST 操作的关键字(PUT、POST、GET、DELETE)与权限不匹配,这样的请求将不会被视作可计费事务。例如,如果权限指定只能执行 PUT 操作,但请求中提供了有效的 SAS 并使用了 GET 动作,该请求将失败,不会被视作可计费事务。
- 匿名请求失败– 如果收获的请求不包含签名,将被视作匿名请求,此时我们会将下列三种类型的错误事务视作非可计费事务:
- 权限错误– 匿名请求只能执行 GET 请求。如果不是 GET 请求,则会被拒绝,同时不会被视作可计费事务。
- 容器未找到– 如果匿名 GET 请求的容器不存在,则不会被视作可计费事务。
- Blob 未找到– 如果匿名 GET 请求试图访问的 Blob 不存在,则不会被视作可计费事务。
- 非预期超时 – 如果请求因存储系统所存在的问题而超时,则不会被视作可计费事务。
如遇上述任何一种情况,相应的事务不会被视作可计费事务,该请求也不产生带宽费用。其他所有事务都会被视作可计费事务,但是否产生带宽费用则需按照“带宽”一节的介绍来确定。
我们会将可计费事务划分为下列类别:
- 成功的事务 – 针对存储系统成功执行的任何事务。
- 预期内错误– 预期内错误主要有下列三个来源:
- 事务错误– 是指正确通过身份验证,具备正确权限,但由于事务针对存储帐户中的数据所用的语义有误而执行失败的请求。例如:
- 试图获取或删除已被删除,或未曾存在过的对象而导致的 NotFound 错误。
- 试图创建已经存在的对象而导致的 AlreadyExists 错误。例如,我们遇到过有应用程序在将消息放入队列之前,针对该队列执行 CreateIfNotExist。这会导致希望入列(Enqueue)的每个消息会针对存储系统产生两个单独的请求,进而导致队列创建失败。为避免产生额外的事务成本,请确保您只在生命周期开始的时候创建需要的 Blob 容器、表,以及队列。
- 使用 ETag Match、Non-Match、Modified-Since,或 Unmodified-Since 执行条件运算,但运算失败(返回 NotModified 或 PreconditionFailed),这样的操作也会被视作事务。
- 试图添加已经存在的表实体会导致出错(Conflict – 409)。同理,试图更新一个不存在的实体也会出错(Not Found – 404)。这些操作都会产生事务成本。而出现这种错误实际上是因为应用程序试图执行 Upsert 操作,目前我们正在研究如何为 Windows Azure 表提供这样的支持。在实现 Upsert 操作前,用户需要仔细检查自己的使用场景,以确定为了将预期内失败数量,以及 Windows Azure 表事务总数量降至最低,是否需要在 UPDATE 之前执行 INSERT,或在 INSERT 之前执行 UPDATE。
- 有效的共享访问签名(SAS)造成的 ContainerNotFound 或 BlobNotFound。如果 SAS 签名验证无误,但容器或试图访问的 Blob 未找到,这些操作也会被视作可计费事务。
- 还有其他诸如此类的情况。同类的情况还有很多,因为取决于存储服务所提供的语义(例如条件运算、租约、序列号等),请求可能因为多种方式遇到预期内错误。
- 限制 – 是指事务速率超出 Windows Azure 存储抽象和可伸缩目标一文中描述的每个分区目标吞吐量而被限制的请求。这些被限制的请求会被视作可计费事务。在遇到这种情况后,客户端需要使用指数退避算法(Exponential backoff)并重试请求,默认情况下存储客户端库会提供此选项。如果服务频繁遇到这样的问题,那么可能需要对服务的数据结构进行额外的分区,具体方法请参阅即将发布的Blob、表,和队列一文。
- 预期超时 – 当您向服务发出请求时,您可以指定自己的超时值,并且可以将其设置为小于 SLA 超时。如果请求的超时值小于 SLA 超时,在请求超时时,将会被算作预期超时,并被视作可计费事务。
- 事务错误– 是指正确通过身份验证,具备正确权限,但由于事务针对存储帐户中的数据所用的语义有误而执行失败的请求。例如:
如何估算容量
客户要求了解有关 Blob、表和队列存储成本的更多详细信息,以便在运行于生产环境之前估算他们的应用程序所使用的存储容量大小。
首先需要明确存储容量的每月计费累计方式。存储容量的计算和汇总至少会每天进行一次,随后用每个月的平均值算出每 GB/月的费用。例如,如果您在上半月使用了 10GB,下半月使用了 0GB,那么当月的费用将以 5GB 容量来计算。
下面将介绍如何计算 Blob、表和队列的存储容量。
其中“Len(X)”代表字符串中的字符数。
Blob 容器 – 估算每个 Blob 容器消耗的存储容量,方法如下:
48 字节 + Len( 容器名 ) * 2 字节 +
对于每个元数据 [3 字节 + Len( 元数据名 ) + Len( 值 )] +
对于每个带签名的标识符 [512 字节 ]
详细分析如下:
- 每个容器首先需要用 48 字节保存 Last Modified Time、Permissions、Public Settings,以及一些系统元数据。
- 容器名以 Unicode 格式存储,因此容量按字符总数并乘以 2计算。
- 对于每个 Blob 容器存储的元数据,我们会(以 ASCII 格式)存储名称的长度,外加字符串值的长度。
- 每个带签名标识符占用的 512 字节包含带签名标识符的名称、开始时间、过期时间,以及权限。
Blob – 估算每个 Blob 消耗的存储容量,方法如下:
对于 Block Blob (基准Blob** 或快照),需要: 124 字节 + Len(Blob名称 ) * 2 字节 +
对于每个元数据 [3 字节 + Len( 元数据名称 ) + Len( 值 )] +
8 字节 + 已提交和未提交块的数量 * 块 ID 大小的字节数 +
大小的字节数 ( 唯一已提交数据块中存储的数据 ) +
大小的字节数 ( 未提交数据块中的数据 )
对于 Page Blob (基准Blob** 或快照),需要: 124 字节 + Len(Blob名称 ) * 2 字节 +
对于每个元数据 [3 字节 + Len( 元数据名称 ) + Len( 值 )] +
包含数据的不相邻页面范围数量 * 12 字节 +
大小的字节数 ( 唯一页面中存储的数据 )
详细分析如下:
- 每个 Blob 首先需要用 124 字节保存 Last Modified Time、Size、Cache-Control、Content-Type、Content-Language、Content-Encoding、Content-MD5、Permissions、快照信息、租约,以及一些系统元数据。
- Blob 名称以 Unicode 格式存储,因此容量按字符总数并乘以 2计算。
- 对于存储的每个元数据,名称的长度(以 ASCII 格式存储),外加字符串值的长度。
- 对于 Block Blob
- 块列表需要 8 字节
- 块的数量乘以块 ID 大小的字节数
- 外加所有已提交和未提交块中所存储数据的大小。请注意,如果使用了快照,那么这个大小将只包含基准或快照 Blob 中具备唯一性的数据。如果未提交的块不再使用的时间超过一周,将会被垃圾回收,随后将不再被计费。
- 对于 Page Blob
- 字节数按具有数据的不连续页面范围数乘以 12计算。这也是当您调用 GetPageRanges API 时看到的具备唯一性的页面范围数量。
- 外加所有已存储页面中所包含数据大小的字节数。请注意,如果使用了快照,这个大小将只包含被统计的基准 Blob 或快照 Blob 中所含的,具备唯一性的页面。
表 – 估算每个表消耗的存储容量,方法如下:
12 字节 + Len( 表名称 ) * 2 字节
详细分析如下:
- 每个表首先需要用 12 字节保存 Last Modified Time 和一些系统元数据。
- 表名称以 Unicode 格式存储,因此容量按字符总数并乘以 2计算。
实体 – 估算每个实体消耗的存储容量,方法如下:
4 字节 + Len (PartitionKey + RowKey) * 2 字节 +
对于每个属性 (8 字节 + Len( 属性名称 ) * 2 字节 + Sizeof(.Net 属性类型 ))
详细分析如下:
- 每个实体首先需要用 4 字节保存 Timestamp,以及一些系统元数据。
- PartitionKey 和 RowKey 值中的字符数量,字符以 Unicode 格式存储(因此要乘以 2)。
- 随后对于每个属性,开头处需要 8 字节,外加属性的名称 * 2 字节,再加下文列表中列出的属性类型的大小。
不同类型的 Sizeof(.Net 属性类型) 为:
- String – 字符数 * 2 字节 + 字符串长度占用的 4 字节
- DateTime – 8 字节
- GUID – 16 字节
- Double – 8 字节
- Int – 4 字节
- INT64 – 8 字节
- Bool – 1 字节
- Binary – sizeof(值)的字节数 + 二进制数组长度占用的 4 字节
队列 – 估算每个队列消耗的存储容量,方法如下:
24 字节 + Len( 队列名称 ) * 2 +
每个元数据 (4 字节 + Len( 队列名称 ) * 2 字节 + Len( 值 ) * 2 字节 )
详细分析如下:
- 每个队列首先需要用 24 字节保存 Last Modified Time 以及一些系统元数据。
- 随后对于所存储的每个队列元数据,名称的长度和值都以 Unicode 格式保存,因此需要乘以 2。
消息 – 估算每个消息消耗的存储容量,方法如下:
12 字节 + Len( 消息 )
详细分析如下:
- 每个消息首先需要用 12 字节保存 Invisibility Time、Creation Time、Dequeue Count、Message ID,以及一些系统元数据。
- 随后如果您使用了 REST 接口,需要统计消息内容占用的字节数。如果您使用了存储客户端库,则会将消息存储为 UTF8(Base64(消息)) 格式,此时所用存储容量等同于该格式的总长度。
Brad Calder