背景参看:

代码的统一上线方案(1-原始篇)

上一篇:

代码的统一上线方案(2-短暂的过渡篇)


上一篇也是用技术手段来解决我上线代码到虚拟主机的一个痛点,是基于svn diff + ftp单文件上传替换的方式完成的,能够在一定程度上解决问题,但是留下了一个巨大的隐患:部分文件尚未完成上线时,过程中的不稳定状态。

那么基于ftp的方案差不多就算是被否定了,单文件ftp不行,多文件ftp我又不能调用系统命令去解压缩。

最终,我想到了一个目前来看,颇为可行的方法。


方案说明

虚拟主机,看起来是只有一个开放接口——ftp,但是实际上,虚拟主机提供的服务是apache+php+mysql,所以最明显,也最容易被我们忽略的就是这个http接口了,也就是我们部署的网站代码本身。

所以最新的方案思路是这样子的:

部署一个zip压缩包,再部署一个接口文件,例如叫做deploy.php,然后通过curl调用deploy.php,对这个压缩包进行解压缩来上线。


思路清晰之后,就考虑一下细节的问题:


  1. 怎样解决前一种技术方案中的致命问题:上线过程中的不稳定态?
  2. 怎样通过ftp来部署zip和deploy接口?
  3. 除了解压缩来部署代码,上线过程中还有什么其他操作要进行呢?
  4. 上线和回滚,通过怎样的接口来触发执行?

ok,现在先来解决第一个关键问题:解决不稳定态。如果要避免不稳定态,就是要防止有的文件更新了,有的文件还留在旧版本;或者换句话说,直到所有文件都更换为最新版本之后,再通过一个基本不耗时的操作,实现整体部署版本的快速切换。


在之前,我部署文件的方式是类似于这样的:


网站根目录(例如叫做/public_html)

 -- index.php

 -- .htaccess

 -- 所有相关文件和资源,一坨文件夹和文件


访问的主页例如是http://xxx.com,实际上是访问了http://xxx.com/index.php,就是/public_html/index.php这个文件对请求进行了处理。

可是在这种部署方式下,很难做到版本切换,因为我只对/public_html这个目录下的文件和目录有权限进行操作,而对/没有操作权限。


所以第一步,我要解决这个问题,先把部署方式修改成以下这样:


网站根目录

 -- abc(随便取的名字)

      -- index.php

      -- .htaccess

      -- 所有相关文件和资源,一坨文件夹和文件


唯一的变化,是多了一层abc目录。

这样的话,abc这个目录就是一个整体,而且我们可以完全地进行操作。例如,我们上线一个版本叫做abc.new,这个里面部署的是新代码,然后只需要将abc改名为abc.old,然后把abc.new重命名为abc,就可以完成一次上线,两个重命名操作可以认为基本不耗时,这就解决了不稳定态的问题。


那么问题来了,多了一层abc目录,岂不是说,我们就得用http://xxx.com/abc或者是http://xxx.com/abc/index.php来访问我们的网站主页了?

可是这显然不符合我们的初衷,所以需要再通过.htaccess文件,将abc这层目录给隐藏掉。

即:



网站根目录

 -- .htaccess

 -- abc(随便取的名字)

      -- index.php

      -- .htaccess

      -- 所有相关文件和资源,一坨文件夹和文件


此处,网站根目录下的.htaccess内容为:


RewriteEngine on
RewriteCond %{HTTP_HOST} ^(www.)?xxx.com$
RewriteCond %{REQUEST_URI} !^/abc/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /abc/$1
RewriteCond %{HTTP_HOST} ^(www.)?xxx.com$
RewriteRule ^(/)?$ abc/index.php [L]


简单解释一下就是:如果访问的url中不是以/abc/开头(例如http://xxx.com/fff),并且不是一个真实存在的文件或目录,就将url重写为http://xxx.com/abc/fff这样的形式。



通过这个简单的.htaccess文件,就消除了多余的abc这层目录的影响,不管url中是否带有abc,都可以正常访问了。

至此,我们解决了最关键的第一个问题。


接下来,是运维方式的改进

前一种技术方案下,是在本地目录执行的,例如windows的cmd,或者mac的shell下执行命令,类似于php deploy.php --version 123:456 --conf module.conf这样。这一次,更新了技术方案之后,我不太想每次都执行一串命令了,所以就请出了ci神器——jenkins。


在文章在openshift上搭建专属jenkins中,我搭了一个jenkins的master,后来又买了个便宜的vps,连到上面成了一个执行任务的slave节点。

我在jenkins上建了个任务(实际上,我设置了两个任务,流水线执行),专门负责上线,例如从SVN上更新最新代码、打zip包并上传ftp、调用deploy.php进行上线的操作,全部封装在任务中。本地测试通过之后,在jenkins上发起上线的操作,就可以实现一键式上线。


jenkins任务执行的具体过程:


  • 通过Jenkins的Multi SCM插件,在slave机器上,从一个SVN上,更新最新的博客代码;从另一个SVN地址上,更新deploy.php文件
  • 将博客源代码中,所有的文件进行打包,例如叫做xx.zip
  • 通过Send files over FTP插件,将xx.zip和deploy.php文件,分别上传到博客所在的ftp主机上
  • 当前,博客代码部署如下

网站根目录


 -- .htaccess

 -- abc

 -- xx.zip

 -- deploy.php

  • 调用https://www.poisonbian.com/deploy.php进行发布



那么deploy.php这个上线脚本,在发布的过程中做了哪些事情?

例如,我的博客,调用deploy.php的方式类似这样:

curl "https://www.poisonbian.com/deploy.php?old=abc&new=abc-deploy-${BUILD_NUMBER}&zip=xx.zip&copy=dir1/file1,dir2&mkdirs=dir3/sub3,dir4/sub4&rm=unused1,unused2"


当然,我实际使用的时候,传入的参数肯定和这个是略有区别的。

另外,${BUILD_NUMBER}这个,是jenkins自带的环境变量,表示构建号,例如1,2,3,每次调用的时候会产生一个整数序号。比如下面就取这个BUILD_NUMBER为1好了。


deploy.php这个文件,我放一下关键的流程代码:


        $log = sprintf('old dir: %s, new dir: %s, zip: %s, copy files: %s, mkdirs: %s',
                        $old_dir, $new_dir, $new_zip, $copy_files, $mkdirs);
        $this->info($log);
        $this->unzip($new_dir . DIRECTORY_SEPARATOR . $new_zip, $new_dir);
        $this->mkdirs($mkdirs);
        $this->copy_files($old_dir, $new_dir, $copy_files);
        $this->rm_files($new_dir, $rms);
        $this->delete($new_dir . DIRECTORY_SEPARATOR . $new_zip);
        $this->rename($old_dir, $new_dir);
        $this->delete(__FILE__);

可以看到,步骤是这样子的:

  1. 解压缩,例如刚刚传入的参数,就会新建一个abc-deploy-1目录,然后将xx.zip中的文件,全部解压缩到abc-deploy-1目录中。这个时候,根目录的abc和abc-deploy-1目录,就分别是旧版本和新版本的部署根目录了。
  2. 在新目录abc-deploy-1中,根据mkdirs这个参数,新建文件夹。这里即在abc-deploy-1目录下,新建了dir3/sub3和dir4/sub4两个目录。
  3. 根据copy这个参数,将旧目录下对应的文件或目录,拷贝到新部署路径abc-deploy-1目录下。
  4. 根据rm这个参数,将新目录下对应的文件或目录删除掉。
  5. 删除abc-deploy-1目录下的xx.zip文件。
  6. 将abc和abc-deploy-1这两个目录分别改名,例如abc改名为abc-rollback-1,然后再将abc-deploy-1改名为abc
  7. 删除deploy.php这个文件本身。

看起来好像比一开始计划得稍微复杂了一些?

其实最基础的操作,就只有解压缩这一个步骤,其他的步骤都是为了解决一些零散的问题。


例如,上线的时候想保留一些data文件,就可以通过copy参数,把旧版本的数据原封不动地拷贝到新上线目录下;例如想创建运行时的文件Cache或者Temp目录,就可以通过mkdirs来做;至于rm参数,可以用来删除SVN目录中一些我们不希望部署上线的文件或目录,例如readme.txt之类。
最后,deploy.php这个文件本身,也被删除了。
这些参数调用虽然复杂了一些,但是我们固化在了jenkins的任务配置中,所以只需要触发任务执行,就能很快上线。至于回滚,我目前没有集成在任务里面,而是直接在ftp客户端里面改名,把abc改名成任意,再把备份的abc-rollback-1改成abc就可以。


未尽的任务


目前,我各个网站都是通过这样的方式来上线了,对我个人使用而言,基本功能肯定是满足需求了。当然,如果你追求完美,还是有一些事情可以做的。

例如对abc-rollback-1这样的文件夹做更多处理,设置一些安全措施,防止别人通过这样的路径来调用旧版本代码;在代码打包前,或者是上线后,通过一些自动化测试的手段,来验证上线的有效性;留下一个deploy.php的口子,可以直接通过界面点击的方式来快速回滚代码到旧版本;旧的备份根据个数或者时间进行清理,防止占用过大空间......


只能说,需求是无限的,有的时候,未必真的要做到极致,够用就好 : )