函式計算|如何使用層解決依賴包問題?

Serverless發表於2023-02-02

作者:落泥

在使用阿里雲函式計算平臺時,如果您曾經遇到過以下問題,本文應該會對您有所幫助:

  1. 第三方依賴包太大,每次更新程式碼都非常耗時,甚至會出現超過程式碼包限制的情況,我該怎麼辦?
  2. 安裝第三方依賴包後,可以在本地執行成功,上傳到阿里雲函式計算平臺上就會報錯,這是什麼情況?
  3. 有很多常用的依賴包,很多使用者應該都會用到,阿里雲函式計算官方不能直接內建到執行時環境中麼?
  4. 我在多個函式中有相同的依賴包,我該如何管理這些相同的依賴包?

提供了集中管理,跨多個功能且能夠共享程式碼和資料的方法。

2021 年 1 月,阿里雲函式計算釋出了 “自定義層”功能,讓使用者可以自定義層,並支援跨函式共享。2022 年 8 月,阿里雲函式計算釋出“公共層”功能,提供了官方公共層,供使用者直接使用,進一步提升了使用者體驗。

接下來先介紹“自定義層”的功能和作用。

自定義層

在層功能釋出之前,必須將程式碼與程式碼的依賴項一起打包和部署,這些依賴項在不同函式中可能是相同的,很多情況下這些依賴項的大小,遠遠大於程式碼的大小。

在層功能釋出後,我們可以將程式碼的依賴項,或者多個函式中共享的部分打包成 Zip 壓縮檔案,並作為函式計算的自定義層釋出,不同函式都可以使用該自定義層。阿里雲函式計算會在呼叫時將層與函式程式碼一起載入。可參考文末文件-建立自定義層 [ 1] 、在函式中配置自定義層 [ 2]

為什麼使用自定義層?

使用自定義層有以下優勢:

  • 跨函式複用程式碼​

將多個函式中的通用程式碼或資料提取出來,打包成 Zip 包,做成自定義層,供不同函式引用,避免了在多個地方維護通用的程式碼或資料。

與此同時,也實現了依賴項和業務邏輯的分離,使用者可以專注於核心的業務邏輯。

  • 使程式碼包更小

函式的程式碼包越來越大時,部署速度也會越來越慢,導致函式的維護和測試愈加困難。

此外函式程式碼包大小也有限制,比如阿里雲函式計算的程式碼包限制為 500MB (2022 年 9 月),層是突破該限制的方法之一。層也有大小限制,目前單個層的程式碼包大小限制為 500MB,單個函式最多可配置5個層,總大小不能超過 2GB。

  • 加速程式碼部署,簡化函式管理

函式程式碼包越小,程式碼包的部署就越快。尤其是一些大型依賴項時,核心功能程式碼可能只有幾兆位元組,但依賴項可能有幾百兆。比如 Puppeteer 依賴包超過 100MB,阿里雲的 DataX 依賴包超過 800MB。

一般來講,這些依賴項很少修改,因此將他們打包成層後,可以避免在核心程式碼修改時頻繁修改這些大型依賴項。對這些依賴項也可以拆分成多個層,每次修改一個功能時,只需要更新其中一個層。比如我們實現了自定義執行時 Python3.10 以及該執行時相容的科學計算庫 SciPy,可以將自定義執行時和依賴包拆成兩個層,當需要更新依賴包時,只需要更新依賴包的層,而自定義執行時的層保持不變。

自定義層的困境

  • 製作層有一定門檻

層的 Zip 包有一定的格式規範,使用者需要按照該規範進行製作。以 Python 的 requests 庫為例,依賴打包後的檔案結構為:

my-layer-code.zip
└── python
    └── requests

為什麼有這種要求呢?這個涉及到不同執行時在搜尋第三方依賴包的實現邏輯,以 Python 為例,Python 執行時會在 sys.path 路徑下搜尋依賴包,上面的 Zip 包會解壓到函式例項的 /opt 目錄下,解壓後 requests 這個包就放到了 /opt/python 目錄下。

然後,函式計算平臺會將一些特定的目錄放到執行時語言的依賴搜尋路徑上,比如 Python 執行時就會將 /opt/python 放到 sys.path 中,這樣,程式碼中就可以直接引用 requests 庫了。其他執行時的使用方法可參考文末文件-建立自定義層。

當然,你也可以不按照這個格式規範來製作層,此時就需要在程式碼中新增對應的搜尋路徑了,具體方法可參考文末文件-如何在 Custom Runtime 中引用層中的依賴? [ 3]

需要在指定作業系統和處理器架構下製作層。有一些依賴是與作業系統和處理器架構有依賴關係的,比如 Python 的科學計算庫 NumPy,假如你在 M1 晶片的 MacOS 下安裝,其版本為:

numpy-1.23.3-cp39-cp39-macosx_11_0_arm64

可看到相容的作業系統為 mac os, 處理器架構為 arm64。但在函式計算平臺的例項環境為 Linux x86_64,作業系統目前使用的發行版為 Debian 9,因此在 M1 Mac 下安裝的 NumPy 庫不能在阿里雲函式計算平臺使用。

我們推薦在 Debian 9 系統下進行安裝,但使用者本地可能沒有該環境,您可以使用線上構建依賴庫或者使用函式計算官方執行時映象來構建,此處不再贅述。

層需要包含新增的共享動態庫。有些依賴庫需要安裝額外的共享動態庫,在構建層的 Zip 包時也需要包含這些共享動態庫。例如 Nodejs 的依賴庫 Puppeteer,需要額外安裝二十多個共享動態庫(如 libxss1,libnspr4 等),這些依賴庫都要打包到層 Zip 包中。如何成功的安裝 Puppeteer 庫並不是簡單的事情。

共享動態庫推薦放到 Zip 包的 lib 目錄下,函式計算平臺會將/opt/lib 目錄新增到 LD_LIBRARY_PATH(僅限於內建執行時)。

  • 無法跨賬號共享

自定義層預設只能在同賬號同地域的不同函式之間共享,無法進行跨賬號共享。因此,使用者 A 建立的自定義層無法給使用者 B 使用,這不僅給使用者帶來了重複的工作量,也不利於宿主機上相同層的複用。

公共層**

由於自定義層的這些痛點,阿里雲函式計算在 2022 年 8 月釋出了公共層功能。實現層跨賬號共享,並提供了一些官方公共層 [ 6] 供使用者直接使用,方便使用者快速開發示例原型。阿里雲函式計算平臺主要提供了三類官方公共層:

  • 自定義執行時(如 Python 3.10、Nodejs17、PHP 8.1、Java17、.NET 6 等)
  • 常用依賴庫 (如 PyTorch、Scipy、Puppeteer 等)
  • 阿里雲 SDK (如 Aliyun DataX ) 

詳情可參考文末官方文件-在函式中配置官方公共層 [ 4] ,目前官方公共層仍在持續補充,如果您有需要的執行時或者依賴庫想透過官方公共層的方式使用,可透過釘釘答疑群(釘釘群號:11721331)與我們聯絡,也可以直接在 Github [ 5] 提交 issue。

如何公開自定義層?

目前,層公開功能在內測中,如有需求歡迎透過釘釘答疑群(釘釘群號:11721331)聯絡我們。

同時,我們也非常歡迎大家貢獻公共層到倉庫,我們很快會在該倉庫提供公共層貢獻的方法和示例。

示例展示

官方公共層的最新版本和使用說明可參考 Github。下面我們介紹一些使用官方公共層的典型示例。

示例一、基於 Nodejs16 + Puppeteer 實現網頁截圖示例程式

Puppeteer 是一個 Nodejs 庫,它提供了高階的 API 並透過 DevTools 協議來控制 Chrome(或 Chromium)。通俗來說就是一個 headless chrome 瀏覽器,可以使用它完成很多自動化的事情,比如:

  • 生成網頁截圖或者 PDF
  • 做表單的自動提交、UI 的自動化測試、模擬鍵盤輸入等
  • more... 

本示例使用 Puppeteer 完成一個網頁截圖示例程式。

首先,我們使用內建執行時 Nodejs16 建立一個函式 start-puppeteer,其中請求處理程式型別選擇“處理 HTTP 請求”。

 title=

然後,在高階配置中將記憶體規格設定為 1GB,示例程式的記憶體使用大概在 550MB 左右。

建立成功後,在控制檯上開啟 index.js 檔案,將下面的程式碼複製並覆蓋該檔案,點選部署按鈕。

const fs = require('fs');
const puppeteer = require('puppeteer');

function autoScroll(page) {
  return page.evaluate(() => {
      return new Promise((resolve, reject) => {
          var totalHeight = 0;
          var distance = 100;
          var timer = setInterval(() => {
              var scrollHeight = document.body.scrollHeight;
              window.scrollBy(0, distance);
              totalHeight += distance;
              if (totalHeight >= scrollHeight) {
                  clearInterval(timer);
                  resolve();
              }
          }, 100);
      })
  });
}

module.exports.handler = function (request, response, context) {
  console.log('Node version is: ' + process.version);
  (async () => {
    const browser = await puppeteer.launch({
      headless: true,
      args: [
        '--disable-gpu',
        '--disable-dev-shm-usage',
        '--disable-setuid-sandbox',
        '--no-first-run',
        '--no-zygote',
        '--no-sandbox'
      ]
    });

    let url = request.queries['url'];

    if (!url) {
      url = 'https://www.serverless-devs.com';
    }

    if (!url.startsWith('https://') && !url.startsWith('http://')) {
      url = 'http://' + url;
    }

    const page = await browser.newPage();

    await page.emulateTimezone('Asia/Shanghai');
    await page.goto(url, {
      'waitUntil': 'networkidle2'
    });
    await page.setViewport({
      width: 1200,
      height: 800
    });
    await autoScroll(page)

    let path = '/tmp/example';
    let contentType = 'image/png';
    await page.screenshot({ path: path, fullPage: true, type: 'png' });
    await browser.close();

    response.setStatusCode(200);
    response.setHeader('content-type', contentType);
    response.send(fs.readFileSync(path))
  })().catch(err => {
    response.setStatusCode(500);
    response.setHeader('content-type', 'text/plain');

    response.send(err.message);
  });
};

簡要介紹一下上述程式碼的核心邏輯,首先程式碼會解析 query 引數獲取需要截圖的 url 地址(如果解析失敗則預設使用 Serverless Devs 官網主頁),然後使用 Puppeteer 對該網頁進行截圖,並儲存到執行例項的 /tmp/example 檔案中,然後將該檔案作為 HTTP 請求的返回體直接返回。

然後,我們需要配置 Puppeteer 公共層,在函式配置中找到,點選編輯,選擇新增官方公共層

 title=

選擇官方公共層 Puppeteer17x,目前最新的層版本為 1。

 title=

參考官方公共層 Nodejs-Puppeteer17x README [ 6] 新增環境變數,對於版本 1,需要新增 LD_LIBRARY_PATH=/opt/lib/x86_64-linux-gnu:/opt/lib 環境變數。

 title=

最後,使用觸發器管理中的測試地址進行測試驗證。

 title=

測試結果如下所示,已成功將 Serverless Devs 官方進行截圖。

 title=

示例二、基於公共層快速實現 .NET 6 自定義執行時

首先,透過控制檯建立 .NET 6 自定義執行時。在最上層選擇 “使用自定義執行時建立”,選擇“處理 HTTP 請求”,選擇 .NET 6 執行時,其他配置使用預設值。

 title=

建立成功後,可以透過 WebIDE 看到示例程式碼 Program.cs

 title=

示例程式碼中需要注意四個部分:

  • 該示例監聽了 0.0.0.0 的 9000 埠,Custom Runtime 啟動的服務一定要監聽 0.0.0.0:CAPort 或*:CAPort 埠,不能監聽 127.0.0.1 或 localhost。詳情參考文件 Custom Runtime>基本原理 [ 7]
  • 新增路由 /,直接返回字串 "Hello World!"
  • 新增路由/invoke,該路由為使用事件請求處理程式的路徑,可參考文件 Custom Runtime >事件請求處理程式(Event Handler) [ 8]
  • 新增路由 /initialize,該路由為函式初始化回撥程式對應的路徑,該方法會在示例初始化時執行一次,可參考文件 Custom Runtime >函式例項生命週期回撥 [ 9]   

首先,我們直接使用觸發器管理頁面中的測試地址進行測試,此時不新增任何 PATH 資訊,結果如下圖所示:

 title=

然後,我們測試新增 /invoke 路徑進行測試,因為該路由方法為 POST,我們直接使用 curl -XPOST 測試:

 title=

同樣,我們用這種方法測試一下/initialize

注意:此處只是做測試,初始化回撥函式不需要主動呼叫,函式計算平臺會在例項啟動後自動呼叫該回撥方法(不要忘記在配置裡啟用 initializer 回撥程式)

 title=

最後,我們再做一個小測試,在觸發器管理頁面將 HTTP 觸發器刪除,刪除後該函式型別會轉換成事件請求處理程式,在函式配置中,將 Initializer 回撥程式啟用

 title=

在控制檯上測試該函式,結果如下圖所示:

 title=

點選實時日誌按鈕,可以看到在該請求執行前,已經執行了 Initialize 回撥方法。

 title=

層的最佳實踐是什麼?

前文介紹了什麼是自定義層,為什麼使用自定義層,什麼是公共層,並介紹了兩個官方公共層的示例。但我們對層的使用仍然還有一些疑惑,比如什麼場景下推薦使用層?層與程式碼包有什麼區別?有沒有與層相似的功能?與這些相似功能相比,層的優缺點是什麼?接下來嘗試回答一下這些問題。

什麼場景下推薦使用層?

目前,使用層的場景主要有兩類,一類是自定義執行時,另一類是各種語言的依賴庫。強烈推薦透過層來構建並使用自定義執行時,但對於各類語言的依賴庫,可以參考下面這些建議:

  • 推薦優先使用官方公共層
  • 非編譯型語言的依賴庫推薦使用層來管理,對編譯行語言需要根據實際情況進行判斷(比如,對自定義執行時,如果使用 JAR 包的方式執行 Java 程式,則無法引入層中的依賴,可參考文件-如何在 Custom Runtime中引用層中的依賴? [ 3]
  • 如果依賴庫較大,並且沒有超過層的限制大小,推薦使用層
  • 如果依賴庫需要額外安裝共享動態庫,推薦使用層(如果構建比較複雜,可聯絡函式計算團隊製作)
  • 如果在多個函式、多個賬號之間有共享程式碼或資料的需求,推薦使用層 

層與程式碼包有什麼區別?

直觀上看,層就是把原來程式碼包的一部分內容拆分出來,再重新建一個程式碼包而已,那為什麼又建立一個層的概念呢?這裡的主要區別是層與程式碼包的設計理念不同

  • 層有更簡潔的版本管理方案

層的版本是從 1 開始自動遞增的,目前一個層最大支援 100 個可用版本(不包括已刪除的版本);而對程式碼包來講沒有版本的概念,只有在服務層面上有版本概念,相對層的版本會更加複雜。

  • 層版本是隻讀的,不可變的

一個層的版本在建立後內容是無法改變的(許可權除外),如果想修改層的內容,只能釋出一個新的版本。層版本的只讀特效能夠避免層的改動對函式的影響。

  • 層的共享能力

層可以跨函式、跨賬號進行共享,而程式碼包不支援。

  • 層版本的軟刪除策略

層版本刪除後,不會影響已經配置改層版本的函式的正常執行。因為在層版本刪除,阿里雲函式計算平臺並不會直接將層版本的程式碼刪掉,而是先進行一次軟刪除操作,避免新的函式使用已刪除的層版本,當該層版本沒有函式引用時,才會徹底刪除該層版本。

阿里雲函式計算中有沒有與層相似的功能?與相似功能相比,層的優缺點是什麼?

在阿里雲函式計算平臺中,與層類似的功能是服務配置中的“掛載 NAS 檔案系統”和“掛載 OSS 物件儲存”功能,層與掛載 NAS/OSS 在功能和應用場景上有一些明顯的差異:

 title=

簡單總結一下,如果程式碼或資料的大小超過層的限制,則推薦使用掛載 NAS/OSS 的方式;如果程式碼或資料會經常改動,或者有執行中修改資料的需求,也推薦使用掛載 NAS/OSS 的方式。

結語

在阿里雲函式計算中,層的定位是一種不可變的基礎設施,透過層版本的只讀特性保證層的一致性和可靠性。本文首先介紹了自定義層的特點和困境,然後介紹了近期釋出的公共層功能,詳細陳述了基於官方公共層實現的兩個示例程式,最後探討了層的最佳實踐是什麼,希望透過本文能讓讀者更好的理解層的概念及其應用場景。

層的功能仍在持續完善中,接下來我們會在一下幾個方向進行重點最佳化:

  • 完善官方公共層體驗,補充更多的常用依賴庫或自定義執行時作為官方公共層,並提供完善的應用示例。
  • 提供公共層貢獻的方法和示例,促進公共層的開源共建。 

如果對層的使用有任何的疑惑或者建議,歡迎搜尋(群號:11721331)進入阿里雲函式計算釘釘群聯絡我們。

延申閱讀

[1] 建立自定義層 :

https://help.aliyun.com/docum...

*[2] 在函式中配置自定義層:*

https://help.aliyun.com/docum...

*[3] 如何在Custom Runtime中引用層中的依賴?* https://help.aliyun.com/document_detail/71142.html

*[4] 在函式中配置官方公共層:*

https://help.aliyun.com/docum...

*[5] 阿里雲函式計算 公共層github:*

github.com/awesome-fc/awesome-layers

*[6] 官方公共層- Nodejs-Puppeteer17x README*

https://github.com/awesome-fc...

*[7] Custom Runtime 基本原理*

https://help.aliyun.com/docum...

*[8] Custom Runtime 事件請求處理程式*

https://help.aliyun.com/docum...

*[9] Custom Runtime 函式例項生命週期回撥*

https://help.aliyun.com/docum...

點選此處,瞭解函式計算 FC!

相關文章