1. 程式人生 > >代碼遷移之旅(二)- 漸進式遷移方案

代碼遷移之旅(二)- 漸進式遷移方案

std api 接收 小事 業務邏輯 hidden img 優先級 default

說在前面

這是代碼遷移的第二篇文章,也是最後一篇了,由於個人原因,原來的遷移我無法繼續參與了,但完整的方案我已經準備好了,在測試環境也已經可以正常進行了。 上篇文章 代碼重構之旅(一) 項目結構 介紹了遷移代碼的前期準備和項目結構的設計,本篇文章來介紹一下可實施的遷移方案。

使代碼的遷移過程更簡單、更安全是我們要追求的目標,在遷移之前,代碼的可用性我們一定也只能畫一個問號。

文章歡迎轉載,但請註明來源: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 有以下幾種形式,優先級從高到低為:

  1. 完全相等匹配 location = uri {}
  2. 前綴匹配 location prefix {}location ^prefix ~ {}
  3. 正則匹配 location ~ regex {} 或不區分大小寫正則匹配 location *~ regex {}
  4. 通用匹配 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 的重定向功能,還需要考慮怎麽使用此功能進行代碼的過渡。

  1. 使用 location 捕獲對應接口;
  2. 使用 if 進行部分流量分發(可選);
  3. 將請求 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 率降到最低。

我們的系統對外提供服務都是通過接口,這也方便了我們進行測試。為了保證測試的完整性,可以將線上流量引入到新代碼中進行測試,而實行請求導流的最好媒介就是日誌。

一般來說,服務器都有完整的線上請求日誌,如果有必要,在給特定接口添加特定日誌以配合測試也是可以的。接入線上日誌,構造跟線上一樣的請求到測試服務器,再對比原始服務器的響應內容,將異常響應記錄下來由開發分析並查找原因,直到最後新舊項目對所有請求的響應完全一致。


小結

項目的重構不是一個小事,特別是大規模的項目代碼遷移,執行它必須膽大心細,但每一次重構,無論是對自己的技術能力還是項目的生命周期都是很大的提升。

雖然不鼓勵沒事就瞎折騰代碼,但一定要時刻警惕,走出代碼的舒適區,一定要提前預防根治代碼疾病,不要在代碼已經無可救藥時才想到重構。

技術發展迅速,代碼總有過時的一天,所以經常對代碼有目的有計劃的小幅優化是非常有意義的。

關於本文有什麽問題可以在下面留言交流,如果您覺得本文對您有幫助,可以點擊下面的 推薦 支持一下我,博客一直在更新,歡迎 關註

代碼遷移之旅(二)- 漸進式遷移方案