Framist's Little House

◇ 自顶而下 - 面向未来 ◇

0%

十分钟睡前开发 —— URL Shortener by Shuttle

本文翻译自 URL Shortener - Shuttle ,有少许修改。当个小软文看吧

现在,星期三晚上,我试图在入睡 —— 我看了下手机,现在是凌晨 2 点 54 分。当我意识到我将无法再次获得超过 5 小时的睡眠时,一种恐惧感笼罩着我。

我做软件开发人员已经将近 10 年了,我是怎么走来的?

我的部署在晚上十点崩溃了(因为我从不学习),我不得不在接下来的 3 小时内应对我们的基础设施和巨大的压力。当我坐在那里反思人生选择时,我有了一个想法。我们能做得更好吗?

我不想在午夜处理 Terraform 和 Kubernetes。我在部署的同时编写可扩展的代码。我希望为我的依赖会帮我自动生成和管理。我希望晚上能够入睡。

现在是 2022 年。当然,我们可以做得更好。当我茫然地盯着我的白色天花板时,我决定看看这是否可能。

我突然在床上坐了起来。我是否可以创建一个有用的应用程序,具有某种数据库状态,一个自定义子域,只关注我的应用程序代码,而不需要担心基础设施,并在 10 分钟或更短的时间内完成它?我会写一个 URL 缩短器什么的。我会用 Rust 写。我今晚会写它。

设计

我下了床,打开了办公室的灯。我坐在符合人体工程学的椅子上,打开了一个滑稽的大曲面显示器。Arch 启动!我赶紧在 Signal 上给我的一个朋友发消息,提醒他们我用的是 Arch。现在我已经准备好编码了。让我们来构建这个东西。

API 将很简单。没有理由使用 GUI 或类似的东西 - 我是工程师,因此我说服自己 UI 在 1970 年代的电传打字机中达到了顶峰。

生命是短暂的,所以我要构建一个 HTTP API。我能想到的最简单的事情。

您可以像这样缩短 URL:

1
2
curl -X POST -d 'https://www.google.com' https://myapp.com
https://myapp.com/uvAivJ

你被重定向如下:

1
2
3
curl https://myapp.com/uvAivJ
< HTTP/2 301
...

是的,那会起作用的。

接下来,我需要某种数据库来存储 URL。我曾短暂考虑过在不需要数据库状态的情况下使用双射压缩方案,但让我们面对现实吧,我不太确定什么是双射,而且已经是凌晨 3 点 02 分了。

我将只获得一个具有基本架构的 Postgres 实例:

1
2
3
4
CREATE TABLE urls (
id VARCHAR(6) PRIMARY KEY,
url VARCHAR NOT NULL
);

天才。

我将添加一个索引或其他东西,以便数据库不会对每个请求进行线性搜索。没有人会真正使用它 - 但我已经可以感觉到任何碰巧浏览我的源代码的人的判断。我需要能够向人们解释,你可以在恒定的时间内搜索网址,这意味着我理解复杂性理论。

好的,我准备好了。现在是凌晨 3 点 05 分。我有 10 分钟。我拿起我的黑色电子烟,受到了很大的打击。烟雾弥漫在房间里,我再也看不见屏幕了。无论什么。我试着掰开我的手指和脖子以获得一些戏剧性的天赋,失败了,然后打开了一个终端。

构建 Barebones - 剩余 09:59

我正在为这个项目使用 Shuttle。这是一个为 Rust 构建的无服务器平台,我不必处理配置数据库、子域或任何类似的问题。我已经安装了 CLI。

我停下来想一想 - 我想使用哪个 Web 框架?我想是 Rocket。它几乎已经准备好使用一个甜蜜的 API 进行生产,而且我对它相当精通。

要使用样板初始化火箭项目,我可以使用 Shuttle CLI init 命令:

1
cargo shuttle init --template rocket url-shortener

这给我留下了以下 main.rs 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[macro_use]
extern crate rocket;

#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}

#[shuttle_runtime::main]
async fn rocket() -> shuttle_rocket::ShuttleRocket {
let rocket = rocket::build().mount("/hello", routes![index]);

Ok(rocket.into())
}

正如你所看到的,这导入了一些依赖项。幸运的是,该 init 命令也处理了这个问题,给我们留下了以下内容 Cargo.toml

1
2
3
4
5
6
7
8
9
10
[package]
name = "url-shortener"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "0.5.0-rc.2"
shuttle-rocket = { version = "0.30.0" }
shuttle-runtime = { version = "0.30.0" }
tokio = { version = "1.26.0" }

init 命令还为我们创建了一个新的 Shuttle 项目。这将在 Shuttle 的基础结构中启动一个隔离的容器。

现在就可以部署:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ cargo shuttle deploy
Packaging url-shortener v0.1.0 (/private/shuttle/examples/url-shortener)
Archiving Cargo.toml
Archiving Cargo.toml.orig
Archiving src/main.rs
Compiling tracing-attributes v0.1.20
Compiling tokio-util v0.6.9
Compiling multer v2.0.2
Compiling hyper v0.14.18
Compiling rocket_http v0.5.0-rc.1
Compiling rocket_codegen v0.5.0-rc.1
Compiling rocket v0.5.0-rc.1
Compiling shuttle-service v0.15.0
Compiling url-shortener v0.1.0 (/opt/shuttle/crates/url-shortener)
Finished dev [unoptimized + debuginfo] target(s) in 1m 01s

Project: url-shortener
Deployment Id: 3d08ac34-ad63-41c1-836b-99afdc90af9f
Deployment Status: DEPLOYED
Host: url-shortener.shuttleapp.rs
Created At: 2022-04-13 03:07:34.412602556 UTC

还行。。。这似乎有点太容易了,让我们看看它是否有效。

1
2
$ curl -X https://url-shortener.shuttleapp.rs/hello
Hello, world!

嗯,还不错。我又给自己倒了一杯……

添加 Postgres - 剩余 07:03

这是我旅程中我通常会有点慌张的部分。我以前设置过数据库,但这总是很痛苦。您需要预配 VM,确保存储不是临时的,安装并启动数据库,创建具有正确权限和安全密码的帐户,将密码存储在 CI 中的某种机密管理器中,将 IP 地址和 VM 的 IP 地址添加到可接受的主机列表中等。哎呀,这听起来像是很多工作。

Shuttle 为你做了很多这样的事情——我只是不记得是怎么做的。我快速转到文档中的 Shuttle 共享数据库 部分。我在 Cargo.toml 添加了 sqlx 依赖,并更改了 main.rs 中的一行:

1
2
3
4
#[shuttle_runtime::main]
async fn rocket(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleRocket {
// [...]
}

通过向 rocket 主函数添加参数,Shuttle 将自动为您预置 Postgres 数据库,创建一个帐户并返回一个经过身份验证的连接池,该连接池可从您的应用程序代码中使用。

让我们部署它,看看会发生什么:

1
2
3
4
5
6
7
8
9
10
cargo shuttle deploy
...
Finished dev [unoptimized + debuginfo] target(s) in 19.50s

Project: url-shortener
Deployment Id: 538e41cf-44a9-4158-94f1-3760b42619a3
Deployment Status: DEPLOYED
Host: url-shortener.shuttleapp.rs
Created At: 2022-04-13 03:08:30.412602556 UTC
Database URI: postgres://***:***@pg.shuttle.rs/db-url-shortener

我有一个数据库!我忍不住笑了一下。目前为止,一切都好。

设置 Schema - 剩余 06:30

配置的 Shuttle 数据库是完全空的 - 我需要连接到 Postgres 并自己创建架构,或者编写某种代码来自动执行迁移。当我开始思考这个看似存在的问题时,我决定不去想太多。我只想用最简单的方法。

我使用提供的数据库 URI 使用 pgAdmin 连接到 Shuttle 配置的数据库,并运行以下脚本:

1
2
3
4
CREATE TABLE urls (
id VARCHAR(6) PRIMARY KEY,
url VARCHAR NOT NULL
);

当我准备在谷歌上搜索“postgres 如何创建索引”时,我意识到由于用于 url 查找的 id 的是主键,这是一个隐含的“唯一”约束,Postgres 会为我创建索引。Cool。

编写 Endpoints - 剩余 05:17

​应用将需要两个 endpoints - 一个用于 URL,另一个用于 shorten 检索 URL 和 redirect 用户。

在考虑实际实现时,我快速为 endpoints 创建了两个实现:

1
2
3
4
5
6
7
8
9
#[get("/<id>")]
async fn redirect(id: String, pool: &State<PgPool>) -> Result<Redirect, Status> {
unimplemented!()
}

#[post("/", data = "<url>")]
async fn shorten(url: String, pool: &State<PgPool>) -> Result<String, Status> {
unimplemented!()
}

我决定从缩短方法开始。我能想到的最简单的实现是使用 nanoid crate 动态生成一个唯一的 id,然后运行一个 INSERT 语句。嗯 - 重复呢?我决定不去想太多🤷.

1
2
3
4
5
6
7
8
9
10
11
12
#[post("/", data = "<url>")]
async fn shorten(url: String, pool: &State<PgPool>) -> Result<String, Status> {
let id = &nanoid::nanoid!(6);
let p_url = Url::parse(&url).map_err(|_| Status::UnprocessableEntity)?;
sqlx::query("INSERT INTO urls(id, url) VALUES ($1, $2)")
.bind(id)
.bind(p_url.as_str())
.execute(&**pool)
.await
.map_err(|_| Status::InternalServerError)?;
Ok(format!("https://url-shortener.shuttleapp.rs/{id}"))
}

接下来,我本着类似的精神实现了该 redirect 方法。在这一点上,我开始恐慌,因为它真的接近 10 分钟大关。我将执行一个 SELECT * 操作并提取与查询 ID 匹配的第一个 URL。如果 id 不存在,则返回:404

1
2
3
4
5
6
7
8
9
10
11
12
#[get("/<id>")]
async fn redirect(id: String, pool: &State<PgPool>) -> Result<Redirect, Status> {
let url: (String,) = sqlx::query_as("SELECT url FROM urls WHERE id = $1")
.bind(id)
.fetch_one(&**pool)
.await
.map_err(|e| match e {
Error::RowNotFound => Status::NotFound,
_ => Status::InternalServerError
})?;
Ok(Redirect::to(url.0))
}

哎呀,SQL 查询中有一个拼写错误。

在我修复了我的错别字并通过让我的 IDE 为我完成繁重的工作来整理各种未解决的依赖关系之后,我最后一次部署到了 Shuttle。

关键时刻 - 剩余 00:25

我感觉自己像一个在不可能完成的任务中的冒牌阿汤哥,我专心致志地盯着倒计时的时钟,因为 Shuttle 部署了我的 url 缩短器。19.3 秒,我们就开始了。DEPLOYED 对话框一出现,我就立即测试了一下:

1
2
$ curl -X POST -d "https://google.com" https://url-shortener.shuttleapp.rs
https://s.shuttleapp.rs/XDlrTB⏎

然后,我将缩短的 URL 复制/粘贴到我的浏览器中,瞧,它被重定向到 Google。

我成功了。

回顾 - 剩余 00:00

我松了一口气,把自己从办公桌上推了回来。我重新装满了杯子,拿起它,走向我废弃的阳台。当我推开窗户,冷空气流进我的公寓时,我向前走了两步,把胳膊肘和杯子放在栏杆上。
我坐在那里思考了一会儿刚刚发生的事情。我成功了。我成功地快速构建了一个有点微不足道的应用程序,而不需要担心配置数据库或网络以及诸如此类。

但这在现实世界中如何衡量呢?真正的软件工程是复杂的,涉及具有不同技能组合的不同团队之间的协作。整个软件世界几乎无法将其保持在一起。用一种完全不必处理基础设施的新范式取代我们现有的、久经考验的云范式真的可行吗?我可以肯定的是,今晚我不会深入了解这个问题。

当我回到卧室,再次躺在床上时,我注意到我在咧嘴笑。我们真的有机会做得更好。也许我们还没有完全到达那里,但我今晚的经历给了我某种乐观,我们并没有像我曾经想象的那么远。怀着对明天更光明的承诺,我侧过身睡着了。