#include "GarbageClean.h" #include #include #include #include #include #include #include #include #include #include "Logger.h" #include "ProcessConfig.h" #define SQLITE_DB_LOCK_TIMEOUT 1000 // SQLITE_BUSY 상황 발생 문제 해결을 위해 DB Lock 관련 timeout 설정값. (1 sec) #define SQLITE_DB_LOCK_MAX_RETRY 60 // SQLITE_BUSY 발생시 재시도 최대 횟수 // meta 에 존재하지 않아.. unlink 여부 판단시... 삭제 방지 day 값 // - unlink 대상인 경우.. 최종 수정일, 최종 access 시간이 본 설정값 이하이면 삭제되지 않도록 처리. // - 2019-08-29 일감 #32774 에 의해 7일 -> 2일로 변경 처리함. #define UNLINK_PREVENT_MAX_DAY 2 // unlink slow 모드인 경우.. unlink 후 sleep 시간 (sec) // - 2 sec sleep : 일 4만건 unlink // - 1 sec sleep : 일 8만건 unlink #define UNLINK_SLOW_MODE_SLEEP 1 // 생성자 // - pLocalDb : sqlite memory db 객체에 대한 포인트 CGarbageClean::CGarbageClean( sqlite3 * pLocalDb ) : m_pLocalStmt( NULL ) { m_pLocalDb = pLocalDb; } // 소멸자 CGarbageClean::~CGarbageClean() { m_vecTargetDirectory.clear(); // Local DB stamt clear if( m_pLocalStmt != NULL ) { sqlite3_finalize( m_pLocalStmt ); m_pLocalStmt = NULL; } } // 객체 초기화 함수 bool CGarbageClean::Init() { // 작업대상 폴더 정보를 저장할 변수 초기화. m_vecTargetDirectory.clear(); // Local sqlite db 관련 변수 int nResult = 0; int nRetryCount = 0; // strQuery = "CREATE TABLE meta( resource_id integer, filename_hash text, get_content_length integer, deleted_yn text )"; // 조회 처리를 수행할 stmt 객체 생성. // - 결과 추출 오류 발생시 sqlite3_column_int() 함수가 0 을 반환하므로.. 오류 와.. 실제 미존재 상태를 구분하기 위해... +1 처리함. std::string strQuery; strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;"; nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL ); // sqlite3_prepare() 함수 실행시 SQLITE_BUSY 오류가 발생할 수 있으므로.. 재시도 로직을 추가한다. while( nResult == SQLITE_BUSY && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY ) { sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT ); ++nRetryCount; _LOG( LWAR, "Local db prepare sql SQLITE_BUSY. Retry[%d]", nRetryCount ); nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL ); } // 최종 실패시... if( nResult != SQLITE_OK ) { LOG( LERR, "sqlite3 prepare sql stmt create failed. [%d][%s]", nResult, sqlite3_errmsg( m_pLocalDb ) ); return false; } return true; } // garbage clean 작업 수행. // - bGarbageCleanSlow : unlink 수행 후 sleep 를 호출하는 slow 모드로 동작시킬지 여부. // - bGarbageForceDelete: unlink 처리시 access time 정보를 check 할지 여부 bool CGarbageClean::Start( bool bGarbageCleanSlow, bool bGarbageForceDelete ) { // 작업 소요 시간 정보 계산을 위한 변수. // - 시간 동기화로 인한 문제를 해결하기 위해 clock_gettime() 함수 사용. struct timespec timeStart; struct timespec timeEnd; double dTimeInterval = 0.0; memset( &timeStart, 0x00, sizeof( struct timespec ) ); memset( &timeEnd, 0x00, sizeof( struct timespec ) ); // Meta 에 존재하지 않는 파일 삭제 처리 관련... // 현재 시간 정보 추출하여.. 정의된 보호 시간을 뺀다. time_t unlinkPreventMaxTime; unlinkPreventMaxTime = time( 0 ) - ( UNLINK_PREVENT_MAX_DAY * 24 * 60 * 60 ); // 작업 시작 로깅 및 시작 시간 정보 추출. _LOG( LINF, "Garbage clean start." ); _LOG( LINF, "- unlink speed: %s", ( bGarbageCleanSlow == true ? "slow" : "normal" ) ); _LOG( LINF, "- unlink modifiy time max prevent : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime); if( bGarbageForceDelete == false ) { _LOG( LINF, "- unlink access time max prevent : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime ); } else { _LOG( LINF, "- unlink access time not check. force unlink." ); } clock_gettime( CLOCK_MONOTONIC, &timeStart ); // 통계 정보 저장을 위한 변수 // 각 폴더별 삭제 count unsigned long long ullDirTotalCount = 0; // 각 폴더별 처리한 count 정보 저장. unsigned long long ullDirUnlinkCount = 0; // 각 폴더별 삭제한 count 정보 저장. unsigned long long ullTotalCount = 0; // 누적 처리 count 정보 저장. unsigned long long ullTotalUnlinkCount = 0; // 누적 삭제 count 정보를 저장 unsigned long long ullTotalUnlinkSize = 0; // 누적 삭제 Size 정보를 저장 // 1. 설정된 대상 경로를 바탕으로.. 실제 최하위 서비스 폴더 목록 list 를 추출한다. if( GetTargetDir() == false ) { // 오류 발생시.. 해당 모듈에서 로깅 처리 했으므로.. 실패로 처리. return false; } else { _LOG( LINF, "Garbage clean [%s] sub service directory count [%lu]", CProcessConfig::GetInstance()->GetJobRoot(), m_vecTargetDirectory.size() ); } // 2. 추출된 각 경로에 대해.. 파일 정보를 추출하여.. 검증 수행. DIR * pDir = NULL; struct dirent * pDirEntry = NULL; std::vector< std::string >::iterator it; // 파일에 대한 정보를 저장할 변수. std::string strFullFileName; int nFullFileNameExistCount; struct stat stFileStat; // Local sqlite db 관련 변수 int nResult = 0; int nRetryCount = 0; for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it ) { pDir = opendir( it->c_str() ); if( pDir == NULL ) { // 폴더 open 실패시... // - 오류 내역 로깅하고 계속 진행. LOG( LERR, "directory[%s] open faild. [%d][%s]", it->c_str(), errno, strerror( errno ) ); continue; } // 개별 폴더 통계 초기화. ullDirTotalCount = 0; ullDirUnlinkCount = 0; _LOG( LINF, "Garbage clean [%s] start", it->c_str() ); while( ( pDirEntry = readdir( pDir ) ) != NULL ) { // 만약 하위에 폴더, 링크, FIFO 등이 존재할 수 있으므로... // 이를 제외하고 only 파일만 비교 처리. // man 5 dir // d_type 정의 내역. // #define DT_DIR 4 // #define DT_REG 8 if( pDirEntry->d_type == DT_REG ) { // '.'으로 시작하는 숨김 파일 등은 제외 처리. ( '.', '..', '..sujournal' 등 ) if( pDirEntry->d_name[0] == '.' ) continue; // 진행 사항 로깅. if( ullDirTotalCount % 10000 == 0 && ullDirTotalCount != 0 ) { _LOG( LINF, "Garbage checking. [%s] count[%10llu] unlink[%10llu]" , it->c_str(), ullDirTotalCount, ullDirUnlinkCount ); } // 통계 정보 증감 처리 ++ullDirTotalCount; // 파일명 전체 경로 정보 조합 strFullFileName = it->c_str(); strFullFileName.append( "/" ); strFullFileName.append( pDirEntry->d_name ); // 이제 실제로 파일이 존재하는지 local DB 에서 확인한다. // select query 에 변수 bind 처리. ( bind 값을 1부터 시작 ) // - bind 처리시.. SQLITE_TRANSIENT 도 사용할 수 있으나.. 내부 copy 로 속도 느림 // - query 처리 중간에 변수값이 변경될 가능성 없으므로.. SQLITE_STATIC 사용. nResult = sqlite3_bind_text( m_pLocalStmt, 1, strFullFileName.c_str(), strFullFileName.size(), SQLITE_STATIC ); if( nResult != SQLITE_OK ) { // bind 처리 실패시.... LOG( LERR, "sqlite3 bind error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) ); // reset 처리 후.. 다음 항목을 진행 처리 sqlite3_reset( m_pLocalStmt ); continue; } // Query 실행. nResult = sqlite3_step( m_pLocalStmt ); nRetryCount = 0; while( nResult == SQLITE_BUSY && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY ) { sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT ); ++nRetryCount; LOG( LWAR, "sqlite3 step SQLITE_BUSY, Retry[%d]", nRetryCount ); // query 재시도 nResult = sqlite3_step( m_pLocalStmt ); } // SELECT query 수행시.... // - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환. // - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다. // - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨. // select query 수행시 오류가 발생한 경우... if( nResult != SQLITE_ROW && nResult != SQLITE_DONE ) { LOG( LERR, "sqlite3 step error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) ); // 만약 오류 코드가 SQLITE_ERROR, SQLITE_INTERNAL, SQLITE_EMPTY, SQLITE_SCHEMA 등이라면.... DB에 문제 있는 것으로 보고.. 작업 중지. if( nResult == SQLITE_ERROR || nResult == SQLITE_INTERNAL || nResult == SQLITE_EMPTY || nResult == SQLITE_SCHEMA ) { LOG( LERR, "sqlite3 db not valid. garbage clean stop."); closedir( pDir ); pDir = NULL; sqlite3_finalize( m_pLocalStmt ); m_pLocalStmt = NULL; return false; } // 그 외 오류에 대해서는 무시하고 계속 진행. } else { // SELECT query 수행시.... // - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환. // - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다. // - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨. // strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;"; // - sqlite3_column_int() 호출시 오류가 발생할 경우에 대한 처리를 위해 +1 처리함. // 조회 결과를 변수에 저장 처리. nFullFileNameExistCount = sqlite3_column_int( m_pLocalStmt, 0 ); // nFullFileNameExistCount == 0 : sqlite3_column_int() 함수 오류로 default 값이 반환된 경우... // nFullFileNameExistCount == 1 : Meta 에 해당 파일 정보 미존재시. // 그 이외에는 Meta 에 해당 파일 정보 존재로 처리. //_LOG( LINF, "[GARBAGE] Meta SELECT. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount -1 ); if( nFullFileNameExistCount == 1 ) { // Meta 에 해당 파일 미존재시. // 해당 파일 속성 정보 추출. if( stat( strFullFileName.c_str(), &stFileStat ) == 0 ) { // 만약 해당 파일이 Meta에 존재하지 않는 경우... // 최종 수정일 정보를 추출하여... UNLINK_PREVENT_MAX_DAY 일이 지났는지 확인 후. // UNLINK_PREVENT_MAX_DAY 일 이상된 content 인 경우.. 삭제 처리한다. // 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이상이면 삭제 처리 // - difftime() 함수를 사용해도 되지만.. CPU 사용률이 거의 100% 수준이라... CPU 부하 낮추기 위해 날코딩 처리. if( stFileStat.st_mtime < unlinkPreventMaxTime ) { // access time 을 check 하지 않고.. unlink 처리 수행한다. if( bGarbageForceDelete == true ) { // 해당 파일 unlink 처리. unlink( strFullFileName.c_str() ); ++ullDirUnlinkCount; ullTotalUnlinkSize += stFileStat.st_size; _LOG( LINF, "[GARBAGE] unlink with force[%s] mtime[%ld] size[%jd]" , strFullFileName.c_str(), stFileStat.st_mtime, stFileStat.st_size ); // unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다. if( bGarbageCleanSlow == true ) sleep( UNLINK_SLOW_MODE_SLEEP ); } else if( stFileStat.st_atime < unlinkPreventMaxTime ) { // -f 옵션을 사용하지 않은 상태이고 access time 이 2일 이상인 경우 // 삭제 처리를 수행한다. // 해당 파일 unlink 처리. unlink( strFullFileName.c_str() ); ++ullDirUnlinkCount; ullTotalUnlinkSize += stFileStat.st_size; _LOG( LINF, "[GARBAGE] unlink[%s] atime[%ld] mtime[%ld] size[%jd]" , strFullFileName.c_str(), stFileStat.st_atime, stFileStat.st_mtime, stFileStat.st_size ); // unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다. if( bGarbageCleanSlow == true ) sleep( UNLINK_SLOW_MODE_SLEEP ); } else { // 파일의 최종 수정 시간은 UNLINK_PREVENT_MAX_DAY 값을 초과했지만. // force mode 가 아니고, access 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우.. // - 로깅만 처리. _LOG( LINF, "[%s] meta not exist, but access occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]" , strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size ); } } else { // 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우.. // - 로깅만 처리. _LOG( LINF, "[%s] meta not exist, but modify occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]" , strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size ); } } else { // stat 호출 오류인 경우... // - multi thread 가 아직 1 process 구조라서.. errno 를 그대로 사용.... // - 만약 multi thread 구조로 변경할 경우.. errno 그대로 사용하면 안됨. // garbage clean 를 very slow 모드로 동작시킬 경우... // - 작업 시간이 오래 걸려... // - 해당 물리 파일이 rc_gcmd 등의 다른 프로세스에 의해 삭제될 가능성이 존재함. // - 이를 경우... 다음과 같은 오류 발생 // [17:25:42] [ERR ] [GARBAGE] meta not exist. but file stat call error. name[/stg/node0/250/22a753b521dfc43ae468854c2089e8ef] [2][No such file or directory] // - 따라서.. 다른 프로세스에 의해 삭제된 경우.. 불필요하게 오류 찍을 필요 없다. if( errno != ENOENT ) { _LOG( LERR, "[GARBAGE] meta not exist. but file stat call error. name[%s] [%d][%s]", strFullFileName.c_str(), errno, strerror( errno ) ); } } } else if( nFullFileNameExistCount == 0 ) { //sqlite3_column_int() 함수 오류로 default 값이 반환된 경우... LOG( LERR, "sqlite3 column get error.[%s] [%d][%s]", strFullFileName.c_str(), nFullFileNameExistCount, sqlite3_errmsg( m_pLocalDb ) ); } else { // Meta 에 해당 파일 정보 존재로 처리. //_LOG( LINF, "[GARBAGE] Meta OK. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount - 1 ); } } // Query 수행된 경우.. prepared 문을 재사용하기 위해 reset 처리한다. sqlite3_reset( m_pLocalStmt ); } } // while( ( pDirEntry = readdir( pDir ) ) != NULL ) // open 처리된 dir 객체를 close 처리한다. closedir( pDir ); pDir = NULL; // 통계 저장 처리 ullTotalCount += ullDirTotalCount; ullTotalUnlinkCount += ullDirUnlinkCount; _LOG( LINF, "Garbage clean [%s] end. unlink count[ %llu / %llu ] ", it->c_str(), ullDirUnlinkCount, ullDirTotalCount ); } // for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it ) // 작업 완료 후 stmt 객체 clear sqlite3_finalize( m_pLocalStmt ); m_pLocalStmt = NULL; // 작업 완료시 시간 정보 추출 후 소요시간 계산. clock_gettime( CLOCK_MONOTONIC, &timeEnd ); dTimeInterval = timeEnd.tv_sec - timeStart.tv_sec; dTimeInterval += ( timeEnd.tv_nsec - timeStart.tv_nsec ) * 1e-9; _LOG( LINF, "Garbage clean complete. path[%s] unlink mode[%s] elapsed time: %.1lf sec." , CProcessConfig::GetInstance()->GetJobRoot() , ( bGarbageCleanSlow == true ? "slow" : "normal" ) , dTimeInterval ); _LOG( LINF, "Garbage clean report: total count[%llu] unlink count[%llu] unlink size[%llu byte]" , ullTotalCount, ullTotalUnlinkCount, ullTotalUnlinkSize ); return true; } // 설정된 기본 경로를 바탕으로.. 작업대상 폴더명 정보를 추출하여.. 멤버변수에 저장 처리. bool CGarbageClean::GetTargetDir() { // 사용자가 입력했거나.. 기본으로 설정된 경로에서... // 실제 정리 작업을 수행할 서비스 폴더를 추출한다. // - 서비스 폴더가 아닌 경우.. 운영 목적으로 설정한 파일들이 존재하여.. // - 삭제될 수 있으므로... // - 서비스 폴더에 대해서만 정리 작업이 수행되도록 한다. // 추출 방식 // - 직접 코드로 opendir, readdir 함수를 이용하여 추출 가능하지만.... // - 불필요하게 시간이 오래 걸리고.. 버그의 가능성 존재. // - 따라서 외부 명령어를 이용하여.. // - 하위 폴더 정보를 추출하고.... 서비스 폴더인지 검증 처리. // 외부 tool // find /stg/node0 -type d -print // find /stg/node0 -type d -print | grep '/[1-9]*$' // 참고사항 // main 모듈에서 root 경로에 대한 // 맨 끝의 '/' 제거 및 // 존재 여부 확인 했으므로... 그냥 사용하면 됨. char tempBuffer[2048]; std::string strTempPath; FILE * fd = NULL; // trim 처리 관련 변수 std::string::size_type pos; // 명령어 조립 snprintf( tempBuffer, sizeof( tempBuffer ), "find %s -type d -print | grep '/[0-9]*$'", CProcessConfig::GetInstance()->GetJobRoot() ); fd = popen( tempBuffer, "r" ); if( fd == NULL ) { int errorNum = errno; LOG( LERR, "service directory search error. popen error. [%d][%s]", errorNum, strerror( errorNum ) ); return false; } else { // 작업 대상 경로를 저장할 멤버 변수 초기화. m_vecTargetDirectory.clear(); memset( tempBuffer, 0x00, sizeof( tempBuffer ) ); while( fgets( tempBuffer, sizeof( tempBuffer ) - 1, fd ) != NULL ) { // 결과 맨 끝에 줄바꿈 문자가 있어.. trim 처리 필요 strTempPath = tempBuffer; if( strTempPath.length() == 0 ) continue; pos = strTempPath.find_last_not_of( " \a\b\f\n\r\t\v" ); if( pos != std::string::npos ) strTempPath.erase( pos + 1 ); // 원래는 추출된 전체 경로.. (/stg/node0/290 ) 에서 // 마지막 290 정보 추출하여 숫자인지 비교해야 하지만... // 귀찬아서.. 외부 명령어의 grep 으로 비교 처리 넣어버림... // 따라서.. 추출된 폴더 정보를 바로 저장 처리하면 됨. m_vecTargetDirectory.push_back( strTempPath ); } pclose( fd ); } return true; }