高并发下传统方式的弊端
void *malloc(size_t size);
在内存的动态存储区中分配一块长度为size
字节的连续区域返回该区域的首地址.
void *calloc(size_t nmemb, size_t size);
与malloc
相似,参数size
为申请地址的单位元素长度,nmemb
为元素个数,即在内存中申请nmemb*size
字节大小的连续地址空间,内存会初始化0
void *realloc(void *ptr, size_t size);
给一个已经分配了地址的指针重新分配空间,参数ptr
为原有的空间地址,newsize
是重新申请的地址长度。ptr
若为NULL
,它就等同于 malloc
。
void free(void *ptr);
释放指针所指的内存空间
在C++中会用到new
,delete
,new[]
和delete[]
。new
和 delete
分别是申请和释放单个对象内存空间, new[]
和delete[]
分别释放连续的内存空间。
问题1: 高并发时较小内存块使用导致系统调用频繁,降低了系统的执行效率。
问题2: 频繁使用时增加了系统内存的碎片,降低内存使用效率。
内部碎片:是指内存已经被分配出去(能明确指出属于哪个进程),却不能被利用;
产生根源:
1.内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址
2.MMU的分页机制的限制
问题3: 没有垃圾回收机制,容易造成内存泄漏,导致内存枯竭。
问题4: 内存分配与释放的逻辑在程序中相隔较远时,降低程序的稳定性。
弊端解决方法
高并发内存管理最佳实践
内存池 (M emory Pool) 是一种动态内存分配与管理技术。 通常情况下,程序员习惯直接使 用 new 、 delete 、 malloc 、 free 等 API 申请分配和释放内存,这样导致的后果是: 当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能 。
内存池则是在真正使用内存之前,先申请分配一大块内存 ( 内存池 ) 留作备用,当程序员申请内存时,从池中取出一块动态分配,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以 再取出来使用 ,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
- 内存池提前预先分配大块内存,统一释放,极大的减少了malloc 和 free 等函数的调用;
- 内存池每次请求分配大小适度的内存块,避免了碎片的产生;
- 在生命周期结束后统一释放内存,完全避免了内存泄露的产生;
- 在生命周期结束后统一释放内存,避免重复释放指针或释放空指针等情况。
高并发(High Concurrency) 是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发特点:
1.响应时间短;
2.吞吐量大;
3.每秒响应请求数;
4.QPS 并发用户数高;
在高并发系统设计时需要考虑:
1.设计逻辑应该尽量简单,避免不同请求之间互相影响,尽量降低不同模块之间的耦合内存池
2.生存时间应该尽可能短,与请求或者连接具有相同的周期,减少碎片堆积和内存泄漏
高效内存池设计与实现
实现思路
对于每个请求或者连接都会建立相应的内存池,建立好内存池之后,我们可以直接从内存池中申请所需要的内存,不用去管内存的释放,当内存池使用完成之后一次性销毁内存池。
区分大小内存块的申请和释放,大于池尺寸的定义为大内存块,使用单独的大内存块链表保存,即时分配和释放;小于等于池尺寸的定义为小内存块,直接从预先分配的内存块中提取,不够就扩充池中的内存,在生命周期内对小块内存不做释放,直到最后统一销毁。
Nginx源码分析
Nginx 内存池结构图
typedef struct {u_char *last; // 保存当前数据块中内存分配指针的当前位置u_char *end; // 保存内存块的结束位置ngx_pool_t *next; // 内存池由多块内存块组成,指向下一个数据块的位置ngx_uint_t failed; // 当前数据块内存不足引起分配失败的次数
} ngx_pool_data_t;struct ngx_pool_s {ngx_pool_data_t d; // 内存池当前的数据区指针的结构体size_t max; // 当前数据块最大可分配的内存大小(Bytes)ngx_pool_t *current; // 当前正在使用的数据块的指针ngx_pool_large_t *large; // pool 中指向大数据块的指针(大数据快是指 size > max 的数据块)
};
ngx_pool_t 结构示意图
Nginx 内存池基本操作
1.内存池创建、销毁和重置
2.内存池申请、释放和回收操作
具体代码实现
1.内存分配 ngx_alloc和ngx_calloc
void *
ngx_alloc(size_t size)
{void *p;//分配一块内存p = malloc(size);if (p == NULL) {fprintf(stderr,"malloc(%zu) failed", size);}if(debug) fprintf(stderr, "malloc: %p:%zu", p, size);return p;
}//调用ngx_alloc方法,如果分配成,则调用ngx_memzero方法,将内存块设置为0
// #define ngx_memzero(buf, n) (void) memset(buf, 0, n)
void *
ngx_calloc(size_t size)
{void *p;p = ngx_alloc(size);if (p) {ngx_memzero(p, size);}return p;
}
2.创建内存池ngx_create_pool
/*** 创建一个内存池*/
ngx_pool_t *
ngx_create_pool(size_t size)
{ngx_pool_t *p;/*** 相当于分配一块内存 ngx_alloc(size, log)*/p = ngx_memalign(NGX_POOL_ALIGNMENT, size);if (p == NULL) {return NULL;}/*** Nginx会分配一块大内存,其中内存头部存放ngx_pool_t本身内存池的数据结构* ngx_pool_data_t p->d 存放内存池的数据部分(适合小于p->max的内存块存储)* p->large 存放大内存块列表* p->cleanup 存放可以被回调函数清理的内存块(该内存块不一定会在内存池上面分配)*/p->d.last = (u_char *) p + sizeof(ngx_pool_t); //内存开始地址,指向ngx_pool_t结构体之后数据取起始位置p->d.end = (u_char *) p + size; //内存结束地址p->d.next = NULL; //下一个ngx_pool_t 内存池地址p->d.failed = 0; //失败次数size = size - sizeof(ngx_pool_t);p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;//有缓存池的父节点,才会用到下面的这些 ,子节点只挂载在p->d.next,并且只负责p->d的数据内容p->current = p;//p->chain = NULL;p->large = NULL;//p->cleanup = NULL;//p->log = log;return p;
}
#define NGX_ALIGNMENT sizeof(unsigned long) /* platform word */
#define ngx_memalign(alignment, size) ngx_alloc(size)ngx_memalign(size_t alignment, size_t size)
{void *p;p = memalign(alignment, size);if (p == NULL) {fprintf(stderr,"memalign(%zu, %zu) failed", alignment, size);}if(debug) fprintf(stderr,"memalign: %p:%zu @%zu", p, size, alignment);return p;
}/** 它的作用和ngx_palloc_large可以是基本一样的,有两个不同点:* 1)调用内存对齐的函数ngx_memalign创建内存,而非像ngx_palloc_large内调用不内存对齐的ngx_alloc。* 2)不再去检查前面三个大内存是否为空,而是直接将新创建的大内存p按照头插法放进大内存链表管理。
*/
void *
ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment)
{void *p;ngx_pool_large_t *large;// 1)创建一个内存对齐的大内存p = ngx_memalign(alignment, size, pool->log);if (p == NULL) {return NULL;}// 2)创建管理大内存的结构体,并在下面赋值进行管理该内存large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);if (large == NULL) {ngx_free(p);return NULL;}//头插法插进大内存链表large->alloc = p;large->next = pool->large;pool->large = large;return p;
}
3.销毁内存池ngx_destroy_pool
void
ngx_destroy_pool(ngx_pool_t *pool)
{ngx_pool_t *p, *n;ngx_pool_large_t *l;//ngx_pool_cleanup_t *c;//首先清理pool->cleanup链表/*for (c = pool->cleanup; c; c = c->next) {//handler 为一个清理的回调函数if (c->handler) {ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, pool->log, 0,"run cleanup: %p", c);c->handler(c->data);}}*/#if (NGX_DEBUG)/** we could allocate the pool->log from this pool* so we cannot use this log while free()ing the pool*///清理pool->large链表(pool->large为单独的大数据内存块)for (l = pool->large; l; l = l->next) {fprintf(stderr,"free: %p", l->alloc);}for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {fprintf(stderr,"free: %p, unused: %zu", p, p->d.end - p->d.last);if (n == NULL) {break;}}#endiffor (l = pool->large; l; l = l->next) {if (l->alloc) {ngx_free(l->alloc);}}//对内存池的data数据区域进行释放for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) {ngx_free(p);if (n == NULL) {break;}}
}
4.重设内存池ngx_reset_pool
void
ngx_reset_pool(ngx_pool_t *pool)
{ngx_pool_t *p;ngx_pool_large_t *l;//清理pool->large链表(pool->large为单独的大数据内存块)for (l = pool->large; l; l = l->next) {if (l->alloc) {ngx_free(l->alloc);}}//循环重新设置内存池data区域的 p->d.last;data区域数据并不擦除for (p = pool; p; p = p->d.next) {p->d.last = (u_char *) p + sizeof(ngx_pool_t);p->d.failed = 0;}pool->current = pool;//pool->chain = NULL;pool->large = NULL;
}
5.使用内存池分配一块内存ngx_palloc和ngx_pnalloc
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)//判断每次分配的内存大小,如果超出pool->max的限制,则需要走大数据内存分配策略if (size <= pool->max) {return ngx_palloc_small(pool, size, 1);}
#endifreturn ngx_palloc_large(pool, size);
}//它的作用和ngx_palloc可以是一样的,只不过多了一步清零操作。
void *
ngx_pcalloc(ngx_pool_t *pool, size_t size)
{void *p;p = ngx_palloc(pool, size);if (p) {ngx_memzero(p, size); }return p;
}void *
ngx_pnalloc(ngx_pool_t *pool, size_t size)
{
#if !(NGX_DEBUG_PALLOC)//判断每次分配的内存大小,如果超出pool->max的限制,则需要走大数据内存分配策略if (size <= pool->max) {return ngx_palloc_small(pool, size, 0);}
#endifreturn ngx_palloc_large(pool, size);
}
分配一块内存,如果分配的内存 size
小于内存池的 pool->max
的限制,则属于小内存块分配,走小内存块分配逻辑;否则走大内存分配逻辑。
小内存分配逻辑:循环读取pool->d
上的内存块,是否有足够的空间容纳需要分配的 size
,如果可以容纳,则直接分配内存;否则内存池需要申请新的内存块,调用ngx_palloc_block
。
大内存分配逻辑:当分配的内存size
大于内存池的pool->max
的限制,则会直接调用ngx_palloc_large
方法申请一块独立的内存块,并且将内存块挂载到pool->large
的链表上进行统一管理。
static inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{u_char *m;ngx_pool_t *p;p = pool->current;do {m = p->d.last;if (align) {m = ngx_align_ptr(m, NGX_ALIGNMENT);}if ((size_t) (p->d.end - m) >= size) {p->d.last = m + size;return m;}p = p->d.next;} while (p);return ngx_palloc_block(pool, size);
}static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{void *p;ngx_uint_t n;ngx_pool_large_t *large;p = ngx_alloc(size);if (p == NULL) {return NULL;}n = 0;/* 去pool->large链表上查询是否有NULL的,只在链表上往下查询3次,主要判断大数据块是否有被释放的,如果没有则只能跳出*/for (large = pool->large; large; large = large->next) {if (large->alloc == NULL) {large->alloc = p;return p;}if (n++ > 3) {break;}}/* 分配一个ngx_pool_large_t 数据结构 */large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);if (large == NULL) {ngx_free(p); //如果分配失败,删除内存块return NULL;}large->alloc = p;large->next = pool->large;pool->large = large;return p;
}
6.ngx_palloc_block,内存池扩容
/*** 申请一个新的缓存池 ngx_pool_t* 新的缓存池会挂载在主缓存池的 数据区域 (pool->d->next)*/
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{u_char *m;size_t psize;ngx_pool_t *p, *new;psize = (size_t) (pool->d.end - (u_char *) pool);/* 申请新的块 */m = ngx_memalign(NGX_POOL_ALIGNMENT, psize);if (m == NULL) {return NULL;}new = (ngx_pool_t *) m;new->d.end = m + psize;new->d.next = NULL;new->d.failed = 0;/* 分配size大小的内存块,返回m指针地址 */m += sizeof(ngx_pool_data_t);m = ngx_align_ptr(m, NGX_ALIGNMENT);new->d.last = m + size;/*** 缓存池的pool数据结构会挂载子节点的ngx_pool_t数据结构* 子节点的ngx_pool_t数据结构中只用到pool->d的结构,只保存数据* 每添加一个子节点,p->d.failed就会+1,当添加超过4个子节点的时候,* pool->current会指向到最新的子节点地址** 这个逻辑主要是为了防止pool上的子节点过多,导致每次ngx_palloc循环pool->d.next链表* 将pool->current设置成最新的子节点之后,每次最大循环4次,不会去遍历整个缓存池链表*/for (p = pool->current; p->d.next; p = p->d.next) {if (p->d.failed++ > 4) {pool->current = p->d.next;}}p->d.next = new;return m;
}
分配一块大内存,挂载到pool->large
链表上ngx_palloc_large
:
/*** 当分配的内存块大小超出pool->max限制的时候,需要分配在pool->large上*/
static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{void *p;ngx_uint_t n;ngx_pool_large_t *large;p = ngx_alloc(size);if (p == NULL) {return NULL;}n = 0;for (large = pool->large; large; large = large->next) {if (large->alloc == NULL) {large->alloc = p;return p;}if (n++ > 3) {break;}}large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);if (large == NULL) {ngx_free(p);return NULL;}large->alloc = p;large->next = pool->large;pool->large = large;return p;
}
7.大内存块的释放 ngx_pfree
内存池释放需要走ngx_destroy_pool
,独立大内存块的单独释放,可以走ngx_pfree
方法。
ngx_int_t
ngx_pfree(ngx_pool_t *pool, void *p)
{ngx_pool_large_t *l;/* 在pool->large链上循环搜索,并且只释放内容区域,不释放ngx_pool_large_t数据结构*/for (l = pool->large; l; l = l->next) {if (p == l->alloc) {fprintf(stderr,"free: %p", l->alloc);ngx_free(l->alloc);l->alloc = NULL; // 大内存块的起始地址return NGX_OK;}}return NGX_DECLINED;
}
总结
1)ngx_alloc函数
作用:单独调用malloc开辟内存。
2)ngx_calloc函数
调用ngx_alloc增加清零操作。可认为与ngx_alloc一样。
3)ngx_memalign函数
开辟内存并内存对齐。4)ngx_palloc函数和ngx_pnalloc函数
这两个函数调用small和large开辟内存。
1)ngx_palloc_samll首先在已有内存池中查找可用内存,没有则调用block开辟新内存,并给新内存池管理再插入到内存池链表,所以调用这两个函数的内存在内存池链表上。但实际并不连续,只是内存池结构体连续。所以调用samll必定在内存池链表。
2)而ngx_palloc_large是调用ngx_alloc直接开辟内存后交给ngx_pool_large_t结构体管理,再插入大内存块链表。所以该函数开辟的内存不在内存池。而是必定在大内存块链表中。
所以:开辟的内存在内存池链表或者在大内存块链表。5)ngx_pcalloc函数
调用ngx_palloc,增加清零操作。可认为与ngx_palloc一样。额外函数(较少使用):
6)ngx_pmemalign函数
调用ngx_memalign开辟内存对齐的空间,然后交给ngx_pool_large_t结构体管理,再插入大内存块链表。
所以可认为与ngx_palloc_large一样,只不过ngx_palloc_large少了内存对齐但多了检查前三个结构的步骤。所以可以总结:
1)前三个都是开辟无任何关系管理的内存。
2)后三个需要进行区分:
1:ngx_palloc和ngx_pnalloc开辟的内存在内存池链表或者在大内存块链表。
2:ngx_palloc_samll(即block)开辟的内存必定在内存池结构体链表上管理。
3:ngx_palloc_large和ngx_pmemalign开辟的内存必定在大内存块结构体的链表上管理。