суббота, 24 июня 2017 г.

Использование Tor Expert Bundle на localhost

Использование Tor Expert Bundle не совсем тривиально, ввиду отсутствия внятной документации а также отсутствия в составе пакета некоторых необходимых компонентов.

Я хочу сразу оговориться, что речь идет о применении Tor не в оппозиционных и иных целях. Мне приходилось сталкиваться с ситуацией, когда в провайдерских сетях отсутствует внутрисетевой роутинг (по различным причинам), и в принципе необходимо попасть в соседнюю сеть (например, с целью администрирования собственных серверов), однако, ввиду повсеместного (и, зачастую, оправданного) резко отрицательного отношения к открытым прокси, это крайне затруднительно.

Дисклеймер. Да, данный инструмент может использоваться в различных целях, как и кухонный нож. Вы принимаете решение о его применении на свой страх и риск и на собственную ответственность.

Итак, исходные данные. 

У нас имеется станция под управлением Windows и нам нужен сервис Tor, с точкой входа в виде SOCKS proxy на локальной машине для торификации некоторых программ с доступом в Интернет. Причем, по-возможности, не зависящий от произвола провайдеров и государственных служб (я предупреждал, господа).

Для решения данной задачи нам понадобится Tor Expert Bundle, а также Pluggable Transports, не входящие в состав пакета.

Вам, в ряде случаев, придется озаботиться тем, как попасть на https://torproject.org и получить оттуда два важных компонента: собственно Tor Expert Bundle и Tor Browser (он нужен, поскольку в его состав входят Pluggable Transports).

Сперва необходимо установить Тор браузер. По умолчанию он ставится в Desktop:


Обычно я просто собираю эту директорию в архив и сохраняю вместе с Tor Expert Bundle.

Здесь обычно задают вопрос - "А на кой мне возиться с Expert Bundle, если у меня уже есть работащий торифицированны браузер?" На той, что не только браузер может нуждаться в торифицированном соединении. А, например, SSH-клиент. Мессенджеры. И так далее. Да и, зачастую, удобнее иметь постоянно работающий сервис, нежели зависеть от браузера, который надо держать запущенным.

Сам Tor Expert Bundle нужно распаковать в Program Files, например в "C:\Program Files (x86)\tor" и туда же распаковать Pluggable Transports:



Самое сложное в использовании Expert Bundle - это конфигурирование. Сначала нужно создать сервис, это просто:


Обратите внимание на замечание. Сервис выполняется из-под локального системного аккаунта. Что означает, что конфигурационные файлы и данные находятся здесь (обратите внимание, torrc отсутствует и сервис запущен с умолчаниями):

Это не то, что нам нужно, к тому же, нас не удовлетворяет запуск сервиса от системного аккаунта (это достаточно небезопасно, к тому же, значительно сложнее сделать правильную конфигурацию). Поэтому запустим управление компьютером и поменяем аккаунт сервиса на пользовательский:

Окей, теперь у нас рабочая директория находится у пользователя. Если по указанному пути ее нет, создадим ее:

Теперь в нее можно положить torrc, который надо написать. Взяв за основу torrc браузера, напишем свой конфигурационный файл:

 ####  
 # cd C:\Users\Yuri\AppData\Roaming  
 # mkdir tor  
 SocksPort 9050 IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth  
   
 Bridge 198.147.22.36:443 58DB653508197599B8CB7EE95772F3EF2255939D  
 Bridge 194.132.209.138:22670 582E2CD0132AE1FD89426EC624B76B36690A622F  
 Bridge 194.132.209.116:20650 0D113A4B44E9B79C604DFC8DAE35C3C74CF60FF9  
   
 Bridge obfs4 194.132.209.138:48574 582E2CD0132AE1FD89426EC624B76B36690A622F cert=G1jt3rntMnoWTy8uEeQu9VPhnmPn7BH5cHwXbSkQDNlyrFD6BL7D8PMiPwPooKGudt1iYw iat-mode=0  
 Bridge obfs4 194.132.209.116:34596 0D113A4B44E9B79C604DFC8DAE35C3C74CF60FF9 cert=QfRegE3lzpqsnWUAQTulUPje7KgdcL2c+qOUtoU0Jw/ln8KpPZbh43XdfKr1kXYYLLcYQg iat-mode=0  
 Bridge obfs4 68.45.52.117:40365 3C89FB56CDEE23F0F16FDF86086866E33EAB24D8 cert=s0SmVQop+pZPZxlHunrXQL6MW4uVOZS55XjDVaBYkaSSoN9FEZOif/dxxrufg6ZnskRkSw iat-mode=0  
   
 Bridge obfs3 194.132.209.138:54673 582E2CD0132AE1FD89426EC624B76B36690A622F  
 Bridge obfs3 194.132.209.116:44364 0D113A4B44E9B79C604DFC8DAE35C3C74CF60FF9  
 Bridge obfs3 68.45.52.117:36125 3C89FB56CDEE23F0F16FDF86086866E33EAB24D8  
   
 GeoIPFile C:\Program Files (x86)\Tor\Data\Tor\geoip  
 GeoIPv6File C:\Program Files (x86)\Tor\Data\Tor\geoip6  
 HiddenServiceStatistics 0  
 UseBridges 1  
   
 ClientTransportPlugin obfs2,obfs3,obfs4,scramblesuit exec C:\Program Files (x86)\Tor\PluggableTransports\obfs4proxy  
   

Бриджи предварительно получим на https://bridges.torproject.org. Я обычно добавляю обычные бриджи для облегчения бутстрапа, но это может вызвать некоторые проблемы - обычные бриджи иногда банятся. Поэтому стоит добавить обфусцированные, запустив соответствующий транспортный плагин (внизу в конфигурации). Обратите внимание на полные пути (в кавычки не берем!). Чтобы не терять соединения в случае активного бана бриджей (а также ввиду того, что иногда они меняются или перестают работать) советую за несколько последующих дней собрать десяток-два бриджей в каждой группе. Обязательно укажите SOCKS-порт, как показано - он будет точкой входа, и, кстати, IPv6 будет полезен даже если в вашей стране IPv6 не применяется - изредка это облегчает бутстраппинг и последующую работу Тор. 

Если применению Тор активно противодействуют, откажитесь от использования необфусцированных бриджей.

Я не использую FTE, так как плагин достаточно капризный и в последней версии Tor Bundle не работает из-за ошибки (исправят - начну использовать снова). В принципе, OBFS3/4 вполне достаточно.

Убедитесь, что конфигурация работает, запустив Тор интерактивно (предварительно остановив сервис, разумеется) - что бутстраппинг проходит до конца и клиентское соединение устанавливается.

Убедившись, что все в порядке и Тор в состоянии соединиться с сетью, запустите сервис.

Обратите внимание вот на что. В Тор есть небольшой баг, касающийся остановки Pluggble Transport при остановке его самого. Ввиду этого может понадобиться руками убить выполнение obfs4proxy в диспетчере задач (и, в общем, это может понадобиться делать не раз при перезапусках):

Теперь, когда сервис работает, можно настроить, например, Телеграм на его использование:


Аналогично можно настроить, например, Mail.ru Agent (хотя я считаю глупостью небезопасный по определению мессенджер прятать в туннель), или SSH client:


Проверяем:

Порядок, соединяемся через Tor. Консоль даже не слишком тормозит.

Резюмируем. Вы имеете туннельный сервис с SOCKS5 на входе. Любой браузер, большинство мессенджеров - словом, все программы, могущие использовать SOCKS в соединениях - могут быть выпущены через туннель.

Пользуйтесь на здоровье. И не забывайте время от времени обновлять/пополнять бриджи в конфигурации, убирая умершие и накапливая рабочие. Опыт показывает, что, чем больше вы имеете бриджей в конфигурации - тем более бесперебойно работает сервис.

воскресенье, 30 апреля 2017 г.

С++: Fastest check all digits in std::string

По мотивам вот этой переписки самое быстрое из известных решение (С++11, никаких итераторов и других извращений):

 /* Fast check if string contains only digits */  
 static bool DigitsOnly(const std::string &p_str) {  
      for (size_t i = 0, v_len = p_str.length(); i < v_len; ++i) {  
            if ((p_str[i] ^ '0') > 9)  
                return false;  
      }  
      return true;  
 }  
   

Можете проделать бенчмарк, если есть сомнения.

суббота, 15 апреля 2017 г.

C++: Использование std::vector как dynamic array

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

Так вот. Найдено наилучшее и наиболее элегантное решение. :)

      v_threads.resize(v_max_threads);  
      /*-------------Main Loop----------------*/  
      while (!std::cin.eof()) {  
           /* Start processing threads */  
           for (auto &t : v_threads) {  
                t = std::thread([]() { processdata(); });  
           }  
           /* Finish all threads */  
           for (auto &t : v_threads) {  
                t.join();  
           }  
      }  
      /*-------------Main Loop----------------*/  
   

Никаких push/emplace_back, никаких clear(). Вектор используется как array, причем с лучшими чертами динамических массивов и std::array.

Адресация элементов уже инициализированного вектора (resize вместо reserve) работает быстрее, чем push_back (примерно в четыре раза), и, хотя resize() и медленнее, чем reserve(), но она находится вне цикла и выполняется только один раз.

пятница, 14 апреля 2017 г.

Solaris: диагностика lock contention


Все знают одну из самых известных проблем с производительностью, а, точнее, с масштабированием. Это конкуренция блокировок (lock contention).

На первый взгляд кажется, что многопоточные приложения должны масштабироваться линейно. Так считают зачастую даже весьма опытные администраторы и даже (!) разработчики.

Как бы не так.

Невозможно всегда и везде писать lock-free код. Атомарные операции предельно ограничены областями, в которых их вообще можно использовать.

Стоит, например, заговорить об обработке строк - и привет, строковые операции невозможно выполнять атомарно по определению, они не допускают тривиального копирования, будучи контейнерами.

Наиболее часто в современном коде используются мьютексы, наиболее часто они эксклюзивные. Если говорить о средах разработки Солярис, то штатные библиотеки так густо усыпаны мютексами для гарантирования потокобезопасности, что непонятно, как код вообще может исполняться параллельно (мы же понимаем, что каждая блокировка мьютекса - это точка сериализации, да?). 

Вместе с тем, стоит отметить, что от среды разработки этот эффект, в общем, зависим мало - можно и с использованием GCC/STL понаписать такого, что куча процессорных ядер будет стоять колом в ожидании снятия блокировки.

Впрочем, речь не об этом.

Поговорим о том, как можно диагностировать lock contention, так как возникновение конкуренции блокировок вообще неочевидно и не всегда легко обнаруживается. 

Это важно, так как чтобы устранить bottleneck, надо его сперва обнаружить.

lockstat и standalone diagnostics

Если у вас есть возможность выделить самостоятельную программу для автономного запуска, вы можете использовать утилиту lockstat для диагностики конкуренции блокировок в различных режимах выполнения вашего кода (ну, или не вашего - главное, иметь возможность его запуска).

Я использовал в примерах одну программу, написанную в соавторстве с Joe Lawand, которая выполняется в асинхронном режиме (тредами) в диапазоне от 1 до 65534 треда на процесс.

Программа содержит три глобальных мьютекса (она оптимизирована, мьютексы эксклюзивные - lock_guard, но чрезвычайно короткие и в основных режимах работы используются лишь два из них):

 std::mutex g_cin_mtx, g_cout_mtx, g_log_mtx;     /* cin/cout/log guards */  

Как я и сказал, мьютексы ограничены очень короткой областью видимости:

      {  
           std::lock_guard<std::mutex> lock(g_cout_mtx);  
           std::cout << v_out;  
      }  
   

и снимаются немедленно по завершении критической секции кода.

Для тестов использовался 8-ядерный сервер с двумя процессорами Xeon под Solaris 10:

 root @ khorne / # psrinfo    
 0    on-line  since 04/12/2017 13:18:20  
 1    on-line  since 04/12/2017 13:18:31  
 2    on-line  since 04/12/2017 13:18:31  
 3    on-line  since 04/12/2017 13:18:31  
 4    on-line  since 04/12/2017 13:18:31  
 5    on-line  since 04/12/2017 13:18:31  
 6    on-line  since 04/12/2017 13:18:31  
 7    on-line  since 04/12/2017 13:18:31  
 root @ khorne / # psrinfo -v  
 Status of virtual processor 0 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:20.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 1 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 2 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 3 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 4 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 5 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 6 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
 Status of virtual processor 7 as of: 04/14/2017 19:40:41  
  on-line since 04/12/2017 13:18:31.  
  The i386 processor operates at 2333 MHz,  
     and has an i387 compatible floating point processor.  
   

Значение степени параллелизма по умолчанию у программы равно 4 треда на процесс, и в нормальном состоянии конкуренции блокировок не фиксируется:

 root @ khorne / # lockstat -s 10 -I ./store-id-helper      
  
 Profiling interrupt: 16 events in 0.021 seconds (750 events/sec)  
   
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 12% 0.00   1280 cpu[0]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2     cpu_idle_mwait+0x145    
                            idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 25% 0.00   1899 cpu[7]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@        1     cpu_idle_mwait+0x145    
    4096 |@@@@@@@@@@@@@@@        1     idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 38% 0.00   1392 cpu[6]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2     cpu_idle_mwait+0x145    
                            idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 50% 0.00   1467 cpu[5]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2     cpu_idle_mwait+0x145    
                            idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 62% 0.00   4039 cpu[3]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@        1     cpu_idle_mwait+0x145    
    4096 |                0     idle+0x89          
    8192 |@@@@@@@@@@@@@@@        1     thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 75% 0.00   1238 cpu[1]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2     cpu_idle_mwait+0x145    
                            idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   2 12% 88% 0.00   949 cpu[2]         i86_mwait+0xd        
   
    nsec ------ Time Distribution ------ count   Stack            
    1024 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2     cpu_idle_mwait+0x145    
                            idle+0x89          
                            thread_start+0x8      
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   1  6% 94% 0.00   1236 cpu[4]         anon_get_slot+0x82     
   
    nsec ------ Time Distribution ------ count   Stack            
    2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 1     anon_array_enter+0x105   
                            segvn_faultpage+0x124    
                            segvn_fault+0x98f      
                            as_fault+0x205       
                            pagefault+0x8b       
                            trap+0x3d7         
                            cmntrap+0x140        
 -------------------------------------------------------------------------------  
 Count indv cuml rcnt   nsec CPU+PIL        Caller           
   1  6% 100% 0.00   2581 cpu[4]         mutex_enter+0x10      
   
    nsec ------ Time Distribution ------ count   Stack            
    4096 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 1     hment_prepare+0x6b     
                            hati_pte_map+0x1e3     
                            hati_load_common+0x12b   
                            hat_memload+0x6a      
                            hat_memload_region+0x9   
                            segvn_faultpage+0x264    
                            segvn_fault+0x98f      
                            as_fault+0x205       
                            pagefault+0x8b       
 -------------------------------------------------------------------------------  

Как видно, ожидания снятия блокировок есть, но очень короткие.

Запустим теперь 1000 тредов на один процесс:

 lockstat -s 10 -I ./store-id-helper<t1.txt -1000  

Взгляните, появились ожидания LWP:


Вот именно то, о чем я говорил - конкуренция за блокировки. Потоки стоят в очереди, ожидая освобождения двух мьютексов.

Более наглядно это можно увидеть на гистограмме:


Значения счетчиков события ожидания выросли. Становится видно, что ожидающий процесс переходит в состояние idle и планировщик снимает его с выполнения.

plockstat и runtime diagnostics


Предыдущий пример показывает, как использовать standalone diagnostics. Чаще всего, однако, нам необходима диагностика на живых системах, в режиме runtine execution.

Для этого используем plockstat.

Данная утилита позволяет увидеть lock contention еще более наглядно. Чтобы довести ситуацию до абсурда, выполним два прогона: один в дефолтной конфигурации (4 треда), и один - с запуском максимального возможного количества, по верхней границе лимита программы:

 /* No lock contention - 4 threads */  
   
 root @ khorne / # plockstat -C -n 1000 -s 1 -e 1 -p 4295   
     0  
 root @ khorne / #   
   
   
   
 /* Lock contention - 65534 threads */  
   
   root 17765 16422  2 18:46:14 pts/2    0:05 store-id-helper -65534  
 root @ khorne / # plockstat -C -n 1000 -s 1 -e 1 -p 17765  
     0  
 Mutex block  
   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   22 204393378 libc.so.1`__sbrk_lock    0  
   
    nsec ---- Time Distribution --- count Stack  
    8192 |@@           |   2 0  
    16384 |@            |   1   
    32768 |            |   0   
    65536 |@            |   1   
   131072 |@@           |   2   
   262144 |            |   0   
   524288 |            |   0   
   1048576 |            |   0   
   2097152 |            |   0   
   4194304 |            |   0   
   8388608 |            |   0   
  16777216 |            |   0   
  33554432 |            |   0   
  67108864 |@@@@@@@@@        |   9   
  134217728 |@            |   1   
  268435456 |@@           |   2   
  536870912 |@@           |   2   
 1073741824 |@@           |   2   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   3 447414272 0x449740           0  
   
    nsec ---- Time Distribution --- count Stack  
    65536 |@@@@@@@@        |   1 0  
   131072 |            |   0   
   262144 |            |   0   
   524288 |            |   0   
   1048576 |            |   0   
   2097152 |            |   0   
   4194304 |            |   0   
   8388608 |            |   0   
  16777216 |            |   0   
  33554432 |            |   0   
  67108864 |            |   0   
  134217728 |            |   0   
  268435456 |@@@@@@@@        |   1   
  536870912 |            |   0   
 1073741824 |@@@@@@@@        |   1   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   3 268457301 0x449980           0  
   
    nsec ---- Time Distribution --- count Stack  
    65536 |@@@@@@@@        |   1 0  
   131072 |            |   0   
   262144 |            |   0   
   524288 |            |   0   
   1048576 |            |   0   
   2097152 |            |   0   
   4194304 |            |   0   
   8388608 |            |   0   
  16777216 |            |   0   
  33554432 |            |   0   
  67108864 |            |   0   
  134217728 |            |   0   
  268435456 |@@@@@@@@        |   1   
  536870912 |@@@@@@@@        |   1   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   13 41341873 libc.so.1`_uberdata     0  
   
    nsec ---- Time Distribution --- count Stack  
    8192 |@@@           |   2 0  
    16384 |            |   0   
    32768 |@            |   1   
    65536 |@@@@@@@         |   4   
   131072 |@@@           |   2   
   262144 |            |   0   
   524288 |            |   0   
   1048576 |            |   0   
   2097152 |            |   0   
   4194304 |            |   0   
   8388608 |            |   0   
  16777216 |            |   0   
  33554432 |            |   0   
  67108864 |@@@           |   2   
  134217728 |@            |   1   
  268435456 |@            |   1   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   10 47026176 store-id-helper`g_cout_mtx  0  
   
    nsec ---- Time Distribution --- count Stack  
    8192 |@@           |   1 0  
    16384 |            |   0   
    32768 |@@           |   1   
    65536 |@@           |   1   
   131072 |@@           |   1   
   262144 |@@           |   1   
   524288 |            |   0   
   1048576 |            |   0   
   2097152 |            |   0   
   4194304 |            |   0   
   8388608 |            |   0   
  16777216 |            |   0   
  33554432 |            |   0   
  67108864 |@@@@@@@         |   3   
  134217728 |@@@@          |   2   
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   1 268435456 0x4497c0           0  
   
    nsec ---- Time Distribution --- count Stack  
  268435456 |@@@@@@@@@@@@@@@@@@@@@@@@|   1 0  
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   1 268435456 0x449700           0  
   
    nsec ---- Time Distribution --- count Stack  
  268435456 |@@@@@@@@@@@@@@@@@@@@@@@@|   1 0  
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   1 134217728 0x449880           0  
   
    nsec ---- Time Distribution --- count Stack  
  134217728 |@@@@@@@@@@@@@@@@@@@@@@@@|   1 0  
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   1  262144 0x449840           0  
   
    nsec ---- Time Distribution --- count Stack  
   262144 |@@@@@@@@@@@@@@@@@@@@@@@@|   1 0  
 -------------------------------------------------------------------------------  
 Count   nsec Lock             Caller  
   1  262144 0x449800           0  
   
    nsec ---- Time Distribution --- count Stack  
   262144 |@@@@@@@@@@@@@@@@@@@@@@@@|   1 0  
 root @ khorne / #   
   

Тут все совершенно очевидно. Нет конкуренции - нет вывода, выводится ноль и все. Во втором случае у нас в ожидании находится свыше 60к процессов, мы видим, что ожидания вызывает именно мьютекс  g_cout_mtx.

Как видите, все просто. Если, при этом, вы видите исходный код, то определение проблемных мест сразу позволяет перейти к их оптимизации.

PS. Интересный факт. Древний (и давно устаревший) top показывает максимум 999 LWP:


Впрочем, в Солярис он отсутствует (его устанавливают отдельно - а потом ему всецело верят, что зря).

А вот штатный prstat показывает все честно, как есть (и в нем нет странных лимитов):



среда, 29 марта 2017 г.

Оптимизация Squid 3+: Часть IV

Конфигурация


Очень часто встречаются несколько распространенных ошибок при конфигурировании Squid, которые принципиально не позволяют достичь сколько-нибудь приемлемой производительности и латентности. Зачастую эти ошибки просто небезопасны.

Характерные примеры подобных ошибок:
  • Слепое копирование default-конфигурации, слепое копирование чужой конфигурации
  • Наполнение конфигурации дефолтными или тупо задранными до максимума значениями
  • Несбалансированные конфигурации, написанные бездумно либо с непропорциональными параметрами, впрямую относящимися к производительности либо ресурсам
  • Неэффективные, чрезмерно избыточные конфигурации с гигантскими ACL либо с избыточно дискретными ACL
  • Несуразные конфигурации - например, чрезмерно гигантский memory cache при cache_dirs по умолчанию или при его отсутствии
Основная причина подобных ошибок - игнорирование концептуальной архитектуры, документации, и непонимание основных принципов работы кэша.

Правила пальца №1. Вдумчиво изучите squid.conf.documented. Пишите свой конфиг с нуля. Структурируйте его. Порядок - важен. Не пишите в него значений по умолчанию, исключая те, которые нужны или полезны для понимания смысла конфигурации.

Параметры кэширования

cache_mem - не задавайте данный параметр равным половине физической памяти или большей ее части. Законы сохранения действуют - где, вы думаете, должна работать ОС и остальные сервисы? Помимо того, что чрезмерно большой кэш может просто быстро привести к панике ядра, он может оказаться медленнее дисковой системы. Кроме того, не забывайте о необходимости памяти для остальных задач кэша, включая обработку запросов, парсинг, кэширование метаданных - все это тоже размещается не в космосе. А кэш метаданных еще и непрерывно прирастает по мере заполнения дискового кэша. 1/8 - 1/4 RAM будет вполне достаточно.

memory_pools - не все ОС толерантно относятся к пулам, иногда пулы текут, иногда они превышают лимиты, и они весьма редко освобождаются. Следует использовать пулы только в случаях, когда они действительно дают выигрыш и помнить правило - "Памяти много не бывает - вранье".

memory_replacement_policy heap GDSF
cache_replacement_policy heap LFUDA

Два этих параметра намеренно привожу в оптимальных (в большинстве случаев) значениях. LRU древний и не слишком эффективный алгоритм замещения, так как он игнорирует распределение объектов по размерам. heap-политики почти во всех случаях оказываются более эффективными - раз, и два - в кэше памяти следует размещать по-возможности часто запрашиваемые некрупные объекты, а на дисках - в первую очередь крупные объекты. Рекомендую почитать об алгоритмах GDSF и LFUDA для того, чтобы определить, подходят ли они вам.

maximum_object_size_in_memory 1 MB - ограничивайте размер объектов в кэше памяти. Но и не занижайте чрезмерно эту величину, things changes.

minimum_object_size 10 bytes
maximum_object_size 4 Gb

Мне не кажется хорошей идея набивать кэш веб-багами размером 1 байт (каждый из которых занимает метаданные целого файла), точно так же, как тратить ресурсы на их кэширование. С другой стороны, не стоит хранить редко используемые гигантские объекты - как правило это неразумно.

cache_dir должен соответствовать (хотя бы приблизительно) объему, который в нем хранится. То есть глупо использовать дефолтные параметры иерархии и задавать размер 1 Тб. Пример относительно эффективных параметров cache_dir (в вашем случае может оказаться неэффективным или неработоспособным!):


 cache_dir diskd /data/cache/d1 48000 64 512  
 cache_dir diskd /data/cache/d2 48000 64 512  
 cache_dir diskd /data/cache/d3 48000 64 512  
 cache_dir diskd /data/cache/d4 48000 64 512     

Распределение объектов разных размеров по cache_dir, размещенных на различным образом оптимизированных дисковых массивах (если они используются), целесообразно. Причем cache_dir для мелких объектов следует помещать первыми (порядок важен). В некоторых случаях целесообразно отключить кэшированиие файловых систем (но не во всех).

SSD могут решить проблему узких мест в СХД, если у вас много денег и есть запасные на замену - интенсивность записи при прогреве и replacement дисковых кэшей весьма высокая. Ресурс может оказаться ограничен. Однако выигрыш в скорости может стоить таких затрат.

При избытке оперативной памяти рассмотрите возможность применения RAM disks для наиболее узких мест - например, кэша сертификатов ssl_crtd (предусмотрите сброс его содержимого на жесткие диски при остановке сервисов кэша). Это более целесообразно, нежели выделение гигантского cache_mem.

Списки контроля доступа

Как и в случае firewall, следует избегать написания гигантских ACL в сотни и тысячи элементов. Помимо того, что они занимают много места в памяти и сильно замедляют перезапуск и refresh, они чрезвычайно замедляют обработку запросов (помните? Однопоточное ПО. Обработка этих списков идет последовательно).

Хинт: ACLы на основе регулярных выражений поддаются одной хитрой и неочевидной оптимизации. Группы с вариантами на основе OR (|) работают быстрее, чем последовательный список всех вариантов. То есть, вот эта строка:

 (stnd\-avpg|avs\-avpg|stnd\-ipsg|bash\-avpg)\.crsi\.symantec\.com  

обрабатывается в несколько раз быстрее, чем ее последовательный эквивалент:

 stnd\-avpg\.crsi\.symantec\.com  
 avs\-avpg\.crsi\.symantec\.com  
 stnd\-ipsg\.crsi\.symantec\.com  
 bash\-avpg\.crsi\.symantec\.com\.com  

Хитрость здесь в том, что последовательное применение функции regex_match требует больше времени, имеет больший оверхед и займет более, чем вдвое больше времени, чем вариант, приведенный выше. Как ни странно, выражения OR обрабатываются в одном вызове regex_match более эффективно. 

Несмотря на утверждение некоторых администраторов, что регистронечувствительные ACL работают быстрее, мы с коллегой не согласны с этим утверждением по алгоритмическим причинам и не рекомендуем щедро посыпать списки контроля доступа опцией -i без настоятельной необходимости ее использования. Это может снизить скорость обработки запросов, причем в некоторых случаях весьма значительно.

refresh_pattern

Наличие огромного количества высокодискретных (на каждый домен и поддомен) refresh_pattern - ошибка, сильно бьющая по производительности. Рекомендация точно такая же, как и в предыдущем примере - сокращайте регулярные выражения при помощи групп с (|):

 # Updates: Windows  
 refresh_pattern (windowsupdate|microsoft|windows)\.com/.*\.(cab|exe|ms[i|u|f|p]|[ap]sf|wm[v|a]|dat|zip|psf) 43200 80% 129600 reload-into-ims  
 refresh_pattern microsoft\.com\.akadns\.net/.*\.(cab|exe|ms[i|u|f|p]|[ap]sf|wm[v|a]|dat|zip|psf) 43200 80% 129600 reload-into-ims  
 refresh_pattern deploy\.akamaitechnologies\.com/.*\.(cab|exe|ms[i|u|f|p]|[ap]sf|wm[v|a]|dat|zip|psf) 43200 80% 129600 reload-into-ims     

Правило пальца: строк refresh_pattern должно быть как можно меньше. Избегайте применения опции -i без необходимости.

DNS

Быстрый DNS имеет прямое влияние на производительность прокси. Как ни странно, статистика показывает, что, почти во всех случаях, не имеет смысла сильно увеличивать параметр ipcache_size. Значения 4096 достаточно в обычных (не слишком крупных) конфигурациях.

Гораздо лучше разместить высокоскоростной рекурсор (например, Unbound) прямо на прокси-боксе либо не далее одного хопа от него. И нацелить кэш непосредственно на него. Только не увлекайтесь чрезмерными TTL для кэшированных ответов на рекурсоре - может привести к проблемам с CDN (хотя да, значительно улучшает отклик). Защищайте рекурсор - это вектор для атаки.

Разное

Есть пара параметров, которые могут отказаться полезными в оптимизации дискового кэша:

 # Default is 20  
 store_objects_per_bucket 32  
   
 store_avg_object_size 200 KB  

Хочу, однако, обратить внимание на то, что данные параметры задаются на основе анализа статистики кэша, а не высасываются из пальца. store_avg_object_size, например, задается на основании параметра Mean object size из статистики (обычно ставится численно равным или близким к статистическому показателю). store_objects_per_bucket влияет на структуру swap-файлов, хранящих хэши и может варьироваться для уменьшения оверхеда. Однако не меняется без перестроения swap-файлов (переиндексации кэша) и изменения не заметны немедленно.

Заключение: Волшебной пули не существует

Оптимизация - это кропотливый процесс, связанный с длительным сбором статистики, ее анализом, и подбором необходимых (не всех и не наугад) параметров. Не увлекайтесь. Синдром чрезмерного администрирования - это профессиональное заболевание. :)


вторник, 28 марта 2017 г.

C++: Гетеросексуальный рефакторинг


Я просто оставлю это здесь:

   /* Alien-faggot-style and bwaaaaaaaaah! */  
   /* 0,25 ms evaluation time on Xeon */  
   /* 1 ms evaluation time on SPARC */  
   if (!(!s1.empty() && s2.empty())) {  
    .....some stuff...  
   } else {  
    .....some stuff...  
   }  
     
   /* Heterosexual refactoring :) */  
   /* Less 0,15 ms on Xeon */  
   if (!s2.empty()) {  
    .....some stuff...  
   } else if (!s1.empty()) {  
    .....some stuff...  
   }  
   
   /* Heterosexual refactoring 2 - best ever :) */  
   /* Less 0,05 ms on Xeon */  
   if (!s2.empty()) {   
    .....some stuff...  
   } else { /* else if (!s1.empty()) */  
    .....some stuff...  
   }  

Ну, вы поняли :) Не надо писать булевского ужаса :) Понятное дело, что тут рефакторинг зависит от внешних условий, которые на примере не показаны, однако он работает. И - да, в последнем примере хорошим тоном будет оставить комментарий в else, иначе логика неочевидна. :)


понедельник, 6 марта 2017 г.

C++: Измерение интервалов с использованием steady_clock

Библиотека sys/time.h, она же Манька Аблигация  ctime - на самая лучшая штука, когда нужно не абсолютное время (с использованием gettimeofday() ), а интервальные замеры - например, для профилирования и тому подобных задач.

Лишние движения арифметические и тому подобное.

Кроме того, время может прыгать (например, при выполнении синхронизаций NTP).

Логически более правильно для подобных задач воспользоваться steady_clock - в большинстве реализаций они действительно steady. Да и считать в коде придется чуть меньше.

Простенько и со вкусом:

 #include <chrono>  
   
 std::chrono::steady_clock::time_point t1, t2;  
 std::chrono::duration<double> elapsedTime;  
   
 int main(int argc, char* argv[])  
 {  
   
  t1 = std::chrono::steady_clock::now();  
   
   /* Some stuff */  
   
  t2 = std::chrono::steady_clock::now();  
   
  /* Compute elapsed time in ms */  
  std::chrono::duration<double, std::milli> elapsedTime = t2 - t1;     /* Fractional duration: no duration_cast needed */  
   
  std::cout << "Time: " << std::to_string(elapsedTime.count()) << " ms" << std::endl;  
 }  
   

Для контраста посмотрим, что было раньше:

 #include <ctime>     /* for gettimeofday() */  
   
 timeval t1, t2;  
 double elapsedTime;  
   
 int main(int argc, char* argv[])  
 {  
   
  gettimeofday(&t1, nullptr);  
   
  /* Some stuff */  
    
  gettimeofday(&t2, nullptr);  
   
   
  /* Compute elapsed time in ms */  
  elapsedTime = (t2.tv_sec - t1.tv_sec) * 1000.0;          /* sec to ms */  
  elapsedTime += (t2.tv_usec - t1.tv_usec) / 1000.0;     /* us to ms */  
        
  std::cout << "Time: " << std::to_string(elapsedTime) << " ms" << std::endl;  
   
 }  
   

Не слишком изящно, не правда ли?

среда, 22 февраля 2017 г.

C++: Эффективная разбивка строки на токены

По интернету гуляет два основных варианта токенайзера строк (strotk() мы не рассматриваем, он из С и корявый, а использование StringStream это для недоумков и тормозов. Ну а Boost для такой задачи тащить это карандаш мельничным жерновом затачивать):

 // Many delimiters possible, null-tokens protection  
 std::vector<std::string> split(const std::string &str, const std::string &delims)  
 {  
   std::vector<std::string> tokens;  
   std::size_t start = str.find_first_not_of(delims), end = 0;  
   
   while((end = str.find_first_of(delims, start)) != std::string::npos)  
   {  
     tokens.push_back(str.substr(start, end - start));  
     start = str.find_first_not_of(delims, end);  
   }  
   if (start != std::string::npos)  
     tokens.push_back(str.substr(start));  
   
   return tokens;  
 }  
   
 // One delimiter only  
 std::vector<std::string> split(const std::string &str, const std::string &delim)  
 {  
      std::vector<std::string> tokens;  
      size_t prev = 0, pos = 0;  
      do  
      {  
           pos = str.find(delim, prev);  
           if (pos == std::string::npos)  
                pos = str.length();  
           std::string token = str.substr(prev, pos-prev);  
           if (!token.empty())  
                tokens.push_back(token);  
           prev = pos + delim.length();  
      }  
      while (pos < str.length() && prev < str.length());  
      return tokens;  
 }  
   

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

Ага, щаз. Второй вариант кажется подходящим в большинстве случаев, кроме того, простейший тайминг показывает, что он раза в три минимум быстрее первого (ага, цена итераторов и вообще ООП).

И что, думаете, он эффективен по максимуму?

Как бы не так! Никто не дает себе труда вчитаться в код и поиграть с ним, прежде, чем использовать.

Вот оптимальный вариант:

 // Optimized split  
 // One delimiter only  
 std::vector<std::string> split(const std::string &str, const std::string &delim)  
 {  
      std::vector<std::string> tokens;  
      size_t prev = 0, pos = 0;  
      do {  
           pos = str.find(delim, prev);  
           if (pos == std::string::npos)  
                pos = str.length();  
           if (!str.substr(prev, pos-prev).empty())  
                tokens.push_back(str.substr(prev, pos-prev));  
           prev = pos + delim.length();  
      } while (pos < str.length());  
      return tokens;  
 }  
   

Не надо конструировать контейнеры в цикле - это раз, и два - выходное условие было избыточным. Вы бы это тоже знали, если бы потестили функцию сперва. И вообще, недоумки, когда копипастите код с StackOverflow, хотя бы вчитывайтесь в него. После оптимизации она минимум втрое эффективнее, чем до (да, профилирование показывает не менее, чем в три раза в среднем).

Ну и помните про реентрантность - с вектором она нереентрантна (см. предыдущую статью).

PS. Да, мне известны недостатки этого алгоритма. Он работает только с одним разделителем, и два разделителя подряд дают пустой токен. Но он экстремально быстр, а его недостатки мне не мешают. Кроме того, он легко трансформируется в потокобезопасный и реентрантный вариант - правда, с ограничением по числу токенов и удобству. Во всех остальных случаях можете тяжко вздохнуть и взять вариант 1. Кстати, его тоже можно оптимизировать. Если немного подумать.

пятница, 17 февраля 2017 г.

std::vector is non-reentrant

Я говорил, что, в отличие от разработчиков Boost, разработчики STL хотя бы люди? Я хочу взять свои слова назад.

Это история ловли одного бага, которая заняла свыше трех недель. При этом баг не воспроизводился, в лабораторных условиях воссоздать его не удалось, а документация и прочие источники не дали ничего вразумительного.

Взгляните на код:

 void processdata()  
 {  
      std::vector<std::string> inToken;  
   
  // Some stuff  
   
  for (size_t i = 1; i < v_max; i++) {  
     // Some stuff  
                     inToken.push_back(token);  
      }  
                       
  // Some stuff  
   
 }
  
 int main(int argc, char* argv[])  
 {  
   
  // Some stuff  
    
      while (!std::cin.eof()) {  
           /* Start processing threads */  
           for (size_t i = 0; i < v_max_threads; i++) {  
                threads.push_back(std::thread([]() {  
                     processdata();  
                }));  
           }  
           /* Finish all threads */  
           std::for_each(threads.begin(), threads.end(), [](std::thread &t) {  
                t.join();  
           });  
           threads.clear();  
      }  
      /*-------------Main Loop----------------*/  
      return 0;  
 }  

Все красиво, не правда ли? Полностью локальный вектор в тредовой процедуре. В этой процедуре вектор используется для парсинга строк (std::string) переменной длины. Конкретно - строка разбивается на токены, причем первый токен коротенький, и его длина не превышает нескольких символов, второй может варьироваться от 20 до 2048 символов, изредка - более.

 Все хорошо и прекрасно, но иногда, вне какой либо связи с нагрузкой или любыми другими видимыми событиями, данное приложение сегфолтилось. Причем GDB при обратной трассировке показывал только выход из процессингового треда по join() (приложение было собрано с ключом -g), а DBX давал странную трассировку:

 t@336 (l@336) terminated by signal SEGV (no mapping at the fault address)  
 Current function is std::__detail::_Executor<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::sub_match<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::__cxx11::regex_traits<char>, true>::_M_dfs  
  795     { return *(this->_M_impl._M_start + __n); }  
 
 (dbx) where  
 current thread: t@336  
 =>[1] std::__detail::_Executor<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::sub_match<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::__cxx11::regex_traits<char>, true>::_M_dfs(this = 0xfffffd7ffea90ef0, __match_mode = _Match_mode::_Prefix, __i = 17), line 795 in "stl_vector.h"  
  [2] std::__detail::_Executor<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::__cxx11::sub_match<__gnu_cxx::__normal_iterator<const char*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >, std::__cxx11::regex_traits<char>, true>::_M_dfs(this = 0xfffffd7ffea90ef0, __match_mode = <value unavailable>, __i = <value unavailable>), line 267 in "regex_executor.tcc"  

Смотрите очень внимательно. Regex тут не при чем. Дамп в принципе не содержит отсылки к коду приложения, то есть исключение прилетает не оттуда. Что-то с аллокатором, да? Какой-то контейнер вызывает проблему.


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

Посмотрите на код еще раз.

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

Говоря простым языком, у контейнера std::vector есть аллокатор памяти, который общий для всех векторов, которые вы определяете - неважно, локальные или глобальные.

И, когда происходит push_back в вектор, иногда перераспределяется память (помните, я говорил выше, что вектор используется для строк переменной длины?). Так вот, поскольку реализация аллокатора сделана не совсем людьми, никто о реентрантности и не думал. Более того, в разных системах, с разными libc и разными реализациями вышеприведенный код может вести себя как угодно. Если какие-нибудь алиены в дистрибутиве Бла-Бла-Бла-Линукс вдруг озаботились реентрантностью, они могли использовать примитивы синхронизации в аллокаторе контейнеров. Хорошо, если мьютексы. Много хуже, если спин-локи. Если не озаботились - получайте сегфолт в любой момент времени. На одной платформе мы не получали сегфолтов, но процессор взлетал в момент вызова аллокатора до 50 или 100% резким и кратковременным пиком. Что сказывалось на работе всего сервера, поскольку происходило достаточно часто.

В нашем случае, когда в двух разных асинхронных тредах одновременно происходил push_back в два - напоминаю, разных локальных вектора - в случае, если вставляемые строки сильно отличались по размеру, в одном из тредов или в двух одновременно происходил вызов нереентрантнолго аллокатора, что приводило к повреждению памяти, повреждению вставляемых строк и обычно оба треда падали в дамп (они обычно попарно и падали, сегфолты почти всегда происходили парами).


Окей, чтобы убедиться что я прав, давайте закостылим:

 std::mutex vec_mtx;  
   
 void processdata()  
 {  
      std::vector<std::string> inToken;  
   
  // Some stuff  
   
  for (size_t i = 1; i < v_max; i++) {  
     // Some stuff  
                {  
                     std::lock_guard<std::mutex> lock(vec_mtx);  
                     inToken.push_back(token);  
                }  
   
      }  
                       
  // Some stuff  
   
      }  
   
 int main(int argc, char* argv[])  
 {  
   
  // Some stuff  
    
      while (!std::cin.eof()) {  
           /* Start processing threads */  
           for (size_t i = 0; i < v_max_threads; i++) {  
                threads.push_back(std::thread([]() {  
                     processdata();  
                }));  
           }  
           /* Finish all threads */  
           std::for_each(threads.begin(), threads.end(), [](std::thread &t) {  
                t.join();  
           });  
           threads.clear();  
      }  
      /*-------------Main Loop----------------*/  
      return 0;  
 }  

Обернем push_back в мьютекс.

Ой! Что мы видим! Сегфолт ушел!

Одно плохо. Скорость выполнения тоже ухудшилась. Примерно в 3-30 раз. В зависимости от данных.

Не говорите мне, что надо было делать вызов Вектор.reserve() перед вставками. Во-первых, это не помогает, а во-вторых, это не имеет отношения к проблеме. К производительности push_back - может быть, но не к реентрантности.

Решение плохое во всех отношениях. Точка сериализации. Конкуренция блокировок.

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

Все было коряво, дьявольски усложняло алгоритмику, вело к утечкам (потенциальным) памяти итд. итп.

В конечном итоге проблему удалось решить радикально, переписав процедуру processdata() полностью без векторов, с использованием лишь наиболее примитивных контейнеров. Мне повезло, что в векторе могло быть от 1 до 4 элементов. Поэтому я отделался малой кровью лишь слегка изменив алгоритм и затолкав элементы в нумерованные переменные типа строка.



Но представьте себе, что вам надо обработать в потоке больше 100 элементов? Представили? Я как-то плохо представляю себе подобное без массив-подобных структур. Более того, я очень плохо представляю себе потоковую обработку, где я ограничен самыми примитивными типами данных из-за реентрантности.

Тщательный (очень тщательный поиск!) привел лишь к одной-единственной ссылке, где в принципе упоминалась теоретическая возможность (лишь возможность!) подобной ситуации. При этом все гуру СПО/GCC/STL всюду вещают, что контейнеры - и векторы в частности - потокобезопасные, нигде не сообщая, что они могут оказаться нереентрантными. (я умолчу о том, что в СПО в принципе дело плохо с документированием. Читайте исходники. Вы видели исходники STL? :)).

Да, я знаю, что можно обойти проблему, написав поток на C и на самом низком уровне - строки как цепочки указателей на символы, или написать свой аллокатор - реентрантный - для одного-единственного приложения.

Вся суть проблемы совершенно понятна - мышление уровня IBM PC с одним-единственным процессором Pentium-D ведет к тому, что программист, пишущий инструментальные средства, принципиально не задумывается, во-первых, обо всех возможных вариантах программирования, а, во-вторых, не представляет, что код может исполняться более, чем в один поток более, чем одним процессорным ядром. Не стоит говорить - "пиши иначе". В данной конкретной задаче иначе - не получится. Надо работать с локальными векторами и баста. И у меня все равно не укладывается в голове, как можно иметь локальную структуру, которая заявлена как потокобезопасная, но имеет глобальный некопируемый аллокатор, который никаким вывертом невозможно сделать локальным. И ради одной небольшой подпрограммы надо либо писать собственный класс контейнера + аллокатор. Если это прелести объектно-ориентированного программирования, то я - за процедурный подход.

Резюмируя вышенаписанное, я утверждаю, что разработчики STL - это дети осла, свиньи и Чужого из фильма "Чужой-4" (таким образом, они лишь на 1/3 генетически люди):


Поздоровайтесь с родителем, упыри! Это один из ваших отцов. 

среда, 1 февраля 2017 г.

Оптимизация Squid 3+: Часть III

Архитектура (продолжение)

Асинхронные задания

Все было бы совершенно печально в одном потоке, если бы не некоторые девелоперские ухищрения. С некоторой натяжкой можно считать Squid частично-параллельным, так как некоторые движения в нем выполняются как асинхронные задания (этот параллелизм так тонок, что его почти не видно. В частности, вы не увидите появления LWP, как бы ни напрягали зрение, вглядываясь в вывод top). 

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

Кстати, не спешите материть разработчиков. Чтобы переписать весь код в thread-aware, надо бОльшую его часть по факту написать заново (вы проклянете только синхронизацию, когда увидите, сколько глобальных переменных и данных вам надо будет защищать мьютексами. И можете даже не заикаться про atomic - это будет еще больше работы и вряд ли получится. Можете попробовать).

Масштабирование

Исходя из всего вышесказанного, следует понять и усвоить следующее.

Возможности по неограниченному масштабированию базовой архитектуры Squid очень сильно ограничены.

Причем масштабирование на одну большую машину весьма быстро упрётся в встроенные ограничения (самое радикальное из которых заключается в том, что Squid внутри далеко не во всех местах 100% 64-битный. Вот так, чешуйчатые. Сюрприз. Я не говорю, что его нельзя собрать в 64 бита. Можно. Только вот указатели и типы данных внутри далеко не все окажется 64-битными. И если вы наивно попытаетесь от собранного кода получить лимиты истинно 64-битного кода, вас ждет нехилый сюрприз). Постепенно и очень неспешно код правится, однако лишь когда репортится действительно фатальный и воспроизводимый баг, связанный с этим кодом.

Главным сюрпризом является тот факт, что большие машины в настоящее время уже  не SMP, а CMT. Соответственно, собственная архитектура SMP в Squid на таких машинах поведет себя.....эээээээ..... несколько не так, как ожидают разработчики.

Из вышесказанного вытекает вот что. Одиночный инстанс Squid лучше не раздувать до небес (выигрыш очень быстро будет перекрыт административными затратами на сопровождение и аномалиями в поведении), а оставить его маленьким. И масштабировать Squid как Grid, то есть создать кластер относительно некрупных кэшей-сиблингов с мощным горизонтальным интерконнектом а-ля-кластер, или иерархическую структуру - двух- или трехуровневую. Возможно - но не обязательно - с использованием CARP.

среда, 11 января 2017 г.

Сборка OpenSSH версии 7.3+ на Solaris 10

Коллеги-соляристы уже, думаю, обратили внимание, что, начиная с версии 7.3, OpenSSH невозможно собрать на Solaris 10 в 64 бита. И сидят на старье - что небезопасно, либо гоняют 32-битный код на 64-битном ядре, пользуясь дуальностью ABI.

На самом деле - невозможно собрать без плясок с бубном. Мне удалось получить решение от разработчиков (причем они сами его не считают нужным вкатить в исходники по религиозным соображениям), и я считаю нужным поделиться этим решением (оно вам пригодится).

Сперва вам нужны GNU patch, autoconf и automake. Их можно установить с OpenCSW, если вы этого до сих пор не сделали.

Затем нужны патчи.

Для версии 7.3 (7.3_solaris10_build_issue.patch):
 --- configure.ac     Thu Jul 28 04:54:27 2016  
 +++ configure.ac     Wed Aug 3 00:05:35 2016  
 @@ -751,6 +751,9 @@  
       use_pie=auto  
       check_for_libcrypt_later=1  
       check_for_openpty_ctty_bug=1  
 +     dnl Target SUSv3/POSIX.1-2001 plus BSD specifics.  
 +     dnl _DEFAULT_SOURCE is the new name for _BSD_SOURCE  
 +     CPPFLAGS="$CPPFLAGS -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_DEFAULT_SOURCE"  
       AC_DEFINE([PAM_TTY_KLUDGE], [1],  
            [Work around problematic Linux PAM modules handling of PAM_TTY])  
       AC_DEFINE([LOCKED_PASSWD_PREFIX], ["!"],  
 @@ -1790,11 +1793,8 @@  
       warn \  
  ])  
    
 -dnl Wide character support. Linux man page says it needs _XOPEN_SOURCE.  
 -saved_CFLAGS="$CFLAGS"  
 -CFLAGS="$CFLAGS -D_XOPEN_SOURCE"  
 +dnl Wide character support.  
  AC_CHECK_FUNCS([mblen mbtowc nl_langinfo wcwidth])  
 -CFLAGS="$saved_CFLAGS"  
    
  AC_LINK_IFELSE(  
      [AC_LANG_PROGRAM(  
   

Для версии 7.4 (7.4_solaris10_build_issue.patch):
 --- configure.ac     Mon Dec 19 21:18:36 2016  
 +++ configure.ac     Mon Dec 19 21:18:40 2016  
 @@ -740,6 +740,9 @@  
       use_pie=auto  
       check_for_libcrypt_later=1  
       check_for_openpty_ctty_bug=1  
 +     dnl Target SUSv3/POSIX.1-2001 plus BSD specifics.  
 +     dnl _DEFAULT_SOURCE is the new name for _BSD_SOURCE  
 +     CPPFLAGS="$CPPFLAGS -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_DEFAULT_SOURCE"  
       AC_DEFINE([PAM_TTY_KLUDGE], [1],  
            [Work around problematic Linux PAM modules handling of PAM_TTY])  
       AC_DEFINE([LOCKED_PASSWD_PREFIX], ["!"],  
 @@ -1771,11 +1774,8 @@  
       warn \  
  ])  
    
 -dnl Wide character support. Linux man page says it needs _XOPEN_SOURCE.  
 -saved_CFLAGS="$CFLAGS"  
 -CFLAGS="$CFLAGS -D_XOPEN_SOURCE"  
 +dnl Wide character support.  
  AC_CHECK_FUNCS([mblen mbtowc nl_langinfo wcwidth])  
 -CFLAGS="$saved_CFLAGS"  
    
  TEST_SSH_UTF8=${TEST_SSH_UTF8:=yes}  
  AC_MSG_CHECKING([for utf8 locale support])  
   

Патч нужно положить в корень дерева исходников OpenSSH и применить:

 root$server /tmp/openssh-7.4.p1 # patch -p0<7.4_solaris10_build_issue.patch   

Затем, в этой же директории, выполнить autoreconf. Если команда выполнится без ошибок - готово, можно приступать к конфигурированию и компиляции.

Безопасности вашему серверу!

PS. Чтобы autoreconf прошел без ошибок, нужно установить пакеты CSWautoconf, CSWautomake, CSWgpatch - если вы используете OpenCSW.

UPDATE

Начиная с версии OpenSSH 7.5 баг исправлен:


Изменение было включено в апстрим после подтверждения.

вторник, 10 января 2017 г.

Оптимизация Squid 3+: Часть II

Архитектура (продолжение)

Дисковый IO

Дисковый IO, за исключением aufs (реализация которого подходит исключительно для Linux и оставляет желать лучшего), реализован как 1 или более процессов. Неблокирующим IO является aufs, diskd и rock (применяемый исключительно в SMP-конфигурациях с множеством ограничений).

Технически aufs использует треды и должен бы по идее работать несколько быстрее diskd, особенно на CMT-машинах. Однако реальных бенчмарков не приводилось, а на ряде платформ, к тому же, невозможна или бессмысленна замена diskd на aufs. Кроме того, по многочисленным сообщениям, aufs нередко приводит к падению disk hit и множественным ошибкам об отсутствии кэшированного файла (в cache.log), прямо приводящим к перезагрузке файла из интернета.

Несмотря на возможные скоростные проблемы, таким образом, единственным прилично отлаженным механизмом неблокирующего дискового IO является diskd (кстати, rock реализует в точности такой же механизм IO и позиционируется как высокоскоростной дисковый кэш). 

Небольшой тюнинг и правильное планирование СХД позволяет получить с применением diskd почти максимально возможную производительность.

Сетевая подсистема

Сетевая подсистема Squid достаточно архаична. 

С одной стороны, она использует платформенно-специфичные механизмы поллинга - SELECT/POLL/EPOLL/dev/poll.

С другой - она также не является многопоточной, не поддерживает - и не будет поддерживать, по словам разработчиков - libevent и event ports.

И, следовательно, крайне плохо масштабируется при увеличении входного потока.

Из имеющихся средств масштабирования стоит упомянуть только pipeline_prefetch (предварительная выборка и пре-парсинг некоторого количества запросов с упреждением в некую очередь - попытка спародировать предвыборку Chrome, достаточно неэффективная) и collapsed_forwarding - а это совсем из другой оперы, это группировка однотипных запросов к одному ресурсу, отдаваемому с заголовками Range Request и Partial Content, в предположении, что, возможно, эти куски можно кэшировать (это приводит к увеличению задержек при обработке запросов, на момент написания статьи полностью не отлажено и не доработано, и против TCP_MISS/206 практически неэффективно, как показывают тесты).

Фактически, разработчики прямо подталкивают в ситуации, требующей масштабирования, к применению SMP (действительно, могущего увеличить нагрузочную способность одиночного инстанса, однако этот функционал опоздал по меньшей мере лет на 5-10), однако ввиду его крайней сырости, платформенной специфичности, высокой сложности в конфигурировании и применении, я врагу не пожелаю его использовать.

Так как разработчики публично отказываются перерабатывать архитектуру на thread-aware по причинам, которые им кажутся объективными, единственная полноценная возможность масштабирования - это построение кластеров parent-child-sibling с распределением входящего трафика по инстансам.

Хелперы

Одно из самых узких и спорных мест в Squid. 

Первое, что вам необходимо помнить. Внутренняя архитектура хелперов - сериальная. То есть ни о каком сколько-нибудь выраженном параллелизме речь не идет. Из особенностей следует упомянуть лишь очереди запросов к хелперам (concurrency; в версии 4+ расширены добавлением достаточно длинных очередей ожидающих обработки запросов queue-size), и крайне кривой механизм распределения запросов по дочерним процессам хелперов - это совсем не round-robin или least-load, распределение на практике чрезвычайно неравномерное, приводящее к перегрузке первого из процессов, вне зависимости от степени загруженности остальных процессов, и почти во всех случаях статус BUSY для этого первого процесса. С соответствующими задержками в обработке. Возможно, ситуация будет исправлена в версии 5+ - было анонсировано изменение алгоритма распределения запросов по хелперам. Посмотрим.

Следует правильно интерпретировать смысл параметра concurrency для дочерних процессов хелперов. Со стороны squid это не более, чем нумерованная channel ID сериальная очередь к каждому из процессов.

Важно следующее. Все имеющиеся хелперы, за редчайшим исключением, следует понимать как сериальные. Объясню, почему. Способность хелпера корректно понимать и обрабатывать channel ID, согласно протоколу взаимодействия, еще не делает хелпер параллельным.

Ну вот так вот, просто. Все имеющиеся в настоящий момент на рынке хелперы, за исключением ufdbguard 1.32.5 beta 5 и DCB, не являются многопоточными. От слова "совсем". Даже если способны работать с concurrency>1.

Истинно параллельный (или thread-aware) хелпер должен сразу же, на стыке с Squid, для каждого запроса, поступающего с отдельным channel-ID, стартовать отдельный тред (или процесс) для обработки.

Если он это не делает - он однопоточный.

С точки зрения масштабирования, сейчас, в 2016м году, каждый хелпер должен быть с возможностью параллельной обработки очередей. Это, однако осложняется технической реализацией потоков IO на интерфейсе хелперов и относительной сложностью программирования многопоточных приложений, использующих cin/cout. Кроме того, многие разработчики не считают целесообразным утруждать себя подобным трудом, не видя очевидной выгоды от подобной реализации (кстати, ошибочно).

Что необходимо держать в голове в связи с хелперами. 

Как и их интерфейс, так и они сами - сериальные. Следовательно, задержки в обработке - как в Squid, так и в хелперах - немедленно сказываются на общей латентности прокси и его производительности, и оказывают взаимное влияние на повременную статистику друг друга. Имейте это в виду при мониторинге и тюнинге.

воскресенье, 8 января 2017 г.

C++: Кто быстрее - слон или кит?

Не холивара ради, не нравоучений для, а просто для прикола. Небольшой кусочек сравнительной анатомии из жизни инопланетян-геев :) Ну или геев-инопланетян, кому как больше нравится.

Как вы думаете, мои хитиновые хвостатые, с рекурсивными челюстями, сошедшие с иллюстраций Гигера, друзья: какая реализация главного цикла из приведенных ниже быстрее выполняется? Нет, не трогая компилятор :)

Вариант I:

 // If max threads not specified, set default  
 std::thread threads[v_max_threads];  
 /*-------------Main Loop----------------*/  
      while (!std::cin.eof()) {  
           // Start all processing threads  
           for (auto& t: threads) {  
                t = std::thread([&]() {  
                     processdata();  
                     });  
           }  
           // Finish all threads  
           for (auto& t: threads) {  
                t.join();  
           }  
      }  
 /*-------------Main Loop----------------*/  
   

Вариант II

 // If max threads not specified, set default  
 std::vector<std::thread> threads;  
 threads.reserve(v_max_threads);  
 /*-------------Main Loop----------------*/  
      while (!std::cin.eof()) {  
           // Start all processing threads  
           for (unsigned int i = 0; i < v_max_threads; i++) {  
                threads.emplace_back(processdata);  
           }  
           // Finish all threads  
           for (auto& t: threads) {  
                t.join();  
           }  
           threads.clear();  
      }  
 /*-------------Main Loop----------------*/  
   

Вариант III

 // If max threads not specified, set default  
 std::thread* threads = new std::thread[v_max_threads];  
 /*-------------Main Loop----------------*/  
      while (!std::cin.eof()) {  
           // Start all processing threads  
           for (unsigned int i = 0; i < v_max_threads; i++) {  
                threads[i] = std::thread([&]() {  
                     processdata();  
                     });  
           }  
           // Finish all threads  
           for (unsigned int i = 0; i < v_max_threads; i++) {  
                threads[i].join();  
           }  
      }  
 /*-------------Main Loop----------------*/  
 delete [] threads;  
   

Сдаетесь? :)

Профайлер говорит (и сравнительный бенчмарк подтверждает): "скорость практически одинакова, плюс-минус миллисекунда".

При этом вариант II генерирует примерно на 1 Кб больше кода (в байтах). (Компилятор GCC 5.2 в режиме -std=c++11, разумеется, режим оптимизации -O3). Вариант III совпадает до байта по генерации кода с вариантом I, который в C++ возможен, однако опасен (компилятор не во всяком режиме вообще позволит собрать такое). При этом, вариант III еще небезопасен тем, что возможны утечки памяти, если произойдет исключение до оператора delete [] .

Соответственно, из вышеперечисленных соображений "правила пальца" выбираем предпочтительным вариант II. :)

PS. Предварительная оптимизация - корень всех зол, да. Любим, помним, скорбим. :)

понедельник, 2 января 2017 г.

Оптимизация Squid 3+: Часть I

Введение

В прошлом я уже писал на эту тему. Однако с тех пор прошла вечность, мир изменился, продукт, о котором идет речь, изменился. Приобретен некоторый опыт, изучена масса исходников.

С учетом вышесказанного, возникла необходимость обобщить этот опыт и знания и немного поделиться им. 

Как и во всех случаях, дисклеймер:

Волшебной пули не существует.

Оптимизация Squid, как и любая другая, требует знаний архитектуры, понимания того, как работают все составные части - включая операционную систему - и кропотливой последовательной работы.

По-другому, увы, не получится. Копипастить конфиги без понимания - как это вообще весьма свойственно гнуторасам и эникеям - не получится тоже. Оптимизация - это всегда подгонка под одну, весьма конкретную, конфигурацию и железо. Лишь архитектура более-менее постоянна.

Начиная оптимизацию, вы должны все это понимать и четко отдавать себе отчет, что вы можете достичь, а чего нет. А также - когда следует остановиться. 

Оптимизация - вещь практически бесконечная. Не зададите условия выхода из цикла - не остановитесь никогда.

Итак, постараюсь связно изложить все, что знаю.

Говоря об оптимизации сабжа, следует выделить три аспекта работы, которые могут быть выполнены как по отдельности, так и все вместе:

  1. Функциональная оптимизация. Иными словами - все должно работать по-возможности правильно. Стабильность входит сюда же, если система работает нестабильно или требует постоянного внимания человека - все остальное не имеет значения.
  2. Оптимизация производительности. Сюда входит время отклика, отсутствие перегрузок CPU, эффективность используемых хелперов и некоторые другие показатели.
  3. Оптимизация byte hit. По принципу - чем выше, тем эффективней работает кэш. Ранее я уже объяснял, почему именно этот показатель имеет значение.

Для понимания всего вышесказанного, следует начать с архитектуры Squid 3+.


Архитектура

Первое, что вам необходимо помнить.

Squid был написан во времена, когда CMT машин не существовало. Это означает, что, за очень немногим исключением (aufs), в нем нет поддержки потоков. Это однопоточный продукт, который в лучшем случае - и частично - может функционировать в режиме SMP. То есть мультипроцессирования.

Таким образом, выполнение практически любых этапов обработки HTTP(S)-запросов в нем выполняется сериализованно. Таким образом, чем больше функциональных возможностей вы задействуете, тем хуже будет производительность и тем выше будет загрузка CPU.

Чтобы сгладить однопоточность, основная работа по обработке запросов выполняется процессом-координатором и некоторым количеством дочерних процессов. Кроме того, имеются ограниченные возможности по выполнению запросов прокси-серверм в режиме pipeline, и некоторые другие возможности по оптимизации.

Отсутствие полноценной поддержки CMT (причем разработчики устно заявляют, что ее и не планируется в обозримом будущем) также означает, что продукт достаточно неудовлетворительно (за немногими весьма специфичными исключениями) работает в виртуализированных средах (VMWare, итп.) и следует предпочитать его выполнение на bare metal для достижения приемлемых показателей.

Физически режим SMP в Squid существует, однако он реализован не в полном объеме, имеет большое количество ограничений реализации и для полноценного применения в крупных инсталляциях фактически непригоден. Он завязан на использование rock, в реальности ограниченной на работу с мелкими файлами, в 2016 году это серьезное ограничение; также режим требует большого количества тюнинговых операций, кроме того, не все данные могут быть эффективно поделены между SMP-обработчиками, начиная с кэша в оперативной памяти.

Использование оперативной памяти

Память cache_mem используется для хранения наиболее горячих объектов не слишком большой величины. Существует распространенное заблуждение, что, если собрать Squid в 64 битах, и если сервер имеет много RAM, то cache_mem можно задавать сколь угодно неограниченно большим.

Это ошибка.

Во-первых, исторически Squid не рассчитан на гигантский кэш в оперативной памяти. На большинстве систем адресация пространства свыше 4 Гб вызывает проблемы или обложена массой ограничений. Кроме того, нубы ошибочно считают, что гигантский кэш в памяти дает гигантский выигрыш. Это не так. После определенного объема скорость произвольного доступа падает, и, в какой-то момент, становится меньше, чем у жесткого диска. Парадокс большого кэша. Сам Squid внутри себя не содержит эффективных механизмов адресации действительно больших объемов оперативной памяти.

Во-вторых, Squid не выполняет никаких проверок при инициализации кэша. Совсем. Вы можете обнаружить проблему с адресацией не сразу, а после некоторого времени нормальной работы. Когда кэш просто падает с дампом на полном скаку.

В-третьих, большинство системных библиотек управления памятью на ОС, произросших из настольных систем x86, физически не предусматривают, что сервер может иметь память больше, чем 4-8 Гб. Что означает непредсказуемое поведение в любой момент времени.

Не надо считать разработчиков геями-инопланетянами. У пианистов просто не всегда есть 48-юнитная стойка, переполненная процессорами и модулями памяти. Точнее сказать, такой стойки почти никогда нет.

Второе распространенное заблуждение заключается в том, что для быстрой работы надо задавать большие пулы памяти (параметры memory_pools, memory_pools_limit). 

Делается это от незнания того, для чего, собственно, эти пулы используются. Это вспомогательный хламовник, используемый для процессинга запросов. Фактически на некоторых ОС память, распределяемая системным аллокатором, выделяется слишком мелкими фрагментами и, в процессе работы, ее количество уменьшается со временем. Пулы позволяют кэшу выделить себе относительно непрерывное пространство под heap, и удерживать его выделенным (подобно TEMP). Если ваша ОС - кустарное самодельное дерьмо - это может быть лучшей стратегией, чем позволить ОС самой выделять и освобождать память. Но не всегда. И уж, без сомнения, не следует выделять пулы гигабайтами, как cache_mem. Размер memory_pools_limit тщательно тюнится пропорционально нагрузке прокси.

Хорошим начальным приближением для memory_pools_limit является 1/5-1/10 от размера cache_mem с последующим тюнингом по месту и нагрузке. Не более, и уж, конечно, не сообразно размеру cache_mem.

Вопрос, который задают все до единого нубы и почти все полевые админы:

- Почему Squid жрет так много оперативной памяти (top показывает величину, в разы превосходящую cache_mem) и этот расход памяти постоянно увеличивается?

Ответ: Память, потребляемая прокси, складывается из следующих элементов:
  1. Собственно память, необходимая для запуска и выполнения процесса-координатора и всех кидов, включая процессы IO.
  2. cache_mem
  3. memory_pools_limit - причем следует помнить, что этот лимит может быть кратковременно превышен
  4. Кэширование индексных файлов всех cache_dir (swap.state). Обычно больше, чем совокупный объем всех swap.state-файлов. Что, в общем_ естественно. Именно за счет этой части происходит прирост потребляемой оперативной памяти. Кэш (а Squid - в первую очередь именно кэш, что бы ни заявляли сейчас его разработчики) по умолчанию пытается любой новый файл прежде всего сохранить на диске. Соответственно, постоянно добавляются новые entries в индексы, и, соответственно, их надо кэшировать в оперативной памяти, чтобы дисковый IO не был совсем уж удручающе медленным и печальным. Как говорит документация, в среднем соотношение составляет 15-17 мегабайт на каждый гигабайт потребленного дискового пространства. В действительности эти цифры весьма ориентировочны, и зависят от массы факторов, как самой ОС и fs, так и параметров собственно Squid. Вам следует иметь в виду эти расчеты при выборе размеров дискового хранилища под кэш и ограничителей размеров cache_dir, которые косвенно влияют на величину потребной оперативной памяти. Нубы часто ставят терабайтные диски и пытаются превратить кэш в arcive.org. И еще быстрой работы ожидают. Это огромная ошибка. У вас мочи не хватит сделать архив всего интернета. Не сейчас. Не в 2016 году.
Еще одна распространенная ошибка нубов, это установка параметра maximum_object_size_in_memory в высокие величины. Не делайте этого. Вы просто выбросите огромные куски оперативной памяти под гигантские файлы, которые, как правило, редко запрашиваются. Производительности это не добавит, более того, это приведет к проблемам, причем очень и очень скоро.

Не в качестве руководства к действию и не для бездумного копирования приведу фрагмент конфигурации оптимизированного кэша:

 # -------------------------------------  
 # Memory parameters  
 # -------------------------------------  
 cache_mem 512 Mb  
   
 memory_pools_limit 50 MB  
   
 maximum_object_size_in_memory 1 MB  
   
 # -------------------------------------  
 # Store parameters  
 # -------------------------------------  
 minimum_object_size 10 bytes  
 maximum_object_size 4 Gb  
   
 cache_dir diskd /data/cache/d1 32767 64 512  
 cache_dir diskd /data/cache/d2 32767 64 512  
 cache_dir diskd /data/cache/d3 32767 64 512  
 cache_dir diskd /data/cache/d4 32767 64 512  
   
 # Default is 20  
 store_objects_per_bucket 32  
   
 store_avg_object_size 200 KB  
   

Машина, на которой задана такая конфигурация, имеет 8 ядер CPU, 4 Гб оперативной памяти, и диски SAS 15K RPM. Приведенные выше параметры оптимальны. Говоря простым языком: хватамбо! Как видите, под дисковый кэш выделено 128 Гб. Для нагрузки порядка 500 пользователей этого более, чем достаточно - с учетом специфики нагрузки данного конкретного сервера. Я намеренно не буду вдаваться в детали этой нагрузки и остальных настроек, так как у вас все равно будет все иначе.

Еще раз повторюсь: не надо бездумно задавать гигантские параметры оперативной памяти безотносительно всех остальных и надеяться на отличные результаты. Даже если у вас в руках огромная - по вашим меркам - железка и вы считаете, что можете себе позволить почти все, что угодно. В дальнейшем я объясню, под что действительно лучше расходовать ресурсы железа.