Что замедляет работу Apache и как получить максимум от PHP? Обработка файлов.xls с помощью Apache POI - Блог.

10.05.2019

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

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

Зависнет сервер или нет зависит от технических характеристик и настроек самого сервера, сайта и используемой CMS. В этой статье я опишу как правильно настроить вэб-сервер Apache2 чтобы даже самый скромный сервер VDS с 512 МБ памяти справлялся с большим количеством запросов, в том числе вызванных клавишей F5 .

Подготовка к настройке сервера

Определим какой модуль MPM использует Apache2. Для CentOS это выглядит так:

# httpd -V | grep MPM

Получаем ответ:

Server MPM: Prefork -D APACHE_MPM_DIR="server/mpm/prefork"

Отлично, у нас Server MPM: Prefork

Настройка Apache, что бы не было проблем с жором памяти

В CentOS надо отредактировать файл /etc/httpd/conf/httpd.conf .

Который по-умолчанию может иметь следующее содержание:

StartServers 8 MinSpareServers 5 MaxSpareServers 20 ServerLimit 256 MaxClients 200 MaxRequestsPerChild 4000

Краткое описание параметров модуля Apache MPM Prefork

  • StartServers — число дочерних процессов, создаваемых при запуске сервера.
  • MinSpareServers — минимальное число неиспользуемых (запасных) дочерних процессов сервера, ожидающих потенциальные запросы.
  • MaxSpareServers - максимальное число запасных процессов, ожидающих потенциальные запросы. Если это число будет превышено, лишние процессы будут убиты.
  • MaxClients - самый важный параметр модуля MPM prefork, устанавливает верхний предел количества одновременно активных процессов. Именно от него зависит потребление памяти. Его значение перекрывает значение предыдущих параметров.
  • ServerLimit обычно равен MaxClients.
  • MaxRequestsPerChild — как часто сервер перерабатывает процессы, убивая старые и начиная (запуская) новые. Полезен при утечках памяти Apache и его библиотек.
  • KeepAlive — обеспечивает долгоживущие сессии HTTP, позволяющие отправлять несколько запросов через одно и то же соединение. Полезно включить, если страницы содержат много изображений. Но если используете NGINX как проксисервер, то оставьте значение OFF .

Самый важный параметр = MaxClients , он как раз и говорит о количестве одновременных процессов вебсервера Apache.

Как узнать значение MaxClients

Определить его значение не сложно. Расчет значения приведу для сервера с размером оперативной памяти 512 МБ. Решаем, что отдаем для ресурсов Apache 50% оперативной памяти, то есть в нашем случае 256 МБ.

Определяем сколько памяти отжирает один процесс:

# ps -ylC httpd | awk "{x += $8;y += 1} END {print "Average Proccess Size (MB): "x/((y-1)*1024)}"

Получаем следующие значения:

Average Proccess Size (MB): 21.5185

Получается, что в среднем один процесс Apache потребляет 21 МБ. Соответственно в отведенном объеме 256 МБ мы можем запустить 12 процессов.

Исправим файл конфигурации под новое значение:

StartServers 3 MinSpareServers 3 MaxSpareServers 9 ServerLimit 256 MaxClients 12 MaxRequestsPerChild 3000

Модуль Apache MPM расшифровывается как Apache Multi-Processing Module, что в переводе означает «Модуль мультипроцессовой обработки». Обычно по-умолчанию в Apache используется модуль MPM prefork .

Определить, какой именно менно модуль Apache MPM используется, можно следующей командой:

Httpd -V | grep mpm -D APACHE_MPM_DIR=»server/mpm/prefork»

Или на системах, подобных Debian, где сервер называется apache2 :

Apache2 -V | grep mpm -D APACHE_MPM_DIR="server/mpm/prefork"

Рассмотрим настройку параметров модуля Apache MPM prefork , исходя из объема оперативной памяти на хосте. Определим средний размер памяти, занимаемый одним процессом Apache:

Ps -ylC httpd | awk "{x += $8;y += 1} END {print "Apache Memory Usage (MB): \ "x/1024; print "Average Proccess Size (MB): "x/((y-1)*1024)}"

На системах, где сервер Apache представлен демоном apache2 , замените в строке httpd на apache2 .

Команда покажет общий объем памяти, потребляемой всеми процессами Apache и средний объем памяти на один процесс. Примеры:

В дистрибутивах, подобных Debian:

User@debian:~$ ps -ylC apache2 | awk "{x += $8;y += 1} \ END {print "Apache Memory Usage (MB): "x/1024; \ print "Average Proccess Size (MB): "x/((y-1)*1024)}" Apache Memory Usage (MB): 231.531 Average Proccess Size (MB): 13.6195

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

Теперь, зная средний объем памяти, используемый сервером Apache, и зная объем физической памяти, можно вычислить значение MaxClients , которое задается в файле конфигурации сервера Apache httpd.conf .

Допустим, на вашем VPS или VDS сервере 1 Гб оперативной памяти, и вы хотите оставить 512 Мб для остальных процессов, отдав серверу Apache 512 Мб.

Предыдущая команда выдала результаты:

# ps -ylC httpd | awk "{x += $8;y += 1} \ END {print "Apache Memory Usage (MB): "x/1024; \ print "Average Proccess Size (MB): "x/((y-1)*1024)}" Apache Memory Usage (MB): 64.3789 Average Proccess Size (MB): 10.7298

Т.е. на один процесс Apache в среднем уходит 10 Мб памяти. Определим значение MaxClients:

MaxClients = Весь объем памяти для Apache / Объем памяти на один процесс

MaxClients = 512 Мб / 10 МБ = 50.

Теперь мы знаем самое важное значение параметра модуля Apache MPM prefork , задающее максимальное число дочерних процессов таким, чтобы не была "съедена" вся оперативная память, а только часть ее (в нашем примере - половина, равная 512 Мб).

Внесем данные в файл настроки модуля Apache MPM prefork, обычно располагающийся по пути /etc/httpd/conf/httpd.conf :

StartServers 2 MinSpareServers 2 MaxSpareServers 5 MaxClients 50 ServerLimit 50 MaxRequestsPerChild 100 KeepAlive Off

Краткое описание параметров модуля Apache MPM Prefork:

StartServers - число дочерних процессов, создаваемых при запуске сервера.

MinSpareServers - минимальное число неиспользуемых (запасных) дочерних процессов сервера, ожидающих потенциальные запросы.

MaxSpareServers — максимальное число запасных процессов, ожидающих потенциальные запросы. Если это число будет превышено, лишние процессы будут убиты.

MaxClients — самый важный параметр модуля MPM prefork, устанавливает верхний предел количества одновременно активных процессов. Именно от него зависит потребление памяти. Его значение перекрывает значение предыдущих параметров.

ServerLimit обычно равен MaxClients.

MaxRequestsPerChild - как часто сервер перерабатывает процессы, убивая старые и начиная (запуская) новые. Полезен при утечках памяти Apache и его библиотек.

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

Также есть хороший скрипт check_httpd_limits.pl , написанный на Perl, позволяющий определить, сколько памяти занимают процессы сервера Apache . Скрипт выдает предупреждения (или ошибки), если установленные в конфигурации Apache пределы памяти превышают размеры доступной памяти на сервере.


Представляю вашему вниманию вторую статью из серии, посвященной процессу записи данных в СУБД Cassandra. В этот раз я планирую представить информацию о следующем компоненте — промежуточном кэше данных колоночных семейств.

Сразу же после помещения данных в Commit Log, информация также дублируется в структуру, называемую Memtable. Этот компонент имеет следующие свойства:

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

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

Для поиска и вставки новых элементов в Memtable используется алгоритм Skip List, вернее его реализация на Java — ConcurrentSkipListMap. Понимание принципа его работы не совсем обязательно в контексте изучения Memtable, однако этот алгоритм делает и без того быстрый поиск по оперативной памяти ещё быстрее и поэтому я вкратце постараюсь объяснить его основы.

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

Более подробно рассмотреть алгоритм по шагам можно в статье на Хабре и некоторых других источниках, а мы идем дальше.

К данным, находящимся в Memtable, впоследствии можно получать доступ как к кэшу при клиентских запросах чтения. Это также одно из отличий от Commit Log, к которому не могут осуществляться клиентские запросы.

Когда Memtable достигает определенного объема, происходит сброс (flush) данных в структуру, называемую SSTable, которая располагается на жестком диске. Регулировать максимальный объем Memtable можно с помощью параметра memtable_total_space_in_mb, который по умолчанию с версии Cassandra 2.0.2 стал равен четверти (ранее треть) от объема памяти, выделяемой для динамической памяти Java (Java heap size).

В свою очередь управление памятью Java осуществляет Cassandra самостоятельно и в зависимости от объема RAM устанавливаются следующие значения:

Many users new to Cassandra are tempted to turn up Java heap size too high, which consumes the majority of the underlying system’s RAM. In most cases, increasing the Java heap size is actually detrimental for these reasons:

— In most cases, the capability of Java to gracefully handle garbage collection above 8GB quickly diminishes.

— Modern operating systems maintain the OS page cache for frequently accessed data and are very good at keeping this data in memory, but can be prevented from doing its job by an elevated Java heap size.

If you have more than 2GB of system memory, which is typical, keep the size of the Java heap relatively small to allow more memory for the page cache.

Следующий параметр, отвечающий за работу Memtable — file_cache_size_in_mb . Судя по описанию в официальной документации, используется как промежуточный кэш для чтения данных (SSTable-reading buffer) перед их записью в SSTable.

Осуществлять гибкое управление Memtable можно с помощью параметров memtable_flush_writers и memtable_flush_queue_size . Первый параметр отражает количество (от 2 до 8 по умолчанию) экземпляров процесса, отвечающего за сброс данных на жесткий диск. Если у вас множество каталогов данных, большой размер Java heap или в в процессе перемещения данных из Memtable на HDD участвует множество жестких дисков, рекомендуется увеличить это значение и таким образом распараллелить процесс. Это актуально также при использовании SSD. Второй параметр задает максимальное количество полных Memtable, ожидающих сброса на жесткий диск. Его рекомендуют устанавливать в значение, которое равно максимальному количеству индексов какого-либо колоночного семейства. Эта рекомендация связана с особенность хранения индексов: индекс для колоночного семейства — это фактически ещё одно колоночное семейство, ключом которого является индексное поле оригинального CF:

At the storage layer, a secondary index is simply another column family, where the key is the value of the indexed column, and the columns contain the row keys of the indexed table…

…Cassandra co-locates index entries with their associated original table keys.

Стоит отдельно рассмотреть изменения касательно Memtable, которые были введены в версии Cassandra 2.1. Если до этой версии Memtable располагались исключительно в Java heap, то теперь можно переместить буфер Memtable в собственную память Cassandra. В связи с этим были добавлены соответствующие переменные в параметры конфигурации. Параметр memtable_allocation_type определяет три новых значения (первое я пропущу, поскольку оно соответствует типу хранения до версии 2.1):

multiple memtables may exist for a single column family, one current and the rest waiting to be flushed.

Однако остается вопрос что значит «полная Memtable» (full memtable) и почему рекомендуется устанавливать значение параметра memtable_flush_queue_size к максимальному количеству индексов для одного колоночного семейства:

The number of full memtables to allow pending flush (memtables waiting for a write thread). At a minimum, set to the maximum number of indexes created on a single table.

Возможно имеет место следующий алгоритм: когда одиночный сегмент commit log достигает своего максимального объема, создается новый файл commit log (один для всех CF) на жестком диске и новые Memtable (по одной для каждого CF) в оперативной памяти. Одновременно старый commit log помечается битом 0 (то есть ожидает операции flush). Если будет достигнут один из максимальных пределов — commitlog_total_space_in_mb для commit log и memtable_total_space_in_mb для memtable, самые старшие полные memtable начинают помещаться в очередь (memtable flush queue) и после их сброса на диск в SSTable, удаляются (точно также и соответствующие им commit log). Это лишь мое предположение на основе той информации, которую мне удалось найти в сети.

Обработка файлов.xls с помощью Apache POI

В ходе работы над различными программными продуктами часто возникает необходимость импорта и экспорта данных из различных "закрытых" форматов файлов. Чаще всего эта необходимость возникает применительно к файлам в форматах офисных продуктов корпорации Microsoft, в частности Word (doc, docx) и Excel (xls, xlsx). В силу особенностей реализации этих форматов, реализация такой обработки "в лоб" целиком своими силами была бы весьма нетривиальной и достаточно трудоёмкой задачей. К счастью, основная часть работы уже сделана за нас - существуют открытые Java-библиотеки, позволяющие преобразовать эти файлы в объектную модель Java, после чего получение из них необходимой нам информации не составит особого труда. В этой заметке показан пример реализации такой выборки данных из документа.xls с помощью Apache POI (на примере версии 3.6) - популярной библиотеки от Apache Software Foundation.

Из чего состоит документ Excel

Для начала, определимся с терминологией. Документ Excel (workbook ) состоит из одного или более листов (или вкладок, в оригинале - sheets ). Каждый лист представляет из себя матрицу из m строк (rows ) по n ячеек (cells ) в каждой. Максимальные размерности m и n различаются для разных версий Excel. В API POI каждому из структурных элементов файла Excel соответствует свой базовый интерфейс и его реализации для документов.xls и.xls, а работа с файловой системой осуществляется посредством класса org.apache.poi.poifs.filesystem.POIFSFileSystem .

Обратите внимание, что для страницы в документе Excel 2007 (.xlsx) существует сразу 3 реализации - помимо "общего" класса XSSFSheet добавлены XSSDialogSheet (страница-диалог) и XSSFChartSheet (страница, содержащая только график).

Алгоритм обработки

Таким образом, в общем случае, обработка документа Excel сводится к последовательному перебору ячеек и строк на каждой его странице. Значение каждой ячейки может быть преобразовано к нужному нам типу (строка, число, дата и т.д.) посредством методов, описанных в интерфейсе Cell и его реализациях.

Тип данных Метод Комментарий
CELL_TYPE_BLANK нет Используется для проверки того, пуста ли ячейка
CELL_TYPE_NUMERIC Возвращает long
Возвращает Date
CELL_TYPE_STRING Возвращает простую строку
Используется для отформатированных строк. Возвращает RichTextString
Используется для получения гиперссылок. Возвращает Hyperlink
CELL_TYPE_FORMULA getCellFormula () Возвращает строку, содержащую формулу, в соответствии с которой формируется значение ячейки
CELL_TYPE_BOOLEAN getBooleanCellValue () Возвращает boolean
CELL_TYPE_ERROR getErrorCellValue () Возвращает код ошибки (byte)

Если тип содержимого ячейки не совпадает с тем типом данных, к которому мы пытаемся его преобразовать, будет сгенерировано исключение. Какое именно - зависит от конкретных типов данных между которыми мы пытаемся осуществить преобразование. К примеру, при попытке получения числового значения из строковой ячейки, будет сгенерирован java.lang.IllegalStateException . Для того, чтобы избежать исключительной ситуации, можно заранее проверить, к какому типу относится содержимое конкретной ячейки - для этой цели существует метод getCellType(), объявленный всё в том же интерфейсе Cell.

Пример разбора файла

Теперь перейдём от теории к практике - рассмотрим на примере, как может выглядеть код простейшей программы на Java, получающей данные из документа xls посредством API Apache POI. Предположим, что мы хотим выбрать данные из некоего справочника контактной информации, который выглядит примерно так:

Таким образом, наш документ состоит из трёх столбцов - ФИО, Адрес и Телефон. Чтобы упростить задачу, создадим класс с теми же самыми полями, который будет хранить информацию о контактных лицах:

package com.tuneit.poi.sample; /* * Класс, предназначенный для хранения * перснональной информации контактного лица */ public class ContactPerson { private String name; //ФИО private String address; //Адрес private String phoneNumber; //Номер телефона //Getters & setters ... }

Это простейший POJO-класс, содержащий только поля (соответствующие столбцам нашего xls-документа) и get- и set-методы к ним (в листинге не приведены).

Теперь рассмотрим основной фрагмент кода - собственно, сам парсер содержимого документа:

public static final NAME_COLUMN_NUMBER = 0; //ФИО public static final ADDRESS_COLUMN_NUMBER = 1; //Адрес public static final PHONE_COLUMN_NUMBER = 2; //Телефон... public ListGetContacts(String path){ ListContacts = new ArrayList(); //Создаём пустой список контактов File addressDB = new File(path); //Переменная path содержит путь к документу в ФС POIFSFileSystem fileSystem = new POIFSFileSystem(addressDB); //Открываем документ HSSFWorkbook workBook = new HSSFWorkbook(fileSystem); // Получаем workbook HSSFSheet sheet = workBook.getSheetAt(0); // Проверяем только первую страницу Iterator rows = sheet.rowIterator(); // Перебираем все строки // Пропускаем "шапку" таблицы if (rows.hasNext()) { rows.next(); } // Перебираем все строки начиная со второй до тех пор, пока документ не закончится while (rows.hasNext()) { HSSFRow row = (HSSFRow) rows.next(); //Получаем ячейки из строки по номерам столбцов HSSFCell nameCell = row.getCell(NAME_COLUMN_NUMBER); //ФИО HSSFCell addressCell = row.getCell(ADDRESS_COLUMN_NUMBER); //Адрес HSSFCell phoneCell = row.getCell(PHONE_COLUMN_NUMBER); //Номер телефона // Если в первом столбце нет данных, то контакт не создаём if (nameCell != null) { Person person = new Person(); person.setName(nameCell.getStringCellValue()); //Получаем строковое значение из ячейки person.setAddress(""); //Адрес может не быть задан if (addressCell != null && !"".equals(addressCell.getStringCellValue())) { person.setAddress(addressCell.getStringCellValue()); //Адрес - строка } person.setPhone(""); //Телефон тоже может не быть задан if (phoneCell != null && !"".equals(phoneCell.getStringCellValue())) { person.setPhoneNumber(phoneCell.getStringCellValue()); // Телефон - тоже строка } contacts.add(person); //Добавляем контакт в список } } return contacts; }

Таким образом, на выходе приведённого выше метода мы получим коллекцию объектов класса Person, сформированных из данных исходного xls-файла. Разумеется, в случае решения более общей задачи - например, написания универсального обработчика xls-документов, логика программы усложнится, но её основа, скорее всего, останется прежней - всё тот же последовательный перебор всех строк таблицы.