自動(dòng)化部署基礎(chǔ)與shell腳本實(shí)現(xiàn)
關(guān)于自動(dòng)化的基礎(chǔ)知識:
1.1:當(dāng)前代碼部署的實(shí)現(xiàn)方式:
運(yùn)維純手工scp到web服務(wù)器
純手工登錄git服務(wù)器執(zhí)行g(shù)it pull或svn服務(wù)器執(zhí)行svn update更新代碼
通過xftp上傳代碼
開發(fā)打壓縮包上傳到服務(wù)器然后解壓
缺點(diǎn):
1.需要運(yùn)維全程參與,占用大量的工作時(shí)間
2.上線時(shí)間比較慢
3.人為造成的失誤較多,管理比較混亂
4.回滾復(fù)雜而且慢,還不及時(shí)
1.2:運(yùn)行環(huán)境規(guī)劃:
開發(fā)環(huán)境:開發(fā)者本地有自己的環(huán)境,然后運(yùn)維需要設(shè)置開發(fā)環(huán)境的公用服務(wù),例如開發(fā)數(shù)據(jù)庫、redis、memcached等
測試環(huán)境:功能測試環(huán)境和性能測試環(huán)境
預(yù)生產(chǎn)環(huán)境:由生產(chǎn)環(huán)境集群中的某一個(gè)節(jié)點(diǎn)擔(dān)任測試,此節(jié)點(diǎn)只做測試不對外提供服務(wù)
生產(chǎn)環(huán)境:直接對外提供服務(wù)的環(huán)境
為什么有預(yù)生產(chǎn)環(huán)境?
可能是生產(chǎn)環(huán)境預(yù)測試環(huán)境的數(shù)據(jù)庫或數(shù)據(jù)庫版本不一樣導(dǎo)致語句出現(xiàn)問題
或者是生產(chǎn)環(huán)境調(diào)用的接口不一樣,例如支付接口在測試環(huán)境無法調(diào)用
1.3:設(shè)計(jì)一套生產(chǎn)環(huán)境的代碼自動(dòng)化部署系統(tǒng):
開發(fā)環(huán)境 --> 功能測試/性能測試 --> 預(yù)生產(chǎn)環(huán)境 --> 生產(chǎn)環(huán)境
1.4:總體規(guī)劃流程:
一個(gè)服務(wù)的集群節(jié)點(diǎn)數(shù)量,是一次部署還是分次部署
一鍵回滾到上個(gè)版本
一鍵回滾到任意版本
代碼保存在SVN還是Git
獲取指定分支或master的指定版本號的代碼,svn指定版本號,git指定tag標(biāo)簽,或直接拉取某個(gè)分支
配置文件差異化,即測試環(huán)境和生產(chǎn)環(huán)境的配置文件不一樣,如IP不一樣或主機(jī)名不一樣或數(shù)據(jù)庫連接不一樣等等
代碼倉庫和實(shí)際的差異,即配置文件是否放在代碼倉庫中,如果保存在git則所有人都可以從配置文件看到數(shù)據(jù)庫用戶密碼等信息,可以使用單獨(dú)分支保存配置文件,或配置文件只在部署服務(wù)器的某個(gè)項(xiàng)目的目錄,比如是config.example
如何更新代碼,java tomcat需要重啟
測試部署后的web頁面是否可以正常訪問是否是想要的頁面
并行(saltstack)或并行(shell)的問題,涉及到分組部署重啟服務(wù)
如何執(zhí)行,shell執(zhí)行還是web執(zhí)行
1.5:總體規(guī)劃如下:
獲取代碼(git pull拉取) --> 是否編譯(java需要編譯) --> 配置文件(統(tǒng)一和差異) --> 打包 --> scp到目標(biāo)服務(wù)器(或者用saltstack) --> 將服務(wù)器移除集群 --> 解壓代碼包 --> 放置到目標(biāo)目錄(如webroot) --> scp差異文件 --> 重啟服務(wù)(可選) --> 測試服務(wù)(訪問web或者post請求) --> 將節(jié)點(diǎn)重新加入集群
二:實(shí)現(xiàn)代碼自動(dòng)化部署
2.1:通過shell腳本實(shí)現(xiàn),shell腳本規(guī)劃如下:
2.1.1:各web服務(wù)器添加一個(gè)uid相同的普通用戶,而且所有的web服務(wù)都以此普通用戶啟動(dòng),默認(rèn)情況下所有的wenb服務(wù)除了負(fù)載均衡之外都不能監(jiān)聽80端口,比如可以監(jiān)聽8008端口
2.1.2:部署服務(wù)器的用戶登錄其他服務(wù)器免密碼登錄,因此需要做秘鑰認(rèn)證,在各主機(jī)執(zhí)行以下命令:
# useradd www -u 1010
# su – www
$ ssh-keygen
#將部署機(jī)www用戶的公鑰復(fù)制到各web服務(wù)器的 /home/www/.ssh/authorized_keys或執(zhí)行ssh-copy-id www@192.168.3.13
$ chmod 600 /home/www/.ssh/authorized_keys
2.1.3:測試部署服務(wù)器是否可以免秘鑰用www用戶登錄各個(gè)web服務(wù)器
2.2:編寫shell腳本:
2.2.1:完成框架編寫:
#!/bin/
bash#shell env
SHELL_NAME=
"deploy.sh"
SHELL_DIR=
"/home/www/" # 腳本路徑
SHELL_LOG=
"${SHELL_DIR}/${SHELL_NAME}.log" # 腳本執(zhí)行日志# code env 代碼變量
CODE_DIR=
"/deploy/code/deploy" # 代碼目錄
CONFIG_DIR=
"/deploy/config" # 配置文件目錄
TMP_DIR=
"/deploy/tmp" # 臨時(shí)目錄
TAR_DIR=
"/deploy/tar" # 打包目錄
LOCK_FILE=
"/tmp/deploy.lock" # 鎖文件標(biāo)示# 使用幫助函數(shù)
usage(){ echo $
"Usage: $0 [ deploy | rollback ]"
}shell_lock(){touch ${LOCK_FILE}
}shell_unlock(){rm -
f ${LOCK_FILE}
}code_get(){echo "code_get"sleep 60
}code_build(){echo "code_build"
}code_config(){echo "code_config"
}code_tar(){echo "code_tar"
}code_scp(){echo "code_scp"
}cluster_node_remove(){echo "cluster_node_remove"
}code_deploy(){echo "code_deploy"
}config_diff(){echo "config_diff"
}code_test(){echo "code_test"
}cluster_node_in(){echo "cluste_node_in"
}rollback(){echo "rollback"
}# 主函數(shù)
main(){ if [ -f $LOCK_FILE ];
then # 先判斷鎖文件在不在echo "Deploy is running" && exit
10; # 如果有鎖文件直接退出fi DEPLOY_METHOD=$
1 # 避免出錯(cuò)誤將腳本的第一個(gè)參數(shù)作為變量case $DEPLOY_METHOD
indeploy) # 如果第一個(gè)參數(shù)是deploy就執(zhí)行以下操作shell_lock; # 執(zhí)行部署之前創(chuàng)建鎖,如果同時(shí)有其他人執(zhí)行則提示鎖文件存在避免沖突code_get;code_build;code_config;code_tar;code_scp;cluster_node_remove;code_deploy;config_diff;code_test;cluster_node_in;shell_unlock;;;rollback) # 如果第一個(gè)參數(shù)是rollback就執(zhí)行以下操作shell_lock; # 回滾之前也是先創(chuàng)建鎖文件rollback; # 執(zhí)行完成刪除鎖文件shell_unlock;;;*
) # 其他輸入執(zhí)行以下操作usage;esac
}
# 執(zhí)行主函數(shù)并把第一個(gè)變量當(dāng)參數(shù)
main $1 ?
2.2.2:完成腳本:實(shí)現(xiàn)代碼部署、測試、回滾等操作:
代碼回滾設(shè)計(jì):
正常回滾是回滾已經(jīng)在web服務(wù)器部署過的版本,因此就不需要獲取代碼打包和部署的過程了
列出回滾版本
將模板服務(wù)器移除集群
執(zhí)行回滾
重啟和測試
將模板服務(wù)器加入集群
#!/bin/
bash#Dir List 部署節(jié)點(diǎn)(即部署節(jié)點(diǎn)需要做的操作)
# mkdir -p /deploy/code/web-
demo
# mkdir -p /deploy/config/web-demo/
base
# mkdir -p /deploy/config/web-demo/
other
# mkdir /deploy/
tmp
# mkdir /deploy/
tar# chown -R www.www /
deploy
# chown -R www.www /
webroot
# chown -R www.www /opt/webroot/
# chown -R www.www /
webroot# 需要在客戶端節(jié)點(diǎn)做的操作
# mkdir /opt/
webroot
# mkdir /
webroot
# chown -R www.www /
webroot
# chown -R www.www /opt/webroot/
# chown -R www.www /
webroot
# [www@ ~]$
touch /webroot/web-
dem# Node List 服務(wù)器節(jié)點(diǎn)
PRE_LIST=
"192.168.3.12" # 預(yù)生產(chǎn)節(jié)點(diǎn)
GROUP1_LIST=
"192.168.3.12 192.168.3.13"
GROUP2_LIST=
"192.168.3.13"
ROLLBACK_LIST=
"192.168.3.12 192.168.3.13"# 日志日期和時(shí)間變量
LOG_DATE=
'date "+%Y-%m-%d"' # 如果執(zhí)行的話后面執(zhí)行的時(shí)間,此時(shí)間是不固定的,這是記錄日志使用的時(shí)間
LOG_TIME=
'date "+%H-%M-%S"'# 代碼打包時(shí)間變量
CDATE=$(
date "+%Y-%m-%d") # 腳本一旦執(zhí)行就會(huì)取一個(gè)固定時(shí)間賦值給變量,此時(shí)間是固定的
CTIME=$(
date +
"%H-%M-%S")# shell env 腳本位置等變量
SHELL_NAME=
"deploy.sh" # 腳本名稱
SHELL_DIR=
"/home/www/" # 腳本路徑
SHELL_LOG=
"${SHELL_DIR}/${SHELL_NAME}.log" # 腳本執(zhí)行日志文件路徑# code env 代碼變量
PRO_NAME=
"web-demo" # 項(xiàng)目名稱的函數(shù)
CODE_DIR=
"/deploy/code/web-demo" # 從版本管理系統(tǒng)更新的代碼目錄
CONFIG_DIR=
"/deploy/config/web-demo" # 保存不同項(xiàng)目的配置文件,一個(gè)目錄里面就是一個(gè)項(xiàng)目的一個(gè)配置文件或多個(gè)配置文件
TMP_DIR=
"/deploy/tmp" # 臨時(shí)目錄
TAR_DIR=
"/deploy/tar" # 打包目錄
LOCK_FILE=
"/tmp/deploy.lock" # 鎖文件路徑usage(){ # 使用幫助函數(shù)echo $
"Usage: $0 [ deploy | rollback [ list | version ]"
}writelog(){ # 寫入日志的函數(shù)LOGINFO=$
1 # 將參數(shù)作為日志輸入echo "${CDATE} ${CTIME} : ${SEHLL_NAME} : ${LOGINFO}" >>
${SHELL_LOG}
}# 鎖函數(shù)
shell_lock(){touch ${LOCK_FILE}
}# 解鎖函數(shù)
shell_unlock(){rm -
f ${LOCK_FILE}
}# 獲取代碼的函數(shù)
code_get(){echo "code_get"writelog code_getcd $CODE_DIR &&
echo "git pull" # 進(jìn)入到代碼目錄更新代碼,此處必須免密碼更新,此目錄僅用于代碼更新不能放其他任何文件cp -rf ${CODE_DIR} ${TMP_DIR}/ # 臨時(shí)保存代碼并重命名,包名為時(shí)間+
版本號,準(zhǔn)備復(fù)制到web服務(wù)器API_VER=
"123" # 版本號
}code_build(){ # 代碼編譯函數(shù)echo code_build
}code_config(){ # 配置文件函數(shù)writelog code_config/bin/
cp -rf ${CONFIG_DIR}/base
/* ${TMP_DIR}/"${PRO_NAME}" # 將配置文件放在本機(jī)保存配置文件的臨時(shí)目錄,用于暫時(shí)保存代碼項(xiàng)目PKG_NAME="${PRO_NAME}"_"$API_VER"_"${CDATE}-${CTIME}" # 定義代碼目錄名稱cd ${TMP_DIR} && mv ${PRO_NAME} ${PKG_NAME} # 重命名代碼文件為web-demo_123-20170629-11-19-10格式}code_tar(){ # 對代碼打包函數(shù)writelog code_tarcd ${TMP_DIR} && tar czf ${PKG_NAME}.tar.gz ${PKG_NAME}writelog "${PKG_NAME}.tar.gz"
}code_scp(){ # 代碼壓縮包scp到客戶端的函數(shù)writelog "code_scp"for node in $PRE_LIST;do # 循環(huán)服務(wù)器節(jié)點(diǎn)列表scp ${TMP_DIR}/${PKG_NAME}.tar.gz $node:/opt/webroot/ # 將壓縮后的代碼包復(fù)制到web服務(wù)器的/opt/webrootdonefor node in $GROUP1_LIST;do # 循環(huán)服務(wù)器節(jié)點(diǎn)列表scp ${TMP_DIR}/${PKG_NAME}.tar.gz $node:/opt/webroot/ # 將壓縮后的代碼包復(fù)制到web服務(wù)器的/opt/webrootdone
}url_test(){URL=$1curl -s --head $URL |grep '200 OK'if [ $? -ne 0 ];thenshell_unlock;writelog "test error" && exit;fi
}cluster_node_add(){ #將web服務(wù)器添加至前端負(fù)載echo cluster_node_add
}cluster_node_remove(){ # 將web服務(wù)器從集群移除函數(shù)(正在部署的時(shí)候應(yīng)該不處理業(yè)務(wù))writelog "cluster_node_remove"
}pre_deploy(){writelog "pre_deploy"for node in ${PRE_LIST};do # 循環(huán)預(yù)生產(chǎn)服務(wù)器節(jié)點(diǎn)列表cluster_node_remove ${node} # 部署之前將節(jié)點(diǎn)從前端負(fù)載刪除echo "pre_deploy, cluster_node_remove ${node}"ssh ${node} "cd /opt/webroot && tar zxf ${PKG_NAME}.tar.gz" #分別到web服務(wù)器執(zhí)行壓縮包解壓命令ssh ${node} "rm -f /webroot/web-demo && ln -s /opt/webroot/${PKG_NAME} /webroot/web-demo" # 整個(gè)自動(dòng)化的核心,創(chuàng)建軟連接done
}pre_test(){ # 預(yù)生產(chǎn)主機(jī)測試函數(shù)for node in ${PRE_LIST};do # 循環(huán)預(yù)生產(chǎn)主機(jī)列表curl -s --head http://${node}:9999/index.html | grep "200 OK" # 測試web界面訪問if [ $? -eq 0 ];then # 如果訪問成功writelog " ${node} Web Test OK!" # 記錄日志echo " ${node} Web Test OK!"cluster_node_add ${node} # 測試成功之后調(diào)用添加函數(shù)把服務(wù)器添加至節(jié)點(diǎn),writelog "pre,${node} add to cluster OK!" # 記錄添加服務(wù)器到集群的日志else # 如果訪問失敗writelog "${node} test no OK" # 記錄日志echo "${node} test not OK"shell_unlock # 調(diào)用刪除鎖文件函數(shù)break # 結(jié)束部署fidone}group1_deploy(){ # 代碼解壓部署函數(shù)writelog "group1_code_deploy"for node in ${GROUP1_LIST};do # 循環(huán)生產(chǎn)服務(wù)器節(jié)點(diǎn)列表cluster_node_remove $node echo "group1, cluster_node_remove $node"ssh ${node} "cd /opt/webroot && tar zxf ${PKG_NAME}.tar.gz" # 分別到各web服務(wù)器節(jié)點(diǎn)執(zhí)行壓縮包解壓命令ssh ${node} "rm -f /webroot/web-demo && ln -s /opt/webroot/${PKG_NAME} /webroot/web-demo" # 整個(gè)自動(dòng)化的核心,創(chuàng)建軟連接donescp ${CONFIG_DIR}/other/192.168.3.13.server.xml 192.168.3.13:/webroot/web-demo/server.xml # 將差異項(xiàng)目的配置文件scp到此web服務(wù)器并以項(xiàng)目結(jié)尾
} group1_test(){ # 生產(chǎn)主機(jī)測試函數(shù)for node in ${PRE_LIST};do # 循環(huán)生產(chǎn)主機(jī)列表curl -s --head http://${node}:9999/index.html | grep "200 OK" #測試web界面訪問if [ $? -eq 0 ];then #如果訪問成功writelog " ${node} Web Test OK!" #記錄日志echo "group1_test,${node} Web Test OK!"cluster_node_addwritelog " ${node} add to cluster OK!" #記錄將服務(wù)器 添加至集群的日志else #如果訪問失敗writelog "${node} test no OK" #記錄日志echo "${node} test no OK"shell_unlock # 調(diào)用刪除鎖文件函數(shù)break # 結(jié)束部署fidone
}rollback_fun(){ for node in $ROLLBACK_LIST;do # 循環(huán)服務(wù)器節(jié)點(diǎn)列表# 注意一定要加"號,否則無法在遠(yuǎn)程執(zhí)行命令ssh $node "rm -f /webroot/web-demo && ln -s /opt/webroot/$1 /webroot/web-demo" # 立即回滾到指定的版本,$1即指定的版本參數(shù)echo "${node} rollback success!"done
}rollback(){ # 代碼回滾主函數(shù)if [ -z $1 ];thenshell_unlock # 刪除鎖文件echo "Please input rollback version" && exit 3;ficase $1 in # 把第二個(gè)參數(shù)做當(dāng)自己的第一個(gè)參數(shù) list)ls -l /opt/webroot/*.tar.gz;;*)rollback_fun $1esac}main(){if [ -f $LOCK_FILE ] ;then # 先判斷鎖文件在不在,如果有鎖文件直接退出echo "Deploy is running" && exit 10fiDEPLOY_METHOD=$1 # 避免出錯(cuò)誤將腳本的第一個(gè)參數(shù)作為變量ROLLBACK_VER=$2case $DEPLOY_METHOD indeploy) # 如果第一個(gè)參數(shù)是deploy就執(zhí)行以下操作shell_lock; # 執(zhí)行部署之前創(chuàng)建鎖。如果同時(shí)有其他人執(zhí)行則提示鎖文件存在code_get; # 獲取代碼code_build; # 如果要編譯執(zhí)行編譯函數(shù)code_config; # cp配置文件code_tar; # 打包c(diǎn)ode_scp; # scp到服務(wù)器pre_deploy; # 預(yù)生產(chǎn)環(huán)境部署pre_test; # 預(yù)生產(chǎn)環(huán)境測試group1_deploy; # 生產(chǎn)環(huán)境部署group1_test; # 生產(chǎn)環(huán)境測試shell_unlock; # 執(zhí)行完成后刪除鎖文件;;rollback) # 如果第一個(gè)參數(shù)是rollback就執(zhí)行以下操作shell_lock; # 回滾之前也是先創(chuàng)建鎖文件rollback $ROLLBACK_VER;shell_unlock; # 執(zhí)行完成刪除鎖文件;;*)usage;esac
}
main $1 $2 ?
3.通過剛才編寫的shell腳本將html官網(wǎng)頁面部署到nginx中
①將代碼上傳到部署節(jié)點(diǎn)的/deploy/code/web-demo目錄中
[www@master web-demo]$
pwd
/deploy/code/web-
demo
[www@master web-
demo]$ ll
total 20
drwxr-xr-x
6 www www
4096 Jun
6 13:
46 assets
-rw-r--r--
1 www www
1150 Jun
6 17:
59 favicon.ico
drwxr-xr-x
2 www www
4096 Jun
6 15:
32 images
-rw-r--r--
1 www www
4323 Jun
6 16:
19 index.html
?
部署節(jié)點(diǎn)執(zhí)行以下操作:
#
mkdir -p /deploy/code/web-
demo
# mkdir -p /deploy/config/web-demo/
base
# mkdir -p /deploy/config/web-demo/
other
# mkdir /deploy/
tmp
# mkdir /deploy/
tar# chown -R www.www /
deploy
# chown -R www.www /
webroot
# chown -R www.www /opt/webroot/
?
②需要在客戶端做的操作
# 安裝nginx
# yum install -y nginx
編輯nginx
vim /etc/nginx/conf.d/
cloudeye.confserver {listen 9999;server_name 192.168.
3.13; # 實(shí)際生產(chǎn)環(huán)境中需要填寫域名location /
{alias /webroot/web-demo/; # 這個(gè)web-demo目錄不需要?jiǎng)?chuàng)建,有軟鏈接指向/webroot/web-
demo目錄index index.html;}
} ?
創(chuàng)建相關(guān)目錄并修改權(quán)限
mkdir /opt/
webroot
mkdir /
webroot
chown -R www.www /
webroot
chown -R www.www /opt/webroot/
[www@ ~]$
touch /webroot/web-demo
?
③執(zhí)行腳本
測試部署:
[www@master ~]$ ./deploy.
sh deploy
code_get
git pull
code_build
web-demo_123_2017-
06-
26-
12-
18-
09.
tar.gz
100% 1214KB
1.2MB/s
00:
00
web-demo_123_2017-
06-
26-
12-
18-
09.
tar.gz
100% 1214KB
1.2MB/s
00:
00
web-demo_123_2017-
06-
26-
12-
18-
09.
tar.gz
100% 1214KB
1.2MB/s
00:
00
pre_deploy, cluster_node_remove 192.168.
3.12
HTTP/
1.1 200 OK
192.168.
3.12 Web Test OK!
cluster_node_add
group1, cluster_node_remove 192.168.
3.12
group1, cluster_node_remove 192.168.
3.13
/deploy/config/web-demo/other/
192.168.
3.13.server.xml: No such
file or directory
HTTP/
1.1 200 OK
group1_test,192.168.
3.12 Web Test OK!
cluster_node_add ?
訪問客戶端,可以看到能夠正常訪問,說明部署成功
http://192.168.3.13:9999
修改代碼,測試回滾效果
[www@master ~]$ vim /deploy/code/web-demo/
index.html
[www@master ~]$ ./deploy.
sh deploy
code_get
git pull
code_build
web-demo_123_2017-
06-
26-
12-
18-
57.
tar.gz
100% 1214KB
1.2MB/s
00:
00
web-demo_123_2017-
06-
26-
12-
18-
57.
tar.gz
100% 1214KB
1.2MB/s
00:
00
web-demo_123_2017-
06-
26-
12-
18-
57.
tar.gz
100% 1214KB
1.2MB/s
00:
00
pre_deploy, cluster_node_remove 192.168.
3.12
HTTP/
1.1 200 OK
192.168.
3.12 Web Test OK!
cluster_node_add
group1, cluster_node_remove 192.168.
3.12
group1, cluster_node_remove 192.168.
3.13
/deploy/config/web-demo/other/
192.168.
3.13.server.xml: No such
file or directory
HTTP/
1.1 200 OK
group1_test,192.168.
3.12 Web Test OK!
cluster_node_add[www@master ~]$ ./deploy.
sh rollback list
-rw-rw-r--
1 www www
1243347 Jun
26 11:
36 /opt/webroot/web-demo_123_2017-
06-
26-
11-
36-
44.
tar.gz
-rw-rw-r--
1 www www
1243347 Jun
26 11:
39 /opt/webroot/web-demo_123_2017-
06-
26-
11-
39-
02.
tar.gz
-rw-rw-r--
1 www www
1243351 Jun
26 12:
04 /opt/webroot/web-demo_123_2017-
06-
26-
12-
04-
19.
tar.gz
-rw-rw-r--
1 www www
1243347 Jun
26 12:
16 /opt/webroot/web-demo_123_2017-
06-
26-
12-
16-
49.
tar.gz
-rw-rw-r--
1 www www
1243347 Jun
26 12:
18 /opt/webroot/web-demo_123_2017-
06-
26-
12-
18-
09.
tar.gz
-rw-rw-r--
1 www www
1243369 Jun
26 12:
18 /opt/webroot/web-demo_123_2017-
06-
26-
12-
18-
57.
tar.gz
?修改部署成功頁面
測試回滾,頁面再次回到修改前,說明回滾成功
[www@master ~]$ ./deploy.
sh rollback web-demo_123_2017-
06-
26-
12-
18-
09
192.168.
3.12 rollback success!
192.168.
3.13 rollback success!
?
轉(zhuǎn)載于:https://www.cnblogs.com/reblue520/p/7110213.html
總結(jié)
以上是生活随笔為你收集整理的运维与自动化系列③自动化部署基础与shell脚本实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。