golang 熔断限流降级

embedded/2024/12/29 8:15:30/

限流 - 2k 但是我的服务能力只有1k,所以这个时候多出来的流量怎么办: 1. 拒绝 2. 排队等待。用户体验不太好: 当前访问用户过多,请稍后重试和你的服务直接挂了
用户体验降级了 - 原本是访问流畅,下单流畅 -> 当前访问用户过多,请稍后重试
熔断 - 比如A服务访问B服务,这个时候B服务很慢 - B服务压力过大,导致了出现了不少请求错误,调用方很容易出现一个问题: 每次调用都超时 2k,结果这个时候数据库出现了问题, 超时重试 - 网络 2k的流量突然变成了3k。这让原本就满负荷的b服务雪上加霜,如果这个时候调用方有一种机制:比如说 1. 发现了大部分请求很慢 - 50%请求都很慢, 2. 发现我的请求有50%都错误了 3. 错误数量很多,比如1s出现了20个错误 。上述三种情况出现,在一段时间内不发给B服务,直接拒绝。一段时间之后再发送。以此来缓解B服务的压力,避免B服务挂掉的可能。
在这里插入图片描述

技术选型

sentinel官方文档
在这里插入图片描述

限流

QPS限流

package mainimport ("fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)const resName = "example-flow-qps-resource"func main() {//初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatal(err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject,Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}ch := make(chan struct{})for i := 0; i < 10; i++ {go func() {for {e, b := sentinel.Entry(resName, sentinel.WithTrafficType(base.Inbound))if b != nil {// Blocked. We could get the block reason from the BlockError.fmt.Println("被限流")time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)} else {// Passed, wrap the logic here.fmt.Println("成功")time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)// Be sure the entry is exited finally.e.Exit()}}}()}// Simulate a scenario in which flow rules are updated concurrentlygo func() {time.Sleep(time.Second * 10)_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject,Threshold:              80,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}}()<-ch
}

warmup限流

WarmUp 方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。这块设计和 Java 类似。通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
在这里插入图片描述

package mainimport ("fmt""log""math/rand""sync""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)const resName = "example-flow-qps-resource"func main() {//初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatal(err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.WarmUp, //冷启动策略ControlBehavior:        flow.Reject, //直接拒绝Threshold:              10,          //1s10个并发WarmUpPeriodSec:        30,          //60s预热},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}var mutex sync.MutexlastSecond := time.Now().Second()total := 0totalRestrict := 0totalPass := 0ch := make(chan struct{})for i := 0; i < 10; i++ {go func() {for {e, b := sentinel.Entry(resName, sentinel.WithTrafficType(base.Inbound))if b != nil {// Blocked. We could get the block reason from the BlockError.mutex.Lock()totalRestrict++mutex.Unlock()time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)} else {// Passed, wrap the logic here.mutex.Lock()totalPass++mutex.Unlock()time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)// Be sure the entry is exited finally.e.Exit()}mutex.Lock()total++mutex.Unlock()if time.Now().Second() != lastSecond {mutex.Lock()lastSecond = time.Now().Second()fmt.Println("total=", total, ",totalRestrict=", totalRestrict, ",totalPass=", totalPass)total = 0totalRestrict = 0totalPass = 0mutex.Unlock()}}}()}<-ch
}

趋势逐渐增加,一直到达每秒10个左右
在这里插入图片描述

Throttling策略

计算时间间隔,每过了时间间隔才允许通过

package mainimport ("fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)const resName = "example-flow-qps-resource"func main() {//初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatal(err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Throttling,Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}ch := make(chan struct{})for i := 0; i < 10; i++ {go func() {for {e, b := sentinel.Entry(resName, sentinel.WithTrafficType(base.Inbound))if b != nil {// Blocked. We could get the block reason from the BlockError.fmt.Println("被限流")time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)} else {// Passed, wrap the logic here.fmt.Println("成功")time.Sleep(time.Duration(rand.Uint64()%10) * time.Millisecond)// Be sure the entry is exited finally.e.Exit()}}}()}// Simulate a scenario in which flow rules are updated concurrentlygo func() {time.Sleep(time.Second * 10)_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject,Threshold:              80,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}}()<-ch
}

在这里插入图片描述

package mainimport ("fmt""log""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/base""github.com/alibaba/sentinel-golang/core/flow"
)const resName = "example-flow-qps-resource"func main() {//初始化sentinelerr := sentinel.InitDefault()if err != nil {log.Fatal(err)}//配置限流规则_, err = flow.LoadRules([]*flow.Rule{{Resource:               resName,TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Throttling,Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {log.Fatalf("Unexpected error: %+v", err)return}for i := 0; i < 10; i++ {e, b := sentinel.Entry(resName, sentinel.WithTrafficType(base.Inbound))if b != nil {// Blocked. We could get the block reason from the BlockError.fmt.Println("被限流")} else {// Passed, wrap the logic here.fmt.Println("成功")// Be sure the entry is exited finally.e.Exit()}time.Sleep(100 * time.Millisecond)}
}

在这里插入图片描述

熔断

Sentinel 熔断降级基于熔断器模式 (circuit breaker pattern) 实现。熔断器内部维护了一个熔断器的状态机,状态机的转换关系如下图所示:
在这里插入图片描述
熔断器有三种状态:
1.Closed 状态:也是初始状态,该状态下,熔断器会保持闭合,对资源的访问直接通过熔断器的检查。
2.Open 状态:断开状态,熔断器处于开启状态,对资源的访问会被切断。
3.Half-Open 状态:半开状态,该状态下除了探测流量,其余对资源的访问也会被切断。探测流量指熔断器处于半开状态时,会周期性的允许一定数目的探测请求通过,如果探测请求能够正常的返回,代表探测成功,此时熔断器会重置状态到 Closed 状态,结束熔断;如果探测失败,则回滚到 Open 状态。
这三种状态之间的转换关系这里做一个更加清晰的解释:
1.初始状态下,熔断器处于 Closed 状态。如果基于熔断器的统计数据表明当前资源触发了设定的阈值,那么熔断器会切换状态到 Open 状态;
2.Open 状态即代表熔断状态,所有请求都会直接被拒绝。熔断器规则中会配置一个熔断超时重试的时间,经过熔断超时重试时长后熔断器会将状态置为 Half-Open 状态,从而进行探测机制;
3.处于 Half-Open 状态的熔断器会周期性去做探测。
Sentinel 提供了监听器去监听熔断器状态机的三种状态的转换,方便用户去自定义扩展:

熔断策略

Sentinel 熔断器的三种熔断策略都支持静默期 (规则中通过MinRequestAmount字段表示)。静默期是指一个最小的静默请求数,在一个统计周期内,如果对资源的请求数小于设置的静默数,那么熔断器将不会基于其统计值去更改熔断器的状态。静默期的设计理由也很简单,举个例子,假设在一个统计周期刚刚开始时候,第 1 个请求碰巧是个慢请求,这个时候这个时候的慢调用比例就会是 100%,很明显是不合理,所以存在一定的巧合性。所以静默期提高了熔断器的精准性以及降低误判可能性。
Sentinel 支持以下几种熔断策略:

慢调用比例策略 (SlowRequestRatio)

Sentinel 的熔断器不在静默期,并且慢调用的比例大于设置的阈值,则接下来的熔断周期内对资源的访问会自动地被熔断。该策略下需要设置允许的调用 RT 临界值(即最大的响应时间),对该资源访问的响应时间大于该阈值则统计为慢调用。

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct {
}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %.2f, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=5s, recoveryTimeout=3s, slowRtUpperBound=50ms, maxSlowRequestRatio=50%{Resource:                     "abc",Strategy:                     circuitbreaker.SlowRequestRatio,RetryTimeoutMs:               3000,MinRequestAmount:             10,StatIntervalMs:               5000,StatSlidingWindowBucketCount: 10,MaxAllowedRtMs:               50, //大于50ms是慢查询Threshold:                    0.5,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker SlowRtRatio] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtime.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {if rand.Uint64()%20 > 9 {// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}// g1 passedtime.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)e.Exit()}}}()go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtime.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtime.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)e.Exit()}}}()<-ch
}

错误比例策略 (ErrorRatio)

Sentinel 的熔断器不在静默期,并且在统计周期内资源请求访问异常的比例大于设定的阈值,则接下来的熔断周期内对资源的访问会自动地被熔断。

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct{}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %.2f, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {total := 0totalPass := 0totalBlock := 0totalErr := 0conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=5s, recoveryTimeout=3s, maxErrorCount=50{Resource:                     "abc",Strategy:                     circuitbreaker.ErrorRatio,RetryTimeoutMs:               3000,MinRequestAmount:             10,StatIntervalMs:               5000,StatSlidingWindowBucketCount: 10,Threshold:                    0.4,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker ErrorCount] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtotalBlock++fmt.Println("熔断")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {if rand.Uint64()%20 > 9 {totalErr++// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}totalPass++// g1 passedtime.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)e.Exit()}}}()go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtotalBlock++fmt.Println("写成熔断了")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtotalPass++time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)e.Exit()}}}()go func() {for {time.Sleep(time.Second)fmt.Println(float64(totalErr) / float64(total))}}()<-ch
}

错误计数策略 (ErrorCount)

Sentinel 的熔断器不在静默期,并且在统计周期内资源请求访问异常数大于设定的阈值,则接下来的熔断周期内对资源的访问会自动地被熔断。

注意:这里的错误比例熔断和错误计数熔断指的业务返回错误的比例或则计数。也就是说,如果规则指定熔断器策略采用错误比例或则错误计数,那么为了统计错误比例或错误计数,需要调用API: api.TraceError(entry, err) 埋点每个请求的业务异常。

package mainimport ("errors""fmt""log""math/rand""time"sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/circuitbreaker""github.com/alibaba/sentinel-golang/core/config""github.com/alibaba/sentinel-golang/logging""github.com/alibaba/sentinel-golang/util"
)type stateChangeTestListener struct {
}func (s *stateChangeTestListener) OnTransformToClosed(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Closed, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToOpen(prev circuitbreaker.State, rule circuitbreaker.Rule, snapshot interface{}) {fmt.Printf("rule.steategy: %+v, From %s to Open, snapshot: %d, time: %d\n", rule.Strategy, prev.String(), snapshot, util.CurrentTimeMillis())
}func (s *stateChangeTestListener) OnTransformToHalfOpen(prev circuitbreaker.State, rule circuitbreaker.Rule) {fmt.Printf("rule.steategy: %+v, From %s to Half-Open, time: %d\n", rule.Strategy, prev.String(), util.CurrentTimeMillis())
}func main() {total := 0totalPass := 0totalBlock := 0totalErr := 0conf := config.NewDefaultConfig()// for testing, logging output to consoleconf.Sentinel.Log.Logger = logging.NewConsoleLogger()err := sentinel.InitWithConfig(conf)if err != nil {log.Fatal(err)}ch := make(chan struct{})// Register a state change listener so that we could observer the state change of the internal circuit breaker.circuitbreaker.RegisterStateChangeListeners(&stateChangeTestListener{})_, err = circuitbreaker.LoadRules([]*circuitbreaker.Rule{// Statistic time span=5s, recoveryTimeout=3s, maxErrorCount=50{Resource:                     "abc",Strategy:                     circuitbreaker.ErrorCount,RetryTimeoutMs:               3000,MinRequestAmount:             10,StatIntervalMs:               5000,StatSlidingWindowBucketCount: 10,Threshold:                    50,},})if err != nil {log.Fatal(err)}logging.Info("[CircuitBreaker ErrorCount] Sentinel Go circuit breaking demo is running. You may see the pass/block metric in the metric log.")go func() {for {total++e, b := sentinel.Entry("abc")if b != nil {// g1 blockedtotalBlock++fmt.Println("熔断")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {if rand.Uint64()%20 > 9 {totalErr++// Record current invocation as error.sentinel.TraceError(e, errors.New("biz error"))}totalPass++// g1 passedtime.Sleep(time.Duration(rand.Uint64()%80+10) * time.Millisecond)e.Exit()}}}()go func() {for {e, b := sentinel.Entry("abc")if b != nil {// g2 blockedtotalBlock++fmt.Println("写成熔断了")time.Sleep(time.Duration(rand.Uint64()%20) * time.Millisecond)} else {// g2 passedtotalPass++time.Sleep(time.Duration(rand.Uint64()%80) * time.Millisecond)e.Exit()}}}()go func() {for {time.Sleep(time.Second)fmt.Println(totalErr)}}()<-ch
}

gin集成sentinel限流

初始化

package initializeimport (sentinel "github.com/alibaba/sentinel-golang/api""github.com/alibaba/sentinel-golang/core/flow""go.uber.org/zap"
)func InitSentinel() {//初始化sentinelerr := sentinel.InitDefault()if err != nil {zap.S().Fatal(err)}//配置限流规则//这种配置从nacos读取_, err = flow.LoadRules([]*flow.Rule{{Resource:               "test",TokenCalculateStrategy: flow.Direct,ControlBehavior:        flow.Reject,Threshold:              10,StatIntervalInMs:       1000,},})if err != nil {zap.S().Fatalf("Unexpected error: %+v", err)return}}

调用

	initialize.InitSentinel()

限流

e, b := sentinel.Entry("goods-list", sentinel.WithTrafficType(base.Inbound))if b != nil {ctx.JSON(http.StatusTooManyRequests, gin.H{"msg": "请求过于频繁,请稍后重试",})return}r, err := global.GoodsSrvClient.GoodsList(context.WithValue(context.Background(), "ginContext", ctx), request)if err != nil {zap.S().Errorw("[List] 查询 【商品列表】失败")return}e.Exit()

http://www.ppmy.cn/embedded/149687.html

相关文章

PHP7内核剖析 学习笔记 第四章 内存管理(2)

4.4 线程安全 单线程环境中&#xff0c;我们经常使用全局变量实现多个函数间共享数据&#xff0c;声明在函数之外的变量为全局变量&#xff0c;全局变量为各线程共享&#xff0c;不同的线程引用同一地址空间&#xff0c;如果一个线程修改了全局变量就会影响所有线程。线程安全…

filament的材质系统

filament是Google开源的一个跨平台实时pbr渲染引擎。注意&#xff0c;这是一个渲染引擎&#xff0c;不是一个完整的游戏引擎。 filament的材质系统文档&#xff1a;Filament Materials Guide&#xff0c;pbr算法文档&#xff1a;Physically Based Rendering in Filament。这些文…

【漏洞复现】CVE-2015-5531 Arbitrary File Reading

漏洞信息 NVD - CVE-2015-5531 Directory traversal vulnerability in Elasticsearch before 1.6.1 allows remote attackers to read arbitrary files via unspecified vectors related to snapshot API calls. 背景介绍 Elasticsearch is an open source distributed, RE…

在 Linux 中如何使用粘滞位 (t-bit)共享文件

在 Linux 系统中&#xff0c;共享文件是日常管理和协作中的常见任务&#xff0c;而粘滞位&#xff08;Sticky Bit 或 t-bit&#xff09;是实现共享目录安全性的重要工具之一。本文将带您详细了解如何在 Linux 中共享文件并配置粘滞位来保护共享资源的安全。 文件共享的常见场景…

开发场景中Java 集合的最佳选择

在 Java 开发中&#xff0c;集合类是处理数据的核心工具。合理选择集合&#xff0c;不仅可以提高代码效率&#xff0c;还能让代码更简洁。本篇文章将重点探讨 List、Set 和 Map 的适用场景及优缺点&#xff0c;帮助你在实际开发中找到最佳解决方案。 一、List&#xff1a;有序存…

LeetCode 热题 100_二叉树的最大深度(37_104_简单_C++)(二叉树;递归;层次遍历)

LeetCode 热题 100_二叉树的最大深度&#xff08;37_104&#xff09; 题目描述&#xff1a;输入输出样例&#xff1a;题解&#xff1a;解题思路&#xff1a;思路一&#xff08;递归&#xff08;深度优先&#xff09;&#xff09;&#xff1a;思路二&#xff08;层次遍历&#x…

ArkTs组件(2)

一.下拉列表组件&#xff1a;Select 1.接口 Select(options: Array<SelectOption>) 参数名类型必填说明optionsArray<SelectOption>是设置下拉选项。 SelectOption对象说明 名称类型必填说明valueResourceStr是 下拉选项内容。 iconResourceStr否 下拉选项图片…

关于高级acl的配置和讲解

高级ACL&#xff08;Access Control List&#xff0c;高级访问控制列表&#xff09;是网络设备&#xff08;如路由器或交换机&#xff09;用来过滤和控制流量的一种机制。它比标准ACL更灵活&#xff0c;能够根据更多的条件来决定是否允许或拒绝数据包通过。高级ACL&#xff08;…