文件上传ceph首次下载耗时慢问题排查

原创 吴就业 164 1 2024-03-02

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://www.wujiuye.com/article/c38a9cf506d6465881598457fbc371c2

作者:吴就业
链接:https://www.wujiuye.com/article/c38a9cf506d6465881598457fbc371c2
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

问题背景

据业务反馈,AI生成图片上传后,首次立即下载耗时可能需要几秒,且出现概率极大,很容易复现。如果是上传后,过个几秒后再下载,耗时则只需要几百毫秒。

定位问题

初步定位

根据业务提供的文件url,从上传服务的访问日记看,下载图片的耗时确实出现需要几秒的情况。

根据上传服务的图片下载代码逻辑和日记来判断,初步怀疑是使用的ceph文件存储系统可能存在性能问题,于是将问题转给运维。

运维查看相关ceph性能指标,排除ceph性能问题。另,通过修改url手段,让下载请求绕过回源上传服务,直接在nginx挂盘ceph获取文件,经过多次验证,未出现耗时情况。

因此,初步定位问题还是出现在上传服务本身。

进一步排查

根据初步的定位,范围已经缩小到上传服务本身,而由于日记给不出更多信息,因此需要使用trace等技术手段查看具体的耗时出现在哪,至少可以进一步缩短排查范围。

使用pprof追踪不到block耗时,但经过trace发现,耗时在调用cephfs这个开源库,而底层是调用ceph的c/c++库。分别耗时在_Cfunc_ceph_open和_Cfunc_ceph_read这两个c函数上,都在下载图片文件这条链路上。

img

然后通过添加日记打印,统计文件读耗时,进一步确认了问题。

[2024/02/29 18:14:01 CST] [INFO] [0] (fio.WriteFromReader.func1:29) write from reader total 3145 ms, total 97 bytes, read total 3145 ms and avg 0 bytes/ms, write total 1 ms and avg 97 bytes/ms

[2024/02/29 18:16:25 CST] [INFO] [0] (fio.WriteFromReader.func1:29) write from reader total 3585 ms, total 97 bytes, read total 3585 ms and avg 0 bytes/ms, write total 1 ms and avg 97 bytes/ms

目前只知道耗时在ceph,但是ceph已经排除了性能问题,而上传服务仅是调用开源库去读写文件,所以问题可以进一步缩小到cephfs开源库,甚至是ceph官方的c/c++库。但具体问题出在哪,目前无法判断。

nginx是挂盘方式去访问ceph的,上传服务是通过go-ceph调用ceph提供的c/c++库去读写文件的,这是差异。

代码实现根据官方demo修改而来,没有过多的逻辑。这一块也缺少文档,找不到更多资料看。

//go:build linux && !notcephlib

// linux平台下,且不存在notcephlib标志的环境才编译使用

package cephclient

import (
    "errors"
    "github.com/ceph/go-ceph/cephfs"
    "io"
    "os"
    "strings"
)

type Client struct {
    mount *cephfs.MountInfo
}

func NewCephFileSystem(cephConfPath string, mountRoot string) (*Client, error) {
    mount, err := cephfs.CreateMount()
    if err != nil {
       return nil, errors.New(getErrorMsg(err))
    }
    if err = mount.ReadConfigFile(cephConfPath); err != nil {
       return nil, errors.New(getErrorMsg(err))
    }
    if err = mount.Init(); err != nil {
       return nil, errors.New(getErrorMsg(err))
    }
    if err = mount.MountWithRoot(mountRoot); err != nil {
       return nil, errors.New(getErrorMsg(err))
    }
    return &Client{mount: mount}, nil
}

func (c *Client) Remove(file string) error {
    return c.mount.Unlink(file)
}

func (c *Client) Open(filePath string, mode uint32) (io.WriteCloser, error) {
    return c.mount.Open(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
}

func (c *Client) OpenReadOnly(filePath string) (io.ReadSeekCloser, uint64, error) {
    // 只读打开
    file, err := c.mount.Open(filePath, os.O_RDONLY, 0)
    if err != nil {
       return nil, 0, errors.New("Open read only file err. " + getErrorMsg(err))
    }
    // 获取文件信息
    stx, err := file.Fstatx(cephfs.StatxSize, cephfs.AtNoAttrSync)
    if err != nil {
       return nil, 0, errors.New("Read file stat err. " + getErrorMsg(err))
    }
    return file, stx.Size, nil
}

func (c *Client) MarkDir(dir string, mode uint32) error {
    err := c.mount.MakeDir(dir, mode)
    if err != nil {
       return errors.New("Make dir " + dir + " fail. " + getErrorMsg(err))
    }
    return nil
}

func (c *Client) MarkDirs(dir string, mode uint32) error {
    // 路径不完整,需要以"/"开头
    if !strings.HasPrefix(dir, "/") {
       return errors.New("Invalid argument: " + dir)
    }
    ss := strings.Split(dir, "/")
    ss = ss[1:]
    for i := 0; i < len(ss); i++ {
       pdir := "/" + strings.Join(ss[:i+1], "/")
       // 获取目录信息
       d, err := c.mount.OpenDir(pdir)
       if d != nil && err == nil {
          d.Close()
          // 已经存在
          continue
       }
       // 创建目录
       err = c.MarkDir(pdir, mode)
       if err != nil && !strings.Contains(err.Error(), "File exists") {
          return err
       }
    }
    return nil
}

func getErrorMsg(err error) string {
    errMsg := strings.Split(err.Error(), ", ")
    if len(errMsg) == 1 {
       return errMsg[0]
    }
    return errMsg[1]
}

func (c *Client) Close() {
    c.mount.Unmount()
    c.mount.Release()
}

由于对ceph的不了解,ceph 对libc库也没有文档介绍,又是c++开发的,没能进一步排查。

最终就是只能定位到ceph读写耗时。

解决方案

由于ceph这套东西目前使用api的方式出问题我们hold不住,所以决定将通过api(libc)访问ceph改造成“挂盘”的使用方式:上传服务改造成PVC方式使用ceph。

而我们给业务的临时解决方式是,下载文件绕过上传服务,直接通过nginx回源下载文件,由运维在nginx配置。

#中间件

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Go服务几个副本同时OOMKilled的诡异问题排查

最近出现一个非常诡异的现象,这个服务部署4个节点,几乎每次4个节点都是同时挂的。挂掉的原因都是OOMkill。

复盘我从0开发文件上传中间件,上线一年多遇到的疑难杂症

难题一:上传MFS文件MD5不一致;难题二:疑是go-ceph导致的内存泄漏;难题三:ceph文件首次下载慢。

中间件服务上线,那跟电影里的拆炸弹一样刺激

而针对这种大迭代的发版,根本原因还是要解决灰度粒度问题,支持流量粒度,支持全链路的灰度。

全球化的IM产品技术架构调研

主要调研学习Slack和WhatsApp这两个产品的全球化架构,单数据中心或是多数据中心,怎样做架构设计,能够解决异地多活和延迟问题。

Go语言内存泄漏问题排查实战-记一次线上容器重启问题排查

代码中,与tls有关的地方就是发送https请求从s3下载文件,所以检查下载文件调用链路上是否存在可疑的内存泄漏,发现如下疑点。统计了访问日记,发现确实经常出现响应403。所以问题就清晰了,由于403是有body的,没有close响应的body导致的内存泄漏。

Java内存GC故障问题排查实战-推送系统频繁GC告警问题排查

记录一次工作中实战的Java内存泄漏问题排查,Pod重启后无法查看现场,但通过gc日记可以确认存在内存泄露,并通过运行一段时间发现有个Java类的实例数量非常高。