From 39c899e6e37bae5deb19a83bcced0e7013b6d2cf Mon Sep 17 00:00:00 2001 From: username Date: Mon, 13 May 2024 16:16:49 +0800 Subject: [PATCH] Site updated: 2024-05-13 16:16:40 --- 2023/10/27/driver_develop/index.html | 2 +- 2023/11/11/Indexing/index.html | 283 +++++++++++++++ 2023/11/18/compilation_principle/index.html | 2 +- 2023/12/10/deduplication_overview/index.html | 2 +- .../deduplication_system_articles/index.html | 21 +- 2024/04/27/algorithm_questions/index.html | 2 +- .../{SMR_DePFC => Rewriting}/2kSfwhw5Sd7.png | Bin .../{SMR_DePFC => Rewriting}/2kSgf9wdlNi.png | Bin .../image-20240511151432270.png | Bin .../image-20240511152707489.png | Bin .../image-20240511154851309.png | Bin .../image-20240511194450908.png | Bin .../image-20240511201404030.png | Bin 2024/05/11/Rewriting/index.html | 334 ++++++++++++++++++ 2024/05/11/SMR_DePFC/index.html | 2 +- archives/2023/11/index.html | 14 + archives/2023/index.html | 10 +- archives/2023/page/2/index.html | 27 +- archives/2023/page/3/index.html | 24 +- archives/2023/page/4/index.html | 12 +- archives/2023/page/5/index.html | 12 +- archives/2023/page/6/index.html | 170 +++++++++ archives/2024/05/index.html | 14 + archives/2024/index.html | 14 + archives/index.html | 25 +- archives/page/2/index.html | 24 +- archives/page/3/index.html | 43 +-- archives/page/4/index.html | 30 +- archives/page/5/index.html | 10 +- archives/page/6/index.html | 17 +- archives/page/7/index.html | 27 ++ search.xml | 30 +- 32 files changed, 1008 insertions(+), 143 deletions(-) create mode 100644 2023/11/11/Indexing/index.html rename 2024/05/11/{SMR_DePFC => Rewriting}/2kSfwhw5Sd7.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/2kSgf9wdlNi.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/image-20240511151432270.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/image-20240511152707489.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/image-20240511154851309.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/image-20240511194450908.png (100%) rename 2024/05/11/{SMR_DePFC => Rewriting}/image-20240511201404030.png (100%) create mode 100644 2024/05/11/Rewriting/index.html create mode 100644 archives/2023/page/6/index.html diff --git a/2023/10/27/driver_develop/index.html b/2023/10/27/driver_develop/index.html index 50677bf4..44443f57 100644 --- a/2023/10/27/driver_develop/index.html +++ b/2023/10/27/driver_develop/index.html @@ -424,7 +424,7 @@

I2C

Wait t

- + diff --git a/2023/11/11/Indexing/index.html b/2023/11/11/Indexing/index.html new file mode 100644 index 00000000..c7c86c2e --- /dev/null +++ b/2023/11/11/Indexing/index.html @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + +Indexing | 修年 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + live2d-demo + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ +
+
+ + +
+
+
+ +
+ + + + + +
+
+

Indexing

+ + + +
+ +
+

sparce indexing

基于chunks的去重都要求使用full index,而这RAM一般承受不起,但是纯用disk io就太慢了。所以它利用了数据局部性:

+

If two pieces of backup streams share any chunks, they are likely to share many chunks. 如果两个segment共享了某个chunk,那么它们很有可能共享很多chunks。

+

是这样的流程:

+
    +
  1. 分段为segment;
  2. +
  3. 计算该segment的每个chunk的fp,然后对每个chunk查询其对应的sparce indexing table: <fp, segment_id>,记录可能跟它共享很多chunk的segment的segment_id;
  4. +
  5. 读取这些segment_id对应的segment的chunk indexing table(存储在disk中);
  6. +
  7. for every chunks: 重复,copy entry ;不重复,add to new container
  8. +
  9. 最后再将该segment的信息写入磁盘,填写sparce indexing表。
  10. +
+

而sparce indexing表最一开始,由对input segment进行chunks的随机抽样得出(或者逐渐构建起来,反正大概是这个意思)

+

可以看到,它将segment info保留在disk中,在RAM中只保留fp2seg_id的映射,每次只需简单从磁盘中读取几个segment info即可,利用数据局部性极大地降低了磁盘IO次数。

+

Odess采用的就是类似这种capping+sparce indexing的方法。

+

将sparce indexing从原来的<fp, seg_id>改为<fp, cid>,并且每次只取top T个包含sample chunks最多的容器,从而将对segment进行cap修改为对container进行cap。仔细想想,这样确实依然保证了原算法的核心思想,也属于是segment size = container size的特种了。

+

不这个<fp, cid>不就是Odess中的recipe(或者说是全局指纹表)吗?乐。Odess也确实体现了这种capping+sparce indexing结合的方法【只不过进行简化了,每个chunk固定取其第一个container】。

+ +
+ + + +
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/2023/11/18/compilation_principle/index.html b/2023/11/18/compilation_principle/index.html index 862397d9..4c53b81b 100644 --- a/2023/11/18/compilation_principle/index.html +++ b/2023/11/18/compilation_principle/index.html @@ -1030,7 +1030,7 @@

- +

diff --git a/2023/12/10/deduplication_overview/index.html b/2023/12/10/deduplication_overview/index.html index 9e1f2078..105d4297 100644 --- a/2023/12/10/deduplication_overview/index.html +++ b/2023/12/10/deduplication_overview/index.html @@ -242,7 +242,7 @@

delta compression

它的提出是针对于小文件/相似chunk的。它的思想感觉有点类似密码学,大概是这样:

1
2
3
given file A,B
calc △ab,
我们就可以通过△ab和B来恢复出一个A
-

目前正在尝试把它纳入到deduplication system中。

+

目前正在尝试把它纳入到deduplication system中。不过目前的瓶颈似乎是这样的,delta compression是要求要将当前chunk同base chunk进行对比,所以怎么找到base chunk就成了问题。

Deduplication

总之,在compression byte-by-byte识别redundant data这样粒度太小的劣势下,通过计算“cryptographically secure hash-based fingerprints”来识别redundant data的chunk-level的deduplication优势就来了!

Overview

image-20231210223322234

这里也是给了一张很棒的图来总结了上文。

diff --git a/2023/12/10/deduplication_system_articles/index.html b/2023/12/10/deduplication_system_articles/index.html index 1d4f3077..a64690b5 100644 --- a/2023/12/10/deduplication_system_articles/index.html +++ b/2023/12/10/deduplication_system_articles/index.html @@ -228,26 +228,7 @@

Deduplication System相关文章

各个超链接导向对应的文章分链接。

-

Deduplication

综述

Indexing

sparce indexing

基于chunks的去重都要求使用full index,而这RAM一般承受不起,但是纯用disk io就太慢了。所以它利用了数据局部性:

-

If two pieces of backup streams share any chunks, they are likely to share many chunks. 如果两个segment共享了某个chunk,那么它们很有可能共享很多chunks。

-

是这样的流程:

-
    -
  1. 分段为segment;
  2. -
  3. 计算该segment的每个chunk的fp,然后对每个chunk查询其对应的sparce indexing table: <fp, segment_id>,记录可能跟它共享很多chunk的segment的segment_id;
  4. -
  5. 读取这些segment_id对应的segment的chunk indexing table(存储在disk中);
  6. -
  7. for every chunks: 重复,copy entry ;不重复,add to new container
  8. -
  9. 最后再将该segment的信息写入磁盘,填写sparce indexing表。
  10. -
-

而sparce indexing表最一开始,由对input segment进行chunks的随机抽样得出(或者逐渐构建起来,反正大概是这个意思)

-

可以看到,它将segment info保留在disk中,在RAM中只保留fp2seg_id的映射,每次只需简单从磁盘中读取几个segment info即可,利用数据局部性极大地降低了磁盘IO次数。

-

Odess采用的就是类似这种capping+sparce indexing的方法。

-

将sparce indexing从原来的<fp, seg_id>改为<fp, cid>,并且每次只取top T个包含sample chunks最多的容器,从而将对segment进行cap修改为对container进行cap。仔细想想,这样确实依然保证了原算法的核心思想,也属于是segment size = container size的特种了。

-

不这个<fp, cid>不就是Odess中的recipe(或者说是全局指纹表)吗?乐。Odess也确实体现了这种capping+sparce indexing结合的方法【只不过进行简化了,每个chunk固定取其第一个container】。

-

Chunking

FastCDC

Fragment

data layout

MFDedup

有机会可以再看看代码实现。

-

rewrite

capping

这篇文章的测试做得很友好很完善,值得精读。

-

对stream进行分段为segment;限制每个版本的容器数(主要是指引用的旧容器数);将那些包含重复块rate较小的容器所包含的重复块视为unique block进行rewrite。

-

SMR && DePFC

非常impressive的两个方法

-

Restore

cache

recipe

forward-assembly

OdessStorage

GC

+

综述

Indexing

Chunking

FastCDC

Fragment

data layout

MFDedup

Rewriting

GC

diff --git a/2024/04/27/algorithm_questions/index.html b/2024/04/27/algorithm_questions/index.html index d8daa0eb..e87573e9 100644 --- a/2024/04/27/algorithm_questions/index.html +++ b/2024/04/27/algorithm_questions/index.html @@ -415,7 +415,7 @@

- + diff --git a/2024/05/11/SMR_DePFC/2kSfwhw5Sd7.png b/2024/05/11/Rewriting/2kSfwhw5Sd7.png similarity index 100% rename from 2024/05/11/SMR_DePFC/2kSfwhw5Sd7.png rename to 2024/05/11/Rewriting/2kSfwhw5Sd7.png diff --git a/2024/05/11/SMR_DePFC/2kSgf9wdlNi.png b/2024/05/11/Rewriting/2kSgf9wdlNi.png similarity index 100% rename from 2024/05/11/SMR_DePFC/2kSgf9wdlNi.png rename to 2024/05/11/Rewriting/2kSgf9wdlNi.png diff --git a/2024/05/11/SMR_DePFC/image-20240511151432270.png b/2024/05/11/Rewriting/image-20240511151432270.png similarity index 100% rename from 2024/05/11/SMR_DePFC/image-20240511151432270.png rename to 2024/05/11/Rewriting/image-20240511151432270.png diff --git a/2024/05/11/SMR_DePFC/image-20240511152707489.png b/2024/05/11/Rewriting/image-20240511152707489.png similarity index 100% rename from 2024/05/11/SMR_DePFC/image-20240511152707489.png rename to 2024/05/11/Rewriting/image-20240511152707489.png diff --git a/2024/05/11/SMR_DePFC/image-20240511154851309.png b/2024/05/11/Rewriting/image-20240511154851309.png similarity index 100% rename from 2024/05/11/SMR_DePFC/image-20240511154851309.png rename to 2024/05/11/Rewriting/image-20240511154851309.png diff --git a/2024/05/11/SMR_DePFC/image-20240511194450908.png b/2024/05/11/Rewriting/image-20240511194450908.png similarity index 100% rename from 2024/05/11/SMR_DePFC/image-20240511194450908.png rename to 2024/05/11/Rewriting/image-20240511194450908.png diff --git a/2024/05/11/SMR_DePFC/image-20240511201404030.png b/2024/05/11/Rewriting/image-20240511201404030.png similarity index 100% rename from 2024/05/11/SMR_DePFC/image-20240511201404030.png rename to 2024/05/11/Rewriting/image-20240511201404030.png diff --git a/2024/05/11/Rewriting/index.html b/2024/05/11/Rewriting/index.html new file mode 100644 index 00000000..ed8c18ac --- /dev/null +++ b/2024/05/11/Rewriting/index.html @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + +Rewriting | 修年 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + live2d-demo + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ +
+
+ + +
+
+
+ +
+ + + + + +
+
+

Rewriting

+ + + +
+ +
+

Capping

测试做得很友好很完善

+

对stream进行分段为segment;限制每个版本的容器数(主要是指引用的旧容器数);将那些包含重复块rate较小的容器所包含的重复块视为unique block进行rewrite。

+

SMR

Improving Restore Performance in Deduplication Systems via a Cost-Efficient Rewriting Scheme

+

在去重时选择容器这个问题可以建模为一个NP的算法问题:(或者说Restoration,由于是反过程所以感觉原理也比较相似)

+

Input: n个等长集合;数组target【container、recipe】

+

Output: 并集包含target中所有数字的最少集合【selected restore containers】

+

Capping事实上就相当于进行了一个妥协,将该问题转化为:

+

Input: n个等长集合;数组target【container、recipe】

+

Output: 选择集合中target比率最大的T个集合【selected restore containers】

+

之前刚入门deduplication system,在看代码的时候,就在想对capping的处理是不是有点暴力了。具体来说,在Capping中,这个块是很多个容器都有,我们每次都默认选择第一个容器来作为这个chunk的index,也即只视第一个容器的chunk为referenced chunks,其它都为仅被引用一次的redundant chunks。而这有时候并不是最优解,因为有可能选同样包含该chunk的其它容器是最优解,也即它们事实上利用比率最大,但是里面部分chunk被视为了redundant而非referenced chunk。

+

本篇文章也正是针对该问题提出。它认为,Capping会产生这个问题是因为它依据了index来进行容器利用率的检测,但其实完全没必要这么做。故而,它提出采用容器之间的差异性来作为指标。

+

image-20240511151432270

+

也即,它将问题转化为了:

+

Input: n个集合;

+

Output: 并集包含的数字最多的T个集合

+

然后只对这个选出来的集合id的容器进行去重,别的都重写。这样一来,它就能够使容器利用率相比于Capping大大增加,重写数大大减少,效果一流。

+

不过这个问题依然还是一个NP问题,故而它使用了贪心算法来进一步解决这个问题:每次从容器全集中选出一个跟当前smr集合不一样的chunk最多的容器。

+

image-20240511152707489

+
+

补充:贪心算法可解的证明
image.png
image.png
求解任何单调子模性函数的最大值,都可以用贪心算法来取得一个质量不错的近似解。于是它的思路就是证明这个是单调子模函数即可

+

image-20240511154851309

+
+

它最后还提出了一个GSMR,依据备份版本之间的特性进行的重写,感觉跟MFDedup的NDF很像,很神奇,后者多加了个更主要的布局整理的第二步。

+

DePFC

Improving Restore Performance of Packed Datasets in Deduplication Systems via Reducing Persistent Fragmented Chunks

+

介绍

大概是说发现了一部分persistent fragment chunk,这些块会一直被重写。具体来说,它关注的是这样的情形。在Deduplication System中,数据流都是以tar形式输入的。而tar的打包方式大概是会给每个文件安上一个metadata block前缀,后面再跟数据块一样,所以最终会是metadata和数据混合存储,并且一般情况下,都是metadata变化较频繁(比如说时间戳变化之类的),数据块变化较为少。而对于小文件较多的场景,这时候就是一个容器中含有很多metadata和少部分data block,如果数据块是持久不变的,由于这个metadata经常变化(unique),导致目前的Rewriting方案都会觉得这个对应的容器的利用率很低,并且加上不会更新index,从而就会导致这个chunk一直被重写,就称这样的chunk为PFC。

+
+

这点其实也是审稿意见中提到的吧,文件间(或者说chunk间)访问频率不固定,不过NACC的做法倒是均等化了每一个chunk的出现几率

+
+

故而,本篇文章提出的思路是,在协作的其它rewriting算法选举出重写块之后,再进一步对重写块分类,分为PFC(persistent fragment chunk) 和 RFC(regular fragment chunk),然后后者普通重写,前者放在一起重写,并且更新fp index指向最新重写块。

+

其中,identify RFC and PFC是这么实现的,维护上一个备份版本的rewritten set。如果FC在其中,则为PFC(被重写过了一次);否则为RFC,加入本次版本的rewritten index。

+

然后,这个过程就会有两个比较关键的点:

+
    +
  1. Limited Scope to Compute the Container’s Utilization

    +

    image-20240511194450908

    +

    在对于Capping协作这种情况会导致该方法失效……

    +

    解决方法是引入一个FCBuffer。具体来说,这个FCBuffer是一个跨segment的数据结构,FC首先被写入FCBuffer,并且实时监测FCBuffer中的各个container的利用率,若利用率达到阈值,就移除FCBuffer中该container对应的所有数据块(因为这说明这些数据块已经被depfc过了)。然后当FCBuffer满的时候再写入新容器。

    +
  2. +
  3. Restore Cache Thrashing

    +

    PFC之间可能间隔很远,特别第一点的FCBuffer带来的PFC和RFC混合存储可能会worsen这一点(毕竟本来RFC就是为了大局起见重写的,把他们集中放置反而可能让PFC之后又面临那个问题了),导致RFC一直滞留更是G。为了防范这点,它引入了一个LRU cache来模拟Restoration阶段,当一个container被驱逐时,就可能说明它里面含有的RFC和当前FCBuffer内所有PFC马上要离当前的context太远了,于是就赶紧写入它对应的所有RFC,以及FCBuffer中所有的PFC。这样一来就能改善距离过远的问题。

    +
    +

    这一点我其实觉得有点费解,虽然说直观上觉得是对的,但是理性上还是很难说服自己,实在没怎么看懂。

    +
    +
  4. +
+

整个算法流程如下图所示:

+

image-20240511201404030

+
+

我看这篇文章,其实是想在当前代码框架尝试着去复现它的。然而,当前的代码框架是一个顺序写入recipe和block的逻辑,貌似不大支持有的chunk可能得在下一个segment的时候才能被确定recipe中的容器位置,我能想到的唯一解决方法是先把这些视为duplicate,等到最后整个dedup完成(所有segment输入完毕)的时候再统一进行一次recipe的更新,不过实现代价可能有点大所以暂时没敢动。

+
+

代码实现

这里也暂且放个我的DePFC + Capping的代码实现吧,虽然最后发现还是不大行(指跟现有代码框架不大兼容,但感觉逻辑一类还是差不多),虽然未经debug,感觉也是可供参考。下面的代码暂没更新fp index,只是基本逻辑框架。

+
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
基本思路:
1. Capping识别出FC
2. doDePFC()分类出PFC和RFC
1. 不断地从wl中获取,unique跳过,普通duplicate跳过
2. FCBuffer满时全部视为PFC,加入PFC set,更新global fp index
3. 驱逐遍历时,PFC加入PFC set,RFC更新rewritten index
4. 如果FCBuffer中某一容器出现频率达到阈值,移出FCBuffer(相当于视作RFC了)
3. 在写入阶段
1. unique和duplicate正常处理(写入data block、写入recipe)
2. 如果是PFC,写入new container
当前代码框架是顺序写入逻辑,要求写入时必须要写入recipe。上述思路会致使一些chunk在本segment输入结束时仍未得知自己是PFC还是RFC还是普通duplicate,从而无法顺序写入recipe,导致可能需要先把FC都视为duplicate,然后之后再进行一个统一的更新recipe。由于实现太过复杂,时间上来不大及,故而暂且先放下。
*/
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Data Structure */
// FCBuffer <cid, FCs>
std::unordered_map<uint64_t, std::list<SHA1FP>> FCBuffer;
const static uint64_t FCBufferCapacity = ContainerSize / ExpectChunkSize;
int FCBufferCurrentSize = 0;
uint64_t FCBuffer_new_cid = fid;

// old rewritten index
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> RCIndex;
// new rewritten index
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> new_RCIndex;

LRUCache SRC; // LRU for container id

std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> PFC;
// 记录FC中被视为duplicate的(与Capping兼容情况)
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> InterCapping;
+ +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// DePFC
Dedup Pipeline:
for (const auto &dedupTask : taskList) {
detectList.push_back(dedupTask);
segmentLength += dedupTask.length;
if (segmentLength > SegmentThreshold || dedupTask.countdownLatch) {
processingWaitingList(detectList);
cappingDedupChunks(detectList); // do Capping first
doDePFC(detectList);

segmentLength = 0;
detectList.clear();
}
}
// change RCIndex to new_RCIndex
RCIndex = new_RCIndex;
new_RCIndex.clear();
+ +
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// baseChunkPositions中是Capping分类出的FC
void dePFC(std::list<DedupTask> &wl) {
for (auto &entry: wl) {
bool isFC = false;
Location location;

if (unlikely(!FLAGS_dedup)) continue; // all chunks are regarded as unique
int result = GlobalMetadataManagerPtr->findRecord(entry.fp, &location);
if (!result) continue; // unique

// now all chunks here is duplicated

auto citer = baseChunkPositions.find(location.fid);
if (citer == baseChunkPositions.end() || citer->second == 0) { // Capping result
isFC = true;
}

if (!isFC) continue; // normal duplicate

// now all chunks here is FC

if (FCBufferCurrentSize + 1 >= FCBufferCapacity) {
// regard all as PFC
for (auto fc_it = FCBuffer.begin(); fc_it != FCBuffer.end();) {
for (auto fp = fc_it->second.begin(); fp != fc_it->second.end();) {
PFC.insert(*fp);
FCBufferCurrentSize --;
fp = fc_it->second.erase(fp);
}
if (fc_it->second.empty() || fc_it->first == vid)
fc_it = FCBuffer.erase(fc_it);
else fc_it ++;
}
assert(FCBufferCurrentSize == 0);
}

uint64_t old_cid = location.fid;

int vid = SRC.put(old_cid);
if (vid >= 0) {
auto it = FCBuffer.find(vid);
assert(it != FCBuffer.end());
bool has_pfc = false;
for (auto &fp : it->second) {
if (RCIndex.find(fp) == RCIndex.end()) {
// RFC, regard as normal denyDedup
FCBufferCurrentSize --;
new_RCIndex.insert(fp);
// TODO: add updates for rewritten index
continue;
}
has_pfc = true;
}

// remove all pfc in the buffer
if (has_pfc) {
for (auto fc_it = FCBuffer.begin(); fc_it != FCBuffer.end();) {
for (auto fp = fc_it->second.begin(); fp != fc_it->second.end();) {
if (RCIndex.find(*fp) == RCIndex.end()) {
fp ++
continue;
}

// PFC
FCBufferCurrentSize --;
PFC.insert(*fp);
fp = fc_it->second.erase(fp);
}
if (fc_it->second.empty() || fc_it->first == vid) fc_it = FCBuffer.erase(fc_it);
else fc_it ++;
}
}
FCBuffer.erase(it);
}

FCBuffer[old_cid].insert(entry);
FCBufferCurrentSize ++;
auto it = FCBuffer.find(old_cid);
if ((it->second.size()) * FLAGS_ExpectSize / ContainerSize >= Threshold) {
for (auto &fp : it->second) {
FCBufferCurrentSize --;
// normal duplicate
InterCapping.insert(fp);
continue;
}
FCBuffer.erase(it);
}
}
}
+ +
1
2
3
4
5
6
7
8
9
// DePFC
auto citer = baseChunkPositions.find(writeTask.location.fid);
if (citer == baseChunkPositions.end() || citer->second == 0) { // FC
if (InterCapping.find(entry.fp) == InterCapping.end()) { // RFC or PFC
result = 0; // capping reject similar chunks.
denyDedup++;
memset(&writeTask.location, 0, sizeof(Location));
}
}
+ + + +

其他

They propose DePFC that identifies and groups PFCs to increase the utilization of containers storing PFCs, making grouped PFCs no longer fragmented. However, DePFC fails to remove redundant data among similar chunks holding metadata blocks.

+

与之相关的是他们团队ICCD’21的另一篇文章,A High-performance Post-deduplication Delta Compression Scheme for Packed Datasets,利用了通过PFC来发现metadata block,然后对其进行内部的delta compression。

+

Comparison

正好一次性读了两篇文章,就来稍微想一下对比吧。我感觉相比DePFC,还是SMR的应用范围可能更广一些,毕竟DePFC可能只在巨多小文件的时候,效果才会出奇的好,平时感觉可能就比一般重写好些。SMR的话,可能还是有一个情况比较受限,比如说一个极端情况,target中正巧很少包含选举出的container中互异的那些chunk,这种情况下target可能需要被大规模重写。emmm…但感觉这种情况也特别少见,不大懂。

+

总之,感觉我还是缺少了点看出这些方法有什么缺点的眼力,不过好在看完了也是收获了挺多。过几天再看看两篇文章的evaluation具体都测了什么吧。

+ +
+ + + +
+ + + + + + +
+ + +
+
+ +
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/2024/05/11/SMR_DePFC/index.html b/2024/05/11/SMR_DePFC/index.html index 47b6744a..916ca3ce 100644 --- a/2024/05/11/SMR_DePFC/index.html +++ b/2024/05/11/SMR_DePFC/index.html @@ -290,7 +290,7 @@

- +
diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index e79d23a2..5d1e1a01 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -171,6 +171,20 @@

2023

+ + + + + + + + + + +
+ Indexing + 十一月 11, 2023 +
diff --git a/archives/2023/index.html b/archives/2023/index.html index cd1c58a8..38b4190c 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -240,8 +240,8 @@

2023

- 驱动开发小记 - 十月 27, 2023 + Indexing + 十一月 11, 2023
@@ -254,14 +254,14 @@

2023

- 开源的第一个月 - 十月 19, 2023 + 驱动开发小记 + 十月 27, 2023
diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index 987cd1ac..fc8b26bd 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -146,6 +146,20 @@

2023

+ + +
+ 开源的第一个月 + 十月 19, 2023 +
+ + + + + + + + @@ -254,20 +268,9 @@

2023

- - - - - - - - - - - diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html index 5f796918..e795e2df 100644 --- a/archives/2023/page/3/index.html +++ b/archives/2023/page/3/index.html @@ -149,6 +149,17 @@

2023

+ + + + + + + + + + + @@ -236,17 +247,6 @@

2023

- - - - - - - - - - - @@ -261,7 +261,7 @@

2023

diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html index 97f4cb42..3a9451b6 100644 --- a/archives/2023/page/4/index.html +++ b/archives/2023/page/4/index.html @@ -234,12 +234,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
@@ -248,14 +245,17 @@

2023

- +
+ cs144 + 二月 25, 2023 +
diff --git a/archives/2023/page/5/index.html b/archives/2023/page/5/index.html index 767a4eae..0c529600 100644 --- a/archives/2023/page/5/index.html +++ b/archives/2023/page/5/index.html @@ -223,12 +223,9 @@

2023

+ -
- xv6 - 一月 10, 2023 -
@@ -237,9 +234,12 @@

2023

- +
+ xv6 + 一月 10, 2023 +
@@ -255,7 +255,7 @@

2023

diff --git a/archives/2023/page/6/index.html b/archives/2023/page/6/index.html new file mode 100644 index 00000000..3fc2f634 --- /dev/null +++ b/archives/2023/page/6/index.html @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + +归档: 2023 | 修年 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + live2d-demo + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+ +
+
+ + + + + +

2023

+ + + + + + + + + + + + +
+
+
+ +
+ +
+ + + \ No newline at end of file diff --git a/archives/2024/05/index.html b/archives/2024/05/index.html index 970c907e..1f4e5212 100644 --- a/archives/2024/05/index.html +++ b/archives/2024/05/index.html @@ -154,6 +154,20 @@

2024

+ + + + + + + + +
+ Rewriting + 五月 11, 2024 +
+ + diff --git a/archives/2024/index.html b/archives/2024/index.html index 52cae171..ccc1f6b2 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -162,6 +162,20 @@

2024

+
+ Rewriting + 五月 11, 2024 +
+ + + + + + + + + +
算法题 四月 27, 2024 diff --git a/archives/index.html b/archives/index.html index aa2939ad..27cf2a22 100644 --- a/archives/index.html +++ b/archives/index.html @@ -163,8 +163,8 @@

2024

@@ -177,8 +177,8 @@

2024

@@ -191,8 +191,8 @@

2024

@@ -205,7 +205,7 @@

2024

@@ -219,8 +219,8 @@

2024

@@ -230,9 +230,12 @@

2024

- + @@ -259,8 +262,6 @@

2024

- -

2023

diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 0774c0a7..198849af 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -157,12 +157,9 @@

2023

+ - @@ -171,9 +168,12 @@

2023

- + @@ -226,12 +226,9 @@

2023

+ - @@ -243,8 +240,8 @@

2023

@@ -254,9 +251,12 @@

2023

- + diff --git a/archives/page/3/index.html b/archives/page/3/index.html index b8ea0661..2d564baf 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -149,8 +149,8 @@

2023

@@ -174,8 +174,8 @@

2023

@@ -185,12 +185,9 @@

2023

+ - @@ -199,9 +196,12 @@

2023

- + @@ -210,9 +210,12 @@

2023

- + @@ -221,12 +224,9 @@

2023

+ - @@ -235,12 +235,9 @@

2023

+ - @@ -249,9 +246,12 @@

2023

- + @@ -260,9 +260,12 @@

2023

- + diff --git a/archives/page/4/index.html b/archives/page/4/index.html index c1c27ba5..6a898925 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -146,12 +146,9 @@

2023

+ - @@ -160,12 +157,9 @@

2023

+ - @@ -174,9 +168,12 @@

2023

- + @@ -185,9 +182,12 @@

2023

- + @@ -196,12 +196,9 @@

2023

+ - @@ -221,9 +218,12 @@

2023

- + diff --git a/archives/page/5/index.html b/archives/page/5/index.html index 58dec055..9f2d4e5b 100644 --- a/archives/page/5/index.html +++ b/archives/page/5/index.html @@ -223,12 +223,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
@@ -248,9 +245,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
diff --git a/archives/page/6/index.html b/archives/page/6/index.html index b237e6f8..ed6d18f2 100644 --- a/archives/page/6/index.html +++ b/archives/page/6/index.html @@ -212,12 +212,9 @@

2023

+ -
- xv6 - 一月 10, 2023 -
@@ -237,25 +234,23 @@

2023

- +
+ xv6 + 一月 10, 2023 +
- -

2022

+ - diff --git a/archives/page/7/index.html b/archives/page/7/index.html index 548943bc..6a8d6235 100644 --- a/archives/page/7/index.html +++ b/archives/page/7/index.html @@ -141,6 +141,19 @@ +

2023

+ + + + + + + + + + + +

2022

@@ -148,6 +161,20 @@

2022

+ + + + + + + + + + +
Java并发编程实战 十一月 6, 2022 diff --git a/search.xml b/search.xml index e97a7165..d3d533ce 100644 --- a/search.xml +++ b/search.xml @@ -16,6 +16,19 @@ + + Rewriting + + /2024/05/11/Rewriting/ + + Capping

测试做得很友好很完善

对stream进行分段为segment;限制每个版本的容器数(主要是指引用的旧容器数);将那些包含重复块rate较小的容器所包含的重复块视为unique block进行rewrite。

SMR

Improving Restore Performance in Deduplication Systems via a Cost-Efficient Rewriting Scheme

在去重时选择容器这个问题可以建模为一个NP的算法问题:(或者说Restoration,由于是反过程所以感觉原理也比较相似)

Input: n个等长集合;数组target【container、recipe】

Output: 并集包含target中所有数字的最少集合【selected restore containers】

Capping事实上就相当于进行了一个妥协,将该问题转化为:

Input: n个等长集合;数组target【container、recipe】

Output: 选择集合中target比率最大的T个集合【selected restore containers】

之前刚入门deduplication system,在看代码的时候,就在想对capping的处理是不是有点暴力了。具体来说,在Capping中,这个块是很多个容器都有,我们每次都默认选择第一个容器来作为这个chunk的index,也即只视第一个容器的chunk为referenced chunks,其它都为仅被引用一次的redundant chunks。而这有时候并不是最优解,因为有可能选同样包含该chunk的其它容器是最优解,也即它们事实上利用比率最大,但是里面部分chunk被视为了redundant而非referenced chunk。

本篇文章也正是针对该问题提出。它认为,Capping会产生这个问题是因为它依据了index来进行容器利用率的检测,但其实完全没必要这么做。故而,它提出采用容器之间的差异性来作为指标。

image-20240511151432270

也即,它将问题转化为了:

Input: n个集合;

Output: 并集包含的数字最多的T个集合

然后只对这个选出来的集合id的容器进行去重,别的都重写。这样一来,它就能够使容器利用率相比于Capping大大增加,重写数大大减少,效果一流。

不过这个问题依然还是一个NP问题,故而它使用了贪心算法来进一步解决这个问题:每次从容器全集中选出一个跟当前smr集合不一样的chunk最多的容器。

image-20240511152707489

补充:贪心算法可解的证明
image.png
image.png
求解任何单调子模性函数的最大值,都可以用贪心算法来取得一个质量不错的近似解。于是它的思路就是证明这个是单调子模函数即可

image-20240511154851309

它最后还提出了一个GSMR,依据备份版本之间的特性进行的重写,感觉跟MFDedup的NDF很像,很神奇,后者多加了个更主要的布局整理的第二步。

DePFC

Improving Restore Performance of Packed Datasets in Deduplication Systems via Reducing Persistent Fragmented Chunks

介绍

大概是说发现了一部分persistent fragment chunk,这些块会一直被重写。具体来说,它关注的是这样的情形。在Deduplication System中,数据流都是以tar形式输入的。而tar的打包方式大概是会给每个文件安上一个metadata block前缀,后面再跟数据块一样,所以最终会是metadata和数据混合存储,并且一般情况下,都是metadata变化较频繁(比如说时间戳变化之类的),数据块变化较为少。而对于小文件较多的场景,这时候就是一个容器中含有很多metadata和少部分data block,如果数据块是持久不变的,由于这个metadata经常变化(unique),导致目前的Rewriting方案都会觉得这个对应的容器的利用率很低,并且加上不会更新index,从而就会导致这个chunk一直被重写,就称这样的chunk为PFC。

这点其实也是审稿意见中提到的吧,文件间(或者说chunk间)访问频率不固定,不过NACC的做法倒是均等化了每一个chunk的出现几率

故而,本篇文章提出的思路是,在协作的其它rewriting算法选举出重写块之后,再进一步对重写块分类,分为PFC(persistent fragment chunk) 和 RFC(regular fragment chunk),然后后者普通重写,前者放在一起重写,并且更新fp index指向最新重写块。

其中,identify RFC and PFC是这么实现的,维护上一个备份版本的rewritten set。如果FC在其中,则为PFC(被重写过了一次);否则为RFC,加入本次版本的rewritten index。

然后,这个过程就会有两个比较关键的点:

  1. Limited Scope to Compute the Container’s Utilization

    image-20240511194450908

    在对于Capping协作这种情况会导致该方法失效……

    解决方法是引入一个FCBuffer。具体来说,这个FCBuffer是一个跨segment的数据结构,FC首先被写入FCBuffer,并且实时监测FCBuffer中的各个container的利用率,若利用率达到阈值,就移除FCBuffer中该container对应的所有数据块(因为这说明这些数据块已经被depfc过了)。然后当FCBuffer满的时候再写入新容器。

  2. Restore Cache Thrashing

    PFC之间可能间隔很远,特别第一点的FCBuffer带来的PFC和RFC混合存储可能会worsen这一点(毕竟本来RFC就是为了大局起见重写的,把他们集中放置反而可能让PFC之后又面临那个问题了),导致RFC一直滞留更是G。为了防范这点,它引入了一个LRU cache来模拟Restoration阶段,当一个container被驱逐时,就可能说明它里面含有的RFC和当前FCBuffer内所有PFC马上要离当前的context太远了,于是就赶紧写入它对应的所有RFC,以及FCBuffer中所有的PFC。这样一来就能改善距离过远的问题。

    这一点我其实觉得有点费解,虽然说直观上觉得是对的,但是理性上还是很难说服自己,实在没怎么看懂。

整个算法流程如下图所示:

image-20240511201404030

我看这篇文章,其实是想在当前代码框架尝试着去复现它的。然而,当前的代码框架是一个顺序写入recipe和block的逻辑,貌似不大支持有的chunk可能得在下一个segment的时候才能被确定recipe中的容器位置,我能想到的唯一解决方法是先把这些视为duplicate,等到最后整个dedup完成(所有segment输入完毕)的时候再统一进行一次recipe的更新,不过实现代价可能有点大所以暂时没敢动。

代码实现

这里也暂且放个我的DePFC + Capping的代码实现吧,虽然最后发现还是不大行(指跟现有代码框架不大兼容,但感觉逻辑一类还是差不多),虽然未经debug,感觉也是可供参考。下面的代码暂没更新fp index,只是基本逻辑框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
基本思路:
1. Capping识别出FC
2. doDePFC()分类出PFC和RFC
1. 不断地从wl中获取,unique跳过,普通duplicate跳过
2. FCBuffer满时全部视为PFC,加入PFC set,更新global fp index
3. 驱逐遍历时,PFC加入PFC set,RFC更新rewritten index
4. 如果FCBuffer中某一容器出现频率达到阈值,移出FCBuffer(相当于视作RFC了)
3. 在写入阶段
1. unique和duplicate正常处理(写入data block、写入recipe)
2. 如果是PFC,写入new container
当前代码框架是顺序写入逻辑,要求写入时必须要写入recipe。上述思路会致使一些chunk在本segment输入结束时仍未得知自己是PFC还是RFC还是普通duplicate,从而无法顺序写入recipe,导致可能需要先把FC都视为duplicate,然后之后再进行一个统一的更新recipe。由于实现太过复杂,时间上来不大及,故而暂且先放下。
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Data Structure */
// FCBuffer <cid, FCs>
std::unordered_map<uint64_t, std::list<SHA1FP>> FCBuffer;
const static uint64_t FCBufferCapacity = ContainerSize / ExpectChunkSize;
int FCBufferCurrentSize = 0;
uint64_t FCBuffer_new_cid = fid;

// old rewritten index
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> RCIndex;
// new rewritten index
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> new_RCIndex;

LRUCache SRC;// LRU for container id

std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> PFC;
// 记录FC中被视为duplicate的(与Capping兼容情况)
std::unordered_set<SHA1FP, TupleHasher, TupleEqualer> InterCapping;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// DePFC
Dedup Pipeline:
for (const auto &dedupTask : taskList) {
detectList.push_back(dedupTask);
segmentLength += dedupTask.length;
if (segmentLength > SegmentThreshold || dedupTask.countdownLatch) {
processingWaitingList(detectList);
cappingDedupChunks(detectList);// do Capping first
doDePFC(detectList);

segmentLength = 0;
detectList.clear();
}
}
// change RCIndex to new_RCIndex
RCIndex = new_RCIndex;
new_RCIndex.clear();
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// baseChunkPositions中是Capping分类出的FC
void dePFC(std::list<DedupTask> &wl) {
for (auto &entry: wl) {
bool isFC = false;
Location location;

if (unlikely(!FLAGS_dedup)) continue; // all chunks are regarded as unique
int result = GlobalMetadataManagerPtr->findRecord(entry.fp, &location);
if (!result) continue; // unique

// now all chunks here is duplicated

auto citer = baseChunkPositions.find(location.fid);
if (citer == baseChunkPositions.end() || citer->second == 0) {// Capping result
isFC = true;
}

if (!isFC) continue; // normal duplicate

// now all chunks here is FC

if (FCBufferCurrentSize + 1 >= FCBufferCapacity) {
// regard all as PFC
for (auto fc_it = FCBuffer.begin(); fc_it != FCBuffer.end();) {
for (auto fp = fc_it->second.begin(); fp != fc_it->second.end();) {
PFC.insert(*fp);
FCBufferCurrentSize --;
fp = fc_it->second.erase(fp);
}
if (fc_it->second.empty() || fc_it->first == vid)
fc_it = FCBuffer.erase(fc_it);
else fc_it ++;
}
assert(FCBufferCurrentSize == 0);
}

uint64_t old_cid = location.fid;

int vid = SRC.put(old_cid);
if (vid >= 0) {
auto it = FCBuffer.find(vid);
assert(it != FCBuffer.end());
bool has_pfc = false;
for (auto &fp : it->second) {
if (RCIndex.find(fp) == RCIndex.end()) {
// RFC, regard as normal denyDedup
FCBufferCurrentSize --;
new_RCIndex.insert(fp);
// TODO: add updates for rewritten index
continue;
}
has_pfc = true;
}

// remove all pfc in the buffer
if (has_pfc) {
for (auto fc_it = FCBuffer.begin(); fc_it != FCBuffer.end();) {
for (auto fp = fc_it->second.begin(); fp != fc_it->second.end();) {
if (RCIndex.find(*fp) == RCIndex.end()) {
fp ++
continue;
}

// PFC
FCBufferCurrentSize --;
PFC.insert(*fp);
fp = fc_it->second.erase(fp);
}
if (fc_it->second.empty() || fc_it->first == vid) fc_it = FCBuffer.erase(fc_it);
else fc_it ++;
}
}
FCBuffer.erase(it);
}

FCBuffer[old_cid].insert(entry);
FCBufferCurrentSize ++;
auto it = FCBuffer.find(old_cid);
if ((it->second.size()) * FLAGS_ExpectSize / ContainerSize >= Threshold) {
for (auto &fp : it->second) {
FCBufferCurrentSize --;
// normal duplicate
InterCapping.insert(fp);
continue;
}
FCBuffer.erase(it);
}
}
}
1
2
3
4
5
6
7
8
9
// DePFC
auto citer = baseChunkPositions.find(writeTask.location.fid);
if (citer == baseChunkPositions.end() || citer->second == 0) {// FC
if (InterCapping.find(entry.fp) == InterCapping.end()) {// RFC or PFC
result = 0; // capping reject similar chunks.
denyDedup++;
memset(&writeTask.location, 0, sizeof(Location));
}
}

其他

They propose DePFC that identifies and groups PFCs to increase the utilization of containers storing PFCs, making grouped PFCs no longer fragmented. However, DePFC fails to remove redundant data among similar chunks holding metadata blocks.

与之相关的是他们团队ICCD’21的另一篇文章,A High-performance Post-deduplication Delta Compression Scheme for Packed Datasets,利用了通过PFC来发现metadata block,然后对其进行内部的delta compression。

Comparison

正好一次性读了两篇文章,就来稍微想一下对比吧。我感觉相比DePFC,还是SMR的应用范围可能更广一些,毕竟DePFC可能只在巨多小文件的时候,效果才会出奇的好,平时感觉可能就比一般重写好些。SMR的话,可能还是有一个情况比较受限,比如说一个极端情况,target中正巧很少包含选举出的container中互异的那些chunk,这种情况下target可能需要被大规模重写。emmm…但感觉这种情况也特别少见,不大懂。

总之,感觉我还是缺少了点看出这些方法有什么缺点的眼力,不过好在看完了也是收获了挺多。过几天再看看两篇文章的evaluation具体都测了什么吧。

]]>
+ + + +
+ + + 算法题 @@ -144,7 +157,7 @@ /2023/12/10/deduplication_overview/ -

Xia W, Jiang H, Feng D, et al. A comprehensive study of the past, present, and future of data deduplication[J]. Proceedings of the IEEE, 2016, 104(9): 1681-1710.

Data Reduction

一开始主要是一步步讲述了Data Deduplication这个概念提出的历程。

Compression

最一开始,都是用的压缩Compression。compression分为lossy和lossless(有损压缩和无损压缩),前者是通过去除一些不必要的信息来不可逆地减少数据大小(如JPEG图片压缩),后者是通过编码或者算术等方法可逆地减少数据大小(如GZIP、LZW等)。由于大规模存储系统(large-scale storage system)主要聚焦于无损压缩,因而下文也主要介绍这个。(deduplication也可以视为无损压缩的一种方法

entropy encoding

信息熵

提到压缩,就不得不提到信息熵。一个变量X的信息熵可以如下计算:

image-20231210221755122

比如说通过字符串abaaacabba,我们可以计算其所构成字母的信息熵:

image-20231210221846621

其实际含义是,对于“abaaacabba”这个上下文,{a, b, c}集合的每个字母至少需要1.295个bit来表示,也即字符串“abaaacabba”至少由12.95个bits来表示。也即,信息熵实际上是算出了压缩的极限

哈夫曼树

早期的压缩理论就是根据信息熵来的,这种我们称为“entropy encoding”或者“statistical-model-based coding”,因为它需要基于某个上下文(statistics)来计算信息熵。最常见的就是哈夫曼树,它用一个frequency-sorted binary tree来生成前缀编码,从而对信息进行压缩。

缺点

然而,显而易见的是这种entropy encoding你首先就得有合适的statistics,这是不scalable的。所以它一般也不适用于现代的storage system的压缩要求。

dictionary-model-based coding

因而,“dictionary-model-based coding”就此浮出水面。它从string-level来识别重复数据,从而简化和加速了压缩。它的主要思想是通过滑动窗口识别重复字符串,并用位置和长度来替代这些重复的。(相当于是unique string只存储一次)代表性的是LZ压缩。

然而,它由于是string-level,所以需要对整个系统的所有string进行扫描,需要在compression ratio和speed之间trade off。

delta compression

它的提出是针对于小文件/相似chunk的。它的思想感觉有点类似密码学,大概是这样:

1
2
3
given file A,B
calc △ab,
我们就可以通过△ab和B来恢复出一个A

目前正在尝试把它纳入到deduplication system中。

Deduplication

总之,在compression byte-by-byte识别redundant data这样粒度太小的劣势下,通过计算“cryptographically secure hash-based fingerprints”来识别redundant data的chunk-level的deduplication优势就来了!

Overview

image-20231210223322234

这里也是给了一张很棒的图来总结了上文。

Key Features

这个部分大概是说key features有两个,一个是chunking,另一个是fingerprinting

chunking有两种方法,fixed-size和variable-size,前者会出现boundary-shift问题,后者更加泛用。

fingerprinting的主流方法还是基于SHA1(现在也用SHA256了)【Cryptographically Secure Hash-Based Fingerprinting】,主要是讨论了它哈希碰撞的可能性很小所以使用安全,还有就是讨论了fingerprint的特性:

  1. 很难找到两个不同msg指纹相同

  2. 很难从fp倒推出一个msg

Basic Workflow

A typical data deduplication system follows the workflow of:

  1. chunking
  2. fingerprinting
  3. indexing
  4. further compression
  5. storage management
    1. data restore
    2. garbage collection
    3. fragment elimination
    4. reliability
    5. security

Deduplication

In this section, we examine the state-of-the-art works on data deduplication in sufficient depth to understand their key and distinguishing features.

本节终于要开始对deduplication的关键技术做详尽的介绍和讨论了。

A.Chunking

这部分确实如他所言主要介绍了chunking。它先是介绍了主流的CDC算法Rabin(具体在FastCDC那篇文章介绍过这部分了,这里就不再赘述),然后讲述了Rabin算法的三个主要缺点:chunk size方差大、计算量大、去重检测还不够精确。

针对这三个缺点,分别有各种文献提出了这几类关键技术(顺序与缺点一一对应):

  1. Reducing Chunk Size Variance by Imposing Limits on MAX/MIN Chunk Sizes for CDC

    当chunk size过大,虽然会加速后续的indexing等步骤,减少space消耗,但是会影响去重率;chunk size过小,虽然会增加去重率,但是会增大后续indexing等步骤的工作量。这又是一个trade-off。

    这里主要介绍了各个主流方法都是怎么限制chunk size的,比如说LBFS简单粗暴,还有别的什么依据极值、非对称滑动窗口等做法。感觉还是FastCDC那个做法更加灵活聪明。

  2. Reducing Computation to Accelerate the Chunking Process

    也是有比如说Gear等等算法或者硬件层面上的改进。

  3. Improving Duplicate-Detection Accuracy by Rechunking Nonduplicate Chunks

    这个问题也是比较普遍,比如如图所示的C2和C5之间就可以再做进一步去重:

    image-20231210235904637

    具体方法有比如说频率分析法选定某些频繁访问的chunk进行rechunking、把几个小的nonduplicate chunk给merge为一个大的然后rechunking等等。

    这个可能对网络场景也有适用,毕竟网络传输也就主要是通过一个个很小的network package数据包。

  4. Impact of Interspersed Metadata

    主要是说如果数据集内meta data和主要数据混在一起可能干扰去重,比如说block header、还有tar打包后产生的文件的包含时间戳等信息的file header等等。

    解决方法大概就是预处理之类的。

B. Accelerate Computational Tasks

这个部分大概就是提出了两种方法,一个是通过将deduplication system给pipeline了(就是Odess那个做法),然后再结合multithreading来对它进行多线程加速;另一个就是通过开发GPU相关库来对deduplication做支持,从而使用GPGPU架构来进行硬件加速。

image-20231211141021880

C. Indexing

image-20231211141035597

不知道这个indexing是不是就是我们pipeline中的dedup阶段,感觉是的。

这个阶段面临的问题就是,数据量太大,导致指纹量也很大装不进内存,也就是说可能得根据磁盘中的指纹进行快速的索引。

indexing大致有两种思路,一个是精准的indexing,另一个就是命中率较低但内存占用也低的indexing。感觉capping有可能也有点后者的感觉()

然后目前流行的也是有四类方法:locality-based, similarity-based, flash-assisted, and cluster deduplication approaches。

  1. locality-based

    大概意思就是说利用数据的局部性,每次要某个指纹不是只读它一个,而是顺带把磁盘中这个指纹后面几个也读进内存,磁盘中的也是按照数据局部性存储的。

    除此之外,DDFS结合Bloom filter使用来精准检测重复。

    A Bloom filter [22] is a space-efficient data structure that uses a bit array with several independent hash functions to represent membership of a set of items (e.g., fingerprints).

    而Sparse indexing则采取“抽样”的方式。

    这个一般用于提高performance。

  2. similarity-based

    最常见的方法是用一个fp set的最大值or最小值来表示一个file,然后对这个建立一个主索引。如果两个文件的代表fp相同,那么这两个文件很有可能重复读极高。

    它这里提到了一个比较值得思考的观点:locality-based是利用了physical-locality,similarity-based是利用了logical-locality。前者还是比较容易理解的,因为它要求磁盘中的指纹按局部性存储,后者我是真没明白。。。之后有兴趣再看看相关论文了解一下吧。

    这个一般用于reduce RAM overhead。

  3. flash-assisted

    感觉这个没啥特别的,相当于换了个闪存介质而不是磁盘来存储index。

  4. Cluster Deduplication

    这个相当于加了层分布式,将输入的数据流分成几个种类(比如说按前缀分)然后送到多个结点上并行地进行去重处理,然后每个结点内部又可以用别的算法了之类的。这就需要涉及到负载均衡、路由算法等等了。

    缺点是可能降低deduplication ratio(可能是因为一些路由算法实现?)。

D. Post-Deduplication Compression

image-20231212224322111

不过即便如此,一个chunk内可能还是有一些小地方是dunplicate的(internal redundancy),这时候压缩就大有用处了。并且多个chunk一起压缩,比单个单个压缩的压缩率更高。

这个还适用于上面说到的一种情况,也即那个用到rechunk的地方,完全可以用delta compression来代替,而且感觉后者可能还更通用()感觉被薄纱。

主要面临的挑战来自于这几个方面:resemblance detection, reading base chunks, and delta encoding。

  1. resemblance detection

    目前大概有这几种方法:

    1. Manber:计算polynomial-based fingerprints,两个文件的相似性取决于它们相同的这个fp的数量。
    2. superfeature:抽样选取一些Rabin fp作为feature,并把它们合起来成为一个大的superfeature,对这个东西进行index。这个好像应用比较广泛。
    3. TAPER:每个file都是一个bloom filter,比较filter相同的bit位数。
  2. delta encoding

  3. Additional Delta Compression Challenges

E. Data Restore

这里笔墨最多的还是在说碎片化问题,同时也简要介绍了去重系统的三个主要应用场景:primary storage、backup storage、cloud storage,以及碎片化问题给它们的薄弱方面的狠狠一击()

  1. primary storage

    它最主要的问题还是IO-sensitive

  2. backup storage

    它最主要的问题是随着备份版本增多碎片化问题的愈发严重

  3. cloud storage

    它最主要的问题是速度,其受网络带宽、碎片化的限制。

F. Garbage Collection

这部分也大概是讲了GC常见的两种方法,一个是reference count,另一个是mark & sweep。

前者需要inline地维护refcnt,后者则可以offline运行。

并且在backup system中,GC一般是删除了几个备份版本之后的background process。而在primary storage中,GC通常是inline的。

]]>
+

Xia W, Jiang H, Feng D, et al. A comprehensive study of the past, present, and future of data deduplication[J]. Proceedings of the IEEE, 2016, 104(9): 1681-1710.

Data Reduction

一开始主要是一步步讲述了Data Deduplication这个概念提出的历程。

Compression

最一开始,都是用的压缩Compression。compression分为lossy和lossless(有损压缩和无损压缩),前者是通过去除一些不必要的信息来不可逆地减少数据大小(如JPEG图片压缩),后者是通过编码或者算术等方法可逆地减少数据大小(如GZIP、LZW等)。由于大规模存储系统(large-scale storage system)主要聚焦于无损压缩,因而下文也主要介绍这个。(deduplication也可以视为无损压缩的一种方法

entropy encoding

信息熵

提到压缩,就不得不提到信息熵。一个变量X的信息熵可以如下计算:

image-20231210221755122

比如说通过字符串abaaacabba,我们可以计算其所构成字母的信息熵:

image-20231210221846621

其实际含义是,对于“abaaacabba”这个上下文,{a, b, c}集合的每个字母至少需要1.295个bit来表示,也即字符串“abaaacabba”至少由12.95个bits来表示。也即,信息熵实际上是算出了压缩的极限

哈夫曼树

早期的压缩理论就是根据信息熵来的,这种我们称为“entropy encoding”或者“statistical-model-based coding”,因为它需要基于某个上下文(statistics)来计算信息熵。最常见的就是哈夫曼树,它用一个frequency-sorted binary tree来生成前缀编码,从而对信息进行压缩。

缺点

然而,显而易见的是这种entropy encoding你首先就得有合适的statistics,这是不scalable的。所以它一般也不适用于现代的storage system的压缩要求。

dictionary-model-based coding

因而,“dictionary-model-based coding”就此浮出水面。它从string-level来识别重复数据,从而简化和加速了压缩。它的主要思想是通过滑动窗口识别重复字符串,并用位置和长度来替代这些重复的。(相当于是unique string只存储一次)代表性的是LZ压缩。

然而,它由于是string-level,所以需要对整个系统的所有string进行扫描,需要在compression ratio和speed之间trade off。

delta compression

它的提出是针对于小文件/相似chunk的。它的思想感觉有点类似密码学,大概是这样:

1
2
3
given file A,B
calc △ab,
我们就可以通过△ab和B来恢复出一个A

目前正在尝试把它纳入到deduplication system中。不过目前的瓶颈似乎是这样的,delta compression是要求要将当前chunk同base chunk进行对比,所以怎么找到base chunk就成了问题。

Deduplication

总之,在compression byte-by-byte识别redundant data这样粒度太小的劣势下,通过计算“cryptographically secure hash-based fingerprints”来识别redundant data的chunk-level的deduplication优势就来了!

Overview

image-20231210223322234

这里也是给了一张很棒的图来总结了上文。

Key Features

这个部分大概是说key features有两个,一个是chunking,另一个是fingerprinting

chunking有两种方法,fixed-size和variable-size,前者会出现boundary-shift问题,后者更加泛用。

fingerprinting的主流方法还是基于SHA1(现在也用SHA256了)【Cryptographically Secure Hash-Based Fingerprinting】,主要是讨论了它哈希碰撞的可能性很小所以使用安全,还有就是讨论了fingerprint的特性:

  1. 很难找到两个不同msg指纹相同

  2. 很难从fp倒推出一个msg

Basic Workflow

A typical data deduplication system follows the workflow of:

  1. chunking
  2. fingerprinting
  3. indexing
  4. further compression
  5. storage management
    1. data restore
    2. garbage collection
    3. fragment elimination
    4. reliability
    5. security

Deduplication

In this section, we examine the state-of-the-art works on data deduplication in sufficient depth to understand their key and distinguishing features.

本节终于要开始对deduplication的关键技术做详尽的介绍和讨论了。

A.Chunking

这部分确实如他所言主要介绍了chunking。它先是介绍了主流的CDC算法Rabin(具体在FastCDC那篇文章介绍过这部分了,这里就不再赘述),然后讲述了Rabin算法的三个主要缺点:chunk size方差大、计算量大、去重检测还不够精确。

针对这三个缺点,分别有各种文献提出了这几类关键技术(顺序与缺点一一对应):

  1. Reducing Chunk Size Variance by Imposing Limits on MAX/MIN Chunk Sizes for CDC

    当chunk size过大,虽然会加速后续的indexing等步骤,减少space消耗,但是会影响去重率;chunk size过小,虽然会增加去重率,但是会增大后续indexing等步骤的工作量。这又是一个trade-off。

    这里主要介绍了各个主流方法都是怎么限制chunk size的,比如说LBFS简单粗暴,还有别的什么依据极值、非对称滑动窗口等做法。感觉还是FastCDC那个做法更加灵活聪明。

  2. Reducing Computation to Accelerate the Chunking Process

    也是有比如说Gear等等算法或者硬件层面上的改进。

  3. Improving Duplicate-Detection Accuracy by Rechunking Nonduplicate Chunks

    这个问题也是比较普遍,比如如图所示的C2和C5之间就可以再做进一步去重:

    image-20231210235904637

    具体方法有比如说频率分析法选定某些频繁访问的chunk进行rechunking、把几个小的nonduplicate chunk给merge为一个大的然后rechunking等等。

    这个可能对网络场景也有适用,毕竟网络传输也就主要是通过一个个很小的network package数据包。

  4. Impact of Interspersed Metadata

    主要是说如果数据集内meta data和主要数据混在一起可能干扰去重,比如说block header、还有tar打包后产生的文件的包含时间戳等信息的file header等等。

    解决方法大概就是预处理之类的。

B. Accelerate Computational Tasks

这个部分大概就是提出了两种方法,一个是通过将deduplication system给pipeline了(就是Odess那个做法),然后再结合multithreading来对它进行多线程加速;另一个就是通过开发GPU相关库来对deduplication做支持,从而使用GPGPU架构来进行硬件加速。

image-20231211141021880

C. Indexing

image-20231211141035597

不知道这个indexing是不是就是我们pipeline中的dedup阶段,感觉是的。

这个阶段面临的问题就是,数据量太大,导致指纹量也很大装不进内存,也就是说可能得根据磁盘中的指纹进行快速的索引。

indexing大致有两种思路,一个是精准的indexing,另一个就是命中率较低但内存占用也低的indexing。感觉capping有可能也有点后者的感觉()

然后目前流行的也是有四类方法:locality-based, similarity-based, flash-assisted, and cluster deduplication approaches。

  1. locality-based

    大概意思就是说利用数据的局部性,每次要某个指纹不是只读它一个,而是顺带把磁盘中这个指纹后面几个也读进内存,磁盘中的也是按照数据局部性存储的。

    除此之外,DDFS结合Bloom filter使用来精准检测重复。

    A Bloom filter [22] is a space-efficient data structure that uses a bit array with several independent hash functions to represent membership of a set of items (e.g., fingerprints).

    而Sparse indexing则采取“抽样”的方式。

    这个一般用于提高performance。

  2. similarity-based

    最常见的方法是用一个fp set的最大值or最小值来表示一个file,然后对这个建立一个主索引。如果两个文件的代表fp相同,那么这两个文件很有可能重复读极高。

    它这里提到了一个比较值得思考的观点:locality-based是利用了physical-locality,similarity-based是利用了logical-locality。前者还是比较容易理解的,因为它要求磁盘中的指纹按局部性存储,后者我是真没明白。。。之后有兴趣再看看相关论文了解一下吧。

    这个一般用于reduce RAM overhead。

  3. flash-assisted

    感觉这个没啥特别的,相当于换了个闪存介质而不是磁盘来存储index。

  4. Cluster Deduplication

    这个相当于加了层分布式,将输入的数据流分成几个种类(比如说按前缀分)然后送到多个结点上并行地进行去重处理,然后每个结点内部又可以用别的算法了之类的。这就需要涉及到负载均衡、路由算法等等了。

    缺点是可能降低deduplication ratio(可能是因为一些路由算法实现?)。

D. Post-Deduplication Compression

image-20231212224322111

不过即便如此,一个chunk内可能还是有一些小地方是dunplicate的(internal redundancy),这时候压缩就大有用处了。并且多个chunk一起压缩,比单个单个压缩的压缩率更高。

这个还适用于上面说到的一种情况,也即那个用到rechunk的地方,完全可以用delta compression来代替,而且感觉后者可能还更通用()感觉被薄纱。

主要面临的挑战来自于这几个方面:resemblance detection, reading base chunks, and delta encoding。

  1. resemblance detection

    目前大概有这几种方法:

    1. Manber:计算polynomial-based fingerprints,两个文件的相似性取决于它们相同的这个fp的数量。
    2. superfeature:抽样选取一些Rabin fp作为feature,并把它们合起来成为一个大的superfeature,对这个东西进行index。这个好像应用比较广泛。
    3. TAPER:每个file都是一个bloom filter,比较filter相同的bit位数。
  2. delta encoding

  3. Additional Delta Compression Challenges

E. Data Restore

这里笔墨最多的还是在说碎片化问题,同时也简要介绍了去重系统的三个主要应用场景:primary storage、backup storage、cloud storage,以及碎片化问题给它们的薄弱方面的狠狠一击()

  1. primary storage

    它最主要的问题还是IO-sensitive

  2. backup storage

    它最主要的问题是随着备份版本增多碎片化问题的愈发严重

  3. cloud storage

    它最主要的问题是速度,其受网络带宽、碎片化的限制。

F. Garbage Collection

这部分也大概是讲了GC常见的两种方法,一个是reference count,另一个是mark & sweep。

前者需要inline地维护refcnt,后者则可以offline运行。

并且在backup system中,GC一般是删除了几个备份版本之后的background process。而在primary storage中,GC通常是inline的。

]]>
@@ -157,7 +170,7 @@ /2023/12/10/deduplication_system_articles/ -

各个超链接导向对应的文章分链接。

Deduplication

综述

Indexing

sparce indexing

基于chunks的去重都要求使用full index,而这RAM一般承受不起,但是纯用disk io就太慢了。所以它利用了数据局部性:

If two pieces of backup streams share any chunks, they are likely to share many chunks. 如果两个segment共享了某个chunk,那么它们很有可能共享很多chunks。

是这样的流程:

  1. 分段为segment;
  2. 计算该segment的每个chunk的fp,然后对每个chunk查询其对应的sparce indexing table: <fp, segment_id>,记录可能跟它共享很多chunk的segment的segment_id;
  3. 读取这些segment_id对应的segment的chunk indexing table(存储在disk中);
  4. for every chunks: 重复,copy entry ;不重复,add to new container
  5. 最后再将该segment的信息写入磁盘,填写sparce indexing表。

而sparce indexing表最一开始,由对input segment进行chunks的随机抽样得出(或者逐渐构建起来,反正大概是这个意思)

可以看到,它将segment info保留在disk中,在RAM中只保留fp2seg_id的映射,每次只需简单从磁盘中读取几个segment info即可,利用数据局部性极大地降低了磁盘IO次数。

Odess采用的就是类似这种capping+sparce indexing的方法。

将sparce indexing从原来的<fp, seg_id>改为<fp, cid>,并且每次只取top T个包含sample chunks最多的容器,从而将对segment进行cap修改为对container进行cap。仔细想想,这样确实依然保证了原算法的核心思想,也属于是segment size = container size的特种了。

不这个<fp, cid>不就是Odess中的recipe(或者说是全局指纹表)吗?乐。Odess也确实体现了这种capping+sparce indexing结合的方法【只不过进行简化了,每个chunk固定取其第一个container】。

Chunking

FastCDC

Fragment

data layout

MFDedup

有机会可以再看看代码实现。

rewrite

capping

这篇文章的测试做得很友好很完善,值得精读。

对stream进行分段为segment;限制每个版本的容器数(主要是指引用的旧容器数);将那些包含重复块rate较小的容器所包含的重复块视为unique block进行rewrite。

SMR && DePFC

非常impressive的两个方法

Restore

cache

recipe

forward-assembly

OdessStorage

GC

]]>
+

各个超链接导向对应的文章分链接。

综述

Indexing

Chunking

FastCDC

Fragment

data layout

MFDedup

Rewriting

GC

]]>
@@ -230,6 +243,19 @@ + + Indexing + + /2023/11/11/Indexing/ + + sparce indexing

基于chunks的去重都要求使用full index,而这RAM一般承受不起,但是纯用disk io就太慢了。所以它利用了数据局部性:

If two pieces of backup streams share any chunks, they are likely to share many chunks. 如果两个segment共享了某个chunk,那么它们很有可能共享很多chunks。

是这样的流程:

  1. 分段为segment;
  2. 计算该segment的每个chunk的fp,然后对每个chunk查询其对应的sparce indexing table: <fp, segment_id>,记录可能跟它共享很多chunk的segment的segment_id;
  3. 读取这些segment_id对应的segment的chunk indexing table(存储在disk中);
  4. for every chunks: 重复,copy entry ;不重复,add to new container
  5. 最后再将该segment的信息写入磁盘,填写sparce indexing表。

而sparce indexing表最一开始,由对input segment进行chunks的随机抽样得出(或者逐渐构建起来,反正大概是这个意思)

可以看到,它将segment info保留在disk中,在RAM中只保留fp2seg_id的映射,每次只需简单从磁盘中读取几个segment info即可,利用数据局部性极大地降低了磁盘IO次数。

Odess采用的就是类似这种capping+sparce indexing的方法。

将sparce indexing从原来的<fp, seg_id>改为<fp, cid>,并且每次只取top T个包含sample chunks最多的容器,从而将对segment进行cap修改为对container进行cap。仔细想想,这样确实依然保证了原算法的核心思想,也属于是segment size = container size的特种了。

不这个<fp, cid>不就是Odess中的recipe(或者说是全局指纹表)吗?乐。Odess也确实体现了这种capping+sparce indexing结合的方法【只不过进行简化了,每个chunk固定取其第一个container】。

]]>
+ + + +
+ + + 驱动开发小记