首页 / 资讯中心 / 文章详情

CentOS 9 单机 Web 栈部署与端口排错指南

CentOS 9 单机 Web 栈部署与端口排错指南 ★ FEATURED ARTICLE
1. 先把目标定清楚CentOS 9 这套栈的整体设计与选型CentOS 9 安装配置 MySQL、Redis、Node.js、pm2、Nginx 加防火墙这套组合我前后搭过几十次它基本是一台“单机跑 Web 服务”的最小可用栈Nginx 在最前面接流量、做 SSL 和静态资源Node.js 跑业务逻辑并由 pm2 守着进程MySQL 存关系数据Redis 扛会话和热数据firewalld 管对外暴露的那几个口子。听起来五件事实际上一台两核四G的小机器上装完配完往往要耗掉一整个下午而且九成的时间不是花在“装”上而是花在“装完之后跑不起来”——端口不通、权限不够、开机不自启、日志把磁盘写满。这篇文章写给两类人一是第一次在 CentOS 9 上从零搭这套环境希望少走弯路的二是已经装过几轮但总觉得配置是东拼西凑抄来的、出了问题不知道该从哪一层下手的。我会把每个组件为什么这么配、参数怎么算出来的、SELinux 和 firewalld 到底在哪一步会拦你都拆开讲明白。先说一个容易被忽略的前提CentOS 9 严格讲叫CentOS Stream 9它默认开着 firewalld、默认 SELinux 是 enforcing、默认用 dnf 和 cgroup v2。这三件事决定了后面的所有操作。很多人从别的发行版搬命令过来第一步就卡在“我明明启动了服务浏览器就是打不开”其实服务活得好好的是防火墙没放行或者 SELinux 不让 Nginx 连本地端口。1.1 为什么是这五件套而不是别的组合选型这件事没有绝对对错只有匹配不匹配。这套组合的核心优势在于组件之间耦合松、单机可承载、故障域清晰。Nginx 做统一入口它负责 TLS 卸载、gzip、静态文件、反代和限流。业务服务只监听 127.0.0.1 的高位端口外网永远只看到 80/443 两个口安全面收得很窄。Node.js pm2Node 适合 I/O 密集的接口服务pm2 解决“进程崩了谁拉起来”“机器重启谁先启动”这两个最实际的问题。我不建议用 systemd 直接托管 node 脚本除非项目非常单一多实例、多环境时 pm2 的 ecosystem 文件更好管。MySQL 8 存主数据8.0 之后默认 utf8mb4、默认 caching_sha2_password性能和信息安全都比 5.7 好但这两点也恰好是迁移时最容易翻车的地方后面细讲。Redis 做缓存和队列会话、验证码、排行榜、简单队列基本都靠它。但它内存有限绝不能当数据库用。firewalld SELinux这不是“可选加固”而是 CentOS 9 的默认状态。想关掉它们当然可以但我的建议是留着学会怎么放行因为你永远不知道这台机器明天会不会被要求做等保整改。有一点要先摆明这套栈适合日活几千到几万的单体应用。再往上Nginx 要做集群、MySQL 要做主从、Redis 要做哨兵单机这套配置只能当参考骨架不能当终态。1.2 装机当天就必须拍板的四个开关我踩过最深的坑是装到一半才发现某个前置决策错了只能推倒重来。所以这四件事建议在敲第一条命令前就想清楚决策项我的默认选择什么情况下改SELinux保持 enforcing按需放行项目有强制要求且明确接受风险时才考虑 permissivefirewalld保持开启只放 80/443 和运维端口前面有硬件防火墙且策略已覆盖可只留必要端口Node 安装方式NodeSource 官方仓库装 LTS一台机器跑多个 Node 版本时改用 nvmMySQL 来源系统 AppStream 自带 8.0需要特定小版本或官方补丁时用 MySQL 官方仓库这里解释一下为什么 SELinux 我坚持留着。很多教程第一句就是setenforce 0图省事。问题是这台机器一旦交给别人运维、或者过几个月你自己都忘了改过什么SELinux 的告警就被永久性地关掉了等于少了一层保险。而实际需要放行的场景就那么几个Nginx 反代本地端口httpd_can_network_connect、非标准端口semanage port、自定义站点目录semanage fcontext。学会这三招比关掉它划算得多。Node 的安装方式也一样。NodeSource 装出来的是系统级、路径固定、开机就在的 Node适合“一台机器一个应用”的常态nvm 装在用户目录下切换版本方便但pm2 的开机自启会变得麻烦因为 systemd 拉起进程时的 PATH 和你的登录 shell 不一样nvm 装的 node 常常找不到。这一点在第 3 节会展开。2. 系统初始化先把地基打平刚装完的 CentOS Stream 9 其实挺干净但离“能跑业务”还差几步。我习惯在装任何组件之前先做一轮初始化顺序是更新系统 → 校对时间 → 装常用工具 → 调内核参数。顺序不能反尤其是时间同步必须放在 MySQL 和 Redis 之前。2.1 换源、时间同步与常用工具首先更新一遍系统并装基础包dnf update -y dnf install -y epel-release dnf groupinstall -y Development Tools dnf install -y vim wget curl git unzip lsof net-tools sysstat chrony tarDevelopment Tools这个包组看着多余其实后面全用得上Redis 如果走编译安装需要编译链Node 的一些原生模块数据库驱动、图像处理库也需要 gcc、g、make 和 python3。现在装好省得以后npm install报node-gyp错误再回头补。时间同步是重中之重。MySQL 的主从复制、Redis 的过期键、JWT 令牌校验、日志排查全都依赖时间一致。CentOS Stream 9 自带 chrony确认一下systemctl enable --now chronyd chronyc sources -v chronyc tracking看到Leap status : Normal和System time偏移在毫秒级就算正常。如果机器在云上一般云厂商有内网 NTP 地址填到/etc/chrony.conf里比用公网源稳定得多。我见过一次线上事故两台机器时间差了四分钟用户登录后拿到的 token 一落地就提示“已过期”查了整整一下午才发现是 NTP 没同步。时区也要一起定timedatectl set-timezone Asia/Shanghai timedatectl status时区不定MySQL 默认时间戳和你日志里看到的差八小时排查问题时能把人逼疯。后面 MySQL 配置里我还会再设一次default-time-zone两边对齐才保险。主机名顺手改掉不要留着localhost.localdomain后面 Nginx 和证书标识、日志标识都会用到hostnamectl set-hostname app-node-012.2 文件句柄、内核参数与 swap这一块是“不调也能跑一压就出事”的典型。默认的ulimit -n通常是 1024Nginx 和 Node 一上并发就报too many open files。我一般直接写到/etc/security/limits.d/99-app.confcat /etc/security/limits.d/99-app.conf EOF * soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535 EOF注意*通配对 root 在某些版本上不生效稳妥做法是再单独写一行root soft nofile 65535。改完必须重新登录才生效ulimit -n只对当前会话有效别用它验证配置。内核参数写在/etc/sysctl.d/99-app.confcat /etc/sysctl.d/99-app.conf EOF net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65000 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 15 fs.file-max 1000000 vm.overcommit_memory 1 vm.swappiness 10 EOF sysctl --system逐个说下理由somaxconn是 listen 队列长度Nginx 的backlog和 Node 的默认 listen backlog 都会受它限制默认 4096 在高并发瞬间会丢连接ip_local_port_range扩大本地端口范围避免反代建立大量短连接时端口耗尽vm.overcommit_memory1是 Redis 官方明确要求的0 的时候 fork 做持久化可能因为内存预估失败而被拒绝swappiness10让系统尽量少用 swap数据库类应用对换页延迟极其敏感。关于 swap 我的态度是保留但别依赖。一台 2G 内存的机器不配 swapNode 编译时直接被 OOM killer 杀掉配了 swap 又容易让 MySQL 出现“看起来没卡其实在换页”的假象。折中方案是给 1~2G swap同时把vm.swappiness压低到 10让系统只在真没内存时才用。3. Node.js 与 pm2进程怎么才能不死这一节是整套栈里最容易出“玄学问题”的地方——明明node app.js手动跑得好好的一挂到 pm2 就报模块找不到明明pm2 start成功了重启机器进程就没了。根因基本都在环境变量和权限上下文上。3.1 Node 装哪个版本走哪条路CentOS Stream 9 的 AppStream 里也有 nodejs但版本通常偏旧我不建议用。生产上我固定走 NodeSource 官方仓库装 LTScurl -fsSL https://rpm.nodesource.com/setup_20.x | bash - dnf install -y nodejs node -v npm -v装完顺手确认一下which node正常应该是/usr/bin/node。这个路径很重要因为后面 pm2 生成 systemd 服务时会把这个路径写进去系统级安装的好处就在这里体现。如果你确实需要多版本共存用 nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20 nvm alias default 20但请记住nvm 装在哪个用户下pm2 就只能在哪个用户下用。如果以后要用 systemd 托管 pm2unit 文件里必须显式写EnvironmentPATH/home/用户名/.nvm/versions/node/v20.x/bin:/usr/bin否则 systemd 找不到 node服务状态会显示status127。这是新手最常见的死结之一。还有一个创建账户的细节生产环境我不建议用 root 跑 Node。新建一个普通用户专门跑应用useradd -m -s /bin/bash deploy passwd deploy后面 pm2 的所有操作都su - deploy后再执行日志、代码、进程都归这个用户所有权限边界清晰。用 root 跑 node 的问题不只是安全还有pm2 restart之后文件属主变了、Nginx 读不到静态文件这类连锁反应。3.2 pm2 的 ecosystem 文件与开机自启先全局装npm install -g pm2 pm2 -v全局装完在/usr/lib/node_modules/pm2命令软链到/usr/bin/pm2。如果装的时候用sudo记得检查pm2命令属于哪个用户混用会导致“root 启动的进程 deploy 用户看不到”这种诡异现象。真正的关键是ecosystem.config.js。手敲pm2 start app.js只适合临时调试生产一律用配置文件module.exports { apps: [{ name: web-api, script: ./server.js, cwd: /home/deploy/apps/web-api, instances: 2, exec_mode: cluster, max_memory_restart: 512M, autorestart: true, max_restarts: 10, min_uptime: 10s, restart_delay: 3000, env: { NODE_ENV: production, PORT: 3000 }, error_file: /home/deploy/logs/web-api-error.log, out_file: /home/deploy/logs/web-api-out.log, merge_logs: true, time: true }] };几个参数值得单独解释instances和exec_mode: cluster是配套的cluster 模式下 pm2 用 Node 的 cluster 模块起多进程共享同一个端口。核数建议是CPU 核数或核数 - 12 核机器起 2 个进程是合理的起 8 个只会互相抢 CPU。max_memory_restart: 512M是防内存泄漏的兜底。Node 的内存泄漏很难一次性根治与其等进程把机器撑爆不如让它到阈值自动重启。阈值怎么定看你的应用稳态内存取 1.5 到 2 倍。稳态 300M 就设 512M设太小会频繁重启反而更糟。min_uptime: 10s配合max_restarts是防止“启动即崩、崩了再启”的死循环把 CPU 占满。10 秒内挂掉的算启动失败连续 10 次就进入 errored 状态不再重试把问题暴露出来。time: true给日志加时间戳。别小看这一行日志没时间戳出事的时候你根本对不上哪条日志对应哪个请求。然后启动并保存pm2 start ecosystem.config.js pm2 save pm2 startup systemd -u deploy --hp /home/deploypm2 save把当前进程列表写入~/.pm2/dump.pm2pm2 startup生成开机自启的 systemd unit。这两条的顺序不能反而且必须在同一个用户下执行。执行pm2 startup后它会打印一条需要 root 执行的命令照着复制执行一遍才真正生效。很多人只跑了第一条重启后进程全没了就是漏了这步。3.3 日志切割与内存守护pm2 的日志默认无限增长一个中等流量的接口服务一周写满 10G 磁盘不稀奇。装个官方模块自动切割pm2 install pm2-logrotate pm2 set pm2-logrotate:max_size 100M pm2 set pm2-logrotate:retain 10 pm2 set pm2-logrotate:compress true pm2 set pm2-logrotate:rotateInterval 0 0 * * *这里我遇到过一个小坑pm2-logrotate是装在 pm2 内部的模块用 npm 装无效必须用pm2 install。装完它自己会以守护进程形式存在pm2 list里能看到pm2-logrotate这一项。另外这套配置在 cgroup v2 下有个已知的兼容性问题pm2 的 systemd 集成pm2 install pm2-systemd在老版本上不支持 cgroup v2装完重启后进程状态异常。CentOS Stream 9 默认就是 cgroup v2所以我的做法是只用pm2 startup不装pm2-systemd开机自启交给 systemd unit 就够了不需要额外的集成模块。最后做一次完整验证这一步别省pm2 list pm2 describe web-api reboot # 重启后重新登录 pm2 list curl -s http://127.0.0.1:3000/health重启后能看到进程、能打通健康检查才算这套进程管理真正落地。只看pm2 list显示 online 是不够的那可能只是你重启前看到的缓存。4. MySQL 8装得快调得好才叫本事MySQL 我一般直接用系统仓库的版本除非项目指定小版本号dnf install -y mysql-server systemctl enable --now mysqld systemctl status mysqld首次启动会做初始化临时密码在日志里grep temporary password /var/log/mysqld.log拿到后立刻跑安全脚本mysql_secure_installation这个脚本会依次问你要不要装密码强度插件、要不要改 root 密码、要不要删匿名用户、要不要禁止 root 远程登录、要不要删 test 库、要不要重载权限。我的回答一律是装强度插件y、改密码、删匿名y、禁止 root 远程y、删 test 库y、重载y。注意MySQL 8 默认密码策略是MEDIUM要求至少 8 位、含大小写字母、数字和特殊字符。如果你的密码一直提示不满足策略不是命令错了是策略拦的。生产环境别关这个策略改成LOW只是把风险往后推。4.1 我常用的一份配置参数配置文件放/etc/my.cnf.d/mysql-server.cnfCentOS 系的惯例是主配置my.cnf只放 include实际参数分文件写升级时不容易被覆盖[mysqld] bind-address 127.0.0.1 port 3306 character-set-server utf8mb4 collation-server utf8mb4_0900_ai_ci default-time-zone 08:00 max_connections 500 max_connect_errors 1000 wait_timeout 600 interactive_timeout 600 innodb_buffer_pool_size 1G innodb_log_file_size 256M innodb_flush_log_at_trx_commit 1 innodb_flush_method O_DIRECT slow_query_log 1 slow_query_log_file /var/log/mysql-slow.log long_query_time 1 log_queries_not_using_indexes 0 sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTIONinnodb_buffer_pool_size是最该算的一个参数不是拍脑袋写的。经验公式是物理内存的 50%~70%前提是这台机器只跑 MySQL。如果像我这种单机五件套MySQL 和 Node、Redis 挤在一起就要往下压4G 内存的机器MySQL 给 1GRedis 给 512M剩下留给系统和 Node。innodb_log_file_size默认 48M 偏小写密集场景会频繁 checkpoint。256M 是个性价比不错的档位改完必须重启才生效且旧日志文件要清理干净否则启动会报错。innodb_flush_log_at_trx_commit 1是最安全的设置每次事务提交都刷盘掉电最多丢一个事务。改成 2 能提升写入性能但掉电可能丢一秒数据。这个取舍必须让业务方知情不能运维自己决定。bind-address 127.0.0.1很关键。单机部署时 MySQL 根本不需要对公网开放绑本地既减少攻击面也省掉防火墙放行 3306 的必要。如果确实要远程连走 SSH 隧道或者内网专线别直接开放端口。慢查询日志一定要开。long_query_time 1意味着超过 1 秒的查询被记录这个阈值对小项目合适大表多的项目可以放到 2 秒否则日志刷得太快。4.2 账号授权与字符集的两个坑建库建号有一套固定动作CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; CREATE USER app_user127.0.0.1 IDENTIFIED BY 强密码放这里; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO app_user127.0.0.1; FLUSH PRIVILEGES;注意主机位写127.0.0.1而不是%。%意味着任意来源单机部署完全没必要。另外权限只给 DML不给DROP、ALTER建表改表用另一个迁移账号做。这个习惯在出过一次“误删表”的事故后我就再没改过。坑一caching_sha2_password 认证失败。MySQL 8 默认认证插件是caching_sha2_password一些老的客户端库、老版本的 Navicat、部分语言的旧驱动连不上报Authentication plugin caching_sha2_password cannot be loaded。解决办法不是把全局默认改回mysql_native_password那等于降级安全而是只给特定账号改ALTER USER legacy_user% IDENTIFIED WITH mysql_native_password BY 密码;坑二字符集不统一导致的乱码和索引失效。建库用 utf8mb4连接层也要设。JDBC 要加characterEncodingutf8useUnicodetrueNode 的 mysql2 驱动默认就是 utf8mb4但连接池配置里显式写一遍更保险。还有排序规则utf8mb4_0900_ai_ci和utf8mb4_general_ci混用会导致 JOIN 时索引失效建库时就定死一种全项目统一。5. Redis别把缓存当数据库用Redis 的安装有两种路子。系统仓库版本稳定、开箱即用dnf install -y redis systemctl enable --now redis redis-cli ping返回PONG就通了。如果你需要新版本特性比如某些 Stream 命令、新的数据结构可以加 remi 源或者下载源码编译。编译安装的好处是版本可控坏处是升级麻烦、systemd unit 要自己写。我的建议是没有明确的新版本需求就用系统仓库的版本稳定压倒一切。5.1 一份生产可用的 redis.conf 关键项配置文件在/etc/redis/redis.conf需要改的其实就那么几行bind 127.0.0.1 protected-mode yes port 6379 requirepass 这里放一个足够长的随机串 timeout 300 tcp-keepalive 300 maxmemory 512mb maxmemory-policy allkeys-lru appendonly yes appendfsync everysec save 900 1 save 300 10逐条解释bind 127.0.0.1protected-mode yes是双保险。只绑本地外面的机器根本连不上。很多人图方便绑0.0.0.0又不设密码等于把数据库放在公网上裸奔。requirepass必须有。哪怕只绑本地也要设因为同机的其他进程、被入侵的低权限账户都可能连上来。密码用openssl rand -base64 32生成别自己编。maxmemory和maxmemory-policy是搭配的。不设maxmemoryRedis 会一直吃内存直到系统 OOM设了allkeys-lru内存满了就淘汰最久未用的键。注意这是删除数据如果你的键不能丢就不能用这个策略得用noeviction并做好容量监控。appendonly yes开 AOF 持久化appendfsync everysec是性能和安全折中最多丢一秒数据。RDB 的save规则也保留着两种持久化并存是最稳的。改完重启并验证systemctl restart redis redis-cli -a 你的密码 ping redis-cli -a 你的密码 config get maxmemoryconfig get能读到正确值说明配置真的生效了而不是改了个没被引用的文件。5.2 系统层面的两个隐藏开关Redis 有两个必须调的系统参数不调会在日志里一直报警告第一个是vm.overcommit_memory 1。Redis 做 RDB 持久化时要 fork 一个子进程Linux 的 overcommit 策略设成 0 时内核会保守估计内存可能导致 fork 失败日志里出现Background saving error。这个参数我们在第 2 节已经写进 sysctl 了这里确认一下sysctl vm.overcommit_memory输出是 1。第二个是关闭透明大页THP。Redis 官方文档明确说 THP 会让 fork 时的内存复制延迟飙升。临时关闭echo never /sys/kernel/mm/transparent_hugepage/enabled永久生效要写进 systemd 服务或者加个开机脚本。CentOS 上用 systemd 的覆盖文件比较规范mkdir -p /etc/systemd/system/redis.service.d cat /etc/systemd/system/redis.service.d/override.conf EOF [Service] OOMScoreAdjust-900 EOF systemctl daemon-reloadOOMScoreAdjust-900让系统在内存紧张时优先杀别的进程而不是 Redis。这个值不能设成 -1000那会完全禁用 OOM killer反而危险。6. Nginx反向代理才是主战场dnf install -y nginx systemctl enable --now nginx nginx -v系统仓库的 Nginx 版本通常落后一两个大版本。如果需要 HTTP/3 或者新版特性加官方仓库cat /etc/yum.repos.d/nginx.repo EOF [nginx-stable] namenginx stable repo baseurlhttp://nginx.org/packages/centos/9/$basearch/ gpgcheck1 enabled1 gpgkeyhttps://nginx.org/keys/nginx_signing.key module_hotfixestrue EOF dnf install -y nginxmodule_hotfixestrue这行不加可能和 AppStream 里的模块冲突导致装不上这是 CentOS 8 之后的老问题。目录约定我习惯这样站点配置全部放/etc/nginx/conf.d/*.conf不要动nginx.conf主文件静态资源放/var/www/站点名/日志默认在/var/log/nginx/。这样以后换机器把conf.d和/var/www打包带走就行。6.1 一份能直接用的反代配置upstream node_backend { server 127.0.0.1:3000; server 127.0.0.1:3001; keepalive 32; } server { listen 80; server_name example.internal; client_max_body_size 50m; access_log /var/log/nginx/app-access.log main; error_log /var/log/nginx/app-error.log warn; location / { proxy_pass http://node_backend; proxy_http_version 1.1; 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_set_header Connection ; proxy_connect_timeout 5s; proxy_send_timeout 60s; proxy_read_timeout 60s; } location /ws/ { proxy_pass http://node_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { root /var/www/app-static; expires 30d; add_header Cache-Control public, immutable; access_log off; } }几个点值得展开proxy_set_header Connection 配合keepalive 32是开启长连接的关键。不写这行Nginx 到后端的连接每个请求都要重新握手QPS 高的时候 TIME_WAIT 能把端口耗尽。X-Forwarded-For和X-Real-IP必须传。否则 Node 里req.ip永远拿到的是 127.0.0.1做限流、审计、风控全都失效。WebSocket 那个 location 的proxy_read_timeout设到 3600s是因为默认 60 秒会把长连接掐断前端表现为“每隔一分钟重连一次”。这个问题我排查过两次每次都是忘了配这一行。静态资源单独 locationexpires 30d加immutable让浏览器强缓存。注意这些文件如果带 hash 文件名才敢这么设不带 hash 的话更新后用户看到的是旧版本。6.2 HTTPS、上传大小与超时证书这块如果是对外服务直接用 Lets Encrypt 最省事dnf install -y certbot python3-certbot-nginx certbot --nginx -d your.domain它会自动改配置、加 443 监听、配好跳转还会装一个续期定时器。验证一下systemctl list-timers | grep certbot certbot renew --dry-run--dry-run一定要跑不然 90 天后证书过期凌晨三点被告警叫醒的滋味不好受。内网服务用自签证书的话重点是给客户端分发 CA而不是在 Nginx 上关掉校验。另外建议加上这几个安全头add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; add_header Referrer-Policy strict-origin-when-cross-origin;client_max_body_size默认是 1m用户传个头像就 413。这个值要按业务定传文档的给 50m纯接口的给 5m 就够。设太大也有风险大文件上传会占用 worker 连接和磁盘临时空间。配套还要调client_body_timeout默认 60 秒大文件在慢网络下传不完会被中断。7. 防火墙与 SELinux九成“端口不通”发生在这里这两块是 CentOS 9 上最容易被误伤的部分。奇怪的是大部分人排查“网页打不开”时会先去翻 Nginx 日志其实日志里干干净净因为请求根本没到 Nginx。7.1 firewalld 的日常动作先看当前状态别急着加规则firewall-cmd --state firewall-cmd --get-default-zone firewall-cmd --list-all--list-all会显示当前 zone 的 services、ports、sources。默认 zone 通常是public默认放行的服务只有 ssh、dhcpv6-client 这些。放行 Web 服务firewall-cmd --permanent --add-servicehttp firewall-cmd --permanent --add-servicehttps firewall-cmd --reload firewall-cmd --list-services放行自定义端口firewall-cmd --permanent --add-port8080/tcp firewall-cmd --reload三个必须记住的点第一--permanent和--reload是一对。不加--permanent规则重启就没了加了不--reload当前运行时没生效。很多人加了规则测不通就是漏了 reload。第二不要用systemctl stop firewalld来“解决”问题。停掉之后systemctl disable一开这台机器就永久失去网络层防护而且下一个人接手时根本不知道。第三如果这台机器前面有云厂商的安全组安全组和 firewalld 是两道独立的门两道都要开。我见过在 firewalld 里开了 443安全组没开对着服务器查了两小时。7.2 SELinux 对 Nginx 的两个强制限制SELinux 默认策略下Nginx 相关的限制主要有两条几乎每个搭反代的人都会撞上限制一Nginx 不允许主动发起网络连接。表现是 502 Bad Gateway/var/log/nginx/error.log里写着Permission denied while connecting to upstream。注意这不是文件权限问题是 SELinux 的布尔值没开setsebool -P httpd_can_network_connect 1-P是持久化重启后仍然生效。不加-P只是临时改重开机就还原。这一条是反代场景的必做项没有例外。限制二Nginx 不能监听非标准端口。比如你想让 Nginx 监听 8080直接改配置会启动失败journalctl -u nginx里报bind() to 0.0.0.0:8080 failed (13: Permission denied)。解决方法是把这个端口注册给 http_port_tsemanage port -a -t http_port_t -p tcp 8080 semanage port -l | grep http_port_t如果提示端口已存在用-m修改而不是-a添加。还有一个容易忽略的是网站目录。如果静态资源没放/var/www而是放在/home/deploy/apps/web/distNginx 会 403。因为 SELinux 给文件的标签不对semanage fcontext -a -t httpd_sys_content_t /home/deploy/apps/web/dist(/.*)? restorecon -Rv /home/deploy/apps/web/distsemanage fcontext只是登记规则restorecon才真正打标签。两条一起敲才算完。排查 SELinux 拒绝的最快方式是看审计日志ausearch -m AVC -ts recent或者临时开 permissive 模式验证一下确认是 SELinux 的问题再回去改策略不要长期停留在 permissive。这个排查手法我用了很多次比一个个猜布尔值快得多。7.3 一份端口规划表单机多服务最容易乱的就是端口。我习惯在部署前先画一张表写进文档服务监听地址端口是否对外说明Nginx0.0.0.080/443是唯一对外入口Node API127.0.0.13000/3001否pm2 cluster 双实例MySQL127.0.0.13306否只允许本机Redis127.0.0.16379否有密码 绑本地SSH0.0.0.022是建议改端口 密钥登录这张表的价值在交接和排查。出了访问问题先看表这个端口本来就不该对外那外部连不上是正常的问题在 Nginx 配置这个端口需要对外那就去查 firewalld 和安全组。8. 上线后最容易撞上的问题与排查顺序装完到跑通之间一定会有几轮反复。我把自己遇到过、也帮别人处理过的高频问题整理成表按现象查原因比一上来就翻日志快。8.1 十类高频问题对照表现象最可能的原因快速验证处理502 Bad GatewaySELinux 禁止 Nginx 连本地端口ausearch -m AVC -ts recentsetsebool -P httpd_can_network_connect 1502 但日志无异常后端进程没起或端口不符ss -lntp | grep 3000检查 pm2 状态与 PORT 环境变量浏览器超时打不开firewalld 未放行firewall-cmd --list-all加 service/port 后 reload重启后进程消失没执行pm2 savels ~/.pm2/dump.pm2pm2 save后重跑 startupnpm 装原生模块报错缺编译链gcc -v装 Development ToolsMySQL 连不上本机密码策略/认证插件mysql -u user -p -h 127.0.0.1查错误码 1045/2059Redis 报 NOAUTH客户端没传密码redis-cli ping客户端配置加 password上传文件 413client_max_body_size太小响应头状态码调整后nginx -t并 reload内存持续上涨最后 OOMNode 泄漏无限制pm2 monit设max_memory_restart时间对不上导致登录异常chrony 未同步chronyc tracking修 NTP 源并重启 chronyd表格里“502 但日志无异常”这一条特别值得说。这种情况九成是后端真的没起来而不是 Nginx 的问题。验证顺序应该是ss -lntp看端口有没有在监听 →pm2 list看进程状态 →curl 127.0.0.1:3000直接打后端。三步下来基本能定位。8.2 我的排查顺序从外到内五层遇到“服务访问不了”我固定按这五层往下走不跳步第一层DNS 和网络。ping 域名看解析对不对curl -v http://IP看能不能连上。如果 IP 能通域名不通问题在 DNS不在服务器。第二层防火墙。firewall-cmd --list-all看规则再看云厂商安全组。这两处是外网流量的必经之路。第三层Nginx。nginx -t检查配置语法systemctl status nginx看进程tail -f /var/log/nginx/error.log看实时错误。注意nginx -t通过不代表配置生效改完必须systemctl reload nginx。第四层后端服务。ss -lntp确认端口在听、curl 127.0.0.1:端口确认本地能通、pm2 logs看应用日志。本地通但通过 Nginx 不通问题就在反代配置或者 SELinux。第五层应用本身。前面四层都正常那就是代码或数据库的问题。看应用日志、看慢查询、看 Redis 连接数。这个顺序的价值在于它保证你不会在错误的地方浪费时间。我见过太多人一上来就翻应用日志翻了半小时发现请求压根没到应用——因为被防火墙挡住了。几个额外的实操心得。nginx -T会把所有 include 的配置合并打印出来排查“我改的这个文件到底有没有被加载”特别好用。pm2 logs --lines 200能一次看两百行比tail方便。journalctl -u 服务名 --since 10 min ago是 systemd 服务的万能日志入口MySQL、Redis、Nginx 都适用。还有一个经常被忽略的小技巧改完任何配置先nginx -t或mysqld --validate-config验证再 reload不要直接 restart。reload 是平滑的restart 会断连接。而且语法错误的情况下 reload 会失败并保留旧进程restart 会直接起不来线上当场挂掉。这个习惯救过我至少两次。我个人在实际操作中的体会是这套环境真正的难点从来不在“装”而在“装完之后每一层都要对得上”。Nginx 监听 80、firewalld 放行 80、安全组放行 80、SELinux 允许 Nginx 连 3000、Node 真的在 3000 上听着、MySQL 密码能过认证、Redis 绑本地且有密码——这七个条件缺一个页面就是打不开。所以我现在的做法是装完一个组件就立刻做一次端到端验证而不是六个全装完再一起测。一步一步稳比一口气装完再回头查要快得多。
阅读完成 · 觉得有帮助?
咨询建站