设计亚马逊按销售排名功能

1: 定义 Use Cases 和 约束

Use cases

作用域内的Use Case
  • Service 通过目录计算过去一周内最受欢迎的产品
  • User 通过目录去View过去周内最受欢迎的产品
  • Service 有高可用
作用域外
  • 整个电商网站
    • 设计组件(只是计算销售排名)

约束和假设

  • Traffic 不是平均分布的
  • 类目会被存进多个不同目录
  • 类目不能改变目录
  • 没有子目录,比如: foo/bar/baz
  • 结果会被定时更新
    • 更受换一个的产品或许需要更频繁的更新
  • 1000 万产品
  • 1000 目录
  • 10 亿交易 / 月
  • 1000 亿 请求 / 月
  • 100:1读写比率

计算使用量

  1. 每个 trasnaction 的 Size

    • created_at - 5 bytes
    • product_id - 8 bytes
    • category_id - 4 bytes
    • seller_id - 8 bytes
    • buyer_id - 8 bytes
    • quantity - 4 bytes
    • total_price - 5 bytes
    • Total: ~40 bytes
  2. 每个月有 40 GB 的新 trasnaction 内容

  • 40 字节 / transaction * 10 亿 transaction / 月
  • 1.44 TB 新 transaction 内容 / 3 年
  • 假设大部分是新的交易,而不是更新已经存在的
  1. 平均 400 transaction / s
  2. 平均 40000 读请求 / s

便利转换指南:

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

2:创建一个High Level设计

设计亚马逊按销售排名功能_第1张图片

3: 设计核心组件

Use case: Service 通过category计算过去周内最受欢迎的产品

我们可以我们可以存储二进制 Sales API server的 log 文件进受管理的独享存储,比如 Amazon S3, 而不是管理我们自己的分布式文件系统

我们将假设这是一个简单的 log entry, tab delimited:

timestamp   product_id  category_id    qty     total_price   seller_id    buyer_id
t1          product1    category1      2       20.00         1            1
t2          product1    category2      2       20.00         2            2
t2          product1    category2      1       10.00         2            3
t3          product2    category1      3        7.00         3            4
t4          product3    category2      7        2.00         4            5
t5          product4    category1      1        5.00         5            6

Sales Rank Service 会使用 MapReduce, 使用这个 Sales API Sever的 log 文件作为输入,而且结果会被写进聚合表 sales_rank. 我们应该讨论数据库是选用SQL 还是NoSQL.

我们将使用多步骤的 MapReduce:

  • Step1: 转移数据到 `(category, produce_id), sum(quantity)
  • Step2: 执行分布式排序
class SalesRanker(MRJob):

    def within_past_week(self, timestamp):
        """Return True if timestamp is within past week, False otherwise."""
        ...

    def mapper(self, _ line):
        """Parse each log line, extract and transform relevant lines.

        Emit key value pairs of the form:

        (category1, product1), 2
        (category2, product1), 2
        (category2, product1), 1
        (category1, product2), 3
        (category2, product3), 7
        (category1, product4), 1
        """
        timestamp, product_id, category_id, quantity, total_price, seller_id, \
            buyer_id = line.split('\t')
        if self.within_past_week(timestamp):
            yield (category_id, product_id), quantity

    def reducer(self, key, value):
        """Sum values for each key.

        (category1, product1), 2
        (category2, product1), 3
        (category1, product2), 3
        (category2, product3), 7
        (category1, product4), 1
        """
        yield key, sum(values)

    def mapper_sort(self, key, value):
        """Construct key to ensure proper sorting.

        Transform key and value to the form:

        (category1, 2), product1
        (category2, 3), product1
        (category1, 3), product2
        (category2, 7), product3
        (category1, 1), product4

        The shuffle/sort step of MapReduce will then do a
        distributed sort on the keys, resulting in:

        (category1, 1), product4
        (category1, 2), product1
        (category1, 3), product2
        (category2, 3), product1
        (category2, 7), product3
        """
        category_id, product_id = key
        quantity = value
        yield (category_id, quantity), product_id

    def reducer_identity(self, key, value):
        yield key, value

    def steps(self):
        """Run the map and reduce steps."""
        return [
            self.mr(mapper=self.mapper,
                    reducer=self.reducer),
            self.mr(mapper=self.mapper_sort,
                    reducer=self.reducer_identity),
        ]

这个结果将变成下面的 sorted list, 我们可以插入 sales_rank 表中:

(category1, 1), product4
(category1, 2), product1
(category1, 3), product2
(category2, 3), product1
(category2, 7), product3

sales_rank table 会有如下的结构:

id int NOT NULL AUTO_INCREMENT
category_id int NOT NULL
total_sold int NOT NULL
product_id int NOT NULL
PRIMARY KEY(id)
FOREIGN KEY(category_id) REFERENCES Categories(id)
FOREIGN KEY(product_id) REFERENCES Products(id)

我们将创建一个index在id, category_id, and product_id 上去加速查询(log 时间而不是扫描整张表)
,而且放进数据去内存。从内存中序列化的读取数据需要250微妙,当从SSD读取需要4倍,从磁盘读取需要80倍。

Use Case: User 通过目录查看过去一周内最受欢迎的产品
  • Client 发送请求到Web Server
  • Web Server 转发请求到 Read API server
  • Read API server 从数据库表 sales_rank 重读取数据

我们可以使用如下Rest API:

$ curl https://amazon.com/api/v1/popular?category_id=1234

Response:

{
    "id": "100",
    "category_id": "1234",
    "total_sold": "100000",
    "product_id": "50",
},
{
    "id": "53",
    "category_id": "1234",
    "total_sold": "90000",
    "product_id": "200",
},
{
    "id": "75",
    "category_id": "1234",
    "total_sold": "80000",
    "product_id": "3",
}

4: 扩展设计

设计亚马逊按销售排名功能_第2张图片

分析数据库可以使用数据仓库解决方案,如Amazon Redshift或Google BigQuery。

我们可能只想在数据库中存储有限时间段的数据,而将剩余数据存储在数据仓库或对象存储中。像亚马逊S3这样的对象存储可以轻松应对每月40 GB新内容的限制。

为了解决每秒 40,000 个平均读取请求(峰值更高),流行内容(及其销售排名)的流量应由内存缓存而不是数据库处理。内存缓存对于处理分布不均的流量和流量尖峰也很有效。由于读取量很大,SQL 读取副本可能无法处理缓存缺失。我们可能需要采用额外的 SQL 扩展模式。

对于单个SQL写主从架构来说,每秒400次平均写入(峰值更高)可能很困难,这也表明需要额外的扩展技术。

你可能感兴趣的:(服务器,设计模式)