Ceph Client 中的 LRU 设计
在 Ceph 中 Client 和 MDS 部分的 lru 实现和淘汰策略稍有不同,但作用都是用来管理 dentry 等结构,方便及时将不使用的文件和目录清出内存,这里主要关注 lru 的实现而不关注 dentry 的淘汰时机。
首先看下 LRU 的结构:
1 | // lru.h |
这里和大部分 LRU 的实现类似, ceph 也使用一个双向链表作为 lru 的基础结构(也就是这里的 xlist,基本可以当作一个双向链表看待)。只不过 ceph 同时使用了三个链表 top、bottom 和 pintail 来细化 lru 的不同位置。
其中 top 和 bottom 比较好理解就是一般 lru 实现中的首尾两端,越靠近 top.front() 则说明最近越多使用,越靠近 bottom.back() 则说明最近越少使用,可以被淘汰。
pintail 稍微特殊一点,在此链中的 dentry 表示正在有人使用(可能是外部应用持有文件的 filehandle,因此随时有可能访问),因此不会被 从 lru 链中淘汰,即使最近没有人使用。
1 | void lru_insert_top(LRUObject *o); |
这里就对应将一个 lru object 插入 lru 链的三个位置(从外部看 lru 是一个链,内部才分为 top 或者 bottom),实际对应就是
1 | top.push_front(&o->lru_link); // 将 object 插入 top 头部 |
另外之前已经说了 xlist 的实现属于是一个双向链表的变种,这里也简单看一下,以 top.push_front 为例:
1 | void push_front(item *i) { |
首先就是如果这个 item(也就是一个 lru object) 已经在某一个链上了,那么先通过 remove 把这个 item 从先前的链上摘下来,这个实现可以避免一个 item 挂在多个链上
1 | // ... |
接着将自己的 _list 指向 this,表示 item 的所属链表(也就是 top)
1 | // ... |
这里就是首先把 item 挂到链表上,因为是 push_front 所以 _prev 为空, _front 是整个链表的头部。
1 | // ... |
同时要更新一下链表的头尾指针,以及整个链表的长度。
接着,必要时 client 通过 lru_get_next_expire 获取最近最少使用的 object 进行 trim,这个过程实际上就是从 lru 中拿出 bottom.back()
1 | LRUObject *lru_get_next_expire() { |
这里 adjust 就是把整个 lru 链中 object 数量的 60%(实际要减去 pinned object)放入 top,剩余部分放入 bottom
接着直接从 bottom 拿出末端 object,如果这个 object 没有 pinned 则直接将其返回,表示此 object 可以被 trim
pin 和 unpin 则是一个特殊的过程, pin 一个指定的 dentry 时我们只需要将对应的 object 从 top 或者 bottom 上移出到 pintail 链上(无关乎首尾,因为 pintail 上的 dentry 都不能 trim),而 unpin 则只需要将其 pintail 中移出到 bottom 的尾部即可
整个 lru 设计相对来说比较容易理解,也非常易于使用。
对于一个特定的 dentry,我们会在初次获取到其 object 时可以将其放至 lru mid,初次使用时将其放至 lru top,接着如果没有使用会随着 lru 的 adjust 逐渐移动到 bottom 然后被 client 取出作为 expired object 淘汰掉,而如果这时通过 pin 加入 pintail 的话就会一直等到 unpin 之后再放到 bottom.back 等待 trim