вторник, 1 ноября 2011 г.

Spring logging under maven

As we all know, Spring framework uses Commons Logging (sometimes called JCL - Jakarta Commons Logging) for its output. And I bet you are a successful developer who knows how cool SLF4J is. Can you spot the problem already?

OK, I'll ease your agony. How are we going to ask Spring to pollute our log files with its log output, provided we use SLF4J as a facade and, say, Log4J as implementation?

I assume you use maven 2 as build tool. The solution should work for Spring 2 and 3.

So here is the plan. First we use a nice library jcl-over-slf4j, which  implements the public API of JCL but using SLF4J underneath. Then we use another nice library slf4j-log4j12 to tell SLF4J to use Log4j as its underlying logging framework. Apart from these two dependencies we obviously need two more: SLF4J itself (slf4j-api) and Log4J itself (log4j). So we end up with following dependencies config in maven pom:

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  ...
  <dependencies>
    ...
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>1.6.3</version>
      </dependency>
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.6.3</version>
          <scope>runtime</scope>
      </dependency>
      <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>jcl-over-slf4j</artifactId>
          <version>1.6.3</version>
          <scope>runtime</scope>
      </dependency>
      <dependency>
          <groupId>log4j</groupId>
          <artifactId>log4j</artifactId>
          <version>1.2.16</version>
          <scope>runtime</scope>
      </dependency>
  </dependencies>
</project>

But before celebrating the victory we have another thing left to solve. Removing commons-logging from classpath. Algorithm is following:
  1. make sure commons-logging is not directly specified in your pom as dependency
  2. run mvn dependency:tree and search for commons-logging in the output
  3. if not found goto 6
  4. for the higher-level dependency which brings commons-logging add the following configuration in pom:
    ...
      <dependencies>
          <dependency>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
          </dependency>
      </dependencies>
    ...
    
  5. goto 2
  6. get some champagne and start celebrating

Now you can comfortably use SLF4J with Spring logging enabled.

Useful links:
http://blog.springsource.com/2009/12/04/logging-dependencies-in-spring/
http://stackoverflow.com/questions/3387441/how-do-i-configure-spring-and-slf4j-so-that-i-can-get-logging

четверг, 13 октября 2011 г.

Обнаружим подводные камни быдлокода Sonar'ом

Все началось с простой задачи - прикрутить к мавен билду отчет о покрытии кода тестами. Погуглил.

Первым всплыла Emma. Плагин для эклипсика поставился быстро и отлично работал - можно было быстро увидеть покрытие кода конкретным тестом. Отличная фича для повседневной работы и быстрой проверки. Но по прежнему оставалась основная задача - узнать покрытие кода всеми тестами в мавен проекте.

И вот с использованием Emma в мавене возник серьезный затык. В распоряжении были три плагина:
У первых двух были подозрительно одинаковые goal-ы, возможно представляют собой одно и то же. Возможности очень узкие, документация беднейшая (для сонатапойвского все что можно было найти - это указанную по ссылке статью, в которой в примерах конфигов xml теги съедены). В итоге все попытки настроить плагины превращаются в битье головой об стену. Про поддержку покрытия межмодульного тестирования и говорить не приходится.
Третий плагин чрезвычайно стар - написан аж для первого мавена. Для написания плагина под второй мавен по словам разработчиков "there is some interest". Ок, вопросов не имеем.

Погоревав немного над унылостью еммо-поддержки в мавене, я смягчил свое сердце по отношению к ранее нагугленной платформе для комплексного анализа кода Sonar.
"Мне всего лишь нужно покрытие кода тестами, зачем ставить целую отдельную платформу?" - думал я раньше.
Но каково же было мое удивление, когда я смог за считанные минуты загрузить сонар, проанализировать с его помощью весь проект, и получить помимо покрытия кода юнит тестами всевозможные указания на проблемные места быдлокода и быдлодизайна.

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

Далее уже залогинившись в систему сонара под дефолтным акком admin/admin сделал следующее:
  • поменял пароль данного аккаунта на... кхм... на секретное значение (Administrator -> My Profile)
  • выставил в качестве дефолтного профиля оценки качества кода 'Sonar way with Findbugs', так как изначальный профиль 'Sonar way' содержит намного меньшую базу данных косяков (Configuration -> Quality Profiles)
  • ВАЖНО! в сонаре имеется возможность просмотра исходников кода, поэтому если проект закрытый, необходимо настроить обязательный вход под каким-либо из используемых аккаунтов, т.е. запретить анонимусам смотреть что-либо в сонаре (Configuration -> General Settings -> Security -> Force user authentication)
  • поставил няшные плагины: Clirr, JaCoCo, Motion Chart, SIG Maintainability Model, Taglist, Timeline, Total Quality (установить и посмотреть описание всех плагинов можно в Configuration -> Update Center -> Available Plugins) Из них уже успел убедится в полезности JaCoCo и Timeline,первый добавляет поддержку JaCoCo - альтернативного дефолтной Cobertura тула для анализа покрытия кода тестами (о нем чуть ниже), второй позволяет заценить динамику изменения параметров качества кода во времени
  • настроил использование JaCoCo в своем проекте. Выбор JaCoCo был сделан из-за его возможности отражать покрытие как юнит тестов так и интеграционных тестов(читай тестов, использующих межмодульные связи). Пришлось немного повозиться. Настройки делал для мультимодульного проекта, имеющего родительский пом, в каждом модуле имелись юнит тесты, кроме того в отдельных модулях имелись тесты, использующие классы из других модулей.
    1. Сначала в самом сонаре ставим JaCoCo как дефолтный аналитик покрытия (Configuration -> General Settings -> Code Coverage -> Code coverage plugin = jacoco). Можно это не выставлять здесь. Но тогда придется при вызове мавена указывать значение в виде параметра.
    2. Настраиваем surefire плагин в родительском pom.xml чтобы он мог принимать параметры JVM:
      <project>
         ...
         <build>
            ...
            <plugins>
               ...
               <plugin>
                  <groupId>org.apache.maven.plugins</groupId>
                  <artifactId>maven-surefire-plugin</artifactId>
                  <configuration>
                     <argLine>${surefire.jvm.args}</argLine>
                  </configuration>
                  <inherited>true</inherited>
               </plugin>
            </plugins>
         </build>
      </project>
      
      
    3. Теперь начинается сам процесс. Он у нас состоит из двух шагов: прокатка билда с паралельным сбором информации о покрытии тестов и проход сонаром по проекту. Все команды выполняем, находясь в папке родительского мавен проекта.
    4. Шаг первый: билдим проект и собираем инфу о покрытии. Для этого нам необходимо выполнять тесты специальным JaCoCo агентом. Команда принимает следующий вид:
      JACOCO_FILE="jacoco.exec"
      JACOCO_DUMP="target/$JACOCO_FILE"
      JACOCO_AGENT="$MAVEN_REPO/repository/org/jacoco/org.jacoco.agent/0.5.3.201107060350/org.jacoco.agent-0.5.3.201107060350-runtime.jar"
      
      mvn -Dsurefire.jvm.args=-javaagent:$JACOCO_AGENT=destfile=$JACOCO_DUMP clean install
      
      Небольшие пояснения. Параметр surefire.jvm.args мы только что использовали при настройке Surefire, с помощью его значения мы говорим "эй, surefire! используй jacoco агент отсюда, а свои результаты сохраняй вот сюда". В моем примере я использую агент из мавен репозитория, но можно его и отдельно качнуть. Путь к сохраняемому файлу указан относительный благодаря чему для каждого модуля получится свой джакоко файл.
    5. Шаг полуторный. Прежде чем начнем сбор инфы для сонара необходимо сделать одну маленькую вещь - создать дополнительный джакоко файл, содержащий в себе все джакоко файлы модулей. Это позволит нам проанализировать межмодульное покрытие кода тестами. Если быть более точным, то в получившемся файле будет информация о покрытии любым тестом - как локальным юнит тестом так и тестом из другого модуля. Делаем:
      JACOCO_DUMP_IT="/tmp/$JACOCO_FILE"
      cat `find -name $JACOCO_FILE` > $JACOCO_DUMP_IT
      
      Как видно объединение представляет собой банальную конкатенацию.
    6. Ну и теперь второй шаг - вызываем сбор инфы для сонара
      SONAR_URL="http://localhost:9000"
      DB_DRIVER="org.postgresql.Driver"
      DB_URL="jdbc:postgresql://localhost:5432/sonar"
      DB_USER="user"
      DB_PASSWORD="password"
      
      mvn -e sonar:sonar -Dsonar.host.url=$SONAR_URL -Dsonar.jdbc.driver=$DB_DRIVER -Dsonar.jdbc.url=$DB_URL -Dsonar.jdbc.username=$DB_USER -Dsonar.jdbc.password=$DB_PASSWORD -Dsonar.jacoco.itReportPath=$JACOCO_DUMP_IT -Dmaven.test.skip -Dsonar.dynamicAnalysis=reuseReports
      
      Параметр sonar.jacoco.itReportPath - то что и позволяет нам разделить информацию о покрытии юнит тестами и тестами вообще. В своем примере я использую эту возможность не совсем по назначению. Предполагается что у вас имеется отдельный модуль для интеграционных тестов, который собирает все модули воедино и тестит их. Покрытие таких ИТ тестов и проверяется. У меня же интеграционные тесты разбросаны по разным модулям, поэтому мне более полезно будет знать об общем покрытии классов любым тестом неважно из какого модуля. Параметр maven.test.skip позволяет избежать повторного запуска тестов. Значение sonar.dynamicAnalysis=reuseReports указывает сонару не удалять уже имеющиеся после нашего первого шага джакоко файлы.
  • Добавил необходимые виджеты на странице Dashboard ( -> Dashboard -> Configure widgets). Имейте ввиду, что на дашборде ничто само по себе не появляется, после установки какого-либо плагина предполагающего доп. вывод, придется здесь добавить виджет. В частности фича покрытия ИТ тестами от джакоко предполагает добавление виджета.
  • Полазил, посмотрел результаты - ай нрайца! Ай люблю! 
Вот так относительно малой кровью получился полезный тул при разработке.  Можно продолжать накручивать возможности, в частности интегрировать Трак и Сонар, будет выводится какая-то инфа по тикетам. Но мне пока хватит и этого.
Для удобства запуска процесса сбора инфы на проекте советую поместить все команды в скриптец.
Вот весь скрипт целиком:
#! /bin/sh
#
#change parameters to fit your needs
SONAR_URL="http://localhost:9000"
DB_DRIVER="org.postgresql.Driver"
DB_URL="jdbc:postgresql://localhost:5432/sonar"
DB_USER="user"
DB_PASSWORD="password"
JACOCO_FILE="jacoco.exec"
JACOCO_DUMP="target/$JACOCO_FILE"
JACOCO_DUMP_IT="/tmp/$JACOCO_FILE"
MAVEN_REPO="/home/user/.m2"
JACOCO_AGENT="$MAVEN_REPO/repository/org/jacoco/org.jacoco.agent/0.5.3.201107060350/org.jacoco.agent-0.5.3.201107060350-runtime.jar"

mvn -Dsurefire.jvm.args=-javaagent:$JACOCO_AGENT=destfile=$JACOCO_DUMP clean install

cat `find -name $JACOCO_FILE` > $JACOCO_DUMP_IT

mvn -e sonar:sonar -Dsonar.host.url=$SONAR_URL -Dsonar.jdbc.driver=$DB_DRIVER -Dsonar.jdbc.url=$DB_URL -Dsonar.jdbc.username=$DB_USER -Dsonar.jdbc.password=$DB_PASSWORD -Dsonar.jacoco.itReportPath=$JACOCO_DUMP_IT -Dmaven.test.skip -Dsonar.dynamicAnalysis=reuseReports

rm $JACOCO_DUMP_IT

Напоследок ссылочки, из которых была почерпнута информация:
http://docs.codehaus.org/display/SONAR/The+2+minutes+tutorial
http://docs.codehaus.org/display/SONAR/Documentation
http://mojo.codehaus.org/sonar-maven-plugin/sonar-mojo.html
http://www.sonarsource.org/measure-code-coverage-by-integration-tests-with-sonar/
http://docs.codehaus.org/display/SONAR/JaCoCo+Plugin
http://www.eclemma.org/jacoco/trunk/doc/agent.html
http://docs.codehaus.org/display/SONAR/Advanced+parameters

среда, 22 июня 2011 г.

Java i18n - никакого мошенничества, только ловкость рук

В очередной раз старушка джава подкидывает прикол. На этот раз история у нас с интернационализацией, или просто i18n.

Как мы все знаем, при использовании стандартного подхода, тексты сообщений для различных языков размещаются в .properties файлах. Для каждого языка создается файл с соответствующим суффиксом. Кроме того, заботливые разработчики джавы настоятельно рекомендуют в доках:
You should always create a default properties file.
И даже пример приводят, где английский вариант помещен в качестве дефолтного и, соответственно, варианта файла с суффиксом для английского языка нету. Вот, взято оттуда же:

The PropertiesDemo sample program ships with three properties files:

LabelsBundle.properties
LabelsBundle_de.properties
LabelsBundle_fr.properties

Отлично! - подумал я. - Всё понятно, предоставишь язык de - выберется LabelsBundle_de.properties, предоставишь язык en, LabelsBundle_en.properties найден не будет и выберется LabelsBundle.properties!

Переполненный оптимизмом и радостью я сделал свой вариант
Messages.properties
Messages_ru.properties
где в дефолтном файле поместил английские тексты.
Протестировал локально, все замечательно работает. Загрузил на сервак.
Каков же был размер кирпича, когда я увидел, что несмотря на самые отчаяннейшие попытки кликать по ссылке "en" я получал один и тот же - русский вариант перевода.

Расследовав дело я убедился в той свинье, которую подложили мне летающие в облаках разработчики джавы.
Читаем хелп по ResourceBundle.getBundle:

Conceptually, getBundle uses the following strategy for locating and instantiating resource bundles:

getBundle uses the base name, the specified locale, and the default locale (obtained from Locale.getDefault) to generate a sequence of candidate bundle names. If the specified locale's language, country, and variant are all empty strings, then the base name is the only candidate bundle name. Otherwise, the following sequence is generated from the attribute values of the specified locale (language1, country1, and variant1) and of the default locale (language2, country2, and variant2):

* baseName + "_" + language1 + "_" + country1 + "_" + variant1
* baseName + "_" + language1 + "_" + country1
* baseName + "_" + language1
* baseName + "_" + language2 + "_" + country2 + "_" + variant2
* baseName + "_" + language2 + "_" + country2
* baseName + "_" + language2
* baseName

Вот она роковая часть. Вызывая ResourceBundle.getBundle("Messages", new Locale("en")) и ожидая что подхватится безсуффиксный файл заместо отсутствующего английского, я не знал, что разработчики джавы слишком умные и прежде чем юзать безсуффиксный файл ищут вот это:

...the default locale (language2, country2, and variant2)

* baseName + "_" + language2 + "_" + country2 + "_" + variant2
* baseName + "_" + language2 + "_" + country2
* baseName + "_" + language2

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

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

вторник, 21 июня 2011 г.

matplotlib - чертим кодом

В гостях у нас новая модная фишка - библиотека питона для отображения 2D графиков и геометрических фигур. Я использовал ее для создания графиков в системе документирования проектов. Достаточно удобно, за исключением того факта, что отсутствуют какие-либо плагины для трака. Эх, как же было бы прекрасно писать в самой вики трака код, нажимать кнопку и получать готовую картинку. Как собственно я делал для графвиза. Но увы, на данный момент приходится сперва с помощью скрипта создавать изображение в каком-нибудь формате PNG, а уж затем впихивать эту картинку в трак. Может взять это за повод разработать плагин для трака?
Кстати раз уж упомянул, в чем же отличие Graphviz от matplotlib? Графвиз, как свидетельствует корень слова, специализируется на графиках. Это высокоуровневый движок предназначенный для отрисовки рафиков, описание которых не указывает форму отрисовки, лишь отношение между графами. Матплотлиб это более низкоуровневый движок, где отрисовка детально описывается кодом, и нарисовать можно все что угодно.

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

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

2. мучаем оси координат
По-умолчанию заботливый матплотлиб дарит нам отметки осей координат на всех четырех сторонах изображения. Что же можно с этим сделать...
a) убираем отметки (так называемые ticks) с верхней оси:

ax.xaxis.set_ticks_position('bottom')

где ax - объект типа matplotlib.axes.Axes , соответствующий метод для yaxis здесь.

б) убираем ось полностью

ax.yaxis.set_visible(False)

метод наследуется классом оси от класса matplotlib.artist.Artist

в) устанавливаем границы изображения по осям

plt.axis([0, 7, 0.5, 2.5])


г) указываем границы отметок-ticks и как их подписать

import pylab
...
pylab.xticks( pylab.arange(8), ('', 't1', '', 't2', '', 't1+d1','t2+d2') )

а что же это за модуль такой pylab? Это всего лишь процедурный интерфейс к другим различным модулям, в том числе к matplotlib и numpy. Достаточно посмотреть исходный код pylab чтобы увидеть все бесстыдство сплошных импортов и ни единого кусочка собственного кода. Однако это должно быть удобным. Так или иначе на моей убунточке матплотлиб установка включает в себя pylab, выбора тут нету.

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

ax.xaxis.set_major_locator(matplotlib.ticker.IndexLocator(1,0))


3. меняем размеры изображения
Размеры картинки при работе с матплотлиб могут пониматься в двух смыслах. В векторном и в растровом. Изначально мы задаем размеры в векторном смысле. Что определяют эти размеры? Они определяют в основном относительное расположение элементом рисунка на координатной оси (при этом некоторые детали сохраняют свои размеры, например текст). Можно сказать что на этом этапе мы просто сжимаем/разжимаем координаты используя категории дюймов, сантиметров и тп. Второй и заключительный этап - преобразование вектора в растр, здесь мы просто указываем DPI (хотя наверное это правильнее было бы назвать pixels per inch) и все что мы наваяли в векторе преобразуется в растр. Вот кусок кода:

defaultSize = fig.get_size_inches() #получаем размер нашего изображения
pylab.savefig("plot01.png", bbox_inches="tight",dpi=75) #сохраняем как png с dpi 75

#уменьшаем изображение по вертикали, при этом текст например сохраняет свои размеры
fig.set_size_inches(defaultSize[0], defaultSize[1] * 0.4)
pylab.savefig("plot02.png", bbox_inches="tight",dpi=75) #вновь сохраняем с теми же dpi


Вот пока и всё. Читаем доки с сайта, смотрим питоновские help(...) и чертим с удовольствием!

пятница, 10 июня 2011 г.

Maven и забота о детях

У меня мультимодульный проект в мавене 2, в котором имеется главный родительский модуль типа pom. Для удобства управления версиями, само указание версии для дочерних модулей было опущено, что, как мы все знаем, приводит к наследования версии родительского модуля.
Однако хардкод версии все же прокрадывается в дочерние модули через секцию <parent>:
<parent>
    <artifactId>parent</artifactId>
    <groupId>com.project</groupId>
    <version>0.1-SNAPSHOT</version>
</parent>
Да-да, именно так. Мы вынуждены указывать версию родителя, которую используем. Возможно кому-то и посчастливиться воспользоваться этой фичей возможности использования разных версий родителя, но это явно не для моего быдло проектика. Я всегда хочу использовать последнюю версию родителя.
И вот мы наконец подошли к проблеме. При обновлении версии в родительском поме, как обновить ссылку на версию родителя во всех дочерних помах?

Добрый товарищ dwh указал на maven-release-plugin и его возможность release:update-versions. Справедливости ради стоит отметить что такое же решение предлагают и на форумах. Но сразу же появились проблемы. Сначала оказалось, что в моем супер поме указана староватая бета-версия релиз плагина, не имеющая указанной возможности. После попытки обновится на более новую релиз плагин начал капризно требовать scm настроек, которые мне совершенно не сдались. В итоге пришлось отказаться от идеи релиз плагина.

Намного лучшим решением оказался плагин versions-maven-plugin из проекта Mojo. Этот плагин и специализируется на работе с версиями (в отличие от релиз плагина, первоочередная задача которого работа с релизами). Добавив его (versions плагин) в plugins секцию родительского пома, а затем вызвав mvn versions:update-parent -DallowSnapshots=true я решил свою проблему! :D Опция allowSnapshots=true позволяет работать в том числе и с snapshot версиями, без этой опции snapshot версии будут игнорироваться.

среда, 23 марта 2011 г.

Private атрибуты - не помеха для JUnit

Правильно ли использовать в юнит-тестах какие-либо private атрибуты (методы или поля) класса? Вопрос философский, сформулировать который можно и так: blackbox vs whitebox testing. В контексте общего понимания вопроса выбор скорее всего будет за blackbox тестированием. Оно красиво, элегантно и соответствует правильным идеям. Однако реальность сложнее и иногда то, что элегантно и красиво тупо не работает.
Если возникла необходимость вызвать какие-либо приват методы тестируемого класса, подходящим решением (не требующим модификации самого класса) является использование reflection, хорошо описанное здесь.
У меня была несколько иная задача. Требовалось вытянуть из класса приватные статик филды. Вот как это было реализовано:
java.lang.reflect.Field field = MyClass.class.getDeclaredField("STATIC_FIELD_NAME");
field.setAccessible(true);
String value = (String) field.get(null);

Все должно быть понятно из самого кода. Единственное - что это за null там такой в методе get. Это означает что мы хотим получить статик филд, если бы был инстанс филд, то вместо null передали бы объект, значение филда которого нам нужно.

пятница, 18 марта 2011 г.

Жабо-регэкспы

Почему-то считал, что джава умеет проводить лишь простейший поиск с помощью регэкспов, однако оказалось, что захват групп, замена - все это также возможно.
Появилась задача передать значение длины промежутка времени, выраженное в минутах, часах, днях, месяцах, годах. Что-то вроде 1d для 1 дня, 3M для 3 месяцев, 75m для 75 минут и т.д. Для разбора строки с таким значением идеально подходит регэксп с захватом групп. Числовое значение выделяем в одну группу, значение единицы времени в другую. Вот регэксп, к которому я пришел:
^([1-9][0-9]*)(y|M|d|H|m|s)$
здесь ^ - начало строки, $ - конец строки. Эти символы добавлены, чтобы исключить какие-либо еще посторонние символы. Первая группа ^([1-9][0-9]*) захватывает числовое значение. Значения вида "01" или "-2" не подходят. Вторая группа (y|M|d|H|m|s) захватывает значение единицы времени. Соответствующий джава-код для обработки значения с помощью этих групп:
            Pattern pattern = Pattern.compile(^([1-9][0-9]*)(y|M|d|H|m|s)$);
       Matcher matcher = pattern.matcher(value);
       boolean matchFound = matcher.find();

       if (matchFound) {
           String numberValue = matcher.group(1);
           String unitValue = matcher.group(2);
           int number = Integer.parseInt(numberValue);
           //do something
       }

Весьма все просто.

Задача номер два заключала в себе необходимость формата строки, задающей путь в файловой системе в зависимости от даты. Использовался синтаксис аналогичный FileNamePattern в log4j. Пример:
%d{yyyy}/%d{MM}/msg.%d{yyyy-MM-dd}.gz
Для 18 марта 2011 года из этого шаблона должно получиться 2011/03/msg.2011-03-18.gz
То есть по сути следует заменить все вхождения %d{...} соответствующими значениями переданной даты, шаблон внутри %d{...} аналогичен используемому в SimpleDateFormat. Вот код который производит необходимое форматирование:
Pattern pattern = Pattern.compile("%d\\{(.*?)\\}");
Matcher matcher = pattern.matcher(pathPattern);
String path = pathPattern;
       
while (matcher.find()) {
      SimpleDateFormat dateFormat = new SimpleDateFormat(matcher.group(1));
      path = matcher.replaceFirst(dateFormat.format(date));
      matcher.reset(path);
}

Здесь мы последовательно в цикле находим вхождение паттерна, захватываем группу, форматируем ее с помощью SimpleDateFormat и производим необходимую замену в входной строке шаблона. Выход из цикла происходит, когда в результате не найдено ни одного блока %d{...}

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

Log4j эксплоит

Все мы знаем о том, как прекрасен log4j. Вставляешь выводы для разных уровней DEBUG, INFO, ERROR в коде, а потом через настройки в текстовом формате можешь контролить степень подробности описания выполнения проги. Код написанный для log4j не является бизнес логикой, он не влияет на ход выполнения проги никак, лишь описывая текущее состояние системы для последующего администрирования.
Однако log4j можно применить и непосредственно для создания функциональности.
Мне потребовалось написать программу, сохраняющую поток некоторых данных в текстовом виде, с разбиением данных на отдельные файлы в зависимости от календарного дня. Была необходима  архивация данных прошедших дней. Можно было написать все это вручную, но зачем, когда все требуемые действия умеет производить log4j.
Разбиение в зависимости от даты с последующей архивацией завершенных файлов умеет выполнять org.apache.log4j.rolling.RollingFileAppender. Это класс из дополнительного пакета log4j-extras. Так что нам понадобятся две джарки: log4j и log4j-extras. Вот пример конфигурации в log4j.xml, в которой

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">

<log4j:configuration>
  <appender name="dailyLog" class="org.apache.log4j.rolling.RollingFileAppender">
    <rollingPolicy class="org.apache.log4j.rolling.TimeBasedRollingPolicy">
      <param name="FileNamePattern" value="/tmp/my-log.%d{yyyy-MM-dd}.gz"/>
    </rollingPolicy>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{HH:mm:ss,SSS} - %c{1} - %m%n"/>
    </layout>
  </appender>
  <logger name="MyBusinessData">
      <level value="info"/>
      <appender-ref ref="dailyLog"/>
  </root>
</log4j:configuration>

В этой конфигурации
  • осуществляется вывод логгера MyBusinessData с уровнем не ниже INFO
  • вывод осуществляется в файлы /tmp/my-log.*  - в зависимости от
    даты, так логирование, произошедшее 14 марта 2011 запишется в файл /tmp/my-log.2011.03.14; подробнее про настройку FileNamePattern здесь
  • по окончанию дня соответствующий файл архивируется gzip'ом.
  • в самом файле формат вывода определяется строчкой "%d{HH:mm:ss,SSS} - %c{1} - %m%n", где вначале идет формат времени, затем указание категории, затем само сообщение и символ новой строки. Подробнее про настройку ConversionPattern здесь

Теперь я могу записывать свои данные с помощью обычного логгера:
private Logger dataLogger = Logger.getLogger("MyBusinessData");
...
dataLogger.info(data);
и при указанной конфигурации данные будут сохраняться так как и требовалось. Однако возникает проблема которая как раз и связана со словами "при указанной конфигурации". Проблема в том, что изменив текстовый файл настройки log4j.xml (либо тупо забыв его поместить в нужное место и указать нужные значения), можно заглушить запись данных, то есть поломать программу. Гибкость настройки log4j в нашем случае оборачивается опасностью. Непорядок!
Поэтому переходим к программной настройке. Идея в том, чтобы игнорировать какие-либо настройки в текстовом файле для нашего дата-логгера, а вместо этого использовать жестко заданные в коде.
Вот те же самые настройки, произведенные программно:
dataLogger.removeAllAppenders();    //we don't want any configuration other than this
dataLogger.setAdditivity(false);    //avoid sending messages to root logger
RollingFileAppender rfa = new RollingFileAppender();
TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy();

//the smallest unit in the pattern is day, so rolling will occur at the end of each day
//.gz suffix tells log4j to compress finished day
rollingPolicy.setFileNamePattern("/tmp/my-log.%d{yyyy-MM-dd}.gz");
rollingPolicy.activateOptions();
rfa.setRollingPolicy(rollingPolicy);
PatternLayout layout = new PatternLayout();
layout.setConversionPattern("%d{HH:mm:ss,SSS} - %c{1} - %m%n");
rfa.setLayout(layout);
rfa.activateOptions();
dataLogger.addAppender(rfa);
dataLogger.setLevel(Level.INFO);

Очень важна здесь вторая строчка dataLogger.setAdditivity(false). Благодаря ей, сообщения, записываемые нашим дата-логером, не будут дублироваться на верхние уровни иерархии, в частности на рут логер. Таким образом, настроив обычное логирование даже для рута, мы не будем получать в довесок все данные логируемые нашим логером. Т.е. мы отдельно настроили дата-логер без ущерба для задачи логирования. Теперь можно удалить файл настроек log4j, либо настроить произвольным образом логирование, наша запись данных всегда будет производиться и не мешать остальному логированию. Ура!


пятница, 11 марта 2011 г.

Меняем сообщения коммитов в SVN и Trac

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

1. Залогиниться на комп, хранящий репозиторий
2. Выполнить sudo rm -rf / Создать где-нибудь в темпе файлик с нужным сообщением для коммита:
sudo vim /tmp/new_log
3. Выполнить магическую команду:
sudo svnadmin setlog REPO_PATH -r N /tmp/new_log  --bypass-hooks
где REPO_PATH - путь к репозиторию, N - номер ревизии, чьё сообщение меняем

Теперь в репозитории красуется нужное нам название для N-й ревизии.
Однако трак не знает о нашем изменении и продолжает показывать старое сообщение. Непорядок!

4. Говорим траку перестать тупить и показать новое сообщение
sudo trac-admin TRAC_PATH resync N
где N - всё тот же номер ревизии, TRAC_PATH - путь к трак-проекту. Можно и без номера вызвать команду, тогда произойдет полная синхронизация со всеми ревизиями. Оно вам надо?
Проделать все без захода на сам сервер (без 1го пункта), удаленно нельзя, потому как 3 и 4 пункты выполняются для локальных путей. Если трак и репозиторий на отдельных компах, то надо зайти на оба. А кто сказал что будет легко?

среда, 9 марта 2011 г.

Установка PostgreSQL

Продолжаем обвешивать сервачок всякой всячиной.
На очереди могучий слон PostgreSQL.
За основу взята статья из убунту доков.

1. Ставим
sudo apt-get install postgresql

2. Так как это у нас отдельный сервачок постгреса, то доступ к нему будет осуществляться через юзер/пароль. Дефолтный юзер - postgres. Установим ему пароль:
sudo -u postgres psql postgres
\password postgres
#вводим пароль дважды

3. Устанавливаем возможность подключение к БД с любого адреса:
# /etc/postgresql/8.4/main/postgresql.conf
listen_addresses = '*'


4. Настраиваем аутентификацию, разрешаем пользователям с любых адресов иметь доступ ко всем базам данных и заходить под любым пользователем, при условии ввода правильного соответствующего пароля, который будет передаваться в зашифрованном md5 виде:
# добавляем строчку в /etc/postgresql/8.4/main/pg_hba.conf
host    all         all         0.0.0.0/0               md5

Дополнительно про настройки аутентификации можно почитать здесь.

5. Перезагружаем постгрес, чтобы настройки в пунктах 3 и 4 вступили в силу:
sudo /etc/init.d/postgresql-8.4 restart

Теперь к постгресу, установленном на сервачке можно подключаться с других компов-десктопов, используя годный ГУИ тул pgadmin3.
Указанная здесь конфигурация на самом деле слишком... гостеприимная, что-ли. Я сделал ее таковой лишь на время разработки, чтобы иметь возможность стучаться к БД не только с работы, но и из дома через динднс. В целом же при настройке доступа к базе следует задавать как можно более жесткие условия для прослушиваемых адресов и аутентификации.
Вот и все, спокойной ночи, девочки и мальчики, до новых встреч!

пятница, 4 марта 2011 г.

Чистим старые ядра

При очередном обновлении моей десктопной убунты, вылезла вот такая страшная ошибка:
Not enough free disk space
The upgrade needs a total of 19.9M free space on disk '/boot'. Please free at least an additional 12.2M of disk space on '/boot'. Empty your trash and remove temporary packages of former installations using 'sudo apt-get clean'.
Как же так? Забился бут?
df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda6             127G   45G   76G  37% /
none                  940M  268K  939M   1% /dev
none                  944M  372K  943M   1% /dev/shm
none                  944M  344K  943M   1% /var/run
none                  944M     0  944M   0% /var/lock
none                  944M     0  944M   0% /lib/init/rw
/dev/sdb1             459G  105G  332G  24% /var/lib/storage
/dev/sda1              92M   79M  7.4M  92% /boot


И правда, забился... Смотрим чем же.
ls -lah /boot
total 70M
drwxr-xr-x  4 root root 3.0K 2011-02-18 15:23 .
drwxr-xr-x 22 root root 4.0K 2011-02-22 11:10 ..
-rw-r--r--  1 root root 637K 2010-09-16 21:14 abi-2.6.32-24-generic
-rw-r--r--  1 root root 637K 2010-10-17 02:46 abi-2.6.32-25-generic
-rw-r--r--  1 root root 637K 2010-11-24 15:57 abi-2.6.32-26-generic
-rw-r--r--  1 root root 637K 2010-12-02 02:48 abi-2.6.32-27-generic
-rw-r--r--  1 root root 637K 2011-01-11 03:18 abi-2.6.32-28-generic
-rw-r--r--  1 root root  512 2011-02-18 15:23 boot.0810
-rw-r--r--  1 root root  512 2011-02-18 15:23 boot.0820
-rw-r--r--  1 root root 114K 2010-09-16 21:14 config-2.6.32-24-generic
-rw-r--r--  1 root root 114K 2010-10-17 02:46 config-2.6.32-25-generic
-rw-r--r--  1 root root 114K 2010-11-24 15:57 config-2.6.32-26-generic
-rw-r--r--  1 root root 114K 2010-12-02 02:48 config-2.6.32-27-generic
-rw-r--r--  1 root root 114K 2011-01-11 03:18 config-2.6.32-28-generic
drwxr-xr-x  3 root root 6.0K 2011-02-02 11:07 grub
-rw-r--r--  1 root root 7.6M 2010-09-28 10:36 initrd.img-2.6.32-24-generic
-rw-r--r--  1 root root 7.6M 2010-11-18 10:02 initrd.img-2.6.32-25-generic
-rw-r--r--  1 root root 7.6M 2010-11-30 10:49 initrd.img-2.6.32-26-generic
-rw-r--r--  1 root root 7.6M 2011-01-21 10:42 initrd.img-2.6.32-27-generic
-rw-r--r--  1 root root 7.6M 2011-02-02 11:07 initrd.img-2.6.32-28-generic
drwx------  2 root root  12K 2010-08-27 16:56 lost+found
-rw-r--r--  1 root root 157K 2010-03-23 11:37 memtest86+.bin
-rw-r--r--  1 root root 1.7M 2010-09-16 21:14 System.map-2.6.32-24-generic
-rw-r--r--  1 root root 1.7M 2010-10-17 02:46 System.map-2.6.32-25-generic
-rw-r--r--  1 root root 1.7M 2010-11-24 15:57 System.map-2.6.32-26-generic
-rw-r--r--  1 root root 1.7M 2010-12-02 02:48 System.map-2.6.32-27-generic
-rw-r--r--  1 root root 1.7M 2011-01-11 03:18 System.map-2.6.32-28-generic
-rw-r--r--  1 root root 1.2K 2010-09-16 21:16 vmcoreinfo-2.6.32-24-generic
-rw-r--r--  1 root root 1.2K 2010-10-17 02:47 vmcoreinfo-2.6.32-25-generic
-rw-r--r--  1 root root 1.2K 2010-11-24 16:00 vmcoreinfo-2.6.32-26-generic
-rw-r--r--  1 root root 1.2K 2010-12-02 02:50 vmcoreinfo-2.6.32-27-generic
-rw-r--r--  1 root root 1.2K 2011-01-11 03:20 vmcoreinfo-2.6.32-28-generic
-rw-r--r--  1 root root 3.9M 2010-09-16 21:14 vmlinuz-2.6.32-24-generic
-rw-r--r--  1 root root 3.9M 2010-10-17 02:46 vmlinuz-2.6.32-25-generic
-rw-r--r--  1 root root 3.9M 2010-11-24 15:57 vmlinuz-2.6.32-26-generic
-rw-r--r--  1 root root 3.9M 2010-12-02 02:48 vmlinuz-2.6.32-27-generic
-rw-r--r--  1 root root 3.9M 2011-01-11 03:18 vmlinuz-2.6.32-28-generic

Гнилые старые ядра забили мой бут и не дают обновляться! Стереть их! Однако стирать нужно аккуратненько, через apt-get. Немного погуглив я нашел вот этот хороший гайд. Продублирую его здесь оставив только варианты команд для няки убунты (для второй няки генту их там и не было).

1. Узнаем, какое ядро используем сейчас:
uname -r
2.6.32-28-generic

2. Узнаем, какие ядра установлены в системе
dpkg --list 'linux-image*'
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Cfg-files/Unpacked/Failed-cfg/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                              Version                           Description
+++-=================================-=================================-==================================================================================
un  linux-image                                                   (no description available)
un  linux-image-2.6                                               (no description available)
ii  linux-image-2.6.32-24-generic     2.6.32-24.43                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-25-generic     2.6.32-25.45                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-26-generic     2.6.32-26.48                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-27-generic     2.6.32-27.49                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-2.6.32-28-generic     2.6.32-28.55                      Linux kernel image for version 2.6.32 on x86/x86_64
ii  linux-image-generic               2.6.32.28.32                      Generic Linux kernel image

Вот все эти позорные саботажники апгрейда! Казнить их! (кроме двух последних, действующего кернела и запасного старого).
3. Удаляем ядрышки ненужных версий (в статье, на которую я ссылаюсь, флага --purge нету, не знаю есть ли тут существенная разница, но --purge должен почистить всякие остаточные файлы)
sudo apt-get remove --purge linux-image-2.6.32-24-generic
sudo apt-get remove --purge linux-image-2.6.32-25-generic
sudo apt-get remove --purge linux-image-2.6.32-26-generic

4. Дополнительно погуглив, обнаружил, что после такого удаления остаются некоторые депенденси. Срубаем головы саботажникам (удаляем linux-headers для соответствующих удаленных ядер)
sudo apt-get remove --purge linux-headers-2.6.32-24-*
sudo apt-get remove --purge linux-headers-2.6.32-25-*
sudo apt-get remove --purge linux-headers-2.6.32-26-*

В результате ядра удалены, /boot освобожден. Однако в результате выполнения команд у меня появились предупреждения такого рода:
dpkg: warning: while removing linux-image-2.6.32-24-generic, directory '/lib/modules/2.6.32-24-generic' not empty so not removed
Возможно стоит вручную почистить эти каталоги, пока не стал этого делать.

пятница, 25 февраля 2011 г.

Установка Trac

Получив готовую чистую систему, я приступил к установке душки трака и няшки свн.
До этого, работая в десктоп убунте, я не слишком задавался вопросами управления пакетами, ставил все что нужно, а это нужное подтягивало дефолтные депенденси и все были счастливы.
Однако на серваке такой подход недопустим и пришлось проработать то, что я в итоге ставлю. В этом плане очень удобной оказалась программа aptitude. Пару хоткеев для нее:
  • / - поиск пакетов
  • + - установить пакет
  • - - удалить пакет
  • Enter - на пакете: описание пакета вместе с депенденси; на свернутой группе: свернуть/развернуть
  • F7 - вернуться на предыдущий экран
  • Ctrl+-> - релиз ноты выделенного пакета
  • g - применить все изменения
Этих хоткеев мне и хватило чтобы просмотреть все депенденси трака, выбрать нужные. Аптитуд очень удобно подсвечивает красным, все чего не хватает. Поэтому перед пометкой трака как устанавливаемый, я прошелся по всем красным, поотмечал предпочтительные варианты где был выбор, и уже затем отметил трак. Важно понимать, что при пометке пакета как устанавливаемый все необходимые депенденси которых не хватает (и которые заранее не были помечены в рамках текущей установки) будут автоматически помечены, но при этом приобретут статус автоматических депенденси, которые удаляются при отсутствии зависящих от них пакетов. Пакеты непосредственно выбранные пользователем самодостаточны и требует непосредственной комманды для удаления.
Таким образом одним махом я поставил apache2, trac, subversion, graphviz. В качестве базы данных я использовал sqlite3.
Настройка трака не сложна, однако наличие многочисленных и не полных гайдов сбивает.
Я взял за основу этот гайд. Приведу кратко здесь то, что использовал если вдруг вики по ссылке выпилят.

1. Создал папку для Subversion и в ней папку для проекта:
sudo mkdir /srv/svn
sudo mkdir /srv/svn/diplopod

2. Создал репозиторий в папке проекта и передал права юзеру www-data (юзер создался автоматически при установке апача):
sudo svnadmin create /srv/svn/diplopod
sudo chown -R www-data /srv/svn/diplopod

3. Создал папку для Trac и в ней папку для проекта:
sudo mkdir /srv/trac
sudo mkdir /srv/trac/diplopod

4. Создал инстанс нового трак-проекта и передал права:
sudo trac-admin /srv/trac/diplopod initenv
#здесь зададут пару вопросов, на вопрос о пути к бд ответить по умолчанию
#а путь к свн вводить полный: /srv/svn/diplopod
sudo chown -R www-data:www-data /srv/trac/diplopod
5. Для следующего шага необходимо убедиться, что установлен пакет libapache2-mod-python (да-да, это тот самый пакет. который все не рекомендуют использовать по причине скоро его депрекейшна, однако настройка трака через WSGI показалась мне непозволительным по времени досугом). После этого надо убедиться, что есть симлинк /etc/apache2/mods-enabled/python.load  на ../mods-available/python.load. В файле конфигурации апача /etc/apache2/httpd.conf добавляем:
#set up Trac handling
    SetHandler mod_python
    PythonHandler trac.web.modpython_frontend
    PythonOption TracEnvParentDir /srv/trac
    PythonOption TracUriRoot /projects
    PythonOption TracLocale "ru_RU.UTF-8"



    AuthType Basic
    AuthName "Trac"
    AuthUserFile /srv/trac/.htpasswd
    Require valid-user

6. Замечаем добавление локали ru_RU.UTF-8 в первом блоке. Я добавил ее, чтобы даты отображались в траке в привычном для нас формате (по-умолчанию там MM/DD/YY что как бы совсем не удобно). На данный момент это единственный способ изменить формат даты - через локаль. Для функционирования локали нужно проверить наличие пакета locales, установить если отсутствует. Далее добавляем ру-локаль:
sudo locale-gen ru_RU.UTF-8
sudo dpkg-reconfigure locales

7. Далее генерим файл с паролями для трак-логина:
sudo htpasswd -c .htpasswd admin
Здесь флаг "-c" устанавливается только при создании первого юзера. В дальнейшем вызывать эту же команду но без этого флага.

8. Добавляем права админу трака:
sudo trac-admin /srv/trac/diplopod permission add admin TRAC_ADMIN
9. Для возможности влиять на тикеты через свн коммиты добавляем в /srv/trac/diplopod/conf/trac.ini :
[components]
tracopt.ticket.commit_updater.* = enabled

...смотрим сюда. Зачеркнутое верно лишь для версии трака 0.12.
Теперь когда коммит включает какое-либо изменение для тикета #123 мы в сообщении коммита пишем "Refs #123 - такие то изменения" и в тикете будет добавлена ссылка на этот коммит. Если же коммит фиксает тикет и мы хотим сразу тикет закрыть, то в сообщении коммита пишем "Fixes #123 - такие-то изменения".

10. Остается добавить поддержку свн репозитория через проткоол http. Для этого в файле /etc/apache2/mods-available/dav_svn.conf добавляем:

     DAV svn
     SVNParentPath /srv/svn
     SVNListParentPath On
     AuthType Basic
     AuthName "Subversion Repository"
     AuthUserFile /etc/subversion/passwd
    
        Require valid-user
    


Здесь используется файл с паролями /etc/subversion/passwd. Он генерится следующим образом:
sudo htpasswd -c /etc/subversion/passwd svn_user
Отличие от шага 7 в том, что здесь мы создаем юзера для Subversion, а там создавали для трака. Как и там флаг "-c" здесь только для первого юзера.

11. Перезагружаемся
sudo /etc/init.d/apache2 restart

Ну и кажется всё. После этого все должно заработать.
Графвиз-плагин включается через ГУИ админки, пока не пробовал его.











четверг, 24 февраля 2011 г.

Подготовка дисков и разделов

Итак после первого впечатления от знакомства с сервачком приступил к установке системы.
Предустановлена была няшная кубунта, и, в последний раз вглянув на красивые сглаженные окошки со всплывающими свистелками и перделками, я ребутнулся чтобы остаться в суровом консольном мире навсегда.
Как уже было сказано, fake-RAID от интела оказался сплошным разочарованием, так что я просто его отключил, перевев контроллер дисков в режим Advanced Host Controller Interface (AHCI). Отличается от стандартного IDE режима наличием дополнительных фич, таких как hot swap дисков и Native Command Queuing (NCQ). Последнее теоретически должно придать перфомансу дискам. Более подробно про IDE vs AHCI здеся.
В итоге я получил 4 отдельных диска на этапе установки.
Для установки использовал лайв сиди Ubuntu 10.04 server edition. К слову, 10.04 это Long Term Support (LTS) релиз, который для серверного варианта поддерживается 5 лет. Юху!
Установка в целом прошла гладко, если не считать крайней конфузный интерфейс для настройки софтового RAID с помощью mdraid, предлагаемого по-умолчанию визардом. Очень неудобно и результаты тех или иных действий порой совершенно нелогичны. Это привело к тому что я отбросил первоначальную идею навернуть поверх всего этого бутерброда еще и Logical Volume Manager (LVM). На фриноде порекомендовали LVM как средство постоянного аптайма сервака при отсутствии каких-либо заметных ухудшений производительности. Ну да хрен с ним, еще будет возможность переставить.
Лучшее, как говорится. враг хорошего и после мучительных стенаний относительно разбиения/собирания дисков в рейды я пришел к следующей суровейшей системе, которая конечно же будет работать хуже самой простой.

Первые 4 ГБ Дисков 1 и 2 в RAID0 - R0
Следующие 90 ГБ Дисков 1 и 2 в RAID1  - R1.1
Оставшиеся 900 ГБ Дисков 1 и 2 в RAID1 -  R1.2
Диски 3 и 4 полностью в RAID1 - R1.3
Рейды R1.2 и R1.3 в RAID0  - R10

На выходе получаем R0 - 8 ГБ, R1.1 - 90 ГБ, R10 - 1.9 ТБ
На R0 организовываю swap
R1.1 разбиваю на два партишна: 30 ГБ ext3 под / , 60 ГБ ext3 под /srv
На R10 ставлю xfs, замаунченый на /var

Вот такой вот винегрет, хз что из этого выйдет, будем тестировать.
К слову, создать файловую систему на R1.1  на этапе установки я смог лишь одну, ту которая под рут. Попытка создать вторую на 60 ГБ под /srv приводила лишь к страшному красному окну с надписью "Failed!". Пришлось ручками уже на рабочей системе допиливать. Использовал при этом устройства /dev/md* которые были созданы mdraid-ом.
# /dev/md3 это мой R1.1, на котором разделы для / и /srv
sudo fdisk /dev/md3

Command (m for help):  n
Command action
   e   extended
   p   primary partition (1-4)
p
Partition number (1-4):  2
...
# далее дефолтные значения начала и конца, чтобы заполнить все оставшееся место
Command (m for help): p
    Device Boot      Start         End      Blocks   Id  System
/dev/md3p1   *           1        3648    29295616   83  Linux
/dev/md3p2            3648       11671    64450667+  83  Linux
Command (m for help): w
После того как раздел был создан и изменения записаны осталось только создать файловую систему и замаунтить ее.
# без -j создалась бы ext2, а так будет ext3
sudo mke2fs -j /dev/md3p2

#смотрим в /etc/fstab  и срем кирпичом от UUID
cat /etc/fstab
#              
proc            /proc           proc    nodev,noexec,nosuid 0       0

# / was on /dev/md3p1 during installation
UUID=e6f5000d-5501-48b2-81bf-8389e6757b5c /               ext3    errors=remount-ro 0       1
...
Небольшое отступление. Почему вместо привычных /dev/sda1 в /etc/fstab мы иногда видим такие вот страшные UUID. Смысл здесь в том, что /dev/sdXX - привязка к слоту материнки. Если у вас скажем два диска в слотах 1 и 2,  и вы скажем приобрели новый диск и вам вдруг позарез нужно вставить этот новый диск между двумя старыми, то старые разместите в слотах 1 и 3, а новый в 2. В итоге получим несоответствие в системе, которая ожидает один из старых дисков в слоте 2, а получает там новый диск. Примдется бутаться как-нить по особому и править fstab. При использовании же UUID ничего править не придется, все заработает сразу, так как UUID раздела не зависит от того, где стоит диск. Как видно, ситуация в крайней степени сферическая, но чтобы построить новое, сохранить лучшее я решил таки замапить и свой раздел через UUID:
# узнаем UUID раздельчика
sudo dumpe2fs /dev/md3p2 | grep -i uuid
Filesystem UUID:          36fbe572-4678-47be-a477-4d28d4a6ae07

sudo vim /etc/fstab
# и добавляем строчки
# /srv was on /dev/md3p2
UUID=36fbe572-4678-47be-a477-4d28d4a6ae07 /srv            ext3    defaults        0       2

Здесь defaults - маунт флаги (точнее их отсутствие), 0 - не производить никаких бекап манипуляций с разделом, 2 - проверка ошибок файловой системы осуществлять позже чем у рута. Сохраняем.
Так как система теперь использует UUID вместо устройства, следует самим помнить что скрывается под тем или иным UUID в fstab, для этого я и добавил коммент по аналогии с теми, которая умненькая убунточка сама сгенерила. Однако это всего лишь для удобства. Если же устройство забылось то всегда можно узнать его по данному UUID:
ls -l /dev/disk/by-uuid/36fbe572-4678-47be-a477-4d28d4a6ae07
lrwxrwxrwx 1 root root 11 2011-02-24 11:06 /dev/disk/by-uuid/36fbe572-4678-47be-a477-4d28d4a6ae07 -> ../../md3p2
После прописанного маунта остается либо ребутнуться либо вручную замаунтить наш раздел:
sudo mount /dev/md3p2 /srv
После этого диски были готовы к работе. Ура!

среда, 23 февраля 2011 г.

Встреча с г-ном Диплоподом

После некоторой бумажной волокиты привезли-таки мне продакшн сервачок. Сервак будет использоваться для проекта, который я назвал Diplopod-ом (злые языки пытались меня убедить, что такого слова нет, а есть только Diplopoda, но викидикшнери свидетельствует), что можно перевести как многоножка или сороконожка, а точнее обозначает особь одного из классов многоножек - двупарноногих многоножек. Смысл названия, помимо банального быдляк-стиля следования моде многих софт-продуктов именоваться всякими животными и растениями, весьма прост - это будет система по обработке данных, которая будет выполнять одновременно много всевозможных задач. Поэтому такой системе нужно много ножек. Может не совсем всё очевидно, элегантно и красиво, однако главная цель названия достигнута - люди не путают этот проект с другими, которые скучковались на таких солидных терминах как "мониторинг", "сервер", "аналитик". Достаточно сказать одно слово и всем понятно о чем речь.
Так вот сервак привезенный ничем особым не выделяется. Не сказать, что зрелый сервер, но и не десктоп:
  • процессор Intel Xeon 4 ядра по 2.3 ГГц
  • 8 Гб DDR3 памяти
  • 4 диска с вращением 7.2к на 1 Тб каждый
  • интеловский корпус, интеловская материнка S3420GPLC
  • все остальное не интересно
Достаточно серьезным разочарованием стала материнка от интела. Было заявлено наличие RAID контроллера и при заказе я ожидал обнаружить там настоящий RAID, который позволит абстрагировать организацию дисков от ОС и уже в самой ОС пользоваться одним устройством-массивом. Однако я был наивен и не знал, что ушлые интеловцы на самом деле используют дешевый вариант так называемого "Firmware RAID" ( насколько я понял после всяческих чтений это то же самое что и "Fake RAID"). Принцип действия я так и не понял, однако факт, что софт (будь то установленная система или загруженная с лайв сиди) видит собранные в рейд диски по отдельности. Кроме того сами интеловцы заявляют о необходимости поддержки этого рейда операционкой и предоставляют драйверы для отдельных систем. Зовется это безобразие Intel Matrix RAID, которое в последствии было переименовано в Intel Rapid Storage Technology (хватило совести убрать RAID из названия). Так что, будьте осторожны. К слову другая супер технология от интела - Embedded RAID со всевозможными вариациями суффиксов и префиксов, по всей видимости всё то же самое говно только в другой обертке.
И вроде бы в BIOS настраиваешь, и вроде бы на сайте интела нету никаких подозрительных упоминаний в спеке (вот такая то технология и все тут), но реальность сурова и беспощадна!
Вобщем по RAID незачет.
Память планирую расширять, 8 гигов очевидно узкое место, больше взять не получилось по техническим причинам.
Неплохо бы добавить еще один или два диска (как раз два слота осталось).
Ставить буду убунту сервер. Посмотрим что за зверь.

вторник, 22 февраля 2011 г.

PostgreSQL и WAL

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

PostgreSQL использует WAL (Write-Ahead Logging). Суть WAL такова. При изменении данных в базе файлы, содержащие данные, не меняются на лету. Вместо этого изменения сперва логгируются в отдельные WAL-файлы, то есть в WAL-файлах сохраняется полное описание производимых изменений. Полное описание означает, что если с самого начала жизни БД изменения вообще не отражать в БД, а только логировать WAL-файлами, то в любой момент можно будет скормить эти WAL-файлы базе и получить в итоге то же состояние БД, как если бы мы отражали изменения в БД сразу.

Благодаря этой технологии нам не обязательно сразу сбрасывать произведенные изменения на файлы данных по окончании каждой транзакции. Даже если база навернется мы сможем восстановить изменения по WAL-файлам. Это дает определенные преимущества при работе с файлами. Во-первых можно одним махом сбрасывать сразу несколько изменений на файлы данных, во-вторых запись WAL-файлов последовательная, а изменения в базе во многих случаях предполагают random access на диске. Так что вместо того, чтобы постоянно дергаться в разные места на диске наша база сперва собирает определенный объем изменений и только потом начинает его выполнять.
Часто с помощью WAL организуют онлайн backup - архивируют все WAL логи. Благодаря этому можно восстановить базу на любой момент времени.

Как часто в PG происходит синхронизация изменений на файлы данных? Точки в последовательности транзакций когда происходит такая синхронизация называются Checkpoint-ами. После синхронизации соответствующие WAL-файлы могут быть удалены. Чекпоинты определяются двумя настройками постгреса: checkpoint_segments и checkpoint_timeout. Первый определяет максимальное число сегментов WAL-файла, по достижении которого нужно производить синхронизацию, второй - максимальное число секунд. по прошествии которых нужно синхронизировать. Что первее происходит, то и генерит чекпоинт. По дефолту это 3 сегмента (48 мегов) WAL-логов и 300 сек соответственно.

Папка с WAL-файлами в постгресе зовется pg_xlog. И для лучшей производительности при обильных записях ее помещают на отдельный диск. Это позволяет избавиться от прыганий головки диска - от данных к WAL-файлам и обратно.
Как правило в папке не более чем  (2 + checkpoint_completion_target) * checkpoint_segments + 1 файлов (каждый из которых по умолчанию 16 мегов). checkpoint_completion_target - значение от 0 до 1 для балансировки нагрузки при синхронизации изменений. По умолчанию 0.5 - означает, что в среднем синхронизация изменений будет заканчивать на половине времени до начала следующей синхронизации. Указанный порог файлов в pg_xlog может быть превышен в случае длительных транзакций.

Информация указана для PostgreSQL 9.0