ceph-csi源码分析(5)-rbd driver-nodeserver分析(上)
更多 ceph-csi 其他源码分析,请查看下面这篇博文:kubernetes ceph-csi分析目录导航
ceph-csi源码分析(5)-rbd driver-nodeserver分析(上)
当ceph-csi组件启动时指定的driver type为rbd时,会启动rbd driver相关的服务。然后再根据controllerserver、nodeserver的参数配置,决定启动ControllerServer与IdentityServer,或NodeServer与IdentityServer。
基于tag v3.0.0
https://github.com/ceph/ceph-csi/releases/tag/v3.0.0
rbd driver分析将分为4个部分,分别是服务入口分析、controllerserver分析、nodeserver分析与IdentityServer分析。

这节进行nodeserver分析,nodeserver主要包括了NodeGetCapabilities(获取driver能力)、NodeGetVolumeStats(存储探测及metrics获取)、NodeStageVolume(map rbd与mount stagingPath)、NodePublishVolume(mount targetPath)、NodeUnpublishVolume(umount targetPath)、NodeUnstageVolume(umount stagingPath与unmap rbd)、NodeExpandVolume(node端存储扩容)操作,将一一进行分析。这节进行NodeGetCapabilities(获取driver能力)、NodeGetVolumeStats(存储探测及metrics获取)、NodeExpandVolume(node端存储扩容)的分析。
nodeserver分析
(1)NodeGetCapabilities
简介
NodeGetCapabilities主要用于获取该ceph-csi driver的能力。
该方法由由kubelet调用,在kubelet调用NodeExpandVolume、NodeStageVolume、NodeUnstageVolume等方法前,会先调用NodeGetCapabilities来获取该ceph-csi driver的能力,看是否支持对这些方法的调用。
kubelet中调用的有关代码位于pkg/volume/csi/csi_client.go。
NodeGetCapabilities
NodeGetCapabilities方法中注册了csi driver的能力。
如下代码表示该csi组件支持的能力有:
(1)挂载存储到节点,把存储从节点上解除挂载;
(2)获取节点上的存储状态;
(3)存储扩容。
// ceph-csi/internal/rbd/nodeserver.go
// NodeGetCapabilities returns the supported capabilities of the node server.
func (ns *NodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
	return &csi.NodeGetCapabilitiesResponse{
		Capabilities: []*csi.NodeServiceCapability{
			{
				Type: &csi.NodeServiceCapability_Rpc{
					Rpc: &csi.NodeServiceCapability_RPC{
						Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME,
					},
				},
			},
			{
				Type: &csi.NodeServiceCapability_Rpc{
					Rpc: &csi.NodeServiceCapability_RPC{
						Type: csi.NodeServiceCapability_RPC_GET_VOLUME_STATS,
					},
				},
			},
			{
				Type: &csi.NodeServiceCapability_Rpc{
					Rpc: &csi.NodeServiceCapability_RPC{
						Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME,
					},
				},
			},
		},
	}, nil
}
(2)NodeGetVolumeStats
简介
NodeGetVolumeStats用于探测挂载存储的状态,并返回该存储的相关metrics给kubelet。
由kubelet定时循环调用,获取volume相关指标。kubelet定时调用的代码位于pkg/kubelet/server/stats/volume_stat_calculator.go-StartOnce()。
NodeGetVolumeStats
主要逻辑:
(1)获取存储挂载路径;
(2)检测存储挂载路径是否为挂载点(对比指定路径与其父目录的stat结果中的device的值,如果device值不一致,则是挂载点);
(3)通过stat获取存储挂载路径的Metrics并返回。
// internal/csi-common/nodeserver-default.go
// NodeGetVolumeStats returns volume stats.
func (ns *DefaultNodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
	// 获取存储挂载路径
	var err error
	targetPath := req.GetVolumePath()
	if targetPath == "" {
		err = fmt.Errorf("targetpath %v is empty", targetPath)
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	/*
		volID := req.GetVolumeId()
		TODO: Map the volumeID to the targetpath.
		CephFS:
		   we need secret to connect to the ceph cluster to get the volumeID from volume
		   Name, however `secret` field/option is not available  in NodeGetVolumeStats spec,
		   Below issue covers this request and once its available, we can do the validation
		   as per the spec.
		   https://github.com/container-storage-interface/spec/issues/371
		RBD:
		   Below issue covers this request for RBD and once its available, we can do the validation
		   as per the spec.
		   https://github.com/ceph/ceph-csi/issues/511
	*/
    // 检测存储挂载路径是否为mountpoint
	isMnt, err := util.IsMountPoint(targetPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, status.Errorf(codes.InvalidArgument, "targetpath %s doesnot exist", targetPath)
		}
		return nil, err
	}
	if !isMnt {
		return nil, status.Errorf(codes.InvalidArgument, "targetpath %s is not mounted", targetPath)
	}
    // 通过stat获取存储挂载路径的Metrics
	cephMetricsProvider := volume.NewMetricsStatFS(targetPath)
	volMetrics, volMetErr := cephMetricsProvider.GetMetrics()
	if volMetErr != nil {
		return nil, status.Error(codes.Internal, volMetErr.Error())
	}
	available, ok := (*(volMetrics.Available)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch available bytes"))
	}
	capacity, ok := (*(volMetrics.Capacity)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch capacity bytes"))
		return nil, status.Error(codes.Unknown, "failed to fetch capacity bytes")
	}
	used, ok := (*(volMetrics.Used)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch used bytes"))
	}
	inodes, ok := (*(volMetrics.Inodes)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch available inodes"))
		return nil, status.Error(codes.Unknown, "failed to fetch available inodes")
	}
	inodesFree, ok := (*(volMetrics.InodesFree)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch free inodes"))
	}
	inodesUsed, ok := (*(volMetrics.InodesUsed)).AsInt64()
	if !ok {
		klog.Errorf(util.Log(ctx, "failed to fetch used inodes"))
	}
	return &csi.NodeGetVolumeStatsResponse{
		Usage: []*csi.VolumeUsage{
			{
				Available: available,
				Total:     capacity,
				Used:      used,
				Unit:      csi.VolumeUsage_BYTES,
			},
			{
				Available: inodesFree,
				Total:     inodes,
				Used:      inodesUsed,
				Unit:      csi.VolumeUsage_INODES,
			},
		},
	}, nil
}
IsMountPoint
通过调用IsLikelyNotMountPoint来判断该路径是否为挂载点。
// internal/util/util.go
// IsMountPoint checks if the given path is mountpoint or not.
func IsMountPoint(p string) (bool, error) {
	dummyMount := mount.New("")
	notMnt, err := dummyMount.IsLikelyNotMountPoint(p)
	if err != nil {
		return false, status.Error(codes.Internal, err.Error())
	}
	return !notMnt, nil
}
dummyMount.IsLikelyNotMountPoint()主要逻辑:
(1)对指定路径执行stat操作;
(2)对指定路径的父目录执行stat操作;
(3)通过对比指定路径与其父目录的stat结果中的device的值,判断出该路径是否为挂载点(如果device值不一致,则是挂载点)。
// vendor/k8s.io/utils/mount/mount_linux.go
// IsLikelyNotMountPoint determines if a directory is not a mountpoint.
// It is fast but not necessarily ALWAYS correct. If the path is in fact
// a bind mount from one part of a mount to another it will not be detected.
// It also can not distinguish between mountpoints and symbolic links.
// mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b")
// will return true. When in fact /tmp/b is a mount point. If this situation
// is of interest to you, don't use this function...
func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) {
	stat, err := os.Stat(file)
	if err != nil {
		return true, err
	}
	rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/")))
	if err != nil {
		return true, err
	}
	// If the directory has a different device as parent, then it is a mountpoint.
	if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev {
		return false, nil
	}
	return true, nil
}
(3)NodeExpandVolume
简介
负责node端的存储扩容操作。主要是在node上做相应操作,将存储的扩容信息同步到node上。
NodeExpandVolume resizes rbd volumes.
实际上,存储扩容分为两大步骤,第一步是csi的ControllerExpandVolume,主要负责将底层存储扩容;第二步是csi的NodeExpandVolume,当volumemode是filesystem时,主要负责将底层rbd image的扩容信息同步到rbd/nbd device,对xfs/ext文件系统进行扩展;当volumemode是block,则不用进行node端扩容操作。
NodeExpandVolume
主体流程:
(1)校验请求参数;
(2)判断指定路径是否为挂载点;
(3)获取devicePath;
(4)调用resizefs.NewResizeFs初始化resizer;
(5)调用resizer.Resize做进一步操作。
func (ns *NodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
	volumeID := req.GetVolumeId()
	if volumeID == "" {
		return nil, status.Error(codes.InvalidArgument, "volume ID must be provided")
	}
	volumePath := req.GetVolumePath()
	if volumePath == "" {
		return nil, status.Error(codes.InvalidArgument, "volume path must be provided")
	}
	if acquired := ns.VolumeLocks.TryAcquire(volumeID); !acquired {
		klog.Errorf(util.Log(ctx, util.VolumeOperationAlreadyExistsFmt), volumeID)
		return nil, status.Errorf(codes.Aborted, util.VolumeOperationAlreadyExistsFmt, volumeID)
	}
	defer ns.VolumeLocks.Release(volumeID)
	// volumePath is targetPath for block PVC and stagingPath for filesystem.
	// check the path is mountpoint or not, if it is
	// mountpoint treat this as block PVC or else it is filesystem PVC
	// TODO remove this once ceph-csi supports CSI v1.2.0 spec
	notMnt, err := mount.IsNotMountPoint(ns.mounter, volumePath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, status.Error(codes.NotFound, err.Error())
		}
		return nil, status.Error(codes.Internal, err.Error())
	}
	if !notMnt {
		return &csi.NodeExpandVolumeResponse{}, nil
	}
	devicePath, err := getDevicePath(ctx, volumePath)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	diskMounter := &mount.SafeFormatAndMount{Interface: ns.mounter, Exec: utilexec.New()}
	// TODO check size and return success or error
	volumePath += "/" + volumeID
	resizer := resizefs.NewResizeFs(diskMounter)
	ok, err := resizer.Resize(devicePath, volumePath)
	if !ok {
		return nil, fmt.Errorf("rbd: resize failed on path %s, error: %v", req.GetVolumePath(), err)
	}
	return &csi.NodeExpandVolumeResponse{}, nil
}
resizer.Resize
根据相应的文件系统格式,调用相应的resize方法。
func (resizefs *ResizeFs) Resize(devicePath string, deviceMountPath string) (bool, error) {
	format, err := resizefs.mounter.GetDiskFormat(devicePath)
	if err != nil {
		formatErr := fmt.Errorf("ResizeFS.Resize - error checking format for device %s: %v", devicePath, err)
		return false, formatErr
	}
	// If disk has no format, there is no need to resize the disk because mkfs.*
	// by default will use whole disk anyways.
	if format == "" {
		return false, nil
	}
	klog.V(3).Infof("ResizeFS.Resize - Expanding mounted volume %s", devicePath)
	switch format {
	case "ext3", "ext4":
		return resizefs.extResize(devicePath)
	case "xfs":
		return resizefs.xfsResize(deviceMountPath)
	}
	return false, fmt.Errorf("ResizeFS.Resize - resize of format %s is not supported for device %s mounted at %s", format, devicePath, deviceMountPath)
}
xfsResize
xfs文件系统使用xfs_growfs命令。
func (resizefs *ResizeFs) xfsResize(deviceMountPath string) (bool, error) {
	args := []string{"-d", deviceMountPath}
	output, err := resizefs.mounter.Exec.Command("xfs_growfs", args...).CombinedOutput()
	if err == nil {
		klog.V(2).Infof("Device %s resized successfully", deviceMountPath)
		return true, nil
	}
	resizeError := fmt.Errorf("resize of device %s failed: %v. xfs_growfs output: %s", deviceMountPath, err, string(output))
	return false, resizeError
}
extResize
ext文件系统使用resize2fs命令。
func (resizefs *ResizeFs) extResize(devicePath string) (bool, error) {
	output, err := resizefs.mounter.Exec.Command("resize2fs", devicePath).CombinedOutput()
	if err == nil {
		klog.V(2).Infof("Device %s resized successfully", devicePath)
		return true, nil
	}
	resizeError := fmt.Errorf("resize of device %s failed: %v. resize2fs output: %s", devicePath, err, string(output))
	return false, resizeError
}
rbd driver-nodeserver分析(上)-小结
这节分析了NodeGetCapabilities、NodeGetVolumeStats、NodeExpandVolume方法,作用分别如下:
NodeGetCapabilities:获取ceph-csi driver的能力。
NodeGetVolumeStats:探测挂载存储的状态,并返回该存储的相关metrics给kubelet。
NodeExpandVolume:在node上做相应操作,将存储的扩容信息同步到node上。
ceph-csi源码分析(5)-rbd driver-nodeserver分析(上)的更多相关文章
- [源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上)
		[源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上) 目录 [源码解析] PyTorch 分布式 Autograd (5) ---- 引擎(上) 0x00 摘要 0x0 ... 
- Apache Spark源码走读之6 -- 存储子系统分析
		欢迎转载,转载请注明出处,徽沪一郎. 楔子 Spark计算速度远胜于Hadoop的原因之一就在于中间结果是缓存在内存而不是直接写入到disk,本文尝试分析Spark中存储子系统的构成,并以数据写入和数 ... 
- Java集合源码学习(四)HashMap分析
		ArrayList.LinkedList和HashMap的源码是一起看的,横向对比吧,感觉对这三种数据结构的理解加深了很多. >>数组.链表和哈希表结构 数据结构中有数组和链表来实现对数据 ... 
- Java集合源码学习(三)LinkedList分析
		前面学习了ArrayList的源码,数组是顺序存储结构,存储区间是连续的,占用内存严重,故空间复杂度很大.但数组的二分查找时间复杂度小,为O(1),数组的特点是寻址容易,插入和删除困难.今天学习另外的 ... 
- Java集合源码学习(二)ArrayList分析
		>>关于ArrayList ArrayList直接继承AbstractList,实现了List. RandomAccess.Cloneable.Serializable接口,为什么叫&qu ... 
- 十大基础排序算法[java源码+动静双图解析+性能分析]
		一.概述 作为一个合格的程序员,算法是必备技能,特此总结十大基础排序算法.java版源码实现,强烈推荐<算法第四版>非常适合入手,所有算法网上可以找到源码下载. PS:本文讲解算法分三步: ... 
- Java源码详解系列(十)--全面分析mybatis的使用、源码和代码生成器(总计5篇博客)
		简介 Mybatis 是一个持久层框架,它对 JDBC 进行了高级封装,使我们的代码中不会出现任何的 JDBC 代码,另外,它还通过 xml 或注解的方式将 sql 从 DAO/Repository ... 
- Mybatis源码学习第七天(PageHelper分析)
		其实我本来是不打算写这个PageHelper的,但是后来想了想,还是写了吧!现在市场用Mybatis的产品分页应该差不多都是用PageHelper来实现的,毕竟Mybatis的分页rowbound.. ... 
- 【源码】Redis exists命令bug分析
		本文基于社区版Redis 4.0.8 1.复现条件 版本:社区版Redis 4.0.10以下版本 使用场景:开启读写分离的主从架构或者集群架构(master只负责写流量,slave负责读流量) 案例: ... 
- 【 js 基础 】【 源码学习 】源码设计 (更新了backbone分析)
		学习源码,除了学习对一些方法的更加聪明的代码实现,同时也要学习源码的设计,把握整体的架构.(推荐对源码有一定熟悉了之后,再看这篇文章) 目录结构:第一部分:zepto 设计分析 第二部分:unders ... 
随机推荐
- 浅尝js垃圾回收机制
			局部作用域内的变量,在函数执行结束之后就会被js的垃圾回收机制销毁 为什么要销毁局部变量? => 为了释放内存 js垃圾回收机制何时会销毁局部变量 : 如果局部变量无法再得到访问,就会被 ... 
- top命令查看CPU状态信息:%us、%sy、%ni、%id、%wa、%hi、%si、%st 表示的是什么意思
			Linux CPU负载状态:%us/%sy/%ni/%id/%wa/%hi/%si/%st含义 2018-08-26 分类:Linux 评论(0) 缙哥哥发现用了雅黑的探针,在 Linux 的 C ... 
- 性能工具 stream 最新版本5.10 The STREAM benchmark
			官网下载最新性能工具 stream 最新版本5.10 https://github.com/jeffhammond/STREAM 官网下载最新性能工具 stream 最新版本5.10 http:/ ... 
- Docker——Jenkins + Git + Registry构建自动化持续集成环境(CI/CD)
			前言 在互联网时代,对于每一家公司,软件开发和发布的重要性不言而喻,目前已经形成一套标准的流程,最重要的组成部分就是持续集成(CI)及持续部署.交付(CD). 本文基于Jenkins+Docker+G ... 
- SSL数字证书颁发
			一.数字证书 1.数字证书实际上是存在于计算机上的一个记录,是由CA签发的一个声明,证明证书主体("证书申请者"拥有了证书后即成为"证书主体")与证书中所包含的 ... 
- shell初学之PHP
			初次接触脚本,写了一个通过Apache实现PHP动态网站的脚本: #!/bin/bash yum -y install php rm -rf /etc/httpd/conf.d/welcome.con ... 
- python3 读取txt文件数据,绘制趋势图,matplotlib模块
			python3 读取txt文件数据,绘制趋势图 test1.txt内容如下: 时间/min cpu使用率/% 内存使用率/% 01/12-17:06 0.01 7.61 01/12-17:07 0.0 ... 
- 关于步进电机驱动板,tb6560
			参考的,淘宝上买来的步进电机S曲线驱动方法,发现 他程序输出的PWM波形全是方波, 占空比为50% 而且他 修改这两个数来输出波形,所以 我打算参考这个来写一个驱动 TIMX_CNT中放置的是当前计 ... 
- STM32F1移植UCOSII
			作者:珵旭媛 下载对应版本的UCOSII https://www.micrium.com/downloadcenter/,你会少修改很多东西: 下载下来后是这样的文件夹,并且Software里面的才是 ... 
- CSS的引入方式和复合选择器
			CSS的引入方式 样式表 优点 缺点 范围 行内样式表 书写方便 结构样式混写 控制一个标签 内部样式表 部分结构和样式相分离 没有彻底 控制一个页面 外部样式表 完全实现结构和样式分离 需要引入 控 ... 
