我是怎麼在Spark中踩到Jetty的坑的

前言

大家知道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() {n super.init();n CommonUtils.assertNotNull(this.proxyGrantingTicketStorage, "proxyGrantingTicketStorage cannot be null.");nn if (this.timer == null) {n this.timer = new Timer(true);n }nn if (this.timerTask == null) {n this.timerTask = new CleanUpTimerTask(this.proxyGrantingTicketStorage);n }n this.timer.schedule(this.timerTask, this.millisBetweenCleanUps, this.millisBetweenCleanUps);n }n

可以看到,它的init方法中會觸發一個定時任務,而顯然我們這個地方報錯的原因是多次觸發了這個定時任務,即多次調用了這個類的init方法。

為什麼會多次調用一個filter的init方法呢?從Spark HistoryServer的代碼入口:

/** Add filters, if any, to the given list of ServletContextHandlers */n def addFilters(handlers: Seq[ServletContextHandler], conf: SparkConf) {n val filters: Array[String] = conf.get("spark.ui.filters", "").split(,).map(_.trim())n filters.foreach {n case filter : String =>n if (!filter.isEmpty) {n logInfo("Adding filter: " + filter)n val holder : FilterHolder = new FilterHolder()n holder.setClassName(filter)n // Get any parameters for each filtern conf.get("spark." + filter + ".params", "").split(,).map(_.trim()).toSet.foreach {n param: String =>n if (!param.isEmpty) {n val parts = param.split("=")n if (parts.length == 2) holder.setInitParameter(parts(0), parts(1))n }n }nn val prefix = s"spark.$filter.param."n conf.getAlln .filter { case (k, v) => k.length() > prefix.length() && k.startsWith(prefix) }n .foreach { case (k, v) => holder.setInitParameter(k.substring(prefix.length()), v) }nn val enumDispatcher = java.util.EnumSet.of(DispatcherType.ASYNC, DispatcherType.ERROR,n DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST)n handlers.foreach { case(handler) => handler.addFilter(holder, "/*", enumDispatcher) }n }n }n }n

可以看到,對每一個配置的filter,spark都會初始化一個FilterHolder去保存filter對應的類以及配置信息,然後對每一個handler調用其addFilter方法加到hanlder的處理邏輯中。

addFilter發生了什麼呢?通過debug,調用棧如下:

ServerContextHandler.addFilter -> ServletHanlder.addFilterWithMapping -> addFilterMapping -> setFilterMappings -> updateMappings -> initialize

好,此處initialize方法中會把這個handler里所有的FilterHolder都拿出來進行初始化,即:

for (FilterHolder f: _filters)n {n tryn {n f.start();n f.initialize();n }n catch (Exception e)n {n mx.add(e);n }n }n

這個初始化過程中會發生什麼呢?再往下看:

@Overriden public void initialize() throws Exceptionn {n super.initialize();n n if (_filter==null)n {n tryn {n ServletContext context=_servletHandler.getServletContext();n _filter=(context instanceof ServletContextHandler.Context)n ?((ServletContextHandler.Context)context).createFilter(getHeldClass())n :getHeldClass().newInstance();n }n catch (ServletException se)n {n Throwable cause = se.getRootCause();n if (cause instanceof InstantiationException)n throw (InstantiationException)cause;n if (cause instanceof IllegalAccessException)n throw (IllegalAccessException)cause;n throw se;n }n }nn _config=new Config();n if (LOG.isDebugEnabled())n LOG.debug("Filter.init {}",_filter);n _filter.init(_config);n }n

原來是實例化其封裝的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 nown if (_filters != null)n {n for (FilterHolder f: _filters)n {n tryn {n f.start();n f.initialize();n }n catch (Exception e)n {n mx.add(e);n }n }n }n

如果你還有印象的話,這段代碼是可以通過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:github.com/eclipse/jett

當然,Jetty不愧是apache的top項目,解決起來也是很快的:github.com/eclipse/jett

這個fix要到Jetty 9.3.x才能修復,咱們現在用的9.2.x。咱們總不能等到他們release咱們才做升級吧?先規避過去再說,怎麼規避的呢?這裡就不透露了,留給諸位去思考吧 :)

聲明:本文為原創,首發於CSDN,版權歸本人所有,禁止用於任何商業目的,轉載請註明出處:blog.csdn.net/asongofic


推薦閱讀:

攜程:請宰你的會員的時候,更高科技一點
抓住數據的小尾巴 - JS浮點數陷阱及解法
圖說財經 | 上升為國家戰略的大數據是什麼呢?
調試 Web Hadoop Distributed File System
物聯網、雲計算、大數據、人工智慧怎麼區分,又有何關係?

TAG:Spark | 大数据 | Jetty |