이번 글에서는 Hive 쿼리를 limit 절과 함께 사용할 때 발생하는 leastNumRows 에러 발생 이슈와 해결 과정에 대해 공유하겠습니다.
문제 발견
Hive 테이블을 select 할 때 limit 절을 사용하면 에러가 발생하는 문제를 발견하게 되었습니다. Hive CLI에서 실행시킨 쿼리 예제와 발생하는 에러는 아래와 같았습니다.
select AA_COL from AA_TABLE
where partition_p=p1
limit 35;
java.io.IOException: org.apache.hadoop.hive.ql.metadata.HiveException: leastNumRows check failed |
문제 상황
다양한 케이스들에 따라 위의 에러 발생 여부가 달랐습니다. 일단 사전에 설정되었던 Hive 옵션 중 하나는 "hive.fetch.task.conversion" 으로 Fetch Task 작동 없이 모두 MR 작업이 수행되도록 "none"으로 설정했다는 점, Hive 엔진은 Tez를 사용했다는 점 참고바랍니다.
쿼리 성공 케이스
- hive.fetch.task.conversion 옵션을 more로 설정해 MR이 아닌 Fetch Task 작업이 수행되었을 때.
- Tez 작업이 아닌 Spark를 이용한 Hive 쿼리를 실행시켰을 때.
- limit 수를 2 이하로 했을 때.
- limit 없이 테이블 전체를 select 할 때.
에러 발생 케이스
- Tez 엔진으로 MR 작업이 수행될 때.
- limit 수를 3 이상으로 했을 때
에러가 발생할 때 나타났던 주요 특징으로는, Hive 쿼리가 실행될 때가 아니라 쿼리 결과를 가져와 출력하는 Fetch 과정에서 에러가 발생했다는 점입니다. 여기서 추측할 수 있는 것은 Hive 쿼리 수행 자체는 크게 문제가 없다는 점이었습니다. 아래의 Hive CLI 출력과 Stack trace를 보면 Fetch 과정에서 문제가 발생한 것을 확인할 수 있습니다.
Hive CLI Console |
---------------------------------------------------------------------------------------------------------------------------------- VERTICES MODE STATUS TOTAL COMPLETED RUNNING PENDING FAILED KILLED ---------------------------------------------------------------------------------------------------------------------------------- Map 1 .... SUCCESS 1 1 0 0 0 0 ---------------------------------------------------------------------------------------------------------------------------------- VERTICES : 01/01. [=====================================>>] 100% ---------------------------------------------------------------------------------------------------------------------------------- Status : DAG finished successfully in 5.67 seconds .... OK java.io.IOException:org.apache.hadoop.hive.ql.metadata.HiveException: leastNumRows check failed |
원인 분석
안타깝게도 "leastNumRows check failed" 관련 에러는 아무리 google에 검색해도 비슷한 게시물이 하나도 나오지 않았습니다.... 그래서 결국에는 Stack trace를 참고해 Hive 소스 코드 분석으로 에러의 원인을 파악해야만 했습니다. 에러에 대한 Stack trace와 소스 코드 분석 내용은 아래와 같습니다.
hiveserver2.log |
java.io.IOException:org.apache.hadoop.hive.ql.metadata.HiveException: leastNumRows check failed at org.apache.hadoop.hive.ql.exec.FetchTask.fetch(FetchTask.java) at org.apache.hadoop.hive.ql.Driver.getResult(Driver.java) at org.apache.hadoop.hive.ql.reexec.ReExecDriver.getResult(ReExecDriver.java) at org.apache.hadoop.hive.cli.CliDriver.processLocalCmd(CliDriver.java) .... |
Hive 3.1.0 Source code from Archive
FetchTask.java
public boolean fetch(List res) throws IOException {
sink.reset(res);
int rowsRet = work.getLeastNumRows();
if (rowsRet <= 0) {
rowsRet = work.getLimit() >= 0 ? Math.min(work.getLimit() - totalRows, maxRows) : maxRows;
}
try {
if (rowsRet <= 0 || work.getLimit() == totalRows) {
fetch.clearFetchContext();
return false;
}
boolean fetched = false;
while (sink.getNumRows() < rowsRet) {
if (!fetch.pushRow()) {
if (work.getLeastNumRows() > 0) {
throw new HiveException("leastNumRows check failed");
}
// Closing the operator can sometimes yield more rows (HIVE-11892)
fetch.closeOperator();
return fetched;
}
fetched = true;
}
....
- fetch 과정에서 row 수 관련 조건으로 인해 "leastNumRows" 에러 발생
Stack trace에서 에러가 발생한 FetchTask.java 아래의 코드 내용은 Hive CLI에서 결과 값을 불러오는 내용이라 원인 분석에는 도움이 되지 않았습니다.
그래서 원인을 찾기 위해 Hive 쿼리의 limit 절과 관련된 옵션들을 찾아봤고, "hive.limit.~~" 으로 시작되는 옵션들을 아래와 같이 존재했습니다.
Hive Limit Option |
|
hive.limit.optimize.enable | Whether to enable to optimization to trying a smaller subset of data for simple LIMIT first. - Default Value: false |
hive.limit.row.max.size | When trying a smaller subset of data for simple LIMIT, how much size we need to guarantee each row to have at least. - Default Value: 100000 |
hive.limit.optimize.limit.file | When trying a smaller subset of data for simple LIMIT, maximum number of files we can sample. - Default Value: 10 |
hive.limit.optimize.fetch.max | Maximum number of rows allowed for a smaller subset of data for simple LIMIT, if it is a fetch query. Insert queries are not restricted by this limit. - Default Value: 50000 |
위의 옵션 중 "hive.limit.row.max.size"의 의미를 정확하게는 알 수 없었습니다. 하지만 "hive.limit.optimize.enable" 옵션에 대해 "false"로 설정 후 테스트를 진행했을 때 정상적으로 수행되었습니다.
쿼리 성공 케이스
- ....
- (+) "hive.limit.optimize.enable" 옵션을 false로 설정 후 실행시켰을 때
이를 통해 Hive의 limit 절 쿼리를 수행할 때 optimization을 하는 과정에서 발생하는 문제라는 것을 알게 되었습니다.
Limit optimization의 동작 원리
위의 사실들을 알아냈고 문제 해결을 하기 위해 Hive가 limit 절 쿼리를 어떻게 optimization 하는지 원리를 알아야 했습니다. 그래서 관련 내용들을 Hive 소스 코드에서 확인하려고 했습니다.
Optimizer.java
...
if (HiveConf.getBoolVar(hiveConf, HiveConf.ConfVars.HIVELIMITOPTENABLE)) {
transformations.add(new GlobalLimitOptimizer());
}
...
우선 "hive.limit.optimize.enable" 옵션을 true로 설정했을 때 Hive에서는 GlobalLimitOptimizer 객체를 추가합니다. 그래서 다시 GlobalLimitOptimizer에 대해 조사하던 중 아래 문구를 발견할 수 있었습니다.
위의 내용을 통해, Hive의 limit optimization은 테이블의 실제 파일을 몇 개만 읽어 sampling 하고 그 안에서 limit 수만큼만 결과로 보여준다는 것을 알게 되었습니다.
테스트 진행
1. 테이블에 (8개 Row가 있는 parquet) + (2개 Row가 있는 parquet) 저장
- 총 10개 Row
- limit 1 ~ 8까지 에러 없이 정상 수행
- limit 9 이상부터 동일한 에러 발생
2. 테이블에 (8개 Row가 있는 parquet) + (2개 Row가 있는 parquet x 5) 저장
- 총 18개 Row
- limit 1 ~ 10까지 에러 없이 정상 수행
- limit 11 이상부터 동일한 에러 발생
이 테스트들을 미루어 보았을 때, 특정 파일 몇 개를 sampling해서 limit 수를 만족하지 못할 때 에러가 발생한다는 것을 대충 알 수 있었지만, 어떤 파일을? 몇개를? sampling 하는지에 대한 상세한 과정을 알 수 없었습니다.
Hive CLI Debug Log 분석 및 문제 해결
Hive의 데이터 sampling 과정에 대해 조금이라도 힌트를 얻기 위해 Hive CLI를 Debug 모드로 실행해 Log를 확인했습니다. 명령어는 아래와 같습니다.
$ hive --hiveconf hive.root.logger=DEBUG,console |
Hive의 쿼리 처리 과정을 상세하게 공부를 하고, Log를 한 줄씩 분석하기 시작했습니다. 확실하게 이론적인 부분에 먼저 접근한 뒤 실제 Log를 읽으니 더 많은 부분에 대해 이해할 수 있었고, 그 결과 sampling 관련 Log를 발견할 수 있었습니다.
Hive CLI Debug Log |
INFO optimizer.GenMapRedUtils: Try to reduce input size for 'limit' sizeNeeded: 2000000 file limit : 10 INFO optimizer.SamplePruner: Path pattern = hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/* INFO optimizer.SamplePruner: Got file: hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/file1.parquet DEBUG optimizer.GenMapRedUtils: Adding hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/file1.parquet of table AA_TABLE DEBUG optimizer.GenMapRedUtils: Information added for path hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/file1.parquet |
Sampling 관련 작업들은 Hive의 optimization 과정에서 이루어진다는 것을 알 수 있었습니다.
( Hive optimization 관련 내용은 [Hive] Compile 상세 과정 #2 - Optimization 종류와 소스 코드 분석 글을 참고해주세요. )
그리고 가장 핵심적인 log로써 테이블 경로에 있는 파일들을 직접 접근하는 부분을 발견할 수 있었습니다. 위의 log 내용을 통해 첫 번째 테스트 과정에서 8개 Row를 가진 "file1.parquet" 파일 1개만 sampling에 사용되었다는 것을 알 수 있었습니다. 그래서 8보다 큰 숫자로 limit을 설정했을 때 에러가 발생한 것입니다. 그렇다면 왜 1개의 파일만 sampling에 사용되었는지 알기 위해 위의 log들이 기록되는 소스 코드를 분석했습니다.
GenMapRedUtils.java
INFO optimizer.GenMapRedUtils: Try to reduce input size for 'limit' sizeNeeded: 2000000 file limit : 10
if (parseCtx.getGlobalLimitCtx().isEnable()) {
...
long sizePerRow = HiveConf.getLongVar(parseCtx.getConf(),
HiveConf.ConfVars.HIVELIMITMAXROWSIZE);
sizeNeeded = (parseCtx.getGlobalLimitCtx().getGlobalOffset()
+ parseCtx.getGlobalLimitCtx().getGlobalLimit()) * sizePerRow;
...
LOG.info("Try to reduce input size for 'limit' " +
"sizeNeeded: " + sizeNeeded +
" file limit : " + fileLimit);
...
GenMapRedUtils에서 log에서 발견된 sizeNeeded 변수의 계산 방법을 확인할 수 있었습니다. 계산 방법은 (limit 수) x (hive.limit.row.max.size) 입니다. limit 수를 Row 수라고 생각하여 최대 사이즈만큼 곱해준 값입니다. sizeNeeded 변수가 sampling에 필요한 값이라고 의심되지만, 아직 확인된 게 없으니 일단 넘어가겠습니다.
SamplePruner.java
INFO optimizer.SamplePruner: Path pattern = hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/*
INFO optimizer.SamplePruner: Got file: hdfs://nameservice1/data_path/AA_TABLE/partition_p=p1/file1.parquet
...
LOG.info("Path pattern = " + pathPattern);
FileStatus srcs[] = fs.globStatus(new Path(pathPattern));
Arrays.sort(srcs);
boolean hasFile = false, allFile = true;
for (FileStatus src : srcs) {
if (sizeLeft <= 0) {
allFile = false;
break;
}
...
LOG.info("Got file: " + src.getPath());
hasFile = true;
retPathList.add(src.getPath());
sizeLeft -= src.getLen();
if (retPathList.size() >= fileLimit && sizeLeft > 0) {
return null;
}
...
SamplePruner.java에서 그토록 원하던 sample 파일 선별 과정을 확인할 수 있었습니다. 파일 선별 과정을 요약하면 이렇습니다.
- 테이블 경로의 모든 파일을 srcs 배열에 저장
- srcs 배열을 파일명 순서로 정렬하고 반복문 실행
- 이때 sizeLeft 변수가 0 이하면 반복문 종료
- 파일을 List에 추가 후 파일 크기만큼 sizeLeft 감소
여기서 sizeLeft 변수는 GenMapRedUtils.java에서 사용된 sizeNeeded라는 것을 코드를 통해 확인할 수 있습니다. 즉 (limit 수) x (hive.limit.row.max.size)로 계산된 값이 sampling 할 총 파일 크기입니다.
결국 문제의 원인은 sizeNeeded 변수 값에 있었습니다. hive.limit.row.max.size 옵션의 default 값은 100,000으로 100KB입니다. 하지만 테이블의 데이터 파일을 확인한 결과 Row당 파일크기는 300KB까지 나가기도 했습니다. 결국 limit 20을 만족할만한 파일 sampling을 할 수 없었습니다.
그래서 hive.limit.row.max.size 옵션을 넉넉하게 3MB인 3,000,000으로 설정했습니다. 쿼리 실행 시 파일 sampling이 충분히 되어 limit 절을 이용하더라도 이전과 같은 에러가 발생하지 않았습니다.
결론
- limit 절을 포함한 쿼리 수행 시 에러 발생
- limit optimization 수행 시 에러 발생
- limit optimization의 파일 sampling 과정에서 용량 조건으로 인해 부족한 sampling 확인
- hive.limit.row.max.size 옵션 값 설정으로 문제 해결
References
https://cwiki.apache.org/confluence/display/Hive/Configuration+Properties
'Hadoop Ecosystem > Hive' 카테고리의 다른 글
[Hive] Complex Json 데이터 테이블 생성하기 (Json SerDe) (0) | 2022.12.15 |
---|---|
[Hive] IntelliJ로 Runtime Debugging하기 (0) | 2022.11.12 |
[Hive] Metastore의 heap memory 증가 이슈 해결 과정 (2) | 2022.07.18 |
[Hive] Hive 아키텍처와 HiveServer2 & Hive Metastore (0) | 2022.07.01 |
[Hive] Unable to fetch table .null 에러 해결 방법 (0) | 2022.05.30 |