3. 对象存储跨云复制实战:AWS S3 → Azure Blob

跨云复制这件事,说白了就是让两个云厂商的存储桶“对齐”。我见过不少团队,数据在 S3 上跑得好好的,突然要迁一部分到 Azure Blob,原因嘛——要么是成本,要么是合规。今天我们就拿 AWS S3 到 Azure Blob 这条线,走一遍实战。

核心思路:用 S3 的复制功能触发事件,然后通过 Azure Blob SDK 把数据搬过去。中间不需要第三方工具,纯代码 + 云原生能力搞定。

3.1 整体架构

先看一张图,把流程理清楚。我个人习惯在动手前先画架构,不然代码写到一半容易跑偏。

S3 → Azure Blob 跨云复制架构 AWS S3 源桶 my-s3-source-bucket 对象上传 / 修改 触发 S3 事件通知 S3 事件通知 PutObject / CopyObject AWS Lambda 同步函数 接收事件 → 解析对象信息 调用 Azure Blob SDK Azure Blob 目标容器 my-blob-target-container 上传块 Blob 设置元数据 / 标签 注意事项 • S3 事件需配置目标 • Lambda 需 VPC 或公网 • 大文件建议分块上传

流程其实不复杂:S3 桶里一有对象上传或修改,事件通知就发给 Lambda。Lambda 拿到事件后,用 Azure Blob SDK 把数据写到 Azure 那边。嗯,这里要注意——Lambda 的执行环境需要能访问公网,或者通过 NAT 网关出去。

3.2 前置准备

动手之前,先把两边的东西准备好。我列个清单,你照着配就行。

平台 需要准备的内容 说明
AWS S3 源桶 + 事件通知配置 开启事件到 SQS / Lambda
AWS Lambda 函数 + IAM 角色 权限:S3 读取 + 日志写入
Azure 存储账户 + Blob 容器 记录连接字符串或 SAS
Azure 服务主体(可选) 用于托管身份认证

我的经验:Azure 的连接字符串里包含账户密钥,安全性一般。生产环境我建议用托管身份 + RBAC,别把密钥写死在代码里。

3.3 核心代码实现

代码部分,我用 Python 写 Lambda 函数。你想想看,Lambda 本身就是事件驱动的,跟 S3 通知是天生一对。

import boto3
import os
from azure.storage.blob import BlobServiceClient
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

s3 = boto3.client('s3')

# Azure 连接配置
AZURE_CONN_STRING = os.environ['AZURE_STORAGE_CONNECTION_STRING']
AZURE_CONTAINER = os.environ['AZURE_TARGET_CONTAINER']

def lambda_handler(event, context):
    # 解析 S3 事件记录
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        
        logger.info(f"收到同步请求: s3://{bucket}/{key}")
        
        # 从 S3 读取对象
        try:
            response = s3.get_object(Bucket=bucket, Key=key)
            data = response['Body'].read()
            metadata = response.get('Metadata', {})
        except Exception as e:
            logger.error(f"读取 S3 对象失败: {str(e)}")
            continue
        
        # 上传到 Azure Blob
        try:
            blob_service = BlobServiceClient.from_connection_string(AZURE_CONN_STRING)
            blob_client = blob_service.get_blob_client(
                container=AZURE_CONTAINER, 
                blob=key
            )
            
            # 上传并设置元数据
            blob_client.upload_blob(data, overwrite=True)
            blob_client.set_blob_metadata(metadata)
            
            logger.info(f"同步成功: {key} → Azure Blob")
        except Exception as e:
            logger.error(f"上传到 Azure 失败: {str(e)}")
            continue
    
    return {'statusCode': 200, 'body': '同步完成'}

这段代码我解释一下关键点:

  • 事件解析:S3 事件通知的格式是固定的,Record 数组里每个元素包含桶名和对象键。
  • 元数据传递:我把 S3 对象的自定义元数据原样带到 Azure Blob,这样两边保持一致。
  • 覆盖写入:overwrite=True 确保同名文件更新时不会报错。

我曾经踩过的坑:Lambda 默认超时是 3 秒,上传大文件时直接超时。记得把超时调到 5 分钟,内存也调到 512MB 以上。另外,S3 事件通知可能会重复发送,Lambda 要做幂等处理。

3.4 大文件分块上传

超过 100MB 的文件,直接 upload_blob 容易内存溢出。我建议用分块上传。说白了就是把文件切成小块,一块一块传,失败了还能重试。

from azure.storage.blob import BlobClient
import math

def upload_large_file(blob_client, file_stream, file_size):
    block_size = 4 * 1024 * 1024  # 4MB 一块
    block_count = math.ceil(file_size / block_size)
    block_ids = []
    
    for i in range(block_count):
        start = i * block_size
        end = min(start + block_size, file_size)
        data = file_stream.read(end - start)
        
        block_id = f"block-{i:06d}"
        blob_client.stage_block(block_id, data)
        block_ids.append(block_id)
        
        logger.info(f"分块 {i+1}/{block_count} 上传完成")
    
    # 提交块列表
    blob_client.commit_block_list(block_ids)
    logger.info("所有分块提交完成")

分块上传的好处很明显:

  • 内存占用可控,不会因为一个大文件把 Lambda 搞崩。
  • 网络中断后只重传失败的块,不用从头再来。
  • 可以并行上传多个块,速度更快。

3.5 增量同步与去重

全量同步一次就够了,后面都是增量。怎么判断文件变了?我一般用 ETag 或 LastModified 做对比。

对比方式 优点 缺点
ETag 精确,文件内容变了 ETag 就变 分块上传的 ETag 不是 MD5
LastModified 简单,直接比较时间戳 时间精度到秒,可能漏掉毫秒级变更
自定义元数据 灵活,可以记录同步版本 需要额外维护

我个人习惯用 LastModified 做第一道过滤,再配合 ETag 做精确校验。代码里可以这样写:

def should_sync(s3_obj, blob_client):
    """判断是否需要同步"""
    # 获取 S3 对象信息
    s3_last_modified = s3_obj['LastModified']
    s3_etag = s3_obj['ETag']
    
    try:
        # 获取 Azure Blob 属性
        blob_properties = blob_client.get_blob_properties()
        blob_last_modified = blob_properties.last_modified
        blob_etag = blob_properties.etag
        
        # 比较
        if s3_last_modified <= blob_last_modified and s3_etag == blob_etag:
            return False
    except Exception:
        # Blob 不存在,需要同步
        pass
    
    return True

一个小技巧:如果文件数量特别多,每次同步都去查 Azure Blob 属性会很慢。我建议在 S3 对象上打一个自定义标签,比如 synced-to-azure: true,同步前先检查标签,省掉一次网络请求。

3.6 错误处理与重试机制

跨云同步最怕网络抖动。Azure 那边偶尔会返回 503,S3 读取也可能超时。我的做法是加指数退避重试。

import time
import random

def retry_with_backoff(func, max_retries=3):
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait = (2 ** attempt) + random.uniform(0, 1)
            logger.warning(f"重试 {attempt+1}/{max_retries},等待 {wait:.2f} 秒")
            time.sleep(wait)

用法很简单:把上传操作包在 retry_with_backoff 里就行。第一次失败等 2 秒,第二次等 4 秒,第三次等 8 秒。三次都失败就记日志,人工介入。

3.7 成本优化要点

跨云复制不是免费的。我算过一笔账,给你参考:

成本项 AWS 侧 Azure 侧
S3 GET 请求 每 1000 次约 $0.0004 —
S3 数据传输出 每 GB 约 $0.09(到互联网) —
Lambda 执行 每 100 万次约 $0.20 —
Azure Blob 写入 — 每 10000 次约 $0.05
Azure 存储 — 每 GB/月约 $0.018

你看,大头其实是 S3 的数据传出费。如果每天同步几百 GB,一个月下来几千美金就没了。怎么省?

  • 只同步增量:别全量跑,用上面说的去重逻辑。
  • 压缩再传:文本类文件先 gzip,能省 60%-80% 流量。
  • 选对区域:如果 AWS 和 Azure 都有香港区域,走内网对等连接,传出费几乎为零。

一句话总结:S3 → Azure Blob 的跨云复制,核心是事件驱动 + SDK 上传。代码量不大,但网络成本、错误重试、增量同步这些细节,决定了方案能不能在生产环境跑稳。