高併發服務器的設計--內存池的設計

不同的業務,設計也不盡相同,但至少都一些共同的追求,比如性能。
做服務器開發很多年了,有時候被人問到,服務器性能是什麼呢?各種服務器間拼得是什麼呢?
簡單的回答就是QPS,併發數,但有時候想想也許也不對。
QPS與併發數是針對同樣的業務而言的,業務不同,相同的服務器能承受的壓力也會不同。
性能,也許可以打個俗點的比方:
服務器就是一艘船,性能就是船的容量,開的速度,行得是否穩當。
該用的用,該省的省。能用內存就別用IO,CPU則能少用就少用,相同的QPS,CPU和內存用的少點的性能就要比用的多點好,同樣,QPS跑得多點的就比
跑得小點的性能要好,哪怕多用了點CPU和內存。
什麼是性能的保障呢?
高效的事件模型,簡單明瞭的業務架構,統一穩定的資源管理,外加純熟的人員。
咱就從資源說起吧。
資源多半與IO有關,如果你看過我前面的文章,一定不會對連接池陌生,沒錯,連接是系統的一種IO資源,下面看看另一種IO資源:內存。

如果你看過apache, nginx之類服務器的代碼,或者想入手,那麼多半應該從內存管理開始。
與服務器性能息息相關,內存池的設計也追求快速與穩定,生命週期一般有下面三種:
global: 全局的內存,存放整個進程的全局信息。
conn: 每個連接的信息,從連接產生到關閉。
busi:業務相關的信息,伴隨每個業務的產生到結束

下面定義一個簡單的內存池:

typedef struct yumei_mem_buf_s yumei_mem_buf_t;
typedef struct yumei_mem_pool_s yumei_mem_pool_t;

struct yumei_mem_buf_s
{
	int                          size;
	char                        *pos;
	char                        *data;
	yumei_mem_pool_t            *pool;
};

struct yumei_mem_pool_s
{
	int                          size;
	char                        *data;
	char                        *last;
	yumei_mem_pool_t            *next;
	yumei_mem_pool_t            *current;
};

yumei_mem_pool_t* yumei_mem_pool_create( int block_size, int block_num );
int yumei_mem_pool_free( yumei_mem_pool_t  *pool );
yumei_mem_buf_t* yumei_mem_malloc( yumei_mem_pool_t   *pool, int size );
int yumei_mem_buf_free( yumei_mem_buf_t *buf );

在每個連接開始的時候,創建連接唯一的內存池,存放IO數據,當要創建新業務時,創建業務內存池,業務處理完畢時釋放內存池:

typedef struct yumei_busi_s yumei_busi_t;

struct yumei_busi_s
{
	yumei_mem_pool_t      *pool;
	...
	...

}

#define yumei_BUSI_MEM_BLOCL_SIZE 512
#define yumei_BUSI_MEM_BLOCK_NUM  32

yumei_busi_t* yumei_busi_create()
{
	yumei_busi_t* busi;
	yumei_pool_t* pool;
	yumei_mem_buf_t* buf;
	int size;

	pool = yumei_mem_pool_create( yumei_BUSI_MEM_BLOCL_SIZE, yumei_BUSI_MEM_BLOCK_NUM );
	if( !pool ){
		return 0;
	}

	size = sizeof( yumei_busi_t );
	buf = yumei_mem_buf_malloc( pool, size );

	if( !buf ){
		yumei_mem_pool_free( pool );
		return 0;
	}

	busi = buf->data;

	return busi;

}

#define YUMEI_BUSI_ERROR -1
#define YUMEI_BUSI_OK     0

int yumei_busi_free( yumei_busi_t* busi )
{
	if( !busi ){
		return YUMEI_BUSI_ERROR;
	}

	yumei_mem_pool_free( busi->pool );

	return YUMEI_BUSI_OK;
}

有些時候業務比較簡單,一個連接僅對應一個業務或多個業務不是並行執行,這樣的情況下,就不再需要業務內存池了,可以直接用連接內存池:

yumei_busi_t* yumei_busi_create( yumei_conn_t* conn )
{
	yumei_busi_t* busi;
	yumei_pool_t* pool;
	yumei_mem_buf_t* buf;
	int size;

	pool = conn->pool;
	if( !pool ){
		retur 0;
	}

	size = sizeof( yumei_busi_t );
	buf = yumei_mem_buf_malloc( pool, size );

	if( !buf ){
		yumei_mem_pool_free( pool );
		return 0;
	}

	busi = buf->data;

	return busi;

}

#define YUMEI_CONN_ERROR -1
#define YUMEI_CONN_OK     0

int yumei_conn_close( yumei_conn_t* conn )
{
	if( !conn ){
		return YUMEI_CONN_ERROR;
	}

	yumei_mem_pool_free( conn->pool );

	return YUMEI_CONN_OK;
}

知道內存池怎麼用了,再來看看內部設計吧,pool 的四個元素裏 size 對應 block_size, data和last 分別對應塊的起始地址和可分配地址,next和current分別對應下塊內存池和當前可用內存池。
在一些通用的服務器上還會看到另一個元素:large。 這個是爭對一些大內存的分配,當不清楚業務到底需要多大內存的時候,large往往是必須的,這樣內存池結構就變成這樣:

typedef struct yumei_mem_large_s yumei_mem_large_t;

struct yumei_mem_large_s
{
	char                      *data;
	int                        size;
	yumei_mem_large_t         *next;
}

struct yumei_mem_pool_s
{
	int                          size;
	char                        *data;
	char                        *last;
	yumei_mem_pool_t            *next;
	yumei_mem_pool_t            *current;
	yumei_mem_large_t           *large;
};

對於一些特殊的業務,比如業務使用的內存大小都固定,且相近的時候,內存池就縮化成了固定大小的內存管理,其實是很簡單了,這樣的內存池可以綁定在連接上,且用完不用釋放,留待下條連接複用,進一步節省開銷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章