1 个不稳定版本
0.0.1 | 2022年7月25日 |
---|
#20 in #面向对象
15KB
opencache
Opencache 是一个在单个节点模式下运行的 Rust 缓存服务器,是一个面向对象的存储,提供两个 API:范围 write
和范围 read
以及后台自动淘汰。
缓存集群管理在客户端。不支持回溯,应用程序必须显式将对象写入其中。
协议
- http2 PUT GET
- (可能) aws-s3
- (可能) grpc stream
存储模型
CacheServer 将对象(由唯一的 utf-8 字符串键标识的字节流)存储在块中。属于同一对象的块具有相同的大小。
-
在读取对象时,如果指定的范围与块大小不匹配,则可能返回第一个和最后一个块的部分。
-
在写入对象时,第一个和最后一个未对齐的字节将被丢弃。因为 opencache 以固定大小的块存储对象内容。
可选地,可以在添加对象时(第一次写入)指定块大小,并且不会更改。
块
Chunk
几乎就是一个字节切片。
块大小可以由应用程序指定(在第一次写入对象时)或由 opencache 服务器决定。默认块大小是
default_chunk_size = max(min(file_size/64, 2M),64K)
块组
不同大小的块存储在不同的块组(ChunkGroup
)中,例如。
(0,4KB]
(4KB,1MB]
(1MB,8MB]
(8MB, +oo]
Opencache 有几个预定义的块组,对象的实际块大小是小于或等于指定大小的最小预定义块大小。
对象
Opencache 中的对象包括对象头和几个块。
头包含 JSON
格式的应用程序元数据和内部元数据。内部元数据包括
- 对象添加的时间,
- 总大小,
- 块大小,
- 存储的块的位图
- 每个存储块的块 ID。
.------------.
| object |
'------------'
| |
| '-------------.
| header |
| |
v |
.-----------------. | .----------.
| json-meta | +------->| chunk-1 |
| size | | '----------'
| chunk-bitmap | |
| chunk-ids | | .----------.
'-----------------' '------->| chunk-2 |
'----------'
Opencache 以 <key, header>
的持久映射存储对象。该映射将在服务器启动时加载到内存中。
数据类型
块组 ID
ChunkGroupId
是文件名中的 2 个 ASCII 字符,或在内存中的 1 个字节。 ChunkGroupId
是其中块大小的编码表示,类似于浮点数的表示。
第一个字符(或 4 个高位)是 power
。第二个字符(或 4 个低位)是 significand
。
power significand
ascii 0 1
bits 4 higher bits 4 lower bits
块大小计算如下:significand * 16^power
例如,48K
块组 ID 为 3c
:12 * 16^3 = 48K // c=12
;
最大块组 ID 是 ff
:15 * 16^15 = 15E
以下列出了一些 ChunkGroupId 的示例
01: 2 * 16^0 = 2 08: 8 * 16^0 = 8
11: 2 * 16^1 = 32 18: 8 * 16^1 = 128
21: 2 * 16^2 = 512 28: 8 * 16^2 = 2K
31: 2 * 16^3 = 8K 38: 8 * 16^3 = 32K
41: 2 * 16^4 = 128K 48: 8 * 16^4 = 512K
51: 2 * 16^5 = 2M 58: 8 * 16^5 = 8M
61: 2 * 16^6 = 32M 68: 8 * 16^6 = 128M
71: 2 * 16^7 = 512M 78: 8 * 16^7 = 2G
块 ID
ChunkId
是一个 u64
,其中最高位字节是 ChunkGroupId
,其他 7 个字节是 54 位整数中的单增量索引。
<ChunkGroupId> <index>
byte: [0] [1, 7]
键
键是一个任意的 utf-8 字符串。
访问条目
缓存替换算法依赖于访问日志来决定应该淘汰哪个对象/块。
可以是 Key
或 ChunkId
,分别用于追踪对象访问或块访问。
存储布局
Opencache 磁盘存储包括
- 描述所有其他磁盘文件的清单文件。
- 键文件用于存储对象键和对象头。
- 块文件用于存储缓存文件数据。
清单文件
清单是当前存储的快照。即,它是一个属于此快照的文件列表。
data_ver: 0.1.0
key_meta_ver: 0.2.0
keys:
key-wal-1, key-wal-2...
key-compacted-1, key-compacted-2...
access:
acc-1, acc-2...
chunk-groups:
CG-1, CG-2...
chunks:
chunk-id-1, chunk-id-2...
对象存储
对象存储是一组文件,用于持久化对象映射。它包括一个活动 WAL 文件和几个压缩文件。
对象头包括
- 此记录被添加时的时间戳。
- 一个标志,指示它是添加操作还是删除操作。
- 此对象的总大小。
- JSON 格式的用户元数据。
- 此对象分配的块组(CG)。
- 存储的块的
ChunkId
列表。在此列表中不需要存储块组。因为一个对象中的每个块都属于同一个块组。
块存储
对象内容以块的形式存储。相同大小的块属于同一个块组。
每个块组由 一个 活动块文件(可写)和几个只读块文件组成。活动块文件是追加只读。
删除是通过另一个按块文件实现的,即 ChunkIndex
(其中逐个追加块索引条目)。
-
对于活动块,ChunkIndex 只包含已删除的块索引:
(chunk_id, REMOVE)
;因为块文件和 ChunkIndex 文件在更新时没有进行fsync
。重启时,当前块 ID 从块文件中加载,而删除的块 ID 从 ChunkIndex 文件中加载。
-
对于关闭的块,ChunkIndex 包含当前块 ID 和删除的块 ID(一个块在关闭(压缩)后被删除):
(chunk_id, ADD)
或(chunk_id, REMOVE)
。当服务器重启时,块索引仅从 ChunkIndex 文件中加载。
访问存储
Opencache 通过访问模式淘汰对象或块。因此,最近访问的顺序必须持久化。否则,当缓存服务器重启时,由于缺乏访问信息,缓存数据将被意外淘汰。
数据访问被跟踪在两个类别中:按键和按块 ID。
数据访问存储在多个仅追加文件中,分别包含键和块-id。
旧访问数据将定期被删除。访问数据不需要压缩。
.----------.
| Manifest |
'----------'
Access Access Keys ChunkGroup-i ChunkGroup-j ..
Key Chunk
.----. .----. Active Active
|key1| |cid1| .---------. .----. Chunk ..
|key2| |cid2| | k1,meta | | c1 | Index
| .. | | .. | | k2,meta | .-|-c2 | .----.
|CKSM| |CKSM| | .. | | | .. |<-|cid5|
|key3| |cid3| | CKSM | | | .. | |cid6|
| .. | | .. | | k3,meta | | '----' | .. |
'----' '----' | .. | | .. '----'
.. .. '---------' |
.----. .----. .. |
|keyi| |cid1| |
|keyj| |cid2| Compacted Keys | Compacted Chunks
| .. | | .. | .---------. |
|CKSM| |CKSM| | k1,meta | | .----. Chunk
|keyk| |cid3| | k2,meta | | | c1 | Index
| .. | | .. | | .. | +-|-c2 | .----.
'----' '----' | CKSM | | | .. |<-|cid5|
| k3,meta | | | .. | |cid6|
| .. | | | '----' | .. |
'---------' | '----'
| '---.
v v
KeyMeta Chunk
.----------. .----.
| ts | |data|
| add|rm | |CHSM|
| size | '----'
| userdata |
| CG |
| chunkids |
'----------'
操作
写入
缓存服务器不需要严格的数据持久性,因此当新数据(键、块或访问数据)写入时,不需要立即执行fsync。
不同类型的数据的fsync策略如下
-
键:键文件每写入
k MB
(其中k
是一个可配置的参数)时执行fsync。在fsync之前必须写入校验和记录。因为磁盘上的数据仍然需要内部一致性。校验和记录包含k MB
数据的校验和,但不包括其自身。换句话说,键文件被分割成几个
k MB
段。 -
访问数据与键文件类似。
-
块不同:每个块包含其自身的校验和。块文件定期执行fsync。
更新清单
写入操作不会更新Manifest
文件。Manifest
仅在文件(键存储文件、块存储文件或访问存储文件)添加或删除(或配置更改)时替换。例如,当打开新的键文件或执行块文件的压缩时。
清单文件的命名采用单调递增整数,例如:manifest-000021
。
清单文件只能添加或删除。在重启时,opencache列出所有清单文件并使用最后一个有效的文件(通过检查校验和)。
内存布局
键和访问数据存储在内存中以快速访问。块可以选择性地缓存在内存中,但没有内存中的块缓存也可以。
内存中的键只是一个HashMap:ObjectMap
。
键访问数据被输入到key-eviction-algorithm。块访问数据被输入到chunk-eviction-algorithm。
当缓存服务器重启时:所有键都加载到内存中。所有访问数据都通过两个淘汰算法实现重新播放。
缓存淘汰
对象和块独立淘汰。
规则是:如果访问了一个块,则它所属的对象也被访问了。这样,如果还有块访问,对象就不太可能被淘汰。
-
可能的情况是,一个块被淘汰,而其他块仍然驻留在CacheServer中。
-
可能的情况是,一个对象驻留在CacheServer中,但其所有块都被淘汰。
-
可能的情况是,一个对象被淘汰,但仍有孤立的块。这些块最终将被淘汰,因为将不会有任何访问。
缓存淘汰策略是LIRS
的修改和简化版本。
API
块API
ChunkStorage提供块访问
trait ChunkStorage {
fn add(&mut self, chunk_id, bytes);
fn remove(&mut self, chunk_id);
fn read(&mut self, chunk_id) -> Stream<>
}
对象API
ObjectStorage提供对象访问
trait ObjectStorage {
fn add(&mut self, key: String, size, meta, prefered_chunck_size);
fn remove(&mut self, key)
fn access(&mut self, key)
}
CacheServer API
服务器级API
trait Cache {
/// Write chunks to an object.
/// Nonexistent object will added implicitly
fn write(&mut self, key, chunks)
/// Read specified `range` of bytes in an object
fn read(&mut self, key, range)
}
客户端
客户端配置版本控制:如果新的缓存节点C
被添加到缓存集群{A,B}
,并且假设缓存客户端使用一致哈希,当新的集群配置{A,B,C}
发布到每个客户端时,1/3的数据访问将遇到失败。
为了平滑升级集群配置,客户端需要使用两个配置来访问缓存集群:首先从新集群读取,如果发生缺失,则读取旧集群,直到数据迁移完成。在此期间,有两个活动配置:{{A,B},ver=1}
和{{A,B,C},ver=2}
。
也可能存在超过两个活动配置的情况。
因此,对于缓存客户端,它必须
- 能够实时升级集群配置,
- 支持一个联合配置,
- 并且能够重试每个配置。
集群升级可能看起来像这样
- 最初,每个缓存客户端都有配置
[{{A,B},ver=1}]
; - 添加了一个新的缓存节点,并将新的配置发布给每个客户端:
[{{A,B},ver=1}, {{A,B,C},ver=2}]
; - 过了一会儿,ver=1的配置被移除,并将另一个新的配置发布给每个客户端:
[{{A,B,C},ver=2}]
。
注意事项
一致性关注
缓存与后端存储存在一致性问题时,即如果在后端存储中更改了一个对象,在缓存侧无法提供强一致视图。除非在存储和缓存上运行分布式共识协议(这很昂贵)。
因此,建议只存储不会更改的对象,即在后端,对象只会被添加和删除,永远不会被修改。
空间管理
建议将缓存空间限制设置为磁盘的80%以下,因为为了减少I/O,删除的块的空间将不会在下一个compaction
之前回收。