Разработка высокопроизводительных серверных...

65
10/21/12 Разработка высокопроизводительных серверных приложений для Linux/UNIX Крижановский Александр [email protected]

description

 

Transcript of Разработка высокопроизводительных серверных...

Page 1: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Разработка высокопроизводительных

серверных приложений для Linux/UNIX

Крижановский Александр[email protected]

Page 2: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Пример (front-end server)

Page 3: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Декомпозиция

● Функциональная декомпозиция

● Декомпозиция по данным

Page 4: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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.

Page 5: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Функциональная декомпозиция

Original:

foo();

bar();

Thread 1: Thread 2:

boo(); bar();

● Пример: поток ввода, поток вывода, рабочий поток, поток реконфигурации и пр.

● Не масштабируется.

Page 6: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Процессы vs Потоки

● Один и тот же task_struct, один и тот же do_fork()

● Потоки разделяют память, а процессы – нет.

PostgreSQL: процессы, shared memory для buffer pool, блокировки тоже в shared memory

Page 7: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Потоки – много или мало?

● Современные планировщики ОС обрабатывают N kernel-thread'ов за O(1) или O(log(N)) время

● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => нужен пул тредов

● M потоков на один CPU => ухудшается cache hit(поток должен завершить текущую задачу и только потом перейти к следующей)

● Если нужен высокий параллелизм, то нужны coroutines; или events...

Page 8: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Hyper Threadingи когда много потоков хорошо

● Потоки делят один кэш

● Если cache hit rate <50%, то всегда выгоднее 2 и более потока на ядро

● Если у одного потока высокий hit rate, то добавление дополнительных потоков на ядро ухудшит производительность

● На одно ядро желательно не планировать потоки, исполняющие разный код с разными данными

Page 9: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Синхронизация

● pthread_mutex_lock() - только один поток владеет ресурсом

● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель или много читателей

● pthread_cond_wait() - ожидать наступления события

● pthread_spin_lock() - проверяет блокировку в цикле

● (первые 3 работают через futex(2))

Page 10: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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);

}

Page 11: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

pthread_cond_broadcast:гремящее стадо

● Атомарные операции и, тем более, системный вызов – дорогие операции

● Поэтому лучше использовать pthread_cond_signal()

Page 12: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Инвалидация кэшей на мьютексе

● Если поток не может захватить мьютекс, то он уходит в сон

● => на текущем процессоре будет запущен другой поток

● => этот поток “вымоет кэш”

● Когда мьютекс будет отпущен, то поток продолжит выполнение с “вымытым” кэшем или на другом процессоре

Page 13: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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;

}

Page 14: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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()

Page 15: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

pthread_rwlock

● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 раза, сложнее операция взятия лока)

● Снижает lock contention при превалировании числа читателей над писателями, но снижается производительность per-cpu

Page 16: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Lock contention

● Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной

● Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе

● Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий

● Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации

Page 17: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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);

Page 18: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Иерархия кэшей

Page 19: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Типы кэшей

● Data cache (L1d, L2d)

● Instruction cache (L1i, L2i)

● TLB – значения преобразований виртуальных адресов страниц в физические (L1, L2)

● L3 часто бывает смешанного типа

Page 20: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

getconf

# getconf LEVEL1_DCACHE_SIZE

65536

# getconf LEVEL1_DCACHE_LINESIZE

64

# grep -c processor /proc/cpuinfo

2

Page 21: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 22: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Пример: std::shared_ptr

● std::shared_ptr использует reference counter – целую переменную, разделяемую и модифицируемую всеми потоками

Page 23: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

False sharing

● MESI оперирует cacheline'ами

● RFO довольно дорогая операция

● Если две различные переменные находятся в одном cacheline, то возникает RFO

Page 24: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Выравнивание

● Компилятор автоматически выравнивает элементы структур данных

● GCC имеет специальные атрибуты, управляющие выравниванием данных

Page 25: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Кэш

1. Структура для хранения и быстрого поиска данных по некоторому ключу

● общая структура для хранения и поиска (дерево или хэш)

● две структуры: для индекса и для алгоритма вытеснения (обычно связный список)

2. Алгоритм вытеснения данных

3. Flush-потоки или вытеснение при записи

Page 26: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Примеры хранилищ

● Состояния HTTP или IM сессий

● Данные о пользователях по ID или логинам

● Буфферный кэш СУБД

Page 27: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Цена доступа

● Основное узкое место: время обращения к памяти

● По мере роста структур, данные перестают помещаться в кэши (L1d, L2d, L3d, TLB L1d/L2d etc)

● Выход их TLB (~1024 страницы) может стоить до 4х обращений к памяти вместо одного

=> Основные критерии к структурам данных:

● малый объем вспомогательных данных

● пространственная локальность обращений

Page 28: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Page Table

Page 29: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Структуры данных & page table

a.0

c.0

a.1

c.1

Page table a b

c

Application Tree

Page 30: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Radix-tree

● Гарантированность времени доступа

● На практике использует больше всего памяти

Очень медленное: на тесте равномерного распределения IPv4 адресов почти в 2 раза проигрывает хэшу с простой хэш-функцией времени и 4 раза по памяти

(Linux VMM выбирает ключи для сохранения пространственной локальности)

Page 31: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

{B,T}-tree

● Хорошая пространственная локальность● Как правило, время поиска можно считать

константным для RAM-only структур данных● Довольно дорогие вставки● На практике все равно медленнее хэшей

Page 32: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Бинарные деревья

● В целом, слишком много обращений к памяти

● Балнсировка может быть довольно дорогой

Page 33: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Хэш

● Как правило, обладает лучшим средним временем доступа

● На практике использует меньше всего памяти

● Хорошая пространственная локальность: 1 случайное обращение и линейное сканирование

● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от ключей и нагрузки)

● В некоторых случаях может обладать очень большим временем поиска

Page 34: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Хэш: оптимизация (1)

● Двойное хэширование

● Определение размера на старте, как процент от доступной памяти (без динамического рехэшинга)

● Просто повысить гранулярность блокировок

Page 35: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Хэш: гранулярность блокировок

Page 36: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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, устойчив к линейному сканированию

Page 37: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Хэш: простое вытеснение

Page 38: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Способ вытеснения

1. Flushing-thread:

● максимальная скорость записи и чтения

● пробуждается по таймеру или при нехватке памяти

2. Во время чтения или записи:

● чтение/запись медленее

● подходит для soft-realtime

● проще алгоритм

Page 39: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Lock-free структуры данных

● Обычно простые структуры данных (односвязные списки, переменные)

● Сущестуют сложные протоколы изменения деревьев и хэшей – достаточно дорогие из-за своей сложности

● Наиболее актуальны в условиях высокого lock contention на многопроцессорных системах

● Очень полезны в «горячих» структурах: очереди задач, аллокаторы и пр.

Page 40: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Очердь задач

● Обычно 1 производитель, N потребителей

● Реализует только операции push() и pop() (нет сканирований по списку)

● Классический (наивный) вариант: std::queue, защищенный mutex'ом

● Может быть реализована на атомарных операциях для снижения lock contention

Page 41: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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)

Page 42: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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)

Page 43: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Атомарные операции(стоимость)

● Реализуются через протокол cache coherency (MESI)

● Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO

Используются в shared_ptr для reference counting

Page 44: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 45: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 46: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

ABA problem

● Поток T1 читает значение A,

● T1 вытесняется, позволяя выполняться T2,

● T2 меняет значение A на B и обратно на A,

● T1 возобновляет работу, видит, что значение не изменилось, и продолжает…

Page 47: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 48: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Решение ABA

● Вводятся счетчики числа pop()'ов и push()'ей для всей структуры или отдельных элементов и атомарно сравниваются

● Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-64

Page 49: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Lock-free очередь на Ring-Buffer: (0)

Page 50: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 51: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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;

Page 52: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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;

Page 53: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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;

Page 54: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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;

Page 55: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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);

Page 56: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Lock-free очередь(список vs ring-buffer)

● Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков)

● Список должен быть интрузивным для избежания аллокации узлов на каждой вставке

● Для списка нужно отдельно реализовать контроль числа элементов

● Локализация и выравнивание памяти - ?

Page 57: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Lock-free очередь (ограничения)

● Работает только для очередей — сканировать такие структуры данных без блокировок нельзя

● Для очереди нужна реализация ожидания:

● usleep(1000) — помещение потока в wait queue на примерно один такт системного таймера

● sched_yield() - busy loop на перепланирование (100% CPU usage)

Page 58: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Zero copy

● Чем больше каждое из сообщений, обрабатываемых сервером, тем актуальнее

● Решается через zero copy IO, zero copy алгоритмы и структуры данных и архитектуру сервера в целом

● Недостатки: сильная связность между IO, аллокатором памяти, управлением потоками

Page 59: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Архитектура zero copy сервера

Page 60: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Zero-copy ввод-вывод

● Сетевой (минуя Socket API/kernel копирования)

● Дисковый (минуя буферы программы или буферный кэш ОС)

● Не “вымывает” кэши процессоров

● Имеет накладные расходы => актуально только при работе с большими блоками данных

Page 61: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Zero-copy Network (I)O

Page 62: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Zero-copy Network (I)O: vmsplice()/splice()

● Только на Output, на Input memcpy() в ядре

● Сетевой стек пишет данные напрямую со страницы => перед использованием страницы снова нужно записать 2 размера буфера отправки (double-buffer write)

Page 63: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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);

}

Page 64: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

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

Page 65: Разработка высокопроизводительных серверных приложений для Linux/Unix (Александр Крижановский)

10/21/12

Спасибо!

[email protected]