Technical Documentation: deepseek-ai/3FS
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (`README.md`)
Fire-Flyer File System
The Fire-Flyer File System (3FS) is a high-performance distributed file system designed to address the challenges of AI training and inference workloads. It leverages modern SSDs and RDMA networks to provide a shared storage layer that simplifies development of distributed applications. Key features and benefits of 3FS include:
Performance and Usability
- Disaggregated Architecture Combines the throughput of thousands of SSDs and the network bandwidth of hundreds of storage nodes, enabling applications to access storage resource in a locality-oblivious manner.
- Strong Consistency Implements Chain Replication with Apportioned Queries (CRAQ) for strong consistency, making application code simple and easy to reason about.
- File Interfaces Develops stateless metadata services backed by a transactional key-value store (e.g., FoundationDB). The file interface is well known and used everywhere. There is no need to learn a new storage API.
Diverse Workloads
- Data Preparation Organizes outputs of data analytics pipelines into hierarchical directory structures and manages a large volume of intermediate outputs efficiently.
- Dataloaders Eliminates the need for prefetching or shuffling datasets by enabling random access to training samples across compute nodes.
- Checkpointing Supports high-throughput parallel checkpointing for large-scale training.
- KVCache for Inference Provides a cost-effective alternative to DRAM-based caching, offering high throughput and significantly larger capacity.
Documentation
Performance
1. Peak throughput
The following figure demonstrates the throughput of read stress test on a large 3FS cluster. This cluster consists of 180 storage nodes, each equipped with 2ร200Gbps InfiniBand NICs and sixteen 14TiB NVMe SSDs. Approximately 500+ client nodes were used for the read stress test, with each client node configured with 1x200Gbps InfiniBand NIC. The final aggregate read throughput reached approximately 6.6 TiB/s with background traffic from training jobs.
To benchmark 3FS, please use our fio engine for USRBIO.
2. GraySort
We evaluated smallpond using the GraySort benchmark, which measures sort performance on large-scale datasets. Our implementation adopts a two-phase approach: (1) partitioning data via shuffle using the prefix bits of keys, and (2) in-partition sorting. Both phases read/write data from/to 3FS.
The test cluster comprised 25 storage nodes (2 NUMA domains/node, 1 storage service/NUMA, 2ร400Gbps NICs/node) and 50 compute nodes (2 NUMA domains, 192 physical cores, 2.2 TiB RAM, and 1ร200 Gbps NIC/node). Sorting 110.5 TiB of data across 8,192 partitions completed in 30 minutes and 14 seconds, achieving an average throughput of 3.66 TiB/min.
3. KVCache
KVCache is a technique used to optimize the LLM inference process. It avoids redundant computations by caching the key and value vectors of previous tokens in the decoder layers.
The top figure demonstrates the read throughput of all KVCache clients (1ร400Gbps NIC/node), highlighting both peak and average values, with peak throughput reaching up to 40 GiB/s. The bottom figure presents the IOPS of removing ops from garbage collection (GC) during the same time period.
Check out source code
Clone 3FS repository from GitHub:
git clone https://github.com/deepseek-ai/3fsWhen deepseek-ai/3fs has been cloned to a local file system, run the
following commands to check out the submodules:
cd 3fs
git submodule update --init --recursive
./patches/apply.shInstall dependencies
Install dependencies:
# for Ubuntu 20.04.
apt install cmake libuv1-dev liblz4-dev liblzma-dev libdouble-conversion-dev libdwarf-dev libunwind-dev \
libaio-dev libgflags-dev libgoogle-glog-dev libgtest-dev libgmock-dev clang-format-14 clang-14 clang-tidy-14 lld-14 \
libgoogle-perftools-dev google-perftools libssl-dev libclang-rt-14-dev gcc-10 g++-10 libboost1.71-all-dev build-essential
# for Ubuntu 22.04.
apt install cmake libuv1-dev liblz4-dev liblzma-dev libdouble-conversion-dev libdwarf-dev libunwind-dev \
libaio-dev libgflags-dev libgoogle-glog-dev libgtest-dev libgmock-dev clang-format-14 clang-14 clang-tidy-14 lld-14 \
libgoogle-perftools-dev google-perftools libssl-dev gcc-12 g++-12 libboost-all-dev build-essential
# for openEuler 2403sp1
yum install cmake libuv-devel lz4-devel xz-devel double-conversion-devel libdwarf-devel libunwind-devel \
libaio-devel gflags-devel glog-devel gtest-devel gmock-devel clang-tools-extra clang lld \
gperftools-devel gperftools openssl-devel gcc gcc-c++ boost-devel
# for OpenCloudOS 9 and TencentOS 4
dnf install epol-release wget git meson cmake perl lld gcc gcc-c++ autoconf lz4 lz4-devel xz xz-devel \
double-conversion-devel libdwarf-devel libunwind-devel libaio-devel gflags-devel glog-devel \
libuv-devel gmock-devel gperftools gperftools-devel openssl-devel boost-static boost-devel mono-devel \
libevent-devel libibverbs-devel numactl-devel python3-develInstall other build prerequisites:
libfuse3.16.1 or newer version- FoundationDB 7.1 or newer version
- Rust toolchain: minimal 1.75.0, recommended 1.85.0 or newer version (latest stable version)
Build 3FS
Build 3FS in build folder:
# Replace <method> with 'g++10' or 'g++11' based on your environment
cmake -S . -B build \
-DCMAKE_CXX_COMPILER=clang++-14 -DCMAKE_C_COMPILER=clang-14 \
-DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DSHUFFLE_METHOD=<method>
cmake --build build -j 32Due to the historical use of std::shuffle, binaries compiled with different compiler versions (e.g., g++10 vs. g++11 +) may be incompatible (issue). To resolve this, you must explicitly specify -DSHUFFLE_METHOD during compilation to lock in a consistent shuffle algorithm:
- Existing Clusters: Use the method corresponding to the compiler version previously used to deploy the cluster (
g++10org++11). - New Clusters: You can choose either
g++10org++11. However, once the cluster is deployed, you must stay with the same configuration for all future builds to maintain compatibility.
Build 3FS use Docker
- For TencentOS-4:
docker pull docker.io/tencentos/tencentos4-deepseek3fs-build:latest - For OpenCloudOS-9:
docker pull docker.io/opencloudos/opencloudos9-deepseek3fs-build:latest
Run a test cluster
Follow instructions in setup guide to run a test cluster.
Report Issues
Please visit https://github.com/deepseek-ai/3fs/issues to report issues.
Overview (`deploy/README.md`)
3FS Setup Guide
This section provides a manual deployment guide for setting up a six-node cluster with the cluster ID stage.
Installation prerequisites
Hardware specifications
| Node | OS | IP | Memory | SSD | RDMA |
|---|---|---|---|---|---|
| meta | Ubuntu 22.04 | 192.168.1.1 | 128GB | - | RoCE |
| storage1 | Ubuntu 22.04 | 192.168.1.2 | 512GB | 14TB ร 16 | RoCE |
| storage2 | Ubuntu 22.04 | 192.168.1.3 | 512GB | 14TB ร 16 | RoCE |
| storage3 | Ubuntu 22.04 | 192.168.1.4 | 512GB | 14TB ร 16 | RoCE |
| storage4 | Ubuntu 22.04 | 192.168.1.5 | 512GB | 14TB ร 16 | RoCE |
| storage5 | Ubuntu 22.04 | 192.168.1.6 | 512GB | 14TB ร 16 | RoCE |
RDMA Configuration
- Assign IP addresses to RDMA NICs. Multiple RDMA NICs (InfiniBand or RoCE) are supported on each node.
- Check RDMA connectivity between nodes using
ib_write_bw.
Third-party dependencies
In production environment, it is recommended to install FoundationDB and ClickHouse on dedicated nodes.
| Service | Node |
|---|---|
| ClickHouse | meta |
| FoundationDB | meta |
FoundationDB
- Ensure that the version of FoundationDB client matches the server version, or copy the corresponding version of
libfdb_c.soto maintain compatibility.- Find the
fdb.clusterfile andlibfdb_c.soat/etc/foundationdb/fdb.cluster,/usr/lib/libfdb_c.soon nodes with FoundationDB installed.
Step 0: Build 3FS
Follow the instructions to build 3FS. Binaries can be found in build/bin.
Services and clients
The following steps show how to install 3FS services in /opt/3fs/bin and the config files in /opt/3fs/etc.
| Service | Binary | Config files | NodeID | Node |
|---|---|---|---|---|
| monitor | monitor_collector_main | monitor_collector_main.toml | - | meta |
| admin_cli | admin_cli | admin_cli.tomlfdb.cluster | - | metastorage1storage2storage3storage4storage5 |
| mgmtd | mgmtd_main | mgmtd_main_launcher.tomlmgmtd_main.tomlmgmtd_main_app.tomlfdb.cluster | 1 | meta |
| meta | meta_main | meta_main_launcher.tomlmeta_main.tomlmeta_main_app.tomlfdb.cluster | 100 | meta |
| storage | storage_main | storage_main_launcher.tomlstorage_main.tomlstorage_main_app.toml | 10001~10005 | storage1storage2storage3storage4storage5 |
| client | hf3fs_fuse_main | hf3fs_fuse_main_launcher.tomlhf3fs_fuse_main.toml | - | meta |
Step 1: Create ClickHouse tables for metrics
Import the SQL file into ClickHouse:
clickhouse-client -n < ~/3fs/deploy/sql/3fs-monitor.sqlStep 2: Monitor service
Install monitor_collector service on the meta node.
- Copy
monitor_collector_mainto/opt/3fs/binand config files to/opt/3fs/etc, and create log directory/var/log/3fs.bashmkdir -p /opt/3fs/{bin,etc} mkdir -p /var/log/3fs cp ~/3fs/build/bin/monitor_collector_main /opt/3fs/bin cp ~/3fs/configs/monitor_collector_main.toml /opt/3fs/etc - Update
monitor_collector_main.tomlto add a ClickHouse connection:toml[server.monitor_collector.reporter] type = 'clickhouse' [server.monitor_collector.reporter.clickhouse] db = '3fs' host = '<CH_HOST>' passwd = '<CH_PASSWD>' port = '<CH_PORT>' user = '<CH_USER>' - Start monitor service:bash
cp ~/3fs/deploy/systemd/monitor_collector_main.service /usr/lib/systemd/system systemctl start monitor_collector_main
Note that
- Multiple instances of monitor services can be deployed behind a virtual IP address to share the traffic.
- Other services communicate with the monitor service over a TCP connection.
Step 3: Admin client
Install admin_cli on all nodes.
- Copy
admin_clito/opt/3fs/binand config files to/opt/3fs/etc.bashmkdir -p /opt/3fs/{bin,etc} rsync -avz meta:~/3fs/build/bin/admin_cli /opt/3fs/bin rsync -avz meta:~/3fs/configs/admin_cli.toml /opt/3fs/etc rsync -avz meta:/etc/foundationdb/fdb.cluster /opt/3fs/etc - Update
admin_cli.tomlto setcluster_idandclusterFile:tomlcluster_id = "stage" [fdb] clusterFile = '/opt/3fs/etc/fdb.cluster'
The full help documentation for admin_cli can be displayed by running the following command:
/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml helpStep 4: Mgmtd service
Install mgmtd service on meta node.
Copy
mgmtd_mainto/opt/3fs/binand config files to/opt/3fs/etc.bashcp ~/3fs/build/bin/mgmtd_main /opt/3fs/bin cp ~/3fs/configs/{mgmtd_main.toml,mgmtd_main_launcher.toml,mgmtd_main_app.toml} /opt/3fs/etcUpdate config files:
- Set mgmtd
node_id = 1inmgmtd_main_app.toml. - Edit
mgmtd_main_launcher.tomlto set thecluster_idandclusterFile:
tomlcluster_id = "stage" [fdb] clusterFile = '/opt/3fs/etc/fdb.cluster'- Set monitor address in
mgmtd_main.toml:
toml[common.monitor.reporters.monitor_collector] remote_ip = "192.168.1.1:10000"- Set mgmtd
Initialize the cluster:
bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml "init-cluster --mgmtd /opt/3fs/etc/mgmtd_main.toml 1 1048576 16"The parameters of
admin_cli:1the chain table ID1048576the chunk size in bytes16the file strip size
Run
help init-clusterfor full documentation.Start mgmtd service:
bashcp ~/3fs/deploy/systemd/mgmtd_main.service /usr/lib/systemd/system systemctl start mgmtd_mainRun
list-nodescommand to check if the cluster has been successfully initialized:bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "list-nodes"
If multiple instances of mgmtd services deployed, one of the mgmtd services is elected as the primary; others are secondaries. Automatic failover occurs when the primary fails.
Step 5: Meta service
Install meta service on meta node.
- Copy
meta_mainto/opt/3fs/binand config files to/opt/3fs/etc.bashcp ~/3fs/build/bin/meta_main /opt/3fs/bin cp ~/3fs/configs/{meta_main_launcher.toml,meta_main.toml,meta_main_app.toml} /opt/3fs/etc - Update config files:
- Set meta
node_id = 100inmeta_main_app.toml. - Set
cluster_id,clusterFileand mgmtd address inmeta_main_launcher.toml:
tomlcluster_id = "stage" [mgmtd_client] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"]- Set mgmtd and monitor addresses in
meta_main.toml.
toml[server.mgmtd_client] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"] [common.monitor.reporters.monitor_collector] remote_ip = "192.168.1.1:10000" [server.fdb] clusterFile = '/opt/3fs/etc/fdb.cluster' - Set meta
- Config file of meta service is managed by mgmtd service. Use
admin_clito upload the config file to mgmtd:bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "set-config --type META --file /opt/3fs/etc/meta_main.toml" - Start meta service:bash
cp ~/3fs/deploy/systemd/meta_main.service /usr/lib/systemd/system systemctl start meta_main - Run
list-nodescommand to check if meta service has joined the cluster:bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "list-nodes"
If multiple instances of meta services deployed, meta requests will be evenly distributed to all instances.
Step 6: Storage service
Install storage service on storage node.
- Format the attached 16 SSDs as XFS and mount at
/storage/data{1..16}, then create data directories/storage/data{1..16}/3fsand log directory/var/log/3fs.bashmkdir -p /storage/data{1..16} mkdir -p /var/log/3fs for i in {1..16};do mkfs.xfs -L data${i} -s size=4096 /dev/nvme${i}n1;mount -o noatime,nodiratime -L data${i} /storage/data${i};done mkdir -p /storage/data{1..16}/3fs - Increase the max number of asynchronous aio requests:bash
sysctl -w fs.aio-max-nr=67108864 - Copy
storage_mainto/opt/3fs/binand config files to/opt/3fs/etc.bashrsync -avz meta:~/3fs/build/bin/storage_main /opt/3fs/bin rsync -avz meta:~/3fs/configs/{storage_main_launcher.toml,storage_main.toml,storage_main_app.toml} /opt/3fs/etc - Update config files:
- Set
node_idinstorage_main_app.toml. Each storage service is assigned a unique id between10001and10005. - Set
cluster_idand mgmtd address instorage_main_launcher.toml.
tomlcluster_id = "stage" [mgmtd_client] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"]- Add target paths in
storage_main.toml:
toml[server.mgmtd] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"] [common.monitor.reporters.monitor_collector] remote_ip = "192.168.1.1:10000" [server.targets] target_paths = ["/storage/data1/3fs","/storage/data2/3fs","/storage/data3/3fs","/storage/data4/3fs","/storage/data5/3fs","/storage/data6/3fs","/storage/data7/3fs","/storage/data8/3fs","/storage/data9/3fs","/storage/data10/3fs","/storage/data11/3fs","/storage/data12/3fs","/storage/data13/3fs","/storage/data14/3fs","/storage/data15/3fs","/storage/data16/3fs",] - Set
- Config file of storage service is managed by mgmtd service. Use
admin_clito upload the config file to mgmtd:bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "set-config --type STORAGE --file /opt/3fs/etc/storage_main.toml" - Start storage service:bash
rsync -avz meta:~/3fs/deploy/systemd/storage_main.service /usr/lib/systemd/system systemctl start storage_main - Run
list-nodescommand to check if storage service has joined the cluster:text/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "list-nodes"
Step 7: Create admin user, storage targets and chain table
- Create an admin user:The admin token is printed to the console, save it tobash
/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "user-add --root --admin 0 root"/opt/3fs/etc/token.txt. - Generate
admin_clicommands to create storage targets on 5 storage nodes (16 SSD per node, 6 targets per SSD).- Follow instructions at here to install Python packages.
bashpip install -r ~/3fs/deploy/data_placement/requirements.txt python ~/3fs/deploy/data_placement/src/model/data_placement.py \ -ql -relax -type CR --num_nodes 5 --replication_factor 3 --min_targets_per_disk 6 python ~/3fs/deploy/data_placement/src/setup/gen_chain_table.py \ --chain_table_type CR --node_id_begin 10001 --node_id_end 10005 \ --num_disks_per_node 16 --num_targets_per_disk 6 \ --target_id_prefix 1 --chain_id_prefix 9 \ --incidence_matrix_path output/DataPlacementModel-v_5-b_10-r_6-k_3-ฮป_2-lb_1-ub_1/incidence_matrix.pickleThe following 3 files will be generated in
outputdirectory:create_target_cmd.txt,generated_chains.csv, andgenerated_chain_table.csv. - Create storage targets:bash
/opt/3fs/bin/admin_cli --cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' --config.user_info.token $(<"/opt/3fs/etc/token.txt") < output/create_target_cmd.txt - Upload chains to mgmtd service:bash
/opt/3fs/bin/admin_cli --cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' --config.user_info.token $(<"/opt/3fs/etc/token.txt") "upload-chains output/generated_chains.csv" - Upload chain table to mgmtd service:bash
/opt/3fs/bin/admin_cli --cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' --config.user_info.token $(<"/opt/3fs/etc/token.txt") "upload-chain-table --desc stage 1 output/generated_chain_table.csv" - List chains and chain tables to check if they have been correctly uploaded:bash
/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "list-chains" /opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "list-chain-tables"
Step 8: FUSE client
For simplicity FUSE client is deployed on the meta node in this guide. However, we strongly advise against deploying clients on service nodes in production environment.
- Copy
hf3fs_fuse_mainto/opt/3fs/binand config files to/opt/3fs/etc.bashcp ~/3fs/build/bin/hf3fs_fuse_main /opt/3fs/bin cp ~/3fs/configs/{hf3fs_fuse_main_launcher.toml,hf3fs_fuse_main.toml,hf3fs_fuse_main_app.toml} /opt/3fs/etc - Create the mount point:bash
mkdir -p /3fs/stage - Set cluster ID, mountpoint, token file and mgmtd address in
hf3fs_fuse_main_launcher.tomltomlcluster_id = "stage" mountpoint = '/3fs/stage' token_file = '/opt/3fs/etc/token.txt' [mgmtd_client] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"] - Set mgmtd and monitor address in
hf3fs_fuse_main.toml.toml[mgmtd] mgmtd_server_addresses = ["RDMA://192.168.1.1:8000"] [common.monitor.reporters.monitor_collector] remote_ip = "192.168.1.1:10000" - Config file of FUSE client is also managed by mgmtd service. Use
admin_clito upload the config file to mgmtd:bash/opt/3fs/bin/admin_cli -cfg /opt/3fs/etc/admin_cli.toml --config.mgmtd_client.mgmtd_server_addresses '["RDMA://192.168.1.1:8000"]' "set-config --type FUSE --file /opt/3fs/etc/hf3fs_fuse_main.toml" - Start FUSE client:bash
cp ~/3fs/deploy/systemd/hf3fs_fuse_main.service /usr/lib/systemd/system systemctl start hf3fs_fuse_main - Check if 3FS has been mounted at
/3fs/stage:bashmount | grep '/3fs/stage'
FAQ
How to troubleshoot admin_cli init-cluster error?
If mgmtd fails to start after running init-cluster, the most likely cause is an error in mgmtd_main.toml. Any changes to this file require clearing all FoundationDB data and re-running init-cluster
How to build a single-node cluster?
A minimum of two storage services is required for data replication. If set --num-nodes=1, the gen_chain_table.py script will fail. In a test environment, this limitation can be bypassed by deploying multiple storage services on a single machine.
How to update config files?
All config files are managed by mgmtd. If any *_main.toml is updated, such as storage_main.toml, the modified file should be uploaded using admin_cli set-config.
How to troubleshoot common deployment issues?
When encountering any error during deployment,
- Check the log messages in
stdout/stderrusingjournalctl, especially during service startup. - Check log files stored in
/var/log/3fs/on service and client nodes. - Ensure that the directory
/var/log/3fs/exists before starting any service.
Overview (`docs/README.md`)
Documentation
Overview (`hf3fs_utils/README.md`)
hf3fs_cli
build:
python3 setup_hf3fs_utils.py bdist_wheelusage:
$ hf3fs_cli rmtree --help
Usage: hf3fs_cli rmtree [OPTIONS] [DIR_PATHS]...
Move a directory tree to the trash and set an expiration time, it will be automatically deleted after expiration
Example:
hf3fs_cli rmtree <path/to/remove> --expire <expire_time>
- Use --expire [1h|3h|8h|1d|3d|7d] to specify the expiration time, the directory will be deleted after expiration.
- Before expiration, you can restore the directory from the trash using `hf3fs_cli mv <trash_path> <target_path>`.
- If you need to free up space immediately, you can use `hf3fs_cli rmtree <trash_path>` to delete the data in the trash immediately, this operation cannot be undone!
- Use `ls /path/to/hf3fs/trash` to view the trash.
Options:
--expire [1h|3h|8h|1d|3d|7d] Expiration time, contents in the trash will be automatically deleted after expiration
-y, --yes Skip confirmation prompt and delete immediately
--help Show this message and exit.
$ hf3fs_cli mv --help
Usage: hf3fs_cli mv [OPTIONS] OLD_PATH NEW_PATH
Move files, supports moving files between different mount points within the same 3FS
Options:
--help Show this message and exit.If you want to use rmtree command, the administrator needs to create a trash directory for each user at /{3fs_mountpoint}/trash/{user_name}. The cleanup of the trash directory is handled by the trash_cleaner. For instructions on how to use it, please refer to src/client/trash_cleaner/.
Overview (`specs/README.md`)
P specifications
Build prerequisites
Follow the official guide to install the P framework.
Or if dotnet has been installed, run the following command to store the p command.
dotnet tool restoreRun tests
A helper script RunTests.ps1, implemented in PowerShell, is used to run tests and summarize the results.
DataStorage specifies the CRAQ implementation in 3FS.
PS > cd DataStorage
PS > ..\RunTests.ps1
...
-----------------------
Summary of test results
-----------------------
[02/26/2025 10:57:58] Elapsed time: 372.4s
test status seed schedules seconds min avg max
---- ------ ---- --------- ------- --- --- ---
tcOneClientWriteNoFailure[0] pass 1402445568 10 15.8 -1 -1 -1
tcTwoClientsWriteNoFailure[0] pass 189933208 10 23.6 -1 -1 -1
tcThreeClientsWriteNoFailure[0] pass 3060254145 10 40.7 -1 -1 -1
tcOneClientWriteUnreliableDetector[0] pass 2016460916 10 17.7 -1 -1 -1
tcTwoClientsWriteUnreliableDetector[0] pass 18777396 10 24.7 -1 -1 -1
tcOneClientWriteWithFailure[0] pass 2559323541 10 15.7 -1 -1 -1
tcTwoClientsWriteWithFailure[0] pass 1199246267 10 29.9 -1 -1 -1
tcOneClientWriteWithFailures[0] pass 672618818 10 15.4 -1 -1 -1
tcTwoClientsWriteWithFailures[0] pass 1908913074 10 32.3 -1 -1 -1
tcOneClientWriteShortChainWithFailure[0] pass 3031701162 10 6.3 -1 -1 -1
tcTwoClientsWriteShortChainWithFailures[0] pass 2907349611 10 16.6 -1 -1 -1
tcTwoClientsWriteLongChainWithFailures[0] pass 260515276 10 67.0 -1 -1 -1
[02/26/2025 10:57:58] All tests passedRDMASocket verifies the RDMA socket implementation in 3FS.
PS > cd RDMASocket
PS > ..\RunTests.ps1
...
-----------------------
Summary of test results
-----------------------
[02/26/2025 11:19:22] Elapsed time: 40.6s
test status seed schedules seconds min avg max
---- ------ ---- --------- ------- --- --- ---
tcPingPong[0] pass 3776118231 10 9.8 -1 -1 -1
tcOneWay[0] pass 200216558 10 3.6 -1 -1 -1
tcTwoWay[0] pass 1923093627 10 7.1 -1 -1 -1
[02/26/2025 11:19:22] All tests passedTimer includes modified portions of the following open-source project:
- The original implementation of
Timeris part of P tutorials licensed under MIT License.
Overview (`benchmarks/fio_usrbio/README.md`)
FIO engine for 3FS USRBIO
This repository contains the fio external plugin used for benchmarking 3FS USRBIO.
Build
First, build 3FS and fio.
Configure the following variables:
HF3FS_LIB_DIR: directory containslibhf3fs_api_shared.so, the default path in 3FS repo is3FS/build/src/lib/api.HF3FS_INCLUDE_DIR: directory containshf3fs_usrbio.h, the default path in 3FS repo is3FS/src/lib/api.FIO_SRC_DIR: directory containsconfig-host.h. After building fio, this header will be in the root of the fio repo.
Then run:
make HF3FS_LIB_DIR=${HF3FS_LIB_DIR} HF3FS_INCLUDE_DIR=${HF3FS_INCLUDE_DIR} FIO_SRC_DIR=${FIO_SRC_DIR}You will get the external plugin as hf3fs_usrbio.so.
Usage
To use this plugin, set the ioengine args in fio as external:hf3fs_usrbio.so. Please refer to fio documentation for further explanation.
Since multiple 3FS instances might be mounted, you need to specify the 3FS instance during benchmarking. This can be done by adding the mountpoint arguments (in the .fio):
mountpoint=/hf3fs/mount/pointTo benchmark batched small I/Os, please set these four parameters to batch_size simultaneously:
iodepth=1024
iodepth_batch_submit=1024
iodepth_batch_complete_min=1024
iodepth_batch_complete_max=1024Overview (`deploy/data_placement/README.md`)
How to generate chain tables
Suppose we are going to setup a small 3FS cluster:
- 3 replicas for each chunk
- 5 storage nodes:
10001 ... 10005 - 16 SSDs attached to each node
- 6 storage targets on each SSD
Install dependencies
The data placement problem is formulated using Pyomo and solved with the HiGHS solver. Install them and other dependencies:
$ cd deploy/data_placement
$ pip install -r requirements.txtGenerate chain table
First generate a solution of the data placement problem.
/* Detailed source-code truncated for AI context efficiency. */Note that some combinations of --num_nodes and --replication_factor may have no solution.
Then generate commands to create/remove storage targets and the chain table.
$ python src/setup/gen_chain_table.py --chain_table_type CR --node_id_begin 10001 --node_id_end 10005 --num_disks_per_node 16 --num_targets_per_disk 6 --incidence_matrix_path output/DataPlacementModel-v_5-b_10-r_6-k_3-ฮป_2-lb_1-ub_1/incidence_matrix.pickle
$ ls -1 output/
DataPlacementModel-v_5-b_10-r_6-k_3-ฮป_2-lb_1-ub_1
appsi_highs.log
create_target_cmd.txt
generated_chain_table.csv
generated_chains.csv
remove_target_cmd.txtOverview (`specs/Timer/README.md`)
Timer
This project includes modified portions of the following open-source project:
- P Licensed under the MIT License.
- Source: the original implementation of
Timer.
- Source: the original implementation of
Overview (`src/core/README.md`)
core is intended as a base component of mgmtd, meta, storage, and other concrete server components.
Overview (`src/simple_example/README.md`)
How to create a new service
- Copy
src/simple_exampleto a new subfolder insrc/. - Copy
src/fbs/simple_exampleto a new subfolder insrc/fbs/. - Rename
simple_exampletoyour_service_name. - Rename
SimpleExampletoYourSimpleExample. - Add the new cmake projects to
CMakeLists.txtfiles insrc/andsrc/fbs/.
svr_name='migration'
SrvName='Migration'
mkdir -p "src/$svr_name" && pushd src/simple_example && cp -rf --parents . "../$svr_name" && popd
mkdir -p "src/fbs/$svr_name" && pushd src/fbs/simple_example && cp -rf --parents . "../$svr_name" && popd
find "src/$svr_name" "src/fbs/$svr_name" -type f | xargs sed -i "s/simple_example/$svr_name/g"
find "src/$svr_name" "src/fbs/$svr_name" -type f | xargs sed -i "s/SimpleExample/$SrvName/g"Overview (`src/client/trash_cleaner/README.md`)
trash_cleaner
usage:
trash_cleaner 0.1.0
USAGE:
trash_cleaner [FLAGS] [OPTIONS] --interval <interval>
FLAGS:
--abort-on-error
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-i, --interval <interval> Scan interval (in seconds), exit after one scan if set to 0
--log <log> Log path, default is current directory [default: ./]
--log-level <log-level> Log level, default is info [default: info]
-p, --paths <paths>... path to trash directory
--stdout-level <stdout-level> stdout log level, default is warn [default: warn]Overview (`src/storage/chunk_engine/README.md`)
chunk-engine
Design
- The entire Chunk Engine can be divided into two components:
- Allocator: Responsible for allocating/reclaiming chunks and modifying memory states.
- MetaStore: Responsible for persisting allocation/reclamation events.
- Workflow for writing a new chunk:
- The Allocator assigns a new chunk position, pointing to a disk space (purely in-memory operation).
- Write data to this chunk position. If a power failure or write failure occurs at this stage, no existing data is affected.
- Generate corresponding chunk metadata and persist it alongside the allocation event to the MetaStore. Using RocksDB's WriteBatch ensures atomic updatesโthe entire write operation either succeeds or fails, with no intermediate states.
- Maintaining the Allocator's in-memory state:
- At startup, the Allocator quickly loads all allocation information from RocksDB.
- Allocation is performed in-memory first, followed by persistence. If a failure occurs before persistence, the allocation event is lost.
- Reclamation first persists the event to disk, then modifies the memory state. Even if a chunk deletion event is persisted, the chunk remains readable as long as memory holds its reference.
- This ensures conflict-free read/write operations: a read operation acquires a chunk reference, guaranteeing the chunk's validity until the read completes.
- Use
Arcto manage ownership of chunk position:- For allocation, returns an
Arc<ChunkPos>. If persistence fails, the position is automatically released when theArcis dropped. - Read operations also return an
Arc<ChunkPos>, ensuring safe data access even during concurrent writes or deletions.
- For allocation, returns an
Allocator
Storage hierarchy:
- Chunk: Basic data unit, currently proposed as 64KB, 512KB, and 4MB.
- Group: Each group contains 256 chunks (16MB, 128MB, or 1GB depending on chunk size).
- File: For 512KB chunks, a single file (~120GB) contains ~960 groups.
- Disk: Single disk capacity of 30TB, divided into 256 files per chunk size.
- Node: A single node contains 10โ20 disks.
This configuration supports up to ~1.2 billion chunks and ~5 million groups per machine.
Implementation details:
- Each group uses a 256-bit bitset (4
uint64_t) to track allocation status. - Maintain three in-memory structures:
allocated_groups: Groups with allocated space but no chunks assigned.unallocated_groups: Groups without allocated space.active_groups: Map of<group_id, group_state>tracking allocation status.
- Chunk allocation workflow:
- Prioritize finding free slots in
active_groupsusing__builtin_ctzfor fast bitwise operations. - If
active_groupsis empty, acquire a new group fromallocated_groups. - If
allocated_groupsis empty, fetch a group fromunallocated_groupsand allocate disk space synchronously.
- Prioritize finding free slots in
- Background threads:
allocate_thread: Maintainsactive_groupswithin a target size range to ensure in-memory allocation efficiency.compact_thread: Periodically scansactive_groups, migrates all chunks from selected groups, releases space, and returns groups toallocated_groups.
MetaStore
Persists three mappings:
chunk_id -> chunk_meta: Metadata includes chunk location, length, hash, version, etc., serialized usingderse.group_id -> group_state: Tracks chunk allocation status within groups, leveraging RocksDB's MergeOp for atomic updates.chunk_pos -> chunk_id: Maps physical positions to chunk IDs, used bycompact_threadduring chunk migration.
Chunk Engine
- MetaCache: Maintains an in-memory
chunk_id -> chunk_infomapping, wherechunk_infoincludeschunk_metaandArc<ChunkPos>. - Read operation: Returns
chunk_info. TheArc<ChunkPos>ensures safe data access until the read completes. - Write operation workflow:
- Query
MetaCacheto retrieve the currentchunk_info. - Invoke
Allocator::allocate()to obtain a new chunk position. - Read existing chunk data, write it to the new chunk position, append the new write request, and generate
new_chunk_info. - Persist
new_chunk_infoto the MetaStore along with a release record for the original chunk position.
- Query
Design Notes (`docs/design_notes.md`)
Design Notes
Design and implementation
The 3FS system has four components: cluster manager, metadata service, storage service and client. All components are connected in an RDMA network (InfiniBand or RoCE).
Metadata and storage services send heartbeats to cluster manager. Cluster manager handles membership changes and distributes cluster configuration to other services and clients. Multiple cluster managers are deployed and one of them is elected as the primary. Another manager is promoted as primary when the primary fails. Cluster configuration is typically stored in a reliable distributed coordination service, such as ZooKeeper or etcd. In our production environment, we use the same key-value store as file metadata to reduce dependencies.
File metadata operations (e.g. open or create files/directories) are sent to metadata services, which implement the file system semantics. Metadata services are stateless, since file metadata are stored in a transactional key-value store (e.g. FoundationDB). Clients can connect to any metadata service.
Each storage service manages a few local SSDs and provides a chunk store interface. The storage service implements Chain Replication with Apportioned Queries (CRAQ) to ensure strong consistency. CRAQโs write-all-read-any approach helps to unleash the throughput of SSDs and RDMA network. A 3FS file is split into equally sized chunks, which are replicated over multiple SSDs.
Two clients are developed for applications: FUSE client and native client. Most applications use FUSE client, which has a low adoption barrier. Performance-critical applications are integrated with the native client.
File system interfaces
Object store is becoming a popular option for data analytics and machine learning. However, file system semantics and a unified namespace where files are organized in directories provide greater flexibility for applications.
Atomic directory manipulation An object store can approximate hierarchical directory structures by using slashes (/) in object keys. However, it doesnโt natively support operations like atomically moving files/directories, or recursively deleting entire directories. Actually a common pattern in our internal applications involves creating a temporary directory, writing files to it, and then moving the directory to its final location. When handling a large number of small files, the recursive delete for directories is crucial. Without it, applications have to traverse each directory and remove files one by one.
Symbolic and hard links Our applications utilize symbolic and hard links to create lightweight snapshots of dynamically updated datasets, where new data is appended as individual files.
Familiar interface The file interface is well known and used everywhere. There is no need to learn a new storage API. Many datasets are stored as CSV/Parquet files. Adapting file-based data loaders to use the 3FS FUSE client or native client is straightforward.
Limitations of FUSE
FUSE (Filesystem in Userspace) simplifies file system client development by redirecting I/O operations to user-space processes through the FUSE kernel module. It creates the illusion that applications are accessing the remote file system as if it were a local file system. However, it has performance limitations:
Memory copy overhead The user-space file system daemon cannot access application memory. Data transfer between kernel and user spaces consumes memory bandwidth and increases end-to-end latency.
Primitive multi-threading support When an application initiates I/O requests, FUSE places these requests into a multi-threaded shared queue, protected by a spin lock. The user-space file system daemon then retrieves and processes requests from this queue. Due to lock contention, FUSEโs I/O processing capability fails to scale with the number of threads. Our benchmark results indicate that FUSE only handles approximately 400K 4KiB reads per second. Further increasing concurrency does not improve performance as lock contention intensifies.
perfprofiling reveals that the kernel-space spin lock consumes a significant amount of CPU time.
Most applications, e.g. data analytics, perform large block writes on 3FS or they can buffer data in memory and flush it to 3FS when write buffer is full. However, FUSE on Linux 5.x does not support concurrent writes to the same file^1. Applications overcome this limitation by writing to multiple files concurrently, maximizing the total throughput.
Read operations exhibit more complex patterns. Some training jobs require random access to dataset samples, with read sizes varying from a few kilobytes to several megabytes per sample. And samples are typically not 4K-aligned in files. Data loaders are specifically designed to fetch batches of samples. But they perform poorly when handling small random reads on FUSE-mounted 3FS. Bandwidth of SSDs and RDMA network are not fully utilized.
Asynchronous zero-copy API
Implementing the file system client as a VFS kernel module avoids performance issues mentioned above. But kernel module development is significantly more challenging than user-space system programming. Bugs are difficult to diagnose and can lead to catastrophic failures in production environments. For example, machines may crash and leave no log message for debugging. When upgrading a kernel module, all processes using the file system must be stopped cleanly; otherwise, a machine restart is required.
For these reasons, we have chosen to implement a native client within the FUSE daemon. This client offers an interface that supports asynchronous zero-copy I/O operations. File meta operations are still handled by FUSE daemon (e.g. open/close/stat files). Applications call open() to obtain a file descriptor (fd) and register it via native API. They can then perform I/O operations on the file with native client. This approach ensures consistency in metadata operations with the POSIX API, making it easier to migrate existing code.
The asynchronous, zero-copy API is inspired by Linux io_uring. Below are the key data structures in the API:
Iov A large memory region for zero-copy read/write operations, shared between the user process and the native client. InfiniBand memory registration is managed by the client. In native API, all read data will be read into Iov, and all write data should be written to Iov before calling the API.
Ior A small shared ring buffer for communication between user process and native client. The usage of Ior is similar to Linux
io_uring, where the user process enqueues read/write requests, and the native client dequeues these requests for completion. The requests are executed in batches, with their sizes controlled by theio_depthparameter. Multiple batches are processed in parallel, whether from different rings or the same ring. However, multiple rings are still recommended for multi-threaded applications, as sharing a ring requires synchronization, which can impact performance.
Within the native client, multiple threads are spawned to fetch I/O requests from the Iors. These requests are batched and dispatched to storage services, reducing RPC overhead caused by small read requests.
File metadata store
Location of file chunks
3FS divides file data into equally sized chunks and stripes them across multiple replication chains (replication chains and chain tables are defined in Section Data placement). Users can specify the chain table, chunk size, and stripe size for files on a per-directory basis. Each chunk is independently stored on multiple storage services, with its chunk ID generated by concatenating the fileโs inode id and chunk index.
When creating a new file, the metadata service employs a round-robin strategy to select consecutive replication chains from the designated chain table, based on the stripe size. Next, a random seed is generated to shuffle the selected chains. This allocation strategy ensures balanced data distribution across chains and SSDs.
When an application opens a file, the client contacts the meta service to obtain the fileโs data layout information. Then the client can independently compute chunk IDs and chains for data operations, minimizing the involvement of the meta service in the critical path.
File metadata on transactional key-value store
3FS uses FoundationDB as its distributed storage system for metadata. FoundationDB provides a key-value store interface and supports transactions with Serializable Snapshot Isolation (SSI). 3FS stores all metadata as key-value pairs in FoundationDB. Meta services follow a stateless architecture, greatly enhancing maintainability by allowing administrators to seamlessly upgrade or restart services without disruption. When clients experience request failures or timeouts, they can automatically fail over to other available services.
The file system metadata primarily consists of two core structures: inodes and directory entries. Inodes store attribute information for files, directories, and symbolic links, each identified by a globally unique 64-bit identifier that increments monotonically. Inode keys are constructed by concatenating the "INOD" prefix with the inode id, which is encoded in little-endian byte order to spread inodes over multiple FoundationDB nodes. The inode values vary by its type:
All inode types contain basic attributes: ownership, permissions, access/modification/change times.
Additional attributes for file inodes: file length, chunk size, selected range in chain table, shuffle seed.
Additional attributes for directory inodes: the parent directoryโs inode id, default layout configurations for subdirectories/files (chain table, chunk size, stripe size). The parentโs inode id is required to detect loops when moving directories. When moving
dir_a/dir_btodir_c/, we need to ensure thatdir_cis not a descendant ofdir_b, which can be achieved by checking all ancestors ofdir_cupward.Additional attributes for symbolic link inodes: target path string.
Directory entry keys are composed of a "DENT" prefix, the parent inode ID, and the entry name. Directory entry values store the target inode id and inode type. All entries within a directory naturally form a contiguous key range, allowing efficient directory listing via range queries.
The meta operations leverage FoundationDBโs transactions:
Read-only transactions used for metadata queries: fstat, lookup, listdir etc.
Read-write transactions used for metadata updates: create, link, unlink, rename etc.
For write transactions, FoundationDB tracks the read/write key sets to form conflict detection sets. When concurrent transaction conflicts are detected, the meta service automatically retries the transaction. This design enables multiple meta services to process requests in parallel while maintaining file system metadata consistency.
Dynamic file attributes
On most local file systems, deleting an opened file is deferred until all associated file descriptors are closed. Consequently, it is necessary to track all file descriptors of the file. Training jobs open a large number of files during startup. Storing all file descriptors would impose heavy load on meta service and FoundationDB. Since training jobs do not depend on this feature, 3FS does not track file descriptors opened in read-only mode.
3FS maintains a file session for each file descriptor (fd) opened in write mode since deleting write opened files may lead to unreclaimable garbage chunks from concurrent writes. When a file with active write sessions is deleted, meta service delays the deletion until all its fds are closed. To prevent lingering sessions from offline clients, the 3FS meta service periodically checks client liveness and cleans up sessions of offline clients.
The file length is stored in the inode. For files being actively updated, the length stored in inode may diverge from the actual length. Clients periodically (5 seconds by default) report to meta service maximum write position of each file opened in write mode. If this position exceeds the length in inode and there is no concurrent truncate operation, this position is adopted as the new file length.
Due to the possibility of concurrent writes from multiple clients, the method described above ensures only eventual consistency for file lengths. When processing close/fsync operations, the meta service obtains the precise file length by querying the ID and length of the last chunk from the storage service. Since file data is striped across multiple chains, this operation incurs non-negligible overhead.
Concurrent updates to the same fileโs length by multiple meta services may cause transaction conflicts and lead to repeated file length computation. To mitigate this, meta service distributes file length update tasks across multiple meta services using inode IDs and the rendezvous hash algorithm.
Our production environments use a large stripe size: 200. For small files, the number of chains containing file chunks is well below this number. The number of potentially used chains is stored in file inode and used as a hint when updating the length. It starts with an initial value of 16 and is doubled each time additional file chunks are written to more chains. This allows us to avoid querying all 200 chains when updating lengths of small files. This optimization can also be extended to the deletion of small files.
Chunk storage system
The design goal of chunk storage system is to achieve the highest bandwidth possible even when there are storage medium failures. The read/write throughput of 3FS should scale linearly with the number of SSDs and bisection network bandwidth between clients and storage services. Applications access storage services in a locality-oblivious manner.
Data placement
Each file chunk is replicated over a chain of storage targets using chain replication with apportioned queries (CRAQ). In CRAQ write requests are sent to the head target and propagated along a chain. Read requests can be sent to any of the storage target. Usually the read traffic is evenly distributed among all targets in a chain for better load balance. Multiple storage targets are created on each SSD and the targets join different chains.
Suppose there are 6 nodes: A, B, C, D, E, F. Each node has 1 SSD. Create 5 storage targets on each SSD: 1, 2, ... 5. Then there are 30 targets in total: A1, A2, A3, ..., F5. If each chunk has 3 replicas, a chain table is constructed as follows.
| Chain | Version | Target 1 (head) | Target 2 | Target 3 (tail) |
|---|---|---|---|---|
| 1 | 1 | A1 |
B1 |
C1 |
| 2 | 1 | D1 |
E1 |
F1 |
| 3 | 1 | A2 |
B2 |
C2 |
| 4 | 1 | D2 |
E2 |
F2 |
| 5 | 1 | A3 |
B3 |
C3 |
| 6 | 1 | D3 |
E3 |
F3 |
| 7 | 1 | A4 |
B4 |
C4 |
| 8 | 1 | D4 |
E4 |
F4 |
| 9 | 1 | A5 |
B5 |
C5 |
| 10 | 1 | D5 |
E5 |
F5 |
Each chain has a version number. The version number is incremented if the chain is changed (e.g. a storage target is offline). Only the primary cluster manager makes changes to chain tables.
A few chain tables can be constructed to support different data placement requirements. For example, two chain tables can be created, one for batch/offline jobs and another for online services. The two tables consist of storage targets on mutually exclusive nodes and SSDs.
Logically, the state of each chain changes independently. Each chain can be included in multiple chain tables. The concept of chain table is created to let metadata service pick a table for each file and stripe file chunks across chains in the table.
Balanced traffic during recovery
Suppose read traffic is evenly distributed among all storage targets in the above chain table. When A fails its read requests would be redirected to B and C. Under heavy load the read bandwidth of B, C is immediately saturated and B, C become the bottleneck of the entire system. Replacing a failed SSD and syncing data to the new SSD can take several hours. The read throughput is impaired during this period.
To reduce the performance impact, we can have more SSDs share the redirected traffic. In the following chain table, A is paired with every other SSDs. When A fails, each of the other SSDs receives 1/5 of Aโs read traffic.
| Chain | Version | Target 1 (head) | Target 2 | Target 3 (tail) |
|---|---|---|---|---|
| 1 | 1 | B1 |
E1 |
F1 |
| 2 | 1 | A1 |
B2 |
D1 |
| 3 | 1 | A2 |
D2 |
F2 |
| 4 | 1 | C1 |
D3 |
E2 |
| 5 | 1 | A3 |
C2 |
F3 |
| 6 | 1 | A4 |
B3 |
E3 |
| 7 | 1 | B4 |
C3 |
F4 |
| 8 | 1 | B5 |
C4 |
E4 |
| 9 | 1 | A5 |
C5 |
D4 |
| 10 | 1 | D5 |
E5 |
F5 |
To achieve maximum read throughput during recovery, the load balance problem can be formulated as a balanced incomplete block design. The optimal solution is obtained by using integer programming solver.
Data replication
CRAQ is a write-all-read-any replication protocol optimized for read-heavy workloads. Utilizing read bandwidth of all replicas is critical to achieve highest read throughput in an all-flash storage system.
When a write request is received by a storage service, it goes through the following steps:
The service checks if the chain version in write request matches with the latest known version; reject the request if itโs not. The write request could be sent by a client or a predecessor in the chain.
The service issues RDMA Read operations to pull write data. If the client/predecessor fails, the RDMA Read operations may time out and the write is aborted.
Once the write data is fetched into local memory buffer, a lock for the chunk to be updated is acquired from a lock manager. Concurrent writes to the same chunk are blocked. All writes are serialized at the head target.
The service reads the committed version of the chunk into memory, applies the update, and stores the updated chunk as a pending version. A storage target may store two versions of a chunk: a committed version and a pending version. Each version has a monotonically-increasing version number. The version numbers of committed version and pending versions are
vandurespectively, and satisfyu = v + 1.If the service is the tail, the committed version is atomically replaced by the pending version and an acknowledgment message is sent to the predecessor. Otherwise, the write request is forwarded to the successor. When the committed version is updated, the current chain version is stored as a field in the chunk metadata.
When an acknowledgment message arrives at a storage service, the service replaces the committed version with the pending version and continues to propagate the message to its predecessor. The local chunk lock is then released.
Suppose there are 3 targets in the chain: A, B, C. A write request has just entered step 5 at A. A forwards the request to successor B. Then B instantly fails and the forwarded write request is lost. When cluster manager detects Bโs failure, it marks B as offline and moves it to the end of chain and broadcasts the updated chain table. Once A receives the latest chain table, it forwards the write request to the new successor C. C may not receive the latest chain table yet and rejects the request. But A can keep forwarding the request to C. Eventually C gets the latest chain table and accepts the request.
When a read request arrives at a storage service:
When the service only has a committed version of the chunk, this version is returned to the client.
Unlike CRAQ, our implementation does not issue version query to the tail target. When there are both committed and pending versions, the service replies a special status code to notify the client. The client may wait for a short interval and retry. Or the client can issue a relaxed read request to get the pending version.
Failure detection
The cluster manager relies on heartbeats to detect fail-stop failures. Cluster manager declares a service failed if it does not receive heartbeats from it for a configurable interval (e.g. T seconds). A service stops processing requests and exits if it cannot communicate with cluster manager for T/2 seconds. The heartbeat can be seen as a request to *renew a lease* granted by the manager.
The metadata services are stateless. The list of online meta services provided by cluster manager is a simple service discovery mechanism that helps clients create connections to metadata services. If one meta service is down, the clients may switch to any other metadata service.
Cluster manager plays a more critical role in membership changes of storage services. It maintains a global view of chain tables and storage targetsโ states. Each storage target has a public state and a local state.
Public state indicates if itโs ready to serve read requests and if write requests would be propagated to it. Public states are stored with chain tables and distributed to services and clients.
| Public State | Read | Write | Notes |
|---|---|---|---|
| serving | Y | Y | service alive and serving client requests |
| syncing | N | Y | service alive and data recovery is in progress |
| waiting | N | N | service alive and data recovery not started yet |
| lastsrv | N | N | service down and it was the last serving target |
| offline | N | N | service down or storage medium failure |
Local state is only known by storage services and cluster manager, and itโs stored in the memory of cluster manager. If a storage target has medium failure, the related service sets the targetโs local state to offline in heartbeat. If a storage service is down, storage targets managed by the service are marked offline.
| Local State | Notes |
|---|---|
| up-to-date | service alive and serving client requests |
| online | service alive and target in syncing or waiting state |
| offline | service down or storage medium failure |
A storage target can change from one public state to another in response to the latest local state. The local state plays the role of a triggering event. The cluster manager periodically scans every chain and updates the public states of targets on the chain according to a state-transition table.
The chain version is incremented if the chain is updated.
If a storage target is marked offline, itโs moved to the end of chain.
If a storage service finds public state of any local storage target is lastsrv or offline, it exits immediately. The service may be isolated from the cluster manager by network partition error.
Once the data recovery of a storage target in syncing state is completed, the storage service set the targetโs local state to up-to-date in subsequent heartbeat messages sent to cluster manager.
| Local State | Current Public State | Predecessorโs Public State | Next Public State |
|---|---|---|---|
| up-to-date | serving | (any) | serving |
| syncing | (any) | serving | |
| waiting | (any) | waiting | |
| lastsrv | (any) | serving | |
| offline | (any) | waiting | |
| online | serving | (any) | serving |
| syncing | serving | syncing | |
| not serving | waiting | ||
| waiting | serving | syncing | |
| not serving | waiting | ||
| lastsrv | (any) | serving | |
| offline | (any) | waiting | |
| offline | serving | has no predecessor | lastsrv |
| has predecessor | offline | ||
| syncing | (any) | offline | |
| waiting | (any) | offline | |
| lastsrv | (any) | lastsrv | |
| offline | (any) | offline |
Data recovery
When a storage service exits (e.g. process crashes or restarts during upgrade), or a storage medium failure occurs, all related storage targets will be marked as offline and moved to the end of chains by cluster manager. Once the service restarts, each target on the service enters into the recovery process independently. The entire recovery process overlaps with normal activity and minimizes any interruption.
When a previously offline storage service starts:
The service periodically pulls latest chain tables from cluster manager. But it does not send heartbeats until all its storage targets have been marked offline in the latest chain tables. This ensures all its targets would go through the data recovery process.
When a write request arrives during recovery, the request is always a full-chunk-replace write. The local committed version is updated and any existing pending version is abandoned. Since current service is the tail, an acknowledgment message is sent to the predecessor. The full state of the predecessor is copied to the returning service through a continuous stream of full-chunk-replace writes.
Before the data recovery of a storage target starts, the predecessor sends a dump-chunkmeta request to the returning service. Then the service iterates the local chunk metadata store to collect the ids, chain versions and committed/pending version numbers of all chunks on the target, and replies the collected metadata to the predecessor.
When a sync-done message arrives, the service knows that the storage target is up-to-date. It sets local state of the target to up-to-date in heartbeat messages sent to cluster manager.
When a storage service finds a previously offline successor is online:
The service starts to forward normal write requests to the successor. Clients may only update a portion of the chunk, but the forwarded write requests should contain the whole chunk, i.e. a full-chunk-replace write.
The service sends a dump-chunkmeta request to the successor. Once the metadata of all chunks on the successor target are received, it collects the chunk metadata on its local target. Then it compares the two copies of chunk metadata to decide which chunks should be transferred.
The selected chunks are transferred to the successor by issuing full-chunk-replace write requests.
The chunk lock is first acquired for each chunk.
The chain version, committed version number and chunk content are read and transferred to successor by sending a full-chunk-replace request.
The chunk lock is released.
When all required chunks have been transferred, a sync-done message is sent to the successor.
The rules used to decide which chunks should be transferred are:
If a chunk only exists on the local target, it should be transferred.
If a chunk only exists on the remote target, it should be removed.
If the chain version of local chunk replica is greater than that of the remote chunk replica, it should be transferred.
If the chain versions of local/remote chunk replicas are the same but local committed version number does not equal to the remote pending version number, it should be transferred.
Otherwise, two chunk replicas are either the same or being updated by in-progress write requests.
Chunks and the metadata
File chunks are stored in the chunk engine. On each SSD, the persistent storage of the chunk engine consists of a fixed number of data files for storing chunk data, and a RocksDB instance for maintaining chunk metadata and other system information. Additionally, the chunk engine maintains an in-memory cache of chunk metadata to enhance query performance. A chunk allocator is implemented for fast allocation of new chunks. The chunk engine interface provides thread-safe access through the following operations:
open/close Initializes the engine by loading metadata from RocksDB and reconstructing chunk allocator states.
get Retrieves chunk metadata and reference-counted handle through a hashmap cache, enabling concurrent access with O(1) average complexity.
update Implements copy-on-write (COW) semantics by allocating new chunks before modifying data. Old chunks remain readable until all handles are released.
commit Commit the updated chunk metadata to RocksDB via write batches to ensure atomic updates; synchronously refresh the chunk metadata cache.
The chunk data will ultimately be stored on physical blocks. Physical block sizes range from 64KiB to 64MiB in increments of powers of two, totaling 11 distinct sizes. The allocator will assign physical blocks whose sizes most closely match the actual chunk size. A resource pool is constructed for each physical block size, with each pool containing 256 physical files. The usage status of physical blocks is maintained in memory using bitmaps. When a physical block is reclaimed, its bitmap flag is set to 0. The actual storage space of the block remains preserved and will be prioritized for subsequent allocations. When no available physical blocks remain, fallocate() will be used to allocate a contiguous large space in physical files, creating 256 new physical blocks - this approach helps reduce disk fragmentation.
When performing write operations on a chunk, the allocator first assigns a new physical block. The system then reads existing chunk data into a buffer, applies the update, and writes the updated buffer to the newly allocated block. An optimized process is implemented for appends, where data is directly added in-place at the end of the existing block. A new copy of metadata is constructed from the new block's location and existing chunk metadata. Subsequently, both the new chunk metadata and statuses of new and old physical blocks are atomically updated in RocksDB.
Metrics (`docs/metrics.md`)
Metric System of 3FS
Type of Metrics
3FS calculates metrics in each service and store metrics to ClickHouse through monitor service. There are some types of metrics in 3FS, and the metric may or may not be reset after each report.
There are many types of metrics:
| Type | Class | Storage Table | Description |
|---|---|---|---|
| value | ValueRecorder |
3fs.counters | Set by code and stored to ClickHouse directly. E.g. capacity |
| count | CountRecorder |
3fs.counters | Increase by code and usually reset to zero after each report. E.g., report read counter every second mean read IOPS.It is also be used to track current on-flight operations, which is increased at the beginning and is decreased after the operation finished, rather than reset automatically after report. |
| distribution | DistributionRecorder |
3fs.distributions | Used to calculate P90, P99 and etc for the period between two reports. |
| latency | LatencyRecorder |
3fs.distributions | Special distribution implementation, record latency with unit ns (nanosecond). |
List of Metrics
Following is a partial list of the metrics and their meanings:
| Metric Name | Type | Reset After Report | Description |
|---|---|---|---|
| fuse.dirty_inodes | value | Y | Current number of dirty inodes |
| fuse.op | count | Y | Number of client operations; instance field is the operation name |
| fuse.piov.bw | count | Y | Number of bytes written by the fuse client to the storage service |
| fuse.write.latency | latency | Y | Write operation latency |
| fuse.write.size | distribution | Y | Write operation size |
| meta_server.batch_op_size | count | Y | Batch operation count of metadata (sync + close + setattr) |
| meta_server.dist_set_map | count | Y | Number of set disk server map operations |
| meta_server.op_code | count | Y | Number of times a specific error code appears, tag is the specific error code |
| meta_server.op_failed | count | Y | Number of failed operations |
| meta_server.op_idempotent | count | Y | Number of idempotent operations executed |
| meta_server.op_duplicated | count | Y | Number of duplicated operations |
| meta_server.op_running | count | N | Number of running operations |
| meta_server.op_total | count | Y | Total number of operations |
| meta_server.op_latency | latency | Y | Metadata operation latency (nanoseconds) |
| meta_server.open_write | count | Y | Number of open/create file operations |
| meta_server.stat_dir | count | Y | Number of directory stat operations executed |
| meta_server.stat_file | count | Y | Number of file stat operations executed |
| meta_server.stat_symlink | count | Y | Number of symbolic link stat operations executed |
| meta_server.auth_failed | count | Y | Number of authentication failures |
| storage.check_disk.current | count | N | Number of currently running disk check operations |
| storage.check_disk.total | count | Y | Total number of disk check operations |
| storage.chunk_engine.allocate_times | count | Y | Number of chunks created |
| storage.chunk_engine.checksum_combine | count | Y | Checksum and merge count (includes checksum and merge, and append operations) |
| storage.chunk_engine.checksum_recalculate | count | Y | Checksum recalculation count |
| storage.chunk_engine.copy_on_write_read_bytes | count | Y | Number of bytes read by COW (Copy-on-Write) |
| storage.chunk_engine.copy_on_write_read_times | count | Y | Number of times COW read data |
| storage.chunk_engine.copy_on_write_times | count | Y | Number of COW executions |
| storage.chunk_engine.new | value | N | Current number of chunk_engines? (Need to check the code) |
| storage.chunk_engine.pwrite_times | count | Y | Number of pwrite calls |
| storage.chunk_engine.safe_write_direct_append | count | Y | Number of direct append writes during safe_write |
| storage.chunk_engine.safe_write_indirect_append | count | Y | Number of indirect append writes during safe_write |
| storage.chunk_remove.times | count | Y | Number of chunk delete operations |
| storage.chunk_write.times | count | Y | Number of chunk write operations |
| storage.disk_info.available | value | N | Disk available capacity (target unused space + reserved space + file system available space, bytes) |
| storage.disk_info.capacity | value | N | Total file system capacity (bytes) |
| storage.disk_info.read_only | value | N | Whether the file system is read-only (1: read-only, implemented by writing .hf3fs_check file in the file system root directory) |
| storage.disk_info.free | value | N | File system available space (bytes) |
| storage.do_commit.current | count | N | Number of IOs currently being committed |
| storage.do_commit.total | count | Y | Total number of committed IOs |
| storage.do_commit.fails | count | Y | Number of failed commit IOs |
| storage.do_commit.succ_latency | latency | Y | Success commit latency |
| storage.do_commit.fail_latency | latency | Y | Failure commit latency |
| storage.do_query.num_chunks | count | Y | Number of chunks in query results |
| storage.do_query.total | count | Y | Total number of chunk query operations |
| storage.do_query.current | count | N | Number of current chunk query operations |
| storage.do_query.succ_latency | latency | Y | Success query latency |
| storage.do_query.fail_latency | latency | Y | Failure query latency |
| storage.do_remove.current | count | N | Number of chunk remove operations |
| storage.do_remove.fails | count | Y | Number of failed chunk remove operations |
| storage.do_remove.num_chunks | count | Y | Number of chunks removed |
| storage.do_remove.total | count | Y | Total number of chunk remove operations |
| storage.do_remove.succ_latency | latency | Y | Success remove latency |
| storage.do_remove.fail_latency | latency | Y | Failure remove latency |
| storage.do_update.current | count | N | Number of current update chunk operations |
| storage.do_update.total | count | Y | Total number of update chunk operations |
| storage.do_update.fails | count | Y | Number of failed update chunk operations |
| storage.do_update.succ_latency | latency | Y | Success update latency |
| storage.do_update.fail_latency | latency | Y | Failure update latency |
| storage.remove_range.current | count | N | Number of current range remove chunk operations |
| storage.remove_range.fails | count | Y | Number of failed range remove chunk operations |
| storage.remove_range.total | count | Y | Total number of range remove chunk operations |
| storage.remove_range.succ_latency | latency | Y | Success remove range latency |
| storage.remove_range.fail_latency | latency | Y | Failure remove range latency |
| storage.req_remove_chunks.current | count | N | Number of current remove chunk requests |
| storage.req_remove_chunks.total | count | Y | Total number of remove chunk requests |
| storage.req_update.bytes | count | Y | Number of chunk update bytes |
| storage.req_update.current | count | N | Number of current chunk update requests |
| storage.req_update.fails | count | Y | Number of failed chunk update requests |
| storage.req_update.total | count | Y | Total number of chunk update requests |
| storage.req_write.bytes | count | Y | Number of bytes written by chunk write requests |
| storage.req_write.current | count | N | Number of current chunk write requests |
| storage.req_write.total | count | Y | Total number of chunk write requests |
| storage.engine_commit.current | count | N | Number of current commit operations |
| storage.engine_commit.total | count | Y | Total number of commit operations |
| storage.engine_commit.fails | count | Y | Number of failed commit operations |
| storage.engine_update.current | count | N | Number of current chunk update operations |
| storage.engine_update.total | count | Y | Total number of chunk update operations |
| storage.engine_update.fails | count | Y | Number of failed chunk update operations |
| storage.forward.write_bytes | count | Y | Number of forwarded write bytes |
| storage.forward.syncing_bytes | count | Y | Number of forwarded sync bytes |
| storage.reliable_forward.current | count | N | Number of current forward operations |
| storage.reliable_forward.total | count | Y | Total number of forward operations |
| storage.reliable_forward.fails | count | Y | Number of failed forward operations |
| storage.target.used_size | value | N | Target used space (bytes) |
| storage.target.reserved_size | value | N | Target reserved space (bytes) |
| storage.target.unrecycled_size | value | N | Target unrecycled space (bytes) |
| storage.target_state | value | N | Target state (0:invalid 1:uptodate 2:online 4:offline) |
| storage.write.bytes | count | Y | Total bytes written (includes write and update operations) |
--- METRICS ---
- Files Extracted: 14
- Estimated Token Budget: ~20305 tokens
- Canonical Reference: https://codewiki.google/github.com/deepseek-ai/3FS