HAProxy内存池简介

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内存池构造

内存池的创建

在创建一个内存池时,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中。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值