1. 程式人生 > >Impala原始碼之資源管理與資源隔離

Impala原始碼之資源管理與資源隔離

前言

Impala是一個MPP架構的查詢系統,為了做到平臺化服務,首先需要考慮就是如何做到資源隔離,多個產品之間儘可能小的甚至毫無影響。對於這種需求,最好的隔離方案無疑是物理機器上的隔離,A產品使用這幾臺機器,B產品使用那幾臺機器,然後前端根據產品路由到不同叢集,這樣可以做到理想中的資源隔離,但是這樣極大的增加了部署、運維等難度,而且無法實現資源的共享,即使A產品沒有任務在跑,B產品也不能使用A產品的資源,這無疑是一種浪費。毛主席教導我們浪費是可恥的,所以我們要想辦法在充分利用資源的情況下實現產品之間的資源隔離,這其實是一個非常有難度的工作。

YARN

在大資料生態圈,談到資源管理(Resource Management)和隔離(Resource Isolation),第一反應想到的肯定是YARN,它是自Hadoop2.0開始並且一直使用的一種資源管理系統, YARN主要通過集中式的資源管理服務Resource Manager管理系統中全部的資源(主要是CPU和記憶體),然後為每一個產品或者業務定義一個佇列,該佇列中定義了提交到該佇列下的任務最大申請的資源總量;當一個任務提交到Resource Manager之後,它會啟動一個ApplicationMaster來負責該任務的資源申請和排程,然後根據任務需要的資源需求,向Resource Manager申請資源,Resource Manager根據當前佇列中資源剩餘情況判斷是否授予資源,如果當前佇列資源已經被用盡則該任務需要等待直到有資源釋放,等到ApplicationMaster申請到資源之後則請求NodeManager啟動包含一定資源的Container,Container利用cgroups輕量級的隔離方案實現,為了根據不同的使用場景YARN也集成了不同的分配和排程策略,典型的有Capacity Scheduler和Fair Scheduler。

這裡寫圖片描述

上圖展示了客戶端提交任務到YARN的流程,平時在提交MR、spark任務時也是通過這種方式,但是對於MPP架構的系統,它的查詢響應時間由最慢的節點執行時間決定,而為了提升查詢效能,又需要儘可能多的節點參與計算,而YARN上的任務每次都是啟動一個新的程序,啟動程序的時間對於批處理任務是可以接受的,畢竟這種任務執行時間比較久,而對於追求低延遲的Ad-hoc查詢而言代價有點大了,很可能出現程序啟動+初始化時間大於真正執行的時間。

除了使用原生的yarn排程,impala也嘗試過使用一個稱之為Llama(Long-Lived Application Master)的服務實現資源的管理和排程,它其實是YARN上的一個ApplicationMaster,實現impala和yarn之間的協調,當一個impala接收到查詢之後,impala根據預估的資源需求向Llama請求資源,後者向YARN的Resource Manager服務申請可用的資源。但是前面提到了,impala為了保證查詢速度需要所有的資源同時獲得,這樣才能推進下一步任務的執行,實際上,Llama實現了這樣的批量申請的功能,所以一個查詢的進行需要等到一批資源同時到達的時候才能夠進行下去,除此之外,Llama還會快取申請到的資源。但是Llama畢竟還是需要從YARN申請資源並且啟動程序,還是會存在延遲比較大的問題,因此Impala也在2.3版本之後不再支援Llama了。

Impala資源隔離

目前Impala的部署方式仍然是啟動一個長時間執行的程序,對於每一個查詢分配資源,而在新版本(2.6.0以後),加入了一個稱為Admission Control的功能,該功能可以實現一定意義上的資源隔離,下面我們就深入瞭解一下這個機制,看一下它是如何對於資源進行隔離和控制的。

首先,如果根據impala的架構,所有的SQL查詢,從解析、執行計劃生成到執行都是在impalad節點上執行的,為了實現Admission Control,需要在impalad配置如下了兩個引數:

  • –fair_scheduler_allocation_path 該引數是用來指定fair-scheduler.xml配置檔案路徑,該檔案類似於YARN的fair-scheduler.xml配置,具體配置內容下面再詳細講述。
  • –llama_site_path 該引數用來指定Llama的配置檔案llama-site.xml,上面不是說到新版本不用Llama了嗎?為什麼還要配置它呢,其實這裡面的配置項都是一些歷史遺留項了吧。

接下來就詳細介紹一下如何配置這兩個檔案,對於第一個檔案fair-scheduler.xml,熟悉YARN的都知道該檔案實現公平排程器的配置,YARN中的公平排程是如何實現的我不懂,但是檔案中基本上需要配置一下每一個佇列的資源分配情況,下面是一個配置例項:

  <queue name="sample_queue">
      <minResources>10000 mb,0vcores</minResources>
      <maxResources>90000 mb,0vcores</maxResources>
      <maxRunningApps>50</maxRunningApps>
      <weight>2.0</weight>
      <schedulingPolicy>fair</schedulingPolicy>
      <aclSubmitApps>charlie</aclSubmitApps>
  </queue>

但是通過impala原始碼發現impala中用到的每一個佇列的配置只有aclSubmitApps和maxResources,前者用於確定該佇列可以由哪些使用者提交任務,如果使用者沒有該佇列的提交許可權(佇列中沒設定),或者使用者沒指定佇列則提交到default佇列,如果default佇列不存在或者使用者沒有提交到的default佇列的許可權,則拒絕該請求;後者是用於確定該佇列在整個叢集中使用的最大資源數,目前impala關注的資源只有memory。在上例中sample_queue佇列在整個叢集中能夠使用的記憶體大小是90GB,只有charlie使用者能夠提交到該佇列。

既然只用到這兩個配置,為什麼impala不單獨搞一個配置格式呢,而選擇直接用fair-schedular.xml呢?我想一方面是為了省去自己寫解析類了,直接使用yarn的介面就可以了,另外為以後更加完善做準備。下面再看一下Llama配置中用到了什麼配置,配置例項如下:

<property>
  <name>llama.am.throttling.maximum.placed.reservations.root.default</name>
  <value>10</value>
</property>
<property>
  <name>llama.am.throttling.maximum.queued.reservations.root.default</name>
  <value>50</value>
</property>
<property>
  <name>impala.admission-control.pool-default-query-options.root.default</name>
  <value>mem_limit=128m,query_timeout_s=20,max_io_buffers=10</value>
</property>
<property>
  <name>impala.admission-control.pool-queue-timeout-ms.root.default</name>
  <value>30000</value>
</property>

這些配置的意義如下,具體的配置項則是如下key後面再加上佇列名。

  • llama.am.throttling.maximum.placed.reservations:佇列中同時在跑的任務的最大個數,預設是不限制的。
  • llama.am.throttling.maximum.queued.reservations:佇列中阻塞的任務的最大個數,預設值是200
  • impala.admission-control.pool-default-query-options:佇列中阻塞的任務在阻塞佇列中最大的等待時間,預設值是60s
  • impala.admission-control.pool-queue-timeout-ms:佇列中預設的配置,這些配置使用key=value的方式配置並用逗號分隔,這些配置都會作為向該佇列中提交查詢的預設配置(在查詢執行的時候替換),可以通過set命令覆蓋。

Impala實現

好了,分析完了Admission Control中使用的配置項,使用者可以在建立一個session之後通過set REQUEST_POOL=pool_name的方式設定改session的請求提交的佇列,當然如果該使用者沒有該佇列的提交許可權,之後執行都會失敗。下面根據查詢的流程看一下impala如何利用這些引數完成資源隔離的
當impala接收到一個查詢請求之後,請求除了包含查詢SQL之外,還包括一批查詢引數,這裡我們關心的是該請求提交的佇列(REQUEST_POOL引數),它首先根據查詢執行的使用者和佇列引數獲得該查詢應該提交到的佇列,選取佇列的規則如下:

  • 1、如果服務端沒有配置fair-scheduler.xml和llama-site.xml,說明沒有啟動資源控制服務,則所有的請求都提交到一個名為default-pool的預設佇列中。
  • 2、如果該查詢沒有指定REQUEST_POOL,則將REQUEST_POOL設定為yarn預設佇列default.
  • 3、判斷佇列名是否存在,然後再根據當前提交任務的使用者和佇列名判斷該使用者是否具有提交任務到佇列的許可權。如果佇列名不存在或者該使用者無許可權提交則查詢失敗。

查詢執行完初始化工作(選擇佇列只是其中的一部分工作)之後會呼叫FE的GetExecRequest介面進行執行計劃的生成,生成執行計劃的流程大致分為三部:1、語法分析生成邏輯執行計劃並進行預處理;2、根據邏輯執行計劃生成單擊執行計劃;3、將單機執行計劃轉換成物理自行計劃,後續再單獨介紹這部分。接著impalad節點會根據執行計劃判斷該查詢是否可以繼續執行,只有出現如下幾種情況時,查詢需要排隊:

  • 當前佇列中已經有查詢在排隊,因為阻塞佇列是FIFO排程的,所以新來的查詢需要直接排隊。
  • 已經達到該佇列設定的併發查詢上限。
  • 當前查詢需要的記憶體不能夠得到滿足。

前兩種條件比較容易判斷,對於第三種情況,impala需要知道當前查詢需要的記憶體和當前佇列中剩餘的記憶體情況進行判斷,這裡的記憶體使用分為兩個方面:叢集中佇列剩餘的總記憶體和單機剩餘記憶體。首先判斷佇列中剩餘記憶體和當前查詢需要在叢集中使用的記憶體是否達到了佇列設定的記憶體上限;然後在判斷該查詢在每一個impalad節點上需要的記憶體和該節點剩餘記憶體是否達到設定的記憶體上限(節點的記憶體上限是由引數mem_limit設定的)。那麼問題又來了,該查詢在整個叢集需要多少記憶體,在每一個節點上需要多少記憶體是如何計算的呢?對於整個叢集上需要記憶體其實就是每一個節點需要的記憶體乘以需要的節點數,那麼核心問題就是該查詢需要在每一個節點使用的記憶體大小。

可能大家和我一樣覺得,對於每一個查詢需要在每一個節點消耗的記憶體是根據查詢計劃預估出來的,但是這樣做是非常難的,那麼來看一下當前impala是如何做的,對於單節點記憶體的預估,按照如下的優先順序計算查詢需要的單機記憶體:

  • 首先判斷查詢引數rm_initial_mem是否被設定(可以通過set rm_initial_mem =xxx設定),如果設定了則直接使用該值作為預估值
  • 然後判斷impalad啟動的時候是否設定rm_always_use_defaults=true,如果設定了則使用rm_default_memory中配置的記憶體大小
  • 接著再判斷該session是否設定了mem_limit(可以通過set mem_limit=xx設定,注意它和impalad啟動時的mem_limit配置的區別),如果設定則使用該值,
  • 最後根據判斷執行計劃中是否計算出了每一個節點需要分配的記憶體大小;
  • 如果以上都沒有命中則使用預設的rm_default_memory配置(impalad啟動時候的引數),該值預設值是“4GB”。

從上面的的判斷邏輯來看,impalad最後才會根據執行計劃中預估的值確定每一個節點分配的記憶體大小,畢竟只是根據統計資訊預估出來的資訊並不是準確的,對於一些複雜的查詢而言,可能是誤差非常大的。

好了,通過上面的分析,整個查詢審計流程梳理了一遍,但是如果當前資源不能夠滿足該查詢呢?此時就需要將該查詢放入佇列中,該查詢則會阻塞直到滿足如下兩種條件之一:

  • 該查詢所使用的佇列擁有了該查詢需要的足夠的資源
  • 該查詢存放在佇列中並超時,超時時間是由佇列中的queue_timeout_ms引數設定,如果佇列沒設定則由impalad啟動時的queue_wait_timeout_ms引數決定,預設是60s

impalad在啟動的時候會啟動一個“admission-thread”執行緒,該執行緒啟動時阻塞在一個條件變數上,該條件變數在每一個查詢完成或者當前impalad節點獲取到其他節點的資源資訊之後notify,然後迴圈檢查每一個佇列中是否有阻塞的查詢,如果有則判斷當前能夠滿足該查詢的資源需求,如果可以則將其喚醒。

又回到Statestored

我們這裡就不深究執行計劃中的記憶體估計是如何計算的了,但是還有一個比較重要的問題:每一個impalad是獨立工作的,只有在需要分配任務的時候才會通知其餘的impalad執行相應的operation,那麼impalad如何知道其他impalad節點的資源狀態的?包括每一個佇列已使用的記憶體大小,每一個節點已使用的記憶體大小等。這就靠我們上一篇文章中介紹的statestored了,在statestored中impalad啟動時會註冊一個impala-request-queue主題,每一個impalad都是該topic的釋出者同時也是訂閱者,週期性的釋出當前節點的記憶體使用情況,然後每一個impalad節點再根據topic中最新的資訊更新整個叢集的資源使用狀態。這種互動方式的確挺方便,但是可能存在一定的不確定性,例如某一次impalad與statestored的網路抖動都有可能導致無法獲取到最新的資源使用狀態。

硬性限制

impalad使用Admission Control實現了一定意義上的資源隔離,但是這畢竟是軟性的,而不是像YARN那種通過cgroup啟動新程序來進行隔離,仍然有可能存在一個比較繁重的查詢將整個叢集搞垮,對於這種情況作為查詢平臺我們需要做到即使返回錯誤也不能影響這個整個叢集的服務,此時就需要祭出impala查詢的一個關鍵性引數:mem_limit,對於impalad中的每一個模組,啟動時都可以設定mem_limit引數,該引數是每一個執行節點能夠分配的最大記憶體(tcmalloc管理),而每一個查詢也可以設定mem_limit,它表示該查詢在每一個節點上最大分配的記憶體大小,再每一個impalad執行查詢(impala中稱之為fragment)的時候會分配一個大小為mem_limit的block pool,需要的記憶體從pool中分配並儲存在記憶體中,如果分配的記憶體超出pool的大小,則選擇一定的block spill到外存中,這部分具體的執行流程也是非常複雜了,可以單獨再講,這部分block在需要的時候再從本地磁碟讀取到記憶體中,需要spill到外存的查詢無疑會拖慢查詢速度,但是這很好的儲存了整個系統的可用性和穩定性。

總結

在實際部署的時候,我們會根據每一個使用者的資料量大小、業務型別分配佇列,甚至相同的業務不同時間區間的查詢分配不同的佇列,並且詳細設定該佇列中的預設查詢引數,尤其是mem_limit引數和最大併發數,這樣可以較好的限制租戶之間的影響,為了避免惡意用於的使用,可以限制使用者自己設定MEM_LIMIT引數,儘可能得保證叢集的穩定性。