这篇文章主要记录最近博客站点的迁移情况,如何在Rails以及PostgreSQL的帮助下成功把原来托管在AWS上的图片资源都迁移到阿里云的OSS平台,以替换相关的存储服务。

hacker

场景

本次的迁移对象是本人的博客站点,身边的朋友都吐槽访问速度太慢了。原因主要是站点的服务器使用了Vultr的日本节点,而里面的图片我全都托管在AWS的S3服务上。两者的线路对于中国的运营商来说都很糟糕,最后还是决定把服务器迁移到阿里云的新加坡节点(国内的机子会更快一些,然而真的不想停站备案了),静态文件就托管在阿里云的OSS服务上。

云服务的迁移相对比较容易,由于博客本身的图片资源都是托管在第三方服务商,数据库只记录了链接,因此只需要换台机器部署网站,把最新的数据导出并迁移到新的数据库中,最后切换域名即可。现在访问应该是快很多了。不过图片资源的迁移会麻烦一些,因为文章都是Markdown格式,里面的图片都是以外链的形式存储,目前的解决方案步骤如下

  1. 从文章中爬取出所有的静态资源链接(主要是图片),存储下来。
  2. 采用Ruby脚本把这些图片下载下来并重新上传到最新的第三方托管服务中(阿里云的OSS服务)。
  3. 上传成功之后把新的URL保存下来,做成一个“旧URL”跟“新URL”的映射表,这个表也会存储文章的对应id。
  4. 使用脚本或者数据库语句把文章中所有的“旧URL”替换成“新URL”,迁移就算完成了。

PS: 由于近期笔者在努力克服对PostgreSQL的恐惧,所以会尽可能使用它,只有在不得不依赖Rails的情况下才使用Ruby脚本作为辅助。

爬取出所有图片资源链接,并保存

我使用PostgreSQL支持的正则表达式实现这一点,由于本人的博客文章都以Markdown的形式存储,于是里面的图片的存储格式都是![可能包含空格的标题](不包含空格的外链)一个匹配的正则表达式大概是这样的

/!\[([^\]]+)\]\((http[^)]*)\)/

在irb里面测试一下

> result = /!\[([^\]]+)\]\((http[^)]*)\)/.match('![松本行  ](https://lanzhiheng.com/matz.png)')
=> #<MatchData "![松本行  ](https://lanzhiheng.com/matz.png)" 1:"松本行  " 2:"https://lanzhiheng.com/matz.p...

> result[1]
=> "松本行  "

> result[2]
=> "https://lanzhiheng.com/matz.png"

接下来可以用下列查询语句把数据库中posts表里面相关的url记录查询出来,并使用SELECT .. INTO语句把查询结果写入到一个映射表中,方便后期处理

> SELECT id, title, (regexp_matches(body, '!\[([^\]]+)\]\((http[^)]*)\)', 'g'))[1] AS description, (regexp_matches(body, '!\[([^\]]+)\]\((http[^)]*)\)', 'g'))[2] AS url, NULL AS new_url INTO map_url_table FROM posts;

SELECT 65

图片外链数据总共有65条,都被筛选出来写入到map_url_table表里了。新表里面的数据大概是这样的

> SELECT id, title, description, url::varchar(20) || '...' AS url, new_url  FROM map_url_table;

 id |                 title                 |            description             |           url           | new_url
----+---------------------------------------+------------------------------------+-------------------------+---------
  3 | 浅谈Ruby中的生成器                    | 松本行弘                           | https://lanzhiheng.s... |
  2 | ActiveSupport::Concern源码分析        | Ruby On Rails                      | https://lanzhiheng.s... |

这里会预留一个new_url列,并全部设置成NULL值。主要是为了记录后续得到的新外链,以做成“新-旧URL”映射表。

上传脚本-上传资源并保存新的链接到中间表

第2,3两个步骤我做到一起了,这样会比较简单。把资源上传到新服务商的操作单纯用PG似乎无法很好地完成,这里还是用Rails的Rake Task来做比较好。笔者用的是ActiveStorage,这里就不详细记录它的配置的,在它封装的基础上切换服务商是如此的简单。可以直接使用它的上传函数来上传图片,流程大概如下

require "addressable/uri"
require 'open-uri'
require 'rainbow'

namespace :oss do
  desc 'Upload all url'
  task upload_all_url: [:environment] do
    conn = ActiveRecord::Base.connection
    results = conn.execute('SELECT * FROM map_url_table WHERE new_url IS NULL') # 只有那些还没有新外链的记录需要重新上传,后期重试会比较有用

    results.each do |item|
      old_url = item['url']
      desc = item['description']
      begin
        io = URI.open(Addressable::URI.encode(old_url))
        blob = ActiveStorage::Blob.create_and_upload!(io: io, filename: desc)
        new_url = blob.service_url
        conn.execute("UPDATE map_url_table SET new_url = '#{new_url}' WHERE url = '#{old_url}'")
        print Rainbow('.').green
      rescue OpenSSL::SSL::SSLError, URI::InvalidURIError
        puts Rainbow("The #{old_url} could not connect").red
      end
    end
  end
end

至于为什么使用Addressable可参考这个issue。正常情况下上传结果应该会全绿,就算有报错,再运行一次就行,成功之后new_url字段都会有对应的值了

                                          new_url
-------------------------------------------------------------------------------------------
 https://step-by-step.oss-cn-beijing.aliyuncs.com/production/38ranpg4zkvi8t8thvkf3jzcqqhz
 ...

正常来说active_storage_blobs表中也会有等量的数据。

> select COUNT(*) from active_storage_blobs;
 count
-------
    65

把数据库中的“老外链”替换成“新外链”

OK。一切如期进行,接下来要做的就是替换掉数据库里面的老数据了,用PG来试试看。为了遍历map_url_table表里面的所有数据,并依次对URL进行替换,我用PL/pgSQL来写,以下是替换脚本

CREATE OR REPLACE FUNCTION replace_all_urls_in_posts_body() RETURNS integer AS $$
DECLARE
    item RECORD;
BEGIN
    FOR item IN
       SELECT id, url, new_url FROM map_url_table
    LOOP

    EXECUTE format('UPDATE posts SET body=REPLACE(body, %L, %L) WHERE id=%s', item.url, item.new_url, item.id);
    RAISE NOTICE 'Replace url: % To new_url: %', item.url, item.new_url;
    END LOOP;

    RETURN 1;
END;
$$ LANGUAGE plpgsql;
select replace_all_urls_in_posts_body();

检验结果

> SELECT COUNT(*) FROM (SELECT (regexp_matches(body, '!\[([^\]]+)\]\((http[^)]*)\)', 'g'))[2] AS url FROM posts) AS sub WHERE url ~ 'oss';

 count
-------
    65
(1 row)
> SELECT (regexp_matches(body, '!\[([^\]]+)\]\((http[^)]*)\)', 'g'))[2] AS url FROM posts;

                                           url
------------------------------------------------------------------------------------------
 https://step-by-step.oss-cn-beijing.aliyuncs.com/production/g57llp0gdlrc4dqqeshnmz26kga6
 https://step-by-step.oss-cn-beijing.aliyuncs.com/production/2k9419tg633pijrjsi4mefob3z9w
 ...

可见所有的数据(总计65条外链)都被替换成阿里云的OSS服务路径了,本次迁移也能告一段落。这下总算可以删除AWS服务的相关代码及依赖,并且再也不用忍受缓慢的图片加载速度了。最后还要删除无用的中间表map_url_table

DROP TABLE map_url_table;

尾声

这篇文章主要记录一下近期服务迁移的情况,对象存储服务的迁移要比一般服务的迁移要麻烦一些。因为有许多资源都要重新上传,这是一个耗时的过程。而且如果外链是保存在数据库中的话,上传完成之后还需要更新数据库中的外链数据。本次迁移如果单纯用Rails来做会简单许多,强行上PostgreSQL也是想借此熟悉一下SQL的相关操作,摆脱一直以来对SQL的恐惧,所幸迁移还算成功。