限流的基本认识
场景分析
一个互联网产品,打算搞一次大促来增加销量以及曝光。公司的架构师基于往期的流量情况做了一个活动流量的预估,然后整个公司的各个技术团队开始按照这个目标进行设计和优化,最终在大家不懈的努力之下,达到了链路压测的目标流量峰值。到了活动开始那天,大家都在盯着监控面板,看着流量像洪水一样涌进来。由于前期的宣传工作做得很好,使得这个流量远远超过预估的峰值,后端服务开始不稳定,CPU、内存各种爆表。部分服务开始出现无响应的情况。最后,整个系统开始崩溃,用户无法正常访问服务。最后导致公司巨大的损失
引入限流
在10.1黄金周,各大旅游景点都是人满为患。所有有些景点为了避免出现踩踏事故,会采取限流措施。那在架构场景中,是不是也能这么做呢?针对这个场景,能不能够设置一个最大的流量限制,如果超过这个流量,我们就拒绝提供服务,从而使得我们的服务不会挂掉。
当然,限流虽然能够保护系统不被压垮,但是对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。但是相比于全部不可用,有损服务是最好的一种解决办法
限流的作用
除了前面说的限流使用场景之外,限流的设计还能防止恶意请求流量、恶意攻击
所以,限流的基本原理是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或者告知资源没有了)、排队或等待(秒杀、下单)、降级(返回兜底数据或默认数据或默认数据,如商品详情页库存默认有货)
一般互联网企业常见的限流有:限制总并发数(如数据库连接池、线程池)、限制瞬时并发数(nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的
RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他的还有限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
有了限流,就意味着在处理高并发的时候多了一种保护机制,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;但是限流需要评估好,不能乱用,否则一些正常流量出现一些奇怪的问题而导致用户体验很差造成用户流失。
常见的限流算法
滑动窗口
发送和接受方都会维护一个数据帧的序列,这个序列被称作窗口。发送方的窗口大小由接受方确定,目的在于控制发送速度,以免接受方的缓存不够大,而导致溢出,同时控制流量也可以避免网络拥塞。下面图中的4,5,6号数据帧已经被发送出去,但是未收到关联的ACK,7,8,9帧则是等待发送。可以看出发送端的窗口大小为6,这是由接受端告知的。此时如果发送端收到4号ACK,则窗口的左边缘向右收缩,窗口的右边缘则向右扩展,此时窗口就向前“滑动了”,即数据帧10也可以被发送。
image.png漏桶(控制传输速率Leaky bucket)
漏桶算法思路是,不断的往桶里面注水,无论注水的速度是大还是小,水都是按固定的速率往外漏水;
如果桶满了,水会溢出;
桶本身具有一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水可以加入。一旦水满,上方的水就无法加入。桶满正是算法中的一个关键的触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方流下来的水,有两种方式
在桶满水之后,常见的两种处理方式为:
1.暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。
2.溢出的上方水直接抛弃。
特点
- 漏水的速率是固定的
-
即使存在突然注水量变大的情况,漏水的速率也是固定的
image.png
令牌桶(能够解决突发流量)
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。
典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
令牌桶是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌; 令牌桶算法实际上由三部分组成:两个流和一个桶,分别是令牌流、数据流和令牌桶
令牌流与令牌桶
系统会以一定的速度生成令牌,并将其放置到令牌桶中,可以将令牌桶想象成一个缓冲区(可以用队列这种数据结构来实现),当缓冲区填满的时候,新生成的令牌会被扔掉。这里有两个变量很重要:
第一个是生成令牌的速度,一般称为 rate 。比如,我们设定 rate = 2 ,即每秒钟生成 2 个令牌,也就是每 1/2 秒生成一个令牌;
第二个是令牌桶的大小,一般称为 burst 。比如,我们设定 burst = 10 ,即令牌桶最大只能容纳 10 个令牌。
有以下三种情形可能发生:
- 1.数据流的速率 等于 令牌流的速率。这种情况下,每个到来的数据包或者请求都能对应一个令牌,然后无延迟地通过队列;
- 2.数据流的速率 小于 令牌流的速率。通过队列的数据包或者请求只消耗了一部分令牌,剩下的令牌会在令牌桶里积累下来,直到桶被装满。剩下的令牌可以在突发请求的时候消耗掉。
- 3.数据流的速率 大于 令牌流的速率。这意味着桶里的令牌很快就会被耗尽。导致服务中断一段时
间,如果数据包或者请求持续到来,将发生丢包或者拒绝响应。
Sentinel整合Dubbo
image.png添加jar依赖
<dependency>
<artifactId>sentinel-api</artifactId>
<groupId>com.wei.sentinel</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
SentinelService
public interface SentinelService {
String sayHello(String name);
}
SentinelServiceImpl
@Service
public class SentinelServiceImpl implements SentinelService{
@Override
public String sayHello(String name) {
System.out.println("begin execute sayHello:"+name);
return "Hello World:"+name+"->timer:"+ LocalDateTime.now();
}
}
ProviderConfig
@Configuration
@DubboComponentScan("com.wei.sentinel")
public class ProviderConfig {
@Bean
public ApplicationConfig applicationConfig(){
ApplicationConfig config=new ApplicationConfig();
config.setName("sentinel-provider");
config.setOwner("wei");
return config;
}
@Bean
public RegistryConfig registryConfig(){
RegistryConfig registryConfig=new RegistryConfig();
registryConfig.setAddress("zookeeper://192.168.1.102:2181");
registryConfig.setCheck(false);
return registryConfig;
}
@Bean
public ProtocolConfig protocolConfig(){
ProtocolConfig protocolConfig=new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setPort(20880);
return protocolConfig;
}
}
Bootstrap
public class Bootstrap {
public static void main(String[] args) throws IOException {
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(ProviderConfig.class);
((AnnotationConfigApplicationContext) applicationContext).start();
System.in.read();
}
}
创建SpringBoot的Consumer项目
添加jar依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<artifactId>sentinel-api</artifactId>
<groupId>com.wei.sentinel</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
SentinelDubboController
@RestController
public class SentinelDubboController {
@Reference
SentinelService sentinelService;
@GetMapping("/say")
public String sayHello(){
String result=sentinelService.sayHello("wei");
return result;
}
}
application.properties
dubbo.registry.address=zookeeper://192.168.1.102:2181
dubbo.application.name=springboot-sentinel
dubbo.scan.base-packages=com.wei.sentinel
添加sentinel限流支持
添加jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-dubbo-adapter</artifactId>
<version>1.6.2</version>
</dependency
设置限流的基准
Service Provider 用于向外界提供服务,处理各个消费者的调用请求。为了保护 Provider 不被激增的流量拖垮影响稳定性,可以给 Provider 配置 QPS 模式的限流,这样当每秒的请求量超过设定的阈值时会自动拒绝多的请求。限流粒度可以是服务接口和服务方法两种粒度。若希望整个服务接口的 QPS 不超过一定数值,则可以为对应服务接口资源(resourceName 为接口全限定名)配置 QPS 阈值;若希望服务的某个方法的 QPS 不超过一定数值,则可以为对应服务方法资源(resourceName 为接口全限定名:方法签名)配置 QPS 阈值
private static void initFlowRule(){
FlowRule flowRule=new FlowRule();
//针对具体的方法限流
flowRule.setResource("com.wei.sentinel.SentinelService:sayHello(java.lang.String)");
flowRule.setCount(10);//限流阈值 qps=10
flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);//限流阈值类型(QPS 或并发线程数)
flowRule.setLimitApp("default");//流控针对的调用来源,若为 default 则不区分调用来源
//流量控制手段(直接拒绝、Warm Up、匀速排队)
flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(Collections.singletonList(flowRule));
}
启动时加入 JVM 参数 -Dcsp.sentinel.dashboard.server=localhost:8080 指定控制台地址和端口
自行用使用jemeter进行压测
参数解释:
1.LimitApp
很多场景下,根据调用方来限流也是非常重要的。比如有两个服务 A 和 B 都向 Service Provider 发起调用请求,我们希望只对来自服务 B 的请求进行限流,则可以设置限流规则的 limitApp 为服务 B 的名称。
Sentinel Dubbo Adapter 会自动解析 Dubbo 消费者(调用方)的 application name 作为调用方
名称(origin),在进行资源保护的时候都会带上调用方名称。若限流规则未配置调用方(default),则该限流规则对所有调用方生效。若限流规则配置了调用方则限流规则将仅对指定调用方生效。
注:Dubbo 默认通信不携带对端 application name 信息,因此需要开发者在调用端手动将 applicationname 置入 attachment 中,provider 端进行相应的解析。Sentinel Dubbo Adapter 实现了一个 Filter 用于自动从 consumer 端向 provider 端透传 application name。若调用端未引入 Sentinel DubboAdapter,又希望根据调用端限流,可以在调用端手动将 application name 置入 attachment 中,key 为dubboApplication
演示流程
- 修改provider中限流规则:flowRule.setLimitApp("springboot-app1");
- 在consumer工程中,做如下处理。其中一个通过attachment传递了一个消费者的
application.name,另一个没有传,通过jemeter工具进行测试
@GetMapping("/say1")
public String sayHello(){
RpcContext.getContext().setAttachment("dubboApplication","springboot-app1\"");
String result=sentinelService.sayHello("wei");
return result;
}
@GetMapping("/say2")
public String say2Hello(){
String result=sentinelService.sayHello("wei");
return result;
}
2.ControlBehavior
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段
- 直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时
- Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式,当系统长期处于低并发的情况下,流量突然增加到qps的最高峰值,可能会造成系统的瞬间流量过大把系统压垮。所以warmup,相当于处理请求的数量是缓慢增加,经过一段时间以后,到达系统处理请求个数的最大值
- 匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法
它的原理是,以固定的间隔时间让请求通过。当请求过来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。
可以设置一个最长排队等待时间: flowRule.setMaxQueueingTimeMs(5 * 1000); // 最长排队等待时间:5s
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
实现分布式限流
要想使用集群流控功能,我们需要在应用端配置动态规则源,并通过 Sentinel 控制台实时进行推送。如下图所示:
image.png
搭建token-server
image.pngJar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
ClusterServer
public class ClusterServer {
public static void main(String[] args) throws Exception {
ClusterTokenServer tokenServer=new SentinelDefaultTokenServer();
ClusterServerConfigManager.loadGlobalTransportConfig(
new ServerTransportConfig().setIdleSeconds(600).setPort(9999));
ClusterServerConfigManager.loadServerNamespaceSet(Collections.singleton("App-wei")); //设置成动态
tokenServer.start();
}
}
DataSourceInitFunc
public class NacosDataSourceInitFunc implements InitFunc {
private final String remoteAddress="192.168.1.102"; //nacos 配置中心的服务host
private final String groupId="SENTINEL_GROUP";
private final String FLOW_POSTFIX="-flow-rules"; //dataid(names+postfix)
//意味着当前的token-server会从nacos上获得限流的规则
@Override
public void init() throws Exception {
ClusterFlowRuleManager.setPropertySupplier(namespace ->{
ReadableDataSource<String, List<FlowRule>> rds=
new NacosDataSource<List<FlowRule>>(remoteAddress,groupId,namespace+FLOW_POSTFIX,
source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>(){}));
return rds.getProperty();
});
}
}
resource目录添加扩展点
/META-INF/services/com.alibaba.csp.sentinel.init.InitFunc = 自定义扩展点
com.wei.sentinel.server.NacosDataSourceInitFunc
启动Sentinel dashboard
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -
Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.6.1.jar
启动nacos以及增加配置
DataID=App-wei-flow-rules
Group=SENTINEL_GROUP
配置内容:
配置之前写死的相关Sentinel 配置,如resource,grade,count等等
配置jvm参数
配置如下jvm启动参数,连接到sentinel dashboard
-Dproject.name=App-wei -Dcsp.sentinel.dashboard.server=192.168.1.102:8080
-Dcsp.sentinel.log.use.pid=true
服务启动之后,在/logs/csp/ 可以找到sentinel-record.log.pid*.date文件,如果看到日
志文件中获取到了远程服务的信息,说明token-server启动成功了
Dubbo接入分布式限流
jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-dubbo-adapter</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-client-default</artifactId>
<version>1.6.3</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.6.3</version>
</dependency>
增加扩展点
扩展点需要在resources/META-INF/services/增加扩展的配置
com.alibaba.csp.sentinel.init.InitFunc = 自定义扩展点
public class DataSourceInitFunc implements InitFunc {
private static final String CLUSTER_SERVER_HOST = "localhost";//token-server的ip
private static final int CLUSTER_SERVER_PORT = 9999;//token-server 端口
private static final int REQUEST_TIME_OUT = 200000; //请求超时时间
private static final String APP_NAME = "App-wei";
private static final String REMOTE_ADDRESS = "192.168.1.102"; //nacos服务的ip
private static final String GROUP_ID = "SENTINEL_GROUP";//group id
private static final String FLOW_POSTFIX = "-flow-rules";//限流规则后缀
@Override
public void init() throws Exception {
loadClusterClientConfig();
registerClusterFlowRuleProperty();
}
//通过硬编码的方式,配置连接到token-server服务的地址,{这种在实际使用过程中不建议,后续可以基于动态配置源改造
public static void loadClusterClientConfig() {
ClusterClientAssignConfig assignConfig = new ClusterClientAssignConfig();
assignConfig.setServerHost(CLUSTER_SERVER_HOST);
assignConfig.setServerPort(CLUSTER_SERVER_PORT);
ClusterClientConfigManager.applyNewAssignConfig(assignConfig);
ClusterClientConfig clientConfig = new ClusterClientConfig();
clientConfig.setRequestTimeout(REQUEST_TIME_OUT); //token-client请求 token - server获取令牌的超时时间
ClusterClientConfigManager.applyNewConfig(clientConfig);
}
/**
* 注册动态规则Property
* 当client与Server连接中断,退化为本地限流时需要用到的该规则
* 该配置为必选项,客户端会从nacos上加载限流规则,请求tokenserver时,会戴上要check的规则id
*/
private static void registerClusterFlowRuleProperty() {
// 使用 Nacos 数据源作为配置中心,需要在 REMOTE_ADDRESS 上启动一个 Nacos 的服务
ReadableDataSource<String, List<FlowRule>> ds = new
NacosDataSource<List<FlowRule>>(REMOTE_ADDRESS, GROUP_ID, APP_NAME + FLOW_POSTFIX,
source -> JSON.parseObject(source, new
TypeReference<List<FlowRule>>() {
}));
// 为集群客户端注册动态规则源
FlowRuleManager.register2Property(ds.getProperty());
}
}
配置jvm参数
这里的project-name要包含在token-server中配置的namespace中,
token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值
-Dproject.name=App-wei -Dcsp.sentinel.dashboard.server=192.168.8.106:8080
-Dcsp.sentinel.log.use.pid=true
服务启动之后,在/logs/csp/ 可以找到sentinel-record.log.pid*.date文件,如果看到日志文件中获取到了token-server的信息,说明连接成功了
Sentinel熔断降级
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断
那么怎么去判断资源是否处于稳定状态呢?
- 平均响应时间,比如,在1s内连续处理5个请求,它的平均响应时间都超过阈值,那么在后续的时
间窗口中,对于这个方法的调用都会自动熔断,sentinel默认的平均响应时间是4900ms - 异常比例,当指定资源每秒请求量大于等于5,并且每秒的异常总数占通过量的比值超过阈值之后
(比如每秒处理1000个请求,那么其中异常请求数为500,那么当前的比值是50%),那么该资源
会进入降级状态。异常的比率范围是[0.0.1.0]表示0%到100% - 异常数,当资源在1分钟的异常数据超过阈值后会进行熔断针对这些规则,Sentinel中给出了响应的字段来设置
-
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的
timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。 -
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
-
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
熔断演示
Jar包依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-dubbo-adapter</artifactId>
<version>1.6.2</version>
</dependency>
配置规则
添加一个DataSourceInitFunc。
然后在resource/METAINF/services/com.alibaba.csp.sentinel.init.InitFunc中配置改类的全路径,这样的话sentinel在触发限流时会去调用这个initFunc来解析规则
public class DataSourceInitFunc implements InitFunc {
@Override
public void init() throws Exception {
List<DegradeRule> rules=new ArrayList<>();
DegradeRule rule=new DegradeRule();
//下面这个配置的意思是,当1s内持续进入5个请求,平均响应时间都超过count(10ms),
// 那么在接下来的timewindow(10s)内,对
//这个方法的调用都会自动熔断,抛出异常:degradeException.
//指定被保护的资源
rule.setResource("com.wei.sentinel.SentinelService");
rule.setCount(10); //阈值
//降级模式, RT(平均响应时间)、异常比例(DEGRADE_GRADE_EXCEPTION_RATIO)/异常数量
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setTimeWindow(10);//降级的时间单位, 单位为s
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
}
增加一个sleep,这个时候就会起到一个熔断的效果
@Override
public String sayHello(String name) {
try {
Thread.sleep(1500); //添加这个和不添加这个的影响
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("begin execute sayHello:"+name);
return "Hello World:"+name+"->timer:"+ LocalDateTime.now();
}
网友评论