当前位置:网站首页>Flink部署 完整使用 (第三章)
Flink部署 完整使用 (第三章)
2022-08-10 08:49:00 【小坏讲微服务】
一、认识
这里需要提到Flink中的几个关键组件:
客户端(Client)
、作业管理器(JobManager)
和任务管理器(TaskManager)
。我们的代码,实际上是由客户端获取并做转换,之后提交给JobManger的。所以JobManager就是Flink集群里的“管事人”,对作业进行中央调度管理;而它获取到要执行的作业后,会进一步处理转换,然后分发任务给众多的TaskManager。这里的TaskManager,就是真正“干活的人”,
数据的处理操作都是它们来做的
在实际项目应用中,我们当然不能使用开发环境的模拟集群,而是需要将Flink部署在生产集群环境中,然后在将作业提交到集群上运行
所以本章我们就来介绍Flink的部署及作业提交的流程。
Flink是一个非常灵活的处理框架,它支持多种不同的部署场景,还可以和不同的资源管理平台方便地集成。所以接下来我们会先做一个简单的介绍,让大家有一个初步的认识,之后再展开讲述不同情形下的Flink部署。
1)、快速启动一个Flink集群
1、环境配置
Flink是一个分布式的流处理框架,所以实际应用一般都需要搭建集群环境。我们在进行Flink安装部署的学习时,需要准备3台Linux机器。具体要求如下:
系统环境为CentOS 7.5版本。
安装Java 8。
安装Hadoop集群,Hadoop建议选择Hadoop 2.7.5以上版本。
配置集群节点服务器间时间同步以及免密登录,关闭防火墙。
本书中三台服务器的具体设置如下:
节点服务器1,IP地址为192.168.2.102,主机名为hadoop102。
节点服务器2,IP地址为192.168.2.103,主机名为hadoop103。
节点服务器3,IP地址为192.168.2.104,主机名为hadoop104。
2、本地启动
最简单的启动方式,其实是不搭建集群,直接本地启动。本地部署非常简单,直接解压安装包就可以使用,不用进行任何配置;一般用来做一些简单的测试。
1. 下载安装包
进入Flink官网,下载1.13.0版本安装包flink-1.13.0-bin-scala_2.12.tgz,注意此处选用对应scala版本为scala 2.12的安装包。
2. 解压
在hadoop102节点服务器上创建安装目录/opt/module,将flink安装包放在该目录下,并执行解压命令,解压至当前目录
$ tar -zxvf flink-1.13.0-bin-scala_2.12.tgz -C /opt/module/
flink-1.13.0/
flink-1.13.0/log/
flink-1.13.0/LICENSE
flink-1.13.0/lib/
3. 启动
进入解压后的目录,执行启动命令,并查看进程。
$ cd flink-1.13.0/
$ bin/start-cluster.sh
Starting cluster. Starting standalonesession daemon on host hadoop102. Starting taskexecutor daemon on host hadoop102.
$ jps
10369 StandaloneSessionClusterEntrypoint
10680 TaskManagerRunner
10717 Jps
4. 访问Web UI
启动成功后,访问http://hadoop102:8081,可以对flink集群和任务进行监控管理
5. 关闭集群
如果想要让Flink集群停止运行,可以执行以下命令
$ bin/stop-cluster.sh
Stopping taskexecutor daemon (pid: 10680) on host hadoop102. Stopping standalonesession daemon (pid: 10369) on host hadoop102.
2)、集群启动
可以看到,Flink本地启动非常简单,直接执行start-cluster.sh就可以了。如果我们想要扩展成集群,其实启动命令是不变的,主要是需要指定节点之间的主从关系。
Flink是典型的Master-Slave架构的分布式数据处理框架,其中Master角色对应着JobManager,Slave角色则对应TaskManager。我们对三台节点服务器的角色分配
具体安装部署步骤如下:
1. 下载并解压安装包
具体操作与上节相同。
2. 修改集群配置
(1)进入conf目录下,修改flink-conf.yaml文件,修改jobmanager.rpc.address参数为hadoop102
cd conf/
$ vim flink-conf.yaml
# JobManager节点地址.
jobmanager.rpc.address: hadoop102
这就指定了hadoop102节点服务器为JobManager节点。
(2)修改workers文件,将另外两台节点服务器添加为本Flink集群的TaskManager节点,具体修改如下:
$ vim workers
hadoop103
hadoop104
这样就指定了hadoop103和hadoop104为TaskManager节点。
(3)另外,在flink-conf.yaml文件中还可以对集群中的JobManager和TaskManager组件进行优化配置,主要配置项如下:
jobmanager.memory.process.size:对JobManager进程可使用到的全部内存进行配置,包括JVM元空间和其他开销,默认为1600M,可以根据集群规模进行适当调整。
taskmanager.memory.process.size:对TaskManager进程可使用到的全部内存进行配置,包括JVM元空间和其他开销,默认为1600M,可以根据集群规模进行适当调整。
taskmanager.numberOfTaskSlots:对每个TaskManager能够分配的Slot数量进行配置,默认为1,可根据TaskManager所在的机器能够提供给Flink的CPU数量决定。所谓Slot就是TaskManager中具体运行一个任务所分配的计算资源。
parallelism.default:Flink任务执行的默认并行度,优先级低于代码中进行的并行度配置和任务提交时使用参数指定的并行度数量。
关于Slot和并行度的概念,我们会在下一章做详细讲解。
3. 分发安装目录
配置修改完毕后,将Flink安装目录发给另外两个节点服务器。
$ scp -r ./flink-1.13.0 atguigu@hadoop103:/opt/module
$ scp -r ./flink-1.13.0 atguigu@hadoop104:/opt/module
4. 启动集群
(1)在hadoop102节点服务器上执行start-cluster.sh启动Flink集群:
$ bin/start-cluster.sh
Starting cluster. Starting standalonesession daemon on host hadoop102. Starting taskexecutor daemon on host hadoop103. Starting taskexecutor daemon on host hadoop104.
(2)查看进程情况:
[atguigu@hadoop102 flink-1.13.0]$ jps
13859 Jps
13782 StandaloneSessionClusterEntrypoint
[atguigu@hadoop103 flink-1.13.0]$ jps
12215 Jps
12124 TaskManagerRunner
[atguigu@hadoop104 flink-1.13.0]$ jps
11602 TaskManagerRunner
11694 Jps
5. 访问Web UI
启动成功后,同样可以访问http://hadoop102:8081对flink集群和任务进行监控管理
这里可以明显看到,当前集群的TaskManager数量为2;由于默认每个TaskManager的Slot数量为1,所以总Slot数和可用Slot数都为2。
3)、向集群提交作业
1、程序打包
(1)为方便自定义结构和定制依赖,我们可以引入插件maven-assembly-plugin进行打包。在FlinkTutorial项目的pom.xml文件中添加打包插件的配置,具体如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
(2)插件配置完毕后,可以使用IDEA的Maven工具执行package命令,出现如下提示即表示打包成功。
[INFO] -----------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] -----------------------------------------------------------------------
[INFO] Total time: 21.665 s
[INFO] Finished at: 2021-06-01T17:21:26+08:00
[INFO] Final Memory: 141M/770M
[INFO] -----------------------------------------------------------------------
打包完成后,在target目录下即可找到所需JAR包,JAR包会有两个,FlinkTutorial-1.0-SNAPSHOT.jar和FlinkTutorial-1.0-SNAPSHOT-jar-with-dependencies.jar,因为集群中已经具备任务运行所需的所有依赖,所以建议使用FlinkTutorial-1.0-SNAPSHOT.jar。
2. 在Web UI上提交作业
(1)任务打包完成后,我们打开Flink的WEB UI页面,在右侧导航栏点击“Submit New Job”,然后点击按钮“+ Add New”,选择要上传运行的JAR包
(2)点击该JAR包,出现任务配置页面,进行相应配置。
主要配置程序入口主类的全类名,任务运行的并行度,任务运行所需的配置参数和保存点路径等,配置完成后,即可点击按钮“Submit”,将任务提交到集群运行。
(3)任务提交成功之后,可点击左侧导航栏的“Running Jobs”查看程序运行列表情况,
(4)点击该任务,可以查看任务运行的具体情况,也可以通过点击“Cancel Job”结束任务运行
Flink的WEB UI页面设计非常简洁明了,读者可以自行尝试其余操作。
3. 命令行提交作业
除了通过WEB UI界面提交任务之外,也可以直接通过命令行来提交任务。这里为方便起见,我们可以先把jar包直接上传到目录flink-1.13.0下
(1)首先需要启动集群。
$ bin/start-cluster.sh
(2)在hadoop102中执行以下命令启动netcat。
$ nc -lk 7777
(3)进入到Flink的安装路径下,在命令行使用flink run命令提交作业。
$ bin/flink run -m hadoop102:8081 -c com.atguigu.wc.StreamWordCount ./FlinkTutorial-1.0-SNAPSHOT.jar
这里的参数 –m指定了提交到的JobManager,-c指定了入口类。
(4)在浏览器中打开Web UI,http://hadoop102:8081查看应用执行情况
用netcat输入数据,可以在TaskManager的标准输出(Stdout)看到对应的统计结果。
(5)在log日志中,也可以查看执行结果,需要找到执行该数据任务的TaskManager节点查看日志。
$ cat flink-atguigu-taskexecutor-0-hadoop102.out
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/opt/module/flink-1.13.0/lib/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/opt/module/hadoop-3.1.3/share/hadoop/common/lib/slf4j-log4j12-1.7.25.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.apache.logging.slf4j.Log4jLoggerFactory]
(hello,1)
(hello,2)
(flink,1)
(hello,3)
(scala,1)
二、部署模式
1)、会话模式(Session Mode)
会话模式其实最符合常规思维。我们需要先启动一个集群,保持一个会话,在这个会话中通过客户端提交作业,如图3-10所示。集群启动时所有资源就都已经确定
,所以所有提交的作业会竞争集群中的资源。
这样的好处很明显
,我们只需要一个集群,就像一个大箱子,所有的作业提交之后都塞进去;集群的生命周期是超越于作业之上的,铁打的营盘流水的兵,作业结束了就释放资源,集群依然正常运行。缺点也是显而易见的:因为资源是共享的,所以资源不够了,提交新的作业就会失败
。另外,同一个TaskManager上可能运行了很多作业,如果其中一个发生故障导致TaskManager宕机,那么所有作业都会受到影响。
我们在3.1节中先启动集群再提交作业,这种方式其实就是会话模式。
会话模式比较适合于单个规模小、执行时间短的大量作业。
2)、单作业模式(Per-Job Mode)
会话模式因为资源共享会导致很多问题,所以为了更好地隔离资源,我们可以考虑为每个提交的作业启动一个集群,这就是所谓的单作业(Per-Job)模式,如图3-11所示。
单作业模式,就是严格的一对一,集群只为这个作业而生
。同样由客户端运行应用程序,然后启动集群,作业被提交给JobManager,进而分发给TaskManager执行。作业作业完成后,集群就会关闭,所有资源也会释放。这样一来,每个作业都有它自己的JobManager管理,占用独享的资源,即使发生故障,它的TaskManager宕机也不会影响其他作业。
这些特性使得单作业模式在生产环境运行更加稳定,所以是实际应用的首选模式。缺点需要注意的是
,Flink本身无法直接这样运行,所以单作业模式一般需要借助一些资源管理框架来启动集群,比如YARN、Kubernetes。
3)、应用模式(Application Mode)
前面提到的两种模式下,应用代码都是在客户端上执行
,然后由客户端提交给JobManager的。但是这种方式客户端需要占用大量网络带宽
,去下载依赖和把二进制数据发送给JobManager;加上很多情况下我们提交作业用的是同一个客户端,就会加重客户端所在节点的资源消耗
。
所以
解决办法就是,我们不要客户端了
,直接把应用提交到JobManger上运行。而这也就代表着,我们需要为每一个提交的应用单独启动一个JobManager,也就是创建一个集群。这个JobManager只为执行这一个应用而存在,执行结束之后JobManager也就关闭了,这就是所谓的应用模式,如图3-12所示。
应用模式与单作业模式,都是提交作业之后才创建集群
;单作业模式是通过客户端来提交的,客户端解析出的每一个作业对应一个集群;而应用模式下,是直接由JobManager执行应用程序的,并且即使应用包含了多个作业,也只创建一个集群。
总结一下,在会话模式下,集群的生命周期独立于集群上运行的任何作业的生命周期,并且提交的所有作业共享资源。而单作业模式为每个提交的作业创建一个集群,带来了更好的资源隔离,这时集群的生命周期与作业的生命周期绑定。最后,应用模式为每个应用程序创建一个会话集群,在JobManager上直接调用应用程序的main()方法。
我们所讲到的部署模式,相对是比较抽象的概念。实际应用时,一般需要和资源管理平台结合起来,选择特定的模式来分配资源、部署应用。接下来,我们就针对不同的资源提供者(Resource Provider)的场景,具体介绍Flink的部署方式。
三、独立模式
独立模式(Standalone)是部署Flink最基本也是最简单的方式:所需要的所有Flink组件,都只是操作系统上运行的一个JVM进程。
独立模式是独立运行的,不依赖任何外部的资源管理平台
;当然独立也是有代价的:如果资源不足,或者出现故障,没有自动扩展或重分配资源的保证,必须手动处理。所以独立模式一般只用在开发测试或作业非常少的场景下。
另外,我们也可以将独立模式的集群放在容器中运行。Flink提供了独立模式的容器化部署方式,可以在Docker或者Kubernetes上进行部署。
1)、会话模式部署
可以发现,独立模式的特点是不依赖外部资源管理平台,而会话模式的特点是先启动集群、后提交作业。所以,我们在第3.1节用的就是独立模式(Standalone)的会话模式部署。
2)、单作业模式部署
在3.2.2中我们提到,Flink本身无法直接以单作业方式启动集群,一般需要借助一些资源管理平台。所以Flink的独立(Standalone)集群并不支持单作业模式部署。
3)、应用模式部署
应用模式下不会提前创建集群,所以不能调用start-cluster.sh脚本。我们可以使用同样在bin目录下的standalone-job.sh来创建一个JobManager。
具体步骤如下:
(1)进入到Flink的安装路径下,将应用程序的jar包放到lib/目录下。
$ cp ./FlinkTutorial-1.0-SNAPSHOT.jar lib/
(2)执行以下命令,启动JobManager。
$ ./bin/standalone-job.sh start --job-classname com.atguigu.wc.StreamWordCount
这里我们直接指定作业入口类,脚本会到lib目录扫描所有的jar包。
(3)同样是使用bin目录下的脚本,启动TaskManager。
$ ./bin/taskmanager.sh start
(4)如果希望停掉集群,同样可以使用脚本,命令如下。
$ ./bin/standalone-job.sh stop
$ ./bin/taskmanager.sh stop
四、YARN 模式
独立(Standalone)模式由Flink自身提供资源,无需其他框架,这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但我们知道,Flink是大数据计算框架,不是资源调度框架,这并不是它的强项;所以还是应该让专业的框架做专业的事,和其他资源调度框架集成更靠谱。而在目前大数据生态中,国内应用最为广泛的资源管理平台就是YARN了。所以接下来我们就将学习,在强大的YARN平台上Flink是如何集成部署的。
整体来说,YARN上部署的过程是:客户端把Flink应用提交给Yarn的ResourceManager, Yarn的ResourceManager会向Yarn的NodeManager申请容器。在这些容器上,Flink会部署JobManager和TaskManager的实例,从而启动集群。Flink会根据运行在JobManger上的作业所需要的Slot数量动态分配TaskManager资源。
1) 、相关准备和配置
1、下载并解压安装包
下载并解压安装包,并将解压后的安装包重命名为flink-1.13.0-yarn,
2、配置环境变量
$ sudo vim /etc/profile.d/my_env.sh
HADOOP_HOME=/opt/module/hadoop-2.7.5
export PATH=$PATH:$HADOOP_HOME/bin:$HADOOP_HOME/sbin
export HADOOP_CONF_DIR=${
HADOOP_HOME}/etc/hadoop
export HADOOP_CLASSPATH=`hadoop classpath`
这里必须保证设置了环境变量HADOOP_CLASSPATH。
3)、启动Hadoop集群,包括HDFS和YARN。
[atguigu@hadoop102 ~]$ start-dfs.sh
[atguigu@hadoop103 ~]$ start-yarn.sh
分别在3台节点服务器查看进程启动情况。
[atguigu@hadoop102 ~]$ jps
5190 Jps
5062 NodeManager
4408 NameNode
4589 DataNode
[atguigu@hadoop103 ~]$ jps
5425 Jps
4680 ResourceManager
5241 NodeManager
4447 DataNode
[atguigu@hadoop104 ~]$ jps
4731 NodeManager
4333 DataNode
4861 Jps
4478 SecondaryNameNode
4)、进入conf目录
进入conf目录,修改flink-conf.yaml文件,修改以下配置,这些配置项的含义在进行Standalone模式配置的时候进行过讲解,若在提交命令中不特定指明,这些配置将作为默认配置。
$ cd /opt/module/flink-1.13.0-yarn/conf/
$ vim flink-conf.yaml
jobmanager.memory.process.size: 1600m
taskmanager.memory.process.size: 1728m
taskmanager.numberOfTaskSlots: 8
parallelism.default: 1
2) 、会话模式部署
YARN的会话模式与独立集群略有不同,需要首先申请一个YARN会话(YARN session)来启动Flink集群
1. 启动集群
(1)启动hadoop集群(HDFS, YARN)。
(2)执行脚本命令向YARN集群申请资源,开启一个YARN会话,启动Flink集群。
$ bin/yarn-session.sh -nm test
可用参数解读:
-d:分离模式,如果你不想让Flink YARN客户端一直前台运行,可以使用这个参数,即使关掉当前对话窗口,YARN session也可以后台运行。
-jm(--jobManagerMemory):配置JobManager所需内存,默认单位MB。
-nm(--name):配置在YARN UI界面上显示的任务名。
-qu(--queue):指定YARN队列名。
-tm(--taskManager):配置每个TaskManager所使用内存。
注意:Flink1.11.0版本不再使用-n参数和-s参数分别指定TaskManager数量和slot数量,YARN会按照需求动态分配TaskManager和slot。所以从这个意义上讲,YARN的会话模式也不会把集群资源固定,同样是动态分配的。
YARN Session启动之后会给出一个web UI地址以及一个YARN application ID,如下所示,用户可以通过web UI或者命令行两种方式提交作业。
2021-06-03 15:54:27,069 INFO org.apache.flink.yarn.YarnClusterDescriptor [] - YARN application has been deployed successfully.
2021-06-03 15:54:27,070 INFO org.apache.flink.yarn.YarnClusterDescriptor [] - Found Web Interface hadoop104:39735 of application 'application_1622535605178_0003'.
JobManager Web Interface: http://hadoop104:39735
2. 提交作业
(1)通过Web UI提交作业
这种方式比较简单,与上文所述Standalone部署模式基本相同。
(2)通过命令行提交作业
① 将Standalone模式讲解中打包好的任务运行JAR包上传至集群
② 执行以下命令将该任务提交到已经开启的Yarn-Session中运行。
$ bin/flink run -c com.atguigu.wc.StreamWordCount FlinkTutorial-1.0-SNAPSHOT.jar
客户端可以自行确定JobManager的地址,也可以通过-m或者-jobmanager参数指定JobManager的地址,JobManager的地址在YARN Session的启动页面中可以找到。
③ 任务提交成功后,可在YARN的Web UI界面查看运行情况。
从图中可以看到我们创建的Yarn-Session实际上是一个Yarn的Application,并且有唯一的Application ID。
④也可以通过Flink的Web UI页面查看提交任务的运行情况,
3)、单作业模式部署
在YARN环境中,由于有了外部平台做资源调度,所以我们也可以直接向YARN提交一个单独的作业,从而启动一个Flink集群。
(1)执行命令提交作业
$ bin/flink run -d -t yarn-per-job -c com.atguigu.wc.StreamWordCount FlinkTutorial-1.0-SNAPSHOT.jar
早期版本也有另一种写法:
$ bin/flink run -m yarn-cluster -c com.atguigu.wc.StreamWordCount FlinkTutorial-1.0-SNAPSHOT.jar
注意这里是通过参数-m yarn-cluster指定向YARN集群提交任务。
(2)在YARN的ResourceManager界面查看执行情况
点击可以打开Flink Web UI页面进行监控,如图3-17所示:
(3)可以使用命令行查看或取消作业
$ ./bin/flink list -t yarn-per-job -Dyarn.application.id=application_XXXX_YY
$ ./bin/flink cancel -t yarn-per-job -Dyarn.application.id=application_XXXX_YY <jobId>
这里的application_XXXX_YY是当前应用的ID,是作业的ID。注意如果取消作业,整个Flink集群也会停掉。
4)、应用模式部署
应用模式同样非常简单,与单作业模式类似,直接执行flink run-application命令即可。
(1)执行命令提交作业。
$ bin/flink run-application -t yarn-application -c com.atguigu.wc.StreamWordCount FlinkTutorial-1.0-SNAPSHOT.jar
(2)在命令行中查看或取消作业。
$ ./bin/flink list -t yarn-application -Dyarn.application.id=application_XXXX_YY
$ ./bin/flink cancel -t yarn-application -Dyarn.application.id=application_XXXX_YY <jobId>
(3)也可以通过yarn.provided.lib.dirs配置选项指定位置,将jar上传到远程。
$ ./bin/flink run-application -t yarn-application -Dyarn.provided.lib.dirs="hdfs://myhdfs/my-remote-flink-dist-dir" hdfs://myhdfs/jars/my-application.jar
这种方式下jar可以预先上传到HDFS,而不需要单独发送到集群,这就使得作业提交更加轻量了
五、本章总结
Flink支持多种不同的部署模式,还可以和不同的资源管理平台方便地集成。本章从快速启动的示例入手,接着介绍了Flink中几种部署模式的区别,并进一步针对不同的资源提供者展开讲解了具体的部署操作。在这个过程中,我们不仅熟悉了Flink的使用方法,而且接触到了很多内部运行原理的知识。
关于Flink运行时组件概念的作用,以及作业提交运行的流程架构,我们会在下一章进一步详细展开。
边栏推荐
猜你喜欢
Spotify expresses its architectural design using the C4 model
不想再干会计了,蝶变向新,勇往直前,最后成功通过转行测试实现月薪翻倍~
速卖通卖家如何抓住产品搜索权重
iwemeta metaverse: Ali's first COO: how to build a sales force
二叉树 --- 堆
DAY25: Logic Vulnerability
00后女孩月薪3200,3年买两套房,这个程序员变现新风口千万要把握住
[OAuth2] 20. OAuth2 Extended Protocol PKCE
【微服务架构】为故障设计微服务架构
ARM结构体系3:ARM指令的寻址和异常中断处理
随机推荐
【OAuth2】二十、OAuth2扩展协议 PKCE
不要把公司当成家,被通知裁员时会变得不幸...
Rust学习:6.5_复合类型之数组
mySQL增删改查进阶
推荐几个高质量的软件测试实战项目
Go-Excelize API源码阅读(十一)—— GetActiveSheetIndex()
js-----数组转换成树形结构
Spotify expresses its architectural design using the C4 model
2 模块一:科研思维培养
Rust learning: 6.4_ enumeration of composite types
NaiveUI中看起来没啥用的组件(文字渐变)实现原来这么简单
PTA 习题2.2 数组循环左移
Rust learning: 6.2_ Tuples of composite types
In the SQL SERVER database, if the data of the table is added, deleted, or modified, will the index of the table be recorded in the ldf log?
DAY25:逻辑漏洞
The implementation of the seemingly useless component (text gradient) in NaiveUI is so simple
Go-Excelize API source code reading (11) - GetActiveSheetIndex()
并查集模板
Rust学习:6.4_复合类型之枚举
iwemeta元宇宙:阿里首任COO:如何打造销售铁军