asan常见的抓错报告



asan常见的 抓错报告 编译带上 -fsanitize=address 链接带上 -lasan

global-buffer-overflow memcmp的长度可能越界

R: AddressSanitizer: global-buffer-overflow on address 0x000000a8f8ff at pc 0x7ff6eafde870 bp 0x7ffc75471220 sp 0x7ffc754709d0 READ of size 49 at 0x000000a8f8ff thread T0 #0 0x7ff6eafde86f in __interceptor_memcmp ../../../../gcc-5.4.0/libsanitizer/asan/asan_interceptors.cc:333

注意memcmp的第三个参数,取两个字符串中最小的长度

相关概念 OOB memory access

heap-buffer-overflow strlen访问内存越界

assert(n == strlen(val)); AddressSanitizer: heap-buffer-overflow

可能字符串没有分配’\0’的空间,用strlen会导致堆空间越界

AddressSanitizer: attempting to call malloc_usable_size

这个rocksdb的报错。 搜了一圈,二进制是jemalloc编的,和asan和rocksdb 有冲突产生的报错。临时禁止掉

ASAN_OPTIONS=check_malloc_usable_size=0

重编二进制,不带jemalloc,好使了

AddressSanitizer: attempting to call malloc_usable_size() for pointer which is not owned: 0x7f121aed6000
    #0 0x7f121f506990 in __interceptor_malloc_usable_size ../../../../gcc-5.4.0/libsanitizer/asan/asan_malloc_linux.cc:104
    #1 0x8c7929 in rocksdb::Arena::AllocateNewBlock(unsigned long) util/arena.cc:221
    #2 0x8c79c4 in rocksdb::Arena::AllocateFallback(unsigned long, bool) util/arena.cc:114
    #3 0x8df67a in rocksdb::LogBuffer::AddLogToBuffer(unsigned long, char const*, __va_list_tag*) util/log_buffer.cc:24
    #4 0x8df8c8 in rocksdb::LogToBuffer(rocksdb::LogBuffer*, char const*, ...) util/log_buffer.cc:88
    #5 0x749300 in rocksdb::DBImpl::FlushMemTableToOutputFile(rocksdb::ColumnFamilyData*, rocksdb::MutableCFOptions const&, bool*, rocksdb::JobContext*, rocksdb::SuperVersionContext*, rocksdb::LogBuffer*) db/db_impl_compaction_flush.cc:183
    #6 0x74c1f4 in rocksdb::DBImpl::FlushMemTablesToOutputFiles(rocksdb::autovector<rocksdb::DBImpl::BGFlushArg, 8ul> const&, bool*, rocksdb::JobContext*, rocksdb::LogBuffer*) db/db_impl_compaction_flush.cc:229
    #7 0x74d3b0 in rocksdb::DBImpl::BackgroundFlush(bool*, rocksdb::JobContext*, rocksdb::LogBuffer*, rocksdb::FlushReason*) db/db_impl_compaction_flush.cc:2025
    #8 0x74da4f in rocksdb::DBImpl::BackgroundCallFlush() db/db_impl_compaction_flush.cc:2059
    #9 0x8e8a27 in std::function<void ()>::operator()() const /usr/local/include/c++/5.4.0/functional:2267
    #10 0x8e8a27 in rocksdb::ThreadPoolImpl::Impl::BGThread(unsigned long) util/threadpool_imp.cc:265
    #11 0x8e8c0e in rocksdb::ThreadPoolImpl::Impl::BGThreadWrapper(void*) util/threadpool_imp.cc:303
    #12 0x7f121e1fb8ef in execute_native_thread_routine ../../../../../gcc-5.4.0/libstdc++-v3/src/c++11/thread.cc:84
    #13 0x7f121dd19dc4 in start_thread (/lib64/libpthread.so.0+0x7dc4)
    #14 0x7f121da477fc in __clone (/lib64/libc.so.6+0xf67fc)

AddressSanitizer can not describe address in more detail (wild memory access suspected).
SUMMARY: AddressSanitizer: bad-malloc_usable_size ../../../../gcc-5.4.0/libsanitizer/asan/asan_malloc_linux.cc:104 __interceptor_malloc_usable_size
Thread T2 created by T0 here:
    #0 0x7f121f4a80d4 in __interceptor_pthread_create ../../../../gcc-5.4.0/libsanitizer/asan/asan_interceptors.cc:179
    #1 0x7f121e1fba32 in __gthread_create /home/vdb/gcc-5.4-build/x86_64-unknown-linux-gnu/libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:662
    #2 0x7f121e1fba32 in std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) ../../../../../gcc-5.4.0/libstdc++-v3/src/c++11/thread.cc:149

ref

  • 这里有建议不要使用memcmp的讨论,还是怕越界 https://github.com/cesanta/mongoose/issues/564
  • https://github.com/pcrain/slippc/issues/16 一个global buffer overflow case

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

fd泄漏 or socket相关问题分析命令总结


fd数目有没有上涨?

 lsof -n|awk '{print $2}'| sort | uniq -c | sort -nr | head

20个最高fd线程

for x in `ps -eF| awk '{ print $2 }'`;do echo `ls /proc/$x/fd 2> /dev/null | wc -l` $x `cat /proc/$x/cmdline 2> /dev/null`;done | sort -n -r | head -n 20

具体到进程

ll /proc/pid/fd | wc -l

fd都用来干啥了

strace -p pid  -f -e read,write,close

Ref

  • https://oroboro.com/file-handle-leaks-server/ 一个fd泄漏总结
    • 大众错误观点
      • time-wait太多导致fd占用 -> 不会。close就可以复用了。和time-wait两回事
      • close fd太慢 -> 不会。调用close返回值后就可以复用,是否真正关闭是系统的事儿
    • 几个常见场景
      • 子进程导致的重复fd
      • 太多连接
      • 创建子进程的时候关闭fd泄漏
  • https://serverfault.com/questions/135742/how-to-track-down-a-file-descriptor-leak
  • 查看所有tcphttp://blog.fatedier.com/2016/07/18/stat-all-connection-info-of-special-process-in-linux/

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

Characterizing, Modeling, and Benchmarking RocksDB Key-Value Workloads at Facebook


根据ppt和论文总结一下


概述

如今KV应用非常广泛,然而

  • KV数据集在不同的应用上有不同的表现。对现实生活中的数据集分析非常有限
  • 同一个应用,数据集也是不断变化的,怎么采集分析这些变动?
  • 基于上,如何分析真正的瓶颈在哪,如何提高性能?

方法和工具

  • 方法 收集数据集,分析数据结构,简历数据集模型,对比,提高benchmark性能,调优
  • 工具 trace collector, trace replayer, trace analyzer, benchmarks

论文基于三个rocksdb应用来分析

案例分析

UDB

facebook做的社交数据收集工具,底层是mysql on myrocks

  rocksdb key rocksdb value
primary key table index number + primary key columns + checksum
secondary key table index number + secondary key + primary key checksum

UDB的RocksDB通过6个ColumnFamily来存储不同类型的数据,分别是:

Object:存储object数据

Assoc:存储associations数据

Assoc_count:存储每个object的association数

Object_2ry,Assoc_2ry:object和association的secondary index

Non-SG:存储其他非社交图数据相关的服务

ZippyDB UP2X

rocksdb kv集群,用来保存AIML信息的

采集的数据类别

  • 查询构成
  • kv大小以及分布
  • kv 热点以及访问分布
  • qps
  • 热key分布
  • Key-space and temporal localities等等

由于上面的特性大多和业务相关,就不列举了。只列keysize

三个应用的 key size特点,都集中在一个范围 这不是废话吗

图太大不贴了,看ppt 15页

然后通过trace_replay重放数据集,自己构造一组类似的数据集,通过ycsb来模拟

具体怎么用的没有讲


ref

  • 详细的论文描述看这里 https://www.jianshu.com/p/97d9bdd3cd4e 我只说了个大概
  • https://www.usenix.org/system/files/fast20-cao_zhichao.pdf
  • https://rockset.com/rocksdb/RocksDBZhichaoFAST2020.pdf?fbclid=IwAR0j6IpFrZ_hiYJOJLf5bMENUC2v86LUw69KWh_0ZBvQxMqWiDahyb0IYDw
  • 文章中提到的工具在论文引用里介绍了,wiki 页面 https://github.com/facebook/rocksdb/wiki/RocksDB-Trace%2C-Replay%2C-Analyzer%2C-and-Workload-Generation 有机会可以试试

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

算法4/手撕算法整理笔记


https://github.com/labuladong/fucking-algorithm

https://vjudge.net/article/187

https://github.com/youngyangyang04/leetcode-master


抽象题型

https://github.com/Strive-for-excellence/ACM-template

https://github.com/atcoder/ac-library

https://github.com/kth-competitive-programming/kactl/blob/master/content/graph/2sat.h

https://github.com/ouuan/Tree-Generator

https://github.com/BedirT/ACM-ICPC-Preparation/tree/master/Week01

https://github.com/hanzohasashi33/Competetive_programming

https://github.com/rachitiitr/DataStructures-Algorithms

https://csacademy.com/app/graph_editor/

经典题型

  • Sliding window,滑动窗口类型

  • two points, 双指针类型

  • Fast & Slow pointers, 快慢指针类型

    • 龟兔赛跑
  • Merge Intervals,区间合并类型

    • 重叠区间,判断交集
  • Cyclic Sort,循环排序

  • In-place Reversal of a LinkedList,链表翻转

  • Tree Breadth First Search,树上的BFS

    • 用队列处理遍历
  • Tree Depth First Search,树上的DFS

  • 模拟堆栈

  • Two Heaps,双堆类型 最大最小堆求中位数

  • 优先队列
  • 找一组数中的最大最小中位数

  • Subsets,子集类型,一般都是使用多重DFS

  • Modified Binary Search,改造过的二分

  • Top ‘K’ Elements,前K个系列

  • K-way merge,多路归并

  • DP

    • 0/1背包类型
    • Unbounded Knapsack,无限背包
    • 斐波那契数列
    • Palindromic Subsequence回文子系列
    • Longest Common Substring最长子字符串系列
  • Topological Sort (Graph),拓扑排序类型

    • hashmap邻接表

博弈问题 https://zhuanlan.zhihu.com/p/50787280

https://www.lintcode.com/ladder/47/

https://www.lintcode.com/ladder/2/

https://hrbust-acm-team.gitbooks.io/acm-book/content/chang_jian_ji_chu_cuo_wu.html

https://github.com/lightyears1998/polymorphism

https://github.com/menyf/acm-icpc-template

https://github.com/nataliekung/leetcode/tree/master/amazon

基础

  • 完整详细的定义问题,找出解决问题所必须的基本抽象操作并定义一份API
  • 间接地实现一种础计算法,给出一个开发用例并使用实际数据作为输入
  • 当实现所能解决的问题的最大规模达不到期望时决定改进还是放弃
  • 逐步改进,通过经验性分析和数学分析验证改进后的结果
  • 用更高层侧的抽象表示数据接口活算法来设计更高级的改进版本
  • 如果可能尽量为最坏情况下的性能提供保证,在处理普通数据时也要有良好的性能
  • 在适当的时候讲更细致的深入援救留给有经验的研究之并继续解决下一个问题

排序

初级排序

  • 选择排序
    • 运行时间和输入无关,并不能保留信息,有记忆信息更高效
  • 插入排序
    • 如果有序,更友好
    • 进阶,希尔排序,分组插入排序
      • 分组怎么确定?

归并排序

  • 难在原地归并
  • 递归归并

快速排序

  • 如何选位置?
  • 改进方案
    • 小数组用插入排序
    • 三取样切分,中位数的选取

优先队列

二叉堆维护

  • 插入
    • 加到数组末尾,上浮
  • 删除最大元素
    • 最后一个元素放到顶端,下沉

多叉堆

堆排序


查找

基本抽象 符号表(dict) put/get/delete/contains

  • 是否需要有序 min/max/range-find
  • 插入和查找能不能都快
    • 插入块不考虑查找,链表,插入慢查找快,哈希表?
    • 二叉查找树,插入对数查找对数(二分)

二叉查找树

左边小右边大

删除节点

  • 右边大,但是右边的左节点小,要找到右边没有左子节点的左节点,作为被删节点的交换节点
  • 左边一定是小于右边的,所以要确定右边最小的,抬到被删节点的位置就行了
  • 如果删除的是最小节点,那一定是左边的没有左子节点的节点,右子节点直接抬上来就行了,因为左边没了

最坏情况,数不平衡,退化成链表

范围查找 也就是中序遍历

平衡查找树

在插入场景下保证二叉查找树的完美平衡难度过大

  • 2-3查找树,插入能尽可能的保持平衡

    • 如果插入终点是2节点,就转换成3节点
    • 如果终点是3节点
      • 只有一个3节点,该节点编程4节点,4节点可以轻松抽出二叉树子树
        • 父2节点,子3节点,同理,抽出子树,把子树父节点塞到父节点
        • 父3节点,子3节点,同理,抽出子树,把子树父节点塞到父节点,父节点再抽出子树,重复
        • 全是3节点 树高 +1
  • 红黑二叉查找树描述2-3树??

    • 替换3节点 抽出子树 左连接子树要标红 有图

    • 红连接放平,就是2-3树了

      image-20200827165325062

    一种等价定义

    • 红连接均为左连接
    • 没有任何一个节点同时和两条红连接相连
    • 完美黑色平衡,任意空连接到根节点的路径上的黑连接相同
  • 旋转 就是单纯的改变方向

  • 插入
    • 2节点插入
      • 单个2节点插入 一个元素就是单个2节点,左边就变红,如果右边 变红+旋转 最终都是3节点
      • 树底2节点插入,右边,那就旋转(交换位置)
    • 双键树插入,3节点插入 三种情况,小于/之间/大于
      • 大于,直接放到右边,平衡了,变黑
      • 小于,放到左边,两连红,旋转 变黑
      • 之间,放左边右子节点,旋转,在旋转,变黑
  • 删除
  • 红黑树性质
    • 高度
    • 到任意节点的路径平均长度

散列表

  • 拉链法,也就是每个表项对应一个链表,有冲突就放到链表里
  • 线性探测,放在下一个???长键簇会很多很难受

应用

  • 查找表
  • 索引,反向索引
  • 稀疏矩阵 哈希表表达

无向图 边(edge) 顶点(vertex)

顶点名字不重要,用数字描述方便数组描述

特殊的图 自环 /平行边 含有平行边的叫多重图 没有平行边/自环的是简单图

两个顶点通过一条边相连 相邻 这条边依附于这两个顶点

依附于顶点的边的总数称为顶点的度数

子图 一幅图的所有变的一个子集以及所依附的顶点构成的图 许多问题要识别各种类型的子图

路径 由边顺序链接一系列顶点 u-v-w-x

简单路径 没有重复顶点的路径

至少包含一条边,起点终点相同的路径 u-v-w-x-u

简单环 不包含重复顶点和重复边的

如果从任意一个顶点都存在一条路径到达另一个顶点,我们称这幅图是连通图

一幅非连通的图由若干连通部分组成,它们都是极大连通子图

` 无环图` 不包含环的图

无环连通图

生成树 连通图的子图,包含所有顶点,且是一棵树

森林 树的集合 生成树森林 生成树的集合

树的定义非常通用便于程序描述

图G 顶点V个

  • G有V-1条边且不含环
  • G有V-1条边且连通
  • G连通,但删除任意一条编都会是它不在联通
  • G是无环图,添加任意一条边会产生一条换
  • G中任意一对顶点之间仅存在一条简单路径

图的密度 已经连接的顶点对栈所有可能被连接的顶点对的比例 稀疏图 被连接的顶点对很少,稠密图 只有很少的抵抗点对之间没有边连接 如果一幅图中不同的边的数量在顶点总数的一个很小的常数倍之内就是稀疏的

二分图 能够将所所有节点分成两部分的图

图的表达方式

  • 要求,空间,快
    • 邻接矩阵 V*V矩阵 空间太大
      • 平行边无法描述
    • 边的数组 查相邻不够快
    • 邻接表数组 顶点为索引的数组,数组内是和该顶点相邻的列表
      • 空间 V+E
  • 遍历
    • DFS 其实也是dp数组方法的一种 递归要用堆栈
    • 连通性判定
  • 寻找路径,路径最短判定?
    • BFS
  • 连通分量?
  • 间隔的度数?

有向图

有向图取反

有向图的可达性

  • mark-sweep gc
  • 有向图寻路
    • 单点有向路径
    • 单点最短有向路径

环/有向无环图(DAG)/有向图中的环

  • 有向环检测,有向无环图就是不含有有向环的有向图
    • dfs
  • 有向图中的强连通性
    • 构成有向环
    • 自反/对称/传递

最小生成树 MST

加权图 权值最小的生成树

  • Prim/Kruskal
    • Prim 贪心 + 优先队列
  • 几个简化
    • 权重不同,路径唯一
    • 只考虑连通
    • 权重可以是负数,意义不一定是距离
  • 树的特点
    • 任意两个顶点都会产生一个新的环
    • 树删掉一条边就会得到两个独立的树
  • 切分定理
    • 给定任意的切分,它的横切边中权重最小的必然是图的最小生成树 ??
    • 这些算法都是一种贪心算法
      • V个顶点的任意加权连通图中属于最小生成树的边标记成黑色,初始为灰色,找到一种切分,横切边均不为黑色,将它权重最小的横切边标记为黑色,反复,直到标记了V-1条黑色边为止 ??

动态规划

  • 求最值的,通常要穷举,聪明的穷举(dp table)
  • 重叠子问题以及最优子结构
    • 如果没有最优子结构,改造转换
  • 正确的状态转移方程
    • 数学归纳
    • dp[i]代表了什么?
      • 最长上升子序列问题,dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
    • 公式条件?
    • dp的遍历方向问题
      • 遍历的过程中,所需的状态必须是已经计算出来的
      • 遍历的终点必须是存储结果的那个位置

https://github.com/xtaci/algorithms


看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

afl测试


AFL是一种fuzz test工具,可以用来测试各种输入是否引起被测工具崩溃,比如gcc之类的。但是如果是网络模块,比如redis,nginx,没有好的模拟网络的办法。下面是一个演示示例,结合preeny来mock网络

准备工作

编译afl ,tarball在这里https://lcamtuf.coredump.cx/afl/下载

CC=/usr/local/bin/gcc make -j#注意自己的gcc版本。如果不需要考虑这个问题直接make
make install
#cmake指定,编译自己的二进制,指定g++
cmake ../ -DCXX_COMPILER_PATH=/usr/local/bin/afl-g++
#如果不是cmake,指定CC
CXX=/usr/local/bin/afl-g++ make -j

编译preeny没什么难的 参考https://github.com/zardus/preeny readme即可

测试

preeny可以把标准输入强制转换成socket输入,指定好LD_PRELOAD即可 参考链接 2 3 分别给了redis和nginx的例子

我这里使用的是redis,环境是wsl,参考的参考链接2生成的用例

LD_PRELOAD=/mnt/d/github/preeny/x86_64-linux-gnu/desock.so afl-fuzz -m 8G -i fuzz_in -o fuzz_out/ ./redis-server

测试preeny是否生效可以使用

LD_PRELOAD=/mnt/d/github/preeny/x86_64-linux-gnu/desock.so ./redis-server ./redis.conf  < set a b

跑了一个周末,没有发现崩溃的现象。

注意

wsl setsockopt TCP_NODELAY会报错invalid argument。屏蔽掉即可


ref

本文参考

  1. 主要思路 https://copyninja.info/blog/afl-and-network-programs.html
  2. https://volatileminds.net/2015/08/20/advanced-afl-usage-preeny.html
  3. https://lolware.net/2015/04/28/nginx-fuzzing.html

几个afl使用例子

  1. http://0x4c43.cn/2018/0722/use-afl-for-fuzz-testing/ 测试imageshark的
  2. https://stfpeak.github.io/2017/06/11/Finding-bugs-using-AFL/ 举例测试输入漏洞
  3. https://www.freebuf.com/articles/system/191536.html fuzz介绍,原理
  4. http://zeroyu.xyz/2019/05/15/how-to-use-afl-fuzz/ afl使用指南
  5. https://paper.seebug.org/496/ 原理
  6. https://www.fastly.com/blog/how-fuzz-server-american-fuzzy-lop

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

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/

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
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亦可。

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


看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
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实例的问题所在。


看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
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函数,检查长度
    • 缺陷,动态分配的空间无法检查

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
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

看到这里或许你有建议或者疑问或者指出我的错误,请留言评论或者邮件mailto:wanghenshui@qq.com, 多谢!

觉得写的不错可以点开扫码赞助几毛 微信转账
Read More

^