2018-09-29 виртуални машини, кеш, cgroup-и и други гадости

by Vasil Kolev

(английската версия е в блога на StorPool)

Това е един случай, който отне известно време да се дебъгне, включващ StorPool, виртуални машини, cgroup-и, OOM killer-а и кеширането. Части от него би трябвало да са полезни и на други хора/админи.

Започна с оплакване от клиента:

> Пак се случва, на тоя hypervisor виртуалните машини ги убива
> OOM killer-а. Това не се случва на hyervisor-и, на които няма
> cgroup-и, не може ли да не ги конфигурирате?

(
Смисълът изобщо да имаме cgroup-и е, че няма друг начин да се резервират памет и процесори за даден набор процеси. Понеже storage системата е една от най-чувствителните части на системата към латентности и подобни проблеми (или, нека сме по-точни, всичко останало е много чувствително към проблеми на storage системата), доста е полезно да е предпазена от всичко останало. По същия начин е полезно и да се разделят виртуалните машини и системните процеси, за да не го отнесе грешното нещо при някой memory leak или побъркан allocation. Също така, има много забавен вариант за memory deadlock (който го има с всяка storage система, която не е в kernel-а и с някои които са вътре, който може да бъде описан по следния начин:

Процес към kernel-а: искам памет
Kernel към процеса: ей-сега
Kernel (говори си сам): то искаш, ама няма. Нямам какво да освободя друго, но мога да flush-на някакъв dirty cache
Kernel към storage системата: на ти тия данни, запиши ги
Storage системата към kernel-а: разбира се, за теб си режа даже ноктите без упойка
Storage системата (говори си сама): тия данни не са aling-нати както трябва за гнусния хардуер отдолу, трябва да ги копирам малко и наместя
Storage системата към kernel-а: искам памет
)

Разбира се, цялото нещо нямаше да е чак такъв проблем, ако Linux-кия OOM killer и cgroup-ите работеха правилно, но версиите по всичките kernel-и, които срещаме (което значи CentOS[67], т.е. kernel с име 3.10.xxx и с diff спрямо оригинала, който вероятно е колкото 30% от кода) се държат странно и от време на време застрелват sshd вместо който трябва. Новите идеи за отношенията м/у OOM killer-а и cgroup-ите се очертава да ни стъжнят живота още повече.

Та, за да си резервира човек някакъв набор памет за набор от процеси на KVM hypervisor, трябва да създаде memory cgroup-а за системните неща (system.slice), виртуалните машини (machine.slice), може би user-ските неща (user.slice), и в нашия случай storpool.slice. После за всички тия групи сборът на лимитите трябва да е с около 1-2GB по-малък от общата памет (понеже някаква част си е за kernel-а и той никъде не я account-ва), и да се подсигури, че всички процеси са по тези cgroup-и или техни деца, и няма никой в root cgroup-ата. Това се постига с разни опции на libvirt, systemd и малко тел+тиксо, но като цяло върши работа.

Има обаче известен проблем с memory cgroup-ите, буфер кеша и OOM killer-а. Ако не ползвате cgroup-и и не ви стига паметта, kernel-ът по принцип flush-ва dirty page-овете (т.е. незаписаните данни) и clean cache (прочетени файлове от файловата система), та да си върне памет и да може да я даде на който я иска. В случая с cgroup-ите обаче clean cache не се почиства, и предпочитания за kernel-а начин е просто да пусне OOM killer-а, който да застреля някой полезен процес.

(За който бори такива проблеми, има доста полезна информация колко памет е account-ната за какво в “memory.stat” за всяка cgroup-а в /sys, например /sys/fs/cgroup/memory/machine.slice/memory.stat)

Ако си говорим принципно, в случая с виртуалните машини това не трябва да е проблем, понеже те няма причина да ползват кеш – вътре във виртуалката ще има какъвто и трябва, и няма смисъл да се хаби двойно памет (съответно всичките дискове се настройват с cache=none). Да не говорим, че не-спирането на кеша (който разбира се е пуснат по default, ама post за идиотските default-и в qemu/libvirt ще е бая) не позволява да се правят live миграции (libvirt-а отказва, щото можело да доведе до загуба на данни).

(Което всъщност е оправено в https://github.com/qemu/qemu/commit/dd577a26ff03b6829721b1ffbbf9e7c411b72378, но още не изглежда да е merge-нато, благодаря на колегите, че ми го посочиха)

Повечето оркестрационни системи в наши дни ползват “cache=none” в техните конфигурации (и интеграциите на StorPool с тях гледат да го настроят, ако има как), но в този конкретен случай системата имаше някакви много стари виртуалки, правени от стари template-и (някои от които ползваха IDE вместо virtio), и със съответния default. Правилното решение за тези виртуалки би било да се оправи template-а и да се рестартират, но по някаква причина хората не са щастливи да рестартират виртуалките на клиентите, и предполагам, че и клиентите не са големи фенове на идеята. Също така имаше някаква странна причина (която мозъкът ми е изтрил) да не може да се сменят конкретно тези template-и.

Не сме първите, които удрят проблема с “твърде много clean cache в паметта, който не ни трябва”. Ето какво ни хрумна и какво направихме в крайна сметка:

Първата идея, която ни хрумна беше периодично да почистваме buffer cache, с “echo 3 > /proc/sys/vm/drop_caches”. Това ще сработи, но като решение е доста тъпа брадва, понеже и ще изхвърли от кеша полезни неща (и системата ще си препрочита libc-то постоянно).

Втората идея се появи с това, че има нещо много хубаво, наречено LD_PRELOAD, с което в общи линии може да се прихване всякаква функция, която се вика от дадено binary и да се добави още нещо. По този начин може да се прихване open() и ако се открие, че е block device, да му се сложи флаг O_DIRECT (който в общи линии значи “опитай се да не ползваш buffer cache”). Проблемът на O_DIRECT е, че има някои неприятни ограничения, като например изискването паметта и offset-ите при писане/четене да са подравнени по някакъв начин, като 512 байта подравняване би трябвало да са ОК, ако не се ползва файлова система (където може да се наложи подравняване на page size или повече). Понеже няма как да знаем какво прави виртуалната машина отгоре, имаше шанс да се наложи да прихващаме всички read() и write() и да правим копие на данните в наша, подравнена памет, което щеше да е прилично количество писане и щеше да е трудно да няма грешки.

Сетихме се също така, че в kernel-а има интерфейс, наречен posix_fadvise(), който може да се използва да маркира някаква част от кеша като “няма да ми трябва повече” (които kernel-а да разкара). Това можеше да се използва с LD_PRELOAD за read()-ове, като просто се маркираше прочетеното като POSIX_FADV_DONTNEED. Идеята я имаше донякъде реализирана в https://code.google.com/archive/p/pagecache-mangagement/ и тръгнах да я дописвам да прави нещо по-просто (просто posix_fadvise() веднага след read, вместо сложни сметки с колко кеш да се позволява на процес).

Някъде в тоя момент CTO-то ни попита “а то всъщност трябва ли да се вика posix_fadvise() от процеса, или може от всякъде”? Оказа се, че в същото repo има прост инструмент, който изхвърля от кеша данните за даден файл (или блоково устройство), наречен “fadv” (който открих след като написах същите 5 реда).

Крайният резултат беше малък скрипт, който пуска “fadv” за всички наши устройства и ги държи извън кеша, което се каза приемлив workaround. Оказа се и доста бърз – на първото си стартиране му отне около минута да изхвърли около 100GB от кеша, на следващите си пускания минаваше за под секунда.

Tags:

Leave a Reply