当先锋百科网

首页 1 2 3 4 5 6 7

本文由 CNCF + Alibaba 云原生技术公开课 整理而来

Kubernetes 存储架构

  • 在 Kubernetes 中挂载一个 Volume
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-disk
spec:
  storageClassName: alicloud-disk-ssd
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
      
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: nginx
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: disk-pvc
          mountPath: /data
      volumes:
      - name: disk-pvc
        persistentVolumeClaim:
          claimName: pvc-disk

上面 yaml 文件中定义了 StatefulSet 的一个应用,其中定义了一个名为 disk-pvc 的 volume,挂载到 Pod 内部的目录是 /data。disk-pvc 是一个 PVC 类型的数据卷,其中定义了一个 storageClassName alicloud-disk-ssd。当 StatefulSet web 被创建之后,由 alicloud-disk-ssd 这个 StorageClass 自动创建 PVPVC disk-pvc 绑定。

Kubernetes 挂载 Volume 的过程如下:

第一步:用户创建一个 包含 PVC 的 Pod;

第二步:PV Controller 会不断观察 ApiServer,如果它发现一个 PVC 已经创建完毕但仍然是未绑定的状态,它就会试图把一个 PV 和 PVC 绑定。
 
        PV Controller 首先会在集群内部找到一个适合的 PV 进行绑定,如果未找到相应的 PV,就调用 Volume Plugin 去做 Provision。
        Provision 就是从远端上一个具体的存储介质创建一个 Volume,并且在集群中创建一个 PV 对象,然后将此 PV 和 PVC 进行绑定;

第三步:通过 Scheduler 完成一个调度功能。
 
        当一个 Pod 运行的时候,需要选择一个 Node,这个 Node 的选择就是由 Scheduler 来完成的。Scheduler 进行调度的时候会有多个参考量,
        比如 Pod 内部所定义的 nodeSelector、nodeAffinity 这些定义以及 Volume 中所定义的一些标签等。

        因此可以在数据卷中添加一些标准,这样使用这个 PV 的 Pod 就会由于标签的限制,被调度器调度到期望的节点上;

第四步:如果有一个 Pod 调度到某个节点之后,它所定义的 PV 还没有被挂载(Attach),此时 AD Controller 就会调用 VolumePlugin,
        把远端的 Volume 挂载到目标节点中的设备上(如:/dev/vdb);

第五步:当 Volum Manager 发现一个 Pod 调度到自己的节点上并且 Volume 已经完成了挂载,它就会执行 mount 操作,将本地设备
        (也就是刚才得到的 /dev/vdb)挂载到 Pod 在节点上的一个子目录中。同时它也可能会做一些像格式化、是否挂载到 GlobalPath 等这样的附加操作。
 
第六步就:绑定操作,就是将已经挂载到本地的 Volume 映射到容器中。
  • Kubernetes 的存储架构:

在这里插入图片描述

  • PV Controller

PV:持久化存储卷,详细定义了预挂载存储空间的各项参数。

PVC:持久化存储声明。它是用户所使用的存储接口,对存储细节无感知,主要是定义一些基本存储的 Size、AccessMode 这些参数在里面,并且它是属于某个 NameSpace 内部的。

StorageClass:存储类。一个动态存储卷会按照 StorageClass 所定义的模板来创建一个 PV,其中定义了创建模板所需要的一些参数和创建 PV 的一个 Provisioner(就是由谁去创建的)。

PV Controller 的主要任务就是完成 PVPVC 的生命周期管理,比如创建、删除 PV 对象,负责 PVPVC 的状态迁移;另一个任务就是绑定 PVCPV 对象,一个 PVC 必须和一个 PV 绑定后才能被应用使用,它们是一一绑定的,一个 PV 只能被一个 PVC 绑定,反之亦然。

一个 PV 的状态迁移图:

在这里插入图片描述

创建好一个 PV 以后,PV 就处于一个 Available 的状态,当一个 PVC 和一个 PV 绑定的时候,这个 PV 就进入了 Bound 的状态,此时如果把 PVC 删掉,Bound 状态的 PV 就会进入 Released 的状态。

一个 Released 状态的 PV 会根据自己定义的 ReclaimPolicy 字段来决定自己是进入一个 Available 的状态还是进入一个 Deleted 的状态。如果 ReclaimPolicy 定义的是 recycle 类型,它会进入一个 Available 状态,如果转变失败,就会进入 Failed 的状态。

一个 PVC 的状态迁移图:

在这里插入图片描述

一个创建好的 PVC 会处于 Pending 状态,当一个 PVCPV 绑定之后,PVC 就会进入 Bound 的状态,当一个 Bound 状态的 PVCPV 被删掉之后,该 PVC 就会进入一个 Lost 的状态。对于一个 Lost 状态的 PVC,它的 PV 如果又被重新创建,并且重新与该 PVC 绑定之后,该 PVC 就会重新回到 Bound 状态。

一个 PVC 去绑定 PV 时会对 PV 进行筛选,筛选流程(从上至下)如下:

VolumeMode 检查:筛选所有具有相同 VolumeMode 的 Volume:Block、FileSystem;

LabelSelector 检查:如果 PVC 配置了 LabelSelector,会筛选符合条件的 Volume;

StorageClassName 检查:如果 PVC 配置了 StorageClassName,会筛选相同名称的 Volume;

AccessMode 检查:筛选 PVC 的 AccessMode 列表中所有选项,都有相应的 AccessMode 配置的 Volume;

Size 检查:筛选哪些 大于等于 PVC 的 Size 的 Volume,若 Volume 的数量大于 1,则筛选符合条件的 Size 最小 的 Volume。
  • AD Controller

AD ControllerAttach/Detach Controller 的一个简称。它有两个核心对象,即 DesiredStateofWorldActualStateOfWorldDesiredStateofWorld 是集群中预期要达到的数据卷的挂载状态;ActualStateOfWorld 则是集群内部实际存在的数据卷挂载状态。

AD Controller 有两个核心逻辑:

desiredStateOfWorldPopulator    主要是用来同步集群的一些数据以及 DSW、ASW 数据的更新,比如创建一个新的 PVC、创建一个新的 Pod 的时候,会把集群里面这些数据的状态同步到 DesiredStateofWorld 中;
 
Reconcile   根据 DesiredStateofWorld 和 ActualStateOfWorld 对象的状态做状态同步。它会把 ActualStateOfWorld 状态变成 DesiredStateofWorld 状态,在这个状态的转变过程中,它会去执行 Attach、Detach 等操作。

AD Controller 中有很多 InformerInformer 会把集群中的 Pod 状态、PV 状态、Node 状态、PVC 状态同步到本地。

在初始化的时候会调用 populateDesireStateofWorld 以及 populateActualStateofWorlddesireStateofWorldactualStateofWorld 两个对象进行初始化。

在执行的时候,通过 desiredStateOfWorldPopulator 进行数据同步,即把集群中的数据状态同步到 desireStateofWorld 中。reconciler 则通过轮询的方式把 actualStateofWorlddesireStateofWorld 这两个对象进行数据同步,在同步的时候,会通过调用 Volume Plugin 进行 attachdetach 操作,同时它也会调用 nodeStatusUpdaterNode 的状态进行更新。

  • Volume Manager

Volume Manager 实际上是 Kubelet 中一部分,是 Kubelet 中众多 Manager 的一个。它主要是用来做本节点 VolumeAttach/Detach/Mount/Unmount 操作。

它和 AD Controller 一样包含有 desireStateofWorld 以及 actualStateofWorld,同时还有一个 volumePluginManager 对象,主要进行节点上插件的管理。在核心逻辑上和 AD Controller 也类似,通过 desiredStateOfWorldPopulator 进行数据的同步以及通过 Reconciler 进行接口的调用。

AD Controller 也会做 Attach/Detach 操作,所以到底是由谁来做呢?可以通过 --enable-controller-attach-detach 标签进行定义,如果它为 True,则由 AD Controller 来控制;若为 False,就由 Volume Manager 来做。

--enable-controller-attach-detachKubelet 的一个标签,只能定义某个节点的行为。如果一个有 10 个节点的集群,它有 5 个节点定义该标签为 False,说明这 5 个节点是由节点上的 Kubelet 来做挂载,而另外 5 个节点是由 AD Controller 来做挂载。

  • Volume Plugins

前面提到的 PV ControllerAD Controller 以及 Volume Manager 其实都是通过调用 Volume Plugin 提供的接口,比如 ProvisionDeleteAttachDetach 等去做一些 PVPVC 的管理。而这些接口的具体实现逻辑是放在 VolumePlugin 中的。

根据源码的位置可将 Volume Plugins 分为 In-TreeOut-of-Tree 两类:

In-Tree     表示源码是放在 Kubernetes 内部的,和 Kubernetes 一起发布、管理与迭代,缺点及时迭代速度慢、灵活性差;

Out-of-Tree     Volume Plugins 的代码独立于 Kubernetes,它是由存储商提供实现的,目前主要有 Flexvolume 和 CSI 两种实现机制,可以根据存储类型实现不同的存储插件。

Kubernetes 会在 PV ControllerAD Controller 以及 Volume Manager 中来做插件管理。通过 VolumePlguinMg 对象进行管理。主要包含 PluginsProber 两个数据结构。

Plugins 主要是用来保存 Plugins 列表的一个对象,而 Prober 是一个探针,用于发现新的 Plugin,比如 FlexvolumeCSI 是扩展的一种插件,它们是动态创建和生成的,一开始无法预知,因此需要一个探针来发现新的 Plugin。

PV ControllerAD Controller 以及 Volume Manager 在启动的时候会执行一个 InitPlugins 方法来对 VolumePluginsMgr 做一些初始化。

它首先会将所有 In-TreePlugins 加入到插件列表中,同时会调用 Proberinit 方法,该方法会首先调用一个 InitWatcher,它会时刻观察着某一个目录(比如 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/),当这个目录每生成一个新文件的时候,也就是创建了一个新的 Plugins,此时就会生成一个新的 FsNotify.Create 事件,并将其加入到 EventsMap 中;同理,如果删除了一个文件,就生成一个 FsNotify.Remove 事件加入到 EventsMap 中。

当上层调用 refreshProbedPlugins 时,Prober 就会把这些事件进行一个更新,如果是 Create,就将其添加到插件列表;如果是 Remove,就从插件列表中删除一个插件。


Flexvolume 介绍及使用

FlexvolumeVolume Plugins 的一个扩展,主要实现 Attach/Detach/Mount/Unmount 这些接口。这些功能本来是由 Volume Plugins 实现的,但是对于某些存储类型,需要将其扩展到 Volume Plugins 以外,所以需要把接口的具体实现放到外面。

Flexvolume 是可被 Kubelet 驱动的可执行文件,每一次调用相当于执行一次 ls 这样的 shell 命令,都是可执行文件的命令行调用,因此它不是一个常驻内存的守护进程。

FlexvolumeStdout 作为 Kubelet 调用的返回结果,这个结果需要是 JSON 格式。Flexvolume 默认的存放地址为 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/alicloud~disk/disk

在这里插入图片描述

  • Flexvolume 的接口介绍:

Flexvolum 包含以下接口:

init:主要做一些初始化的操作,比如部署插件、更新插件的时候做 init 操作,返回的时候会返回 DriveCapabilities 类型的数据结构,用来说明 Flexvolume 插件有哪些功能

GetVolumeName:返回插件名

Attach: 挂载功能的实现。根据 --enable-controller-attach-detach 标签来决定是由 AD Controller 还是 Kubelet 来进行挂载操作

WaitforAttach:Attach 经常是异步操作,因此需要等待挂载完成,才能需要进行下面的操作

MountDevice:它是 mount 的一部分。这里将 mount 分为 MountDevice 和 SetUp 两部分,MountDevice 主要做一些简单的预处理工作,比如将设备格式化、挂载到 GlobalMount 目录中等

GetPath:获取每个 Pod 对应的本地挂载目录

Setup:使用 Bind 方式将 GlobalPath 中的设备挂载到 Pod 的本地目录

TearDown、UnmountDevice、Detach:实现的是上面一些借口的逆过程

ExpandVolumeDevice:扩容存储卷,由 Expand Controller 发起调用

NodeExpand:扩容文件系统,由 Kubelet 发起调用
  • Flexvolume 的挂载:

在这里插入图片描述

从挂载流程和卸载流程两个方向来分析 Flexvolume 的挂载过程:

挂载流程:
    1. Attach 操作,调用一个远端的 API 把 Storage 挂载到目标节点中的某个设备上去

    2. MountDevice 操作,将本地设备挂载到 GlobalPath 中,同时也会做一些格式化这样的操作

    3. Mount 操作,它会把 GlobalPath 挂载 PodPath 中,PodPath 就是 Pod 启动时所映射的一个目录
    
卸载流程:
    1. Unmount 操作,把 GlobalPath 从 PodPath 中卸载
    
    2. UnmountDevice 操作,从 GlobalPath 中卸载本地设备
    
    3. Detach 操作,调用一个远端的 API 把 Storage 从目标节点中的某个设备上卸载

卸载流程就是挂载流程的逆过程。对于块设备的挂载过程,需要 AttachMountDeviceMount 操作;而对于文件存储类型,就无需 AttachMountDevice 操作,只需要 Mount 操作,因此文件系统的 Flexvolume 实现较为简单,只需要 MountUnmount 过程即可。

  • Flexvolume 的使用:

Flexvolume 中定义了 driverfsTypeoptions

driver 定义的是实现的某种驱动,比如 aliclound/disk,也可以是 aliclound/nas 等

fsType 定义的是文件系统类型,比如 "ext4"

options 包含了一些具体的参数,比如定义云盘的 id 等

PVPVC 示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-disk
  labels:
    failure-domain.beta.kubernetes.io/zone: cn-hangzhou-a
    failure-domain.beta.kubernetes.io/rehion: cn-hangzhou
    pvname: pv-disk
spec:
  capacity:
    storage: 20Gi
  accessModes:
  - ReadWriteOnce
  flexVolume:
    driver: "alicloud/disk"
    fsType: "ext4"
    options:
      volumeId: "d-wz9bkj7jhbb4j077n1tz"
      
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-disk
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  selector:
    matchLabels:
      pvname: pv-disk

SCPVC 示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-disk-ssd-hangzhou-a
provisioner: alicloud/disk
reclaimPolicy: Retain
parameters:
  type: cloud_ssd
  region: cn-hangzhou
  zoneid: cn-hangzhou-a
  encrypted: "true"
      
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: disk-ssd1
spec:
  storageClassName: alicloud-disk-ssd-hangzhou-a
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

CSI 介绍及使用

Flexvolume 类似,CSI 也是为第三方存储提供数据卷实现的抽象接口。有了 Flexvolume,为何还要 CSI 呢?

Flexvolume 只是给 kubernetes 这一个编排系统来使用的,而 CSI 可以满足不同编排系统的需求,比如 MesosSwarm

其次 CSI 是容器化部署,可以减少环境依赖,增强安全性,丰富插件的功能。Flexvolume 是在 host 空间一个二进制文件,执行 Flexvolum 时相当于执行本地的一个 shell 命令,这使得在安装 Flexvolume 时需要同时安装某些依赖,而这些依赖可能会对客户的应用产生一些影响,因此在安全性上、环境依赖上会有不好的影响。

CSI 主要包含两个部分:CSI Controller ServerCSI Node Server

Controller Server 是控制端的功能,主要实现创建、删除、挂载、卸载等功能;

Node Server 主要实现的是节点上的 Mount、Unmount 功能。
  • CSI 的接口介绍:

CSI 的接口主要分为 3 类:通用管控接口、节点管控接口、中心管控接口。

通用管控接口主要返回 CSI 的一些通用信息,像插件的名字、Driver 的身份信息、插件所提供的能力等

节点管控接口的 NodeStageVolume 和 NodeUnstageVolume 就相当于 Flexvolume 中的 MountDevice 和 UnmountDevice,
            NodePublishVolume 和 NodeUnpublishVolume 就相当于 SetUp 和 TearDown 接口

中心管控接口的 CreateVolume 和 DeleteVolume 就是 Provision 和 Delete 存储卷的一个接口,
            ControllerPublishVolume 和 ControllerUnPublishVolume 则分别是 Attach 和 Detach 的接口。 

在这里插入图片描述

  • CSI 的系统结构:

CSI 是通过 CRD 的形式实现的,所以 CSI 引入了这么几个对象类型:VolumeAttachmentCSINodeCSIDriver 以及 CSI Controller ServerCSI Node Server 的一个实现。

在这里插入图片描述

CSI Controller Server 中,有传统的类似 Kubernetes 中的 AD ControllerVolume PluginsVolumeAttachment 对象就是由它们所创建的。此外,还包含多个 External Plugin 组件,每个组件和 CSI Plugin 组合的时候会完成某种功能。例如:

External Provisioner 和 Controller Server 组合的时候就会完成数据卷的创建与删除功能

External Attacher 和 Controller Server 组合起来可以执行数据卷的挂载和操作

External Resizer 和 Controller Server 组合起来可以执行数据卷的扩容操作

External Snapshotter 和 Controller Server 组合则可以完成快照的创建和删除

CSI Node Server 中主要包含 Kubelet 组件,包括 VolumeManagerVolumePlugin,它们会去调用 CSI Plugin 去做 MountUnmount 操作;另外一个组件 Driver Registrar 主要实现的是 CSI Plugin 注册的功能。

  • CSI 对象:

主要介绍 3 种对象:VolumeAttachmentCSIDriverCSINode

第二个对象是 VolumeAttachmentVolumeAttachment 描述一个 VolumePod 使用中挂载、卸载的相关信息。例如,对一个 Volume 在某个节点上的挂载,通过 VolumeAttachment 对该挂载进行跟踪。AD Controller 创建一个 VolumeAttachment,而 External-attacher 则通过观察该 VolumeAttachment,根据其状态来进行挂载和卸载操作。

第二个对象是 CSIDriver,它描述了集群中所部署的 CSI Plugin 列表,需要管理员根据插件类型进行创建。在 CSI Driver 中定义了它的名字,在 spec 中还定义了 attachRequiredpodInfoOnMount 两个标签。

attachRequired 定义一个 Plugin 是否支持 Attach 功能,主要是为了对块存储和文件存储做区分。比如文件存储不需要 Attach 操作,因此将该标签定义为 False

podInfoOnMount 则是定义 Kubernetes 在调用 Mount 接口时是否带上 Pod 信息

第三个对象是 CSINode,它是集群中的节点信息,由 node-driver-registrar 在启动时创建。它的作用是每一个新的 CSI Plugin 注册后,都会在 CSINode 列表里添加一个 CSINode 信息。

  • CSI 组件之 Node-Driver-Registrar

Node-Driver-Registrar 主要实现了 CSI Plugin 注册机制。实现过程如下:

第一步:在启动的时候有一个约定,比如说在 /var/lib/kuberlet/plugins_registry 这个目录每新加一个文件,就相当于新加了一个 Plugin。
        启动 Node-Driver-Registrar,它首先会向 CSI-Plugin 发起一个接口调用 GetPluginInfo,这个接口会返回 CSI 所监听的地址以及 CSI-Plugin 的一个 Driver name;

第二步,Node-Driver-Registrar 会监听 GetInfo 和 NotifyRegistrationStatus 两个接口;
 
第三步,会在 /var/lib/kuberlet/plugins_registry 目录下启动一个 Socket,生成一个 Socket 文件 ,如 diskplugin.csi.alibabacloud.com-reg.sock,
        此时 Kubelet 通过 Watcher 发现这个 Socket 后,它会通过该 Socket 向 Node-Driver-Registrar 的 GetInfo 接口进行调用。
        GetInfo 会把刚获得的的 CSI-Plugin 的信息返回给 Kubelet,该信息包含了 CSI-Plugin 的监听地址以及它的 Driver name;

第四步,Kubelet 通过得到的监听地址对 CSI-Plugin 的 NodeGetInfo 接口进行调用;

第五步,调用成功之后,Kubelet 会去更新一些状态信息,比如节点的 Annotations、Labels、status.allocatable 等信息,同时会创建一个 CSINode 对象;

第六步,通过对 Node-Driver-Registrar 的 NotifyRegistrationStatus 接口的调用告诉它已经把 CSI-Plugin 注册成功了。
  • CSI 组件之 External-Attacher

在这里插入图片描述

External-Attacher 主要是通过 CSI Plugin 的接口来实现数据卷的挂载与卸载功能,它通过观察 VolumeAttachment 对象来实现状态的判断。VolumeAttachment 对象则是通过 AD Controller 来调用 Volume Plugin 中的 CSI Attacher 来创建的。CSI Attacher 是一个 In-Tree 类,也就是说这部分是 Kubernetes 完成的。

VolumeAttachment 的状态是 False 时,External-Attacher 就去调用底层的一个 Attach 功能;若期望值为 False,就通过底层的 ControllerPublishVolume 接口实现 Detach 功能。同时,External-Attacher 也会同步一些 PV 的信息在里面。

  • CSI 部署:

CSIController 分为两部分,一个是 Controller Server Pod,一个是 Node Server Pod

一般只需要部署一个 Controller Server,如果是多备份的,可以部署两个。Controller Server 主要是通过多个外部插件来实现的,比如说一个 Pod 中可以定义多个 ExternalContainer 和一个包含 CSI Controller ServerContainer,此时不同的 External 组件会和 Controller Server 组成不同的功能。

Node Server Pod 是个 DaemonSet,它会在每个节点上进行注册。Kubelet 会直接通过 Socket 的方式直接和 CSI Node Server 进行通信、调用 Attach/Detach/Mount/Unmount 等。

Driver Registrar 只是做一个注册的功能,会在每个节点上进行部署。

在这里插入图片描述

在这里插入图片描述

  • CSI 使用:

CSI 中定义了 drivervolumeHandlevolumeAttributenodeAffinity

driver 就是定义是由哪一个插件来去实现挂载

volumeHandle 主要是指示 PV 的唯一标签

volumeAttribute 用于附加参数,比如 PV 如果定义的是 OSS,那么就可以在 volumeAttribute 定义 bucket、访问的地址等信息在里面

nodeAffinity 则可以定义一些调度信息。与 Flexvolume 类似,还可以通过 selector 和 Label 定义一些绑定条件

PVPVC 示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: csi-pv
  labels:
    alicloud-pvname: csi-pv
spec:
  capacity:
    storage: 25Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  csi:
    driver: diskplugin.csi.alibabacloud.com
    volumeHandle: d-wz9bkj7jhbb4jo77n1tz
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: topology.diskplugin.csi.alibabacloud.com/zone
          operater: In
          values:
          - cn-hangzhou-a
      
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-disk
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 25Gi
  selector:
    matchLabels:
      alicloud-pvname: csi-pv

SCPVC 示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-disk-ssd-hangzhou-a
provisioner: diskplugin.csi.alibabacloud.com
reclaimPolicy: Delete
parameters:
  type: cloud_ssd
  region: cn-hangzhou
  zoneid: cn-hangzhou-a
      
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: disk-ssd1
spec:
  storageClassName: csi-disk-ssd-hangzhou-a
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 25Gi