云商城-百万并发站点构建
第14章 百万并发架构
课程目标
1、Sentinel
1)Sentinel介绍
2)Sentinel核心功能
3)Sentinel集成Gateway
4)Sentinel控制台
2、Lvs+Nginx集群
1)Lvs负载均衡模式
- NAT模式
- TUN模式
- DR模式
2)Lvs+Nginx集群实现
1 Sentinel
Sentinel主页 https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5
1.1 Sentinel介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
1)Sentinel核心组件
1:核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 7 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
2:控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
2)Sentinel vs Hystrix
| 对比内容 | Sentinel | Hystrix |
|---|---|---|
| 隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
| 熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
| 实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
| 规则配置 | 支持多种数据源 | 支持多种数据源 |
| 扩展性 | 多个扩展点 | 插件的形式 |
| 基于注解的支持 | 支持 | 支持 |
| 限流 | 基于 QPS,支持基于调用关系的限流 | 不支持 |
| 流量整形 | 支持慢启动、匀速器模式 | 不支持 |
| 系统负载保护 | 支持 | 不支持 |
| 控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
| 常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix |
3)Sentinel基本概念
- 资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
- 规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
1.2 Sentinel核心功能
1.2.1 流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
1.2.2 熔断降级
1)什么是熔断降级
除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。

Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
2)Sentinel熔断降级设计
Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。
Sentinel熔断降级设计:
并发线程数限制:和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
响应时间降级:除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
3)系统自适应保护
Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
1.3 Sentinel集成Gateway
我们的项目流量入口是SpringCloud Gateway,因此我们重点讲解Sentinel集成Gateway。
1.3.1 Sentinel对网关支持
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。

Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:
GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫my_api,请求 path 模式为/foo/**和/baz/**的都归到my_api这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
其中网关限流规则 GatewayFlowRule 的字段解释如下:
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。grade:限流指标维度,同限流规则的grade字段。count:限流阈值intervalSec:统计时间窗口,单位是秒,默认是 1 秒。controlBehavior:流量整形的控制效果,同限流规则的controlBehavior字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。burst:应对突发请求时额外允许的请求数目。maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。paramItem
:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)
用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。
1.3.2 集成Sentinel
我们如果想要让微服务网关集成Sentinel,需要引入依赖包,使用时只需注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
依赖:
<!--Sentinel-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.0</version>
</dependency>
实例引入:创建配置类com.gupaoedu.vip.mall.api.config.GatewayConfiguration:
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 限流的异常处理器
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/***
* Sentinel路由处理核心过滤器
* @return
*/
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}
此时集成就完成了。
1.3.3 配置/API定义
1)API定义
正如前面所说,ApiDefinition用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
我们在配置类com.gupaoedu.vip.mall.api.config.GatewayConfiguration创建Api:
/***
* Api定义
*/
private void initCustomizedApis() {
//Api集合
Set<ApiDefinition> definitions = new HashSet<ApiDefinition>();
ApiDefinition cartApi = new ApiDefinition("mall_cart_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/cart/list"));
add(new ApiPathPredicateItem().setPattern("/cart/*")
//参数值的匹配策略
// 精确匹配(PARAM_MATCH_STRATEGY_EXACT)
// 子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)
// 正则匹配(PARAM_MATCH_STRATEGY_REGEX)
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(cartApi);
//加载Api
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
2)规则创建
GatewayFlowRule网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
我们在配置类com.gupaoedu.vip.mall.api.config.GatewayConfiguration创建配置:
/***
* 规则定义
*/
private void initGatewayRules() {
//网关限流规则
Set<GatewayFlowRule> rules = new HashSet<GatewayFlowRule>();
//商品微服务规则配置
//资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
rules.add(new GatewayFlowRule("goods_route")
//限流阈值
.setCount(2)
//应对突发请求时额外允许的请求数目。
.setBurst(2)
//统计时间窗口,单位是秒,默认是 1 秒。
.setIntervalSec(1)
//限流行为
//CONTROL_BEHAVIOR_RATE_LIMITER 匀速排队
//CONTROL_BEHAVIOR_DEFAULT 快速失败(默认)
.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)
//匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
.setMaxQueueingTimeoutMs(1000)
);
//API 【mall_cart_api】限流配置
rules.add(new GatewayFlowRule("mall_cart_api")
//限流阈值
.setCount(3)
//统计时间窗口,单位是秒,默认是 1 秒。
.setIntervalSec(1)
);
//加载网关规则
GatewayRuleManager.loadRules(rules);
}
平时工作中对参数过滤的需求并不多,这里的参数过滤我们就不在讲解了,大家可以当做作业实践。
3)配置/Api加载
上面两个方法创建了API并且创建了配置,但并没有在程序启动加载,我们可以采用@PostConstruct注解实现加载调用,在com.gupaoedu.vip.mall.api.config.GatewayConfiguration中创建方法:
/***
* 初始化加载Api和规则
*/
@PostConstruct
public void doInit() {
initCustomizedApis();
initGatewayRules();
}
1.4 Sentinel控制台
Sentinel 控制台是流量控制、熔断降级规则统一配置和管理的入口,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。在 Sentinel 控制台上,我们可以配置规则并实时查看流量控制效果。
1.4.1 Sentinel控制台安装
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。
Sentinel 控制台包含如下功能:
- 查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
- 监控 (单机和集群聚合):通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。
- 规则管理和推送:统一管理推送规则。
- 鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。
Sentinel控制台安装可以基于jar包启动的方式安装,也可以基于docker安装,为了方便操作,我们这里采用docker安装方式:
docker run --name sentinel -d -p 8858:8858 -d bladex/sentinel-dashboard
安装好了后,我们可以直接访问http://192.168.100.130:8858访问控制台,默认用户名和密码都是 sentinel:

登录后,效果如下:

客户端需要引入 Transport 模块来与 Sentinel 控制台进行通信,可以通过 pom.xml 引入 JAR 包:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>x.y.z</version>
</dependency>
如果是SpringBoot工程接入Sentinel,可以直接引入如下依赖包:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1.4.2 接入控制台
我们将Gateway微服务网关接入Sentinel控制台,首先引入依赖包:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在核心配置文件中配置Sentinel服务地址
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: 192.168.100.130:8858
这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互,比如限流规则拉取。
此时我们出发一些请求操作,再看Sentinel控制台会多一个服务监控:

2 Lvs+Nginx集群

高并发站点不仅要考虑网站后端服务的稳定,还需要考虑服务能否接入巨大流量、承受巨大流量,如上图:
1:流量接入,可以采用Lvs+Nginx集群,这种方式能接入的QPS能高达数百万
2:通过Lvs实现Nginx集群,Nginx+Tomcat实现后端服务集群,完成了从接入层流量处理到后端服务集群高并发处理
我们接下来展开对Lvs的学习。
2.1 Lvs介绍
LVS(Linux Virtual Server),即Linux虚拟服务器。它用于多服务器的负载均衡,工作在网络四层,可以实现高性能,高可用的服务器集群技术,它稳定可靠,即使在集群的服务器中某台服务器无法正常工作,也不影响整体效果。是基于TCP/IP做的路由和转发,稳定性和效率极高。

一个LVS集群往往包含以下角色:
1:DS:Director Server。虚拟服务,负责调度
2:RS:Real Server。后端真实的工作服务器。
3:VIP:向外部直接面向用户请求,作为用户请求的目标的IP地址
4:DIP:Director Server IP,DS的IP
5:RIP:Real Server IP,后端服务器的IP地址
6:CIP:Client IP,访问客户端的IP地址
2.2 Lvs 负载均衡模式
lvs提供了3种负载均衡模式,每种负载均衡模式适用的场景有差异,我们来讲解一下这三种负载均衡模式。
2.2.1 NAT
用户的请求到分发器后,通过预设的iptables规则,把请求的数据包转发到后端的RS上去。RS需要设定网关为分发器的内网IP。用户请求的数据包和返回给用户的数据包全部经过分发器,所以分发器称为瓶颈。在NAT模式中,只需要分发器有公网IP即可,所以比较节省公网IP资源。

2.2.2 TUN
这种模式需要有一个公共的IP配置在分发器和所有的RS上,我们把它叫做VIP。客户端请求的目标IP为VIP,分发器接收到请求数据包后,会对数据包做一个加工,会把目标IP改为RS的IP,这样数据包就到了RS上。RS接收数据包后,会还原原始数据包,这样目标IP为VIP,因为所有RS上配置了这个VIP,所以他会认为是它自己。

2.2.3 DR模式
和IP Tunnel较为相似,不同的是,它会把数据包的MAC地址修改为RS的MAC地址。真实服务器将响应直接返回给客户。
这种方式没有IP隧道的开销,对集群中的真实服务期也没有必须支持IP隧道协议的要求,但是要求调度器与真实服务器都有一块网卡连在同一物理网段上。

2.3 Lvs DR模式配置
综合上面分析,我们可以得出结论,DR模式性能效率比较高,安全性很高,因此一般公司都推荐使用DR模式。我们这里也配置DR模式实现Lvs+Nginx集群。
我们准备了3台机器:
1:192.168.100.130 (DS) 192.168.100.150 对外提供服务
2:192.168.100.131 (RS) 192.168.100.150 真实服务处理业务流程
3:192.168.100.133 (RS) 192.168.100.150 真实服务处理业务流程
VIP:192.168.100.150
2.3.1 Vip配置
关闭网络配置管理器(每台机器都要做)
systemctl stop NetworkManager
systemctl disable NetworkManager
配置虚拟IP(VIP 192.168.100.130中配置)
在/etc/sysconfig/network-scripts创建文件ifcfg-ens33:1,内容如下:
BOOTPROTO=static
DEVICE=ens33:1
ONBOOT=yes
IPADDR=192.168.100.150
NETMASK=255.255.255.0
重启网络服务:
service network restart
我们可以看到在原来的网卡上面添加了一个虚拟IP 150。

同时需要对192.168.100.131、192.168.100.133构建虚拟机IP,但只是用于返回数据,而不能被用户访问到,这时候需要操作ifcfg-lo。
IPADDR=127.0.0.1,这里127.0.0.1属于本地回环地址,不属于任何一个有类别地址类。它代表设备的本地虚拟接口,所以默认被看作是永远不会宕掉的接口。
NETMASK=255.255.255.255
192.168.100.131:
将ifcfg-lo拷贝一份ifcfg-lo:1,并修改ifcfg-lo:1配置,内容如下:

刷新lo:
ifup lo
查看IP可以发现lo下多了150ip。
192.168.100.133知行和上面相同的操作。
2.3.2 LVS集群管理工具安装
ipvsadm用于对lvs集群进行管理,需要手动安装。
安装命令:
yum install ipvsadm
版本查看:
ipvsadm -Ln
效果如下:

2.3.3 地址解析协议
在192.168.100.131和192.168.100.133中操作。
arp_ignore和arp_announce参数都和ARP协议相关,主要用于控制系统返回arp响应和发送arp请求时的动作。这两个参数很重要,特别是在LVS的DR场景下,它们的配置直接影响到DR转发是否正常。
arp-ignore:arp_ignore参数的作用是控制系统在收到外部的arp请求时,是否要返回arp响应(0~8,2-8用的很少)
配置文件:/etc/sysctl.conf,将如下文件拷贝进去:
net.ipv4.conf.all.arp_ignore = 1
net.ipv4.conf.default.arp_ignore = 1
net.ipv4.conf.lo.arp_ignore = 1
net.ipv4.conf.all.arp_announce = 2
net.ipv4.conf.default.arp_announce = 2
net.ipv4.conf.lo.arp_announce = 2
刷新配置:
sysctl -p
添加路由:此时如果无法识别route,需要安装相关工具yum install net-tools。
route add -host 192.168.100.150 dev lo:1
添加了一个host地址,目的是用于接收数据报文,接收到了数据报文后会交给lo:1处理。(防止关机失效,需要将上述命令添加到/etc/rc.local中)
添加完host后,可以查看一下:route -n,能明显看到效果。
上述配置我们同样要在192.168.100.133中配置。
2.3.4 集群配置
ipvsadm命令讲解:
ipvsadm -A:用于创建集群
ipvsadm -E:用于修改集群
ipvsadm -D:用于删除集群
ipvsadm -C:用于清除集群数据
ipvsadm -R:用于重置集群配置规则
ipvsadm -S:用于保存修改的集群规则
ipvsadm -a:用于添加一个rs节点
ipvsadm -e:用于修改一个rs节点
ipvsadm -d:用于删除一个rs节点
添加集群TCP服务地址:(外部请求由该配置指定的VIP处理)
ipvsadm -A -t 192.168.100.150:80 -s rr
参数说明:
-A:添加集群配置
-t:TCP请求地址(VIP)
-s:负载均衡算法
负载均衡算法:
| 算法 | 说明 |
|---|---|
| rr | 轮询算法,它将请求依次分配给不同的rs节点,也就是RS节点中均摊分配。这种算法简单,但只适合于RS节点处理性能差不多的情况 |
| wrr | 加权轮训调度,它将依据不同RS的权值分配任务。权值较高的RS将优先获得任务,并且分配到的连接数将比权值低的RS更多。相同权值的RS得到相同数目的连接数。 |
| Wlc | 加权最小连接数调度,假设各台RS的全职依次为Wi,当前tcp连接数依次为Ti,依次去Ti/Wi为最小的RS作为下一个分配的RS |
| Dh | 目的地址哈希调度(destination hashing)以目的地址为关键字查找一个静态hash表来获得需要的RS |
| SH | 源地址哈希调度(source hashing)以源地址为关键字查找一个静态hash表来获得需要的RS |
| Lc | 最小连接数调度(least-connection),IPVS表存储了所有活动的连接。LB会比较将连接请求发送到当前连接最少的RS. |
| Lblc | 基于地址的最小连接数调度(locality-based least-connection):将来自同一个目的地址的请求分配给同一台RS,此时这台服务器是尚未满负荷的。否则就将这个请求分配给连接数最小的RS,并以它作为下一次分配的首先考虑。 |
配置rs(2个)节点:
ipvsadm -a -t 192.168.100.150:80 -r 192.168.100.131:80 -g
ipvsadm -a -t 192.168.100.150:80 -r 192.168.100.133:80 -g
参数说明:
-a:给集群添加一个节点
-t:指定VIP地址
-r:指定real server地址
-g:表示LVS的模式为dr模式
添加了节点后,我们通过ipvsadm -Ln查看,可以看到多了2个节点。
此时集群列表中客户端请求数据和TCP通信数据会持久化保存,为了更好看到效果,我们可以把时间设置成2秒保存,如下命令:
ipvsadm --set 2 2 2
此时我们请求http://192.168.100.150/测试
可以发现第1次请求是如下效果:

可以发现第2次请求是如下效果:
