Тестирование производительности языка Go в сетевых приложениях

В последнее время на хабре проскальзывало несколько постов посвященных языку программирования Go, но все они по большей части носили достаточно поверхностный и ознакомительных характер. Это натолкнуло меня на мысль провести более глубокое тестировани этого языка. Для начала пару слов почему меня заинтересовал именно этот язык, а не Python, Ruby, Java и тд. Основным языком разработки последние 4 годя для меня является PHP, и поэтому я давно чуствовал потребность в изучении языка который удовлетворял бы следующим критериям:

  1. Быстрота. Уточню — не на 15-25% быстрее чем в PHP/Perl/Ruby (в хорошую погоду / с попутным ветром / в цикле вычисляющем число с тремя единичками/<добавьте свое условие>), а реально быстрее — в разы, а еще лучше на порядок быстрее чем популярные интерпретируемые языки.
  2. Простота написания. Да писать на C/C++ это круто, это для настоящих суровых программеров(а еще лучше взять и отдельные куски кода на asm переписать, чтобы «еще быстрее» стало), но этот путь не для меня.
  3. Удобство написания демонов. Конечно на их Perl/PHP тоже пишут… но я я думаю все согласяться что по надежности и стабильности работы они никогда не сравняться с аналогами написанными на C/C++.

На мой взгляд Go идеально подходит под эти критерии — хотя я уверен у каждого из читателей будет свое мнение на эту тему. Python/Perl/<....> тоже идеально подходят для этого, но этот пост не о них. Так как язык Go привлек меня в первую очередь с точки зрения написания сетевых демонов, то и тестировать его я буду на сетевых задачах. В качестве тестовой задачи я выбрал написание echo демона работающего по websockets протоколу.
Другой важной особенностью языка Go о которой обычно забывают упоминуть — это реализация в нем легковесных процессов(как например в Erlang или OCaml). В теории это должно резко повысить масштабируемость сетевых приложений написанных на Go, но хотелось бы узнать как дело обстоит на самом деле.

И так приступим непосредственно к тестам. На удивление написание сервера не заняло много времни:

//echod.go
package echod
import (
"http"
"io"
"websocket"
)

//Обработчик соединения
func EchoServer(ws *websocket.Conn) {
io.Copy(ws, ws)
}

func main() {
http.Handle("/echo", websocket.Handler(EchoServer))
err := http.ListenAndServe(":12345", nil)
if (err != nil) {
panic("ListenAndServe: " + err.String())
}
}


Все что от нас потребовалось это написать функцию обработчик соединения и использовать встроенную в Go реализацию websocket сервера. Для тестирования производительности новоиспеченного демона нам понадобиться клиент, который будет создавать для него нагрузку:

//echoclient.go
package echoclient;

import (
"websocket"
"log"
)

func echo() {
ws, err := websocket.Dial("ws://localhost:12345/echo", "", "http://localhost/")
defer ws.Close()

if err != nil {
panic("Dial: " + err.String())
}
pingMsg := "Hello, echod!\n"
if _, err := ws.Write([]byte(pingMsg)); err != nil {
panic("Write: " + err.String())
}
var receivedMsg = make([]byte, len(pingMsg) + 1);
if n, err := ws.Read(receivedMsg); err != nil {
panic("Read: " + err.String())
} else {
receivedMsg = receivedMsg[0:n]
}
if receivedStr := string(receivedMsg); pingMsg != receivedStr {
log.Stdoutf("Strings not equal !%s!,!%s!, %i, %i ", pingMsg, receivedStr, len(receivedStr), len(pingMsg))
}
}

func main() {
echo()
}


Как вы можете убедиться написание websocket клиента заняло еще меньше времени. Но запускать каждый тесты вручную и замерять время нехочтся, поэтому я использовал для этих целей встроенный в язык Go механизм юнит тестировния и профилирования. Для этого достаточно создать файл echoclient_test.go и написать в нем функции вида:
func TestEcho(t *testing.T) {} — для выполнения юнит тестирования и
func BenchmarkEcho(b *testing.B) {} — для выполнения профилирования соответсвенно.
Ниже исходный код функции которую я использовал для профилирования:

package echoclient;

import (
"testing"
)
//Функция используемая для запуска echo клиента в отдельном процессе
func echoRoutine(c chan int, index int) {
//Обработка ошибок, об этом подробнее ниже по тексту
defer func() {
//проверяем была ли какая-нибуть ошибка 
if err := recover(); err != nil {
print("Routine failed:", err, "\n")
c <- -index
} else {
c <- index
print("Exit go routine\n") 
}
}()

echo()
}
// Непосредственно сам бенчмарк
func BenchmarkCSP(b *testing.B) {
//b.N = 2000; - если нужно подсказываем профилировщику сколько раз мы хотим прогнать тест

// канал используемый для синхронизации потоков
c := make(chan int)
for i:= 0; i < b.N; i++ {
// запускаем каждый клиент в отдельной goroutine
go echoRoutine(c, i)
}
print("Fork all routines \n") 
var (
success, failed int
)
for i:= 0; i < b.N; i++ {
//ждем когда поток вернет результат выполнения
// если он отрицательный тест провален, если положительный соответсвенно выполнен
index := <- c
if index < 0 {
failed++
print(i, ". Goroutine failed:", -index, "\n")
} else {
success++
print(i, ". Goroutine:", index, "\n")
}
}
print("Totals, success:", success," failed: ", failed, " \n")
}


Теперь для того чтобы прогнать тесты нам достаточно запустить утилиту gotest и усказать ей регулярные выражения для выбора запускаемых тестов и бенчмарков. Несколько слов о том что же делает данный код — он одновременно запускает заданное количество легковесных потоков (goroutines), каждый из которых пингует наш echo сервер.
Остановлюсь немного подробнее на обработке ошибок. В Go она построена на основе 3-х встроенных функций — defer, recover и panic. Это аналог конструкции try… catch в других языках программирования, который позволяет поймать и обработать абсолютно любую ошибку. Ключевое defer говорит компилятору о том что данную функцию необходимо выполнить непосредственно при выходе из текущей функции, причем независимо от того каким образом завершилось ее выполнение — в результате какой-либо ошибки(panic) или обычным образом. Если исполнение функции завершилось в результате ошибки то мы можем восстановить нормальное выполнение программы путем вызова функции recover.

Результаты тестов




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


А также зависимость количества успешных/провальных попыток от количества одновременных соединений:


Немного об условиях проведения тестов:
И клиент и сервер запускались на одной и тойже машине, с весьма скромными характеристиками —
Intel® Pentium® Processor T2390 (1M Cache, 1.86 GHz, 533 MHz FSB), 2GB RAM.

Выводы


Язык показал довольно высокую производительность в сетевых приложениях, но он еще недостаточно стабилен. В ходе проведения тестов обнаружилось что при нагрузке выше 1500 одновременных соединений сам сервер начал регулярно падать. Возможно дело в том что я исползовал 32 битный компилятор, в то время как основной разработчики считают именно 64 битную версию. Хочется верить что к моменту выхода протокола websockets в массы разработчики смогут довести язык Go до стабильности, достаточной для использования его в качестве основного языка для написания высоконагруженных websocket демонов.

Компактный и гибкий Javascript шаблонизатор

В последние дни на Хабре одна за одной вышли несколько статей посвященных шаблонизаторам на Javascript.
Идея вынести «View» из паттерна MVC на сторону клиента очень интересная, но поизучав существующие билиотеки я понял что мне категорически не хватает их возможностей. Простейший обход свойств объекта зачастую был невозможен.
Естественным образом родилась идея использовать в качестве языка разметки тот же Javascript.

Код получился весьма компактным и сходу удалось применить его в реальных задачах. Можно использовать всю гибкость Javascript, так как на выходе шаблонизатор его и генерит.
Читать дальше →

Что такое Media RSS?

Написание этой статьи навеяло, скудность информации в сети по столь полезной вещи как Media RSS. Media RSS — это расширенная спецификация RSS которая позволяет доставлять медиа-контент (аудио, видео и изображения). С самой описанием спецификации можно ознакомиться тут.

Самое распространенное применение mRSS получила для индексации видео в сети, поэтому поговорим именно об этом. В настоящий момент google и яндекс, yahoo успешно поддерживают формат mRSS. Причем если Вы на своем сайте регулярно размещаете видео, неважно физически расположено оно у Вас на сервере или для этого используются специализированные видео-серверы (youtube, vimeo, rutube), Вы имеете возможность проиндексировать его. После чего видео размещенное на сайте будет доступно поисковикам, это может оказаться очень не плохим подспорьем в продвижение вашего сайте.

В случае с Google Вы можете ссылку на Ваш mRSS указать в качестве sitemap через инструменты веб-разработчика. Для остальных поисковых систем можно просто разместить ссылку, например на главной, аналогично обычной RSS ленте, и видео попадет в индекс. Хочу обратить Ваше внимание, что часто встречается ситуация что не все видео будет индексируется, с этим придется смириться.

Теперь хочу рассказать не много о формате. Пример варианта mrss.xml:

<?xml version="1.0" encoding="UTF-8"?> <rss xmlns:media="http://search.yahoo.com/mrss/" xmlns:dcterms="http://purl.org/dc/terms/" version="2.0"> <channel> <title>Заголовок</title> <link>http://www.site.ru</link> <description>Описание</description> <image> <title>Заголовок изображения</title> <url>http://www.site.ru/images/logo.png</url> <link>http://www.site.ru</link> <width>145</width> <height>122</height> </image> <item> <title>Заголовок для отображения конктерного видео в ленте RSS</title> <link>http://site.ru/myvideo1</link> <description>Описание видео в ленте RSS</description> <pubDate>Tue, 21 Sep 2010 15:51:59 +0400</pubDate> <media:content duration="209"> <media:title type="plain">Заголовок видео для поисковика</media:title> <media:description type="plain">Описание видео для поисковика</media:description> <media:player url="http://vimeo.com/moogaloop.swf?clip_id=1"/> <media:thumbnail url="http://b.vimeocdn.com/ts/907/862/90786246_200.jpg"/> </media:content> <media:community> <media:tags>теги, через, запятую</media:tags> </media:community> </item> </channel> </rss>


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

В заголовке добавляется объявления пространства имен, в нашем случае достаточно использовать только

xmlns:media=«search.yahoo.com/mrss/»

<rss xmlns:media=«search.yahoo.com/mrss/» version=«2.0»>

Собственно основной элемент <media:content>

<media:content duration=«209»>

Для него необходимо задать время видео ролика в нашем случае duration=«209», этим атрибутом можно пренебречь но гугл настоятельно рекомендует его использовать.

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

<media:content url=«www.site.com/movie.mov»>

Также для этого элемента существует большой набор атрибутов (fileSize, type, medium, etc) подробнее об их назначение Вы можете узнать прочитав спецификацию. Для индексации видео этими атрибутами можно пренебречь.

Элемент media:content по сути является контейнером который содержит всю необходимая информация о самом видео:
<media:title> — заголовок видео
<media:description> — описание видео
<media:player> — указывается атрибут url который содержит ссылку на плеер с указанием видео.

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

<media:player url=«vimeo.com/moogaloop.swf?clip_id=1»/>

где clip_id в данном случае уникальный идентификатор видео в системе vimeo.ru

<media:thumbnail> — превью для видео

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

Например в случае vimeo информация о любом файле доступна в виде xml файла
vimeo.com/api/v2/video/id.xml
где id — это уникальный идентификатор видео на сайте

Ну а далее я думаю не представляет труда получить необходимую Вам информацию.

Вторичный элемент это <media:community> — будем говорить социальная составляющая ролика (рейтинг, количество просмотров, теги и пр). В нашем случае достаточно использовать <media:tags> для размещения тегов, так как это наиболее значимый элемент для поисковой индексации.

Но для более полного отображения после поисковых запросов я рекомендую все таки использовать <media:statistics> и <media:starRating>

<media:statistics> — статистика по видео: просмотры и избранное

<media:statistics views=«132» favorites=«24»/>

<media:starRating> — информация о рейтинге видео: оценка, количество голосов, минимальная и максимальная оценка

<media:starRating average=«3.5» count=«20» min=«1» max=«10»/>

Надеюсь после прочтения Вы узнаете что то новое и это тайное знание пригодиться Вам для развития Ваших проектов. Если эта статья Вас заинтересует я хотел бы рассказать о практическом создание mRSS. Удачи!

Стоило ли так ждать Internet Explorer 9?

Корпорация Microsoft наконец-то представила публичную бета-версию IE9, которая отличается от предыдущих тестовых версий наличием полноценного интерфейса. И если о движке браузера многое было известно уже заранее, то новая внешность IE должна была стать сюрпризом для всех. Интерес пользователей постоянно подогревался различными утечками информации, скриншотами и даже видео работы нового интерфейса. В результате к презентации первой бета-версии IE9 было приковано внимание почти всех ведущих мировых IT-СМИ и немалого колличества простых пользователей. Получился ли сюрприз? Ответ читайте в этом топике.


Читать дальше →

Как взобраться на рельсы или первые шаги в освоении Ruby on Rails

Предыстория

Ruby on Rails — фрэймворк на языке Ruby, предназначенный для разработки веб-приложений. Сам Ruby был написан этим милым японцем в 1995 году. В 2003 году Дэвид и Джэйсон начали работу над системой управления проектами под названием Basecamp. В поиске инструмента для разработки выбор пал на Ruby. К июлю 2004 году был выпущен всеми ныне любимый Ruby on Rails, как результат «причесанного» программного каркаса, который был написан для Basecamp.

Читать дальше →

Аналог cookeis в spring remoting

В браузере есть способ хранить и передавать некоторую дополнительную информацию, которая не присутствует в параметрах при вызове GET или не хранится в теле запроса POST. Данная функциональность реализована с помощью кук.
Для браузера это позволяет создать некоторое постоянное хранилище данных на стороне клиента, с помощью которых сервер может идентифицировать клиента.
Spring Remoting можно реализовать поверх протокола http, однако ни одного упоминания про куки я не нашел. Попробуем реализовать их самостоятельно.

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

package foo;
public interface FooService {
   public String hello(String name);
}



package foo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class FooServiceImpl {
   private Log logger = LogFactory.getLog(getClass());
   public String hello(String name) {
      logger.info("Greeting "+name);
      return "Hello "+name;
   }
}



<bean id="fooService" class="foo.FooServiceImpl">

<bean name="/FooService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
   <property name="service" ref="fooService"/>
   <property name="serviceInterface" value="foo.FooService"/>
</bean>


На клиенте мы получаем ссылку на сервис:

<bean id="fooService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
   <property name="serviceUrl" value="http://${server.address}:${server.port}/server/remoting/FooService"/>
   <property name="serviceInterface" value="foo.FooService"/>
</bean>


Для начала реализуем способ хранения кук на клиенте. Так как клиент — swing-приложение, то в качестве хранителя можно использовать обычный синглтон. Все куки будем хранить в карте, ключ — имя куки.

package foo;

import java.util.*;
import java.io.Serializable;

public class CookiesHolder {

    private Map<String, Serializable> cookies;

    public void addCookie(String name, Serializable o) {
        if (cookies == null) {
            cookies =new HashMap<String, Serializable>();
        }
        cookies.put(name, o);
    }

    public Serializable getCookie(String name) {
        return cookies!=null ? cookies.get(name) : null;
    }

    public Set<String> getCookiesNames() {
        if (cookies==null) {
            return Collections.emptySet();
        }
        else {
            return cookies.keySet();
        }
    }
}



<bean id="cookieHolder" class="core.service.remote.CookiesHolder" />



Далее необходим способ передать куки на сервер при вызове любого метода. Изучая класс org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean, я наткнулся на метод 

/**
 * Set the RemoteInvocationFactory to use for this accessor.
 * Default is a {@link DefaultRemoteInvocationFactory}.
 * <p>A custom invocation factory can add further context information
 * to the invocation, for example user credentials.
 */
public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
   this.remoteInvocationFactory =
    (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory());
}


По описанию выглядит как то, что нам нужно. Если изучить код
RemoteInvocationFactory, мы увидим единственный метод:

RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation);

Очевидно, что RemoteInvocation содержит данные о том, какой метод вызывается, какие параметры в него передаются. Посмотрим на код RemoteInvocation. Сразу же бросается в глаза поле

private Map attributes;

смотрим описание:
/**
* Add an additional invocation attribute. Useful to add additional
* invocation context without having to subclass RemoteInvocation
* Attribute keys have to be unique, and no overriding of existing
* attributes is allowed.
* The implementation avoids to unnecessarily create the attributes
* Map, to minimize serialization size.
* @param key the attribute key
* @param value the attribute value
* @throws IllegalStateException if the key is already bound
*/
public void addAttribute(String key, Serializable value) throws IllegalStateException {


Итак, при вызове метода сервиса, есть возможность, переопределив RemoteInvocationFactory, поместить в RemoteInvocation дополнительные атрибуты, в частности, наши куки:

package foo;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
import org.springframework.remoting.support.RemoteInvocation;

import java.io.Serializable;

public class CookiesBasedRemoteInvocationFactory extends DefaultRemoteInvocationFactory {
   
    private CookiesHolder cookiesHolder;
    @Override
    public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
        final RemoteInvocation invocation = super.createRemoteInvocation(methodInvocation);
        final Application instance = Application.getInstance();
        CookiesHolder holder;
        if (instance!=null) {
            holder = instance.getCookiesHolder();
        }
        else {
            holder = new CookiesHolder();            
        }
        for (String name : holder.getCookiesNames()) {
            final Object value = holder.getCookie(name);
            if (value instanceof Serializable) {
                invocation.addAttribute(name, (Serializable) value);
            }
        }
        return invocation;
    }

    public void setCookiesHolder(CookiesHolder cookiesHolder) {
        this.cookiesHolder = cookiesHolder;
    }

}



теперь изменим конфигурацию ссылки на сервис:
<bean id='cookiesBasedRemoteInvocationFactory' class="foo.CookiesBasedRemoteInvocationFactory"/>

<bean id="fooService" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
   <property name="serviceUrl" value="http://${server.address}:${server.port}/server/remoting/FooService"/>
   <property name="serviceInterface" value="foo.FooService"/>
   <property name="remoteInvocationFactory" ref="cookiesBasedRemoteInvocationFactory"/>
</bean>


Таким образом, любой вызов метода сервиса (в частности hello()) будет передавать на сервер наши куки. Осталось их получить на сервере. Будем действовать аналогичным способом.

Создаем структуру для хранения кук:

package foo;

import org.springframework.util.Assert;

public class ServerCookiesHolder {

    private static ThreadLocal contextHolder = new ThreadLocal();

    public static void clearContext() {
        contextHolder.set(null);
    }

    public static CookiesHolder getContext() {
        if (contextHolder.get() == null) {
            contextHolder.set(new CookiesHolder());
        }

        return (CookiesHolder) contextHolder.get();
    }

    public static void setContext(CookiesHolder context) {
        Assert.notNull(context, "Only non-null CookiesHolder instances are permitted");
        contextHolder.set(context);
    }

}



Изучаем HttpInvokerServiceExporter, находим метод setRemoteInvocationExecutor, который устанавливает обработчика вызова метода. Переопределяем обработчик:

package foo;

import org.springframework.remoting.support.DefaultRemoteInvocationExecutor;
import org.springframework.remoting.support.RemoteInvocation;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.util.Set;

public class CookiesBasedRemoteInvocationExecutor extends DefaultRemoteInvocationExecutor {

    @Override
    public Object invoke(RemoteInvocation invocation, Object targetObject) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        final Map map = invocation.getAttributes();
        if (map!=null) {

            final Set<String> set = map.keySet();
            final CookiesHolder cookiesContext = ServerCookiesHolder.getContext();
            for (String name : set) {
                cookiesContext.addCookie(name, invocation.getAttribute(name));
            }
        }
        return super.invoke(invocation, targetObject);
    }
}


Изменяем конфигурацию сервиса:


<bean id="cookiesBasedRemoteInvocationExecutor" class="foo.CookiesBasedRemoteInvocationExecutor"/>

<bean name="/FooService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
   <property name="service" ref="fooSearcher"/>
   <property name="serviceInterface" value="foo.FooService"/>
   <property name="remoteInvocationExecutor" ref="cookiesBasedRemoteInvocationExecutor"/>
</bean>



Вуаля. Проверяем. Измененный код сервиса:


   public String hello(String name) {
      logger.info("Greeting "+ServerCookiesHolder.getContext().getCookie("realName"));
      return "Hello "+name;
   }






Код вызова сервиса на клиенте:

public void test() {
   CookiesHolder.setCookie("realName", "Foo");
   fooService.hello("Fred");
}

Алармы — хорошо или плохо? Текущее положение дел в wap'e

Многие из вас далеки от этого «маленького интернета», который в народе принято называть «wap».
Но, все таки, хочу рассмотреть нынешнюю, не легкую ситуацию, которая сложилась в нашем рувапе.


Читать дальше →

«Именованные параметры» в Delphi

Иногда возникает ситуация, когда в функцию требуется передавать много различных параметров, но при этом необходимый набор этих параметров может сильно различаться. В таких случаях, для Delphi, как правило, есть несколько путей решения:
  1. Просто забить все возможные параметры в интерфейс функции.
  2. Сделать множество перегрузок функции на все случаи жизни.
  3. Передать параметры массивом.
  4. Воспользоваться обходным путём. Например, вынести параметры в класс и проставлять их перед вызовом функции.
Всё эти способы получаются довольно громоздкими в реализации и имеют массу недостатков. А самое главное, что над их реализацией необходимо думать в каждом конкретном случае отдельно — не существует простого общего решения.

В некоторых языках (Scala, Python, Ruby...) такой проблемы не стоит в принципе — там есть такая замечательная вещь как именованные параметры. В Delphi же приходится всегда следовать установленному порядку аргументов. Не спасают даже значения по-умолчанию (их не всегда можно применить из-за конфликта типов, к тому же их использование нередко приводит к путанице).

Однако, используя небольшую хитрость, в Delphi вполне можно написать, к примеру, вот так:

ProcessParams(Par('Param1', 'test') + Par('Param2', 38) + Par('Param3', 3.2));

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


Читать дальше →