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.

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

10,652 views

Published on

  • Be the first to comment

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

  1. 1. Как построить высокопроизводительный front-end сервер Александр Крижановский NatSys Lab
  2. 2. Содержание <ul><li>Атомарные операции </li></ul><ul><li>Снижение lock contention </li></ul><ul><li>Lock-free структуры данных (FIFO/LIFO списки) </li></ul><ul><li>Zero-copy network IO </li></ul><ul><li>Аллокаторы памяти </li></ul><ul><li>Привязка потоков/процессов к CPU </li></ul>
  3. 3. Front-end сервер: <ul><li>В основном работает только с RAM </li></ul><ul><li>Как правило многопоточный </li></ul><ul><li>Часто между потоками/процессами разделяется одна или несколько структур данных как на чтение, так и на запись </li></ul>
  4. 4. Front-end сервер:
  5. 5. Очередь <ul><li>Обычно 1 производитель , N потребителей </li></ul><ul><li>Реализует только операции push() и pop() (нет сканирований по списку) </li></ul><ul><li>Классический (наивный) вариант: std::queue , защищенный mutex 'ом </li></ul><ul><li>Может быть реализована на атомарных операциях для снижения lock contention </li></ul>
  6. 6. Атомарные операции: стоимость <ul><li>Реализуются через протокол cache coherency (MESI) </li></ul><ul><li>Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO ( R equest F or O wnership) </li></ul>
  7. 7. Lock contention <ul><li>Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной </li></ul><ul><li>Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе </li></ul><ul><li>Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий </li></ul><ul><li>Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации </li></ul>
  8. 8. Блокировки <ul><li>pthread_mutex , pthread_rwlock - используют futex(2) , достаточно тяжеловестны сами по себе </li></ul><ul><li>pthread_spinlock («busy loop») — в ядре ОС используется в сочетании с запретом вытеснения, в user space может привести к нежалательным последствиям </li></ul><ul><li>Атомарные операции , барьеры и double-check locking — наиболее легкие, но все равно не «бесплатны» </li></ul>
  9. 9. Атомарные операции ( R ead- M odify- W rite) unsigned long a, b = 1; a = __sync_fetch_and_add (&a, 1); mov $0x1,%edx lock xadd %rdx,(%rax)
  10. 10. Атомарные операции ( C ompare- A nd- S wap) unsigned long val = 5; __sync_val_compare_and_swap (&val, 5, 2); mov $0x5,%eax mov $0x2,%ecx lock cmpxchg %rcx,(%rdx)
  11. 11. Атомарные операции: стоимость <ul><li>Реализуются через протокол cache coherency (MESI) </li></ul><ul><li>Писать в одну область памяти на разных процессорах дорого, т.к. процессоры должны обмениваться сообщениями RFO ( R equest F or O wnership) </li></ul><ul><li>Используются в shared_ptr для reference counting </li></ul>
  12. 12. Lock-free очередь (список) push (ff: pointer to fifo, cl: pointer to cell): Loop: cl->next = ff->tail if CAS (&ff->tail, cl->next, cl): break
  13. 13. Lock-free очередь (список) pop (ff: pointer to fifo): Loop: cl = ff->head next = cl->next if CAS (&ff->head, cl, next): break return cl
  14. 14. ABA problem <ul><li>Поток T1 читает значение A, </li></ul><ul><li>T1 вытесняется, позволяя выполняться T2, </li></ul><ul><li>T2 меняет значение A на B и обратно на A, </li></ul><ul><li>T1 возобновляет работу, видит, что значение не изменилось, и продолжает… </li></ul>
  15. 15. Lock-free очередь: ABA pop (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
  16. 16. Решение ABA <ul><li>Вводятся счетчики числа pop()'ов и push()'ей для всей структуры или отдельных элементов и атомарно сравниваются </li></ul><ul><li>Нужна операция CAS2 (Double CAS) для сравнения двух операндов: CMPXCHG16B на x86-64 </li></ul>
  17. 17. Lock-free очередь (ring-buffer)
  18. 18. Lock-free очередь (список vs ring-buffer) <ul><li>Ring-buffer сложнее в реализации (требуется синхронизированное передвижение указателей на tail и head для каждого из потоков) </li></ul><ul><li>Список должен быть интрузивным для избежания аллокации узлов на каждой вставке </li></ul><ul><li>Для списка нужно отдельно реализовать контроль числа элементов </li></ul><ul><li>Локализация и выравнивание памяти - ? </li></ul>
  19. 19. Lock-free очередь (только) <ul><li>Работает только для очередей — сканировать такие структуры данных без блокировок нельзя </li></ul><ul><li>Для очереди нужна реализация ожидания: </li></ul><ul><ul><ul><ul><li>usleep(1000) — помещение потока в wait queue на примерно один такт системного таймера </li></ul></ul></ul></ul><ul><ul><ul><ul><li>sched_yield() - busy loop на перепланирование (100% CPU usage) </li></ul></ul></ul></ul>
  20. 20. Кэш <ul><li>Hash table, RB-tree, T-tree, хэш деревьев и пр. </li></ul><ul><li>Lock contention снижается путем введения отдельных блокировок для каждого bucket'а хэша или поддерева </li></ul><ul><li>Часто нужно вытеснение элементов : Hash table как список или спикок + основная структура данных </li></ul><ul><li>RW-блокировки, ленивые блокировки </li></ul>
  21. 21. Zero-copy Network (I)O
  22. 22. vmsplice()/splice() <ul><li>Только на Output, на Input memcpy() в ядре </li></ul><ul><li>Сетевой стек пишет данные напрямую со страницы => перед использованием страницы снова нужно записать 2 размера буфера отправки ( double-buffer write ) </li></ul>
  23. 23. 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
  24. 24. Когда полезны свои аллокаторы <ul><li>Специальный аллокатор, позволяющий читать из сокета по несколько сообщений и освобождать весь кусок разом (страницы + referece counting) </li></ul><ul><li>Page allocator для работы c vmsplice/splice </li></ul><ul><li>SLAB-аллокатор для объектов одинакового размера </li></ul><ul><li>Boost::pool (частный случай SLAB-аллокатора): пул объектов одинакового размера, освобождаемых одновременно </li></ul>
  25. 25. CPU binding (interrupts) <ul><li>APIC балансирует нагрузку между свободными ядрами </li></ul><ul><li>Irqbalance умеет привязывать прерывания в зависимости от процессорной топологии и текущей нагрузки </li></ul><ul><li>=> не следует привязывать прерывания руками </li></ul><ul><li>Прерывание обрабатывается локальным softirq, прикладной процесс мигрирует на этот же CPU </li></ul>Cpu9 : 13.3%us, 62.1%sy , 0.0%ni, 1.0%id, 0.0%wa, 0.0%hi, 23.6%si , 0.0%st Cpu10 : 0.0%us, 0.7%sy, 0.0%ni, 82.7%id, 0.0%wa, 0.0%hi, 16.6%si , 0.0%st
  26. 26. CPU binding (процессы) <ul><li>Не имеет смысла создавать больше тредов, чем физических ядер процессора для улучшения cache hit </li></ul><ul><li>Часто кэши процессора разделяются ядрами (L2, L3) </li></ul><ul><li>Шины между ядрами одного процессора заметно быстрее шины между процессорами </li></ul><ul><li>=> worker'ов (разделяющих кэш) лучше привязывать к ядрам одного процессора. </li></ul>
  27. 27. 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
  28. 28. Спасибо! [email_address]

×