Pull to refresh
492.9
Яндекс
Как мы делаем Яндекс

Облачная платформа Яндекса: Cocaine в действии

Reading time 21 min
Views 37K
Мы уже рассказали вам, что такое Cocaine и как его развернуть «в домашних условиях». Сегодня речь пойдёт о том, как пользоваться его инфраструктурой на уровне программиста. Кстати, 26 апреля в 14:00 в московском офисе Яндекса пройдет встреча, на которой можно будет вживую пообщаться с нами — командой, которая делает Cocaine. Приходите, но не забывайте регистрироваться.



Итак, из сегодняшнего поста вы узнаете:

  • как писать приложения;
  • как пользоваться приложениями и сервисами нативно, используя предоставленные фреймворки;
  • как изменить приложение, чтобы оно отвечало по http, а также как потестировать эти приложения, используя Cocaine http proxy;
  • как написать собственный сервис.

Давайте же начнем наше погружение в «кокаиновые» будни программиста.

Этот текст достаточно объёмный и очень технический. Подразумевается, что вы читали наши предыдущие посты, и у вас есть доступ к кокаиновому облаку, на машинках которого установлены требуемые библиотеки и программы. И что вы достаточно хорошо знакомы с асинхронным программированием и вас не пугают слова: event loop, callback и корутины. Для каждого примера требования будут перечислены под катом или явно. И да, для повторения всего рассказанного совершенно не обязательно иметь кластер из 50 машин. Мне было достаточно одного ноутбука с Mac OS.

Часть 1. Работаем с Cocaine


Начнём наш обзор с того, что загрузим в облако уже сконфигурированное готовое приложение, мощь которого не знает границ, — echo. Приложение написано на Питоне и представляет собой типичный Hello World для тестирования корректности новых фреймворков.

Как загрузить приложение


Практически по всей статье будут использоваться консольные утилиты cocaine-tool, которые сами зависят от установленного cocaine-framework-python. В волшебном мире Питона данные библиотеки устанавливаются командой:

pip install cocaine-tools


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

cocaine-tool app upload


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

chmod u+x echo.py


В результате будет выведено:
Загрузка тестового приложения

Для его запуска нам понадобится готовый профиль. В случае, если у вас Cocaine настроен на использование Docker в качестве системы изоляции, можно воспользоваться профилем, приведенным в предыдущей статье. Если же Cocaine настроен на обычный process spawn, просто воспользуйтесь профилем, который я подготовил (profile.json):

{
    "isolate": {
        "args": {
            "spool": "/var/spool/cocaine"
        },
        "type": "process"
    }
}


Загрузить профиль можно аналогичной командой:

cocaine-tool profile upload --name profile@test --profile ./profile.json


Результат работы должен быть примерно таким:
Загрузка профиля
Имея готовый профиль, остается лишь запустить недавно загруженное приложение:

cocaine-tool app start --name echo --profile profile@test


Если в консоли будет выведено что-то типа такого, значит, все было проделано успешно и наше приложение теперь
часть облака.
Запуск тестового приложения
Вернемся к примеру. Пока не особо вникайте в код. Скоро мы напишем аналогичное, но более функциональное приложение, но на этот раз
уже с объяснениями. Единственное, что делает это приложение — отвечает на событие с именем ping тем же
самым сообщением, которое было ему послано.

Сейчас же наша задача — написать код, формирующий запрос и принимающий ответ от этого приложения. Стоит отметить, что кокаиновые приложения могут быть доступны как по HTTP, так и напрямую из клиентского кода. Первый способ мы рассмотрим чуть позднее, второй способ предполагает использование одного из предоставленных фреймворков. Для разминки мы напишем код на Питоне, используя питонячий фреймворк, который должен был поставиться вместе с cocaine-tool.

from cocaine.futures import chain
from cocaine.services import Service

from tornado.ioloop import IOLoop

# Alias for more readability.
asynchronous = chain.source

if __name__ == '__main__':
    io_loop = IOLoop.current()
    service = Service('echo')

    @asynchronous
    def invoke(message):
        result = yield service.enqueue('ping', message)
        print(result)

    invoke('Hello World!')
    invoke('Hello again!')
    io_loop.start()


После импортирования необходимых библиотек и классов мы просто создает объект кокаинового сервиса, используя библиотечный класс Service, передав ему в качестве параметра имя приложения (в нашем случае — echo). Этот объект представляет собой отображение методов и событий облачного сервиса/приложения на методы класса в используемом языке. Наша работа с ним заключается в том, чтобы асинхронно вызвать предоставляемый всеми приложениями метод enqueue, передав ему в качестве первого параметра имя события, а в качестве остальных параметров (внезапно) — остальные параметры, требующиеся приложению; а затем так же асинхронно дождаться ответа. Для этого мы получаем объект цикла обработки событий, используя библиотеку Tornado, запускаем его и ждем. Когда будет получен ответ, цикл не останавливается явно.

Это важно: когда писался питонячий фреймворк, то библиотека Asyncio еще не была полностью готова, и выбирать приходилось между Twisted и Tornado. Выбор пал на Tornado в основном из-за его более современного вида и функциональности. Сейчас полным ходом идет полное переписывание питонячего фреймворка на asyncio, а точнее— на его бэкпорт для второго Питона — Trollius.

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

В облако же приложение будет ходить вот так:
Поход в Облако


Таинственный декоратор @asynchronous подмешивает в декорируемую функцию дополнительные действия, а именно:
  • добавляет ее вызов в цикл обработки событий;
  • если функция является корутиной — контролирует ее процесс асинхронной раскрутки.

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

Наверняка у вас сразу появились вопросы:
  • Зачем так сложно?
  • Почему была выбрана асинхронная модель?
На них мы обязательно ответим несколькими абзацами ниже, а пока посмотрим, как Cocaine (и фреймворк соответственно)
реагирует на ошибочные запросы. Например, зададим в качестве имени приложения, передаваемому объекту Service не
echo, а какое-нибудь имя несуществующего приложения. Скажем, 42, то есть:

echo = Service('42')


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

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

Кокаиновый протокол


Давайте на минутку остановимся и заглянем под капот нашей облачной платформы. Каким образом Cocaine узнает о том, какой именно
запрос к нему пришел и как его обрабатывать? Другими словами, кратко рассмотрим протокол, по которому Cocaine общается с клиентами.

На каждый вызов Cocaine открывает типизированный канал, который позволяет обмениваться с приложением сообщениями. Сообщения могут быть трех типов:

0, Chunk    ::= <Tuple>
1, Error    ::= (<Number>, <String>)
2, Choke    ::= ()


Chunk представляет собой обычное сообщение, информацию, которую клиент хочет передать приложению или наоборот. В случае возникновенbя какой-либо ошибки приложение (или сам Cocaine, если он сможет распознать ошибку заранее) отвечает типом Error, несущим какой-то код и человекопонятное описание возникшей ошибки. Наконец, каждый канал должен закрываться сообщением типа Choke. Cocaine гарантирует, что после такого сообщения не будет больше никаких других сообщений.

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

Так как протокол является стримовым, и последним сообщением всегда приходит Choke, строго говоря, приложения могут
возвращать неизвестное количество Chunk'ов, тем самым организуя настоящую поточную обработку данных и их асинхронный
возврат.

Возвращаясь к примеру


Давайте модифицируем наше приложение, чтобы оно возвращало не один Chunk, а несколько. Скажем, два. Рассмотрим код нашего супер-приложения, из которого был выкинут логгер:

from cocaine.worker import Worker</python>
def echo(request, response):
    message = yield request.read()
    response.write(message)
    response.close()


W = Worker()
W.run({
    'ping': echo,
})


Как видно, используется практически тот же паттерн обработки, который мы писали в клиентском коде. На каждое приходящее событие с именем ping вызывается функция echo, которая в качестве аргументов принимает уже подготовленные объекты запроса и ответа. Вызывая метод request.read() мы асинхронно считываем по одному Chunk'у запроса (в нашем случае он строго один), а отвечаем — используя метод response.write(...).

Соответственно, чтобы наше приложение отвечало несколькими Chunk'ами, достаточно вызвать этот метод еще раз. Например:

def echo(request, response):
    message = yield request.read()
    response.write(message)
    response.write('Additional message')
    response.close()


Как это проверить? Ну, во-первых, приложение следует перезалить и перезапустить:

cocaine-tool app upload && cocaine-tool app restart --name echo --profile profile@test


Это важно: если измененное приложение упорно не хочет обновляться и работает по-старому даже после перезаливки,
проверьте настройки кэша в конфиге cocaine-runtime (секция cache).

После чего заново запустим наш клиент, и он выдаст… то же самое, что и раньше. Это нормальное поведение. Напомню, что протокол у нас стримовый, и мы вычитываем из канала ровно по одном Chunk'у. Чтобы все заработало, изменим функцию invoke следующим образом:

@asynchronous
def invoke(message):
    result = yield service.enqueue('ping', message)
    another_result = yield
    print(result, another_result)
    io_loop.stop()


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

Асинхронность везде


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

Для этого рассмотрим Cocaine и окружающие его фреймворки более детально. Как было отмечено в нашей первой статье, основная модель, заложенная в основу Cocaine'а — это модель акторов. Фреймворки же в свою очередь представляют собой не более чем обертки над RPC-вызовами к Cocaine и обработку его ответов. Вот, мы запросили что-то у объекта Service. Что у него происходит? После кодирования сообщения и его отправки, он, ждет. Ждет ответа, ждет ошибку, ждет наступления таймаута. Все эти ожидания — обычные IO операции, не тратящие ресурсы процессора. Так зачем давать ему лишний раз остывать, если можно дать еще работы? Представьте, что вы делаете запросы к какому-нибудь приложению или сервису не из клентского кода, а из другого приложения. Понятно, что если это приложение будет делать stop the world на каждом таком запросе, это приведет к быстрому спавнингу таких приложений, что впоследствии будет просто тратой ресурсов системы.

В таких случаях используют два распространенных подхода. В первом из них каждый запрос обрабатывается в отдельном потоке, а управление в код возвращается в основном в методе ожидания выданного future'а, во втором случае используется цикл обработки событий и кооперативная многозадачность (callback'и, coroutine'ы, fiber'ы — не важно). Но опять-таки в случае обработки тысячи одновременных запросов — что вполне себе реальная ситуация для высоконагруженных систем — первый способ дает существенный оверхед в производительности.

Именно поэтому все кокаиновые фреймворки реализуют в первую очередь асинхронный способ взаимодействия с сервисами. Некоторые в дополнение реализуют также синхронный способ, но не все.

А как же остальные языки?


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

Продемонстрируем это, написав клиентов для нескольких фреймворков на различных языках. При этом тестировать будем изначальную версию echo приложения.

C++


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

#include <cocaine/framework/services/app.hpp>
#include <cocaine/framework/services/storage.hpp>

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <atomic>

namespace cf = cocaine::framework;

int main(int argc, char** argv) {
    auto manager = cf::service_manager_t::create(cf::service_manager_t::endpoint_t("localhost", 10053));

    // Get application service object.
    auto app = manager->get_service<cf::app_service_t>("echo");

    std::atomic<int> counter(0);

    std::condition_variable cv;

    // Call application.
    auto g1 = app->enqueue("ping", "Hello from C++");
    auto g2 = app->enqueue("ping", "Hello again!");
    auto handler = [&counter, &cv](cf::generator<std::string>& g) {
        counter++;

        try {
            // Always packed data.
            std::cout << "result: " << g.next() << std::endl;
        } catch (const std::exception& e) {
            std::cout << "error: " << e.what() << std::endl;
        }

        cv.notify_all();
    };
    g1.then(handler);
    g2.then(handler);

    std::mutex m;
    std::unique_lock<std::mutex> guard(m);

    while (counter < 2) {
        cv.wait(guard);
    }

    return 0;
}


Компилировать нужно следующим образом (в случае использования clang):

clang++ -std=c++11 -stdlib=libc++ -lcocaine-core -lcocaine-framework -lmsgpack -lboost_system -lev ../src/clients/echo-client.cpp


Если же вы используете GCC, то просто замените clang++ на g++, и уберите необходимость использования libc++ вместо libstdc++, убрав -stdlib=libc++.

Особо комментировать код не буду, более подробно об использованиии native framework'а можно почитать здесь.

Ruby


В случае Ruby все совсем просто. Мы используем прекрасный фреймворк eventmachine, который предоставляет возможность
использовать Ruby Fiber и писать асинхронный код так, как будто бы он синхронный. Никаких колбэков и фьючеров!

require 'cocaine'
require 'cocaine/synchrony/service'
require 'em-synchrony'

class EchoClient
  EM.synchrony do
    results = []
    service = Cocaine::Synchrony::Service.new 'echo'
    channel = service.enqueue('ping', 'Hello from Ruby!')
    channel.each do |result|
      results.push result
    end
    puts results
    EM.stop
  end
end


Как видно из примеров, в отличии от CORBA в Cocaine нет необходимости заранее определять IDL используемых приложений и сервисов. В случае статически типизированных языков для удобства использования и проверки типов можно писать заглушки для каждого сервиса, но обычно их реализация занимает не очень много места. Если язык имеет хоть сколько-нибудь нормальный рефлекшн, то даже нет необходимости писать реализацию — достаточно лишь интерфейсов. В случае же динамически типизированных языков можно вообще обойтись без заглушек и создавать API сервиса динамически, получая ее после определения основной информации о нем во время работы метода Локатора resolve.

Часть 2. Воркеры


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

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

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

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

Возвращаясь к нашему примеру, мы дадим нашему приложению событие с именем generate. Оно будет возвращать сгенерированный QR-код в бинарном виде в виде какого-то массива байтов (ну, а как иначе?), который мы скормим специальному объекту, дабы сконвертировать их в настоящую картинку.

Это важно: при написании этого примера мы будем пользоваться питонячей библиотекой qrcode. Она зависит от известного PIL или pillow, который в свою очередь, требует множество бинарных зависимостей, например libpng. Убедитесь, что они у вас есть, а затем поставьте qrcode, используя тот же pip:

>pip install qrcode


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

#!/usr/bin/env python

import StringIO
import msgpack

import qrcode

from cocaine.exceptions import ServiceError
from cocaine.decorators import http
from cocaine.logging import Logger
from cocaine.services import Service
from cocaine.worker import Worker

storage = Service('storage')


def generate_qr(message, size=10):
    if size <= 0 or size >= 20:
        raise ValueError('size argument must fit in [0; 20]')

    out = StringIO.StringIO()
    img = qrcode.make(message, box_size=size)
    img.save(out, 'png')
    return out.getvalue()


def generate(request, response):
    rq = yield request.read()
    message, size = msgpack.loads(rq)

    try:
        if size < 10:
            data = generate_qr(message, size)
        else:
            key = '{0}size={1}'.format(message, size)
            try:
                data = yield storage.read('qr-codes', key)
            except ServiceError:
                data = generate_qr(message, size)
                yield storage.write('qr-codes', key, data)
        response.write(data)
    except Exception as err:
        response.error(1, str(err))
    finally:
        response.close()


w = Worker()
w.run({
    'generate': generate
})


Строкой storage = Service('storage') мы объявляем глобальный для воркера сервис Storage, который, напомню, мы
настраивали в конфиге cocaine-runtime во второй статье. По умолчанию это будет файловое хранилище. В данном случае нам это совершенно не важно, так как мы используем предоставляемую нам инфраструктуру облака.

Теперь обратите внимание на последние строчки:

w = Worker()
w.run({
    'generate': generate
})


Этим кодом мы создаем объект воркера, который за сценой установит подключение с Cocaine'ом, а также некоторые другие действия. Затем запускаем цикл обработки событий, попутно регистрируя обработчик события generate. Теперь любое invoke сообщение с правильными параметрами будет перенаправлено в указанную функцию. Рассмотрим ее подробнее.

В каждую такую функцию передается ровно два параметра:
  • подготовленный объект запроса, который содержит единственный метод read для отложенного чтения присланных данных
    из сокета;
  • объект ответа, который представляет собой канал, в который можно писать (вспомните описание протокола) все
    перечисленные типы сообщений (Chunk, Error, Choke).
Собственно, строками:

rq = yield request.read()
message, size = msgpack.loads(rq)


Мы считываем данные из сокета и распаковываем их. Зачем делать дополнительную запаковку, почему нельзя сразу написать что-то типа: message, size = yield request.read()? Не забывайте, что мы должны распаковать из сырых байтов какую-то структуру. В прошлый раз это была строка, в этот раз — кортеж. Нужен какой-то кодер/декодер. Для этой цели может сгодиться хоть json, хоть msgpack, хоть protobuf. Мы выбрали msgpack в этом примере, так как его реализации существуют для всех популярных языков и он быстрый.

После распаковки необходимых аргументов следует логика самого приложения. Завернув ее в большой try/except блок мы гарантируем, что все выкинутые исключения будут пойманы не фреймворком, а нами, и будут иметь необходимые нам коды ошибок и сообщения.

Это важно: если мы не поймаем исключение сами, это сделает фреймворк.

После проверки размера картинки у нас происходит ветвление. Если размер достаточно мал, то мы генерируем QR-код заново, в противном случае строками:

key = '{0}size={1}'.format(message, size)
try:
    data = yield storage.read('qr-codes', key)
except ServiceError:
    data = generate_qr(message, size)
    yield storage.write('qr-codes', key, data)


Мы формируем ключ и проверяем, есть ли такой документ в нашем хранилище по такому ключу. Если его нет, то вызванный метод storage.read('qr-codes', key) выбросит исключение. В этом случае мы все же генерируем картинку и сохраняем ее.

Это важно: операции read и write выполняются асинхронно. Каждый раз, когда мы в нашей корутине натыкаемся на ключевое слово yield, управление возвращается в цикл обработки событий. Обратно оно возвращается лишь тогда,
когда наше ожидаемое событие настало. До сих пор обрабатываются остальные события. Так, если у нас сильно тормозит
Storage, вполне реально ожидание тысяч корутин на методе read. Экономия ресурсов налицо.

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

Последовательность вызовов на этот раз будет такая:
Последовательность вызовов в примере


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



Как проверить наше приложение? Аналогичным образом, как и проверяли echo приложение. Вновь загружаем наш код в Cocaine, используя cocaine-tool:

cd src/qr && cocaine-tool app upload && cocaine-tool app start --name qr --profile profile@test


Затем пишем небольшой клиентский код. На самом деле, тут различия от клиентского кода проверки echo приложения минимальны:
import StringIO
import msgpack

from PIL import Image

from tornado.ioloop import IOLoop

from cocaine.futures import chain
from cocaine.services import Service

# Alias for more readability.
asynchronous = chain.source


if __name__ == '__main__':
    io_loop = IOLoop.current()
    service = Service('qr')

    @asynchronous
    def invoke(message):
        try:
            result = yield service.enqueue('generate', msgpack.dumps([message, 10]))
            print('Result:', result)
            out = StringIO.StringIO()
            out.write(result)
            out.seek(0)
            img = Image.open(out)
            img.save('qr.png', 'png')
        except Exception as err:
            print('Error: ', err)
        finally:
            io_loop.stop()

    invoke('What is best in life? To crush your enemies, see them driven before you, and to hear the lamentation of '
           'their women.')
    io_loop.start()


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

Видите, достаточно несложно. Давайте теперь чуть-чуть расширим приложение, добавив него дополнительное событие, научив его отвечать по HTTP.

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

В случае Питона все совсем просто: мы награждаем наш новый обработчик события @http декоратором. При этом (к сожалению Питон этого не показывает синтаксически) семантика передаваемых объектов request и response меняется. Объект запроса по-прежнему имеет единственный метод read, но в этом случае он возвращает не сырые байты, переданные в запросе, а уже подготовленный объект cocaine.decorators.http._HTTPRequest, хранящий всю необходимую информацию о запросе — заголовки, тело, параметры и т.п. Объект канала (response) в свою очередь имеет методы записи заголовков и тела ответа.

Код измененного приложения:

>#!/usr/bin/env python

import StringIO
import msgpack

import qrcode

from cocaine.exceptions import ServiceError
from cocaine.decorators import http
from cocaine.logging import Logger
from cocaine.services import Service
from cocaine.worker import Worker


storage = Service('storage')
log = Logger()


def generate_qr(message, size=10):
    if size <= 0 or size >= 20:
        raise ValueError('size argument must fit in [0; 20]')

    out = StringIO.StringIO()
    img = qrcode.make(message, box_size=size)
    img.save(out, 'png')
    return out.getvalue()


@http
def generate(request, response):
    request = yield request.read()
    try:
        message = request.request['message']
        size = int(request.request.get('size', 10))

        if size < 10:
            data = generate_qr(message, size)
        else:
            key = '{0}size={1}'.format(message, size)
            try:
                data = yield storage.read('qr-codes', key)
            except ServiceError:
                data = generate_qr(message, size)
                yield storage.write('qr-codes', key, data)

        response.write_head(200, [('Content-type', 'image/png')])
        response.write(data)
    except KeyError:
        response.write_head(400, [('Content-type', 'text/plain')])
        response.write('Query field "message" is required')
    except Exception as err:
        response.write_head(400, [('Content-type', 'text/plain')])
        response.write(str(err))
    finally:
        response.close()


w = Worker()
w.run({
    'generate-http': generate
})


Кроме декорирования функции обработчика события мы просто воспользовались новым API передаваемых объектов, никаких изменений функциональной части нет.

Обратите внимание: событие теперь называется generate-http — это просто переименование и ни на что не влияет.

Как это протестировать? Понятное дело, что раз теперь приложение отдает нам какой-то запакованный кортеж, состоящий из http-данных, то на клиентской части нам нужно всего лишь его распаковать. Но этот код мы писать не будем, потому что такие программы уже написаны, и о них было рассказано в предыдущих статьях. Речь идет о cocaine-proxy — специальной программе, которая слушает лицом HTTP, трансформирует его в запросы к облаку и делает обратные преобразования над полученным ответом. Во второй статье рассказывалось как развернуть у себя cocaine-native-proxy. В случае, если вы по каким-то причинам сделать этого не смогли, можно воспользоваться cocaine-tool, которые имеют встроенную проксю для тестовых целей, написанную на Python, но которая по функционалу совпадает с вышеуказанной:

cocaine-tool proxy start --count=32


где count — число процессов, которые будут обрабатывать входящие запросы.

Это важно: в production окружении настоятельно рекомендуется пользоваться все же cocaine-native-proxy. Она быстрее, надежней и является основной проксей.

Итак, будем предполагать, что прокся успешно сконфигурирована и работает. Как ею пользоваться? На данный момент существует два паттерна ее использования.

Первый способ заключается в использовании URI синтаксиса. Обращение к интересующему нас приложению происходит следующим образом: <APP>/<METHOD>[?<Args>].

Во втором способе имя приложения и события передается в заголовках X-Cocaine-Service и X-Cocaine-Event соответственно.

Мы будем пользоваться первым способом. В нашем случае URI запроса будет: /qr/generate-http?message=Hello%20World!&size=10.

Сходим по нему браузером. Если все работает правильно, то в результате должно быть что-то вроде этого:



Это важно: через cocaine-proxy нельзя ходить в сервисы (например, в Storage) — только в приложения.

Подключаем нагрузку


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

Так давайте же нагрузим наше приложение и проанализируем, как себя ведет Cocaine под нагрузкой.

Для этого воспользуемся утилитой ab. План такой: мы будем стрелять в наше приложение, скажем, 100000 раз в 32 потока (столько же, сколько проксей) и посмотрим, как Cocaine будет себя вести при этом.

Предполагается, что одного инстанса приложения не хватит для разгребания всей очереди сообщений, поэтому будут
созданы дополнительные экземпляры воркера. Мониторить текущее состояние cocaine-runtime мы будем, воспользовавшись
командой:

cocaine-tool info


Если Cocaine не под нагрузкой, то будет выведено что-то типа:



Кратко поясним, что же значат показанные параметры. Перед вами список приложений. Поле «state» говорит о текущем статусе приложения. Оно может быть запущено или сломано. Поля «slaves» показывают сколько экземпляров данного приложение запущено, сколько в данный момент обрабатывают запросы и максимальное количество экземпляров, которое может запустить Cocaine (конфигурируется в профиле). Поля «queue» показывают состояние очереди сообщений актора (то есть, сервиса) в настоящее время и ее максимальный размер. Поле профиля показывает, с каким профилем запущено приложение. Наконец, поля «sessions» показывают состояние сессий в данный момент, например, их количество. Напоминаем, что приложение может отвечать достаточно долго, используя каналы. Каждый такой канал — отдельная сессия.

Пальнем по нашему приложению! Выполните следующую команду и понаблюдайте, что творится в логах cocaine-runtime и cocaine-tool info:

ab -n 100000 -c 32 'localhost:8080/qr/generate-http?message=Hello%20World!&size=10'


Имя хоста и номер порта, понятное дело, надо подставить свои. Эта команда начинает пальбу в 100000 запросов в 32 потока. При этом в моем случае cocaine-runtime понял, что одного воркера недостаточно для своевременой обработки запроса и наплодил целых 6 экземпляров нашего QR-приложения, что можно видеть в логах cocaine-runtime по строчке:

[Tue Apr  8 15:36:18 2014] [INFO] app/qr: enlarging the pool from 5 to 6 slaves


Показания cocaine-tool info в моем случае где-то в середине стрельбы были следующие:

Видно, что количество активных воркеров равно 6, при их максимальном количестве равном 10. В очереди запросов нет, но висит 7 активных сессий. Если нагрузка будет увеличиваться, то Cocaine будет пытаться сбалансировать ее, спавня дополнительные воркеры.

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

Так выглядит жизненный цикл нашего приложения в Cocaine.

Но я не умею писать на Python!


Не беда! У нас же есть множество различных фреймворков под разные языки программирования, чье назначение — позволить пользователю (то есть нам) писать приложения на любимом языке, чтобы оно потом было доступно по единому интерфейсу.

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

В случае Go — это привычные каналы и горутины. Have fun!

>package main

import (
    "net/http"

    "code.google.com/p/rsc/qr"
    "github.com/ugorji/go/codec"

    "github.com/cocaine/cocaine-framework-go/cocaine"
)

//msgpack specific
var (
    mh codec.MsgpackHandle
    h  = &mh
)

var (
    OkHeaders    cocaine.Headers = cocaine.Headers{[2]string{"Content-type", "image/png"}}
    ErrorHeaders cocaine.Headers = cocaine.Headers{[2]string{"Content-type", "text/plain"}}

    storage *cocaine.Service
)

const (
    cacheNamepspace = "qr-code"
    cacheTag        = "qr-tag"
)

func qenerate(text string) (png []byte, err error) {
    res := <-storage.Call("read", cacheNamepspace, text)
    if res.Err() == nil {
        err = res.Extract(&png)
        return
    }

    c, err := qr.Encode(text, qr.L)
    if err != nil {
        return
    }
    png = c.PNG()

    <-storage.Call("write", cacheNamepspace, text, string(png), []string{cacheTag})
    return
}

func on_generate(request *cocaine.Request, response *cocaine.Response) {
    defer response.Close()
    inc := <-request.Read()
    var task struct {
        Text string
        Size int
    }

    err := codec.NewDecoderBytes(inc, h).Decode(&task)
    if err != nil {
        response.ErrorMsg(-100, err.Error())
        return
    }

    png, err := qenerate(task.Text)
    if err != nil {
        response.ErrorMsg(-200, err.Error())
        return
    }

    response.Write(png)
}

func on_http_generate(request *cocaine.Request, response *cocaine.Response) {
    defer response.Close()
    r, err := cocaine.UnpackProxyRequest(<-request.Read())
    if err != nil {
        response.ErrorMsg(-200, err.Error())
        return
    }

    message := r.FormValue("message")
    if len(message) == 0 {
        response.Write(cocaine.WriteHead(http.StatusBadRequest, ErrorHeaders))
        response.Write("Missing argument `message`")
        return
    }

    png, err := qenerate(message)
    if err != nil {
        response.Write(cocaine.WriteHead(http.StatusInternalServerError, ErrorHeaders))
        response.Write("Unable to generate QR")
        return
    }

    response.Write(cocaine.WriteHead(http.StatusOK, OkHeaders))
    response.Write(png)
}

func main() {
    binds := map[string]cocaine.EventHandler{
        "generate":      on_generate,
        "generate-http": on_http_generate,
    }

    Worker, err := cocaine.NewWorker()
    if err != nil {
        panic(err)
    }

    storage, err = cocaine.NewService("storage")
    if err != nil {
        panic(err)
    }

    Worker.Loop(binds)
}



Несмотря на различие в написании приложений, код клиентов для их тестирования не изменится! Попадая в Cocaine ваш код становится унифицированным и взаимозаменяемым.

Вместо заключения



Сегодня мы кратко рассмотрели основные возможности платформы Cocaine и средства, позволяющие писать различные приложения и клиенты к ним. Конечно, множество вопросов все еще поджидают впереди. Например, как писать сервисы, которые являются неотъемлемой частью Cocaine Runtime, или как добавить поддержку любимого языка. Конечно, у нас есть документация, которая охватывает большую часть проблем, но иногда и ее недостаточно. Еще больше вопросов могут возникать по мере практического опыта использования Cocaine.

Поэтому мы организуем встречу, о которой я сказал в самом начале поста. Если у вас есть или остались вопросы по Cocaine, или вам хочется узнать больше и попробовать его под присмотром опытных специалистов, приходите. А для тех, кто попасть на неё не сможет, мы, как обычно, организуем трансляцию.
Tags:
Hubs:
+64
Comments 18
Comments Comments 18

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия
Representative