
简介本资源是一份面向Linux运维工程师与自动化部署初学者的Ansible高可用架构实践文档聚焦LNMP环境的一键式Role化部署与HAProxyKeepalived双活容灾方案。文档系统讲解Ansible Playbook结构设计、6大核心Rolecommon、nginx、php、mysql、haproxy、keepalived职责划分、模板变量管理及高可用切换逻辑覆盖负载均衡配置、健康检查脚本、VIP漂移机制等关键运维场景。资源为单个40KB的Word文档.docx内容精炼含完整目录结构说明、ansible.cfg与hosts配置范例、roles层级组织图示及各模块tasks/main.yml与templates/*.j2文件功能解析便于快速理解并复用代码框架。目前已有403人学习下载适合需落地生产级Web高可用架构、提升Ansible工程化能力的中级运维人员参考实践。1. 用 Ansible Role 快速部署 LNMP HAProxy Keepalived 高可用集群不是堆命令而是理清服务边界与角色分工你手头有一套 Web 应用流量开始上涨单台 Nginx PHP-FPM MySQL 已经扛不住突发请求数据库慢查询增多、PHP 进程频繁超时、用户反馈页面加载卡顿——这时候“加机器”不是目的“加稳定性”才是。但直接手动在 4 台服务器上逐台装软件、改配置、开防火墙、配 VIP、测故障转移三天都调不通。真正的高可用落地不靠人肉复制粘贴而靠可复现、可审计、可回滚的声明式编排。本方案用 Ansible Role 拆解 LNMPLinuxNginxMySQLPHP基础栈、HAProxy 四层负载均衡、Keepalived VIP 管理三类职责每个 Role 只做一件事nginx_role不碰 PHP 版本mysql_role不写 Web 配置haproxy_role不管理后端 PHP-FPM 进程。这种“职责隔离”让扩容时只需修改inventory中的web_servers组成员重跑 playbook 即可自动完成全链路部署。适合运维工程师快速交付生产环境也适合 DevOps 团队嵌入 CI/CD 流水线做环境一致性保障。2. 拆解 Role 结构为什么必须按服务边界划分而不是按服务器类型打包Ansible Role 的本质是封装“某类能力”的最小可复用单元。若把所有组件Nginx、PHP、MySQL、HAProxy、Keepalived塞进一个lnmp_ha_role会导致三个硬伤一是升级 PHP 版本时必须连带重启 MySQL 和 HAProxy二是测试阶段无法单独验证 Keepalived VIP 切换逻辑三是当业务需要将数据库迁至独立集群时该 Role 就彻底失效。因此我们严格按服务生命周期和运维权责划分 Rolerole_nginx_php仅部署 Nginx PHP-FPM含 opcache、php-fpm pool 配置绑定监听127.0.0.1:9000不暴露公网端口role_mysql部署 MySQL 8.0禁用 symbolic-links启用 slow_query_log初始化只读账号app_reader不创建业务库由应用发布流程执行role_haproxy部署 HAProxy 2.6配置frontend http_front绑定0.0.0.0:80backend web_pool指向role_nginx_php所在主机的127.0.0.1:80注意非直接代理 PHP-FPM而是代理 Nginxrole_keepalived部署 Keepalived 2.2定义vrrp_instance VI_1VIP192.168.10.100/24健康检查脚本检测 HAProxy 进程存活而非仅端口通提示Role 名称中避免使用lnmp这类组合缩写它掩盖了服务边界。role_nginx_php明确表达“Nginx 与 PHP-FPM 的协同部署”而role_mysql独立存在未来可被role_mariadb或role_postgresql替换不影响其他 Role。2.1 Role 目录结构标准化从tasks/main.yml到handlers/main.yml的协作逻辑每个 Role 必须遵循 Ansible 官方推荐结构以role_nginx_php为例roles/role_nginx_php/ ├── defaults/ │ └── main.yml # 定义 php_version: 8.1, nginx_worker_processes: 4 ├── files/ │ └── php.ini.j2 # Jinja2 模板引用 {{ php_memory_limit }} ├── handlers/ │ └── main.yml # 定义 restart_nginx 和 restart_php_fpm 两个 handler ├── tasks/ │ └── main.yml # 主任务流安装包 → 写配置 → 启动服务 → 触发 handler ├── templates/ │ ├── nginx.conf.j2 # Nginx server 块中 proxy_pass 指向 127.0.0.1:9000 │ └── www.conf.j2 # PHP-FPM pool 配置listen 127.0.0.1:9000 └── vars/ └── main.yml # 覆盖 defaults如生产环境设 php_opcache_enable: true关键点在于handlers与notify的配合tasks/main.yml中执行copy操作后若配置文件内容变更则触发restart_nginx同理修改www.conf.j2后 notifyrestart_php_fpm。这保证了“配置变更即生效”且避免重复重启。2.1.1tasks/main.yml核心片段解析为什么用service模块而非systemd- name: Ensure nginx is started and enabled ansible.builtin.service: name: nginx state: started enabled: true daemon_reload: yes # 关键确保 systemctl daemon-reload 在启动前执行 notify: restart_nginx - name: Copy nginx configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644 notify: restart_nginxdaemon_reload: yes是必须项。因为nginx.conf.j2中可能新增stream { }块为后续支持 TCP 代理预留而 Nginx 默认不加载 stream 模块需先执行systemctl daemon-reload让 systemd 识别新 unit 文件。若省略此参数service模块会报错Failed to reload nginx.service: Unit nginx.service not found.—— 这是线上高频报错根源不在 Nginx 本身而在 Ansible service 模块未同步 systemd 状态。2.2 Role 间依赖声明用meta/main.yml显式定义而非 inventory 中硬编码顺序role_keepalived依赖role_haproxy的运行状态但不能靠 playbook 中roles:列表顺序保证Ansible 不保证跨 Role 的 handler 触发顺序。正确做法是在roles/role_keepalived/meta/main.yml中声明dependencies: - role: role_haproxy vars: haproxy_check_port: 80这样Ansible 在执行role_keepalived前会先完整执行role_haproxy的所有任务并将haproxy_check_port传入其defaults/main.yml。同时在role_keepalived/tasks/main.yml中健康检查脚本明确调用#!/bin/bash # /etc/keepalived/check_haproxy.sh if ss -tln | grep :80 /dev/null; then exit 0 else systemctl stop haproxy # 主动降级避免 VIP 漂移后流量打到无响应节点 exit 1 fi注意ss -tln比netstat -tln更快且在 Alpine 等精简镜像中默认存在systemctl stop haproxy是主动干预防止 Keepalived 因检测失败反复切换 VIP 导致脑裂。3. Inventory 分组与变量设计让同一套 Role 适配开发、测试、生产三套环境Inventory 不是 IP 列表而是环境拓扑的声明。我们按角色分组而非按环境分组# inventory/production [load_balancers] lb1 ansible_host192.168.10.101 lb2 ansible_host192.168.10.102 [web_servers] web1 ansible_host192.168.10.201 web2 ansible_host192.168.10.202 [db_servers] db1 ansible_host192.168.10.301 [all:vars] ansible_userdeploy ansible_ssh_private_key_file~/.ssh/id_rsa_prod关键变量全部通过group_vars/注入# group_vars/load_balancers/haproxy.yml haproxy_frontend_bind: 0.0.0.0:80 haproxy_backend_servers: - name: web1 address: 192.168.10.201 port: 80 - name: web2 address: 192.168.10.202 port: 80 # group_vars/web_servers/nginx_php.yml nginx_worker_connections: 1024 php_max_children: 50 php_start_servers: 10 # group_vars/db_servers/mysql.yml mysql_root_password: {{ vault_mysql_root_password }} mysql_bind_address: 192.168.10.3013.1 使用hostvars动态生成 HAProxy backend避免静态 IP 列表维护haproxy_backend_servers若手动维护每次增删 Web 节点都要改 YAML。更可靠的方式是用hostvars动态收集# roles/role_haproxy/templates/haproxy.cfg.j2 backend web_pool balance roundrobin {% for host in groups[web_servers] %} server {{ host }} {{ hostvars[host][ansible_host] }}:80 check {% endfor %}这样只要在 inventory 中将新服务器加入[web_servers]组重跑 playbook 即自动加入 HAProxy 后端池。hostvars[host][ansible_host]确保取到实际 IP而非别名规避/etc/hosts解析错误。3.1.1hostvars安全边界为什么不能在defaults/main.yml中直接引用以下写法是危险的# 错误defaults/main.yml 中不能用 hostvars haproxy_backend_ips: {{ hostvars[groups[web_servers][0]][ansible_host] }}因为defaults在 playbook 解析初期加载此时hostvars尚未填充会返回空值或报错hostvars is not defined。正确位置是templates/或tasks/中的set_fact- name: Collect web server IPs ansible.builtin.set_fact: web_ips: - {%- set ips [] -%} {%- for host in groups[web_servers] -%} {%- set _ ips.append(hostvars[host][ansible_host]) -%} {%- endfor -%} {{ ips }} run_once: truerun_once: true保证只在一个节点执行避免重复收集。3.2 生产环境敏感变量加密Vault 不是可选项而是强制项数据库密码、SSL 私钥、Keepalivedauth_pass绝对不可明文存储。使用 Ansible Vault 加密group_vars/all/vault.ymlansible-vault create group_vars/all/vault.yml内容如下vault_mysql_root_password: XyZ9!qW2#eR4 vault_keepalived_auth_pass: K33pl1v3d!2024 vault_ssl_private_key: | -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAu... -----END RSA PRIVATE KEY-----在playbook.yml中引用时无需额外操作Ansible 自动解密- name: Deploy MySQL ansible.builtin.include_role: name: role_mysql vars: mysql_root_password: {{ vault_mysql_root_password }}提示vault_keepalived_auth_pass必须在lb1和lb2上完全一致否则 VRRP 协议握手失败VIP 无法建立。建议用ansible-vault encrypt_string生成单行密文再粘贴到vault.yml中。4. Playbook 编排与执行从单机验证到全集群滚动上线Playbook 是 Role 的调度中枢。我们不写一个巨型site.yml而是按阶段拆分为setup.yml、deploy.yml、ha.yml便于分步调试# setup.yml基础环境准备所有节点 - hosts: all become: true roles: - role: role_common tags: common# deploy.yml部署核心服务分组并行 - hosts: db_servers become: true roles: - role: role_mysql tags: mysql - hosts: web_servers become: true roles: - role: role_nginx_php tags: nginx_php# ha.yml高可用组件必须串行因 VIP 冲突 - hosts: load_balancers become: true serial: 1 # 关键lb1 和 lb2 逐台部署避免 VIP 抢占 roles: - role: role_haproxy tags: haproxy - role: role_keepalived tags: keepalived4.1 执行策略如何用--limit和--tags精准控制范围首次部署时先验证单节点# 只在 web1 上部署 NginxPHP跳过 MySQL 和 LB ansible-playbook deploy.yml \ -i inventory/production \ --limit web1 \ --tags nginx_php \ --ask-vault-pass确认无误后再全量执行# 全量部署 Web 层 ansible-playbook deploy.yml \ -i inventory/production \ --tags nginx_php \ --ask-vault-pass # 部署 DB 层独立执行避免与 Web 层并发 ansible-playbook deploy.yml \ -i inventory/production \ --tags mysql \ --ask-vault-pass # 最后部署 LB 层且必须串行 ansible-playbook ha.yml \ -i inventory/production \ --ask-vault-pass4.1.1serial: 1的底层机制为什么 Keepalived 必须串行部署Keepalived 的vrrp_instance启动时会发送VRRP Advertisement报文宣告自己为 MASTER。若lb1和lb2同时启动且priority相同默认 100则触发MASTER - BACKUP频繁切换导致 VIP 在两台机器间抖动客户端连接中断。serial: 1强制 Ansible 在lb1完成role_keepalived全部任务包括启动服务、等待 5 秒稳定后再执行lb2。lb2启动时lb1已是 MASTERlb2自动进入 BACKUP 状态VIP 归属稳定。4.2 验证高可用三步确认 VIP、健康检查、故障转移真实生效部署完成后必须验证而非假设VIP 是否绑定在lb1上执行ip addr show | grep 192.168.10.100 # 应输出inet 192.168.10.100/24 scope global secondary eth0HAProxy 后端是否健康访问http://192.168.10.100/haproxy?stats需在haproxy.cfg.j2中启用 stats查看web_pool下web1和web2状态为UP。模拟故障转移在lb1上执行systemctl stop keepalived # 等待 3 秒检查 lb2 ip addr show | grep 192.168.10.100 # 应出现 # 同时检查 lb1 上 HAProxy 是否已停止由 check_haproxy.sh 触发 systemctl is-active haproxy # 应返回 inactive注意check_haproxy.sh中的systemctl stop haproxy是主动防御。若仅依赖 Keepalived 自身的notify_stopHAProxy 进程可能残留导致 VIP 切换后流量仍打到故障节点。5. 故障排查与性能调优当could not add role column to users table类 SQL 报错出现时如何定位真因标题中提到的热搜词could not add role column to users table sql: you have an error in your sql表面看是 Laravel 或 Django 迁移脚本报错实则常暴露底层高可用架构缺陷。这类报错极少因 SQL 语法错误引发多因数据库连接不稳定导致事务中断。以下是典型排查路径5.1 排查链路从应用日志反推数据库连接瓶颈当应用报SQLSTATE[HY000]: General error: 1030 Got error 28 from storage engine磁盘满或SQLSTATE[HY000]: General error: 2006 MySQL server has gone away连接断开需按顺序检查层级检查命令关键指标异常表现网络层ping -c 3 192.168.10.301丢包率 0%应用连接超时MySQL 层mysqladmin -u root -p extended-status | grep Threads_connectedThreads_connected max_connections * 0.8连接数耗尽新请求被拒绝HAProxy 层echo show stat | nc -U /var/run/haproxy.sock | awk -F, {print $1,$2,$18,$19}statusUP但chkfail0后端健康检查失败流量被丢弃若chkfail持续增长说明check_haproxy.sh检测到 HAProxy 不可用但根本原因可能是 PHP-FPM 进程崩溃导致 Nginx 返回 502进而 HAProxy 标记后端为 DOWN。5.2 PHP-FPM 调优解决max_children设置不当引发的雪崩role_nginx_php中php_max_children默认设为 50但需根据内存计算# 查看单个 PHP-FPM 进程内存占用 ps aux \| grep php-fpm: pool www \| awk {sum$6} END {print sum/NR KB} # 假设为 40MB则 2GB 内存服务器最大 children 2048 / 40 ≈ 51若设为 100内存溢出后 OOM Killer 杀掉 MySQL 进程导致could not add role column报错——因为迁移脚本执行到一半连接中断表结构变更未完成。修正方式在group_vars/web_servers/nginx_php.yml中动态计算php_max_children: {{ (ansible_memtotal_mb // 40) | int }} php_start_servers: {{ (php_max_children // 4) | int }}5.3 Keepalived 日志精简过滤无关信息聚焦 VIP 切换事件Keepalived 默认日志级别过高/var/log/messages中充斥VRRP_Script执行记录。在role_keepalived/templates/keepalived.conf.j2中设置global_defs { notification_email { adminexample.com } router_id LVS_DEVEL enable_script_security # 启用脚本安全防止恶意 exec } vrrp_script chk_haproxy { script /etc/keepalived/check_haproxy.sh interval 2 weight 2 fall 2 rise 1 } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority {{ 100 if inventory_hostname lb1 else 99 }} advert_int 1 authentication { auth_type PASS auth_pass {{ vault_keepalived_auth_pass }} } virtual_ipaddress { 192.168.10.100/24 dev eth0 label eth0:1 } track_script { chk_haproxy } # 关键关闭冗余日志 notify_master /bin/true notify_backup /bin/true notify_fault /bin/true }notify_*设为/bin/true后Keepalived 只记录 VIP 状态变更如VRRP_Instance(VI_1) Entering MASTER STATE不再刷屏执行脚本日志便于快速定位切换时间点。提示fall 2表示连续 2 次检测失败才触发状态切换避免网络抖动误判rise 1表示恢复 1 次即认为正常加快故障恢复速度。本文还有配套的精品资源点击获取