OTP 15->18升级后,zlib:zip/1引起内存使用升高

来源:互联网 发布:淘宝今日销量 编辑:程序博客网 时间:2024/05/01 10:53

问题

系统原先使用OTP R15B03版本,在升级到OTP 18.0后,CPU下降20%左右(我会告诉你升级就是为了这个么~),但同时发现erlang内存使用比原先高出4倍多。代码、测试环境和用户量均没有变化。通过系统命令发现升级后binary消耗内存显著增加。
升级前,

> erlang:memory(binary).132252416

升级后,

> erlang:memory(binary).487293840

排查

参考Erlang-In-Anger使用recon API recon:bin_leak/1和recon:proc_count/2,并没有发现特定进程占用大量binary的现象。系统启动后不接入用户,并没有观察到内存有显著增长。在接入用户并且用户数量稳定一段时间后,binary内存开销逐渐稳定.
升级前后binary内存对比

当清空用户后,binary内存回落。故推测内存开销和用户行为相关。
随机抓取用户pid,通过erlang:process_info(Pid, binary)查看用户binary内存开销。发现OTP升级后,用户进程会多出4000 bytes的ref binary开销。

> process_info(pid(0,4398,0),binary).{binary,[{2017145916624,128,3},         {2017145923600,4000,3},  %% <-升级后出现         {2017144817240,176,2}         ...]}

系统中每个用户只会处理基本的消息收发,并且消息数据量远远小于4000 bytes,故推测4000 bytes应该是由erlang库函数导致的。搜索erlang源代码,在zlib中有如下

#define DEFAULT_BUFSZ   4000...static ErlDrvData zlib_start(ErlDrvPort port, char* buf){    ZLibData* d;    if ((d = (ZLibData*) driver_alloc(sizeof(ZLibData))) == NULL)        return ERL_DRV_ERROR_GENERAL;    memset(&d->s, 0, sizeof(z_stream));    d->s.zalloc = zlib_alloc;    d->s.zfree  = zlib_free;    d->s.opaque = d;    d->s.data_type = Z_BINARY;    d->port      = port;    d->state     = ST_NONE;    d->bin       = NULL;    d->binsz     = 0;    d->binsz_need = DEFAULT_BUFSZ;      /* initial buf size for zlib driver*/    d->crc       = crc32(0L, Z_NULL, 0);    d->inflate_eos_seen = 0;    d->want_crc  = 0;    return (ErlDrvData)d;}

在用户执行的代码里,我们使用zlib对用户数据进行压缩并存储

compress(UState) ->    ...    zlib:zip(term_to_binary(UData)).

验证后发现在OTP 18.0调用zib:zip/1,即使压缩后实际数据量远小于4000字节,返回数据仍会占用4000字节空间

1> process_info(self(),binary).{binary,[]}2> Bin = list_to_binary(lists:seq(1,100)).<<1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,  22,23,24,25,26,27,28,29,...>>3> process_info(self(),binary).           {binary,[{2786449689392,100,3}]}4> CompressB = zlib:zip(Bin).              %% <- 调用zlib:zip 对100字节数据进行压缩<<99,100,98,102,97,101,99,231,224,228,226,230,225,229,227,  23,16,20,18,22,17,21,19,151,144,148,146,150,145,...>>5> process_info(self(),binary).{binary,[{2786449689392,100,5},{2786449696784,4000,3}]} %% <- 压缩后会产生4000字节ref binary6> size(CompressB).   %% <- 压缩后,实际数据只占用102字节102

解决

压缩数据时,使用term_to_binary(Data, [{compressed, 9}])取代zlib:zip/1,代码修改如下

compress(UState) ->    ...    term_to_binary(UData),[{compressed, 9}]).

修改后,系统binary内存使用回到升级前。


后记

今儿看了下zip_drv.c的源代码,发现DEFAULT_BUFSZ 4000那个值在R15版本的时候已经存在了。所以升级到OTP 18后的内存增长,应该是erlang zlib:zip/1本身改动引起的。

OTP R15B0367> A = term_to_binary({ckiekd,clowkkd,cldoqpa,difejisdf,iesdifjoe,dkjfe,dkeifjowkkd}).<<131,104,7,100,0,6,99,107,105,101,107,100,100,0,7,99,108,  111,119,107,107,100,100,0,7,99,108,100,111,...>>68> process_info(self(),binary).{binary,[{140271109614736,78,3}]} %% A 78 bytes71> B = zlib:zip(A).  %% 再调用zlib:zip对A进行压缩<<107,206,96,79,97,96,75,206,206,76,205,78,73,97,96,79,  206,201,47,207,134,178,82,242,11,11,18,83,24,...>>72> process_info(self(),binary).{binary,[{140271109614736,78,3},         {140271113073160,66,3}]}  %% 新生成的B(66 bytes),返回实际大小73> size(B).66 

区别在于,新版本调用zlib:zip/1的时候,会返回整个zlip:zip/1压缩使用的buffer。

OTP 18.015> A = term_to_binary({ckiekd,clowkkd,cldoqpa,difejisdf,iesdifjoe,dkjfe,dkeifjowkkd}). <<131,104,7,100,0,6,99,107,105,101,107,100,100,0,7,99,108,  111,119,107,107,100,100,0,7,99,108,100,111,...>>16> process_info(self(),binary).{binary,[{48762496,78,3}]} %% A 78 bytes17> B = zlib:zip(A).<<107,206,96,79,97,96,75,206,206,76,205,78,73,97,96,79,  206,201,47,207,134,178,82,242,11,11,18,83,24,...>>18> size(B).6619> process_info(self(),binary).{binary,[{48762496,78,6},         {133169328,4000,4}]}    %% 区别在这里,没有单独为B分配66 bytes的空间

懒得看OTP源码,不清楚是从那个版本引入的,感觉是OTP的一个小bug,回头找他们确认下。
如果一定要使用zlib:zip/1,千万记得调用binary:copy/1,显式地把所需要的结果从4000的buffer里拷贝出来,这样做可以让内存尽快回收,不至于消耗过多内存,如下所示。

OTP 18.031>  A = term_to_binary({ckiekd,clowkkd,cldoqpa,difejisdf,iesdifjoe,dkjfe,dkeifjowkkd}). <<131,104,7,100,0,6,99,107,105,101,107,100,100,0,7,99,108,  111,119,107,107,100,100,0,7,99,108,100,111,...>>32> size(A).7833> B = binary:copy(zlib:zip(A)).<<107,206,96,79,97,96,75,206,206,76,205,78,73,97,96,79,  206,201,47,207,134,178,82,242,11,11,18,83,24,...>>34> size(B).6635> process_info(self(),binary).{binary,[{48791552,78,6},  %%  A 对应的78bytes         {48761696,66,4},  %%  从zlip:zip返回值里copy出来的66 bytes         {48761072,2,1}]}  %%  这个我就不清楚了….

总之,在使用zlib:zip/1的时候需要格外小心binary内存的开销。
尽量 避免使用这个函数
或者 使用binary:copy/1将返回值强制copy一份

1 0
原创粉丝点击