вторник, 8 декабря 2015 г.

if_enc(4) интерфейс во FreeBSD

Ни для кого, наверное, не новость уже, что IPSec во FreeBSD 11 включён в GENERIC ядро. Но вот if_enc(4) туда включать почему-то не стали. Модулем оно загружаться не умеет, а временами его функционал бывает довольно полезен. Поэтому, вслед за if_gre(4) и if_gif(4), я решил переделать и if_enc(4). Теперь в head/ его можно загружать из модуля, а так же выгружать после использования.
Пару слов для тех кто не знает, для чего он нужен. IPSec  может работать в туннельном и транспортном режимах. В туннельном режиме IPSec код инкапсулирует IP пакет внутрь IP-IP (другими словами - добавляет новый IP заголовок) и затем шифрует. При этом адреса во внешнем заголовке могут отличаться от адресов оригинальной (ещё незашифрованной) IP  дейтаграммы.
Внутри того же IPSec кода есть четыре места (два для входящих пакетов и два для исходящих), из которых происходит обращение к коду if_enc(4) драйвера. В этих местах у пользователя есть возможность посмотреть содержимое пакета, либо отфильтровать его файрволом. Про файрвол многие забывают, и именно поэтому при использовании if_enc(4) можно заблокировать весь IPSec трафик одним лишь поднятием интерфейса ifconfig enc0 up. Пока интерфейс не поднят, оверхеда он практически не создаёт, т.е. не выполняет никаких действий. Как только он поднят, пакеты начинают попадать в bpf(4) и могут быть просмотрены в tcpdump(8), а так же начинают отправляться в pfil(9), откуда, соответственно, попадают на рассмотрение пакетных фильтров.
Для исходящих пакетов в if_enc(4) пакет попадает перед тем как будет добавлен внешний IP заголовок и сразу после добавления. Если используется транспортный режим, то в обоих случаях пакет будет передан без изменений.
Для входящих пакетов, соответственно, сразу после расшифровки, но до того как внешний IP заголовок будет отброшен и после отбрасывания внешнего заголовка. Опять же, для транспортного режима пакет будет тот же самый.
В if_enc(4) имеется возможность контролировать, в каких из этих четырёх точкек мы хотим "видеть" пакеты. Управление осуществляется при помощи sysctl(8) net.enc.[in|out].ipsec_bpf_mask и net.enc.[in|out].ipsec_filter_mask.
Битовая маска определяет из каких мест IPSec кода следует обрабатывать пакеты. ipsec_bpf_mask отвечает за bpf(4), ipsec_filter_mask за pfil(9). Значение 1 включает обработку пакета "до" изменения, 2 - "после". Если хочется видеть оба, нужно использовать значение 3.
Внутри файрвола пакеты полученные из if_enc(4) выглядят как если бы они были получены или отправлены через интерфейс "enc0". И в зависимости от момента, когда вызван обработчик, проверять там нужно соответствующие адреса (внешнего заголовка или внутреннего).

среда, 3 сентября 2014 г.

Отладка ядра при помощи DTrace

Я не могу назвать себя большим специалистом в использовании DTrace, но время от времени я использую возможности этой технологии для отладки на живой системе. Особенно, когда систему нельзя перезагрузить, чтобы добавить в ядро дополнительные отладочные сообщения. В этой заметке я опишу некоторые способы и скрипты для dtrace. Возможно, кому-то они будут полезны, да и самому мне частенько приходится подглядывать в старые скрипты, когда срочно нужно что-то сделать, а уже не помнишь как :)

Всё что необходимо сделать на работающей системе для запуска dtrace:
  1. # kldload dtraceall
  2. # vim script.d
  3. # chmod +x script.d
  4. # ./script.d
Начну с последней задачи, которая ещё свежа в памяти. На маршрутизаторе с нагрузкой примерно 1Mpps на приём и на отдачу в логах время от времени появлялись сообщения от named о невозможности отправки ответа клиенту "error sending response: not enough free resources". При этом счётчик "output packets dropped due to no bufs, etc." в выводе netstat -sp ip растёт со скоростью примерно 100 пакетов в секунду.
% while true; do i=$j; j=`netstat -sp ip | awk '/no bufs/ {print $1}'`; echo $((j-i)); sleep 1; done
99499853
81
86
99
104
79
96
86
94
95
86
Если посмотреть в исходный код named'а, то можно увидеть, что такое сообщение возможно только при возвращении системным вызовом кода ошибки ENOBUFS. В сетевом стеке есть несколько мест, которые могут являться источником такого кода ошибки:
% grep -l ENOBUFS netinet/* net/* | column -c 80
netinet/in_gif.c netinet/sctputil.c net/if_fwsubr.c
netinet/in_mcast.c netinet/tcp_output.c net/if_gre.c
netinet/in_pcb.c netinet/tcp_syncache.c net/if_iso88025subr.c
netinet/ip_divert.c netinet/tcp_timewait.c net/if_spppsubr.c
netinet/ip_input.c netinet/tcp_usrreq.c net/if_stf.c
netinet/ip_mroute.c netinet/udp_usrreq.c net/if_tap.c
netinet/ip_output.c net/if_arcsubr.c net/if_tun.c
netinet/raw_ip.c net/if_atmsubr.c net/ifq.h
netinet/sctp_output.c net/if_bridge.c  net/netisr.c
netinet/sctp_pcb.c net/if_ethersubr.c net/route.c
netinet/sctp_usrreq.c net/if_fddisubr.c net/rtsock.c

Так как речь идёт о протоколе UDP и IPv4 (IPv4 адреса отображаются в сообщении об ошибке), то начиная с верхнего уровня будем спускаться вниз по стеку. Открываем netinet/udp_usrreq.c и смотрим, где может возвращаться ENOBUFS. Параллельно с этим, создаём скрипт для dtrace, чтобы в реальном времени найти функцию, возвращающую этот код ошибки. Одна из функций, которая может вернуть ENOBUFS - udp_output. Но если посмотреть в вывод dtrace -l, то мы не увидим там возможности трассировать эту функцию. Вероятно компилятор оптимизировал её... Придётся смотреть функцию, которая вызывает udp_output, а это - udp_send.
% sudo dtrace -l | grep udp_output
% sudo dtrace -l | grep udp_send
20109        fbt            kernel                          udp_send entry
20110        fbt            kernel                          udp_send return
Функция udp_send имеет такой код:
1756 static int
1757 udp_send(struct socket *so, int flags, struct mbuf *m, struct sockaddr *addr,
1758     struct mbuf *control, struct thread *td)
1759 {
1760         struct inpcb *inp;
1761 
1762         inp = sotoinpcb(so);
1763         KASSERT(inp != NULL, ("udp_send: inp == NULL"));
1764         return (udp_output(inp, m, addr, control, td));
1765 }
Здесь четвёртый аргумент (arg3 - в dtrace нумерация аргументов идёт с нуля) представляет из себя структуру sockadd_in, в которой содержится адрес и порт назначения отправляемой датаграммы. Его можно использовать в нашем скрипте:
% vim script.d
#!/usr/sbin/dtrace -s

fbt::udp_send:entry
{
        self->addr = (struct sockaddr_in *)arg3;
}
fbt::udp_send:return
/arg1 == 55/
{
        printf("%s:%d", inet_ntoa(&self->addr->sin_addr.s_addr), htons(self->addr->sin_port));
}

При запуске скрипта подсистема dtrace будет вызывать функцию fbt::udp_send:entry каждый раз, когда ядро вызывает udp_send. Всё что будет делать наша функция - сохранять указатель на адрес назначения. А обработчик fbt::udp_send:return будет вызываться при завершении функции udp_send, но наличие предиката /arg1 == 55/ ограничит количество вызовов только теми, когда код ошибки, возвращаемый udp_send, будет равен 55 (это числовое значение ENOBUFS). При этом будет напечатан адрес и порт назначения для датаграммы, которую система не смогла отправить (мы сохранили эти данные при входе в функцию).  Наблюдая одновременно за логом системы и за выводом этого скрипта можно сделать вывод, что мы идём верным направлением. Адреса совпадают.
Теперь можно спуститься на уровень ниже - в IP, чтобы убедиться что ошибка происходит не в самой udp_output, а в функции, которую она вызывает - ip_output. Добавляем в script.d такую функцию:
fbt::ip_output:return
/arg1 == 55/
{
        stack(5);
}

И снова запускаем скрипт. Функция fbt::ip_output:return будет вызываться при выходе из ip_output, когда возвращаемое ею значение будет ENOBUFS. При этом будет печататься набор вызовов функций, который привёл в ip_output:
% sudo ./script.d
dtrace: script './script.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
 30  49706                 ip_output:return 
              kernel`udp_send+0x519
              kernel`sosend_dgram+0x2d2
              kernel`kern_sendit+0x1a3
              kernel`sendit+0xdc
              kernel`sys_sendto+0x4d

 30  20110                  udp_send:return XX.88.58.YY:67
 30  49706                 ip_output:return 
              kernel`udp_send+0x519
              kernel`sosend_dgram+0x2d2
              kernel`kern_sendit+0x1a3
              kernel`sendit+0xdc
              kernel`sys_sendmsg+0x87

 30  20110                  udp_send:return XX.8.YY.55:51283

Как видим, udp_send не виновата. ENOBUFS, который возвращает udp_send приходит к ней из ip_output. ip_output может возвращать эту ошибку сама, а может получать от нижнего уровня - Ethernet. Таким образом, опускаясь всё ниже и ниже я добрался до драйвера сетевой карты ixgbe(4). В скрипте dtrace у меня из значимого остался вот такой код:
fbt::lagg_transmit:entry
{
        self->ifp = (struct ifnet *)arg0;
        self->sc = (struct lagg_softc *)self->ifp->if_softc;
}
fbt::lagg_enqueue:entry
{
        self->lp = (struct ifnet *)arg0;
}
fbt::lagg_transmit:return
/arg1 == 55/
{
        printf("ENOBUFS from %s / %d / %s", self->ifp->if_xname,
            self->sc->sc_proto, self->lp->if_xname);
        stack(10);
}

Вывод которого показал:
CPU     ID                    FUNCTION:NAME
 30  56590             lagg_transmit:return ENOBUFS from lagg0 / 4 / ix1
              kernel`vlan_transmit+0xcf
              kernel`ether_output_frame+0x33
              kernel`ether_output+0x4a2
              kernel`ip_output+0xdc0
              kernel`udp_send+0x519
              kernel`sosend_dgram+0x2d2
              kernel`kern_sendit+0x1a3
              kernel`sendit+0xdc
              kernel`sys_sendmsg+0x87
              kernel`amd64_syscall+0x5ea

 30  56590             lagg_transmit:return ENOBUFS from lagg0 / 4 / ix0
              kernel`vlan_transmit+0xcf
              kernel`ether_output_frame+0x33
              kernel`ether_output+0x4a2
              kernel`ip_output+0xdc0
              kernel`udp_send+0x519
              kernel`sosend_dgram+0x2d2
              kernel`kern_sendit+0x1a3
              kernel`sendit+0xdc
              kernel`sys_sendmsg+0x87
              kernel`amd64_syscall+0x5ea

Здесь немного поясню. Когда приложение делает системный вызов sendmsg для отправки датаграммы, мы попадаем в udp_send, затем вызываем ip_output, от туда мы попадаем в ether_output и ether_output_frame, далее в код vlan'а - vlan_transmit, от туда в код lagg(4), на котором создан vlan - lagg_transmit. lagg(4) создан на двух 10G карточках ixgbe(4). И внутри lagg(4) мы получаем ENOBUFS уже от самого драйвера ixgbe. Внутри кода драйвера ENOBUFS без отображения в статистике мы можем получить только при переполнении кольцевых буферов:
 848         err = drbr_enqueue(ifp, txr->br, m);
 849         if (err)
 850                 return (err);

Но и в этом случае ведётся статистика, только доступа к ней нет. Хотя, есть же отладчик! Запускаем kgdb, находим там нужную структуру ifnet, её softc, заглядываем внутрь каждой структуры buf_ring и смотрим на показания счётчика br_drops. Как-то так:
# kgdb
...
(kgdb) set $ifs=ifindex_table
(kgdb) set $i=1
(kgdb) while ($ifs[$i]->ife_ifnet != 0)
 >printf "%i -> %s\n", $i, $ifs[$i++]->ife_ifnet->if_xname
 >end
1 -> igb0
2 -> igb1
3 -> ix0
4 -> ix1
...
(kgdb) set $s=(struct adapter *)ifindex_table[3]->ife_ifnet->if_softc
(kgdb) set $i=0
(kgdb) while ($i < $s->num_queues)
 >printf "%d -> %d\n", $i, $s->tx_rings[$i++]->br->br_drops
 >end
0 -> 24
1 -> 4
2 -> 18
3 -> 7
4 -> 2
5 -> 9
6 -> 9
7 -> 22
8 -> 2
9 -> 1
10 -> 3
11 -> 2
12 -> 2
13 -> 2
14 -> 1046
15 -> 507

В первом цикле мы находим нужную нам структуру ifnet внутри массива ifindex_table. Это структуры с индексами 3 и 4. Ориентируясь на количество очередей внутри драйвера ixgbe(4), выводим на печать содержимое счётчика дропов кольцевых буферов. Как видим, у каждой очереди он не нулевой.
На второй карте было нечто похожее. И это никак не укладывалось в 100 дропов в секунду (в выводе netstat -sp ip). Так как здесь не более 2 тысяч дропов с момента включения сервера.

Что ж, продолжаем искать. В выводе netstat нужный нам счётчик имеет имя ips_odropped (это можно узнать из кода netstat(8), например). Ищем, где этот счётчик изменяется:
% grep -l ips_odropped * | column -c 80
igmp.c  ip_fastfwd.c ip_output.c ip_var.h

igmp.c отбрасываем, т.к. в выводе netstat -sp igmp количество пакетов составляло всего пару десятков. Остаются два кандидата ip_output и ip_fastforward. Для первого мы уже делали dtrace скрипты, и частых срабатываний там не было. А вот ip_fastforward ещё не проверяли. В этой функции счётчик ips_odropped может увеличиваться при любой ошибке полученной от Ethernet уровня:
583         if (error != 0)
584                 IPSTAT_INC(ips_odropped);

Раз ошибка может быть любой, то посмотрим - а какие вообще ошибки возвращает Ethernet уровень?
fbt::ether_output:return
{
        @err[arg1] = count();
}

# ./script.d
dtrace: script './script.d' matched 1 probe
^C

               55                2
               64             4043
                0         34401844

В этом dtrace скрипте я использовал функцию аггреагирования count(). Она сохраняет число вызовов в массив err, где ключём является код возвращаемой ошибки. Теперь смотрим в man errno: 55 код - это ENOBUFS, а 64 - EHOSTDOWN. И тут мы вспоминаем, что когда роутер не может определить L2 адрес хоста назначения, он возвращает эту ошибку из функции arpresolv. Кстати, в коде той же функции ведётся статистика для протокола ARP, которую можно увидеть в выводе netstat -sp arp:
$ while true; do i=$j; j=`netstat -sp arp | awk '/dropped/ {print $1}'`; echo $((j-i)); sleep 1; done
101807936
93
101
106
79
93
78
104
100
89
^C
Примерно вот так знание кода системы, некоторые навыки работы в отладчике и использование dtrace помогают убить время :)

вторник, 12 августа 2014 г.

kern.geom.part.mbr.enforce_chs

Сегодня добавил в FreeBSD 11 sysctl kern.geom.part.mbr.enforce_chs, который позволяет отключить автоматическое выравнивание по дорожке диска в MBR. Теперь, если у вас диск с 4k секторами, создаваемый раздел можно будет без проблем выравнять по границам сектора. В 10-ую и 9-ую версию будет MFC, но там это нужно будет отключать с помощью этого sysctl, либо при помощи настройки в loader.conf.

вторник, 1 июля 2014 г.

Реализация disklabel64 для FreeBSD

В этом посте я хочу рассказать о не так давно добавленной поддержке disklabel64 в GEOM_PART. Эта "таблица разделов" используется в DragonFlyBSD. На сколько я могу судить по коду, используется она там по-умолчанию. Я не являюсь пользователем DragonFlyBSD, но не смотря на это, я подписан на их рассылку для разработчиков, в которую посылаются commit messages. Так как это всё же форк FreeBSD, то их кодовая база пока ещё довольно близка к FreeBSD и там бывают интересные коммиты.
disklabel64 является неким гибридом GPT и disklabel, используемой во всех BSD. Число 64 в названии намекает на то, что все смещения и размеры внутри метаданных являются 64-битными, в отличие от disklabel, где они были 32-битными. Ещё одно важное отличие - в 64-битной версии все смещения указаны в байтах, а не в секторах. Это, при соблюдении некоторых условий, должно позволить disklabel64 оставаться работоспособной при изменении размера сектора. Например, когда у вас есть диск, где размер сектора можно переключить перемычкой. Если создать disklabel64 на диске, у которого размер сектора 512 байт, а потом через некоторое время переключить его на 4096 байт, то для пользователя ничего не изменится. А в GPT такой трюк не получится. Полезность этого функционала конечно довольно сомнительна, но она заложена в дизайн.
По-умолчанию disklabel64 может иметь 16 разделов, но резервирует место для 32. Они так же как в disklabel именуются буквами латиницы. Но для совместимости со старым софтом, раздел 'с' недоступен для пользователя. Если кто-то не помнит, то раньше в disklabel этот раздел адресовал весь диск или раздел, на котором находится disklabel. В disklabel64 пользователь не может обратиться к области метаданных через разделы. Т.е. метаданные защищены от "самоуничтожения". В disklabel если вы специально не позаботились и не создали раздел 'a' со смещением относительно начала диска, то у вас имеется возможность уничтожить метаданные выполнив запись на раздел.
Сами разделы обзавелись UUID'ами. Каждый раздел имеет свой уникальный UUID. Тип раздела может быть указан двумя способами - так же как в disklabel при помощи 8-битного числа, либо как в GPT при помощи UUID. Сама disklabel64 тоже имеет UUID, а так же 64 байта для символьной метки. Т.е. при желании можно добавить соответствующую поддержку в geom_label и использовать эти UUID'ы как метки.
Место для загрузочного кода резервируется между метаданными и началом доступного пользователю места для создания разделов. По-умолчанию это 32 кбайта.
В метаданные заложена возможность наличия резервной копии, но пока что она нигде не реализована.

Если у вас установлена DragonFlyBSD, то теперь у вас есть возможность обратиться к её разделам. Если конечно вы используете UFS :)

среда, 20 ноября 2013 г.

Изменение размера зеркала GEOM_MIRROR

Недавно мне был задан вопрос о том, как можно изменить размер зеркала, созданного при помощи geom_mirror(4), после замены дисков на бОльшие? Честно говоря, самому использовать geom_mirror мне довелось немного, а после появления zfs и совсем как-то перестал. Но посмотрев в код, понял, что простого способа нет.

В итоге требуемый функционал был добавлен и сейчас он уже включён в 11.0-CURRENT. Теперь, после того как вы заменили диски и они синхронизировались, можно выполнить команду gmirror resize и изменить размер зеркала. Тут я применил подход, используемый в gpart(8). Если пользователь при выполнении команды не указывает конкретный размер, то размер расчитывается автоматически до максимально доступного. Т.е. команда:
# gmirror resize gm0

просмотрит все компоненты зеркала, определит максимально доступный размер и установит его.

Впрочем, с помощью ключа -s можно задать и конкретный размер, который может быть и меньше текущего. Но если зеркало используется, то уменьшение зеркала выдаст ошибку EBUSY.

Кроме того, я добавил реализацию метода g_resize, про который я упоминал в предыдущей заметке. Теперь, если родительский провайдер изменит свой размер, то geom_mirror автоматически запишет свои метаданные в "новый" последний сектор. Это может быть использовано, например, при зеркалировании разделов. После изменения размера раздела при помощи gpart resize метаданные gmirror будут автоматически обновлены и записаны в последний сектор. Останется только выполнить ту же процедуру для второго компонента и сделать gmirror resize.

вторник, 12 ноября 2013 г.

Автоматическое изменение размера в GEOM_PART

Некоторое время назад Edward Tomasz Napierala выполнял проект, который позволяет изменять размеры файловых систем без необходимости их размонтирования (при определённых условиях конечно). В результате выполнения этого проекта у GEOM появились новый метод класса g_resize и вспомогательная функция g_resize_provider(). Так же были сделаны соответствующие изменения для некоторых GEOM классов, чтобы задействовать новый функционал.

Теперь несколько слов о том, как это работает.  В качестве целевой аудитории пользователей этого функционала предполагались владельцы умных СХД (систем хранения данных), которые могут динамически изменять размеры своих виртуальных дисков. Администратор такой системы может подвигать ползунком в веб морде СХД (например) и увеличить размер виртуального диска. Эта информация может быть донесена до SCSI диска, а драйвер диска передаёт её geom объекту класса GEOM_DISK. Geom объект вызывает функцию g_resize_provider() и это запускает цепочку действий по информированию всех consumer'ов этого диска об изменившемся размере. Если GEOM класс consumer'а имеет реализацию метода g_resize, то этот метод будет вызван. Таким образом GEOM класс может обработать это событие и выполнить какие-то свои действия.

Как же этот новый функционал можно применить в GEOM_PART? Самое малое было реализовано Эдвардом в рамках выполняемого им проекта - он сделал так, чтобы при выполнении команды gpart resize вызывалась функция g_resize_provider().  Это позволило по аналогии с цепочкой передачи события об изменении размера диска "СХД - драйвер диска - consumer'ы диска" реализовать цепочку "пользователь - раздел в таблице - consumer'ы раздела".  Т.е. теперь, все consumer'ы подключённые к разделу, размер которого изменился, могут что-то предпринять в ответ на это событие.
Если же диск меняет размер, то очевидно, что информация в метаданных таблицы разделов может перестать соответствовать действительности.  И здесь были нужны несколько большие изменения.

В итоге в FreeBSD 11.0-CURRENT сейчас сделано следующее. Для класса GEOM_PART реализован метод g_resize и теперь, при получении события об изменении размера родительского провайдера, будет произведена корректировка метаданных. Но, чтобы уменьшить вероятность совершения тяжело поправимых ошибок со стороны пользователя, эти изменения в метаданных не записываются сразу же на диск. Вместо этого в консоль выводится сообщение "GEOM_PART: %s was automatically resized" и диск помечается как "modified". Увидев такое сообщение в консоли или в системном журнале сообщений, пользователь должен сам решить, хочет он сохранить эти изменения или нет. Для сохранения ему необходимо выполнить команду gpart commit, или сделать любые изменения в таблице разделов, которые вызывают gpart commit неявно.

Это реализовано практически для всех поддерживаемых таблиц разделов, за исключением LDM и GPT. GPT имеет возможность быть помеченной CORRUPT, и для неё может быть использована команды recover, так что не особенно актуально. А LDM поддерживается в режиме только для чтения.

четверг, 11 апреля 2013 г.

PCPU счётчики в ядре

Несколько дней назад Глеб Смирнов (glebius@) добавил в ядро FreeBSD 10-CURRENT новое API для работы со счётчиками - counter(9). Счётчики в системе используются для ведения статистики. Обычно это структура данных, значения полей которой изменяются при возникновении тех или иных событий. Например, получение сетевого пакета, обнаружение некорректных данных, подсчёт количества определённых действий совершённых системой и т.п. Некоторые счётчики изменяются относительно редко, а некоторые могут изменяться миллионы раз в секунду. Ядром используется обычно два способа их изменения - это простой инкремент (операция добавления) или атомарное добавление (atomic(9)). 
У обоих методов есть недостатки. Обычный инкремент на многопроцессорных/многоядерных системах приводит к потере данных, т.е. когда несколько ядер одновременно изменяют одну и ту же переменную, выполнить это удаётся, грубо говоря, только одному ядру. Поэтому статистика временами заметно не соответствует действительности.
Атомарные операции не теряют данные благодаря блокированию шины памяти, что значительно сказывается на скорости выполнения. 
Общим недостатком является то, что изменение счётчика приводит к инвалидированию cache line, в которой находится счётчик из кешей процессоров, что опять же отрицательно сказывается на производительности.
Теперь о том, как эти проблемы решает новое API. Для каждого ядра процессора выделяется своя область памяти, а счётчики преобразуются таким образом, что каждому ядру соответствует своя копия счётчика. Поэтому, даже при одновременном изменении счётчика несколькими ядрами, каждое ядро будет изменять свой счётчик. При этом не будет происходить блокирование шины памяти, не будет потери данных и не будет вымывания кеша, т.к. каждое ядро обращается к своим данным. 
Чтобы обновить счётчик, нужно вычислить адрес счётчика в памяти приватной для данного ядра, а затем произвести его изменение. Для того чтобы в это время контекст не мигрировал на другое ядро, используются критические секции. Однако, не на всех архитектурах это необходимо и с помощью некоторых know-how удаётся избежать использования критических секций. В частности, на нашей основной архитектуре amd64 обновление счётчика осуществляется в одну процессорную инструкцию!
Для того чтобы прочитать значение счётчика, нужно сложить значения копий счётчика со всех ядер. Ну и объём памяти, занимаемый счётчиком, конечно же стал в несколько раз больше (в зависимости от числа ядер процессоров).
Это, на мой взгляд, приемлемая плата вот за какие результаты:
  • в ряде тестов новые счётчики показывают повышение производительности примерно на 63% по сравнению с обычной операцией инкремента, но при этом нет потерь данных, а обычный инкремент теряет 98% данных;
  • в сравнении с атомарными инкрементами новые счётчики в 22 раза быстрее;
  • на реальных тестах по приёму сетевого трафика замена счётчиков IP статистики приводит к снижению нагрузки на CPU примерно на 50%.