Wednesday, July 27, 2011

Удалённый рабочий стол в Linux

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

Однако, что же делать, если хочется именно поработать за своей офисной машиной, находясь при этом дома?




Windows


В случае, если на работе Windows, то всё просто - сервер терминалов (работающий по протоколу RDP) уже присутствует в системе (в зависимости от версии ОС в разной степени обрезанности) и к нему нужно лишь подключиться. Если на клиенте (дома) Windows, то для подключения можно воспользоваться программой mstsc (более известной как "Удалённый рабочий стол"). Если же на клиенте Linux, то вам поможет замечательный пакет rdesktop.


Linux


Но что же делать, если нужно подключиться к машине под управлением Linux? Да, ssh - это правильный ответ, но с его помощью можно подключиться лишь к консоли, а не к рабочему столу. Если вы суровый админ, то X-ы вам действительно ни к чему. Но если вы, например, адекватный программист, то прекрасно понимаете, что быдлокодить с помощью Eclipse гораздо приятней/продуктивней, чем с помощью vi.

Понятно, что решением в данном случае будет установка на рабочей машине сервера терминалов. Для семейства ОС Linux де-факто стандартным протоколом для удалённого доступа к рабочему столу является VNC. В качестве сервера для Linux можно использовать пакетом vnc4server. В качестве клиента для под Linux - gvncviewer / tightvncviewer, для Windows - тот же tightvncviewer или mRemote.


Vnc4server


Запускается vnc4server одноимённой командой:
# vnc4server
New '127.0.0.1:1 (root)' desktop is 127.0.0.1:1

Останавливается немного сложнее:
# vnc4server -kill :1

Настройки хранятся в создаваемом при первом запуске файле ~/.vnc/xstartup.


P.S.


Так же можно попробовать поднять на клиенте (дома) X-сервер и настроить офисную машину на работу с ним... Но это на мой взгляд уж больно хитро и нестабильно.


Read more...

Thursday, June 16, 2011

Проблема N+1 SELECT'ов

Есть в мире ORM такая проблема как N+1 SELECT. Заключается она в следующем. Пусть у нас есть два домена, связанных между собой отношением one-to-many: например, заказ и его элементы.

@Entity
class Order {
  @Id
  private Long id;

  @Column(name="info", nullable=false)
  private String info;

  @OneToMany
  private List<Item> items;

  // Getters and setters
}

@Entity
class Item {
  @Id
  private Long id;

  @Column(name="info", nullable=false)
  private String info;

  @ManyToOne
  private Order order;

  // Getters and setters
}

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

Order order = em.find(Order.class, 1L);
System.out.println(order.getInfo());

for (Item item : order.getItems()) {
  System.out.println(item.getInfo());
}

В данном примере (с учётом lazy fetch'a) будет выполнено 1 + N запросов к БД. Один запрос - на чтение информации о заказе, N запросов - на чтение информации о его элементах. Это безусловно грустно и неэффективно.

Если вы используете Hibernate, то проблема легко может быть решена с помощью HQL-запроса с явным указанием типа fetch'а для свойства items [1]. Вариант с повсеместным использованием eager fetch'a не рассматривается в виду его брутальности.

Если же у вас нет никакого ORM'а, то всё ещё проще: один запрос на чтение данных о заказе, один запрос на чтение данных элементов заказа. Если нужно прочесть несколько заказов - тоже самое: один запрос на чтение заказов, один запрос на чтение элементов заказов (всех вместе).


P.S.


Сей пост меня сподвигла написать моя последняя задача, суть которой заключалась в рефакторинге с целью увеличения производительности проекта. Банально исправив в быдлокоде несколько проблем с выбором N+1 записи (в ключевых местах проекта) удалось увеличить скорость его работы в 10-20 раз.
Read more...

Tuesday, May 31, 2011

Annotation driven development

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


И так, обратите внимание на следующую службу:

/**
 * Служба работы с фичами
 *
 * @author Mikhail Krestyaninoff
 * @since 31.05.2011
 */
@Service("featureService")
public class FeatureService {

@PersistenceContext
private EntityManager entityManager;

/**
     * Добавление фичи
     *
     * @param feature
     * Фича
     */
@Loggable
@Catchable("Ошибка добавления фичи")
@Secured("ROLE_ADMIN")
@Transactional(rollbackFor = java.lang.Exception.class)
@TriggersRemove(cacheName = "feature", removeAll = true)
public void addFeature(Feature feature) throws Exception {
entityManager.persist(feature);
}

/**
     * Получение фич
     *
     * @return Список фич
     */
@Loggable
@Cacheable(cacheName = "feature")
@Transactional(readOnly=true)
public List<Feature> getFeatures() {
return entityManager.createQuery("from Feature as feature",
Feature.class).getResultList();
}
}


Элегантно, неправда ли? Почти весь не относящийся к бизнес-логике функционал вынесен за скобки - внутри методов остаётся только бизнес-логика (в данном случае работа с БД).

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

  • @Loggable - логирование и профилирование метода (самописный аспект);

  • @Catchable - оборачивание исключений нижележащих слоёв в исключения слоя бизнес-логики (самописный аспект);

  • @Cachable / @TriggersRemove - кеширование (Ehcache Annotations for Spring);

  • @Transactional - управление транзакциями (Spring Framework);

  • @Secured - безопасность и разграничение прав (Spring Security).


Только представьте, сколько не относящегося к сути кода, нам удалось не написать! :)

Read more...

Tuesday, March 22, 2011

Выделенный сервер логов

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

Понятно, что делать всё вышеперечисленное удобно в одном месте. Да и вообще, плюсов выделенного сервера логов можно выделить как минимум несколько:

  • единое место хранения / формат логов;

  • удобство настройки политики обработки логов;

  • вынос с серверов приложений непрофильной для них задачи;

  • повышение безопасности (чистка логов становится не такой простой задачей).



Как же нам реализовать такой выделенный сервер логов? К счастью, у нас есть *nix с его с его системой логирвания - syslog. Syslog - это демон, способный обрабатывать запросы на логирование от ядра системы, различных служб и приложений, а так же удалённых серверов. Протокол работы syslog стандартизирован в RFC 3164.

Существует несколько реализаций syslog'a: syslogd, syslog-ng, rsyslogd (реализации расположены в порядке возрастания их навороченности). В данной статье приводятся примеры для syslog-ng.



Syslog оперирует следующими понятиями (настраиваемыми в его конфиге - /etc/syslog-ng/syslog-ng.conf):

  • source - источник логов (файл, сеть и т.д.);

  • filter - фильтр логов (тип сервиса, приоритет, имя приложения, хост, regexp и т.д.);

  • destination - направление записи лога (файл, консоль, сеть и т.д.);

  • log - структура, объединяющая source, filter, destination.


Более подробное описание можно посмотреть в man-странице конфига (man syslog-ng.conf).

Ещё двумя важными атрибутами syslog являются тип источника сообщения (facility) и уровень логирования (severity). Оба атрибута очень удобно применять для фильтрации.

Рассмотрим пример конфигурации syslog для обработки логов некоторого приложения:

# Источник
source s_all {
# Собственные сообщения
internal();

# Стандартный источник логов в Linux (место, куда пишет функция syslog())
unix-stream("/dev/log");

# Сообщения от ядра
file("/proc/kmsg" log_prefix("kernel: "));

# Сообщения по UDP (порт 514)
udp();
};

# Направления
destination df_my_app_err { file("/var/log/my-app.err.log" template("$MESSAGE\n")); };
destination df_my_app_common { file("/var/log/my-app.log" template("$MESSAGE\n")); };

# Фильтры
filter f_my_app_err { facility(user) and level(err,crit,warn,notice); };
filter f_my_app_common { facility(user) and program(my_app) and not level(err,crit,warn,notice); };

# Логи
log {
source(s_all);
filter(f_my_app_err);
destination(df_my_app_err);
flags(final);
};
log {
source(s_all);
filter(f_my_app_common);
destination(df_my_app_common);
flags(final);
};


Настройки для ротации и сжатия находятся в отдельном конфигурационном файле (/etc/logrotate.d/syslog-ng).


Log4J


Работа с syslog'ом реализована уже почти во всех уважающих себя фреймворках для логирования. Так, например, Log4J имеет специальный appender для записи в syslog:

#log4j.appender.syslog=org.apache.log4j.net.SyslogAppender
#log4j.appender.syslog.syslogHost=log-server
#log4j.appender.syslog.layout=org.apache.log4j.PatternLayout
#log4j.appender.syslog.layout.ConversionPattern=my-app: %d{yyyy-MM-dd HH:mm:ss} %5p - %m%n
#log4j.appender.syslog.Facility=USER


Стоит заметить, что syslog довольно криво обрабатывает stacktrace'ы, в виду того, что экранирует все управляющие символы (\t, \n) и удаляет пробелы из начала строк. Решением тут может послужить кастомизация стандартного SyslogAppender для Log4J с учётом всех странностей особенностей syslog.


PHP



В PHP есть нативная поддержка работы с syslog (функция syslog). Однако, не всё так просто - работать PHP умеет только с локальным сервером syslog (это же касается и Log4PHP, суть соответствующего appender'а которого сводиться к вызову PHP-фунции syslog). Для работы с удалённым сервером придётся писать свою реализацию клиента, работающего через запись в сокет.


Полезные ссылки




Read more...

Saturday, March 12, 2011

Выступление перед публикой

Сегодня посетил мероприятие по подготовке докладчиков к выступлению на конференции CodeFest. Главным пунктом программы был тренинг ораторского мастерства, основные моменты которого я и хочу резюмировать в данном посте.


При выступлении перед публикой следует обращать внимание на следующие моменты:

  • поддерживать постоянный визуальный контакт с аудиторией;

  • использовать жестикуляцию (в меру, а не размахивать руками);

  • явно расстанавливать акценты в речи;

  • следить за темпом речи;

  • следить за громкостью речи;

  • следить за своей позой (руки должны быть на уровне живота, а не в карманах);

  • использовать мимику/эмоции;

  • не использовать листочек с подсказками листочек с подсказками лучше держать при себе (очень добавляет уверенности), но при этом пользоваться им лишь как указкой.




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

  • придумайте какую-нибудь ассоциацию относительно последней вашей темы;

  • используйте последнее сказанное вами слово, как начало нового предложения (работает в связке с ассоциациями);

  • задайте аудитории какой-нибудь тематический вопрос: риторический, закрытый (да/нет).



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

  • Кто моя аудитория?

  • Какую цель я преследую?

  • В чём должен выражаться результат успеха моего выступления?

  • Что может не понравиться мне (например, тролли), и как я с этим буду бороться?

  • Что может не понравиться аудитории (например, скучный/неинтересный доклад), и что сделать, что бы все остались довольны?



Общие советы:

  • Представьтесь: где работаете, чем занимаетесь;

  • Слайды - опорный материал: меньше букв, больше слов;

  • Шутки / картинки, для разряжения обстановки;

  • Брать фидбек: остановиться, спросить, не уснул ли кто?



И в качестве бонуса - крайне няшная презентация о том, как делать презентации - "Смерть от PowerPoint"!


Read more...

Tuesday, March 1, 2011

Знакомство с АОП

Парадигмы программирования


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

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

  • инструкция (императивное программирование, FORTRAN/C/PHP),

  • функция (функциональное программирование, Haskell/Lisp/F#/Scala),

  • прототип (прототипное программирование, JavaScript),

  • объект (объектно-ориентированное программирование, С++/Java),

  • факт (логическое программирование, PROLOG).


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

В этой статье я хочу рассказать о сравнительно молодой, но крайне, на мой взгляд, полезной парадигме программирования – аспектно-ориентированном программировании.




Основы АОП


Рассмотри некоторую сферическую службу в вакууме (например, web-сервис), реализующую следующий метод:
public BookDTO getBook(Integer bookId) {
BookDTO book = bookDAO.readBook(bookId);
return book;
}

Метод довольно прост и очевиден: чтение информации о некоторой книге по её идентификатору. Но давайте подумаем, чего тут не хватает? Первым делом нам стоит задуматься о логировании – без него, как вы сами понимаете, в web-службе никуда:
public BookDTO getBook(Integer bookId) {
LOG.debug("Call method getBook with id " + bookId);

BookDTO book = bookDAO.readBook(bookId);

LOG.debug("Book info is: " + book.toString());
return book;
}


Далее необходимо реализовать обработку исключений (сделать так, что бы слой служб возвращал соответствующие ему исключения, скрывая исключения нижележащих слоёв):

public BookDTO getBook(Integer bookId) throws ServiceException {
LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;

try {
book = bookDAO.readBook(bookId);
} catch(SQLException e) {
throw new ServiceException(e);
}


LOG.debug("Book info is: " + book.toString());
return book;
}


Так же не стоит забывать о проверке прав доступа:

public BookDTO getBook(Integer bookId) throws ServiceException, AuthException {
if (!SecurityContext.getUser().hasRight("GetBook"))
throw new AuthException("Permission Denied");


LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;

try {
book = bookDAO.readBook(bookId);
} catch(SQLException e) {
throw new ServiceException(e);
}

LOG.debug("Book info is: " + book.toString());
return book;
}


Кроме того имеет смысл кешировать результат работы:

public BookDTO getBook(Integer bookId) throws ServiceException, AuthException {
if (!SecurityContext.getUser().hasRight("GetBook"))
throw new AuthException("Permission Denied");

LOG.debug("Call method getBook with id " + bookId);
BookDTO book = null;
String cacheKey = "getBook:" + bookId;

try {
if (cache.contains(cacheKey)) {
book = (BookDTO) cache.get(cacheKey);
} else {

book = bookDAO.readBook(bookId);
cache.put(cacheKey, book);
}

} catch(SQLException e) {
throw new ServiceException(e);
}

LOG.debug("Book info is: " + book.toString());
return book;
}


Можно продолжать совершенствовать данный метод, но для начала - достаточно. В ходе наших доработок мы получили метод в 10 раз (с 2 до 20 LOC) превышающий исходный размер. Самое интересное, что объём бизнес-логики в нём не изменился – это всё та же 1 строка. Остальной код реализует некоторую общую служебную функциональность приложения: логирование, обработку ошибок, проверку прав доступа, кеширование и так далее.

В принципе, переплетение бизнес-логики со служебным функционалом не так страшно, пока ваше приложение невелико. Однако, чем сложнее становится программа, тем более тщательно следует подходить к её архитектуре в целом и выделении общей функциональности в частности. Именно поэтому, наблюдая за эволюцией языков программирования, сначала мы видим появление функций, потом модулей, затем объектов. Однако, практика показывает, что для выделения некоторой общей функциональности, упомянутых выше парадигм недостаточно. Такую функциональность называют «сквозной» или «разбросанной», в виду того, что её реализация действительно разбросана по разным частям приложения. Примерами сквозной функциональности, как мы уже видели выше, могут служить:

Основной задачей аспектно-ориентированного программирования (АОП) является модуляризация сквозной функциональности, выделение её в аспекты. Для этого языки, поддерживающие концепцию АОП, реализуют следующие средства для выделения сквозной функциональности:

  • аспект (aspect) – модуль или класс, реализующий сквозную функциональность. Аспект изменяет поведение остального кода, применяя совет в точках соединения, определённых некоторым срезом. Так же аспект может использоваться для внедрения функциональности;

  • совет (advice) – дополнительная логика - код, который должен быть вызван из точки соединения. Совет может быть выполнен до, после или вместо точки соединения;

  • точка соединения (join point) - точка в выполняемой программе (вызов метода, создание объекта, обращение к переменной), где следует применить совет ;

  • срез (pointcut) - набор точек соединения. Срез определяет, подходит ли данная точка соединения к заданному совету;

  • внедрение (introduction) - изменение структуры класса и/или изменение иерархии наследования для добавления функциональности аспекта в инородный код;

  • цель (target) – объект, к которому будут применяться советы;

  • переплетение (weaving) – связывание объектов с соответствующими аспектами (возможно на этапе компиляции, загрузки или выполнения программы).




Пример использования (AspectJ)


AspectJ является аспектно-ориентированным расширением/framework’ом для языка Java. На данный момент это, пожалуй, самый популярный и развивающийся АОП движок.

Рассмотрим реализацию аспекта логирования с его помощью:
@Aspect
public class WebServiceLogger {
private final static Logger LOG =
Logger.getLogger(WebServiceLogger.class);

@Pointcut("execution(* example.WebService.*(..))")
public void webServiceMethod() { }

@Pointcut("@annotation(example.Loggable)")
public void loggableMethod() { }

@Around("webServiceMethod() && loggableMethod()")
public Object logWebServiceCall(ProceedingJoinPoint thisJoinPoint) {
String methodName = thisJoinPoint.getSignature().getName();
Object[] methodArgs = thisJoinPoint.getArgs();

LOG.debug("Call method " + methodName + " with args " + methodArgs);

Object result = thisJoinPoint.proceed();

LOG.debug("Method " + methodName + " returns " + result);

return result;
}
}


Первым делом создаётся аспект логирования методов сервисов – класс WebServiceLogger, помеченный аннотацией @Aspect. Далее определяются два среза точек соединения: webServiceMethod (вызов метода, принадлежащего классу WebService) и loggableMethod (вызов метода, помеченного аннотацией @Loggable). В завершении объявляется совет (метод logWebServiceCall), который выполняется вместо (аннотация @Around) точек соединения, удовлетворяющих срезу ("webServiceMethod() && loggableMethod()").

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

AspectJ обладает довольно большим объёмом поддерживаемых срезов точек пересечения. Ниже приведены основные из них:

  • execution(static * com.xyz..*.*(..)) – выполнение кода любого статического метода в пакете com.xyz;

  • call(void MyInterface.*(..)) – вызов любого метода, возвращающего void, интерфейса MyInterface;

  • initialization(MyClass || MyOtherClass) – инициализация класса MyClass или MyOtherClass;

  • staticinitialization(MyClass+ && !MyClass) – статическая инициализация класса, имя которого начинается на MyClass, но не сам MyClass;

  • handler(ArrayOutOfBoundsException) – выполнение обработчика исключения ArrayOutOfBoundsException;

  • get/set(static int MyClass.x) - чтение / запись свойства x класса MyClass;

  • this/target(MyClass) – выполнение точки соединения, соответствующей объекту типа MyClass;

  • args(Integer) – выполнение точки соединения, в которой доступен аргумент типа Integer;

  • if(thisJoinPoint.getKind().equals("call")) – совпадает со всеми точками соединения, в которых заданное выражение истинно;

  • within/withincode(MyClass) - совпадает со всеми точками соединения, встречающимися в коде заданного класса;

  • cflow/cflowbelow(call(void MyClass.test())) – совпадает со всеми точками соединения, встречающимися в потоке выполнения заданного среза;

  • @annotation(MyAnnotation) – выполнение точки пересечения, цель которой помечена аннотацией @MyAnnotation.



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

  • before – запуск совета до выполнения точки соединения,

  • after returning - запуск совета после нормального выполнения точки соединения,

  • after throwing - запуск совета после выброса исключения в процессе выполнения точки соединения,

  • after - запуск совета после любого варианта выполнения точки соединения,

  • around – запуск совета вместо выполнения точки соединения (выполнение точки соединения может быть вызвано внутри совета).


Подробнее о конструкциях AspectJ можно прочитать в соответствующем разделе [1,2] официальной документации.

Для того, что бы использовать аспекты AspectJ их придётся скомпилировать и «вшить» в основные классы с помощью специального компилятора AJC.

Продукт бесплатный. Распространяется под Eclipse License.


Пример использования (PostSharp)



PostSharp является аспектно-ориентированным framework’ом для платформы .NET. Существуют и другие реализации АОП для .NET, однако, судя по сравнениям с сайта PostSharp, лидирующую позицию занимает именно он.

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

public class ExceptionDialogAttribute : OnExceptionAspect
{
public override void OnException(MethodExecutionEventArgs eventArgs)
{
string message = eventArgs.Exception.Message;
Window window = Window.GetWindow((DependencyObject)eventArgs.Instance);
MessageBox.Show(window, message, "Exception");
eventArgs.FlowBehavior = FlowBehavior.Continue;
}
}


Строго говоря, аспекты в терминологии PostSharp – это, как мы можем видеть, аспект и совет в терминологии АОП.

Для того, что бы указать срез точек пересечения для данного аспекта необходимо в файл настроек сборки (AssemblyInfo.cs) добавить следующую строку:

[assembly: ExceptionDialog ( AttributeTargetTypes="Example.WorkflowService.*",
AttributeTargetMemberAttributes = AttributeTargetElements.Public )]


Или же явно пометить интересующие вас методы атрибутом ExceptionDialog:

[ExceptionDialog]
public BookDTO GetBook(Integer bookId)


Вот собственно и всё: теперь все выброшенные в соответствующих методах исключения будут обрабатываться созданным аспектом.

В виду того, что PostSharp частично склеивает понятия совета и аспекта, последних у него получается довольно немало. Подробно с ними можно познакомиться в документации. Ниже приведены основные из них:

  • OnMethodBoundary/OnMethodInvocation – обращение к методу (начало, конец, выход, выход с исключением);

  • OnFieldAccess – обращение к свойству;

  • OnException – обработка исключения;

  • Composition – внедрение кода;



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

Продукт платный. Есть Community Edition.


От теории к практике



И так, мы только что увидели, как красиво и эффективно можно решить проблему «выноса за скобки» сквозного функционала в вашем приложении. Однако, это всё теория. На практике всё, естественно, немного иначе :)

Прежде всего, в обоих случаях для компиляции и «вшивания» (weaving) аспектов придётся использовать специальный компилятор и тащить вместе с проектом дополнительные библиотеки. Вроде бы, это не проблема: компилятор легко скачивается и интегрируется в среду (например, при использовании maven’a задача сведётся всего лишь к добавлению плагина aspectj-maven-plugin), а множество зависимостей – обычное дело, по крайней мере для Java-приложений (решаемая с помощью того же maven’a) . Однако, необходимость включения в проект чего-то, что требует отдельной компиляции, да ещё и не имеет широкого распространения, зачастую отпугивает разработчиков, не смотря на все потенциальные плюсы.

В данном случае решением проблемы может стать Spring Framework [1,2]. Данный фреймворк имеет много достоинств, однако в рамках данной статьи нас интересует его AOP-составляющая. Spring Framework реализует ограниченную AOP-функциональность на чистом Java (C#) без использования сторонних библиотек с помощью создания прокси-объектов (JDK Dynamic Proxy, CGLIB). Другими словами в Spring AOP можно использовать только точки соединения типа «выполнение метода». Однако, как показывает практика, данное ограничение не играет значительной роли, так как для решения большинства задач, требуется точки соединения именно этого типа.

Кроме того, Spring Framework поддерживает конфигурирование приложений c помощью @AspectJ аннотаций, а так же интеграцию аспектов скомпилированных непосредственно с помощью AspectJ.

У себя в компании мы используем именно Spring AOP. Учитывая прочие заслуги Spring Framework, на мой взгляд, он является самой доступной и удобной площадкой для работы с AOP, внося значительный вклад в его популяризацию и развитие.


Резюме



Подводя итоги, хочется выделить три основные мысли относительно АОП:

  • Основная цель АОП - выноса «общей» (сквозной) функциональности «за скобки» (модуляризация сквозной функциональности);

  • Для Java AOP доступен через проект AspectJ, для .NET – через PostSharp;

  • Наиболее простая и проверенная реализация AOP – Spring AOP.




Ссылки по теме





P.S.


Во время в вёрстки этой статьи наткнулся на довольно няшную и в тоже время простую систему подсветки кода. На много приятней, чем Source Code Highlighter из Хабраредактора.

Read more...

Wednesday, February 16, 2011

Масштабирование нагрузки web-приложений

С ростом популярности web-приложения его поддержка неизбежно начинает требовать всё больших и больших ресурсов. Первое время с нагрузкой можно (и, несомненно, нужно) бороться путём оптимизации алгоритмов и/или архитектуры самого приложения. Однако, что делать, если всё, что можно было оптимизировать, уже оптимизировано, а приложение всё равно не справляется с нагрузкой?


Оптимизация


Первым делом стоит сесть и подумать, а всё ли вам уже удалось оптимизировать:

  • оптимальны ли запросы к БД (анализ EXPLAIN, использование индексов)?

  • правильно ли хранятся данные (SQL vs NoSQL)?

  • используется ли кеширование?

  • нет ли излишних запросов к ФС или БД?

  • оптимальны ли алгоритмы обработки данных?

  • оптимальны ли настройки окружения: Apache/Nginx, MySQL/PostgreSQL, PHP/Python?


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


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


И так, допустим, что оптимизация уже проведена, но приложение всё равно не справляется с нагрузкой. В таком случае решением проблемы, очевидно, может послужить разнесение его по нескольким хостам, с целью увеличения общей производительности приложения за счёт увеличения доступных ресурсов. Такой подход имеет официальное название – «масштабирование» (scale) приложения. Точнее говоря, под «масштабируемостью» (scalability) называется возможность системы увеличивать свою производительность при увеличении количества выделяемых ей ресурсов. Различают два способа масштабирования: вертикальное и горизонтальное. Вертикальное масштабирование подразумевает увеличение производительности приложения при добавлении ресурсов (процессора, памяти, диска) в рамках одного узла (хоста). Горизонтальное масштабирование характерно для распределённых приложений и подразумевает рост производительности приложения при добавлении ещё одного узла (хоста).

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


Архитектура приложения


Большинство web-приложений априори являются распределёнными, так как в их архитектуре можно выделить минимум три слоя: web-сервер, бизнес-логика (приложение), данные (БД, статика).

Диаграмма

Каждый их этих слоёв может быть масштабирован. Поэтому если в вашей системе приложение и БД живут на одном хосте – первым шагом, несомненно, должно стать разнесение их по разным хостам.


Узкое место


Приступая к масштабированию системы, первым делом стоит определить, какой из слоёв является «узким местом» - то есть работает медленнее остальной системы. Для начала можно воспользоваться банальными утилитами типа top (htop) для оценки потребления процессора/памяти и df, iostat для оценки потребления диска. Однако, желательно выделить отдельный хост, с эмуляцией боевой нагрузки (c помощью AB или JMeter), на котором можно будет профилировать работу приложения с помощью таких утилит как xdebug, oprofile и так далее. Для выявления узких запросов к БД можно воспользоваться утилитами типа pgFouine (понятно, что делать это лучше на основе логов с боевого сервера).

Обычно всё зависит от архитектуры приложения, но наиболее вероятными кандидатами на «узкое место» в общем случае являются БД и код. Если ваше приложение работает с большим объёмом пользовательских данных, то «узким местом», соответственно, скорее всего будет хранение статики.


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


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

Снизить нагрузку на БД можно разнеся её на несколько хостов. При этом остро встаёт проблема синхронизации между ними, решить которую можно путём реализации схемы master/slave с синхронной или асинхронной репликацией. В случае с PostgreSQL реализовать синхронную репликацию можно с помощью Slony-I, асинхронную – PgPool-II или WAL (9.0). Решить проблему разделения запросов чтения и записи, а так же балансировки нагрузку между имеющимися slave’ами, можно с помощью настройки специального слоя доступа к БД (PgPool-II).

Проблему хранения большого объёма данных в случае использования реляционных СУБД можно решить с помощью механизма партицирования (“partitioning” в PostgreSQL), либо разворачивая БД на распределённых ФС типа Hadoop DFS.

Об обоих решениях можно почитать в замечательной книге по настройке PostgreSQL.

Однако, для хранения больших объёмов данных лучшим решением будет «шардинг» (sharding) данных, который является встроенным преимуществом большинства NoSQL БД (например, MongoDB).

Кроме того, NoSQL БД в общем работают быстрее своих SQL-братьев за счёт отсутствия overhead’а на разбор/оптимизацию запроса, проверки целостности структуры данных и т.д. Тема сравнения реляционных и NoSQL БД так же довольно обширна и заслуживает отдельной статьи.

Отдельно стоит отметить опыт Facebook, который используют MySQL без JOIN-выборок. Такая стратегия позволяет им значительно легче масштабировать БД, перенося при этом нагрузку с БД на код, который, как будет описано ниже, масштабируется проще БД.


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


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

Далее необходимо настроить балансировку нагрузки/запросов между этими хостами. Сделать это можно как на уровне TCP (haproxy), так и на HTTP (nginx) или DNS.

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

Файлы статики можно смонтировать с некого общего файлового хранилища по NFS/CIFS или использовать распределённую ФС (HDFS, GlusterFS, Ceph).

Так же можно хранить файлы в БД (например, Mongo GridFS), решая тем самым проблемы доступности и масштабируемости (с учётом того, что для NoSQL БД проблема масштабируемости решена за счёт шардинга).

Отдельно стоит отметить проблему деплоймента на несколько хостов. Как сделать так, что бы пользователь, нажимая «Обновить», не видел разные версии приложения? Самым простым решением, на мой взгляд, будет исключение из конфига балансировщика нагрузки (web-сервера) не обновлённых хостов, и последовательного их включения по мере обновления. Так же можно привязать пользователей к конкретным хостам по cookie или IP. Если же обновление требует значимых изменений в БД, проще всего, вообще временно закрыть проект.


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


При необходимости хранения большого объёма статики можно выделить две проблемы: нехватка места и скорость доступа к данным. Как уже было написано выше, проблему с нехваткой места можно решить как минимум тремя путями: распределённая ФС, хранение данных в БД с поддержкой шардинга и организация шардинга «вручную» на уровне кода.

При этом стоит понимать, что раздача статики тоже не самая простая задача, когда речь идёт о высоких нагрузках. Поэтому в вполне резонно иметь множество серверов предназначенных для раздачи статики. При этом, если мы имеем общее хранилище данных (распределённая ФС или БД), при сохранении файла мы можем сохранять его имя без учёта хоста, а имя хоста подставлять случайным образом при формировании страницы (случайным образом балансирую нагрузку между web-серверами, раздающими статику). В случае, когда шардинг реализуется вручную (то есть, за выбор хоста, на который будут залиты данные, отвечает логика в коде), информация о хосте заливки должна либо вычисляться на основе самого файла, либо генерироваться на основании третьих данных (информация о пользователе, количестве места на дисках-хранилищах) и сохраняться вместе с именем файла в БД.


Мониторинг


Понятно, что большая и сложная система требует постоянного мониторинга. Решение, на мой взгляд, тут стандартное – zabbix, который следит за нагрузкой/работой узлов системы и monit для демонов для подстраховки.


Заключение


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


Read more...

Tuesday, January 25, 2011

Особенности выделения памяти под OpenVZ для java-приложений

OpenVZ как система виртуализации хороша тем, что позволяет легко балансировать ресурсы хост-системы между своими контейнерами (что, по сравнению с тем же Xen'ом, является явным преимуществом). Однако, у неё, конечно, есть и недостатки. Одним из них является отсутствие явной поддержки swap'а.

Сам по себе swap на хост-системе вы использовать, естественно, можете. Но вот внутри контейнера процессам он будет недоступен. Другими словами, выделяя память под контейнер (параметр privvmpages), вы должны учитывать, что её объём должен быть не меньше всей потенциально необходимой вашим процессам памяти.

Отдельное внимание в рамках данного вопроса стоит обратить на java-приложения. В случае, если ваша программа вызывает множество внешних процессов (ярким примером такой программы будет Maven), вам стоит быть готовым, что для его работы потребуется гораздо больше памяти, чем кажется на первый взгляд.
Происходит это из-за того, что каждый вызов внешнего приложения в java (Runtime.getRuntime().exec()) сводится к системному вызову fork() linux'a c последующим замещением кода дочернего процесса. При fork'e дочернему процессу выделяется тоже количество памяти, что и родительскому. Не смотря на то, что в современных ядрах системный вызов fork() сводиться к системному вызову clone(), а, следовательно, должен реализовывать механизм Copy-On-Write, для java-приложений это не играет роли в виду особенностей инициализации памяти в JVM. Таким образом, если вы запускаете приложение, которому необходимо 40Мб, и это приложение делает 10 вызовов внешних приложений, то для того, что бы не получить java.io.IOException: error=12, Cannot allocate memory, вам потребуется выделить для openvz-контейнера минимум 40 + 40*10 = 440Мб.

Поэтому, при нехватке памяти под OpenVZ, первым делом следует посмотреть текущее значение параметра privmpages для контейнера с помощью команды cat /proc/user_beancounters, вторым - увеличить его значение в конфиге контейнера до нужного объёма.

P.S.
Кстати, в ходе своих изысканий наткнулся на один довольно интересный параметр ядра Linux - overcommit_memory (cat /proc/sys/vm/overcommit_memory), с помощью которого можно регулировать политику выделения памяти в системе. По-умолчанию ядро может выделить процессу большее количество памяти, чем реально у него есть (с учётом swap'a). Но это уже совсем другая история.
Read more...