背景
最近遇到一个运维的任务,因为安全和隐私的法规要求,原来的静态资源数据(图片、附件等)要求迁移到对应地区的存储中(海外)。目前已有静态资源约2亿个文件,总大小20TB,每日增长在15万左右。根据估算,如果直接进行跨区域在线复制,大约需要1个月左右的时间才能完成复制。而这个过程中又会不断写入新的数据。
方案
考虑到迁移过程中不能停机中止业务进行,所以想到了一个是否可以建立一个机制,当用户请求静态资源时,先尝试从新的存储中读取。如果新的存储中找不到对应的资源,那么就尝试从旧的存储中读取。由于一些原因,目前没有办法考虑应用的改造。之前有另外一个想法,新的静态资源存储在一个新目录下,然后通过7层的负载均衡器来判断应该访问哪个存储,结果受限于实际情况只能采用尝试的办法。
具体步骤考虑如下:
- 创建一个新的CDN服务,用来加速静态资源的访问。原有的CDN服务因为区域问题安排下线
- 使用 Azure API Management 服务来实现重试。当CDN回源时,APIM会先请求后端 Azure Blob 存储(海外),如果 Blob 中不存在,则重新尝试从原有国内服务器获取
- 更新 DNS 解析到新的 CDN 服务,这个时候由于Blob 中没有文件,实际所有请求都是发到了国内存储中
- 修改应用配置,将新文件写入海外 Blob 中。这时候如果用户请求新的文件,APIM 会从 Blob存储中获取资源
- 利用 azcopy 工具复制国内存储中的静态资源到海外Azure Blob中,直到完成复制
- (可选)修改 CDN 的源,直接从Azure Blob中获取静态资源,删除 APIM 配置
思考点:
- 为什么使用APIM?原因很简单,不需要开发代码,并且可以复用现有的
- APIM尝试访问静态资源时,因为考虑到老的图片的远高于新图片的数量,所以先尝试访问 国内存储(source)
- 应用无法修改的情况下,增加了一个中间件用于适配不同的协议
- 因为CDN的存在,所以理论上APIM可以不用再做缓存
- 考虑优先迁移最近被访问到的数据,这样可以减少重试次数
POC验证
从目前来看,关键点是 APIM 如何实现重试。
1. 创建2个 Azure Blob ,其中分别代表国内存储和海外存储,另外新建一个APIM。
2. 打开APIM,新增2个Backends
3. 在 APIM 中新增一个API,用于请求静态资源
4. 设置 API 的Policies
<policies>
<inbound>
<base />
<set-backend-service backend-id="blob-source" />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 404)" count="2" interval="1" first-fast-retry="true">
<choose>
<when condition="@(context.Response.StatusCode == 404)">
<set-backend-service backend-id="blob-target" />
</when>
</choose>
<forward-request />
</retry>
</backend>
<outbound>
<base />
</outbound>
</policies>
这里解释一下,首先在 inbound 里设置了 backend-id 为源存储服务,这个发送到这个API的请求会被转发到刚才 Backends 添加的对应服务器地址中。
然后在 backend 配置中增加了一个重试(retry),条件是当 backend 返回的 response.StatusCode 为404的时候,立刻(first-fast-retry)进行重试。并且在重试的时候我们重新设置 backend-id 为新的存储服务。
这里用了一个choose,我一开始理解是不需要的,但实际上如果不加会无法实现我们的效果。
5. 在 source 和 target 存储中分别创建 images 容器,然后分别上传 2张不同的图片(名称也不同)。然后如果我从source访问只存在于target存储中的文件,就会出现404错误。
6. 尝试从 APIM 访问图片,无论输入哪个图片名称都可以访问成功。使用跟踪看了一下日志,当第一个服务器返回404后,请求了第二个服务器。
7. 当访问一个真实不存在的图片时,会反馈一个404页面,这个不想看到,所以增Policies,在outbound里,当最终404时,不要再返回下面这个error的具体信息,而是set-body为空。
<policies>
<inbound>
<base />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 404)" count="2" interval="1" first-fast-retry="true">
<choose>
<when condition="@(context.Response.StatusCode == 404)">
<set-backend-service backend-id="blob-target" />
</when>
</choose>
<forward-request />
</retry>
</backend>
<outbound>
<base />
<choose>
<when condition="@(context.Response.StatusCode == 404)">
<return-response>
<set-status code="404" reason="NotFound" />
<set-body />
</return-response>
</when>
</choose>
</outbound>
<on-error>
<base />
</on-error>
</policies>
8. 添加一个 CDN 服务,配置一个 Endpoint,origin type 为 API Management
9. 测试一下从CDN的域名访问两张不同的图片,没有问题,可以正常访问。测试图片来源于互联网。
10. 我们在 azcopy 的基础上优先迁移最近被访问的图片,所以增加了一个 eventhubs ,当用户访问的图片不在 source 中,我们就发送一个消息到队列中。然后通过其他一些服务(比如 logic app)订阅消息,实现异步迁移。
11. 给APIM增加一个记录器,目前居然没有UI可以操作,只能通过调用API的方式进行。(文章参考),首先准备请求的 json 文件, 配置 eventhubs 的 connectionString
{
"properties": {
"loggerType": "azureEventHub",
"description": "adding a new logger",
"credentials": {
"name": "apim",
"connectionString": "Endpoint=sb://static-resources-migration-task.servicebus.windows.net/;SharedAccessKeyName=apim;SharedAccessKey=xxxx;EntityPath=migration-task"
}
}
}
12. 创建logger
$subscriptionId="xxxx"
$resourceGroupName="static_resources_migration"
$serviceName="static0resources0controller"
$loggerId="migrationtask"
$API_URI="https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}/loggers/${loggerId}?api-version=2021-08-01"
$ACCESS_TOKEN=(az account get-access-token --resource=https://management.azure.com/ --query accessToken -o tsv)
$beareruthValue = "Bearer $ACCESS_TOKEN"
$body = [IO.File]::ReadAllBytes("ApiManagementCreateEHLogger.json")
$Headers = @{
Authorization = $beareruthValue
}
Invoke-RestMethod -Method PUT -Uri $API_URI -ContentType application/json -Headers $Headers -Body $body
13. 执行完成,得到返回结果
14. 增加发送到 eventhubs 的 Policies ,只有访问 source 没有,并返回404 以后才记录到 event hubs 中。这里不考虑重复发送的问题,订阅此事件的应用来做判断就好了。
<policies>
<inbound>
<base />
</inbound>
<backend>
<retry condition="@(context.Response.StatusCode == 404)" count="2" interval="1" first-fast-retry="true">
<choose>
<when condition="@(context.Response.StatusCode == 404)">
<set-backend-service backend-id="blob-target" />
<log-to-eventhub logger-id="migrationtask">@{
return new JObject(
new JProperty("EventTime", DateTime.UtcNow.ToString()),
new JProperty("ServiceName", context.Deployment.ServiceName),
new JProperty("RequestId", context.RequestId),
new JProperty("RequestPath", context.Request.Url.Path),
new JProperty("OperationName", context.Operation.Name)
).ToString();
}</log-to-eventhub>
</when>
</choose>
<forward-request />
</retry>
</backend>
<outbound>
<base />
<choose>
<when condition="@(context.Response.StatusCode == 404)">
<return-response>
<set-status code="404" reason="NotFound" />
<set-body />
</return-response>
</when>
</choose>
</outbound>
<on-error>
<base />
</on-error>
</policies>
15. 利用 Service Bus Exporer 查看发送到 Eventhubs 的记录。