Tags
- Airchina
- Android
- Anti-Spam
- Bluetooth
- bugs
- ChinaSouthern
- Django
- DNS
- Enterprise
- fastcgi
- Hainanair
- HAM
- HongKong
- InnoDB
- IPTV
- iSCSI
- kubernetes
- large-scale
- lighty
- Linux
- Logging
- Macao
- Meituan
- MM
- MySQL
- nginx
- Oracle
- Outdoor
- Percona
- Photo
- postfix
- Python
- RedHat
- Sentry
- systemd
- Traffic
- Travel
- Virtualization
- xtrabackup
- 信用卡
- 奥运
- 美食
- 规范化
- 软件工程
Meta
Blogroll
Mine
Tag Archives: 规范化
supervisor泄漏进程案例分析
起因 前几天使用 salt ‘*’ test.ping 的时候发现响应内容中有一些“某某minion was already deleted from tracker, probably a duplicate key“的提示信息。刚开始误以为是salt-key管理有问题,尝试删除再重新accept,但是依然会出错。到该minion上检查,发现上面运行了两套salt-minion*三层进程树,一共6个进程,其中一套的PPID为1,另一套的Parent是supervisord。 然后就开始研究这种情况是怎么产生的,发现有两种可能: 第一种可能 supervisor本身不被systemd监管,被SIGKILL信号杀死时,因为SIGKILL由内核直接处理,所以并没有机会关闭下属的进程,导致下属salt-minion进程树泄漏。而且不但salt-minion进程树泄漏,连同样被supervisor监管的另一个服务也一并泄漏,二者的PPID都变成了1号。 不过,如果supervisor本身被systemd监管,在其主进程被杀死时,systemd会给整个service slice cgroup里所有进程补刀,所以并不会泄漏进程;如果supervisor是被SIGTERM信号杀死,它也会给下属子进程发信号,一般也不会泄漏进程。 第二种可能 supervisor没有受到影响,正常运行;supervisor监管的salt-minion三层进程树的其中最高层进程(也就是supervisord的直属子进程)被SIGKILL信号杀死,随即,第二层进程exit(1) (不明原因,可能需要看一下salt-minion源码),导致第三层进程变成孤儿。经检查源代码的_spawn_as_child()函数,supervisor针对其监管下的每一个服务,都是采用 fork() + setpgid() +execve() 的方式来启动的,在调用setpgid()改变了process group id之后,第三层进程的孤儿收养关系就不再归属于supervisord进程,而是归属于1号进程。 随后supervisor会重启salt-minion服务,产生新的3个进程,加上之前剩下的,一共4个。 结论 考虑到观察到6个进程而不是4个,实际发生的大概是前一种情况 supervisor虽然有“能力”处理进程退出之后马上重启的工作,但是因为使用了setpgid()把下属服务与自己隔离,没使用cgroup机制把下属服务单独圈起来,又不具备1号的神圣地位,其实它并不知道到底下属了多少、哪些进程,从机制原理上就根本无法保证所有下属的孤儿进程都被其reap。还是建议不要在严肃场合使用 1号进程神圣,所有的服务进程监管工作都应该交给1号进程来处理
滥用crond触发systemd-login故障一例
故障现象 2021年1月20日接到通知,要把systemd升级到219-73.tl2.10或以上、并把rsyslog一起升级,以修复/var/log/messages无日志内容的bug。经实验,发现使用yum升级两个软件包之后,systemd-logind的可执行文件也被更新,导致该服务处于原可执行文件已删除的状态,所以我提议,在升级步骤中增加重启systemd-logind服务的动作。在Ansible playbook里,因为不能表达“大于219-73.tl2.10“这种范围型版本号,所以就明确指定systemd的版本为当前yum能自动安装到的最新版本219-78.tl2.3 2月1日由同事执行更新操作之后,大部分节点都正常工作,但有两台发生重启事故,另有一台上的 35777 systemd-login进程占内存高达4~6G。这三台恰好是一组elasticsearch的三台master节点,均为C8机型,即16G内存的kubernetes容器。 检查修复 我尝试重启剩余的这台的system-logind,发现新进程3851号仍然占6G内存。查看/proc/3851/smaps,该区域为heap;用pmap命令查看,显示为[ anon ]。对比正常服务器的同一个内存区域,才244K而已。 检查三台故障机及其宿主机的日志,发现大量oom记录,其中重启的两台所属宿主机的kubelet也发生故障重启:Feb 1 18:43:50 TENCENT64 kubelet: panic: runtime error: invalid memory address or nil pointer dereference 先gcore一份保留故障现场。由于操作系统组同事不登录上来观察,仅提供重启进程等建议,我只好自己做检查。根据建议,检查了dbus服务(dbus-daemon进程),发现也是可执行文件被删除的状态。检查yum日志,发现在去年6月升级了dbus包,但是服务进程是3月5日启动的,也就是升级包的时候并没有重启这个服务。再次尝试重启systemd-logind,新进程14278号,发现用内存VmPeak: 5270484 kB;但是过了一会儿再观察,发现增加到了VmPeak: 6599828 kB。这说明内存的增长是一个过程,虽然增长比较快,但并不是一下子就6G的。于是我决定strace一下它。 先关闭systemd-logind服务。使用命令strace -ff -s 1000 -p 1挂在systemd主进程上做跟踪,并用-o参数把多个进程的跟踪记录分别写在文件里。然后启动systemd-logind服务。这样,strace可以跟踪到 1号进程clone+execv执行systemd-logind的瞬间,以及systemd-login最开头的行为。检查systemd-login的strace记录,发现大量访问 /run/systemd/session/ 目录下面文件的动作。检查该目录,发现大量残留文件。搜索,发现 https://www.jianshu.com/p/343a072e2521 … Continue reading
Reproducible builds 果然是很重要的
去年10月17日公司出了个事故,“幸好有我“,力挽狂澜。我觉得还是把具体内容脱敏,记录一下吧,免得将来忘了。 早晨test环境某个自研产品组件build成功,但是运行时错误日志里一堆“Broker hostname updated”等等,仿佛孔乙己说的话,让人听不懂。我刚开始没把这个当回事,请Kafka管理员去检查,几个小时都没查出原因来。 到了午后,各自研组件纷纷开始更新Live环境的版本,发布的都是前几天在Test环境已经测试定稿的版本,居然也出现了类似的问题。更狠的是,回滚居然也无效! 最终查实,有不少自研产品的build脚本里,使用wget下载master版本的librdkafka然后编译使用。而librdkafka恰在前一天(10月16日)被提交了很多修改。当时紧急解决的方法是使用librdkafka的上一个被明确标记了tag的v1.2.1版本代替master版本。 事后复盘处理过程,觉得当时的处理过程也有问题:所谓回滚其实并非直接使用旧版本编译出来的docker image,而是重新build了旧版本,而且编译时指定的是branch名字,而不是精确的commit id,这样的问题是无论外部代码(master版本的librdkafka)还是自研代码(注意branch有可能move forward哦),都不一定是原来的版本,这个行为其实并不能称作回滚,而是带有主观回滚意向的一次重新构建。 在这种情况下,讨论自研代码和外部库的质量,都是缺乏讨论基础的。这种讨论应该基于确定的版本。 说实话,我以前对于传说中的“Google把自研代码、外部库和工具链”全都纳入版本化管理,是有点嗤之以鼻的。当时我觉得这种做法,在处理多个自研软件的外部依赖相互冲突时,会带来额外的行政成本,在国内企业的KPI风格下,会导致相互推诿。经过这次力挽狂澜之后,觉得相互推诿其实是KPI至上主义导致的,而不是把所有代码都管起来导致的;不过是否真的要管这么大范围,尚有待商榷。
经典错误——使用/etc/security/limits.conf配置文件 和 ulimit -n命令
很多以讹传讹的半桶水文章,都教人修改/etc/security/limits.conf配置文件来放宽“打开的文件数量”限制,如果可以再多一滴水的话,还会加一句“重启后生效”。 其实,使用这个配置文件,和使用ulimit -n命令一样,属于很经典的错误。 设置或放宽“打开的文件数量“限制,其本质是调用了setrlimit()函数,设置了RLIMIT_NOFILE资源。在有特权的程序中调用这个函数,可以提高上限(放宽限制),而普通权限的程序只能自己勒死自己和新生的子进程。 而/etc/security/limits.conf这个配置文件是怎么生效的呢?其实用dpkg -S或rpm -qf查一下就很容易知道,这个文件是pam_limits.so的配置文件,而pam_limits.so是在/etc/pam.d/中被login和sshd等多个配置文件声明将要被调用的。 系统开机的时候,1号进程init“自然而然”是root身份运行,其下属的getty/login和sshd进程,也都是root身份。这些程序都可以随意调用setrlimit。当身份认证(部分工作由PAM来做,所以可以读shadow文件)完成之后,login和sshd的子进程会为用户准备好session(网络登录调用pam_mkhomdir建设HOME目录、pam_limits模块设置rlimit、pam_env模块读取/etc/environment设置环境变量,甚至显示motd这种功能也是PAM模块实现的)并将自己降级到登录的用户身份,再启动一个shell给用户使用。 /etc/security/limits.conf 只对“调用过pam_limits.so“的登录过程有效。但并不是所有场景都经过这个过程的。而ulimit命令呢,它本身只是shell是一个内部命令而已,只对“该shell进程”及随后新产生的子进程有效。 但是需要放宽rlimit的程序,往往不是在shell中由用户手工运行的程序,而是提供大规模网络服务的后台进程。它们所需的rlimit,要在init脚本、service unit文件中设置;支持从root身份启动的服务,一般都有自行设置rlimit的能力。 如果不理解上面的内容,就容易引发一些莫名其妙的故障。比如之前我在FreeWheel工作的时候,前辈为后台服务写的的init脚本里没有调用ulimit -n命令,而在root用户的~/.bash_profile里有这个命令。造成的后果,就是开机自动启动该服务的时候,启动的是一个打开文件数量受限,以至于无法保持很多socket的网络服务,而当运维人员登录进去手工重启服务之后,又莫名其妙变好了,以至于没法检查这个故障到底是怎么发生的。
配置管理 vs provisioning 及配置管理工具的几点随想
关于“怎么构建一个确定的运行环境”这件事,有多个流派,其中一个是配置管理,另一个是provision流。 配置管理流派,适合于物理服务器、虚拟机等等,有机会长期存活的环境。因为它们有机会长期存活,在其生命周期里需要使用技术手段去维持一个有序可控的状态,排除软件运行时累计的旧日志和临时数据、手工操作带来的计划外变更等等影响。 而provision流派,直接从模版构建一个(容器或虚拟机)实例起来运行,用完就扔,基本上没有重新调整的价值。这种东西往往依赖于服务注册与发现机制,因为在它真正起来运行之前,网络通信方面的参数无法被外界预知,外界不知道如何跟它通信;对于日志和数据,也提倡使用显式远程的方式去访问。 再说说配置管理工具的几点随想: 我最近一年在给下属的一个公司做一些产品运维工作,其中遇到把设备投放到客户的网络环境去运行这种情况。考虑到网络通信的问题,我只好选择了“反向连接”的saltstack软件。在通信的角度来考虑,配置管理工具可以分为:master主动连接minion(ansible等)、minion主动连接master(puppet、saltstack等) 今天听师兄说他的一个同事因为认知问题,把一批机器glibc给删掉了。尝试恢复的时候,他发现ansible无法正常运行(我猜想sshd启动python的时候因为缺glibc而失败了),只好改用saltstack。因为saltstack的salt-minion是长期运行的,一旦启动之后,外部依赖较少,才能在glibc被删除这么极端的条件下苟活。在“有没有agent”的角度考虑,配置管理工具可以分为:有agent(saltstack、puppet、cfengine等)和无agent(ansible等) 另外,其实还有一个分类角度,就是主动和被动。saltstack和ansible是主动式的,运维工程师可有更多的主动权,可以用手工指定minion,或者指定批次规模分批执行等手段,控制变更的节奏;cfengine、puppet等是agent定时刷新式的(虽然听说也可以主动?不过我经验较少还没用过),得按定时器来,这种情况下就得运维工程师多看监控了,一不小心,死都不知道怎么死的。
为什么有些系统是动作式的而非描述式的?
接手一个旧系统,上线发布是“拷贝文件过去,替换掉,重启服务”这样的,而不是“把编译好的东西打包,发上去安装,包的scripts部分带有重启动作”。 是否打包、包里包含什么,很有讲究: 查询当前版本号:XXX -v vs rpm -qi XXX 重启服务:killall XXX && sleep 30 && XXX vs /sbin/service XXX restart 删除软件:rm -fr XXX vs rpm -e XXX 安装依赖: ldd XXX |xargs -n1 yum provides |xargs yum -y install vs rpm -i XXX … Continue reading
Sentry整理杂记
本讨论均基于Sentry 7.7版本 插件机制 自带插件 src/sentry/plugins/ 每插件一个目录 自带插件loader:src/sentry/conf/server.py 里的 INSTALLED_APPS tuple 外装插件loader:utils/runner.py 里的 install_plugins()函数,对iter_entry_points()遍历并将其加入 INSTALLED_APPS tuple 中 外装插件注册:插件的 setup.py 里执行注册 entry_points 的过程: 例:https://github.com/getsentry/sentry-groveio/blob/master/setup.py 参考资料:https://pythonhosted.org/setuptools/pkg_resources.html #!/usr/bin/env python import pkg_resources for ep in pkg_resources.iter_entry_points(‘sentry.apps’): print str(ep) for ep in pkg_resources.iter_entry_points(‘sentry.plugins’): print str(ep) sentry-jira … Continue reading
Python import语句一处文档和行为不一致的问题
话说,我团运维组曾一度极为昌盛,可惜后来公司里有人搞政治,好好的一个组被拆成了好几个,后来竟分属不同大部门,工作关系也开始形同陌路。考虑到丰富经验,换换方向,今年7月,我转岗到了运维开发组,开始真正把Python作为职业。 无奈,基础差,还得学,8月18日,这么吉利的一天,我第n次在看Python的文档,看着看着,注意到一些标准库的名字是带点的,比如os.path,再比如logging.config等。然后就开始help各个模块,发现os.path的名字居然不叫path而是posixpath!!再看,logging是个package,但os居然是个module!! 于是,问题就来了: Python语言里对 import A.B.C 这种形式是这么规定的: when using syntax like import item.subitem.subsubitem, each item except for the last must be a package; the last item can be a module or a package but can’t be a class or function … Continue reading
墨菲定律之多处保存必然会出现不一致——谈Linux上的时区问题
技术部的工作日报,原本是早晨9点发,脚本换了一台服务器运行之后变成17点了。算算时差也知道是时区问题。 检查了一下, /etc/timezone 内容误为 Etc/UTC,而 /etc/localtime 是从 /usr/share/zoneinfo/Asia/Shanghai 复制过来的,二者不一致。date命令使用 /etc/localtime 而 cron 按 /etc/timezone 文件执行,于是造成了执行时间不对的问题 另外需要注意的是,cron 只在开始运行时读取一次该文件,而每分钟唤醒时不再读取,所以改过文件之后还得重启该服务;又得注意的是,service cron stop 似乎并不会终止cron进程,所以……你懂的 以上就是墨菲定律之:多处保存,必然会出现不一致
MySQL 备份、版本兼容性二三事
最近几天在筹备多做一个 slave 数据库,所以就用 InnoDB Hot Backup 工具把原来的 slave 库备份了一份,拷贝过来,执行了一段 binlog 之后卡住了,然后发现上述工具对 ARCHIVE 存储引擎的表没有考虑,没把数据文件复制过来。于是改用 Percona 出品的 xtrabackup 工具,其 innobackupex 脚本考虑的比较周全。这样总算搞到一份相对完整的备份。 搞到备份数据之后,我又想尝鲜,于是装了 Percona Server (即 MySQL 的一个增强 fork)的 5.5 版本,结果发现无法操作上述 ARCHIVE 存储引擎的表。查了查 MySQL 5.1 的文档,说从 5.0 到 5.1 之间,ARCHIVE存储引擎变了存储格式,不可能兼容,要求 dump/restore 操作。服了…… 考虑到 … Continue reading