深入doris查询计划以及io调度(三)查询执行协调器QE

概述

在 Doris 的查询执行体系中,Coordinator(协调器)是 FE 端负责查询执行协调的核心组件。它由第 7 章的查询规划器生成分布式执行计划后接管,负责将逻辑执行计划转化为实际的物理执行,协调各个 Backend 节点上的 Fragment 执行,并收集最终查询结果。

1.1 核心职责

Coordinator 的核心职责包括:

  1. Fragment 调度与分发:根据执行计划生成 Fragment 执行参数,并分发到相应的 BE 节点
  2. 执行协调:协调多个 BE 节点上的 Fragment 执行顺序和依赖关系
  3. 状态跟踪:跟踪每个 Fragment 实例的执行状态,及时发现异常
  4. 结果收集:从 BE 节点接收查询结果,并返回给客户端
  5. 资源管理:管理查询执行过程中的内存、线程等资源
  6. 错误处理:处理执行过程中的各种异常情况,支持查询取消和超时

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 的发送和启动分为两个阶段:

  1. 准备阶段(Prepare):将执行计划发送到 BE,BE 创建 Fragment 执行环境但不启动
  2. 启动阶段(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();
}
相关推荐
液态不合群6 小时前
【面试题】MySQL 三层 B+ 树能存多少数据?
java·数据库·mysql
龙亘川7 小时前
【课程5.1】城管住建核心功能需求分析:市政设施、市容秩序等场景痛点拆解
数据库·oracle·智慧城市·城管住建
飞鸟真人7 小时前
Redis面试常见问题详解
数据库·redis·面试
fanruitian8 小时前
Springboot项目父子工程
java·数据库·spring boot
super_lzb8 小时前
mybatis拦截器ParameterHandler详解
java·数据库·spring boot·spring·mybatis
CV工程师的自我修养8 小时前
数据库出现死锁了。还不知道什么原因引起的?快来看看吧!
数据库
码界奇点9 小时前
灵活性与高性能兼得KingbaseES 对 JSON 数据的全面支持深度解析
数据库·json·es
2501_941871459 小时前
面向微服务链路追踪与全局上下文管理的互联网系统可观测性设计与多语言工程实践分享
大数据·数据库·python
·云扬·9 小时前
MySQL单机多实例部署两种实用方法详解
数据库·mysql·adb
odoo中国9 小时前
Pgpool-II 在 PostgreSQL 中的用例场景与优势
数据库·postgresql·中间件·pgpool