最近处理了一台服务器上的端口占用问题。机器上卸载了某国产数据库的安全版 V4.5.10,卸载程序和界面都提示完成,结果重启后发现 6666 端口依然被监听,进程名叫 db_ha。说白了就是卸载工具没把高可用组件的 agent 进程一起带走。这个问题在实施现场很典型,尤其是带 HA 组件的数据库产品,卸载不干净比安装失败更让人头疼。因为端口被占用,后续要部署新实例、或者让其他服务使用这个端口,都会卡在这一步,而且来源还不容易一下子看出来。
这篇内容主要讲清楚三件事:怎么从端口反查残留进程,为什么卸载之后 db_ha 还在跑,以及如何把进程、自启动项、目录文件一次清干净。如果你是做数据库运维、实施交付、或者日常维护服务器的,这篇可以直接当成一套排查模板用。即使你手上跑的并不是这款产品,只要思路不变,同样能迁移到其他中间件、其他带独立 agent 的服务上。文章里面有实际命令、有操作顺序、也有我踩过坑以后的复盘,照着做基本能解决九成以上的“卸载残留导致端口占用”问题。
1. 先描述现场:卸载后留下了什么痕迹
1.1 最直观的异常:端口还在监听
数据库卸载完成后,第一件应该做的事就是检查端口是否释放。在这台服务器上,执行ss -lntp | grep 6666的时候,输出显示有什么进程在0.0.0.0:6666上监听,PID 是一个固定的数字,进程名就是db_ha。这台机器主要用途是跑数据库实例,6666 是 HA 组件用于节点状态同步的专用端口,正常情况下卸载后这个端口应该变成空闲状态。
出现这种情况,说明内核里还驻留有该组件的活动进程,并且正在等待新的连接或定期发送心跳包。除了端口被占用,这可能还会带来一些更隐蔽的副作用:比如该进程持续占用少量 CPU,网络连接显示为 ESTABLISHED 状态或者 LISTEN 状态长期不消退,甚至在进程的日志文件里还在不断写入报错信息。不过这些变化都不如端口占用来得直接,所以最先被发现的通常都是“新装的软件启动时报端口被占用”,顺藤摸瓜才找到这个残留进程。
1.2 周边痕迹:自启动项、安装目录、日志文件
继续排查会发现,不止端口一个问题。/etc/systemd/system/下面还有一个以 db_ha 命名的 service 文件,/opt/某数据库/ha目录完整存在,日志目录/var/log/某数据库/ha里还在产生新的日志。再用rpm -qa | grep 某数据库查询,已经查不到对应包名了,说明软件主程序确实被卸载掉,但辅助进程和配置文件并没有被清理干净。
这种残留的危害主要体现在几个地方。如果后续要重新安装同版本或升级到新版本,安装程序检测端口占用时直接报错,整个部署流程被卡在中间。如果服务器上还有其他高可用组件用了同一个端口,还会出现互相抢端口、监听失败的情况。残留的 service 文件更麻烦,它会让系统在每次重启时自动把 agent 拉起来,这就是为什么机器一重启端口又出现的直接原因。所以处理这类问题,绝对不能只杀进程就算完事,必须把自启动项和文件系统里的痕迹一起清理掉。
2. 排查过程:从端口反查到根因
2.1 第一步永远是从端口反查进程
处理端口占用,我习惯的第一步不是去翻日志,而是直接用命令看谁在监听端口。三个命令各有适用场景:netstat -lnpt是老牌工具,绝大多数系统都带;ss -lntp是性能更好的替代品,在连接数很多的服务器上优势明显;lsof -i:6666最直观,但需要额外安装 lsof 工具。实际排查时,我一般直接输入:
ss -lntp | grep 6666输出里会包含协议、监听地址、端口和进程 PID。注意-t表示只看 TCP,-l表示 listening 状态,-n表示不做 DNS 反解,-p必须配合 root 权限才能看到进程名,否则只会显示一个 PID,缺少这个字段后面就很难定位。如果服务器上没有 ss,用netstat -lnpt效果一样。这里我推荐优先用 ss,因为它在网络连接数量多的时候明显更快,不会出现长时间卡住的情况。
2.2 确认进程身份和所属安装包
知道 PID 后,要先确认这个进程到底是什么,从哪里拉起来的。执行下面几条命令:
ps -fp 1234 readlink /proc/1234/exe cat /proc/1234/cmdline | tr '\0' ' '第一眼看到/opt/某数据库/ha/bin/db_ha,就能判断是数据库的高可用组件还在运行。/proc/PID/exe符号链接会指向可执行文件的真实路径,这一步非常关键。如果二进制文件已经被删除,这条路径末尾可能会显示(deleted),但进程依然在内存里运行,这种情况更需要强制终止,因为普通停止信号可能已经无法让程序完整退出。/proc/PID/cmdline里的参数可以看到启动时带了哪些配置,比如指定了端口、指定了配置文件路径,这些信息对顺藤摸瓜清理残余文件很有帮助。
2.3 找出它的父进程与守护关系
单看 PID 不够,还要弄清楚是谁启动了它。执行ps -o ppid= -p 1234可以拿到父进程 PID,再用pstree -p 1234看整棵进程树。如果父进程是 systemd,说明存在对应的 service 文件,要去 systemd 里处理;如果父进程是一个监控脚本,那就得找到脚本来源,比如/etc/rc.local或某个计划任务,把脚本本身也停掉,否则前面刚杀掉的进程很快又会被拉起。这一步直接决定了后面清理自启动项的方向。
我见过不少人卡在“杀了又活、活了又杀”的循环里,其实就是没查父进程。比如这次处理的机器上,db_ha 的父进程就是 systemd,而 systemd 之所以会拉起它,就是因为/etc/systemd/system/db_ha.service还躺在那里。如果不删掉这个 service 文件,就算你杀了进程一百遍,重启后它还是会原地复活。
2.4 检查卸载后的包管理记录
还可以用系统包管理器确认卸载状态,执行:
rpm -qa | grep -i 某数据库 dpkg -l | grep -i 某数据库在这台机器上,主程序包已经查询不到,但 db_ha 的进程还在,这说明卸载时主程序被移除,而 HA 组件的卸载逻辑没有执行完整。还有一种更隐蔽的情况:安装方式不是标准包安装,而是直接解压到目录里的免安装模式。如果是免安装模式,包管理器里永远查不到记录,排查时依赖进程路径和自启动项反而更可靠。所以包管理器查询可以作为参考,但不能作为唯一判断依据,最终还是要回到readlink /proc/PID/exe和systemctl list-unit-files这两个方向上来。
3. 解析根因:为什么 db_ha 进程会被残留
3.1 高可用组件 db_ha 的工作机制
db_ha 本质上是数据库高可用体系里的一个代理进程,负责节点间通信、健康检查、故障状态上报。它并不像数据库主进程那样跟随登录会话运行,而是通常由系统服务管理器托管,独立驻留后台。设计上它必须一直活着,才能在数据库主进程异常退出时执行故障切换。所以它使用的端口比如 6666,通常是固定的通信端口,集群内其他节点通过这个端口探测本节点的存活状态。
这个机制平时很可靠,但到了卸载场景就变成麻烦。用户以为卸载数据库就是“停止服务、删除文件”,但对高可用组件来说,agent 进程是独立运行的生命周期,它并不会因为主数据库程序被删掉就自觉退出。它甚至会认为数据库主进程的消失是一种故障状态,于是反复尝试重连对端节点,进一步延长了自身存活时间。
3.2 卸载脚本只管了数据库主进程
大多数数据库安装包的卸载脚本会按照“停止服务、删除文件、清理配置”的顺序执行,但停止服务这一段通常只处理主实例,没有遍历所有辅助进程。尤其高可用组件会由 HA agent 主动拉起,卸载脚本并不知道还有独立运行的后台进程存在。结果就是主程序卸载完成,agent 进程还留在内存里,继续占用端口。
在实际实施中,问题还会被复杂化。如果环境里配置了两个节点,卸载了一台节点上的数据库,另一台节点的 HA 进程还在正常发送心跳包,残留的 db_ha 接收到对端心跳后,会认为当前节点只是暂时异常,于是继续保持监听状态等待恢复,这就导致进程异常顽固,普通的 kill 信号可能根本不会让它干净退出。
3.3 自启动机制被遗漏
即便手动杀了残留 agent,如果不处理 service 文件和 rc 脚本,系统一重启 agent 会被再次拉起。这是“重启后端口又出现”的高频原因。systemd 的 service 单元是常见宿主,卸载程序没有删除/etc/systemd/system/下对应文件,也没有执行systemctl disable。另一种情况是/etc/rc.local里被人手动加过启动命令,卸载脚本不可能知道这份改动。
还有一种比较隐蔽的是临时写入的/etc/init.d/脚本。某些安装包在安装时会复制启动脚本到 init.d 目录,卸载过程只删除了主目录,却没有移除 init 脚本。所以清理必须走到自启动层面,不能只停留在进程层面。我处理这个案例时,最花时间的反而不是杀进程,而是把所有可能调用 db_ha 的启动入口全部找出来。
3.4 为什么是 6666 端口
6666 并非随机出现,它往往是 HA agent 的默认配置端口,也可能由配置文件里的 port 参数指定。实际部署中如果安装了两个高可用组件、或者历史版本遗留了配置,就可能造成端口互相占用。尤其在安全版软件里,高可用组件的端口通常会被固定配置,不会跟随主数据库端口一起变化。
因此清理后不仅要确认 6666 不再被监听,还要去配置文件里查看是否还有引用这个端口的参数。如果旧配置文件没有被删除干净,后续安装新版时安装程序会读取到这个旧参数,然后再次把 6666 配置给新的 HA 组件,最终导致新旧进程冲突。这也是为什么我后面会强调,清理残留必须连配置目录一起处理,不能只看进程和端口。
4. 完整清理实操:从进程、自启动到残留文件
4.1 先停服务再杀进程,别急着 kill -9
我看到有人一处理残留进程就直接kill -9,这样确实能把进程干掉,但并不可取。正确的顺序是先看有没有可控的停止方式。如果存在 systemd 服务,先执行:
systemctl stop db_ha systemctl disable db_ha如果没有服务文件,就先用kill PID发送 SIGTERM,给进程一个做退出清理的时间。只有等待几秒后进程仍不退出,才用kill -9 PID。强制终止的代价是可能丢失一些状态信息,在集群场景下还可能让其他节点误判本节点故障,触发不必要的切换动作,所以能正常停就先正常停。
实际操作中还有个细节:执行kill之前最好先记录当前 PID 和可执行文件路径,因为一旦进程被杀掉,/proc/PID目录就没了,再想通过它找回安装路径就要靠历史命令记录或者全局搜索,麻烦得多。
4.2 清掉自启动项
进程杀掉只是第一步,必须处理自启动。先查 systemd 单元:
systemctl list-unit-files | grep -i db_ha找到文件后执行:
systemctl disable db_ha rm -f /etc/systemd/system/db_ha.service systemctl daemon-reload这里disable和删除文件都要做,因为光 disable 只是去掉开机自启,service 文件还会留在系统里,后续可能被其他工具扫描识别到,认为软件还在安装状态。删除 service 文件后一定要执行systemctl daemon-reload,让 systemd 重新读取配置,否则已删除的 unit 状态还会被缓存。
接着检查/etc/rc.local、/etc/init.d/:
grep -rn "db_ha" /etc/rc.local /etc/init.d/ 2>/dev/null如果有匹配行,在确认不是系统需要的内容后直接删除对应行。也可以用sed批量处理,但手动打开编辑更稳妥,因为rc.local里可能同时存在多条启动命令,删错一行会影响其他服务的启动。
4.3 清理文件系统残留
进程和自启动项清理完后,开始删除目录和文件。先用readlink /proc/原来的PID/exe已经定位到路径,现在执行:
rm -rf /opt/某数据库/ha rm -rf /var/log/某数据库/ha /etc/某数据库/ha /var/opt/某数据库/ha这一步容易漏的是日志和配置目录。有些数据库版本会把 HA 配置放在/etc/某数据库/下面,有些放在安装目录的 conf 子目录,删除前最好用find / -iname "*db_ha*" 2>/dev/null全局搜索一遍,把零散文件清干净。清理前务必备份配置和数据目录,尤其当服务器上还有生产数据时,宁多留一份也不要急着删。
删除前还要再确认一下,是否还有其他进程正在使用这些目录里的文件。保险起见,执行一遍lsof +D /opt/某数据库,确认没有 open 的文件句柄后再删除。如果一个文件还在被进程使用,但目录已经被删,系统日志里会持续出现 “text file busy” 之类的报错,增加不必要的操作噪音。
4.4 清理用户和权限
某些安装包会创建专用系统用户,比如dbha用户用来跑 HA 进程。此时要检查用户是否还有其他关联服务在运行。确认无关联后删除:
userdel -r dbha执行前可以用ps -u dbha查看该用户下是否还有活动进程。除了用户本身,还要检查 sudo 配置或安全策略里是否有针对这个用户的残留规则,如果存在就一并清理。这类清理虽然不影响端口占用,但关系到系统安全基线,不能忽略。
4.5 验证端口释放
全部操作完成后,先验证端口释放情况:
ss -lntp | grep 6666这里应该没有任何输出。最好再重启一次系统,然后执行同样的检查,并观察服务器启动后的进程列表,确认 db_ha 没有被任何开机项拉起来。这一步可以把自启动清理遗漏的问题提前暴露出来,避免过了几天突然又发现端口被占用。
我自己的习惯是重启后还会再跑一次systemctl list-unit-files | grep -i db_ha,确认服务文件已经彻底不存在。这样才算真正收工,而不是当下看起来没问题就算完成。
5. 防患于未然:卸载前、安装时应该注意什么
5.1 卸载前的标准动作
软件卸载前,先记录现场。执行ss -lntp看所有监听端口,执行ps -ef | grep -E "某数据库|db_ha"看相关进程。把服务停止后再执行卸载程序,不要卸载工具一提示“完成”就走人。卸载完成后立刻核对之前的记录,确认所有端口都已释放,所有进程都已消失。这个流程很简单,但每次实施都能避免很大一部分残留问题。
为什么很多人会遇到残留问题?因为卸载过程里最容易被跳过的就是“先停止高可用组件”。数据库的 HA agent 一旦处于运行状态,它的配置和日志文件会被持续占用,卸载程序在删除文件时可能跳过正在使用的文件,阶段性地造成残留。先停服务,再删文件,顺序对了,残留概率会大幅降低。
5.2 安装时规划好目录和端口
实施经验里,最容易被忽略的是安装时的规划。不要在服务器上直接使用默认端口、默认管理员用户、默认安装路径。把安装路径、数据目录、日志目录、监听端口、集群通信端口记录到服务器的归档文档里。将来卸载或者排障时,这些信息能让排查时间缩短一半。如果同机需要部署多个实例,端口规划更要避免交叉,像 6666 这种常见的 HA 端口很容易重复。
这里我特别想说一下文档化的重要性。很多时候排障难,不是问题本身多复杂,而是根本不知道当初装了哪些组件、用了哪些端口。每次安装完顺手写一行记录,后续能省下大把时间。我自己吃过不少亏,现在每装一套环境都会把端口、路径、用户列成一张表放在/root下面,后面处理问题直接翻表,效率提升非常明显。
5.3 用脚本固化状态确认
最后建议把确认工作做成一个简单的检查脚本,每次卸载后执行一次。脚本核心是三段检查:
echo "== 监听端口 ==" ss -lntp | grep -E "6666|5432" echo "== 相关进程 ==" ps -ef | grep -E "某数据库|db_ha" | grep -v grep echo "== 自启动项 ==" systemctl list-unit-files | grep -E "某数据库|db_ha"脚本可以持续维护,在不同项目间复用。遇到其他软件残留,只需要替换关键字。之所以把确认流程脚本化,是因为人总有疲惫的时候,尤其在割接窗口或者凌晨变更的场景下,手工检查非常容易漏。脚本输出固定格式,一眼就能看出有没有异常,比靠记忆离线检查可靠得多。
6. 常见问题速查
6.1 端口被占用但 ss 里看不到进程名
如果你执行ss -lntp时没有得到 root 权限,进程名那一列会显示不出来。解决办法是sudo ss -lntp | grep 6666。如果加 sudo 后仍然看不到对应 PID,但端口确实是在监听,那可能是 selinux 或容器网络命名空间导致。容器环境下,宿主机看到的端口空间和容器内部不一致,排查时要注意当前 shell 所在的 namespace,不要拿宿主机命令去查容器内进程。
还有一种情况是端口处于非 TCP LISTEN 状态,比如 UDP 监听或者 TIME_WAIT 状态。ss -lntp只显示 TCP 的 LISTEN 状态,如果你要找的是 UDP,就要换成ss -lnup。我在处理一些中间件服务时经常会遇到这个误区,明明端口被占用,却因为只查了 TCP 就误判为没占用。
6.2 kill -9 后过一会儿进程又出现
这种现象基本可以断定有守护进程在拉起。重点排查父进程和计划任务:pstree -a -p PID、crontab -l、/etc/cron.d/、/etc/rc.local。另外还要看有没有监控 agent 在周期探测并复活服务。在数据库场景里,HA 组件通常有自我恢复机制,如果配置文件里还指定了另一节点的地址,agent 被终止后可能由另一节点的心跳触发重新拉起,这时候光杀本机进程是不够的,还要把配置文件里的连接参数删掉或修改。
我还遇到过一种诡异情况:某个定时任务每分钟检查一次指定端口,发现端口没监听就执行启动脚本。解决方案是先把 cron 里的相关行注释掉,再杀进程,最后再处理配置文件。如果顺序反了,你杀完进程之后很快就会被 cron 拉起来,给人感觉怎么都杀不死。
6.3 服务已 disable 但重启后端口仍被占用
disable 只取消了当前 unit 的开机自启,但如果服务文件被其他目录或某个 target 引用,仍然可能在启动阶段被调用。用systemctl is-enabled db_ha查看状态,用find /etc/systemd -name "*db_ha*"找残留文件。还可以查看启动日志定位是谁拉起的:journalctl -b -u db_ha --no-pager。通过日志能看到进程是在哪个启动阶段被拉起的,顺着时间线去查,通常能找到某个 target 或某个服务的依赖关系。
这类问题的核心在于,systemd 的 enable 状态和服务文件是否被其他单元依赖是两回事。有时候你明明 disable 了一个服务,但另一个服务的Requires=或Wants=里引用了它,开机时依然会被拉起。所以清理自启动项时要多想一步:不只查目标服务本身,还要查谁依赖了它。
我把常见问题整理成一张表,方便你对照排查:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 端口被占用但看不到进程名 | 权限不足或网络命名空间不同 | 用 root 权限重新执行 ss |
| kill -9 后进程又出现 | 存在守护进程或定时任务 | 排查父进程、cron、rc.local |
| disable 后重启又出现 | service 文件被其他 target 依赖 | 删除残留 unit,查看启动日志 |
我自己在多次实施里遇到过类似问题,后来每次卸载带高可用组件的软件,都会事前把端口、进程、服务文件列成三行清单,卸载后再逐项核对。这次服务器上残留的 db_ha 就是个典型:主程序卸载成功,但 HA agent 还占着 6666 端口。清理完自启动项后,重启验证才真正算收工。这个处理方法不只适用于这一款数据库产品,其他带 agent、带守护进程的中间件软件,遇到卸载残留时都可以按同样的思路排查,抓住端口反查和自启动清理这两个关键点,问题基本就能解决。