# 第 3 章 向世界發布你的代碼
在前面一章,我們已經在本地部署好LNMP開發環境,并且也完成了我們第一個小網站的開發。這一章,我們將會進行激動人心的操作——向世界發布我們的代碼。我們先來看下為了完成正常發布需要做哪些工作,然后學習完整的生產環境有哪些注意事項,最后我將分享哪些糟糕的實踐會阻礙我們追求完美的項目。
## 3.1 從零到一搭建發布系統
互聯網大型公司是如何做到每天發布成千上萬次卻依然保持有條不紊的?創業團隊又該如何實現敏捷發布進行穩定持續的交付?小團隊項目時又該如何發布?更重要的是,針對不同規模的團隊和項目,分別如何搭建相應的發布流程;又應怎樣實現平滑切換,不斷演進發布系統?對于這些問題,我們將會在這一節探索它的解空間。
在開始繼續往下閱讀之前,讀者不妨想一下,當前所在的公司是如何發布的?并且思考一下,它的整個發布流程是如何實現的,存在哪些問題,它的優勢與特點是什么,是否與當前的發展吻合?
### 3.1.1 不推薦的發布方式
俗話說:幸福的人都是一樣的,不幸的人各有各的不幸。同樣地,優秀的發布都是一樣的,糟糕的發布各有各的不同。
但對于一些糟糕的發布,有些人卻持有不同的意見,因為他們正在使用它而不覺得有什么問題,并且他們已經通過這樣發布方式工作了好多年。“存在即合理”,他們這樣想,哪怕是糟糕的發布方式,既然它已經存在了那么多年,就自然有它存在的道理。我為什么還要做出改變呢?
這樣想是無可厚非的,因為他們已經形成了一種發布文化,在他們的公司內形成了一種局部社會認同感。公司其他團隊,團隊內其他成員都是這樣做的,那么他們也會這樣做,以致不會顯得突兀、不合群。持有優秀發布實踐的人很難理解為什么還有那么多人愿意忍受這些糟糕的發布方式,另一方面,“沉醉”于糟糕發布方式的人也難以明白如何切換到優秀發布,甚至為什么要做出改變。
我們先來看下常見糟糕的發布方式有哪些。這些糟糕的做法雖然有一定的價值,但綜合考慮權衡的話,是弊大于利的。特別從長期角度考慮,從順應不確定未來和滿足多變市場的角度來看,更是如此。
#### 隨時發布
對于年輕的軟件開發領域,很多方面都還沒形成公認的行業標準和規范,其中發布流程更是如此。每個公司都有自己的發布方式,每個技術人員又有自己不同的做法。而公司的發布流程在很大程度上決定于最初負責發布人員的實現方式,不同技術人員由于背景不同,經歷的項目經驗不同,其做法各有千秋。但最糟糕的莫過于,這位負責最初發布的同學,恰好又是之前從未接觸過正式商業項目發布的,這時他很可能就會傾向于一種隨意的發布意識。也就是我們接下來要談到的——隨時發布。
隨時發布,可以說是一種不負責任的發布方式。它完全不考慮版本的概念,也沒有所謂的迭代周期,對于線上生產系統的穩定性、健壯性以及項目的質量都置于九宵云外而不顧。
不管什么時候,開發人員代碼一完成后,就同步更新到生產環境。這非常容易造成有問題就改,有故障就查的局面,從而使得技術開發人員疲于奔命,四處“救火”。注意,這里所說的完成,只是技術開發人員的“一廂情愿”,從他們自己的技術開發角度認為是完成了業務功能需求的開發,而沒有經過測試團隊的功能測試和回歸測試,更別說通過測試了。而且這里需要特別闡明的是,對于這時的完成,大部分開發人員(尤其是絕大部分的初級開發人員)更多的定義是他們只完成了快樂路徑的功能開發和自測,而對于失敗路徑、異常路徑、邊緣路徑、整個業務流程則沒有考慮進內,也沒對應的代碼實現。
舉個例子,我們現在有一個共享平臺系統,在上面人們可以通過共享的方式向平臺通過極低的費用租到價格高昂的商品,例如只需要100元就能租用價值5000元的單反數碼相機并使用一周,但需要額外支付一定的押金。一切都運行良好,我們的網站受到了廣大消費者的喜好,滿足了人們短期內對高端消費品的訴求。隨著項目的發展壯大,現在需要接入一家新的第三方支付系統,并且對方允許給消費者一定的支付折扣。負責這塊的技術開發人員很快就完成了功能的開發并發布上線,一切看起來很順利。但過一段時間后,我們發現出問題了。這是因為用戶的押金需要退還,而當第三方支付系統識別到用戶有退費時,不僅會原路還回,還會把之前的折扣也回收!最終導致了消費者在享受支付折扣后,拿到歸還的押金卻少了!因為技術開發人員只考慮到支付這條快樂路徑,而沒考慮到押金退還這一點,使得公司在面臨眾多用戶投訴的同時,也增加了應對這一故障的處理成本。
隨時發布這種方式好嗎?
也許好,因為需求方可以“很快”就看到他們的需求上線了,技術開發人員可以“很快”就看到他們的代碼在生產環境運行了。但是!以此隨時發布、快速上線換來的代價是,發布到生產環境的代碼都是脆弱的,未經全面測試,兼容性極差的代碼。如果說隨時發布在競爭激烈、節奏飛快的當下,相當于在高速公路上換輪胎,那么很容易讓我們無法慢下來思考如何做到更好、更出色。說到底,我們隨時發布的不是優質的代碼,取而代之的是不斷往臃腫的系統再塞進一些熵,再堆積一些代碼異味。慢慢地,越往后,項目越臃腫,越失控,越糟糕。
在軟件開發過程中,我們應當時刻意識到,對于同一件事情,不同的做事方式,所產生的直接影響、間接影響都是不同的。在只有一位技術開發人員時,他可以完全根據自己的喜好和便利性來操作,但一旦形成了團隊、涉及多方溝通以及項目發展到一定規模時,則需要一些約定,以保證可以創造一個容易催生高效團隊協作和溝通的環境。
對于隨時發布,我個人覺得是過于隨意,缺少嚴謹性,所以我是不推薦的。由隨時發布而產生的間接負面影響,要遠大于它的直接正面收獲。看似非常快速的發布方式,實際上卻讓我們不停在原地打轉。所以,我不推薦初學者一開始就鐘情并習慣于這種發布方式,更不推薦高級資深開發同學固守這種發布方式。如果你當前已經是隨時發布,請不要妥協。
#### 通過FTP上傳發布代碼
比隨時發布稍微好一點的是,在時間上,在版本上有一定的規劃,或者說有一定的發布頻率、發布節奏或發布窗口。按照不成文的約定,通常在每周五是不會進行發布的。因為接下來就是周六、周日,如果一旦因新功能發布上線而出現問題,需要找到技術人員、產品人員、測試人員等項目干系人及時響應和處理是非常困難的。除此之外,溝通成本也會變得非常大,平時在工作時間一起緊密溝通的團隊,到了周末就分散在各個城區甚至各個城市,通過遠程方式進行不太順暢的溝通。
那其他工作日期間,應該以什么節奏來發布呢?是每天N次,還是每周M次?這個時間節點可根據項目的情況具體而定。下面我們來看下另一個糟糕的發布方式。
時到今日,我們很遺憾地看到,仍然有不少公司未使用版本控制系統對項目源代碼進行版本控制。由此帶來的間接影響是他們很多時候都是通過FTP方式上傳待發布的項目代碼,然后發布上線。不僅如此,那些即使是使用了類似Git,SVN的團隊,出于歷史遺留原因也在使用FTP方式更新線上環境的代碼。
這種發布方式,最明顯的問題,它是嚴重依賴人工來操作的。它需要人為地來選擇需要更新哪些代碼目錄、哪些代碼文件,重復性的人工操作,都蘊藏著一個魔鬼——低效且容易出錯。此外,還有一個很大的問題是,缺少發布回滾機制。
我只在最初做項目時,而且還只是在大學期間從事校內網站項目開發時才用過FTP方式上傳發布代碼。畢業工作后就再也沒接觸過這種發布方式。所以現在當我聽到還有公司使用FTP來發布代碼,在驚訝的同時感覺到了我們在軟件開發領域的薄弱。這種發布方式太業余了,在倡導快速交付的當下還會大大拖慢我們的發布節奏。
#### 生產環境即開發環境
隨時發布缺乏對發布時間的統一控制,通過FTP發布是缺乏對發布方式實現自動化,但更為糟糕的是,還有一種發布方式是既忽略了發布時間也忽略了發布方式,那就是開發環境與生產環境共用,技術開發人員直接在生產環境修改代碼。生產環境即開發環境,開發環境即生產環境。
這種方式的危害是相當大的,因為它往往也沒有對代碼進行版本控制。假設一不小心刪除了某個源代碼文件,就只能自求多福了。而且,代碼一修改完,馬上生效,這導致了開發調試期間,線上環境的系統功能基本是完全不可用的。因為或多或少會出現一些語法錯誤、運行時異常導致系統異常中斷和退出。而更深遠的影響莫過于通過錯誤的處理方式對數據進行了持久化,對系統產生了臟數據。
不得不承認,這種方式是有好處的。它節省了部署成本,也降低了技術人員的要求。因為不需要區分開發環境和生產環境,只需要部署一套環境就可以了,也不需要再去折騰Git或SVN這類的版本控制系統。技術人員也可以不用學習那么多工具、系統和軟件,直接在生產環境修改代碼,直到調試完畢就可以了。但是,軟件開發是一門專業的工程學科,它需要全面、專業的技藝。如果什么都只圖方便,不考量架構合理性,不研究相關技術和原理,那只是小白的表現。
軟件開發從來都不是一件容易的事,如果你覺得容易,要么你是還沒入門的小白,要么你已經是資深專家級人物。生產環境即開發環境這種做法,雖然節省了發布這一環節,看似高效,實際風險巨大。除了前面說的缺少版本控制,缺少系統穩定性外,它會催生一種野草式的開發模式。迫于壓力,為了讓系統能快速恢復、正常工作,技術人員都是以當前能想到的方式,以最快的速度進行開發和修復,而不管這種做法是臨時的、糟糕的、還是本身就是有問題的。這導致,很多功能性的開發,很多技術決策都是臨時性的、突發性的、未加全面考慮的。軟件開發有點類似建筑行業,需要提前規劃藍圖,有計劃地進行實施,而不是像路邊的野草放而任之,隨心而行。
我曾經也有過使用這種方式的項目開發經驗。當時有一個項目也是開發環境與生產環境共用一套環境,技術人員平時在上面直接修改代碼,改完后立馬生效。當時我在生產環境上修改這個項目系統的代碼時,也是如履薄冰,生怕刪錯源代碼文件。但越擔心什么,就越會發生什么。有一次,我還真的刪錯文件了,Linux的rm命令及其背后的文化讓我無從恢復剛才刪錯的文件 。幸好,另外一個團隊成員在本地剛好有備份,才得以恢復,虛驚一場。另外比較有趣的是,一旦公司的老板需要對外演示此系統,或者有重要商業合作展覽時,我們技術人員就要停止對代碼的修改,這時不時間接阻礙了技術開發的進度。
### 3.1.2 簡單的發布
接下來,我們將開始從零到一搭建發布系統,這是一種更為正式、更為正統的發布方式。可能在具體實施過程中,不同團隊會有不同的具體做法,但在實現方式、關鍵流程和核心概念上是類似的。
對于簡單的項目,以及初級的項目,我們可以進行簡單的發布。這里,只需要三個關鍵的要素就可以了。它們分別是:
+ **開發環境** 即本地開發環境,可以參考前一章,搭建本地LNMP開發環境,讓技術開發人員可以在本地安全地進行開發、調試和自測。
+ **托管倉庫** 不管項目有多小,都應盡量納入源代碼版本控制系統,將項目的源代碼托管到遠程服務器倉庫。即需要在遠程服務器存有項目代碼的最新代碼,便于團隊內部共享,也保障了個人電腦硬盤損壞后代碼仍能幸存于世。
+ **生產環境** 系統最終的運行環境,并開放給最終用戶人群訪問和使用。
不難看出,簡單的發布流程是,先在本地開發環境進行代碼的編寫,然后更新提交到遠程代碼托管倉庫,最后發布到生產環境。

圖3-1 簡單的發布流程
當技術人員在開發環境完成需求開發,進行充分測試后,便可將代碼提交到遠程代碼托管倉庫。這時,開發環境通常都是位于個人開發電腦上,并且在公司局域網內。托管倉庫,可以是公司的內部服務器,也可以使用第三方云服務器。當全部待發布的功能點都經過功能測試和回歸測試后,就可以執行發布操作,將最新版本的代碼發布更新到外網生產環境。這里,在外網生產環境也是類似平時在開發環境上更新代碼一樣,例如Git下的git pull命令,SVN下的更新操作。如果需要回滾,可以通過源代碼控制系統進行特定版本的回滾。
這種做法,在項目初期,對于簡單的項目,對于單臺服務器是適用的。若線上的服務器是多臺,或者是一個集群,又怎樣才能保證代碼在同一時間進行更新,我們又該如何快速、便捷、統一控制呢?下面我們來看下更為專業的發布方式。
### 3.1.3 實現一鍵發布
鑒于我們部署的系統環境是LNMP,故而可以使用一些基本的shell命令快速搭建一鍵發布,實現發布流程化、自動化。這些shell命令主要有打包解壓命令tar、遠程文件傳輸命令scp、遠程登錄和操作命令ssh。
不過在實現一鍵發布前,我們需要進行一些約定,以便能更好地進行整體的把控。主要有以下幾個方面:統一LNMP環境的版本和配置、統一項目路徑、統一系統賬號。
#### 統一LNMP環境的版本和配置
對于Linux、Nginx、MySQL和PHP,不管選擇哪個版本,為生產環境的服務器集群安裝同樣版本的操作系統和軟件是大有裨益的。因為我們可以實現無差異批量化的操作,方便統一管理。
假設選擇的是PHP 7.2.3,那么就全部服務器都安裝PHP 7.2.3,盡量保持版本上的一致性。不要服務器A安裝了PHP 7.2.3,而服務器B則安裝PHP 7.1.15;也不要部分服務器運行的是PHP 7,而另外一部分服務器則運行PHP 5。因為不同版本下的軟件,都會或多或少存在一些差異。這些差異體現在配置、語法、和安裝方式、使用方式上。
除了安裝相同版本的環境外,還要進行相同的配置。假設在PHP的配置文件/etc/php.ini內修改variables_order的配置項為EGPCS,如:
```
variables_order = "EGPCS"
```
那么就應該同步此配置到其他服務器,保證在其他服務器上通過PHP也能正常讀取到環境變量$_ENV。此時,你可以選擇類似puppet這樣的自動化運維工具。
#### 統一項目路徑
上面是對LNMP環境的要求,對于項目運行時的部署也應盡量做到一致。具體包括項目源代碼的部署路徑、日志路徑和計劃任務等。
例如,我們前面的網站項目www.examples.com,假設源代碼的部署路徑為:/home/apps/projects/www.examples.com,那么其他服務器也應安裝在這一目錄下。如果項目的日志存放路徑為:/var/logs/apps/projects/www.examples.com,那么其他服務器也要保持一致。
#### 統一系統賬號
細心的讀者已經留意到,前面在Linux系統上我們主要使用了apps這位用戶來部署系統項目。由此不難推斷,我們同樣建議全部服務器都應使用apps賬號來統一操作。此外,引申到啟動Nginx和MySQL等,也應該為其分配相同的賬號。因為不同的賬號在Linux系統上有不同的權限。保持操作系統賬號的一致性,可以保證權限的一致性,而不會發生在這臺機可以讀取執行,到另外一臺機卻不行。
關于Linux的權限,已經有大量相關書籍。這里不再重復詳細展開,但我們可以通過一個例子稍微回顧一下,溫故而知新。例如對于前面的projects目錄,它的權限如下:
```
drwxrwxr-x. 8 apps apps 4096 Mar 16 23:42 projects
```
則最前面的d表明projects是一個目錄,中間的兩個apps,分別表示屬于apps用戶和表示屬于apps這個用戶組。對于前面的權限,r表示可讀,w表示可寫,x表示可執行,分別對應二進制的4、2、1。也就是說,如果權限是rwx,則對應是7,如果權限是r-x,則對應是5。權限有三組,依次是用戶自己的、相同用戶組和其他用戶組。例如常見的755則表示用戶本人有全部權限,相同用戶組和其他用戶組只能讀取和執行,不能修改。
當LNMP環境、項目路徑和系統賬號都統一約定好后,我們就可以進行下一步了。
為實現一鍵發布,構建完整的發布流程,我們需要引入一些新的要素和設定不同的角色。先來看下新的發布流程。

圖3-2 一鍵發布
相比前面的簡單發布,這里額外引入了兩個要素:
+ **測試環境** 測試環境是用于進行功能測試和回歸測試的環境,主要使用對象是QA測試團隊。和開發環境一樣,通常部署在公司內網,可以安全地進行各種集成測試,甚至可以把整個數據庫清空再重建,以滿足不同場景下的測試要求。
+ **預發布環境** 預發布環境類似仿真環境,和生產環境一樣部署在外網,使用的也是和生產環境同樣的數據庫。稍微不同的是,預發布環境是只開放給公司內部人員使用的,并且使用獨立的緩存機制。也就是說,項目代碼在正式發布上線前,會先灰度發布到預發布進行更貼近真實環境的測試。
簡單做個小總結,我們的項目代碼會先在開發環境進行編寫和調試,自測通過后提交到遠程代碼倉庫。測試團隊會將開發完成的代碼部署到測試環境,進行冒煙測試、功能測試、集成測試和系統測試等。全部驗收通過后,在正式發布前,會灰度發布到預發布環境進行回歸測試。最后,一切準備就緒后,更新發布到生產環境。從開發環境,到測試環境,再到預發布環境,最后到生產環境,就構成了完整的一個發布流程。
在這個過程中,針對不同的階段,針對不同專業領域的要求,我們還需要設定不同的角色,以進行良好的團隊協作。通過集體智慧,完成這一系列的發布操作。前面有說到開發人員、測試人員,此外為了進行發布,可再設立發布人員,專門負責進行發布操作。
下面,我們將重點詳細講解如何構建一鍵發布。一鍵發布主要用于將項目代碼從開發環境發布到預發布環境,當回歸測試通過后,再將項目代碼從開發環境發布到生產環境。發布到預發布和生產環境是類似的,只是發布的目的地不同。關鍵的核心操作環節有:
+ 1、準備待發布的版本代碼
+ 2、傳輸待發布的代碼到各服務器
+ 3、解壓待發布的代碼并完成發布的收尾工作
上面的三個核心環節,分別涉及了發布前、發布中和發布后。在發布前,我們需要把待發布的代碼進行打包,如果需要進行前端的構建,或者需要把靜態的資源文件同步到CDN的話,也可在這一環節完成。尤其對于代碼有版本號,或者需要進行一些前置性的工作,都可在這時固化下來。可以說,這一環節非常關鍵,任何一處缺失都有可能會導致上線后故障的發生。
在我曾經任職過的一個游戲公司里,每次發布前,都需要將根據數據字典自動生成PHP代碼,根據XML配置刷新內存中的項目配置,需要提前將對應的Flash資源上傳到外網CDN,需要進行必要的數據庫變更以及準備新增的Redis實例。當這些都準備妥當之后,我們才可以開始打包待發布的代碼。
為方便理解,我們結合shell發布腳本來一起講解。截至當前,我們可以快速寫出下面這樣的shell腳本,命名為publish.sh并保存到項目合適的目錄下。
```
#!/bin/bash
# 一鍵發布
# @author dogstar 20180318
# Step 1. 選擇發往的環境
if [ $# -lt 1 ]; then
echo "Usage: $0 <pre|prod>"
echo " - $0 pre # publish to preview environment"
echo " - $0 prod # publish to product environment"
exit 1
fi
```
首先,讓發布人員選擇是發往哪個環境,pre表示預發布環境,prod表示生產環境。接下來,選擇需要發布的代碼并進行打包操作。注意,這里可選擇發布的范圍,并不是項目下的全部文件都要發布,只有需要才發布。例如對于日志目錄就不需要發布,又例如這里排除了單元測試目錄。這時用到了第一個shell命令,即tar打包命令。
```
# Step 2. 打包待發布的文件
tar -czf ./examples.tar.gz \
./config \
./src \
--exclude ./tests \
./public \
./statics \
./data
```
這里待打包的目錄和文件,需要根據你的項目情況進行調整,你可追加需要發布的目錄和文件,也可以排除不需要用到的目錄和文件。
再接下來,就是進行發布中的操作,即傳輸待發布的代碼到各服務器。這里的IP地址都是虛擬的,可以根據自己的情況進行修改。分別是預發布的IP地址,和生產環境服務器集群的IP地址。這時需要使用到scp命令。
```
# Step 3. 傳輸待發布的代碼到各服務器
API_ROOT="/home/apps/projects/www.examples.com"
USER="apps"
IPS=("192.168.1.1" "192.168.1.2" "192.168.1.3") # 生產環境
if [ "$1" == "pre" ]; then
IPS=("192.168.1.100") # 預發布環境
fi
for ip in ${IPS[*]}
do
echo -e "[$ip] start to scp ..."
scp ./examples.tar.gz $USER@$ip:$API_ROOT
done
```
當全部的發布包都傳輸完畢后,我們就可以執行批量解壓,讓PHP代碼幾乎同時生效啦。注意,是全部傳輸再一同解壓,而不是傳輸完一臺服務器就解壓一臺服務器。這樣可以盡量縮短代碼同時生效的時間間隔。這里使用了最后一個命令,ssh。
```
# Step 4. 解壓待發布的代碼并完成發布的收尾工作
cmd="tar -xzf $API_ROOT/examples.tar.gz -C $API_ROOT"
for ip in ${IPS[*]}
do
echo "[$ip] start to run: $cmd ..."
ssh -t $USER@$ip "$cmd"
done
```
將以上shell代碼片段連起來,并在最前面加入備份操作,以及發布后進行相關操作外,我們就初步搭建好了一鍵發布的自動化流程。合成后的代碼如下:
```
#!/bin/bash
# 一鍵發布
# @author dogstar 20180318
# Step 0. 備份
cp ./examples.tar.gz ./examples.bak.tar.gz
# Step 1. 選擇發往的環境
if [ $# -lt 1 ]; then
echo "Usage: $0 <pre|prod>"
echo " - $0 pre # publish to preview environment"
echo " - $0 prod # publish to product environment"
exit 1
fi
# Step 2. 打包待發布的文件
tar -czf ./examples.tar.gz \
./config \
./src \
--exclude ./tests \
./public \
./statics \
./data
# Step 3. 傳輸待發布的代碼到各服務器
API_ROOT="/home/apps/projects/www.examples.com"
USER="apps"
IPS=("192.168.1.1" "192.168.1.2" "192.168.1.3") # 生產環境
if [ "$1" == "pre" ]; then
IPS=("192.168.1.100") # 預發布環境
fi
for ip in ${IPS[*]}
do
echo "[$ip] start to scp ..."
scp ./examples.tar.gz $USER@$ip:$API_ROOT
done
# Step 4. 解壓待發布的代碼并完成發布的收尾工作
cmd="tar -xzf $API_ROOT/examples.tar.gz -C $API_ROOT"
for ip in ${IPS[*]}
do
echo "[$ip] start to run: $cmd ..."
ssh -t $USER@$ip "$cmd"
done
# Step 5. TODO: 發布后的操作
echo "Finish!"
```
關于上面的發布操作,我們在背后還隱藏了一些其他的細節。例如,為了能免密碼遠程操作,我們需要在各遠程服務器上的/home/apps/.ssh/authorized_keys文件內,添加操作發布的電腦的RSA公鑰。但只要大家能理解整個發布過程的主要環節,至于如何具體實現,則可以靈活進行調整和擴展。
在一鍵發布時,我們特意考慮到了需要發布回滾的操作,故此提前做好版本備份的工作。對于如何進行發布回滾,其實和正常的發布是類似的,這里不再贅述。感興趣的同學可當作練習,自己親自實踐一下。
這里描述了一鍵發布的基本輪廓,大家可以在實際應用中進行細化。在這基礎上,我們就可以在后續實現更快速、更穩定地進行發布操作。特別當需要增加新的服務器時,只需要稍加配置,追加到上面的IPS配置,就可以實現對新服務器的發布。同樣地,當需要摘除某臺服務器時,也只需要在IPS變量中刪除相應的服務器IP即可。這是一個好的開始。
### 3.1.4 建立更完善的發布流程
當項目發展到更大規模時,對線上環境的發布操作會移交給專門的團隊來負責。這時需要更完善的發布流程,而這些流程化的操作則會通過更友好的界面,集成度更廣的發布系統來體現。將上面的一鍵發布整合并升級為完整的發布系統,又將是項目成長的一大里程碑。
雖然要完成的工作還有很多,但我們可以更方便地進行整合和集成。界面化的操作,最終都可以歸為在Linux上的命令操作。當底層的腳本命令已經基本形成時,在上面再加上界面化就輕而易舉了。
## 3.2 生產環境注意事項
我們從簡單的發布開始,演進到一鍵發布,最后通過發布系統建立了更完善的發布流程。這是否意味著發布就此結束了呢?
不,恰好相反,這只是一個開始。因為當代碼發布到生產環境之后,正是一系列操作和工作的開始。為保障系統的安全性、穩定性、健壯性,我們還需要完成以下短期性或長期性的工作。
+ 更改默認端口
+ 開啟錯誤日記
+ 開啟慢日志
+ 進行服務器監控
+ 對數據庫進行備份
下面分別簡單說明。
### 3.2.1 更改默認端口
在當前環境下,網絡安全一直都是個嚴峻的問題。即便沒有專業的安全專家,我們也可以通過簡單的方式來加強我們服務器對外辦的防范。其中之一就是更改默認端口。例如更改SSH登錄下默認的22端口,修改MySQL數據庫默認的3306端口,以及Redis和Memcache的默認端口,以增加外界暴力破解的難度。
### 3.2.2 開啟錯誤日記
開啟錯誤日記,能方便我們遇到問題時快速定位到具體的原因。這里可以開啟Nginx的錯誤日志,例如前面配置的:
```
error_log /var/log/nginx/www.examples.com.error_log;
```
此外,還可以開啟PHP-FPM的錯誤日志,例如/etc/php-fpm.conf配置里的:
```
error_log = /var/log/php-fpm/error.log
```
### 3.2.3 開啟慢日志
除了要通過錯誤日志關注異常情況外,還要通過慢日志來關注系統的響應速度。例如開啟PHP-FPM的慢日志,可以修改/etc/php-fpm.d/www.conf配置文件中的:
```
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 2s
```
當PHP請求的響應時間超過2秒時,將會在/var/log/php-fpm/www-slow.log日志文件內產生一條紀錄。
例如,我們可以編寫一個慢日志的測試腳本test_slow.php,并放置以下代碼。
```
// $ vim ./public/test_slow.php
<?php
echo "start to run ...";
sleep(1);
$arr = range(0, 10);
sleep(1);
$arr = range(0, 10);
sleep(1);
$arr = range(0, 10);
echo "finish to run ...";
```
在這里,我們故意分別睡眠了三次,每次1秒。所以通過瀏覽器訪問后,我們可以看到類似這樣的慢日志。
```
[18-Mar-2018 20:38:42] [pool www] pid 3535
script_filename = /home/apps/www.examles.com/public/test_slow.php
[0x00007f4ff8213120] sleep() /home/apps/www.examles.com/public/test_slow.php:11
```
特別對于數據庫,更需要開啟慢查詢,以便及時對數據庫的慢查詢語句進行針對性的優化。修改/etc/my.cnf配置文件,并往里面添加以下配置,可以開啟MySQL數據庫的慢查詢日志。
```
slow_query_log = 1
slow_query_log_file = /var/log/mysql_slow_query.log
long_query_time = 2
```
同樣地,當數據庫查詢語句執行時間超過2秒時,將會產生一條慢日志。
### 3.2.4 服務器監控
除了對PHP、Nginx和MySQL開啟錯誤日志和慢查詢日志外,我們還需要對網站賴以生存的系統環境進行監控,以便及時發現服務器在CPU、內存、硬盤空間和網絡IO等方面的指標性能。例如你可以選擇使用Zabbix進行全方位的監控。
### 3.2.5 對數據庫進行備份
最后,還有一個建議是非常重要的,這個建議很有可能在日后為你的公司挽回數十萬的經濟損失。那就是,定時對數據庫進行備份。通過在不同的服務器物理機上,對當前系統使用的數據庫進行自動化數據備份,以應對人為錯誤操作、機房服務器被外界因素毀壞等不可控的風險。
例如,對于MySQL數據庫,可以使用automysqlbackup進行備份。簡單配置后,再結合crontab計劃任務,我們就可以實現定時自動化數據庫備份了。
```
# 數據庫備份
30 3 * * * /usr/bin/automysqlbackup /etc/automysqlbackup/automysqlbackup.conf
```
如上面的crontab計劃任務配置,就可以實現了在每天凌晨3點半時,根據automysqlbackup.conf配置文件進行MySQL數據庫的備份。
成功備份后,可以看到在CONFIG_backup_dir配置的備份目錄下,有類似如下目錄,分別存放了每日、每周、每月備份的數據。更多使用說明,可參考automysqlbackup的相關文檔介紹。
```
.
|-- daily
| |-- mysql
| |-- performance_schema
| `-- test
| `-- daily_test_2018-03-19_21h54m_Monday.sql.gz
|-- fullschema
|-- latest
|-- monthly
|-- status
|-- tmp
`-- weekly
`-- test
`-- weekly_test_2018-03-19_21h54m_12.sql.gz
```
## 3.3 入侵式設計
通過前面介紹的一鍵發布腳本或者更為強大的發布系統,我們已經向世界發布了我們的代碼。但是除了發布以外,在項目部署和架構搭建上,如果方式不對,會產生入侵式的設計。我曾見過因入侵式設計而苦苦掙扎在其中的團隊,下面看下哪些是不合理、會導致破壞原有體系的做法。
### 3.3.1 觸電式架構
在編程世界里,對于類、文件和模塊之前的依賴,一直都有著類似的原則。例如盡量使用單向依賴而不是雙向循環依賴,高層模塊不應該依賴于底層模塊。
在C++中,若兩個類需要相互依賴,那么在代碼編寫上需要一點技巧,也會引入一些復雜性。如果相互間的依賴過于復雜,就會容易產生接二連三的問題,正所謂“剪不斷,理還亂”。DLL地獄就是這類問題的史證。
那這些和使用PHP開發的網站項目有什么關系呢?重點來了,由于PHP是動態腳本,是解釋性編程語言,不需要編譯就能直接運行,并且在文件引入和類引入上更為靈活。靈活滋生混亂。在編寫C++程序時,程序員需要嚴格遵循規范和標準,使得編寫的代碼符合要求,通過編譯。比較明顯的要求就是你在實例化某個類之前,這個類需要先存在,并且將要調用的類成員函數也是確定的。而在編寫PHP程序時,你可以動態實例化某個類,也可以動態調用某個類成員函數,甚至傳遞的參數也是動態變化的。這些靈活的語法特性,在帶來了編程開發便利性的同時也會容易被人濫用,從而產生不能事先確定、非線性邏輯的代碼。順便提及一下,也許PHP語言還不是最為復雜的,因為還有更為靈活、通過代碼生成代碼的元編程語言,例如Ruby。
我們暫時不進行過多的發散,下面回歸正題。我們先通過一些代碼片段示例,看下PHP都有哪些靈活性。再來看下在這基礎上,為何會容易構建出觸電式架構。
“一般的開源框架,是怎樣做到根據指定URL執行相應PHP代碼的?你能具體說明一下嗎?”
在面試中,有時面試官或問到這樣的問題。對于這個問題,回答的核心正是PHP如何動態執行某個類的某個成員函數。假設客戶端訪問的URL是:
```
http://localhost/?c=Site&a=Index
```
其中,參數c表示Controller控制器(即:類名),a表示Action動作(即:方法名)。最終對應執行SiteController類的actionIndex方法,如下:
```
<?php
class SiteController {
public function actionIndex() {
echo "Hello World!";
}
}
```
那怎么實現這一動態執行的過程呢?
為突出如何動態執行這一關注點,我們暫時不考慮路由,也不考慮文件引入。在PHP中,需要動態執行類的某個方法,是比較簡單的,并且實現方式可以有多種。簡單地,可以直接這樣:
```
<?php
$controller = $_GET['c'] . 'Controller';
$action = 'action' . $_GET['a'];
$instance = new $controller();
$instance->$action();
```
為簡單起見,這里也省略了對參數的校驗以及異常情況的處理。
另外,我們也可以通過call_user_func()函數來實現。目前這里不需要傳遞參數列表,結合恰當的回調類型的表示方式,還可以這樣實現:
```
<?php
$controller = $_GET['c'] . 'Controller';
$action = 'action' . $_GET['a'];
$instance = new $controller();
// 注意最后這一行,執行回調函數
call_user_func(array($instance, $action));
```
你還可以使用call_user_func_array()函數來實現調用,不過其用法和call_user_func()函數類似,不同的是可以把參數列表作為一個數組動態傳入。
對于動態執行某個函數,其做法也是類似的。那這樣會容易帶來什么問題呢?這里有兩個要點需要關注的,第一點是PHP是解釋性語言,不需要經過編譯就能執行,這意味著即便存在語法錯誤的PHP文件,項目也能正常工作,前提是不觸發執行這些有問題的PHP文件即可。更直白來說,哪怕你往網站項目中故意放入了一些有問題的PHP代碼,或者添加其他項目的代碼也沒什么大不了的,只要別踩到這些“地雷”。第二點是,PHP是動態執行的,你將要執行的類靜態方法、類成員函數和普通函數,事先是不確定的。你可能找得到它們,也可以找到了但無法執行,比如試圖調用protected級別的成員函數,或者傳遞的參數列表不符。
而這兩點,不需要全部符合語法規范以及不確定的動態執行,在項目級別和系統架構級別造就了更大的靈活性。隨著公司業務的發展,最初只有一個PHP網站項目,然后PHP項目數量增長到2個、3個、5個、12個……原來只是項目內的依賴關系,現在為貪圖方便、或者是缺少考慮、或者是其他歷史原因,慢慢擴散到不同項目之間。即項目A,嚴重依賴于項目B的代碼;項目B直接依賴于項目C的底層代碼;項目C又需要調用項目A的函數獲取最新商品數據。這就慢慢形成了觸電式的架構,一旦某個項目的函數出問題了,就會影響其他項目的正常運行。
觸電式架構是一種反模式,我覺得應當堅決抵制。觸電式架構為線上系統帶來了極其脆弱性和不穩定性,項目與項目之間就像手拉手的一群人,只要其中一個人觸電了,其他全部人都會跟著受累。而且,由此而引入的復雜性也是高得嚇人。深陷在觸電式架構內的項目,它的復雜度不是在于它自身業務規則的復雜度,也不在于內部模塊與模塊之間錯亂的調用關系,而是在于它與外部項目的直接依賴關系!由于PHP代碼在邏輯上(類與類、函數與函數間的調用)以及物理上(文件與文件之間的include/require引用),最終也決定了這些有依賴的不同項目,都需要部署在同一臺服務器上,從而產生了更大層面的依賴。如果某個網站項目因為訪問量過高而導致服務器性能負載過高,就會直接影響其他網站項目的正常運行。這種整機部署的策略,暗含了巨大的不確定性和脆弱性。但也許,這還不是最糟糕的。除了在代碼上,在服務器部署上有依賴外,根據破窗理論,在觸電式架構這一大背景下,開發團隊也就“順其自然”地在數據這一領域上也產生了嚴重的依賴。他們通常會聯表跨庫查詢和操作,因為既然代碼都“在一起”了,數據庫也應該“在一起”。
從選擇了PHP這門編程語言開發網站開始,得益于PHP的靈活性,我們能快速進行迭代,但如果控制不好也會因其靈活性而產生不恰當的依賴關系。當類與類之間不恰當的依賴關系,蔓延到模塊與模塊之間,再蔓延到項目與項目之間,就會發生由量變到質變的轉換。隨之而來的,就是服務器部署上的依賴,和數據庫上的依賴。慢慢地,就編織了一張更大的網、更大的坑。

圖3-3 觸電式架構示例
形成觸電式架構的關鍵決策點在于,第二個項目誕生時如何約束依賴關系。新項目如果需要使用底層的公共函數和配置,該如何處理?新項目如果需要調用最初項目的某個模塊完成特定業務功能時,又該如何設計?新項目在不同的業務場景上需要依賴第一個項目的數據庫數據時,又該如何應對?如果不加考慮,為了貪圖方便,或者迫于上線時間壓力,一旦把第二個項目和第一個項目直接部署在一起,并且通過PHP代碼直接相互調用,還允許操作其他項目的數據庫,那么后來的新項目也會陸續效仿這一做法。如果不加以控制、調整和約束,假以時日,觸電式架構就會形成,牽一發而動全身。
通俗來說,觸電式架構就是“你中有我,我中有你”,說得動情一點就是共赴患難,有難一起當。但是這種做法是不可取的,除非有特殊的原因(實際上,我也想不出有任何充足的理由),不要采用這種做法。在部署發布第二個項目時,一定要回過頭考察評估是否有類似觸電式架構這般強依賴的關系網的存在。如果有,請說不!
關于觸電式架構,我們暫時說到這里。下面再來看下壓縮環境。
### 3.3.2 壓縮環境
平時我們都會吃到壓縮餅干,這些餅干將小麥粉、糖、油脂、乳制品等主要原料壓縮成餅干,方便食用。但如果追求更健康的生活,我們不能長期只吃壓縮餅干,還要補充維生素、微量元素、蛋白質等,均衡飲食。同樣,在開發網站項目過程中,我們可能也會用到壓縮環境。通常而言,部署的環境按項目的不同階段和不同使用人員,可以分為:開發環境,測試環境,回歸環境,預發布環境,生產環境。但小團隊或者小公司,出于低成本,都喜歡使用壓縮環境。例如只使用開發環境和生產環境,而沒有測試、回歸、預發布這些環境。更有甚者,如前面所說,只使用生產環境,即開發環境與生產環境使用同一個。這些就是我們所說的壓縮環境。
在觸電式架構下,別人發布了新的代碼,有可能會導致你的項目首頁無法訪問,間接無辜受到影響,而且這種影響是不可控、不可預見的。對于使用壓縮環境而言,類似地,如果開發環境與生產環境共用一套,就會容易造成因功能開發調試過程影響線上環境的穩定性。如果開發環境與測試環境是同一個,那么開發人員和測試人員就會容易產生沖突。測試人員正在進行功能驗收的代碼,卻被開發人員修改或者切換了;而開發人員正在進行測試的數據,又會被測試人員用于進行功能驗收。由于關注點不同,參與的角色越多,集成的系統越多時,壓縮環境就會越容易產生更頻繁的沖突。
當然,我們需要做到平衡,實現均衡發展。一開始就搭建全部環境也是不現實的,但當確實需要那么多不同的環境卻依然使用壓縮環境時也是不應該的。需要注意的是,在不同的環境上,應盡量保持一致的部署方式,通過前面搭建的一鍵發布,我們可以很容易實現這點。
### 3.3.3 不同環境,不同域名
即便已經根據需要,采用了不同的環境。但如果對于域名的分配和使用方式不對的話,也會產生另一種入侵式的設計。甚至是使用壓縮環境,也會有這個問題。這種反模式就是:不同環境,不同域名。尤其體現在測試環境的域名與正式線上環境的域名不同。
實現這種反模式可以有幾種做法,分別是:
+ **通過不同頂級域名區分不同環境**
例如,假設生產環境域名是www.examples.com,那么測試環境則使用.cn域名,即測試環境域名是www.examples.cn。由于使用了不同域名,除了不能共享session會話外,也不能共享客戶端COOKIE,這會對開發造成一定的困惑。此外,還會因為不同域名而產生跨域問題,例如前端Javascript在發起Ajax請求時,如果未在不同環境時進行相應切換,就會發生在測試環境跨域請求了生產環境,或者在生產環境跨域請求了測試環境。前者的影響只是會讓開發人員產生困惑,而后者一旦發布上線就會直接導致故障的發生。
+ **通過添加前綴區分不同環境**
另一種區分的方式是,使用相同的頂級域名,但使用不同的域名前綴來區分。例如,生產環境域名是www.examples.com,測試環境域名是test.www.examples.com。這在一定程度上可以緩解或解決COOKIE共享、Ajax跨域和靜態資源引用的問題,因為可以使用相對路徑而非絕對路徑來實現。但當需要和外部系統進行交互或者集成時就會遇到問題。例如在測試環境分享了一個鏈接,到外網卻打不開。
+ **通過動態開發分支名切換不同環境**
最后,還有一種方式是使用分層存儲,動態根據開發時的分支名稱,切換到不同環境。例如master分支的訪問域名是www_master.examples.com,dev分支的訪問域名是www_dev.examples.com,依此類推。默認的則是生產環境的域名www.examples.com。這種方式通常是應用在壓縮環境。
不管是通過何種方式,一旦不同環境所用的域名不一樣,就會產生入侵式的設計。首當其沖就是對代碼編寫上的影響。網站開發人員在編寫PHP代碼時,經常要根據不同的環境手動或自動切換到不同的域名,并且還要時刻記得在測試通過、上線發布前把域名恢復為正式域名,否則就會產生故障。不同環境,不同域名,不僅為開發階段帶來了額外的負擔,還為測試階段引入了不必要的復雜性,更讓人不能接受的是在線上環境會容易滋生故障。例如發布后,線上環境引用了測試環境的靜態資源,導致因缺少Javascript文件而報錯,或因缺少CSS樣式文件而布局錯亂。又或者,因為前端Javascript或者后端PHP內部系統在生產環境調用訪問了測試環境的接口鏈接,無法獲取或獲取了錯誤的數據信息。
不同環境使用不同域名,這種做法造成的影響是直接且深遠的。對于前端開發員,他們要注意Ajax接口請求、靜態資源文件引用上的差異;后端開發人員需要時刻關注根據不同環境進行切換和調整才能保證內部接口調用和系統之間的正確訪問;測試人員則要在回歸測試時警惕發現不同環境下有沒相互穿越錯亂的現象,并及時知會技術人員進行修正;產品人員則時不時要擔心會不會在線上環境再次發布類似的故障。
## 本章小結
發布是一件可大可小的事情。說它小,是因為你可以直接通過很簡單的方式就能實現線上代碼的更新和同步;說它大,是因為如果想做到完整全面,需要考慮的問題會很多,除了要搭建自動化一鍵部署外,還需要配套集成度更高的發布系統。建立起完善的發布流程,實現項目的快速發布,將會幫助你在項目迭代過程中實現小步快跑。
生產環境是暴露給世界訪問的,它處于現實世界的殘酷和危險之中。為了最大化保護我們的網站、我們的系統和我們的服務器,需要注意以下事項:
+ 更改默認端口
+ 開啟錯誤日記
+ 開啟慢日志
+ 進行服務器監控
+ 對數據庫進行備份
除了在生產環境做足了準備外,還需要防范入侵式的設計,例如:觸電式架構、壓縮環境和不同環境不同域名。任何一種入侵式設計,都會對今后的項目迭代產生深遠的負面影響。
發布代碼到線上環境,只是一個開始。除了要留意注意事項和防范入侵式設計,我們還會陸續迎接到更大的問題、更嚴峻的挑戰。不過不要急于馬上投入到實際商業產品、項目或系統的開發中,先來回顧一下PHP開發相關的知識,看下我們是否遺漏或忽略了某些重要信息。