0620 - 关于 Klib 分享,各种纠结之后,我选了如下方案

技术方案选型是件很有意思的事,各个环节都有各种选择,可以组合出各种可能。在这些可能性中,挑选出最佳方案,是我很喜欢做的事

最近刚刚完成 Klib 的标注分享,趁着热乎劲,小结一下:过程中纠结了哪些方案,以后最后选择了什么。

0) 先来看看最终效果

这就是 Klib 分享标注的操作流程:点击分享,立即得到可以全球访问的网页。操作不能更简单,背后的技术逻辑却很复杂:

实际的开发是混在一起的、思路也是交叉的,不过,为了介绍方便,我大致按照数据流来推演。

1) Klib 与接口服务器

这部分的功能比较直接:Klib 将标注内容发送给接口服务器,服务器处理完后返回结果。

需要介绍的,倒是功能之外的东西:

  • 如何防止接口被攻击
  • 如何做身份识别

这部分内容其实是很复杂的,我最终采用了和 Klib 安全性相称的方案。

1.0) 防止接口被攻击

1.0.0) 接口服务器使用 https

这是最基础、但非常有效的方式,全程使用 https 加密,已经可以大大提高安全性。

1.0.1) 防止接口被非法使用

如果接口是公开的、所有人都可以任意访问,就可以随意地向服务器丢垃圾数据,迅速将服务器挤爆。

比如好的做法是 使用非对称加密,即使用一对私钥、公钥,使用 私钥加密 的数据,只能使用 公钥解密;反之,使用 公钥加密 的数据,只能使用 私钥解密。整体流程大致如下:

  • 接口服务器开放 公钥 A
  • 每个 Klib 客户端生成新的 私钥 B公钥 B
  • Klib 客户端使用 公钥 A 加密 公钥 B,并将其发送给接口服务器
  • 接口服务器使用 私钥 A 解密后,存储该客户端对应的 公钥 B
  • 之后,Klib 客户端发送数据时,使用 私钥 B 加密,接口服务器收到后使用 公钥 B 解密,并用 公钥 B 加密后返回数据

听起来有点像绕口令?

开发上也有点麻烦,毕竟服务器还要保存每个 Klib 客户端对应的公钥。如果有多个服务器,则需要在不同服务器间同步公钥,更加麻烦。对于我这个小产品 + 实验功能来说,暂时不需要这么高的安全级别。

于是,我采用了更简单、但够用的 AES 对称加密。即 Klib 客户端和接口服务器使用相同的 AES 加密方法、同一个密码,加密请求和响应的数据;如果不能提供正确的加密,就无法使用服务器接口。

这一方案主要的风险是:黑客可以反编译 Klib 得到密码。除了 Klib 本身会编译并签名,我还在代码里加密存储密码。基本上除了跟我有八辈子解不开的愁,99.9999% 的人是不会花精力来破解这个密码的。

1.0.2) 使用时间戳 + MD5

即使加密过的数据,最终也只是表现为一个 http 请求,而这个请求是可能被本地拦截,进而用于模拟正常用户请求。

对应的防护是,在 http 请求中加入时间戳,并对 http 头的内容部分计算 MD5(或 CRC 等),服务器端进行验证,就可保证 http 头不被滥用。

其实,这是 OAuth 的范畴。好在,我在开发 图床神器 iPic 时,先后从客户端的角度实现了七牛、又拍、阿里云、Imgur、Flickr、Amazon S3 的 OAuth,这次实现一个简单的服务器端部分,也不算麻烦。

1.1) 如何做身份识别

上面说的是在面对黑客时的防护,听着有点晕是吧?下面来说说正常情况下的身份识别。

比如:如果用户尝试停止一个分享,如何判断该用户是否有权限?

如果有账户系统,这点比较容易解决。而 Klib 尚未引用账户系统,怎么办呢?比较高级的是使用区块链(咳咳),我目前的做法是:用户使用 Klib 分享一本书的标注时,服务器会返回一个随机数。下次用户在停止分享时,只要能提供这个随机数,即判定为有效请求。在上述各种防护的前提下,可以有效地防止被恶意停止分享。

2) 接口服务器

接口服务器是整个系统中最复杂的部分,它的职责比较多:

  • 验证请求,并接收数据
  • 存储数据
  • 根据数据生成静态网页
  • 将静态网页输送给静态服务器
  • 更新、删除分享时,更新数据存储和静态服务器

验证请求和前面的介绍是对应的,这里略过不表。

2.0) 使用 Python + Flask 实现功能部分

所谓接口服务器,首先就是要开放接口(开门接客)具体的,就是 http 请求的路由表。比如,当 Klib 客户端向 https://api.klib.me/share 发送数据时,要有相应的代码来接收处理这个请求。

在之前的文章 我入门 Python 后总结的基础教程 中,我已经介绍了使用 Flask 框架,这里不再重复。

2.1) 使用 Nginx + Gunicorn 搭建服务器

同上,请参考 我入门 Python 后总结的基础教程

另外,使用 Supervisorctl 保证服务可靠运行。

2.2) 使用 MySQL + SQLAlchemy 存储数据

从数据存储的角度看,书的标注都是很规整的,无非是书名、作者、笔记内容等等。于是我选择了最常用的关系型数据库:MySQL

如果直接使用 SQL 语句操作数据库,既繁琐又不安全,这里我使用可称为 ORM (Object Relational Mapping) 界事实标准的 SQLAlchemy 构建 Model、操作数据库。

我本来想说「这没什么好介绍的」,但实际上,MySQL 的坑很多。比如,如果要支持 Emoji 表情,就要全程使用 utf8mb4 编码。还有很多其他的坑,此处略去一万字…

2.3) 使用 Jinja 模板生成静态网页

关于标注部分,Klib 发送的是 Markdown 格式,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 简单思考
## 卷首语
- 商业的本质就是“持续提供用户真正想要的东西”,除此无他。
- 召集具备回应用户需求的热情与能力的员工,并为他们营造出无拘无束可最大限度地发挥其才能的环境,除此无他。
## 第一章 经商不是“打仗”
- 重要的是不断磨炼对“大众真实需求”的感知能力和使之实体化的技术。
- 音乐和体育不同,不用与任何人战斗。

需要使用 markdown 模式将其转换成 html 格式,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<h1>简单思考</h1>
<h2>卷首语</h2>
<ul>
<li>
<p>商业的本质就是“持续提供用户真正想要的东西”,除此无他。</p>
</li>
<li>
<p>召集具备回应用户需求的热情与能力的员工,并为他们营造出无拘无束可最大限度地发挥其才能的环境,除此无他。</p>
</li>
</ul>
<h2>第一章 经商不是“打仗”</h2>
<ul>
<li>
<p>重要的是不断磨炼对“大众真实需求”的感知能力和使之实体化的技术。</p>
</li>
<li>
<p>音乐和体育不同,不用与任何人战斗。</p>
</li>
</ul>

这里赞叹一下:Python 轮子就是多。只需轻轻地导入 markdown 模块,即可优雅地将 Markdown 转换为 html 格式,舒爽。

1
2
import markdown
html_str = markdown.markdown(markdown_str)

对于最终生成的静态网站,像 css/js 等部分都是一样的,只是页面标题、正文等内容性的东西不同。于是,使用 Jiaja 模式表示这些通用部分,并 0620 - 关于 Klib 分享,各种纠结之后,我选了如下方案 这样的标注符表示各个分享所不同的内容部分;再用 render_template 方法替换模板中的内容,即可生成对应的静态文件。

感叹:这样简洁直接的操作、无需各种复杂的配置,就能得到最后想要的东西,真真是编程中最可爱的环节

3) 静态服务器与 CDN

有了静态服务器,就像是有了宝贝,不能只是自己藏着,得拿出来让大家瞧瞧,这就是静态 (Web) 服务器要干的事。

当然,静态服务器和接口服务器,在物理上可以是同一台服务器,这里只是从角色上进行区分。

在展示静态网页方面,技术选型上主要有 2 方面的需求:

  • 网页内容能实时更新
  • 用户访问速度快

其中,内容的更新对应用户分享时的 3 种操作:

  • 创建分享
    • 对应创建静态 html 文件
  • 更新分享的内容
    • 对应更新 html 文件内容
  • 停止分享
    • 对应删除 html 文件

好,带着「实时」创建、更新、删除 html 文件这 3 个需求,我们来看看如何提高访问速度

3.0) 😔 仅使用单一服务器

首先,如何什么都不做,意味全球的用户(Klib 必须是国际性产品,得考虑全球用户,嗯)都要连接这台服务器。

且不说并发数等限制,单从网速上看,如果将服务器放在国内,国外用户势必慢;反之亦然。更何况国内还是电信、网通、以及神奇的长宽,国外也有 N 多国家。

如果确实要这么做,比较好的方案是使用 阿里云香港服务器,可以兼顾国内国外用户。暂时,不采用这一方案,每月省下 $19…

3.1) 😔 CDN

进而,通常的做法是使用 CDN.

CDN 确实可以有效提高不同地区、不同网络环境下的访问速度,且极大地降低对静态服务器的压力。不过,CDN 有个致命的局限:内容更新慢。尤其在更新、删除内容时,这种慢会带来业务上的问题

比如,用户在 Klib 中分享标注后又停止,却发现之前产生的网页依然可以访问,用户会觉得这是 Bug,进而会带来很大的客服压力。于是,跳过这一方案。

3.2) 😔 国内、国外多台服务器

下一方案是:国内、国外各一台(或多台)服务器,通过 DNS 服务器进行分流,相当于自建 CDN。

不料,却遇到一个坑:国内服务器的外网速度普遍较慢。比如我试了阿里云上海节点,从国外服务器使用 scp 或 rsync 传输一个 10 KB 的文件需要 4s,跌破了我的眼镜。并且,阿里云我也只买了 1 MB 带宽的小水管,并发时速度会很慢。于是,这一方案也被放弃。

3.3) 😃 最终方案

最终采用的方案时:国内使用阿里云 OSS、国外使用 Amazon S3(注:因为测速显示,我的国外服务器在全球的访问速度尚可,暂未实施,不过原理是一样的)

  • 速度 方面,测试软件显示,阿里云 OSS,在国内的访问速度是不错的。
  • 可靠性 方面,阿里云 OSS 本身肯定值得依赖的(至少比我自己搭静态服务器靠谱的多)
    • 另外,阿里云 OSS 可以从我的国外静态服务器回源,意味着用户通过阿里云 OSS 访问网站时,肯定可以得到内容,这已经满足了 90% 的场景
  • 实时更新 方面,服务器端使用 pyinotify 监控 html 文件所在目录,一旦发现有文件更新、删除,立即同步至阿里云 OSS 上。同步速度也不错(比阿里云 ECS 强太多),用户的响应几乎是实时的

比如,你访问这篇分享试试速度如何:
http://s.klib.me/share.html

以及 全国测速结果,打开时间基本在 1s 内,满意。

注:我另外也考虑了七牛云,不过 七牛不支持为存储空间绑定域名、而只支持 CDN 的方式绑定域名,而 CDN 的方案已经被放弃,所以只能放弃七牛(以及七牛的免费 http 流量…)

3.4) 搜索引擎优化

这是个全方面的话题,这里只提几点:

  • 搜索引擎直接访问静态服务器,因为 CDN 等因素对搜索引擎是种干扰。这一点,可以通过 DNS 服务器来解决。

  • 向搜索引擎提供 sitemap 文件,包含静态网页的网址列表。虽然搜索引擎不一定真的使用这个文件,但必要的基本功还是要做的,万一被用了呢?

4) 网页的适配

好,经过千辛万苦,用户终于可以打开网址、看到分享内容了,是不是大功告成了?

错,还有很大一个坑:网页布局及适配

比如:

  • 作为文字类的网页,字体、字号、配色、行间距、留白得看得过去吧?
  • 用户可能在 24 寸显示器、iPad、手机等不同设备上打开网页,总得能正确显示吧?
  • 总得方便用户分享到不同 SNS 吧?
  • 用户分享到微信、朋友圈,总得显示个缩略图吧?
  • 用户分享到 Facebook、Twitter,总得以卡片形式显示、摘要总要有吧?

这方面我不是专家,Klib 的分享也只是做个入门。

在 Klib 中阅读效果类似:

PC 端最终效果类似:

手机端类似:

5) 其他

表一漏万,除了看得见的、说得出的,还有很多看不见的东西,挑几个来说。

5.0) 区分开发、测试、线上环境

如何在同一代码仓库下,区分开发、测试、线上环境?

我目前的做法是,创建排除在 Git 外的 .env 文件,其中存储了开发、测试、线上环境所对应的配置。程序启动时,先读取配置文件,然后根据配置文件启动对应的参数,如数据库地址、服务端口、日志级别及文件位置等等。

5.1) 服务监控

从上面的介绍可以看到,整体的框架涉及多个模式,并且互为备份的环节并不多,有一个模块出问题,就可以导致整个流程出问题。最好有个机器人一直盯着,一旦出问题,立即通知我(总比用户跑过来报 Bug 强)

如何能有效监控呢?几个供参考:

  • 接口服务器方面,一旦发现严重错误,立即发告警邮件(目前还没做)
  • 在接口服务器创建测试脚本,包含创建、更新、删除分享功能,并定期工作。一旦出问题,立即发告警邮件
  • 使用 360 监控等服务,监控某分享链接是否可访问
  • 以及,一些基础的服务器监控工具

5.2) 一定要有日志

日志可以说是线上服务的生命线,一旦出问题,第一件要做的事:查日志。那首先得有日志呀?恩,代码里要输出。

另外,日志文件要 定期切割,避免过大。

5.3) 一定要有备份

万一服务器被黑了、被人 rm / -rf 了,万一数据库被删了…作为独立开发者,我总不能申请破产保护吧?

备份一定要有(虽然最好不要用),比如:

  • 服务器级别的定期制作镜像,一般云服务器都有此功能
  • 制作脚本,定期将数据库、日志等关键数据,备份至 Dropbox、Amazon S3 等可靠的网络存储中,当然要加密保存
  • 使用 GitHub/GitLab 等私有 Git 仓库备份代码。什么都没了,只要有代码,就能东山再起

6) 事务诸葛亮

以上就是 Klib 分享大体的技术选型、以及期间的纠结。虽然已经能干活,现在回过头来看,主要的问题是:

涉及模块多,维护成本高

想象一下这个场景:

  • 在吃年夜饭时,收到不能创建分享的报警邮件
  • 一边后悔没有给服务器上香,一边放下筷子、打开 MBP
  • 查了半天,发现是 MySQL 的问题
  • 又查了半天,发现竟然是因为 VPS 硬盘空间不够了
  • 于是,添加新数据盘,小心翼翼地迁移数据,胆战心惊地重启数据库
  • 手动测试,持续观测日志一段时间,好在没有问题
  • 刚巧,新年的钟声敲响,完美…

相比之下,如果使用 AWS 这种能托管 MySQL 的服务,上述问题都不会发生,还不会错过微信红包里的几个亿。

再比如,使用 Heroku 之类的服务,就可以免去大部分需要自建服务器部分。没选 Heroku 主要的原因是:国内访问速度太慢。不过现在想想,可以通过国内、或香港服务器回调的方式加速。于是,Heroku 又成为很有优势的选择。

听完之后,你会怎么选?


我所说的,都是错的。

那么,我的公众号「自在开发」,你还关注吗?每周二早 8 点,技术长文、准时推送。

自在开发