Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Разработка        высокопроизводительных       серверных приложений для               Linux/UNIX           Крижановский Ал...
Пример (front-end server)10/21/12
Декомпозиция●   Функциональная декомпозиция●   Декомпозиция по данным10/21/12
Декомпозиция по данным                               Original:                       for (int i = 0; i < 10; ++i)         ...
Функциональная декомпозиция                              Original:                               foo();                   ...
Процессы vs Потоки●   Один и тот же task_struct, один и тот же do_fork()●   Потоки разделяют память, а процессы – нет.Post...
Потоки – много или мало?●   Современные планировщики ОС обрабатывают N kernel-threadов за    O(1) или O(log(N)) время●   K...
Hyper Threading           и когда много потоков хорошо●   Потоки делят один кэш●   Если cache hit rate <50%, то всегда выг...
Синхронизация●   pthread_mutex_lock() - только один поток владеет ресурсом●   pthread_rwlock_wrlock()/pthread_rwlock_rdloc...
Pthreads & futex(2)       static volatile int val = 0;       void lock() {              int c;              while ((c = __...
pthread_cond_broadcast:                  гремящее стадо●   Атомарные операции и, тем более, системный вызов – дорогие    о...
Инвалидация кэшей на мьютексе●   Если поток не может захватить мьютекс, то он уходит в сон●   => на текущем процессоре буд...
pthread_spin_lock (упрощенно)       static volatile int lock = 0;       void lock() {              while (!__sync_bool_com...
pthread_spin_lock (scheduled)               CPU0                                CPU1             Thread0: lock()          ...
pthread_rwlock●   “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 раза,    сложнее операция взятия лока)●   С...
Lock contention●   Один процесс удерживает лок, другие процессы ждут —    система становится однопроцессной●   Актуален с ...
Lock upgrade       pthread_rwlock_rdlock(&lock);       if (v > shared) { pthread_rwlock_unlock(&lock); return; }       pth...
Иерархия кэшей10/21/12
Типы кэшей●   Data cache (L1d, L2d)●   Instruction cache (L1i, L2i)●   TLB – значения преобразований виртуальных адресов с...
getconf       # getconf LEVEL1_DCACHE_SIZE       65536       # getconf LEVEL1_DCACHE_LINESIZE       64       # grep -c pro...
Когерентность кэшей●   Непротиворечивость кэшированных данных на многопроцессорных    системах●   Обеспечивается протоколо...
Пример: std::shared_ptr●   std::shared_ptr использует reference counter – целую переменную,    разделяемую и модифицируему...
False sharing●   MESI оперирует cachelineами●   RFO довольно дорогая операция●   Если две различные переменные находятся в...
Выравнивание●   Компилятор автоматически выравнивает элементы структур данных●   GCC имеет специальные атрибуты, управляющ...
Кэш1. Структура для хранения и быстрого поиска данных по некоторому  ключу   ●   общая структура для хранения и поиска (де...
Примеры хранилищ●   Состояния HTTP или IM сессий●   Данные о пользователях по ID или логинам●   Буфферный кэш СУБД10/21/12
Цена доступа●   Основное узкое место: время обращения к памяти●   По мере роста структур, данные перестают помещаться в кэ...
Page Table10/21/12
Структуры данных & page table                             Application Tree    Page table                       a.1       a...
Radix-tree●   Гарантированность времени доступа●   На практике использует больше всего памятиОчень медленное: на тесте рав...
{B,T}-tree●   Хорошая пространственная локальность●   Как правило, время поиска можно считать    константным для RAM-only ...
Бинарные деревья●   В целом, слишком много обращений к памяти●   Балнсировка может быть довольно дорогой10/21/12
Хэш●   Как правило, обладает лучшим средним временем доступа●   На практике использует меньше всего памяти●   Хорошая прос...
Хэш: оптимизация (1)●   Двойное хэширование●   Определение размера на старте, как процент от доступной памяти    (без дина...
Хэш: гранулярность блокировок10/21/12
Алгоритмы вытеснения●   LRU (Least Recently Used): наиболее простой (O(1)), fundamental    locality●   Clok: алгоритм «вто...
Хэш: простое вытеснение10/21/12
Способ вытеснения1. Flushing-thread:   ●   максимальная скорость записи и чтения   ●   пробуждается по таймеру или при нех...
Lock-free структуры данных●   Обычно простые структуры данных (односвязные списки,    переменные)●   Сущестуют сложные про...
Очердь задач●   Обычно 1 производитель, N потребителей●   Реализует только операции push() и pop() (нет сканирований по   ...
Атомарные операции                 (Read-Modify-Write)       unsigned long a, b = 1;       b = __sync_fetch_and_add(&a, 1)...
Атомарные операции                (Compare-And-Swap)       unsigned long val = 5;       __sync_val_compare_and_swap(&val, ...
Атомарные операции                     (стоимость)●   Реализуются через протокол cache coherency (MESI)●   Писать в одну о...
Lock-free очередь (список):                      push()push (q: pointer to fifo, cl: pointer to cell):       Loop:        ...
Lock-free очередь (список):                      pop()pop (q: pointer to fifo):       Loop:               cl = q->head    ...
ABA problem●   Поток T1 читает значение A,●   T1 вытесняется, позволяя выполняться T2,●   T2 меняет значение A на B и обра...
Lock-free очередь: ABApop (q: pointer to fifo):       Loop:               cl = q->head               next = cl->next      ...
Решение ABA●   Вводятся счетчики числа pop()ов и push()ей для всей структуры или    отдельных элементов и атомарно сравнив...
Lock-free очередь на Ring-Buffer: (0)10/21/12
Lock-free очередь на Ring-Buffer: (1)●   Избавляемся от мьютексаpush():          while (tail_ + Q_SIZE < head_)           ...
Lock-free очередь на Ring-Buffer: (2)●   Резервируем место для вставки (→ одно чтение)push():          unsigned long tmp_h...
Lock-free очередь на Ring-Buffer: (3)●   Per-thread current LH & LTpush():           volatile unsigned long last_head_;   ...
Lock-free очередь на Ring-Buffer: (4)●  Чтение tail i-го потока только один разpush():       auto min = tail_;       for (...
Lock-free очередь на Ring-Buffer: (5)●   Не оставляем текущий tail i-го потока надолгоpop():         thr_pos().tail = __sy...
Lock-free очередь на Ring-Buffer: (6)●   Двойная запись текущего значения tail и синхронизация с    инкрементом tail_pop()...
Lock-free очередь                (список vs ring-buffer)●   Ring-buffer сложнее в реализации (требуется синхронизированное...
Lock-free очередь (ограничения)●   Работает только для очередей — сканировать такие структуры    данных без блокировок нел...
Zero copy●   Чем больше каждое из сообщений, обрабатываемых    сервером, тем актуальнее●   Решается через zero copy IO, ze...
Архитектура zero copy сервера10/21/12
Zero-copy ввод-вывод●   Сетевой (минуя Socket API/kernel копирования)●   Дисковый (минуя буферы программы или буферный кэш...
Zero-copy Network (I)O10/21/12
Zero-copy Network (I)O:                vmsplice()/splice()●   Только на Output, на Input memcpy() в ядре●   Сетевой стек п...
Zero-copy Network (I)O: примерvoid zcp_send(int sd, const struct iovec *iov, size_t num) {        pipe(pipe_);        size...
Splice: производительность(Тесты splice() от Jens Axboe):        # ./nettest/xmit -s65536 -p1000000   127.0.0.1 5500      ...
Спасибо!           ak@natsys-lab.com10/21/12
Upcoming SlideShare
Loading in …5
×

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

1,705 views

Published on

  • Be the first to comment

  • Be the first to like this

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

  1. 1. Разработка высокопроизводительных серверных приложений для Linux/UNIX Крижановский Александр ak@natsys-lab.com10/21/12
  2. 2. Пример (front-end server)10/21/12
  3. 3. Декомпозиция● Функциональная декомпозиция● Декомпозиция по данным10/21/12
  4. 4. Декомпозиция по данным 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
  5. 5. Функциональная декомпозиция Original: foo(); bar(); Thread 1: Thread 2: boo(); bar();● Пример: поток ввода, поток вывода, рабочий поток, поток реконфигурации и пр.● Не масштабируется.10/21/12
  6. 6. Процессы vs Потоки● Один и тот же task_struct, один и тот же do_fork()● Потоки разделяют память, а процессы – нет.PostgreSQL: процессы, shared memory для buffer pool, блокировки тоже вshared memory10/21/12
  7. 7. Потоки – много или мало?● Современные планировщики ОС обрабатывают N kernel-threadов за O(1) или O(log(N)) время● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => нужен пул тредов● M потоков на один CPU => ухудшается cache hit (поток должен завершить текущую задачу и только потом перейти к следующей)● Если нужен высокий параллелизм, то нужны coroutines; или events...10/21/12
  8. 8. Hyper Threading и когда много потоков хорошо● Потоки делят один кэш● Если cache hit rate <50%, то всегда выгоднее 2 и более потока на ядро● Если у одного потока высокий hit rate, то добавление дополнительных потоков на ядро ухудшит производительность● На одно ядро желательно не планировать потоки, исполняющие разный код с разными данными10/21/12
  9. 9. Синхронизация● pthread_mutex_lock() - только один поток владеет ресурсом● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель или много читателей● pthread_cond_wait() - ожидать наступления события● pthread_spin_lock() - проверяет блокировку в цикле● (первые 3 работают через futex(2))10/21/12
  10. 10. 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
  11. 11. pthread_cond_broadcast: гремящее стадо● Атомарные операции и, тем более, системный вызов – дорогие операции● Поэтому лучше использовать pthread_cond_signal()10/21/12
  12. 12. Инвалидация кэшей на мьютексе● Если поток не может захватить мьютекс, то он уходит в сон● => на текущем процессоре будет запущен другой поток● => этот поток “вымоет кэш”● Когда мьютекс будет отпущен, то поток продолжит выполнение с “вымытым” кэшем или на другом процессоре10/21/12
  13. 13. 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
  14. 14. 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
  15. 15. pthread_rwlock● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 раза, сложнее операция взятия лока)● Снижает lock contention при превалировании числа читателей над писателями, но снижается производительность per-cpu10/21/12
  16. 16. Lock contention● Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной● Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе● Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий● Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации10/21/12
  17. 17. 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
  18. 18. Иерархия кэшей10/21/12
  19. 19. Типы кэшей● Data cache (L1d, L2d)● Instruction cache (L1i, L2i)● TLB – значения преобразований виртуальных адресов страниц в физические (L1, L2)● L3 часто бывает смешанного типа10/21/12
  20. 20. getconf # getconf LEVEL1_DCACHE_SIZE 65536 # getconf LEVEL1_DCACHE_LINESIZE 64 # grep -c processor /proc/cpuinfo 210/21/12
  21. 21. Когерентность кэшей● Непротиворечивость кэшированных данных на многопроцессорных системах● Обеспечивается протоколом MESI (Modified, Exclusive, Shared, Invalid)● RFO (Request For Ownership) := M → I – CPU1 пишет по адресу X: X → M – CPU2 пишет по адресу X: – CPU1: X → I – CPU2: X → M10/21/12
  22. 22. Пример: std::shared_ptr● std::shared_ptr использует reference counter – целую переменную, разделяемую и модифицируемую всеми потоками10/21/12
  23. 23. False sharing● MESI оперирует cachelineами● RFO довольно дорогая операция● Если две различные переменные находятся в одном cacheline, то возникает RFO10/21/12
  24. 24. Выравнивание● Компилятор автоматически выравнивает элементы структур данных● GCC имеет специальные атрибуты, управляющие выравниванием данных10/21/12
  25. 25. Кэш1. Структура для хранения и быстрого поиска данных по некоторому ключу ● общая структура для хранения и поиска (дерево или хэш) ● две структуры: для индекса и для алгоритма вытеснения (обычно связный список)2. Алгоритм вытеснения данных3. Flush-потоки или вытеснение при записи10/21/12
  26. 26. Примеры хранилищ● Состояния HTTP или IM сессий● Данные о пользователях по ID или логинам● Буфферный кэш СУБД10/21/12
  27. 27. Цена доступа● Основное узкое место: время обращения к памяти● По мере роста структур, данные перестают помещаться в кэши (L1d, L2d, L3d, TLB L1d/L2d etc)● Выход их TLB (~1024 страницы) может стоить до 4х обращений к памяти вместо одного=> Основные критерии к структурам данных:● малый объем вспомогательных данных● пространственная локальность обращений10/21/12
  28. 28. Page Table10/21/12
  29. 29. Структуры данных & page table Application Tree Page table a.1 a b a.0 c c.0 c.110/21/12
  30. 30. Radix-tree● Гарантированность времени доступа● На практике использует больше всего памятиОчень медленное: на тесте равномерного распределения IPv4 адресовпочти в 2 раза проигрывает хэшу с простой хэш-функцией времени и 4раза по памяти(Linux VMM выбирает ключи для сохранения пространственнойлокальности)10/21/12
  31. 31. {B,T}-tree● Хорошая пространственная локальность● Как правило, время поиска можно считать константным для RAM-only структур данных● Довольно дорогие вставки● На практике все равно медленнее хэшей10/21/12
  32. 32. Бинарные деревья● В целом, слишком много обращений к памяти● Балнсировка может быть довольно дорогой10/21/12
  33. 33. Хэш● Как правило, обладает лучшим средним временем доступа● На практике использует меньше всего памяти● Хорошая пространственная локальность: 1 случайное обращение и линейное сканирование● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от ключей и нагрузки)● В некоторых случаях может обладать очень большим временем поиска10/21/12
  34. 34. Хэш: оптимизация (1)● Двойное хэширование● Определение размера на старте, как процент от доступной памяти (без динамического рехэшинга)● Просто повысить гранулярность блокировок10/21/12
  35. 35. Хэш: гранулярность блокировок10/21/12
  36. 36. Алгоритмы вытеснения● 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
  37. 37. Хэш: простое вытеснение10/21/12
  38. 38. Способ вытеснения1. Flushing-thread: ● максимальная скорость записи и чтения ● пробуждается по таймеру или при нехватке памяти2. Во время чтения или записи: ● чтение/запись медленее ● подходит для soft-realtime ● проще алгоритм10/21/12
  39. 39. Lock-free структуры данных● Обычно простые структуры данных (односвязные списки, переменные)● Сущестуют сложные протоколы изменения деревьев и хэшей – достаточно дорогие из-за своей сложности● Наиболее актуальны в условиях высокого lock contention на многопроцессорных системах● Очень полезны в «горячих» структурах: очереди задач, аллокаторы и пр.10/21/12
  40. 40. Очердь задач● Обычно 1 производитель, N потребителей● Реализует только операции push() и pop() (нет сканирований по списку)● Классический (наивный) вариант: std::queue, защищенный mutexом● Может быть реализована на атомарных операциях для снижения lock contention10/21/12
  41. 41. Атомарные операции (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
  42. 42. Атомарные операции (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
  43. 43. Атомарные операции (стоимость)● Реализуются через протокол cache coherency (MESI)● Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFOИспользуются в shared_ptr для reference counting10/21/12
  44. 44. Lock-free очередь (список): push()push (q: pointer to fifo, cl: pointer to cell): Loop: cl->next = q->tail if CAS (&q->tail, cl->next, cl): break10/21/12
  45. 45. Lock-free очередь (список): pop()pop (q: pointer to fifo): Loop: cl = q->head next = cl->next if CAS (&q->head, cl, next): break return cl10/21/12
  46. 46. ABA problem● Поток T1 читает значение A,● T1 вытесняется, позволяя выполняться T2,● T2 меняет значение A на B и обратно на A,● T1 возобновляет работу, видит, что значение не изменилось, и продолжает…10/21/12
  47. 47. Lock-free очередь: ABApop (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 cl10/21/12
  48. 48. Решение ABA● Вводятся счетчики числа pop()ов и push()ей для всей структуры или отдельных элементов и атомарно сравниваются● Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-6410/21/12
  49. 49. Lock-free очередь на Ring-Buffer: (0)10/21/12
  50. 50. 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 element10/21/12
  51. 51. 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
  52. 52. 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
  53. 53. 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
  54. 54. 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
  55. 55. 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
  56. 56. Lock-free очередь (список vs ring-buffer)● Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков)● Список должен быть интрузивным для избежания аллокации узлов на каждой вставке● Для списка нужно отдельно реализовать контроль числа элементов● Локализация и выравнивание памяти - ?10/21/12
  57. 57. Lock-free очередь (ограничения)● Работает только для очередей — сканировать такие структуры данных без блокировок нельзя● Для очереди нужна реализация ожидания: ● usleep(1000) — помещение потока в wait queue на примерно один такт системного таймера ● sched_yield() - busy loop на перепланирование (100% CPU usage)10/21/12
  58. 58. Zero copy● Чем больше каждое из сообщений, обрабатываемых сервером, тем актуальнее● Решается через zero copy IO, zero copy алгоритмы и структуры данных и архитектуру сервера в целом● Недостатки: сильная связность между IO, аллокатором памяти, управлением потоками10/21/12
  59. 59. Архитектура zero copy сервера10/21/12
  60. 60. Zero-copy ввод-вывод● Сетевой (минуя Socket API/kernel копирования)● Дисковый (минуя буферы программы или буферный кэш ОС)● Не “вымывает” кэши процессоров● Имеет накладные расходы => актуально только при работе с большими блоками данных10/21/12
  61. 61. Zero-copy Network (I)O10/21/12
  62. 62. Zero-copy Network (I)O: vmsplice()/splice()● Только на Output, на Input memcpy() в ядре● Сетевой стек пишет данные напрямую со страницы => перед использованием страницы снова нужно записать 2 размера буфера отправки (double-buffer write)10/21/12
  63. 63. 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
  64. 64. 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=3426110/21/12
  65. 65. Спасибо! ak@natsys-lab.com10/21/12

×