Spark 2.x 中 Sort-Based Shuffle 产生的内幕
本课主题
- Sorted-Based Shuffle 的诞生和介绍
- Shuffle 中六大令人费解的问题
- Sorted-Based Shuffle 的排序和源码鉴赏
- Shuffle 在运行时的内存管理
引言
在历史的发展中,为什么 Spark 最终还是选择放弃了 HashShuffle 而使用了 Sorted-Based Shuffle,而且作为后起之秀的 Tungsten-based Shuffle 它到底在什么样的背景下产生的。Tungsten-Sort Shuffle 已经并入了 Sorted-Based Shuffle,Spark 的引擎会自动识别程序需要原生的 Sorted-Based Shuffle 还是用 Tungsten-Sort Shuffle,那识别的依据是什么,其实 Spark 会检查相对的应用程序有没有 Aggregrate 的操作。文章的后续部份会介绍 Tungsten-Sort Shuffle 是如何管理内存和CPU。其实 Sorted-Based Shuffle 也有缺点,其缺点反而是它排序的特性,它强制要求数据在 Mapper 端必须要先进行排序 (注意,这里没有说对计算结果进行排序),所以导致它排序的速度有点慢。而 Tungsten-Sort Shuffle 对它的排序算法进行了改进,优化了排序的速度。希望这篇文章能为读者带出以下的启发:
- 了解为什么 Spark 最终选择了 Sorted-Based Shuffle 而放弃了Hash-Based Shuffle
- 了解什么是 Spark Sorted-Based Shuffle
- 了解 Spark Shuffle 中六大令人费解的问题
- 了解 Sorted-Based Shuffle 具体是如何排序
Spark Sorted-Based Shuffle 的诞生
为什么 Spark 用 Sorted-Based Shuffle 而放弃了 Hash-Based Shuffle?在 Spark 里为什么最终是 Sorted-Based Shuffle 成为了核心,有基本了解过 Spark 的学习者都会知道,Spark会根据宽依赖把它一系列的算子划分成不同的 Stage,Stage 的内部会进行 Pipeline,Stage 与 Stage 之间进行 Shuffle,Shuffle 的过程包含三部份。
第一部份是 Shuffle 的 Write;第二部份是网络的传输;第三部份就是 Shuffle 的 Read,这三大部份设置了内存操作、磁盘IO、网络IO以及 JVM 的管理。而这些东西影响了 Spark 应用程序在 95%以上效率的唯一原因,假设你程序代码的质素本身是非常好的情况下,你性能的95%都消耗在 Shuffle 阶段的本地写磁盘文件,网络传输数据以及抓取数据这样的生命周期中。
在 Shuffle 写数据的时候,内存中有一个缓存区叫 Buffer,你可以想像成一个Map,同时在本地磁盘有相对应的本地文件。如果本地磁盘有文件你在内存中肯定也需要有相对应的管理句柄。也就是说,单是从 ShuffleWrite 内存占用的角度讲,已经有一部份内存空间是用在存储 Buffer 数据的,另一部份的内存空间是用来管理文件句柄的,回顾 HashShuffle 所产生小文件的个数是 Mapper 分片数量 x Reducer 分片数量 (MxR)。比如Mapper端有1千个数据分片,Reducer端也有1千过数据分片,在 HashShuffle 的机制下,它在本地内存空间中会产生 1000 * 1000 = 1,000,000 个小文件,可想而知的结果会是什么,这么多的 IO,这么多的内存消耗、这么容易产生 OOM、以及这么沉重的 CG 负担。再说,如果Reducer端去读取 Mapper端的数据时,Mapper 端有这么多的小文件,要打开很多网络通道去读数据,打开 1,000,000 端口不是一件很轻松的事。这会导致一个非常经典的错误:Reducer 端也就是下一个 Stage 通过 Driver 去抓取上一个 Stage 属于它自己的数据的时候,说文件找不到。其实这个时候不是真的是磁盘上文件找不到,而是程序不响应,因为它在进行垃圾回收 (GC) 操作。
因为 Spark 想完成一体化多样化的数据处理中心或者叫一统大数据领域的一个美梦,肯定不甘心于自己只是一个只能处理中小规模的数据计算平台,所以Spark最根本要优化和逼切要解决的问题是:减少 Mapper 端 ShuffleWriter 所产生的文件数量,这样便可以能让 Spark 从几百台集群的规模中瞬间变成可以支持几千台甚至几万台集群的规模。(一个Task背后可能是一个Core去运行、也可能是多个Core去运行,但默认情况下是用一个Core去运行一个Task)。
减少Mapper端的小文件所带来的好处是:
- Mapper端的内存占用变少了;
- Spark可以处理不竟竟是小规模的数据,处理大规模的数据也不会很容易达到性能瓶颈;
- Reducer端抓取数据的次数也变少了;
- 网络通道的句柄也变少;
- 极大了减少 Reducer 的内存不竟竟是因为数据级别的消耗,而且是框架时要运行的必须消耗。
Spark Sorted-Based Shuffle介绍
Sorted-Based Shuffle 的出现,最显著的优势就是把 Spark 从只能处理中小规模的数据平台,变成可以处理无限大规模的数据平台。可能你会问规模真是这么重要吗?当然有,集群规模意为著它处理数据的规模,也意为著它的运算能力。
Sorted-Based Shuffle 不会为每个Reducer 中的Task 生产一个单独的文件,相反Sorted-Based Shuffle 会把Mapper 中每个ShuffleMapTask 所有的输出数据Data 只写到一个文件中,因为每个ShuffleMapTask 中的数据会被分类,所以Sort-based Shuffle 使用了index 文件存储具体ShuffleMapTask 输出数据在同一个Data 文件中是如何分类的信息。所以说基于 Sort-based Shuffle 会在 Mapper 中的每一个 ShuffleMapTask 中产生两个文件 (并发度的个数 x 2)!!!
它会产生一个 Data 文件和一个 Index 文件,其中 Data 文件是存储当前 Task 的 Shuffle 输出的, 而 Index 文件则存储了 Data 文件中的数据通过 Partitioner 的分类信息,此时下一个阶段的 Stage 中的 Task 就是根据这个 Index 文件获取自己所需要抓取的上一个 Stage 中 ShuffleMapTask 所产生的数据;
假设现在 Mapper 端有 1000 个数据分片,Reducer 端也有 1000 个数据分片,它的并发度是100,使用 Sorted-Based Shuffle 会产生多少个 Mapper端的小文件,答案是 100 x 2 = 200 个。它的 MapTask 会独自运行,每个 MapTask 在运行的时候写2个文件,运行成功后就不需要这个 MapTask 的文件句柄,无论是文件本身的句柄还是索引的句柄都不需要,所以如果它的并发度是 100 个 Core,每次运行 100 个任务的话,它最终只会占用 200 个文件句柄,这跟 HashShuffle 的机制不一样,HashShuffle 最差的情况是 Hashed 句柄存储在内存中的。
Sorted-Based Shuffle 主要是在Mapper阶段,这个跟Reducer端没有任何关系,在Mapper阶段它要进行排序,你可以认为是二次排序,它的原理是有2个Key进行排序,第一个是 PartitionId进行排序,第二个是就是本身数据的Key进行排序。看下图,它会把 PartitionId 分成3个,分别是索引为 0、1、2,这个在Mapper端进行排序的过程其实是让Reducer去抓取数据的时候变得更高效,比如说第一个Reducer,它会到Mappr端的索引为 0 的数据分片中抓取数据。
具体而言,Reducer 首先找 Driver 去获取父 Stage 中每个 ShuffleMapTask 输出的位置信息,跟据位置信息获取 Index 文件,解析 Index 文件,从解析的 Index 文件中获取 Data 文件中属于自己的那部份内容。
一个Mapper任务除了有一个数据文件以外,它也会有一个索引文件,Map Task 把数据写到文件磁盘是顺序根据自身的Key写进去的,也同时按照 Partition写进去的,因为它是顺序写数据,记录每个 Partition 的大小。
Sort-Based Shuffle 的弱点:
- 如果 Mapper 中 Task 的数量过大,依旧会产生很多小文件,此时在 Shuffle 传数据的过程中到 Reducer 端,Reducer 会需要同时大量的记录来进行反序例化,导致大量内存消耗和GC 的巨大负担,造成系统缓慢甚至崩溃!
- 强制了在 Mapper 端必顺要排序,这里的前提是本身数据根本不需要排序的话;
- 如果需要在分片内也进行排序的话,此时需要进行 Mapper 端和 Reducer 端的两次排序!
- 它要基于记录本身进行排序,这就是 Sort-Based Shuffle 最致命的性能消耗;
Shuffle 中六大令人费解的问题
- 第一大问题:什么时候进行 Shuffle 的 fetch 操作?Shuffle 是在一边进行 Mapper 端 map 操作的同时,一边进行 Reducer 端的 shuffle 和 reduce 操作吗?
错误的观点是:Spark 是一边 Mapper 一边 Shuffle 的,而 Hadoop MapReduce 是先完成 Mapper 然后才进行 Reducer 的 Shuffle。正确的观点是 Spark 一定是先完成 Mapper 端所有的 Tasks,才会进行 Reducer 端的 Shuffle 过程。这是因为 Spark Job 是按照 Stage 线性执行的,前面的 Stage 必须执行完毕,才能够执行后面 Reducer 端的 Shuffle 过程。- 更准确来说 Spark Shuffle 的过程是边拉取数据边进行 Aggregrate 操作的,其实与 Hadoop MapReduce 相比其优势确实是在速度上,但是也会导致一些算法不好实现,例如求平均值等,为什么呢?因为边拉取数据边进行 Aggregrate 这个过程中,后面的Stage依赖于前面的Stage,Spark 是以 Stage 为单位进行计算的,如果里面的任务没有计算完,后面你怎么计算呢。但如果你是求和的话,它就会计算的特别快;
- Hadoop MapReduce 是把数据拉过来之后,然后进行计算,如果用 MapReduce 求平均值的话,它的算法就会很好实现。
- 第二大问题:Shuffle fetch 过来的数据到底放在了那里?
Spark 这种很灵活地使用并行度以及倾向于优先使用内存的计算模型,如果不正常地使用这些特徵的话会很容易导致 Spark 的应用程序出现 OOM 的情况,Spark 在不同的版本 fetch 过来的数据放在哪里是有不同的答案。抓过来的数据首先会放在 Reducer 端的内存缓存区中,Spark曾经有版本要求只能放在内存缓存中,其数据结构类似于 HashMap (AppendOnlyMap),显然这个设计特别消耗内存和极易出现OOM,同时这也极大的限制了 Spark 集群的规模,现在的实现都是内存 + 磁盘的方式 (数据结构类使用了 ExternalAppendOnlyMap),当然也可以通过调以下参数来设置只能使用内存。1spark.shuffle.spill
=
false
如果设置了这个运行模式,在生产环境下建义对内存的数据作2份备份,因为在默认情况下内存数据只有1份,它不像HDFS那样,天然有3份备份。使用 ExternalAppendOnlyMap 的方式时,如果内存占用率达到一定的临界值后会首先尝试在内存中扩大 ExternalAppendOnlyMap (内部有实现算法),如果不能扩容的话才会 spill 到磁盘。
第三大问题:Shuffle 的数据在 Mapper 端如何存储,在 Reducer 端如何知道数据具体在那里的?在Spark的实现上每一个Stage (里面是 ShuffleMapTask) 中的 Task 在 Stage 的最后一个 RDD 上一定会注册给 Driver 上的 MapOutputTrackerMaster,Mapper 通过和 MapOutputTrackerMaster 来汇报 ShuffleMapTask 具体输出数据的位置 (具体的输出文件及内容是和 Reducer 有关的),Reducer 是向 Driver 中的 MapOutputTrackerMaster 请求数据的元数据信息,然后和 Mapper 所在的 Executor 进行通信。
- 第四大问题:竟竟从 HashShuffle 的角度来讲,我们在 Shuffle 的时候到底可以产生多少 Mapper 端的中间文件?
这里有一个很重要的调优参数 (可以在 TaskSchedulerImpl.scala 中找到此参数),该参数决定了 Spark 在运行时每个 Task 所需要的 Core 的个数,默认情况是1个,现在假设 spark.task.cpus=T。
- 问题一、例如说有M个Mapper、R个Reducer 和 C个Core,那么 HashShuffle 可以产生多少个 Mapper 的中间文件?
- HashShuffle 会产生 C x R 的小文件。
- Consolidated HashShuffle 有可能产生 C x R 个小文件。因为设置了 spark.task.cpus 的参数,那么真实的答案是 (C / T) x R 个小文件。
- 问题二、例如在生产环境下有 E 个 Executors (例如100个),每个 Executor上有 C 个Cores (例如10个),同时也有 R 个Reducer,那么 HashShuffle 可以产生多少个 Mapper 的中间文件?
- HashShuffle 会产生实际 Task 的个数 x R 个的小文件。
- Consolidated HashShuffle 会产生 (E x (C / T)) x R 个的小文件。
- 问题一、例如说有M个Mapper、R个Reducer 和 C个Core,那么 HashShuffle 可以产生多少个 Mapper 的中间文件?
- 第五大问题:Spark中Sorted-Based Shuffle 数据结果默认是排序的吗?Sorted-Based Shuffle 采用了什么的排序算法?这个排序算法的好处是什么?
Spark Sorted-Based Shuffle 在 Mapper 端是排序的,包括 partition 的排序和每个 partition 内部元素的排序!但在 Reducer 端是没有进行排序,所以 Job 的结果默认不是排序的。Sorted-Based Shuffle 采用了 Tim-Sort 排序算法,好处是可以极为高效的使用 Mapper 端的排序成果全局排序。 - 第六大问题:Spark Tungsten-Sorted Shuffle 在 Mapper 中会对内部元素进行排序吗?Tungsten-Sorted Shuffle不适用于什么情况?说出具体的原因。
Tungsten-Sorted Shuffle 在 Mapper 中不会对内部元素进行排序 (它只会对Partition进行排序),原因是它自己管理的二进制序列化后的数据,问题来啦:数据是进入 Buffer 时或者是进入磁盘的时才进行排序呢?答案是数据的排序是发生在 Buffer 要满了 spill 到磁盘时才进行排序的。所以 Tungsten-Sorted Shuffle 它对内部不会进行排序
Tungsten-Sorted Shuffle 什么时候会退化成为 Sorted-Based Shuffle?它是在程序有 Aggregrate 操作的时候;或者是 Mapper 端输出的 partition 大于 16777216;或者是一条 Record 大于128M的时候,原因也是因为它自己管理的二进制序列化后的数据以及数组指针管理范围。
Sorted-Based Shuffle 的排序算法
人们会对 Sorted-Based Shuffle 有一种误解,就是它产出的结果是有序的,这一节会讲解 Sorted-Based Shuffle 是如何工作的并配合源码看看它具体的实现,Sorted-Based Shuffle 的核心是借助于 ExternalSorter 把每个 ShuffleMapTask 的输出排序到一个文件中 (FileSegmentGroup),为了区分下一个阶段 Reducer Task 不同的内容,它还需要有一个索引文件 (Index) 来告诉下游 Stage 的并行任务,那一部份是属于你的。
上图在 Reducer 端有4个Reducer Task,它会产生一组 File Group 和 一个索引文件,File Group 里的 FileSegement 会进行排序,下游的 Task 可以很容易跟据索引 (index) 定位到这个 Fie 中的那一部份 FileSegement 是属于下游的,它相当于一个指针,下游的 Task 要向 Driver 去碓定文件在那里,然后到了这个 File 文件所在的地方,实际上会跟 BlockManager 进行沟通,BlockManager 首先会读一个 Index 文件,根据它的命名则进行解析,比如说下一个阶段的第一个 Task,一般就是抓取第一个 Segment,这是一个指针定位的过程。
再次强调 Sort-Based Shuffle 最大的意义是减少临时文件的输出数量,且只会产生两个文件:一个是包含不同内容划分成不同 FileSegment 构成的单一文件 File,另外一个是索引文件 Index。上图在 Sort-Based Shuffle 的介绍中看见了一个 Sort and Spill 的过程 (它是 Spill 到磁盘的时候再进行排序的),现在我们从源码的角度去看看到底它这个排序实际上是在干什么的。
Sort-Based Shuffle 排序源码
- 首先从 ShuffleMapTask 的 runTask方法中找出当前的 ShuffleManager,然后找出 writer 的方法,然后把 shuffleHandle、partitionId 以及內容作为参数传进来,并调用它的 getWriter 方法。
[下图是一个 ShuffleMapTask.scala 中 writer 方法] - 找出 ShuffleManager,在Spark2.X版本中只有 SortShuffleManager,已经没有了 Hash-Based Shuffle Manager 了
[下图是一个 SparkEnv.scala 中 shuffleManager 成员] - 定义 ShuffleManager 的 Handler 方式,通过调用 registerShuffle 方法来定义要用那种排序策略。
[下图是一个 Dependency.scala 中 shuffleHandle 成员]
[下图是一个 SortShuffleManager.scala 中 registerShuffle 方法] - 在这里看到有三种 ShuffleWriter 的实现方式,第一种是 UnsafeShuffleWriter,第二种是 BypassMergeSortShuffleWriter; 第三种是 SortShuffleWriter。现在我们深入看看 SortShuffleWriter 的具体实现方式。
[下图是一个 SortShuffleManager.scala 中 getWriter 方法] - 然后创建了 SortShuffleWriter 的实例对象,在这里你看见它创建了一个 ExternalSorter 的实例,write 方法会首先判断一下会不会进行 combine 操作,其实就是本地需不需要有 aggregrator 的问题,创建了 sorter 之后,它会先对我们的 sorted 进行 insertAll 的操作,它会根据 ShuffleId 和 StageId 去获得 output,然后它通过 sorter 中的 writePartitionedFie 方法来获得 partitionLengths。最后是一个 mapStatus,它是我们运制程序最后返回的一个数据结构,它会告诉你数据在那里。
[下图是一个 SortShuffleManager.scala 中 write 方法] - 这种基于Sort的Shuffle实现机制中引入了外部排序器(ExternalSorter),ExternalSorter继承了 spillable,因为内存攸用在达到一定值时,会spill到磁盘中,这样的设计可以减少内存的开销。在 External Sorter 中会定义 fileBufferSize 的大小,默应是 32k x 1024 的大小。
[下图是一个 ExternalSorter.scala 中 fileBufferSize 大小] - 通过查看外部排序器(ExternalSorter)的insertAll方法,然后 sorter 调用了 insertAll( ) 的方法。
[下图是一个 ExternalSorter.scala 中 insertAll 方法] - 对于外部排序器(External Sorter),除了 insertAll 方法外,它的 writePartitionedFile 方法也非常重要,其中BlockId是数据块的逻辑位置,File 参数则是对应逻辑位置的物理存储位置,这两个参数值获取方法和使用 BypassMergeSOrtShuffleHandle及其对应的 ShuffleWriter 是一样的。
- xxxxx
- xxx
- 创建了一个 Sorter 的实例
- 代码追迹到这里可以看见它使用了什么排序的方法
总结
Sorted-Based Shuffle 的诞生和出现意味著 Spark 从只能处理〝中小规模数据的数据处理平台"从新定位为能够处理〝大规模数据的数据处理平台",更进一步巩故它在数据处理领域的龙头地位。它最大的优化就是:减少了因为 HashShuffle 机制不论是原生 HashShuffle、还是 Consolidated Shuffle 在 Mapper 端所产生的海量小文件 (这是应用程序运行时的一个中间过程),中间文件数量从 M x R 个数 变成 2M 的小文件。数据量愈大,这个所优化带来的效果便愈来愈强烈,这是优化的第一步。
虽然 Sort-Based Shuffle 已经大大提升了序程运行时的效率,但如果 Mapper 端并行度的数据分片过多的话,也会导致大量内存消耗和GC的巨大负担,造成系统缓慢甚至崩溃。基于这观点 Spark 再一次突破自己,推出了 Tungsten-Based Shuffle,提升了在Mapper端进行排序的速度,充分利用了的 CPU 等资源。
參考資料
Spark 2.x 中 Sort-Based Shuffle 产生的内幕的更多相关文章
- Spark技术内幕:Sort Based Shuffle实现解析
在Spark 1.2.0中,Spark Core的一个重要的升级就是将默认的Hash Based Shuffle换成了Sort Based Shuffle,即spark.shuffle.manager ...
- Spark-1.6.0中的Sort Based Shuffle源码解读
从Spark-1.2.0开始,Spark的Shuffle由Hash Based Shuffle升级成了Sort Based Shuffle.即Spark.shuffle.manager从Hash换成了 ...
- Spark Sort Based Shuffle内存分析
分布式系统里的Shuffle 阶段往往是非常复杂的,而且分支条件也多,我只能按着我关注的线去描述.肯定会有不少谬误之处,我会根据自己理解的深入,不断更新这篇文章. 前言 借用和董神的一段对话说下背景: ...
- Spark Tungsten揭秘 Day2 Tungsten-sort Based Shuffle
Spark Tungsten揭秘 Day2 Tungsten-sort Based Shuffle 今天在对钨丝计划思考的基础上,讲解下基于Tungsten的shuffle. 首先解释下概念,Tung ...
- [Spark性能调优] 第三章 : Spark 2.1.0 中 Sort-Based Shuffle 产生的内幕
本課主題 Sorted-Based Shuffle 的诞生和介绍 Shuffle 中六大令人费解的问题 Sorted-Based Shuffle 的排序和源码鉴赏 Shuffle 在运行时的内存管理 ...
- [Spark性能调优] 第四章 : Spark Shuffle 中 JVM 内存使用及配置内幕详情
本课主题 JVM 內存使用架构剖析 Spark 1.6.x 和 Spark 2.x 的 JVM 剖析 Spark 1.6.x 以前 on Yarn 计算内存使用案例 Spark Unified Mem ...
- spark性能调优(四) spark shuffle中JVM内存使用及配置内幕详情
转载:http://www.cnblogs.com/jcchoiling/p/6494652.html 引言 Spark 从1.6.x 开始对 JVM 的内存使用作出了一种全新的改变,Spark 1. ...
- Spark Shuffle 中 JVM 内存使用及配置内幕详情
本课主题 JVM 內存使用架构剖析 Spark 1.6.x 和 Spark 2.x 的 JVM 剖析 Spark 1.6.x 以前 on Yarn 计算内存使用案例 Spark Unified M ...
- Apache Spark 2.2中基于成本的优化器(CBO)(转载)
Apache Spark 2.2最近引入了高级的基于成本的优化器框架用于收集并均衡不同的列数据的统计工作 (例如., 基(cardinality).唯一值的数量.空值.最大最小值.平均/最大长度,等等 ...
随机推荐
- applets
Java Applet 可以大大提高Web页面的交互能力和动态执行能力.包含Applet的网页被称为Java-powered页,可以称其为Java支持的网页. Applet 当用户访问这样的网页时,A ...
- PHP之mb_stripos使用
mb_stripos (PHP 5 >= 5.2.0, PHP 7) mb_stripos - Finds position of first occurrence of a string wi ...
- Session 与 Cookie
Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态.它们本身只是HTTP中的一个配置项,在servlet规范中也只对应一个类而已. 理解 Cookie 通俗地说就是当一 ...
- 剑指offer(11-20)编程题
二进制中1的个数 数值的整数次方 调整数组顺序使奇数位于偶数前面 链表中倒数第k个结点 反转链表 合并两个排序的链表 树的子结构 二叉树的镜像 顺时针打印矩阵 包含min函数的栈 11.输入一个整数, ...
- springboot主从数据库
是从springmvc的思路上来做的,主要就是配置主.从DataSource,再继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法,通过 ...
- OC基础之访问修饰符
做C#.Java的人都知道private.protect.public这些访问权限,在OC中看项目基本都没用到这些,昨天就好奇的仔细了解了下,其实OC中也是有这些访问权限的修饰.说实话有好多做过编程的 ...
- 在用户控件(ASCX)创建用户控件(ASCX)
"我建了两个ascx,ascxA,ascxBascxA中放了一个PlaceHold,ascxB中放了一个textBoxascxA在page_load中动态创建了5个ascxB但是页面上什么都 ...
- 四:SpringCloud-Hystrix
八:Hystrix断路器 1. 概述 1.1 分布式系统面临的问题 ==复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败.== 上图中的请求需要调用A, P, H ...
- memcached 细究(一)
memcached是高性能的分布式的内存缓存服务器.由国外社区网站LIVEJOURNAL的开发团队开发. 使用目的: 通过缓存数据库查询结果,减少数据库的访问次数,以提高动态web应用的速度.提高可扩 ...
- WCF 4.0 如何编程修改wcf配置,不使用web.config静态配置
How to programmatically modify WCF without web.config setting WCF 4.0 如何编程修改wcf配置,不使用web.config静态配置 ...