上周三晚上十一点,我还在盯着 Jenkins 的构建队列发呆——三个 Java 服务同时发版,一台 Master 又要编译又要打镜像又要推部署,队列从"等待中"排到了第十八位,测试同学在群里连着问了三遍"还要多久"。那天之后我做的第一件事,就是把编译和部署这两类活儿拆出去,给 Jenkins 创建了第一批专用节点,让它们各自认领标签、并行干活。这篇就聊透一件事:Jenkins 创建节点并连接,从头到尾怎么落地。不管你是刚开始接触 jenkins教程 的新手,还是已经在跑 jenkins自动化部署、想把构建能力横向扩一扩的老手,这里面的字段含义、连接方式选型、密钥配置、报错排查,我都会按我实际操作的顺序讲一遍,能直接抄的配置我都贴出来。
1. 先想清楚:为什么非得给 Jenkins 加节点
1.1 单机 Master 撑不住的三道坎
很多人搭 Jenkins 的第一天,就是把 Master 当万能机器用:拉代码、跑单测、打 jar、构建 Docker 镜像、推镜像仓库、SSH 到目标服务器重启服务,全在这一个实例上完成。小团队、低频发版的时候这么干确实省事,但只要你踩过下面三道坎中的任意一道,就会开始琢磨加节点这件事。
第一道坎是算力争抢。Jenkins 本身是个 Java 进程,构建任务也是一个个 Java 或者 shell 子进程,编译大型项目时 CPU 和内存会被瞬间抽干。这时候你会发现 Jenkins 的 Web 界面点一下卡三秒,日志刷新转圈圈,甚至出现 Master 直接被 OOM Killer 干掉的情况。第二道坎是环境冲突。前端项目要 Node 18,老项目只能跑 Node 14,Python 项目之间还互相嫌弃虚拟环境版本,全塞在一台机器上迟早打架。第三道坎是环境异构,比如你的构建产物需要在 Windows 上跑 MSBuild,或者需要一台 macOS 机器做特定打包任务,那 Master 是 Linux 就彻底没辙了。
节点(Node)这个东西,本质就是把"执行构建"这件事从 Master 上剥离出去。Jenkins 的架构天生就是 Master-Agent 模型:Master 负责调度、存配置、展示界面、记录构建历史,Agent(也就是节点)负责真正跑活儿。这个拆分不是可有可无的优化,而是这套工具设计时就留好的扩展口。
1.2 节点能解决的三类问题,以及解决不了什么
先说节点能解决的。算力横向扩展是最直接的收益:一台 16 核的机器加三个 8 核节点,并行构建能力直接翻倍多,发版日排队的问题几乎立刻缓解。构建环境隔离是第二个收益:我把前端构建固定在标签为node-frontend的节点上,Node 版本锁死;把 Java 构建固定在jdk17标签的节点上,各版本 JDK 各回各家,谁也别污染谁。异构平台支持是第三个收益:Windows 节点跑 MSBuild 和批处理脚本,Linux 节点跑 shell 和 Docker 构建,需要一个特殊 ARM 环境就单独挂一个 ARM 节点,非常灵活。
但节点解决不了什么,这点我必须提前泼盆冷水。节点不会让单条流水线本身变快,它只是让多条流水线能同时跑;节点也不解决磁盘空间问题,如果你的构建产物堆在节点本地不清理,节点照样会被塞满;节点更不解决网络问题,如果节点拉代码要跨机房,那拉代码的时间反而更长。所以加节点之前,先确保你的痛点确实属于"资源不够"或者"环境冲突",而不是"流水线写得有问题"。
提示:判断是否需要加节点有个很实用的信号——看 Jenkins 首页的"构建执行状态"里,队列长度是否长期大于执行器总数。如果长期是这样,加节点就是对的;如果队列很空但单条构建特别慢,那问题在流水线本身,加节点没用。
2. 动手前必须敲定的四件事
2.1 连接方式怎么选:SSH 还是 Inbound
Jenkins 创建节点时,最核心的一个选择就是"启动方式"(Launch method)。这个选项决定了 Master 和节点之间谁主动发起连接,也决定了后面所有的配置细节。
SSH 方式是 Master 主动去连节点,用的是标准 SSH 协议,走 22 端口(或者你自己改的端口)。它的优点是配置简单、稳定性好、日志直观,节点机器只要开了 SSH 服务并配好密钥就行,不需要在节点上常驻一个 Java 客户端进程。缺点也很明确:Master 必须能直接访问节点的 SSH 端口,而且必须在节点上有一个可用的登录账号。我把公司内网里的 Linux 构建机全部用这种方式接入,一共十几个节点,两年下来基本没出过连接层面的问题。
Inbound 方式(界面上叫"Launch agent by connecting it to the controller",老版本叫 JNLP)是反过来的:节点主动去连 Master。它的优势是穿透性强,节点不需要开放任何入站端口,只要节点能访问 Master 的地址就行,特别适合那种"节点在内网隔离区、Master 在外围"的网络结构。代价是要在节点上跑一个常驻的 agent 进程(agent.jar),并且要用 systemd 之类的工具做成服务,不然连接一断就得手动重来。
选型的判断标准其实只有一条:谁在网络拓扑上更容易被连到。Master 能直连节点 SSH,就选 SSH;节点只能单向访问 Master,就选 Inbound。两种方式的字段配置、日志位置都不一样,后面我会分别走一遍。
2.2 Java 版本对齐,这是最容易翻车的前置条件
Jenkins 2.357 之后,Master 和 Agent 的最低 Java 要求都提到了 Java 11,更新的版本(2.463 之后)已经要求 Java 17。这里有一个特别常见的误解:很多人以为节点上装的 Java 版本只要比 Master 低一点没关系,实际上Agent 的 Java 版本必须满足它所连接的那版 Jenkins 的最低要求,否则连接会在启动后几秒内断开,日志里抛出一堆UnsupportedClassVersionError。
我一般的处理方式是:在节点机器上装一个与 Master 主版本一致的 JDK(比如都用 Temurin 17),而不是用系统自带的 OpenJDK。Ubuntu 上执行java -version看看当前版本,Ubuntu 22.04 自带的是 Java 11,如果 Master 是要求 Java 17 的新版本,就得手动装。安装完之后要确认JAVA_HOME指向正确的路径,因为 SSH 方式启动节点时,Jenkins 会用你配置的javaPath或者 PATH 里的 java,找错版本就直接连不上。
另外,节点上的 Java 只是用来跑 Agent 进程的,跟你的项目构建用哪个 JDK 是两码事。项目构建的 JDK 可以通过 Jenkins 的"全局工具配置"指定,也可以直接在节点上用 Docker 隔离,不要混为一谈。
2.3 执行器数量、远程工作目录和标签怎么规划
执行器数量(# of executors)决定了这个节点能同时跑几个任务。这个数字不是越大越好。我的算法是:先看节点的物理内存,减去系统占用,再除以"单次构建的峰值内存",取小值;同时不超过 CPU 核数。举个例子,一台 32GB 内存、16 核的机器,系统和其他服务占 8GB,单次 Maven 构建峰值大概 2.5GB,那 24 / 2.5 ≈ 9,但更保守点我一般设 4 到 6,因为并发构建时磁盘 IO 和网络带宽也会互相压制。设成 16 个执行器看起来能跑很多任务,实际结果是所有任务一起变慢。
远程工作目录(Remote root directory)是节点上存放工作空间的路径,比如/data/jenkins-agent。这个目录必须存在、必须属于运行 Agent 的账号、必须有足够空间。我踩过的坑是用默认路径导致构建产物堆在系统盘上,某天日志暴涨把/撑满,整台机器上的所有服务都开始报错。所以从第一天起就把工作目录放到独立的数据盘上,并且给它设个监控。
标签(Labels)是调度的核心。标签本质上是给节点打的分类记号,流水线里写agent { label 'jdk17 && linux' }就能精确指定到某个节点。我的习惯是至少打三类标签:操作系统(linux / windows)、工具链(jdk17 / node18 / docker)、用途(build / deploy / test)。标签不要用中文,不要带空格,用短横线或者下划线连接,这样在流水线的表达式里最不容易出错。
2.4 凭据设计:专门建账号,别用 root
这一步是我见过最多人偷懒的地方。用 root 连 SSH 确实一路顺畅,但后果是构建脚本里任何一个rm -rf都可能是灾难级的,而且 root 的密钥一旦泄露,整台机器的边界就没了。正确做法是创建一个专用的jenkins用户,给它配置好工作目录的读写权限,再按需给它有限的 sudo 权限。
具体来说,我会给这个用户配免密 sudo 到特定命令,比如只有systemctl restart xxx.service这一条,而不是给全量 sudo。配置写在/etc/sudoers.d/jenkins里,用visudo -f编辑,避免把主配置文件写坏。至于 SSH 密钥,统一在 Master 上用一个专用密钥对,私钥加密后存进 Jenkins 凭据库,不要在多个节点之间复用同一把私钥,更不要把私钥直接放在节点的authorized_keys之外的地方。
3. 跟做一遍:SSH 方式创建并连接 Linux 节点
3.1 节点机器前置准备
假设我们有一台干净的内网 Ubuntu 机器,IP 是10.0.20.31,目标是把它的前八个核和 16GB 内存贡献给构建。整套前置准备分四步,每步我都会说明为什么要做。
先用 root 或有 sudo 权限的账号创建 jenkins 用户并建工作目录:
sudo useradd -m -s /bin/bash jenkins sudo mkdir -p /data/jenkins-agent sudo chown -R jenkins:jenkins /data/jenkins-agent-m的作用是自动创建家目录,因为 SSH 密钥默认放~/.ssh,没有家目录会直接连不上。工作目录单独放/data,是为了让它和系统盘分离,这条我在上一节已经解释过原因。
接着装 JDK。以 Temurin 17 为例,Ubuntu 上的标准做法是加仓库再装:
sudo apt update sudo apt install -y wget apt-transport-https gnupg wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/adoptium.gpg echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list sudo apt update sudo apt install -y temurin-17-jdk装完务必验证java -version和which java,把路径记下来,待会儿创建节点时要填。如果你的环境不能直连外网,那就用离线包解压安装,把 tar.gz 解到/opt再配JAVA_HOME,效果一样。
然后是给 jenkins 用户配 SSH 密钥登录。在节点上先切换到该用户,把 Master 生成的公钥写进去:
sudo su - jenkins mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "ssh-ed25519 AAAAC3N...你的公钥内容" >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys权限位这两个数字不是可选项,SSH 服务端对~/.ssh和authorized_keys的权限检查非常严格,权限过宽会直接拒绝登录。这一步和下面生成密钥的顺序其实可以互换,只要最终两端匹配就行。
3.2 在 Master 上生成密钥并创建凭据
登录 Master 的服务器(或者用有权访问 Jenkins 家目录的账号),切换到 Jenkins 运行用户,生成专用密钥:
sudo su - jenkins ssh-keygen -t ed25519 -C "jenkins-agent-key" -f ~/.ssh/id_ed25519_agent -N "" cat ~/.ssh/id_ed25519_agent.pub-N ""表示私钥不加口令,这样 Jenkins 连接时可以全自动,不需要人工输入。安全性靠的是私钥文件本身的权限和服务器访问控制,所以这个私钥文件权限必须是 600,绝不能放到代码仓库里。
打开 Jenkins 界面,进入Manage Jenkins→Credentials→System→Global credentials,点Add Credentials。类型选SSH Username with private key,ID 起个有意义的名字比如agent-10.0.20.31-ssh,Username 填jenkins,Private Key 选Enter directly把刚生成的私钥内容粘贴进去。ID 一定要有意义,因为后面在流水线里可能会用credentials()引用它,随手打的 ID 过两周你自己都不认识。
注意:粘贴私钥时要包含完整的
-----BEGIN OPENSSH PRIVATE KEY-----和-----END OPENSSH PRIVATE KEY-----两行,少一行 Jenkins 会报凭据无法解析,而且报错信息很不友好,只会告诉你连接失败。
3.3 在 Manage Nodes 里新建节点并逐字段填写
进入Manage Jenkins→Nodes(新版本在Manage Jenkins→System下的 Nodes 区域,或者直接访问/computer/),点New Node。填写名称,比如linux-build-01,勾选Permanent Agent(永久节点),确定后进入详细配置页。
页面上的字段看起来多,实际必须理解的就七八个,我按重要性排一遍:
| 字段 | 建议值 | 说明 |
|---|---|---|
| Name | linux-build-01 | 节点名,全唯一,用短横线不用中文 |
| Remote root directory | /data/jenkins-agent | 必须是已存在且 jenkins 用户可写的绝对路径 |
| Labels | linux jdk17 build | 空格分隔,流水线按这个匹配 |
| Usage | 尽量使用这个节点 | 让调度器尽量把任务分过来 |
| Launch method | Launch agents via SSH | 连接方式,本方案选它 |
| Host | 10.0.20.31 | 节点 IP 或可解析的主机名 |
| Credentials | 上一步创建的凭据 | 选 SSH Username with private key 那条 |
| Host Key Verification Strategy | Non verifying verification strategy | 内网固定 IP 环境可接受,见下方说明 |
| Availability | Keep this agent online as much as possible | 保持在线,除非你做动态节点 |
这里有两个字段值得多说两句。Host Key Verification Strategy默认是"Known hosts file verification",需要你在 Master 的 known_hosts 里提前存好节点的主机指纹,麻烦但更安全。如果你管理的是内网固定 IP 的机器,选Non verifying能省去每次加节点都要 ssh-keyscan 的麻烦,但代价是失去主机身份校验,所以只建议在可控内网里这么干。Availability那一栏如果选"Take this agent online/offline based on schedule",可以实现工作时段在线、夜间下线,适合按需节约资源的场景。
填完后点 Save,Jenkins 会立刻尝试连接。这时候别急着刷页面看状态,先去节点页面点进去看日志,日志才是判断连接成功与否的唯一依据。
3.4 验证连接:日志在哪看,成功长什么样
节点创建后,在节点列表里点击节点名,左侧有个Log菜单,这就是实时连接日志。成功的日志大概长这样(不同版本措辞略有差异):
[SSH] Opening SSH connection to 10.0.20.31:22. [SSH] Success. (Connection established) [SSH] Checking java version of /usr/bin/java [SSH] 17.0.10 [SSH] Starting agent process... Agent successfully connected and online几个关键判断点:看到Success说明 SSH 认证通过了,如果你的凭据或者权限有问题,这里就会停住并报Auth fail或者Permission denied;看到Checking java version说明已经开始检查 Java,这一行如果报版本过低,就回头去节点上换 JDK;最后一行online才是真正的成功。从点击保存到变成在线,正常情况十秒以内,如果需要更久,多半是在等 SSH 超时。
如果日志停在中途,我一般按这个顺序排查:先在 Master 上用ssh -i 私钥文件 jenkins@10.0.20.31手工连一次,手工能通说明网络和密钥没问题,问题在 Jenkins 侧配置;手工不通就直接看 SSH 服务端的/var/log/auth.log,里面的拒绝原因写得非常明确,比 Jenkins 的日志详细得多。
顺便提一个界面访问的细节。有时候你从公网地址跳转到内网 Jenkins 控制台,浏览器会因为私有网络访问策略拦截请求,页面提示某个连接被阻止。这不是 Jenkins 的问题,是浏览器对新版本安全策略的执行结果。最稳妥的规避方式就是始终用内网地址或者带域名的内网入口访问 Jenkins,别用公网页面作为跳板。
3.5 用流水线验证任务是不是真的落到节点上了
节点显示在线只是第一步,真正的验证是让一个任务跑上去。新建一个 Pipeline 任务,脚本这么写:
pipeline { agent { label 'linux && jdk17' } stages { stage('CheckEnv') { steps { sh ''' echo "当前节点: $(hostname)" echo "工作目录: $(pwd)" echo "Java 版本: $(java -version 2>&1 | head -n 1)" ''' } } } }跑完之后看控制台输出,hostname如果是节点机器的主机名,说明调度成功;pwd应该落在/data/jenkins-agent/workspace/任务名下面;Java 版本应该是节点上装的那个。三项都对,这个节点就算真正接入完成了。我强烈建议把这段检查脚本固化成一个"环境自检"任务,每次新加节点都跑一遍,比人工登机器查快得多。
另外,agent { label 'xxx' }里的表达式支持&&和||,写jdk17 && linux是要求两个标签同时具备,写jdk17 || jdk21则是满足其一即可。标签表达式一旦写错,Jenkins 不会报错,而是会让任务一直卡在"等待可用的执行器"上,这个现象要能识别出来。
4. 内网隔离环境:Inbound 方式怎么接
4.1 什么情况下必须用 Inbound
有些环境里,节点机器位于一个只能出、不能入的网络区域,Master 根本没法主动去连它的 SSH 端口。举个我遇到的真实场景:构建机被放在一个受控区,只允许它访问 Master 的 8080,不允许 Master 反向连它的任何端口。这种情况下 SSH 方式无解,必须让节点主动发起连接。
Jenkins 对这种模式的官方叫法是"通过让 agent 连接到 controller 来启动 agent",在新建节点时选Permanent Agent之后,"Launch method" 那一栏选Launch agent by connecting it to the controller。这种模式下,Master 不再作为客户端,而是作为一个监听服务端等待节点连过来,两种角色的对调带来了完全不同的配置方式和排错思路。
4.2 拆解 secret 和 agent.jar 启动命令
创建完这类节点后,节点详情页会给你一段现成的启动命令,大意是这样:
java -jar agent.jar -url http://jenkins.example.com:8080/ \ -secret 0f1e2d3c4b5a69788796a5b4c3d2e1f0a1b2c3d4e5f60718293a4b5c6d7e8f90 \ -name "linux-inbound-01" \ -workDir "/data/jenkins-agent"这段命令里有四个参数,每个都不能省。-url是 Master 的地址,如果 Master 前面有反向代理,要填对外可访问的域名和端口;-secret是节点身份的密钥,每个节点独立,泄露了等于别人可以冒充你的节点,所以它在 Jenkins 里是加密存储的;-name必须和你在 Jenkins 里创建的节点名完全一致,差一个字符就会认证失败;-workDir是节点上的工作目录,要确保目录存在并且有写权限。
agent.jar可以从 Master 的/jnlpJars/agent.jar路径下载,也就是在浏览器里访问http://你的jenkins地址/jnlpJars/agent.jar。如果这台节点不能访问外网也没关系,agent.jar 是从 Master 拿的,本身就是内网流量。
这里涉及一个通信层面的细节:Inbound 模式的节点和 Master 之间保持的是一条 TCP 长连接,而不是每传一次数据就重新握手。这条长连接会周期性发心跳,一旦网络抖动导致连接断开,节点会尝试自动重连,重连期间节点状态显示为离线。Jenkins 新版本默认使用 WebSocket 作为传输层,好处是可以复用 Master 的 HTTP 端口(8080 或 443),不需要额外开放那个传统的 50000 端口。如果你的环境防火墙卡得很死,只放行了 443,那就一定要确认 WebSocket 模式开启,否则连接会一直停在"握手失败"。
4.3 做成 systemd 服务,别让它挂在终端里
用nohup或者screen跑 agent 命令,短期看着能工作,但只要机器重启、会话超时或者运维同学误关窗口,节点就掉了,而且没有任何自动恢复机制。正确做法是注册成 systemd 服务:
sudo tee /etc/systemd/system/jenkins-agent.service > /dev/null <<'EOF' [Unit] Description=Jenkins Agent After=network.target [Service] Type=simple User=jenkins WorkingDirectory=/data/jenkins-agent ExecStart=/usr/bin/java -jar /data/jenkins-agent/agent.jar \ -url http://jenkins.example.com:8080/ \ -secret 你的secret \ -name "linux-inbound-01" \ -workDir "/data/jenkins-agent" Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl enable --now jenkins-agent sudo systemctl status jenkins-agentRestart=always配合RestartSec=10是关键,它保证了节点进程无论因为什么原因退出,十秒后都会自动拉起来。这样网络抖动、Master 重启、机器重启这些情况都不需要人工干预。做完之后用journalctl -u jenkins-agent -f看日志,能看到和 Web 界面同样的连接过程信息。
5. 常见报错与排查速查表
5.1 SSH 连接类问题的定位思路
SSH 方式的失败几乎都集中在认证和端口两件事上。我整理了一张速查表,按"现象—原因—处理"三列来组织,遇到问题直接对号入座。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| Auth fail / Permission denied | 用户名不匹配、私钥无权限、authorized_keys 权限过宽 | 在 Master 手工 ssh 验证,检查 600 / 700 权限位 |
| Connection refused | 节点 SSH 服务没启动或端口不对 | `ss -tlnp |
| Connection timed out | 防火墙拦截或 IP 不通 | telnet 节点IP 22或nc -zv测试连通性 |
| Host key verification failed | Master 的 known_hosts 没有节点指纹 | 用 ssh-keyscan 补指纹,或改用非验证策略 |
| No such file or directory | Remote root directory 不存在 | 在节点上手动创建目录并授权 |
排查 SSH 问题时有个特别好用的方法:用 Jenkins 完全相同的私钥和用户名,在 Master 上用ssh -vvv手工连一次。三档 verbose 会把密钥协商、认证过程全部打出来,最后失败的那一步在输出里一目了然。这个操作能省掉你在 Jenkins 界面上反复试错的时间。
5.2 节点能连上但频繁掉线怎么办
有一种情况比彻底连不上更折磨人:节点显示在线,跑着跑着突然离线,过几秒又回来了,构建任务跟着失败。这种抖动通常来自四个方面。
一是网络层有心跳超时限制。中间的网络设备可能会把长时间空闲的 TCP 连接清掉,而 Agent 默认的心跳间隔如果比这个超时时间长,连接就会被中途掐断。解决办法是在节点启动参数里加-pingTimeout之类的调优参数,或者让心跳更频繁。二是Java 堆内存不足,Agent 进程 OOM 之后被系统杀掉,systemd 拉起来又跑一阵又崩,表现就是周期性掉线,这时候去看journalctl里有没有 OOM 相关的记录。三是时钟不同步,Master 和节点时间差太大时,TLS 握手或者某些基于时间戳的校验会失败,装个 NTP 客户端同步一下就好。四是磁盘写满导致的工作目录写入失败,这个最隐蔽,因为连接本身是好的,只有构建时会出问题。
顺便说一句,节点的日志分两层:连接层日志在 Jenkins 界面的节点页面里,Agent 进程自身的日志在系统日志里(systemd 管的就用journalctl,SSH 启动的在节点工作目录下有slave.log之类的文件)。两层都要看,只看一层很容易误判。
5.3 任务明明配了标签却不上节点
这大概是被问得最多的一个问题。任务卡在"等待可用的执行器"上不动,节点明明是绿的。原因一般有这几种:标签表达式写错,比如节点的标签是jdk-17而任务里写的是jdk17,少一个横线就匹配不上;节点的 Usage 被设成了"只允许绑定到此节点的任务"(Only build jobs with label expressions matching this node),而任务没有显式绑定这个节点;节点的执行器被占满了,也就是并发数不够;还有一种是任务在agent里用了node直接调用,绕过了标签匹配逻辑。
排查顺序建议是:先在节点的详情页看"构建执行状态",确认执行器是不是真的被占满;再用脚本控制台执行Jenkins.instance.getNodes()或者在节点列表页看标签;最后把任务的标签表达式简化成只写一个标签试试。把大表达式拆开来测,比一次写完整表达式更容易定位问题。
5.4 环境变量和工具路径对不上
同一条流水线在 Master 上跑得好好的,一到节点上就报"命令找不到",这通常是因为节点上的 PATH 和 Master 不一样。SSH 方式启动的非交互式会话里,/etc/profile和~/.bashrc不一定会被加载,所以你在 shell 里手敲能跑通的命令,在 Jenkins 里可能就找不到。
处理方式有两种。一种是在节点配置页面的"Node Properties"里勾选"Environment variables",把PATH、JAVA_HOME、M2_HOME这些关键变量显式写进去,这样它们会注入到节点的 Agent 进程环境中。另一种是在流水线里用withEnv临时设置。我更推荐前者,因为它是节点级的全局配置,一次配好所有任务都受益。至于构建工具的具体版本,比如 Maven、Gradle、Node,交给 Jenkins 的"全局工具配置"来管理最规范,配置好后在流水线里用tools { maven 'maven-3.9' }引用就行,这块和节点配置是配套使用的。
5.5 节点用久了越来越慢
节点跑几个月之后开始变慢、磁盘告警,多半是历史工作空间和构建缓存没清理。Jenkins 默认不会自动清理旧的工作空间,每次构建都会在workspace/任务名下面堆积文件。我的做法是在节点上挂一个定时任务,或者干脆用 Jenkins 的"Discard old builds"策略配合工作空间清理插件来控制。另外,节点上的 Docker 如果被用于构建,镜像和容器层会疯狂膨胀,定期执行docker system prune这类清理是必要的运维动作。
还有一个容易被忽略的点是连接数。如果节点参与的任务很多,同时打开的连接和文件句柄数量会上来,系统级的ulimit -n如果还是默认的 1024,可能会在高并发时出现"打开文件过多"的错误。把nofile提到 65535,改在 systemd unit 的LimitNOFILE里,比重启整个系统影响面小得多。
6. 几条我踩过坑之后总结的经验
6.1 那些低级但致命的小问题
第一,尽量不要复用同一把私钥给所有节点。一开始图省事复制粘贴,后来某个节点退役了,私钥还散落在各处,只能全量轮换一遍,工作量比当初多建几把密钥大得多。第二,节点目录一定不要建在/tmp下,很多系统会定期清理/tmp,某天重启之后工作目录连同 Agent 的 jar 一起消失了,报错信息还指向一个根本不存在的路径。第三,SSH 方式下一定要确认节点的~/.ssh目录权限是 700,authorized_keys 是 600,这两个数字我见过太多人栽在上面,而且 SSH 的报错往往只说"认证失败",不会告诉你真正原因是权限。
第四,关于 Jenkins 插件和升级,如果你在内网环境,插件市场访问不了的时候要提前准备好离线插件包,或者把更新站点配置成可访问的镜像地址,这类配置在离线环境和受限网络里是必须项,否则某天需要装个插件会让你很被动。
6.2 节点规模上来以后怎么维护
当节点从两三个变成十几个,手工维护就不现实了。我现在基本采用"镜像化 + 标签规范"两条腿走路。镜像化是指所有节点的环境用同一份系统镜像或者 Docker 镜像构建出来,JDK 版本、目录结构、用户配置全部一致,出问题直接重建而不是登机器修。标签规范是指标签的命名规则写进团队文档,谁加节点都要遵守,避免出现jdk17、jdk-17、JDK17三种写法混用导致调度失灵的尴尬局面。
如果是纯粹为了跑构建、对环境一致性要求高的场景,其实可以考虑用容器化的动态节点,任务来了自动起一个容器,跑完销毁,环境天然干净。这类方案适合构建量波动大、又不想长期维护一堆物理机的团队,代价是镜像构建和拉取会有额外开销,冷启动慢一些。我在构建量大的项目上用过这种模式,配合镜像预热之后效果很好;构建量小的时候还是固定节点更划算。
至于节点健康监控,我一般会写一个定时流水线,每隔一段时间遍历所有节点,检查在线状态、工作目录剩余空间、Java 版本是否仍然匹配,结果推到内部通知渠道。这套自检跑起来之后,节点掉线这个问题从"用户报障才发现"变成了"提前十分钟自己知道"。
再补一个小技巧。如果你需要在节点上跑 Docker 构建,把节点上的 jenkins 用户加入 docker 组是常见的做法,但要清楚这等于给了这个用户近似 root 的权限,因为它可以挂载宿主机文件系统。所以要么接受这个风险,要么改用无 root 的构建工具方案。这个取舍没有标准答案,取决于你的安全边界要求有多高。