Анонс новых высокопроизводительных точек доступа семейства 3600.
Разработка высокопроизводительных серверных...
description
Transcript of Разработка высокопроизводительных серверных...
10/21/12
Разработка высокопроизводительных
серверных приложений для Linux/UNIX
Крижановский Александр[email protected]
10/21/12
Пример (front-end server)
10/21/12
Декомпозиция
● Функциональная декомпозиция
● Декомпозиция по данным
10/21/12
Декомпозиция по данным
Original:
for (int i = 0; i < 10; ++i)
foo(i);
Thread 1: Thread 2:
for (int i = 0; i < 5; ++i) for (int i = 5; i < 10; ++i)
foo(i); foo(i);
● Пример: рабочие потоки, “разгребающие” очередь задач
● Хорошо масштабируется с ростом CPU.
10/21/12
Функциональная декомпозиция
Original:
foo();
bar();
Thread 1: Thread 2:
boo(); bar();
● Пример: поток ввода, поток вывода, рабочий поток, поток реконфигурации и пр.
● Не масштабируется.
10/21/12
Процессы vs Потоки
● Один и тот же task_struct, один и тот же do_fork()
● Потоки разделяют память, а процессы – нет.
PostgreSQL: процессы, shared memory для buffer pool, блокировки тоже в shared memory
10/21/12
Потоки – много или мало?
● Современные планировщики ОС обрабатывают N kernel-thread'ов за O(1) или O(log(N)) время
● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => нужен пул тредов
● M потоков на один CPU => ухудшается cache hit(поток должен завершить текущую задачу и только потом перейти к следующей)
● Если нужен высокий параллелизм, то нужны coroutines; или events...
10/21/12
Hyper Threadingи когда много потоков хорошо
● Потоки делят один кэш
● Если cache hit rate <50%, то всегда выгоднее 2 и более потока на ядро
● Если у одного потока высокий hit rate, то добавление дополнительных потоков на ядро ухудшит производительность
● На одно ядро желательно не планировать потоки, исполняющие разный код с разными данными
10/21/12
Синхронизация
● pthread_mutex_lock() - только один поток владеет ресурсом
● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель или много читателей
● pthread_cond_wait() - ожидать наступления события
● pthread_spin_lock() - проверяет блокировку в цикле
● (первые 3 работают через futex(2))
10/21/12
Pthreads & futex(2)
static volatile int val = 0;
void lock() {
int c;
while ((c = __sync_fetch_and_add(&val, 1))
lll_futex_wait(&val, c + 1);
}
void unlock() {
val = 0;
lll_futex_wake(&val, 1);
}
10/21/12
pthread_cond_broadcast:гремящее стадо
● Атомарные операции и, тем более, системный вызов – дорогие операции
● Поэтому лучше использовать pthread_cond_signal()
10/21/12
Инвалидация кэшей на мьютексе
● Если поток не может захватить мьютекс, то он уходит в сон
● => на текущем процессоре будет запущен другой поток
● => этот поток “вымоет кэш”
● Когда мьютекс будет отпущен, то поток продолжит выполнение с “вымытым” кэшем или на другом процессоре
10/21/12
pthread_spin_lock (упрощенно)
static volatile int lock = 0;
void lock() {
while (!__sync_bool_compare_and_swap(&lock, 0, 1));
}
void unlock() {
lock = 0;
}
10/21/12
pthread_spin_lock (scheduled)
CPU0 CPU1
Thread0: lock() . . .
Thread0: some work Thread1: try lock() → loop
. . . Thread2: try lock() → loop // Thread0 preempted . . . (loop) . . .
Thread1: try lock() → loop Thread2: try lock() → loop
// 200% CPU usage
. . . // Thread2 preempted
. . . Thread0: unlock()
Ядро для этого использует preempt_disable()
10/21/12
pthread_rwlock
● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 раза, сложнее операция взятия лока)
● Снижает lock contention при превалировании числа читателей над писателями, но снижается производительность per-cpu
10/21/12
Lock contention
● Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной
● Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе
● Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий
● Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации
10/21/12
Lock upgrade
pthread_rwlock_rdlock(&lock);
if (v > shared) { pthread_rwlock_unlock(&lock); return; }
pthread_rwlock_unlock(&lock);
pthread_rwlock_wrlock(&lock);
if (v > shared) { pthread_rwlock_unlock(&lock); return; }
shared = v;
pthread_rwlock_unlock(&lock);
10/21/12
Иерархия кэшей
10/21/12
Типы кэшей
● Data cache (L1d, L2d)
● Instruction cache (L1i, L2i)
● TLB – значения преобразований виртуальных адресов страниц в физические (L1, L2)
● L3 часто бывает смешанного типа
10/21/12
getconf
# getconf LEVEL1_DCACHE_SIZE
65536
# getconf LEVEL1_DCACHE_LINESIZE
64
# grep -c processor /proc/cpuinfo
2
10/21/12
Когерентность кэшей
● Непротиворечивость кэшированных данных на многопроцессорных системах
● Обеспечивается протоколом MESI (Modified, Exclusive, Shared, Invalid)
● RFO (Request For Ownership) := M → I
– CPU1 пишет по адресу X: X → M– CPU2 пишет по адресу X:
– CPU1: X → I– CPU2: X → M
10/21/12
Пример: std::shared_ptr
● std::shared_ptr использует reference counter – целую переменную, разделяемую и модифицируемую всеми потоками
10/21/12
False sharing
● MESI оперирует cacheline'ами
● RFO довольно дорогая операция
● Если две различные переменные находятся в одном cacheline, то возникает RFO
10/21/12
Выравнивание
● Компилятор автоматически выравнивает элементы структур данных
● GCC имеет специальные атрибуты, управляющие выравниванием данных
10/21/12
Кэш
1. Структура для хранения и быстрого поиска данных по некоторому ключу
● общая структура для хранения и поиска (дерево или хэш)
● две структуры: для индекса и для алгоритма вытеснения (обычно связный список)
2. Алгоритм вытеснения данных
3. Flush-потоки или вытеснение при записи
10/21/12
Примеры хранилищ
● Состояния HTTP или IM сессий
● Данные о пользователях по ID или логинам
● Буфферный кэш СУБД
10/21/12
Цена доступа
● Основное узкое место: время обращения к памяти
● По мере роста структур, данные перестают помещаться в кэши (L1d, L2d, L3d, TLB L1d/L2d etc)
● Выход их TLB (~1024 страницы) может стоить до 4х обращений к памяти вместо одного
=> Основные критерии к структурам данных:
● малый объем вспомогательных данных
● пространственная локальность обращений
10/21/12
Page Table
10/21/12
Структуры данных & page table
a.0
c.0
a.1
c.1
Page table a b
c
Application Tree
10/21/12
Radix-tree
● Гарантированность времени доступа
● На практике использует больше всего памяти
Очень медленное: на тесте равномерного распределения IPv4 адресов почти в 2 раза проигрывает хэшу с простой хэш-функцией времени и 4 раза по памяти
(Linux VMM выбирает ключи для сохранения пространственной локальности)
10/21/12
{B,T}-tree
● Хорошая пространственная локальность● Как правило, время поиска можно считать
константным для RAM-only структур данных● Довольно дорогие вставки● На практике все равно медленнее хэшей
10/21/12
Бинарные деревья
● В целом, слишком много обращений к памяти
● Балнсировка может быть довольно дорогой
10/21/12
Хэш
● Как правило, обладает лучшим средним временем доступа
● На практике использует меньше всего памяти
● Хорошая пространственная локальность: 1 случайное обращение и линейное сканирование
● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от ключей и нагрузки)
● В некоторых случаях может обладать очень большим временем поиска
10/21/12
Хэш: оптимизация (1)
● Двойное хэширование
● Определение размера на старте, как процент от доступной памяти (без динамического рехэшинга)
● Просто повысить гранулярность блокировок
10/21/12
Хэш: гранулярность блокировок
10/21/12
Алгоритмы вытеснения
● LRU (Least Recently Used): наиболее простой (O(1)), fundamental locality
● Clok: алгоритм «второй попытки» без перемещений элементов в списке (лучше concurrency)
● LRFU: модификация LFU (Least Frequently Used), но работает за линейное время, cлишком долго “помнит” историю
● CAR (Clock with Adaptive Replacement)/CART (CAR with Temporal filtering): объединяет fundamental и advanced locality, устойчив к линейному сканированию
10/21/12
Хэш: простое вытеснение
10/21/12
Способ вытеснения
1. Flushing-thread:
● максимальная скорость записи и чтения
● пробуждается по таймеру или при нехватке памяти
2. Во время чтения или записи:
● чтение/запись медленее
● подходит для soft-realtime
● проще алгоритм
10/21/12
Lock-free структуры данных
● Обычно простые структуры данных (односвязные списки, переменные)
● Сущестуют сложные протоколы изменения деревьев и хэшей – достаточно дорогие из-за своей сложности
● Наиболее актуальны в условиях высокого lock contention на многопроцессорных системах
● Очень полезны в «горячих» структурах: очереди задач, аллокаторы и пр.
10/21/12
Очердь задач
● Обычно 1 производитель, N потребителей
● Реализует только операции push() и pop() (нет сканирований по списку)
● Классический (наивный) вариант: std::queue, защищенный mutex'ом
● Может быть реализована на атомарных операциях для снижения lock contention
10/21/12
Атомарные операции(Read-Modify-Write)
unsigned long a, b = 1;
b = __sync_fetch_and_add(&a, 1);
mov $0x1,%edx
lock xadd %rdx,(%rax)
10/21/12
Атомарные операции(Compare-And-Swap)
unsigned long val = 5;
__sync_val_compare_and_swap(&val, 5, 2);
mov $0x5,%eax
mov $0x2,%ecx
lock cmpxchg %rcx,(%rdx)
10/21/12
Атомарные операции(стоимость)
● Реализуются через протокол cache coherency (MESI)
● Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO
Используются в shared_ptr для reference counting
10/21/12
Lock-free очередь (список):push()
push (q: pointer to fifo, cl: pointer to cell):
Loop:
cl->next = q->tail
if CAS (&q->tail, cl->next, cl):
break
10/21/12
Lock-free очередь (список):pop()
pop (q: pointer to fifo):
Loop:
cl = q->head
next = cl->next
if CAS (&q->head, cl, next):
break
return cl
10/21/12
ABA problem
● Поток T1 читает значение A,
● T1 вытесняется, позволяя выполняться T2,
● T2 меняет значение A на B и обратно на A,
● T1 возобновляет работу, видит, что значение не изменилось, и продолжает…
10/21/12
Lock-free очередь: ABA
pop (q: pointer to fifo):
Loop:
cl = q->head
next = cl->next # cl = A, next = B
------->scheduled # pop(A), pop(B), push(A)
if CAS (&q->head, cl, next): # cl->head => B (вместо C)
break
return cl
10/21/12
Решение ABA
● Вводятся счетчики числа pop()'ов и push()'ей для всей структуры или отдельных элементов и атомарно сравниваются
● Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-64
10/21/12
Lock-free очередь на Ring-Buffer: (0)
10/21/12
Lock-free очередь на Ring-Buffer: (1)
● Избавляемся от мьютексаpush():
while (tail_ + Q_SIZE < head_)
sched_yield();
Thread 1 Thread 2
read tail_ read tail_
read head_ read head_
------->scheduled push an element
push an element
10/21/12
Lock-free очередь на Ring-Buffer: (2)
● Резервируем место для вставки (→ одно чтение)push():
unsigned long tmp_head = __sync_fetch_and_add(&head_, 1);
while (tail_ + Q_SIZE < tmp_head)
sched_yield();
ptr_array_[tmp_head & Q_MASK] = x;
10/21/12
Lock-free очередь на Ring-Buffer: (3)
● Per-thread current LH & LTpush(): volatile unsigned long last_head_;
volatile unsigned long last_tail_;
auto min = tail_;
for (size_t i = 0; i < n_consumers_; ++i) {
if (thr_p_[i].tail < min)
min = thr_p_[i].tail;
}
last_tail_ = min;
10/21/12
Lock-free очередь на Ring-Buffer: (4)
● Чтение tail i-го потока только один разpush(): auto min = tail_;
for (size_t i = 0; i < n_consumers_; ++i) {
auto tmp_t = thr_p_[i].tail;
if (tmp_t < min)
min = tmp_t;
}
last_tail_ = min;
10/21/12
Lock-free очередь на Ring-Buffer: (5)
● Не оставляем текущий tail i-го потока надолгоpop(): thr_pos().tail = __sync_fetch_and_add(&tail_, 1);
// ….........
T *ret = ptr_array_[thr_pos().tail & Q_MASK];
thr_pos().tail = ULONG_MAX;
return ret;
10/21/12
Lock-free очередь на Ring-Buffer: (6)
● Двойная запись текущего значения tail и синхронизация с инкрементом tail_
pop(): thr_pos().tail = tail_;
__sync_synchronize();
thr_pos().tail = __sync_fetch_and_add(&tail_, 1);
10/21/12
Lock-free очередь(список vs ring-buffer)
● Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков)
● Список должен быть интрузивным для избежания аллокации узлов на каждой вставке
● Для списка нужно отдельно реализовать контроль числа элементов
● Локализация и выравнивание памяти - ?
10/21/12
Lock-free очередь (ограничения)
● Работает только для очередей — сканировать такие структуры данных без блокировок нельзя
● Для очереди нужна реализация ожидания:
● usleep(1000) — помещение потока в wait queue на примерно один такт системного таймера
● sched_yield() - busy loop на перепланирование (100% CPU usage)
10/21/12
Zero copy
● Чем больше каждое из сообщений, обрабатываемых сервером, тем актуальнее
● Решается через zero copy IO, zero copy алгоритмы и структуры данных и архитектуру сервера в целом
● Недостатки: сильная связность между IO, аллокатором памяти, управлением потоками
10/21/12
Архитектура zero copy сервера
10/21/12
Zero-copy ввод-вывод
● Сетевой (минуя Socket API/kernel копирования)
● Дисковый (минуя буферы программы или буферный кэш ОС)
● Не “вымывает” кэши процессоров
● Имеет накладные расходы => актуально только при работе с большими блоками данных
10/21/12
Zero-copy Network (I)O
10/21/12
Zero-copy Network (I)O: vmsplice()/splice()
● Только на Output, на Input memcpy() в ядре
● Сетевой стек пишет данные напрямую со страницы => перед использованием страницы снова нужно записать 2 размера буфера отправки (double-buffer write)
10/21/12
Zero-copy Network (I)O: пример
void zcp_send(int sd, const struct iovec *iov, size_t num) {
pipe(pipe_);
size_t data_len = 0;
for (size_t i = 0; i < num; ++i)
data_len += iov[i].iov_len;
vmsplice(pipe_[1], iov, num, 0);
for (int r; data_len; data_len -= r)
r = splice(pipe_[0], NULL, sd, NULL, data_len, 0);
}
10/21/12
Splice: производительность
(Тесты splice() от Jens Axboe):
# ./nettest/xmit -s65536 -p1000000 127.0.0.1 5500
xmit: msg=64kb, packets=1000000 vmsplice() -> splice()
usr=9259, sys=6864, real=27973
# ./nettest/xmit -s65536 -p1000000 -n 127.0.0.1 5500
xmit: msg=64kb, packets=1000000 send()
usr=8762, sys=25497, real=34261