很多团队的裸机部署还停留在「SCP 传 jar + SSH 重启」的阶段。能用,但不可复用、不可回滚、不可审计。本文拆解一个真实在产的 Ansible 部署项目------它用不到 200 行 YAML + 一个 Shell 脚本,实现了 Java 应用的幂等初始化、四目录版本管理、七命令生命周期、开机自启和服务注销闭环。面向正在用 Ansible 管理裸机/VM Java 部署的运维与 SRE 工程师,聚焦可复用的设计模式与踩坑点。
一、先看全貌:一个最小可用的 Ansible 部署项目
整个项目的文件结构:
#mermaid-svg-6s6m3PXVdQjBTPvU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6s6m3PXVdQjBTPvU .error-icon{fill:#552222;}#mermaid-svg-6s6m3PXVdQjBTPvU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6s6m3PXVdQjBTPvU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6s6m3PXVdQjBTPvU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6s6m3PXVdQjBTPvU .marker.cross{stroke:#333333;}#mermaid-svg-6s6m3PXVdQjBTPvU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6s6m3PXVdQjBTPvU p{margin:0;}#mermaid-svg-6s6m3PXVdQjBTPvU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster-label text{fill:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster-label span{color:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster-label span p{background-color:transparent;}#mermaid-svg-6s6m3PXVdQjBTPvU .label text,#mermaid-svg-6s6m3PXVdQjBTPvU span{fill:#333;color:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU .node rect,#mermaid-svg-6s6m3PXVdQjBTPvU .node circle,#mermaid-svg-6s6m3PXVdQjBTPvU .node ellipse,#mermaid-svg-6s6m3PXVdQjBTPvU .node polygon,#mermaid-svg-6s6m3PXVdQjBTPvU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6s6m3PXVdQjBTPvU .rough-node .label text,#mermaid-svg-6s6m3PXVdQjBTPvU .node .label text,#mermaid-svg-6s6m3PXVdQjBTPvU .image-shape .label,#mermaid-svg-6s6m3PXVdQjBTPvU .icon-shape .label{text-anchor:middle;}#mermaid-svg-6s6m3PXVdQjBTPvU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6s6m3PXVdQjBTPvU .rough-node .label,#mermaid-svg-6s6m3PXVdQjBTPvU .node .label,#mermaid-svg-6s6m3PXVdQjBTPvU .image-shape .label,#mermaid-svg-6s6m3PXVdQjBTPvU .icon-shape .label{text-align:center;}#mermaid-svg-6s6m3PXVdQjBTPvU .node.clickable{cursor:pointer;}#mermaid-svg-6s6m3PXVdQjBTPvU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6s6m3PXVdQjBTPvU .arrowheadPath{fill:#333333;}#mermaid-svg-6s6m3PXVdQjBTPvU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6s6m3PXVdQjBTPvU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6s6m3PXVdQjBTPvU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6s6m3PXVdQjBTPvU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6s6m3PXVdQjBTPvU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6s6m3PXVdQjBTPvU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster text{fill:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU .cluster span{color:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6s6m3PXVdQjBTPvU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6s6m3PXVdQjBTPvU rect.text{fill:none;stroke-width:0;}#mermaid-svg-6s6m3PXVdQjBTPvU .icon-shape,#mermaid-svg-6s6m3PXVdQjBTPvU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6s6m3PXVdQjBTPvU .icon-shape p,#mermaid-svg-6s6m3PXVdQjBTPvU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6s6m3PXVdQjBTPvU .icon-shape .label rect,#mermaid-svg-6s6m3PXVdQjBTPvU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6s6m3PXVdQjBTPvU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6s6m3PXVdQjBTPvU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6s6m3PXVdQjBTPvU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用
条件包含
上传
上传
可选上传
weifuwu/
deploy.yml
入口 Playbook
hosts
目标主机清单
group_vars/all
应用变量
roles/deploy/
tasks/main.yml
主任务流程
tasks/initial.yml
首次初始化
files/jar_app.sh
生命周期脚本
templates/dele_down_service.py
Consul 注销脚本
核心只有 7 个文件,却覆盖了一个 Java 应用从「首次装机」到「日常更新回滚」的完整闭环。入口 deploy.yml 极简:
yaml
- name: Deploy Jar Package
hosts: all
remote_user: root
roles:
- deploy
这种「入口只做声明,逻辑全在 Role 里」的写法是 Ansible 的最佳实践。deploy.yml 可以被复用、被组合进更大的 Playbook,而 deploy Role 本身保持自包含。
二、幂等初始化:条件执行的艺术
部署脚本最常见的坑是「重复执行会出错」------比如重复创建用户、重复写开机启动项。这个项目用 stat 检测 + 条件 include 解决了幂等性。
2.1 主任务流程:检测 → 初始化 → 上传 → 更新
yaml
# roles/deploy/tasks/main.yml
- name: 检测应用目录
stat: path=/fjf_work/{{jobname}}
register: app_dir
- include_tasks: initial.yml
when: app_dir.stat.exists == false
- name: 获取jar包名称
shell: basename `ls roles/deploy/files/*.jar`
args:
warn: no
delegate_to: 127.0.0.1
register: package
- name: 上传jar包
copy: src={{package.stdout}} dest=/fjf_work/{{jobname}}/package/ owner=fjf group=fjf mode=0644
- name: 运行jar包
command: /fjf_work/{{jobname}}/bin/jar_app.sh update
args:
warn: no
这段流程的执行逻辑:
#mermaid-svg-bTHynPNA1PlKLohz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bTHynPNA1PlKLohz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bTHynPNA1PlKLohz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bTHynPNA1PlKLohz .error-icon{fill:#552222;}#mermaid-svg-bTHynPNA1PlKLohz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bTHynPNA1PlKLohz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bTHynPNA1PlKLohz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bTHynPNA1PlKLohz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bTHynPNA1PlKLohz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bTHynPNA1PlKLohz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bTHynPNA1PlKLohz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bTHynPNA1PlKLohz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bTHynPNA1PlKLohz .marker.cross{stroke:#333333;}#mermaid-svg-bTHynPNA1PlKLohz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bTHynPNA1PlKLohz p{margin:0;}#mermaid-svg-bTHynPNA1PlKLohz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bTHynPNA1PlKLohz .cluster-label text{fill:#333;}#mermaid-svg-bTHynPNA1PlKLohz .cluster-label span{color:#333;}#mermaid-svg-bTHynPNA1PlKLohz .cluster-label span p{background-color:transparent;}#mermaid-svg-bTHynPNA1PlKLohz .label text,#mermaid-svg-bTHynPNA1PlKLohz span{fill:#333;color:#333;}#mermaid-svg-bTHynPNA1PlKLohz .node rect,#mermaid-svg-bTHynPNA1PlKLohz .node circle,#mermaid-svg-bTHynPNA1PlKLohz .node ellipse,#mermaid-svg-bTHynPNA1PlKLohz .node polygon,#mermaid-svg-bTHynPNA1PlKLohz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bTHynPNA1PlKLohz .rough-node .label text,#mermaid-svg-bTHynPNA1PlKLohz .node .label text,#mermaid-svg-bTHynPNA1PlKLohz .image-shape .label,#mermaid-svg-bTHynPNA1PlKLohz .icon-shape .label{text-anchor:middle;}#mermaid-svg-bTHynPNA1PlKLohz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bTHynPNA1PlKLohz .rough-node .label,#mermaid-svg-bTHynPNA1PlKLohz .node .label,#mermaid-svg-bTHynPNA1PlKLohz .image-shape .label,#mermaid-svg-bTHynPNA1PlKLohz .icon-shape .label{text-align:center;}#mermaid-svg-bTHynPNA1PlKLohz .node.clickable{cursor:pointer;}#mermaid-svg-bTHynPNA1PlKLohz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bTHynPNA1PlKLohz .arrowheadPath{fill:#333333;}#mermaid-svg-bTHynPNA1PlKLohz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bTHynPNA1PlKLohz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bTHynPNA1PlKLohz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bTHynPNA1PlKLohz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bTHynPNA1PlKLohz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bTHynPNA1PlKLohz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bTHynPNA1PlKLohz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bTHynPNA1PlKLohz .cluster text{fill:#333;}#mermaid-svg-bTHynPNA1PlKLohz .cluster span{color:#333;}#mermaid-svg-bTHynPNA1PlKLohz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bTHynPNA1PlKLohz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bTHynPNA1PlKLohz rect.text{fill:none;stroke-width:0;}#mermaid-svg-bTHynPNA1PlKLohz .icon-shape,#mermaid-svg-bTHynPNA1PlKLohz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bTHynPNA1PlKLohz .icon-shape p,#mermaid-svg-bTHynPNA1PlKLohz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bTHynPNA1PlKLohz .icon-shape .label rect,#mermaid-svg-bTHynPNA1PlKLohz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bTHynPNA1PlKLohz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bTHynPNA1PlKLohz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bTHynPNA1PlKLohz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不存在
已存在
执行 deploy.yml
应用目录
是否存在?
执行 initial.yml
首次初始化
获取本地 jar 包名
上传 jar 到 package 目录
执行 jar_app.sh update
部署完成
关键设计是 when: app_dir.stat.exists == false------只有首次部署才执行初始化 。后续每次更新都跳过 initial.yml,直接走「上传 jar → update」的快路径。这保证了:
- 幂等 :重复跑
ansible-playbook deploy.yml不会重复建用户、重复写 rc.local; - 快:日常更新只做必要的两步;
- 安全:不会因为重复初始化破坏已有配置。
最佳实践 #1 :Ansible 的幂等性不能只靠模块自身的
state=present,跨多步骤的「首次/非首次」分支要用stat+register+when显式控制。把「初始化」和「更新」拆成不同 task 文件,用条件 include 串联,比把所有逻辑塞进一个main.yml用一堆when判断清晰得多。
2.2 初始化任务:建用户、建目录、装脚本、设自启
yaml
# roles/deploy/tasks/initial.yml
- name: 添加fjf账号
user: name=fjf state=present
- name: 创建目录结构
file: path={{item.path}} state=directory owner=fjf group=fjf mode={{item.mode}}
with_items:
- { path: "/fjf_work", mode: "0755" }
- { path: "/fjf_work/{{jobname}}", mode: "0755" }
- { path: "/fjf_work/{{jobname}}/bin", mode: "0755" }
- { path: "/fjf_work/{{jobname}}/logs", mode: "0755" }
- { path: "/fjf_work/{{jobname}}/backup", mode: "0750" }
- { path: "/fjf_work/{{jobname}}/package", mode: "0750" }
- { path: "/fjf_work/{{jobname}}/rollback", mode: "0750" }
- { path: "/fjf_work/{{jobname}}/runtime", mode: "0750" }
- name: 上传脚本文件
copy: src=jar_app.sh dest=/fjf_work/{{jobname}}/bin/ owner=fjf group=fjf mode=0755
- name: 设置文件权限
file: path=/etc/rc.d/rc.local state=file mode=0755
- name: 设置开机启动
lineinfile:
path: '/etc/rc.d/rc.local'
regexp: '^/fjf_work/{{jobname}}/bin/jar_app.sh'
line: '/fjf_work/{{jobname}}/bin/jar_app.sh start'
这里有四个值得拆解的细节。
第一,权限分层。 注意目录权限不是一刀切:
| 目录 | 权限 | 原因 |
|---|---|---|
bin/、logs/ |
0755 |
脚本要执行,日志可能被监控Agent读取 |
runtime/、package/、rollback/、backup/ |
0750 |
包含制品,只允许属主和属组访问 |
0750 比 0755 收紧了「其他用户」的读权限------jar 包里可能有配置密码,不应让同机其他应用读到。
第二,lineinfile 的幂等性。 设置开机启动用的是 lineinfile 而不是 echo >>:
yaml
lineinfile:
path: '/etc/rc.d/rc.local'
regexp: '^/fjf_work/{{jobname}}/bin/jar_app.sh'
line: '/fjf_work/{{jobname}}/bin/jar_app.sh start'
regexp 保证同一应用的启动项只写一行------重复执行会替换 而不是追加 。如果用 echo >> /etc/rc.d/rc.local,跑 10 次 playbook 就会有 10 行重复的启动命令,开机时启动 10 次应用。
最佳实践 #2 :修改配置文件永远用
lineinfile/blockinfile/template,不要用shell: echo >>。前者幂等,后者每次追加。这是 Ansible 新手最容易犯的错。
第三,/etc/rc.d/rc.local 的执行权限。 CentOS 7+ 默认 rc.local 没有执行权限,开机不会执行它。所以初始化里有一行:
yaml
- name: 设置文件权限
file: path=/etc/rc.d/rc.local state=file mode=0755
没有这一步,写了启动项也白写------这是 CentOS 7 迁移到 systemd 后的经典坑。
第四,降权运行。 初始化建了 fjf 用户,但 playbook 是 remote_user: root 执行的。脚本以 root 跑,应用以 fjf 跑------执行权和运行权分离,这个设计在后面的 jar_app.sh 里体现。
三、四目录结构:用文件系统表达部署状态
初始化创建了 4 个核心目录,它们不是随意命名,而是一套用文件系统状态表达部署阶段的设计:
#mermaid-svg-5UxK92AL8U75LSp9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5UxK92AL8U75LSp9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5UxK92AL8U75LSp9 .error-icon{fill:#552222;}#mermaid-svg-5UxK92AL8U75LSp9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5UxK92AL8U75LSp9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5UxK92AL8U75LSp9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5UxK92AL8U75LSp9 .marker.cross{stroke:#333333;}#mermaid-svg-5UxK92AL8U75LSp9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5UxK92AL8U75LSp9 p{margin:0;}#mermaid-svg-5UxK92AL8U75LSp9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5UxK92AL8U75LSp9 .cluster-label text{fill:#333;}#mermaid-svg-5UxK92AL8U75LSp9 .cluster-label span{color:#333;}#mermaid-svg-5UxK92AL8U75LSp9 .cluster-label span p{background-color:transparent;}#mermaid-svg-5UxK92AL8U75LSp9 .label text,#mermaid-svg-5UxK92AL8U75LSp9 span{fill:#333;color:#333;}#mermaid-svg-5UxK92AL8U75LSp9 .node rect,#mermaid-svg-5UxK92AL8U75LSp9 .node circle,#mermaid-svg-5UxK92AL8U75LSp9 .node ellipse,#mermaid-svg-5UxK92AL8U75LSp9 .node polygon,#mermaid-svg-5UxK92AL8U75LSp9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5UxK92AL8U75LSp9 .rough-node .label text,#mermaid-svg-5UxK92AL8U75LSp9 .node .label text,#mermaid-svg-5UxK92AL8U75LSp9 .image-shape .label,#mermaid-svg-5UxK92AL8U75LSp9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5UxK92AL8U75LSp9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5UxK92AL8U75LSp9 .rough-node .label,#mermaid-svg-5UxK92AL8U75LSp9 .node .label,#mermaid-svg-5UxK92AL8U75LSp9 .image-shape .label,#mermaid-svg-5UxK92AL8U75LSp9 .icon-shape .label{text-align:center;}#mermaid-svg-5UxK92AL8U75LSp9 .node.clickable{cursor:pointer;}#mermaid-svg-5UxK92AL8U75LSp9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5UxK92AL8U75LSp9 .arrowheadPath{fill:#333333;}#mermaid-svg-5UxK92AL8U75LSp9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5UxK92AL8U75LSp9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5UxK92AL8U75LSp9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UxK92AL8U75LSp9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5UxK92AL8U75LSp9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UxK92AL8U75LSp9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5UxK92AL8U75LSp9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5UxK92AL8U75LSp9 .cluster text{fill:#333;}#mermaid-svg-5UxK92AL8U75LSp9 .cluster span{color:#333;}#mermaid-svg-5UxK92AL8U75LSp9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5UxK92AL8U75LSp9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5UxK92AL8U75LSp9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5UxK92AL8U75LSp9 .icon-shape,#mermaid-svg-5UxK92AL8U75LSp9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UxK92AL8U75LSp9 .icon-shape p,#mermaid-svg-5UxK92AL8U75LSp9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5UxK92AL8U75LSp9 .icon-shape .label rect,#mermaid-svg-5UxK92AL8U75LSp9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UxK92AL8U75LSp9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5UxK92AL8U75LSp9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5UxK92AL8U75LSp9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 部署状态机
update
update时备份
update时存入
rollback
package/
待部署版本
runtime/
正在运行
backup/
历史归档
rollback/
上一版本
| 目录 | 作用 | 数量约束 | 权限 |
|---|---|---|---|
runtime/ |
当前正在运行的 jar | 恰好 1 个 | 0750 |
package/ |
待部署的新版本 jar | 恰好 1 个 | 0750 |
rollback/ |
上一版本(用于回滚) | 至多 1 个 | 0750 |
backup/ |
带时间戳的历史备份 | 任意 | 0750 |
这套设计的精妙之处:任何一个时刻,看四个目录里有什么 jar,就能推断出应用处于什么状态 。而且 jar_app.sh 对异常状态有显式拦截:
bash
function get_jar_name() {
i=0
for subdir in runtime package rollback
do
number=`ls ${subdir}/*.jar 2> /dev/null | wc -l`
if [ ${number} -eq 0 ]; then
jar_name[$i]='no_exist'
elif [ ${number} -eq 1 ]; then
jar_name[$i]=$(basename `ls ${subdir}/*.jar`)
else
jar_name[$i]='too_many'
fi
let "i=i+1"
done
}
每个目录的 jar 数量有三种状态:no_exist(没有)、正常(1 个)、too_many(多个)。too_many 是危险信号------说明之前某次部署中断了或有人手动塞了文件,此时脚本直接 exit 1 拒绝继续,避免在不确定状态下操作。
最佳实践 #3 :部署脚本的所有操作前置都应该做「状态校验」。宁可拒绝执行,也不要在脏状态下继续。
check_runtime_jar()函数就是这道闸门------no_exist和too_many都会被拦截。
注意 rollback/ 目录只保留一个版本 :每次 update 时先 rm -f rollback/*.jar 再把当前版本拷进去。这是有意为之------回滚只允许回到「上一次可用版本」,而不是任意历史版本。回滚到太旧的版本往往比故障本身更危险(数据库 schema 可能已经不兼容)。
四、jar_app.sh:七命令生命周期管理
jar_app.sh 提供 7 个子命令,覆盖应用全生命周期:
bash
case "$1" in
'start') start_function ;;
'stop') stop_function ;;
'restart') restart_function ;;
'status') status_function ;;
'backup') backup_function ;;
'update') update_function ;;
'rollback') rollback_function ;;
*) echo "Usage: $0 {start|stop|restart|status|backup|update|rollback}" ;;
esac
这 7 个命令不是平铺的,它们有明确的职责分层:
#mermaid-svg-79XtkeilG2dtWldT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-79XtkeilG2dtWldT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-79XtkeilG2dtWldT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-79XtkeilG2dtWldT .error-icon{fill:#552222;}#mermaid-svg-79XtkeilG2dtWldT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-79XtkeilG2dtWldT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-79XtkeilG2dtWldT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-79XtkeilG2dtWldT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-79XtkeilG2dtWldT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-79XtkeilG2dtWldT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-79XtkeilG2dtWldT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-79XtkeilG2dtWldT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-79XtkeilG2dtWldT .marker.cross{stroke:#333333;}#mermaid-svg-79XtkeilG2dtWldT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-79XtkeilG2dtWldT p{margin:0;}#mermaid-svg-79XtkeilG2dtWldT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-79XtkeilG2dtWldT .cluster-label text{fill:#333;}#mermaid-svg-79XtkeilG2dtWldT .cluster-label span{color:#333;}#mermaid-svg-79XtkeilG2dtWldT .cluster-label span p{background-color:transparent;}#mermaid-svg-79XtkeilG2dtWldT .label text,#mermaid-svg-79XtkeilG2dtWldT span{fill:#333;color:#333;}#mermaid-svg-79XtkeilG2dtWldT .node rect,#mermaid-svg-79XtkeilG2dtWldT .node circle,#mermaid-svg-79XtkeilG2dtWldT .node ellipse,#mermaid-svg-79XtkeilG2dtWldT .node polygon,#mermaid-svg-79XtkeilG2dtWldT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-79XtkeilG2dtWldT .rough-node .label text,#mermaid-svg-79XtkeilG2dtWldT .node .label text,#mermaid-svg-79XtkeilG2dtWldT .image-shape .label,#mermaid-svg-79XtkeilG2dtWldT .icon-shape .label{text-anchor:middle;}#mermaid-svg-79XtkeilG2dtWldT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-79XtkeilG2dtWldT .rough-node .label,#mermaid-svg-79XtkeilG2dtWldT .node .label,#mermaid-svg-79XtkeilG2dtWldT .image-shape .label,#mermaid-svg-79XtkeilG2dtWldT .icon-shape .label{text-align:center;}#mermaid-svg-79XtkeilG2dtWldT .node.clickable{cursor:pointer;}#mermaid-svg-79XtkeilG2dtWldT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-79XtkeilG2dtWldT .arrowheadPath{fill:#333333;}#mermaid-svg-79XtkeilG2dtWldT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-79XtkeilG2dtWldT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-79XtkeilG2dtWldT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-79XtkeilG2dtWldT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-79XtkeilG2dtWldT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-79XtkeilG2dtWldT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-79XtkeilG2dtWldT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-79XtkeilG2dtWldT .cluster text{fill:#333;}#mermaid-svg-79XtkeilG2dtWldT .cluster span{color:#333;}#mermaid-svg-79XtkeilG2dtWldT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-79XtkeilG2dtWldT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-79XtkeilG2dtWldT rect.text{fill:none;stroke-width:0;}#mermaid-svg-79XtkeilG2dtWldT .icon-shape,#mermaid-svg-79XtkeilG2dtWldT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-79XtkeilG2dtWldT .icon-shape p,#mermaid-svg-79XtkeilG2dtWldT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-79XtkeilG2dtWldT .icon-shape .label rect,#mermaid-svg-79XtkeilG2dtWldT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-79XtkeilG2dtWldT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-79XtkeilG2dtWldT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-79XtkeilG2dtWldT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 版本管理命令
日常运维命令
内部调用
内部调用
内部调用
内部调用
内部调用
内部调用
归档命令
backup
start
stop
restart
status
update
rollback
update 和 rollback 是「编排命令」,它们内部组合调用 stop_jar + start_jar 等原子函数。start/stop/restart/status 是「原子命令」,可以单独执行。这种分层让脚本既能在 Ansible 里被编排,也能登录机器手动救火。
4.1 进程检测:用 jar 名匹配 java 进程
bash
function status_jar() {
get_jar_name
pids=`ps -f -C java --no-headers | grep -E "(/|\s)+${runtime_jar_name}" | awk '{print $2}'`
if [ -n "${pids}" ]; then
return 0
else
return 1
fi
}
ps -C java 按命令名过滤 java 进程,再用 jar 包名做二次匹配,最后取 PID。这种「软匹配」的好处是零配置------不需要额外的 PID 文件,只要知道 jar 名就能查进程。
踩坑警示 :用 jar 文件名匹配 java 进程的前提是「同机不同应用的 jar 名不相似」。如果一台机器上跑
app-1.0.jar和app-1.0.1.jar,正则app-1.0可能误伤两个进程。生产环境中,PID 文件 或唯一端口绑定是更可靠的进程标识。这份脚本用 jar 名匹配是为了零配置,代价是牺牲了一部分严谨性------适合「一机一应用」的部署模型。
4.2 启动:daemon 降权 + 超时判定
bash
function start_jar() {
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] The program is running. No need to start it again."
return 0
fi
cd ${parent_path}
if [ $UID -eq 0 ]; then
daemon --user=${user} java -server -Xms1024m -Xmx2048m -jar ${parent_path}/runtime/${runtime_jar_name} >/dev/null 2>&1 &
else
daemon java -server -Xms1024m -Xmx2048m -jar ${parent_path}/runtime/${runtime_jar_name} >/dev/null 2>&1 &
fi
timeout=${start_timeout}
step=5
for (( count=0; count<timeout; count=count+step))
do
sleep $step
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] Starting program successfully."
return 0
fi
done
# ...
}
两个关键点。
第一,root 降权启动。 当脚本以 root 执行时,通过 daemon --user=fjf 降权到普通用户启动 Java 进程。daemon 函数来自 /etc/rc.d/init.d/functions,它封装了 setsid、umask、工作目录切换等细节,比裸 nohup 更可靠。
这是安全基线------生产应用绝不应该以 root 身份运行。Ansible playbook 以 root 执行(为了建用户、改权限),但应用进程必须降权。这份脚本做到了执行权和运行权分离。
最佳实践 #4:部署脚本的执行用户和应用的运行用户应该分离。脚本可以 root,但应用进程必须降权。一个 root 跑的 Java 进程一旦被 RCE,攻击者直接拿到 root,整台机器沦陷。
第二,轮询判定启动成功。 这个版本的 start_timeout=10 秒,每 5 秒检查一次进程是否存活。这是「乐观判定」------进程还在就算成功,适合启动快、没有健康检查端点的应用。
进阶提示 :这个版本没有 HTTP 健康检查。如果应用启动慢(Spring Boot 加载 2 分钟),10 秒判定窗口太短,可能误判失败。生产环境建议增加
health_uri配置,启动时轮询 HTTP 端点返回 200 才算成功。本项目的演进版本已经加入了这个能力。
4.3 停止:两段式优雅停止
bash
function stop_jar() {
status_jar
if [ $? -eq 1 ]; then
echo "[INFO] The program is not running. No need to stop it."
return 0
fi
for pid in ${pids}; do
kill ${pid} # 第一段:SIGTERM
done
timeout=${shutdown_timeout} # 10 秒
step=2
for (( count=0; count<timeout; count=count+step))
do
sleep $step
status_jar
if [ $? -eq 1 ]; then
echo "[INFO] Shutting down program successfully."
return 0
fi
done
for pid in ${pids}; do
kill -9 ${pid} # 第二段:SIGKILL
done
echo "[WARNING] Program has been killed forcibly."
}
这是教科书式的两段式停止:先发 SIGTERM 给应用留优雅退出的窗口(10 秒),超时再 SIGKILL 强杀。
Java 应用 jar_app.sh Java 应用 jar_app.sh #mermaid-svg-EQY6fvWXlm7d8O1j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EQY6fvWXlm7d8O1j .error-icon{fill:#552222;}#mermaid-svg-EQY6fvWXlm7d8O1j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EQY6fvWXlm7d8O1j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EQY6fvWXlm7d8O1j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EQY6fvWXlm7d8O1j .marker.cross{stroke:#333333;}#mermaid-svg-EQY6fvWXlm7d8O1j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EQY6fvWXlm7d8O1j p{margin:0;}#mermaid-svg-EQY6fvWXlm7d8O1j .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EQY6fvWXlm7d8O1j text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EQY6fvWXlm7d8O1j .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-EQY6fvWXlm7d8O1j .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-EQY6fvWXlm7d8O1j #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-EQY6fvWXlm7d8O1j .sequenceNumber{fill:white;}#mermaid-svg-EQY6fvWXlm7d8O1j #sequencenumber{fill:#333;}#mermaid-svg-EQY6fvWXlm7d8O1j #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-EQY6fvWXlm7d8O1j .messageText{fill:#333;stroke:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EQY6fvWXlm7d8O1j .labelText,#mermaid-svg-EQY6fvWXlm7d8O1j .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .loopText,#mermaid-svg-EQY6fvWXlm7d8O1j .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-EQY6fvWXlm7d8O1j .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-EQY6fvWXlm7d8O1j .noteText,#mermaid-svg-EQY6fvWXlm7d8O1j .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-EQY6fvWXlm7d8O1j .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EQY6fvWXlm7d8O1j .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EQY6fvWXlm7d8O1j .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-EQY6fvWXlm7d8O1j .actorPopupMenu{position:absolute;}#mermaid-svg-EQY6fvWXlm7d8O1j .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-EQY6fvWXlm7d8O1j .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-EQY6fvWXlm7d8O1j .actor-man circle,#mermaid-svg-EQY6fvWXlm7d8O1j line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-EQY6fvWXlm7d8O1j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 触发 shutdown hook关闭线程池、刷新缓存 继续等待 alt进程已退出仍在运行 loop每 2 秒轮询 立即终止shutdown hook 不执行 alt超过 10 秒仍未退出 1. kill (SIGTERM)status_jar 检查进程return 成功2. kill -9 (SIGKILL)
SIGTERM 给了应用执行 shutdown hook 的机会------关闭数据库连接池、刷新缓存、完成在途请求。SIGKILL 是兜底,但代价是 shutdown hook 不执行,可能导致数据不一致。
最佳实践 #5 :停止用两段式:SIGTERM + 超时 + SIGKILL。
shutdown_timeout要大于应用最长优雅退出时间。这个版本设的 10 秒偏短------如果应用有大量在途请求或慢 SQL,10 秒可能来不及排空。生产环境建议 30-60 秒。
4.4 update:状态机式部署
bash
function update_function() {
get_jar_name
# 前置校验:package 必须有且仅有 1 个 jar,runtime 不能有多个
if [ "${package_jar_name}" == "no_exist" ]; then exit 1; fi
if [ "${package_jar_name}" == "too_many" ]; then exit 1; fi
if [ "${runtime_jar_name}" == "too_many" ]; then exit 1; fi
if [ "${runtime_jar_name}" == "no_exist" ]; then
# 首次部署,无需停机备份
echo "[INFO] No need to stop it or backup."
else
# 非首次:备份 → 存 rollback → 停止 → 清 runtime
cp -a runtime/${runtime_jar_name} backup/${runtime_jar_name}.bak_`date +%Y%m%d_%H:%M:%S`
rm -f rollback/*.jar
cp -a runtime/${runtime_jar_name} rollback/${runtime_jar_name}
stop_jar
rm -f runtime/*.jar
fi
# 替换 + 启动
mv -f package/${package_jar_name} runtime/${package_jar_name}
get_jar_name
start_jar
}
把 update 流程画成状态机:
#mermaid-svg-kjbeS4TurcA8OvxL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kjbeS4TurcA8OvxL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kjbeS4TurcA8OvxL .error-icon{fill:#552222;}#mermaid-svg-kjbeS4TurcA8OvxL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kjbeS4TurcA8OvxL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kjbeS4TurcA8OvxL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL .marker.cross{stroke:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kjbeS4TurcA8OvxL p{margin:0;}#mermaid-svg-kjbeS4TurcA8OvxL defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-kjbeS4TurcA8OvxL g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-kjbeS4TurcA8OvxL g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-kjbeS4TurcA8OvxL g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-kjbeS4TurcA8OvxL g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-kjbeS4TurcA8OvxL .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-kjbeS4TurcA8OvxL .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-kjbeS4TurcA8OvxL .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-kjbeS4TurcA8OvxL .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-kjbeS4TurcA8OvxL .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-kjbeS4TurcA8OvxL .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-kjbeS4TurcA8OvxL .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-kjbeS4TurcA8OvxL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kjbeS4TurcA8OvxL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kjbeS4TurcA8OvxL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kjbeS4TurcA8OvxL .edgeLabel .label text{fill:#333;}#mermaid-svg-kjbeS4TurcA8OvxL .label div .edgeLabel{color:#333;}#mermaid-svg-kjbeS4TurcA8OvxL .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-kjbeS4TurcA8OvxL .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-kjbeS4TurcA8OvxL .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-kjbeS4TurcA8OvxL .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kjbeS4TurcA8OvxL .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kjbeS4TurcA8OvxL #statediagram-barbEnd{fill:#333333;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kjbeS4TurcA8OvxL .cluster-label,#mermaid-svg-kjbeS4TurcA8OvxL .nodeLabel{color:#131300;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-kjbeS4TurcA8OvxL .note-edge{stroke-dasharray:5;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-note text{fill:black;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram-note .nodeLabel{color:black;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagram .edgeLabel{color:red;}#mermaid-svg-kjbeS4TurcA8OvxL #dependencyStart,#mermaid-svg-kjbeS4TurcA8OvxL #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-kjbeS4TurcA8OvxL .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kjbeS4TurcA8OvxL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 首次部署(runtime 为空)
非首次(runtime 有 jar)
跳过备份
cp runtime→backup(带时间戳)
rm rollback/* + cp runtime→rollback
stop_jar
rm runtime/*
mv package→runtime
start_jar
update 成功
Empty
Running
BackedUp
RollbackReady
Stopped
Cleared
Swapped
这套设计的核心假设是:目录操作是原子的 (mv 在同一文件系统下是原子的)。这比「先删后拷」安全得多------如果拷贝中断,runtime 目录就空了,应用起不来。
部署顺序也讲究:先备份 → 再存 rollback → 才停止 → 最后替换 。停止前把 rollback 准备好,这样即使替换后新版本启动失败,也能立即 rollback 恢复,不用等备份目录里翻时间戳。
最佳实践 #6 :部署的中间状态要可恢复。这份脚本通过「先备份再替换 + rollback 目录」保证了任何一步失败都能退。比起直接
rm + cp,多一步备份换来的是部署的可逆性。回滚路径要在停止前就准备好,而不是失败后才临时找。
4.5 rollback:一键回到上一版本
bash
function rollback_function() {
get_jar_name
# 前置校验
if [ "${rollback_jar_name}" == "no_exist" ]; then exit 1; fi
if [ "${rollback_jar_name}" == "too_many" ]; then exit 1; fi
if [ "${runtime_jar_name}" == "no_exist" ]; then
echo "[INFO] No need to stop it."
else
stop_jar
rm -f runtime/*.jar
fi
mv -f rollback/${rollback_jar_name} runtime/${rollback_jar_name}
get_jar_name
start_jar
}
rollback 比 update 简单:停止 → rollback 目录的 jar 移到 runtime → 启动 。注意 rollback 执行后,rollback/ 目录就空了------不支持连续回滚两次。这是有意的安全设计,防止误操作回滚到太旧的版本。
运维提示 :rollback 命令执行后,rollback 目录清空。如果需要再次回滚,要先从
backup/目录手动恢复一个版本到 rollback 目录。backup/目录保留所有带时间戳的历史版本,是最后的安全网。
五、服务注册与注销:Consul 集成
项目里有一个 dele_down_service.py,用于从 Consul 注销 critical 状态的服务:
python
# roles/deploy/templates/dele_down_service.py
service_url = 'http://{{consul_server}}:8500/v1/health/state/critical'
r = requests.get(service_url)
data = json.loads(r.content)
dele_url = []
for i in data:
serviceID = i['ServiceID']
ip = re.findall(r'\d+.\d+.\d+.\d+', i['Node'])
url = 'http://%s:8500/v1/agent/service/deregister/%s' % (ip[0], serviceID)
dele_url.append(url)
for s in dele_url:
r = requests.get(s)
逻辑是:查询 Consul 所有 critical 状态的服务 → 提取 ServiceID 和节点 IP → 调用各节点的 deregister API 注销。
但注意:这个脚本在两处都被注释掉了。
jar_app.sh 的 stop_jar 里:
bash
#/usr/bin/python dele_down_service.py >/dev/null 2>&1
initial.yml 的上传步骤:
yaml
#- name: 上传python脚本
# template: src=dele_down_service.py dest=/fjf_work/{{jobname}}/bin/ owner=fjf group=fjf mode=0755
这说明这个能力设计过但未启用。为什么?
注释掉是有原因的。这个脚本的设计有个隐患:它注销的是所有 critical 服务,不分应用。如果同一 Consul 集群里有别的应用刚好处于 critical(比如正在重启),会被一起注销掉,造成误伤。
正确的做法应该是按 ServiceID 或 ServiceName 过滤,只注销当前应用的 critical 实例:
python
# 改进版思路(伪代码)
my_service_name = '{{jobname}}'
for i in data:
if i.get('ServiceName') == my_service_name: # 只处理自己的
# ... deregister
最佳实践 #7:服务注销要「精准定位」,不能「无差别清理」。这个脚本被注释掉,说明作者意识到了风险------宁可手动处理 critical 实例,也不要让脚本误删别人的服务注册。这种「有问题先下线,宁缺毋滥」的决策是成熟的运维判断。
完整的脚本
bash
#!/bin/bash
export LANG=en_US.UTF-8
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
user=fjf
start_timeout=10
shutdown_timeout=10
# Source function library.
. /etc/rc.d/init.d/functions
#export JAVA_HOME=/usr/local/jdk-11.0.1
#export PATH=${JAVA_HOME}/bin:${PATH}
# 获取本脚本所在目录
basepath=$(cd `dirname $0`; pwd)
# 获取父目录(应用安装目录)
parent_path=`dirname ${basepath}`
cd ${parent_path}
# 先尝试获取runtime目录下的jar包名称,如没有,再获取package目录下的jar包名称
function get_jar_name() {
i=0
for subdir in runtime package rollback
do
number=`ls ${subdir}/*.jar 2> /dev/null | wc -l`
if [ ${number} -eq 0 ]; then
jar_name[$i]='no_exist'
elif [ ${number} -eq 1 ]; then
jar_name[$i]=$(basename `ls ${subdir}/*.jar`)
else
jar_name[$i]='too_many'
fi
let "i=i+1"
done
runtime_jar_name=${jar_name[0]}
package_jar_name=${jar_name[1]}
rollback_jar_name=${jar_name[2]}
}
# 检查jar包运行状态:运行,返回值为0;未运行,返回值为1。
# 本函数需提供给其它函数调用。本函数中不对runtime目录下的jar包数量做校验,交给调用它的函数进行校验。
function status_jar() {
get_jar_name
pids=`ps -f -C java --no-headers | grep -E "(/|\s)+${runtime_jar_name}" | awk '{print $2}'`
if [ -n "${pids}" ]; then
return 0
else
return 1
fi
}
# 本函数检查runtime目录下面jar包的数量。
# 本函数需提供给其它函数调用。
function check_runtime_jar() {
get_jar_name
if [ "${runtime_jar_name}" == "no_exist" ]; then
echo "[INFO] No jar package is found under '${parent_path}/runtime' directory."
exit 1
fi
if [ "${runtime_jar_name}" == "too_many" ]; then
echo "[INFO] Two or more jar packages are found under '${parent_path}/runtime' directory."
exit 1
fi
}
# 本函数实现本脚本的status功能。
# 本函数不提供给其它函数调用。
function status_function() {
check_runtime_jar
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] Jar package '${runtime_jar_name}' is running."
else
echo "[INFO] Jar package '${runtime_jar_name}' is not running."
fi
}
# 停止jar包。
# 本函数需提供给其它函数调用。本函数中不对runtime目录下的jar包数量做校验,交给调用它的函数进行校验。
function stop_jar() {
status_jar
if [ $? -eq 1 ]; then
echo "[INFO] The program is not running. No need to stop it."
return 0
fi
echo "[INFO] Shutting down program . . . . . . "
for pid in ${pids}; do
kill ${pid}
done
timeout=${shutdown_timeout}
step=2
for (( count=0; count<timeout; count=count+step))
do
sleep $step
status_jar
if [ $? -eq 1 ]; then
echo "[INFO] Shutting down program successfully."
return 0
fi
done
status_jar
for pid in ${pids}; do
kill -9 ${pid}
done
#/usr/bin/python dele_down_service.py >/dev/null 2>&1
echo "[WARNING] Program has been killed forcibly."
}
# 本函数实现本脚本的stop功能。
# 本函数不提供给其它函数调用。
function stop_function() {
check_runtime_jar
stop_jar
}
# 运行jar包。
# 本函数需提供给其它函数调用。本函数中不对runtime目录下的jar包数量做校验,交给调用它的函数进行校验。
function start_jar() {
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] The program is running. No need to start it again."
return 0
fi
echo "[INFO] Starting program . . . . . . "
# 启动程序前确保先切换到应用根目录,因有些jar程序会将它的日志写到相对启动路径的logs/目录中。
cd ${parent_path}
if [ $UID -eq 0 ]; then
daemon --user=${user} java -server -Xms1024m -Xmx2048m -jar ${parent_path}/runtime/${runtime_jar_name} >/dev/null 2>&1 &
else
daemon java -server -Xms1024m -Xmx2048m -jar ${parent_path}/runtime/${runtime_jar_name} >/dev/null 2>&1 &
fi
timeout=${start_timeout}
step=5
for (( count=0; count<timeout; count=count+step))
do
sleep $step
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] Starting program successfully."
return 0
fi
done
status_jar
if [ $? -eq 0 ]; then
echo "[INFO] Starting program successfully."
else
echo "[ERROR] Starting program times out."
exit 1
fi
}
# 本函数实现本脚本的start功能。
# 本函数不提供给其它函数调用。
function start_function() {
check_runtime_jar
start_jar
}
# 本函数实现本脚本的restart功能。
# 本函数不提供给其它函数调用。
function restart_function() {
check_runtime_jar
stop_jar
start_jar
}
# 本函数实现本脚本的backup功能。
# 本函数不提供给其它函数调用。
function backup_function() {
check_runtime_jar
cp -a runtime/${runtime_jar_name} backup/${runtime_jar_name}.bak_`date +%Y%m%d_%H:%M:%S`
echo "[INFO] Package '${parent_path}/runtime/${runtime_jar_name}' has been backed up successfully."
}
# 本函数实现本脚本的update功能。
# 本函数不提供给其它函数调用。
function update_function() {
get_jar_name
if [ "${package_jar_name}" == "no_exist" ]; then
echo "[INFO] No jar package is found under '${parent_path}/package' directory."
exit 1
fi
if [ "${package_jar_name}" == "too_many" ]; then
echo "[INFO] Two or more jar packages are found under '${parent_path}/package' directory."
exit 1
fi
if [ "${runtime_jar_name}" == "too_many" ]; then
echo "[INFO] Two or more jar packages are found under '${parent_path}/runtime' directory."
exit 1
fi
echo "[INFO] Starting to update . . . "
if [ "${runtime_jar_name}" == "no_exist" ]; then
echo "[INFO] No jar package is found under '${parent_path}/runtime' directory. No need to stop it or backup."
else
cp -a runtime/${runtime_jar_name} backup/${runtime_jar_name}.bak_`date +%Y%m%d_%H:%M:%S`
rm -f rollback/*.jar
cp -a runtime/${runtime_jar_name} rollback/${runtime_jar_name}
stop_jar
rm -f runtime/*.jar
fi
mv -f package/${package_jar_name} runtime/${package_jar_name}
get_jar_name
start_jar
echo "[INFO] Program has been updated successfully."
}
# 本函数实现本脚本的rollback功能。
# 本函数不提供给其它函数调用。
function rollback_function() {
get_jar_name
if [ "${rollback_jar_name}" == "no_exist" ]; then
echo "[INFO] No jar package is found under '${parent_path}/rollback' directory."
exit 1
fi
if [ "${rollback_jar_name}" == "too_many" ]; then
echo "[INFO] Two or more jar packages are found under '${parent_path}/rollback' directory."
exit 1
fi
if [ "${runtime_jar_name}" == "too_many" ]; then
echo "[INFO] Two or more jar packages are found under '${parent_path}/runtime' directory."
exit 1
fi
echo "[INFO] Starting to roll back . . . "
if [ "${runtime_jar_name}" == "no_exist" ]; then
echo "[INFO] No jar package is found under '${parent_path}/runtime' directory. No need to stop it."
else
stop_jar
rm -f runtime/*.jar
fi
mv -f rollback/${rollback_jar_name} runtime/${rollback_jar_name}
get_jar_name
start_jar
echo "[INFO] Program has been rolled back successfully."
}
case "$1" in
'start')
start_function
;;
'stop')
stop_function
;;
'restart')
restart_function
;;
'status')
status_function
;;
'backup')
backup_function
;;
'update')
update_function
;;
'rollback')
rollback_function
;;
*)
echo "Usage: $0 {start|stop|restart|status|backup|update|rollback}"
esac
六、开机自启:rc.local 的取舍
初始化任务里设置了开机启动:
yaml
- name: 设置文件权限
file: path=/etc/rc.d/rc.local state=file mode=0755
- name: 设置开机启动
lineinfile:
path: '/etc/rc.d/rc.local'
regexp: '^/fjf_work/{{jobname}}/bin/jar_app.sh'
line: '/fjf_work/{{jobname}}/bin/jar_app.sh start'
用 rc.local 做开机自启是传统方案,简单直接,但有局限:
| 方案 | 优点 | 缺点 |
|---|---|---|
rc.local(本项目) |
简单、无学习成本 | 不监控进程,挂了不重启;启动顺序难控制 |
| systemd service | 进程挂了自动重启、依赖管理、日志journal | 需要写 unit 文件 |
| supervisord | 跨语言统一管理、Web UI | 多一个依赖 |
rc.local 的本质是「开机时跑一次」,不提供进程守护 。如果 Java 进程 OOM 挂了,rc.local 不会拉起来。
最佳实践 #8 :
rc.local适合「开机启动」这个场景,但不适合「进程守护」。生产环境建议用 systemd 的Restart=on-failure或 supervisord 做进程守护。如果坚持用 rc.local,至少配合一个 cron 定时检查进程存活并拉起。
七、变量管理:group_vars 的作用
yaml
# group_vars/all
jobname: core-amc-job_allauto
ini
# hosts
172.18.199.134
应用名 jobname 放在 group_vars/all,所有主机组共享。目标主机写在 hosts 清单里。
这个设计很轻量------只有一个变量、一台主机。但它的价值在于应用名与部署逻辑解耦。要部署别的应用,只需:
- 改
group_vars/all里的jobname; - 把新应用的 jar 放到
roles/deploy/files/; - 改
hosts里的目标 IP。
Role 本身一行不用改。这就是 Ansible Role 可复用的核心------把变化的抽成变量,不变的写成逻辑。
进阶提示 :如果要部署多个应用,可以改用
group_vars按应用分组,或用 inventory 的 host_vars 区分。当前的单变量设计适合「一个 Role 管一类应用」的场景。
八、踩坑清单与最佳实践总结
把整个项目能提炼的经验,浓缩成一份可复用的清单:
Ansible 编排
- 入口 playbook 只做声明,逻辑全在 Role 里,保持可复用可组合
- 幂等性靠
stat+register+when显式控制「首次/非首次」分支 - 修改配置文件用
lineinfile/template,不要用shell: echo >>(会重复追加) - CentOS 7+ 的
rc.local默认无执行权限,设开机启动前必须chmod +x
目录与权限
- 用四目录结构(runtime/package/rollback/backup)表达部署状态
- 目录权限分层:
bin//logs/用0755,制品目录用0750 - 创建专用运行用户(如 fjf),执行权和运行权分离
进程与生命周期
- 进程检测用
ps -C java+ jar 名匹配,注意「一机一应用」前提 - 启动用
daemon --user降权,不要以 root 跑应用 - 停止用两段式:SIGTERM + 超时 + SIGKILL,
shutdown_timeout大于最长优雅退出时间 - update 流程:先备份 → 存 rollback → 停止 → 替换 → 启动,保证每步可回退
- rollback 只允许回到上一版本,不支持连续回滚(安全设计)
服务治理
- 服务注销要「精准定位」,不能「无差别清理」(本项目的 Consul 脚本因风险被注释)
- 有问题的能力宁可先下线,宁缺毋滥
版本管理
- 用
mv而非rm + cp做版本替换(mv在同文件系统下原子) - rollback 目录只保留一个版本;backup 目录保留带时间戳的全量历史
- 应用名抽成变量(
group_vars),Role 逻辑保持应用无关
九、局限与演进方向
公平地说,这套方案有其适用边界,也有明显的演进空间:
当前局限
- 无进程守护 :
rc.local只管开机启动,进程挂了不重启; - 无健康检查:启动判定只看进程存活,不看 HTTP 就绪;
- JVM 参数硬编码 :
-Xms1024m -Xmx2048m写死在脚本里,不同应用无法独立调优; - 单机脚本,无滚动更新:多机部署靠 Ansible 逐台执行,会有短暂不可用窗口;
- 日志直接丢弃 :
>/dev/null 2>&1丢弃了标准输出,完全依赖应用自身的日志框架。
演进路径
#mermaid-svg-XI8MFtqKIirrwytR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XI8MFtqKIirrwytR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XI8MFtqKIirrwytR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XI8MFtqKIirrwytR .error-icon{fill:#552222;}#mermaid-svg-XI8MFtqKIirrwytR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XI8MFtqKIirrwytR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XI8MFtqKIirrwytR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XI8MFtqKIirrwytR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XI8MFtqKIirrwytR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XI8MFtqKIirrwytR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XI8MFtqKIirrwytR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XI8MFtqKIirrwytR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XI8MFtqKIirrwytR .marker.cross{stroke:#333333;}#mermaid-svg-XI8MFtqKIirrwytR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XI8MFtqKIirrwytR p{margin:0;}#mermaid-svg-XI8MFtqKIirrwytR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XI8MFtqKIirrwytR .cluster-label text{fill:#333;}#mermaid-svg-XI8MFtqKIirrwytR .cluster-label span{color:#333;}#mermaid-svg-XI8MFtqKIirrwytR .cluster-label span p{background-color:transparent;}#mermaid-svg-XI8MFtqKIirrwytR .label text,#mermaid-svg-XI8MFtqKIirrwytR span{fill:#333;color:#333;}#mermaid-svg-XI8MFtqKIirrwytR .node rect,#mermaid-svg-XI8MFtqKIirrwytR .node circle,#mermaid-svg-XI8MFtqKIirrwytR .node ellipse,#mermaid-svg-XI8MFtqKIirrwytR .node polygon,#mermaid-svg-XI8MFtqKIirrwytR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XI8MFtqKIirrwytR .rough-node .label text,#mermaid-svg-XI8MFtqKIirrwytR .node .label text,#mermaid-svg-XI8MFtqKIirrwytR .image-shape .label,#mermaid-svg-XI8MFtqKIirrwytR .icon-shape .label{text-anchor:middle;}#mermaid-svg-XI8MFtqKIirrwytR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XI8MFtqKIirrwytR .rough-node .label,#mermaid-svg-XI8MFtqKIirrwytR .node .label,#mermaid-svg-XI8MFtqKIirrwytR .image-shape .label,#mermaid-svg-XI8MFtqKIirrwytR .icon-shape .label{text-align:center;}#mermaid-svg-XI8MFtqKIirrwytR .node.clickable{cursor:pointer;}#mermaid-svg-XI8MFtqKIirrwytR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XI8MFtqKIirrwytR .arrowheadPath{fill:#333333;}#mermaid-svg-XI8MFtqKIirrwytR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XI8MFtqKIirrwytR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XI8MFtqKIirrwytR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XI8MFtqKIirrwytR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XI8MFtqKIirrwytR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XI8MFtqKIirrwytR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XI8MFtqKIirrwytR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XI8MFtqKIirrwytR .cluster text{fill:#333;}#mermaid-svg-XI8MFtqKIirrwytR .cluster span{color:#333;}#mermaid-svg-XI8MFtqKIirrwytR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XI8MFtqKIirrwytR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XI8MFtqKIirrwytR rect.text{fill:none;stroke-width:0;}#mermaid-svg-XI8MFtqKIirrwytR .icon-shape,#mermaid-svg-XI8MFtqKIirrwytR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XI8MFtqKIirrwytR .icon-shape p,#mermaid-svg-XI8MFtqKIirrwytR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XI8MFtqKIirrwytR .icon-shape .label rect,#mermaid-svg-XI8MFtqKIirrwytR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XI8MFtqKIirrwytR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XI8MFtqKIirrwytR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XI8MFtqKIirrwytR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 当前: rc.local
- jar_app.sh
演进1: systemd
进程守护
演进2: 健康检查
HTTP 就绪探针
演进3: 配置外置
配置中心/环境变量
演进4: 容器化
Docker + K8s
每一步演进都在解决上一层的局限,但也引入新的复杂度。rc.local + jar_app.sh 适合「裸机、少量应用、运维人力充足」的场景------它的好处是简单透明、排障容易、没有额外依赖。当应用数量增长、可用性要求提高时,再逐步演进到 systemd、配置中心、容器化。
不是所有团队都需要一步到位上 K8s。先用简单方案把部署规范化、可回滚,比盲目上云更有价值。这份 Ansible 项目就是「规范化裸机部署」的典型样本------它把「SCP + SSH 重启」升级成了「幂等初始化 + 版本管理 + 一键回滚」,投入产出比极高。
结语
这个项目不到 200 行 YAML + 一个 Shell 脚本,却实现了生产级部署该有的全部要素:幂等、可回滚、可审计、权限分离、状态可观测。它不是最先进的方案,但它是最适合裸机场景的成熟方案。
本文基于一个真实在产的 Ansible 部署项目整理。涉及的主机 IP、应用名等已做脱敏处理。设计原则适用于任何基于 Ansible 的裸机/VM Java 部署场景。
