(原创)OrientDB的本地模式以及索引使用的小经历

2021-01-12  本文已影响0人  mona_alwyn

1.简单介绍

OrientDB是第一个多模型开源NoSQL DBMS,它将图形的功能和文档的灵活性结合到一个可扩展的高性能操作数据库中,简单地说它是一个开源的图形数据库。OrientDB Overview

根据操作来分的话,它支持command lineStudio UIAPIs(目前代码示例只有Java开发),以及其他工具的访问。

而从链接的形式来说,它支持 远端模式Remote Pattern和本地模式Local Pattern两种,在当前3.0的官方文档中基本上所有讲解都是围绕着Remote Pattern来讲解的。我写这篇文章的出发点就是因为在使用Remote Pattern遇到了性能瓶颈,不得不使用Local Pattern时又遇到了点小问题,经过两番尝试优化终于开窍搞定,特此记录一下。

2.Remote Pattern through HTTP

所谓的Remote Pattern其实就是要先执行 server.sh 命令启动一个Server,然后所有的操作(CURD)都是通过HTTP访问到Server上来执行的。

server和dserver

比如通过Studio UI界面操作,server启动后默认运行在2480端口,server shutdown的时候当然没法访问。而在command line中如果使用的url 是以remote为前缀,其实也是Remote Pattern.

对了,Remote Pattern的标志性url就是remote:localhost/demo,其中localhost则是OrientDB Server所运行的地址,它自动映射到内部的databases文件夹,所有的DB instances都默认放在它下面,而demo就是db instance name

具体参考OrientDB for Java Developers in Five Minutes - 3,这里使用的依赖包是orientdb-core

    OrientDB orient = new OrientDB("remote:localhost", OrientDBConfig.defaultConfig());
    ODatabaseSession db = orient.open("test", "admin", "admin");

    //let's do something with this session!

    db.close();    
    orient.close();

3.问题出现

在使用初期,上手开发还是比较简单愉快的,但是当项目迭代到性能测试的时候,问题来了。
在一次测试中,使用到了近127M的数据,跑了5个小时,最后还需要连续创建80多万条边的时候,报了ODatabaseSession链接的异常,当时查看Exception StackTrace后搜索了一番,大概原因是线程池耗尽了,不知道为什么没有及时释放。

然后在网上查找解决方案(想抄代码)的时候,得到了关于本地模式的信息:

orientdb> connect  remote:localhost/graphxdb root root
其中remote表示远程连接,连接成功显示:
Connecting to database [remote:localhost/graphxdb] with user 'root'...OK
orientdb {db=graphxdb}>

Connecting to database [plocal:/opt/orientdb/databases/demo] with user 'admin'…OK 
Orientdb {db = demo}>

作者在这里并没有解释为什么是rootadmin两个账户的调试信息,估计他也不清楚?

但是所给出的参考链接是OrientDB 2.0的,已经失效了。
其实我之前看官方文档的时候,也确实有看到过本地模式和远端模式的说法,但文档写的平平无奇,并没有强调什么Key Point,所以也就没留下特别的印象。

4.Local Pattern by file

本地模式是通过文件直接写数据的,而且没有http链接访问的问题,当然更快。有了方向当然就是继续前进,但是当我把url改为plocal:$OrientDB_DirPath/databases/之后再去跑的时候,却一直就这么来来回回报这两个错误:

<1> 我之前的代码,考虑了兼容从零新建、删除后新建和重复插入的三种情况:
        OrientDB orient = new OrientDB(url, username, password, OrientDBConfig.defaultConfig());
        if (orient.exists(dbName)) {
            if (isNew != null && isNew.equals("yes")) {
                orient.drop(dbName);
                LOG.info("drop the old instance: " + dbName + " by required.");
                orient.create(dbName, ODatabaseType.PLOCAL);
                LOG.info("create new instance: " + dbName + " by required.");
            }
        } else {
            orient.create(dbName, ODatabaseType.PLOCAL);
            LOG.info("create instance: " + dbName + " by default.");
        }
        orient.close();


        orient = new OrientDB(url, OrientDBConfig.defaultConfig());
        ODatabaseSession db = orient.open(dbName, username, password);
<2> 而这是官方文档中搜索plocal的结果之一:connect语法和示例:
connect syntax
connect example
<3> 而这是 orientdb-coreOrientDB.java的源代码中的注释:
 * Usage example:
 * <p>
 * Remote Example:
 * <pre>
 * <code>
 * OrientDB orientDb = new OrientDB("remote:localhost","root","root");
 * if(orientDb.createIfNotExists("test",ODatabaseType.MEMORY)){
 *  ODatabaseDocument session = orientDb.open("test","admin","admin");
 *  session.createClass("MyClass");
 *  session.close();
 * }
 * ODatabaseDocument session = orientDb.open("test","writer","writer");
 * //...
 * session.close();
 * orientDb.close();
 * </code>
 * </pre>
 * <p>
 * Embedded example:
 * <pre>
 * <code>
 * OrientDB orientDb = new OrientDB("embedded:./databases/",null,null);
 * orientDb.create("test",ODatabaseType.PLOCAL);
 * ODatabaseDocument session = orientDb.open("test","admin","admin");
 * //...
 * session.close();
 * orientDb.close();
 * </code>
 * </pre>
 * <p>
<4> 破案了

是不是平平无奇?也没有任何强调说明?
最后还是灵光一闪(实际耗时好几天 ),联想到公司另一个产品中内置了OrientDB,用的时候并不需要启动Server,而且账号密码是admin/admin,然后就大胆猜测:

最后修改代码后一调试果然如此,所经历过的这些折腾,最后也就是一声,不就是在文档或者代码注释里多写一个提示的事么,为什么不呢?为啥子哟!(此处无力吐槽)

<5> 调整后的代码:
OrientDB orient;
        String username;
        String password;
        if (url.startsWith("embedded:") || url.startsWith("plocal:")) {
            /**
             *!IMPORTANT: required hard code to access OrientDB through local pattern
             */
            username = "admin";
            password = "admin";
            orient = new OrientDB(url, OrientDBConfig.defaultConfig());
        } else {
            username = dbConfig.get("username");
            password = dbConfig.get("password");
            orient =  new OrientDB(url, username, password, OrientDBConfig.defaultConfig());
        }

        // 1) If the DB Instance is existing, and requires re-creating it, drop it and then create a new Instance.
        // 2) If the DB Instance isn't existing, create it by default.
        if (orient.exists(dbName)) {
            if (isNew) {
                orient.drop(dbName);
                LOG.info("drop the old instance: " + dbName + " by required.");
                orient.create(dbName, ODatabaseType.PLOCAL);
                LOG.info("create new instance: " + dbName + " by required.");
            }
        } else {
            orient.create(dbName, ODatabaseType.PLOCAL);
            LOG.info("create instance: " + dbName + " by default.");
        }

        ODatabaseSession db = orient.open(dbName, username, password);
<6> Local PatternRemote Pattern 的性能对比

上面说了在使用Remote Pattern导入大概127M数据时,跑了5个小时之后因为http线程池耗尽而不断异常. 在使用Local Pattern之后,50分钟就完成了这127M数据的导入。
而另一份常用的测试数据,大概12M,花费的时间分别是120秒和45秒。
可见不论数据量的多少,Local Pattern都比Remote Pattern更快,这就不难理解为什么那个产品要把OrientDB内嵌了。

不过127M数据50分钟,这个速度还是不够快,因为这只是拿到的实际数据的1/7,也就说光这一份数据全部导入就可能需要350分钟(6个小时),而且这还只是这一个环节,还是要加加速。

Index的使用

继续分析当时的代码包括观察导入数据时的日志发现:在插入Vertex时会比插入Edge要快许多
这是因为orientdb-core.jar中实现的save()方法只有简单的Insert功能,并没有想JPA封装的save或者saveAll()那样是InsertOrUpsert()功能。
所以我必须要自己解决重复插入的问题,所以在创建Vertex类的时候添加了一个id属性(默认的叫#rid)来存放原本的源数据中的一个唯一值,然后据此建立了Unique index,就可以通过getNodeById()的方法来操作Insert or Upsert

    private OVertex getNodeById(String classType, String id) {
        OVertex node = null;
        OResultSet rs;
        if (classType == null || classType.equals("")) {
            rs = this.db.query("SELECT FROM V WHERE id = ?", id);
        } else {
            rs = this.db.query("SELECT FROM ? WHERE id = ?", classType, id);
        }
        while (rs.hasNext()) {
            OResult row = rs.next();
            if (row.isVertex()) {
                node = row.getVertex().get();
            }
        }
        rs.close();
        return node;
    }

Edge类 在源数据中是通过多个属性来联合确定唯一性的,再加上最开始追求实现功能,也没有仔细地想过这个问题,就通过for循环来遍历两端顶点的所有边的方式来操作Insert or Upsert的,当然要慢很多。

public void buildOrCheckEdge(OVertex source, OVertex target, String relationshipName) {
        boolean existing = false;
        if (source != null && target != null) {
            for (OEdge edge : source.getEdges(ODirection.OUT, relationshipName)) {
                if (edge.getTo().asVertex().get().compareTo(target) == 0) {
                    existing = true;
                }
            }
            // create relationship if not exist
            if (!existing) {
                OEdge relationship = source.addEdge(target, relationshipName);
                relationship.save()
            }
        }
    }

当然现在经过一番查询关于OrientDB 索引的文档(3.0官方文档第三方翻译 )以及思考后搞定了这个事。

具体思路和改进的演进过程:

最后,同样的127M数据,给Edge添加索引之后, 完成导入的时间是13分钟,而完全不做一致性检查只是插入的时候,也要12分钟。

    public void createOrUpdateDLASchema() {
        String[] nodes = new String[]{
              "a", "b", "c"
        };
        for (String node : nodes) {
            OClass nodeClass = db.getClass(node);
            if (nodeClass == null) {
                nodeClass = db.createVertexClass(node);
            }
            if (nodeClass.getProperty("id") == null) {
                nodeClass.createProperty("id", OType.STRING);
                nodeClass.createIndex(node + "_idx", OClass.INDEX_TYPE.UNIQUE, "id");
            }
        }
        LOG.info("OVertex schema have been checked.");

        String[] relationships = new String[]{
                "x", "y", "z"
        };
        for (String relationship : relationships) {
            OClass edgeClass = db.getClass(relationship);
            if (edgeClass == null) {
                edgeClass = db.createEdgeClass(relationship);
            }
            if (edgeClass.getProperty("source_target") == null) {
                edgeClass.createProperty("source_target", OType.STRING);
                edgeClass.createIndex(relationship + "_idx", OClass.INDEX_TYPE.UNIQUE, "source_target");
            }
        }
        LOG.info("OEdge schema have been checked.");
    }

对Orient-core的一点改进想法

我当前使用的是orientdb-core 3.0.25:

上一篇 下一篇

猜你喜欢

热点阅读