本次调优主要解决的问题的是ygc的问题,不涉及到full gc。
gc切到了g1,主要是由于是并行-并发,相比CMS没碎片的考虑,而且STW的时间是可预测的。
线上开启gc日志,参数如下:
-verbose:gc -XX:+PrintHeapAtGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -Xloggc:logs/gc.log
在gc log里可以看到如下的日志[GC pause (G1 Evacuation Pause) (young),一般情况就是Eden区满了,触发了Young gc。
g1也是分代回收gc, young gen的大小一般不固定通过NewSize进行配置,而是通过-XX:G1NewSizePercent以及-XX:XX:G1MaxNewSizePercent控制新生代的大小,由gc根据-XX:MaxGCPauseMillis动态进行控制。
动态调整简单理解在剩余内存充足的情况下当满足-XX:MaxGCPauseMillis使用G1MaxNewSizePercent,当不满足的时候使用G1NewSizePercent。
其中默认值G1MaxNewSizePercent=60, G1NewSizePercent=5,需要UnlockExperimentalVMOptions才能配置。
查看默认值可以通过如下命令:
java -XX:+UseG1GC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -version|grep "G1"
g1一样会把young gen分为Eden区以及2个Survivor区(From以及To),其中-XX:SurvivorRatio 指定Eden space与单个Survivor space的大小比例。默认值为8,则Eden区占用8/10, Survivor为1/10。
默认会开启TLAB,对于小对象,g1通过TLAB分配在eden区,大对象会分配在Tenred区。Eden区满了会触发ygc,Eden中存活的对象以及From存活的对象被移动到To中, 存活的对象熬过了一定次数的gc就会被移动到老年代。
这里有几个细节需要注意的:
1) Survivor中最大的gc年龄通过-XX:InitialTenuringThreshold 和 -XX:MaxTenuringThreshold 可以设定晋升到老年代age阀值的初始值和最大值,最大为15。
对象晋升的age会根据-XX:TargetSurvivorRatio动态进行调整,可以通过-XX:+PrintTenuringDistribution查看survivor的分布以及threshold的大小。
2) 在g1中如果存活对象太多,超过了Survivor space的大小,就发生了survivor overflow,会将overflow的数据存在从free space拿的region上,然后放在Tenured区。
如果free space太小没空间可以存储这么多数据,就发生了survivor exhaustion,导致full gc的发生。g1只有ygc以及mixed gc,其中配套的full gc为serial old GC,会严重影响性能。
根据分代的理论,ygc主要存储的是临时的对象,比较频繁就是临时对象分配的太快了。
根据服务的qps以及每个请求所需要分配的平均空间大小大致可以算出需要young gc的使用情况。
young gc频繁了就可以降低qps或者减少每个请求的平均空间大小来解决。怎么分流请求就不提了,主要是怎么降低请求的平均空间大小了。
具体而已就是得到请求的对象的分配情况,然后有针对性的减少频繁分配对象的创建。可以通过提供了allocation profiler功能的profiler对jvm进行监控。
一般可以通过visualvm或者jfr,但是visualvm不支持远程jvm的profiler,这里使用的是jmc。
关于jmc,jdk11不再自带jmc,从JMC7开始已经开源,需要独立下载。同时支持sun jdk以及openjdk, 之前版本的jvm需要商业授权使用。
启动jfr有好几种方式,这里是通过jcmd进行启动。
具体的操作步骤
1)开启jfr
jcmd <pid> JFR.start settings=profile maxage=10m maxsize=150m disk=true name=<name>
2)导出jfr文件
jcmd <pid> JFR.dump name=<name>/recording=<num> filename=FILEPATH
3)关闭jfr
jcmd <pid> JFR.stop name=<name>
关于jfr的命令大家可以参考
导出jfr记录后可以通过jmc打开,在memory分析里可以查看对象分配的callstack进行具体分析,如下图所示,显示有很多内存是由于日志导致的,是由于部分日志打印的不合理,占用了大量的内存。 把日志调整后,ygc的频次有明显下降。
g1的日志打印的比较详细了,里面打印了ygc每个阶段所执行的步骤以及耗时。
主要有几个时间主要注意
1)Object Copy的处理,这是copy存活对象的时间,主要受live object的影响。
2)Ref Proc/Ref Enq的处理,主要是处理Reference objects以及将引用入队列ReferenceQueues。 其中Ref Proc可以通过设置-XX:+ParallelRefProcEnabled启用并发。
分析后可以看到ygc的耗时主要受live object的影响,与当前具体的young gen的大小无关。
当然如果young gen比较小,对象分配的少,live object也相对会比较少,但是会影响ygc的频率。
live object的大小怎么看呢?
其中在ygc的日志最后会打印survivor的大小变化,例如Survivors: 65.0M->66.0M。
在ygc后,一般eden区会清空,eden以及survivor存活的对象都会copy到 to survivor里,查看最后survivor的大小就是live objects的大小。
live object的回收情况怎么看呢?
eden区的live object在ygc后会在下一次ygc的age 1里,survivor区的live object在下一次ygc里的age+1。例如
刚开始发生gc age 1: 27148352 bytes, 27148352 total
在下一次gc age 2: 23486112 bytes, 30075160 total
age 3: 23433744 bytes, 38232368 total
age 4: 22143864 bytes, 40173376 total
age 5: 18722216 bytes, 58582224 total
age 6: 18096752 bytes, 68430552 total
age 7: 18096368 bytes, 73527672 total
通过对比几次ygc发现,可以观看age的变化,观察survivor区回收的情况。看着随着gc次数的增加,survivor的回收效率逐渐下降了。
通过设置-XX:MaxTenuringThreshold=8,减少对象在survivor区存活的age,使其快速晋级到Tenred区,减少对象copy时间。
有个考虑,调整SurvivorRatio的比例,减少survivor的大小是不是也能满足要求?考虑到减少survivor的大小,可能会提高survivor exhaustion的概率,这里使用的是降低TenuringThreshold。
参考日志如下:
Desired survivor size 100663296 bytes, new threshold 8 (max 8)
- age 1: 10421288 bytes, 10421288 total
- age 2: 7052472 bytes, 17473760 total
- age 3: 20880216 bytes, 38353976 total
- age 4: 3790008 bytes, 42143984 total
- age 5: 75032 bytes, 42219016 total
- age 6: 70952 bytes, 42289968 total
- age 7: 1021760 bytes, 43311728 total
- age 8: 405704 bytes, 43717432 total
, 0.0762263 secs]
[Parallel Time: 52.8 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 149435454.4, Avg: 149435454.4, Max: 149435454.5, Diff: 0.1]
[Ext Root Scanning (ms): Min: 2.7, Avg: 2.9, Max: 3.2, Diff: 0.4, Sum: 11.8]
[Update RS (ms): Min: 12.7, Avg: 12.7, Max: 12.7, Diff: 0.0, Sum: 50.9]
[Processed Buffers: Min: 72, Avg: 82.2, Max: 96, Diff: 24, Sum: 329]
[Scan RS (ms): Min: 0.4, Avg: 0.4, Max: 0.4, Diff: 0.0, Sum: 1.7]
[Code Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.3]
[Object Copy (ms): Min: 36.2, Avg: 36.4, Max: 36.6, Diff: 0.4, Sum: 145.5]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[GC Worker Total (ms): Min: 52.6, Avg: 52.7, Max: 52.7, Diff: 0.1, Sum: 210.6]
[GC Worker End (ms): Min: 149435507.1, Avg: 149435507.1, Max: 149435507.1, Diff: 0.1]
[Code Root Fixup: 0.2 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.4 ms]
[Other: 22.8 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 19.5 ms]
[Ref Enq: 0.2 ms]
[Redirty Cards: 0.2 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.2 ms]
[Free CSet: 1.6 ms]
[Eden: 1471.0M(1471.0M)->0.0B(1470.0M) Survivors: 65.0M->66.0M Heap: 1795.5M(2560.0M)->319.5M(2560.0M)]
Heap after GC invocations=557 (full 0):
garbage-first heap total 2621440K, used 327149K [0x0000000720000000, 0x0000000720105000, 0x00000007c0000000)
region size 1024K, 66 young (67584K), 66 survivors (67584K)
Metaspace used 116198K, capacity 126309K, committed 126592K, reserved 1161216K
class space used 13548K, capacity 15194K, committed 15232K, reserved 1048576K
}
[Times: user=0.29 sys=0.00, real=0.08 secs]
https://product.hubspot.com/blog/g1gc-fundamentals-lessons-from-taming-garbage-collection
http://tech.meituan.com/g1.html
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
-EOF-
]]>一般的方案是通过HashMap这样的结构保存数据, 但是这样一般有两个问题,一是太浪费内存,java对象占用的空间太大。二是随着内存使用的增大,对于GC而已负担太大。所以我实现的方案是将大部分数据保存在非托管堆上,部分索引数据保存在托管堆上。
主要的Feature如下:
主要的实现细节如下:
记录格式如下::
+--------------+------------------+
| name | length |
+--------------+------------------+
| magic number | 1byte |
| next | 8byte |
| key size | varint |
| value size | varint |
| key | <data> |
| value | <data> |
| padding | <not used space> |
+--------------+------------------+
记录存放在在NonHeap分配的数据段,每个数据段为固定大小,可以在初始化的时候指定。
在DB被初始化的时候可以指定hashpower,其用于定于索引buckets的大小,根据预计的缓存量进行分配,如果太小,将导致冲突链增大,将极大的影响性能。
索引buckets为long类型的数组,buckets里的值保存记录在NonHeap数据区的偏移以及整个大小。前2个字节为大小,后2个字节为数据段的index, 最后4个字节为在数据段的偏移。
记录被删除后,将magic number置为删除状态,并按照其size有序的保存在分配在托管堆的TreeSet里。在下次分配记录空间的时候,先通过bestfit的方式查找到最适合的大小,复用原有的空间。
在碎片率到达一定程度后,将对数据段进行碎片整理,将多个碎片合并为一个碎片。
PS: 可以使用类似memcached slab分配器的方式分配内存,这样出现碎片的机率比较小。
项目地址在https://github.com/believe3301/NonHeapDB
-EOF-
]]>主要分为如下三个部分:
####指标收集
指标分为系统指标以及应用指标. 在linux系统指标收集的程序挺多的,中意collectd,主要是由于其支持java plugin以及write_graphite插件,这样通过java plugin写java代码监控java进程。而且数据可以直接发送到graphite。
应用指标的收集通过进程将指标喂给statsd,由其进行聚集之后发送到graphite,具体可以参考etsy写的文章。对于nginx或者apache这样的进程,可以通过etsy出品的logster进行分析处理,然后发送给graphite。
####指标存储以及展现
graphite是一个可伸缩的实时的绘图系统,主要分为3个部分。
graphite专注与绘图,实现的非常棒,使用其提供的graph function(timeShift),可以非常容易的实现指标的环比或者同比。 其提供的dashboard功能太简单,不过有很多第三方的实时的dashboard实现,我比较喜欢https://github.com/jondot/graphene。
####报警
报警可以使用nagios来完成,etsy提供了nagios插件用于分析graphite的数据进行报警。
具体可以查看https://github.com/etsy/nagios_tools/blob/master/check_graphite_data。
http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/
https://graphite.readthedocs.org/en/latest/
http://blog.dccmx.com/2012/12/build-perfect-monitoring-system/
-EOF-
]]>数据存储在计算机系统以及信息交互为0/1编码,必须通过某种编码方式将文字、符号转换为数字,然后通过特定的实现将其存储到计算机系统里。
ASCII
ASCII字符集为美国信息交互标准编码,通过7位编码拉丁字母,包含128个字符,无法显示重音字母。其国际标准为ISO 646。Ascii字符表可以通过man ascii显示
Latin-1
由于ASCII只使用了7位编码,Latin-1为8位编码,其中前128个编码与ASCII一致。其国际标准为ISO 8859-1。Latin-1字符表可以通过man ISO_8859-1显示, 其中A0-FF为西欧字符,80-9F为控制字符
Unicode
http://en.wikipedia.org/wiki/Unicode
Unicode基于通用字符集UCS(ISO 10646),其包含所有的其他字符集标准,而且保证与其它字符集的双向兼容。
其定义了1,114,112个码位,区间为0-0x10FFFF。UCS标准定义了一个31位的字符集架构,首先根据最高字节将其分了128(2^7)个24位的组,然后每个组根据次高字节分为256(2^8)个面。每个平面根据第3个字节分为256行 (row),每行有256个码位(cell)。
group 0的平面0被称作BMP(Basic Multilingual Plane),通过2个字节表示65536个字符,也称为UCS-2。 其中0x0000-0x00FF与Latin-1编码一致
UTF
Unicode只是一种编码方式,具体的实现方式称为Unicode Transformation Format。
UTF-8以字节为单位对Unicode进行编码。从Unicode到UTF-8的编码方式如下:
Unicode编码(16进制) UTF-8 字节流(二进制)
000000 - 00007F 0xxxxxxx
000080 - 0007FF 110xxxxx 10xxxxxx
000800 - 00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。
UTF-16编码以16位无符号整数为单位,假设其字符编码为U,则编码方式为
U<0x10000 U
U≥0x10000 U`=U-0x10000,转为20位的二进制,其中前10位加前缀组成110110,后10为加前缀110111
UTF-32编码使用32位进行编码,无需进行转换
字节序
Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符”零宽无中断空格”。这个字符的编码是FEFF。对于UTF-8,其使用EF BB BF来标识编码
java编译后的class文件通过UTF-16编码,加载到jvm中也是通过UTF-16编码。对于从数据库以及文件读取的字节流而言,可能为了存储优化,编码方式不同,需要根据源地址的编码进行转换
可以通过file
例如: $ echo 中国 > uni.txt
$ file uni.txt
uni.txt: UTF-8 Unicode text
$ hexdump uni.txt
0000000 b8e4 e5ad bd9b 000a
0000007
$ unicode 中国
U+4E2D CJK UNIFIED IDEOGRAPH-4E2D
UTF-8: e4 b8 ad UTF-16BE: 4e2d Decimal: 中
中 (中)
Uppercase: U+4E2D
Category: Lo (Letter, Other)
Bidi: L (Left-to-Right)
U+56FD CJK UNIFIED IDEOGRAPH-56FD
UTF-8: e5 9b bd UTF-16BE: 56fd Decimal: 国
国 (国)
Uppercase: U+56FD
Category: Lo (Letter, Other)
Bidi: L (Left-to-Right)
$iconv -f UTF-8 -t UTF-16 uni.txt > uni16.txt
$hexdump -C uni16.txt
00000000 ff fe 2d 4e fd 56 0a 00 |..-N.V..|
00000008
$file uni16.txt
uni16.txt: Little-endian UTF-16 Unicode text, with no line terminators
通过vim打开uni16.txt,使用命令set fileencoding=utf-8,然后保存
$file uni16.txt
uni16.txt: UTF-8 Unicode (with BOM) text
$ hexdump -C uni16.txt
00000000 ef bb bf e4 b8 ad e5 9b bd 0a |..........|
0000000a
注释为 #,如果是中文注释,在第一行添加#coding=utf-8
或者通过
# -*- coding:
encoding注释必须在第一行或者第二行
python默认情况下认为源码的编码为latin-1
http://www.python.org/dev/peps/pep-3120/ http://www.python.org/dev/peps/pep-0263/
str和unicode都是basestring的子类,str为通过utf-8编码后的字符序列,unicode是通过utf-16编码后的字符序列。
>>> s='中国'
>>> u=u'中国'
>>> print repr(s)
'\xe4\xb8\xad\xe5\x9b\xbd'
>>> print repr(u)
u'\u4e2d\u56fd'
>>> print len(s)
6
>>> print len(u)
2
string可以和unicode互相转换.默认编码可以sys.setdefaultencoding进行设置
sd=s.decode('utf-8')
>>> type(sd)
<type 'unicode'>
>>> print repr(sd)
u'\u4e2d\u56fd'
>>> us=unicode(s,encoding='utf-8')
>>> print repr(us)
u'\u4e2d\u56fd'
>>> su=us.encode('utf-8')
>>> print repr(su)
'\xe4\xb8\xad\xe5\x9b\xbd'
unicode的方法实参可以为unicode类型或者str类型,不过str类型会通过默认的codec转换为unicode类型 通过unichr方法可以将10进制的码位与unicode进行转换,通过ord方法可以将unicode转为10进制的码位
对于文件读写,可以使用codecs提供的open方法打开,其会返回一个wrapped file对象,读取返回的为unicode对象。写入时,如果参数是unicode,则使用open()时指定的编码进行编码后写入;如果是str,则先根据源代码文件声明的字符编码,解码成unicode后再进行前述操作。 对于字符串format,如果format string为unicode则返回的也为unicode类型
http://docs.python.org/2/howto/unicode.html
http://blog.csdn.net/fmddlmyy/article/details/372148
http://hektor.umcs.lublin.pl/~mikosmul/computing/articles/linux-unicode.html
-EOF-
]]>find src/ -name "*.[c|h]" |xargs wc -l
37216 total
http://en.wikipedia.org/wiki/Binary-safe
二进制安全是一个针对字符串处理函数的计算机编程术语,一个二进制安全函数的最基本要求就是它把输入完全当作未处理的无任何格式要求的数据流。 假定8个比特位的字符,那么二进制安全函数就必须能够处理所有可能的256个值。
http://redis.io/topics/protocol
请求以及应答都是通过\r\n结束
请求协议:
*<参数数量> CRLF $<第一个参数字节数> CRLF <参数数据> CRLF ... $<第N个参数字节数> CRLF <参数数据> CRLF
应答协议:
旧的协议:
key不是二进制安全的,协议中通过空白字符以及回车换号符号进行分隔,key为不包含空格或者\n符号的字符串
启动的时候可以指定配置文件,配置文件支持include指令。 配置可以热更新,可以通过CONFIG GET/SET指定进行配置项的获取以及更新
启动参数:
启动:
http://en.wikipedia.org/wiki//dev/random
####hashtable
特性:1)支持自动expand 2)支持迭代
使用结构dict表示,其中对于两个dictht,主要用于在expand的0/1切换。使用dictType抽象出不同类型的hashtable类型。dictht是只要的hashtable数据结构,使用链地址法解决冲突,每个项存储在dictEntry结构里。
dictAdd:
rehash:
只有在dict_can_resize以及elements/buckets >5的时候才开始扩容,在add/find/delete 以及iterator为0 的时候都会进行rehash. 如果配置文件里配置了activerehashing,则会进行增量rehash 1ms.
iterator:
通过dictGetIterator获取迭代器,然后通过dictNext获取下一个元素。通过dictGetSafeIterator获取的迭代其保证在迭代的过程中不进行rehash.
hash function:
key为sds,使用hash函数为MurmurHash2
####sds (string datastruct)
sds结构其实就是char *,字符串函数都可以使用。只是在分配字符串前添加一个字符串前缀记录其使用的大小(len)以及空闲的大小(free),则其整个结构的大小为使用的空间+空闲的空间+8
…
http://redis.io/topics/internals-rediseventlib
Eventloop支持不同的平台,对于linux使用的是epoll实现。只要支持两种事件(File以及Timer)
实现的比较简单,注意的是其timer通过链表实现,对于redis还好,其只使用了一个timer,所有的异步操作都在这个timer里完成。
可以设置beforesleep,在每次处理时间之前进行的操作。redis在1s内调用REDIS_HZ次serverCron方法,serverCron处理很多异步操作。时间控制通过REDIS_HZ配置
https://github.com/antirez/redis/commit/94343492361a04301a48fc56490d6113ff97aba9
accept
redis对于通过acceptTcpHandler
或者acceptUnixHandler
处理客户端连接事件,对于获取每个连接建立一个redisClient
对象,其通过readQueryFromClient
读取数据
REDIS_IOBUF_LEN
(1024 *16)的数据到querybuf,然后调用processInputBuffer处理数据。reply
通过addReply相关方法发送应答给client。通过client-output-buffer-limit可以限制output buffer的大小
http://antirez.com/post/redis-as-LRU-cache.html
通过maxmemory设置最大使用的内存,通过maxmemory-policy设置踢数据的策略,支持
redis的lru以及ttl算法不是精确,是通过取样判断需要踢除的数据,可以通过maxmemory-samples设置取样的大小。
在processCommand
里,如果设置了maxmory,则通过freeMemoryIfNeeded释放可以释放的内存
time
为了减少time调用,对于时间准确性不高的地方通过server.unixtime获取系统时间,其在serverCron每10ms更新一次
redis为了节省内存,robj的lru通过22位保存,精度通过REDIS_LRU_CLOCK_RESOLUTION控制,默认为10s.在serverCron通过updateLRUClock进行更新。
void updateLRUClock(void) {
server.lruclock = (server.unixtime/REDIS_LRU_CLOCK_RESOLUTION) &
REDIS_LRU_CLOCK_MAX;
}
发布者发布消息到指定的channel,不关注订阅者。订阅者订阅channel,并接收数据。
订阅者进入SUB模式后,只能使用(P)SUBSCRIBE / (P)UNSUBSCRIBE /QUIT命令,而且支持pattern-model (glob)的订阅方式。注意:如果pattern sub符合相同的channel多次,则该channel上的消息会接收多次。
glob可以参考:
http://en.wikipedia.org/wiki/Glob_(programming)
publish返回接收的client的数量,subscribe通过multi-bulk reply返回,格式为
对于client端redisClient 通过dict pubsub_channel保存订阅的channel,通过list pubsub_pattern保存订阅的pattern模式
对于server端redisServer通过dict pubsub_channel保存订阅的client,通过list pubsub_pattern保存订阅的pattern模式
subscribe
通过pubsubSubscribeChannel处理subscribe命令
1、迭代client发送的channel,将其加入到client的pubsub_channel里。将client添加到server的pubsub_channel里对应channel的client链表上. 复杂度为O(1)
unsubscribe
如果无参数则调用pubsubUnsubscribeAllChannels取消所有的订阅,否则迭代调用pubsubUnsubscribeChannel取消指定的订阅。过程与subscribe类似,先删除client pubsub_channel的值,然后删除server pubsub_channel指定channel 对于的client链表上的值. 复杂度为O(n),对于Channel的client的数量
psubscribe
通过pubsubSubscribePattern处理psubscribe命令
1、通过pubsubPattern保存pattern以及其对于的client,添加到server pubsub_patterns链表
publish
通过pubsubPublishMessage处理publish命令
punsubscribe 与unsubscribe类似
http://redis.io/topics/transactions
https://groups.google.com/forum/?fromgroups=#!topic/redis-db/a4zK2k1Lefo
简单的事务,保证一个client发起的命令可以连续执行,中间不会插入其他client的命令。而且能保证命令要么全部执行要么全部回滚。
注意:事务只能保证执行的时候顺序执行,而不能保证中间的某个命令执行错误而回滚。
使用multi命令进入transaction上下文,所有的command进入队列,使用discard放弃所有入队的command
通过Watch命令实现了类似CAS的乐观锁。如果Watch的key在事务执行前被修改了则整个事务被回滚,Exec返回nil reply(*-1)。当事务结束后所有被Watch的key会被UnWatch.
redis script执行的都是事务性的,可以使用script处理事务
http://redis.io/topics/persistence
http://oldblog.antirez.com/post/redis-persistence-demystified.html
http://code.google.com/p/redis/issues/detail?id=150
一般的write可以简化为下面几个步骤:
数据恢复可以使用如下几种方式:
####Snapshot
https://github.com/sripathikrishnan/redis-rdb-tools/wiki/Redis-RDB-Dump-File-Format
数据库某一时刻的镜像,根据save point将其dump到磁盘。某一段时间的数据可能丢失,而且不能增量dump。
可以通过bgsave以及save命令启动Snapshot。
相关配置:
rdb filename: rdb保存的文件名
rdb compression: 是否启用LZF压缩
rdb checksum: 是否使用checksum
相关流程:
迭代dict里所有的key,调用rdbSaveKeyValuePair存储kv(expire time,type,key,value)
[REDIS_RDB_OPCODE_EXPIRETIME_MS 252][ExpireTime 8bytes][Value Type][Key][Value]
其中存储数字以及OpCode调用rdbSaveLen进行存储:
1、存储长度与ziplist类似,根据MSB的不同编码,不同数量bit存储
00|000000 表示6位存储长度
01|000000 00000000 表示14位存储长度
10|000000 [32bit] 表示32位存储长度,数据需要转换为big endian存储
11|000000 后6位指定编码方式
[Key]:
int: [encoding type 1bytes][data]
string: [string len][data]
其中保存key的时候,如果小于INTMAX,数字类型按照区间设置不同的编码(8/16/32位),否则按照字符串进行编码
如果字符串长度>20并启动压缩则LZF压缩后存储
[Value]: (rdbSaveObject)
List/Set/Hash: 如果为Encoded类型(例如ziplist,intset等,具体可以参考DataType)则直接保存原始数据,否则迭代保存
Example:
保存key1,value1通过save命令保存,然后通过
hexdump -C dump.rdb 显示如下:
00000000 52 45 44 49 53 30 30 30 36 fe 00 00 04 6b 65 79 |REDIS0006....key|
00000010 31 06 76 61 6c 75 65 31 ff 38 c9 db e7 ad 48 42 |1.value1.8....HB|
00000020 2c |,|
总大小为33个字节
通过dump命令可以导出指定key的值,其记录格式与rdb类似
####AOF
记录所有更改数据库记录的命令,记录方式和客户端发送的类似,可能会改变命令,例如incr变为set。随着AOF的增加,redis通过rewrite缩小AOF的大小,rewrite fork新进程根据内存里的数据建立临时的aof文件。
可以通过bgrewriteaof重建AOF,serverCron调用rewriteAppendOnlyFileBackground()方法执行rewrite。
启动的时候通过bioInit启动后台线程,使用bio处理文件删除,unlink/aof操作可能会阻塞当前进程,开启线程处理job queue,现在提供两种job (REDIS_BIO_CLOSE_FILE
以及REDIS_BIO_AOF_FSYNC
)(libeio提供异步接口)
控制命令:
config appendonly no|yes
bgrewriteaof
feed
与replication类似,在执行command的时候调用call(),然后调用feedAppendOnlyFile将命令添加到aof_buf.
在rewrite的时候,也需要将其加入到aof_rewrite_buf.
aof_rewrite_buf与aof_buf的实现不同,rewrite buf每次分配一定大小(10M)的block,通过链表连接起来,而不是通过append buf来实现。其由于耗时比较长,realloc的开销比较大。
flush
rewrite
load
slowlog
记录慢日志,可以通过slowlog-log-slower-than以及slowlog-max-len配置slowlog的执行时间的下限以及记录的数量
slowlog get <num> (返回指定条数的slowlog,包含unique id/log time/execute time/command args)
slowlog len (获取当前slowlog的数量)
slowlog reset (清空slowlog)
支持logfile以及syslog
http://redis.io/topics/latency
可以通过Sofeware watchdog 分析延迟,主要是通过SIGALARM信号进行处理,timer超期后通过ucontext打印backtrace
http://redis.io/topics/data-types
DB默认为16个,通过redisDB定义
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id;
} redisDb;
dict为keyspace,key为sds string, value为redis object。dictType为dbDictType。所有的数据都保存在dict里. expires为有过期时间的keyspace
typedef struct redisObject {
unsigned type:4; (数据类型,
unsigned notused:2; /* Not used */
unsigned encoding:4; (编码方式,ENCODING_RAW|INT
unsigned lru:22; /* lru time (relative to server.lruclock) */ 精度为分钟级别
int refcount; (引用计数)
void *ptr; (引用的数据)
} robj;
redis对象用于保存string/list/set的值,sharedObjectsStruct 用于共享的redis对象,减少内存消耗以及加速读取
注意的是:
http://redis.io/topics/memory-optimization
通过object命令可以查看robj的状态,例如编码方式,其有raw/int/hashtable/linkedlist/ziplist/intset/skiplist
####Ziplist
使用字符串表示双向链表,可以用于存储string以及integer,内存使用效率更高
整体结构
<zlbytes><zltail><zlen><entry><entry><zlend>
其中integer通过little endian存储
Entry结构
[previous entry length][encode and type][data]
prevlen 的存储 如果 prevlen 小于 ZIP_BIGLEN(254),则用一个字节来保存; 否则需要 5 个字节来保存,第 1 个字节存 ZIP_BIGLEN,作为标识符.
encode and type(length):
前两个位区分为string还是int,11为int类型,其余为string类型
string根据不同的长度占用不同的字节数
00pppppp (占用一个字节,长度为6位)
01pppppp|qqqqqqqq (占用两个字节,长度为14位)
10----------|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt (占用5个字节,长度为32位)
int类型都只占用一个字节,可以编码为不同的不同的长度(2、4、8、3、1字节,以及0000-1101 4位符号,用于表示0-12)
Function
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where);
在ziplist的尾端或头部添加一个结点
zl是ziplist的指针
s是待添加结点的值
slen是待添加结点的值长度
返回最新的ziplist的指针
每次操作都需要realloc大小,对于数据量太大就不太适合了,相比adlist主要节省在:
####Set
无序集合列表,通过sinterGenericCommand可以获取集合的交集
intset
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
设计比较简单,对于int类型进行定长升序存储,encoding有int16/32/64三种,读取的时候根据encoding设定步长。数字都是小端存储
intsetadd
1、根据插入的值判断encoding,如果大于当前encoding则需要调用intsetUpgradeAndAdd进行升级。否则判断是否已经存在,存在则返回,否则将数据按照升序插入到合适的位置。
根据encoding resize当前的容量大小,然后从后到前的重新设置已经插入的值到当前encoding.
####Zset 有序集合列表,按照指定的score排序。如果通过skiplist编码,则通过一个dict保持element到score的映射以及通过一个skiplist保存score到element的映射
skiplist
http://www.dirinfo.unsl.edu.ar/eda/EsDaAl/teorias/skip2-11.pdf
http://blog.nosqlfan.com/html/3041.html
http://rdc.taobao.com/blog/cs/?p=1327
特性:
key X如果存在level i,则存在level k (k < i)
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Redis的HA有如下几个特性:
对于slave而言,通过server.repl_state表示Replication的状态
对于master而言,每个slave的状态存储在redisClient.replstate里,每个slave对于master而言为一个client
master更新流程
redis执行command的时候,调用call(),根据flag参数判断是否需要propagate到AOF以及replication link, 根据cmd flags以及是否修改数据为参数调用propogate -> replicationFeedSlaves传递数据到slave
call flag:
#define REDIS_CALL_NONE 0
#define REDIS_CALL_SLOWLOG 1
#define REDIS_CALL_STATS 2
#define REDIS_CALL_PROPAGATE 4
#define REDIS_CALL_FULL (REDIS_CALL_SLOWLOG | REDIS_CALL_STATS | REDIS_CALL_PROPAGATE)
propagate flag:
#define REDIS_PROPAGATE_NONE 0
#define REDIS_PROPAGATE_AOF 1
#define REDIS_PROPAGATE_REPL 2
slave更新流程
启动的时候server.repl_state为REDIS_REPL_CONNECT状态,在replicationcron的时候调用connectWithMaster() (REDIS_REPL_CONNECT -> REDIS_REPL_CONNECTING)
连接成功后调用syncWithMaster,如果状态为CONNECTING 阻塞发送PING到master,检测master能正常工作 (REDIS_REPL_CONNECTING -> REDIS_REPL_RECEIVE_PONG)
发送PING后,阻塞读取PONG。如果需要验证发送验证信息。发送REPLCONF listening-port给master。
发送sync命令给master。创建临时文件用于存储发送过来的rdb文件信息。(以上的send/read都为blocking)
添加readSyncBulkPayload到Eventloop (REDIS_REPL_RECEIVE_PONG-> REDIS_REPL_TRANSFER)
readSyncBulkPayload首先同步读取发送的rdb size。然后读取发送过来的数据,同时调用sync_file_range,防止传输结束后fsync导致的延迟。
传输结束后调用rdbLoad加载rdb文件,创建Redisclient master 并重新rewrite AOF (REDIS_REPL_TRANSFER -> REDIS_REPL_CONEECTED)
ReplicationCron
在serverCron里,每1000ms调用replicationCron对master进行重连以及检测传输失败
slave:
mater:
通过slaveof命令可以停止replication,转为master.通过sync发送同步请求给master
lua是一个短小精悍的脚本语言,很多服务器端程序嵌入其作为扩展,nginx lua模块就很强悍。
命令格式为
eval <script> <keynum> <keys> <args>
其中keynum为keys的数量,在lua里可以通过KEYS全局变量进行获取,args为除keys以为所有的参数可以通过ARGV全局变量进行获取。
其中lua脚本与redis通过redis.call以及redis.pcall进行交互,两者之间的区别就是对于错误的不同处理,call命令将错误抛出给调用者,不会继续向下执行。而pcall捕获错误然后继续执行,执行结束后将error通过table返回
lua脚本可以通过return语句返回值,返回值通过redis协议返回给客户端。经测试可以返回integer/string/list,对于table不返回。而且对于多重返回值,只返回第一个值
协议转换
通过redisProtocolToLuaType进行redis到lua协议的转换,通过luaReplyToRedisReply进行lua到redis的协议转换
lua numer <-> redis integer reply
lua string <-> redis bulk reply
lua table(array) <-> redis mult bulk reply
lua table.ok <-> redis status reply
lua table.err <-> redis error reply
lua boolean false <-> redis nil reply
lua boolean true -> redis integer reply 1
注意
对于float类型返回int,返回的table如果包含nil则其后的值不会被返回 对于replication以及AOF存储的是执行的脚本而不是命令,需要保证在不同的时间相同的输入其输出是相同(例如脚本里使用时间、随机数或者外部状态进行求值),对此redis对于lua脚本的执行的命令进行了限制,而且redis不容许建立全局变量
使用eval命令需要每次都传递script body,可以通过evalsha命令节省带宽,传递script body的sha1摘要,如果服务器端不存在则返回特定错误。
对于最大执行时间可以通过lua-time-limit进行限制,如果超过限制,则可以接收客户端发送script kill命令终止脚本执行
redis嵌入lua
启动的时候调用scriptingInit初始化环境
lua访问redis
通过luaRedisGenericCommand执行redis.call/pcall的功能
eval/evalsha
通过evalGenericCommand处理命令
不考虑Replication、AOF、Slowlog、Transcation、Blocking等消耗的资源, 其中几个关键对象的大小为
sizeof(redisClient) = 16648
sizeof(sds) = 8
sizeof(robj) = 16
sizeof(dictEntry) = 24
Connection
每条连接需要建立一个redisClient对象,其中主要是redisClient->buf为16k数据。redisClient->querybuf,初始通过sdsMakeRoomFor分配32k的大小。
如果对于10k活跃连接,其分配的为(16k + 32k)* 10k = 480M
Hash
假设key为16个字符,value为32个字符,通过set命令进行存储.
key的存储为sizeof(sds) = 8 + 16 = 24个字节。
value的存储为sizeof(robj) = 16 + 32 = 48个字节。
由于都是存储在dict里,则每个节点的大小为sizeof(dictEntry) = 24 + 24(key) + 48(value) = 96个字节
则对于1,000k的数据,其存储的大小为 96 * 1000k = 96M,利用率为48/96 = 0.5
http://redis.io/topics/benchmarks
http://redis.io/topics/twitter-clone http://coolshell.cn/articles/7270.html http://blog.codingnow.com/2011/11/dev_note_2.html
对于redis建模需要将关系型数据库里的二维的数据变为一维的,而且需要自己维护索引。
对于大批量的数据,如果内存比较敏感,可以对数据进行分段,然后使用hash结构代替kv存储
http://instagram-engineering.tumblr.com/post/12202313862/storing-hundreds-of-millions-of-simple-key-value-pairs
twitter clone:
table user
global:nextUserId 用于获取userid
user:<userid>:name 指定userid的用户名
user:<userid>:email 指定userid的email信息
如果需要通过name获取id,则可以添加
user:<name>:id 获取指定name的userid
table user-followers (1:n)对于1对n的关系可以通过set来保存(无序)或者list(有序)
user:<userid>:followers ->(set of followers)
user:<userid>:following ->(set of following)
table user-posts
user:<userid>:post ->(list of posts,可以使用lrange进行分页操作)
http://oldblog.antirez.com/post/short-term-redis-plans.html
http://oldblog.antirez.com/post/redis-sentinel-beta-released.html
http://www.antirez.com/news/45
监控
https://github.com/kumarnitin/RedisLive
https://github.com/steelThread/redmon
http://www.percona.com/doc/percona-monitoring-plugins/cacti/redis-templates.html
Partition
http://redis.io/topics/partitioning
应用
运维
….
实例分析
假设一个user有uid,mobileno以及email信息,现在需要通过uid查询到mobileno以及email,数据量在1亿条左右,update/select的比重基本为1:5. 需要可靠性高以及延时低.
方案1
可以通过hset进行存储,例如
hset <uid> m <mobileno>
hset <uid> e <email>
假设每个uid平均占用9个字符,email平均占用30个字节,则使用的空间为
ZIPLIST_HEADER_SIZE = 10
filed m : <pre> 1byte + <len> 1byte + <value> 1byte = 3
filed value: <pre> 1byte + <len> 1byte + <mobile> 8byte = 10
field e : 3
field email: <pre> 1byte + <len> 1byte +<email> 30 = 32
tail:1
(9+4) * 100,000k + (16 + 24 + 59) * 100,000k = 11200M内存
如果对于email的后缀进行映射,应该可以使占用的空间更小,单机可以容纳,通过HA方式进行处理,可以使用keepalived进行主从切换
方案2
可以通过set进行存储,例如
set u:<uid>:m <mobileno>
set u:<uid>:e <email>
这样占用的空间为(13+ 4) * 2 * 100,000k + 40 * 100,000k + 70 * 100,000k = 14400M内存
方案3
可以通过类似的将uid进行分段,然后通过hset进行储存,其中mobileno以及email可以储存在不同的db
-EOF-
]]>主要使用sysstat包里的系列软件,用于观察服务器的CPU、内存、网络等使用情况。
ubuntu下通过如下命令进行安装
sudo apt-get install sysstat
####CPU监控
通过mpstat可以方便监控多核系统下的CPU使用率以及处理的中断数量。
mpstat是Multiprocessor Statistics的缩写,是实时系统监控工具。
其报告与CPU的一些统计信息,这些信息存放在/proc/stat文件中。
在多CPUs系统里,其不但能查看所有CPU的平均状况信息,而且能够查看特定CPU的信息。
mpstat 语法如下
Usage: mpstat [ options ] [ <interval> [ <count> ] ]
Options are:
[ -A ] [ -I { SUM | CPU | SCPU | ALL } ] [ -u ]
[ -P { <cpu> [,...] | ON | ALL } ] [ -V ]
-P {<cpu> [,…] | ON | ALL}
表示监控哪个CPU, cpu在[0,cpu个数-1]中取值, ALL表示监控所有cpu。
-I {SUM | CPU | SCPU | ALL }
报告中断统计信息,SUM显示总的中断数量,CPU显示每个cpu处理的每个中断数量, SCPU显示每个cpu处理的软中断的数量。
internal
相邻的两次采样的间隔时间,以s为单位
count
采样的次数,如果不写采样次数,则一直进行采样。
当没有参数时,mpstat则显示系统启动以后所有信息的平均值。
有interval时,第一行的信息自系统启动以来的平均信息。从第二行开始,输出为前一个interval时间段的平均信息。
一般用法如下
mpstat -P ALL 1
mpstat -I SUM 1
在旧版的mpstat里无-I选项,会将intr/s作为-P选项的一列显示。
####网卡监控
查看网卡信息
lspci -vvv
搜索Ethernet查看网卡型号、驱动、能力等相关信息。
通过ethtool查看网卡的信息以及修改网卡配置
$ ethtool eth0
Settings for eth0:
Supported ports: [ TP ]
Supported link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Half 1000baseT/Full
Supported pause frame use: No
Supports auto-negotiation: Yes
Advertised link modes: 10baseT/Half 10baseT/Full
100baseT/Half 100baseT/Full
1000baseT/Half 1000baseT/Full
Advertised pause frame use: Symmetric
Advertised auto-negotiation: Yes
Speed: 100Mb/s
Duplex: Full
Port: Twisted Pair
PHYAD: 1
Transceiver: internal
Auto-negotiation: on
MDI-X: Unknown
Supports Wake-on: g
Wake-on: g
Current message level: 0x000000ff (255)
drv probe link timer ifdown ifup rx_err tx_err
Link detected: yes
上面给出的例子说明网卡有 10baseT,100baseT 和 1000baseT 三种选择,目前正自适应为 100baseT(Speed: 100Mb/s)
查看Ring buffer的大小
$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 511
RX Mini: 0
RX Jumbo: 0
TX: 511
Current hardware settings:
RX: 200
RX Mini: 0
RX Jumbo: 0
TX: 511
设置Ring buffer的大小
$ sudo ethtool -G eth0 rx 400
查看网卡统计信息
$ sudo ethtool -S eth0
NIC statistics:
rx_octets: 277931
rx_fragments: 0
rx_ucast_packets: 1750
rx_mcast_packets: 30
rx_bcast_packets: 975
rx_fcs_errors: 0
rx_align_errors: 0
rx_xon_pause_rcvd: 0
rx_xoff_pause_rcvd: 0
rx_mac_ctrl_rcvd: 0
rx_xoff_entered: 0
rx_frame_too_long_errors: 0
rx_jabbers: 0
rx_undersize_packets: 0
rx_in_length_errors: 0
rx_out_length_errors: 0
rx_64_or_less_octet_packets: 0
rx_65_to_127_octet_packets: 0
rx_128_to_255_octet_packets: 0
rx_256_to_511_octet_packets: 0
rx_512_to_1023_octet_packets: 0
rx_1024_to_1522_octet_packets: 0
rx_1523_to_2047_octet_packets: 0
rx_2048_to_4095_octet_packets: 0
rx_4096_to_8191_octet_packets: 0
rx_8192_to_9022_octet_packets: 0
tx_octets: 290648
tx_collisions: 0
tx_xon_sent: 0
tx_xoff_sent: 0
tx_flow_control: 0
tx_mac_errors: 0
tx_single_collisions: 0
tx_mult_collisions: 0
tx_deferred: 0
tx_excessive_collisions: 0
tx_late_collisions: 0
tx_collide_2times: 0
tx_collide_3times: 0
tx_collide_4times: 0
tx_collide_5times: 0
tx_collide_6times: 0
tx_collide_7times: 0
tx_collide_8times: 0
tx_collide_9times: 0
tx_collide_10times: 0
tx_collide_11times: 0
tx_collide_12times: 0
tx_collide_13times: 0
tx_collide_14times: 0
tx_collide_15times: 0
tx_ucast_packets: 1876
tx_mcast_packets: 0
tx_bcast_packets: 0
tx_carrier_sense_errors: 0
tx_discards: 0
tx_errors: 0
dma_writeq_full: 0
dma_write_prioq_full: 0
rxbds_empty: 0
rx_discards: 0
rx_errors: 0
rx_threshold_hit: 0
dma_readq_full: 0
dma_read_prioq_full: 0
tx_comp_queue_full: 0
ring_set_send_prod_index: 0
ring_status_update: 0
nic_irqs: 0
nic_avoided_irqs: 0
nic_tx_threshold_hit: 0
mbuf_lwm_thresh_hit: 0
结果如上,可以查看各种类型包的数量以及丢包量等相关数据。
通过sysstat的sar命令查看网卡流量等相关信息。
sar 的命令如下
sar [options] [ <interval> [ <count> ]]
主要用其中的-n选项监控网络子系统相关的数据。
-n { keyword [,...] | ALL }
keyword为DEV可以报告网络的流量相关信息,EDEV可以报告错误包的相关信息。
$ sar -n DEV 1
Linux 3.2.0-23-generic (ubuntu) 05/30/2012 _x86_64_ (4 CPU)
04:20:34 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
04:20:35 PM lo 2.00 2.00 4.73 4.73 0.00 0.00 0.00
04:20:35 PM eth0 9.00 3.00 5.33 0.25 0.00 0.00 0.00
04:20:35 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s
04:20:36 PM lo 2.00 2.00 4.63 4.63 0.00 0.00 0.00
04:20:36 PM eth0 9.00 3.00 5.55 0.22 0.00 0.00 0.00
$ sar -n EDEV 1
Linux 3.2.0-23-generic (ubuntu) 05/30/2012 _x86_64_ (4 CPU)
04:23:48 PM IFACE rxerr/s txerr/s coll/s rxdrop/s txdrop/s txcarr/s rxfram/s rxfifo/s txfifo/s
04:23:49 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
04:23:49 PM eth0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
04:23:49 PM IFACE rxerr/s txerr/s coll/s rxdrop/s txdrop/s txcarr/s rxfram/s rxfifo/s txfifo/s
04:23:50 PM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
04:23:50 PM eth0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
主要对于网卡中断负载以及tcp协议栈进行调优。
####最大句柄数
linux限制了每个进程能开的最大句柄数,默认不进行修改的话为1024。可以通过ulimit进行参看以及设置。
$ ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 30006
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 30006
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
$ ulimit -n
1024
$ ulimit -n 32768
可以通过参看/proc/
$ cat /proc/11020/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size unlimited unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 30006 30006 processes
Max open files 1024 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 30006 30006 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
####软中断分布不均
对于网络程序,容易导致网络中断分布不均导致cpu某个核被压爆,通过mpstat可以进行监控。
对于内核版本为2.6.35的系统,默认已经安装RPS以及RFS补丁,可以参考如下文档进行设置。
http://code.google.com/p/kernel/wiki/NetScalingGuide
对于内核版本为2.6.35以下的系统,如果是多队列的网卡可以进行如下设置。
参看中断号
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 1652 0 0 0 IO-APIC-edge timer
1: 870400 0 0 0 IO-APIC-edge i8042
8: 1 0 0 0 IO-APIC-edge rtc0
9: 194 0 0 0 IO-APIC-fasteoi acpi
12: 4429616 0 0 0 IO-APIC-edge i8042
16: 134965 0 0 0 IO-APIC-fasteoi ehci_hcd:usb1, mmc0
23: 1546310 0 292374 0 IO-APIC-fasteoi ehci_hcd:usb2
40: 236364 0 0 0 PCI-MSI-edge ahci
41: 17 0 0 0 PCI-MSI-edge mei
42: 142 0 0 0 PCI-MSI-edge snd_hda_intel
43: 19547644 0 0 0 PCI-MSI-edge i915@pci:0000:00:02.0
44: 1638700 0 0 0 PCI-MSI-edge eth0-0
45: 1779727 0 0 0 PCI-MSI-edge eth0-1
46: 725208 0 0 0 PCI-MSI-edge eth0-2
47: 894102 0 0 50434 PCI-MSI-edge eth0-3
48: 493319 106065 0 0 PCI-MSI-edge eth0-4
如上所示,可以看到网络接口eth0分配了44-48的5个中断号。
设置中断亲和,将队列绑定到不同的CPU上。
计算中断亲和可以参考http://www.vpsee.com/2010/07/smp-irq-affinity/
$ echo 2 > /proc/irq/44/smp_affinity
$ echo 4 > /proc/irq/45/smp_affinity
...
...
设置完后通过mpstat监控是否分布均匀了。
-EOF-
]]>对于dns相关有如下几个关键点:
dns协议相对比较简单,这里就不介绍,主要介绍RR的格式
[domain] [ttl] IN [RR type] [[RR data]]
常见的正解文件 RR 相关信息
[domain] IN [[RR type] [RR data]]
主机名. IN A IPv4 的 IP 地址
主机名. IN AAAA IPv6 的 IP 地址
域名. IN NS 管理这个域名的服务器主机名字.
域名. IN SOA 管理这个域名的七个重要参数(容后说明)
域名. IN MX 邮件交换记录,顺序数字 接收邮件的服务器主机名字
主机别名. IN CNAME 规范名称,实际代表这个主机别名的主机名字.
主要的标志为A以及NS,A记录主机名称对应的ip地址,NS记录域名对应的域名服务器
dns的测试工具一般有host、nslookup、dig,建议使用dig进行测试
dig www.baidu.com
; <<>> DiG 9.8.1-P1 <<>> www.baidu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48376
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;www.baidu.com. IN A
;; ANSWER SECTION:
www.baidu.com. 19 IN CNAME www.a.shifen.com.
www.a.shifen.com. 62 IN A 61.135.169.105
www.a.shifen.com. 62 IN A 61.135.169.125
;; Query time: 0 msec
;; SERVER: 10.0.1.90#53(10.0.1.90)
;; WHEN: Tue Aug 7 10:52:01 2012
;; MSG SIZE rcvd: 90
简单分析下输出,前缀没有;为应答的数据,第一行为dig的版本号,第二行为dig的全局选项,第4、5行为dns应答头的解析。
QUESTION SECTION 为请求的格式,ANSWER SECTION 为应答,最后为请求的相关统计数据。
默认查询的资源类型为A,就是查询主机名称对应的ip地址,可以通过-t选项指定查询的资源类型,通过@指定使用dns服务器。
dig -t ANY www.baidu.com @8.8.8.8
; <<>> DiG 9.8.1-P1 <<>> -t ANY www.baidu.com @8.8.8.8
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20947
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 4, ADDITIONAL: 4
;; QUESTION SECTION:
;www.baidu.com. IN ANY
;; ANSWER SECTION:
www.baidu.com. 744 IN CNAME www.a.shifen.com.
;; AUTHORITY SECTION:
baidu.com. 54639 IN NS dns.baidu.com.
baidu.com. 54639 IN NS ns4.baidu.com.
baidu.com. 54639 IN NS ns3.baidu.com.
baidu.com. 54639 IN NS ns2.baidu.com.
;; ADDITIONAL SECTION:
dns.baidu.com. 61193 IN A 202.108.22.220
ns2.baidu.com. 58797 IN A 61.135.165.235
ns3.baidu.com. 61195 IN A 220.181.37.10
ns4.baidu.com. 67812 IN A 220.181.38.10
;; Query time: 1 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Tue Aug 7 11:03:02 2012
;; MSG SIZE rcvd: 194
其中AUTHORITY SECTION为授权信息,dns服务器信息。ADDITIONAL SECTION 为额外信息。
可以通过dig -x 反向查询,通过ip地址查询主机名称。
-EOF-
]]>周一来了后进行问题的排查,首先检查服务器端,服务器端运行正常。然后在其他机器通过zk客户端连接,发觉特别的慢。通过tcpdump抓包发现启动客户端到发送sync包建立连接耗费了10s左右的时间。 决定通过strace查看在connect系统调用之前有啥系统调用特别耗时间。
strace -f -t -econnect -o2.log ./zkCli.sh -server 192.168.110.231:8998
结果如下:
23647 15:49:10 connect(13, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.1.93")}, 16) = 0
23649 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23634 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23652 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23634 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23654 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23634 15:49:10 --- SIGCHLD (Child exited) @ 0 (0) ---
23647 15:49:15 connect(14, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.1.95")}, 16) = 0
23647 15:49:18 connect(15, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.1.92")}, 16) = 0
23647 15:49:38 connect(13, {sa_family=AF_FILE, path="/var/run/avahi-daemon/socket"}, 110) = 0
23647 15:49:43 connect(11, {sa_family=AF_INET6, sin6_port=htons(8998), inet_pton(AF_INET6, "::ffff:192.168.110.231", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, 28) = -1 EINPROGRESS (Operation now in progress)
简单说下参数含义
还有个-c选项很有用,可以对系统调用进行统计。
输出的含义为
通过输出发现会反复的连接10.0.1.95:53, 10.0.1.92:53,怀疑是dns配置错误。
查看/etc/resolv.conf文件,发觉确实配置了一个坏掉的dns,导致查询dns非常慢。
ps:
实际排错走了很多弯路,就不一一详细描述了。其实早就知道dns坏了,一直认为是通过ip去访问的,没想到会是dns的问题。
最后发觉是zookeeper客户端会调用getHostName,通过ip查找主机名称然后设置线程名
setName(getName().replaceAll("\\(.*\\)", "(" + addr.getHostName() + ":" + addr.getPort() + ")"));
-EOF-
]]>