太厉害了!大神用62图带你入门Docker

admin 2025-02-28 68人围观 ,发现53个评论
第一趴---Docker容器圈简介

Docker容器圈简介

第二趴---Docker基本操作

Docker基本操作

容器圈

容器这个新生事物,现在还可以说是新生事物吗?对于我们学生而言,我觉得没毛病,你说呢?

Pass项目之所以被很多公司所接受,自然是因为解放了部分开发人员的劳动力,尽快干完活儿早点下班。其依赖的就是「应用托管」的能力,在电脑上斗过地主的应该知道,托管了以后就会自动出牌,同样的道理,为了尽量的弥补本地和云上的环境差异,就出现了Pass开源项目。

举个例子来说,运维人员小仙云上部署一个CloudFoundry项目,开发人员只需要简单的一行代码就可以实现将本地的应用部署到云上

就这样一行代码就实现了将本地应用上传到云上,属实很轻松。

实际上,我们可以将其最核心的组件理解为一套应用的打包和分发机制。云上部署的CloudFoundry会为大部分编程语言定义一种打包的格式,当开发人员执行命令的时候,实际上是将可执行文件和启动脚本打包上传到云上的CouludFoudry中,然后CloudFoundry通过相应的调度器选择一个虚拟机的Agent将压缩包下载后启动

虚拟机一般不可能只跑一个应用,因为这样确实也太浪费资源了,我们可以想想,现在手上的电脑,可以用Vmvare导入几个虚拟机,所以诸如CloudFoundry通过引入操作系统的Cgroups和Namespace等机制,从而来为每个应用单独创建一个叫做「沙盒」的隔离环境,然后在这些「沙盒」中启动应用,通过这样的方法就让虚拟机中应用各自互不干扰,让其自由翱翔,至于Cgroups和**Namespace**的实现原理,后续我们再共同的探讨

这里所谓的隔离环境就是「容器」。

自然不一样,不然现在我们一旦提到容器,想到的不会是Docker,而是CloudFoundry了吧。CloudFoundry的首席产品经理就觉得没什么,毕竟自己放的屁都是香的!

没想到的是,随后短短的几个月,Docker项目迅速起飞以至于其他所有Paas社区都还没来及反应过来,就已经宣告出局

就是提出了镜像的概念。上面我们说过,Paas提供的一套应用打包的功能,看起很轻松省事,但是一旦使用了Paas,你就要终身服务于它,毕竟他是提供商,是「爸爸」,用户需要为每个版本,每种语言去维护一个包,这个打包的过程是需要多次的尝试,多次试错后,才能摸清本地应用和远端Paas的脾气,从而顺利部署。

而Docker镜像恰恰就是解决了打包这一根本问题。

Docker镜像也是一个压缩包,只是这个压缩包不只是可执行文件,环境部署脚本,它还包含了完整的操作系统。因为大部分的镜像都是基于某个操作系统来构建,所以很轻松的就可以构建本地和远端一样的环境。

这就很牛皮了,如果我们的应用是在Centos7上部署,我们只需要将项目环境部署在基于Centos7的环境中,然后无论在哪里去解压这个压缩包,都可以保证环境的一致性。在整个过程中,我们根本不需要进行任何的配置,因为这个压缩包可以保证:本地的环境和云端是一致的,这也是Docker镜像的精髓

开发者体验到了Docker的便利,从而很快宣布Paas时代的结束,不过对于大规模应用的部署,Docker能否实现在当时还是个问号

就在2014年的DockerCon上,紧接着发布了自研的「Dockerswarm」,Docker就这样一度奔向高潮,即将就到达了自己梦想之巅。

虽然Docker通过「容器」完成了对Paas的「降维打击」,但是Docker的目的是:如何让更多的开发者将项目部署在自己的项目上,从技术,商业,市场多方位的争取开发者的群体,为此形成自家的Paas平台做铺垫

Docker项目虽然很受欢迎,就目前看来只是一个创建和启动容器的小工具。需要应该清楚的一点是,用户最终部署的还是他们的网站,服务甚至云计算业务。所以推出一个完整的整体对外提供集群管理功能的Swarm势在必行,这个项目中的最大亮点即直接使用了Docker原生的API来完成对集群的管理。

对于单机项目,只需要执行下面一条语句即可实现容器

对于多机的项目,只需要执行

你看,从单机切换到多机,使用的方法也就参数不同而已,所以这样一个原生的「Docker容器集群管理」一发布就受到大家的青睐。随着自身生态的逐渐完善,借助这一波浪潮并通过各种并购来强大自己的平层能力

要说最成功的案例,非Fig项目莫属。之所以这么屌,是因为作者提出了「容器编排」的概念。

其实这也不是什么新鲜内容,比如在Linux中的Makefile和常见的SHELL脚本,它是一种通过工具或者配置来完成一组虚拟机或者关联资源的定义、配置、创建等工具。

我们先以Fig为例,假设开发人员小黑,现在要部署一个项目,其中包含了应用容器A,数据库容器B,负载容器C,这个时候Fig只需要将三个容器定义在一个配置文件,然后指定他们的关联关系,执行下面的命令即可

当然也可以在Fig的配置文件中配置各种容器的副本,然后加上Swarm的集群管理功能,这样不就是Paas了么。只是这个项目被收购以后,修改名字为compose了,后续也会的compose进行详细的阐述

就这样一个以「鲸鱼」为商标的Docker,火遍全球,因为它秉持将「开发者」群体放在食物链的顶端。一分钟实现网站的部署,三分钟搭建集群,这么多年以来,很多后端开发者很少将眼光放在Linux技术上,开发者们为了深入的了解Docker的技术原理,终于将眼光放入诸如Cgroups和Namespace技术中。

这还没完,说了这么久,还没有提到在基础设施领域翘楚的Google公司,是的,同在这一年,宣告了一个叫做Kubernetes项目的诞生。

其实在Docker兴起的时候,谷歌也开源了一个Linux容器:Imctfy,在当时这个项目在Docker面前真是弟弟,所以向Docker公司表示想合作的意愿,Docker显然不同意,且在之后的不久自己发布了一个容器运行时的库Libcontainer,可能由于太急躁,其代码可读性极差,不稳定和频繁的变更,让社区叫苦不迭

为了切割Docker项目的话语权,决定成立一个中立的基金会。所以于2015年将这个Libcontainer捐出,并修改名称为Runc,然后依据RunC项目,制定了一套容器和镜像的标准和规范----OCI

为了让Docker不能太嚣张,其他玩家构建自身平台的时候不依赖于Docker项目,提出一个标准和规范----OCI。这一标准并没有改变Docker在容器领域一家独大的现状。Google坐不住了,必须得搞点大招

**Google**给RedHat等伙伴打了电话,说咱们共同牵头发起一个基金会-----CNCF。目的很简单,以kubernetes为基础,建立一个以由开源基础设置主导,按照独立基金会方式运营的平台级社区,来对抗Docker公司为核心的容器商业生态

为了做好这个事儿,CNCF必须完成两件事儿

必须在编排领取足够的优势

CNCF必须以kubernetes为核心,覆盖更多的场景

Swarm的无缝集成以及Mesos的大规模集群的调度管理能力,很明显,如果继续往这两个方向发展,后面的路不一定好走。所以,kubernetes选择的方式是Borg,其基础特性是Google在容器化基础设施多年来实践的经验,这也正是项目从一开始就避免了和Swarm,mesos社区同质化的重要手段

RedHat正好擅长这玩意呀,它能真正的理解开源社区运作和项目研发真谛的合作伙伴。作为Docker一方,主要不断地强调「Dockernative」,但是由于kubernetes没有跟Swarm展开同质化的竞争,所以这个「DockerNative」的说法并没有什么杀伤力。反而其独特的设计理念和号召力,让其构建了一个完全与众不同的容器编排管理生态。

就这样很快就把Swarm甩在了身后。随机开始探讨第二个问题,CNCF添加了一系列容器工具和项目,面对这样的压迫,Docker在2016年决定放弃现有的Swarm项目,而是将容器编排等全部内置到Docker项目中。

而kubunetes的应对方案也蛮有意思,开启「民主化架构」,kubernetes为开发者暴露可以扩展的插件机制,让用户可以随意的通过植入代码的方式介入到kubernetes的每一个阶段,很快,整个容器圈出现了优秀的作品:火热的微服务治理项目lstio等

面对kubernetes的强力攻击,Docker公司不得不面对失败的事实,只好放弃开源社区专注于商业化转型,所以于2017年将容器运行时部分containerd捐赠给了CNCF,从而将Docker项目改名为Moby,然后交给社区维护,于2017年,**Docker**公司宣布将在企业版内置kubernetes项目,这也标志了kubernetes「编排之争」的结束

Docker能做什么

Docker是一个用于开发,发布,运行应用的程序于一体的开放平台。如果我们需要将货物整齐地摆放在船上且互不影响,那么一种可行的方案即通过集装箱进行标准化,我们将各种货品通过集装箱打包,然后统一的放在船上进行运输,Docker其实就是这样一个将各种软件进行打包成集装箱,然后分发。

Docker的安装

Docker是一个跨平台的解决方案,支持各大平台比如Centos,Ubuntu等Linux发行版。下面讲述的是在Centos中的使用,安装

卸载当前已经存在的旧版Docker,执行下面的命令

添加Docker安装源

安装最新版本

如果需要安装指定版本,可以通过下面命令查看版本并选择需要的版本

安装完成,启动Docker

按照国际案例,先跑一个helloworld

运行上述命令,Docker首先会检查本地是否有hello-world这个镜像,如果发现本地没有这个镜像,Docker就会去DockerHub官方仓库下载此镜像,然后运行它。最后我们看到该镜像输出"HellofromDocker!"并退出。

Docker核心概念

Docker的操作主要围绕镜像,容器,仓库三大核心概念

一句话说即镜像是Docker容器启动的先决条件,因为镜像会提供容器运行的一些基础文件和配置文件,是容器启动的基础。说白了,要启动容器,需要镜像来提供一些基础环境。

自定义创建镜像。首先找一个基础镜像,比如此镜像是Centos,然后在此镜像基础上自定义需要的内容。举个例子,基础镜像为Centos,先安装Nginx服务,然后部署咱们的应用程序,最后做一些自定义的配置,这样一个镜像就完成了,此镜像的操作系统是Centos,其中包含了Nginx服务

从仓库寻找别人已经做好的镜像。直接去**Dockerhub**或者其他公开仓库下载即可

容器是镜像的运行实体。镜像是静态的只读文件,可是容器是要运行的,需要可写文件层。所以容器运行着真正的应用进程,所以自然会有创建,运行,停止,暂停和删除五种状态

既然容器是直接运行的运行程序,那它是有自己的命名空间嘛?

容器有自己独立的命名空间和资源限制,意味着在容器内部,你无法看到主机上面的进程,环境变量等信息,这就是容器和物理机上的进程本质区别

镜像仓库类似于代码仓库,用来分发和存储镜像,分为公共镜像和私有镜像。Dockerhub是Docker的官方公开镜像仓库,很多的官方镜像都可以在上面找到,但是访问很慢,所以可以找国内的镜像源,当然后面我们也会自己搭建一个私有镜像仓库

上图清晰地展现了镜像是容器的基石,容器是在镜像的基础上创建的。一个镜像可以创建多个容器,仓库用来存放和分发镜像

Docker架构

容器技术的发展可说突飞猛进了,市面上除了Docker容器还有coreos的rkt,lxc等,这么多种容器,是不是需要一个标准呢,不然就太容易乱套了

你可能会说直接把Docker作为标准不就好了,但是有这么多相关的容器技术,谁不想吃个好瓜,除此之外,当时的编排的技术也竞争火爆,当时的三主力分别是DockerSwarm,kubernetes以及mesos。作为原生的DockerSwarm自然优势明显,但是kubernetes不同意啊,它们觉得调度形式太单一了

因此爆发了容器大战,OCI也就在此出现。

OCI是开放的容器标准,轻量级开放的治理结构,目前主要有两个标准,分别是容器运行时标准和容器镜像标准

在如此竞争激烈下面,Docker的架构成为了下面这个样子

Docker的整体架构为CS架构,客户端和服务端两部分组成,客户端发送命令,服务端接受处理指令,其通信的方式有多种,即可以通过Unix套接字通信,也可以网络链接远程通信

Docker客户端

我们平时通常使用Docker命令行的方式和服务端打交道,其实还可以通过**RESTAPI**的方式和Docker服务端交互,甚至使用各种预言的sdk和Docker的服务端交互,美滋滋

Docker服务端

Docker服务端是Docker后台服务的总称。其中Dockerd是一个非常重要的后台进程,它负责响应并处理Docker客户端的请求,然后转化为Docker的具体操作

Docker重要的组件

我们去Docker默认安装路径先看看有哪些组件

这里主要说明下runc和contained组件

runc:是一个用来运行容器的轻量级工具

contained:是容器标准化后的产物,从Dockerd剥离出来,contained通过contained-shim启动并管理runc,可以说contained是真正管理容器的生命周期

通过上图,可以看到,dockerd通过gRPC与containerd通信,由于dockerd与真正的容器运行时,runC中间有了containerd这一OCI标准层,使得dockerd可以确保接口向下兼容。

gRPC是一种远程服务调用。containerd-shim的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim的主要作用是将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,从而实现重启dockerd不影响已经启动的容器进程。

启动一个容器

启动完成,通过下面命令查看docker的pid

此时发现其pid为4428,随后我们查看进程之间的关系

通过pstree查看进程之间的关系

注意,docker19就看不到两者是父子关系了

可以先使用psaux|grepcontained,然后使用pstree查看contained的pid,实际上,Docker启动的时候,contained就启动了,dockerd和contained一直就存在。当执行了dockerrun以后,contained就会创建contained-shim充当垫片进程,然后启动容器的真正进程sleep3600,这和架构图一致

075528566666

Docker相关组件

docker

对于我们最直观的即Docker命令,作为Docker客户端的完整实现,通过Docker命令来实现所有的Docker客户与服务端的通信

Docker组件向服务端发送请求后,服务端根据请求执行具体的动作并将结果返回给Docker,Docker解析服务端的返回结果,并将结果通过命令行标准输出展示给用户。这样一次完整的客户端服务端请求就完成了

dockerd

dockerd为Docker服务端后台的常驻进程,负责接收客户端的请求,处理具体的任务并将结果返回客户端

第一种方式:通过unix套接字与服务端通信,配置的格式为:unix://socket_path。默认的dockerd生成的socket文件存放在/var/run/,此文件只能是root用户才能访问,这也是为什么刚安装完Docker后只能root来进行访问操作

第二种方式:采用TCP的方式与服务端通信,配置格式为:tcp://host:por,为了保证安全,通常还需要使用TLS认证

第三种方式:通过fd文件描述符的方式,配置格式为:fd://这种格式一般用于systemd管理的系统中。

docker-init

在Linux中,有一个叫做init的进程,是所有进程的父进程,用来回收那些没有回收的进程,同样的道理,在容器内部,可以通过加上参数--init的方式,让1号进程管理所有的子进程,例如回收僵尸进程

此时的1号进程为为sh进程,如果加上--init

你会发现,此时的1号进程为docker-init,而不是sh了

docker-proxy

docker-proxy用来将容器启动的端口映射到主机,方便主机的访问。

假设目前启动一个nginx容器并将容器的80端口映射到主机的8080端口

查看容器IP

此时使用ps查看主机是否有docker-proxy进程

可以发现当进行端口映射的时候,docker为我们创建了一个docker-proxy进程,并且通过参数将容器的IP和端口传递给docker-proxy,然后proxy通过iptables完成nat的转发

从最后一句可以看出,当我们主机访问8080端口的时候,iptable将流量会转发给172.17.0.2的80,从而实现主机上直接访问容器的业务

使用curl访问一下nginx容器

contained组件

containerd

contained主要负责容器的生命周期管理,同时还会负责一些其他的功能

镜像的管理

接受dockerd的请求

管理存储相关资源

管理网络资源

containerd-shim

containerd-shim的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim的主要作用是将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,从而实现重启containerd不影响已经启动的容器进程。

ctr

ctr实际上是containerd-ctr,它是containerd的客户端,主要用来开发和调试,在没有dockerd的环境中,ctr可以充当docker客户端的部分角色,直接向containerd守护进程发送操作容器的请求。

Docker镜像使用

来,继续,我们看看镜像是什么。镜像是一个只读的镜像模版且包含了启动容器所需要的文件结构内容。镜像不包含动态数据,构建完成将不会改变

对于镜像的操作分为:

拉取镜像:通过dockerpull拉取远程仓库的镜像

重命名镜像:通过dockertag重命名镜像

查看镜像:通过dockerimagels查看本地已经存在的镜像

删除镜像:通过docekrrmi删除没有用的镜像

构建镜像

第一种是通过dockerbuild命令基于dockerfile构建镜像,推荐

第二种是通过dockercommit基于运行的容器提交为镜像

拉取镜像直接使用dockerpull命令即可,命令的格式如下

registry未注册的服务器,docker默认会从官网上拉取镜像,当然可以将registry注册为自己的服务器

repository为镜像仓库,library为默认的镜像仓库

image为镜像的名称

tag为给镜像打的标签

现在举个例子,这里有个镜像叫做busybox,这个镜像集成了上百个常用的Linux命令,可以通过这个镜像方便快捷地查找生产环境中的问题,下面我们一起操作一波

dockerpullbusybox

首先会在本地镜像库查找,如果不存在本地库则直接去官网拉取镜像。拉取完镜像后随即查看镜像

查看镜像---dockerimages

如果要查看指定的镜像,则使用dockerimagels命令进一步查询

重命名镜像采用打标签的方式重命名,格式如下

我们仔细观察这两个镜像,就会发现这两个镜像的IMAGEID其实是一样的,这是什么原因呢

实际上他们都是指向的同一个镜像文件,只不过其别名不一样而已,如果此时不想要mybox镜像,想删除这个镜像

使用dockerrmi删除镜像

此时再次使用dockerimages查看确实删除了

之前说过,有两种方式,一种是通过dockercommit的方式,一种是dockerbuild的方式。首先看看使用容器提交镜像的方式

此时启动了一个busybox容器并进入到容器,并在容器中创建一个文件,并写入内容

此时就在当前目录下创建了一个文件并写入了内容。现在新建另外一个窗口,然后提交为一个镜像

然后使用dockerimagels查看发现确实生成了镜像

然后我们再看看使用dockerfile的方式

dockerfile的每一行的命令都会生成独立的镜像层并拥有唯一的id

dockerfile命令是完全透明的,通过查看dockerfile的内容,就可以知道镜像是怎么一步步构建的

dockerfile为纯文本,方便做版本控制

先看看都有哪些命令

这么多,不存在的,我们先看一个dockerfile就知道如何用了

首先第一行表示基于什么镜像构建

第二行是拷贝文件nginx。repo到容器内的/etc/

第三行为容器中运行yuminstall命令,安装nginx命令到容器

第四行为生命容器使用80端口对外开放

第五行定义容器启动时的环境变量HOST=mynginx,容器启动后可以获取到环境变量HOST的值为mynginx。

第六行定义容器的启动命令,命令格式为json数组。这里设置了容器的启动命令为nginx,并且添加了nginx的启动参数-g'daemonoff;',使得nginx以前台的方式启动。

第一行:创建一个busybox镜像层

第二行:拷贝本机test文件到镜像内

第三行在tmp文件夹创建一个目录testdir

为了清楚的看见镜像的存储结构,通过dockerbuild构建镜像

因为我的docker使用的是overlay2文件驱动,所以进入到/var/lib/docker/overlay2,使用tree查看

可以清楚的看到,dockerfile的每一行命令都会生成一个镜像层

Docker容器操作

我们通过一个镜像可以轻松的创建一个容器,一个镜像可以有多个容器,在运行容器的时候,实际上是在容器内部创建了这个文件系统的读写副本,如下图所示

容器的生命周期一共有五个状态分别为

created初建状态

running运行状态

stopped停止状态

opaused暂停状态

deleted删除状态

通过dockercretate进入容器的初建状态,然后通过dockerstart进入运行状态,通过dockerstop进入停止状态,运行状态的容器可以通过dockerpause让其变为暂停状态,为了详细的查看这些过程,我们实操一下

创建并启动容器

通过dockercreate创建的容器处于停止的状态,使用dockerstartbusybox进入启动状态

当使用dockerrun创建并启动容器的时候,docker后台的执行逻辑为

首先检查本地是否有busybox镜像,不存在则取dockerhub中拉取

使用busybox镜像启动一个容器

分配文件系统,并在镜像的只读层外创建一个读写层

从dockerip池分配个ip给容器

运行镜像

同时使用-it参数可以让我们进入交互模式,容器内部和主机是完全隔离的。另外由于此时的sh为1号进程,所以如果通过exit退出sh,那么容器也就退出,所以对于容器而言,杀死容器中的主进程,那么容器也就会被杀死

通过dockerstop停止容器,其原理是给运行中的容器给sigterm信号,如果容器为1号进程接受并处理sigterm,则等待1号进程处理完毕后就退出,如果等待一段时间后还是没有处理,则会通过发送sigkill命令强制终止容器

想要进入容器,有三种方案,分别是dockerattach,dockerexec,nsenter等

使用dockerattach方式进入容器

通过dockerps-a查看当前的进程信息

可是当我们在进行窗口进行dockerattach的时候,这个命令就不好用了,所以使用dockerexec的方式

使用dockerexec进入容器

奇怪的发现居然是两个sh进程,主要是因为,当我们使用dockerexec方式进入容器的时候,会单独启动一个sh进程,此时的多个窗口都是独立且不受干扰,也是非常常用的方式

删除容器

现在基本上知道了如何创建,启动容器,那么怎么删除容器呢

如果此时,容器正在运行,那么需要添加-f的方式停止正在运行的容器

这简单,不过在导出之前先进入容器创建一个文件

然后导出为文件

此时会在当前目录生成一个文件,此时就可以将其拷贝到其他的机器上使用

通过dockerimport的方式导入,然后使用dockerrun启动就完成了容器的迁移

此时容器名称为busybox:test,然后我们使用dockerrun启动并进入容器

此时发现之前在/tmp创建的目录也被迁移了过来

仓库

容器的基本操作应该都会了,那么我们应该如何去存储和分发这些镜像,这就需要介绍下仓库;

我们可以使用共有镜像仓库分发,也可以搭建私有的仓库

钱钱仓库放钱,这个仓库放镜像。Github放代码,我们理解镜像的仓库可以联想Github仓库。

在学习的过程中,不太能区分注册服务器和仓库的关系。注册服务器其实是用来存放仓库的实际机器,而仓库我们可以将其理解为具体的项目或者目录。一个注册服务器可以存放多个仓库,每个仓库可以存放多个镜像

Dockerhub是当前全球最大的镜像市场,差不多超过10w个容器镜像,大部分操作系统镜像都来自于此。

注册Dockerhub

创建仓库

实战镜像推送到仓库

此时假设我的账户是xiaolantest,创建一个busybox的仓库,随后将镜像推送到仓库中。

第一步:拉取busybox镜像

第二步:推送镜像之前先登录镜像服务器(注意用户名密码哦),出现loginSucceeded表示登录成功

第三步:推送之前还要做一件事,重新对镜像命名,这样才能正确地推送到自己创建的仓库中

第四步:dockerpush到仓库中

Docker官方提供了开源的镜像仓库Distribution,镜像存放于Dockerhub的Registry中

启动本地镜像仓库

使用dockerps查看启动的容器

重命名镜像

此时Docker为busybox镜像创建了一个别名localhost:5000/busybox,localhost:5000为主机名和端口,Docker将会把镜像推送到这个地址。

推送镜像到本地仓库

删除之前存在的镜像

此时,我们验证一下从本地镜像仓库拉取镜像。首先,我们删除本地的busybox和localhost:5000/busybox镜像。

查看当前本地镜像

可以看到此时本地已经没有busybox这个镜像了。下面,我们从本地镜像仓库拉取busybox镜像:

随后再使用dockerimagelsbusybox命令,这时可以看到我们已经成功从私有镜像仓库拉取busybox镜像到本地了

总结

不知不觉,每周下班开始写,做实验,截图,至此也就告一段落了,开篇思维导图由于完整版太大,我计划一共分为四篇文章,分别为Docker基本认识,Docker实战,Docker底层原理以及Docker集群安全四个方面来分享,后续精简后再给大家。本篇文章从Docker容器圈到基本使用,写的应该蛮清楚了,另外说明一下,由于直接使用代码很可能导致格式排版混乱,所以全部采用截图的方式,更加直观和清晰,当然如有不妥之处也望大家指正。点赞,转发,收藏,感谢!我们下期见!

猜你喜欢
    不容错过