среда, 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 pools. Это куча, которую Сквид использует в своих целях: для парсинга запросов, при выполнении операций. Обычно, большинство администраторов не задумываясь задают пул гигантского размера, соизмеримый с cache_mem. Порядка гигабайт. А потом жалуется что памяти нет и начался свопинг. Пулы отключают, и обнаруживают, что память начала "течь", как им кажется. Так вот. Зарубите на носу. Размер пулов памяти пропорционален количеству обрабатываемых запросов в единицу времени. Для средних размеров инсталляции с ~300 клиентских станций за глаза достаточно 50-150 мегабайт пула. Не более. Если пул отключить с целью экономии памяти - начинается trashing memory. То есть память кучи очень сильно фрагментируется и по факту выглядит так, будто память течет - там просто невозможно что-либо выделить, приходится наращивать размер кучи. Об этом сказано и в squid.conf.documented. Таким образом, пул нужен  - не надо лишь делать его чрезмерно большим по принципу "памяти много не бывает". Вы плохо себе представляете, что такое куча размером 8 гигабайт.

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.

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

Преамбула
Если вы этого до сих пор не знаете, то вам следует это знать. Принципиальная работоспособность ACL на основе регулярных выражений.
Опытным путем мы с коллегой выяснили, что Squid в ACL поддерживает в подавляющем большинстве случаев грамматику POSIX Basic. Изредка POSIX Extended. И все. Никаких ECMAScript и в помине нет. При этом текущие разработчики сами затрудняются определить, какую в точности грамматику использует их софт, туманно ссылаются на системные библиотеки (ввиду того, что идет активное переписывание Squid на C++11, это, вероятно, следует понимать как "Squid использует по умолчанию STL regex" - однако STL Regex по умолчанию использует грамматику ECMAScript, а мы уже выяснили, что этой грамматикой и не пахнет), кроме того, поведение Squid's regex абсолютно недокументировано (ответ разработчиков - "Да, мы знаем - но времени нет и делать мы этого не будем" - читай: "Не хотим") - как, впрочем, почти весь OpenShitOpenSource. Читайте исходники (с сарказмом). Если вы этого не умеете или не хотите - это ваши личные проблемы.

Шутки в сторону. POSIX Basic - и ваши конфигурации будут работать практически везде.

Вернемся, однако, к производительности.

Как и в случае 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 достаточно в обычных (не слишком крупных) конфигурациях. Опыт показал, однако, что в случае использования Squid ветки 5.x, имеет смысл несколько увеличить данный параметр (до значения 8192), так как это повышает ipcache hit и несколько ускоряет выполнение запросов.

Гораздо лучше разместить высокоскоростной рекурсор (например, 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;  
   
 }  
   

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