Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMake/Findphoton.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ set(FETCHCONTENT_QUIET false)
set(PHOTON_ENABLE_EXTFS ON CACHE BOOL "Build Photon extfs support" FORCE)
set(PHOTON_BUILD_OCF_CACHE ON CACHE BOOL "Build Photon OCF cache support" FORCE)
set(PHOTON_ENABLE_URING ON CACHE BOOL "Build Photon io_uring support" FORCE)
# Photon must link the liburing ublksrv uses (Finduring.cmake), never a private copy.
set(PHOTON_URING_SOURCE "" CACHE STRING "Photon's own liburing source, unused" FORCE)

if(NOT ORIGIN_EXT2FS)
set(PHOTON_ENABLE_RESIZE ON CACHE BOOL "Build Photon extfs resize support" FORCE)
Expand Down Expand Up @@ -70,4 +72,7 @@ if(NOT ORIGIN_EXT2FS)
add_dependencies(photon_obj libext2fs)
endif()

# The io_uring wrapper includes headers that liburing's configure generates.
add_dependencies(photon_obj liburing_build)

set(PHOTON_INCLUDE_DIR ${photon_SOURCE_DIR}/include/)
12 changes: 12 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ Overlaybd achieves fast container startup through the following key designs:
without copy-up. Actually writes to overlaybd are pure writes - no lower-layer data
is ever copied on write. ~~CoW~~

- **Userspace conversion**: overlaybd integrates
[erofs-utils](https://github.com/erofs/erofs-utils) and
[e2fsprogs](https://github.com/tytso/e2fsprogs) to build **erofs** and **ext4** layers entirely in
userspace — an OCI tar layer is turned into a block layer directly, with no kernel mount and
no loop device in the conversion path.

**For ext4 the layout is optimized further**, all file data is placed ahead of the ext4 metadata,
so metadata reads concentrate in a small region at the end of the layer instead of being scattered
across it. On a 182-layer benchmark this cuts the unique 1 MiB coverage of a metadata-heavy
workload by over 95%, which directly reduces how much data on-demand fetching has to pull.
See [data-layout](apply-data-layout.md) for details.

- **Seekable compression (ZFile)**: Random reads decompress only the requested data range,
not the entire compressed block. Because compressed data is smaller, the actual I/O
transfer is reduced enough that compressed random reads are often *faster* than
Expand Down
58 changes: 58 additions & 0 deletions docs/apply-data-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# overlaybd Data Layout Optimization (EXT4)

[简体中文](apply-data-layout_zh.md)

`overlaybd-apply` converts an OCI tar layer into an overlaybd layer via **e2fsprogs**. Rearranging where the file data payload and the ext4 metadata land inside the layer substantially improves the locality of metadata access.

## New layout

```text
old: | Header | data | fsmeta | data | fsmeta copy | data | ... | index | Trailer |

new: | Header 4 KiB | payload: 4 KiB aligned per file | fsmeta: live metadata | index | Trailer 4 KiB |
[4096, payload_end) [payload_end, index_offset)
```

- **payload**: the data of every regular file, staged in tar order; each non-empty file starts at a 4 KiB aligned offset and occupies a 4 KiB aligned length with the tail zeroed. Empty files take no space.
- **fsmeta**: the metadata blocks that are live at the end, appended compactly in ascending logical order, with no duplicate copies.
- **index / Trailer**: appended in place by `overlaybd-commit --seal`; the data is never rearranged.

The result is still a standard sealed LSMT — no new format version, no new tag, no external payload file.

## How it works

```text
tar stream → append payload sequentially (aligned per file, no mapping yet) + temp tar headers (recording payload offsets)
temp headers → ext4 replays metadata only → FsMetaFile (sparse file, original logical offsets)
└─ regular files: fallocate + FIEMAP → pending ext4-block-to-payload mappings
close ext4 → drop payload blocks → append live fsmeta compactly → install full-block payload mappings
commit --seal → merge adjacent mappings, append index and Trailer in place
```

## Measured results

Converted 10 SWE-benchmark images into overlaybd images with a 64GB block size, 182 layers in total. After mounting, `du`/`find` were run to collect and compare the distribution of accessed ranges between the old and the new layout:

| Metric (du+find, MB is decimal) | Old | New | Change |
|---|---:|---:|---:|
| LSMT requested MB | 1949.475 | 1948.631 | nearly unchanged |
| LSMT bytes in the second half | 63.20% | 99.92% | more concentrated |
| LSMT position p5–p95 span | 90.82% | 6.38% | more concentrated |
| LSMT 1 MiB blocks touched | 12851 | 1276 | −90.07% |
| ZFile local requested MB | 110.352 | 103.290 | −6.40% |
| ZFile unique 4 KiB coverage MB | 436.945 | 56.717 | −87.02% |
| ZFile unique 1 MiB coverage MB | 6299.222 | 271.553 | −95.69% |
| Sealed total MB | 44574.161 | 32020.957 | −28.16% |
| ZFile total MB | 17818.462 | 17192.135 | −3.52% |

- **Access is concentrated**: in the new layout 99.95% of the R requested bytes fall in the fsmeta region, which is only 5.80% of the data area; in compressed space the range corresponding to fsmeta accounts for 99.45% of the bytes read.
- **Coverage shrinks sharply at coarser fetch granularities**: requested bytes drop by only 6.40%, while unique coverage aligned to 4 KiB / 1 MiB drops by 87.02% / 95.69%.

## Related code

| File | Contents |
|---|---|
| `src/tools/overlaybd-apply.cpp` | path selection and orchestration |
| `src/overlaybd/tar/libtar.cpp` | stage_tar, extract_staged, apply_pending; per-file alignment and zeroed tails |
| `src/overlaybd/tar/fsmeta_file.{h,cpp}` | sparse fsmeta staging, compact append of live blocks |
| `src/overlaybd/lsmt/file.cpp` | staged RemoteData mappings, close_seal and index merging |
58 changes: 58 additions & 0 deletions docs/apply-data-layout_zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# overlaybd 数据布局优化(EXT4)

[English](apply-data-layout.md)

`overlaybd-apply` 通过 **e2fsprogs** 把 OCI tar 层转换为 overlaybd 层, 通过调整文件数据 payload 和 ext4 元数据在layer中的布局,可有效改善元数据访问的局部性。

## 新布局

```text
旧:| Header | 数据 | fsmeta | 数据 | fsmeta 副本 | 数据 | ... | index | Trailer |

新:| Header 4 KiB | payload:逐文件 4 KiB 对齐 | fsmeta:有效元数据 | index | Trailer 4 KiB |
[4096, payload_end) [payload_end, index_offset)
```

- **payload**:按 tar 顺序暂存所有普通文件的数据;每个非空文件起点和占用长度均按 4 KiB 对齐,尾部补零,空文件不占空间。
- **fsmeta**:最终有效的元数据块,按逻辑地址升序紧凑追加,无重复副本。
- **index / Trailer**:`overlaybd-commit --seal` 原地追加,数据不重排。

结果仍是标准 sealed LSMT,没有新格式版本、tag 或外部 payload 文件。

## 基本原理

```text
tar 流 → 顺序追加 payload(逐文件对齐,不建映射)+ 临时 tar 头部(记录 payload 偏移)
临时头部 → ext4 只回放元数据 → FsMetaFile(稀疏文件,原逻辑偏移)
└─ 普通文件:fallocate + FIEMAP → 待安装 ext4 块到 payload 的映射
关闭 ext4 → 剔除 payload 块 → 有效 fsmeta 紧凑追加 → 安装完整块 payload 映射
commit --seal → 合并相邻映射,原地追加 index 和 Trailer
```

## 收益实测

将 10 个 SWE评测镜像转换成大小为块大小为 64GB 的 overlaybd 镜像,共 182 层。挂载后跑 du/find,统计并对比新旧布局的访问区间分布:

| 指标(du+find,MB 为十进制) | 旧版 | 新版 | 变化 |
|---|---:|---:|---:|
| LSMT 请求 MB | 1949.475 | 1948.631 | 近乎不变 |
| LSMT 后半段字节占比 | 63.20% | 99.92% | 更集中 |
| LSMT 位置 p5–p95 跨度 | 90.82% | 6.38% | 更集中 |
| LSMT 触及 1 MiB 块 | 12851 | 1276 | −90.07% |
| ZFile 本地请求 MB | 110.352 | 103.290 | −6.40% |
| ZFile 唯一 4 KiB 覆盖 MB | 436.945 | 56.717 | −87.02% |
| ZFile 唯一 1 MiB 覆盖 MB | 6299.222 | 271.553 | −95.69% |
| sealed 合计 MB | 44574.161 | 32020.957 | −28.16% |
| ZFile 合计 MB | 17818.462 | 17192.135 | −3.52% |

- **访问集中**:新版 99.95% 的 R 请求字节落在 fsmeta 区,该区只占数据区 5.80%;压缩空间中 fsmeta 对应区间占读取字节 99.45%。
- **较粗回填粒度的覆盖明显减少**:请求字节仅下降 6.40%,对齐 4 KiB/1 MiB 的唯一覆盖分别下降 87.02%/95.69%。

## 相关代码

| 文件 | 内容 |
|---|---|
| `src/tools/overlaybd-apply.cpp` | 流程选择与编排 |
| `src/overlaybd/tar/libtar.cpp` | stage_tar、extract_staged、apply_pending;逐文件对齐与零尾 |
| `src/overlaybd/tar/fsmeta_file.{h,cpp}` | 稀疏 fsmeta 暂存、有效块紧凑追加 |
| `src/overlaybd/lsmt/file.cpp` | staged RemoteData 映射、close_seal 与索引合并 |
75 changes: 58 additions & 17 deletions src/overlaybd/lsmt/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ class LSMTReadOnlyFile : public IFileRW {
}

UNIMPLEMENTED(int restack(IFileRW *upper) override);
UNIMPLEMENTED(int can_map_staged_data() const override);
};

// The append-only mapping log in the index file of a writable layer. A mapping is
Expand Down Expand Up @@ -977,13 +978,62 @@ class LSMTFile : public LSMTReadOnlyFile {
}
return 0;
}
if (request == RemoteData) {
return map_staged_data(va_arg(args, RemoteMapping));
}
if (request != Index_Group_Commit)
LOG_ERROR_RETURN(EINVAL, -1, "invaid request code");

auto buffer_size = va_arg(args, size_t);
return m_log ? m_log->set_group_commit(buffer_size) : 0;
}

virtual int can_map_staged_data() const override {
if (m_rw_type == RWType::Append)
return 0;
errno = ENOTSUP;
return -1;
}

// maps [offset, +count) of the virtual file to moffset of the data file of tag
int insert_mappings(off_t offset, uint32_t count, off_t moffset, uint8_t tag) {
if (offset < 0 || moffset < 0 || !is_aligned((uint64_t)offset | count | (uint64_t)moffset))
LOG_ERROR_RETURN(EINVAL, -1, "unaligned mapping {offset: `, count: `, moffset: `}",
offset, count, moffset);
size_t nwrite = 0;
while (count > 0) {
auto length = min(count / ALIGNMENT, (uint32_t)Segment::MAX_LENGTH);
SegmentMapping m((uint64_t)offset / ALIGNMENT, length,
(uint64_t)moffset / ALIGNMENT, tag);
LOG_DEBUG("insert segment: `", m);
if (commit_mapping(m, [&] { index_insert(m); }) != 0)
return -1;
auto bytes = length * ALIGNMENT;
nwrite += bytes;
offset += bytes;
count -= bytes;
moffset += bytes;
}
return nwrite;
}

// maps [lba.offset, +lba.count) of the virtual file to the data staged at lba.roffset
int map_staged_data(RemoteMapping lba) {
if (can_map_staged_data() != 0)
LOG_ERRNO_RETURN(0, -1, "the layer cannot map staged data");
Lock lock(m_rw_mtx);
struct stat st;
if (m_files[m_rw_tag]->fstat(&st) != 0)
LOG_ERRNO_RETURN(0, -1, "failed to stat the upper data file");
if (lba.roffset < (off_t)HeaderTrailer::SPACE || lba.roffset > st.st_size ||
lba.count > (uint64_t)(st.st_size - lba.roffset))
LOG_ERROR_RETURN(EINVAL, -1,
"mapping {offset: `, count: `, roffset: `} out of data [`, `)",
lba.offset, lba.count, lba.roffset, (uint64_t)HeaderTrailer::SPACE,
st.st_size);
return insert_mappings(lba.offset, lba.count, lba.roffset, m_rw_tag);
}

virtual int init_concurrency() {
if (m_init_concurrency)
return 0;
Expand Down Expand Up @@ -1241,9 +1291,12 @@ class LSMTFile : public LSMTReadOnlyFile {
mapping.reset(m_index0->dump(ALIGNMENT));
index_size = m_index0->size();
}
index_size = compress_raw_index(mapping.get(), index_size);
uint64_t index_offset = m_files[m_rw_tag]->lseek(0, SEEK_END);
ssize_t index_bytes = index_size * sizeof(SegmentMapping);
index_bytes = (index_bytes + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
std::fill(mapping.get() + index_size, mapping.get() + index_bytes / sizeof(SegmentMapping),
SegmentMapping::invalid_mapping());
auto ret = m_files[m_rw_tag]->pwrite(mapping.get(), index_bytes, index_offset);
if (ret < index_bytes)
LOG_ERRNO_RETURN(0, -1, "failed to write index.");
Expand Down Expand Up @@ -1484,6 +1537,7 @@ class LSMTSparseFile : public LSMTFile {
return 0;
}

UNIMPLEMENTED(int can_map_staged_data() const override);
};

class LSMTWarpFile : public LSMTFile {
Expand Down Expand Up @@ -1530,25 +1584,12 @@ class LSMTWarpFile : public LSMTFile {
va_end(tmp);
LOG_DEBUG("RemoteMapping: {offset: `, count: `, roffset: `}", lba.offset, lba.count,
lba.roffset);
size_t nwrite = 0;
while (lba.count > 0) {
SegmentMapping m;
m.offset = lba.offset / ALIGNMENT;
m.length = (Segment::MAX_LENGTH < lba.count / ALIGNMENT ? Segment::MAX_LENGTH
: lba.count / ALIGNMENT);
m.moffset = lba.roffset / ALIGNMENT;
m.tag = m_rw_tag + (uint8_t)SegmentType::remoteData;
LOG_DEBUG("insert segment: `", m);
if (commit_mapping(m, [&] { index_insert(m); }) != 0)
return -1;
nwrite += m.length * ALIGNMENT;
lba.offset += m.length * ALIGNMENT;
lba.count -= m.length * ALIGNMENT;
lba.roffset += m.length * ALIGNMENT;
}
return nwrite;
return insert_mappings(lba.offset, lba.count, lba.roffset,
m_rw_tag + (uint8_t)SegmentType::remoteData);
}

UNIMPLEMENTED(int can_map_staged_data() const override);

size_t compact(CompactOptions &opts, size_t moffset, size_t &nindex) const {

auto dest_file = opts.commit_args->as;
Expand Down
3 changes: 3 additions & 0 deletions src/overlaybd/lsmt/file.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class IFileRW : public IFileRO {

// close_seal current RW layer (change it to RO layer) and re-stack with upper layer
virtual int restack(IFileRW *upper) = 0;

// returns 0 if RemoteData can map the data appended to the top data file, -1 otherwise
virtual int can_map_staged_data() const = 0;
};

enum class RWType : uint8_t {
Expand Down
Loading
Loading