Oom killer, morderca procesów
Dzisiaj przeżyliśmy falę ataku oom killera. Ponieważ procesy padały jak muchy zainteresowałem się jego algorytmem - co powoduje, że właśnie dany proces zostanie ubity. Zainteresowałem to eufemizm - stwierdziłem, że działa on zupełnie chaotycznie i losowo ;) Acha, dla nie zaznajomionych z tematem - oom killer to feature ;) powodujący śmierć procesów w sytuacji kiedy brakuje pamięci. Zaciekawiony tematem sięgnąłem do źródeł kernela i zrozumiałem ;). Ale po kolei. Kiedy dochodzi do morderstwa w logach znajdujemy coś takiego:
Sep 13 13:12:48 xxxxx-yy kernel: [4279051.889887] Out of Memory: Kill process 18053 (python) score 103551 and children.
Kolega słusznie zauważył, że prawdopodobnie umiera proces z największym scorem. Algorytm liczenia punktacji znajduje się w pliku mm/oom_kill.c w katalogu ze źródłami kernela. Przeglądając go dowiadujemy się, że założenia oomkillera są następujące:
* The formula used is relatively simple and documented inline in the * function. The main rationale is that we want to select a good task * to kill when we run out of memory. * * Good in this context means that: * 1) we lose the minimum amount of work done * 2) we recover a large amount of memory * 3) we don't kill anything innocent of eating tons of memory * 4) we want to kill the minimum amount of processes (one) * 5) we try to kill the process the user expects us to kill, this * algorithm has been meticulously tuned to meet the principle * of least surprise ... (be careful when you change it)
Brzmi rozsądnie i obiecująco prawda? Zagłębiamy się dalej:
unsigned long badness(struct task_struct *p, unsigned long uptime)
{
unsigned long points, cpu_time, run_time, s;
Mamy już więc naszą funkcję wyliczającą, jak bardzo dany proces jest evil, oraz inicjalizację zmiennej points. Pora więc na kolejny krok:
/*
* The memory size of the process is the basis for the badness.
*/
points = mm->total_vm;
To jest w miarę zrozumiałe - ten kto żre dużo podpada do ubicia. Ale idziemy dalej ;):
/*
* Processes which fork a lot of child processes are likely
* a good choice. We add half the vmsize of the children if they
* have an own mm. This prevents forking servers to flood the
* machine with an endless amount of children. In case a single
* child is eating the vast majority of memory, adding only half
* to the parents will make the child our kill candidate of choice.
*/
list_for_each_entry(child, &p->children, sibling) {
task_lock(child);
if (child->mm != mm && child->mm)
points += child->mm->total_vm/2 + 1;
task_unlock(child);
}
Ha, forkujące się procesy - czujecie oddech zabójcy na karku? Następnie mamy:
/*
* CPU time is in tens of seconds and run time is in thousands
* of seconds. There is no particular reason for this other than
* that it turned out to work very well in practice.
*/
cpu_time = (cputime_to_jiffies(p->utime) + cputime_to_jiffies(p->stime))
>> (SHIFT_HZ + 3);
Zużycie cpu - słusznie.
/*
* Niced processes are most likely less important, so double
* their badness points.
*/
if (task_nice(p) > 0)
points *= 2;
Dobrze wiedzieć, że możemy nicem sugerować co ma wylecieć w pierwszej kolejności :)
/*
* Superuser processes are usually more important, so we make it
* less likely that we kill those.
*/
if (cap_t(p->cap_effective) & CAP_TO_MASK(CAP_SYS_ADMIN) ||
p->uid == 0 || p->euid == 0)
points /= 4;
Dadam! Odkąd pamiętam uczono nas aby nie odpalaćc procesów z konta roota! Wysoki dzielnik wskazuje, że odpalanie usług z konta superusera pomaga im przeżyć! Koniec z forkującym apachem na userze www-data ;)
/*
* We don't want to kill a process with direct hardware access.
* Not only could that mess up the hardware, but usually users
* tend to only have this flag set on applications they think
* of as important.
*/
if (cap_t(p->cap_effective) & CAP_TO_MASK(CAP_SYS_RAWIO))
points /= 4;
Jak trafnie kolega zauważył - nie ubije nam iksów ;) I na zakończenie:
/*
* If p's nodes don't overlap ours, it may still help to kill p
* because p may have allocated or otherwise mapped memory on
* this node before. However it will be less likely.
*/
if (!cpuset_excl_nodes_overlap(p))
points /= 8;
Nie wiem dokładnie co robi tak funkcja - ale po nazwie i opisie wnioskuję, że sprawdza czy node'y się nie pokrywają. Jeśli tak - no cóż. Pozostaje nam wyświetlić score:
printk(KERN_DEBUG "OOMkill: task %d (%s) got %d points\n",
p->pid, p->comm, points);
Tak więc - zwracam honor, nie jest losowy! ;( Najlepszym cytatem na zakończenie tego posta będzie:
Go to hell
Raise in hell
Your bastard's at the funeral
Join me at the funeral diner
Funeral diner

07:30:09
genialny wpis, nieźle się ubawiłem, chcoiaż myślałem, że nagle na końcu będzie zaskakująca puenta n.p.: liczy score, a później w ogóle się nim nie kieruje przy ubijaniu :)