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

1.简单介绍

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

根据操作来分的话,它支持command line、Studio UI、APIs(目前代码示例只有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连接数据库
    当中提到了 - 定义模式,即本地模式或远程模式。以及一段日志:
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最快的批量导入
    这里唯一的回答提到了要在本地工作
    image

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

4.Local Pattern by file

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

  • 先是报账号密码错误,我设的是root/root
  • 然后我用studio打开对应的db后又一直报Orientdb File is allowed to be opened only once.
<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:
 * 

* Remote Example: *

 * 
 * 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();
 * 
 * 
*

* Embedded example: *

 * 
 * OrientDB orientDb = new OrientDB("embedded:./databases/",null,null);
 * orientDb.create("test",ODatabaseType.PLOCAL);
 * ODatabaseDocument session = orientDb.open("test","admin","admin");
 * //...
 * session.close();
 * orientDb.close();
 * 
 * 
*

<4> 破案了

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

  • 在使用OrientDB的本地模式访问时,只能使用admin这个账号,其余账号都是无效的
  • 本地模式不能访问远端模式打开过的db实例,除非shutdown server或者删除后通过本地模式新建
  • 而本地模式下admin创建的db实例,在远端模式下rootadmin账号均可以打开 访问
  • 通过JAVA API访问时,本地模式创建OrientDB是不能传入账号密码的

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

<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官方文档,第三方翻译 )以及思考后搞定了这个事。

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

  • 1)创建Edge Schema时新增sourcetarget两个属性(fromtoinout这四个都是自带的隐藏属性)对应两边的Vertex id
  • 2)基于sourcetarget创建了一个INDEX_TYPE.UNIQUE的索引,结果一跑才发现他是要求两个属性都唯一,对于多边关系来说没法正常存储。
  • 3)基于sourcetarget创建了一个INDEX_TYPE.UNIQUE_HASH_INDEX的索引,结果发现这种索引不支持“无条件查询”,就是说select语句里必须配一个where条件,这样一来我就无法根据已知的两个属性来查询、推测出未知的哈希算法,从而没法去生成正确的哈希值进而来匹配查找了。
  • 4)尝试放弃索引,只凭借select * from EdgeClass where source=$source and target=$target 来匹配查找,结果跑出来发现导入数据的效率慢了一个量级
  • 5)最终在一个周末的下午又是一个灵感来了:只新增一个属性source_target,由两端的Vertex id拼接而成,再据此创建INDEX_TYPE.UNIQUE的索引,这样就跟Vertex id一样好使了。

最后,同样的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:

  • 首先当然就是文档描述应该有突出和对比,像Local Pattern这样平平无奇,真的很容易造成困惑。
  • 其次,就是真的可以向JPA学习一下,把save扩展一下内在兼容upsert而不只是insert,还可以顺便推出saveAll,不过这就要求新增主键功能,而当前它自身的#rid是不具备这个作用的,或许这也是一个难点吧。
  • 再次就是对创建schema功能的增强了,我在使用期间发现在新增property的时候只能设置类型,无法指定Not Null 这样的 ,而在Studio手动操作时是可以的。
  • 还有就是给property赋值只能通过 setProperty(propertyName, value) 这种形式,实在是很hard code了。

你可能感兴趣的:((原创)OrientDB的本地模式以及索引使用的小经历)