Боюсь потерять записи сделанные на последней конференция разработчиков высоконагруженных систем (Highload 2008) поэтому напишу сюда. Лучше поздно чем никогда, да и основные концепции останутся неизменными еще очень долго.
Это будут основные тезисы и местами мои комменты.
Среди присутствующих на конференции хочу отметить следующие личности: Игорь Сысоев (ведущий, nginx, поклонник мультиплексинга ввода/вывода для решения проблемы C10K), Алексей Тутубалин (русский Апач, сидел в зале и частенько делал едкие замечания), Анатолий Орлов (Яндекс, отличный спец по специфике железа), Филипп Дельгядо (Яндекс, отличный project-менеджер и системный архитектор), Антон Самохвалов (Яндекс, программер, базовый поиск, ярый фанат 10000 потоков на фрюхе как ответ на проблему C10K, много спорил с Сысоевым).
Сразу скажу, я поддерживаю Сысоева, это действительно работает, работает быстро и здравый смысл говорит что так и должно быть (хотя при первом рассмотрении может показаться что это не так). Многопоточное программирование хорошо, но не везде. Одно верно программа проще :)
Как сказал один великий человек (не помню кто): многопоточное программирование для тех, кто не умеет программировать конечные автоматы.
Итак погнали, идеи не мои, а уважаемых людей, материал в основном для программистов и архитекторов (в основном Си/С++ и в основном не Windows):
- Обращаем внимание на load average в top`е, это количество конкурирующих потоков за процессорное время. Если цифры большие - все плохо даже если top показывает низкую загрузку процессора (бывает и так).
- Пользуемся iostat из sysstat и похожими инструментами для выявления узких мест про процессе эксплуатации софта.
- Процессор по своей сути последовательное устройство и механизм многозадачности на уровне операционной системы может не оптимально подходить к решению ваших задач, поэтому лучше выстраивать таски в очередь (4 ядра - 4 очереди) и выполнять последовательно. Пример - есть 5 задач, каждая выполняется 1 секунду. Если запустить их на выполнение одновременно велика вероятность того, что каждая из них в результате отработает примерно за 5 секунд с учетом конкуренции. Если выстроить их в очередь, то получим следующее: первая гарантированно отработает за секунду, вторая за 2, третья за 3 и т.д. В итоге в условиях конкуренции хоть кто-то будет удовлетворен быстрее.
- Не рекомендуется мешать разнородные задачи в рамках одной машины (база данных должна быть базой данных, а Web-сервер Web-сервером - очень разная специфика у задач и разная конфигурация железа в идеале).
- Данные и код в ОЗУ могут кэшироваться процессором, если научиться подстраиваться под этот процесс можно добиться сильного увеличения производительности. Например, первым после 'if' лучше ставить блок вероятность исполнения которого выше.
- При работе с диском поменьше seek`ов! Линейная скорость чтения/записи на порядки выше скорости позиционирования головок (readahead).
- При возникновении ошибок в процессе эксплуатации библиотеки OpenSSL необходимо выбирать их из стека ошибок в цикле (пока не кончатся), если надеяться на то, что она там одна можно получить неприятные сюрпризы.
- идеальная схема сервера приложений: мультиплексинг ввода/вывода (epoll, kqueue, как вариант libevent) в одном или нескольких потоках/процессах для общения с медленными клиентами, далее очередь, далее несколько обработчиков в отдельных потоках/процессах (для использования возможностей SMP). Медленный клиент не будет впустую занимать обработчик долгой отправкой запроса/чтением ответа. За время ввода/вывода обработчик успеет выполнить кучу запросов. Количество обработчиков можно прикинуть исходя из формулы: кол-во ядер + кол-во дисков + 5 :)
- Правильные пацаны смотрят в сторону FreeBSD 7
- Многоядерный/многопроцессорный сервер не панацея - ядра будут упираться в шину памяти. Лучше собрать кластер из нескольких дешевых машин чем покупать дорогой 16-и ядерник (от себя добавлю, возможны самые последние варианты: 16-ти ядерник с VMWare и кучей гостевых ОС на борту :) ).
- При тестировании софта используем профайлеры.
- Не забываем о страничной адресации памяти, если угадали и наши данные попали в одну страницу получаем прирост производительности.
- Двухзвенные системы для серьезных проектов не годятся (это когда как можно больше всего стараются впихнуть в базу данных) - не масштабируются.
- Надо иметь как минимум 3 независимых датацентра :).
- Для выживания бизнеса после серьезной аварии необходимы регулярные бэкапы и подробные логи.
- Как можно больше всего кэшировать вне базы снимая с нее нагрузку. Масштабировать базу гораздо сложнее чем сервер приложений. База должна использоваться только для длительного хранения (persist).
- Необходимо добиться от системы возможности горизонтального масштабирования - это когда можно тупо воткнуто в стойку еще один юнит и получить еще производительности. Об этом стоит задумываться уже на этапе проектирования системы!
- Лучше использовать stateless протоколы. Это снимет нагрузку с базы/кэша при хранении промежуточных данных (типа сессий PHP). Хорошо подойдут средства криптографии. Можно заставить клиента возвращать обратно что-то (промежуточные данные), что вы вернули на предыдущем шаге и проверять валидность этого, например, при помощи MD5 или RSA.
- memcached сила, последние версии используют libevent. Основное достоинство - отсутствие блокировок при конкуренции!
- При использовании кэша надо позаботиться о контроле версий того, что там лежит. Может получиться так, что софт поменяется, а в кэше останутся данные в старом формате.
- База данных масштабируется плохо, поэтому лучше как можно больше выносить на сервера приложений.
- Если в базе данных появились тригеры - значит у вас в архитектуре уже что-то не так :)
- Для масштабирования баз данных необходимо придумать механизм распределения контента между базами и алгоритм поиска нужной базы по входным данным (Database sharding).
- Java - идеальный вариант для стартапа, из-за своей ущербности она на даст вам накосячить :)
- DB2 - предпочтительнее Oracle т.к. сейчас развилось множество быдлоспециалистов по последнему. DB2шников найти сложнее, но уровень у них будет выше.
- Автоматические тесты софта - наше все.
- INSERT лучше UPDATE!
- База данных должна быть простой.
- Возможность сериализации внутренних структур программы для хранения в БД или кэше.
- Асинхронная обработка данных лучше синхронной, если есть выбор - выбираем первое.
- В лог пишем как можно больше всего, логи должны быть удобными для машинного разбора (поможет выжить).
- Необходима возможность постоянного мониторинга состояния узлов системы.
- Не забываем о системах контроля версий (SVN,CVS).
- Длинные транзакции в БД - зло!
- Люди - дорого, железо - дешево!
- Все базы INSERT делают одинаково, так что и MySQL по большому счету может сделать тот, же Oracle.
- fork очень дорог.
- Не забываем про TCP_NODELAY и TCP_CORK.
- Нельзя доверять операционной системе, если что-то от нее получили - не отдавайте (например, память или поток... используем механизм пулов).
- Как можно меньше копирований, это сожрет всю производительность, используйте ссылки (zerocopy)!
- Кэшируем-кэшируем и еще раз кэшируем.
- Говорят на FreeBSD потоки реализованы лучше чем в Linux.
- Местами Windows показывает себя очень хорошо в плане производительности, выбирайте ту ОС, где оно работает лучше.
- Как можно меньше блокировок, FSM - наш путь :).
От себя добавлю, STL это сила но надо думать головой когда выбираешь контейнер или что-то делаешь. Стоит обратить внимание на такие методы контейнеров как reserve (избегаем realloc`ов) и swap (избегаем копирований).
Следующий код может сильно ускорить процесс в сравнении с более лаконичным list.pash_back(s)
list.push_back(std::string());
list.back().swap(s)
В критических местах вообще не рекомендуют использовать STL из-за ее непредсказуемости в плане работы с памятью.
Поэтому собственные аллокаторы, контейнеры и вперед к звездам.
Вот интересная статья про оптимизацию программ с STL.
К вопросу о многопроцессном программировании - оно подкупает надежностью программы из-за полной изоляции потоков исполнения, но может сильно усложнить жизнь и сделать невозможной реализацию концепции zerocopy из-за необходимости межроцессного взаимодействия (все что может нас спасти - разделяемая память). Поэтому тут верный путь - потоки.
Еще надо стремиться к тому, что бы производительность сервера не зависила от количества клиентов, так называемый O(1) (O от единицы). Например, epoll в линуксе соответствует этому требованию, а старые select и pool - нет.
Возьмете за основу select и привет, O(1) не видать :) Если все отдельные алгоритмы удовлетворяют данному требованию можно утверждать что эффективность всей программы стремится к O(1). Среди узких мест могу отметить память и таймеры, надо искать подходящие алгоритмы.
Последний параметр (backlog) системного вызова listen очень важен, в серьезных приложениях стандартных 5 не хватит, а вот 50 самое то :)
Не забываем об оптимизации компилятора, например для gcc подойдут ключи '-O2' и '-O3', только внимательнее... могут быть сюрпризы если код не совсем корректен. Если у нас есть сигналы, разделяемая память или потоки обязательно использовать ключевое слово volatile.
Хотел сказать - Core 2 Duo очень крут, сам лично наблюдал как старый дорогой Xeon 3.6 Ггц слил Core 2 Duo E8400 3.0 Ггц (15000 запросов в секунду на одном ядре против 10000).
Перед Web-сервером общего назначения (например, Apache) обязательно выставлять реверсный прокси, например, nginx. Это снимит с "дорогих" воркеров бремя чтения/записи при общении с клиентом. Может получиться так, что это будет отнимать времени больше чем полезная работа + это отличная дырка для DOS атак.
И на последок... CGI это зло и пережиток прошлого - fork`и, пайпы и множественные копирования делают свое дело. На крайний случай FastCGI, а лучше модули расширения Web-сервера... если используем динамические языки для Web-программирования, то PHP, mod_perl или даже luasp - наш путь.
Это будут основные тезисы и местами мои комменты.
Среди присутствующих на конференции хочу отметить следующие личности: Игорь Сысоев (ведущий, nginx, поклонник мультиплексинга ввода/вывода для решения проблемы C10K), Алексей Тутубалин (русский Апач, сидел в зале и частенько делал едкие замечания), Анатолий Орлов (Яндекс, отличный спец по специфике железа), Филипп Дельгядо (Яндекс, отличный project-менеджер и системный архитектор), Антон Самохвалов (Яндекс, программер, базовый поиск, ярый фанат 10000 потоков на фрюхе как ответ на проблему C10K, много спорил с Сысоевым).
Сразу скажу, я поддерживаю Сысоева, это действительно работает, работает быстро и здравый смысл говорит что так и должно быть (хотя при первом рассмотрении может показаться что это не так). Многопоточное программирование хорошо, но не везде. Одно верно программа проще :)
Как сказал один великий человек (не помню кто): многопоточное программирование для тех, кто не умеет программировать конечные автоматы.
Итак погнали, идеи не мои, а уважаемых людей, материал в основном для программистов и архитекторов (в основном Си/С++ и в основном не Windows):
- Обращаем внимание на load average в top`е, это количество конкурирующих потоков за процессорное время. Если цифры большие - все плохо даже если top показывает низкую загрузку процессора (бывает и так).
- Пользуемся iostat из sysstat и похожими инструментами для выявления узких мест про процессе эксплуатации софта.
- Процессор по своей сути последовательное устройство и механизм многозадачности на уровне операционной системы может не оптимально подходить к решению ваших задач, поэтому лучше выстраивать таски в очередь (4 ядра - 4 очереди) и выполнять последовательно. Пример - есть 5 задач, каждая выполняется 1 секунду. Если запустить их на выполнение одновременно велика вероятность того, что каждая из них в результате отработает примерно за 5 секунд с учетом конкуренции. Если выстроить их в очередь, то получим следующее: первая гарантированно отработает за секунду, вторая за 2, третья за 3 и т.д. В итоге в условиях конкуренции хоть кто-то будет удовлетворен быстрее.
- Не рекомендуется мешать разнородные задачи в рамках одной машины (база данных должна быть базой данных, а Web-сервер Web-сервером - очень разная специфика у задач и разная конфигурация железа в идеале).
- Данные и код в ОЗУ могут кэшироваться процессором, если научиться подстраиваться под этот процесс можно добиться сильного увеличения производительности. Например, первым после 'if' лучше ставить блок вероятность исполнения которого выше.
- При работе с диском поменьше seek`ов! Линейная скорость чтения/записи на порядки выше скорости позиционирования головок (readahead).
- При возникновении ошибок в процессе эксплуатации библиотеки OpenSSL необходимо выбирать их из стека ошибок в цикле (пока не кончатся), если надеяться на то, что она там одна можно получить неприятные сюрпризы.
- идеальная схема сервера приложений: мультиплексинг ввода/вывода (epoll, kqueue, как вариант libevent) в одном или нескольких потоках/процессах для общения с медленными клиентами, далее очередь, далее несколько обработчиков в отдельных потоках/процессах (для использования возможностей SMP). Медленный клиент не будет впустую занимать обработчик долгой отправкой запроса/чтением ответа. За время ввода/вывода обработчик успеет выполнить кучу запросов. Количество обработчиков можно прикинуть исходя из формулы: кол-во ядер + кол-во дисков + 5 :)
- Правильные пацаны смотрят в сторону FreeBSD 7
- Многоядерный/многопроцессорный сервер не панацея - ядра будут упираться в шину памяти. Лучше собрать кластер из нескольких дешевых машин чем покупать дорогой 16-и ядерник (от себя добавлю, возможны самые последние варианты: 16-ти ядерник с VMWare и кучей гостевых ОС на борту :) ).
- При тестировании софта используем профайлеры.
- Не забываем о страничной адресации памяти, если угадали и наши данные попали в одну страницу получаем прирост производительности.
- Двухзвенные системы для серьезных проектов не годятся (это когда как можно больше всего стараются впихнуть в базу данных) - не масштабируются.
- Надо иметь как минимум 3 независимых датацентра :).
- Для выживания бизнеса после серьезной аварии необходимы регулярные бэкапы и подробные логи.
- Как можно больше всего кэшировать вне базы снимая с нее нагрузку. Масштабировать базу гораздо сложнее чем сервер приложений. База должна использоваться только для длительного хранения (persist).
- Необходимо добиться от системы возможности горизонтального масштабирования - это когда можно тупо воткнуто в стойку еще один юнит и получить еще производительности. Об этом стоит задумываться уже на этапе проектирования системы!
- Лучше использовать stateless протоколы. Это снимет нагрузку с базы/кэша при хранении промежуточных данных (типа сессий PHP). Хорошо подойдут средства криптографии. Можно заставить клиента возвращать обратно что-то (промежуточные данные), что вы вернули на предыдущем шаге и проверять валидность этого, например, при помощи MD5 или RSA.
- memcached сила, последние версии используют libevent. Основное достоинство - отсутствие блокировок при конкуренции!
- При использовании кэша надо позаботиться о контроле версий того, что там лежит. Может получиться так, что софт поменяется, а в кэше останутся данные в старом формате.
- База данных масштабируется плохо, поэтому лучше как можно больше выносить на сервера приложений.
- Если в базе данных появились тригеры - значит у вас в архитектуре уже что-то не так :)
- Для масштабирования баз данных необходимо придумать механизм распределения контента между базами и алгоритм поиска нужной базы по входным данным (Database sharding).
- Java - идеальный вариант для стартапа, из-за своей ущербности она на даст вам накосячить :)
- DB2 - предпочтительнее Oracle т.к. сейчас развилось множество быдлоспециалистов по последнему. DB2шников найти сложнее, но уровень у них будет выше.
- Автоматические тесты софта - наше все.
- INSERT лучше UPDATE!
- База данных должна быть простой.
- Возможность сериализации внутренних структур программы для хранения в БД или кэше.
- Асинхронная обработка данных лучше синхронной, если есть выбор - выбираем первое.
- В лог пишем как можно больше всего, логи должны быть удобными для машинного разбора (поможет выжить).
- Необходима возможность постоянного мониторинга состояния узлов системы.
- Не забываем о системах контроля версий (SVN,CVS).
- Длинные транзакции в БД - зло!
- Люди - дорого, железо - дешево!
- Все базы INSERT делают одинаково, так что и MySQL по большому счету может сделать тот, же Oracle.
- fork очень дорог.
- Не забываем про TCP_NODELAY и TCP_CORK.
- Нельзя доверять операционной системе, если что-то от нее получили - не отдавайте (например, память или поток... используем механизм пулов).
- Как можно меньше копирований, это сожрет всю производительность, используйте ссылки (zerocopy)!
- Кэшируем-кэшируем и еще раз кэшируем.
- Говорят на FreeBSD потоки реализованы лучше чем в Linux.
- Местами Windows показывает себя очень хорошо в плане производительности, выбирайте ту ОС, где оно работает лучше.
- Как можно меньше блокировок, FSM - наш путь :).
От себя добавлю, STL это сила но надо думать головой когда выбираешь контейнер или что-то делаешь. Стоит обратить внимание на такие методы контейнеров как reserve (избегаем realloc`ов) и swap (избегаем копирований).
Следующий код может сильно ускорить процесс в сравнении с более лаконичным list.pash_back(s)
list.push_back(std::string());
list.back().swap(s)
В критических местах вообще не рекомендуют использовать STL из-за ее непредсказуемости в плане работы с памятью.
Поэтому собственные аллокаторы, контейнеры и вперед к звездам.
Вот интересная статья про оптимизацию программ с STL.
К вопросу о многопроцессном программировании - оно подкупает надежностью программы из-за полной изоляции потоков исполнения, но может сильно усложнить жизнь и сделать невозможной реализацию концепции zerocopy из-за необходимости межроцессного взаимодействия (все что может нас спасти - разделяемая память). Поэтому тут верный путь - потоки.
Еще надо стремиться к тому, что бы производительность сервера не зависила от количества клиентов, так называемый O(1) (O от единицы). Например, epoll в линуксе соответствует этому требованию, а старые select и pool - нет.
Возьмете за основу select и привет, O(1) не видать :) Если все отдельные алгоритмы удовлетворяют данному требованию можно утверждать что эффективность всей программы стремится к O(1). Среди узких мест могу отметить память и таймеры, надо искать подходящие алгоритмы.
Последний параметр (backlog) системного вызова listen очень важен, в серьезных приложениях стандартных 5 не хватит, а вот 50 самое то :)
Не забываем об оптимизации компилятора, например для gcc подойдут ключи '-O2' и '-O3', только внимательнее... могут быть сюрпризы если код не совсем корректен. Если у нас есть сигналы, разделяемая память или потоки обязательно использовать ключевое слово volatile.
Хотел сказать - Core 2 Duo очень крут, сам лично наблюдал как старый дорогой Xeon 3.6 Ггц слил Core 2 Duo E8400 3.0 Ггц (15000 запросов в секунду на одном ядре против 10000).
Перед Web-сервером общего назначения (например, Apache) обязательно выставлять реверсный прокси, например, nginx. Это снимит с "дорогих" воркеров бремя чтения/записи при общении с клиентом. Может получиться так, что это будет отнимать времени больше чем полезная работа + это отличная дырка для DOS атак.
И на последок... CGI это зло и пережиток прошлого - fork`и, пайпы и множественные копирования делают свое дело. На крайний случай FastCGI, а лучше модули расширения Web-сервера... если используем динамические языки для Web-программирования, то PHP, mod_perl или даже luasp - наш путь.