记录一个拷贝文件到GlusterFS卡住的解决过程

本文解决了一个在GlusterFS上拷贝文件时出现的卡顿问题,通过调整性能参数performance.flush-behind从off改为on,最终解决了线上环境和高仿环境中文件拷贝卡住的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题简介

我们有一个分布式服务,存储为Gluster FS,需要大量的读写文件。在公司开发环境、测试环境都正常的情况下,在线上环境、高仿环境却屡屡出现拷贝文件到Gluster FS卡住的问题(文件若是200M~5G大小,概率大概在3~4%左右,文件已拷贝完成,源文件和目标文件md5一致,卡在目标文件句柄close处。)。

func CopyFile(src, dest string) (copiedSize int64, err error) {
    copiedSize = 0
    srcFile, err := os.Open(src)
    if err != nil {
        return copiedSize, err
    }
    defer srcFile.Close()

    destFile, err := os.Create(dest)
    if err != nil {
        return copiedSize, err
    }
    defer destFile.Close() // 卡在这
    return io.Copy(destFile, srcFile)
}

卡住的goroutine信息示例:

goroutine 109667 [syscall, 711 minutes]:
syscall.Syscall(0x3, 0xf, 0x0, 0x0, 0xafb1a0, 0xc42000c150, 0x0)
    /usr/local/go/src/syscall/asm_linux_amd64.s:18 +0x5
syscall.Close(0xf, 0x0, 0x0)
    /usr/local/go/src/syscall/zsyscall_linux_amd64.go:296 +0x4a
os.(*file).close(0xc420344f00, 0x455550, 0xc4203696d0)
    /usr/local/go/src/os/file_unix.go:140 +0x86
os.(*File).Close(0xc4200289f0, 0x1b6, 0xc4200289f0)
    /usr/local/go/src/os/file_unix.go:132 +0x33
Common/utils.CopyFile(0xc42031eea0, 0x5d, 0xc420314840, 0x36, 0x10ce9d94, 0x0, 0x0)
......

最后的/usr/local/go/src/syscall/asm_linux_amd64.s第18行前后代码如下

TEXT    ·Syscall(SB),NOSPLIT,$0-56
    CALL    runtime·entersyscall(SB)   // 卡在系统调用开始处
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    $0, R10
    MOVQ    $0, R8
    MOVQ    $0, R9
    MOVQ    trap+0(FP), AX  // syscall entry
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS ok
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX
    MOVQ    AX, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET

解决过程

由于开发环境、测试环境用的Gluster FS是3.3.2,线上环境、高仿环境的Gluster FS版本是3.7.6,最开始是猜测可能是版本不一致导致的问题。因此最开始是从Gluster FS版本是否有问题,部署GlusterFS的软硬件是否有问题开始入手,
但始终找不出真正的原因。这时候公司流程就成了阻碍,因为原因基本靠经验猜测,但即便改一点点代码,都要提测,找多个领导签字,最坏的一次情况是一天都没走完一个流程。
最后,实在无奈,向领导申请操作部分高仿环境的权限。好了,终于可以施展拳脚了。

第一次,采用超时处理机制

我们想到的是,参考tensorflow的源码,在Golang中用reflect实现一个类似 ./tensorflow/core/platform/cloud/retrying_utils.cc的代码。基本原理就是,close等一些可能会卡住的函数是新启一个goroutine来做,如果在close阶段卡住,超过一定时间继续往下走,反正文件都已经拷贝完了。
主要代码如下:

type RetryingUtils struct {
    Timeout    time.Duration
    MaxRetries int
}

type CallReturn struct {
    Error        error
    ReturnValues []reflect.Value
}

func NewRetryingUtils(timeout time.Duration, maxRetries int) *RetryingUtils {
    return &RetryingUtils{Timeout: timeout, MaxRetries: maxRetries}
}

func (r *RetryingUtils) CallWithRetries(any interface{}, args ...interface{}) CallReturn {
    var callReturn CallReturn
    var retries int
    for {
        callReturn.Error = nil
        done := make(chan int, 1)
        go func() {
            function := reflect.ValueOf(any)
            inputs := make([]reflect.Value, len(args))
            for i, _ := range args {
                inputs[i] = reflect.ValueOf(args[i])
            }
            callReturn.ReturnValues = function.Call(inputs)
            done <- 1
        }()
        select {
        case <-done:
            return callReturn
        case <-time.After(r.Timeout):
            callReturn.Error = errTimeout
        }
        retries++
        if retries >= r.MaxRetries {
            break
        }
    }
    return callReturn
}

调用方式示例:

NewRetryingUtils(time.Second*10, 1).CallWithRetries(fd.Close)

压测两天,此方法不可行。因为卡住之后,任务goroutine继续往下走,会有概率导致进程为defunct。(注:无数的文章告诉我们,defunct僵尸进程的产生是没有处理子进程退出信息之类的,这个只是僵尸进程的一部分;比如只要goroutine卡住,若kill该进程,进程就会成为defunct)

第二次,采用系统命令拷贝文件

我们采用linux的cp命令拷贝文件,压测两天,通过。(它为什么成功的原因,还没完全整明白。因为每次压测需要占用两位测试美女的大量时间,反反复复地压测,也不太好。如果是开发环境或测试环境,那好办,我们开发可以自己压测)

第三次,测试是否由于多申请了Read权限引起

我们开始通过阅读linux cp命令的源码来寻找原因。发现coreutils/src/copy.c的copy_reg函数,这个函数的功能是拷贝普通文件。golang的os.Create函数默认比该函数多申请了一个read权限。
copy_reg中的创建文件:

int open_flags =
    O_WRONLY | O_BINARY | (x->data_copy_required ? O_TRUNC : 0);
dest_desc = open (dst_name, open_flags);

golang中的创建文件:

func Create(name string) (*File, error) {
    return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

由于以前我做远程进程注入的时候,知道申请不必要的权限,会导致失败率增加。因此,猜测可能是此原因。测试结果,很遗憾,不是。

第四次,在目标文件句柄关闭之前,显式调用Sync函数

如下所示:

func CopyFile(src, dest string) (copiedSize int64, err error) {
    copiedSize = 0
    srcFile, err := os.Open(src)
    if err != nil {
        return copiedSize, err
    }
    defer srcFile.Close()

    destFile, err := os.Create(dest)
    if err != nil {
        return copiedSize, err
    }
    defer func() {
        destFile.Sync()  // 卡在这
        destFile.Close()
    }
    return io.Copy(destFile, srcFile)
}

卡住的goroutine示例:

goroutine 51634 [syscall, 523 minutes]:
syscall.Syscall(0x4a, 0xd, 0x0, 0x0, 0xafb1a0, 0xc42000c160, 0x0)
    /usr/local/go/src/syscall/asm_linux_amd64.s:18 +0x5
syscall.Fsync(0xd, 0x0, 0x0)
    /usr/local/go/src/syscall/zsyscall_linux_amd64.go:492 +0x4a
os.(*File).Sync(0xc420168a00, 0xc4201689f8, 0xc420240000)
    /usr/local/go/src/os/file_posix.go:121 +0x3e
Common/utils.CopyFile.func3()

越来越接近真相了,激动。想必原因是:write函数只是将数据写到缓存,并没有实际写到磁盘(这里是GlusterFS)。由于网络或其它原因,导致最后Sync()卡住。

真相

我们找到了这篇文章https://lists.gnu.org/archive/html/gluster-devel/2011-09/msg00005.html

这时运维查了下各个环境的GlusterFS配置,跟我们说,线上环境和高仿环境的GlusterFS的配置项performance.flush-behind为off,开发环境和测试环境是on。智者千虑,必有一失。严谨的运维们,偶尔疏忽实属正常。当然最开心的是终于解决了问题。

gluster> volume info

Volume Name: pre-volume
Type: Striped-Replicate
Volume ID: 3b018268-6b4b-4659-a5b0-38e1f949f10f
Status: Started
Number of Bricks: 1 x 2 x 2 = 4
Transport-type: tcp
Bricks:
Brick1: 10.10.20.201:/data/pre
Brick2: 10.10.20.202:/data/pre
Brick3: 10.10.20.203:/data/pre
Brick4: 10.10.20.204:/data/pre
Options Reconfigured:
performance.flush-behind: OFF // 此处若为on,就Ok
diagnostics.count-fop-hits: on
diagnostics.latency-measurement: on
performance.readdir-ahead: on

相关issue:https://github.com/gluster/glusterfs/issues/341

转载于:https://www.cnblogs.com/Lemuria/p/7783715.html

<think>我们正在处理用户关于GlusterFS迁移的问题。用户希望将GlusterFS配置从一台机器迁移到另一台机器。根据引用[1]和引用[2],我们知道GlusterFS存储的数据文件是原样存储的,这有利于迁移。同时,引用[2]提到了重新平衡卷以修复布局变化,这可能与迁移后的数据平衡有关。 迁移GlusterFS配置实际上包括两个主要部分: 1. 迁移GlusterFS的配置(卷定义、拓扑等) 2. 迁移实际的数据文件 但是,用户的问题中只提到了“配置迁移”,所以我们重点放在配置迁移上。然而,配置迁移通常需要伴随数据迁移(因为配置定义了数据如何分布)。因此,我们将整个迁移过程分为几个步骤。 注意:我们假设源机器和目标机器都是GlusterFS集群中的节点,或者至少目标机器已经安装了相同版本的GlusterFS。 步骤概述: 1. 准备工作:确保目标机器满足要求(相同的操作系统、相同的GlusterFS版本、网络互通等)。 2. 安装GlusterFS:在目标机器上安装与源机器相同版本的GlusterFS。 3. 迁移卷配置:导出源节点的卷配置,然后在目标节点上导入。 4. 迁移数据:将数据文件从源节点复制到目标节点(如果目标节点是替换源节点,则需要复制数据;如果是新增节点则不需要)。 5. 替换节点(如果需要):如果目标节点是用于替换源节点,则需要将源节点从集群中移除,并加入目标节点,然后进行数据重新平衡。 6. 重新平衡卷:根据引用[2],在节点变更后,可能需要重新平衡卷以修复布局或迁移数据。 详细步骤: 步骤1:准备工作 - 检查目标机器的操作系统和架构是否与源机器一致。 - 确保目标机器有足够的磁盘空间,且磁盘路径与源机器相同(如果不同,则需要在配置中调整)。 - 确保网络互通,并且目标机器的主机名能够被集群中其他节点解析。 步骤2:安装GlusterFS - 在目标机器上安装GlusterFS,版本要与源集群一致。安装方法可参考官方文档或之前提供的安装步骤。 步骤3:迁移卷配置 - 在源节点上,可以使用以下命令导出卷配置: ```bash gluster volume get all <卷名> > volume-options.txt ``` 但是,这个命令只能导出卷选项,不包括卷的拓扑结构(即哪些brick属于哪个节点)。因此,更推荐的方法是记录下卷的拓扑结构(通过`gluster volume info`),然后在目标节点上重新创建卷。 - 另一种方法是备份整个GlusterFS的配置目录(`/var/lib/glusterd`),然后复制到目标机器上。但是,这个方法需要谨慎,因为其中包含节点UUID等信息,直接复制可能导致冲突。官方不推荐直接复制此目录。 - 推荐做法:在目标节点上重新创建卷。步骤如下: a. 在目标节点上创建brick目录(与源节点相同的路径,或者根据需求调整)。 b. 使用`gluster volume create`命令重新创建卷,指定相同的卷类型和brick路径(但使用目标节点的主机名或IP)。 注意:如果目标节点是作为源节点的替代,那么我们需要将源节点从卷中移除,然后添加目标节点。但是,如果整个集群迁移(即所有节点都要迁移),则需要逐个节点替换。 步骤4:迁移数据 - 如果目标节点是替换源节点,那么需要将源节点上的brick数据复制到目标节点对应的brick目录。由于GlusterFS的数据是原样存储(引用[1]),我们可以直接使用rsync等工具复制。 ```bash rsync -avzP /path/to/source/brick/ user@target:/path/to/target/brick/ ``` 注意:复制数据时,需要停止源节点上的glusterfs服务(或者确保在复制期间没有写入操作,否则可能导致数据不一致)。 步骤5:替换节点(如果目标节点是替换源节点) - 将目标节点加入集群(如果尚未加入): ```bash # 在集群中任意节点上执行 gluster peer probe <目标节点主机名> ``` - 将目标节点添加到卷中(以替换源节点): ```bash gluster volume replace-brick <卷名> <源节点>:<brick路径> <目标节点>:<brick路径> commit force ``` 这个命令会启动数据迁移(从源节点到目标节点),并完成替换。 但是,如果我们要完全移除源节点,则需要先确保目标节点已经加入并复制了数据,然后从卷中移除源节点: ```bash gluster volume remove-brick <卷名> <源节点>:<brick路径> start gluster volume remove-brick <卷名> <源节点>:<brick路径> status # 查看状态 gluster volume remove-brick <卷名> <源节点>:<brick路径> commit ``` 然后,将源节点从集群中移除(如果不再需要): ```bash gluster peer detach <源节点主机名> ``` 步骤6:重新平衡卷 - 在节点变更后,根据引用[2],我们需要修复布局以使文件分布到新节点。执行: ```bash gluster volume rebalance <卷名> fix-layout start ``` 或者,如果需要迁移数据(如添加了多个节点,需要数据重新分布): ```bash gluster volume rebalance <卷名> start ``` 然后可以通过`status`命令查看进度。 注意事项: - 迁移过程中应避免写入操作,或者确保在业务低峰期进行。 - 迁移后要测试卷的挂载和使用是否正常。 - 如果整个集群迁移,需要逐个节点替换,并确保每次替换后集群健康。 以上步骤适用于节点替换或新增节点。如果用户是迁移整个集群(例如,从旧服务器迁移到新服务器),则需要按照集群的拓扑结构,逐个节点进行替换。 由于用户的问题描述是“将一个机器上的GlusterFS配置迁移到另一台机器”,我们假设是单节点迁移(即替换一个节点)。如果是整个集群迁移,则需要重复上述步骤(从非冗余节点开始,逐个替换,并确保每一步后集群仍然冗余)。 引用说明: - 引用[1]说明了GlusterFS的数据存储方式,这让我们可以直接复制数据文件。 - 引用[2]提供了重新平衡卷的方法,这在迁移后调整数据分布时非常重要。 因此,我们按照上述步骤回答用户。 注意:在迁移过程中,如果卷是复制卷或分布式复制卷,那么替换节点时,数据会自动从其他副本同步,但上述步骤(使用replace-brick)是官方推荐的方式。 最后,生成相关问题。</think>根据您的需求,我将详细说明如何将 GlusterFS 配置从一台机器迁移到另一台机器,结合引用内容的关键特性: ### 🔧 迁移步骤(保留原数据格式特性) 1. **准备目标机器** ```bash # 在目标机器安装相同版本的 GlusterFS sudo apt install glusterfs-server glusterfs-client # Ubuntu/Debian # 或 sudo yum install glusterfs-server glusterfs-client # CentOS/RHEL ``` 2. **迁移配置文件** ```bash # 在源机器备份配置(关键目录) sudo tar -czvf gluster-backup.tar.gz /etc/glusterfs /var/lib/glusterd # 复制到目标机器 scp gluster-backup.tar.gz user@new-server:/tmp/ # 在目标机器恢复配置 sudo tar -xzvf /tmp/gluster-backup.tar.gz -C / ``` 3. **迁移数据文件(利用原样存储特性)** ```bash # 直接复制原始数据文件(无需格式转换) rsync -avzP /data/gluster-brick/ user@new-server:/data/new-brick/ ``` > 📌 **重要提示**: > 得益于 GlusterFS 的原样存储特性(引用[1]),数据文件可直接复制使用,无需转换格式[^1] 4. **重建卷配置** ```bash # 在目标机器重建卷(使用相同卷类型) sudo gluster volume create vol1 replica 2 new-server:/data/new-brick force # 启动卷 sudo gluster volume start vol1 ``` 5. **重新平衡数据(关键步骤)** ```bash # 修复布局并迁移数据(引用[2]) sudo gluster volume rebalance vol1 fix-layout start sudo gluster volume rebalance vol1 start # 监控进度 sudo gluster volume rebalance vol1 status ``` > ⚠️ 此步骤确保新节点正确接管数据分布(分布式卷/复制卷场景尤其重要)[^2] 6. **客户端切换** ```bash # 更新客户端挂载点 sudo umount /mnt/gluster sudo mount -t glusterfs new-server:/vol1 /mnt/gluster ``` ### 🌟 迁移优势(基于引用特性) 1. **零格式转换** 数据文件保持原生格式,可直接访问备份文件(引用[1])[^1] 2. **在线迁移** 支持 `replace-brick` 命令实现热迁移,减少业务中断: ```bash gluster volume replace-brick vol1 old-server:/brick new-server:/brick commit force ``` 3. **拓扑灵活性** 可无缝整合到新的分布复制卷架构(引用[3]),支持跨机架/可用区部署[^3] ### ⚠️ 注意事项 1. 版本一致性:源/目标服务器需保持相同 GlusterFS 主版本 2. 防火墙配置:确保新节点开放 `24007/tcp` 和 `49152-49251/tcp` 端口 3. 数据验证:迁移后运行 `gluster volume heal vol1 info` 检查数据完整性 4. 灰度切换:建议先迁移非生产卷测试 > 💡 **经验提示**:对于分布式复制卷(Distributed Replicated),建议逐个brick迁移,避免同时下线多个副本节点[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值