《k8s权威指南:从Docker到k8s实践全接触-第4版》-读书笔记

《Kubernetes权威指南:从Docker到Kubernetes实践全接触(第4版)》

作者 龚正等

image

1.3 从一个简单的例子开始

将创建好的RC文件发布到Kubernetes集群中: kubectl create -f mysql-rc.yaml
在创建好mysql-rc.yaml文件后,为了将它发布到Kubernetes集群中,我们在Master上执行命令:

创建一个与MySql相关的RC, yaml文件中kind对应value为:ReplicationController; 创建一个与MySQL相关的kubernetes Service,yaml文件中kind对应value为:Service;

其中,metadata.name是Service的服务名(ServiceName);port属性则定义了Service的虚端口;spec.selector确定了哪些Pod副本(实例)对应本服务。类似地,我们通过kubectl create命令创建Service对象。

通过kubectl指令查看Service: kubectl get svc

运行kubectl命令查看刚刚创建的Service:

根据yaml文件(metadata.name和spec.ports)创建MySQL Service后, Kubernetes系统会自动为MySQL服务分配一个Cluster Ip和spec.port;

可以发现,MySQL服务被分配了一个值为169.169.253.143的Cluster IP地址。随后,Kubernetes集群中其他新创建的Pod就可以通过Service的Cluster IP+端口号3306来连接和访问它了。通常,Cluster IP是在Service创建后由Kubernetes系统自动分配的,其他Pod无法预先知道某个Service的Cluster IP地址,因此需要一个服务发现机制来找到这个服务。

k8s Service中 spec.type=NodePort,——表明该service开启了NodePort方式的外网访问模式; spec.ports.nodeport=30001——将宿主机的30001端口映射到当前服务的虚端口上;

type=NodePort和nodePort=30001的两个属性表明此Service开启了NodePort方式的外网访问模式。在Kubernetes集群之外,比如在本机的浏览器里,可以通过30001这个端口访问myweb(对应到8080的虚端口上)。

1.4 Kubernetes的基本概念和术语

Kubernetes为每个资源对象都增加了通用属性字段——Annotations,用来存放资源对象不断引入的新属性;
为此,Kubernetes为每个资源对象都增加了类似数据库表里备注字段的通用属性Annotations,以实现方法1的升级。以Kubernetes 1.3版本引入的Pod的Init Container新特性为例,一开始,Init Container的定义是在Annotations中声明的,如下面代码中粗体部分所示,是不是很不美观

Master——每个Kubernetes集群中的管理和控制节点,所有的控制指令都通过Master具体执行;

Kubernetes里的Master指的是集群控制节点,在每个Kubernetes集群里都需要有一个Master来负责整个集群的管理和控制,基本上Kubernetes的所有控制命令都发给它,它负责具体的执行过程,我们后面执行的所有命令基本都是在Master上运行的。Master通常会占据一个独立的服务器(高可用部署建议用3台服务器),主要原因是它太重要了,是整个集群的“首脑”,如果它宕机或者不可用,那么对集群内容器应用的管理都将失效。

Kubernetes集群中的其他节点——Node; Master分配工作负载到Node节点上; Node上的关键进程: 1. kubelet——负责Pod对应容器的创建、启停等任务; 2. kube-proxy——实现Kubernetes Service的通信与负载均衡机制; 3. Docker Engine(docker)——Docker引擎,负责本机的容器创建和管理工作;

与Master一样,Node可以是一台物理主机,也可以是一台虚拟机。Node是Kubernetes集群中的工作负载节点,每个Node都会被Master分配一些工作负载(Docker容器),当某个Node宕机时,其上的工作负载会被Master自动转移到其他节点上。在每个Node上都运行着以下关键进程。

查看某个Node的详细信息: kubectl describe node <node_name>

然后,通过kubectl describe node <node_name>查看某个Node的详细信息:

k8s中,为什么集群中一个Pod里的容器可以与另外主机上的Pod容器能够直接通信? 1. k8s通过虚拟二层网络技术保证了在底层网络上支持集群中任意两个Pod之间得我TCP/IP直接通信; 2. k8s会为每个Pod分配一个唯一的IP地址,一个Pod里的多个容器共享Pod IP地址;

Kubernetes为每个Pod都分配了唯一的IP地址,称之为Pod IP,一个Pod里的多个容器共享Pod IP地址。Kubernetes要求底层网络支持集群内任意两个Pod之间的TCP/IP直接通信,这通常采用虚拟二层网络技术来实现,例如Flannel、OpenvSwitch等,因此我们需要牢记一点:在Kubernetes里,一个Pod里的容器与另外主机上的Pod容器能够直接通信。

默认情况下,当Pod里的某个容器停止时,k8s会自动检测到这个问题,并重启这个Pod里的所有容器;

默认情况下,当Pod里的某个容器停止时,Kubernetes会自动检测到这个问题并且重新启动这个Pod(重启Pod里的所有容器)

Endpoint——Pod IP + containerPort; 它代表此pod里的一个服务进程的对外通信地址;

Pod的IP加上这里的容器端口(containerPort),组成了一个新的概念——Endpoint,它代表此Pod里的一个服务进程的对外通信地址。

k8s Event通常会被关联到某个具体的资源对象上; 例如,可以通过Event查看某个Pod无法创建的原因: kubectl describe pod <pod_name>

当我们发现某个Pod迟迟无法创建时,可以用kubectl describe pod xxxx来查看它的描述信息,以定位问题的成因,比如下面这个Event记录信息表明Pod里的一个容器被探针检测为失败一次:

k8s中,通过Label和Label Seletor可以使得被管理对象被精细地分组管理: 1. kube-controller通过RC上定义的label Seletor来监控Pod副本数量; 2. kube-proxy进程通过Service的Label Seletor来选择对应的Pod,从而自动建立每个Service到对应Pod的请求转发路由表; 3. 通过在Pod定义文件中使用NodeSeletor,kube-schedule进程可以实现Pod定向调度;

总之,使用Label可以给对象创建多组标签,Label和Label Selector共同构成了Kubernetes系统中核心的应用模型,使得被管理对象能够被精细地分组管理,同时实现了整个集群的高可用性。

运行时,通过RC实现Pod的动态缩放: kubectl scale rc redis-slave --replicas=3

此外,在运行时,我们可以通过修改RC的副本数量,来实现Pod的动态缩放(Scaling),这可以通过执行kubectl scale命令来一键完成:

删除RC并不影响通过该RC已经创建好的Pod: 基于此,k8s可以实现滚动升级(停止一个Pod,创建一个Pod)

需要注意的是,删除RC并不会影响通过该RC已创建好的Pod。为了删除所有Pod,可以设置replicas的值为0,然后更新该RC。另外,kubectl提供了stop和delete命令来一次性删除RC和RC控制的全部Pod。应用升级时,通常会使用一个新的容器镜像版本替代旧版本。我们希望系统平滑升级,比如在当前系统中有10个对应的旧版本的Pod,则最佳的系统升级方式是旧版本的Pod每停止一个,就同时创建一个新版本的Pod,在整个升级过程中此消彼长,而运行中的Pod数量始终是10个,几分钟以后,当所有的Pod都已经是新版本时,系统升级完成。通过RC机制,Kubernetes很容易就实现了这种高级实用的特性,被称为“滚动升级”(Rolling Update),具体的操作方法详见3.11节的说明。

Replica Set:下一代的RC; 1. Replication Controller的升级,新增支持基于集合的Label Selector; 2. Replica Set主要被更高层的资源对象Deployment所用;

eplication Controller由于与Kubernetes代码中的模块Replication Controller同名,同时“Replication Controller”无法准确表达它的本意,所以在Kubernetes1.2中,升级为另外一个新概念——Replica Set,官方解释其为“下一代的RC”。Replica Set与RC当前的唯一区别是,Replica Sets支持基于集合的Label selector(Set-based selector),而RC只支持基于等式的Label Selector(equality-based selector),这使得Replica Set的功能更强。下面是等价于之前RC例子的Replica Set的定义(省去了Pod模板部分的内容):

HPA——Pod横向扩容,与RC一样,也是K8s的资源对象; 1. 通过追踪分析指定RC控制的所有目标Pod的负载变化情况,来针对性地调整目标Pod的副本数量; 2. Pod负载度量指标: 2.1 目标Pod所有副本自身的CPU利用率的平均值(当前pod CPU的使用量/ Pod request的值); 2.2 应用自定义的度量指标;

HPA与之前的RC、Deployment一样,也属于一种Kubernetes资源对象。通过追踪分析指定RC控制的所有目标Pod的负载变化情况,来确定是否需要有针对性地调整目标Pod的副本数量,这是HPA的实现原理。当前,HPA有以下两种方式作为Pod负载的度量指标。

客户端如何访问一个服务集群中的Pod容器呢? 1. k8s在Node上部署了负载均衡器,为某一个服务集群中的Pod开启了对外端口,并将集群内Pod的Endpoint(PodIP+ ContainerPort)加入到对外端口的转发列表中; 2. k8s Node上的kube-proxy就是这样一个智能软件负载均衡器,负责把Service的请求转发到后端的某个Pod实例上; 3. k8s如何实现的负载均衡? 3.1 为每一个Service分配一个全局唯一的Cluster IP,这样客户端访问服务就变成了先访问clusterIP +服务对外端口; 3.2 访问达到Cluster IP上后,通过对外端口上保存的EndPoint转发列表,实现请求转发;

Kubernetes也遵循上述常规做法,运行在每个Node上的kube-proxy进程其实就是一个智能的软件负载均衡器,负责把对Service的请求转发到后端的某个Pod实例上,并在内部实现服务的负载均衡与会话保持机制。但Kubernetes发明了一种很巧妙又影响深远的设计:Service没有共用一个负载均衡器的IP地址,每个Service都被分配了一个全局唯一的虚拟IP地址,这个虚拟IP被称为Cluster IP。这样一来,每个服务就变成了具备唯一IP地址的通信节点,服务调用就变成了最基础的TCP网络通信问题。

如何查看k8s自动为Service分配的Ip? kubectl get svc <服务名> -o yaml

你可能有疑问:“说好的Service的Cluster IP呢?怎么没有看到?”运行下面的命令即可看到tomct-service被分配的Cluster IP及更多的信息:

spec.ports.targetPort——Service下集群中提供服务的容器所暴露的端口;

在spec.ports的定义中,targetPort属性用来确定提供该服务的容器所暴露(EXPOSE)的端口号,即具体业务进程在容器内的targetPort上提供TCP/IP接入;port属性则定义了Service的虚端口。前面定义Tomcat服务时没有指定targetPort,则默认targetPort与port相同。

k8s的服务发现机制? 1.0 :k8s中每个Service都有唯一的Cluster IP和唯一的Name;最早时k8s为每个Service都生成一些对应的Linux ENV,并在每个Pod的容器启动时自动注入这些环境变量; 2.0 :k8s通过Add-On增值包引入了DNS系统,把服务名作为DNS域名;

首先,每个Kubernetes中的Service都有唯一的Cluster IP及唯一的名称,而名称是由开发者自己定义的,部署时也没必要改变,所以完全可以被固定在配置中。接下来的问题就是如何通过Service的名称找到对应的Cluster IP。

k8s集群内Node IP网、Pod IP网和Cluster IP网之间的通信采用自己设计的编程方式路由规则,与我们熟知的IP路由有很大不同; Cluster IP属于k8s集群内部的地址,虚拟IP,外部无法访问;

根据上面的分析和总结,我们基本明白了:Service的Cluster IP属于Kubernetes集群内部的地址,无法在集群外部直接使用这个地址。那么矛盾来了:实际上在我们开发的业务系统中肯定多少有一部分服务是要提供给Kubernetes集群外部的应用或者用户来使用的,典型的例子就是Web端的服务模块,比如上面的tomcat-service,那么用户怎么访问它?

k8s集群外部如何访问内部服务? Cluster IP——集群内部IP,不可行; Node Port——Service.spec.ports.nodePort,k8s会在集群里的每个Node上都为该服务开启一个对应的TCP监听端口,外部系统只需要任意使用一个Node IP + NodePort即可访问该服务;

NodePort的实现方式是在Kubernetes集群里的每个Node上都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+具体的NodePort端口号即可访问此服务,在任意Node上运行netstat命令,就可以看到有NodePort端口被监听:

Job-Pod副本自动控制器:restartPoliy全部设置为never;

应的Job也就结束了。Job在实现方式上与RC等副本控制器不同,Job生成的Pod副本是不能自动重启的,对应Pod副本的RestartPoliy都被设置为Never

Volume(存储卷)——是Pod中能够被多个容器访问的共享目录,生命周期与Pod相同; 1. 用法:先在Pod上声明一个Volume,然后再容器里引用该Volume并Mount到容器的某个目录上; 2. 丰富的Volume类型: 2.1 emptyDir Volume: 该Volume是Pod分配到Node时创建的,k8s自动分配一个宿主机上的对应目录,用来存放容器运行时所需的临时数据; 2.2 hostPath Volume:hostPath为在Pod上挂载在宿主机上的文件或目录;

Volume(存储卷)是Pod中能够被多个容器访问的共享目录。Kubernetes的Volume概念、用途和目的与Docker的Volume比较类似,但两者不能等价。首先,Kubernetes中的Volume被定义在Pod上,然后被一个Pod里的多个容器挂载到具体的文件目录下;其次,Kubernetes中的Volume与Pod的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume中的数据也不会丢失。最后,Kubernetes支持多种类型的Volume,例如GlusterFS、Ceph等先进的分布式文件系统。

创建Pod资源时指定命名空间: Pod.metadata.namespace = development 查看某个命名空间中的对象: kubectl get pods -n development

这是因为如果不加参数,则kubectl get命令将仅显示属于default命名空间的资源对象。可以在kubectl命令中加入--namespace参数来查看某个命名空间中的对象:

如何修改分布式系统中不同Pod中运行时容器的配置文件参数? ——k8s的ConfigMap资源对象,持久化在Etcd上; 1. k8s提供了内建3机制,将存储在etcd中的CondigMap通过Volume映射的方式变成目标Pod内的配置文件(在Pod内自动隐射); 2. 如果Config中的key-value数据被修改,则映射到Pod中的“配置文件”自动更新;

接下来,Kubernetes提供了一种内建机制,将存储在etcd中的ConfigMap通过Volume映射的方式变成目标Pod内的配置文件,不管目标Pod被调度到哪台服务器上,都会完成自动映射。进一步地,如果ConfigMap中的key-value数据被修改,则映射到Pod中的“配置文件”也会随之自动更新。于是,KubernetesConfigMap就成了分布式系统中最为简单(使用方法简单,但背后实现比较复杂)且对应用无侵入的配置中心。ConfigMap配置集中化的一种简单方案如图1.16所示。

2.2 使用kubeadm工具快速安装Kubernetes集群

kubeadm在Master安装了kubelet,默认情况下并不参与工作负载; 1. 如果希望安装一个单机All-In-One的k8环境,需要让Master成为一个Node: 删除Node的Label “node-role.kubernetes.io/master”: kubectl taint nodes --all node-role.kubernetes.io/master
kubeadm在Master上也安装了kubelet,在默认情况下并不参与工作负载。如果希望安装一个单机All-In-One的Kubernetes环境,则可以执行下面的命令(删除Node的Label“node-role.kubernetes.io/master”),让Master成为一个Node:

1. 查询k8s集群中相关Pod是否都正常创建并运行: kubectl get pods -all-namespace 2. 如果发现有状态错误的Pod,通过describe查看错误原因: kubectl --namespace=kube-system describe pod <pod_name>

如果发现有状态错误的Pod,则可以执行kubectl --namespace=kube-systemdescribe pod<pod_name>来查看错误原因,常见的错误原因是镜像没有下载完成。

2.3 以二进制文件方式安装Kubernetes集群

  1. systemctl start 服务——启动服务; 2. systemctl enable 服务——将服务加入开机启动列表中
    配置完成后,通过systemctl start命令启动etcd服务。同时,使用systemctlenable命令将服务加入开机启动列表中:

etcdctl endpoint health ——验证etcd是否正确启动

通过执行etcdctl cluster-health,可以验证etcd是否正确启动:

2.4 Kubernetes集群的安全设置

安全内网中,k8s各组件与Master之间可以通过kube-apiserver的非安全端口 http://<kube-apiserver-ip>:8080进行访问;
在一个安全的内网环境中,Kubernetes的各个组件与Master之间可以通过kube-apiserver的非安全端口http://<kube-apiserver-ip>:8080进行访问。

2.6 内网中的Kubernetes相关配置

keubectl创建Pod: 1. 通过启动一个名为k8s.gcr.io/pause:3.1的镜像来实现Pod的概念; 2. 从谷歌镜像仓库k8s.gcr.io中下载,push到私有Docker Registry中; 3. 然后给每个Node的kubelet服务都加上启动参数; /etc/kubernetest/kubelet KUBELET_ARGS="--pod-infra-container-image=myregistry/pause:3.1"
由于在Kubernetes中是以Pod而不是以Docker容器为管理单元的,在kubelet创建Pod时,还通过启动一个名为k8s.gcr.io/pause:3.1的镜像来实现Pod的概念。该镜像存在于谷歌镜像库k8s.gcr.io中,需要通过一台能够连上Internet的服务器将其下载,导出文件,再push到私有Docker Registry中。

2.9 CRI(容器运行时接口)详解

容器运行时: 1. 主要功能就是启动和停止容器;最出名的是Docker; 2. k8s从1.5版本开始引入了CRI接口规范,通过插件接口模式,k8s无需重新编译就可以使用更多的容器运行时;
归根结底,Kubernetes Node(kubelet)的主要功能就是启动和停止容器的组件,我们称之为容器运行时(Container Runtime),其中最知名的就是Docker了。为了更具扩展性,Kubernetes从1.5版本开始就加入了容器运行时插件API,即Container Runtime Interface,简称CRI。

2.10 kubectl命令行工具用法详解

kubectl 在线编辑运行中的资源对象: kubectl edit deploy nginx
可以使用kubectl edit命令编辑运行中的资源对象,例如使用下面的命令编辑运行中的一个Deployment:

kubectl port-forward --address 0.0.0.0 pod/nginx-6dd 8888:80

将集群上Pod的80端口映射到本地的8888端口,在浏览器http://127.0.0.1:8888中就能够访问到容器提供的服务了:

kubectl cp vs docker cp

10.在Pod和本地之间复制文件把Pod上的/etc/fstab复制到本地的/tmp目录:

k8s 1.8版本引入插件机制,1.14版本达到稳定: 1. 自定义插件-可执行文件:以“kubectl-”开头,如kubectl-hello; 2. 将该插件复制到¥PATh中的某个目录,即可完成插件安装; 3. kebectl hello;

为了扩展kubectl的功能,Kubernetes从1.8版本开始引入插件机制,在1.14版本时达到稳定版。用户自定义插件的可执行文件名需要以“kubectl-”开头,复制到$PATH中的某个目录(如/usr/local/bin),然后就可以通过kubectl <plugin-name>运行自定义插件了

3.2 Pod的基本用法

k8s系统对长时间运行容器的要求: 1. 容器主程序需要一直在前台执行;
Kubernetes系统中对长时间运行容器的要求是:其主程序需要一直在前台执行

3.3 静态Pod

静态Pod: 1. 定义:由kebelet进行管理的仅存在于特定Node上的Pod; 2. 创建静态Pod的两种方式:配置文件个HTTP: 2.1 配置文件方式: 2.1.1 设置kubelet的启动参数:--config=/etc/kubelet.d,kubelet会定期监控扫描该目录,并根据该目录下的.yaml或.json文件进行创建操作; 2.1.2 删除该POD的办法:只能到其所在的Node上将该Pod的定义文件static-web.yaml从/etc/kubelet.d目录下删除; 2.2 HTTP方式: 通过kubelete的启动参数 "--manifest-url"
静态Pod是由kubelet进行管理的仅存在于特定Node上的Pod。它们不能通过APIServer进行管理,无法与ReplicationController、Deployment或者DaemonSet进行关联,并且kubelet无法对它们进行健康检查。静态Pod总是由kubelet创建的,并且总在kubelet所在的Node上运行。

3.5 Pod的配置管理

应用部署最佳实践:将应用所需的配置信息与程序分离; 配置通过ENV或外挂文件的方式进行注入,但在大规模容器集群环境中,对多个容器进行不同的配置将变得非常复杂; ——解决方案:ConfigMap
3.5.1 ConfigMap概述

ConfigMap使用限制: 1. 在Pod的定义中,通过环境变量envFrom将ConfigMap中所有定义的key=value自动生成环境变量; 2. 另外,可以通过volumeMounts将ConfigMap的内容以文件的形式mount到容器内部的/configfiles目录; 3. ConfigMap必须在Pod之前创建,只能用于同一个namespace,且kubelet只支持可以被API Server管理的Pod使用ConfigMap,即静态Pod无法引用ConfigMap;

kubelet只支持可以被API Server管理的Pod使用ConfigMap。kubelet在本Node上通过--manifest-url或--config自动创建的静态Pod将无法引用ConfigMap。

3.6 在容器内获取Pod信息(Downward API)

容器内部获取Pod信息P(PodName、PodIP)方式: ——Downward API;
我们知道,每个Pod在被成功创建出来之后,都会被系统分配唯一的名字、IP地址,并且处于某个Namespace中,那么我们如何在Pod的容器内获取Pod的这些重要信息呢?答案就是使用Downward API。

Downward API的作用? 1. 实质是在Pod定义时,将Pod自身信息注入为Container中的ENV或者是将pod自身信息通过Volume以文件形式Mount到Container目录上; 2. valueFrom:fieldRef:item:此类特殊语法都是DownwardAPI的写法; 3. 在集群中,每个节点将自身的ID以及进程绑定的IP地址等信息通过DownwardAPI写入到主程序的配置文件中,然后由主程序将这些信息发布到服务注册中心,这样就实现了集群节点的自动发现功能;

那么,Downward API有什么价值呢?在某些集群中,集群中的每个节点都需要将自身的标识(ID)及进程绑定的IP地址等信息事先写入配置文件中,进程在启动时会读取这些信息,然后将这些信息发布到某个类似服务注册中心的地方,以实现集群节点的自动发现功能。此时Downward API就可以派上用场了,具体做法是先编写一个预启动脚本或InitContainer,通过环境变量或文件方式获取Pod自身的名称、IP地址等信息,然后将这些信息写入主程序的配置文件中,最后启动主程序。

3.7 Pod生命周期和重启策略

管理Pod的控制器包括: 1. RC; 2. Job; 3. Daemonset; 4. kubelet管理(静态Pod);
Pod的重启策略与控制方式息息相关,当前可用于管理Pod的控制器包括ReplicationController、Job、DaemonSet及直接通过kubelet管理(静态Pod)。每种控制器对Pod的重启策略要求如下。◎ RC和DaemonSet:必须设置为Always,需要保证该容器持续运行。

3.8 Pod健康检查和服务可用性检查

k8s对Pod的健康状态检查: 1. LivenessProbe探针:如果探测到容器 Not Running,则kubelet将杀掉该容器,并根据容器的重启策略做相应的处理; 2. ReadinessProbe探针:用于判断容器服务是否可用(Ready?),达到Ready状态的Pod才可以接收请求;
Kubernetes对Pod的健康状态可以通过两类探针来检查: LivenessProbe和ReadinessProbe,kubelet定期执行这两类探针来诊断容器的健康状况

k8s如何保证客户端在访问Service时,请求不会被转发到服务不可用的Pod实例上? 1. k8s通过ReadinessProbe探针,定期诊断容器服务是否可用(Ready?)达到ready状态的Pod才可以接收请求; 2. Service与Pod Endpoint的关联关系也是基于Pod是否Ready进行设置: 2.1 如果Pod运行过程中Ready状态变为False,则系统自动将该Pod从Service的后端Endpoind列表中隔离出去;

ReadinessProbe探针:用于判断容器服务是否可用(Ready状态),达到Ready状态的Pod才可以接收请求。对于被Service管理的Pod,Service与PodEndpoint的关联关系也将基于Pod是否Ready进行设置。如果在运行过程中Ready状态变为False,则系统自动将其从Service的后端Endpoint列表中隔离出去,后续再把恢复到Ready状态的Pod加回后端Endpoint列表。这样就能保证客户端在访问Service时不会被转发到服务不可用的Pod实例上。

3.9 玩转Pod调度

RC、ReplicaSet、Deployment这三个控制器的区别: 1. ReplicaSet在RC的基础上增加了集合式的标签selector,即可以选择多个Pod标签到服务中; 2. Deployment是通过ReplicaSet来shixianPod副本自动控制的; 3. 理论上哪一种控制器创建的Pod副本实例都是归属于这些控制器的,但是如果控制器被删除后,这些Pod副本何去何从? ——k8s 1.9之前,这些Pod副本会保留;k8s 1.9以后,会全部一并删除;
与单独的Pod实例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?在Kubernates 1.9之前,在RC等对象被删除后,它们所创建的Pod副本都不会被删除;在Kubernates 1.9以后,这些Pod副本会被一并删除。如果不希望这样做,则可以通过kubectl命令的--cascade=false参数来取消这一默认特性:

在线给Node打标签: kubectl label ndoes <node-name> <label-key>=<label-value>

(1)首先通过kubectl label命令给目标Node打上一些标签:

如果Pod的nodeSelector在集群中匹配失败,则该Pod无法被调度成功;

需要注意的是,如果我们指定了Pod的nodeSelector条件,且在集群中不存在包含相应标签的Node,则即使在集群中还有其他可供使用的Node,这个Pod也无法被成功调度。

NodeAffinity vs NodeSeletor vs PodAffinity用法注意事项: 1. NodeSeletor从集群中没选到对应标签的Node时,Pod无法被成功调度; 2. NodeAffinity通过IgnoredDuringExection允许一个Pod所在的节点标签在Pod运行期间变更,则Pod能继续运行; 3. PodAffinity:在创建Pod前,如果删掉Node的topologyKey标签,则Pod将调度失败(Pending);

IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间标签发生了变更,不再符合该Pod的节点亲和性需求,则系统将忽略Node上Label的变化,该Pod能继续在该节点运行。

设置PodAffinity时: 1. 具有标签X的Node,其中X是指一个集群中的节点、机架、区域等概念,通过k8s内置节点标签topologyKey声明,内容如下: kubernetes.io/hostname failure-domain.beta.kubernetes.io/zone failure-domain.beta.kubernetes.io/region

这里X指的是一个集群中的节点、机架、区域等概念,通过Kubernetes内置节点标签中的key来进行声明。这个key的名字为topologyKey,意为表达节点所属的topology范围。◎ kubernetes.io/hostname◎ failure-domain.beta.kubernetes.io/zone◎ failure-domain.beta.kubernetes.io/region与节点不同的是,Pod是属于某个命名空间的,所以条件Y表达的是一个或者全部命名空间中的一个Label Selector。

Pod亲和性调度功能: 1. NodeAffinity——Node亲和性的调度策略,用于替换NodeSelector; 1.1 RequiredDuringSchedulingIgnoredDuringExeution: ——硬限制,必须满足指定的规则才可以调度Pod到Node上;多个优先级规则还可以设置权重; 1.2 PreferredDuringSchedulingIgnoredDuringExecution: ——软限制,强调优先满足指定规则,调度器会尝试调度Pod到Node上; 2. PodAffinity: 2.1 Pod亲和性调度策略:如果在具有标签X的Node上运行了一个或者多个符合条件Y的Pod,那么Pod应该运行在这个Node上;(互斥性调度,则相反); 2.2 Pod的互斥调度:通过PodSpec.affinity.podAntiAffinity关键字标识; 遗留问题:beta.kubernetes.io/zone——业务含义是什么?

3.Pod的互斥性调度创建第3个Pod,我们希望它不与目标Pod运行在同一个Node上:

Zone vs Node : 1. Zone是集群的概念? 2. 如何配置?

这里要求这个新Pod与security=S1的Pod为同一个zone,但是不与app=nginx的Pod为同一个Node。创建Pod之后,同样用kubectl get pods -o wide来查看,会看到新的Pod被调度到了同一Zone内的不同Node上。

PodAffiniy规则设置注意事项: 1. Pod.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.labelSeletor:用来设置于目标Pod亲和性的Label(使用matchExpressions); 2. Pod.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.topologyKey:用来标识具有相同;

PodAffinity规则设置的注意事项如下。◎ 除了设置Label Selector和topologyKey,用户还可以指定Namespace列表来进行限制,同样,使用Label Selector对Namespace进行选择。Namespace的定义和Label Selector及topologyKey同级。省略Namespace的设置,表示使用定义了affinity/anti-affinity的Pod所在的Namespace。如果Namespace被设置为空值(""),则表示所有Namespace。

书签-玩转Pod调度

前面提到的NoExecute这个Taint效果对节点上正在运行的Pod有以下影响。◎ 没有设置Toleration的Pod会被立刻驱逐。

3.12 Pod的扩缩容

HPA metrics.type主要有: 1. Resource:cpu和内存;指标数据可以通过API "metrics.k8s.io"进行查询; 2. Pods和Object——属于自定义指标,数据需要搭建自定义Metrics Server和监控工具采集和处理;指标数据可以通过API“custom.metrics.k8s.io”查询;
Resource类型的指标可以设置CPU和内存。对于CPU使用率,在target参数中设置averageUtilization定义目标平均CPU使用率。对于内存资源,在target参数中设置AverageValue定义目标平均内存使用值。指标数据可以通过API“metrics.k8s.io”进行查询,要求预先启动Metrics Server服务。Pods类型和Object类型都属于自定义指标类型,指标的数据通常需要搭建自定义Metrics Server和监控工具进行采集和处理。指标数据可以通过API“custom.metrics.k8s.io”进行查询,要求预先启动自定义Metrics Server服务。

基于外部服务的指标时,需要预先部署自定义的Metrics Server,对接k8s HPA控制器;

在使用外部服务的指标时,要安装、部署能够对接到Kubernetes HPA模型的监控系统,并且完全了解监控系统采集这些指标的机制,后续的自动扩缩容操作才能完成。

1. 基于Prometyheus监控系统采集的每秒Http_request平均值自定义Metrics Server; 2. 通过K8s的Metrics Aggregation层将自定义指标API注册到Master的API Server中; 3. 创建一个Prometheus的ServiceMonitor对象,用来监控应用程序提供的指标; 4. 创建一个HPA对象,设置作用对象为之前部署的sample-app deployment,通过metrics.type=Pods和metrics.pods.name=http_requests来设置监控指标类型;

本节基于Prometheus监控系统对HPA的基础组件部署和HPA配置进行详细说明。基于Prometheus的HPA架构如图3.10所示。

4.2 Service的基本用法

多实例容器提供服务时,由于Pod易变化,使用PodIP+ containerPort访问服务不可靠; 另外,外部的请求如何均衡得分发到每个容器实例上,也是要解决的问题; ——Service就是用来解决这个问题的;
直接通过Pod的IP地址和端口号可以访问到容器应用内的服务,但是Pod的IP地址是不可靠的,例如当Pod所在的Node发生故障时,Pod将被Kubernetes重新调度到另一个Node,Pod的IP地址将发生变化。更重要的是,如果容器应用本身是分布式的部署方式,通过多个实例共同提供服务,就需要在这些实例的前端设置一个负载均衡器来实现请求的分发。Kubernetes中的Service就是用于解决这些问题的核心组件。

k8s基于RC实现了一种快速创建服务的方法: kubectl expose rc name;

Kubernetes提供了一种快速的方法,即通过kubectl expose命令来创建Service:

Service资源中,spec.ports.port和spec.ports.targetPort的区别: 1. spec.ports.port是Service对外暴露的端口,用户可以通过ClusterIp + port实现外部访问服务; 2. spec.ports.targetPort是Service对应的每个容器实例暴露的端口;

Service定义中的关键字段是ports和selector。本例中ports定义部分指定了Service所需的虚拟端口号为8081,由于与Pod容器端口号8080不一样,所以需要再通过targetPort来指定后端Pod的端口号。selector定义部分设置的是后端Pod所拥有的label:app=webapp。

1. k8s默认使用RoundRobin模式对客户端请求实现负载分发; 2. 通过service.spec.sessionAffinity=ClientIp来启用主题亲和性设置;这样,同一个客户端IP发来的请求就会被转发到后端固定某个Pod上;

在默认情况下,Kubernetes采用RoundRobin模式对客户端请求进行负载分发,但我们也可以通过设置service.spec.sessionAffinity=ClientIP来启用SessionAffinity策略。这样,同一个客户端IP发来的请求就会被转发到后端固定的某个Pod上了。

在默认情况下,Kubernetes采用RoundRobin模式对客户端请求进行负载分发,但我们也可以通过设置service.spec.sessionAffinity=ClientIP来启用SessionAffinity策略。这样,同一个客户端IP发来的请求就会被转发到后端固定的某个Pod上了。

通过设置无label的service + 同名的EndPoint,可以实现Service将请求转发到外部服务; 1. 无label,则不会实际选择对应后端的Pod; 2. 手动创建和该Seevice同名的EndPoint,用于指向实际的后端访问地址;

通过该定义创建的是一个不带标签选择器的Service,即无法选择后端的Pod,系统不会自动创建Endpoint,因此需要手动创建一个和该Service同名的Endpoint,用于指向实际的后端访问地址。创建Endpoint的配置文件内容如下:

4.3 Headless Service

何为“去中心化模式”? ——各节点实现相互查找和集群自动搭建?
由于Cassandra使用的是“去中心化”模式,所以在集群里的一个节点启动之后,需要一个途径获知集群中新节点的加入。Cassandra使用了Seed(种子)来完成集群中节点之间的相互查找和通信。

4.4 从集群外部访问Pod或Service

将容器应用的端口号 -map->到 物理机: 1. 设置容器级别的hostPort:Pod.spec.containers.ports.containerPort Pod.spec.containers.ports.containerPort; 2. 设置Pod级别的hostNetwork = true: Pod.spec.hostNetwork = true;
将容器应用的端口号映射到物理机(1)通过设置容器级别的hostPort,将容器应用的端口号映射到物理机上:

将Service的端口号 -map->到物理机: 1. 设置nodePort类型Port: Service.spec.type = NodePort && Service.spec.ports.port.nodePort = 8081;

将Service的端口号映射到物理机(1)通过设置nodePort映射到物理机,同时设置Service的类型为NodePort:

4.5 DNS服务搭建和配置指南

如果集群中的Node数量庞大,如何自动修改每个Node上的kubelet启动参数?
在创建DNS服务之前修改每个Node上kubelet的启动参数修改每个Node上kubelet的启动参数,加上以下两个参数。

ClusterIp是k8s创建Service后自动给Service的分配的内部IP, 那么此时添加到Service.yml里面的ClusterIp又是什么?

Service“kube-dns”是DNS服务的配置,示例如下。这个服务需要设置固定的ClusterIP,也需要将所有Node上的kubelet启动参数--cluster-dns设置为这个ClusterIP:

6.1 API Server认证管理

CA通过证书来证实他人的公钥信息,而证书作为有效证据,用户可以基于此追究Ca的法律责任
CA作为可信第三方的重要条件之一就是CA的行为具有非否认性。作为第三方而不是简单的上级,就必须能让信任者有追究自己责任的能力。CA通过证书证实他人的公钥信息,证书上有CA的签名。用户如果因为信任证书而有了损失,则证书可以作为有效的证据用于追究CA的法律责任。

7.1 Kubernetes网络模型

k8s网络模型设计的基础原则:IP-Per-Pod;
实际上,在Kubernetes的世界里,IP是以Pod为单位进行分配的。一个Pod内部的所有容器共享一个网络堆栈(相当于一个网络命名空间,它们的IP地址、网络设备、配置等都是共享的)。按照这个网络原则抽象出来的为每个Pod都设置一个IP地址的模型也被称作IP-per-Pod模型。

Docker原生的通过动态端口映射方式实现多节点访问模式?

IP-per-Pod模式和Docker原生的通过动态端口映射方式实现的多节点访问模式有什么区别呢?

IP-Per-Pod网络模型对集群网络要求如下: 1. 无论是Pod内部还是外部,看到的容器地址是一样的; 2. 所有容器都可以在不用NAT的方式下同别的容器通信; 2. 所有节点都可以在不用NAT的方式下同所有容器通信;

按照这个网络抽象原则,Kubernetes对网络有什么前提和要求呢?Kubernetes对集群网络有如下要求。

7.2 Docker网络基础

Dokcer技术依赖Linux内核虚拟化技术: 网络命名空间、Veth设备对、网桥、iptables和路由;
Docker本身的技术依赖于近年来Linux内核虚拟化技术的发展,所以Docker对Linux内核的特性有很强的依赖。这里将Docker使用到的与Linux网络有关的主要技术进行简要介绍,这些技术有:网络命名空间(Network Namespace)、Veth设备对、网桥、ipatables和路由。

Linux网络协议栈若要支持独立的协议栈: 1. 先将协议栈相关的全局变量修改为一个私有的Net Namespace变量的成员; 2. 然后为每个协议栈的函数调用加一个Namespace参数;

Linux的网络协议栈是十分复杂的,为了支持独立的协议栈,相关的这些全局变量都必须被修改为协议栈私有。最好的办法就是让这些全局变量成为一个Net Namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。这就是Linux实现网络命名空间的核心。

网络命名空间代表的是一个独立的协议栈,正常是相互隔离、无法通信的,应用Veth设备对可以打破这个限制;

前面提到,由于网络命名空间代表的是一个独立的协议栈,所以它们之间是相互隔离的,彼此无法通信,在协议栈内部都看不到对方。那么有没有办法打破这种限制,让处于不同命名空间的网络相互通信,甚至和外部的网络进行通信呢?答案就是“有,应用Veth设备对即可”。

网桥:二层的虚拟网络设备,把若干个网络接口连接起来,以使得网络接口之间的报文能够相互转发; 作用: 1. 解析收发的报文,读取目标MAC地址的信息,结合自己记录的MAC表,来决策报文转发的目标网络接口; 2. 周期学习源的MAC地址;

Linux可以支持多个不同的网络,它们之间能够相互通信,如何将这些网络连接起来并实现各网络中主机的相互通信呢?可以用网桥。网桥是一个二层的虚拟网络设备,把若干个网络接口“连接”起来,以使得网络接口之间的报文能够互相转发

Linux中,网桥虽然是虚拟二层设备,但是可以拥有IP; 另外网桥下面连接的每个网口,其实就是一个物理网卡;

在Linux中,一个网口其实就是一个物理网卡。将物理网卡和网桥连接起来:

Linux网络协议栈提供了一组回调函数挂接点,来为用户实现了自定义的数据包处理过程: 包括过滤、修改、丢弃等; 整个挂接点技术叫做Netfilter和iptables;

Linux提供了一套机制来为用户实现自定义的数据包处理过程。在Linux网络协议栈中有一组回调函数挂接点,通过这些挂接点挂接的钩子函数可以在Linux网络栈处理数据包的过程中对数据包进行一些操作,例如过滤、修改、丢弃等。整个挂接点技术叫作Netfilter和iptables。

注意:如果主机没有配置路由功能,报文将会被丢弃;

如果报文中的IP地址不是主机自身的地址,并且主机配置了路由功能,那么报文将

通过主机上IP路由转发时,若: 1. 匹配到目标IP地址或者匹配到目标网络ID上,则该数据报文将会被转发到指定的路由器上; 2. 如果1条件不满足,则数据报文将会被转发到一个默认的路由器上; 3. 如果1、2条件都不满足,则应答ICMP主机不可达或ICMP网络不可达错误;

如果上述两个条件都不匹配,那么该数据报文将被转发到一个默认的路由器上。如果上述步骤都失败,默认路由器也不存在,那么该数据报文最终无法被转发。任何无法投递的数据报文都将产生一个ICMP主机不可达或ICMP网络不可达的错误,并将此错误返回给生成此数据报文的应用程序。

7.3 Docker的网络实现

同一台主机上容器间相互通信的秘密: 1. Docker Daemon在第一次启动时,会自动创建一个docker0网桥,并为它分配一个IP地址(一般以172开头); 2. dockers在启动容器时,会在这个地址段选择一个新IP给容器; 3. 同时docker会根据容器IP地址,为每个容器生成一个MAC地址,范围是02:42:ac:11:00:00 ~ 02:42:ac:11:ff:ff(为什么是这个范围?) 4. 后续容器内的数据报文,通过docker0网桥转发给对应MAC地址上的容器;
其中ip1是网桥的IP地址,Docker Daemon会在几个备选地址段里给它选一个地址,通常是以172开头的一个地址。这个地址和主机的IP地址是不重叠的。ip2是Docker在启动容器时,在这个地址段选择的一个没有使用的IP地址分配给容器。相应的MAC地址也根据这个IP地址,在02:42:ac:11:00:00和02:42:ac:11:ff:ff的范围内生成,这样做可以确保不会有ARP冲突。

ip addr——查看当前主机上网络和端口配置; ip route——查看路由表 iptable-save——查看本地的iptables;

3.查看容器启动后的情况(容器有端口映射)下面用带端口映射的命令启动registry:

7.4 Kubernetes的网络实现

同一个Node上运行的Pod中不同容器实例共享一个网络的命名空间,可以直接使用IPC通信; 同一个Node上不同Pod通过Veth连接到同一个docker0网桥,PodId都是从docker0的网段上动态分配的,属于同一个网段; 每个Node上都有一个docker0网桥,它是该Node上所有Pod的默认路由;
在Node上运行着一个Pod实例。在我们的例子中,容器就是图7.8中的容器1和容器2。容器1和容器2共享一个网络的命名空间,共享一个命名空间的结果就是它们好像在一台机器上运行,它们打开的端口不会有冲突,可以直接使用Linux的本地IPC进行通信

7.5 Pod和Service网络实战

书签:2021.11.14
我们深入容器内部去看一下具体原因。使用Docker的inspect命令来查看容器的详细信息,特别要关注容器的网络模型:

第10章 Kubernetes集群管理

将某个Node隔离或者纳入调度范围: 1. kubectl replace -f unschedule_node.yaml; 2. kubectl patch node ;
同样,如果需要将某个Node重新纳入集群调度范围,则将unschedulable设置为false,再次执行kubectl replace或kubectl patch命令就能恢复系统对该Node的调度。

新Node加入K8s集群依赖项: Docker、kubelet、kube-proxy

一个新Node的加入是非常简单的。在新的Node上安装Docker、kubelet和kube-proxy服务

新Node自动注册并加入现有的Kubernetes集群中, 需要依赖在新Node上将Master URL指定为当前Kubernetes集群的Master的地址;

将Master URL指定为当前Kubernetes集群Master的地址,最后启动这些服务。通过kubelet默认的自动注册机制,新的Node将会自动加入现有的

10.3 Namespace:集群环境共享与隔离

kubectl create -f namespace-development.yaml ——创建命名空间
使用kubectl create命令完成命名空间的创建:

定义Context(运行环境): 1. kubectl config set-cluster ; //设置集群 2. kubectl config set -context ctx-dev --namespace --cluster; 即:设置Context依赖集群和Namespace

接下来,需要为这两个工作组分别定义一个Context,即运行环境。这个运行环境将属于某个特定的命名空间。通过kubectl config set-context命令定义Context,并将Context置于之前创建的命名空间中:

10.4 Kubernetes资源管理

KiB、MiB——以二进制表示的字节单位: 1 KiB = 2^10 bytes KB、MB——以十进制表示的字节单位: 1 KB = 1000 Bytes
KiB与MiB是以二进制表示的字节单位,常见的KB与MB则是以十进制表示的字节单位,比如:

k8s启动Pod的某个容器时,将spec.container[].resources.requests.cpu传递给容器执行环境: docker run --cpu-share

时Kubernetes会将这个参数的值传递给docker run的--cpu-shares参数。--cpu-shares参数对于Docker而言是相对值,主要用于资源分配比例。

换句话说,从k8s 1.2版本开始,kubelet 会强制要求所有pod都必须配置CPU Limits;

注意:如果kubelet的启动参数--cpu-cfs-quota被设置为true,那么kubelet会强制要求所有Pod都必须配置CPU Limits(如果Pod没有配置,则集群提供了默认配置也可以)。从Kubernetes 1.2版本开始,这个--cpu-cfs-quota启动参数的默认值就是true。

对memory limits,如果容器在运行过程中使用内存超限了,那么kubelet极有可能会把它杀掉; 对CPU limits,CPU在容器技术中属于可压缩资源,容器运行时CPU超限,偶然超标不会导致被系统杀掉;

如果一个容器在运行过程中使用了超出了其内存Limits配置的内存限制值,那么它可能会被杀掉,如果这个容器是一个可重启的容器,那么之后它会被kubelet重新启动。因此对容器的Limits配置需要进行准确测试和评估。与内存Limits不同的是,CPU在容器技术中属于可压缩资源,因此对CPU的Limits配置一般不会因为偶然超标使用而导致容器被系统杀掉。

kubectl get pod -o go-template=''{{range.status.containerStatuses}} ——查看已终止容器前的状态信息

我们可以在使用kubectl get pod命令时添加-o go-template=...格式参数来读取已终止容器之前的状态信息:


使用 小悦记 导出 | 2022年2月23日

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容