Fork me on GitHub

Java开源APM概要

Published on:

候选APM

韩国的一个公司开源的,有待评估使用情况,就是整体还不是JDK8,有些还是有点费劲,技术上采用agent的方式,对java友好

看接入的公司还是挺多的,个人感觉是点评名气还可以,但是搭建起来有点费劲,很多东西都写死配置了,不灵活。整体设计的话,由于没有采用agent的方式,采用的是api手工埋点的方式,跟SNG的很像,好处的是跨语言,不好的地方就是对java来说用起来还需要包装一下

开发团队加入了OneAPM,目前看使用的公司不多,整体技术采用agent方式,对java友好。提供了对dubbo等的支持,属于soa时代的产品

技术架构

pinpoint

CAT

skywalking

简要评价

从技术架构上看,对于log的存储都使用了hbase,也都是自己实现了日志/监控数据的上报。pinpoint支持udp的方式,这个好一点。这类还是有点SOA时代的痕迹,更为符合大数据时代的做法是,监控数据丢给kafka,然后监控server来消费数据即可,这一点在cat中使用了consumer有点这个味道,但是没有彻底转型过来。

展望

APM整体的功能结构,主要是 1.日志追踪,2.监控报警 3.性能统计。对于日志追踪,已经有spirng cloud zipkin了,这个对spring cloud体系结合的很好,确的就是监控报警和性能统计,可以采用agent的方式进行无侵入的监控,或者采用log appender的方式到kafka,之后再进行error的监控报警,以及把performance的数据log到日志,发送到kafka来进行统计。

docs

转自原文地址:https://segmentfault.com/a/1190000006817114

跨库分页-架构技术实践

Published on:
Tags: rdb-paging

文章来源:http://gitbook.cn/books/58a98f512bd83c246b6b8866/index.html 作者:@58沈剑 说明:文章转自沈老板的文章,分析的很不错

一、需求缘起

分页需求

互联网很多业务都有分页拉取数据的需求,例如:

  • 微信消息过多时,拉取第N页消息。
  • 京东下单过多时,拉取第N页订单。
  • 浏览58同城,查看第N页帖子。 这些业务场景对应的消息表,订单表,帖子表分页拉取需求有这样一些特点:
  • 有一个业务主键id,例如msg_id,order_id,tiezi_id
  • 分页排序是按照非业务主键id来排序的,业务中经常按照时间time来排序order by

在数据量不大时,可以通过在排序字段time上建立索引,利用SQL提供的offset/limit功能就能满足分页查询需求:

select * from t_msg order by time offset 200 limit 100   
select * from t_order order by time offset 200 limit 100   
select * from t_tiezi order by time offset 200 limit 100

此处假设一页数据为100条,均拉取第3页数据。

分库需求

高并发大流量的互联网架构,一般通过服务层来访问数据库,随着数据量的增大,数据库需要进行水平切分,分库后将数据分布到不同的数据库实例(甚至物理机器)上,以达到降低数据量,增加实例数的扩容目的。

一旦涉及分库,逃不开“分库依据”patition key的概念,使用哪一个字段来水平切分数据库呢:大部分的业务场景,会使用业务主键id。

确定了分库依据patition key后,接下来要确定的是分库算法:大部分的业务场景,会使用业务主键id取模的算法来分库,这样即能够保证每个库的数据分布是均匀的,又能够保证每个库的请求分布是均匀的,实在是简单实现负载均衡的好方法,此法在互联网架构中应用颇多。

举一个更具体的例子:

用户库user,水平切分后变为两个库,分库依据patition key是uid,分库算法是uid取模:uid%2余0的数据会落到db0,uid%2余1的数据会落到db1。

问题的提出

仍然是上述用户库的例子,如果业务要查询“最近注册的第3页用户”,该如何实现呢?单库上,可以select * from t_user order by time offset 200 limit 100,变成两个库后,分库依据是uid,排序依据是time,数据库层失去了time排序的全局视野,数据分布在两个库上,此时该怎么办呢?

如何满足“跨越多个水平切分数据库,且分库依据与排序依据为不同属性,并需要进行分页”的查询需求,实现select*from T order by time offset X limit Y的跨库分页SQL,是本文将要讨论的技术问题。

二、全局视野法

如上图所述,服务层通过uid取模将数据分布到两个库上去之后,每个数据库都失去了全局视野,数据按照time局部排序之后,不管哪个分库的第3页数据,都不一定是全局排序的第3页数据。

那到底哪些数据才是全局排序的第3页数据呢,暂且分三种情况讨论。

(1)极端情况,两个库的数据完全一样

如果两个库的数据完全相同,只需要每个库offset一半,再取半页,就是最终想要的数据(如上图中粉色部分数据)。

(2)极端情况,结果数据来自一个库

也可能两个库的数据分布及其不均衡,例如db0的所有数据的time都大于db1的所有数据的time,则可能出现:一个库的第3页数据,就是全局排序后的第3页数据(如上图中粉色部分数据)。

(3)一般情况,每个库数据各包含一部分

正常情况下,全局排序的第3页数据,每个库都会包含一部分(如上图中粉色部分数据)。

由于不清楚到底是哪种情况,所以必须每个库都返回3页数据,所得到的6页数据在服务层进行内存排序,得到数据全局视野,再取第3页数据,便能够得到想要的全局分页数据。

再总结一下这个方案的步骤:

  • order by time offset X limit Y,改写成order by time offset 0 limit X+Y
  • 服务层将改写后的SQL语句发往各个分库:即例子中的各取3页数据。
  • 假设共分为N个库,服务层将得到N*(X+Y)条数据:即例子中的6页数据。
  • 服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录,就是全局视野所需的一页数据。

方案优点:通过服务层修改SQL语句,扩大数据召回量,能够得到全局视野,业务无损,精准返回所需数据。

方案缺点(显而易见):

  • 每个分库需要返回更多的数据,增大了网络传输量(耗网络);
  • 除了数据库按照time进行排序,服务层还需要进行二次排序,增大了服务层的计算量(耗CPU);
  • 最致命的,这个算法随着页码的增大,性能会急剧下降,这是因为SQL改写后每个分库要返回X+Y行数据:返回第3页,offset中的X=200;假如要返回第100页,offset中的X=9900,即每个分库要返回100页数据,数据量和排序量都将大增,性能平方级下降。

三、业务折衷法

“全局视野法”虽然性能较差, 但其业务无损,数据精准,不失为一种方案,有没有性能更优的方案呢?

“任何脱离业务的架构设计都是耍流氓”,技术方案需要折衷,在技术难度较大的情况下,业务需求的折衷能够极大的简化技术方案。

业务折衷一:禁止跳页查询

在数据量很大,翻页数很多的时候,很多产品并不提供“直接跳到指定页面”的功能,而只提供“下一页”的功能,这一个小小的业务折衷,就能极大的降低技术方案的复杂度。

如上图,不能够跳页,那么第一次只能够查询第一页:

  • 将查询order by time offset 0 limit 100,改写成order by time where time>0 limit 100
  • 上述改写和offset 0 limit 100的效果相同,都是每个分库返回了一页数据(上图中粉色部分)。

  • 服务层得到2页数据,内存排序,取出前100条数据,作为最终的第一页数据,这个全局的第一页数据,一般来说每个分库都包含一部分数据(如上图粉色部分)。

咦,这个方案也需要服务器内存排序,岂不是和“全局视野法”一样么?第一页数据的拉取确实一样,但每一次“下一页”拉取的方案就不一样了。

点击“下一页”时,需要拉取第二页数据,在第一页数据的基础之上,能够找到第一页数据time的最大值:

这个上一页记录的time_max,会作为第二页数据拉取的查询条件:

  • 将查询order by time offset 100 limit 100,改写成order by time where time>$time_max limit 100

  • 这下不是返回2页数据了(“全局视野法,会改写成offset 0 limit 200”),每个分库还是返回一页数据(如上图中粉色部分)。

  • 服务层得到2页数据,内存排序,取出前100条数据,作为最终的第2页数据,这个全局的第2页数据,一般来说也是每个分库都包含一部分数据(如上图粉色部分)。

如此往复,查询全局视野第100页数据时,不是将查询条件改写为offset 0 limit 9900+100(返回100页数据),而是改写为time>$time_max99 limit 100(仍返回一页数据),以保证数据的传输量和排序的数据量不会随着不断翻页而导致性能下降。

业务折衷二:允许数据精度损失

“全局视野法”能够返回业务无损的精确数据,在查询页数较大,例如第100页时,会有性能问题,此时业务上是否能够接受,返回的100页不是精准的数据,而允许有一些数据偏差呢?

数据库分库-数据均衡原理

使用patition key进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有非patition key属性,在各个分库上的数据分布,统计概率情况应该是一致的。

例如,在uid随机的情况下,使用uid取模分两库,db0和db1:

  • 性别属性,如果db0库上的男性用户占比70%,则db1上男性用户占比也应为70%;
  • 年龄属性,如果db0库上18-28岁少女用户比例占比15%,则db1上少女用户比例也应为15%;
  • 时间属性,如果db0库上每天10:00之前登录的用户占比为20%,则db1上应该是相同的统计规律;

利用这一原理,要查询全局100页数据,offset 9900 limit 100改写为offset 4950 limit 50,每个分库偏移4950(一半),获取50条数据(半页),得到的数据集的并集,基本能够认为,是全局数据的offset 9900 limit 100的数据,当然,这一页数据的精度,并不是精准的。

根据实际业务经验,用户都要查询第100页网页、帖子、邮件的数据了,这一页数据的精准性损失,业务上往往是可以接受的,但此时技术方案的复杂度便大大降低列,既不需要返回更多的数据,也不需要进行服务内存排序了。

四、终极武器:二次查询法

有没有一种技术方案,即能够满足业务的精确需要,无需业务折衷,又高性能的方法呢?这就是接下来要介绍的终极武器:“二次查询法”。

为了方便举例,假设一页只有5条数据,查询第200页的SQL语句为select*from T order by time offset 1000 limit 5

步骤一:查询改写

select*from T order by time offset 1000 limit 5改写为select*from T order by time offset 500 limit 5并投递给所有的分库,注意,这个offset的500,来自于全局offset的总偏移量1000,除以水平切分数据库个数2。

如果是3个分库,则可以改写为select*from T order by time offset 333 limit 5,假设这三个分库返回的数据(time, uid)如下:

可以看到,每个分库都是返回的按照time排序的一页数据。

步骤二:找到所返回3页全部数据的最小值

  • 第一个库,5条数据的time最小值是1487501123
  • 第二个库,5条数据的time最小值是1487501133
  • 第三个库,5条数据的time最小值是1487501143

故,三页数据中,time最小值来自第一个库,time_min=1487501123,这个过程只需要比较各个分库第一条数据,时间复杂度很低。

步骤三:查询二次改写

第一次改写的SQL语句是select*from T order by time offset 333 limit 5。第二次要改写成一个between语句,between的起点是time_min,between的终点是原来每个分库各自返回数据的最大值:

第一个分库,第一次返回数据的最大值是1487501523;所以查询改写为select*from T order by time where time between time_min and 1487501523

第二个分库,第一次返回数据的最大值是1487501323;所以查询改写为select*from T order by time where time between time_min and 1487501323

第三个分库,第一次返回数据的最大值是1487501553;所以查询改写为select*from T order by time where time between time_min and 1487501553

相对第一次查询,第二次查询条件放宽了,故第二次查询会返回比第一次查询结果集更多的数据,假设这三个分库返回的数据(time, uid)如下:

可以看到:

  • 由于time_min来自原来的分库一,所以分库一的返回结果集和第一次查询相同(所以其实这次查询是可以省略的);
  • 分库二的结果集,比第一次多返回了1条数据,头部的1条记录(time最小的记录)是新的(上图中粉色记录);
  • 分库三的结果集,比第一次多返回了2条数据,头部的2条记录(time最小的2条记录)是新的(上图中粉色记录)。

步骤四:在每个结果集中虚拟一个time_min记录,找到time_min在全局的offset

  • 在第一个库中,time_min在第一个库的offset是333;
  • 在第二个库中,(1487501133, uid_aa)的offset是333(根据第一次查询条件得出的),故虚拟time_min在第二个库的offset是331;
  • 在第三个库中,(1487501143, uid_aaa)的offset是333(根据第一次查询条件得出的),故虚拟time_min在第三个库的offset是330。

综上,time_min在全局的offset是333+331+330=994。

步骤五:既然得到了time_min在全局的offset,就相当于有了全局视野,根据第二次的结果集,就能够得到全局offset 1000 limit 5的记录

第二次查询在各个分库返回的结果集是有序的,又知道了time_min在全局的offset是994,一路排下来,容易知道全局offset 1000 limit 5的一页记录(上图中黄色记录)。

是不是非常巧妙?这种方法的优点是:可以精确的返回业务所需数据,每次返回的数据量都非常小,不会随着翻页增加数据的返回量。

不足是:需要进行两次数据库查询。

五、总结

今天分享了解决“夸N库分页”这一技术难题的四种方法,稍作总结:

方法一:全局视野法

  • order by time offset X limit Y,改写成order by time offset 0 limit X+Y
  • 服务层对得到的N*(X+Y)条数据进行内存排序,内存排序后再取偏移量X后的Y条记录。

方法二:业务折衷法-禁止跳页查询

  • 用正常的方法取得第一页数据,并得到第一页记录的time_max。
  • 每次翻页,将order by time offset X limit Y,改写成order by time where time>$time_max limit Y以保证每次只返回一页数据,性能为常量。

方法三:业务折衷法-允许模糊数据

  • order by time offset X limit Y,改写成order by time offset X/N limit Y/N

方法四:二次查询法

  • order by time offset X limit Y,改写成order by time offset X/N limit Y
  • 找到最小值time_min;
  • between二次查询,order by time between $$time_min and $time_i_max
  • 设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset;
  • 得到了time_min在全局的offset,自然得到了全局的offset X limit Y。

BTrace使用笔记

Published on:
Tags: btrace

BTrace是什么?

Btrace是由sundararajan在2009年6月开发的一个开源项目,是一种动态跟踪分析一个运行中的Java应用程序的工具。 BTrace是一个为Java平台开发的安全、动态的追踪工具。BTrace动态地向目标应用程序的字节码注入追踪代码(字节码追踪),这些追踪字节码追踪代码使用Java语言表达,也就是BTrace的脚本。

BTrace能做什么?

BTrace可以用来帮我们做运行时的JAVA程序分析,监控等等操作,BTrace也有一些使用上的限制,如:不能在脚本中新建类等。 Btrace是通过Attach API中提供的VirtualMachine.attach(PID)方法来获得要监控的JVM,然后使用VirtualMachine.loadAgent(”*.jar”)方法来加载jar文件。

特别注意

BTrace植入过的代码,会一直在,直到应用重启为止。所以即使Btrace退出了,业务函数每次执行时都会执行Btrace植入的代码

Btrace术语

Probe Point(探测点) 追踪语句(或者一组追踪语句)被触发执行的“位置”或“事件”。也就是我们想要执行一些追踪语句的“位置”或“事件”。 Trace Actions or Actions(追踪动作) probe被触发时,执行的追踪语句。 Action Methods(动作方法) 我的理解是定义追踪动作的方法,当然根据官方的说明这个方法应该是静态的。 在静态方法中定义probe触发所调用的trace语句,那么这种定义了trace脚本的静态方法就是”动作方法”

BTrace程序结构

一个BTrace程序是其实就是一个普通的java类,特别之处就是由一个或者多个被(public static void)组合修饰的方法并且这些方法被BTrace对应的annotations注解。注解用来指出被追踪程序的位置(probe point)。追踪动作须书写在静态方法体中,也就是action方法(可以有多个action方法)。

BTrace约束

为了保证追踪动作是“只读”的(也就是这些动作不可以修改被追踪程序的状态)和有限度的(比如在固定时间里结束)。一个BTrace程序只允许完成一些指定的动作。下面是BTrace一些不可以完成的事情:

  • 不能创建新的对象
  • 不能创建新的数组
  • 不能抛出异常
  • 不能捕获异常
  • 不能进行任何的实例函数或者静态函数 – 只有com.sun.btrace.BTraceUtils类中的静态函数或者BTrace程序自己声明的函数才可以被BTrace调用
  • 不可以在目标程序的类,或者对象的静态或者实例级别的field进行赋值。但是,BTrace自身的类是可以给它的静态field进行赋值的
  • 不能有outer,inner,嵌套的或者本地类。
  • 不能有同步代码块或者同步的函数
  • 不能有循环语句(for,while, do..while)
  • 不能继承其它类(父类只能是java.lang.Object)
  • 不能实现接口
  • 不能包含断言(assert)语句
  • 不能使用类字面值

这上面的种种限制可以通过一个配置改变:unsafe=true,在使用BTrace注解时修改该属性的默认值(false)为true,即@BTrace(unsafe=true);也可以启动选项中显式声明-Dcom.sun.btrace.unsafe=true(响应也有-u参数);现在你可以为所欲为了。BUT,这样做之前最好考虑好风险并再三检查脚本,请斟酌使用!

BTrace安装

btrace git下载地址 下载下来直接解压就可以使用

基本语法

btrace <pid> <btrace-script>脚本

btrace命令行工具运行命令如下:

btrace <options> <pid> <btrace source or .class file> <btrace arguments>
常用选项:
[-I <include-path>] [-p <port>] [-cp <classpath>]

参数说明:

where possible options include:
  --version             Show the version
  -v                    Run in verbose mode
  -o <file>             The path to store the probe output (will disable showing the output in console)
  -u                    Run in trusted mode
  -d <path>             Dump the instrumented classes to the specified path
  -pd <path>            The search path for the probe XML descriptors
  -classpath <path>     Specify where to find user class files and annotation processors
  -cp <path>            Specify where to find user class files and annotation processors
  -I <path>             Specify where to find include files
  -p <port>             Specify port to which the btrace agent listens for clients
  -statsd <host[:port]> Specify the statsd server, if any
  • include-path : 是一些用来查找头文件的目录。BTrace包含一个简单的预处理,支持# define,# + include和条件编译。它不像一个完整的C / c++预处理器–而是一个有用的子集。详见demo代码“ThreadBean.java”,如果没有显式的声明选项-I,Btrace跳过预处理程序调用步骤。
  • port: BTrace代理程序所侦听的端口,这是可选的选项。默认是2020
  • classpath: 是一些用来查找jar文件的目录。默认是当前目录”.”
  • pid:是要追踪目标程序id
  • btrace-script: 就是追踪程序本身。如果这是个java文件,那么提交前会进行编译。否则,它被认为已预编译(即它必须是一个类)并提交
  • arguments: 这是传递给BTrace程序的参数。BTrace程序可以通过内置的符号来引用这些参数,length是这些参数的个数。

在samples目录下有很多示例,并且有的跟踪很有用可直接使用,下来让我们编写一个脚本来看一下具体是怎么使用的

BTrace的注解

方法注解

  • @com.sun.btrace.annotations.OnMethod 该注解可用来指定目标类,目标方法,以及目标方法里的“位置”。加了该注解后的操作方法会在对应的方法运行到指定的“位置”时被执行。这该注解中,目标类用“clazz”属性来指定,而目标方法用“method”属性来指定。”clazz”可以是类的全路径(比如java.awt.Component或者用两个反斜杠中间的正则表达式,参考例子NewComponent.java和Classload.java来看它们的用法,正则表达式可以匹配0个或多个目标类,这个时候多个类都会被进行动态指令更换。如/java.awt.+/匹配java.awt包下的所有类)。方法名也可以用这样的正则表达式 来匹配零个或者多个多个方法。参考例子MultiClass.java来查看用法。 还有一种方法来指定追踪类和函数。被追踪的类和函数可以用注解来指定。比如,如果”clazz”属性是@javax.jws.Webservice.那么BTrace会会把所有注解是这个的函数都进行动态指令更换。类似地,方法级别的注解也可以用来执行方法。参看例子WebServiceTracker.java来了解如何使用。可以把正则表达式和注解放在一起用,比如@/com.acme..+/可以匹配任何类,只要这个类的注解能跟那段正则表达式匹配即可。可以通过指定父类来匹配多个类名,比如+java.lang.Runnable就可以匹配所有实现了java.lang.Runnable这个接口的类。参考例子SubtypeTracer.java来看它的用法。
  • @com.sun.btrace.annotations.OnTimer 该注解可以用来执行那些需要周期性(间隔是毫秒)的追踪操作。参考Histogram.java来看它的用法。
  • @com.sun.btrace.annotations.OnError 该注解可以用来指定当任何异常抛出时需要执行的操作。被该注解修饰后的BTrace函数会在同一个BTrace类的其他操作方法抛出异常时执行。
  • @com.sun.btrace.annotations.OnExit 该注解用来执行党BTrace代码调用了exit(int)结束追踪会话后需要执行的操作。参考例子ProbeExit.java来了解如何使用。
  • @com.sun.btrace.annotations.OnEvent 该注解用来追踪函数与”外部”的事件关联起来。当BTrace客户端发送了一个“事件”后,该注解里的操作就会被执行。客户端发送的事件可能是由用户触发的(比如按下Ctrl-C)。事件的名字是个字符串,这样追踪操作就只会在对应的事件触发后被执行。到目标为止,BTrace命令行客户端会在用户按下Ctrl-C后发送事件,参考例子HistoOnEvent.java来了解用法。
  • @com.sun.btrace.annotations.OnLowMemory 该注解可以用来追踪特定内存阈值被用光的事件。参看例子MemAlerter.java了解用法。
  • @com.sun.btrace.annotations.OnProbe 该注解可以用来避免使用BTrace脚本的内部类。@OnProbe探测点被映射到一个或多个@OnMethod上。目前这个映射是通过一个XML探测描述文件类指定的(这个文件会被BTrace代理所使用)。参考例子SocketTracker1.java和对应的描述文件java.net.socket.xml.当运行这个例子时,xml文件需要放在目标JVM所有运行的目录下(或者修改btracer.bat中的probeDescPath选项来指向任意的xml文件)。
  • @com.sun.btrace.annotations.Location:该注解在一个traced/probed方法中指定一个特定的“位置”
  • @com.sun.btrace.annotations.Simpled:标记@OnMethod注解处理器采样。采样处理程序时并不是所有的事件将被追踪,只有一个统计样品与给定的意思。在默认情况下使用一种自适应采样。BTrace将增加或减少样品之间的调用数量保持平均时间窗口,因此减少整体的开销。

参数相关的注解

  • @com.sun.btrace.annotations.Self:该注解把一个参数标识为保留了目标函数所指向的this的值。参考例子AWTEventTracer.java和AllCalls1.java.
  • @com.sun.btrace.annotations.Return:该注解说明这个参数保存目标函数的返回值。参考例子Classload.java
  • @com.sun.btrace.annotations.ProbeClassName:所修饰的参数保留了探测类的类名 。参看AllMethods.java(有多个探测类)
  • @com.sun.btrace.annotations.ProbeMethodName:所修饰的参数保留了探测函数的函数名。参考WebServiceTracker.java(多个探测函数)
  • @com.sun.btrace.annotations.TargetInstance:修饰的参数保留了被调用的实例。参考例子AllCall2.java.
  • @com.sun.btrace.annotations.TargetMethodOrField:该注解修饰的参数保存了调用的函数名。参考AllCalls1.java 和AllCall2.java
  • @com.sun.btrace.annotations.Duration:探测方法参数标记为持续时间值的接收者,即目标方法执行的时间,单位纳秒。只是用带Location属性的@OnMethod,并且需要配合Kind.ERROR或者Kind.RETURN使用

无注解的参数

没有注解的BTrace探测函数参数是用来作签名匹配的,因为他们必须必须在固定的位置上出现。然而,它们可以和其他的注解的参数进行交换。如果一个参数的类型是_AnyType[]_,它就会“吃”掉所所有剩下的参数。没有注解的参数的具体含义与他们所在的位置有关:

名称 作用
Kind.ARRAY_GET 数组元素加载
Kind.ARRAY_SET 数组元素存储
Kind.CALL 方法调用
Kind.CATCH 异常捕获
Kind.CHECKCAST checkcast
Kind.ENTRY 方法进入。意指进入匹配probe点,跟你@Location设置的clazz和method没有任何关系
Kind.ERROR 错误,异常没有捕获,返回
Kind.FIELD_GET field获取
Kind.FIELD_SET field设置
Kind.INSTANCEOF 实例检测
Kind.LINE 源代码行号
Kind.NEW 创建新实例
Kind.NEWARRAY 新的数组对象被创建
Kind.RETURN 意指从某个匹配probe的方法中调用了匹配A class method的点,一定要和clazz,method配合使用。clazz和method的默认值为”“,所以不能被匹配
Kind.SYNC_ENTRY 进入一个同步方法锁
Kind.SYNC_EXIT 离开一个同步方法锁
Kind.THROW 抛出异常

字段相关的注解

  • @com.sun.btrace.annotations.Export BTrace字段使用该注解来说明它已经被映射到一个jvmstat计数器上。使用该注解,BTrace程序可以把追踪计数器暴露给外部的jvmstat客户端(比如jstat)。参考例子ThreadCounter.java
  • @com.sun.btrace.annotations.Property该注解可以把一个字段标识为一个MBean属性。如果一个BTrace类至少有一个静态的字段使用了该注解。那么一个MBean就会被创建并且注册到平台MBean服务器上。JMX客户端比如VisualVM,jconsole可以访问这个字段来查看BTrace的MBean。在把BTrace附加到目标程序上后,你可以把VisualVM或者jconsole也附加到同一个目标程序上来查看刚创建好的MBean属性。通过VisualVM或者jconsole,你可以通过MBeans tab页来查看BTrace相关的域,然后查看它们的值。参考例子ThreadCounterBean.java 和HistogramBean.java来了解用法
  • @com.sun.btrace.annotations.TLS BTrace字段使用该注解来说明它自己是一个线程本地字段(thread local field).注意你只能在@OnMethod注解后的函数里访问这样的字段。每个Java线程都有一个这个字段的拷贝。为了让这样的方式能够工作,这个字段的类型只能是immutable(比如原始类型) 或者是cloneable (实现了Cloneable接口并且覆盖了clone()函数)的。这些线程本地字段可以被BTrace程序用来识别它是否在同一个线程里执行了多个探测操作。参考例子OnThrow.java和WebServiceTracker.java

类相关的注解

  • @com.sun.btrace.annotations.DTrace该注解用来把一小段D脚本(嵌在BTrace 的java类中)和BTrace程序关联起来。参考例子DTraceInline.java
  • @com.sun.btrace.annotations.DTraceRef 和上个注解一样,不同的是D脚本是在独立的文件中,不是嵌在java类中。
  • @com.sun.btrace.annotations.BTrace必须使用该注解来指定一个Java类是BTrace程序。BTrace编译器会强制查找该注解,BTrace代理也会检查这个是否有该注解。如果没有,则提示错误,并且不会执行。

脚本编写


package btrace;

import com.sun.btrace.BTraceUtils;
import com.sun.btrace.annotations.*;

@BTrace
public class UniqueIdMgrBtrace {
    @OnMethod(clazz = "com.atomikos.util.UniqueIdMgr", method = "get", location = @Location(Kind.RETURN))
    public static void onGet(@Return String result) {
        long millis = BTraceUtils.timeMillis();
        String threadName = BTraceUtils.Threads.name(BTraceUtils.currentThread());
        String str = BTraceUtils.strcat(BTraceUtils.str(millis), " - [");
        str = BTraceUtils.strcat(str, BTraceUtils.str(threadName));
        str = BTraceUtils.strcat(str, "] - com.atomikos.util.UniqueIdMgr.get()-->");
        str = BTraceUtils.strcat(str, BTraceUtils.str(result));
        BTraceUtils.println(BTraceUtils.str(str));
    }

    @OnMethod(clazz = "com.atomikos.icatch.imp.TransactionServiceImp", method = "setTidToTx")
    public static void onSetTidToTx(String tid) {
        long millis = BTraceUtils.timeMillis();
        String threadName = BTraceUtils.Threads.name(BTraceUtils.currentThread());
        String str = BTraceUtils.strcat(BTraceUtils.str(millis), " - [");
        str = BTraceUtils.strcat(str, BTraceUtils.str(threadName));
        str = BTraceUtils.strcat(str, "] - com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx(");
        str = BTraceUtils.strcat(str, BTraceUtils.str(tid));
        str = BTraceUtils.strcat(str, ")");
        BTraceUtils.println(BTraceUtils.str(str));
    }
}

上面代码意思是在com.atomikos.util.UniqueIdMgr.get()方法上面进行跟踪返回值,要跟踪赶回值必须要加@Location(Kind.RETURN)),才能使用参数的@Return

如果要使用方法参数,可以在脚本方法上直接写跟踪的原始方法参数并且类型保持一样,例如:

package com.btrace;
//需要跟踪的类
public class RemoteClass {

    public String f1(String a, int b) {
        System.out.println(a + " " + b);
        return a;
    }
}

//btrace脚本
@BTrace public class HelloBtrace {

  @OnMethod(
    clazz="com.btrace.RemoteClass",
    method="f1"
  ) 
  public static void onF1() {
    println("Hello BTrace");
  }

  @OnMethod(
    clazz="com.btrace.RemoteClass",
    method="f1"
  ) 
  public static void onF2(String a,int b) {
    println(str(a));
    println(str(b));
    println("");
  }
}

注意事项

  1. 脚本中方法参数需要跟原方法参数类型保持一致
  2. 脚本中不允许使用除btrace之外的类,拼接字符串使用BTraceUtils.strcat(),打印使用BTraceUtils.println(),获取线程使用BTraceUtils.Threads
  3. BTrace植入过的代码,会一直在,直到应用重启为止。所以即使Btrace退出了,业务函数每次执行时都会执行Btrace植入的代码

ActiveMQ发送速度慢问题排查

Published on:

目录:

  1. 关于使用发送消息给activemq的同步/异步发送问题需要注意
  2. 同步/异步发送使用场景
  3. maxConnections配置问题注意事项
  4. idleTimeout配置问题注意事项
  5. 关于Failover的问题

关于使用发送消息给activemq的同步/异步发送问题需要注意

activemq发送异步参数:useAsyncSend与发送超时参数:sendTimeout是存在冲突的, 1. 当useAsyncSend=true,没有sendTimeout参数时(sendTimeout默认值0秒),走异步发送 2. 当useAsyncSend=false,没有sendTimeout参数时(sendTimeout默认值0秒),走同步发送 3. 当useAsyncSend=true,sendTimeout=1000,优先根据sendTimeout参数走同步发送

同步/异步发送使用场景

场景一:业务可以容忍消息丢失(日志记录)这样的场景使用: 使用:异步发送 配置:useAsyncSend=true,sendTimeout不配置(sendTimeout默认值0秒) 注意:可以不需要补偿机制

场景二:业务不能容忍消息丢失,这样的场景使用: 使用1:异步发送 配置1:useAsyncSend=true,sendTimeout不配置(sendTimeout默认值0秒) 注意1:当异步发送消息失败或异常导致消息丢失时有补偿的做法(如:定时任务、重发消息、等) 使用2:同步发送 配置2:useAsyncSend=false(useAsyncSend默认值false),sendTimeout=2000(超时时间一定要配置) 注意2:可以不需要补偿机制

场景三:业务必须将消息发送和jdbc事务放在一个事务内,保证数据的强一致性,这样的场景使用: 使用:同步发送 配置:useAsyncSend=false(useAsyncSend默认值false),sendTimeout=2000(超时时间一定要配置) 注意:消息发送的超时时间(sendTimeout)< jdbc事务超时时间

禁止使用的配置: 配置:useAsyncSend=false(useAsyncSend默认值false),sendTimeout不配置(sendTimeout默认值0秒) 注意:上面不配置超时时间的同步发送会造成请求阻塞在这里。

maxConnections配置问题注意事项

根据activemq的连接池实现代码,发现maxconnections不适合设置很大,除非并发非常高的情况下,因为现在activemq创建一个连接平均在1-2秒钟左右,根据activemq的连接实现发现

if (getConnectionsPool().getNumIdle(key) < getMaxConnections()) {
            try {
                connectionsPool.addObject(key);
                connection = mostRecentlyCreated.getAndSet(null);
                connection.incrementReferenceCount();
            } catch (Exception e) {
                throw createJmsException("Error while attempting to add new Connection to the pool", e);
            }
        } else {
            try {
                // We can race against other threads returning the connection when there is an
                // expiration or idle timeout.  We keep pulling out ConnectionPool instances until
                // we win and get a non-closed instance and then increment the reference count
                // under lock to prevent another thread from triggering an expiration check and
                // pulling the rug out from under us.
                while (connection == null) {
                    connection = connectionsPool.borrowObject(key);
                    synchronized (connection) {
                        if (connection.getConnection() != null) {
                            connection.incrementReferenceCount();
                            break;
                        }
                        // Return the bad one to the pool and let if get destroyed as normal.
                        connectionsPool.returnObject(key, connection);
                        connection = null;
                    }
                }
            } catch (Exception e) {
                throw createJmsException("Error while attempting to retrieve a connection from the pool", e);
            }
            try {
                connectionsPool.returnObject(key, connection);
            } catch (Exception e) {
                throw createJmsException("Error when returning connection to the pool", e);
            }
        }

当MaxConnections设置的很大的时候,会在发消息的时候一直判断池子中数量是否达到最大值,如果小于最大值再创建一个新的连接放入池子,这样就会前面发送消息的动作都会创建连接从而发送时间会增长。 比如:MaxConnections=20,发送消息50次,前20次都会去创建连接并且发送,后面30次会去复用连接池内的连接

idleTimeout配置问题注意事项

空闲时间配置问题,activemq默认idleTimeout=30秒,activemq开启failover的话它的连接创建时间相对较长,因此建议这个时间设置大一些,尽量不要让超时清空掉,提高复用率

关于Failover的问题

activemq开启failover策略会根据配置的连接串中的tpc ip按顺序迭代去检测可用来创建连接,当可用的连接排在第一个的时候他的创建连接时间相比可用连接排在后面的时间短一些。 但是我们现在单个连接的时间耗时确实很高,这个问题不太清楚具体是什么问题,如下是创建连接耗时日志: 不开启failover的日志

耗时0:945ms
耗时1:1040ms
耗时2:595ms
耗时3:853ms
耗时4:716ms
耗时5:0ms
耗时6:0ms
耗时7:0ms

开启failover,可用连接排在第一位置,的日志

耗时0:2689ms
2017-11-03 18:47:20.599 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时1:1944ms
2017-11-03 18:47:22.615 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时2:1968ms
2017-11-03 18:47:24.724 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时3:2079ms
2017-11-03 18:47:25.318 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时4:608ms
耗时5:0ms
耗时6:0ms
耗时7:0ms

开启failover,可用连接排在最后的位置,的日志

耗时0:1960ms
2017-11-03 18:49:14.991 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时1:2084ms
2017-11-03 18:49:16.661 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时2:1775ms
2017-11-03 18:49:17.397 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时3:708ms
2017-11-03 18:49:18.066 [ActiveMQ Task-1] INFO  o.a.activemq.transport.failover.FailoverTransport - Successfully connected to tcp://10.51.232.238:61616
耗时4:864ms
耗时5:3ms
耗时6:0ms
耗时7:0ms

以上创建连接包括vpn加密的过程,可能会影响时间。 ps.前五个是创建连接,因为我配置的5个连接数,后面都是连接复用,异步发送

atomikos jta(xa) transaction问题:Already mapped: xxxx

Published on:

目录:

  1. 问题现象
  2. 问题分析
  3. 修改验证
  4. 解决方案
  5. 总结

问题现象

库存中心在压测过程中会时不时的报错,错误如下:

2017-11-02 11:38:37.620 [DubboServerHandler-10.27.69.168:20888-thread-156] ERROR xx.xx.inv.service.impl.OptionApiImpl - java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559
xx.xx.exception.BizException: java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559
        at xx.xx.inv.service.impl.OptionApiImpl.invWmsOption(OptionApiImpl.java:290) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]
        at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) [na:2.5.3]
        at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.AccessLogFilter.invoke(AccessLogFilter.java:199) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:60) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:112) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:108) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52) [dubbo-2.5.3.jar:2.5.3]
        at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82) [dubbo-2.5.3.jar:2.5.3]
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_79]
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_79]
        at java.lang.Thread.run(Thread.java:745) [na:1.7.0_79]
Caused by: java.lang.IllegalStateException: Already mapped: 10.27.69.168.tm150959391756909559
        at com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx(TransactionServiceImp.java:191) ~[transactions-4.0.0.jar:na]
        at com.atomikos.icatch.imp.TransactionServiceImp.createCT(TransactionServiceImp.java:277) ~[transactions-4.0.0.jar:na]
        at com.atomikos.icatch.imp.TransactionServiceImp.createCompositeTransaction(TransactionServiceImp.java:783) ~[transactions-4.0.0.jar:na]
        at com.atomikos.icatch.imp.CompositeTransactionManagerImp.createCompositeTransaction(CompositeTransactionManagerImp.java:393) ~[transactions-4.0.0.jar:na]
        at com.atomikos.icatch.jta.TransactionManagerImp.begin(TransactionManagerImp.java:271) ~[transactions-jta-4.0.0.jar:na]
        at com.atomikos.icatch.jta.TransactionManagerImp.begin(TransactionManagerImp.java:249) ~[transactions-jta-4.0.0.jar:na]
        at com.atomikos.icatch.jta.UserTransactionImp.begin(UserTransactionImp.java:72) ~[transactions-jta-4.0.0.jar:na]
        at org.springframework.transaction.jta.JtaTransactionManager.doJtaBegin(JtaTransactionManager.java:874) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.transaction.jta.JtaTransactionManager.doBegin(JtaTransactionManager.java:831) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:447) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:277) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) ~[spring-tx-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
        at xx.xx.inv.service.impl.VoucherExecutor$$EnhancerBySpringCGLIB$$a5e2dd9c.doWms(<generated>) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]
        at xx.xx.inv.service.impl.OptionApiImpl.invWmsOption(OptionApiImpl.java:286) ~[inv-api-impl-1.0.1-SNAPSHOT.jar:na]
        ... 30 common frames omitted

问题分析

跟踪源码:com.atomikos.icatch.imp.TransactionServiceImp.setTidToTx()

private void setTidToTx ( String tid , CompositeTransaction ct )
        throws IllegalStateException
{
    synchronized ( tidToTransactionMap_ ) {
        if ( tidToTransactionMap_.containsKey ( tid.intern () ) )
            throw new IllegalStateException ( "Already mapped: " + tid );
        tidToTransactionMap_.put ( tid.intern (), ct );
        ct.addSubTxAwareParticipant(this); // for GC purposes
    }
}

发现在tidToTransactionMap_中存在tid重复的情况,这个方法判断如果出现重复报:Already mapped: ${tid},继续跟踪找到tid生成的地方

public CompositeTransaction createCompositeTransaction ( long timeout ) throws SysException
{
    if ( !initialized_ ) throw new IllegalStateException ( "Not initialized" );
    if ( maxNumberOfActiveTransactions_ >= 0 &&
         tidToTransactionMap_.size () >= maxNumberOfActiveTransactions_ ) {
        throw new IllegalStateException ( "Max number of active transactions reached:" + maxNumberOfActiveTransactions_ );
    }
     
    String tid = tidmgr_.get ();
    Stack<CompositeTransaction> lineage = new Stack<CompositeTransaction>();
    // create a CC with heuristic preference set to false,
    // since it does not really matter anyway (since we are
    // creating a root)
    CoordinatorImp cc = createCC ( null, tid, true, false, timeout );
    CompositeTransaction ct = createCT ( tid, cc, lineage, false );
    return ct;
}

tid是通过tidmgr_.get ();这个东西生成的,那我们进去看一下生成的代码具体是什么?

private final static int MAX_LENGTH_OF_NUMERIC_SUFFIX = 8 + 5;
private final static int MAX_COUNTER_WITHIN_SAME_MILLIS = 32000;
 
 
private final String commonPartOfId; //name of server
private int lastcounter;
 
public String get()
{
    incrementAndGet();
    StringBuffer buffer = new StringBuffer();
    return buffer.append(commonPartOfId).
                  append(System.currentTimeMillis()).
                  append(getCountWithLeadingZeroes ( lastcounter )).
                  toString() ;
}
 
private synchronized void incrementAndGet() {
    lastcounter++;
    if (lastcounter == MAX_COUNTER_WITHIN_SAME_MILLIS) lastcounter = 0;
}

那极其有可能get的时候在极端的情况下生成的id是相同的,incrementAndGet方法是synchronized 理论上不会有并发问题,但是lastcounter这个属性不是支持并发的对象,在get方法中先调用同步方法incrementAndGet对属性lastcounter++,后面buffer在append的时候直接使用的是属性lastcounter属性的值,很有可能问题就出在这里,那让我们使用btrace验证一下。

通过btrace对get方法拦截验证发现确实在极端的情况下会有多个线程生成同一个tid,如下:

[DubboServerHandler-10.27.69.168:20888-thread-177] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391749109556
[DubboServerHandler-10.27.69.168:20888-thread-156] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391749509557
[DubboServerHandler-10.27.69.168:20888-thread-156] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391756909559
[DubboServerHandler-10.27.69.168:20888-thread-177] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391756909559
[DubboServerHandler-10.27.69.168:20888-thread-155] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391780109560
[DubboServerHandler-10.27.69.168:20888-thread-155] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391786909561
[DubboServerHandler-10.27.69.168:20888-thread-112] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391791609562
[DubboServerHandler-10.27.69.168:20888-thread-197] - com.atomikos.util.UniqueIdMgr.get()-->10.27.69.168.tm150959391794109563

出现了两个tm150959391756909559,那就能断定肯定是这块出问题,那如何解决呢?

首先查看我们使用的atomikos transaction的版本号 – > 4.0.0

去maven官服上搜索transaction的版本信息:http://mvnrepository.com/artifact/com.atomikos/atomikos-util

1.png

看来有更高的版本,那我们下载一个版本看一下get的代码是否发生了变化,我们从4.0.1版本开始查看。

public String get()
{
  StringBuffer buffer = new StringBuffer();
  String id = this.commonPartOfId + System.currentTimeMillis() + getCountWithLeadingZeroes(incrementAndGet());
  return id;
}
 
private synchronized int incrementAndGet()
{
  this.lastcounter += 1;
  if (this.lastcounter == 32000) {
    this.lastcounter = 0;
  }
  return this.lastcounter;
}

从上面代码可以发现跟4.0.0的代码是有变化的

一、4.0.0版本在incrementAndGet方法同步的对lastcounter++之后,在拼接id的时候是直接使用属性lastcounter进行拼接 二、4.0.1版本在incrementAndGet方法同步的对lastcounter++之后直接将lastcounter值返回,在拼接的时候使用返回的lastcounter值来进行拼接

从代码上看好像是为了解决这个问题,那我们还需要进一步验证

首先先找到官方的chang log看是否有明确的版本升级描述中fixed并发tid的bug,翻atomikos的官网站点

2.png

功夫不负有心人找到了fixed记录,接下来就需要升级程序然后再进行实际压测过程去校验是否真的解决了这个问题

修改验证

升级atomikos transactions版本–>4.0.1,打包程序发布进行压力测试 压测场景: 4个仓,一个仓10个线程,一个线程2000单,一单2个商品,一个商品6个sku 压测后再没有Already mapped: xxxx的错误爆出,库存扣除也是正确的。

解决方案

升级atomikos transactions版本–>4.0.1

总结

在使用任何第三方框架都是存在风险,就看如何进行权衡,出现问题能否hold的住,当出现由于使用第三方框架带来的问题时。 1. 首先要彻底的分析出问题的原因 2. 其次就去社区或者官网或者问作者是否bug已经fixed。 3. 上面的都尝试之后如果还不能解决,要么寻找替换方案,要么修改源码。 能使用官网升级的版本解决问题尽量升级版本解决,第三步的方法虽然不推荐,但是在特定的环境也是一个兜底的方案。

数据源连接泄漏问题分析

Published on:

目录:

  1. 问题现象
  2. 问题分析
  3. 修改验证
  4. 解决方案
  5. 总结

问题现象

开启druid数据源的连接泄漏开关(removeAbandoned=true),设置强制回收非法连接的超时时间为120(removeAbandonedTimeout=120,2分钟,目的是调试方便,让非法连接快速close掉)。 启动程序,等待2分钟会有连接泄漏的异常爆出,具体日志如下:

2017-10-25 17:19:52.858 [qtp365976330-72] WARN  org.jasig.cas.client.session.SingleSignOutHandler - Front Channel single sign out redirects are disabled when the 'casServerUrlPrefix' value is not set.
2017-10-25 17:21:56.531 [Druid-ConnectionPool-Destroy-678372234] ERROR com.alibaba.druid.pool.DruidDataSource - abandon connection, open stackTrace
    at java.lang.Thread.getStackTrace(Thread.java:1588)
    at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:995)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4544)
    at com.alibaba.druid.filter.FilterAdapter.dataSource_getConnection(FilterAdapter.java:2723)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)
    at com.alibaba.druid.filter.stat.StatFilter.dataSource_getConnection(StatFilter.java:661)
    at com.alibaba.druid.filter.FilterChainImpl.dataSource_connect(FilterChainImpl.java:4540)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:919)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:911)
    at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:98)
    at com.github.pagehelper.PageHelper.initSqlUtil(PageHelper.java:165)
    at com.github.pagehelper.PageHelper.intercept(PageHelper.java:148)
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60)
    at com.sun.proxy.$Proxy64.query(Unknown Source)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358)
    at com.sun.proxy.$Proxy57.selectList(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198)
    at com.xx.xx.xx.mybatis.MyBatisDao.selectList(MyBatisDao.java:391)
    at com.xx.xx.xx.xx.xx.xx.XXDaoImpl.queryByDeliverCode(XXDaoImpl.java:158)
    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl.queryByDeliverCode(XXServiceImpl.java:159)
    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$FastClassByCGLIB$$41eff1cc.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:642)
    at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$EnhancerByCGLIB$$708c18f3.queryByDeliverCode(<generated>)
    at com.xx.xx.xx.xx.xx.XXController.initId(XXController.java:168)
    at com.xx.xx.xx.xx.xx.XXController.afterPropertiesSet(XXController.java:2080)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1612)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1549)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:700)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:760)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482)
    at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:381)
    at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:293)
    at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106)
    at com.bstek.dorado.web.servlet.SpringContextLoaderListener.contextInitialized(SpringContextLoaderListener.java:73)
    at org.eclipse.jetty.server.handler.ContextHandler.callContextInitialized(ContextHandler.java:782)
    at org.eclipse.jetty.servlet.ServletContextHandler.callContextInitialized(ServletContextHandler.java:424)
    at org.eclipse.jetty.server.handler.ContextHandler.startContext(ContextHandler.java:774)
    at org.eclipse.jetty.servlet.ServletContextHandler.startContext(ServletContextHandler.java:249)
    at org.eclipse.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1242)
    at org.eclipse.jetty.server.handler.ContextHandler.doStart(ContextHandler.java:717)
    at org.eclipse.jetty.webapp.WebAppContext.doStart(WebAppContext.java:494)
    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)
    at org.eclipse.jetty.server.handler.HandlerWrapper.doStart(HandlerWrapper.java:95)
    at org.eclipse.jetty.server.Server.doStart(Server.java:282)
    at org.eclipse.jetty.util.component.AbstractLifeCycle.start(AbstractLifeCycle.java:64)
    at net.sourceforge.eclipsejetty.starter.embedded.JettyEmbeddedAdapter.start(JettyEmbeddedAdapter.java:67)
    at net.sourceforge.eclipsejetty.starter.common.AbstractJettyLauncherMain.launch(AbstractJettyLauncherMain.java:84)
    at net.sourceforge.eclipsejetty.starter.embedded.JettyEmbeddedLauncherMain.main(JettyEmbeddedLauncherMain.java:42)

问题分析

断点调试com.alibaba.druid.pool.DruidDataSource与com.alibaba.druid.pool.DruidPooledConnection中的close方法均有调用,如果都有关闭的话那怎么还会有连接泄漏呢?肯定有地方不对劲,因此进一步查询,开启druid的管理页面查看连接数,如下

1.png

逻辑连接打开次数132,逻辑连接关闭次数131,发现问题有一个连接是没有放回连接池的,当到2分钟报了连接泄漏异常后再刷新查看,如下:

2.png

逻辑连接打开次数和关闭次数一致了。

于是从上面的错误日志跟踪代码,第一感觉就是自己的业务代码出现了问题,找到业务代码的地方

at com.xx.xx.xx.xx.xx.xx.XXDaoImpl.queryByDeliverCode(XXDaoImpl.java:158)
at com.xx.xx.xx.xx.xx.xx.XXServiceImpl.queryByDeliverCode(XXServiceImpl.java:159)
at com.xx.xx.xx.xx.xx.xx.XXServiceImpl$$FastClassByCGLIB$$41eff1cc.invoke(<generated>)

打开:XXServiceImpl.queryByDeliverCode代码第159行,代码如下:

@Override
public DeliverEntity queryByDeliverCode(String code) {
    return deliverDao.queryByDeliverCode(code);
}

代码非常简单调用dao的方法,代开dao的queryByDeliverCode方法,代码如下:

@Override
public DeliverEntity queryByDeliverCode(String deliverCode) {
    Map<String,Object> map=new HashMap<String,Object>();
    map.put("deliverCode", deliverCode);
    List<DeliverEntity> list = selectList("com.xx.xx.xx.xx.xx.XXMapper.queryByDeliverCode", map);
    return list.size() > 0 ? list.get(0) : null;
}

代码也非常简单调用的是基类:MybatisDao的selectList方法,代码如下:

public List<E> selectList(final String aStatement, final Map<String, Object> aCondition) {
    SqlSession session = getSqlSessionTemplate();
    return session.selectList(aStatement, aCondition);
}

就是调用sqlsession的selectList方法,这个没有问题,连接是可以正常回收的,如果不能回收那上面的数字不可能是只有1个连接泄漏,应该是逻辑打开的132个都没有关闭才对。因此排除了这个地方,那还有什么地方会有问题呢? 肯定是有地方getConnection之后没有close导致! 继续分析连接泄漏打出来的日志!日志中的代码逐个分析,最终找到PageHelper.initSqlUtil方法

at com.github.pagehelper.PageHelper.initSqlUtil(PageHelper.java:165)
at com.github.pagehelper.PageHelper.intercept(PageHelper.java:148)

打开PageHelper.initSqlUtil代码,如下:

public synchronized void initSqlUtil(Invocation invocation) {
       if (sqlUtil == null) {
            String url = null;
            try {
                MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
                MetaObject msObject = SystemMetaObject.forObject(ms);
                DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource");
                url = dataSource.getConnection().getMetaData().getURL();
            } catch (SQLException e) {
                throw new RuntimeException("分页插件初始化异常:" + e.getMessage());
            }
            if (url == null || url.length() == 0) {
                throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
            }
            String dialect = Dialect.fromJdbcUrl(url);
            if (dialect == null) {
                throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!");
            }
            sqlUtil = new SqlUtil(dialect);
            sqlUtil.setProperties(properties);
            properties = null;
            autoDialect = false;
       }
}

貌似问题找到了,第8行代码:dataSource.getConnection(),但是没有在finally中对connection进行回收,罪魁祸首竟然是PageHelper

public Object intercept(Invocation invocation) throws Throwable {
    if (autoDialect) {
        initSqlUtil(invocation);
    }
    return sqlUtil.processPage(invocation);
}

根据代码逻辑发现当autoDialect=true时会调用initSqlUtil(invocation);,因此核对了我们的配置mybatis-config.xml

<plugins>
        <!-- com.github.pagehelper为PageHelper类所在包名 -->
        <plugin interceptor="com.github.pagehelper.PageHelper">
<!--             <property name="dialect" value="mysql" /> -->
             
            <property name="autoDialect" value="true" />
             
            <!-- 该参数默认为false -->
            <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
            <!-- 和startPage中的pageNum效果一样 -->
            <property name="offsetAsPageNum" value="true" />
            <!-- 该参数默认为false -->
            <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
            <property name="rowBoundsWithCount" value="true" />
            <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
            <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) -->
            <property name="pageSizeZero" value="true" />
            <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
            <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
            <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
            <property name="reasonable" value="false" />
            <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
            <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
            <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 -->
            <!-- 不理解该含义的前提下,不要随便复制该配置 -->
            <property name="params" value="pageNum=start;pageSize=limit;" />
        </plugin>
    </plugins>

我们果然配置的是:autoDialect=true,PageHelper在没有设置数据库方言的时候,他会主动的获取jdbc url来判断时那种数据库,因此会发生有一个连接是泄漏的,那这个问题如何解决呢? 我们打开PageHelper.setProperties方法,如下:

public void setProperties(Properties p) {
    //MyBatis3.2.0版本校验
    try {
        Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的类
    } catch (ClassNotFoundException e) {
        throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分页插件PageHelper支持MyBatis3.2.0及以上版本!");
    }
    //数据库方言
    String dialect = p.getProperty("dialect");
    if (dialect == null || dialect.length() == 0) {
        autoDialect = true;
        this.properties = p;
    } else {
        autoDialect = false;
        sqlUtil = new SqlUtil(dialect);
        sqlUtil.setProperties(p);
    }
}

只要我们在plugin配置的时候设置具体的方言就可以避免这个问题:dialect=mysql,如果有明确的dialect设置,autoDialect就会等于false,因此在intercept方法中就不会走initSqlUtil(invocation);方法,这就间接的避免了PageHelper的这个bug。 但是如果我们的数据源有不同的dialect怎么办呢?有两个办法解决 1. 构造SessionFactory的时候加载不同的mybatis-config.xml配置,如果有两种数据库类型就写两个mybatis-config.xml分别配置不同的dialect 2. 查看PageHelper高版本是否修复了这个bug,升级PageHelper版本 3. 修改PageHelper源码,在dataSource.getConnection()之后增加close调用

ps. 我们现在用的PageHelper版本–>4.0.0,根据官方的chang log可以看出4.X的版本修复了这个问题,可以升级到4.x的final released version –> 4.2.1解决这个问题,5.x版本变更比较大。

修改验证

修改mybatis-config.xml的plugin中PageHelper的dialect的配置

<plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql" />
<!--             <property name="autoDialect" value="true" /> -->
 
</plugin>

修改后启动程序,打开druid的管理页面和等待2分钟超时看是否还有泄漏的异常爆出,如下:

3.png

超过2分钟并没有泄漏异常爆出

解决方案

升级pagehelper版本–>4.2.1,升级jsqlparser版本–>0.9.5,其余配置无需变更

如果升级了4.2.1,如果出现SqlUtil.java(120)行报NullPointerException,具体异常如下:

4.png

遇到上面问题,请修改pagehelper的配置参数,参数修改有两种方式,如下:

  1. 直接配置dialect=目标数据源类型(适合使用场景:项目中只有一个固定的数据库类型,例如:mysql,无需开启自动发现dialect)
  2. 配置autoRuntimeDialect=true走自动获取,这个属性是替换老属性(autoDialect),老的属性为了向下兼容在并发获取dialect时会有bug存在。(适合使用场景:项目中有多个数据库类型,需要运行中自动发现时使用)

总结

这个问题告诉我们使用第三方的组件的风险很大。

Redis RDB文件格式全解析

Published on:
Tags: Redis RDB

点评

这篇文章作为对RDB理解的教程文章,对RDB文件的原理理解有助于进行Redis高阶应用的设计与开发。

文章转自:http://blog.nosqlfan.com/html/3734.html 作者:@nosqlfan

RDB文件是Redis持久化的一种方式,Redis通过制定好的策略,按期将内存中的数据以镜像的形式转存到RDB文件中。那么RDB文件内部格式是什么样的呢,Redis又做了哪些工作让RDB能够更快的dump和加载呢,下面我们深入RDB文件,来看一看其内部结构。 首先我们来看一个RDB文件的概况图:

----------------------------# RDB文件是二进制的,所以并不存在回车换行来分隔一行一行.
52 45 44 49 53              # 以字符串 "REDIS" 开头
30 30 30 33                 # RDB 的版本号,大端存储,比如左边这个表示版本号为0003
----------------------------
FE 00                       # FE = FE表示数据库编号,Redis支持多个库,以数字编号,这里00表示第0个数据库
----------------------------# Key-Value 对存储开始了
FD $length-encoding         # FD 表示过期时间,过期时间是用 length encoding 编码存储的,后面会讲到
$value-type                 # 1 个字节用于表示value的类型,比如set,hash,list,zset等
$string-encoded-key         # Key 值,通过string encoding 编码,同样后面会讲到
$encoded-value              # Value值,根据不同的Value类型采用不同的编码方式
----------------------------
FC $length-encoding         # FC 表示毫秒级的过期时间,后面的具体时间用length encoding编码存储
$value-type                 # 同上,也是一个字节的value类型
$string-encoded-key         # 同样是以 string encoding 编码的 Key值
$encoded-value              # 同样是以对应的数据类型编码的 Value 值
----------------------------
$value-type                 # 下面是没有过期时间设置的 Key-Value对,为防止冲突,数据类型不会以 FD, FC, FE, FF 开头
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding         # 下一个库开始,库的编号用 length encoding 编码
----------------------------
...                         # 继续存储这个数据库的 Key-Value 对
FF                          ## FF:RDB文件结束的标志

下面我们对上面的内容进行详细讲解

Magic Number

第一行就不用讲了,REDIS字符串用于标识是Redis的RDB文件

版本号

用了4个字节存储版本号,以大端(big endian)方式存储和读取

数据库编号

以一个字节的0xFE开头,后面存储数据库的具体编号,数据库的编号是一个数字,通过 “Length Encoding” 方式编码存储,“Length Encoding” 我们后面会讲到。

Key-Value值对

值对包括下面四个部分 1. Key 过期时间,这一项是可有可无的 2. 一个字节表示value的类型 3. Key的值,Key都是字符串,通过 “Redis String Encoding” 来保存 4. Value的值,通过 “Redis Value Encoding” 来根据不同的数据类型做不同的存储

Key过期时间

过期时间由 0xFD 或 0xFC开头用于标识,分别表示秒级的过期时间和毫秒级的过期时间,后面的具体时间是一个UNIX时间戳,秒级或毫秒级的。具体时间戳的值通过“Redis Length Encoding” 编码存储。在导入RDB文件的过程中,会通过过期时间判断是否已过期并需要忽略。

Value类型

Value类型用一个字节进行存储,目前包括以下一些值:

  • 0 = “String Encoding”
  • 1 = “List Encoding”
  • 2 = “Set Encoding”
  • 3 = “Sorted Set Encoding”
  • 4 = “Hash Encoding”
  • 9 = “Zipmap Encoding”
  • 10 = “Ziplist Encoding”
  • 11 = “Intset Encoding”
  • 12 = “Sorted Set in Ziplist Encoding”

Key

Key值就是简单的 “String Encoding” 编码,具体可以看后面的描述

Value

上面列举了Value的9种类型,实际上可以分为三大类

  • type = 0, 简单字符串
  • type 为 9, 10, 11 或 12, value字符串在读取出来后需要先解压
  • type 为 1, 2, 3 或 4, value是字符串序列,这一系列的字符串用于构建list,set,hash 和 zset 结构

Length Encoding

上面说了很多 Length Encoding ,现在就为大家讲解。可能你会说,长度用一个int存储不就行了吗?但是,通常我们使用到的长度可能都并不大,一个int 4个字节是否有点浪费呢。所以Redis采用了变长编码的方法,将不同大小的数字编码成不同的长度。

  1. 首先在读取长度时,会读一个字节的数据,其中前两位用于进行变长编码的判断
  2. 如果前两位是 0 0,那么下面剩下的 6位就表示具体长度
  3. 如果前两位是 0 1,那么会再读取一个字节的数据,加上前面剩下的6位,共14位用于表示具体长度
  4. 如果前两位是 1 0,那么剩下的 6位就被废弃了,取而代之的是再读取后面的4 个字节用于表示具体长度
  5. 如果前两位是 1 1,那么下面的应该是一个特殊编码,剩下的 6位用于标识特殊编码的种类。特殊编码主要用于将数字存成字符串,或者编码后的字符串。具体见 “String Encoding”

这样做有什么好处呢,实际就是节约空间:

  1. 0 – 63的数字只需要一个字节进行存储
  2. 而64 – 16383 的数字只需要两个字节进行存储
  3. 16383 - 2^32 -1 的数字只需要用5个字节(1个字节的标识加4个字节的值)进行存储

String Encoding

Redis的 String Encoding 是二进制安全的,也就是说他没有任何特殊分隔符用于分隔各个值,你可以在里面存储任何东西。它就是一串字节码。 下面是 String Encoding 的三种类型

  1. 长度编码的字符串
  2. 数字替代字符串:8位,16位或者32位的数字
  3. LZF 压缩的字符串

长度编码字符串

长度编码字符串是最简单的一种类型,它由两部分组成,一部分是用 “Length Encoding” 编码的字符串长度,第二部分是具体的字节码。

数字替代字符串

上面说到过 Length Encoding 的特殊编码,就在这里用上了。所以数字替代字符串是以 1 1 开头的,然后读取这个字节剩下的6 位,根据不同的值标识不同的数字类型:

  • 0 表示下面是一个8 位的数字
  • 1 表示下面是一个16 位的数字
  • 2 表示下面是一个32 位的数字

LZF压缩字符串

和数据替代字符串一样,它也是以1 1 开头的,然后剩下的6 位如果值为4,那么就表示它是一个压缩字符串。压缩字符串解析规则如下:

  1. 首先按 Length Encoding 规则读取压缩长度 clen
  2. 然后按 Length Encoding 规则读取非压缩长度
  3. 再读取第二个 clen
  4. 获取到上面的三个信息后,再通过LZF算法解码后面clen长度的字节码

List Encoding

Redis List 结构在RDB文件中的存储,是依次存储List中的各个元素的。其结构如下:

  1. 首先按 Length Encoding 读取这个List 的长度 size
  2. 然后读取 size个 String Encoding的值
  3. 然后再用这些读到的 size 个值重新构建 List就完成了

Set Encoding

Set结构和List结构一样,也是依次存储各个元素的

Sorted Set Encoding

todo

Hash Encoding

  1. 首先按 Length Encoding 读出hash 结构的大小 size
  2. 然后读取2×size 个 String Encoding的字符串(因为一个hash项包括key和value两项)
  3. 将上面读取到的2×size 个字符串解析为hash 和key 和 value
  4. 然后将上面的key value对存储到hash结构中

Zipmap Encoding

参见本站之前的文章:Redis zipmap内存布局分析

Redis数据结构使用以及注意事项,运维问题总结

文章转自:http://www.cnblogs.com/cnmenglang/p/6225987.html 作者:@江南白衣

优缺点

非常非常的快,有测评说比Memcached还快(当大家都是单CPU的时候),而且是无短板的快,读写都一般的快,所有API都差不多快,也没有MySQL Cluster、MongoDB那样更新同一条记录如Counter时慢下去的毛病。

丰富的数据结构,超越了一般的Key-Value数据库而被认为是一个数据结构服务器。组合各种结构,限制Redis用途的是你自己的想象力,作者自己捉刀写的用途入门。

因为是个人作品,Redis目前只有2.3万行代码,Keep it simple的死硬做法,使得普通公司而不需淘宝那个级别的文艺公司也可以吃透它。

Redis宣言就是作者的自白,我最喜欢其中的”代码像首诗”,”设计是一场与复杂性的战斗”,”Coding是一件艰苦的事情,唯一的办法是享受它。如果它已不能带来快乐就停止它。为了防止这一天的出现,我们要尽量避免把Redis往乏味的路上带。

让人又爱又恨的单线程架构,使得代码不用处理平时最让人头痛的并发而大幅简化,也不用老是担心作者的并发有没有写对,但也带来CPU的瓶颈,而且单线程被慢操作所阻塞时,其他请求的延时变得不确定。

那Redis不是什么?

Redis 不是Big Data,数据都在内存中,无法以T为单位。

在Redis-Cluster发布并被稳定使用之前,Redis没有真正的平滑水平扩展能力。

Redis 不支持Ad-Hoc Query,提供的只是数据结构的API,没有SQL一样的查询能力。

Feature速览

所有数据都在内存中。

五种数据结构:String / Hash / List / Set / Ordered Set。

数据过期时间支持。

不完全的事务支持。

服务端脚本:使用Lua Script编写,类似存储过程的作用。

PubSub:捞过界的消息一对多发布订阅功能,起码Redis-Sentinel使用了它。

持久化:支持定期导出内存的Snapshot 与 记录写操作日志的Append Only File两种模式。

Replication:Master-Slave模式,Master可连接多个只读Slave,暂无专门的Geographic Replication支持。

Fail-Over:Redis-Sentinel节点负责监控Master节点,在master失效时提升slave,独立的仲裁节点模式有效防止脑裂。

Sharding:开发中的Redis-Cluser。

动态配置:所有参数可用命令行动态配置不需重启,并重新写回配置文件中,对云上的大规模部署非常合适。

八卦

作者是意大利的Salvatore Sanfilippo(antirez),又是VMWare大善人聘请了他专心写Redis。

EMC与VMWare将旗下的开源产品如Redis和Spring都整合到了孙公司Pivotal公司。

Pivotal做的antirez访谈录,内含一切八卦,比如他的爱好是举重、跑步和品红酒。

默认端口6379,是手机按键上MERZ对应的号码,意大利歌女Alessia Merz是antirez和朋友们认为愚蠢的代名词。

数据结构

Key

Key 不能太长,比如1024字节,但antirez也不喜欢太短如”u:1000:pwd”,要表达清楚意思才好。他私人建议用”:“分隔域,用”.“作为单词间的连接,如”comment:1234:reply.to”。

Keys,返回匹配的key,支持通配符如 “keys a*” 、 “keys a?c”,但不建议在生产环境大数据量下使用。

Sort,对集合按数字或字母顺序排序后返回或另存为list,还可以关联到外部key等。因为复杂度是最高的O(N+M*log(M))(N是集合大小,M 为返回元素的数量),有时会安排到slave上执行。

Expire/ExpireAt/Persist/TTL,关于Key超时的操作。默认以秒为单位,也有p字头的以毫秒为单位的版本, Redis的内部实现见2.9 过期数据清除。

String

最普通的key-value类型,说是String,其实是任意的byte[],比如图片,最大512M。 所有常用命令的复杂度都是O(1),普通的Get/Set方法,可以用来做Cache,存Session,为了简化架构甚至可以替换掉Memcached。

Incr/IncrBy/IncrByFloat/Decr/DecrBy,可以用来做计数器,做自增序列。key不存在时会创建并贴心的设原值为0。IncrByFloat专门针对float,没有对应的decrByFloat版本?用负数啊。

SetNx, 仅当key不存在时才Set。可以用来选举Master或做分布式锁:所有Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过期时间。

如果Master倒掉了key就会失效,剩下的节点又会发生新一轮抢夺。

其他Set指令:

SetEx, Set + Expire 的简便写法,p字头版本以毫秒为单位。

GetSet, 设置新值,返回旧值。比如一个按小时计算的计数器,可以用GetSet获取计数并重置为0。这种指令在服务端做起来是举手之劳,客户端便方便很多。

MGet/MSet/MSetNx, 一次get/set多个key。

2.6.12版开始,Set命令已融合了Set/SetNx/SetEx三者,SetNx与SetEx可能会被废弃,这对Master抢注非常有用,不用担心setNx成功后,来不及执行Expire就倒掉了。可惜有些懒惰的Client并没有快速支持这个新指令。

GetBit/SetBit/BitOp,与或非/BitCount/BitMap的玩法,比如统计今天的独立访问用户数时,每个注册用户都有一个offset,他今天进来的话就把他那个位设为1,用BitCount就可以得出今天的总人树。

Append/SetRange/GetRange/StrLen,对文本进行扩展、替换、截取和求长度,只对特定数据格式如字段定长的有用,json就没什么用。

Hash

Key-HashMap结构,相比String类型将这整个对象持久化成JSON格式,Hash将对象的各个属性存入Map里,可以只读取/更新对象的某些属性。

这样有些属性超长就让它一边呆着不动,另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突。

另一个用法是土法建索引。比如User对象,除了id有时还要按name来查询。

可以有如下的数据记录:

(String) user:101 -> {"id":101,"name":"calvin"...}
(String) user:102 -> {"id":102,"name":"kevin"...}
(Hash) user:index-> "calvin"->101, "kevin" -> 102

底层实现是hash table,一般操作复杂度是O(1),要同时操作多个field时就是O(N),N是field的数量。

List

List是一个双向链表,支持双向的Pop/Push,江湖规矩一般从左端Push,右端Pop——LPush/RPop,而且还有Blocking的版本BLPop/BRPop,客户端可以阻塞在那直到有消息到来,所有操作都是O(1)的好孩子,可以当Message Queue来用。

当多个Client并发阻塞等待,有消息入列时谁先被阻塞谁先被服务。任务队列系统Resque是其典型应用。

还有RPopLPush/ BRPopLPush,弹出来返回给client的同时,把自己又推入另一个list,LLen获取列表的长度。

还有按值进行的操作:LRem(按值删除元素)、LInsert(插在某个值的元素的前后),复杂度是O(N),N是List长度,因为List的值不唯一,所以要遍历全部元素,而Set只要O(log(N))。

按下标进行的操作:下标从0开始,队列从左到右算,下标为负数时则从右到左。

LSet ,按下标设置元素值。

LIndex,按下标返回元素。

LRange,不同于POP直接弹走元素,只是返回列表内一段下标的元素,是分页的最爱。

LTrim,限制List的大小,比如只保留最新的20条消息。

复杂度也是O(N),其中LSet的N是List长度,LIndex的N是下标的值,LRange的N是start的值+列出元素的个数,因为是链表而不是数组,所以按下标访问其实要遍历链表,除非下标正好是队头和队尾。LTrim的N是移除元素的个数。

在消息队列中,并没有JMS的ack机制,如果消费者把job给Pop走了又没处理完就死机了怎么办?

解决方法之一是加多一个sorted set,分发的时候同时发到list与sorted set,以分发时间为score,用户把job做完了之后要用ZREM消掉sorted set里的job,并且定时从sorted set中取出超时没有完成的任务,重新放回list。

另一个做法是为每个worker多加一个的list,弹出任务时改用RPopLPush,将job同时放到worker自己的list中,完成时用LREM消掉。

如果集群管理(如zookeeper)发现worker已经挂掉,就将worker的list内容重新放回主list。

Set

Set就是Set,可以将重复的元素随便放入而Set会自动去重,底层实现也是hash table

SAdd/SRem/SISMember/SCard/SMove/SMembers,各种标准操作。除了SMembers都是O(1)。

SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各种集合操作。交集运算可以用来显示在线好友(在线用户 交集 好友列表),共同关注(两个用户的关注列表的交集)。

O(N),并集和差集的N是集合大小之和,交集的N是小的那个集合的大小*2。

Sorted Set

有序集,元素放入集合时还要提供该元素的分数。

ZRange/ZRevRange,按排名的上下限返回元素,正数与倒数。

ZRangeByScore/ZRevRangeByScore,按分数的上下限返回元素,正数与倒数。

ZRemRangeByRank/ZRemRangeByScore,按排名/按分数的上下限删除元素。

ZCount,统计分数上下限之间的元素个数。

ZRank/ZRevRank ,显示某个元素的正倒序的排名。

ZScore/ZIncrby,显示元素的分数/增加元素的分数。

ZAdd(Add)/ZRem(Remove)/ZCard(Count),ZInsertStore(交集)/ZUnionStore(并集),Set操作,与正牌Set相比,少了IsMember和差集运算。

Sorted Set的实现是hash table(element->score, 用于实现ZScore及判断element是否在集合内),和skip list(score->element,按score排序)的混合体。

skip list有点像平衡二叉树那样,不同范围的score被分成一层一层,每层是一个按score排序的链表。

ZAdd/ZRem是O(log(N)),ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是结果/操作元素的个数。

可见,原本可能很大的N被很关键的Log了一下,1000万大小的Set,复杂度也只是几十不到。

当然,如果一次命中很多元素M很大那谁也没办法了。

事务

Multi(Start Transaction)、Exec(Commit)、Discard(Rollback)实现。

在事务提交前,不会执行任何指令,只会把它们存到一个队列里,不影响其他客户端的操作。在事务提交时,批量执行所有指令。《Redis设计与实现》中的详述。

注意,Redis里的事务,与我们平时的事务概念很不一样:

它仅仅是保证事务里的操作会被连续独占的执行。因为是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的。

它没有隔离级别的概念,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。

它不保证原子性——所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力。

在redis里失败分两种,一种是明显的指令错误,比如指令名拼错,指令参数个数不对,在2.6版中全部指令都不会执行。

另一种是隐含的,比如在事务里,第一句是SET foo bar, 第二句是LLEN foo,对第一句产生的String类型的key执行LLEN会失败,但这种错误只有在指令运行后才能发现,这时候第一句成功,第二句失败。

还有,如果事务执行到一半redis被KILL,已经执行的指令同样也不会被回滚。

Watch指令,类似乐观锁,事务提交时,如果Key的值已被别的客户端改变,比如某个list已被别的客户端push/pop过了,整个事务队列都不会被执行。

Lua Script

Redis2.6内置的Lua Script支持,可以在Redis的Server端一次过运行大量逻辑,就像存储过程一样,避免了海量中间数据在网路上的传输。

Lua自称是在Script语言里关于快的标准,Redis选择了它而不是流行的JavaScript。

因为Redis的单线程架构,整个Script默认是在一个事务里的。

Script里涉及的所有Key尽量用变量,从外面传入,使Redis一开始就知道你要改变哪些key,为了日后做水平分区做准备。如果涉及的key在不同服务器……

Eval每次传输一整段Script比较费带宽,可以先用Script Load载入script,返回哈希值。然后用EvalHash执行。因为就是SHA-1,所以任何时候执行返回的哈希值都是一样的。

内置的Lua库里还很贴心的带了CJSON,可以处理json字符串。

Script一旦执行则不容易中断,中断了也会有不可知后果,因此最好在开发环境充分测试了再上线。

一段用Redis做Timer的示例代码,下面的script被定期调用,从以触发时间为score的sorted set中取出已到期的Job,放到list中给Client们blocking popup。

-- KEYS: [1]job:sleeping, [2]job:ready
-- ARGS: [1]currentTime
-- Comments: result is the  job id
local jobs=redis.call('zrangebyscore', KEYS[1], '-inf', ARGV[1])
local count = table.maxn(jobs)

if count>0  then
  -- Comments: remove from Sleeping Job sorted set
  redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1])
  
  -- Comments: add to the Ready Job list
  -- Comments: can optimize to use lpush id1,id2,... for better performance
  for i=1,count do 
    redis.call('lpush', KEYS[2], jobs[i])
  end
end

过期数据清除

官方文档《Redis设计与实现》中的详述,过期数据的清除从来不容易,为每一条key设置一个timer,到点立刻删除的消耗太大,每秒遍历所有数据消耗也大,Redis使用了一种相对务实的做法:

当client主动访问key会先对key进行超时判断,过时的key会立刻删除。

如果clien永远都不再get那条key呢? 它会在Master的后台,每秒10次的执行如下操作: 随机选取100个key校验是否过期,如果有25个以上的key过期了,立刻额外随机选取下100个key(不计算在10次之内)。

可见,如果过期的key不多,它最多每秒回收200条左右,如果有超过25%的key过期了,它就会做得更多,但只要key不被主动get,它占用的内存什么时候最终被清理掉只有天知道。

性能

测试结果

测试环境: RHEL 6.3 / HP Gen8 Server/ 2 * Intel Xeon 2.00GHz(6 core) / 64G DDR3 memory / 300G RAID-1 SATA / 1 master(writ AOF), 1 slave(write AOF & RDB)

数据准备: 预加载两千万条数据,占用10G内存。

测试工具:自带的redis-benchmark,默认只是基于一个很小的数据集进行测试,调整命令行参数如下,就可以开100条线程(默认50),SET 1千万次(key在0-1千万间随机),key长21字节,value长256字节的数据。

redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256 

测试结果(TPS): 1.SET:4.5万, 2.GET:6万 ,3.INCR:6万,4.真实混合场景: 2.5万SET & 3万GET

单条客户端线程时6千TPS,50与100条客户端线程差别不大,200条时会略多。

Get/Set操作,经过了LAN,延时也只有1毫秒左右,可以反复放心调用,不用像调用REST接口和访问数据库那样,每多一次外部访问都心痛。

资源监控:

  1. CPU: 占了一个处理器的100%,总CPU是4%(因为总共有2CPU*6核*超线程 = 24个处理器),可见单线程下单处理器的能力是瓶颈。 AOF rewrite时另一个处理器占用50-70%。
  2. 网卡:15-20 MB/s receive, 3Mb/s send(no slave) or 15-20 MB/s send (with slave) 。当把value长度加到4K时,receive 99MB/s,已经到达千兆网卡的瓶颈,TPS降到2万。
  3. 硬盘:15MB/s(AOF append), 100MB/s(AOF rewrite/AOF load,普通硬盘的瓶颈)

为什么快

纯ANSI C编写。

不依赖第三方类库,没有像memcached那样使用libevent,因为libevent迎合通用性而造成代码庞大,所以作者用libevent中两个文件修改实现了自己的epoll event loop。微软的兼容Windows补丁也因为同样原因被拒了。

快,原因之一是Redis多样的数据结构,每种结构只做自己爱做的事,当然比数据库只有Table,MongogoDB只有JSON一种结构快了。

可惜单线程架构,虽然作者认为CPU不是瓶颈,内存与网络带宽才是。但实际测试时并非如此,见上。

性能调优

官方文档关于各种产生Latency的原因的详细分析, 中文版

正视网络往返时间:

1.MSet/LPush/ZAdd等都支持一次输入多个Key。

2.PipeLining模式 可以一次输入多个指令。

3.更快的是Lua Script模式,还可以包含逻辑,直接在服务端又get又set的,见2.8 Lua Script。

发现执行缓慢的命令,可配置执行超过多少时间的指令算是缓慢指令(默认10毫秒,不含IO时间),可以用slowlog get 指令查看(默认只保留最后的128条)。

单线程的模型下,一个请求占掉10毫秒是件大事情,注意设置和显示的单位为微秒。

CPU永远是瓶颈,但top看到单个CPU 100%时,就是垂直扩展的时候了。

持久化对性能的影响很大,见5.1持久化。

要熟悉各指令的复杂度,不过只要不是O(N)一个超大集合,都不用太担心。

容量

最大内存

所有的数据都必须在内存中,原来2.0版的VM策略(将Value放到磁盘,Key仍然放在内存),2.4版后嫌麻烦又不支持了。

一定要设置最大内存,否则物理内存用爆了就会大量使用Swap,写RDB文件时的速度慢得你想死。

多留一倍内存是最安全的。重写AOF文件和RDB文件的进程(即使不做持久化,复制到Slave的时候也要写RDB)会fork出一条新进程来,采用了操作系统的Copy-On-Write策略(子进程与父进程共享Page。

如果父进程的Page-每页4K有修改,父进程自己创建那个Page的副本,不会影响到子进程,父爱如山)。留意Console打出来的报告,如”RDB: 1215 MB of memory used by copy-on-write”。

在系统极度繁忙时,如果父进程的所有Page在子进程写RDB过程中都被修改过了,就需要两倍内存。

按照Redis启动时的提醒,设置 vm.overcommit_memory = 1 ,使得fork()一条10G的进程时,因为COW策略而不一定需要有10G的free memory。

其他需要考虑的内存包括:

1.AOF rewrite过程中对新写入命令的缓存(rewrite结束后会merge到新的aof文件),留意”Background AOF buffer size: 80 MB”的字样。

2.负责与Slave同步的Client的缓存,默认设置master需要为每个slave预留不高于256M的缓存(见5.1持久化)。

当最大内存到达时,按照配置的Policy进行处理, 默认策略为volatile-lru,对设置了expire time的key进行LRU清除(不是按实际expire time)。

如果沒有数据设置了expire time或者policy为noeviction,则直接报错,但此时系统仍支持get之类的读操作。

另外还有几种policy,比如volatile-ttl按最接近expire time的,allkeys-lru对所有key都做LRU。

内存占用

测试表明,string类型需要90字节的额外代价,就是说key 1个字节,value 1个字节时,还是需要占用92字节的长度,而上面的benchmark的记录就占用了367个字节。

其他类型可根据文档自行计算或实际测试一下。

使用jemalloc分配内存,删除数据后,内存并不会乖乖还给操作系统而是被Redis截留下来重用到新的数据上,直到Redis重启。

因此进程实际占用内存是看INFO里返回的used_memory_peak_human。

Redis内部用了ziplist/intset这样的压缩结构来减少hash/list/set/zset的存储,默认当集合的元素少于512个且最长那个值不超过64字节时使用,可配置。

用make 32bit可以编译出32位的版本,每个指针占用的内存更小,但只支持最大4GB内存。

水平分区,Sharding

其实,大内存加上垂直分区也够了,不一定非要沙丁一把。

Jedis支持在客户端做分区,局限是不能动态re-sharding, 有分区的master倒了,不能减少分区必须用slave顶上。要增加分区的话,呃…..

antire在博客里提到了Twemproxy,一个Twitter写的Proxy,但它在发现节点倒掉后,只会重新计算一致性哈希环,把数据存到别的master去,而不是集成Sentinel指向新由slave升级的master,像Memcached一样的做法也只适合做Cache的场景。

Redis-Cluster是今年工作重点,支持automatic re-sharding, 采用和Hazelcast类似的算法,总共有N个分区(eg.N=1024),每台Server负责若干个分区。

在客户端先hash出key 属于哪个分区,随便发给一台server,server会告诉它真正哪个Server负责这个分区,缓存下来,下次还有该分区的请求就直接发到地儿了。

Re-sharding时,会将某些分区的数据移到新的Server上,完成后各Server周知分区<->Server映射的变化,因为分区数量有限,所以通讯量不大。

在迁移过程中,客户端缓存的依然是旧的分区映射信息,原server对于已经迁移走的数据的get请求,会返回一个临时转向的应答,客户端先不会更新Cache。

等迁移完成了,就会像前面那样返回一条永久转向信息,客户端更新Cache,以后就都去新server了。

高可用性

高可用性关乎系统出错时到底会丢失多少数据,多久不能服务。要综合考虑持久化,Master-Slave复制及Fail-Over配置,以及具体Crash情形,比如Master死了,但Slave没死。或者只是Redis死了,操作系统没死等等。

持久化

综述: 解密Redis持久化(中文概括版), 英文原版,《Redis设计与实现》: RDBAOF

很多人开始会想象两者是互相结合的,即dump出一个snapshot到RDB文件,然后在此基础上记录变化日志到AOF文件。

实际上两者毫无关系,完全独立运行,因为作者认为简单才不会出错。如果使用了AOF,重启时只会从AOF文件载入数据,不会再管RDB文件。

正确关闭服务器:redis-cli shutdown 或者 kill,都会graceful shutdown,保证写RDB文件以及将AOF文件fsync到磁盘,不会丢失数据。

如果是粗暴的Ctrl+C,或者kill -9 就可能丢失。

RDB文件

RDB是整个内存的压缩过的Snapshot,RDB的数据结构,可以配置复合的快照触发条件,默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。

RDB写入时,会连内存一起Fork出一个新进程,遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程中又有新的写入请求进来的问题。 Fork的细节见4.1最大内存。

RDB会先写到临时文件,完了再Rename成,这样外部程序对RDB文件的备份和传输过程是安全的。而且即使写新快照的过程中Server被强制关掉了,旧的RDB文件还在。

可配置是否进行压缩,压缩方法是字符串的LZF算法,以及将string形式的数字变回int形式存储。

动态所有停止RDB保存规则的方法:redis-cli config set save “”

AOF文件

操作日志,记录所有有效的写操作,等于mysql的binlog,格式就是明文的Redis协议的纯文本文件。

一般配置成每秒调用一次fdatasync将kernel的文件缓存刷到磁盘。当操作系统非正常关机时,文件可能会丢失不超过2秒的数据(更严谨的定义见后)。 如果设为fsync always,性能只剩几百TPS,不用考虑。

如果设为no,靠操作系统自己的sync,Linux系统一般30秒一次。

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件,最后再rename,), 遍历新进程的内存中数据,每条记录有一条的Set语句。

默认配置是当AOF文件大小是上次rewrite后大小的一倍,且文件大于64M时触发。

Redis协议,如set mykey hello,将持久化成*3 $3 set $5 mykey $5 hello, 第一个数字代表这条语句有多少元,其他的数字代表后面字符串的长度。

这样的设计,使得即使在写文件过程中突然关机导致文件不完整,也能自我修复,执行redis-check-aof即可。

综上所述,RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。

那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

读写性能

AOF重写和RDB写入都是在fork出新进程后,遍历新进程的内存顺序写的,既不阻塞主进程继续处理客户端请求,顺序写的速度也比随机写快。

测试把刚才benchmark的11G数据写成一个1.3的RDB文件,或者等大的AOF文件rewrite,需要80秒,在redis-cli info中可查看。启动时载入一个AOF或RDB文件的速度与上面写入时相同,在log中可查看。

Fork一个使用了大量内存的进程也要时间,大约10ms per GB的样子,但Xen在EC2上是让人郁闷的239ms (KVM和VMWare貌似没有这个毛病),各种系统的对比,Info指令里的latest_fork_usec显示上次花费的时间。

在bgrewriteaof过程中,所有新来的写入请求依然会被写入旧的AOF文件,同时放到buffer中,当rewrite完成后,会在主线程把这部分内容合并到临时文件中之后才rename成新的AOF文件。

所以rewrite过程中会不断打印”Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB”,计算系统容量时要留意这部分的内存消耗。

注意,这个合并的过程是阻塞的,如果你产生了280MB的buffer,在100MB/s的传统硬盘上,Redis就要阻塞2.8秒!!!

NFS或者Amazon上的EBS都不推荐,因为它们也要消耗带宽。

bgsave和bgaofrewrite不会被同时执行,如果bgsave正在执行,bgaofrewrite会自动延后。

2.4版以后,写入AOF时的fdatasync由另一条线程来执行,不会再阻塞主线程。

2.4版以后,lpush/zadd可以输入一次多个值了,使得AOF重写时可以将旧版本中的多个lpush/zadd指令合成一个,每64个key串一串。

性能调整

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。

代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。

只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。

默认超过原大小100%大小时重写可以改到适当的数值,比如之前的benchmark每个小时会产生40G大小的AOF文件,如果硬盘能撑到半夜系统闲时才用cron调度bgaofrewrite就好了。

如果不Enable AOF,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。

代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构,见Tim的博客

Trouble Shooting —— Enable AOF可能导致整个Redis被Block住,在2.6.12版之前

现象描述:

当AOF rewrite 15G大小的内存时,Redis整个死掉的样子,所有指令甚至包括slave发到master的ping,redis-cli info都不能被执行。

原因分析:

官方文档,由IO产生的Latency详细分析, 已经预言了悲剧的发生,但一开始没留意。

Redis为求简单,采用了单请求处理线程结构。

打开AOF持久化功能后, Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer,如果此时write(2)被阻塞,Redis就不能处理下一个事件。

Linux规定执行write(2)时,如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘,或者有system wide sync在执行,write(2)会被block住,整个Redis被block住。

如果系统IO繁忙,比如有别的应用在写盘,或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件,虽然各自都在连续写,但两个文件间的切换使得磁盘磁头的寻道时间加长),就可能导致fdatasync(2)迟迟未能完成从而block住write(2),block住整个Redis。

为了更清晰的看到fdatasync(2)的执行时长,可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”,但会影响系统性能。

Redis提供了一个自救的方式,当发现文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被block。但如果已经超过两秒都还是这个样子,则会硬着头皮执行write(2),即使redis会被block住。

此时那句要命的log会打印:“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.”

之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。

因此,对于fsync设为everysec时丢失数据的可能性的最严谨说法是:如果有fdatasync在长时间的执行,此时redis意外关闭会造成文件里不多于两秒的数据丢失。

如果fdatasync运行正常,redis意外关闭没有影响,只有当操作系统crash时才会造成少于1秒的数据丢失。

解决方法:

最后发现,原来是AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。在RedHat Enterprise 6里,默认配置vm.dirty_background_ratio=10,也就是占用了10%的可用内存才会开始后台flush,而我的服务器有64G内存。

很明显一次flush太多数据会造成阻塞,所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M),问题解决。

然后提了个issue,AOF rewrite时定时也执行一下fdatasync嘛, antirez三分钟后就回复了,新版中,AOF rewrite时32M就会重写主动调用fdatasync。

Master-Slave复制

概述

slave可以在配置文件、启动命令行参数、以及redis-cli执行SlaveOf指令来设置自己是奴隶。

测试表明同步延时非常小,指令一旦执行完毕就会立刻写AOF文件和向Slave转发,除非Slave自己被阻塞住了。

比较蠢的是,即使在配置文件里设了slavof,slave启动时依然会先从数据文件载入一堆没用的数据,再去执行slaveof。

“Slaveof no one”,立马变身master。

2.8版本将支持PSYNC部分同步,master会拨出一小段内存来存放要发给slave的指令,如果slave短暂的断开了,重连时会从内存中读取需要补读的指令,这样就不需要断开两秒也搞一次全同步了。

但如果断开时间较长,已经超过了内存中保存的数据,就还是要全同步。

Slave也可以接收Read-Only的请求。

slaveof执行过程,完全重用已有功能,非常经济

先执行一次全同步 – 请求master BgSave出自己的一个RDB Snapshot文件发给slave,slave接收完毕后,清除掉自己的旧数据,然后将RDB载入内存。

再进行增量同步 – master作为一个普通的client连入slave,将所有写操作转发给slave,没有特殊的同步协议。

Trouble Shooting again

有时候明明master/slave都活得好好的,突然间就说要重新进行全同步了:

1)Slave显示:

# MASTER time out: no data nor PING received...

slave会每隔repl-ping-slave-period(默认10秒)ping一次master,如果超过repl-timeout(默认60秒)都没有收到响应,就会认为Master挂了。

如果Master明明没挂但被阻塞住了也会报这个错。可以适当调大repl-timeout。

2)Master显示:

# Client addr=10.175.162.123:44670 flags=S oll=104654 omem=2147487792 events=rw cmd=sync scheduled to be closed ASAP for overcoming of output buffer limits.

当slave没挂但被阻塞住了,比如正在loading Master发过来的RDB, Master的指令不能立刻发送给slave,就会放在output buffer中(见oll是命令数量,omem是大小),

在配置文件中有如下配置:client-output-buffer-limit slave 256mb 64mb 60, 这是说负责发数据给slave的client,如果buffer超过256m或者连续60秒超过64m,就会被立刻强行关闭!!! Traffic大的话一定要设大一点。

否则就会出现一个很悲剧的循环,Master传输一个大的RDB给Slave,Slave努力的装载,但还没装载完,Master对client的缓存满了,再来一次。

平时可以在master执行 redis-cli client list 找那个cmd=sync,flag=S的client,注意OMem的变化。

Fail-Over

Redis-sentinel是2.6版开始加入的另一组独立运行的节点,提供自动Fail Over的支持。

官方文档Redis核心解读–集群管理工具(Redis-sentinel)

antirez 对 Sentinel的反驳,与下篇

主要执行过程

Sentinel每秒钟对所有master,slave和其他sentinel执行Ping,redis-server节点要应答+PONG或-LOADING或-MASTERDOWN.

如果某一台Sentinel没有在30秒内(可配置得短一些哦)收到上述正确应答,它就会认为master处于sdown状态(主观Down)

它向其他sentinel询问是否也认为该master倒了(SENTINEL is-master-down-by-addr ), 如果quonum台(默认是2)sentinel在5秒钟内都这样认为,就会认为master真是odown了(客观Down)。

此时会选出一台sentinel作为Leader执行fail-over, Leader会从slave中选出一个提升为master(执行slaveof no one),然后让其他slave指向它(执行slaveof new master)。

master/slave 及 其他sentinel的发现

master地址在sentinel.conf里, sentinel会每10秒一次向master发送INFO,知道master的slave有哪些。 如果master已经变为slave,sentinel会分析INFO的应答指向新的master。

以前,sentinel重启时,如果master已经切换过了,但sentinel.conf里master的地址并没有变,很可能有悲剧发生。

另外master重启后如果没有切换成slave,也可能有悲剧发生。新版好像修复了一点这个问题,待研究。

另外,sentinel会在master上建一个pub/sub channel,名为”sentinel:hello”,通告各种信息,sentinel们也是通过接收pub/sub channel上的+sentinel的信息发现彼此,因为每台sentinel每5秒会发送一次自己的host信息,宣告自己的存在。

自定义reconfig脚本

sentinel在failover时还会执行配置文件里指定的用户自定义reconfig脚本,做用户自己想做的事情,比如让master变为slave并指向新的master。

脚本的将会在命令行按顺序传入如下参数:

脚本返回0是正常,如果返回1会被重新执行,如果返回2或以上不会。 如果超过60秒没返回会被强制终止。

觉得Sentinel至少有两个可提升的地方:

一是如果master 主动shutdown,比如系统升级,有办法主动通知sentinel提升新的master,减少服务中断时间。

二是比起redis-server太原始了,要自己丑陋的以nohup sentinel > logfile 2>&1 & 启动,也不支持shutdown命令,要自己kill pid。

Client的高可用性

基于Sentinel的方案,client需要执行语句SENTINEL get-master-addr-by-name mymaster 可获得当前master的地址。

Jedis正在集成sentinel,已经支持了sentinel的一些指令,但还没发布,但sentinel版的连接池则暂时完全没有,在公司的项目里我参考网友的项目自己写了一个。

淘宝的Tedis driver,使用了完全不同的思路,不基于Sentinel,而是多写随机读, 一开始就同步写入到所有节点,读的话随便读一个还活着的节点就行了。

但有些节点成功有些节点失败如何处理? 节点死掉重新起来后怎么重新同步?什么时候可以重新Ready? 所以不是很敢用。

另外如Ruby写的redis_failover,也是抛开了Redis Sentinel,基于ZooKeeper的临时方案。

Redis作者也在博客里抱怨怎么没有人做Dynamo-style 的client

Geographic Replication

没有特别支持,依然用Master Slave复制,3Scale想出了诸如用压缩的SSH隧道降低传输量等方法

运维

安装

安装包制作:没有现成,需要自己编译,自己写rpm包的脚本,可参考utils中的install_server.sh与redis_init_script。

但RHEL下设定script runlevel的方式不一样,redis_init_script中要增加一句 “# chkconfig: 345 90 10” ,而install_server.sh可以删掉后面的那句chkconfig --level 345 reis

云服务:Redis Cloud,在Amazon、Heroku、Windows Azure、App Frog上提供云服务,供同样部署在这些云上的应用使用。

其他的云服务有GarantiaData,已被redis-cloud收购。另外还有Redis To Go, OpenRedis, RedisGreen

CopperEgg统计自己的用户在AWS上的数据库部署:mysqld占了50%半壁江山, redis占了18%排第二, mongodb也有11%, cassandra是3%,Oracle只有可怜的2%。

Chef Recipes:brianbianco/redisio,活跃,同步更新版本。

部署模型

Redis只能使用单线程,为了提高CPU利用率,有提议在同一台服务器上启动多个Redis实例,但这会带来严重的IO争用,除非Redis不需要持久化,或者有某种方式保证多个实例不会在同一个时间重写AOF。

一组sentinel能同时监控多个Master。

有提议说环形的slave结构,即master只连一个slave,然后slave再连slave,此部署有两个前提,一是有大量的只读需求需要在slave完成,二是对slave传递时的数据不一致性不敏感。

配置

约30个配置项,全都有默认配置,对redif.conf默认配置的修改见附录1。

三条路

可以配置文件中编写。

可以在启动时的命令行配置,redis-server –port 7777 –slaveof 127.0.0.1 8888。

云时代大规模部署,把配置文件满街传显然不是好的做法, 可以用redis-cli执行Config Set指令, 修改所有的参数,达到维护人员最爱的不重启服务而修改参数的效果,而且在新版本里还可以执行 Config Rewrite 将改动写回到文件中,不过全部默认值都会打印出来,可能会破坏掉原来的文件的排版,注释。

安全保护

在配置文件里设置密码:requirepass foobar。

禁止某些危险命令,比如残暴的FlushDB,将它rename成”“:rename-command FLUSHDB ““。

监控与维护

综述: Redis监控技巧

监控指令

Info指令将返回非常丰富的信息。 着重监控检查内存使用,是否已接近上限,used_memory是Redis申请的内存,used_memory_rss是操作系统分配给Redis的物理内存,两者之间隔着碎片,隔着Swap。

还有重点监控 AOF与RDB文件的保存情况,以及master-slave的关系。Statistic 信息还包括key命中率,所有命令的执行次数,所有client连接数量等, CONFIG RESETSTAT 可重置为0。

Monitor指令可以显示Server收到的所有指令,主要用于debug,影响性能,生产环境慎用。

SlowLog 检查慢操作(见2.性能)。

Trouble Shooting支持

日志可以动态的设置成verbose/debug模式,但不见得有更多有用的log可看,verbose还会很烦的每5秒打印当前的key情况和client情况。指令为config set loglevel verbose。

最爱Redis的地方是代码只有2.3万行,而且编码优美,而且huangz同学还在原来的注释上再加上了中文注释——Redis 2.6源码中文注释版 ,所以虽然是C写的代码,虽然有十年没看过C代码,但这几天trouble shooting毫无难度,一看就懂。

Trobule shotting的经历证明antirez处理issue的速度非常快(如果你的issue言之有物的话),比Weblogic之类的商业支持还好。

持久化文件维护

如果AOF文件在写入过程中crash,可以用redis-check-aof修复,见5.1.2

如果AOF rewrite和 RDB snapshot的过程中crash,会留下无用的临时文件,需要定期扫描删除。

三方工具

官网列出了如下工具,但暂时没发现会直接拿来用的:

Redis Live,基于Python的web应用,使用Info和Monitor获得系统情况和指令统计分析。 因为Monitor指令影响性能,所以建议用cron定期运行,每次偷偷采样两分钟的样子。

phpRedisAdmin,基于php的Web应用,目标是MysqlAdmin那样的管理工具,可以管理每一条Key的情况,但它的界面应该只适用于Key的数量不太多的情况,Demo。

Redis Faina,基于Python的命令行,Instagram出品,用户自行获得Monitor的输出后发给它进行统计分析。由于Monitor输出的格式在Redis版本间不一样,要去github下最新版。

Redis-rdb-tools 基于Python的命令行,可以分析RDB文件每条Key对应value所占的大小,还可以将RDB dump成普通文本文件然后比较两个库是否一致,还可以将RDB输出成JSON格式,可能是最有用的一个了。

Redis Sampler,基于Ruby的命令行,antirez自己写的,统计数据分布情况。

Java Driver

Driver选择

各个Driver好像只有Jedis比较活跃,但也5个月没提交了,也是Java里唯一的Redis官方推荐。

Spring Data Redis的封装并不太必要,因为Jedis已足够简单,没有像Spring Data MongoDB对MongoDB java driver的封装那样大幅简化代码,顶多就是加强了一点点点pipeline和transaction状态下的coding,禁止了一些此状态下不能用的命令。

而所谓屏蔽各种底层driver的差异并不太吸引人,因为我就没打算选其他几种driver。有兴趣的可以翻翻它的JedisConnection代码

所以,SpringSide直接在Jedis的基础上,按Spring的风格封装了一个JedisTemplate,负责从池中获取与归还Jedis实例,处理异常。

Jedis的细节

Jedis基于Apache Commons Pool做的连接池,默认MaxActive最大连接数只有8,必须重新设置。而且MaxIdle也要相应增大,否则所有新建的连接用完即弃,然后会不停的重新连接。

另外Jedis设定了每30秒对所有连接执行一次ping,以发现失效的连接,这样每30秒会有一个拿不到连接的高峰。

但效果如何需要独立分析。比如系统高峰之后可能有一长段时间很闲,而且Redis Server那边做了Timeout控制会把连接断掉,这时候做idle checking是有意义的,但30秒一次也太过频繁了。否则关掉它更好。

Jedis的blocking pop函数,应用执行ExecutorService.shutdownNow()中断线程时并不能把它中断,见讨论组。

两个解决方法:

不要用不限时的blocking popup,传多一个超时时间参数,如5秒。

找地方将调用blocking popup的jedis保存起来,shutdown时主动调用它的close。

Redis对Client端连接的处理

Redis默认最大连接数是一万。

Redis默认不对Client做Timeout处理,可以用timeout 项配置,但即使配了也不会非常精确。

Windows的版本

Windows版本方便对应用的本地开发调试,但Redis并没有提供,好在微软提供了一个依赖LibUV实现兼容的补丁,https://github.com/MSOpenTech/redis,但redis作者拒绝合并到master中,微软只好苦憋的时时人工同步

目前的稳定版是2.6版本,支持Lua脚本。

因为github现在已经没有Download服务了,所以编译好的可执行文件藏在这里:

https://github.com/MSOpenTech/redis/tree/2.6/bin/release

成功案例

注:下文中的链接都是网站的架构描述文档。

Twitter新浪微博, 都属于将Redis各种数据结构用得出神入化的那种,如何发布大V如奥巴马的消息是它们最头痛的问题。

Tumblr: 11亿美刀卖给Yahoo的图片日志网站,22 台Redis server,每台运行8 - 32个实例,总共100多个Redis实例在跑。

有着Redis has been completely problem free and the community is great的崇高评价。Redis在里面扮演了八爪鱼多面手的角色:

Dashboard的海量通知的存储。

Dashboard的二级索引。

存储海量短链接的HBase前面的缓存。

Gearman Job Queue的存储。

正在替换另外30台memcached。

Instagram ,曾经,Redis powers their main feed, activity feed, sessions system, and other services

但可惜目前已迁往Cassandra,说新架构只需1/4的硬件费用,是的,就是那个导致Digg CTO辞职的Canssandra。

Flickr , 依然是asynchronous task system and rudimentary queueing system。之前Task system放在mysql innodb,根本,撑不住。

The Others:

Pinterest,混合使用MySQL、Membase与Redis作为存储。

Youporn.com,100%的Redis,MySQL只用于创建新需求用到的sorted set,300K QPS的大压力。

日本微信 ,Redis在前负责异步Job Queue和O(n)的数据,且作为O(n*t)数据的cache,HBase在后,负责O(n*t)数据, n是用户,t是时间。

StackOverflow ,2 Redis servers for distribute caching,好穷好轻量。

Github,任务系统Resque的存储。

Discourge,号称是为下一个十年打造的论坛系统, We use Redis for our job queue, rate limiting, as a cache and for transient data,刚好和我司的用法一样。

情色网站 YouPorn,使用 Redis 进行数据存储,Redis 服务器每秒处理30万个页面请求,每小时会记录8-15GB数据。

In SpringSide

extension modules项目封装了常用的函数与场景,showcase example的src/demo/redis目录里有各场景的benchmark测试。

Jedis Template

典型的Spring Template风格,和JdbcTemplate,HibernateTemplate一样,封装从JedisPool获取与归还Connecton的代码,有带返回值与无返回值两种返回接口。

同时,对最常用的Jedis调用,直接封装了一系列方法。

Scheduler与Master Elector

Scheduler实现了基于Redis的高并发单次定时任务分发。具体选型见Scheduler章节。

Master Elector基于redis setNx()与expire()两个api实现,与基于Zookeeper,Hazelcast实现的效果类似。

Showcase中的Demo

计有Session,Counter,Scheduler 与 Master Elector四款。

附录

附录1: 对redis.conf默认配置的修改

Master主机

daemonize no -> yes ,启动daemonize模式,注意如果用daemon工具启动redis-server时设回false。 logfile stdout -> /var/log/redis/redis.log ,指定日志文件 注释掉RDB的所有触发规则,在Master不保存RDB文件。 dir ./ -> /var/data/redis,指定持久化文件及临时文件目录. maxmemory,设置为可用内存/2. (可选)appendonly no->yes,打开AOF文件. auto-aof-rewrite-percentage 100, 综合考虑硬盘大小,可接受重启加载延时等尽量的大,减少AOF rewrite频率. auto-aof-rewrite-min-size 64mb,同上,起码设为5G. client-output-buffer-limit slave 256mb 64mb 60. 考虑Traffic及Slave同步是RDB加载所需时间,正确设置避免buffer撑爆client被关掉后又要重新进行全同步。 Master上的安全配置,可选。

Slave主机

设置RDB保存频率,因为RDB只作为Backup工具,只保留15分钟的规则,设置为15分钟保存一次就够了save 900 1。

(可选)slaveof 设置master地址,也可动态设定。

repl-timeout 60, 适当加大比如120,避免master实际还没倒掉就认为master倒了。

附录2:版本变更历史

3.0.1版-3.0.3版 2013-8-1,在微博发布后反应良好,持续修改。

3.0版 2013-6-29,在公司Workshop后修订,提高wiki的可读性而不只是简单的记录知识点。

附录3:其他参考资料

Redis的几个认识误区 by Tim yang。

Trouble Shooting —— Enable AOF可能导致整个Redis被Block住,在3.0.6版本仍然存在

Redis会有短暂的几秒Block,应用报:Jedis connection failed, retrying…

这个问题现象是这样的,应用周期性的报:Jedis connection failed, retrying…,Redis开启AOF会被Block住导致无法连接,查看redis的日志

1486:M 09 Oct 09:33:18.072 * 10 changes in 300 seconds. Saving...
1486:M 09 Oct 09:33:18.075 * Background saving started by pid 20706
1486:M 09 Oct 09:33:34.011 * Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
20706:C 09 Oct 09:33:42.629 * DB saved on disk
20706:C 09 Oct 09:33:42.630 * RDB: 178 MB of memory used by copy-on-write
1486:M 09 Oct 09:33:42.723 * Background saving terminated with success

重点:Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.

为什么每次写入磁盘会有disk is busy?这个问题?

网上有人写到:当AOF rewrite 15G大小的内存时,Redis整个死掉的样子,所有指令甚至包括slave发到master的ping,redis-cli info都不能被执行。

原因分析

官方文档,由IO产生的Latency详细分析, 已经预言了悲剧的发生,但一开始没留意。

Redis为求简单,采用了单请求处理线程结构。

打开AOF持久化功能后, Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer,如果此时write(2)被阻塞,Redis就不能处理下一个事件。

Linux规定执行write(2)时,如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘,或者有system wide sync在执行,write(2)会被Block住,整个Redis被Block住。

如果系统IO繁忙,比如有别的应用在写盘,或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件,虽然各自都在连续写,但两个文件间的切换使得磁盘磁头的寻道时间加长),就可能导致fdatasync(2)迟迟未能完成从而Block住write(2),Block住整个Redis。

为了更清晰的看到fdatasync(2)的执行时长,可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”,但会影响系统性能。

Redis提供了一个自救的方式,当发现文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被Block。但如果已经超过两秒都还是这个样子,则会硬着头皮执行write(2),即使redis会被Block住。

此时那句要命的log会打印:“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.”

之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。

因此,对于fsync设为everysec时丢失数据的可能性的最严谨说法是:如果有fdatasync在长时间的执行,此时redis意外关闭会造成文件里不多于两秒的数据丢失。

如果fdatasync运行正常,redis意外关闭没有影响,只有当操作系统crash时才会造成少于1秒的数据丢失。

影响版本

网上有说是在2.6.12版之前,但是我们使用的版本:redis_version:3.0.6 任然存在这个问题

解决方法

最后发现,原来是AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。在RedHat Enterprise 6里,默认配置vm.dirty_background_ratio=10,也就是占用了10%的可用内存才会开始后台flush,而我的服务器有8G内存。

很明显一次flush太多数据会造成阻塞,所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M),问题解决。

然后提了个issue,AOF rewrite时定时也执行一下fdatasync嘛, antirez回复新版中,AOF rewrite时32M就会重写主动调用fdatasync。

  • 查看一下系统内核参数
>sysctl -a | grep dirty_background_ratio
vm.dirty_background_ratio = 10

>sysctl -a | grep vm.dirty_bytes
vm.dirty_bytes = 0

ps.尝试修改一下

  • 编辑/etc/sysctl.conf
>vi /etc/sysctl.conf

## 在最后面增加
# 32M
vm.dirty_bytes=33554432

ps.保存后下次启动会生效,下面是立即生效的修改方法:

  • 立即生效的修改方法
>sysctl vm.dirty_bytes=33554432
>sysctl -p
  • 验证修改是否成功
>sysctl -a | grep vm.dirty_bytes
vm.dirty_bytes = 33554432
  • 修改后redis下次RDB和AOF时的日志
1486:M 09 Oct 10:05:02.043 * 10 changes in 300 seconds. Saving...
1486:M 09 Oct 10:05:02.046 * Background saving started by pid 20987
20987:C 09 Oct 10:05:17.188 * DB saved on disk
20987:C 09 Oct 10:05:17.188 * RDB: 944 MB of memory used by copy-on-write
1486:M 09 Oct 10:05:17.274 * Background saving terminated with success

从redis的日志中发现已经没有了这句:Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.

应用日志中也看不到:Jedis connection failed, retrying...异常

这个问题解决