配合Rails和PostgreSQL实现站点对象存储服务的更换
这篇文章主要记录最近博客站点的迁移情况,如何在Rails以及PostgreSQL的帮助下成功把原来托管在AWS上的图片资源都迁移到阿里云的OSS平台,以替换相关的存储服务。
场景
本次的迁移对象是本人的博客站点,身边的朋友都吐槽访问速度太慢了。原因主要是站点的服务器使用了Vultr的日本节点,而里面的图片我全都托管在AWS的S3服务上。两者的线路对于中国的运营商来说都很糟糕,最后还是决定把服务器迁移到阿里云的新加坡节点(国内的机子会更快一些,然而真的不想停站备案了),静态文件就托管在阿里云的OSS服务上。
云服务的迁移相对比较容易,由于博客本身的图片资源都是托管在第三方服务商,数据库只记录了链接,因此只需要换台机器部署网站,把最新的数据导出并迁移到新的数据库中,最后切换域名即可。现在访问应该是快很多了。不过图片资源的迁移会麻烦一些,因为文章都是Markdown格式,里面的图片都是以外链的形式存储,目前的解决方案步骤如下
- 从文章中爬取出所有的静态资源链接(主要是图片),存储下来。
- 采用Ruby脚本把这些图片下载下来并重新上传到最新的第三方托管服务中(阿里云的OSS服务)。
- 上传成功之后把新的URL保存下来,做成一个“旧URL”跟“新URL”的映射表,这个表也会存储文章的对应id。
- 使用脚本或者数据库语句把文章中所有的“旧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的恐惧,所幸迁移还算成功。