Как построить высокопроизводительный Front-end сервер...

Post on 14-Dec-2014

7.634 views 0 download

description

 

Transcript of Как построить высокопроизводительный Front-end сервер...

Как построитьвысокопроизводительный 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

Спасибо!

ak@natsys-lab.com