zzzvvvxxxd

最快的外卖小哥


  • Home

  • About

  • Archives

【排查日记】Pigeon序列化错误死循环错误引起的应用僵死

Posted on 2017-12-04 | In 经验 |

前因后果

外卖某需求在上线前,有同学反馈测试环境机器每次部署完几分钟后就会进入无响应状态,显然是因为某种原因,应用发生了僵死。由于发生在上线前所以立即进行排查,并找出Pigeon(大众点评自研RPC框架)的一个bug,并提前避免了严重的线上故障。

排障流程

1. 更换测试环境机器

由于僵死现象之前开发和测试过程中一直没有发生,第一时间先选择相信代码质量,同时,由于beta环境中一台宿主机会运行超过100台的虚拟机,所以出现宿主机资源不足导致虚拟机僵死是有可能发生的。所以第一步是更换测试环境机器来排除这个原因,遗憾的是应用依然会进入僵死状态。

2. 进行初步分析,并收集机器状态

僵死的出现,显然是CPU出现了瓶颈,因此在机器上使用:

top -H

发现机器的us和应用的java进程的%CPU都已经打满了四核。对于Java应用而言一般出现这种情况,可能有两种原因:

  1. 大量线程频繁执行无阻塞、循环、正则或纯粹的计算等动作造成
  2. 频繁的GC。如每次请求都需要单配较多内存,当访问量过高时就导致不断的进行GC,系统响应速度下降,进而造成堆积的请求过多,消耗的内存严重不足,最严重的情况会使系统不断进行Full GC,对频繁的gc需要通过分析jvm内存的消耗来查找原因

因此我立马拉下了监控上该机器的GC报表,发现确实进入了频繁的GC状态,每秒的GC数量高的吓人。一般GC较高的处理也没有更多的办法,只能拉下服务器dump进行观察。但是这里有一点需要注意的是,在文中的场景下, dump分析不仅仅是在观察内存中内存占用情况,也是需要通过内存对象来校验GC的源自频繁的内存分配还是内存泄露。
分析的结果倒是出人意料的简单,内存中大多数都是log4j2和char。显然是日志打爆了机器,也引出了更多的结论:

  1. 日志疯狂打印多半是程序进入了无阻塞的循环或者某种现象不断发生
  2. 排查一下服务器日志文件,找到日志应该就可以解决问题

3. 查找日志

首先,这么大量的日志肯定会在磁盘容量上会有体现。

df -H

发现inf文件夹下的日志文件都接近16GB。立马打开文件夹发现了pigeon正在风控打印encode相关的日志,然后观察stack strce,立马发现了重复的循环日志。至此基本定位到了问题,上报给了Pigeon。

4. 根因

首先,Pigeon作为RPC框架在遇到异常时的处理逻辑是Server的异常会上抛Client来进行处理。这次的业务开发,我们提供了Pigeon的Server端,供给给外卖某thrift client调用。在这种服务模式下pigeon
需要对server response进行thrift编码,问题就发生在如果Server抛出异常,Pigeon会将异常作为reponse上抛。在对Exception进行thrift编码bug导致又出现了异常,此时还没有走出pigeon的发送逻辑,pigeon会再次捕捉这个异常并将其认定为Server
端的异常,重新作为reponse尝试进行thrift编码。这样这个Exception就触发了这样的循环过程。

Exception -> 编码 -> new Exception -> 编码 -> new Exception -> ...

值得一提的,是这样的异常只会在服务返回值为void的情况下发生,代码分析就略过。只能说Pigeon的同学确实疏忽了这里的测试,这种稳定必现的bug实在是不应该。

【一致性协议05】 ZAB协议详细介绍

Posted on 2016-08-22 | In 分布式 |

一、消息广播

1.1 类似二阶段提交的消息广播

ZAB协议的消息广播过程本质就是一个简单的原子广播协议,类似于2PC。针对客户端的事务请求,Leader服务器会为其是生成对应的事务Proposal,并将其发送给集群中其余的所有机器,然后再分别收集各自的选票,最后进行事务提交。
ZAB协议消息广播流程示意图

和2PC略有不同,这里的二阶段提交过程移除了中断(abort)逻辑。所有的Follower要么正常反馈,要么就抛弃Leader服务器。同时ZAB协议将二阶段提交中的中断逻辑移除意味着我们可以在过半的Follower服务器已经反馈Ack之后就开始提交Proposal了,而不需要等待集群中的所有Follower服务器都反馈响应。

在这种简化的二阶段提交模型下,无法处理Leader服务器崩溃带来的数据不一致问题,因此引入了崩溃回复模式。

1.2 ZXID

Leader会为每个事物Proposal分配一个全局唯一的单调递增ID:ZXID(事务ID)。由于ZAB协议要保证每一个消息严格的因果关系,因此必须将每一个事务Proposal按照其ZXID先后顺序来进行排序与处理:

1.3 具体流程

  • Leader为每一个Follower服务器分配一个单独的队列,然后将需要广播的事务Proposal依次放入队列中,并且根据FIFO策略进行消息发送。
  • Follower接受到Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给Leader一个Ack
  • 当接收到超过半数Follower反馈Ack之后,就会广播一个Commit消息给所有Follower来通知事务提交,同时Leader自身提交该事务
  • Follower收到commit之后进行事务提交

二、崩溃恢复

2.1 概述

  • Leader崩溃
  • 网络原因导致Leader失去与过半Follower的联系

则进入崩溃恢复模式。恢复过程结束后,还需要选举出新的Leader。Leader选举算法满足:

  • 确保能够快速地选举出新的Leader
  • Leader自身知道已经被选举为Leader
  • 让集群中其他的机器快速感知到新的Leader服务器

2.2 基本特性

ZAB协议规定:

如果一个事务Proposal在一台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。

在崩溃恢复过程中可能会出现两种数据不一致的隐患:

2.2.1 ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交

崩溃恢复1
Leader(Server1)已经得到过半的Ack反馈,但是在它将Commit消息发送给所有Follower之前,Leader挂了。
如上图,ZAB需要确保事务P2最终能够在所有的服务器上都被提交成功,否则将出现不一致。

2.2.2 ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务

崩溃恢复2
如果在崩溃恢复过程中出现一个需要被丢弃的提案,那么在崩溃恢复后需要跳过该事物。
如上图,Leader(Server1)在提出P3后崩溃退出,其他机器都没有收到P3,于是当Server1恢复后,ZAB需要确保其丢弃事务P3。

2.3 选举算法

以上决定了ZAB需要设计这一个Leader算法:

能够确保提交已经被Leader提交的事务Proposal,丢弃已经被跳过的Proposal

选举出来的Leader具有最高ZXID,保证了新的Leader一定具有所有已经提交的提案,也省去了检查Proposal的提交和丢弃的工作。

三、数据同步

3.1 正常的数据恢复流程

Leader服务器会为每个Follower服务器准备一个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送,并紧跟一个commit消息。等到Follower服务器将所有的尚未同步事务Proposal都从Leader服务器上同步过来并成功应用到内存数据库后,Leader服务器就会将该Follower加入到可用Follower列表中,开始后续流程。

3.2 如何处理需要被丢弃的事务Proposal

ZXID:
64位数字

  • 低32位看成是一个简单的递增计数器,针对每一个客户端请求,Leader每生成一个Proposal就会进行加1
  • 高32位代表Leader周期epoch的编号,每当选举产生一个新的Leader,epoch+1,低32位同时恢复为0

基于这样的策略,当一个包含了上一个Leader周期中没有提交的事务Proposal的服务器启动时,其肯定无法成为Leader,则以Follower角色连上当前的Leader服务器。Leader会根据自己服务器上最后提交的Proposal和Follower的Proposal进行比较,Leader会要求Follower进行一定的回退操作,回退到最后一个确认已经被过半机器提交的事务。

【Linux命令】照看好咱们的进程和线程

Posted on 2016-08-20 | In Linux |

1. 查找占CPU最多的线程

步骤1: 查找出占用CPU最高的java线程pid

1
top -Hp 18207 #查看运行进程18207内所有线程的CPU消耗情况

可以找到%CPU项最高的一个pid,就是该线程,假设pid是18250,将其转换为16进制:0x474a

步骤2: jstack查看各个线程栈

1
jstack 18207

找到nid为0x474a的线程栈信息,其中包含了该线程的name,OK定位到了

2. 查看一个进程运行了多久

1
2
3
4
ps -p {PID-HERE} -o etime
ps -p {PID-HERE} -o etimes
ps -p {PID-HERE} -o etime=
ps -p {PID-HERE} -o etimes=

实际上可以打印的信息有很多:

1
ps -p 6176 -o pid,cmd,etime,uid,gid

3. 统计一个进程的线程数

方法1: ps

1
ps hH p <pid> | wc -l

方法2:/proc
proc 伪文件系统,它驻留在 /proc 目录,这是最简单的方法来查看任何活动进程的线程数。 /proc 目录以可读文本文件形式输出,提供现有进程和系统硬件相关的信息如 CPU、中断、内存、磁盘等等

1
cat /proc/<pid>/status

上面的命令将显示进程 的详细信息,包括过程状态(例如, sleeping, running),父进程 PID,UID,GID,使用的文件描述符的数量,以及上下文切换的数量

4. 查看某个进程的线程

方法1:

1
ps -T -p <pid>

SID栏表示线程ID,而CMD栏则显示了线程名称

方法2:
top可以使用-H开启线程查看模式,如果要查看特定进程的线程:

1
top -H -p <pid>

方法3:
要在htop中启用线程查看,请开启htop,然后按<F2>来进入htop的设置菜单。选择“设置”栏下面的“显示选项”,然后开启树状视图和显示自定义线程名选项。按<F10>退出设置
Htop的具体使用后续会专门写一篇记录一下。

5. 查看进程被运行在哪个CPU上

方法1:taskset

1
taskset -c -p <pid>

方法2:ps

1
ps -o pid,psr,comm -p pid

方法3:top

1
top -p <pid> # 好处是可以持续观察

【一致性协议04】 ZAB协议概述

Posted on 2016-08-09 | In 分布式 |

所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被成为Leader服务器,而余下的服务器则成为Follower服务器.
Leader服务器负责将一个客户端请求转换成一个事务Proposal(提议),并分发给所有的Follower服务器.之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower进行了正确反馈后,那么Leader就会向所有的Follower服务器分发commit消息,要求将前一个Proposal进行提交

整个ZAB协议主要包括消息广播和崩溃恢复两个过程,进一步可以分为三个阶段,分别是:

  • 发现 Discovery
  • 同步 Synchronization
  • 广播 Broadcast
    组成ZAB协议的每一个分布式进程,都会循环执行这三个阶段,将这样一个循环称为一个主进程周期。

整个服务框架在启动过程中

Leader服务器出现网络中断、崩溃退出与重启等异常情况

ZAB协议就会进入崩溃恢复模式,选举新的Leader服务器。当选举产生新的Leader服务器,同时集群中已有过半的机器与该Leader完成了状态同步之后,ZAB就会退出恢复模式
状态同步是指数据同步,用来保证集群有过半的机器能够和Leader服务器数据状态保持一致。

集群中有过半的Follower和Leader完成了状态同步,那么服务框架就可以进入消息广播模式。

新加入一台机器

当一台遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器会自觉进入数据恢复模式:
找到Leader所在的服务器,与其进行数据同步,然后一起参与到消息广播流程中。

Leader服务器

Leader在收到客户端的事务请求后,会生成对应的Proposal并发起一轮广播协议;而如果集群中其他机器接收到客户端的事务请求,那么这些非Leader服务器首先会将这个事务请求转发给Leader服务器。

消息广播 –> 崩溃恢复模式

(Leader服务器出现崩溃退出或机器重启,亦或时集群中已经不存在过半的服务器与Leader正常通信)
这种情况下,在开始新一轮的原子广播事务之前,所有进程需要使用崩溃恢复协议来让彼此达到一致的状态,于是ZAB的流程就会从消息广播模式进入到崩溃恢复模式。

选举Leader

一个进程要成为新的Leader,必须获得过半的支持。进入崩溃恢复模式后,只要集群中存在过半的机器能够彼此通信,那么就可以产生一个新的Leader,进入消息广播模式
例子:
一个由3台机器组成的ZAB服务,通常由一个Leader,2个Follower组成。某一个时刻,其中一个Follower服务器挂了,整个ZAB不会中断服务,因为Leader服务器依然能够获取过半机器(包括自己)的支持

【一致性协议03】 Paxos和分布式系统

Posted on 2016-08-08 | In 分布式 |

资源汇总

  • Paxos Made Simple
  • 使用Basic-Paxos协议的日志同步与恢复
  • 可靠分布式系统基础 Paxos 的直观解释
  • paxos和分布式系统(视频)

Paxos和分布式存储系统(问题定义)

Paxos用来确定一个不可变变量的取值

  • 取值可以是任意二进制数据
  • 一旦确定将不再更改,并且可以被获取到(不可变性、可读取性)

在分布式存储系统中应用Paxos

  • 在分布式存储系统中,数据本身可变,采用多部分进行存储
  • 多个副本的更新操作序列[op1, op2, op3, ... , opn]是相同的、不变的。
  • 用Paxos依次来确定不可变变量opi的取值(即第i个操作时什么)
  • 每确定完Opi之后,让各个数据副本执行opi

将问题转换为,设计一个系统,来存储名称为var的变量

  • 系统内部由多个Acceptor组成,负责存储和管理var变量
  • 外部有多个proposer机器任意并发调用API,向系统提交不同的var取值
  • var可以是任意二进制数据
  • 系统对外的API库接口为:
    • propose(var, V)返回<ok, f>或者<error>(f是系统内部已经确定的取值,如果propose设置成功则f为V,否则为其他proposer设置成功的结果)

系统要保证var的取值满足一致性

  • 如果var的取值没有确定,则var的取值为null
  • 一旦var的取值被确定,则不可更改。并且一直可以获取到这个值

系统要满足一定的容错特性

  • 可以容忍任意proposer机器出现故障
  • 可以容忍少数Acceptor故障(半数以下)

暂时不考虑:

  • 网络分化
  • Acceptor故障会丢失var

难点

分布式环境下确定一个不可变变量的难点有:

  • 管理多个Proposer的并发执行
  • 保证var变量的不可变性
  • 容忍任意Proposer机器故障
  • 容忍半数以下Acceptor机器故障

方案1

先考虑由单个Acceptor组成,通过类似互斥锁机制来管理并发的Proposer运行

  • Proposer首相向Acceptor申请Acceptor的互斥访问权限,然后才能请求Acceptor接受自己的取值
  • Acceptor给Proposer发放互斥访问权,谁申请到互斥访问权,就接受谁提交的值

这样就让Proposer按照获取互斥访问权的顺序依次访问Acceptor,一旦Acceptor接收了某个Proposer的取值,则认为var取值被确定
,其他Proposer不再更改。

基于互斥访问权的Acceptor的实现

1
2
3
4
5
6
7
8
9
10
Acceptor保存变量var和一个互斥锁lock
Acceptor::prepare():
加互斥锁,给予var的互斥访问权,并返回当前的var取值f
Acceptor::relaese():
解互斥锁,收回var的互斥访问权
Acceptor::acceptor(var, V)
如果已经加锁,并且var没有取值,则设置var为V,并且释放锁

Proposer(var, V)的两阶段实现

1
2
3
4
5
6
第一阶段:通过Acceptor::prepare()获取互斥访问权和当前var的取值
如果不能,返回<error>(锁被其他Proposer占用)
第二阶段:根据当前var的取值f,选择执行
如果f为null,则通过Acceptor::accept(var, V)提交数据V
如果f不会null,则通过Acceptor::release()释放访问权,返回<ok,f>

缺陷:

如果Proposer在释放互斥锁之前发生故障,会导致系统陷入死锁。(不能容忍任何Proposer机器故障)

方案2

引入抢占式访问权
Acceptor可以让某个Proposer获取到的访问权失效,不再接收它的访问。之后,可以将访问权交给其他Proposer,让其他Proposer访问Acceptor
Proposer向Acceptor申请访问权时,指定编号epoch,并认为越大的epoch值越新,获取到访问权后,才能向Acceptor提交值。
Acceptor采取喜新厌旧原则:

  • 一旦接收到更新的epoch申请,马上让之前的旧epoch访问权限失效,不再接收它们提交的取值
  • 给新的epoch发放访问权,只接受新epoch提交的取值

也就是新的epoch可以抢占旧的epoch,为了保持一致性,不同epoch的Proposer之间采后者认同前者的原则。

  1. 肯定旧的epoch无法生成确定性取值时,新的epoch会提交自己的value,不会冲突
  2. 一旦旧epoch形成了确定性取值,新的epoch可以获取到该值,并会认同,而不会破坏

基于抢占式访问权的Acceptor的实现

1
2
3
4
5
6
7
8
9
10
11
Acceptor保存的状态
当前var的取值<accepted_epoch, accepted_value>
最新发放访问权的epoch(lastest_prepared_epoch)
Acceptor::prepare(epoch)
只接受比latest_prepared_epoch更大的epoch,并给予访问权
记录latest_prepared_epoch = epoch;返回当前var取值
Acceptor::accept(vae, prepared_epoch, V)
验证latest_prepared_epoch == prepared_epoch,如果不等则说明已经有一个更新的epoch抢占了访问权
设置var的取值<accepted_epoch, accepted_value> = <prepared_epoch, V>

Propose(var, V)的两阶段实现

1
2
3
4
5
6
7
8
9
第一阶段:获取epoch轮次的访问权和当前var的取值
简单选取当前时间戳为epoch,通过Acceptor::prepare(epoch),获取epoch轮次的访问权和当前的var
如果不能获取返回<error>,Proposer运行失效
第二阶段:采用"后者认同前者"原则执行
1. 如果var的取值为null,则肯定旧epoch无法生成确定性取值,则通过Acceptor::accept(var, epoch, V)提交数据。
成功后返回<ok, V>
如果accept失败,则返回<error>(被新的epoch抢占,或者Acceptor故障)
2. 如果var取值存在,则此确定值肯定是确定性取值,此时认同它不再更改,直接返回<ok, accepted_value>

总结:
基于抢占式访问权的核心思想让Proposer将epoch递增的顺序抢占式的依次运行,后者会认同前者。
【优点】:
可以避免方案一种Proposer故障带来的死锁问题,并且仍可以保证var取值的一致性
【缺点】:
单点问题任然存在

Paxos

Paxos就是在方案二的基础上,试图解决Acceptor的单点问题,引入了多Acceptor。仍然采用喜新厌旧的原则,但是
因为引入了多个Acceptor,则不再简单地”后者认同前者”,还采用了”少数服从多数”原则:

一旦某个epoch的取值f被半数以上的Acceptor接受,则认为此var的取值确定为f,不再更改。

Paxos中的Acceptor实现相较方案二保持不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Propose(var, V)第一阶段: 选定epoch,获取epoch访问权和对应的var取值
获取半数以上Acceptor的访问权和对应一组var取值
和方案二一致
Propse(var, V)第二阶段: 采用后者服从前者的原则
# 在肯定旧epoch无法生成确定性取值时,新的epoch会提交自己的取值,不会冲突
# 一旦旧epoch形成确定性取值,新的epoch肯定可以获取到此取值,并且会认同此取值,不会破坏
1. 如果获取的var都为空,则旧epoch无法形成确定性取值,此时努力使<epoch, V>成为确定性取值
* 向epoch对应的所有Acceptor提交取值<epoch, V>
* 如果收到半数以上的成功,则返回<ok, V>
* 否则返回<error>(新epoch抢占或者Acceptor故障)
2. 如果获取的var取值存在,直接认同最大accepted_epoch对应的取值f,努力使<epoch, f>成为确定性取值
* 如果f出现半数以上,则说明f已经是确定性取值,直接返回<ok, f>
* 否则,向epoch对应的所有Acceptor提交取值<epoch, f>

总结:
核心思想:在抢占式访问(方案2)的基础上引入了多Acceptor,保证一个epoch,只有一个Proposer运行,Proposer按照递增的
次序运行。
新epoch的Proposer采用后者认同前者的思路运行。

Paxos算法可以满足容错性要求。

  • 半数以下的Acceptor出现故障时,存货的Acceptor依然可以生成var的确定性取值
  • 一旦var的值被确定,即使出现半数以下的Acceptor故障,此取值可以被获取,且不再被更改。

Paxos算法的Liveness问题

新轮次的抢占会让旧轮次停止运行,如果每一轮在第二阶段执行成功之前都被新一轮次抢占,则导致活锁。如何解决?

【天池比赛日记 - 2】MaxDirectMemorySize和堆外内存

Posted on 2016-07-22 | In JVM |

最近在做阿里的中间件比赛,在第二题的背景下,我们的方案需要一个基于磁盘的map和B+索引,显然IO就成为了瓶颈之一。为了提升IO的速度,我们选择MappedByteBuffer和channel.map()作为我们的IO处理方案。但是这样的方案存在一个问题,堆外内存不受JVM的控制,可能会因为一些原因导致JVM Crash,所以对这个问题做一点必要的研究。

1. 首先

首先,根据JDK中对MappedByteBuffer的说明:

A direct byte buffer whose content is a memory-mapped region of a file.
Mapped byte buffers are created via the FileChannel.map method. This class extends the ByteBuffer class with operations that are specific to memory-mapped file regions.

MappedByteBuffer本质上就一种继承了ByteBuffer的direct byte buffer,是分配在JVM堆外的内存。因而也就不受JVM的控制,也只有在full gc的时候才会被回收。
虽然,堆外内存包含了:

  • jvm本身在运行过程中分配的内存
  • codecache
  • jni里分配的内存
  • DirectByteBuffer分配的内存

我们在比赛过程中更关注的是DirectByteBuffer所产生的堆外内存,包括它的配置和回收等操作。

2. 然后

【比赛陷入僵局,结束了再续写】

【天池比赛日记 - 1】稀疏文件和文件空洞

Posted on 2016-07-21 | In JVM |

log

在Linux和Windows下,我们都曾试图通过fileChannel.map来使用稀疏文件构建<Long, Long>和<Long, String>磁盘map方案。发现,在Windows下,系统和JVM对稀疏文件和MappedByteBuffer支持的更好,在Linux下我们使用了6个线程并发插入(我们的方案是模拟ConcurrentHashMap完成的线程安全map)的情况下,才达到Windows下单线程的构建速度。显然,这是操作系统实现决定的。而且,发现在文件大小小于4GB时,Linux速度较为出色,但是大于4GB就远远逊色于Windows。

另外,我们试图提前构建好空白文件(写入全量的0),但并没有拯救我们的map在Linux下的性能。所以,也导致了后期切换了Map的方案(做到了200s并发插入3亿索引数据)(后面会详细介绍,我们map的实现过程,和实现多线程和Page Cache的思路)。

文件空洞

在unix文件操作中,文件位移量可以大于文件的当前长度。这种情况下,对该文件的下一次写将延长该文件,并在该文件中构成一个空洞。位于文件中,但没有写过的字节都被假设为0。

如果offset比文件的长度更大,下一个写操作就会将文件extend,造成文件空洞(hole)。没有实际写入文件的所有自己由重复的0表示,空洞是否占用磁盘由文件系统决定。

稀疏文件 Sparse File

和普通文件基本相同,区别在于文件中的部分数据是全0,这部分数据不需要占用磁盘空间。
1

inode数据块存储

索引节点采用了多重索引结构,体现在直接指针和三个间接指针,直接指针包含12个直接指针块,直接指向包含文件数据的数据块,紧跟在后面的三个间接指针是为了适应文件的大小变化而设计的

stat命令可以查看某个文件的inode信息

inode会消耗磁盘空间,所以os一般将硬盘分为两个区域,数据区和inode区。每个inode大小一般是128/256byte

sudo dumpe2fs -h /dev/hda | grep “Inode size” # 查看inode节点大小

由于每个文件都必须有一个inode,所以可能出现inode耗尽,但是磁盘未满的情况,此时已经无法创建新的文件。

2

文件系统存储稀疏文件时,inode索引节点中只给出实际占用磁盘空间的Block号,数据全0且不占用磁盘空间的文件Block没有物理磁盘Block号。

文件所占用的磁盘空间仍然是连续的

3

多重索引

Linux文件系统数据结构存放采用inode多重索引结构,有直接指针,和3个间接指针。

  • 直接指针直接指向保存出具的Block

    12个,若Block大小为4096Byte,则可以保存48KB文件

  • 一级指针指向一个Block,该Block中的数据是Block指针,指向真正数据Block

    假设每个指针占用4个字节,则一级指针指向Block可以保存4096/4个指针,即1024个Blocks,算出一级指针可以保存的文件数据大小为4MB

  • 二级和三级类推

    二级: (1024*1024)*4096=4GB ; 三级: (1024*1024*1024) * 4096 = 4TB

【mysql】binlog详解(2) binlog格式

Posted on 2016-06-22 | In 数据库 |

可以指定三种binary log的格式(启动时指定):

--binlog-format=STATEMENT
--binlog-format=ROW
--binlog-format=MIXED
  • statement-based logging: 基于SQL语句,Mysql5.6默认,某些语句和函数如UUID, LOAD DATA INFILE等在复制过程可能导致数据不一致甚至出错。
  • row-based logging:基于行,记录影响table中每一行的事务,很安全,很安全。但是binlog会比其他两种模式大很多,在一些大表中清除大量数据时在binlog中会生成很多条语句,可能导致从库延迟变大。
  • mixed logging:使用statement-based logging作为默认,但是日志模式可能会在某些情况下自动切换到row-based logging。

statement-based logging可能回带来复制上的安全问题:

With statement-based replication, there may be issues with replicating nondeterministic statements. In deciding whether or not a given statement is safe for statement-based replication, MySQL determines whether it can guarantee that the statement can be replicated using statement-based logging. If MySQL cannot make this guarantee, it marks the statement as potentially unreliable and issues the warning, Statement may not be safe to log in statement format.
You can avoid these issues by using MySQL’s row-based replication instead.

除了开头提到的启动参数,binlog的格式还可以在运行时切换:

// GLOBAL
mysql> SET GLOBAL binlog_format = 'STATEMENT';
mysql> SET GLOBAL binlog_format = 'ROW';
mysql> SET GLOBAL binlog_format = 'MIXED';

// SESSION
mysql> SET SESSION binlog_format = 'STATEMENT';
mysql> SET SESSION binlog_format = 'ROW';
mysql> SET SESSION binlog_format = 'MIXED';

一般基于以下几个理由会设置SESSION级别的binlog format切换:

  • 在对数据库做出了很多小的改变时,可能会需要使用 row-based logging
  • 在使用WHERE进行更新操作时,可能会影响很多行记录,使用statement-based logging来记录少量的事务语句日志,会比记录很多行的改动有效得多。
  • 有些语句可能需要执行很长时间,但是实际只改动几行记录。使用row-based logging会对复制功能比较友好。

有些情况切换logging format可能会返回error:

  1. 在使用InnoDB时,如果隔离级别是READ COMMITTED或READ UNCOMMITTED,只有row-based format可以使用。切换到statement-based其实也是可行的,但是很加就会导致错误,因为这种情况下InnoDB就无法插入数据
  2. 临时表存在时,不推荐切换logging format

    Switching the replication format at runtime is not recommended when any temporary tables exist, because temporary tables are logged only when using statement-based replication, whereas with row-based replication they are not logged. With mixed replication, temporary tables are usually logged; exceptions happen with user-defined functions (UDFs) and with the UUID() function.

注意:
ROW格式下依然会有部分的语句按照STATEMENT格式记录,例如所有的DDL语句:CREATE TABLE, ALTER TABLE和 DROP TABLE

阿里霸面的事

Posted on 2016-05-12 | In 日记 |

我一定要5年内进入阿里的中间件团队,再做一次infoq讲师
今年4月12日的时候,我很郑重的对同学们说过这句话。今天是5月12日,恰好一个月。

今天是阿里实习校招的上海站。
距离我之前实习内推失败也有1月有余。

Trie树

Posted on 2016-04-06 | In 多线程系列 |

Trie树模板

题目来源:http://hihocoder.com/problemset/problem/1014

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <iostream>
#include <string>
using namespace std;
enum NODE_TYPE {
COMPLETED,
UNCOMPLETED
};
class TrieNode {
public:
TrieNode* childNodes[26];
int freq;
char nodeChar;
enum NODE_TYPE type;
TrieNode(char ch) {
this->nodeChar = ch;
this->freq = 0;
for(int i = 0; i < 26; i++) {
this->childNodes[i] = NULL;
}
this->type = UNCOMPLETED;
}
void add_freq();
void set_type(enum NODE_TYPE);
};
class Trie {
public:
TrieNode* root;
Trie() {
root = new TrieNode(' ');
}
void insert(string str);
bool find(string str);
int query(string str);
};
int charToIndex(char ch) {
return ch - 'a';
}
void Trie::insert(string str) {
TrieNode *current_node = this->root;
for(int i = 0; i < str.length(); i++) {
int index = charToIndex(str[i]);
char ch = str[i];
if(current_node->childNodes[index] == NULL) {
current_node->childNodes[index] = new TrieNode(ch);
}
current_node->childNodes[index]->add_freq();
if(i == str.length()-1) {
current_node->childNodes[index]->set_type(COMPLETED);
} else if(current_node->childNodes[index]->type != COMPLETED){
current_node->childNodes[index]->set_type(UNCOMPLETED);
}
current_node = current_node->childNodes[index];
}
}
bool Trie::find(string str) {
TrieNode *current_node = this->root;
int i = 0;
while(i < str.length()) {
char ch = str[i];
int index = charToIndex(ch);
if(current_node->childNodes[index] == NULL) {
break;
}
i++;
current_node = current_node->childNodes[index];
}
cout << current_node->nodeChar << " " << current_node->type << endl;
return current_node->type == COMPLETED && i == str.length();
}
int Trie::query(string str) {
TrieNode *current_node = this->root;
int i = 0;
int count;
while(i < str.length()) {
char ch = str[i];
int index = charToIndex(ch);
if(current_node->childNodes[index] == NULL) {
break;
}
i++;
current_node = current_node->childNodes[index];
}
if(i == str.length()){
return current_node->freq;
}
return 0;
}
void TrieNode::set_type(enum NODE_TYPE t) {
this->type = t;
}
void TrieNode::add_freq() {
this->freq++;
}
int main() {
Trie trie;
int loop;
cin >> loop;
while(loop > 0) {
string str;
cin >> str;
trie.insert(str);
loop--;
}
cin >> loop;
while(loop > 0) {
string str;
cin >> str;
cout << trie.query(str) << endl;
loop--;
}
return 0;
}

Java包装类与常量池

Posted on 2016-04-03 | In 笔试 |

Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池。

【不简单的Atomic工具】getAndAddInt in getAndIncrement

Posted on 2016-04-02 | In atomic |

上一篇Atmoic文章中提到,JDK8在AtomicInteger的getAndIncrement方法实现上发生了变化:

JDK7
1
2
3
4
5
6
7
8
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
JDK8
1
2
3
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

getAndAddInt

1
2
3
4
5
6
7
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {
int i;
do
i = getIntVolatile(paramObject, paramLong);
while (!(compareAndSwapInt(paramObject, paramLong, i, i + paramInt)));
return i;
}

1
2
3
4
5
6
7
8
jint
sun::misc::Unsafe::getIntVolatile (jobject obj, jlong offset)
{
volatile jint *addr = (jint *) ((char *) obj + offset);
jint result = *addr;
read_barrier ();
return result;
}

汇编之后

1
2
3
0x0000000002c207f5: lock cmpxchg %r8d,0xc(%rdx)
0x0000000002c207fb: sete %r11b
0x0000000002c207ff: movzbl %r11b,%r11d
1
2
3
0x0000000002cf49c7: mov %rbp,0x10(%rsp)
0x0000000002cf49cc: mov $0x1,%eax
0x0000000002cf49d1: lock xadd %eax,0xc(%rdx)

原本的lock cmpxchg被lock xadd所替换,也就是在1.8之后,在硬件支持lock XADD这样的操作情况下,都会用fetch-and-add来替换CAS操作。

参考:

  • unsafe中getAndAddInt性能疑问

【不简单的Atomic工具】CAS

Posted on 2016-03-27 | In atomic |

CAS算法:

包含三个参数

1
CAS(V, E, N)

  • V 表示要更新的变量
  • E 表示预期值
  • N 表示新值

仅当V值等于E值时,才会将V的值设置为N。如果V的值和E不同,表示已经有其他线程对其进行了更新,则当前线程什么都不做。最后CAS返回V的真实值。

CAS是抱着乐观的态度进行的,总认为自己可以成功的完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出。失败的线程不会被挂起,而是返回失败信息,并允许再次尝试。

CPU指令:

1
2
cmpxchg
# accumulator = AL, AX, or EAX, depending on whether a byte, word, or doubleword comparison is being performed

该指令的基本逻辑如下:

1
2
3
4
5
6
7
if(accumulator == Destination) {
ZF = 1;
Destination = Source;
} else {
ZF = 0;
accumulator = Destination;
}

缺点:

  1. ABA问题。

    因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
    从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    1
    2
    3
    4
    5
    public boolean compareAndSet
    (V expectedReference,//预期引用
    V newReference,//更新后的引用
    int expectedStamp, //预期标志
    int newStamp) //更新后的标志
  2. 循环时间长开销大。

    自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果JVM能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation:内存顺序冲突一般是由伪/假共享引起,假共享是指多个 CPU 同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。

  3. 只能保证一个共享变量的原子操作。

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

AtmoicInteger中的CAS实现

JDK7:

1
2
3
4
5
6
7
8
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}

JDK8:

1
2
3
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看到JDK8在getAndIncrement()方法的实现上做出了一定的改变,在AtomicInteger Java 7 vs Java 8中列出了两个版本的benchmark,JDK8的实现大约提升了1.5~2倍的性能,作者也直呼Unsafe变得更加与有用了。

两个栈组成队列

Posted on 2016-03-26 | In 算法 |

【题目】

编写一个类,用两个栈实现队列的基本操作:add、poll、peek

【解法】

一个栈作为压入栈,在压入数据时只往这个栈中压入,记为stackPush。另一个栈只作为单出栈,在弹出数据时只从这个栈弹出,记为stackPop。
stackPop会将stackPush中的数据整体弹出再压入stackPop作为弹出顺序。
需要做到以下两点:

  1. 如果stackPush要往stackPop中压入数据,那么必须一次性把stackPush中的数据全部压入
  2. 如果stackPop不为空,stackPush绝对不能向stackPop中压入数据

【代码】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TwoStackQueue {
public Stack<Integer> stackPush;
public Stack<Integer> stackPop;
public TwoStackQueue() {
stackPush = new Stack<Integer>();
stackPop = new Stack<Integer>();
}
public void add(int pushInt) {
stackPush.push(pushInt);
}
public int poll() {
putPushToPop();
return stackPop.pop();
}
public int peek() {
putPushToPop();
return stackPop.peek();
}
public void putPushToPop() {
if(stackPop.empty() && stackPush.empty()) {
throw new RuntimeException("Queue is empty");
}else if(stackPop.empty()) {
stackPush.stream().forEach(item -> {
stackPop.push(item);
});
}
}
}

OneToOneQueue —— 终极性能的无锁队列算法

Posted on 2016-03-26 | In 多线程 |

基本的队列设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class OneToOneConcurrentArrayQueue<E> implements Queue<E>{
private final E[] buffer;
private volatile long tail = 0;
private volatile long head = 0;
public OneToOneConcurrentArrayQueue(int capacity) {
buffer = (E[])new Object[capacity];
}
public boolean offer(E e) {
final long currentTail = tail;
final long wrapPoint = currentTail - buffer.length; // tail位置往前移动length个位置
if(head <= wrapPoint) {
return false;
}
buffer[(int)(currentTail % buffer.length)] = e;
tail = currentTail + 1;
return true;
}
public E poll() {
final long currentHead = head;
if(currentHead >= tail) {
return null;
}
final int index = (int)(currentHead % buffer.length);
final E e = buffer[index];
buffer[index] = null;
head = currentHead + 1;
return e;
}
}

这样的队列性能损耗主要是一下两点:

  1. 取余的计算
  2. volatile write以及该过程中可能发生的false sharing
    因为我们探讨的OneToOne队列,那么显然volatile的刷新要求并不是很严格,可以不用每次的写操作都从cache中刷回主存

改进 - 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public final class OneToOneConcurrentArrayQueue2<E>
implements Queue<E>
{
private final int mask;
private final E[] buffer;
private final AtomicLong tail = new AtomicLong(0);
private final AtomicLong head = new AtomicLong(0);
public OneToOneConcurrentArrayQueue2(int capacity) {
// 确保capacity是2^n
capacity = findNextPositivePowerOfTwo(capacity);
mask = capacity - 1;
buffer = (E[])new Object[capacity];
}
public boolean offer(final E e)
{
final long currentTail = tail.get();
final long wrapPoint = currentTail - buffer.length;
if (head.get() <= wrapPoint) {
return false;
}
// 替换了: buffer[(int)(currentTail % buffer.length)] = e;
// 优化了&操作
buffer[(int)currentTail & mask] = e;
tail.lazySet(currentTail + 1);
return true;
}
public E poll()
{
final long currentHead = head.get();
if (currentHead >= tail.get())
{
return null;
}
final int index = (int)currentHead & mask;
final E e = buffer[index];
buffer[index] = null;
head.lazySet(currentHead + 1);
return e;
}
}

优化点1

注意这里使用(int)currentTail & (capacity - 1)优化了buffer[(int)(currentTail % capacity)] = e;的取余操作。
不过这样的优化存在一个问题就是capacity必须是2^n

优化点2

head和tail用AtomicLong替代long
使用了lazySet来避免了频繁的volatile write在维持内存可见性时的内存损耗


改进 - 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public final class OneToOneConcurrentArrayQueue3<E> implements Queue<E>
{
private final int capacity;
private final int mask;
private final E[] buffer;
private final AtomicLong tail = new PaddedAtomicLong(0);
private final AtomicLong head = new PaddedAtomicLong(0);
public static class PaddedLong {
public long value = 0, p1, p2, p3, p4, p5, p6;
}
private final PaddedLong tailCache = new PaddedLong();
private final PaddedLong headCache = new PaddedLong();
public boolean offer(final E e) {
final long currentTail = tail.get();
final long wrapPoint = currentTail - capacity;
if (headCache.value <= wrapPoint) {
headCache.value = head.get();
if (headCache.value <= wrapPoint) {
return false;
}
}
buffer[(int)currentTail & mask] = e;
tail.lazySet(currentTail + 1);
return true;
}
public E poll()
{
final long currentHead = head.get();
if (currentHead >= tailCache.value) {
tailCache.value = tail.get();
if (currentHead >= tailCache.value) {
return null;
}
}
final int index = (int)currentHead & mask;
final E e = buffer[index];
buffer[index] = null;
head.lazySet(currentHead + 1);
return e;
}
}

优化点:

使用tailCache和headCache来避免对head和tail两个AtomicLong的频繁get(),读取普通变量肯定会比Atomic快很多

实验结果

贴上参考文章中的实验结果:

Qps/Sec(Millions) Mean Latency(ns)
LinkedBlockingQueue 4.3 ~32000 / ~500
ArrayBlockingQueue 3.5 ~32000 / ~600
ConcurrentLinkedQueue 13 NA / ~180
ConcurrentArrayQueue 13 NA / ~150
ConcurrentArrayQueue2 45 NA / ~120
ConcurrentArrayQueue3 150 NA / ~100

Note: None of these tests are run with thread affinity set, Sandy Bridge 2.4 GHz Latency: Blocking - put() & take() / Non-Blocking - offer() & poll()

参考

1. 终极性能的无锁队列算法

False Sharing问题(2)

Posted on 2016-03-26 | In 多线程 |

如果避免频繁的false sharing造成的性能损耗:

PaddedLong

1
2
3
public static class PaddedLong {
public long value = 0, p1, p2, p3, p4, p5, p6;
}

PaddedAtomicLong

1
2
3
4
5
6
7
8
9
10
11
public class PaddedAtomicLong extends AtomicLong {
public PaddedAtomicLong() {
super();
}
public PaddedAtomicLong(final long initialValue) {
super(initialValue);
}
public volatile long p1, p2, p3, p4, p5, p6 = 7;
}

为何是插入7个long

一般而言cache line是64byte,而每个Java对象都有一个2word(2*4byte)的对象头,而long本身是8byte。

PaddedLong的应用

一般是用在频繁读写的情况下,可以参考之后的OneToOne队列的优化文章

False Sharing问题

Posted on 2016-03-25 | In 多线程 |

背景

Cache

对着多核的发展,CPU cache分成了三个级别:L1、L2、L3。L1是最接近CPU的cache,容量也是最小的,但是速度最快。

从CPU到 大约需要的CPU周期 大约需要的时间(单位ns)
寄存器 1 cycle NA
L1 Cache 约3~4 cycles 约0.5~1ns
L2 Cache 约10~20 cycles 约3~7ns
L3 Cache 约40~45 cycles 约15ns
跨槽传输 空 约20ns
内存 约120~240 cycles 约60-120ns

Cache line

为了高效的存取缓存,不是简单地随意地将单条数据写入缓存,缓存是由Cache line构成存储的最小单位,intel平台一般是64字节
可以通过命令

1
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size

Java中long占8个字节,再加上对象头(2字节)或者数组对象头(3字节),则可以获取7个long

Cache条目

cache entry: 包含了如下部分

  • cache line: 从主存一次copy的数据大小
  • tag:标记cache line对应的主存的地址
  • falg:标记当前cache是否invalid,如果是数据cache,还有是否dirty

CPU的cache策略

  1. cpu从不直接访问主存,通过cache间接访问
  2. 每次需要访问主存时,遍历一遍全部cache line,查找主存的地址是否在某个cache line中
  3. 如果cache中没有找到,则分配一个新的cache entry,把主存copy到cache line中,再从cache line中读取
    同时,Cache中包含的cache entry有限,所以必须要有合理cache淘汰策略,一般是LRU,这里不再赘述。

False Sharing

在多处理器,多线程的情况下,如果两个线程分别运行在不同CPU上,而其中某个线程修改了cache line中的元素,由于cache一致性原因,另一个线程的cache的line被宣告无效,在下一次访问时会出现一次cache line miss。

如下图所示,thread1修改了memory灰化区域的第[2]个元素,而Thread0只需要读取灰化区域的第[1]个元素,由于这段memory被载入了各自CPU的硬件cache中,虽然在memory的角度这两种的访问时隔离的,但是由于错误的紧凑地放在了一起,而导致了,thread1的修改,在cache一致性的要求下,宣告了运行Thread0的CPU0的cache line非法,从而出现了一次miss,导致从小从memory中读取到cache line中,而一致性的代价付出了,但结果并不是thread0所care的,导致了效率的降低

figure 1

在做多线程程序的时候,为了避免使用锁,我们通常会采用这样的数据结构:根据线程的数目,安排一个数组, 每个线程一个项,互相不冲突. 从逻辑上看这样的设计无懈可击,但是实践的过程我们会发现这样并没有提高速度. 问题在于cpu的cache line. 我们在读主存的时候,数据同时被读到L1,L2中去,而且在L1中是以cache line(通常64)字节为单位的. 每个Core都有自己的L1,L2,所以每个线程在读取自己的项的时候, 也把别人的项读进去, 所以在更新的时候,为了保持数据的一致性, core之间cache要进行同步, 这个会导致严重的性能问题. 这就是所谓的False sharing问题。

解决

打算分成两篇来写,嘿嘿

参考

False Sharing问题
由一道淘宝面试题到False sharing问题

【不简单的Atomic工具】lazySet & set

Posted on 2016-03-24 | In atomic |

在研究Martin Thompson的演讲终极性能的无锁队列算法时遇到了AtomicLong的lazySet方法,作者用这个方法替代了set。之前在公司设计MulSemaphore类时,我就并没有注意到Atomic类还有这样一个方法。

lazySet()

经常在维护一个非阻塞的数据结构时,内部显然常常会有volatile成员,而对于通信队列这样的对性能要求极端的数据结构,JMM要维持volatile的内存可见性,在对这样的成员写操作时,会有频繁的cache的读写(save和load?)到主内存。这样的时间耗损队形能而言,其实也十分重要。
lazySet()可以让一个线程在没有其他线程volatile写或者synchronized动作发生前,本地缓存的写操作不必刷回主内存。

1
2
3
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}

lazySet的本质就是putOrderInt(),该函数是putIntVolatile()方法的延迟实现。

正是因为这样的特性,使得lazySet适合于SPSC的数据结构中,对于单Producer单Consumer这样的结构,延迟写并不是什么问题。同时还优化了store-load屏障在性能上的损耗(store-store屏障替代)。

【mysql】binlog详解(1)

Posted on 2016-02-13 | In 数据库 |

The binary log contains “events” that describe database changes such as table creation operations or changes to table data. It also contains events for statements that potentially could have made changes (for example, a DELETE which matched no rows), unless row-based logging is used. The binary log also contains information about how long each statement took that updated data.

binlog是mysql的二进制日志,记录了mysql数据的更新或者潜在的更新记录,同时还包含了每条语句的执行耗时和更新时间。
注意: binlog主要有statement-based、row-based logging和mixed logging 三种格式,row-based的记录中不包括潜在更新记录。

binlog有两个主要的功能:

  • 用于复制

    master发送了包含了所有events的binary log给slaves,slaves执行binary log中的event来保证和master之间的数据一致性

  • 一些特定的数据恢复操作,会使用binlog

1. binlog相关的启动参数

1
--binlog-row-event-max-size=N

指定行格式复制日志event的最大大小,单位为byte,每一行数据会被切分到多个小于该限制的event包中,必须是256byte的倍数。

  • 默认值:8192
  • min:256
  • max:18446744073709551615(64位平台)
1
--log-bin[=base_name]

开启binary log功能(用于复制和备份),指定日志名称的base name,mysql会使用指定的base name作为前缀,连续的数据作为后缀生成一系列的日志文件。默认会使用host_name作为base name。

1
--log-bin-trust-function-creators[={0|1}]

指定mysql如何处理函数及存储过程的创建,取决于用户是否认为自己的存储过程及函数是否安全(确定的或者不修改数据),默认对于不安全的存储过程及函数不会写入binlog。这也就是,在开启binlog的时候,如果不设置该参数,使用自定义函数时会出现错误的原因。

2. Statement selection options

下面介绍的选项会影响写入到binary log中的记录语句,因此会控制master发送到slaves中的日志的内容。同样,slaves也有相应的参数,在日志选择哪些命令可以执行。

2.1 --binlog-do-db

1
--binlog-do-db=db_name

这个参数的影响取决是使用statement-based还是row-based logging格式的日志。
statement-based logging
只有操作默认数据库(USE语句指定)的语句才会被记录。如果要指定多个数据库,需要重复使用该参数。但是跨数据库的操作
不会因此被记录,因为这里的检查原则,mysql为了尽可能的简单,只会去检查最近的USE语句指定的数据库是否在该参数指定的数据库中。例如:

1
2
USE sales;
UPDATE prices.discounts SET percentage = percentage + 10;

该语句在参数为--binlog-do-db=sales的情况下会被记录进binlog的,虽然看起来不太能理解。
再举个例子:

1
2
USE prices;
UPDATE sales.january SET amount=amount+1000;

该语句在参数为--binlog-do-db=sales的情况下是不会被记录进binlog的。
Row-based logging
日志会被严格限定在db_name所指定的数据库上,不再受USE的影响。

在跨数据的修改操作上,需要举一个具体的例子来加深说明statement-based和row-based logging两者的区别,假设初始参数为binlog-do-db=db1:

1
2
USE db1;
UPDATE db1.table1 SET col1 = 10, db2.table2 SET col2 = 20;

这条操作statement-based的情况下,对两个表的UPDATE操作都被记录,如果是row-based logging,只有针对table1的操作会被记录。
现在假设更换默认数据库为db4

1
2
USE db4;
UPDATE db1.table1 SET col1 = 10, db2.table2 SET col2 = 20;

statement-based不会记录这条操作,而row-based logging则会记录table1的UPDATE操作日志。

2.2 --binlog-ignore

1
--binlog-ignore-db=db_name

这个参数看起来很好理解,但是要注意的是,CREATE TABLE和ALTER TABLE不会受其影响都一定会被写入log,一般这个参数影响的是UPDATE操作。

2.3 --binlog-format

指定binlog使用的格式,可选:statement、row、mixed,具体参考下一篇日志

更多的配置参数可以参考[1]中的官方说明。

相关系统参数和配置

sync_binlog

binlog刷新到磁盘的时机跟sync_binlog参数相关,如果设置为0,则表示MySQL不控制binlog的刷新,由文件系统去控制它缓存的刷新,而如果设置为不为0的值则表示每sync_binlog次事务,MySQL调用文件系统的刷新操作刷新binlog到磁盘中。设为1是最安全的,在系统故障时最多丢失一个事务的更新,但是会对性能有所影响,一般情况下会设置为100或者0,牺牲一定的一致性来获取更好的性能。

expire_logs_days

指定binlog保留时间

清理binlog

要手动清理binlog可以通过指定binlog名字或者指定保留的日期

1
2
purge master logs to BINLOGNAME;
purge master logs before DATE;

查看binlog情况

1
2
3
4
5
6
7
SHOW MASTER LOGS;
+------------------+-----------+
| mysql-bin.000018 | 515 |
| mysql-bin.000019 | 504 |
| mysql-bin.000020 | 107 |
+------------------+-----------+

第一列是binlog文件名,第二列是binlog文件大小

binlog和redo/undo log的区别

两者是完全不同的日志,主要有一下2个区别:

  • 层次不同。redo/undo log是innodb层维护的,而binlog是mysql server层维护的,跟采用何种引擎没有关系,记录的是所有引擎的更新操作的日志记录。
  • 记录内容不同。redo/undo日志记录的是每个页的修改情况,属于物理日志+逻辑日志结合的方式,目的是保证数据的一致性。binlog记录的都是事务操作内容,格式是二进制的。

参考:

  1. mysql文档 18.1.6.4 Binary Logging Options and Variables
  2. mysql文档 6.4.4 The Binary Log

【一致性协议02】 3PC

Posted on 2015-12-13 | In 分布式 |

3PC是在2PC的基础上,将第二阶段的提交协议中“提交事务请求”过程一分为二,形成了由CanCommit、PreCommit和do Commit三个阶段组成的事务处理协议。

阶段1: CanCommit

  1. 事务询问
  2. 各参与者向协调者反馈事务询问的相应

阶段2: PreCommit

在阶段2中,协调者会根据参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能:

  • 执行事务预提交

    假如协调者从所有参与者获得的反馈都是YES,就会执行事务的预提交

  1. 发送预提交请求
  2. 事务预提交
  3. 各参与者向协调者反馈事务执行的相应
  • 中断事务

    假如任何一个参与者向协调者反馈了NO响应,或者在等待超时之后,协调者尚无法接收到所有的参与者反馈,就会中断事务

  1. 发送中断请求:协调者发送abort请求
  2. 中断事务:无论是收到来自协调者的abort请求,或是在等待协调者请求过程中超时,参与者都会中断事务

阶段3: doCommit

进行真正的事务提交

  • 执行提交
  1. 发送提交请求: 协调者发送doCommit请求
  2. 事务提交
  3. 反馈事务提交结果
  4. 完成事务
  • 中断事务
  1. 发送中断请求
  2. 事务回滚:参与者在接收到abort请求后,会利用在第二个阶段记录的undo信息来执行事务回滚操作,并在回滚之后释放在整个事务执行期间占用的资源。
  3. 反馈事务回滚结果
  4. 中断事务

注意

阶段3可能会出现两种故障:

  • 协调者出现问题
  • 协调者和参与者之间网络出现问题
    无论那种情况,最终都会导致参与者无法及时收到来协调者的doCommit或是abort请求,参与者在等待超时后,会放弃此次事务再次进行新的事务提交。
12

zzzvvvxxxd

30 posts
12 categories
15 tags
RSS
© 2017 zzzvvvxxxd
Powered by Hexo
|
Theme — NexT.Muse v5.1.3