Conflict Free Replicated Data Types 无冲突复制数据类型 CRDTs


  • 基于状态:(英文:state-based),即将各个节点之间的CRDT数据直接进行合并,所有节点都能最终合并到同一个状态,数据合并的顺序不会影响到最终的结果。
  • 基于操作:(英文:operation-based,可以简写为op-based)。将每一次对数据的操作通知给其他节点。只要节点知道了对数据的所有操作(收到操作的顺序可以是任意的),就能合并到同一个状态。

实际上,基于状态基于操作的CRDT已经在数学上被证明可以相互转换。

具体概念看参考链接2 ,图非常清晰

下面介绍几个比较有意思的CRDT类型。

G-Counter (Grow-only Counter)

这是一个只增不减的计数器。对于N个节点,每个节点上维护一个长度为N的向量$V={P_0, P_1, P2, …, P{n-1}}。。P_m表示节点m上的计数。当需要增加这个计数器时,只需要任意选择一个节点操作,操作会将对应节点的计数器表示节点m上的计数。当需要增加这个计数器时,只需要任意选择一个节点操作,操作会将对应节点的计数器P_m := P_m + 1。当要统计整个集群的计数器总数时,只需要对向量。当要统计整个集群的计数器总数时,只需要对向量V$中的所有元素求和即可。

举个例子,我们有A, B, C三个节点上分别部署了同一个G-Counter。初始状态如下:

A: {A: 0, B: 0, C: 0} = 0
B: {A: 0, B: 0, C: 0} = 0
C: {A: 0, B: 0, C: 0} = 0

每个节点都维护着其他节点的计数器状态,初始状态下,所有计数器都为0。现在我们假设有三个客户端a,b,c,a和b先后在节点A上增加了一次计数器,c在节点B上增加了一次计数器:

A: {A: 2, B: 0, C: 0} = 2
B: {A: 0, B: 1, C: 0} = 1
C: {A: 0, B: 0, C: 0} = 0

此时如果分别向A, B, C查询当前计数器的值,得到的结果分别是{2, 1, 0}。而实际上一共有3次增加计数器的操作,因此全局计数器的正确值应该为3,此时系统内状态是不一致的。不过没关系,我们追求的是最终一致性。假设经过一段时间,B向A发起了合并的请求:

A: {A: 2, B: 1, C: 0} = 3
B: {A: 2, B: 1, C: 0} = 3
C: {A: 2, B: 1, C: 0} = 3

经过合并后,A和B的计数器状态合并,现在从A和B读取到的计数器的值变为3。接下来C和A进行合并:

A: {A: 2, B: 1, C: 0} = 3
B: {A: 2, B: 1, C: 0} = 3
C: {A: 2, B: 1, C: 0} = 3

现在节点ABC都达到了相同的状态,从任意一个节点获取到的计数器值都为3。也就是说3个节点达成了最终一致性。

我们可以用以下伪代码描述G-Counter的逻辑:

payload integer[n] P
	initial [0, 0, 0, ..., 0]
update increment()
	let g = myId()
	P[g] := P[g] + 1 // 只更新当前节点对应的计数器
query value() : integer v
	let v = P[0] + P[1] + ... + P[n-1] // 将每个节点的计数器进行累加
merge (X, Y) : payload Z
	for i = 0, 1, ... , n-1:
		Z.P[i] = max(X.P[i], Y.P[i]) // 通过max操作来合并各个节点的计数器

G-Counter使用max()操作来进行各个状态的合并,我们知道函数max满足可交换性,结合性,幂等性,即:

  • 可交换性: max(X,Y)=max(Y,X)
  • 结合性: max(max(X,Y),Z)=max(X,max(Y,Z))
  • 幂等性: max(X,X)=X

所以G-Counter可以在分布式系统中使用,并且可以无冲突合并。

PN-Counter (Positive-Negative-Counter)

G-Counter有一个限制,就是计数器只能增加,不能减少。不过我们可以通过使用两个G-Counter来实现一个既能增加也能减少计数器(PN-Counter)。简单来说,就是用一个G-Counter来记录所有累加结果,另一个G-Counter来记录累减结果,需要查询当前计数器时,只需要计算两个G-Counter的差即可。

payload integer[n] P, integer[n] N
	initial [0, 0, ..., 0], [0, 0, ..., 0]
update increment()
	let g = myId()
	P[g] := P[g] + 1 // 只更新当前节点对应的计数器
update decrement()
	let g = myId()
	N[g] := N[g] + 1
query value() : integer v
	let v = sum(P) - sum(N) // 用P向量的和减去N向量的和
merge (X, Y) : payload Z
	for i = 0, 1, ... , n-1:
		Z.P[i] = max(X.P[i], Y.P[i]) // 通过max操作来合并各个节点的计数器
		Z.N[i] = max(N.P[i], Y.N[i])

Registers

register有assign()及value()两种操作

  • Last Write Wins -register(LWW-Register)

给每个assign操作添加unique ids,比如timestamps或者vector clock,使用max函数进行merge

  • Multi-valued -register(MV-Register)

类似G-Counter,每次assign都会新增一个版本,使用max函数进行merge

Sets

  • Grow-only set(G-Set)

使用union操作进行merge

  • Two-phase set(2P-Set)

使用两个G-Set来实现,一个addSet用于添加,一个removeSet用于移除

  • Last write wins set(LWW-element Set)

类似2P-Set,有一个addSet,一个removeSet,不过对于元素增加了timestamp信息,且timestamp较高的add及remove优先

  • Observed-remove set(OR-Set)

类似2P-Set,有一个addSet,一个removeSet,不过对于元素增加了tag信息,对于同一个tag的操作add优先于remove

其他数据类型

Array

关于Array有Replicated Growable Array(RGA),支持addRight(v, a)操作

Graph

Graph可以基于Sets结构实现,不过需要处理并发的addEdge(u, v)、removeVertex(u)操作

Map

Map需要处理并发的put、rmv操作


ref

  1. https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type

  2. http://galudisu.info/2018/06/06/akka/ddata/Akka-Distributed-Data-Deep-Dive/

  3. http://liyu1981.github.io/what-is-CRDT/

  4. https://lfwen.site/2018/06/09/crdt-counter/

  5. 详细介绍了各种CRDT实现https://juejin.im/post/5cd25e886fb9a0322758d23f

  6. 论文 https://hal.inria.fr/file/index/docid/555588/filename/techreport.pdf

  7. 一个系统的阅读导航http://christophermeiklejohn.com/crdt/2014/07/22/readings-in-crdts.html

    1. 博主的另外两篇导航也很不错
      1. http://christophermeiklejohn.com/distributed/systems/2013/07/12/readings-in-distributed-systems.html
      2. http://christophermeiklejohn.com/linear/logic/2014/01/04/readings-in-linear-logic.html

contact

Read More

最近遇到的几个打包问题


这活真恶心。瞎忙还没意义。

编译libhdfs提示找不到xml2

错误提示

CMake Error at cmake-3.10.3/share/cmake-3.10/Modules/FindPackageHandleStandardArgs.cmake:137 (message):  
 Could NOT find LibXml2 (missing: LIBXML2_LIBRARY LIBXML2_INCLUDE_DIR)

用的cmake版本是3.10.

编译机是docker容器。本地测试好使

试着安装库依赖
yum install -y libxml2-devel

依旧不好使。我在其他环境的docker机器上测试cmake 3 cmake2都没问题

试着看cmakeerr log

看到了错误日志 发现 dladdr 连接错误,加上链接ldl修了半天才发现方向不对

手动指定 LIBXML2_LIBRARY LIBXML2_INCLUDE_DIR目录到bootstrap,cmake命令家伙是哪个

-DLIBXML2_INCLUDE_DIR=/usr/include/libxml2/ -DLIBXML2_LIBRARY=/usr/lib64/

,但是编译不过

回到开头

安装的日志是这样的

--> Running transaction check  
---> Package libxml2-devel.x86_64 0:2.9.1-6.3 will be installed  
--> Processing Dependency: libxml2 = 2.9.1-6.3 for package: libxml2-devel-2.9.1-6.3.x86_64  
--> Processing Dependency: xz-devel for package: libxml2-devel-2.9.1-6.3.x86_64  
--> Running transaction check  
---> Package libxml2.x86_64 0:2.9.1-6.el7_2.3 will be updated  
--> Processing Dependency: libxml2 = 2.9.1-6.el7_2.3 for package: libxml2-python-2.9.1-6.el7_2.3.x86_64  
---> Package libxml2.x86_64 0:2.9.1-6.3 will be an update  
---> Package xz-devel.x86_64 0:5.2.2-1.el7 will be installed  
--> Running transaction check  
---> Package libxml2-python.x86_64 0:2.9.1-6.el7_2.3 will be updated  
---> Package libxml2-python.x86_64 0:2.9.1-6.3.h3 will be an update  
--> Processing Dependency: libxml2 = 2.9.1-6.3.h3 for package: libxml2-python-2.9.1-6.3.h3.x86_64  
--> Running transaction check  
---> Package libxml2.x86_64 0:2.9.1-6.el7_2.3 will be updated  
---> Package libxml2.x86_64 0:2.9.1-6.el7_2.3 will be updated  
---> Package libxml2.x86_64 0:2.9.1-6.3 will be an update  
--> Processing Dependency: libxml2 = 2.9.1-6.3 for package: libxml2-devel-2.9.1-6.3.x86_64  
---> Package libxml2.x86_64 0:2.9.1-6.3.h3 will be an update  
--> Finished Dependency Resolution  
 You could try using --skip-broken to work around the problem  
 You could try running: rpm -Va --nofiles --nodigest  

卧槽,没装成功。妈的

加上–skip-broken还是不行

错误冲突实际上是这样的

Error: Package: libxml2-devel-2.9.1-6.3.x86_64 (current)  
          Requires: libxml2 = 2.9.1-6.3  
          Installed: libxml2-2.9.1-6.3.h3.x86_64 (@stable)  
               libxml2 = 2.9.1-6.3.h3  
          Available: libxml2-2.9.1-6.el7_2.3.i686 (base)  
               libxml2 = 2.9.1-6.el7_2.3  
          Available: libxml2-2.9.1-6.3.x86_64 (current)  
               libxml2 = 2.9.1-6.3 

试着删掉libxml2提示 yum is protecte,换另外的删除方法

rpm -e --nodeps libxml2

但是会报错

 There was a problem importing one of the Python modules  
 required to run yum. The error leading to this problem was:  
   
    libxml2.so.2: cannot open shared object file: No such file or directory  
   
 Please install a package which provides this module, or  
 verify that the module is installed correctly.  
   
 It's possible that the above module doesn't match the  
 current version of Python, which is:  
 2.7.5 (default, Nov  6 2016, 00:28:07)   
 [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]  
   
 If you cannot solve this problem yourself, please go to   
 the yum faq at:

yum完全就不工作了。

唉。我最后自带了一个xml源码包自己编。

RPM打包

我只是想把编好的二进制和相关lib库打包后放到rpm里。网上搜了一天。各种从头开始。各种源码开始编。一点意义都没有。

rpmbuild打包就有三种模式,ba bb bs。还有各种奇怪的宏。还有各种make install流程。很容易就陷入这些没用的细节中。我根本都不需要的。最终我使用了bs。就当我打包源码包了。

另外rpmbuild默认目录是root,还需要指定

rpmbuild --define '_topdir ./rpmbuild' -bs rpmbuild/SPECS/rpm.spec

另外,解压rpm

rpm2cpio xxx.rpm | cpio -div

又遇到了个问题,要求目录。只能从二进制打包,又浪费一上午

照着这个文档看了半天 http://kuanghy.github.io/2015/11/13/rpmbuild

我的步骤里总会删掉buildroot,原来还需要install流程里加二进制。

最后发一个我的模板

Name:  xx
Version:	1.0
Release:	1%{?dist}
Summary: xx binary
#Group:		
License: WTF	
 
%install
mkdir -p  %{buildroot}/dir/dir/somedir/
cp -a %(pwd)/your.tar.gz %{buildroot}/dir/dir/somedir/
echo "install done"

%files
%defattr (-,root,root,-)
/dir/

然后打包,bb是二进制打包,之前用的bs 源码打包

rpmbuild --define '_topdir ./rpmbuild' --buildroot=$(pwd)/rpmbuild/BUILDROOT -bb rpm.spec

ref

  1. 找不到iddr https://github.com/ContinuumIO/libhdfs3-downstream/issues/9 严重误导我。没有没啥影响。

  2. https://www.mail-archive.com/dev@hawq.incubator.apache.org/msg04104.html 严重误导

  3. https://stackoverflow.com/questions/12993460/why-am-i-getting-undefined-reference-to-dladdr-even-with-ldl-for-this-simpl 严重误导。不是同一个场景。

  4. https://github.com/libical/libical/issues/248 这个链接让我误以为装了glibc2-devel就能找到xml2了。没仔细读就用,误导

  5. https://gitlab.kitware.com/cmake/cmake/issues/18078 这个给了我一点思路,我可以定义这两个变量

  6. https://trac.macports.org/ticket/55386 误导

  7. https://stackoverflow.com/questions/15799047/trying-to-remove-yum-which-is-protected-in-centos 删除包

  8. https://serverfault.com/questions/742160/conflicts-in-files-when-installing-libxml2

  9. https://www.centos.org/forums/viewtopic.php?t=68202 这个和我遇到的问题差不多,但是是arm机器

  10. https://forums.cpanel.net/threads/requires-libxml2-update-error.636661/ 真正的原因,但没说解决办法

  11. https://stackoverflow.com/questions/416983/why-is-topdir-set-to-its-default-value-when-rpmbuild-called-from-tcl rpmbuild更改目录的办法

  12. 网上一大堆rpm详解封装。都是翻译的外国的文章。说不到重点。比如

    1. https://blog.csdn.net/Nedved_L/article/details/78548101
    2. https://blog.csdn.net/u012373815/article/details/73257754
    3. https://www.iarno.cn/2019/04/22/RPM%E6%89%93%E5%8C%85/ 这个是写得好的。不过我也没用上
    4. https://stackoverflow.com/questions/42773687/creating-rpm-spec-file-from-compiled-binary-files 没啥用
  • https://stackoverflow.com/questions/39743808/cannot-make-rpm-from-spec-file-file-not-found-errors 这个有点用

contact

Read More

log4cplus ERROR No appenders could be found for logger root 报错


hdfs client 使用log4cplus是需要配置文件的,一般长这个样子

log.properties

log4cplus.logger.defaultLogger=INFO, LogToFile
log4cplus.appender.LogToFile=log4cplus::RollingFileAppender
log4cplus.appender.LogToFile.MaxFileSize=64MB
log4cplus.appender.LogToFile.MaxBackupIndex=20
log4cplus.appender.LogToFile.File=/opt/hdfs-client.log
log4cplus.appender.LogToFile.ImmediateFlush=true
log4cplus.appender.LogToFile.Append=true
log4cplus.appender.LogToFile.CreateDirs=true 
log4cplus.appender.LogToFile.layout=log4cplus::PatternLayout
log4cplus.appender.LogToFile.layout.ConversionPattern=%D{\%Y-%m-%d %H:%M:%S,%Q} %-5p [pid:%i pth:%t] %m %l%n

注意%D{\%Y-%m中间多加了个斜线,githubpage编译不过只好出此下策

通常库是所有模块共用同一个,动态库也是放在系统PATH下的。

如果单独打包可能就有问题

log4cplus:ERROR could not open file ./log.properties
log4cplus:ERROR No appenders could be found for logger (root).
log4cplus:ERROR Please initialize the log4cplus system properly.

需要把该文件放到模块同目录下,并且需要提供rootLogger

改成

log4cplus.rootLogger=INFO, LogToFile
...

就不会报错了。

Google一圈搜到了各种解决方案。被严重误导,只有1 贴点边

2是别人使用log4cplus遇到该问题以及解决方案。


ref

  1. https://bbs.csdn.net/topics/370248522

  2. https://github.com/nsacyber/HIRS/issues/95

contact

Read More

一次ldconfig导致linux 崩溃的问题

TL,DR: ldconfig 加载了两次libc.so导致崩溃

编译好了自己的模块,依赖很多动态库,打包

mkdir -p package
cp myserver package
for i in `ldd myserver | awk '{print $3}' | grep "/" | sort | uniq`; do cp $i package; done
tar -czvf package.tar.gz package

把包挪到另一个机器上,试着运行,提示 找不到动态库

 error while loading shared libraries: libxxx.so.1.0.0: cannot open shared object file: No such file or directory

搜到了一个解决方案1 先添加到LD_LIBRARY_PATH 然后再ldconfig

但是我执行发现

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD
Segmentation fault

然后后面的命令都会报错,开始我以为是机器问题,然后换了个机器也这样,然后我直接把PWD加到/etc/ld.so.conf里,结果机器立马报错,然后就打不开了。

在此警告,千万不要这么搞,这么搞是全局的,整个机器就挂了,export只是当前shell,所以还好。幸亏我有镜像,重新复原就搞定了。

应该是打包的动态库有问题

后面我逐个排查重试会不会报错

删到libc之后就不会报错了。。。确定了是libc的问题。由于上面的打包脚本会把libc也带上。需要去掉一下

也搜到了一个解决记录2 可惜看到的比较晚,不然能更早定位到是libc的问题。不过链接里说的strace和ldconfig -iv都没用,直接就segmentation fault了。不会执行的。

为什么两个libc.so会导致ldconfig崩溃目前还没有找到原因。总之打包的时候去掉libc

mkdir -p package
cp myserver package
for i in `ldd myserver | awk '{print $3}' | grep "/" | grep -v libc | sort | uniq`; do cp $i package; done
tar -czvf package.tar.gz package

ref

  1. https://unix.stackexchange.com/questions/279397/ldd-does-not-find-path-how-to-add
  2. https://askubuntu.com/questions/1000606/how-do-i-figure-out-which-library-is-causing-ldconfig-to-segfault

contact

Read More

rocksdb IO error No Space Left


遇到两个问题

rocksdb一直在compaction,但是compaction有写放大需要额外的空间,然后没空间了,结果就一直阻塞写了

这个解决办法只能是重启,并且需要预估硬盘的使用情况避免这种问题的发生。

rocksdb报错IO error No Space Left,但是du查看有空间

这个是发生过compaction已经收回了空间,但是之前有的bg_error一直存在没有清掉。这个是rocksdb故意设计成这样的,上层应用需要决定是否处理这种场景,选择重启还是自己实现高可用

rocksdb的issue在这里

crdb的设计讨论在这里

ardb的解决办法,重启

crdb的讨论我没有仔细看,有时间看完补充一下


ref

  1. https://github.com/facebook/rocksdb/issues/919
  2. crdb的设计讨论https://github.com/cockroachdb/cockroach/issues/8473
  3. ardb的解决办法, https://github.com/yinqiwen/ardb/issues/310

contact

Read More

No route to host vs Connection refused


遇到个问题

用的某弹性云服务器,两台机器,内网ip却连不上,跑不了benchmark,提示是No route to host

而不是connnect err timeout之类的。两侧关掉iptables解决。route被iptables限制了。

另外,之前在安全组上分了神,安全组vpc和iptables是两套体系。安全组一般同一个vpc就没问题了。


ref

  1. https://yq.aliyun.com/articles/174058
  2. https://blog.csdn.net/bisal/article/details/44731431

contact

Read More

SO_REUSEPORT与惊群问题


why

和同事聊天提到SO_REUSEPORT是解决惊群问题而引入的,我有点不服,决定顺一顺这个问题


参考链接1中说的比较详尽,我之前以为惊群就是accept本身的问题,在linux2.6就被解决掉了,实际上不是

accept惊群问题

多进程模式,父进程监听,子进程继承监听并accept,每次accept都会导致所有子进程响应,nginx解决方案,加个大锁。导致效率低下

epoll惊群问题

nginx解决的方案


ref

  1. https://simpleyyt.com/2017/06/25/how-ngnix-solve-thundering-herd/

contact

Read More

Linux调优指南


只列重点和延伸

linux内存

页帧分配(Page frame allocation)

页是物理内存或虚拟内存中一组连续的线性地址,Linux内核以页为单位处理内存,页的大小通常是4KB。当一个进程请求一定量的页面时,如果有可用的页面,内核会直接把这些页面分配给这个进程,否则,内核会从其它进程或者页缓存中拿来一部分给这个进程用。内核知道有多少页可用,也知道它们的位置。

伙伴系统(Buddy system)

Linux内核使用名为伙伴系统(Buddy system)的机制维护空闲页,伙伴系统维护空闲页面,并且尝试给发来页面申请的进程分配页面,它还努力保持内存区域是连续的。如果不考虑到零散的小页面可能会导致内存碎片,而且在要分配一个连续的大内存页时将变得很困难,这就可能导致内存使用效率降低和性能下降。下图说明了伙伴系统如何分配内存页。 buddy-system

如果尝试分配内存页失败,就启动回收机制。参考”内存页回收(Page fram reclaiming)”

可以在/proc/buddyinfo文件看到伙伴系统的信息。详细内容参考”zone中使用的内存(Memory used in a zone)”

页帧回收

如果在进程请求指定数量的内存页时没有可用的内存页,内核就会尝试释放特定的内存页(以前使用过,现在没有使用,并且基于某些原则仍然被标记为活动状态)给新的请求使用。这个过程叫做内存回收kswapd内核线程和try_to_free_page()内核函数负责页面回收。

kswapd通常在task interruptible状态下休眠,当一个区域中的空闲页低于阈值的时候,它就会被伙伴系统唤醒。它基于最近最少使用原则(LRU,Least Recently Used)在活动页中寻找可回收的页面。最近最少使用的页面被首先释放。它使用活动列表和非活动列表来维护候选页面。kswapd扫描活动列表,检查页面的近期使用情况,近期没有使用的页面被放入非活动列表中。使用vmstat -a命令可以查看有分别有多少内存被认为是活动和非活动状态。详细内容可以参考”vmstat”一节。

kswapd还要遵循另外一个原则。页面主要有两种用途:页面缓存(page cahe)进程地址空间(process address space)。页面缓存是指映射到磁盘文件的页面;进程地址空间的页面(又叫做匿名内存,因为不是任何文件的映射,也没有名字)使用来做堆栈使用的,参考1.1.8 “进程内存段”。在回收内存时,kswapd更偏向于回收页面缓存。

Page out和swap out:“page out”和“swap out”很容易混淆。“page out”意思是把一些页面(整个地址空间的一部分)交换到swap;”swap out”意味着把所有的地址空间交换到swap。

如果大部分的页面缓存和进程地址空间来自于内存回收,在某些情况下,可能会影响性能。我们可以通过/proc/sys/vm/swappiness文件来控制这个行为

swap

在发生页面回收时,属于进程地址空间的处于非活动列表的候选页面会发生page out。拥有交换空间本身是很正常的事情。在其它操作系统中,swap无非是保证操作系统可以分配超出物理内存大小的空间,但是Linux使用swap的空间的办法更加高效。如图1-12所示,虚拟内存由物理内存和磁盘子系统或者swap分区组成。在Linux中,如果虚拟内存管理器意识到内存页已经分配了,但是已经很久没有使用,它就把内存页移动到swap空间。

像getty这类守护进程随着开机启动,可是却很少使用到,此时,让它腾出宝贵的物理内存,把内存页移动到swap似乎是很有益的,Linux正是这么做的。所以,即使swap空间使用率到了50%也没必要惊慌。因为swap空间不是用来说明内存出现瓶颈,而是体现了Linux的高效性。

ps -o majflt,minflt -p pid

minor fault 在内核中,缺页中断导致的异常叫做page fault。其中,因为filemap映射导致的缺页,或者swap导致的缺页,叫做major fault;匿名映射导致的page fault叫做minor fault。 作者一般这么区分:需要IO加载的是major fault;minor fault则不需要IO加载

I/O子系统结构

一图胜千言

刷新缓冲区

网络

网络层级结构和网络通信过程概览

socket buffer详情

/proc/sys/net/core/rmem_max
/proc/sys/net/core/rmem_default
/proc/sys/net/core/wmem_max
/proc/sys/net/core/wmem_default
/proc/sys/net/ipv4/tcp_mem
/proc/sys/net/ipv4/tcp_rmem
/proc/sys/net/ipv4/tcp_wmem

tcp链接状态图

TCP连接状态图

指标

处理器指标如下:

  • CPU利用率(CPU utilization) 这个可能是最直接的指标,它全面展示了每个处理器的利用率。在IBM System x架构中,如果CPU利用率持续高于80%,就可能遇到了处理器瓶颈。

  • 用户时间(User time) 表示CPU在用户进程上的时间百分比,包括nice时间。用户时间值高是一个较好的状态,在这种情况下,系统在处理真正的任务。

  • 系统时间(System time) 表示CPU花在内核操作上的时间百分比,包括IRQ和softirq时间。持续的高系统时间可以指出网络和驱动栈的瓶颈。CPU花在内核上的时间越少越好。

  • 等待(Waiting) CPU花在等待I/O操作上的时间总和。类似blocked值,系统不应该把大量时间花在等待I/O操作上;否则,你应该调查I/O子系统的性能。

  • 空闲时间(Idle time) 表示系统处于空闲等待任务的时间比。

  • Nice时间(Nice time) 表示CPU花在re-nicing进程,改变进程执行顺序和优先级上的时间。

  • 平均负载(Load average) 平均负载不是百分比,是下面的和的滚动平均值:

    • 在队列中等待被处理的进程数

    • 等待非中断任务完成的进程数

      是TASK_RUNNING和TASK_UNINTERRUPTIBLE的和的平均值。如果进程请求CPU时间被阻塞(表示CPU没有时间处理它们),平均负载就会升高。另一方面,如果每个进程直接就能获得CPU时间并且没有CPU周期丢失,负载就会降下来。

  • 可运行进程(Runable processes) 表示已经准备好要执行的进程。这个值不应该持续超过CPU个数的10倍,否则就是出现了CPU瓶颈。

  • 阻塞的(Blocked) 在等待I/O操作完成的时候,进程不能执行。阻塞进程可以指出你的I/O瓶颈。

  • 上下文切换(Context switch) 系统上有大量的切换在线程间发生,在有大量中断和上下文切换发生时,表示驱动或应用程序出现了问题。一般来说,上下文切换不是好现象,因为CPU缓存需要刷新,但是有些上下文切换是必要的。

  • 中断(Interrupts)

    中断值包含硬中断和软中断。硬中断对系统性能有更大的影响。高中断值指示了软件瓶颈,无论是内核还是驱动程序层面的。记住中断值包含CPU时钟引起的中断。

    内存指标

    如下是内存度量值:

  • 空闲内存(Free memory) 和其它操作系统相比,不应该过分担心Linux内存的问题。在“虚拟内存管理”一节中已经说过,Linux把大部分没用到的内存作为文件系统缓存,所以计算空闲内存的时候还得加上已用内存中的缓冲(buffer)和缓存(cache)大小。

  • Swap利用率(Swap usage) 这个数值表明已经使用的swap的空间。如“虚拟内存管理”一节中所说的那样,swap使用率只是表明Linux的内存管理有多么高效。Swap In/Out才是识别内存瓶颈的手段,长时间每秒200到300以上的swap in/out次数表明可能出现内存瓶颈。

  • 缓冲和缓存(Buffer and cache) cache被用作文件系统缓存和块设备缓存。

  • Slabs 描述内核的内存使用量。注意,内核页不能page out到磁盘。

  • 活动和非活动内存 提供关于系统中活动的内存信息。非活动内存是可能由kswapd守护进程交换到磁盘的候选。参考“页帧回收”。

网络指标

以下是网络指标:

  • 接收和发送的包(Packets received and sent) 这个指标告诉你指定网络接口的接收和发送网络包的数量。
  • 接收和发送的字节(Bytes received and sent) 这个值是指定网卡的发送和接收的字节数。
  • 每秒碰撞(collisions per second) 这个值提供了各个网络接口所连接网络的所发生的冲突数量。持续的冲突可能是由于网络基础设施导致的,而不是服务器。在大多数正确配置的网络中,碰撞很少发送,除非网络是由集线器组成的。
  • 丢包 这是被内核丢弃的包的数量,可能是防火墙配置导致的,也可能是由于缺少网络缓冲。
  • 过载(Overruns)  过载表示网络接口用光缓冲空间的次数。这个指标应该和丢包联合起来使用,来判断瓶颈是由网络缓冲还是网络队列长度导致的。
  • 错误(Errors) 被标识为故障的帧数目。这通常是由于网络不匹配或者部分网线损坏导致的。在铜基千兆中,部分损坏网线可能导致显著的网络性能问题。

块设备指标

以下是块设备的相关指标:

  • IO等待(Iowait) CPU花在等待I/O操作发生上的时间。该值长时间飙高预示着可能出现了I/O瓶颈。
  • 平均队列长度(Average queue length) 未完成的I/O请求数量。通常,2到3的磁盘队列是很理想的;太高可能表示出现了I/O瓶颈。
  • 平均等待(Average wait) 一个IO请求被服务的平均等待时间,以毫秒计算。等待时间由真实的I/O操作时间和I/O队列的等待时间组成。
  • 每秒传输(Transfers per second) 表示每秒有多少个I/O操作被执行(读和写)。transfers per secondkBytes per second可以联合使用,来表示系统每秒的平均传输大小。平均传输大小通常应该和所使用的磁盘子系统的条带大小相匹配。
  • 每秒读写块(Blocks read/write per second) 在内核2.6中,它表示每秒读取和写入1024字节块的数目。更早的内核,块大小可能不一样,从512字节到4K字节不等。
  • 每秒读写的千字节(Kilobytes per second read/write) 从块设备读和写的千字节,表示从块设备中读取和写入的实际大小。

工具

工具 常用功能
top 所有进程情况
vmstat 系统活动,硬件和系统信息
uptime, w 系统平均负载
ps, pstree 显示进程
free 内存使用情况
iostat cpu负载和磁盘活动
sar 收集和报告系统状态
mpstat 多处理器使用情况
numastat NUMA相关统计
pmap 进程内存情况
netstat 网络统计
iptraf 实时网络统计
tcpdump,ethereal 详细网络流量分析
nmon 收集和报告系统活动
strace 系统调用
proc文件系统 各种内核统计信息
KDE system guard 实时的系统图形报告
Gnome System Monitor 实时的系统图形报告
工具 常用功能
lmbench 微型系统功能评测工具
iozone 文件系统压测
netperf 网络性能测试

分析性能瓶颈

分析服务器性能

重要: 在做排错之前,备份所有数据和配置信息,以免丢失。

你应该要监控服务器,最简单的办法是在被监控服务器上运行监控工具。

应该在操作高峰记录服务器的性能日志,例如早9点到晚上5点,这取决于服务器提供的是什么服务,和谁在使用这些服务。在创建日志的时候,应该尽力把如下的数据记录在里面:

  • 处理器
  • 系统
  • 服务器工作队列
  • 内存
  • 页文件
  • 物理磁盘
  • 重定向器
  • 网卡

在开始之前,请注意性能调优的办法很重要。我们推荐的服务器性能调优过程如下:

  • 1 理解影响服务器性能的因素
  • 2 根据当前服务器性能表现,设立基准线,用来和未来的测试数据做比较,找出性能瓶颈。
  • 3 使用监控工具来找出性能瓶颈。通过接下来的章节中的方法,你就可以把瓶颈范围缩小到部分子系统上。
  • 4 通过对造成瓶颈的组件执行某些操作,提升服务器性能。

注意:很重要的是,在其它组件具有足够能力维持高性能的时候,通过升级造成瓶颈的组件,可以获得极大的性能提升。

    1. 衡量新的性能。这一步可以帮助你比较调优前和调优后的性能差别。

在尝试修复性能问题的时候,注意:

  • 应用程序应该用适当的方法来编译,减少路径长度。
  • 在做任何升级和修改之前做性能衡量,这样就可以知道修改是否有用。
  • 除了检查新添加的硬件,还要检查重新配置过的旧硬件。

对于应用或数据库服务器来说,CPU是十分关键的资源,也常常是性能瓶颈的源头。需要明白的是,高的CPU利用率并不总是意味着CPU正在繁忙的工作;也可能是正在等着其它子系统完成工作。正确的判断,需要把整个系统作为一个整体,并且观察到每一个子系统,因为子系统可能存在关联的反应。

在普遍的认知中,总是误以为CPU是服务器最重要的部件。但情况并不总是这样,在服务器上配置的CPU总是好于磁盘、内存、网络等子系统组件。只有特定的CPU密集型程序才能正真利用到今天的高端处理器。

找出CPU瓶颈

有好几种办法可以确认瓶颈出现在CPU上。已经在第二章”监控和测试工具”中讨论过了,Linux有各类工具来帮助我们。关键是选择什么工具。

可以使用uptime。通过分析uptime的输出,可以粗略知道在过去15分钟里,服务器上发生了什么

使用top工具,你可以看到CPU利用率和哪个进程是消耗CPU的大户。如果设置了sar,你会收集到很多信息,比如一段时间内的CPU利用率。分析这些信息的办法可能不同,使用isag,可以对sar的输出画出图形。你还可以通过脚本和表格来分析信息,看看CPU利用率的走向。你也可以在命令行使用sar -u或者sar -U processornumber。要想获得系统更全面情况,而不只是CPU子系统,vmstat是个好帮手

有个流程图最好

SMP

基于SMP的系统呈现出来的问题可能是很难检测到的。在SMP环境中,有一个CPU affinity的概念,表示绑定进程到CPU上。 这么做的主要好处是CPU缓存优化,可以让同样的进程运行在一个CPU上,而不是在多个CPU上切换。当进程在CPU间切换的时候,要刷新新的CPU的缓存。进程在处理器间切换的时候会导致很多缓存刷新,那样,一个独立的进程要花费更多的时间才能处理完。而探测器很难检测到,在监控中,CPU负载会十分均衡,不会在任何一个上出现高峰。在基于NUMA的系统上,比如IBM System x3950上,CPU affinity也很有用,重要的是保持内存、缓存和CPU访问都是本地的。

性能优化选项

第一步是要确保,系统性能问题是由CPU引起的,而不是其它子系统。如果处理器是服务器瓶颈,可以采取如下的办法来增强性能:

  • 使用ps -ef来确保没有不必要的进程程序在后台运行,如果找到了这样的程序,关掉它,或者使用cron让它在非高峰的时候运行。
  • 通过top找到非关键的、CPU密集型进程,然后用renice修改它的优先级。
  • 在基于SMP的机器上,尝试使用taskset命令绑定进程到CPU上,避免进程在多个处理器之间切换,引起cache刷新。
  • 基于运行的应用,确认你的应用是否能高效的利用多处理器。来决定是否应该使用更强劲的CPU而不是更多的CPU。例如,单线程应用,会从更快的CPU中受益,增加值CPU个数也没用。
  • 还有其它办法,比如,确保你使用的是最新的驱动和固件,这能影响到他们在系统上的负载。

内存瓶颈

页错误(Page faults) 有两类页错误:软页错误,在内存中发现页;硬页错误,在内存中没有发现页,而必须从磁盘中获取。访问磁盘会显著的使应用变慢。sar -B命令可以提供观察页错误的信息。,尤其是pgpgin/s和pgpgout/s两列。

分页可能是严重的性能问题,当空闲内存量小于预设的值时,因为分页机制不能处理对物理内存页的请求,于是调用swap机制释放更多的页,把内存数据放入到swap中。这会导致增加I/O,并且明显的性能降低。 如果服务器总是分页到磁盘(page-out很高)上,应该考虑增加更多的内存。然而,如果系统的page-out很低,可能是性能利用不充分

性能调优选项

如果确定是内存瓶颈,可以执行下面的操作:

  • 使用bigpages、hugetlb和共享内存调优swap空间。
  • 增加或者减少页大小。
  • 改善活动和非活动的内存处理
  • 调整page-out率
  • 限制服务器上每个用户可使用的资源
  • 关掉用不到的服务
  • 增加内存

没有严肃的判定标准

找到磁盘瓶颈

服务器表现出如下的症状,可能是磁盘出现了瓶颈:

  • 磁盘慢的表现:
    • 内存缓冲中填满了写数据(或者在等待读数据),因为没有可用的空闲内存缓冲供写(或者是在等待磁盘队列中的读数据响应),拖慢了所有请求。
    • 内存不足,在没有可以为网络请求分配足够内存缓冲的时候,会产生同步磁盘I/O。
  • 磁盘或者控制器使用率变高。
  • 大多数网络传输都是在磁盘I/O完成之后。表现形式为极长的响应时间和非常低的网络利用率。
  • 磁盘I/O花费相当长的时间,并且磁盘队列变满,因为处理请求时间变长,所以CPU利用率变得很低。

磁盘子系统可能是最难配置的子系统。除了查看磁盘接口速度和磁盘容量,还要理解磁盘负载。访问磁盘是随机的还是顺序的?I/O是大还是小?为了充分利用磁盘,需要回答上面的这些问题。

厂商会一般会给你展示它们设备的吞吐量上限。但是,花时间来了解你的工作负载吞吐量将会帮你找到你所需要的磁盘子系统。

下表展示不同驱动在8KB I/Os下的真实吞吐。

磁盘速度 延时 寻道时间 完全随机访问时间 单盘每秒I/O 8 KB I/O的吞吐
15000 RPM 2.0 ms 3.8 ms 6.8 ms 147 1.15 Mbps
10000 RPM 3.0 ms 4.9 ms 8.9 ms 112 900 KBps
7200 RPM 4.2 ms 9 ms 13.2 ms 75 600 KBps
  • a. 如果处理命令 + 传输数据 < 1ms,完全随机访问 = 延时 + 寻道时间 + 1ms
  • b. 以1/随机访问时间为吞吐量。

随机读写负载通常需要多个磁盘决定。SCSI或者光纤的带宽不太要关注。大量随机访问负载的数据库最好有多块磁盘。大的SMP服务器最好配置多块磁盘。通常磁盘可以简单的平均划分为70%的读和30%的写,RAID10的性能比RAID5要高出50%到60%。

顺序读写需要看重磁盘子系统的总线带宽。需要最大吞吐量的时候,需要特别关注SCSI总线或者光纤控制器的数量。在阵列中,为每个盘指定相同的数量,RAID-10、RAID-0和RAID-5的读写吞吐流很相似。

分析磁盘瓶颈的办法:实时监控和跟踪.

  • 在问题发生的时候一定要做实时监控。在动态系统负载和问题不可重现的情况下,这可能是不实际的。然而,如果问题是可重现的,通过这个办法就可以增加对象和计数器使问题更清晰。
  • 跟踪是通过收集一段时间的性能数据来诊断问题。这是远程性能分析的好办法。缺点是在问题不可重现的时候,需要分析大量的文件,如果没有跟踪到所有的关键对象和参数,必须等待下一次问题出现来获取额外的数据。

使用vmstat工具。vmstat中关于I/O最重要的列是bi和bo。

iostat命令

在反复同时打开、读、写、关闭太多文件的时候可能遇到性能问题。这可能会以寻道时间(把磁头移动到数据存储位置的时间)变长的现象表现出来。使用iostat工具,可以监控I/O设备的实时负载。不同的选项可以帮你挖掘到更深更多有用的数据。输出显示平均等待时间(await),服务时间(svctm) util看负载繁忙度

性能调优选项

在确定磁盘子系统瓶颈之后,有如下可能的解决方法:

  • 如果负载是顺序的,压力在控制器带宽上,办法就是添加更快的磁盘控制器。然而,如果负载是随机的,瓶颈可能在磁盘上,增加更多多的磁盘可以帮助增加性能。
  • 在RAID中添加更多的磁盘,把数据分散到多块物理磁盘,可以同时增强读和写的性能。增加磁盘会提升每秒的读写I/O数。另外,请使用硬件RAID而不是Linux提供的RAID软件。如果是硬件RAID,RAID级别对操作系统是不可见的。
  • 考虑使用Linux逻辑卷分区,而不是没有分区的单块大磁盘或者逻辑卷。
  • 把处理负载转移到网络中的其它系统(用户,应用程序或者服务)。
  • 添加RAM。添加内存会提升系统磁盘缓冲,增强磁盘响应速度

内核参数存储路径

内核参数存储在/proc(一般来说是/proc/sys)目录下。

通过/proc目录树下的文件,可以简单的了解与内核、进程、内存、网络以及其它组件相关的参数配置。每一个进程在/proc目录下都有一个以它的PID命令的目录。下表是部分文件所包含内核信息说明。

文件/目录 作用
/proc/sys/abi/* 用于提供对外部二进制的支持,比如在类UNIX系统,SCO UnixWare 7、SCO OpenServer和SUN Solaris 2上编译的软件。默认情况下是安装的,也可以在安装过程中移除。
/proc/sys/fs/* 设置系统允许的打开文件数和配额等。
/proc/sys/kernel/* 可以启用热插拔、操作共享内存、设置最大的PID文件数和syslog中的debug级别。
/proc/sys/net/* 优化网络,IPV4和IPV6
/proc/sys/vm/* 管理缓存和缓冲

使用sysctl命令

sysctl使用/proc/sys目录树中的文件名作为参数。例如,shmmax内核参数保存在/proc/sys/kernel/shmmax中,你可以使用cat来读取、echo来修改:

#cat /proc/sys/kernel/shmmax
33554432
#echo 33554430 > /proc/sys/kernel/shmmax
#cat /proc/sys/kernel/shmmax
33554430

然而,使用echo很容易弄错,所以我们推荐使用sysctl命令,因为它会修改前检查数据一致性,如下:

#sysctl kernel.shmmax
kernel.shmmax = 33554432
#sysctl -w kernel.shmmax=33554430
kernel.shmmax = 33554430
#sysctl kernel.shmmax
kernel.shmmax = 33554430

上面的修改会在下次重启之后消失。如果你想做永久修改,你应该编辑/etc/sysctl.conf文件,添加参数如下:

kernel.shmmax = 33554439

下次重启系统的时候会读取/etc/sysctl.conf文件,你也可以通过如下的命令,不用重启就让配置生效:

# sysctl -p

进程优先级

前面已经说过,我们不可能修改某个进程的优先级。唯一的办法是间接的通过调整进程nice级别,但这也并非总是可行。如果某个进程运行太慢,你就可以调低它的nice级别,给它更过的CPU时间。当然,这就意味着其它进程拥有更低的处理器时间,将会变得更慢。

Linux支持的nice级别从19(最低级别)到-20(最高级别)。默认值是0。需要使用root权限,才能把进程的nice级别设置为负数(更高的优先级)。

以nice级别-5启动xyz程序,使用如下命令:

nice -n -5 xyz

修改运行中的程序优先级,使用如下命令:

renice level pid

把PID为2500的进程优先级设置为10:

renice 10 2500

CPU中断处理

在中断处理中,如下两个原则是十分有效的:

  • 把产生大量中断的进程绑定到一个CPU上。

CPU亲和力使得管理员可以把中断绑定到某个或者某组物理处理器上(当然,这对单CPU系统无效)。要更改某个IRQ的亲和力,进入到/proc/irq/%{irq号码}/目录,修改smp_affinity文件的CPU mask值。把19号IRQ的亲和力设置到第三个CPU的命令如下(不适用SMT):

echo 03 > /proc/irq/19/smp_affinity
  • 让物理处理器处理中断

在对称多线程(SMT,symmetric multi-threadind)系统中,例如IBM POWER 5+处理器支持多线程,它就推荐将中断处理绑定到物理处理器上,而不是SMT实例。在双路多线程系统中,物理处理器通常都是较低的CPU数,CPU 编号 0和2通常是物理CUP,而1和3通常是指多线程实例。

如果你不使用smp_affnity标志,你就没必要担心这个了!

考虑NUMA系统

非统一内存架构(NUMA,Non-Uniform Memory Architecture)系统正在获取市场份额,被看做是传统统一多处理系统的自然演化。尽管当前Linux发行版的CPU调度也同样适用于NUMA系统,但是应用程序可能就不一定了。由non-NUMA导致的性能降低是很难识别的。可以使用numactl包中的numastat工具找到在NUMA架构上有问题进程。

numastat统计数据保存在/sys/devices/system/node/%{node number}%/numastat文件中,可以用来帮助找到瓶颈。numa_miss和other_node字段偏高可能是MUMA引起的。如果你发现给进程分配的内存不是在进程的本地节点上(运行程序的处理器节点),尝试对这个进程renice或者采用NUMA。

网络调优

在相同的配置的情况下,即使细微的流量行为差别也能导致巨大的性能差别。请熟悉下面的网络流量行为和要求:

  • 事务吞吐需求(峰值和平均值)
  • 数据传输的吞吐需求(峰值和平均值)
  • 延时需求
  • 传输数据大小
  • 接收和发送的比例
  • 连接建立和关闭的频率,还有连接并发数
  • 协议(TCP,UDP,应用协议,比如HTTP,SMTP,LDAP等等)

使用netstat,tcpdump,ethereal等工具可以获得准确的行为。

MTU大小

在Gb网络中,最大传输单元(maximum transmission units,MTU)越大,网络性能越好。问题是,太大的MTU可能不受大多数网络的支持,大量的网卡不支持大MTU。如果要在Gb速度上传输大量数据(例如HPC环境),增加默认MTU可以导致明显的性能提升。使用/sbin/ifconfig修改MTU大小

增加网络缓冲

Linux网络栈在分配内存资源给网络缓冲时十分谨慎!在服务器所连接的现代高速网络中,如下的参数应该增大,使得系统能够处理更多网络包。

  • TCP的初始内存大小是根据系统内存自动计算出来的;可以在如下文件中找到这个值:
/proc/sys/net/ipv4/tcp_mem
  • 调高默认以及最大接收Socket的内存值:
/proc/sys/net/core/rmem_default
/proc/sys/net/core/rmem_max
  • 调高默认和最大发送Socket的内存值:
/proc/sys/net/core/wmem_default
/proc/sys/net/core/wmem_max
  • 调高最大缓存值:
/proc/sys/net/core/optmem_max

调整窗口大小

可以通过上面的网络缓冲值参数来优化最大窗口大小。可以通过BDP(时延带宽积,bandwidth delay product)来获得理论上的最优窗口大小。BDP是导线中的传输的数据量。BDP可以使用如下的公式计算得出:

BDP = Bandwidth (bytes/sec) * Delay (or round trip time) (sec)

要使网络管道塞满,达到最大利用率,网络节点必须有和BDP相同大小的缓冲区。另一方面,发送方还必须等待接收方的确认,才能继续发送数据。

例如,在1ms时延的GB级以太网中,BDP等于:

125Mbytes/sec (1Gbit/sec) * 1msec = 125Kbytes

在大多数发行版中,rmem_max和wmem_max的默认值都是128KB,对一般用途的低时延网络环境已经足够。然而,如果时延很大,这个默认值可能就太小了!

再看看另外一个例子,假如一个samba文件服务器要支持来自不同地区的16个同时在线的文件传输会话,在默认配置下,平均每个会话的缓冲大小就降低为8KB。如果数据传输量很大,这个值就会显得很低了!

  • 把所有协议的系统最大发送缓冲(wmem)和接收缓冲(rmem)设置为8MB。
sysctl -w net.core.wmem_max=8388608
sysctl -w net.core.rmem_max=838860

指定的这个内存值会在TCP socket创建的时候分配给每个TCP socket。

  • 此外,还得使用如下的命令设置发送和接收缓冲。分别指定最小、初始和最大值。
sysctl -w net.ipv4.tcp_rmem="4096 87380 8388608"
sysctl -w net.ipv4.tcp_wmem="4096 87380 8388608"

第3个值必须小于或等于wmem_max和rmem_max。在高速和高质量网络上,建议调大第一个值,这样,TCP窗口在一个较高的起点开始。

  • 调高/proc/sys/net/ipv4/tcp_mem大小。这个值的含义分别是最小、压力和最大情况下分配的TCP缓冲。

可以使用tcpdump查看哪些值在socket缓冲优化中被修改了。如下图所示,把socket缓冲限制在较小的值,导致窗口变小,引起频繁的确认包,会使网络效率低下。相反,增加套接字缓冲会增加窗口大小。

socket缓冲大小

在服务器处理许多大文件并发传输的时候,小socket缓冲可能引起性能降低。如下图所示,在小socket缓冲的情况下,导致明显的性能下降。在rmem_max和wmem_max很小的情况下,即使对方拥有充足的socket缓冲可用,还是会影响可用缓冲大小。这使得窗口变小,成为大数据传输时候的瓶颈。下图中没有包含小数据(小于4KB)传输的情况,实际中,小数据传输不会受到明显的影响。

额外的TCP/IP调整

还有很多其它增加或降低网络性能的配置。如下的参数可能会帮助提升网络性能。

优化IP和ICMP

如下的sysctl命令可以优化IP和ICMP:

  • 禁用如下的参数可以阻止骇客进行针对服务器IP的地址欺骗攻击。
sysctl -w net.ipv4.conf.eth0.accept_source_route=0
sysctl -w net.ipv4.conf.lo.accept_source_route=0
sysctl -w net.ipv4.conf.default.accept_source_route=0
sysctl -w net.ipv4.conf.all.accept_source_route=0
  • 如下的服务器配置用来忽略来自网关机器的重定向。重定向可能是攻击,所以我们值允许来自可信来源的重定向。
sysctl -w net.ipv4.conf.eth0.secure_redirects=1
sysctl -w net.ipv4.conf.lo.secure_redirects=1
sysctl -w net.ipv4.conf.default.secure_redirects=1
sysctl -w net.ipv4.conf.all.secure_redirects=1
  • 你可以设置网卡是否接收ICMP重定向。ICMP重定向是路由器向主机传达路由信息的一种机制。例如,当路由器从某个接口收到发往远程网络的数据时,发现源ip地址与下一跳属于同一网段,这是路由器会发送ICMP重定向报文。网关检查路由表获取下一跳地址,下一个网关把网络包发给目标网络。使用如下的命令静止重定向:
sysctl -w net.ipv4.conf.eth0.accept_redirects=0
sysctl -w net.ipv4.conf.lo.accept_redirects=0
sysctl -w net.ipv4.conf.default.accept_redirects=0
sysctl -w net.ipv4.conf.all.accept_redirects=0
  • 如果服务器不是网关,它就没必要发送重定向包,可以禁用如下的参数
sysctl -w net.ipv4.conf.eth0.send_redirects=0
sysctl -w net.ipv4.conf.lo.send_redirects=0
sysctl -w net.ipv4.conf.default.send_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0
  • 配置服务器忽略广播ping和smurf攻击:
sysctl -w net.ipv4.icmp_echo_ignore_broadcasts=1
  • 忽略所有类型的icmp包和ping:
sysctl -w net.ipv4.icmp_echo_ignore_all=1
  • 有些路由器会发送错误的广播响应包,内核会对每一个包都会生成日志,这个响应包可以忽略:
sysctl -w net.ipv4.icmp_ignore_bogus_error_responses=1
  • 还应该设置ip碎片参数,尤其是NFS和Samba服务器来说。可以设置用来做IP碎片整理的最大和最小缓冲。当以bytes为单位设置ipfrag_high_thresh的值之后,分配处理器会丢掉报文,直到达到ipfrag_low_thresh的值。

当TCP包传输中发生错误时,就会产生碎片。有效的数据包会放在缓冲中,而错误的包会重传。

例如,设置可用内存范围为256M到384M,使用:

sysctl -w net.ipv4.ipfrag_low_thresh=262144
sysctl -w net.ipv4.ipfrag_high_thresh=393216

优化TCP

这里讨论通过调整参数,修改TCP的行为。

如下的命令可以用来调整服务器支持巨大的连接数。

  • 对于并发连接很高的服务器,TIME-WAIT套接字可以重复利用。这对web服务器很有用:
sysctl -w net.ipv4.tcp_tw_reuse=1

如果使用了上面的参数,还应该开启TIME-WAIT的套接字快速回收参数:

sysctl -w net.ipv4.tcp_tw_recycle=1
  • tcp_fin_timeout是socket在服务器上关闭后,socket保持在FIN-WAIT-2状态的时间。

TCP连接以三次握手同步syn开始,以3次FIN结束,过程中都不会传递数据。通过修改tcp_fin_timeout值,定义何时FIN队列可以释放内存给新的连接,由此提升性能。因为死掉的socket有内存溢出的风险,这个值必须在仔细的观察之后才能修改:

sysctl -w net.ipv4.tcp_fin_timeout=30
  • 服务器可能会遇到大量的TCP连接打开着,却没有使用的问题。TCP有keepalive功能,探测这些连接,默认情况下,在7200秒(2小时)后释放。这个时间对服务器来说可能太长了,还可能导致超出内存量,降低服务器性能。

把这个值设置为1800秒(30分钟):

sysctl -w net.ipv4.tcp_keepalive_time=1800
  • 当服务器负载很高,拥有很多坏的高时延客户端连接,会导致半开连接的增长。在web服务器中,这个现象很常见,尤其是在许多拨号用户的情况下。这些半开连接保存在backlog connections队列中。你应该该值最小为4096(默认是1024)。

即使服务器不会收到这类连接,也应该设置这个参数,他可以保护免受DoS(syn-flood)攻击。

sysctl -w net.ipv4.tcp_max_syn_backlog=4096
  • TCP SYN cookies可以帮助保护服务器免受syn-flood攻击,无论是DoS(拒绝服务攻击,denial-of-service)还是DDoS(分布式拒绝服务攻击,distributed denial-of-service),都会对系统性能产生不利影响。建议只有在明确需要TCP SYN cookies的时候才开启。
sysctl -w net.ipv4.tcp_syncookies=1

注意,只有在内核编译了CONFIG_SYNCOOKIES选项的时候,上面的命令才是正确的

优化TCP选项

如下的TCP选项可以进一步优化Linux的TCP协议栈。

  • 选择性确认可以在相当大的成都上优化TCP流量。然而,SACK和DSACK可能对Gb网络产生不良影响。tcp_sack和tcp_dsack默认情况下是启用的,但是和优化TCP/IP性能向背,在高速网络上应该禁用这两个参数。
sysctl -w net.ipv4.tcp_sack=0
sysctl -w net.ipv4.tcp_dsack=0
  • 每个发往Linux网络栈的以太帧都会收到一个时间戳。这对于防火墙、Web服务器这类系统是很有用且必要的,但是后端服务器可能会从禁用TCP时间戳,减少负载中获益。可以使用如下的命令禁用TCP时间戳:
sysctl -w net.ipv4.tcp_timestamps=0
  • 我们已经知道缩放窗口可以增大传输窗口。然而,测试表明,窗口缩放对高网络负载的环境不合适。另外,某些网络设备不遵守RFC指导,可能导致窗口缩放故障。建议禁用窗口缩放,并且手动设置窗口大小。
sysctl -w net.ipv4.tcp_window_scaling=0

增加包队列

在增加各种网络缓冲大小之后,建议增加未处理包的数量,那么,花更长的时间,内核才会丢弃包。可以通过修改/proc/sys/net/core/netdev_max_backlog来实现。

增加发送队列长度

把每个接口的txqueuelength参数值增加到1000至20000。这对数据均匀且量大的高速网络连接十分有用。可以使用ifconfig命令调整传输队列长度。

[root@linux ipv4]# ifconfig eth1 txqueuelen 2000

减少中断

除非使用NAPI,处理网络包的时候需要Linux内核处理大量的中断和上下文切换。对Intel e1000–based网卡来说,要确保网卡驱动编译了CFLAGS_EXTRA -DCONFIG_E1000_NAP 标志。Broadcom tg3模块的最新版本内建了NAPI支持。

如果你需要重新编译e1000驱动支持NAPI,你可以通过在你的系统做如下操作;

make CFLAGS_EXTRA -DCONFIG_E1000_NAPI

此外,在多处理器系统中,把网卡中断绑定到物理CPU可能带来额外的性能提升。为实现这个目标,首先要识别特定网卡的IRQ。可以通过ifconfig命令获得中断号。

获取中断号之后,你可以使用在/proc/irq/%{irq number}中mp_affinity参数把中断和CPU绑定在一起。如下图示范了如何把之前获取的eth1网卡的169号中断,绑定到系统的第二个处理器。

ref
  1. https://lihz1990.gitbooks.io/transoflptg/content/

Read More

ACCU an adventure in race conditions


why

演讲人是 Felix Petriconi,这个ppt讲了几个典型的竞争场景,ppt见参考链接1

而且作者列了详尽的资料。够看一个月


作者的工作属性经常需要做图像处理,压缩等,就需要一个并发场景

一个典型的图像压缩场景

将图分成若干片 ->分别压缩 ->合并

如果用future就很简单

struct CompresssContext{} ctx;
bool compress(CompresssContext&){return true;}
void merge(CompresssContext&){}
int main(){
    vector<boost::future<void>> tasks{16};
    for(auto & f: tasks)
        f = async([]{compress(ctx);});
    auto done = boost::when_all(task.begin(),task.end())
        .then([]{merge(ctx);});
}

这套future使用组件已经进TS了,可能后续能用上 2

面对这种场景,简单粗暴的方法就是起线程

int main() {
  const int ThreadNumber = 2;
  vector <thread> threads{ThreadNumber};
  for (auto& item : threads)
    item = thread{ []{ compress(ctx); } };

  for (auto& item : threads)
    item.join();
  merge(ctx);
}

如果考虑到切换的开销,妥协方案就是线程池模型了

这也是作者的方向,然后作者遇到了三个竞争问题

1

int main()
{
    const int TaskNumber{16};
    atomic_int to_do{TaskNumber};
    mutex block;
    condition_variable cv;
    for (int i = 0; i < TaskNumber; ++i)
        stlab::default_executor( // thread pool from stlab/concurrency
            [&]() {
                compress(ctx);
                --to_do;
                cv.notify_one();
            });

    unique_lock lock{block};
    while (to_do != 0)
        cv.wait(lock);

    merge(ctx);
}

第一稿是这个样子,注意executor是个线程池,直接把lambda放到后台执行。

这里的todo是条件变量用来wait的值,注意是原子量,没有加锁,这是错误的,本质上mutex就是个channel,保证这个访问的严格串行通知,如果不加锁,todo的load过程可能就会被上层切走,执行wait,然后又被切回来,导致wait值和notify_one不一致,丢掉唤醒。保证这种场景的要求就是控制在一个channel内,这样系统上层保证不yield

2

...
            [&]() {
                unique_lock l(lock);
                {
                    compress(ctx);
                    --to_do;
                }
                cv.notify_one();
            });

然后作者第二部改动是加了个unique_lock 锁住–todo,而没锁notify_one,作者可能考虑锁住判断条件就足够了。。。实际上原因和上面是一样的。notify_one不在lock下,在mutex外的执行区间可能就会被遗漏。第二版改进就是把notify_one挪到括号内

3

全局变量问题。

代码中的todo等等都是全局的,这会有一个问题,有的线程退出但是全局变量被污染了。解决办法就是引入上下文

struct CompressContext{} ctx;
bool compress(CompressContext&)
{ return true; }
void merge(CompressContext&) {}
struct ProcessContext
{
  mutex block;
  condition_variable cv;
  int to_do = 0;
  atomic_bool abort{false};
};
const int TaskNumber{16};
auto pctx = make_shared <ProcessContext >();
pctx->to_do = TaskNumber;
for (int i = 0; i < TaskNumber; ++i)
stlab::default_executor(
  [_weakContext = weak_ptr <ProcessContext >(pctx)] {
    auto p = _weakContext.lock();
    if (!p || p->abort)
      return;
    auto do_abort = !compress(ctx);
    {
      unique_lock guard{p->block};
      --p->to_do;
      p->abort = do_abort || p->abort;
      p->cv.notify_one();
    }
  });

unique_lock lock{pctx->block};
while (pctx->to_do != 0 && !pctx->abort)
  pctx->cv.wait(lock);
merge(ctx);

作者的总结是这些底层原语相当难用容易用错,不如用future promise来的块些

另外,作者的引用文章很有分量,看不完

  • Concurrency library https://github.com/stlab/libraries
  • Documentation http://stlab.cc/libraries
  • Communicating Sequential Processes by C. A. R. Hoare http://usingcsp.com/cspbook.pdf
  • The Theory and Practice of Concurrency by A.W. Roscoe http://www.cs.ox.ac.uk/people/bill.roscoe/publications/68b.pdf
  • Towards a Good Future, C++ Standard Proposal by Felix Petriconi, David Sankel and Sean Parent http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0676r0.pdf
  • A Unified Futures Proposal for C++ by Bryce Adelstein Lelbach, et al http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1054r0.html

Software Principles and Algorithms

  • Elements of Programming by Alexander Stepanov, Paul McJones, Addison Wesley
  • From Mathematics to Generic Programming by Alexander Stepanov, Daniel Rose, Addison Wesley

Concurrency and Parallelism

  • HPX http://stellar-group.org/libraries/hpx/
  • C++CSP https://www.cs.kent.ac.uk/projects/ofa/c++csp
  • CAF C++ Actor Framework http://actor-framework.org/
  • C++ Concurrency In Action by Anthony Williams, Manning, 2nd Edition

  • Goals for better code by Sean Parent:http://sean-parent.stlab.cc/papers-and-presentations

  • Goals for better code by Sean Parent: Concurrency: https://youtu.be/au0xX4h8SCI?t=16354
  • Future Ruminations by Sean Parent http://sean-parent.stlab.cc/2017/07/10/future-ruminations.html
  • CppCast with Sean Parent http://cppcast.com/2015/06/sean-parent/
  • Thinking Outside the Synchronization Quadrant by Kevlin Henney: https://vimeo.com/205806162
  • Inside Windows 8 Thread Pool https://channel9.msdn.com/Shows/Going+Deep/Inside-Windows-8-Pedro-Teixeira-Thread-pool

ref

  1. https://github.com/ACCUConf/PDFs_2019/blob/master/felix_petriconi_-_an_adventure_in_race_conditions.pdf

  2. https://en.cppreference.com/w/cpp/experimental/when_all

  3. 这里也介绍了pthread cv原语用法中的著名错误,但是例子不同,判断条件不是原子的。https://zhuanlan.zhihu.com/p/55123862

  4. 这是个好问题,为什么pthread_cond_signal需要在mutex下执行,即使判断条件是原子的也是不行的 https://www.zhihu.com/question/53631897

    通过这个问题也能理解,为什么必须要锁,可以理解成mutex是channel,wait和signal通过channel来通信。如果不走这个channel,可能消息就会丢,metux保护的并不是判断条件,保护的是wait和signal之间的条件同步,即signal改动透过channel的维持让wait知道。否则wait很容易丢掉这个通知

  5. https://stackoverflow.com/questions/41867228/why-do-i-need-to-acquire-a-lock-to-modify-a-shared-atomic-variable-before-noti?r=SearchResults

    这个答案进一步解释了这个问题

    代码

    static std::atomic_bool s_run {true};
    static std::atomic_bool s_hasEvent {false};
    static std::mutex s_mtx;
    static std::condition_variabel s_cv;
    // Thread A - the consumer thread
    function threadA()
    {
        while (s_run)
        {
            {
                std::unique_lock<std::mutex> lock(s_mtx);
                s_cv.wait(lock, [this]{
                    return m_hasEvents.load(std::memory_order_relaxed);
                });
            }
       
            // process event
            event = lockfree_queue.pop();
            ..... code to process the event ....
        }
    }
    // Thread B - publisher thread
    function PushEvent(event)
    {
        lockfree_queque.push(event)
        s_hasEvent.store(true, std::memory_order_release);
        s_cv.notify_one();
    }
    

    可能丢掉notify_one的场景

    1. Thread A locks the mutex.

    2. Thread A calls the lambda’s closure which does m_hasEvents.load(std::memory_order_relaxed); and returns the value false.
    3. Thread A is interrupted by the scheduler and Thread B starts to run.
    4. Thread B pushes an event into the queue and stores to s_hasEvent
    5. Thread B runs s_cv.notify_one().
    6. Thread B is interrupted by the scheduler and Thread A runs again.
    7. Thread A evaluates the false result returned by the closure, deciding there are no pending events.
    8. Thread A blocks on the condition variable, waiting for an event.

    其中第四步如果有锁,这个改动就不会丢,你改动是不是原子的无所谓,需要保证观测者是一个原子的状态,即通过这个channel来控制。

  6. https://stackoverflow.com/questions/32978066/why-is-there-no-wait-function-for-condition-variable-which-does-not-relock-the-m/32978267#32978267

    这是上面的链接中提到的一个场景,yakk的代码不错

contact

Read More

libstdc++.so.6 version GLIBCXX_3.4.20 not found


屡次被stdc++ glibc 符号困扰。

从4.9开始记录。这个版本算是真正支持c++11的

gcc版本 libstdc++版本号 glibc版本号 cxxabi版本号
4.9 libstdc++.so.6.0.20 GLIBCXX_3.4.20 CXXABI_1.3.8
5.1 libstdc++.so.6.0.21 GLIBCXX_3.4.21 CXXABI_1.3.9
6.1 libstdc++.so.6.0.22 GLIBCXX_3.4.22 CXXABI_1.3.10
7.1 libstdc++.so.6.0.23 GLIBCXX_3.4.23 CXXABI_1.3.11
7.2 libstdc++.so.6.0.24 GLIBCXX_3.4.24 CXXABI_1.3.11
8.1 libstdc++.so.6.0.25 GLIBCXX_3.4.25 CXXABI_1.3.11
9.1 libstdc++.so.6.0.26 GLIBCXX_3.4.26 CXXABI_1.3.12
9.2 libstdc++.so.6.0.27 GLIBCXX_3.4.27 CXXABI_1.3.12
9.3 libstdc++.so.6.0.28 GLIBCXX_3.4.28 CXXABI_1.3.12
10.1 libstdc++.so.6.0.28 GLIBCXX_3.4.28 CXXABI_1.3.12

why

CI编译机是docker,内置了gcc4.8.5,在上层目录gcc5.4,不能网页端设定gcc版本,要用需要自己脚本适配。这是个踩坑记录

首先编译环境需要引入新的gcc

export PATH=$WORKSPACE/../buildbox/gcc-5.4.0/bin:$PATH

然后注意,gcc对应的libstdc++ abi不同, 不匹配就会遇到标题中的错误,

/lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found
/lib64/libstdc++.so.6: version `CXXABI_1.3.8' not found
...

所以需要调整一下libso,gcc 版本与libstdc++对应关系如下

GCC 4.9.0: libstdc++.so.6.0.20
GCC 5.1.0: libstdc++.so.6.0.21
GCC 6.1.0: libstdc++.so.6.0.22
GCC 7.1.0: libstdc++.so.6.0.23
GCC 7.2.0: libstdc++.so.6.0.24
GCC 8.0.0: libstdc++.so.6.0.25

解决方法,改软链接

cp $WORKSPACE/../buildbox/gcc-5.4.0/lib64/libstdc++.so.6.0.21 /lib64/
cd /lib64/
rm -f libstdc++.so.6
ln -s libstdc++.so.6.0.21 libstdc++.so.6
ldconfig

到这里,普通makefile编译应该就没有问题了。

注意,此时 lib64下,libstdc++.so.6.0.21和libstdc++.so.6.0.19是共存的,如果依赖cmake的项目是不能删掉19的

对于cmake,cmake会探测到默认的gcc而不使用新的gcc,所以需要指定编译器

cmake . -DCXX_COMPILER_PATH=$WORKSPACE/../buildbox/gcc-5.4.0/bin/g++

但是,cmake会通过编译来判定工作环境,如果之前删掉了libstdc++.so.6.0.19,gcc4.8.5就不能工作,就会有编译错误

-- Check for working CXX compiler: /usr/bin/c++ -- broken
  CMake Error at cmake-3.10.3/share/cmake-3.10/Modules/CMakeTestCXXCompiler.cmake:45 (message):
    The C++ compiler  
      "/usr/bin/c++"  
    is not able to compile a simple test program.  
    It fails with the following output:  
      Change Dir: build/CMakeFiles/CMakeTmp      
      Run Build Command:"/usr/bin/gmake" "cmTC_2fb3b/fast"
      /usr/bin/gmake -f CMakeFiles/cmTC_2fb3b.dir/build.make CMakeFiles/cmTC_2fb3b.dir/build
      gmake[1]: Entering directory `build/CMakeFiles/CMakeTmp'
      Building CXX object CMakeFiles/cmTC_2fb3b.dir/testCXXCompiler.cxx.o
      /usr/bin/c++     -o CMakeFiles/cmTC_2fb3b.dir/testCXXCompiler.cxx.o -c /data/fuxi-task/workSpace/5d1b501bdf2fcc0001b69669/DFV-Redis_222652/build/CMakeFiles/CMakeTmp/testCXXCompiler.cxx
      Linking CXX executable cmTC_2fb3b
      cmake-3.10.3/bin/cmake -E cmake_link_script CMakeFiles/cmTC_2fb3b.dir/link.txt --verbose=1
      /usr/bin/c++       -rdynamic CMakeFiles/cmTC_2fb3b.dir/testCXXCompiler.cxx.o  -o cmTC_2fb3b 
      /usr/bin/ld: cannot find -lstdc++
      collect2: error: ld returned 1 exit status
      gmake[1]: *** [cmTC_2fb3b] Error 1
      gmake[1]: Leaving directory `build/CMakeFiles/CMakeTmp'
      gmake: *** [cmTC_2fb3b/fast] Error 2 
    CMake will not be able to correctly generate this project.
  Call Stack (most recent call first):
    CMakeLists.txt:2 (project)  
  -- Configuring incomplete, errors occurred!

再或者,直接静态链接libstdc++就好了

target_link_libraries(gemini-proxy -static-libgcc -static-libstdc++)

ref

  1. https://stackoverflow.com/questions/44773296/libstdc-so-6-version-glibcxx-3-4-20-not-found/46613656
  2. https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html

contact

Read More

^