一、问题场景:数据库突然“堵车”了
假设你维护着一个在线商城的订单服务,每天用户下单、查订单、看推荐,一切都很流畅。突然某天中午高峰,你发现大量请求超时,应用日志里频频出现“429 Too Many Requests”,用户开始抱怨“订单页面转圈圈”。你赶紧登录 Azure 门户,看到 Cosmos DB 的“请求单位”使用率已经顶到 100%,数据进出被限流了。这就是典型的“请求单元不足”引发的限流。
简单来说,Cosmos DB 就像一个按次收费的健身房,每次你使用器械(读取、写入、查询)都要消耗一定的“次数(请求单元)”。当你买的基本次数用完,健身房保安就不再让你进去了。这时候你必须学会怎么合理分配这些次数,也就是容量规划。而容量规划的第一步,通常不是急着去按“加钱”按钮,而是回头看看你的分区键设计是不是合理。
二、理解请求单元:数据库的“体力值”
Cosmos DB 里每次操作都要消耗“请求单元”,英文叫 Request Unit,简称 RU。你可以把它想象成游戏里的体力值:读一条 1KB 的数据,大概消耗 1 个 RU;写一条 1KB 的数据,大概消耗 5 到 10 个 RU;复杂查询可能要几十甚至上百个 RU。你不能说“我加带宽”或者“我加 CPU”,因为在这里唯一的硬通货就是 RU。
这个设计的好处是性能可预期,坏处是你得为“峰值”买单。如果你的限额是每秒 1000 RU,而某 3 秒内突然需要 2000 RU,那这 3 秒内的多余请求就会被拒之门外,直接返回 429 错误。很像打车高峰时,平台临时加价,你不加钱就坐不上车。
要解决这个问题,底层逻辑只有一个:要么让每个操作少吃点,要么把“食堂”的窗口开多点,也就是扩容。但在动手扩容之前,我们更需要关注一个问题——为什么有时候扩容了,依然限流?答案往往藏在分区键设计里。
三、分区键设计:决定负载分散的关键
3.1 什么是分区键
Cosmos DB 会把数据按“分区键”拆开存放。比如订单表用“用户ID”做分区键,那么同一个用户的所有订单就会挨在一起,而不同用户的订单则被分配到不同的物理分区上。物理分区像一个个小仓库,每个小仓库都有自己独立的 RU 配额。
3.2 分区键选不好,扩容也白搭
如果你选了“下单日期”作为分区键,比如 2025-03-27,那么今天所有订单全部塞进同一个分区。这一个分区再厉害,也只有那点 RU,一旦请求量上去,其他分区的 RU 闲着,也没法借给它用。这就是“热分区”。结果就是:你总 RU 明明升到了 100000,但今天这个分区还是只能承受 1000 RU,照样限流。
好的分区键,要满足两个条件:一是“值足够多”,二是“分布够均匀”。最常见的做法是选“用户 ID”或者“设备 ID”这类高基数字段。
3.3 用 Python 示例对比分区键选择
下面我们统一使用 Python 技术栈来演示。先看看如何定义一个分区键路径。假设我们要设计一个订单容器,用用户 ID 作为分区键:
# Python 3.10+
# 使用官方 azure-cosmos 库创建容器
from azure.cosmos import CosmosClient, PartitionKey, exceptions
# 连接你的 Cosmos DB 账号
endpoint = "https://your-account.documents.azure.com:443/"
key = "your-master-key"
client = CosmosClient(endpoint, key)
# 选择数据库(不存在则创建)
database = client.create_database_if_not_exists(id="shop_db")
# 定义容器配置
container_name = "orders"
# 关键点:partition_key_path 设为 "/userId"
# 这样可以保证每个用户的订单都在同一个逻辑分区,同时不同用户分散到不同物理分区
container = database.create_container_if_not_exists(
id=container_name,
partition_key=PartitionKey(path="/userId", kind="Hash"),
offer_throughput=4000 # 初始手动每秒 4000 RU
)
print(f"容器 {container_name} 已创建,分区键为 /userId")
你看,分区键一旦定下来,后期很难改。如果一开始没想清楚,后面极大概率要重建容器、迁移数据。所以我们总说,分区键是容量规划的“地基”,地基歪了,楼上怎么装修都漏水。
再来看一个“错误示例”的感受:如果使用日期作为分区键,代码虽然看起来没问题,但每天的请求都会集中在当天分区上,如同几百辆货车同时挤进一条小巷子。这里就不再写重复代码了,你只需要把 partition_key_path 换成 "/date",剩下的逻辑一样,可热区问题就冒出来了。
四、容量规划:让每分钱都花在刀刃上
4.1 手动扩容的理论基础
既然 RU 这么重要,我们怎么决定需要多少 RU?先做个思维实验:假设你的系统平均每秒有 100 个请求,每个请求平均消耗 15 RU,那么你至少需要 1500 RU。但别忘了峰值,可能是平峰的三倍,那你就要预留 4500 RU 左右。
手动扩容就是你提前设定一个固定值,比如 10000 RU。不管实际用不用,都按这个值计费。优点是简单、可控,适合流量平稳的应用;缺点是浪费,因为深夜可能只有 100 RU 的负载,你却依然付着 10000 RU 的钱。
4.2 用 Python 做手动扩容操作
在 Azure 上手动扩容可以通过门户、CLI 或 Python SDK 完成。为了保持示例统一,这里展示使用 Python 的管理 SDK 来修改容器的吞吐量。注意:这是管理操作,需要另装 azure-mgmt-cosmosdb。
# Python 3.10+
# 使用 Azure 管理 SDK 手动调整吞吐量
from azure.identity import DefaultAzureCredential
from azure.mgmt.cosmosdb import CosmosDBManagementClient
# 需要你的订阅 ID 和资源组名
subscription_id = "your-subscription-id"
resource_group_name = "my-resource-group"
account_name = "my-cosmos-account"
database_name = "shop_db"
container_name = "orders"
# 使用 Azure 默认凭证(需要先 az login)
credential = DefaultAzureCredential()
client = CosmosDBManagementClient(credential, subscription_id)
# 设置新的吞吐量,这里手动设为 10000 RU/秒
new_throughput = 10000
# 调用更新接口
# 注意:这个操作可能耗时几分钟
client.sql_resources.begin_update_sql_container_throughput(
resource_group_name,
account_name,
database_name,
container_name,
{
"resource": {
"throughput": new_throughput
}
}
)
print(f"已提交手动扩容请求,目标吞吐量:{new_throughput} RU/秒")
扩容不是瞬时的,可能需要几十秒到几分钟。这期间如果还在被限流,就得靠客户端重试来扛过“青黄不接”的关口。
4.3 一个完整的 RU 预估工具函数
手动扩容前,你得先算清楚需要多少 RU。下面这个函数帮助你根据请求量和平均消耗来估算:
# Python 3.10+
# 一个朴素的 RU 估算工具
def estimate_ru(avg_requests_per_second, avg_ru_per_request, peak_factor=3):
"""
估算所需 RU 数。
参数:
avg_requests_per_second: 平峰时期每秒请求数
avg_ru_per_request: 每个请求平均消耗 RU
peak_factor: 高峰流量相对平峰的倍数,默认 3
返回:
建议的每秒 RU 值
"""
baseline_ru = avg_requests_per_second * avg_ru_per_request
peak_ru = baseline_ru * peak_factor
# 一般还要留出 20% 的缓冲,避免突刺
recommended_ru = int(peak_ru * 1.2)
return recommended_ru
# 假设平峰 500 个请求/秒,每个请求平均 12 RU
need_ru = estimate_ru(500, 12)
print(f"建议配置吞吐量:{need_ru} RU/秒")
你可以在上线前用测试工具模拟压测,把实际平均 RU 填进函数里,再考虑要不要加缓冲。代码看起来平常,但它是容量规划中非常实用的一把尺子。
4.4 手动扩容与自动扩容的取舍
Cosmos DB 还支持“自动缩放”模式,它会根据负载自动增加 RU,不过设置的是“RU 上限”。这里不展开太多,只提一句:如果你怕手动扩容不够灵敏,可以考虑自动缩放,但自动缩放有最小计费下限,而且峰值响应可能仍有延迟。手动扩容更适合你能预知流量峰值的场景,比如“双 11”或“春节抢票”。因为你明确知道那天下午 2 点会有洪峰,那么提前半小时把 RU 手动拉高,等峰过了再降下来,省钱又靠谱。
五、限流之后的应对策略
5.1 429 错误是什么意思
429 就是“请求太多,服务让我歇会儿”。在 Cosmos DB 的 SDK 里,它通常以 CosmosHttpResponseError 形式出现,状态码为 429。这时候不要慌,更不要盲目重试,因为重试反而加重拥堵。
5.2 客户端重试与退避策略
Cosmos DB SDK 内置了自动重试机制,默认会等一段时间再重试。你可以自己控制最大重试次数和等待时间。用 Python 官方 SDK 可以这样配置:
# Python 3.10+
# 配置 Cosmos DB 客户端重试策略
from azure.cosmos import CosmosClient, RetryPolicy
# 自定义重试策略
retry_policy = RetryPolicy(
max_retry_attempts=5, # 最多重试 5 次
fixed_retry_interval_ms=500 # 每次固定等 0.5 秒(也可以改为指数退避)
)
# 创建客户端时注入重试策略
client = CosmosClient(
url="https://your-account.documents.azure.com:443/",
credential="your-master-key",
retry_policy=retry_policy
)
# 之后正常查询即可
db = client.get_database_client("shop_db")
container = db.get_container_client("orders")
try:
# 这条查询如果碰到限流,会自动按策略重试
result = container.query_items(
query="SELECT * FROM orders o WHERE o.userId = @uid",
parameters=[{"name": "@uid", "value": "user-123"}],
partition_key="user-123"
)
for item in result:
print(f"订单编号:{item['orderId']}")
except Exception as e:
# 重试耗尽后仍未成功,我们可以给出友好提示
print(f"请求最终失败:{e}")
重试是“事后补救”,最理想的还是让不让限流发生。所以有了下面这些技巧。
5.3 降低 RU 消耗的常见技巧
- 不要用
SELECT *,只查你需要列,能省不少 RU。 - 不要跨分区查询,因为跨分区查询会读取所有物理分区,RU 消耗会成倍增加。正确做法是查询时带上分区键。
- 合理设计索引,不必要的索引字段会拖慢写入。
- 避免在单个容器里塞太多无关数据,保持每个逻辑分区在 10GB 以内(尽管默认 20GB,还是留点余量好)。
下面这段代码演示了如何“只查需要的字段”,并带上分区键,让查询更省 RU:
# Python 3.10+
# 一个更省 RU 的查询:只取少量字段,并指定分区键
query = """
SELECT o.orderId, o.amount
FROM orders o
WHERE o.userId = @uid
"""
# 同时带分区键参数,让请求只访问一个逻辑分区
items = container.query_items(
query=query,
parameters=[{"name": "@uid", "value": "user-123"}],
partition_key="user-123",
enable_cross_partition_query=False # 明确不需要跨分区
)
for item in items:
print(f"订单 {item['orderId']} 金额:{item['amount']}")
这些技巧看着不起眼,但每个查询少个十几 RU,积少成多,可能就从“限流边缘”回到“安全区”了。
六、应用场景、优缺点与注意事项
6.1 适合手动扩容的场景
手动扩容适合几类场景:一是流量时间规律明显,比如早上 9 点报表任务跑批、晚上 8 点用户活跃;二是固定成本可控,老板希望预算一眼看透,不想因为自动缩放产生意外账单;三是容器刚上线,RU 需求还没摸清,先用固定值观察一阵子。
6.2 手动扩容的优缺点
优点:规则简单,不会突然涨价;性能可预期,你设置多少 RU 就享有多少保障;适合配合定期任务(比如每天凌晨调低 RU)来优化成本。
缺点:不够敏捷,如果突然出热点,手动扩容往往来不及;容易浪费,因为永远要按峰值预留;对运维人员要求高,你得盯紧监控并提前规划扩容窗口。
6.3 注意事项
第一,扩容操作本身会消耗秒级到分钟级的时间,不要在限流警报响起来之后才去按按钮。建议提前预判,比如业务大促前就完成扩容。第二,修改容器吞吐量时,容器的旧数据不会变,但要留意重启后的连接是否正常。第三,如果多次扩容仍然限流,一定要检查分区键是否存在严重倾斜。你可以在 Azure 门户的“Metrics”里看到每个分区键的 RU 消耗占比,如果某一个分区键占 80% 以上,那就是热分区了。那时候加 RU 不是锦上添花,而是杯水车薪,最彻底的解决方案是修改分区键,重新设计容器。第四,不要把 RU 上限设到天价,先用估算函数算好,再留一点缓冲,比如 20% 到 30%,不要翻十倍预留。
七、文章总结
今天我们聊了 Cosmos DB 因请求单元不足而限流的整个处理思路。先回头看分区键设计,再通过 Python 示例演示了如何手动扩容和估算 RU,最后讨论了限流后的应对策略和几种优化技巧。核心要记住一句话:扩容不是第一步,分区键才是。RU 只是“体力值”,而分区键决定了你的体力能不能均匀发力。如果分区键设计得像一个均匀散布的渔网,那么手动扩容就是一个可预期的老式锅炉,加煤就能升温;如果分区键本身偏到一边,那锅炉内部全是结块的煤渣,加再多煤也白搭。
实际运营中,建议你把容量规划变成例行流程。上线前用 Python 脚本压测,把 RU 需求算明白;运营中时刻盯着监控;大促前提前主动扩容;活动结束后及时把 RU 降下来。这并不神秘,就像家里过日子,知道冰箱里剩多少菜,才会决定今天要不要买菜,否则要么饿肚子,要么浪费钱。希望这篇博客能帮你少几次“429 报警”的惊慌,让数据库不再扮演限流的“恶人”。
Comments