手把手教您將libreoffice移植到函式計算平臺

倚賢發表於2018-12-02

LibreOffice 是由文件基金會開發的自由及開放原始碼的辦公室套件。LibreOffice 套件包含文書處理器、電子表格、簡報程式、向量圖形編輯器和圖表工具、資料庫管理程式及建立和編輯數學公式的應用程式。藉助 LibreOffice 的命令列介面可以方便地將 office 檔案轉換成 pdf。如下所示:

$ soffice --convert-to pdf --outdir /tmp /tmp/test.doc

一個完整版本的 LibreOffice 大小為 2 GB,而函式計算執行時快取目錄 /tmp 空間限制為 512M,zip 程式包大小限制為 50M。好在社群已經有專案 aws-lambda-libreoffice 成功的將 libreoffice 移植到 AWS Lambda 平臺,基於前人的方法和經驗,本人建立了 fc-libreoffice 專案,使 libreoffice 成功的執行在阿里雲函式計算平臺。fc-libreoffice 在 aws-lambda-libreoffice 的基礎上解決了如下問題:

  1. 重新編譯和裁剪 libreoffice ,使其適配 FC nodejs8 runtime 內建的 gcc 和核心版本;
  2. 安裝執行時缺失的 libssl3 依賴;
  3. 藉助 OSS 執行時下載解壓,以繞過 zip 程式包 50M 的限制;
  4. 製作了一個 example 專案,支援一鍵部署,快速體驗。

本文側重於記述整個移植過程,記錄關鍵步驟以備忘,也為類似的轉換工具移植到函式計算平臺提供參考。如果您對於如何快速搭建一個廉價且可擴充套件的 word 轉換 pdf 雲服務更感興趣,可以閱讀另一篇文章《五分鐘上線——函式計算 Word 轉 PDF 雲服務》

準備工作

在開始之前建議找一個臺配置較好的 Debain/Ubuntu 機器,libreoffice 編譯比較消耗計算資源。並在機器上安裝和配置如下工具:

  • docker-ce 安裝方法參考官方安裝文件
  • fun 一款函式計算的編排工具,用於快速部署函式計算應用。

    MacOS 平臺可以使用如下方法安裝

    brew tap vangie/formula
    brew install fun

    其他平臺可以通過 npm 安裝

    npm install @alicloud/fun -g
  • ossutil oss 的命令列工具。將其下載並放置到 $PATH 所在目錄。

編譯 libreoffice

我們會採用 fc-docker 提供的 aliyunfc/runtime-nodejs8:build docker 映象進行編譯。fc-docker 提供了一系列的 docker 映象,這些 docker 映象環境非常接近函式計算的真實環境。因為我們打算把 libreoffice 跑在 nodejs8 環境中,所以我們選用了 aliyunfc/runtime-nodejs8:build,build 標籤映象相比於其他映象會多一些構建需要的基礎包

啟動一個編譯環境

通過如下命令可啟動一個用於構建 libreoffice 的容器。

docker run --name libre-builder --rm  -v $(pwd):/code -d -t --cap-add=SYS_PTRACE --security-opt seccomp=unconfined aliyunfc/runtime-nodejs8:build bash

上面的命令,我們啟動了一個名為 libre-builder 的容器並把當前目錄掛載到容器內檔案系統的 /code 目錄。附加引數 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined 是 cpp 程式編譯需要的,否則會報出一些警告。-d 表示以後臺 daemon 的方式啟動。-t 表示啟動 tty,配合後面的 bash 命令是為了卡主容器不退出。而 --rm 表示一旦容器停止了就自動刪除容器。

安裝編譯工具

接下來進入容器安裝編譯工具

apt-get install -y ccache
apt-get build-dep -y libreoffice

ccache 是一個編譯工具,可以加速 gcc 對同一個程式的多次編譯。儘管第一次編譯會花費長一點的時間,有了ccache,後續的編譯將變得非常非常快。

apt-get 的 build-dep 子命令會建立某個要編譯軟體的環境。具體行為就是把所有依賴的工具和軟體包都安裝上。

克隆原始碼

git clone --depth=1 git://anongit.freedesktop.org/libreoffice/core libreoffice
cd libreoffice

記得加上 --depth=1 引數,因為 libreoffice 專案比較大,進行全量克隆會比較費時間,對於編譯來說 git 提交歷史沒有意義。

配置並編譯

# 如果多次編譯,該設定可以加速後續編譯
ccache --max-size 16 G && ccache -s

通過 –disable 引數去掉不需要的模組,以減少最終編譯產物的體積。

# the most important part. Run ./autogen.sh --help to see wha each option means
./autogen.sh --disable-report-builder --disable-lpsolve --disable-coinmp 
    --enable-mergelibs --disable-odk --disable-gtk --disable-cairo-canvas 
    --disable-dbus --disable-sdremote --disable-sdremote-bluetooth --disable-gio --disable-randr 
    --disable-gstreamer-1-0 --disable-cve-tests --disable-cups --disable-extension-update 
    --disable-postgresql-sdbc --disable-lotuswordpro --disable-firebird-sdbc --disable-scripting-beanshell 
    --disable-scripting-javascript --disable-largefile --without-helppack-integration 
    --without-system-dicts --without-java --disable-gtk3 --disable-dconf --disable-gstreamer-0-10 
    --disable-firebird-sdbc --without-fonts --without-junit --with-theme="no" --disable-evolution2 
    --disable-avahi --without-myspell-dicts --with-galleries="no" 
    --disable-kde4 --with-system-expat --with-system-libxml --with-system-nss 
    --disable-introspection --without-krb5 --disable-python --disable-pch 
    --with-system-openssl --with-system-curl --disable-ooenv --disable-dependency-tracking

開始編譯

make

最終的編譯結果位於 ./instdir/ 目錄下。

精簡尺寸

使用 strip 命令去除二進位制檔案中的符號資訊和編譯資訊

# this will remove ~100 MB of symbols from shared objects
strip ./instdir/**/*

刪除不必要的檔案

# remove unneeded stuff for headless mode
rm -rf ./instdir/share/gallery 
    ./instdir/share/config/images_*.zip 
    ./instdir/readmes 
    ./instdir/CREDITS.fodt 
    ./instdir/LICENSE* 
    ./instdir/NOTICE

驗證

使用如下命令,測試一下編譯出來的 soffice 是否能正常將 txt 檔案轉換成 pdf 檔案。

echo "hello world" > a.txt
./instdir/program/soffice --headless --invisible --nodefault --nofirststartwizard 
    --nolockcheck --nologo --norestore --convert-to pdf --outdir $(pwd) a.txt

打包

# archive
tar -zcvf lo.tar.gz instdir

然後使用如下命令將 lo.tar.gz 檔案從容器檔案系統拷貝到宿主機檔案系統。

docker cp libre-builder:/code/libreoffice/lo.tar.gz ./lo.tar.gz

Gzip vs Zopfli vs Brotli
Gzip 、Zopfli 和 Brotli 是三種開源的壓縮演算法,對於一個 130M 的 chromium 檔案,分別採用這三種壓縮演算法最大 level 的壓縮效果是

檔案 演算法 MiB 壓縮比 解壓耗時
chromium 130.62
chromium.gz Gzip 44.13 66.22% 0.968s
chromium.gz Zopfli 43.00 67.08% 0.935s
chromium.br Brotli 33.21 74.58% 0.712s

從上面的結果看 Brotli 演算法的效果最優。

由於 aliyunfc/runtime-nodejs8:build 是基於 debain jessie 發行版的。在 debain jessie 上安裝 brotli 較為麻煩,所以我們藉助 ubuntu 容器安裝 brotli 工具,將 tar.gz 格式轉為 tar.br 格式。

docker run --name brotli-util --rm -v $(pwd):/root -w /root -d -t ubuntu:18.04 bash
docker exec -t brotli-util apt-get update
docker exec -t brotli-util apt-get install -y brotli
docker exec -t brotli-util gzip -d lo.tar.gz
docker exec -t brotli-util brotli -q 11 -j -f lo.tar

然後當前目錄會多一個 lo.tar.br 檔案。

安裝依賴

在函式計算 nodejs8 環境中執行 soffice ,需要安裝通過 npm 安裝 tar.br 的解壓依賴包 @shelf/aws-lambda-brotli-unpacker 和 通過 apt-get 安裝 libnss3 依賴。先啟動一個 nodejs8 的容器,以保證依賴的安裝環境和執行時環境是一致的。

docker run --rm --name libreoffice-builder -t -d -v $(pwd):/code --entrypoint /bin/sh aliyunfc/runtime-nodejs8

注意:@shelf/aws-lambda-brotli-unpacker 存在 native binding,所以在開發機 MacOS 上 npm install 打包上傳是無法工作。

docker exec -t libreoffice-builder npm install

由於函式計算執行時無法安裝全域性的 deb 包,所以需要將 deb 和依賴的 deb 包下載下來,再安裝到當前工作目錄而不是系統目錄。當前工作目錄下可以隨程式碼一起打包上傳。

docker exec -t libreoffice-builder apt-get install -y -d -o=dir::cache=/code libnss3
docker exec -t libreoffice-builder bash -c `for f in $(ls /code/archives/*.deb); do dpkg -x $f $(pwd) ; done;`

libnss3 包含了許多 .so 動態連結庫檔案,linux 系統下 LD_LIBRARY_PATH 環境變數裡的動態連結庫才能被找到,而函式計算將程式碼目錄/code 下的 lib 目錄預設新增到了 LD_LIBRARY_PATH 中。所以我們寫個指令碼,把所有安裝的 .so 檔案軟連線到 /code/lib 目錄下

docker exec -t libreoffice-builder bash -c "rm -rf /code/archives/; mkdir -p /code/lib;cd /code/lib; find ../usr/lib -type f ( -name `*.so` -o -name `*.chk` ) -exec ln -sf {} . ;"

下載並解壓 tar.br

為了使用 這個 lo.tar.br 檔案,需要先上傳到 OSS

ossutil cp $SCRIPT_DIR/../node_modules/fc-libreoffice/bin/lo.tar.br oss://${OSS_BUCKET}/lo.tar.br 
     -i ${ALIBABA_CLOUD_ACCESS_KEY_ID} -k ${ALIBABA_CLOUD_ACCESS_KEY_SECRET} -e oss-${ALIBABA_CLOUD_DEFAULT_REGION}.aliyuncs.com -f

在函式的 initializer 方法中下載。

module.exports.initializer = (context, callback) => {

    store = new OSS({
        region: `oss-${process.env.ALIBABA_CLOUD_DEFAULT_REGION}`,
        bucket: process.env.OSS_BUCKET,
        accessKeyId: context.credentials.accessKeyId,
        accessKeySecret: context.credentials.accessKeySecret,
        stsToken: context.credentials.securityToken,
        internal: process.env.OSS_INTERNAL === `true`
    });

    if (fs.existsSync(binPath) === true) {
        callback(null, "already downloaded.");
        return;
    }

    co(store.get(`lo.tar.br`, binPath)).then(function (val) {
        callback(null, val)
    }).catch(function (err) {
        callback(err)
    });
};

然後藉助於 @shelf/aws-lambda-brotli-unpacker npm 包解壓 lo.tar.br

const {unpack} = require(`@shelf/aws-lambda-brotli-unpacker`);
const {execSync} = require(`child_process`);

const inputPath = path.join(__dirname, `..`, `bin`, `lo.tar.br`);
const outputPath = `/tmp/instdir/program/soffice`;

module.exports.handler = async event => {
  await unpack({inputPath, outputPath});

  execSync(`${outputPath} --convert-to pdf --outdir /tmp /tmp/example.docx`);
};

fun 部署函式

編寫一個 template.yml 檔案,將函式計算的配置都寫在該檔案中,然後使用 fun deploy 命令部署函式。

ROSTemplateFormatVersion: `2015-09-01`
Transform: `Aliyun::Serverless-2018-04-03`
Resources:
  libre-svc: # service name
    Type: `Aliyun::Serverless::Service`
    Properties:
      Description: `fc test`
      Policies: 
        - AliyunOSSFullAccess
    libre-fun: # function name
      Type: `Aliyun::Serverless::Function`
      Properties:
        Handler: index.handler
        Initializer: index.initializer
        Runtime: nodejs8
        CodeUri: `./`
        Timeout: 60
        MemorySize: 640
        EnvironmentVariables:
          ALIBABA_CLOUD_DEFAULT_REGION: ${ALIBABA_CLOUD_DEFAULT_REGION}
          OSS_BUCKET: ${OSS_BUCKET}
          OSS_INTERNAL: `true`

真實場景下,把祕鑰和一起變數寫在 template.yml 裡並不合適。為了做到程式碼和配置相分離,上面使用了變數佔位符 ${ALIBABA_CLOUD_DEFAULT_REGION}${OSS_BUCKET}

然後使用 envsubst 進行替換

SCRIPT_DIR=`dirname -- "$0"`
source $SCRIPT_DIR/../.env

export ALIBABA_CLOUD_DEFAULT_REGION OSS_BUCKET
envsubst < $SCRIPT_DIR/../template.yml.tpl > $SCRIPT_DIR/../template.yml

cd $SCRIPT_DIR/../

上面所有的配置都寫在了 .env 檔案中,dotenv 是社群常見的方案,也有廣泛的工具支援。

小結

本文重點介紹了編譯 libreoffice 的過程,這也是移植中較為困難的部分。由於 libreoffice 又涉及到 npm 的 native binding 和 apt-get 安裝到本地目錄的問題,所以在函式計算依賴方面本例也是非常經典的場景。無論是編譯還是依賴安裝,本文中的步驟都強烈地依賴 fc-docker 映象,正因為有了該映象,解決了環境差異問題,大大降低了移植的難度。大檔案執行時載入也是函式計算的常見問題,對於轉換工具場景中常見的大檔案是二進位制程式,對於機器學習場景中大檔案常是訓練模型的資料問題,但是無論是哪一種,採用 OSS 下載解壓的方法都是通用的,隨著函式計算支援了 NAS,使用 NAS 掛載共享網盤的方式也是一種新的路徑。

上文完整的原始碼可以在 fc-libreoffice 專案中找到。

參考閱讀

  1. https://zh.wikipedia.org/wiki/LibreOffice
  2. How to Run LibreOffice in AWS Lambda for Dirty-Cheap PDFs at Scale
  3. https://github.com/alixaxel/chrome-aws-lambda
  4. https://github.com/shelfio/aws-lambda-brotli-unpacker


相關文章