设计亚马逊销量前10产品仪表盘的技术实现
1. 故障恢复与容错机制
当批量管道(Batch pipeline)失败时,可由另一主机从 HDFS 读取数据,以检查写入是否成功或是否需要重试。不过,从 HDFS 读取数据是一项开销较大的操作,但由于主机故障是罕见事件,所以这种高开销操作也不常发生。若担心这种高开销的故障恢复机制,可将其实现为定期操作,读取一分钟到几分钟前所有处于“处理中”状态的检查点。
故障恢复机制本身应具有幂等性,以防在执行过程中失败后需要重复执行。同时,要考虑容错性,因为任何写入操作都可能失败,聚合服务、Redis 服务、HDFS 集群或流管道中的任何主机都可能随时出现故障,还可能存在网络问题中断对服务中任何主机的写入请求,而且写入事件响应代码为 200 时也可能实际发生了静默错误,这些情况会导致三个服务处于不一致状态。因此,需要为 HDFS 和流管道分别写入检查点,写入事件应具有 ID,以便目标服务在需要时进行去重。
为防止在向多个服务写入事件时出现不一致,可采用以下方法:
1. 每次向每个服务写入后进行检查点操作。
2. 若需求允许不一致情况存在,则可不采取任何措施。例如,可容忍流管道中的一些不准确,但批量管道必须准确。
3. 定期审计(也称为监督)。若数据不一致,则丢弃不一致的结果并重新处理相关数据。
4. 使用分布式事务技术,如 2PC、Saga、Change Data Capture 或 Transaction Supervisor。
2. 批量管道(Batch pipeline)
批量管道在概念上比流管道更简单。它由一系列按递增时间间隔进行的聚合/汇总任务组成,依次按小时、天、周、月和年进行汇总。假设我们有 100 万个产品 ID:
- 按小时汇总,每天会产生 2400 万行,每周会产生 1.68 亿行。
- 按月汇总,每月会产生 2800 - 3100 万行,每年会产生 3.36 - 3.72 亿行。
- 按天汇总,每周会产生 700 万行,每年会产生 3.64 亿行。
以下是批量管道汇总任务的简化流程图:
graph LR
A[聚合事件(HDFS)] --> B[按小时汇总]
B --> C[按天汇总]
C --> D[按周汇总]
D --> E[按月汇总]
B --> F[按小时汇总表(batch_table)]
C --> G[按天汇总表(batch_table)]
D --> H[按周汇总表(batch_table)]
E --> I[按月汇总表(batch_table)]
存储需求方面,4 亿行数据,每行有 10 个 64 位列,占用 32GB 空间,可轻松存储在单个主机中。每小时的汇总作业可能需要处理数十亿个销售事件,可使用 Hive 查询从 HDFS 读取数据,然后将结果计数写入 SQL 批量表(batch_table)。其他时间间隔的汇总操作利用小时汇总后行数的大幅减少,只需对该 SQL 批量表进行读写操作。
在每个汇总操作中,可按计数降序排序,并将前 K 行(或为灵活起见取 K * 2 行)写入 SQL 数据库,以便在仪表盘上显示。
每个汇总作业的 ETL DAG 包含以下四个任务(第三和第四个任务为兄弟任务):
1. 对于除小时汇总之外的其他汇总操作,需要一个任务来验证依赖的汇总运行是否已成功完成。也可验证所需的 HDFS 或 SQL 数据是否可用,但这会涉及代价高昂的数据库查询。
2. 运行 Hive 或 SQL 查询,按降序对计数进行求和,并将结果计数写入批量表。
3. 删除速度表(speed_table)中的相应行。此任务与任务 2 分开,因为前者可在不重新运行后者的情况下重新执行。若任务 3 在尝试删除行时失败,应重新执行删除操作,而无需重新运行步骤 2 中代价高昂的 Hive 或 SQL 查询。
4. 使用新的批量表行生成或重新生成相应的前 K 列表。这些前 K 列表很可能已经使用准确的批量表数据和不准确的速度表数据生成过,因此这里只需使用批量表数据重新生成。此任务开销不大,若失败也可独立重新执行。
以下是一个汇总作业的 ETL DAG 示例:
graph LR
A[验证依赖的汇总] --> B[计数]
B --> C[删除速度表行]
B --> D[生成前 K 列表]
3. 流管道(Streaming pipeline)
批量作业可能需要数小时才能完成,这会影响所有时间间隔的汇总操作。例如,最新的小时汇总作业的 Hive 查询可能需要 30 分钟才能完成,那么该小时的前 K 列表、包含该小时的当天的前 K 列表以及包含该天的周和月的前 K 列表都将不可用。流管道的目的是提供批量管道尚未提供的计数(和前 K 列表),它必须比批量管道更快地计算这些计数,并且可能使用近似技术。
3.1 单主机的哈希表和最大堆方法
可使用哈希表和大小为 K 的最大堆按频率计数进行排序。以下是一个使用 Go 语言实现的示例函数:
type HeavyHitter struct {
identifier string
frequency int
}
func topK(events []string, k int) []HeavyHitter {
frequencyTable := make(map[string]int)
for _, event := range events {
value := frequencyTable[event]
if value == 0 {
frequencyTable[event] = 1
} else {
frequencyTable[event] = value + 1
}
}
pq := make(PriorityQueue, 0, k)
i := 0
for key, element := range frequencyTable {
pq = append(pq, &HeavyHitter{
identifier: key,
frequency: element
})
if len(pq) > k {
pq = pq[:k]
}
}
var result []HeavyHitter
for pq.Len() > 0 {
result = append(result, pq.Pop().(*HeavyHitter))
}
return result
}
在系统中,可针对不同的时间桶(小时、天、周、月和年)并行运行该函数的多个实例。每个周期结束时,存储最大堆的内容,将计数重置为 0,然后开始新周期的计数。
3.2 多主机水平扩展和多层聚合
若最终哈希表主机的流量过高,可对流管道采用多层方法。在第一层主机和最终哈希表主机之间插入更多层,确保没有主机的流量超过其处理能力。这种方法将实现多层聚合服务的复杂性从聚合服务转移到了流管道,同时会引入一定的延迟。还可进行分区操作,按产品 ID 进行分区,也可按销售事件时间戳进行分区。
聚合操作按产品 ID 和时间戳的组合进行,原因是前 K 列表有一个时间段,需要确保每个销售事件在其正确的时间范围内进行聚合。例如,2023 年 1 月 1 日 10:08 UTC 发生的销售事件应在以下时间范围内进行聚合:
- 小时范围:[2023-01-01 10:08 UTC, 2023-01-01 11:00 UTC)
- 天范围:[2023-01-01, 2023-01-02)
- 周范围:[2022-12-28 00:00 UTC, 2023-01-05 00:00 UTC)(2022-12-28 和 2023-01-05 均为周一)
- 月范围:[2023-01-01, 2023-02-01)
- 年范围:[2023, 2024)
每个周期结束一分钟后,最终层的主机可将其堆写入 SQL 速度表,此时仪表盘即可显示该周期的相应前 K 列表。偶尔,事件可能需要超过一分钟才能通过所有层,此时最终主机可将更新后的堆写入速度表。可设置最终主机保留旧聚合键的保留期,之后将其删除。
4. 近似技术
为实现更低的延迟,可能需要限制聚合服务中的层数。可采用仅由最大堆组成的层的设计,这种方法以牺牲准确性为代价,换取更快的更新速度和更低的成本,可依靠批量管道进行更慢但高度准确的聚合。
最大堆放在单独的主机上是为了在集群扩展时简化新主机的配置。可分别为哈希表主机和最大堆主机创建 Docker 镜像,因为哈希表主机的数量可能会频繁变化,而活动的最大堆主机(及其副本)数量最多为一个。
然而,这种设计生成的前 K 列表可能不准确,不能简单地在每个主机中使用最大堆并合并它们,因为最终的最大堆可能不包含真正的前 K 产品。
4.1 Count - min sketch 算法
之前的方法需要每个主机为与产品数量相同大小的哈希表分配大量内存。可考虑使用近似算法 Count - min sketch 来降低内存消耗。
Count - min sketch 可看作是一个二维表,有宽度和高度。宽度通常为几千,高度较小,表示哈希函数的数量(例如 5)。每个哈希函数的输出范围受宽度限制。当新元素到达时,对其应用每个哈希函数并增加相应单元格的值。
以下是使用 Count - min sketch 处理序列 “A C B C C” 的示例:
|操作|表格状态|
| ---- | ---- |
|添加 “A”|1
1
1
1
1|
|添加 “A C”|1
1
1
1
1
1
1
1
2 (碰撞)|
|添加 “A C B”|1
1
1
1
1
1
1
1
1
2 (碰撞)
1
3 (碰撞)|
|添加 “A C B C”|1
1
2
1
2
1
2
1
1
2
2
4 (碰撞)|
|添加 “A C B C C”|1
1
3
1
3
1
3
1
1
2
3
5 (碰撞)|
要找到出现次数最多的元素,先取每行的最大值 {3, 3, 3, 3, 5},然后取这些最大值中的最小值 “3”。要找到出现次数第二多的元素,先取每行的第二大值 {1, 1, 1, 2, 5},然后取这些值中的最小值 “1”,依此类推。通过取最小值可降低高估的可能性。
Count - min sketch 二维数组可替代之前方法中的哈希表,仍需一个堆来存储频繁元素列表,但用固定大小的 Count - min sketch 二维数组替代了可能很大的哈希表。
5. 仪表盘与 Lambda 架构
仪表盘可能是一个浏览器应用程序,它向后端服务发送 GET 请求,后端服务再运行 SQL 查询。目前讨论的是批量管道写入批量表(batch_table),流管道写入速度表(speed_table),仪表盘应从这两个表构建前 K 列表。
SQL 表不保证顺序,对批量表和速度表进行过滤和排序可能需要数秒时间。为实现 P99 小于 1 秒的响应时间,SQL 查询应是对包含排名和计数列表的单个视图(称为 top_1000 视图)的简单 SELECT 查询。该视图可通过在每个时间段从速度表和批量表中选择前 1000 个产品来构建,还可包含一个额外的列,指示每行是来自速度表还是批量表。当用户请求特定时间间隔的前 K 仪表盘时,后端可查询该视图,尽可能从批量表获取数据,并用速度表填充空白。同时,浏览器应用程序和后端服务可缓存查询响应。
6. Kappa 架构方法
Kappa 架构是一种处理流数据的软件架构模式,使用单个技术栈同时执行批量和流处理。它使用像 Kafka 这样的追加式不可变日志来存储传入数据,然后进行流处理并将数据存储在数据库中供用户查询。
6.1 Lambda 与 Kappa 架构对比
Lambda 架构较为复杂,因为批量层和流层各自需要独立的代码库和集群,以及相关的操作开销、开发、维护、日志记录、监控和警报的复杂性和成本。
Kappa 架构是 Lambda 架构的简化,只有流层,没有批量层,相当于使用单个技术栈同时执行流处理和批量处理。服务层提供从流层计算得到的数据,所有数据在插入消息引擎后立即读取和转换,并通过流技术进行处理,适用于低延迟和近实时的数据处理,如实时仪表盘或监控。可选择牺牲准确性来换取性能,也可选择不做这种权衡,计算高度准确的数据。
批量作业与流作业相比,开发和操作开销更高,因为使用分布式文件系统(如 HDFS)的批量作业即使处理少量数据也往往需要至少几分钟才能完成,而流作业处理少量数据可能只需几秒。批量作业在软件开发的整个生命周期(从开发到测试再到生产)中几乎不可避免地会失败,失败后必须重新运行。虽然可将批量作业分成多个阶段以减少等待时间,但开发人员和运维人员仍需等待 30 分钟或 1 小时才能知道作业是否成功。而且,批量作业中的单个错误会导致整个作业崩溃,而流作业中的单个错误仅影响特定事件的处理。
此外,Kappa 架构相对简单,使用单个处理框架,而 Lambda 架构的批量和流管道可能需要不同的框架。流处理可使用 Redis、Kafka 和 Flink 等框架。
设计亚马逊销量前10产品仪表盘的技术实现
7. 操作步骤总结
为了更清晰地展示整个系统的操作流程,下面将关键操作步骤进行总结:
7.1 批量管道操作步骤
- 数据汇总 :
- 按小时、天、周、月和年依次进行汇总任务。
- 每小时汇总作业使用 Hive 查询从 HDFS 读取数十亿销售事件数据,写入 SQL 批量表。
- 其他时间间隔汇总利用小时汇总结果,对 SQL 批量表进行读写。
- ETL DAG 任务执行 :
- 验证依赖汇总运行是否完成(除小时汇总外)。
- 运行 Hive 或 SQL 查询,将计数结果写入批量表。
- 删除速度表相应行,若失败可单独重执行。
- 用新批量表行生成或重生成前 K 列表。
7.2 流管道操作步骤
- 单主机处理 :
- 使用哈希表和最大堆按频率计数排序,实现
topK函数。 - 针对不同时间桶并行运行函数实例,周期结束存储堆内容并重置计数。
- 使用哈希表和最大堆按频率计数排序,实现
- 多主机扩展与聚合 :
- 插入多层主机,避免单主机流量过高。
- 按产品 ID 和时间戳组合进行聚合。
- 周期结束一分钟后,最终层主机将堆写入 SQL 速度表。
7.3 近似技术操作步骤
- 使用 Count - min sketch 算法 :
- 构建二维表,设置宽度和高度(如宽度几千,高度为 5 个哈希函数)。
- 新元素到达时,应用哈希函数并增加相应单元格值。
- 通过取每行最大值的最小值找到频繁元素。
8. 性能优化建议
为了提高系统性能,可考虑以下优化建议:
1. 故障恢复优化 :将故障恢复机制实现为定期操作,读取特定时间范围内的“处理中”检查点,减少从 HDFS 读取数据的高开销操作频率。
2. SQL 查询优化 :为实现 P99 小于 1 秒的响应时间,使用简单的 SELECT 查询针对 top_1000 视图,避免复杂的过滤和排序操作。
3. 内存优化 :使用 Count - min sketch 算法替代大哈希表,降低内存消耗。
4. 缓存机制 :浏览器应用程序和后端服务缓存查询响应,减少重复查询的开销。
9. 系统架构总结
整个系统架构包括批量管道、流管道、近似技术、仪表盘和不同架构方法,它们相互协作以实现高效、准确的销量前 10 产品仪表盘展示。以下是系统架构的 mermaid 流程图:
graph LR
A[销售事件] --> B[批量管道]
A --> C[流管道]
B --> D[批量表(batch_table)]
C --> E[速度表(speed_table)]
D --> F[top_1000视图]
E --> F
F --> G[仪表盘]
H[近似技术] --> C
I[Kappa架构] --> C
J[Lambda架构] --> B
J --> C
10. 代码示例回顾
为了方便参考,再次给出关键代码示例:
10.1 单主机 topK 函数
type HeavyHitter struct {
identifier string
frequency int
}
func topK(events []string, k int) []HeavyHitter {
frequencyTable := make(map[string]int)
for _, event := range events {
value := frequencyTable[event]
if value == 0 {
frequencyTable[event] = 1
} else {
frequencyTable[event] = value + 1
}
}
pq := make(PriorityQueue, 0, k)
i := 0
for key, element := range frequencyTable {
pq = append(pq, &HeavyHitter{
identifier: key,
frequency: element
})
if len(pq) > k {
pq = pq[:k]
}
}
var result []HeavyHitter
for pq.Len() > 0 {
result = append(result, pq.Pop().(*HeavyHitter))
}
return result
}
10.2 Count - min sketch 示例
| 操作 | 表格状态 |
|---|---|
| 添加 “A” | 1 1 1 1 1 |
| 添加 “A C” | 1 1 1 1 1 1 1 1 2 (碰撞) |
| 添加 “A C B” | 1 1 1 1 1 1 1 1 1 2 (碰撞) 1 3 (碰撞) |
| 添加 “A C B C” | 1 1 2 1 2 1 2 1 1 2 2 4 (碰撞) |
| 添加 “A C B C C” | 1 1 3 1 3 1 3 1 1 2 3 5 (碰撞) |
11. 总结与展望
通过上述技术实现和优化策略,能够设计出一个高效、准确的亚马逊销量前 10 产品仪表盘。在实际应用中,可根据具体业务需求和系统性能表现,进一步调整和优化系统架构和算法。未来,随着数据量的不断增长和业务需求的变化,可探索更先进的技术和算法,如更高效的流处理框架、更精准的近似算法等,以提升系统的性能和准确性,为用户提供更好的数据分析和展示服务。
超级会员免费看
842

被折叠的 条评论
为什么被折叠?



