不知不觉踩到PHP内存泄漏的雷

27 Jun 2019 Category: php

最近工作上需要排查php频繁达到内存限制进程被杀掉的原因。项目中使用php写一个死循环,把mysql的数据同步到mq或者mongodb当中。内存问题主要出现在mq消息的发布上。项目中有使用到php-amqplib。

跟踪代码发现,循环内部,获取mq单例对象有问题导致每次循环都是new的一个mq对象。刚开始以为是这个原因导致内存不断增长。三下五除二就改完了,结果一试,没什么效果,还是不断飙升啊。

既然不是新对象引起的,那估计就是就对象的问题。因为新建对象都没有对已有的mq对象进行处理,例如端口连接,释放资源等。因此在新建对象之前,执行php-amqplib 中connection的close操作,关闭连接以及释放资源。关闭之后再操作,确实有些改变,飚的慢点,但是还是会飚。然后又在循环结束的时候unset对象,结果依然没什么变化。

只能接着看代码。php-amqplib中connection的属性中有一个channels属性,用于保存channel对象数组。然而这个channel对象本身又有一个connection属性,这样这两个对象之间就构成一个循环引用,当我们删除connection以及channel的时候,内部引用计数器不会到0,所以内存不会被释放。

用一下简化版说明一下其中的问题:

!image


class Channel{
    protected $connection;
    function __construct($connection){
        $this->connection = $connection;
    }

    public function release(){
        $this->connection = null;
    }

}

class Connection{
    protected $channel;
    protected $data = null;
    function __construct(){
        $this->channel = new Channel($this);
        $this->data= '这里填充数据';
    }
    function __destruct(){
        $this->channel->release();
        $this->data = null;

    }
}



echo "start time ".date('Y-m-d H:i:s')."\n";
for($i=0;$i<20000;$i++){
    $ch = new Connection();
    $ch = null;
    if($i%100==0){
        echo date('Y-m-d H:i:s')." instance memory [".$i."]".(memory_get_usage()/1024)."k\n";
    }
}

按正常的逻辑,对象赋值null,那对象所占用的内存应该要被释放。上面的代码输出内容如下:


2019-06-17 13:23:28 instance memory [0]246.5546875k
2019-06-17 13:23:28 instance memory [300]8541.109375k
2019-06-17 13:23:28 instance memory [600]16867.6484375k
2019-06-17 13:23:28 instance memory [900]25162.1953125k
2019-06-17 13:23:28 instance memory [1200]33520.734375k
2019-06-17 13:23:28 instance memory [1500]41815.28125k
2019-06-17 13:23:28 instance memory [1800]50109.828125k
2019-06-17 13:23:28 instance memory [2100]58532.3671875k
2019-06-17 13:23:28 instance memory [2400]66826.9140625k
2019-06-17 13:23:28 instance memory [2700]75121.46875k
2019-06-17 13:23:28 instance memory [3000]83416.015625k
2019-06-17 13:23:28 instance memory [3300]91710.5546875k
2019-06-17 13:23:28 instance memory [3600]100005.09375k
2019-06-17 13:23:28 instance memory [3900]108299.6640625k
2019-06-17 13:23:28 instance memory [4200]116850.1953125k
2019-06-17 13:23:28 instance memory [4500]125144.7421875k
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted

可以看出,跑了4500次之后内存就已经操作128M了。如果Channel中没有connection的属性,则会有不一样的结果。我们把Channel的构造方法注释掉,再重新跑

start time 2019-06-17 13:34:34
2019-06-17 13:34:34 instance memory [0]218.8046875k
2019-06-17 13:34:34 instance memory [300]218.8125k
2019-06-17 13:34:34 instance memory [600]218.8125k
2019-06-17 13:34:34 instance memory [900]218.8125k
2019-06-17 13:34:34 instance memory [1200]218.8125k
2019-06-17 13:34:34 instance memory [1500]218.8125k
2019-06-17 13:34:34 instance memory [1800]218.8125k
2019-06-17 13:34:34 instance memory [2100]218.8125k
2019-06-17 13:34:34 instance memory [2400]218.8125k
2019-06-17 13:34:34 instance memory [2700]218.8125k
2019-06-17 13:34:34 instance memory [3000]218.8125k
2019-06-17 13:34:34 instance memory [3300]218.8125k
2019-06-17 13:34:34 instance memory [3600]218.8125k
2019-06-17 13:34:34 instance memory [3900]218.8125k
2019-06-17 13:34:35 instance memory [4200]218.8125k
2019-06-17 13:34:35 instance memory [4500]218.8125k


只是一个简单的修改,循环就没有内存的问题了。

问题的根本就是对象之间循环引用。有个很有趣的现象,如果对象之间构成循环引用,在xdebug中就可以看到一个无限的树状对象。Connection->Channel->Connection->Channel….

对于普通的web应用而已,一般不会有什么问题,每次请求结束之后fpm会释放掉。但是对于cli应用,这就是致命的。基本上跑个一天就挂了。

但是,现实就是这样。对象之间相互引用很容易出现。这个model需要那个model,几个model之间也很容易构成一个回环。同时,很多东西需要引用第三方类,没办保证第三方类没有相互引用。那有没有不改类之间引用可以解决的呢?

在这次排查,我使用的是gc_collect_cycles()强制执行gc操作,释放内存。还是第一段程序代码,循环内容改为一下内容:


echo "start time ".date('Y-m-d H:i:s')."\n";
for($i=0;$i<20000;$i++){
    $ch = new Connection();
    $ch = null;
    gc_collect_cycles();
    if($i%100==0){
        echo date('Y-m-d H:i:s')." instance memory [".$i."]".(memory_get_usage()/1024)."k\n";
    }
}

输出内容如下:


start time 2019-06-17 13:43:41
2019-06-17 13:43:41 instance memory [0]219.125k
2019-06-17 13:43:41 instance memory [300]219.1328125k
2019-06-17 13:43:41 instance memory [600]219.1328125k
2019-06-17 13:43:41 instance memory [900]219.1328125k
2019-06-17 13:43:41 instance memory [1200]219.1328125k
2019-06-17 13:43:41 instance memory [1500]219.1328125k
2019-06-17 13:43:41 instance memory [1800]219.1328125k
2019-06-17 13:43:41 instance memory [2100]219.1328125k
2019-06-17 13:43:41 instance memory [2400]219.1328125k
2019-06-17 13:43:41 instance memory [2700]219.1328125k
2019-06-17 13:43:41 instance memory [3000]219.1328125k
2019-06-17 13:43:41 instance memory [3300]219.1328125k
2019-06-17 13:43:41 instance memory [3600]219.1328125k
2019-06-17 13:43:41 instance memory [3900]219.1328125k
2019-06-17 13:43:41 instance memory [4200]219.1328125k
2019-06-17 13:43:41 instance memory [4500]219.1328125k

内存飙升的问题解决了。

网上很多描述都是php5.3之后的gc会自动回收类似这类的垃圾,但是前提是zend节点满了。但实际上,说的只是数组类型。下面的代码在循环结束之后,局部变量data的资源会得到释放。


echo "start time ".date('Y-m-d H:i:s')."\n";
for($i=0;$i<20000;$i++){
    $data = ['a'=>'11111111111111111111111111'];
    $data['b'] = &$data;
    if($i%300==0){
        echo date('Y-m-d H:i:s')." instance memory [".$i."]".(memory_get_usage()/1024)."k\n";
    }
}

总的而言,PHP在一些长时间的循环运行当中,一定要小心对象之间相互引用造成内存上升的问题。如果遇到内存上升问题,可以先看看代码当中有没有什么类之间存在循环引用。平时写代码的时候也需要尽量避免对象之间构成循环引用,避免在不经意之间给自己或团队挖个坑。

查看更多

Laravel 5.8关于数组的小彩蛋

19 May 2019 Category: php

今天看Laravel5.8代码的时候,看到几个小彩蛋。

1、获取关联数组中指定部分键值组成的数组

项目中常常有这么一种场景,一些数据的生成需要一些其他的数据获得,但是返回给用户的不需要原始数据。比如对于一个用户数据如下:

{
    "userid":10,
    "username":"abc",
    "type":1,
    "status":1,
    "system_code":"xtfy",
    "system_uid":1,
    "file_id":1
}

如果我最终想要返回给用户的结果如下

{
    "userid":10,
    "username":"abc",
    "type":1,
    "status":1
}

这种情况就需要获取数组中指定的部分内容。实现方式有一下几种方式:

  • unset不需要的数据字段
  • 重新创建一个变量,然后一个个字段的添加到新数组中去

在Laravel5.8中Support\Arr中有一个only方法,使用php原生数组函数的键名交集,返回指定键数组的内容。实现方式如下:

array_intersect_key(
	$array, 
	array_flip((array) $keys)
);

根据这个思路,获取指定键之外的数组可以用下面的方式实现:

array_diff_key(
	$array, 
	array_flip((array) $keys)
)

2、如何判断关联数组

首先明白关联数组是什么。下面这样是关联数组

$a = [3=>1,4=>'a'];
$b=['a'=>1,'b'=>2];

而以下内容则不是

$a = [1,2,3];
$b = [0=>1,1=>2,2=>3];

如何判断一个数组是不是关联数组呢?之前的做法都是判断键是否都是数字,其实是不准确的。

在Laravel5.8中有一个isAssoc方法。实现方式如下:


$keys = array_keys($array);

return array_keys($keys) !== $keys;

简单而言,如果数组key的key还是一样的,则不是关联数组。

查看更多

web 开发会用到的linux命令

07 Mar 2019 Category: php

作为web开发,难免和服务器打交道。这里整理一下工作上会用到的命令。

1、获取服务器类型 很多情况下,由于历史遗留因素,给到你的服务器信息都只是一个访问ip,用户名,密码,其他什么都没了。所以登录到服务器上,一开始先要确定的是这台服务器是什么系统,什么版本。

cat /etc/issue

查看日志

cat -n 10 access.log 查看文件并显示行号

tail -n 10 access.log 查最后集10行日志

head -n 10 access.log 查看文件前10行

tail -n +10 access.log 查看第10行之后的日志

tail -n +10 access.log|head -n 5查看第10行后面5行日志

tail -f access.log 实时输出日志内容

cat -n 10 access.log|tail -n +10 |head -n 5 查看第10行后面5行日志并显示原始行号

grep "who" -n access.log 在日志中查找包含who关键字的行,-n显示行号

grep "who|abcdef" -n access.log 在日志中查找包含who或者abcdef关键字的行,-n显示行号

grep -E "abcdef.*who" access.log 在日志中使用正则查找匹配行日志

sed -n '/08\/Mar\/2019:18:13:00/,/08\/Mar\/2019:18:13:25/p' access.log 查看2019:18:13:00~2019:18:13:25时间段的日志(两个时间段必须是日志中出现的)

文件太大,传输不变,分割合并

split -l 300 file.txt new_prefix 将文件每300行分隔成一个文件,并指定分割后的文件名前缀

split -b 10m file.txt new_prefix 将文件每10M分割成一个文件,并指定分割后的文件名前缀

cat new_prefix* > file.txt 将多个分割文件合并

查找文件

find / -name php.ini 在根目录下,按文件名查找文件

find / -name php* 在根目录下,查找以php开始的文件

find / -size 1500c 在根目录下,查找文件大于1500byte的文件

find命令选项: -amin n 查找系统中最后N分钟访问的文件 -atime n 查找系统中最后n*24小时访问的文件 -cmin n 查找系统中最后N分钟被改变状态的文件 -ctime n 查找系统中最后n*24小时被改变状态的文件 -empty

locate命令其实是”find -name”的另一种写法,但是要比后者快得多,原因在于它不搜索具体目录,而是搜索一个数据库(/var/lib/locatedb),这个数据库中含有本地所有文件信息。Linux系统自动创建这个数据库,并且每天自动更新一次

whereis命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。

查看磁盘使用率,目录大小,大文件大目录

df -h 查看所有磁盘挂载点使用情况

df -h /dev 查看dev磁盘使用情况

du --max-depth=1 -h /dev 查看dev下一级文件夹以及dev本身的大小

``du -hm –max-depth=1 /dev sort -nr head -12 查看dev目录下一级目录大小,并转换成M大小单位,逆序,取前12个

查看网络流量

查看更多

PHP 关于数组排序的函数

06 Mar 2019 Category: php

php的数组排序函数有很多。有按键排序,有按值排序。有升序,有降序。有的排序后改变原数组索引,有的不改变。

关于PHP的排序函数,官方文档给出了下面的一个总结表:

php array sort functions

以上函数排序结果都是通过引用传递到原数组中去,而不是返回一个新的有序的数组。

查看更多

清理代码提交记录--代码管理的git

04 Mar 2019 Category: 工具

变化总是存在。

如果有一天,你们代码仓库服务器挂了怎么办?

如果有一天,你们需要分离测试与线上代码仓库,那怎么合并代码,手工合并吗?

如果你们好多个版本,不同的版本对应不同的用户,用户希望他们的代码仓库在他们的服务器而不是你的,怎么办?

千万不要相信那些诸如”你先弄过去,以后代码丢给他们就不管了”的鬼话。如果你信了,你真的新建一个仓库,把代码推到新仓库上了。当新的需求 “把最新版本合并到xxx的代码上”,你就懵逼了,这都不是一个源,怎么合并。一行行代码去查吗?

查看更多

09 Jul 2019 Category:

查看更多

代码管理的git--应对百变的需求

01 Mar 2019 Category: 工具

需求变更主要涉及到时间安排的变化,上线内容的变化,版本回退等。

分支

git 可以使用多个分支同时进行开发。一般情况,在开发团队中会有开发版本分支(dev),发布版本分支(release)。再日常项目中,我一般还会添加一个预发布分支(pre-release)。作为预发布代码的测试。

开发分支用于做日常开发。发布分支用于发布线上代码,预发布分支用于预发布测试。正常开发流程,在开发分支完成测试环境测试之后,会合并到预发布分支上,在预发布环境进行测试。预发布环境测试完之后,再合并到发布分支上。

如果在新版本开发的过程中,线上代码有个bug需要修复,可以从预发布分支上检出一份代码,修复bug之后再合并到预发布分支上,在预发布环境测试。测试没问题再发布到线上分支。

整个过程流程图如下:

git 开发 版本 流程图

以下是实际开发的一个截图

git dev branch

git fix bug

分支相关命令如下:

## 创建新分支
git branch newbranchname 

## 切换到新分支
git checkout newbranchname

## 创建并切换到新分支
git branch -b newbranchname

## 将新分支推送到远程服务器(先进入到分支中)
git push --set-upstream origin newbranchname

## 删除分支(本地分支)
git branch newbranchname

## 强制删除(本地分支)
git branch -D newbranchname

## 删除远程分支(不会删除本地分支)
git push origin --delete newbranchname

## 查看本地分支
git branch 

## 查看所有分支(会把远程分支列出来)
git barnch -a

## 查看本地分支与远程分支关联关系
git branch -vv

分支解决了开发环境,测试环境,预发布环境代码问题。有了分支之后,开发人员可以在不同的情况下切换到不同分支进行开发。

合并指定内容

一般的版本开发都需要一个周期。但是有的时候,前期评估周期是一个月,但是在刚开发到半个月的时候就在想能否部分功能先上线。开发过程提倡的是频繁提交,因此一些功能散落在多次提交当中,并且每次提交都是相对的功能专一。这种情况下,比较适合单独合并指定commit。

git中cherry-pick就可以实现这个功能。cherry-pick会把commit的变更内容”复制”过来,同时在当前分支生成一个新的commit。commit hash与原来的不再相同。如果没有冲突,则commit msg 是一样的。如果有冲突,则需要自己手动解决冲突,编辑提交信息

## 合并指定的commitid到本分支
git cherry-pick commit-hash

## 合并连续的多个commit
git cherry-pick commit-start..commit-end

在测试分支上的version1的提交

dev version1

在预发布分支上,使用cherry-pick version1

cherry-pick version1

使用cherry-pick之后,预发布版本上的内容有了version1的提交。同时创建了一个新的commit。

找回历史的一个版本

需求变更是常事,经常做了一圈之后发现还是第一个版本好,这个时候想要找到以一个版本的内容。

恢复指定文件到指定版本。首要任务是先找到指定版本。可以使用git log查找指定版本。

## 查看提交记录
## (包含提交信息,时间,提交人)
## 只会显示当前分支记录
git log

## 查看所有分支记录
git log -g

## 按commit msg 查找
git log -g --grep="查找关键字"

## 按时间查找
git log --after="2019-02-27 00:00" --before="2019-02-27 12:00"

## 显示指定文件的历史记录
## (会显示文件变更内容)
git log -p filename

## 按文件内容变化查找
git log --all -S "文件内容关键字" filename

使用git log 查找到需要恢复的commitid。一般情况,由于版本开发过程,文件之间相互引用,不会直接替换成旧版本,而只是把一部分功能恢复成旧版本。

可以使用checkout将旧版本和当前版本合并。

## checkout 文件 
## -p参数显示commit变更内容
## 然后根据提示合并内容
git checkout commit-id filename -p

也可以使用git show将旧版本输出到一个文件,然后手动合并内容

git show commit-id:filename > old_version_savepath

以下是演示截图

checkout show

对于线上版本,如果真的不幸,上线之后又bug无法修复,那只能是进行版本回滚。回滚到上一个发布版本的commit-id。

## 将版本回退到指定commit
git checkout commit-id

以上内容就是在需求变更使用频繁的命令。主要设计到分支操作,git log的操作,以及checkout 历史的操作。

开发过程中除了上述命令之外,还有一些其他操作,可以很好的帮助开发人员提高代码管理的效率。比如整理commit,使得发布版本变得干净。更换,添加远程地址,修改提交信息,放弃提交内容等。避免篇幅过长,放在下一篇文章写。

查看更多

SVM支持向量机推导过程

28 Feb 2019 Category: 算法

SVM是一种二类分类模型,目的是找到一个超平面,使得样本点之间的间隔尽可能的远。样本中距离超平面最近的点的集合叫做支持向量。分类平面仅由这些支持向量决定。当两类样本的最近距离最大,分类器的泛化能力才越强。

推导svm算法,涉及到下面几点知识:

  • 点到平面的距离

对于点$x_i$ 到平面$w^Tx+b$的距离为: $d=\frac{w^Tx_i+b} {\parallel w \parallel}$

  • 不等式约束极値问题求解(KKT条件)

对于极値问题

可以使用KKT条件求解

如果$x ^{*} $ 是极值点,则一定有$\lambda ^{*} $

以上三个条件叫做KKT条件,更多内容参考链接

回到SVM当中,目标是最大间隔$max\ margin(w,b)$。间隔由两个分类最近的样本决定。假设存在一个平面$w^Tx_i+b$ 划分两个样本,则两个样本的间隔可以表示为最近样本到平面的距离。

正样本$y_i=1$样本,距离边界点的距离为$ d=\frac{w^Tx_i+b} {\parallel w \parallel}\geq 0 $,负样本$y_i=-1$ 距离边界点的距离$ d=\frac{w^Tx_i+b} {\parallel w \parallel}\leq 0 $ (距离是有方向的) ,对上述距离进行转换为无方向的距离

$r = y_i \frac{w^Tx_i+b} {\parallel w \parallel} $ ,$r$叫做函数距离。

查看更多