为云量身定制您的服务

  相信大家都听说过Amazon的AWS。作为业内最为成熟的云服务提供商,其运行规模,稳定性,安全性都已经经过了市场的考验。时至今日,越来越多的应用被部署在了AWS之上。这其中不乏Zynga及Netflix这样著名的服务。

  然而这一切并没有停滞不前:AWS根据市场的变化提供了越来越多的内建服务,在降低了用户成本的同时更是提高了用户开发的效率。而且随着各个企业对云的兴趣的不断增加,网络上也出现了越来越多的有关如何正确高效地使用AWS的讨论。

  在这里,本文将会介绍一系列在云上创建服务所常常使用的一些方法及设计思路。当然,它们并不局限于AWS。您同样也可以将这里介绍的各种思路和准则借鉴到您部署在Azure上,阿里云上,或者其它云上的应用中。

  另外,本文不限于公有云,Openstack等私有云上的应用同样也可以借鉴本文中所提到的思路和部分功能。

 

认识云所带来的不同

  在介绍这些准则和最佳实践之前,我们首先要坐下来好好想一想,我们部署在云上的应用需要是什么样子的。在云的早期阶段,用户只能从它那里得到一系列虚拟机,以及一些非常有限的附加服务。在那个年代,在云上承载一个服务和通过物理机承载一个服务的确没有什么太大的不同。时至今日,云已经不限于为用户提供虚拟机这样单一的服务了。各个云所提供的众多附加功能使得用户可以非常灵活地对这些虚拟机进行操作及管理。

  这导致了最根本的两点不同:对虚拟机的处理方式以及对自动化功能的支持。

  在以往的基于物理机的服务中,每台物理机都是弥足珍贵的。因此服务中的每个服务实例都需要运维人员的细致照顾。一旦其中一个服务实例出现了问题,那么运维人员就需要通过各种方式尝试对该服务实例进行修复,并将重新恢复到健康状态的服务实例重新添加到整个服务中。

  基于云的解决方案则不需要这么麻烦。用户完全可以通过云在几分钟内重新部署一台具有完整功能的虚拟机,因此相应的解决方案也就变成了通过重新创建一台具有完整功能的虚拟机来替换出现问题的虚拟机。

  同样的,如果我们要增加服务实例,那么基于物理机的服务常常需要几个月的准备才能完成。而在云上,这个过程常常只需要几分钟。

  所以在这里,我们再强调一遍您在为云设计服务时需要牢牢记住的第一条准则,那就是:在云上创建虚拟机非常快速,我们要尽量利用这种特性。

  现在我们再来看第二点不同:对自动化功能的支持。我们知道,管理一个基于物理机的服务集群常常是一件非常麻烦的事情,尤以一系列常见的运维操作为甚。这些运维操作包括服务实例失效处理,添加/移除服务实例,更改服务实例设置等。反过来,市面上常见的云平台常常允许用户标示在特定事件发生时需要被触发的自动化脚本。如果用户能够通过这些脚本来控制在特定情况下需要执行哪些运维操作,那么对云上应用的管理就会简单得多,而且对各种状况的响应也会快速得多。

  好,让我们来强调第二点:尽量通过云平台所提供的自动化支持来完成我们所需要的功能。

  当然,这只是开发云上服务的两个最基本的准则。由于它们是如此常见,因此很多云服务提供商都已经将它们以附加功能的形式直接暴露给了用户。例如各公有云中常见的一个附加功能就是Auto Scaling。亚马逊的AWS和微软的Azure都提供了该功能,而私有云解决方案Openstack也有了一个提案。该功能会根据当前Auto Scaling所管理的各虚拟机实例的负载来自动调整其所包含的虚拟机数量,从而保证系统不会产生过载或系统不会有过多的冗余容量:

  而这一切对容量的控制都是自动完成的,基本不必由我们写任何的监控和控制逻辑。

  当然,这种功能还有很多,让我们在后面的章节中逐渐为您展示。

 

该好好使用的特色功能

  在当前业界内,无论是公有云平台还是私有云平台,都会提供一系列特色功能。对这些特色功能的使用常常需要我们在设计及实现服务时采取和以往不同的视角。因此在在本节中,我们将会介绍这些常见的特色功能以及有关它们的一系列使用经验。

 

AMI

  第一个要讨论的就是AMI。可以说,这是用户所最为熟知但是也最不容易被使用好的一个功能。AMI的全称是Amazon Machine Image,也即是在AWS上创建新的虚拟机时所需要使用的模板。其不仅仅可以包含虚拟机运行时所需要的操作系统,更可以包含一系列已经配置完毕的各个组件。一旦用户通过某个AMI创建了一台虚拟机,那么该虚拟机在创建完毕之后将直接拥有这些AMI上所预先配置好的各个组成。

  Openstack的Glance也提供了类似的功能。

  那么这些预先配置好的组成都有哪些呢?答案就是,它可以是您正常运行的虚拟机上所拥有的部分甚至所有组成。试想一下,要让一个虚拟机实例能够在集群中正常工作,其常常需要包含操作系统,Servlet Container,服务运行所需要的类库,服务的源码以及服务的配置等众多组成。一个AMI所包含的组成越全面,那么经由它所创建的虚拟机实例所需要配置,安装等工作的工作量就越少:

  从上图中可以看到,如果一个AMI中所包含的组成越全面,那么通过它来创建一个虚拟机实例所需要的时间就越短。熟悉高可用性等非功能性需求的读者可能会知道,这就意味着越短的恢复时间以及更高的安全系数。

  反过来,一个AMI所包含的组成越全面,那么它的更改就越为频繁。设想一下,如果一个AMI包括了服务运行所需要的源码,那么每次对源码的修改都需要我们重新制作AMI。这对于一个拥有几十个人甚至上百个人的开发团队来说就是一个灾难。因为制作AMI也是需要时间和精力的。

  那么一个AMI到底应该包含哪些组成呢?相信读者们自己也能估计得出:在服务的开发过程中,AMI所包含的内容将频繁地发生变化,因此此时AMI所包含的组成应该尽量的少。而在开发完成之后,AMI的变化将不再那么频繁了,因此为它创建一个包含较多固定组成的AMI则可以减少整个系统对各种情况的响应时间。

 

Instance Monitoring & Lifecycle Hooks

  在知道了这些AMI应该包含哪些组成之后,接下来我们要考虑的就是如何使用这些AMI了。在系统发生异常或者需要通过创建新的虚拟机实例来提高系统容量时,我们首先需要通过AMI来创建一个虚拟机实例,然后再在虚拟机实例创建完毕以后执行必要的安装及配置。仅仅拥有AMI的支持是不够的,我们还需要能够监控到这些事件并在虚拟机实例的各个生存期事件发生时对其进行处理,不是么?

  在以往的服务开发过程中,监控系统的设计很少被软件开发人员所重视。毕竟这绝大部分是运维人员所需要负责的事情。而在云上的应用中,所有这些事件都需要自动化起来。也就是说,与软件开发人员的距离更近了。

  AWS提供的监控系统是CloudWatch(Openstack似乎也有了一个提案)。其主要的工作原理就是:在AWS上的每种资源都会将其指标定时地保存在相应的Repository中。用户可以通过一系列API来读取这些指标,也可以通过这些API来保存一系列自定义指标。接下来用户就可以通过创建一个Alarm来对这些指标进行侦听。一旦发现这些指标达到了Alarm所标示的条件,那么CloudWatch就会将该Alarm发送到Amazon SNS或Auto Scaling Group之上:

  讲到这里估计您已经看出来了,监控是云应用的核心。如果没有将监控系统放在云应用的核心位置来考虑,那么我们就没有办法遵守本文刚开始时候所提到的“尽量通过云平台所提供的自动化支持来完成我们所需要的功能”这样一条准则。而某些AWS的附加功能已经为某些常用的组成抽象出了一系列生存期事件。

  例如在AWS的Auto Scaling功能中,我们可以通过其所包含的Lifecycle Hook来指定创建或移除一台虚拟机实例时所需要执行的用户自定义逻辑。让我们首先考虑一下Auto Scaling是如何与Cloud Watch协作处理如下图所示的负载峰值应对方案的:

  对于上图所示的容量变化过程,Auto Scaling与CloudWatch之间的互动将如下所示:

  1. Auto Scaling通过CloudWatch发现其所管理的虚拟机已经出现过载的情况,并通过创建新的虚拟机实例来开始扩容。
  2. 在新的虚拟机创建完毕后,Auto Scaling会通知CloudWatch执行预先定义好的对虚拟机进行配置的逻辑。在这些逻辑执行完成以后,我们就可以将该虚拟机的状态设置为可以正常工作了。
  3. 一旦Auto Scaling通过Cloud Watch所记录的状态发现其所拥有的虚拟机负载并不够多,那么缩减容量的操作就将开始了。整个过程与第1、2步所列出的步骤正好相反:在移除虚拟机之前,我们需要执行在CloudWatch上所定义好的虚拟机移除处理逻辑。一旦该逻辑执行完毕,那么该虚拟机将被正式地销毁。

  整个运行流程大致如下所示:

  而且在其它一些附加功能中,我们也常常会看到这种对自动化脚本的支持。有些附加功能直接提供了对自动化脚本的支持,如用户可以直接在OpsWorks中标示针对特定事件的执行逻辑。而在另一些组成中,就比如我们刚刚提到的Auto Scaling对自动化脚本的支持,则是需要多个组成协同配合来完成的。

  如果实在没有找到一个合适的解决您所需要的Hook,那么最终极的办法就是在制作您自己的AMI时在里面放一个Agent,以通过它来执行您的自定义逻辑,不是么?

 

虚拟机实例管理

  现在我们已经知道如何通过AMI快速地创建一台虚拟机,以及如何通过脚本来协调这些虚拟机之间的协同工作。但是这里还有一个问题,那就是,难道需要我们手动地一台台部署虚拟机,并逐个配置它们么?

                其实并不必要。针对这个需求,AWS为我们提供了三种不同的工具:CloudFormation,Beanstalk以及OpsWorks。

                先来看看最为简单但是灵活度也最高的CloudFormation。简单地说,软件开发人员只需要通过一个JSON格式的模板来描述所需要的所有种类的AWS资源,并将其推送到CloudFormation上即可。在接收到该模板之后,CloudFormation就会根据其所包含的内容来分配并配置资源。例如下面就是一段CloudFormation模板(来自于Amazon官方文档):

 1 {
 2     "Resources": {
 3         "Ec2Instance": {
 4             "Type": "AWS::EC2::Instance",
 5             "Properties": {
 6                 "SecurityGroups": [{
 7                     "Ref" : "InstanceSecurityGroup"
 8                 }],
 9                 "KeyName": "mykey",
10                 "ImageId": ""
11             }
12         },
13 
14         "InstanceSecurityGroup" : {
15             "Type": "AWS::EC2::SecurityGroup",
16             "Properties": {
17                 "GroupDescription": "Enable SSH access via port 22",
18                 "SecurityGroupIngress": [{
19                     "IpProtocol": "tcp",
20                     "FromPort": "22",
21                     "ToPort": "22",
22                     "CidrIp": "0.0.0.0/0"
23                 }]
24             }
25         }
26     }
27 }

  略为熟悉AWS的读者可能已经能够读懂上面的代码所描述的资源组合:创建一个名称为“Ec2Instance”的虚拟机实例,以及一个名称为“InstanceSecurityGroup”的Security Group。Ec2Instance实例将被置于InstanceSecurityGroup这个Security Group中。

  然而我们能做的不只是通过JSON文件来描述一些静态资源,更可以通过Conditions来指定条件,Fn:FindInMap等函数来执行特定逻辑,更可以通过cfn-init等helper script来完成软件安装,虚拟机配置等一系列动作。只不过CloudFormation更多地关注资源管理这一层面,因此用它对大型服务中的实例进行管理则会略显吃力。

  另一个工具,Beanstalk,则最适合于在项目的初期使用。在使用Beanstalk创建服务时,我们只需要上传该应用的Source Bundle,如WAR包,并提供一系列部署的信息即可。Beanstalk会帮助我们完成资源分配,服务部署,负载平衡,伸缩性以及服务实例的监控等一系列操作。如果需要对服务进行更新,我们只需要上传新版本的Source Bundle并指定新的配置即可。在部署完成以后,我们还可以通过一系列管理工具,如AWS Management Console,对这些应用进行管理。

  随着服务的规模逐渐增大,我们就需要使用更复杂一些的工具了,那就是OpsWorks。在OpsWorks中包含一个被称为Layer的概念。每个Layer包含一系列用于某一特定用途的EC2实例,如一系列数据库实例。而每个Layer则依赖于一系列Chef recipe来在特定生存期事件发生时执行相应的逻辑,如安装软件包,执行脚本等(没错,配置管理软件Chef)。这些事件有Setup,Configure,Deploy,Undeploy以及Shutdown等。

  在Openstack中,您可能需要考虑Heat。它在Openstack中是负责Cloud Orchestration的。

 

其它工具

  好,剩下的就是一些常用且容易用对,或者并不非常常用或适用于特定领域的功能了。例如我们可以通过Route 53所提供的功能实现基于DNS的负载平衡及灾难恢复解决方案,通过CloudFront为我们的应用添加一个CDN,通过EBS,S3,Glacier等不同种类的存储记录不同种类的数据等。

  鉴于本文的定位是一篇综述性质的文章,因此我们就不再花较大精力对它们进行介绍了。毕竟本文的目标就是让大家意识到云上服务和基于物理机上的服务之间的不同,并能够根据这些不同来以正确的方式思考如何搭建一个云上的服务。

  在我的计划中,后面还会有几篇和Amazon相关的文章。这些文章抑或是如何以更适合的方式满足服务的非功能性需求,要么就是对Amazon中的一些较为相似的功能的归纳总结,因此我们还有机会谈到它们。

 

创建云服务的最佳实践

  这部分是从我笔记中抽出来的。这些笔记很多都是大家在网络上讨论的总结,而且我也没有在笔记中逐个记明出处是在哪里,因此可能无法按照标准做法给出这些观点的原始出处。

  好,那我们开始。

 

考虑所有可能的失效

  我们知道,云下面有一层是虚拟化层。所以相较于直接运行于物理机上的服务而言,运行在云上的服务不仅仅需要面对物理机的失效,更需要面对虚拟化层的失效,甚至有时云平台上某些功能的失效也可能影响我们服务的运行。因此就云上的单个虚拟机而言,其发生失效的概率将远大于物理机。因此在云上应用所需要遵守的第一条守则就是:要假设所有的组成都有可能失效。

  这些失效可能存在于云上服务的任何地方,甚至我们都需要考虑云平台的数据中心失效的情况。就以AWS为例,其所提供的最基本的资源就是虚拟机。虚拟机之间彼此相互隔离。因此在一台虚拟机出现了问题的时候,其它虚拟机的运行也不会受到任何影响。而在虚拟机之上则是Availability Zone。Availability Zone在Region之内彼此隔离,因此如果其中的一个Availability Zone出现了问题,那么其它的Availability Zone中的虚拟机仍能够正常工作。而Region则是世界范围内的相互隔离。如果一个Region出现了问题,那么其它Region不会受到任何影响。在通常情况下,整个Region发生宕机的概率实际上是微乎其微的。

  但是AWS自身在今年内也出现过整个Region失效的情况。如果软件开发人员在实现部署在AWS上的服务时心存侥幸,认为Region宕机的概率很小,那么在相应的Region宕机时,该服务将无法为用户提供服务。有时候,这种服务中断是致命的。

  所以在实现一个需要承载于AWS上的服务时,我们必须要考虑:如果虚拟机出现了问题,我们的应用应该如何处理;如果Availability Zone出现了问题,我们又应该如何处理;如果整个Region都不能正常工作,那么我们又该如何处理?这些问题发生时,我们应该提供什么样的服务?又需要在多少时间内恢复?

  作为一个可选的解决方案,我们可以将一个服务部署在多个Availability Zone中,而且在不同的Region中拥有一个拷贝。这样在整个Region失效的情况下,用户仍然可以通过其它Region访问服务,只不过由于用户所访问的是离他较远的Region,因此整个服务的响应速度会显得有些慢。

  而在Region中的某个Availability Zone失效的情况下,其它Availability Zone中的拷贝将仍能够提供服务。因此对于用户而言,其基本不会受到很大的影响:

  除此之外,有些存储也可能达不到您的要求。例如,如果我没有记错的话,EBS存储的可靠率是99.99%,而S3的可靠率则是11个9。因此一种误用就是用EBS当做持久化存储,那么结果可想而知:数据丢失。

  其实不仅仅是针对于AWS。在其它云上运行的应用,无论是公有云,私有云,还是混合云,我们都需要在实现时就考虑如何避免这些层次上的失效。除非云平台自身已经为某些组成提供了高可用性保证。例如AWS的Route 53就是一个具有高可用性的DNS服务。

  除了这些可能的失效,我们还需要考虑如何处理这些失效。这常常和我们所提供服务的自身特性有关。如果在某些组成失效的时候,我们仍需要能够提供服务,那么我们就需要创建一个具有容错性的系统;如果某些关键组成失效,那么我们需要多少时间能够恢复到正常服务状态,又可能出现多少数据丢失等,都是由SLA来规定的。我们要做的,就是根据SLA的要求设计基于云上的具有容错性,高可用性的服务,以及数据的备份及恢复等方案。

  关于云上如何设计一个具有容错性的系统,以及如何执行数据的备份和恢复,我都会在其它文章中加以讲解。毕竟不同的需求会导致不同的解决方案。这也不是一句两句就能说清楚的。

  而我们只需要记住一点:假设云上服务的所有组成都有可能失效,除非云服务提供商声明了该组成的高可用性。

 

通过较小的服务换取较高的伸缩性

  对于一个在云上运行的服务而言,良好的伸缩性是它能够成功运行的一个基本条件。由于在云上创建一个服务实例常常只需要几分钟的时间,因此其所包含的服务实例个数常常会根据当前负载变化,甚至一天会变化很多次:

  市面上常见的云基本上都是根据服务所占用的资源数量来计费的。如果云上的服务被设计为一个不可分割的整体,那么我们就需要在某部分组成负载过重时对服务进行整体扩容。这使得其它的并不需要扩容的组成也同时进行了扩容,进而增加了对资源的不必要的占用:

  而且如果一个服务包含了太多的组成,那么它的启动时间也会受到一定的影响。反过来,如果云上应用的各个组成彼此相互独立,并能够独立地进行扩展,那么这个问题就将迎刃而解:

  除此之外,这些小服务之间的较好的隔离性也会将错误隔离在较小的范围内,进而提高了整个系统的稳定性。

  如果您需要更多地了解如何创建一个具有高扩展性的应用,请查看《服务的扩展性》一文。如果您更希望能了解如何在云上对服务进行切割,并有效地组织这些子服务的开发,请查看文章《Microservice简介》及《Microservice Anti-patterns》。

 

注意服务的切割粒度和方式

  我们刚刚提到,如果希望我们的服务能够在云上具有良好的伸缩性,那么我们就需要将它划分为较小的子服务。但是这也容易让一些读者走到另一个极端,那就是子服务的分割粒度太小,或者是在不适合的地方对服务进行了分割。

  服务的分割粒度太小常常会导致服务对单一请求的响应速度变慢。这在某些系统中将会变成非常严重的问题。试想一下,如果一个请求需要由多个服务实例处理,那么对该请求进行处理的过程就需要多次的信息传递:

  而如果将两个频繁交互的组成切割到了不同的子服务中,那么对请求进行响应的过程也常常需要更多的信息传递:

  从上面两个示例中可以看到,过细粒度的分割以及不合适的分割都会导致单次消息处理的流程变得更为复杂,也即是消息的处理时间变长。这对于那些对单一请求处理时间较为敏感的服务来说是非常不好的设计。

  而另一个与之相似的情况就是两个频繁沟通的子服务。如果在经过正常分割之后,两个子服务需要进行频繁地通信,那么我们就需要想办法让它们之间能够更高效地通信,如通过在AWS上购买Dedicated Host实例让两个子服务在同一台物理机上运行。

  也就是说,为了能让云上的服务能更为高效地响应用户的请求,我们同时需要考虑数据流的拓扑结构,并根据该拓扑结构优化云上服务的部署。

  当然了,对于一个基于消息的服务,每个子服务的划分主要是通过考察是否能够增加整个系统的吞吐量来决定的。如果您对基于消息的服务感兴趣,请查看《Enterprise Integration Pattern – 组成简介》一文。

 

可配置的自动化解决方案

  勿需质疑,一个云上的服务基本上都拥有自定义的负载周期。例如一个主要面向国内客户的服务,其负载高峰常常是在白天,而自凌晨之后其负载将一直处于一个较低的水平。而且该服务的负载可能会在某一个时间结点上迅速地增加,因此直到负载到达阈值才开始创建新的虚拟机实例是来不及的:

  除此之外,这些服务可能还需要在特定时期内作为某些服务的平台。在活动时间内,其负载将可能较其它时期的负载重得多。

  针对第一个问题,我们需要为这种负载定义一个周期性的伸缩计划。也就是说,对于一个AWS上的具有周期性负载变化的服务,我们常常需要使用Auto Scaling Group中的Scheduled Scaling计划。

  而在面对举办活动这种打破周期性的负载时,我们就需要一些额外的逻辑以保证我们的服务拥有宽裕的容量。也就是说,此时我们的Auto Scaling Group所使用的Scaling Plan处于一个特殊的状态之中。

  而这仅仅是处理有规律的负载所需要考虑的一些情况。而对于那些具有非规律负载的服务,对服务容量的管理会变得更为复杂。

  所有这一切都有一个前提,那就是我们需要能够将服务实例的伸缩自动化起来。在AWS中,这并不是太大的问题。Auto Scaling已经提供了足够的灵活度,以允许我们通过Scaling Plan等组成提供自定义的伸缩逻辑。但是对于某些平台,您可能就需要自行实现这些功能了。在此之上,您还需要让它能够通过用户所指定的配置以及负载规律进行伸缩。因此设计一个良好的解决方案并不是一个简单的事情。而这也是我在这里提起这些的原因。

  为什么我们要做这些呢?节省资金。其实将服务放到云上的最终极目标无非就是为了节省资金。在云上,我们可以快速地获取想要数量的服务实例,而不需要预先指定硬件的购置计划;在云上,我们的容量可以随时根据负载进行伸缩,而不需要购买远远超过日常需求的足以应付负载峰值的服务器。

  甚至在有些云平台中,同一种服务实例有多种不同的购买方式。这些购买方式的价格有时会相差很多。如果我们能够在自动化伸缩功能的支持下充分利用这些购买方式所提供的优惠,那么我们的云应用的运营成本将大大降低。

  例如在AWS中,我们有如下种类的虚拟机实例购买方式:

  • On-Demand:随时可以获得并按小时收费的虚拟机实例
  • Reserved:以优惠价格购买较长使用时间的虚拟机实例
  • Scheduled:在固定时间可以使用的虚拟机实例,价格较为便宜
  • Spot:以喊价方式获得的虚拟机实例,出价高的用户将获得该实例。因此较适合需要较多计算资源而并不那么紧迫的任务
  • Dedicated host:指定虚拟机在特定的硬件上运行。其价格较为高昂

  那么对于上面所展示的周期性的负载,我们就可以通过以下方式的购买组合来降低整个服务的运行成本:

  这种购买方法常常可以帮您节省1/3甚至以上的服务运行成本。

  除了能够完成服务容量的伸缩,更重要的是,这些自动化功能还可以在其它一系列解决方案中使用,如灾难恢复,服务的升级等。而我们的目标就是,您的服务基本上不再需要通过人为干预就能完成自动伸缩,灾难恢复等Day 2 Operation。这可以显著地降低服务的运营成本。

  但是这里有一点需要注意,那就是对日志的保护,尤其是发生故障的服务实例中的日志。因为这些日志常常记录了服务产生故障的原因,因此它们对于服务的开发人员非常有价值。因此在某些云上(我忘记了是哪个云还是哪个解决方案提供商)提供了对云上服务的日志进行分析并将异常日志进行保留的功能。如果您希望您的服务能够持续地改进,那么对日志的保护必不可少。

  总结起来,那就是,让您的服务的日常操作变成一个自动化流程,并基于这些自动化流程搭建您的日常运维解决方案,并通过它们来帮助您节省服务运行的开销。而且在自动化过程中,我们要注意日志的保护。

 

  好了,本文要讲的就只有这些了。对于文中所提到的一系列技术,我会在后续的文章中对它们进行较为详细地介绍。您只需要理解我们为什么要这样搭建云服务,为什么要遵守这些守则即可。这是后面一系列云服务解决方案文章的理论基础。

 

转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/5327584.html

商业转载请事先与我联系:silverfox715@sina.com

公众号一定帮忙别标成原创,因为协调起来太麻烦了。。。

posted @ 2016-03-28 01:21  loveis715  阅读(1596)  评论(0编辑  收藏  举报