阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!

共 10663字,需浏览 22分钟

 ·

2021-10-16 20:28

点击关注公众号,Java干货及时送达


    1、前言

    微服务的注册中心目前主流的有以下五种:

    • Zookeeper
    • Eureka
    • Consul
    • Nacos
    • Kubernetes

    那么实际开发中到底如何选择呢?这是一个值得深入研究的事情,别着急,今天陈某就带大家深入了解一下这五种注册中心以及如何选型的问题。

    推荐下自己做的 Spring Boot 的实战项目:

    https://github.com/YunaiV/ruoyi-vue-pro

    2、为什么需要注册中心?

    随着单体应用拆分,首当面临的第一份挑战就是服务实例的数量较多,并且服务自身对外暴露的访问地址也具有动态性。可能因为服务扩容、服务的失败和更新等因素,导致服务实例的运行时状态经常变化,如下图:


    商品详情需要调用营销订单库存 三个服务,存在问题有:

    • 营销、订单、库存这三个服务的地址都可能动态的发生改变,单存只使用配置的形式需要频繁的变更,如果是写到配置文件里面还需要重启系统 ,这对生产来说太不友好了;
    • 服务是集群部署的形式调用方负载均衡如何去实现;

    解决第一个问题办法就是用我们用伟人说过一句话,没有什么是加一个中间层解决不了的 ,这个中间层就是我们的注册中心。

    解决第二问题就是关于负载均衡的实现,这个需要结合我们中间层老大哥来实现。

    推荐下自己做的 Spring Cloud 的实战项目:

    https://github.com/YunaiV/onemall

    3、如何实现一个注册中心?

    对于如何实现注册中心这个问题,首先将服务之间是如何交互的模型抽象出来,我们结合实际的案例来说明这个问题,以商品服务为例:

    1. 当我们搜索商品的时候商品服务就是提供者;
    2. 当我们查询商品详情的时候即服务的提供者又是服务的消费者,消费是订单、库存等服务;由此我们需要引入的三个角色就是:中间层(注册中心)、生产者、消费者,如下图:

    整体的执行流程如下:

    1. 在服务启动时,服务提供者通过内部的注册中心客户端应用自动将自身服务注册到注册中心,包含主机地址、服务名称等等信息;
    2. 在服务启动或者发生变更的时候,服务消费者的注册中心客户端程序则可以从注册中心中获取那些已经注册的服务实例信息或者移除已下线的服务;

    上图还多一个设计缓存本地路由,缓存本地路由是为了提高服务路由的效率容错性 ,服务消费者可以配备缓存机制以加速服务路由。更重要的是,当服务注册中心不可用时,服务消费者可以利用本地缓存路由实现对现有服务的可靠调用。

    在整个执行的过程中,其中有点有一点是比较难的,就是服务消费者如何及时知道服务的生产者如何及时变更的,这个问题也是经典的生产者消费者的问题,解决的方式有两种:

    1. 发布-订阅模式 :服务消费者能够实时监控服务更新状态,通常采用监听器以及回调机制,经典的案例就是Zookeeper
    2. 主动拉取策略 :服务的消费者定期调用注册中心提供的服务获取接口获取最新的服务列表并更新本地缓存,经典案例就是Eureka

    对于如何选择这两种方式,其实还有一个数据一致性 问题可以聊聊,比如选择定时器肯定就抛弃了一致性,最求的是最终一致,这里就不深入展开了,另外你可能还会说服务的移除等等这些功能都没介绍,在我看来那只是一个附加功能,注册中心重点还是在于服务注册和发现,其他都是锦上添花罢了。

    4、如何解决负载均衡的问题?

    负载均衡的实现有两种方式:

    1. 服务端的负载均衡;
    2. 客户端的负载均衡; 对于实现的方案来说本质上是差不多的,只是说承接的载体不一样,一个是服务端,一个客户端,如下图:

    服务端的负载均衡,给服务提供者更强的流量控制权,但是无法满足不同的消费者希望使用不同负载均衡策略的需求。

    客户端的负载均衡则提供了这种灵活性,并对用户扩展提供更加友好的支持。但是客户端负载均衡策略如果配置不当,可能会导致服务提供者出现热点,或者压根就拿不到任何服务提供者。

    服务端负载均衡典型的代表就是Nginx ,客户端负载均衡典型代表是Ribbon ,每种方式都有经典的代表,我们都是可以深入学习的。

    常见的负载均衡器的算法的实现,常见的算法有以下六种 :

    1、轮询法

    将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。

    2、随机法

    通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多;其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

    3、哈希算法

    哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

    4、加权轮询法

    不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

    5.加权随机法

    与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

    6.最小连接数法

    最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前 积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。

    5、注册中心如何选型?

    现在注册中心的选择也是五花八门,现阶段比较流行有以下几种:


    在介绍这个之前大家有些需要了解的知识有CAPPaxos 、Raft算法 这里我就不进行过多介绍了。开始介绍以上5种实现注册中心的方式。

    1、Zookeeper

    这个说起来有点意思的是官方并没有说他是一个注册中心,但是国内Dubbo场景下很多都是使用Zookeeper来完成了注册中心的功能。


    当然这有很多历史原因,这里我们就不追溯了,我还是来聊聊作为注册中心使用的情况下,Zookeeper有哪些表现吧。


    Zookeeper基础概念

    1、三种角色

    Leader 角色 :一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。

    Follower角色 :一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。

    Observer角色 :与Follower类似,但是无投票权。

    2、四种节点

    PERSISTENT-持久节点 :除非手动删除,否则节点一直存在于Zookeeper上

    EPHEMERAL-临时节点 :临时节点的生命周期与客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。

    PERSISTENT_SEQUENTIAL-持久顺序节点 :基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

    EPHEMERAL_SEQUENTIAL-临时顺序节点 :基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

    3、一种机制

    Zookeeper的Watch机制 ,是一个轻量级的设计。因为它采用了一种推拉结合的模式。一旦服务端感知主题变了,那么只会发送一个事件类型和节点信息给关注的客户端,而不会包括具体的变更内容,所以事件本身是轻量级的,这就是推的部分。然后,收到变更通知的客户端需要自己去拉变更的数据,这就是拉的部分。

    Zookeeper如何实现注册中心?

    简单来讲,Zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者。如下图所示:


    每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port}

    比如我们的HelloWorldService 部署到两台机器,那么Zookeeper上就会创建两条目录:

    • /HelloWorldService/1.0.0/100.19.20.01:16888
    • HelloWorldService/1.0.0/100.19.20.02:16888。

    这么描述有点不好理解,下图更直观,

    在Zookeeper中,进行服务注册,实际上就是在Zookeeper中创建了一个Znode节点,该节点存储了该服务的IP、端口、调用方式(协议、序列化方式)等。

    该节点承担着最重要的职责,它由服务提供者(发布服务时)创建,以供服务消费者获取节点中的信息,从而定位到服务提供者真正IP,发起调用。通过IP设置为临时节点,那么该节点数据不会一直存储在 ZooKeeper 服务器上。

    当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除,剔除或者上线的时候会触发Zookeeper的Watch机制,会发送消息给消费者,因此就做到消费者信息的及时更新。

    Zookeeper从设计上来说的话整体遵循的CP的原则,在任何时候对 Zookeeper 的访问请求能得到一致的数据结果,同时系统对网络分区具备容错性,在使用 Zookeeper 获取服务列表时,如果此时的 Zookeeper 集群中的 Leader 宕机了,该集群就要进行 Leader 的选举,又或者 Zookeeper 集群中半数以上服务器节点不可用(例如有三个节点,如果节点一检测到节点三挂了 ,节点二也检测到节点三挂了,那这个节点才算是真的挂了),那么将无法处理该请求。

    所以说,Zookeeper 不能保证服务可用性。

    2、Eureka

    Netflix我感觉应该是在酝酿更好的东西的,下面我们重点还是来介绍Ereka 1.x相关的设计。


    Eureka由两个组件组成:Eureka服务端Eureka客户端 。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。

    Eureka的基本架构,由3个角色组成:

    1、Eureka Server: 提供服务注册和发现功能;

    2、Service Provider 服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到;

    3、Service Consumer 服务消费方,从Eureka获取注册服务列表,从而能够消费服务


    Eureka 在设计时就紧遵AP 原则,Eureka Server 可以运行多个实例来构建集群,解决单点问题,实例之间通过彼此互相注册来提高可用性,是一种去中心化的架构,无 master/slave 之分,每一个实例 都是对等的,每个节点都可被视为其他节点的副本。

    在集群环境中如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点上,当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。

    当节点开始接受客户端请求时,所有的操作都会在节点间进行复制操作,将请求复制到该 Eureka Server 当前所知的其它所有节点中。

    当一个新的 Eureka Server 节点启动后,会首先尝试从邻近节点获取所有注册列表信息,并完成初始化。Eureka Server 通过 getEurekaServiceUrls() 方法获取所有的节点,并且会通过心跳契约的方式定期更新。

    默认情况下,如果 Eureka Server 在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Eureka Server 将会注销该实例(默认为90秒, eureka.instance.lease-expiration-duration-in-seconds 进行自定义配置)。

    当 Eureka Server 节点在短时间内丢失过多的心跳时,那么这个节点就会进入自我保护模式,这个测试环境的时候需要注意一下。

    Eureka的集群中,只要有一台Eureka还在,就能保证注册服务可用,只不过查到的信息可能不是最新的(不保证强一致性)。

    除此之外,Eureka还有一种自我保护机制,如果在15分钟 内超过85% 的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

    • Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务;
    • Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)
    • 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。

    3、Nacos

    Nacos 无缝支持一些主流的开源生态,如下图:

    Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。

    Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

    Nacos除了服务的注册发现之外,还支持动态配置服务 。动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。


    Nacos特点

    服务发现和服务健康监测

    Nacos 支持基于 DNS 和基于 RPC 的服务发现。服务提供者使用 原生SDK、OpenAPI、或一个独立的Agent TODO注册 Service 后,服务消费者可以使用DNS TODO 或HTTP&API查找和发现服务。

    Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层 (PING 或 TCP)和应用层 (如 HTTP、MySQL、用户自定义)的健康检查。对于复杂的云环境和网络拓扑环境中(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了 agent 上报模式和服务端主动检测2种健康检查模式。Nacos 还提供了统一的健康检查仪表盘,帮助您根据健康状态管理服务的可用性及流量。

    动态配置服务

    动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

    动态配置消除了配置变更时重新部署应用和服务的需要,让配置管理变得更加高效和敏捷。

    配置中心化管理让实现无状态服务变得更简单,让服务按需弹性扩展变得更容易。

    Nacos 提供了一个简洁易用的UI (控制台样例 Demo) 帮助您管理所有的服务和应用的配置。Nacos 还提供包括配置版本跟踪、金丝雀发布、一键回滚配置以及客户端配置更新状态跟踪在内的一系列开箱即用的配置管理特性,帮助您更安全地在生产环境中管理配置变更和降低配置变更带来的风险。

    动态 DNS 服务

    动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务。动态DNS服务还能让您更容易地实现以 DNS 协议为基础的服务发现,以帮助您消除耦合到厂商私有服务发现 API 上的风险。

    Nacos 提供了一些简单的 DNS APIs TODO 帮助您管理服务的关联域名和可用的 IP:PORT 列表.

    服务及其元数据管理

    Nacos 能让您从微服务平台建设的视角管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

    Nacos支持插件管理

    ** **

    关于Nacos数据的存储来说,支持临时也支持持久化。


    关于设计来说支持CP也支持AP,对他来说只是一个命令的切换,随你玩,还支持各种注册中心迁移到Nacos,反正一句话,只要你想要的他就有。

    4、Consul

    Consul是HashiCorp公司推出的开源工具,Consul由Go语言开发,部署起来非常容易,只需要极少的可执行程序和配置文件,具有绿色、轻量级的特点。Consul是分布式的、高可用的、 可横向扩展的用于实现分布式系统的服务发现与配置。

    Consul的特点

    服务发现(Service Discovery)

    Consul提供了通过DNS或者HTTP接口的方式来注册服务和发现服务。一些外部的服务通过Consul很容易的找到它所依赖的服务。

    健康检查(Health Checking)

    Consul的Client可以提供任意数量的健康检查,既可以与给定的服务相关联(“webserver是否返回200 OK”),也可以与本地节点相关联(“内存利用率是否低于90%”)。操作员可以使用这些信息来监视集群的健康状况,服务发现组件可以使用这些信息将流量从不健康的主机路由出去。

    Key/Value存储

    应用程序可以根据自己的需要使用Consul提供的Key/Value存储。Consul提供了简单易用的HTTP接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。

    安全服务通信

    Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。意图可用于定义允许哪些服务通信。服务分割可以很容易地进行管理,其目的是可以实时更改的,而不是使用复杂的网络拓扑和静态防火墙规则。

    多数据中心

    Consul支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。

    Consul支持多数据中心,在上图中有两个DataCenter,他们通过Internet互联,同时请注意为了提高通信效率,只有Server节点才加入跨数据中心的通信。

    在单个数据中心中,Consul分为Client和Server两种节点(所有的节点也被称为Agent),Server节点保存数据,Client负责健康检查及转发数据请求到Server;Server节点有一个Leader和多个Follower,Leader节点会将数据同步到Follower,Server的数量推荐是3个或者5个,在Leader挂掉的时候会启动选举机制产生一个新的Leader。

    集群内的Consul节点通过gossip协议(流言协议)维护成员关系,也就是说某个节点了解集群内现在还有哪些节点,这些节点是Client还是Server。单个数据中心的流言协议同时使用TCP和UDP通信,并且都使用8301端口。跨数据中心的流言协议也同时使用TCP和UDP通信,端口使用8302。

    集群内数据的读写请求既可以直接发到Server,也可以通过Client使用RPC转发到Server,请求最终会到达Leader节点,在允许数据延时的情况下,读请求也可以在普通的Server节点完成,集群内数据的读写和复制都是通过TCP的8300端口完成。

    Consul其实也可以在应用内进行注册,后续采用Spring Cloud全家桶这套做负载

    我们这里聊聊关于Consul的应用外的注册:


    上图主要多出来两个组件,分别是Registrator和Consul Template,接下来我们介绍下这两个组件如何结合可以实现在应用发进行服务发现和注册。

    Registrator :一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。

    Consul Template :定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息,达到动态调节负载均衡的目的。

    整体架构图可能是这样:

    我们用Registrator来监控每个Server的状态。当有新的Server启动的时候,Registrator会把它注册到Consul这个注册中心上。

    由于Consul Template已经订阅了该注册中心上的服务消息,此时Consul注册中心会将新的Server信息推送给Consul Template,Consul Template则会去修改nginx.conf的配置文件,然后让Nginx重新载入配置以达到自动修改负载均衡的目的。

    5、Kubernetes

    Kubernetes是一个轻便的和可扩展的开源平台,用于管理容器化应用和服务。通过Kubernetes能够进行应用的自动化部署和扩缩容。

    在Kubernetes中,会将组成应用的容器组合成一个逻辑单元以更易管理和发现。Kubernetes积累了作为Google生产环境运行工作负载15年的经验,并吸收了来自于社区的最佳想法和实践。

    Kubernetes经过这几年的快速发展,形成了一个大的生态环境,Google在2014年将Kubernetes作为开源项目。Kubernetes的关键特性包括:

    • 自动化装箱 :在不牺牲可用性的条件下,基于容器对资源的要求和约束自动部署容器。同时,为了提高利用率和节省更多资源,将关键和最佳工作量结合在一起。
    • 自愈能力 :当容器失败时,会对容器进行重启;当所部署的Node节点有问题时,会对容器进行重新部署和重新调度;当容器未通过监控检查时,会关闭此容器;直到容器正常运行时,才会对外提供服务。
    • 水平扩容 :通过简单的命令、用户界面或基于CPU的使用情况,能够对应用进行扩容和缩容。
    • 服务发现和负载均衡:开发者不需要使用额外的服务发现机制,就能够基于Kubernetes进行服务发现和负载均衡。
    • 自动发布和回滚 :Kubernetes能够程序化的发布应用和相关的配置。如果发布有问题,Kubernetes将能够回归发生的变更。
    • 保密和配置管理 :在不需要重新构建镜像的情况下,可以部署和更新保密和应用配置。
    • 存储编排 :自动挂接存储系统,这些存储系统可以来自于本地、公共云提供商(例如:GCP和AWS)、网络存储(例如:NFS、iSCSI、Gluster、Ceph、Cinder和Floker等)。

    Kubernetes属于主从分布式架构,主要由Master Node和Worker Node组成,以及包括客户端命令行工具Kubectl和其它附加项。

    Master Node :作为控制节点,对集群进行调度管理,Master主要由三部分构成:

    1. Api Server 相当于 K8S 的网关,所有的指令请求都必须经过 Api Server;
    2. Kubernetes调度器 ,使用调度算法,把请求资源调度到某个 Node 节点;
    3. Controller控制器 ,维护 K8S 资源对象(CRUD:添加、删除、更新、修改);
    4. ETCD存储资源对象 (可以服务注册、发现等等);

    Worker Node :作为真正的工作节点,运行业务应用的容器;Worker Node主要包含五部分:

    1. Docker是运行容器的基础环境,容器引擎;
    2. Kuberlet 执行在 Node 节点上的资源操作,Scheduler 把请求交给Api ,然后 Api Sever 再把信息指令数据存储在 ETCD 里,于是 Kuberlet 会扫描 ETCD 并获取指令请求,然后去执行;
    3. Kube-proxy是代理服务,起到负载均衡作用;
    4. Fluentd采集日志;
    5. Pod:Kubernetes 管理的基本单元(最小单元),Pod 内部是容器。Kubernetes 不直接管理容器,而是管理 Pod;

    6、总结

    1、高可用

    这几款开源产品都已经考虑如何搭建高可用集群,这个地方有些差别而已;

    2、关于CP还是AP的选择

    对于服务发现来说,针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不尽相同,也并不会造成灾难性的后果。

    但是对于服务消费者来说,如果因为注册中心的异常导致消费不能正常进行,对于系统来说是灾难性,因此我觉得对于注册中心选型应该关注可用性,而非一致性,所以我选择AP

    3、技术体系

    对于语言来说我们都是Java技术栈,从这点来说我们更倾向于Eureka、Nacos,但是Eureka已经停止维护了,因此我会选择Nacos。

    如果公司内部有专门的中间件或者运维团队的可以Consul、Kubernetes,毕竟Kubernetes才是未来,我们追求的就是框架内解决这些问题,不要涉及到应用内的业务开发,我们其实后者是有的,只是可能不能达到能自主研发程度,这样只能要求自己走的远一些。

    应用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系;应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景。

    关于Eureka、Nacos如何选择,这个选择就比较容易做了,那个让我做的事少,我就选择那个,显然Nacos帮我们做了更多的事。

    4、产品的活跃度

    这几款开源产品整体上都比较活跃


    1、灵魂一问:你的登录接口真的安全吗?

    2、HashMap 中这些设计,绝了~

    3、在 IntelliJ IDEA 中这样使用 Git,贼方便了!

    4、计算机时间到底是怎么来的?程序员必看的时间知识!

    5、这些IDEA的优化设置赶紧安排起来,效率提升杠杠的!

    6、21 款 yyds 的 IDEA插件

    7、真香!用 IDEA 神器看源码,效率真高!

    点分享

    点收藏

    点点赞

    点在看

    浏览 10
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    分享
    举报
    评论
    图片
    表情
    推荐
    点赞
    评论
    收藏
    分享

    手机扫一扫分享

    分享
    举报