ShardingSphere Show processlist & Kill 功能解读

alan_ham
发布于 2022-10-21 15:07
浏览
0收藏

经常使用数据库的朋友,我想你一定会有以下疑问:

1. 如何查看数据库当前正在执行哪些 SQL,以及这些 SQL 处于什么状态?

2. 如何终止异常的 SQL,比如一条查询大数据量表的 SELECT 语句没有携带查询条件,会拖垮整个数据库性能,这时候希望将这条异常的查询 SQL 终止掉。
Apache ShardingSphere 基于这样的需求,推出了 Show processlist 和 Kill <processID> 这样的功能。


一、功能介绍


Show processlist:此命令能展示出 ShardingSphere 当前正在执行的 SQL 列表,以及每条 SQL 的执行进度,如果用集群模式部署 ShardingSphere,Show processlist 功能会把集群中所有 Proxy 实例正在运行的 SQL 进行汇总,然后展示出来,因此总是能够看到这一时刻正在运行的全量 SQL。

mysql> show processlist \G;
*************************** 1. row ***************************
     Id: 82a67f254959e0a0807a00f3cd695d87
   User: root
   Host: 10.200.79.156
     db: root
Command: Execute
   Time: 19
  State: Executing 0/1
   Info: update t_order set version = 456
1 row in set (0.24 sec)

Kill <processID>:此命令是基于 Show processlist 实现的功能,能够将 Show processlist 中列出的正在运行的 SQL 取消执行。

mysql> kill 82a67f254959e0a0807a00f3cd695d87;
Query OK, 0 rows affected (0.17 sec)


二、原理解析


了解了 Show processlist  和 Kill <processID> 的基本功能之后,让我们一起来探究 Show processlist 背后的原理。Kill <processID> 的原理和 Show processlist 类似,因此我们着重介绍 Show processlist 的实现原理。


2.1 SQL 是如何保存与销毁的


每一条 SQL 执行 ShardingSphere 会生成一个 ExecutionGroupContext 执行上下文对象,这个对象里会包含这条 SQL 的所有信息,其中有一个 executionID 字段来保证自己的唯一性。当 ShardingSphere 收到一条 SQL 指令(目前只对 MySQL 的 DML 和 DDL 语句做了处理,其它类型数据库会在后续版本进行支持,查询语句也被归类到了 DML 里)后,会调用 GovernanceExecuteProcessReporter#report来将 ExecutionGroupContext 信息缓存到 ConcurrentHashMap 中。

public final class GovernanceExecuteProcessReporter implements ExecuteProcessReporter {
    
    @Override
    public void report(final QueryContext queryContext, final ExecutionGroupContext<? extends SQLExecutionUnit> executionGroupContext,
                       final ExecuteProcessConstants constants, final EventBusContext eventBusContext) {
        ExecuteProcessContext executeProcessContext = new ExecuteProcessContext(queryContext.getSql(), executionGroupContext, constants);
        ShowProcessListManager.getInstance().putProcessContext(executeProcessContext.getExecutionID(), executeProcessContext);
        ShowProcessListManager.getInstance().putProcessStatement(executeProcessContext.getExecutionID(), executeProcessContext.getProcessStatements());
    }
}

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ShowProcessListManager {
    
    private static final ShowProcessListManager INSTANCE = new ShowProcessListManager();
    
    @Getter
    private final Map<String, ExecuteProcessContext> processContexts = new ConcurrentHashMap<>();
    
    @Getter
    private final Map<String, Collection<Statement>> processStatements = new ConcurrentHashMap<>();
 
    public static ShowProcessListManager getInstance() {
        return INSTANCE;
    }
    
    public void putProcessContext(final String executionId, final ExecuteProcessContext processContext) {
        processContexts.put(executionId, processContext);
    }
    
    public void putProcessStatement(final String executionId, final Collection<Statement> statements) {
        if (statements.isEmpty()) {
            return;
        }
        processStatements.put(executionId, statements);
    }
}

如上,ShowProcessListManager 类有两个缓存 Map,processContexts 和 processStatements。processContexts 里存放的是 executionID与 ExecuteProcessContext 的映射关系,processStatements 里存放的是 executionID 与 SQL 被改写后可能生成多条语句的 Statement 对象的映射关系。

当 ShardingSphere 每收到一条 SQL 语句时,就会往这两个 Map 里缓存 SQL 信息。SQL 执行完毕之后,会对 Map 缓存做清理。

@RequiredArgsConstructor
public final class ProxyJDBCExecutor {
    
    private final String type;
    
    private final ConnectionSession connectionSession;
    
    private final JDBCDatabaseCommunicationEngine databaseCommunicationEngine;
    
    private final JDBCExecutor jdbcExecutor;
    
    public List<ExecuteResult> execute(final QueryContext queryContext, final ExecutionGroupContext<JDBCExecutionUnit> executionGroupContext,
                                       final boolean isReturnGeneratedKeys, final boolean isExceptionThrown) throws SQLException {
        try {
            MetaDataContexts metaDataContexts = ProxyContext.getInstance().getContextManager().getMetaDataContexts();
            EventBusContext eventBusContext = ProxyContext.getInstance().getContextManager().getInstanceContext().getEventBusContext();
            ShardingSphereDatabase database = metaDataContexts.getMetaData().getDatabase(connectionSession.getDatabaseName());
            DatabaseType protocolType = database.getProtocolType();
            DatabaseType databaseType = database.getResource().getDatabaseType();
            ExecuteProcessEngine.initialize(queryContext, executionGroupContext, eventBusContext);
            SQLStatementContext<?> context = queryContext.getSqlStatementContext();
            List<ExecuteResult> result = jdbcExecutor.execute(executionGroupContext,
                    ProxyJDBCExecutorCallbackFactory.newInstance(type, protocolType, databaseType, context.getSqlStatement(), databaseCommunicationEngine, isReturnGeneratedKeys, isExceptionThrown,
                            true),
                    ProxyJDBCExecutorCallbackFactory.newInstance(type, protocolType, databaseType, context.getSqlStatement(), databaseCommunicationEngine, isReturnGeneratedKeys, isExceptionThrown,
                            false));
            ExecuteProcessEngine.finish(executionGroupContext.getExecutionID(), eventBusContext);
            return result;
        } finally {
            ExecuteProcessEngine.clean();
        }
    }

如上代码所示,ExecuteProcessEngine.initialize(queryContext, executionGroupContext, eventBusContext); 会缓存 SQL 信息到两个 Map 中,finally 代码块里 ExecuteProcessEngine.clean(); 会对缓存里的 Map 做清理操作。

Show processlist 功能里展示出的 SQL 就是从这 processContexts 里获取的。但是这个 Map 只是本地缓存,如果 ShardingSphere 部署的是集群模式,那么 Show processlist 是如何获取集群内其它机器的正在运行的 SQL 呢,下面我们来探究下 ShardingSphere 是如何处理这种情况的。


2.2 Show processlist 实现原理


ShardingSphere 收到 Show process 命令后,会交给 ShowProcessListExecutor#execute 执行器来处理。重点关注 getQueryResult() 方法的实现。

public final class ShowProcessListExecutor implements DatabaseAdminQueryExecutor {
    
    private Collection<String> batchProcessContexts;
    
    @Getter
    private QueryResultMetaData queryResultMetaData;
    
    @Getter
    private MergedResult mergedResult;
    
    public ShowProcessListExecutor() {
        ProxyContext.getInstance().getContextManager().getInstanceContext().getEventBusContext().register(this);
    }
    
    @Subscribe
    public void receiveProcessListData(final ShowProcessListResponseEvent event) {
        batchProcessContexts = event.getBatchProcessContexts();
    }
    
    @Override
    public void execute(final ConnectionSession connectionSession) {
        queryResultMetaData = createQueryResultMetaData();
        mergedResult = new TransparentMergedResult(getQueryResult());
    }
    
    private QueryResult getQueryResult() {
        ProxyContext.getInstance().getContextManager().getInstanceContext().getEventBusContext().post(new ShowProcessListRequestEvent());
        if (null == batchProcessContexts || batchProcessContexts.isEmpty()) {
            return new RawMemoryQueryResult(queryResultMetaData, Collections.emptyList());
        }
        Collection<YamlExecuteProcessContext> processContexts = new LinkedList<>();
        for (String each : batchProcessContexts) {
            processContexts.addAll(YamlEngine.unmarshal(each, BatchYamlExecuteProcessContext.class).getContexts());
        }
        List<MemoryQueryResultDataRow> rows = processContexts.stream().map(processContext -> {
            List<Object> rowValues = new ArrayList<>(8);
            rowValues.add(processContext.getExecutionID());
            rowValues.add(processContext.getUsername());
            rowValues.add(processContext.getHostname());
            rowValues.add(processContext.getDatabaseName());
            rowValues.add("Execute");
            rowValues.add(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - processContext.getStartTimeMillis()));
            int processDoneCount = processContext.getUnitStatuses().stream().map(each -> ExecuteProcessConstants.EXECUTE_STATUS_DONE == each.getStatus() ? 1 : 0).reduce(0, Integer::sum);
            String statePrefix = "Executing ";
            rowValues.add(statePrefix + processDoneCount + "/" + processContext.getUnitStatuses().size());
            String sql = processContext.getSql();
            if (null != sql && sql.length() > 100) {
                sql = sql.substring(0, 100);
            }
            rowValues.add(null != sql ? sql : "");
            return new MemoryQueryResultDataRow(rowValues);
        }).collect(Collectors.toList());
        return new RawMemoryQueryResult(queryResultMetaData, rows);
    }
    
    private QueryResultMetaData createQueryResultMetaData() {
        List<RawQueryResultColumnMetaData> columns = new ArrayList<>();
        columns.add(new RawQueryResultColumnMetaData("", "Id", "Id", Types.VARCHAR, "VARCHAR", 20, 0));
        columns.add(new RawQueryResultColumnMetaData("", "User", "User", Types.VARCHAR, "VARCHAR", 20, 0));
        columns.add(new RawQueryResultColumnMetaData("", "Host", "Host", Types.VARCHAR, "VARCHAR", 64, 0));
        columns.add(new RawQueryResultColumnMetaData("", "db", "db", Types.VARCHAR, "VARCHAR", 64, 0));
        columns.add(new RawQueryResultColumnMetaData("", "Command", "Command", Types.VARCHAR, "VARCHAR", 64, 0));
        columns.add(new RawQueryResultColumnMetaData("", "Time", "Time", Types.VARCHAR, "VARCHAR", 10, 0));
        columns.add(new RawQueryResultColumnMetaData("", "State", "State", Types.VARCHAR, "VARCHAR", 64, 0));
        columns.add(new RawQueryResultColumnMetaData("", "Info", "Info", Types.VARCHAR, "VARCHAR", 120, 0));
        return new RawQueryResultMetaData(columns);
    }
}

这里会用到 guava 包的 EventBus 功能,这是一个消息发布-订阅类库,是观察者设计模式的优雅实现,EventBus 可以实现类与类之间的解耦,更加详细的信息可以自行了解。

getQueryResult() 方法里会 post ShowProcessListRequestEvent 事件, ProcessRegistrySubscriber#loadShowProcessListData 使用了 @Subscribe 注解订阅了此事件,这个方法是实现 Show processlist 的核心方法。下面我们来解析一下这个方法里做了哪些事情。

public final class ProcessRegistrySubscriber {    
    @Subscribe
    public void loadShowProcessListData(final ShowProcessListRequestEvent event) {
        String processListId = new UUID(ThreadLocalRandom.current().nextLong(), ThreadLocalRandom.current().nextLong()).toString().replace("-", "");
        boolean triggerIsComplete = false;
        // 1. 获取集群模式下所有 proxy 存活节点的 process list 路径
        Collection<String> triggerPaths = getTriggerPaths(processListId);
        try {
            // 2. 循环遍历路径,写入空字符串到节点内容中,目的是为了触发节点监听
            triggerPaths.forEach(each -> repository.persist(each, ""));
            // 3. 加锁等待每个节点写入自己当前正在运行的 SQL 信息到持久层中,会等待 5 秒钟
            triggerIsComplete = waitAllNodeDataReady(processListId, triggerPaths);
            // 4. 从持久化层取出各个 proxy 节点写入的数据,聚合之后通过 EventBus post 一个 ShowProcessListResponseEvent 指令,表示操作已完成
            sendShowProcessList(processListId);
        } finally {
            // 5. 清空资源
            repository.delete(ProcessNode.getProcessListIdPath(processListId));
            if (!triggerIsComplete) {
                triggerPaths.forEach(repository::delete);
            }
        }
    }
}

这个方法分为 5 个步骤,重点讲解一下第 2 步和第 3 步是如何处理的。


2.2.1 第 2 步 集群获取数据实现


第 2 步会写入空字符串到节点 /nodes/compute_nodes/process_trigger/<instanceId>:<processlistId>,这个操作会触发 ShardingSphere 的监听逻辑,ShardingSphere 在启动时持久化层会 watch 监听一系列 path 的变更,比如会对路径 /nodes/compute_nodes 的增删改操作进行监听。但监听是一个异步的过程,主线程不会阻塞,因此需要第 3 步,加锁等待每个 ShardingSphere 节点写入自己当前正在运行的 SQL 信息到持久层中。先来看一下 ShardingSphere 是如何处理监听逻辑的。

public final class ComputeNodeStateChangedWatcher implements GovernanceWatcher<GovernanceEvent> {
    
    @Override
    public Collection<String> getWatchingKeys(final String databaseName) {
        return Collections.singleton(ComputeNode.getComputeNodePath());
    }
    
    @Override
    public Collection<Type> getWatchingTypes() {
        return Arrays.asList(Type.ADDED, Type.UPDATED, Type.DELETED);
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public Optional<GovernanceEvent> createGovernanceEvent(final DataChangedEvent event) {
        String instanceId = ComputeNode.getInstanceIdByComputeNode(event.getKey());
        if (!Strings.isNullOrEmpty(instanceId)) {
            ...
        } else if (event.getKey().startsWith(ComputeNode.getOnlineInstanceNodePath())) {
            return createInstanceEvent(event);
            // show processlist
        } else if (event.getKey().startsWith(ComputeNode.getProcessTriggerNodePatch())) {
            return createShowProcessListTriggerEvent(event);
            // kill processlistId
        } else if (event.getKey().startsWith(ComputeNode.getProcessKillNodePatch())) {
            return createKillProcessListIdEvent(event);
        }
        return Optional.empty();
    }
    
        
    private Optional<GovernanceEvent> createShowProcessListTriggerEvent(final DataChangedEvent event) {
        Matcher matcher = getShowProcessTriggerMatcher(event);
        if (!matcher.find()) {
            return Optional.empty();
        }
        if (Type.ADDED == event.getType()) {
            return Optional.of(new ShowProcessListTriggerEvent(matcher.group(1), matcher.group(2)));
        }
        if (Type.DELETED == event.getType()) {
            return Optional.of(new ShowProcessListUnitCompleteEvent(matcher.group(2)));
        }
        return Optional.empty();
    }
}

ComputeNodeStateChangedWatcher#createGovernanceEvent 监听到消息后会根据路径来区分创建哪一个事件。如上代码所示,因为是新增节点,所以会 post ShowProcessListTriggerEvent 事件。因为每一台 ShardingSphere 实例都会监听 /nodes/compute_nodes 节点,所以每台实例都会处理 ShowProcessListTriggerEvent 事件,这就由单机处理变为集群处理。接下来看看 ShardingSphere 是如何处理的。

public final class ClusterContextManagerCoordinator {

    @Subscribe
    public synchronized void triggerShowProcessList(final ShowProcessListTriggerEvent event) {
        if (!event.getInstanceId().equals(contextManager.getInstanceContext().getInstance().getMetaData().getId())) {
            return;
        }
        Collection<ExecuteProcessContext> processContexts = ShowProcessListManager.getInstance().getAllProcessContext();
        if (!processContexts.isEmpty()) {
            registryCenter.getRepository().persist(ProcessNode.getProcessListInstancePath(event.getProcessListId(), event.getInstanceId()),
                    YamlEngine.marshal(new BatchYamlExecuteProcessContext(processContexts)));
        }
        registryCenter.getRepository().delete(ComputeNode.getProcessTriggerInstanceIdNodePath(event.getInstanceId(), event.getProcessListId()));
    }
}

ClusterContextManagerCoordinator#triggerShowProcessList 会订阅 ShowProcessListTriggerEvent 事件,这个方法里会处理自己实例的 processContext 数据,ShowProcessListManager.getInstance().getAllProcessContext(); 会获取到当前正在运行中的 processContext(这些数据是由开篇介绍的每次执行 SQL 之前,ShardingSphere 会把 SQL 信息存放到 Map 中),然后持久化到持久层中,并且删除 /nodes/compute_nodes/process_trigger/<instanceId>:<processlistId> 节点,表示自己已经处理完成。

删除节点同样会触发监听,会 post ShowProcessListUnitCompleteEvent 事件,这个事件最终会唤醒前面等待的锁。

public final class ClusterContextManagerCoordinator {
    
    @Subscribe
    public synchronized void completeUnitShowProcessList(final ShowProcessListUnitCompleteEvent event) {
        ShowProcessListSimpleLock simpleLock = ShowProcessListManager.getInstance().getLocks().get(event.getProcessListId());
        if (null != simpleLock) {
            simpleLock.doNotify();
        }
    }
}


2.2.2 第 3 步 加锁等待获取数据实现


每台实例处理完之后会删除节点以表示操作已完成,ShardingSphere 通过 isReady(paths) 方法来判断是否所有的实例都已处理完成,必须所有的实例都处理完成才会返回 true,处理数据有一个最长等待时间 5 秒钟,如果 5 秒钟都没处理完毕的话,那么就会返回 false。

public final class ClusterContextManagerCoordinator {
    
    @Subscribe
    public synchronized void completeUnitShowProcessList(final ShowProcessListUnitCompleteEvent event) {
        ShowProcessListSimpleLock simpleLock = ShowProcessListManager.getInstance().getLocks().get(event.getProcessListId());
        if (null != simpleLock) {
            simpleLock.doNotify();
        }
    }
}

2.2.3 聚合 processList 数据返回
每台实例处理完数据之后,接收到 Show processlist 指令的实例需要对数据进行汇总,然后展示出来。

public final class ProcessRegistrySubscriber {  
    
    private void sendShowProcessList(final String processListId) {
        List<String> childrenKeys = repository.getChildrenKeys(ProcessNode.getProcessListIdPath(processListId));
        Collection<String> batchProcessContexts = new LinkedList<>();
        for (String each : childrenKeys) {
            batchProcessContexts.add(repository.get(ProcessNode.getProcessListInstancePath(processListId, each)));
        }
        eventBusContext.post(new ShowProcessListResponseEvent(batchProcessContexts));
    }
}

ProcessRegistrySubscriber#sendShowProcessList 会将正在运行中的 SQL 数据聚合到 batchProcessContexts 中,然后 post ShowProcessListResponseEvent 事件。这个事件会被 ShowProcessListExecutor#receiveProcessListData 消费,然后 getQueryResult() 方法会继续往下执行,展示 queryResult 结果集。

至此,Show processlist 指令的执行流程就走完了。

3.3 Kill <processId> 实现原理
清楚了 Show processlist 的执行流程之后,Kill <processId> 的逻辑是类似的,同样也是结合 EventBus + watch机制实现。

由于不知道 processId 是属于哪一台实例的 SQL 进程 ID,因此同样需要每台实例都新增空节点,通过watch机制,每台 ShardingSphere 实例 watch 到新增节点事件,判断自己的缓存 Map 里是否有 processId 这个 key,如果有就取出这个 key 对应的 value,value 是 Collection<Statement> 集合。遍历 Statement 集合,依次调用 statement.cancel(); 方法即可。底层是调用的 java.sql.Statement#cancel() 方法来取消 SQL 执行。


三、结语


目前 Apache ShardingSphere 只实现了 MySQL 方言的 Show processlist 和 Kill <processId> 功能,其它方言还未实现,相信了解了原理和使用的你一定也能够轻松参与到相关功能的开发中。如果你对参与贡献开源代码有兴趣,也欢迎来到社区参与贡献,相信通过贡献代码,你能更深刻地理解 Apache ShardingSphere 的相关功能。

作者
徐杨,税友软件集团中间件研发工程师,负责公司内部海量数据分库分表。开源爱好者,ShardingSphere Contributor,目前对 ShardingSphere 社区内核模块功能感兴趣。

 

本文转载自公众号ShardingSphere官微

分类
已于2022-10-21 15:10:35修改
收藏
回复
举报
回复
    相关推荐