valgrind sup文件的作用以及生成


redis的runtest支持valgrind,里面有这么一条

if {$::valgrind} {
        set pid [exec valgrind --track-origins=yes --suppressions=src/valgrind.sup --show-reachable=no --show-possibly-lost=no --leak-check=full src/redis-server $config_file > $stdout 2> $stderr &]
    } elseif ($::stack_logging) {
        set pid [exec /usr/bin/env MallocStackLogging=1 MallocLogFile=/tmp/malloc_log.txt src/redis-server $config_file > $stdout 2> $stderr &]
    } else {
        set pid [exec src/redis-server $config_file > $stdout 2> $stderr &]
    }

里面引用到sup文件https://github.com/antirez/redis/blob/unstable/src/valgrind.sup

sup表示suppress,避免valgrind出错的意思,这个文件定义一系列规则,valgrind检测的时候跳过这些触发条件,比如redis的是这样的

{
   <lzf_unitialized_hash_table>
   Memcheck:Cond
   fun:lzf_compress
}

{
   <lzf_unitialized_hash_table>
   Memcheck:Value4
   fun:lzf_compress
}

{
   <lzf_unitialized_hash_table>
   Memcheck:Value8
   fun:lzf_compress
}

对于提示unitialized hash table提醒,跳过不处理

再比如我遇到的一个epoll提示

...
==13711== Syscall param epoll_ctl(event) points to uninitialised byte(s)
==13711==    at 0x6E30CBA: epoll_ctl (in /usr/lib64/libc-2.17.so)
...
==13711==  Address 0xffefffad8 is on thread 1's stack
==13711==  in frame #1, created by xxxx
==13711==  Uninitialised value was created by a stack allocation
==13711==    at 0x561500: xxxx

这个很明显,valgrind有问题,这段代码没问题(问题不大)却告警了

  struct epoll_event ee;
  ee.data.fd = fd;
  ee.events = mask;
  return epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ee);

这个原因见参考链接,实际上需要加个memset,由于padding问题。

但是这步完全可以省掉,生成类似的sup规则

{
   <epoll_add>
   Memcheck:Param
   epoll_ctl(event)
   fun:epoll_ctl
}

读入,就会避免这些告警

如何生成sup规则?

规则肯定不是自己手写的,如果很多告警挨个手写也太低效了,实际上valgrind支持导出sup规则 ,见参考链接, 比如二进制是minimal

valgrind --leak-check=full --show-reachable=yes --error-limit=no --gen-suppressions=all --log-file=minimalraw.log ./minimal

另外,valgrind可以指定log文件。我一直都是重定向 >log 2>&1 特傻

也可以不像redis这样读入,直接写到 .valgrindrc里,套路类似bashrc(不过有污染也不好)

参考链接中的文章用了两步,先是上面这个命令,提取出log,然后log中已经有一系列sup信息了,再通过一个脚本parse一下。不过据我分析,第一步就能整理出来,不过有重复,冗余比较多,可以整理成上面epoll_ctl这种形式的四行一组就行了。

详情可以读参考链接中的文章。写的很好

ref

contact

Read More

一个 Valgrind Address is on Thread's 1 stack 搞笑场景


用valgrind扫了一遍模块,有个函数附近报错 ,简单说这个函数是读取配置文件,解析配置文件保存,提示Address is on Thread’s 1 stack,各种string越界

搜索了好几个案例,见参考链接,我仔细检查了这一系列函数,最后发现了问题。比较搞笑,就记录下来了

bool Config::Load()
{ 
  if (!FileExists(path_)) {
    return -1;
  }
  // read conf items

  char line[CONFLEN];
  char name[CONFLEN], value[CONFLEN];
  int line_len = 0;
  int name_len = 0, value_len = 0;
  int sep_sign = 0;
...
}

这个CONFLEN的长度是

static const int CONFLEN = 1024 * 1024;

也就是说是物理意义上的栈溢出。。我还以为代码有bug写穿了。。改小,改成一半,告警就消失了

案例中的第四个链接有点类似

ref

contact

Read More

valgrind跑一个rocksdb应用出现错误,以及背后的write hint


valgrind 3.10 日志是这样的

valgrind: m_syswrap/syswrap-linux.c:5255 (vgSysWrap_linux_sys_fcntl_before): Assertion 'Unimplemented functionality' failed.
valgrind: valgrind

host stacktrace:
==26531==    at 0x3805DC16: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x3805DD24: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x3805DEA6: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x380D53A0: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x380B0834: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x380AD242: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x380AE6F6: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)
==26531==    by 0x380BDD7C: ??? (in /usr/lib64/valgrind/memcheck-amd64-linux)

sched status:
  running_tid=1

Thread 1: status = VgTs_Runnable
==26531==    at 0x60903A4: fcntl (in /usr/lib64/libpthread-2.17.so)
==26531==    by 0x53571F6: rocksdb::PosixWritableFile::SetWriteLifeTimeHint(rocksdb::Env::WriteLifeTimeHint) (io_posix.cc:897)
...

找到rocksdb代码,接口是这样的

env/io_posix.cc

void PosixWritableFile::SetWriteLifeTimeHint(Env::WriteLifeTimeHint hint) {
#ifdef OS_LINUX
// Suppress Valgrind "Unimplemented functionality" error.
#ifndef ROCKSDB_VALGRIND_RUN
  if (hint == write_hint_) {
    return;
  }
  if (fcntl(fd_, F_SET_RW_HINT, &hint) == 0) {
    write_hint_ = hint;
  }
#else
  (void)hint;
#endif // ROCKSDB_VALGRIND_RUN
#else
  (void)hint;
#endif // OS_LINUX
}

…结果显然了,不支持fcntl F_SET_RW_HINT选项。

注意:

  • 如果需要跑valgrind,编译的rocksdb需要定义ROCKSDB_VALGRIND_RUN
  • 如果有必要,最好也定义PORTABLE,默认是march=native可能会遇到指令集不支持

特意搜了一下,这个参数是这样定义的

env/io_posix.cc

#if defined(OS_LINUX) && !defined(F_SET_RW_HINT)
#define F_LINUX_SPECIFIC_BASE 1024
#define F_SET_RW_HINT (F_LINUX_SPECIFIC_BASE + 12)
#endif

在linux中是这样的

https://github.com/torvalds/linux/blob/dd5001e21a991b731d659857cd07acc7a13e6789/include/uapi/linux/fcntl.h#L53

/*
 * Set/Get write life time hints. {GET,SET}_RW_HINT operate on the
 * underlying inode, while {GET,SET}_FILE_RW_HINT operate only on
 * the specific file.
 */
#define F_GET_RW_HINT		(F_LINUX_SPECIFIC_BASE + 11)
#define F_SET_RW_HINT		(F_LINUX_SPECIFIC_BASE + 12)
#define F_GET_FILE_RW_HINT	(F_LINUX_SPECIFIC_BASE + 13)
#define F_SET_FILE_RW_HINT	(F_LINUX_SPECIFIC_BASE + 14)

另外这里也移植了一个https://github.com/riscv/riscv-gnu-toolchain/blob/master/linux-headers/include/linux/fcntl.h

write life time hints

搜到了这个实现的patch https://patchwork.kernel.org/patch/9794403/

和这个介绍,linux 4.13引入的https://www.phoronix.com/scan.php?page=news_item&px=Linux-4.13-Write-Hints

简单说,这是为NVMe加上的功能,暗示写入数据是ssd的话,调度就会把数据尽可能靠近,这样方便后续回收

patchset讨论里还特意说到了rocksdb。。。https://lwn.net/Articles/726477/

降低写放大十分可观

A new iteration of this patchset, previously known as write streams. As before, this patchset aims at enabling applications split up writes into separate streams, based on the perceived life time of the data written. This is useful for a variety of reasons:

  • For NVMe, this feature is ratified and released with the NVMe 1.3 spec. Devices implementing Directives can expose multiple streams. Separating data written into streams based on life time can drastically reduce the write amplification. This helps device endurance, and increases performance. Testing just performed internally at Facebook with these patches showed up to a 25% reduction in NAND writes in a RocksDB setup.

  • Software caching solutions can make more intelligent decisions on how and where to place data.

这背后又有一个NVMe特性,Stream ID,https://lwn.net/Articles/717755/

一个用法,见pg的patch以及讨论 https://www.postgresql.org/message-id/CA%2Bq6zcX_iz9ekV7MyO6xGH1LHHhiutmHY34n1VHNN3dLf_4C4Q%40mail.gmail.com

这里还没有更深入讨论。只是罗列了资料。下班了,就到这里

ref

contacts

Read More


lsm-tree延迟分析


  • L0满, 无法接收 write-buffer不能及时Flush,阻塞客户端
    • 高层压缩占用IO
    • L0 L1压缩慢
    • L0 空间少
  • Flush 太慢,客户端阻塞 -> rate limiter限速

rocksdb的解决方案就是rate limiter限速

slik思路

(1)优先flushing和lower levels的compaction,低level的compaction优先于高level的compaction,这样保证flushing能尽快的写入level 0,level 0尽快compaction level 1,(a)尽量避免因为memtable到达上限卡client I/O,(b)尽量避免因为level 0的sstable文件数到达上限卡client I/O。实际实现的时候会有一个线程池专门用于Flushing;另外一个线程池用于其他的Compaction

(2)Preempting Compactions:支持抢占式Compaction,也就是高优先级的compaction可以抢占低优先级的compaction,也就是说另外一个用于L0~LN之间的compaction的线程池,优先级高的low level compaction可以抢占high level的compaction,这样L0->L1在这个线程池,最高优先级的compaction就能够在必要的时候抢占执行,保证尽量不会出现level 0的sstable数量超过阈值

(3)在low load的时候,给Internal Operation(compaction)分配更多的bandwidth,相反,在high load的时候,给Client operation分配更多的带宽,这样可以保证Compaction在适当的时候也能得到更多的处理,减少read放大和空间放大,这个调度策略也是基于通常的client operation load是波动这事实来设计的,如下图,就是一个真实环境的workload变化规律:

ref

  1. https://blog.csdn.net/Z_Stand/article/details/109683804
  2. https://zhuanlan.zhihu.com/p/77543818
  3. 论文地址 https://www.usenix.org/system/files/atc19-balmau.pdf

Read More

MongoRocks优化与实践


腾讯云mongorocks做的工作,作者kdy是个大神。

mongorocsk编码原理不说了。只说他们做的改进点

分ColumnFamily存储

  • 多ColumnFamily
    • kv业务索引少
    • 每个索引单独CF/数据单独CF
  • 索引/表快速删除
    • dropColumnFamily 物理删除CF数据
    • 便于Oplog按sst文件删除
      • 计算出需要删除的oplog所在的sstfiles
      • RocksDB::DeleteFilesInRange
    • 方便按CF为粒度对Cache大小调参

针对KV业务的缓存优化

  • 开启RowCache,减小BlockCache
    • 对于KV业务,点查询优先于区间查询
  • 对于存数据的CF,开启optimize_filters_for_hits
    • 索引CF中存在,数据CF中一定存在
    • 数据CF无bloomFilter的必要性
    • 该参数减小CF的bloomFilter大小
ref
  1. https://mongoing.com/wp-content/uploads/2017/04/mongoRocks.pdf

Read More

工作环境中的proxy使用


why

工作环境,由于某种原因,不能直接使用pip vcpkg git npm go get pacman 等直接下载,需要设置代理


如果都是通过cmd调用的话,可以在cmd层设置代理

直接

Set https_proxy=https://username:password@proxy.xxx.com:8080/

具体的账户密码网址,按照公司给的使用就行,go get/vcpkg按照上面的设置也有效

针对npm

npm config set proxy https_proxy=https://username:password@proxy.xxx.com:8080/

针对git 修改.gitconfig文件,在C:\Users\xxxusername 下,也可以自己新增一个

[http]
	sslVerify = false
	proxy =  https://username:password@proxy.xxx.com:8080/
[https]
	proxy = https://username:password@proxy.xxx.com:8080/

如果还不行,对于pip和pacman更有可能是源的问题,可以考虑换成公司内部源,配置文件位置

msys64\etc\pacman.d
C:\Users\username\pip\pip.ini

easy_install和pip类似,linux在home下,.pydistutils.cfg

注意密码转义

空格    -    %20
"          -    %22
#         -    %23
%        -    %25
&         -    %26
(          -    %28
)          -    %29
+         -    %2B
,          -    %2C
/          -    %2F
:          -    %3A
;          -    %3B
<         -    %3C
=         -    %3D
>         -    %3E
?         -    %3F
@       -    %40
\          -    %5C
|          -    %7C 

ref

contact

Read More

使用gsl::not_null封装raw pointer


why

这篇文章是参考链接的总结,主要是讲替代原生指针,使用not_null封装


如果是原生指针,就会有很多if

if (pMyData)
    pMyData->Process();

or:

auto result = pObj ? pObj->Compute() : InvalidVal;

or

void Foo(Object* pObj)
{
    if (!pObj)
        return;

    // Do stuff...
}

多余的if判断,使代码复杂等等等等

一个使用not_null的例子

// { autofold
// not_null playground
// bfilipek.com


#include <iostream>
#include <string_view>
#include <string>
#include <memory>
// }

#include "gsl/gsl"

// { autofold
class App
{
public:
	App(const std::string& str) : m_name(str) { }

	void Run() { std::cout << "Running " << m_name << "\n"; }
	void Shutdown() { std::cout << "App " << m_name << " is closing...\n"; }
	void Diagnose() { std::cout << "Diagnosing...\n"; }

private:
	std::string m_name;
};
// }


void RunApp(gsl::not_null<App *> pApp)
{
	pApp->Run();
	pApp->Shutdown();
}

void DiagnoseApp(gsl::not_null<App *> pApp)
{
	pApp->Diagnose();
}

int main()
{
    // first case: deleting and marking as null:
	{
		gsl::not_null<App *> myApp = new App("Poker");

		// we can delete it, but cannot assign null
		delete myApp;
		//myApp = nullptr;
	}

    // second case: breaking the contract
	{
		// cannot invoke such function, contract violation
		//RunApp(nullptr);
	}

    // assigning a null on initilization
	{
		//gsl::not_null<App *> myApp = nullptr;
	}
	
	std::cout << "Finished...\n";
}

接口语义保证不会null,更清晰,并且编译期就能确保不是null

另外,参考链接二提到,既然有not_null,就应该有optional_ptr, 保证默认null,这个实际上也是observer_ptr的加强版,observer_ptr是对原生ptr的封装,也是通过一个接口语义来清晰观测或者对应rustborrow

拓展阅读

在not_null的实现里,get指针是这样的

    template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
    constexpr not_null(U&& u) : ptr_(std::forward<U>(u))
    {
        Expects(ptr_ != nullptr);
    }

    template <typename = std::enable_if_t<!std::is_same<std::nullptr_t, T>::value>>
    constexpr not_null(T u) : ptr_(std::move(u))
    {
        Expects(ptr_ != nullptr);
    }

    constexpr std::conditional_t<std::is_copy_constructible<T>::value, T, const T&> get() const
    {
        Ensures(ptr_ != nullptr);
        return ptr_;
    }

其中expects和ensures定义是这样的

#if defined(__clang__) || defined(__GNUC__)
#define GSL_LIKELY(x) __builtin_expect(!!(x), 1)
#define GSL_UNLIKELY(x) __builtin_expect(!!(x), 0)

#else

#define GSL_LIKELY(x) (!!(x))
#define GSL_UNLIKELY(x) (!!(x))
#endif // defined(__clang__) || defined(__GNUC__)

#define GSL_CONTRACT_CHECK(type, cond)                                                             \
    (GSL_LIKELY(cond) ? static_cast<void>(0) : gsl::details::terminate())

#define Expects(cond) GSL_CONTRACT_CHECK("Precondition", cond)
#define Ensures(cond) GSL_CONTRACT_CHECK("Postcondition", cond)

为啥get需要Ensures校验?本身都非空了,这样不是多此一举吗?

考虑只能指针的move,比如gsl::not_null<unique_ptr<T> >发生了move行为,那旧的not_null就是是空指针了

这里是为了留这么一手 至于运行时的消耗,就让编译期优化吧,编译器能判定出来分支走到static_cast<void>(0) 整个分支都没有任何作用,应该就会优化掉

ref

contact

Read More

Buffering SQL Writes with Redis


why

这篇文章Sentry公司的博客,介绍他们怎么用redis的(好像是一个运营监控服务软件公司)


Sentry公司大量使用pg,两个集群,一个集群存储事件元数据,另一个存储Sentry平台数据,都有冷备,只在灾备或者维护期间使用

数据流分成三类,时序数据

  • 事件blob数据,不变的,直接存储到Riak集群里
  • kv对,表示tag,通常是设备名,操作系统之类的
  • 属性 聚合操作用

大多数数据是SQL,事件Blob数据存在Riak来保持扩展性,实际上也是用来做KV存储

突发情况

Sentry的数据信息有特征,比如突然传来一个错误事件,这些事件重复程度高,出现频率也高,所以需要一个去重-聚合动作,又分两种情况

  • 相同错误出现多次
  • 大量不同错误生成新的聚合

可以从下面两个属性来观测到

  • 基数计数器 Cardinality counters,
  • Latest Value 看聚合操作中出现key用个字段记录

第一种设置个counter

UPDATE table SET counter = counter + 1 WHERE key = %s;

第二种就设置个字段记录

UPDATE table SET last_seen = %s WHERE key = %s;

count 引入的锁还有可能影响性能,所以把count拆开,拆成多行,避免行锁竞争

下面这个例子是一百行的

UPDATE table SET counter = counter + 1 WHERE key = %s AND partition = ABS(RANDOM() * 100);

pg提供强一致,浪费很多时间在锁上,所以用buffer来省掉这些竞争

buffer write,先聚合一堆写,定时刷回去,需要考虑最低损失数据,据此来决定回写周期,Sentry实践是10秒一次

这引入两种问题

  • UI不会自动更新,十秒需要触发一次更新
  • 可能丢失数据,比如网络问题

使用Redis

  • 每个entity 一个hash

  • flush 一组hash集合

当数据来

  • 对每个entity hashkey
    • hincrby counter
    • 更新其他字段,hset,比如last seen timestamp
  • zadd 把pending key加入集合中

然后类似cron任务,10秒一次,zrange拿到集合,对于pending hash key 加入到队列,然后zrem移除集合

worker从队列中拿到job,开始写

  • pipeline
    • zrem,保证没副作用
    • hgetall 拿到key
    • rem hashkey
  • 转换成sql,直接执行最后结果
    • set counter = counter +%d
    • set value = %s

限制条数,使用sorted set(zset)

可以水平扩展,加redis节点

这个模型保证一行sql就更新一次,降低锁竞争,达到预期

问题:为什么不用时序数据库?

博客还提到了几个提升的点子

  • 通常优先级高的事情发生的概率比较低,当前是模型是LIFO,也可改成FIFO 用zadd nx
  • 数据冲突踩踏,大量数据任务可能同时触发,虽然可能实际处理后和noop没区别,但还是有这种突发增加延迟的问题,当前是push模型,可以改成pull,控制主动权 限流器也行 实现上的复杂导致没能采用
  • 丢写,加个备份集合

ref

contact

其他联系方式在主页

Read More

unique_ptr实现pimpl惯用法


why

这篇文章是参考链接的总结


pimpl惯用法,pointer to implementation,就是用指针来拆分实现,这样改动不会导致所有文件都编译一遍,也是一种解耦

以前的实现

#include "Engine.h"
 
class Fridge
{
public:
   void coolDown();
private:
   Engine engine_;
};

这样改动Engine就会重编Fridge

引入指针分离

class Fridge
{
public:
   Fridge();
   ~Fridge();
 
   void coolDown();
private:
   class FridgeImpl;
   FridgeImpl* impl_;
};

FridgeImpl封装一层Engine,不可见

#include "Engine.h"
#include "Fridge.h"
 
class FridgeImpl
{
public:
   void coolDown()
   {
      /* ... */
   }
private:
   Engine engine_;
};
 
Fridge::Fridge() : impl_(new FridgeImpl) {}
 
Fridge::~Fridge(){
   delete impl_;
}
 
void Fridge::coolDown(){
   impl_->coolDown();
}

这样还是需要管理impl_生命周期,如果用unique_ptr就更好了

改进的代码

#include <memory>
 
class Fridge
{
public:
   Fridge();
   void coolDown();
private:
   class FridgeImpl;
   std::unique_ptr<FridgeImpl> impl_;
};
#include "Engine.h"
#include "Fridge.h"

class FridgeImpl
{
public:
   void coolDown()
   {
      /* ... */
   }
private:
   Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

这样会有新问题,编译不过

use of undefined type ‘FridgeImpl’ can’t delete an incomplete type

因为unique_ptr需要知道托管对象的析构,最起码要保证可见性

析构可见性

c++规则,以下两种情况,delete pointer会有未定义行为

  • void* 类型
  • 指针的类型不完整,比如这种前向声明类指针

由于unique_ptr检查,会在编译期直接拒绝 同理的还有boost::checked_delete

进一步讨论,Fridge 和FridgeImpl的析构函数都是没定义的,编译器会自动定义并内联,在Fridge的编译单元,就已经见到了Fridge的析构了,但是见不到FridgeImpl的析构,解决办法就是加上Fridge的析构声明,并把实现放到实现文件中,让Fridge和FridgeImpl的析构同时可见

#include <memory>
 
class Fridge
{
public:
   Fridge();
   +~Fridge();
...
};
#include "Engine.h"
#include "Fridge.h"
 
class FridgeImpl
....
Fridge::Fridge() : impl_(new FridgeImpl) {}
 
+Fridge::~Fridge() = default;

ref

contact

Read More

^