1. 程式人生 > >Memcached原始碼分析之記憶體管理篇之item結構圖及slab結構圖

Memcached原始碼分析之記憶體管理篇之item結構圖及slab結構圖

.Memcached原始碼分析之記憶體管理篇 部落格分類: linuxc
 .
使用命令 set(key, value) 向 memcached 插入一條資料, memcached 內部是如何組織資料呢

一 把資料組裝成 item

memcached 接受到客戶端的資料後, 把資料組裝成 item, item 的格式如下:


    圖1 struct item 的結構

CAS可選:cas的版本號

原始碼中這樣定義 struct item:

/**
 * Structure for storing items within memcached.
 */
typedef struct _stritem {
    struct _stritem *next;
    struct _stritem *prev;
    struct _stritem *h_next;    /* hash chain next */
    rel_time_t      time;       /* least recent access */
    rel_time_t      exptime;    /* expire time */
    int             nbytes;     /* size of data */
    unsigned short  refcount;
    uint8_t         nsuffix;    /* length of flags-and-length string */
    uint8_t         it_flags;   /* ITEM_* above */
    uint8_t         slabs_clsid;/* which slab class we're in */
    uint8_t         nkey;       /* key length, w/terminating null and padding */
    /* this odd type prevents type-punning issues when we do
     * the little shuffle to save space when not using CAS. */
    union {
        uint64_t cas;
        char end;
    } data[];
    /* if it_flags & ITEM_CAS we have 8 bytes CAS */
    /* then null-terminated key */
    /* then " flags length\r\n" (no terminating null) */
    /* then data with terminating \r\n (no terminating null; it's binary!) */
} item;

從原始碼可以得出 item 的結構分兩部分, 第一部分定義 item 結構的屬性, 包括連線其它 item 的指標 (next, prev),

還有最近訪問時間(time), 過期的時間(exptime), 以及資料部分的大小, 標誌位, key的長度, 引用次數, 以及 item 是

從哪個 slabclass 分配而來.

item 結構體的定義使用了一個常用的技巧: 定義空陣列 data, 用來指向 item 資料部分的首地址, 使用空陣列的

好處是 data 指標本身不佔用任何儲存空間, 為 item 分配儲存空間後, data 自然而然就指向資料部分的首地址.

第二部分是 item 的資料, 由 CAS, key, suffix, value 組成.

二 為 item 分配儲存空間

把資料組裝成 item 之前, 必須為 item 分配儲存空間, memcached 不是直接從作業系統分配記憶體的, memcached

內部使用了類似記憶體池的東西, 即slab機制, 來管理記憶體. 記憶體的分配和回收都交給 slab 子系統實現. 所以我們先理解

slab, 再回過頭來看如何為 item 分配儲存空間.

三 使用 slab 管理記憶體

memcached 中, 記憶體的分配和回收, 都是通過 slab 實現的, slab機制相當於記憶體池機制, 實現從作業系統分配一大塊

記憶體, 然後 memcached 自己管理這塊記憶體, 負責分配與回收. 接下來我們詳細剖析 slab 機制.

3.1 關於 slabclass

像一般的記憶體池一樣,  從作業系統分配到一大塊記憶體後, 為了方便管理, 把這大塊記憶體劃分為各種大小的 chunk,

chunk的大小按照一定比例逐漸遞增, 如下圖所示:


   圖2: 各個 slabclass 的 chunk size 按比例遞增

從 slab 分配記憶體的時候, 根據請求記憶體塊的大小, 找到大小最合適的 chunk 所在的 slabclass, 然後從這個

slabclass 找空閒的 chunk 分配出去. 所謂最合適就是指 chunk 的大小能夠滿足要求, 而且碎片最小.

如下圖所示:

 

  圖3 尋找最合適的 slabclass

這種分配方式的缺點是存在記憶體碎片, 例如, 將 100位元組的 item 儲存到一個 128 位元組的 chunk, 就有 28 位元組

的記憶體浪費, 如下圖所示:

 

   圖5 記憶體碎片

3.2 slabclass 的內部實現

slabclass 是由 chunk size 確定的, 同一個 slabclass 內的 chunk 大小都一樣,  每一個 slabclass 要負責管理

一些記憶體, 初始時, 系統為每個 slabclass 分配一個 slab, 一個 slab 就是一個記憶體塊,  其大小等於 1M.  然後每個

slabclass 再把 slab 切分成一個個 chunk, 算一下, 一個 slab 可以切分得到 1M/chunk_size 個chunk.

先來看一下原始碼中 slabclass 的定義:

typedef struct {
    unsigned int size;      /* sizes of items */
    unsigned int perslab;   /* how many items per slab */

    void *slots;           /* list of item ptrs */
    unsigned int sl_curr;   /* total free items in list */

    void *end_page_ptr;         /* pointer to next free item at end of page, or 0 */
    unsigned int end_page_free; /* number of items remaining at end of last alloced page */

    unsigned int slabs;     /* how many slabs were allocated for this class */

    void **slab_list;       /* array of slab pointers */
    unsigned int list_size; /* size of prev array */

    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;

slabclass 的結構圖如下所示:

 

      圖6 slabclass 結構圖

 結合 slabclass 的結構圖, 我們說明一下 slabclass 結構體的各個屬性:

(1) size 和 perslab

size 定義該 slabclass 的 chunk 大小, perslab 表示每個 slab 可以切分成多少個 chunk, 如果一個 slab 等於

1M, 那麼就有 perslab = 1M / size

(2) slots 和 sl_curr

slots 是回收的 item 連結串列, 從某個 slabclass 分配出去一個 item, 當 item 回收的時候,

不是把這 item 使用的記憶體交還給 slab, 而是讓這個 item 掛在 slots 連結串列的尾部. sl_curr 表示當前連結串列中

有多少個回收而來的空閒 item.

(3) slab_list 和 list_size

前面說過, 初始時, memcached 為每個 slabclass 分配一個 slab, 當這個 slab 記憶體塊使用完後, memcached

就分配一個新的 slab, 所以 slabclass 可以擁有多個 slab, 這些 slab 就是通過 slab_list 陣列來管理的, list_size

表示當前 slabclass 有多少個 slab.

(4) end_page_ptr 和 end_page_free

在 subclass 內, 只有最後一個 slab 存在空閒的記憶體, 其它 slab 的 chunk 都分配出去了, end_page_ptr

指向最後一個 slab 中的空閒記憶體塊, end_page_free 表示最後一個 slab 中還剩下多少個空閒 chunk.  圖6

中綠色部分的 chunk 表示空閒 chunk

(5) static item *heads[LARGEST_ID];

      static item *tails[LARGEST_ID];

 當 memcached 沒有足夠的記憶體使用時, 必須選擇性地回收一些 item, 回收採用 LRU 演算法, 這就需要維護

一個按照最近訪問時間排序的 LRU 佇列. 在 memcached 中,

每個 slabclass 維護一個連結串列, 比如 slabclass[i] 的連結串列頭指標為 heads[i], 尾指標為 tails[i],

已分配出去的 item 都儲存在連結串列中. 而且連結串列中 item 按照最近訪問時間排序, 這樣一些連結串列相當於

LRU 佇列.

四 原始碼分析

4.1 do_slabs_newslab

前面說過每個 slabclass 都擁有一些 slab, 當所有 slab 都用完時, memcached 會給它分配一個新的 slab,

do_slabs_newslab 就是做這個工作的.

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;
    char *ptr;

    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {

        MEMCACHED_SLABS_SLABCLASS_ALLOCATE_FAILED(id);
        return 0;
    }

    memset(ptr, 0, (size_t)len);
    p->end_page_ptr = ptr;
    p->end_page_free = p->perslab;

    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
    MEMCACHED_SLABS_SLABCLASS_ALLOCATE(id);

    return 1;
}

 

分配一個新的 slab, 必須把該 slab 的首地址安插在 slab_list 陣列中, 所以先呼叫 grow_slab_list 來確保 slab_list

陣列有足夠的容量, 如果容量不足, grow_slab_list 會對 slab_list 擴容.

然後呼叫 memory_allocate 分配 1M 的記憶體空間, memory_allocate 從預先分配的記憶體取下 1M 大小的

空閒記憶體塊,作為新的 slab.

最後調整 end_page_ptr, 新分配的 slab 全部都是空閒記憶體塊, 所以 end_page_ptr 指向新 slab 的首地址.

這個新 slab 的首地址也被安插在 slab_list 陣列中.

4.2 do_slabs_alloc

這個函式從指定的 slabclass, 即 slabclass[id], 分配大小為 size 的記憶體塊供申請者使用.

 分配的原則是, 優先從 slots 指向的空閒連結串列中分配, 空閒連結串列沒有, 才從 slab 中分配一個空閒的 chunk.

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
    item *it = NULL;

    if (id < POWER_SMALLEST || id > power_largest) {
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, 0);
        return NULL;
    }

    p = &slabclass[id];
    assert(p->sl_curr == 0 || ((item *)p->slots)->slabs_clsid == 0);

 /* 如果不使用 slab 機制, 則直接從作業系統分配 */
#ifdef USE_SYSTEM_MALLOC
    if (mem_limit && mem_malloced + size > mem_limit) {
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, id);
        return 0;
    }
    mem_malloced += size;
    ret = malloc(size);
    MEMCACHED_SLABS_ALLOCATE(size, id, 0, ret);
    return ret;
#endif

    /* fail unless we have space at the end of a recently allocated page,
       we have something on our freelist, or we could allocate a new page */
 /* 確保最後一個slab 有空閒的chunk */
    if (! (p->end_page_ptr != 0 || p->sl_curr != 0 ||
           do_slabs_newslab(id) != 0)) {
        /* We don't have more memory available */
        ret = NULL;
    } else if (p->sl_curr != 0) {
  /* 從空閒list分配一個item */
        /* return off our freelist */
        it = (item *)p->slots;
        p->slots = it->next;
        if (it->next) it->next->prev = 0;
        p->sl_curr--;
        ret = (void *)it;
    } else {
  /* 從最後一個slab中分配一個空閒chunk */
        /* if we recently allocated a whole page, return from that */
        assert(p->end_page_ptr != NULL);
        ret = p->end_page_ptr;
        if (--p->end_page_free != 0) {
            p->end_page_ptr = ((caddr_t)p->end_page_ptr) + p->size;
        } else {
            p->end_page_ptr = 0;
        }
    }

    if (ret) {
        p->requested += size;
        MEMCACHED_SLABS_ALLOCATE(size, id, p->size, ret);
    } else {
        MEMCACHED_SLABS_ALLOCATE_FAILED(size, id);
    }

    return ret;
}

 

4.3 do_slabs_free

把 ptr 指向的 item 歸還給 slabclass[id]

操作很簡單, 把 ptr 指向的 item 掛在 slots 空閒連結串列的最前面

static void do_slabs_free(void *ptr, const size_t size, unsigned int id) {
    slabclass_t *p;
    item *it;

    assert(((item *)ptr)->slabs_clsid == 0);
    assert(id >= POWER_SMALLEST && id <= power_largest);
    if (id < POWER_SMALLEST || id > power_largest)
        return;

    MEMCACHED_SLABS_FREE(size, id, ptr);
    p = &slabclass[id];

#ifdef USE_SYSTEM_MALLOC
    mem_malloced -= size;
    free(ptr);
    return;
#endif

 /* 把 item 歸還給slots指向的空閒連結串列, 插在連結串列的最前面 */
    it = (item *)ptr;
    it->it_flags |= ITEM_SLABBED;
    it->prev = 0;
    it->next = p->slots;
    if (it->next) it->next->prev = it;
    p->slots = it;

    p->sl_curr++;
    p->requested -= size;
    return;
}

4.4 do_item_alloc

從 slab 系統分配一個空閒 item

item *do_item_alloc(char *key, const size_t nkey, const int flags, const rel_time_t exptime, const int nbytes) {
    uint8_t nsuffix;
    item *it = NULL;
    char suffix[40];
    size_t ntotal = item_make_header(nkey + 1, flags, nbytes, suffix, &nsuffix);
    if (settings.use_cas) {
        ntotal += sizeof(uint64_t);
    }

    unsigned int id = slabs_clsid(ntotal);
    if (id == 0)
        return 0;

    mutex_lock(&cache_lock);
    /* do a quick check if we have any expired items in the tail.. */
    item *search;
    rel_time_t oldest_live = settings.oldest_live;

    search = tails[id];
    if (search != NULL && (refcount_incr(&search->refcount) == 2)) {
  /* 先檢查 LRU 佇列最後一個 item 是否超時, 超時的話就把這個 item 分配給使用者 */
        if ((search->exptime != 0 && search->exptime < current_time)
            || (search->time <= oldest_live && oldest_live <= current_time)) {  // dead by flush
            STATS_LOCK();
            stats.reclaimed++;
            STATS_UNLOCK();
            itemstats[id].reclaimed++;
            if ((search->it_flags & ITEM_FETCHED) == 0) {
                STATS_LOCK();
                stats.expired_unfetched++;
                STATS_UNLOCK();
                itemstats[id].expired_unfetched++;
            }
            it = search;
            slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);
   /* 把這個 item 從 LRU 佇列和雜湊表中移除 */
            do_item_unlink_nolock(it, hash(ITEM_key(it), it->nkey, 0));
            /* Initialize the item block: */
            it->slabs_clsid = 0;
  /* 沒有超時的 item, 那就嘗試從 slabclass 分配, 運氣不好的話, 分配失敗,
     那就把 LRU 佇列最後一個 item 剔除, 然後分配給使用者 */
        } else if ((it = slabs_alloc(ntotal, id)) == NULL) {
            if (settings.evict_to_free == 0) {
                itemstats[id].outofmemory++;
                pthread_mutex_unlock(&cache_lock);
                return NULL;
            }
            itemstats[id].evicted++;
            itemstats[id].evicted_time = current_time - search->time;
            if (search->exptime != 0)
                itemstats[id].evicted_nonzero++;
            if ((search->it_flags & ITEM_FETCHED) == 0) {
                STATS_LOCK();
                stats.evicted_unfetched++;
                STATS_UNLOCK();
                itemstats[id].evicted_unfetched++;
            }
            STATS_LOCK();
            stats.evictions++;
            STATS_UNLOCK();
            it = search;
            slabs_adjust_mem_requested(it->slabs_clsid, ITEM_ntotal(it), ntotal);
   /* 把這個 item 從 LRU 佇列和雜湊表中移除 */
            do_item_unlink_nolock(it, hash(ITEM_key(it), it->nkey, 0));
            /* Initialize the item block: */
            it->slabs_clsid = 0;
        } else {
            refcount_decr(&search->refcount);
        }
 /* LRU 佇列是空的, 或者鎖住了, 那就只能從 slabclass 分配記憶體 */
    } else {
        /* If the LRU is empty or locked, attempt to allocate memory */
        it = slabs_alloc(ntotal, id);
        if (search != NULL)
            refcount_decr(&search->refcount);
    }

    if (it == NULL) {
        itemstats[id].outofmemory++;
        /* Last ditch effort. There was a very rare bug which caused
         * refcount leaks. We leave this just in case they ever happen again.
         * We can reasonably assume no item can stay locked for more than
         * three hours, so if we find one in the tail which is that old,
         * free it anyway.
         */
        if (search != NULL &&
            search->refcount != 2 &&
            search->time + TAIL_REPAIR_TIME < current_time) {
            itemstats[id].tailrepairs++;
            search->refcount = 1;
            do_item_unlink_nolock(search, hash(ITEM_key(search), search->nkey, 0));
        }
        pthread_mutex_unlock(&cache_lock);
        return NULL;
    }

    assert(it->slabs_clsid == 0);
    assert(it != heads[id]);

 /* 順便對 item 做一些初始化 */
    /* Item initialization can happen outside of the lock; the item's already
     * been removed from the slab LRU.
     */
    it->refcount = 1;     /* the caller will have a reference */
    pthread_mutex_unlock(&cache_lock);
    it->next = it->prev = it->h_next = 0;
    it->slabs_clsid = id;

    DEBUG_REFCNT(it, '*');
    it->it_flags = settings.use_cas ? ITEM_CAS : 0;
    it->nkey = nkey;
    it->nbytes = nbytes;
    memcpy(ITEM_key(it), key, nkey);
    it->exptime = exptime;
    memcpy(ITEM_suffix(it), suffix, (size_t)nsuffix);
    it->nsuffix = nsuffix;
    return it;
}

4.5 do_item_link

形成了一個完成的 item 後, 就要把它放入兩個資料結構中, 一是 memcached 的雜湊表,

memcached 執行過程中只有一個雜湊表, 二是 item 所在的 slabclass 的 LRU 佇列.

如 圖6 所示, 每個 slabclass 都有一個 LRU 佇列

int do_item_link(item *it, const uint32_t hv) {
    MEMCACHED_ITEM_LINK(ITEM_key(it), it->nkey, it->nbytes);
    assert((it->it_flags & (ITEM_LINKED|ITEM_SLABBED)) == 0);
    mutex_lock(&cache_lock);
    it->it_flags |= ITEM_LINKED;
    it->time = current_time;

    STATS_LOCK();
    stats.curr_bytes += ITEM_ntotal(it);
    stats.curr_items += 1;
    stats.total_items += 1;
    STATS_UNLOCK();

    /* Allocate a new CAS ID on link. */
    ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);
 /* 把 item 放入雜湊表 */
    assoc_insert(it, hv);
 /* 把 item 放入 LRU 佇列*/
    item_link_q(it);
    refcount_incr(&it->refcount);
    pthread_mutex_unlock(&cache_lock);

    return 1;
}
 

4.6 do_item_unlink

do_item_unlink 與 do_item_unlink 做的工作相反, 把 item 從雜湊表和 LRU 佇列中刪除,

而且還釋放掉 item 所佔的記憶體 (其實只是把 item 放到空閒連結串列中).

void do_item_unlink(item *it, const uint32_t hv) {
    MEMCACHED_ITEM_UNLINK(ITEM_key(it), it->nkey, it->nbytes);
    mutex_lock(&cache_lock);
    if ((it->it_flags & ITEM_LINKED) != 0) {
        it->it_flags &= ~ITEM_LINKED;
        STATS_LOCK();
        stats.curr_bytes -= ITEM_ntotal(it);
        stats.curr_items -= 1;
        STATS_UNLOCK();
  /* 從雜湊表中刪除 item */
        assoc_delete(ITEM_key(it), it->nkey, hv);
  /* 從 LRU 佇列中刪除 item */
        item_unlink_q(it);
  /* 釋放 item 所佔的記憶體 */
        do_item_remove(it);
    }
    pthread_mutex_unlock(&cache_lock);
}

4.7 item_get

根據 key 找對應的 item, 為了加快查詢速度, memcached 使用一個雜湊表對 key 和 item 所在的記憶體

地址做對映. item_get 直接從雜湊表中查詢就可以了, 當然找到了還要檢查 item 是否超時. 超時了的 item

將從雜湊表和 LRU 佇列中刪除掉

/** wrapper around assoc_find which does the lazy expiration logic */
item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {
    mutex_lock(&cache_lock);
 /* 從雜湊表中找 item */
    item *it = assoc_find(key, nkey, hv);
    if (it != NULL) {
        refcount_incr(&it->refcount);
        /* Optimization for slab reassignment. prevents popular items from
         * jamming in busy wait. Can only do this here to satisfy lock order
         * of item_lock, cache_lock, slabs_lock. */
        if (slab_rebalance_signal &&
            ((void *)it >= slab_rebal.slab_start && (void *)it < slab_rebal.slab_end)) {
            do_item_unlink_nolock(it, hv);
            do_item_remove(it);
            it = NULL;
        }
    }
    pthread_mutex_unlock(&cache_lock);
    int was_found = 0;

    if (settings.verbose > 2) {
        if (it == NULL) {
            fprintf(stderr, "> NOT FOUND %s", key);
        } else {
            fprintf(stderr, "> FOUND KEY %s", ITEM_key(it));
            was_found++;
        }
    }

 /* 找到了, 然後檢查是否超時 */
    if (it != NULL) {
        if (settings.oldest_live != 0 && settings.oldest_live <= current_time &&
            it->time <= settings.oldest_live) {
            do_item_unlink(it, hv);
            do_item_remove(it);
            it = NULL;
            if (was_found) {
                fprintf(stderr, " -nuked by flush");
            }
        } else if (it->exptime != 0 && it->exptime <= current_time) {
            do_item_unlink(it, hv);
            do_item_remove(it);
            it = NULL;
            if (was_found) {
                fprintf(stderr, " -nuked by expire");
            }
        } else {
            it->it_flags |= ITEM_FETCHED;
            DEBUG_REFCNT(it, '+');
        }
    }

    if (settings.verbose > 2)
        fprintf(stderr, "\n");

    return it;
}