使用shell併發上傳文件到hdfs

    最初業務需求:將文件從ftp下到本地並上傳到hdfs,其中ftp到本地和本地到hdfs皆有文件完整性校驗,ftp到本地的邏輯由於和業務耦合度較高,因此本文不再敘述,這裏僅說一下從本地到hdfs的併發腳本相關說明及代碼實現。


  測試環境: RHEL6.4 x86_64 / Hadoop 2.4.0.2.1.5.0-695

  

部分需求說明:

  1、需要提供一個文件列表,以文件的形式,每行一個文件,所有文件有一個共同的父目錄,且文件是有效存在的,當然,不存在腳本也會判斷並記錄的。

  2、需要提供一個hdfs的父路徑(絕對路徑),此路徑用於將本地文件的父路徑替換,此hdfs路徑需要執行腳本的用戶有讀寫權限,當然,沒有權限的話會報錯並記錄日誌。

  3、可以併發上傳,可以設置併發個數,當然,會有最大個數限制(32,可直接修改相關變量)

  4、可以檢測是否有已傳成功的,並忽略本地上傳(重複大文件特別節省時間)

  5、可以根據提供的帶寬計算每個文件的上傳超時時間,並記錄日誌以便於調試合適的執行

  6、上傳失敗的要記錄日誌,並計入重試列表,可自動處理重試列表

  7、不匹配的文件要記錄日誌,並放入無效文件列表

  8、一個時間段內腳本只能在系統中有一個正在運行的

  9、文件完整性校驗通過對於文件的大小(未找到在hdfs上直接獲取某個文件的md5的方法...)

  10、暫時就想到這麼多了...

部分邏輯說明:

  1、文件上傳超時時間公式: 文件大小/總帶寬(默認5MB/s,)/併發個數+60

  2、由於無法把函數單獨放入後臺執行,因此腳本分爲控制腳本和上傳腳本,用戶使用控制腳本即可。

  3、上傳日誌,上傳列表文件,重試列表文件,上傳線程文件都放在/var/log/ftp_op目錄下

  4、併發進程數必須爲正整數數字,如果輸入錯誤則報錯(但亂碼處理仍不太理想,算bug..)。併發進程數當大與32(ctrl_put.sh: 28行max_threads=32控制)時,則強制修改爲32;當上傳列表行數小於進程數時,則修改進程數爲上傳列表行數。

  6、上某個線程超時,則將其需要上傳的文件放入重試列表,並kill掉其進程,刪除掉其標誌文件

  7、上傳線程的標誌文件命名爲 前綴_線程id_時間戳_文件大小_超時時間

  8、....不說了,代碼中有註釋.......

  注:相關日誌格式;如下

   1) 上傳成功日誌格式: 時間、具體操作函數、hdfs文件路徑、狀態、文件大小、上傳所用時間、分配的超時時間、有(check_size)表示此文件已存在,且對比大小一致,直接標價爲成功;日誌如下:

wKiom1SNPHTR8ZxyAAIkPg9ze6M580.jpg   2)當要處理的文件的父路徑中沒有參數或變量定義的路徑時,日誌如下:

wKioL1SNPbWBZpfVAAG2iR6G9cQ785.jpg   3)當上傳時,在hdfs上無法創建路徑,或者無法修改權限時,日誌如下:

wKioL1SNPlHjSHigAAK63Y5Yg38704.jpg  4) 當文件大小對比失敗,或hdfs dfs -put命令執行失敗時,日誌如下:

wKiom1SNPnTy7OWrAACtgo4aji0886.jpg

  5) 腳本中定義了其它相關報錯日誌,但由於筆者測試過程未出現相關報錯,也無法一一列出

腳本


腳本使用說明

計劃任務使用:

* * * * * /opt/ctrl_put.sh 10 /opt/upload_thread.sh /opt/localfiles /tmp/ftpfiles

# /opt/ctrl_put.sh 10 /opt/upload_thread.sh /opt/localfiles /tmp/ftpfiles

上述命令說明:

  控制腳本: /opt/ctrl_put.sh

  上傳教程: /opt/upload_thread.sh

  線程: 10

  本地文件列表中文件的父目錄: /opt/localfiles

  要上傳到hdfs的父目錄: /tmp/ftpfiles

  注: /opt/ctrl_put.sh腳本的第39行是文件上傳列表變量;40行是重試列表變量;38行是無效文件列表;19行是log_dir的變量,此路徑需要腳本執行者有所有權限


好吧,囉嗦了這麼久,見代碼如下

控制腳本: ctrl_put.sh

#!/bin/bash

[ -x /bin/basename ] && bn_cmd=/bin/basename
[ -x /usr/bin/basename ] && bn_cmd=/usr/bin/basename
[ -x /usr/bin/dirname ] && dn_cmd=/usr/bin/dirname
[ -x /usr/bin/wc ] && wc_cmd=/usr/bin/wc
[ -x /usr/bin/uniq ] && uq_cmd=/usr/bin/uniq
[ -x /usr/bin/hdfs ] && hdp_cmd="/usr/bin/hdfs dfs"

# 檢查是否有本腳本pid
pid_file=/tmp/`$bn_cmd $0`_ftp_op.pid
if [[ -f $pid_file ]];then
 ps -p `cat $pid_file` &> /dev/null
 [[ "$?" -eq "0" ]] && echo "`$log_date` : $0 exist." && exit 0
fi
echo $$ > $pid_file 

log_date="/bin/date +%H:%M:%S/%Y-%m-%d"
log_dir=/var/log/ftp_op
log_file=$log_dir/ftp_op.log

threads=${1:-10}
thread_script=${2:-/opt/upload_thread.sh}
#check_period=${5:-10}
check_period=5
timestamp="/bin/date +%s"
thread_file_pre=$log_dir/threadfile
max_threads=32
# 5242880 = 5M/s
network_speed=5242880
net_speed=`echo $network_speed $threads|awk '{printf("%.0lf",$1/$2)}'`

if [[ ! -d $log_dir ]];then
  mkdir -p $log_dir ; mkdir_res=$?
  [[ $mkdir_res -ne 0 ]] && echo "$log_dir : Can't create directory" && exit 1
fi

put_invalid_list=$log_dir/put_hdfs_invalid.list
put_hdfs_list=$log_dir/put_hdfs.list
put_retry_list=$log_dir/retry_put.list

final_dir=${3:-/storage/disk9/localfiles}
hdfs_dir=${4:-/tmp/hdfs/files}

# 日誌記錄函數
TEE(){
  /usr/bin/tee -a $log_file
}


# 重試列表追加入當前列表
# 如果檢測到重試列表不爲空,追加進上傳列表
# 此函需要兩個參數 $1 $2
# $1 : 重試列表文件
# $2 : 標準處理列表
RETRY_LIST(){
  if [ -f $1 ];then
    retry_sum=`cat $1|/usr/bin/wc -l`
    if [[ $retry_sum -ne 0 ]];then
      cat $1 >> $2
      rm -rf $1
    fi
  fi
}

# 線程個數策略,此函數需要提供兩個參數
# $1 : 原始上傳列表文件
# $2 : 用戶提供的線程個數
THREAD_POLICY(){
  if [[ $# -ne 2 ]];then 
    echo "`$log_date` $FUNCNAME Error: \$# 1= 2" >> $log_file
    return 1 
  fi
  if [[ ! -f $1 ]];then
    echo "`$log_date` $FUNCNAME $1 No such file" >> $log_file
    return 2 
  fi
  echo "$2"|grep -q '^[-]\?[0-9]\+$'
  if [[ $? -ne 0 ]];then
    echo "`$log_date` $FUNCNAME $2 Invalid number" >> $log_file 
    return 3 
  fi
  local list_sum=`cat $1|$wc_cmd -l`
  if [[ $list_sum -eq 0 ]];then
    #echo "`$log_date` $FUNCNAME $1 is empty" >> $log_file
    return 0
  else
    if [[ $2 -ge $max_threads ]];then
      [[ "$list_sum" -le "$max_threads" ]] && echo $list_sum || echo $max_threads
    else
      [[ "$list_sum" -le "$2" ]] && echo $list_sum || echo $2
    fi
  fi
}

# 超時失敗處理,此函數需要提供一個參數
# $1 : 超時線程的pid標識文件
TIMEOUT_HANDLE(){
  if [[ ! -f $1 ]];then
    echo "`$log_date` $FUNCNAME $1 no such file" >> $log_file 
    return 1 
  fi
  local old_pid=`/usr/bin/tail -1 $1`
  ps -p $old_pid &> /dev/null
  if [[ $? -eq 0 ]];then 
    kill $old_pid &> /dev/null
    if [[ $? -eq 0 ]];then
      sed -n "1p" $1 >> $put_retry_list
      rm -rf $1
      return 0
    else
      echo "`$log_date` $FUNCNAME $2 kill $old_pid fail." >> $log_file
      local file_dir=`$dn_cmd $1`  ; local file_name=`$bn_cmd $1`
      sed -n "1p" $1 >> $put_retry_list
      mv -f $1 $file_dir/fail_kill_$file_name
      ps -p $old_pid &> /dev/null
      if [[ $? -eq 0 ]];then
        echo "`$log_date` $FUNCNAME $2 kill $old_pid fail." >> $log_file 
        return 1
      else
        return 0
      fi
    fi
  else
    sed -n "1p" $1 >> $put_retry_list
    rm -rf $1
  fi
# $put_hdfs_list $put_retry_list $threads
}

# 創建線程執行腳本所需文件,此函數需要兩個參數
# $1 : 線程執行腳本id號
# $2 : 要處理的具體文件的絕對路徑 
CREATE_THREAD_FILE(){
  if [[ $# -ne 2 ]];then
    echo "`$log_date` $FUNCNAME Error \$#!=2" >> $log_file
    return 1 
  fi
  if [[ -z $1 ]];then
    echo "`$log_date` $FUNCNAME $1 is empty" >> $log_file
    return 0 
  fi
  if [[ ! -f $2 ]];then
    echo "`$log_date` $FUNCNAME $2 no such file" >> $log_file 
    return 2 
  fi
  local file_size=`/usr/bin/du -b $2|awk '{print $1}'`
  local time_out=`echo $file_size $net_speed|awk '{printf("%.0lf",$1/$2+60)}'`
  local thread_file="$thread_file_pre"_"$1"_`$timestamp`_"$file_size"_"$time_out"
  echo $2 > $thread_file
  if [[ $? -eq 0 ]];then
    echo $thread_file 
    return 0
  else
    echo "`$log_date` $FUNCNAME $thread_file Can't create file" >> $log_file
    return 3 
  fi
}

# 超時策略,此函數需要提供兩個參數
# $1 : 當前需要創建的線程個數id
# $2 : 要處理文件的絕對路徑
THREAD_FILE_POLICY(){
  if [[ $# -ne 2 ]];then
    echo "`$log_date` $FUNCNAME Error \$#!=2" >> $log_file
    return 1 
  fi
  if [[ -z $1 ]];then
    echo "`$log_date` $FUNCNAME $1 is empty" >> $log_file
    return 0 
  fi
  if [[ ! -f $2 ]];then
    echo "`$log_date` $FUNCNAME $2 no such file" >> $log_file 
    return 2 
  fi
  local old_file=`/bin/ls "$thread_file_pre"_"$1"_* 2> /dev/null`
  if [[ -f $old_file ]];then
    local now_time=`$timestamp`
    local old_time=`$bn_cmd $old_file|awk -F_ '{print $3}'`
    local file_timeout=`$bn_cmd $old_file|awk -F_ '{print $NF}'`
    local now_timeout=`echo $now_time $old_time|awk '{printf("%.0lf",$1-$2)}'`
    if [[ $now_timeout -le $file_timeout ]];then
      return 0 
    else
      if TIMEOUT_HANDLE $old_file ;then
        echo `CREATE_THREAD_FILE $1 $2`
      fi
    fi 
  else
    echo `CREATE_THREAD_FILE $1 $2`
  fi
}

# 主控進程函數
MASTER_CTRL(){
  if [[ $# -ne 4 ]];then
    echo "`$log_date` $FUNCNAME Error \$#!=4" >> $log_file
    return 1 
  fi
  while :;do
    RETRY_LIST $2 $1
    local final_threads=`THREAD_POLICY $1 $4`
    [[ -z $final_threads ]] && break
    for t in `/usr/bin/seq 1 $final_threads`;do
      local file_path=`sed -n "1p" $1`
      echo $file_path|grep -q $final_dir
      if [[ $? -ne 0 ]];then
        echo "`$log_date` $FUNCNAME $file_path invalid file" >> $log_file
        echo $file_path >> $put_invalid_list 
        sed -i "1d" $1
        continue
      fi
      local thread_file=`THREAD_FILE_POLICY $t $file_path`
      if [[ -f $thread_file ]];then
        /bin/bash $3 $thread_file $final_dir $hdfs_dir $2 &
        sed -i "1d" $1
      fi
    done
    [[ ! -z $final_threads ]] && sleep $check_period 
  done
  rm -rf $pid_file
}

MASTER_CTRL $put_hdfs_list $put_retry_list $thread_script $threads


上傳線程腳本:upload_thread.sh

#!/bin/bash

[[ ! -f $1 ]] && echo "Error, Invalid File" && exit 1
[[ ! -d $2 ]] && echo "Error, Invalid Directory" && exit 1
echo $$ >> $1

[ -x /bin/basename ] && bn_cmd=/bin/basename
[ -x /usr/bin/basename ] && bn_cmd=/usr/bin/basename
[ -x /usr/bin/dirname ] && dn_cmd=/usr/bin/dirname
[ -x /usr/bin/wc ] && wc_cmd=/usr/bin/wc
[ -x /usr/bin/uniq ] && uq_cmd=/usr/bin/uniq
[ -x /usr/bin/hdfs ] && hdp_cmd="/usr/bin/hdfs dfs"
[ -x /usr/bin/md5sum ] && ms_cmd=/usr/bin/md5sum

log_date="/bin/date +%H:%M:%S/%Y-%m-%d"
log_dir=/var/log/ftp_op
log_file=$log_dir/ftp_op.log
put_retry_list=${5:-$log_dir/retry_put.list}
timestamp="/bin/date +%s"
now_timestamp=`$timestamp`

[ ! -d $log_dir ] && mkdir -p $log_dir
# 日誌記錄函數
TEE(){
  /usr/bin/tee -a $log_file
}

# 本地和hdfs的文件大小對比函數
# 此函數需要兩個參數 $1 $2
# $1爲本地文件大小 $2爲hdfs文件路徑
HDFS_SIZE_CHECK(){
  if [[ $# -ne 2 ]];then
    echo "`$log_date` $FUNCNAME Error \$#!=2 \$1 or \$2 is empty"|TEE
    return 1
  fi
  local hdfs_size=`$hdp_cmd -du $2|awk '{print $1}'`
  [[ $1 -eq $hdfs_size ]] && return 0 || return 1
}

# 此函數需要三個參數
# $1 : hdfs的文件名
# $2 : 本地的對應文件的大小
# $3 : hdfs文件的目錄
HDFS_LOCATION_CHECK(){
  if [[ $# -ne 3 ]];then
    return 2
  fi
  if $hdp_cmd -test -d $3 ;then
    if $hdp_cmd -test -f $1 ;then
       if HDFS_SIZE_CHECK $2 $1 ;then
         $hdp_cmd -rm -r -f -skipTrash $1.tmp
         return 1 
       else
         $hdp_cmd -rm -r -f -skipTrash $1
       fi
    fi
    if $hdp_cmd -test -f $1.tmp ;then
      if HDFS_SIZE_CHECK $2 $1.tmp ;then
        $hdp_cmd -mv $1.tmp $1
        return 1
      else
        $hdp_cmd -rm -r -f -skipTrash $1.tmp
        return 0
      fi
    else
      return 0
    fi
  else
    if $hdp_cmd -mkdir -p $3 ;then
      $hdp_cmd -chmod 777 $3 && return 0 || return 4
    else
      return 3
    fi
  fi
}

# 此函數僅作上傳處理,此函數需要五個參數
# $1 需要上傳的本地文件
# $2 要上傳到hdfs的目標文件
# $3 本地文件的大小byte
# $4 分配的超時時間 
# $5 本地文件的du -sh的統計大小
ONLY_UPLOAD(){
  if [[ $# -ne 5 ]];then
    echo "`$log_date` $FUNCNAME Error: \$# != 5"|TEE
    return 1
  fi
  if [[ ! -f $1 ]];then
    echo "`$log_date` $FUNCNAME Error: \$1=$1 no such file"|TEE
    return 1
  fi
  $hdp_cmd -put -f $1 $2.tmp &> /dev/null
  if HDFS_SIZE_CHECK $3 $2.tmp ;then
    $hdp_cmd -mv $2.tmp $2 &> /dev/null
    local nowtime=`$timestamp` ; local costtime=`/usr/bin/expr $nowtime - $now_timestamp`
    echo "`$log_date` $FUNCNAME $2 Upload Success $5 $costtime $4" >> $log_file
    return 0
  else
    $hdp_cmd -rm -r -f -skipTrash $2.tmp &> /dev/null
    return 1
  fi
}

# 上傳HDFS
PUT_TO_HDFS(){
  if [[ $# -ne 3 ]];then
    echo "`$log_date` $FUNCNAME Error: \$# 1= 3"|TEE
    return 1
  elif [[ ! -f $1 ]];then
    echo "`$log_date` $FUNCNAME Error: \$1 Invalid File"|TEE
    return 1
  elif [[ ! -d $2 ]];then
    echo "`$log_date` $FUNCNAME Error: \$2 Invalid Directory"|TEE
    return 1
  elif [[ -z $3 ]];then
    echo "`$log_date` $FUNCNAME Error: \$3 is Empty"|TEE
    return 1
  fi

  local list_sum=`cat $1|$wc_cmd -l`
  if [[ $list_sum -ne 2 ]];then
    echo "`$log_date` $FUNCNAME  $1 is invalid pidfile"|TEE
    return 2
  fi

  local local_file=`sed -n "1p" $1`
  local local_size=`$bn_cmd $1|awk -F_ '{print $4}'`
  local hdfs_file=`echo $local_file|sed "s@$2@$3@1"`
  local hdfs_dir=`/usr/bin/dirname $hdfs_file`

  local valid_time=`$bn_cmd $1|awk -F_ '{print $NF}'`
  local filesize=`/usr/bin/du -sh $local_file|awk '{print $1}'`

  HDFS_LOCATION_CHECK $hdfs_file $local_size $hdfs_dir ; hlc_rev=$?
  local nowtime=`$timestamp`
  local costtime=`/usr/bin/expr $nowtime - $now_timestamp`
  case $hlc_rev in
    0)
      ONLY_UPLOAD $local_file $hdfs_file $local_size $valid_time $filesize
      if [[ $? -ne 0 ]] ;then
        sed -n "1p" $1 >> $put_retry_list
        local nowtime=`$timestamp` ; local costtime=`/usr/bin/expr $nowtime - $now_timestamp`
        echo "`$log_date` ONLY_UPLOAD Upload Failed $filesize $costtime $valid_time" >> $log_file
      fi
      ;;
    1)
      echo "`$log_date` $FUNCNAME $hdfs_file Upload Success $filesize $costtime $valid_time (check size)" >> $log_file ;;
    2)
      sed -n "1p" $1 >> $put_retry_list
      echo "`$log_date` HDFS_LOCATION_CHECK Upload Failed: \$# != 2 $filesize $costtime $valid_time" >> $log_file ;;
    3)
      sed -n "1p" $1 >> $put_retry_list
      echo "`$log_date` HDFS_LOCATION_CHECK Upload Failed: Can't create directory -> $hdfs_dir $filesize $costtime $valid_time" >> $log_file ;;
    4)
      sed -n "1p" $1 >> $put_retry_list
      echo "`$log_date` HDFS_LOCATION_CHECK Upload Failed: Can't chmod 777 $hdfs_dir on the hdfs $filesize $costtime $valid_time" >> $log_file ;;
  esac
  rm -rf $1
}

PUT_TO_HDFS $1 $2 $3



    如果覺得代碼複製麻煩,附件中提供了代碼文件...轉載請註明出處!謝謝!

    我擦!!多上傳的附件,後續編輯時不能刪?還是我沒找到....

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章