原创 吴就业 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函数上,都在下载图片文件这条链路上。
然后通过添加日记打印,统计文件读耗时,进一步确认了问题。
[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艺术”的水印。
代码中,与tls有关的地方就是发送https请求从s3下载文件,所以检查下载文件调用链路上是否存在可疑的内存泄漏,发现如下疑点。统计了访问日记,发现确实经常出现响应403。所以问题就清晰了,由于403是有body的,没有close响应的body导致的内存泄漏。
记录一次工作中实战的Java内存泄漏问题排查,Pod重启后无法查看现场,但通过gc日记可以确认存在内存泄露,并通过运行一段时间发现有个Java类的实例数量非常高。
订阅
订阅新文章发布通知吧,不错过精彩内容!
输入邮箱,提交后我们会给您发送一封邮件,您需点击邮件中的链接完成订阅设置。