Reduce loaded range tree memory usage

This patch implements a new tree structure for ZFS, and uses it to 
store range trees more efficiently.

The new structure is approximately a B-tree, though there are some 
small differences from the usual characterizations. The tree has core 
nodes and leaf nodes; each contain data elements, which the elements 
in the core nodes acting as separators between its children. The 
difference between core and leaf nodes is that the core nodes have an 
array of children, while leaf nodes don't. Every node in the tree may 
be only partially full; in most cases, they are all at least 50% full 
(in terms of element count) except for the root node, which can be 
less full. Underfull nodes will steal from their neighbors or merge to 
remain full enough, while overfull nodes will split in two. The data 
elements are contained in tree-controlled buffers; they are copied 
into these on insertion, and overwritten on deletion. This means that 
the elements are not independently allocated, which reduces overhead, 
but also means they can't be shared between trees (and also that 
pointers to them are only valid until a side-effectful tree operation 
occurs). The overhead varies based on how dense the tree is, but is 
usually on the order of about 50% of the element size; the per-node 
overheads are very small, and so don't make a significant difference. 
The trees can accept arbitrary records; they accept a size and a 
comparator to allow them to be used for a variety of purposes.

The new trees replace the AVL trees used in the range trees today. 
Currently, the range_seg_t structure contains three 8 byte integers 
of payload and two 24 byte avl_tree_node_ts to handle its storage in 
both an offset-sorted tree and a size-sorted tree (total size: 64 
bytes). In the new model, the range seg structures are usually two 4 
byte integers, but a separate one needs to exist for the size-sorted 
and offset-sorted tree. Between the raw size, the 50% overhead, and 
the double storage, the new btrees are expected to use 8*1.5*2 = 24 
bytes per record, or 33.3% as much memory as the AVL trees (this is 
for the purposes of storing metaslab range trees; for other purposes, 
like scrubs, they use ~50% as much memory).

We reduced the size of the payload in the range segments by teaching 
range trees about starting offsets and shifts; since metaslabs have a 
fixed starting offset, and they all operate in terms of disk sectors, 
we can store the ranges using 4-byte integers as long as the size of 
the metaslab divided by the sector size is less than 2^32. For 512-byte
sectors, this is a 2^41 (or 2TB) metaslab, which with the default
settings corresponds to a 256PB disk. 4k sector disks can handle 
metaslabs up to 2^46 bytes, or 2^63 byte disks. Since we do not 
anticipate disks of this size in the near future, there should be 
almost no cases where metaslabs need 64-byte integers to store their 
ranges. We do still have the capability to store 64-byte integer ranges 
to account for cases where we are storing per-vdev (or per-dnode) trees, 
which could reasonably go above the limits discussed. We also do not 
store fill information in the compact version of the node, since it 
is only used for sorted scrub.

We also optimized the metaslab loading process in various other ways
to offset some inefficiencies in the btree model. While individual
operations (find, insert, remove_from) are faster for the btree than 
they are for the avl tree, remove usually requires a find operation, 
while in the AVL tree model the element itself suffices. Some clever 
changes actually caused an overall speedup in metaslab loading; we use 
approximately 40% less cpu to load metaslabs in our tests on Illumos.

Another memory and performance optimization was achieved by changing 
what is stored in the size-sorted trees. When a disk is heavily 
fragmented, the df algorithm used by default in ZFS will almost always 
find a number of small regions in its initial cursor-based search; it 
will usually only fall back to the size-sorted tree to find larger 
regions. If we increase the size of the cursor-based search slightly, 
and don't store segments that are smaller than a tunable size floor 
in the size-sorted tree, we can further cut memory usage down to 
below 20% of what the AVL trees store. This also results in further 
reductions in CPU time spent loading metaslabs.

The 16KiB size floor was chosen because it results in substantial memory 
usage reduction while not usually resulting in situations where we can't 
find an appropriate chunk with the cursor and are forced to use an 
oversized chunk from the size-sorted tree. In addition, even if we do 
have to use an oversized chunk from the size-sorted tree, the chunk 
would be too small to use for ZIL allocations, so it isn't as big of a 
loss as it might otherwise be. And often, more small allocations will 
follow the initial one, and the cursor search will now find the 
remainder of the chunk we didn't use all of and use it for subsequent 
allocations. Practical testing has shown little or no change in 
fragmentation as a result of this change.

If the size-sorted tree becomes empty while the offset sorted one still 
has entries, it will load all the entries from the offset sorted tree 
and disregard the size floor until it is unloaded again. This operation 
occurs rarely with the default setting, only on incredibly thoroughly 
fragmented pools.

There are some other small changes to zdb to teach it to handle btrees, 
but nothing major.
                                           
Reviewed-by: George Wilson <gwilson@delphix.com>
Reviewed-by: Matt Ahrens <matt@delphix.com>
Reviewed by: Sebastien Roy seb@delphix.com
Reviewed-by: Igor Kozhukhov <igor@dilos.org>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Signed-off-by: Paul Dagnelie <pcd@delphix.com>
Closes #9181
This commit is contained in:
Paul Dagnelie
2019-10-09 10:36:03 -07:00
committed by Brian Behlendorf
parent d0a84ba92b
commit ca5777793e
50 changed files with 3722 additions and 638 deletions
+219 -23
View File
@@ -30,7 +30,7 @@
#ifndef _SYS_RANGE_TREE_H
#define _SYS_RANGE_TREE_H
#include <sys/avl.h>
#include <sys/btree.h>
#include <sys/dmu.h>
#ifdef __cplusplus
@@ -41,20 +41,35 @@ extern "C" {
typedef struct range_tree_ops range_tree_ops_t;
typedef enum range_seg_type {
RANGE_SEG32,
RANGE_SEG64,
RANGE_SEG_GAP,
RANGE_SEG_NUM_TYPES,
} range_seg_type_t;
/*
* Note: the range_tree may not be accessed concurrently; consumers
* must provide external locking if required.
*/
typedef struct range_tree {
avl_tree_t rt_root; /* offset-ordered segment AVL tree */
zfs_btree_t rt_root; /* offset-ordered segment b-tree */
uint64_t rt_space; /* sum of all segments in the map */
uint64_t rt_gap; /* allowable inter-segment gap */
range_seg_type_t rt_type; /* type of range_seg_t in use */
/*
* All data that is stored in the range tree must have a start higher
* than or equal to rt_start, and all sizes and offsets must be
* multiples of 1 << rt_shift.
*/
uint8_t rt_shift;
uint64_t rt_start;
range_tree_ops_t *rt_ops;
/* rt_avl_compare should only be set if rt_arg is an AVL tree */
/* rt_btree_compare should only be set if rt_arg is a b-tree */
void *rt_arg;
int (*rt_avl_compare)(const void *, const void *);
int (*rt_btree_compare)(const void *, const void *);
uint64_t rt_gap; /* allowable inter-segment gap */
/*
* The rt_histogram maintains a histogram of ranges. Each bucket,
@@ -64,36 +79,217 @@ typedef struct range_tree {
uint64_t rt_histogram[RANGE_TREE_HISTOGRAM_SIZE];
} range_tree_t;
typedef struct range_seg {
avl_node_t rs_node; /* AVL node */
avl_node_t rs_pp_node; /* AVL picker-private node */
typedef struct range_seg32 {
uint32_t rs_start; /* starting offset of this segment */
uint32_t rs_end; /* ending offset (non-inclusive) */
} range_seg32_t;
/*
* Extremely large metaslabs, vdev-wide trees, and dnode-wide trees may
* require 64-bit integers for ranges.
*/
typedef struct range_seg64 {
uint64_t rs_start; /* starting offset of this segment */
uint64_t rs_end; /* ending offset (non-inclusive) */
} range_seg64_t;
typedef struct range_seg_gap {
uint64_t rs_start; /* starting offset of this segment */
uint64_t rs_end; /* ending offset (non-inclusive) */
uint64_t rs_fill; /* actual fill if gap mode is on */
} range_seg_t;
} range_seg_gap_t;
/*
* This type needs to be the largest of the range segs, since it will be stack
* allocated and then cast the actual type to do tree operations.
*/
typedef range_seg_gap_t range_seg_max_t;
/*
* This is just for clarity of code purposes, so we can make it clear that a
* pointer is to a range seg of some type; when we need to do the actual math,
* we'll figure out the real type.
*/
typedef void range_seg_t;
struct range_tree_ops {
void (*rtop_create)(range_tree_t *rt, void *arg);
void (*rtop_destroy)(range_tree_t *rt, void *arg);
void (*rtop_add)(range_tree_t *rt, range_seg_t *rs, void *arg);
void (*rtop_remove)(range_tree_t *rt, range_seg_t *rs, void *arg);
void (*rtop_add)(range_tree_t *rt, void *rs, void *arg);
void (*rtop_remove)(range_tree_t *rt, void *rs, void *arg);
void (*rtop_vacate)(range_tree_t *rt, void *arg);
};
static inline uint64_t
rs_get_start_raw(const range_seg_t *rs, const range_tree_t *rt)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32:
return (((range_seg32_t *)rs)->rs_start);
case RANGE_SEG64:
return (((range_seg64_t *)rs)->rs_start);
case RANGE_SEG_GAP:
return (((range_seg_gap_t *)rs)->rs_start);
default:
VERIFY(0);
return (0);
}
}
static inline uint64_t
rs_get_end_raw(const range_seg_t *rs, const range_tree_t *rt)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32:
return (((range_seg32_t *)rs)->rs_end);
case RANGE_SEG64:
return (((range_seg64_t *)rs)->rs_end);
case RANGE_SEG_GAP:
return (((range_seg_gap_t *)rs)->rs_end);
default:
VERIFY(0);
return (0);
}
}
static inline uint64_t
rs_get_fill_raw(const range_seg_t *rs, const range_tree_t *rt)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32: {
const range_seg32_t *r32 = rs;
return (r32->rs_end - r32->rs_start);
}
case RANGE_SEG64: {
const range_seg64_t *r64 = rs;
return (r64->rs_end - r64->rs_start);
}
case RANGE_SEG_GAP:
return (((range_seg_gap_t *)rs)->rs_fill);
default:
VERIFY(0);
return (0);
}
}
static inline uint64_t
rs_get_start(const range_seg_t *rs, const range_tree_t *rt)
{
return ((rs_get_start_raw(rs, rt) << rt->rt_shift) + rt->rt_start);
}
static inline uint64_t
rs_get_end(const range_seg_t *rs, const range_tree_t *rt)
{
return ((rs_get_end_raw(rs, rt) << rt->rt_shift) + rt->rt_start);
}
static inline uint64_t
rs_get_fill(const range_seg_t *rs, const range_tree_t *rt)
{
return (rs_get_fill_raw(rs, rt) << rt->rt_shift);
}
static inline void
rs_set_start_raw(range_seg_t *rs, range_tree_t *rt, uint64_t start)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32:
ASSERT3U(start, <=, UINT32_MAX);
((range_seg32_t *)rs)->rs_start = (uint32_t)start;
break;
case RANGE_SEG64:
((range_seg64_t *)rs)->rs_start = start;
break;
case RANGE_SEG_GAP:
((range_seg_gap_t *)rs)->rs_start = start;
break;
default:
VERIFY(0);
}
}
static inline void
rs_set_end_raw(range_seg_t *rs, range_tree_t *rt, uint64_t end)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32:
ASSERT3U(end, <=, UINT32_MAX);
((range_seg32_t *)rs)->rs_end = (uint32_t)end;
break;
case RANGE_SEG64:
((range_seg64_t *)rs)->rs_end = end;
break;
case RANGE_SEG_GAP:
((range_seg_gap_t *)rs)->rs_end = end;
break;
default:
VERIFY(0);
}
}
static inline void
rs_set_fill_raw(range_seg_t *rs, range_tree_t *rt, uint64_t fill)
{
ASSERT3U(rt->rt_type, <=, RANGE_SEG_NUM_TYPES);
switch (rt->rt_type) {
case RANGE_SEG32:
/* fall through */
case RANGE_SEG64:
ASSERT3U(fill, ==, rs_get_end_raw(rs, rt) - rs_get_start_raw(rs,
rt));
break;
case RANGE_SEG_GAP:
((range_seg_gap_t *)rs)->rs_fill = fill;
break;
default:
VERIFY(0);
}
}
static inline void
rs_set_start(range_seg_t *rs, range_tree_t *rt, uint64_t start)
{
ASSERT3U(start, >=, rt->rt_start);
ASSERT(IS_P2ALIGNED(start, 1ULL << rt->rt_shift));
rs_set_start_raw(rs, rt, (start - rt->rt_start) >> rt->rt_shift);
}
static inline void
rs_set_end(range_seg_t *rs, range_tree_t *rt, uint64_t end)
{
ASSERT3U(end, >=, rt->rt_start);
ASSERT(IS_P2ALIGNED(end, 1ULL << rt->rt_shift));
rs_set_end_raw(rs, rt, (end - rt->rt_start) >> rt->rt_shift);
}
static inline void
rs_set_fill(range_seg_t *rs, range_tree_t *rt, uint64_t fill)
{
ASSERT(IS_P2ALIGNED(fill, 1ULL << rt->rt_shift));
rs_set_fill_raw(rs, rt, fill >> rt->rt_shift);
}
typedef void range_tree_func_t(void *arg, uint64_t start, uint64_t size);
void range_tree_init(void);
void range_tree_fini(void);
range_tree_t *range_tree_create_impl(range_tree_ops_t *ops, void *arg,
int (*avl_compare) (const void *, const void *), uint64_t gap);
range_tree_t *range_tree_create(range_tree_ops_t *ops, void *arg);
range_tree_t *range_tree_create_impl(range_tree_ops_t *ops,
range_seg_type_t type, void *arg, uint64_t start, uint64_t shift,
int (*zfs_btree_compare) (const void *, const void *), uint64_t gap);
range_tree_t *range_tree_create(range_tree_ops_t *ops, range_seg_type_t type,
void *arg, uint64_t start, uint64_t shift);
void range_tree_destroy(range_tree_t *rt);
boolean_t range_tree_contains(range_tree_t *rt, uint64_t start, uint64_t size);
range_seg_t *range_tree_find(range_tree_t *rt, uint64_t start, uint64_t size);
boolean_t range_tree_find_in(range_tree_t *rt, uint64_t start, uint64_t size,
uint64_t *ostart, uint64_t *osize);
void range_tree_verify_not_present(range_tree_t *rt,
uint64_t start, uint64_t size);
range_seg_t *range_tree_find(range_tree_t *rt, uint64_t start, uint64_t size);
void range_tree_resize_segment(range_tree_t *rt, range_seg_t *rs,
uint64_t newstart, uint64_t newsize);
uint64_t range_tree_space(range_tree_t *rt);
@@ -120,12 +316,12 @@ void range_tree_remove_xor_add_segment(uint64_t start, uint64_t end,
void range_tree_remove_xor_add(range_tree_t *rt, range_tree_t *removefrom,
range_tree_t *addto);
void rt_avl_create(range_tree_t *rt, void *arg);
void rt_avl_destroy(range_tree_t *rt, void *arg);
void rt_avl_add(range_tree_t *rt, range_seg_t *rs, void *arg);
void rt_avl_remove(range_tree_t *rt, range_seg_t *rs, void *arg);
void rt_avl_vacate(range_tree_t *rt, void *arg);
extern struct range_tree_ops rt_avl_ops;
void rt_btree_create(range_tree_t *rt, void *arg);
void rt_btree_destroy(range_tree_t *rt, void *arg);
void rt_btree_add(range_tree_t *rt, range_seg_t *rs, void *arg);
void rt_btree_remove(range_tree_t *rt, range_seg_t *rs, void *arg);
void rt_btree_vacate(range_tree_t *rt, void *arg);
extern range_tree_ops_t rt_btree_ops;
#ifdef __cplusplus
}