当前位置: 首页 > news >正文

Jenkins 安全清理孤立工作区(workspace)的 Shell 脚本:原理、实现与实战

前言:如果你是 Jenkins 运维或开发人员,大概率遇到过这样的窘境:
某天监控突然告警“Jenkins 服务器磁盘使用率超 90%”,登录服务器排查,却发现 workspace 目录下堆积了上百个陌生文件夹——它们对应的任务早就被删除,却像“隐形垃圾”一样占据着几十甚至几百 GB 空间。
更让人头疼的是,很多人误以为“Jenkins 配置‘只保留最近 N 次构建’就能自动清理 workspace”,但实际上,这个策略仅会删除构建历史记录和制品文件,对存储构建中间产物、依赖缓存的 workspace 目录“视而不见”。
手动清理?风险太高——一旦误删正在运行的任务 workspace,可能导致构建失败;逐一审核目录是否“有用”?效率低下,尤其对有上百个任务的 Jenkins 集群来说,简直是“体力活”。
正是为了解决这个“痛点”,我们开发了这套 Jenkins 孤立工作区安全清理脚本。它不只是一个简单的删除工具,更像是为 Jenkins 量身定制的“磁盘管家”:默认“预演不执行”,通过多层安全校验(进程占用、目录老化、Jenkins 活跃构建检测)避免误删,还能生成可追溯的计划与日志,让清理工作“安全、可控、可审计”。
接下来,我们从问题背景、脚本功能、使用方法到原理拆解,一步步带你掌握这套清理方案,彻底告别 Jenkins 磁盘“臃肿”难题。

适用环境:Linux 上的 Jenkins Controller 或 Agent
目标:自动发现并清理“孤立”的 workspace 目录(对应的 Jenkins Job 已删除或不再存在)

  • Jenkins 设置“只保留最近 N 次构建”不会自动清理 workspace
  • 这篇文章提供一个安全优先的 Shell 脚本:默认 Dry-Run,多重安全检查(年龄、占用、白/黑名单、Jenkins API 活动构建检查),二次确认,生成计划与日志,才会执行删除。
  • 覆盖 Folder/Multibranch 的命名(%2F)和 @tmp/@2 等后缀的处理。

⚠️ 重要提示:操作前务必对目标工作区目录进行完整备份

由于脚本涉及文件系统的删除操作,为避免意外情况(如误删仍在使用的工作区、环境差异导致的非预期行为等),请在执行任何清理动作前,通过 tarrsync 等工具对 Jenkins 工作区根目录(或相关子目录)创建完整归档备份,确保数据具备可恢复性。

一、背景与问题

  • Jenkins 的构建保留策略numToKeepStr 等)只会清理 构建历史和制品(artifacts),不会自动清理 workspace
  • workspace 目录通常在:
    $JENKINS_HOME/workspace/<job-full-name-with-%2F>
    
    或者在 Agent 节点相应路径。
  • 如果大量 Job 被删除或迁移,可能遗留很多孤立工作区目录(包含 @tmp@2@script 等后缀目录),占用大量磁盘。
  • 团队需要一个安全可审计的自动清理工具。

二、脚本能做什么(功能清单)

  • 发现现存 Job 的“期望工作区名集合”(递归扫描 $JENKINS_HOME/jobs,处理多层 Folder)。
  • $WORKSPACE_ROOT 的实际目录对比,找出疑似孤立的工作区目录。
  • 默认 Dry-run:只生成清理计划,不做删除。
  • 安全保障
    • 老化阈值(例如只删“最近 3 天以外”的目录)。
    • 进程占用检测lsoffuser)。
    • Jenkins API 检查是否有正在构建(可选)。
    • 白/黑名单正则过滤。
    • 删除需二次确认(输入候选数量)。
  • 审计输出:计划文件 + 删除日志文件。
  • 仅在对应 Job 已不存在时,才考虑清理 *@tmp(可选开关)。

三、安全设计(为什么它安全)

  1. 默认预演(Dry-run),不碰任何文件。
  2. 老化检查:避免清理刚产生/仍在使用的目录(默认 3 天)。
  3. 占用检查:检测是否有进程占用目录,避免误删。
  4. Jenkins API 检测:发现活动构建就退出(可选)。
  5. 白/黑名单:逐步放开,防止“一刀切”。
  6. 交互确认:必须手动输入候选数量才会删除。
  7. 计划与日志:可审计、可回溯。

四、前置条件与环境

  • 操作系统:Linux(Bash 环境)
  • 执行用户:建议 jenkins 或具备删除权限的用户(谨慎使用 root)
  • 命令工具:bashfindstatdatesedawk 等基础工具
  • 可选工具:lsoffuser(占用检查,不装也可用,但安全性降低)
  • 可选 Jenkins API:curl + JENKINS_URL/JENKINS_USER/JENKINS_API_TOKEN

五、脚本源码

文件名建议:safe-clean-orphan-workspaces.sh

#!/usr/bin/env bash
# safe-clean-orphan-workspaces.sh
# Safely detect and (optionally) clean Jenkins orphaned workspaces.
# Author: You & Copilot
# License: MITset -Eeuo pipefail# -------- Defaults (override via CLI flags) --------
WORKSPACE_ROOT="${WORKSPACE_ROOT:-/data1/var/lib/jenkins/workspace}"
JENKINS_HOME="${JENKINS_HOME:-/data1/var/lib/jenkins}"
OLDER_THAN_DAYS="${OLDER_THAN_DAYS:-3}"     # only delete if dir mtime older than this many days
APPLY=false                                 # dry-run by default
INCLUDE_TMP=false                           # also delete *@tmp when base is orphan
CHECK_OPEN_FILES=true                       # use lsof/fuser if available
VERBOSE=false
EXCLUDE_PATTERN=""                          # e.g. "^(keep-this|dont-touch-.*)$" (regex on dirname)
INCLUDE_PATTERN=""                          # optional regex to narrow candidates
PLAN_DIR="${PLAN_DIR:-./}"                  # where to write plan & logs# Optional: if set, we try to detect active builds and abort if any
JENKINS_URL="${JENKINS_URL:-}"              # e.g. http://127.0.0.1:8080
JENKINS_USER="${JENKINS_USER:-}"            # user for API
JENKINS_API_TOKEN="${JENKINS_API_TOKEN:-}"  # API token# --------- Helpers ----------
log() { printf '%s\n' "$*" >&2; }
vlog() { $VERBOSE && printf '[verbose] %s\n' "$*" >&2 || true; }usage() {cat <<'EOF'
Usage:safe-clean-orphan-workspaces.sh [options]Options:--workspace-root PATH     Workspace root (default: /data1/var/lib/jenkins/workspace)--jenkins-home PATH       Jenkins home (default: /data1/var/lib/jenkins)--older-than DAYS         Only delete dirs older than DAYS (default: 3)--include-tmp             Also delete *@tmp when base job is orphan (default: off)--no-open-files-check     Skip lsof/fuser check (default: on)--include-regex REGEX     Only consider dirnames matching this regex--exclude-regex REGEX     Skip dirnames matching this regex--plan-dir PATH           Where to write the plan & logs (default: ./)--verbose                 More logs--apply                   Actually delete (requires interactive confirm)-h, --help                Show this helpOptional safety integration with Jenkins API (to avoid cleaning during active builds):env JENKINS_URL, JENKINS_USER, JENKINS_API_TOKENExamples:Dry-run (preview):./safe-clean-orphan-workspaces.sh --workspace-root /data1/var/lib/jenkins/workspace \--jenkins-home /data1/var/lib/jenkins --verboseApply (after preview looks good):./safe-clean-orphan-workspaces.sh --applyEOF
}while [[ $# -gt 0 ]]; docase "$1" in--workspace-root) WORKSPACE_ROOT="$2"; shift 2;;--jenkins-home)   JENKINS_HOME="$2"; shift 2;;--older-than)     OLDER_THAN_DAYS="$2"; shift 2;;--include-tmp)    INCLUDE_TMP=true; shift;;--no-open-files-check) CHECK_OPEN_FILES=false; shift;;--include-regex)  INCLUDE_PATTERN="$2"; shift 2;;--exclude-regex)  EXCLUDE_PATTERN="$2"; shift 2;;--plan-dir)       PLAN_DIR="$2"; shift 2;;--verbose)        VERBOSE=true; shift;;--apply)          APPLY=true; shift;;-h|--help)        usage; exit 0;;*) log "Unknown arg: $1"; usage; exit 1;;esac
donetimestamp="$(date +'%Y%m%d-%H%M%S')"
PLAN_FILE="${PLAN_DIR%/}/orphan-workspaces-plan-${timestamp}.txt"
LOG_FILE="${PLAN_DIR%/}/orphan-workspaces-delete-${timestamp}.log"# ---- Preflight checks ----
[[ -d "$WORKSPACE_ROOT" ]] || { log "Workspace root not found: $WORKSPACE_ROOT"; exit 1; }
[[ -d "$JENKINS_HOME/jobs" ]] || { log "Jenkins jobs dir not found: $JENKINS_HOME/jobs"; exit 1; }if [[ -n "$JENKINS_URL" && -n "$JENKINS_USER" && -n "$JENKINS_API_TOKEN" ]]; thenlog "Checking Jenkins executors for active builds via API..."# crumb not required for GET; we only look for '"building":true'if curl -fsS -u "${JENKINS_USER}:${JENKINS_API_TOKEN}" \"$JENKINS_URL/computer/api/json?depth=1" | grep -q '"building":true'; thenlog "Detected active builds. For safety, aborting now. (Unset JENKINS_* to skip this check.)"exit 1elsevlog "No active builds reported by Jenkins API."fi
fi# ---- Build expected workspace basenames set from $JENKINS_HOME/jobs ----
# For any config.xml under jobs/..../jobs/<name>/..., we extract each <name> after '/jobs/' and join with '%2F'.
declare -A EXPECTED
vlog "Discovering existing job full names under: $JENKINS_HOME/jobs"# We intentionally avoid parsing XML; we rely on folder structure.
while IFS= read -r -d '' cfg; dorel="${cfg#$JENKINS_HOME/jobs/}"         # strip prefix# Extract every component that follows '/jobs/' (e.g., jobs/f1/jobs/f2/jobs/jobA/config.xml => f1 f2 jobA)# shellcheck disable=SC2001names=$(sed -E 's#(^|/)jobs/#\n#g' <<<"$rel" | tr '\n' ' ' | awk '{for(i=1;i<=NF;i++) print $i}' | sed -E 's#/.*##')# join with %2Ffull=""for n in $names; do[[ -z "$n" ]] && continueif [[ -z "$full" ]]; then full="$n"; else full="${full}%2F${n}"; fidone[[ -n "$full" ]] && EXPECTED["$full"]=1
done < <(find "$JENKINS_HOME/jobs" -type f -name 'config.xml' -print0)vlog "Total expected workspace basenames discovered: ${#EXPECTED[@]}"# ---- Enumerate actual workspace dirs ----
log "Scanning workspace root: $WORKSPACE_ROOT"
mapfile -d '' DIRS < <(find "$WORKSPACE_ROOT" -mindepth 1 -maxdepth 1 -type d -print0)# Prepare plan & header
{echo "# Orphan workspace deletion plan @ ${timestamp}"echo "# WORKSPACE_ROOT=$WORKSPACE_ROOT"echo "# JENKINS_HOME=$JENKINS_HOME"echo "# OLDER_THAN_DAYS=$OLDER_THAN_DAYS  INCLUDE_TMP=$INCLUDE_TMP  APPLY=$APPLY"echo "# INCLUDE_PATTERN=${INCLUDE_PATTERN:-<none>}  EXCLUDE_PATTERN=${EXCLUDE_PATTERN:-<none>}"echoprintf "%-8s | %-50s | %-19s | %s\n" "CANDID." "DIRNAME" "LAST_MODIFIED" "FULL_PATH"echo "---------+----------------------------------------------------+---------------------+----------------------------------------"
} > "$PLAN_FILE"candidates=0
skipped=0
considered=0is_in_use() {local d="$1"if ! $CHECK_OPEN_FILES; then return 1; fiif command -v lsof >/dev/null 2>&1; thentimeout 5s lsof +D "$d" >/dev/null 2>&1 && return 0 || return 1elif command -v fuser >/dev/null 2>&1; thenfuser -s "$d" 2>/dev/null && return 0 || return 1elsereturn 1fi
}for d in "${DIRS[@]}"; do((considered++)) || truename="$(basename "$d")"base="${name%%@*}"   # strip '@...' suffix (covers @tmp, @2, @script, etc.)# Include/Exclude filtersif [[ -n "$INCLUDE_PATTERN" ]]; thenif ! [[ "$name" =~ $INCLUDE_PATTERN ]]; then((skipped++)) || true; $VERBOSE && vlog "Skip by include-regex: $name"; continuefifiif [[ -n "$EXCLUDE_PATTERN" ]]; thenif [[ "$name" =~ $EXCLUDE_PATTERN ]]; then((skipped++)) || true; $VERBOSE && vlog "Skip by exclude-regex: $name"; continuefifi# If base is expected -> not orphan; skip (and do not touch its @tmp)if [[ -n "${EXPECTED[$base]:-}" ]]; then((skipped++)) || truevlog "Keeps (expected job): $name"continuefi# If it's an @tmp (or @N) but base exists in workspace (rare), still treat via base rule above.# Now base is NOT expected -> consider orphan# Age checkif ! find "$d" -prune -mtime +"$OLDER_THAN_DAYS" | grep -q .; then((skipped++)) || truevlog "Skip (too new, <= ${OLDER_THAN_DAYS}d): $name"continuefi# If this is a pure '@tmp' of an existing base? Already excluded by EXPECTED check.# For extra safety: if name ends with '@tmp' and INCLUDE_TMP=false, skip.if [[ "$name" == *"@tmp" && "$INCLUDE_TMP" != true ]]; then((skipped++)) || truevlog "Skip @tmp (INCLUDE_TMP=false): $name"continuefi# Check if directory is being used by some process (best-effort)if is_in_use "$d"; then((skipped++)) || truevlog "Skip (in use by process): $name"continuefi# Record candidatelm="$(date -d "@$(stat -c %Y "$d")" +'%F %T' 2>/dev/null || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$d")"printf "%-8s | %-50s | %-19s | %s\n" "YES" "$name" "$lm" "$d" >> "$PLAN_FILE"((candidates++)) || true
done# Report summary
{echoecho "# Summary:"echo "# Considered: $considered"echo "# Skipped:    $skipped"echo "# Candidates: $candidates"
} >> "$PLAN_FILE"log "Plan written to: $PLAN_FILE"
log "Candidates found: $candidates"if ! $APPLY; thenlog "Dry-run only. Review the plan above. Re-run with --apply to actually delete."exit 0
fi# Apply mode: ask for interactive confirmation
echo
echo "🚨 You are about to DELETE $candidates directories listed in:"
echo "    $PLAN_FILE"
read -r -p "Type exactly the number of candidates ($candidates) to confirm: " confirm
if [[ "$confirm" != "$candidates" ]]; thenlog "Confirmation failed. Aborting."exit 1
fi# Execute deletions according to plan file
deleted=0
failed=0
echo "# Delete log @ $timestamp" > "$LOG_FILE"while IFS= read -r line; do# Only lines marked as candidate ("YES | ... | ... | /full/path")[[ "$line" =~ ^YES\ \ \ \ \ \ \ \ \ \|\  ]] || continuepath="$(awk -F '|' '{print $4}' <<<"$line" | sed -E 's#^[[:space:]]+##')"if [[ -d "$path" ]]; thenif rm -rf --one-file-system -- "$path"; thenecho "[OK] $(date +'%F %T') Deleted: $path" | tee -a "$LOG_FILE"((deleted++)) || trueelseecho "[ERR] $(date +'%F %T') FAILED:  $path" | tee -a "$LOG_FILE"((failed++)) || truefielseecho "[SKIP] $(date +'%F %T') Missing (already gone): $path" | tee -a "$LOG_FILE"fi
done < "$PLAN_FILE"echo
log "Deletion finished. Deleted=$deleted, Failed=$failed"
log "Log written to: $LOG_FILE"

六、工作原理详解

1)构建“期望存在的工作区名集合”

  • Jenkins 在 $JENKINS_HOME/jobs 下按 Folder/Job 分层保存:jobs/<folder>/jobs/<sub-folder>/jobs/<job>/config.xml
  • 脚本不解析 XML,而是利用目录结构:遇到每个 config.xml,就把路径中的层级名提取出来并用 %2F 连接(Jenkins 会把 Job 全名中的斜杠替换为 %2F 作为 workspace 名称的一部分)。
  • 例如:jobs/TeamA/jobs/Service/jobs/build-api/config.xmlTeamA%2FService%2Fbuild-api

2)枚举实际的工作区目录

  • 枚举 $WORKSPACE_ROOT(如 /data1/var/lib/jenkins/workspace)下一层目录,每个目录名可能是:
    • 标准 workspace 名:TeamA%2FService%2Fbuild-api
    • 临时/派生目录:xxx@tmpxxx@2xxx@script
  • 将目录名按 @ 切分取“基础名”(base="${name%%@*}"),用于与“期望集合”匹配。

3)筛选与安全过滤

  • 包含/排除正则:你可以只针对特定目录处理(--include-regex)或排除一些目录(--exclude-regex)。
  • 年龄:只清理修改时间超过 N 天的目录(--older-than)。
  • @tmp 策略:默认不清理 *@tmp;若加 --include-tmp,仅在对应基础名不在期望集合里(即 Job 真正不存在)时才会清理。

4)占用检测

  • 如果安装了 lsoffuser,脚本会检查目录是否被进程使用。被占用 → 跳过。

5)Jenkins API 检测(可选)

  • 如果配置了 JENKINS_URL/JENKINS_USER/JENKINS_API_TOKEN,脚本会访问:
    /computer/api/json?depth=1
    
    若发现 "building": true(有活跃构建),直接退出,避免在构建高峰清理。

6)计划文件与汇总

  • Dry-run 阶段会生成一个 plan 文件orphan-workspaces-plan-*.txt),列出候选目录、最后修改时间、完整路径及汇总信息。

7)执行与日志

  • --apply 会进入执行模式,需要二次确认(输入候选数量)。
  • 删除动作与结果写入 删除日志orphan-workspaces-delete-*.log)。

七、使用指南(完整范例)

1)创建与赋权

nano safe-clean-orphan-workspaces.sh    # 粘贴脚本
chmod +x safe-clean-orphan-workspaces.sh

2)Dry-run(默认仅预览)

./safe-clean-orphan-workspaces.sh \--workspace-root /data1/var/lib/jenkins/workspace \--jenkins-home   /data1/var/lib/jenkins \--verbose

输出中会提示计划文件位置,打开查看候选列表。

3)加过滤器(逐步放开)

  • 只清理包含关键词的目录:
./safe-clean-orphan-workspaces.sh --include-regex 'Webhook|Timer'
  • 排除某类:
./safe-clean-orphan-workspaces.sh --exclude-regex '^(keep-.*|do-not-touch)$'

4)清理 *@tmp(可选)

./safe-clean-orphan-workspaces.sh --include-tmp

只有当对应基础工作区名不在期望集合里(即 Job 已删除)时才会考虑清理 @tmp

5)调整“老化天数”

./safe-clean-orphan-workspaces.sh --older-than 7

6)启用 Jenkins API 安全检测(推荐)

export JENKINS_URL="http://127.0.0.1:8080"
export JENKINS_USER="jenkins-admin"
export JENKINS_API_TOKEN="********"
./safe-clean-orphan-workspaces.sh

一旦检测到 "building": true,脚本会退出

7)真正执行删除

./safe-clean-orphan-workspaces.sh --apply

会要求你输入候选数量以确认。执行结果会写入日志 orphan-workspaces-delete-*.log

8)自动化(建议仍然保留 Dry-run + 人工审核)

  • 生成计划(每天凌晨 2 点):
# /etc/crontab
0 2 * * * jenkins /path/safe-clean-orphan-workspaces.sh --plan-dir /var/log/jenkins-clean-plans > /var/log/jenkins-clean-cron.log 2>&1
  • 由管理员审核计划文件后,手动执行 --apply

八、常见场景与建议

  • 多分支/PR(Multibranch):每个分支/PR 都是一个“Job 全名”,脚本会正确处理 %2F 命名映射。
  • 自定义 workspace:如果某些 Job 使用了“自定义 workspace”(不在标准路径/命名),脚本可能无法关联。建议对这类路径手工确认或通过 --include/--exclude-regex 精准控制。
  • 多节点(Agent):每个 Agent 有自己的 workspace 根目录,需要在各节点分别运行脚本。
  • 缓存策略node_modules/.m2/.gradle 等缓存很大。若你想保留以加速构建,不建议“每次构建后清理 workspace”。可以只定期清理孤儿目录。
  • 与 WS Cleanup 插件配合:日常构建层面用 cleanWs() 处理临时文件;存量垃圾、孤儿目录用本文脚本处理。
  • 权限与安全:尽量以 jenkins 用户执行,避免 root;生产环境先 Dry-run,确认后再 Apply。
  • 回滚与审计:删除本质不可逆;通过计划与日志实现事前/事后审计。关键服务器建议配合快照或备份策略

九、故障排查(FAQ)

  1. 计划里一个候选都没有?
    • 说明没有孤儿目录,或 --older-than 太大,或正则过滤过于严格。
  2. 全都提示 “too new”?
    • 调小 --older-than,或再等几天。
  3. lsof / fuser 不可用?
    • 占用检查会自动跳过(安全性降低),建议安装 lsof 或改为 --no-open-files-check 明确跳过。
  4. 目录名有空格/特殊字符?
    • 脚本对路径做了适当引用和 -print0 读取,通常安全。仍建议避免手工改名。
  5. 没有生成计划文件?
    • 检查 --plan-dir 路径是否存在/可写;查看标准输出中的报错信息。
  6. Jenkins API 认证失败?
    • 确认 URL 正确、用户与 Token 有权限访问 /computer/api/json,网络可达。

十、扩展

A)只清理“孤儿 @tmp”的极简命令

在确认 Job 已删除的前提下清理所有 @tmp 残留(仍建议先 Dry-run):

find /data1/var/lib/jenkins/workspace -maxdepth 1 -type d -name '*@tmp' -mtime +1 -print
# 确认后:
find /data1/var/lib/jenkins/workspace -maxdepth 1 -type d -name '*@tmp' -mtime +1 -exec rm -rf {} +

这个方法不检查对应 Job 是否存在,不如本文脚本安全,除非你已在 Jenkins 中确认 Job 确实删除。

B)Groovy 版本(思路)

  • 在 Jenkins Script Console 中用 Groovy 调用 Jenkins 内部 API,可以直接获取 Job 与 workspace 的真实映射,精度更高,能跨控制器/节点执行。但需要管理员权限,且必须非常谨慎。
  • 建议 Groovy 版本仅用于打印候选清单,删除仍通过你完全理解的 Shell 脚本执行。

十一、完整使用流程建议(SOP)

  1. Dry-run 生成计划文件 → 人工审核
  2. (可选)重复 Dry-run,添加 --include-regex / --exclude-regex 精准控制。
  3. 非工作时段确保无构建(或启用 API 检查)时,执行 --apply
  4. 归档计划与日志以便审计。
  5. 周期性重复:结合 cron 实现“先计划后审批”的流程。

十二、结语

上面这套方案,既解决了 Jenkins 长期运行后 workspace 垃圾堆积 的痛点,又通过多层安全策略确保稳、准、狠

  • 先发现,再确认;
  • 不在高峰时段下手;
  • 有日志可查;
  • 避免影响仍在使用的目录。
    在这里插入图片描述
http://www.dtcms.com/a/389008.html

相关文章:

  • WebDancer论文阅读
  • Node.js、npm 和 npx:前端开发的三剑客
  • Node.js 创建 UDP 服务
  • 【NodeJS 二维码】node.js 怎样读取二维码信息?
  • IRN论文阅读笔记
  • pacote:Node.js 生态中的包获取工具
  • 使用 Ansible 管理 Docker 容器:开关机、定时开关机及 VNC 控制
  • 【Spring AI】实现一个基于 Streamable HTTP 的 MCP Server
  • 云手机:概念、历史、内容与发展战略
  • linux服务器上安装oss对象存储(命令行工具使用oss)
  • 强化学习1.1 使用Gymnasium库
  • 日语学习-日语知识点小记-进阶-JLPT-N1阶段蓝宝书,共120语法(11):101-110语法 +(考え方15)
  • 运维分享:神卓 N600 如何实现 NAS 安全稳定访问
  • 系统集成项目管理工程师:第十四章 收尾过程组
  • 云手机通道具体是指什么?
  • C++ :实现多线程编程
  • 嵌入式科普(40)浅谈“功能安全“概念,深悟“功能安全“本质
  • 分布式系统理论-CAP和BASE
  • SaaS 安全的原则、挑战及其最佳实践指南
  • Flink on Native K8S源码解析
  • VMwarea安装
  • HarmonyOS之Swiper全解析
  • React18中性能优化方式
  • X133核心板--智能教育平板的芯动力​
  • 下载flink和flink cdc jar
  • 华为三层交换技术
  • 潮起之江:算力创新与赋能开启AI产业新征程
  • 华为链路聚合技术基础
  • 百度智能云车牌识别API官方配置指南
  • Git 拉Github的仓库却要求登录GitLab