一文读懂 K8s 持久化存储流程 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
AlibabaSS
V2EX    推广

一文读懂 K8s 持久化存储流程

  •  
  •   AlibabaSS 2020-04-26 15:19:31 +08:00 4613 次点击
    这是一个创建于 2084 天前的主题,其中的信息可能已经有所发展或是发生改变。

    4.9 头条.png

    作者 | 孙志恒(惠志) 阿里巴巴开发工程师

    导读:众所周知,K8s 的持久化存储( Persistent Storage )保证了应用数据独立于应用生命周期而存在,但其内部实现却少有人提及。K8s内部的存储流程到底是怎样的? PV 、PVC 、StorageClass 、Kubelet 、CSI 插件等之间的调用关系又如何,这些谜底将在本文中一一揭晓。

    K8s 持久化存储基础

    在进行 K8s 存储流程讲解之前,先回顾一下 K8s 中持久化存储的基础概念。

    1. 名词解释

    • in-tree:代码逻辑在 K8s 官方仓库中;

    • out-of-tree:代码逻辑在 K8s 官方仓库之外,实现与 K8s 代码的解耦;

    • PV:PersistentVolume,集群级别的资源,由 集群管理员 or External Provisioner 创建。PV 的生命周期独立于使用 PV 的 Pod,PV 的 .Spec 中保存了存储设备的详细信息;

    • PVC:PersistentVolumeClaim,命名空间( namespace )级别的资源,由 用户 or StatefulSet 控制器(根据 VolumeClaimTemplate ) 创建。PVC 类似于 Pod,Pod 消耗 Node 资源,PVC 消耗 PV 资源。Pod 可以请求特定级别的资源( CPU 和内存),而 PVC 可以请求特定存储卷的大小及访问模式( Access Mode );

    • StorageClass:StorageClass 是集群级别的资源,由集群管理员创建。SC 为管理员提供了一种动态提供存储卷的“类”模板,SC 中的 .Spec 中详细定义了存储卷 PV 的不同服务质量级别、备份策略等等;

    • CSI:Container Storage Interface,目的是定义行业标准的“容器存储接口”,使存储供应商( SP )基于 CSI 标准开发的插件可以在不同容器编排( CO )系统中工作,CO 系统包括 Kubernetes 、Mesos 、Swarm 等。

    2. 组件介绍

    • PV Controller:负责 PV/PVC 绑定及周期管理,根据需求进行数据卷的**Provision/Delete**操作;

    • AD Controller:负责数据卷的**Attach/Detach**操作,将设备挂接到目标节点;

    • Kubelet:Kubelet 是在每个 Node 节点上运行的主要 “节点代理”,功能是 Pod 生命周期管理、容器健康检查、容器监控等;

    • Volume Manager:Kubelet 中的组件,负责管理数据卷的**Mount/Umount**操作(也负责数据卷的**Attach/Detach**操作,需配置 kubelet 相关参数开启该特性)、卷设备的格式化等等;

    • Volume Plugins:存储插件,由存储供应商开发,目的在于扩展各种存储类型的卷管理能力,实现第三方存储的各种操作能力,即是上面蓝色操作的实现。Volume Plugins 有in-tree和 out-of-tree 两种;

    • External Provioner:External Provioner 是一种sidecar 容器,作用是调用 Volume Plugins 中的CreateVolume 和DeleteVolume 函数来执行**Provision/Delete**操作。因为 K8s 的 PV 控制器无法直接调用 Volume Plugins 的相关函数,故由 External Provioner 通过 gRPC 来调用;

    • External Attacher:External Attacher 是一种sidecar 容器,作用是调用 Volume Plugins 中的ControllerPublishVolume 和ControllerUnpublishVolume 函数来执行**Attach/Detach**操作。因为 K8s 的 AD 控制器无法直接调用 Volume Plugins的相关函数,故由 External Attacher 通过 gRPC 来调用。

    3. 持久卷使用

    Kubernetes 为了使应用程序及其开发人员能够正常请求存储资源,避免处理存储设施细节,引入了 PV 和 PVC 。创建 PV 有两种方式:

    • 一种是集群管理员通过手动方式静态创建应用所需要的 PV ;

    • 另一种是用户手动创建 PVC 并由 Provisioner 组件动态创建对应的 PV 。

    下面我们以 NFS 共享存储为例来看二者区别。

    静态创建存储卷

    静态创建存储卷流程如下图所示:

    1.png

    第一步:集群管理员创建 NFS PV,NFS 属于 K8s 原生支持的 in-tree 存储类型。yaml 文件如下:

    apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain nfs: server: 192.168.4.1 path: /nfs_storage 

    第二步:用户创建 PVC,yaml 文件如下:

    apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi 

    通过kubectl get pv 命令可看到 PV 和 PVC 已绑定:

    [root@huizhi ~]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nfs-pvc Bound nfs-pv-no-affinity 10Gi RWO 4s 

    第三步:用户创建应用,并使用第二步创建的 PVC 。

    apiVersion: v1 kind: Pod metadata: name: test-nfs spec: containers: - image: nginx:alpine imagePullPolicy: IfNotPresent name: nginx volumeMounts: - mountPath: /data name: nfs-volume volumes: - name: nfs-volume persistentVolumeClaim: claimName: nfs-pvc 

    此时 NFS 的远端存储就挂载了到 Pod 中 nginx 容器的 /data 目录下。

    动态创建存储卷

    动态创建存储卷,要求集群中部署有nfs-client-provisioner以及对应的storageclass

    动态创建存储卷相比静态创建存储卷,少了集群管理员的干预,流程如下图所示:

    2.png

    集群管理员只需要保证环境中有 NFS 相关的 storageclass 即可:

    kind: StorageClass apiVersion: storage.k8s.io/v1 metadata: name: nfs-sc provisioner: example.com/nfs mountOptions: - vers=4.1 

    第一步:用户创建 PVC,此处 PVC 的 storageClassName 指定为上面 NFS 的 storageclass 名称:

    kind: PersistentVolumeClaim apiVersion: v1 metadata: name: nfs annotations: volume.beta.kubernetes.io/storage-class: "example-nfs" spec: accessModes: - ReadWriteMany resources: requests: storage: 10Mi storageClassName: nfs-sc 

    第二步:集群中的nfs-client-provisioner 会动态创建相应 PV 。此时可看到环境中 PV 已创建,并与 PVC 已绑定。

    [root@huizhi ~]# kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM REASON AGE pvc-dce84888-7a9d-11e6-b1ee-5254001e0c1b 10Mi RWX Delete Bound default/nfs 4s 

    第三步:用户创建应用,并使用第二步创建的 PVC,同静态创建存储卷的第三步。

    K8s 持久化存储流程

    1. 流程概览

    此处借鉴 @郡宝云原生存储课程中的流程图

    3.png

    流程如下:

    1. 用户创建了一个包含 PVC 的 Pod,该 PVC 要求使用动态存储卷;

    2. Scheduler根据 Pod 配置、节点状态、PV 配置等信息,把 Pod 调度到一个合适的 Worker 节点上;

    3. PV 控制器watch 到该 Pod 使用的 PVC 处于 Pending 状态,于是调用Volume Plugin( in-tree )创建存储卷,并创建 PV 对象( out-of-tree 由 External Provisioner 来处理);

    4. AD 控制器发现 Pod 和 PVC 处于待挂接状态,于是调用**Volume Plugin**挂接存储设备到目标 Worker 节点上

    5. 在 Worker 节点上,Kubelet 中的 Volume Manager等待存储设备挂接完成,并通过Volume Plugin将设备挂载到全局目录/var/lib/kubelet/pods/[poduid]/volumes/kubernetes.io~iscsi/[PV name](以 iscsi 为例);

    6. **Kubelet**通过 Docker 启动Pod的 Containers,用bind mount方式将已挂载到本地全局目录的卷映射到容器中。

    更详细的流程如下:

    4.png

    2. 流程详解

    不同 K8s 版本,持久化存储流程略有区别。本文基于 Kubernetes 1.14.8版本。

    从上述流程图中可看到,存储卷从创建到提供应用使用共分为三个阶段:Provision/Delete 、Attach/Detach 、Mount/Unmount 。

    provisioning volumes

    5.png

    PV 控制器中有两个 Worker:

    • ClaimWorker:处理 PVC 的 add / update / delete 相关事件以及 PVC 的状态迁移;
    • VolumeWorker:负责 PV 的状态迁移。

    PV 状态迁移( UpdatePVStatus ):

    • PV 初始状态为Available,当 PV 与 PVC 绑定后,状态变为Bound ;
    • 与 PV 绑定的 PVC 删除后,状态变为Released ;
    • 当 PV 回收策略为 Recycled 或手动删除 PV 的 .Spec.ClaimRef 后,PV 状态变为Available ;
    • 当 PV 回收策略未知或 Recycle 失败或存储卷删除失败,PV 状态变为Failed ;
    • 手动删除 PV 的 .Spec.ClaimRef,PV 状态变为Available 。

    PVC 状态迁移( UpdatePVCStatus ):

    • 当集群中不存在满足 PVC 条件的 PV 时,PVC 状态为Pending 。在 PV 与 PVC 绑定后,PVC 状态由Pending变为Bound ;
    • 与 PVC 绑定的 PV在环境中被删除,PVC 状态变为Lost ;
    • 再次与一个**同名 PV**绑定后,PVC 状态变为Bound。

    Provisioning 流程如下(此处模拟用户创建一个新 PVC ):

    静态存储卷流程( FindBestMatch )PV 控制器首先在环境中筛选一个状态为 Available 的 PV 与新 PVC 匹配。

    • DelayBinding:PV 控制器判断该 PVC 是否需要**延迟绑定:**1.查看 PVC 的 annotation 中是否包含 volume.kubernetes.io/selected-node,若存在则表示该 PVC 已经被调度器指定好了节点(属于ProvisionVolume),故不需要延迟绑定; 2.若 PVC 的 annotation 中不存在volume.kubernetes.io/selected-node,同时没有StorageClass,默认表示不需要延迟绑定;若有 StorageClass,查看其VolumeBindingMode 字段,若为 WaitForFirstConsumer 则需要延迟绑定,若为 Immediate 则不需要延迟绑定;

    • FindBestMatchPVForClaim:PV 控制器尝试找一个满足 PVC 要求的环境中现有的 PV 。PV 控制器会将**所有的 PV**进行一次筛选,并会从满足条件的 PV 中选择一个最佳匹配的 PV 。筛选规则:1.VolumeMode 是否匹配; 2. PV 是否已绑定到 PVC 上; 3. PV 的 .Status.Phase 是否为 Available ; 4. LabelSelector 检查,PV 与 PVC 的 label 要保持一致; 5. PV 与PVC的 StorageClass 是否一致; 6.每次迭代更新最小满足 PVC requested size 的 PV,并作为最终结果返回;

    • Bind:PV 控制器对选中的 PV 、PVC 进行绑定:1. 更新 PV 的 .Spec.ClaimRef 信息为当前 PVC ; 2. 更新 PV 的 .Status.Phase 为 Bound ; 3. 新增PV 的 annotation:pv.kubernetes.io/bound-by-controller: "yes"; 4.更新 PVC 的 .Spec.VolumeName 为 PV 名称; 5.更新PVC 的 .Status.Phase 为 Bound ; 6.新增 PVC 的 annotation:pv.kubernetes.io/bound-by-controller: "yes" 和 pv.kubernetes.io/bind-completed: "yes";

    动态存储卷流程( ProvisionVolume ):若环境中没有合适的 PV,则进入动态 Provisioning 场景:

    • Before Provisioning:1.PV 控制器首先判断 PVC 使用的 StorageClass 是in-tree 还是 out-of-tree:通过查看StorageClass的Provisioner 字段是否包含**"kubernetes.io/"**前缀来判断; 2.PV 控制器更新 PVC 的 annotation:claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] = storageClass.Provisioner ;

    • in-tree Provisioning ( internal provisioning ):1.in-tree 的Provioner 会实现 ProvisionableVolumePlugin 接口的 NewProvisioner 方法,用来返回一个新的 Provisioner ; 2.PV 控制器调用Provisioner 的 Provision 函数,该函数会返回一个 PV 对象; 3.PV 控制器创建上一步返回的 PV 对象,将其与 PVC 绑定,Spec.ClaimRef 设置为 PVC,.Status.Phase 设置为 Bound,.Spec.StorageClassName设置为与 PVC 相同的 StorageClassName ;同时新增 annotation:"pv.kubernetes.io/bound-by-controller"="yes"和"pv.kubernetes.io/provisioned-by"=plugin.GetPluginName();

    • out-of-tree Provisioning ( external provisioning ):1.ExternalProvisioner检查 PVC 中的 claim.Spec.VolumeName 是否为空,不为空则直接跳过该 PVC ; 2. ExternalProvisioner检查 PVC 中的 claim.Annotations["volume.beta.kubernetes.io/storage-provisioner"] 是否等于自己的 Provisioner Name ( ExternalProvisioner在启动时会传入--provisioner 参数来确定自己的Provisioner Name ); 3.若 PVC 的VolumeMode=Block,检查ExternalProvisioner是否支持块设备; 4.ExternalProvisioner 调用Provision 函数:通过 gRPC 调用CSI 存储插件的CreateVolume接口; 5.ExternalProvisioner 创建一个 PV 来代表该 volume,同时将该 PV 与之前的 PVC 做绑定。

    deleting volumes

    Deleting 流程为 Provisioning 的反操作

    用户删除 PVC,删除 PV 控制器改变 PV.Status.Phase为Released 。

    当 PV.Status.Phase == Released 时,PV 控制器首先检查 Spec.PersistentVolumeReclaimPolicy 的值,为Retain 时直接跳过,为 Delete 时:

    • **in-tree Deleting:**1.in-tree 的Provioner 会实现DeletableVolumePlugin接口的NewDeleter方法,用来返回一个新的 Deleter ; 2.控制器调用Deleter的Delete函数,删除对应 volume ; 3. 在 volume 删除后,PV 控制器会删除 PV 对象;

    • **out-of-tree Deleting:**1.ExternalProvisioner 调用Delete 函数,通过 gRPC 调用CSI插件的DeleteVolume接口; 2.在 volume 删除后,ExternalProvisioner会删除 PV 对象

    Attaching Volumes

    Kubelet 组件和 AD 控制器都可以做 attach/detach 操作,若Kubelet的启动参数中指定了--enable-controller-attach-detach,则由Kubelet来做;否则默认由 AD 控制起来做。下面以 AD 控制器为例来讲解 attach/detach 操作。

    6.png

    AD 控制器中有两个核心变量

    • DesiredStateOfWorld ( DSW ):集群中预期的数据卷挂接状态,包含了 nodes->volumes->pods 的信息;
    • ActualStateOfWorld ( ASW ):集群中实际的数据卷挂接状态,包含了 volumes->nodes 的信息。

    Attaching 流程如下

    AD 控制器根据集群中的资源信息,初始化DSW 和ASW 。

    AD 控制器内部有三个组件周期性更新DSW 和ASW:

    • **Reconciler 。**通过一个 GoRoutine 周期性运行,确保 volume 挂接 /摘除完毕。此期间不断更新 ASW:

    in-tree attaching:1.in-tree 的 Attacher 会实现AttachableVolumePlugin接口的 NewAttacher 方法,用来返回一个新的Attacher ; 2. AD 控制器调用Attacher的Attach函数进行设备挂接; 3.更新 ASW 。

    out-of-treeattaching:1.调用 in-tree 的 CSIAttacher 创建一个**VolumeAttachement ( VA )**对象,该对象包含了 Attacher 信息、节点名称、待挂接 PV 信息; 2.ExternalAttacher 会 watch 集群中的VolumeAttachement 资源,发现有需要挂接的数据卷时,调用Attach 函数,通过 gRPC 调用CSI 插件的ControllerPublishVolume接口。

    • DesiredStateOfWorldPopulator 。通过一个GoRoutine周期性运行,主要功能是更新 DSW:

    findAndRemoveDeletedPods - 遍历所有 DSW 中的 Pods,若其已从集群中删除则从 DSW 中移除; findAndAddActivePods - 遍历所有 PodLister 中的 Pods,若 DSW 中不存在该 Pod 则添加至 DSW 。

    • **PVC Worker 。**watch PVC 的 add/update 事件,处理 PVC 相关的 Pod,并实时更新 DSW 。

    Detaching Volumes

    Detaching 流程如下

    • 当 Pod 被删除,AD 控制器会 watch 到该事件。首先 AD 控制器检查 Pod 所在的 Node 资源是否包含"volumes.kubernetes.io/keep-terminated-pod-volumes"标签,若包含则不做操作;不包含则从 DSW 中去掉该 volume ;

    • AD 控制器通过**Reconciler**使 ActualStateOfWorld 状态向 DesiredStateOfWorld 状态靠近,当发现 ASW 中有 DSW 中不存在的 volume 时,会做 Detach 操作:

    in-tree detaching:1.AD 控制器会实现 AttachableVolumePlugin 接口的 NewDetacher 方法,用来返回一个新的 Detacher ; 2. 控制器调用Detacher的Detach函数,detach 对应 volume ; 3. AD 控制器更新 ASW 。

    out-of-treedetaching:1.AD 控制器调用 in-tree 的 CSIAttacher 删除相关 VolumeAttachement 对象; 2. ExternalAttacher 会 watch 集群中的VolumeAttachement ( VA )资源,发现有需要摘除的数据卷时,调用Detach 函数,通过 gRPC 调用CSI 插件的 ControllerUnpublishVolume 接口; 3.AD 控制器更新 ASW 。

    7.png

    **Volume Manager**中同样也有两个核心变量:

    • DesiredStateOfWorld ( DSW ):集群中预期的数据卷挂载状态,包含了 volumes->pods 的信息;
    • ActualStateOfWorld ( ASW ):集群中实际的数据卷挂载状态,包含了 volumes->pods 的信息。

    Mounting/UnMounting 流程如下

    全局目录( global mount path )存在的目的:块设备在 Linux 上只能挂载一次,而在 K8s 场景中,一个 PV 可能被挂载到同一个 Node 上的多个 Pod 实例中。若块设备格式化后先挂载至 Node 上的一个临时全局目录,然后再使用 Linux 中的 bind mount 技术把这个全局目录挂载进 Pod 中对应的目录上,就可以满足要求。上述流程图中,全局目录即/var/lib/kubelet/pods/[pod uid]/volumes/kubernetes.io~iscsi/[PV name]

    VolumeManager根据集群中的资源信息,初始化DSW 和ASW 。

    VolumeManager内部有两个组件周期性更新DSW 和ASW:

    • DesiredStateOfWorldPopulator:通过一个GoRoutine周期性运行,主要功能是更新 DSW ;
    • Reconciler:通过一个 GoRoutine 周期性运行,确保 volume 挂载 /卸载完毕。此期间不断更新 ASW:

    unmountVolumes:确保 Pod 删除后 volumes 被 unmount 。遍历一遍所有 ASW 中的 Pod,若其不在 DSW 中(表示 Pod 被删除),此处以 VolumeMode=FileSystem 举例,则执行如下操作:

    1. Remove all bind-mounts:调用 Unmounter 的 TearDown 接口(若为 out-of-tree 则调用CSI 插件的NodeUnpublishVolume接口);
    2. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用CSI 插件的NodeUnstageVolume接口);
    3. 更新 ASW 。

    moutAttachVolumes:确保 Pod 要使用的 volumes 挂载成功。遍历一遍所有 DSW 中的 Pod,若其不在 ASW 中(表示目录待挂载映射到 Pod 上),此处以 VolumeMode=FileSystem 举例,执行如下操作:

    1. 等待 volume 挂接到节点上(由 External Attacher or Kubelet 本身挂接);
    2. 挂载 volume 到全局目录:调用 DeviceMounter 的 MountDevice 函数(若为 out-of-tree 则调用CSI 插件的NodeStageVolume接口);
    3. 更新 ASW:该 volume 已挂载到全局目录;
    4. bind-mount volume 到 Pod 上:调用 Mounter 的SetUp接口(若为 out-of-tree 则调用CSI 插件的NodePublishVolume接口);
    5. 更新 ASW 。

    unmountDetachDevices:确保需要 unmount的 volumes 被 unmount 。遍历一遍所有 ASW中的UnmountedVolumes,若其不在 DSW 中(表示 volume 已无需使用),执行如下操作:

    1. Unmount volume:调用 DeviceUnmounter 的 UnmountDevice 函数(若为 out-of-tree 则调用CSI 插件的 NodeUnstageVolume 接口);
    2. 更新 ASW 。

    总结

    本文先对K8s持久化存储基础概念及使用方法进行了介绍,并对K8s 内部存储流程进行了深度解析。在 K8s 上,使用任何一种存储都离不开上面的流程(有些场景不会用到 attach/detach ),环境上的存储问题也一定是其中某个环节出现了故障。

    容器存储的坑比较多,专有云环境下尤其如此。不过挑战越多,机遇也越多!目前国内专有云市场在存储领域也是群雄逐鹿,我们敏捷 PaaS 容器团队欢迎大侠的加入,一起共创未来!

    参考链接

    1. Kubernetes 社区源码
    2. [云原生公开课] Kubernetes 存储架构及插件使用(郡宝)
    3. [云原生公开课] 应用存储和持久化数据卷 - 核心知识(至天)
    4. [ kubernetes-design-proposals ] volume-provisioning
    5. [ kubernetes-design-proposals ] CSI Volume Plugins in Kubernetes Design Doc

    云原生应用团队招人啦!

    阿里云原生应用平台团队目前求贤若渴,如果你满足:

    • 对容器和基础设施相关领域的云原生技术充满热情,在相关领域如 Kubernetes 、Serverless 平台、容器网络与存储、运维平台等云原生基础设施其中某一方向有丰富的积累和突出成果(如产品落地,创新技术实现,开源贡献,领先的学术成果);

    • 优秀的表达能力,沟通能力和团队协作能力;对技术和业务有前瞻性思考;具备较强的 ownership,以结果为导向,善于决策;

    • 至少熟悉 Java 、Golang 中的一项编程语言;

    • 本科及以上学历、3 年以上工作经验。

    简历可投递至邮箱: huizhi.szh@alibaba-inc.com ,如有疑问欢迎加微信咨询:TheBeatles1994 。

    阿里巴巴云原生关注微服务、Serverless 、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

    hardysimpson1984
        1
    hardysimpson1984  
       2020-04-26 16:13:40 +08:00
    大家好,我就是惠志所在团队的 Leader,我们正在开放招聘哦,详见招聘贴: https://v2ex.com/t/659081#reply4

    也可以直接加我的微信聊

    [地址]( https://i.loli.net/2020/04/03/zWacUtgkdmHXNu5.jpg)
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2455 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 01:24 PVG 09:24 LAX 17:24 JFK 20:24
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86