我是怎么在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) }        }    }  }


可以看到,对每一个配置的filter,spark都会初始化一个FilterHolder去保存filter对应的类以及配置信息,然后对每一个handler调用其addFilter方法加到hanlder的处理逻辑中。

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

0 0
原创粉丝点击