【译】在AWS上扩展到数百万用户的系统

Posted by 王天一 on 2019-03-06

第一步:弄清用例与约束

收集需求和问题的范围
通过问问题来弄清用例与约束
讨论假设

我们假定以下用例

用例

解决这个问题需要采用迭代的方法:

  1. 基准/负载测试
  2. 瓶颈检测
  3. 评估替代方案来解决瓶颈
  4. 重复以上

这是将基本设计升级为可扩展设计的良好模式

除非你有AWS的背景或者正在申请AWS的相关职位,否则在AWS上的实现细节不需要了解。然而大部分在这里讨论的原理可以应用到除了AWS以外更通用的地方

我们将问题约束到如下范围

  • 用户发送读或写请求
    • 服务处理,存储用户数据然后返回结果
  • 服务需要从少量用户发展到数百万用户
    • 在我们升级架构来处理大量用户请求时,讨论通用的扩展模式
  • 服务需要高可用

约束和假设

状态假设

  • 流量分布不均
  • 需要关系型数据
  • 从单个用户扩展到千万级用户
    • 用户增加的标识:
      • 用户数+
      • 用户数++
      • 用户数+++
    • 一千万用户
    • 每月10亿次写入
    • 每月1000亿次读取
    • 100:1读写比
    • 每次写入1KB内容

计算方式

如果你想做一个大致估算,请向你的面试官表明以下数据:

  • 每月1TB数据写入
    • 每次写入1KB数据 * 每月10亿次写入
    • 3年有3TB数据写入
    • 假设大多数写入是新的内容而不是已有内容的更新
  • 平均每秒400次写入
  • 平均每秒40000次读取

方便的转换公式:

  • 每月有250万秒
  • 每秒一个请求 = 每月250万个请求
  • 每秒40个请求 = 每月1亿个请求
  • 每秒400个请求 = 每月10亿个请求

第二步:创建高层设计

大致写出包含所有重要组件的高层设计

第三步:设计核心组件

深入每个核心组件的细节

用例:用户发送读或写的请求

目标

  • 对于仅仅的1-2个用户,你只需要一个基本的配置
    • 简单的单体应用
    • 当需要的时候垂直缩放
    • 监控来确定瓶颈

从单体应用开始

使用垂直扩展:

  • 选择更好性能的机器
  • 密切关注监控指标以确定如何扩大规模
    • 使用基本监控来确定瓶颈:CPU,内存,IO,网络等
    • CloudWatch, top, nagios, statsd, graphite等
  • 垂直缩放可能会很昂贵
  • 没有故障转移措施

替代方案和其他细节:

从SQL开始,考虑NoSQL

约束里我们需要关系型数据。我们在开始的时候可以在单机上用MySQL数据库.

替代方案和其他细节:

分配公网静态IP

  • 弹性IP提供一个重启之后不会更改的公网端口
  • 有效的帮助故障转移,只需要将域名指向新IP

使用DNS

使用Route 53添加DNS将域名映射到实例的公共IP

替代方案和其他细节:

保护web服务器

  • 开启必要的端口
    • 允许web服务器对于以下端口回复:
      • 80 - HTTP
      • 443 - HTTPS
      • 22 - SSH(白名单)
    • 阻止web服务器进行出站连接

替代方案和其他细节:

第四步:扩展设计

鉴于约束条件,确定并解决瓶颈

用户数+

假设

我们的用户数正在增加并且在我们单体应用上的负载也在增加。我们的基准/负载测试瓶颈指向了MySQL数据库占用更多内存和CPU资源,同时用户内容正在填满磁盘空间

到目前为止我们可以通过水平扩展解决问题。但不幸的是已经变得非常昂贵并且MySQL数据库web服务器无法独立扩展

目标

  • 减轻单体应用的负载并且允许独立扩展
    • 将静态内容分开存储到AWS对象存储
    • 移动MySQL数据库到独立的服务上
  • 缺点
    • 这些改变将增加复杂度并且需要Web服务器指向对象存储MySQL数据库
    • 新组件额外的安全措施
    • AWS的费用将会增加但应该与自己管理类似系统成本进行权衡

分离存储静态内容

  • 考虑使用S3作为对象存储
    • 高扩展和可靠性
    • 服务端加密
  • 移动静态内容到S3
    • 用户文件
    • JS
    • CSS
    • 图片
    • 视频

移动MySQL数据库到独立的服务

  • 考虑使用RDS服务管理MySQL数据库
    • 扩展和管理简单
    • 多个可用区
    • 静态加密

保护系统

  • 在传输和静止时加密数据
  • 使用虚拟私有网络
    • 为单个Web服务器创建公共子网以便可以发送和接收网上的流量
    • 为其他组件创建私有网络,组织外部访问
    • 每个组件仅仅对白名单IP开放端口

用户数++

假设

我们的基准/负载测试瓶颈检测表明我们的单体Web服务器在高峰期出现瓶颈,导致回应慢,在某些情况下宕机。随着服务的成熟,我们希望提高可用性和冗余度

目的

  • 以下目标尝试解决Web服务器的扩展问题
    • 基于基准/负载测试瓶颈检测,你可能只需要实现这些技术中的一个或者两个
  • 使用水平扩展处理不断增加的负载并解决单体故障
    • 添加负载均衡器
      • ELB是高可用的
      • 如果你想配置自己的负载均衡器, 在多个可用区配置主-主主-备可以提高可用性
      • 负载均衡器上关闭SSL去减少在后端服务器上的计算负载并简化证书管理
    • 使用多个Web服务器分布到多个区域
    • 使用多个主从故障切换模式的MySQL实例来增进冗余度
  • Web服务器应用服务器分开
    • 独立扩展和配置这两层
    • Web服务器可以作为反向代理服务器
    • 比如你可以添加应用服务器处理读API而其他应用服务器处理写API
  • 移动静态(和一些动态)内容到CDN比如CloudFount去减少负载和延迟

Users+++

注意: 为了避免过于混乱,没有显示内部负载均衡器

假设

我们的基准/负载测试瓶颈检测表明我们的读请求很多(100:1读写比),我们的数据库因为大量读取请求导致性能不佳

目标

  • 以下目标尝试去解决在MySQL数据库上的问题
    • 基于基准/负载测试瓶颈检测,你可能只需要实现这些技术中的一个或者两个
  • 移动以下数据到内存缓存,比如Elasticache去减少负载和延迟:
    • MySQL中经常读取的内容
      • 首先,在实现内存缓存之前试图配置MySQL数据库的缓存看是否足以解决瓶颈
    • 来自Web服务器的session数据
      • Web服务器变成无状态服务,允许自动缩放
    • 从内存读取1MB需要250微秒,而SSD需要4倍的时间,从硬盘读取需要80倍时间
  • 添加MySQL只读副本来减少主服务器的负载
  • 添加更多Web服务器应用服务器来提升响应

添加MySQL只读副本

  • 除了增加和扩展内存缓存外, MySQL只读副本也能帮助减轻MySQL主节点的负载
  • 添加Web服务器的逻辑来分开读写数据
  • MySQL只读副本前添加负载均衡器(图里没画)

用户数++++

假设

我们的基准/负载测试瓶颈检测表明在正常工作时间内流量激增,在用户离开办公室时显著下降。我们认为我们可以根据实际负载自动调整服务器来降低成本。我们是个小公司,因此我们希望尽可能多地自动缩放

目标

  • 添加自动缩放来根据需求提供实例数量
    • 跟上流量的高峰
    • 通过关闭未使用的实例来减少费用
  • DevOps自动化
    • Chef, Puppet, Ansible等
  • 继续监控指标以解决瓶颈
    • 主机级别 - 查看单个EC2实例
    • 汇总级别 - 查看负载均衡器统计信息
    • 日志分析 - CloudWatch, CloudTrail, Loggly, Splunk, Sumo
    • 外部网站性能 - Pingdom或New Relic
    • 处理通知和时间 - PagerDuty
    • 错误报告 - Sentry

添加自动缩放

  • 考虑AWS的托管服务自动缩放
    • 为每个Web服务器应用服务器创建一个组, 每个组放到多个可用区中
    • 设置最小和最大实例数
    • 通过CloudWatch触发向上和向下扩展
      • 一段时间内的指标:
        • CPU负载
        • 延迟
        • 网络流量
        • 自定义指标
    • 缺点
      • 自动缩放可能会带来复杂性
      • 系统可能需要一段时间才能适当扩展以满足不断增长的需求,或者在需求下降时缩小规模

Users+++++

注意: 自动缩放组未在图中显示

假设

随着服务继续朝着约束中的数字增长, 基准/负载测试瓶颈检测继续迭代来发现和解决新的瓶颈

目标

由于问题的限制,我们将继续解决扩展问题:

  • 如果我们的MySQL数据库开始变得非常大,我们可能会考虑只将有限时间段的数据存储在数据库中,同时将其余数据存储在Redshift等数据仓库中
    • 像Redshift这样的数据仓库可以轻松处理每月1TB的新内容
  • 每秒平均读取请求4万次,读取常用数据的流量可以通过扩展内存缓存来解决,这对于处理不均匀分布的流量和流量峰值也很有用
    • SQL只读副本可能在处理缓存未命中时遇到问题,我们可能需要采用其他SQL扩展模式
  • 对于单个SQL写服务来说,每秒400次平均写入次数(可能更高的峰值)可能很难,同时也表明需要额外的缩放技术

SQL扩展模式包括:

为了进一步解决高读取和写入请求,我们还应考虑将适当的数据移动到NoSQL数据库,例如DynamoDB

我们可以进一步分离应用服务器来允许独立的缩放。不需要实时完成的批处理和计算可以使用队列工作程序异步完成:

  • 例如,在照片服务中,照片上传和缩略图创建可以分开:
    • 客户端上传图片
    • 应用程序服务器放一个任务到队列
    • 工作服务队列中拉取到任务:
      • 创建缩略图
      • 上传到数据库
      • 存储缩略图到对象存储