代码迁移之旅(二)- 渐进式迁移方案

说在前面

这是代码迁移的第二篇文章,也是最后一篇了,由于个人原因,原来的迁移我无法继续参与了,但完整的方案我已经准备好了,在测试环境也已经可以正常进行了。 上篇文章 代码重构之旅(一) 项目结构 介绍了迁移代码的前期准备和项目结构的设计,本篇文章来介绍一下可实施的迁移方案。

使代码的迁移过程更简单、更安全是我们要追求的目标,在迁移之前,代码的可用性我们一定也只能画一个问号。

文章欢迎转载,但请注明来源:https://cloud.tencent.com/developer/user/1148723/activities, 谢谢。

问题抽象分析

首先要看一下一次完整的迁移需要满足什么要求:

  • 灰度发布,谁也无法保证一次将整个系统迁移到另一个系统不会发生问题,而以接口或接口部分流量为单位进行迁移则可以大大提升可控性。
  • 客户端无感知,即迁移平滑,长时间的系统不可用是完全无法接受的。
  • 可回滚,一旦出现异常问题可以快速回滚,避免造成较大影响。
  • 易实现,尽量避免大量地操作,操作多意味着犯错的可能性更大,回滚的难度也大。

只有实现了以上要求,才算是一次成功的迁移。那么先分析一下目前的情况:

如上图是我们两个系统的目前状态:

  • 两个系统共享一个 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 率降到最低。

我们的系统对外提供服务都是通过接口,这也方便了我们进行测试。为了保证测试的完整性,可以将线上流量引入到新代码中进行测试,而实行请求导流的最好媒介就是日志。

一般来说,服务器都有完整的线上请求日志,如果有必要,在给特定接口添加特定日志以配合测试也是可以的。接入线上日志,构造跟线上一样的请求到测试服务器,再对比原始服务器的响应内容,将异常响应记录下来由开发分析并查找原因,直到最后新旧项目对所有请求的响应完全一致。

小结

项目的重构不是一个小事,特别是大规模的项目代码迁移,执行它必须胆大心细,但每一次重构,无论是对自己的技术能力还是项目的生命周期都是很大的提升。

虽然不鼓励没事就瞎折腾代码,但一定要时刻警惕,走出代码的舒适区,一定要提前预防根治代码疾病,不要在代码已经无可救药时才想到重构。

技术发展迅速,代码总有过时的一天,所以经常对代码有目的有计划的小幅优化是非常有意义的。

关于本文有什么问题可以在下面留言交流,如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我,一直在更新,欢迎 关注

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏葡萄城控件技术团队

HTML5 & CSS3初学者指南(3) – HTML5新特性

介绍 本文介绍了 HTML5 的一些新特性。主要包含以下几个方面: Web 存储 地理位置 拖放 服务器发送事件 Web存储 HTML5...

1878
来自专栏FreeBuf

对windows密码抓取神器mimikatz的逆向分析

mimikatz可谓获取windows明文密码神器,新版本更是加上了64位支持。用过一个小型获取明文密码程序,只有一个可执行文件ReadPSW.exe,通过逆向...

3908
来自专栏deepcc

linux中nodejs后台运行工具forever

2708
来自专栏nummy

cookie模块学习

输出是一个合法的Set-Cookie首部, 可以作为HTTP响应的一部分传递给客户端。

551
来自专栏FreeBuf

深入解析新型加密货币挖矿恶意软件ZombieBoy

延续了2018年加密货币挖矿恶意软件的趋势,我发现了另一种类似于5月初发现的“MassMine”的挖矿恶意软件。我把这个家族称为ZombieBoy,因为它使用了...

562
来自专栏FreeBuf

CSRF(跨站点请求伪造)在Flash中的利用

0x00 前言 CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session R...

3185
来自专栏QQ音乐技术团队的专栏

使用 Jest 进行前端单元测试

目前 Jest 已经在 Facebook 开源的 React, React Native 等前端项目中被做为标配测试框架。下面简单介绍一些 Jest 比较有用的...

5099
来自专栏枕边书

Linux - 请允许我静静地后台运行

前言 常在 linux 下玩耍的开发者肯定会经常遇到需要对进程调度的情况,在 windows 中点击 最小化 去干别的就 OK 了,那么在 linux 下怎么办...

1875
来自专栏我的小碗汤

浅谈json web token及应用

Json Web Token (JWT),是一个非常轻巧的规范,这个规范允许在网络应用环境间客户端和服务器间较安全的传递信息。该token被设计为紧凑且安全的,...

763
来自专栏信安之路

打造一款自动扫描全网漏洞的扫描器

在渗透测试中,扫描器必不可少,毕竟目标很多,需要检测点也很多,不可能全部手工搞定的,所以很多渗透者都有自己的自动化工具或者脚本,这里就为大家分享一款由我自己开发...

950

扫码关注云+社区