背景
面对高峰期间低频慢业务对主业务的影响时,一般的做法有两种,一种是大公司采用的底层优化, 代价大收效高,但性价比低,一种是小微企业采用的降级方案,在服务器资源消耗较高时,此做法性价比相对高一些,既不需要太大的研发工作量,也较好的保护了主业务不受影响。
方案
三层防御,首先是数据库层,一旦发现CPU较高,甚至达到100%,可使用脚本批量kill mysql线程,第二层是dubbo层,可利用dubbo admin一键禁止某服务,杀伤力较大,请谨慎使用,第三层也就是web层的防御,通过ngnix对请求进行拦截,只要url在我的黑名单里面,nginx统一返回500,提示用户当前业务已降级,请稍后再试,从而释放了底层数据库服务器资源,保护主业务正常访问。这里主要介绍第三层方案。
实施
- 使用redis destop创建键值集用于存放需要拦截的url地址,后续可以持续加入
SADD url_block /webapp/path/get
- 确保已安装了nginx并启用了lua,在nginx.conf文件中http模块加入以下配置:
lua_shared_dict limit 50m;
lua_shared_dict block_url 50m;
access_by_lua_file /usr/local/nginx/conf/url_block.lua;
- 上述url_block.lua主要内容是每隔10秒从redis中取出最新需要拦截的url,如果客户端url在黑名单中,则返回提示语,中断请求。脚本如下:
function get_client_url()
local CLIENT_URL = ngx.var.uri
if CLIENT_URL == nil then
CLIENT_URL = "unknown"
end
return CLIENT_URL
end
local redis_host = "172.27.66.1"
local redis_port = 6379
local redis_auth = "xxxxxxxx"
local limit = ngx.shared.limit
local random = ngx.var.cookie_seed
local CC_TOKEN = ngx.var.remote_addr .. "_token"
local redis_conn_timeout = 1000
local redis_key = "url_block"
local cache_ttl = 10
local url = get_client_url()
local url_list = ngx.shared.block_url
local last_update_time = url_list:get("last_update_time");
if last_update_time == nil or last_update_time < (ngx.now() - cache_ttl) then
local redis = require "resty.redis";
local r = redis:new();
r:set_timeout(redis_conn_timeout);
local ok,err = r:connect(redis_host, redis_port);
r:auth(redis_auth)
if not ok then
ngx.say(ngx.DEBUG, "failed" .. err)
ngx.log(ngx.DEBUG, "connect redis failed " .. err);
else
local new_url_list, err = r:smembers(redis_key);
if err then
ngx.say(err)
ngx.log(ngx.DEBUG, "get block url failed " .. err)
return ngx.exit(401)
else
url_list:flush_all();
for index, banned_url in ipairs(new_url_list) do
url_list:set(banned_url, true)
end
url_list:set("last_update_time", ngx.now());
end
end
end
if url_list:get(url) then
json = require "cjson"
local ret = {};
ret["errorCode"] = "-1";
ret["message"] = "😭亲爱的用户,当前使用此业务的用户较多,请稍后再试";
ngx.header['Content-Type'] = 'application/json; charset=utf-8';
ngx.status = ngx.HTTP_INTERNAL_SERVER_ERROR
ngx.say(json.encode(ret));
ngx.exit(405);
end
附批量杀mysql线程的脚本:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lrapi.lr;
import lrapi.lr;
public class Actions
{
Connection connect=null;
Statement stmt=null;
public int init() throws Throwable {
Class.forName("com.mysql.jdbc.Driver");
System.out.println("Success loading Mysql Driver!");
lr.start_transaction("conncetion");
connect = DriverManager.getConnection("jdbc:mysql://mysql001/information_schema", "user01", "123456");
lr.end_transaction("conncetion", lr.AUTO);
stmt = connect.createStatement();
return 0;
}//end of init
public int action() throws Throwable {
String sql="select ID,DB,COMMAND,TIME,STATE,left(INFO,100) as INFO from information_schema.`PROCESSLIST` "+
"where DB is not null AND COMMAND<>'Sleep' AND TIME>=1 and DB='ei_eqs'";
lr.start_transaction("事务开始");
ResultSet rs = stmt.executeQuery(sql);
Map<String,String> map = new HashMap();
while(rs.next()){
String id = rs.getString("ID");
String time = rs.getString("TIME");
String cmd = rs.getString("COMMAND");
String state = rs.getString("STATE");
String db = rs.getString("DB");
String info = rs.getString("INFO");
map.put(id,"耗时"+time+":"+cmd+":"+state+":"+db+":"+info);
}
// 开始杀线程
for (String id : map.keySet()) {
try {
stmt.execute("kill "+ id);
} catch (Exception e) {
lr.error_message("异常Killed:"+id+":"+map.get(id)+":"+e);
}
System.out.println("Killed:"+id+":"+map.get(id));
}
lr.error_message("Killed线程数:"+map.size());
rs.close();
lr.end_transaction("事务开始", lr.AUTO);
return 0;
}//end of action
public int end() throws Throwable {
lr.start_transaction("disconncetion");
connect.close();
lr.end_transaction("disconncetion", lr.AUTO);
return 0;
}//end of end
}