NUMA node와 zone은 allocation 후보를 어떻게 나눕니까

free memory가 충분한데도 특정 allocation이 실패할 수 있습니다. 요청한 device가 낮은 주소만 접근할 수 있거나, task가 일부 node만 사용할 수 있거나, 가까운 node에서 시작한 탐색이 제한된 후보 안에서 끝날 수 있기 때문입니다. allocator가 보는 것은 시스템 전체의 free 합계 하나가 아닙니다.

node와 zone은 이 후보 공간을 조직하는 두 축입니다. node는 locality와 topology를, zone은 주소·용도 제약을 표현합니다. Linux v6.1의 pg_data_t, zone, zonelist를 따라 이 장부가 어떻게 연결되는지 살펴보겠습니다.

Node는 DIMM 하나와 같은 말이 아닙니다

NUMA node는 memory 접근 locality를 관리하는 단위입니다. 여러 CPU가 하나의 node와 가깝게 연결될 수 있고, CPU가 없는 memory-only node도 가능합니다. memory tier나 interleaving이 개입한 시스템에서 node 하나를 hole 없는 단일 DIMM 범위와 같다고 보는 것은 지나친 단순화입니다.

kernel은 node별 상태를 pg_data_t, 즉 struct pglist_data에 둡니다. node_id가 식별자이고 node_start_pfn, node_spanned_pages, node_present_pages가 범위를 설명합니다. span은 시작부터 끝까지의 넓이를 나타내므로 hole을 포함할 수 있습니다. present count와 span이 같아야 한다는 조건은 없습니다. 범위의 끝은 시작 PFN에 span을 더한 exclusive end로 읽는 것이 맞습니다.

node에는 node_zones[MAX_NR_ZONES]와 zonelist가 있습니다. reclaim에 필요한 LRU·통계·daemon 관련 상태도 node 단위로 모입니다. v6.1의 node flag인 PGDAT_DIRTY, PGDAT_WRITEBACK, PGDAT_RECLAIM_LOCKED는 reclaim 관찰과 실행을 위한 상태이지 개별 page의 dirty·locked bit를 복제한 것이 아닙니다.

NUMA가 활성화된 구성에서는 node가 하나뿐이어도 같은 node·zone 기반 API를 사용합니다. 여러 node의 시작·끝 span이 주소상 겹쳐 보이는 배치가 있어도 개별 page의 node 소속을 두 개로 읽지는 않습니다. NODE_DATA(nid)는 node 장부를, page_to_nid()는 page의 소속을 얻는 helper입니다. topology를 확인하는 numactl --hardware의 거리 정보와 zone의 여유량은 다른 자료입니다.

PGDAT_DIRTY와 PGDAT_WRITEBACK은 reclaim에서 관찰한 dirty·writeback 부담을 나타내며 shrink_node()와 연결됩니다. PGDAT_RECLAIM_LOCKED는 node_reclaim()의 동시 실행을 제어하는 상태입니다. 개별 page가 locked라는 뜻으로 확장하지 않는 것이 중요합니다.

Zone은 같은 이름이어도 node마다 다른 객체입니다

DMA, DMA32, NORMAL 같은 이름은 zone 종류입니다. 시스템 전체에 NORMAL 객체가 하나만 있는 것이 아니라 각 node가 자기 zone을 가집니다. architecture가 지원하지 않는 zone은 configuration에서 빠질 수 있고, 지원하더라도 특정 node에서는 비어 있을 수 있습니다.

x86의 대표적인 배치에서는 DMA가 낮은 16 MiB, DMA32가 4 GiB 아래의 범위와 연결됩니다. 이 수치를 모든 architecture에 적용하면 안 됩니다. NORMAL은 일반 kernel allocation의 주요 대상이며 HIGHMEM은 직접 mapping 제약이 있는 configuration에서 중요합니다. 64-bit라고 모든 가능한 physical memory가 항상 같은 direct-map 조건을 갖는다고 일반화할 필요도 없습니다.

MOVABLE은 kernel이 이동 가능한 용도로 다룰 수 있도록 떼어 둔 영역입니다. kernelcore·movablecore 같은 boot parameter가 그 구성에 관여합니다. 모든 movable page가 반드시 ZONE_MOVABLE에만 있는 것도, ZONE_MOVABLE의 page가 아무 조건 없이 즉시 migration되는 것도 아닙니다. pin이나 현재 상태는 별도 제약입니다.

ZONE_DEVICE는 device memory 관리와 연결되는 zone입니다. ordinary buddy allocation이 부족하면 DMA 다음에 DEVICE에서 무조건 가져오는 식의 fallback 순서를 만들면 안 됩니다. descriptor를 제공하는 목적과 일반 RAM 공급원이 되는 목적은 다릅니다.

Zonelist는 실제 탐색 순서를 표현합니다

struct zonelist의 핵심 entry는 zoneref입니다. zone pointer와 zone index를 함께 저장하여 후보를 순회하면서 요청의 highest_zoneidx를 적용할 수 있게 합니다. index는 physical address가 아니라 zone 종류의 순서를 나타냅니다.

node별 fallback zonelist는 가까운 곳을 우선하고 다른 node의 허용 zone까지 이어지는 탐색을 표현합니다. __GFP_THISNODE가 적용된 no-fallback 선택은 이 범위를 제한합니다. 여기에 nodemask, cpuset, memory policy가 추가로 개입하므로 zonelist에 실린 entry가 곧 caller에게 허용된 최종 후보라는 뜻은 아닙니다.

populated_zone()도 “allocation 성공 가능한 zone”의 동의어가 아닙니다. present memory가 있는지와 free block·watermark·접근 권한 검사는 별개입니다. zoneref helper로 entry를 얻은 뒤에도 실제 allocation 경로가 여러 조건을 검사하는 이유입니다.

zoneref의 zone·종류·node를 읽는 helper는 각각 zonelist_zone(), zonelist_zone_idx(), zonelist_node_idx()입니다. node_zones[]에는 비어 있는 종류의 slot도 있지만, build_zonerefs_node()는 populated_zone()을 통과한 객체만 zoneref에 추가합니다. node 배열의 slot과 실제 탐색 목록을 구별해야 합니다. for_each_zone()과 for_each_populated_zone()도 같은 순회가 아닙니다.

nr_zones의 선언 주석은 populated zone 수라고 설명하지만, v6.1의 init_currently_empty_zone()은 초기화한 zone의 index에 1을 더한 값까지 이 field를 올립니다. 중간 zone이 비어 있으면 단순한 populated 객체 개수와 다를 수 있으므로 순회 상한의 의미를 구현과 함께 읽어야 합니다. name도 enum 식별자를 그대로 출력하는 field는 아닙니다. 이 tag의 일반 이름은 Normal·Movable 등입니다.

Zone의 세 가지 크기를 나누어 읽습니다

zone에도 zone_start_pfn, spanned_pages, present_pages, managed_pages가 있습니다. span은 주소상의 범위이고 present는 실제 존재하는 page 수이며 managed는 page allocator가 관리하는 규모입니다. reserved 영역이나 관리 대상에서 제외되는 부분 때문에 값이 달라질 수 있습니다.

managed_pages는 현재 free 수가 아닙니다. allocation되어 사용 중인 page도 managed에 포함될 수 있습니다. 현재 free 상황은 free area와 zone 통계를 별도로 읽어야 합니다. zone->contiguous 같은 표시도 주소 범위 검사와 관련된 최적화 정보이지 zone 전체가 하나의 free block이라는 뜻은 아닙니다.

zone_pgdat는 소속 node로 돌아가는 연결이고 name은 진단에 쓰입니다. free_area에는 order별 buddy free list가 있으며 zone->lock이 이 list와 관련 accounting 변경을 보호합니다. per-CPU pageset과 per-CPU vmstat는 공통 lock의 경합을 줄이지만, 통계값과 list 상태가 언제나 하나의 원자적 snapshot으로 보인다는 보장은 없습니다.

per-CPU 통계에는 per_cpu_zonestats가 있고 zone의 vm_stat·vm_numa_event와 함께 일반 memory·NUMA 사건을 추적합니다. PCP의 threshold를 갱신하기 위한 pageset_high·pageset_batch와 CPU별 실제 pageset도 구별합니다. PFN이 zone span 안에 있는지는 zone_spans_pfn()으로 판단하며 시작은 포함하고 끝은 제외합니다. 이 범위 검사 역시 present·free 판별을 대신하지 않습니다.

Watermark와 reserve는 서로 다른 보호 장치입니다

zone의 _watermark 배열은 MIN·LOW·HIGH와 구성에 따른 promotion 기준을 담고, watermark_boost는 fragmentation 등의 영향으로 기준을 올리는 데 쓰입니다. 낮아진 free 상황에서 kswapd를 깨울지, allocation을 허용할지는 요청의 GFP와 order, 내부 allocation flag까지 고려합니다. HIGH를 넘으면 어떤 reclaim도 절대로 실행하지 않는 식의 전역 규칙은 아닙니다.

lowmem_reserve[]는 더 높은 zone을 쓸 수 있는 요청이 희소한 낮은 zone을 모두 소모하지 않도록 보호하는 값입니다. nr_reserved_highatomic은 atomic higher-order 요청을 위한 별도 reserve 장부입니다. 두 값을 모두 “예약된 같은 page 묶음”으로 합쳐 이해하면 watermark 계산을 잘못 읽게 됩니다.

zone flag인 ZONE_BOOSTED_WATERMARK는 boost 관련 kswapd 동작과, ZONE_RECLAIM_ACTIVE는 reclaim 중의 정책과 연결됩니다. node flag, zone flag, page flag가 각각 어느 범위의 상태인지 구별하면 free 총량에서 실제 allocation 실패 지점까지 한 단계씩 좁혀 갈 수 있습니다.

page의 node ID를 항상 flags의 고정 bit field에서 얻는다고 가정해서도 안 됩니다. NODE_NOT_IN_PAGE_FLAGS 구성에서는 section 정보 등을 통한 별도 경로를 사용합니다. zone·node 식별 helper를 쓰는 이유 중 하나입니다.

wake_all_kswapds()도 모든 node를 무조건 깨우는 호출로 읽지는 않습니다. allocation context의 highest zone과 nodemask에 맞춰 후보를 순회합니다. 어느 watermark를 쓰는지와 NUMA promotion 기준이 작동하는지는 reclaim 모드까지 함께 확인해야 합니다.

전체 선언에서 범위와 수명 보호를 확인합니다

필드 이름만으로는 어느 lock을 잡아야 값의 관계를 믿을 수 있는지 알기 어렵습니다. 아래는 Linux v6.1의 include/linux/mmzone.h에 있는 네 선언의 전문입니다. SPDX는 GPL-2.0이며 들여쓰기만 정리했습니다. configuration 분기를 남겼으므로 특정 실행 중인 kernel의 고정된 byte layout을 뜻하지는 않습니다.

먼저 zoneref는 실제 zone 객체를 가리키며, zonelist는 끝을 표시할 추가 entry까지 저장합니다. 같은 zone이 여러 node의 탐색 목록에 참조될 수 있어 목록 entry의 소유와 zone 객체의 소유가 같지 않습니다.

struct zoneref {
    struct zone *zone;    /* Pointer to actual zone */
    int zone_idx;        /* zone_idx(zoneref->zone) */
};
struct zonelist {
    struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};

pg_data_t의 앞부분은 topology와 범위, 중간은 reclaim·compaction의 실행 상태, 뒤쪽은 LRU와 통계를 묶습니다. node_size_lock은 해당 configuration에서 node 범위와 nr_zones, deferred 초기화 경계를 안정적으로 읽고 변경하는 규칙에 연결되며 zone->lock과 span lock보다 바깥 순서입니다. 반면 kswapd_wait, pfmemalloc_wait, reclaim_wait는 daemon·reserve·throttling의 대기 관계를 나타냅니다. 대기열이 범위 field를 보호하는 lock을 대신하지는 않습니다.

typedef struct pglist_data {
    /*
     * node_zones contains just the zones for THIS node. Not all of the
     * zones may be populated, but it is the full list. It is referenced by
     * this node's node_zonelists as well as other node's node_zonelists.
     */
    struct zone node_zones[MAX_NR_ZONES];

    /*
     * node_zonelists contains references to all zones in all nodes.
     * Generally the first zones will be references to this node's
     * node_zones.
     */
    struct zonelist node_zonelists[MAX_ZONELISTS];

    int nr_zones; /* number of populated zones in this node */
#ifdef CONFIG_FLATMEM    /* means !SPARSEMEM */
    struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
    struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
    /*
     * Must be held any time you expect node_start_pfn,
     * node_present_pages, node_spanned_pages or nr_zones to stay constant.
     * Also synchronizes pgdat->first_deferred_pfn during deferred page
     * init.
     *
     * pgdat_resize_lock() and pgdat_resize_unlock() are provided to
     * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG
     * or CONFIG_DEFERRED_STRUCT_PAGE_INIT.
     *
     * Nests above zone->lock and zone->span_seqlock
     */
    spinlock_t node_size_lock;
#endif
    unsigned long node_start_pfn;
    unsigned long node_present_pages; /* total number of physical pages */
    unsigned long node_spanned_pages; /* total size of physical page
                         range, including holes */
    int node_id;
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;

    /* workqueues for throttling reclaim for different reasons. */
    wait_queue_head_t reclaim_wait[NR_VMSCAN_THROTTLE];

    atomic_t nr_writeback_throttled;/* nr of writeback-throttled tasks */
    unsigned long nr_reclaim_start;    /* nr pages written while throttled
                     * when throttling started. */
#ifdef CONFIG_MEMORY_HOTPLUG
    struct mutex kswapd_lock;
#endif
    struct task_struct *kswapd;    /* Protected by kswapd_lock */
    int kswapd_order;
    enum zone_type kswapd_highest_zoneidx;

    int kswapd_failures;        /* Number of 'reclaimed == 0' runs */

#ifdef CONFIG_COMPACTION
    int kcompactd_max_order;
    enum zone_type kcompactd_highest_zoneidx;
    wait_queue_head_t kcompactd_wait;
    struct task_struct *kcompactd;
    bool proactive_compact_trigger;
#endif
    /*
     * This is a per-node reserve of pages that are not available
     * to userspace allocations.
     */
    unsigned long        totalreserve_pages;

#ifdef CONFIG_NUMA
    /*
     * node reclaim becomes active if more unmapped pages exist.
     */
    unsigned long        min_unmapped_pages;
    unsigned long        min_slab_pages;
#endif /* CONFIG_NUMA */

    /* Write-intensive fields used by page reclaim */
    CACHELINE_PADDING(_pad1_);

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
    /*
     * If memory initialisation on large machines is deferred then this
     * is the first PFN that needs to be initialised.
     */
    unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    struct deferred_split deferred_split_queue;
#endif

#ifdef CONFIG_NUMA_BALANCING
    /* start time in ms of current promote rate limit period */
    unsigned int nbp_rl_start;
    /* number of promote candidate pages at start time of current rate limit period */
    unsigned long nbp_rl_nr_cand;
    /* promote threshold in ms */
    unsigned int nbp_threshold;
    /* start time in ms of current promote threshold adjustment period */
    unsigned int nbp_th_start;
    /*
     * number of promote candidate pages at stat time of current promote
     * threshold adjustment period
     */
    unsigned long nbp_th_nr_cand;
#endif
    /* Fields commonly accessed by the page reclaim scanner */

    /*
     * NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
     *
     * Use mem_cgroup_lruvec() to look up lruvecs.
     */
    struct lruvec        __lruvec;

    unsigned long        flags;

#ifdef CONFIG_LRU_GEN
    /* kswap mm walk data */
    struct lru_gen_mm_walk    mm_walk;
#endif

    CACHELINE_PADDING(_pad2_);

    /* Per-node vmstats */
    struct per_cpu_nodestat __percpu *per_cpu_nodestats;
    atomic_long_t        vm_stat[NR_VM_NODE_STAT_ITEMS];
#ifdef CONFIG_NUMA
    struct memory_tier __rcu *memtier;
#endif
} pg_data_t;

kswapd_order와 kswapd_highest_zoneidx는 background reclaim이 어느 요청까지 돕는지 전달하고, kswapd_failures는 회수 진전이 없던 실행을 추적합니다. COMPACTION 구성의 kcompactd 상태는 별도의 daemon과 요청 order를 가집니다. first_deferred_pfn은 아직 초기화할 descriptor의 경계이며 free-list의 첫 PFN이 아닙니다. THP의 deferred_split_queue와 NUMA balancing의 promotion rate·threshold 장부도 서로 다른 목적의 상태입니다.

__lruvec 주석에 있는 MEMCG 조건도 중요합니다. memcg를 사용하면 이 field를 무조건 직접 쓰지 않고 mem_cgroup_lruvec()로 해당 cgroup과 node의 LRU를 찾습니다. per_cpu_nodestats와 vm_stat는 통계 계층이고, memtier의 __rcu 표시는 tier pointer의 접근·수명 규칙을 따로 따라야 한다는 뜻입니다. node pointer를 얻었다는 이유로 그 아래의 모든 객체가 영구히 고정되지는 않습니다.

zone 선언에는 allocation의 read-mostly 정책, buddy list, compaction scanner, 통계가 함께 있습니다. zone_start_pfn과 spanned_pages의 runtime 관계는 hotplug 구성의 span seqlock으로 보호합니다. present_pages의 변화가 허용되지 않는 독자는 memory hotplug의 안정화 규칙도 지켜야 합니다. managed_pages가 atomic type이라는 사실만으로 이 여러 field를 한 순간의 일관된 snapshot으로 읽을 수 있는 것은 아닙니다.

struct zone {
    /* Read-mostly fields */

    /* zone watermarks, access with *_wmark_pages(zone) macros */
    unsigned long _watermark[NR_WMARK];
    unsigned long watermark_boost;

    unsigned long nr_reserved_highatomic;

    /*
     * We don't know if the memory that we're going to allocate will be
     * freeable or/and it will be released eventually, so to avoid totally
     * wasting several GB of ram we must reserve some of the lower zone
     * memory (otherwise we risk to run OOM on the lower zones despite
     * there being tons of freeable ram on the higher zones).  This array is
     * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
     * changes.
     */
    long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
    int node;
#endif
    struct pglist_data    *zone_pgdat;
    struct per_cpu_pages    __percpu *per_cpu_pageset;
    struct per_cpu_zonestat    __percpu *per_cpu_zonestats;
    /*
     * the high and batch values are copied to individual pagesets for
     * faster access
     */
    int pageset_high;
    int pageset_batch;

#ifndef CONFIG_SPARSEMEM
    /*
     * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
     * In SPARSEMEM, this map is stored in struct mem_section
     */
    unsigned long        *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

    /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
    unsigned long        zone_start_pfn;

    /*
     * spanned_pages is the total pages spanned by the zone, including
     * holes, which is calculated as:
     *     spanned_pages = zone_end_pfn - zone_start_pfn;
     *
     * present_pages is physical pages existing within the zone, which
     * is calculated as:
     *    present_pages = spanned_pages - absent_pages(pages in holes);
     *
     * present_early_pages is present pages existing within the zone
     * located on memory available since early boot, excluding hotplugged
     * memory.
     *
     * managed_pages is present pages managed by the buddy system, which
     * is calculated as (reserved_pages includes pages allocated by the
     * bootmem allocator):
     *    managed_pages = present_pages - reserved_pages;
     *
     * cma pages is present pages that are assigned for CMA use
     * (MIGRATE_CMA).
     *
     * So present_pages may be used by memory hotplug or memory power
     * management logic to figure out unmanaged pages by checking
     * (present_pages - managed_pages). And managed_pages should be used
     * by page allocator and vm scanner to calculate all kinds of watermarks
     * and thresholds.
     *
     * Locking rules:
     *
     * zone_start_pfn and spanned_pages are protected by span_seqlock.
     * It is a seqlock because it has to be read outside of zone->lock,
     * and it is done in the main allocator path.  But, it is written
     * quite infrequently.
     *
     * The span_seq lock is declared along with zone->lock because it is
     * frequently read in proximity to zone->lock.  It's good to
     * give them a chance of being in the same cacheline.
     *
     * Write access to present_pages at runtime should be protected by
     * mem_hotplug_begin/done(). Any reader who can't tolerant drift of
     * present_pages should use get_online_mems() to get a stable value.
     */
    atomic_long_t        managed_pages;
    unsigned long        spanned_pages;
    unsigned long        present_pages;
#if defined(CONFIG_MEMORY_HOTPLUG)
    unsigned long        present_early_pages;
#endif
#ifdef CONFIG_CMA
    unsigned long        cma_pages;
#endif

    const char        *name;

#ifdef CONFIG_MEMORY_ISOLATION
    /*
     * Number of isolated pageblock. It is used to solve incorrect
     * freepage counting problem due to racy retrieving migratetype
     * of pageblock. Protected by zone->lock.
     */
    unsigned long        nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
    /* see spanned/present_pages for more description */
    seqlock_t        span_seqlock;
#endif

    int initialized;

    /* Write-intensive fields used from the page allocator */
    CACHELINE_PADDING(_pad1_);

    /* free areas of different sizes */
    struct free_area    free_area[MAX_ORDER];

    /* zone flags, see below */
    unsigned long        flags;

    /* Primarily protects free_area */
    spinlock_t        lock;

    /* Write-intensive fields used by compaction and vmstats. */
    CACHELINE_PADDING(_pad2_);

    /*
     * When free pages are below this point, additional steps are taken
     * when reading the number of free pages to avoid per-cpu counter
     * drift allowing watermarks to be breached
     */
    unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* pfn where compaction free scanner should start */
    unsigned long        compact_cached_free_pfn;
    /* pfn where compaction migration scanner should start */
    unsigned long        compact_cached_migrate_pfn[ASYNC_AND_SYNC];
    unsigned long        compact_init_migrate_pfn;
    unsigned long        compact_init_free_pfn;
#endif

#ifdef CONFIG_COMPACTION
    /*
     * On compaction failure, 1<<compact_defer_shift compactions
     * are skipped before trying again. The number attempted since
     * last failure is tracked with compact_considered.
     * compact_order_failed is the minimum compaction failed order.
     */
    unsigned int        compact_considered;
    unsigned int        compact_defer_shift;
    int            compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* Set to true when the PG_migrate_skip bits should be cleared */
    bool            compact_blockskip_flush;
#endif

    bool            contiguous;

    CACHELINE_PADDING(_pad3_);
    /* Zone statistics */
    atomic_long_t        vm_stat[NR_VM_ZONE_STAT_ITEMS];
    atomic_long_t        vm_numa_event[NR_VM_NUMA_EVENT_ITEMS];
} ____cacheline_internodealigned_in_smp;

nr_isolate_pageblock은 isolated pageblock 수를 zone lock 아래 관리하는 장부입니다. initialized와 contiguous는 초기화·범위 검사에 쓰이며 free block의 존재를 보증하지 않습니다. compaction의 cached·initial PFN은 다음 scanner 위치이고, compact_considered·compact_defer_shift·compact_order_failed는 실패 후 재시도 간격을 조정합니다. compact_blockskip_flush는 skip 표시의 정리와 연결됩니다.

percpu_drift_mark 아래에서는 per-CPU counter의 오차가 watermark를 넘지 않도록 free 수를 더 신중히 읽습니다. 마지막 vm_stat·vm_numa_event 배열과 cacheline padding은 갱신 경합을 줄이기 위한 배치입니다. 이런 field를 모두 “현재 free page 수”로 환원하면 allocator의 정책, topology, scanner 진행, 통계 사이의 경계를 잃습니다.

참고 자료