mdbx: исправление возврата и подсчета "грязных" страниц в режиме MDBX_WRITEMAP.

Исправление регрессии после коммита db72763de049d6e4546f838277fe83b9081ad1de.

После отключения затратой поддержки списка "грязных" страниц логика
page_retire_ex() оказалась не полной и требовала доработки. Из-за этого
страницы добавленные или клонированные-и-измененные в текущей
транзакции, которые становились не нужными, не возвращались к доступным
для немедленного использования, а помещались в retired-список
становящихся доступными в последующих транзакциях.

В результате, в некоторых сценариях, особенно с интенсивным расщеплением
страниц из-за вставки ключей, происходило необоснованно сильное
потребление/выделение страниц БД. В свою очередь, это приводило к
использованию излишнего кол-ва страниц, увеличению GC, росту RSS и
размеру БД.
This commit is contained in:
Леонид Юрьев (Leonid Yuriev) 2022-11-27 12:31:42 +03:00
parent c521a21f05
commit 7685b4080e

View File

@ -4078,20 +4078,31 @@ __cold static void kill_page(MDBX_txn *txn, MDBX_page *mp, pgno_t pgno,
} }
} }
/* Remove page from dirty list */ /* Remove page from dirty list, etc */
static __inline void page_wash(MDBX_txn *txn, const size_t di, static __inline void page_wash(MDBX_txn *txn, const size_t di,
MDBX_page *const mp, const size_t npages) { MDBX_page *const mp, const size_t npages) {
tASSERT(txn, (txn->mt_flags & MDBX_TXN_RDONLY) == 0); tASSERT(txn, (txn->mt_flags & MDBX_TXN_RDONLY) == 0);
tASSERT(txn, (txn->mt_flags & MDBX_WRITEMAP) == 0 || MDBX_AVOID_MSYNC); tASSERT(txn, (di > 0) == (txn->tw.dirtylist != nullptr));
tASSERT(txn, di && di <= txn->tw.dirtylist->length &&
txn->tw.dirtylist->items[di].ptr == mp);
dpl_remove_ex(txn, di, npages);
txn->tw.dirtyroom++;
tASSERT(txn, txn->tw.dirtyroom + txn->tw.dirtylist->length ==
(txn->mt_parent ? txn->mt_parent->tw.dirtyroom
: txn->mt_env->me_options.dp_limit));
mp->mp_txnid = INVALID_TXNID; mp->mp_txnid = INVALID_TXNID;
mp->mp_flags = P_BAD; mp->mp_flags = P_BAD;
if (di) {
tASSERT(txn, txn->tw.dirtylist != nullptr);
tASSERT(txn, (txn->mt_flags & MDBX_WRITEMAP) == 0 || MDBX_AVOID_MSYNC);
tASSERT(txn, di <= txn->tw.dirtylist->length &&
txn->tw.dirtylist->items[di].ptr == mp);
dpl_remove_ex(txn, di, npages);
txn->tw.dirtyroom++;
tASSERT(txn, txn->tw.dirtyroom + txn->tw.dirtylist->length ==
(txn->mt_parent ? txn->mt_parent->tw.dirtyroom
: txn->mt_env->me_options.dp_limit));
} else {
tASSERT(txn, txn->tw.dirtylist == nullptr);
tASSERT(txn, (txn->mt_flags & MDBX_WRITEMAP) != 0 && !MDBX_AVOID_MSYNC);
tASSERT(txn, txn->tw.writemap_dirty_npages >= npages);
txn->tw.writemap_dirty_npages -= npages;
}
VALGRIND_MAKE_MEM_UNDEFINED(mp, PAGEHDRSZ); VALGRIND_MAKE_MEM_UNDEFINED(mp, PAGEHDRSZ);
if (txn->mt_flags & MDBX_WRITEMAP) { if (txn->mt_flags & MDBX_WRITEMAP) {
VALGRIND_MAKE_MEM_NOACCESS(page_data(mp), VALGRIND_MAKE_MEM_NOACCESS(page_data(mp),
@ -4128,9 +4139,9 @@ static int page_retire_ex(MDBX_cursor *mc, const pgno_t pgno,
* requires support the list of dirty pages and avoid explicit spilling. * requires support the list of dirty pages and avoid explicit spilling.
* So for flexibility and avoid extra internal dependencies we just * So for flexibility and avoid extra internal dependencies we just
* fallback to reading if dirty list was not allocated yet. */ * fallback to reading if dirty list was not allocated yet. */
size_t di = 0, si = 0; size_t di = 0, si = 0, npages = 1;
size_t npages = 1; bool is_frozen = false, is_spilled = false, is_shadowed = false,
bool is_frozen = false, is_spilled = false, is_shadowed = false; is_modifable = false;
if (unlikely(!mp)) { if (unlikely(!mp)) {
if (ASSERT_ENABLED() && pageflags) { if (ASSERT_ENABLED() && pageflags) {
pgr_t check; pgr_t check;
@ -4154,6 +4165,7 @@ static int page_retire_ex(MDBX_cursor *mc, const pgno_t pgno,
if ((di = dpl_exist(txn, pgno)) != 0) { if ((di = dpl_exist(txn, pgno)) != 0) {
mp = txn->tw.dirtylist->items[di].ptr; mp = txn->tw.dirtylist->items[di].ptr;
tASSERT(txn, IS_MODIFIABLE(txn, mp)); tASSERT(txn, IS_MODIFIABLE(txn, mp));
is_modifable = true;
goto status_done; goto status_done;
} }
if ((si = search_spilled(txn, pgno)) != 0) { if ((si = search_spilled(txn, pgno)) != 0) {
@ -4185,10 +4197,10 @@ static int page_retire_ex(MDBX_cursor *mc, const pgno_t pgno,
is_frozen = IS_FROZEN(txn, mp); is_frozen = IS_FROZEN(txn, mp);
if (!is_frozen) { if (!is_frozen) {
const bool is_dirty = IS_MODIFIABLE(txn, mp); is_modifable = IS_MODIFIABLE(txn, mp);
is_spilled = IS_SPILLED(txn, mp) && !(txn->mt_flags & MDBX_WRITEMAP);
is_shadowed = IS_SHADOWED(txn, mp); is_shadowed = IS_SHADOWED(txn, mp);
if (is_dirty) { is_spilled = IS_SPILLED(txn, mp) && !(txn->mt_flags & MDBX_WRITEMAP);
if (is_modifable) {
tASSERT(txn, !is_spilled); tASSERT(txn, !is_spilled);
tASSERT(txn, !txn->tw.spilled.list || !search_spilled(txn, pgno)); tASSERT(txn, !txn->tw.spilled.list || !search_spilled(txn, pgno));
tASSERT(txn, debug_dpl_find(txn, pgno) == mp || txn->mt_parent || tASSERT(txn, debug_dpl_find(txn, pgno) == mp || txn->mt_parent ||
@ -4197,9 +4209,9 @@ static int page_retire_ex(MDBX_cursor *mc, const pgno_t pgno,
tASSERT(txn, !debug_dpl_find(txn, pgno)); tASSERT(txn, !debug_dpl_find(txn, pgno));
} }
di = (is_dirty && txn->tw.dirtylist) ? dpl_exist(txn, pgno) : 0; di = (is_modifable && txn->tw.dirtylist) ? dpl_exist(txn, pgno) : 0;
si = is_spilled ? search_spilled(txn, pgno) : 0; si = is_spilled ? search_spilled(txn, pgno) : 0;
tASSERT(txn, !is_dirty || di || (txn->mt_flags & MDBX_WRITEMAP)); tASSERT(txn, !is_modifable || di || (txn->mt_flags & MDBX_WRITEMAP));
} else { } else {
tASSERT(txn, !IS_MODIFIABLE(txn, mp)); tASSERT(txn, !IS_MODIFIABLE(txn, mp));
tASSERT(txn, !IS_SPILLED(txn, mp)); tASSERT(txn, !IS_SPILLED(txn, mp));
@ -4240,22 +4252,19 @@ status_done:
* нераспределенного "хвоста" БД сдвигается только при их коммите. */ * нераспределенного "хвоста" БД сдвигается только при их коммите. */
if (MDBX_ENABLE_REFUND && unlikely(pgno + npages == txn->mt_next_pgno)) { if (MDBX_ENABLE_REFUND && unlikely(pgno + npages == txn->mt_next_pgno)) {
const char *kind = nullptr; const char *kind = nullptr;
if (di) { if (is_modifable) {
/* Страница испачкана в этой транзакции, но до этого могла быть /* Страница испачкана в этой транзакции, но до этого могла быть
* аллоцирована, испачкана и пролита в одной из родительских транзакций. * аллоцирована, испачкана и пролита в одной из родительских транзакций.
* Её МОЖНО вытолкнуть в нераспределенный хвост. */ * Её МОЖНО вытолкнуть в нераспределенный хвост. */
kind = "dirty"; kind = "dirty";
/* Remove from dirty list */ /* Remove from dirty list */
page_wash(txn, di, mp, npages); page_wash(txn, di, mp ? mp : pgno2page(txn->mt_env, pgno), npages);
} else if (si) { } else if (si) {
/* Страница пролита в этой транзакции, т.е. она аллоцирована /* Страница пролита в этой транзакции, т.е. она аллоцирована
* и запачкана в этой или одной из родительских транзакций. * и запачкана в этой или одной из родительских транзакций.
* Её МОЖНО вытолкнуть в нераспределенный хвост. */ * Её МОЖНО вытолкнуть в нераспределенный хвост. */
kind = "spilled"; kind = "spilled";
spill_remove(txn, si, npages); spill_remove(txn, si, npages);
} else if (txn->mt_flags & MDBX_WRITEMAP) {
kind = "writemap";
tASSERT(txn, mp && IS_MODIFIABLE(txn, mp));
} else { } else {
/* Страница аллоцирована, запачкана и возможно пролита в одной /* Страница аллоцирована, запачкана и возможно пролита в одной
* из родительских транзакций. * из родительских транзакций.
@ -4286,67 +4295,74 @@ status_done:
return MDBX_SUCCESS; return MDBX_SUCCESS;
} }
if (di) { if (is_modifable) {
/* Dirty page from this transaction */ if (di) {
/* If suitable we can reuse it through loose list */ /* Dirty page from this transaction */
if (likely(npages == 1 && /* If suitable we can reuse it through loose list */
txn->tw.loose_count < txn->mt_env->me_options.dp_loose_limit && if (likely(
(!MDBX_ENABLE_REFUND || npages == 1 &&
/* skip pages near to the end in favor of compactification */ txn->tw.loose_count < txn->mt_env->me_options.dp_loose_limit &&
txn->mt_next_pgno > (!MDBX_ENABLE_REFUND ||
pgno + txn->mt_env->me_options.dp_loose_limit || /* skip pages near to the end in favor of compactification */
txn->mt_next_pgno <= txn->mt_env->me_options.dp_loose_limit))) { txn->mt_next_pgno >
DEBUG("loosen dirty page %" PRIaPGNO, pgno); pgno + txn->mt_env->me_options.dp_loose_limit ||
if (MDBX_DEBUG != 0 || unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB)) txn->mt_next_pgno <= txn->mt_env->me_options.dp_loose_limit))) {
memset(page_data(mp), -1, txn->mt_env->me_psize - PAGEHDRSZ); DEBUG("loosen dirty page %" PRIaPGNO, pgno);
mp->mp_txnid = INVALID_TXNID; if (MDBX_DEBUG != 0 ||
mp->mp_flags = P_LOOSE; unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB))
mp_next(mp) = txn->tw.loose_pages; memset(page_data(mp), -1, txn->mt_env->me_psize - PAGEHDRSZ);
txn->tw.loose_pages = mp; mp->mp_txnid = INVALID_TXNID;
txn->tw.loose_count++; mp->mp_flags = P_LOOSE;
mp_next(mp) = txn->tw.loose_pages;
txn->tw.loose_pages = mp;
txn->tw.loose_count++;
#if MDBX_ENABLE_REFUND #if MDBX_ENABLE_REFUND
txn->tw.loose_refund_wl = (pgno + 2 > txn->tw.loose_refund_wl) txn->tw.loose_refund_wl = (pgno + 2 > txn->tw.loose_refund_wl)
? pgno + 2 ? pgno + 2
: txn->tw.loose_refund_wl; : txn->tw.loose_refund_wl;
#endif /* MDBX_ENABLE_REFUND */ #endif /* MDBX_ENABLE_REFUND */
VALGRIND_MAKE_MEM_NOACCESS(page_data(mp), VALGRIND_MAKE_MEM_NOACCESS(page_data(mp),
txn->mt_env->me_psize - PAGEHDRSZ); txn->mt_env->me_psize - PAGEHDRSZ);
MDBX_ASAN_POISON_MEMORY_REGION(page_data(mp), MDBX_ASAN_POISON_MEMORY_REGION(page_data(mp),
txn->mt_env->me_psize - PAGEHDRSZ); txn->mt_env->me_psize - PAGEHDRSZ);
return MDBX_SUCCESS; return MDBX_SUCCESS;
} }
#if !MDBX_DEBUG && !defined(MDBX_USE_VALGRIND) && !defined(__SANITIZE_ADDRESS__) #if !MDBX_DEBUG && !defined(MDBX_USE_VALGRIND) && !defined(__SANITIZE_ADDRESS__)
if (unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB)) if (unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB))
#endif #endif
{ {
/* Страница могла быть изменена в одной из родительских транзакций, /* Страница могла быть изменена в одной из родительских транзакций,
* в том числе, позже выгружена и затем снова загружена и изменена. * в том числе, позже выгружена и затем снова загружена и изменена.
* В обоих случаях её нельзя затирать на диске и помечать недоступной * В обоих случаях её нельзя затирать на диске и помечать недоступной
* в asan и/или valgrind */ * в asan и/или valgrind */
for (MDBX_txn *parent = txn->mt_parent; for (MDBX_txn *parent = txn->mt_parent;
parent && (parent->mt_flags & MDBX_TXN_SPILLS); parent && (parent->mt_flags & MDBX_TXN_SPILLS);
parent = parent->mt_parent) { parent = parent->mt_parent) {
if (intersect_spilled(parent, pgno, npages)) if (intersect_spilled(parent, pgno, npages))
goto skip_invalidate; goto skip_invalidate;
if (dpl_intersect(parent, pgno, npages)) if (dpl_intersect(parent, pgno, npages))
goto skip_invalidate; goto skip_invalidate;
} }
#if defined(MDBX_USE_VALGRIND) || defined(__SANITIZE_ADDRESS__) #if defined(MDBX_USE_VALGRIND) || defined(__SANITIZE_ADDRESS__)
if (MDBX_DEBUG != 0 || unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB)) if (MDBX_DEBUG != 0 ||
unlikely(txn->mt_env->me_flags & MDBX_PAGEPERTURB))
#endif #endif
kill_page(txn, mp, pgno, npages); kill_page(txn, mp, pgno, npages);
if ((txn->mt_flags & MDBX_WRITEMAP) == 0) { if ((txn->mt_flags & MDBX_WRITEMAP) == 0) {
VALGRIND_MAKE_MEM_NOACCESS(page_data(pgno2page(txn->mt_env, pgno)), VALGRIND_MAKE_MEM_NOACCESS(page_data(pgno2page(txn->mt_env, pgno)),
pgno2bytes(txn->mt_env, npages) - PAGEHDRSZ); pgno2bytes(txn->mt_env, npages) -
MDBX_ASAN_POISON_MEMORY_REGION(page_data(pgno2page(txn->mt_env, pgno)), PAGEHDRSZ);
pgno2bytes(txn->mt_env, npages) - MDBX_ASAN_POISON_MEMORY_REGION(
PAGEHDRSZ); page_data(pgno2page(txn->mt_env, pgno)),
pgno2bytes(txn->mt_env, npages) - PAGEHDRSZ);
}
} }
skip_invalidate:;
} }
skip_invalidate:
/* Remove from dirty list */ /* wash dirty page */
page_wash(txn, di, mp, npages); page_wash(txn, di, mp, npages);
reclaim: reclaim: