我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 本文作者:琉易 liuxianyu.cn 這一篇是系列文章: 搭建自動化 Web 頁面性能檢測系統 —— 設計篇 搭建自動化 Web 頁面性能檢測系統 —— 實現篇 作 ...
我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。
本文作者:琉易 liuxianyu.cn
這一篇是系列文章:
搭建自動化 Web 頁面性能檢測系統 —— 設計篇
搭建自動化 Web 頁面性能檢測系統 —— 實現篇
作為一個前端想去做全棧的項目時,可能第一個思路是 node + vue/react。一開始可能會新建多個工程目錄去實現,假設分別為 web 和 server,也許還有管理後臺的代碼 admin,那麼就有了三個工程的代碼。此時為了方便管理就需要在遠程倉庫新建一個 group 統一管理代碼,一般這種方式稱之為 MultiRepo。
這顯然是不夠簡潔的,對於開發者而言也不便於開發和部署。這類多模塊的項目我們可以引入 Monorepo 的概念,下麵是一些優化方法的嘗試,以 yice-performance(易測) 作為例子講解,本地設備為 M1 晶元的 arm64v8 平臺。
一、node 托管靜態頁面
可以將 web 打包的代碼交給 node 托管,此時就可以將 web 的代碼作為一個文件夾放到 server 的目錄中,這時候我們一般直接訪問後端介面的根路徑即可。如:yice-performance - v1.0
對應的 nginx 配置一般為:
server {
listen 80;
server_name yice.dtstack.cn;
location / {
proxy_pass http://localhost:4000/;
}
}
常見的 node 框架都支持托管靜態文件目錄:
// express
app.use(express.static(path.join(__dirname, 'web/dist')));
// NestJS
import { ServeStaticModule } from '@nestjs/serve-static';
ServeStaticModule.forRoot({
serveRoot: '/',
rootPath: join(__dirname, '.', 'web/dist'),
}),
// egg
{
static: {
dir: path.join(appInfo.baseDir, 'web/dist'),
}
}
代碼基本大同小異,從 nginx 配置和項目結構我們也能看出這還是屬於一個 node 項目的結構,前端項目的 nginx 配置一般為:
server {
listen 80;
server_name yice.dtstack.cn;
root /opt/dtstack/yice-performance/web/dist/
location /api {
proxy_pass http://localhost:4000/;
}
location / {
try_files $uri $uri/ /index.html;
}
}
二、Turborepo
Turborepo 是用於 JavaScript 和 TypeScript 代碼庫的高性能構建系統。
藉助 Turborepo 我們可以並行的運行和構建代碼,當我們使用傳統的 yarn workspace 管理代碼時,我們的一般會執行以下命令:
# server
yarn
yarn dev
# web
cd web
yarn
yarn dev
此時,本地開發不僅需要同時開啟兩個終端,而且還得分別註意兩個終端所在的路徑,lint、build、test 等命令皆如此。
想要更快的完成以上工作,可以使用 turbo run lint test build
。
新項目往往更容易使用 Turborepo,使用 create-turbo
創建即可,參考 官方文檔。歷史項目想要使用 Turborepo 時需要註意一下項目結構:
yice-performance
├─package.json
├─pnpm-lock.yaml
├─pnpm-workspace.yaml
├─turbo.json
├─apps
| ├─server
| └─web
將歷史項目的代碼整合到單個文件夾後移入 apps ,註意需要修改相對路徑等代碼,比如 tsconfig.json
文件中關於 @/*
等路徑別名的寫法,以及 import
依賴的路徑,將公共依賴包統一提到根目錄的 package.json 中。
在根目錄添加 turbo.json
文件,這裡是 dev 和 build 命令為例:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".apps/server/dist/**", "!.apps/server/cache/**"]
},
"dev": {
"persistent": true,
"cache": false
}
}
}
然後在 apps 下的產品中依次添加兩種命令:
{
"scripts": {
"dev": "NODE_ENV=development nest start --watch",
"build": "NODE_ENV=production nest build"
}
}
{
"scripts": {
"dev": "NODE_ENV=development vite --port 7001",
"build": "tsc && NODE_ENV=production vite build"
}
}
這樣就可以通過 pnpm dev
一條命令同時啟多個服務了,pnpm build
可以快速完成多個項目的打包工作。
三、docker
以易測依賴的 Puppeteer 為例,對於設備環境的要求就比較多,參考 Puppeteer 故障排除;再比如易測 v2.x 版本新增的數據周報功能使用到 node 端的 echarts,最終依賴 node-canvas,對設備環境的要求也很苛刻。
同時,部署命令寫的腳本中還需要考慮不同環境的差異,比如 Windows 中的情況。
docker
在這裡的作用就是抹平不同設備間的環境差異,減少補充安裝依賴包的痛苦,amd64、arm64 等環境差異導致的依賴包安裝失敗問題,我們可以構建適用於不同平臺的 docker 鏡像包(以下以 linux/amd64
為例,也就是常說的 x86_64
架構)。
Dockerfile
本地編寫 Dockerfile
文件,然後執行 docker build
命令構建鏡像。在構建鏡像之前,需要註意下 Dockerfile 構建鏡像時有一個 層
的概念,對於構建時間會有較大影響。
Docker 鏡像是由多個只讀的層疊加而成的,每一層都是基於前一層構建。Dockerfile 文件中的每條指令都會創建一個新的層,並對鏡像進行修改,執行
docker build
命令時會使用緩存,當前面的層不發生變化時,我們再次構建鏡像時就會更快速。但因為每一層都是基於前一層構建,所以我們應該把變化可能性小的操作放到前面,後續改動只會構建變化的內容,而無需構建整個鏡像,這能大大加快鏡像的構建速度。
比如下方 Dockerfile.server
中的 nodejs
的安裝,如果放在 COPY . .
之後,則每次構建都需要安裝一次 nodejs
,我們利用緩存可以大大減少構建時間。
FROM ubuntu:22.04
# 設置時區
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \
&& apt-get update -y && apt-get install -y tzdata
# puppeteer 和 node-canvas 對系統依賴的要求
# https://github.com/Automattic/node-canvas?tab=readme-ov-file#compiling
# https://github.com/puppeteer/puppeteer/blob/puppeteer-v19.6.3/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix
RUN apt-get update -y \
&& apt-get install -y build-essential libcairo2-dev libpango1.0-dev libnss3 libatk1.0-0 \
&& apt-get install -y ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 \
&& apt-get install -y libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \
&& apt-get install -y libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpangocairo-1.0-0 \
&& apt-get install -y libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
&& apt-get install -y libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
&& apt-get install -y libxss1 libxtst6
# 處理 chromium 等依賴問題
# https://github.com/puppeteer/puppeteer/blob/puppeteer-v19.6.3/docker/Dockerfile
RUN apt-get update -y \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg \
&& sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update -y \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get remove -y wget gnupg
# deb [arch=amd6 配置可能會在 /etc/apt/sources.list.d/google.list 和 /etc/apt/sources.list.d/google-chrome.list 中重覆,再嘗試一次
RUN rm -rf /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update -y \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends
# 安裝 nodejs
RUN apt-get update -y && apt-get install -y curl \
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get remove -y curl \
&& apt-get install -y nodejs \
&& npm config set registry https://registry.npmmirror.com/ \
&& npm install [email protected] -g
# 設置工作目錄
WORKDIR /yice-performance
# 拷貝代碼安裝依賴
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY apps/server/package.json ./apps/server/
COPY apps/web/package.json ./apps/web/
RUN pnpm install
# 複製項目文件
COPY apps .env ./
# 減少 node_modules 的磁碟占用
RUN pnpm build \
&& find . -name "node_modules" -type d -prune -exec rm -rf '{}' + \
&& pnpm install --production
# 暴露埠
EXPOSE 4000
# 定義環境變數
ENV NODE_ENV=production
# Dockerfile 中需指定 chromium 路徑
ENV PUPPETEER_EXECUTABLE_PATH='google-chrome-stable'
VOLUME [ "/yice-performance/apps/server/yice-report" ]
# 啟動應用程式
CMD ["node", "apps/server/dist/main.js"]
ARG BASE_IMAGE=mysql:5.7
FROM ${BASE_IMAGE}
# 當容器啟動時,會自動執行 /docker-entrypoint-initdb.d/ 下的所有 .sql 文件
COPY ./mysql/demo-data.sql /docker-entrypoint-initdb.d/
# 附加的 mysql 配置
COPY ./mysql/my_custom.cnf /etc/mysql/conf.d/
# 設置 MySQL root 用戶的密碼
ENV MYSQL_ROOT_PASSWORD=123456
ENV MYSQL_DATABASE=yice-performance
# 設置時區
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 暴露埠
EXPOSE 3306
根據 Dockerfile 文件本地構建鏡像,構建完成後在 Docker Desktop 中就可以看到剛剛構建的鏡像。我們新建一個腳本文件來統一管理命令,併在 package.json
中添加 build:docker
命令:
#!/bin/sh
cd docker
# amd64
docker buildx build --platform linux/amd64 -f Dockerfile.mysql -t liuxy0551/yice-mysql .
docker buildx build --platform linux/amd64 -f Dockerfile.server -t liuxy0551/yice-server ../
此時執行 pnpm build:docker
即可打包鏡像。
多平臺打包鏡像
由於我們目前使用的 Mac M 系列晶元較多,這是 arm64 v8 平臺的,但往往我們打包後的鏡像是在 x86 的機器上使用,比如 Centos、Ubuntu 等伺服器系統,這就要求我們應該相容 x86 平臺。
使用 docker inspect
的命令可以查看鏡像架構,如下:
docker pull alpine
docker inspect alpine | grep Architecture
修改剛剛寫的 Dockerfile
文件,支持通過 docker build
命令的 build argument
傳遞參數,這在明確不同平臺使用的基礎鏡像時比較有用。有些常用的基礎鏡像是支持多平臺,只需要添加 --platform linux/amd64, linux/arm64
即可,docker buildx 會自動處理一切,yice-mysql
支持了 arm64 v8
,其他內容可以自行研究。
鏡像發佈
這裡使用的是阿裡雲容器鏡像服務:https://cr.console.aliyun.com/。
docker login --username=your_username -p your_password registry.cn-hangzhou.aliyuncs.com
docker tag liuxy0551/yice-mysql registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-mysql:latest
docker tag liuxy0551/yice-server registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-server:latest
docker push registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-mysql:latest
docker push registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-server:latest
docker run
為了保證 yice-server
可以訪問到 yice-mysql
,兩個容器需要使用同一個網路。
docker network create yice-network
docker run -p 3306:3306 -d --name yice-mysql --network=yice-network -v /opt/dtstack/yice-performance/yice-mysql/conf:/etc/mysql/conf.d -v /opt/dtstack/yice-performance/yice-mysql/log:/var/log/mysql -v /opt/dtstack/yice-performance/yice-mysql/data:/var/lib/mysql registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-mysql:latest
docker run -p 4000:4000 -d --name yice-server --network=yice-network -v /opt/dtstack/yice-performance/yice-report:/yice-performance/apps/server/yice-report registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-server:latest
-p
表示埠映射,-p 宿主機 port:容器 port
,這裡暴漏埠是為了外部可以通過 GUI 工具查看數據-d
表示後臺運行並返回容器 id--name
表示給容器指定的名稱-v /opt/dtstack/yice-performance/yice-mysql:/etc/mysql/conf.d
等掛載路徑表示將容器中的配置項、數據、日誌都掛載到主機的/opt/dtstack/yice-performance/yice-mysql
下-v /opt/dtstack/yice-performance/yice-report:/yice-performance/apps/server/yice-report
表示將容器中的檢測報告掛載到宿主機- 掛載的目的是為了在刪除容器時數據不丟失,且儘量保持容器存儲層不發生寫操作。
執行 docker run
命令生成容器並運行,訪問 http://localhost:4000 即可看到頁面了。
docker-compose
docker-compose
是 Docker 官方提供的一個工具,用於管理多個 Docker 容器的應用程式,使用 docker-compose
可以協同多個容器運行。
新增 docker-compose.yml
文件,在這個文件里定義應用程式所需的服務和容器,包括鏡像、環境變數、埠映射、掛載目錄等信息。
version: '3'
services:
mysql-service:
container_name: yice-mysql
image: registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-server:latest
ports:
- '3306:3306'
restart: always
networks:
- yice-network
server-service:
container_name: yice-server
image: registry.cn-hangzhou.aliyuncs.com/liuxy0551/yice-mysql:latest
ports:
- '4000:4000'
restart: always
depends_on:
- mysql-service
networks:
- yice-network
networks:
yice-network:
driver: bridge
docker-compose -f docker/docker-compose.yml -p yice-performance up -d
命令 | 作用 |
---|---|
docker-compose up | 啟動程式,-d 後臺運行 |
docker-compose down | 停止並移除容器、捲、鏡像等 |
docker-compose ps | 列出正在運行的容器 |
docker-compose logs | 查看日誌 |
docker-compose stop | 停止服務 |
docker-compose start | 啟動服務 |
docker-compose restart | 重啟服務 |
四、常見問題
yice-server
無法啟動
可能是 docker 版本較低,建議升級到 docker v24 及以上,升級前應當備份。
yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
node[1]: ../src/node_platform.cc:61:std::unique_ptr<long unsigned int> node::WorkerThreadsTaskRunner::DelayedTaskScheduler::Start(): Assertion `(0) == (uv_thread_create(t.get(), start_thread, this))' failed.
1: 0xb090e0 node::Abort() [node]
2: 0xb0915e [node]
3: 0xb7512e [node]
4: 0xb751f6 node::NodePlatform::NodePlatform(int, v8::TracingController*) [node]
5: 0xacbf74 node::InitializeOncePerProcess(int, char**, node::InitializationSettingsFlags, node::ProcessFlags::Flags) [node]
6: 0xaccb59 node::Start(int, char**) [node]
7: 0x7f2ffac64d90 [/lib/x86_64-linux-gnu/libc.so.6]
8: 0x7f2ffac64e40 __libc_start_main [/lib/x86_64-linux-gnu/libc.so.6]
9: 0xa408ec [node]
gcc 版本過低
主機部署時建議使用 Ubuntu。
主機模式部署時 CentOS7
上啟動服務時報錯:Error: /lib64/libstdc++.so.6: version 'CXXABI_1.3.9' not found
,這是因為 CentOS7
的 gcc
版本過低,需要升級到 gcc-4.8.5
以上,執行下方命令可以看到沒有 CXXABI_1.3.9
。
strings /lib64/libstdc++.so.6 | grep CXXABI
相關鏈接:
https://github.com/Automattic/node-canvas/issues/1796
https://gist.github.com/nchaigne/ad06bc867f911a3c0d32939f1e930a11
https://ftp.gnu.org/gnu/gcc/
cd /etc/gcc
wget https://ftp.gnu.org/gnu/gcc/gcc-9.5.0/gcc-9.5.0.tar.gz
tar xzvf gcc-9.5.0.tar.gz
mkdir obj.gcc-9.5.0
cd gcc-9.5.0
./contrib/download_prerequisites
cd ../obj.gcc-9.5.0
../gcc-9.5.0/configure --disable-multilib --enable-languages=c,c++
make -j $(nproc)
make install
最後
歡迎關註【袋鼠雲數棧UED團隊】~
袋鼠雲數棧 UED 團隊持續為廣大開發者分享技術成果,相繼參與開源了歡迎 star
- 大數據分散式任務調度系統——Taier
- 輕量級的 Web IDE UI 框架——Molecule
- 針對大數據領域的 SQL Parser 項目——dt-sql-parser
- 袋鼠雲數棧前端團隊代碼評審工程實踐文檔——code-review-practices
- 一個速度更快、配置更靈活、使用更簡單的模塊打包器——ko
- 一個針對 antd 的組件測試工具庫——ant-design-testing