Статьи

Neo4j на смехотворной скорости

spaceballs_ludicrous_speed

В последнем сообщении в блоге мы увидели, как мы можем получать около 1250 запросов в секунду (с задержкой 10 мс), используя неуправляемое расширение, работающее на сервере Neo4j … но что, если мы хотим работать быстрее?

Простой ответ — увеличить масштаб . Тем не менее, попытка добавить больше ядер к моему ноутбуку Apple не похоже на хорошее время. Другой ответ — запуск кластера Neo4j и (почти) линейное масштабирование наших запросов на чтение по мере добавления новых серверов. Таким образом, кластер из 3 серверов будет давать нам от 3500 до 3750 запросов в секунду.

Но можем ли мы работать быстрее на одном сервере без нового оборудования? Ну, да .

Neo4j начал свою жизнь как встраиваемая библиотека Java, и использование встроенного ПО все еще является верным способом развертывания Neo4j. Однако в основном это зарезервировано для клиентов, которые используют Java в качестве основного языка. Для нас, не являющихся Java, давайте взглянем на один из способов сделать это.

Я искал действительно простой и производительный веб-сервер Java и побежал в Undertow .

undertow_banner

Undertow выглядит действительно простым в использовании и получает высокие оценки в тестах TechEmpower Web Framework

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

static GraphDatabaseService graphDb = new GraphDatabaseFactory()
        .newEmbeddedDatabaseBuilder( STOREDIR )
        .loadPropertiesFromFile( PATHTOCONFIG + "neo4j.properties" )
        .newGraphDatabase();

Первое, что мы сделаем, это зарегистрируем Shutdown Hook для нашей базы данных Neo4j, чтобы он корректно завершал работу при остановке сервера. Далее мы создадим наш сервер Undertow, который будет слушать тот же порт, что и Neo4j. У нас здесь будет два пути. Корневой путь просто скажет «Hello World» просто для того, чтобы убедиться, что все подключено, и мы можем использовать его для проверки базовой производительности Undertow в нашей системе. Второй путь будет соответствовать нашему неуправляемому расширению, чтобы мы могли повторно использовать наш тест производительности.

public static void main(final String[] args) {
    registerShutdownHook(graphDb);
    Undertow server = Undertow.builder()
            .addListener(7474, "localhost")
            .setHandler(new PathHandler()
                    .addPath("/", new HelloWorldHandler())
                    .addPath("/example/service/crossreference", new CrossReferenceHandler(objectMapper, graphDb))
            ).build();
 
    server.start();
}

Давайте сначала посмотрим на HelloWorldHandler . Он обрабатывает запрос, просто отвечая простым текстом Hello World.

public class HelloWorldHandler implements HttpHandler {
    @Override
    public void handleRequest(final HttpServerExchange exchange) throws Exception {
        exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
        exchange.getResponseSender().send("Hello World");
    }
}

Мы можем проверить производительность этого запроса с небольшим Гатлингом :

val base = scenario("Get Hello World")
   .during(30) {
     exec(
       http("Get Base Request")
         .get("/")
         .check(status.is(200))
     )
     .pause(0 milliseconds, 1 milliseconds)
 }
 
setUp(
   base.users(16).protocolConfig(httpConf)
 )

… И наши результаты составляют чуть менее 20 000 запросов в секунду:

Снимок экрана 2014-02-27 в 11.25.39

Теперь давайте посмотрим на наш CrossReferenceHandler . Вы заметите, что мы передаем в GraphDB, который мы создали ранее, и objectMapper. Это не нужно, поскольку у нас есть только одна конечная точка, но если у вас их много, вы не захотите каждый раз воссоздавать эти объекты. Фактически, Neo4j не позволит вам, так как ему нужен эксклюзивный доступ к каталогу graph.db.

public class CrossReferenceHandler implements HttpHandler {
    private static final RelationshipType RELATED = DynamicRelationshipType.withName("RELATED");
    ObjectMapper objectMapper;
    GraphDatabaseService graphDb;
 
    public CrossReferenceHandler(ObjectMapper objectMapper, GraphDatabaseService graphDb){
        this.objectMapper = objectMapper;
        this.graphDb = graphDb;
    }

Когда мы обрабатываем запрос, мы сначала проверяем, является ли запрос методом POST, затем мы получим InputStream запроса (BLOB-объект JSON) и преобразуем его в HashMap. Забавный вызов startBlocking () необходим для чтения inputStream.

public void handleRequest(final HttpServerExchange exchange) throws Exception {
     try {
         if (exchange.getRequestMethod().equals(Methods.POST)) {
             exchange.startBlocking();
             final InputStream inputStream = exchange.getInputStream();
             final String body = new String(ByteStreams.toByteArray(inputStream), Charsets.UTF_8);
             HashMap input = objectMapper.readValue(body, HashMap.class);
             ...   

Отсюда наш код точно соответствует тому, что сделал наше неуправляемое расширение, и отвечает ответом в формате JSON:

exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json; charset=utf-8");
exchange.getResponseSender().send(ByteBuffer.wrap(objectMapper.writeValueAsBytes(results))); 

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

Снимок экрана 2014-02-27 в 11.29.50 утра

Over 8000 requests per second with a latency of 1ms. Thats about 6.5x the number of requests we we able to do before and our mean latency dropped to just 1ms…on my laptop. Now that’s fast! The complete source code is available on github as always, try it out with your own Neo4j projects.

I’ll leave you with other options including running Neo4j embedded with RatPack on Stefan Armbruster’s blog…. and even more fun ideas from Nigel Small:

If you have a problem that is graphy in nature and your Relational Database just isn’t cutting it, you gotta try Neo4j.