一、什么是服务熔断降级,为啥需要它
很多做线上业务的人都遇过这种情况:某个核心服务突然挂了,比如电商的支付服务、外卖的订单服务,结果整个系统跟着瘫痪,用户点啥都卡,甚至直接打不开。服务熔断降级就是专门解决这种问题的“系统安全门”。
先简单说清楚两个概念:熔断就像家里的漏电保护器,一旦检测到电路出问题(比如短路),立刻断电,避免整个房子烧起来;降级就是“退而求其次”,比如支付服务挂了,先给用户显示“稍后再试”,而不是让整个页面卡成白屏。
为啥要用它?举个实际的例子:假设一个电商系统,有商品列表、购物车、支付三个服务,其中支付服务是最容易出问题的。如果没有熔断降级,一旦支付服务超时10秒还没返回,用户点支付就会一直等,浏览器的请求会越堆越多,把系统的内存、网络资源全占满,最后商品列表、购物车也用不了,整个系统全崩。有了熔断降级,支付服务出问题时,系统会立刻“关断”对支付服务的请求,同时给用户返回友好提示,其他服务还能正常用,避免了“一个服务挂,整个系统瘫”的灾难。
二、OpenResty是什么,为啥适合做熔断降级
可能有人会问,为啥选OpenResty来做这个?先搞懂OpenResty到底是什么:它其实是把Nginx(一个大家常用的高性能服务器)和Lua(一种轻量的脚本语言)打包到一起的工具包。简单说,它既有Nginx的快(每秒能处理几十万次请求),又能通过Lua写自定义的逻辑,比如判断请求状态、做复杂的业务判断。
那它为啥适合做熔断降级?有两个核心原因: 第一,它能“挡在所有请求前面”。正常的业务请求都是先经过OpenResty,再转发到后面的业务服务,所以它能第一时间检测到业务服务的异常,不用等后面的业务代码处理,反应特别快。 第二,它性能足够好。熔断降级需要实时统计请求的成功、失败、超时情况,OpenResty的Nginx内核处理这种统计几乎不耗资源,不会给系统增加额外负担。
这里补充一个小知识点:OpenResty里的Lua是运行在Nginx的worker进程里的,每个worker是独立的,所以统计数据要存在共享内存里,不然每个worker的数据会不一样,导致逻辑混乱。后面的示例会用到这一点。
三、用OpenResty实现服务熔断降级的完整示例
接下来给大家一个完整的示例,所有代码都能直接跑,只要装了OpenResty就能用。首先明确技术栈:OpenResty 1.21.4.1(基于Nginx 1.21.4)+ Lua 5.1。
3.1 前期准备:安装OpenResty
先教大家怎么装,Windows、Mac、Linux都能装,以Linux(CentOS 7)为例,执行下面的命令:
# 安装yum源
yum install -y yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
# 安装OpenResty
yum install -y openresty
# 安装完成后,OpenResty的配置文件在 /usr/local/openresty/nginx/conf/nginx.conf
3.2 核心逻辑实现:熔断降级的代码
整个逻辑分三部分:统计请求状态、判断是否触发熔断、熔断后的降级处理。我们先写Lua的核心逻辑,再写Nginx的配置。
首先是Lua的核心代码,存为 /usr/local/openresty/nginx/conf/fuse.lua,代码里加了详细注释:
-- 1. 定义共享内存:用来存所有服务的熔断状态、统计数据
-- 第一个参数是共享内存的名字,第二个是大小(这里给10MB,足够用)
local shared_dict = ngx.shared.service_fuse
-- 2. 配置项:可以根据业务调整
local CONFIG = {
FAIL_THRESHOLD = 5, -- 触发熔断的失败次数阈值:10秒内失败5次就熔断
RECOVERY_TIME = 10, -- 熔断后恢复的时间:熔断10秒后,进入半开状态,尝试放少量请求
SUCCESS_THRESHOLD = 2 -- 半开状态下,连续成功2次就恢复正常
}
-- 3. 核心函数:检查某个服务是否需要熔断
local function check_fuse(service_name)
-- 先获取服务的熔断状态
local is_fused = shared_dict:get(service_name .. "_is_fused")
-- 如果已经熔断,检查是否到了恢复时间
if is_fused then
local fuse_start_time = shared_dict:get(service_name .. "_fuse_start_time")
-- 计算当前时间和熔断开始时间的差
local now = ngx.time()
if now - fuse_start_time >= CONFIG.RECOVERY_TIME then
-- 到了恢复时间,把状态改成半开:允许1个请求进来测试
shared_dict:set(service_name .. "_is_fused", false)
shared_dict:set(service_name .. "_is_half_open", true)
return false -- 半开状态下,允许请求通过,用来测试
else
-- 还在熔断时间内,直接拦截
return true
end
end
-- 统计10秒内的失败次数:失败次数=总请求数-成功次数
local total = shared_dict:get(service_name .. "_total") or 0
local success = shared_dict:get(service_name .. "_success") or 0
local fail = total - success
-- 如果失败次数达到阈值,触发熔断
if fail >= CONFIG.FAIL_THRESHOLD then
-- 记录熔断状态和开始时间
shared_dict:set(service_name .. "_is_fused", true)
shared_dict:set(service_name .. "_fuse_start_time", ngx.time())
-- 重置统计数据,方便下次统计
shared_dict:set(service_name .. "_total", 0)
shared_dict:set(service_name .. "_success", 0)
return true -- 触发熔断,拦截请求
end
-- 没达到阈值,允许请求通过
return false
end
-- 4. 核心函数:记录请求结果(成功/失败)
local function record_result(service_name, is_success)
-- 增加总请求数
local total = shared_dict:get(service_name .. "_total") or 0
shared_dict:set(service_name .. "_total", total + 1)
-- 如果成功,增加成功次数
if is_success then
local success = shared_dict:get(service_name .. "_success") or 0
shared_dict:set(service_name .. "_success", success + 1)
-- 半开状态下,检查连续成功次数是否达到恢复阈值
local is_half_open = shared_dict:get(service_name .. "_is_half_open")
if is_half_open then
local half_success = shared_dict:get(service_name .. "_half_success") or 0
half_success = half_success + 1
shared_dict:set(service_name .. "_half_success", half_success)
-- 连续成功2次,恢复正常状态
if half_success >= CONFIG.SUCCESS_THRESHOLD then
shared_dict:set(service_name .. "_is_half_open", false)
shared_dict:set(service_name .. "_half_success", 0)
-- 重置统计数据,开始新的周期
shared_dict:set(service_name .. "_total", 0)
shared_dict:set(service_name .. "_success", 0)
end
end
end
end
-- 5. 对外暴露的接口:给Nginx调用
-- 第一个参数是服务名(用来区分不同的服务,比如"pay"代表支付服务)
-- 第二个参数是请求结果(true=成功,false=失败)
return {
check_fuse = check_fuse,
record_result = record_result
}
接下来是Nginx的配置,修改 /usr/local/openresty/nginx/conf/nginx.conf,核心部分如下:
# 全局配置:定义共享内存,所有worker进程都能访问
http {
# 定义共享内存:名字是service_fuse,大小10MB
lua_shared_dict service_fuse 10m;
# 引入我们写的Lua逻辑
init_by_lua_block {
local fuse = require "fuse"
ngx.shared.fuse_module = fuse -- 把fuse模块存在全局,方便后面调用
}
# 上游服务:模拟支付服务,假设支付服务的地址是127.0.0.1:8080
upstream pay_service {
server 127.0.0.1:8080;
}
# 定义支付服务的location
location /pay {
# 第一步:检查是否需要熔断
set $service_name "pay";
set $is_fused "0";
access_by_lua_block {
local fuse = ngx.shared.fuse_module
local is_fused = fuse.check_fuse(ngx.var.service_name)
if is_fused then
-- 熔断状态:直接返回降级结果,不转发请求到支付服务
ngx.status = 503 -- 503是服务不可用的状态码
ngx.say("支付服务暂时不可用,请稍后再试")
return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
}
# 第二步:转发请求到上游的支付服务
proxy_pass http://pay_service;
# 第三步:记录请求结果
body_filter_by_lua_block {
local fuse = ngx.shared.fuse_module
local service_name = ngx.var.service_name
-- 判断请求是否成功:状态码200-399是成功,其他是失败
local is_success = ngx.status >= 200 and ngx.status < 400
fuse.record_result(service_name, is_success)
}
}
# 其他服务的location可以按同样的逻辑写,比如商品列表、购物车
# 这里只演示支付服务,其他服务的逻辑完全一致
}
3.3 测试示例:验证熔断降级的效果
为了测试,我们先模拟一个异常的支付服务,写一个简单的Python脚本(技术栈和OpenResty无关,只是用来模拟异常服务),存为 pay.py:
from flask import Flask, request
app = Flask(__name__)
# 模拟支付服务:前3次请求返回成功,后面的请求随机返回失败(用来模拟服务异常)
import random
count = 0
@app.route('/pay', methods=['POST'])
def pay():
global count
count += 1
if count <= 3:
return "支付成功", 200
else:
# 80%的概率返回失败,20%的概率返回成功
if random.random() < 0.8:
return "支付服务异常", 500
else:
return "支付成功", 200
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080)
测试步骤:
- 先启动Python的模拟支付服务:
python pay.py - 启动OpenResty:
/usr/local/openresty/nginx/sbin/nginx - 用curl测试请求:
# 连续发10次请求
for i in {1..10}; do
echo "第$i次请求:"
curl -X POST http://localhost/pay
echo -e "\n"
done
测试结果会是这样的:前3次请求返回“支付成功”,第4到第7次请求会返回“支付服务异常”(因为我们的阈值是10秒内失败5次),第8到第12次请求会直接返回“支付服务暂时不可用”(触发了熔断),10秒后会进入半开状态,允许少量请求通过,连续成功2次后恢复正常。
四、应用场景、优缺点、注意事项
4.1 应用场景
服务熔断降级不是所有场景都要用,适合以下情况:
- 核心依赖服务不稳定:比如电商的支付、物流,外卖的订单、派单,这些服务一旦挂了,整个业务会受影响。
- 系统资源有限:比如中小公司的服务器配置不高,一旦某个服务出问题,很容易占满资源。
- 高并发场景:比如大促、秒杀,请求量突然暴增,某个服务扛不住时,熔断降级能避免整个系统崩溃。
4.2 技术优缺点
优点:
- 反应快:OpenResty直接在请求入口做判断,能在毫秒级触发熔断,比业务层的熔断逻辑快很多。
- 性能高:Nginx内核处理请求的能力是百万级的,不会因为加了熔断逻辑而变慢。
- 不侵入业务:业务代码不用改,只要在OpenResty里配置好就行,对业务开发透明。
缺点:
- 配置复杂:如果有很多服务,每个服务的熔断阈值、恢复时间都不一样,配置起来会比较麻烦。
- 统计粒度粗:OpenResty的共享内存统计是基于服务的,如果一个服务有多个接口,没法针对单个接口做熔断。
- 不支持复杂的业务逻辑:比如要根据用户的等级、请求的参数做不同的熔断策略,OpenResty的Lua逻辑会变得很复杂。
4.3 注意事项
- 阈值设置要合理:如果失败阈值设得太低,比如设成2,可能会因为偶尔的网络波动触发熔断;设得太高,比如设成20,又起不到保护作用。最好先做压力测试,根据实际情况调整。
- 恢复时间要合适:如果恢复时间设得太短,比如设成2秒,可能服务还没恢复,又开始接收请求,导致再次熔断;设得太长,比如设成1分钟,用户体验不好。
- 降级内容要友好:熔断后给用户返回的内容不能是乱码或者500错误,要像示例里那样,返回“支付服务暂时不可用,请稍后再试”这种友好的提示。
- 要加监控:最好能实时看到哪些服务触发了熔断,熔断的次数、时间,方便后续优化。
五、文章总结
服务熔断降级是保障线上系统稳定的重要手段,OpenResty因为其高性能、能拦截所有请求的特点,非常适合做这个功能。本文从概念讲起,一步步实现了完整的熔断降级逻辑,包括统计请求、触发熔断、降级处理,还给出了测试方法。
需要注意的是,OpenResty的熔断降级适合做全局的、粗粒度的服务保护,如果需要更细粒度的控制,比如针对单个接口、单个用户的熔断,可以结合业务层的熔断逻辑(比如Hystrix、Sentinel)一起用。总的来说,只要合理配置,OpenResty的熔断降级能有效避免“一个服务挂,整个系统瘫”的问题,提升系统的稳定性。
评论
围绕“基于OpenResty的服务熔断降级:实现业务故障时的系统自我保护机制”参与讨论