479 lines
16 KiB
C++
479 lines
16 KiB
C++
// Copyright (c) Vitaliy Filippov, 2019+
|
|
// License: VNPL-1.1 (see README.md for details)
|
|
|
|
#include "blockstore_impl.h"
|
|
#include "blockstore_internal.h"
|
|
#include "allocator.h"
|
|
|
|
#define _REDIRECT_INTENT 0x101
|
|
|
|
bool blockstore_impl_t::enqueue_write(blockstore_op_t *op)
|
|
{
|
|
clock_gettime(CLOCK_REALTIME, &PRIV(op)->tv_begin);
|
|
return true;
|
|
}
|
|
|
|
void blockstore_impl_t::prepare_meta_block_write(uint32_t modified_block)
|
|
{
|
|
if (modified_blocks.find(modified_block) != modified_blocks.end())
|
|
return;
|
|
io_uring_sqe *sqe = get_sqe();
|
|
assert(sqe != NULL);
|
|
ring_data_t *data = ((ring_data_t*)sqe->user_data);
|
|
uint8_t *buf = (uint8_t*)memalign_or_die(MEM_ALIGNMENT, dsk.meta_block_size);
|
|
data->iov = (struct iovec){ buf, (size_t)dsk.meta_block_size };
|
|
data->callback = [this, modified_block](ring_data_t *data)
|
|
{
|
|
live = true;
|
|
if (data->res != data->iov.iov_len)
|
|
{
|
|
// FIXME: our state becomes corrupted after a write error. maybe do something better than just die
|
|
disk_error_abort("data write", data->res, data->iov.iov_len);
|
|
}
|
|
auto it = modified_blocks.find(modified_block);
|
|
assert(it != modified_blocks.end());
|
|
free(it->second.buf);
|
|
modified_blocks.erase(it);
|
|
heap->complete_block_write(modified_block);
|
|
ringloop->wakeup();
|
|
};
|
|
io_uring_prep_writev(
|
|
sqe, dsk.meta_fd, &data->iov, 1, dsk.meta_offset + ((uint64_t)modified_block+1)*dsk.meta_block_size
|
|
);
|
|
unsynced_meta_write_count++;
|
|
pending_modified_blocks.push_back(modified_block);
|
|
modified_blocks[modified_block] = { .sent = false, .buf = buf };
|
|
}
|
|
|
|
bool blockstore_impl_t::meta_block_is_pending(uint32_t modified_block)
|
|
{
|
|
auto mb_it = modified_blocks.find(modified_block);
|
|
return mb_it != modified_blocks.end();
|
|
}
|
|
|
|
bool blockstore_impl_t::intent_write_allowed(blockstore_op_t *op, heap_entry_t *obj)
|
|
{
|
|
// Parallel writes to the same object are forbidden so "one intent at a time" is fulfilled automatically
|
|
// Intent writes are disabled when metadata fsync is enabled
|
|
if (!dsk.disable_meta_fsync)
|
|
{
|
|
return false;
|
|
}
|
|
// Intent writes are only for replication
|
|
if (op->opcode != BS_OP_WRITE_STABLE)
|
|
{
|
|
return false;
|
|
}
|
|
// Operation size should be less than or equal to atomic write size
|
|
if (!op->len || op->len > dsk.atomic_write_size)
|
|
{
|
|
return false;
|
|
}
|
|
// Intent-writes are disabled if "absolutely correct during compaction" checksum validation algorithm is enabled
|
|
// We could also do RMW here when perfect_csum_update is enabled, but it's unclear if we need it
|
|
if (perfect_csum_update && dsk.csum_block_size > dsk.bitmap_granularity &&
|
|
((op->offset % dsk.csum_block_size) || (op->len % dsk.csum_block_size)))
|
|
{
|
|
return false;
|
|
}
|
|
bool ok = true;
|
|
heap->iterate_with_stable(obj, obj->lsn, [&](heap_entry_t *wr, bool stable)
|
|
{
|
|
// Intent writes are not allowed over buffered writes
|
|
auto t = wr->type();
|
|
if (t == BS_HEAP_SMALL_WRITE)
|
|
{
|
|
ok = false;
|
|
return false;
|
|
}
|
|
// Intent writes are not allowed over unstable writes
|
|
if (!stable)
|
|
{
|
|
ok = false;
|
|
return false;
|
|
}
|
|
// Intent writes are not allowed over unfinished intent writes
|
|
if ((t == BS_HEAP_INTENT_WRITE || t == BS_HEAP_BIG_INTENT) && wr->lsn > heap->get_fsynced_lsn())
|
|
{
|
|
ok = false;
|
|
return false;
|
|
}
|
|
// Intent writes are allowed over BIG_WRITEs even with fsyncs because BIG_WRITE is always counted as fsynced
|
|
if (t == BS_HEAP_BIG_WRITE || t == BS_HEAP_BIG_INTENT)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
int blockstore_impl_t::dequeue_write(blockstore_op_t *op)
|
|
{
|
|
if (PRIV(op)->op_state)
|
|
{
|
|
return continue_write(op);
|
|
}
|
|
if (write_iodepth >= max_write_iodepth)
|
|
{
|
|
return 0;
|
|
}
|
|
PRIV(op)->modified_block = UINT32_MAX;
|
|
PRIV(op)->write_type = 0;
|
|
heap_entry_t *obj = heap->read_entry(op->oid);
|
|
if (op->opcode == BS_OP_DELETE)
|
|
{
|
|
// Delete
|
|
if (!obj || obj->type() == BS_HEAP_DELETE)
|
|
{
|
|
// Already deleted
|
|
op->retval = 0;
|
|
FINISH_OP(op);
|
|
return 2;
|
|
}
|
|
PRIV(op)->write_type = BS_HEAP_DELETE;
|
|
BS_SUBMIT_CHECK_SQES(1);
|
|
int res = heap->add_delete(obj, &PRIV(op)->modified_block);
|
|
if (res == ENOSPC)
|
|
goto enospc;
|
|
assert(res == 0);
|
|
prepare_meta_block_write(PRIV(op)->modified_block);
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 5;
|
|
write_iodepth++;
|
|
}
|
|
// FIXME: Allow to do initial writes as buffered, not redirected
|
|
// FIXME: Allow to do direct writes over holes
|
|
else if (!obj || obj->type() == BS_HEAP_DELETE || op->offset == 0 && op->len == dsk.data_block_size)
|
|
{
|
|
// Big (redirect) write
|
|
PRIV(op)->write_type = dsk.disable_data_fsync || op->opcode != BS_OP_WRITE_STABLE ? BS_HEAP_BIG_WRITE : _REDIRECT_INTENT;
|
|
BS_SUBMIT_CHECK_SQES(1);
|
|
PRIV(op)->location = heap->find_free_data();
|
|
if (PRIV(op)->location == UINT64_MAX)
|
|
{
|
|
enospc:
|
|
if (!heap->get_to_compact_count())
|
|
{
|
|
// no space
|
|
op->retval = -ENOSPC;
|
|
FINISH_OP(op);
|
|
return 2;
|
|
}
|
|
PRIV(op)->wait_for = WAIT_COMPACTION;
|
|
PRIV(op)->wait_detail = heap->get_compacted_count();
|
|
flusher->request_trim();
|
|
return 0;
|
|
}
|
|
uint64_t loc = PRIV(op)->location;
|
|
#ifdef BLOCKSTORE_DEBUG
|
|
printf(
|
|
"Allocate offset %ju for %jx:%jx v%ju\n",
|
|
loc, op->oid.inode, op->oid.stripe, op->version
|
|
);
|
|
#endif
|
|
heap->use_data(op->oid.inode, PRIV(op)->location);
|
|
io_uring_sqe *sqe = get_sqe();
|
|
ring_data_t *data = ((ring_data_t*)sqe->user_data);
|
|
data->iov = (struct iovec){ op->buf, op->len };
|
|
data->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
|
|
io_uring_prep_writev(sqe, dsk.data_fd, &data->iov, 1, dsk.data_offset + loc + op->offset);
|
|
PRIV(op)->pending_ops++;
|
|
write_iodepth++;
|
|
if (PRIV(op)->write_type == BS_HEAP_BIG_WRITE)
|
|
{
|
|
PRIV(op)->op_state = 1;
|
|
inflight_big++;
|
|
}
|
|
else
|
|
PRIV(op)->op_state = 3;
|
|
}
|
|
else if (intent_write_allowed(op, obj))
|
|
{
|
|
// Direct intent-write
|
|
BS_SUBMIT_CHECK_SQES(1);
|
|
int res = 0;
|
|
if (dsk.csum_block_size <= dsk.bitmap_granularity &&
|
|
(obj->entry_type == (BS_HEAP_BIG_INTENT|BS_HEAP_STABLE) ||
|
|
obj->entry_type == (BS_HEAP_BIG_WRITE|BS_HEAP_STABLE)))
|
|
{
|
|
// Even more simplified BIG_INTENT writes
|
|
// FIXME: Support RMW mode for csum_block_size > bitmap_granularity
|
|
PRIV(op)->write_type = BS_HEAP_BIG_INTENT;
|
|
PRIV(op)->location = obj->big_location(heap);
|
|
res = heap->add_big_intent(op->oid, &obj, op->version, op->offset, op->len, op->bitmap,
|
|
(uint8_t*)op->buf, NULL, &PRIV(op)->modified_block);
|
|
if (res == ENOSPC)
|
|
goto enospc;
|
|
assert(res == 0);
|
|
PRIV(op)->lsn = obj->lsn;
|
|
}
|
|
else
|
|
{
|
|
PRIV(op)->write_type = BS_HEAP_INTENT_WRITE;
|
|
auto wr = obj;
|
|
while (wr && (wr->type() == BS_HEAP_INTENT_WRITE || wr->type() == BS_HEAP_COMMIT || wr->type() == BS_HEAP_ROLLBACK))
|
|
{
|
|
wr = heap->prev(wr);
|
|
}
|
|
assert(wr && (wr->type() == BS_HEAP_BIG_WRITE || wr->type() == BS_HEAP_BIG_INTENT));
|
|
PRIV(op)->location = wr->big_location(heap);
|
|
res = heap->add_small_write(op->oid, &obj, (BS_HEAP_INTENT_WRITE | (op->opcode == BS_OP_WRITE_STABLE ? BS_HEAP_STABLE : 0)),
|
|
op->version, op->offset, op->len, 0, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
|
|
if (res == ENOSPC)
|
|
goto enospc;
|
|
assert(res == 0);
|
|
PRIV(op)->lsn = obj->lsn;
|
|
}
|
|
prepare_meta_block_write(PRIV(op)->modified_block);
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 9;
|
|
write_iodepth++;
|
|
}
|
|
else
|
|
{
|
|
// Small (buffered) overwrite
|
|
// First check if there is free buffer space
|
|
PRIV(op)->write_type = BS_HEAP_SMALL_WRITE;
|
|
uint64_t loc = !op->len ? 0 : heap->find_free_buffer_area(op->len);
|
|
if (loc == UINT64_MAX)
|
|
{
|
|
PRIV(op)->wait_for = WAIT_COMPACTION;
|
|
PRIV(op)->wait_detail = heap->get_compacted_count();
|
|
flusher->request_trim();
|
|
return 0;
|
|
}
|
|
// There is sufficient space. Check SQE(s)
|
|
BS_SUBMIT_CHECK_SQES(1 + (op->len > 0 ? 1 : 0));
|
|
int res = heap->add_small_write(op->oid, &obj, (BS_HEAP_SMALL_WRITE | (op->opcode == BS_OP_WRITE_STABLE ? BS_HEAP_STABLE : 0)),
|
|
op->version, op->offset, op->len, loc, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
|
|
if (res == ENOSPC)
|
|
goto enospc;
|
|
assert(res == 0);
|
|
PRIV(op)->lsn = obj->lsn;
|
|
prepare_meta_block_write(PRIV(op)->modified_block);
|
|
PRIV(op)->pending_ops++;
|
|
if (op->len > 0)
|
|
{
|
|
// Prepare buffered data write
|
|
heap->use_buffer_area(op->oid.inode, loc, op->len);
|
|
if (dsk.inmemory_journal)
|
|
{
|
|
memcpy((uint8_t*)buffer_area + loc, op->buf, op->len);
|
|
}
|
|
BS_SUBMIT_GET_SQE(sqe2, data2);
|
|
data2->iov = (struct iovec){ op->buf, op->len };
|
|
data2->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
|
|
io_uring_prep_writev(sqe2, dsk.journal_fd, &data2->iov, 1, dsk.journal_offset + loc);
|
|
PRIV(op)->pending_ops++;
|
|
}
|
|
else
|
|
{
|
|
// Zero-length overwrite. Allowed to bump object version in EC placement groups without actually writing data
|
|
}
|
|
assert(PRIV(op)->pending_ops);
|
|
PRIV(op)->op_state = 5;
|
|
write_iodepth++;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int blockstore_impl_t::continue_write(blockstore_op_t *op)
|
|
{
|
|
int op_state = PRIV(op)->op_state;
|
|
again:
|
|
if (op_state == 2)
|
|
goto resume_2;
|
|
else if (op_state == 4)
|
|
goto resume_4;
|
|
else if (op_state == 6)
|
|
goto resume_6;
|
|
else if (op_state == 8)
|
|
goto resume_8;
|
|
else if (op_state == 10)
|
|
goto resume_10;
|
|
else if (op_state == 11)
|
|
goto resume_11;
|
|
else if (op_state == 12)
|
|
goto resume_12;
|
|
else
|
|
{
|
|
// In progress
|
|
assert(op_state < 10);
|
|
if (PRIV(op)->modified_block != UINT32_MAX &&
|
|
!meta_block_is_pending(PRIV(op)->modified_block))
|
|
{
|
|
PRIV(op)->pending_ops--;
|
|
PRIV(op)->modified_block = UINT32_MAX;
|
|
}
|
|
if (PRIV(op)->pending_ops > 0)
|
|
return 1;
|
|
op_state++;
|
|
goto again;
|
|
}
|
|
resume_2:
|
|
// We must fsync all big writes to avoid complex write workflows
|
|
// It's OK for all HDDs and for server SSDs, but slightly worse for desktop SSDs
|
|
inflight_big--;
|
|
if (!dsk.disable_data_fsync)
|
|
{
|
|
// fsync data in a batch
|
|
resume_11:
|
|
if (inflight_big > 0)
|
|
{
|
|
PRIV(op)->op_state = 11;
|
|
return 1;
|
|
}
|
|
if (fsyncing_data)
|
|
{
|
|
resume_12:
|
|
if (fsyncing_data)
|
|
{
|
|
PRIV(op)->op_state = 12;
|
|
return 1;
|
|
}
|
|
goto resume_4;
|
|
}
|
|
fsyncing_data = true;
|
|
BS_SUBMIT_GET_SQE(sqe, data);
|
|
io_uring_prep_fsync(sqe, dsk.data_fd, IORING_FSYNC_DATASYNC);
|
|
data->iov = { 0 };
|
|
data->callback = [this, op](ring_data_t *data)
|
|
{
|
|
fsyncing_data = false;
|
|
handle_write_event(data, op);
|
|
};
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 3;
|
|
return 1;
|
|
}
|
|
resume_4:
|
|
{
|
|
BS_SUBMIT_CHECK_SQES(1);
|
|
auto obj = heap->read_entry(op->oid);
|
|
int res = 0;
|
|
if (PRIV(op)->write_type == _REDIRECT_INTENT)
|
|
{
|
|
res = heap->add_redirect_intent(op->oid, &obj, op->version, op->offset, op->len,
|
|
PRIV(op)->location, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
|
|
}
|
|
else
|
|
{
|
|
res = heap->add_big_write(op->oid, obj, op->opcode == BS_OP_WRITE_STABLE,
|
|
op->version, op->offset, op->len, PRIV(op)->location, op->bitmap, (uint8_t*)op->buf, &PRIV(op)->modified_block);
|
|
}
|
|
if (res == ENOSPC)
|
|
{
|
|
if (!heap->get_to_compact_count())
|
|
{
|
|
// no space
|
|
heap->free_data(op->oid.inode, PRIV(op)->location);
|
|
write_iodepth--;
|
|
op->retval = -ENOSPC;
|
|
FINISH_OP(op);
|
|
return 2;
|
|
}
|
|
PRIV(op)->wait_for = WAIT_COMPACTION;
|
|
PRIV(op)->wait_detail = heap->get_compacted_count();
|
|
flusher->request_trim();
|
|
return 0;
|
|
}
|
|
assert(res == 0);
|
|
prepare_meta_block_write(PRIV(op)->modified_block);
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 5;
|
|
return 1;
|
|
}
|
|
resume_6:
|
|
// Apply throttling to not fill the journal too quickly for the SSD+HDD case
|
|
if (PRIV(op)->write_type == BS_HEAP_SMALL_WRITE && throttle_small_writes)
|
|
{
|
|
// Apply throttling
|
|
timespec tv_end;
|
|
clock_gettime(CLOCK_REALTIME, &tv_end);
|
|
uint64_t exec_us =
|
|
(tv_end.tv_sec - PRIV(op)->tv_begin.tv_sec)*1000000 +
|
|
(tv_end.tv_nsec - PRIV(op)->tv_begin.tv_nsec)/1000;
|
|
// Compare with target execution time
|
|
// 100% free -> target time = 0
|
|
// 0% free -> target time = iodepth/parallelism * (iops + size/bw) / write per second
|
|
uint64_t buffer_free_space = dsk.journal_len - heap->get_buffer_area_used_space();
|
|
uint64_t ref_us =
|
|
(write_iodepth <= throttle_target_parallelism ? 100 : 100*write_iodepth/throttle_target_parallelism)
|
|
* (1000000/throttle_target_iops + op->len*1000000/throttle_target_mbs/1024/1024)
|
|
/ 100;
|
|
ref_us -= ref_us * buffer_free_space / dsk.journal_len;
|
|
if (ref_us > exec_us + throttle_threshold_us)
|
|
{
|
|
// Pause reply
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 7;
|
|
// Remember that the timer can in theory be called right here
|
|
tfd->set_timer_us(ref_us-exec_us, false, [this, op](int timer_id)
|
|
{
|
|
PRIV(op)->pending_ops--;
|
|
ringloop->wakeup();
|
|
});
|
|
return 1;
|
|
}
|
|
}
|
|
resume_8:
|
|
// Acknowledge write
|
|
#ifdef BLOCKSTORE_DEBUG
|
|
printf("Ack write %jx:%jx v%ju\n", op->oid.inode, op->oid.stripe, op->version);
|
|
#endif
|
|
op->retval = op->len;
|
|
if (PRIV(op)->write_type == BS_HEAP_BIG_WRITE)
|
|
{
|
|
unsynced_data_write_count++;
|
|
}
|
|
else if (PRIV(op)->write_type == BS_HEAP_SMALL_WRITE)
|
|
{
|
|
unsynced_buffer_write_count++;
|
|
heap->complete_lsn_write(PRIV(op)->lsn);
|
|
}
|
|
else if (PRIV(op)->write_type == BS_HEAP_BIG_INTENT ||
|
|
PRIV(op)->write_type == BS_HEAP_INTENT_WRITE)
|
|
{
|
|
unsynced_data_write_count++;
|
|
intent_write_counter++;
|
|
heap->complete_lsn_write(PRIV(op)->lsn);
|
|
}
|
|
else if (PRIV(op)->write_type == _REDIRECT_INTENT)
|
|
{
|
|
unsynced_data_write_count++;
|
|
intent_write_counter++;
|
|
}
|
|
write_iodepth--;
|
|
FINISH_OP(op);
|
|
return 2;
|
|
resume_10:
|
|
// Direct intent-write
|
|
// LSN is not marked as completed so big_write won't be freed
|
|
BS_SUBMIT_GET_SQE(sqe, data);
|
|
data->iov = (struct iovec){ op->buf, op->len };
|
|
data->callback = [this, op](ring_data_t *data) { handle_write_event(data, op); };
|
|
io_uring_prep_writev(sqe, dsk.data_fd, &data->iov, 1, dsk.data_offset + PRIV(op)->location + op->offset);
|
|
if (dsk.use_atomic_flag)
|
|
sqe->rw_flags = RWF_ATOMIC;
|
|
PRIV(op)->pending_ops++;
|
|
PRIV(op)->op_state = 7;
|
|
return 1;
|
|
}
|
|
|
|
void blockstore_impl_t::handle_write_event(ring_data_t *data, blockstore_op_t *op)
|
|
{
|
|
live = true;
|
|
if (data->res != data->iov.iov_len)
|
|
{
|
|
// FIXME: our state becomes corrupted after a write error. maybe do something better than just die
|
|
disk_error_abort("data write", data->res, data->iov.iov_len);
|
|
}
|
|
PRIV(op)->pending_ops--;
|
|
assert(PRIV(op)->pending_ops >= 0);
|
|
if (PRIV(op)->pending_ops == 0)
|
|
{
|
|
ringloop->wakeup();
|
|
}
|
|
}
|