深入了解 Docker 存储驱动
MadSturgess
8年前
<p>如果你曾经上手用过Docker,你可能已经见过存储驱动(storage driver)这个词。或者你已经偶然听过graphdriver这个词,并心想这到底是个什么鬼?或者你听过大牛们谈论的话题中出现的aufs和devicemapper这样的词,你一定想知道他们都在讲的是什么? 最近,我协助The New Stack编辑了他们的容器生态系列丛书(container ecosystem series)中关于存储、网络和安全的第四本电子书。该书有一个标题为《处理容器存储的方式》(http://thenewstack.io/methods-dealing-container-storage/)的章节,章节开头花了几页讲了一些Docker中存储驱动相关的一般性主题,但是很快地话题就转移到了比较火热的持久型存储上面。介于该书中的介绍太简短,我想用本文对它做一点补充,并尝试着为诸君理清相关概念,同时介绍一些Docker引擎中相关的关键背景。</p> <p>首先厘清一个事情:有很多的资源能帮你理解持久性存储,volume API和一些插件如ClusterHQ推出的Flocker,EMC的rexray/libstorage项目,Portworx等等,它们都是在处理Docker生态中的持久性存储。但是这边文章仅仅关注的是当你运行docker run时,在将镜像分层组装进根文件系统过程中,目前有哪些选择可以做本地容器镜像存储。</p> <p>在过去已经有一些围绕这个主题的文章。Red Hat几年前发表了“graphdriver实现概概览”。在2015年,Jérôme Petazzoni做了一个关于Docker中各种graphdriver的历史和实现的演讲。今年早些时候,Jess Frazelle发表了她“特别实诚的graphdriver指南”,提供了当时所有选项一个很好而简短的介绍。但是从那时起已经有一些事情发生了变化:Docker 1.12中推出了一个新的驱动叫做“overlay2”,较之最初的overlay的实现有重大的提升,并且最近一些graphdriver的选项中增加了一些关于配额的支持。</p> <p>背景介绍就这么多,我想有必要关注并更加深入了解以下话题:</p> <ol> <li> <p>为什么Docker中存在graphdriver,它的角色是什么?</p> </li> <li> <p>为什么一种graphdriver不够用,要有这么多种选择?</p> </li> <li> <p>这么多种类的graphdriver,我该怎么选?</p> </li> </ol> <h3><strong>Graphdriver是何物?</strong></h3> <p>要开始理解graphdriver这个概念,我们首先得理解本地的Docker引擎有一个Docker镜像层的缓存。 层叠镜像模型(layered image model)是Docker引擎最具特色的特性之一。 它能允许一个或者多个容器进程共享文件系统内容。这些分层的缓存会在 docker pull 或者 docker build 命令执行的时候,显式地进行构建。当 docker run 运行的时候,因为分层在本地不存在需要从注册表中拉取的时候,也会向缓存中隐式地添加。要在运行的时候管理这些分层,需要一个支持一组特定能力的驱动 – 这组能力被抽象成接口可以在Docker引擎代码里找到 - 来将这些分层挂载到一个合并的根文件系统里面。因为分层内容的“镜像图(image graph)”代表了各种分层之间的关系,用来处理这些分层的驱动就被叫做“图驱动(graphdriver)”。</p> <p>在运行的时候用来处理这个 <em>分层图</em> 有两个非常重要的概念。一个概念是联合文件系统(union filesystem),它最好的定义位于维基百科词条中。( 译注:该维基词条说的是,“在操作系统中,联合挂载(union mounting)是一种将多个目录结合成一个目录的方式,这个目录看起来就像包含了他们结合的内容一样。” ) 联合文件系统的实现将这种文件系统内容合并来,形成一个单一的挂载点。除非只读的根文件系统已经能满足你的需求,联合文件系统的实现通常辅有“写时复制(CoW)”的实现技术,这样任何对于底层文件系统分层的更改都会被“向上拷贝”到文件系统的一个临时、工作、或高层的分层里面。这个可写的层然后可以被看做是一个“改动(diff)”,能将之应用到下层只读的层,而这些层很可能作为底层被很多容器的进程中共享。这是一个很重要的点。一个Docker中使用分层文件系统的好处就是,1000个运行着 bash 的 ubuntu:latest 容器的副本,会共享一个底层的镜像,而并不会产生1000个文件系统的副本(vfs是个例外,请参考下边vfs部分)。并且同样重要的是,对于aufs和overlay的实现,用来读取或执行共享库的共享内存也在所有运行的容器之间共享,大大的减少了通用库如’libc’的内存占用。这是一个分层策略的巨大优势,同时也是Docker的graphdriver是引擎中相当重要的一部分的原因之一。</p> <p>现在诸君应该清楚了graphdriver是何物了,并且为什么Dokcer引擎要实现这个特性。现在让我们接着看看为什么Docker中有众多graphdriver的选择吧。</p> <h3><strong>都有哪些Graphdriver?</strong></h3> <p>在最近的Docker引擎1.12版本中,会发现如下的graphdriver:vfs、aufs、overlay、overlay2、btrfs、zfs、devicemapper和windows。将这个列表归一下类有助于对它们进行更好地定义。</p> <h3><strong>特别的白雪公主:vfs</strong></h3> <p>首先,让我们挑出一个特别的graphdriver – vsf。vfs是接口的“原生”的实现,完全没有使用联合文件系统或者写时复制技术,而是将所有的分层依次拷贝到静态的子文件夹中,然后将最终结果挂载到容器的根文件系统。它并不适合实际或者生产环境使用,但是对于需要进行简单验证的场景,或者需要测试Docker引擎的其他部件的场景,是很有价值的。对于在Docker中运行Docker的场景也很有用,要知道graphdriver嵌套起来,可能会让你丈二和尚摸不着头脑。顺便一提:Docker引擎开发者用来构建Docker自己所使用的 Dockerfile ,也是采用vfs来作为里边Docker的graphdriver。</p> <h3><strong>联合文件系统</strong></h3> <p>有意思的是,现有的graphdriver中只有少部分是真正的有写时复制语义的联合文件系统:Overlay的两个版本,从Docker早期就存在的aufs。记住联合文件系统只是一个基于文件的接口,通过把一组目录交错起来来,形成一个单一视图。所以与它不是一个真正的文件系统,如ext4或者xfs,它仅仅是站在一个已有的文件系统上提供了这些功能。在一些场景,对于底层文件系统是有要求的,并且Docker也会同时检查请求的联合文件系统和底层的文件系统,来保证它们是兼容的。</p> <h3><strong>特定文件系统的实现</strong></h3> <p>剩下的graphdriver都是建立在具体文件系统实现的基础上,需要依赖其内置特性(如快照)能提供必需的能力。这些包含devicemaper、zfs和btrfs驱动。在这每一个情形中,你都需要新建一个磁盘并用该文件系统格式化磁盘(或者为了快速测试,用循环挂载的文件作为磁盘),来使用这些选项作为Docker引擎的存储后端。</p> <h3><strong>Graphdriver必须要执行什么操作?</strong></h3> <p>首先我们要简要地解释一下graphdriver必须执行什么操作。相关信息在已经被代码化在了守护进程代码库中 ProtoDriver 和 Driver 接口的定义中。同时值得注意的是,有一个ProtoDriver接口的包装器实现叫做NativeDiffDriver。对于那些无法通过原生处理方式来得出分层差异或改动的文件系统,该包装器能通过借用归档软件包,和驱动实现一起来提供这些计算差异的特性。 除了(计算)差别和改动相关的方法,graphdriver最重要的能力是 Get、 Put、 Create 和 Remove 方法 。要帮助理解graphdriver的API,我们需要简单地谈一下这个API的消费者。在Docker中的实现被称作 layerStore(分层仓库) 。当终端用户使用Docker客户端或者API下载或导入镜像时,分发(注册表)客户端代码用“分层仓库”来进行添加或者删除层的操作。我们知道镜像可以包含多个分层,并且这些分层有存在父级子级的关系。“分层仓库”的代码利用graphdriver驱动,采用最适合该文件系统实现中类似联合和写时复制(union+CoW-like)的叠层技术,来保存这些分层和它们之间的关系。要处理这些分层镜像的创建和解开(un-tar)操作,以及将镜像解开的内容放到创建的位置,会用到graphdriver的 Create 和 ApplyDiff 接口。显然,当镜像从本地缓存删除的时候需要执行的相反的操作,“分层仓库”会调用graphdriver的 Remove 接口来将分层的内容从系统中删除。</p> <p>经历了上面这些过程,graphdriver现在已经包含了很多分层的本地缓存,同时包含下载的具名镜像之间的关系。容器需要运行时,在容器启动之前这些必须被组装成可运行的根文件系统。graphdriver的 Get 方法会被调用并带上一个特定的标识符,此时根据graphdriver特定文件系统的实现,需要根据父级连接关系遍历并且使用该文件系统提供的相应技术来将分层堆叠成一个单独的挂载点,并创建可写的上层或者顶部分层来满足容器更改文件系统的需要。 Put 方法来告知graphdriver,某挂载的资源没有用了,并在绝大多数的场景下卸载相关的层。</p> <h3><strong>现在的Graphdriver概览</strong></h3> <p>知道了graphdriver在意图解决什么问题之后,让我们快速概览一下在当下Docker 1.12引擎中可以有哪些选择。对于那些将要尝试,或者已经尝试过不同的grpahdriver的人来说,因为每一个graphdriver的分层存储是依赖具体实现的,当更改了graphdriver并重启了Docker引擎后,之前拉取或者构建的任何镜像将无法继续使用。这是一个之前广为人知的会让用户困惑的地方,但是不要害怕;切换回之前的graphdriver会唤回之前的镜像或者容器,它们没有消失,只是在你切换了不同的graphdrirver之后从你的视野中躲起来了而已。</p> <h3><strong>AUFS</strong></h3> <p>历史:aufs驱动老早就在Docker中存在了!其实,他在使用 graphdriver 这个名字之前久存在了。如果你查看项目在那(即首次使用graphdriver名称)提交之前的历史,之前项目中当时只有一个aufs的实现。下边devicemapper部分会讲到更多关于graphdriver这个名称诞生的历史。</p> <p>实现:Aufs最初代表的意思“另一个联合文件系统(another union filesystem)”,试图对当时已经存在的UnionFS实现进行重写。正如你期望的那样,它是一个传统意义的上层覆盖,通过利用aufs称作为“分支(branch)”的特性,让堆叠的目录合并成一个堆叠内容单一挂载点视图。此驱动会将父级信息组合一个有序列表,并把它作为挂载参数,然后把重活移交给aufs来把这些分层组装成一个联合视图。更多的细节信息可以在aufs的帮助文档(http://aufs.sourceforge.net/aufs3/man.html)上看到。</p> <p>优点:这可能是历史最久且测试最完善的graphdriver后端了。它拥有不错的性能,也比较稳定,适用于广泛的场景。尽管它只在Ubuntu或者Debian的内核上才可以启用(下边有说明),但是这两个发行版和Docker一起使用的场景已经非常多,这让它在广阔的环境中得到了验证。同时,通过让不同的容器从同一个分层里面加载相同的库(因为他们在磁盘上是相同的inode)达到了共享内存页的效果。</p> <p>缺点:Aufs从来没有被上游Linux内核社区接受。多年来Ubuntu和Debian都需要往内核集成一个历史久远的补丁包,且原作者已经放弃了让它被内核采纳的努力。可能与IPV4和IPv6的辩论有些类似,人们担心某一天内核更新后会出现难以整合aufs的补丁的情况,从而导致aufs没得玩。但是就如IPv6,替换aufs势在必行的决心讲了一年又一年。除此之外,它面临着很多其他比较棘手的问题。其中一个最麻烦的、也是比较有历史的问题(尽管某种程度上这是一个安全的特性),是关于在高层更改向上拷贝的文件的权限的,这个问题困扰了不少用户。最终在2015年早期的时候通过编号为#11799(http://dockone.io/docker/docker#11799)的PR使用aufs的 dirperm1 特性修复了。自然,这需要内核中有具有 dirperm1 能力aufs,然而这在今天任何较新版本的Ubuntu或者Debian上都已经不成问题了。</p> <p>总结:如果你在使用Ubtuntu或者Debian,那默认的graphdriver就是aufs,它能满足你绝大多数需求。有人期望有一天它能被overlay的实现取代,但是考虑到overlay文件系统的诸多问题,以及在上游内核中的成熟程度等挑战,这尚未实现。最后,aufs中没有配额的支持。</p> <h3><strong>Overlay</strong></h3> <p><strong>历史</strong>:2014年8月,Red Hat的 Alex Larsson在编号为453552c8384929d8ae04dcf1c6954435c0111da0的代码提交中添加了针对OverlayFS(最初的上游内核的名称)的graphdriver。</p> <p><strong>实现:</strong>Overlay是一个联合文件系统,它的概念较之aufs的分支模型更为简单。Overlay通过三个概念来实现它的文件系统:一个“下层目录(lower-dir)”,一个“上层目录(upper-dir)”,和一个做为文件系统合并视图的“合并(merged)”目录。受限于只有一个“下层目录”,需要额外的工作来让“下层目录”递归嵌套(下层目录自己又是另外一个overlay的联合),或者按照Docker的实现,将所有位于下层的内容都硬链接到“下层目录”中。正是这种可能潜在的inode爆炸式增长(因为有大量的分层和硬连接)阻碍了很多人采用Overlay。Overlay2通过利用更高内核(4.0以及以上的版本)中提供了的更优雅处理多个位于下层分层的机制解决了这个问题。</p> <p>优点:Overlay作为一个合并进主线Linux内核的一个有完整支持的联合文件系统有望成为人们的焦点。与aufs类似,通过使用磁盘上相同的共享库,它也能让分散的容器实现内存共享。Overlay同时有很多的上游Linux内核基于现代的应用场景,如Docker,被持续开发(参看overlay2)。</p> <p><strong>缺点:</strong>硬链接的实现方式已经引发了 inode耗尽(http://dockone.io/docker/docker#10613)的问题,这阻碍了它的大规模采用。inode耗尽并不是唯一的问题,还有其他一些与用户命名空间、SELinux支持有关的问题,且整体的成熟状况不足也阻碍着overlay直接取代aufs成为Docker默认的graphdriver。随着很多问题的解决,特别是在最新的内核发新版中,overlay的可用度越来越高了。如今出现的Overlay2修复了inode耗尽的问题,应该是从Docker 1.12版本之后的焦点,成为overlay驱动的后续开发对象。出于向后兼容的原因, overlay 驱动将会继续留在Docker引擎中继续支持现有的用户。</p> <p><strong>总结:</strong>考虑到aufs没有足够多的发行版的支持,能有一个上游集成的联合文件系统且拥有Linux内核文件系统社区的支持,overlay驱动的加入是一个重大进步。Overlay在过去的18-24个月已经成熟了很多,并且随着overlay2的出现,它之前一些麻烦的问题已经解决了。希望overlay(或者更具可能性的overlay2)会成为未来默认的graphdriver。为了overlay最好的体验,上游内核社区在4.4.x的内核系列里面修复了很多overlay实现中存在的问题;选择该系列中更新的版本可以获得overlay更好的性能和稳定性。</p> <h3><strong>Overlay2</strong></h3> <p><strong>历史:</strong>Derek McGowan在编号为#22126(https://github.com/docker/docker/pull/22126)的PR中添加了overlay2的graphdriver,在2016年6月被合并进Docker 1.12版本,正如该PR的标题注明的,要取代之前overlay的主要原因是它能“支持多个下层目录”,能解决原先驱动中inode耗尽的问题。</p> <p><strong>实现:</strong>在上面的overlay部分已经讲述了Linux内核中的Overlay的框架。上面链接的PR中改进了原有的设计,基于Linux内核4.0和以后版本中overlay的特性,可以允许有多个下层的目录。</p> <p><strong>优点:</strong>overlay2解决了一些因为最初驱动的设计而引发的inode耗尽和一些其他问题。Overlay2继续保留overlay已有的优点,包括在同一个引擎的多个容器间从同一个分层中加载内库从而达到内存共享。</p> <p><strong>缺点:</strong>现在可能唯一能挑出overlay2的问题是代码库还比较年轻。很多早期的问题已经在早期测试过程中发现并被及时解决了。但是Docker 1.12是第一个提供overlay2的发行版本,随着使用量的增长,相信可能还会发现其他问题。</p> <p><strong>总结:</strong>将Linux内核中的一个现代的、广受支持的联合文件系统,和一个和Docker中一个性能优秀的graphdriver结合起来,这应该是Docker引擎未来打造默认的graphdriver最好的道路,只有这样才能获得各种Linux发行版广泛的支持。</p> <h3><strong>Btrfs</strong></h3> <p><strong>历史:</strong>2013年12月较晚的时候,Red Hat公司的Alex Larsson在编号为e51af36a85126aca6bf6da5291eaf960fd82aa56的提交中,让使用btrfs作为管理 /var/lib/docker 的文件系统成为可能。</p> <p><strong>实现:</strong>Btrfs的原生特性中,有两个是“子卷(subvolumes)”和“快照(snapshots)”。 (译注:根据Wikipedia,“子卷在btrfs中不是一个块设备,也不应该被当做是一个块设备。相反,子卷可以被想象成POSIX文件的命名空间。这个命名空间可以通过顶层的子卷来访问到,也可以独立地被挂载。快照在Btrfs中实际上是一个子卷,通过使用Btrfs的写时复制来和其他的子卷共享数据,对快照的更改不会影响原先的子卷。” ) graphdriver实现中主要结合了这两个能力,从而提供了堆叠和类似写时复制的特性。当然,graphdriver的根(默认情况下是: /var/lib/docker )需要是一个被btrfs文件系统格式化的磁盘。</p> <p><strong>优点:</strong>Btrfs几年前发布的时候(2007-2009时代),它被视作一个未来的Linux文件系统并受到了大量的关注(https://lwn.net/Articles/342892/)。如今在上游Linux内核中,该文件系统已经比较健壮,并受到良好的支持,是众多可选的文件系统之一。</p> <p><strong>缺点:</strong>但是Btrfs并没有成为Linux发行版的主流选择,所以你不大可能已经有一个btrfs格式化的磁盘。因为这种在Linux发行版中采用不足的原因,它并没有受到类似其他graphdriver一样的关注和采用。</p> <p><strong>总结:</strong>如果你正在使用btrfs,那很显然的这个graphdriver应该迎合了你的需求。在过去几年有过很多Bug,并且有一段时间缺乏对SELinux的支持,但是这已经被修复了。同时,对btrfs配额的支持也直接加进了docker守护进程中,这是Zhu Guihua在编号为#19651(http://dockone.io/docker/docker#19651)的PR中添加的,这个特性包含在了Docker 1.12版本中。</p> <h3><strong>Devicemapper</strong></h3> <p><strong>历史:</strong>Devicemapper很早就以C代码的包装器面貌存在了,用来和libdevmapper进行交互; 是2013的9月Alex Larsson在编号为 739af0a17f6a5a9956bbc9fd1e81e4d40bff8167的代码提交中添加的。几个月后的重构了才诞生了我们现在所知道的“graphdriver”这个词;Solomon Hykes在2013年10月份早期代码合并的注释中说:将devmapper和aufs整合进通用的“graphdriver”框架。</p> <p><strong>实现:</strong>devicemapper这个graphdriver利用了Linux中devicemapper代码中众多特性之一,“轻配置(thin provisioning)”,或者简称为“thinp”。 (译注:根据Wikipedia,“thin provisioning是利用虚拟化技术,让人觉得有比实际可用更多的物理资源。如果系统的资源足够,能同时满足所有的虚拟化的资源,那就不能叫做thin-provisioned。”) 这与之前提到的联合文件系统不同,因为devicemapper是基于块设备的。这些“轻配置(thin-provisioned)”的块设备带来的是如联合文件系统所提供的一样轻量的行为,但是最重要的一点是,他们不是基于文件的(而是基于块设备的)。正如你能推测的,这让计算分层之间的差别变得不再容易,也丧失了通过在容器间使用同样的库片段而共享内存的能力。</p> <p><strong>优点:</strong>Devicemapper在过去的年间也被一些人感到不屑,但是它提供的一个非常重要的能力让红帽系(Fedora,RHEL,Project Atomic)也有了一个graphdriver。因为它是基于块设备而不是基于文件的,它有一些内置的能力如配额支持,而这在其他的实现中是不容易达到的。</p> <p><strong>缺点:</strong>使用devicemapper没有办法达到开箱立即唾手可得很好的性能。你必须遵循安装和配置指示才能得到性能还可以的配置。并且最重要的是,在任何需要用Docke引擎来做点正事的地方,都不要使用“虚拟设备(loopback)”模式(对于运行有devicemapper且负载高的系统,如延迟删除( deferred removal)这样的特性绝对有必要的,这能减少引擎看起来好似夯住了一样的悲剧。)。它的一些特性依赖libdevmaper特定的版本,并且需要比较高级的技能来验证系统上所有的设置。同时,如果Docker Engine的二进制是静态编译的话,devicemapper会完全无法工作,因为它需要udev sync的支持,而这不能被静态编译进引擎中。</p> <p><strong>总结:</strong>对于红帽类发行版本来说,devicemapper已经成为“可以直接用”的选择,并且在过去几年间里得到了红帽团队的大力支持和改进。它质量上有优点也有缺点,如果安装/配置过程中没有特别格外注意的话,可能导致和其他选项比较起来性能低下、质量不高。鉴于overlay和overlay2受到了Fedora和RHEL最新的内核的支持,并且拥有SELinux的支持,除非在Red Hat场景中有某种必须使用devicemapper的需求,我想随着用户的成熟他们会转向overlay的怀抱。</p> <h3><strong>Zfs</strong></h3> <p><strong>历史:</strong>ZFS的graphdriver是由Arthur Gautier和Jörg Thalheim一起在#9411(http://dockone.io/docker/docker#9411)的PR中实现的,在2014年的5月被合并进了Docker引擎里面,并且从Docker 1.7版本开始用户可以使用。该实现依赖Go的一个三方包go-zfs(https://github.com/mistifyio/go-zfs)进行相关zfs命令的交互。</p> <p><strong>实现:</strong>与btrfs和devicemapper类似,要使用zfs驱动必需要有一个ZFS格式化的块设备挂载到graphdriver路径(默认是/var/lib/docker)。同时也需要安装好zfs工具(在绝大多数的发行版上是一个名为zfs-utils的包)供zfs Go库调用来执行相关操作。ZFS有能力创建快照(与btrfs类似),然后以快照的克隆作为分享层的途径(在ZFS的实现中成了一个快照)。因为ZFS不是一个基于文件的实现,aufs和overlay中所拥有的内存共享能力在ZFS是没有的。</p> <p><strong>优点:</strong>ZFS正在受到越来越多的欢迎,在Ubuntu 16.04中,在Ubuntu的LXC/LXD中已经被使用。最初由Sun创建,ZFS已经存在很长的时间了,并且在Solaris和很多BSD的衍生版中使用,并且它的Linux移植版实现看起来也比较稳定,对于容器文件系统的场景也有足够合理性能。 ZFS graphdriver也很及时的在Dockr 1.12中通过PR #21946(http://dockone.io/docker/docker#21946)添加了配额的支持,这让它在配额支持方面和btrfs、devicemapper站在了同一起跑线上。</p> <p><strong>缺点:</strong>除了没有基于文件(inode)的共享达到内库共享之外,很难说ZFS和其它同样基于块设备的实现相比有什么缺点。通过比较,ZFS看起来欢迎程度越来越高。对于那些完全支持或者正在使用ZFS的Linux发行版或者UNIX衍生版而言,zfs graphdriver可以是一个非常好的选择。</p> <p><strong>总结:</strong>ZFS的支持为Docker引擎中稳定的graphdriver加了分。对于那些ZFS的使用者,或者那些ZFS扮演了更要角色的发行版来说,Docker能直接支持该文件系统,对这些社区来说是一个好消息。对于那些默认文件系统是ext4和xfs的发行版,默认采用overlay驱动的用户来说,时间会告诉我们他们是否会对zfs驱动产生更多的兴趣。</p> <h3><strong>更深层次的细节</strong></h3> <p>要真的需要深挖每一个文件系统如何通过graphdriver来运作的需要更多的篇幅。更重要的是,Docker社区已经将一部分写成了文档,可以在官方的存储驱动文档查看。如果对任何graphdriver的细节有不清楚的,可以点过去看一下。这里是官方文档中有关每个graphdriver的链接:aufs、devicemapper、overlay、zfs和btrfs。</p> <p>细心的读者会发现,我在开头提到了“windows”的graphdriver,但是之后再也没有提到。很显然windows graphdriver是最近Docker向Windows Sever 2016移植中使用的graphdriver,这个消息是这周在Atlanta的MS Ignite宣布的。我本人没有足够多的细节,但希望以后我们能写一篇相关的文章或者链接到微软团队讲述该驱动是如何在Windows上运作的。</p> <p style="text-align:center"> </p> <p> </p> <p>来自:https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA==&mid=2649692382&idx=1&sn=a880d1220eefaa6d9a4ce39590af4e1d&chksm=889327bdbfe4aeab16e7fc1984d5e2ae72c9538daa0d0c54adcab16e1ea48c9374630edb609e&mpshare=1&scene=1&srcid=1101He3tp0eYRzE9OX75cUTw&pass_ticket=Ftr/CG7ZOlqH4VDW8cvl/MCaMrXKWNz3Jx94eDvx0 M=</p> <p> </p>