hadoop3.X版本特性:纠删码

2020/11/4 [email protected]

一、纠删码(Erasure Coding/EC)

1.1背景

​ 从存储领域来看,数据冗余机制其实这几十年来没有太多进展,RAID,副本一直是当仁不让的最终选择。而近几年,尤其是规模较大的应用场景下,纠删码越来越多的出现在选择的视野范围,成为RAID,副本之外的第三种选择,因此也获得了越来越多的关注。

​ 从传统情况来看,RAID通常用在企业级环境里较多,在几台和十几台存储设备规模的IT系统中,一向是使用稳定可靠历经数十年磨砺的RAID技术。而在数据中心级的大规模部署中,RAID不再受欢迎,大部分的分布式系统都偏好副本模式,看重的是其高可靠性和读性能优化的特点。然而副本带来的成本压力实在是有些吃不消,这时候Erasure Code适时出现,以更低成本和更高技术含量提供近似可靠性这几点,就吸引到了众多分布式存储/云存储的厂商和用户。

  • RAID(独立磁盘冗余阵列)

​ 把相同的数据存储在多个硬盘的不同的地方(因此,冗余地)的方法。通过把数据放在多个硬盘上,输入输出操作能以平衡的方式交叠,改良性能。因为多个硬盘增加了[平均故障间隔时间(MTBF),储存冗余数据也增加了容错。

​ RAID技术主要有以下三个基本功能:

(1)通过对磁盘上的数据进行条带化,实现对数据成块存取,减少磁盘的机械寻道时间,提高了数据存取速度。

(2)通过对一个阵列中的几块磁盘同时读取,减少了磁盘的机械寻道时间,提高数据存取速度。

(3)通过镜像或者存储奇偶校验信息的方式,实现了对数据的冗余保护。

  • 副本

​ HDFS通过多副本机制来保证。在HDFS中的每一份数据都有两个副本,1TB的原始数据需要占用3TB的磁盘空间,存储利用率只有1/3。而且系统中大部分是使用频率非常低的冷数据,却和热数据一样存储3个副本,给存储空间和网络带宽带来了很大的压力。

  • 纠删码

​ 纠删码技术简称EC,本身是一种编码容错技术,最早是在通信行业解决部分数据在传输中损耗的问题,它的基本原理是把传输的信号分段,加入一定的校验再让各段间发生一定的联系,即使在传输过程中丢失掉部分信号,接收端仍然能通过算法把完整的信息计算出来。

​ 如果严格的区分,实际上按照误码控制的不同功能,可分为检错、纠错和纠删三种类型。

(1)检错码:识别错码

(2)纠错码:识别错码,纠正错码

(3)纠删码:识别错码,纠正错吗,删除超出纠正范围的错码

​ 将纠删码与HDFS集成可以提高存储效率,同时仍提供与传统的基于复制的HDFS部署类似的数据持久性。例如,一个具有6个块的3x复制文件将消耗6 * 3 = 18个磁盘空间。但是,使用EC(6个数据,3个奇偶校验)部署时,它将仅消耗9个磁盘空间块。

1.2纠删码原理

Reed-Solomon(RS)码是存储系统较为常用的一种纠删码,它有两个参数k和m,记为RS(k,m)。如下图所示,k个数据块组成一个向量D被乘上一个生成矩阵B从而得到一个码字向量,该向量由k个数据块和m个校验块构成。如果一个数据块丢失,可以用(GT)-1乘以码字向量来恢复出丢失的数据块。RS(k,m)最多可容忍m个块(包括数据块和校验块)丢失。

  • RS(k,m)计算方法

以冗余级别为5+3的纠删码为例说明。将n个源数据块D1~Dn按列排成向量D,再构造一个(n+m)n矩阵B (图2),B称为分布矩阵。对矩阵B有一个要求:它的任意n个行向量都是相互独立的,即这n个行向量组成的nn矩阵可逆。

(由线性代数知道,对互不相等的实数a1,a2,…,ak(k≥n),矩阵V的任意n行组成的矩阵都可逆。
在这里插入图片描述

从矩阵V中取出m行,用做分布矩阵B的下部m行,恰好满足对B的要求:任意n行都相互独立。例如冗余级别为5+3纠删码的分布矩阵B可以采用如下形式。

在这里插入图片描述
在这里插入图片描述
​ 图2
执行矩阵向量乘B*D,得到m个校验块C1~ Cm(图3)。

在这里插入图片描述
​ 图3

  • 数据恢复算法

(1)假设m个硬盘发生了故障,即图4中的数据块D1、D4、C2丢失,需要从剩下的n个数据块中恢复出来源数据D1~Dn

在这里插入图片描述
​ 图4

(2)从矩阵B中将剩余数据块对应的行向量挑出来,组成新矩阵B’,B’乘以向量D的结果恰好是未故障的数据块(图5)

在这里插入图片描述
​ 图5

(3)因为B的任意n行组成的矩阵都可逆,所以矩阵B’存在逆矩阵,记为B’-1,显然有B’-1*B’=E(单位矩阵乘任何矩阵B都等于B)。
在这里插入图片描述
(4)将图5公式的左右两边同时左乘矩阵B’-1,就得到了n个源数据块D1~Dn,完成数据恢复.

矩阵乘矩阵的逆结果为等行列的单位矩阵

在这里插入图片描述

  • 小结:

​ 纠删码和RAID技术看起来是有些类似,一个条带是由多个数据块构成,分为数据块和校验块。但与RAID5、RAID6不同的是,纠删码从功能上来看最大的区分特点是校验和数据的比例按N+M可调整,并且校验块数量不再受限于2个(RAID最多2个,比如RAID6),纠删码的M可以是3、4甚至更多。

​ 相对传统RAID技术,系统的容错能力得到大幅度的提高,即可以允许系统内同时损坏的硬盘数量(或者存储节点数)大幅度增加;实现了多对多的快速的数据重构,传统RAID方式需要十几个小时的重构时间,而这种方式磁盘重构时间可以缩短至分钟级的速度。

1.3块存储方式

​ 传统模式下HDFS中文件的基本构成单位是block,而EC模式下文件的基本构成单位是block group。以RS(3,2)为例,每个block group包含3个数据块,2个校验块。

  • 连续布局:文件数据被依次写入块中,一个块写满之后再写入下一个块,这种分布方式称为连续布局。

    (1)优点:

    ​ 容易实现

    ​ 方便和多副本存储策略进行转换

    (2)缺点:

    ​ 需要客户端缓存足够的数据块、

    ​ 不适合存储小文件

  • 条形布局:条(stripe)是由若干个相同大小的单元(cell)构成的序列。文件数据被依次写入条的各个单元中,当一个条写满之后再写入下一个条,一个条的不同单元位于不同的数据块中。这种分布方式称为条形布局。

    (1)优点:

    ​ 客户端缓存数据较少

    ​ 无论文件大小都适用
    (2)缺点:

    ​ 会影响一些位置敏感任务的性能,因为原先在一个节点上的块被分散到了多个不同的节点上

    ​ 和多副本存储策略转换比较麻烦

在这里插入图片描述

  • 布局的选择

​ 连续布局实现起来较为容易,但它只适合较大的文件。如果让client端直接写一个连续布局文件需要缓存下足够的数据块,然后生成校验块并写入,以RS(6,3),blockSize=128M为例,client端需要缓存1.12G的数据,这点决定了连续布局的文件更适合由普通文件转化而来,而条形布局就不存在上述缺点。

​ 由于一个条的单元往往较小(通常为64K或1M),因此无论文件大小,条形布局都可以为文件节省出空间。client端在写完一个条的数据单元后就可以计算出校验单元并写出,因此client端需要缓存的数据很少。 条形布局的一个缺点是会影响一些位置敏感任务的性能,因为原先在一个节点上的一个块被分散到了多个不同的节点上。

​ 文件大小是最关键的决定因素。如果集群中存储的都是大文件 - 每个文件至少由6个128MB的block组成,可以满足RS(6,3)模式下的完整EC组 - 那么连续布局是合适的,因为我们可以不用去实现合并多个小文件到一个EC组。但是如果集群中保存的是大量小文件,从存储成本和管理上来说的话,条带化布局是更好的选择

​ 基于以上分析,HDFS EC优先考虑对条形布局的支持

1.4NameNode端扩展

​ 在EC模式下,构成文件的基本单位为块组。一个块ID有64位,这里将第1个位作为flag来区分块的类型:如果为1,则为EC块(条形布局的EC块,连续布局将在第二阶段考虑);如果为0,则为普通块。对EC块来说,会将剩下的63位分成两部分:最后的4位用来标识内部块在块组中的位置,前面的59位用来区分不同的块组。块组ID等同于第0个内部块ID,其他的内部块ID可由块组ID加上其在块组中的位置索引得到,比如第0个内部块ID为0xB23400(也即块组ID),那么第3个内部块的ID为0xB23403。由于只是用最后4位来区分一个块组中的内部块,因此对一个块组来说,系统目前支持最多16个内部块。

1.5Client端扩展

​ HDFS客户端的主要I/O逻辑在DFSInputStream和DFSOutputStream中实现。为了支持数据条带化和EC,将它们扩展为DFSStripedInputStream和DFSStripedOutputStream。扩展背后的基本原理是允许客户端节点并行处理逻辑块中的多个存储块。当与HDFS加密一起使用时,这些扩展在加密数据上运行 - 即在加密层下面。

​ 在输出/写入路径上,DFSStripedOutputStream管理一组数据流(data streamers),每个DataNode用于在当前逻辑块中存储内部存储块。streamers大多是异步工作的。协调器负责整个逻辑块的操作,包括结束当前逻辑块,分配新的逻辑块,等等。

​ 在输入/读取路径上,DFSStripedInputStream将请求的逻辑字节数据范围转换为存储在DataNode上的内部存储块。然后它并行发出读取请求。失败后,它会发出额外的解码读取请求。

1.6DataNode扩展

为了避免在客户端进行数据重建,这个成本往往较高,后台能够识别和修复DataNode故障是非常重要的。与以前的复制备份方式一样,NameNode需要负责跟踪EC条带中的缺失块,并给DataNode分配恢复这些缺失块的任务。DataNode上的恢复工作由新的ErasureCodingWorker(ECWorker)组件处理,该组件执行以下操作以重建缺少的EC块:

(1)从源节点读取数据:在ErasureCodingWorker启动时会初始化一个专用的线程池用于从不同的源节点读取数据块。基于EC schema,它调度对所有源目标的读取请求,并确保仅读取重建所需的最小输入块。

(2)解码数据并生成输出数据:与EC客户端类似,ECWorker会在Erasure Codec Framework中引入的编解码器框架完成解码/编码工作

(3)将生成的数据块传输到目标节点:解码完成后,它会将输出数据封装到数据包并将它们发送到目标DataNode。

1.7纠删码策略

  • 为了适应异构的工作负载,允许HDFS群集中的文件和目录具有不同的复制和纠删码策略。纠删码策略封装了如何对文件进行编码/解码。每个策略由以下信息定义:

​ (1)EC模式:这包括EC组(例如6 + 3)中的数据和奇偶校验块的数量,以及编解码器算法(例如Reed-Solomon,XOR)

​ (2)条带化单元的大小。这确定了条带读取和写入的粒度,包括缓冲区大小和编码工作。

  • 策略命名:编解码器-数据块数-奇偶校验块数-信元大小,当前支持六种内置策略

RS-3-2-1024kRS-6-3-1024kRS-10-4-1024kRS-LEGACY-6-3-1024kXOR-2-1- 1024k复制

  • 复制是一项特殊的策略,它只能在目录上设置,以强制目录采用3副本模式的复制方案,而不继承上层(祖先)的纠删码策略,此策略可以使得3副本复制方案与纠删码目录交错。复制策略始终处于启用状态,对于其他内置策略启用后,该复制策略被禁用。

  • 与HDFS的存储策略类似,在目录上设置纠删码策略,创建文件时,他将继承其最近祖先目录的EC策略

  • 目录级EC策略仅影响目录中创建的新文件。创建文件后,可以查询其纠删码策略,但不能更改。如果将纠删码文件重命名为具有其他EC策略的目录,则该文件将保留其现有的EC策略。将文件转换为其他EC策略需要重写其数据。通过复制文件(例:distcp)而不是重命名来做到这一点。

  • 允许用户通过定义XML文件定义自己的EC策略,该文件必须包含一下三个部分:

    (1)layoutversion:这表示EC策略XML文件格式的版本。

    (2)模式:这包括所有用户定义的EC模式。

    (3)策略:这包括所有用户定义的EC策略,每个策略均由架构ID和条带化单元的大小(cellsize)组成。

Hadoop conf目录中有一个名为user_ec_policies.xml.template的示例EC策略XML文件,用户可以参考该文件。

1.8纠删码配置

​ 默认情况下,所有的内置EC策略都是不可用的,除了由配置项 dfs.namenode.ec.system.default.policy指定的策略默认是可用的。集群管理员可以基于集群规模和期望的容错性使用命令 hdfs ec [-enablePolicy -policy ] 设置可用的策略例如,对于具有9个机架的群集,像RS-10-4-1024k这样的策略将不会保留机架级的容错能力,而RS-6-3-1024k或RS-3-2-1024k可能更合适。如果管理员仅关心节点级的容错能力,则只要集群中至少有14个DataNode ,RS-10-4-1024k仍然适用。通过“ dfs.namenode.ec.system.default.policy”配置来配置系统默认EC策略。使用此配置时,如果在“ -setPolicy”命令中未将任何策略名称作为参数传递,则将使用默认的EC策略(RS-6-3-1024k)。

​ Reed Solomon和XOR的编解码器实现可以使用以下客户端和DataNode的配置项进行配置:

​ (1)默认RS编解码器:io.erasureCode.codec.rs.rawcoders

​ (2)legacyRS编解码器:io.erasureCode.codec.xor.rawcoders

​ (3)配置项自定义编解码器:io.erasurecode.codec.self-defined-codec.rawcoders

tips:这些配置项的值都是有回退机制的编解码器名称列表。这些编解码器工厂按配置值指定的顺序加载,直到成功加载一个编解码器。默认的RS和XOR编解码器配置更喜欢纯Java的Native实现。没有RS-LEAGCY的Native编解码器实现,因此默认仅为纯Java实现。所有这些编解码器都使用纯Java实现。对于默认的RS编解码器,还有一个native实现,它利用Intel ISA-L库来提高编解码器的性能。对于XOR编解码器,也有一个native实现利用Intel ISA-L库提高编解码器性能。可以参阅“启用Intel ISA-L”一节了解更多信息。RS Legacy的默认实现是纯Java,默认的RS和XOR的默认实现是使用英特尔ISAL-L库的native实现。

​ DataNode上的纠删码后台恢复工作也可以通过以下的配置参数进行调整:

​ (1)条带读取超时时间:dfs.datanode.ec.restruction.stripedread.timeout.millis,默认为5000ms

​ (2)读服务的缓冲区大小:dfs.datanode.ec.restruction.stripedread.buffer.size,默认64kb

​ (3) DataNode用于后台重建工作的线程数:dfs.datanode.ec.restruction.threads,默认8线程

​ (4)EC后台恢复任务与复制的块恢复相比使用的xmits的相对权重:dfs.datanode.ec.restruction.xmits.weight,默认0.5

设置为0表示禁用计算EC恢复任务的权重,也就是说,EC任务总是有1个xmit。纠删码码恢复任务的xmit取读取流数量和写入流数量之间的最大值。例如,如果一个EC恢复任务需要从6个节点读取并写入2个节点,那么它的xmits为max(6,2)*0.5=3。复制文件的恢复任务始终计为1 xmit。namenode使用 df.namenode.replication.max-streams 减去datanode上的 xmitsinprogress 总数,后者将replicated文件和ec文件中的xmits组合在一起,以将恢复任务调度到此 datanode。

1.9使用Intel ISA-L

HDFS默认的RS编解码器的native实现利用了 Intel ISA-L库提升编码和解码计算性能。使用 Intel ISA-L 有三个步骤:

  1. 构建 ISA-L库。详细信息,参阅官方网站 https://github.com/01org/isa-l/
  2. 构建hadoop对 ISA-L的支持,请参阅源代码(building.txt)中“Build instructions for Hadoop”中的“Intel ISA-L build options”部分。
  3. 使用 -Dbundle.isal 将isal.lib目录的内容复制到最终的tar文件中。使用tar文件部署hadoop。确保ISA-L在HDFS客户机和DataNode上可用。

使用hadoop checknative命令验证hadoop是否正确检测到ISA-L。

1.10HDFS-EC子命令

hdfs ec [通用选项]
     [-setPolicy -path <路径> [-策略<policyName>] [-复制]]
     [-getPolicy -path <路径>]
     [-unsetPolicy -path <路径>]
     [-listPolicies]
     [-addPolicies -policyFile <文件>]
     [-listCodecs]
     [-enablePolicy -policy <policyName>]
     [-disablePolicy -policy <policyName>]
     [-help[cmd ...]]

以下是有关每个命令的详细信息。

  • [-setPolicy -path [-policy ] [-replicate]]
    设置纠删码策略到指定路径的目录上,
    path: HDFS中的一个目录,这是一个必选参数,设置的策略只会影响到新建的文件,对于已经存在的文件不会有影响。
    policyName: 指定纠删码策略的名称,如果配置项 dfs.namenode.ec.system.default.policy设置了,这个参数可以省略。路径的EC策略就会使用配置项中的默认值
    -replicate 适用于特殊的REPLICATION策略,强制目录采用 3x 复制schema
    -replicate-policy 是可选参数,二者不能同时使用

    示例:

$ hdfs ec -setPolicy -path /tmp/ecdata
  Set RS-6-3-1024k erasure coding policy on /tmp/ecdata
  • [-getPolicy -path ]
    获取指定路径的目录或文件的纠删码的详细信息

    示例:

$ hdfs ec -getPolicy -path /tmp/ecdata
RS-6-3-1024k
  • [-unsetPolicy -path ]
    取消指定目录之前设置的纠删码策略,如果目录从祖先目录继承了纠删码策略,则unsetpolicy为no-op,也就是如果我们对一个目录执行了取消策略的操作,如果它的祖先目录设置过了策略,那么取消操作是不会生效的。在没有显式策略集的目录上取消策略不会返回错误。

    示例:

$ hadoop fs -mkdir /tmp/ecdata/data1
$ hdfs ec -getPolicy -path /tmp/ecdata/data1
RS-6-3-1024k
$ hdfs ec -unsetPolicy -path /tmp/ecdata/data1
Unset erasure coding policy from /tmp/ecdata/data1
$ hdfs ec -getPolicy -path /tmp/ecdata/data1
RS-6-3-1024k
$ hadoop fs -put test.zip  /tmp/ecdata/data1/123
$ hadoop fs -du -s -h /tmp/ecdata/data1
112.0 M  169.0 M  /tmp/ecdata/data1
  • [-listPolicies]
    列出所有注册到HDFS(enabled, disabled 和 removed)的EC策略,只有状态为enabled的策略才能使用setPolicy命令设置

    示例:

$ hdfs ec -listPolicies
Erasure Coding Policies:
ErasureCodingPolicy=[Name=RS-10-4-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=10, numParityUnits=4]], CellSize=1048576, Id=5], State=DISABLED
ErasureCodingPolicy=[Name=RS-3-2-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=3, numParityUnits=2]], CellSize=1048576, Id=2], State=DISABLED
ErasureCodingPolicy=[Name=RS-6-3-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=1], State=ENABLED
ErasureCodingPolicy=[Name=RS-LEGACY-6-3-1024k, Schema=[ECSchema=[Codec=rs-legacy, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=3], State=DISABLED
ErasureCodingPolicy=[Name=XOR-2-1-1024k, Schema=[ECSchema=[Codec=xor, numDataUnits=2, numParityUnits=1]], CellSize=1048576, Id=4], State=DISABLED
  • [-addPolicies -policyFile ]
    添加EC策略,可以参考etc/hadoop/user_ec_policies.xml.template文件查看示例策略文件,最大的cell大小有选项 dfs.namenode.ec.policies.max.cellsize 定义,默认是4MB。当前HDFS支持添加最多64种策略,策略ID的范围是 64-127,如果已经添加了64中策略,那么后面的添加会失败。

  • [-listCodecs]
    获取系统中支持的EC codecs 和coders 的列表。coder是codec的实现。codec可以有不同的实现,也就是不同的coder。一个codec的coder按返回顺序列出。

    示例:

$ hdfs ec -listCodecs
Erasure Coding Codecs: Codec [Coder List]
	        RS [RS_NATIVE, RS_JAVA]
	        RS-LEGACY [RS-LEGACY_JAVA]
	        XOR [XOR_NATIVE, XOR_JAVA]
  • [-removePolicy -policy ]
    删除EC策略

  • [-enablePolicy -policy ]
    启用EC策略

    示例:

$ hdfs ec -enablePolicy -policy XOR-2-1-1024k
$ hdfs ec -listPolicies
Erasure Coding Policies:
ErasureCodingPolicy=[Name=RS-10-4-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=10, numParityUnits=4]], CellSize=1048576, Id=5], State=DISABLED
ErasureCodingPolicy=[Name=RS-3-2-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=3, numParityUnits=2]], CellSize=1048576, Id=2], State=DISABLED
ErasureCodingPolicy=[Name=RS-6-3-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=1], State=ENABLED
ErasureCodingPolicy=[Name=RS-LEGACY-6-3-1024k, Schema=[ECSchema=[Codec=rs-legacy, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=3], State=DISABLED
ErasureCodingPolicy=[Name=XOR-2-1-1024k, Schema=[ECSchema=[Codec=xor, numDataUnits=2, numParityUnits=1]], CellSize=1048576, Id=4], State=ENABLED
  • [-disablePolicy -policy ]
    禁用EC策略

    示例:

$ hdfs ec -disablePolicy -policy XOR-2-1-1024k
Erasure coding policy XOR-2-1-1024k is disabled
$ hdfs ec -listPolicies
Erasure Coding Policies:
ErasureCodingPolicy=[Name=RS-10-4-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=10, numParityUnits=4]], CellSize=1048576, Id=5], State=DISABLED
ErasureCodingPolicy=[Name=RS-3-2-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=3, numParityUnits=2]], CellSize=1048576, Id=2], State=DISABLED
ErasureCodingPolicy=[Name=RS-6-3-1024k, Schema=[ECSchema=[Codec=rs, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=1], State=ENABLED
ErasureCodingPolicy=[Name=RS-LEGACY-6-3-1024k, Schema=[ECSchema=[Codec=rs-legacy, numDataUnits=6, numParityUnits=3]], CellSize=1048576, Id=3], State=DISABLED
ErasureCodingPolicy=[Name=XOR-2-1-1024k, Schema=[ECSchema=[Codec=xor, numDataUnits=2, numParityUnits=1]], CellSize=1048576, Id=4], State=DISABLED

参考资料

http://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs/HDFSErasureCoding.html

http://www.360doc.com/content/18/1117/18/99071_795528067.shtml

https://www.baidu.com/link?url=_V6dQjzPlRV2KZg9o2rt-CM8rCGUwLlAqUzYTLCkBKHdtrFGUiRYwsDXlM8L40RIhQIScST9lqBqsgddwBK2CFCSkYpTcit8SP29jJvPeEO&wd=&eqid=96b344a70000416a000000035fa251a7

https://www.baidu.com/link?url=yONiDBXtH3BEwZ4yu4e_ZF90pr_vtcpPt4VVxFDD8zijbxYXrgCL861r9XnOppWn9rl-GgH3TVHvYlCXbEkgZq&wd=&eqid=939abd480008f7bf000000035fa20b65

https://www.aboutyun.com/thread-26223-1-1.html

https://blog.csdn.net/runningtortoises/article/details/51589918

https://blog.csdn.net/qq_42502354/article/details/105543553?biz_id=102&utm_term=HDFS%E7%BA%A0%E5%88%A0%E7%A0%81%E9%85%8D%E7%BD%AE&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-105543553&spm=1018.2118.3001.4449

(官方文档翻译)https://blog.csdn.net/CPP_MAYIBO/article/details/89978275?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160464561319724839258961%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=160464561319724839258961&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v28_p-9-89978275.pc_first_rank_v2_rank_v28p&utm_term=%E7%BA%A0%E5%88%A0%E7%A0%81%E9%83%A8%E7%BD%B2&spm=1018.2118.3001.4449

request%255Fid%2522%253A%2522160464561319724839258961%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=160464561319724839258961&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v28_p-9-89978275.pc_first_rank_v2_rank_v28p&utm_term=%E7%BA%A0%E5%88%A0%E7%A0%81%E9%83%A8%E7%BD%B2&spm=1018.2118.3001.4449

猜你喜欢

转载自blog.csdn.net/nothair/article/details/110392201