Kubernetes(K8S)入门到运维 ( 一 ) > 什么是Kubernetes (K8S)?

前言

    是想要去学习这个技术的,就不要只看文章,不动手去实践。

    只是想做了解的,那么则要重点结合图一起看故事。

    在进行Kubernetes(K8S)实验时,Pod等没有运行起来,那么则使用kubectl describe pod pod名查看是否是镜像下载的问题,如果是镜像下载的问题,具体参考《Kubernetes(K8S)入门到运维 ( 二 ) > 部署及Docker私库安装》这篇文章的镜像下载去解决,即寻找国内阿里云的镜像下载,下载完了则修改docker tag。https://blog.csdn.net/Su_Levi_Wei/article/details/103404234

    前面有一些概念需要大家看一看的,看完了可能会忘记了,但在后续的实验中,在做之前建议可以回来看看这些概念。

    在学习Kubernetes(K8S)之前要先学习Docker。

    请先学习以下链接知识:

    https://blog.csdn.net/Su_Levi_Wei/article/details/101689276

       https://blog.csdn.net/Su_Levi_Wei/article/details/101072641

       https://blog.csdn.net/Su_Levi_Wei/article/details/101531835

 

为什么要有Kubernetes(K8S)?

    有了Docker、Docker-Compose,且Docker、Docker-Compose操作起来也不复杂,为什么还要来一个Kubernetes?

    的确,有了Docker、Docker-Compose的确是方便了运维的部署,直接几条命令就可以启动起来一个环境了,在加个Volume修改对应的配置文件,就可以实现大部分的环境部署需求。

    问题在于,如果此时服务器的性能不够用了,怎么办?

    加硬件,在学过分布式思想时,都知道到了某种程度,加机器比在一台机器上堆硬件还要便宜,且堆硬件的问题还在于服务器的硬件接口够不够,如果插槽不够就更悲催了。

    所以现在的主流方案都是采用加机器,横向扩展,在加了机器的情况下,这个时候要把应用的环境全部拷贝一份过去就算了,问题在于怎么去管理整个集群,或者说怎么去管理整个已经进行了容器化的集群,性能怎么去平衡?

    有的可能会想既然拷贝过去了,那就前置一个Nginx,修改Nginx的配置文件,在加一台机器的配置信息就好了,流量就会平衡过去了。那么又有个问题,如果出现了资源倾斜了怎么办?

    在以上的种种问题下,我们需要一个能够帮助我们去管理这个集群的工具,而且这个工具能够帮助我们去协调好各个机器的。

    目前业界最火的就是Kubernetes。

 

Kubernetes(K8S)是什么?

    Kubernetes这个名字比较陌生,K8S这个名字似乎听过,其实Kubernetes的别名就是K8S,为什么叫作K8S。应该是中间有8个字母吧。接下来,我们都会称其为K8S。

    进入正题。

    K8S是谷歌在2014年6月开源的一个容器集群管理系统,采用Go语言开发,也称为K8S。

    Kubernetes(K8S)是基于谷歌内部一个叫Borg的容器集群系统衍生出来的,Borg已在谷歌大规模生产中运行了十余年了。

    Kubernetes(K8S)主要是用于自动化部署、扩展和管理容器应用,提供了资源调度、部署管理、服务发现、弹性伸缩、监控等一整套的功能。

   

                                                                             Borg架构图

 

特点

    轻量级

    消耗资源小

    开源

    弹性伸缩

    采用IPVS的负载均衡

 

主要功能

    自动装箱:在不牺牲可用性的条下,基于容器对资源的要求和约束自动部署容器。同时,为了提高利用率和节省更多的资源,将关键和最佳工作量结合在一起。

    自愈能力:当容器失败时,会对容器进行重启;当部署的Node节点有问题时,会对容器进行重新部署和重新调度;当容器未通过监控检查时,会关闭此容器,直到容器正常运行才会对外提供服务。

    水平扩容:简单的命令、用户界面或基于CPU的使用情况,能够对应用进行缩容、扩容。

    服务发现和负载均衡:开发者不需要使用额外的服务发现机制就能够基于Kubernetes(K8S)进行服务发现和负载均衡。一组Pod副本分配一个私有的集群IP地址,集群内部其他Pod可以通过这个ClusterIP访问应用。

    自动发布和回滚:Kubernetes(K8S)能够程序化的发布应用和相关的配置。如果发布有问题,Kubernetes(K8S)能够回滚。且一次更新一个Pod,而不是同时删除整个服务。

    保密和配置管理:在不需要重新构建的情况下,可以部署和更新秘钥和应用配置。

    存储编排:自动挂接存储系统,存储系统可以是本地、公共云(阿里云、腾讯云等)、网络(NFS、Ceph等)等。

    服务编排:通过问卷描述部署服务,使得应用程序部署变得更高效。

    复制应用实例:控制器维护着Pod副本数量,保证一个或一组同类Pod数量始终可用。

    资源监控:Node节点组件继承cAdvisor资源收集工具(以下的架构图中有),可通过Heapster汇总整个集群节点资源数据,然后存储到InfluxDB时序数据库,再由Grafana展示。

    提供认证和授权:支持属性访问(ABAC)、角色访问控制(DBAC)认证授权策略。

   一堆的理论,还是要看看的,稍后把接下来的脚本和命令敲完在回过头来看这一堆理论就会不一样了。

 

缺点

    学习成本较高。

    对无状态服务的支持度更好,对有状态服务需要做额外的配置。

    没有提供七层的应用网络访问,需要借助第三方。

 

使用者范围

    软件工程师

    测试工程师

    运维工程师

    软件架构师

    项目经理

 

资源管理器

名称

描述

Mesos

分布式资源管理框架,加州伯克利分校开发的,给Apache吸收了;2019年5月,Twitter宣布抛弃Mesos,全面转向Kubernetes(K8S);

Docker Swarm

Docker组件之一,功能相较于Kubernetes(K8S)还是比较少;2019年7月,阿里云停止对docker swarm的支持

Kubernetes(K8S)

基于运行了十余年的Borg容器基础架构,目前主流的选择方案。

 

历史小故事

    2013年2月,Docker发布了首个演示版本。

    Alex Polvi在计划开发一个足以颠覆传统的服务器系统的Liunx发行版,为了提供能够从任意操作系统版本稳定、无缝的升级到最新版系统的能力,Alex需要解决应用于操作系统之间的耦合问题。因此Alex Polvi将Docker作为这个系统的第一套应用隔离解决方案。随后他们成立了以自己的系统发行版命名的组织:CoreOS。

    Docker一开始的时候是作为组件来构建平台,一个构建块,可以将它分层置入系统来利用容器…这是支撑Docker的原始价值,是一个帮助构建东西的简单工具。但是Alex Polvi认为Docker忽略了自己的核心,反而是期望拥有更多的功能并成为一个平台。他们一直认为Docker应该成为一个基础单元,但事情不是他们所想的那样,Docker正在构建一些工具用于发布云服务器、集群系统以及构建、运行、上传和下载镜像等服务,甚至包括底层网络的功能等,以打造自己的Docker平台或生态圈。

    Docker很快就拿了融资,还得到了谷歌这些巨头的支持。CoreOS在背后一直为Docker提供技术支持服务,似乎没有知道他。

    于是Alex Polvi认为,由于Docker貌似以及从原本做业界标准容器的初心转变成打造一款以容器为中心的企业服务平台,CoreOS才决定推出自己的标准化产品Rocket。

    Alex Polvi的CoreOS与Docker正式走向决裂。很快互怼开始了,你的产品有缺陷,你的也有缺陷,你们应该用我的……大戏上演。

    谷歌、Red hat、Vmware这些巨头自然不想Docker一家独大了,免得以后还会受到专利等限制,于是这些公司是支持CoreOS的Rocket。

   故事继续:看完故事记得回头在看看Kubernetes(K8S)的内容。

谷歌那么多人才,自然在这个过程中,肯定是有开源的容器技术发布的。

    Cgroup最初由谷歌的工程师提出的,后来被整合进去Liunx的内核中,而后在安卓系统中也是用这个技术来为每个应用分配不同的cgroup,进而达到应用隔离的目的。

    Liunx容器正式业界一直关注的谷歌基础设施Borg和Omega的基础之一,基于之前的谷歌开源的cgroup项目。

    Borg这是一个生产环境下产生的论文,也是来自谷歌。

    Borg的特点:任何东西都运行在Borg之中,包含存储系统如CFS和BigTable;中等类型的集群大小有10K左右的节点,尽管有的要大的多;节点开源是十分异构的;使用了Liunx的进程隔离(本质上来说是容器),因为Borg出现在现在的虚拟机基础设施之前,效率和启动时间都是十分重要的;所有作业都是静态的链接的可执行文件;有非常复杂,十分丰富的资源定义语言可用;可以滚动升级运行的作业,这意味着配置和执行文件。这有时需要任务重启,因而容错是很重要

    2013年10月,谷歌发布了自己所用的Liunx容器系统的开源版本lmctfy。

    看到这里,还有技术人员认为可能是Rocket比Docker的自定义程度更高,谷歌才支持CoreOS的吗?

   这就是商业,对于到了一定程度的企业来说,重点在于自身的护城河能够承受外部产品的可能带来的冲击。

    2014年6月:谷歌宣布kubernetes(K8S)开源。

    2014年7月:Mircrosoft、Red Hat、IBM、Docker、CoreOS、Mesosphere和Saltstack加入kubernetes(K8S)。

    2014年8月:Mesosphere宣布将kubernetes(K8S)作为frame整合到mesosphere生态系统中,用于Docker容器集群的调度、部署和管理。Vmware加入kubernetes(K8S)社区,谷歌公开表示,VMware将会帮助kubernetes(K8S)实现利用虚拟化来保证物理主机安全的功能模式。

    2014年11月:HP加入kubernetes(K8S)社区。

    2015年1月:谷歌和Mirantis及其他伙伴将kubernetes(K8S)引入OpenStack,开发者可以在OpenStack上部署运行kubernetes(K8S)应用。

    2015年4月:谷歌和CoreOS联合发布Tectonic,它将kubernetes(K8S)和CoreOS软件栈整合在了一起。

    2015年5月:Intel加入kubernetes(K8S)社区,宣布合作加速Tectonic软件栈的发展。

    2015年6月:谷歌容器引擎进入beta版。同期,在Docker Con大会上Polvi和Hykes(Docker负责人之一)握手宣布,启动开放容器基金会,旨在为容器提供一种通用的Runtime OpenContainer Project,目标是实现容器镜像格式与运行时标准化。项目成为包括Docker、CoreOS、Red Hat、IBM、谷歌、微软、英特尔、Amazon、HP、华为、思科、EMC等。

    2015年7月:谷歌正式加入OpenStack基金会,并成为发起人之一,谷歌还把自己的容器计算的专利技术带入OpenStack。

    2015年7月:kubernetes(K8S) v1.0正式发布。

Kubernetes(K8S)架构

 

    Kubernetes(K8S)是主从分布式架构,主要由MasterNode和WorkerNode组成,以及包括客户端命令行工具Kubectl和其他附加项。

    Kubernetes(K8S)集群包含节点代理Kubelet和Master组件,一切基于分布式存储系统。

 

Master节点

    Master作为控制节点,对集群进行调度管理、认证和授权、RESTful API Entry Point、对K8S Node容器部署的调度、扩容和复制容器、读取配置去创建集群等工作。

    APIServer集群的统一入口,各个组件的协调者,以Http API提供接口服务,所有对象资源的增删盖茶和监听操作都交给APIServer处理后在提交给ETCD存储。

    ControllerManager处理集群中常规后台任务,一个资源对应一个控制器,就是负责管理这些控制器的,即维持副本期望数。

    Scheduler根据调度算法为新创建的Pod选择一个合适的Node节点。

 

WorkerNode节点

    WorkerNode节点时真正的工作节点,运行业务应用的容易,WorkerNode包含kubelet、kube proxy和Container Runtime。

    KubeletMaster在Node节点上的Agent(代理人),管理本机运行的容器的生命周期,比如创建容器、Pod挂载数据卷、下载镜像、获取容器和节点状态等工作,kubelet将每个Pod转换成一组容器。

    Kube Proxy实现Pod网络代理、服务发现的功能,维护网络规则写入至IPTables、IPVS等实现网络服务映射访问和四层负载均衡工作,默认基于Round Robin(轮询)算法将客户端流量转发到Service对应的一组后端Pod;服务发现方面,使用ETCD的watch机制,监控集群中Service和EndPoint对象数据的动态变化,并且维护一个Service到EndPoint的映射关系,从而保证后端Pod的IP变化不会对访问者造成影响,另外还支持Session Affinity。

    Container Runtime容器运行环境,目前支持docker和ekt两种容器。

 

Kubeadm、Kublet、Kubectl

    Kubeadm用于初始化Cluster。

    Kubelet运行在Cluster所有及诶单上,负责启动Pod和容器。

Kubectl是运维人员用于通过命令与APIService进行交互的桥梁,进而对Kubernetes(K8S)进行操作,实现在及群众进行各种资源的增删改查、配置等操作。

 

Etcd

    可信赖的分布式键值存储服务,保存了整个集群的状态、网络配置信息、元数据信息等,因此是作为Kubernetes(K8S)数据存储的一个存储库;采用Http状态,读写信息会存储在Raft,避免读写信息丢失,会有WAL确保信息完整性;Kubernetes(K8S)的v1.11版本中是不支持Etcd的V2、V3版本;V2会把数据写到内存;V3在本地引入一个持久化数据库,关机后不会造成数据丢失;

 

Add-on

    Add-on是对Kubernetes(K8S)核心功能的扩展,例如增加网络和网络策略等能力。

    Core DNS可以为集群中的SVC创建一个域名IP的对应关系解析,即为整个集群提供DNS服务。

    Ingress Controller官方只实现了四层代理,Ingress可以实现七层代理,即为应用服务提供外网入口。

    Heapster提供资源监控。

    Dashboard提供一个B/S结构访问体系,并提供了一个GUI。

    Federaion可跨集群中心多Kubernetes(K8S)统一管理功能。

    ELK集群日志采集、存储、查询等功能的平台。

    Prometheus对Kubernetes(K8S)集群的监控。

 

Kubectl、Master、WorkerNode、Etcd的故事

    Master相当于一家酒店,桌子、包厢、椅子都是Master负责去解决的,如果节假日,客人太多了,此时需要增加桌子扩大餐厅的范围,也是Master去干的。

    APIServer就是服务员,负责协调厨房和客人之间的需求交替,且把客人点的菜记录下来交给厨房。

    ControllerManager相当于厨房管理员,负责使用他的电脑(调度算法)去看要找哪位厨师炒这个菜,或者说哪位厨师比较空闲就给到谁。

    WorkerNode是厨房里的大厨,假设是很有名气的大厨。

kubelet相当于酒店与这些名厨之间的代理人,厨师炒菜前的需求都是他去解决。

    Kube Proxy就是保证厨师炒的菜的输入和输出。

    Container Runtime就是厨师使用的锅,要用哪种锅炒。

    Kubectl自然就是客人了。

    Add-on的功能内容则是对酒店或厨房等其他地方的美化,或者说是优化,也可以说是某位客人可能会要求有个情境套间,即由此提供。

    Etcd就是记账本、菜单本。

核心概念

    Pod最小的部署单元,由一个或多个容器组成,Pod中容器共享存储和网络,在同一台Docker主机上运行。即nginx-pod-xxx就是一个容器。Pod也分为自主式Pod和控制器管理的Pod。自主式Pod一旦死亡了,就没有其他将其从资源中拉下来,也不会去创建一个新的去替代,即退出了就不会被创建。控制器管理的Pod,在控制器的生命周期里,始终要维持Pod的副本数目。

    Pod解释:在分布式项目下,运行一个应用服务(登录服务)假设部署2个服务(容器),而使用如前缀tomcat-pod-xx1、tomcat-pod-xx2这样来去管理,这样就是一个Pod了。Pod是逻辑上的概念。现在不理解没关系,稍后部署完,自己体验下,在回来看这些理论就会理解了。

    Service一个应用服务抽象,定义了Pod逻辑集合和访问这个Pod集合的策略;代理Pod集群对外表现是为一个访问入口,分配一个集群IP地址,来自这个IP的请求将负载均衡转发到后端Pod的容器;通过Label Selector选择一组Pod提供服务;Service手机Pod是通过一组标签来收集的。

    Service解释:分布式项目下,用户服务部署在2个Tomcat中,前置了一个Nginx将请求进行转发到这2个Tomcat中的其中一个去,Service可以理解为Nginx,就是帮助解决我们在多个容器的情况下,网络流量进来了,但是要进去哪个Pod的转发问题。

    Label用于区分对象(如Pod、Service),键值对的形式存在,每个对象可以有多个标签,通过标签关联对象。

    Label解释:用户服务要部署10个,即部署在10个容器中,开发人员是通过某个地址去访问其中5个容器提供的服务的,测试人员是通过另外一个地址去访问另外的5个容器提供的服务的。此时,可以通过打标签的方式,Service提供了网络访问解决方案,那么在部署Service时,就可以通过标签选择来决定这个IP是访问哪些容器的服务了。

    Volume数据卷,共享Pod中容器使用的数据。

    NameSpace命令空间将对象逻辑上分配到不同的NameSpace可以是不同的项目、用户等区分管理,并设定控制策略,从而实现多租户,命名空间也称为虚拟集群。

    NameSpace解释:也可以拿Label的那个例子来套用,例子和应用场景很多,没有固定一成不变。

    RelacationController简称RC,用来确保容器应用的副本数始终保持在用户定义的副本数,即如果有容器异常退出了,会自动创建新的Pod来替代,而如果异常多出来的容器也会自动回收,新版本Kubernetes(K8S)建议使用RelicaSet来取代ReplicationController。在v1.11版本被废弃了。

    RelicaSet简称RS,功能基本和RelacationController差不多,也是确保副本数量,并提供声明式更新等功能;RC和RS的区别在于Label Selector支持不同,RS支持新的基于集群的标签,即集合式。RC只支持基于等式。

    RelacationController和RelicaSet解释:假设用户服务设置了2个,有其中一个容器挂了,这个时候RC或RS就会在启动一个新的容器来提供服务。而挂了的容器又起来了,那么又会干掉一个,始终维持在2个容器。

    Deployment更高层次的API对象,管理ReplicaSet和Pod,并提供声明式更新等功能;官方建立使用Deployment管理RS,而不是直接使用RS,这意味着可能永远不需要直接操作RS对象,也不需要担心跟其他机制不兼容(如RS不支持rooling-update,但Deployment支持),Deployment不支持Pod创建,但RS支持,但不支持滚动更新。

    Deployment解释:可以理解为公司的高层,管理RS,通过RS管理Pod。

    HPA(Horizontal Pod Autoscaling):适用于Deployment和ReplicaSet,在V1版本中仅支持根据Pod的CPU利用率扩缩容,在v1alpha版本中,支持根据内存和用户自定义的metric扩缩容。

    StatefulSet为了解决有状态服务的问题(阶级Deployment和ReplicaSet是为无状态服务而设计),适合持久性的应用程序,有唯一的网络标识符(Pod重新调度后,其PodName和HostName不变,基于Headless Service来实现,即没有ClsterIP的Service来实现)、持久化存储(Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现)、有序部署(Pod是由顺序的,在部署或扩展的时候要一句来定义的顺序依次进行,即下一个Pod运行之前,之前所有的Pod必须是Running和Ready状态,这个是基于init constainers实现的)、扩展、删除和滚动更新,有序收缩,有序删除(从N - 1 到0)。

    StatefulSet解释:服务分为有状态服务和无状态服务。有状态服务就是踢出去了在放回来就无法工作了,如数据库;无状态服务就是踢出去了还可以回来工作,如LVS、Apache。

    DaemonSet确保所有或部分节点运行同一个Pod,当有节点加入Kubernetes集群中,Pod会被调度到该节点上运行,当节点从集群移除时,DaemonSet的Pod会被删除,删除的DaemonSet会清理它所有创建的Pod;典型的用法,运行存储daemon,例如在每个Node上运行glusterdsh;在每个Node上运行监控daemon,例如Prometheus Node Exporter;

    Job一次性任务,运行完成后,Pod销毁,不在重新启动新容器,还可以任务定时执行,保证批处理任务的一个或多个Pod成功结束。

    CronJob管理基于时间的Job,给定时间点只运行一次,或周期性的在给定时间点运行。

 

网络通讯

    Kubernetes(K8S)的网络模型假定了所有Pod都在一个开源的直通连通的扁平的网络空间中,这在GCE(Google Compute Engine)里面是现成的网络模型,Kubernetes(K8S)假定这个网络已经存在,而在私有云里搭建Kubernetes(K8S)集群,就不能假定这个网络已经存在了,需要自己实现这个网络假设,将不同节点上的Docker容器之间的互相访问先打通,然后运行Kubernetes(K8S)同一个Pod内的多个容器之间(lo);各个Pod之间的通讯(Overlay Network);Pod与Service之间的通讯(各个节点的IPTables规则)。

    Flannel网络解决方案:Flannel是CoreOS团队针对Kubernetes(K8S)设计的一个网络规划服务,是让集群的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址,而且还能在哲学IP地址之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络将数据包原封不动的传递目标容器中。

    Etcd与Flannel:存储管理Flannel可分配的IP地址段源;监控Etcd中每个Pod的实际地址,并在内存中建立维护Pod节点路由表。

    通讯场景:同一个Pod内的多个容器之间,即共享同一个网络命名空间,共享统一Liunx协议栈,各Pod之间的通讯(Overlay Network);

不同情况下网络通信方式:

    同一个Pod内部通讯:同一个Pod共享同一个网络命令空间,共享同一个Liunx栈。

    Pod1 至 Pod2:

       Pod1与Pod2不在同一台主机,Pod的地址是与docker0在同一个网段的,但docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行。将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问。

        Pod1与Pod2在同一台机器,由docker0网桥直接转发请求至Pod2,不需要经过Flannel。

    Pod至Service的网络:目前性能考虑,全部都为iptables维护和转发。

    Pod到外网:Pod向外网发送请求,查找路由表,转发数据包到宿主机的网卡,宿主网卡完成路由选择后,iptables执行Masquerade,把源IP更改为宿主网卡的IP,然后向外网服务器发送请求。

 

Kubernetes(K8S)网络:

    节点网络

Service网络

    Pod网络

 

一个资源创建的过程

    在接下来学习完了资源清单后,要回来看看这里。

   

    1、准备好一个包含应用的yml文件,然后通过kubectl客户端工具发送给APIServer。

    2、APIServer接受到客户端的请求并将资源内容存储到数据库(Etcd)中。

    3、Controller组件(包括Scheduler、Replication、EndPoint)监控资源变化并作出反应。

    4、ReplicaSet检查数据库变化,创建期望数量的Pod实例。

    5、Scheduler通过和APIServer的watch机制,查看到新的Pod,再次检查数据库变化,发现尚未分配到具体执行节点(Node)的Pod,然后尝试为Pod绑定Node,根据一组相关规则将Pod分配到可以运行的节点上,并更新数据库,记录Pod分配情况。

    5.1、在这个过程中,会过滤掉主机,调度器用一组规则过滤掉不符合要求的主机,比如Pod指定了所需要的资源,那么就要过滤掉资源不够的主机。

    5.2、主机打分,对第一步筛选出来符合要求的主机进行打分,在主机打分阶段,调度器会考虑一些整体优化策略,比如把一个RS的副本分布到不同的主机上,使用最低负载的主机等。

    5.3、选择到打分最高的主机,进行绑定操作,绑定成功后,会启动container、docker run、scheduler会调用API Server的API在etcd中创建一个bound pod对象,描述在一个工作节点上绑定运行的所有pod信息,并存储结果到Etcd中。

    6、Kubelet监控数据库变化,根据调度结果执行Pod创建操作,管理后续Pod的声明周期,发现被分配到所在节点上运行的那些Pod。如果找到新Pod,则会在该节点上运行这个新的Pod。Kubectl也会定期与Etcd同步绑定的Pod的信息,一旦发现应该在该工作节点上运行的绑定的Pod对象没有更新,则会调用Docker API创建并启动Pod内的容器。

    7、KubeProxy运行在集群各个主机上,管理网络通信,如服务发现、负载均衡。例如当有数据发送到主机时,将其路由到正确的Pod或容器、对于主机上发出的数据,它可以基于请求地址发现远程服务器,并将数据正确路由,在某些情况下会使用轮询调度算法(Round-Robin)将请求发送到集群中的多个实例。

 

 

发布了103 篇原创文章 · 获赞 34 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/Su_Levi_Wei/article/details/103410078