打造支撑海量用户高性能server的内存管理

以此系列文章纪念因 QQ 空间、QQ 农场等">互联网业务爆炸式增长,而日夜 不眠地为公共组件做架构调整、性能优化的激情岁月。

以 QQ 农场、好友买卖为代表的 SNS 网页游戏的突然兴起,全国男女老少一 起日夜不眠的在电脑面前奋战,鼠标、键盘还有各路外挂汇成的请求像洪水一般 涌来。当流量把机房核心交换机冲得七零八落,当我们把市场上的服务器全部买 光都不够时,我终于懂了,这就是 TMD 的所谓的海量。
在这个过程中,从 web 层到逻辑层,再到数据层,每一个组件都经过了一次
洗礼,从小男孩成长为真正的男人。
现在我已经投身于云平台的建设了,这个支撑海量用户的高性能 server 系列 文章就想为这段激情燃烧的岁月做个迟到的总结。目前打算从以下几个方面展开 来:
1) 高性能 server 的内存管理。
2) 大并发下的 server 模型选择。
3) 高效定时器的实践。
4) 异步框架的设计与威力。
5) socket 及 OS 优化。
6) 业务应用的架构调整与思考。
这其中大部分是泥腿子办法,土法炼钢思路,不过却的的确确解决了问题, 经受住了考验。
在这儿把这些东东放出来,我按我的想法写,您按您的想法看,然后,然后 就没有了。。。

如何衡量好与坏

首先,要做好一件事情,就要知道什么是好与坏。那如何评价一个 server 写 得怎么样?我的看法是,能最有效地榨干系统资源。
两个关键词,有效和榨干。 有效表明全在干正事儿,不做无用功;榨干表明充分利用,不浪费。
拿我自己实现的一个 web server 来举例(嘿嘿,我比较得意的一个作品 TencentWebProxy),在压力测试过程中,表现如下:

i. 充分地利用了 CPU,机器共四个核,TencentWebProxy 相应地启动了 四个工作线程,每个线程都完全把系统 CPU 吃尽。每个 CPU 使用都非 常均匀,不管是系统态,用户态,还是软中断。当有更多的 CPU 核心 时,也有通过启动相匹配的线程数,很好的利用多核。此为榨干。

ii. 按 CPU 各个维度的比例来说,系统态和软中断占了近 90%,说明系统大

部分资源在处理网络服务,只有 10%多一点的用在用户态,说明对 HTTP 协议解析等操作的资源消耗控制地比较理想,全在干正事儿。此为有效。
下面就进入这篇的主题,如果构建高性能 server 的第一项,内存管理。

内存管理

最省力的方法--PRELOAD 高效内存分配库

tcmalloc

业界最有名的内存分配库,当数 google 的 tcmalloc。Tcmalloc 在管理小 内存块时非常有效,而且能够避免在大内存分配时的 mmap()系统调用。它在多 线程中的表现也不错能很好的减少锁碰撞(glibc 致命的问题)。Tcmalloc 现在基 本上成了 mysqlDBA 的标配了。

lockless

另一个比较常用的内存分配器是 lockless,着眼点是使用远锁技术来提升多 线程内存分配性能。官方测试数据显示,在多线程环境下表现比 tcmalloc 还好。 当然,官方数据也只能当参考,信一半就差不多了,呵呵。

(The Lockless memory allocator can take about half the time as

other allocators to complete the benchmarks. The Lockless memory

allocator does not suffer from slowdowns at larger allocation sizes.)

hoard

最后一个是 fory 最近向我推荐的 hoard 库,他在 UMass 的老师写的,hoard 最主要的着眼点是在 SMP 系统上,当核心越多的时候它的优势相当明显。用它 自已经的话来说就是“dramatically improve application performance,

especially for multithreaded programs running on multiprocessors and

multicore CPUs”

我还没用过这个 hoard 库,后面有时间试试。
我不爽 glibc 的内存分配算法主要是两点:一是大内存的 mmap,其实这也
不是很大的问题,凡是逼得 libc 调用 mmap,一般都是用法太不讲究了;其二, 这才是最大的问题,多线程分配内存时的锁碰撞,会造成 server 性能直线下降, 直到不可忍受的程度!
用 preload 高效内存库的好处就是立杆见影,不用改代码,不用重新编译。 把 LD_PRELOAD 环境变量导出来就好了。如果线上系统出现内存管理的问题, 我一般都会把 tcmalloc 加载上去看看效果。
要注意的是,所有的动态内存分配算法,肯定是在众多方面做了 trade-off, 用之前要理解清楚了,方能起到预期效果。

通用内存池

内存池是常见的一种自己管理内存的方式,比如说 nginx 内部的所有内存分 配都是基于 ngx_palloc 和 ngx_pfree 及其家族函数进行管理的。
Nginx 分配内存过程一般如下:
1. 如果分配大内存,则直接调 malloc 分配,用完后挂到 large 链表中,下 次分配大内存可从 large 链表中分配。
2. 如果分配的是小内存,则从 current 所指的链表中寻找大小合适的内存, 找到就返回,找不到就新分配。
腾讯使用的三网代理 qhttpd 也使用了内存池技术,不同的是 qhttpd 的内 存是预分配的,启动的时候就分配了一大块内存,运行过程中不再分配。
内存池的缓存级别还是比较低,对应的是内存 buffer 级别抽象。但由于
高性能内存分配库的不断改时,这些自己实现的内存池,不一定就更有效, 并且是各种 bug 的温床。我对这种方式持比较保留的意见。

通用对象池

对象池最有名的当数 linux 内核中的 slab 高速缓冲区,为内核中各种常 见数据结构做缓存,如 skb 等。
在用户空间里一般这样实现:把空闲对象挂到一个链表上,分配的时候 就可以从链表上取下来,释放的时候再挂回去。这都是 O(1)的操作。这样做 的好处是,每种对象基本上都是固定大小的,所以可以直接从空闲链表上拿 下来用。
不过这儿还有一个问题,就是多线程操作同一个链表的锁问题,如果不 做特别设计,会出现类似 malloc 在多线程环境下的锁碰撞问题。
这个问题的解决思路一般有两个:

i. 对象池局部化。局部化后,每个线程都有自己独立的对象池,这样可 以完全避免锁,但是资源不能全局调度。

ii. 对象池半局部化。这是一种折中的办法,指主线程控制所有的资源,线 程来取对象的时候采用批发的方式要一大批对象过去,退还的时候也 批量退还,这样基本上就有效地解决了锁碰撞的问题。

对象池可以很好的解决固定大小的对象问题,但是对于变长类型,如字
符串就不大起作用了,而这也是 web 服务器常常消耗非常大的一些方面。

高性能 Server 专用对象池!!

这才是我真正想要说的终极方案!!!

我写的所有 server 几乎无一例外的使用了这一方式的对象缓存,极其简 单,极其有效,完全解决了内存分配的所有问题……
事情是这样的:
Server 服务器的设计都是围绕着 socket 展开的,通常会把 socket fd 包 装成一个对象,TencentWebProxy 使用的是 epoll,每个 fd 包装成一个 poller 对象。而 fd 是天然的数组下标!!!并且天然地具有唯一性!!!
所以,直接静态分配 poller 对象数组,数组大小为 ulimit 中 fd 的上限。 当 accept 到一个 fd 时,直接到 poller[fd]就找到唯一属于它的 poller 对象 了。对,数组可以随机访问!
那字符串类的对象呢。。。
由于整个 server 是以 poller 为中心设计的,poller 对象的内存 buffer 都是跟 poller 有关联的,比如说 recv_buff, send_buff, tmp_buff 等等,都 会被 poller 用指针或都内嵌 buffer 对象引用到(TencentWebProxy 中使用 xbuffer_t 来管理字符串和变长 buffer)。这些内存被使用到的时候,直接分 配,fd 关闭时,调用 poller 的 reset 函数,将 poller 的对象里面的所有内
容重置,以供下次使用。对于已经分配的 buffer 不释放,只要把标记 buffer
里数据长度(len)的变量置为 0 即可。

这样一来,只有 server 一启动的时候会分配内存,当收到更大的数据包 的时候 xbuffer_t 的 buf 会 remalloc 一下,相应的 size 也会变大。当运行 一段时间后,server 几乎没有内存分配操作了。
当系统中的内存吃紧的时候,可以沿着 poller[]统一回收一下内存。不过 在实际运营过程中发现,这个设计其实没大有必要。

!!!使用以 fd 为下标的对象数据,一定要记住对象 reset 完成之后, 才能关闭 fd。提前关闭 fd 会造成其它线程重新使用了这个 fd,进而两个线程 同时操作同一个 poller 对象,造成内存错误。本人消耗了一下午的时候来解 决这个 bug。。。这儿的时序是一定要保障的,为了安全,我还在这儿加了 内存屏障。

相关代码:
分配 poller 静态数组

Conncetion 是 poller 的子类。
NEW_ARRAY 是分配数组对象的宏,定义如下:

poller 对象时序保证的内存屏障:

字符串方面操作的优化:

1. 用数字比较代替字符串比较,nginx 中也大量使用了这个技巧。这个技巧我 之前在微博上提过。
2. 不用 memset 将 buffer 置 0,只要将第一个字节置 0,然后使用安全的字符
串操作函数就不可能出错。
3. 当 xbuffer_t 在扩展 buf 大小的时候,每次以 128 的粒度增加,避免频繁扩 展。(128 是经验值)
4. 将 memcpy,strlen 等函数自己实现或将代码 copy 过来然后 inline 掉,将会 极大减少函数调用开销。

效果

经过以上方面的优化,我们在使用 perf 来看 TWP 的运行情况来看,已经非 常理想了,占用率高的,不管是内核态还是用户态的,都是有效操作。

呵呵,效果还不错吧。。

昨天晚上写到 3 点多,今天又起了个大早起写完了这篇文章,内心肯定是狂野地 期待各位网友的反馈和意见。
这也是我把这个系列文章写下去的所有动力。。

时间: 2024-12-27 11:05:54

打造支撑海量用户高性能server的内存管理的相关文章

打造支撑百万用户的分布式代码托管平台

在2017在线技术峰会--首届阿里巴巴研发效能嘉年华上,来自研发效能事业部的杨再新分享了<打造支撑百万用户的分布式代码托管平台>.他主要介绍了GIT和SVN思想差异.开源的代码托管平台的挑战.云代码托管平台的架构设计以及云代码托管后续发展.其中,他主要分享了云代码托管平台的设计思路,稳定性.安全性的构建.   以下内容根据直播视频整理而成. 直播视频:https://yq.aliyun.com/edu/lesson/548 PDF下载:https://yq.aliyun.com/attachm

阿里专家倪超:支撑海量用户的阿里中间件技术

大流量高并发互联网应用实践在线峰会官网:https://yq.aliyun.com/activity/112 峰会统一报名链接:http://yq.aliyun.com/webinar/join/49 议题名称:<支撑海量用户的阿里中间件技术> 议题简介:伴随着互联网和移动互联网的盛行,海量的用户一次又一次的洗礼了各个机构的IT系统,而在阿里,这种改变无疑更加频繁与剧烈--这些年下来,中间件技术完成了从1.0到3.0时代的蜕变,并已经完成了将技术变成商业化产品,与业界分享.本议题将围绕这一变革

改善SQL Server的内存管理

最近,为了能在数据库服务器中运行其他应用程序,在保持数据库操作系统版本不变的前提下对数据库服务器进行了软.硬件上的升级.在软件上,将操作系统从Windows 2000升级到Windows Server 2003:在硬件上,将服务器中的内存由原来的512MB增加到1GB(1024MB). 在升级后的开始几个星期之内,服务器在使用中表现良好.但是不久后就发现,在服务器上同时运行的其他应用程序却出现了异常,不时地报出内存分配不足的警告.经过几次跟踪后发现,原来是SQL Server吞去了大部分内存所致

SQL Server 2000内存管理内幕

Introduction 在这篇专栏里,我们将从开发者的角度来探讨SQL Server内存管理内幕.就是说,我们将讨论SQL Server使用API和操作系统功能管理内存的方式及其工作原理.通过这种方式探讨一个产品,将有助于我们理解产品开发者的思路,以及他们所设计的使用方法.理解一个产品的工作原理和它的设计用途,是掌握这个产品的关键. 我们将从一些基础的Windows内存管理基本原理介绍开始.和所有的32位Windows应用程序一样,SQL Server使用Windows内存管理功能分配.释放.

SQL Server 2012内存管理和以前版本相比有些哪变化?

SQL Server 2012 的内存管理和以前的版本相比,有以下的一些变化. 一.内存分配器的变化 SQL Server 2012以前的版本,比如SQL Server 2008 R2等, 有single page allocator 和multi page allocator. 也就是说, 如果申请的内存是8k以内的, 就会有单页分配器分配,而大于8kb的内存请求,使用multi page 分配器来管理.所以,如果你运行DBCC MemoryStatus,你会发现这两个分配器分配的内存情况:

RDC如何打造支撑百万用户的分布式代码托管平台

一.背景介绍 毋庸置疑,代码是DevOps流程的起点,是所有研发流程的基础:代码托管为代码"保驾护航",确保代码的安全性.可用性,同时提供围绕代码的一些基础服务,如MR.Issues等等. 阿里巴巴集团GitLab是基于GitLab社区版8.3版本开发,目前支撑全集团数万规模的研发团队,累计创建数十万项目,日请求量千万级别,存储TB级别,早已超过了GitLab社区版承诺的单机上限能力,且增长速度迅猛. 面对这种情况,顺理成章的做法就是--扩容.然而非常不幸,GitLab的设计没有遵守H

如何打造支撑百万用户的分布式代码托管平台

本文整理自2017云栖大会-上海峰会中阿里云高级技术专家杨再新的分享讲义,讲义主要介绍了开源代码托管平台的挑战,以及云代码托管平台的架构设计与产品规划.

SQL SERVER的内存会不断增加,问题分析

server|问题 SQL SERVER的内存会不断增加 当 SQL Server 数据库引擎在 Microsoft Windows NT 或 Windows 2000 上运行时,其默认内存管理行为并不是获取特定的内存量,而是在不产生多余换页 I/O 的情况下获取尽可能多的内存.为此,数据库引擎获取尽可能多的可用内存,同时保留足够的可用内存以防操作系统交换内存. SQL Server 实例在启动时通常获取 8 到 12 MB 的内存以完成初始化过程.当实例完成初始化后,就不会再获取更多的内存,直

浅谈SQL Server对于内存的管理

简介 理解SQL Server对于内存的管理是对于SQL Server问题处理和性能调优的基本,本篇文章讲述SQL Server对于内存管理的内存原理. 二级存储(secondary storage) 对于计算机来说,存储体系是分层级的.离CPU越近的地方速度愉快,但容量越小(如图1所示).比如:传统的计算机存储体系结构离CPU由近到远依次是:CPU内的寄存器,一级缓存,二级缓存,内存,硬盘.但同时离CPU越远的存储系统都会比之前的存储系统大一个数量级.比如硬盘通常要比同时代的内存大一个数量级.