调度、模型、同步与任务——阿里云大数据数仓建设性能优化方案

摘要:对于阿里云大数据数仓建设性能优化而言,主要可以从调度优化、模型优化、同步优化以及任务优化这四个方面着手。其实,对于性能优化而言,最终还是会归结到“资源”之上,所以资源是否足够,分配是否合理也是我们在进行性能优化时必须考虑的关键所在。

本文将主要围绕以下四个方面进行介绍:调度优化、模型优化、同步优化以及任务优化。对于调度优化而言,将分享任务调度如何进行优化,以及如何看到调度的瓶颈点,以及在初步进行建设和使用数据仓库的任务之后,对于任务如何进行调整来满足业务的时间要求。对于模型优化而言,主要包括一些优化相关的想法、建议以及技术的优化点。对于数据同步优化而言,也是大家在建设数据仓库的过程中经常遇到的问题,也就是将数据从其他数据库同步过来或者向其他数据库进行数据同步的时候,经常会遇到一些像某些任务运行过慢或者影响其他任务的情况。对于任务优化而言,主要指的是计算任务,也可以理解为MaxCompute的SQL任务,这部分将与大家分享如何去优化这部分的任务。

一、调度优化
在数据仓库建设的过程中,大家都会需要跑一些任务,那么这些任务如何进行配置才会是最优的呢?如果出现了瓶颈点或者业务第二天所需要的数据并没有给到,那么很大一部分的情况需要从调度方面来考虑,是不是有些任务的时间点设置的不合理?或者是不是有些任务的优先级设置的不合理?这些可能是在调度层面,大家需要优先考虑的一个点。

调度优化方式
调度优化的主要方式如下图所示,按照道理前三点应该在设计初期提前想到或者提前规划好的。而目前大部分客户还是用了一段时间的数据仓库的时候,才发现存在一些问题,当第二天需要出报表的时候才想到去优化这些点。

第一点就是对于大任务而言,需要将其预定处理的时间提前,这里的大任务也就是耗时比较长的任务,如果任务已经在跑了,那么很好评估,在DataWorks里面可以看到哪些任务跑得慢。此外还有一个评估方法就是在第一次建立数仓的时候,表的数据量很大那么也肯定是大任务。对于这些大任务而言,需要将其定时的时间提前,也就是将其优先级提前。第二点就是将关键节点的定时时间提前,这里所谓的关键节点并不是说其数据量大,而是业务很重要的任务。第三点就是需要做到任务的隔离,这里主要指的是在使用DataWorks的时候会用到一些调度资源,不管是运行SQL也好还是运行同步任务也好,这些任务都需要跑在DataWorks的调度资源里面,那么如果将这些任务都放在一个项目就会出现问题,比如某个同步任务设置了10个并发,这样就占据了10多个调度资源,这样就可能将资源全部占满了,这样就会导致其他任务需要等待,这里不是指的MaxCompute资源不够,而是DataWorks的调度资源不够了。因此,一方面需要将开发和生产隔离开,避免因为开发临时启动了测试任务导致生产环境受到影响,因此要尽量将DataWorks里面的开发项目和生产项目分离开。此外,如果生产项目也很大,可能就需要按照数仓的分层或者不同业务拆分成不同的项目,这也是避免资源出现抢占,避免影响其他业务的一种方式。当然,这样的做法有利也有弊,因为这样做会使得复杂度增加,对于企业而言,后续的运维成本也会高一些,因此这需要看大家应该如何评估,如果数据量达到了一定的规模其实可以分拆出来的,但是如果数据量不是很大,那么就可以先不考虑分拆。第四点就是减少任务层级的依赖,大家在进行调度的时候,都会在DataWorks里面看到依赖的上一层或者下游依赖哪一些任务,而任务互相依赖的层级应该是越少越好的,但是按照数仓分层,依赖至少需要三层,这三层依赖是肯定存在的,除此之外还会有一些中间表,这样就会有四层或者五层,但是尽量不要出现10层以上甚至20层的依赖,这样复杂的任务依赖会使得后期去排查任务的依赖成本升高。如果在数仓建设的初期或者建设的过程中发现了一些问题就可以从以上四个点出发进行考虑。

二、模型优化
对于模型优化而言,必须要按照什么方式进行设计以及模型必须是什么样子的,其实没有一个定性的结论。这里也只是给出一些建议和想法。

对于数仓的建模而言,其实可以分为3NF建模和维度建模等,而推荐使用维度建模方式,可以按照星型模型或者雪花型架构的方式去建模。3NF建模方式或者实体建模方式的应用型会差一点,在很多时候其性能也会差一点,但是3NF在很多时候都会避免数据的冗余,其扩展性会好一些。而维度建模会有一定的数据冗余,并且冗余程度会很高,但是对于上层使用者而言,其易用性要好很多,并且其查询的性能也会好很多,虽然可扩展性会稍微差一些,但是仍然处于可接受的范围之内。之所以在MaxCompute这边推荐大家使用维度建模,是因为其特点之一就是会存在数据冗余,但是数据冗余对于MaxCompute这种离线数据仓库来说,存储成本并不是很高,因为其都属于SATA盘的存储,这样的存储成本是很低的,而传统的数据仓库比如使用Oracle等其他的关系型数据库构建的数据仓库,大家往往会选择3NF的建模方式,这是因为其数据冗余存储成本会很高,磁盘很贵。

总之,在MaxCompute上推荐大家使用维度建模,使用星型建模或者雪花型建模的方式,这无论对于后续的运维还是后续对于数据的使用而言,都是比较便利的,并且性能也会好一些。星型模型其实就是中间一个事实表,周边围绕着一堆维度表,其结构会简单一些,使用比较方便,性能也比较好;对于雪花模型而言,维度表可能还会继续关联其他的维度表,这种方式就是雪花模型,它会略微比星型模型复杂一些。其实星型模型也可以理解为较为简单的雪花模型。这里推荐大家使用星型模型,当然如果业务非常复杂,必须要使用雪花型也可以使用。这是因为星型模型虽然有数据冗余,但是其结构比较简单,容易理解,而且使用起来只需要A传给B就可以了,不需要再关联一个C。

除了上述两个较大的关键点之外,还有一些值得注意的小点,比如中间表的利用,在这部分主要是将数仓分为三层,第一层做缓冲,第二层做整合,第三层做应用。但是并不是严格地只能分为三层,中间还是会有一些中间表的,如果能够利用好中间表则会增强数仓的易用性以及整体的性能。其主要是在数仓的第二层里面,因为需要整合一些数据,但是整合之后的数据依旧是明细的,可能有几百亿甚至几千亿的量级,对于这些表而言,数据量往往很大,而且下游任务以及依赖于这个表的报表任务有很多,因此可以做一些轻度的汇总,也就是做一些公共的汇总的中间表,这样应用好了可以节省很多的计算量和成本的。虽然建议大家利用中间表,但是也不建议使用太多的中间表,这还是因为中间表越多,依赖的层级也会越多。

在某些情况下还需要进行拆表,比如某一个大表字段比较多,但是可能其中某两三个字段的产出比较慢,产出很慢可能是因为其加工逻辑很复杂或者数据量比较大导致的,而其他字段产出却是很快的,此时就可以将数据表拆开,将过慢的字段拆出来,并将原来正常的字段留在原来的表,这样就可以避免因为两个过慢的字段影响其他业务,拆表的场景虽然比较常见,但是可能不会在数仓建设初期就出现。

还有一种场景及就是合表,这与拆表是相对的,当大家使用数仓一段时间之后会发现A业务部门出了一些表,B业务部门也出了一些表,而这些表或者数据可能是重叠的,也可能业务含义是一样的,只不过字段不一样。对于这些表而言是可以进行合并的,因为在合并之后可以做整体批量加工的SQL,这样要比多个表批量加工的SQL复杂度要低很多,而且性能要好很多。对于分区的场景而言,也要合理地设置MaxCompute的分区。

此外还有拉链算法,这在传统数仓里面也会用到,大家往往会需要使用拉链算法来记录历史变化情况。而拉链算法会使得计算成本变得比较高,尤其在MaxCompute里面或者离线数仓Hive里面,这是因为其没有Update的操作,因此需要遍历全表,需要对比昨天的全量和今天的增量,甚至是比较昨天的全量和今天的全量,才能得到所想要的拉链算法的结果,这样的计算成本对于MaxCompute而言要高很多。如果数据量不大,每天做全量的拉链算法也是没有问题的,只需要考虑保留多久历史数据的问题。而实际上,有些业务不会关心这些历史数据的变化问题,对于这样的业务其实可以只保留最近多少天的历史数据就可以了。其实是因为MaxCompute这边的数据存储成本很低,如果不使用拉链算法,那么就意味着数据冗余会高很多,所以其实大家可以计算一下每天增量数据的存储成本有多少,再对比一下数据的计算成本,根据自己的业务进行均衡。但是如果每天增量数据达到百亿这种级别,保留全量数据肯定是不现实的,那么就还是去做拉链算法。

模型优化-合理设计分区
MaxCompute分区的功能一定要合理利用,这对于性能会产生很大的影响,一级分区一般都是按照天划分的,建议大家一天一个增量或者一天一个全量来做。二级分区的选择反而会多一些,首先大家可以选择是否建立二级分区,其次大家可以选择二级分区的建立方式。二级分区比较适合于在where语句中经常使用到的字段,而且这个字段应该是可枚举的,比如“男”和“女”这样的。这里还有一个前提,就是如果这个字段的值的分布是非常不均匀的,那么就不太建议做二级分区。

如下图中的例子所示,登录表每天会有9个亿的数据,而其中的一个字段是“是否登录成功”,成功可能有4亿,失败可能有5亿,这就比较适合做二级分区,因为比较均衡。第二个例子是用户访问表,每天新增20亿数据,其中一个字段是“页面访问状态”,成功访问“202”是18亿,而失败“203”只有0.5亿,其他就更少了,这样的字段就不适合做二级分区。在数量级不大的情况下,不建议做二级分区,因为几百万的数据在MaxCompute里面扫描起来也会很快,在数据量大了之后可以再考虑二级分区,因为MaxCompute本身对于分区有一个上限就是6万,也就是一级分区乘以二级分区的个数不能超过6万个。

三、同步任务优化
同步任务优化可以从下图所示的这样几个点进行考虑。正如下面的这张PPT中图所示。数据同步其实就是源库通过网络进入到DataWorks或者自定义的调度资源里,再从DataWorks里面同步到MaxCompute里面,或者反过来从MaxCompute同步到源库,但是无论怎么说同步就是分为这样的几个点:源库、网络1、DataWorks调度资源、网络2以及MaxCompute,出现瓶颈的地方也就在这几部分中,如果同步任务运行缓慢,那么瓶颈点就只能出现在这几个点中。最常见的情况就是从其他数据库向MaxCompute抽取数据,一般情况下的瓶颈点就在源库这部分,出现问题大家可以优先在源库处寻找。在网络层面,从DataWorks到MaxCompute之间的网络2大家一般不用关心,因为这部分是由阿里云负责的,但是从源库到DataWorks调度的网络1这一段需要由用户自己保证,公网、内网和专线,不同的网络环境中同步的速度也是不一样的。

再回到同步优化的几个关键点,首先核心同步任务需要定时优先考虑,如果表的数据量比较大或者业务的优先级比较高,那么这些绝对需要提前考虑,因为如果这样的任务不提前,那么排在其后面的任务就会受到影响。第二点就是网路对于同步性能的影响,公网、内网或者专线对于性能也会有一定的影响。第三点就是DataWorks调度资源对于同步任务的影响,大家在DataWorks里面进行同步都是使用默认的调度资源,如果同步任务设置的并发过高,就会导致某一个任务会影响其他任务,比如处理一百万数据启动了20个并发,显然这是没有必要的,但是这样就占掉了全部的同步任务,导致后续运行SQL以及其他的同步任务都跑不起来了,这是因为DataWorks的调度资源不够了。所以数据同步的并发绝对不是越多越好的,当处理一两百万数据的时候,仅需要2到3个并发就足够了。此外,还有就是如何判断源库和目标库哪个是瓶颈点。数据同步主要使用的是数据集成,当离线任务运行完成之后都会产生这样的一个日志,在日志的最后会显示开始时间、结束时间以及写入速度等。在图中有标红的两个点,分别是Task WaitWriterTime和Task WaitReaderTime。如果是从RDS往MaxCompute同步,那么Reader指的就是读取RDS等待的时间,那Writer指的就是写入MaxCompute的等待的时间,哪一边的时间更长就意味着哪一边存在瓶颈点,如果读的方面时间更长,那么就需要从RDS或者网络1入手,也就是通过两方面的时间来判断瓶颈点究竟在哪一部分。

计算任务优化
在计算任务优化部分部分,也只与大家分享在SQL部分开发者应该如何进行优化。大家平时在进行数据处理、数据清洗、数据转换、数据加工等过程中都会使用到SQL。对于SQL的优化而言,主要集中在这样的两个大方面进行:减少数据输入和避免数据倾斜。减少数据输入是最核心的一点,如果数据输入量太大,包括很多无效的数据,那么就会占用很多的计算资源。而数据倾斜是在离线的数仓里面经常会遇到的,几乎每个人都会遇到,数据倾斜也分为好几种,需要对应地进行优化。接下来就为大家展开进行论述。

在正式展开之前还需要讲解一下LogView的用法,因为想要判断问题究竟是因为什么导致都需要从分析LogView入手。每一个SQL执行的时候都会产生一个LogView,如下图中的网址所示,大家可以直接在浏览器打开,之后就能打开汇总的页面,再打开Detail就能看到如下图所示的明细页面。对于明细页面而言,首先需要关注左侧的执行计划,也就是分为了多少个Map、Reduce以及Join节点。其次需要关注TimeLine可以看到哪一个Map运行的时间长,这是寻找数据倾斜的依据。当点击每一个Map就能看到下面的明细,比如某一个Map有10个节点在跑,那么就会有10个点。对于明细而言,重点需要关注的也是TimeLine,需要关注在分成10个的节点里面,究竟哪一个跑得快,哪一个跑得慢。下图中就存在明显的倾斜,也就是0号节点跑得很慢,而其他的节点跑得就比较快,这样就是一个非常明显Map阶段的倾斜。而使用Long-Task则可以快速定位到跑得慢的节点,帮助进行快速定位。

当然目前也有了比较好用的工具——MaxCompute Studio,其对于LogView的支持更加强大。在这里面可以直接将刚才的网址粘贴过来,也可以直接连接MaxCompute的项目找到Instance,然后直接点击Instance查看其执行日志,甚至可以将LogView保存在本地或者在本地打开,而网页版本过期之后就无法打开了。MaxCompute Studio最好用的地方在于其时序图功能,时序图能够列出某一个时间段,哪一个节点跑得快,哪一个节点跑得慢,做一个整体地列举出来,更加方便地定位到Map、Reduce以及里面小的节点。还有一个分析功能,能够直接为用户提供结果,提示用户哪一个节点跑得慢,哪里出现长尾等问题。

分区的合理使用
前面讲述了分区应该如何设计,这里着重讲解分区应该如何使用。如果表存在一级分区,那么将分区的筛选放到了条件里面就是一种错误的写法,有可能导致全表扫描。最好的写法就是像下图右侧所示的一样,把Table1的PT先进行筛选做一个子查询,再把Table2的也做一个分区先筛选了作为t2,之后将它们两个Join在一起再加上一个where条件,这样就能避免全表扫描。对于PT而言,如果使用了系统自带的函数,应该会做分区裁剪,而如果使用了自定义的函数对于PT进行加工,并放到了where条件里就有可能导致全表扫描,而现在DataWorks里面也会有系统提示,也便于大家进行判断。

多路输入
在MaxCompute里面支持多路输入,可以读取一个表的数据并将其同时写入到两个地方,这样就保证了只做了一次查询,而可以直接生成两个结果表。下图是一个电商的例子,大概就是在销售订单表里面有卖家和买家,分别统计了卖家和买家的数量分别是多少,以前可能需要拆分成两个SQL,而现在可以用一个SQL同时统计两者的数量,只需要读取一次原表就可以了,既能够节省时间,也能够节省成本。

**慎用SELECT**
因为MaxCompute里面是列式存储,所以同一列的数据都是存储在一起的,甚至于因为列内容相似都会有一些压缩算法在里面。而SELECT
查询全列与直接查询两个字段的性能差距是非常大的,所以作为数据开发的规范,无论数据量大小都一定不要使用SELECT*就好了。

先过滤JOIN,REDUCE,UDF
还有一个减少数据量的办法就是在使用Join、Reduce或者UDF的时候,先做过滤在做具体的计算或者Join。

合表
合表也是减少数据输入的一种方式,它其实是从业务的角度切入考虑的。比如有一个业务分别用到了T1和T2的两列,另外一个业务分别用到了T1和T3的两列,而这两个业务其实是可以合并到一起的,但是却放到了两个表,而这样就可以将这两个表合并到一起,这样只做一次计算就完成了。

Map倾斜
在LogView里面有一个Map的时序,可以看到每个Map里面有多少个Instance,里面的哪一个耗时比较长就是发生了数据倾斜。同样的,在LogView里面也能找到Map的平均执行时间以及最大执行时间,如果两者相差很大,那么必然出现了倾斜。对于这样的问题,从业务层面进行解决一般是修改上游数据,让上游按照均衡的KV值进行重新分布。如果业务层面无法规避,那么可以调整Map的个数,也就是加大Map的计算节点,在默认情况下是每256M数据切一个节点,可以将其调小,也就加大了Map处理节点的个数,使得数据分割得更加均匀一些。

Join倾斜
Join阶段的倾斜也是比较常见的,这一现象的发现与Map倾斜基本相同,也是可以通过LogView判断。但是其解决方案却需要分为几种情况进行处理:
-情况1:如果为大表与小表(加载到内存不超过512M),则对小表加MAPJOIN HINT
-情况2:两个大表Join,KEY值出现数据倾斜,倾斜值为NULL,则需对NULL进行随时值处理
-情况3:两个大表Join,可以尽量先去重后再Join
-情况4:两个大表Join,业务层面考虑优化,检查业务的必要性

下图展现的是Join倾斜的几个具体例子,可以分析具体造成倾斜的情况做出相应的处理。

Reduce倾斜
Reduce倾斜现象的查看方式和前面的Map以及Join查看的方式相同,可以从TimeLine看到。可能的情况主要有以下四种:
-情况1:GROUP BY 某个KEY倾斜严重(1. 是否可以过滤 2. 写法改变,见图)
-情况2:DISTINCT引起的倾斜(打标+GROUPBY)
-情况3:动态分区引起的倾斜,尽量避免使用动态分区
-情况4:窗口函数引起的倾斜,尽量避免使用窗口函数,要视具体情况而定

Reduce倾斜-DISTINCT
如果是因为DISTINCT造成的数据倾斜,有一种解决方法就是打标+GROUPBY,比如在下图的例子中就是对于求IP段,求美国IP段、中国IP段以及总的IP段一共有多少个,左边这种图简单的写法,当出现IP Key的倾斜就会使得作业比较慢,那么就可以将其打散,先求解这条ID的记录是美国的还是中国的,在子查询里先做这一步,在外面再去求解总的Count或者Sum,从原本Map-Reduce两个阶段的处理改成了Map-Reduce-Reduce三个阶段处理,这种方案也能解决数据倾斜问题。

总结一下,性能调优归根结底还是资源不够了或者资源使用的不合理,或者是因为任务分配的不好,使得某些资源分配和利用不合理。大家需要根据本文的内容考虑如何将自己的任务打散,保证任务在规定的时间内能够执行完毕,同时能够保证成本的节约。当然了,大家不仅需要考虑MaxCompute的计算资源,也需要考虑DataWorks的调度资源,所以性能优化最终还是在和资源作斗争,看资源是否足够,分配是否合理。

原文链接请添加链接描述

本文为云栖社区原创内容,未经允许不得转载。

原文地址:http://blog.51cto.com/13876536/2154407

时间: 2024-11-07 00:28:18

调度、模型、同步与任务——阿里云大数据数仓建设性能优化方案的相关文章

企业大数据平台下数仓建设思路

免费开通大数据服务:https://www.aliyun.com/product/odps 介然(李金波),阿里云高级技术专家,现任阿里云大数据数仓解决方案总架构师.8年以上互联网数据仓库经历,对系统架构.数据架构拥有丰富的实战经验,曾经数据魔方.淘宝指数的数据架构设计专家. 与阿里云大数据数仓结缘 介然之前在一家软件公司给企业客户做软件开发和数仓开发实施,数仓开发和实施都是基于传统的基础架构.2008年加入阿里进入淘宝数据平台部后,他开始接触分布式计算平台Hadoop. 初始时在Hadoop平

阿里云大数据三次技术突围:Greenplum、Hadoop和“飞天”

阿里云大数据三次技术突围:Greenplum.Hadoop和"飞天"  对于企业来说,到底什么是云计算?相信很多企业都有这样的困惑,让我们一起回到这个原始的起点探讨究竟什么是云计算?云计算对于企业而言到底意味什么? 云计算的三条发展路径及三种落地形态 当回到最初的起点再审视云计算的发展路径,可以发现,经过十余年的发展演进,云计算有三条发展路径,并且最终沉淀下来了三种落地形态. 第一条路:源自于谷歌对大规模数据的处理,谷歌为全球的互联网用户提供同一个服务--搜索,它需要将全世界所有的网站

阿里云大数据总监:计算让城市更智能

摘要: 10月25日,2018中国计算机大会上举办了主题是智慧城市下个十年:智在"数"还是"术"的技术论坛.目前,城市已经具备了丰富多样的大数据积累,成体系的数据管理标准,开放自由的数据流动环境等重要基础,但对于已经同时掌握"数"据和技"术"的玩家,在许多城市和现实环境中,城市大数据管理和处理技术难以得到同步发展. 10月25日,2018中国计算机大会上举办了主题是智慧城市下个十年:智在"数"还是"

[转帖]etcd 在超大规模数据场景下的性能优化

etcd 在超大规模数据场景下的性能优化 阿里系统软件技术 2019-05-27 09:13:17 本文共5419个字,预计阅读需要14分钟. http://www.itpub.net/2019/05/27/1958/ 不明觉厉 作者 | 阿里云智能事业部高级开发工程师 陈星宇(宇慕) 划重点 etcd 优化背景 问题分析 优化方案展示 实际优化效果 本文被收录在 5 月 9 日 cncf.io 官方 blog 中,链接:https://www.cncf.io/blog/2019/05/09/p

大数据分页实现与性能优化

摘要:Web 应用程序中经常使用数据分页技术,该技术是提高海量数据访问性能的主要手段.实现web数据分页有多种方案,本文通过实际项目的测试,对多种数据分页方案深入分析和比较,找到了一种更优的数据分页方案Row_number()二分法.它依靠二分思想,将整个待查询记录分为2部分,使扫描的记录量减少一半,进而还通过对数据表及查询条件进行优化,实现了存储过程的优化.根据Row_number()函数的特性,该方案不依赖于主键或者数字字段,大大提高了它在实际项目中的应用,使大数据的分页效率得到了更显著的提

阿里云大数据MaxCompute计算资源分布以及LogView分析优化

摘要: 海量数据处理平台,服务于批量结构化数据的存储和计算,提供海量数据仓库的解决方案以及针对大数据的分析建模服务.(官方文档有这里就不多做介绍了)官方文档链接 优势 用户不必关心分布式计算细节,从而达到分析大数据的目的. MaxCompute(原ODPS)的概念 大数据计算服务(MaxCompute,原名ODPS)是一种快速.完全托管的PB/EB级数据仓库解决方案,具备万台服务器扩展能力和跨地域容灾能力,是阿里巴巴内部核心大数据平台,支撑每日百万级作业规模.MaxCompute向用户提供了完善

TI C6000 数据存储处理与性能优化

存储器之于CPU好比仓库之于车间.车间加工过程中的原材料.半成品.成品等均需入出仓库,生产效率再快,如果仓库周转不善,也必然造成生产阻塞.如同仓库需要合理地规划管理一般,数据存储也需要恰当的处理技巧来提升CPU的运算性能. 本文基于TI C6000系列DSP,介绍了与运算性能优化有关的存储器知识.针对具体的数据存储问题,给出相应的代码优化策略,并将容易混淆的概念集中讨论.  名词说明   EMIF: External Memory Interface PMC: Program Memory Co

阿里云大数据架构专业术语

来自大神:http://pengoneeast.blog.163.com/blog/static/55045403201526111648857/ 负载均衡(Server Load Balancer,简称SLB)是对多台云服务器进行流量分发的负载均衡服务.SLB可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性. SLB基本概念 SLB服务主要由3个基本概念组成. LoadBalancer代表一个SLB实例 Listener代表用户定制的负载均衡策略和转发规则 Ba

阿里云大数据计算服务 - MaxCompute (原名 ODPS)

原文地址:https://www.cnblogs.com/barrywxx/p/10739834.html