热点
模块化思维实战:后端站长从0到1构建高效网站,reasoning_content:我们要求以后端站长的口吻,写一个与“技术、科技”相关,关于[从0到1:运用模块化思维与实战技巧,轻松构建高效网站]的标题需要简短精炼,30字以内直接输出标题,不要加说明nn可以考虑:后端站长视角,强调模块化思维和实战技巧,从0到1构建高效网站可能的从0到1:后端站长教你模块化构建高效网站 但字数可能超?数一下:从0到1:后端站长教你模块化构建高效网站 —— 共18个字符(包括标点)?汉字每个算一个字,冒号、空格?严格来说:从0到1:后端站长教你模块化构建高效网站 共16个字(从0到1:后端站长教你模块化构建高效网站,数一下:从0到1:(3个汉字+1个冒号?不,是从0到1是4个字符,但通常算4个?我们按汉字+符号更好的:去掉一些词nn模块化思维实战:后端站长从0到1建高效站 数一下:模块化思维实战:后端站长从0到1建高效站 —— 共18个?模块化思维实战(6字)冒号(1)后端站长(4)从0到1(4)建高效站(4)总19?有点多nn更简洁:模块化构建高效网站:后端站长的从0到1 共14个字?模块化构建高效网站:后端站长的从0到1 —— 13个汉字+1冒号+1数字?其实从0到1是4个字符可以nn或者:后端站长:模块化思维从0到1打造高效网站 共16字?后端站长:模块化思维从0到1打造高效网站 —— 数:后(1)端(2)站(3)长(4):(5)模(6)块(7)化(8)思(9)维(10)从(11)0(12)到(13)1(14)打(15)造(16)高(17)效(18)网(19)站(20) —— 20字,超了nn要控制在30字以内,所以20字以内都行但最好更精炼nn考虑直接用原话中的从0到1和模块化思维、实战技巧、高效网站但题目要求以后端站长口吻,所以可以加上后端站长nn思路:从0到1:后端站长的模块化实战,高效网站轻松建 数一下:从0到1:后端站长的模块化实战,高效网站轻松建 —— 从0到1(4)冒号(1)后端站长(4)的(1)模块化实战(5)逗号(1)高效网站(4)轻松建(3)总共4+1+4+1+5+1+4+3=23字可以nn更简洁:模块化思维实战:后端站长从0到1建高效站 前面数过19字nn或者:后端站长带你从0到1模块化构建高效网站 15字nn注意:要求与技术、科技相关,但标题本身已经涉及nn我会选择:后端站长:模块化思维从0到1轻松构建高效网站 数:后(1)端(2)站(3)长(4):(5)模(6)块(7)化(8)思(9)维(10)从(11)0(12)到(13)1(14)轻(15)松(16)构(17)建(18)高(19)效(20)网(21)站(22) —— 22字可以nn另一个:从0到1:后端站长模块化实战构建高效网站 22字左右nn为了更口语化:从0到1轻松建站:后端站长教你模块化实战 15字?从0到1轻松建站:后端站长教你模块化实战 —— 共18字?nn我最后决定用:从0到1:模块化+实战,后端站长教你轻松建高效站 数:从0到1:模块化+实战,后端站长教你轻松建高效站 —— 共?从(1)0(2)到(3)1(4):(5)模(6)块(7)化(8)+(9)实(10)战(11),(12)后(13)端(14)站(15)长(16)教(17)你(18)轻(19)松(20)建(21)高(22)效(23)站(24) —— 24字略长去掉教你变成从0到1:模块化+实战,后端站长轻松建高效站 22字nn或者直接:模块化思维实战:后端站长从0到1构建高效网站 18字?数一下:模(1)块(2)化(3)思(4)维(5)实(6)战(7):(8)后(9)端(10)站(11)长(12)从(13)0(14)到(15)1(16)构(17)建(18)高(19)效(20)网(21)站(22) —— 22字还是22nn我认为22字完全在30字以内可以输出这个nn注意:不要加任何说明直接输出标题
16 9 月 2026, 周三

副标题#e#

技术沙龙 | 邀您于8月25日与国美/AWS/转转三位专家共同探讨小程序电商实战

前言

这两年做 streamingpro 时,不可避免的需要对Spark做大量的增强。就如同我之前吐槽的,Spark大量使用了new进行对象的创建,导致里面的实现基本没有办法进行替换。

对Spark的那些【魔改】

比如SparkEnv里有个属性叫closureSerializer,是专门做任务的序列化反序列化的,当然也负责对函数闭包的序列化反序列化。我们看看内部是怎么实现的:

  1. val serializer = instantiateClassFromConf[Serializer]( 
  2.       "spark.serializer", "org.apache.spark.serializer.JavaSerializer") 
  3.     logDebug(s"Using serializer: ${serializer.getClass}") 
  4.  
  5.     val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey) 
  6.  
  7.     val closureSerializer = new JavaSerializer(conf) 
  8.  
  9. val envInstance = new SparkEnv( 
  10. ..... 
  11.  closureSerializer, .... 

这里直接new了一个JavaSerializer,并不能做配置。如果不改源码,你没有任何办法可以替换掉掉这个实现。同理,如果我想替换掉Executor的实现,基本也是不可能的。

今年有两个大地方涉及到了对Spark的【魔改】,也就是不通过改源码,使用原有发型包,通过添加新代码的方式来对Spark进行增强。

二层RPC的支持

我们知道,在Spark里,我们只能通过Task才能touch到Executor。现有的API你是没办法直接操作到所有或者指定部分的Executor。比如,我希望所有Executor都加载一个资源文件,现在是没办法做到的。为了能够对Executor进行直接的操作,那就需要建立一个新的通讯层。那具体怎么做呢?

首先,在Driver端建立一个Backend,这个比较简单,

  1. class PSDriverBackend(sc: SparkContext) extends Logging { 
  2.  
  3.   val conf = sc.conf 
  4.   var psDriverRpcEndpointRef: RpcEndpointRef = null 
  5.  
  6.   def createRpcEnv = { 
  7.     val isDriver = sc.env.executorId == SparkContext.DRIVER_IDENTIFIER 
  8.     val bindAddress = sc.conf.get(DRIVER_BIND_ADDRESS) 
  9.     val advertiseAddress = sc.conf.get(DRIVER_HOST_ADDRESS) 
  10.     var port = sc.conf.getOption("spark.ps.driver.port").getOrElse("7777").toInt 
  11.     val ioEncryptionKey = if (sc.conf.get(IO_ENCRYPTION_ENABLED)) { 
  12.       Some(CryptoStreamUtils.createKey(sc.conf)) 
  13.     } else { 
  14.       None 
  15.     } 
  16.     logInfo(s"setup ps driver rpc env: ${bindAddress}:${port} clientMode=${!isDriver}") 
  17.     var createSucess = false 
  18.     var count = 0 
  19.     val env = new AtomicReference[RpcEnv]() 
  20.     while (!createSucess && count < 10) { 
  21.       try { 
  22.         env.set(RpcEnv.create("PSDriverEndpoint", bindAddress, port, sc.conf, 
  23.           sc.env.securityManager, clientMode = !isDriver)) 
  24.         createSucess = true 
  25.       } catch { 
  26.         case e: Exception => 
  27.           logInfo("fail to create rpcenv", e) 
  28.           count += 1 
  29.           port += 1 
  30.       } 
  31.     } 
  32.     if (env.get() == null) { 
  33.       logError(s"fail to create rpcenv finally with attemp ${count} ") 
  34.     } 
  35.     env.get() 
  36.   } 
  37.  
  38.   def start() = { 
  39.     val env = createRpcEnv 
  40.     val pSDriverBackend = new PSDriverEndpoint(sc, env) 
  41.     psDriverRpcEndpointRef = env.setupEndpoint("ps-driver-endpoint", pSDriverBackend) 
  42.   } 
  43.  

#p#副标题#e#

这样,你可以理解为在Driver端启动了一个PRC Server。要运行这段代码也非常简单,直接在主程序里运行即可:

  1. // parameter server should be enabled by default 
  2.     if (!params.containsKey("streaming.ps.enable") || params.get("streaming.ps.enable").toString.toBoolean) { 
  3.       logger.info("ps enabled...") 
  4.       if (ss.sparkContext.isLocal) { 
  5.         localSchedulerBackend = new LocalPSSchedulerBackend(ss.sparkContext) 
  6.         localSchedulerBackend.start() 
  7.       } else { 
  8.         logger.info("start PSDriverBackend") 
  9.         psDriverBackend = new PSDriverBackend(ss.sparkContext) 
  10.         psDriverBackend.start() 
  11.       } 
  12.     } 

这里我们需要实现local模式和cluster模式两种。

Driver启动了一个PRC Server,那么Executor端如何启动呢?Executor端似乎没有任何一个地方可以让我启动一个PRC Server? 其实有的,只是非常trick,我们知道Spark是允许自定义Metrics的,并且会调用用户实现的metric特定的方法,我们只要开发一个metric Sink,在里面启动RPC Server,骗过Spark即可。具体时下如下:

  1. class PSServiceSink(val property: Properties, val registry: MetricRegistry, 
  2.                     securityMgr: SecurityManager) extends Sink with Logging { 
  3.   def env = SparkEnv.get 
  4.  
  5.   var psDriverUrl: String = null 
  6.   var psExecutorId: String = null 
  7.   var hostname: String = null 
  8.   var cores: Int = 0 
  9.   var appId: String = null 
  10.   val psDriverPort = 7777 
  11.   var psDriverHost: String = null 
  12.   var workerUrl: Option[String] = None 
  13.   val userClassPath = new mutable.ListBuffer[URL]() 
  14.  
  15.   def parseArgs = { 
  16.     //val runtimeMxBean = ManagementFactory.getRuntimeMXBean(); 
  17.     //var argv = runtimeMxBean.getInputArguments.toList 
  18.     var argv = System.getProperty("sun.java.command").split("\\s+").toList 
  19.  
  20.    ..... 
  21.     psDriverHost = host 
  22.     psDriverUrl = "spark://ps-driver-endpoint@" + psDriverHost + ":" + psDriverPort 
  23.   } 
  24.  
  25.   parseArgs 
  26.  
  27.   def createRpcEnv = { 
  28.     val isDriver = env.executorId == SparkContext.DRIVER_IDENTIFIER 
  29.     val bindAddress = hostname 
  30.     val advertiseAddress = "" 
  31.     val port = env.conf.getOption("spark.ps.executor.port").getOrElse("0").toInt 
  32.     val ioEncryptionKey = if (env.conf.get(IO_ENCRYPTION_ENABLED)) { 
  33.       Some(CryptoStreamUtils.createKey(env.conf)) 
  34.     } else { 
  35.       None 
  36.     } 
  37.     //logInfo(s"setup ps driver rpc env: ${bindAddress}:${port} clientMode=${!isDriver}") 
  38.     RpcEnv.create("PSExecutorBackend", bindAddress, port, env.conf, 
  39.       env.securityManager, clientMode = !isDriver) 
  40.   } 
  41.  
  42.   override def start(): Unit = { 
  43.  
  44.     new Thread(new Runnable { 
  45.       override def run(): Unit = { 
  46.         logInfo(s"delay PSExecutorBackend 3s") 
  47.         Thread.sleep(3000) 
  48.         logInfo(s"start PSExecutor;env:${env}") 
  49.         if (env.executorId != SparkContext.DRIVER_IDENTIFIER) { 
  50.           val rpcEnv = createRpcEnv 
  51.           val pSExecutorBackend = new PSExecutorBackend(env, rpcEnv, psDriverUrl, psExecutorId, hostname, cores) 
  52.           PSExecutorBackend.executorBackend = Some(pSExecutorBackend) 
  53.           rpcEnv.setupEndpoint("ps-executor-endpoint", pSExecutorBackend) 
  54.         } 
  55.       } 
  56.     }).start() 
  57.  
  58.   } 
  59. ... 

到这里,我们就能成功启动RPC Server,并且连接上Driver中的PRC Server。现在,你就可以在不修改Spark 源码的情况下,尽情的写通讯相关的代码了,让你可以更好的控制Executor。

#p#副标题#e##p#分页标题#e#

比如在PSExecutorBackend 实现如下代码:

  1. override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = { 
  2.     case Message.TensorFlowModelClean(modelPath) => { 
  3.       logInfo("clean tensorflow model") 
  4.       TFModelLoader.close(modelPath) 
  5.       context.reply(true) 
  6.     } 
  7.     case Message.CopyModelToLocal(modelPath, destPath) => { 
  8.       logInfo(s"copying model: ${modelPath} -> ${destPath}") 
  9.       HDFSOperator.copyToLocalFile(destPath, modelPath, true) 
  10.       context.reply(true) 
  11.     } 
  12.   } 

接着你就可以在Spark里写如下的代码调用了:

  1. val psDriverBackend = runtime.asInstanceOf[SparkRuntime].psDriverBackend psDriverBackend.psDriverRpcEndpointRef.send(Message.TensorFlowModelClean("/tmp/ok")) 

是不是很酷。

修改闭包的序列化方式

Spark的任务调度开销非常大。对于一个复杂的任务,业务逻辑代码执行时间大约是3-7ms,但是整个spark运行的开销大概是1.3s左右。

经过详细dig发现,sparkContext里RDD转化时,会对函数进行clean操作,clean操作的过程中,默认会检查是不是能序列化(就是序列化一遍,没抛出异常就算可以序列化)。而序列化成本相当高(默认使用的JavaSerializer并且对于函数和任务序列化,是不可更改的),单次序列化耗时就达到200ms左右,在local模式下对其进行优化,可以减少600ms左右的请求时间。

当然,需要申明的是,这个是针对local模式进行修改的。那具体怎么做的呢?

我们先看看Spark是怎么调用序列化函数的,首先在SparkContext里,clean函数是这样的:

  1. private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = { 
  2.     ClosureCleaner.clean(f, checkSerializable) 
  3.     f 
  4.   } 

#p#副标题#e#

调用的是ClosureCleaner.clean方法,该方法里是这么调用学序列化的:

  1. try { 
  2.       if (SparkEnv.get != null) { 
  3.         SparkEnv.get.closureSerializer.newInstance().serialize(func) 
  4.       } 
  5.     } catch { 
  6.       case ex: Exception => throw new SparkException("Task not serializable", ex) 
  7.     } 

#p#副标题#e##p#分页标题#e#

SparkEnv是在SparkContext初始化的时候创建的,该对象里面包含了closureSerializer,该对象通过new JavaSerializer创建。既然序列化太慢,又因为我们其实是在Local模式下,本身是可以不需要序列化的,所以我们这里想办法把closureSerializer的实现替换掉。正如我们前面吐槽,因为在Spark代码里写死了,没有暴露任何自定义的可能性,所以我们又要魔改一下了。

首先,我们新建一个SparkEnv的子类:

  1. class WowSparkEnv( 
  2.                    ....) extends SparkEnv( 

接着实现一个自定义的Serializer:

  1. class LocalNonOpSerializerInstance(javaD: SerializerInstance) extends SerializerInstance { 
  2.  
  3.   private def isClosure(cls: Class[_]): Boolean = { 
  4.     cls.getName.contains("$anonfun$") 
  5.   } 
  6.  
  7.   override def serialize[T: ClassTag](t: T): ByteBuffer = { 
  8.     if (isClosure(t.getClass)) { 
  9.       val uuid = UUID.randomUUID().toString 
  10.       LocalNonOpSerializerInstance.maps.put(uuid, t.asInstanceOf[AnyRef]) 
  11.       ByteBuffer.wrap(uuid.getBytes()) 
  12.     } else { 
  13.       javaD.serialize(t) 
  14.     } 
  15.  
  16.   } 
  17.  
  18.   override def deserialize[T: ClassTag](bytes: ByteBuffer): T = { 
  19.     val s = StandardCharsets.UTF_8.decode(bytes).toString() 
  20.     if (LocalNonOpSerializerInstance.maps.containsKey(s)) { 
  21.       LocalNonOpSerializerInstance.maps.remove(s).asInstanceOf[T] 
  22.     } else { 
  23.       bytes.flip() 
  24.       javaD.deserialize(bytes) 
  25.     } 
  26.  
  27.   } 
  28.  
  29.   override def deserialize[T: ClassTag](bytes: ByteBuffer, loader: ClassLoader): T = { 
  30.     val s = StandardCharsets.UTF_8.decode(bytes).toString() 
  31.     if (LocalNonOpSerializerInstance.maps.containsKey(s)) { 
  32.       LocalNonOpSerializerInstance.maps.remove(s).asInstanceOf[T] 
  33.     } else { 
  34.       bytes.flip() 
  35.       javaD.deserialize(bytes, loader) 
  36.     } 
  37.   } 
  38.  
  39.   override def serializeStream(s: OutputStream): SerializationStream = { 
  40.     javaD.serializeStream(s) 
  41.   } 
  42.  
  43.   override def deserializeStream(s: InputStream): DeserializationStream = { 
  44.     javaD.deserializeStream(s) 
  45.   } 

#p#副标题#e#

接着我们需要再封装一个LocalNonOpSerializer,

  1. class LocalNonOpSerializer(conf: SparkConf) extends Serializer with Externalizable { 
  2.   val javaS = new JavaSerializer(conf) 
  3.  
  4.   override def newInstance(): SerializerInstance = { 
  5.     new LocalNonOpSerializerInstance(javaS.newInstance()) 
  6.   } 
  7.  
  8.   override def writeExternal(out: ObjectOutput): Unit = Utils.tryOrIOException { 
  9.     javaS.writeExternal(out) 
  10.   } 
  11.  
  12.   override def readExternal(in: ObjectInput): Unit = Utils.tryOrIOException { 
  13.     javaS.readExternal(in) 
  14.   } 

现在,万事俱备,只欠东风了,我们怎么才能把这些代码让Spark运行起来。具体做法非常魔幻,实现一个enhance类:

  1. def enhanceSparkEnvForAPIService(session: SparkSession) = { 
  2.       val env = SparkEnv.get 
  3.    //创建一个新的WowSparkEnv对象,然后将里面的Serializer替换成我们自己的LocalNonOpSerializer 
  4.     val wowEnv = new WowSparkEnv( 
  5.  ..... 
  6.       new LocalNonOpSerializer(env.conf): Serializer, 
  7.  ....) 
  8.     // 将SparkEnv object里的实例替换成我们的 
  9.     //WowSparkEnv 
  10.     SparkEnv.set(wowEnv) 
  11.   //但是很多地方在SparkContext启动后都已经在使用之前就已经生成的SparkEnv,我们需要做些调整 
  12. //我们先把之前已经启动的LocalSchedulerBackend里的scheduer停掉 
  13.     val localScheduler = session.sparkContext.schedulerBackend.asInstanceOf[LocalSchedulerBackend] 
  14.  
  15.     val scheduler = ReflectHelper.field(localScheduler, "scheduler") 
  16.  
  17.     val totalCores = localScheduler.totalCores 
  18.     localScheduler.stop() 
  19.  
  20.   //创建一个新的LocalSchedulerBackend 
  21.     val wowLocalSchedulerBackend = new WowLocalSchedulerBackend(session.sparkContext.getConf, scheduler.asInstanceOf[TaskSchedulerImpl], totalCores) 
  22.     wowLocalSchedulerBackend.start() 
  23.  //把SparkContext里的_schedulerBackend替换成我们的实现 
  24.     ReflectHelper.field(session.sparkContext, "_schedulerBackend", wowLocalSchedulerBackend) 
  25.   } 

完工。

其实还有很多

比如在Spark里,Python Worker默认一分钟没有被使用是会被杀死的,但是在StreamingPro里,这些python worker因为都要加载模型,所以启动成本是非常高的,杀了之后再启动就没办法忍受了,通过类似的方式进行魔改,从而使得空闲时间是可配置的。如果大家感兴趣,可以翻看StreamingPro相关代码。

【编辑推荐】

  1. 一文理清Apache Spark内存管理脉络
  2. 深度:Hadoop对Spark五大维度正面比拼报告!
  3. 机器学习实践:如何将Spark与Python结合?
  4. Spark性能优化:开发调优篇
  5. 大数据处理引擎Spark与Flink大比拼

【责任编辑:未丽燕 TEL:(010)68476606】
点赞 0

dawei

【声明】:北京站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。

您错过了

模块化思维实战:后端站长从0到1构建高效网站,reasoning_content:我们要求以后端站长的口吻,写一个与“技术、科技”相关,关于[从0到1:运用模块化思维与实战技巧,轻松构建高效网站]的标题需要简短精炼,30字以内直接输出标题,不要加说明nn可以考虑:后端站长视角,强调模块化思维和实战技巧,从0到1构建高效网站可能的从0到1:后端站长教你模块化构建高效网站 但字数可能超?数一下:从0到1:后端站长教你模块化构建高效网站 —— 共18个字符(包括标点)?汉字每个算一个字,冒号、空格?严格来说:从0到1:后端站长教你模块化构建高效网站 共16个字(从0到1:后端站长教你模块化构建高效网站,数一下:从0到1:(3个汉字+1个冒号?不,是从0到1是4个字符,但通常算4个?我们按汉字+符号更好的:去掉一些词nn模块化思维实战:后端站长从0到1建高效站 数一下:模块化思维实战:后端站长从0到1建高效站 —— 共18个?模块化思维实战(6字)冒号(1)后端站长(4)从0到1(4)建高效站(4)总19?有点多nn更简洁:模块化构建高效网站:后端站长的从0到1 共14个字?模块化构建高效网站:后端站长的从0到1 —— 13个汉字+1冒号+1数字?其实从0到1是4个字符可以nn或者:后端站长:模块化思维从0到1打造高效网站 共16字?后端站长:模块化思维从0到1打造高效网站 —— 数:后(1)端(2)站(3)长(4):(5)模(6)块(7)化(8)思(9)维(10)从(11)0(12)到(13)1(14)打(15)造(16)高(17)效(18)网(19)站(20) —— 20字,超了nn要控制在30字以内,所以20字以内都行但最好更精炼nn考虑直接用原话中的从0到1和模块化思维、实战技巧、高效网站但题目要求以后端站长口吻,所以可以加上后端站长nn思路:从0到1:后端站长的模块化实战,高效网站轻松建 数一下:从0到1:后端站长的模块化实战,高效网站轻松建 —— 从0到1(4)冒号(1)后端站长(4)的(1)模块化实战(5)逗号(1)高效网站(4)轻松建(3)总共4+1+4+1+5+1+4+3=23字可以nn更简洁:模块化思维实战:后端站长从0到1建高效站 前面数过19字nn或者:后端站长带你从0到1模块化构建高效网站 15字nn注意:要求与技术、科技相关,但标题本身已经涉及nn我会选择:后端站长:模块化思维从0到1轻松构建高效网站 数:后(1)端(2)站(3)长(4):(5)模(6)块(7)化(8)思(9)维(10)从(11)0(12)到(13)1(14)轻(15)松(16)构(17)建(18)高(19)效(20)网(21)站(22) —— 22字可以nn另一个:从0到1:后端站长模块化实战构建高效网站 22字左右nn为了更口语化:从0到1轻松建站:后端站长教你模块化实战 15字?从0到1轻松建站:后端站长教你模块化实战 —— 共18字?nn我最后决定用:从0到1:模块化+实战,后端站长教你轻松建高效站 数:从0到1:模块化+实战,后端站长教你轻松建高效站 —— 共?从(1)0(2)到(3)1(4):(5)模(6)块(7)化(8)+(9)实(10)战(11),(12)后(13)端(14)站(15)长(16)教(17)你(18)轻(19)松(20)建(21)高(22)效(23)站(24) —— 24字略长去掉教你变成从0到1:模块化+实战,后端站长轻松建高效站 22字nn或者直接:模块化思维实战:后端站长从0到1构建高效网站 18字?数一下:模(1)块(2)化(3)思(4)维(5)实(6)战(7):(8)后(9)端(10)站(11)长(12)从(13)0(14)到(15)1(16)构(17)建(18)高(19)效(20)网(21)站(22) —— 22字还是22nn我认为22字完全在30字以内可以输出这个nn注意:不要加任何说明直接输出标题