subprocess一次挂死问题


用python脚本拉起后台进程,拉起的代码是这样的

cmds = cmd.split("|")
previous_result, p = None, None
for c in cmds:
    p = subprocess.Popen(shlex.split(c), stdin=previous_result, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
    previous_result = p.stdout
result, err = p.communicate()

我有两个二进制,一个二进制用的是glog做打印日志,默认输出到stderr,用这个拉起没有问题

另一个二进制使用的print打印日志,默认输出到stdout,用这个拉起会hang住

原因见参考链接1 默认是 shell=True , 如果调用了communicate,表示和stdout交互,除非显式输入EOF,否则会一直等待输入。解决方法就是加上shell=False ,不调用communicate

subprocess.Popen(shlex.split(cmd), stdin=subprocess.PIPE, stdout=subprocess.PIPE,stderr=subprocess.PIPE, close_fds=True, shell=False)

这样输出到stdout的二进制也能拉起。 我考虑过调整日志输出,不输出到stdout, 太麻烦了。作罢


ref

  1. https://stackoverflow.com/questions/2408650/why-does-python-subprocess-hang-after-proc-communicate
Read More

移动构造函数的生成时机


场景是想确定什么时候调用移动构造函数,参考链接有解释

  • X does not have a user-declared copy constructor,

  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator,
  • X does not have a user-declared destructor, and
  • the move constructor would not be implicitly defined as deleted.

比如下面这段代码

#include <iostream>
#include <tuple>

struct A{
A(){std::cout<<"ctor\n";}
};

int main()
{
   A a;
   A b(std::move(a));
}

无法判断

#include <iostream>
#include <tuple>

struct A{
A(){std::cout<<"ctor\n";}
A(const A& a)=delete;//{std::ignore = a; std::cout<<"copy"<<'\n';}
};

int main(){
   A a;
   A b(std::move(a));
}

这样会提示

prog.cc: In function 'int main()':
prog.cc:15:20: error: use of deleted function 'A::A(const A&)'
    A b(std::move(a));
                    ^
prog.cc:7:1: note: declared here
 A(const A& a)=delete;//{std::ignore = a; std::cout<<"copy"<<'\n';}

当有析构的时候也无法生成move ctor,比如

#include <iostream>
#include <tuple>
#include <memory>
struct A{
A(int a=0):a_(std::make_unique<int>(a)){std::cout<<"ctor\n";}
//A(const A& a)=delete;//{std::ignore = a; std::cout<<"copy"<<'\n';}
//A(A&& a){std::ignore = a; std::cout<<"move"<<'\n';}
~A(){std::cout<<"dtor\n";}
std::unique_ptr<int> a_;
};

int main()
{
   A a;
   A b(std::move(a));
}
/*
prog.cc:16:20: error: use of deleted function 'A::A(const A&)'
    A b(std::move(a));
                    ^
prog.cc:5:8: note: 'A::A(const A&)' is implicitly deleted because the default definition would be ill-formed:
 struct A{
        ^
prog.cc:5:8: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]'
In file included from /opt/wandbox/gcc-5.4.0/include/c++/5.4.0/memory:81:0,
                 from prog.cc:4:
/opt/wandbox/gcc-5.4.0/include/c++/5.4.0/bits/unique_ptr.h:356:7: note: declared here
       unique_ptr(const unique_ptr&) = delete;
*/

由于有dtor,没有默认生成move ctor,而是生成了copy ctor,而unique_ptr的copy ctor是删除的导致错误

如何捕捉编译器调用了什么构造函数?有没有例外情况?

貌似汇编能看出来https://godbolt.org/z/Nn4iod


ref

  1. https://zh.cppreference.com/w/cpp/language/move_constructor
  2. https://stackoverflow.com/questions/8283589/are-move-constructors-produced-automatically
Read More

base from member

一个遇到的技巧


场景是这样的,基类需要子类的成员变量来初始化父类

#include <streambuf>  // for std::streambuf
#include <ostream>    // for std::ostream

class fdoutbuf : public std::streambuf {
public:
    explicit fdoutbuf(int fd);
    //...
};

class fdostream : public std::ostream {
protected:
    fdoutbuf buf;
public:
    explicit fdostream(int fd) : buf(fd), std::ostream(&buf) {}
};

这种场景是无法编译通过的,因为需要基类先初始化

但交换初始化列表和基类构造函数的位置

    explicit fdostream(int fd) :  std::ostream(&buf), buf(fd) {}

这样语义不对,buf十有八九是错的。

需要提前把buf初始化。所以加了一个中间层,把buf放到另外一个基类里,让这个基类先于原来的基类。

class fdoutbuf : public std::streambuf {
public:
    explicit fdoutbuf(int fd);
    //...
};

struct fdostream_pbase {
    fdoutbuf sbuffer;
    explicit fdostream_pbase(int fd) : sbuffer(fd) {}
};

class fdostream : private fdostream_pbase, public std::ostream{
    typedef fdostream_pbase  pbase_type;
    typedef std::ostream     base_type;
public:
    explicit fdostream(int fd): pbase_type(fd), base_type(&sbuffer)  {}
    //...
};

就解决了。

boost提供了一个base_from_member类,用于托管你要抽离出来的字段

用法是这样的

#include <boost/utility/base_from_member.hpp>
#include <streambuf>  // for std::streambuf
#include <ostream>    // for std::ostream

class fdoutbuf : public std::streambuf {
public:
    explicit fdoutbuf(int fd);
    //...
};

class fdostream : private boost::base_from_member<fdoutbuf> , public std::ostream {
    // Helper typedef's
    typedef boost::base_from_member<fdoutbuf>  pbase_type;
    typedef std::ostream                        base_type;

public:
    explicit fdostream(int fd) : pbase_type(fd), base_type(&member) {}
    //...
};

base_from_member有个member字段就是要抽离出来托管的字段。

实际上这个类也没那么复杂,自己写一个base_from_member或者自己写个类似的类,不用base_from_member这种模板继承也是可以的

template < typename MemberType, int UniqueID = 0 >
class boost::base_from_member
{
protected:
    MemberType  member;
    base_from_member();
    template< typename T1 >
    explicit  base_from_member( T1 x1 );
    //...
};

ref

  1. 介绍
    1. http://sns.hwcrazy.com/boost_1_41_0/libs/utility/base_from_member.html boost相关中文翻译
    2. http://www.josuttis.com/cppcode/ronsmember.html
    3. https://remonstrate.wordpress.com/tag/base-from-member/
    4. https://remonstrate.wordpress.com/2011/01/26/boost-%e7%9a%84-utility/ 这个博客其实还可以。文章不错
    5. https://stackoverflow.com/questions/4815956/base-from-member-idiom-in-c
    6. https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Base-from-Member
  2. 一个类似的应用例子 https://stackoverflow.com/questions/16613191/stl-initializing-a-container-with-an-unconstructed-stateful-comparator 值得一看
Read More

std::condition_variable::notify_one 使用细节


官方的这个例子,真给我看傻了

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

为什么在notify_one之前需要unlock?

为什么notify_one不用在锁里?不怕丢吗(当然这个例子里不会丢,一共就俩线程)

notify_one有这么个注释

The effects of notify_one()/notify_all() and each of the three atomic parts of wait()/wait_for()/wait_until() (unlock+wait, wakeup, and lock) take place in a single total order that can be viewed as modification order of an atomic variable: the order is specific to this individual condition_variable. This makes it impossible for notify_one() to, for example, be delayed and unblock a thread that started waiting just after the call to notify_one() was made.

The notifying thread does not need to hold the lock on the same mutex as the one held by the waiting thread(s); in fact doing so is a pessimization, since the notified thread would immediately block again, waiting for the notifying thread to release the lock. However, some implementations (in particular many implementations of pthreads) recognize this situation and avoid this “hurry up and wait” scenario by transferring the waiting thread from the condition variable’s queue directly to the queue of the mutex within the notify call, without waking it up.

Notifying while under the lock may nevertheless be necessary when precise scheduling of events is required, e.g. if the waiting thread would exit the program if the condition is satisfied, causing destruction of the notifying thread’s condition_variable. A spurious wakeup after mutex unlock but before notify would result in notify called on a destroyed object.

这个注释能解释第一个notify_one不加锁

另外,wait必须要有条件,无条件wait容易丢失notify 已经写到官方建议里了 https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#cp42-dont-wait-without-a-condition

主要是notify_one多线程消费场景,不知道被谁消费了,所以指定某个满足条件的去wait wait一个触发条件。这样配对用

在这个讨论里, 有个结论,也有人提问

unlocking the mutex before notifying is an optimisation, and not essential. I intentionally didn’t do that, to keep the example simple. There could be a second guideline about that point, but it’s not related to the “always use a predicate” rule. I would object strongly to complicating the example by doing that.

anyway 提前unlock算是个人选择(有优化),不提前unlock也没啥大的影响


说句题外话

最近在看一个时间队列实现,这个cond var用的让我有点迷惑

class TimerQueue {
public:
    TimerQueue() {
        m_th = std::thread([this] { run(); });
    }
 
    ~TimerQueue() {
        cancelAll();
        add(0, [this](bool) { m_finish = true; });
        m_th.join();
    }
 
    uint64_t add(int64_t milliseconds, std::function<void(bool)> handler) {
        WorkItem item;
        item.end = Clock::now() + std::chrono::milliseconds(milliseconds);
        item.handler = std::move(handler);
 
        std::unique_lock<std::mutex> lk(m_mtx);
        uint64_t id = ++m_idcounter;
        item.id = id;
        m_items.push(std::move(item));
        lk.unlock();
 
        // Something changed, so wake up timer thread
        m_checkWork.notify();
        return id;
    }
  ....

注意是先lk.unlock再notify,这个unlock有必要么?

后来发现是用cond var封装了一个 信号量,自己用内部的mtx。和这个没啥关系。

这个代码给我整晕了。rocksdb的timerqueue抄了这个,但是体验没那么迷糊


ref

  • https://www.crazygaze.com/blog/2016/03/24/portable-c-timer-queue/
  • https://en.cppreference.com/w/cpp/thread/condition_variable

Read More

Keepalive

layout: post title: tcp keepalive categories: [linux] tags: [tcp,keepalive,c]


int opt_val = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &opt_val, sizeof(opt_val)) 

启用后,socket 默认的检测参数使用内核参数,使用 sysctl -a|grep tcp_keepalive查看,或者使用以下命令查看:

cat /proc/sys/net/ipv4/tcp_keepalive_time
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_probes

修改内核的参数可以使用 vim /etc/sysctl.conf 修改,然后使用 sysctl -p 应用

如果需要设置自定义的心跳参数,则需要使用 setsockopt 函数设置:

setsockopt (sock_fd,  SOL_TCP,  TCP_KEEPIDLE,  &idle, sizeof(idle)) 
setsockopt (sock_fd,  SOL_TCP,  TCP_KEEPINTVL,  &intvl, sizeof(intvl)) 
setsockopt (sock_fd,  SOL_TCP,  TCP_KEEPCNT,  &cnt,  sizeof(cnt))

以上几个参数含义为:

  • TCP_KEEPIDLE:多久没有交互时,发送一个心跳包
  • TCP_KEEPINTVL:每次探活间隔多久
  • TCP_KEEPCNT:一共探活多少次
Read More

raft

基本概念

三个角色

  • Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
  • Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
  • Candidate:Leader选举过程中的临时角色。

优化角色

  • Learner:同Follower,但不能投票
  • Witness:能投票,不能当Leader,不用同步snapshot,为了保证log写扩散开

一图流

Source file: mmd/raft-v1.mmd:

flowchart LR Follower -->|超时参与选举| Candidate Witness -->|超时参与投票| Candidate Follower --- |心跳|Leader Leader --> |转移让出权限|Witness Candidate --> |超时重新选举| Candidate Candidate --> |收到大多数投票成为|Leader Leader --> |发现更高的term|Follower Candidate --> |发现新的term
发现新的leader|Follower Leader --- |读写扩散|Learner Learner --- |读写扩散|Learner Learner <--> |角色转换/读写扩散|Follower

learner和follower互相转换较为复杂,不如下线重新上线这种添加更干净一些

可以看etcd 的那个learner在角色变换中的处理,太恶心了

Read More

(转)beansdb

是beansdb的一些资料整理和总结

转自这里 http://sunisdown.me/gobeansdb-jia-gou-she-ji.html

为什么要自己实现一套 k/v 存储

我在刚刚接手 GoBeansDB 的时候,想过这个问题。既然有那么多优秀的数据库系统,为什么豆瓣还需要自己重新实现一套 k/v 存储? 这个问题可以拆分成两个方面,一个是为什么要用 K/V 数据库。一个是为什么要自己造轮子。

  1. 首先是因为数据大,而且数据是非结构化数据,像豆瓣的日记,就是一批很长的字符串。
  2. 其次是非常多的随机读。
  3. 有的时候会有大量的写操作
  4. 不需要外键什么的

上面四点可以排除掉类似 MySQL 这种传统的关系型数据库。

排除掉传统的关系行数据库之后,就需要对比现存的 K/V 数据库。

现在比较流行的 K/V 数据库有 LevelDBMongoDB ,还有 Cassandra ,现在看来这些项目都足够成熟。但是如果追溯到 BeansDB 项目最开始的时候,也就是 2012 年的时候,那个时间点并没有太好的选择。即使现在看来,除了 Cassandra 之外,像 LevelDBMongoDB 也不能满足我们的目标:

  1. 读写都需要比较低的 latency
  2. 数据量非常大,所以数据要写在磁盘上,数据库需要能够容纳比内存大的多的数据
  3. 高可用,单点故障不影响系统正常运行
  4. 高吞吐,尤其是针对写操作
  5. 能够快速恢复有问题的节点

这 5 点也可以排除调 MongoDBLevelDB

当然上面这些都是我做的推断,但是这些应该都不是最主要的原因。最主要的原因应该是豆瓣的工程师文化比较好,鼓励工程师去寻找一个最贴合业务的解决方案,并且这个工程师的团队还足够强,两者缺一不可。如果没有很强的工程师文化,可能会直接引入开源的解决方案,虽然不一定合适,但是应该足够解决痛点。如果工程师实力不够,也就没有能力去自己实现一套类似的系统。而且与其去引入一个复杂的,自己无法完全掌控的开源项目,不如自己实现一套贴合业务的,简单的系统。这样内部可以根据业务的需要来作调整,同时自己实现一个系统也比完全掌握一个庞大的开源项目要简单。一旦出现问题也比较容易找到问题所在。

为什么要用 Go 重新实现 BeansDB

BeansDB 是用 C 来实现的,为什么现在改用 Go 来实现?

  • 一个很重要的原因是 Go 的代码相比与 C 更容易维护。对一个工程师而言,Go 的学习成本比 C 要低很多。
    • 用 Go 可以比较快速的写出健壮的系统,而用 C 来写的话,则需要一定的经验积累。
    • Go 提供了可用的测试框架,写测试相对于 C/C++ 要方便
    • Go 标准库里面提供了方便的性能分析工具,用 C 也有类似的工具,但是做不到开箱即用
  • 还有 Go 的标准库也足够完善,不需要用 C 来重复造轮子。
  • Go 的执行效率虽然比 C 差,但是 BeansDB 的瓶颈是 IOPS,所以 Go 的执行效率并不会成为瓶颈。

GoBeansDB 的架构设计

GoBeansDB 是基于 DynamoBitcask 两篇论文来做的实现,这里优先讨论基于 Bitcask 实现的存储引擎。Bitcask 有一个缺点在于所有的 key 值都必须放在内存里面,GoBeansDB 这这个基础之上做了一些优化,绕过了 Bitcask 这个痛点。

GobeansDB 的存储有有两个比较重要的组成部分,一个是索引(htree),一个是数据文件(data)。索引与数据文件组成 Bucket。Bucket 的概念类似与关系行数据库里面的 table,在 GoBeansDB 的实现中就是给一个 Bucket 分配一个文件夹,这个文件夹下面放着相关的数据。每个 Bucket 下面一次只允许打开一个文件。打开的这个文件会一直保持打开的状态,一直等到追加到活跃文件超出阈值。文件超出阈值之后就关闭,然后新建一个新的继续添加。data 文件一旦关闭之后,文件就转换成为不活跃的数据文件。无法再往这个 data 文件上面追加数据。

img

状态为 active 的数据文件只做追加操作,这样连续的写入操作也不会明显增加磁盘的 IO 使用量。这种设计也极大的简化了数据的写入操作。

上面的图简单描述了 Bucket 内部文件的架构,每条数据里面包含TS(TimeStamp),Flag,Ver(Version),ValueHash,RecSize(单条记录的主要内容的大小),Value,crc(key,value,header 的 crc),ksz(Key Size),vsz(Value Size),pos(Position,这条记录在文件中的位置),Header。

当插入新数据的时候,直接在文件尾部添加这种结构的数据。删除操作是对原有的数据做更新操作,并将 Ver 绝对值+1,转变为负数。

在文件写入完成之后,需要更新内存里面的数据结构,也就是前面提到的 HTree,HTree 是一个 Hash Tree,结构如下

img

levels 表示真实的树状结构, leafs 是树的叶子,保存着真实的数据。

img

这种数据结构下读取一个值也非常简单,大多数情况下最多只需要一次 seek 。我们首先在 Htree 中通过 levels 找到 key 对应的 leafs , 然后通过 leafs 里面的报错的 Pos 来拿到文件编号(chunkID)以及 offset,这样就可以通过文件编号(chunkID)和 offset 来拿到保存的数据。在很多情况下文件系统的缓存会让这个读操作比预期的要快。

到这里关于 GoBeansDB wirte/delete/read 相关的操作都已经基本完成。但是仅仅这样还不能完备。

GC 操作

GoBeansDB 的模型非常简单,write/delete 操作都是在文件尾部追加新的数据,这样存在一个问题就是占用的磁盘空间比真实的数据要多。所以我们引入了 GC 机制来回收垃圾,释放内存与磁盘空间。

在 GoBeansDB 中,通过增量 GC 来减小 GC 的开销。xiufeng 通过分析 BeansDB 的日志,统计出一条新写入的数据,修改操作基本在写入之后的 7 天之内,所以我们保留 7 天之内的新数据不做 GC。然后在每天晚上,访问量较低的时候,分批做增量 GC。

GC 的过程是将 datafile 里面已经过时的数据清除掉,比如旧版本的value,已经标记为删除掉的key。

img

如 上图所示,GC 会读取状态为不活跃的数据文件,用其中存活的数据或者最新版本的数据生成一份新的数据文件,同时为这个新的数据文件创建一个 hint file。


c的代码走读

hstore

main -> 各种init -> store = hs_open(dbhome, height, before_time, settings.num_threads); -> bc_scan -> scanHintFile根据hintRecord能重建 hashtree -> thread_init(settings.num_threads); -> pthread_create(&flush_id, NULL, do_flush, NULL) -> loop_run(settings.num_threads);

hs是重点 应该代表hashtree,会根据hashtree的高度创建 2 的 4* 高度次方个bitcask实例。然后各个实例下面来进行实际的动作

inline int get_index(HStore *store, char *key)
{
    if (store->height == 0) return 0;
    uint32_t h = fnv1a(key, strlen(key));
    return h >> ((8 - store->height) * 4);
}

hstore有锁

struct t_hstore {
    int height, count;
    time_t before;
    int scan_threads;
    int op_start, op_end, op_limit; // for optimization
    Mgr* mgr;
    pthread_mutex_t locks[NUM_OF_MUTEX];
    Bitcask* bitcasks[];
};

这个锁只在hs_append和hs_incr上会加锁,因为涉及到get

bitcask内部有三个锁

struct bitcask_t {
    uint32_t depth, pos;
    time_t before;
    Mgr    *mgr;
    HTree  *tree, *curr_tree;
    int    last_snapshot;
    int    curr;
    uint64_t bytes, curr_bytes;
    char   *write_buffer;
    time_t last_flush_time;
    uint32_t    wbuf_size, wbuf_start_pos, wbuf_curr_pos;
    pthread_mutex_t flush_lock, buffer_lock, write_lock;
    int    optimize_flag;
};

本身是一个服务而不是一个嵌入式的库,锁多可以理解。

CURD

hs_set -> get_index -> bc_set -> pthread_mutex_lock(&bc->write_lock); -> ht_get 根据key查hashtree拿到value的指针。可以比较value hash,如果hash相同,区分不出,那就去拿到实际的value比较value bc_get(bc, key); -> 有版本信息,ht_add(bc->tree, key, it->pos, it->hash, ver); 更新版本 通常逻辑,走append -> pthread_mutex_lock(&bc->buffer_lock); -> 如果空间不够走bc_flush -> 重新分配buffer -> bc_rotate -> build_thread -> build_hint -> ht_add(bc->curr_tree, key, pos, hash, ver); 这里tree和curr_tree都更新了。为啥需要两个hashtree? -> tree也有个锁 pthread_mutex_lock(&tree->lock);

hs_get -> get_index -> bc_get DataRecord -> ht_get -> check_key -> 锁tree -> get_item_hash 虽然是hashtree,但是get没用上hash,前面用到了,tree内部还是对比key

hashtree 和hint文件的关系

hashtree 构成,Node -> Data -> Item

struct t_item {
    uint32_t pos;
    int32_t  ver;
    uint16_t hash;
    uint8_t  length;
    char     key[1];
};

如何定位到Node?

通过build_hint生成hint -> ht_visit(tree, collect_items, &p); -> lock, visit node, -> collect_items -> write_hint_file(p.buf, p.curr, hintpath);

typedef struct hint_record {
    uint32_t ksize:8;
    uint32_t pos:24;
    int32_t version;
    uint16_t hash;
    char key[NAME_IN_RECORD]; // allign
} HintRecord;

hint文件是key hash pos,通过这个就构建出hashtree了

void scanHintFile(HTree* tree, int bucket, const char* path, const char* new_path)
{
    HintFile* hint = open_hint(path, new_path);
    char *p = hint->buf, *end = hint->buf + hint->size;
    while (p < end) {
        HintRecord *r = (HintRecord*) p;
        p += sizeof(HintRecord) - NAME_IN_RECORD + r->ksize + 1;
        if (p > end){
            break;
        }
        uint32_t pos = (r->pos << 8) | (bucket & 0xff);
        if (r->version > 0)
            ht_add2(tree, r->key, r->ksize, pos, r->hash, r->version);
        else
            ht_remove2(tree, r->key, r->ksize);
    }
    close_hint(hint);
}

这个hint用rocksdb存也是可以的,scan一遍就重新构造出来了。问题在于什么时候hint文件落地?会存在hashtree和hint不对应的场景么?

越看越眼熟。这和blobdb差不太多吧,无非索引有差别,一个是hashtree一个是skiplist

如果把rocksdb用hashtable替换skiplist我估计差不多吧

ref

  1. https://blog.csdn.net/lemonk3664/article/details/9970293
  2. 作者的记录 比较bitcask https://www.douban.com/note/122507891/
  3. https://github.com/douban/beansdb 代码
  4. https://github.com/douban/gobeansdb/ 后面他们用go实现了
  5. https://zhuanlan.zhihu.com/p/53682577 提到了c版本的优化,位域之类的
Read More

pebblesdb


是pebblesdb的一些资料整理和总结

参考链接1 是论文

参考链接2是提到的一点介绍

而在 2017 年 SOSP 上发表的论文 PebblesDB 提出了另外一种思路。在传统 LSM-Tree 中,每一层由多个 SSTable 组成,每一个 SSTable 中保存了一组排好序 Key-Value,相同层的 SSTable 之间的 Key 没有重叠。当进行 Compaction 时,上层的 SSTable 需要与下层的 SSTable 进行合并,也就是将上层的 SSTable 和下层的 SSTable 读取到内存中,进行合并排序后,组成新的 SSTable,并写回到磁盘中。由于 Compaction 的过程中需要读取和写入下层的 SSTable,所以造成了读写放大,影响应能。

PebblesDB 将 LSM-Tree 和 Skip-List 数据结构进行结合。在 LSM-Tree 中每一层引入 Guard 概念。 每一层中包含多个 Guard,Guard 和 Guard 之间的 Key 的范围是有序的,且没有重叠,但 Guard 内部包含多个 SSTable,这些 SSTable 的 Key 的范围允许重叠。

当需要进行 Compaction 时,只需要将上层的 SSTable 读入内存,并按照下层的 Guard 将 SSTable 切分成多个新的 SSTable,并存放到下层对应的 Guard 中。在这个过程中不需要读取下层的 SSTable,也就在一定程度上避免了读写放大。作者将这种数据结构命名为 Fragemented Log-Structured Tree(FLSM)。PebblesDB 最多可以减低 6.7 倍的写放大,写入性能最多提升 105%。

和 WiscKey 类似,PebblesDB 也会多 Range Query 的性能造成影响。这是由于 Guard 内部的 SSTable 的 Key 存在重叠,所以在读取连续的 Key 时,需要同时读取 Guard 中所有的 SSTable,才能够获得正确的结果。

参考链接3pebblesdb的总结,博主写的不错

skiplist的访问选择是一种guard,将这种guard推广到lsm上,然后通过guard组织数据,允许小范围的重复,但是这种guard是很难判定的。所以借鉴意义不大,而且读数据性能肯定也会下降,insert等操作也会加剧。

参考链接3也是pebblesdb的总结,

提到一个观点

弱化全局有序,切分片段,这样实际上加重了有序对系统的影响

比如scan,读性能肯定会下降。

参考链接5 6 是代码

参考链接7是个ppt介绍。值得看一看。方便理解论文


ref

  1. http://www.cs.utexas.edu/~vijay/papers/sosp17-pebblesdb.pdf
  2. https://zhuanlan.zhihu.com/p/34455548
  3. https://zhuanlan.zhihu.com/p/46069535
  4. https://zhuanlan.zhihu.com/p/32225460
  5. https://github.com/utsaslab/pebblesdb
  6. https://github.com/xxks-kkk/HyperPebblesDB
  7. http://www.cs.utexas.edu/~vijay/papers/pebblesdb-sosp17-slides.pdf
Read More

Abseil Tip of the Week

本文是Abseil库 tip of the week的总结。不是翻译,有些点子还是不错的。


totw #1 string_view

厌烦了const char*到string之间的处理转换?你只是想用一下而已不需要构造一个拷贝?string_view就是为此而生的,它是一个视图,就是一个借用,也是类似go rust胖指针 slice之类的东西。内部有一个指针和一个长度

注意,string_view是没有\0的

totw #3 String Concatenation and operator+ vs. StrCat()

简单说,不要用string::operator +() 会有临时变量。absl::StrCat用来解决这个问题

totw #10 Splitting Strings, not Hairs

absl提供了string split相关函数

// Splits on commas. Stores in vector of string_view (no copies).
std::vector<absl::string_view> v = absl::StrSplit("a,b,c", ',');

// Splits on commas. Stores in vector of string (data copied once).
std::vector<std::string> v = absl::StrSplit("a,b,c", ',');

// Splits on literal string "=>" (not either of "=" or ">")
std::vector<absl::string_view> v = absl::StrSplit("a=>b=>c", "=>");

// Splits on any of the given characters (',' or ';')
using absl::ByAnyChar;
std::vector<std::string> v = absl::StrSplit("a,b;c", ByAnyChar(",;"));

// Stores in various containers (also works w/ absl::string_view)
std::set<std::string> s = absl::StrSplit("a,b,c", ',');
std::multiset<std::string> s = absl::StrSplit("a,b,c", ',');
std::list<std::string> li = absl::StrSplit("a,b,c", ',');

// Equiv. to the mythical SplitStringViewToDequeOfStringAllowEmpty()
std::deque<std::string> d = absl::StrSplit("a,b,c", ',');

// Yields "a"->"1", "b"->"2", "c"->"3"
std::map<std::string, std::string> m = absl::StrSplit("a,1,b,2,c,3", ',');

要是c++有python那种split就好了。(那种效率比较低)

totw #11 RVO 返回值优化。返回局部变量不必考虑多余的拷贝构造等。现代编译器默认功能
totw #42: Prefer Factory Functions to Initializer Methods

google是禁止使用异常的。如果初始化会失败,那就用工厂函数来搞,大概这样

class Foo {
 public:
  // Factory method: creates and returns a Foo.
  // May return null on failure.
  static std::unique_ptr<Foo> Create();

  // Foo is not copyable.
  Foo(const Foo&) = delete;
  Foo& operator=(const Foo&) = delete;

 private:
  // Clients can't invoke the constructor directly.
  Foo();
};

std::unique_ptr<Foo> Foo::Create() {
  // Note that since Foo's constructor is private, we have to use new.
  return absl::WrapUnique(new Foo());
}

这里注意,std::unique_ptr不能访问private构造函数,所以absl提供了一个wrapunique的一个wrapper

totw #45 不要用全局变量,尤其是库代码

(只能说尽量啦)

totw #88: Initialization: =, (), and {}

总结好了

  • 简单的构造逻辑,直接用=初始化基本类型(结合{}),结构体,以及拷贝构造

    int x = 2;
    std::string foo = "Hello World";
    std::vector<int> v = {1, 2, 3};
    std::unique_ptr<Matrix> matrix = NewMatrix(rows, cols);
    MyStruct x = {true, 5.0};
    MyProto copied_proto = original_proto;
    
    // Bad code
    int x{2};
    std::string foo{"Hello World"};
    std::vector<int> v{1, 2, 3};
    std::unique_ptr<Matrix> matrix{NewMatrix(rows, cols)};
    MyStruct x{true, 5.0};
    MyProto copied_proto{original_proto};
    
  • 用传统的构造函数语义()来调用复杂的构造语义

    Frobber frobber(size, &bazzer_to_duplicate);
    std::vector<double> fifty_pies(50, 3.14);
    // Bad code 
    // Could invoke an intializer list constructor, or a two-argument   constructor.
    Frobber frobber{size, &bazzer_to_duplicate};
    
    // Makes a vector of two doubles.
    std::vector<double> fifty_pies{50, 3.14};
    
  • {}不用=这种场景, 只用在上面这两种场景不能编译的情况下

    class Foo {
     public:
      Foo(int a, int b, int c) : array_{a, b, c} {}
      
     private:
      int array_[5];
      // Requires {}s because the constructor is marked explicit
      // and the type is non-copyable.
      EventManager em{EventManager::Options()};
    };
    
  • {}auto不要混用

    // Bad code
    auto x{1};
    auto y = {2}; // This is a std::initializer_list<int>!
    
totw #93: using absl::Span 也就是std::span ,container_view,更好用。也可以理解成flat pointer
totw #117: Copy Elision and Pass-by-value

就是一种吃掉拷贝的优化

// First constructor version
explicit Widget(const std::string& name) : name_(name) {}

// Second constructor version
explicit Widget(std::string name) : name_(std::move(name)) {}
totw #120: Return Values are Untouchable
MyStatus DoSomething() {
  MyStatus status;
  auto log_on_error = RunWhenOutOfScope([&status] {
    if (!status.ok()) LOG(ERROR) << status;
  });
  status = DoA();
  if (!status.ok()) return status;
  status = DoB();
  if (!status.ok()) return status;
  status = DoC();
  if (!status.ok()) return status;
  return status;
}

这段代码是有问题的,lambda中访问的status是在return执行完之后才会析构访问的,但是鉴于返回值优化,return没有执行,勉强正确,如果最后一行换成return MyStatus(); 这个lambda将永远错误的status,行为是未定义的 ,可能是错误的逻辑

解决办法,不用这种RAII访问返回值

totw #123: absl::optional and std::unique_ptr

这个表概括的很好了,absl::optional就是std::optional的替代版

  Bar absl::optional<Bar> std::unique_ptr<Bar>
Supports delayed construction  
Always safe to access    
Can transfer ownership of Bar    
Can store subclasses of Bar    
Movable If Bar is movable If Bar is movable
Copyable If Bar is copyable If Bar is copyable  
Friendly to CPU caches  
No heap allocation overhead  
Memory usage sizeof(Bar) sizeof(Bar) + sizeof(bool)2 sizeof(Bar*) when null, sizeof(Bar*) + sizeof(Bar) otherwise
Object lifetime Same as enclosing scope Restricted to enclosing scope Unrestricted
Call f(Bar*) f(&val_) f(&opt_.value()) or f(&*opt_) f(ptr_.get()) or f(&*ptr_)
Remove value N/A opt_.reset(); or opt_ = absl::nullopt; ptr_.reset(); or ptr_ = nullptr;
totw#126: make_unique is the new new
How Should We Choose Which to Use?
  1. By default, use absl::make_unique() (or std::make_shared() for the rare cases where shared ownership is appropriate) for dynamic allocation. For example, instead of: std::unique_ptr<T> bar(new T()); write auto bar = absl::make_unique<T>(); and instead of bar.reset(new T()); write bar = absl::make_unique<T>();
  2. In a factory function that uses a non-public constructor, return a std::unique_ptr<T> and use absl::WrapUnique(new T(...)) in the implementation.
  3. When dynamically allocating an object that requires brace initialization (typically a struct, an array, or a container), use absl::WrapUnique(new T{...}).
  4. When calling a legacy API that accepts ownership via a T*, either allocate the object in advance with absl::make_unique and call ptr.release() in the call, or use new directly in the function argument.
  5. When calling a legacy API that returns ownership via a T*, immediately construct a smart pointer with WrapUnique (unless you’re immediately passing the pointer to another legacy API that accepts ownership via a T*).
Summary

Prefer absl::make_unique() over absl::WrapUnique(), and prefer absl::WrapUnique() over raw new.

totw #131: Special Member Functions and = default

Prefer =default over writing an equivalent implementation by hand, even if that implementation is just {}

totw#134: make_unique and private constructors

和42条一样场景,make_unique不能直接创建private 构造函数,几个办法

声明成友元函数,或者用个wrapunique,或者替换成shared_ptr

totw #141: Beware Implicit Conversions to bool

bool转换的问题。属于老生常谈了。特定类型就需要实现safe bool,一般类型用option<T>包装一层 absl::optional<T>::has_value()判断

Tote #144 : Heterogeneous Lookup in Associative Containers

看代码

struct StringCmp {
  using is_transparent = void;
  bool operator()(absl::string_view a, absl::string_view b) const {
    return a < b;
  }
};

std::map<std::string, int, StringCmp> m = ...;
absl::string_view some_key = ...;
// The comparator `StringCmp` will accept any type that is implicitly
// convertible to `absl::string_view` and says so by declaring the
// `is_transparent` tag.
// We can pass `some_key` to `find()` without converting it first to
// `std::string`. In this case, that avoids the unnecessary memory allocation
// required to construct the `std::string` instance.
auto it = m.find(some_key);

省一个拷贝

再比如

struct ThreadCmp {
  using is_transparent = void;
  // Regular overload.
  bool operator()(const std::thread& a, const std::thread& b) const {
    return a.get_id() < b.get_id();
  }
  // Transparent overloads
  bool operator()(const std::thread& a, std::thread::id b) const {
    return a.get_id() < b;
  }
  bool operator()(std::thread::id a, const std::thread& b) const {
    return a < b.get_id();
  }
  bool operator()(std::thread::id a, std::thread::id b) const {
    return a < b;
  }
};

std::set<std::thread, ThreadCmp> threads = ...;
// Can't construct an instance of `std::thread` with the same id, just to do the lookup.
// But we can look up by id instead.
std::thread::id id = ...;
auto it = threads.find(id);
totw #149 Object Lifetimes vs. =delete

不只是构造函数析构函数可以标记为delete,普通成员函数也可以标记delete。这就引入了复杂性问题,是让类更完备还是更复杂

=delete for Lifetimes

假如你标记为delete就是为了限制参数的生存周期

class Request {
  ...

  // The provided Context must live as long as the current Request.
  void SetContext(const Context& context);
  void SetContext(Context&& context) = delete;

但是报错并不能告诉你正确的做法,只会告诉你因为什么错了

error: call to deleted function 'SetContext'

  SetContext(Context{});
  ^~~~~~~~~~

<source>:4:6: note: candidate function has been explicitly deleted

void SetContext(Context&& context) = delete;

你看了看报错,决定改一下,但是为什么这样改,不知道

这种设计会避免bug,但是也需要完善注释,不然不知所以

要是编译时报错能够定制错误信息,就牛逼了。

=delete for “Optimization”

这个场景是上面的反面,假如只接受右值,不接受拷贝,这样所有调用的时候都得显式加上std::move,显然更麻烦

这样做实际上是复杂化了,totw不建议使用,keep it simple

Tip of the Week #152 : AbslHashValue and You

把内部用的hash算法暴露出来了,可以这么用

struct Song {
  std::string name;
  std::string artist;
  absl::Duration duration;

  template <typename H>
  friend H AbslHashValue(H h, const Song& s) {
    return H::combine(std::move(h), s.name, s.artist, s.duration);
  }

  // operator == and != omitted for brevity.
};

Tip of the Week #161: Good Locals and Bad Locals

Good

auto& subsubmessage = *myproto.mutable_submessage()->mutable_subsubmessage();
subsubmessage.set_foo(21);
subsubmessage.set_bar(42);
subsubmessage.set_baz(63);

Bad

MyType value = SomeExpression(args);
return value;

Tip of the Week #171: Avoid Sentinel Values

返回值不清晰,建议用optional或者status

Tip of the Week #173: Wrapping Arguments in Option Structs

多参数构造抽象成option,不然难维护,构造指定不合理

Tip of the Week #175: Changes to Literal Constants in C++14 and C++17.

尽可能用1’000’000’000

以及chrono的ms min

Tip of the Week #176: Prefer Return Values to Output Parameters

尽可能用返回值。如果出参需要非常灵活,返回值控制不了,就用出参

Tip of the Week #181: Accessing the value of a StatusOr<T>

就是rocksdb status那种东西

// The same pattern used when handling a unique_ptr...
std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
  foo->DoBar();  // use the value object
}

// ...or an optional value...
absl::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
  foo->DoBar();
}

// ...is also ideal for handling a StatusOr.
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
  foo->DoBar();
}

表达能力比option和expect要好一些

此外,abseil还支持解析参数,支持gflags,可以说是把gflags迁移到这个库了,header only更友好一些


ref

  1. https://abseil.io/tips/
Read More


^