
1. 项目背景与node2的角色定位先说清楚这个项目是干嘛的。中州养老是一套面向养老机构的信息化管理系统HM是其中一个核心服务模块负责业务数据的采集、处理和上报。这套系统采用分布式部署架构规划了两个Linux节点node1承担数据库和基础中间件node2承载HM服务本身以及配套的缓存、消息队列和反向代理。整条链路说白了就是node2把业务服务跑起来然后和node1的数据层对接。我在这次部署中负责的是node2的完整配置。相比node1node2更偏应用侧配置的核心不再是调优数据库参数而是把服务运行环境、依赖组件、网络通信、日志收集、守护进程这些层面的工作一次做到位。整个配置过程踩了不少坑尤其是服务启动失败后的排查思路值得整理成一篇经验记录。如果你是第一次接触类似的多节点部署项目或者正准备给一套Linux服务做上线配置这篇文章可以帮你把node2这类的应用节点配置避坑点提前看清。文中所有操作基于Linux环境不涉及任何商业工具全部使用开源组件和系统原生能力。2. 部署前的整体设计思路2.1 node2为什么要单独配置而不是复用node1的模板有人会问既然都是Linux服务器为什么不能把node1的配置直接复制到node2这个想法看起来很省事但在实际项目中基本不可行。原因有三点。第一两个节点的职责不同node1偏重数据层对磁盘IO、文件句柄数、内核参数的要求很高node2偏重应用层对网络连接数、进程并发数、内存分配策略更为敏感。如果沿用同一套配置要么node2的资源配额不够要么白白浪费系统资源。第二HM服务本身有独立的依赖链比如它依赖特定版本的运行环境、缓存组件和消息队列这些在node1上不一定有盲目复用配置会导致启动阶段就报依赖缺失。第三两套机器即使都是同一系统版本硬件配置、网络分区、安全策略也可能有差异不能想当然。所以我的做法是先明确node2的职责边界再针对职责逐项配置不做无脑拷贝也不遗漏节点间的互联互通。2.2 HM服务的技术栈与依赖梳理在动手配置前我把HM服务的依赖项仔细梳理了一遍。HM本身是一个JAVA系的服务运行需要JDK环境同时对外提供HTTP接口所以前面要架一层Nginx做反向代理。业务数据要缓存于是引入了Redis异步任务需要解耦配置了消息队列而服务注册发现则用到了Nacos。这些组件多数部署在node1但node2作为服务提供方必须确保自己的运行环境和网络配置与这些组件兼容。我另外把系统层面的依赖也列出来了时钟同步、主机名解析、防火墙策略、文件描述符上限、swap策略。这些项目平时不起眼但任何一个出问题都会让HM服务的表现变得诡异比如请求超时、连接被拒、启动报OOM。依赖清单整理成了一张表方便后续配置时逐项核对依赖项用途配置位置JDK 11运行HM服务node2Redis 6.x缓存业务数据node1node2配置连接RabbitMQ异步消息node1node2配置连接Nacos 2.x服务注册发现node1node2注册Nginx 1.20反向代理node2MySQL 8.x持久化存储node1node2连接Chrony时钟同步node22.3 硬件配置评估与资源分配node2的硬件配置是16核CPU、64GB内存、SSD数据盘。对于HM这类偏IO密集型又带一些计算量的业务服务这套配置的余量还是够的。但我要说句实话配置过程中最容易翻车的地方不是硬件不够而是JVM参数和系统资源上限不匹配。比如默认的文件描述符上限是1024而HM服务启起来之后连接池、日志文件句柄、socket连接加起来很容易超过这个值。如果不在部署前把limits.conf和systemd服务配置里的LimitNOFILE调到65535服务运行一两天后就会出现句柄耗尽表现为接口偶尔报错、日志里刷“Too many open files”。内存方面我给JVM堆分配了16GB元空间512MB留给系统缓存和Redis客户端连接的内存大概还有四十多GB整体是够用的。但需要注意这里不能贪心把堆调太大否则系统本身的内存不足会触发OOM Killer把HM进程直接杀掉。3. node2系统基础环境配置3.1 主机名、网络与hosts解析拿到node2这台机器后第一件事不是装软件而是把操作系统的身份和网络配置好。主机名我用node2这个规范命名改了之后同时更新/etc/hosts把node1和node2的内网IP都写进去。这样一来服务配置里可以直接用主机名互相调用不用记IP后续迁移也方便。具体操作是这样# 设置主机名 hostnamectl set-hostname node2 # 编辑hosts文件 vi /etc/hosts在hosts文件里加上两行实际IP按项目环境替换192.168.10.11 node1 192.168.10.12 node2改了hosts之后一定要验证一下node1和node2能不能互相ping通域名解析是否正常。我习惯用getent hosts node1来验证解析比直接ping更清楚。3.2 关闭防火墙或放行指定端口这是一个非常容易被忽略但影响巨大的步骤。HM服务涉及多个端口包括Nginx的80/443、HM服务本身的8080、以及Nacos注册用的8848等。如果防火墙没有放行这些端口服务进程虽然能启动但从外部访问时一直连接超时排查起来极其痛苦。我在这台机器上的做法是先确认服务器所在网络环境是内网隔离还是公网可达。如果是内网部署直接关闭防火墙服务是最省事的systemctl stop firewalld systemctl disable firewalld如果是公网环境就不能直接关了而是要把需要的端口逐个放行firewall-cmd --permanent --add-port80/tcp firewall-cmd --permanent --add-port8080/tcp firewall-cmd --reload另外SELinux也是一个大坑。默认的enforcing模式会让Nginx无法代理到本地服务端口表现为访问Nginx返回502。建议在部署阶段先临时关闭SELinuxsetenforce 0 sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config注意这些操作要在系统上线前完成并且要经过安全评审。内网环境可以适当放宽公网环境必须细化到端口级别控制。3.3 统一时间同步分布式系统里时间不同步是个隐形杀手。HM服务产生的业务数据带有时间戳如果node2的时间比node1快了几分钟查数据库的时候就会出现“数据还没产生”的怪现象。更严重的是如果用了基于时间戳的token校验机制时间偏差直接导致鉴权失败。我的做法是安装chrony并让它向内网的NTP服务器同步yum install -y chrony systemctl enable chronyd systemctl start chronyd然后用chronyc sources -v检查同步状态。正常情况下应该能看到同步源处于^*状态表示已经完成同步。如果长时间处于^?状态说明网络到NTP服务器不通需要排查路由或防火墙。3.4 创建专用运行用户与目录规划我不会直接用root跑HM服务这是运维规范问题。单独创建一个hm用户归属到一个独立的用户组然后规划好服务的安装目录、日志目录和数据目录。目录规划我推荐这种布局/opt/hm ├── app # HM服务程序包 ├── logs # 日志文件 ├── config # 配置文件 └── data # 本地数据文件创建用户的命令groupadd hm useradd -g hm -d /opt/hm -s /bin/bash hm mkdir -p /opt/hm/{app,logs,config,data} chown -R hm:hm /opt/hm之后所有和HM服务相关的进程都通过这个用户启动避免权限过大的问题。Nginx的worker进程也建议改成hm用户运行而不是默认的nobody。3.5 文件描述符与内核参数调整刚才提到过文件描述符的问题。在这里系统性调一下首先修改/etc/security/limits.conf在文件末尾加上hm soft nofile 65535 hm hard nofile 65535 hm soft nproc 65535 hm hard nproc 65535其次因为HM服务是通过systemd管理的还需要在service文件里显式声明LimitNOFILE65535 LimitNPROC65535另外网络层的内核参数也值得顺手优化特别是TIME_WAIT连接数偏多的时候net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_tw_reuse 1 net.ipv4.ip_local_port_range 1024 65535修改/etc/sysctl.conf后执行sysctl -p使其生效。4. HM核心依赖组件的安装配置4.1 JDK环境的安装与版本管理HM服务端要求的JDK版本是11。我在部署时没有直接用包管理器安装而是从官方下载的tar包解压放在/opt/jdk11目录下然后用update-alternatives管理版本。tar -zxvf jdk-11.0.20_linux-x64_bin.tar.gz -C /opt/ mv /opt/jdk-11.0.20 /opt/jdk11配置环境变量cat /etc/profile.d/jdk.sh EOF export JAVA_HOME/opt/jdk11 export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar EOF source /etc/profile.d/jdk.sh验证安装java -version输出中能看到openjdk version 11.0.20就代表OK了。这里有个小经验JDK版本不能只看主版本号HM服务可能对JDK的build号有要求如果启动报UnsupportedClassVersionError多半是JDK版本低了。4.2 Nginx反向代理配置在node2上Nginx的职责是把外部请求按照URL路径转发到HM服务监听的端口同时承担部分静态资源的直接返回减轻后端服务压力。安装Nginxyum install -y nginx核心配置写在/etc/nginx/conf.d/hm.confupstream hm_backend { server 127.0.0.1:8080 max_fails3 fail_timeout30s; keepalive 32; } server { listen 80; server_name hm.example.cn; access_log /var/log/nginx/hm.access.log main; error_log /var/log/nginx/hm.error.log warn; location / { proxy_pass http://hm_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 15s; proxy_read_timeout 60s; proxy_send_timeout 60s; } location /static/ { alias /opt/hm/static/; expires 7d; access_log off; } }配置里我特意加了keepalive 32这个参数能复用后端连接减少TCP握手次数对高并发场景的吞吐量提升非常明显。如果不加每个请求都会新建一条到后端的TCP连接Nginx的转发效率会低不少。检查配置并重载nginx -t systemctl reload nginx4.3 Redis客户端连接配置Redis是部署在node1上的node2这边不需要安装服务端但要确保HM服务能通过内网IP正常连接。HM的配置文件里需要写入Redis的连接信息。因为redis默认监听127.0.0.1node1上必须修改配置让它监听内网IP# 在node1上修改 /etc/redis.conf bind 192.168.10.11 127.0.0.1 protected-mode no requirepass yourpasswordHM服务的application.yml里对应内容spring: redis: host: node1 port: 6379 password: yourpassword timeout: 5000ms lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5这里想提醒一下Redis连接池的max-active不是越大越好。如果HM服务是单实例连接数撑到50个已经很高了但如果后面要扩多个实例连接数会翻倍增长node1的Redis最大连接数配置要提前评估否则会报ERR max number of clients reached。4.4 消息队列与注册中心的接入验证消息队列我用的是RabbitMQ同样在node1上部署。node2需要做的就是在HM配置里把RabbitMQ的连接地址和账号写上然后写一个简单的连通性测试确认能正常发布和消费消息。注册中心这边HM服务需要注册到Nacos。Nacos部署在node1端口为8848。HM的配置里需要指定Nacos的地址和命名空间spring: cloud: nacos: discovery: server-addr: node1:8848 namespace: hm-prod config: server-addr: node1:8848 namespace: hm-prod file-extension: yml接入Nacos之后有个好处后续如果HM服务要滚动升级只要在Nacos里把权重调低流量自然就切走了不用手动改Nginx配置。这一点在项目上线后的运维中非常有用。经验Nacos和RabbitMQ这类中间件配置完之后一定要做连通性测试不要等HM服务启动后才发现连不上。我一般会分别用命令行客户端先测一遍确认能连通再往下走。5. HM服务包部署与上线实操5.1 服务包上传与目录解压HM的服务包是标准的Spring Boot可执行jar包命名类似hm-service-1.0.0.jar。我使用scp从跳板机上传到node2scp hm-service-1.0.0.jar hmnode2:/opt/hm/app/上传之后解压静态资源cd /opt/hm/app jar xf hm-service-1.0.0.jar BOOT-INF/classes/static mv BOOT-INF/classes/static /opt/hm/static chown -R hm:hm /opt/hm/static这里把静态资源单独解压出来的目的是为了配合Nginx直接处理静态请求不用让Spring Boot容器去扛这些IO压力。5.2 配置文件按环境拆分HM的配置目录结构是config/里面放着application.yml和application-prod.yml两个文件。线上环境激活的是prod配置cd /opt/hm/app jar xf hm-service-1.0.0.jar BOOT-INF/classes/application.yml # 把解压出来的配置文件复制到config目录 cp BOOT-INF/classes/application*.yml /opt/hm/config/修改config/application-prod.yml把其中的中间件地址改成实际的node1内网IP数据库账号密码换成生产环境的强密码日志路径指到/opt/hm/logs/。5.3 systemd服务文件编写通过systemd托管HM服务的生命周期可以做到开机自启、异常重启、日志统一收集。服务文件放在/etc/systemd/system/hm.service[Unit] DescriptionHM Service Afternetwork.target Wantsnetwork-online.target [Service] Typesimple Userhm Grouphm WorkingDirectory/opt/hm/app EnvironmentJAVA_HOME/opt/jdk11 EnvironmentSPRING_PROFILES_ACTIVEprod ExecStart/opt/jdk11/bin/java -Xms16g -Xmx16g -XX:MetaspaceSize512m -XX:MaxMetaspaceSize512m -XX:UseG1GC -XX:MaxGCPauseMillis200 -jar /opt/hm/app/hm-service-1.0.0.jar --spring.config.additional-location/opt/hm/config/ ExecStop/bin/kill -s TERM $MAINPID Restartalways RestartSec15 StartLimitIntervalSec0 LimitNOFILE65535 LimitNPROC65535 [Install] WantedBymulti-user.target这段配置里说明几个关键点--spring.config.additional-location是Spring Boot提供的配置覆盖机制可以让外部配置文件优先于jar包内配置生效这样升级服务包时不需要重新打入配置。G1垃圾回收器配合MaxGCPauseMillis200适合HM这种中等堆内存、响应时间敏感的业务。Restartalways配合RestartSec15服务崩溃后15秒自动重启不会因为瞬时故障导致服务长时间不可用。5.4 启动服务与日志验证配置完成后执行守护进程重载并启动服务systemctl daemon-reload systemctl start hm systemctl status hm查看启动日志tail -f /opt/hm/logs/hm-service.log正常情况下日志中会出现Spring Boot的启动横幅然后是各个组件的初始化信息。重点关注最后几行Started HmApplication in xx.xxx seconds这句话一出现就说明服务已经成功启动了。但启动成功不代表配置完成还要做一次业务自检。我习惯先看健康检查接口比如curl http://127.0.0.1:8080/actuator/health返回{status:UP}才代表服务内部的中间件连接全部正常。如果某个组件连接失败健康检查会显示DOWN并标明具体组件。5.5 注册到Nacos后的验证HM服务启动后需要在Nacos控制台上确认服务实例已经注册成功。在浏览器打开http://node1:8848/nacos在服务管理页面搜索hm-service应该能看到node2的IP和端口出现在实例列表中。同时为了确认Nginx转发功能正常我用curl模拟了一次请求curl -I http://node2/hm/api/health期望返回HTTP/1.1 200 OK同时能看到Via或X-Application-Context等头部信息说明请求确实经由Nginx到达了HM服务。6. 常见问题与排查技巧实录6.1 服务启动失败端口被占用HM服务的默认端口是8080启动时提示端口占用。我用lsof -i:8080查了下发现是一个残留的Java进程占着端口。排查命令ss -lntp | grep 8080 lsof -i :8080处理方式# 找到对应PID kill -9 PID # 或者更稳妥的方式使用systemd管理旧服务并停止 systemctl stop 旧服务名踩坑记录如果直接用kill -9杀掉一个systemd托管服务的进程systemd检测到进程异常退出会自动把它拉起来。所以一定要先systemctl stop再检查端口释放。6.2 连接数据库超时HM服务启动时数据库连接池初始化超时报了Communications link failure。排查思路先ping node1验证网络通不通再用mysql客户端命令测一下连接mysql -h node1 -u hmuser -p -P 3306结果发现MySQL账号的host限制是hmuserlocalhost没有允许node2的IP访问。需要在node1的MySQL里执行授权CREATE USER hmusernode2 IDENTIFIED BY password; GRANT ALL PRIVILEGES ON hmdb.* TO hmusernode2; FLUSH PRIVILEGES;这种问题在部署初期出现频率极高建议在部署检查清单里加一条“数据库账号授权host范围”。6.3 日志中频繁出现连接重置服务运行半小时后日志开始刷java.io.IOException: Connection reset by peer。我第一反应是Nginx到后端超时时间太短查配置后发现proxy_read_timeout设置的是60秒而HM有个接口处理耗时超过这个阈值。把Nginx的超时时间调大proxy_read_timeout 120s;同时检查HM服务侧的Tomcat连接配置看是否设置了connection-timeout。另外如果服务间走的是HTTP长连接还需要确保连接池的空闲检测时间比服务端的keepalive时间短否则复用的连接可能已经被服务端关闭导致reset。6.4 Redis连接池满导致请求阻塞上线跑了一天后发现部分接口响应极慢日志里出现RedisCommandTimeoutException。一看监控Redis连接池的active数量长期维持在最大值50。排查后发现是HM服务里有个定时任务一次性从Redis批量读取大量数据且没有及时归还连接。这个问题的修复分两步第一把连接池的max-active临时调到80缓解燃眉之急。 第二定位到具体代码优化数据读取逻辑将大事务拆分成小批量操作避免长时间持有连接。部署层面的教训是连接池参数要根据实际业务峰值来定不能完全照抄配置模板。上线前最好做一次压测观察连接池的使用趋势。6.5 多节点负载不均衡的排查项目里node1和node2各跑了一个HM实例但在运维监控中发现node2的负载总是明显高于node1。排查方式是通过Nacos控制台查看两个节点的权重配置再检查Nginx的负载均衡策略。最终发现是Nacos中node2的实例权重被误设为了5而node1的权重是1。把权重调成一致后就恢复正常了。这里要提个醒如果以后遇到类似情况不要盲目调整Nginx配置先看注册中心里的权重再查各节点的连接数分布。6.6 常见问题速查表问题现象可能原因排查/解决服务启动报端口占用旧进程残留或服务重复启动ss -lntp | grep 8080定位PIDsystemd服务先stop再kill数据库连接失败账号host限制、密码错误、网络不通用mysql客户端在node2本机测试检查授权hostNginx返回502后端服务未启动、SELinux拦截验证后端端口连通性临时关闭SELinux测试请求超时Nginx超时时间过短、后端处理慢调整proxy_read_timeout优化代码逻辑Redis命令超时连接池满或大key阻塞观察连接池指标优化代码批量操作日志出现OutOfMemoryJVM堆内存不足或泄漏检查heap使用曲线必要时dump分析负载不均衡注册中心权重配置不正确在Nacos控制台调整权重7. 上线检查清单与容灾验证7.1 最终检查清单部署完成后我习惯用一份检查清单逐项确认避免有遗漏项导致上线后出事。这里分享给大家做参考主机名和hosts解析是否正常node1和node2能否互相通信防火墙/SELinux状态是否符合预期端口是否放行系统时间是否同步chrony状态是否为正常JDK版本是否正确java -version输出符合要求HM服务配置文件中的中间件地址、账号、密码是否都正确服务是否注册到Nacos实例列表里能看到node2的地址Nginx配置是否正确静态资源访问是否正常systemd服务是否设置开机自启日志路径是否正确日志轮转是否有配置关键接口做一次冒烟测试健康检查返回UP7.2 容灾演练手动杀掉HM服务上线前我习惯做一次容灾测试。具体做法是主动杀掉HM进程验证systemd能否自动拉起。pkill -f hm-service-1.0.0.jar sleep 20 systemctl status hm正常情况下服务会在15秒内被自动重启。这样一个简单的验证能有效避免服务因未知原因挂掉后无人发现的情况。再配合一个日志监控把HM服务的ERROR日志接入告警平台一旦连续出现严重错误就触发告警。这些措施加起来基本能保证node2上的HM服务是可控的。7.3 日志轮转配置日志如果不做轮转很快就会把磁盘写满。我在/etc/logrotate.d/hm中加了如下配置/opt/hm/logs/*.log { daily rotate 30 compress delaycompress missingok notifempty copytruncate }copytruncate这个参数比较关键它先复制日志文件再清空原文件适用于那些不直接支持SIGUSR1日志重开的进程。Spring Boot应用如果用Logback也可以用logback.xml里的SizeAndTimeBasedRollingPolicy来做滚动效果更好。两种方式选一种就行不要叠加。8. 部署过程中的几个体会最后分享几点实际操作中的体会。第一多节点部署项目最容易出事的不是技术难点而是初始配置的遗漏。主机名没改好、hosts没写全、防火墙没放行端口这些小事积累起来会让排查过程变得非常痛苦。所以部署前花半小时列一份配置清单绝对值得。第二systemd的selinux上下文和配置路径需要特别留意。有些团队会自己写服务脚本但放在/usr/local/bin下可能因为权限问题无法启动。统一用/usr/local/bin或/opt下的目录配合systemd的ExecStart路径能减少很多不必要的麻烦。第三node2这类应用节点调优的重点不在于某一个组件的参数调到最优而是要让组件之间的配合节奏一致。比如Nginx的超时配置要和后端接口的响应时间匹配Redis连接池的max-active要和并发量匹配JVM的堆大小要和节点的物理内存匹配。任何一环脱节整个链路都会出问题。第四生产环境的变更一定要留痕。我在部署过程中把每一步操作都记录到了运维文档里包括执行过的命令、修改过的文件、验证过的结果。这样万一出现问题可以快速回溯是哪个环节出的错。这个习惯在几次紧急故障排查中救过大忙。node2的配置到这里就全部完成了。这套流程跑通之后后面如果再新增node3、node4节点只要照着这个思路走一遍把ip和主机名替换掉基本半小时内就能完成一个应用节点的配置。希望这篇文章能帮正在做类似项目的朋友少踩几个坑。