社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  NoSql

放弃SQL改用NoSQL!疯狂的决定让性能提升了5倍……

DBAplus社群 • 1 月前 • 49 次点击  


这个"疯狂"的数据库切换决策,证明了批评者的错误……





我们的旗舰应用曾在流量高峰时崩溃,导致大量用户无法访问。查询延迟飙升到 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_countFROM orders oJOIN customers c ON o.customer_id = c.idJOIN order_items i ON o.id = i.order_idJOIN products p ON i.product_id = p.idJOIN addresses a ON o.shipping_address_id = a.idWHERE 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 次/分钟


四、失败的解决方案


尝试 #1:查询优化


我们首先尝试查询优化。DBA 建议:


- Added composite indexesCREATE 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 queriesCREATE MATERIALIZED VIEW order_summaries ASSELECT     o.id,    COUNT(i.id) as items_count,    SUM(p.price * i.quantity) as total_amountFROM orders oJOIN order_items i ON o.id = i.order_idJOIN products p ON i.product_id = p.idGROUP BY o.id;
-- Query rewriteWITH 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_amountFROM order_data odJOIN order_summaries os ON od.id = os.id;


结果: 查询时间优化至 800 毫秒。仍不达标。


尝试 #2:Redis缓存


我们实施激进缓存策略:


// Redis caching layerconst 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, 300JSON.stringify(orderDetails));
  return orderDetails;};
// Cache invalidation on updatesconst updateOrder = async (orderId, data) => {  await db.query(UPDATE_ORDER_QUERY, [data, orderId]);  await redis.del(`order:${orderId}:details`);};


甚至增加缓存预热:


// Warm cache for active ordersconst 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 minutescron.schedule('*/5 * * * *', warmOrderCache);


结果: 有所改善,但高流量时缓存失效成为新噩梦。


尝试 #3:只读副本


扩展到 5 个只读副本并实施负载均衡:


// Database connection pool with read-write splitconst pool = {  writenew Pool({    host'master.database.aws',    max20,    min5  }),  readnew Pool({    hosts: [      'replica1.database.aws',      'replica2.database.aws',      'replica3.database.aws',      'replica4.database.aws',      'replica5.database.aws'    ],    max50,    min10  })};
// Load balancer for read replicasconst getReadConnection = () => {  const replicaIndex = Math.floor(Math.random() * 5);  return pool.read.connect(replicaIndex);};
// Query routerconst 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();  }};


结果: 高峰时段复制延迟难以忍受。


五、使用NoSQL


经过三个月优化尝试,我们仍面临:


  • 每月因宕机损失 11 万美元收入

  • 工程师团队 38% 时间耗费在数据库问题上

  • 客户满意度持续下降

  • 功能开发停滞

  • AWS RDS 成本攀升至 5,750 美元/月


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 概念验证


我们从最棘手的订单处理服务开始迁移,文档模型设计如下:


// 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 validationdb.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 indexesdb.orders.createIndex({ "created_at"1"status"1 });db.orders.createIndex({ "customer.email"1 });db.orders.createIndex({ "items.product_id"1 });


七、迁移策略


采用双写机制确保迁移期间 PostgreSQL 与 MongoDB 数据一致性:


// Dual-write implementationclass 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 monitoringconst 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 monitoringconst 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  }};


九、结果


经过三个月的精心迁移:



1、业务影响


  • 在黑色星期五期间零停机时间(处理了超3倍的正常流量)

  • 开发速度提高了57%

  • 客户满意度评分提升了42%

  • 消除了每月11万美元的收入损失,并新增了7.5万美元的收入来源

  • 工程团队的士气大幅提升


2、如果重新开始,我们会怎么做?


如果我们要重新开始这段旅程:


  • 我们会从一个较小的服务入手,而不是从订单处理系统开始。虽然最终也成功了,但这就好比在深水区学习游泳。


  • 我们会在前期更多地投资于团队培训。第一个月非常艰难,因为每个人都需要适应新的范式。


  • 我们会更早地构建更好的监控工具。我们早期的一些性能问题本可以更早被发现。


3、最后的总结 


转向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

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn


数据库相关活动推荐


汇集2025年讨论度最高的数据库议题,XCOPS智能运维管理人年会将于5月16日在广州举办。大会精选金融核心系统数据库切换、多模态数据库设计、存算分离架构搭建,以及云原生数据库、数仓及数据湖的创新实践等干货案例,就等你扫码一起来探讨↓


图片

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/181585
 
49 次点击