全文内容主要来自对课程《深入浅出计算机组成原理》的学习笔记。
52 | 设计大型DMP系统(上)
DMP:数据管理平台
DMP
系统的全称叫作数据管理平台
(Data Management Platform),在搜索、推荐、广告领域使用很广。
DMP 可以简单地看成是一个键 - 值对(Key-Value)数据库,用来存储画像信息。期望的性能:
- 低响应时间(Low Response Time);
- 高可用性(High Availability);
- 高并发(High Concurrency);
- 海量数据(Big Data);
- 可负担的成本(Affordable Cost)
如上图,为了维持 DMP 的运转,上游需要不断的采集数据更新其中信息:
- 采集埋点日志,通过数据管道(Data Pipeline)落入数据仓库(Data Warehouse),挖掘和抽取画像更新到 DMP 中;
- 通过实时数据处理模块(Realtime Data Processing),进行实时的清洗和聚合,更新一些实时画像。
MongoDB 的例子
MongoDB 的设计宣传:不需要预设数据 Schema,访问速度很快,还能够无限水平扩展。
有人说只用它就够了,实际上很难有如此完美的情况,看下不同环节的性能取舍:KV存取
:响应快、高并发、写多读少(全随机);数据管道
:高吞吐量、响应时间松、顺序读写。数据仓库
:读取量巨大,很重的离线抽取和分析需求。
MongoDB 缺陷:
- 没有针对 SSD 优化,高并发读取差;
- 顺序写入和吞吐率差;
- 没有 Schema,元信息占用空间大。
相对可行的方案:KV 数据库
:SSD + AeroSpike;(高并发、成本可控)数据管道
:HDD + Kafka;(充分利用 Zero-Copy 和 顺序读写)数据仓库
:HDD + Hive 等 Schema 数据库。(序列化存储)
53 | 设计大型DMP系统(下)
关系型数据库
传统关系型数据库,为了避免读取的时候过多的扫描,往往给数据的行号加一个索引,这个映射关系可以让行号直接从硬盘的某个位置去读。索引不仅可以索引行号,还可以索引某个字段。但,写入数据时还要更新的索引。最终还要落到 HDD 硬盘的话,就很难做到高并发了。
DMP 的 KV 数据库主要是随机查询,数据管道的需求主要是写入和顺序读取就好了。因此就会面临大量的随机写入和随机读取的挑战。
Cassandra:顺序写和随机读
Cassandra 的数据模型
它是一个分布式 KV 数据库,键一般称为 Row Key
,一个 16-36 字节的字符串。每个 Key 对应的 Value 是一个 Hash 表,可用键值对存入需要的数据。
有严格的 Schema,提前定义好列(Column),常一起用的聚合为列族
(Column Family)。既保持了不需要严格的 Schema 这样的灵活性,也保留了可以把常常一起使用的数据存放在一起的空间局部性。
Cassandra 的写操作
简单概述为不随机写,只顺序写。过程是两个动作:
- 往磁盘上写入一条提交日志(Commit Log);
- 直接在内存的数据结构上去更新数据。
优势:
- 都是顺序写(Sequential Write),可最大化吞吐量;
- 内存的数据量或条目超过限额,会 dump 到硬盘上,也顺序写;
- Dump 同时,会根据 row key 来生成索引文件,用于快速查询;
- 当 Dump 的文件过多,Cassandra 会在后台进行文件的对比合并。
Cassandra 的读操作
先从内存查,再从硬盘读,合并成最终结果。
- 在内存会有 Cache,Cache 里面找不到,我们才会去请求硬盘里面的数据。
- 硬盘可能 Dump 了不同时间点的快照,所以按照时间从新的往旧的里面找。
- 为了避免查找过多 Dump 文件,会为每一个 Dump 的文件里面所有 Row Key 生成一个 BloomFilter 放进内存;
- 所以,不在内存,但是在 BloomFilter 中的时候,才会请求硬盘了。
利用 SSD 的优势
Cassandra 的特点:
- 没有任何的随机写请求,无论是 Commit Log 还是 Dump;
- 会优先从内存读,这相当于 LRU 的缓存机制;
- BloomFilter,把本来因为 Dump 文件带来的需要多次读硬盘的问题,简化成多次内存读和一次硬盘读。
顺序读写下,HDD 硬盘的吞吐率还是很不错的:
每秒写入 100MB 数据,如果一条 1KB,那么 10 万的 WPS(Writes per seconds)对于 DMP 也不错。
但 DMP 的数据访问分布,往往缺少局部性的,随机读避免不了。HDD 硬盘在这块较差,全都放内存,成本在 HDD 硬盘 100 倍以上。相比较,用 SSD 硬盘,我们可以用 1/10 的成本获得和内存同样的 QPS。
54 | 理解Disruptor(上)
作者在最后2节主要以开源项目 Disruptor 为例,介绍如何利用 CPU 和高速缓存的硬件特性。Disruptor 是由一家专门做高频交易的公司 LMAX 开源出来的。
Padding Cache Line
1 | ...... |
看上面一段代码,Disruptor 分别在 RingBufferPad 和 RingBuffer 类里面都定义了 p1-p7 这样 7 个 long 变量。看上去很突兀,实际上这 14 个变量没有任何实际的用途,只是缓存行填充(Padding Cache Line),以发挥 CPU 高速缓存(CPU Cache)。
我们知道,高速缓存的写回和加载,都是以整个 Cache Line 作为单位的。比如,64 位 Intel CPU,缓存行通常是 64 个字节(Bytes),即 8 个 long 类型的数据。
我想读者你大概已经猜到了。在 RingBufferFields 里 final 定义了一系列真正要使用的变量,我们期望他们一直在 CPU Cache 里。而高速缓存在写回和加载的时候,还会关联到这个数据前后定义的其他变量,以满足 64 个字节的 Cache Line 大小。而系统会有其他程序在使用,为了保证数据的同步更新,常量的缓存也就失效了,会频繁的读写,降低这里的速度。
但是,当我们按照上述前后各新增 7 个 long 数据后,如下图所示。无论加载其中哪个 final 变量,对应的 Cache Line 都只包含这批 final 变量和定义的 pad 变量。所以,只要被频繁地读取访问,就不会再被换出 Cache 了。
使用 RingBuffer
Disruptor 整个框架,其实就是一个高速的生产者 - 消费者模型(Producer-Consumer)下的队列。
队列的实现,最合适的是链表,例如 java 中的 LinkedBlockingQueue。但在 Disruptor 里面用的是 RingBuffer 的数据结构,其底层是一个固定长度数组。
数据存在空间局部性,即连续多个元素会一并加载到 CPU Cache 里面来,访问遍历的速度会更快。反观链表也不具备这个优势特性。
55 | 理解Disruptor(下)
缓慢的锁
上节提到的通过 RingBuffer 实现一个队列实际上是无锁的。
先回到 java 的 LinkedBlockingQueue,它是依赖锁的。其需要锁的原因是:
- 可能会有多个生产者在队列尾加任务、多个消费者在消费队列头;
- 哪怕生产者、消费者均只有1个时,后者也会比前者快来防止任务积压,从而大多时候二者指向队列同一个节点产生锁竞争;
所以,为了解决此问题,jvm 中实现了加锁机制,没有拿到锁的线程会挂起等待。
按照下面代码测试有无锁的性能差了几十倍。
1 | package com.xuwenhao.perf.jmm; |
无锁的 RingBuffer
Disruptor 是通过用 CPU 硬件支持的指令(CAS,Compare And Swap,比换和交换)来实现无锁。
如下图所示,
- 创建了一个 Sequence 对象,用来指向当前的 RingBuffer 的头和尾,通过一个序号;
- 对比序号的方式:当生产者添加时,它会把当前的序号,加上新数据的数量,然后和消费者位置对比,防止覆盖掉还没消费的数据。