美文网首页
Ceph Filesystem 能保证强一致性吗?

Ceph Filesystem 能保证强一致性吗?

作者: IvanGuan | 来源:发表于2018-12-03 17:40 被阅读0次

    CephFs 简介

     Ceph Filesystem 是基于librados给应用提供的一种兼容posix语义的文件接口。它提供了Multi MetaData Server架构,这样能够大幅的提高元数据存取Performence,而且有效规避了单点瓶颈的问题。现在支持FUSE,Kernel 和 Libcephfs 三种使用方式。关于它的详细介绍社区都有成熟的文档,我就不过多介绍了,进入今天的主题。

    CephFS 一致性分析

     个人觉得CephFs的优势一是不俗的元数据存取能力,二就是它的强一致特性。你可能想问,既然强一致性是它的一个特性,那标题的的答案不就是肯定的么,但其实答案是片面的,也就是在某些场景是强一致的,有时候不是!!!

    强一致场景(元数据)

     我们先来说说强一致的场景,假如有客户端Jack和客户端Rose。我们有时候需要Jack客户端创建一个文件,然后在Rose客户端立即要使用该文件。这种需求CephFs可以保证绝对强一致性,因为它不缓存创建,删除等元数据操作,任何的创建和删除(文件或者目录)都会直接发到MDS端。MDS端收到创建请求之后会顺势revoke掉Rose客户端对该文件父目录的共享权限,换句话说就是标志了Rose客户端和MDS的元数据信息不一致。这样当Rose客户端去读取该文件或者readdir的时候,就会直接去MDS获取,而不是读本地的缓存。

    "非强一致场景(数据)"

     说完了元数据的强一致,那么我们说说CephFilesystem对于数据的强一致特性。这个时候可能就又人要怀疑了,谁说ceph不能保证数据的强一致?不应该是这样的吗?


    fuse-client

     有两个fuse客户端Jack 和 Rose。当Rose要读数据,会通知Jack把你的dirty数据刷下来,我要读取最新的数据。对应的,当Jack要写数据数据,那么要通知Rose你把你的缓存失效吧,我要写新数据了。基于上面的逻辑好像确实能够保证的数据的一致性!
     但是他们两个进程是独立的,可能在两个完全不知道对方服务器上,为了和对方通信那就只能通过第三方MetaData Server。看起来很美好的事情,往往都有坑,因为这两个不可能每次读写数据都跟MDS通信,这样数据和元数据耦合性太高了,效率太低了,再说了当客户端多了之后MDS压力会非常大。所以就会有问题了,如果Rose之前缓存过数据,那么它的下一次读取就非常可能读到旧的数据。
     Rose发现Jack总是背着自己偷偷做一些事情(写数据),不高兴了,就去找Jack理论了,两人达成统一意见。以后Jack每次写之前都给MDS说一下,Rose每次读之前也都去问问MDS,这样就不会误会了。但即便是这样Rose还是会发现偶尔会读到旧数据,又在怀疑Jack在搞鬼了。这时候MDS站出来为Jack说了句公道话,它说Jack确实给我说了,而且我也给你说了,读到旧数据这事真不能懒Jack,赖你。后来Rose不服气又找来OSD来理论,最后发现确实是自己的错,就没有给OSD发送正确的读取offset和length。Jack确实把数据写在OSD上,但是Rose她自己没有去OSD去读最新的数据,二是直接从fuse page cache里面命中直接返回了。这回Rose决定从自己身上找问题,反省了好几天终于发现原来是自己的配置有问题,自己的“fuse_use_invalidate_cb”选项没有打开,导致MDS通知自己清理缓存的时候没有执行下面的函数从而没能清除掉fuse page cache。

    Option("fuse_use_invalidate_cb", Option::TYPE_BOOL, Option::LEVEL_ADVANCED)
    .set_default(true)
    .set_description("use fuse 2.8+ invalidate callback to keep page cache consistent")
    
    static void ino_invalidate_cb(void *handle, vinodeno_t vino, int64_t off,
                      int64_t len)
    {
    #if FUSE_VERSION >= FUSE_MAKE_VERSION(2, 8)
      CephFuse::Handle *cfuse = (CephFuse::Handle *)handle;
      fuse_ino_t fino = cfuse->make_fake_ino(vino.ino, vino.snapid);
      fuse_lowlevel_notify_inval_inode(cfuse->ch, fino, off, len);
    #endif
    }
    

     自从Rose打开了这个选项之后,跟Jack相处的很和谐,再也没有误会产生了。但是好景不长,随着大版本升级到Luminous, Rose发现老问题又来了,经历了上次的闹剧之后,这次她先从自身找问题,确定打开了"fuse_use_invalidate_cb" 选项之后又找MDS理论去了。
     MDS 看着阵势,不把这事解决了今天没发给Rose一个交代,深深吸了一口烟之后带上眼睛开始找问题到底出在哪里了。仔细地翻阅着所有的Pull request,终于发现了一个可疑patch:https://github.com/ukernel/ceph/commit/7db1563416b5559310dbbc834795b83a4ccdaab4
     经过分析发现这个commit是为了所有客户端在getattr的时候拍长队,所以做了优化,这样就导致Jack和Rose通信不是每次都有效了,这下可把Rose急坏了。不过姜还是老的辣,MDS说了,别慌,我给你出个主意,你开启一下"fuse_disable_pagecache"选项就可以了。

    Option("fuse_disable_pagecache", Option::TYPE_BOOL, Option::LEVEL_ADVANCED)
    .set_default(false)
    .set_description("disable page caching in the kernel for this FUSE mount"),
    

     从此两人和谐共处...

     上面Jack 和 Rose的场景可以用下面的两个小程序来验证:

    write_file_step.c
    #include<stdio.h>
    #include<fcntl.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<error.h>
    #include<string.h>
    
    int fd = 0;
    char buf[131073]={0};
    char read_buf[131073]={0};
    int step_length = 0;
    
    void fill_buf(char *buf, char chars, int len)
    {
        int i = 0;
        for (i=0; i < len; i++)
            buf[i] = chars;
    }
    
    int write_file(char *a, int len, int fd, unsigned long off)
    {
        int r = pwrite(fd, a, len, off);
        printf("write sucess: offset %d %d %c\n", off, r, a[0]);
    }
    
    int main(int argc, char **argv)
    {
        char command = ' ';
        unsigned long offset = 0;
    
        if (argc < 2)
        {
            printf("Lack of argument:\n");
            printf("%s file step_length letter\n", argv[0]);
            return -1;
        }
        char *file = argv[1];
        step_length = atoi(argv[2]);
    
        printf("file is %s step_length is %d \n", file, step_length);
    
        if ((fd = open(file, O_RDWR | O_CREAT)) == -1)
        {
            printf("can open file %s\n", file);
            return -1;
        }
    
        printf("please input comand 'n' or 'w' \n \
               'n': offset will add step_length afeter write\n \
               'w/any letter': write the letter and don't change offset\n");
    
        command = getchar();
        while (command != 'q') {
            getchar();
            char letter = command;
            printf("command is : %c\n", command);
            fill_buf(buf, letter, step_length);
            write_file(buf, step_length, fd, offset);
    
            if (command == 'r') {
                printf("will write at same offset: %d\n", offset);
            } else if (command == 'n') {
                offset += step_length;
                printf("will write at next offset: %d\n", offset);
            }
            command = getchar();
        }
    
        printf("command %c not right, exit ...\n", command);
    }
    
    read_file_step.c
    #include<stdio.h>
    #include<fcntl.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<error.h>
    #include<string.h>
    
    int fd = 0;
    char buf[131073]={0};
    char read_buf[131073]={0};
    int step_length = 0;
    
    int read_file(char *buf, int len, int fd, unsigned long off)
    {
        //lseek(fd, 0, SEEK_SET);
        //int r = read(fd, buf, len);
        //int r = pread(fd, buf, len);
        int r = pread(fd, buf, len, off);
        printf("read sucess: offset %d %d %c\n",off, r, buf[0]);
    }
    
    
    int main(int argc, char **argv)
    {
        char command = ' ';
        unsigned long offset = 0;
    
        if (argc < 2)
        {
            printf("Lack of argument:\n");
            printf("Usage: %s file step_length \n", argv[0]);
            return -1;
        }
        char *file = argv[1];
        step_length = atoi(argv[2]);
    
        printf("file is %s step_length is %d \n", file, step_length);
    
        if ((fd = open(file, O_RDWR | O_CREAT)) == -1)
        {
            printf("can open file %s\n", file);
            return -1;
        }
    
        printf("please input comand 'n' or 'w' \n \
               'n': offset will add step_length afeter read\n \
               'r': read at offset and will not increase it\n");
    
        command = getchar();
        while (command != 'q') {
            getchar();
    
            printf("command is : %c\n", command);
            read_file(buf, step_length, fd, offset);
    
            if (command == 'r') {
                printf("will read at same offset: %d\n", offset);
            } else if (command == 'n') {
                offset += step_length;
                printf("will read at next offset: %d\n", offset);
            }
            command = getchar();
        }
    
        printf("command %c not right, exit ...\n", command);
    }
    

    验证过程如图

    Jack.jpg Rose.jpg file.jpg

    大家从Jack.jpg中可以看到Jack对文件/mnt/seven/congcong 每次写30个字符(/mnt/seven/是FUSE挂载点),依次是30个‘X’, 'T', 'A', 'O'。每次写完之后都让Rose客户端去读取内容(图-Rose.jpg所示),你发现不对的地方了吗?Rose客户端读到的居然每次都是Jack第一次写入的字符‘X’。随后我们查看该文件的时候内容确实已经全部是‘O’了,也就是说Jack客户端是写成功了的,但是Rose客户端没有读成功。

    小结

     CephFs 在元数据方面不用配置任何选项可以保证实时一致性。但是对于数据需要开启一些选项才能够保证一致性。在Jewel版开启"fuse_use_invalidate_cb"这个选项之后就可以读到最新的数据了,但是这还要依赖客户端发送"getattr"才能使MDS端对于该Inode的filelock状态机发生变化,从而会revoke掉该客户端的缓存权限,这样才能把自己在ObjectCache层和FUSE Kernel层缓存的数据去掉。但是我们不能期待客户端每次读去或者写之前都会发送"getattr",而且在Luminous版优化了"getattr"。所以应用场景对数据一致性有很严格的要求,那么建议开启 "fuse_disable_pagecache"。但是使能该选项的话,客户端将会以DIRECT IO模式进行读写,又会影响性能,所以这块目前还需要优化。

     如有描述不准确的地方,欢迎各位提出,互相探讨。

    相关文章

      网友评论

          本文标题:Ceph Filesystem 能保证强一致性吗?

          本文链接:https://www.haomeiwen.com/subject/arbygftx.html