b-tree


一棵传统的B+树需要满足以下几点要求:

  • 从根节点到叶节点的所有路径都具有相同的长度
  • 所有数据信息都存储在叶节点上,非叶节点仅作为叶节点的索引存在
  • 根结点至少拥有两个键值对
  • 每个树节点最多拥有M个键值对
  • 每个树节点(除了根节点)拥有至少M/2个键值对

一棵传统的B+需要支持以下操作:

  • 单键值操作:Search/Insert/Update/Delete(下文以Search/Insert操作为例,其它操作的实现相似)
  • 范围操作:Range Search

基本的b+tree的同步问题

lock-coupling和lock-subtree

索引节点叶子结点加锁 -> 避免锁索引 -> 避免锁整个树,锁分支 -> 锁升级 -> 加版本号

B+tree每个节点都额外增加一个‘rightlink’指向它的右邻居节点。允许btree的操作并发执行,后续再根据rightlink来复原出完整的btree。

原理以及正确性证明 https://zhuanlan.zhihu.com/p/165149237

上文没提到的删除

https://zhuanlan.zhihu.com/p/166398779

link可以理解成一种hazard pointer

Masstree

解决的问题

palmtree

解决的问题

https://github.com/runshenzhu/palmtree

bw-tree

解决的问题 epoch base回收

Bw tree的基本结构和B+ tree相似,区别在于:

  • Mapping Table
  • Base Nodes and Delta Chains

先介绍Mapping Table。传统的B+ tree中,节点和节点之间用指针连接,这里的指针是物理指针,直接指向一个内存块。而在Bw tree中,节点之间存的是逻辑指针,即指向某个节点对应的page-id。而我们要访问这个节点,则需要在Mapping Table中找到这个page-id对应的物理位置,再进行寻址。这样做的好处在于,当我们产生一个新修改过的页时,它的父节点、兄弟节点都不需要进行指针的修改,只需要在Mapping Table中修改逻辑指针指向的新的具体物理位置即可。而这个操作,可以利用CaS(compare-and-swap)进行,这个命令是原子命令(atomic primitive)

原理介绍

https://zhuanlan.zhihu.com/p/37365403

https://zhuanlan.zhihu.com/p/146974619

https://nan01ab.github.io/2018/06/Bw-Tree.html

新硬件

比如LB+Tree:面向3DXPoint优化的B+Tree http://loopjump.com/pr-lbtree/


几个lockfree gc算法

实现看这里 https://github.com/rmind/libqsbr 这有个介绍 https://blog.csdn.net/zhangyifei216/article/details/52767236

QSBR简介

QSBR是通过quiescent state来检测grace period。如果线程T在某时刻不再持有共享对象的引用,那么该线程T在此时就处于quiescent state。如果一个时间区间内,所有线程都曾处于quiescent state,那么这个区间就是一个grace period。QSBR需要实现时明确手动指明在算法某一步处于quiescent state。

具体实现时,可以在时间轴上划分出interval,每个interval内,每一个线程至少有一次quiescent state。那么当前interval删除的对象的内存可以在下一个interval结束时释放掉。

需要注意的是,QSBR是个blocking的算法。如果某个线程卡死了,那么就等不到grace period了,最终导致内存都无法释放。

EBR简介

EBR将所有的线程的操作都归到某个epoch,通过有条件地增大epoch值来限制只使用连续三个epoch值,使得每个线程本地的epoch最多只落后全局epoch一个,线程在epoch维度上基本上是齐步走的。

具体实现时,设置一个全局的global_epoch,每个线程操作前将线程本地的local_epoch设置为global_epoch。

当线程尝试周期性更新global_epoch时,如果发现每一个在临界区内的线程的local_epoch都等于global_epoch,则递增global_epoch,否则放弃递增保持原来的值(有线程还在更旧的epoch)。如果更新成功,表明global_epoch-2时期下被删除的对象都可以回收。因为只需要三个连续epoch,所以可以用模3的方式修改epoch。

HPBR简介

Hazard Pointer思路比较简单,线程在使用一个共享对象时,为了避免该共享对象被释放,将其指针放在本线程局部声明成风险指针保护起来。如果某个线程想释放一个对象对象时,先看看有没有其他线程保护该对象,没有线程保护时才释放。

Hazard Pointer适合lock free的queue或者stack之类的简单数据结构,这种数据结构要保护的指针只有一两个。如果是hash或者tree等基本不实用。


https://github.com/wangziqi2016/index-microbench

参考链接

  • http://mysql.taobao.org/monthly/2018/09/01/ 介绍了同步的演化
  • http://mysql.taobao.org/monthly/2018/11/01/ bw-tree
  • http://mysql.taobao.org/monthly/2019/02/01/ 后续发展,新硬件
  • 几个索引实现 https://github.com/UncP/aili
  • LockFree数据结构的内存回收性能测试 阅读笔记https://loopjump.com/lockfree_reclaim_perf_note/
  • 讲锁 https://zhewuzhou.github.io/posts/weekly-paper-a-survey-of-b-tree-locking-techniques/

Read More

(转)设计是自找的+定位Python执行命令僵尸卡死

转载自

https://www.mnstory.net/2017/04/16/design-is-self-imposed/

https://www.mnstory.net/2017/04/16/locate-problem-of-python-child-zombie/

非常感谢! 写代码+抓bug一条龙,思路相当不错,开阔眼界


一直有个悖论,如果一个人,没有设计能力,那就不会给你模块设计;但是,一个人的设计能力,需要从实际的设计中锻炼出来,如果不给你模块锻炼,如何得来设计能力?

看样子是这样的,但是,也不尽然,我之前给同事吹过牛逼:设计,是自找的。

你可以从每天改BUG的生活中,找到设计,之前举了一个我在改BUG的时候如何为HCI引入redis的例子,我今天看一下,一个普通的API,如何自找设计。

V1

写一个Python执行Shell命令的API,看似乎非常简单,我的需求是,可以输入一点数据也可不输入(不交互),主要是能分别获取STDOUT和STDERR,还有退出码,方便外部判断命令是否执行正确(然而我最害怕的是,有同事根本不关心返回值,那就没下面什么事了)。

一般来说,写到这个水平,已经差不多了:


def run(cmd, input=None):
    try:
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = p.communicate(input)
    except Exception, e:
        return (127, "", str(e))
    
    return (p.returncode, out, err)

V2

当然,作为老码农,写的代码总应该和新员工有所区别,必须细读API DOC,搞懂每个参数是做啥的,测试验证,有疑问的配合源码阅读,然后我又发现几个问题:

  1. 是否应该记录一下程序的执行时间? 毕竟太多时候,定位性能问题,就靠这个时间。(经验)
  2. 是否需要对输出的数据做一下formal处理? 例如out数据有的是带回车换行,有的不带,当然,作为通用API,我应该原封不动返回,但是我是个懒人,我不想每次外部获取到的out数据还要自己trim一下,事实上,我至今没有见过谁的命令调用,结果分析依赖于out的首位两端空白符的,所以,我认为应该API内部做formal处理。(个人需求)
  3. 此API里面是否应该输出一些正常日志。 不是异常日志,异常日志我是一定会输出的,也会返回,但是正常日志,一般情况下,我是拒绝的。 但这个地方我认为有必要,因为我是一个反对在程序里面掉命令来完成任务的人,所以说,这个run函数,使用应该非常少,也需要非常明确哪些逻辑使用了,所以我输出一些日志,第一,可以警示使用者,命令是否调用过多;第二,调命令完成任务是最容易出错的逻辑,应该有全面的日志记录。(设计取舍)
  4. 经验告诉我们,毫不相干的子进程应该close所有继承自Parent的句柄。(经验)

于是更改为如下版本:


_lastOutDict={}
def run(cmd, input=None):
    # 1. 记录执行时间
    timeStart = time.time()
    try:
        #4. close_fds=True 关闭所有从父进程继承的句柄
        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
        (out, err) = p.communicate(input)
    except Exception, e:
        timeEnd = time.time()
        # 3. 错误日志输出
        l.error("<EXE>(%ds):%s failed(%s)" % ((timeEnd-timeStart), cmd, str(e)))
        return (127, "", str(e))
    timeEnd = time.time()

    # 2. 对out和err做trim处理
    if out:
        out = out.strip()
    else:
        out = ""
    if err:
        err = err.strip()
    exitCode = p.returncode

    # 3. 正常日志输出的时候,要考虑是否太过冗余,所有对于超过256字节的相同输出信息,第二次就做了supress,防止日志干扰
    debugSupressOut = out
    if out and len(out) > 256:
        if _lastOutDict.get(cmd, "") == out:
            debugSupressOut = "<equal last...>"
        else:
            _lastOutDict[cmd] = out
    # 3. 正常日志输出
    l.debug("<EXE>(%d,%ds):%s%s%s%s" % (exitCode, (timeEnd-timeStart), cmd, (" <IN>:%s" % input) if input else "", (" <OUT>:%s" % debugSupressOut) if debugSupressOut else "", (" <ERR>:%s" % err) if err else ""))
    return (exitCode, out, err)

V3

我对命令行调用的敬畏之心,远远超过很多人,所以,我还觉得差点什么。

是的,差一个TIMEOUT。

经验告诉我们,依赖外部命令的时候,有一个常见的风险,便是卡死,这是个头疼的问题。 Python2.7里面没有TIMEOUT执行命令的API,需要借助线程的TIMOUT来实现。

于是,有了第三个版本:


# 杀进程树,而不是子进程,单杀子进程,孙子进程还在,残留逻辑没人收拾
def killTree(rootPid, killRoot):
    try:
        rootProcess = psutil.Process(rootPid)
        children = rootProcess.get_children(recursive=True)
        for child in children:
            l.info("kill tree child %d:%s (parent %d:%s)" % (child.pid, child.cmdline, rootProcess.pid, rootProcess.cmdline))
            child.kill()
        psutil.wait_procs(children, timeout=7)

        if killRoot:
            l.info("kill tree root %d:%s" % (rootProcess.pid, rootProcess.cmdline))
            rootProcess.kill()
            rootProcess.wait(5)
    except Exception, e:
        l.warning("kill tree %d found exception, %s" % (rootPid, str(e)))

class Command(object):
    def __init__(self, cmd, input=None):
        self.cmd = cmd
        self.input = input
        self.process = None
        self.out = ""
        self.err = ""
        self.errDesc = ""

    def _target(self):
        try:
            self.process = subprocess.Popen(self.cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
            (self.out, self.err) = self.process.communicate(self.input)
        except Exception, e:
            if self.errDesc:
                self.errDesc += ", "
            self.errDesc += str(e)

    def _run(self):
        self._target()

    def _runTimeout(self, timeout):
        thread = threading.Thread(target=self._target)
        thread.start()
        thread.join(timeout)

        if thread.is_alive(): # 超时后,线程还没有主动结束,表示还卡着,这个时候,就要主动KILL了
            if self.errDesc:
                self.errDesc += ", "
            self.errDesc += "timeout(%ds)" % timeout
            if None == self.process:
                self.errDesc += ", no process object"
            else:
                self.errDesc += ", kill process tree(%d)" % self.process.pid
                killTree(self.process.pid, True) #全部杀死
            thread.join(1) #再给他一个机会

    def run(self, timeout=-1):
        global _lastOutDict

        timeStart = time.time()
        if timeout <= 0:
            self._run() #如果是没有timeout,就不需要开启线程
        else:
            self._runTimeout(timeout) #用新线程来等待
        timeEscape = (time.time()-timeStart)

        if self.out:
            self.out = self.out.strip()
        else:
            self.out = ""

        if self.err:
            self.err = self.err.strip()
        else:
            self.err = ""

        if self.errDesc:
            exitCode = -1
            if self.err:
                self.err += " "
            self.err += "<EXCEPTION>:" +self.errDesc
        else:
            exitCode = self.process.returncode

        debugSupressOut = self.out
        if self.out and len(self.out) > 256:
            if _lastOutDict.get(self.cmd, "") == self.out:
                debugSupressOut = "<equal last...>"
            else:
                _lastOutDict[self.cmd] = self.out

        l.debug("<EXE>(%d,%ds):%s%s%s%s" % (exitCode, timeEscape, self.cmd, (" <IN>:%s" % self.input) if self.input else "", (" <OUT>:%s" % debugSupressOut) if debugSupressOut else "", (" <ERR>:%s" % self.err) if self.err else ""))
        return (exitCode, self.out, self.err)

def run(cmd, input=None, timeout=-1):
    command = Command(cmd, input)
    return command.run(timeout)

有TIMEOUT的逻辑和最开始的逻辑比起来,多了很多代码,很满意,这过程中,你是不是需要学习很多东西,例如,为何上面要KILL进程树而不是进程?例如,如何利用jone做线程协同?所有的细微知识,积累起来,就是功力。


结合《设计是自找的》看,前面设计了一个Python调用命令行的封装,我一般在自测上做很多功夫,所以,幸与不幸,还是测试出了问题。

构造必现环境

在启动mysql的时候,进程会卡主直到TIMEOUT,子进程是僵尸进程defunct,如下:


root     28058  0.4  0.0 120068 15648 pts/4    S+   11:41   0:00  \_ python test.py
root     28455  0.0  0.0      0     0 pts/4    Z+   11:41   0:00      \_ [sh] <defunct>
root     28459  0.0  0.0  22416  1328 pts/4    S+   11:41   0:00 /bin/sh /var/lib/mysql/bin/mysqld_safe --datadir=/var/lib/mysql/data --pid-file=/var/lib/mysql/data/host-a0369f033dcb.pid
mysql    28678  1.4  0.5 727476 175156 pts/4   Sl+  11:41   0:00  \_ /var/lib/mysql/bin/mysqld --basedir=/var/lib/mysql --datadir=/var/lib/mysql/data --plugin-dir=/var/lib/mysql/lib/plugin --user=mysql --log-error=/var/log/mysql/mysql-error.log --pid-file=/var/lib/mysql

子进程是僵尸进程,那肯定是父进程没有去waitpid,这个问题不是必然的,如果调用的是其他命令,不会出现,所以,我先把命令精简一下,构造一个必现的环境。 test.py代码:


p=subprocess.Popen("./b.sh", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=False)
(out, err) = p.communicate(None)
print out, err

b.sh代码:


#!/bin/sh
pkill mysqld_safe
pkill mysqld
/var/lib/mysql/bin/mysqld_safe --datadir=/var/lib/mysql/data --pid-file=/var/lib/mysql/data/host-a0369f033dcb.pid &
echo "start ok"

strace工具定位

先strace看下test.py在做啥。


pipe([3, 4])                            = 0
fcntl(3, F_GETFD)                       = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
fcntl(3, F_GETFL)                       = 0 (flags O_RDONLY)
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb1b3282000
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
munmap(0x7fb1b3282000, 4096)            = 0
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
read(3, "start ", 6)                    = 6
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
read(3, "ok\n", 6)                      = 3
--- SIGCHLD (Child exited) @ 0 (0) ---
read(3, "201", 3)                       = 3
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(3, "31T03:47:18.655324Z mysqld_sa", 29) = 29
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
read(3, "fe Starting mysqld daemon with d"..., 33) = 33
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Illegal seek)
read(3, "tabases from /var/lib/mysql/data"..., 37) = 33
read(3,

卡在了read函数,从strace跟踪看fd=3是读管道,子进程已经退出了,父进程还在读管道。

proc文件系统定位

从上面的进程列表可以看出, 28459进程是28058的孙子进程,既然28058卡在读管道上,那孙子进程是否会有相应的写管道未CLOSE?我们查看一下:


# lh /proc/28058/fd
total 0
lrwx------ 1 root root 64 Mar 31 14:31 0 -> /dev/pts/4
lrwx------ 1 root root 64 Mar 31 14:31 1 -> /dev/pts/4
lr-x------ 1 root root 64 Mar 31 14:33 11 -> /dev/urandom
lrwx------ 1 root root 64 Mar 31 14:31 2 -> /dev/pts/4
lr-x------ 1 root root 64 Mar 31 14:31 3 -> pipe:[44876238]

# lh /proc/28459/fd
total 0
lr-x------ 1 root root 64 Mar 31 14:33 0 -> /dev/null
l-wx------ 1 root root 64 Mar 31 14:33 1 -> /dev/null
lr-x------ 1 root root 64 Mar 31 14:33 10 -> /var/lib/mysql/bin/mysqld_safe*
lr-x------ 1 root root 64 Mar 31 14:33 11 -> /dev/null
l-wx------ 1 root root 64 Mar 31 14:33 12 -> pipe:[44876238]
l-wx------ 1 root root 64 Mar 31 14:33 13 -> pipe:[44876238]
l-wx------ 1 root root 64 Mar 31 14:31 2 -> /dev/null

GDB工具验证

的确,孙子继承了咱们的句柄,我们尝试关闭孙子进程继承的管道看看:


# gdb -p 28459
(gdb) call close(12)
$1 = 0
(gdb) call close(13)
$2 = 0

28058进程的终于往下走了,从fork到我们close管道另一端,耗时690s:


#strace -Ttt python test.py
14:31:25.203008 fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0 <0.000004>
14:31:25.203040 lseek(3, 0, SEEK_CUR)   = -1 ESPIPE (Illegal seek) <0.000003>
14:31:25.203064 read(3, "tabases from /var/lib/mysql/data"..., 37) = 33 <0.000005>
14:31:25.203092 read(3, 
"", 4)          = 0 <690.316968>

这里,等待了很久没有继续,现在终于结束了


14:42:55.520112 close(3)                = 0 <0.000010>
14:42:55.520203 wait4(28455, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 28455 <0.000020>
14:42:55.520275 write(1, "start ok\n2017-03-31T06:31:25.182"..., 282start ok

问题确认,造成python 执行命令卡死的原因是管道读写句柄继承了,然而继承端并没有关闭管道。

原因分析

我们知道,linux句柄会继承是个好事也是个头疼的问题,很少有人记得加:FD_CLOEXEC或SOCK_CLOEXEC,于是,我在做supervisor模块的时候,特别处理过类似问题,处理办法很暴力,直接在fork后exec前关闭句柄:


int closeAllfds(int bIngoreDftFD) {
    struct rlimit rl;
    int closeCnt = 0;

    if(-1 == getrlimit(RLIMIT_NOFILE, &rl)) {
        lerror("getrlimit RLIMIT_NOFILE failed %d:%s\n", errno, strerror(errno));
        return -1;
    }
    if(rl.rlim_max == RLIM_INFINITY) {
        //If many files were opened and then this limit was reduced to 1024, 
        //we may not close all file descriptors.
        rl.rlim_max = 1024;
    }

    int fd = 0;
    while(fd < (int)rl.rlim_max) {
        if(!bIngoreDftFD || (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO)) {
            if(-1 == close(fd)) {
                if(EINTR == errno) {
                    continue; //try again
                }
                if(EBADF != errno) {
                    lerror("close fd %d failed %d:%s\n", fd, errno, strerror(errno));
                }
            } else {
                ++closeCnt;
                lerror("close fd %d, total count %d\n", fd, closeCnt);
            }
        }
        ++fd;
    }

    return closeCnt;
}

既然subprocess.Popen对象参数里面可以设置close_fds标记,那为何不生效? 看看subprocess的源码:


try:
    MAXFD = os.sysconf("SC_OPEN_MAX")
except:
    MAXFD = 256

errpipe_read, errpipe_write = self.pipe_cloexec()

# Close all other fds, if asked for - after
# preexec_fn(), which may open FDs.
if close_fds:
    self._close_fds(but=errpipe_write)

def _close_fds(self, but):
    if hasattr(os, 'closerange'):
        os.closerange(3, but)
        os.closerange(but + 1, MAXFD)
    else:
        for i in xrange(3, MAXFD):
            if i == but:
                continue
            try:
                os.close(i)
            except:
                pass

关闭方法和我的一样暴力,但是有一个but参数,会将写入端的管道排除,子进程其实是没有继承其他句柄的,但是,偏偏就在排除的句柄上,出了问题,真是防不胜防。

再一睹Python库communicate代码,看是否和分析吻合:


def _readerthread(self, fh, buffer):
    buffer.append(fh.read())

def _communicate(self, input):
    stdout = None  # Return
    stderr = None  # Return

    # 指定了stdout为PIPE的时候,会开一个线程来读取
    if self.stdout:
        stdout = []
        stdout_thread = threading.Thread(target=self._readerthread,
                                         args=(self.stdout, stdout))
        stdout_thread.setDaemon(True)
        stdout_thread.start()
    if self.stderr:
        stderr = []
        stderr_thread = threading.Thread(target=self._readerthread,
                                         args=(self.stderr, stderr))
        stderr_thread.setDaemon(True)
        stderr_thread.start()

    if self.stdin:
        if input is not None:
            try:
                self.stdin.write(input)
            except IOError as e:
                if e.errno == errno.EPIPE:
                    # communicate() should ignore broken pipe error
                    pass
                elif (e.errno == errno.EINVAL
                      and self.poll() is not None):
                    # Issue #19612: stdin.write() fails with EINVAL
                    # if the process already exited before the write
                    pass
                else:
                    raise
        self.stdin.close()

    # 主线程会JOIN
    if self.stdout:
        stdout_thread.join()
    if self.stderr:
        stderr_thread.join()

    # All data exchanged.  Translate lists into strings.
    if stdout is not None:
        stdout = stdout[0]
    if stderr is not None:
        stderr = stderr[0]

    # Translate newlines, if requested.  We cannot let the file
    # object do the translation: It is based on stdio, which is
    # impossible to combine with select (unless forcing no
    # buffering).
    if self.universal_newlines and hasattr(file, 'newlines'):
        if stdout:
            stdout = self._translate_newlines(stdout)
        if stderr:
            stderr = self._translate_newlines(stderr)

    # 等PIPE读完了,才会waitpid
    self.wait()
    return (stdout, stderr)

虽然子进程已经退出了,但是test.py并没有调用wait,因为它被read PIPE卡主了,所以才会出现子进程defunct,而父进程一直不去回收,和现象完全吻合。

解决

解决方法比较简单,问题出在脚本执行的时候,不能用后台运行符号&简单了事,需要用daemon命令替代,daemon命令的源码我之前参考过,里面特别干过close fd的事情,所以,将:


/var/lib/mysql/bin/mysqld_safe --datadir=/var/lib/mysql/data --pid-file=/var/lib/mysql/data/host-a0369f033dcb.pid &

改为:


daemon -U -- /var/lib/mysql/bin/mysqld_safe --datadir=/var/lib/mysql/data --pid-file=/var/lib/mysql/data/host-a0369f033dcb.pid

即可。

或者,Python里面不要用PIPE方式取STDOUT亦可。

通过调试过程记录,可以看出,也就是一些知识和工具的运用,技巧不多,还在于积累。


Read More

(转)(译)Redis响应延迟问题排查



转载自https://nullcc.github.io/2018/02/15/(%E8%AF%91)Redis%E5%93%8D%E5%BA%94%E5%BB%B6%E8%BF%9F%E9%97%AE%E9%A2%98%E6%8E%92%E6%9F%A5/

非常感谢! 博客不错,比我的好看多了


本文翻译自Redis latency problems troubleshooting

本文将帮助你了解当你遇到了Redis延迟问题时究竟发生了什么。

在这里延迟指的是客户端从发送命令到接收命令回复这段时间的最大值。一般情况下Redis处理命令的时间非常短,基本上在微秒级别,但是这里有几种情况会导致高延迟。

我很忙,把清单给我

下面的文档对于想要以低延迟运行Redis来说非常重要。然而我知道大家都很忙,所以我们先来看一个快速清单。如果你没有遵守这些步骤,请回到这里阅读整个文档。

  1. 确保服务器没有被慢查询阻塞。使用Redis的慢日志功能来检查是否有慢查询。
  2. 对于EC2的用户,确保你基于现代的EC2实例使用HVM,比如m3.medium。否则fock()操作会很慢。
  3. 必须禁用内核的Transparent huge pages特性。使用echo never > /sys/kernel/mm/transparent_hugepage/enabled来禁用它,然后重启你的Redis进程。
  4. 如果你在使用虚拟机,很可能存在一种和Redis无关的内在延迟。检查机器的最小延迟,你可以在你的运行环境中使用./redis-cli --intrinsic-latency 100。注意:你需要在服务器上运行这个命令而不是在客户端上。
  5. 打开并使用Redis的延迟监控特性来获取你机器上的人类可读的延迟事件描述。

一般来说,使用下表进行持久化和延迟/性能的权衡,顺序从最高安全性/最高延迟到最低安全性/最低延迟。

  1. AOF + fsync always: 非常慢,只有当你确实需要时才使用该配置。
  2. AOF + fsync every second: 一个比较均衡的选择。
  3. AOF + fsync every second + no-appendfsync-on-rewrite选项为yes: 也是一个比较均衡的选择,但是要避免重写期间执行fsync,这可以降低磁盘压力。
  4. AOF + fsync never: 将fsync操作交给内核,减少了对磁盘的压力和延迟。
  5. RDB: 这里你可以配置触发生成RDB文件的条件。

以下我们花费15分钟时间来看看细节。

测量延迟

如果你对处理延迟问题很有经验,可能你知道在你的应用程序中如何测量延迟,也许你的延迟问题是非常明显的,甚至是肉眼可见的。然而redis-cli可以在毫秒级别测量一个Redis服务器的延迟,只需要运行:

redis-cli --latency -h `host` -p `port`

使用Redis内置的延迟监控子系统

从Redis 2.8.13开始,Redis提供了延迟监控功能,能够取样检测出是哪里导致了服务器阻塞。这使得本文档所列举的问题的调试更加简单,所以我们建议尽量开启延迟监控。有关这方面更详细的说明请查阅延迟监控的文档

虽然延迟监控的采样和报告能力可以使我们更容易地了解造成Redis延迟的原因,但还是建议你阅读本文档更广泛地了解Redis的延迟尖峰。

延迟的基线

在你运行Redis的环境中有一种固有的延迟,这种延迟来自操作系统内核,如果你正在使用虚拟化,这种延迟来自于你使用的虚拟机管理程序。

虽然这个延迟无法被抹去,但这是我们学习的重要对象,因为它是基线,或者换句话说,由于内核和虚拟机管理程序的存在,你无法将Redis的延迟优化得比你系统中正在运行的进程的延迟还要低。

我们称这种延迟为内在延迟,redis-cli从Redis 2.8.7版本之后就可以测量内在延迟了。下面是一个运行在Linux 3.11.0入门级服务器上的实例。

注意:参数100表示测试执行的时间的秒数。我们运行测试的时间越久,就越有可能发现延迟尖峰。100秒通常是合适的,不过你可能希望在测试过程中不同的时间执行一些其他的操作。请注意,测试是CPU密集型的,这可能会使系统中的单个内核跑满。

$ ./redis-cli --intrinsic-latency 100
Max latency so far: 1 microseconds.
Max latency so far: 16 microseconds.
Max latency so far: 50 microseconds.
Max latency so far: 53 microseconds.
Max latency so far: 83 microseconds.
Max latency so far: 115 microseconds.

注意:在这个特殊情况下,redis-cli需要在服务器端运行,而不是在客户端。这种特殊模式下,redis-cli根本不需要连接到一台Redis服务器:它只是试图测量内核不提供CPU时间给redis-cli进程本身的最大时间。

上面的例子中,系统固有延迟只有0.115毫秒(或115微秒),这是个好消息,但是请记住,系统内在延迟可能随着系统负载而随时间变化。

虚拟化环境的情况会差一些,特别是在共享虚拟环境中有高负载的其他应用在运行时。下面是一个在Linode 4096实例上运行Redis和Apache的例子:

$ ./redis-cli --intrinsic-latency 100
Max latency so far: 573 microseconds.
Max latency so far: 695 microseconds.
Max latency so far: 919 microseconds.
Max latency so far: 1606 microseconds.
Max latency so far: 3191 microseconds.
Max latency so far: 9243 microseconds.
Max latency so far: 9671 microseconds. 

这里我们测量出有9.7毫秒的内在延迟:这意味着Redis的延迟不可能比这个数字更低了。然而,在不同的虚拟化环境中,如果有高负载的其他应用程序在运行时,很容易出现更高的内在延迟。除非我们能够在系统中测量出40毫秒的内在延迟,否则显然Redis运行正常。

网络通信引起的延迟

客户端使用一条TCP/IP连接或一条UNIX域连接来连接Redis。一个带宽为1 Gbit/s的网络典型的延迟为200μs,然而一个UNIX域套接字的延迟可以低至30μs。这具体依赖你的网络和系统硬件情况。高层的通信增加了更多的延迟(由于线程调度、CPU缓存、NUMA配置等等)。系统内部引起的延迟在虚拟化环境中要比在物理机上高得多。

其结果是尽管Redis处理大部分命令的时间都在亚微秒级别,但一个客户端和服务器之间的多次往返会增加网络和系统的延迟。

一个高效的客户端将会通过使用流水线来限制执行多个命令时的通信往返次数。流水线特性被服务器和绝大多数客户端所支持。批量操作命令如MSET/MGET也是为了这个目的。从Redis 2.4起,一些命令还支持所有数据类型的可变参数。

下面是一些准则:

  • 如果经济上允许,优先选择使用物理机而不是虚拟机来承载Redis服务端。
  • 不要随意连接/断开到服务器(尤其是web应用程序)。尽量保持连接长时间可用。
  • 如果Redis的服务端和客户端部署在同一台机器上,请使用UNIX域套接字。
  • 相比起流水线,尽量使用批量操作命令(MSET/MGET),或可变参数命令(如果可能的话)。
  • 相比起发送多个单独命令,尽量使用流水线(如果可能的话)。
  • 在不适合使用原生流水线功能的场景,Redis支持服务端Lua脚本(针对一个命令的输出是另一个命令的输入的情况)。

在Linux中,你可以通过process placement (taskset)、 cgroups、 real-time priorities (chrt)、 NUMA configuration (numactl)或使用一个低延迟内核来获得更低的延迟。请注意Redis并不适合被绑定在一个CPU内核上运行。Redis会fork出一些后台任务比如bgsave或AOF重写这些非常消耗CPU的任务。这些任务禁止和Redis的主事件循环运行在同一个CPU上。

大部分情况下,我们不需要这种类型的系统级优化。只有当你确实需要或者对它们很熟悉的情况下再去使用它们。

Redis的单线程属性

Redis被设计成大部分情况下是单线程的。这意味着使用一个线程处理所有的客户端请求,其中使用了多路复用技术。这意味着Redis在一个时刻只能处理一个命令,所以所有命令都是串行执行的。这和Node.js的工作机制很类似。然而,Redis和Node.js通常都被认为是非常高性能的。这有部分原因是因为它们处理每个请求的时间都很短,但是主要原因是因为它们都被设计成不会被系统调用锁阻塞,比如从套接字中读取或写入数据。

之所以说Redis大部分情况下是单线程的,是因为从Redis 2.4版本起,为了在后台执行一些慢速的I/O操作,一般是磁盘I/O,Redis使用了其他线程来执行。但这也不能改变Redis使用单线程处理所有请求这个事实。

慢查询命令引起的响应延迟

使用单线程的一个结果是,当一个请求的处理很慢时,所有其他客户端将等待该请求被处理完毕。当执行普通命令时,比如GET或SET或LPUSH时这完全不是问题,因为这几个命令的执行时间是常数(非常短)。然而,有些命令会操作多个元素,比如SORT、LREM、SUNION等。例如,计算两个大集合的交集需要花费很长的时间。

所有命令的算法复杂度都有文档记录。一个好的实践是当你使用你不熟悉的命令之前先检查该命令的算法复杂度。

如果你关注Redis的响应延迟问题,你就不应该对有多个元素的值使用慢查询命令,你应该在Redis复制节点上运行你所有的慢查询。

可以使用Redis的Slow Log功能来监控慢查询命令。

而且,你可以使用你最喜欢的进程级监控程序(top, htop, prstat等)来快速检查Redis主进程的CPU消耗。如果并发量并不是很高,很可能是因为你使用了慢查询命令。

重要提示:一个非常常见的造成Redis响应延迟的情况是在生产环境中使用KEYS命令。Redis文档中指出KEYS命令只能用于调试目的。从Redis 2.8之后,为了在键空间或大集合中增量地迭代键而引入了一些命令,请查阅SCAN, SSCAN, HSCAN and ZSCAN的文档来获取更多信息。

fork引起的响应延迟

为了在后台生成RDB文件,或者当AOF持久化开启时重写AOF文件,Redis需要执行fork。fork操作(在主线程中执行)会引发响应延迟。

在大多数类UNIX系统中fork是一个开销很昂贵的操作,因为它涉及复制与进程相关联的大量对象。对于和虚拟内存相关联的页表尤其如此。

例如在一个Linux/AMD64系统上,内存被划分为一个个个4KB大小的页面。为了将逻辑地址转换成物理地址,每个进程都维护一个页表(在内部用一棵树表示),每个页面包含进程地址空间中的至少一个指针。所以一个拥有24GB内存的Redis实例需要的页表大小为24 GB / 4 kB * 8 = 48 MB。

当执行一个后台持久化任务时,该Redis实例需要执行fork,这将涉及分配和复制48MB的内存。这需要消耗时间和CPU资源,特别是在虚拟机上执行分配和初始化大内存时开销尤其昂贵。

不同系统中fork操作的耗时

现代硬件在复制页表这个操作上非常快,但Xen却不是这样。Xen的问题不在于虚拟化,而在于Xen本身。一个例子是使用VMware或Virtual Box不会导致fork变慢。下面比较了不同Redis实例执行fork操作的耗时。数据来自于执行BGSAVE,并观察INFO命令输出的latest_fork_usec信息。

然而,好消息是基于EC2 HVM的实例执行fork操作的表现很好,几乎和在物理机上执行差不多,所以使用m3.medium(或高性能)的实例将会得到更好的结果。

  • 运行于VMware的Linux对一个6.0GB的Redis实例执行fork操作耗时77ms(12.8ms/GB)。
  • 运行于物理机(硬件未知)上的Linux对一个6.1GB的Redis实例执行fork操作耗时80ms(13.1ms/GB)。
  • 运行于物理机(Xeon @ 2.27Ghz)对一个6.9GB的Redis实例执行fork操作耗时62ms(9ms/GB)。
  • 运行于6sync(KVM)虚拟机的Linux对360MB的Redis实例执行fork操作耗时8.2ms(23.3ms/GB)。
  • 运行于EC2,旧实例类型(Xen)的Linux对6.1GB的Redis实例执行fork操作耗时1460ms(239.3ms/GB)。
  • 运行于EC2,新实例类型(Xen)的Linux对1GB的Redis实例执行fork操作耗时10ms(10ms/GB)。
  • 运行于Linode(Xen)虚拟机的Linux对0.9GB的Redis实例执行fork操作耗时382ms(424ms/GB)。

你可以看到运行在Xen上的虚拟机会有一到两个数量级的性能损失。对于EC2的用户有个很简单的建议:使用现代的基于HVM的实例。

transparent huge pages引起的响应延迟

遗憾的是如果一个Linux内核启用了transparent huge pages,Redis为了将数据持久化到磁盘时调用fork将会引起很大的响应延迟。大内存页导致了以下问题:

  1. 当调用fork时,共享大内存页的两个进程将被创建。
  2. 在一个高负载的实例上,一些事件循环就将导致访问上千个内存页,导致几乎整个进程执行写时复制。
  3. 这将导致高响应延迟和大内存的使用。

请确保使用下面的命令关闭transparent huge pages:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

页交换引起的响应延迟(操作系统分页)

为了更高效地利用系统内存,Linux(以及很多其他的现代操作系统)能够将内存页迁移到磁盘,反之亦然。

如果内核将一个Redis的内存页从内存交换到磁盘文件,当Redis要访问该内存页中的数据时(比如访问该内存页中的一个键),内核为了将内存页从磁盘文件迁移回内存将会暂停Redis进程。这是一个涉及随机I/O的慢速操作(和访问一个已经在内存中的页面相比是非常慢的),这将导致导致Redis客户端感觉到异常的响应延迟。

内核将Redis内存页从内存交换到磁盘主要有三个原因:

  • 系统有内存压力,比如正在运行的进程需要比当前可用物理内存更多的内存。最简单的例子就是Redis使用了比可用内存更多的内存。
  • Redis实例中的数据集,或数据集中的一部分几乎是闲置状态(从未被客户端访问过),此时内核将把这部分内存页交换到磁盘上。这种情况非常罕见,因为即使是一个中等速度的Redis实例也经常会访问所有内存页,迫使内核将所有内存页保留在内存中。
  • 系统中的一些进程引发了大量读或者写这种I/O操作。因为一般文件都会被缓存,这将导致内核需要增加文件系统缓存,这会导致内存页交换。请注意这包括生成Redis RDB和/或AOF这些会生成大文件的后台线程。

幸运的是Linux提供了很不错的工具来检查这些问题,所以当由于内存页交换导致的响应延迟发生时我们应该怀疑是否是上面三个原因导致的。

首先要做的是检查有多少Redis内存页被交换到了磁盘。为了达到这个目的我们需要获得Redis实例的pid:

$ redis-cli info | grep process_id
process_id:5454

现在进入这个进程的文件系统目录:

$ cd /proc/5454

你可以在这里找到一个名为smaps的文件,这个文件描述了Redis进程的内存布局(假设你正在使用Linux 2.6.16或更高版本的内核)。这个文件包含了进程非常详细的内存布局信息,其中有一个名为Swap字段对我们很重要。然而,这里面不仅仅只有一个swap字段,因为smaps文件还包含了Redis进程的其他内存映射(进程的内存布局比一个内存页的线性数组要复杂得多)。

由于我们对进程的所有内存交换情况感兴趣,因此首先要做的就是找出该文件中的所有Swap字段:

$ cat smaps | grep 'Swap:'
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                 12 kB
Swap:                156 kB
Swap:                  8 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  4 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  4 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  4 kB
Swap:                  4 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB
Swap:                  0 kB

如果所有Swap低端都是0 Kb,或者只有零星的字段是4k,那么一切正常。实际上在我们这个例子中(线上真实的每秒处理上千请求的Redis实例)有一些条目表示存在更多的内存页交换问题。为了调查这是否是一个严重的问题,我们使用其他命令以便打印出内存映射的大小:

$ cat smaps | egrep '^(Swap|Size)'
Size:                316 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  8 kB
Swap:                  0 kB
Size:                 40 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:             720896 kB
Swap:                 12 kB
Size:               4096 kB
Swap:                156 kB
Size:               4096 kB
Swap:                  8 kB
Size:               4096 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:               1272 kB
Swap:                  0 kB
Size:                  8 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                 16 kB
Swap:                  0 kB
Size:                 84 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  8 kB
Swap:                  4 kB
Size:                  8 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  4 kB
Size:                144 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  4 kB
Size:                 12 kB
Swap:                  4 kB
Size:                108 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                272 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB

正如你在上面输出中所看到的,有一个720896 kB(其中只有12 kB的内存页交换)的内存映射,在另一个内存映射中交换了156 kB:只有很少一部分内存页被交换到磁盘,这没什么问题。

相反,如果有大量进程内存页被交换到磁盘,那么你的响应延迟问题可能和内存页交换有关。如果是这样的话,你可以使用vmstat命令来进一步检查你的Redis实例:

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
0  0   3980 697932 147180 1406456    0    0     2     2    2    0  4  4 91  0
0  0   3980 697428 147180 1406580    0    0     0     0 19088 16104  9  6 84  0
0  0   3980 697296 147180 1406616    0    0     0    28 18936 16193  7  6 87  0
0  0   3980 697048 147180 1406640    0    0     0     0 18613 15987  6  6 88  0
2  0   3980 696924 147180 1406656    0    0     0     0 18744 16299  6  5 88  0
0  0   3980 697048 147180 1406688    0    0     0     4 18520 15974  6  6 88  0
^C

你需要注意查看siso两列,这两列统计了内存页从内存交换到磁盘和从磁盘交换到内存的次数。如果在这两列中你看到非零值,就说明你的系统中存在内存页交换。

最后,可以使用iostat命令来检查系统的全局I/O活动。

$ iostat -xk 1
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
        13.55    0.04    2.92    0.53    0.00   82.95

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await  svctm  %util
sda               0.77     0.00    0.01    0.00     0.40     0.00    73.65     0.00    3.62   2.58   0.00
sdb               1.27     4.75    0.82    3.54    38.00    32.32    32.19     0.11   24.80   4.24   1.85

如果你的响应延迟问题是由Redis内存页交换导致的,你就需要降低系统中的内存压力,如果Redis使用了比可用内存更多内存的话你就增加更多内存,或者避免在同一个系统中运行其他需要大量内存的进程。

AOF和磁盘I/O引起的响应延迟

另一个响应延迟的原因是Redis的AOF。Redis使用了两个系统调用来完成AOF功能。一个是使用write(2)来将数据写入到只追加的文件中,另一个是使用fdatasync(2)来刷新内核文件缓冲区到磁盘以满足用户指定的持久化级别。

write(2)和fdatasync(2)都会造成响应延迟。例如当系统进程同步时write(2)会造成阻塞,或者当输出缓冲区满时内核需要将数据刷到磁盘上以便能接受新的写入。

fdatasync(2)会导致更严重的响应延迟,许多内核和文件系统对它的结合使用会导致数毫秒到数秒的延迟。当特别是在有其他进程正在执行I/O时。出于这些原因,从Redis 2.4开始fdatasync(2)会在另一个线程中执行。

我们将看到在使用AOF功能时,不同配置如何影响Redis的响应延迟。

AOF的配置项appendfsync可以有三种不同的方式来执行磁盘的fsync(这些配置可以在运行时使用CONFIG SET命令动态修改)。

  • 当appendfsync被设置为no时,Redis不执行fsync。这种配置下响应延迟的唯一原因就是write(2)了。这种情况下发生响应延迟一般没有解决方案,因为磁盘的处理速度跟不上Redis接收数据的速度,然而,如果当磁盘没有被其他进程的I/O拖慢时,这是很少见的。
  • 当appendfsync被设置成everysec时,Redis每秒执行一次fsync。Redis使用另外的线程执行fsync,如果此时fsync正在执行,Redis使用缓冲区来延迟2秒执行write(2)(因为在Linux中对一个正在执行fsync的文件执行write将被阻塞)。然而,如果fsync执行时间太长,即使fsync正在执行,Redis也将执行write(2),这会引起响应延迟。
  • 当appendfsync被设置成always时,Redis将在每次写操作发生时,返回OK给客户端之前执行fsync(实际上Redis会尝试将同一时间的多个命令的执行使用fsync一次性进行写入)。这种模式下Redis性能很低,此时一般建议使用高速硬盘和文件系统的实现以便能更快地完成fsync。

大多数Redis用户将appendfsync配置项设置为no或everysec。将响应延迟降低到最小的建议是避免在同一个系统中有其他进程执行I/O。使用固态硬盘也能降低I/O造成的的响应延迟,但一般来说当Redis写AOF时,如果此时硬盘上没有其他查找操作,非固态硬盘的性能也还不错。

如果你想调查AOF引起的响应延迟问题,你可以使用strace命令:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync

上面的命令将显示Redis在主线程中执行的所有fdatasync(2)系统调用情况。当appendfsync配置项被设置为everysec时,使用上面的命令无法查看到后台进程执行fdatasync系统调用情况,如果需要查看后台进程的情况,在strace命令上加上-f选项即可。

如果你同事想查看fdatasync和write两个系统调用的情况,使用下面的命令:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write

然而因为write(2)也被用来向客户端套接字写入数据,所有可能会显示很多与磁盘I/O无关的信息。显然strace命令无法只显示慢系统调用的信息,所以我们使用下面的命令:

sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished

过期数据引起的响应延迟

Redis有两种方式淘汰过期键:

  • 一个被动删除过期键的方式是当一个键被一个命令访问时,如果发现它已经过期就删除之。
  • 一个主动删除过期键的方式是每隔100毫秒删除掉一些过期键。

主动删除过期键这种方式被设计成自适应的。每隔100毫秒(即一秒执行10次)执行一次过期键删除,方式如下:

  • 根据ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP的值采样键,删除所有已经过期的键。
  • 如果采样出的键有超过25%的键过期了,重复这个采样过程。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP的默认值是20,采样删除过期键这个过程一秒执行10次,一般来说每秒最多有200个过期键被删除。那些已经过期很久的键也可以通过这种主动淘汰的方式被清除出数据库,所以被动删除方式意义不大。同时每秒钟删除200个键也不会引起Redis实例的响应延迟。

然而,主动淘汰算法是自适应的,如果在采样删除时有25%以上的键过期,将直接执行下一次循环。但考虑到我们每秒运行该算法10次,这意味着可能发生在同一秒内被采样的键25%以上都过期的情况。

基本上就是说,如果数据库在同一秒中有非常多键过期,而这些键至少占当前已经过期键的25%时,Redis为了让过期键占所有键的比例下降到25%以下将会阻塞。

这种做法是必要的,这可以避免已经过期的键占用太多内存。而且这种方式通常来说是绝对无害的,因为在同一秒有大量键过期的情况非常奇怪,但这种情况也不是完全不可能发生,因为用户可以使用EXPIREAT命令为键设置相同的过期时间。

简而言之:注意大量键同时过期引起的响应延迟。

Redis软件监控

Redis 2.6引入了Redis软件监控的调试工具,用来跟踪那些无法用常规工具分析出的响应延迟问题。

Redis的软件监控是一个实验性地功能。虽然它被设计用于生产环境,由于在使用时它可能会与Redis服务器的正常运行产生意外的交互,所以使用前应该先备份数据库。

需要特别说明的是只有在万不得已的情况下再使用这种方式跟踪响应延迟问题。

下面是这个功能的工作细节:

  • 用户使用CONFIG SET命令开启软件监控功能。
  • Redis不断地监控自己。
  • 如果Redis检测到服务器被一些操作阻塞导致无法快速响应,则可能是响应延迟的问题所在,将会生成一份服务器在何处被阻塞的底层日志报告。
  • 用户在Redis的Google Group中联系开发者,并展示监控报告的内容。

注意该功能无法在redis.conf文件中开启,因为这个功能被设计用于调试正在运行的实例。

使用下面的命令开启次功能:

CONFIG SET watchdog-period 500

命令中的时间单位是毫秒。上面的示例中制定了尽在检测到服务器有超过500毫秒的延迟时才记录问题。最小的克配置时间是200毫秒。

当你不需要软件监控功能时,可以通过将watchdog-period参数设置为0来关闭它。 非常重要:请记得在不需要软件监控时关闭它,因为一般来说长时间在实例上运行软件监控不是个好主意。

下面的例子展示了软件监控检测到了响应延迟超过配置时间的情况,在日志文件中输出的信息:

[8547 | signal handler] (1333114359)
--- WATCHDOG TIMER EXPIRED ---
/lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d]
/lib/libpthread.so.0(+0xf8f0) [0x7f16b5f158f0]
/lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d]
/lib/libc.so.6(usleep+0x34) [0x7f16b5c62844]
./redis-server(debugCommand+0x3e1) [0x43ab41]
./redis-server(call+0x5d) [0x415a9d]
./redis-server(processCommand+0x375) [0x415fc5]
./redis-server(processInputBuffer+0x4f) [0x4203cf]
./redis-server(readQueryFromClient+0xa0) [0x4204e0]
./redis-server(aeProcessEvents+0x128) [0x411b48]
./redis-server(aeMain+0x2b) [0x411dbb]
./redis-server(main+0x2b6) [0x418556]
/lib/libc.so.6(__libc_start_main+0xfd) [0x7f16b5ba1c4d]
./redis-server() [0x411099]
------

注意:在这个例子中DEBUG SLEEP命令用来使服务器阻塞。这个服务器阻塞的栈跟踪信息会随服务器上下文而异。

我们鼓励你将所搜集到的监控栈跟踪信息发送至Redis Google Group:获得的信息越多,就越能轻松了解你的Redis实例的问题所在。


Read More

c/c++安全编码 总结


十分粗糙的读书记录,主要是案例大多不怎么醒目

字符串

  • 标准库函数拷贝越界 puts越界 -> 缓冲区溢出 ->栈溢出 ->代码注入

  • 字符串数组越界

  • 空字符串结尾错误

  • 处理字符串漏洞的方案

    • 输入验证,避免缓冲区溢出
  • 堆栈保护器

几个安全函数替代以及使用(注意,gcc基本没实现,用不上唉)

原函数 安全版本 变化后的函数原型 引入版本
vsnprintf vsnprintf_s 无变化 C11
memcpy memcpy_s 增加长度为第二个参数,限制拷贝长度 errno_t memcpy_s( void *restrict dest, rsize_t destsz, const void *restrict src, rsize_t count ); C11
strncpy strncpy_s 同上 errno_t strncpy_s(char *restrict dest, rsize_t destsz, const char *restrict src, rsize_t count); C11
snprintf snprintf_s 无变化 c11
sscanf sscanf_s 无变化,但涉及到%c %s %[ 要提供长度信息 https://en.cppreference.com/w/c/io/fscanf https://stackoverflow.com/questions/24078746/confusion-with-sscanf-s c11
memset memset_s 增加长度为第二个参数,限制拷贝长度errno_t memset_s( void *dest, rsize_t destsz, int ch, rsize_t count ); c11
注意需要定义
`__STDC_WANT_LIB_EXT1__`

指针

  • 缓冲溢出改写函数指针
  • 修改指令指针的指向
  • 修改全局偏移表
  • 改写.dtor区,调用析构转移权限
  • 改写虚函数指针
  • atexit注入,转移权限
  • longjmp溢出

内存管理

  • 不要假定分配的内存被初始化了
  • 检查malloc返回值 (不过现在的设备,霉有检查的必要吧,如果用new 遇到bad_alloc挂掉就挂掉吧)
  • 指针引用,不要引用已经释放 的指针
  • double free
  • 内存泄漏问题
  • malloc(0) 傻逼行为
  • 垃圾回收中的伪装指针 -> std::pointer_safety 看SO这个问题https://stackoverflow.com/questions/27728142/c11-what-is-its-gc-interface-and-how-to-implement
  • 在条件分支中没有检查new分配失败,因为new失败抛异常,强制nothrow
  • new delete malloc free没有正确配对
    • 注意new数组的坑
    • 如果是placement new一定要知道自己在做什么
  • 容器装指针导致的释放 ->智能指针 ptr_container等等
    • 引入智能指针,比如shared_ptr,有可能又引入循环引用。注意
  • 析构函数不能抛异常
  • gc库原理以及对gc库的缓冲区溢出共计,复写边界标志块
  • 一些解决方案,静态检查等等

整数安全

  • 无符号数回绕
    • ` if (sum +i > UINT_MAX) -> if (i > UINT_MAX - sum)`
    • if (sum -j < 0) -> if (sum < j)
  • 有符号整数类型溢出,注意值范围

    • 除了char默认都是signed char在arm上表现是unsigned
  • 整数转换与类型提升。

    • 简单说,低到高的转换提升可以,注意符号可能错误解释但是没关系,高到低可能会丢或者读错数据
  • 整数操作,注意溢出回绕与截断场景,几个规避方案
    • 用更大的类型强制转换,简单粗暴
    • 先验 ,类似上面的回绕判定
    • 后验
      • 状态位,比如jc。这种实践价值不大
      • 类似回绕,反向判断运算结果是不是被回绕了
  • 重点考虑一下场景
    • 数组索引

    • 指针计算

    • 对象长度大小

    • 循环边界

    • 内存分配

      • 比如传入有符号数但是这个数可能溢出了 or截断了

格式化输出

  • %s替换导致的缓冲区溢出
  • 格式化字符和对应的参数不匹配,导致读取栈上内存,可能读到错误地址segfault
    • 通过这个手段查看栈内容
    • %n 通过printf改写内存
  • 规避方案。避免用户输入字符串,编译检查避免低级错误

并发

  • 概念:阿姆达尔定律?竞争条件和临界区。也有翻译成竞态的
  • volatile 不保证多个线程间同步,不防止并发内存访问,不保证对对象的原子性访问(经常被滥用)
  • 内存模型以及原语。这里没有讲漏洞,只是讲了几个锁,原子量的用法以及避免用错锁

文件IO

  • 进程特权
  • 文件权限
  • 目录 缓冲区溢出
  • 等价错误
  • 符号链接串改

上述解决方案,目录规范化

  • 文件竞争!

推荐实践

  • 安全的开发生命周期
  • QA
  • 设计
  • 静态检查,编译检查等等
  • 验证,代码审计,静态分析,渗透测试,安全检查,攻击面回顾等等

###

gcc/linux平台安全编译链接选项

  • 栈溢出保护 (c_flags)-fstack-protector-all/-fstack-protector-strong
    • 原理,canary word,在堆栈边界标记,然后检查,变过,则发生了溢出 会打印__stack_chk_fail
  • 重定位只读 (ld_flags) -Wl, -z,relro 避免缓冲区溢出修改GOT
    • 立即绑定 (ld_flags) -Wl, -z,relro, -z,now
    • 原理,符号只读不能复写,直接segfault
    • 防不了GOT PLT重写
  • 禁止缓存区溢出shellcode (ld_flags) -Wl, -z, noexecstack
    • 原理,代码段标记不可执行,访问直接segfault 通过观察data端可以看到权限是rwp不是rwxp
    • 注意,堆区,栈区shellcode无效
  • 位置无关代码(c_flags) -fPIC 这个和安全关系不是特别大感觉
  • 位置无关可执行文件(c_flags, ld_flags) -fPIE -pie
  • 实时路径选择 –rpath,可能会被替换造成hook,不安全
    • 动态链接库查找顺序,检查rpath,检查LD_LIBRARY_PATH,检查ld.so.cache,rpath优先级较高导致hook
  • 隐藏符号 -fvisibility=hidden针对共享库隐藏符号
  • 堆栈检查 -fstack-check影响性能
    • 检查栈空间大小,预留缓冲区被使用会告警
  • 删除符号 strip(ld_flags) -s 影响调试
  • 整数溢出-ftrapv 强制检查一遍所有参数再计算,异常直接abort,严重影响性能和稳定性,应该没人用这个吧
  • 缓冲区检查 宏-D_FORTIFY_SOURCE=2 影响运行时性能
    • 原理,hook函数, 替换成_chk函数,检查长度
    • 缺陷,动态分配的空间无法检查

Read More

mysql几个优化


查询

where子句有没有用错??索引有没有用错,有没有索引?表规模大不大?能不能并发遍历?

能不能归档?

参考链接 1 2 3

插入

1)提高数据库插入性能中心思想:尽量将数据一次性写入到Data File和减少数据库的checkpoint 操作。这次修改了下面四个配置项:

1)将 innodb_flush_log_at_trx_commit 配置设定为0;按过往经验设定为0,插入速度会有很大提高。

0: 日志缓冲每秒一次地被写到日志文件,并且对日志文件做到磁盘操作的刷新,但是在一个事务提交不做任何操作。

1:在每个事务提交时,日志缓冲被写到日志文件,对日志文件做到磁盘操作的刷新。

2:在每个提交,日志缓冲被写到文件,但不对日志文件做到磁盘操作的刷新。对日志文件每秒刷新一次。

默认值是 1,也是最安全的设置,即每个事务提交的时候都会从 log buffer 写

到日志文件,而且会实际刷新磁盘,但是这样性能有一定的损失。如果可以容忍在数

据库崩溃的时候损失一部分数据,那么设置成 0 或者 2 都会有所改善。设置成 0,则

在数据库崩溃的时候会丢失那些没有被写入日志文件的事务,最多丢失 1 秒钟的事

务,这种方式是最不安全的,也是效率最高的。设置成 2 的时候,因为只是没有刷新

到磁盘,但是已经写入日志文件,所以只要操作系统没有崩溃,那么并没有丢失数据 ,

比设置成 0 更安全一些。

在 mysql 的手册中,为了确保事务的持久性和复制设置的耐受性、一致性,都是

建议将这个参数设置为 1 的。

2)将 innodb_autoextend_increment 配置由于默认8M 调整到 128M

此配置项作用主要是当tablespace 空间已经满了后,需要MySQL系统需要自动扩展多少空间,每次tablespace 扩展都会让各个SQL 处于等待状态。增加自动扩展Size可以减少tablespace自动扩展次数。

3)将 innodb_log_buffer_size 配置由于默认1M 调整到 16M

此配置项作用设定innodb 数据库引擎写日志缓存区;将此缓存段增大可以减少数据库写数据文件次数。

4)将 innodb_log_file_size 配置由于默认 8M 调整到 128M

此配置项作用设定innodb 数据库引擎UNDO日志的大小;从而减少数据库checkpoint操作。

经过以上调整,系统插入速度由于原来10分钟几万条提升至1秒1W左右;注:以上参数调整,需要根据不同机器来进行实际调整。特别是 innodb_flush_log_at_trx_commit、innodb_log_buffer_size和 innodb_log_file_size 需要谨慎调整;因为涉及MySQL本身的容灾处理。

(2)提升数据库读取速度,重数据库层面上读取速度提升主要由于几点:简化SQL、加索引和分区; 经过检查程序SQL已经是最简单,查询条件上已经增加索引。我们只能用武器:表分区。

数据库 MySQL分区前准备:在MySQL中,表空间就是存储数据和索引的数据文件。

将S11数据库由于同享tablespace 修改为支持多个tablespace;

将wb_user_info_sina 和 wb_user_info_tx 两个表修改为各自独立表空间;(Sina:1700W数据,2.6G 大数据文件,Tencent 1400W,2.3G大数据文件);

分区操作:

将现有的主键和索引先删除

重现建立id,uid 的联合主键

再以 uid 为键值进行分区。这时候到/var/data/mysql 查看数据文件,可以看到两个大表各自独立表空间已经分割成若干个较少独立分区空间。(这时候若以uid 为检索条件进行查询,并不提升速度;因为键值只是安排数据存储的分区并不会建立分区索引。我非常郁闷这点比Oracle 差得不是一点半点。)

再以 uid 字段上进行建立索引。再次到/var/data/mysql 文件夹查看数据文件,非常郁闷地发现各个分区Size竟然大了。MySQL还是老样子将索引与数据存储在同一个tablespace里面。若能index 与 数据分离能够更加好管理。

经过以上调整,暂时没能体现出系统读取速度提升;基本都是在 2~3秒完成5K数据更新。

MySQL数据库插入速度调整补充资料:

MySQL 从最开始的时候 1000条/分钟的插入速度调高至 10000条/秒。 相信大家都已经等急了相关介绍,下面我做调优时候的整个过程。提高数据库插入性能中心思想:

1、尽量使数据库一次性写入Data File

2、减少数据库的checkpoint 操作

3、程序上尽量缓冲数据,进行批量式插入与提交

4、减少系统的IO冲突

根据以上四点内容,作为一个业余DBA对MySQL服务进行了下面调整:

修改负责收录记录MySQL服务器配置,提升MySQL整体写速度;具体为下面三个数据库变量值:innodb_autoextend_increment、innodb_log_buffer_size、innodb_log_file_size;此三个变量默认值分别为 5M、8M、8M,根据服务器内存大小与具体使用情况,将此三只分别修改为:128M、16M、128M。同时,也将原来2个 Log File 变更为 8 个Log File。此次修改主要满足第一和第二点,如:增加innodb_autoextend_increment就是为了避免由于频繁自动扩展Data File而导致 MySQL 的checkpoint 操作;

将大表转变为独立表空并且进行分区,然后将不同分区下挂在多个不同硬盘阵列中。

完成了以上修改操作后;我看到下面幸福结果:

获取测试结果:

Query OK, 2500000 rows affected (4 min 4.85 sec)

Records: 2500000 Duplicates: 0 Warnings: 0

Query OK, 2500000 rows affected (4 min 58.89 sec)

Records: 2500000 Duplicates: 0 Warnings: 0

Query OK, 2500000 rows affected (5 min 25.91 sec)份额为

Records: 2500000 Duplicates: 0 Warnings: 0

Query OK, 2500000 rows affected (5 min 22.32 sec)

Records: 2500000 Duplicates: 0 Warnings: 0

最后表的数据量:

+————+

count(*)

+————+

10000000

+————+

从上面结果来看,数据量增加会对插入性能有一定影响。不过,整体速度还是非常面议。一天不到时间,就可以完成4亿数据正常处理。

参考链接 4 5 6 7

主从复制延时

https://www.wencst.com/archives/1750

https://www.jianshu.com/p/ed19bb0e748a

mysql数据库优化

https://www.wencst.com/archives/1781

https://www.wencst.com/archives/1774

不得不说资料真多啊

ref

  1. https://blog.csdn.net/lchq1995/article/details/83308290
  2. https://blog.csdn.net/u011296485/article/details/77509628
  3. https://blog.csdn.net/u011193276/article/details/82195039
  4. https://www.jianshu.com/p/d017abaea8d1
  5. https://database.51cto.com/art/201901/590958.htm 提到了innodb_flush_log_at_trx_commit
  6. https://www.cnblogs.com/jpfss/p/10772962.html 同上
  7. 常见的插入慢 http://mysql.taobao.org/monthly/2018/09/07/

contacts

Read More

gprof和oprofile使用


gprof

编译

如果是makefile or 命令行 需要在编译选项里加上-pg -g 在链接里加上 -pg

cmake见参考链接1

cmake -DCMAKE_CXX_FLAGS=-pg -DCMAKE_EXE_LINKER_FLAGS=-pg -DCMAKE_SHARED_LINKER_FLAGS=-pg <SOURCE_DIR>

使用

见参考链接4

主要用法,编译生成二进制,运行一段时间,正常退出后生成gmon.out,然后gprof解析

gprof ./prog gmon.out -b 

具体的参数解析见参考链接4

缺陷 /注意事项

如果进程不是正常退出,程序不会生成gmon.out文件。详见参考链接2和3,3也有规避方法,利用信号让他正常退出即可,参考链接4内记录了一些注意事项。列在引用。值得一看

oprofile

安装5

yum -y install oprofile
yum -y install kernel-debuginfo

注意debuginfo必须,不然没有vmlinux文件/usr/lib/debug/lib/modules/3.10.0.x86_64/vmlinux

启动需要设置

opcontrol --vmlinux=/usr/lib/debug/lib/modules/3.10.0.x86_64/vmlinux
opcontrol --start-daemon
opcontrol --start
opcontrol --dump
opreport
opreport --symbols
opannotate -s --source=binname  >opannotate.log

ref

  1. https://stackoverflow.com/questions/26491948/how-to-use-gprof-with-cmake

  2. https://web.eecs.umich.edu/~sugih/pointers/gprof_quick.html

    ​ Things to keep in mind:

    • If gmon.out already exists, it will be overwritten.
    • The program must exit normally. Pressing control-c or killing the process is not a normal exit.
    • Since you are trying to analyze your program in a real-world situation, you should run the program exactly the same way as you normally would (same inputs, command line arguments, etc.).
  3. https://blog.csdn.net/crazyhacking/article/details/11972889

    gprof不能产生gmom.out文件的原因:gprof只能在程序正常结束退出之后才能生成程序测评报告,原因是gprof通过在atexit()里注册了一个函数来产生结果信息,任何非正常退出都不会执行atexit()的动作,所以不会产生gmon.out文件。所以,以下情况可能不会有gmon.out文件产生: 1,程序不是从main return或exit()退出,则可能不生成gmon.out。 2,程序如果崩溃,可能不生成gmon.out。 3,测试发现在虚拟机上运行,可能不生成gmon.out。 4,程序忽略SIGPROF信号!一定不能捕获、忽略SIGPROF信号。man手册对SIGPROF的解释是:profiling timer expired. 如果忽略这个信号,gprof的输出则是:Each sample counts as 0.01 seconds. no time accumulated. 5,如果程序运行时间非常短,则gprof可能无效。因为受到启动、初始化、退出等函数运行时间的影响。如果你的程序是一个不会退出的服务程序,那就只有修改代码来达到目的。如果不想改变程序的运行方式,可以添加一个信号处理函数解决问题(这样对代码修改最少),例如:  static void sighandler( int sig_no )  {  exit(0);  }  signal( SIGUSR1, sighandler ); 这样当使用kill -USR1 pid 后,程序退出,生成gmon.out文件。

  4. https://www.cnblogs.com/youxin/p/7988479.html

    1. g++在编译和链接两个过程,都要使用-pg选项。

    2. 只能使用静态连接libc库,否则在初始化*.so之前就调用profile代码会引起“segmentation fault”,解决办法是编译时加上-static-libgcc或-static。

    3. 如果不用g++而使用ld直接链接程序,要加上链接文件/lib/gcrt0.o,如ld -o myprog /lib/gcrt0.o myprog.o utils.o -lc_p。也可能是gcrt1.o

    4. 要监控到第三方库函数的执行时间,第三方库也必须是添加 –pg 选项编译的。

    5. gprof只能分析应用程序所消耗掉的用户时间.

    6. 程序不能以demon方式运行。否则采集不到时间。(可采集到调用次数)

    7. 首先使用 time 来运行程序从而判断 gprof 是否能产生有用信息是个好方法。

    8. 如果 gprof 不适合您的剖析需要,那么还有其他一些工具可以克服 gprof 部分缺陷,包括 OProfile 和 Sysprof。

    9. gprof对于代码大部分是用户空间的CPU密集型的程序用处明显。对于大部分时间运行在内核空间或者由于外部因素(例如操作系统的 I/O 子系统过载)而运行得非常慢的程序难以进行优化。

    10. gprof 不支持多线程应用,多线程下只能采集主线程性能数据。原因是gprof采用ITIMER_PROF信号,在多线程内只有主线程才能响应该信号。但是有一个简单的方法可以解决这一问题:http://sam.zoy.org/writings/programming/gprof.html

    11. gprof只能在程序正常结束退出之后才能生成报告(gmon.out)。

      a) 原因: gprof通过在atexit()里注册了一个函数来产生结果信息,任何非正常退出都不会执行atexit()的动作,所以不会产生gmon.out文件。

      b) 程序可从main函数中正常退出,或者通过系统调用exit()函数退出。

  5. http://www.serpentine.com/blog/2006/12/17/make-linux-performance-analysis-easier-with-oprofile/

contacts

Read More

docker pull Error response from daemon x509 certificate signed by unknown authority.


场景

centos环境。docker 1.18版本

使用公司内网镜像库,docker pull报错提示

docker: Error response from daemon: Get linkxxxxx : x509: certificate signed by unknown authority.

上面的linkxxxxx是内网网址

解决办法 -> daemon.json

daemon.json相当于工程模式加载配置。

默认这个json是不存在的,在/etc/docker/daemon.json,需要创建一个,另外,低版本的docker(1.12.6以下)不支持这个配置加载。需要注意

然后添加如下json

{
    "insecure-registries":["linkxxxxx"]
}

其中linkxxxxx是上面的网址。注意这个镜像网址,不需要多余的东西,类似 register.xx.com 不要有https,不要有斜杠,否则会有报错

systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Fri 2020-04-03 09:52:46 CST; 53s ago
     Docs: https://docs.docker.com
  Process: 30506 ExecStart=/usr/bin/dockerd $OPTIONS $DOCKER_STORAGE_OPTIONS $DOCKER_NETWORK_OPTIONS $INSECURE_REGISTRY (code=exited, status=1/FAILURE)
 Main PID: 30506 (code=exited, status=1/FAILURE)

Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.183750000+08:00" level=info msg=serving... address="/var/run/docker/containerd/cont>
Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.183810260+08:00" level=info msg=serving... address="/var/run/docker/containerd/cont>
Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.183828300+08:00" level=info msg="containerd successfully booted in 0.006990s"
Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.188993960+08:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0x400>
Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.189921780+08:00" level=warning msg="insecure registry https://registry-xx>
Apr 03 09:52:45 kwephispra38428 dockerd[30506]: time="2020-04-03T09:52:45.190047920+08:00" level=info msg="stopping healthcheck following graceful shutdown" >
Apr 03 09:52:46 kwephispra38428 dockerd[30506]: Error starting daemon: insecure registry registry-xx.com/ is not valid: invalid host "
Apr 03 09:52:46 kwephispra38428 systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
Apr 03 09:52:46 kwephispra38428 systemd[1]: docker.service: Failed with result 'exit-code'.
Apr 03 09:52:46 kwephispra38428 systemd[1]: Failed to start Docker Application Container Engine.

修改完成后重启docker daemon

systemctl daemon-reload
systemctl restart docker

我搜到了很多导入证书之类的邪门歪道。误入歧途了一上午

ref

  • https://blog.csdn.net/u013948858/article/details/79974796 这个文档介绍了daemon.json的具体使用,介绍了上面那个json用法。官方给的配置项比较多。不需要都了解,用啥加啥
  • https://forums.docker.com/t/docker-private-registry-x509-certificate-signed-by-unknown-authority/21262/8 看这个社区问答,大家都把关注点放在证书上了,各种折腾,最后有个人说了在mac下 insecure registries 设置。在linux下也就是上面daemon.json的搞法
  • docker镜像默认是在/var/lib/docker里的,可以手动设置改目录,也是在daemon.json里,加上"graph":"newpath/docker/" 参考这里https://blog.51cto.com/forangela/1949947

contacts

Read More

(译)unordered set 背后的堆分配行为

翻译整理自 https://bduvenhage.me/performance/2019/04/22/size-of-hash-table.html

其实文章写得通俗易懂没必要翻译。感谢原作者的分享

1. std::unoredered_set的实现

std::unorederd_set 是hash set实现的,这个容器通过桶 bucket来维护元素,当插入一个元素,先计算hash计算出桶索引bucket index,然后元素加入到桶中。桶典型由链表来维护

unoredered_set会记录每个桶的负载因子(load factor) 也就是平均每个桶放几个元素,默认是1,如果负载因子超过了默认值,那就需要扩容2倍,即让负载因子降低成原来的一半。负载因子低,每个桶的元素少,这样桶的链表就短,维护链表的开销旧地。

2. 堆分配表现

测试数据条件

32位无符号整形,插入2000万数据,代码见参考链接,编译使用clang10 O3 (xcode release)

上图,每次大幅度跳跃都是 调整负载因子的场景。作者想用valgrind massif工具来分析 std::unordered_set的内存占用,但是valgrind不支持macos,所以作者写了个alloctor来调用默认的alloctor,只是记录次数

上图,随机插入数据,每次负载因子达到上限,都会重新调整桶数量,使负载因子降低到上限的一半

增加桶的个数需要重新分配内存,调整元素的位置,上图展示了每次重整耗费的时间

保存两千万个32位无符号数据需要657M内存,细节如下

元素大小 已经分配的元素总数 在堆上的元素总数
size=8 52679739 26339969
size = 24 19953544 19953544

每个桶的元素结构基本上是这样的

struct BucketItem
{
    size_t hash_;
    uint32_t item_; 
    //4 bytes of padding lay here.
    BucketItem *next_;
};

数据size=24为什么两个数据是相等的?

3. 总结 && 参考链接

博客地址 https://bduvenhage.me/performance/2019/04/22/size-of-hash-table.html

作者的代码 https://github.com/bduvenhage/Bits-O-Cpp/blob/master/containers/main_hash_table.cpp


Read More

如何快速制作一个python包

主要是依靠modern-package-template这个工具。非常方便,推荐给大家

按照readme一步一步来就可以了

以我这个包 https://github.com/wanghenshui/fake_redis_command为例子

首先安装

pip install modern-package-template

然后创建一个包项目

paster create -t modern_package helloworld

然后项目就搞好了,按照生成文件夹里的hacking一步一步搞就行了,当然最后

python setup.py install

就可以安装了。后面还有上传到pip仓库的操作。很简单,但是我网络不行传不上去。

要注意的只有setup.py文件的写法

目录结构不重要,重要的是setup.py文件要写好

以我的包为例,目录结构是

fake_redis_command/
├── HACKING.txt
├── MANIFEST.in
├── NEWS.txt
├── README.rst
├── bootstrap.py
├── build
├── dist
│   ├── fake_redis_command-0.1-py3.6.egg
│   └── fake_redis_command-0.1.tar.gz
├── examples
│   └── random_command.py
├── setup.py
└── src
    ├── fake_redis_command
    │   └── `__init__`.py
    └── fake_redis_command.egg-info
        ├── PKG-INFO
        ├── SOURCES.txt
        ├── dependency_links.txt
        ├── entry_points.txt
        ├── not-zip-safe
        ├── requires.txt
        └── top_level.txt

大部分都是和包不相关的,主要setup.py文件要写对,比如我的文件只有src/fake_redis_command/下一个文件,还是__init__.py文件,所以setup.py文件里写的

    packages=find_packages('src'),
    package_dir = {'': 'src'},include_package_data=True,

使用就可以直接用

from fake_redis_command import redis_command

因为我的redis_command实现在__init__.py文件, 最简单。但是项目复杂一些,不可能放在同一个文件,来看看redis-py的setup.py文件 https://github.com/andymccurdy/redis-py/blob/master/setup.py

packages=['redis'],

redis的目录也很简单,就一层redis目录,没有我那层src嵌套(模板生成的我也不想)

写的很简单,说明其他定义在redis目录下的__init__.py文件里,但是目录下有很多文件,那肯定就是只导出了。

再看https://github.com/andymccurdy/redis-py/blob/master/redis/init.py

果不其然,把所有的类和异常都暴露出来了。这样import redis就能直接用了。

说的十分粗糙,但是临时用一下造一个粗糙的轮子包够用了。


Read More

(译)yugabytedb 在rocksdb上做的改动

这是翻译整理

一张图概括改动点

Scan Resistant Cache

这个概念我不知道怎么翻译,总之就是一个能够抗住Scan动作污染的cache

yugabytedb针对rocksdb lru cache的缺陷,做了个优化 https://github.com/YugaByte/yugabyte-db/commit/0c6a3f018ac90724ac1106ff248c051afbdd6979

作者也说了,这个实现和mysql/hbase类似,是2Q

我的疑问

  • 为啥rocksdb不加上?找到了一个commit,没看出来加在哪里了 https://github.com/facebook/rocksdb/commit/6e78fe3c8d35fa1c0836af4501e0f272bc363bab
  • 实现Scan Resistant cache需要做什么?2Q ARC都是怎么抵消影响的?

Block-based Splitting of Bloom/Index Data

rocksdb的sst文件包含元数据,比如index和bloom filter。作者观点这个数据是没有必要的,可以砍掉了,毕竟都在内存里 修改在这里https://github.com/YugaByte/yugabyte-db/commit/147312863b104d2d4b2f267cbb6b4fc95f35f3a8

Multiple Instances of RocksDB Per Node

多rocksdb实例。这个很常见,作者列出来几个设计优势

  • 集群节点的负载均衡 节点failover 添加节点都变的非常简单,可以直接搬迁sst文件。
  • 删掉表直接删掉rocksdb实例
  • 可以对一个表做不同的存储策略,压缩开关?压缩算法自定义?编码自定义(前缀etc)
  • 可以对一个表做不通的布隆过滤侧率,不同主键,不同桶数etc
  • 。。。还有一个和yogabyte db业务相关,没看懂,没翻译 Allows DocDB to track min/max values for each clustered column in the primary key of a table (another enhancement to RocksDB) and store that in the corresponding SSTable file as metadata. This enables YugabyteDB to optimize range predicate queries like by minimizing the number of SSTable files that need to be looked up:

但是这种用法工程实践上的坑需要注意

全局block cache

writebuffer memtable wal大小限制

过大重启重放日志会很痛苦,这里yugabyte db做了改进,尽可能的刷数据 https://github.com/YugaByte/yugabyte-db/commit/faed8f0cd55e25f2e72c39fffa72c27c5f84fca3

针对不同规模的compaction做分类,不同的队列来做

改进在这里https://github.com/YugaByte/yugabyte-db/commit/dde2ecd5ddf4b01879e32f033e0a80e37e18341a

  • 事实上有观点认为应该把linux调度算法搬到这里。rocksdb的compaction策略粗糙,而两个队列算是个粗糙调度

Smart Load Balancing Across Multiple Disks 这个没看懂

DocDB supports a just-a-bunch-of-disks (JBOD) setup of multiple SSDs and doesn’t require a hardware or software RAID. The RocksDB instances for various tablets are balanced across the available SSDs uniformly, on a per-table basis to ensure that each SSD has a similar number of tablets from each table and is taking uniform type of load. Other types of load balancing in DocDB are also done on a per-table basis, be it:

  • Balancing of tablet replicas across nodes
  • Balancing of leader/followers of Raft groups across nodes
  • Balancing of Raft logs across SSDs on a node

附加改动

用raft替代rocksdb的wal

mvcc

用的混合逻辑时钟

Hybrid Logical Clock (HLC), the hybrid timestamp assignment algorithm, is a way to assign timestamps in a distributed system such that every pair of “causally connected” events results in an increase of the timestamp value. Please refer to these reports (#1 or #2) for more details.

参考

  • 原文 https://blog.yugabyte.com/enhancing-rocksdb-for-speed-scale/
  • 这个是arc介绍,效果优于lru http://hcoona.github.io/Paper-Note/arc-one-up-on-lru/
    • 论文原文https://www.usenix.org/conference/fast-03/arc-self-tuning-low-overhead-replacement-cache
    • 据说zfs也用。
  • 搜到的一个issue,建议kudu替换他们的cache系统,换成arc或者hbase 2Q,https://issues.apache.org/jira/browse/KUDU-613
  • scan resistant cache貌似是个热点问题?搜关键字蹦出好几个论文。不列举了。
  • 这里有个2Q的介绍 https://flak.tedunangst.com/post/2Q-buffer-cache-algorithm 值得翻译一下

Read More

^