Fork me on GitHub

深入浅出计算机组成原理——应用篇(52-55)

全文内容主要来自对课程《深入浅出计算机组成原理》的学习笔记。

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 的读操作

先从内存查,再从硬盘读,合并成最终结果。

  1. 在内存会有 Cache,Cache 里面找不到,我们才会去请求硬盘里面的数据。
  2. 硬盘可能 Dump 了不同时间点的快照,所以按照时间从新的往旧的里面找。
  3. 为了避免查找过多 Dump 文件,会为每一个 Dump 的文件里面所有 Row Key 生成一个 BloomFilter 放进内存;
  4. 所以,不在内存,但是在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
......

abstract class RingBufferPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}


abstract class RingBufferFields<E> extends RingBufferPad
{
......
private final long indexMask;
private final Object[] entries;
protected final int bufferSize;
protected final Sequencer sequencer;
......
}

public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E>
{
......
protected long p1, p2, p3, p4, p5, p6, p7;
......
}

看上面一段代码,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. 可能会有多个生产者在队列尾加任务、多个消费者在消费队列头;
  2. 哪怕生产者、消费者均只有1个时,后者也会比前者快来防止任务积压,从而大多时候二者指向队列同一个节点产生锁竞争;

所以,为了解决此问题,jvm 中实现了加锁机制,没有拿到锁的线程会挂起等待。

按照下面代码测试有无锁的性能差了几十倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.xuwenhao.perf.jmm;


import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class LockBenchmark{


public static void runIncrement()
{
long counter = 0;
long max = 500000000L;
long start = System.currentTimeMillis();
while (counter < max) {
counter++;
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end-start) + "ms without lock");
}


public static void runIncrementWithLock()
{
Lock lock = new ReentrantLock();
long counter = 0;
long max = 500000000L;
long start = System.currentTimeMillis();
while (counter < max) {
if (lock.tryLock()){
counter++;
lock.unlock();
}
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end-start) + "ms with lock");
}


public static void main(String[] args) {
runIncrement();
runIncrementWithLock();

无锁的 RingBuffer

Disruptor 是通过用 CPU 硬件支持的指令(CAS,Compare And Swap,比换和交换)来实现无锁。

如下图所示,

  1. 创建了一个 Sequence 对象,用来指向当前的 RingBuffer 的头和尾,通过一个序号;
  2. 对比序号的方式:当生产者添加时,它会把当前的序号,加上新数据的数量,然后和消费者位置对比,防止覆盖掉还没消费的数据。


-------------本文结束感谢您的阅读-------------

本文标题:深入浅出计算机组成原理——应用篇(52-55)

文章作者:

原始链接:https://www.xiemingzhao.com/posts/computerOrgArc52to55.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。