代码迁移之旅(二)- 渐进式迁移方案
說在前面
這是代碼遷移的第二篇文章,也是最后一篇了,由于個人原因,原來的遷移我無法繼續參與了,但完整的方案我已經準備好了,在測試環境也已經可以正常進行了。 上篇文章 代碼重構之旅(一) 項目結構 介紹了遷移代碼的前期準備和項目結構的設計,本篇文章來介紹一下可實施的遷移方案。
使代碼的遷移過程更簡單、更安全是我們要追求的目標,在遷移之前,代碼的可用性我們一定也只能畫一個問號。
文章歡迎轉載,但請注明來源: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 有以下幾種形式,優先級從高到低為:
不同的 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 ~ /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 率降到最低。
我們的系統對外提供服務都是通過接口,這也方便了我們進行測試。為了保證測試的完整性,可以將線上流量引入到新代碼中進行測試,而實行請求導流的最好媒介就是日志。
一般來說,服務器都有完整的線上請求日志,如果有必要,在給特定接口添加特定日志以配合測試也是可以的。接入線上日志,構造跟線上一樣的請求到測試服務器,再對比原始服務器的響應內容,將異常響應記錄下來由開發分析并查找原因,直到最后新舊項目對所有請求的響應完全一致。
小結
項目的重構不是一個小事,特別是大規模的項目代碼遷移,執行它必須膽大心細,但每一次重構,無論是對自己的技術能力還是項目的生命周期都是很大的提升。
雖然不鼓勵沒事就瞎折騰代碼,但一定要時刻警惕,走出代碼的舒適區,一定要提前預防根治代碼疾病,不要在代碼已經無可救藥時才想到重構。
技術發展迅速,代碼總有過時的一天,所以經常對代碼有目的有計劃的小幅優化是非常有意義的。
關于本文有什么問題可以在下面留言交流,如果您覺得本文對您有幫助,可以點擊下面的 推薦 支持一下我,博客一直在更新,歡迎 關注 。
轉載于:https://www.cnblogs.com/zhenbianshu/p/8110912.html
總結
以上是生活随笔為你收集整理的代码迁移之旅(二)- 渐进式迁移方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MyBatis动态SQL小结
- 下一篇: 数据库监视器(SQL Server Pr