Arbess+GitLab+Hadess:Java微服务自动化部署流水线实战
开头先亮个底我最近把公司一套Java微服务项目的交付链路从“开发自己打包、运维手动部署”的原始状态改造成了基于Arbess、GitLab和Hadess三件套的自动化流水线。核心效果就一句话——开发把代码推到指定分支剩下的编译、打包、推送制品、登录主机、停老进程、起新进程全自动完成。这套方案折腾了大概两周中间踩了不少坑今天把完整思路和实操细节写出来给正准备搞DevOps落地、尤其是预算有限不想上K8s的团队做个参考。这套组合适合什么场景呢我判断的标准很直接你的Java项目还是以物理机或云主机为主要部署目标团队已经有GitLab在管代码但构建和发布还靠人肉又或者你试过Jenkins觉得维护插件、配置节点太麻烦想要一条更轻的链路。只要符合其中一条Arbess加Hadess这套玩法就比硬上Kubernetes划算得多学习成本和运维成本都能压得很低。1. 整体链路设计与选型思路先说结论Arbess在整个链路里相当于“总导演”负责把GitLab的代码事件和Hadess的部署能力串成一条可编排的流水线GitLab是代码仓库和触发源Hadess则负责把构建好的Java制品分发到目标主机并执行部署动作。1.1 为什么不直接全用GitLab CI或Jenkins这个可能是大家第一个想问的。GitLab自带的CI/CD其实已经很强单项目用完全够但当我手上项目管理多了每个项目都得写一份.gitlab-ci.yml公共逻辑抽不干净而且主机部署这种动作放在CI里总要靠ssh穿来穿去权限和审计都很难受。Jenkins则是另一座大山插件版本兼容性、Master-Slave节点管理、Pipeline脚本维护动不动就给你个红色报错。项目多了之后维护成本直接起飞。Arbess强悍在它把“事件”—“编排”—“执行”拆成了三层GitLab只负责产生push事件Arbess负责定义“事件来了之后该怎么走”Hadess负责真正到主机上干活。这样好处很明显代码仓库不必再跟某个CI系统强绑定不同项目可以用同一套编排模板Java、Python、Node项目都能共存以后想迁移到K8s只需要把“主机部署”这个步骤换成“镜像部署”就行编排层不用动。1.2 三件套各自扮演什么角色我类比一下GitLab是前台的“收件柜”开发把代码往那一放系统就知道有新需求了Arbess是中间那层“调度室”它收到收件柜的通知不会自己跑去拆快递而是翻开自己的流程手册把任务一个一个派下去Hadess就是“执行专员”专门负责到目标机器上把老服务停掉、新包放上去、把服务拉起来。这个拆法的价值在于可替换性。我今天可以用Hadess做主机部署明天想换成容器编排只要在Arbess里把执行节点换掉就行我也可以把Arbess背后的代码托管从GitLab换成其他平台触发源调整一下其他环节完全不受影响。这种解耦思路是纯用Jenkins或纯用GitLab CI很难做到的。1.3 我们最终跑通的链路全景我画一条文字版的链路大家先建立全局印象后面每一段都会细拆。开发提交代码到GitLab的release分支。GitLab通过Webhook把push事件推送给Arbess。Arbess根据项目绑定的流水线模板启动一次自动化流程。流程第一步调用构建节点执行Maven编译打包。产物被推送到Hadess的制品库模块集中存储每次构建都有唯一版本号。部署阶段Hadess的Agent登录目标主机执行备份、停服、更新、重启等动作。完成后Arbess汇总日志把结果回写到GitLab的Commit状态上。我之所以把“制品集中管理”这一步单独拎出来而不是构建完直接scp上服务器是因为生产环境出问题时你要能快速回滚到上一个版本如果没有历史制品回滚就等于重新构建时间根本等不起。这条链路看着简单真正落地要考虑的细节其实非常多。2. 环境准备与组件部署这套方案里GitLab和Arbess我用Docker部署Hadess的Agent单独安装到目标主机服务端走容器化。如果团队没有Docker基础强烈建议先补一补因为GitLab的依赖项很多容器化能省掉大量系统兼容性折磨。2.1 主机资源与网络规划先算资源。我们公司的项目规模不大不小30个Java服务、10人研发团队我准备了两台8核16G的服务器一台放在内网作为“管理中心”跑GitLab、Arbess、Hadess服务端另一台作为“构建执行机”跑编译任务和Hadess Agent。两台机器都装了Docker和Docker Compose。内存分配上有个小建议GitLab比较吃内存尤其执行代码质量检查时给它留足6G左右Arbess和Hadess服务端都是Java写的各给2G就够构建执行机上Maven构建非常吃内存8G给构建JVM进程和Docker守护进程分。实际用下来这套配置并发跑3个Java项目的构建不会卡。端口规划也要提前想清楚我列一下常用的GitLab Web端口映射为80和443。GitLab SSH端口映射为222避免跟宿主机SSH冲突。Arbess服务端口8000。Hadess服务端端口8081。Hadess Agent通信端口8082。注意如果服务器有防火墙记得把这些端口加白名单。我在测试环境吃过亏忘了放行Agent的8082端口导致构建成功但部署永远卡在“等待Agent连接”。2.2 用Docker快速拉起GitLabGitLab的容器化部署很成熟直接用一个docker-compose文件管理。我的docker-compose.yml大概长这样version: 3 services: gitlab: image: gitlab/gitlab-ce:16.4.1-ce.0 container_name: gitlab restart: always hostname: gitlab.internal environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.internal gitlab_rails[gitlab_shell_ssh_port] 222 prometheus_monitoring[enable] false ports: - 80:80 - 443:443 - 222:22 volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/data:/var/opt/gitlab shm_size: 256m首次启动大概需要等两三分钟可以用docker logs -f gitlab观察初始化进度。等看到“gitlab Reconfigured!”的日志后访问服务器IP初次进入会让设置root密码。这里有个经验如果想让研发直接通过域名访问要把external_url设成你规划的域名同时在内网DNS做好解析。如果暂时没有域名先用IP访问也行但后续配置Webhook时回调地址也得填IP容易出乱子。2.3 Arbess与Hadess的最小化部署Arbess和Hadess都提供了官方Docker镜像部署命令其实就一条docker run的事。我当时图省事把两个服务写在同一个Compose文件里version: 3 services: arbess: image: arbess/arbess-server:latest container_name: arbess restart: always ports: - 8000:8000 volumes: - /data/arbess/data:/data environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: arbess DB_USER: arbess DB_PASSWORD: yourpassword hadess-server: image: hadess/hadess-server:latest container_name: hadess-server restart: always ports: - 8081:8081 volumes: - /data/hadess/data:/dataArbess依赖MySQL做元数据存储先用Docker单独起一个MySQL实例再启动Arbess。我最初图简单直接用H2内嵌数据库结果重启几次后流水线历史记录丢了一部分排查半天才发现是文件权限的问题。这里建议一步到位测试环境也别省直接上MySQL。Hadess设计上更轻一些服务端负责管理Agent的注册和制品索引Agent装在每一台目标部署机上负责执行实际的部署脚本。Agent安装包里就一个可执行文件加一份配置文件改一下Server地址和认证Token然后跑hadess-agent install就能注册成系统服务。2.4 GitLab Runner注册与凭证配置虽然前文说不想过度依赖GitLab CI但构建动作还是要有人去跑的。我选择的方式是Arbess触发构建时调用的是独立构建机上的GitLab Runner让Runner执行Maven命令。原因不复杂GitLab Runner跟GitLab代码仓库之间拉代码最顺畅权限模型也是现成的。Runner注册主要记住几个参数sudo gitlab-runner register \ --url http://gitlab.internal/ \ --token glrt-xxxxxxxx \ --executor shell \ --description java-build-runner \ --tag-list java,maven,build \ --run-untaggedfalse \ --lockedfalse我用的是shell执行器因为Maven构建直接在本机跑最直接。如果想隔离得更干净可以用docker执行器让每个构建跑在独立容器里但多了一层镜像管理的成本。注册完Runner后把构建机的Java环境、Maven环境、私有制品库的settings.xml都配好这部分属于“一次配置、长期受益”做规范一点后面省心。3. Java项目接入流水线的实操细节环境搭好只是第一步真正的重头戏是让Java项目跑通整条自动化流水线。这一章我按“构建—存储—部署”三段来写每个环节都放关键配置和脚本。3.1 Maven多模块项目的构建设计现在Java后端项目基本没有单模块的大多是多模块结构父pom统一管依赖版本下面挂common、service、web等子模块。构建时如果无脑mvn clean package会浪费大量时间在重复打包不相关的模块上。我在Arbess里为Java项目设计了两种构建模式通过项目参数控制全量构建执行mvn clean package -DskipTests产物为一个zip包包含所有模块的Jar。增量构建通过-pl参数指定需要构建的模块例如mvn clean package -pl service-web -am -DskipTests适合改动只涉及个别模块时的快速部署。增量构建能把一个30个模块项目的构建时间从8分钟压到2分钟以内这个优化对研发体验提升特别明显。但要注意增量构建产出的制品是单个Jar部署脚本里要对“本次是哪个Jar”有明确的标识不然容易部署错模块。还有几个构建参数需要统一管理JDK版本必须跟项目要求一致现在公司新项目基本都用JDK17老项目还停在JDK8所以构建机上要同时装两个JDK通过JAVA_HOME变量切换Maven的settings.xml里要配好私有仓库地址和认证信息否则构建时候会卡在下载依赖。3.2 流水线配置从代码提交到制品归档的串联Arbess里创建一条“Java主机部署流水线”模板我这边配了4个阶段拉取代码、编译打包、制品归档、主机部署。每个阶段之间可以设置“失败自动跳过后续阶段”这个一定要开避免代码编译失败后还在那傻傻执行部署。日志里曾经出现过一出很尴尬的事编译明明失败了流水线居然继续往下跑把上一次构建的旧包部署到了生产。查下来发现是Arbess的阶段状态判断没有配置好失败条件没勾上“异常结束”。这种事故一旦发生团队对这套系统的信任直接就崩了所以阶段失败策略务必仔细检查。流水线触发方式我强烈建议用“分支过滤Tag驱动”普通分支的push只触发构建并推送制品到测试环境只有打上v*格式的Tag才触发生产部署。这是在安全性和便利性之间比较平衡的选择。直接在release分支上push就触发生产部署听起来很爽但研发提交一个临时彩色日志代码都可能发到生产时间长了总会出事。3.3 制品推送与版本管理Hadess的制品库在阶段设计上很像一个简化版Nexus支持按“项目—环境—版本号”三层结构来组织软件包。Arbess在构建完成后会调用Hadess的开放接口把产物打成.tar.gz包推上去版本号默认用时间戳-短GitCommit号。举个例子这次构建的提交号是7f2c91e那版本号大概是20241011-1530-7f2c91e。这个命名规则建议写死在流水线模板里不要让人手工填。出问题时凭版本号就能知道是哪天构建的、对应哪次代码提交排查起来效率高很多。制品归档时还有个小细节要把Maven构建生成的build-info.properties或类似的信息一起归档进去里面记录Git提交时间、分支、构建机的hostname等。这些在出事的时候就是“最后一根救命稻草”。3.4 主机部署与优雅重启脚本部署动作是整套链路里最容易出问题的一环。我先梳理一下正常流程Hadess Agent收到部署指令后从制品库下载对应版本的tar包。Agent把包解压到发布目录例如/data/apps/yourproject/versions/20241011-1530-7f2c91e。备份当前运行版本备份到/data/apps/yourproject/backups。把current软链接切换到新版本目录。执行停止命令、启动命令、健康检查。这个顺序非常关键。很多团队部署失败是因为先停服务再解压文件一旦新包有问题旧服务已经起不来了。用软链接这个方案整个切换过程是原子操作新包启动失败时只需要一条命令把current指回旧版本服务就回来了。我用的Java服务启停脚本核心部分如下#!/bin/bash APP_NAMEyourproject BASE_DIR/data/apps/yourproject CURRENT_DIR${BASE_DIR}/current LOG_DIR${BASE_DIR}/logs # 停止旧进程 if [ -f ${CURRENT_DIR}/app.pid ]; then OLD_PID$(cat ${CURRENT_DIR}/app.pid) if kill -0 $OLD_PID 2/dev/null; then kill $OLD_PID for i in {1..30}; do if ! kill -0 $OLD_PID 2/dev/null; then break fi sleep 1 done # 如果还没有退出强杀 if kill -0 $OLD_PID 2/dev/null; then kill -9 $OLD_PID fi fi fi # 启动新进程 nohup java -Xms512m -Xmx1024m \ -jar ${CURRENT_DIR}/lib/service-web.jar \ --spring.profiles.activeprod \ --server.port8080 \ ${LOG_DIR}/app.log 21 echo $! ${CURRENT_DIR}/app.pid # 健康检查 for i in {1..60}; do if curl -f http://127.0.0.1:8080/actuator/health; then echo Health check passed exit 0 fi sleep 2 done echo Health check failed exit 1这个脚本的细节值得说道说道等老进程退出最多给30秒超时就强杀避免线程卡住拖着不退出健康检查循环60次、每次间隔2秒总共120秒给足Spring Boot冷启动时间。如果项目比较大启动慢这个时间还要调大。4. 常见问题与排查技巧实录这一章记录的每一个问题都是我在真实的落地过程中碰到的不是网上抄来的。我把它们整理成速查表再挑几个典型的展开讲讲排查思路。问题现象可能原因解决办法构建卡在下载依赖Maven私服地址配置错误或私服宕机配置settings.xml镜像为内网私服添加健康检查构建成功但部署超时Hadess Agent与Server网络不通检查Agent心跳telnet测试8082端口部署后服务频繁重启健康检查命令错误或端口配置不符确认服务实际端口用curl的返回值作为判断回滚后配置丢失配置文件包含在制品包外的路径共享配置放置到固定路径不放进版本目录Webhook触发不生效GitLab网络与Arbess网络隔离URL填了localhost使用双方可达的内网域名或IP看Arbess访问日志并发构建互相影响多个构建共用同一个workspace目录配置Runner每次build前清理workspace用日期子目录隔离4.1 场景一GitLab Webhook触发不稳定这个问题困扰了我差不多一天。明明在GitLab后台测试推送事件时Arbess都能收到但开发真正push代码后流水线十次里只有七八次能跑起来。后来仔细排查发现GitLab后台测试Webhook是直接发出请求的而实际push事件触发时GitLab会带一个token签名字段Arbess验签失败的事件直接丢弃了。原因就在于我重新部署Arbess后系统自动生成了一组新的Webhook密钥而GitLab那边还留着老的配置。解决办法在GitLab项目设置里打开Webhook配置把Secret Token重新填成Arbess界面里显示的最新token然后点“Push events”旁边的Test按钮确认返回200。这个坑的教训是所有外部集成的凭据类配置一定要纳入配置管理别只存在部署人脑子里。4.2 场景二主机部署时老进程总是停不干净用PID文件判断进程是否存在的方法理论上没问题但实际会碰到两种情况一是应用进程被守护进程自动拉起kill后又起来了二是Spring Boot启动时PID文件里写的是Shell进程号而不是Java进程号。前一种需要排查是否有systemd或Supervisor类守护工具后一种要在启动命令里用exec java或在脚本里正确获取子进程PID。还有一个比较隐蔽的场景同一个服务端口被另一个进程占用健康检查明明探测到了8080端口有响应但那其实是老进程还没死透或别的服务。我现在的部署脚本里会先执行ss -lntp | grep 8080确认到底哪个进程在监听避免把“端口通”当成“服务就绪”。4.3 场景三构建并发高时Maven内存溢出团队规模一上来并发构建的数量自然就上去了。两台构建机跑八个项目的构建终于有一天Jenkins任务列表变成一片红色——哦不是Arbess流水线列表全是失败。登录构建机一看Maven编译进程表现得很不正常GC日志频繁。原因很直接Maven构建时默认的MAVEN_OPTS给的堆内存太小多个构建同时跑直接内存溢出。我的调整方案是在构建机的/etc/profile里加上export MAVEN_OPTS-Xms1024m -Xmx2048m同时给Arbess配置每个项目的并发上限为2个构建。并发拉的太高没有意义构建主要瓶颈在CPU和网络内存给够了就稳。4.4 场景四GitLab Runner创建不了构建日志文件用shell执行器的时候Runner默认会以gitlab-runner用户的身份执行命令但这个用户在很多系统上权限有限连在项目目录创建文件都可能没有权限。我的处理方式比较直接注册Runner时指定使用root用户执行同时在Runner配置里把shell改成bash。需要说明这样确实存在一定安全隐患构建脚本里如果被塞了恶意命令能拿到root权限。但在内网开发环境团队代码本身就是可信的权衡便利和风险后我选择了root。如果公司要求更严格建议单独建一个构建账号把需要用到的目录权限精确分配好。5. 上线后的运维沉淀与心得流水线跑通只是开始真正难的是让它稳定地跑下去。这个阶段我主要补了三件事回滚策略、日志监控、模板固化。5.1 回滚策略怎么设计才不慌我刚才提到用软链接的方式部署天然就支持秒级回滚。实际做法是部署成功后在versions目录下保留最近5个版本包回滚时把current软链接指到目标版本再执行一次重启脚本。因为制品和配置是分离的回滚不会把环境配置带回旧状态出问题的概率很低。需要注意的是回滚操作本身也应该走Arbess的流水线而不是人肉登录服务器去改软链接。我给回滚做了一条专属流水线参数是“项目名”和“目标版本号”这样回滚动作也有审计日志出了问题能追到人。5.2 告警与通知别偷懒自动化程度越高告警就越重要。以前的模式是“发布失败有人立刻知道”现在的模式是“发布流程完全无人值守”那就必须让系统在关键节点主动通知到人。我用的是“企业微信机器人”配合Arbess的Webhook通知流水线执行到“构建完成”和“部署成功”时各发一条消息到技术群失败则带上具体阶段和日志摘要对应的负责人。通知消息里一定要带上版本号和提交号不然群里刷屏后根本不知道在说哪个项目。我曾经因为通知里没带项目名导致同事在群里反复问“这是哪个服务的构建消息”体验很差。格式上可以参考“服务名环境版本号状态耗时”。5.3 把最佳实践固化成模板到了这一步我不建议每个项目各自为战地配置流水线否则维护成本会重新把人拖垮。正确的做法是把最标准的那条链路——拉代码、构建、归档、部署、健康检查、通知——固化成Arbess的模板新项目接入时只要填项目名、Git地址、部署主机、启动参数这四个字段就能跑起来分钟级接入。我复盘整套改造时有个体会DevOps工具链不是越复杂越好关键是切中团队的痛点。Arbess加GitLab加Hadess这套组合避开了Kubernetes的高门槛把Java主机部署这件事做到了自动化、标准化、可回滚对我们这种几十个服务的团队来说性价比极高。如果你现在正被手动部署折磨不妨按这套思路搭一遍链路跑通的那个瞬间你会觉得之前踩的那些坑都值了。