小白也能懂的 Nacos 服务模型介绍

简介: 理解了 Nacos 的服务模型,也有利于我们了解 Nacos 背后的工作原理,从而确保我们正确地使用 Nacos。

作者:岛风

前言

 

按照目前市场上的主流使用场景,Nacos 被分成了两块功能:服务注册发现(Naming)和配置中心(Config)。在之前的文章中我介绍了 Nacos 配置中心的实现原理,今天这篇文章所介绍的内容则是与 Nacos 服务注册发现功能相关,来聊一聊 Nacos 的服务模型。

 

说到服务模型,其实需要区分视角,一是用户视角,一个内核视角。即 Nacos 用户视角看到的服务模型和 Nacos 开发者设计的内核模型可能是完全不一样的,而今天的文章,是站在用户视角观察的,旨在探讨 Nacos 服务发现的最佳实践。

 

服务模型介绍

 

一般我在聊注册中心时,都会以 Zookeeper 为引子,这也是很多人最熟悉的注册中心。但如果你真的写过或看过使用 Zookeeper 作为注册中心的适配代码,会发现并不是那么容易,再加上注册中心涉及到的一致性原理,这就导致很多人对注册中心的第一印象是:这个东西好难! 但归根到底是因为 Zookeeper 根本不是专门为注册中心而设计的,其提供的 API 以及内核设计,并没有预留出「服务模型」的概念,这就使得开发者需要自行设计一个模型,去填补 Zookeeper 和服务发现之间的鸿沟。

 

微服务架构逐渐深入人心后,Nacos、Consul、Eureka 等注册中心组件进入大众的视线。可以发现,这些”真正“的注册中心都有各自的「服务模型」,在使用上也更加的方便。

 

为什么要有「服务模型」?理论上,一个基础组件可以被塑造成任意的模样,如果你愿意,一个数据库也可以被设计成注册中心,这并不是”夸张“的修辞手法,在阿里还真有人这么干过。那么代价是什么呢?一定会在业务发展到一定体量后遇到瓶颈,一定会遇到某些极端 case 导致其无法正常工作,一定会导致其扩展性低下。正如刚学习数据结构时,同学们常见的一个疑问一样:为什么栈只能先进后出。不是所有开发都是中间件专家,所以 Nacos 设计了自己的「服务模型」,这虽然限制了使用者的”想象力“,但保障了使用者在正确地使用 Nacos。

 

花了一定的篇幅介绍 Nacos 为什么需要设计「服务模型」,再来看看实际的 Nacos 模型是个啥,其实没那么玄乎,一张图就能表达清楚:

 

 

与 Consul、Eureka 设计有别,Nacos 服务发现使用的领域模型是命名空间-分组-服务-集群-实例这样的多层结构。服务 Service 和实例 Instance 是核心模型,命名空间 Namespace 、分组 Group、集群 Cluster 则是在不同粒度实现了服务的隔离。

 

为了更好的理解两个核心模型:Service 和 Instance,我们以 Dubbo 和 SpringCloud 这两个已经适配了 Nacos 注册中心的微服务框架为例,介绍下二者是如何映射对应模型的。

 

  • Dubbo。将接口三元组(接口名+分组名+版本号)映射为 Service,将实例 IP 和端口号定义为 Instance。一个典型的注册在 Nacos 中的 Dubbo 服务:providers:com.alibaba.mse.EchoService:1.0.0:DUBBO
  • Spring Cloud。将应用名映射为 Service,将实例 IP 和端口号定义为 Instance。一个典型的注册在 Nacos 中的 Spring Cloud 服务:helloApp

 

下面我们将会更加详细地阐释 Nacos 提供的 API 和服务模型之间的关系。

 

环境准备

 

需要部署一个 Nacos Server 用于测试,我这里选择直接在 https://mse.console.aliyun.com/ 购买一个 MSE 托管的 Nacos,读者们可以选择购买 MSE Nacos 或者自行搭建一个 Nacos Server。

 

 

MSE Nacos 提供的可视化控制台,也可以帮助我们更好的理解 Nacos 的服务模型。下文的一些截图,均来自 MSE Nacos 的商业化控制台。

 

快速开始

 

先来实现一个最简单的服务注册与发现 demo。Nacos 支持从客户端注册服务实例和订阅服务,具体代码如下:

 

Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, "mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848");String serviceName = "nacos.test.service.1";
String instanceIp = InetAddress.getLocalHost().getHostAddress();
int instancePort = 8080;namingService.registerInstance(serviceName, instanceIp, instancePort);System.out.println(namingService.getAllInstances(serviceName));

 

上述代码定义了一个 service:nacos.test.service.1;定义了一个 instance,以本机 host 为 IP 和 8080 为端口号,观察实际的注册情况:

 

 

 

并且控制台也打印出了服务的详情。至此一个最简单的 Nacos 服务发现 demo 就已经完成了。对一些细节稍作解释:

 

  • 属性 PropertyKeyConst.SERVER_ADDR 表示的是 Nacos 服务端的地址。
  • 创建一个 NamingService 实例,客户端将为该实例创建单独的资源空间,包括缓存、线程池以及配置等。Nacos 客户端没有对该实例做单例的限制,请小心维护这个实例,以防新建多于预期的实例。
  • 注册服务 registerInstance 使用了最简单的重载方法,只需要传入服务名、IP、端口就可以。

 

上述的例子中,并没有出现 Namespace、Group、Cluster 等前文提及的服务模型,我会在下面一节详细介绍,这个例子主要是为了演示 Nacos 支持的一些缺省配置,其中 Service 和 Instance 是必不可少的,这也验证了前文提到的服务和实例是 Nacos 的一等公民。

 

通过截图我们可以发现缺省配置的默认值:

 

  • Namespace:默认值是 public 或者空字符串,都可以代表默认命名空间。
  • Group:默认值是 DEFAULT_GROUP。
  • Cluster:默认值是 DEFAULT。

 

构建自定义实例

 

为了展现出 Nacos 服务模型的全貌,还需要介绍下实例相关的 API。例如我们希望注册的实例中,有一些能够被分配更多的流量;或者能够传入一些实例的元信息存储到 Nacos 服务端,例如 IP 所属的应用或者所在的机房,这样在客户端可以根据服务下挂载的实例元信息,来自定义负载均衡模式。Nacos 也提供了另外的注册实例接口,使得用户在注册实例时可以指定实例的属性:

 

/*** register a instance to service with specified instance properties.** @param serviceName name of service* @param groupName   group of service* @param instance    instance to register* @throws NacosException nacos exception*/
void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException;

 

这个方法在注册实例时,可以传入一个 Instance 实例,它的属性如下:

 

public class Instance {/*** unique id of this instance.*/private String instanceId;/*** instance ip.*/private String ip;/*** instance port.*/private int port;/*** instance weight.*/private double weight = 1.0D;/*** instance health status.*/private boolean healthy = true;/*** If instance is enabled to accept request.*/private boolean enabled = true;/*** If instance is ephemeral.** @since 1.0.0*/private boolean ephemeral = true;/*** cluster information of instance.*/private String clusterName;/*** Service information of instance.*/private String serviceName;/*** user extended attributes.*/private Map<String, String> metadata = new HashMap<String, String>();
}

 

有一些字段可以望文生义,有一些则需要花些功夫专门去了解 Nacos 的设计,我这里挑选几个我认为重要的属性重点介绍下:

 

  • healthy 实例健康状态。标识该实例是否健康,一般心跳健康检查会自动更新该字段。
  • enable 是否启用。它跟 healthy 区别在于,healthy 一般是由内核健康检查更新,而 enable 更多是业务语义偏多,可以完全根据业务场景操控。例如在 Dubbo 中,一般使用该字段标识某个实例 IP 的上下线状态。
  • ephemeral 临时实例还是持久化实例。非常关键的一个字段,需要对 Nacos 有较为深入的了解才能够理解该字段的含义。区别在于,心跳检测失败一定时间之后,实例是自动下线还是标记为不健康。一般在注册中心场景下,会使用临时实例。这样心跳检测失败之后,可以让消费者及时收到下线通知;而在 DNS 模式下,使用持久化实例较多。在《一文详解 Nacos 高可用特性》中我也介绍过,该字段还会影响到 Nacos 的一致性协议。
  • metadata 元数据。一个 map 结构,可以存储实例的自定义扩展信息,例如机房信息,路由标签,应用信息,权重信息等。

 

这些信息在由服务提供者上报之后,由服务消费者获取,从而完成信息的传递。以下是一个完整的实例注册演示代码:

 

Properties properties = new Properties();
// 指定 Nacos Server 地址
properties.setProperty(PropertyKeyConst.SERVER_ADDR, "mse-xxxx-p.nacos-ans.mse.aliyuncs.com:8848");
// 指定命名空间
properties.setProperty(PropertyKeyConst.NAMESPACE, "9125571e-bf50-4260-9be5-18a3b2e3605b");NamingService namingService = NacosFactory.createNamingService(properties);
String serviceName = "nacos.test.service.1";
String group = "DEFAULT_GROUP";
String clusterName = "cn-hangzhou";
String instanceIp = InetAddress.getLocalHost().getHostAddress();
int instancePort = 8080;
Instance instance = new Instance();
// 指定集群名
instance.setClusterName(clusterName);
instance.setIp(instanceIp);
instance.setPort(instancePort);
// 指定实例的元数据
Map<String, String> metadata = new HashMap<>();
metadata.put("app", "nacos-demo");
metadata.put("site", "cn-hangzhou");
metadata.put("protocol", "1.3.3");
instance.setMetadata(metadata);
// 指定服务名、分组和实例
namingService.registerInstance(serviceName, group, instance);System.out.println(namingService.getAllInstances(serviceName));

 

 

构建自定义服务

 

除了实例之外,服务也可以自定义配置,Nacos 的服务随着实例的注册而存在,并随着所有实例的注销而消亡。不过目前 Nacos 对于自定义服务的支持不是很友好,除使用 OpenApi 可以修改服务的属性外,就只能使用注册实例时传入的服务属性来进行自定义配置。所以在实际的 Dubbo 和 SpringCloud 中,自定义服务一般较少使用,而自定义实例信息则相对常用。

 

Nacos 的服务与 Consul、Eureka 的模型都不同,Consul 与 Eureka的服务等同于 Nacos 的实例,每个实例有一个服务名属性,服务本身并不是一个单独的模型。Nacos 的设计在我看来更为合理,其认为服务本身也是具有数据存储需求的,例如作用于服务下所有实例的配置、权限控制等。实例的属性应当继承自服务的属性,实例级别可以覆盖服务级别。以下是服务的数据结构:

 

/*** Service name*/private String name;/*** Protect threshold*/private float protectThreshold = 0.0F;/*** Application name of this service*/private String app;/*** Service group which is meant to classify services into different sets.*/private String group;/*** Health check mode.*/private String healthCheckMode;private Map<String, String> metadata = new HashMap<String, String>();

 

在实际使用过程中,可以像快速开始章节中介绍的那样,仅仅使用 ServiceName 标记一个服务。

 

服务隔离:Namespace&Group&Cluster

 

出于篇幅考虑,这三个概念放到一起介绍。

 

襄王有意,神女无心。Nacos 提出了这几种隔离策略,目前看来只有 Namespace 在实际应用中使用较多,而 Group 和 Cluster 并没有被当回事。

 

Cluster 集群隔离在阿里巴巴内部使用的非常普遍。一个典型的场景是这个服务下的实例,需要配置多种健康检查方式,有一些实例使用 TCP 的健康检查方式,另外一些使用 HTTP 的健康检查方式。另一个场景是,服务下挂载的机器分属不同的环境,希望能够在某些情况下将某个环境的流量全部切走,这样可以通过集群隔离,来做到一次性切流。在 Nacos 2.0 中,也在有意的弱化集群的概念,毕竟开源还是要面向用户的,有些东西适合阿里,但不一定适合开源,等再往后演进,集群这个概念又有可能重新回到大家的视线中了,history will repeat itself。

 

Group 分组隔离的概念可以参考 Dubbo 的服务隔离策略,其也有一个分组。支持分组的扩展,用意当然是好的,实际使用上,也的确有一些公司会习惯使用分组来进行隔离。需要注意的一点是:Dubbo 注册三元组(接口名+分组+版本)时,其中 Dubbo 的分组是包含在 Nacos 的服务名中的,并不是映射成了 Nacos 的分组,一般 Nacos 注册的服务是默认注册到 DEFAULT_GROUP 分组的。

 

Namespace 命名空间隔离,我认为是 Nacos 一个比较好的设计。在实际场景中使用也比较普遍,一般用于多个环境的隔离,例如 daily,dev,test,uat,prod 等环境的隔离。特别是当环境非常多时,使用命名空间做逻辑隔离是一个比较节约成本的策略。但强烈建议大家仅仅在非线上环境使用 Namespace 进行隔离,例如多套测试环境可以共享一套 Nacos,而线上环境单独搭建另一套 Nacos 集群,以免线下测试流量干扰到线上环境。

 

服务发现:推拉模型

 

上面介绍完了 Nacos 服务发现的 5 大领域模型,最后一节,介绍下如何获取服务模型。

 

Nacos 的服务发现,有主动拉取和推送两种模式,这与一般的服务发现架构相同。以下是拉模型的相关接口:

 

/*** Get all instances of a service** @param serviceName name of service* @return A list of instance* @throws NacosException*/
List<Instance> getAllInstances(String serviceName) throws NacosException;/*** Get qualified instances of service** @param serviceName name of service* @param healthy     a flag to indicate returning healthy or unhealthy instances* @return A qualified list of instance* @throws NacosException*/
List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException;/*** Select one healthy instance of service using predefined load balance strategy** @param serviceName name of service* @return qualified instance* @throws NacosException*/
Instance selectOneHealthyInstance(String serviceName) throws NacosException;

 

Nacos 提供了三个同步拉取服务的方法,一个是查询所有注册的实例,一个是只查询健康且上线的实例,还有一个是获取一个健康且上线的实例。一般情况下,订阅端并不关心不健康的实例或者权重设为 0 的实例,但是也不排除一些场景下,有一些运维或者管理的场景需要拿到所有的实例。细心的读者会注意到上述 Nacos 实例中有一个 weight 字段,便是作用在此处的selectOneHealthyInstance接口上,按照权重返回一个健康的实例。个人认为这个功能相对鸡肋,一般的 RPC 框架都有自身配套的负载均衡策略,很少会由注册中心 cover,事实上 Dubbo 和 Spring Cloud 都没有用到 Nacos 的这个接口。

 

除了主动查询实例列表,Nacos还提供订阅模式来感知服务下实例列表的变化,包括服务配置或者实例配置的变化。可以使用下面的接口来进行订阅或者取消订阅:

 

/*** Subscribe service to receive events of instances alteration** @param serviceName name of service* @param listener    event listener* @throws NacosException*/
void subscribe(String serviceName, EventListener listener) throws NacosException;
/*** Unsubscribe event listener of service** @param serviceName name of service* @param listener    event listener* @throws NacosException*/
void unsubscribe(String serviceName, EventListener listener) throws NacosException;

 

在实际的服务发现中,订阅接口尤为重要。消费者启动时,一般会同步获取一次服务信息用于初始化,紧接着订阅服务,这样当服务发生上下线时,就可以感知变化了,从而实现服务发现。

 

总结

 

Nacos 为了更好的实现服务发现,提供一套成熟的服务模型,其中重点需要关注的是 Namespace、Service 和 Instance,得益于这一套服务模型的抽象,以及对推拉模型的支持,Nacos 可以快速被微服务框架集成。

 

理解了 Nacos 的服务模型,也有利于我们了解 Nacos 背后的工作原理,从而确保我们正确地使用 Nacos。但 Nacos 提供的这些模型也不一定所有都需要用上,例如集群、分组、权重等概念,被实践证明是相对鸡肋的设计,在使用时,也需要根据自身业务特点去评估特性用量,不要盲目地为了使用技术而去用。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/513820.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

那些与 IE 相伴的日子

来源&#xff1a;零一作者&#xff1a;前端印象大家好&#xff0c;IE 大家都不陌生&#xff0c;毕竟出现在大家的视野中已经很久很久&#xff0c;久到有20多年&#xff0c;当然也因前端技术的快速更新&#xff0c;给需要兼容IE浏览器的前端程序员带来了不少的困扰。慢慢地&…

html代码style图片width,HTML Style columnWidth用法及代码示例

DOM中的columnWidth属性用于指定列的宽度。用法:返回columnWidth属性&#xff1a;object.style.columnWidth设置columnWidth属性&#xff1a;object.style.columnWidth "auto | length | initial | inherit"属性值&#xff1a;auto:缺省值。列宽将由浏览器确定lengt…

KubeVela 1.0 :开启可编程式应用平台的未来

简介&#xff1a; 如果你对云原生领域不太关注&#xff0c;可能对 KubeVela 还没有做过太深入的了解。别着急&#xff0c;本文就借着 v1.0 发布之际&#xff0c;为你详细的梳理一次 KubeVela 项目的发展脉络&#xff0c;解读它的核心思想和愿景&#xff0c;领悟这个正冉冉升起的…

android-x86 镜像iso下载_2019年微软MSDN原版镜像系统下载地址 Win10/7原版系统iso镜像文件...

如今&#xff0c;不少用户开始讨厌以GHOST形式来安装操作系统&#xff0c;虽然步骤十分简单&#xff0c;但是从网上下载的GHOST系统&#xff0c;已经形成了一个黑色产业链&#xff0c;为了盈利&#xff0c;捆绑了软件全家桶、恶意强制主页&#xff0c;甚至捆绑木马&#xff0c;…

大流量场景下如何云淡风轻地进行线上发布?

简介&#xff1a; 本文介绍了微服务治理下金丝雀发布的能力&#xff0c;解决了发布期间少量流量验证新功能的问题。 前言 本文&#xff0c;我们继续聊聊《揭秘大流量场景下发布如丝般顺滑背后的原因》中的另外一环&#xff0c;灰度发布&#xff0c;也叫金丝雀发布。 ​ 很多互…

匿名提问:rm -rf了怎么办?

整理 | 易璜珵出品 | 《新程序员》IT界流传着一个神秘的代码&#xff0c;老程序员听了总是意味深长地一笑&#xff0c;而新手程序员则总是手痒地想试试&#xff0c;那就是删库指令rm -rf。这一行代码下去&#xff0c;海量数据可能就荡然无存。近几年发生的“删库跑路”事件让这…

ubuntu matlab_有没有人和我一起整理Python的matlab代替

想找人一起整理Python中matlab代替的包&#xff0c;最好是能够将常用功能用tkinter封装起来&#xff0c;积少成多&#xff0c;逐步逼近完整。比如将scipy中的最小二乘法拟合功能&#xff0c;找个图形界面封装一下&#xff0c;就变成了曲线拟合工具&#xff0c;可以代替matlab的…

逸仙电商Seata企业级落地实践

简介&#xff1a; 本文将会以逸仙电商的业务作为背景&#xff0c; 先介绍一下seata的原理&#xff0c; 并给大家进行线上演示&#xff0c; 由浅入深去介绍这款中间件&#xff0c; 以便读者更加容易去理解 Seata 这个中间件。 作者 | 张嘉伟&#xff08;GitHub ID&#xff1a;l…

“类云”的存储服务什么样?Pure Storage发布Pure Fusion等系列新品

一键部署自动化存储平台与云原生数据库即服务&#xff0c;无缝连接基础设施运营与应用程序。 编辑 | 宋慧 出品 | CSDN 云计算 近日&#xff0c;专为多云环境提供存储即服务的Pure Storage发布一系列现代化基础设施、运营及应用程序&#xff0c;这是Pure Storage迈向创新现代…

mac mongodb可视化工具_MongoDB从立地到成佛(介绍、安装、增删改查)

文章作者公众号bigsai&#xff0c;已收录在回车课堂,如有帮助还请不吝啬点个赞赞支持一下&#xff01;课程导学大家好我是bigsai&#xff0c;我们都学过数据库&#xff0c;但你可能更熟悉关系(型)数据库例如MySQL&#xff0c;SQL SERVER&#xff0c;ORACLE等&#xff0c;对于非…

阿里巴巴云原生 etcd 服务集群管控优化实践

简介&#xff1a; 这些年&#xff0c;阿里云原生 etcd 服务发生了翻天覆地的变化&#xff0c;这篇文章主要分享一下 etcd 服务在面对业务量大规模增长下遇到的问题以及我们是如何解决的&#xff0c;希望对读者了解 etcd 的使用和管控运维提供经验分享。 作者 | 陈星宇&#xff…

计算机组装与维护思考问题,计算机组装与维护中的常见问题及解决方法

郜庆国摘要&#xff1a;在如今的社会下&#xff0c;各个领域的很多行业在工作的时候都需要用到计算机来帮助工作的进行&#xff0c;因为计算机在很多情况下都能够很好地进行计算与帮助&#xff0c;所以我们在进行工作时&#xff0c;不仅提高了工作的效率&#xff0c;还解决了很…

淘票票首次公开小程序开发秘籍,踩过坑才知道怎么走!

简介&#xff1a; 在2019年&#xff0c;阿里巴巴文娱的淘票票几乎涉足了当时市面上所有的小程序。在不少平台上&#xff0c;淘票票是阿里“第一批吃螃蟹”的技术团队。回顾过往&#xff0c;阿里文娱做过很多尝试&#xff0c;也踩过很多坑。《小程序 大世界》总结了淘票票过去 2…

stm32f407 6个串口dma_stm32之DMA

一. 对于大容量的STM32芯片有2个DMA控制器&#xff0c;控制器1有7个通道&#xff0c;控制器2有5个通道每个通道都可以配置一些外设的地址。二. 通道的配置过程&#xff1a;1. 首先设置CPARx寄存器和CMARx寄存器。通过DMA控制器把一个地址的值复制到另外一个地址&#xff0c;通过…

立足当下,塑造未来

今天&#xff0c;以“5G与世界同行”为主题的2021全球移动宽带论坛&#xff08;Global MBB Forum&#xff09;在迪拜举行。期间&#xff0c;华为轮值董事长胡厚崑发表了题为“立足当下&#xff0c;塑造未来”的主题演讲。胡厚崑指出&#xff1a;“5G预商用五年以来&#xff0c;…

jfinal html5,Jfinal框架整合webSocket技术功能实现

技术难度&#xff1a;简单在这里我会用最简单的方法实现JFinal框架结合webSocket最基础的功能&#xff0c;以至于后续业务的拓展需要小伙伴们依据实际情况去实现相应的开发&#xff01;废话不多说&#xff0c;直接上代码&#xff01;1、编写webSocket类package morality.ws;imp…

行业实战 | 5G+边缘计算+“自由视角” 让体育赛事更畅快

简介&#xff1a; 世界本是多维的。进入5G时代&#xff0c;观众对多维度视觉体验的需求日益增长&#xff0c;5G MEC网络与边缘计算的结合&#xff0c;具备大带宽、低延迟特性&#xff0c;使视频多维视觉呈现成为现实。在第二十三届CUBA中国大学生篮球联赛期间&#xff0c;中国电…

华为汪涛:走向智能世界2030,无线网络未来十年十大产业趋势

2021全球移动宽带论坛&#xff08;Global MBB Forum&#xff09;期间&#xff0c;华为常务董事、ICT基础设施业务管理委员会主任汪涛发表了题为“走向智能世界2030&#xff0c;无线网络未来十年十大趋势”的主题演讲。汪涛表示&#xff1a;“未来十年&#xff0c;是走向智能世界…

python怎么输入一个数字并调用_Python3 实例(一)

原标题&#xff1a;Python3 实例&#xff08;一&#xff09; Python Hello World 实例 以下实例为学习Python的第一个实例&#xff0c;即如何输出"Hello World!"&#xff1a; 实例 # -*- coding: UTF-8 -*- # Filename : helloworld.py # author by : www.runoob.com…

未来教育计算机书,未来教育.全国计算机等级考试

1册图书1张光盘&#xff0c;轻松应对2018年一级计算机基础及MS Office应用考试 n 1.历年真题精选&#xff0c;全方位把握真考动向&#xff0c;具有练习价值 n (1)新大纲、新题型、新题库&#xff0c;全方位解读无纸化考试&#xff0c;帮助考生轻松过关。 n (2)精选2017年~2016年…