Presto記憶體調優及原理(基礎篇)

IT技術精選文摘發表於2018-07-15

640?wx_fmt=gif

Presto是一個開源的分散式SQL查詢引擎,適用於互動式分析查詢,資料量支援GB到PB位元組。Presto支援線上資料查詢,包括Hive, Cassandra, 關聯式資料庫以及專有資料儲存。 一條Presto查詢可以將多個資料來源的資料進行合併,可以跨越整個組織進行分析。Presto以分析師的需求作為目標,他們期望響應時間小於1秒到幾分鐘。 Presto終結了資料分析的兩難選擇,要麼使用速度快的昂貴的商業方案,要麼使用消耗大量硬體的慢速的“免費”方案。

presto0.131開始對記憶體模型進行優化,直至當前EMRv2中上線版本0.188(包括EMRV1的0.161)都是使用的這個記憶體模型。使用的是一種稱為記憶體池(memory-pool)的機制來管理presto中任務及presto本身的記憶體使用。

目標和設計思路可檢視:https://github.com/prestodb/presto/issues/2624

當前presto已經實現了其設定的目標:

  1. 更容易的讓使用者控制每個查詢記憶體限制。

  2. 預防記憶體溢位及由此引發的崩潰。

  3. 充分利用叢集記憶體,不被一個“大查詢”引起整個叢集記憶體資源的限制。

Presto記憶體調優引數

presto把每個worker節點可分配記憶體(jvm Xmx)分成三份,分別是系統記憶體池(SystemMemoryPool),保留記憶體池(ReservedMemoryPool)和普通記憶體池(GeneralMemoryPool)。在Presto啟動時,它們會隨著worker節點初始化時被分配,然後通過服務發現各個worker節點上報給coordinator節點。

下圖是presto worker節點的記憶體示意圖:


640?wx_fmt=png

worker   節點記憶體分佈示意圖

從示意圖中可以看到,一個worker節點的記憶體堆大小可以最大分成兩份:系統預留記憶體+查詢記憶體。而查詢記憶體又分為最大查詢記憶體+其他查詢記憶體。

系統預留記憶體:worker節點初始化和執行任務必要的記憶體,包括preto發現服務的定時上報、每個query中task管理資料結構等。使用resources.reserved-system-memory配置項配置,預設是worker節點堆大小的0.4。

除了系統預留記憶體,其餘給woker 記憶體都會給查詢使用:

最大查詢記憶體:coordinator節點會定時排程檢視每個query使用的時長和記憶體,在此過程中會找到耗用記憶體最大的一個query,並會為此query排程最大的記憶體使用。這個query可獲得各個worker節點最大配置的專用最大記憶體量。使用query.max-memory-per-node配置項可以配置,預設是worker節點堆大小的0.1。這個值可根據query監控的peak Mem作為參考設定。

其他查詢記憶體:worker節點堆中除了系統預留的記憶體和最大查詢的記憶體就是其他查詢記憶體。

worker節點的堆記憶體的配置跟使用者使用兩個場景關係最大:

1.使用者查詢資料量/複雜性

2.使用者查詢併發度

1.決定了改用多大的最大查詢記憶體 2.決定了該用多大jvm堆。

舉個簡單的例子,如果有一批(n=5)查詢同時提交有10個worker節點的presto叢集,其中資料量/複雜度最高的一個query語句在一個節點中要佔用20GB的記憶體,那麼我們應該給每個worker節點最大查詢記憶體即是20GB/10 = 2GB,而需要併發執行,那麼留給查詢記憶體的應該是2GB*5大約為10GB,此時可以推出應該給每個worker節點配置堆大小為10/0.6 =17。(預留暫用0.4,留給查詢的為0.6)。

除了上述的三個配置,還有一個配置需要關注,即查詢最大可支援記憶體:表示每個query最大可支援記憶體。配置項為:query.max-memory這個值可配置為 query.max-memory-per-node*worker數目,以上個例子來說就是2GB*10=20GB。

用這個幾個引數就能基本解決在使用presto叢集時碰到的大部分查詢慢和OOM問題。當然,需要對一個presto叢集做更多精細化記憶體管理:比如針對到使用者的記憶體排程,比如使用排隊限制進入叢集而限制整個叢集query使用記憶體的限額,比如coordinator的記憶體精細管理。可以檢視下一篇文章presto調優中級篇。

Presto記憶體調優原理

看完上一部分可以直觀的在emr配置下發控制檯操作實踐起來了,對於想了解其中原理和排查更深層原因可以繼續往下看(開始從原始碼角度講原理,因為原始碼才能瞭解一切細節):

presto把每個worker節點可分配記憶體(jvm Xmx)分成三份,分別是系統記憶體池(SystemMemoryPool),保留記憶體池(ReservedMemoryPool)和普通記憶體池(GeneralMemoryPool)。在Presto啟動時,它們會隨著worker節點初始化時被分配,然後通過服務發現各個worker節點上報給coordinator節點。具體初始化是通過構造LocalMemoryManager類時完成的:

public final class LocalMemoryManager{
    public static final MemoryPoolId GENERAL_POOL = new MemoryPoolId("general");
    public static final MemoryPoolId RESERVED_POOL = new MemoryPoolId("reserved");
    public static final MemoryPoolId SYSTEM_POOL = new MemoryPoolId("system");

    private final DataSize maxMemory;
    private final Map<MemoryPoolId, MemoryPool> pools;

    @Inject    public LocalMemoryManager(NodeMemoryConfig config, ReservedSystemMemoryConfig systemMemoryConfig)
    {
        requireNonNull(config, "config is null");
        requireNonNull(systemMemoryConfig, "systemMemoryConfig is null");
        long maxHeap = Runtime.getRuntime().maxMemory();
        checkArgument(systemMemoryConfig.getReservedSystemMemory().toBytes() < maxHeap, "Reserved memory %s is greater than available heap %s", systemMemoryConfig.getReservedSystemMemory(), new DataSize(maxHeap, BYTE));
        maxMemory = new DataSize(maxHeap - systemMemoryConfig.getReservedSystemMemory().toBytes(), BYTE);

        ImmutableMap.Builder<MemoryPoolId, MemoryPool> builder = ImmutableMap.builder();
        checkArgument(config.getMaxQueryMemoryPerNode().toBytes() <= maxMemory.toBytes(), format("%s set to %s, but only %s of useable heap available", QUERY_MAX_MEMORY_PER_NODE_CONFIG, config.getMaxQueryMemoryPerNode(), maxMemory));
        builder.put(RESERVED_POOL, new MemoryPool(RESERVED_POOL, config.getMaxQueryMemoryPerNode()));
        DataSize generalPoolSize = new DataSize(Math.max(0, maxMemory.toBytes() - config.getMaxQueryMemoryPerNode().toBytes()), BYTE);
        builder.put(GENERAL_POOL, new MemoryPool(GENERAL_POOL, generalPoolSize));
        builder.put(SYSTEM_POOL, new MemoryPool(SYSTEM_POOL, systemMemoryConfig.getReservedSystemMemory()));
        this.pools = builder.build();
    }

reservatedMemoryPool的大小根據config.properties檔案配置query.max-memory-per-node決定。這個值的預設值是jvm(xmX)的0.1。

public class NodeMemoryConfig{
    public static final String QUERY_MAX_MEMORY_PER_NODE_CONFIG = "query.max-memory-per-node";

    private DataSize maxQueryMemoryPerNode = new DataSize(Runtime.getRuntime().maxMemory() * 0.1, BYTE);

    @NotNull    public DataSize getMaxQueryMemoryPerNode()
    {
        return maxQueryMemoryPerNode;
    }

    @Config(QUERY_MAX_MEMORY_PER_NODE_CONFIG)
    public NodeMemoryConfig setMaxQueryMemoryPerNode(DataSize maxQueryMemoryPerNode)
    {
        this.maxQueryMemoryPerNode = maxQueryMemoryPerNode;
        return this;
    }}

systemMemoryPool大小則根據配置resources.reserved-system-memory決定,這個值的預設值是jvm(xmX)的0.4。

public class ReservedSystemMemoryConfig{
    private DataSize reservedSystemMemory = new DataSize(Runtime.getRuntime().maxMemory() * 0.4, BYTE);

    @NotNull    public DataSize getReservedSystemMemory()
    {
        return reservedSystemMemory;
    }

    @Config("resources.reserved-system-memory")
    public ReservedSystemMemoryConfig setReservedSystemMemory(DataSize reservedSystemMemory)
    {
        this.reservedSystemMemory = reservedSystemMemory;
        return this;
    }}

最大記憶體排程策略

Presto一條query語句巨集觀上來說是把它轉換成基於stage的邏輯執行計劃。然後再通過邏輯執行計劃轉成物理執行計劃在worker節點間分成並行的task,每個task內又轉換成具體的Operator實際執行。在真正執行物理計劃前,記憶體需求都來自於systemMemoryPool,包括臨時資料結構,傳輸buffer等。執行物理計劃時,不同的Operator型別都根據需要申請記憶體,比如aggregationOperator使用getEsctimatedSize()方法預估需要的記憶體。這裡獲取的記憶體來自於reservatedMemoryPool或者generalMemoryPool,究竟使用哪個pool取決於當前查詢是否耗用記憶體最大。

在查詢建立時,預設使用generalMemoryPool:

public SqlTaskManager(
            LocalExecutionPlanner planner,
            LocationFactory locationFactory,
            TaskExecutor taskExecutor,
            QueryMonitor queryMonitor,
            NodeInfo nodeInfo,
            LocalMemoryManager localMemoryManager,
            TaskManagementExecutor taskManagementExecutor,
            TaskManagerConfig config,
            NodeMemoryConfig nodeMemoryConfig,
            LocalSpillManager localSpillManager,
            NodeSpillConfig nodeSpillConfig)
    {     ...      

  queryContexts = CacheBuilder.newBuilder().weakValues().build(CacheLoader.from(

                queryId -> new QueryContext(
                        queryId,
                        maxQueryMemoryPerNode,
                        localMemoryManager.getPool(LocalMemoryManager.GENERAL_POOL),
                        localMemoryManager.getPool(LocalMemoryManager.SYSTEM_POOL),
                        taskNotificationExecutor,
                        driverYieldExecutor,
                        maxQuerySpillPerNode,
                        localSpillManager.getSpillSpaceTracker())));

presto會定時檢查所有查詢消耗的記憶體,這個定時器在presto初始化時被構造,實現如下:

queryManagementExecutor.scheduleWithFixedDelay(() -> {
    ...
    enforceMemoryLimits();
    ...}, 1, 1, TimeUnit.SECONDS);

其中的updateAssignments方法,其作用是找出最耗費記憶體的查詢,並放入RESERVED_POOL: 

if (reservedPool.getAssignedQueries() == 0 && generalPool.getBlockedNodes() > 0) {
    QueryExecution biggestQuery = null;
    long maxMemory = -1;
    for (QueryExecution queryExecution : queries) {

        if (resourceOvercommit(queryExecution.getSession())) {     
            continue;
        }

        long bytesUsed = queryExecution.getTotalMemoryReservation();

        if (bytesUsed > maxMemory) {
            biggestQuery = queryExecution;
            maxMemory = bytesUsed;
        }
    }

    if (biggestQuery != null) {
        biggestQuery.setMemoryPool(new VersionedMemoryPoolId(RESERVED_POOL, version));
    }}

再看外層的updateNodes方法,可以發現RESERVED POOL的這種分配策略會應用到每個節點。也就是說這個查詢在每個節點都會獨佔RESERVED POOL的空間。

for (RemoteNodeMemory node : nodes.values()) {
    node.asyncRefresh(assignments);}

Presto記憶體監控

可以通過presto提供的WebUi來即時追蹤整個叢集或者每個查詢執行的記憶體的使用情況。presto的WebUI後臺使用的是DI框架Guice,前臺是jquery+react實現。


640?wx_fmt=png

presto 叢集overview

640?wx_fmt=png

presto 某個query監控圖

web頁面中reservedMemmory即是ReservedMemoryPool的大小。而在查詢資源彙總中,peakMemory表示當前查詢使用memory峰值。memory pool表示當前使用的pool(reserved或者genaral)。

公眾號推薦:

640?wx_fmt=jpeg

640?wx_fmt=jpeg


相關文章