28
Как построить высокопроизводительный front-end сервер Александр Крижановский NatSys Lab

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

  • Upload
    ontico

  • View
    7.634

  • Download
    0

Embed Size (px)

DESCRIPTION

 

Citation preview

Page 1: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

end сервер

Александр Крижановский

NatSys Lab

Page 2: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Содержание Атомарные операции Снижение lock contention Lock-free структуры данных (FIFO/LIFO списки) Zero-copy network IO Аллокаторы памяти Привязка потоков/процессов к CPU

Page 3: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Front-end сервер: В основном работает только с RAM Как правило многопоточный Часто между потоками/процессами разделяется одна или несколько структур данных как на чтение, так и на запись

Page 4: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Front-end сервер:

Page 5: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Очередь Обычно 1 производитель, N потребителей Реализует только операции push() и pop() (нет сканирований по списку) Классический (наивный) вариант: std::queue, защищенный mutex'ом Может быть реализована на атомарных операциях для снижения lock contention

Page 6: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Атомарные операции: стоимость Реализуются через протокол cache coherency (MESI) Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO (Request For Ownership)

Page 7: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Lock contention Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации

Page 8: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Блокировки pthread_mutex, pthread_rwlock - используют futex(2), достаточно тяжеловестны сами по себе pthread_spinlock («busy loop») — в ядре ОС используется в сочетании с запретом вытеснения, в user space может привести к нежалательным последствиям Атомарные операции, барьеры и double-check locking — наиболее легкие, но все равно не «бесплатны»

Page 9: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Атомарные операции (Read-Modify-Write)

unsigned long a, b = 1;

a = __sync_fetch_and_add(&a, 1);

mov $0x1,%edx

lock xadd %rdx,(%rax)

Page 10: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Атомарные операции (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 11: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Атомарные операции: стоимость Реализуются через протокол cache coherency (MESI) Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO (Request For Ownership)

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

Page 12: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Lock-free очередь (список)push (ff: pointer to fifo, cl: pointer to cell):

Loop:

cl->next = ff->tail

if CAS (&ff->tail, cl->next, cl):

break

Page 13: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Lock-free очередь (список)pop (ff: pointer to fifo):

Loop:

cl = ff->head

next = cl->next

if CAS (&ff->head, cl, next):

break

return cl

Page 14: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

ABA problem Поток T1 читает значение A, T1 вытесняется, позволяя выполняться T2, T2 меняет значение A на B и обратно на A, T1 возобновляет работу, видит, что значение не изменилось, и продолжает…

Page 15: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Page 16: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Решение ABA Вводятся счетчики числа pop()'ов и push()'ей для всей структуры или отдельных элементов и атомарно сравниваются Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-64

Page 17: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Lock-free очередь(ring-buffer)

Page 18: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков) Список должен быть интрузивным для избежания аллокации узлов на каждой вставке Для списка нужно отдельно реализовать контроль числа элементов Локализация и выравнивание памяти - ?

Page 19: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Lock-free очередь (только) Работает только для очередей — сканировать такие структуры данных без блокировок нельзя Для очереди нужна реализация ожидания:

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

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

Page 20: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Кэш Hash table, RB-tree, T-tree, хэш деревьев и пр. Lock contention снижается путем введения отдельных блокировок для каждого bucket'а хэша или поддерева Часто нужно вытеснение элементов: Hash table как список или спикок + основная структура данных RW-блокировки, ленивые блокировки

Page 21: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Zero-copy Network (I)O

Page 22: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Page 23: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Page 24: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Когда полезны свои аллокаторы Специальный аллокатор, позволяющий читать из сокета по несколько сообщений и освобождать весь кусок разом (страницы + referece counting) Page allocator для работы c vmsplice/splice SLAB-аллокатор для объектов одинакового размера Boost::pool (частный случай SLAB-аллокатора): пул объектов одинакового размера, освобождаемых одновременно

Page 25: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Page 26: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

CPU binding (процессы) Не имеет смысла создавать больше тредов, чем физических ядер процессора для улучшения cache hit

Часто кэши процессора разделяются ядрами (L2, L3) Шины между ядрами одного процессора заметно быстрее шины между процессорами

=> worker'ов (разделяющих кэш) лучше привязывать к ядрам одного процессора.

Page 27: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

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

Page 28: Как построить высокопроизводительный Front-end сервер (Александр Крижановский)

Спасибо!

[email protected]