record-9.调度

技术栈:

1、中断均衡irqbalance

功能:使用irqbalance服务可以在很大程度上均衡各个CPU核上的中断负载,达到充分利用多核资源以提升系统整体性能的效果。

约束:中断均衡服务是通过改变中断的CPU亲和性来重新分布中断压力的,而某些中断的CPU亲和性不能或者没有必要修改,因此服务在生效时有一定的局限性,下述几类中断不会参与中断均衡:

  • 标识为IRQD_NO_BALANCING的中断不能进行中断均衡;
  • 标识为IRQD_PER_CPU的中断不能进行中断均衡;
  • 特殊中断(/proc/interrupts中断号包含非数字的中断)不能进行中断均衡。

2、抢占

(1)抢占的核心操作包括 2 个步骤:

1、从用户态陷入到内核态 (trap kernel),3 个路径:

a. 系统调用,本质是 soft interrupt,通常就是一条硬件指令 (x86 的 int 0x80)。

b. 硬件中断,最典型的就是会周期性发生的 timer 中断,或者其他各种外设中断.

c. exception,例如 page fault、div 0。

record-9.调度_第1张图片

2、陷入到内核态后,在合适的时机下,调用 scheduler 选出一个最重要的进程,如果被选中的不是当前正在运行的进程的话,就会执行 context switch 切换到新的进程。

(2)抢占类型

根据抢占时机点的不同,抢占分为 2 种类型

用户抢占

当内核即将返回用户空间时,内核会检查need_resched是否设置,如果设置,则调用schedule(),此时,发生用户抢占。一般来说,用户抢占发生几下情况:
(1) 从系统调用返回用户空间
(2) 从中断(异常)处理程序返回用户空间

内核抢占

1、当进程处于内核态时,是不能被抢占的

2、运行于内核态的进程可以主动放弃CPU

3、为了支持内核抢占,内核引入了preempt_count字段,该计数初始值为0,每当使用锁时加1,释放锁时减1。

当preempt_count为0时,表示内核可以被安全的抢占,大于0时,则禁止内核抢占。

#define preempt_disable()	uatomic_inc(&preempt_count)

record-9.调度_第2张图片

进程 A 已经通过系统调用进入内核,也许是对设备或文件的 write() 调用。

内核代表进程 A 执行时,具有更高优先级的进程 B 被中断唤醒。

内核抢占进程 A 并将 CPU 分配给进程 B,即使进程 A 既没有阻塞也没有完成其在内核里的工作。

内核抢占时机:

用户抢占:

  • 系统调用和中断返回用户空间时
// arch/arm64/kernel/entry.S
ret_to_user() // 返回到用户空间
    work_pending()
        do_notify_resume()
            schedule() 

// arch/arm64/kernel/signal.c
asmlinkage void do_notify_resume(struct pt_regs *regs,
    unsigned long thread_flags)
{
 do {
      [...]
        // 检查是否要需要调度/抢占
      if (thread_flags & _TIF_NEED_RESCHED) {
           local_daif_restore(DAIF_PROCCTX_NOIRQ);
           schedule();
      } else {
           [...]
 } while (thread_flags & _TIF_WORK_MASK);
}

内核抢占:

  • 中断返回内核空间
// arch/arm64/kernel/entry.S
el1_irq
    irq_handler
    arm64_preempt_schedule_irq
        preempt_schedule_irq
            __schedule(true) 

// kernel/sched/core.c
/* This is the entry point to schedule() from kernel preemption */
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
 [...]
 do {
      preempt_disable();
      local_irq_enable();
      __schedule(true);
      local_irq_disable();
      sched_preempt_enable_no_resched();
 } while (need_resched());

 exception_exit(prev_state);
}
  • 当内核代码再次变得可抢占时,如spinlock解锁时
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
     spin_release(&lock->dep_map, 1, _RET_IP_);
     do_raw_spin_unlock(lock);
     preempt_enable();  // 使能抢占时,如果需要,就会执行抢占
}

// include/linux/preempt.h
#define preempt_enable() \
do { \
     barrier(); \
     if (unlikely(preempt_count_dec_and_test())) \
          __preempt_schedule(); \
} while (0)
  • 如果内核中的任务显式调用schedule()

内核里有大量的地方会显式地要求进行调度,最常见的是:cond_resched() 和 sleep()类函数,它们最终都会调用到 __schedule()。

cond_resched
    _cond_resched
        preempt_schedule_common
            __schedule(true);
  • 如果内核中的任务阻塞(这导致对schedule()的调用)

例如 mutex,sem,waitqueue 获取不到资源,或者是等待 IO。这种情况下进程会将自己的状态从 TASK_RUNNING 修改为 TASK_UNINTERRUPTIBLE,然后调用 schedule() 主动让出 CPU 并等待唤醒。

static void wb_wait_for_completion(struct backing_dev_info *bdi,
				   struct wb_completion *done)
{
	atomic_dec(&done->cnt);		/* put down the initial count */
	wait_event(bdi->wb_waitq, !atomic_read(&done->cnt));
}

#define wait_event(wq_head, condition)						\
do {										\
	might_sleep();								\
	if (condition)								\
		break;								\
	__wait_event(wq_head, condition);					\
} while (0)

#define __wait_event(wq_head, condition)					\
	(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0,	\
			    schedule())
为什么要引入内核抢占

根本原因:在系统延迟和吞吐量之间进行权衡。

并不是说内核抢占就是绝对的好,使用什么抢占机制最优是跟你的应用场景挂钩的。如果不是为了满足用户,内核其实是完全不想进行进程切换的,因为每一次 context switch,都会有开销,这些开销就是对 cpu 的浪费,意味着吞吐量的下降

如果你想要系统的响应性好一点,就得尽量多的允许抢占的发生,这是 Linux 作为一个通用操作系统所必须支持的。当你的系统做到随时都可以发生抢占时,系统的响应性就会非常好。

CONFIG_PREEMPT_NONE=y:不允许内核抢占,吞吐量最大的 Model,一般用于 Server 系统。
CONFIG_PREEMPT_VOLUNTARY=y:在一些耗时较长的内核代码中主动调用cond_resched()让出CPU,对吞吐量有轻微影响,但是系统响应会稍微快一些。
CONFIG_PREEMPT=y:除了处于持有 spinlock 时的 critical section,其他时候都允许内核抢占,响应速度进一步提升,吞吐量进一步下降,一般用于 Desktop Embedded 系统。

(3)抢占发生条件

抢占发生需要同时满足:需要抢占和能抢占;

1、是否需要抢占?

判断是否需要抢占的依据是:thread_info 的成员 flags 是否设置了 TIF_NEED_RESCHED 标志位。

struct thread_info {
	struct pcb_struct	pcb;		/* palcode state */

	struct task_struct	*task;		/* main task structure */
	unsigned int		flags;		/* low level flags */
	...
}

相关的 API:

  • set_tsk_need_resched() 用于设置该 flag。
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
	set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
  • tif_need_resched() 被用来判断该 flag 是否置位。
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
  • resched_curr(struct rq *rq),标记当前 runqueue 需要抢占。

2、是否能抢占?

抢占发生的前提是要确保此次抢占是安全的 (preempt-safe)。什么才是 preempt-safe:不产生 race condition deadlock

值得注意的是,只有 kernel preemption 才有被禁止的可能,而 user preemption 总是被允许,因此这时马上就要返回 user space 了,肯定是处于一个可抢占的状态了。

引入内核抢占机制的同时引入了为 thread_info 添加了新的成员:preempt_count ,用来保证抢占的安全性,获取锁时会增加 preempt_count,释放锁时则会减少。抢占前会检查 preempt_count 是否为 0,为 0 才允许抢占。

相关的 API:

  • preempt_enable(),使能内核抢占,可嵌套调用。

  • preempt_disable(),关闭内核抢占,可嵌套调用。

#define preempt_disable()	uatomic_inc(&preempt_count)
#define preempt_enable()	uatomic_dec(&preempt_count)
  • preempt_count(),返回 preempt_count。

(4)什么场景会设置需要抢占 (TIF_NEED_RESCHED = 1)

大部分场景都是与resched_curr有关

1、周期性的时钟中断

时钟中断处理函数会调用 scheduler_tick(),它通过调度类(scheduling class) 的 task_tick 方法 检查进程的时间片是否耗尽,如果耗尽则标记需要抢占

scheduler_tick
	curr->sched_class->task_tick
	
//Linux 的调度策略被封装成调度类,例如 CFS、Real-Time
//CFS 调度类的 task_tick() 如下
const struct sched_class fair_sched_class = {
    ...
    .task_tick		= task_tick_fair,
    ...
}
task_tick_fair
	entity_tick
    	resched_curr(rq_of(cfs_rq));
			set_tsk_need_resched(curr);
				set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);

2、唤醒进程

当进程被唤醒的时候,如果优先级高于 CPU 上的当前进程,就会触发抢占。

相应的内核代码中,try_to_wake_up() 最终通过 check_preempt_curr() 检查是否标记需要抢占:

wake_up_process(struct task_struct *p)
	try_to_wake_up(p, TASK_NORMAL, 0);
		ttwu_queue
            ttwu_do_activate
            	ttwu_do_wakeup
            		check_preempt_curr

参数 “p” 指向被唤醒进程,“rq” 代表抢占的 CPU。

如果 p 的调度类和 rq 当前的调度类相同,则调用 rq 当前的调度类的 check_preempt_curr() (例如 cfs 的 check_preempt_wakeup()) 来判断是否要标记需要抢占。

如果 p 的调度类 > rq 当前的调度类,则用 resched_curr() 标记需要抢占,反之,则不标记。

3、新进程创建

如果新进程的优先级高于 CPU 上的当前进程,会需要触发抢占。相应的代码是 sched_fork(),它再通过调度类的 task_fork() 标记需要抢占

_do_fork
	copy_process
		sched_fork
    		p->sched_class->task_fork(p);

const struct sched_class fair_sched_class = {
    ...
    .task_tick		= task_tick_fair,
    ...
}

task_tick_fair
	entity_tick
    	resched_curr

4、进程修改nice值

如果修改进程 nice 值导致优先级高于 CPU 上的当前进程,也要标记需要抢占

void set_user_nice(struct task_struct *p, long nice)
{
    [...]
    // If the task increased its priority or is running and lowered its priority, then reschedule its CPU
    if (delta < 0 || (delta > 0 && task_running(rq, p)))
         resched_curr(rq);
}

(5)什么场景下要禁止内核抢占 (preempt_count > 0)

1、访问 Per-CPU data structures 的时候

struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* task is preempted here... */
something = tux[smp_processor_id()];

如果抢占发生在注释所在的那一行,当进程再次被调度时,smp_processor_id() 值可能已经发生变化了,这种场景下需要通过禁止内核抢占来做到 preempt safe

由于数据是per-cpu的,它可能没有显式SMP锁。

其次,当被抢占的任务最终被重新调度时,smp_processor_id的先前值可能不等于当前值。您必须通过禁用周围的抢占来保护这些情况。

您还可以使用put_cpu() 和get_cpu(),这将禁用抢占。

#define get_cpu()		({ preempt_disable(); smp_processor_id(); })
#define put_cpu()		preempt_enable()

2、访问cpu state

正在操作 CPU 相关的寄存器以进行 context switch 时,肯定是不能再允许抢占

asmlinkage __visible void __sched schedule(void)
{
	struct task_struct *tsk = current;

	sched_submit_work(tsk);
	do {
		preempt_disable();//调度前禁止内核抢占
		__schedule(false);
		sched_preempt_enable_no_resched();
	} while (need_resched());
}
EXPORT_SYMBOL(schedule);

3、持有 spinlock 的时候

支持内核抢占,这意味着进程有可能与被抢占的进程在相同的 critical section 中运行。为防止这种情况,当持有自旋锁时,要禁止内核抢占。

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
     preempt_disable();
     spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
     LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

内核不能被抢占的情况如下:

1)内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。

2)内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。

3)内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占。

4)内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。

5)内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占

可中断不可抢占

内核是不可抢占的,一旦切进内核态,只要代码不是主动释放CPU它就可以一直占着CPU。

虽不可抢占,但若此时发生中断,代码还是要交出CPU,但是中断返回之后,代码又能霸占CPU了,此为可中断但不可抢占

3、中断

1、类别

同步中断:是由CPU控制单元产生的,“同步”是指只有在一条指令执行完毕后,CPU才会发出中断,而不是发生在代码指令执行期间,比如系统调用

异步中断:是由其他硬件设备依照CPU时钟信号产生的,即意味着中断能够在指令之间发生,例如键盘中断

2、中断过程

内核态能够触发的唯一异常就是缺页异常,其他的都是用户态触发的。

名称 上下文 特点 概述
softirq 中断上下文 可中断不可睡眠 速度最快。同一个Softirq可能会同时运行在多个核上,必须非常小心的处理数据同步
tasklet 中断上下文 可中断不可睡眠 基于Softirq实现,同一类的Tasklet不会被同时运行,编程代价小
Working quere 进程上下文 可中断可睡眠 基于内核线程实现

4、内核锁

原子操作

atomic_t数据类型

原子操作比普通操作效率要低,因此必要时才使用,且不能与普通操作混合使用

如果是单核处理器,则原子操作与普通操作相同

自旋锁

spinlock_t数据类型,spin_lock(&lock)和spin_unlock(&lock)是加锁和解锁
等待解锁的进程将反复检查锁是否释放,而不会进入睡眠状态(忙等待),所以常用于短期保护某段代码

同时,持有自旋锁的进程也不允许睡眠,不然会造成死锁——因为睡眠可能造成持有锁的进程被重新调度,而再次申请自己已持有的锁
如果是单核处理器,则自旋锁定义为空操作,因为简单的关闭中断即可实现互斥

信号量与互斥量

struct semaphore数据类型,down(struct semaphore * sem)和up(struct semaphore * sem)是占用和释放
struct mutex数据类型,mutex_lock(struct mutex *lock)和mutex_unlock(struct mutex *lock)是加锁和解锁
竞争信号量与互斥量时需要进行进程睡眠和唤醒,代价较高,所以不适于短期代码保护,适用于保护较长的临界区

(1)mutex

mutex是一种保证串行化的睡眠锁机制,和spinlock语义类似,都是允许一个执行线程进入临界区;

不同的是当无法获得锁的时候,spinlock原地自旋,而mutex则是选择挂起当前线程,进入阻塞状态

使用规则:
  • 只有mutex的owner可以才可以释放锁
  • 不可以多次释放同一把锁
  • 不允许重复获取同一把锁,否则会死锁
  • 必须使用mutex初始化API来完成锁的初始化,不能使用类似memset或者memcpy之类的函数进行mutex初始化
  • 不可以多次重复对mutex锁进行初始化
  • 线程退出后必须释放自己持有的所有mutex锁
工作原理:

record-9.调度_第3张图片

传统的mutex只需要一个状态标记和一个等待队列;

等待队列中是一个个阻塞的线程,thread owner当前持有mutex,当它离开临界区释放锁的时候,会唤醒等待队列中第一个线程(top waiter),这时候top waiter会去竞争持锁,如果成功,那么从等待队列中摘下,成为owner。

如果失败,继续保持阻塞状态,等待owner释放锁的时候唤醒它。

在owner task持锁过程中,如果有新的任务来竞争mutex,那么就会进入阻塞状态并插入等待队列的尾部

struct mutex {
	atomic_long_t		owner;
	spinlock_t		wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
	struct list_head	wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
	void			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
};

linux内核进行了一些乐观自旋的优化,也就是说当线程持锁失败的时候,可以选择在mutex状态标记上自旋,等待owner释放锁,也可以选择进入阻塞状态并挂入等待队列。具体如何选择是在自旋等待的时间开销和进程上下文切换的开销之间进行平衡

(2)spinlock

背景:

共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了(中断上下文不能睡眠),这时候,可以考虑使用spin lock

工作原理:

1、spin lock是一种死等的机制,当前的执行thread会不断的重新尝试直到获取锁进入临界区;

2、只允许一个thread进入。semaphore可以允许多个thread进入,spinlock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试

3、执行时间短。

4、可以在中断上下文执行

内核抢占场景【解释了spinlock需要关抢占的原因】:

(1)进程A在某个系统调用过程中访问了共享资源 R

(2)进程B在某个系统调用过程中也访问了共享资源 R

会不会造成冲突呢?假设在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生进程切换,B启动执行,并通过系统调用访问了R,如果没有锁保护,则会出现两个thread进入临界区,导致程序执行不正确。

**【重点】**我们加上spin lock看看如何:

A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么破?

linux的kernel很简单,在A进程获取spin lock的时候,禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。

【非per-cpu可以抢占】

如果A和B运行在不同的CPU上,那么情况会简单一些:A进程虽然持有spin lock而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并会很快释放spin lock,解除B进程的spin状态

服务器os是默认关抢占的,嵌入式系统才会开抢占;故以服务器os的角度来看,可以忽略此场景

中断上下文场景【解释了spinlock需要关中断的原因】:

(1)运行在CPU0上的进程A在某个系统调用过程中访问了共享资源 R

(2)运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源 R

(3)外设P的中断handler中也会访问共享资源 R

在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?

我们假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没有什么问题,执行在CPU1上的handler会稍微等待一会CPU0上的进程A,等它离开临界区就会释放spin lock的;

正常情况下,是不需要关中断的,中断处理程序中重入已获得自旋锁的代码段才会导致一直spin,代码问题不应该关中断。

**【重点】**但是,如果外设P的中断事件被调度到了CPU0上执行会怎么样?

CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock

悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态

这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。

为了解决这样的问题,linux kernel采用了这样的办法:如果涉及到中断上下文的访问,spin lock需要和禁止本 CPU 上的中断联合使用

(3)mutex与spinlock

1、mutex是sleep-waiting。 就是说当没有获得mutex时,会有上下文切换,将自己加到忙等待队列中,直到另外一个线程释放mutex并唤醒它,而这时CPU是空闲的,可以调度别的任务处理

2、自旋锁spin lock是busy-waiting。就是说当没有可用的锁时,就一直忙等待并不停的进行锁请求,直到得到这个锁为止。这个过程中cpu始终处于忙状态,不能做别的任务

参考链接:

1、Linux内核同步机制之(八):mutex

http://www.wowotech.net/kernel_synchronization/504.html

2、Linux内核同步机制之(四):spin lock

http://www.wowotech.net/kernel_synchronization/spinlock.html

3、Linux 内核同步(二):自旋锁(Spinlock) 【写的很不错!】

https://blog.csdn.net/zhoutaopower/article/details/86598839

定位方法:

1、trace

echo > /sys/kernel/debug/tracing/trace
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo 0 > /sys/kernel/debug/tracing/trace
echo 10240 > /sys/kernel/debug/tracing/buffer_size_kb
echo 1 > /sys/kernel/debug/tracing/events/sched/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on

sleep 10
echo 0 > /sys/kernel/debug/tracing/events/sched/enable
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace > trace_log

2、收集进程调度信息

echo 1 > /proc/sys/kernel/sched_schedstats
cat /proc/$pid/sched > proc_sched_​pid

3、火焰图

perf record -g -p pid sleep 10
mv perf.data perf_record.data
perf script -i perf_record.data &> perf.unfold_$pid

4、perf sched timehist

perf sched record – sleep 10
perf sched timehist -p=pid &> perf_sched_timehist_$pid

5、top查看%si

top -b -d 1 -n 10 >> top_log

6、ftrace

echo > /sys/kernel/debug/tracing/trace
echo irq_handler_exit > /sys/kernel/debug/tracing/set_event
echo irq_handler_entry >> /sys/kernel/debug/tracing/set_event
echo softirq_entry >> /sys/kernel/debug/tracing/set_event
echo softirq_exit >> /sys/kernel/debug/tracing/set_event
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace > irq_trace_log

案例记录:

1、numa距离差异导致cpu利用率不均(doing 陶勇)

背景:

在4.19 ARM(128核)上做了2个cgroup,一个分组是0-31核,一个是32-127核,但是业务跑起来后,发现64核会有个分水岭。32-63,64-127这两个段的分组CPU不均匀

分析过程:

嵌入式修改numa距离后发现可以让cpu利用率均衡

结论:

关闭numa balance后确实能够均匀

2、umount耗时长

背景:

现网频繁出现挂载了EVS的pod在删除时由于umount耗时长导致pod删除慢及EVS资源回收慢的问题。

进展:

1)分析4.18 kernel代码发现在卸载过程中回收inode, dentry, page cache时存在多个调度点;

2)构造压测条件:容器内创建300w文件,节点page cache冲到90GB左右时进行卸载。在压测条件下,umount进程的stack与现网相似。

3)在umount时抓取调度相关信息,查看trace文件发现每100ms的周期大概有10ms左右在跑umount进程(cpu占用率为10%),查看umount所处的cpu组下的cpu.cfs_quota_us参数配置为10000(单位为us,表示100ms内只有10ms在运行进程),与现象对应

规避方法:

cpu.cfs_quota_us参数配置为-1,表示cpu不受cgroup组限制

一句话结论:

环境上容器内cpu.cfs_quota_us参数配置为10000,使得umount进程的cpu利用率只有10%,最终导致umount耗时长

你可能感兴趣的:(linux,运维,服务器)