上次我写过一篇文章https://blog.youkuaiyun.com/u012543819/article/details/104540056,其中讲到,在hiveserver异常挂掉的情况下,可能会导致部分事务为o状态且残留在TXNS元数据表中。
通过仔细研究TxnHandler的代码,我发现其中有一个方法叫timeOutTxns,其实它核心也就是去修改TXNS表中超时的o状态事务为a状态,以便后续的合并任务不会被卡住。具体内容如下:
private void timeOutTxns(Connection dbConn) throws SQLException, MetaException, RetryException {
long now = getDbTime(dbConn);
Statement stmt = null;
try {
stmt = dbConn.createStatement();
// Abort any timed out locks from the table.
String s = "select txn_id from TXNS where txn_state = '" + TXN_OPEN +
"' and txn_last_heartbeat < " + (now - timeout);
LOG.debug("Going to execute query <" + s + ">");
ResultSet rs = stmt.executeQuery(s);
List<Long> deadTxns = new ArrayList<Long>();
// Limit the number of timed out transactions we do in one pass to keep from generating a
// huge delete statement
do {
deadTxns.clear();
for (int i = 0; i < TIMED_OUT_TXN_ABORT_BATCH_SIZE && rs.next(); i++) {
deadTxns.add(rs.getLong(1));
}
// We don't care whether all of the transactions get deleted or not,
// if some didn't it most likely means someone else deleted them in the interum
if (deadTxns.size() > 0) abortTxns(dbConn, deadTxns);
} while (deadTxns.size() > 0);
LOG.debug("Going to commit");
dbConn.commit();
} catch (SQLException e) {
LOG.debug("Going to rollback");
rollbackDBConn(dbConn);
checkRetryable(dbConn, e, "abortTxn");
throw new MetaException("Unable to update transaction database "
+ StringUtils.stringifyException(e));
} finally {
closeStmt(stmt);
}
}
我查看了调用他它的地方,在TxnHandler的getOpenTxns方法中。然后我们又持续跟踪的getOpenTxns方法的调用,发现合并时并未能够调用getOpenTxns方法,getOpenTxns方法是在DbTxnManager的getValidTxns方法中被调用,而getValidTxns的调用,在spark中并没有找到,因此,spark在产生新事务的时候,并不会去触发timeoutTxn是动作。 而合并时,hive是在Initiator的run方法中调用了getOpenTxnsInfo方法,
ValidTxnList txns = CompactionTxnHandler.createValidCompactTxnList(txnHandler.getOpenTxnsInfo());
然而getOpenTxnsInfo又并没有去调用timeoutTxns, 这就导致了如果遇到异常失败的状态o事务,那么就无法将失败的残留为o状态的事务状态修改为a,导致合并被阻塞。
我们只好在getOpenTxnsInfo中也调用了一下timeOutTxns,在合并前清理一下残留的o状态问题。 虽然这样听起来很合理,但是后面发生了意见诡异的事情。
我们在compact前timeoutTxns的方式听起来很合理,的确,这确实解决了事务的状态o残留的问题,但是却引发了另外的问题。
用户在使用的过程中发现,有时候执行delete操作后,数据就变成null了,数据丢失了,这是个非常严重的问题。
用户的操作流程大概是一个插入,更新,再删除的过程,简单表示如下:
insert ->update ->(auto merge)->delete
经过分析用户的数据,我们发现delete以后查询出的数据,非常像只读取了update的那部分delta文件的内容。除了update的字段以外,其余的字段都变成了null。我们通过查看spark-UI的job和分析spark的log发现,update 和delete操作中间有一次自动的major合并操作。
后面我们又神奇地发现, 在TXNs表中,状态为a的事务,事务的start_time 和last_heartbeat_time 都是同一个时间,也就是说,spark在执行事务操作时,未能正常地去发送事务心跳(为了验证这个问题我们专门构造了一个长事务去验证)。那么至此问题也就能够解释了:
长事务未能正确发送事务心跳,导致在合并时,被判断为timeout的事务,因此状态从o被标记为了a,合并时忽略了状态a事务的数据,恰好此处用户insert的数据非常大,是一个长事务,因此insert的数据在合并后被标记无效且删除了,就只剩下了update 的数据,导致很多列 数据都为null。因此,要解决该问题,需要去让spark的事务能够正常地发送心跳,保证长事务不会被异常淘汰。
具体的解决方法,我会在后面的文章中贴出来。