問題背景
大一點的公司都會建立一套規章流程來避免低級錯誤,例如合入代碼前必需經過同行評審;上線前必需提測且通過 QA 驗證;全量前必需經過 1%、5%、10%、20%、50% 的灰階過程。尤其是最後一步,需要嚴密的監控發版名額來保證新版本的品質,如果與主力版本的名額相比有異常變動,就需要及時停止放量并分析原因。
一個版本的重點觀察名額,除崩潰率外有小 20 項,分布在系統的 10 多個頁面,且每個名額均需要指定多達 6-10 個過濾條件,最常用的包括版本号、端類型 (PC/ Mac/Android/iOS/…)、使用者類型 (user/vip/svip),此外還有一些複雜的下拉清單選項,每次都記不住,需要參考文檔才能确定選對了 😓;另外像版本号這種選項,系統需要很長時間才能刷出來全部版本清單,有時等了很長時間也出不來,還得手動刷一下才能好;最後,有一些名額系統裡沒有直接給出,需要綜合多個名額資料進行計算,例如版本流量占比是由版本流量除以總流量得出的,類似的還有播放流量占比;另外還有一些通用的計算,例如速度的機關是 B/s,實際上使用 MB/s 更貼切,人工記錄資料時,一般直接除以 1000 來進行簡單估算,與除以 1024 相比還是有比較大誤判的。走一遍完整流程下來,快了也得半小時,慢了一上午就過去了。
解決方案
凡是重複性的勞動都有優化空間,凡是收集資料的工作都能用腳本完成——本着這兩個原則,嘗試做一個自動擷取發版名額資料的 shell 腳本。之前有使用 curl 通路 restful api 的經驗 (用 shell 腳本做 restful api 接口監控),這次通路 web 伺服器原理也是一樣的,通過浏覽器的頁面調試功能,可以檢視到一次請求的詳細資訊:
主要使用的是 http post 資料,資料基于 json 格式傳回:
不同請求傳回的 json 格式不同,不過都可以使用 jq 指令處理。
拉取資料
用 curl 嘗試一下:
curl -s "http://iyuntu.xxxxx.com/xxxxxx/api/xxxxxxxxxxxxx/" -H "Accept: */*" -H "Connection: keep-alive"
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Accept-Encoding: gzip, deflate"
-d "start=1642137792&end=1642310592&method=p2pflow&version=3.0.0.112&vipLevel=all&clusterItem=cluster_hour&clientType=pc"
複制
送出的表單資料與 web 請求完全一緻,然而得到了伺服器錯誤:
{"error_code":1006,"message":"userinfo is wrong.","data":[]}
複制
提示使用者資訊錯誤,難道是因為沒有攜帶登入資訊?再看一下浏覽器中請求的 cookie 資訊:
确實不少,将整個 cookie 攜帶到 curl 的請求中:
curl -s "http://iyuntu.baidu.com/clientive" -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Accept-Encoding: gzip, deflate"
-d "start=1642137792&end=1642310592&method=p2pflow&version=3.0.0.112&vipLevel=all&clusterItem=cluster_hour&clientType=pc"
--cookie 'XXXXXXX=6955BFF6EBA75EA12FB35312F4B67309:FG=1; UUAP_TRACE_TOKEN=00253a0352ac05ddf2abb3867e1383dc; Hm_lvt_8d2a248ae863804cbd8d4f34ef769db3=1641283185,1641283813,1641780693,1642304682; jsdk-uuid=0c8f1b25-11a8-4b29-9bde-8203c9d92ba6; RT="z=1&dm=baidu.com&si=h7sa38uz0c7&ss=kycewdz5&sl=0&tt=0&bcn=https%3A%2F%2Ffclog.xxxxx.com%2Flog%2Fweirwood%3Ftype%3Dperf&ld=itd&cl=hr8&ul=kzlat&hd=kzlf2"; XXX_X_XXXXX=BppxvwS4efrHrfU1N5YBV52pvnablZcVWysHnik+JuWM8I/Ujn+rS8e2vD2ig3MkYKVYXq326XyE8GeQThgT7g==; XXXXX=GkyaW9RbklUY0VLRWpac3hMUlRsNjh5M25PTlNiZWxGRVdvT1pnWDE3ZXVuUHRoRVFBQUFBJCQAAAAAAAAAAAEAAABjkC9mY2F2ZXBhcGVybWFuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK4P1GGuD9RhS; UUAP_P_TOKEN=PT-685223097977524224-iAPxtFmvd3-uuap; jsdk-user=d6zmte6yU7ahGWlxTQZghw==; PHPSESSID=ST-689506677214060545-7KJVo-uuap; Hm_lpvt_8d2a248ae863804cbd8d4f34ef769db3=1642320803'
複制
這次不報錯了,但是也沒有請求到任何結果,檢視 curl 傳回值:
$ echo $?
23
複制
百度了一下,curl 23 錯誤是寫失敗,難道需要重定向到檔案?為上面的指令加入:
--output temp.dat
複制
将結果儲存在 temp.dat 檔案中,這次 curl 正常了,但檢視 temp.dat 卻是一團亂麻:
$ head -n 1 temp.dat
?ٮ?E?E?7?!?b
A,9p?6
???YuN?n ?c?'??n?5????[?????c/?>??_?????????????̥?z_?_??m>~?R?ʥ??gI?=_????G_????Xş??????9k?5????
複制
難道是被壓縮了?使用 gunzip 解壓試試:
$ cat temp.dat | gunzip
{"p2p\u6d41\u91cf":[[1642140000000,28249601382.447],[1642143600000,29701279461.349],[1642147200000,30004732054.571],
[1642150800000,28226621579.753],[1642154400000,27565004131.18],[1642158000000,30050204384.371],[1642161600000,34357590257.653],
[1642165200000,37445146977.384],[1642168800000,37507405282.629],……
複制
确實是,解壓後得到的就是 json 内容了,内容解析暫時放一下,先聚焦一下 cookie 。
使用浏覽器 cookie 可以得到想要的結果,但會對浏覽器形成依賴——每次跑腳本前需要從浏覽器抓一份 cookie 儲存在本地。經過一番探究,發現隻要保留 cookie 中的這一條就能通路:
PHPSESSID=ST-689506677214060545-7KJVo-uuap;
複制
應該是 SSO 登入後的通路憑證。從浏覽器複制一條 cookie 雖然有一點麻煩,但也不是不能接受,相比手工記錄發版名額資料,還是友好不少了嘛~
下面以流量名額為例,串起來上面的一系列指令:
# @param: starttime
# @param: endtime
# @param: version
# @param: clienttype
# @param: cookie
# @param: select-time [option]
function fetch_flow()
{
local starttime="$1"
local endtime="$2"
local version="$3"
local clienttype="$4"
local cookie="$5"
local selecttime=""
if [ $# -gt 5 ]; then
selecttime="$6"
fi
local data="start=${starttime}&end=${endtime}&method=p2pflow&version=${version}&vipLevel=all&clusterItem=cluster_hour&clientType=${clienttype}"
curl -s "http://${HOST}/client/api/xxxxxxxxxxxx/" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Accept-Encoding: gzip, deflate" -H "Origin: ${HOST}" -H "Referer: http://${HOST}/stability/main?typeName2=total_flow&typeName2=cluster_hour&typeName2=${clienttype}&datepicker0=${starttime}&datepicker1=${endtime}" -H "X-Requested-With: XMLHttpRequest" --cookie "$cookie" -d "$data" --output temp.gzip
if [ $? -eq 0 ]; then
echo "request flow ok"
cat temp.gzip | gunzip > temp.txt
# handle data here...
fi
}
複制
做個簡單說明:
- 一些參數是從外部傳入的,詳見參數命名
- 提前拼接好表單資料備用
- curl 發送請求,多了一些 http 頭,主要是參考 web 請求設定的,實測可有可無
- 請求中指定的 cookie 是從外部傳入的,這個參數其實就是從浏覽器存儲到檔案後傳遞進來的
- curl 響應存放在 temp.gzip 檔案中,使用 gunzip 解壓縮到 temp.txt 檔案,後面就可以用 txt 進行資料解析了
實測 cookie 中的 PHPSESSID 失效時間非常短,可能也就半個小時,基本每次執行腳本時都需要重新抓取。
解析資料
有了 json 資料,剩下的就是從中取得關心的部分。從上一節的示例可以看出,Web 接口傳回的資料都是按時間順序排列的,而發版資料隻記錄某一個斷面的名額 (精确到小時) ,一般是選取流量高峰時刻。結合以上兩個需求,首先需要按時間的順序列出總流量的清單,使用者根據這個資訊選取流量高峰,或者選擇某個時刻;然後根據選取的時刻,所有名額資料向這個時刻看齊,保證資料的一緻性。
例如對于上一節擷取到的資料,如何選取總流量呢:
{
"p2p\u6d41\u91cf":Array[48],
"dcdn\u6d41\u91cf":Array[48],
"third_http\u6d41\u91cf":Array[48],
"onecloud\u6d41\u91cf":Array[48],
"relay\u6d41\u91cf":Array[48],
"\u603b\u6d41\u91cf":Array[48],
"cdn\u603b\u6d41\u91cf":Array[48],
"bt\u603b\u6d41\u91cf":Array[48],
"onecloud-http\u603b\u6d41\u91cf":[
[
1642140000000,
84124693465.349
],
[
1642143600000,
90980062017.307
],
[
1642147200000,
86051227112.929
],
...
]
}
複制
整個 json 的結構是這樣的:
- key-value 構成的二值數組 (更像 pair) 是最基本的機關,代表一個時刻的流量值
- pair 組成的數組構成一個次元,代表某一分量随時間變化的曲線,每條曲線的次元由名稱确定
- 多個次元組合成一個最終的 json object
首先要确認擷取哪個次元,json 中的漢字會被轉碼為 utf8,"\u603b\u6d41\u91cf" 代表的就是"總流量"了,在 jq 中可以直接指定漢字:
$ cat temp.txt | jq '."總流量"'
[
[
1642140000000,
1126527342256.8
],
[
1642143600000,
1176541641172.7
],
[
1642147200000,
1177760044237.3
],
...
]
複制
注意這裡漢字必需用引号包裹,否則會報錯:
jq: error: syntax error, unexpected INVALID_CHARACTER (Unix shell quoting issues?) at <top-level>, line 1:
.總流量
jq: error: try .["field"] instead of .field for unusually named fields at <top-level>, line 1:
.總流量
jq: 2 compile errors
複制
接着通過數組符将資料一維化 (去掉最外層數組),友善後續處理:
$ cat temp.txt | jq '."總流量"[]'
[
1642140000000,
1126527342256.8
]
[
1642143600000,
1176541641172.7
]
[
1642147200000,
1177760044237.3
]
...
複制
将 key-value 的二值數組也去掉,這個費了很大周折,不過總算找到了的辦法:
$ cat temp.txt | jq '."總流量"[]|.[0],.[1]'
1642140000000
1126527342256.8
1642143600000
1176541641172.7
1642147200000
1177760044237.3
...
複制
使用了 jq 的内置管道,在數組中挑選要提取的元素下标,關于 jq 文法可參考文末連結。
簡化為這樣的形式,再展示給使用者就友善多了:
# @param data-file
# @param unit
# @param select-time [option]
function pick_time()
{
local file="$1"
local unit="$2"
local selecttime=""
if [ $# -gt 2 ]; then
selecttime="$3"
fi
local n=0
local m=0
local line=""
local stamp=0
local time=()
local value=()
local match=-1
while read line; do
if [ "$(($n%2))" -eq 0 ]; then
# time field at event line
stamp=$(($line/1000))
if [ ${is_macos} -eq 1 ]; then
time[$m]=$(date -j -r "$stamp" "+%Y%m%d%H")
#time[$m]=$(date -j -f "%Y%m%d%H" -r "$stamp")
else
time[$m]=$(date -d "@$stamp" "+%Y%m%d%H")
fi
if [ -z "$selecttime" ]; then
if [ ${is_macos} -eq 1 ]; then
# macos builtin echo does not recognize -n
#echo "$stamp"
/bin/echo -n "$m: ${time[$m]} "
else
echo -n "$m: ${time[$m]} "
fi
else
if [ "$selecttime" = "${time[$m]}" ]; then
# match selected time
picked_time="$selecttime"
match=$m
fi
fi
else
# value field at odd line
value[$m]="$line"
if [ -z "$selecttime" ]; then
if [ "${unit:0:1}" = ' ' ]; then
# unit start with a space means no byte transfer
echo "${value[$m]}${unit}"
else
# append their unit after B/KB/MB/GB
format_bytes "${value[$m]}"
fi
else
if [ $match -eq $m ]; then
# the value is what we want
picked_value="${value[$m]}"
#echo "matched: $picked_value"
fi
fi
m=$(($m+1))
fi
n=$(($n+1))
done < $file
if [ $n -lt 2 ]; then
echo "no data"
picked_time=0
picked_value=0
return 1
fi
if [ -z "$selecttime" ]; then
# only prompt user when:
# 1. no time provided
read -p "pick a time to record (-1 to quit): " n
if [ $n -lt 0 ]; then
exit 1
fi
picked_time="${time[$n]}"
picked_value="${value[$n]}"
echo "pick ${picked_time} ${picked_value}"
elif [ $match -eq -1 ]; then
# only prompt user when:
# 2. provided time but no match
#
# call myself without 2nd parameter to make prompt
echo "given time ${selecttime} not find"
pick_time "$file" "$unit"
else
echo "pick ${picked_time} ${picked_value}"
fi
}
複制
腳本接受的三個參數分别是:
- data-file:上面 json 過濾掉括号後的結果,key、value 交替存放,key 位于奇數行,value 位于偶數行
- unit:展示給使用者的數值機關,如 B/KB/MB/GB ...
- selecttime:使用者選擇的時刻
pick_time 用于三個場景:
- 沒有提供第三個參數 (selecttime) 時,展示整個清單,供使用者選擇;
- 提供了 selecttime 且有資料比對時,傳回比對的資料;
- 提供了 selecttime 但沒有資料比對時,展示整個清單,供使用者重新選擇;
第一次展示時走的是場景一;後面再展示時走的是場景三;場景二一般不會出現,隻有當背景服務傳回的資料集缺失了資料時才會命中,可以起到提醒使用者的效果,避免資料不一緻。對整個腳本做個簡單說明:
- 主體就是一個循環周遊 json 資料源 (去除括号)
- 根據奇偶行将 key 和 value 分别放入 stamp() 和 value() 數組
- 時間戳機關為毫秒,需要轉換到 epoch (整點),再基于 date 指令把 unix time 轉換為 YYYYMMDDHH 的形式,注意 mac 和 linux 上的 date 指令有差異,需要分平台處理
- 沒有給定 selecttime 時,列印轉換為時間字元串的 key,這裡使用 echo -n 來避免換行,因為緊接着要列印 value 部分,注意 mac 和 linux 上的 echo 指令有差異,需要分平台處理 (mac 上的 bultin echo 不識别 -n 參數,需要調用 echo 指令)
- 如果給定了 selecttime,進行對比,若比對則記錄使用者選擇的索引至 match 中,用于稍後的 value 比對
- 處理 value 時也是差不多的邏輯:不給定 selecttime 就輸出 value 的值和機關;給定 selecttime 且目前索引比對 match 值,則記錄 value 至 picked_value,這是一個全局變量,稍後可以讓調用函數引用來擷取結果
- 循環結束後,若未給定 selecttime,要求使用者輸入索引來選擇一個時間,記錄對應的 time 和 value 至 picked_time 與 picked_value
- 若給定 selecttime 但未能比對,再次調用兩參數的自己,來列印全部資料供使用者選擇
- 若給定 selecttime 比對了,列印使用者選擇時間的對應值
一般 value 的機關是位元組,遇到流量這種上 T 級别的資料,直接給展示給使用者一長串資料非常沒有可讀性,通過 format_bytes 可以将位元組按數量自動轉換為合适的機關 (B/KB/MB/GB):
# @brief format size into B/KB/MB/GB according to bytes amount
# @param size
function format_bytes()
{
local size="$1"
echo "${size}" | awk '{ if ($1 <= 1024) { print $1 " B" } else if ($1 <= 1024*1024) { print $1/1024 " KB" } else if ($1 <= 1024*1024*1024) { print $1/1024/1024 " MB"} else { print $1/1024/1024/1024 " GB"} }'
}
複制
這是一個基于 awk 的實作。除了位元組機關,還有一些是百分比,如果對它們也進行轉換就鬧錯誤了,可以在這種機關前給一個空格來避免這種轉換,例如 " %"。
最終的效果可以看下面這段輸出:
$ sh fetch_iyuntu.sh -v '3.0.0.112' -p 'pc'
request flow ok
0: 2022011519 1487.74 GB
1: 2022011520 1563.08 GB
2: 2022011521 1636.64 GB
3: 2022011522 1618.64 GB
4: 2022011523 1443.26 GB
5: 2022011600 1205.57 GB
6: 2022011601 1096.95 GB
7: 2022011602 905.025 GB
8: 2022011603 654.045 GB
9: 2022011604 482.283 GB
10: 2022011605 391.029 GB
11: 2022011606 355.738 GB
12: 2022011607 390.631 GB
13: 2022011608 589.954 GB
14: 2022011609 827.38 GB
15: 2022011610 1050.17 GB
16: 2022011611 1242.52 GB
17: 2022011612 1348.08 GB
18: 2022011613 1405.8 GB
19: 2022011614 1472.75 GB
20: 2022011615 1492.5 GB
pick a time to record (-1 to quit):
複制
觀察清單,發現昨天晚上 21 點是流量高峰,輸入索引 2 來收集對應的名額 (場景一);後面就不需要使用者再選擇了,腳本會自動比對 2022011521 的時刻去選擇其它名額資料 (場景三);如果某個名額的資料清單沒有 2022011521 這個時刻,腳本會自動列出名額的全部時刻供使用者重新選擇 (場景二),一般是由于背景發版資料缺失了 (資料量太大算不過來,偶爾發生),一般輸入 -1 退出腳本重新選擇一個其它時刻再跑一遍。
把上面的都結合在一起,就是一個完整的拉取和解析總流量的過程啦:
# @param: starttime
# @param: endtime
# @param: version
# @param: clienttype
# @param: cookie
# @param: select-time [option]
function fetch_flow()
{
local starttime="$1"
local endtime="$2"
local version="$3"
local clienttype="$4"
local cookie="$5"
local selecttime=""
if [ $# -gt 5 ]; then
selecttime="$6"
fi
local data="start=${starttime}&end=${endtime}&method=p2pflow&version=${version}&vipLevel=all&clusterItem=cluster_hour&clientType=${clienttype}"
curl -s "http://${HOST}/client/api/xxxxxxxxxxxx/" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "Accept-Encoding: gzip, deflate" -H "Origin: ${HOST}" -H "Referer: http://${HOST}/stability/main?typeName2=total_flow&typeName2=cluster_hour&typeName2=${clienttype}&datepicker0=${starttime}&datepicker1=${endtime}" -H "X-Requested-With: XMLHttpRequest" --cookie "$cookie" -d "$data" --output temp.gzip
if [ $? -eq 0 ]; then
echo "request flow ok"
cat temp.gzip | gunzip > temp.txt
# to see if 'null'
if [ $? -eq 0 -a "$(head -c 4 temp.txt)" != "null" ]; then
jq -r '."總流量"[]|.[0],.[1]' temp.txt > data.txt
pick_time "data.txt" "" "$selecttime"
else
echo "null"
fi
else
echo "request flow failed"
fi
}
複制
增加了以下内容:
- 當選擇的時間範圍内沒有任何資料時,temp.txt 将僅包含四個字元:null,這可以通過 head 截取來判斷,沒有資料時直接輸出 null 并跳過這個名額的擷取
- jq 解析"總流量"次元并将資料寫入 data.txt 檔案中
- pick_time 從 data.txt 檔案中擷取資料,由于第一次請求總流量 (version=pc-all) 時 selecttime 還為空,是以它僅展示清單,當它傳回後使用者已經選好了時刻;如果是請求版本流量 (version=3.0.0.112) selecttime 不為空,将直接從 data.txt 中選擇對應時刻的資料并記錄在 picked_value 中,供後面使用
至此,完成了第一個名額從拉取資料、解析内容到擷取名額資料的全過程。
聯接起來
有了第一個,第二、第三……就容易多了,copy & paste 再改改就行了。來看下腳本 main 函數是如何聯接并驅動這一切的:
function main()
{
local version=""
local endtime=$(date +%s)
local starttime=0
local cookiefile="cookie.txt"
local clienttype="android"
#os="${OSTYPE/"darwin"//}"
local OSTYPE=$(uname -s)
local os="${OSTYPE/"Darwin"//}"
if [ "$os" != "$OSTYPE" ]; then
# darwin: macos
is_macos=1
fi
while getopts 'hv:p:s:e:c:' flag; do
case "${flag}" in
v)
version="${OPTARG}"
;;
p)
clienttype="${OPTARG}"
;;
s)
if [ ${is_macos} -eq 1 ]; then
# convert date as some format to stamp only supported on mac
starttime=$(date -j -f "%Y%m%d%H" "${OPTARG}" "+%s")
else
# date on linux only recongnize date part..
# so: 2021090914 = 20210909 + 14 * 3600
starttime=$(date -d "$((${OPTARG}/100))" "+%s")
starttime=$((${starttime}+${OPTARG}%100*3600))
fi
if [ $? -ne 0 ]; then
echo "convert starttime failed"
return 1
fi
;;
e)
if [ ${is_macos} -eq 1 ]; then
# convert date as some format to stamp only supported on mac
endtime=$(date -j -f "%Y%m%d%H" "${OPTARG}" "+%s")
else
# date on linux only recongnize date part..
# so: 2021090914 = 20210909 + 14 * 3600
endtime=$(date -d "$((${OPTARG}/100))" "+%s")
endtime=$((${endtime}+${OPTARG}%100*3600))
fi
if [ $? -ne 0 ]; then
echo "convert endtime failed"
return 1
fi
;;
c)
cookiefile="${OPTARG}"
;;
h|*)
echo "Usage: sh fetch_iyuntu.sh -v version [-h] [-s starttime(YYYYMMDDHH)] [-e endtime(YYYYMMDDHH)] [-c cookiefile] [-p platform]"
return 0
;;
esac
done
if [ -z "$version" ]; then
echo "no version specified"
return 1
fi
echo "query data for platform: ${clienttype}"
if [ $starttime -eq 0 ]; then
starttime=$(($endtime-86400)) # default 1 day ago
fi
echo "starttime: $starttime, endtime: $endtime"
if [ "$endtime" -lt "$starttime" ]; then
echo "invalid start & end time"
return 1
fi
if [ ! -f "${cookiefile}" ]; then
echo "cookie file not find: ${cookiefile}"
return 1
fi
local cookie=$(cat ${cookiefile})
#echo "cookie: $cookie"
# 1. fetch total flow first to determine time
fetch_flow "$starttime" "$endtime" "${clienttype}-all" "${clienttype}" "$cookie"
select_time="$picked_time"
total_flow="$picked_value"
# 2. fetch flow of that version
fetch_flow "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "$select_time"
version_flow="$picked_value"
# 3. fetch slow speed ratio
fetch_slow_speed "$starttime" "$endtime" "$version" "${clienttype}" "$cookie"
slow_speed="$picked_value"
# 4. fetch nat connectivity
fetch_nat_connectivity "$starttime" "$endtime" "$version" "${clienttype}" "$cookie"
nat_connectivity="$picked_value"
# 5. fetch total download speed, 0 - normal user
fetch_total_download_speed "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "$select_time"
total_download_speed_for_normal_user="$picked_value"
# 6. fetch total download speed, 2 - svip user
fetch_total_download_speed "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "$select_time"
total_download_speed_for_svip_user="$picked_value"
# 7. fetch vod result success, 0 - normal user
fetch_vod_result_success "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "$select_time"
vod_result_success_for_normal_user="$picked_value"
# 8. fetch vod result success, 2 - svip user
fetch_vod_result_success "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "$select_time"
vod_result_success_for_svip_user="$picked_value"
# 9. fetch vod p2p share ratio, 0 - normal user
fetch_vod_p2p_share_ratio "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "$select_time"
vod_p2p_share_ratio_for_normal_user="$picked_value"
#10. fetch vod p2p share ratio, 2 - svip user
fetch_vod_p2p_share_ratio "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "$select_time"
vod_p2p_share_ratio_for_svip_user="$picked_value"
if [ "${clienttype}" != "pc" ]; then
#11. fetch ts download speed, 0 - normal user
fetch_ts_download_speed "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "$select_time"
ts_download_speed_for_normal_user="$picked_value"
#12. fetch ts download speed, 2 - svip user
fetch_ts_download_speed "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "$select_time"
ts_download_speed_for_svip_user="$picked_value"
#13. fetch ts result success, 0 - normal user
fetch_ts_result_success "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "$select_time"
ts_result_success_for_normal_user="$picked_value"
#14. fetch ts result success, 2 - svip user
fetch_ts_result_success "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "$select_time"
ts_result_success_for_svip_user="$picked_value"
#15. fetch ts p2p share ratio, 0 - normal user, 3 - downloading
fetch_ts_p2p_share_ratio "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "0" "3" "$select_time"
ts_p2p_share_ratio_for_normal_user="$picked_value"
#16. fetch ts p2p share ratio, 2 - svip user, 3 - downloading
fetch_ts_p2p_share_ratio "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "3" "$select_time"
ts_p2p_share_ratio_for_svip_user="$picked_value"
else
# pc use ts playing share ratio instead of ts downloading
#11. fetch ts p2p share ratio, 2 - svip user, 1 - playing
fetch_ts_p2p_share_ratio "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "2" "1" "$select_time"
ts_p2p_share_ratio_for_svip_user="$picked_value"
fi
#17. fetch ts flow ratio
fetch_ts_play_flow "$starttime" "$endtime" "$version" "${clienttype}" "$cookie" "$select_time"
local ts_play_flow_version="$picked_value"
fetch_ts_play_flow "$starttime" "$endtime" "${clienttype}-all" "${clienttype}" "$cookie" "$select_time"
local ts_play_flow_all="$picked_value"
ts_play_flow_ratio=$(echo "${ts_play_flow_version},${ts_play_flow_all}" | awk -F',' '{print $1*100/$2}')
print_statistic "${clienttype}"
}
複制
做個簡單說明:
- 腳本可以接收 6 個參數
- -v version,指定收集資料名額的版本
- -p platform,指定收集資料的端,預設為 android,可以指定 pc 或其它端
- -e endtime,指定結束時間,格式為 YYYYMMDDHH,精确到小時,如果不指定,預設為目前時間
- -s starttime,指定開始時間,格式同上,如果不指定,預設為結束時間前推 24 小時
- -c cookiefile,指定 cookie 内容,預設為 cookie.txt
- -h,輸出 usage
- 注:mac 上可以直接将字元串 YYYYMMDDHH 轉換為 unix time;linux 上不能,需要兩步,第一步轉換為到日期的時間戳,第二步加上小時數
- 完整性檢查,沒有版本号、cookie 檔案、結束時間小于開始時間等都是緻命錯誤,直接退出
- fetch_flow 擷取總流量,記錄使用者選擇時間 (select_time) 和總流量 (total_flow)
- 分别擷取各個分量
- fetch_flow 擷取版本流量 (version_flow)
- fetch_slow_speed 擷取慢速比 (slow_speed)
- fetch_nat_connectivity 擷取 NAT 連通率 (nat_connectivity)
- fetch_total_download_speed 分别擷取普通 (total_download_speed_for_normal_user) 和 svip 使用者總速度 (total_download_speed_for_svip_user)
- fetch_vod_result_success 分别擷取普通 (vod_result_success_for_normal_user) 和 svip 使用者原畫下載下傳成功率 (vod_result_success_for_svip_user)
- fetch_vod_p2p_share_ratio 分别擷取普通 (vod_p2p_share_ratio_for_normal_user) 和 svip 使用者原畫下載下傳分享率 (vod_p2p_share_ratio_for_svip_user)
- fetch_ts_download_speed 分别擷取普通 (ts_download_speed_for_normal_user) 和 svip 使用者轉碼下載下傳速度 (ts_download_speed_for_svip_user)
- fetch_ts_result_success 分别擷取普通 (ts_result_success_for_normal_user) 和 svip 使用者轉碼下載下傳成功率 (ts_result_success_for_svip_user)
- fetch_ts_p2p_share_ratio 分别擷取普通 (ts_share_ratio_for_normal_user) 和 svip 使用者轉碼下載下傳分享率 (ts_share_ratio_for_svip_user)
- fetch_ts_play_slow 分别擷取版本 (ts_play_flow_version) 和總的轉碼播放流量 (ts_play_flow_all),最後計算出轉碼播放流量占比 (ts_play_flow_ration)
- 注:pc 端轉碼名額隻有 svip 轉碼播放分享率 (ts_share_ratio_for_svip_user)
- 最後列印擷取到的名額資料 (print_statistic)
在每個 fetch_xxx 函數擷取名額資料後都跟着一個指派操作,将 pick_value 放入對應的全局變量中,在最後列印名額資訊時 (print_statistic) 會用到它們:
# @param: clienttype
function print_statistic()
{
local clienttype="$1"
local flow_ratio="0"
local result_success="0"
echo "======================================="
echo "total flow: $(format_bytes ${total_flow})"
echo "version flow: $(format_bytes ${version_flow})"
# eat divided by zero error
flow_ratio=$(echo "${version_flow},${total_flow}" | awk -F',' '{print $1*100/$2}' 2>/dev/null)
echo "flow ratio: ${flow_ratio} %"
echo "slow speed: ${slow_speed} %"
echo "nat connectivity: ${nat_connectivity} %"
echo "total download speed (normal) $(format_bytes ${total_download_speed_for_normal_user})/s"
echo "total download speed (svip) $(format_bytes ${total_download_speed_for_svip_user})/s"
result_success=$(echo "${vod_result_success_for_normal_user}" | awk '{print $1*100}')
echo "vod result success (normal) ${result_success} %"
result_success=$(echo "${vod_result_success_for_svip_user}" | awk '{print $1*100}')
echo "vod result success (svip) ${result_success} %"
echo "vod p2p share ratio (normal) ${vod_p2p_share_ratio_for_normal_user} %"
echo "vod p2p share ratio (svip) ${vod_p2p_share_ratio_for_svip_user} %"
# pc has vod downloading & ts playing & NO ts downloading
if [ "${clienttype}" != "pc" ]; then
echo "ts download speed (normal) $(format_bytes ${ts_download_speed_for_normal_user})/s"
echo "ts download speed (svip) $(format_bytes ${ts_download_speed_for_svip_user})/s"
result_success=$(echo "${ts_result_success_for_normal_user}" | awk '{print $1*100}')
echo "ts result success (normal) ${result_success} %"
result_success=$(echo "${ts_result_success_for_svip_user}" | awk '{print $1*100}')
echo "ts result success (svip) ${result_success} %"
echo "ts p2p share ratio (normal) ${ts_p2p_share_ratio_for_normal_user} %"
echo "ts p2p share ratio (svip) ${ts_p2p_share_ratio_for_svip_user} %"
else
echo "ts play share ratio (svip) ${ts_p2p_share_ratio_for_svip_user} %"
fi
echo "ts play flow ratio ${ts_play_flow_ratio} %"
}
複制
這個函數還負責計算版本流量占比,注意這裡采用了 awk 來進行浮點運算,shell 内建的運算隻支援整型。
最後來看下運作效果吧:
=======================================
total flow: 1636.64 GB
version flow: xxxx.xx GB
flow ratio: xx.xxxx %
slow speed: x.xxxxx %
nat connectivity: xx.xxxx %
total download speed (normal) x.xxxxx MB/s
total download speed (svip) xx.xxxx MB/s
vod result success (normal) xx.xxxx %
vod result success (svip) xx.xxxx %
vod p2p share ratio (normal) xx.xx %
vod p2p share ratio (svip) xx.xx %
ts play share ratio (svip) xx.xx %
ts play flow ratio xx.xxxx %
複制
自從有了這個腳本,填個灰階發版名額就是分分鐘的事兒了,程式員的效率又有提升,節約下的時間又可以愉快的摸魚了~
結語
本文介紹了一種使用 shell 腳本自動擷取發版名額資料的方法,主要有以下幾個關鍵點:
- curl 基于浏覽器 cookie 通路 web 伺服器擷取名額資料
- jq 解析複雜 json 格式資料
- pick_time 從 key-value 清單中提取某個時刻的名額值
其中第二點又是關鍵中的關鍵,之前也用 jq 做過 json 資料的解析,但處理這樣複雜的 json 形式還是頭一遭,當時差點就在這裡卡殼了,對 jq 文法還需要系統的學習一下,不然遇到更複雜的資料形式,可能又要卡殼 😂。
說一下工具與效率的問題,在比較強調流程的公司幹活,不斷在工作中積累一些工具、腳本是非常必要的,不然随着工作量的加碼,個人精力會被消耗在日常重複工作中,導緻效率降低。同樣一件事,剛入職時花一個小時搞定,入職幾年了還需要差不多的時間來搞定,絕對需要考慮下有沒有優化空間。把一些流程化的、可自動化的工作提煉出來用腳本、工具完成,會極大的節約時間、保證準确性并将注意力集中于該集中的地方,這就是所謂的工欲善其事、必先利其器吧。
後記
這個腳本總體上已經很友善了,美中不足的地方是前面提到的擷取浏覽器 cookie,如何自動登入 web 并記錄 cookie?這個我又有一系列探索,後面會寫成一篇單獨的文章分享出來。
參考
[1]. Shell:jq 循環 json 對象, jq 循環 json 數組, jq 用法實踐, jq converts a JSON object to key=value, jq parses one field from an JSON array into bash array
[2]. shell程式設計學習之使用jq對json提取
[3]. linux工具之jq
[4]. mac date指令
[5]. Linux date指令時間戳和時間之間的轉換
[6]. linux shell實作随機數多種方法(date,random,uuid)