Upload
ontico
View
7.634
Download
0
Embed Size (px)
DESCRIPTION
Citation preview
Как построитьвысокопроизводительный front-
end сервер
Александр Крижановский
NatSys Lab
Содержание Атомарные операции Снижение lock contention Lock-free структуры данных (FIFO/LIFO списки) Zero-copy network IO Аллокаторы памяти Привязка потоков/процессов к CPU
Front-end сервер: В основном работает только с RAM Как правило многопоточный Часто между потоками/процессами разделяется одна или несколько структур данных как на чтение, так и на запись
Front-end сервер:
Очередь Обычно 1 производитель, N потребителей Реализует только операции push() и pop() (нет сканирований по списку) Классический (наивный) вариант: std::queue, защищенный mutex'ом Может быть реализована на атомарных операциях для снижения lock contention
Атомарные операции: стоимость Реализуются через протокол cache coherency (MESI) Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO (Request For Ownership)
Lock contention Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации
Блокировки pthread_mutex, pthread_rwlock - используют futex(2), достаточно тяжеловестны сами по себе pthread_spinlock («busy loop») — в ядре ОС используется в сочетании с запретом вытеснения, в user space может привести к нежалательным последствиям Атомарные операции, барьеры и double-check locking — наиболее легкие, но все равно не «бесплатны»
Атомарные операции (Read-Modify-Write)
unsigned long a, b = 1;
a = __sync_fetch_and_add(&a, 1);
mov $0x1,%edx
lock xadd %rdx,(%rax)
Атомарные операции (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)
Атомарные операции: стоимость Реализуются через протокол cache coherency (MESI) Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO (Request For Ownership)
Используются в shared_ptr для reference counting
Lock-free очередь (список)push (ff: pointer to fifo, cl: pointer to cell):
Loop:
cl->next = ff->tail
if CAS (&ff->tail, cl->next, cl):
break
Lock-free очередь (список)pop (ff: pointer to fifo):
Loop:
cl = ff->head
next = cl->next
if CAS (&ff->head, cl, next):
break
return cl
ABA problem Поток T1 читает значение A, T1 вытесняется, позволяя выполняться T2, T2 меняет значение A на B и обратно на A, T1 возобновляет работу, видит, что значение не изменилось, и продолжает…
Lock-free очередь: ABApop (ff: pointer to fifo):
Loop:
cl = ff->head
next = cl->next # cl = A, next = B
------------------->scheduled # pop(A), pop(B), push(A)
if CAS (&ff->head, cl, next): # cl->head => B (вместо C)
break
return cl
Решение ABA Вводятся счетчики числа pop()'ов и push()'ей для всей структуры или отдельных элементов и атомарно сравниваются Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-64
Lock-free очередь(ring-buffer)
Lock-free очередь(список vs ring-buffer)
Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков) Список должен быть интрузивным для избежания аллокации узлов на каждой вставке Для списка нужно отдельно реализовать контроль числа элементов Локализация и выравнивание памяти - ?
Lock-free очередь (только) Работает только для очередей — сканировать такие структуры данных без блокировок нельзя Для очереди нужна реализация ожидания:
usleep(1000) — помещение потока в wait queue на примерно один такт системного таймера
sched_yield() - busy loop на перепланирование (100% CPU usage)
Кэш Hash table, RB-tree, T-tree, хэш деревьев и пр. Lock contention снижается путем введения отдельных блокировок для каждого bucket'а хэша или поддерева Часто нужно вытеснение элементов: Hash table как список или спикок + основная структура данных RW-блокировки, ленивые блокировки
Zero-copy Network (I)O
vmsplice()/splice() Только на Output, на Input memcpy() в ядре Сетевой стек пишет данные напрямую со страницы => перед использованием страницы снова нужно записать 2 размера буфера отправки (double-buffer write)
Splice:производительность# ./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
Когда полезны свои аллокаторы Специальный аллокатор, позволяющий читать из сокета по несколько сообщений и освобождать весь кусок разом (страницы + referece counting) Page allocator для работы c vmsplice/splice SLAB-аллокатор для объектов одинакового размера Boost::pool (частный случай SLAB-аллокатора): пул объектов одинакового размера, освобождаемых одновременно
CPU binding (interrupts) APIC балансирует нагрузку между свободными ядрами Irqbalance умеет привязывать прерывания в зависимости от процессорной топологии и текущей нагрузки
=> не следует привязывать прерывания руками Прерывание обрабатывается локальным softirq, прикладной процесс мигрирует на этот же CPU
Cpu9 : 13.3%us, 62.1%sy, 0.0%ni, 1.0%id, 0.0%wa, 0.0%hi, 23.6%si, 0.0%stCpu10 : 0.0%us, 0.7%sy, 0.0%ni, 82.7%id, 0.0%wa, 0.0%hi, 16.6%si, 0.0%st
CPU binding (процессы) Не имеет смысла создавать больше тредов, чем физических ядер процессора для улучшения cache hit
Часто кэши процессора разделяются ядрами (L2, L3) Шины между ядрами одного процессора заметно быстрее шины между процессорами
=> worker'ов (разделяющих кэш) лучше привязывать к ядрам одного процессора.
CPU binding: пример# dd if=/dev/zero count=2000000 bs=8192 | nc 10.10.10.10 7700
16384000000 bytes (16 GB) copied, 59.4648 seconds, 276 MB/s
# taskset 0x400 dd if=/dev/zero count=2000000 bs=8192 \
| taskset 0x200 nc 10.10.10.10 7700
16384000000 bytes (16 GB) copied, 39.8281 seconds, 411 MB/s
Спасибо!