ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库卸载后端口被占?瀚高db_ha残留进程排查与彻底清理

数据库卸载后端口被占?瀚高db_ha残留进程排查与彻底清理

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 节的方法把进程树和服务文件完整看一遍,答案往往就藏在进程的父进程里。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进