[原]关于高并发下限流用户访问

    在高并发大流量下总是有一些意想不到或者考虑不周全的事情发生。而一个网站首先是保证能够提供服务,哪怕分流或者降低速度(不得已情况下)。这就需要有一套限流的方案,在不得已的情况下启用,防止服务器停止服务。就好比小米商城抢小米时候的排队,其实就是类似的。这里我们向讨论一些简单的方案思路。

   方案1:

    直接按照用户访问数量的百分比进行限制。比如要举行大的促销。预计并发访问量是 15W,集群服务器平均打到每台机器上的并发访问量是 500,而单台机器的最大承受并发400,这时候只要限制20%的流量就可以了。(这些数据都是粗糙的举例,不要较真啊。)

    但是这种方案明显有一个弊端就是你判断不出来,当前的访问量是否需要启用限流,以为这样的限流都是针对某次大规模请求,有预估,促销之前,代码启用,促销之后,流量下降,代码就要下线。很是麻烦和不好评估统计。

   方案2:

    首先我们可以确定,一台机器的承受能力一般是固定的,可以测试出来的。如果我们能涉及一种计数机制,当QPS达到机器极限的时候限制,就可可以了。

    另外设计这种计数机制不能同步进行,也就是不能加锁,因为同步计数的话,会导致另外的进程等待。。这明显不符合初衷。比如说,我试过用memcache计数,但是很明显,链接memcache然后计数是一个同步过程,最后计数倒是一个都不差,但是很慢。但是不加锁并发惊醒的话就会导致有误差,也就是说多个进程取到同一个值,这个其实我们可以在测试过程中获得一个这种情况出现的概率。然后只要概率在接收范围之内就可以了。所以宗上,PHP的无锁共享内存就是一个可以考虑的方式。

    这里先说一下结果。

    在我自己的机器上,PHP5.4+nginx 四核 i5 nginx进程数4 模拟并发请求 200 限制用户数 100,超过计数的用户会跳转到一个等待页面。

    在这种情况下,实际数据是 进入的用户回进入 100-110左右。也就是说 会上浮 5%-10% (不过我觉得线上服务器CPU多达20多个核心,进程数也多很多,所以误差可能会更大一些

    下面看说一下实现方式:

    在共享内存当中存两个字段,一个字段用来计数,另一个字段用来计时,每一秒中,计数器从0开始重新计,如果超过预设值,就 跳出,否则进入。

    注意:PHP共享内存有两种方式,一种是 shmop系列,还有一种是 shm_attach 系列,这两种都可以完成共享内存的操作,具体区别。。请自行补脑。。我这里使用的是 shmop。

    

<?php
/**
 * PHP 共享内存的封装
 * @version 2014-1-17
 * @author wwpeng
 *
 */
class SHM
{
    /**
     * 共享内存块的ID
     * @var int
     * @access protected
     */
    protected $id;
    
    /**
     * 根据共享内存块ID shmop_open 成功后 返回的shmID
     * @var int
     * @access protected
     */
    protected $shmid;
    
    /**
     * 共享内存块的默认权限(八进制)
     * @var int
     * @access protected
     */
    protected $perms = 0644;
    
    
    /**
     * 传入一个共享内存ID,如果不传入将生成一个随机ID 1-65535
     * @param string $id
     */
    public function __construct($id = null,$size = 1024)
    {
        if(is_null($id))
        {
            $this->id = $this->getRandID();
            
        } else {
        	
            $this->id = $id;
        }

        try{
                if(!($this->shmid = @shmop_open($this->id, 'w', 0, 0)))
                {
                        $this->shmid = shmop_open($this->id, 'c', $this->perms, $size);
                }
        
        }catch(Exception $e){
        
                $this->shmid = shmop_open($this->id, 'c', $this->perms, $size);
        }
    }
    
    /**
     * 产生一个随机ID
     * @return number
     */
    protected function getRandID()
    {
        $id = mt_rand(1, 65535);
        return $id;
    }
    
    /**
     * 共享内存使用覆盖写入,每次写入都会覆盖以前的数据
     * @param string $data
     */
    public function write($data,$offset = 0)
    {
    	shmop_write($this->shmid, $data, $offset);
    }
    
    /**
     * 读取共享内存块中的全部数据
     * @return string
     */
    public function read($start = 0,$size = null)
    {
    	if (empty($size)) 
    	{
    		$size = shmop_size($this->shmid);
    	}
        $data = shmop_read($this->shmid, $start, $size);
        return $data;
    }
    
    /**
     * 给共享内存添加删除标记,这样其他进程就不能使用这个共享内存块了
     */
    public function delete()
    {
        shmop_delete($this->shmid);
    }
    /**
     * 关闭共享内存块,这样共享内存块会被删除
     */
    public function close()
    {
    	shmop_close($this->shmid);
    }
    
    /**
     * 获得当前共享内存块的ID
     * @return number
     */
    public function getId()
    {
        return $this->id;
    }
    
    /**
     * 获得当前共享内存的权限
     * @return number
     */
    public function getPermissions()
    {
        return $this->perms;
    }
    
    /**
     * 设置当前共享内存的权限(八进制)
     * @param unknown $perms
     */
    public function setPermissions($perms)
    {
        $this->perms = $perms;
    }
    
    /**
     * 自动关闭共享内存快
     */
    public function __destruct()
    {
        shmop_close($this->shmid);
    }
}
    
function traffic()
    {
    	$shm = new SHM(ftok(__FILE__, 's'),100);
    	$data = explode('}',$shm->read());
    	if (count($data) >= 2)
    	{
    		$data[0] .= '}';
    		$data = json_decode($data[0],true);
    
    	}else{
    
    		$data = '';
    	}
    	if (empty($data))
    	{
    		$data = array('count'=>1,'time'=>time());
    	  
    	}else{
    	  
    		if ($time > $data['time'])
    		{
    			$data = array('count'=>1,'time'=>time());
    			 
    		} else {
    
    			$data['count'] += 1;
    		}
    	}
    	$shm->write(json_encode($data));
    	return $data['count'];
    }

     再次注意:关于SHM这个类库。是从github上面的 SimpleSHM改写过来的。GitHub 因为原来的类库对我们的这个需求在实际使用上会有问题。。主要改动实在 __construct 和 write 方法中。具体原来的写发可以去GitHub查看。原来他为了每次申请的共享内存正好能存取想要存的字符串长度,所有会有频繁的删除重新申请共享内存的操作。而我们的需求在并发量较高的情况下,会有并发操作,如果两个进程同时删除共享内存就是出现错误,因为是无锁共享内存,我改写的不删除的方式其实也会有多进程同时读写数据的问题,这就是我们上面说的 5-10个百分点误差的来源。

    traffic 函数会返回当前访问者是当前这一秒内第几个人。这样就可以针对自己的服务器情况,去对比这个数字然后做一些你想做的事请啦。。。。。。。。肯定有大神有更完善的方式。欢迎拍砖