一、问题背景
大部分的应用运维工作随着服务器数量和产品数量的增长而增加,而运维人数的不足导致单个运维人员所承担的工作任务较为繁重,同时运维工作的不标准、无自动化使得应用运维任务十分复杂,耗费的大量的人员成本、时间成本和沟通成本。
应用运维工作说白了大体可以分为两种情况:
1. 在某个或某些服务器上执行某个脚本或命令;
2. 将某个或某些文件传输到某个或某些特定的服务器的特定位置上。在服务器数量较少的情况下,可以通过ssh或scp命令实现上面两个操作;服务器数量较多的情况下,我们可以通过包装ssh或者使用批量ssh工具,如pssh,ansible等来解决问题,但这种方式大多数都是一次性的方式,无论使用方法以及后续跟踪来看都并不友好。
还有,由于一系列历史原因,现网的服务环境也较为繁杂,现网的服务器上的代码、配置、软件包、脚本等文件没有进行统一的版本管理与配置管理,比如某个产品的代码版本多种多样;现网的代码和文件几乎都是通过一台复制到另一台的方式来实现;由于代码版本、配置版本等问题导致的现网质量事件也并不在少数。
另外,应用运维也需要一个统一的资源管理系统,对现网的服务的数据进行业务维度的资源管理,系统运维的CMDB系统只在静态资源维度进行了管控,动态的业务数据等资源需要应用运维团队自行来管理。
因此,针对上面所述的各种问题,需要一个运维管控系统,来解决包括:资源管理、配置管理、任务管理、文件发布等一些列常用的运维跟踪,通过简单高效、自动化的方式将繁琐的应用运维工作通过管控系统来完成,即可以降低运维的难度,也可以提高运维的效率,同时可以提高运维操作的成功率,并实现运维任务的持续跟踪和管理,甚至在不远的将来可以实现移动运维。
二、功能结构
经过上述的分析和整理,我们将整个管控平台的功能细化到如下几个大功能,如图所示:
管控平台的功能
三、详细设计
这里围绕上面功能结构图中的4个大功能,进行详细的分析和设计,其中移动运维功能为附加功能,这里暂时不介绍。
1. 资源管理
先说资源管理,资源管理是一切后续自动化运维功能的前提,也是所有自动化功能的数据依赖。
资源管理的功能可以较为薄弱,但是对数据的要求比较高,可以基于系统运维的CMDB系统进行二次构建,主要的功能可以分为:
物理机资源管理:物理机资源管理功能,需要将CMDB中所有交付到应用运维的物理机资源进行重新整理,按照二级业务产品线进行管理,支持多种服务器状态(如部署中,备用池等等)标注。可以基于物理机资源管理系统进行服务器初始化管理操作,加快服务初始化部署工作的效率。物理机资源管理对于后续的配置管理和作业管理来说是最为重要的,是后续两个功能的数据基础。
管理虚机资源管理:所有的管理服务都部署在管理虚机上,因此我们也需要对管理虚机进行管理,管理虚机的数据和物理机资源管理一样,可以依赖系统运维的CMDB中的数据进行二次管理,功能和物理机资源管理类似,这里不再阐述。
虚拟资源管理:虚拟资源管理就是在每一台物理机上的虚拟机/业务DB的资源管理,可以基于业务管理数据库中的数据进行二次整合,或者通过数据采集上报的方式实现,尽可能的做到虚拟资源数据的有效性和状态一致性。
虚拟资源数据在进行后续的虚拟资源管理、虚拟资源迁移等功能上会有较大帮助,可以基于虚拟资源管理完成自动化的实例迁移工作,节省大量的手动实例迁移任务。另外虚拟资源管理对宕机恢复或机房、机架断电等问题也会有较大的帮助。
整个资源管理比较简单,可以优先完成物理机资源管理功能,再实现虚拟资源管理功能。
2. 作业管理
作业管理是应用运维管控系统的核心功能,也是应用运维工作中最经常使用到的功能,作业管理功能也可以分为如下子功能,如图所示:
下面将分别对上面几个子功能进行详细介绍:
(1) 脚本执行:
脚本执行和文件分发是整个作业功能的基础功能,其他的功能都是通过对着两个功能进行组装和装饰来实现的。
脚本执行的表现形式就是运维人员在页面中提交一个脚本,脚本建议支持shell和python两种形式,可以通过三种方式:
手动书写;也就是在页面上的编辑器中编写脚本;
上传脚本;也就是通过浏览器将本地的脚本上传到系统中;
克隆系统脚本;所谓的系统脚本就是运维人员通过上面两个步骤提交到系统中的脚本,可以分为基础系统脚本和用户系统脚本,基础系统脚本就是那些我们明确可能会多次执行的脚本,比如服务器初始化等脚本;用户系统脚本就是用户自定义的脚本,可以实现任何功能。
脚本的编写要依赖一定的语法规范,我们可以为shell和python语言提供基础的类包和函数库,同时所有的脚本的执行结果都会根据脚本的exit的
$? 值来判断,$? 值为0,则表示该脚本执行成功,若为其他值则表示脚本执行失败。脚本的输出内容会存储到数据库中用于后续的问题跟踪和排查,因此需要运维人员写脚本的时候注意写上详细的日志内容。
脚本支持输入参数。用户可以在页面中的参数一栏输入执行参数,当然也可以直接写到脚本里。
支持选择执行账户。目前大部分服务的运行账户仍然是root用户,后续可能会改成非root用户,所以这里支持选择执行账户,具体的账户名称,可以在用户管理的功能中维护。
支持选择所执行的服务器。用户在选择好相应的脚本之后,需要指定在哪些服务器上执行该脚本,因此需要支持选择所需的服务器。这里面服务器选择分为两种模式:
本地执行模式;本地模式指,该脚本的执行并不会到远程服务器去执行,而是在管理服务所在的本地执行,这种模式可以用来判断远程服务器是否启动等功能,或是某些只能在管理服务器上执行的命令;
远程服务器执行模式;远程服务器执行模式就是比较常规的将脚本发送到远程服务器上,并在远程服务器上执行的模式,这种模式需要选择所需要执行服务器列表,这里就需要依靠资源管理系统中的物理机管理功能了,可以通过某种方式对服务器进行手动分组或自动分组,然后执行的时候可以按照不同的维度(比如按ip选择,按机房选择,按业务选择,或按自定义组名选择)选择所需的服务器组,进而将脚本下发到这些服务器上执行。
总结一下,脚本执行功能比较基础,同时还包含三个子功能,这三个子功能放到配置管理里面去实现,后面会具体阐述:
脚本管理功能;用于管理用户上传的脚本和常用的基础脚本。
账户管理功能;用于管理具体的执行用户。
服务器分组功能;用于基于资源管理系统的物理机管理数据按照某种方式划分成一定的服务器组。
说完具体的功能之后,我们需要确定具体的实现方式,准确的说,实现方式有两种:
1)SSH远程执行;可以通过封装ssh和paramiko的方式实现,python中的ansible和fabric包也都支持远程SSH执行的功能。Shell就用ssh命令即可。
该方式对中心节点要求比较高,每一个远程执行命令都会在中心节点启动一个进程(线程)来执行,对中心机的压力较大,同时由于是基于SSH的原理,故命令执行的效率较差,命令执行时间长。
2)Agent执行;在每台服务器上部署一个Agent,用户执行管控系统发送的脚本。这种方式的优点是可以采用拉的方式,由各个子节点订阅分给自己的任务,执行完成后再讲结果推给中心节点的数据;当然也可以像SSH方式通过推的方式由中心节点将任务发给远程的Agent,然后Agent去执行,并将结果反馈给中心节点。这种方式的执行效率会比较高,可扩展性强,将来甚至可以通过该Agent进行配置或运行数据上报,对于配置管理也比较方便,缺点是需要开发Agent,Agent需要支持异步非阻塞模式,且需要维护每台服务器的Agent,包括部署和管理。
Python的saltstack软件就是通过Agent的方式实现的,需要在每台物理机上部署一个saltstack
minion进程用于接受任务和执行任务,在每个中心机上部署saltstack master进程用于下发任务,saltstack
master通过将任务下发到zeromq中从而实现各个minion获取任务并执行,同时saltstack也支持将任务执行结果存储到Redis等数据库中,便于后续的跟踪和查看。
不过Agent也可以通过自行开发的方式实现,线上所有的Agent大多都是这种原理,也都是自己实现的,不过具有一定的开发难度。
因此对于脚本执行的实现方式来看总结如下:
如果用SSH的方式,使用python的ansible api或fabric。
如果用Agent的方式,可以考虑使用Python的saltstack minion Agent。
如果用自研Agent的方式,可以使用golang。上面两种方式相对快速容易实现,这种方式会有一点的时间消耗,不过可以积累一定的开发经验。
(2) 文件分发
文件分发是除了脚本执行的另一个基础功能,该功能的技术描述就是将某台服务器上的某个或某些文件,传输到其他的某个或某些服务器的某个位置上。
文件分发功能分为以下两个子功能,相关文件和模板的管理工作放到配置管理中去实现,这里只介绍如何实现文件分发:
普通文件分发,如代码包,镜像文件,软件包等。
模板文件分发,如配置文件等。
模板文件分发与普通文件分发不同的区别是模板文件中存储变量替换的逻辑,这里普通文件的管理和模板文件的管理以及配置变量的管理都放到配置管理系统中去实现,用户选择文件下发的时候可以选择是普通文件还是模板文件,如果是模板文件就会自动去加载配置管理中的变量管理生成临时的普通文件并下发。
具体的下发方式也分为两种:
1)管理机文件下发到普通的业务物理机上。
其实大部分的文件分发场景应该都是这一种,也就是我们将基础文件或模板文件放到管理机上,作为我们的基准文件,比如我们的部署代码,软件包等等,我们只要保证管理机上的文件版本的一致性,就可以保证普通的业务物理机上面的文件和管理机上的文件一致,也就是配置管理功能的中的文件版本一致性管理。
这种模式下的文件可能相对容量较小,比如小文件(几k),软件包(几M),或者稍大文件(几G),文件从管理机下发到某些业务物理机上面临的问题是并发导致的传输速度问题,小文件情况下还可以忽略,基本可以在秒级完成,但稍大文件的传输就会有时间问题,随着业务物理机的个数增加而增加,因此这种情况会有并发和流量的控制。
另外文件的传输会提前进行文件md5的对比判断,如果文件的md5值相同,那就没有再传输文件的必要,在稍大文件情况下可以节省较多的时间。
文件传输,可以使用ansible的copy模块,或者salt的文件file模块,当然也可以自研方式中封装rsync的方式,rsync支持文件md5值校验以及断点续传等方式。
2)物理机之间的文件互传。
物理机之间的文件互传准确的来说是反配置管理逻辑的,这种方式就和我们现在的文件传输方式很类似,也就是从一台服务器scp文件到另一台服务器上。对于配置文件和小文件、软件包等需要配置管理的文件来说,我们需要明确禁止使用这种方式。但这种方式的好处就是可以用来P2P的传输较大的文件,比如几百G的镜像文件。
对于几百G的镜像文件来说,如果使用第一种管理机管理的方式,只能1对多的传输将会造成传输时间的极大占用,而使用这种P2P的方式,即可以极大的提升文件传输的速度。
物理机之间的文件互传使用rsync即可,由于文件较大,且可能出现断点,rsync是比较方便的解决大文件传输的方法,需要在每台服务器上的Agent上对rsync功能进行封装,支持Agent之间的rsync文件互传。
物理机文件中需要管理的文件列表,可以通过Agent汇报到配置管理系统中,用户选择文件分发的时候,可以选择配置管理系统中的点对点大文件传输功能,配置管理系统中可以自动选择传输源,针对被传输物理机的个数,实现真正的点对点传输,不需要用户自己去选择源文件服务器,可以节省大量的人为运维操作。
(3) 批量执行:
所谓的批量执行功能就是上面两个基础功能中在选择目的服务器的个数的时候支持多个服务器,而不是只能选择单个服务器。
在使用SSH方式情况下并发的速度会较差,可能控制在单机30-50个任务同时进行。
在使用Agent的方式情况下,任务本身是异步下发的,可以实现同时1000+任务同时进行。
由于服务器数量较多,且分机房的模式,整个后端架构会是与目前的UCloud后端架构类似的分布式架构,一个中心任务下发节点,每个机房一个任务中转节点,每台服务器一个Agent(或者中转节点SSH的方式)。
(4) 定制任务:
所谓的定制任务,就是可以通过任务组装的方式,将脚本执行和文件分发这两个主要功能(可以根据需要添加其他的功能,比如重启后的服务器ping探测,端口探测等),将上面的这些功能,按照1、2、3、4、5的方式组装,具体的支持功能如下:
支持使用基础功能模块的子任务组装成完成任务
支持设置每个子任务的子步骤的执行控制。比如失败后停止,完成后停止等待确认,忽略失败等功能。
支持每个子任务的执行服务器ip修改。
定制任务适用于某些使用频次较高的任务,比如服务器初始化上线,代码版本发布,软件包更新等操作。
(5) 定时任务:
定时任务是对普通的基础任务以及定制任务实现定时执行的功能,该功能的实现也相对简单,即按照crontab的分时日月周的语法,支持周期性或者定时性,也可以按照手机创建闹铃的方式选择触发时间和触发频率。
具体的实现方式如下:
用户输入的时间计划通过转换后写到管理机的crontab服务中。
每次有定时任务产生或删除或修改后生成新的计划任务。
计划任务中写明具体的执行命令
通过脚本调度API的方式实现任务的定时执行。
(6) 灰度计划:
对于非周期性重复执行的任务,都可以加入灰度计划功能,该功能又结合定时任务实现。
灰度计划可以有两个维度体现:
定制任务中的每个子任务的子步骤都可以实现灰度,即上面所说的完成后暂停,或失败后暂停。
物理机个数维度,并发执行的时候支持自定义灰度计划,比如:任务先执行1台服务器,成功后隔多久后可以执行第二批服务器,再间隔一段时间再执行第三批任务。同时可以设置任务失败条件,比如:任何一台服务器失败,则整个任务失败;或当失败服务器的数量达到灰度计划服务器数量的百分比后整个任务放弃执行。
上面这些是作业管理系统的主要功能,当然想要实现整个作业管理系统还会有一些其他的子功能,比如:
任务结果记录跟踪系统。
任务重做,重做任务子步骤是否重做控制。
脚本内容安全审核功能。
其他暂时未想到功能。
3. 配置管理
配置管理功能是整个管控功能中十分重要的一环,上面的作业管理系统中很多功能都依赖配置管理系统才能实现。
配置管理整个功能经过梳理之后如下图所示:
下面将依次介绍上面所述的功能:
(1) 账户管理:
所谓的账户管理十分简单,就是服务运行的时候所使用的用户,目前线上的服务大多都是root用户,但是有改成业务用户的趋势,故需要有一个账户管理功能。运维人员在使用作业管理的时候,选择相应的用户进行任务执行。
该功能可以发展成用户权限管理系统,即在该系统中控制每一个用户的系统权限。生成相应的权限配置文件,并下发到所有服务器上。不过用户管理的功能可能系统运维已经管理,这里就不再叙述了。
(2) 文件管理:
文件管理功能比较基础,其实主要是对管理机上的重要文件进行管理,比如代码版本,脚本文件,软件包文件等等,文件类型包括:普通文件,文件夹,压缩文件和模板文件。
文件管理中,用户按照一定的规范将所需要的文件放到管理机上的指定位置,并将相应的路径配置在数据库中。
文件管理主要用在作业管理中进行文件分发的基础文件依赖。
(3) 脚本管理:
脚本管理也是文件管理中的一种,但需要单独管理,脚本是作业管理系统的基础依赖。脚本分为基础系统脚本和用户自定义脚本两种。
所谓的基础系统脚本就是经过严格认证过的,可以重复多次使用的基础功能脚本。而用户自定义脚本指用户通过上传或是通过web
编辑器提交的脚本文件,存储下来用户后续可能的重复使用。
基础系统脚本和用户自定义脚本都会存储在管理机的固定位置上,在文件管理中进行管理,用户在使用的时候选择对应的脚本名称即可。
脚本也支持添加标签,方便记忆和查找。
(4) 分组管理:
分组管理也是基础功能之一,在作业系统中脚本执行和文件分发都需要有明确的服务器ip地址,而分组管理就是将这些ip地址按照特定的用户自定义的方式来命名和分类,支持添加标签,用户在选择服务器的时候可以通过搜索的方式选择到一组服务器中的一个或几个。
分组管理依赖于资源管理系统,以资源管理系统的物理机资源(管理服务是虚拟机资源)为依据,用户选择一部分服务器并进行命名或添加标签从而得到一个新的分组。
分组可以分为静态分组和动态分组两种。
所谓的静态分组指的是确定完分组后,如需变化必须手动添加或修改的分组。这类分组会占很大一部分。
而动态分组指的是可以通过对服务器资源的某种匹配而自动将其添加到某个分组中的功能,比如说某个机房新上线N台服务器,可以不需要手动,而自动的添加到某个组中。这种功能可以通过手动来实现,在技术和时间限制的情况下,可以暂时不考虑。
分组功能还有组优先级的概念,主要是结合变量管理、模板管理结合使用,这些在变量管理和模板管理里面再进行介绍。
(5) 变量管理:
变量管理主要是配合分组管理和模板管理使用,所谓的模板就是在不同的情况层显不同内容的文件,而如何呈现不同内容就是在模板中配置了相关的变量名称,而变量的具体赋值则需要在变量管理里面体现。
变量管理建议使用yml语法格式来编写,比如如下的格式:
region:
id: 7001
name: js
mongodb:
hosts:
- 172.27.117.201
- 172.27.117.202
- 172.27.117.203
port: 27017 |
编写完成后解析成格式化的数据,比如下面表格:
key value region.id
7001 region.name js region.mongodb.hosts [‘172.27.117.201’,’172.27.117.202’,’172.27.117.203’]
region.mongodb.port 27017 |
用户可以选择两种可视化方式,但使用的时候需要按照解析后格式化的方式来使用,比如用户在模板中需要使用某个region的mongodb的服务器列表,则需要这种方式来使用:region.mongodb.hosts
, 如果需要进一步处理该数据,可以在模板文件中直接使用Python语法处理,这个在模板管理中再介绍。
之所以需要有变量管理,就是因为模板管理的存在,很多配置需要按照不同的环境生成,而这些环境就需要有变量管理这个功能来控制。
变量管理细分可以分为几个维度:
a. 全局变量
全局变量指的是那些在任何场景下都只有一个的变量,可能会改变,但全局只有这一个变量,它一变会影响到所有的使用它的模板。
b. 组变量
组变量是指和分组管理结合后对某个分组设置的变量,组变量的使用场合比较多,而且不同的分组之间可能还有优先级的问题,因此需要在分组的时候设置组的优先级。
建议将组的优先级分为如下几类:
全局组(级别最低)
IDC组(级别次之)
Set组(级别次之)
自定义组(级别次之)
c. 主机变量
主机变量是主机级别的动态变量,具有较高的优先级,建议结合资源管理系统使用,可以给每一台物理主机(虚拟主机)都设置一组相应的动态变量,比如某个set的某些mongodb集群,需要区分主、从,设置的变量可以这个样子:
172.27.117.252
id=0 priority=2 arbiterOnly=False
172.27.117.248 id=1 priority=1 arbiterOnly=False
172.27.117.247 id=2 priority=0 arbiterOnly=True
|
在服务部署的时候可以根据相应的优先级决定生成的配置文件的不同,甚至执行脚本的不同。
d. 任务变量
任务变量是具有最高优先级的变量,这个变量只有任务执行的时候,通过输入的参数来控制,该变量实际并不进行管理,使用用户在使用的时候输入而已,一次性的操作。
(6) 模板管理
模板管理就是管理各种配置文件的管理系统。
配置文件之所以需要管理,是因为两个原因:
在不同的环境中配置文件的内容可能是不同的
配置文件中的某些内容可能是会被修改的
我们以下面的配置文件为例,分别说说这两种情况下使用模板管理的必要性:
[common]
region = {{ region.id }}
set = 1
instance = 1
.
.
.
[network]
listen-ip = {{ inventory_hostname }}
file_ulimit = {{ global.file_ulimit }} |
上面配置文件中的 {{ region.id }}以及{{ inventory_hostname }}说的就是第一种情况,
而{{ global.file_path }}就是第二种情况。
第一种情况中,在不同的region之间,文件的格式不变,但region.id的值是有变化的;inventory_hostname这里表示的是某个服务器的ip地址,这个在服务器级别都是变化的。因此这类文件需要是需要进行模板管理的。
第二种情况中,所有的file_ulimit都是一样的,那我们为什么不写死在文件里而把它变成变量呢?是因为这个配置,虽然现在没有变化,但将来可能会发生变化,在变量管理中直接修改一下,那新的配置文件就都会按照这个生成了,比起去改一个文件内容还有可能产生格式错误的风险来说,这种方式是不是简单多了。
至于模板文件如何编写,这里将会使用python的最通用的模板引擎jinja2引擎,所有的语法都必须遵循jinja2的引擎即可,变量使用变量管理中定义的变量,对于每一台主机都是在使用的时候动态生成最新的临时文件,并通过文件分发的方式传输就可以了。
(7) 软件管理
所谓的软件管理,也就是软件包的管理,软件包的管理有两种:
rpm或yum,npm,pip等安装的软件包,具有明确的包管理工具。
压缩包或目录格式的代码版本。
具有软件包管理工具的代码,比较容易进行管理,只要通过每台服务器自动的Agent定期执行list操作将所需要跟踪的软件包的版本进行跟踪,并汇报到中心管理数据库即可。
而压缩包或目录格式的代码版本则比较麻烦,需要对比MD5值,以及具有参照样本才可以管理。
这些所有的软件包都需要有一个最新可用的全局版本管理,用于进行版本对比操作,甚至可以直接点击升级。
总之,最终的软件管理的结果会呈现如下的形态,以某台服务器为例:
服务器ip: xxx.xxx.xxx.xxx
| 软件包名称| 版本号 | 是否是最新可用版本| 点击升级
| :—-|:—-|
| nodejs| v0.1.1| 是|
| libvirt| x.x.x| 是|
| zookeeper| 3.3.5| 否| 升级 |
当然有了软件管理之后,当我们有某种类似如: 将某些服务器上面的某个软件包升级! 这样的问题的时候,无论是获取基准ip列表,还是后续的升级工作,都十分简单了。
当然版本升级工作会和作业管理相结合,每个版本升级都会是一个单独的作业,这样版本升级的进度,结果也都一目了然,而且还可以做到灰度。
(8) 服务管理
服务管理有点类似监控工具,它所层显的状态和版本管理类似,实现的方式也类似,都是通过Agent定时上报的机制获取最新的数据。
所谓的服务管理,就是讲每一台服务器上所运行这些进程进行管理,当然不是全部进程,而是我们所关注的服务进程,呈现的状态如:
以服务器:xxx.xx.xxx.xxx为例:
| 进程id| 进程名称| 启动时间| 检查时间|运行时间 | 运行用户| 运行状态| 操作
|:—-:
| 1234| uhost-action| 2016-2-07 10:00:00| 2016-2-17
10:00:00 | 10day | root|运行中 | 重启/停止
| 2345| uimage3-action| 2016-2-07 10:00:00 |2016-2-17
10:00:00| 8day | root|已停止 | 启动 |
上面是某台服务器上的服务管理的实时情况,每一个任务都可以有详细的跟踪记录,可以用于问题跟踪,服务报警,dashborad展现等等。
另外服务管理可以和作业管理相结合,实现服务的重启,停止,启动等功能。
服务管理功能也是很有用的功能。
到此位置,整个配置管理工具的功能介绍也完成了,具体的实现方式可以有两种实现方式:
1) 基于过程的实现方式(主动)
基于过程的实现方式,就是所说的的配置变化,通过中心触发的方式实现,服务器上所有的配置变化都是主动的而不是被动的,可以通过SSH的方式,也可以通过Agent的方式来实现。可以使用Ansible、fabirc的API或者salt的daemon,也可以自研daemon的方式实现。
2) 基于结果的实现方式(被动)
基于结果的实现方式是指整个操作并不关心过程,也不是主动触发的,而是被动触发的,这种方式是基于Agent的实现方式。
运维人员对某台服务器的某个配置设置一个最终的状态,Agent获取这个最终状态后执行相应的操作,只要Agent满足条件需求,那么这台服务器最终所呈现的结果就是我们配置的结果,Puppet就是这种理念设计的,当然也可以自研。
四、总结
整个运维管控系统还是比较大的系统,每一个子系统的功能的很复杂,而且还需要结合使用,整体的架构是分布式的,多种开源软件与自研系统结合的方式实现,大体功能和架构如上面所述,具体的实现上肯定会有很多细节需要攻克的。
PS: 这个功能和架构设计参考了腾讯的蓝鲸系统,并结合了Ansible、saltstack和puppet的理念,综合而成,而且其中的部分功能已经实现完成。
|