1. 故障现场回顾:业务起不来,6666端口被谁占了?
先还原一下当时的场景。我在某台数据库服务器上处理过一个挺典型的卸载残留问题,现象非常明确:某个应用连不上数据库,启动日志直接报Address already in use,定位到端口发现是 6666 被占用。第一反应肯定是查端口对应的进程,结果发现占用 6666 的进程叫db_ha相关组件,路径指向的目录早在几天前就已经被删掉了,但进程仍然活着。再一问之前的操作记录,确实卸载过一套瀚高安全版 V4.5.10 的数据库环境,卸载完当时没细看,以为删了主目录就万事大吉,结果 agent 进程留在了系统里,把端口一直占着。
这类问题在国产数据库、商业数据库的卸载场景里其实很常见。尤其是带高可用功能的版本,卸载时不仅会留下主程序文件,还会留下独立运行的 agent 进程、系统服务注册文件、开机自启脚本等。大部分卸载工具只会清理自己知道的部分,高可用子组件往往不在卸载脚本的覆盖范围内,最后就会出现“主程序没了、进程还在跑”的尴尬状态。
这次的故障本身不难处理,但排查过程里绕了不少弯路,我打算把完整流程拆开讲一遍。适合正在维护数据库服务器、或者遇到类似端口占用又找不到根因的人参考,尤其是负责卸载大型软件之后还需要保证系统干净的人员。下面从端口定位、进程分析、根因理解、彻底清理这几个环节逐个说清楚。
1.1 第一步确认端口占用情况
遇到端口被占,首先要确认的并不是“谁占着”,而是先把占用的证据抓全。Linux 下常用的几个命令:
ss -tlnp | grep 6666 netstat -tlnp | grep 6666 lsof -i :6666我这边实测时netstat的输出是这样的:
tcp LISTEN 0 128 *:6666 *:* users:(("db_ha",pid=12345,fd=16))从输出里能拿到两个关键信息:进程名是db_ha,PID 是 12345。到这里只能说明“有一个叫 db_ha 的进程占着端口”,还不能确定它是不是残留,需要顺着 PID 继续挖。
lsof -i :6666的输出通常会多一个 COMMAND 列,看起来更直观。但注意,lsof在部分精简系统里默认没装,需要先yum install lsof或者apt install lsof。在没有 lsof 的机器上,ss是更可靠的选择,它来自iproute2包,几乎主流发行版都自带。
1.2 顺着 PID 挖出残留进程
拿到 PID 之后,立刻执行:
ps -fp 12345正常情况下你会看到一个完整的启动命令。如果是残留进程,往往会出现一个特征:命令路径后面带着(deleted)。比如:
root 12345 1 0 Apr10 ? 00:01:23 /opt/HighGo4.5.10/db_ha/bin/agent (deleted)这个(deleted)就是关键证据。它说明程序文件已经被删除,但进程还在内存里跑。Linux 下删除正在运行的文件不会直接终止进程,只是文件名从文件系统里消失,进程持有的文件句柄仍然有效,所以它还能继续监听端口、继续跑逻辑。
接着看一下进程的启动时间和父进程:
ps -o pid,ppid,lstart,cmd -p 12345输出里 PPID 是 1,lstart是几天前,说明它大概率是被 systemd 或某个开机脚本拉起来的常驻进程。到这里基本可以下结论:这就是瀚高安全版 V4.5.10 卸载后残留的db_haagent。
2. 为什么卸载后进程还赖着不走:db_ha agent的运行机制
很多人到这里会直接kill -9,杀完发现端口确实空了,但系统一重启进程又回来了。原因在于只处理了表象,没有处理守护机制。要彻底解决,得先理解 db_ha agent 是什么,它为什么这么顽强。
2.1 db_ha agent是什么,为什么会监听6666
瀚高安全版 V4.5.10 这一类带高可用能力的数据库产品,通常会内置一个高可用组件。db_ha是这个组件的进程名前缀,而 agent 是部署在每个数据库节点上的代理进程,负责节点状态采集、心跳检测、故障切换信号传递、主备状态同步等任务。
6666 端口通常是 agent 用于监听管理指令或与对端节点通信的端口,具体端口号可能随版本和配置变化,但在这个版本里默认落在 6666 上。也就是说,只要 agent 进程还在,端口就不会释放,哪怕数据库主程序已经卸载干净。
agent 进程的特殊之处在于它不是“数据库主服务的一部分”,而是独立注册的常驻服务。卸载工具如果只处理数据库主服务,并不会通知 agent 退出。更麻烦的是,agent 通常带有自守护机制——主进程被杀死,管理进程会在几秒内把它重新拉起。所以直接 kill 往往无效。
2.2 卸载工具没清干净的几种典型原因
结合我处理过的几次类似案例,卸载残留主要出在下面几个环节:
- 卸载脚本覆盖范围不全。V4.5.10 版本的主卸载程序主要删除数据库安装目录、数据目录、系统用户等,高可用组件 agent 被设计成独立模块,不随主卸载程序联动。
- 服务注册残留。安装时会向 systemd 写入 service 文件(比如
db_ha_agent.service),卸载时如果只执行主卸载脚本,这个 service 文件不会自动删除,系统每次启动都会尝试拉起 agent。 - 停止顺序错误。正确顺序应该是先停服务再卸载,但很多人都直接执行卸载命令。卸载脚本在删除文件时发现进程占用,会选择跳过某些操作,最后留下孤儿进程。
- 文件删除但进程不退出。Linux 的运行机制决定了一个正在执行的二进制文件被删除后,相关进程可以继续运行,除非手动 kill 或重启。
用一个生活化类比来理解:卸载数据库就像清理一套房子,主卸载程序是把客厅家具搬走了,但高可用 agent 是住在阁楼里的租户,不仅没走,还在继续开着窗户。你不去把租户请走,光搬家具当然没用。
3. 定位残留进程的完整排查清单
处理这类问题最忌讳“猜”。一上来就删目录、杀端口,很有可能误伤别的组件。我建议按下面这套清单走一遍,每个环节都把证据留好。
3.1 端口、进程、父进程三层定位
第一层是端口定位,确认占用端口的是谁:
ss -tlnp | grep 6666第二层是进程定位,确认这个进程的完整启动命令、运行状态、是不是已删除文件:
ps -ef | grep db_ha | grep -v grep ls -l /proc/12345/exe ls -l /proc/12345/cwd/proc/PID/exe是一个符号链接,指向进程对应的可执行文件。如果文件已删除,这个链接会显示(deleted)。同理,/proc/PID/cwd指向进程的工作目录,残留进程的工作目录往往也指向已经删除的路径。
第三层是父进程定位,确认是谁把它拉起来的:
pstree -asp 12345 cat /proc/12345/status | grep -E "PPid|Name"如果 PPid 是 1,说明已经托管给 init/systemd,背后一定有对应的服务文件或启动机制。如果 PPid 是某个 shell 脚本,那说明存在一个包装脚本在守护它,杀完 agent 还可能被脚本二次拉起。
3.2 检查系统服务与自启动项
进程的启动来源通常有四个地方:systemd 服务、init.d 脚本、rc.local、计划任务。逐一排查:
systemctl list-units --all | grep -iE "highgo|db_ha|ha_agent" systemctl status db_ha_agent.service ls -l /etc/systemd/system/ | grep -iE "highgo|db_ha|hgdb" chkconfig --list 2>/dev/null | grep -i highgo cat /etc/rc.local crontab -e -l 2>/dev/null | grep -i highgo实际案例里,残留的 service 文件是db_ha_agent.service,它一开机就会去执行某个已经不存在或已经删除的二进制文件。由于文件已删除,systemd 的 ExecStart 指向路径失效,但因为进程之前已经被拉起来了,systemd 会认为服务仍在运行中。
3.3 判断结论:是不是卸载残留
综合上述信息,判断条件有三个:
- 端口被进程监听,且进程名与数据库组件强相关。
- 进程的可执行文件路径已经带有
(deleted)标记。 - 存在对应的残留服务文件或自启动项,且数据库主程序已不存在。
三个条件都满足,就不是普通端口冲突,而是典型的卸载残留。这时候需要做的不是单纯杀进程,而是把启动源头一起拔掉,否则当时清净、重启复发。
4. 根因深挖:瀚高安全版V4.5.10为什么容易留下db_ha进程
这次故障虽然处理起来不算复杂,但为什么偏偏是 V4.5.10 安全版容易留下这个坑,值得单独说一段。
4.1 高可用组件的守护机制
数据库产品的高可用组件为了保障“数据库挂了能自动拉起”,会在设计上加入多层守护。agent 不是一个孤零零的进程,而是由 manager 进程 + worker 进程组成的进程组。manager 负责监控 worker 的状态,worker 负责实际的心跳通信和状态上报。
卸载主程序时,如果 agent 进程组正在运行,它会在自己目录被删除后仍然存活。甚至在某些场景下,manager 发现工作目录异常后会尝试重新初始化配置,反而让残留更加顽固。
我在这次故障中看到的进程关系大致是这样的:
systemd └─ db_ha_manager └─ db_ha_agent(监听 6666)杀掉 agent 只解决端口问题,manager 会把它重新拉起来。必须连根拔掉 manager 和服务文件,才算真正清理干净。
4.2 卸载顺序与服务注册残留
瀚高安全版 V4.5.10 的安装过程通常会做这几件事:
- 创建系统用户和用户组。
- 在
/opt下创建数据库安装目录。 - 在
/etc/systemd/system下注册数据库服务和 HA 服务。 - 在
/etc/profile.d/下添加环境变量脚本。 - 在数据目录中初始化集群配置。
卸载时如果按正常流程走,理论上会把这些都清掉。但这个版本的卸载程序对 HA 组件的处理确实存在盲区,它的实现是先删主程序文件,再删除数据目录,最后尝试停止服务。顺序不对导致 agent 进程在文件已被删除时还在运行,卸载程序又检测到进程占用,于是跳过了一些清理操作,最终留下了 service 文件和 agent 进程。
4.3 Linux“文件已删进程仍在”特性放大了问题
这是 Linux 系统本身的一个特性:进程启动时通过 inode 引用二进制文件,文件被删除只是从目录结构中移除,进程已有的 inode 引用不会消失,因此进程可以继续运行。Windows 上如果程序文件被占用,通常会提示“文件正在使用中”,而 Linux 默认不阻止删除正在运行的文件,也不会提示。这个特性是很多运维事故的来源,不只是数据库,Nginx、Tomcat 这类服务在升级时如果忘了停服务直接覆盖文件,也可能出现“旧版本还在跑”的现象。
理解这一点,就能明白为什么光看磁盘目录没用,必须正面处理进程本身。
5. 彻底清理步骤:从停止守护到端口释放
清理的动作看起来是“杀进程、删文件”,但顺序很重要。我见过不少人上来就rm -rf,结果文件被进程占用删不干净,还破坏了一些共享资源。下面是整理好的操作路径,可直接参考。
5.1 停服务、禁自启(顺序很重要)
不要先 kill agent,先停掉可能把它拉起来的服务。这次故障里,残留的 systemd 服务名类似db_ha_agent.service,执行:
systemctl stop db_ha_agent.service systemctl disable db_ha_agent.service如果你不确定服务名,先查:
systemctl list-units --all | grep -i ha也可以用systemctl status看具体是哪个 unit 在管理 6666 端口的进程。关键在于先做 stop 和 disable,切断“被重新拉起”的路径。
同时检查其他自启动位置:
grep -l "db_ha\|highgo" /etc/rc.local /etc/init.d/* 2>/dev/null crontab -l 2>/dev/null | grep -i highgo有相关行就注释或删除。注意修改/etc/rc.local之前先备份,毕竟这是系统级文件。
5.2 按进程树逐层清理
停掉服务后,再查看进程树:
pstree -asp 12345这次故障中,父进程是 manager,子进程是 agent。清理顺序必须是先杀父进程、再杀子进程。如果倒着来,子进程被杀了,父进程会马上再拉起一个,等于白杀。
kill -9 12345先记录下所有相关 PID。如果进程已经变成孤儿进程(PPID 为 1)且没有 systemd 服务保护,可以直接 kill。如果有 manager 在,优先 kill manager:
kill -9 <manager_pid> sleep 2 ps -ef | grep db_ha | grep -v grep确认没有 db_ha 相关进程残留,再进入文件清理阶段。
5.3 清理目录、服务文件、自启动配置
这一阶段要胆大心细。先确认凡是与数据库相关的目录确实不需要保留数据。如果你还有备份需求,先把数据目录整体备份到别的机器,再执行清理。
常见残留位置包括:
- 数据库主安装目录,比如
/opt/HighGo4.5.10 - HA 组件目录,可能在主目录下的
db_ha子目录,也可能独立安装在/opt/hgdb_ha - 数据目录,如
/var/lib/highgo、/data/highgo - 系统配置目录,如
/etc/highgo、/etc/HighGo - 环境变量脚本,如
/etc/profile.d/highgo.sh - systemd service 文件,如
/etc/systemd/system/db_ha_agent.service
具体操作示例:
rm -rf /opt/HighGo4.5.10 rm -rf /var/lib/highgo rm -f /etc/systemd/system/db_ha_agent.service rm -f /etc/profile.d/highgo.sh systemctl daemon-reload如果担心误删,可以先把目录 mv 成一个临时名:
mv /opt/HighGo4.5.10 /opt/HighGo4.5.10_bak_$(date +%Y%m%d)系统运行几天确认没问题后再真正删除。这样至少给自己留了一条后悔的退路。
5.4 验证清理结果
文件清完之后,做四件事:
第一,确认端口已释放:
ss -tlnp | grep 6666没有输出就说明端口已经释放。
第二,确认没有残留进程:
ps -ef | grep db_ha | grep -v grep第三,确认服务不存在:
systemctl list-units --all | grep -i highgo systemctl list-unit-files | grep -i highgo第四,也是容易被忽略的一步:重启服务器,再执行一次第一步到第三步,确保不会开机复发。这一步很有必要,因为很多残留进程只在开机阶段被某些机制拉起,不重启根本发现不了。
我这次处理时,清完文件和进程后特意重启了机器,再次检查 6666 端口已经彻底空了出来,应用重新启动一切正常。
6. 常见问题与避坑实录
这类故障在实际处理中还会遇到一些变种,下面把常见的问题整理一下。
6.1 kill -9杀不掉的一类特殊情况
如果你执行完kill -9之后发现进程还在,多半是这几类原因:
- 有守护进程在持续重新拉起。
- 进程处于不可中断睡眠状态(D state),正在等待磁盘 IO,暂时无法被杀死。
- 进程变成了僵尸进程(Z state),需要父进程回收。
处理方式:先看进程状态:
ps -o pid,ppid,stat,cmd -p <PID>如果是 Z 状态,杀不掉是正常的,它已经死了,只是因为父进程还没 wait,需要处理父进程。如果是 D 状态,只能等待 IO 恢复,或者等系统重启。如果是被守护拉起,回到第 5 节先切断守护源头。
6.2 端口明明不再监听,但业务还是起不来
有一种情况比较迷惑:ss -tlnp | grep 6666没有结果,但应用启动仍然报端口被占用。这通常是 TCP 连接处于 TIME_WAIT 状态,或者端口绑定在某个特定网卡上,ss -tlnp默认可能不显示所有状态。
用这个命令看全一点:
ss -tunap | grep 6666或者用:
netstat -a | grep 6666如果显示大量TIME_WAIT,说明是旧连接在排队释放,一般等几分钟就会消失。如果绑定在特定地址,先确认应用监听的地址是否与其他服务冲突。
6.3 重启后残留进程又回来了
这说明清理不彻底,大概率服务文件没有删干净。有时候服务文件名和“highgo”完全不相关,比如叫hg_agent.service,或者被写进了/usr/lib/systemd/system/而不是/etc/systemd/system/。排查时要扩大范围:
find /etc/systemd -name "*ha*" -o -name "*hg*" -o -name "*db*" 2>/dev/null grep -rl "db_ha\|HighGo" /etc/systemd /usr/lib/systemd 2>/dev/null另外一个容易忽略的位置是/etc/systemd/system/multi-user.target.wants/,服务 enable 后会在里面创建软链。如果只删了 service 文件忘了处理软链,systemctl disable的注册信息其实还残留着,但通常daemon-reload后不会重新拉起。为了彻底,建议直接把 service 文件和相关软链一起清掉。
6.4 没有安装目录却还有进程怎么办
如果进程的启动路径已经显示(deleted),你又找不到安装目录在哪,可以继续从/proc/PID里挖信息:
cat /proc/PID/environ | tr '\0' '\n' | grep -iE "highgo|hg" cat /proc/PID/maps | grep -i highgo ls -l /proc/PID/fd | grep deleted/proc/PID/fd会显示已删除但仍被进程打开的文件描述符,可以顺藤摸瓜找到之前关联的数据文件或日志文件路径。即使目录已经删掉,日志文件如果被进程持续写入,依然可以通过 fd 找到,并看到内容。这也是确认残留来源的好办法。
6.5 经验清单与操作禁忌
这一节总结几条踩坑换来的经验。
- 卸载数据库前,必须先确认高可用相关服务已经停止,包括主服务和 agent。
- 执行卸载后,不要立刻跑一堆无关任务,先检查端口、进程、服务、自启动项四类残留。
- 清理残留时,不要一上来就
rm -rf,先停服务、再杀进程、最后清文件。 - 不要在业务高峰期做这类清理,如果误杀正常服务,影响会被放大。
- 重要数据一定要先备份再清理,任何卸载工具都可能存在覆盖不完整或误删的风险。
7. 一点个人体会
处理完这次瀚高安全版 V4.5.10 的残留问题之后,我调整了自己所有数据库卸载的标准流程:卸载前先记录端口监听基线、停服务、卸载后再检查进程和服务文件。相比之下,单纯依赖卸载工具的自查报告,风险很高。
这次问题的根源不在于kill没打对,而在于没有先理解进程被谁保护、被谁拉起。先把守护机制断了,再去清进程和文件,顺序对了,问题就变得很简单。如果你也遇到类似端口被占但找不到根因的情况,建议不要急着重启机器,先用第 3 节的方法把进程树和服务文件完整看一遍,答案往往就藏在进程的父进程里。