零基础教程!一文教你使用Rancher 2.3和Terraform运行Windows容器
本文来自Rancher Labs
介 绍
在Kubernetes 1.14版本中已经GA了对Windows的支持。这一结果凝结了一群优秀的工程师的努力,他们来自微软、Pivotal、VMware、红帽以及现在已经关闭的Apprenda等几家公司。我在Apprenda工作时,不定时会为sig-windows社区做出一些贡献。即便现在在Rancher Labs任职,也一直关注它的动向。所以当公司决定在Rancher中增加对Windows支持时,我极为兴奋。
Rancher 2.3已于本月月初发布,这是首个GA支持Windows容器的Kubernetes管理平台。它极大降低了企业使用Windows容器的复杂性,并为基于Windows遗留应用程序的现代化提供快捷的途径——无论这些程序是在本地运行还是在多云环境中运行。此外,Rancher 2.3还可以将它们容器化并将其转换为高效、安全和可迁移的多云应用程序,从而省去重写应用程序的工作。
在本文中,我们将在运行RKE的Kubernetes集群之上配置Rancher集群。我们也将会配置同时支持Linux和Windows容器的集群。完成配置后,我们将聊聊操作系统的定位,因为Kubernetes scheduler需要指导各种Linux和Windows容器在启动时的部署位置。
我们的目标是以完全自动化的方式执行这一操作。由于Rancher 2.3目前尚未stable,因此这无法在生产环境中使用,但如果你正需要使用Azure和Rancher来完成基础架构自动化,那么这对你们的团队而言将会是一个良好的开端。退一万步而言,即使你不使用Azure,在本例中的许多概念和代码都可以应用于其他环境中。
考虑因素
Windows vs. Linux
在我们正式开始之前,我需要告知你许多注意事项和“陷阱”。首先,最显而易见的就是:Windows不是Linux。Windows新增了支持网络网格中的容器化应用程序所需的子系统,它们是Windows操作系统专有的,由Windows主机网络服务和Windows主机计算服务实现。操作系统以及底层容器运行时的配置、故障排除以及运维维护将会有显著区别。而且,Windows节点收到Windows Server licensing的约束,容器镜像也受Windows容器的补充许可条款的约束。
Windows OS版本
WindowsOS版本需要绑定到特定的容器镜像版本,这是Windows独有的。使用Hyper-V隔离可以克服这一问题,但是从Kubernetes 1.16开始,Kubernetes不支持Hyper-V隔离。因此,Kubernetes和Rancher仅能在Windows Server 1809/Windows Server 2019以及Windows Server containers Builds 17763 以及Docker EE-basic 18.09之前的版本中运行。
持久性支持和CSI插件
Kubernetes 1.16版本以来,CSI插件支持alpha版本。Windows节点支持大量的in-tree和flex volume。
CNI插件
Rancher 支持仅限于flannel提供的主机网关(L2Bridge)和VXLAN(Overlay)网络支持。在我们的方案中,我们将利用默认的VXLAN,因为当节点并不都在同一个网络上时,主机网关选项需要User Defined Routes配置。这取决于提供商,所以我们将利用VXLAN功能的简单性。根据Kubernetes文档,这是alpha级别的支持。当前没有支持Kubernetes网络策略API的开源Windows网络插件。
其他限制
请确保你已经阅读了Kubernetes文档,因为在Windows容器中有许多功能无法实现,或者其功能和Linux中的相同功能实现方式有所不同。
Kubernetes文档:
基础架构即代码
自动化是实现DevOps的第一种方式。我们将自动化我们的Rancher集群和要在该集群中配置的Azure节点的基础架构。
Terraform
Terraform是一个开源的基础架构自动化编排工具,它几乎支持所有市面上能见到的云服务提供商。今天我们将使用这一工具来自动化配置。确保你运行的Terraform版本至少是Terraform 12。在本文中,使用Terraform 版本是v0.12.9。
$ terraform version
Terraform v0.12.9
RKE Provider
用于Terraform 的RKE provider是一个社区项目,并非由Rancher官方进行研发的,但包括我在内的Rancher的很多工程师都在使用。因为这是一个社区provider而不是Terraform官方的provider,因此你需要安装最新版本到你的Terraform插件目录中。对于大部分的Linux发行版来说,你可以使用本文资源库中包含的setup-rke-terraform-provider.sh
脚本。
Rancher Provider
用于Terraform的Rancher2 provider是Terraform支持的provider,它通过Rancher REST API来自动化Rancher。我们将用它从Terraform的虚拟机中创建Kubernetes集群,这一虚拟机需要使用Azure Resource Manager和Azure Active Directory Terraform Provider进行创建。
本例的Format
本文中的Terraform模型的每个步骤都会被拆分成子模型,这将增强模型可靠性并且将来如果你创建了其他自动化架构,这些模型都可以重新使用。
Part1:设置Rancher集群
登录到Azure
Azure Resource Manager和Azure Active Directory Terraform Provider将使用一个激活的Azure Cli登录以访问Azure。他们可以使用其他认证方法,但在本例中,我在运行Terraform之前先登录。
az login
Note, we have launched a browser for you to login. For old experience with device code, use "az login --use-device-code"
You have logged in. Now let us find all the subscriptions to which you have access...
[
{
"cloudName": "AzureCloud",
"id": "14a619f7-a887-4635-8647-d8f46f92eaac",
"isDefault": true,
"name": "Rancher Labs Shared",
"state": "Enabled",
"tenantId": "abb5adde-bee8-4821-8b03-e63efdc7701c",
"user": {
"name": "jvb@rancher.com",
"type": "user"
}
}
]
设置Resource Group
Azure Resource Group是Rancher集群的节点和其他虚拟硬件存储的位置范围。我们实际上将会创建两个组,一个用于Rancher集群,另一个用于Kubernetes集群。那将在resource-group module
中完成。
https://github.com/JasonvanBrackel/cattle-drive/tree/master/terraform-module/resourcegroup-module
resource "azurerm_resource_group" "resource-group" {
name = var.group-name
location = var.region
}
设置硬件
虚拟网络
我们将需要一个虚拟网络和子网。我们将使用network-module
在各自的资源组中分别设置它们。
我们将使用node-module设置每个节点。既然每个节点都需要安装Docker,那么我们在使用Rancher install-docker脚本配置和安装Docker时,我们需要运行cloud-init文件。这个脚本将检测Linux发行版并且安装Docker。
os_profile {
computer_name = "${local.prefix}-${count.index}-vm"
admin_username = var.node-definition.admin-username
custom_data = templatefile("./cloud-init.template", { docker-version = var.node-definition.docker-version, admin-username = var.node-definition.admin-username, additionalCommand = "${var.commandToExecute} --address ${azurerm_public_ip.publicIp[count.index].ip_address} --internal-address ${azurerm_network_interface.nic[count.index].ip_configuration[0].private_ip_address}" })
}
#cloud-config
repo_update: true
repo_upgrade: all
runcmd:
- [ sh, -c, "curl https://releases.rancher.com/install-docker/${docker-version}.sh | sh && sudo usermod -a -G docker ${admin-username}" ]
- [ sh, -c, "${additionalCommand}"]
模板中的附加命令块用这些节点的sleep 0填充,但是稍后该命令将用于Linux节点,以将Rancher管理的自定义集群节点加入平台。
设置节点
接下来,我们将为每个角色创建几组节点:控制平面、etcd和worker。我们需要考虑几件事,因为Azure处理其虚拟网络的方式有一些独特之处。它会保留前几个IP供自己使用,因此在创建静态IP时需要考虑这一特性。这就是在NIC创建中的4个IP,由于我们也管理子网的IP,因此我们在每个IP中都进行了处理。
resource "azurerm_network_interface" "nic" {
count = var.node-count
name = "${local.prefix}-${count.index}-nic"
location = var.resource-group.location
resource_group_name = var.resource-group.name
ip_configuration {
name = "${local.prefix}-ip-config-${count.index}"
subnet_id = var.subnet-id
private_ip_address_allocation = "static"
private_ip_address = cidrhost("10.0.1.0/24", count.index + var.address-starting-index + 4)
public_ip_address_id = azurerm_public_ip.publicIp[count.index].id
}
}
为什么不对私有IP使用动态分配?
在创建并完全配置节点之前,Azure的Terraform provider将无法获取IP地址。而通过静态处理,我们可以在生成RKE集群期间使用地址。当然,还有其他方法也能解决这一问题,如将基础架构配置分成多个来运行。但是为简单起见,还是对IP地址进行静态管理。
设置前端负载均衡器
默认情况下,Rancher安装程序将会在每个worker节点安装一个ingress controller,这意味着我们应该在可用的worker节点之间负载均衡任何流量。我们也将会利用Azure的功能来为公共IP创建一个公共的DNS入口,并且将其用于集群。这可以在loadbalancer-module
中完成。
https://github.com/JasonvanBrackel/cattle-drive/tree/master/terraform-module/loadbalancer-module
resource "azurerm_public_ip" "frontendloadbalancer_publicip" {
name = "rke-lb-publicip"
location = var.resource-group.location
resource_group_name = var.resource-group.name
allocation_method = "Static"
domain_name_label = replace(var.domain-name-label, ".", "-")
}
作为替代方案,其中包含使用cloudflare DNS的代码。我们在这篇文章中没有使用这一方案,但是你可以不妨一试。如果你使用这个方法,你将需要重置DNS缓存或主机文件入口,以便你的本地计算机可以调用Rancher来使用Rancher terraform provider。
provider "cloudflare" {
email = "${var.cloudflare-email}"
api_key = "${var.cloudflare-token}"
}
data "cloudflare_zones" "zones" {
filter {
name = "${replace(var.domain-name, ".com", "")}.*" # Modify for other suffixes
status = "active"
paused = false
}
}
#Add a record to the domain
resource "cloudflare_record" "domain" {
zone_id = data.cloudflare_zones.zones.zones[0].id
name = var.domain-name
value = var.ip-address
type = "A"
ttl = "120"
proxied = "false"
}
使用RKE安装Kubernetes
我们将使用Azure和Terraform的动态代码块创建的节点与开源RKE Terraform Provider来创建一个RKE集群。
dynamic nodes {
for_each = module.rancher-control.nodes
content {
address = module.rancher-control.publicIps[nodes.key].ip_address
internal_address = module.rancher-control.privateIps[nodes.key].private_ip_address
user = module.rancher-control.node-definition.admin-username
role = ["controlplane"]
ssh_key = file(module.rancher-control.node-definition.ssh-keypath-private)
}
}
使用RKE安装Tiller
有很多种方式可以安装Tiller,你可以使用Rancher官方文档中的方法,但是在本教程中我们将利用RKE Add-On的特性。
官方文档:
https://rancher.com/docs/rancher/v2.x/en/installation/ha/helm-init/
addons = <<EOL
---
kind: ServiceAccount
apiVersion: v1
metadata:
name: tiller
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: tiller
namespace: kube-system
subjects:
- kind: ServiceAccount
name: tiller
namespace: kube-system
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
EOL
}
初始化Helm
Terraform可以运行本地脚本。既然我们将要使用Helm来安装cert-manager
和Rancher,我们需要初始化Helm。
安装cert-manager
这一步和Rancher文档中【使用Tiller安装cert-manager】的内容一样。
https://rancher.com/docs/rancher/v2.x/en/installation/ha/helm-rancher/#let-s-encrypt
resource "null_resource" "install-cert-manager" {
depends_on = [null_resource.initialize-helm]
provisioner "local-exec" {
command = file("../install-cert-manager.sh")
}
}
安装Rancher
这一步和官方文档中【安装Rancher】的内容一样。
https://rancher.com/docs/rancher/v2.x/en/installation/ha/helm-rancher/
install-rancher脚本有很多个版本,我们所使用的版本要求要有Let’s Encrypt的证书。如果你更习惯使用自签名的证书,你需要将install-rancher.sh
的symlink更改为指向其他版本,并从下面的示例代码中删除let-encrypt变量。
resource "null_resource" "install-rancher" {
depends_on = [null_resource.install-cert-manager]
provisioner "local-exec" {
command = templatefile("../install-rancher.sh", { lets-encrypt-email = var.lets-encrypt-email, lets-encrypt-environment = var.lets-encrypt-environment, rancher-domain-name = local.domain-name })
}
}
Rancher引导程序
Terraform的Rancher2 Provider包含了一个引导模式。这允许我们可以设置一个admin密码。你可以在rancherbootstrap-module
中看到这一步。
provider "rancher2" {
alias = "bootstrap"
api_url = var.rancher-url
bootstrap = true
insecure = true
}
resource "rancher2_bootstrap" "admin" {
provider = rancher2.bootstrap
password = var.admin-password
telemetry = true
}
我们在这里设置集群url。
provider "rancher2" {
alias = "admin"
api_url = rancher2_bootstrap.admin.url
token_key = rancher2_bootstrap.admin.token
insecure = true
}
resource "rancher2_setting" "url" {
provider = rancher2.admin
name = "server-url"
value = var.rancher-url
}
Part2:设置Rancher管理的Kubernetes集群
为Azure创建服务主体
在我们可以使用Azure cloud来创建Load Balancer 服务和Azure存储之前,我们需要先为Cloud Controller Manager配置connector。因此,我们在cluster-module和serviceprincipal-module中创建了一个服务主体,其作用域为集群的Resource Group。
resource "azuread_application" "ad-application" {
name = var.application-name
homepage = "https://${var.application-name}"
identifier_uris = ["http://${var.application-name}"]
available_to_other_tenants = false
}
resource "azuread_service_principal" "service-principal" {
application_id = azuread_application.ad-application.application_id
app_role_assignment_required = true
}
resource "azurerm_role_assignment" "serviceprincipal-role" {
scope = var.resource-group-id
role_definition_name = "Contributor"
principal_id = azuread_service_principal.service-principal.id
}
resource "random_string" "random" {
length = 32
special = true
}
resource "azuread_service_principal_password" "service-principal-password" {
service_principal_id = azuread_service_principal.service-principal.id
value = random_string.random.result
end_date = timeadd(timestamp(), "720h")
}
定义自定义集群
我们需要设置flannel 网络选项以支持Windows flannel驱动。你将会注意到Azure provider的配置。
resource "rancher2_cluster" "manager" {
name = var.cluster-name
description = "Hybrid cluster with Windows and Linux workloads"
# windows_prefered_cluster = true Not currently supported
rke_config {
network {
plugin = "flannel"
options = {
flannel_backend_port = 4789
flannel_backend_type = "vxlan"
flannel_backend_vni = 4096
}
}
cloud_provider {
azure_cloud_provider {
aad_client_id = var.service-principal.client-id
aad_client_secret = var.service-principal.client-secret
subscription_id = var.service-principal.subscription-id
tenant_id = var.service-principal.tenant-id
}
}
}
}
创建虚拟机
这些虚拟机的创建过程与早期计算机相同,并且包含Docker安装脚本。这里唯一的改变是使用来自之前创建集群的linux节点命令的附加命令。
module "k8s-worker" {
source = "./node-module"
prefix = "worker"
resource-group = module.k8s-resource-group.resource-group
node-count = var.k8s-worker-node-count
subnet-id = module.k8s-network.subnet-id
address-starting-index = var.k8s-etcd-node-count + var.k8s-controlplane-node-count
node-definition = local.node-definition
commandToExecute = "${module.cluster-module.linux-node-command} --worker"
}
创建Windows Workers
Windows worker进程类似于Linux进程,但有一些例外。由于Windows不支持cloud-init文件,我们需要创建一个Windows自定义脚本扩展。你可以在windowsnode-module中看到它:
https://github.com/JasonvanBrackel/cattle-drive/tree/master/terraform-module/windowsnode-module
Windows worker使用密码进行认证,还需要VM Agent来运行自定义脚本扩展。
os_profile {
computer_name = "${local.prefix}-${count.index}-vm"
admin_username = var.node-definition.admin-username
admin_password = var.node-definition.admin-password
}
os_profile_windows_config {
provision_vm_agent = true
}
加入Rancher
节点配置完成之后,自定义脚本扩展将运行Windows节点命令。
注意:
这是与Terraform文档中不同类型的自定义脚本扩展,适用于Linux虚拟机。Azure可以允许你对Windows节点尝试使用Terraform类型的扩展,但它最终会失败。
耐心一点噢
整个进程需要花费一些时间,需要耐心等待。当Terraform完成之后,将会有一些项目依旧在配置。即使在Kubernetes集群已经启动之后,Windows节点将至少需要10分钟来完全初始化。正常工作的Windows节点看起来类似于下面的终端输出:
C:\Users\iamsuperman>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
832ef7adaeca rancher/rke-tools:v0.1.50 "pwsh -NoLogo -NonIn…" 10 minutes ago Up 9 minutes nginx-proxy
7e75dffce642 rancher/hyperkube:v1.15.4-rancher1 "pwsh -NoLogo -NonIn…" 10 minutes ago Up 10 minutes kubelet
e22b656e22e0 rancher/hyperkube:v1.15.4-rancher1 "pwsh -NoLogo -NonIn…" 10 minutes ago Up 9 minutes kube-proxy
5a2a773f85ed rancher/rke-tools:v0.1.50 "pwsh -NoLogo -NonIn…" 17 minutes ago Up 17 minutes service-sidekick
603bf5a4f2bd rancher/rancher-agent:v2.3.0 "pwsh -NoLogo -NonIn…" 24 minutes ago Up 24 minutes gifted_poincare
Terraform将为新平台输出凭据。
Outputs:
lets-encrypt-email = jason@vanbrackel.net
lets-encrypt-environment = production
rancher-admin-password = {REDACTED}
rancher-domain-name = https://jvb-win-hybrid.eastus2.cloudapp.azure.com/
windows-admin-password = {REDACTED}
Part3:使用Windows工作负载
通过OS定位工作负载
因为Windows容器镜像和Linux容器镜像并不相同,我们需要使用Kubernetes节点的关联性来确定我们的部署目标。每个节点都有OS标签以帮助实现这一目的。
> kubectl get nodes
NAME STATUS ROLES AGE VERSION
control-0-vm Ready controlplane 16m v1.15.4
etcd-0-vm Ready etcd 16m v1.15.4
win-0-vm Ready worker 5m52s v1.15.4
worker-0-vm Ready worker 12m v1.15.4
> kubectl describe node worker-0-vm
Name: worker-0-vm
Roles: worker
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=worker-0-vm
kubernetes.io/os=linux
node-role.kubernetes.io/worker=true
...
> kubectl describe node win-0-vm
Name: win-0-vm
Roles: worker
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=windows
kubernetes.io/arch=amd64
kubernetes.io/hostname=win-0-vm
kubernetes.io/os=windows
由Rancher 2.3部署的集群会自动使用NoSchedule污染Linux worker节点,这意味着工作负载将始终流向Windows节点,除非特别调度了Linux节点并且配置为可容忍污染。
根据计划使用集群的方式,您可能会发现,设置类似的Windows或Linux默认首选项可以在启动工作负载时减少开销。
欢迎添加微信助手(rancher2),进官方技术群,了解更多Kubernetes使用攻略
零基础教程!一文教你使用Rancher 2.3和Terraform运行Windows容器的更多相关文章
- Qt零基础教程(四) QWidget详解篇
在博客园里面转载我自己写的关于Qt的基础教程,没次写一篇我会在这里更新一下目录: Qt零基础教程(四) QWidget详解(1):创建一个窗口 Qt零基础教程(四) QWidget详解(2):QWid ...
- Qt零基础教程(四)QWidget详解(3):QWidget的几何结构
Qt零基础教程(四) QWidget详解(3):QWidget的几何结构 这篇文章里面分析了QWidget中常用的几种几何结构 下图是Qt提供的分析QWidget几何结构的一幅图,在帮助的 Wind ...
- Photoshop零基础教程集锦,助你快速进阶为大佬,轻松、任性!!!
现今,对于Web或App UI设计师而言,除了不断学习专业知识,提升设计技能.掌握一款得心应手的设计工具(例如设计师们常用的图像处理工具PhotoShop,矢量图绘制工具AI, 图形视频处理工具AE, ...
- Memcache教程 Memcache零基础教程
Memcache是什么 Memcache是danga.com的一个项目,来分担数据库的压力. 它可以应对任意多个连接,使用非阻塞的网络IO.由于它的工作机制是在内存中开辟一块空间,然后建立一个Hash ...
- HTML真零基础教程
这是为完全没有接触过的童鞋写的,属于真正的傻瓜式教程,当然由于本人也不是什么大佬,可能有些知识的理解与自己想的不一样,如果有大佬看到,还请帮我指出.以下主要是HTML5的基础标签的使用. 开发前的准备 ...
- Java零基础教程(二)基础语法
Java 基础语法 一个 Java 程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作.下面简要介绍下类.对象.方法和实例变量的概念. 对象:对象是类的一个实例,有状态和行为.例如 ...
- iOS开发零基础教程之生成git所需的SSH keys
在我们github看到了一个不错的第三方库时,可能我们想把他git clone到本地,我们需要复制他的SSH URL,如下图: 复制完地址之后,我们需要打开终端,然后输入命令: git clone + ...
- Java零基础教程(一)环境搭建
本文将带领您一步一步地搭建Java开发环境 一.认识什么是Java Java 是由Sun Microsystems公司于1995年5月推出的高级程序设计语言. Java可运行于多个平台,如Window ...
- Swift零基础教程2019最新版(一)搭建开发环境
Swift简单介绍 Swift是苹果强力推荐的新型开发语言,能开发苹果下属所有软件平台(iOS,iPadOS,macOS,watchOS,tvOS)初学者如果想进入苹果的开发体系,从Swift开始学习 ...
随机推荐
- App引流增长技术:Deeplink(深度链接)技术
移动互联网时代,信息的分享传播无疑是 App 引流增长的关键,与其花费大量精力和成本找渠道.硬推广,不如从细节下手,用最快最简便的方法实现 Deeplink(深度链接)技术,打破信息孤岛.缩短分享路径 ...
- 【linux】【tomcat】tomcat8.5安装
系统环境:Centos7 1.下载tomcat8.5 wget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.45/b ...
- Java匹马行天下之 Java国出了个Java——举国欢庆
Java帝国的崛起 前言: 看庭前花开花落,宠辱不惊, 望天上云卷云舒,去留无意. 闹心的事儿,选择释怀: 纠缠的人儿,试着放下, 生活其实很美. 心若向阳,就无惧悲伤. 愿你明朗坦荡纵情豁达,有得有 ...
- Android adb shell am 命令学习(1)
am:activity manager 启动Activity,打开或关闭进程,发送广播等操作 为什么学习: 主要应用部分,后台启动对应的package的Activity adb shell am st ...
- Python爬虫(一):爬虫伪装
1 简介 对于一些有一定规模或盈利性质比较强的网站,几乎都会做一些防爬措施,防爬措施一般来说有两种:一种是做身份验证,直接把虫子挡在了门口,另一种是在网站设置各种反爬机制,让虫子知难而返. 2 伪装策 ...
- OpenGl 导入读取多个3D模型 并且添加鼠标控制移动旋转
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/11627508.html 前言: 因为接下来的项目需求是要读取多个3D模型,并且移动拼接,那么我 ...
- .Net Core 商城微服务项目系列(十五): 构建定时任务调度和消息队列管理系统
一.系统描述 嗨,好久不见各位老哥,最近有点懒,技术博客写的太少了,因为最近在写小说,写的顺利的话说不定就转行了,哈哈哈哈哈哈哈哈哈. 今天要介绍的是基于.Net Core的定时任务调度和消息队列管理 ...
- mybatis - 通用mapper
title: 玩转spring-boot-mybatis date: 2019-03-11 19:36:57 type: "mybatis" categories: mybatis ...
- 实现一个图片轮播-3d播放效果
前言:最近在做一个音乐播放器,首页要做一个图片轮播,看了bootstrap的carousel插件以及移动端的swipe.js库,都是平面图片轮播的效果,所以自己想着实现类似网易云app里那种3d图片轮 ...
- Currying 及应用
Currying,中文多翻译为柯里化,感觉这个音译还没有达到类似 Humor 之于幽默的传神地步,后面直接使用 Currying. 什么是 Currying Currying 是这么一种机制,它将一个 ...