关于后端:Sentinel高级

34次阅读

共计 14319 个字符,预计需要花费 36 分钟才能阅读完成。

Sentinel 高级

sentinel 和 springCloud 整合

缩小开发的复杂度,对大部分的支流框架,例如:Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等做了适配。只须要引入对应利用的以来即可不便地整合 Sentinel。

如果要实现 SpringCloud 和 Sentinel 的整合,能够通过引入 Spring Cloud Alibaba Sentinel 来更不便得整合 Sentinel。

Spring Cloud Alibaba 是阿里巴巴团体提供的,致力于提供微服务开发的一站式解决方案。Spring Cloud Alibaba 默认为 Sentinel 整合 Servlet、RestTemplate、FeignClient 和 Spring WebFlux、Sentinel 在 Spring Cloud 生态中,不仅补全了 hystrix 在 Servlet 和 RestTemplate 这一块的空白,而且齐全兼容 hystrix 在 FeignClient 种限流降级的用法,并且反对使用时灵便地配置和调整限流降级规定。

需要

应用 SpringCloud + Sentinel 实现拜访 http://localhost:8080/ann 门路的流量管制。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {id("org.springframework.boot") version "2.3.7.RELEASE"
    id("io.spring.dependency-management") version "1.0.10.RELEASE"
    kotlin("jvm") version "1.3.72"
    kotlin("plugin.spring") version "1.3.72"
    java
}

group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

repositories {mavenCentral()
}

extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"

dependencies {implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

dependencyManagement {
    imports {mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
    }
}

tasks.withType<Test> {useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}
@SentinelResource(value = "spring_cloud_sentinel_test", blockHandler = "exceptionHandler")
    @GetMapping("ann")
    fun springCloudSentinelTest(): String {return "hello Spring-Cloud-Sentinel_test"}


    fun exceptionHandler(bx: BlockException): String {return "零碎忙碌,请稍后重试"}

Sentinel 对 Feign 的反对

Sentinel 适配了 Feign 组件,如果想应用,除了引入 spring-cloud-starter-alibaba-sentinel 的依赖外还须要 2 个步骤:

  • 配置文件关上 Sentinel 对 Feign 的反对:feign.sentinel.enabled=true
  • 退出 spring-cloud-starter-openfeign 依赖 Sentinel starter 中的自动化配置类失效

需要

实现 sentinel_feign_client 微服务通过 Feign 拜访 sentinel_feign_provider 微服务的流量管制

创立 spring-cloud-parent 父工程

  1. 依赖文件
extra["springCloudVersion"] = "Hoxton.SR9"
extra["springCloudAlibabaVersion"] = "2.2.2.RELEASE"

group = "xyz.ytfs"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8

allprojects {
    repositories {maven(url = "http://maven.aliyun.com/nexus/content/groups/public/")
        mavenCentral()
        maven {url = uri("https://repo.spring.io/snapshot") }
        maven {url = uri("https://repo.spring.io/milestone") }
    }
}

dependencies {implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

创立 eureka-server 注册核心子工程

  1. 依赖增加
dependencies {implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-server")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

dependencyManagement {
    imports {mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}
  1. 启动类和配置文件的批改
@EnableEurekaServer  // 在启动类上增加此注解,示意开启 eureka 注册核心服务
@SpringBootApplication
class EurekaServerApplication

fun main(args: Array<String>) {runApplication<EurekaServerApplication>(*args)
}
# 利用名称
spring.application.name=eureka-server
server.port=8060

#eureka 配置
eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
#不拉去服务
eureka.client.fetch-registry=false
#不注册本人
eureka.client.register-with-eureka=false

创立 sentinel-feign-client

  1. 增加依赖

    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
    implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
  1. 创立代理的个接口

    @FeignClient(value="sentinel-feign-provider", fallback = FallBackService::class)
    interface ProviderClient {@GetMapping("hello")
        fun hello(): String}
  2. 创立 controller

    @RestController
    class TestController(val providerClient: ProviderClient) {@GetMapping("hello")
        fun hello(): String{return this.providerClient.hello()
        }
    
    }
  3. 创立降级相应示例

    @Service
    /**
    * 实现代理接口
    **/
    class FallBackService : ProviderClient {override fun hello(): String {return "零碎忙碌,请稍后重试"}
    }
  4. 配置文件

    # 利用名称
    spring:
      application:
        name: sentinel-feign-client
      cloud:
        sentinel:
          transport:
            dashboard: localhost:8045
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8060/eureka
    server:
      port: 8061
      # 开启 Sentinel 对 feign 的反对
    feign:
      sentinel:
        enabled: true
  5. 启动类增加注解

    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    class SentinelFeignClientApplication
    
    fun main(args: Array<String>) {runApplication<SentinelFeignClientApplication>(*args)
    }

创立 sentinel-feign-provider

  1. 增加依赖

    dependencies {implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
        implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
        testImplementation("org.springframework.boot:spring-boot-starter-test") {exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
        }
    }
    
    dependencyManagement {
        imports {mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
            mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
        }
    }
  2. 批改配置文件

    # 利用名称
    spring.application.name=sentinel-feign-provider
    # 应用服务 WEB 拜访端口
    server.port=8062
    
    eureka.client.service-url.defaultZone=http://127.0.0.1:8060/eureka
  3. 启动类减少注解

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    class SentinelFeignProviderApplication
    
    fun main(args: Array<String>) {runApplication<SentinelFeignProviderApplication>(*args)
    }
  4. 提供接口

    @RestController
    class ProviderController {@GetMapping("hello")
        fun hello(): String {return "Hello Feign Sentintl"}
    
    }

运行测试

启动我的项目,在 Sentinel 控制台中减少对于资源流控规定.Sentinel 和 Feign 整合时,流控规定的编写模式为:http 申请形式: 协定 // 服务名称 / 申请门路跟参数 例如GET:http://sentinel-feign-provider/hello

Sentinel 对 Spring Cloud Gateway 的反对

从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,能够提供两种资源维度的限流:

  • route 维度:即在 Spring 的配置文件种配置的路由条目,资源名对应相应的 routeId
  • 自定义 API 维度:用户能够利用 Sentinel 提供的 API 来自定义一些 API 分组

微服务网关搭建

在下面根底上创立

创立子工程 sentinel-gateway,在build.gradle.kts 中引入依赖

implementation("org.springframework.cloud:spring-cloud-starter-gateway")

整合 Sentinel

  1. 导入依赖

    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")
    implementation("com.alibaba.cloud:spring-cloud-alibaba-sentinel-gateway")
  2. 创立一个配置类,配置流控降级回调操作
@Configuration
class GatewayConfiguration {


    @PostConstruct
    fun doInit() {
        GatewayCallbackManager.setBlockHandler(BlockRequestHandler {
                serverWebExchange: ServerWebExchange?, throwable: Throwable? ->
            return@BlockRequestHandler ServerResponse.status(200).bodyValue("零碎忙碌,请稍后再试!")
         })
    }

}
  1. 路由的配置

    # 配置路由
    spring.cloud.gateway.routes[0].id=sentinel-feign-gateway
    # lb 代表的是 Load Balance 负载平衡,如果是一个服务(auth-service)多个实例,实现自主散发
    spring.cloud.gateway.routes[0].uri=lb://sentinel-feign-client
    # 匹配门路
    spring.cloud.gateway.routes[0].predicates[0]=Path=/hello/**
    # 配置 Stentinel 的控制台地址
    spring.cloud.sentinel.transport.dashboard=http://localhost:8045

流量管制实现

Sentinel 的所有规定都能够在内存太中动静的查问及批改,批改之后立刻失效。同时 Sentinel 也提供相干 API,供您来定制本人的规定策略。

Sentinel 次要反对一下几种规定:

  • 流量管制规定
  • 熔断降级规定
  • 零碎爱护规定
  • 起源访问控制规定
  • 动静布局扩大

流量管制规定实现

流量管制(Flow Control),其原理是监控利用流量的 QPS 或并发线程数等指标,当达到指定的阀值时对流量进行管制,免得被刹时的流量顶峰冲垮,从而保障利用的高可用性。

流量管制次要两种形式:

  • 并发线程数:并发线程数限流用于爱护业务线程数不被耗尽
  • QPS:当 QPS 超过某个阀值的时候,则采取措施进行流量管制

一条限流规定次要由几个因素组成,咱们能够组合这些元素来实现不同的限流成果:

  • resource:资源名,即限流规定的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS 或并发线程数)
  • limitApp: 流控针对的调用起源,若为 default 则不辨别调用起源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量管制成果(间接回绝、Warm Up、匀速排队)

间接回绝

间接回绝:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)形式是默认的流量管制形式,当 QPS 超过任意规定的阈值后,新的申请就会被立刻回绝,回绝形式为抛出FlowException。这种形式实用于对系统解决能力确切已知的状况下,比方通过压测确定了零碎的精确水位时。

Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)形式,即预热 / 冷启动形式。当零碎长期处于低水位的状况下,当流量忽然减少时,间接把零碎拉升到高水位可能霎时把零碎压垮。通过 ” 冷启动 ”,让通过的流量迟缓减少,在肯定工夫内逐步减少到阈值下限,给冷零碎一个预热的工夫,防止冷零碎被压垮。

匀速排队

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)形式会严格控制申请通过的间隔时间,也即是让申请以平均的速度通过,对应的是漏桶算法。该形式的作用如下图所示:

这种形式次要用于解决间隔性突发的流量,例如音讯队列。设想一下这样的场景,在某一秒有大量的申请到来,而接下来的几秒则处于闲暇状态,咱们心愿零碎可能在接下来的闲暇期间逐步解决这些申请,而不是在第一秒间接回绝多余的申请。

留神:匀速排队模式临时不反对 QPS > 1000 的场景。

熔断降级

概述

除了流量管制以外,对调用链路中不稳固的资源进行熔断降级也是保障高可用的重要措施之一。一个服务经常会调用别的模块,可能是另外的一个近程服务、数据库,或者第三方 API 等。例如,领取的时候,可能须要近程调用银联提供的 API;查问某个商品的价格,可能须要进行数据库查问。然而,这个被依赖服务的稳定性是不能保障的。如果依赖的服务呈现了不稳固的状况,申请的响应工夫变长,那么调用服务的办法的响应工夫也会变长,线程会产生沉积,最终可能耗尽业务本身的线程池,服务自身也变得不可用。

古代微服务架构都是分布式的,由十分多的服务组成。不同服务之间互相调用,组成简单的调用链路。以上的问题在链路调用中会产生放大的成果。简单链路上的某一环不稳固,就可能会层层级联,最终导致整个链路都不可用。因而咱们须要对不稳固的 弱依赖服务调用 进行熔断降级,临时切断不稳固调用,防止部分不稳固因素导致整体的雪崩。熔断降级作为爱护本身的伎俩,通常在客户端(调用端)进行配置。

留神:本文档针对 Sentinel 1.8.0 及以上版本。1.8.0 版本对熔断降级个性进行了全新的改良降级,请应用最新版本以更好地利用熔断降级的能力。

重要的属性

Field 阐明 默认值
resource 资源名,即规定的作用对象
grade 熔断策略,反对慢调用比例 / 异样比例 / 异样数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异样比例 / 异样数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小申请数,申请数小于该值时即便异样比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式无效(1.8.0 引入)

熔断降级策略详解

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):抉择以慢调用比例作为阈值,须要设置容许的慢调用 RT(即最大的响应工夫),申请的响应工夫大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内申请数目大于设置的最小申请数目,并且慢调用的比例大于阈值,则接下来的熔断时长内申请会主动被熔断。通过熔断时长后熔断器会进入探测复原状态(HALF-OPEN 状态),若接下来的一个申请响应工夫小于设置的慢调用 RT 则完结熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异样比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内申请数目大于设置的最小申请数目,并且异样的比例大于阈值,则接下来的熔断时长内申请会主动被熔断。通过熔断时长后熔断器会进入探测复原状态(HALF-OPEN 状态),若接下来的一个申请胜利实现(没有谬误)则完结熔断,否则会再次被熔断。异样比率的阈值范畴是 [0.0, 1.0],代表 0% – 100%。
  • 异样数 (ERROR_COUNT):当单位统计时长内的异样数目超过阈值之后会主动进行熔断。通过熔断时长后熔断器会进入探测复原状态(HALF-OPEN 状态),若接下来的一个申请胜利实现(没有谬误)则完结熔断,否则会再次被熔断。

留神异样降级 仅针对业务异样,对 Sentinel 限流降级自身的异样(BlockException)不失效。为了统计异样比例或异样数,须要通过 Tracer.trace(ex) 记录业务异样。示例:

Entry entry = null;
try {entry = SphU.entry(key, EntryType.IN, key);

  // Write your biz code here.
  // <<BIZ CODE>>
} catch (Throwable t) {if (!BlockException.isBlockException(t)) {Tracer.trace(t);
  }
} finally {if (entry != null) {entry.exit();
  }
}

开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解会主动统计业务异样,无需手动调用。

熔断器事件监听

Sentinel 反对注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例:

EventObserverRegistry.getInstance().addStateChangeObserver("logging",
    (prevState, newState, rule, snapshotValue) -> {if (newState == State.OPEN) {
            // 变换至 OPEN state 时会携带触发时的值
            System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(),
                TimeUtil.currentTimeMillis(), snapshotValue));
        } else {System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(),
                TimeUtil.currentTimeMillis()));
        }
    });

代码实现

// 定义熔断资源和回调函数
@SentinelResource(value = "degrade_rule", blockHandler = "exceptionHandler")
@GetMapping("degrade")
fun ruleHello(): String {return "hello rule  sentinel"}

// 降级办法
fun exceptionHandler(e: BlockException): String {e.printStackTrace()
    return "零碎忙碌,请稍后!,降级"
}

@PostConstruct
fun initDegradeRule() {
    //1、创立寄存熔断规定的汇合
    val rules: ArrayList<DegradeRule> = ArrayList()
    //2、创立熔断规定
    val rule: DegradeRule = DegradeRule()
    // 设置熔断资源名称
    rule.resource = "degrade_rule"
    // 阀值
    rule.count = 0.01
    // 降级的工夫,单位 S
    rule.timeWindow = 10
    // 设置熔断类型
    /**
     * 当资源的均匀响应工夫超过阀值(DegradeRule 中的 count 以毫秒为单位)之后,资源进入准降级状态。* 而后继续进入 5 个申请,他们的 RT 都继续超过这个阀值,* 那么在接下来的工夫窗口(DegradeRule 中的 timeWindow,以 s 秒为单位)之内
     * 将抛出 DegradeException
     */
    rule.grade = RuleConstant.DEGRADE_GRADE_RT
    //3、将熔断规定存入汇合
    rules.add(rule)
    //4、加载熔断规定汇合
    DegradeRuleManager.loadRules(rules)
}

黑白名单管制

很多时候,咱们须要依据调用起源来判断该次申请是否容许放行,这时候能够应用 Sentinel 的起源访问控制(黑白名单管制)的性能。起源访问控制依据资源的申请起源(origin)限度资源是否通过,若配置白名单则只有申请起源位于白名单内时才可通过;若配置黑名单则申请起源位于黑名单时不通过,其余的申请通过。

调用方信息通过 ContextUtil.enter(resourceName, origin) 办法中的 origin 参数传入。

规定配置

起源访问控制规定(AuthorityRule)非常简单,次要有以下配置项:

  • resource:资源名,即限流规定的作用对象。
  • limitApp:对应的黑名单 / 白名单,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限度模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。

示例

比方咱们心愿管制对资源 test 的拜访设置白名单,只有起源为 appAappB 的申请才可通过,则能够配置如下白名单规定:

AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

动静规定

规定

Sentinel 的理念是开发者只须要关注资源的定义,当资源定义胜利后能够动静减少各种流控降级规定。Sentinel 提供两种形式批改规定:

  • 通过 API 间接批改 (loadRules)
  • 通过 DataSource 适配不同数据源批改

手动通过 API 批改比拟直观,能够通过以下几个 API 批改不同的规定:

FlowRuleManager.loadRules(List<FlowRule> rules); // 批改流控规定
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 批改降级规定

手动批改规定(硬编码方式)个别仅用于测试和演示,生产上个别通过动静规定源的形式来动静治理规定。

DataSource 扩大

上述 loadRules() 办法只承受内存态的规定对象,但更多时候规定存储在文件、数据库或者配置核心当中。DataSource 接口给咱们提供了对接任意配置源的能力。相比间接通过 API 批改规定,实现 DataSource 接口是更加牢靠的做法。

咱们举荐 通过控制台设置规定后将规定推送到对立的规定核心,客户端实现 ReadableDataSource 接口端监听规定核心实时获取变更,流程如下:

DataSource 扩大常见的实现形式有:

  • 拉模式:客户端被动向某个规定管理中心定期轮询拉取规定,这个规定核心能够是 RDBMS、文件,甚至是 VCS 等。这样做的形式是简略,毛病是无奈及时获取变更;
  • 推模式:规定核心对立推送,客户端通过注册监听器的形式时刻监听变动,比方应用 Nacos、Zookeeper 等配置核心。这种形式有更好的实时性和一致性保障。

Sentinel 目前反对以下数据源扩大:

  • Pull-based: 动静文件数据源、Consul, Eureka
  • Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd

示例

1、启动本地的 nacos

nacos 下载地址

启动文件在 `nacos/bin 目录上面

startup.cmd -m standalone : 代表单机启动的意思

2、向 nacos 中增加限度规定

/**
 * 向 nacos 中发送配置
 */
fun send() {
    val remoteAddress = "localhost"
    val groupId = "Sentinel:Demo"
    val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"
    val rule = """[
                  {
                    "resource": "TestResource",
                    "controlBehavior": 0,
                    "count": 5.0,
                    "grade": 1,
                    "limitApp": "default",
                    "strategy": 0
                  }
                ]"""
    val configService = NacosFactory.createConfigService(remoteAddress)
    println(configService.publishConfig(dataId, groupId, rule))
}

3、从 nacos 中读取配置规定

// remoteAddress 代表 Nacos 服务端的地址
val remoteAddress = "127.0.0.1"

// groupId 和 dataId 对应 Nacos 中相应配置
val groupId = "Sentinel:Demo"

val dataId = "com.alibaba.csp.sentinel.demo.flow.rule"

/**
 * 加载规定
 */
fun loadRules() {
    val flowRuleDataSource: NacosDataSource<List<FlowRule?>> = NacosDataSource<List<FlowRule?>>(remoteAddress, groupId, dataId) { source: String? ->
        JSON.parseObject<List<FlowRule?>>(
            source,
            object : TypeReference<List<FlowRule?>?>() {})
    }
    FlowRuleManager.register2Property(flowRuleDataSource.property)
}

正文完
 0