提交 54ee0f0d authored 作者: 陈泽健's avatar 陈泽健

feat(deploy): 新增 X86/ARM Tengine 容器部署方案及负载均衡配置指导

- 新增 X86/ARM 双架构 tengine 部署文档、Dockerfile 和部署脚本
- 解决 nginx:1.30.2 (Debian Trixie) 缺少 libpcre3-dev 编译问题
- 新增 tengine 负载均衡配置指导文档(含 upstream/健康检查/会话保持)
- 更新 X86-TX-XTYBS SKILL.md:新增授权教训和 phase4/verify 脚本引用
- upload_to_nas.py 输出格式优化(统一 [OK]/[FAIL] 标记)
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 2fe45fe4
......@@ -255,3 +255,132 @@ python AuxiliaryTool/ScriptTool/RemoteDeploy/full_deploy.py --arch x86_uos --ver
- `deploy_config.json` - 部署配置文件
- `verify_x86.py` - X86验证脚本
- `verify_x86_api.py` - X86 API验证脚本
- `phase4_authorize.py` - 阶段4 Web授权脚本(Playwright,含文件上传+下载激活文件)
- `download_activation.py` - 下载激活文件脚本
- `verify_business.py` - 业务功能验证脚本(后台页面访问检查)
## 🔧 实战教训(根因+对策,下次不再踩)
### A. 授权必须先"下载激活文件"再上传license.zip → 否则meeting2.0报"CPU/MAC不匹配"、预定500
**现象**:阶段4上传了license.zip后,运维接口恢复但预定系统接口仍然500。meeting2.0日志显示:
```
Caused by: java.lang.RuntimeException: 授权校验失败,CPU序列号或MAC地址不匹配
```
**根因**:服务器重新部署后(新docker网络、新MAC地址),旧的license.zip中的.uas文件绑定了旧硬件指纹。必须先点"下载激活文件"让后端注册当前服务器的CPU/MAC,下载得到的`license.txt`才是正确绑定当前硬件的。虽然原始license.zip的`.uas`文件被正确分发到各服务config目录,但硬件指纹不匹配导致校验失败。
**对策**:
1. 严格按 **"下载激活文件→上传license.zip→重启"** 顺序操作,"下载激活文件"不可省略
2. 如果已上传但校验失败:重新点"下载激活文件"→获取新的`license.txt`→手动复制到`/data/services/api/java-meeting/java-meeting2.0/config/license.txt`→`docker restart ujava2`→等10-15分钟
3. 验证:`tail -f /data/services/api/java-meeting/java-meeting2.0/logs/ubains-INFO-AND-ERROR.log | grep -i "授权\|CPU\|MAC"`
### B. Element UI 身份校验对话框+文件上传的时序问题
**现象**:Playwright点击"上传授权文件"按钮后,弹出的身份校验对话框内没有`<input type="file">`元素,整个页面都找不到file input。点确定关闭对话框后也没有文件选择器弹出。
**根因**:Element UI的授权流程是:
1. 点"上传授权文件"→弹出**身份校验对话框**(只有账号[disabled]+密码+验证码)
2. 填入密码和验证码→点确定→**后端验证身份**
3. 身份验证通过→对话框关闭→**文件选择器弹出**(el-upload组件动态创建)
关键点:身份验证通过后文件选择器才出现;`fill()`方法比`type()`更可靠。
**对策**:
1. 使用 `fill()` 方法填入密码和验证码(不用 `type()`)
2. 在点击"确定"按钮**之前**设置 `page.expect_file_chooser()` 监听
3. 文件选择器弹出后使用 `fc.value.set_files(license_path)` 选择文件
4. 如果file_chooser未触发:检查对话框是否关闭(身份验证失败→对话框仍然display=true),检查错误消息
5. 上传成功后后端日志会显示:`"上传ZIP授权文件" → "文件写入完成" → "iszip:true"`
### C. X86 UOS部署用时约34分钟,非40分钟
**现象**:`new_auto.sh --all` 实际执行约23分钟(非PRD预计的40分钟)。
**说明**:X86架构服务器性能较好,实际部署时间可能比文档预估的短。但超时设置仍应保持60分钟作为安全边界。
### D. Playwright连接自签名HTTPS站点
**现象**:Chrome DevTools MCP无法连接自签名证书的`https://192.168.5.70`,报`ERR_CERT_AUTHORITY_INVALID`。
**对策**:使用Playwright替代Chrome DevTools MCP操作Web界面:
```python
browser = p.chromium.launch(headless=False, args=['--ignore-certificate-errors'])
context = browser.new_context(ignore_https_errors=True)
```
### E. LoginAdmin后台页面结构(iframe内嵌模式)
**现象**:登录`/#/LoginAdmin`后,URL跳转到`/#/backend/backstage?backstage=...`的iframe嵌入模式。直接用`/#/Home`访问会报"页面不存在"。
**说明**:新统一平台的后台是维护平台(LoginConfig)内嵌backstage实现。**登录后先刷新页面**(`page.reload()`),等待约5秒,侧边栏菜单才会完整渲染。
**超管superadmin侧边栏菜单结构(2026-07-02验证)**:
```
组织信息
└ 组织信息(默认首页)
用户管理 ← 一级子菜单
├ 用户列表
├ 部门管理
├ 职位管理
├ 角色管理
├ 新增用户
├ 批量新增
└ 下载模板
转录管理
└ 组织架构 / 人员管理 / 部门管理 / 审批管理 / ...
授权管理
├ 会议授权
├ 型号管理
└ 运维授权
日志管理
├ 会议日志 / 通知日志 / 客服日志 / 设备日志 / 运维日志
系统管理 ← 一级子菜单(需先展开!)
├ 系统设置
├ 系统信息
├ 管理员设置 ← 在公司管理员设置页面(非"公司管理")
├ 角色管理
├ 权限管理
├ 字典管理
├ 运维角色
├ 运维人员
├ 转录信息
├ 会议授权
├ 服务升级
└ 服务信息
```
**已验证页面(2026-07-02)**
- ✅ 管理员设置(系统管理→管理员设置):正常,显示公司管理员列表(superadmin / 优班标准统一平台)
- ✅ 权限管理(系统管理→权限管理):正常,显示权限组列表("超级管理员")
- ✅ 部门管理(用户管理→部门管理):正常,显示部门列表(研发部/财务部/运营部)
- ✅ 用户列表(用户管理→用户列表):正常,显示superadmin用户
- ✅ 服务升级(系统管理→服务升级):正常
**关键操作要点**
1. 登录后必须**刷新页面**`page.reload()`),否则菜单可能不完整
2. **【管理员设置】在【系统管理】子菜单下**,必须先点击展开【系统管理】,再点击【管理员设置】
3. **不要直接拼URL访问**(如`/Backend/System/AdminSetting`),通过侧边栏菜单点击导航
4. Element UI子菜单展开有时序:点击`.el-submenu__title`后等约2秒子项才可见
### F. 讯飞转录接口成功标志修正
**现象**:X86架构讯飞服务已适配,接口返回`{"success":true,"data":[...]}`(带正常会议室数据),而非文档描述的`"缺少关键参数"`
**说明**`"缺少关键参数"`是ARM未适配场景的预期响应。X86架构讯飞服务正常,成功标志应为`"success":true`
### G. 授权后预定系统接口可能需要较长时间恢复
**现象**:授权+重启后,对外接口和运维接口几分钟内就恢复,但预定系统接口持续返回500约15-20分钟。
**说明**:ujava2容器内的meeting2.0 Spring Boot启动需要较长时间(X86约10-15分钟,ARM约28分钟)。重启后需耐心等待,不要反复重启重置启动周期。验证时预定接口放最后测。
### H. Playwright文件上传的Element UI el-upload处理
**现象**:el-upload组件将`<input type="file">`隐藏(display:none),直接用`query_selector('input[type="file"]')`可能找不到或设置文件无效。
**对策**
1. 优先使用 `page.expect_file_chooser()` 监听方式
2. 备选:找到全局所有file input,逐个尝试`set_input_files()`
3. 最后手段:用JS将隐藏的file input变为可见后再设置文件
4. 上传成功后检查后端日志确认:`tail -f /data/services/api/java-meeting/java-meeting-extapi/logs/ubains-INFO-AND-ERROR.log | grep "uploadLincenceFile"`
......@@ -401,9 +401,9 @@ def package_and_upload(source_dir: str, target_dir: str) -> dict:
missing = validate_source_dir(source_dir)
if missing:
result["error"] = f"源目录缺少核心文件: {', '.join(missing)}"
print(f"\n {result['error']}")
print(f"\n [FAIL] {result['error']}")
return result
print(" 源目录验证通过")
print(" [OK] 源目录验证通过")
# ---- 阶段2: 网盘连接验证 ----
print(f"\n{'=' * 60}")
......@@ -412,13 +412,13 @@ def package_and_upload(source_dir: str, target_dir: str) -> dict:
if not validate_nas_path(target_dir):
result["error"] = (
"网盘路径不可访问,请检查:\n"
" 是否连接公司网络\n"
" 是否具有 \\\\192.168.9.9\\发布版本 的访问权限\n"
" 当前 Windows 用户是否已授权该共享目录"
" 1. 是否连接公司网络\n"
" 2. 是否具有 \\\\192.168.9.9\\发布版本 的访问权限\n"
" 3. 当前 Windows 用户是否已授权该共享目录"
)
print(f"\n {result['error']}")
print(f"\n [FAIL] {result['error']}")
return result
print(" 网盘连接验证通过")
print(" [OK] 网盘连接验证通过")
# ---- 阶段3: 打包 ----
print(f"\n{'=' * 60}")
......@@ -434,9 +434,9 @@ def package_and_upload(source_dir: str, target_dir: str) -> dict:
print("=" * 60)
if not backup_existing(target_dir, ZIP_FILENAME):
result["error"] = "备份网盘旧工具包失败,终止上传"
print(f"\n {result['error']}")
print(f"\n [FAIL] {result['error']}")
return result
print(" 备份阶段完成")
print(" [OK] 备份阶段完成")
# ---- 阶段5: 上传 ----
print(f"\n{'=' * 60}")
......@@ -451,14 +451,14 @@ def package_and_upload(source_dir: str, target_dir: str) -> dict:
print(f" 正在上传: {ZIP_FILENAME} ...")
uploaded_zip = upload_file(local_zip, target_dir)
result["zip_path"] = uploaded_zip
print(f" {ZIP_FILENAME} 上传成功")
print(f" [OK] {ZIP_FILENAME} 上传成功")
# 5c. 上传 md5 文件
md5_basename = os.path.basename(local_md5)
print(f" 正在上传: {md5_basename} ...")
uploaded_md5 = upload_file(local_md5, target_dir)
result["md5_path"] = uploaded_md5
print(f" {md5_basename} 上传成功")
print(f" [OK] {md5_basename} 上传成功")
result["success"] = True
......@@ -473,13 +473,13 @@ def package_and_upload(source_dir: str, target_dir: str) -> dict:
print(f" 已清理: {os.path.basename(tmp_file)}")
except OSError as e:
print(f" 清理失败: {os.path.basename(tmp_file)}, 错误: {e}")
print(" 清理完成")
print(" [OK] 清理完成")
return result
except Exception as e:
result["error"] = str(e)
print(f"\n 上传流程异常: {e}")
print(f"\n [FAIL] 上传流程异常: {e}")
# 异常时也尝试清理临时文件
for tmp_file in [local_zip, local_zip + ".md5"]:
......@@ -504,7 +504,7 @@ def print_summary(result: dict):
print("=" * 60)
if result["success"]:
print(f" 状态: 成功")
print(f" 状态: [OK] 成功")
print(f" 上传路径: {result['zip_path']}")
print(f" MD5路径: {result['md5_path']}")
print(f" 文件大小: {result['file_size']}")
......@@ -514,7 +514,7 @@ def print_summary(result: dict):
else:
print(f" 备份文件: 无(首次上传)")
else:
print(f" 状态: 失败")
print(f" 状态: [FAIL] 失败")
print(f" 错误信息: {result['error']}")
print("=" * 60)
......
FROM nginx:1.30.2
# 安装编译依赖(Trixie 移除了 libpcre3-dev,需手动编译 PCRE2)
RUN apt-get update && apt-get install -y \
gcc \
make \
libc-dev \
libssl-dev \
zlib1g-dev \
pkg-config \
curl \
&& rm -rf /var/lib/apt/lists/*
# 下载并编译安装 PCRE2(Trixie 仓库缺少 libpcre2-dev)
ARG PCRE2_VERSION=10.45
RUN curl -sSL https://github.com/PCRE2Project/pcre2/releases/download/pcre2-${PCRE2_VERSION}/pcre2-${PCRE2_VERSION}.tar.gz -o /tmp/pcre2.tar.gz && \
cd /tmp && tar xzf pcre2.tar.gz && \
cd pcre2-${PCRE2_VERSION} && \
./configure --prefix=/usr && \
make -j$(nproc) && make install && \
cd /tmp && rm -rf pcre2-${PCRE2_VERSION} pcre2.tar.gz
# 拷贝 tengine 源码包并编译安装
COPY tengine-3.1.0.tar.gz /tmp/
WORKDIR /tmp
RUN tar xzf tengine-3.1.0.tar.gz && \
cd tengine-3.1.0 && \
./configure \
--prefix=/usr/local/tengine \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--with-http_sub_module \
--with-stream \
--with-stream_ssl_module \
--conf-path=/etc/tengine/tengine.conf \
--error-log-path=/var/log/tengine/error.log \
--http-log-path=/var/log/tengine/access.log \
--pid-path=/var/run/tengine.pid \
&& make -j$(nproc) && make install && \
cd /tmp && rm -rf tengine-3.1.0 tengine-3.1.0.tar.gz
# 创建运行时目录
RUN mkdir -p /etc/tengine/conf.d /var/log/tengine /usr/local/tengine/html && \
chmod 755 /var/log/tengine
# 生成自签名 SSL 证书(如无正式证书可替换)
RUN mkdir -p /etc/tengine/ssl && \
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/tengine/ssl/utengine.key \
-out /etc/tengine/ssl/utengine.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=UTengine/CN=utengine"
# 将 tengine 二进制加入 PATH
ENV PATH="/usr/local/tengine/sbin:${PATH}"
EXPOSE 80 443
STOPSIGNAL SIGQUIT
CMD ["tengine", "-g", "daemon off;"]
#!/bin/bash
# ============================================================
# Tengine 容器一键部署脚本 (ARM 架构)
# 适用于:openEuler 24.03 (aarch64)
# 版本:1.0
# 日期:2026-07-06
# ============================================================
set -e
# ==================== 配置变量 ====================
# ARM 服务器 docker 需 sudo(admin 用户),X86 设为空
DOCKER_CMD="sudo docker"
# DOCKER_CMD="docker" # X86 用这行
IMAGE_NAME="utengine:v1"
CONTAINER_NAME="utengine"
OLD_CONTAINER="unginx"
BUILD_DIR="/data/tengine_build"
DOCKERFILE_SRC="/data/Dockerfile.utengine"
TENGINE_TAR="/data/tengine-3.1.0.tar.gz"
# 容器运行参数
HOST_PORT="443"
CONTAINER_PORT="443"
# 挂载映射(宿主机:容器:模式)
# 格式: "source:dest:mode" (mode: ro/rw)
declare -a MOUNTS=(
"/data/security/nginx_cert:/data/security/nginx_cert:ro"
"/data/services/web:/data/services/web:rw"
"/etc/localtime:/etc/localtime:ro"
"/data/middleware/nginx/config:/etc/tengine/conf.d:rw"
"/data/middleware/nginx/data/html:/usr/local/tengine/html:rw"
"/data/middleware/nginx/data/cache:/var/cache/nginx:rw"
"/data/middleware/nginx/log:/var/log/tengine:rw"
)
# Tengine 主配置文件(覆盖默认配置)
TENGINE_MAIN_CONF='user root;
worker_processes auto;
error_log /var/log/tengine/error.log notice;
pid /var/run/tengine.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '"'"'$remote_addr - $remote_user [$time_local] "$request" '"'"'
'"'"'$status $body_bytes_sent "$http_referer" '"'"'
'"'"'"$http_user_agent" "$http_x_forwarded_for"'"'"';
access_log /var/log/tengine/access.log main;
sendfile on;
keepalive_timeout 65;
# 加载站点配置
include /etc/tengine/conf.d/*.conf;
}'
# ==================== 颜色输出 ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
# ==================== 前置检查 ====================
preflight_check() {
log_step "执行前置检查..."
# 检查 Docker
if ! command -v ${DOCKER_CMD%% *} &> /dev/null; then
log_error "Docker 未安装"
exit 1
fi
# 检查基础镜像
if ! ${DOCKER_CMD} image inspect nginx:1.30.2 &> /dev/null; then
log_error "基础镜像 nginx:1.30.2 不存在"
exit 1
fi
# 检查源码包
if [ ! -f "$TENGINE_TAR" ]; then
log_error "Tengine 源码包不存在: $TENGINE_TAR"
exit 1
fi
if [ ! -f "$DOCKERFILE_SRC" ]; then
log_error "Dockerfile 不存在: $DOCKERFILE_SRC"
exit 1
fi
# 检查挂载源目录
for mount in "${MOUNTS[@]}"; do
IFS=':' read -r src dest mode <<< "$mount"
if [ ! -d "$src" ] && [ ! -f "$src" ]; then
log_error "挂载源路径不存在: $src"
exit 1
fi
done
# 检查端口是否被占用
if ss -tlnp | grep -q ":${HOST_PORT} "; then
log_warn "端口 ${HOST_PORT} 已被占用,将先停止旧容器"
fi
log_info "前置检查通过"
}
# ==================== 构建镜像 ====================
build_image() {
# 如果镜像已存在,询问是否重建
if ${DOCKER_CMD} image inspect "$IMAGE_NAME" &> /dev/null; then
log_warn "镜像 $IMAGE_NAME 已存在"
read -p "是否重新构建?[y/N]: " rebuild
if [ "$rebuild" != "y" ] && [ "$rebuild" != "Y" ]; then
log_info "跳过构建,使用已有镜像"
return 0
fi
fi
log_step "准备构建目录..."
# 清理并创建构建目录
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
cp "$DOCKERFILE_SRC" "$BUILD_DIR/Dockerfile"
cp "$TENGINE_TAR" "$BUILD_DIR/"
# 创建 .dockerignore
cat > "$BUILD_DIR/.dockerignore" << 'DOCKERIGNORE'
*
!Dockerfile
!tengine-3.1.0.tar.gz
DOCKERIGNORE
log_step "开始构建镜像(预计 5-10 分钟)..."
cd "$BUILD_DIR"
if ${DOCKER_CMD} build -t "$IMAGE_NAME" . ; then
log_info "镜像构建成功: $IMAGE_NAME"
else
log_error "镜像构建失败"
exit 1
fi
}
# ==================== 停止旧容器 ====================
stop_old_container() {
log_step "停止旧容器..."
# 停止 utengine(如果正在运行)
if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "停止现有 ${CONTAINER_NAME} 容器..."
${DOCKER_CMD} stop "$CONTAINER_NAME" 2>/dev/null || true
${DOCKER_CMD} rm "$CONTAINER_NAME" 2>/dev/null || true
elif ${DOCKER_CMD} ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
${DOCKER_CMD} rm "$CONTAINER_NAME" 2>/dev/null || true
fi
# 停止旧 unginx(如果正在运行)
if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q "^${OLD_CONTAINER}$"; then
log_info "停止旧 ${OLD_CONTAINER} 容器..."
${DOCKER_CMD} stop "$OLD_CONTAINER"
log_info "${OLD_CONTAINER} 已停止(保留容器,可回滚用 ${DOCKER_CMD} start ${OLD_CONTAINER})"
fi
}
# ==================== 启动 Tengine 容器 ====================
start_tengine() {
log_step "启动 Tengine 容器..."
# 构建 docker run 命令
local run_cmd="${DOCKER_CMD} run -d --name ${CONTAINER_NAME}"
run_cmd+=" --restart always"
run_cmd+=" --network bridge"
run_cmd+=" -p ${HOST_PORT}:${CONTAINER_PORT}"
for mount in "${MOUNTS[@]}"; do
IFS=':' read -r src dest mode <<< "$mount"
if [ "$mode" = "ro" ]; then
run_cmd+=" -v ${src}:${dest}:ro"
else
run_cmd+=" -v ${src}:${dest}"
fi
done
run_cmd+=" --entrypoint /usr/local/tengine/sbin/nginx"
run_cmd+=" ${IMAGE_NAME}"
run_cmd+=" -g \"daemon off;\""
echo "执行命令: $run_cmd"
eval "$run_cmd"
sleep 2
# 检查容器是否启动
if ${DOCKER_CMD} ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "容器已启动"
else
log_error "容器启动失败"
${DOCKER_CMD} logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
}
# ==================== 配置 Tengine ====================
configure_tengine() {
log_step "配置 Tengine 主配置文件..."
# 写入主配置文件到临时文件
echo "$TENGINE_MAIN_CONF" > /tmp/tengine_main.conf
# 拷入容器
${DOCKER_CMD} cp /tmp/tengine_main.conf "${CONTAINER_NAME}:/etc/tengine/tengine.conf"
rm -f /tmp/tengine_main.conf
# 测试配置
if ${DOCKER_CMD} exec "$CONTAINER_NAME" nginx -t 2>&1; then
log_info "配置文件语法检查通过"
else
log_error "配置文件语法错误"
exit 1
fi
# 重载配置
${DOCKER_CMD} exec "$CONTAINER_NAME" nginx -s reload 2>&1
log_info "配置已重载"
}
# ==================== 验证部署 ====================
verify_deployment() {
log_step "验证部署..."
# 1. 容器状态
echo ""
echo "=== 容器状态 ==="
${DOCKER_CMD} ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 2. Tengine 版本
echo ""
echo "=== Tengine 版本 ==="
${DOCKER_CMD} exec "$CONTAINER_NAME" nginx -v 2>&1
# 3. HTTPS 连通性测试
echo ""
echo "=== HTTPS 连通性测试 ==="
if curl -skI --connect-timeout 5 https://127.0.0.1/ 2>&1 | head -5; then
log_info "HTTPS 响应正常"
else
log_warn "HTTPS 测试异常,请检查"
fi
# 4. Server 头验证
echo ""
SERVER_HEADER=$(curl -skI --connect-timeout 5 https://127.0.0.1/ 2>&1 | grep -i "Server:" || echo "")
if echo "$SERVER_HEADER" | grep -qi "Tengine"; then
log_info "Server 头确认为 Tengine: $SERVER_HEADER"
else
log_warn "Server 头未确认为 Tengine: $SERVER_HEADER"
fi
# 5. 编译模块
echo ""
echo "=== 编译的模块 ==="
${DOCKER_CMD} exec "$CONTAINER_NAME" nginx -V 2>&1
echo ""
log_info "======== 部署完成 ========"
echo ""
echo " 访问地址: https://192.168.9.76"
echo " 容器名称: ${CONTAINER_NAME}"
echo " 查看日志: ${DOCKER_CMD} logs -f ${CONTAINER_NAME}"
echo " 配置目录: /data/middleware/nginx/config/"
echo " 日志目录: /data/middleware/nginx/log/"
echo " 回滚命令: ${DOCKER_CMD} stop ${CONTAINER_NAME} && ${DOCKER_CMD} start ${OLD_CONTAINER}"
echo ""
}
# ==================== 主流程 ====================
main() {
echo ""
echo "============================================"
echo " Tengine 容器一键部署脚本 (ARM)"
echo " 目标服务器: $(hostname)"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================"
echo ""
preflight_check
build_image
stop_old_container
start_tengine
configure_tengine
verify_deployment
}
# 执行
main "$@"
# Tengine 容器部署文档 (ARM 架构)
## 环境信息
| 项目 | 详情 |
|------|------|
| 服务器 | 192.168.9.76 |
| OS | openEuler 24.03 (LTS-SP3) |
| 架构 | aarch64 (ARM) |
| 基础镜像 | nginx:1.30.2 (Debian Trixie, arm64) |
| Tengine 版本 | 3.1.0 |
| 兼容 Nginx | 1.24.0 |
| 镜像名 | `utengine:v1` |
| 容器名 | `utengine` |
| 部署日期 | 2026-07-06 |
> **注意**:ARM 服务器上 Docker 需要 `sudo` 权限(admin 用户)。
## 与 X86 部署的差异
| 项目 | X86 (5.70) | ARM (9.76) |
|------|-----------|------------|
| OS | UOS Server 20 | openEuler 24.03 |
| Docker 权限 | root 直接执行 | admin 需 `sudo` |
| unginx 挂载 config | ✅ 已挂载 | ❌ 未挂载,本次新增 |
| nginx 日志目录挂载 | `/var/log/tengine` | `/var/log/tengine`(原为 `/var/log/nginx`) |
| Dockerfile | 相同 | 相同(PCRE2 从源码编译) |
## 容器运行配置
### 端口映射
| 宿主机 | 容器 | 说明 |
|--------|------|------|
| 443 | 443 | HTTPS |
### 目录挂载
| 宿主机路径 | 容器路径 | 模式 | 说明 |
|-----------|----------|------|------|
| `/data/security/nginx_cert` | `/data/security/nginx_cert` | ro | SSL 证书 |
| `/data/services/web` | `/data/services/web` | rw | 前后端静态资源 |
| `/etc/localtime` | `/etc/localtime` | ro | 时区同步 |
| `/data/middleware/nginx/config` | `/etc/tengine/conf.d` | rw | Nginx 站点配置(⚠ ARM 新增) |
| `/data/middleware/nginx/data/html` | `/usr/local/tengine/html` | rw | 静态文件 |
| `/data/middleware/nginx/data/cache` | `/var/cache/nginx` | rw | 缓存目录 |
| `/data/middleware/nginx/log` | `/var/log/tengine` | rw | 日志目录 |
### 重启策略
`--restart always`
## 部署步骤
### 前置条件
- Docker 已安装,admin 用户可 `sudo docker`
- `nginx:1.30.2` 基础镜像已拉取
- `/data/` 下存在:
- `tengine-3.1.0.tar.gz` — Tengine 源码包
- `/data/middleware/nginx/` — 配置和日志目录结构
- `/data/security/nginx_cert/` — SSL 证书目录
- `/data/services/web/` — 前端静态资源
### 步骤 1:构建镜像
```bash
# 创建隔离构建目录
sudo mkdir -p /data/tengine_build
sudo cp /data/tengine-3.1.0.tar.gz /data/tengine_build/
# 上传 Dockerfile(或从本仓库复制)
# Dockerfile 内容见 deploy_tengine.sh 或 Dockerfile.utengine
# 添加 .dockerignore
sudo bash -c 'cat > /data/tengine_build/.dockerignore << EOF
*
!Dockerfile
!tengine-3.1.0.tar.gz
EOF'
# 构建镜像
cd /data/tengine_build
sudo docker build -t utengine:v1 .
```
### 步骤 2:停止旧 Nginx
```bash
sudo docker stop unginx
```
### 步骤 3:启动 Tengine 容器
```bash
sudo docker run -d \
--name utengine \
--restart always \
--network bridge \
-p 443:443 \
-v /data/security/nginx_cert:/data/security/nginx_cert:ro \
-v /data/services/web:/data/services/web \
-v /etc/localtime:/etc/localtime:ro \
-v /data/middleware/nginx/config:/etc/tengine/conf.d \
-v /data/middleware/nginx/data/html:/usr/local/tengine/html \
-v /data/middleware/nginx/data/cache:/var/cache/nginx \
-v /data/middleware/nginx/log:/var/log/tengine \
--entrypoint /usr/local/tengine/sbin/nginx \
utengine:v1 \
-g "daemon off;"
```
### 步骤 4:覆盖主配置文件
```bash
# 写入配置(见下方模板)
cat > /tmp/tengine_main.conf << 'CONF'
user root;
worker_processes auto;
error_log /var/log/tengine/error.log notice;
pid /var/run/tengine.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/tengine/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/tengine/conf.d/*.conf;
}
CONF
# 拷入容器并重载
sudo docker cp /tmp/tengine_main.conf utengine:/etc/tengine/tengine.conf
sudo docker exec utengine nginx -t && sudo docker exec utengine nginx -s reload
```
### 步骤 5:验证
```bash
# 容器状态
sudo docker ps --filter name=utengine
# HTTPS 测试
curl -skI https://127.0.0.1/
# Server 头确认
curl -skI https://127.0.0.1/ 2>&1 | grep "Server:"
# 版本
sudo docker exec utengine nginx -v
# 编译模块
sudo docker exec utengine nginx -V
```
## 常用运维命令
```bash
# 停止/启动/重启
sudo docker stop utengine
sudo docker start utengine
sudo docker restart utengine
# 查看日志
sudo docker logs -f utengine
sudo tail -f /data/middleware/nginx/log/access.log
sudo tail -f /data/middleware/nginx/log/error.log
# 配置重载
sudo docker exec utengine nginx -t
sudo docker exec utengine nginx -s reload
# 进入容器
sudo docker exec -it utengine /bin/bash
# 回滚
sudo docker stop utengine && sudo docker start unginx
```
## 注意事项
1. **Docker sudo**:ARM 服务器上 admin 用户需 `sudo` 执行 docker 命令
2. **config 挂载**:ARM 的 unginx 原未挂载 `/data/middleware/nginx/config`,本次 tengine 部署已新增此挂载,使配置修改后可直接 `nginx -s reload` 生效
3. **二进制名**:Tengine 安装的二进制为 `nginx`,需覆盖 entrypoint
4. **PCRE2**:同 X86,需从源码编译
5. **站点配置**`unified443.conf``server_name``192.168.9.76`,已匹配 ARM 服务器 IP
# Tengine 负载均衡配置指导文档
## 当前架构分析
### 现有代理模式
当前 `unified443.conf` 中所有后端服务都通过 `proxy_pass http://172.17.0.1:PORT` 直连单个 Docker 宿主机 IP,例如:
```nginx
# Nacos
location /nacos/ {
proxy_pass http://172.17.0.1:8848/nacos/;
}
# Java 后端
location /api/ {
proxy_pass http://172.17.0.1:8000/;
}
# EMQX WebSocket
location /mqtt {
proxy_pass http://172.17.0.1:8083/mqtt;
}
```
**问题**:所有请求指向同一台宿主机的同一端口,没有冗余,无法利用 Tengine 的负载均衡能力。
### Tengine 负载均衡增强模块(已编译)
相比原生 Nginx,Tengine 3.1.0 自带以下增强模块可用于负载均衡:
| 模块 | 功能 |
|------|------|
| `ngx_http_upstream_check_module` | 主动健康检查(TCP/HTTP 探测),自动剔除故障节点 |
| `ngx_http_upstream_session_sticky_module` | 会话保持(Cookie/URL 参数路由到固定后端) |
| `ngx_http_upstream_consistent_hash_module` | 一致性哈希(减少节点增减时的缓存失效) |
| `ngx_http_upstream_dynamic_module` | 动态 upstream(运行时通过 HTTP API 增删后端节点) |
| `ngx_http_upstream_keepalive_module` | 后端连接池复用 |
| `ngx_http_reqstat_module` | 请求统计(按 upstream/server 维度展示 QPS/延迟/状态码) |
## 单机负载均衡配置
### 场景:Java 后端多实例
假设宿主机上有多个 Java 后端实例(不同端口),配置如下:
```nginx
# 定义 upstream 池
upstream java_backend {
# 负载均衡算法(默认加权轮询)
# 可选: ip_hash / least_conn / hash $request_uri consistent
# 加权轮询(weight 越大分配越多)
server 172.17.0.1:8000 weight=3 max_fails=3 fail_timeout=30s;
server 172.17.0.1:8001 weight=2 max_fails=3 fail_timeout=30s;
server 172.17.0.1:8002 weight=1 max_fails=3 fail_timeout=30s backup; # 备用节点
# 连接池复用
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 100;
}
server {
listen 443 ssl;
# ...
location /api/ {
proxy_pass http://java_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
```
### Tengine 增强:主动健康检查
这是 Tengine 相比原生 Nginx 最大的优势——可以在 upstream 层面配置主动探测:
```nginx
upstream java_backend {
server 172.17.0.1:8000 weight=3;
server 172.17.0.1:8001 weight=2;
server 172.17.0.1:8002 weight=1;
keepalive 32;
# ========== Tengine 健康检查 ==========
# HTTP 探测:每隔 5 秒请求 /health,2 次成功认为上线,2 次失败认为下线
check interval=5000 rise=2 fall=2 timeout=3000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
```
### Tengine 增强:会话保持
```nginx
upstream java_backend {
server 172.17.0.1:8000;
server 172.17.0.1:8001;
# Cookie 会话保持:浏览器首次请求时下发 cookie,后续请求路由到同一后端
session_sticky cookie=backend_route mode=insert maxage=3600;
# 或 URL 参数模式
# session_sticky param=route mode=rewrite;
}
```
### Tengine 增强:一致性哈希
```nginx
upstream java_backend {
# 按请求 URI 哈希,同一 URI 始终打到同一节点
consistent_hash $request_uri;
server 172.17.0.1:8000;
server 172.17.0.1:8001;
}
```
## 多机负载均衡配置(双机热备场景)
### 场景:两台服务器均部署 Java 后端
```
┌─────────────────────────────────────┐
│ Tengine (VIP) │
│ 192.168.5.70:443 │
└──────────┬──────────┬───────────────┘
│ │
┌──────▼──┐ ┌───▼──────┐
│ 5.70 │ │ 9.76 │
│ :8000 │ │ :8000 │
└─────────┘ └──────────┘
```
```nginx
upstream java_backend {
# 跨服务器负载
server 192.168.5.70:8000 weight=5; # 主节点,权重高
server 192.168.9.76:8000 weight=3; # 备节点
server 192.168.9.76:8001 backup; # 纯备用
keepalive 32;
# Tengine 健康检查
check interval=3000 rise=2 fall=3 timeout=2000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
# 上游端口代理(如 Nacos、EMQX 等同理)
upstream nacos_cluster {
server 192.168.5.70:8848;
server 192.168.9.76:8848;
check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;
}
upstream emqx_cluster {
server 192.168.5.70:8083;
server 192.168.9.76:8083;
check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;
}
```
### 完整示例:改造 unified443.conf 的 proxy_pass
将原来直连的:
```nginx
location /api/ {
proxy_pass http://172.17.0.1:8000/;
}
```
改为引用 upstream:
```nginx
location /api/ {
proxy_pass http://java_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
```
## 负载均衡算法对比
| 算法 | 配置 | 适用场景 |
|------|------|----------|
| 加权轮询(默认) | 默认 | 后端性能不均时用 weight 调节 |
| `ip_hash` | `ip_hash;` | 需要客户端 IP 会话保持 |
| `least_conn` | `least_conn;` | 长连接场景(WebSocket/MQTT) |
| `hash` | `hash $request_uri consistent;` | 缓存命中率优先 |
| `random` | `random two least_conn;` | 大规模集群,选两个随机挑连接最少的 |
## 关键参数说明
| 参数 | 说明 | 推荐值 |
|------|------|--------|
| `weight=N` | 权重,默认 1 | 按性能比例 |
| `max_fails=N` | N 次失败后标记不可用 | 3 |
| `fail_timeout=Ns` | 不可用状态的持续时间 | 30s |
| `backup` | 标记为备用节点(仅其他全部不可用时启用) | 按需 |
| `down` | 手动标记下线(维护时用) | 按需 |
| `keepalive N` | 到后端的空闲连接池大小 | 32 |
| `check interval` | 健康检查间隔(毫秒) | 3000-5000 |
| `rise/fall` | 连续成功/失败 N 次后变更状态 | 2/3 |
## 监控请求统计
Tengine 的 `ngx_http_reqstat_module` 可以实时查看各 upstream 的请求分布:
```nginx
# 在 http 块中开启
http {
reqstat_zone backend "$host" 10m;
server {
listen 80;
# 统计展示页面
location /reqstat {
reqstat;
allow 127.0.0.1;
allow 192.168.0.0/16;
deny all;
}
}
}
```
访问 `/reqstat` 可看到:
- 每个 server 的请求数、字节数
- 响应时间分布
- 各 HTTP 状态码数量
## 实施建议
### 优先级
| 优先级 | 配置项 | 改动范围 | 收益 |
|--------|--------|----------|------|
| P0 | 核心 API upstream + 健康检查 | `unified443.conf` | 自动剔除故障节点 |
| P1 | 多服务器负载 | upstream 中加对端 IP | 双机冗余 |
| P2 | 会话保持 | 加 `session_sticky` | WebSocket/MQTT 不断连 |
| P3 | 请求统计 | 加 `reqstat` | 可观测性 |
### 注意事项
1. **WebSocket 代理**`/mqtt` 等 WebSocket 路径代理到 upstream 时需保持 HTTP/1.1 和 Upgrade 头
2. **健康检查端口**:TCP 检查需要后端端口可达;HTTP 检查需要后端有 `/health` 端点
3. **配置变更流程**:修改配置后必须 `nginx -t` 测试语法 → `nginx -s reload` 热重载
4. **双机热备**:配合 Keepalived VIP 时,Tengine 的 upstream 指向真实 IP 而非 VIP,避免环路
5. **现有配置兼容**:可将 upstream 定义放在独立文件 `/etc/tengine/conf.d/upstreams.conf`,在 `tengine.conf` 的 http 块中 `include` 加载,保持 `unified443.conf` 不变
FROM nginx:1.30.2
# 安装编译依赖(Trixie 移除了 libpcre3-dev,需手动编译 PCRE2)
RUN apt-get update && apt-get install -y \
gcc \
make \
libc-dev \
libssl-dev \
zlib1g-dev \
pkg-config \
curl \
&& rm -rf /var/lib/apt/lists/*
# 下载并编译安装 PCRE2(Trixie 仓库缺少 libpcre2-dev)
ARG PCRE2_VERSION=10.45
RUN curl -sSL https://github.com/PCRE2Project/pcre2/releases/download/pcre2-${PCRE2_VERSION}/pcre2-${PCRE2_VERSION}.tar.gz -o /tmp/pcre2.tar.gz && \
cd /tmp && tar xzf pcre2.tar.gz && \
cd pcre2-${PCRE2_VERSION} && \
./configure --prefix=/usr && \
make -j$(nproc) && make install && \
cd /tmp && rm -rf pcre2-${PCRE2_VERSION} pcre2.tar.gz
# 拷贝 tengine 源码包并编译安装
COPY tengine-3.1.0.tar.gz /tmp/
WORKDIR /tmp
RUN tar xzf tengine-3.1.0.tar.gz && \
cd tengine-3.1.0 && \
./configure \
--prefix=/usr/local/tengine \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--with-http_sub_module \
--with-stream \
--with-stream_ssl_module \
--conf-path=/etc/tengine/tengine.conf \
--error-log-path=/var/log/tengine/error.log \
--http-log-path=/var/log/tengine/access.log \
--pid-path=/var/run/tengine.pid \
&& make -j$(nproc) && make install && \
cd /tmp && rm -rf tengine-3.1.0 tengine-3.1.0.tar.gz
# 创建运行时目录
RUN mkdir -p /etc/tengine/conf.d /var/log/tengine /usr/local/tengine/html && \
chmod 755 /var/log/tengine
# 生成自签名 SSL 证书(如无正式证书可替换)
RUN mkdir -p /etc/tengine/ssl && \
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/tengine/ssl/utengine.key \
-out /etc/tengine/ssl/utengine.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=UTengine/CN=utengine"
# 将 tengine 二进制加入 PATH
ENV PATH="/usr/local/tengine/sbin:${PATH}"
EXPOSE 80 443
STOPSIGNAL SIGQUIT
CMD ["tengine", "-g", "daemon off;"]
#!/bin/bash
# ============================================================
# Tengine 容器一键部署脚本
# 适用于:UOS Server 20 / 麒麟 V10 (x86_64)
# 版本:1.0
# 日期:2026-07-06
# ============================================================
set -e
# ==================== 配置变量 ====================
IMAGE_NAME="utengine:v1"
CONTAINER_NAME="utengine"
OLD_CONTAINER="unginx"
BUILD_DIR="/data/tengine_build"
DOCKERFILE_SRC="/data/Dockerfile.utengine"
TENGINE_TAR="/data/tengine-3.1.0.tar.gz"
# 容器运行参数
HOST_PORT="443"
CONTAINER_PORT="443"
# 挂载映射(宿主机:容器:模式)
# 格式: "source:dest:mode" (mode: ro/rw)
declare -a MOUNTS=(
"/data/security/nginx_cert:/data/security/nginx_cert:ro"
"/data/services/web:/data/services/web:rw"
"/etc/localtime:/etc/localtime:ro"
"/data/middleware/nginx/config:/etc/tengine/conf.d:rw"
"/data/middleware/nginx/data/html:/usr/local/tengine/html:rw"
"/data/middleware/nginx/data/cache:/var/cache/nginx:rw"
"/data/middleware/nginx/log:/var/log/tengine:rw"
)
# Tengine 主配置文件(覆盖默认配置)
TENGINE_MAIN_CONF='user root;
worker_processes auto;
error_log /var/log/tengine/error.log notice;
pid /var/run/tengine.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '"'"'$remote_addr - $remote_user [$time_local] "$request" '"'"'
'"'"'$status $body_bytes_sent "$http_referer" '"'"'
'"'"'"$http_user_agent" "$http_x_forwarded_for"'"'"';
access_log /var/log/tengine/access.log main;
sendfile on;
keepalive_timeout 65;
# 加载站点配置
include /etc/tengine/conf.d/*.conf;
}'
# ==================== 颜色输出 ====================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
# ==================== 前置检查 ====================
preflight_check() {
log_step "执行前置检查..."
# 检查 Docker
if ! command -v docker &> /dev/null; then
log_error "Docker 未安装"
exit 1
fi
# 检查基础镜像
if ! docker image inspect nginx:1.30.2 &> /dev/null; then
log_error "基础镜像 nginx:1.30.2 不存在"
exit 1
fi
# 检查源码包
if [ ! -f "$TENGINE_TAR" ]; then
log_error "Tengine 源码包不存在: $TENGINE_TAR"
exit 1
fi
if [ ! -f "$DOCKERFILE_SRC" ]; then
log_error "Dockerfile 不存在: $DOCKERFILE_SRC"
exit 1
fi
# 检查挂载源目录
for mount in "${MOUNTS[@]}"; do
IFS=':' read -r src dest mode <<< "$mount"
if [ ! -d "$src" ] && [ ! -f "$src" ]; then
log_error "挂载源路径不存在: $src"
exit 1
fi
done
# 检查端口是否被占用
if ss -tlnp | grep -q ":${HOST_PORT} "; then
log_warn "端口 ${HOST_PORT} 已被占用,将先停止旧容器"
fi
log_info "前置检查通过"
}
# ==================== 构建镜像 ====================
build_image() {
# 如果镜像已存在,询问是否重建
if docker image inspect "$IMAGE_NAME" &> /dev/null; then
log_warn "镜像 $IMAGE_NAME 已存在"
read -p "是否重新构建?[y/N]: " rebuild
if [ "$rebuild" != "y" ] && [ "$rebuild" != "Y" ]; then
log_info "跳过构建,使用已有镜像"
return 0
fi
fi
log_step "准备构建目录..."
# 清理并创建构建目录
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
cp "$DOCKERFILE_SRC" "$BUILD_DIR/Dockerfile"
cp "$TENGINE_TAR" "$BUILD_DIR/"
# 创建 .dockerignore
cat > "$BUILD_DIR/.dockerignore" << 'DOCKERIGNORE'
*
!Dockerfile
!tengine-3.1.0.tar.gz
DOCKERIGNORE
log_step "开始构建镜像(预计 5-10 分钟)..."
cd "$BUILD_DIR"
if docker build -t "$IMAGE_NAME" . ; then
log_info "镜像构建成功: $IMAGE_NAME"
else
log_error "镜像构建失败"
exit 1
fi
}
# ==================== 停止旧容器 ====================
stop_old_container() {
log_step "停止旧容器..."
# 停止 utengine(如果正在运行)
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "停止现有 ${CONTAINER_NAME} 容器..."
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
docker rm "$CONTAINER_NAME" 2>/dev/null || true
fi
# 停止旧 unginx(如果正在运行)
if docker ps --format '{{.Names}}' | grep -q "^${OLD_CONTAINER}$"; then
log_info "停止旧 ${OLD_CONTAINER} 容器..."
docker stop "$OLD_CONTAINER"
log_info "${OLD_CONTAINER} 已停止(保留容器,可回滚用 docker start ${OLD_CONTAINER})"
fi
}
# ==================== 启动 Tengine 容器 ====================
start_tengine() {
log_step "启动 Tengine 容器..."
# 构建 docker run 命令
local run_cmd="docker run -d --name ${CONTAINER_NAME}"
run_cmd+=" --restart always"
run_cmd+=" --network bridge"
run_cmd+=" -p ${HOST_PORT}:${CONTAINER_PORT}"
for mount in "${MOUNTS[@]}"; do
IFS=':' read -r src dest mode <<< "$mount"
if [ "$mode" = "ro" ]; then
run_cmd+=" -v ${src}:${dest}:ro"
else
run_cmd+=" -v ${src}:${dest}"
fi
done
run_cmd+=" --entrypoint /usr/local/tengine/sbin/nginx"
run_cmd+=" ${IMAGE_NAME}"
run_cmd+=" -g \"daemon off;\""
echo "执行命令: $run_cmd"
eval "$run_cmd"
sleep 2
# 检查容器是否启动
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log_info "容器已启动"
else
log_error "容器启动失败"
docker logs "$CONTAINER_NAME" 2>&1 | tail -20
exit 1
fi
}
# ==================== 配置 Tengine ====================
configure_tengine() {
log_step "配置 Tengine 主配置文件..."
# 写入主配置文件到临时文件
echo "$TENGINE_MAIN_CONF" > /tmp/tengine_main.conf
# 拷入容器
docker cp /tmp/tengine_main.conf "${CONTAINER_NAME}:/etc/tengine/tengine.conf"
rm -f /tmp/tengine_main.conf
# 测试配置
if docker exec "$CONTAINER_NAME" nginx -t 2>&1; then
log_info "配置文件语法检查通过"
else
log_error "配置文件语法错误"
exit 1
fi
# 重载配置
docker exec "$CONTAINER_NAME" nginx -s reload 2>&1
log_info "配置已重载"
}
# ==================== 验证部署 ====================
verify_deployment() {
log_step "验证部署..."
# 1. 容器状态
echo ""
echo "=== 容器状态 ==="
docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
# 2. Tengine 版本
echo ""
echo "=== Tengine 版本 ==="
docker exec "$CONTAINER_NAME" nginx -v 2>&1
# 3. HTTPS 连通性测试
echo ""
echo "=== HTTPS 连通性测试 ==="
if curl -skI --connect-timeout 5 https://127.0.0.1/ 2>&1 | head -5; then
log_info "HTTPS 响应正常"
else
log_warn "HTTPS 测试异常,请检查"
fi
# 4. Server 头验证
echo ""
SERVER_HEADER=$(curl -skI --connect-timeout 5 https://127.0.0.1/ 2>&1 | grep -i "Server:" || echo "")
if echo "$SERVER_HEADER" | grep -qi "Tengine"; then
log_info "Server 头确认为 Tengine: $SERVER_HEADER"
else
log_warn "Server 头未确认为 Tengine: $SERVER_HEADER"
fi
# 5. 编译模块
echo ""
echo "=== 编译的模块 ==="
docker exec "$CONTAINER_NAME" nginx -V 2>&1
echo ""
log_info "======== 部署完成 ========"
echo ""
echo " 访问地址: https://192.168.5.70"
echo " 容器名称: ${CONTAINER_NAME}"
echo " 查看日志: docker logs -f ${CONTAINER_NAME}"
echo " 配置目录: /data/middleware/nginx/config/"
echo " 日志目录: /data/middleware/nginx/log/"
echo " 回滚命令: docker stop ${CONTAINER_NAME} && docker start ${OLD_CONTAINER}"
echo ""
}
# ==================== 主流程 ====================
main() {
echo ""
echo "============================================"
echo " Tengine 容器一键部署脚本"
echo " 目标服务器: $(hostname)"
echo " 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================"
echo ""
preflight_check
build_image
stop_old_container
start_tengine
configure_tengine
verify_deployment
}
# 执行
main "$@"
# Tengine 容器部署文档
## 环境信息
| 项目 | 详情 |
|------|------|
| 服务器 | 192.168.5.70 |
| OS | UOS Server 20 (x86_64) |
| 基础镜像 | nginx:1.30.2 (Debian Trixie) |
| Tengine 版本 | 3.1.0 |
| 兼容 Nginx | 1.24.0 |
| 镜像名 | `utengine:v1` |
| 容器名 | `utengine` |
| 部署日期 | 2026-07-06 |
## 编译的模块
通过 `nginx -V` 确认已编译模块:
```
Tengine version: Tengine/3.1.0
nginx version: nginx/1.24.0
built by gcc 14.2.0 (Debian 14.2.0-19)
built with OpenSSL 3.5.6 7 Apr 2026
TLS SNI support enabled
```
### 核心模块
| 模块 | 说明 |
|------|------|
| `http_ssl_module` | HTTPS/SSL 支持 |
| `http_v2_module` | HTTP/2 协议支持 |
| `http_realip_module` | 真实客户端 IP 获取 |
| `http_stub_status_module` | 状态监控页面 |
| `http_gzip_static_module` | 静态文件 Gzip 压缩 |
| `http_sub_module` | 响应内容替换 |
| `stream` | TCP/UDP 四层代理 |
| `stream_ssl_module` | Stream SSL/TLS 支持 |
### Tengine 内置模块(默认编译)
Tengine 3.1.0 自动包含以下增强模块:
| 模块 | 说明 |
|------|------|
| `ngx_http_upstream_check_module` | 上游服务健康检查 |
| `ngx_http_upstream_session_sticky_module` | 会话保持 |
| `ngx_http_upstream_dynamic_module` | 动态 upstream |
| `ngx_http_upstream_consistent_hash_module` | 一致性哈希 |
| `ngx_http_reqstat_module` | 请求统计监控 |
| `ngx_http_sysguard_module` | 系统过载保护 |
| `ngx_http_concat_module` | CSS/JS 合并 |
| `ngx_http_footer_filter_module` | 响应页脚注入 |
| `ngx_http_trim_filter_module` | 空白过滤 |
| `ngx_http_user_agent_module` | UA 过滤 |
| `ngx_http_slice_module` | 大文件分片 |
| `ngx_http_lua_module` | Lua 脚本支持 |
| `ngx_http_proxy_connect_module` | HTTPS CONNECT 代理 |
| `ngx_debug_pool` / `ngx_debug_timer` / `ngx_debug_conn` | 调试工具 |
| `ngx_slab_stat` | 共享内存统计 |
| `ngx_backtrace_module` | 崩溃回溯 |
## 容器运行配置
### 端口映射
| 宿主机 | 容器 | 说明 |
|--------|------|------|
| 443 | 443 | HTTPS |
### 目录挂载
| 宿主机路径 | 容器路径 | 模式 | 说明 |
|-----------|----------|------|------|
| `/data/middleware/nginx/config` | `/etc/tengine/conf.d` | rw | Nginx 站点配置 |
| `/data/middleware/nginx/data/html` | `/usr/local/tengine/html` | rw | 静态文件 |
| `/data/middleware/nginx/data/cache` | `/var/cache/nginx` | rw | 缓存目录 |
| `/data/middleware/nginx/log` | `/var/log/tengine` | rw | 日志目录 |
| `/data/security/nginx_cert` | `/data/security/nginx_cert` | ro | SSL 证书 |
| `/data/services/web` | `/data/services/web` | rw | 前后端静态资源 |
| `/etc/localtime` | `/etc/localtime` | ro | 时区同步 |
### 重启策略
`--restart always`:容器退出后自动重启,Docker 启动时自动启动。
## 部署步骤
### 前置条件
- 宿主机已安装 Docker
- 存在 nginx:1.30.2 基础镜像
- `/data/` 下存在以下文件/目录:
- `tengine-3.1.0.tar.gz` — Tengine 源码包
- `Dockerfile.utengine` — Docker 构建文件
- `/data/middleware/nginx/` — 配置和日志目录结构
- `/data/security/nginx_cert/` — SSL 证书目录
- `/data/services/web/` — 前端静态资源
### 步骤 1:构建镜像
```bash
# 创建隔离构建目录(避免上下文过大)
mkdir -p /data/tengine_build
cp /data/Dockerfile.utengine /data/tengine_build/Dockerfile
cp /data/tengine-3.1.0.tar.gz /data/tengine_build/
# 添加 .dockerignore 防止打包无关文件
cat > /data/tengine_build/.dockerignore << 'EOF'
*
!Dockerfile
!tengine-3.1.0.tar.gz
EOF
# 构建镜像
cd /data/tengine_build
docker build -t utengine:v1 .
```
### 步骤 2:停止旧 Nginx
```bash
docker stop unginx
```
### 步骤 3:启动 Tengine 容器
```bash
docker run -d \
--name utengine \
--restart always \
--network bridge \
-p 443:443 \
-v /data/security/nginx_cert:/data/security/nginx_cert:ro \
-v /data/services/web:/data/services/web \
-v /etc/localtime:/etc/localtime:ro \
-v /data/middleware/nginx/config:/etc/tengine/conf.d \
-v /data/middleware/nginx/data/html:/usr/local/tengine/html \
-v /data/middleware/nginx/data/cache:/var/cache/nginx \
-v /data/middleware/nginx/log:/var/log/tengine \
--entrypoint /usr/local/tengine/sbin/nginx \
utengine:v1 \
-g "daemon off;"
```
### 步骤 4:覆盖主配置文件
Tengine 默认 `tengine.conf` 缺少 `include conf.d/*.conf`,需要用以下内容覆盖 `/etc/tengine/tengine.conf`
```nginx
user root;
worker_processes auto;
error_log /var/log/tengine/error.log notice;
pid /var/run/tengine.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/tengine/access.log main;
sendfile on;
keepalive_timeout 65;
# 加载站点配置
include /etc/tengine/conf.d/*.conf;
}
```
```bash
# 将配置文件拷入容器并重载
docker cp /tmp/tengine_main.conf utengine:/etc/tengine/tengine.conf
docker exec utengine nginx -t && docker exec utengine nginx -s reload
```
### 步骤 5:验证
```bash
# 检查容器状态
docker ps --filter name=utengine
# 测试 HTTPS 访问
curl -skI https://127.0.0.1/
# 验证响应头包含 Server: Tengine
curl -skI https://127.0.0.1/ 2>&1 | grep "Server:"
# 查看版本
docker exec utengine nginx -v
# 查看编译模块
docker exec utengine nginx -V
```
## 常用运维命令
```bash
# 停止/启动/重启
docker stop utengine
docker start utengine
docker restart utengine
# 查看日志
docker logs -f utengine
tail -f /data/middleware/nginx/log/access.log
tail -f /data/middleware/nginx/log/error.log
# 配置测试(修改配置后先测试再重载)
docker exec utengine nginx -t
docker exec utengine nginx -s reload
# 进入容器
docker exec -it utengine /bin/bash
# 删除容器(需要先停止)
docker stop utengine && docker rm utengine
# 删除镜像
docker rmi utengine:v1
```
## 注意事项
1. **二进制名称**:Tengine 安装的二进制名为 `nginx`(不是 `tengine`),位于 `/usr/local/tengine/sbin/nginx`,因此需要覆盖 entrypoint 使用完整路径
2. **PCRE2 依赖**:nginx:1.30.2 基于 Debian Trixie,该版本移除了 `libpcre3-dev`,Dockerfile 中已通过编译 PCRE2 源码解决
3. **配置兼容**:现有 `unified443.conf``moblie8081.conf` 可直接被 Tengine 兼容使用,无需修改
4. **端口 80**:当前未暴露 HTTP 80 端口,仅开放 HTTPS 443(与 unginx 一致)
5. **回滚**:如需回滚到原 nginx,执行 `docker stop utengine && docker start unginx`
# Tengine 负载均衡配置指导文档
## 当前架构分析
### 现有代理模式
当前 `unified443.conf` 中所有后端服务都通过 `proxy_pass http://172.17.0.1:PORT` 直连单个 Docker 宿主机 IP,例如:
```nginx
# Nacos
location /nacos/ {
proxy_pass http://172.17.0.1:8848/nacos/;
}
# Java 后端
location /api/ {
proxy_pass http://172.17.0.1:8000/;
}
# EMQX WebSocket
location /mqtt {
proxy_pass http://172.17.0.1:8083/mqtt;
}
```
**问题**:所有请求指向同一台宿主机的同一端口,没有冗余,无法利用 Tengine 的负载均衡能力。
### Tengine 负载均衡增强模块(已编译)
相比原生 Nginx,Tengine 3.1.0 自带以下增强模块可用于负载均衡:
| 模块 | 功能 |
|------|------|
| `ngx_http_upstream_check_module` | 主动健康检查(TCP/HTTP 探测),自动剔除故障节点 |
| `ngx_http_upstream_session_sticky_module` | 会话保持(Cookie/URL 参数路由到固定后端) |
| `ngx_http_upstream_consistent_hash_module` | 一致性哈希(减少节点增减时的缓存失效) |
| `ngx_http_upstream_dynamic_module` | 动态 upstream(运行时通过 HTTP API 增删后端节点) |
| `ngx_http_upstream_keepalive_module` | 后端连接池复用 |
| `ngx_http_reqstat_module` | 请求统计(按 upstream/server 维度展示 QPS/延迟/状态码) |
## 单机负载均衡配置
### 场景:Java 后端多实例
假设宿主机上有多个 Java 后端实例(不同端口),配置如下:
```nginx
# 定义 upstream 池
upstream java_backend {
# 负载均衡算法(默认加权轮询)
# 可选: ip_hash / least_conn / hash $request_uri consistent
# 加权轮询(weight 越大分配越多)
server 172.17.0.1:8000 weight=3 max_fails=3 fail_timeout=30s;
server 172.17.0.1:8001 weight=2 max_fails=3 fail_timeout=30s;
server 172.17.0.1:8002 weight=1 max_fails=3 fail_timeout=30s backup; # 备用节点
# 连接池复用
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 100;
}
server {
listen 443 ssl;
# ...
location /api/ {
proxy_pass http://java_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
```
### Tengine 增强:主动健康检查
这是 Tengine 相比原生 Nginx 最大的优势——可以在 upstream 层面配置主动探测:
```nginx
upstream java_backend {
server 172.17.0.1:8000 weight=3;
server 172.17.0.1:8001 weight=2;
server 172.17.0.1:8002 weight=1;
keepalive 32;
# ========== Tengine 健康检查 ==========
# HTTP 探测:每隔 5 秒请求 /health,2 次成功认为上线,2 次失败认为下线
check interval=5000 rise=2 fall=2 timeout=3000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
```
### Tengine 增强:会话保持
```nginx
upstream java_backend {
server 172.17.0.1:8000;
server 172.17.0.1:8001;
# Cookie 会话保持:浏览器首次请求时下发 cookie,后续请求路由到同一后端
session_sticky cookie=backend_route mode=insert maxage=3600;
# 或 URL 参数模式
# session_sticky param=route mode=rewrite;
}
```
### Tengine 增强:一致性哈希
```nginx
upstream java_backend {
# 按请求 URI 哈希,同一 URI 始终打到同一节点
consistent_hash $request_uri;
server 172.17.0.1:8000;
server 172.17.0.1:8001;
}
```
## 多机负载均衡配置(双机热备场景)
### 场景:两台服务器均部署 Java 后端
```
┌─────────────────────────────────────┐
│ Tengine (VIP) │
│ 192.168.5.70:443 │
└──────────┬──────────┬───────────────┘
│ │
┌──────▼──┐ ┌───▼──────┐
│ 5.70 │ │ 9.76 │
│ :8000 │ │ :8000 │
└─────────┘ └──────────┘
```
```nginx
upstream java_backend {
# 跨服务器负载
server 192.168.5.70:8000 weight=5; # 主节点,权重高
server 192.168.9.76:8000 weight=3; # 备节点
server 192.168.9.76:8001 backup; # 纯备用
keepalive 32;
# Tengine 健康检查
check interval=3000 rise=2 fall=3 timeout=2000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
# 上游端口代理(如 Nacos、EMQX 等同理)
upstream nacos_cluster {
server 192.168.5.70:8848;
server 192.168.9.76:8848;
check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;
}
upstream emqx_cluster {
server 192.168.5.70:8083;
server 192.168.9.76:8083;
check interval=5000 rise=2 fall=3 timeout=3000 type=tcp;
}
```
### 完整示例:改造 unified443.conf 的 proxy_pass
将原来直连的:
```nginx
location /api/ {
proxy_pass http://172.17.0.1:8000/;
}
```
改为引用 upstream:
```nginx
location /api/ {
proxy_pass http://java_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
```
## 负载均衡算法对比
| 算法 | 配置 | 适用场景 |
|------|------|----------|
| 加权轮询(默认) | 默认 | 后端性能不均时用 weight 调节 |
| `ip_hash` | `ip_hash;` | 需要客户端 IP 会话保持 |
| `least_conn` | `least_conn;` | 长连接场景(WebSocket/MQTT) |
| `hash` | `hash $request_uri consistent;` | 缓存命中率优先 |
| `random` | `random two least_conn;` | 大规模集群,选两个随机挑连接最少的 |
## 关键参数说明
| 参数 | 说明 | 推荐值 |
|------|------|--------|
| `weight=N` | 权重,默认 1 | 按性能比例 |
| `max_fails=N` | N 次失败后标记不可用 | 3 |
| `fail_timeout=Ns` | 不可用状态的持续时间 | 30s |
| `backup` | 标记为备用节点(仅其他全部不可用时启用) | 按需 |
| `down` | 手动标记下线(维护时用) | 按需 |
| `keepalive N` | 到后端的空闲连接池大小 | 32 |
| `check interval` | 健康检查间隔(毫秒) | 3000-5000 |
| `rise/fall` | 连续成功/失败 N 次后变更状态 | 2/3 |
## 监控请求统计
Tengine 的 `ngx_http_reqstat_module` 可以实时查看各 upstream 的请求分布:
```nginx
# 在 http 块中开启
http {
reqstat_zone backend "$host" 10m;
server {
listen 80;
# 统计展示页面
location /reqstat {
reqstat;
allow 127.0.0.1;
allow 192.168.0.0/16;
deny all;
}
}
}
```
访问 `/reqstat` 可看到:
- 每个 server 的请求数、字节数
- 响应时间分布
- 各 HTTP 状态码数量
## 实施建议
### 优先级
| 优先级 | 配置项 | 改动范围 | 收益 |
|--------|--------|----------|------|
| P0 | 核心 API upstream + 健康检查 | `unified443.conf` | 自动剔除故障节点 |
| P1 | 多服务器负载 | upstream 中加对端 IP | 双机冗余 |
| P2 | 会话保持 | 加 `session_sticky` | WebSocket/MQTT 不断连 |
| P3 | 请求统计 | 加 `reqstat` | 可观测性 |
### 注意事项
1. **WebSocket 代理**`/mqtt` 等 WebSocket 路径代理到 upstream 时需保持 HTTP/1.1 和 Upgrade 头
2. **健康检查端口**:TCP 检查需要后端端口可达;HTTP 检查需要后端有 `/health` 端点
3. **配置变更流程**:修改配置后必须 `nginx -t` 测试语法 → `nginx -s reload` 热重载
4. **双机热备**:配合 Keepalived VIP 时,Tengine 的 upstream 指向真实 IP 而非 VIP,避免环路
5. **现有配置兼容**:可将 upstream 定义放在独立文件 `/etc/tengine/conf.d/upstreams.conf`,在 `tengine.conf` 的 http 块中 `include` 加载,保持 `unified443.conf` 不变
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论