共计 9725 个字符,预计需要花费 25 分钟才能阅读完成。
敏捷之歌
我抽数故我存在 | DBus
人人玩转流处理 | Wormhole
就当吾是数据库 | Moonbox
颜值最后十公里 | Davinci
导读:实时数据平台(RTDP,Real-time Data Platform)是一个重要且常见的大数据基础设施平台。在上篇(设计篇)中,我们从现代数仓架构角度和典型数据处理角度介绍了 RTDP,并探讨了 RTDP 的整体设计架构。本文作为下篇(技术篇),则是从技术角度入手,介绍 RTDP 的技术选型和相关组件,探讨适用不同应用场景的相关模式。RTDP 的敏捷之路就此展开~
拓展阅读:以企业级实时数据平台为例,了解何为敏捷大数据
如何设计实时数据平台(设计篇)
一、技术选型介绍
在设计篇中,我们给出了 RTDP 的一个整体架构设计(图 1)。在技术篇里,我们则会推荐整体技术组件选型;对每个技术组件做出简单介绍,尤其对我们抽象并实现的四个技术平台(统一数据采集平台、统一流式处理平台、统一计算服务平台、统一数据可视化平台)着重介绍设计思路;对 Pipeline 端到端切面话题进行探讨,包括功能整合、数据管理、数据安全等。
图 1 RTDP 架构
1.1 整体技术选型
图 2 整体技术选型
首先,我们简要解读一下图 2:
- 数据源、客户端,列举了大多数数据应用项目的常用数据源类型。
- 数据总线平台 DBus,作为统一数据采集平台,负责对接各种数据源。DBus 将数据以增量或全量方式抽取出来,并进行一些常规数据处理,最后将处理后的消息发布在 Kafka 上。
- 分布式消息系统 Kafka,以分布式、高可用、高吞吐、可发布 - 订阅等能力,连接消息的生产者和消费者。
- 流式处理平台 Wormhole,作为统一流式处理平台,负责流上处理和对接各种数据目标存储。Wormhole 从 Kafka 消费消息,支持流上配置 SQL 方式实现流上数据处理逻辑,并支持配置化方式将数据以最终一致性(幂等)效果落入不同数据目标存储(Sink)中。
- 在数据计算存储层,RTDP 架构选择开放技术组件选型,用户可以根据实际数据特性、计算模式、访问模式、数据量等信息选择合适的存储,解决具体数据项目问题。RTDP 还支持同时选择多个不同数据存储,从而更灵活的支持不同项目需求。
- 计算服务平台 Moonbox,作为统一计算服务平台,对异构数据存储端负责整合、计算下推优化、异构数据存储混算等(数据虚拟化技术),对数据展示和交互端负责收口统一元数据查询、统一数据计算和下发、统一数据查询语言(SQL)、统一数据服务接口等。
- 可视应用平台 Davinci,作为统一数据可视化平台,以配置化方式支持各种数据可视化和交互需求,并可以整合其他数据应用以提供数据可视化部分需求解决方案,另外还支持不同数据从业人员在平台上协作完成各项日常数据应用。其他数据终端消费系统如数据开发平台 Zeppelin、数据算法平台 Jupyter 等在本文不做介绍。
- 切面话题如数据管理、数据安全、开发运维、驱动引擎,可以通过对接 DBus、Wormhole、Moonbox、Davinci 的服务接口进行整合和二次开发,以支持端到端管控和治理需求。
下面我们会进一步细化上图涉及到的技术组件和切面话题,介绍技术组件的功能特性,着重讲解我们自研技术组件的设计思想,并对切面话题展开讨论。
1.2 技术组件介绍
1.2.1 数据总线平台 DBus
图 3 RTDP 架构之 DBus
1.2.1.1 DBus 设计思想
1)从外部角度看待设计思想
- 负责对接不同的数据源,实时抽取出增量数据,对于数据库会采用操作日志抽取方式,对于日志类型支持与多种 Agent 对接。
- 将所有消息以统一的 UMS 消息格式发布在 Kafka 上,UMS 是一种标准化的自带元数据信息的 JSON 格式,通过统一 UMS 实现逻辑消息与物理 Kafka Topic 解耦,使得同一 Topic 可以流转多个 UMS 消息表。
- 支持数据库的全量数据拉取,并且和增量数据统一融合成 UMS 消息,对下游消费透明无感知。
2)从内部角度看待设计思想
- 基于 Storm 计算引擎进行数据格式化,确保消息端到端延迟最低。
- 对不同数据源数据进行标准化格式化,生成 UMS 信息,其中包括:
✔ 生成每条消息的唯一单调递增 id,对应系统字段 ums_id_
✔ 确认每条消息的事件时间戳(event timestamp),对应系统字段 ums_ts_
✔ 确认每条消息的操作模式(增删改,或 insert only),对应系统字段 ums_op_
- 对数据库表结构变更实时感知并采用版本号进行管理,确保下游消费时明确上游元数据变化。
- 在投放 Kafka 时确保消息强有序(非绝对有序)和 at least once 语义。
- 通过心跳表机制确保消息端到端探活感知。
1.2.1.2 DBus 功能特性
- 支持配置化全量数据拉取
- 支持配置化增量数据拉取
- 支持配置化在线格式化日志
- 支持可视化监控预警
- 支持配置化多租户安全管控
- 支持分表数据汇集成单逻辑表
1.2.1.3 DBus 技术架构
图 4 DBus 数据流转架构图
更多 DBus 技术细节和用户界面,可以参看:
GitHub:https://github.com/BriData
1.2.2 分布式消息系统 Kafka
Kafka 已经成为事实标准的大数据流式处理分布式消息系统,当然 Kafka 在不断的扩展和完善,现在也具备了一定的存储能力和流式处理能力。关于 Kafka 本身的功能和技术已经有很多文章信息可以查阅,本文不再详述 Kafka 的自身能力。
这里我们具体探讨 Kafka 上消息元数据管理(Metadata Management)和模式演变(Schema Evolution)的话题。
图 5
图片来源:http://cloudurable.com/images…
图 5 显示,在 Kafka 背后的 Confluent 公司解决方案中,引入了一个元数据管理组件:Schema Registry。这个组件主要负责管理在 Kafka 上流转消息的 元数据信息和 Topic 信息,并提供一系列元数据管理服务。之所以要引入这样一个组件,是为了 Kafka 的消费方能够了解不同 Topic 上流转的是哪些数据,以及数据的元数据信息,并进行有效的解析消费。
任何数据流转链路,不管是在什么系统上流转,都会存在这段数据链路的元数据管理问题,Kafka 也不例外。Schema Registry 是一种中心化的 Kafka 数据链路元数据管理解决方案,并且基于 Schema Registry,Confluent 提供了相应的 Kafka 数据安全机制和模式演变机制。
更多关于 Schema Registry 的介绍,可以参看:
Kafka Tutorial:Kafka, Avro Serialization and the Schema Registry
http://cloudurable.com/blog/k…
那么在 RTDP 架构中,如何解决 Kafka 消息元数据管理和模式演变问题呢?
1.2.2.1 元数据管理(Metadata Management)
- DBus 会自动将实时感知的数据库元数据变化记录下来并提供服务
- DBus 会自动将在线格式化的日志元数据信息记录下来并提供服务
- DBus 会发布在 Kafka 上发布统一 UMS 消息,UMS 本身自带消息元数据信息,因此下游消费时无需调用中心化元数据服务,可以直接从 UMS 消息里拿到数据的元数据信息
1.2.2.2 模式演变(Schema Evolution)
- UMS 消息会自带 Schema 的 Namespace 信息,Namespace 是一个 7 层定位字符串,可以唯一定位任何表的任何生命周期,相当于数据表的 IP 地址,形式如下:
[Datastore].[Datastore Instance].[Database].[Table].[TableVersion].[Database Partition].[Table Partition]
例:oracle.oracle01.db1.table1.v2.dbpar01.tablepar01
其中 [Table Version] 代表了这张表的某个 Schema 的版本号,如果数据源是数据库,那么这个版本号是由 DBus 自动维护的。
- 在 RTDP 架构中,Kafka 的下游是由 Wormhole 消费的,Wormhole 在消费 UMS 时,会将 [TableVersion] 作为 * 处理,意味着当某表上游 Schema 变更时,Version 会自动升号,但 Wormhole 会无视这个 Version 变化,将会消费此表所有版本的增量 / 全量数据,那么 Wormhole 如何做到兼容性模式演变支持呢?在 Wormhole 里可以配置流上处理 SQL 和输出字段,当上游 Schema 变更是一种“兼容性变更”(指增加字段,或者修改扩大字段类型等)时,是不会影响到 Wormhole SQL 正确执行的。当上游发生非兼容性变更时,Wormhole 会报错,这时就需要人工介入对新 Schema 的逻辑进行修复。
由上文可以看出,Schema Registry 和 DBus+UMS 是两种不同的解决元数据管理和模式演变的设计思路,两者各有优势和劣势,可以参考表 1 的简单比较。
表 1 Schema Registry 与 DBus+UMS 对比
这里给出一个 UMS 的例子:
图 6 UMS 消息举例
1.2.3 流式处理平台 Wormhole
图 7 RTDP 架构之 Wormhole
1.2.3.1 Wormhole 设计思想
1)从外部角度看待设计思想
- 消费来自 Kafka 的 UMS 消息和自定义 JSON 消息
- 负责对接不同的数据目标存储 (Sink),并通过幂等逻辑实现 Sink 的最终一致性
- 支持配置 SQL 方式实现流上处理逻辑
- 提供 Flow 抽象。Flow 由一个 Source Namespace 和一个 Sink Namespace 定义,且具备唯一性。Flow 上可以定义处理逻辑,是一种流上处理的逻辑抽象,通过与物理 Spark Streaming、Flink Streaming 解耦,使得同一个 Stream 可以处理多个 Flow 处理流,且 Flow 可以在不同 Stream 上任意切换。
- 支持基于回灌(backfill)的 Kappa 架构;支持基于 Wormhole Job 的 Lambda 架构
2)从内部角度看待设计思想
- 基于 Spark Streaming、Flink 计算引擎进行数据流上处理。Spark Streaming 可支持高吞吐、批量 Lookup、批量写 Sink 等场景;Flink 可支持低延迟、CEP 规则等场景。
- 通过 ums_id_, ums_op_实现不同 Sink 的幂等入库逻辑
- 通过计算下推实现 Lookup 逻辑优化
- 抽象几个统一以支持功能灵活性和设计一致性
✔ 统一 DAG 高阶分形抽象
✔ 统一通用流消息 UMS 协议抽象
✔ 统一数据逻辑表命名空间 Namespace 抽象
- 抽象几个接口以支持可扩展性
✔ SinkProcessor:扩展更多 Sink 支持
✔ SwiftsInterface:自定义流上处理逻辑支持
✔ UDF:更多流上处理 UDF 支持
- 通过 Feedback 消息实时归集流式作业动态指标和统计
1.2.3.2 Wormhole 功能特性
- 支持可视化,配置化,SQL 化开发实施流式项目
- 支持指令式动态流式处理的管理、运维、诊断和监控
- 支持统一结构化 UMS 消息和自定义半结构化 JSON 消息
- 支持处理增删改三态事件消息流
- 支持单个物理流同时并行处理多个逻辑业务流
- 支持流上 Lookup Anywhere,Pushdown Anywhere
- 支持基于业务策略的事件时间戳流式处理
- 支持 UDF 的注册管理和动态加载
- 支持多目标数据系统的并发幂等入库
- 支持多级基于增量消息的数据质量管理
- 支持基于增量消息的流式处理和批量处理
- 支持 Lambda 架构和 Kappa 架构
- 支持与三方系统无缝集成,可作为三方系统的流控引擎
- 支持私有云部署,安全权限管控和多租户资源管理
1.2.3.3 Wormhole 技术架构
图 8 Wormhole 数据流转架构图
更多 Wormhole 技术细节和用户界面,可以参看:
GitHub:https://github.com/edp963/wor…
1.2.4 常用数据计算存储选型
RTDP 架构对待数据计算存储选型的选择采取开放整合的态度。不同数据系统有各自的优势和适合的场景,但并没有一个数据系统可以适合各种各样的存储计算场景。因此当有合适的、成熟的、主流的数据系统出现,Wormhole 和 Moonbox 会按照需要相应的扩展整合支持。
这里大致列举一些比较通用的选型:
- 关系型数据库(Oracle/MySQL 等):适合小数据量的复杂关系计算
- 分布式列存储系统
✔ Kudu:Scan 优化,适合 OLAP 分析计算场景
✔ HBase:随机读写,适合提供数据服务场景
✔ Cassandra:高性能写,适合海量数据高频写入场景
✔ ClickHouse:高性能计算,适合只有 insert 写入场景(后期将支持更新删除操作)
- 分布式文件系统
✔ HDFS/Parquet/Hive:append only,适合海量数据批量计算场景
- 分布式文档系统
✔ MongoDB:平衡能力,适合大数据量中等复杂计算
- 分布式索引系统
✔ ElasticSearch:索引能力,适合做模糊查询和 OLAP 分析场景
- 分布式预计算系统
✔ Druid/Kylin:预计算能力,适合高性能 OLAP 分析场景
1.2.5 计算服务平台 Moonbox
图 9 RTDP 架构之 Moonbox
1.2.5.1 Moonbox 设计思想
1)从外部角度看待设计思想
- 负责对接不同的数据系统,支持统一方式跨异构数据系统即席混算
- 提供三种 Client 调用方式:RESTful 服务、JDBC 连接、ODBC 连接
- 统一元数据收口;统一查询语言 SQL 收口;统一权限控制收口
- 提供两种查询结果写出模式:Merge、Replace
- 提供两种交互模式:Batch 模式、Adhoc 模式
- 数据虚拟化实现,多租户实现,可看作是虚拟数据库
2)从内部角度看待设计思想
- 对 SQL 进行解析,经过常规 Catalyst 处理解析流程,最终生成可下推数据系统的逻辑执行子树进行下推计算,然后将结果拉回进行混算并返回
- 支持两层 Namespace:database.table,以提供虚拟数据库体验
- 提供分布式服务模块 Moonbox Grid 提供高可用高并发能力
- 对可全部下推逻辑(无混算)提供快速执行通道
1.2.5.2 Moonbox 功能特性
- 支持跨异构系统无缝混算
- 支持统一 SQL 语法查询计算和写入
- 支持三种调用方式:RESTful 服务、JDBC 连接、ODBC 连接
- 支持两种交互模式:Batch 模式、Adhoc 模式
- 支持 Cli Command 工具和 Zeppelin
- 支持多租户用户权限体系
- 支持表级权限、列级权限、读权限、写权限、UDF 权限
- 支持 YARN 调度器资源管理
- 支持元数据服务
- 支持定时任务
- 支持安全策略
1.2.5.3 Moonbox 技术架构
图 10 Moonbox 逻辑模块
更多 Moonbox 技术细节和用户界面,可以参看:
GitHub:https://github.com/edp963/moo…
1.2.6 可视应用平台 Davinci
图 11 RTDP 架构之 Davinci
1.2.6.1 Davinci 设计思想
1)从外部角度看待设计思想
- 负责各种数据可视化展示功能
- 支持 JDBC 数据源
- 提供平权用户体系,每个用户可以建立属于自己的 Org、Team 和 Project
- 支持 SQL 编写数据处理逻辑,支持拖拽式编辑可视化展示,提供多用户社交化分工协作环境
- 提供多种不同的图表交互能力和定制化能力,以应对不同数据可视化需求
- 提供嵌入整合进其他数据应用的能力
2)从内部角度看待设计思想
- 围绕 View 和 Widget 展开。View 是数据的逻辑视图;Widget 是数据可视化视图
- 通过用户自定义选择分类数据、有序数据和量化数据,按照合理的可视化逻辑自动展现视图
1.2.6.2 Davinci 功能特性
1)数据源
- 支持 JDBC 数据源
- 支持 CSV 文件上传
2)数据视图
- 支持定义 SQL 模版
- 支持 SQL 高亮显示
- 支持 SQL 测试
- 支持回写操作
3)可视组件
- 支持预定义图表
- 支持控制器组件
- 支持自由样式
4)交互能力
- 支持可视组件全屏显示
- 支持可视组件本地控制器
- 支持可视组件间过滤联动
- 支持群控控制器可视组件
- 支持可视组件本地高级过滤器
- 支持大数据量展示分页和滑块
5)集成能力
- 支持可视组件 CSV 下载
- 支持可视组件公共分享
- 支持可视组件授权分享
- 支持仪表板公共分享
- 支持仪表板授权分享
6)安全权限
- 支持数据行列权限
- 支持 LDAP 登录集成
更多 Davinci 技术细节和用户界面,可以参看:
GitHub:https://github.com/edp963/dav…
1.3 切面话题讨论
1.3.1 数据管理
1)元数据管理
- DBus 可以实时拿到数据源的元数据并提供服务查询
- Moonbox 可以实时拿到数据系统的元数据并提供服务查询
- 对于 RTDP 架构来说,实时数据源和即席数据源的元数据信息可以通过调用 DBus 和 Moonbox 的 RESTful 服务归集,可以基于此建设企业级元数据管理系统
2)数据质量
- Wormhole 可以配置消息实时落入 HDFS(hdfslog)。基于 hdfslog 的 Wormhole Job 支持 Lambda 架构;基于 hdfslog 的 Backfill 支持 Kappa 架构。可以通过设置定时任务选择 Lambda 架构或者 Kappa 架构对 Sink 进行定时刷新,以确保数据的最终一致性。Wormhole 还支持将流上处理异常或 Sink 写入异常的消息信息实时 Feedback 到 Wormhole 系统中,并提供 RESTful 服务供三方应用调用处理。
- Moonbox 可以对异构系统进行即席混算,这个能力赋予 Moonbox“瑞士军刀”般的便利性。可以通过 Moonbox 编写定时 SQL 脚本逻辑,对关注的异构系统数据进行比对,或对关注的数据表字段进行统计等,可以基于 Moonbox 的能力二次开发数据质量检测系统。
3)血缘分析
- Wormhole 的流上处理逻辑通常 SQL 即可满足,这些 SQL 可以通过 RESTful 服务进行归集。
- Moonbox 掌管了数据查询的统一入口,并且所有逻辑均为 SQL,这些 SQL 可以通过 Moonbox 日志进行归集。
- 对于 RTDP 架构来说,实时处理逻辑和即席处理逻辑的 SQL 可以通过调用 Wormhole 的 RESTful 服务和 Moonbox 的日志归集,可以基于此建设企业级血缘分析系统。
1.3.2 数据安全
图 12 RTDP 数据安全
上图给出了 RTDP 架构中,四个开源平台覆盖了端到端数据流转链路,并且在每个节点上都有对数据安全各个方面的考量和支持,确保了实时数据管道端到端的数据安全性。
另外,由于 Moonbox 成为了面向应用层数据访问的统一入口,因此基于 Moonbox 的操作审计日志可以获得很多安全层面的信息,可以围绕操作审计日志建立数据安全预警机制,进而建设企业级数据安全系统。
1.3.3 开发运维
1)运维管理
- 实时数据处理的运维管理向来是个痛点,DBus 和 Wormhole 通过可视化 UI 提供了可视化运维管理能力,让人工运维变得简单。
- DBus 和 Wormhole 提供了健康检查、操作管理、Backfill、Flow 漂移等 RESTful 服务,可以基于此研发自动化运维系统。
2)监控预警
- DBus 和 Wormhole 均提供可视化监控界面,可以实时看到逻辑表级的吞吐和延迟等信息。
- DBus 和 Wormhole 提供了心跳、Stats、状态等 RESTful 服务,可以基于此研发自动化预警系统。
二、模式场景探讨
上一章我们介绍了 RTDP 架构各个技术组件的设计架构和功能特性,至此读者已经对 RTDP 架构如何落地有了具体的认识和了解。那么 RTDP 架构可以解决哪些常见数据应用场景呢?下面我们会探讨几种使用模式,以及不同模式适应何种需求场景。
2.1 同步模式
2.1.1 模式描述
同步模式,是指只配置异构数据系统之间的数据实时同步,在流上不做任何处理逻辑的使用模式。
具体而言,通过配置 DBus 将数据从数据源实时抽取出来投放在 Kafka 上,然后通过配置 Wormhole 将 Kafka 上数据实时写入到 Sink 存储中。同步模式主要提供了两个能力:
- 后续数据处理逻辑不再执行在业务备库上,减少了对业务备库的使用压力
- 提供了将不同物理业务备库数据实时同步到同一物理数据存储的可能性
2.1.2 技术难点
具体实施比较简单。
IT 实施人员无需了解太多流式处理的常见问题,不需要考虑流上处理逻辑实现的设计和实施,只需要了解基本的流控参数配置即可。
2.1.3 运维管理
运维管理比较简单。
需要人工运维。但由于流上没有处理逻辑,因此容易把控流速,无需考虑流上处理逻辑本身的功耗,可以给出一个相对稳定的同步管道配置。并且也很容易做到定时端到端数据比对来确保数据质量,因为源端和目标端的数据是完全一致的。
2.1.4 适用场景
- 跨部门数据实时同步共享
- 交易数据库和分析数据库解耦
- 支持数仓实时 ODS 层建设
- 用户自助实时简单报表开发
- 等等
2.2 流算模式
2.2.1 模式描述
流算模式,是指在同步模式的基础上,在流上配置处理逻辑的使用模式。
在 RTDP 架构中,流上处理逻辑的配置和支持主要在 Wormhole 平台上进行。在同步模式的能力之上,流算模式主要提供了两个能力:
- 流上计算将批量计算集中功耗分散在流上增量计算持续功耗,极大降低了结果快照的时间延迟
- 流上计算提供了跨异构系统混算的新的计算入口(Lookup)
2.2.2 技术难点
具体实施相对较难。
用户需要了解流上处理能做哪些事,适合做哪些事,如何转化全量计算逻辑成为增量计算逻辑等。还要考虑流上处理逻辑本身功耗和依赖的外部数据系统等因素来调节配置更多参数。
2.2.3 运维管理
运维管理相对较难。
需要人工运维。但比同步模式运维管理更难,主要体现在流控参数配置考虑因素较多、无法支持端到端数据比对、要选择结果快照最终一致性实现策略、要考虑流上 Lookup 时间对齐策略等方面问题。
2.2.4 适用场景
- 对低延迟要求较高的数据应用项目或报表
- 需要低延迟调用外部服务(如流上调用外部规则引擎、在线算法模型使用等)
- 支持数仓实时事实表 + 维度表的宽表建设
- 实时多表融合、分拆、清洗、标准化 Mapping 场景
- 等等
2.3 轮转模式
2.3.1 模式描述
轮转模式,是指在流算模式的基础上,在数据实时落库中,同时跑短时定时任务在库上进一步计算后,将结果再次投放在 Kafka 上跑下一轮流上计算,这样流算转批算、批算转流算的使用模式。
在 RTDP 架构中,可以利用 Kafka->Wormhole->Sink->Moonbox->Kafka 的整合方式实现任何轮次任何频次的轮转计算。在流算模式的能力之上,轮转模式提供的主要能力是:理论上支持低延迟的任何复杂流转计算逻辑。
2.3.2 技术难点
具体实施难。
Moonbox 转 Wormhole 能力的引入,比流算模式进一步增加了考虑的变量因素,如多 Sink 的选择、Moonbox 计算的频率设定、如何拆分 Wormhole 和 Moonbox 的计算分工等方面问题。
2.3.3 运维管理
运维管理难。
需要人工运维。和流算模式比,需要更多数据系统因素的考虑、更多参数的配置调优、更难的数据质量管理和诊断监控。
2.3.4 适用场景
- 低延迟的多步骤的复杂数据处理逻辑场景
- 公司级实时数据流转处理网络建设
2.4 智能模式
2.4.1 模式描述
智能模式,是指利用规则或算法模型来进行优化和增效的使用模式。
可以智能化的点:
- Wormhole Flow 的智能漂移(智能化自动化运维)
- Moonbox 预计算的智能优化(智能化自动化调优)
- 全量计算逻辑智能转换成流式计算逻辑,然后部署在 Wormhole + Moonbox(智能化自动化开发部署)
- 等等
2.4.2 技术难点
具体实施在理论上最简单,但有效的技术实现最难。
用户只需要完成离线逻辑开发,剩下交由智能化工具完成开发、部署、调优、运维。
2.4.3 运维管理
零运维。
2.4.4 适用场景
全场景。
自此,我们对“如何设计实时数据平台”这个话题的讨论暂时告一段落。我们从概念背景,讨论到架构设计,接着介绍了技术组件,最后探讨了模式场景。由于这里涉及到的每个话题点都很大,本文只是做了浅层的介绍和探讨。后续我们会不定期针对某个具体话题点展开详细讨论,将我们的实践和心得呈现出来,抛砖引玉,集思广益。如果对 RTDP 架构中的四个开源平台感兴趣,欢迎在 GitHub 上找到我们,了解使用,交流建议。
作者:卢山巍
来源:宜信技术学院