我是怎么在Spark中踩到Jetty的坑的
来源:互联网 发布:开源yii框架商城源码 编辑:程序博客网 时间:2024/06/04 18:05
前言
大家知道Spark有一个HistoryServer可以用来查看event log,即在app运行完成后仍然可以查看它的统计信息等,便于事后分析。我司做的产品化的Spark中,对于web页面均有一定的安全要求,例如说必须通过https访问,且禁止一些不安全的协议和算法(例如SSLv3等)。
另外一点,还基于web的filter机制做了简单的cas认证,即访问HistoryServer的用户必须先跳转到cas server进行认证,认证通过后才可以继续访问。
之前的方案是基于SAML filter做的方案,HistoryServer这边只需要作为cas server的客户单,增加两个SAML的filter(Saml11AuthenticationFilter/Saml11TicketValidationFilter)并添加一些和服务端相对应的参数就可以了(关于SAML/SSO的相关知识可以参考这里)。这几天突然说要升级,基于cas20来做,即将SAML的filter替换成Cas20ProxyCasAuthenticator/Cas20ProxyReceivingTicketValidationFilter。
本来以为是挺简单一个活,无非是改改配置项,联调一下就ok了嘛。但没想到替换之后,HistoryServer无法启动了,一直报错“Java.lang.IllegalStateException: TimerTask is scheduled already”
原因
好嘛,既然是替换出了原因,看这个报错就是Java直接报出来的,从错误栈看是Cas20ProxyReceivingTicketValidationFilter这个类报的,打开看看吧:
public void init() { super.init(); CommonUtils.assertNotNull(this.proxyGrantingTicketStorage, "proxyGrantingTicketStorage cannot be null."); if (this.timer == null) { this.timer = new Timer(true); } if (this.timerTask == null) { this.timerTask = new CleanUpTimerTask(this.proxyGrantingTicketStorage); } this.timer.schedule(this.timerTask, this.millisBetweenCleanUps, this.millisBetweenCleanUps); }
可以看到,它的init方法中会触发一个定时任务,而显然我们这个地方报错的原因是多次触发了这个定时任务,即多次调用了这个类的init方法。
为什么会多次调用一个filter的init方法呢?从Spark HistoryServer的代码入口:
/** Add filters, if any, to the given list of ServletContextHandlers */ def addFilters(handlers: Seq[ServletContextHandler], conf: SparkConf) { val filters: Array[String] = conf.get("spark.ui.filters", "").split(',').map(_.trim()) filters.foreach { case filter : String => if (!filter.isEmpty) { logInfo("Adding filter: " + filter) val holder : FilterHolder = new FilterHolder() holder.setClassName(filter) // Get any parameters for each filter conf.get("spark." + filter + ".params", "").split(',').map(_.trim()).toSet.foreach { param: String => if (!param.isEmpty) { val parts = param.split("=") if (parts.length == 2) holder.setInitParameter(parts(0), parts(1)) } } val prefix = s"spark.$filter.param." conf.getAll .filter { case (k, v) => k.length() > prefix.length() && k.startsWith(prefix) } .foreach { case (k, v) => holder.setInitParameter(k.substring(prefix.length()), v) } val enumDispatcher = java.util.EnumSet.of(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST) handlers.foreach { case(handler) => handler.addFilter(holder, "/*", enumDispatcher) } } } }
addFilter发生了什么呢?通过debug,调用栈如下:
ServerContextHandler.addFilter -> ServletHanlder.addFilterWithMapping -> addFilterMapping -> setFilterMappings -> updateMappings -> initialize
好,此处initialize方法中会把这个handler里所有的FilterHolder都拿出来进行初始化,即:
for (FilterHolder f: _filters) { try { f.start(); f.initialize(); } catch (Exception e) { mx.add(e); } }
这个初始化过程中会发生什么呢?再往下看:
@Override public void initialize() throws Exception { super.initialize(); if (_filter==null) { try { ServletContext context=_servletHandler.getServletContext(); _filter=(context instanceof ServletContextHandler.Context) ?((ServletContextHandler.Context)context).createFilter(getHeldClass()) :getHeldClass().newInstance(); } catch (ServletException se) { Throwable cause = se.getRootCause(); if (cause instanceof InstantiationException) throw (InstantiationException)cause; if (cause instanceof IllegalAccessException) throw (IllegalAccessException)cause; throw se; } } _config=new Config(); if (LOG.isDebugEnabled()) LOG.debug("Filter.init {}",_filter); _filter.init(_config); }
原来是实例化其封装的filter,并实例化一个Config类来初始化filter。
好了,既然知道调用的整个过程了,接下来就是要弄明白此处代码为何会被多次调用呢?
回到HistoryServer的代码,发现对每个handler,我们add的都是同一个FilterHolder对象,而在FilterHolder中,其封装的filter只会被实例化一次,后面就是用不同的Config对象对齐进行初始化了。在HistoryServer中,通常会有多个handler,那每个handler的addFilter方法都会调用被add的Filter的init方法,我们使用的又是同一个FilterHolder对象,当然这个FilterHolder对象内的filter会被不同的Config对象init多次了。
bingo!这里就是问题的一个疑点,那我们应该怎样做呢?
简单,对每个handler都重新构造一个FilterHolder对象,这样的话只要保证封装的filter不是单例类,就不会init多次了!
改改改,打包打包打包,重新启动。。。。。。。。。。还是同样的错误……
说明filter被初始化多次的原因不止这一个,那到底是怎样呢?
我们再回去看看代码,等等,不对,这个地方为什么会是这样……
//start filter holders now if (_filters != null) { for (FilterHolder f: _filters) { try { f.start(); f.initialize(); } catch (Exception e) { mx.add(e); } } }
如果你还有印象的话,这段代码是可以通过addFilter调用到的。每次addFIlter都要把现有的FilterHolder初始化一遍,后果不就是将先被add的Filter初始化了N次吗?用图来表示的话,是这样的:
看到了吗……本来多个handler重复添加的就是同一个filter对象,更可怕的是handler每添加一个filter,都要把之前添加过的filter拉出来初始化一遍。也就是说,在这个场景中,每个filter被初始化的次数是(N - i) * M,其中N是filter个数,i是被此filter被添加的次序,M是handler的次数。
我们再之前只是将M降低到了1,但这个N - i还是不可避免的。
解决思路
接下来怎么办呢?这看起来是个Jetty的坑,就是标题中所提到的啦。Jetty的问题嘛,交给Jetty的专家们去解决好了,咱们只管提issue:https://github.com/eclipse/jetty.project/issues/1050
当然,Jetty不愧是apache的top项目,解决起来也是很快的:https://github.com/eclipse/jetty.project/commit/f3f31d163c4f04d5c8f1bc2e4ae38f8c88583e77
这个fix要到Jetty 9.3.x才能修复,咱们现在用的9.2.x。咱们总不能等到他们release咱们才做升级吧?先规避过去再说,怎么规避的呢?这里就不透露了,留给诸位去思考吧 :)
声明:本文为原创,版权归本人所有,禁止用于任何商业目的,转载请注明出处:http://blog.csdn.net/asongoficeandfire/article/details/53180375
- 我是怎么在Spark中踩到Jetty的坑的
- 我是怎么做开源的
- 我是怎么做开源的
- 我是怎么做QA的
- 我是怎么学习VC的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- 我是怎么招聘程序员的
- Android捕获异常保存SD卡或上传服务器
- MapReduce作业提交流程
- Ubuntu安装JDK及环境变量配置
- iOS新项目的环境配置info.plist文件
- script片段在前导致对下文的html元素引用失效
- 我是怎么在Spark中踩到Jetty的坑的
- ⑨Mysql POST注入
- NAS和CIFS区别
- macOS 中的 Rootless 机制
- 设计模式六大原则——迪米特法则(LoD,Law of Demeter)
- 《JavaScript 闯关记》之 DOM(上)
- 从源码的角度分析Hashtable和HashMap的区别
- J - 再贪一心
- 非压缩BCD码转压缩BCD码汇编语言