YiluPHP
这家伙很懒,什么都没有留下...

经验 源码干货:一台PHP服务器怎么扛住百万的并发攻击?

浏览数 93108 最后修改时间
正常的秒杀抢购活动,黑客的CC、DDOS攻击都会给系统带来非常高的并发量,如果系统抗不住高并发的请求,就会慢的像乌龟爬,严重时导致服务器宕机,即使重启服务器也无济于事。

PHP的并发量是有限的,配置高一点的服务器空跑PHP能达到6、7千的QPS并发量,而便宜的云服务器空跑PHP只有5、6百的QPS。如果有人持续发起很高的并发请求,很容易就把PHP的请求资源占满,结果就是正常的用户无法访问系统。对于某些人恶意的并发攻击,你还在使用PHP做IP请求的频率限制吗?刚刚说了配置高的服务器空跑PHP的并发量才6、7千QPS,攻击者可以轻松的在一台普通的电脑上发起几万的并发请求,PHP是完全扛不住的。

我们知道一个请求过来,是先由Nginx接收到请求,如果Nginx发现是PHP请求就转交给PHP去处理,而Nginx可承受的并发量是很高的,如果能在Nginx就把非法的请求拦截下来,那可承受的并发量就完全取决于Nginx了。
根据百度百科中的描述,nginx可以达到5万的并发连接数,就是说可以5万个请求同时连接着,如果一个请求的处理时间是10毫秒,那一秒就可以处理500万个请求,即QPS为500万。


当然我们的服务器没有这么高的配置,达不到5万并发连接量,不过网上已经有人给出了“Nginx单机百万QPS的搭建方法”,接下来我不讲搭建Nginx的方法,我要讲两个简单易行的、复制代码即可使用的处理高并发的方法。


第一种方法

Nginx可以通过limit_conn_zone 和limit_req_zone两个组件来对客户端访问目录和文件的访问频率和次数进行限制,巧妙的使用可以给系统的安全进行加固,如对 cc、ddos这类的攻击进行有效的防御。结合公司的业务场景,可以针对不同的请求做不一样的客户端限制。
先看一个应用实例
http {
    #下行定义了一个限制规则容器,其中$binary_remote_addr为客户端IP的变量,demo_name是随便取的一个名称,1m表示给这个规则容器分配1m的内存,,按照32bytes/连接来算,同时可以处理32000个连接。
    imit_conn_zone $binary_remote_addr zone=demo_name:1m;
    ............
    server {
        listen  80;
        server_name  www.yiluphp.com;
        location / {
            limit_conn demo_name 2;  #这里指定一个IP同时只能存在2个请求连接。demo_name 与上面的窗口名称对应,是可以自定义的名称
            limit_rate 100k;   #这里是请求限速,每一个请求限速为100KB/秒
        }
}

通过上面的例子可以看到大致的使用方法,下面介绍指令的详细语法及参数的意义。

限连接数的指令:limit_conn_zone
语法:limit_conn_zone $variable zone=name:size;
默认值:无
写在 http{}里面,用于定义一个限制规则的容器。
其中$variable是一个变量,变量名可自定义,变量需真实存在。name是给这个容器取的名称,可自定义。size是给这个容器分配的内存大小。
会话的数目按照指定的变量来决定,它依赖于使用的变量大小和memory_max_size的值。

限连接数的指令:limit_conn
语法:limit_conn zone_name max_clients_per_ip
默认值:无
写在 http{}、server{}、location{}里面,用于设置具体的限制数量。由此可见限连接数的范围可大可小,想在哪里限制就放在哪里。
指令指定一个会话的最大同时连接数,超过这个数字的请求将被返回”Service unavailable” (503)代码。
其中 zone_name 就是limit_conn_zone指令中定义的容器名称,表示使用此名称的容器规则进行限制。max_clients_per_ip是一个整数,即单个IP的最大同时连接数。

限速指令:limit_rate
语法:limit_rate speed_per_sec
默认值:无
写在 http{}、server{}、location{}里面,用于指定最大网速。
其中speed_per_sec为每秒的最大网速限制,如 limit_rate 300k 是每个连接限速300k/s. 注意,这里是对连接限速,而不是对IP限速。如果一个IP允许多个并发连接,那么每个连接的限速是独立计算的。

这时候你再回头看上面的例子就清楚多了。

限制访问频率的指令:limit_req_zone
语法:limit_req_zone  $variable  zone=name:size  rate=rate_num
默认值:none
写在 http{} 里面,
命令解析:为session会话状态分配一个大小为size的内存存储区,限制了每秒(分、小时)只接受rate_num个IP的频率。

限制访问频率的指令:limit_req
语法:limit_req  zone=name  burst=burst  [nodelay]
默认值:none
使用字段:http、server、location
命令解析:该指令用于指定使用的内存存储区(zone)名称,以及最大的突发请求数(burse)。如果请求的速率超过了limit_req_zone指令中设置的速率,这些请求将被延迟处理,在这种情况下,请求获得服务不可用信息,返回503状态码。

如下例:
http {
  limit_req_zone  $binary_remote_addr  zone=demo_name:10m   rate=5r/s;

  server {
    location /{
      limit_req   zone=demo_name  burst=10;
    }
}

这个例子会让 Nginx 每个IP一秒钟只处理5个请求,超出此频率的请求会在队列里面等待处理,但这样仍然会占用很多tcp连接,如果加上nodelay就会立即丢弃这些超出设定频率的请求。

limit_req zone=demo_name burst=10 nodelay;

设置日志信息等级的指令:limit_conn_log_level
语法: limit_conn_log_level info | notice | warn | error
默认值: error
使用字段: http, server, location
指定当连接数超过设定的最大连接数,服务器限制连接时的日志等级。

1、在 nginx.conf 里的 http {}里加上如下代码:
#ip limit
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
参数说明:
用limit_conn_zone声明一个限制规则。
"zone=" 给它一个名字,可以随便叫,这个名字要跟下面的 limit_conn 一致。
$binary_remote_addr = 用二进制来储存客户端的IP地址,1m 可以储存 32000 个并发会话。
$server_name是限制同一server最大并发数。

2、在需要 限制并发数 和 下载带宽 的网站配置 server {}里加上如下代码:
limit_conn perip 2;
limit_conn perserver 20;
limit_rate 100k;
参数说明:
limit_conn为限制并发连接数;
limit_rate为限制下载速度;

也就是说,要限制连接,必须先有一个容器对连接进行计数,在http段加入如下代码:
... 省掉 N 字
http
{
limit_conn_zone $binary_remote_addr zone=demo_name:10m;

接下来可以在server不同的位置(location段)进行限速,比如限制每个IP并发连接数为3,则
server
{
listen 80;
server_name www.yiluphp.com;
index index.html index.htm index.php;
limit_conn demo_name 3; #是限制每个IP同时只能发起3个连接 (demo_name 要与 limit_conn_zone 的变量对应)
limit_rate 200k; #限速为 200KB/秒
root html;
注意事项:
limit_rate 200k; 是对每个连接限速200k,是对连接限速,而不是对IP限速,如果一个IP允许三个并发连接,那么这个IP就是限速limit_rate值 * 3

第二种方法

在Nginx中使用Lua和Redis拦截高并发。
Lua是轻量级嵌入式脚本语言,Lua在脚本语言中是运行时速度最快的,有人做过测试用nginx_lua实现简易的API是PHP的8倍,而且lua运行时占用内存特别少。nginx_lua是nginx的一个模块,是异步非阻塞的运行方式。
使用Lua你可以实现更复杂的业务处理逻辑,比如秒杀抢购时先把请求存入Redis队列、对特定的账号或特定的IP或特定的链接做不一样的限制规则等等。

操作方法是:
1、给Nginx安装lua支持
安装方法在这里:

2、下载redis.lua库
其实就是一个文件,已经写好了操作redis的方法,代码在此,复制粘贴到你的文件中就行了。

3、使用Lua编写你想实现的代码
这里提供一个Lua例子:限制单个IP在5秒和1小时内的请求频率
--将此文件存放在/usr/local/tengine-2.2.1/lua_file/ip_limit.lua
--同时此脚本依赖于/usr/local/luajit/share/luajit-2.0.5/resty/redis.lua,如果没有此文件可以从yiluphp官网搜索
--在nginx配置中使用此方法调用此lua脚本:
--access_by_lua_file lua_file/ip_limit.lua;

--5秒内的最大请求频率数
local secMax = 3;

--1小时内的最大请求频率数
local hourMax = 6;

--redis键名为IP_REQUEST_LIMIT加客户端的IP
local cacheKey = "IP_REQUEST_LIMIT" .. ngx.var.remote_addr;

--连接redis
local redis = require "resty.redis";
local redisClient = redis:new();
redisClient:set_timeout(20);
local ok, err = redisClient:connect("127.0.0.1", 6379);
if not ok then
    ngx.log(ngx.DEBUG, "error:" .. err);
else
	--往列表中添加一次请求
	redisClient:lpush(cacheKey, ngx.now());
	--删除列表中多余的数据
	redisClient:ltrim(cacheKey,0,hourMax);
	--设置缓存时间为1小时
	redisClient:expire(cacheKey, 3600);
	-- 获取刚好超出5秒内频率次数的值
	local checkVal, err = redisClient:lindex(cacheKey, secMax);
	if err then
		ngx.log(ngx.DEBUG, "error:" .. err);
	else
	    if checkVal ~= ngx.null then
			if tonumber(checkVal)>tonumber(ngx.now()-5) then
				redisClient:close();
				--返回一个提示错误的页面
				--ngx.exec("/error/request_frequency_limit.html", "a=goodbye");
				--如果不使用上面的提示方法,则可以通过下面三行输出提示
				ngx.header['Content-Type']="text/html;charset=UTF-8";
				ngx.say('你请求太快了,休息一下吧');
				--ngx.say(checkVal .. cacheKey);
				ngx.exit(200);
			end
			-- 获取刚好超出1小时内频率次数的值
			local checkVal, err = redisClient:lindex(cacheKey, hourMax);
			if err then
				ngx.log(ngx.DEBUG, "error:" .. err);
			else
				if checkVal ~= ngx.null and tonumber(checkVal)>ngx.now()-3600 then
					redisClient:close();
					--返回一个提示错误的页面
					--ngx.exec("/error/request_frequency_limit.html", "a=goodbye");
					--如果不使用上面的提示方法,则可以通过下面三行输出提示
					ngx.header['Content-Type']="text/html;charset=UTF-8";
					ngx.say('你请求太快了,休息一下吧!');
					--ngx.say(checkVal .. cacheKey);
					ngx.exit(200);
				end
			end
		end
	end
end
redisClient:close();

快检查一下你的网站,看看有哪些页面或接口需要提高性能或者做频率限制吧。

我来说说