1. 程式人生 > >memcached原始碼分析-----slab automove和slab rebalance

memcached原始碼分析-----slab automove和slab rebalance

需求:

        考慮這樣的一個情景:在一開始,由於業務原因向memcached儲存大量長度為1KB的資料,也就是說memcached伺服器程序裡面有很多大小為1KB的item。現在由於業務調整需要儲存大量10KB的資料,並且很少使用1KB的那些資料了。由於資料越來越多,記憶體開始吃緊。大小為10KB的那些item頻繁訪問,並且由於記憶體不夠需要使用LRU淘汰一些10KB的item。

        對於上面的情景,會不會覺得大量1KB的item實在太浪費了。由於很少訪問這些item,所以即使它們超時過期了,還是會佔據著雜湊表和LRU佇列。LRU佇列還好,不同大小的item使用不同的LRU佇列。但對於雜湊表來說大量的殭屍item會增加雜湊衝突的可能性,並且在

遷移雜湊表的時候也浪費時間。有沒有辦法幹掉這些item?使用LRU爬蟲+lru_crawler命令是可以強制幹掉這些殭屍item。但幹掉這些殭屍item後,它們佔據的記憶體是歸還到1KB的那些slab分配器中。1KB的slab分配器不會為10KB的item分配記憶體。所以還是功虧一簣。

        那有沒有別的辦法呢?是有的。memcached提供的slab automove 和 rebalance兩個東西就是完成這個功能的。在預設情況下,memcached不啟動這個功能,所以要想使用這個功能必須在啟動memcached的時候加上引數-o slab_reassign。之後就可以在客戶端傳送命令slabsreassign <source class> <dest class>

,手動將source class的記憶體頁分給dest class。後文會把這個工作稱為記憶體頁重分配。而命令slabs automove則是讓memcached自動檢測是否需要進行記憶體頁重分配,如果需要的話就自動去操作,這樣一切都不需要人工的干預。

        如果在啟動memcached的時候使用了引數-o slab_reassign,那麼就會把settings.slab_reassign賦值為true(該變數的預設值為false)。還記得《slab記憶體分配器》說到的每一個記憶體頁的大小嗎?在do_slabs_newslab函式中,一個記憶體頁的大小會根據settings.slab_reassign是否為true而不同。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
	//settings.slab_reassign的預設值為false
    int len = settings.slab_reassign ? settings.item_size_max
        : p->size * p->perslab;

	//len就是一個記憶體頁的大小
	...
}

        當settings.slab_reassign為true,也就是啟動rebalance功能的時候,slabclass陣列中所有slabclass_t的記憶體頁都是一樣大的,等於settings.item_size_max(預設為1MB)。這樣做的好處就是在需要將一個記憶體頁從某一個slabclass_t強搶給另外一個slabclass_t時,比較好處理。不然的話,slabclass[i]從slabclass[j] 搶到的一個記憶體頁可以切分為n個item,而從slabclass[k]搶到的一個記憶體頁卻切分為m個item,而本身的一個記憶體頁有s個item。這樣的話是相當混亂的。假如畢竟統一了記憶體頁大小,那麼無論從哪裡搶到的記憶體頁都是切分成一樣多的item個數。


啟動和終止rebalance:

        main函式會呼叫start_slab_maintenance_thread函式啟動rebalance執行緒和automove執行緒。main函式是在settings.slab_reassign為true時才會呼叫的。

//slabs.c檔案
static pthread_cond_t maintenance_cond = PTHREAD_COND_INITIALIZER;
static pthread_cond_t slab_rebalance_cond = PTHREAD_COND_INITIALIZER;
static volatile int do_run_slab_thread = 1;
static volatile int do_run_slab_rebalance_thread = 1;

#define DEFAULT_SLAB_BULK_CHECK 1
int slab_bulk_check = DEFAULT_SLAB_BULK_CHECK;

static pthread_mutex_t slabs_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t slabs_rebalance_lock = PTHREAD_MUTEX_INITIALIZER;

static pthread_t maintenance_tid;
static pthread_t rebalance_tid;



//由main函式呼叫,如果settings.slab_reassign為false將不會呼叫本函式(預設是false)
int start_slab_maintenance_thread(void) {
    int ret;
    slab_rebalance_signal = 0;
    slab_rebal.slab_start = NULL;
    char *env = getenv("MEMCACHED_SLAB_BULK_CHECK");
    if (env != NULL) {
        slab_bulk_check = atoi(env);
        if (slab_bulk_check == 0) {
            slab_bulk_check = DEFAULT_SLAB_BULK_CHECK;
        }
    }

    if (pthread_cond_init(&slab_rebalance_cond, NULL) != 0) {
        fprintf(stderr, "Can't intiialize rebalance condition\n");
        return -1;
    }
    pthread_mutex_init(&slabs_rebalance_lock, NULL);

    if ((ret = pthread_create(&maintenance_tid, NULL,
                              slab_maintenance_thread, NULL)) != 0) {
        fprintf(stderr, "Can't create slab maint thread: %s\n", strerror(ret));
        return -1;
    }
    if ((ret = pthread_create(&rebalance_tid, NULL,
                              slab_rebalance_thread, NULL)) != 0) {
        fprintf(stderr, "Can't create rebal thread: %s\n", strerror(ret));
        return -1;
    }
    return 0;
}

void stop_slab_maintenance_thread(void) {
    mutex_lock(&cache_lock);
    do_run_slab_thread = 0;
    do_run_slab_rebalance_thread = 0;
    pthread_cond_signal(&maintenance_cond);
    pthread_mutex_unlock(&cache_lock);

    /* Wait for the maintenance thread to stop */
    pthread_join(maintenance_tid, NULL);
    pthread_join(rebalance_tid, NULL);
}

        要注意的是,start_slab_maintenance_thread函式啟動了兩個執行緒:rebalance執行緒和automove執行緒。automove執行緒會自動檢測是否需要進行記憶體頁重分配。如果檢測到需要重分配,那麼就會叫rebalance執行緒執行這個記憶體頁重分配工作。

        預設情況下是不開啟自動檢測功能的,即使在啟動memcached的時候加入了-o slab_reassign引數。自動檢測功能由全域性變數settings.slab_automove控制(預設值為0,0就是不開啟)。如果要開啟可以在啟動memcached的時候加入slab_automove選項,並將其引數數設定為1。比如命令$memcached -o slab_reassign,slab_automove=1就開啟了自動檢測功能。當然也是可以在啟動memcached後通過客戶端命令啟動automove功能,使用命令slabsautomove <0|1>。其中0表示關閉automove,1表示開啟automove。客戶端的這個命令只是簡單地設定settings.slab_automove的值,不做其他任何工作。


automove執行緒:

item狀態記錄儀:

        由於rebalance執行緒啟動後就會由於等待條件變數而進入休眠狀態,等待別人給它記憶體頁重分配任務。所以我們先來看一下automove執行緒。

        automove執行緒要進行自動檢測,檢測就需要一些實時資料進行分析。然後得出結論:哪個slabclass_t需要更多的記憶體,哪個又不需要。automove執行緒通過全域性變數itemstats收集item的各種資料。下面看一下itemstats變數以及它的型別定義。

//items.c檔案
typedef struct {
    uint64_t evicted;//因為LRU踢了多少個item
    //即使一個item的exptime設定為0,也是會被踢的
    uint64_t evicted_nonzero;//被踢的item中,超時時間(exptime)不為0的item數

	//最後一次踢item時,被踢的item已經過期多久了
	//itemstats[id].evicted_time = current_time - search->time;
    rel_time_t evicted_time;

	
    uint64_t reclaimed;//在申請item時,發現過期並回收的item數量
    uint64_t outofmemory;//為item申請記憶體,失敗的次數
    uint64_t tailrepairs;//需要修復的item數量(除非worker執行緒有問題否則一般為0)
	
	//直到被超時刪除時都還沒被訪問過的item數量
    uint64_t expired_unfetched;
	//直到被LRU踢出時都還沒有被訪問過的item數量
    uint64_t evicted_unfetched;
	
    uint64_t crawler_reclaimed;//被LRU爬蟲發現的過期item數量

	//申請item而搜尋LRU佇列時,被其他worker執行緒引用的item數量
    uint64_t lrutail_reflocked;
} itemstats_t;

#define POWER_LARGEST  200
#define LARGEST_ID POWER_LARGEST
static itemstats_t itemstats[LARGEST_ID];

        注意上面程式碼是在items.c檔案的,並且全域性變數itemstats是static型別。itemstats變數是一個數組,它是和slabclass陣列一一對應的。itemstats陣列的元素負責收集slabclass陣列中對應元素的資訊。itemstats_t結構體雖然提供了很多成員,可以收集很多資訊,但automove執行緒只用到第一個成員evicted。automove執行緒需要知道每一個尺寸的item的被踢情況,然後判斷哪一類item資源緊缺,哪一類item資源又過剩。

        itemstats廣泛分佈在items.c檔案的多個函式中(主要是為了能收集各種資料),所以這裡就不給出itemstats的具體收集實現了。當然由於evicted是重要的而且只在一個函數出現,就貼出evicted的收集程式碼吧。

item *do_item_alloc(char *key, const size_t nkey, const int flags,
                    const rel_time_t exptime, const int nbytes,
                    const uint32_t cur_hv) {
    item *it = NULL;

    int tries = 5;
    item *search;
    item *next_it;
    rel_time_t oldest_live = settings.oldest_live;

    search = tails[id];
    for (; tries > 0 && search != NULL; tries--, search=next_it) {
        /* we might relink search mid-loop, so search->prev isn't reliable */
        next_it = search->prev;

		...
		
        if ((search->exptime != 0 && search->exptime < current_time)
            || (search->time <= oldest_live && oldest_live <= current_time)) {
			...	
        } else if ((it = slabs_alloc(ntotal, id)) == NULL) {//申請記憶體失敗
			//此刻,過期失效的item沒有找到,申請記憶體又失敗了。看來只能使用
			//LRU淘汰一個item(即使這個item並沒有過期失效)
			
            if (settings.evict_to_free == 0) {//設定了不進行LRU淘汰item
            	//此時只能向客戶端回覆錯誤了
                itemstats[id].outofmemory++;
            } else {
                itemstats[id].evicted++;//增加被踢的item數
                itemstats[id].evicted_time = current_time - search->time;
				//即使一個item的exptime成員設定為永不超時(0),還是會被踢的
				if (search->exptime != 0)
                    itemstats[id].evicted_nonzero++;
                if ((search->it_flags & ITEM_FETCHED) == 0) {
                    itemstats[id].evicted_unfetched++;
                }
                it = search;

                //一旦發現有item被踢,那麼就啟動記憶體頁重分配操作
                //這個太頻繁了,不推薦				
                if (settings.slab_automove == 2)
                    slabs_reassign(-1, id);
            }
        }

        break;
    }

	...
    return it;
}

        從上面的程式碼可以看到,如果某個item因為LRU被踢了,那麼就會被記錄起來。在最後還可以看到如果settings.slab_automove 等於2,那麼一旦有item被踢了就呼叫slabs_reassign函式。slabs_reassign函式就是記憶體頁重分配處理函式。明顯一有item被踢就重分配太頻繁了,所以這是不推薦的。

確定貧窮和富有item:

        現在回過來看一下automove執行緒的執行緒函式slab_maintenance_thread。

static void *slab_maintenance_thread(void *arg) {
    int src, dest;

    while (do_run_slab_thread) {
        if (settings.slab_automove == 1) {//啟動了automove功能
            if (slab_automove_decision(&src, &dest) == 1) {
                /* Blind to the return codes. It will retry on its own */
                slabs_reassign(src, dest);
            }
            sleep(1);
        } else {//等待使用者啟動automove
            /* Don't wake as often if we're not enabled.
             * This is lazier than setting up a condition right now. */
            sleep(5);
        }
    }
    return NULL;
}

        可以看到如果settings.slab_automove就呼叫slab_automove_decision判斷是否應該進行記憶體頁重分配。返回1就說明需要重分配記憶體頁,此時呼叫slabs_reassign進行處理。現在來看一下automove執行緒是怎麼判斷要不要進行記憶體頁重分配的。

//items.c檔案
void item_stats_evictions(uint64_t *evicted) {
    int i;
    mutex_lock(&cache_lock);
    for (i = 0; i < LARGEST_ID; i++) {
        evicted[i] = itemstats[i].evicted;
    }
    mutex_unlock(&cache_lock);
}


//slabs.c檔案
//本函式選出最佳被踢選手,和最佳不被踢選手。返回1表示成功選手兩位選手
//返回0表示沒有選出。要同時選出兩個選手才返回1。並用src引數記錄最佳不
//不踢選手的id,dst記錄最佳被踢選手的id
static int slab_automove_decision(int *src, int *dst) {
    static uint64_t evicted_old[POWER_LARGEST];
    static unsigned int slab_zeroes[POWER_LARGEST];
    static unsigned int slab_winner = 0;
    static unsigned int slab_wins   = 0;
    uint64_t evicted_new[POWER_LARGEST];
    uint64_t evicted_diff = 0;
    uint64_t evicted_max  = 0;
    unsigned int highest_slab = 0;
    unsigned int total_pages[POWER_LARGEST];
    int i;
    int source = 0;
    int dest = 0;
    static rel_time_t next_run;

    /* Run less frequently than the slabmove tester. */
	//本函式的呼叫不能過於頻繁,至少10秒呼叫一次
    if (current_time >= next_run) {
        next_run = current_time + 10;
    } else {
        return 0;
    }

	//獲取每一個slabclass的被踢item數
    item_stats_evictions(evicted_new);
    pthread_mutex_lock(&cache_lock);
    for (i = POWER_SMALLEST; i < power_largest; i++) {
        total_pages[i] = slabclass[i].slabs;
    }
    pthread_mutex_unlock(&cache_lock);

	//本函式會頻繁被呼叫,所以有次數可說。
	
    /* Find a candidate source; something with zero evicts 3+ times */
	//evicted_old記錄上一個時刻每一個slabclass的被踢item數
	//evicted_new則記錄了現在每一個slabclass的被踢item數
	//evicted_diff則能表現某一個LRU佇列被踢的頻繁程度
    for (i = POWER_SMALLEST; i < power_largest; i++) {
        evicted_diff = evicted_new[i] - evicted_old[i];
        if (evicted_diff == 0 && total_pages[i] > 2) {
			//evicted_diff等於0說明這個slabclass沒有item被踢,而且
			//它又佔有至少兩個slab。			
            slab_zeroes[i]++;//增加計數
            //這個slabclass已經歷經三次都沒有被踢記錄,說明空間多得很
            //就選你了,最佳不被踢選手
            if (source == 0 && slab_zeroes[i] >= 3)
                source = i;
        } else {
            slab_zeroes[i] = 0;//計數清零
            if (evicted_diff > evicted_max) {
                evicted_max = evicted_diff;
                highest_slab = i;
            }
        }
        evicted_old[i] = evicted_new[i];
    }

    /* Pick a valid destination */
	//選出一個slabclass,這個slabclass要連續3次都是被踢最多item的那個slabclass
    if (slab_winner != 0 && slab_winner == highest_slab) {
        slab_wins++;
        if (slab_wins >= 3)//這個slabclass已經連續三次成為最佳被踢選手了
            dest = slab_winner;
    } else {
        slab_wins = 1;//計數清零(當然這裡是1)
        slab_winner = highest_slab;//本次的最佳被踢選手
    }

    if (source && dest) {
        *src = source;
        *dst = dest;
        return 1;
    }
    return 0;
}

        從上面的程式碼也可以看到,其實判斷的方法也比較簡單。從slabclass陣列中選出兩個選手:一個是連續三次沒有被踢item了,另外一個則是連續三次都成為最佳被踢手。如果找到了滿足條件的兩個選手,那麼返回1。此時automove執行緒就會呼叫slabs_reassign函式。

下達 rebalance任務:

        在貼出slabs_reassign函式前,回想一下slabs reassign命令。前面講的都是自動檢測要不要進行記憶體頁重分配,都快要忘了還有一個手動要求記憶體頁重分配的命令。如果客戶端使用了slabs reassign命令,那麼worker執行緒在接收到這個命令後,就會呼叫slabs_reassign函式,函式引數是slabs reassign命令的引數。現在自動檢測和手動設定大一統了。

enum reassign_result_type {
    REASSIGN_OK=0, REASSIGN_RUNNING, REASSIGN_BADCLASS, REASSIGN_NOSPARE,
    REASSIGN_SRC_DST_SAME
};


enum reassign_result_type slabs_reassign(int src, int dst) {
    enum reassign_result_type ret;
    if (pthread_mutex_trylock(&slabs_rebalance_lock) != 0) {
        return REASSIGN_RUNNING;
    }
    ret = do_slabs_reassign(src, dst);
    pthread_mutex_unlock(&slabs_rebalance_lock);
    return ret;
}


static enum reassign_result_type do_slabs_reassign(int src, int dst) {
    if (slab_rebalance_signal != 0)
        return REASSIGN_RUNNING;

    if (src == dst)//不能相同
        return REASSIGN_SRC_DST_SAME;

    /* Special indicator to choose ourselves. */
    if (src == -1) {//客戶端命令要求隨機選出一個源slab class
		//選出一個頁數大於1的slab class,並且該slab class不能是dst
		//指定的那個。如果不存在這樣的slab class,那麼返回-1
        src = slabs_reassign_pick_any(dst);
        /* TODO: If we end up back at -1, return a new error type */
    }

    if (src < POWER_SMALLEST || src > power_largest ||
        dst < POWER_SMALLEST || dst > power_largest)
        return REASSIGN_BADCLASS;

	//源slab class沒有或者只有一個記憶體頁,那麼就不能分給別的slab class
    if (slabclass[src].slabs < 2)
        return REASSIGN_NOSPARE;

	//全域性變數slab_rebal
    slab_rebal.s_clsid = src;//儲存源slab class
    slab_rebal.d_clsid = dst;//儲存目標slab class

    slab_rebalance_signal = 1;
	//喚醒slab_rebalance_thread函式的執行緒.
	//在slabs_reassign函式中已經鎖上了slabs_rebalance_lock
    pthread_cond_signal(&slab_rebalance_cond);

    return REASSIGN_OK;
}


//選出一個記憶體頁數大於1的slab class,並且該slab class不能是dst
//指定的那個。如果不存在這樣的slab class,那麼返回-1
static int slabs_reassign_pick_any(int dst) {
    static int cur = POWER_SMALLEST - 1;
    int tries = power_largest - POWER_SMALLEST + 1;
    for (; tries > 0; tries--) {
        cur++;
        if (cur > power_largest)
            cur = POWER_SMALLEST;
        if (cur == dst)
            continue;
        if (slabclass[cur].slabs > 1) {
            return cur;
        }
    }
    return -1;
}

        do_slabs_reassign會把源slab class 和目標slab class儲存在全域性變數slab_rebal,並且在最後會呼叫pthread_cond_signal喚醒rebalance執行緒。


rebalance執行緒:

        現在automove執行緒已經退出歷史舞臺了,rebalance執行緒也從沉睡中甦醒過來並登上舞臺。現在來看一下rebalance執行緒的執行緒函式slab_rebalance_thread。注意:在一開始slab_rebalance_signal是等於0的,當需要進行記憶體頁重分配就會把slab_rebalance_signal變數賦值為1。

static void *slab_rebalance_thread(void *arg) {
    int was_busy = 0;
    /* So we first pass into cond_wait with the mutex held */
    mutex_lock(&slabs_rebalance_lock);

    while (do_run_slab_rebalance_thread) {
        if (slab_rebalance_signal == 1) {
			//標誌要移動的記憶體頁的資訊,並將slab_rebalance_signal賦值為2
			//slab_rebal.done賦值為0,表示沒有完成
            if (slab_rebalance_start() < 0) {//失敗
                /* Handle errors with more specifity as required. */
                slab_rebalance_signal = 0;
            }

            was_busy = 0;
        } else if (slab_rebalance_signal && slab_rebal.slab_start != NULL) {
            was_busy = slab_rebalance_move();//進行記憶體頁遷移操作
        }

        if (slab_rebal.done) {//完成記憶體頁重分配操作
            slab_rebalance_finish();
        } else if (was_busy) {//有worker執行緒在使用記憶體頁上的item
            /* Stuck waiting for some items to unlock, so slow down a bit
             * to give them a chance to free up */
            usleep(50);//休眠一會兒,等待worker執行緒放棄使用item,然後再次嘗試
        }

        if (slab_rebalance_signal == 0) {//一開始就在這裡休眠
            /* always hold this lock while we're running */
            pthread_cond_wait(&slab_rebalance_cond, &slabs_rebalance_lock);
        }
    }
    return NULL;
}


鎖定記憶體頁:

        函式slab_rebalance_start對要源slab class進行一些標註,當worker執行緒要訪問源slab class的時候意識到正在記憶體頁重分配。

//memcached.h檔案
struct slab_rebalance {
	//記錄要移動的頁的資訊。slab_start指向頁的開始位置。slab_end指向頁
	//的結束位置。slab_pos則記錄當前處理的位置(item)
    void *slab_start;
    void *slab_end;
    void *slab_pos;
    int s_clsid; //源slab class的下標索引
    int d_clsid; //目標slab class的下標索引
    int busy_items; //是否worker執行緒在引用某個item
    uint8_t done;//是否完成了記憶體頁移動
};
//memcached.c檔案
struct slab_rebalance slab_rebal;

//slabs.c檔案
static int slab_rebalance_start(void) {
    slabclass_t *s_cls;
    int no_go = 0;

    pthread_mutex_lock(&cache_lock);
    pthread_mutex_lock(&slabs_lock);

    if (slab_rebal.s_clsid < POWER_SMALLEST ||
        slab_rebal.s_clsid > power_largest  ||
        slab_rebal.d_clsid < POWER_SMALLEST ||
        slab_rebal.d_clsid > power_largest  ||
        slab_rebal.s_clsid == slab_rebal.d_clsid)//非法下標索引
        no_go = -2;

    s_cls = &slabclass[slab_rebal.s_clsid];

	//為這個目標slab class增加一個頁表項都失敗,那麼就
	//根本無法為之增加一個頁了
    if (!grow_slab_list(slab_rebal.d_clsid)) {
        no_go = -1;
    }

    if (s_cls->slabs < 2)//目標slab class頁數太少了,無法分一個頁給別人
        no_go = -3;

    if (no_go != 0) {
        pthread_mutex_unlock(&slabs_lock);
        pthread_mutex_unlock(&cache_lock);
        return no_go; /* Should use a wrapper function... */
    }

	//標誌將源slab class的第幾個記憶體頁分給目標slab class
	//這裡是預設是將第一個記憶體頁分給目標slab class
    s_cls->killing = 1;

	//記錄要移動的頁的資訊。slab_start指向頁的開始位置。slab_end指向頁
	//的結束位置。slab_pos則記錄當前處理的位置(item)
    slab_rebal.slab_start = s_cls->slab_list[s_cls->killing - 1];
    slab_rebal.slab_end   = (char *)slab_rebal.slab_start +
        (s_cls->size * s_cls->perslab);
    slab_rebal.slab_pos   = slab_rebal.slab_start;
    slab_rebal.done       = 0;

    /* Also tells do_item_get to search for items in this slab */
    slab_rebalance_signal = 2;//要rebalance執行緒接下來進行記憶體頁移動
  

    pthread_mutex_unlock(&slabs_lock);
    pthread_mutex_unlock(&cache_lock);

    return 0;
}

        slab_rebalance_start會將一個slab class的一個記憶體頁標註為要移動的,此時就不能讓worker執行緒訪問這個記憶體頁的item了。現在看一下假如worker執行緒剛好要訪問這個記憶體頁的一個item時會發生什麼。

item *do_item_get(const char *key, const size_t nkey, const uint32_t hv) {
    item *it = assoc_find(key, nkey, hv);//assoc_find函式內部沒有加鎖
    
    if (it != NULL) {//找到了,此時item的引用計數至少為1
        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)) {
			//這個item剛好在要移動的記憶體頁裡面。此時不能返回這個item
			//worker執行緒要負責把這個item從雜湊表和LRU佇列中刪除這個item,避免
			//後面有其他worker執行緒又訪問這個不能使用的item
			do_item_unlink_nolock(it, hv);
            do_item_remove(it);
            it = NULL;
        }
    }

 	...
    return it;
}

移動(歸還)item:

        現在回過頭繼續看rebalance執行緒。前面說到已經標註了源slab class的一個記憶體頁。標註完rebalance執行緒就會呼叫slab_rebalance_move函式完成真正的記憶體頁遷移操作。源slab class上的記憶體頁是有item的,那麼在遷移的時候怎麼處理這些item呢?memcached的處理方式是很粗暴的:直接刪除。如果這個item還有worker執行緒在使用,rebalance執行緒就等你一下。如果這個item沒有worker執行緒在引用,那麼即使這個item沒有過期失效也將直接刪除。

        因為一個記憶體頁可能會有很多個item,所以memcached也採用分期處理的方法,每次只處理少量的item(預設為一個)。所以呢,slab_rebalance_move函式會在slab_rebalance_thread執行緒函式中多次呼叫,直到處理了所有的item。

/* refcount == 0 is safe since nobody can incr while cache_lock is held.
 * refcount != 0 is impossible since flags/etc can be modified in other
 * threads. instead, note we found a busy one and bail. logic in do_item_get
 * will prevent busy items from continuing to be busy
 */
static int slab_rebalance_move(void) {
    slabclass_t *s_cls;
    int x;
    int was_busy = 0;
    int refcount = 0;
    enum move_status status = MOVE_PASS;

    pthread_mutex_lock(&cache_lock);
    pthread_mutex_lock(&slabs_lock);

    s_cls = &slabclass[slab_rebal.s_clsid];

	//會在start_slab_maintenance_thread函式中讀取環境變數設定slab_bulk_check
	//預設值為1.同樣這裡也是採用分期處理的方案處理一個頁上的多個item
    for (x = 0; x < slab_bulk_check; x++) {
        item *it = slab_rebal.slab_pos;
        status = MOVE_PASS;
        if (it->slabs_clsid != 255) {
            void *hold_lock = NULL;
            uint32_t hv = hash(ITEM_key(it), it->nkey);
            if ((hold_lock = item_trylock(hv)) == NULL) {
                status = MOVE_LOCKED;
            } else {
                refcount = refcount_incr(&it->refcount);
                if (refcount == 1) { /* item is unlinked, unused */
					//如果it_flags&ITEM_SLABBED為真,那麼就說明這個item
					//根本就沒有分配出去。如果為假,那麼說明這個item被分配
					//出去了,但處於歸還途中。參考do_item_get函式裡面的
					//判斷語句,有slab_rebalance_signal作為判斷條件的那個。
                    if (it->it_flags & ITEM_SLABBED) {//沒有分配出去
                        /* remove from slab freelist */
                        if (s_cls->slots == it) {
                            s_cls->slots = it->next;
                        }
                        if (it->next) it->next->prev = it->prev;
                        if (it->prev) it->prev->next = it->next;
                        s_cls->sl_curr--;
                        status = MOVE_DONE;//這個item處理成功
                    } else {//此時還有另外一個worker執行緒在歸還這個item
                        status = MOVE_BUSY;
                    }
                } else if (refcount == 2) { /* item is linked but not busy */
                	//沒有worker執行緒引用這個item
                    if ((it->it_flags & ITEM_LINKED) != 0) {
						//直接把這個item從雜湊表和LRU佇列中刪除
                        do_item_unlink_nolock(it, hv);
                        status = MOVE_DONE;
                    } else {
                        /* refcount == 1 + !ITEM_LINKED means the item is being
                         * uploaded to, or was just unlinked but hasn't been freed
                         * yet. Let it bleed off on its own and try again later */
                        status = MOVE_BUSY;
                    }
                } else {//現在有worker執行緒正在引用這個item
                    status = MOVE_BUSY;
                }
                item_trylock_unlock(hold_lock);
            }
        }

        switch (status) {
            case MOVE_DONE:
                it->refcount = 0;//引用計數清零
                it->it_flags = 0;//清零所有屬性
                it->slabs_clsid = 255;
                break;
            case MOVE_BUSY:
                refcount_decr(&it->refcount); //注意這裡沒有break
            case MOVE_LOCKED:
                slab_rebal.busy_items++;
                was_busy++;//記錄是否有不能馬上處理的item
                break;
            case MOVE_PASS:
                break;
        }

		//處理這個頁的下一個item
        slab_rebal.slab_pos = (char *)slab_rebal.slab_pos + s_cls->size;
        if (slab_rebal.slab_pos >= slab_rebal.slab_end)//遍歷完了這個頁
            break;
    }

	//遍歷完了這個頁的所有item
    if (slab_rebal.slab_pos >= slab_rebal.slab_end) {
        /* Some items were busy, start again from the top */
		//在處理的時候,跳過了一些item(因為有worker執行緒在引用)
        if (slab_rebal.busy_items) {//此時需要從頭再掃描一次這個頁
            slab_rebal.slab_pos = slab_rebal.slab_start;
            slab_rebal.busy_items = 0;
        } else {
            slab_rebal.done++;//標誌已經處理完這個頁的所有item
        }
    }

    pthread_mutex_unlock(&slabs_lock);
    pthread_mutex_unlock(&cache_lock);

    return was_busy;//返回記錄
}

劫富濟貧:

        上面程式碼中的was_busy就標誌了是否有worker執行緒在引用記憶體頁中的一個item。其實slab_rebalance_move函式的名字取得不好,因為實現的不是移動(遷移),而是把記憶體頁中的item刪除從雜湊表和LRU佇列中刪除。如果處理完記憶體頁的所有item,那麼就會slab_rebal.done++,標誌處理完成。線上程函式slab_rebalance_thread中,如果slab_rebal.done為真就會呼叫slab_rebalance_finish函式完成真正的記憶體頁遷移操作,把一個記憶體頁從一個slab class 轉移到另外一個slab class中。

static void slab_rebalance_finish(void) {
    slabclass_t *s_cls;
    slabclass_t *d_cls;

    pthread_mutex_lock(&cache_lock);
    pthread_mutex_lock(&slabs_lock);

    s_cls = &slabclass[slab_rebal.s_clsid];
    d_cls   = &slabclass[slab_rebal.d_clsid];

    /* At this point the stolen slab is completely clear */
	//相當於把指標賦NULL值
    s_cls->slab_list[s_cls->killing - 1] =
        s_cls->slab_list[s_cls->slabs - 1];
    s_cls->slabs--;//源slab class的記憶體頁數減一
    s_cls->killing = 0;

	//記憶體頁所有位元組清零,這個也很重要的
    memset(slab_rebal.slab_start, 0, (size_t)settings.item_size_max);

	//將slab_rebal.slab_start指向的一個頁記憶體饋贈給目標slab class
	//slab_rebal.slab_start指向的頁是從源slab class中得到的。
    d_cls->slab_list[d_cls->slabs++] = slab_rebal.slab_start;
	//按照目標slab class的item尺寸進行劃分這個頁,並且將這個頁的
	//記憶體併入到目標slab class的空閒item佇列中
    split_slab_page_into_freelist(slab_rebal.slab_start,
        slab_rebal.d_clsid);

	//清零
    slab_rebal.done       = 0;
    slab_rebal.s_clsid    = 0;
    slab_rebal.d_clsid    = 0;
    slab_rebal.slab_start = NULL;
    slab_rebal.slab_end   = NULL;
    slab_rebal.slab_pos   = NULL;

    slab_rebalance_signal = 0;//rebalance執行緒完成工作後,再次進入休眠狀態

    pthread_mutex_unlock(&slabs_lock);
    pthread_mutex_unlock(&cache_lock);

}