问题起源

服务跑在富容器中。容器使用init进程作为一号进程,然后用systemd管理所有service。

在做一次升级时,nginx启动脚本有更新,原来是root拉起,现在进行了去root改造,使用nginx用户拉起。


升级过程中,发现nginx进程无法被拉起,报错:

"Refusing to accept PID outside of service control group, acquired through unsafe symlink chain: %s", s->pid_file);

经排查,相同版本systemd和nginx,相同改动,在物理机上正常,但在k8s环境上有问题。因此猜测时容器化环境相关问题。

提出疑问:

●什么场景会触发这个问题?

●为什么物理化环境没有问题,而容器化环境有此问题?

问题分析

要确认问题所在,需要从systemd代码入手,从有问题的日志反查。

最初直接从github中查看代码仓库,但是难以与当前环境的版本对应,因此直接下载到src.rpm包,解压包出来,打上所有patch,来获取当前rpm的源代码:

现场systemd版本为systemd.x86_64:219-78.tl2.7.1 (该版本与219-78.el7_9.7代码相同)。

对应src rpm下载地址:https://mirrors.tencent.com/tlinux/2.4/tlinux/SRPMS/systemd-219-78.tl2.7.1.src.rpm

通过src rpm获取源代码:

# 起一个tlinux2.4容器作为编译机,挂载/data/目录:
IMAGE_ID='xxx'
NAME=tlinux2_compile
docker run --privileged -idt \
--name $NAME \
-v /data:/data \
--net host \
${IMAGE_ID} \
/usr/sbin/init
docker exec -it $NAME /bin/bash # 设定rpmbuild目录为/data/rpmbuild,以方便后续安装rpm:
bash-4.2# cat ~/.rpmmacros
%_topdir /data/rpmbuild cd /data/rpmbuild
# 放置rpm到本目录, 安装基础systemd rpm:
rpm -ivh systemd-219-78.tl2.7.1.src.rpm
cd SOURCES
ls

使用如下脚本,获取打了所有patch的完整源码:

#!/bin/bash

# 检查是否提供了 SRPM 文件
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <path_to_srpm>"
exit 1
fi SRPM_FILE=$1 # 提取 SRPM 文件
# rpm -ivh $SRPM_FILE # 获取包名和版本号
SPEC_FILE=$(find /data/rpmbuild/SPECS -name "*.spec" | head -n 1)
PACKAGE_NAME=$(rpmspec -q --qf "%{NAME}\n" $SPEC_FILE | head -n 1)
VERSION=$(rpmspec -q --qf "%{VERSION}\n" $SPEC_FILE | head -n 1) # 解压源代码
SOURCE_TARBALL=$(find /data/rpmbuild/SOURCES -name "${PACKAGE_NAME}-${VERSION}*.tar.*" | head -n 1)
mkdir -p /tmp/${PACKAGE_NAME}
tar -xf $SOURCE_TARBALL -C /tmp/${PACKAGE_NAME}
cd /tmp/${PACKAGE_NAME}/${PACKAGE_NAME}-${VERSION} # 应用补丁
PATCHES=$(grep '^Patch[0-9]*:' $SPEC_FILE | awk '{print $2}')
for patch in $PATCHES; do
patch -p1 < /data/rpmbuild/SOURCES/$patch
done echo "所有补丁已应用,打了补丁的源码在 /tmp/${PACKAGE_NAME}/${PACKAGE_NAME}-${VERSION} 目录中。"

添加打印日志,重新编译,查看日志,gdb

# 进入/data/rpmbuild目录:
cd /data/rpmbuild
# 修改代码,添加日志: # 提交commit,生成patch:
git commit -m "xxx"
git format-patch HEAD^ # 将patch放到/data/rpmbuild/SOURCES: # 修改/data/rpmbuild/SPECS/systemd.spec
## 添加该patch信息:
Patch0852: 0001-comment-to-test.patch ## 添加-g -O0 flag以允许gdb:
%configure "${CONFIGURE_OPTS[@]}"
export CFLAGS="-g -O0" #HERE
make %{?_smp_mflags} GCC_COLORS="" V=1 # 重新打包rpm
rpmbuild -ba SPECS/systemd.spec # 在目标机器上重新安装:
rpm -e --nodeps systemd
rpm -e --nodeps systemd-debuginfo
rpm -ivh /data/rpmbuild/systemd-219-78.tl2.7.1.x86_64.rpm
rpm -ivh /data/rpmbuild/systemd-debuginfo-219-78.tl2.7.1.x86_64.rpm # tailf -f查看systemd日志:
journalctl -f # 重启csp-nginx
systemctl restart csp-nginx # gdb systemd:
gdb /usr/lib/systemd/systemd 1

参考:1.https://systemd-devel.freedesktop.narkive.com/bLn5kkmz/systemd-debugging-with-gdb

最小复现环境

1.tlinux2.4操作系统,systemd版本为219-78.tl2.7.1 ,以docker容器拉起。

2.配置nginx为nginx用户:

chown nginx:nginx -R /var/log/nginx/
chown nginx:nginx -R /var/lib/nginx/ nginx_user=$(cat /etc/passwd|grep -w nginx|wc -l)
if [ $nginx_user == 0 ] ; then
useradd -d /var/lib/nginx -s /sbin/nologin nginx
fi
sed -i 's/user root;/user nginx;/g' /var/lib/nginx/nginx/conf/nginx.conf

之后重启nginx:

systemctl restart nginx

代码流程分析

service_load_pid_file(Service *s, bool may_warn)

// 传入pid_file与CHASE_SAFE的flag,检查权限是否正常:
fd = chase_symlinks(s->pid_file, NULL, CHASE_OPEN|CHASE_SAFE, NULL);
if (fd == -EPERM) {
questionable_pid_file = true;//设置该flag,表示该pid_file是有疑问的,可以暂且不检查该项目,继续只检查其他项目。后面还有其他判断,再决定本pidfile是否可以使用:
fd = chase_symlinks(s->pid_file, NULL, CHASE_OPEN, NULL);
}

此处chase_symlinks的作用:

  1. 逐级遍历pid路径:如当前服务的pid路径被定义为/var/lib/csp_nginx/nginx/sbin/nginx.pid,则会遍历到如下路径:

    /var

    /var/csp_nginx

    /var/csp_nginx/nginx

    /var/csp_nginx/nginx/sbin

    /var/csp_nginx/nginx/sbin/nginx.pid

    检查每个层级目录或文件归属的用户uid。若前者权限高于后者,则认为允许。若后者权限高于前者,则认为有安全风险,设置flag:questionable_pid_file=true,以供后续进一步检查,如下代码所示:

    而物理机中,r > 0,因此未进入当前流程,而是认为允许,可以继续后续流程。

    而当前docker中返回0,同时判断变量questionable_pid_file=true,随即打印日志并退出。
//进一步检查当前pid是否符合预期。
r = service_is_suitable_main_pid(s, pid, prio);
if (r == 0) {
if (questionable_pid_file) {
log_unit_error(UNIT(s)->id, "Refusing to accept PID outside of service control group, acquired through unsafe symlink chain: %s", s->pid_file);
return -EPERM;
}
}

函数static int service_is_suitable_main_pid(Service *s, pid_t pid, int prio)的主要作用为:

1.基本容错判断(略);

2.通过本service获取到manager,根据pid获取Unit owner。

3.判断owner是否与UNIT(s)相同。若相同,则返回1(物理机流程)。否则,返回0(当前docker容器流程)。

static int service_is_suitable_main_pid(Service *s, pid_t pid, int prio) {
Unit *owner;
/* Checks whether the specified PID is suitable as main PID for this service. returns negative if not, 0 if the
* PID is questionnable but should be accepted if the source of configuration is trusted. > 0 if the PID is
* good */
owner = manager_get_unit_by_pid(UNIT(s)->manager, pid);
if (owner == UNIT(s)) {
log_unit_debug(UNIT(s)->id, "New main PID "PID_FMT" belongs to service, we are happy.", pid);
return 1; /* Yay, it's definitely a good PID */
}
return 0; /* Hmm it's a suspicious PID, let's accept it if configuration source is trusted */
}

通过pid获取unit:

Unit *manager_get_unit_by_pid(Manager *m, pid_t pid) {
_cleanup_free_ char *cgroup = NULL;
int r;
//通过name:systemd和pid获取cgroup
r = cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, pid, &cgroup);
if (r < 0) {
return NULL;
}
//根据cgroup,获取unit:
return manager_get_unit_by_cgroup(m, cgroup);
}

此时docker里获取到的cgroup为/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice/nginx.service

根据cgroup获取unit:

Unit* manager_get_unit_by_cgroup(Manager *m, const char *cgroup) {
char *p;
Unit *u;
//1
u = hashmap_get(m->cgroup_unit, cgroup);
if (u){
return u;
}
p = strdupa(cgroup);
for (;;) {
char *e;
// find the position where '/' last appears, set it to e:
e = strrchr(p, '/');
if (e == p || !e){
return NULL;
}
// set *e to 0, so p can be stripped by position e:
*e = 0;
//2
u = hashmap_get(m->cgroup_unit, p);
if (u){
return u;
}
}
}

此时未从1中直接获取到。而是对传入的cgroup进行切割,获取不同层级的新cgroup名称传给p,然后在m->cgroup_unit中查询。

例如:

/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice/

/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/

/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/

/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d

最终通过如下cgroup切割结果,可以从manager->cgroup_unit哈希表中找到unit,且该unit的id为'-.slice',即根层:p:'/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d', u->id:'-.slice', u->instance:'(null)'

而该函数返回后,在判断通过cgroup获取到的owner,与通过UNIT(s)获取到的unit时,二者不同,因此未返回1:

static int service_is_suitable_main_pid(Service *s, pid_t pid, int prio) {
if (owner == UNIT(s)) {
log_unit_debug(UNIT(s)->id, "New main PID "PID_FMT" belongs to service, we are happy.", pid);
return 1; /* Yay, it's definitely a good PID */
}
}

owner.id:'-.slice', UNIT(s).id:'nginx.service'

为何通过pid->cgroup->unit会与UNIT(s)获取到的不同?

UNIT(s)的定义为,根据service的meta指针获取unit:

/* For casting the various unit types into a unit */
#define UNIT(u) (&(u)->meta)

该值的初始化:

static void service_init(Unit *u) {
Service *s = SERVICE(u); assert(u);
assert(u->load_state == UNIT_STUB); s->timeout_start_usec = u->manager->default_timeout_start_usec;
s->timeout_stop_usec = u->manager->default_timeout_stop_usec;
s->restart_usec = u->manager->default_restart_usec;
s->type = _SERVICE_TYPE_INVALID;
s->socket_fd = -1;
s->bus_endpoint_fd = -1;
s->guess_main_pid = true; RATELIMIT_INIT(s->start_limit, u->manager->default_start_limit_interval, u->manager->default_start_limit_burst); s->control_command_id = _SERVICE_EXEC_COMMAND_INVALID;
}

service_is_suitable_main_pid()函数中:

service->meta->cgroup_path:为/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice/nginx.service:

在/proc/<PID/cgroup中,获取定义:

int cg_pid_get_path(const char controller, pid_t pid, char *path)
{ //在/proc/<PID>/cgroup
fs = procfs_file_alloca(pid, "cgroup");

得到的cgroup为:“/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/s

ystem.slice/nginx.service”

注意此时manager->cgroup_root为"/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d"

从右向左,截取cgroup路径:

/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice/nginx.service
/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice
/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d
/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/docker
/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d

获取到的 id = 0x55910789b670 "-.slice",

p owner,可以看到id为"-.slice", description为"Root Slice",cgroup_path为: cgroup_path = 0x5591078b40d0 "/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d",

添加unit到manager->cgroup_unit时机

service_spawn
unit_realize_cgroup
unit_realize_cgroup_now
unit_create_cgroups

此时给manager->cgroup_unit添加的值为

cgroup_path: "/docker/9fc2f4125a5a54bdc029dc4c4a9a73f6524c9aee41a09b56d6b4cbfd28b3179d/system.slice/nginx.service"

是正确的值。

这一点也可以在/sys/fs/cgroup/systemd/docker/xxx/system.slice中得到印证:

也就是,通过service定义的unit还是正确的,但是在通过/proc//cgroup获取unit时,由于获取到的cgroup地址不正确(如:/docker/xx/docker/xx/systemd.slice/nginx,重复了两遍),且systemd代码中将其截取查询,最终以/docker/xxx为key,在manager->cgroup_unit中找到了root slice,即-.slice。

因此问题的根因在于:csp-nginx被拉起时,生成的/proc//cgroup是何处定义的,为什么不对?

《TODO》

cgroup与systemd: 通过src rpm获取systemd源代码,添加日志并使用rpmbuild重新打包的更多相关文章

  1. SLES 12 SP2 安装src.rpm软件包

      系统型号: SUSE Enterprise  mv systemd-228-117.12.src.rpm   systemd     cd systemd 执行下面的命令解压:     rpm2c ...

  2. systemd 和 如何修改和创建一个 systemd service (Understanding and administering systemd)

    系统中经常会使用到 systemctl 去管理systemd程序,刚刚看了一篇关于 systemd 和 SysV 相关的文章,这里简要记录一下: systemd定义: (英文来解释更为原汁原味) sy ...

  3. 一次性安装src.rpm编译所依赖的软件包

    yum-builddep SRPMS/fcitx-4.2.8.4-4.1.cgdl21.src.rpm NAME       yum-builddep - install missing depend ...

  4. 从.src.rpm包中提取出完整的源码的方法

    1 什么是完整的源码 就是说,最初始的源码加上打了所有的patch后的源码,即最新的源码. 2 过程 2.1 从.src.rpm中提取完整的rpm工程文件 2.1.1 rpm to cpio rpm2 ...

  5. 安装.src.rpm

    .src.rpm在坟墓镜像中能找到,例如6.8 os 的rpm包的.src.rpm格式就存放在http://vault.centos.org/6.8/os/Source/ .src.rpm是源码包,是 ...

  6. 活用RPM获取包的信息

    rpm -q 功效大 如果你想要在系统上安装.卸载或是升级软件,需要对系统软件进行查询:或是有如下的场景: 安装了一个软件,需要知道这个软件的版本. 遇到一个文件,不认识它,需要知道它是什么软件,有什 ...

  7. src.rpm包的解压

    有时候,我们在找源码包时候,发现有src.rpm的包:而不是tar.gz/tgz/zip结尾的. 那么如何去看这个src.rpm里面的详细信息呢? 看完下面这个例子,基本上明白了. 1,首先,生成sp ...

  8. src.rpm格式的RHCS源码提取

    RHCS源码下载(地址1:地址2) 参考文档(RHCS安装也配置) RHCS源码提取(参考) 方法一: (1)rpm –ivh magma-plugins-1.0.15-3.src.rpm   执行r ...

  9. [转] Linux 安装.src.rpm源码包的方法

    方法一:以setarch-1.3-1.src.rpm 软件包为例(可以到CSDN http://download.csdn.net/source/215173#acomment下载) 假设该文件已经存 ...

  10. src.rpm包安装方法

    有些软件包是以.src.rpm结尾的,这类软件包是包含了源代码的rpm包,在安装时需要进行编译.这类软件包有多种安装方法,以redhat为例说明如下: 注意: 如果没有rpmbuild可以从系统安装光 ...

随机推荐

  1. 记ios的input框获取焦点之后界面放大问题

    在移动端开发项目中,发现页面在使用 iPhone 访问的时候,点击 input 和 textarea 等文本输入框聚焦 focus() 时,页面会整体放大,而且失去焦点之后页面不能返回原来的样子.检查 ...

  2. Dapper.SimpleCRUD:Dapper的CRUD助手

    我们在项目开发中,面对一些高并发.大数据量等业务场景,往往对SQL语句的性能要求比较高,这个时候为了方便灵活控制,我们一般就会编写原生的SQL. Dapper就是一个非常高性能的轻量级ORM框架,Da ...

  3. Java调用与发布Webservice接口(一)

    ​ 一  准备工作 (一)开发环境 demo以springboot为基础框架,使用到了httpclient.hutool等依赖,详情如下:         springboot版本: org.spri ...

  4. Linux sudo 提权之软链接攻击

    软链接提权的原理 低权限用户能够以 root 用户的权限执行某个脚本,该脚本中又使用到了诸如 chown 等命令修改文件的权限,且该文件又能够被低权限的用户所修改.因此低权限的用户可以删除该文件,然后 ...

  5. fatal: repository 'http:/xxxx/root/x.git/' not found

    我遇到的问题其实就是gitlab旧服务器迁移到新服务器之后用户的没权限了没有权限操作了 所以抛出该异常 解决办法是赋予新的权限

  6. Qt编写视频播放器(支持pbonon/qmediaplayer/ffmpeg/vlc/mpv等多种内核)

    一.前言 花了一年多的时间,终于把这个超级播放器做成了自己想要的架构,用户的需求是一方面,自己架构方面的提升也是一方面,最主要是将界面和解码解耦了,这样才能动态的挂载不同的解码内核到不同的视频监控窗体 ...

  7. Qt编写安防视频监控系统57-子模块1设备列表

    一.前言 近期在经历过这次UI大重构以后,很多拆分的功能都以单独的模块的形式出现,以悬停窗体的形式嵌入或者悬浮在主窗体中,这种方式极大的增强了系统的拓展性,客户想要什么模块就开启什么模块,放置到合适的 ...

  8. Linux服务器环境安装mysql

    背景 1.安装环境:kvm虚拟机 2.运行环境:linux 3.架构:x86 4.安装mysql版本:mysql-5.7 1.安装准备 # Mysql官网 https://downloads.mysq ...

  9. 通过WebView2获取HTTP-only cookie

    通过WebView2获取HTTP-only cookie可以使用`WebView2.CookieManager`类的方法.以下是一个示例代码,演示如何获取HTTP-only cookie: using ...

  10. 引发类型为“System.Windows.Forms.AxHost+InvalidActiveXStateException”的异常 解决办法

    出现题目的异常,多是引用第三方控件引起的. 在NEW时,需要初始化该对象. AxESACTIVEXLib.AxESActiveX ax = new AxESACTIVEXLib.AxESActiveX ...