概述
在 Doris 的查询执行体系中,Coordinator(协调器)是 FE 端负责查询执行协调的核心组件。它由第 7 章的查询规划器生成分布式执行计划后接管,负责将逻辑执行计划转化为实际的物理执行,协调各个 Backend 节点上的 Fragment 执行,并收集最终查询结果。
1.1 核心职责
Coordinator 的核心职责包括:
- Fragment 调度与分发:根据执行计划生成 Fragment 执行参数,并分发到相应的 BE 节点
- 执行协调:协调多个 BE 节点上的 Fragment 执行顺序和依赖关系
- 状态跟踪:跟踪每个 Fragment 实例的执行状态,及时发现异常
- 结果收集:从 BE 节点接收查询结果,并返回给客户端
- 资源管理:管理查询执行过程中的内存、线程等资源
- 错误处理:处理执行过程中的各种异常情况,支持查询取消和超时
1.2 类层次结构
Coordinator 位于 fe/fe-core/src/main/java/org/apache/doris/qe/Coordinator.java,主要类关系如下:
javascript
Coordinator
├── ConnectContext context // 查询上下文
├── DistributedPlan executionDAG // 分布式执行计划
├── Map<PlanFragmentId, FragmentExecParams> fragmentExecParamsMap // Fragment 执行参数
├── Map<TNetworkAddress, PipelineExecContexts> beToPipelineExecCtxs // BE 到 Pipeline 上下文映射
├── MarkedCountDownLatch fragmentsDoneLatch // Fragment 完成计数器
├── Status queryStatus // 查询整体状态
├── QeProcessorImpl processor // QE 处理器
└── ResultReceiver receiver // 结果接收器
8.2 查询执行流程
Coordinator 的查询执行流程始于 exec() 方法,整体流程如下:
2.1 入口方法:exec()
java
public void exec() throws Exception {
// 1. 检查查询是否已被取消
if (!isActive()) {
LOG.warn("Query {} has been cancelled", DebugUtil.printId(queryId));
return;
}
// 2. 注册查询到 QeProcessor
processor = QeProcessorImpl.INSTANCE.registerQuery(queryId, this);
try {
// 3. 执行核心逻辑
execInternal();
} catch (Exception e) {
// 4. 处理异常
queryStatus.setStatus(e);
throw e;
} finally {
// 5. 清理资源
processor.unregisterQuery(queryId);
}
}
2.2 核心执行:execInternal()
execInternal() 方法包含了 Coordinator 的核心执行逻辑:
java
private void execInternal() throws Exception {
// 1. 计算 Fragment 执行参数
computeFragmentExecParams();
// 2. 初始化 fragmentsDoneLatch(用于等待所有 fragment 完成)
fragmentsDoneLatch = new MarkedCountDownLatch(instanceIds.size());
// 3. 创建结果接收器(仅对 SELECT 查询)
if (isSelectQuery()) {
receiver = new ResultReceiver(context, fragmentExecParamsMap.get(rootFragmentId));
}
// 4. 发送 Pipeline 执行上下文到 BE
sendPipelineCtx();
// 5. 等待 RPC 完成
waitPipelineRpc();
// 6. 启动结果接收器(如果是 SELECT 查询)
if (receiver != null) {
receiver.start();
}
// 7. 等待查询完成或超时
waitForFragmentsFinish();
}
3. Fragment 调度机制
3.1 计算 Fragment 执行参数
computeFragmentExecParams() 负责为每个 Fragment 计算执行参数,包括:
- 扫描范围分配:将 Tablet 扫描任务分配到不同的 BE 节点
- 实例数确定:根据并行度计算每个 Fragment 的实例数
- 数据分区:确定数据 Shuffle 的分区策略
核心代码逻辑:
java
private void computeFragmentExecParams() throws Exception {
// 遍历执行计划中的所有 Fragment
for (PlanFragment fragment : executionDAG.getFragments()) {
FragmentExecParams params = new FragmentExecParams(fragment);
// 1. 计算扫描节点的数据分布
List<ScanNode> scanNodes = fragment.getScanNodes();
for (ScanNode scanNode : scanNodes) {
// 获取 Tablet 分布信息
Collection<Long> tabletIds = scanNode.getTabletIds();
// 根据 Tablet 副本位置分配扫描任务
for (Long tabletId : tabletIds) {
Tablet tablet = catalog.getTablet(tabletId);
List<Replica> replicas = tablet.getReplicas();
// 选择健康的副本
Replica selectedReplica = selectReplica(replicas);
Backend backend = selectedReplica.getBackend();
// 将扫描任务分配给该 BE
params.addScanRange(backend, tabletId, scanNode);
}
}
// 2. 计算并行实例数
int instanceNum = computeInstanceNum(fragment);
params.setInstanceNum(instanceNum);
// 3. 创建 Fragment 实例
for (int i = 0; i < instanceNum; i++) {
TUniqueId instanceId = new TUniqueId();
instanceId.setHi(queryId.hi);
instanceId.setLo(queryId.lo + instanceIdGenerator.getAndIncrement());
params.addInstance(instanceId);
}
fragmentExecParamsMap.put(fragment.getFragmentId(), params);
}
}
3.2 两阶段执行模式
Doris 支持两阶段执行模式(Two-phase Execution),将 Fragment 的发送和启动分为两个阶段:
- 准备阶段(Prepare):将执行计划发送到 BE,BE 创建 Fragment 执行环境但不启动
- 启动阶段(Start):通知所有 BE 同时启动 Fragment 执行
这种模式可以避免某些 Fragment 先启动后因依赖的 Fragment 未准备好而等待的问题。
java
private void sendPipelineCtx() throws Exception {
// 判断是否使用两阶段执行
boolean useTwoPhaseExecution = (fragmentExecParamsMap.size() > 1);
if (useTwoPhaseExecution) {
// 阶段1:发送 prepare 请求到所有 BE
sendPrepareRequests();
// 等待所有 prepare 完成
waitPrepareFinish();
// 阶段2:发送 start 请求到所有 BE
sendStartRequests();
} else {
// 单阶段执行:直接发送执行请求
sendExecRequests();
}
}
3.3 发送执行上下文到 BE
sendPipelineCtx() 方法负责将执行上下文发送到各个 BE 节点:
java
private void sendPipelineCtx() throws Exception {
// 为每个 BE 构建 PipelineExecContext
for (Map.Entry<TNetworkAddress, FragmentExecParams> entry : beToFragments.entrySet()) {
TNetworkAddress backend = entry.getKey();
List<FragmentExecParams> fragments = entry.getValue();
// 创建 Pipeline 执行上下文
PipelineExecContexts ctx = new PipelineExecContexts();
ctx.queryId = queryId;
ctx.queryOptions = context.getSessionVariable().toThrift();
ctx.descTable = descTable;
// 添加所有 Fragment 实例
for (FragmentExecParams params : fragments) {
for (FInstanceExecParam instance : params.getInstances()) {
TPipelineFragmentParams fragmentParams = new TPipelineFragmentParams();
fragmentParams.setProtocolVersion(PaloInternalServiceVersion.V1);
fragmentParams.setFragment(params.fragment.toThrift());
fragmentParams.setFragmentInstanceId(instance.instanceId);
fragmentParams.setPerNodeScanRanges(instance.perNodeScanRanges);
ctx.addFragmentParams(fragmentParams);
}
}
beToPipelineExecCtxs.put(backend, ctx);
// 发送 RPC 请求
BackendServiceProxy proxy = BackendServiceProxy.getInstance();
Future<PExecPlanFragmentResult> future =
proxy.execPlanFragmentsAsync(backend, ctx);
beToRpcFutures.put(backend, future);
}
}
4 Backend RPC 通信
4.1 BackendServiceProxy
BackendServiceProxy 是 FE 与 BE 通信的 RPC 代理,位于 fe/fe-core/src/main/java/org/apache/doris/rpc/BackendServiceProxy.java。它封装了与 BE 的各种 RPC 调用:
java
public class BackendServiceProxy {
private static final BackendServiceProxy INSTANCE = new BackendServiceProxy();
// 异步执行 Fragment
public Future<PExecPlanFragmentResult> execPlanFragmentsAsync(
TNetworkAddress address,
PipelineExecContexts request) throws RpcException {
// 选择 RPC 客户端(GRPC 或 BRPC)
BackendServiceClient client = getClient(address);
// 发送异步请求
return client.execPlanFragmentsAsync(request);
}
// 启动 Fragment 执行(两阶段执行的第二阶段)
public Future<PExecPlanFragmentStartResult> execPlanFragmentStartAsync(
TNetworkAddress address,
TUniqueId queryId,
TUniqueId fragmentInstanceId) throws RpcException {
BackendServiceClient client = getClient(address);
return client.execPlanFragmentStartAsync(queryId, fragmentInstanceId);
}
// 取消 Fragment 执行
public Future<PCancelPlanFragmentResult> cancelPlanFragmentAsync(
TNetworkAddress address,
TUniqueId queryId,
TUniqueId fragmentInstanceId,
PCancelPlanFragmentRequest.CancelReason cancelReason) throws RpcException {
BackendServiceClient client = getClient(address);
return client.cancelPlanFragmentAsync(queryId, fragmentInstanceId, cancelReason);
}
}
4.2 等待 RPC 完成
发送完 RPC 请求后,Coordinator 需要等待所有 BE 的响应:
java
private void waitPipelineRpc() throws Exception {
long timeout = context.getSessionVariable().getQueryTimeout() * 1000;
long startTime = System.currentTimeMillis();
for (Map.Entry<TNetworkAddress, Future<PExecPlanFragmentResult>> entry :
beToRpcFutures.entrySet()) {
TNetworkAddress backend = entry.getKey();
Future<PExecPlanFragmentResult> future = entry.getValue();
// 计算剩余超时时间
long elapsed = System.currentTimeMillis() - startTime;
long remainingTimeout = timeout - elapsed;
if (remainingTimeout <= 0) {
throw new UserException("Query timeout when waiting for RPC");
}
try {
// 等待 RPC 完成
PExecPlanFragmentResult result = future.get(remainingTimeout, TimeUnit.MILLISECONDS);
// 检查执行结果
if (result.getStatus().getStatusCode() != 0) {
String errMsg = "Backend " + backend + " exec failed: " +
result.getStatus().getErrorMsgs();
throw new UserException(errMsg);
}
} catch (TimeoutException e) {
throw new UserException("RPC to backend " + backend + " timeout");
} catch (Exception e) {
throw new UserException("RPC to backend " + backend + " failed", e);
}
}
}
5. Fragment 执行状态跟踪
5.1 MarkedCountDownLatch
Coordinator 使用 MarkedCountDownLatch 来跟踪所有 Fragment 实例的完成状态。这是一个带标记的计数器,可以记录每个 Fragment 的完成情况:
java
public class MarkedCountDownLatch {
private final ConcurrentHashMap<Long, Status> marks;
private final CountDownLatch latch;
public MarkedCountDownLatch(int count) {
this.latch = new CountDownLatch(count);
this.marks = new ConcurrentHashMap<>();
}
// 标记某个 Fragment 完成
public void markedCountDown(long fragmentId, Status status) {
marks.put(fragmentId, status);
latch.countDown();
}
// 等待所有 Fragment 完成
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return latch.await(timeout, unit);
}
// 获取失败的 Fragment
public Map<Long, Status> getFailedMarks() {
Map<Long, Status> failed = new HashMap<>();
for (Map.Entry<Long, Status> entry : marks.entrySet()) {
if (!entry.getValue().ok()) {
failed.put(entry.getKey(), entry.getValue());
}
}
return failed;
}
}
5.2 Fragment 状态更新
BE 在执行 Fragment 的过程中,会通过 RPC 向 FE 报告状态更新。Coordinator 需要处理这些状态更新:
java
// Coordinator 实现了状态更新的回调接口
public void updateFragmentStatus(TUniqueId fragmentInstanceId,
TFragmentInstanceStatus status) {
// 1. 检查查询是否已取消
if (!isActive()) {
return;
}
// 2. 更新 Fragment 状态
Status execStatus = new Status(status.getStatus());
if (status.isDone()) {
// Fragment 执行完成
fragmentsDoneLatch.markedCountDown(
fragmentInstanceId.getLo(),
execStatus
);
// 如果失败,更新查询状态
if (!execStatus.ok()) {
queryStatus.setStatus(execStatus);
// 取消其他正在执行的 Fragment
cancelFragments();
}
}
// 3. 更新执行统计信息
if (status.isSetRuntimeProfile()) {
updateProfile(fragmentInstanceId, status.getRuntimeProfile());
}
}
5.3 等待查询完成
Coordinator 需要等待所有 Fragment 执行完成:
java
private void waitForFragmentsFinish() throws Exception {
long timeout = context.getSessionVariable().getQueryTimeout();
boolean finished = fragmentsDoneLatch.await(timeout, TimeUnit.SECONDS);
if (!finished) {
// 超时,取消所有 Fragment
cancelFragments();
throw new UserException("Query timeout after " + timeout + " seconds");
}
// 检查是否有 Fragment 执行失败
Map<Long, Status> failedFragments = fragmentsDoneLatch.getFailedMarks();
if (!failedFragments.isEmpty()) {
// 构造错误信息
StringBuilder errMsg = new StringBuilder("Query failed:\n");
for (Map.Entry<Long, Status> entry : failedFragments.entrySet()) {
errMsg.append("Fragment ").append(entry.getKey())
.append(": ").append(entry.getValue().getErrorMsg())
.append("\n");
}
throw new UserException(errMsg.toString());
}
}
8.6 查询取消与超时机制
6.1 查询取消
Coordinator 支持主动取消查询,会向所有 BE 发送取消请求:
java
public void cancel(String reason) {
// 1. 更新查询状态
queryStatus.setStatus(new Status(TStatusCode.CANCELLED, reason));
// 2. 取消所有 Fragment
cancelFragments();
// 3. 停止结果接收器
if (receiver != null) {
receiver.cancel();
}
}
private void cancelFragments() {
// 遍历所有 BE,发送取消请求
for (Map.Entry<TNetworkAddress, PipelineExecContexts> entry :
beToPipelineExecCtxs.entrySet()) {
TNetworkAddress backend = entry.getKey();
PipelineExecContexts ctx = entry.getValue();
// 为每个 Fragment 实例发送取消请求
for (TPipelineFragmentParams params : ctx.getFragmentParams()) {
TUniqueId instanceId = params.getFragmentInstanceId();
try {
BackendServiceProxy.getInstance().cancelPlanFragmentAsync(
backend,
queryId,
instanceId,
PCancelPlanFragmentRequest.CancelReason.USER_CANCEL
);
} catch (Exception e) {
LOG.warn("Failed to cancel fragment {} on backend {}",
instanceId, backend, e);
}
}
}
}
6.2 超时检测
Coordinator 会定期检查查询是否超时:
java
private void checkTimeout() {
long timeout = context.getSessionVariable().getQueryTimeout() * 1000;
long elapsed = System.currentTimeMillis() - queryStartTime;
if (elapsed > timeout) {
cancel("Query timeout after " + elapsed + " ms");
}
}
7. 结果接收机制
7.1 ResultReceiver
对于 SELECT 查询,Coordinator 需要从 BE 接收查询结果。ResultReceiver 负责从 Root Fragment 所在的 BE 节点拉取数据:
java
public class ResultReceiver {
private final ConnectContext context;
private final FragmentExecParams rootFragmentParams;
private final BlockingQueue<RowBatch> resultQueue;
private volatile boolean cancelled = false;
public void start() {
// 启动结果接收线程
Thread receiverThread = new Thread(() -> {
try {
receiveResults();
} catch (Exception e) {
LOG.error("Failed to receive results", e);
}
});
receiverThread.start();
}
private void receiveResults() throws Exception {
// 获取 Root Fragment 的实例
FInstanceExecParam rootInstance = rootFragmentParams.getInstances().get(0);
TNetworkAddress backend = rootInstance.getHost();
// 创建结果拉取客户端
BackendServiceClient client = BackendServiceProxy.getInstance()
.getClient(backend);
while (!cancelled) {
// 拉取一批结果
PFetchDataResult result = client.fetchData(
queryId,
rootInstance.instanceId,
BATCH_SIZE
);
// 检查状态
if (result.getStatus().getStatusCode() != 0) {
throw new UserException("Fetch data failed: " +
result.getStatus().getErrorMsgs());
}
// 将结果放入队列
RowBatch batch = convertToRowBatch(result.getRowBatch());
resultQueue.put(batch);
// 检查是否已接收完所有数据
if (result.isEos()) {
break;
}
}
}
// 供上层调用,获取下一批结果
public RowBatch getNextBatch(long timeout, TimeUnit unit)
throws InterruptedException {
return resultQueue.poll(timeout, unit);
}
public void cancel() {
cancelled = true;
}
}
7.2 结果流式返回
对于大结果集,Coordinator 支持流式返回,避免一次性加载所有结果到内存:
java
public RowBatch getNext() throws Exception {
// 检查查询是否已完成
if (queryStatus.isCancelled()) {
throw new UserException("Query has been cancelled");
}
if (queryStatus.ok() && !queryFinished) {
throw new UserException("Query failed: " + queryStatus.getErrorMsg());
}
// 从结果接收器获取下一批数据
if (receiver != null) {
RowBatch batch = receiver.getNextBatch(FETCH_TIMEOUT, TimeUnit.SECONDS);
if (batch == null) {
// 超时或已结束
queryFinished = true;
return null;
}
return batch;
}
return null;
}
8. BE 端 Fragment 执行
8.1 FragmentMgr
BE 端的 FragmentMgr(位于 be/src/runtime/fragment_mgr.cpp)负责接收 FE 发送的 Fragment 执行请求:
cpp
Status FragmentMgr::exec_plan_fragment(const TPipelineFragmentParams& params) {
// 1. 创建 Query Context
auto query_id = params.query_id;
auto fragment_instance_id = params.fragment_instance_id;
QueryContext* query_ctx = nullptr;
{
std::lock_guard<std::mutex> lock(_lock);
auto it = _query_ctx_map.find(query_id);
if (it == _query_ctx_map.end()) {
// 创建新的 Query Context
query_ctx = new QueryContext(query_id);
_query_ctx_map[query_id] = query_ctx;
} else {
query_ctx = it->second;
}
}
// 2. 创建 Fragment Context
auto fragment_ctx = new FragmentContext(
query_ctx,
fragment_instance_id,
params.fragment,
params.per_node_scan_ranges
);
// 3. 注册 Fragment
query_ctx->register_fragment(fragment_instance_id, fragment_ctx);
// 4. 如果是两阶段执行,只准备不启动
if (params.is_two_phase_execution) {
// 准备执行环境,但不启动
RETURN_IF_ERROR(fragment_ctx->prepare());
return Status::OK();
}
// 5. 单阶段执行:直接启动
RETURN_IF_ERROR(fragment_ctx->prepare());
RETURN_IF_ERROR(fragment_ctx->execute());
return Status::OK();
}
8.2 两阶段执行的启动
对于两阶段执行,BE 在收到 FE 的 start 请求后才真正启动 Fragment:
cpp
Status FragmentMgr::start_query_execution(const TUniqueId& query_id) {
// 1. 查找 Query Context
QueryContext* query_ctx = nullptr;
{
std::lock_guard<std::mutex> lock(_lock);
auto it = _query_ctx_map.find(query_id);
if (it == _query_ctx_map.end()) {
return Status::NotFound("Query not found");
}
query_ctx = it->second;
}
// 2. 启动所有 Fragment
auto fragments = query_ctx->get_all_fragments();
for (auto fragment_ctx : fragments) {
RETURN_IF_ERROR(fragment_ctx->execute());
}
return Status::OK();
}
8.3 Fragment 执行与状态报告
Fragment 在执行过程中会定期向 FE 报告状态:
cpp
void FragmentContext::execute() {
Status exec_status = Status::OK();
try {
// 1. 执行 Pipeline
exec_status = _pipeline_executor->execute();
// 2. 定期报告状态
report_status(exec_status);
} catch (std::exception& e) {
exec_status = Status::InternalError(e.what());
report_status(exec_status);
}
// 3. 标记完成
_done = true;
report_final_status(exec_status);
}
void FragmentContext::report_status(const Status& status) {
// 构造状态报告
TFragmentInstanceStatus report;
report.fragment_instance_id = _fragment_instance_id;
report.status = status.to_thrift();
report.done = _done;
// 添加 Runtime Profile
if (_runtime_profile) {
report.runtime_profile = _runtime_profile->to_thrift();
}
// 通过 RPC 发送给 FE
FrontendServiceClient client(_fe_address);
client.updateFragmentStatus(report);
}
9. 性能优化
9.1 并行度控制
Coordinator 会根据数据规模和集群资源动态调整 Fragment 的并行度:
java
private int computeInstanceNum(PlanFragment fragment) {
// 1. 获取扫描数据量
long scanDataSize = estimateScanDataSize(fragment);
// 2. 根据数据量计算并行度
int parallelism = (int) Math.ceil(scanDataSize / TARGET_SIZE_PER_INSTANCE);
// 3. 限制最大并行度
int maxParallelism = context.getSessionVariable().getParallelExecInstanceNum();
parallelism = Math.min(parallelism, maxParallelism);
// 4. 限制最小并行度
parallelism = Math.max(parallelism, MIN_PARALLELISM);
return parallelism;
}
9.2 数据本地性优化
在分配扫描任务时,Coordinator 会优先选择数据所在节点的副本:
java
private Replica selectReplica(List<Replica> replicas) {
// 1. 过滤不健康的副本
List<Replica> healthyReplicas = replicas.stream()
.filter(r -> r.getState() == ReplicaState.NORMAL)
.collect(Collectors.toList());
if (healthyReplicas.isEmpty()) {
throw new UserException("No healthy replica available");
}
// 2. 优先选择本地副本(如果当前 BE 在候选列表中)
if (preferLocalReplica) {
for (Replica replica : healthyReplicas) {
if (isLocalBackend(replica.getBackendId())) {
return replica;
}
}
}
// 3. 选择负载最低的副本
return healthyReplicas.stream()
.min(Comparator.comparing(r -> getBackendLoad(r.getBackendId())))
.get();
}