Web集群初始化实战:6台裸机标准化配置全流程
这活儿说起来不大但坑是真不少。一个新建的 web 集群项目6 台工作主机刚从机架上架系统是装完了剩下全是原始状态——没有统一用户、没配时间同步、防火墙规则各自为政连主机名都是默认的 localhost。如果这个时候急着往上搭业务后面一定会被各种“基础不一致”拖死这边连不上数据库那边日志时间差了 8 个小时排查问题的时候光是“环境都不一样的锅”就能耗掉大半天。所以这个阶段的核心任务只有一个趁业务还没上把 6 台主机的初始化配置一次性做扎实、做成标准化。这篇文章就以我这次实际操作为例把这套初始化的完整思路和细节拆开来讲。适合正在搭集群环境的运维新人也适合那些已经踩过坑、想把自己手头流程重新捋一遍的同行。下面所有命令和步骤都是我在这套环境里实际执行过的可以直接拿来抄作业。1. 初始化前的规划6 台裸机先定规矩再动手1.1 这次集群的拓扑与资源情况先说下这批机器的基本情况6 台工作主机全部是物理服务器统一走千兆内网互联。硬件配置基本一致每台 16 核 CPU、64GB 内存、两块 480GB 的 SSD 系统盘加两块 4TB 的机械盘作为数据盘。这种配置跑一个中等规模的 web 集群是够用的但正因为硬件一样才更需要软件层面的初始状态也完全一致否则等于白瞎了这套齐整的硬件。我的规划思路是6 台机器中其中 2 台承担前端流量接入和负载分发另外 4 台作为实际的工作节点运行业务进程。不过这里有个原则问题——初始化的内容不应该因为角色不同而做差异化太早。所有机器先做同一套基础初始化后续角色差异通过单独的角色配置来叠加而不是从一开始就各配各的。这样做的好处非常明显任何一台机器出现问题都可以随时被替换成另一台同配置的机器不会出现“这台机器是特殊的不能动”这种运维噩梦。1.2 主机命名和 IP 规划小决定影响半年排查效率主机名这个东西很多新手觉得无所谓反正 IP 能通就行。但实际上主机名决定了你以后看监控、看日志、查告警的时候能不能一眼定位问题。这次我用的命名规则是web-node-01到web-node-06前两台01、02作为流量入口节点后四台03 到 06作为工作节点。IP 规划上采用内网静态 IP范围定在10.10.20.11到10.10.20.16预留了.21到.30这一段给后续扩容。注意不要把 IP 的编排和实际机架上网络端口的对应关系搞混我在配置前专门拿一张表记录每台机器的物理位置、IP 地址、主机名三者的对应关系随手上机操作的时候随时对照。还有一个小细节/etc/hosts 里把 6 台机器的主机名和 IP 全部写进去。这一步很多人忽略但后面跑配置管理工具、做分布式日志采集、配监控 agenthosts 文件里的完整映射能帮你省掉大量“DNS 没通”的排查时间。内网环境不见得有 DNS 服务器就算有静态映射也更稳。1.3 磁盘分区与目录规范给数据盘单独一条路磁盘分区是初始化里最不能图省事的部分。这批机器系统盘是 480GB SSD数据盘是 4TB 机械盘。我采用的分区方案是系统盘只做/根分区和/boot启动分区不单独划/home数据盘整块单独分区、格式化后统一挂载到/data。目录规范上在/data下按业务职能预设了几个目录/data/logs应用日志、/data/www站点代码、/data/backup备份文件。这里有一个非常关键的挂载点选择逻辑日志和站点数据必须放在数据盘上绝不能堆在系统盘。否则一旦日志膨胀把根分区写满轻则服务异常重则整机起不来。我在/etc/fstab里统一采用 UUID 方式挂载而不是设备名/dev/sdb1因为设备名在重启后有极小概率因为内核识别顺序变化而漂移UUID 则是文件系统级别的唯一标识稳得多。1.4 系统版本选型这次为什么没用旧版系统版本我们统一选了 Rocky Linux 9.x。原因很直接我们团队前几年的存量业务还在用旧版系统但新项目从维护周期和软件兼容性考虑不应该再继续用已经停止常规维护的老版本了。Rocky 9 和主流云厂商镜像兼容性不错dnf 包管理操作也更顺手。这里顺带说一句如果你想照抄这篇文章但团队里用的是 Debian 系那只需要把包管理器从 dnf 换成 apt其他初始化思路完全通用。集群初始化重点根本不在具体命令而在“标准化”和“可复现”这两个词上。2. 系统层初始化清单包源、时钟、内核参数、文件句柄2.1 包管理源统一先解决“从哪下载”的问题系统装完第一件事就是统一包管理源。新装系统的默认源指向官方站点在国内网络的稳定性大家都懂而且默认源的软件版本可能和我们内部要求不一致所以需要统一替换。我们这次做法是将 dnf 源指向内网镜像仓库或者团队规定的公共镜像站具体域名按各自环境替换即可同时也把 EPEL 源加上因为后续不少运维工具要从 EPEL 里装。替换之后一定跑一遍dnf clean all dnf makecache确认源可用。这里有个经验不要把所有源都混在一起一股脑往上加先只保留系统和 EPEL其他第三方源哪个软件需要再单独加。源加得太多太乱后面dnf update的时候极易出现依赖冲突那种情况比不换源更痛苦。2.2 时间同步日志差 8 小时的锅最好别背集群环境里时间同步不是“建议”是“必须”。分布式系统里各节点时间不一致会导致日志时间线错乱、缓存失效、认证票据验证失败排查的时候极其狰狞。我们这次统一用 chrony 做时间同步服务。配置上把/etc/chrony.conf的 server 指向内网时间服务器没有内网时间源的就指向公共 NTP 服务器这里不展开具体域名按团队实际环境填写。同时把allow段放开内网网段让这 6 台机器内部可以互相授时后面就算外网抖动也不会导致集群内时间漂移。执行完systemctl restart chronyd systemctl enable chronyd之后用chronyc sources -v验证时间源状态确认输出为^*才是已同步。这里也顺手把时区设置了用timedatectl set-timezone统一设置为项目所在地时区我们按东八区处理让date命令显示的时间与业务日志一致避免后期写日志采集规则时还要额外换算时区纯属给自己找罪受。2.3 内核参数优化让系统更适合高并发 web 场景默认内核参数对桌面或者小型服务够用但对 web 集群来说需要调整。我把关键参数写进/etc/sysctl.d/99-web-cluster.conf这样既不会污染系统默认配置又方便统一管理。主要调了以下几项net.core.somaxconn握手队列长度默认 128业务量稍微上来就容易丢连接我调整为 65535。net.ipv4.tcp_max_syn_backlog半连接队列长度同样调整为 65535。net.ipv4.ip_local_port_range本地端口范围从默认的 32768 到 60999 扩成 1024 到 65535保证高并发下出站连接不因为端口池枯竭而失败。net.ipv4.tcp_tw_reuse开启 TIME_WAIT 复用这里提醒一句这个参数只配合tcp_timestamps开启时有效所以还要确认net.ipv4.tcp_timestamps没被关掉。vm.swappiness降低到 10避免系统过早使用 swap 导致性能抖动。fs.file-max提高到 2097152配合后面要配的 ulimit 使用。调整完执行sysctl -p /etc/sysctl.d/99-web-cluster.conf让它立即生效。这里有个坑我后面专门讲就是sysctl -p不带文件路径时只读/etc/sysctl.conf你写在/etc/sysctl.d/下的规则根本不会加载当时我差点以为自己的参数写错了。2.4 文件描述符与进程数限制ulimits 配置的细节web 服务连接数高了文件描述符不够用是最常见的故障之一。我把nofile文件描述符上限和nproc进程数上限都做了调整配置文件写在/etc/security/limits.d/99-web-cluster.conf* soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535注意这里我用的是通配符*对所有用户生效。但这里有个很烦人的细节通过 SSH 登录的会话ulimit 可能会被 PAM 模块重新设置光改 limits.conf 不一定会生效。我后面验证的时候发现没有效果最后确认需要在/etc/pam.d/sshd里确认pam_limits.so这一行没有被注释掉同时/etc/ssh/sshd_config里要设置UsePAM yes再重启 sshd 服务才能真正把文件描述符限制改过来。这个过程也是我踩坑踩出来的后面在踩坑部分再展开。3. 安全基线加固ssh、防火墙、SELinux、部署账号3.1 SSH 加固从密码登录切换到密钥登录6 台机器初始状态都开着密码登录这在一个集群里是非常危险的状态任何一台密码泄露就等于整个集群暴露。我的加固步骤是首先在管理机生成一对密钥然后用ssh-copy-id批量将公钥分发到 6 台主机的 root 用户这一步要求先把密码登录开着所以执行顺序很关键——先分密钥确认密钥能顺利登录再关密码登录。密钥部署完成后修改/etc/ssh/sshd_config的关键项PermitRootLogin改为prohibit-password、PasswordAuthentication改为no、PubkeyAuthentication保持yes。改完用sshd -t检查语法然后systemctl restart sshd。这里有一个必须反复确认的点sshd -t通过不代表配置没问题ssh 服务重启绝对不能断开当前连接之前做。正确的做法是开两个终端一个保持现有连接另一个用来重启 sshd 后重新建立新连接验证密钥登录可用确保万无一失再退出旧会话。如果只有你一个人在操作那就修改配置后先不重启等确认新连接能建立再说。3.2 防火墙规则先拒绝一切再放行该用的防火墙我直接用 firewalld6 台机器采用同一套规则策略。思路是默认 zone 设置为 drop丢弃所有未显式放行的流量然后逐条放行。需要放行的端口按角色来区分所有机器放行SSH端口并限定来源 IP 仅为内网管理网段前端两台机器放行 80、443 给外部访问后四台工作节点只需要放行内网网段互相访问的端口不直接暴露任何公网端口。命令用firewall-cmd --permanent --add-rich-rule来加来源限制比如只允许内网段访问 SSH而不仅仅是简单--add-port。这里要给个中肯的建议别在初始化阶段把所有业务的端口一次性放完。业务还没部署你根本不知道最终哪些端口要开一次性开完等于给自己挖坑。先把基础管理通道放行后面按业务需求逐条加用firewall-cmd --reload生效每次都确认是不是真的需要这个端口。SELinux 方面我们的策略是保持 Enforcing 模式不关闭。当前很多业务镜像和部署脚本对 SELinux 兼容性没那么好但 Linux 发行版默认开启它是有理由的。这里我给一个折中方案保持 Enforcing真遇到被 SELinux 拦截的确认是业务文件上下文标注问题用chcon或semanage修正上下文而不是直接setenforce 0。如果团队运维能力有限也可以明确选择关闭但要写进初始化文档里的已知风险项由负责人决策而不是某个人顺手就关了。3.3 部署账号与 sudo 权限不用 root 跑业务集群里所有业务进程我都不建议用 root 直接跑。这次我统一创建了一个部署账号比如叫webapp6 台机器用户 UID 都保持一致后面做文件同步和代码发布的时候权限问题好查很多。创建命令是useradd -m -s /bin/bash webapp。给webapp配置sudo权限使用/etc/sudoers.d/webapp这个单独文件内容按需写部署用户不需要全量 root 权限我给了它免密执行所有命令的权限实际很多发布系统也是这么要求的如果你介意可以直接限制到特定命令。但至少比让服务跑在 root 下要安全得多也能在审计日志里看到是谁在哪个时间执行了什么操作。4. 运行时环境安装JDK、Node、Nginx 与日志目录4.1 语言运行时避免每次部署时临时装的尴尬这个 web 集群里跑的既有 Java 应用也有 Node 服务所以在初始化阶段就要把两套运行时统一装好。Java 环境我们统一装 OpenJDK 17用包管理器直接装而不是手动下载 JDK 压缩包。原因很简单包管理器装的 JDK 有完善的卸载和升级机制后续打补丁省事。Node 方面我们用了一个专用的版本管理工具来装固定在一个 LTS 版本上方便后续多版本共存也方便不同业务需求的切换。环境变量统一写在/etc/profile.d/下让所有用户登录都能直接使用避免每个用户自己在~/.bashrc里写半天最后大家的环境还不一致。4.2 反向代理与 Web 服务器Nginx 提前就位6 台机器全部预装 Nginx注意这里说的是全部包括后四台工作节点。为什么因为后面不管业务怎么编排本地总需要一个能快速响应健康检查或静态文件的 web 服务入口统一预装 Nginx 可以保证这一点。前端两台机器额外安装并启用 Nginx 作为流量入口配置暂时留默认等业务部署阶段再按真实场景写 server 块。Nginx 的编译安装参数这里不多说了我们直接用 dnf 装官方仓库的稳定版省心而且后续升级方便。4.3 日志目录规划统一路径避免采集规则到处改日志是所有运维排查问题的第一手资料但日志路径如果每台机器都不一样那监控系统、日志采集 agent 的配置会写得非常痛苦。这次我在/data/logs下按组件预设了子目录比如/data/logs/nginx和/data/logs/app同时统一了 Nginx 的 access log 和 error log 路径全部指向这些预置目录。权限上把/data/logs属主调整为webapp业务进程写入日志不需要再额外授权。这一步看起来很简单但它是后面日志平台接入时最省心的一步。4.4 容器运行时先说清楚为什么这次没启用很多团队现在搭集群已经默认容器化了但这次项目初始化阶段我并没有在 6 台机器上预先安装完整的容器运行时环境。原因是这个项目的业务形态还处于评估阶段部署方式未必往容器化方向走过早把容器环境装好除了多占一点资源外不会带来什么收益。但我做了另一件事把容器运行时会用到的内核参数、cgroup 相关配置全部提前调好并且确认了系统内核版本满足主流容器引擎的运行要求。这样后面真要容器化只需要再装运行时组件不必回头啃系统层面的硬骨头。这也是初始化配置里一种常见的“预留接口”的思路要在文档里写清楚“这里为什么没装”“什么时候应该装”。5. 6 台机器怎么批量初始化从手工敲命令到配置管理5.1 为什么最终选了配置管理工具而不是挨个敲6 台机器说多不多说少不少。有人可能会觉得“就 6 台我挨个登上去敲一遍命令不就行了”。但我强烈不建议这么做原因有两条第一手敲命令总会有疏漏第一台敲到第六台的时候很可能漏了哪条配置自己都没注意第二也是最关键的手工操作没有可复现性。现在内存里记住的是 6 台等下个月要扩到 10 台你就得再敲一遍到时候谁能保证还记得当初敲了哪些所以这次初始化我用 Ansible 来做。它默认走 SSH不需要在目标机器上装客户端这正好适合当下的“从零初始化”阶段。我们把所有配置步骤写成 playbook执行一遍之后 6 台机器的状态完全一致任何一台配置漂移了再跑一遍 playbook 就能拉回标准状态。5.2 主机清单与核心 Playbook 的写法Ansible 的 inventory 文件这一步不要偷懒注释写得越清楚越方便后期维护。我的写法大致是[web_node] web-node-01 ansible_host10.10.20.11 web-node-02 ansible_host10.10.20.12 web-node-03 ansible_host10.10.20.13 web-node-04 ansible_host10.10.20.14 web-node-05 ansible_host10.10.20.15 web-node-06 ansible_host10.10.20.16 [web_frontend] web-node-01 web-node-02 [web_worker] web-node-03 web-node-04 web-node-05 web-node-06Playbook 的结构上我把初始化任务拆成几个大块每一块内部保持幂等——重复执行不会改变系统状态。比如处理系统配置的 tasks 大概长这样只列核心片段完整内容在项目文档里- name: Set hostname for each node hostname: name: {{ inventory_hostname }} - name: Write systemd sysctl config copy: src: files/99-web-cluster.conf dest: /etc/sysctl.d/99-web-cluster.conf notify: reload sysctl - name: Configure ulimits copy: src: files/99-web-cluster.conf dest: /etc/security/limits.d/99-web-cluster.confhandlers 部分主要是定义reload sysctl、restart chronyd、restart sshd这类动作这里不展开但要说一下思路初始化 playbook 要设计成可以反复执行一旦你对某个配置不满意改掉文件内容再跑一遍就同步到所有机器这个威力是手工敲命令完全比不上的。5.3 分批次执行与幂等性验证第一次跑 playbook 的时候我不建议 6 台同时全量执行原因只有一个出了问题不好排查。我习惯的做法是先用--limit web-node-01只对第一台机器执行把整条链路跑通检查没有任何报错之后再对其余机器全部执行。执行完毕后连续再跑一遍 playbook观察输出应该全是ok状态没有changed这代表系统已经处于配置的目标状态幂等性验证通过。这一步是配置管理的灵魂第一次跑通不算通第二次跑仍然是干净的 ok才说明这套配置是可靠的。然后我会顺手跑一遍后面讲的验收巡检脚本确认 6 台机的关键配置完全一致。6. 初始化完成的验收巡检怎么确认 6 台机器真的完全一致6.1 一次性巡检脚本几个命令读出一致性所有配置完成后我写了一个小小的巡检脚本用来快速核对 6 台主机的关键状态。脚本本身没什么高科技就是依次 SSH 到每台主机执行固定的几个命令然后把输出汇总起来对比。核心检查项包括hostname确认主机名是否符合规划timedatectl确认时间同步状态和时区sysctl vm.swappiness等参数确认内核参数生效值ulimit -n确认文件描述符限制已经是 65535firewall-cmd --state确认防火墙运行正常df -h | grep /data确认数据盘正确挂载脚本输出格式上我会加一个简单的分隔符和主机名前缀比如for host in web-node-01 web-node-02 web-node-03 web-node-04 web-node-05 web-node-06; do echo $host ssh $host hostname ulimit -n systemctl is-active chronyd systemctl is-active firewalld df -h /data done把 6 台机器的输出并排放在一起看哪台有问题一眼就能扫出来。这个脚本不用写得非常复杂它的核心价值是让“验收”从“感觉没问题”变成“有据可查”。6.2 六台主机差异对比比逐台看更省事上面那个脚本虽然能看到每台机器的状态但输出比较长人工对比依然费眼神。我这次又加了一个小环节用ansible all -m command -a hostname; date; cat /etc/hostname做一次批量执行把结果统一拿到控制台。Ansible 天然会对不同主机的输出按主机分组展示异同之处非常直观。更彻底的差异检查用ansible的fetch模块来拉取关键配置文件比如/etc/ssh/sshd_config、/etc/sysctl.d/99-web-cluster.conf拉到本地用diff逐一比对。这里我的个人经验是配置文件不要只看内容相同还要看权限和属主是否一致曾经就遇到过一次配置内容完全一样但权限是 644 和 600 的不同导致某台机器 SSH 拒绝加载密钥的怪问题。所以巡检脚本里可以加上对关键文件权限位和属主的检查。6.3 容易被忽略的验收项hosts、sudo、时区、内核启动项最后列几个我每次都会单独复核、但新手经常忽略的验收项/etc/hosts里的 6 条映射是否都在以及本机 hostname 是否出现在映射里Ansible 批量建连时会依赖它。webapp用户在每台机器的 UID 是否完全一致执行id webapp对比不一致会影响后续 NFS 共享和文件同步权限。内核参数是否在/etc/sysctl.d/里配置且在运行值中生效避免出现“机器重启后配置丢了”的情况。sshd_config的PermitRootLogin值确认六台机器改的状态完全一样避免出现管理机连接 A 机器用 root 密钥能登录、连接 B 机器却被拒绝这种诡异现象。验收这一步做得好不好直接决定了这个集群后面运维顺不顺手。多花半小时做差异比对后面省下的是几十倍的排查时间。7. 踩坑记录这几个问题让我多干了半天活7.1 默认主机名没改干净Ansible 连接时错乱第一次用 Ansible 跑批量命令时发现有几台机器返回的hostname不是预期的web-node-xx。排查下来原因是系统安装时用的是 cloud-init 或者安装程序生成的临时主机名虽然我用hostnamectl set-hostname设过了但/etc/hosts里旧主机名映射还在导致部分工具仍然解析到旧值。解决方式是把/etc/hosts里旧的localhost.localdomain行清理掉统一替换成规划里的主机名和 IP 映射然后重启systemd-hostnamed并重新登录会话确认。这个坑也让我意识到改主机名不仅仅是改一个文件还要同步清理 hosts 和相关应用缓存的引用。7.2 SSH 批量分发公钥时的指纹确认卡住用ssh-copy-id给 6 台机器分发密钥时每一台第一次连接都会弹出“确认主机指纹”的交互提示。如果一台台手动确认6 台还好说后面扩展到 30 台就会疯掉。我的处理方式是在命令里加上-o StrictHostKeyCheckingaccept-new选项这样第一次连接会自动接受新的主机指纹不需要人工交互。这里顺便提醒一下-o StrictHostKeyCheckingno是完全禁用检查安全性较差accept-new则只是对未知主机自动信任、已知主机指纹变化时仍然会警告作为初始化阶段的一次性操作是合理的选择。后面等主机指纹固化后建议再恢复为默认检查强度。7.3 sysctl 参数改了不生效路径加载顺序问题当时我把内核参数写进/etc/sysctl.d/99-web-cluster.conf后执行sysctl -p结果提示文件不存在让我愣了一下。后来发现sysctl -p默认只读取/etc/sysctl.conf并不会自动加载/etc/sysctl.d/下的所有文件。正确做法是执行sysctl -p /etc/sysctl.d/99-web-cluster.conf显式指定文件路径或者干脆执行sysctl --system让它按系统默认顺序加载全部配置。而且这个优先级还有个规则多个配置文件里有相同参数时文件名排序靠后的覆盖靠前的所以我们的核心参数全放在99-web-cluster.conf里用最大编号确保覆盖任何发行版自带的默认配置。7.4 ulimit 的 nofile 和 nproc 傻傻分不清楚配置 ulimit 时我把nofile和nproc都写成了 65535想着反正是改大就没细想。结果在某个机器上执行ulimit -u查看进程数限制时发现还是 4096系统默认值。排查了一会发现问题是nproc这个限制在 RHEL 系系统里同时受到 systemd 的TasksMax和用户 systemd 会话的限制光改/etc/security/limits.d/不全面。另外更隐蔽的是nproc配置在 limits.conf 里对 root 用户其实是无效的因为 root 被特殊对待。所以如果你需要为业务用户限制进程数要确认登录方式是通过 SSH 还是 systemd service 启动的后者需要在 service 文件里显式设置LimitNPROC。这些细节在初始化文档里最好都标注一下。结尾的几句实在话这次 6 台工作主机的初始化配置做下来最大的体会就一句话集群初始化这件事慢即是快。前期花时间把命名规范、目录规范、安全基线和自动化工具一次性铺好看起来比“先装上业务再说”慢了半拍但从整个项目周期来看这点时间投入是性价比最高的投资。后面无论扩容、排障还是变更每一件都能顺着这套基础快速落地而不是每次都在“这台机器环境怎么跟那台不一样”这种问题上扯皮。最后分享一个小技巧把整个初始化过程中用到的配置文件按99-web-cluster.conf这种编号风格统一命名统一放进各自的/etc子目录下以后不管是查问题还是做合规审计相关人员看到文件编号就能知道这是我们集群初始化加进去的规则不是系统自带的杂项。这个习惯我保留了很多年每次接手新环境都能少走不少弯路。