你好,朋友,很高兴能和你一起分享我对Cassandra源码的研究和分析成果,虽然分析不能面面俱到,但是以下的文章涵盖了大部分Cassandra的核心内容,展示了主要的工作逻辑和关键代码。这里我将假定您对Cassandra已经有了基础的了解,假如您对它还一无所知,建议您可以先到Cassandra官网上简单了解一下Cassandra是什么,有什么用途以及NoSQL相比传统的数据库如MySQL,Oracle等有怎样的优势。如果你是一位Cassandra高手,以下内容我有分析不到位的地方,希望你能在评论区留言,我们可以一起讨论,共同进步。
以下我将对Cassandra的一致性哈希原理,Gossip通信协议,集群的备份机制,集群状态变化的处理机制,数据读写一致性原理,数据压缩机制这几个部分进行介绍。
我这里的Cassandra是最新的3.11.1版本,如何在eclipse上启动Cassandra可以参考这篇文章
Cassandra的主要程序入口是org.apache.cassandra.service.EmbeddedCassandraService类
public class EmbeddedCassandraService
{
CassandraDaemon cassandraDaemon;
public void start() throws IOException
{
//开启Cassandra后台的守护进程
cassandraDaemon = CassandraDaemon.instance;
/**
* 1.加载cassandra.yarm配置文件,获取配置信息
* 2.为启动Cassandra节点计算Partitioner,获取哈希值
* 3.获取监听节点,获取rpc(远程节点,如不同数据中心的节点),获取广播节点,获取广播的远程节点
* 4.申请本地数据中节点间的告密者(Snitch)
* 5.初始化节点的令牌,即(Token)
* 6.对Cassandra集群中的种子节点(Seeds)进行配置
* 7.获取EncryptionContext
*/
cassandraDaemon.applyConfig();
/**
* 初始化守护进程自身
* 1.初始化jmx(Java 管理拓展,用于监控程序的性能)
* 2.如果加载mx4j-tools.jar,将开启jmx,监听地址127.0.0.1,默认端口是8081
* 3.开启线程安全管理
* 4.日志的系统输出,内容JVM(Java虚拟机)的版本和参数,(Heap size)堆内存的大小,主机名等等
* 5.不同操作系统连接本地库
* 6.开启检查
* 7.初始化本地表local table,初始化cluster_name,key,data_center,native_protocol_version等等元数据
* 8.从系统表加载TokenMetadata
* 9.从磁盘中加载Schema
* 10.初始化键空间
* 11.加载行键内存
* 12.jmx注册GC(垃圾回收机制)监控,观察性能和内存占用情况
* 13.CommitLog删除磁盘碎片
* 14.启动键空间,启动ActiveRepairService服务,预加载PreparedStatement
* 15.加载metrics-reporter-config配置
* 16.调度线程的一些工作
*/
cassandraDaemon.init(null);
/**
* 2.开启本地节点传输,配置ServerBootstrap
* 3.Gossiper(通信协议)本地节点的状态初始化
*/
cassandraDaemon.start();
}
}
至此复杂而强大的Cassandra项目就启动了,我这里忽略了很多具体细节和部分功能,仅仅展示了主要的启动工作的内容。下面我们就进入正题吧。
1.一致性哈希
一致性哈希是Cassandra的核心内容之一,意在提供一个可以增加机器线性扩展性能的集群。一致性哈希是这样工作的:如上图所示,Cassandra会进行计算一个数据中心一个集群环境中每一个节点的哈希值,并且将该哈希值映射到圆环上,然后从数据映射的位置开始顺时针寻找,将数据保存在找到的第一台服务器上。如果新增加一台服务器只会影响到相邻节点的哈希值,而不需要进行重新计算,最大限度抑制了重新计算分布的时间和性能消耗。2.x版本后的Cassandra加入了虚拟节点的概念更大程度上抑制了分布不均的问题。
哈希值的计算过程主要集中在org.apache.cassandra.dht包中,主要包括了
(1)Token抽象类,每一个节点都对应一个唯一Token
(2)Range类代表从上一个Token(开区间)到下一个Token(闭区间)的一段数据
(3)IPartitioner接口,决定了每一个Token的生成规则,提供了一系列的算法。
由于一致性哈希的代码结构较为简单,算法内容较为枯燥,在此就不进行展示了,感兴趣的读者可以自行翻看源码进行阅读。
2. Gossip:集群节点之间的通信协议
在Cassandra集群中实现节点之间通信的代码在org.apache.cassandra.gms包中,Cassandra各节点之间通过gossip来通信,Gossip初始化时候将构造4个集合分别保存存活的节点(liveEndpoints_),失效的节点(unreachableEndpoints_),种子节点中(seeds_)和各个节点信息(endpointStateMap_)。Cassandra在启动时从配置文件中加载seeds的信息,启动GossipTask定时任务,每隔1秒钟执行一次。
Gossiper.java类中addSavedEndPoint方法添加cassandra.yarm配置中的节点,该方法在StorageService类的initServer方法中调用。
/**
* 加入我们预先定义好的节点,但是节点状态尚未明确
*/
public void addSavedEndpoint(InetAddress ep)
{
//初始保存节点信息,如位于哪一个数据中心,哪一个机架,心跳版本等
EndpointState epState = endpointStateMap.get(ep);
epState.markDead();
**endpointStateMap.put(ep, epState);
unreachableEndpoints.put(ep, System.nanoTime());**
}
接下来进入Gossip定时任务的主要内容:
private class GossipTask implements Runnable
{
public void run()
{
try
{
//等待消息服务开启监听
MessagingService.instance().waitUntilListening();
//加锁
taskLock.lock();
/**
* 更新本节点的心跳版本号.目的在于检查宕机的节点,定时轮询检查
* 最新的心跳,如果检查时时间间隔不合理,就认为该节点宕机了,
* 放入失败队列中,再定时检查是否已经恢复了,可以重新假如集群
*
*/
endpointStateMap.get(FBUtilities.getBroadcastAddress()).getHeartBeatState().updateHeartBeat();
//构建摘要列表
Gossiper.instance.makeRandomGossipDigest(gDigests);
if (gDigests.size() > 0)
{
GossipDigestSyn digestSynMessage = new GossipDigestSyn(DatabaseDescriptor.getClusterName(),
DatabaseDescriptor.getPartitionerName(),
gDigests);
/*创建发送的消息*/
MessageOut<GossipDigestSyn> message = new MessageOut<GossipDigestSyn>(MessagingService.Verb.GOSSIP_DIGEST_SYN,
digestSynMessage,
GossipDigestSyn.serializer);
/* 随机向集群中存活的节点发送GossipDigestSyn消息 */
boolean gossipedToSeed = doGossipToLiveMember(message);
/* 根据一定的概率向未响应的节点发送消息,检查是否能够正常响应 */
maybeGossipToUnreachableMember(message);
if (!gossipedToSeed || liveEndpoints.size() < seeds.size())
/*如果此前发送消息的节点不包含seed节点或者活着的节点数量小于seed数量,则随机向一个seed节点发送消息*/
maybeGossipToSeed(message);
//状态检查
doStatusCheck();
}
}
catch (Exception e)
{
JVMStabilityInspector.inspectThrowable(e);
logger.error("Gossip error", e);
}
finally
{
taskLock.unlock();
}
}
}
然后我们来一起分析一下Gossip接收了哪些消息以及消息的处理逻辑吧,主要分成三步:
- 接收GossipDigestSynMessage消息并处理
- 接收GossipDigestAckMessage消息并处理
- 接收GossipDigestAck2Message消息并处理
三种都注册到了MessagingService处理器中
MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_SYN, new GossipDigestSynVerbHandler());
MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_ACK, new GossipDigestAckVerbHandler());
MessagingService.instance().registerVerbHandlers(MessagingService.Verb.GOSSIP_DIGEST_ACK2, new GossipDigestAck2VerbHandler());
消息的组成结构如下图所示,Gossip将下列的组装好的message组成pocket 在不同节点间进行传递:
我们来依次分析一下:
<1>GossipDigestSynMessage:
1. 接收到GossipDigests内容集合,判断是否可以进行通信,是否处于同一个集群当中以及两个通信的节点是否处于环形的同一区域内(通过partitioned计算hash值判断处于环形的位置)
2. 如果两个节点可以进行通信并且不处于集群环形同一partitione计算区域中,则进行排序
3. 利用deltaGossipDigestList和deltaEpStateMap构建GossipDigestAck消息并发送
代码位于GossipDigestSynVerbHandler类中展示如下:
public void doVerb(MessageIn<GossipDigestSyn> message, int id)
{
InetAddress from = message.from;
/**
* 对消息版本进行排序通知,如果是更新的消息则合并本地,否则通知远程节点你需要更新最新的版本。
*/
doSort(gDigestList);
List<GossipDigest> deltaGossipDigestList = new ArrayList<GossipDigest>();
Map<InetAddress, EndpointState> deltaEpStateMap = new HashMap<InetAddress, EndpointState>();
Gossiper.instance.examineGossiper(gDigestList, deltaGossipDigestList, deltaEpStateMap);
MessageOut<GossipDigestAck> gDigestAckMessage = new MessageOut<GossipDigestAck>(MessagingService.Verb.GOSSIP_DIGEST_ACK,
new GossipDigestAck(deltaGossipDigestList, deltaEpStateMap),
GossipDigestAck.serializer);
MessagingService.instance().sendOneWay(gDigestAckMessage, from);
}
<2>GossipDigestAckMessage:
1.接收GossipDigest消息,同GossipDigestSynMessage第一步类似,检查若是处于集群环形同一区域内则不进行下一步发送。若GossipDigestAckMessage的消息中包含需要更新的节点信息,则调用IFailureDetector方法的report方法更新集群中的节点状态。
2. 构建GossipDigestAck2消息,并将GossipDigestAck2发送给GossipDigestAckMessage消息来源的节点。
代码位于GossipDigestAckVerbHandler类中展示如下:
public void doVerb(MessageIn<GossipDigestAck> message, int id)
{
InetAddress from = message.from;
GossipDigestAck gDigestAckMessage = message.payload;
List<GossipDigest> gDigestList = gDigestAckMessage.getGossipDigestList();
Map<InetAddress, EndpointState> epStateMap = gDigestAckMessage.getEndpointStateMap();
if (epStateMap.size() > 0)
{
/* 通知失败的节点 */
Gossiper.instance.notifyFailureDetector(epStateMap);
/* 将远程收到的信息跟本地的信息进行合并*/
Gossiper.instance.applyStateLocally(epStateMap);
}
/* 构造GossipDigestAck2Message */
Map<InetAddress, EndpointState> deltaEpStateMap = new HashMap<InetAddress, EndpointState>();
MessageOut<GossipDigestAck2> gDigestAck2Message = new MessageOut<GossipDigestAck2>(MessagingService.Verb.GOSSIP_DIGEST_ACK2,
new GossipDigestAck2(deltaEpStateMap),
GossipDigestAck2.serializer);
MessagingService.instance().sendOneWay(gDigestAck2Message, from);
}
<3>GossipDigestAck2Message:
接收到GossipDigestAck2Message消息并且进行调用IFailureDetector方法的report方法更新集群中的节点状态。代码位于GossipDigestAck2VerbHandler类中,代码与GossipDigestAckVerbHandler类似,这里就不展示了。
总结一下:其实Gossiper源码机构还是很清晰的,假设我们有两个节点A和B。
第一步:A节点找到B节点,通知B节点自己已经拥有的信息
第二步:B节点收到A节点的信息,会和本地的信息进行一个比对,如果比A节点新,那么就会通知A节点新的消息,如果比A节点旧,那么告诉A节点你要再发一次给我,我要合并一下。
第三步:A节点收到B节点的消息,就自己先本地合并B节点最新的消息,然后再将B节点需要的消息发给B节点
第四步:B节点本地合并一下,就搞定了。
3.集群的备份机制
如上图所示,Cassandra是一个高容错的系统,数据将在集群中保存多份,避免服务器宕机导致数据丢失,在跨数据中心时候,需要提供机架感应的相关功能。数据备份的代码位于org.apache.cassandra.locator包中。实现的主要功能为机架感应(EndpointSnitch)与数据备份策略(ReplicationStrategy)
<1>机架感应
所有的机架感应信息都实现了IEndpointSnitch接口,如
SimpleSnitch,PropertyFileSnitch,RackInferringSnitch等,该接口有如下几个重要方法:
/**
* 判断节点属于哪一个机架
*/
public String getRack(InetAddress endpoint);
/**
* 判断节点属于哪一个数据中心
*/
public String getDatacenter(InetAddress endpoint);
/**
* 根据由近到远的规则对地址列表进行排序
*/
public void sortByProximity(InetAddress address, List<InetAddress> addresses);
<2>ReplicationStrategy
AbstractReplicationStrategy是所有replication strategies的基类。它包含如下几个重要方法:
/**
* 从集群中找出负责所有Token对应数据的节点集合
*/
public ArrayList<InetAddress> getNaturalEndpoints(RingPosition searchPosition)
/**
* 计算在TokenMetadata(一致性哈希圆环)中,负责所有Token对应数据的节点集合
*/
public abstract List<InetAddress> calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata)
Cassandrda提供以下三种策略:
(1)SimpleStrategy:是最简单的备份策略,按照顺时针方向(向右边)在一致性哈希圆环中找出需要备份的节点
public List<InetAddress> calculateNaturalEndpoints(Token token, TokenMetadata metadata)
{
int replicas = getReplicationFactor();
ArrayList<Token> tokens = metadata.sortedTokens();
List<InetAddress> endpoints = new ArrayList<InetAddress>(replicas);
if (tokens.isEmpty())
return endpoints;
// Add the token at the index by default
Iterator<Token> iter = TokenMetadata.ringIterator(tokens, token, false);
while (endpoints.size() < replicas && iter.hasNext())
{
InetAddress ep = metadata.getEndpoint(iter.next());
if (!endpoints.contains(ep))
endpoints.add(ep);
}
return endpoints;
}
(2)OldNetworkTopologyStrategy:寻找第一个备份节点处于所属数据中心,寻找第二个备份节点找和第一个备份节点所处数据中心不同的数据中心进行存储,寻找第三个备份节点则是不同机器,但是同一数据中心的节点进行备份。
public List<InetAddress> calculateNaturalEndpoints(Token token, TokenMetadata metadata)
{
int replicas = getReplicationFactor();
List<InetAddress> endpoints = new ArrayList<InetAddress>(replicas);
ArrayList<Token> tokens = metadata.sortedTokens();
if (tokens.isEmpty())
return endpoints;
Iterator<Token> iter = TokenMetadata.ringIterator(tokens, token, false);
Token primaryToken = iter.next();
endpoints.add(metadata.getEndpoint(primaryToken));
boolean bDataCenter = false;
boolean bOtherRack = false;
while (endpoints.size() < replicas && iter.hasNext())
{
//找不同数据中心的节点进行备份
Token t = iter.next();
if (!snitch.getDatacenter(metadata.getEndpoint(primaryToken)).equals(snitch.getDatacenter(metadata.getEndpoint(t))))
{
// 如果已经找到了不同的数据中心就不用继续往下找了
if (!bDataCenter)
{
endpoints.add(metadata.getEndpoint(t));
bDataCenter = true;
}
continue;
}
// 寻找不同的机架
if (!snitch.getRack(metadata.getEndpoint(primaryToken)).equals(snitch.getRack(metadata.getEndpoint(t))) &&
snitch.getDatacenter(metadata.getEndpoint(primaryToken)).equals(snitch.getDatacenter(metadata.getEndpoint(t))))
{
// 如果已经找到不同的机架则不继续往下找了
if (!bOtherRack)
{
endpoints.add(metadata.getEndpoint(t));
bOtherRack = true;
}
}
}
// 如果我们已经找到N个数量的备份节点,则退出。否则继续寻找
// loop through the list and add until we have N nodes.
if (endpoints.size() < replicas)
{
iter = TokenMetadata.ringIterator(tokens, token, false);
while (endpoints.size() < replicas && iter.hasNext())
{
Token t = iter.next();
if (!endpoints.contains(metadata.getEndpoint(t)))
endpoints.add(metadata.getEndpoint(t));
}
}
return endpoints;
}
(3)NetworkTopologyStrategy:在OldNetworkTopologyStrategy
基础上指定具体的备份数量,如在数据中心1备份3个节点,在数据中心2中备份2个节点,做到备份节点尽量分配均匀散乱:
public List<InetAddress> calculateNaturalEndpoints(Token searchToken, TokenMetadata tokenMetadata)
{
// we want to preserve insertion order so that the first added endpoint becomes primary
Set<InetAddress> replicas = new LinkedHashSet<>();
Set<Pair<String, String>> seenRacks = new HashSet<>();
Topology topology = tokenMetadata.getTopology();
// all endpoints in each DC, so we can check when we have exhausted all the members of a DC
Multimap<String, InetAddress> allEndpoints = topology.getDatacenterEndpoints();
// all racks in a DC so we can check when we have exhausted all racks in a DC
Map<String, Multimap<String, InetAddress>> racks = topology.getDatacenterRacks();
assert !allEndpoints.isEmpty() && !racks.isEmpty() : "not aware of any cluster members";
int dcsToFill = 0;
Map<String, DatacenterEndpoints> dcs = new HashMap<>(datacenters.size() * 2);
// Create a DatacenterEndpoints object for each non-empty DC.
for (Map.Entry<String, Integer> en : datacenters.entrySet())
{
String dc = en.getKey();
int rf = en.getValue();
int nodeCount = sizeOrZero(allEndpoints.get(dc));
if (rf <= 0 || nodeCount <= 0)
continue;
DatacenterEndpoints dcEndpoints = new DatacenterEndpoints(rf, sizeOrZero(racks.get(dc)), nodeCount, replicas, seenRacks);
dcs.put(dc, dcEndpoints);
++dcsToFill;
}
Iterator<Token> tokenIter = TokenMetadata.ringIterator(tokenMetadata.sortedTokens(), searchToken, false);
while (dcsToFill > 0 && tokenIter.hasNext())
{
Token next = tokenIter.next();
InetAddress ep = tokenMetadata.getEndpoint(next);
Pair<String, String> location = topology.getLocation(ep);
DatacenterEndpoints dcEndpoints = dcs.get(location.left);
if (dcEndpoints != null && dcEndpoints.addEndpointAndCheckIfDone(ep, location))
--dcsToFill;
}
return new ArrayList<>(replicas);
}
4.集群状态变化的处理机制
如果集群中的状态发生了一些变化如机器失效,新的机器加入或者移除某一个节点,Gossip将会通知事件的订阅者,想对事件进行订阅的类仅需实现org.apache.cassanddra.gms.IendpointStateChangeSubscriber接口
//当有新节点加入到集群中时候,触发该事件
public void onJoin(InetAddress endpoint, EndpointState epState);
//当集群状态发生了改变,比如新加入了节点,触发该事件
public void onChange(InetAddress endpoint, ApplicationState state, VersionedValue value);
//当有失效节点恢复服务的时候,触发该事件
public void onAlive(InetAddress endpoint, EndpointState state);
//当有节点失效的时候,触发该事件
public void onDead(InetAddress endpoint, EndpointState state);
//当有节点被移除出集群时候触发该事件
public void onRemove(InetAddress endpoint);
//集群中的某一个节点重启则触发该事件
public void onRestart(InetAddress endpoint, EndpointState state);
接下来我们来讨论一下主要的实现类:
StorageService
StorageService集合了整个Cassandra集群间的关系,是一个十分重要的类,它管理着整个集群。
当集群中某个节点发生了变化,工作逻辑如下:
public void onChange(InetAddress endpoint, ApplicationState state, VersionedValue value)
{
if (state == ApplicationState.STATUS)
{
String[] pieces = splitValue(value);
assert (pieces.length > 0);
String moveName = pieces[0];
switch (moveName)
{
//引导节点需要进行替换状态(如:宕机)
case VersionedValue.STATUS_BOOTSTRAPPING_REPLACE:
handleStateBootreplacing(endpoint, pieces);
break;
//引导节点状态发生改变,如节点需更新令牌
case VersionedValue.STATUS_BOOTSTRAPPING:
handleStateBootstrap(endpoint);
break;
//节点进入正常状态,节点进入集群环形链并进行分区计算
case VersionedValue.STATUS_NORMAL:
handleStateNormal(endpoint, VersionedValue.STATUS_NORMAL);
break;
//节点宕机
case VersionedValue.SHUTDOWN:
handleStateNormal(endpoint, VersionedValue.SHUTDOWN);
break;
//节点正在移除出集群环形链
case VersionedValue.REMOVING_TOKEN:
//节点已经移除出集群环形链
case VersionedValue.REMOVED_TOKEN:
handleStateRemoving(endpoint, pieces);
break;
//节点正在离开
case VersionedValue.STATUS_LEAVING:
handleStateLeaving(endpoint);
break;
//节点已经离开
case VersionedValue.STATUS_LEFT:
handleStateLeft(endpoint, pieces);
break;
//节点正在移动
case VersionedValue.STATUS_MOVING:
handleStateMoving(endpoint, pieces);
break;
}
}
}
5. 数据读写一致性原理
<1>数据写入
Cassandra写入需要经过以下3个过程,如下图所示:
先写入Commitlog;
其次写入Memtable;
最后写入SSTable
代码位于org.apache.cassandra.thrift包中,写入请求将调用CassandraServer类的doInsert方法,展示如下:
private void doInsert(ConsistencyLevel consistency_level, List<? extends IMutation> mutations, boolean mutateAtomically) throws UnavailableException, TimedOutException, InvalidRequestException {
//关键语句调用存储代理方法,如果提交原子性操作,则调用batch方法
//此处默认mutateAtomically=false,非原子性提交,将直接调用 mutate方法
StorageProxy.mutateWithTriggers(mutations, consistencyLevel, mutateAtomically);
}
public static void mutate(Collection<? extends IMutation> mutations, ConsistencyLevel consistency_level) {
//本地数据中心
String localDataCenter = DatabaseDescriptor.getEndpointSnitch().getDatacenter(FBUtilities.getBroadcastAddress());
//封装写响应处理的列表
ArrayList responseHandlers = new ArrayList(mutations.size());
try {
Iterator e = mutations.iterator();
while(e.hasNext()) {
IMutation responseHandler1 = (IMutation)e.next();
//对每一个Mutation判断是否是需要被写入的replicas备份节点,若是则进行mutateCounter操作,若不是则进行正常写入即performWrite操作,performWrite如下展示
if(responseHandler1 instanceof CounterMutation) {
responseHandlers.add(mutateCounter((CounterMutation)responseHandler1, localDataCenter));
} else {
WriteType mutation1 = mutations.size() <= 1?WriteType.SIMPLE:WriteType.UNLOGGED_BATCH;
responseHandlers.add(performWrite(responseHandler1, consistency_level, localDataCenter, standardWritePerformer, (Runnable)null, mutation1));
}
}
e = responseHandlers.iterator();
while(e.hasNext()) {
AbstractWriteResponseHandler responseHandler2 = (AbstractWriteResponseHandler)e.next();
//等待执行结果
responseHandler2.get();
}
}
}
public static AbstractWriteResponseHandler performWrite(IMutation mutation, ConsistencyLevel consistency_level, String localDataCenter, StorageProxy.WritePerformer performer, Runnable callback, WriteType writeType) throws UnavailableException, OverloadedException {
//得到键空间
String keyspaceName = mutation.getKeyspaceName();
//获取备份策略
AbstractReplicationStrategy rs = Keyspace.open(keyspaceName).getReplicationStrategy();
//得到分区计算的令牌(哈希值)
Token tk = StorageService.getPartitioner().getToken(mutation.key());
//得到所有节点
List naturalEndpoints = StorageService.instance.getNaturalEndpoints(keyspaceName, tk);
//获取集群中需要备份的所有节点
Collection pendingEndpoints = StorageService.instance.getTokenMetadata().pendingEndpointsFor(tk, keyspaceName);
//获取写入策略
AbstractWriteResponseHandler responseHandler = rs.getWriteResponseHandler(naturalEndpoints, pendingEndpoints, consistency_level, callback, writeType);
//如果不满足写入一致性策略,则直接返回结果如节点数小于指定节点数量
responseHandler.assureSufficientLiveNodes();
//具体执行写入任务
performer.apply(mutation, Iterables.concat(naturalEndpoints, pendingEndpoints), responseHandler, localDataCenter, consistency_level);
return responseHandler;
}
我们再来具体看看performer.apply的这个方法。这个方法主要是调用WritePerformer接口中的apply方法,WritePerformer具体有三个实现类,分别是s
tandardWritePerformer,counterWritePerformer和counterWriteOnCoordinatorPerformer三个类。接下来我们依次看一下三个类中的apply方法里面的核心内容:
standardWritePerformer(标准写入)
//当集群中有服务器处于不可用的状态的时候,那么发送给不可用的服务器的更新消息将缓存在集群其他机器中,等该不可用服务器恢复后,再将集群其他机器中的数据发送给恢复的服务器,这张消息在Cassandra中叫做HINT消息。
sendToHintedEndpoints((Mutation) mutation, targets, responseHandler, localDataCenter, Stage.MUTATION);
具体sendToHintedEndpoints方法代码较长,具体展示了以下的四种情况和写入方式。
//第一步:如果先前宕机的节点恢复了,则将原先备份在其他节点的数据写回到已恢复的节点上
if (endpointsToHint != null)
submitHint(mutation, endpointsToHint, responseHandler);
//第二步:如果是写入到本地,则直接写入本地
if (insertLocal)
performLocally(stage, Optional.of(mutation), mutation::apply, responseHandler);
//第三步:如果是写入本数据中心的其他节点,则通过消息服务获取其他节点的连接,写入数据
if (localDc != null)
{
for (InetAddress destination : localDc)
MessagingService.instance().sendRR(message, destination, responseHandler, true);
}
//第四步:如果是写入非本数据中心的节点,则发送给非本地的数据中心,以达到每一个数据中心的每一个备份节点都可以写入数据
if (dcGroups != null)
{
// 遍历每一个数据中心,发送消息写入到其他备份节点
for (Collection<InetAddress> dcTargets : dcGroups.values())
sendMessagesToNonlocalDC(message, dcTargets, responseHandler);
}
此处我们来稍微看一下怎么样在非同一数据中心里面进行消息的传送,具体方法如sendMessagesToNonlocalDC方法展示如下:
private static void sendMessagesToNonlocalDC(MessageOut<? extends IMutation> message,Collection<InetAddress> ,targetsAbstractWriteResponseHandler<IMutation> handler)
{
Iterator<InetAddress> iter = targets.iterator();
InetAddress target = iter.next();
// 向需要发送相同消息的地址进行发送
try(DataOutputBuffer out = new DataOutputBuffer())
{
out.writeInt(targets.size() - 1);
while (iter.hasNext())
{
InetAddress destination = iter.next();
CompactEndpointSerializationHelper.serialize(destination, out);
//消息服务增加回调信息。向MesssagingServie注入必要的消息。
int id = MessagingService.instance().addCallback(handler, message,destination,message.getTimeout(),handler.consistencyLevel true);
out.writeInt(id);
}
message = message.withParameter(Mutation.FORWARD_TO, out.getData());
// 发送(消息+转向headers)
int id = MessagingService.instance().sendRR(message, target, handler, true);
}
}
以上我们讨论完了写入数据的处理策略,接下来我们再来具体看看数据写入是如何先写CommitLog再写入Memtable,最后从Memtable刷到SSTable的全过程。
以下的方法便是在执行此前展示的四步中的第二步:(如果是写入到本地,则直接写入本地)这里启动一个 LocalMuatationRunnable 的线程
private static void performLocally(Stage stage, Optional<IMutation> mutation, final Runnable runnable, final IAsyncCallbackWithFailure<?> handler)
{
StageManager.getStage(stage).maybeExecuteImmediately(new LocalMutationRunnable(mutation)
{
public void runMayThrow()
{
try
{
runnable.run();
handler.response(null);
}
catch (Exception ex)
{
if (!(ex instanceof WriteTimeoutException))
logger.error("Failed to apply mutation locally : ", ex);
handler.onFailure(FBUtilities.getBroadcastAddress(), RequestFailureReason.UNKNOWN);
}
}
@Override
protected Verb verb()
{
return MessagingService.Verb.MUTATION;
}
});
}
之后进入Keyspace的applyInternal方法,此时,数据将先写入commitlog中,再保存到memtable里。如以下代码所示:由于方法体代码较长,我将为您展示比较重要的部分。
private CompletableFuture<?> applyInternal(final Mutation mutation,final boolean writeCommitLog,boolean updateIndexes,boolean isDroppable,boolean isDeferrable,CompletableFuture<?> future)
{
int nowInSec = FBUtilities.nowInSeconds();
try (OpOrder.Group opGroup = writeOrder.start())
{
// 写入 commitlog 和 memtables
CommitLogPosition commitLogPosition = null;
if (writeCommitLog)
{
//写入commitLog
commitLogPosition = CommitLog.instance.add(mutation);
}
for (PartitionUpdate upd : mutation.getPartitionUpdates())
{ ColumnFamilyStorecfs=columnFamilyStores.get(upd.metadata().id);
if (cfs == null)
{
continue;
}
AtomicLong baseComplete = new AtomicLong(Long.MAX_VALUE);
if (requiresViewUpdate)
{
try
{ viewManager.forTable(upd.metadata().id).pushViewReplicaUpdates(upd, writeCommitLog, baseComplete);
}
catch (Throwable t)
{
JVMStabilityInspector.inspectThrowable(t);
throw t;
}
}
UpdateTransaction indexTransaction = updateIndexes ? cfs.indexManager.newUpdateTransaction(upd, opGroup, nowInSec)
: UpdateTransaction.NO_OP;
//更新内存中的memtable表,索引等信息
cfs.apply(upd, indexTransaction, opGroup, commitLogPosition);
if (requiresViewUpdate)
baseComplete.set(System.currentTimeMillis());
}
if (future != null) {
future.complete(null);
}
return future;
}
}
我们首先来看一下commitlog的写入方法
public CommitLogPosition add(Mutation mutation) throws CDCWriteException
{
try (DataOutputBuffer dob = DataOutputBuffer.scratchBuffer.get())
{
//计算需要写入的文件的大小,保存在size变量中
Mutation.serializer.serialize(mutation, dob, MessagingService.current_version);
int size = dob.getLength();
int totalSize = size + ENTRY_OVERHEAD_SIZE;
if (totalSize > MAX_MUTATION_SIZE)
{
throw new IllegalArgumentException(String.format("Mutation of %s is too large for the maximum size of%s",FBUtilities.prettyPrintMemory(totalSize), FBUtilities.prettyPrintMemory(MAX_MUTATION_SIZE)));
}
//向内存碎片管理机制申请文件大小相同的内存块
Allocation alloc = segmentManager.allocate(mutation, totalSize);
CRC32 checksum = new CRC32();
final ByteBuffer buffer = alloc.getBuffer();
try (BufferedDataOutputStreamPlus dos = new DataOutputBufferFixed(buffer))
{
// 写入commitlog
dos.writeInt(size);
updateChecksumInt(checksum, size);
buffer.putInt((int) checksum.getValue());
// checksummed mutation
dos.write(dob.getData(), 0, size);
updateChecksum(checksum, buffer, buffer.position() - size, size);
buffer.putInt((int) checksum.getValue());
}
catch (IOException e)
{
throw new FSWriteError(e, alloc.getSegment().getPath());
}
finally
{
//刷写磁盘
alloc.markWritten();
}
executor.finishWriteFor(alloc);
return alloc.getCommitLogPosition();
}
catch (IOException e)
{
throw new FSWriteError(e, segmentManager.allocatingFrom().getPath());
}
}
接着我们再来看一下memtable的这个方法:
public void apply(PartitionUpdate update, UpdateTransaction indexer, OpOrder.Group opGroup, CommitLogPosition commitLogPosition)
{
//根据commitlog的位置信息获取原先的memtable
Memtable mt = data.getMemtableFor(opGroup, commitLogPosition);
//加入内存BTree中
long timeDelta = mt.put(update, indexer, opGroup)
//更新分区key值 DecoratedKey key = update.partitionKey();
//删除内存中的key值,避免重复
invalidateCachedPartition(key);
metric.samplers.get(Sampler.WRITES).addSample(key.getKey(), key.hashCode(), 1);
//写入memtable
StorageHook.instance.reportWrite(metadata.id, update);
metric.writeLatency.addNano(System.nanoTime() - start);
}
最后我们来研究一下怎么写入SSTable表中:
一般Cassandra是当memtable积累到了一定的大小则自动刷写到SSTable中进行保存,memtable的内存大小设置在cassandra.yarm配置文件,默认大小memtable_heap_space_in_mb=2048,假如memtable的超过了配置的,就会进入一个队列然后刷新到磁盘中去
我们注意看ColumnFamilyStore这个类中的reload()方法,其中调用了刷写的方法scheduleFlush(),我们可以看到方法如下展示
void scheduleFlush()
{
int period = metadata().params.memtableFlushPeriodInMs;
if (period > 0)
{
logger.trace("scheduling flush in {} ms", period);
WrappedRunnable runnable = new WrappedRunnable()
{
protected void runMayThrow()
{
synchronized (data)
{
//获取当前的memtable
Memtable current = data.getView().getCurrentMemtable();
// 如果memtable没有超出预设的大小范围,并且我们已经调度刷新了该节点应有的hit数据信息,那么直接跳过
if (current.isExpired())
{
if (current.isClean())
{
// if we're still clean, instead of swapping just reschedule a flush for later
scheduleFlush();
}
else
{
// 强制刷新
forceFlush();
}
}
}
}
};
ScheduledExecutors.scheduledTasks.schedule(runnable, period, TimeUnit.MILLISECONDS);
}
}
forceFlush此后会进入如下的方法中,执行一个异步刷写任务,到此完成了memtable对SSTable的刷写任务:
public ListenableFuture<CommitLogPosition> switchMemtable()
{
synchronized (data)
{
logFlush();
Flush flush = new Flush(false);
flushExecutor.execute(flush);
postFlushExecutor.execute(flush.postFlushTask);
return flush.postFlushTask;
}
}
<2>数据读入
Cassandra在写入数据的过程中,会为每一个ColumnFamily生成一个或者多个SSTable文件。所以在读取过程中,会查询ColumnFamily该ColumnFamily下的Memtable以及所有的SSTable,合并查询的结果。如下图所示,客户端进行读取时候,需要对此前写入时候分布到不同节点上的SSTable表的分割部分进行一个汇总。
数据读入与数据写入有些类似,都是进入了Dispatcher类的channelRead0()方法中。代码详情见数据写入的方法,其中真正执行是response = request.execute(qstate, queryStartNanoTime);这一条语句的,request继承至于message,根据不同的子类我们进行不同的execute,一共有8个子类,集中在org.apache.cassandra.transport.message包中如下图红色框图所示
我们来看其中的QueryMessage,通常我们读取都是执行查询语句如”select * from table where…”,所以我们来看看QueryMessage的execute中有什么,关键语句如下所示。
public Message.Response execute(QueryState state, long queryStartNanoTime)
{
...
Message.Response response = ClientState.getCQLQueryHandler().process(query, state, options, getCustomPayload(), queryStartNanoTime);
...
}
我们再来进入到process方法中看一看
public ResultMessage process(String queryString, QueryState queryState, QueryOptions options, long queryStartNanoTime)
throws RequestExecutionException, RequestValidationException
{
ParsedStatement.Prepared p = getStatement(queryString, queryState.getClientState().cloneWithKeyspaceIfSet(options.getKeyspace()));
options.prepare(p.boundNames);
CQLStatement prepared = p.statement;
if (prepared.getBoundTerms() != options.getValues().size())
throw new InvalidRequestException("Invalid amount of bind variables");
if (!queryState.getClientState().isInternal)
metrics.regularStatementsExecuted.inc();
return processStatement(prepared, queryState, options, queryStartNanoTime);
}
这里组装好了ParsedStatement.Prepared类,就直接进入了processStatement方法中了。
public ResultMessage processStatement(CQLStatement statement, QueryState queryState, QueryOptions options, long queryStartNanoTime)
throws RequestExecutionException, RequestValidationException
{
logger.trace("Process {} @CL.{}", statement, options.getConsistency());
ClientState clientState = queryState.getClientState();
statement.checkAccess(clientState);
statement.validate(clientState);
//关键语句,执行SelectStatement中的execute方法
ResultMessage result = statement.execute(queryState, options, queryStartNanoTime);
return result == null ? new ResultMessage.Void() : result;
}
SelectStatement中的具体执行如下:
public ResultMessage.Rows execute(QueryState state, QueryOptions options, long queryStartNanoTime) throws RequestExecutionException, RequestValidationException
{
ConsistencyLevel cl = options.getConsistency();
checkNotNull(cl, "Invalid empty consistency level");
cl.validateForRead(keyspace());
int nowInSec = FBUtilities.nowInSeconds();
int userLimit = getLimit(options);
int userPerPartitionLimit = getPerPartitionLimit(options);
int pageSize = options.getPageSize();
Selectors selectors = selection.newSelectors(options);
ReadQuery query = getQuery(options, selectors.getColumnFilter(), nowInSec, userLimit, userPerPartitionLimit, pageSize);
if (aggregationSpec == null && (pageSize <= 0 || (query.limits().count() <= pageSize)))
return execute(query, options, state, selectors, nowInSec, userLimit, queryStartNanoTime);
QueryPager pager = getPager(query, options);
return execute(Pager.forDistributedQuery(pager, cl, state.getClientState()),
options,
selectors,
pageSize,
nowInSec,
userLimit,
queryStartNanoTime);
}
接下去我就不再具体分析了,最后它会执行如下的语句
ResultMessage.Rows msg;
try (PartitionIterator page = pager.fetchPage(pageSize, queryStartNanoTime))
{
msg = processResults(page, options, selectors, nowInSec, userLimit);
}
void processPartition(RowIterator partition, QueryOptions options, ResultSetBuilder result, int nowInSec)
读入时候它会在所处分区中进行搜索,根据列的元信息读取出需要的数据并且返回。processPartition()方法代码也较长,在此我就不展示了。至此我们对数据的读取进行了简单的分析。当然其中的各种读取策略我们都还没有进行分析,如果你感兴趣可以自行研究。
6.数据压缩机制
从上文的讨论中我们知道数据写入最终将会落实到磁盘上的SSTable表,而读取表时候将会从不同表中汇总读取。那么随着数据量的不断累积,便会出现一系列性能问题,如SSTable文件增大,占用的磁盘空间就会不断增大,操作系统可操作的空间就会不断减少;此外不同SSTable中保存着相同的数据将会造成索引文件的增大,占用较多的内存空间,显然对SSTable进行压缩就显得很有必要了。Cassandra数据压缩执行流程主要集中在org.apache.cassandra.db.compaction包中的CompactionTask类中的runMayThrow()方法中,由于代码量较大,我们将挑选其中关键的语句进行分析
protected void runMayThrow() throws Exception
{
//判断是否在压缩之前需要进行快照
if (DatabaseDescriptor.isSnapshotBeforeCompaction())
cfs.snapshotWithoutFlush(System.currentTimeMillis() + "-compact-" + cfs.name);
try (CompactionController controller = getCompactionController(transaction.originals()))
{
//获取文件大小过大需要进行压缩的SSTable表
final Set<SSTableReader> fullyExpiredSSTables = controller.getFullyExpiredSSTables();
//压缩时候确保有足够的磁盘空间 buildCompactionCandidatesForAvailableDiskSpace(fullyExpiredSSTables);
Set<SSTableReader> actuallyCompact = Sets.difference(transaction.originals(), fullyExpiredSSTables);
Collection<SSTableReader> newSStables;
long[] mergedRowCounts;
long totalSourceCQLRows;
try (Refs<SSTableReader> refs = Refs.ref(actuallyCompact);
//压缩策略中获取扫描器,获取必要的参数
AbstractCompactionStrategy.ScannerList scanners = strategy.getScanners(actuallyCompact);
//构建需要压缩的SSTable文件的迭代器
CompactionIterator ci = new CompactionIterator(compactionType, scanners.scanners, controller, nowInSec, taskId))
{
//开始压缩
if (collector != null)
collector.beginCompaction(ci);
//创建writer,开始遍历需要压缩的SSTable进行写入新文件
try (CompactionAwareWriter writer = getCompactionAwareWriter(cfs, getDirectories(), transaction, actuallyCompact))
{
estimatedKeys = writer.estimatedKeys();
while (ci.hasNext())
{
if (writer.append(ci.next()))
totalKeysWritten++;
}
// 产生新的SSTable表
newSStables = writer.finish();
}
finally
{
if (collector != null)
//关闭压缩
collector.finishCompaction(ci);
}
}
}
}
感谢你能阅读完这篇长长的文章,Cassandra的源代码内容远不止以上内容,作为NoSql中具有代表性的项目,其中还有许多内容值得我们深入研究和学习。希望和你共同进步,想说的话可以在评论区留言,我会持续关注。