HAProxy介绍
HAProxy是一款提供高可用性、负载均衡以及基于TCP(第四层)和HTTP(第七层)应用的代理软件,HAProxy是完全免费的、借助HAProxy可以快速并且可靠的提供基于TCP和HTTP应用的代理解决方案。
HAProxy内存池概述
HAProxy的内存池按照类型分类,每种类型的内存池都有一个名字(name),用链表记录空闲链表,每种类型的内存池中的元素大小都是相等的,并且按照16字节对齐。
HAProxy用struct pool_head记录每种类型的内存池:
struct pool_head {
void **free_list; // 空闲链表
struct list list; // 链接每种类型的内存池的链表元素
unsigned int used; // 当前用了多少节点
unsigned int allocated; // 这种类型的内存池一共分配了多少个
unsigned int limit; // 这种类型的内存池最多分配多少个
unsigned int minavail; // 最少保留多少个可用节点,在内存回收时,最少保留这么多而不会全部回收
unsigned int size; // 一个节点有多大,16字节对齐
unsigned int flags; // 标志位,目前只有是否可共享,就是不同类型但是大小相同的,用同一个pool_head
unsigned int users; // 有多少个使用者,因为一个内存池是可以共享的,所以可能有多个使用者
char name[12]; // 内存池的名称
};
在程序执行过程中,内存池可能会像以下这个图形:
内存池的创建
在创建一个内存池时,HAProxy会首先检测当前是否已经包含了与需要大小(是指一个节点,比如一个object)相等的内存池,如果这个内存池支持共享,就将引用计数,即pool_header.users加1。否则创建一个新的内存池。另外,内存池列表是按照它们元素对象大小从小到大排序的。
struct pool_head *create_pool(char *name, unsigned int size, unsigned int flags)
{
struct pool_head *pool;
struct pool_head *entry;
struct list *start;
unsigned int align;
align = 16; // 申请的内存按照16字节对齐
size = (size + align - 1) & -align;
// pools是一个全局变量,是pool_header链表的一个节点,作为内存池的头结点
start = &pools;
pool = NULL;
list_for_each_entry(entry, &pools, list) {
if (entry->size == size) {
// 首先找到大小相同的并且允许共享的内存池,直接使用;
// 或者,找到一个合适的插入的位置
if (flags & entry->flags & MEM_F_SHARED) {
pool = entry;
DPRINTF(stderr, "Sharing %s with %s\n", name, pool->name);
break;
}
}
else if (entry->size > size) {
// 内存池链表是按照大小从小到大排列的
start = &entry->list;
break;
}
}
// 没有找到可以共享的内存池,创建一个新的
if (!pool) {
pool = CALLOC(1, sizeof(*pool));
if (!pool)
return NULL;
if (name)
strlcpy2(pool->name, name, sizeof(pool->name));
pool->size = size;
pool->flags = flags;
LIST_ADDQ(start, &pool->list);
}
pool->users++;
return pool;
}
内存申请
内存的申请是比较简单的,先尝试从空闲列表中获取,如果没有,向系统申请新的内存。如果再失败,主动调用垃圾回收接口,再次尝试申请内存。但只会尝试一次,如果失败,会直接退出,这样不会浪费太多的时间。
内存申请的核心函数是pool_refill_alloc:
这个函数请求申请avail+1个节点,放到空闲列表中,如果申请内存失败,会调用垃圾回收接口,但最多调用一次。
void *pool_refill_alloc(struct pool_head *pool, unsigned int avail)
{
void *ptr = NULL;
int failed = 0;
// avail:要申请的个数
// 加上pool->used是为了方便计算当前申请多少个结束
avail += pool->used;
while (1) {
// 如果内存池限制了总数,并且当前数目已经超过了限制,就不再申请,并且返回失败
if (pool->limit && pool->allocated >= pool->limit)
return NULL;
ptr = MALLOC(pool->size);
if (!ptr) {
if (failed) // failed表示曾经申请内存失败,又调用了垃圾回收接口,这次又失败了…
return NULL;
failed++;
pool_gc2();
continue;
}
if (++pool->allocated > avail)
break;
*(void **)ptr = (void *)pool->free_list; // 下一个节点的地址直接存放在当前节点内存中前sizeof(void *)中
pool->free_list = ptr;
}
pool->used++;
return ptr;
}
当然,使用者申请内存不会直接调用这个接口,一般调用pool_alloc2,这个函数再调用pool_alloc_dirty。其实逻辑还是比较简单的,先从空闲列表中申请内存,如果已经用光,最终还是调用pool_refill_alloc获取更多内存。另外,如果设置mem_poison_byte为非0,pool_alloc2还会对申请到的新内存做一个填充,但是mem_poison_byte默认就是0。
内存的回收
HAProxy使用内存池,但不会让内存一直生长,有一个垃圾回收机制,特别是当内存申请失败时,会主动调用垃圾回收接口。HAProxy的垃圾回收非常简单,它会遍历所有内存池,尽量释放所有可以释放的内存。
void pool_gc2()
{
static int recurse;
struct pool_head *entry;
if (recurse++) // 防止递归调用,变成了死循环
goto out;
list_for_each_entry(entry, &pools, list) {
void *temp, *next;
//qfprintf(stderr, "Flushing pool %s\n", entry->name);
next = entry->free_list;
while (next &&
(int)(entry->allocated - entry->used) > (int)entry->minavail) { // 并不是直接全部释放,还是会保留最少可用内存数的
temp = next;
next = *(void **)temp;
entry->allocated--;
FREE(temp);
}
entry->free_list = next;
}
out:
recurse--;
}
使用者可以也可以主动调用垃圾回收,但是只会回收自己创建的内存池,HAProxy提供的接口是pool_flush2,这个直接将所有空闲内存全部释放掉了(自己创建的内存池,不会释放其它创建者的内存池)。
void pool_flush2(struct pool_head *pool)
{
void *temp, *next;
if (!pool)
return;
// 这里只是释放所有空闲内存,并不考虑最少可用内存
next = pool->free_list;
while (next) {
temp = next;
next = *(void **)temp;
pool->allocated--;
FREE(temp);
}
pool->free_list = next;
/* here, we should have pool->allocate == pool->used */
}
内存释放
从内存池的构造来看,就知道HAProxy的内存池有这么一个问题:如果使用者不主动释放申请的内存,那释放内存池后,这块内存就无法被内存池回收了。不过这不是问题,只需要稍微注意一下就好了。内存释放时,只是将指针放到对应内存池的空闲链表的第一个节点。放到第一个节点有一个好处,下次申请时,也是优先获取第一个,就是使用最近使用的内存。
static inline void pool_free2(struct pool_head *pool, void *ptr)
{
if (likely(ptr != NULL)) {
*(void **)ptr= (void *)pool->free_list;
pool->free_list = (void *)ptr;
pool->used--;
}
}
内存池的销毁
内存池的销毁是很简单的,释放所有内存池的空闲内存,然后释放所有内存池。但是如果还有正在使用的内存,pool->used != 0,那就不能释放内存,要不然会造成内存泄露。
void *pool_destroy2(struct pool_head *pool)
{
if (pool) {
pool_flush2(pool);
if (pool->used)
return pool;
pool->users--;
if (!pool->users) {
LIST_DEL(&pool->list);
FREE(pool);
}
}
return NULL;
}
监测工具
HAProxy还提供了一个工具,可以打印出当前内存池的状况,比如使用了多少,有多少空闲等。
void dump_pools_to_trash()
{
struct pool_head *entry;
unsigned long allocated, used;
int nbpools;
allocated = used = nbpools = 0;
chunk_printf(&trash, "Dumping pools usage. Use SIGQUIT to flush them.\n");
list_for_each_entry(entry, &pools, list) {
chunk_appendf(&trash, " - Pool %s (%d bytes) : %d allocated (%u bytes), %d used, %d users%s\n",
entry->name, entry->size, entry->allocated,
entry->size * entry->allocated, entry->used,
entry->users, (entry->flags & MEM_F_SHARED) ? " [SHARED]" : "");
allocated += entry->allocated * entry->size;
used += entry->used * entry->size;
nbpools++;
}
chunk_appendf(&trash, "Total: %d pools, %lu bytes allocated, %lu used.\n",
nbpools, allocated, used);
}
PS: 这些代码在include/common/memory.h和src/memory.c中。