我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 2.5 秒,订单处理失败,错误日志中充斥着死锁报错。
"改用 NoSQL 会破坏数据完整性"、"优化 SQL 查询就行了"、"NoSQL 就是个营销噱头"。
这些声音我都听过。但我选择无视。
因为当所有人都在鼓吹 SQL 优化时,我们的应用正在垂死挣扎。
我们面临两个选择:再次纵向扩展 SQL 数据库,或者彻底重构数据架构。批评者说 NoSQL 是玩具,"它处理不了事务",他们警告说。
三个月后,我们的应用处理能力提升 5 倍,查询速度加快 10 倍,宕机时间?零。
以下是我们的完整实践路径,也许值得你参考。
我们的架构堪称教科书级完美:
PostgreSQL RDS 处理事务数据
Redis 作为缓存层
Elasticsearch 处理搜索
多个只读副本
精心优化的查询
使用 DataDog 全方位监控
我们做了所有"正确"的事情:数据库查询都有索引,表结构严格规范化,团队工程师能闭着眼睛背诵 PostgreSQL 文档。
然而...我们不得不推倒重来。
PostgreSQL 架构已触及瓶颈:
复杂 JOIN 查询在负载下耗时 1.5 秒+
行级锁导致持续死锁
纵向扩展成本失控
流量高峰频繁宕机
工程师团队疲于救火而非开发
我们尝试了所有 SQL 专家的建议:
增加更多索引(收效甚微)
数据分区(仍受写入竞争困扰)
考虑纵向扩展(无法承担 5 万美元的服务器)
但现实是:读密集型负载压垮了数据库。无论怎么优化,复杂的 JOIN 查询在规模面前都成了性能噩梦。
监控面板揭示残酷真相:
-- Our most problematic query (2.5s+ execution time)
SELECT
o.id, o.status, o.created_at,
c.name, c.email,
p.title, p.price,
i.quantity,
a.street, a.city, a.country,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as items_count
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
JOIN addresses a ON o.shipping_address_id = a.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
ORDER BY o.created_at DESC;
执行计划如同噩梦:
Nested Loop (cost=1.13..2947.32 rows=89 width=325)
-> Index Scan using orders_created_at on orders (cost=0.42..1234.56 rows=1000)
-> Materialize (cost=0.71..1701.23 rows=89 width=285)
-> Nested Loop (cost=0.71..1698.12 rows=89 width=285)
-> Index Scan using customers_pkey on customers
-> Index Scan using order_items_pkey on order_items
指标全线告急:
平均查询时间:1.5 秒+(原 200 毫秒)
CPU 使用率:89%
IOPS:触顶
缓冲缓存命中率:65%(原 87%)
死锁频率:6-7 次/分钟
我们首先尝试查询优化。DBA 建议:
- Added composite indexes
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
-- Materialized views for common queries
CREATE MATERIALIZED VIEW order_summaries AS
SELECT
o.id,
COUNT(i.id) as items_count,
SUM(p.price * i.quantity) as total_amount
FROM orders o
JOIN order_items i ON o.id = i.order_id
JOIN products p ON i.product_id = p.id
GROUP BY o.id;
-- Query rewrite
WITH order_data AS (
SELECT
o.id, o.status, o.created_at,
c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'processing'
AND o.created_at > NOW() - INTERVAL '24 HOURS'
)
SELECT
od.*,
os.items_count,
os.total_amount
FROM order_data od
JOIN order_summaries os ON od.id = os.id;
结果: 查询时间优化至 800 毫秒。仍不达标。
我们实施激进缓存策略:
// Redis caching layer
const getOrderDetails = async (orderId) => {
const cacheKey = `order:${orderId}:details`;
// Try cache first
let orderDetails = await redis.get(cacheKey);
if (orderDetails) {
return JSON.parse(orderDetails);
}
// Cache miss - query database
orderDetails = await db.query(ORDER_DETAILS_QUERY, [orderId]);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(orderDetails));
return orderDetails;
};
// Cache invalidation on updates
const updateOrder = async (orderId, data) => {
await db.query(UPDATE_ORDER_QUERY, [data, orderId]);
await redis.del(`order:${orderId}:details`);
};
甚至增加缓存预热:
// Warm cache for active orders
const warmOrderCache = async () => {
const activeOrders = await db.query(`
SELECT id FROM orders
WHERE status IN ('processing', 'shipped')
AND created_at > NOW() - INTERVAL '24 HOURS'
`);
await Promise.all(
activeOrders.map(order => getOrderDetails(order.id))
);
};
// Run every 5 minutes
cron.schedule('*/5 * * * *', warmOrderCache);
结果: 有所改善,但高流量时缓存失效成为新噩梦。
扩展到 5 个只读副本并实施负载均衡:
// Database connection pool with read-write split
const pool = {
write: new Pool({
host: 'master.database.aws',
max: 20,
min: 5
}),
read: new Pool({
hosts: [
'replica1.database.aws',
'replica2.database.aws',
'replica3.database.aws',
'replica4.database.aws',
'replica5.database.aws'
],
max: 50,
min: 10
})
};
// Load balancer for read replicas
const getReadConnection = () => {
const replicaIndex = Math.floor(Math.random() * 5);
return pool.read.connect(replicaIndex);
};
// Query router
const executeQuery = async (query, params, queryType = 'read') => {
const connection = queryType === 'write'
? await pool.write.connect()
: await getReadConnection();
try {
return await connection.query(query, params);
} finally {
connection.release();
}
};
结果: 高峰时段复制延迟难以忍受。
每月因宕机损失 11 万美元收入
工程师团队 38% 时间耗费在数据库问题上
客户满意度持续下降
功能开发停滞
AWS RDS 成本攀升至 5,750 美元/月
1、实例成本(约 3,200 美元)
1)主数据库实例:db.r6g.4xlarge
按需价格:约 2,000 美元/月
16 vCPU, 128 GB 内存
24/7 生产负载
2)只读副本:db.r6g.2xlarge
成本:约 1,200 美元/月
8 vCPU, 64 GB 内存
用于读扩展和备份
2、存储成本(约 1,150 美元)
1)预置存储:2 TB gp3
基础存储:0.125 美元/GB/月
2000 GB × 0.125 = 250 美元/月
主库和副本均需支付
2)备份存储:4 TB
前 2 TB 免费
额外 2 TB 按 0.095 美元/GB/月
2000 GB × 0.095 = 190 美元/月
3)IOPS 和吞吐量
额外预置 IOPS:12,000
成本:0.10 美元/IOPS/月
12,000 × 0.10 = 460 美元/月
3、网络传输(约 900 美元)
1)跨可用区数据传输
复制流量:约 8 TB/月
成本:0.02 美元/GB
8000 GB × 0.02 = 160 美元/月
2)互联网数据传输
出站流量:约 15 TB/月
首 TB:0.09 美元/GB
后续 14 TB:0.085 美元/GB
总计:约 740 美元/月
4、附加功能(约 500 美元)
1)多可用区部署
冗余附加费:约 300 美元/月
2)性能洞察
高级监控
长期数据保留
成本:约 200 美元/月
5、月度总成本:5,750 美元
实例成本:56%
存储成本:20%
网络传输:16%
附加功能:8%
我们需要彻底变革。
MongoDB 在讨论中频繁出现。我们犹豫:它能处理事务吗?会破坏数据一致性吗?
这些疑虑合理,但我们别无选择。
我们从最棘手的订单处理服务开始迁移,文档模型设计如下:
// MongoDB order document model
{
_id: ObjectId("507f1f77bcf86cd799439011"),
status: "processing",
created_at: ISODate("2024-02-07T10:00:00Z"),
customer: {
_id: ObjectId("507f1f77bcf86cd799439012"),
name: "John Doe",
email: "john@example.com",
shipping_address: {
street: "123 Main St",
city: "San Francisco",
country: "USA"
}
},
items: [{
product_id: ObjectId("507f1f77bcf86cd799439013"),
title: "Gaming Laptop",
price: 1299.99,
quantity: 1,
variants: {
color: "black",
size: "15-inch"
}
}],
payment: {
method: "credit_card",
status: "completed",
amount: 1299.99
},
shipping: {
method: "express",
tracking_number: "1Z999AA1234567890",
estimated_delivery: ISODate("2024-02-10T10:00:00Z")
},
metadata: {
user_agent: "Mozilla/5.0...",
ip_address: "192.168.1.1"
}
}
结果:原本在 PostgreSQL 耗时 2.3 秒的查询,在 MongoDB 仅需 200 毫秒。
我们通过模式验证保证数据完整性:
// MongoDB schema validation
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["customer", "items", "status", "created_at"],
properties: {
customer: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string" },
email: { bsonType: "string" }
}
},
items: {
bsonType: "array",
items: {
bsonType: "object",
required: ["product_id", "price", "quantity"],
properties: {
product_id: { bsonType: "objectId" },
price: { bsonType: "double" },
quantity: { bsonType: "int" }
}
}
}
}
}
}
});
并设置合理索引:
MongoDB indexes
db.orders.createIndex({ "created_at": 1, "status": 1 });
db.orders.createIndex({ "customer.email": 1 });
db.orders.createIndex({ "items.product_id": 1 });
采用双写机制确保迁移期间 PostgreSQL 与 MongoDB 数据一致性:
// Dual-write implementation
class OrderService {
async createOrder(orderData) {
try {
// Start MongoDB transaction
const session = await mongoose.startSession();
session.startTransaction();
// Write to MongoDB
const mongoOrder = await this.createMongoOrder(orderData, session);
// Write to PostgreSQL
const pgOrder = await this.createPostgresOrder(orderData);
// Verify consistency
if (!this.verifyOrderConsistency(mongoOrder, pgOrder)) {
throw new Error('Data inconsistency detected');
}
await session.commitTransaction();
return mongoOrder;
} catch (error) {
await session.abortTransaction();
throw error;
}
}
private async verifyOrderConsistency(mongoOrder, pgOrder) {
const checksums = await Promise.all([
this.calculateChecksum(mongoOrder),
this.calculateChecksum(pgOrder)
]);
return checksums[0] === checksums[1];
}
}
建立全方位监控体系:
// MongoDB change streams for real-time monitoring
const monitorOrderChanges = async () => {
const changeStream = db.collection('orders').watch();
changeStream.on('change', async (change) => {
// Send metrics to DataDog
const metrics = {
operation_type: change.operationType,
execution_time: change.clusterTime.getTime() - change.operationTime.getTime(),
collection: 'orders'
};
await datadog.gauge('mongodb.operation', metrics);
// Alert on specific conditions
if (change.operationType === 'update' &&
change.updateDescription.updatedFields.status === 'failed') {
await slack.sendAlert({
channel: '#db-alerts',
text: `Order ${change.documentKey._id} failed processing`,
level: 'critical'
});
}
});
};
// Performance monitoring
const monitorPerformance = async () => {
while (true) {
const stats = await db.collection('orders').stats();
await Promise.all([
datadog.gauge('mongodb.size', stats.size),
datadog.gauge('mongodb.count', stats.count),
datadog.gauge('mongodb.avgObjSize', stats.avgObjSize)
]);
await sleep(60000); // Check every minute
}
};
经过三个月的精心迁移:
在黑色星期五期间零停机时间(处理了超3倍的正常流量)
开发速度提高了57%
客户满意度评分提升了42%
消除了每月11万美元的收入损失,并新增了7.5万美元的收入来源
工程团队的士气大幅提升
如果我们要重新开始这段旅程:
我们会从一个较小的服务入手,而不是从订单处理系统开始。虽然最终也成功了,但这就好比在深水区学习游泳。
我们会在前期更多地投资于团队培训。第一个月非常艰难,因为每个人都需要适应新的范式。
我们会更早地构建更好的监控工具。我们早期的一些性能问题本可以更早被发现。
转向NoSQL对我们来说是正确的选择吗?绝对正确。
我会推荐每个人使用吗?不会。
事实是,数据库选择就像架构风格一样——没有所谓的“最佳”选择。
SQL并没有消亡,NoSQL也不是魔法。
但对我们来说,在我们的规模下,针对我们特定的问题,它带来了变革。
六个月前,我在凌晨3点42分盯着手机,怀疑自己是否即将犯下职业生涯中最大的错误。
今天,我从一个更加平静的地方写下这篇文章,拥有一个能够自主运行的系统和一个对未来充满期待的团队。 有时候,最冒险的选择反而是最安全的。
作者丨Crafting-Code 编译丨Rio
来源丨网址:https://blog.stackademic.com/i-dropped-sql-for-nosql-our-app-now-handles-5x-the-traffic-99d07519d843
汇集2025年讨论度最高的数据库议题,XCOPS智能运维管理人年会将于5月16日在广州举办。大会精选金融核心系统数据库切换、多模态数据库设计、存算分离架构搭建,以及云原生数据库、数仓及数据湖的创新实践等干货案例,就等你扫码一起来探讨↓
如果字段的最大可能长度超过255字节,那么长度值可能…
只能说作者太用心了,优秀
感谢详解
一般干个7-8年(即30岁左右),能做到年入40w-50w;有…
230721