存档

文章标签 ‘Storage’

使用 s3fs 挂载对象存储到本地磁盘

2016年5月10日 没有评论

虽然对象存储的协议和使用场景与传统的文件系统存储(传统的 NAS 等)存在较大的差异, 但是
某些场景下, 将对象存储当作普通的文件系统更为方便, 比如不用再为了一些基本的文件操作而
专门使用第三方客户端等.

以下介绍通过 s3fs 来将 S3 兼容对象存储作为文件系统使用的方法.

PS1: 对于 windows 用户来讲, 类似的工具很多, 比如使用 CloudBerry 提供的 CloudBerry Drive.

PS2: 对于非 S3 兼容的对象存储产品, 由于各大厂商都是模拟重新实现 S3 协议, 所以可以找到基于 s3fs
的工具, 比如阿里云对象存储的 ossfs, 用法和 s3fs 一样

安装

Linux

由于大部分发行版都没有在软件仓库中收录改包, 所以在 Linux 需要自行编译.

  • 安装开发依赖包
$ sudo apt-get install -y automake autotools-dev g++ git libcurl4-gnutls-dev libfuse-dev libssl-dev libxml2-dev make pkg-config # for ubuntu
$ sudo yum install -y automake fuse-devel gcc-c++ git libcurl-devel libxml2-devel make openssl-devel # for fedora
  • 下载编译 s3fs
$ git clone https://github.com/s3fs-fuse/s3fs-fuse.git
$ cd s3fs-fuse
$ ./autogen.sh
$ ./configure # 如果报错, 查看具体错误类型安装相应的依赖
$ make -j3
$ sudo make install

OSX

Mac OS 由于有 Homebrew 的存在, 安装很方便:

$ brew install s3fs

使用

设置密码文件

echo {AccessKey}:{SecretKey} > $HOME/.passwd-s3fs && chmod 600 $HOME/.passwd-s3fs

挂载

$ s3fs {BucketName} {MountPoint} -o host=http://obs.eayun.com:9090 -o umask=0022
  • obs.eayun.com 是一个自己搭建的兼容 S3 的对象存储服务, 如果使用的是 S3, 则该选项不需要使用
  • umask 是挂载的权限, 和 liunx 下的 umask 的一致, 这个参数很重要, 否则挂载用户无法读写某些对象

示例:

$ mkdir mnt
$ s3fs tmp mnt -o host=http://obs.eayun.com:9090 -o umask=0022
$ df -h mnt
文件系统        容量  已用  可用 已用% 挂载点
s3fs            256T     0  256T    0% /home/dunrong/mnt

之后就可以像使用一般文件系统一样使用 bucket 了

$ cp Kvm-forum-2013-COLO.pdf vm.sh mnt/  # <-- 复制本地文件到对象存储
$ s3cmd ls s3://tmp  # <-- 在对象存储中查看是否成功
2016-05-09 08:04   2440042   s3://tmp/Kvm-forum-2013-COLO.pdf
2016-05-09 08:04       586   s3://tmp/vm.sh
$ du -sh mnt/  # <-- 查看使用量的大小
2.4M	mnt/
$ ls -l mnt/  # <-- 查看挂载点上是否存在上传的对象
总用量 2384
-rwxr-xr-x 1 dunrong dunrong 2440042  59 16:04 Kvm-forum-2013-COLO.pdf
-rwxr-xr-x 1 dunrong dunrong     586  59 16:04 vm.sh
$ rm mnt/vm.sh  # <-- 删除对象
rm:是否删除普通文件 "mnt/vm.sh"? y
$ ls -l mnt/
总用量 2383
-rwxr-xr-x 1 dunrong dunrong 2440042  59 16:04 Kvm-forum-2013-COLO.pdf
$ s3cmd ls s3://tmp  # <-- 在对象存储中查看是否删除成功
2016-05-09 08:04   2440042   s3://tmp/Kvm-forum-2013-COLO.pdf

优缺点

优点

  • 可以像传统文件系统一样读写对象存储, 比如复制删除查看文件等操作
  • 在不同设备(windows, linux, macos) 之间共享 bucket 较为方便

缺点

虽然 s3fs 基于 FUSE, 但是底层的实现协议还是对象存储的协议, 很多基于文件的操作无法实现,
或者实现起来性能很差. 所以和传统的 NAS 等文件系统存储比较, 存在很多缺点.

  • 由于原生 S3 协议不支持基于文件的读写, 追加文件和随机写文件非常慢
  • 由于 S3 协议没有所谓的元数据服务器, 列出文件, 查看文件夹大小等操作非常慢
分类: Storage 标签:

Ceph 深入解析(2) — Ceph Admin Socket

2015年9月1日 1 条评论

介绍

在 Ceph 每个组件(e.g. Monitor, OSD, CephFS) 运行的时候,会创建一个基于 UNIX socket 的服务,该服务通过这个 socket 提供一个和该组件交互的接口. 用户可以 通过 ceph daemon {socket-file} command 查询/配置一些常用的参数.

使用

使用下列格式访问 admin socket:

$ ceph daemon [socket-file] [command]

其中, socket-file 是该组件的 unix socket 的路径, command 是具体的查询指令

一般可以通过 help 得到改组件的 admin socket 支持的命令

$ ceph daemon [socket-file] help

下面举几个例子:

  1. 查询 monitor 的 quorum 状态:
$ ceph daemon /var/run/ceph/ceph-mon.ceph1.asok quorum_status
{ "election_epoch": 304,
  "quorum": [
        0,
        1,
        2],
  "quorum_names": [
        "ceph1",
        "ceph2",
        "ceph3"],
  "quorum_leader_name": "ceph1",
  "monmap": { "epoch": 3,
      "fsid": "8e92b691-a67a-4e26-969a-4e4f18ed3fa0",
      "modified": "2015-06-17 19:35:00.946699",
      "created": "0.000000",
      "mons": [
            { "rank": 0,
              "name": "ceph1",
              "addr": "192.168.3.137:6789\/0"},
            { "rank": 1,
              "name": "ceph2",
              "addr": "192.168.3.138:6789\/0"},
            { "rank": 2,
              "name": "ceph3",
              "addr": "192.168.3.139:6789\/0"}]}}
  1. 查询本节点的 OSD 状态
$ ceph daemon /var/run/ceph/ceph-osd.1.asok status
{ "cluster_fsid": "8e92b691-a67a-4e26-969a-4e4f18ed3fa0",
  "osd_fsid": "05ee7ec8-2309-47b1-81bf-f6ef49d0f87f",
  "whoami": 1,
  "state": "active",
  "oldest_map": 1,
  "newest_map": 280,
  "num_pgs": 699}

实现代码解析

流程

bool AdminSocket::init(const std::string &path)
{
    // 创建一个管道,该管道用来让父进程关闭该子进程,看下面 entry 的处理
    err = create_shutdown_pipe(&pipe_rd, &pipe_wr);
 
    // 创建 Unix socket, 在该 socket 上监听客户端的连接
    err = bind_and_listen(path, &sock_fd);
 
    // 注册通用的命令 hook
    m_version_hook = new VersionHook;
    register_command("0", "0", m_version_hook, "");
    register_command("version", "version", m_version_hook, "get ceph version");
    register_command("git_version", "git_version", m_version_hook, "get git sha1");
    m_help_hook = new HelpHook(this);
    register_command("help", "help", m_help_hook, "list available commands");
    m_getdescs_hook = new GetdescsHook(this);
    register_command("get_command_descriptions", "get_command_descriptions",
                     m_getdescs_hook, "list available commands");
 
    // 进入新线程中执行 AdminSocket::entry()
    create();
}
 
void* AdminSocket::entry()
{
    while (true) {
        // 使用 poll 来轮训 socket, 注意,该线程轮训 Unix socket 和与父进程通信的
        // 管道
        struct pollfd fds[2];
        memset(fds, 0, sizeof(fds));
        fds[0].fd = m_sock_fd;
        fds[0].events = POLLIN | POLLRDBAND;
        fds[1].fd = m_shutdown_rd_fd;
        fds[1].events = POLLIN | POLLRDBAND;
 
        int ret = poll(fds, 2, -1);
        if (fds[0].revents & POLLIN) {
            // 处理这个请求
            do_accept();
        }
        // 如果进程推出,会通过 pipe 的写端发送一个字节
        if (fds[1].revents & POLLIN) {
            // Parent wants us to shut down
            return PFL_SUCCESS;
        }
    }
}
 
bool AdminSocket::do_accept()
{
    int connection_fd = accept(m_sock_fd, (struct sockaddr*) &address,
                               &address_length);
    char cmd[1024];
    int pos = 0;
 
    // 读取客户端发送的请求到 cmd
    while (1) {
        int ret = safe_read(connection_fd, &cmd[pos], 1);
        // new protocol: null or \n terminated string
        if (cmd[pos] == '\n' || cmd[pos] == '\0') {
            cmd[pos] = '\0';
            c = cmd;
            break;
        }
        pos++;
    }
 
    // 通过得到的 cmd 从所有注册的 hooks(AdminSocket::m_hooks)中找到匹配的
    // hook
    p = m_hooks.find(match);
 
    bufferlist out;
    // 调用 hook 的 AdminSocketHook::call 方法处理改请求,将结果填充到 out
    // 中
    bool success = p->second->call(match, cmdmap, format, out);
 
    // 发送请求结果
    uint32_t len = htonl(out.length());
    int ret = safe_write(connection_fd, &len, sizeof(len));
    if (out.write_fd(connection_fd) >= 0)
        rval = true;
}

分类: Storage 标签:

Ceph 深入解析(1) — Ceph 的消息处理架构

2015年8月14日 2 条评论

Ceph 的消息处理主要关联到以下几个类:

消息处理架构

架构上采用 Publish/subscribe(发布/订阅) 的设计模式.

模块说明

class Messenger

该类作为消息的发布者, 各个 Dispatcher 子类作为消息的订阅者, Messenger 收到消息之后,通过 Pipe 读取消息,然后转给 Dispatcher 处理

class SimpleMessenger

Messenger 接口的实现

class Dispatcher

该类是订阅者的基类,具体的订阅后端继承该类,初始化的时候通过 Messenger::add_dispatcher_tail/head 注册到 Messenger::dispatchers. 收到消息后,通知改类处理

class Accepter

监听 peer 的请求, 有新请求时, 调用 SimpleMessenger::add_accept_pipe() 创建新的 Pipe 到 SimpleMessenger::pipes 来处理该请求

class Pipe

用于消息的读取和发送,该类主要有两个组件,Pipe::Reader 和 Pipe::Writer, 分别用来处理 消息的读取和发送. 这两个类都是 class Thread 的子类,意味这每次处理消息都会有两个 线程被分别创建.

消息被 Pipe::Reader 读取后,该线程会通知注册到 Messenger::dispatchers 中的某一个 Dispatcher(如 Monitor) 处理, 处理完成之后将回复的消息放到 SimpleMessenger::Pipe::out_q 中,供 Pipe::Writer 来处理发送

class DispatchQueue

该类用来缓存收到的消息, 然后唤醒 DispatchQueue::dispatch_thread 线程找到后端的 Dispatch 处理消息

深入解析

流程

下面的代码涉及到的订阅子类以 Monitor 为例:

初始化

int main(int argc, char *argv[])
{
    // 创建一个 Messenger 对象,由于 Messenger 是抽象类,不能直接实例化,提供了一个
    // ::create 的方法来创建子类,目前 Ceph 所有模块使用 SimpleMessenger
    Messenger *messenger = Messenger::create(g_ceph_context,
                                             entity_name_t::MON(rank),
                                             "mon",
                                             0);
 
    /**
     * 执行 socket() -> bind() -> listen() 等一系列动作, 执行流程如下:
     SimpleMessenger::bind()
         --> Accepter::bind()
             socket() -> bind() -> listen()
    */
    err = messenger->bind(ipaddr);
 
    // 创建一个 Dispatch 的子类对象, 这里是 Monitor
    mon = new Monitor(g_ceph_context, g_conf->name.get_id(), store, 
                      messenger, &monmap);
 
    // 启动 Reaper 线程
    messenger->start();
 
    /**
     * a). 初始化 Monitor 模块
     * b). 通过 SimpleMessenger::add_dispatcher_tail() 注册自己到
     * SimpleMessenger::dispatchers 中, 流程如下:
     * Messenger::add_dispatcher_tail()
     *      --> ready()
     *        --> dispatch_queue.start()(新 DispatchQueue 线程)
              --> Accepter::start()(启动start线程)
     *            --> accept
     *                --> SimpleMessenger::add_accept_pipe
     *                    --> Pipe::start_reader
     *                        --> Pipe::reader()
     * 在 ready() 中: 通过 Messenger::reader(),
     * 1) DispatchQueue 线程会被启动,用于缓存收到的消息消息
     * 2) Accepter 线程启动,开始监听新的连接请求.
     */
    mon->init();
 
    // 进入 mainloop, 等待退出
    messenger->wait();
    return 0;
}

消息处理

收到连接请求

请求的监听和处理由 SimpleMessenger::ready –> Accepter::entry 实现

void SimpleMessenger::ready()
{
    // 启动 DispatchQueue 线程
    dispatch_queue.start();
 
    lock.Lock();
    // 启动 Accepter 线程监听客户端连接, 见下面的 Accepter::entry
    if (did_bind)
        accepter.start();
    lock.Unlock();
}
 
void *Accepter::entry()
{
    struct pollfd pfd;
    // listen_sd 是 Accepter::bind() 中创建绑定的 socket
    pfd.fd = listen_sd;
    pfd.events = POLLIN | POLLERR | POLLNVAL | POLLHUP;
    while (!done) {
        int r = poll(&pfd, 1, -1);
        if (pfd.revents & (POLLERR | POLLNVAL | POLLHUP))
            break;
        if (done) break;
        entity_addr_t addr;
        socklen_t slen = sizeof(addr.ss_addr());
        int sd = ::accept(listen_sd, (sockaddr*)&addr.ss_addr(), &slen);
        if (sd >= 0) {
            // 调用 SimpleMessenger::add_accept_pipe() 处理这个连接
            msgr->add_accept_pipe(sd);
        } 
    }
    return 0;
}

随后创建 Pipe() 开始消息的处理

Pipe *SimpleMessenger::add_accept_pipe(int sd)
{
    lock.Lock();
    Pipe *p = new Pipe(this, Pipe::STATE_ACCEPTING, NULL);
    p->sd = sd;
    p->pipe_lock.Lock();
    // 
    /**
     * 调用 Pipe::start_reader() 开始读取消息, 将会创建一个读线程开始处理.
     * Pipe::start_reader() --> Pipe::reader
     */
    p->start_reader();
    p->pipe_lock.Unlock();
    pipes.insert(p);
    accepting_pipes.insert(p);
    lock.Unlock();
    return p;
}

创建消息读取和发送线程

处理消息由 Pipe::start_reader() –> Pipe::reader() 开始,此时已经是在 Reader 线程中. 首先会调用 accept() 做一些简答的处理然后创建 Writer() 线程,等待发送回复 消息. 然后读取消息, 读取完成之后, 将收到的消息封装在 Message 中,交由 dispatch_queue() 处理.

dispatch_queue() 找到注册者,将消息转交给它处理,处理完成唤醒 Writer() 线程发送回复消息.

void Pipe::reader()
{
    /**
     * Pipe::accept() 会调用 Pipe::start_writer() 创建 wirter 线程, 进入 writer 线程
     * 后,会 cond.Wait() 等待被激活,激活的流程看下面的说明. Writer 线程的创建见后后面
     * Pipe::accept() 的分析
     */
    if (state == STATE_ACCEPTING) {
        accept();
    }
 
    while (state != STATE_CLOSED &&
           state != STATE_CONNECTING) {
        // 读取消息类型,某些消息会马上激活 writer 线程先处理
        if (tcp_read((char*)&tag, 1) < 0) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE2) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_KEEPALIVE2_ACK) {
            continue;
        }
        if (tag == CEPH_MSGR_TAG_ACK) {
            continue;
        }
        else if (tag == CEPH_MSGR_TAG_MSG) {
            // 收到 MSG 消息
            Message *m = 0;
            // 将消息读取到 new 到的 Message 对象
            int r = read_message(&m, auth_handler.get());
 
            // 先激活 writer 线程 ACK 这个消息
            cond.Signal();  // wake up writer, to ack this
 
            // 如果该次请求是可以延迟处理的请求,将 msg 放到 Pipe::DelayedDelivery::delay_queue, 
            // 后面通过相关模块再处理
            // 注意,一般来讲收到的消息分为三类:
            // 1. 直接可以在 reader 线程中处理,如上面的 CEPH_MSGR_TAG_ACK
            // 2. 正常处理, 需要将消息放入 DispatchQueue 中,由后端注册的消息处理,然后唤醒发送线程发送
            // 3. 延迟发送, 下面的这种消息, 由定时时间决定什么时候发送
            if (delay_thread) {
                utime_t release;
                if (rand() % 10000 < msgr->cct->_conf->ms_inject_delay_probability * 10000.0) {
                    release = m->get_recv_stamp();
                    release += msgr->cct->_conf->ms_inject_delay_max * (double)(rand() % 10000) / 10000.0;
                    lsubdout(msgr->cct, ms, 1) << "queue_received will delay until " << release << " on " << m << " " << *m << dendl;
                }
                delay_thread->queue(release, m);
            } else {
                // 正常处理的消息,放到 Pipe::DispatchQueue *in_q 中, 以下是整个消息的流程
                // DispatchQueue::enqueue()
                //     --> mqueue.enqueue() -> cond.Signal()(激活唤醒 DispatchQueue::dispatch_thread 线程)
                //         --> DispatchQueue::dispatch_thread::entry() 该线程得到唤醒
                //             --> Messenger::ms_deliver_XXX
                //                 --> 具体的 Dispatch 实例, 如 Monitor::ms_dispatch()
                //                     --> Messenger::send_message()
                //                         --> SimpleMessenger::submit_message()
                //                             --> Pipe::_send()
                //                                 --> Pipe::out_q[].push_back(m) -> cond.Signal 激活 writer 线程
                //                                     --> ::sendmsg() // 发送到 socket
                in_q->enqueue(m, m->get_priority(), conn_id);
            }
        } 
 
        else if (tag == CEPH_MSGR_TAG_CLOSE) {
            cond.Signal();
            break;
        }
        else {
            ldout(msgr->cct,0) << "reader bad tag " << (int)tag << dendl;
            pipe_lock.Lock();
            fault(true);
        }
    }
}

Pipe::accept() 做一些简单的协议检查和认证处理,之后创建 Writer() 线程: Pipe::start_writer() –> Pipe::Writer

int Pipe::accept()
{
    ldout(msgr->cct,10) << "accept" << dendl;
    // 检查自己和对方的协议版本等信息是否一致等操作
    // ......
 
    while (1) {
        // 协议检查等操作
        // ......
 
        /**
         * 通知注册者有新的 accept 请求过来,如果 Dispatcher 的子类有实现
         * Dispatcher::ms_handle_accept(),则会调用该方法处理
         */
        msgr->dispatch_queue.queue_accept(connection_state.get());
 
        // 发送 reply 和认证相关的消息
        // ......
 
        if (state != STATE_CLOSED) {
            /**
             * 前面的协议检查,认证等都完成之后,开始创建 Writer() 线程等待注册者
             * 处理完消息之后发送
             * 
             */
            start_writer();
        }
        ldout(msgr->cct,20) << "accept done" << dendl;
 
        /**
         * 如果该消息是延迟发送的消息, 且相关的发送线程没有启动,启动之
         * Pipe::maybe_start_delay_thread()
         *     --> Pipe::DelayedDelivery::entry()
         */
        maybe_start_delay_thread();
        return 0;   // success.
    }
}

随后 Writer 线程等待被唤醒发送回复消息

void Pipe::writer()
{
    while (state != STATE_CLOSED) {// && state != STATE_WAIT) {
        if (state != STATE_CONNECTING && state != STATE_WAIT && state != STATE_STANDBY &&
            (is_queued() || in_seq > in_seq_acked)) {
 
            // 对 keepalive, keepalive2, ack 包的处理
            // ......
 
            // 从 Pipe::out_q 中得到一个取出包准备发送
            Message *m = _get_next_outgoing();
            if (m) {
                // 对包进行一些加密处理
                m->encode(features, !msgr->cct->_conf->ms_nocrc);
 
                // 包头
                ceph_msg_header& header = m->get_header();
                ceph_msg_footer& footer = m->get_footer();
 
                // 取出要发送的二进制数据
                bufferlist blist = m->get_payload();
                blist.append(m->get_middle());
                blist.append(m->get_data());
 
                // 发送包: Pipe::write_message() --> Pipe::do_sendmsg --> ::sendmsg()
                ldout(msgr->cct,20) << "writer sending " << m->get_seq() << " " << m << dendl;
                int rc = write_message(header, footer, blist);
                m->put();
            }
            continue;
        }
 
        // 等待被 Reader 或者 Dispatcher 唤醒
        ldout(msgr->cct,20) << "writer sleeping" << dendl;
        cond.Wait(pipe_lock);
    }
}

消息的处理

Reader 线程将消息交给 dispatch_queue 处理,流程如下:

Pipe::reader() –> Pipe::in_q->enqueue()

void DispatchQueue::enqueue(Message *m, int priority, uint64_t id)
{
    Mutex::Locker l(lock);
    ldout(cct,20) << "queue " << m << " prio " << priority << dendl;
    add_arrival(m);
    // 将消息按优先级放入 DispatchQueue::mqueue 中
    if (priority >= CEPH_MSG_PRIO_LOW) {
        mqueue.enqueue_strict(
            id, priority, QueueItem(m));
    } else {
        mqueue.enqueue(
            id, priority, m->get_cost(), QueueItem(m));
    }
    // 唤醒 DispatchQueue::entry() 处理消息
    cond.Signal();
}
 
void DispatchQueue::entry()
{
    while (true) {
        while (!mqueue.empty()) {
            QueueItem qitem = mqueue.dequeue();
            Message *m = qitem.get_message();
            /**
             * 交给 Messenger::ms_deliver_dispatch() 处理,后者会找到
             * Monitor/OSD 等的 ms_deliver_dispatch() 开始对消息的逻辑处理
             * Messenger::ms_deliver_dispatch()
             *     --> Monitor::ms_dispatch()
             */
            msgr->ms_deliver_dispatch(m);
        }
        if (stop)
            break;
 
        // 等待被 DispatchQueue::enqueue() 唤醒
        cond.Wait(lock);
    }
    lock.Unlock();
}

下面简单看一下在订阅者的模块中消息是怎样被放入 Pipe::out_q 中的:

Messenger::ms_deliver_dispatch()
    --> Monitor::ms_dispatch()
        --> Monitor::_ms_dispatch
            --> Monitor::dispatch
                --> Monitor::handle_mon_get_map
                    --> Monitor::send_latest_monmap
                        --> SimpleMessenger::send_message()
                            --> SimpleMessenger::_send_message()
                                --> SimpleMessenger::submit_message()
                                    --> Pipe::_send()
bool Monitor::_ms_dispatch(Message *m)
{
    ret = dispatch(s, m, src_is_mon);
 
    if (s) {
        s->put();
    }
 
    return ret;
}
 
bool Monitor::dispatch(MonSession *s, Message *m, const bool src_is_mon)
{
    switch (m->get_type()) {
    case CEPH_MSG_MON_GET_MAP:
        handle_mon_get_map(static_cast<MMonGetMap*>(m));
        break;
    // ......
    default:
        ret = false;
    }
    return ret;
}
 
void Monitor::handle_mon_get_map(MMonGetMap *m)
{
    send_latest_monmap(m->get_connection().get());
    m->put();
}
 
void Monitor::send_latest_monmap(Connection *con)
{
    bufferlist bl;
    monmap->encode(bl, con->get_features());
    /**
     * SimpleMessenger::send_message()
     *     --> SimpleMessenger::_send_message()
     *         --> SimpleMessenger::submit_message()
     *             --> Pipe::_send()
     */
    messenger->send_message(new MMonMap(bl), con);
}
 
void Pipe::_send(Message *m)
{
    assert(pipe_lock.is_locked());
    out_q[m->get_priority()].push_back(m);
    // 唤醒 Writer 线程
    cond.Signal();
}

总结

由上面的所有分析,除了订阅者/发布者设计模式,对网络包的处理上采用的是古老的 生产者消费者问题 线程模型,每次新的请求就会有创建一对收/发线程用来处理消息的接受 发送,如果有大规模的请求,线程的上下文切换会带来大量的开销,性能可能产生瓶颈。

不过在较新的 Ceph 版本中,新增加了两种新的消息模型: AsyncMessenger 和 XioMessenger 让 Ceph 消息处理得到改善.

相关的评测以后带来

分类: Storage 标签:

RadosGW 初体验

2015年8月3日 没有评论

介绍

Ceph 的最底层模块是对象存储,本质上就是一个 Rados, 如果不考虑用户体验,利用 rados 就可以使用 Ceph 对象存储. 例如:

$ rados -p test_pool get SI7W1FUI43H3I3ED9CWX test.data
  • test_pool: pool 名
  • SI7W1FUI43H3I3ED9CWX: object 名
  • test.data 保存到这里

和 RBD 以及 CephMDS 一样,RadosGW 提供了对 Radow 对象存储访问的更友好支持. 它允许用户通过 Restful API 的方式进行使用 RadowGW 。

从技术架构上讲, 和 RBD 一样, 它位于 Librados 之上, 而且正如它的名字所暗示的, 它是 Rados 的一个网关,通过 HTTP RestFul API, 对外提供对象存储的服务. 为了用户使用的方便性和兼容性,还提供了兼容 S3 和 Swfit 的接口.

以上,使得可以基于 RadosGW 构建云存储.

环境描述

假设已经有一个安装好的 Ceph 集群. 在一台新的节点上配置 RadowGW

节点信息如下:

主机名字
ceph.test.com
IP 地址
192.168.3.140

NOTE: 所有节点的操作系统都是 centos 7.

安装部署

安装配置 Apache

  1. 安装
$ sudo yum install -y httpd
  1. 配置: 编辑 /etc/httpd/conf/httpd.conf
# 设置 ServerName
ServerName ceph.test.com
 
# 添加下面内容
<IfModule !proxy_fcgi_module>
    LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
</IfModule>
 
# 修改监听参数: 192.168.3.140 为本机 IP
Listen 192.168.3.140:80
  1. 重启 Apache
$ sudo systemctl restart httpd

安装配置 RadosGW

  1. 安装 RadosGW
$ sudo yum install -y radosgw-agent
  1. 创建用于 RadosGW 的用户等信息(在 ceph admin 节点执行)
$ sudo ceph-authtool --create-keyring /etc/ceph/ceph.client.radosgw.keyring
$ sudo chmod +r /etc/ceph/ceph.client.radosgw.keyring
$ sudo ceph-authtool /etc/ceph/ceph.client.radosgw.keyring -n client.radosgw.gateway --gen-key
$ sudo ceph-authtool -n client.radosgw.gateway --cap osd 'allow rwx' --cap mon 'allow rwx' /etc/ceph/ceph.client.radosgw.keyring
$ sudo ceph -k /etc/ceph/ceph.client.admin.keyring auth add client.radosgw.gateway -i /etc/ceph/ceph.client.radosgw.keyring

将这个 keyring 和 admin.keyring 复制到 ceph.test.com

$ scp /etc/ceph/ceph.client.radosgw.keyring root@ceph.test.com:/etc/ceph
$ scp /etc/ceph/ceph.client.admin.keyring root@ceph.test.com:/etc/ceph
  1. 创建 RadosGW 需要的 pool(在 ceph admin 节点)
$ ceph osd pool create .rgw 64 64
$ ceph osd pool create .rgw.root 64 64 
$ ceph osd pool create .rgw.control 64 64 
$ ceph osd pool create .rgw.gc 64 64
$ ceph osd pool create .rgw.buckets 64 64 
$ ceph osd pool create .rgw.buckets.index 64 64 
$ ceph osd pool create .log 64 64 
$ ceph osd pool create .intent-log 64 64 
$ ceph osd pool create .usage 64 64 
$ ceph osd pool create .users 64 64 
$ ceph osd pool create .users.email 64 64
$ ceph osd pool create .users.swift 64 64
$ ceph osd pool create .users.uid 64 64
  1. 修改并 gateway.client 的配置到各个节点.

将下列内容添加到 ceph 节点的各个配置文件(包括 RadosGW 节点)

[client.radosgw.gateway]
host = ceph
keyring = /etc/ceph/ceph.client.radosgw.keyring
rgw socket path = /var/run/ceph/ceph.radosgw.gateway.fastcgi.sock
log file = /var/log/radosgw/client.radosgw.gateway.log
rgw print continue = false
  1. 创建 RadosGW 运行目录
$ sudo mkdir -p /var/lib/ceph/radosgw/ceph-radosgw.gateway
$ sudo chown apache:apache /var/log/radosgw/client.radosgw.gateway.log
  1. 启动 RadosGW
$ /etc/init.d/ceph-radosgw start
  1. 将 RadosGW 的配置写入 Apache 中 a) 创建文件 /etc/httpd/conf.d/rgw.conf, 内容为:
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/html
 
ErrorLog /var/log/httpd/rgw_error.log
CustomLog /var/log/httpd/rgw_access.log combined
 
# LogLevel debug
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
SetEnv proxy-nokeepalive 1
ProxyPass / unix:///var/run/ceph/ceph.radosgw.gateway.fastcgi.sock|fcgi://localhost:9000/
</VirtualHost>

b) 重启 Apache

$ sudo systemctl restart httpd

测试

创建用户

为了完成测试,分别创建一个用于 S3 和 Swift 的用户:

  1. 创建 S3 用户:
$ sudo radosgw-admin user create --uid="testuser" --display-name="First User"
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
  ... ...
  "temp_url_keys": []}
  1. 创建 Swift 用户: a) 创建 Swift 用户
$ sudo radosgw-admin subuser create --uid=testuser --subuser=testuser:swift
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
        { "user": "testuser:swift",
          "access_key": "SI7W1FUI43H3I3ED9CWX",
          "secret_key": ""}],
  ... ...
  "temp_url_keys": []}

b) 设置 Swift 用户的密钥:

$ sudo radosgw-admin subuser create --uid=testuser --subuser=testuser:swift
{ "user_id": "testuser",
  ... ...
  "keys": [
        { "user": "testuser",
          "access_key": "EZU2MX4CJCIZILATWQSK",
          "secret_key": "4z18K+f7MZQop2Z99PxN2KKGSX9rb6KBd9ioW0D\/"},
        { "user": "testuser:swift",
          "access_key": "SI7W1FUI43H3I3ED9CWX",
          "secret_key": ""}],
  "swift_keys": [
        { "user": "testuser:swift",
          "secret_key": "yTXiN+2y1Uf6j+CXioZwhqzwCPhOgqVblm2iShj+"}],
  ... ...
  "temp_url_keys": []}

测试

下面的例子使用 s3cmd 测试用 S3 接口连接使用 RadosGW:

NOTE: s3cmd的安装配置请参考 s3cmd 的相关文档

$ s3cmd mb s3://my-bucket # 创建一个 bucket
Bucket 's3://my-bucket/' created
$ s3cmd ls
2015-08-03 10:11  s3://my-bucket
$ cat test.sh 
source openrc
python foo.py $@
$ s3cmd put test.sh s3://my-bucket/my-dir/test.sh # 将 test.sh 上传到 my-bucket/my-dir 下
test.sh -> s3://my-bucket/my-dir/test.sh  [1 of 1]
 31 of 31   100% in    0s   196.90 B/s  done
$ s3cmd ls s3://my-bucket # 查看目录
                       DIR   s3://my-bucket/my-dir/
$ s3cmd get s3://my-bucket/my-dir/test.sh test1.sh # 下载 test.sh 这个对象
s3://my-bucket/my-dir/test.sh -> test1.sh  [1 of 1]
s3://my-bucket/my-dir/test.sh -> test1.sh  [1 of 1]
 31 of 31   100% in    0s     5.83 kB/s  done
$ cat test1.sh
source openrc
python foo.py $@

分类: Storage 标签: ,

SSD 中的 TRIM 指令

2014年5月21日 没有评论

SSD 写数据的原理

在机械硬盘中, 写入新数据的时候可以直接覆盖原数据, 但是在 SSD 中, 情况发生
了变化. SSD 并不能直接覆盖原来的数据, 必须先要把旧的数据先擦除然后才可以
把新的数据写入.

从物理结构上讲, SSD 被分为许多 block, 每个 block 又可以
再往下划分为 pages. 数据可以直接以页面为单位写入, 但是要想删除数据却需要以
块为单位. 因此在删除数据的时候稍微麻烦一些: 首先需要把一个块内包含有用的数据
先复制粘贴到全新的块中的页面内, 这样原来块中包含的无用数据才能够以块为单位
删除. 删除后, 才能够写入新的数据.

所谓的 SSD 的垃圾回收就是把现存数据重新转移到其他闪存位置, 并且把一些无用
的数据彻底删除的过程.

这样会带来一个问题: 系统删除某个文件只是简单的在逻辑数据表内把存储要删除的数据
的位置标记为可用. 这在机械硬盘的情况很正常. 但对于 SSD , 由于其并不具备直接
覆盖旧数据的能力, 直到写入那个位置的时候, 它才意识到这些数据已经无
效了(被操作系统删除了). 在此期间发生的垃圾回收操作就会读写这些无效的数据,
大大降低 SSD 的性能.

TRIM 指令解析

TRIM 是基于 SATA 控制器的一个指令, 一旦有文件删除或者分区格式化. 操作系统就会
发送 TRIM 指令给 SSD 告诉它某处的数据已经删除了, SSD 因而知道这些数据是无效
的, 在垃圾回收的时候就可以直接忽略这些区域, 对这些区域可以直接进行清空. TRIM
指令的存在使得 SSD 能够紧紧跟随 OS 的操作意图, 达到性能的最优化和空间的最大化.

TRIM 的优势

  • 写入的吞吐量大大得到改善: 因为在垃圾回收的时候那些被标记为删除的数据不需要重写,
    节省了大量时间
  • 更多的空闲空间, 一旦数据被标记为删除, 这些空间马上就可以利用.
  • 设备的使用寿命得到增加: 由于不在需要重新写入无用数据, 减少了 SSD 实际写入次数.

如何使用

Mac

对于原装 Mac 的 SSD 硬盘来讲, 默认情况下 TRIM 已经打开了. 无论如何, 可以使用:
关于本机 -> 更多信息 -> 系统报告 -> SATA/SATA Express
里面的信息查看 “TRIM 支持”.

Linux

用下列指令查看 SSD 是否支持 TRIM:

# hdparm -I /dev/sda | grep -i trim

然后在 mount 的挂载参数添加 discard 选项.

或者调用 fstrim 向 SSD 设备发送 trim 指令.

Windows

Note: 首先 XP 系统不支持 TRIM 指令.

一般 Windows 系统下, TRIM 指令默认是开启的, TRIM 本来就是 Windows 联合厂商
提出来的.

在命令行提示符下: 输入以下指令查询 TRIM 的开启情况

C:\Windows\system32> fsutil behavior QUERY DisableDeleteNotify
DisableDeleteNotify = 0

0 表示启用, 1 表示没有启用.

用户可以使用下列指令开启 TRIM:

C:\Windows\system32> fsutil behavior set disabledeletenotify 0
DisableDeleteNotify = 0

用户可以使用下列指令关闭 TRIM:

C:\Windows\system32> fsutil behavior set disabledeletenotify 1
DisableDeleteNotify = 1

分类: Storage 标签:

Ceph 中的网络配置

2014年4月3日 没有评论

Ceph 是分布式的文件系统, 由于它是网络文件系统, 它的性能不仅仅受限于物理磁盘的
读写带宽(IOPS), 好的网络架构对发挥 Ceph 的性能也至关重要.

在这里, 我不去详细探讨 Ceph 内部的架构细节(在另外文章我会详细分析), 仅仅通过

Ceph 的配置文件和一些测试来简单研究一下 Ceph 的网络配置.

Ceph 里的数据同步

Ceph 里面的数据同步包括 Monitor 之间的增量同步配置(CRUSH啊这些), osd 之间
的数据冗余. 为了让 Ceph 的性能发挥到最好, 对于数据的传输, Ceph 定义了
public network 和 cluster network, public network 是外部访问用到的
网络(fuse client, rbd client), cluster network 为了 Ceph 内部组件的
数据同步(monitor, osd).

Ceph 的网络配置

前面说过, Ceph 区分外部访问的数据和内部集群使用的数据, 用两个不同的网络配置来
区分: public addrcluster addr.

一般来说, 生产环境中的服务器都有两块以上的网卡, 配置的时候, 一块网卡主要来跑
Ceph 的 对外的数据传输服务(保证足够的 IOPS), 另一块网卡来做集群内部的数据同步.

简单测试

这里的测试仅仅是为了测试上面说过的理论, 不涉及其它 Ceph 的内容. 为了方便计,
只用了一台机器同时跑 monitor 和 osd(这在生产环境中是非常不推荐的).

你可以用我之前的文章讲到的 Ceph 初体验 来搭建测试环境.

附上我的配置文件(注意我的 osd.0 的配置):

root@ceph1:~# cat /etc/ceph/ceph.conf 
[global]
fsid = 382f9965-75ad-403a-b063-4d1fa3e9fb52
mon_initial_members = ceph1
mon_host = 192.168.3.30
auth_supported = cephx
osd_journal_size = 1024
filestore_xattr_use_omap = true
 
[osd.0]
public_addr = 192.168.3.30
cluster_addr = 192.168.3.33

下面的图是该配置在 client 下用 iftop 截到的图, 可以看到数据的传输
走的是 192.168.3.33 这个网络.

ceph-iftop1

改变上面的 osd.0 配置为下面的内容

[osd.0]
public_addr = 192.168.3.33
cluster_addr = 192.168.3.30

用 iftop 可以看到, 数据传输的接口果然变了.

ceph-iftop2

总结

Ceph 的网络配置大体就这么两个参数, 当然使用上要比上面讲的灵活一点. 灵活配置
Ceph 的网络参数可以满足性能上的要求, 甚至在网络带宽远远小于磁盘的 I/O 带宽的
时候(在生产环境中比较少见), 可以给每一个 osd 配置一个网络出口达到最好的性能.

更多具体详细的讨论请参考 Ceph 官网上 网络配置文档.

分类: Storage 标签: ,

用 Puppet 部署 CEPH

2014年2月23日 没有评论

相比其他分布式系统(e.g. glusterfs), Ceph 的复杂毋庸置疑, 甚至在部署上也稍显
麻烦, 目前主流的部署方式主要有以下几种:

  • ceph-deploy: 取代之前的 mkcephfs, 不推荐在生产环节使用
  • chef: 官方推荐的部署工具
  • puppet: 大家部署这些服务最喜欢的方式
  • XXX: 自己从底层定制, 比如我这里写的 ceph-aio

本文主要介绍用 puppet 部署 ceph.

基本配置

三台机器, 每台机器都分别作为 monitor 和 osd, 并且每台机器都有两个磁盘,
把第二个磁盘(/dev/vdb)作为 osd. 我这里使用 这个 puppet 模块, 但是这个模块
官方没有对同时把一台节点变为 monitor 和 osd的测试, 我这里测试中也出了一点问题,
我用了一些 work around 的方法, 需要注意, 下面的 Troubleshoot 中会提到.

我部署的基本环境如下:

Hostname Ip Address Roles osd
ceph1.test 192.168.176.30 master + mon + osd /dev/vdb
ceph2.test 192.168.176.31 agent + mon + osd /dev/vdb
ceph3.test 192.168.176.32 agent + mon + osd /dev/vdb

修改每台节点的 hosts 文件, 使得可以互相用主机名 ping 通

# cat /etc/hosts
... ...
192.168.176.30  ceph1.test
192.168.176.31  ceph2.test
192.168.176.32  ceph3.test

puppet master 配置

1 安装 puppet 软件包

# apt-get install -y ruby1.8 puppetmaster sqlite3 libsqlite3-ruby \
  libactiverecord-ruby git augeas-tools puppet ruby1.8-dev libruby1.8
# update-alternatives --set ruby /usr/bin/ruby1.8

2 配置 puppet

修改 /etc/puppet/puppet.conf 的相应配置项为:

[master]
storeconfigs = true
dbadapter = sqlite3
 
[agent]
pluginsync = true
server = ceph1.test

设置我们的节点不需要认证:

# echo "*.test" > /etc/puppet/autosign.conf

3 puppet 的模块设置

安装需要的 puppet 模块:

# puppet module install ripienaar/concat
# puppet module install puppetlabs/apt

安装 puppet 的 ceph 模块:

# cd /etc/puppet/modules
# git clone git://github.com/enovance/puppet-ceph.git ceph

4 配置 site.pp 文件

我照着文档写了对应上面三个节点的 site.pp 文件, 可以从发 这里 下载, 然后拷贝到
/etc/puppet/manifests 目录下

5 重启 puppetmaster

# service puppetmaster restart

puppet agent 配置

注意: 此操作要在三个节点上同时执行.

1 安装 puppet 软件包

# apt-get -y install puppet

2 修改 /etc/puppet/puppet.conf 的相应配置项为:

[agent]
pluginsync = true
server = ceph1.test

3 大功告成, 开始利用 puppet 部署 ceph

# puppet agent --enable
# export AGENT_OPTIONS="--onetime --verbose --ignorecache --no-daemonize --no-usecacheonfailure --no-splay --show_diff --debug"
# puppet agent $AGENT_OPTIONS # 运行五步这步操作, 分别可以获取 keyring, 配置磁盘等等

4 依次登陆每个节点同步 ceph 的配置文件

# ssh cephx.test
# puppet agent -vt

Troubleshoot

在配置 osd 的时候会发现初始化 osd 的时候失败, 官方对同时把节点当做 monitor
和 osd 没有进行详细的测试, 我这里手动初始化 osd 来 work around 这个问题, 我这里
以 ceph1.test, 也就是 osd.0 为例来说明

# rm -rf /var/lib/ceph/osd/ceph-0/* # 先清空之前初始化失败的数据
# ceph auth add osd.0 osd 'allow *' mon 'allow rwx' -i /var/lib/ceph/osd/ceph-0/keyring # 初始化 osd 的目录
# service ceph start osd.0 # 启动 osd 服务

可以看到现在 osd 正常了

# ceph osd tree
# id    weight  type name   up/down reweight
-1  0.04999 root default
-2  0.04999     host ceph1
0   0.04999         osd.0   up  1

分类: Storage 标签: ,

Rados API 的使用

2014年1月4日 没有评论

Rados 介绍

Rados 是 Ceph 最底层的组件, 基于对象存储模型, 向上提供 object, file system, block 的存储方式. 这里我主要关注 rados block storage: rbd

rbd 用两种方式提供的客户端访问, 一种方式是是把客户端操作集成在内核中, 用户 只需要像使用其他文件系统一样挂载就可以直接使用了, 我在 CEPH 初体验 里面有 简单的提及.

另一种方式是通过 librados 来访问, 这个库有各种语言支持, e.g. C, python, java 等. QEMU 中对 Ceph 的支持即是通过这种方式来实现的.

本文主要讨论 librados(librbd) 的使用.

API 说明

为了使用 rbd, 需要使用以下几个 API:

/** 
 * 创建一个 rados_t 句柄, 该句柄存储了rados 客户端的数据结构, 用来和
 * rados 通信, 第二个参数是连接 rados 的客户端 ID
 * 
 * @param cluster 存储句柄的指针
 * @param id 连接 rados 的用户名
 * @return 
 */
int rados_create(rados_t *cluster, const char * const id);
 
/** 
 * 设置 rados 参数, 包括认证信息, monitor 地址等, 如果设置了 cephx 认证,
 * 那么之前创建 rados 句柄的时候, 必须设置客户端 ID, 并且必须设置 key 的密码
 * 
 * @param cluster 上面创建的 rados 句柄
 * @param option 
 * @param value 
 * @return 
 */
int rados_conf_set(rados_t cluster, const char *option, const char *value);
 
/** 
 * 完成了上面的设置之后, 使用 rados 句柄连接 rados 服务器
 * 
 * @param cluster 
 * 
 * @return 
 */
int rados_connect(rados_t cluster);
 
/** 
 * 成功连接上 rados 服务器之后, 就可以用 rados_ioctx_create() 打开 rados
 * 上的 pool, 该函数需要传递一个 rados_ioctx_t, 用来保存 IO 句柄
 * 
 * @param cluster rados 句柄
 * @param pool_name 需要打开的 pool
 * @param ioctx 保存 IO 句柄
 * 
 * @return 
 */
int rados_ioctx_create(rados_t cluster, const char *pool_name, rados_ioctx_t *ioctx);
 
/** 
 * 利用上面创建的关联到 pool 上的 IO 句柄打开具体的镜像
 * 
 * @param io IO 句柄
 * @param name 镜像名称
 * @param image 存储 image 句柄, 以后需要这个指针读写镜像
 * @param snap_name 打开镜像上的快照, 如果不是, 传递 null
 * @return 
 */
int rbd_open(rados_ioctx_t io, const char *name, rbd_image_t *image, const char *snap_name);
 
/** 
 * rados 支持异步操作, 当执行大量 I/O 的时候, 不需要等待每一个操作完成,
 * 只需要传递一个回调函数给读写函数, 当操作完成后, librados 会自动条用
 * 我们的回调函数. 下面的函数创建一个 rados 的异步句柄
 * 
 * @param cb_arg 传递给回调函数的参数
 * @param complete_cb 回调函数
 * @param c 存储异步句柄
 * 
 * @return 
 */
int rbd_aio_create_completion(void *cb_arg, rbd_callback_t complete_cb, rbd_completion_t *c);
 
/** 
 * 异步读的 API
 * 
 * @param image 上面谈到的 image 句柄
 * @param off 读写的文件位置
 * @param len 读写大小
 * @param buf 存储读写的数据
 * @param c 异步句柄
 * 
 * @return 
 */
int rbd_aio_read(rbd_image_t image, uint64_t off, size_t len, char *buf, rbd_completion_t c);

测试

完整的测试代码可以到 这里 下载, 我已经详细的写了程序注释.

首先, 我们创建一个测试的镜像, 写入一些数据, 看看我们的程序能不能读到:

# 1. 创建一个名为 test 的 pool
# rados mkpool test
# 2. 在 test 中创建镜像 hello
# rbd create -p test --size 10 hello
# 3. 在 hello 中写一些数据(为了使用这个 rbd, 先 map 到我们的文件系统, 写入数据, 再 unmap)
rbd map test/hello
echo "hello, you are reading image hello which is in test pool, have fun" > hello.txt
dd if=hello.txt of=/dev/rbd/test/hello
rbd unmap /dev/rbd/test/hello

然后用我们的测试程序来读取上面写入的数据

# ./rados test hello admin AQBhjqlSKBBTCxAAwchc9GauJ4+MPHz9hkV9Iw== 192.168.176.30:6789
open rados as following setting:
poolname: test
imagename: hello
username: admin
password: AQBhjqlSKBBTCxAAwchc9GauJ4+MPHz9hkV9Iw==
monitor: 192.168.176.30:6789
buffer read:
========================================
hello, you are reading image hello which is in test pool, have fun
 
========================================

分类: Storage 标签: ,

整合 Openstack 和 Ceph

2013年12月18日 没有评论

环境

Hostname IP Address Roles
ceph1 192.168.176.30 ceph-monitor/admin
ceph2 192.168.176.31 ceph-osd
ceph3 192.168.176.32 ceph-osd
os-control 192.168.176.152 controller/compute/cinder-scheduler
os-compute 192.168.176.156 compute
cinder1 192.168.176.156 cinder-node

为 openstack 创建 osd pool

在 ceph-admin 节点上:

root@ceph1:~# ceph osd pool create volumes 128
root@ceph1:~# ceph osd pool create images 128
root@ceph1:~# ceph osd pool stats
pool data id 0
  nothing is going on
 
pool metadata id 1
  nothing is going on
 
pool rbd id 2
  nothing is going on
 
pool volumes id 3
  nothing is going on
 
pool images id 4
  nothing is going on

为 openstack 节点配置 ceph

安装软件包

在 glance-node(os-control) 上:

root@os-control:~# apt-get install -y python-ceph

在 cinder-node(cinder1) 上:

root@os-compute:~# apt-get install -y python-ceph ceph-common

配置

配置文件

在 ceph-admin 节点把 ceph 的配置文件复制到 {glance,cinder-node}:/etc/ceph, 如果这两个节点不存在该文件夹, 先创建

root@ceph1:~# ceph-deploy config push os-control
root@ceph1:~# ceph-deploy config push cinder1

设置认证

1 因为我们之前的 Ceph 配置开启了 cephx 认证, 创建相应的用户来使用 Ceph 服务.

root@ceph1:~# ceph auth get-or-create client.volumes mon 'allow r' \
osd 'allow class-read object_prefix rbd_children, allow rwx pool=volumes, allow rx pool=images'
 
root@ceph1:~# ceph auth get-or-create client.images mon 'allow r' \ 
osd 'allow class-read object_prefix rbd_children, allow rwx pool=images'

2 然后把 client.{volumes/images} 的 keyring 复制到相应的节点(glance-node/cinder-node)

root@ceph1:~# ceph auth get-or-create client.images | ssh os-control tee /etc/ceph/ceph.client.images.keyring
root@ceph1:~# ssh os-control sudo chown glance:glance /etc/ceph/ceph.client.images.keyring
 
root@ceph1:~# ceph auth get-or-create client.volumes | ssh cinder1 tee /etc/ceph/ceph.client.volumes.keyring
root@ceph1:~# ssh cinder1 chown cinder:cinder /etc/ceph/ceph.client.volumes.keyring

3 配置 compute-node(os-control), 在 compute 节点上, 认证是由 libvirt 来完成的.

# 1) 把 client.volumes 的 keyring 复制到 compute-node
root@ceph1:~# ceph auth get-key client.volumes | ssh os-control tee client.volumes.key
 
# 2) 在 compute-node 中, 创建 secret.xml 文件, 内容如下
<secret ephemeral='no' private='no'>
  <usage type='ceph'>
    <name>client.volumes secret</name>
  </usage>
</secret>
 
# 3) 把这个文件导入到 libvirt 中
# virsh secret-define --file secret.xml
3888d137-bcda-45fe-93e3-4fdd0112d0df
 
# 4) 把 client.volumes 的密钥导入 libvirt 中
# virsh secret-set-value --secret 3888d137-bcda-45fe-93e3-4fdd0112d0df \
--base64 $(cat client.volumes.key) && rm client.volumes.key secret.xml

设置 openstack

1 在 glance-node(os-control) 中, 修改配置文件 /etc/glance/glance-api.conf 的以下值

default_store=rbd
rbd_store_user=images
rbd_store_pool=images
show_image_direct_url=True

2 在 cinder-node(cinder1) 中, 修改 /etc/cinder/cinder.conf 的以下值

volume_driver=cinder.volume.drivers.rbd.RBDDriver
rbd_pool=volumes
glance_api_version=2
rbd_user=volumes
rbd_secret_uuid=3888d137-bcda-45fe-93e3-4fdd0112d0df

3 在各自节点重启 openstack 相关服务

# service glance-api restart
# service nova-compute restart
# service cinder-volume restart

使用

创建 images

# 1) 用 glance 上传一个镜像, 命令和普通 glance 操作一样 ==>
# du -h /mnt/Images/ubuntu-13.10-server-cloudimg-amd64-disk1.img 
232M    /mnt/Images/ubuntu-13.10-server-cloudimg-amd64-disk1.img
# glance image-create \
--name="ubuntu-ceph-image" \
--is-public True \
--container-format bare \
--disk-format qcow2 \
--file /mnt/Images/ubuntu-13.10-server-cloudimg-amd64-disk1.img \
--progress
 
# 2) 查看上传的镜像
# nova image-list
+--------------------------------------+-----------------------+--------+--------+
| ID                                   | Name                  | Status | Server |
+--------------------------------------+-----------------------+--------+--------+
| 8aa5b765-1a4d-4b98-b5ad-9b6c7b58d57b | Ubuntu 13.10 cloudimg | ACTIVE |        |
| 7849c15e-9c28-49c6-97ab-d278bc27b525 | ubuntu-ceph-image     | ACTIVE |        |
+--------------------------------------+-----------------------+--------+--------+
 
# 3) 在 ceph 节点上查看 rados 的情况
# rbd ls images
7849c15e-9c28-49c6-97ab-d278bc27b525
# rbd info images/7849c15e-9c28-49c6-97ab-d278bc27b525
rbd image '7849c15e-9c28-49c6-97ab-d278bc27b525':
    size 231 MB in 29 objects                         <== 注意这个大小
    order 23 (8192 kB objects)
    block_name_prefix: rbd_data.10c2375b6
    format: 2
    features: layering

创建可启动 volumes

# 1) 创建一个 10G 的可启动盘
# cinder create --image-id 7849c15e-9c28-49c6-97ab-d278bc27b525 --display-name boot-vol-on-ceph 10
 
# 2) 查看创建的磁盘信息
# cinder list
+--------------------------------------+-----------+------------------+------+-------------+----------+--------------------------------------+
|                  ID                  |   Status  |   Display Name   | Size | Volume Type | Bootable |             Attached to              |
+--------------------------------------+-----------+------------------+------+-------------+----------+--------------------------------------+
| 6f01529e-1d42-4c53-b4c7-e50d466571cc |   in-use  |     volume1      |  1   |     None    |  false   | 015ac60a-1902-4d39-b4ea-11376838872b |
| 7604e894-231b-4d86-b613-9659c7583936 |   in-use  |     volume2      |  1   |     None    |  false   | 015ac60a-1902-4d39-b4ea-11376838872b |
| bd3a9b3a-64c8-4865-8f55-32dfc683ff67 | available | boot-vol-on-ceph |  10  |     None    |   true   |                                      |
+--------------------------------------+-----------+------------------+------+-------------+----------+--------------------------------------+
 
# 3) 查看 rbd 的信息
# rbd ls volumes
volume-bd3a9b3a-64c8-4865-8f55-32dfc683ff67
# rbd info volumes/volume-bd3a9b3a-64c8-4865-8f55-32dfc683ff67
rbd image 'volume-bd3a9b3a-64c8-4865-8f55-32dfc683ff67':
    size 10240 MB in 2560 objects                           <== 创建的大小 10G
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.10e02ae8944a
    format: 2
    features: layering

启动

# nova boot --flavor 2 --key_name mykey --block_device_mapping vda=bd3a9b3a-64c8-4865-8f55-32dfc683ff67:::0 myvm-on-ceph
# nova list
+--------------------------------------+--------------+--------+--------------+-------------+-----------------------+
| ID                                   | Name         | Status | Task State   | Power State | Networks              |
+--------------------------------------+--------------+--------+--------------+-------------+-----------------------+
| 015ac60a-1902-4d39-b4ea-11376838872b | myvm         | ACTIVE | None         | Running     | private=192.168.22.34 |
| 10309940-f56a-4c64-9910-561e29fb7b81 | myvm-on-ceph | ACTIVE | None         | Running     | private=192.168.22.36 |
+--------------------------------------+--------------+--------+--------------+-------------+-----------------------+

查看 QEMU 的命令行信息, 在 compute-node 节点

# ps aux | grep '[1]9537' | awk -F " -" '{ i=1;while(i<NF) {print NF,$i;i++}}' | grep 'drive file'
32 drive file=rbd:volumes/volume-bd3a9b3a-64c8-4865-8f55-32dfc683ff67:id=volumes:key=AQBkJK9SOA1UDBAAUqjfU6SvWzEv/w7ZH8nBmg==:auth_supported=cephx\;none:mon_host=192.168.176.30\:6789\;192.168.176.31\:6789\;192.168.176.32\:6789,if=none,id=drive-virtio-disk0,format=raw,serial=bd3a9b3a-64c8-4865-8f55-32dfc683ff67,cache=none

可以看到, QEMU 已经使用了 rbd 的 url 来读写虚拟机镜像了.

其它

如果配置多个 {compute,cinder}-node, 按照上面各自节点的配置做相应的配置即可.

分类: OpenStack, Storage 标签: , ,

CEPH 初体验

2013年12月12日 2 条评论

介绍

Ceph 是新一代的分布式文件系统, 具有高扩展, 高可靠性, 高性能的特点. 它采用多个 多个元数据 + 多个存储节点 + 多个监控节点的架构, 解决单点故障的问题.

架构简介

如图, ceph 的后端是 RADOS, 一个对象存储系统. 通过 rados, 提供了以下接口:

  • 类似于 amazon S3 的对象存储系统
  • Block 设备接口
  • 文件系统接口, 兼容 POSIX

ceph architecture

组件简介

ceph 主要由以下组件:

  • osd: Object Storage Device, 负责提供存储资源
  • monitor: 维护整个 ceph 集群的状态
  • mds: 保存 cephfs 的元数据, 非必须, 只有 cephfs 接口需要此模块
  • rados gateway: 提供 REST 接口, 兼容 S3 和 Swift 的 API

搭建环境准备

准备了三台机器, 每台机器有两个磁盘(vda, vdb), 初始搭建用 ceph1 作为 mon, ceph2, ceph3 作为 osd, osd mon, osd 的扩展之后完成

  | Hostname |     IP Address | Role       |
  |----------+----------------+------------|
  | ceph1    | 192.168.176.30 | mon, admin |
  | ceph2    | 192.168.176.31 | osd        |
  | ceph3    | 192.168.176.32 | osd        |

1 修改三台节点的 hosts 文件, 使得彼此可见, 在每一个节点 的/etc/hosts 中, 加入

192.168.176.30  ceph1
192.168.176.31  ceph2
192.168.176.32  ceph3

2 把 admin 节点(ceph1) 的 ssh 密钥拷贝到其他节点, 使得 ceph1 可以无密码访问其他节点

 # ssh-keygen
 # ssh-copy-id ceph1
 # ssh-copy-id ceph2
 # ssh-copy-id ceph3

3 在 admin node(ceph1) 上, 设置 ceph 的软件源仓库

 # wget -q -O- 'https://ceph.com/git/?p=ceph.git;a=blob_plain;f=keys/release.asc' | \
apt-key add -
 # echo deb http://ceph.com/packages/ceph-extras/debian $(lsb_release -sc) main | \
tee /etc/apt/sources.list.d/ceph-extras.list
 # CODENAME 是 lsb_release -sc 得到的值
 # apt-add-repository 'deb http://ceph.com/debian-emperor/ {CODENAME} main'
 # apt-get update && apt-get install ceph-deploy

部署

部署工作是在 admin node 上完成的.

 # 1. 创建一个工作目录
 # mkdir -p ~/my-cluster && cd ~/my-cluster
 
 # 2. 创建一个集群, 默认集群名为 ceph, 默认 monitor 为 ceph1
 # ceph-deploy new ceph1
 
 # 3. 在所有节点上安装 ceph 软件包, 此操作将在所有节点上执行以下操作, 1) 添加 ceph
 #    的软件源, 2) 更新软件源数据库, 安装 ceph ceph-mds ceph-common ceph-fs-common
 #    和 gdisk 软件包
 # ceph-deploy install ceph1 ceph2 ceph3
 
 # 4. 创建 mon, 这里选择 ceph1 作为 monitor 节点, 大概的流程如下:
 #    1) 把 ceph.conf 文件复制到 ceph1:/etc/ceph
 #    2) 在 ceph1 中创建 ceph1:/var/lib/ceph/mon/ceph-ceph1 并把 ceph.mon.keyring 
 #       复制到 ceph1:/var/lib/ceph/tmp/ceph-ceph1.mon.keyring
 #    3) 在 ceph1 中使用以下命令初始化 monitor data: 
 #       ceph-mon --cluster ceph --mkfs -i ceph1 --keyring /var/lib/ceph/tmp/ceph-ceph1.mon.keyring
 #    4) 在 ceph1 中运行 initctl emit ceph-mon cluster=ceph id=ceph1 启动 ceph-mon
 #       或者使用 service ceph -c /etc/ceph/ceph.conf start mon.ceph1
 #    5) 在 ceph1 中用 ceph --cluster=ceph --admin-daemon /var/run/ceph/ceph-mon.ceph1.asok mon_status
 #        检查 ceph1 的 mon 状态, 如果有错误, 报告之
 # ceph-deploy mon create ceph1
 
 # 5. 复制远程 node 上的密钥复制到当前文件夹, 包括:
 #    ceph.client.admin.keyring ==> ceph.client.admin.keyring
 #    /var/lib/ceph/mon/ceph-ceph1/keyring ==> ceph.mon.keyring
 #    /var/lib/ceph/bootstrap-osd/ceph.keyring ==> ceph.bootstrap-osd.keyring
 #    /var/lib/ceph/bootstrap-mds/ceph.keyring ==> ceph.bootstrap-mds.keyring
 # ceph-deploy gatherkeys ceph1
 
 # 6. 增加 osd
 #    prepare 主要步骤:
 #    1) push 配置文件 到 ceph{2,3}:/etc/ceph/ceph.conf
 #    2) push ceph.bootstrap-osd.keyring 复制到 ceph{2,3}:/var/lib/ceph/bootstrap-osd/ceph.keyring
 #    3) udevadm trigger --subsystem-match=block --action=add
 #    4) ceph-disk-prepare --fs-type xfs --cluster ceph -- /dev/vdb ==> 创建分区, 格式化 等
 #    activate 步骤:
 #    ceph-disk-activate --mark-init upstart --mount /dev/vdb ==> 挂载 ceph 分区, 使用以下命令启动
 #    initctl emit --no-wait -- ceph-osd cluster=ceph id=0
 # ceph-deploy osd prepare ceph2:/dev/vdb ceph3:/dev/vdb
 # ceph-deploy osd activate ceph2:/dev/vdb ceph3:/dev/vdb
 # ceph osd tree # 查看 osd 的状态
 # id   weight  type name   up/down reweight
-1  0.3 root default
-2  0.09999     host ceph2
0   0.09999         osd.0   up  1   
-3  0.09999     host ceph3
1   0.09999         osd.1   up  1   
 
 # 7. 复制 admin 密钥到其他节点
 #    复制 ceph.conf, ceph.client.admin.keyring 到 ceph{1,2,3}:/etc/ceph
 # ceph-deploy admin ceph1 ceph2 ceph3
 
 # 8. 查看部署状态
 # ceph health
HEALTH_OK ==> that's all

添加新的 osd

这里准备把 ceph1 变为 一个新的 osd

 # 根据上面的介绍, 新增一个 osd 很简单
 # ceph-deploy osd prepare ceph1:/dev/vdb
 # ceph-deploy osd activate ceph1:/dev/vdb
 # ceph osd tree # 查看状态
 # id   weight  type name   up/down reweight
-1  0.3 root default
-2  0.09999     host ceph2
0   0.09999         osd.0   up  1   
-3  0.09999     host ceph3
1   0.09999         osd.1   up  1   
-4  0.09999     host ceph1
2   0.09999         osd.2   up  1

添加新的 mon

为了 monitor 的高可用, 一般部署的时候推荐设置多个 monitor, 这里把 ceph2 和 ceph3 也变为 monitor.

 # 1. ceph2, ceph3 不在 mon_initial_members 中, 这里把 ceph{2,3} 也加入配置文件中
 #    顺便把 public 网络也设置一下
 # emacsclient ceph.conf
 # cat ceph.conf
root@ceph1:~/my-cluster# cat ceph.conf 
[global]
...
mon_initial_members = ceph1 ceph2 ceph3
public_network = 192.168.176.0/24
...
 
 # 2. 把配置文件同步到其它节点
 # ceph-deploy --overwrite-conf config push ceph1 ceph2 ceph3
 
 # 3. 创建 monitor
 # ceph-deploy mon create ceph2 ceph3

添加新 mds

mds 是 metadata service, 目前官方只推荐在生产环境中使用一个 mds. 因为 object 和 block storage 没有文件的概念, 不需要元数据, 所以 mds 只有在部署 CephFS 才有 意义.

 # 1. 细节解释:
 #    1) 把 ceph.conf 和 ceph.bootstrap-mds.keyring 复制到 ceph1:/etc/ceph 和
 #       /var/lib/ceph/bootstrap-mds/ceph.keyring
 #    2) 创建 ceph1:/var/lib/ceph/mds/ceph-ceph1
 #    3) ceph --cluster ceph --name client.bootstrap-mds \
 #       --keyring /var/lib/ceph/bootstrap-mds/ceph.keyring \
 #       auth get-or-create mds.ceph1 osd allow rwx mds allow mon allow profile mds \
 #       -o /var/lib/ceph/mds/ceph-ceph1/keyring
 #    4) 启动 initctl emit ceph-mds cluster=ceph id=ceph1, 或者
 #       service ceph start mds.ceph1
 # ceph-deploy mds create ceph1

作为文件系统使用

在一台安装了 ceph 客户端的机器上, 直接挂在

 # mount -t ceph ceph1:6789:/ /mnt -o name=admin,secret=AQBhjqlSKBBTCxAAwchc9GauJ4+MPHz9hkV9Iw==
 # df -h | grep mnt
192.168.176.30:6789:/        297G  108M  297G   1% /mnt

作为块设备使用

在一台安装了 ceph 的机器上(或者可以用 ceph-deploy install machine-name 安装), 执行以下命令:

 # modprobe rbd # 挂载 rbd 的内核模块
 # rbd create --size 4096 test1 # 创建一个 4G 的 rbd 镜像
 # 把 上面创建的 rbd 镜像映射到块设备 /dev/rbd/{poolname}/imagename , 这里为 /dev/rbd/rbd/test
 # rbd map test1 --pool rbd
 # 然后, 就可以像使用普通快设备一样使用这个块设备了(格式化,分区,直接给虚拟机使用), 例如
 # mkfs.ext4 /dev/rbd/rbd/test1 # 格式化
 # mount /dev/rbd/rbd/test1 /rbd
 # df -h | grep rbd
/dev/rbd1                    3.9G  8.0M  3.6G   1% /rbd

参考

分类: Storage 标签: ,