Files
tromcho.net/src/nfs/nfs_kv_defrag.cpp
T

531 lines
18 KiB
C++

// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
//
// NFS proxy over VitastorKV database - defragmentation
#include <sys/time.h>
#include "nfs_proxy.h"
#include "nfs_common.h"
#include "nfs_kv.h"
#include "str_util.h"
#include "cli.h"
struct kv_fs_defrag_t
{
nfs_proxy_t *proxy = NULL;
inode_t shared_ino = 0;
bool dry_run = false;
bool no_rm = false;
bool progress = true;
uint64_t bitmap_granularity = 0;
uint64_t buf_size = 0;
uint8_t *block_buf = NULL;
timespec prev_progress = {};
int errcode = 0;
bool reading = false;
bool empty = false;
uint64_t last_offset = 0;
uint64_t real_size = 0;
uint64_t buf_pos = 0;
uint64_t iodepth = 0;
uint64_t num_moved = 0, num_unused = 0;
uint64_t bytes_moved = 0, bytes_unused = 0;
uint64_t max_ctime = 0;
bool handling = false;
std::function<void(int, uint64_t, uint64_t, uint64_t)> cb;
void read();
void handle_read();
void finish(int retval);
};
void kv_fs_defrag_t::finish(int retval)
{
auto cb = std::move(this->cb);
delete block_buf;
block_buf = NULL;
cb(retval, real_size, bytes_unused, max_ctime);
delete this;
}
void kv_fs_defrag_t::read()
{
if (errcode)
{
finish(errcode);
return;
}
auto op = new cluster_op_t;
op->opcode = OSD_OP_READ;
op->inode = shared_ino;
op->offset = last_offset;
op->len = buf_size;
op->iov.push_back(block_buf, buf_size);
reading = true;
op->callback = [this](cluster_op_t *op)
{
reading = false;
if (op->retval != op->len)
{
fprintf(stderr, "Error reading 0x%jx bytes from volume 0x%jx at 0x%jx: %s (code %d)\n",
op->len, shared_ino, op->offset, strerror(-op->retval), op->retval);
finish(op->retval >= 0 ? -EIO : op->retval);
}
else
{
// Check that any data was actually read or it's the last iteration
uint64_t bitmap_size = (op->len / bitmap_granularity + 7) / 8;
uint64_t bitmap_pos = 0;
empty = true;
for (; bitmap_pos < bitmap_size; bitmap_pos += 8)
{
if (*((uint64_t*)((uint8_t*)op->bitmap_buf + bitmap_pos)))
empty = false;
}
for (; bitmap_pos < bitmap_size; bitmap_pos++)
{
if (*((uint8_t*)((uint8_t*)op->bitmap_buf + bitmap_pos)))
empty = false;
}
buf_pos = 0;
handle_read();
}
delete op;
};
proxy->cli->execute(op);
}
void kv_fs_defrag_t::handle_read()
{
if (handling)
{
return;
}
handling = true;
while (!empty && !errcode && buf_pos < buf_size && iodepth < proxy->kvfs->defrag_iodepth)
{
// Next object header may be at any position after any number of zeroes
// Commonly it's either in the beginning of a 4 KB sector or in the end of it
if ((*(uint64_t*)(block_buf+buf_pos)) == SHARED_FILE_MAGIC_V1)
{
iodepth++;
shared_file_header_t *hdr = (shared_file_header_t*)(block_buf+buf_pos);
uint64_t shared_offset = last_offset + buf_pos;
buf_pos += hdr->alloc;
real_size = shared_offset + hdr->alloc;
auto move_cb = [this, ino = hdr->inode, alloc = hdr->alloc, shared_offset](int res, bool was_moved)
{
if (res < 0 && res != -ENOENT)
{
fprintf(stderr, "Error checking/moving inode 0x%jx from volume 0x%jx offset 0x%jx: %s (code %d)\n",
ino, shared_ino, shared_offset, strerror(-res), res);
errcode = res;
}
else
{
if (was_moved)
{
bytes_moved += alloc;
num_moved++;
}
else
{
bytes_unused += alloc;
num_unused++;
}
if (proxy->trace)
{
fprintf(
stderr, was_moved
? (dry_run
? "In use inode 0x%jx (%ju bytes) in volume 0x%jx at offset 0x%jx\n"
: "Moved inode 0x%jx (%ju bytes) in volume 0x%jx at offset 0x%jx\n")
: "Unused inode 0x%jx (%ju bytes) in volume 0x%jx at offset 0x%jx\n",
ino, alloc, shared_ino, shared_offset
);
}
else if (progress)
{
timespec now;
clock_gettime(CLOCK_REALTIME, &now);
if (now.tv_sec >= prev_progress.tv_sec+2)
{
prev_progress = now;
fprintf(stderr, "Processed %s, %s %s, unused %s\n", format_size(real_size).c_str(),
dry_run ? "in use" : "moved", format_size(bytes_moved).c_str(), format_size(bytes_unused).c_str());
}
}
}
iodepth--;
handle_read();
};
if (dry_run)
{
kv_read_inode(proxy, hdr->inode, [=](int res, const std::string & value, json11::Json attrs)
{
uint64_t ctime = (uint64_t)attrs["ctime"].number_value();
if (max_ctime < ctime)
max_ctime = ctime;
move_cb(res, !res && attrs["shared_ino"] == shared_ino && attrs["shared_offset"] == shared_offset);
});
}
else
{
nfs_move_inode_from(proxy, hdr->inode, shared_ino, shared_offset, move_cb);
}
}
else
{
buf_pos += 8;
}
}
handling = false;
if (errcode)
{
if (iodepth)
{
// Wait for completion
return;
}
finish(errcode);
}
else if (empty)
{
if (iodepth)
{
// Wait for completion
return;
}
// Finish - now we can purge shared inode
fprintf(
stderr, dry_run
? "Estimated volume 0x%jx - in use %s (%ju files), unused %s (%ju files), last inode change time %s\n"
: "Defragmented volume 0x%jx - moved %s (%ju files), unused %s (%ju files), last inode change time %s. Purging volume data\n",
shared_ino, format_size(bytes_moved).c_str(), num_moved, format_size(bytes_unused).c_str(), num_unused, format_datetime(max_ctime).c_str()
);
if (dry_run || no_rm)
{
finish(0);
}
else
{
proxy->cmd->loop_and_wait(proxy->cmd->start_rm_data(json11::Json::object {
{ "inode", INODE_NO_POOL(shared_ino) },
{ "pool", (uint64_t)INODE_POOL(shared_ino) },
{ "progress", (uint64_t)proxy->trace }
}), [this](const cli_result_t & r)
{
if (r.err)
{
fprintf(stderr, "Failed to remove volume 0x%jx data: %s (code %d)\n",
shared_ino, r.text.c_str(), r.err);
finish(r.err);
}
else
{
proxy->db->del(kv_inode_key(shared_ino), [=](int res)
{
if (res < 0)
{
fprintf(stderr, "Failed to remove volume key %s: %s (code %d)\n",
kv_inode_key(shared_ino).c_str(), strerror(-res), res);
finish(res);
}
else
{
proxy->db->del(kv_inode_prefix_key(shared_ino, "shared"), [=](int res)
{
if (res < 0)
{
fprintf(stderr, "Failed to remove volume key %s: %s (code %d)\n",
kv_inode_prefix_key(shared_ino, "shared").c_str(), strerror(-res), res);
}
finish(res);
});
}
});
}
});
}
}
else if (!reading && buf_pos >= buf_size)
{
last_offset += buf_pos;
read();
}
}
// Linear read all object headers, check which of them are still alive, move them away
void kv_fs_state_t::defrag_volume(inode_t ino, bool no_rm, bool dry_run, std::function<void(int, uint64_t, uint64_t, uint64_t)> cb)
{
auto pool_it = proxy->cli->st_cli->pool_config.find(INODE_POOL(ino));
if (pool_it == proxy->cli->st_cli->pool_config.end())
{
fprintf(stderr, "Volume 0x%jx references a non-existing pool with ID %u, skipping\n", ino, INODE_POOL(ino));
cb(0, 0, 0, 0);
return;
}
auto st = new kv_fs_defrag_t;
st->proxy = proxy;
st->shared_ino = ino;
st->dry_run = dry_run;
st->no_rm = no_rm;
st->buf_size = pool_it->second.pg_stripe_size * defrag_block_count;
st->block_buf = (uint8_t*)malloc_or_die(st->buf_size);
st->bitmap_granularity = pool_it->second.bitmap_granularity;
st->cb = cb;
clock_gettime(CLOCK_REALTIME, &st->prev_progress);
st->read();
}
struct kv_fs_defrag_all_t
{
std::function<void(int)> cb;
nfs_proxy_t *proxy = NULL;
bool dry_run = false;
bool no_rm = false;
bool recalc_stats = false;
bool include_empty = false;
timespec now = {};
void *list_shared = NULL;
uint64_t ino = 0;
json11::Json ientry;
bool recalc = false;
uint64_t real_size = 0;
uint64_t removed_size = 0;
uint64_t opentime = 0;
int res = 0;
void run(int);
};
void kv_fs_defrag_all_t::run(int st)
{
if (st == 1)
goto resume_1;
else if (st == 2)
goto resume_2;
else if (st == 3)
goto resume_3;
else if (st == 4)
goto resume_4;
else if (st == 5)
goto resume_5;
else if (st == 6)
goto resume_6;
clock_gettime(CLOCK_REALTIME, &now);
list_shared = proxy->db->list_start("shared");
proxy->db->list_next(list_shared, [this](int res, const std::string & key, const std::string & value)
{
if (res == -ENOENT || key.substr(0, 6) != "shared")
this->res = -ENOENT;
else
{
this->res = res;
this->ino = kv_key_inode(key, 6);
}
run(1);
});
return;
while (true)
{
resume_1:
if (res < 0)
{
if (res == -ENOENT)
res = 0;
break;
}
kv_read_inode(proxy, ino, [this](int res, const std::string & value, json11::Json attrs)
{
this->res = res;
this->ientry = attrs;
run(2);
});
return;
resume_2:
if (res == -ENOENT)
{
// This shared inode is already removed
proxy->db->del(kv_inode_prefix_key(ino, "shared"), [this](int res)
{
run(3);
});
return;
resume_3:
proxy->db->list_next(list_shared, NULL);
return;
}
real_size = ientry["size"].uint64_value();
removed_size = ientry["removed"].uint64_value();
opentime = (uint64_t)ientry["opentime"].number_value();
recalc = false;
if (!real_size && !opentime || recalc_stats)
{
// Statistics are missing - recalculate statistics
recalc = true;
fprintf(stderr, "Shared volume 0x%jx misses size and removal statistics, recalculating\n", ino);
proxy->kvfs->defrag_volume(ino, true, true, [this](int res, uint64_t sz, uint64_t rm, uint64_t tm)
{
this->res = res;
this->real_size = sz;
this->removed_size = rm;
this->opentime = tm;
run(4);
});
return;
resume_4:
if (res < 0)
{
break;
}
proxy->kvfs->update_inode(ino, true, [this](json11::Json::object & ientry)
{
ientry["size"] = real_size;
ientry["removed"] = removed_size;
ientry["opentime"] = opentime;
}, [this](int res)
{
this->res = res;
run(5);
});
return;
resume_5:
if (res < 0)
{
fprintf(stderr, "Warning: Failed to update shared volume 0x%jx metadata: %s (code %d)\n", ino, strerror(-res), res);
}
}
if ((opentime && opentime < now.tv_sec - proxy->kvfs->volume_untouched_sec || !opentime && include_empty) &&
(real_size && removed_size || include_empty) &&
removed_size >= (real_size * proxy->kvfs->defrag_percent / 100))
{
// This volume needs defrag
fprintf(
stderr, "Shared volume 0x%jx requires defragmentation: last "
"open-for-append time %s, size %s, removed %s\n",
ino, format_datetime(opentime).c_str(), format_size(real_size).c_str(), format_size(removed_size).c_str()
);
if (!recalc || !dry_run)
{
proxy->kvfs->defrag_volume(ino, no_rm, dry_run, [this](int res, uint64_t, uint64_t, uint64_t)
{
this->res = res;
run(6);
});
return;
resume_6:
if (res < 0)
{
break;
}
}
}
else
{
fprintf(
stderr, "Shared volume 0x%jx does not require defragmentation: last "
"open-for-append time %s, size %s, removed %s\n",
ino, format_datetime(opentime).c_str(), format_size(real_size).c_str(), format_size(removed_size).c_str()
);
}
proxy->db->list_next(list_shared, NULL);
return;
}
proxy->db->list_close(list_shared);
auto cb = std::move(this->cb);
cb(res);
delete this;
}
void kv_fs_state_t::defrag_all(json11::Json cfg, std::function<void(int)> cb)
{
auto st = new kv_fs_defrag_all_t;
st->cb = cb;
st->proxy = proxy;
st->dry_run = cfg["dry_run"].bool_value();
st->no_rm = cfg["no_rm"].bool_value();
st->recalc_stats = cfg["recalc_stats"].bool_value();
st->include_empty = cfg["include_empty"].bool_value();
st->run(0);
}
void kv_fs_state_t::upgrade_db(std::function<void(int)> cb)
{
// In the future, FS metadata format upgrades should be added here
// Currently we only do one thing: we create missing shared inode list keys ("sharedXXX")
proxy->db->get("version", [=](int res, const std::string & ver_value)
{
if (res < 0 && res != -ENOENT)
{
cb(res);
return;
}
json11::Json ver;
if (res == 0)
{
std::string err;
ver = json11::Json::parse(ver_value, err);
if (err != "")
{
fprintf(stderr, "Invalid JSON in `version` key, value: %s, error: %s\n", ver_value.c_str(), err.c_str());
cb(-EINVAL);
return;
}
}
if (ver.uint64_value() > 1 || ver.is_object())
{
cb(0);
return;
}
// Create missing shared inode index keys
auto list_inodes = proxy->db->list_start("i");
proxy->db->list_next(list_inodes, [=](int res, const std::string & key, const std::string & value)
{
if (res == -ENOENT || key.substr(0, 1) != "i" || key == "id")
{
proxy->db->list_close(list_inodes);
proxy->db->set("version", "1", [=](int res)
{
cb(0);
}, [=](int res, const std::string & value)
{
return res == -ENOENT || ver_value == value;
});
return;
}
uint64_t inode_id = kv_key_inode(key, 1);
if (!inode_id)
{
fprintf(stderr, "Invalid inode key %s, skipping\n", key.c_str());
}
else
{
std::string err;
auto ientry = json11::Json::parse(value, err);
if (err != "")
{
fprintf(stderr, "Invalid JSON in key %s (inode %ju), skipping\n", key.c_str(), inode_id);
}
else if (ientry["type"] == "shared")
{
proxy->db->set(kv_inode_prefix_key(inode_id, "shared"), "{}", [=](int res)
{
if (res < 0)
{
fprintf(stderr, "Error writing key %s: %s (code %d)\n",
kv_inode_prefix_key(inode_id, "shared").c_str(), strerror(-res), res);
}
proxy->db->list_next(list_inodes, NULL);
});
return;
}
}
proxy->db->list_next(list_inodes, NULL);
});
});
}