实时⽇志检索分析应⽤


实时⽇志检索分析应⽤ ELKstack@sina ⾃我介绍 • 先后给 CloudEx, China.com, RenRen, Sina 重启 服务器; • 精通 echo/say/puts/console.log(“Hello World”); • 写过《 ⽹站运维技术与实践》 ; • 翻过《 Puppet 实战⼿册》 ; • 微博账号:@ARGV,求关注。 内容概要 • ELKstack集群概况 • ELKstack场景 ⽰例 • 从ELK到 ERK的 演进 • LERK性能优化细节 ERK集群规模 • 26 个datanode: • 2.4Ghz*8, 42G, 300G *10 RAID5 • 25种⽇志,7天,650亿条数据,6万个字段 • 单⽇数据8TB,写⼊峰值19万qps • 10 个rsyslog/logstash • rsyslog/logstash/kibana都有⼆次开发 • 使⽤⼈员:故障管理组,客户端开发,服务端开发,运维 • 维护⼈员:我*0.8 kopf 数据状态和设置调整 bigdesk 节点性能数据实时监控 zabbix trapper 关键指标的监控和报警 But,为什么要 ELK? 先说说⽇志能⼲嘛 • 找问题 • data-driven develop/test/operate • 安全审计 • Laws of Marcus J. Ranum • 监控 • Monitoring is the aggregation of health and performance data, events, and relationships delivered via an interface that provides an holistic view of a system's state to better understand and address failure scenarios. @etsy ⽇志分析的难点(1) • timestamp + data = log • 好,昨晚23:12到23:29那会⼉⽇志有啥异常? ⽇志分析的难点(2) • ⽂本内容⾮结构化数据 ⽇志分析的难点(2) • grep/awk只能单机跑 ⽇志分析的难点(3) • 格式复杂不⽅便可视化效果 So... • 我们需要⼀个 实时⼤数据搜 索平台 • 不过,splunk好贵好贵... • 只能⾃⼰拼开源玩具了~ ELKstack简明⼊⻔ Hello World # bin/logstash -e ‘input{stdin{}} output{stdout{codec=>rubydebug}}’ Hello World { "message" => "Hello World", "@version" => "1", "@timestamp" => "2014-08-07T10:30:59.937Z", "host" => "raochenlindeMacBook- Air.local", } How Powerful • $ ./bin/logstash -e ‘input{generator{count=>10000 0000000} output{stdout{codec=>dots}}}’ | pv -abt > /dev/null • 15.1MiB 0:02:21 [ 112kiB/s] How scaling Talk is cheap, 
 show me the case! php记录的⽇志 logstash相关配置 Kibana3最终效果 服务端开发和运维⽤来观测定位接⼜和应⽤错误 以及Kibana4效果 好吧,k4还没优化到配⾊⽅⾯ PHP的slowlog 经过multiline处理后 运维可以快速对⽐各机房的php慢函数堆栈异常, 以及单机异常数据的排名。 发现单机异常后的诊断 Nginx的errorlog grok { match => { "message" => "(?\d{4}/\d\d/\d\d \d\d:\d\d:\d\d) \[(? \w+)\] \S+: \*\d+ (?[^,]+), (?.*)$" } } mutate { gsub => [ "errmsg", "too large body: \d+ bytes", "too large body" ] } if [errinfo] { ruby { code => "event.append(Hash[event['errinfo'].split(', ').map{|l| l.split(': ')}])" } } grok { match => { "request" => '"%{WORD:verb} %{URIPATH:urlpath}(?:\?% {NGX_URIPARAM:urlparam})?(?: HTTP/%{NUMBER:httpversion})"' } } kv { prefix => "url_" source => "urlparam" field_split => "&" } date { locale => 'en' match => [ "datetime", "yyyy/MM/dd HH:mm:ss" ] } 运维对性能数据做优化调研时, 可以同时参照多个维度的数据。 时间段变化导致的各维度差异 app crash堆栈 客户端开发关注堆栈排⾏, 堆栈内容是在logstash⾥过滤掉系统函数之后的结果。 发布新版,即时过滤,关注堆栈 故障管理组和开发通过快捷搜索, 快速定位投诉⽤户的报错类型和内容。 H5开发关注的⾸⻚接⼝性能趋势数据 响应时间的概率分布 平均时间不靠谱,划分区间又不能拍脑门 ELK到 ERK的优化之路 别⼈家孩⼦ 穷⼈家孩⼦ WHY? 优劣对⽐ logstash • 设计:多线程共享 SizedQueue • 语⾔: JRuby开发 • 语法: DSL • 环境: jre1.7 • 队列:依赖外部系统 • 正则: ruby实现 • 输出:同⺴段内⾛ java协议写 ES • 插件: 182个 • 监控:暂⽆ rsyslog • 设计:多线程共享 mainQ 接收 • 语⾔: C开发 • 语法: rainerscript • 环境: rhel6⾃带 • 队列:内置异步队列 • 正则: ERE • 输出:使⽤ HTTP协议写 ES • 插件: 57个 • 监控:有 pstats数据 Logstash性能已知问题 • Input/syslog性能极差,改⽤ input/tcp+多线程 filter/grok • Filter/geoip性能较差,开发⼀个 filter/geoip2 • Filter/grok费 CPU,改⽤ filter/ruby⾃⼰写 split • Input/tcp有内存溢出 (1.4.2版本之前 ) • Output/elasticsearch有内存溢出 (1.5.0之前 ) • Output/elasticsearch的 retry逻辑跟 stud的 SizedQueue重复 (⾄今 ) LogStash问题 (1) • LogStash::Inputs::Syslog性能极差 • logstash的 pipeline介绍: • input thread
 -> filterworker threads * Num
 -> output thread • ⽽ Inputs::Syslog的逻辑是: • TCPServer/accept
 -> client thread -> filter/grok -> filter/date
 -> filterworker threads • 也就是在单线程中,要完成正则和时间转换两个极废资源的操作。 • 经测试, TCPServer能到 50k的 qps,经过 filter/grok后下降成 6k,再经 filter/date后下降成 0.7k! LogStash问题 (1) • LogStash::Inputs::Syslog性能极差 • 解决办法: • 把 grok和 date搬出来,⾃⼰通过配置实现这个功能: input { tcp { port => 514 } } filter { grok { match => ["message", "%{SYSLOGLINE}"] } syslog_pri { } date { match => ["timestamp", "ISO8601"] } } • 使⽤ logstash -w 20运⾏,可以达到 30k的 qps。 LogStash问题 (2) • LogStash::Filters::Grok性能 • Grok是在标准正则基础上,增加了正则预定义和引⽤ 功能: • 预定义好: NUMBER \d+
 使⽤ %{NUMBER:score} 等价于 (?\d+) • 采⽤正则处理⽇志,虽然灵活,但是消耗 CPU⽐较严 重。⽽且 grok原先是 C程序 (现在通过 epel库还可以直 接 yum install libgrok) LogStash问题 (2) • LogStash::Filters::Grok性能 • 解决办法: • 对有明显格式的⽇志,避免采⽤ grok,⽐如有固定分隔符的 : filter { ruby { init => "@kname = ['datetime','uid','limittype','limitkey','client','clientip','request_time','url']" code => "event.append(Hash[@kname.zip(event['message'].split('|'))])" } mutate { convert => ["request_time", "float"] } } • 实践证明:采⽤ split代码可以降低 20%的 cpu使⽤。 LogStash问题 (3) • LogStash::Filters::GeoIP性能 • 即使在 logstash -w 30的情况下,也只能到 7k的 qps。 • MaxMind公司后来推出的 MaxMindDB格式,即 GeoIP2地址库,性能有极⼤提⾼。但是因为 GeoIP2-Lite的发布协议只允许⼀年内使⽤,不⽅ 便分发,所以 LogStash开发者⽆法使⽤。 LogStash问题 (3) • LogStash::Filters::GeoIP性能 • 解决办法: • MaxMindDB提供了 Writer⼯具 (感谢陈刚童鞋 )。转 换我司 ip.db成 ip.mmdb, 300MB->50MB • JRuby平台采⽤ java_import导⼊ maxminddb-java 库实现 LogStash::Filters::MaxMindDB,性能提⾼ 到 28k 的 qps。 LogStash问题 (4) • LogStash::Outputs::Elasticsearch稳定性 • 到⺫前为⽌,踩过三个 bug: 1. logstash1.4.2采⽤的 ftw-0.0.39有内存泄露问题,跑⼏个⼩时就堵死了 ; 2. logstash1.5.0beta1改⽤了 Manticore,但是 retry逻辑和 logstash pipeline中使⽤的 ThreadQueue库的 retry逻辑有重复,导致反复发送; 3. logstash1.5.0rc1记录 ES错误响应时,⽆法匹配 reject错误的 429状态 码,看着报错⽇志⾥ "got response of . source:",完全茫然。 • ⺫前最新的 logstash1.5.0rc3版本内已解决 1和 3。 LogStash问题 (5) • LogStash::Pipeline的隐藏问题 • 前⾯检结果 pipeline的逻辑。但是这⾥ Logstash对 filterworkers没有做 supervisor。所以,⼀旦配置写得不全⾯,有可能导致 thread异常退出 的话,慢慢的所有 filterworkers都退出掉了, logstash就处于⼀个堵死但 进程还活着的状态! • 官⽅插件的代码⼀般都还⽐较可靠。但是前⾯介绍的采⽤ filter/ruby来⾃ ⼰操作 `event['field']`,就很可能碰到了。所以,⼀定要先检查有没有这 个 field存在! if [url] { ruby { code => "event['urlpath']=event['url'].split('?')[0]" } } LogStash问题 (6) • LogStash::Pipeline的⼜⼀个隐藏问题 • logstash1.5.0之前, filter阶段,某个插件代码内如 果⽣成新 event,通过 `yield`加⼊ pipeline的时候, 并不会继续配置中书写在该插件配置块后⾯的其他 filter插件流程,⽽是直接进⼊ output thread。 • 官⽅插件中, split和 clone都有⽤到。我们⾃定义 插件 json2也是。 Rsyslog性能优化 • action采⽤ linkedlist异步队列 • imfile启⽤合适的 statepresistinterval保证异常重启不重复发送太多⽇志 • omfwd采⽤ rebindinterval保证负载均衡 • 配置合适的 global.maxmessagesize⼤⼩ • 配置合适的 queue.size和 queue.highwatermask • 采⽤ CEE格式记录⽇志,配合 mmjsonparse解析 • 利⽤ mmfields切割固定分隔符的⽇志 • 利⽤ rainerscript语法做⼆次处理 • 利⽤ property replacer⼿⼯拼接 JSON字符串 • 开发 mmdb插件做 ip地址解析 rsyslog问题 (1) • 在源码的测试集中发现 rsyslog8.7新加实验性的 foreach指令,正好 ⽤来处理客户端⽇志的 JSON数组,使⽤中发现三个相关 bug: 1.foreach 操作没有判断传⼊变量是不是数组; 2.action() 不会复制消息内容⽽是直接引⽤指针,这样在 foreach ⾥发 送消息数组⾥单个元素,使⽤ linkedlist 异步⽅式的 action 队列,就 会出问题。之前测试 omfile 不会有问题,因为 omfile 默认是同步⽅ 式。对此,给 action() 新增⼀个 copymsg 参数控制; 3.omelasticsearch 插件在记录 errorfile 的逻辑⾥发现⼀个未初始化的 变量。 修复的补丁已经 merge进 rsyslog8.10, 5⽉ 20⽇发布。 rsyslog问题 (2) • message modification插件较少, mmexternal插件 ⺫前不稳定,对复杂需求需要⼆次开发。 • ⺫前完成 rsyslog-mmdb插件开发,在 rsyslog中完 成 clientip的解析, 5⽉下旬上线稳定运⾏。 input( type=“imtcp” port=“514” ) template( name=“clientlog" type="list" ) { constant(value="{\"@timestamp\":\"") property(name="timereported" dateFormat="rfc3339") constant(value="\",\"host\":\"") property(name="hostname") constant(value="\",\“mmdb\":") property(name="!iplocation") constant(value=",") property(name="$.line" position.from="2") } ruleset( name=“clientlog” ) { action(type="mmjsonparse") if($parsesuccess == "OK") then { foreach ($.line in $!msgarray) { if($.line!rtt == “-”) then { set $.line!rtt = 0; } set $.line!urlpath = field($.line!url, 63, 1); set $.line!urlargs = field($.line!url, 63, 2); set $.line!from = ""; if ( $.line!urlargs != "***FIELD NOT FOUND***" ) then { reset $.line!from = re_extract($.line!urlargs, "from=([0-9]+)", 0, 1, ""); } else { unset $.line!urlargs; } action(type=“mmdb” key=“.line!clientip” fields=[“city”,“isp”,“country”] mmdbfile="./ip.mmdb") action(type="omelasticsearch" server=“1.1.1.1“ bulkmode=“on“ template=“clientlog” queue.size="10000" queue.dequeuebatchsize="2000“ ) } } } if ($programname startswith “mweibo_client”) then { call clientlog stop } ES性能 优化 • 不要照抄⽹上的说法!! • 单机单索引单分⽚零副本的测试数据做基线 • ⽤unicast,加⼤fd.ping_timeout •doc_values是稳定性的基础 • 加快恢复:gateway,recovery和allocation相关参数 • 适当加⼤refresh_interval和flush_threshold_size • 适当加⼤store.throttle.max_bytes_per_sec • 升级到1.5.1以上 • 新加节点:限制索引级别的分⽚策略 • 要bulk,不要multithreads,更不要async •⽤ curator做定时 optimize • 简单⽇志可以不要_all ES稳定性问题 (1) • OOM • segment⾥的 fielddata是 ES做即时搜索和聚合的来源,默认 是在 query的时候,从 term加载到内存。所以数据量⼀⼤, 就会 OOM。 • Kibana3⾥⼲泛采⽤ facet_filter⽅式,意味着 QUERY阶段数 据量不做过滤,加重了出问题的可能。 • 在新版本中,引⼊了 circuit breaker概念, fielddata到 heap 的 60%的时候,直接断掉查询,报错: Data too large, data for field [@timestamp] would be larger than limit of[639015321/609.4mb]] ES稳定性问题 (1) • OOM 解决办法: • 启⽤ doc_values设置。该设置让 ES在 indexing的 时候提前做好 fielddata到磁盘上。以后 query的就 是读取磁盘内容。性能⽅⾯则主要靠⽂件系统的缓 存。 • 这样意味着反⽽不⽤设置太⼤的 heap(所谓的 31GB问题 ),多留⼀些内存给⽂件系统。 ES稳定性问题 (2) • 数据迁移和恢复导致的停机不可⽤ • 默认策略: • 节点启动即开始做恢复 • 集群内同时只能迁移⼀个 shard • 迁移限速 20MB • translog在 flush后就清除,所以 replica是全量从 primary⾛⺴络 复制! • 所以只要有宕机,集群就可能⼏个⼩时不可⽤ …… ES稳定性问题 (2) • 数据迁移和恢复导致的停机不可⽤的解决办法 • 节点启动即开始做恢复 • gateway相关参数,强制等集群数量够了再恢复 • 集群内同时只能迁移⼀个 shard • cluster.routing.allocation相关参数,加⼤并发 • 迁移限速 20MB • indices.recovery相关参数,放宽限速 • ⺫前 30个节点的集群,全集群重启 20分钟恢复完毕 (副本分⽚恢复速度⺫前⽆解,在 ES2.0计划中有对⽇志场景冷索引关闭 IndexWriter结构后加快恢复的计划 )。 • 注: 1.5.1之前, ES另外还有⼀个 bug导致分⽚恢复过程卡在 translog阶段过不去。⽽ 1.4.0之前默认开启的 bloom filter偏巧可以掩盖这个问题。 ES稳定性问题 (3) • 新增节点被压死 • ES集群对分⽚分布的默认策略: • 以节点为统计单位,尽量每个节点的分⽚总数均衡 • 节点磁盘使⽤率达到阈值的时候,不接新分⽚ • 所以新增节点会被尽量分配多分⽚来做到总数均衡。但 是⽇志场景下,只有当天热索引的分⽚有 IO压⼒。所以 新增节点的第⼆天,全部热索引分⽚都集中在这台机器 上,直接被干死! ES稳定性问题 (3) • 新增节点被压死的解决办法 1. 在新建索引出来之前,完成 relocation。 2. 通过 index.routing.allocation.total_shards_per_node 配置强制每个索引在单节点上的分⽚数。 • 注 1:强制要放宽余量,不然在故障的时候,副本要迁 移却没地⽅允许它落地 …… • 注 2:已经悲剧了的索引就不要亡⽺补牢了,结果只会 对新节点 IO雪上加霜。 ES稳定性问题 (4) • replica的 async⽅式 • ES默认要求数据在发送给 replica shards的时候,达到⼀半以上 shards成功,才返回写⼊ 成功。 • 看似如果采⽤ async⽅式,能提⾼写⼊性能? • 实际上, async⽅式没有数据校验,没法流控。⼀旦因为负载⾼导致某个 segment有偏 差,反复修复会导致恶性循环, cpu util%飙升,反⽽写⼊性能下降。 • 注: ES2.0⾥将取消这个功能。 ES性能问题 (1) • bulk批量写⼊,经常返回状态码 429,队列容量不 ⾜。 • 我们单条⽇志⻓度较⼤, nginx⽇志平均 600B, client⽇志平均 2KB。尤其是 client_net_fatal_error 有时候单条过 MB。 • ES接收 HTTP请求是可以 101接收,但是处理 HTTP 完整 body的上限是 100MB。所以 bulk_size需要根 据⽇志单条⻓度仔细计算。 ES性能问题 (2) • 数据容量膨胀率较⾼ (默认 logstash模板膨胀到 3倍⼤ ),对 IO和容量都有压 ⼒。要减少冗余数据,减少不必要的分词。 • _source: 存储源 JSON • _all: 存储各 field的 term⽅便在不指定 field的时候直接搜索单词 • multi-field: logstash默认模板会对每个字段附加⼀个 .raw不分词字段 • 所以: • 对于字段很明确的⽇志,如 nginx⽇志,舍弃 _all。 • 对于没啥⽂本,都是数值的聚合需求的监控数据,可以放弃掉 _source。 • 对于⼤多数字段内容很明确不会有拆分搜索和统计的直接设置不分词。 ES性能问题 (3) • ⻓期处于 CPU util%⾼⽔位运⾏。 • CPU主要就是 segment merge(永远的 hot threads)。默认规则是: • 单个 segment上限 5GB • 单个 segment下限 2MB • 考虑⽇志场景不要求多么实时,所以最简单的办法:加⼤ refresh(默认 1s) 和 flush(默认 200MB)的 interval。 cluster.name: es1003 cluster.routing.allocation.node_initial_primaries_recoveries: 30 cluster.routing.allocation.node_concurrent_recoveries: 5 cluster.routing.allocation.cluster_concurrent_rebalance: 5 cluster.routing.allocation.enable: all node.name: esnode001 node.master: false node.data: data node.max_local_storage_nodes: 1 index.routing.allocation.total_shards_per_node : 3 index.merge.scheduler.max_thread_count: 1 index.refresh_interval: 30s index.number_of_shards: 26 index.number_of_replicas: 1 index.translog.flush_threshold_size : 5000mb index.translog.flush_threshold_ops: 50000 index.search.slowlog.threshold.query.warn: 30s index.search.slowlog.threshold.fetch.warn: 1s index.indexing.slowlog.threshold.index.warn: 10s indices.store.throttle.max_bytes_per_sec: 1000mb indices.cache.filter.size: 10% indices.fielddata.cache.size: 10% indices.recovery.max_bytes_per_sec: 2gb indices.recovery.concurrent_streams: 30 path.data: /data1/elasticsearch/data path.logs: /data1/elasticsearch/logs bootstrap.mlockall: true http.max_content_length: 400mb http.enabled: true http.cors.enabled: true http.cors.allow-origin: "*" gateway.type: local gateway.recover_after_nodes: 30 gateway.recover_after_time: 5m gateway.expected_nodes: 30 discovery.zen.minimum_master_nodes: 3 discovery.zen.ping.timeout: 100s discovery.zen.ping.multicast.enabled: false discovery.zen.ping.unicast.hosts: ["10.19.0.97","10.19.0.98","10.19.0.99"] monitor.jvm.gc.young.warn: 1000ms monitor.jvm.gc.old.warn: 10s monitor.jvm.gc.old.info: 5s monitor.jvm.gc.old.debug: 2s ES的搜索问题 (1) • 搜索结果跟过滤条件 “明显不对 ”。 curl es.domain.com:9200/logstash-accesslog-2015.04.03/nginx/_search?q=_id:AUx- QvSBS-dhpiB8_1f1\&pretty -d '{ "fields": ["requestTime"], "script_fields" : { "test1" : { "script" : "doc[\"requestTime\"].value" }, "test2" : { "script" : "_source.requestTime" }, "test3" : { "script" : "doc[\"requestTime\"].value * 1000" } } }' NOT schema free! "hits" : { "total" : 1, "max_score" : 1.0, "hits" : [ { "_index" : "logstash-accesslog-2015.04.03", "_type" : "nginx", "_id" : "AUx-QvSBS-dhpiB8_1f1", "_score" : 1.0, "fields" : { "test1" : [ 4603039107142836552 ], "test3" : [ -8646911284551352000 ], "requestTime" : [ 0.54 ], "test2" : [ 0.54 ], } } ] } ES的搜索问题 (2) • 部分⽇志搜不着! • 因为ES要求同⼀个_index下同⼀个_type的同⼀个名字的field, mapping type必须⼀致。否则整条数据直接写⼊失败! • client_net_fatal_error⽇志中,某次发版后字段内容变了: • {"reqhdr":{"Host":"api.weibo.cn"}} • {"reqhdr":"{\"Host\":\"api.weibo.cn\"}"} • 设置object的mapping为{"enabled":false},跳过indexing阶段。 意味着这部分内容不能搜索,只能在_source JSON中可见。 ES的搜索问题 (3) •部分⽇志还是搜不着! •logstash⾃带template中对不分词的字段定义了ignore_above:256。也就是字段内 容超过256字节,就⾃动跳过indexing阶段。没有fielddata,针对这条记录的这个 字段的搜索和统计都是⽆效的。 curl 10.19.0.100:9200/logstash-mweibo-2015.05.18/mweibo_client_crash/_search?q=_id:AU1ltyTCQC8tD04iYBIe\&pretty -d '{ "fielddata_fields" : ["jsoncontent.content", "jsoncontent.platform"], "fields" : ["jsoncontent.content","jsoncontent.platform"] }' ... "fields" : { "jsoncontent.content" : [ "dalvik.system.NativeStart.main(Native Method)\nCaused by: java.lang.ClassNotFoundException: Didn't find class \"com.sina.weibo.hc.tracking.manager.TrackingService\" on path: DexPathList[[zip file \"/data/app/com.sina.weibo-1.apk\", zip file \"/data/data/com.sina.weibo/code_cache/secondary- dexes/com.sina.weibo-1.apk.classes2.zip\", zip file \"/data/data/com.sina.weibo/app_dex/ dbcf1705b9ffbc30ec98d1a76ada120909.jar\"],nativeLibraryDirectories=[/data/app-lib/com.sina.weibo-1, /vendor/lib, / system/lib]]" ], "jsoncontent.platform" : [ "Android_4.4.4_MX4 Pro_Weibo_5.3.0 Beta_WIFI", "Android_4.4.4_MX4 Pro_Weibo_5.3.0 Beta_WIFI" ] } Kibana⼆次开发 • 升级 k3的 elastic.js版本,到 ES1.2的 API。可以使⽤ ES1.0版本新增的 aggs接⼝ (新写了 percentile和 range两个 panel,重写了 histogram panel,添加了 cardinality metric)。 • 给 table添加 export as csv功能。 • 给 bettermap添加地图切换功能。 • 给 map添加 term_stats功能,添加 china选项。 • 通过 mapping多选框⾃助⽣成 querystring。 • 给 terms panel添加 script field功能,添加 either filtering多选功能。 • 还有⼗余项其他优化详⻅ 参考阅读 •《 Elasticsearch 服务器开发(第2版)》 •《 ⽇志管理与分析权威指南》 •《 数据之魅:基于开源⼯具的数据分析》 •《 ⽹站运维:保持数据实时的秘笈》 •《 Web 容量规划的艺术》 •《 ⼤规模 Web 服务开发技术》 •https://codeascraft.com/ •http://calendar.perfplanet.com •http://kibana.logstash.es –JordanSissel@logstash.net “If a newbie has a bad time, it's a bug.”
还剩72页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

mpgg2

贡献于2015-09-27

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf