среда, 1 мая 2013 г.

Apache Thrift RPC Server. Дружим C++ и Java

Thrift - технология для организации межпроцессного взаимодействия между компонентами системы. Была разработана где то в недрах Facebook. Посути это кросс-языковой фреймворк для создания RPC сервисов, на бинарном протоколе. С помощью этого решения можно "подружить" компоненты написанные на разных языках  C#, C++, Delphi, Erlang, Go, Java, PHP, Python, Ruby, итд. Описание сигнатур сервисов и данных осуществляется с помощью специального IDL - языка. Технология, по своей сути, похожа на COM, но без всей этой обвязки с регистрацией компонент. Так же не будем забывать, что COM это технология только для Windows, в то время как Thrift - кросплатформенна.

Вобщем решил поэкспериментировать, попробовать вынести часть нагруженной-вычислительной логики из Java в С++, в надежде что нативный С++ код будет производительней, за одно опробовать Thrift RPC, в надежде что это быстрее чем REST.
Как и положено, без бубнов и граблей не обошлось!

И так, для начала надо всё это поставить:
1. Ставим поддержку для Boost, ибо всё завязано на нём

$ sudo apt-get install libboost-dev libboost-test-dev libboost-program-options-dev libevent-dev automake libtool flex bison pkg-config g++ libssl-dev

2. качаем thrift tarball http://apache.softded.ru/thrift/0.9.0/thrift-0.9.0.tar.gz
распаковываем, запускаем configure, затем собираем

$ ./configure
$ make
$ sudo make install

Вроде бы всё... Можно даже попробовать сгенерировать код из учебника, который идет вместе с thrift tarball

$ thrift --gen cpp tutorial.thrift

команда thrift сгенерирует С++ обвертки, и бережно положит их в директорию gen-cpp. Тоже самое можно сделать для Java, PHP итд...

Пробуем скомпилировать и собрать нагенеренные исходники

$ g++ -Wall -I/usr/local/include/thrift *.cpp -L/usr/local/lib -lthrift -o something

Упс, получите:  error: ‘uint32_t’ does not name a type
Оказывается есть небольшой таракан в библиотеках thrift связанный с uint32_t.  Лечится  добавлением "#include <stdint.h>" в "Thrift.h", или (что лучше всего) специальными опциями компилятора -DHAVE_NETINET_IN_H -DHAVE_INTTYPES_H

Теперь это выглядит так:

$ g++ -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -I/usr/local/include/thrift *.cpp -L/usr/local/lib -lthrift -o something

Вот и всё, появился исполнимый файл, под названием something.
Запускаем, и получаем: error while loading shared libraries: libthrift.so.0: cannot open shared object file: No such file or directory
Возможно есть какие-то элегантные методы решения этой проблемы, но я решил её в лоб, копированием thrift файлов из /usr/local/lib в /lib

Всё, пример запустился. Значит, все на местах, и всё работает.

Теперь можно писать свой RPC сервер.
Его задача, быть key-value хранилищем. Хранить длинные (в несколько сот тысяч) битовые маски. Складывать их (AND), и отдавать клиенту массив индексов, в которых получились еденицы. Да, почти тоже самое умеет Redis, но он мне не подходит.

Полный код лежит здесь: https://github.com/2anikulin/fast-hands.git

Описываем сигнатуры данных и сервисов, в thrift definition file:

namespace cpp fasthands
namespace java fasthands
namespace php fasthands
namespace perl fasthands

exception InvalidOperation {
  1: i32 what,
  2: string why
}

service FastHandsService {

 i32 put(1:i32 key, 2:binary value),
 
 binary get(1:i32 key),
 
 list <i32> bitAnd(1:list<i32> keys) throws (1:InvalidOperation ouch)
}

И генерируем обвертки.
Реализация на C++
Этот код, создает, и запускает RPC сервер

#define PORT 9090
#define THREAD_POOL_SIZE 15


int main() {

  printf("FastHands Server started\n");

  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
  shared_ptr<FastHandsHandler> handler(new FastHandsHandler());
  shared_ptr<TProcessor> processor(new FastHandsServiceProcessor(handler));

  shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(THREAD_POOL_SIZE);
  shared_ptr<PosixThreadFactory> threadFactory = shared_ptr<PosixThreadFactory>(new PosixThreadFactory());
  threadManager->threadFactory(threadFactory);
  threadManager->start();

  TNonblockingServer server(processor, protocolFactory, PORT, threadManager);
  server.serve();
  printf("done.\n");

  return 0;
}

В классе FastHandsHandler - имплементируется весь наш, прикладной функционал
Это заголовочный файл

class FastHandsHandler : virtual public FastHandsServiceIf {

 public:
  FastHandsHandler();
  int32_t put(const int32_t key, const std::string& value);
  void get(std::string& _return, const int32_t key);
  void bitAnd(std::vector<int32_t> & _return, const std::vector<int32_t> & keys);

 private:
  void appendBitPositions(std::vector<int32_t> & positions, unsigned char bits, int offset);

 private:
  std::map<int32_t, std::string> m_store;

};


Пробуем собрать, и получаем очередную ошибку: c++ undefined reference to apache::thrift::server::TNonblockingServer
Дело в том, что, в отличии от учебника, мой сервер - ассинхронный, и использует класс TNonblockingServer. Чтобы код собирался, надо добавить дополнительные библиотеки -lthriftnb -levent

т.е сборка сейчас будет выглядеть так:
g++ -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H -Wall -I/usr/local/include/thrift *.cpp -L/usr/local/lib -lthrift -lthriftnb -levent -o something

Теперь все хорошо. Код скомпилирован, слинкован, на выходе файл по имени something
Генерируем обвертки для java, и пишем вот такого клиента


import fasthands.FastHandsService;
import fasthands.InvalidOperation;
import org.apache.thrift.TException;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransportException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

public class JavaClient {

    public static void main(String [] args) {
        TTransport transport = new TFramedTransport(new TSocket("localhost", 9090));
        TProtocol protocol = new TBinaryProtocol(transport);
        final FastHandsService.Client client = new FastHandsService.Client(protocol);

        final List<Integer> filters = new ArrayList<Integer>();

        try {
            transport.open();

            int count = 12500;
            byte bt[] = new byte[count];
            for (int i =0; i < count; i++) {
                bt[i] = (byte)0xFF;
            }

            for (int i = 0; i < 50; i++) {
                client.put(i, ByteBuffer.wrap(bt)) ;
                filters.add(i);
            }

            List<Integer> list = client.bitAnd(filters);
            System.out.println(list.size());  

        } catch (TTransportException e) {
            e.printStackTrace();
        } catch (TException e) {
            e.printStackTrace();  
        }

        transport.close();
    }
}

Что в итоге.
Интересная технология, и не плохой способ прикрутить транспортный функционал к голому коду на С++. Но не скажу, что это намного быстрее чем REST, бинарные данные прекрасно передаются и по http. Что касается производительности кода, вынесенного из Java в С++, то чуда не произошло, он быстрее в 1.2 - 1.4 раза, ибо сериализация + расходы на транспортный уровень.


Полезные ссылки:
http://thrift.apache.org/
http://wiki.apache.org/thrift/ThriftUsageC%2B%2B
http://fundoonick.blogspot.ru/2010/06/sample-thrift-program-for-server-in.html