本文是牛冬的 《Elasticsearch实战与原理解析》的读书笔记。电子书还是看文字类的舒服,可以在PC上阅读,也可以在手机上阅读。看文章最后,提供原文链接和源代码链接。
(1)Elasticsearch基于Java编写,其内部使用Lucene做索引与搜索。
(2)2019年4月10日,Elasticsearch发布了7.0版本。该版本的重要特性包含引入内存断路器、引入Elasticsearch的全新集群协调层——Zen2、支持更快的前k个查询、引入Function score 2.0等。
curl http://localhost:29200
{
"name" : "node-1",
"cluster_name" : "my-es",
"cluster_uuid" : "xbE6G3BcQQKcQwCT8tWI_A",
"version" : {
"number" : "6.6.0",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "a9861f4",
"build_date" : "2019-01-24T11:27:09.439740Z",
"build_snapshot" : false,
"lucene_version" : "7.6.0",
"minimum_wire_compatibility_version" : "5.6.0",
"minimum_index_compatibility_version" : "5.0.0"
},
"tagline" : "You Know, for Search"
}
有些设置是敏感的,依赖文件系统权限来保护它们的值是不够的。对于这个用例,Elasticsearch提供了一个密钥存储库和一个Elasticsearch -keystore工具来管理密钥存储库中的设置。
注意:elasticsearch密钥库目前只提供混淆。未来将增加密码保护。
帮助文档:
../bin/elasticsearch-keystore --help
A tool for managing settings stored in the elasticsearch keystore
Commands
--------
create - Creates a new elasticsearch keystore
list - List entries in the keystore
add - Add a string setting to the keystore
add-file - Add a file setting to the keystore
remove - Remove a setting from the keystore
使用create命令来创建elasticsearch.keystore:
bin/elasticsearch-keystore create
文件elasticsearch.keystore将创建于elasticsearch.yml的目录位置。
可以通过list命令获得密钥存储库中的设置列表:
bin/elasticsearch-keystore list
可以使用add命令添加敏感的字符串设置,比如云插件的身份验证凭据:
bin/elasticsearch-keystore add the.setting.name.to.set
该工具将提示设置的值。要通过stdin传递该值,请使用--stdin标志:
cat /file/containing/setting/value | bin/elasticsearch-keystore add --stdin the.setting.name.to.set
要从密钥库中删除设置,请使用remove命令:
bin/elasticsearch-keystore remove the.setting.name.to.remove
Elasticsearch的核心概念有Node、Cluster、Shards、Replicas、Index、Type、Document、Settings、Mapping和Analyzer,其含义分别如下所示。
即节点。节点是组成Elasticsearch集群的基本服务单元,集群中的每个运行中的Elasticsearch服务器都可称之为节点。
即集群。Elasticsearch的集群是由具有相同cluster.name (默认值为elasticsearch)的一个或多个Elasticsearch节点组成的,各个节点协同工作,共享数据。同一个集群内节点的名字不能重复,但集群名称一定要相同。
在Elasticsearch集群中,节点的状态有Green、Yellow和Red三种,分别如下所述。
① Green:绿色,表示节点运行状态为健康状态。所有的主分片和副本分片都可以正常工作,集群100%健康。
② Yellow:黄色,表示节点的运行状态为预警状态。所有的主分片都可以正常工作,但至少有一个副本分片是不能正常工作的。此时集群依然可以正常工作,但集群的高可用性在某种程度上被弱化。
③ Red:红色,表示集群无法正常使用。此时,集群中至少有一个分片的主分片及它的全部副本分片都不可正常工作。虽然集群的查询操作还可以进行,但是也只能返回部分数据(其他正常分片的数据可以返回),而分配到这个有问题分片上的写入请求将会报错,最终导致数据丢失。
即分片。当索引的数据量太大时,受限于单个节点的内存、磁盘处理能力等,节点无法足够快地响应客户端的请求,此时需要将一个索引上的数据进行水平拆分。拆分出来的每个数据部分称之为一个分片。一般来说,每个分片都会放到不同的服务器上。
进行分片操作之后,索引在规模上进行扩大,性能上也随之水涨船高的有了提升。Elasticsearch依赖Lucene,Elasticsearch中的每个分片其实都是Lucene中的一个索引文件,因此每个分片必须有一个主分片和零到多个副本分片。
在Elasticsearch中,默认为一个索引创建5个主分片,并分别为每个主分片创建一个副本。
即备份,也可称之为副本。副本指的是对主分片的备份,这种备份是精确复制模式。每个主分片可以有零个或多个副本,主分片和备份分片都可以对外提供数据查询服务。当构建索引进行写入操作时,首先在主分片上完成数据的索引,然后数据会从主分片分发到备份分片上进行索引。
当主分片不可用时,Elasticsearch会在备份分片中选举出一个分片作为主分片,从而避免数据丢失。
即索引。在Elasticsearch中,索引由一个和多个分片组成。在使用索引时,需要通过索引名称在集群内进行唯一标识。
即类别。类别指的是索引内部的逻辑分区,通过Type的名字在索引内进行唯一标识。在查询时如果没有该值,则表示需要在整个索引中查询。
即文档。索引中的每一条数据叫作一个文档,与关系数据库的使用方法类似,一条文档数据通过_id在Type内进行唯一标识。
Settings是对集群中索引的定义信息,比如一个索引默认的分片数、副本数等。
Mapping表示中保存了定义索引中字段(Field)的存储类型、分词方式、是否存储等信息,有点类似于关系数据库(如MySQL)中的表结构信息。
在Elasticsearch中,Mapping是可以动态识别的。如果没有特殊需求,则不需要手动创建Mapping,因为Elasticsearch会根据数据格式自动识别它的类型。一个索引的Mapping一旦创建,若已经存储了数据,就不可修改了。
Analyzer表示的是字段分词方式的定义。一个Analyzer通常由一个Tokenizer和零到多个Filter组成。在Elasticsearch中,默认的标准Analyzer包含一个标准的Tokenizer和三个Filter,即Standard Token Filter、Lower Case Token Filter和Stop TokenFilter。
ES架构图.png
我们将Elasticsearch的架构自底向上分为五层,分别是核心层、数据处理层、发现与脚本层、协议层和应用层。
发现与脚本层主要是Discovery(节点发现)模块、Script(脚本)模块和第三方插件模块。Discovery模块是Elasticsearch自动发现节点的机制。Script模块支持脚本的执行,脚本的应用使得我们能很方便的对查询出来的数据进行加工处理,目前Elasticsearch支持JavaScript、Python等多种语言。第三方插件模块表示Elasticsearch支持安装很多第三方的插件,如elasticsearch-ik分词插件、elasticsearch-sql插件等。
协议层是Elasticsearch中的数据交互协议。目前Elasticsearch支持Thrift、Memcached和HTTP三种协议,默认的是HTTP。JMX指的是在Elasticsearch中对Java的管理框架,用来管理Elasticsearch应用。
应用层指的是Elasticsearch的API支持模式。Elasticsearch的特色之一就是RESTFul风格的API,这种API接口风格也是当前十分流行的风格之一。
Elasticsearch内嵌自动发现功能,主要提供了4种可供选择的发现机制。其中一种是默认实现,其他都是通过插件实现的,具体如下所示。
(1)Azure discovery插件方式:多播模式。
(2)EC2 discovery插件方式:多播模式。
(3)Google Compute Engine(GCE)discovery插件方式:多播模式。
(4)Zen Discovery,默认实现方式,支持多播模式和单播模式。
Zen Discovery是Elasticsearch内置的默认发现模块。发现模块用于发现集群中的节点及选举主节点(又称master节点)。Zen Discovery提供单播模式和基于文件的发现,并且可以扩展为通过插件支持其他形式的发现机制。
在Elasticsearch中,每个节点可以有多个角色,节点既可以是候选主节点,也可以是数据节点。
其中,数据节点负责数据的存储相关的操作,如对数据进行增、删、改、查和聚合等。其中,数据节点负责数据的存储相关的操作,如对数据进行增、删、改、查和聚合等。正因为如此,数据节点往往对服务器的配置要求比较高,特别是对CPU、内存和I/O的需求很大。
候选主节点是被选举为主节点的节点,在集群中,只有候选主节点才有选举权和被选举权,其他节点不参与选举工作。
片的数量和副本数量都可以通过创建索引时的Settings来配置,Elasticsearch默认为一个索引创建5个主分片,并分别为每个分片创建一个副本。配置的参数如下所示:
index.number_of_shards:5
index.number_of_replicas:1
对文档的新建、索引和删除请求等写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。Elasticsearch为了加快写入的速度,写入过程往往是并发实施的。为了解决在并发写的过程中出现的数据冲突的问题,Elasticsearch通过乐观锁进行控制,每个文档都有一个version(版本号),当文档被修改时版本号递增。
索引数据在磁盘上的是以分段形式存储的。“段”是Elasticsearch从Lucene中继承的概念。在索引中,索引文件被拆分为多个子文件,其中每个子文件就叫作段,每个段都是一个倒排索引的小单元。段具有不变性,一旦索引的数据被写入硬盘,就不能再修改。
在Elasticsearch中,索引写入磁盘的过程是异步的。因此,为了提升写的性能,Elasticsearch并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写策略。
虽然延迟写策略可以减少数据往磁盘上写的次数,提升Elasticsearch的整体写入能力,但文件缓存系统的引入同时也带来了数据丢失的风险,如机房断电等。为此,Elasticsearch引入事务日志(Translog)机制。事务日志用于记录所有还没有持久化到磁盘的数据。
在Elasticsearch自动刷新流程中,每秒都会创建一个新的段。这自然会导致短时间内段的数量猛增,而当段数量太多时会带来较大的资源消耗,如对文件句柄、内存和CPU的消耗。而在内容搜索阶段,由于搜索请求要检查到每个段,然后合并查询结果,因此段越多,搜索速度越慢。为此,Elasticsearch引入段合并机制。段合并机制在后台定期进行,从而小的段被合并到大的段,然后这些大的段再被合并到更大的段。
在段合并过程中,Elasticsearch会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中,当然,在合并的过程中不会中断索引和搜索。
...
[待补充]
一般分析器会包含三个部分:
(1)character filter:分词之前的预处理,过滤HTML标签、特殊符号转换等。
(2)tokenizer:用于分词。
(3)token filter:用于标准化输出。
ELK Stack指的就是Elastic Stack。
“ELK”是三个开源项目的首字母缩写,这三个项目分别是:Elasticsearch、Logstash和Kibana,如图10-1所示。当然,这并非是Elastic Stack的全部,读者可以根据需要在生态中添加Redis、Kafka、Filebeat等软件。
ELK产品.png
在当前的软件开发过程中,业务发展节奏越来越快,服务器梳理越来越多,随之而来的就是各种访问日志、应用日志和错误日志。这时ELK就可以“隆重登场”啦!
首先,我们使用Logstash进行日志的搜集、分析和过滤。一般工作方式为C/S架构,Client端会被安装在需要收集日志的主机上,Server端则负责收集的各节点的日志数据,并进行过滤、修改和分析等操作,预处理过的数据会一并发到Elasticsearch上。
随后将Kibana接入Elasticsearch,并为Logstash和Elasticsearch提供日志分析友好的Web界面,帮助用户汇总、分析和搜索重要数据的日志。
ELK架构为数据分布式存储、可视化查询和日志解析创建了一个功能强大的管理链。
4.ELK部署架构1.png
首先由分布于各个服务节点上的Logstash搜集相关日志和数据,经过Logstash的分析和过滤后发送给远端服务器上的Elasticsearch进行存储。Elasticsearch将数据以分片的形式压缩存储,并提供多种API供用户进行查询操作。用户还可以通过配置Kibana Web Portal对日志进行查询,并根据数据生成报表。
该架构最显著的优点是搭建简单,易于上手。但缺点同样很突出,因为Logstash消耗资源较大,所以在运行时会占用很多的CPU和内存。并且系统中没有消息队列缓存等持久化手段,因而存在数据丢失隐患。因此,一般这种部署架构通常用于学习和小规模集群。
4.ELK部署架构2.png
位于各个节点上的Logstash客户端先将数据和日志等内容传递给Kafka,当然,也可以用其他消息机制,如各类MQ(Message Queue)和Redis等。Kafka会将队列中的消息和数据传递给Logstash,经过Logstash的过滤和分析等处理后,传递给Elasticsearch进行存储。最后由Kibana将日志和数据呈现给用户。在该部署架构中,Kafka的引入使得即使远端Logstash因故障而停止运行,数据也会被存储下来,从而避免数据丢失。
第二种部署架构解决了数据的可靠性问题,但Logstash的资源消耗依然较多
4.ELK部署架构3.png
Logstash-forwarder将日志数据搜集并统一后发送给主节点上的Logstash,Logstash在分析和过滤日志数据后,把日志数据发送至Elasticsearch进行存储,最后由Kibana将数据呈现给用户。这种架构解决了Logstash在各计算机点上占用系统资源较多的问题。与Logstash相比,Logstash-forwarder所占系统的CPU和内存几乎可以忽略不计。而且,Logstash-forwarder的数据安全性更好。Logstash-forwarder和Logstash之间的通信是通过SSL加密传输的,因此安全有保障。
4.ELK部署架构4.png
在实际使用中,Beats平台在满负荷状态时所耗系统资源和Logstash-forwarder相当,但其扩展性和灵活性更好。Beats平台目前包含Packagebeat、Topbeat和Filebeat三个产品,均为Apache 2.0 License。同时用户可以根据需要进行二次开发。
与前面三个部署架构相比,显然第四种架构更灵活,可扩展性更强。
Logstash由三部分组成,即输入模块(INPUTS)、过滤器模块(FILTERS)和输出模块(OUTPUTS),如图10-6所示。
5.logstach框架.png
Logstash能够动态地采集、转换和传输数据,不受格式或复杂度的影响。利用Grok从非结构化数据中派生出结构,从IP地址解码出地理坐标,匿名化或排除敏感字段,并简化整体处理过程。
Logstash支持各种输入选择,可以在同一时间从众多常用来源捕捉事件,能够以流式传输方式,轻松地从用户的日志、指标、Web应用、数据存储及各种AWS服务中采集数据。
为了支持各种数据输入,Logstash提供了很多输入插件,汇总如下。
(1)azure_event_hubs:该插件从微软Azure事件中心接收数据。读者可访问GitHub官网,搜索logstash-input-azure_event_hubs获取插件。
(2) beats:该插件从Elastic Beats框架接收数据。读者可访问GitHub官网,搜索logstash-input-beats获取插件。
(3)cloudwatch:该插件从Amazon Web Services CloudWatch API中提取数据。读者可访问GitHub官网,搜索logstash-input-cloudwatch获取插件。
(4)couchdb_changes:该插件从CouchDB更改URI的流式处理事件中获取数据。读者可访问GitHub官网,搜索logstash-input-couchdb_changes获取插件。
(5)dead_letter_queue:该插件从logstash的dead letter队列中读取数据。读者可访问GitHub官网,搜索logstash-input-dead_letter_queue获取插件。
(6)elasticsearch:该插件从ElasticSearch群集中读取查询结果。读者可访问GitHub官网,搜索logstash-input-elasticsearch获取插件。
(7)exec:该插件将shell命令的输出捕获为事件,并获取数据。读者可访问GitHub官网,搜索logstash-input-exec获取插件。
(8)file:该插件从文件流式处理中获取数据。读者可访问GitHub官网,搜索logstash-input-file获取插件。
(9)ganglia:该插件通过UDP数据包读取ganglia中的数据包来获取数据。读者可访问GitHub官网,搜索logstash-input-ganglia获取插件。
(10)gelf:该插件从graylog2中读取gelf格式的消息获取数据。读者可访问GitHub官网,搜索logstash-input-gelf获取插件。
(11)http:该插件通过HTTP或HTTPS接收事件获取数据。读者可访问GitHub官网,搜索logstash-input-http获取插件。
(12)jdbc:该插件通过JDBC接口从数据库中获取数据。读者可访问GitHub官网,搜索logstash-input-jdbc获取插件。
(13)kafka:该插件从Kafka主题中读取事件,从而获取数据。读者可访问GitHub官网,搜索logstash-input-kafka获取插件。
(14)log4j:该插件通过TCP套接字从Log4J SocketAppender对象中读取数据。读者可访问GitHub官网,搜索logstash-input-log4j获取插件。
(15)rabbitmq:该插件从RabbitMQ数据交换中提取数据。读者可访问GitHub官网,搜索logstash-input-rabbitmq获取插件。https://github.com/logstash-plugins/logstash-input-rabbitmq
(16)redis:该插件从redis实例中读取数据。读者可访问GitHub官网,搜索logstash-input-redis获取插件。
Logstash过滤器用于实时解析和转换数据。
在数据从源传输到存储库的过程中,Logstash过滤器能够解析各个数据事件,识别已命名的字段,构建对应的数据结构,并将它们转换成通用格式,以便更轻松、更快速地进行分析,实现商业价值。
在数据从源传输到存储库的过程中,Logstash过滤器能够解析各个数据事件,识别已命名的字段,构建对应的数据结构,并将它们转换成通用格式,以便更轻松、更快速地进行分析,实现商业价值。
为了处理各种各样的数据源,Logstash提供了丰富多样的过滤器库,常用的过滤器插件汇总如下。
(1)aggregate:该插件用于从一个任务的多个事件中聚合信息。读者可访问GitHub官网,搜索logstash-filter-aggregate获取插件。
(2)alter:该插件对mutate过滤器不处理的字段执行常规处理。读者可访问GitHub官网,搜索logstash-filter-alter获取插件。
(3)bytes:该插件将以计算机存储单位表示的字符串形式,如“123MB”或“5.6GB”,解析为以字节为单位的数值。读者可访问GitHub官网,搜索logstash-filter-bytes获取插件。
(4)cidr:该插件根据网络块列表检查IP地址。读者可访问GitHub官网,搜索logstash-filter-cidr获取插件。
(5)cipher:该插件用于对事件应用增加或移除密钥。读者可访问GitHub官网,搜索logstash-filter-cipher获取插件。
(6)clone:该插件用于复制事件。读者可访问GitHub官网,搜索logstash-filter-clone获取插件。
(7)csv:该插件用于将逗号分隔的值数据解析为单个字段。读者可访问GitHub官网,搜索logstash-filter-csv获取插件。
(8)date:该插件用于分析字段中的日期,多用于事件日志中存储的时间戳。读者可访问GitHub官网,搜索logstash-filter-date获取插件。
(9) dns:该插件用于执行正向或反向DNS查找。读者可访问GitHub官网,搜索logstash-filter-dns获取插件。(10)elasticsearch:该插件用于将Elasticsearch日志事件中的字段复制到当前事件中。读者可访问GitHub官网,搜索logstash-filter-elasticsearch获取插件。
(11)geoip该插件用于添加有关IP地址的地理信息。读者可访问GitHub官网,搜索logstash-filter-geoip获取插件。
(12)json:该插件用于解析JSON事件。读者可访问GitHub官网,搜索logstash-filter-json获取插件。
(13)kv:该插件用于分析键值对。读者可访问GitHub官网,搜索logstash-filter-kv获取插件。
(14)memcached:该插件用于提供与memcached中数据的集成。读者可访问GitHub官网,搜索logstash-filter-memcached获取插件。
(15)split:该插件用于将多行消息拆分为不同的事件。读者可访问GitHub官网,搜索logstash-filter-split获取插件。
Logstash的输出模块用于将目标数据导出到用户选择的存储库。
在Logstash中,尽管Elasticsearch是Logstash官方首选的,但它并非唯一选择。
(1) csv:该插件以CVS格式将结果数据写入磁盘。读者可访问GitHub官网,搜索logstash-output-csv获取插件。
(2) mongodb:该插件将结果数据写入MongoDB。读者可访问GitHub官网,搜索logstash-output-mongodb获取插件。
(3)elasticsearch:该插件将结果数据写入Elasticsearch。读者可访问GitHub官网,搜索logstash-output-elasticsearch获取插件。
(4)email:该插件将结果数据发送到指定的电子邮件。读者可访问GitHub官网,搜索logstash-output-email获取插件。
(5)kafka:该插件将结果数据写入Kafka的Topic主题。读者可访问GitHub官网,搜索logstash-output-kafka获取插件。
(6)file:该插件将结果数据写入磁盘上的文件。读者可访问GitHub官网,搜索logstash-output-file获取插件。(7)redis:该插件使用redis中的rpush命令将结果数据发送到redis队列。读者可访问GitHub官网,搜索logstash-output-redis获取插件。
Kibana是一个基于Web的图形界面,可以让用户在Elasticsearch中使用图形和图表对数据进行可视化。
在实际使用过程中,Kibana一般用于搜索、分析和可视化存储在Elasticsearch指标中的日志数据。Kibana利用Elasticsearch的REST接口检索数据,不仅允许用户创建自己的数据定制仪表板视图,还允许他们以特殊的方式查询和过滤数据。可以说从跟踪、查询、负载到理解请求如何流经整个应用,Kibana都能轻松完成。
Kibana提供了基本内容服务、位置分析服务、时间序列服务、机器学习服务,以及图表和网络服务。
(1)基本内容服务:指的是Kibana核心产品中搭载的一批经典功能,如基于筛选数据绘制柱形图、折线图、饼图、旭日图等。
(2)位置分析服务:主要借助Elastic Maps探索位置数据。另外,还可以获得创意,对定制图层和矢量形状进行可视化。
(3)时间序列服务:借助Kibana团队精选的时序数据UI,对用户所用Elasticsearch中的数据执行高级时间序列分析。因而,用户可以利用功能强大、简单易学的表达式来描述查询、转换和可视化。
(4)机器学习服务:主要是借助非监督型机器学习功能检测隐藏在用户所用Elasticsearch数据中的异常情况,并探索那些对用户有显著影响的属性。
(5)图表和网络服务:凭借搜索引擎的相关性功能,结合Graph关联分析,揭示用户所用Elasticsearch数据中极其常见的关系。
此外,Kibana还支持用户把Kibana可视化内容分享给他人,如团队成员、老板、客户、合规经理或承包商等,进而让每个人都感受到Kibana的便利。除分享链接外,Kibana还有其他内容输出形式,如嵌入仪表板,导出为PDF、PNG或CSV等格式文件,以便把这些文件作为附件发送给他人。
KIBANA示例图:
6.KIBANA示例图.png
Logstash在数据收集上并不出色,而作为代理,其性能也并不达标。于是,Elastic官方发布了Beats系列轻量级采集组件。
Beats平台集合了多种单一用途的数据采集器。它们从成百上千台机器和系统向Logstash或Elasticsearch发送数据。
Beats是一组轻量级采集程序的统称,如图10-14所示。
7.beats组件.png
Beats中包括但不限于以下组件:
(1)Filebeat:该组件会进行文件和目录的采集,主要用于收集日志数据。
(2)Metricbeat:该组件会进行指标采集。这里说的指标可以是系统的,也可以是众多中间件产品的。主要用于监控系统和软件的性能。
(3)Packetbeat:该组件通过网络抓包和协议分析,对一些请求响应式的系统通信进行监控和数据收集,可以收集到很多常规方式无法收集到的信息。
(4)Winlogbeat:该组件专门针对Windows的event log进行数据采集。
(5)Audibeat:该组件用于审计数据场景,收集审计日志。
(6)Heartbeat:该组件用于系统间连通性检测,如ICMP、TCP、HTTP等的连通性监控。
(7)Functionbeat:该组件用于无须服务器的采集器。
官方支持的7种组件与ELK的数据流转关系如图10-15所示。
8.beats组件与ELK的关系.png
Beats轻量级设计的实现
Beats是一组轻量级采集程序的统称,那么Beats是如何做到轻量级的呢?
(1)数据处理简单。在数据收集层面,Beats并不进行过于复杂的数据处理,只是将数据简单的组织并上报给上游系统。
(2)并发性好、便于部署。Beats采用Go语言开发而成。
因此,Beats的性能显著好于Logstash。
Beats架构设计
Beats之所以有上乘的性能及良好的可扩展性,能获得如此强大的开源支持,其根本原因在于它有一套设计良好的代码框架。Beats的架构设计如图10-16所示。
9.Beats架构设计.png
libbeat是Beats的核心包。在Beats架构中,有输出模块(Publisher)、数据收集模块(Logging)、配置文件模块(Configuration)、日志处理模块和守护进程模块(Daemon/service)。其中,输出模块负责将收集到的数据发送给Logstash或者Elasticsearch。
因为Go语言天然就有channel,所以收集数据的逻辑代码与输出模块都是通过channel通信的。也就是说,两个模块的耦合度最低。
(1) 《Elasticsearch实战与原理解析》 https://weread.qq.com/web/reader/01a32c5071d7c79a01a0831
(2)配套源代码 http://www.broadview.com.cn/file/resource/003179083145134038138254251183181156060026092118