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

742 阅读10分钟

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

最近刚刚完成 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 客户端向 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 格式,如:

# 简单思考

## 卷首语

- 商业的本质就是“持续提供用户真正想要的东西”,除此无他。

- 召集具备回应用户需求的热情与能力的员工,并为他们营造出无拘无束可最大限度地发挥其才能的环境,除此无他。

## 第一章 经商不是“打仗”

- 重要的是不断磨炼对“大众真实需求”的感知能力和使之实体化的技术。

- 音乐和体育不同,不用与任何人战斗。

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

<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 格式,舒爽。

import markdown
html_str = markdown.markdown(markdown_str)

对于最终生成的静态网站,像 css/js 等部分都是一样的,只是页面标题、正文等内容性的东西不同。于是,使用 Jiaja 模式表示这些通用部分,并 {{ title }} 这样的标注符表示各个分享所不同的内容部分;再用 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) 😃 最终方案

如何实现 全国秒开、及更多详尽分析,尽在「自在开发」公众号:

自在开发


3.4) 搜索引擎优化

4) 网页的适配

5) 其他

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

5.1) 服务监控

5.2) 一定要有日志

5.3) 一定要有备份

6) 事务诸葛亮