customCommand详解[一]
# 1. 引言与问题概述 🎯
# 1.1 背景介绍
在大数据集群管理和运维的日常工作中,标准的命令如安装、启动、停止和状态检查只能满足基础的服务管理需求。然而,随着大数据应用场景的复杂性增加,集群管理员常常面临需要更多定制化操作的挑战,比如动态资源调整、集群高可用性配置、以及服务的自定义健康检查等。这些特定需求往往要求更灵活、更细粒度的操作,而标准命令已经无法覆盖所有场景。此时,自定义命令(Custom Command)成为解决问题的重要手段。
以 Ambari 为例,它是一个流行的开源大数据管理平台,支持用户通过配置文件如 metainfo.xml
和 commandScript
进行自定义命令的定义,从而为服务增加灵活的管理能力。自定义命令允许管理员为某些服务和组件定义额外的操作,从而实现一些常见但非标准的任务。比如,
YARN 服务中可以定义刷新 YARN 调度器,HDFS 服务中可以定义重新平衡数据节点,Hive
服务可以定义启用或禁用特定节点的操作等。这些操作并不属于系统预设的默认命令集,而是通过自定义命令实现的功能拓展。
通过自定义命令,管理员可以根据生产环境的实际需求,灵活地扩展操作范围,使得大数据集群的管理变得更加灵活高效。具体来说,自定义命令不仅能够简化复杂操作,还可以通过脚本化处理实现自动化运维,大幅提高工作效率和集群的稳定性。
例如,在 HDFS 服务中,通过自定义命令实现的健康检查可以让管理员对服务的健康状态进行定时检查,并自动触发相应的修复操作。类似地,启动维护模式或者启用高可用性模式这些在日常运维中频繁使用的操作都可以通过自定义命令来实现,从而降低人工干预的频率。
官方案例的应用 在 Ambari 的多个官方服务集成中,自定义命令的灵活性得到了充分体现。例如,以下是一些常见的大数据服务中通过自定义命令实现的高级操作:
服务 | 常用自定义命令 | 说明 |
---|---|---|
YARN | REFRESHQUEUES, DECOMMISSION | 刷新调度器、下线节点等 |
HDFS | BALANCER, NAMENODE-HA | 数据节点再平衡、NN高可用 |
Hive | MOVE_METASTORE, MOVE_HIVESERVER2 | 元数据/Server 迁移操作 |
如图所示,每个服务都定义了多个自定义命令,以支持特定的运维场景。这些命令都是基于服务需求定制的,不属于系统默认的安装、启动或停止命令集。
提示
自定义命令极大丰富了大数据运维工具箱,能有效支撑自动化与高复杂场景下的高效管控。
# 1.2 目标
本篇文章的目标是通过深入讲解 Ambari 中自定义命令(Custom Command)的配置与执行流程,帮助读者全面理解其使用场景和技术细节。具体目标包括:
了解自定义命令的配置方式:学习如何在
metainfo.xml
中通过<customCommand>
标签定义自定义命令,掌握其配置项及实际作用。掌握自定义命令的生命周期与触发原理:通过源码分析,自定义命令从配置文件到触发执行的整个生命周期将被详细剖析,帮助读者理解命令是如何在 Ambari 中生效的。
理解自定义命令的扩展应用:除了常见的操作,学会通过自定义命令扩展集群管理的灵活性,例如自动化健康检查、动态资源分配和高可用性模式的管理。
通过实践提升生产环境管理能力:结合实际案例,展示如何灵活运用自定义命令解决生产环境中的特定问题,优化大数据集群的管理,降低人工操作的复杂度。
# 2. 核心概念解析 🧠
# 2.1 metainfo.xml
与 customCommands
标签解析
在 YARN 服务的管理中,metainfo.xml
是服务定义的关键文件,它负责定义服务的组件、依赖关系、配置文件以及自定义命令。在 YARN
服务的集成中,我们常常通过 customCommands
标签来自定义一些特定的操作,例如刷新资源调度器、检查服务健康状况等。
以下是 YARN
的 metainfo.xml
文件中关于 customCommands
的配置示例:
<service>
<name>YARN</name>
<components>
<component>
<!-- 自定义命令 -->
<customCommands>
<customCommand>
<name>REFRESHQUEUES</name>
<commandScript>
<script>scripts/resourcemanager.py</script>
<scriptType>PYTHON</scriptType>
<timeout>600</timeout>
</commandScript>
</customCommand>
</customCommands>
</component>
</components>
</service>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.2 customCommand
的详细解析
在上面的示例中,customCommand
被定义在 RESOURCEMANAGER
组件下,具体的自定义命令包括 DECOMMISSION
和 REFRESHQUEUES
,这些命令用于停用节点和刷新 YARN 的资源队列。
customCommand
的标签结构如下:
<name>
: 命令的名称,例如DECOMMISSION
和REFRESHQUEUES
。这些命令名称将会出现在 Ambari 的操作菜单中,供管理员选择。<commandScript>
: 定义了自定义命令执行的脚本。<script>
: 指定了用于执行命令的脚本路径。例如scripts/resourcemanager.py
。<scriptType>
: 定义了脚本的类型。YARN 的customCommand
一般使用PYTHON
类型的脚本。<timeout>
: 定义了命令的超时时间。例如,DECOMMISSION
的超时时间为 600 秒。
通过自定义命令,管理员可以执行一些复杂的操作,比如刷新 YARN 的资源队列,或执行维护操作(如停用节点、开启高可用等),这些操作并不包含在默认的安装、启动、停止等标准命令中。
# 2.3 customCommand
的执行流程
自定义命令的执行流程如下:
- 命令定义: 在
metainfo.xml
文件中为 YARN 的RESOURCEMANAGER
或其他组件定义自定义命令。 - 触发执行: 在 Ambari 的 Web UI 中,管理员选择并触发自定义命令(例如
REFRESHQUEUES
)。 - 执行脚本: Ambari Server 调用相应的脚本,如
scripts/resourcemanager.py
,并在集群中指定的节点上执行该脚本。 - 结果反馈: 脚本执行完成后,结果会反馈给 Ambari Server,并显示在 Web UI 中,供管理员查看执行状态(成功或失败)。
# 2.4 customCommand
的应用场景
customCommands
常用于处理服务或组件的高级操作,以下是一些常见的应用场景:
- 队列刷新: 在 YARN 服务中,自定义命令
REFRESHQUEUES
用于在资源调度配置发生变化后,动态刷新队列而无需重启整个服务。 - 节点停用: 通过自定义命令
DECOMMISSION
,可以安全地停用某个节点而不影响集群整体运行。 - 高可用配置: 在 ResourceManager 启用或禁用高可用性模式的过程中,自定义命令可以帮助简化操作流程。
- 健康检查: 定义健康检查命令,定期对服务的运行状况进行监控,并自动触发修复操作。
# 3. 实操与代码解析 🔧
# 3.1 完整请求闭环
在集群管理中,执行操作的请求从前端发起,经过后端的处理后,最终交给集群中的 Agent 进行实际执行。以 YARN
的 REFRESHQUEUES
命令为例,我们将详细剖析这一请求的完整闭环。
# 3.1.1 前端请求发起
在 Ambari 的前端界面中,用户点击 REFRESHQUEUES 按钮触发操作。这个按钮通过前端框架 Ember.js 定义,在用户点击之后,会向后端发送一个 Ajax 请求。
下面是请求的 curl 示例:
curl 'http://192.168.3.46:28080/api/v1/clusters/dev/requests' \
-H 'Accept: application/json, text/javascript, */*; q=0.01' \
-H 'Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8' \
-H 'Cache-Control: no-cache' \
-H 'Connection: keep-alive' \
-H 'Content-Type: text/plain' \
-H 'Cookie: AMBARISESSIONID=node01j80oaxd1aasy1fxbas98ip3104.node0' \
-H 'Origin: http://192.168.3.46:28080' \
-H 'Pragma: no-cache' \
-H 'Referer: http://192.168.3.46:28080/' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36' \
-H 'X-Requested-By: X-Requested-By' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-raw '{"RequestInfo":{"context":"刷新YARN容量调度程序","command":"REFRESHQUEUES","parameters/forceRefreshConfigTags":"capacity-scheduler"},"Requests/resource_filters":[{"service_name":"YARN","component_name":"RESOURCEMANAGER","hosts":"centos1"}]}' \
--insecure
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个请求的核心参数如下:
- RequestInfo: 包含命令上下文和命令类型。在这个例子中,命令是
REFRESHQUEUES
,用于刷新 YARN 的调度队列。 - Requests/resource_filters: 指定了服务名称、组件名称和目标主机,这里是 YARN 的 RESOURCEMANAGER
组件,并且目标主机是
centos1
。
用户点击按钮后,前端通过 Ember.js 框架发出这个请求,它会携带相应的命令和主机信息,并交由后端进行处理。
# 3.1.2 后端处理流程
当请求到达 Ambari Server,会由 RequestResourceProvider 类的 createResources
方法进行处理。这个方法是 Ambari
后端接收和处理前端发来的 API 请求的主要入口之一。
@Override
public RequestStatus createResources(Request request)
throws SystemException, UnsupportedPropertyException, NoSuchParentResourceException, ResourceAlreadyExistsException {
if (request.getProperties().size() > 1) {
throw new UnsupportedOperationException("Multiple actions/commands cannot be executed at the same time.");
}
final ExecuteActionRequest actionRequest = getActionRequest(request);
final Map<String, String> requestInfoProperties = request.getRequestInfoProperties();
return getRequestStatus(createResources(new Command<RequestStatusResponse>() {
@Override
public RequestStatusResponse invoke() throws AmbariException, AuthorizationException {
String clusterName = actionRequest.getClusterName();
ResourceType resourceType;
Long resourceId;
if (StringUtils.isEmpty(clusterName)) {
resourceType = ResourceType.AMBARI;
resourceId = null;
} else {
resourceType = ResourceType.CLUSTER;
resourceId = getClusterResourceId(clusterName);
}
if (actionRequest.isCommand()) {
String commandName = actionRequest.getCommandName();
if (StringUtils.isEmpty(commandName)) {
commandName = "_unknown_command_";
}
if (commandName.endsWith("_SERVICE_CHECK")) {
if (!AuthorizationHelper.isAuthorized(resourceType, resourceId, RoleAuthorization.SERVICE_RUN_SERVICE_CHECK)) {
throw new AuthorizationException("The authenticated user is not authorized to execute service checks.");
}
} else if (commandName.equals("DECOMMISSION")) {
if (!AuthorizationHelper.isAuthorized(resourceType, resourceId, RoleAuthorization.SERVICE_DECOMMISSION_RECOMMISSION)) {
throw new AuthorizationException("The authenticated user is not authorized to decommission services.");
}
} else {
// 初始化并解析目录-------核心中的核心 请往这里看👀
// 初始化并解析目录-------核心中的核心 请往这里看👀
if (!AuthorizationHelper.isAuthorized(resourceType, resourceId, RoleAuthorization.SERVICE_RUN_CUSTOM_COMMAND)) {
throw new AuthorizationException(String.format("The authenticated user is not authorized to execute the command, %s.",
commandName));
}
// 初始化并解析目录-------核心中的核心 请往这里看👀
// 初始化并解析目录-------核心中的核心 请往这里看👀
}
} else {
String actionName = actionRequest.getActionName();
if (StringUtils.isEmpty(actionName)) {
actionName = "_unknown_action_";
}
if (actionName.contains("SERVICE_CHECK")) {
if (!AuthorizationHelper.isAuthorized(resourceType, resourceId, RoleAuthorization.SERVICE_RUN_SERVICE_CHECK)) {
throw new AuthorizationException("The authenticated user is not authorized to execute service checks.");
}
} else {
// A custom action has been requested
ActionDefinition actionDefinition = getManagementController().getAmbariMetaInfo().getActionDefinition(actionName);
Set<RoleAuthorization> permissions = (actionDefinition == null)
? null
: actionDefinition.getPermissions();
// here goes ResourceType handling for some specific custom actions
ResourceType customActionResourceType = resourceType;
if (actionName.contains("check_host")) { // check_host custom action
customActionResourceType = ResourceType.CLUSTER;
}
if (!AuthorizationHelper.isAuthorized(customActionResourceType, resourceId, permissions)) {
throw new AuthorizationException(String.format("The authenticated user is not authorized to execute the action %s.", actionName));
}
}
}
return getManagementController().createAction(actionRequest, requestInfoProperties);
}
}));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
关键点:
权限检查: 系统首先检查用户是否有执行此命令的权限,具体通过
AuthorizationHelper.isAuthorized
方法进行校验。比如RoleAuthorization.SERVICE_RUN_CUSTOM_COMMAND
表示用户是否可以执行自定义命令。如果用户没有相应的权限,系统会抛出AuthorizationException
,终止请求。命令创建和执行: 如果权限验证通过,系统会根据请求内容创建一个
ExecuteActionRequest
对象,它包含了需要执行的命令、目标服务和目标组件的信息。然后getManagementController().createAction
方法会创建并执行相应的操作。
在这个过程中,后端的 RequestResourceProvider 会将命令转换成具体的服务操作,并生成对应的任务。
# 3.1.3 Ambari Agent 执行命令
当后端创建了对应的任务后,这些任务会下发到 Ambari Agent,由 Agent 在目标主机上执行相关操作。以 REFRESHQUEUES
命令为例,Ambari Agent 会在 ResourceManager 所在的主机上执行一个 Python 脚本来刷新调度队列。
我们通过ambari-agent 日志观察
查询agent 订阅到的json文件
============Received message: {'clusters': {'2': {'commands': [
{'commandParams': {'hooks_folder': 'stack-hooks', 'custom_command': 'REFRESHQUEUES', 'script': 'scripts/resourcemanager.py', 'version': '3.2.0', 'forceRefreshConfigTags': 'capacity-scheduler', 'command_timeout': '1200', 'HAS_RESOURCE_FILTERS': 'true', 'script_type': 'PYTHON'}, 'roleCommand': 'CUSTOM_COMMAND', 'repositoryFile': {'resolved': True, 'repoVersion': '3.2.0', 'repositories': [{'mirrorsList': None, 'tags': [], 'ambariManaged': True, 'baseUrl': 'http://172.20.0.3', 'repoName': 'bigtop', 'components': None, 'distribution': None, 'repoId': 'BIGTOP-3.2.0-repo-1', 'applicableServices': []}], 'feature': {'preInstalled': False, 'scoped': True}, 'stackName': 'BIGTOP', 'repoVersionId': 1, 'repoFileName': 'ambari-bigtop-1'}, 'clusterId': '2', 'commandType': 'EXECUTION_COMMAND', 'clusterName': 'dev', 'serviceName': 'YARN', 'role': 'RESOURCEMANAGER', 'requestId': 43, 'taskId': 311, 'roleParams': {'component_category': 'MASTER'}, 'componentVersionMap': {'HDFS': {'NAMENODE': '3.2.0', 'SECONDARY_NAMENODE': '3.2.0', 'DATANODE': '3.2.0', 'JOURNALNODE': '3.2.0', 'HDFS_CLIENT': '3.2.0'}, 'ZOOKEEPER': {'ZOOKEEPER_SERVER': '3.2.0', 'ZOOKEEPER_CLIENT': '3.2.0'}, 'REDIS': {'REDIS_MASTER': '3.2.0', 'REDIS_CLIENT': '3.2.0', 'REDIS_SLAVE': '3.2.0'}, 'HIVE': {'HCAT': '3.2.0', 'HIVE_METASTORE': '3.2.0', 'HIVE_SERVER': '3.2.0', 'WEBHCAT_SERVER': '3.2.0', 'HIVE_CLIENT': '3.2.0'}, 'KAFKA': {'KAFKA_BROKER': '3.2.0'}, 'RANGER': {'RANGER_TAGSYNC': '3.2.0', 'RANGER_ADMIN': '3.2.0', 'RANGER_USERSYNC': '3.2.0'}, 'TEZ': {'TEZ_CLIENT': '3.2.0'}, 'MAPREDUCE2': {'MAPREDUCE2_CLIENT': '3.2.0', 'HISTORYSERVER': '3.2.0'}, 'YARN': {'NODEMANAGER': '3.2.0', 'RESOURCEMANAGER': '3.2.0', 'YARN_CLIENT': '3.2.0'}, 'HBASE': {'HBASE_MASTER': '3.2.0', 'HBASE_THRIFTSERVER': '3.2.0', 'PHOENIX_QUERY_SERVER': '3.2.0', 'HBASE_CLIENT': '3.2.0', 'HBASE_REGIONSERVER': '3.2.0'}}, 'commandId': '43-0'}
]
}
}, 'requiredConfigTimestamp': 1728024452628
}
2
3
4
5
6
格式化后如下:
{
'clusters': {
'2': {
'commands': [
{
'commandParams': {
'hooks_folder': 'stack-hooks',
'custom_command': 'REFRESHQUEUES',
'script': 'scripts/resourcemanager.py',
'version': '3.2.0',
'forceRefreshConfigTags': 'capacity-scheduler',
'command_timeout': '1200',
'HAS_RESOURCE_FILTERS': 'true',
'script_type': 'PYTHON'
},
'roleCommand': 'CUSTOM_COMMAND',
'repositoryFile': {
'resolved': True,
'repoVersion': '3.2.0',
'repositories': [
{
'mirrorsList': None,
'tags': [],
'ambariManaged': True,
'baseUrl': 'http://172.20.0.3',
'repoName': 'bigtop',
'components': None,
'distribution': None,
'repoId': 'BIGTOP-3.2.0-repo-1',
'applicableServices': []
}
],
'feature': {
'preInstalled': False,
'scoped': True
},
'stackName': 'BIGTOP',
'repoVersionId': 1,
'repoFileName': 'ambari-bigtop-1'
},
'clusterId': '2',
'commandType': 'EXECUTION_COMMAND',
'clusterName': 'dev',
'serviceName': 'YARN',
'role': 'RESOURCEMANAGER',
'requestId': 43,
'taskId': 311,
'roleParams': {
'component_category': 'MASTER'
},
'componentVersionMap': {
'HDFS': {
'NAMENODE': '3.2.0',
'SECONDARY_NAMENODE': '3.2.0',
'DATANODE': '3.2.0',
'JOURNALNODE': '3.2.0',
'HDFS_CLIENT': '3.2.0'
},
'ZOOKEEPER': {
'ZOOKEEPER_SERVER': '3.2.0',
'ZOOKEEPER_CLIENT': '3.2.0'
},
'REDIS': {
'REDIS_MASTER': '3.2.0',
'REDIS_CLIENT': '3.2.0',
'REDIS_SLAVE': '3.2.0'
},
'HIVE': {
'HCAT': '3.2.0',
'HIVE_METASTORE': '3.2.0',
'HIVE_SERVER': '3.2.0',
'WEBHCAT_SERVER': '3.2.0',
'HIVE_CLIENT': '3.2.0'
},
'KAFKA': {
'KAFKA_BROKER': '3.2.0'
},
'RANGER': {
'RANGER_TAGSYNC': '3.2.0',
'RANGER_ADMIN': '3.2.0',
'RANGER_USERSYNC': '3.2.0'
},
'TEZ': {
'TEZ_CLIENT': '3.2.0'
},
'MAPREDUCE2': {
'MAPREDUCE2_CLIENT': '3.2.0',
'HISTORYSERVER': '3.2.0'
},
'YARN': {
'NODEMANAGER': '3.2.0',
'RESOURCEMANAGER': '3.2.0',
'YARN_CLIENT': '3.2.0'
},
'HBASE': {
'HBASE_MASTER': '3.2.0',
'HBASE_THRIFTSERVER': '3.2.0',
'PHOENIX_QUERY_SERVER': '3.2.0',
'HBASE_CLIENT': '3.2.0',
'HBASE_REGIONSERVER': '3.2.0'
}
},
'commandId': '43-0'
}
]
}
},
'requiredConfigTimestamp': 1728024452628
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 3.1.3.1 命令解析与执行
下发到 Agent 的命令格式如下:
/usr/bin/python \
/var/lib/ambari-agent/cache/stacks/BIGTOP/3.2.0/services/YARN/package/scripts/resourcemanager.py \
REFRESHQUEUES \
/var/lib/ambari-agent/data/command-312.json \
/var/lib/ambari-agent/cache/stacks/BIGTOP/3.2.0/services/YARN/package \
/var/lib/ambari-agent/data/structured-out-312.json \
INFO \
/var/lib/ambari-agent/tmp \
PROTOCOL_TLSv1_2
2
3
4
5
6
7
8
9
接收到命令后,Ambari Agent 将会解析传递的参数,主要包括以下几项:
- 命令名称:
REFRESHQUEUES
,定义要执行的操作。 - 脚本路径:
/var/lib/ambari-agent/cache/stacks/BIGTOP/3.2.0/services/YARN/package/scripts/resourcemanager.py
,指明了执行REFRESHQUEUES
的具体脚本。 - JSON 参数:
/var/lib/ambari-agent/data/command-312.json
,包括了当前
任务执行的所有配置信息,如 YARN 的调度器配置。
# 3.1.3.2 resourcemanager.py
的工作原理
该命令中的核心部分是调用了 Ambari 缓存中的 resourcemanager.py
脚本,它继承自 Script 类,这是 Ambari
提供的标准接口,用于执行特定组件的操作。在 resourcemanager.py
中,定义了多个方法以支持不同的命令执行,其中 refreshqueues
方法用于执行 REFRESHQUEUES
操作。
脚本的核心代码如下:
from ambari_commons.os_family_impl import OsFamilyImpl
from setup_ranger_yarn import setup_ranger_yarn
class Resourcemanager(Script):
def install(self, env):
self.install_packages(env)
def stop(self, env, upgrade_type=None):
import params
env.set_params(params)
service('resourcemanager', action='stop')
def configure(self, env):
import params
env.set_params(params)
yarn(name='resourcemanager')
def refreshqueues(self, env):
pass
if __name__ == "__main__":
Resourcemanager().execute()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在这个方法中,refreshqueues
方法首先调用了 self.configure(env)
,确保 YARN
的配置文件和环境参数被正确加载。然后,它调用了 service
方法执行 refreshQueues
操作。service
函数内部执行了实际的命令,将刷新队列的操作发送到
YARN 的 ResourceManager。
# 3.1.3.3 父类 Script
的 execute
方法
在脚本的最后,会调用父类 Script 的 execute()
方法,该方法是 Ambari 中执行组件命令的核心。通过这个方法,Ambari
Agent 将收到的命令及其相关参数传递给相应的服务组件,从而触发实际的执行。
if __name__ == "__main__":
Resourcemanager().execute()
2
这个方法执行后,Resourcemanager
类中的各个方法会根据传递的命令参数(在本例中是 REFRESHQUEUES
)来执行相应的操作,最终完成对
YARN 调度器的队列刷新。
# 3.1.3.4 命令执行的关键流程
- 脚本调用:
resourcemanager.py
脚本被 Agent 执行,带有REFRESHQUEUES
参数。 - 加载配置: 脚本通过
configure(env)
方法加载 YARN 的配置信息,确保执行环境正确。 - 执行刷新操作: 脚本调用
service('resourcemanager', action='refreshQueues')
,向 ResourceManager 发送刷新队列的命令。 - 反馈结果: 脚本执行完成后,Ambari Agent 会将执行结果返回给 Ambari Server,并在前端显示。
- 方法名称转小写:方法名称转小写代码如下,最终触发执行。