文章目录
前言
在分布式系统中,为了避免中心服务节点存在单点问题,我们往往会有HA(High Availability)的防御措施。比如一个简单的HA部署方案,额外在另一个机器上部署一个完全一样的服务。当当前的这个服务出现问题时,能够快速切换到这个用来做HA的服务。HA服务切换本身并不是重点,这里的重点是如何保持HA服务间的状态同步。这点在分布式系统的HA设计实现中尤其之关键。本文笔者来聊聊Ozone对象存储系统中核心服务OzoneManager的HA的设计实现原理。
OzoneManager HA的目标
在之前Ozone的alpha版本中,OM服务都是以单点服务部署的方式来处理外部请求的。显然这种缺乏HA的运行模式在运用到实际生产环境中将会存在风险。于是Ozone社区在JIRA[HDDS-505: OzoneManager HA]下设计实现了OM HA的特性。同样是作为分布式存储系统的HA实现,OzoneManager的HA实现和HDFS NameNode HA实现有许多异同之处。
在官方社区的OM HA的设计文档中,列出了以下几点目标实现要点:
- 底层使用Raft协议来实现OM服务的HA特性。换句话说,这里将不是只有1个Active,另1个Standby服务这样的模式。而是2N+1的模式,1个OM Leader服务,2N个OM Follower服务。OM Follwer间通过投票选举出Leader服务。中间投票选举的过程遵从的就是Raft协议。这点即OM HA服务实现基于的一大核心要点。
- 有了OM Leader,OM Follower服务之后,另外一个要点是Leader/Follower间的状态同步控制,以此保证服务在角色切换后,还能保证完全一致的服务状态,就是我们所说的Strong Consistency。
- OM HA服务对于用户的透明性。用户理应对于OM HA完全透明,它在配置了给定的多OM服务地址后,就可以进行服务的请求了了。不管背后的服务处于什么角色状态,客户端用户都无须做任何调整。换言之,客户端始终能找到正确的OM服务,进行请求的发送,而不是单一定向的定点服务发送。
OM HA的Raft方式实现
OM HA中使用了Raft做HA的底层实现,那么它是如何工作的呢?以及它是如何做服务状态的同步的呢?
OM HA在这里使用了Raft的Java实现库Apache Ratis,里面实现了基于Raft协议的Leader选举以及Leader/Follower的状态一致性的控制。不过今天笔者并不准备展开篇幅阐述Raft中一致性同步控制的具体机理,我们还是着重谈谈OM HA本身。
因为有了Raft库Ratis的底层一致性的实现保证后,在上层OM中,它需要做的事情就变得清晰,明了了。在Apache Ratis中,是通过定义StateMachine状态机的方式来表示一个服务的当前状态。外界的每次请求就是此StateMachine即将要apply的一个新的State,每次apply完这个State后,整个状态机就会变为下一个State。因此,多个服务初始拥有相同初始State的StateMachine,在经历了若干次顺序一致的状态改变后,StateMachine的最终状态必然是一致的。这就是基于StateMachine的保证状态同步的核心原理。
这里我们将StateMachine的抽象概念定义转化为更为具体化的概念定义:
- 每次外界而来的待apply的State即为外界的request请求。
- 服务的StateMachine为OM服务的metadata信息,包括volume, bucket,key信息等各种表信息。
在Leader/Follower的HA模式下,请求状态的处理流程如下:
1)客户端发送请求给Leader处理
2)Leader接收到客户端请求,然后将请求复制分发到其它Follower上
3)Leader服务获取到超过半数以上Follower收到请求信息的ack回复之后,apply用户请求到内部状态机。
4)Leader会通知其它Follower去apply它们接收到的请求。
上述请求我们可以理解为是Transaction,从StateMachine的角度来看,概念叫做log entry。Leader复制请求到其它Follower的过程为log的write过程, 如果log被其它Follower成功接收到了,则意为此log为committed的log。
因此在这里StateMachine的实现异常关键,Ozone在这里实现了其内部使用的状态机类OzoneManagerStateMachine,对应的2个核心方法,log entry的写入,和log entry的状态apply方法:
/**
* Validate/pre-process the incoming update request in the state machine.
* @return the content to be written to the log entry. Null means the request
* should be rejected.
* @throws IOException thrown by the state machine while validating
*/
@Override
public TransactionContext startTransaction(
RaftClientRequest raftClientRequest) throws IOException {
ByteString messageContent = raftClientRequest.getMessage().getContent();
OMRequest omRequest = OMRatisHelper.convertByteStringToOMRequest(
messageContent);
Preconditions.checkArgument(raftClientRequest.getRaftGroupId().equals(
raftGroupId));
try {
handler.validateRequest(omRequest);
} catch (IOException ioe) {
TransactionContext ctxt = TransactionContext.newBuilder()
.setClientRequest(raftClientRequest)
.setStateMachine(this)
.setServerRole(RaftProtos.RaftPeerRole.LEADER)
.build();
ctxt.setException(ioe);
return ctxt;
}
return handleStartTransactionRequests(raftClientRequest, omRequest);
}
/*
* Apply a committed log entry to the state machine.
*/
@Override
public CompletableFuture<Message> applyTransaction(TransactionContext trx) {
try {
OMRequest request = OMRatisHelper.convertByteStringToOMRequest(
trx.getStateMachineLogEntry().getLogData());
long trxLogIndex = trx.getLogEntry().getIndex();
CompletableFuture<Message> ratisFuture =
new CompletableFuture<>();
applyTransactionMap.put(trxLogIndex, trx.getLogEntry().getTerm());
CompletableFuture<OMResponse> future = CompletableFuture.supplyAsync(
() -> runCommand(request, trxLogIndex), executorService);
future.thenApply(omResponse -> {
if(!omResponse.getSuccess()) {
if (omResponse.getStatus() == INTERNAL_ERROR) {
terminate(omResponse, OMException.ResultCodes.INTERNAL_ERROR);
} else if (omResponse.getStatus() == METADATA_ERROR) {
terminate(omResponse, OMException.ResultCodes.METADATA_ERROR);
}
}
// For successful response and for all other errors which are not
// critical, we can complete future normally.
ratisFuture.complete(OMRatisHelper.convertResponseToMessage(
omResponse));
return ratisFuture;
});
return ratisFuture;
} catch (Exception e) {
return completeExceptionally(e);
}
}
从上述方法我们可以看到,只有在apply State操作的时候,才是真正请求处理执行的阶段。
Ozone的读写请求的区别处理
Ozone为了保证请求状态处理的一致,引入了StateMachine的方式来做一致性的处理。但并不是所有的请求都需要以这样的方式被处理,比如那些不会影响服务内部状态的请求,即READ类型的请求,其实可以直接被服务处理返回。
简单来说,Client甚至可以直接请求到其中任何一个服务,然后被服务处理得到返回结果。但是为了保证服务内部持有最新的状态数据,Ozone目前只允许Leader服务接受READ读请求,客户端发送到Follower的请求会被failover到当前的Leader服务中去。然后直接被Leader服务的RPC Server处理逻辑所处理。
WRITE类型的请求同样会被failover到Leader服务,但是它会有上述提到的replicate request的额外过程,会有上述startTransaction(log entry written),applyTransaction(app transaction log)的过程来保证状态一致性的处理。
对于请求客户端而言,它在发起请求前并不知道哪些服务是Leader服务,因此在Client层面需要实现一个FailOverProxy类,做自动的重定向处理,Ozone这边专门实现了这样的类,叫做OMFailoverProxyProvider。
这个FailoverProxy在处理返回得到Follower服务返回的OMNotLeaderException后,会提取里面的Leader ID信息,进行请求的重试,并failover当前指向的OM为建议的Leader OM服务。
相关代码逻辑如下,
if (exception instanceof ServiceException) {
OMNotLeaderException notLeaderException =
getNotLeaderException(exception);
if (notLeaderException != null &&
notLeaderException.getSuggestedLeaderNodeId() != null) {
// We need to failover manually to the suggested Leader OM Node.
// OMFailoverProxyProvider#performFailover() is a dummy call and
// does not perform any failover.
omFailoverProxyProvider.performFailoverIfRequired(
notLeaderException.getSuggestedLeaderNodeId());
return getRetryAction(RetryAction.FAILOVER_AND_RETRY, failovers);
}
OM服务的定期Snapshot、Checkpoint行为
和HDFS的FSImage Checkpoint行为类似,Ozone为了OM服务避免长时间做log entry的状态重建,也做了定期生成Snapshot的行为处理。由于Follower定期做Checkpoint,然后Leader定期向Follower上download最新的Snapshot到其本地。新加入的OM服务也可以快速从Leader 服务中bootstrap相关Snapshot文件和log数据,进行状态的快速同步。
综合上述几方面的模块描述,OM HA的实现整体架构如下图所示:
附:DoubleBuffer+Table Cache的请求处理
Ozone社区在实现OM HA模式下的请求处理时,额外设计实现了基于双缓冲+Table Cache的高效请求处理方式,大致原理如下:
- OM的StateMachine在处理transaction请求时,先将请求执行到Table Cache中,然后返回一个OMClientResponse对象。OMClientResponse定义了执行写入db具体操作逻辑。
- OM将OMClientResponse加入到OM DoubleBuffer中。
- OM DoubleBuffer内部线程定期同步OMClientResponse的transaction到db中,然后清理掉对应过期的Table Cache的entry项。
核心处理方法如下,这个处理逻辑同样适用于非HA模式,
@Override
public OMClientResponse handleWriteRequest(OMRequest omRequest,
long transactionLogIndex) {
OMClientRequest omClientRequest =
OzoneManagerRatisUtils.createClientRequest(omRequest);
OMClientResponse omClientResponse =
omClientRequest.validateAndUpdateCache(getOzoneManager(),
transactionLogIndex, ozoneManagerDoubleBuffer::add);
return omClientResponse;
}
引用
[1]. https://issues.apache.org/jira/browse/HDDS-505 . OzoneManager HA
来源:CSDN
作者:Android路上的人
链接:https://blog.csdn.net/Androidlushangderen/article/details/103997315