rdflib向Virtuoso创建命名图报HTTP500错误的原因及解决方案
在使用Apache Jena Fuseki 4.3.2(docker镜像secoresearch/fuseki:4.3.2)搭配rdflib 6.1.1时,以下代码可正常运行:
from rdflib import Graph from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore FUSEKI_QUERY = 'http://localhost:3030/ds/sparql' FUSEKI_UPDATE = 'http://localhost:3030/ds/update' store = SPARQLUpdateStore(query_endpoint=FUSEKI_QUERY, update_endpoint=FUSEKI_UPDATE, method='POST', autocommit=False) graph = Graph(store=store, identifier=GRAPH_NAME) graph.parse('./dump.ttl') # 包含1000条示例三元组的文件 store.commit()
但将后端切换为OpenLink Virtuoso 07.20.3233(docker镜像tenforce/virtuoso:latest)时,会抛出如下错误:urllib.error.HTTPError: HTTP Error 500: SPARQL Request Failed
经过反复调试,找到以下可在Virtuoso环境下运行的实现方式:
from rdflib import Graph from rdflib.plugins.stores.sparqlstore import SPARQLUpdateStore VIRTUOSO_QUERY = 'http://localhost:8890/sparql' VIRTUOSO_UPDATE = 'http://localhost:8890/sparql' store = SPARQLUpdateStore(query_endpoint=VIRTUOSO_QUERY, update_endpoint=VIRTUOSO_UPDATE, method='POST', autocommit=False) intermediate_graph = Graph() intermediate_graph.parse('./dump.ttl') graph = Graph(store=store, identifier=GRAPH_NAME) for triple in intermediate_graph: graph.add(triple) store.commit() # 此处必须每次add后执行commit
该实现必须在每次add三元组后执行commit操作,若在三元组遍历循环结束后统一执行commit,则会复现上述500错误。当前HTTP响应与Virtuoso服务端日志均未输出可定位问题的有效信息,核心疑问如下:
- 该错误的产生原因是什么
- 是否与Virtuoso实例配置有关
- 对应解决方案是什么
2022年6月3日更新
发现此前使用的tenforce/virtuoso:latest镜像携带的Virtuoso版本较旧(07.20.3233),因此切换为官方镜像openlink/virtuoso-opensource-7:latest(版本07.20.3234),但切换后上述可在旧版本运行的Virtuoso适配代码也复现了前述500错误。
另外正如评论区TallTed指出的,当前实现将/sparql同时作为查询与更新端点使用——因暂未完成rdflib侧的基础认证对接,我通过Virtuoso Conductor管理界面为SPARQL用户授予了SPARQL_UPDATE角色,以此作为临时方案复用/sparql端点,未使用官方要求的/sparql-auth端点处理更新请求,该配置是否是问题的诱因?
核心诱因
- 端点使用不符合官方规范:Virtuoso的公开
/sparql端点本身对批量更新请求有严格的大小、事务时长限制,即使手动给匿名用户加了SPARQL_UPDATE权限,公开端点的请求配额、单事务最大语句数限制依然生效,批量提交多三元组时很容易触发阈值返回500错误。 - 事务配置不匹配:rdflib的SPARQLUpdateStore在非自动提交模式下,会把多次add操作攒成一个大的SPARQL UPDATE请求一次性发给服务端。Fuseki对单请求的体积、事务执行时长容忍度很高,而Virtuoso默认配置下
MaxQueryCostEstimationTime、MaxQueryExecutionTime、单SPARQL更新请求允许插入的三元组数量阈值都很低,攒出来的大请求直接触发服务端限流/执行超时,就会返回无明确日志的500错误。逐条add后立刻commit本质是把大事务拆成了单条三元组的极小请求,刚好绕开了阈值限制,但执行效率极低。
解决方案
- 首先修正端点配置:使用Virtuoso官方要求的带认证更新端点
/sparql-auth处理所有写请求,rdflib的SPARQLUpdateStore原生支持传入认证参数,配置方式如下:store = SPARQLUpdateStore( query_endpoint='http://localhost:8890/sparql', update_endpoint='http://localhost:8890/sparql-auth', auth=('你的用户名', '你的密码'), # 对应用户需持有SPARQL_UPDATE权限 method='POST', autocommit=False ) - 调整Virtuoso实例配置,在
virtuoso.ini的[SPARQL]段调高相关阈值:
修改后重启Virtuoso服务即可生效。[SPARQL] MaxQueryCostEstimationTime = 600 ; 单位秒,查询成本估算最大时长,设为0为无限制 MaxQueryExecutionTime = 600 ; 单位秒,查询实际执行最大时长 MaxDataInsertSize = 1000000 ; 单更新请求允许插入的最大三元组数量 ResultSetMaxRows = 100000 ; 查询返回最大结果行数 - 批量导入数据时不要逐条add提交,可直接调用
graph.parse()对接存储,或者将批量三元组拆分为每批次100~500条的小事务提交,既不会触发服务端阈值,执行效率也比逐条提交高几个数量级。
不要长期给匿名/公开
/sparql端点开放更新权限,会带来严重的安全风险,任何人都可以随意修改、删除知识库内的数据。
内容的提问来源于stack exchange,提问作者Roman

