Archive for January, 2013

作者:AngryFox 分类: Uncategorized January 30th, 2013 暂无评论

MySQL大量unauthenticated user 最近数据库经常出现异常停顿和阻塞,通过show processlist发现大量类似如下的连接:

|592|unauthenticated user|192.168.3.20:35320|NULL|Connect| |login|NULL|
|593|unauthenticated user|192.168.3.20:35321|NULL|Connect| |login|NULL|
|594|unauthenticated user|192.168.3.20:35322|NULL|Connect| |login|NULL| google后发现和域名解析有关系。不管连接的方式是经过hosts或是IP,mysql都会对DNS做反查mysql尝试反查IP->DNS,由于反查解析过慢或出问题,无法应对快速多量的查询 3种解决方法(前2种解决方法的缺点是不能再使用主机名,而要使用IP地址):

在启动参数中添加–skip-name-resolve,关闭反向域名解析功能
在my.cnf的[mysqld]一节中增加skip-name-resolve
在/etc/hosts文件中添加对应的host记录

作者:AngryFox 分类: Uncategorized January 29th, 2013 暂无评论

http://qing.weibo.com/1639780001/61bd0ea133002460.html

http://qing.weibo.com/1639780001/61bd0ea1330025sq.html

对于一个简单的计数服务来说,确实非常的简单,我们可以有很多的解决方案:

方案一: 直接上mysql

这个不用多说了吧,足够的简单暴力。 但是在产品发展的初期快速迭代的阶段,他能够解决很多的问题,也不失为一个不错的解决方案。

数据量过大怎么办?

对于一亿甚至几亿以下的数据规模来说,拆表能够解决很多问题,对于微博计数器来说至少有两种经典的拆法:

一. 按id取模,把数据拆分到N个表当中去。 这个方案的悲剧是: 扩展性不好,不好加表,数据一旦满了,加起来很郁闷。虽然可以预先多分一些表,但是对于weibo这种快速增长的业务来说,严重影响了业务的快速增长需求。

二. 按id的时间来分段拆表,满了就建新表。 这个方案的悲剧是: 冷热不均,最近的weibo肯定是被访问最频繁的,而老的库又基本没有访问。 可以通过冷热库混合部署的方案来缓解,但是部署和维护的成本非常大。

数据量从亿上升到千亿后,这个问题的本质就发生了变化,维护上千张表,热点还各不相同需要经常切换调整,这是一件非常悲剧的事情。。。

访问量太大怎么办?

应对访问量,也有很多的经典的方法:

一. 上Cache(Eg: Memcache), 访问时先访问Cache,不命中时再访问mysql. 这样做有两个郁闷点: 空数据也得Cache(有一半以上的微博是没有转发也没有评论的,但是依然有大量的访问会查询他); Cache频繁失效(由于计数更新非常快,所以经常需要失效Cache再重种,还会导致数据不一致);做为最基础的服务,使用复杂,客户端需要关注的东西更多

二. 更好的硬件解决。 上FusionIO + HandleSocket + 大内存 优化. 通过硬件的方式也能够解决问题,但是这是最典型的Scale up的方案。虽然完全不用开发,但是硬件成本不低,且对于更复杂的需求,以及流量快速的增长,也很难应对。

优点:
一. 不用开发, 码农们可以用写代码的时间出去泡泡妞。
二. 方案成熟, 数据复制,管理,修复方案都很成熟。

缺点:
一. 对大数据量和高并发访问支持不好,非常的力不从心。
二. 维护成本和硬件成本都很高。

总的来说: Mysql分表 + Cache/硬件 加速的方案 对于数据规模和访问量不是特别巨大的情况下,非常不错的解决方案,但是量大了之后非常不合事宜.

既然 Mysql不行,那用NoSQL 呢?

方案二: Redis

做为一个简单的内存数据结构来说,Redis提供非常简单易用的访问接口,而且有相当不错的单机性能。 通过incr实现的 Counter Pattern,用来做计数器服务,更是简单轻松。 通过上层的分表,增加slave等方式,堆一些机器,也能够解决大数据量和高并发访问的问题。

但是Redis是纯内存的(vm机制不成熟而且即将被废弃,我们线上肯定是不敢直接使用的!),所以成本也不算低,我们简单的来估算一下数据存储量(下面都是按照Redis 2.4.16的实现,在64位系统,指针为8字节来估算的) :

假设 key 为8字节,value为 4字节,通过incr存储的话:
一个 value 通过 createStringObjectFromLongLong 创建一个robj,由于value在LONG_MIN 和LONG_MAX 之间,所以可以将value用 ptr指针来存储,需要占用 sizeof(robj) = 16 字节;
一个key(即微博id) 最长64位数字(Eg: 5612814510546515491),但通过 sdsdup 以字符串的形式存储,至少需要 8(struct sdshdr)+19+1 = 28字节;
为了存到Redis 的dict里面,需要一个dictEntry对象,继续 3*8 = 24字节;
放到db->dict->ht[0]->table中存储dictEntry的指针,再要 8个字节;

存储一个64位key,32位value的计数,Redis也至少需要耗费: 16 + 28 + 24 + 8 = 76 字节。 1000亿个key全内存的话,就至少需要 100G * 76 = 7.6TB的内存了(折算76G内存机器也需要100台!)。 我们的有效数据其实是 1000亿*32位 = 400GB,但是却需要7.6TB来存储,内存的有效利用率约为: 400GB/7600GB = 5.3%.

即使这样,对于很多热点的数据,只有一个副本,单机性能不够,系统的稳定性也无法保证(单机Down掉咋办?), 还需要复制多份。 再算上为了避免内存碎片引入的jemalloc的内存开销; 再算了dictExpand等需要的临时内存空间; 再算上系统要用的内存开销。。。那要的机器就更多了,保守估计需要300-400台以上的机器。

总的来说: Redis做为优秀的内存数据结构,接口方便,使用简单,对于小型数据量的中高访问量的计数类服务来说,是一个很不错的选择,但是对于微博计数器这种极端的应用场景,成本还是无法接受!

还有一些同学提出了用 Cassandra,MongoDB 等其他NoSQL的方案,无论是从可维护性的角度,还是从机器利用率的角度,都很难以接受(有兴趣的同学可以仔细分析一下)。

普通的NoSQL也不行,那怎么办? 尝试定制我们自己的Counter!

Update4:
//@刘浩bupt: @cydu 刚刚仔细阅读了文中redis容量预估的部分,有两点小瑕疵:1.对于value的存储,文中估算了16个字节,其实这部分开销是可以节省的。createStringObjectFromLongLong函数,对于小于REDIS_SHARED_INTEGERS的value值,不会额外分配空间。REDIS_SHARED_INTEGERS默认是10000,调大一些可以满足大部分需求

//@刘浩bupt: @cydu 2.是可以评估下使用zipmap达到的内存利用率。redis不是只有string->string的kv存储,还是有一些可以挖掘的东西的。instagram在其工程博客中介绍过(http://t.cn/S7EUKe),改用zipmap后,其存储1M的数据,内存占用由70M优化到了16M。鉴于新浪微博大量的使用redis,定制redis实现服务也是个思路。

感谢 @刘浩bupt 同学帮我指出对于Redis容量预估的不准确,通过Redis自带的 REDIS_SHARED_INTEGERS 机制确实可能大量节省value所占的内存,但是由于这个方案需要依赖存储shared_int的指针,不太好迁移到方案三里面去。
Zipmap这个优化的思路是相当不错的,对于通用的Redis的使用,我们会持续关注。

方案三: Counter

计数器是一个普通的基础服务,但是因为数据量太大了,从而量变引发了质变。 所以我们做Counter时的一个思路就是: 牺牲部分的通用性,针对微博转发和评论的大数据量和高并发访问的特点来进行定点优化。

1. 大量微博(一半以上)是没有转发,或者没有评论,甚至是没有转发也没有评论。

针对这种情况的优化: 抛弃 存储+Cache的思路, 因为这些为0的数据,也必须进到Cache中(无论是旁路还是穿透),因为查询量并不小,这对于我们Cache的利用率影响非常非常的大(有一半的数据是空的。) 而我们采用类似 存储即Cache(存储本身就在内存中) 时,这一类的数据是可以不存储的,当查不到的时候,就返回0。

通过这种情况,1000亿个数字,我们可以减少3/5,即最多只需要存 400亿个数字。这算是最经典的稀疏数组的优化存储方式了。

2. 微博的评论数和转发数 的关联度非常的高。

他们都有相同的主Key, 有大量转发的微博一般也有评论,有大量评论的一般转发量也不小。 而且访问量最大的Feed页基本上取评论数的时候,也会取转发数。。。

针对这种情况的优化: 我们将评论数和转发数 可以考虑存储在一起,这样的话,可以节省大量key的存储空间。 由 微博ID+评论数; 微博ID+转发数 变为: 微博ID+评论数+转发数的结构。

PS: 这个优化和上一个优化是有一些小冲突的,部分有转发没有评论的微博,需要多存一个0; 但是经过数据评估,我们发现这个优化还是相当必要的: a. key存储的空间比评论数还要长,一个8字节,一个4字节; b. 对于应用层来说,批量请求可以减少一次访问,能够降请求的压力,同时提升响应的时间;

(具体的数字不方便透露,但是这个结论大家可以随机抽取一批公开的微博来验证)

3. 数据结构的优化

通过方案二中Redis对内存使用的分析,我们发现是非常”奢侈”的, 大量的重复存储着指针和预留的字段,而且造成大量的碎片内存的使用, 当然Redis主要是出于通用性的考虑。 针对这种情况:
@果爸果爸 同学设计了一个更轻量更简单的数据结构,能更好的利用内存,核心思路:

a. 通过下面的item结构来存储 转发和评论数:
struct item{
int64_t weibo_id;
int repost_num;
int comment_num;
};
存储数字,而不是字符串,没有多余的指针存储, 这样的话,两个数字只占 16个字节;

b. 程序启动的时候,开辟一大片的内存 (table_size * sizeof(item)) 并清0他。

c. 插入时:
h1 = hash1(weibo_id);
h2 = hash2(weibo_id);

如果 h1%table_size 是空的,则把item存储到这个位置上;
否则 s=1 并找 ( h1 + h2*s ) % table_size 的位置,如果还不空的话,s++继续找空位。。。

d. 查询时:
和插入的过程类似,找到一个数据后,比较weibo_id 和 item.weibo_id 是否一致,一致
则表示查到,否则查到空的则表示为值为0;

e. 删除时:
查找到所在位置,设置特殊的标志; 下次插入时,可以填充这个标志位,以复用内存。。。

经过我们实测,当2亿数据这种长度的数组中,容量不超过95%的时候,冲突率是可以接受的
(最悲剧的时候可能需要做几百次的内存操作才能找到相应的空位, 性能上完全能够接受; )

经过这个优化之后,我们的总数据量变成了:
400亿 * 16B = 640GB; 基本是方案二的 十分之一还少!

4. 转发和评论数 Value的优化

继续观察,我们发现大量的微博,虽然有转发和评论,但是值一般都比较小,几百或者几千的,
超过几万的weibo很少(数据调研显示在十万分之一以下)。

所以我们把 item 升级为:
struct item{
int64_t weibo_id;
unsigned short repost_num;
unsigned short comment_num;
};
对于转发数和评论数大于 65535 的weibo,我们在这里记录一个特殊的标志位FFFF,然后去
另外的dict中去查找(那边不做这个优化)。事实上,还可以把 unsigned short优化为 int:12 之类的极端情况,但是更复杂,且收益一般,所以我们还是选用unsigned short。

经过这个优化后,我们的总数据量变成了:
400亿 * 12B = 480GB, 这个数据量已经差不多是单机能够存储的容量了。
每秒的查询量由100W变成了50W, 更新量每秒只有数万没有变化,和查询量比可以先忽略。

4.1 补充 Value的优化

@吴廷彬: 另外,64bit value可以用utf-8的类似思想再压缩。最后因为cpu/mem不是瓶颈,可以将weibo_id和后面的value分开放在两个数组里面,对应的index一样即可。然后会发现value数组里面的64bit很多位全是0,或许可以考虑以K为单位的数据做简单数据压缩放入内存里面,这个压缩比应该是惊人的。

@吴廷彬: 回复@cydu:value可以用二维数组怎么样。 如果1K为单位压缩则每一行表示1K个数据。然后对数据进行压缩写入。 一般可能每行只用100个字节?

@cydu: 这样确实可以,变长编码会有意义,反正cpu应该不是瓶颈,有更新的时候整块重新编码,取也是全取出再解压。还一个好处是我加列更方便了,现在我加列的代价其实是很高的。

最早的时候,我也想过用变长压缩,但是思路一直局限在一个value里面做压缩,由于只有两列,我们又是用定长的存储,一方面变长有开销(标志位标志用了多少位来表示),另一方面定长开给的32位省出来也没有合适的用处(可以和key的优化结合起来,用更少的字段)。 @吴廷彬 一说二维数据,立马变长压缩的好处就显现出来了。

我可以把key单独存储,把value,按 1024个value甚至更多个value 压缩到一个mini block中存储,在定长的情况下,这个mini block的size是 1024*32 = 4K. 但是事实上,这4K中包含了大量的 0, 我不用自己整复杂的变长编码,直接拿这4K的数据做LZF压缩,只存储压缩后的数据就行了, 取的时候先解压缩。 具体的压缩效率得看数据才能定,但是根据一般文本的压缩到 50% 应该是非常轻松的,也就是说,至少可以节省 400亿 * 2 = 80GB的内存。

这个方案最大的一个好处还不在于这80GB的内存的节省,而是:
1. 我前面优化提到的 大于 65535 的转发和评论,我可以考虑简单做了,反正变长嘛,不影响,整个方案是简化了的。(当然需要具体的数据测试一下,验证哪个更好)

2. [相当重要!!] 对于微博的计数,其实我们是有加列的需求的,比如其他的类似评论数的数字,我原来的方案中,加列的代价是相当高的,需要重开一个大数组,还要事先设好hint(对于新业务来说,hint值的不好选取,但是他对性能和内存的使用率影响又是致命的!),而这个方案,无论你加多少列都其实没啥关系,用内存的长度只和你真实的数据量相关!

经过这个优化后,我保守的估计,我们能够在之前的基础上,再节省 80GB的内存!

5. key的优化

@吴廷彬 很好的文章。weibo_id是8byte的,压缩能够压到接近4byte.假如一堆数据是AB,AC,AD,AE,AF, XE,XY,XZ.把他在内存里面A开头放在一坨内存,X开头放在另外一坨,里面只用存B,C,D,E,F和Y,Z. 基本上能减少4个字节。能省掉40G*4=160G?

@drdrxp: 存储分成2^24个区,weibo_id%(2^24)指到区的号上,记录中再用40bit 存储weibo_id/(2^24),记录中另外12bit 存转发,12bit存评论, 1条记录总共8字节,480G可以优化到320G. 如果能实际考察下评论转发数的分布应该可以更加优化1些.

感谢 @吴廷彬 @drdrxp 提的这个建议,这一块的优化空间确实非常的大。后面其实有提到,我们会根据时间段或者根据weibo id把大的table 划分成多个小的table(主要是为了能够序列化到磁盘腾空间给更热的数据)。 所以在一个小table里面的数据都是weibo_id比较接近的,Eg: 5612814510546515491, 5612814510546515987, 我们可以把这64位key中相同的高32位归并起来。做为小table的属性(prefix),就不必每一条都存储了。 8字节的key,至少能够节省 4字节。

struct item{
int weibo_id_low;
unsigned short repost_num;
unsigned short comment_num;
};

经过这个优化后,我们的总数据量变成了:
400亿 * 8B = 320GB, ^_^

也感谢 @drdrxp 的建议,之前也考虑过12bit来存评论数和转发数,确实能够优化不少,但是由于多出来的bit不知道干嘛,就没搞了,呵呵。你的建议和 @吴廷彬 提的建议都主要是在key上做文章,很赞!

6. 批量查询

对于Feed页来说,一次取到N条微博,然后查询他的计数,这里可以很好的批量化查询来优化响应时间。
以一次批量访问10个微博的计数来说,对于Counter碰到的压力就是 5W requests/second, 100W keys/second;

对于全内存的简单服务来说,单机已经基本能够扛 5W+ 的请求了。

7. 冷热数据

继续看这400亿个数字,我们发现,访问热点非常的集中,大量去年,甚至前年的weibo无人访问。
本能的可能想到经典Cache的做法,热的数据在内存,冷的数据放磁盘。 但是如果引入lru的话,意味
着我们的struct item得膨胀,会占用更多内存。而且对于0数据也得Cache。。。

对于这种情况,我们设计了一个非常简单的内存和磁盘淘汰策略,根据weiboid的区间(其实是时间)
来进行淘汰,按区间划分,超过半年的dump到磁盘上去,半年内的继续留存在内存,当少量用户挖坟的时候
(访问很老的微博并转发/评论),我们去查询磁盘,并将查询的结果放到 Cold Cache当中.

为了方便把旧的数据dump到磁盘,我们把那个大的table_size拆成多个小的table,每个table都是不同的
时间区间内的weibo计数,dump的时候,以小的table为单位。

为了提高磁盘的查询效率,dump之前先排序,并在内存中建好索引,索引建在Block上,而非key上。
一个Block可以是4KB甚至更长,根据索引最多一次随机IO把这个Block取出来,内存中再查询完成;

经过这个优化,我们可以把将近200G的数据放到磁盘当中, 剩余120GB的数据仍然留在内存当中。 而且即使随着weibo数越来越多,我们也依然只要保存120GB的数据在内存中就行了,磁盘的数据量会增加,热点的数据也会变化,但是总的热点数据的量是变化很少的!

[微架构设计]微博计数器的设计(下)

8. 数据的持久化

对于Sorted 部分的数据,一旦刷到磁盘后,就只会读,不会修改,除非在做和Cold Block做merge的时候,才会重写 (目前这一块merge的逻辑没有实现,因为必要性不高)。

对于内存中的数据,我们会定期把 Block 完整的dump到 磁盘中,形成 unsorted block。 然后每一次内存操作都会有相应的Append log, 一旦机器故障了,可以从 磁盘上的Block上加载,再追加Append log中的操作日志来恢复数据。

当然,从整个架构上,一旦Counter崩溃等严重错误,导致数据错误,我们还可以通过 具体数据的存储服务上把数据重新计算出来,恢复到Counter当中。 当然这种计数的代价是非常高的,你想想姚晨那么多粉丝,counter一遍很恐怖的, 我们也另外做了一些二级索引之类的简单优化。

9. 一致性保证

@lihan_harry上边文章提到计数对正确性要求高,由于计数不满足幂等性。那么这个问题是怎么解决的

@cydu回复 @lihan_harry :是这样的,前面有一个消息队列,通过类似于transid的方案来做除重,避免多加和少加; 当然这里主要是指用主从的结构,incr累加,即使是最终一致也不至于太离谱; 另外,我们还有做实际的存储数据到Counter的定期数据校验,以后面的数据存储为准

@郑环Zheng貌似还会有写请求单点问题,老数据的删除递减走硬盘,多机房冗余,机器假死宕机数据会不会丢失,删微博的时候还要清空相关计算不呢

@cydu回复 @郑环Zheng :是的,为了incr 的准确性,还是使用Master-Slave的结构,所以Master的单点问题依然存在,需要靠主从切换,以及事后的数据修复来提高数据的准确性。

10. 分布式化

出于稳定性的数据冗余的考虑,而且考虑到weibo现在数据增长的速度,在可预见的未来,数字会
变成1500亿,2000亿甚至更高。

我们在上层还是做了一些简单的拆分的,按照weiboid取模,划分到4套上(主要是考虑到后续数据的增长),
每套Master存储后面又挂2个Slave, 一方面是均摊读的压力,另一方面主要是容灾(当主挂掉的时候,
还有副在,不影响读,也能够切换)

so, 我还是没能单机扛住这1000亿个数字,和每秒100W次的查询。。。只好厚着脸皮问老大申请了十几台机器。

优点: 单机性能真的很好,内存利用率很高,对后续扩展的支持也相当不错。
缺点: 我们码农泡妞的时间少了,得抽空写写代码。。。但是,如果不用写码的话,那码农还能干嘛呢?

总之: 对于这种极端的情况,所以我们采用了同样极端的方式来优化,牺牲了部分的通用性。

方案四: Counter Service

方案三出来后, 微博计数的问题是解决了,但是我们还有用户关注粉丝计数呢,好友计数,会员计数…
数字社会嘛,自然是很多数字,每一个数字背后都是一串串的故事。

[微架构设计]微博计数器的设计(下)

针对这种情况,我们在Counter的基础上,再把这个模块服务化,对外提供一整套的 Counter Service,
并支持动态的Schema修改(主要是增加),这个服务的核心接口类似于下面这个样子:

//增加计数, 计数的名字是: “weibo”
add counter weibo

// 向”weibo”这个计数器中增加一列,列名是 weibo_id, 最长为64位,一般也是64位,默认值为0, 而且这一列是key
add column weibo weibo_id hint=64 max=64 default=0 primarykey

// 向”weibo”这个计数器中增加一列,列名是 comment_num, 最长为32位,一般是16位,默认值为0
add column weibo comment_num hint=16 max=32 default=0 suffix=cntcm

// 向”weibo”这个计数器中增加一列,列名是 repost_num, 最长为32位,一般是16位,默认值为0
add column weibo repost_num hint=16 max=32 default=0 suffix=cntrn

// 向”weibo”这个计数器中增加一列,列名是 attitude_num, 最长为32位,一般是8位,默认值为0
add column weibo attitude_num hint=8 max=32 default=0 suffix=cntan

….

// 设置weibo计数中 weibo_id=1234 的相关计数,包括 comment_num, repost_num, attitude_num
set weibo 1234 111 222 333

// 获取weibo计数中 weibo_id=1234 的相关计数,包括 comment_num, repost_num, attitude_num
get weibo 1234

// 获取weibo计数中 weibo_id=1234 的相关 comment_num
get weibo 1234.cntcm

// 增加weibo计数中 weibo_id=1234 的相关 comment_num
incr weibo 1234.cntcm

….

当 add column的时候,我们会根据hint值再增加一个大的table (table_size * sizeof(hint)),
但是这里不存储key,只有value,用原来item那个大table的相同key。 对于超过部分依然是走
另外的存储。

通过计数器服务化之后,最大的好处就是,后面我们再要加计数,有可能量没有那么大,可以很快的
创建出来。。。

缺点:
对于非数值类的key名,可能会退化到字符串的存储,我们可以通过简化的base64等机制来缩短空间;
对于频繁修改老的数据,导致cold buffer膨胀的问题,可以通过定期的merge来缓解(类似于Leveldb的机制);

作者:AngryFox 分类: Uncategorized January 24th, 2013 暂无评论

Memcached有个stats命令,通过它可以查看Memcached服务的许多状态信息。使用方法如下:
先在命令行直接输入telnet 主机名端口号,连接到memcached服务器,然后再连接成功后,输入stats 命令,即可显示当前memcached服务的状态信息。
比如在我本机测试如下:
stats
STAT pid 1552
STAT uptime 3792
STAT time 1262517674
STAT version 1.2.6
STAT pointer_size 32
STAT curr_items 1
STAT total_items 2
STAT bytes 593
STAT curr_connections 2
STAT total_connections 28
STAT connection_structures 9
STAT cmd_get 3
STAT cmd_set 2
STAT get_hits 2
STAT get_misses 1
STAT evictions 0
STAT bytes_read 1284
STAT bytes_written 5362
STAT limit_maxbytes 67108864
STAT threads 1
END
这里显示了很多状态信息,下边详细解释每个状态项:
1. pid: memcached服务进程的进程ID
2. uptime: memcached服务从启动到当前所经过的时间,单位是秒。
3. time: memcached服务器所在主机当前系统的时间,单位是秒。
4. version: memcached组件的版本。这里是我当前使用的1.2.6。
5. pointer_size:服务器所在主机操作系统的指针大小,一般为32或64.
6. curr_items:表示当前缓存中存放的所有缓存对象的数量。不包括目前已经从缓存中删除的对象。
7. total_items:表示从memcached服务启动到当前时间,系统存储过的所有对象的数量,包括目前已经从缓存中删除的对象。
8. bytes:表示系统存储缓存对象所使用的存储空间,单位为字节。
9. curr_connections:表示当前系统打开的连接数。
10. total_connections:表示从memcached服务启动到当前时间,系统打开过的连接的总数。
11. connection_structures:表示从memcached服务启动到当前时间,被服务器分配的连接结构的数量,这个解释是协议文档给的,具体什么意思,我目前还没搞明白。
12. cmd_get:累积获取数据的数量,这里是3,因为我测试过3次,第一次因为没有序列化对象,所以获取数据失败,是null,后边有2次是我用不同对象测试了2次。
13. cmd_set:累积保存数据的树立数量,这里是2.虽然我存储了3次,但是第一次因为没有序列化,所以没有保存到缓存,也就没有记录。
14. get_hits:表示获取数据成功的次数。
15. get_misses:表示获取数据失败的次数。
16. evictions:为了给新的数据项目释放空间,从缓存移除的缓存对象的数目。比如超过缓存大小时根据LRU算法移除的对象,以及过期的对象。
17. bytes_read:memcached服务器从网络读取的总的字节数。
18. bytes_written:memcached服务器发送到网络的总的字节数。
19. limit_maxbytes:memcached服务缓存允许使用的最大字节数。这里为67108864字节,也就是是64M.与我们启动memcached服务设置的大小一致。
20. threads:被请求的工作线程的总数量。这个解释是协议文档给的,具体什么意思,我目前还没搞明白。
总结:stats命令总体来说很有用,通过这个命令我们很清楚当前memcached服务的各方面的信息

作者:AngryFox 分类: Uncategorized January 23rd, 2013 暂无评论

注:本文所述内容基于 Redis2.6 及以上版本。
连接的建立

Redis 通过监听一个 TCP 端口或者 Unix socket 的方式来接收来自客户端的连接,当一个连接建立后,Redis 内部会进行以下一些操作:

首先,客户端 socket 会被设置为非阻塞模式,因为 Redis 在网络事件处理上采用的是非阻塞多路复用模型。
然后为这个socket 设置 TCP_NODELAY 属性,禁用 Nagle 算法
然后创建一个 readable 的文件事件用于监听这个客户端 socket 的数据发送

当客户端连接被初始化后,Redis 会查看目前的连接数,然后对比配置好的 maxclients 值,如果目前连接数已经达到最大连接数 maxclients 了,那么说明这个连接不能再接收,Redis 会直接返回客户端一个连接错误,并马上关闭掉这个连接。
服务端处理顺序

如果有多个客户端连接上 Redis,并且都向 Redis 发送命令,那么 Redis 服务端会先处理哪个客户端的请求呢?答案其实并不确定,主要与两个因素有关,一是客户端对应的 socket 对应的数字的大小,二是 kernal 报告各个客户端事件的先后顺序。

Redis 处理一个客户端传来数据的步骤如下:

它对触发事件的 socket 调用一次 read(),只读一次(而不是把这个 socket 上的消息读完为止),是为了防止由于某个别客户端持续发送太多命令,导致其它客户端的请求长时间得不到处理的情况。
当然,当这一次 read() 调用完成后,它里面无论包含多少个命令,都会被一次性顺序地执行。这样就保证了对各个客户端命令的公平对待。

关于最大连接数 maxclients

在 Redis2.4 中,最大连接数是被直接硬编码在代码里面的,而在2.6版本中这个值变成可配置的。maxclients 的默认值是 10000,你也可以在 redis.conf 中对这个值进行修改。

当然,这个值只是 Redis 一厢情愿的值,Redis 还会照顾到系统本身对进程使用的文件描述符数量的限制。在启动时 Redis 会检查系统的 soft limit,以查看打开文件描述符的个数上限。如果系统设置的数字,小于咱们希望的最大连接数加32,那么这个 maxclients 的设置将不起作用,Redis 会按系统要求的来设置这个值。(加32是因为 Redis 内部会使用最多32个文件描述符,所以连接能使用的相当于所有能用的描述符号减32)。

当上面说的这种情况发生时(maxclients 设置后不起作用的情况),Redis 的启动过程中将会有相应的日志记录。比如下面命令希望设置最大客户端数量为100000,所以 Redis 需要 100000+32 个文件描述符,而系统的最大文件描述符号设置为10144,所以 Redis 只能将 maxclients 设置为 10144 – 32 = 10112。

$ ./redis-server –maxclients 100000
[41422] 23 Jan 11:28:33.179 # Unable to set the max number of files limit to 100032 (Invalid argument), setting the max clients configuration to 10112.

所以说当你想设置 maxclients 值时,最好顺便修改一下你的系统设置,当然,养成看日志的好习惯也能发现这个问题。

具体的设置方法就看你个人的需求了,你可以只修改此次会话的限制,也可以直接通过sysctl 修改系统的默认设置。如:

ulimit -Sn 100000 # This will only work if hard limit is big enough.
sysctl -w fs.file-max=100000
输出缓冲区大小限制

对于 Redis 的输出(也就是命令的返回值)来说,其大小经常是不可控的,可能是一个简单的命令,能够产生体积庞大的返回数据。另外也有可能因为执行命令太多,产生的返回数据的速率超过了往客户端发送的速率,这时也会产生消息堆积,从而造成输出缓冲区越来越大,占用过多内存,甚至导致系统崩溃。

所以 Redis 设置了一些保护机制来避免这种情况的出现,这些机制作用于不同种类的客户端,有不同的输出缓冲区大小限制,限制方式有两种:

一种是大小限制,当某一个客户端的缓冲区超过某一大小时,直接关闭掉这个客户端连接
另一种是当某一个客户端的缓冲区持续一段时间占用空间过大时,也直接关闭掉客户端连接

对于不同客户端的策略如下:

对普通客户端来说,限制为0,也就是不限制,因为普通客户端通常采用阻塞式的消息应答模式,如:发送请求,等待返回,再发请求,再等待返回。这种模式通常不会导致输出缓冲区的堆积膨胀。
对于 Pub/Sub 客户端来说,大小限制是32m,当输出缓冲区超过32m时,会关闭连接。持续性限制是,当客户端缓冲区大小持续60秒超过8m,也会导致连接关闭。
而对于 Slave 客户端来说,大小限制是256m,持续性限制是当客户端缓冲区大小持续60秒超过64m时,关闭连接。

上面三种规则都是可配置的。可以通过 CONFIG SET 命令或者修改 redis.conf 文件来配置。
输入缓冲区大小限制

Redis 对输入缓冲区大小的限制比较暴力,当客户端传输的请求大小超过1G时,服务端会直接关闭连接。这种方式可以有效防止一些客户端或服务端 bug 导致的输入缓冲区过大的问题。
Client 超时

对当前的 Redis 版本来说,服务端默认是不会关闭长期空闲的客户端的。但是你可以修改默认配置来设置你希望的超时时间。比如客户端超过多长时间无交互,就直接关闭。同理,这也可以通过 CONFIG SET 命令或者修改 redis.conf 文件来配置。

值得注意的是,超时时间的设置,只对普通客户端起作用,对 Pub/Sub 客户端来说,长期空闲状态是正常的。

另外,实际的超时时间可能不会像设定的那样精确,这是因为 Redis 并不会采用计时器或者轮训遍历的方法来检测客户端超时,而是通过一种渐近式的方式来完成,每次检查一部分。所以导致的结果就是,可能你设置的超时时间是10s,但是真实执行的时间是超时12s后客户端才被关闭。
CLIENT 命令

Redis 的 CLIENT 命令能够实现三种功能:检查连接的状态,杀掉某个连接以及为连接设置名字。

CLIENT LIST 命令能够获取当前所有客户端的状态,使用方法如下:

redis 127.0.0.1:6379> client list
addr=127.0.0.1:52555 fd=5 name= age=855 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
addr=127.0.0.1:52787 fd=6 name= age=6 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping

如上面命令的输出可知,目前此 Redis 有两个客户端连接,每一行表示一个连接的各项信息:

addr: 客户端的TCP地址,包括IP和端口
fd: 客户端连接 socket 对应的文件描述符句柄号
name: 连接的名字,默认为空,可以通过 CLIENT SETNAME 设置
age: 客户端存活的秒数
idle: 客户端空闲的秒数
flags: 客户端的类型 (N 表示普通客户端,更多类型见 http://redis.io/commands/client-list)
omem: 输出缓冲区的大小
cmd: 最后执行的命令名称

你可以查看 CLIENT LIST 的文档来具体查看所有输出的含义。

当你通过上面命令获取到客户端列表后,就可以通过 CLIENT KILL 命令来杀死指定的连接了。CLIENT KILL 的参数就是上面的 addr 值。

如上面提到的 CLIENT SETNAME 和 CLIENT GETNAME 可以用来为一个连接设置一个名字。

参考来源:http://redis.io/topics/clients

作者:AngryFox 分类: Uncategorized January 23rd, 2013 暂无评论

基于 Redis 2.4.7 stable 版本。
概述

Redis通过定义一个 struct redisServer 类型的全局变量server 来保存服务器的相关信息(比如:配置信息,统计信息,服务器状态等等)。启动时通过读取配置文件里边的信息对server进行初始化(如果没有指定配置文件,将使用默认值对sever进行初始化),初始化的内容有:起监听端口,绑定有新连接时的回调函数,绑定服务器的定时函数,虚拟内存初始化,log初始化等等。
启动
初始化服务器配置

先来看看redis 的main函数的入口

Redis.c:1694

int main(int argc, char **argv) {
time_t start;

initServerConfig();
if (argc == 2) {
if (strcmp(argv[1], “-v”) == 0 ||
strcmp(argv[1], “–version”) == 0) version();
if (strcmp(argv[1], “–help”) == 0) usage();
resetServerSaveParams();
loadServerConfig(argv[1]);
} else if ((argc > 2)) {
usage();
} else {

}
if (server.daemonize) daemonize();
initServer();

initServerConfig初始化全局变量 server 的属性为默认值。
如果命令行指定了配置文件, resetServerSaveParams重置对落地备份的配置(即重置为默认值)并读取配置文件的内容对全局变量 server 再进行初始化 ,没有在配置文件中配置的将使用默认值。
如果服务器配置成后台执行,则对服务器进行 daemonize。
initServer初始化服务器,主要是设置信号处理函数,初始化事件轮询,起监听端口,绑定有新连接时的回调函数,绑定服务器的定时函数,初始化虚拟内存和log等等。
创建服务器监听端口。

Redis.c:923

if (server.port != 0) {
server.ipfd= anetTcpServer(server.neterr,server.port,server.bindaddr);
if (server.ipfd == ANET_ERR) {
redisLog(REDIS_WARNING, “Opening port %d: %s”,
server.port, server.neterr);
exit(1);
}
}

anetTcpServer创建一个socket并进行监听,然后把返回的socket fd赋值给server.ipfd。

事件轮询结构体定义

先看看事件轮询的结构体定义

Ae.h:88

/* State of an event based program */
typedef struct aeEventLoop {
int maxfd;
long long timeEventNextId;
aeFileEvent events[AE_SETSIZE]; /* Registered events */
aeFiredEvent fired[AE_SETSIZE]; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
} aeEventLoop;

maxfd是最大的文件描述符,主要用来判断是否有文件事件需要处理(ae.c:293)和当使用select 来处理网络IO时作为select的参数(ae_select.c:50)。
timeEventNextId 是下一个定时事件的ID。
events[AE_SETSIZE]用于保存通过aeCreateFileEvent函数创建的文件事件,在sendReplyToClient函数和freeClient函数中通过调用aeDeleteFileEvent函数删除已经处理完的事件。
fired[AE_SETSIZE]用于保存已经触发的文件事件,在对应的网络I/O函数中进行赋值(epoll,select,kqueue),不会对fired进行删除操作,只会一直覆盖原来的值。然后在aeProcessEvents函数中对已经触发的事件进行处理。
timeEventHead 是定时事件链表的头,定时事件的存储用链表实现。
Stop 用于停止事件轮询处理。
apidata 用于保存轮询api需要的数据,即aeApiState结构体,对于epoll来说,aeApiState结构体的定义如下:

typedef struct aeApiState {
int epfd;
struct epoll_event events[AE_SETSIZE];
} aeApiState;

beforesleep 是每次进入处理事件时执行的函数。

创建事件轮询

Redis.c:920

server.el = aeCreateEventLoop();
Ae.c:55
aeEventLoop *aeCreateEventLoop(void) {
aeEventLoop *eventLoop;
int i;

eventLoop = zmalloc(sizeof(*eventLoop));
if (!eventLoop) return NULL;
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
if (aeApiCreate(eventLoop) == -1) {
zfree(eventLoop);
return NULL;
}
/* Events with mask == AE_NONE are not set. So let’s initialize
* the vector with it. */
for (i = 0; i < AE_SETSIZE; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
}

绑定定时函数和有新连接时的回调函数

redis.c:973

aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);
if (server.ipfd > 0 &&
aeCreateFileEvent(server.el,server.ipfd,AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR) oom(“creating file event”);

aeCreateTimeEvent创建定时事件并绑定回调函数serverCron,这个定时事件第一次是超过1毫秒就有权限执行,如果其他事件的处理时间比较长,可能会出现超过一定时间都没执行情况。这里的1毫秒只是超过后有可执行的权限,并不是一定会执行。第一次执行后,如果还要执行,是由定时函数的返回值确定的,在processTimeEvents(ae.c:219)中,当调用定时回调函数后,获取定时回调函数的返回值,如果返回值不等于-1,则设置定时回调函数的下一次触发时间为当前时间加上定时回调函数的返回值,即调用间隔时间。serverCron的返回值是100ms,表明从二次开始,每超过100ms就有权限执行。(定时回调函数serverCron用于更新lru时钟,更新服务器的状态,打印一些服务器信息,符合条件的情况下对hash表进行重哈希,启动后端写AOF或者检查后端写AOF或者备份是否完成,检查过期的KEY等等)
aeCreateFileEvent创建监听端口的socket fd的文件读事件(即注册网络io事件)并绑定回调函数acceptTcpHandler。

进入事件轮询

初始化后将进入事件轮询

Redis.c:1733

aeSetBeforeSleepProc(server.el,beforeSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);

设置每次进入事件处理前会执行的函数beforeSleep。
进入事件轮询aeMain。
退出事件轮询后删除事件轮询,释放事件轮询占用内存aeDeleteEventLoop(不过没在代码中发现有执行到这一步的可能,服务器接到shutdown命令时通过一些处理后直接就通过exit退出了,可能是我看错了,待验证)。

事件轮询函数aeMain

看看aeMain的内容

Ae.c:382

void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}

每次进入事件处理前,都会调用设置的beforesleep,beforeSleep函数主要是处理被阻塞的命令和根据配置写AOF。
aeProcessEvents处理定时事件和网络io事件。

启动完毕,等待客户端请求

到进入事件轮询函数后,redis的启动工作就做完了,接下来就是等待客户端的请求了。
接收请求
新连接到来时的回调函数

在绑定定时函数和有新连接时的回调函数中说到了绑定有新连接来时的回调函数acceptTcpHandler,现在来看看这个函数的具体内容

Networking.c:427

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd;
char cip[128];
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
REDIS_NOTUSED(privdata);

cfd = anetTcpAccept(server.neterr, fd, cip, &cport);
if (cfd == AE_ERR) {
redisLog(REDIS_WARNING,”Accepting client connection: %s”, server.neterr);
return;
}
redisLog(REDIS_VERBOSE,”Accepted %s:%d”, cip, cport);
acceptCommonHandler(cfd);
}

anetTcpAccept 函数 accept新连接,返回的cfd是新连接的socket fd。
acceptCommonHandler 函数是对新建立的连接进行处理,这个函数在使用 unix socket 时也会被用到。

接收客户端的新连接

接下来看看anetTcpAccept函数的具体内容

Anet.c:330
int anetTcpAccept(char *err, int s, char *ip, int *port) {
int fd;
struct sockaddr_in sa;
socklen_t salen = sizeof(sa);
if ((fd = anetGenericAccept(err,s,(struct sockaddr*)&sa,&salen)) == ANET_ERR)
return ANET_ERR;

if (ip) strcpy(ip,inet_ntoa(sa.sin_addr));
if (port) *port = ntohs(sa.sin_port);
return fd;
}

再进去anetGenericAccept 看看

Anet.c:313

static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len) {
int fd;
while(1) {
fd = accept(s,sa,len);
if (fd == -1) {
if (errno == EINTR)
continue;
else {
anetSetError(err, “accept: %s”, strerror(errno));
return ANET_ERR;
}
}
break;
}
return fd;
}

anetTcpAccept 函数中调用anetGenericAccept 函数进行接收新连接,anetGenericAccept函数在 unix socket 的新连接处理中也会用到。
anetTcpAccept 函数接收新连接后,获取客户端得ip,port 并返回。

创建redisClient进行接收处理

anetTcpAccept 运行完后,返回新连接的socket fd, 然后返回到调用函数acceptTcpHandler中,继续执行acceptCommonHandler 函数

Networking.c:403

static void acceptCommonHandler(int fd) {
redisClient *c;
if ((c = createClient(fd)) == NULL) {
redisLog(REDIS_WARNING,”Error allocating resoures for the client”);
close(fd); /* May be already closed, just ingore errors */
return;
}
/* If maxclient directive is set and this is one client more… close the
* connection. Note that we create the client instead to check before
* for this condition, since now the socket is already set in nonblocking
* mode and we can send an error for free using the Kernel I/O */
if (server.maxclients && listLength(server.clients) > server.maxclients) {
char *err = “-ERR max number of clients reached\r\n”;

/* That’s a best effort error message, don’t check write errors */
if (write(c->fd,err,strlen(err)) == -1) {
/* Nothing to do, Just to avoid the warning… */
}
freeClient(c);
return;
}
server.stat_numconnections++;
}

创建一个 redisClient 来处理新连接,每个连接都会创建一个 redisClient 来处理。
如果配置了最大并发客户端,则对现有的连接数进行检查和处理。
最后统计连接数。

绑定有数据可读时的回调函数

Networking.c:15

redisClient *createClient(int fd) {
redisClient *c = zmalloc(sizeof(redisClient));
c->bufpos = 0;

anetNonBlock(NULL,fd);
anetTcpNoDelay(NULL,fd);
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}

selectDb(c,0);
c->fd = fd;
c->querybuf = sdsempty();
c->reqtype = 0;

}

创建新连接的socket fd对应的文件读事件,绑定回调函数readQueryFromClient。
如果创建成功,则对 redisClient 进行一系列的初始化,因为 redisClient 是通用的,即不管是什么命令的请求,都是通过创建一个 redisClient 来处理的,所以会有比较多的字段需要初始化。

createClient 函数执行完后返回到调用处acceptCommonHandler函数,然后从acceptCommonHandler函数再返回到acceptTcpHandler函数。
接收请求完毕,准备接收客户端得数据

到此为止,新连接到来时的回调函数acceptTcpHandler执行完毕,在这个回调函数中创建了一个redisClient来处理这个客户端接下来的请求,并绑定了接收的新连接的读文件事件。当有数据可读时,网络i/o轮询(比如epoll)会有事件触发,此时绑定的回调函数readQueryFromClient将会调用来处理客户端发送过来的数据。
读取客户端请求的数据

在绑定有数据可读时的回调函数中的createClient函数中绑定了一个有数据可读时的回调函数readQueryFromClient函数,现在看看这个函数的具体内容

Networking.c:874

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = (redisClient*) privdata;
char buf[REDIS_IOBUF_LEN];
int nread;
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);

server.current_client = c;
nread = read(fd, buf, REDIS_IOBUF_LEN);
if (nread == -1) {
if (errno == EAGAIN) {
nread = 0;
} else {
redisLog(REDIS_VERBOSE, “Reading from client: %s”,strerror(errno));
freeClient(c);
return;
}
} else if (nread == 0) {
redisLog(REDIS_VERBOSE, “Client closed connection”);
freeClient(c);
return;
}
if (nread) {
c->querybuf = sdscatlen(c->querybuf,buf,nread);
c->lastinteraction = time(NULL);
} else {
server.current_client = NULL;
return;
}
if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
sds ci = getClientInfoString(c), bytes = sdsempty();

bytes = sdscatrepr(bytes,c->querybuf,64);
redisLog(REDIS_WARNING,”Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)”, ci, bytes);
sdsfree(ci);
sdsfree(bytes);
freeClient(c);
return;
}
processInputBuffer(c);
server.current_client = NULL;
}

调用系统函数read来读取客户端传送过来的数据,调用read后对读取过程中被系统中断的情况(nread == -1 && errno == EAGAIN),客户端关闭的情况(nread == 0)进行了判断处理。
如果读取的数据超过限制(1GB)则报错。
读取完后进入processInputBuffer进行协议解析。

请求协议

从readQueryFromClient函数读取客户端传过来的数据,进入processInputBuffer函数进行协议解析,可以把processInputBuffer函数看作是输入数据的协议解析器

Networking.c:835

void processInputBuffer(redisClient *c) {
/* Keep processing while there is something in the input buffer */
while(sdslen(c->querybuf)) {
/* Immediately abort if the client is in the middle of something. */
if (c->flags & REDIS_BLOCKED || c->flags & REDIS_IO_WAIT) return;

/* REDIS_CLOSE_AFTER_REPLY closes the connection once the reply is
* written to the client. Make sure to not let the reply grow after
* this flag has been set (i.e. don’t process more commands). */
if (c->flags & REDIS_CLOSE_AFTER_REPLY) return;

/* Determine request type when unknown. */
if (!c->reqtype) {
if (c->querybuf[0] == ‘*’) {
c->reqtype = REDIS_REQ_MULTIBULK;
} else {
c->reqtype = REDIS_REQ_INLINE;
}
}

if (c->reqtype == REDIS_REQ_INLINE) {
if (processInlineBuffer(c) != REDIS_OK) break;
} else if (c->reqtype == REDIS_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != REDIS_OK) break;
} else {
redisPanic(“Unknown request type”);
}

/* Multibulk processing could see a <= 0 length. */
if (c->argc == 0) {
resetClient(c);
} else {
/* Only reset the client when the command was executed. */
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
}
}

Redis支持两种协议,一种是inline,一种是multibulk。inline协议是老协议,现在一般只在命令行下的redis客户端使用,其他情况一般是使用multibulk协议。
如果客户端传送的数据的第一个字符时‘*’,那么传送数据将被当做multibulk协议处理,否则将被当做inline协议处理。Inline协议的具体解析函数是processInlineBuffer,multibulk协议的具体解析函数是processMultibulkBuffer。
当协议解析完毕,即客户端传送的数据已经解析出命令字段和参数字段,接下来进行命令处理,命令处理函数是processCommand。

Inline请求协议

Networking.c:679

int processInlineBuffer(redisClient *c) {

}

根据空格分割客户端传送过来的数据,把传送过来的命令和参数保存在argv数组中,把参数个数保存在argc中,argc的值包括了命令参数本身。即set key value命令,argc的值为3。详细解析见协议详解

Multibulk请求协议

Multibulk协议比inline协议复杂,它是二进制安全的,即传送数据可以包含不安全字符。Inline协议不是二进制安全的,比如,如果set key value命令中的key或value包含空白字符,那么inline协议解析时将会失败,因为解析出来的参数个数与命令需要的的参数个数会不一致。

协议格式

* CR LF
$ CR LF
CR LF

$ CR LF
CR LF

协议举例

*3
$3
SET
$5
mykey
$7
myvalue

具体解析代码位于

Networking.c:731

int processMultibulkBuffer(redisClient *c) {

}

详细解析见协议详解
处理命令

当协议解析完毕,则表示客户端的命令输入已经全部读取并已经解析成功,接下来就是执行客户端命令前的准备和执行客户端传送过来的命令

Redis.c:1062

/* If this function gets called we already read a whole
* command, argments are in the client argv/argc fields.
* processCommand() execute the command or prepare the
* server for a bulk read from the client.
*
* If 1 is returned the client is still alive and valid and
* and other operations can be performed by the caller. Otherwise
* if 0 is returned the client was destroied (i.e. after QUIT). */
int processCommand(redisClient *c) {

/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);

call(c);

}

lookupCommand先根据客户端传送过来的数据查找该命令并找到命令的对应处理函数。
Call函数调用该命令函数来处理命令,命令与对应处理函数的绑定位于。

Redi.c:72

struct redisCommand *commandTable;
struct redisCommand readonlyCommandTable[] = {
{“get”,getCommand,2,0,NULL,1,1,1},

}

回复请求

回复请求位于对应的命令中,以get命令为例

T_string.c:67

void getCommand(redisClient *c) {
getGenericCommand(c);
}

T_string.c:52

int getGenericCommand(redisClient *c) {
robj *o;

if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return REDIS_OK;

if (o->type != REDIS_STRING) {
addReply(c,shared.wrongtypeerr);
return REDIS_ERR;
} else {
addReplyBulk(c,o);
return REDIS_OK;
}
}

getGenericCommand在getset 命令中也会用到。
lookupKeyReadOrReply是以读数据为目的查询key函数,并且如果该key不存在,则在该函数中做不存在的回包处理。
如果该key存在,则返回该key对应的数据,addReply函数以及以addReply函数开头的都是回包函数。

绑定写数据的回调函数

接下来看看addReply函数里的内容

Networking.c:190

void addReply(redisClient *c, robj *obj) {
if (_installWriteEvent(c) != REDIS_OK) return;

}

Networking.c:64

int _installWriteEvent(redisClient *c) {
if (c->fd <= 0) return REDIS_ERR;
if (c->bufpos == 0 && listLength(c->reply) == 0 &&
(c->replstate == REDIS_REPL_NONE ||
c->replstate == REDIS_REPL_ONLINE) &&
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
sendReplyToClient, c) == AE_ERR) return REDIS_ERR;
return REDIS_OK;
}

addReply函数一进来就先调用绑定写数据的回调函数installWriteEvent。
installWriteEvent函数中创建了一个文件写事件和绑定写事件的回调函数为sendReplyToClient。

准备写的数据内容

addReply函数一进来后就绑定写数据的回调函数,接下来就是准备写的数据内容

Networking.c:190

void addReply(redisClient *c, robj *obj) {
if (_installWriteEvent(c) != REDIS_OK) return;
redisAssert(!server.vm_enabled || obj->storage == REDIS_VM_MEMORY);

/* This is an important place where we can avoid copy-on-write
* when there is a saving child running, avoiding touching the
* refcount field of the object if it’s not needed.
*
* If the encoding is RAW and there is room in the static buffer
* we’ll be able to send the object to the client without
* messing with its page. */
if (obj->encoding == REDIS_ENCODING_RAW) {
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != REDIS_OK)
_addReplyObjectToList(c,obj);
} else {
/* FIXME: convert the long into string and use _addReplyToBuffer()
* instead of calling getDecodedObject. As this place in the
* code is too performance critical. */
obj = getDecodedObject(obj);
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != REDIS_OK)
_addReplyObjectToList(c,obj);
decrRefCount(obj);
}
}

先尝试把要返回的内容添加到发送数据缓冲区中(redisClient->buf),如果该缓冲区的大小已经放不下这次想放进去的数据,或者已经有数据在排队(redisClient->reply 链表不为空),则把数据添加到发送链表的尾部。

给客户端答复数据

在绑定写数据的回调函数中看到绑定了回调函数sendReplyToClient,现在来看看这个函数的主要内容

Networking.c:566

void sendReplyToClient(aeEventLoop *el, int fd, …) {

while(c->bufpos > 0 || listLength(c->reply)) {

if(c->bufpos > 0){

nwritten=write(fd,…,c->bufpos-c->sentlen);

} else {
o = listNodeValue(listFirst(c->reply));

nwritten=write(fd,…,objlen-c->sentlen);

}
}
}

通过调用系统函数write给客户端发送数据,如果缓冲区有数据就把缓冲区的数据发送给客户端,缓冲区的数据发送完了,如果有排队数据,则继续发送。

退出

Redis 服务器的退出是通过shutdown命令来退出的,退出前会做一系列的清理工作

Db.c:347

void shutdownCommand(redisClient *c) {
if (prepareForShutdown() == REDIS_OK)
exit(0);
addReplyError(c,”Errors trying to SHUTDOWN. Check logs.”);
}

总结

框架从启动,接收请求,读取客户端数据,请求协议解析,处理命令,回复请求,退出对redis运行的整个流程做了一个梳理。对整个redis的运作和框架有了一个初步的了解。

作者:AngryFox 分类: Uncategorized January 23rd, 2013 暂无评论

memcached的简单限制就是键(key)和item的限制。最大键长为250个字符。可以接受的储存数据不能超过1MB,因为这是典型slab 的最大值。这里我们可以突破对key长度的限制。
问题解决:
修改memcached源文件,在memcached.h中定义key的长度,其代码为:
1 #define KEY_MAX_LENGTH 250

更换为所需要的长度,比如:1024
1 #define KEY_MAX_LENGTH 1024

而value的默认限制为1M
1 -I Override the size of each slab page. Adjusts max item size
2 (default: 1mb, min: 1k, max: 128m)

要增加的话,启动时添加-I 10m参数就可以。

会有一个警告,但无大碍,一般是不建议修改。
1 WARNING: Setting item max size above 1MB is not recommended!
2 Raising this limit increases the minimum memory requirements
3 and will decrease your memory efficiency.

作者:AngryFox 分类: Uncategorized January 20th, 2013 暂无评论

一、操作符优化

1、IN 操作符

用IN写出来的SQL的优点是比较容易写及清晰易懂,这比较适合现代软件开发的风格。但是用IN的SQL性能总是比较低的,从Oracle执行的步骤来分析用IN的SQL与不用IN的SQL有以下区别:

ORACLE试图将其转换成多个表的连接,如果转换不成功则先执行IN里面的子查询,再查询外层的表记录,如果转换成功则直接采用多个表的连接方式查询。由此可见用IN的SQL至少多了一个转换的过程。一般的SQL都可以转换成功,但对于含有分组统计等方面的SQL就不能转换了。

推荐方案:在业务密集的SQL当中尽量不采用IN操作符,用EXISTS 方案代替。

2、NOT IN操作符

此操作是强列不推荐使用的,因为它不能应用表的索引。

推荐方案:用NOT EXISTS 方案代替

3、IS NULL 或IS NOT NULL操作(判断字段是否为空)

判断字段是否为空一般是不会应用索引的,因为索引是不索引空值的。

推荐方案:用其它相同功能的操作运算代替,如:a is not null 改为 a>0 或a>’’等。不允许字段为空,而用一个缺省值代替空值,如申请中状态字段不允许为空,缺省为申请。

4、> 及 < 操作符(大于或小于操作符)

大于或小于操作符一般情况下是不用调整的,因为它有索引就会采用索引查找,但有的情况下可以对它进行优化,如一个表有100万记录,一个数值型字段A,30万记录的A=0,30万记录的A=1,39万记录的A=2,1万记录的A=3。那么执行A>2与A>=3的效果就有很大的区别了,因为A>2时ORACLE会先找出为2的记录索引再进行比较,而A>=3时ORACLE则直接找到=3的记录索引。

5、LIKE操作符

LIKE操作符可以应用通配符查询,里面的通配符组合可能达到几乎是任意的查询,但是如果用得不好则会产生性能上的问题,如LIKE ‘%5400%’ 这种查询不会引用索引,而LIKE ‘X5400%’则会引用范围索引。

一个实际例子:用YW_YHJBQK表中营业编号后面的户标识号可来查询营业编号 YY_BH LIKE ‘%5400%’ 这个条件会产生全表扫描,如果改成YY_BH LIKE ’X5400%’ OR YY_BH LIKE ’B5400%’ 则会利用YY_BH的索引进行两个范围的查询,性能肯定大大提高。

6、UNION操作符

UNION在进行表链接后会筛选掉重复的记录,所以在表链接后会对所产生的结果集进行排序运算,删除重复的记录再返回结果。实际大部分应用中是不会产生重复的记录,最常见的是过程表与历史表UNION。如:
select * from gc_dfys
union
select * from ls_jg_dfys
这个SQL在运行时先取出两个表的结果,再用排序空间进行排序删除重复的记录,最后返回结果集,如果表数据量大的话可能会导致用磁盘进行排序。

推荐方案:采用UNION ALL操作符替代UNION,因为UNION ALL操作只是简单的将两个结果合并后就返回。

select * from gc_dfys
union all
select * from ls_jg_dfys

二、SQL书写的影响

1、同一功能同一性能不同写法SQL的影响。

如一个SQL在A程序员写的为 Select * from zl_yhjbqk

B程序员写的为 Select * from dlyx.zl_yhjbqk(带表所有者的前缀)

C程序员写的为 Select * from DLYX.ZLYHJBQK(大写表名)

D程序员写的为 Select * from DLYX.ZLYHJBQK(中间多了空格)

以上四个SQL在ORACLE分析整理之后产生的结果及执行的时间是一样的,但是从ORACLE共享内存SGA的原理,可以得出ORACLE对每个SQL 都会对其进行一次分析,并且占用共享内存,如果将SQL的字符串及格式写得完全相同,则ORACLE只会分析一次,共享内存也只会留下一次的分析结果,这不仅可以减少分析SQL的时间,而且可以减少共享内存重复的信息,ORACLE也可以准确统计SQL的执行频率。

2、WHERE后面的条件顺序影响

WHERE子句后面的条件顺序对大数据量表的查询会产生直接的影响。如:
Select * from zl_yhjbqk where dy_dj = ’1KV以下’ and xh_bz=1
Select * from zl_yhjbqk where xh_bz=1 and dy_dj = ’1KV以下’
以上两个SQL中dy_dj(电压等级)及xh_bz(销户标志)两个字段都没进行索引,所以执行的时候都是全表扫描,第一条SQL的dy_dj = ’1KV以下’条件在记录集内比率为99%,而xh_bz=1的比率只为0.5%,在进行第一条SQL的时候99%条记录都进行dy_dj及xh_bz的比较,而在进行第二条SQL的时候0.5%条记录都进行dy_dj及xh_bz的比较,以此可以得出第二条SQL的CPU占用率明显比第一条低。

3、查询表顺序的影响

在FROM后面的表中的列表顺序会对SQL执行性能影响,在没有索引及ORACLE没有对表进行统计分析的情况下,ORACLE会按表出现的顺序进行链接,由此可见表的顺序不对时会产生十分耗服物器资源的数据交叉。(注:如果对表进行了统计分析,ORACLE会自动先进小表的链接,再进行大表的链接)

三、SQL语句索引的利用

1、操作符优化(同上)

2、对条件字段的一些优化

采用函数处理的字段不能利用索引,如:

substr(hbs_bh,1,4)=’5400’,优化处理:hbs_bh like ‘5400%’

trunc(sk_rq)=trunc(sysdate), 优化处理:sk_rq>=trunc(sysdate) and sk_rq

进行了显式或隐式的运算的字段不能进行索引,如:ss_df+20>50,优化处理:ss_df>30

‘X’ || hbs_bh>’X5400021452’,优化处理:hbs_bh>’5400021542’

sk_rq+5=sysdate,优化处理:sk_rq=sysdate-5

hbs_bh=5401002554,优化处理:hbs_bh=’ 5401002554’,注:此条件对hbs_bh 进行隐式的to_number转换,因为hbs_bh字段是字符型。

条件内包括了多个本表的字段运算时不能进行索引,如:ys_df>cx_df,无法进行优化
qc_bh || kh_bh=’5400250000’,优化处理:qc_bh=’5400’ and kh_bh=’250000’

四、其他

ORACLE的提示功能是比较强的功能,也是比较复杂的应用,并且提示只是给ORACLE执行的一个建议,有时如果出于成本方面的考虑ORACLE也可能不会按提示进行。根据实践应用,一般不建议开发人员应用ORACLE提示,因为各个数据库及服务器性能情况不一样,很可能一个地方性能提升了,但另一个地方却下降了,ORACLE在SQL执行分析方面已经比较成熟,如果分析执行的路径不对首先应在数据库结构(主要是索引)、服务器当前性能(共享内存、磁盘文件碎片)、数据库对象(表、索引)统计信息是否正确这几方面分析。

作者:AngryFox 分类: Uncategorized January 20th, 2013 暂无评论

0、用单引号代替双引号来包含字符串,这样做会更快一些。因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会,注意:只有echo能这么做,它是一种可以把多个字符串当作参数的“函数”(译注:PHP手册中说echo是语言结构,不是真正的函数,故把函数加上了双引号)。

1、如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍。

2、$row[’id’] 的速度是$row[id]的7倍。

3、echo 比 print 快,并且使用echo的多重参数(译注:指用逗号而不是句点)代替字符串连接,比如echo $str1,$str2。

4、在执行for循环之前确定最大循环数,不要每循环一次都计算最大值,最好运用foreach代替。

5、注销那些不用的变量尤其是大数组,以便释放内存。

6、尽量避免使用__get,__set,__autoload。

7、require_once()代价昂贵。

8、include文件时尽量使用绝对路径,因为它避免了PHP去include_path里查找文件的速度,解析操作系统路径所需的时间会更少。

9、如果你想知道脚本开始执行(译注:即服务器端收到客户端请求)的时刻,使用$_SERVER[‘REQUEST_TIME’]要好于time()。

10、函数代替正则表达式完成相同功能。

11、str_replace函数比preg_replace函数快,但strtr函数的效率是str_replace函数的四倍。

12、如果一个字符串替换函数,可接受数组或字符作为参数,并且参数长度不太长,那么可以考虑额外写一段替换代码,使得每次传递参数是一个字符,而不是只写一行代码接受数组作为查询和替换的参数。

13、使用选择分支语句(译注:即switch case)好于使用多个if,else if语句。

14、用@屏蔽错误消息的做法非常低效,极其低效。

15、打开apache的mod_deflate模块,可以提高网页的浏览速度。

16、数据库连接当使用完毕时应关掉,不要用长连接。

17、错误消息代价昂贵。

18、在方法中递增局部变量,速度是最快的。几乎与在函数中调用局部变量的速度相当。

19、递增一个全局变量要比递增一个局部变量慢2倍。

20、递增一个对象属性(如:$this->prop++)要比递增一个局部变量慢3倍。

21、递增一个未预定义的局部变量要比递增一个预定义的局部变量慢9至10倍。

22、仅定义一个局部变量而没在函数中调用它,同样会减慢速度(其程度相当于递增一个局部变量)。PHP大概会检查看是否存在全局变量。

23、方法调用看来与类中定义的方法的数量无关,因为我(在测试方法之前和之后都)添加了10个方法,但性能上没有变化。

24、派生类中的方法运行起来要快于在基类中定义的同样的方法。

25、调用带有一个参数的空函数,其花费的时间相当于执行7至8次的局部变量递增操作。类似的方法调用所花费的时间接近于15次的局部变量递增操作。

26、Apache解析一个PHP脚本的时间要比解析一个静态HTML页面慢2至10倍。尽量多用静态HTML页面,少用脚本。

27、除非脚本可以缓存,否则每次调用时都会重新编译一次。引入一套PHP缓存机制通常可以提升25%至100%的性能,以免除编译开销。

28、尽量做缓存,可使用memcached。memcached是一款高性能的内存对象缓存系统,可用来加速动态Web应用程序,减轻数据库负载。对运算码 (OP code)的缓存很有用,使得脚本不必为每个请求做重新编译。

29、当操作字符串并需要检验其长度是否满足某种要求时,你想当然地会使用strlen()函数。此函数执行起来相当快,因为它不做任何计算,只返回在zval 结构(C的内置数据结构,用于存储PHP变量)中存储的已知字符串长度。但是,由于strlen()是函数,多多少少会有些慢,因为函数调用会经过诸多步骤,如字母小写化(译注:指函数名小写化,PHP不区分函数名大小写)、哈希查找,会跟随被调用的函数一起执行。在某些情况下,你可以使用isset() 技巧加速执行你的代码。

(举例如下)

if (strlen($foo) < 5) { echo “Foo is too short”$$ }

(与下面的技巧做比较)

if (!isset($foo{5})) { echo “Foo is too short”$$ }

调用isset()恰巧比strlen()快,因为与后者不同的是,isset()作为一种语言结构,意味着它的执行不需要函数查找和字母小写化。也就是说,实际上在检验字符串长度的顶层代码中你没有花太多开销。

34、当执行变量$i的递增或递减时,$i++会比++$i慢一些。这种差异是PHP特有的,并不适用于其他语言,所以请不要修改你的C或Java代码并指望它们能立即变快,没用的。++$i更快是因为它只需要3条指令(opcodes),$i++则需要4条指令。后置递增实际上会产生一个临时变量,这个临时变量随后被递增。而前置递增直接在原值上递增。这是最优化处理的一种,正如Zend的PHP优化器所作的那样。牢记这个优化处理不失为一个好主意,因为并不是所有的指令优化器都会做同样的优化处理,并且存在大量没有装配指令优化器的互联网服务提供商(ISPs)和服务器。

35、并不是事必面向对象(OOP),面向对象往往开销很大,每个方法和对象调用都会消耗很多内存。

36、并非要用类实现所有的数据结构,数组也很有用。

37、不要把方法细分得过多,仔细想想你真正打算重用的是哪些代码?

38、当你需要时,你总能把代码分解成方法。

39、尽量采用大量的PHP内置函数。

40、如果在代码中存在大量耗时的函数,你可以考虑用C扩展的方式实现它们。

41、评估检验(profile)你的代码。检验器会告诉你,代码的哪些部分消耗了多少时间。Xdebug调试器包含了检验程序,评估检验总体上可以显示出代码的瓶颈。

42、mod_zip可作为Apache模块,用来即时压缩你的数据,并可让数据传输量降低80%。

43、在可以用file_get_contents替代file、fopen、feof、fgets等系列方法的情况下,尽量用file_get_contents,因为他的效率高得多!但是要注意file_get_contents在打开一个URL文件时候的PHP版本问题;

44、尽量的少进行文件操作,虽然PHP的文件操作效率也不低的;

45、优化Select SQL语句,在可能的情况下尽量少的进行Insert、Update操作(在update上,我被恶批过);

46、尽可能的使用PHP内部函数(但是我却为了找个PHP里面不存在的函数,浪费了本可以写出一个自定义函数的时间,经验问题啊!);

47、循环内部不要声明变量,尤其是大变量:对象(这好像不只是PHP里面要注意的问题吧?);

48、多维数组尽量不要循环嵌套赋值;

49、在可以用PHP内部字符串操作函数的情况下,不要用正则表达式;

50、foreach效率更高,尽量用foreach代替while和for循环;

51、用单引号替代双引号引用字符串;

52、“用i+=1代替i=i+1。符合c/c++的习惯,效率还高”;

53、对global变量,应该用完就unset()掉;

作者:AngryFox 分类: Uncategorized January 20th, 2013 暂无评论

而只是针对MySQL这一Web应用最多的数据库。希望下面的这些优化技巧对你有用。

  1. 为查询缓存优化你的查询

  大多数的MySQL服务器都开启了查询缓存。这是提高性最有效的方法之一,而且这是被MySQL的数据库引擎处理的。当有很多相同的查询被执行了多次的时候,这些查询结果会被放到一个缓存中,这样,后续的相同的查询就不用操作表而直接访问缓存结果了。

  这里最主要的问题是,对于程序员来说,这个事情是很容易被忽略的。因为,我们某些查询语句会让MySQL不使用缓存。请看下面的示例:

  上面两条SQL语句的差别就是 CURDATE() ,MySQL的查询缓存对这个函数不起作用。所以,像 NOW() 和 RAND() 或是其它的诸如此类的SQL函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替MySQL的函数,从而开启缓存。

  2. EXPLAIN 你的 SELECT 查询

  使用 EXPLAIN 关键字可以让你知道MySQL是如何处理你的SQL语句的。这可以帮你分析你的查询语句或是表结构的性能瓶颈。

  EXPLAIN 的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序的……等等,等等。

  挑一个你的SELECT语句(推荐挑选那个最复杂的,有多表联接的),把关键字EXPLAIN加到前面。你可以使用phpmyadmin来做这个事。然后,你会看到一张表格。下面的这个示例中,我们忘记加上了group_id索引,并且有表联接:

  当我们为 group_id 字段加上索引后:

  我们可以看到,前一个结果显示搜索了 7883 行,而后一个只是搜索了两个表的 9 和 16 行。查看rows列可以让我们找到潜在的性能问题。

  3. 当只要一行数据时使用 LIMIT 1

  当你查询表的有些时候,你已经知道结果只会有一条结果,但因为你可能需要去fetch游标,或是你也许会去检查返回的记录数。

  在这种情况下,加上 LIMIT 1 可以增加性能。这样一样,MySQL数据库引擎会在找到一条数据后停止搜索,而不是继续往后查少下一条符合记录的数据。

  下面的示例,只是为了找一下是否有“中国”的用户,很明显,后面的会比前面的更有效率。(请注意,第一条中是Select *,第二条是Select 1)

  4. 为搜索字段建索引

  索引并不一定就是给主键或是唯一的字段。如果在你的表中,有某个字段你总要会经常用来做搜索,那么,请为其建立索引吧。

  从上图你可以看到那个搜索字串 “last_name LIKE ‘a%’”,一个是建了索引,一个是没有索引,性能差了4倍左右。

  另外,你应该也需要知道什么样的搜索是不能使用正常的索引的。例如,当你需要在一篇大的文章中搜索一个词时,如: “WHERE post_content LIKE ‘%apple%’”,索引可能是没有意义的。你可能需要使用MySQL全文索引 或是自己做一个索引(比如说:搜索关键词或是Tag什么的)

  5. 在Join表的时候使用相当类型的例,并将其索引

  如果你的应用程序有很多 JOIN 查询,你应该确认两个表中Join的字段是被建过索引的。这样,MySQL内部会启动为你优化Join的SQL语句的机制。

  而且,这些被用来Join的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段Join在一起,MySQL就无法使用它们的索引。对于那些STRING类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)

  6. 千万不要 ORDER BY RAND()

  想打乱返回的数据行?随机挑一个数据?真不知道谁发明了这种用法,但很多新手很喜欢这样用。但你确不了解这样做有多么可怕的性能问题。

  如果你真的想把返回的数据行打乱了,你有N种方法可以达到这个目的。这样使用只让你的数据库的性能呈指数级的下降。这里的问题是:MySQL会不得不去执行RAND()函数(很耗CPU时间),而且这是为了每一行记录去记行,然后再对其排序。就算是你用了Limit 1也无济于事(因为要排序)

  下面的示例是随机挑一条记录

  7. 避免 SELECT *

  从数据库里读出越多的数据,那么查询就会变得越慢。并且,如果你的数据库服务器和WEB服务器是两台独立的服务器的话,这还会增加网络传输的负载。

  所以,你应该养成一个需要什么就取什么的好的习惯。

  8. 永远为每张表设置一个ID

  我们应该为数据库里的每张表都设置一个ID做为其主键,而且最好的是一个INT型的(推荐使用UNSIGNED),并设置上自动增加的AUTO_INCREMENT标志。

  就算是你 users 表有一个主键叫 “email”的字段,你也别让它成为主键。使用 VARCHAR 类型来当主键会使用得性能下降。另外,在你的程序中,你应该使用表的ID来构造你的数据结构。

  而且,在MySQL数据引擎下,还有一些操作需要使用主键,在这些情况下,主键的性能和设置变得非常重要,比如,集群,分区……

  在这里,只有一个情况是例外,那就是“关联表”的“外键”,也就是说,这个表的主键,通过若干个别的表的主键构成。我们把这个情况叫做“外键”。比如:有一个“学生表”有学生的ID,有一个“课程表”有课程ID,那么,“成绩表”就是“关联表”了,其关联了学生表和课程表,在成绩表中,学生ID和课程ID叫“外键”其共同组成主键。

  9. 使用 ENUM 而不是 VARCHAR

  ENUM 类型是非常快和紧凑的。在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美。

  如果你有一个字段,比如“性别”,“国家”,“民族”,“状态”或“部门”,你知道这些字段的取值是有限而且固定的,那么,你应该使用 ENUM 而不是 VARCHAR。

  MySQL也有一个“建议”(见第十条)告诉你怎么去重新组织你的表结构。当你有一个 VARCHAR 字段时,这个建议会告诉你把其改成 ENUM 类型。使用 PROCEDURE ANALYSE() 你可以得到相关的建议。

  10. 从 PROCEDURE ANALYSE() 取得建议

  PROCEDURE ANALYSE() 会让 MySQL 帮你去分析你的字段和其实际的数据,并会给你一些有用的建议。只有表中有实际的数据,这些建议才会变得有用,因为要做一些大的决定是需要有数据作为基础的。

  例如,如果你创建了一个 INT 字段作为你的主键,然而并没有太多的数据,那么,PROCEDURE ANALYSE()会建议你把这个字段的类型改成 MEDIUMINT 。或是你使用了一个 VARCHAR 字段,因为数据不多,你可能会得到一个让你把它改成 ENUM 的建议。这些建议,都是可能因为数据不够多,所以决策做得就不够准。

  在phpmyadmin里,你可以在查看表时,点击 “Propose table structure” 来查看这些建议

  一定要注意,这些只是建议,只有当你的表里的数据越来越多时,这些建议才会变得准确。一定要记住,你才是最终做决定的人。

  11. 尽可能的使用 NOT NULL

  除非你有一个很特别的原因去使用 NULL 值,你应该总是让你的字段保持 NOT NULL。这看起来好像有点争议,请往下看。

  首先,问问你自己“Empty”和“NULL”有多大的区别(如果是INT,那就是0和NULL)?如果你觉得它们之间没有什么区别,那么你就不要使用NULL。(你知道吗?在 Oracle 里,NULL 和 Empty 的字符串是一样的!)

  不要以为 NULL 不需要空间,其需要额外的空间,并且,在你进行比较的时候,你的程序会更复杂。 当然,这里并不是说你就不能使用NULL了,现实情况是很复杂的,依然会有些情况下,你需要使用NULL值。

  12. Prepared Statements

  Prepared Statements很像存储过程,是一种运行在后台的SQL语句集合,我们可以从使用 prepared statements 获得很多好处,无论是性能问题还是安全问题。

  Prepared Statements 可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式”攻击。当然,你也可以手动地检查你的这些变量,然而,手动的检查容易出问题,而且很经常会被程序员忘了。当我们使用一些framework或是ORM的时候,这样的问题会好一些。

  在性能方面,当一个相同的查询被使用多次的时候,这会为你带来可观的性能优势。你可以给这些Prepared Statements定义一些参数,而MySQL只会解析一次。

  虽然最新版本的MySQL在传输Prepared Statements是使用二进制形势,所以这会使得网络传输非常有效率。

  当然,也有一些情况下,我们需要避免使用Prepared Statements,因为其不支持查询缓存。但据说版本5.1后支持了。

  在PHP中要使用prepared statements,你可以查看其使用手册:mysqli 扩展 或是使用数据库抽象层,如: PDO.

  13. 无缓冲的查询

  正常的情况下,当你在当你在你的脚本中执行一个SQL语句的时候,你的程序会停在那里直到没这个SQL语句返回,然后你的程序再往下继续执行。你可以使用无缓冲查询来改变这个行为。

  mysql_unbuffered_query() 发送一个SQL语句到MySQL而并不像mysql_query()一样去自动fethch和缓存结果。这会相当节约很多可观的内存,尤其是那些会产生大量结果的查询语句,并且,你不需要等到所有的结果都返回,只需要第一行数据返回的时候,你就可以开始马上开始工作于查询结果了。

  然而,这会有一些限制。因为你要么把所有行都读走,或是你要在进行下一次的查询前调用 mysql_free_result() 清除结果。而且, mysql_num_rows() 或 mysql_data_seek() 将无法使用。所以,是否使用无缓冲的查询你需要仔细考虑。

  14. 把IP地址存成 UNSIGNED INT

  很多程序员都会创建一个 VARCHAR(15) 字段来存放字符串形式的IP而不是整形的IP。如果你用整形来存放,只需要4个字节,并且你可以有定长的字段。而且,这会为你带来查询上的优势,尤其是当你需要使用这样的WHERE条件:IP between ip1 and ip2。

  我们必需要使用UNSIGNED INT,因为 IP地址会使用整个32位的无符号整形。

  而你的查询,你可以使用 INET_ATON() 来把一个字符串IP转成一个整形,并使用 INET_NTOA() 把一个整形转成一个字符串IP。在PHP中,也有这样的函数 ip2long() 和 long2ip()。

  15. 固定长度的表会更快

  如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要你包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。

  固定长度的表会提高性能,因为MySQL搜寻得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。

  并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论你用不用,他都是要分配那么多的空间。

  使用“垂直分割”技术(见下一条),你可以分割你的表成为两个一个是定长的,一个则是不定长的。

  16. 垂直分割

  “垂直分割”是一种把数据库中的表按列变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。(以前,在银行做过项目,见过一张表有100多个字段,很恐怖)

  示例一:在Users表中有一个字段是家庭地址,这个字段是可选字段,相比起,而且你在数据库操作的时候除了个人信息外,你并不需要经常读取或是改写这个字段。那么,为什么不把他放到另外一张表中呢? 这样会让你的表有更好的性能,大家想想是不是,大量的时候,我对于用户表来说,只有用户ID,用户名,口令,用户角色等会被经常使用。小一点的表总是会有好的性能。

  示例二: 你有一个叫 “last_login” 的字段,它会在每次用户登录时被更新。但是,每次更新时会导致该表的查询缓存被清空。所以,你可以把这个字段放到另一个表中,这样就不会影响你对用户ID,用户名,用户角色的不停地读取了,因为查询缓存会帮你增加很多性能。

  另外,你需要注意的是,这些被分出去的字段所形成的表,你不会经常性地去Join他们,不然的话,这样的性能会比不分割时还要差,而且,会是极数级的下降。

  17. 拆分大的 DELETE 或 INSERT 语句

  如果你需要在一个在线的网站上去执行一个大的 DELETE 或 INSERT 查询,你需要非常小心,要避免你的操作让你的整个网站停止相应。因为这两个操作是会锁表的,表一锁住了,别的操作都进不来了。

  Apache 会有很多的子进程或线程。所以,其工作起来相当有效率,而我们的服务器也不希望有太多的子进程,线程和数据库链接,这是极大的占服务器资源的事情,尤其是内存。

  如果你把你的表锁上一段时间,比如30秒钟,那么对于一个有很高访问量的站点来说,这30秒所积累的访问进程/线程,数据库链接,打开的文件数,可能不仅仅会让你泊WEB服务Crash,还可能会让你的整台服务器马上掛了。

  所以,如果你有一个大的处理,你定你一定把其拆分,使用 LIMIT 条件是一个好的方法。下面是一个示例:

  18. 越小的列会越快

  对于大多数的数据库引擎来说,硬盘操作可能是最重大的瓶颈。所以,把你的数据变得紧凑会对这种情况非常有帮助,因为这减少了对硬盘的访问。

  参看 MySQL 的文档 Storage Requirements 查看所有的数据类型。

  如果一个表只会有几列罢了(比如说字典表,配置表),那么,我们就没有理由使用 INT 来做主键,使用 MEDIUMINT, SMALLINT 或是更小的 TINYINT 会更经济一些。如果你不需要记录时间,使用 DATE 要比 DATETIME 好得多。

  当然,你也需要留够足够的扩展空间,不然,你日后来干这个事,你会死的很难看,参看Slashdot的例子(2009年11月06日),一个简单的ALTER TABLE语句花了3个多小时,因为里面有一千六百万条数据。

  19. 选择正确的存储引擎

  在 MySQL 中有两个存储引擎 MyISAM 和 InnoDB,每个引擎都有利有弊。酷壳以前文章《MySQL: InnoDB 还是 MyISAM?》讨论和这个事情。

  MyISAM 适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM 对于 SELECT COUNT(*) 这类的计算是超快无比的。

  InnoDB 的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM 还慢。他是它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。

  下面是MySQL的手册

  target=”_blank”MyISAM Storage Engine

  InnoDB Storage Engine

  20. 使用一个对象关系映射器(Object Relational Mapper)

  使用 ORM (Object Relational Mapper),你能够获得可靠的性能增涨。一个ORM可以做的所有事情,也能被手动的编写出来。但是,这需要一个高级专家。

  ORM 的最重要的是“Lazy Loading”,也就是说,只有在需要的去取值的时候才会去真正的去做。但你也需要小心这种机制的副作用,因为这很有可能会因为要去创建很多很多小的查询反而会降低性能。

  ORM 还可以把你的SQL语句打包成一个事务,这会比单独执行他们快得多得多。

  目前,个人最喜欢的PHP的ORM是:Doctrine。

  21. 小心“永久链接”

  “永久链接”的目的是用来减少重新创建MySQL链接的次数。当一个链接被创建了,它会永远处在连接的状态,就算是数据库操作已经结束了。而且,自从我们的Apache开始重用它的子进程后——也就是说,下一次的HTTP请求会重用Apache的子进程,并重用相同的 MySQL 链接。

  PHP手册:mysql_pconnect()

  在理论上来说,这听起来非常的不错。但是从个人经验(也是大多数人的)上来说,这个功能制造出来的麻烦事更多。因为,你只有有限的链接数,内存问题,文件句柄数,等等。

  而且,Apache 运行在极端并行的环境中,会创建很多很多的了进程。这就是为什么这种“永久链接”的机制工作地不好的原因。在你决定要使用“永久链接”之前,你需要好好地考虑一下你的整个系统的架构。

作者:AngryFox 分类: Uncategorized January 20th, 2013 暂无评论

mysql> show global status;

可以列出mysql服务器运行各种状态值,另外,查询mysql服务器配置信息语句:

mysql> show variables;

一、慢查询

mysql> show variables like ‘%slow%‘;
+——————+——-+
| variable_name     | value |
+——————+——-+
| log_slow_queries | on     |
| slow_launch_time | 2      |
+——————+——-+

mysql> show global status like ‘%slow%‘;
+———————+——-+
| variable_name        | value |
+———————+——-+
| slow_launch_threads | 0      |
| slow_queries         | 4148 |
+———————+——-+

配置中打开了记录慢查询,执行时间超过2秒的即为慢查询,系统显示有4148个慢查询,你可以分析慢查询日志,找出有问题的sql语句,慢查询时间 不宜设置过长,否则意义不大,最好在5秒以内,如果你需要微秒级别的慢查询,可以考虑给mysql打补丁:http://www.percona.com /docs/wiki/release:start,记得找对应的版本。

打开慢查询日志可能会对系统性能有一点点影响,如果你的mysql是主-从结构,可以考虑打开其中一台从服务器的慢查询日志,这样既可以监控慢查询,对系统性能影响又小。

二、连接数

经常会遇见”mysql: error 1040: too many connections”的情况,一种是访问量确实很高,mysql服务器抗不住,这个时候就要考虑增加从服务器分散读压力,另外一种情况是mysql配 置文件中max_connections值过小:

mysql> show variables like ‘max_connections‘;
+—————–+——-+
| variable_name    | value |
+—————–+——-+
| max_connections | 256   |
+—————–+——-+

这台mysql服务器最大连接数是256,然后查询一下服务器响应的最大连接数:

mysql> show global status like ‘max_used_connections‘;

mysql服务器过去的最大连接数是245,没有达到服务器连接数上限256,应该没有出现1040错误,比较理想的设置是

max_used_connections / max_connections * 100% ≈ 85%

最大连接数占上限连接数的85%左右,如果发现比例在10%以下,mysql服务器连接数上限设置的过高了。

三、key_buffer_size

key_buffer_size是对myisam表性能影响最大的一个参数,下面一台以myisam为主要存储引擎服务器的配置:

mysql> show variables like ‘key_buffer_size‘;+—————–+————+
| variable_name    | value       |
+—————–+————+
| key_buffer_size | 536870912 |
+—————–+————+

分配了512mb内存给key_buffer_size,我们再看一下key_buffer_size的使用情况:

mysql> show global status like ‘key_read%‘;
+————————+————-+
| variable_name           | value        |
+————————+————-+
| key_read_requests       | 27813678764 |
| key_reads               | 6798830      |
+————————+————-+

一共有27813678764个索引读取请求,有6798830个请求在内存中没有找到直接从硬盘读取索引,计算索引未命中缓存的概率:

key_cache_miss_rate = key_reads / key_read_requests * 100%

比如上面的数据,key_cache_miss_rate为0.0244%,4000个索引读取请求才有一个直接读硬盘,已经很bt 了,key_cache_miss_rate在0.1%以下都很好(每1000个请求有一个直接读硬盘),如果key_cache_miss_rate在 0.01%以下的话,key_buffer_size分配的过多,可以适当减少。

mysql服务器还提供了key_blocks_*参数:

mysql> show global status like ‘key_blocks_u%‘;
+————————+————-+
| variable_name           | value        |
+————————+————-+
| key_blocks_unused       | 0            |
| key_blocks_used         | 413543       |
+————————+————-+

key_blocks_unused表示未使用的缓存簇(blocks)数,key_blocks_used表示曾经用到的最大的blocks数, 比如这台服务器,所有的缓存都用到了,要么增加key_buffer_size,要么就是过渡索引了,把缓存占满了。比较理想的设置:

key_blocks_used / (key_blocks_unused + key_blocks_used) * 100% ≈ 80%

四、临时表

mysql> show global status like ‘created_tmp%‘;
+————————-+———+
| variable_name            | value    |
+————————-+———+
| created_tmp_disk_tables | 21197    |
| created_tmp_files        | 58       |
| created_tmp_tables       | 1771587 |
+————————-+———+

每次创建临时表,created_tmp_tables增加,如果是在磁盘上创建临时表,created_tmp_disk_tables也增加,created_tmp_files表示mysql服务创建的临时文件文件数,比较理想的配置是:

created_tmp_disk_tables / created_tmp_tables * 100% <= 25%比如上面的服务器created_tmp_disk_tables / created_tmp_tables * 100% = 1.20%,应该相当好了。我们再看一下mysql服务器对临时表的配置:

mysql> show variables where variable_name in (‘tmp_table_size‘, ‘max_heap_table_size‘);
+———————+———–+
| variable_name        | value      |
+———————+———–+
| max_heap_table_size | 268435456 |
| tmp_table_size       | 536870912 |
+———————+———–+

只有256mb以下的临时表才能全部放内存,超过的就会用到硬盘临时表。

五、open table情况

mysql> show global status like ‘open%tables%‘;
+—————+——-+
| variable_name | value |
+—————+——-+
| open_tables    | 919    |
| opened_tables | 1951  |
+—————+——-+

open_tables表示打开表的数量,opened_tables表示打开过的表数量,如果opened_tables数量过大,说明配置中 table_cache(5.1.3之后这个值叫做table_open_cache)值可能太小,我们查询一下服务器table_cache值:

mysql> show variables like ‘table_cache‘;
+—————+——-+
| variable_name | value |
+—————+——-+
| table_cache    | 2048  |
+—————+——-+

比较合适的值为:

open_tables / opened_tables * 100% >= 85%

open_tables / table_cache * 100% <= 95%

六、进程使用情况

mysql> show global status like ‘thread%‘;
+——————-+——-+
| variable_name      | value |
+——————-+——-+
| threads_cached     | 46     |
| threads_connected | 2      |
| threads_created    | 570    |
| threads_running    | 1      |
+——————-+——-+

如果我们在mysql服务器配置文件中设置了thread_cache_size,当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下 一个客户而不是销毁(前提是缓存数未达上限)。threads_created表示创建过的线程数,如果发现threads_created值过大的话, 表明mysql服务器一直在创建线程,这也是比较耗资源,可以适当增加配置文件中thread_cache_size值,查询服务器 thread_cache_size配置:

mysql> show variables like ‘thread_cache_size‘;
+——————-+——-+
| variable_name      | value |
+——————-+——-+
| thread_cache_size | 64     |
+——————-+——-+

示例中的服务器还是挺健康的。

七、查询缓存(query cache)

mysql> show global status like ‘qcache%‘;
+————————-+———–+
| variable_name            | value      |
+————————-+———–+
| qcache_free_blocks       | 22756      |
| qcache_free_memory       | 76764704  |
| qcache_hits              | 213028692 |
| qcache_inserts           | 208894227 |
| qcache_lowmem_prunes     | 4010916    |
| qcache_not_cached        | 13385031  |
| qcache_queries_in_cache | 43560      |
| qcache_total_blocks      | 111212     |
+————————-+———–+

mysql查询缓存变量解释:

qcache_free_blocks:缓存中相邻内存块的个数。数目大说明可能有碎片。flush query cache会对缓存中的碎片进行整理,从而得到一个空闲块。

qcache_free_memory:缓存中的空闲内存。

qcache_hits:每次查询在缓存中命中时就增大

qcache_inserts:每次插入一个查询时就增大。命中次数除以插入次数就是不中比率。

qcache_lowmem_prunes:缓存出现内存不足并且必须要进行清理以便为更多查询提供空间的次数。这个数字最好长时间来看;如果这个 数字在不断增长,就表示可能碎片非常严重,或者内存很少。(上面的 free_blocks和free_memory可以告诉您属于哪种情况)

qcache_not_cached:不适合进行缓存的查询的数量,通常是由于这些查询不是 select 语句或者用了now()之类的函数。

qcache_queries_in_cache:当前缓存的查询(和响应)的数量。

qcache_total_blocks:缓存中块的数量。

我们再查询一下服务器关于query_cache的配置:

mysql> show variables like ‘query_cache%‘;
+——————————+———–+
| variable_name                 | value      |
+——————————+———–+
| query_cache_limit             | 2097152    |
| query_cache_min_res_unit      | 4096       |
| query_cache_size              | 203423744 |
| query_cache_type              | on         |
| query_cache_wlock_invalidate | off        |
+——————————+———–+

各字段的解释:

query_cache_limit:超过此大小的查询将不缓存

query_cache_min_res_unit:缓存块的最小大小

query_cache_size:查询缓存大小

query_cache_type:缓存类型,决定缓存什么样的查询,示例中表示不缓存 select sql_no_cache 查询

query_cache_wlock_invalidate:当有其他客户端正在对myisam表进行写操作时,如果查询在query cache中,是否返回cache结果还是等写操作完成再读表获取结果。

query_cache_min_res_unit的配置是一柄”双刃剑”,默认是4kb,设置值大对大数据查询有好处,但如果你的查询都是小数据查询,就容易造成内存碎片和浪费。

查询缓存碎片率 = qcache_free_blocks / qcache_total_blocks * 100%

如果查询缓存碎片率超过20%,可以用flush query cache整理缓存碎片,或者试试减小query_cache_min_res_unit,如果你的查询都是小数据量的话。

查询缓存利用率 = (query_cache_size – qcache_free_memory) / query_cache_size * 100%

查询缓存利用率在25%以下的话说明query_cache_size设置的过大,可适当减小;查询缓存利用率在80%以上而且qcache_lowmem_prunes > 50的话说明query_cache_size可能有点小,要不就是碎片太多。

查询缓存命中率 = (qcache_hits – qcache_inserts) / qcache_hits * 100%

示例服务器 查询缓存碎片率 = 20.46%,查询缓存利用率 = 62.26%,查询缓存命中率 = 1.94%,命中率很差,可能写操作比较频繁吧,而且可能有些碎片。

八、排序使用情况

mysql> show global status like ‘sort%‘;
+——————-+————+
| variable_name      | value       |
+——————-+————+
| sort_merge_passes | 29          |
| sort_range         | 37432840    |
| sort_rows          | 9178691532 |
| sort_scan          | 1860569     |
+——————-+————+

sort_merge_passes 包括两步。mysql 首先会尝试在内存中做排序,使用的内存大小由系统变量 sort_buffer_size 决定,如果它的大小不够把所有的记录都读到内存中,mysql 就会把每次在内存中排序的结果存到临时文件中,等 mysql 找到所有记录之后,再把临时文件中的记录做一次排序。这再次排序就会增加 sort_merge_passes。实际上,mysql 会用另一个临时文件来存再次排序的结果,所以通常会看到 sort_merge_passes 增加的数值是建临时文件数的两倍。因为用到了临时文件,所以速度可能会比较慢,增加 sort_buffer_size 会减少 sort_merge_passes 和 创建临时文件的次数。但盲目的增加 sort_buffer_size 并不一定能提高速度,见 how fast can you sort data with mysql?(引自http://qroom.blogspot.com/2007/09/mysql-select-sort.html,貌似被墙)

另外,增加read_rnd_buffer_size(3.2.3是record_rnd_buffer_size)的值对排序的操作也有一点的好处,参见:http://www.mysqlperformanceblog.com/2007/07/24/what-exactly-is-read_rnd_buffer_size/

九、文件打开数(open_files)

mysql> show global status like ‘open_files‘;
+—————+——-+
| variable_name | value |
+—————+——-+
| open_files     | 1410  |
+—————+——-+

mysql> show variables like ‘open_files_limit‘;
+——————+——-+
| variable_name     | value |
+——————+——-+
| open_files_limit | 4590  |
+——————+——-+

比较合适的设置:open_files / open_files_limit * 100% <= 75%

十、表锁情况

mysql> show global status like ‘table_locks%‘;
+———————–+———–+
| variable_name          | value      |
+———————–+———–+
| table_locks_immediate | 490206328 |
| table_locks_waited     | 2084912    |
+———————–+———–+

table_locks_immediate表示立即释放表锁数,table_locks_waited表示需要等待的表锁数,如果 table_locks_immediate / table_locks_waited > 5000,最好采用innodb引擎,因为innodb是行锁而myisam是表锁,对于高并发写入的应用innodb效果会好些。示例中的服务器 table_locks_immediate / table_locks_waited = 235,myisam就足够了。

十一、表扫描情况

mysql> show global status like ‘handler_read%‘;
+———————–+————-+
| variable_name          | value        |
+———————–+————-+
| handler_read_first     | 5803750      |
| handler_read_key       | 6049319850  |
| handler_read_next      | 94440908210 |
| handler_read_prev      | 34822001724 |
| handler_read_rnd       | 405482605    |
| handler_read_rnd_next | 18912877839 |
+———————–+————-+

各字段解释参见http://hi.baidu.com/thinkinginlamp/blog/item/31690cd7c4bc5cdaa144df9c.html,调出服务器完成的查询请求次数:

mysql> show global status like ‘com_select‘;
+—————+———–+
| variable_name | value      |
+—————+———–+
| com_select     | 222693559 |
+—————+———–+

计算表扫描率:

表扫描率 = handler_read_rnd_next / com_select

如果表扫描率超过4000,说明进行了太多表扫描,很有可能索引没有建好,增加read_buffer_size值会有一些好处,但最好不要超过8mb。