說在前面
這是代碼遷移的第二篇文章,也是最后一篇了,由於個人原因,原來的遷移我無法繼續參與了,但完整的方案我已經准備好了,在測試環境也已經可以正常進行了。 上篇文章 代碼重構之旅(一) 項目結構 介紹了遷移代碼的前期准備和項目結構的設計,本篇文章來介紹一下可實施的遷移方案。
使代碼的遷移過程更簡單、更安全是我們要追求的目標,在遷移之前,代碼的可用性我們一定也只能畫一個問號。
文章歡迎轉載,但請注明來源:http://www.cnblogs.com/zhenbianshu/p/8110912.html, 謝謝。
問題抽象分析
首先要看一下一次完整的遷移需要滿足什么要求:
- 灰度發布,誰也無法保證一次將整個系統遷移到另一個系統不會發生問題,而以接口或接口部分流量為單位進行遷移則可以大大提升可控性。
- 客戶端無感知,即遷移平滑,長時間的系統不可用是完全無法接受的。
- 可回滾,一旦出現異常問題可以快速回滾,避免造成較大影響。
- 易實現,盡量避免大量地操作,操作多意味着犯錯的可能性更大,回滾的難度也大。
只有實現了以上要求,才算是一次成功的遷移。那么先分析一下目前的情況:
如上圖是我們兩個系統的目前狀態:
- 兩個系統共享一個 Nginx 服務器,而且在 Nginx 中,由於新老系統的
Host::Ip
也不需要變動,所以新老系統還共享一個同一個 Server。 - 新舊兩個模塊分別對應着兩個版本控制目錄,舊模塊將 Http 請求進行 url 重寫后直接分發到各 PHP 腳本,例如:
rewrite ^/api/common/test.json?(.*)$ /api_test.php?$1;
- 新模塊將 Http 請求直接分發到 index.php 后,由 index.php 進行內部路由轉發。
兩個模塊初始狀態相安無事,現在的問題是如何將舊模塊的接口逐漸過渡到新模塊中。由於舊模塊的分發入口在 Nginx 中,最簡單的辦法自然是修改其原來的重定向規則。
Nginx重定向
先看一個典型的 Nginx Http 服務器配置:
http {
upstream stream_name{
}
server {
listen port;
server_name domain_name host_name;
rewrite ori destA;
location pathA {
rewrite ori destB type;
}
location pathB {
if(match){
rewrite ori destC type;
}
rewrite ori destD type;
}
}
}
我們要使用的就是 Nginx 強大的路由重定向功能。
location
location 是一個 URI 捕獲語句,它被定義在 server 模塊內,會對 server 內的所有請求進行 uri 匹配,一旦匹配,則進入 location 模塊內部執行。
location 常見的使用形式是:
location path_pattern {
operation;
}
它的 path_pattern
有以下幾種形式,優先級從高到低為:
- 完全相等匹配
location = uri {}
- 前綴匹配
location prefix {}
或location ^prefix ~ {}
- 正則匹配
location ~ regex {}
或不區分大小寫正則匹配location *~ regex {}
- 通用匹配
location / {}
不同的 pattern 類型匹配順序與定義順序無關,而是由優先級從高到低進行匹配,同一類型的,優先使用 pattern 串更長的進行匹配,因為長串會更精確。
它的 operation 一般是 rewrite 或 proxy_pass 語句,對捕獲到的請求進行重寫或轉發。用於轉發的 proxy_pass 語句很簡單, proxy_pass proxy_name;
即可,下面具體說一下路由重寫功能。
if
if 語句可以對 uri 進行更加靈活的判斷和操作,它的常見使用形式是:
if (match) {
rewrite ori destA type;
}
rewrite ori destB type;
在 match
語句中,可以使用如 $request_uri
等全局變量,常見的還有 $query_string,$uri,$remote_addr
等。
但是需要注意使用 if 語句是十分低效的行為,它就像普通的代碼一樣,每個 Http 請求碰到 if 語句都會進行一次 match
計算並判斷,雖然寫在 location 內部會好一些,但最好還是極力避免此語句。
rewrite
rewrite 是對匹配到的請求進行 uri 重寫,它可以被寫在 server/location/if
模塊中,使用方式 是 rewrite ori dest type;
。在 server 模塊中,rewrite 和 location 的執行順序為:server中的rewrite -> location -> location中的rewrite
我們可以使用正則或全相等來匹配 ori
,並將正則結果應用於 dest
上,如 rewrite ^/api/common/test.json?(.*)$ /api_test.php?$1;
則將 ori 內部的 query_string 匹配出來並使用 $1
賦值給 dest。
rewrite 默認將 uri 重寫后並不直接將請求分發到 CGI,而是將結果 uri 作為一個新的請求再次進行 server 模塊內處理,如果循環重入超 10 次 nginx 會直接返回 500 internal server error
,而控制 rewrite 匹配后的行為 主要依靠其 type 參數:
- last 結束此模塊(server/location) 匹配,並重入 server 模塊處理,rewrite 默認使用此項;
- break 結束所有模塊匹配,直接將請求分發到 CGI;
- redirect 直接分發請求,返回 Http 狀態碼 302 臨時重定向;
- permanment 直接分發請求,返回 Http 狀態碼 301 永久重定向;
應用
介紹完了 Nginx 的重定向功能,還需要考慮怎么使用此功能進行代碼的過渡。
- 使用 location 捕獲對應接口;
- 使用 if 進行部分流量分發(可選);
- 將請求 rewrite 到新模塊。
如:
location ~ /api/test.json { # 匹配到 test 接口
if ($remote_addr ~* 1$) { # 分流 IP 末位為 1 的請求
root new_dir/public; # 設置新項目的目錄為根目錄
rewrite ^(.*)$ /index.php$1 break; # 將請求分發到新項目的 index.php 入口文件
}
rewrite ^/api/test.json?(.*)$ /api_test.php?$1; # IP 末位不為 1 的請求繼續訪問舊項目
}
Linux鏈接
如上,我們發現如果針對每個接口進行一次 location 重定向,都需要寫 7 行代碼,即使不用 if 語句(多數情況如此),每次也需要 4 行代碼。
location ~ /api/test.json { # 匹配到 test 接口
root new_dir/public; # 設置新項目的目錄為根目錄
rewrite ^(.*)$ /index.php$1 break; # 將請求分發到新項目的 index.php 入口文件
}
如此下來,項目如果有 100 個接口,那么維護這100個 location 模塊也頗為廢勁。其實更多時刻,我們並不需要使用 location 語句,直接在 server 模塊內部使用 rewrite 即可,而阻止我們直接使用 rewrite 的,就是由於新舊模塊不在同一文件夾下,我們必須使用 root 語句將根目錄定義到新項目下。至於為什么不將新舊項目的父文件夾定義為 root,是因為舊項目中有一些路徑可能會有深坑。
這里我們可以使用 linux 的 軟鏈接
來 把新項目“放置”在舊項目下:linux 中軟鏈接的功能就像 windows 中的快捷方式
一樣,是一個指向文件或真實目錄的符號。至於其實現,就要說到 linux 文件結構中的重要概念 inode
了,不過這里不再多提。
使用 ln -s /path/to/dir_new /path/to/dir_old/yaf
在舊項目目錄下創建一個 yaf
軟鏈接指向新項目目錄;
這樣,就可以以舊項目目錄為根目錄,找到新項目目錄下的文件了,使用單行命令 rewrite ^(/api/test.json(.*)$) /yaf/public/index.php$1 break;
即可。
框架內URL重寫
通過上面 Nginx 的重定向,所有的請求都會被分發到 index.php 中, 接下來就需要在 yaf 內對 index.php 接收到的 Http 請求進行內部分發。
yaf 提供了 Yaf_Route_Static、Yaf_Route_Simple、Yaf_Route_Supervar、Yaf_Route_Map、Yaf_Route_Rewrite、Yaf_Route_Regex
六種路由方式,各有其適合的場景,需要在 /conf/application.ini
中配置 application.dispatcher.defaultRoute.type="type"
。
我們的內部接口名完全不規則,有改寫為 .json
后綴的,也有保持 .php
的,有帶下划線的,也有大小寫敏感的,找不到什么規律,於是使用了 map
類型,直接匹配 uri
然后映射向 controller 類。
我們將 uri 和controller的映射統一保存在一個文件內,形如:
return array(
// 接口作用
'key' =>
array(
'type' => 'rewrite',
'match' => '/api/test.json',
'route' =>
array(
'controller' => 'Api_Test'
),
),
...
);
然后在 Bootstrap.php
內加載此配置文件:
public function _initRouter() {
$router = \Yaf\Dispatcher::getInstance()->getRouter();
$config = getConfig('rewrite_file_name');
$router->addConfig($config);
}
自此,關於遷移的配置就完成了。
測試
一次安全的遷移,完整的測試當然必不可少。在保證技術方案沒問題的前提下,還要進行完整的業務邏輯測試。在 QA 測試之前,開發首先要通過盡可能完整的測試,將 BUG 率降到最低。
我們的系統對外提供服務都是通過接口,這也方便了我們進行測試。為了保證測試的完整性,可以將線上流量引入到新代碼中進行測試,而實行請求導流的最好媒介就是日志。
一般來說,服務器都有完整的線上請求日志,如果有必要,在給特定接口添加特定日志以配合測試也是可以的。接入線上日志,構造跟線上一樣的請求到測試服務器,再對比原始服務器的響應內容,將異常響應記錄下來由開發分析並查找原因,直到最后新舊項目對所有請求的響應完全一致。
小結
項目的重構不是一個小事,特別是大規模的項目代碼遷移,執行它必須膽大心細,但每一次重構,無論是對自己的技術能力還是項目的生命周期都是很大的提升。
雖然不鼓勵沒事就瞎折騰代碼,但一定要時刻警惕,走出代碼的舒適區,一定要提前預防根治代碼疾病,不要在代碼已經無可救葯時才想到重構。
技術發展迅速,代碼總有過時的一天,所以經常對代碼有目的有計划的小幅優化是非常有意義的。
關於本文有什么問題可以在下面留言交流,如果您覺得本文對您有幫助,可以點擊下面的 推薦
支持一下我,博客一直在更新,歡迎 關注
。