普通视图

发现新文章,点击刷新页面。
昨天以前Log4D

2023 年终总结

作者 alswl
2024年1月8日 23:38

flower

时间已做了选择,太多感受,绝非三言两语能形容

生活 - 陪伴和成长

boy

这是第 35 个年头,我熟悉地扮演着多个角色,父亲、丈夫、儿子,每一刻都在陪伴和成长中交织。 生活的步伐似乎匆匆,但我努力让自己拥有一颗年轻的心,渴望保持对世界的好奇和激情。

时光大多被生活所占据,只有地铁上和饭桌上我能成为时间主宰。 好在我并未感到疲惫或沉闷,反而逐渐适应了这个身份的变化。 或许,正是在这些琐碎的日常中,我找到了一种生活的节奏,一种平和而温馨的状态。

今年我们走过了北京、汉中、西安、淳安、长沙、张家界、台州。 新年即将到来,准备给孩子办理一下护照,走出去看看。

tower

在陪伴孩子的过程中,参与各种自然知识课程,参观各种展览,我发现在陪伴的同时,我们也在不知不觉中共同成长。 生活中的另一个领域,我从母亲那里薅了两只相机,终于决心好好学习摄像, 我把 Canon 6D 出售,保留了 SONY a6500 这支轻便的 APS-C 相机。 期望摄影成为我表达内心、记录生活的一种方式,每一张照片都是时光的凝固,是岁月的见证。

five

游戏的世界中,我似乎进入了一段电子阳痿期。购买的游戏几乎只能玩上一个小时就变得索然无味, 也许现实生活才是最引人入胜的游戏吧。

或许,人生就是一场不断变化的冒险。在时间的舞台上,我们扮演着各色角色, 演绎着属于自己的故事。

工作 - 精进

balloon

工作上一直压力和张力巨大,我开始进一步成为探索者。 这两年,我在工作中不断推动项目的上线,部门推出的新产品中的一半是我负责的,我很喜欢这个领域,也确实想把事情做好。

但有时候,我感觉自己有点像是公司招进来的清理工,身处于一个「散多垂」的状态, 面对复杂的环境,解决问题绝非易事。在整理垃圾的过程中还在自动产生垃圾,而清理的工作永远不会终结。 现实往往是,大家注意力持续被新事物(比如 AIGC)吸引走,对现存的问题更容易选择性忽视。

企业的大环境在不断变化,一些老朋友选择离开,大部门也经历了一些变革。 从面向风险的团队 re-org 到面向算力的基础设施团队。我认为这是一个好的信号, AI Infra将继续裹挟着整个 Infra 领域前进,算力管理将成为一个新的命题。

业余 - 更多连接

在 Github 数据的细碎图形中,映射出一年的自娱自乐,可惜的是,未给开源社区更多的贡献。

contributions

今年,我在开源领域主要的贡献是 alswl/excalidraw-collaboration。 这个 self-host 的 Excalidraw 版本集协作和中文化字体于一身。这个项目以及相关项目吸引了近300个 star,成为我个人最有影响力的开源项目之一,尽管它是一个前端产品。

excalidraw-collboration

在暑假期间,趁着家中小神兽不在,我开发了一个关于起名的小程序。虽然这款产品目前有点烂尾, 亏损严重,但我依然希望花更多时间进行开发和改进。一个美好的名字可以给家庭带来无限愉悦, 希望这个项目可以养活服务器资源~

另外,今年我重新活跃在 Twitter 上,分享一些技巧和心得。我的 Follower 从几百人增长到近 4000 人, 虽然离有影响力的推友还有差距,但与许多有趣的朋友交流本身就是一种有趣的事情。

今年一年最受欢迎的内容是:

  • 167k 转载:对抗软件复杂度的战争 X
  • 160k 英语学习经验介绍 X
  • 127k 介绍 Lightboard X
  • 120k Web 框架讨论 - Kratos X
  • 90k 介绍 dumi X

今年最具价值的文章是介绍「许世伟的架构课」X,赚了几个月 Twitter 的订阅费。

今年我还重新开始听播客,聆听了一大半「内核恐慌」的存档,虽然未能赶上他们活跃的时期。 幸运的是,在外滩大会上我有机会参与了他们的聚会,与 Rio 和吴涛面对面交流。搞笑的是,虽然现场还有一位同事, 但我们却没有互相认出来,令人感叹在庞大的公司中,有时即便共事也未必能够相识(我们一起担任 Go 语言评委)。

panic

除了内核恐慌,我还一直在听「硬地骇客」,一集都没拉,最近还开始听「有知有行」的播客。

在博客输出方面,我分享了两篇关于工程实践心得的文章,希望能够对读者有所帮助。

我最想分享的是 Obsidian Tasks 插件,详细信息可以在我的博客文章中找到, 从 Toodledo 到 Obsidian Tasks - 我的 GTD 最佳实践。我也很高兴成为 Obsidian Tasks 的 Sponser。

回顾一年的时间,我意识到自己在业余时光中的每周时间仅有10小时左右,非常宝贵。 期许着未来能够实现财务自由,以获得更多的自由时间,投入更多的兴趣爱好。

读书

cup

读书仍然大部分都是非虚构类书籍。

牛棚杂忆 (豆瓣)

士可杀亦可辱;过去带来惆怅,现在带来迷惘,未来带来希望。

素书 (豆瓣)

讲述做人做事的道理,古人的智慧。常读常新,尤其烦躁时候可以翻出来静一下。

翻译乃大道 (豆瓣)

就是为了看 中文的常态与变态。

沙丘 (豆瓣)

老男爵举家迁新球,贵公子初入沙漠星。 老皇帝密授哈克南,雷托族全体遭判断。 小保罗掌权弗雷曼,杰西卡诞下遗腹子。 穆阿迪布反攻沙丘,娶伊勒朗再封帝位。

跌荡一百年 (豆瓣)

国、企、民、央、地。 悲观。

被讨厌的勇气 (豆瓣)

好希望自己能在 20 岁时候读到这本书。(现在的我已经不需要啦)。教读者如何和自己、周边、世界相处,如何和自己对话以及改变自己。和遇见未知的自己属于同一个路数。

旧制度与大革命 (豆瓣)

治乱循环在反复。群体的无意识;民主和精英政治是否是解药?评估稳定性一个指标是贫富差距。极权下也孕育变革风险。

门后的秘密 (豆瓣)

管理入门快速操作手册

为什么 (豆瓣)

这本书我给不出星级,超出了我的评价范围。 它可能是一个新学科(因果推断)理论,也可能是统计学中的一个星火闪烁。 作者 Pearl 是统计学大拿,也是人工智能领域权威专家,他确在晚年提出了反对自己过去一系列方法路线。 今天为我们所熟知的大部分机器学习技术,都是基于概率上相关性,从啤酒和尿布,到今天 GPT 大杀四方,AIGC 智能涌现。Peral 认为真正有意义的是提出「为什么」,即解释因果关系。因果关系的论述需要智能能够想象不存在的事物,而这正是当前人工智能无法理解的(Maybe?) 本书成于 2019 年,作者今年已经 87 高龄,不知道他对当前 AIGC 风起云涌是怎么看待的。

为什么中国人勤劳而不富有 (豆瓣)

作者说的正确但是不全面。

Flag

高质量陪伴家人,放下手机,走向户外

执行了周三、周五家庭日给小朋友陪伴;每天早上送小朋友上学;周末一定有一天陪出行。

陪伴小孩这块我做的不如我老婆好,感谢老婆对家庭的贡献。

每月输出文章,特别是 Kubernetes / 研发设计领域可以写一些心得

今年输出 6 篇文章,达标率 50%。其中两篇 实用 Web API 规范架构设计 the Easy Way 我都是很满意的。

经历了新冠,今年计划安排个私教教我健身房运动

没有完成。

投资收益率能做到 10%,今年新手阶段投资以股票型基金为主,投资收益 3.9%,跑赢了大盘和余额宝

今年投资收益率 -1.35%,刚出新手村就被暴击,我还是缺乏对市场和商业的理解。

新的一年 Flag:

  • 高质量陪伴家人,走向户外,一起参与
  • 持续高质量输出文章,特别是 Kuberntes / PaaS 领域
  • 更多运动
  • 学习投资的基本框架,建立常识和投资逻辑

Last

每段经历,每次重逢,每本书籍,都是独特的命运线。新的一年已经来临, 期待着与家人、朋友一同继续探寻生活的真谛,去体验伟大与渺小。

往年总结:

实用 Web API 规范

作者 alswl
2023年4月3日 11:34

当开始创建一个新系统,或参与一个新团队或项目时,都会面临一个简单却深刻的问题:这个系统(Web Server)的 API 是否有设计规范?

pyramid

image by stable difussion, prompt by alswl

这个问题困扰了我很长时间,始于我求学时期,每一次都需要与团队成员进行交流和讨论。 从最初的自由风格到后来的 REST,我经常向项目组引用 Github v3 和 Foursqure API(已经无法访问,暴露年龄) 文档。 然而,在实践过程中,仍然会有一些与实际工作或公司通用规范不匹配的情况, 这时候我需要做一些补充工作。最终,我会撰写一个简要的 DEVELOPMENT.md 文档,以描述设计方案。

但我对该文档一直有更多的想法,它还不够完善。因此,我想整理出一份简单(Simple)而实用(Pragmatic)的 Web API 最佳实践,也就是本文。

为什么我们需要 API 统一规范

这个问题似乎很明显,但是深入剖析涉及团队协作效率和工程设计哲学。

API(Application Programming Interface,应用程序编程接口)是不同软件系统之间交互的桥梁。在不同软件系统之间进行通信时, API 可以通过标准化的方式进行数据传输和处理,从而实现各种应用程序的集成。

当我们开始撰写 API 文档时,就会出现一个范式(Design Pattern),这是显式还是隐式的, 是每个人一套还是公用同一套。这就像我们使用统一的 USB 接口一样,统一降低了成本,避免了可能存在的错误。具体来说,这有以下几个原因:

  • 容易理解,提高效率:服务提供方和消费方使用统一形式、结构和使用方式,以及统一的生产消费协议,从而减少沟通成本。
  • 专家经验:它包含最佳的工程实践,常见场景都有对应的解决方案,避免了每个人都要重新思考整个 API 系统。 例如,如何处理 API 缓存?如何进行鉴权?如何进行数据格式处理?
  • 面向未来的扩展,需要稳定的协议:协议是抽象的、独立于实现的,不是每个人都具备 设计面向不确定系统的能力,一些广泛使用的技术则为更广泛的场景做了规划。

why

image by alswl

虽然使用统一规范确实有一些成本,需要框架性的了解和推广,但我相信在大部分场景下, 统一规范所带来的收益远远高于这些成本。

然而,并非所有的情况下都需要考虑 API 规范。对于一些短生命周期的项目、影响面非常小的内部项目和产品, 可能并不需要过多关注规范。 此外,在一些特殊的业务场景下, 协议底层可能会发生变化,这时候既有的规范可能不再适用。但即使如此,我仍然建议重新起草新的规范,而不是放弃规范不顾。

规范的原则

在制定 API 规范时,我们应该遵循一些基本原则,以应对技术上的分歧,我总结了三个获得广泛认可的原则:

  • 简洁:简洁是抵抗复杂性的最直接和最有效的策略,利用简洁原则降低复杂度,避免复杂性的滋生和扩散;
  • 一致性:统一的设计模式和延续的设计风格有助于降低工程成本和工程师的心理负担;
  • 遵循现实:遵循现有工程领域的抽象和分层(例如 HTTP,REST,RBAC,OIDC 等),不要自己发明新的概念, 要始终思考这个问题是否只有自己遇到了(答案肯定是否定的)。

principle

image by alswl

REST 到底行不行?

在 Web API 领域,RESTful API 已经成为广受欢迎的协议。 其广泛适用性和受众范围之广源于其与 HTTP 协议的绑定,这使得 RESTful API 能够轻松地与现有的 Web 技术进行交互。如果您对 REST 不熟悉, 可以查看 阮一峰的 RESTful API 设计指南 以及 RESTful API 设计最佳实践

REST 是一种成熟度较高的协议,Leonard Richardson 将其描述为四种成熟度级别:

rest-four-level

image by alswl

  1. The Swamp of POX,使用 HTTP 承载 Legacy 协议(XML)
  2. Resources:使用资源抽象
  3. HTTP Verbs:使用丰富的 HTTP Verbs
  4. Hypermedia Controls:使用 rel 链接进行 API 资源整合,JSON:API 是登峰造极的表现

REST 的核心优势在于:

  • 它充分利用了 HTTP 协议的设计(HTTP Protocol)
  • 它具有出色的资源定位能力(Identification of resources)
  • 它设计了完备的资源操作方式(Manipulation of resources)
  • 它具备自解释性(Self-descriptive messages)
  • 它支持多种形态的呈现方式(hypermedia as the engine of application state)

然而,REST 并非一种具体的协议或规范,而是一种风格理念。尽管 REST 定义了一些规则和原则,如资源的标识、统一接口、无状态通信等, 但它并没有规定一种具体的实现方式。因此,在实际开发中,不同的团队可能会有不同的理解和实践, 从而导致 API 的不一致性和可维护性降低。

此外,REST 也有一些局限性和缺陷:

  • 并非所有请求都可以用资源描述,比如登录(/login)操作,转换成 session 就非常绕口; 同样的问题在转账这种业务也会出现。HTTP 有限的动词无法支撑所有业务场景。
  • REST 并未提供针对必然面临的问题,如分页、返回体具体结构、错误处理和鉴权等,明确的解决方案。
  • 对于复杂的查询(如搜索 Search),RESTful API 的查询参数可能会变得非常复杂,难以维护。

因此,虽然 REST 风格是一个不错的指导思想,但在具体实现时需要结合具体业务需求和技术特点,有所取舍,才能实现良好的 API 设计。 最后,我们是否需要 Web API 设计规范,遵循 REST 风格呢?我认为 REST 能够解决 90% 的问题,但还有 10% 需要明确规定细节。

Web API 规范的选择题

因为我们的协议基于 HTTP 和 REST 设计,我们将以 HTTP 请求的四个核心部分为基础展 开讨论,这些部分分别是:URL、Header、Request 和 Response。

URL 最佳实践

我的 URL 设计启蒙来自于 Ruby on Rails。 在此之前,我总是本能地将模型信息放到 URL 之上,但实际上良好的 URL 设计应该是针对系统信息结构的规划。 因此,URL 设计不仅仅要考虑 API,还要考虑面向用户的 Web URL。

为了达到良好的 URL 设计,我总结了以下几个规则:

  • 定位资源(这就回答分页是否应该在 Header)
  • 自解释(可读性强,URL 自身即包含核心信息)
  • 安全(不能包含用户认证信息,OAuth 为了解这个花了很多精力,防伪造)

通常情况下,URL 的模型如下所示:

/$(prefix)/$(module)/$(model)/$(sub-model)/$(verb)?$(query)#${fragment}

其中,Prefix 可能是 API 的版本,也可能是特殊限定,如有些公司会靠此进行接入层分流; Module 是业务模块,也可以省略;Model 是模型;SubModel 是子模型,可以省略; Verb 是动词,也可以省略;Query 是请求参数;Fragment 是 HTTP 原语 Fragment。

需要注意的是,并非所有的组成部分都是必须出现的。例如,SubModel 和 Verb 等字段可 以在不同的 URL 风格中被允许隐藏。

设计风格选择

注:请注意,方案 A / B / C 之间没有关联,每行上下也没有关联

问题 解释(见下方单列分析) 方案 A 方案 B 方案 C
API Path 里面 Prefix /apis /api 二级域名
Path 里面是否包含 API 版本 版本在 URL 的优势 🚫
Path 是否包含 Group 🚫
Path 是否包含动作 HTTP Verb 不够用的情况 🚫 (纯 REST) 看情况(如果 HTTP Verb CRUD 无法满足就包含)
模型 ID 形式 Readable Stable Identity 解释 自增 ID GUID Readable Stable ID
URL 中模型单数还是复数 单数 复数 列表复数,单向单数
资源是一级(平铺)还是多级(嵌套) 一级和多级的解释 一级(平铺) 多级(嵌套)
搜索如何实现,独立接口(/models/search)还是基于列表/models/ 接口 独立 合并
是否有 Alias URL Alias URL 解释 🚫
URL 中模型是否允许缩写(或精简) 模型缩写解释 🚫
URL 中模型多个词语拼接的连字符 - _ Camel
是否要区分 Web API 以及 Open API(面向非浏览器) 🚫

版本在 URL 的优势

我们在设计 URL 时遵循一致性的原则,无论是哪种身份或状态,都会使用相同的 URL 来访问同一个资源。 这也是 Uniform Resource Location 的基本原则。虽然我们可以接受不同的内容格式(例如 JSON / YAML / HTML / PDF / etc), 但是我们希望资源的位置是唯一的。

然而,问题是,对于同一资源在不同版本之间的呈现,是否应该在 URL 中体现呢?这取决于设计者是否认为版本化属于位置信息的范畴。

根据 RFC 的设计,除了 URL 还有 URN(Uniform Resource Name), 后者是用来标识资源的,而 URL 则指向资源地址。实际上,URN 没有得到广泛的使用,以至于 URI 几乎等同于 URL。

HTTP Verb 不够用的情况

在 REST 设计中,我们需要使用 HTTP 的 GET / POST / PUT / DELETE / PATCH / HEAD 等动词对资源进行操作。 比如使用 API GET /apis/books 查看书籍列别,这个自然且合理。 但是,当需要执行类似「借一本书」这样的动作时, 我们没有合适的动词(BORROW)来表示。针对这种情况,有两种可行的选择:

  1. 使用 POST 方法与自定义动词,例如 POST /apis/books/borrow,表示借书这一动作;
  2. 创建一个借书记录,使用资源新增方式来结构不存在的动作,例如 POST /apis/books/borrow-log/

这个问题在复杂的场景中会经常出现,例如用户登录(POST /api/auth/login vs POST /api/session)和帐户转账(vs 转账记录创建)等等。 API 抽象还是具体,始终离不开业务的解释。我们不能简单地将所有业务都笼统概括到 CRUD 上面, 而是需要合理划分业务,以便更清晰地实现和让用户理解。

在进行设计时,我们可以考虑是否需要为每个 API 创建一个对应的按钮来方便用户的操作。 如果系统中只有一个名为 /api/do 的 API 并将所有业务都绑定在其中,虽然技术上可行, 但这种设计不符合业务需求,每一层的抽象都是为了标准化解决特定问题的解法,TCP L7 设计就是这种理念的体现。

Readable Stable Identity 解释

在标记一个资源时,我们通常有几种选择:

  • 使用 ID:ID 通常与数据库自增 ID 绑定。
  • 使用 GUID:例如 UUID,尽管不那么精确。
  • 使用可读性和稳定性标识符(Readable Stable Identity):通常使用名称、UID 或特定 ID(如主机名、IP 地址或序列号)来标识, 要求该标识符具有稳定性且全局唯一,在内部系统中非常有用。

我个人有一个设计小技巧:使用 ${type}/${type-id} 形式的 slug 来描述标识符。Slug 是一种人类可读的唯一标识符, 例如 hostname/abc.sqaip/172.133.2.1。 这种设计方式可以在可读性和唯一性之间实现很好的平衡。

A slug is a human-readable, unique identifier, used to identify a resource instead of a less human-readable identifier like an id .

from What’s a slug. and why would I use one? | by Dave Sag

PS:文章最末我还会介绍一套 Apple Music 方案,这个方案兼顾了 ID / Readable / Stable 的特性。

一级和多级的解释

URL 的层级设计可以根据建模来进行,也可以采用直接单层结构的设计。具体问题的解决方式, 例如在设计用户拥有的书籍时,可以选择多级结构的 /api/users/foo/books 或一级结构的 /api/books?owner=foo

技术上这两种方案都可以,前者尊重模型的归属关系,后者则是注重 URL 结构的简单

多级结构更直观,但也需要解决可能存在的多种组织方式的问题,例如图书馆中书籍按照作者或类别进行组织? 这种情况下,可以考虑在多级结构中明确模型的归属关系, 例如 /api/author/foo/books(基于作者)或 /api/category/computer/books(基于类别)。

Alias URL 解释

对于一些频繁使用的 URL,虽然可以按照 URL 规则进行设计,但我们仍然可以设计出一个更为简洁的 URL, 以方便用户的展示和使用。这种设计在 Web URL 中尤其常见。比如一个图书馆最热门书籍的 API:

# 原始 URL
https://test.com/apis/v3/books?sort=hot&limit=10

# Alias URL
https://test.com/apis/v3/books/hot

模型缩写解释

通常,在对资源进行建模时,会使用较长的名称来命名,例如书籍索引可能被命名为 BookIndex ,而不是 Index。 在 URL 中呈现时,由于 /book/book-index 的 URL 前缀包含了 Book,我们可以减少一层描述, 使 URL 更为简洁,例如使用 /book/index。这种技巧在 Web URL 设计中非常常见。

此外,还有一种模型缩写的策略,即提供一套完整的别名注册方案。别名是全局唯一的, 例如在 Kubernetes 中, Deployment 是一种常见的命名,而 apps/v1/Deployment 是通过添加 Group 限定来表示完整的名称, 同时还有一个简写为 deploy。这个机制依赖于 Kubernetes 的 API Schema 系统进行注册和工作。

Header 最佳实践

我们常常会忽略 Header 的重要性。实际上,HTTP 动词的选择、HTTP 状态码以及各种身 份验证逻辑(例如 Cookie / Basic Auth / Berear Token)都依赖于 Header 的设计。

设计风格选择

问题 解释(见下方单列分析) 方案 A 方案 B 方案 C
是否所有 Verb 都使用 POST 关于全盘 POST 🚫
修改(Modify)动作是 POST 还是 PATCH? POST PATCH
HTTP Status 返回值 2XX 家族 充分利用 HTTP Status 只用核心状态(200 404 302 等) 只用 200
是否使用考虑限流系统 ✅ 429 🚫
是否使用缓存系统 ✅ ETag / Last Modify 🚫
是否校验 UserAgent 🚫
是否校验 Referrral 🚫

关于全盘 POST

有些新手(或者自认为有经验的人)可能得出一个错误的结论,即除了 GET 请求以外, 所有的 HTTP 请求都应该使用 POST 方法。甚至有些人要求 所有行为(即使是只读的请求)也应该使用 POST 方法。 这种观点通常会以“简单一致”、“避免缓存”或者“运营商的要求”为由来支持。

然而,我们必须明白 HTTP 方法的设计初衷:它是用来描述资源操作类型的,从而派生出了包括缓存、安全、幂等性等一系列问题。 在相对简单的场景下,省略掉这一层抽象的确不会带来太大的问题,但一旦进入到复杂的领域中, 使用 HTTP 方法这一层抽象就显得非常重要了。这是否遵循标准将决定你是否能够获得标准化带来的好处, 类比一下就像一个新的手机厂商可以选择不使用 USB TypeC 接口。 技术上来说是可行的,但同时也失去了很多标准化支持和大家心智上的约定俗成。

我特别喜欢一位 知乎网友评论:「路由没有消失,只是转移了」。

2XX 家族

HTTP 状态码的用途在于表明客户端与服务器间通信的结果。2XX 状态码系列代表服务器已经成功接收、 理解并处理了客户端请求,回应的内容是成功的。以下是 2XX 系列中常见的状态码及其含义:

  • 200 OK:请求已成功处理,服务器返回了响应。
  • 201 Created:请求已经被成功处理,并且在服务器上创建了一个新的资源。
  • 202 Accepted:请求已被服务器接受,但尚未执行。该状态码通常用于异步处理。
  • 204 No Content:请求已成功处理,但是服务器没有返回任何响应体内容。

2XX 系列的状态码表示请求已被成功处理,这些状态码可以让客户端明确知晓请求已被正确处理,从而进行下一步操作。

是否需要全面使用 2XX 系列的状态码,取决于是否需要向客户端明确/显示的信息, 告知它下一步动作。如果已经通过其他方式(包括文档、口头协议)描述清楚, 那么确实可以通盘使用 200 状态码进行返回。但基于行为传递含义, 或是基于文档(甚至口头协议)传递含义,哪种更优秀呢?是更为复杂还是更为简洁?

Request 最佳实践

设计风格选择

问题 解释(见下方单列分析) 方案 A 方案 B 方案 C
复杂的参数是放到 Form Fields 还是单独一个 JSON Body Form Fields Body
子资源是一次性查询还是独立查询 嵌套 独立查询
分页参数存放 Header URL Query
分页方式 分页方式解释 Page based Offset based Continuation token
分页控制者 分页控制者解释 客户端 服务端

分页方式解释

我们最为常见的两种分页方式是 Page-based 和 Offset-based,可以通过公式进行映射。 此外,还存在一种称为 Continuation Token 的方式,其技术类似于 Oracle 的 rownum 分页方案,使用参数 start-from=? 进行描述。 虽然 Continuation Token 的优缺点都十分突出,使用此种方式可以将顺序性用于替代随机性。

分页控制者解释

在某些情况下,我们需要区分客户端分页(Client Pagination)和服务器分页(Server Pagniation)。 客户端分页是指下一页的参数由客户端计算而来,而服务器分页则是由服务器返回 rel 或 JSON.API 等协议。 使用服务器分页可以避免一些问题,例如批量屏蔽了一些内容,如果使用客户端分页,可能会导致缺页或者白屏。

Response 最佳实践

设计风格选择

问题 解释(见下方单列分析) 方案 A 方案 B 方案 C
模型呈现种类 模型的几种形式 单一模型 多种模型
大模型如何包含子模型模型 模型的连接、侧载和嵌入 嵌入 核心模型 + 多次关联资源查询 链接
字段返回是按需还是归并还是统一 统一 使用 fields 字段按需
字段表现格式 Snake Camel
错误码 无自定,使用 Message 自定义
错误格式 全局统一 按需
时区 UTC Local Local + TZ
HATEOAS 🚫

模型的几种形式

在 API 设计中,对于模型的表现形式有多种定义。虽然这并不是 API 规范必须讨论的话题,但它对于 API 设计来说是非常重要的。

我将模型常说的模型呈现方式分为一下几类,这并非是专业的界定,借用了 Java 语境下面的一些定义。 这些名称在不同公司甚至不同团队会有不一样的叫法:

models

image by alswl

  • Business Object(BO):原始的业务模型
  • Data Object(DO):存储到 RDBMS 的模型,所以必须是打平的字段结构,有时候一个 BO 会对应到多个 DO
  • View Object(VO):呈现到表现层的模型,只保留用户需要看到信息,比如会去掉敏感信息
  • Data Transfer Object(DTO):用来在 RPC 系统进行传输的模型,一般和 原始的 Model 差异不大,根据不同序列化系统会有差异 (比如枚举的处理)

除此之外,还经常使用两类:Rich Model 和 Tiny Model(请忽略命名,不同团队叫法差异比较大):

  • Rich Model:用来描述一个丰富模型,这个模型包含了几乎所有需要用的的数据,也允许子资源进行嵌套
  • Tiny Model:是一个精简模型,往往用来在列表 API 里面被使用

模型的连接、侧载和嵌入

在 API 设计中,我们经常需要处理一个模型中包含多个子模型的情况,例如 Book 包含 Comments。 对于这种情况,通常有三种表现形式可供选择:链接(Link)、侧载(Side)和嵌入(Embed)。

models-with-children

image by alswl

链接(有时候这个 URL 也会隐藏,基于客户端和服务端的隐式协议进行请求):

{
  "data": {
    "id": 42,
    "name": "朝花夕拾",
    "relationships": {
      "comments": "http://www.domain.com/book/42/comments",
      "author": ["http://www.domain.com/author/鲁迅"]
    }
  }
}

侧载:

{
  "data": {
    "id": 42,
    "name": "朝花夕拾",
    "relationships": {
      "comments": "http://www.domain.com/book/42/comments",
      "authors": ["http://www.domain.com/author/鲁迅"]
    }
  },
  "includes": {
    "comments": [
      {
        "id": 91,
        "author": "匿名",
        "content": "非常棒"
      }
    ],
    "authors": [
      {
        "name": "鲁迅",
        "description": "鲁迅原名周树人"
      }
    ]
  }
}

嵌入:

{
  "data": {
    "id": 42,
    "name": "朝花夕拾",
    "comments": [
      {
        "id": 91,
        "author": "匿名",
        "content": "非常棒"
      }
    ],
    "authors": [
      {
        "name": "鲁迅",
        "description": "鲁迅原名周树人"
      }
    ]
  }
}

其他

还有一些问题没有收敛在四要素里面,但是我们在工程实践中也经常遇到,我将其捋出来:

我不是 HTTP 协议,怎么办?

Web API 中较少遇到非 HTTP 协议,新建一套协议的成本太高了。在某些特定领域会引入一些协议, 比如 IoT 领域的 MQTT

此外,RPC 是一个涉及广泛领域的概念,其内容远远不止于协议层面。 通常我们会将 HTTP 和 RPC 的传输协议以及序列化协议进行对比。 我认为,本文中的许多讨论也对 RPC 领域具有重要意义。

有些团队或个人计划使用自己创建的协议,但我的观点是应尽量避免自建协议,因为真正需要创建协议的情况非常罕见。 如果确实存在强烈的需要,那么我会问两个问题:是否通读过 HTTP RFC 文档和 HTTP/2 RFC 文档?

我不是远程服务(RPC / HTTP 等),而是 SDK 怎么办?

本文主要讨论的是 Web API(HTTP)的设计规范,并且其中一些规则可以借鉴到 RPC 系统中。 然而,讨论的基础都是建立在远程服务(Remote Service)的基础之上的。 如果你是 SDK 开发人员,你会有两个角色,可能会作为客户端和远程服务器进行通信, 同时还会作为 SDK 提供面向开发人员的接口。对于后者,以下几个规范可以作为参考:

后者可以参考一下这么几个规范:

认证鉴权方案

一般而言,Web API 设计中会明确描述所采用的认证和鉴权系统。 需要注意区分「认证」和「鉴权」两个概念。关于「认证」这一话题,可以在单独的章节中进行讨论,因此本文不会展开这一方面的内容。

在 Web API 设计中,常见的认证方式包括:HTTP Basic Auth、OAuth2 和账号密码登录等。 常用的状态管理方式则有 Bearer Token 和 Cookie。此外,在防篡改等方面,还会采用基于 HMac 算法的防重放和篡改方案。

忽略掉的话题

在本次讨论中,我未涉及以下话题:异步协议(Web Socket / Long Pulling / 轮训)、CORS、以及安全问题。 虽然这些话题重要,但是在本文中不予展开。

什么时候打破规则

有些开发者认为规则就是为了打破而存在的。现实往往非常复杂,我们难以讨论清楚各个细节。 如果开发者觉得规则不符合实际需求,有两种处理方式:修改规则或打破规则。 然而,我更倾向于讨论和更新规则,明确规范不足之处,确定是否存在特殊情况。 如果确实需要创建特例,一定要在文档中详细描述,告知接任者和消费者这是一个特例,说明特例产生的原因以及特例是如何应对的。

一张风格 Checklist

Github 风格

Github 的 API 是我常常参考的对象。它对其业务领域建模非常清晰,提供了详尽的文档,使得沟通成本大大降低。 我主要参考以下两个链接: API 定义 GitHub REST API documentation 和 面向应用程序提供的 API 列表 Endpoints available for GitHub Apps ,该列表几乎包含了 Github 的全部 API。

问题 选择 备注
URL
API Path 里面 Prefix 二级域名 https://api.github.com
Path 里面是否包含 API 版本 🚫 Header X-GitHub-Api-Version API Versions
Path 是否包含 Group 🚫
Path 是否包含动作 看情况(如果 HTTP Verb CRUD 无法满足就包含) 比如 PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge POST /repos/{owner}/{repo}/releases/generate-notes
模型 ID 形式 Readable Stable Identity
URL 中模型单数还是复数 复数
资源是一级(平铺)还是多级(嵌套) 多级
搜索如何实现,独立接口(/models/search)还是基于列表/models/ 接口 独立
是否有 Alias URL ?
URL 中模型是否允许缩写(或精简) 🚫 没有看到明显信息,基于多级模型也不需要,但是存在 GET /orgs/{org}/actions/required_workflows
URL 中模型多个词语拼接的连字符 -_ GET /repos/{owner}/{repo}/git/matching-refs/{ref} vs GET /orgs/{org}/actions/required_workflows
是否要区分 Web API 以及 Open API(面向非浏览器) 🚫
Header
是否所有 Verb 都使用 POST 🚫
修改(Modify)动作是 POST 还是 PATCH? PATCH
HTTP Status 返回值 充分利用 HTTP Status 常用,包括限流洗损
是否使用考虑限流系统 ✅ 429
是否使用缓存系统 ✅ ETag / Last Modify Resources in the REST API#client-errors
是否校验 UserAgent
是否校验 Referrral 🚫
Request
复杂的参数是放到 Form Fields 还是单独一个 JSON Body Body 参考 Pulls#create-a-pull-request
子资源是一次性查询还是独立查询 嵌套 从 Pulls 进行判断
分页参数存放 URL Query
分页方式 Page Using pagination in the REST API
分页控制者 服务端 同上
Response
模型呈现种类 多种模型 比如 Commits 里面的 明细和 Parent Commits
大模型如何包含子模型模型 核心模型 + 多次关联资源查询? 没有明确说明,根据几个核心 API 反推
字段返回是按需还是归并还是统一 统一
字段表现格式 Snake
错误码 Resources in the REST API#client-errors
错误格式 全局统一 Resources in the REST API#client-errors
时区 复合方案(ISO 8601 > Time-Zone Header > User Last > UTC) Resources in the REST API#Timezones
HATEOAS 🚫

Azure 风格

Azure 的 API 设计遵循 api-guidelines/Guidelines.md at master · microsoft/api-guidelines, 这篇文章偏原理性,另外还有一份实用指导手册在 Best practices in cloud applicationsWeb API design best practices

需要注意的是,Azure 的产品线远比 Github 丰富,一些 API 也没有遵循 Azure 自己的规范。 在找实例时候,我主要参考 REST API Browser Azure Storage REST API Reference 。 如果具体实现和 Guidelines.md 冲突,我会采用 Guidelines.md 结论。

问题 选择 备注
URL
API Path 里面 Prefix 二级域名
Path 里面是否包含 API 版本 🚫 x-ms-version
Path 是否包含 Group
Path 是否包含动作 🚫? 没有明确说明,但是有倾向使用 comp 参数来进行动作,保持 URL 的 RESTful 参考 Lease Container (REST API) - Azure Storage
模型 ID 形式 Readable Stable Identity Guidelines.md#73-canonical-identifier
URL 中模型单数还是复数 复数 Guidelines.md#93-collection-url-patterns
资源是一级(平铺)还是多级(嵌套) 多级 / 一级 api-design#define-api-operations-in-terms-of-http-methods,注 MS 有 comp=? 这种参数,用来处理特别的命令
搜索如何实现,独立接口(/models/search)还是基于列表/models/ 接口 ? 倾向于基于列表,因为大量使用 comp= 这个 URL Param 来进行子命令,比如 Incremental Copy Blob (REST API) - Azure Storage
是否有 Alias URL ?
URL 中模型是否允许缩写(或精简) ?
URL 中模型多个词语拼接的连字符 Camel Job Runs - List - REST API (Azure Storage Mover)
是否要区分 Web API 以及 Open API(面向非浏览器) 🚫
Header
是否所有 Verb 都使用 POST 🚫
修改(Modify)动作是 POST 还是 PATCH? PATCH Agents - Update - REST API (Azure Storage Mover)
HTTP Status 返回值 充分利用 HTTP Status Guidelines.md#711-http-status-codes
是否使用考虑限流系统 ?
是否使用缓存系统 Guidelines.md#75-standard-request-headers
是否校验 UserAgent 🚫
是否校验 Referrral 🚫
Request
复杂的参数是放到 Form Fields 还是单独一个 JSON Body Body 参考 Agents - Create Or Update - REST API (Azure Storage Mover)
子资源是一次性查询还是独立查询 ?
分页参数存放 ? 没有结论
分页方式 Page based
分页控制者 服务端 Agents - List - REST API (Azure Storage Mover)
Response
模型呈现种类 单一模型 推测
大模型如何包含子模型模型 ? 场景过于复杂,没有单一结论
字段返回是按需还是归并还是统一 ?
字段表现格式 Camel
错误码 使用自定错误码清单 至少在各自产品内
错误格式 自定义
时区 ?
HATEOAS ? api-design#use-hateoas-to-enable-navigation-to-related-resources

Azure 的整体设计风格要比 Github API 更复杂,同一个产品的也有多个版本的差异,看 上去统一性要更差一些。这种复杂场景想用单一的规范约束所有团队的确也是更困难的。 我们可以看到 Azaure 团队在 Guidelines 上面努力,他们最近正在推出 vNext 规范。

我个人风格

我个人风格基本继承自 Github API 风格,做了一些微调,更适合中小型产品开发。 我的改动原因都在备注中解释,改动出发点是:简化 / 减少歧义 / 考虑实际成本。如果备注里面标记了「注」,则是遵循 Github 方案并添加一些观点。

问题 选择 备注
URL
API Path 里面 Prefix /apis 我们往往只有一个系统,一个域名要承载 API 和 Web Page
Path 里面是否包含 API 版本
Path 是否包含 Group 做一层业务模块拆分,隔离一定合作边界
Path 是否包含动作 看情况(如果 HTTP Verb CRUD 无法满足就包含)
模型 ID 形式 Readable Stable Identity
URL 中模型单数还是复数 复数
资源是一级(平铺)还是多级(嵌套) 多级 + 一级 注:80% 情况都是遵循模型的归属,少量情况(常见在搜索)使用一级
搜索如何实现,独立接口(/models/search)还是基于列表/models/ 接口 统一 > 独立 低成本实现一些(早期 Github Issue 也是没有 /search 接口
是否有 Alias URL 🚫 简单点
URL 中模型是否允许缩写(或精简) 一旦做了精简,需要在术语表标记出来
URL 中模型多个词语拼接的连字符 -
是否要区分 Web API 以及 Open API(面向非浏览器) 🚫
Header
是否所有 Verb 都使用 POST 🚫
修改(Modify)动作是 POST 还是 PATCH? PATCH
HTTP Status 返回值 充分利用 HTTP Status
是否使用考虑限流系统 ✅ 429
是否使用缓存系统 🚫 简单一些,使用动态数据,去除缓存能力
是否校验 UserAgent
是否校验 Referrral 🚫
Request
复杂的参数是放到 Form Fields 还是单独一个 JSON Body Body
子资源是一次性查询还是独立查询 嵌套
分页参数存放 URL Query
分页方式 Page
分页控制者 客户端 降低服务端成本,容忍极端情况空白
Response
模型呈现种类 多种模型 使用的 BO / VO / Tiny / Rich
大模型如何包含子模型模型 核心模型 + 多次关联资源查询
字段返回是按需还是归并还是统一 统一 Tiny Model(可选) / Model(默认) / Rich Model(可选)
字段表现格式 Snake
错误码 注:很多场景只要 message
错误格式 全局统一
时区 ISO 8601 只使用一种格式,不再支持多种方案
HATEOAS 🚫

题外话 - Apple Music 的一个有趣设计

Apple Music

image from Apple Music

我最近在使用 Apple Music 时注意到了其 Web 页面的 URL 结构:

/cn/album/we-sing-we-dance-we-steal-things/277635758?l=en

仔细看这个 URL 结构,可以发现其中 Path 包含了人类可读的 slug,分为三个部分:alumn/$(name)/$(id) (其中包含了 ID)。 我立即想到了一个问题:中间的可读名称是否无机器意义,纯粹面向自然人? 于是我测试了一个捏造的地址:/cn/album/foobar/277635758?l=en。 在您尝试访问之前,您能猜出结果是否可以访问吗?

这种设计范式比我现在常用的 URL 设计规范要复杂一些。我的规范要求将资源定位使用两层 slug 组织,即 $(type)/$(id)。 而苹果使用了 $(type)/(type-id)/$(id),同时照顾了可读性和准确性。

题外话 - 为什么 GraphQL 不行

GraphQL 是一种通过使用自定义查询语言来请求 API 的方式,它的优点在于可以提供更灵活的数据获取方式。 相比于 RESTful API 需要一次请求获取所有需要的数据,GraphQL 允许客户端明确指定需要的数据,从而减少不必要的数据传输和处理。

然而,GraphQL 的过于灵活也是它的缺点之一。由于它没有像 REST API 那样有一些业务场景建模的规范, 开发人员需要自己考虑数据的处理方式。 这可能导致一些不合理的查询请求,对后端数据库造成过度的压力。此外,GraphQL 的实现和文档相对较少,也需要更多的学习成本。

因此,虽然 GraphQL 可以在一些特定的场景下提供更好的效果,但它并不适合所有的 API 设计需求。 实际上,一些公司甚至选择放弃支持 GraphQL,例如 Github 的 一些项目

最后

Complexity is incremental (复杂度是递增的)

  • John Ousterhout (via

风格没有最好,只有最适合,但是拥有风格是很重要的。

建立一个优秀的规则不仅需要对现有机制有深刻的理解,还需要对业务领域有全面的掌握,并在团队内进行有效的协作与沟通, 推广并实施规则。 不过,一旦规则建立起来,就能够有效降低系统的复杂度,避免随着时间和业务的推进而不断增加的复杂性, 并减少研发方面的沟通成本。

这是一项长期的投资,但能够获得持久的回报。希望有长远眼光的人能够注意到这篇文章。

主要参考文档:

码转电子厂 - 教你修键盘

作者 alswl
2022年10月19日 10:51

fu-tu-kang

为了做好万全的准备,如何在 35 岁毕业后能够顺利入职电子厂?

从修键盘学起

原因

我使用的键盘是 ErgoDox,一个人体工程学设计的分体键盘。关于 ErgoDox 更多详情可以见我之前的 回答

keyboard-view

(前任键帽配色 + 手托):

keyboard-view-2

经过七八年工作,它进过水,进过咖啡,还进过豆浆,现在终于有几个键不灵活了,按起来有粘滞感,无法提供顺畅的 coding feel 了。

在使用备胎 Filco 几个月之后,我终于下定决心,要将 ErgoDox 修好。

准备

prepare

  • 电烙铁
  • 吸锡器
  • 焊锡
  • 键轴
  • 精工螺丝起子套件
  • 拔键器
  • 起键轴小起子

没有焊接经验的朋友,可以学习一下如何焊接:

女生都能学会的键盘焊接换轴教程_哔哩哔哩_bilibili

电烙铁的错误和正确使用方法_哔哩哔哩_bilibili

过程

检查要换哪些键轴,按一按,听一听

step-1

卸下外壳卸下伪装

step-2

去除对应的键帽(注意,F J 键帽不一样哦)

step-3

融化焊锡,用吸锡器将融化的焊锡吸走

step-4

用键轴小起子,将其从正面摘除,记得有个软卡子,要上下方向(即 cherry logo 方向 + 对面用力)

step-5

看看进去的咖啡和豆浆

step-6

将新的键轴焊上去,效果

step-7

吸锡器吸走的碎屑吐出来的样子

step-8

最后完成组装

keyboard-final

总结

又有了那种打字畅快的感觉了~

青春回来了~

最后分享一下我的 ErgoDox Layout 配置:

若有收获,就点个赞吧。

Python 的类型系统

作者 alswl
2020年6月23日 17:54

wall image from pixabay.com

静态类型正在逐渐成为潮流, 2010 年之后诞生的几门语言 Go、Rust、TypeScript 等都走了静态类型路线。 过往流行的一些动态语言(Python、PHP、JavaScript)也在积极引入语言新特性(Type Hint、TypeScript)对静态类型增强。

我曾使用 Python 开发规模较大的项目,感受过动态语言在工程规模变大时候带来的困难: 在重构阶段代码回归成本异常之高,很多历史代码不敢动。 后来技术栈转到 Java,被类型系统怀抱让人产生安全感。

最近一年在一个面向稳定性的运维系统耕耘。系统选型之初使用了 Python。 我在项目中力推了 Python 3.7,并大规模使用了 Python 的类型系统来降低潜在风险。

追根溯源,我花了一些时间了解 Python 在类型系统的设计和实现, 本文以 PEP 提案介绍一下 Python 在类型系统上面走过的路。

类型系统

谈类型系统之前,要厘定两个概念,动态语言和动态类型。

动态语言(Dynamic Programming Language)则是指程序在运行时可以改变结构。 这个结构可以包含函数、对象、变量类型、程序结构。 动态类型是类型系统(Type System)其中一类,即程序在运行期间可以修改变量类型。 另外一种是静态类型:在编译期就决定了变量类型,运行期不允许发生变化。 类型系统还有一种分法是强类型和弱类型,强类型是指禁止类型不匹配的指令,弱类型反之。

动态语言和动态类型这两个概念切入点不一样, Python 是一门动态语言,也是动态类型语言,还是强类型的动态类型。 这篇文章主要讨论 Python 语言的类型系统,不会涉及动态语言特性。

类型安全之路

行业里面一直有一个争论:动态类型和静态类型哪一种更强大。 静态类型的支持者认为三个方面具备优势:性能、错误发现、高效重构。 静态类型通过编译期决定具体类型可以显著的提高运行期效率; 编译期就能够发现错误,在工程规模逐步变大时候尤其明显; 类型系统可以帮助 IDE 提示,高效重构。 动态类型的支持者则认为分析代码会更简单,减少出错机会,写起来也更为快速。

Python 开发者们并非没有看到这个痛点, 一系列 PEP 提案应运而生。 在保留 Python 动态类型系统优势前提,通过语法、特性增强,将类型系统引入 Python。

Python 在 2014 年即提出了 PEP 484,随后提出一个精粹版 PEP 483(The Theory of Type Hints), 其工程实现 typing 模块在 3.5 发布。 经过 PEP 484,PEP 526,PEP 544,PEP 586,PEP 589,PEP 591 的多次版本迭代,Python 的类型系统已经很丰富。 甚至包含了比如 Structural Subtyping 以及 Literal Typing 这边相对罕见的特性。

PEP 483 - 核心概念

PEP 483 在 2014 年 12 月发布, 是 Guido 起笔的核心概念版,简明扼要的写清楚 Python 的类型系统建设方向、边界、要和不要。

PEP 483 没有谈具体工程实现,提纲挈领地讲了一下 Python 类型系统如何对外呈现。 厘定 Type / Class 差别,前者是语法分析概念,后者是运行时概念。 在这个定义下面 Class 都是一个 Type,但 Type 未必是 Class。 举例 Union[str, int] 是 Type 但并不是 Class。

PEP 483 还介绍内建基础类型:Any / Unison / Optional / Tuple / Callable,这些基础类型支撑上游丰富变化。

静态类型系统最大的诟病是不够灵活,Go 语言现在还没有实现泛型。 PEP 483 介绍了 Python Generic types 泛型使用方法, 形式如下:

S = TypeVar('S', str, bytes)

def longest(first: S, second: S) -> S:
    return first if len(first) >= len(second) else second

最后,PEP 483 还提了一些重要的小特性:

  • 别称 Alias
  • 前置引用 Farward Reference(在定义类方法注解中使用定义类),eg.:解决二叉树 Node 节点中需要引用 Node 问题
  • covariance contravariant 协变逆变
  • 使用注释标记类型
  • 转型 Cast

PEP 483 的实现,主要依赖了 PEP 3107 – Function Annotations 这个提案。PEP 3107 介绍 function 注解使用。比如, func(a: a1, b: b1) -> r1 这段代码, 其中冒号后面的描述符记录会到 func 的 __annotations__ 变量中。

PEP 3107 效果展示如下,可以清晰看到函数变量存放:

def add(x: int, y: int) -> int:
    return x + y

add.__annotations__
# {'x': int, 'y': int, 'return': int}

PS:现在 Python 有了 Decorator 装饰器 / Annotation 注解,其中 Annotation 的设计还和 Java 的 Annotation 同名,一锅粥。

PEP 484 - Type Hints 核心

PEP 484 – Type Hints 在 PEP 483 基础上完整讲述 Python 类型系统如何设计,如何使用,细节如何(typing 模块)

这篇提案开宗明义地点出:

Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention.

一句话断绝了 Python 在语言级别进化到静态系统的可能。

提案除了 PEP 483 已经讲解的特性,还有以下吸引我的点:

  • 允许通过 Stub Files 为已经存在的库添加类型描述。具体是使用 Python 文件对应的 .pyi 文件描述 Python 代码的带类型签名。 这个方案和 TS 的 @types 文件有异曲同工之妙。
  • 允许使用 @overload 进行类型重载,这也是活久见,Python 居然可以(在某种意义上)支持重载了。
  • 介绍了 typing 实现细节,比如使用 abs(Abstract Base Class)构建常见类型的 interface,包括 Sized / Iterable 这些基础接口。 我个人认为这个工作量是其实挺大,是给已有的类进行一次依赖梳理。
  • 介绍了 Python 向后(Python 2)兼容方法,有这么几种策略: 使用 decorator(@typehints(foo=str, returns=str))、comments、Stub files、Docstring

PEP 526 - 变量也安排上了

PEP 526 – Syntax for Variable Annotations 核心提案是给变量加上 Type Hints 支持。

function annotation 类似,也是通过注解方式存放。 差异是并不是给实例添加一个 __annotations__ 成员,而是将变量的 annotations 信息存放在上下文变量 __annotations__ 之中。 这个其实也比较好理解:定义一个变量类型时候,这个变量还没有初始化。

我写一段 Demo 展示一下:

from typing import List
users: List[int]

# print(__annotations__)
# {'users': typing.List[int]}

可以看到,上述 Demo 效果是在上下文变量创建了一个 users,但这个 users 其实并不存在,只是定义了类型, 如果运行 print(users) 会抛出 NameError: name 'users' is not defined

观察字节码会更清晰:

 L.   1         0  SETUP_ANNOTATIONS

 L.   1         2  LOAD_CONST               0
                4  LOAD_CONST               ('List',)
                6  IMPORT_NAME              typing
                8  IMPORT_FROM              List
               10  STORE_NAME               List
               12  POP_TOP

 L.   3        14  LOAD_NAME                List
               16  LOAD_NAME                int
               18  BINARY_SUBSCR
               20  LOAD_NAME                __annotations__
               22  LOAD_STR                 'users'
               24  STORE_SUBSCR
               26  LOAD_CONST               None
               28  RETURN_VALUE

可以清晰看到,并没有创建一个名为 users 的变量,而是使用了 __annotations__ 变量。 注:Python 存储变量使用 opcode 是 STORE_NAME

PS:本提案中有不少被否决的提案,挺有趣的,社区提出了很多奇淫巧计。 可以看出社区决策的慎重,存量系统升级的难度。

PEP 544 - Nominal Subtyping vs Structural Subtyping

PEP 484 里面类型系统讨论的是 Nominal Subtyping, 这个 PEP 544 – Protocols: Structural subtyping (static duck typing) 则是提出了Structural Subtyping。 如果非要翻译,我觉得可以称为具名子类型 / 同构子类型。 注意,也有人将 Structural Subtyping 称之为 Duck Typing,其实这两者不相同,具体可以见 Duck typing / Comparison with other type systems

Nominal Subtyping 是指按字面量匹配类型,而 Structural Subtyping 则是按照结构(行为)进行匹配, 比如 Go 的 Type 就是 Structural Subtyping 实现。

这里写个简单 Demo 展示一下后者:

from typing import Iterator, Iterable

class Bucket:
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

def collect(items: Iterable[int]) -> int: ...
result: int = collect(Bucket())  # Passes type check

代码中定义了 Bucket 这种类型,并且提供了两个类成员。这两个类成员刚好是 Interator 的定义。 那么在实际使用中,就可以使用 Bucket 替换 Iterable。

PEP 586 / PEP 589 / PEP 591 持续增强

PEP 586 – Literal Types 在 Python 3.8 实现,支持了字面量作为类型使用。 比如 Literal[4],举一个更有语义的例子 Literal['GREEN']

我第一反应这和 Scala 里面的 Symbol 非常像,Scala 中写法是 Symbol("GREEN")。 这个特性使用挺学院派,很容易在 DSL 里面写的天花乱坠。 Scala 官方有说过可能在未来移除 Symbol 特性,建议直接使用常量替代。

PEP 589 – TypedDict: Type Hints for Dictionaries with a Fixed Set of Keys 给 Dict 增加 key 的 Type,继承 TypedDict

PEP 591 – Adding a final qualifier to typing 增加 final / Final 两个概念,前者是装饰器,后者是注解,标注该类 / 函数 / 变量无法修改

至此,Python 3.8 已经具备我们日常需要的类型系统特性(非运行时 😂)。

总结

遗憾的是,typing 模块在文档鲜明的标注:

The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.

即:Python 运行时(Intercepter / Code Evaluator)并不支持函数和变量的类型装饰符。 这些装饰符只能由第三方工具检查,比如类型检查器、IDE、静态、Linter。

这个信息说明了 Python 在类型安全上尝试的局限性。所有的限制、约束都不会发生在运行时, 想要从类型系统中收获工程上面的价值,只能借助第三方工具。

诚然,Python 社区在竭力向类型系统靠拢,但是这种非语言级别 Runtime 的支持,到底能走多远呢? Python 缺少金主爸爸,干爹 Red Hat 投入资源也有限。连社区从 Python 2 切换到 Python 3 都还没走完,为何? 投入产出比太低,新特性缺乏足够的吸引力,替代品太多。

另一方面,看看竞对们: 动态语言在往静态语言靠拢,而静态语言也在不断吸收动态语言的特性。比如 Java 14 里面的 REPL(Read-Eval-Print-Loop), Kotlin / Scala 等语言的类型推断(Type Inference)。 也许这种演进方式更能够让用户接受吧。

参考

再见 2019

作者 alswl
2020年3月29日 23:48

bali 2019 年 9 月摄于巴厘岛

和老友聚餐时候完了一个游戏,大家各自找了一个词形容自己的 2019。我用的词是「累」和「平凡」。

2019 关键词:累和平凡

2019 没有进入生门。

加入阿里之后,组织像榨汁机一样将个体精力榨干。 工作日几乎没有自己的时间。这样画面经常出现:回到酒店先倒头趴一会,然后洗完澡看眼儿子视频就睡觉,醒来又是新的循环。 周末的时间则是交给了家人,今年母亲身体抱恙,我尽量每月都能回一趟老家,以至于陪伴儿子的时间所剩无几。

工作日典型的一天是:早上 8:00 起床,酒店就餐,9:30 到公司,项目进度同步,杂项计划, 客户落地沟通,协同外部同事(外包 + 外部合作公司)困难事项解决。 下午 14:00 开始细项工作,写设计方案、写一点代码,做一些技术攻坚。 傍晚到 22:00 很可能开会(需求会、设计会),如果不开会就需要写一些设计文档。 另外一周有两个晚上在出差路上。 这还不算有些团队其他突发的小型项目需求过来。为了追求更多价值输出地方,我来者不拒。

这样算来,每周真正能自由支配时间只有 4-6 小时,这么一点时间刚好够做下工作日志整理,谈不上去学习新知识。 每天最适合学习的时间可能是在出差高铁路和日常打车时坐在后座。

个人 OKR 则是惨不忍睹,Q4 整个季度累的没有翻开 OKR 帐簿。 回过头来看,Q4 有些量化的产出完全靠日常习惯养成大类目。 最佳 OKR 贡献者居然是 RescueTime 效率大于 80 和工作日工作在案时间 8h 这两条。

我的 2019 是累的,由内而外。工作、家庭压力扑面而来,而立之年感受到生命的脆弱,以及个体在大组织的身不由己。 彻头彻尾感受到自己的渺小和普通。 王国维说人生有三种境界,我倒是听过人生三种落空:「对父母的期望落空」、「对自己的期望落空」和 「对子女的期望落空」。落空让我重新思考自己定位,欲望和所得是否匹配。

量化 OKR

照例回顾 OKR,四个季度的 OKR 完成度分别是:

  • 13.06%
  • 13.98%
  • 11.21%
  • 7.85%

今年开局的两个 Q 的 OKR 完成度还不错,第三个季度则是由于家中有变,一下子打破了平衡。 第四个季度更是在照顾家中和 KPI 冲刺双重打压之下更为惨不忍睹。

新年的 OKR 怎么计划,我想了几天。 现阶段遇到的工作家庭负载太高了,强行设定高 OKR 不切现实。 与其总是在疲惫追逐,不如调整一下心态,将注意力收拢在少量的目标上。 因此我砍掉了大部分目标明确型 OKR,除了 Daiily 习惯养成,将 OKR 压缩到 2 个:

  • 输入:读书
  • 输出:写作

期待 2020 有收获,拿回生活和工作的主控权。

2019 年五星书籍推荐

附录 1:2019 OKR 清单

Direction

D Rank

Objectives

O Rank

Key Result

KR Rank

Rank Score

Long Task

Progress

Plan

Done

Done Norm

Score Total

习惯养成

20%

运动

20%

每周跑步或游泳 2 次

50%

2%

V

■□□□

104

6

0.0576923076923077

0.00115384615384615

体重 80kg -> 70 kg

50%

2%

V

■■□□

10

5.1

0.51

0.0102

高效率

50%

工作日个人时间 > 1h 每周 3 次

10%

1%

V

■□□□

156

45

0.288461538461538

0.00288461538461538

工作日工作时间 > 8h 每周 4 次

25%

2.5%

V

■■□□

208

117

0.5625

0.0140625

周末个人时间 > 3h 每周周 2 次

25%

2.5%

V

■□□□

104

18

0.173076923076923

0.00432692307692308

RT 值达标(80/60) 每周 6 次

40%

4%

V

■■■□

312

246

0.788461538461538

0.0315384615384615

读书

20%

每月读书 4 本

100%

4%

V

■□□□

48

15

0.3125

0.0125

计划和复盘

10%

每周做早晨计划 6 次

30%

0.6%

V

■■□□

312

233

0.746794871794872

0.00448076923076923

每日做习惯追踪

30%

0.6%

V

■■□□

364

193

0.53021978021978

0.00318131868131868

每周做 Review

40%

0.8%

V

■□□□

52

11

0.211538461538462

0.0016923076923077

影响力

5%

写作

100%

写作 12 篇

100%

5%

V

■□□□

12

1

0.0833333333333333

0.00416666666666667

家庭

25%

旅行

10%

出国旅行 1 次

50%

1.25%

 

■■■■

1

1

1

0.0125

出沪旅行 2 次

50%

1.25%

 

□□□□

2

0

0

0

父母

30%

回父母家 6 次

100%

7.5%

V

■■■□

6

15

2.5

0.1875

子女

60%

每周陪伴小孩 >3h 2 天

60%

9%

V

■■□□

96

56

0.583333333333333

0.0525

制定并执行子女成长计划

20%

3%

V

■□□□

12

1

0.0833333333333333

0.0025

笔记 3 本教育书籍

20%

3%

V

■□□□

3

1

0.333333333333333

0.00999999999999999

基础技能

20%

数学

30%

看完「统计学习方法」

25%

1.5%

 

■□□□

1

0.3

0.3

0.0045

看完「什么是数学」

25%

1.5%

 

□□□□

1

0

0

0

看完「怎样解题」

25%

1.5%

 

□□□□

1

0

0

0

写一篇关于数学的文章

25%

1.5%

 

□□□□

1

0

0

0

英文

30%

扇贝背 GAE 词汇(剩余 3600)

30%

1.8%

V

□□□□

3600

0

0

0

懂你英语打卡两个月 30*2

30%

1.8%

V

■□□□

60

5

0.0833333333333333

0.0015

写 2 篇英文博客

20%

1.2%

 

□□□□

2

0

0

0

读完「语法俱乐部」并做完习题

20%

1.2%

 

□□□□

13

0

0

0

系统思考

20%

笔记「认知科学」6 本书籍

100%

4%

 

■■□□

4

2

0.5

0.02

团队管理

20%

笔记「创业和管理」4 本书籍

70%

2.8%

 

■■■□

4

3

0.75

0.021

写 2 篇相关文章

30%

1.2%

 

□□□□

2

0

0

0

专业技能

15%

综合技能

40%

关注 3 个会议(InfoQ、AS、?)

40%

2.4%

 

□□□□

3

0

0

0

云原生相关技术学习(待定)

60%

3.6%

 

□□□□

1

0

0

0

语言和基础

60%

笔记「Java Concurrency in Practice」

30%

2.7%

 

■□□□

1

0.2

0.2

0.0054

读完「七周七并发」

10%

0.9%

 

□□□□

1

0

0

0

笔记「架构思想」4 本书

30%

2.7%

 

□□□□

4

0

0

0

读完「自己动手写Java虚拟机」或者类似书

30%

2.7%

 

□□□□

1

0

0

0

面向未来技能

15%

机器学习

60%

学完 Coursera Machine Learning

50%

4.5%

 

□□□□

1

0

0

0

学习 Google Machine Learning Crash

30%

2.7%

 

□□□□

1

0.2

 

0%

学完「集体编程智慧」

20%

1.8%

 

■■■■

1

1

1

0.018

数据分析

40%

笔记「深入浅出数据分析」

50%

3%

 

■■■■

1

1

1

0.03

笔记「利用 Python 进行数据分析」

50%

3%

 

 

1

 

 

 

附录 2:2020 OKR 清单

Direction

D Rank

Objectives

O Rank

Key Result

KR Rank

Rank Score

Long Task

Is Done

Plan

习惯养成

30%

运动

20%

每周跑步或游泳 2 次

100%

6%

V

□□□□

104

高效率

50%

工作日个人时间 > 1h 每周 3 次

10%

1.5%

V

□□□□

156

工作日工作时间 > 8h 每周 4 次

25%

3.75%

V

□□□□

208

周末个人时间 > 3h 每周周 2 次

25%

3.75%

V

□□□□

104

RT 值达标(80/60) 每周 6 次

40%

6%

V

□□□□

312

计划和复盘

30%

每周做早晨计划 6 次

30%

2.7%

V

□□□□

312

每日做习惯追踪

30%

2.7%

V

□□□□

364

每周做 Review

40%

3.6%

V

□□□□

52

家庭

20%

旅行

10%

出国旅行 1 次

50%

1%

 

□□□□

1

出沪旅行 2 次

50%

1%

 

□□□□

2

父母

30%

回父母家 12 次

100%

6%

V

□□□□

12

子女

60%

每周陪伴小孩 >3h 2 天

100%

12%

V

□□□□

96

输入

25%

读书

100%

每月读书 4 本出笔记

100%

25%

 

□□□□

48

输出

25%

写作

100%

写作 24 篇

100%

25%

V

□□□□

24

 

100%

 

 

 

 

100%

 

 

 

PS:文章成于 2020-01-19。

从 SQL Server 到 MySQL(三):愚公移山 - 开源力量

作者 alswl
2018年6月20日 20:18

该系列三篇文章已经全部完成:

201806/refactor.png

我们用了两章文章 从 SQL Server 到 MySQL(一):异构数据库迁移 / 从 SQL Server 到 MySQL(二):在线迁移,空中换发动机 介绍我们遇到问题和解决方案。 不管是离线全量迁移还是在线无缝迁移, 核心 ETL 工具就是 yugong。

Yugong 是一个成熟工具, 在阿里巴巴去 IOE 行动中起了重要作用, 它与 Otter / Canal 都是阿里中间件团队出品。 它们三者各有分工: Yugong 设计目标是异构数据库迁移; Canal 设计用来解决 MySQL binlog 订阅和消费问题; Otter 则是在 Canal 之上,以准实时标准解决数据库同步问题。 Otter 配备了相对 yugong 更健壮管理工具、分布式协调工具, 从而长期稳定运行。Yugong 设计目标则是一次性迁移工作,偏 Job 类型。 当然 yugong 本身质量不错,长期运行也没问题。 我们有个产线小伙伴使用我们魔改后 yugong, 用来将数据从管理平台同步数据到用户前台,已经稳定跑了半年多了。

yugong 系统结构

这里我不赘述如何使用 yugong,有需求同学直接去 官方文档 查看使用文档。

我直接进入关键环节:解剖 yugong 核心模块。 Yugong 数据流是标准 ETL 流程,分别有 Extractor / Translator / Applier 这三个大类来实现 ETL 过程:

ETL & Java Class

我们依次来看看这三大类具体设计。

Extractor

Extractor Class

  • YuGongLifeCycle:Yugong 组件生命周期声明
  • AbstractYuGongLifeCycle:Yugong 组件生命周期一些实现
  • RecordExtractor:基础 Extractor Interface
  • AbstractRecordExtractor:基础 Extractor 虚拟类,做了一部分实现
  • AbstractOracleRecordExtractor:Oracle Extractor 虚拟类,做了一部分 Oracle 相关实现
  • OracleOnceFullRecordExtractor:Oracle 基于特定 SQL 一次性 Extractor
  • OracleFullRecordExtractor:Oracle 全量 Extractor
  • OracleRecRecordExtractor:Oracle 记录 Extractor,用来创建物化视图
  • OracleMaterializedIncRecordExtractor:基于(已有)物化视图 Oracle 增量 Extrator
  • OracleAllRecordExtractor:Oracle 自动化 Extractor,先 Mark 再 Full,再 Inc

Exctractor 从 Source DB 读取数据写入内存, Yugong 官方提供 Extractor 抽象出 AbstractRecordExtractor 类, 其余类都是围绕 Oracle 实现。 另外 Yugong 设计了 YuGongLifeCycle 类实现了组件生命周期管理。

Translator

Translator Class

  • DataTranslator:Translator 基类,为 Row 级别数据处理
  • TableTranslator:Translator 基类,为 Table 级别提供处理(官方代码中没有使用)
  • AbstractDataTranslator:Data Translator 虚拟类,做了部分实现
  • EncodeDataTranslator:转换编码格式 Translator
  • OracleIncreamentDataTranslator:为 Oracle 增量数据准备 Translator,会调整一些数据状态
  • BackTableDataTranslator:Demo,允许在 Translator 中做回写数据操作
  • BillOutDataTranslator:Demo,包含一些阿里业务逻辑 Translator
  • MidBillOutDetailDataTranslator:Demo,包含一些阿里业务逻辑 Translator

Translator 读取内存中 RowData 然后变换, 大部分 Translator 做一些无状态操作,比如编码转换。 另外还有一小部分 Translator 做了业务逻辑操作,比如做一些数据回写。

Applier

Applier Class

  • RecordApplier:基础 Applier Interface
  • AbstractRecordApplier:基础 Applier 虚拟类,做了一部分实现
  • CheckRecordRecordApplier:检查数据一致性 Applier,不做数据写入
  • FullRecordRecordApplier:全量数据 Applier,使用 UPSERT 做数据更新
  • IncreamentRecordApplier:增量 Applier,使用 Oracle 物化视图为数据源
  • AllRecordRecordApplier:自动化 Applier,先使用全量数据 Applier,然后使用增量数据 Applier

Applier 将经过 Translator 处理过的数据写入 Target DB。 Yugong 提供了一致性检查、全量、增量 Applier。 比较特殊是 AllRecordRecordApplier 提供了全套自动化操作。

Others

除了 ETL 三个要素,yugong 还有一些重要类:控制类和工具类。

  • SqlTemplate:提供 CRUD / UPSERT 等操作的基类 SQL 模板
  • OracleSqlTemplate:基于 SqlTemplate 实现的 Oracle SQL 模板
  • RecordDiffer:一致性检查 differ
  • YugongController:应用控制器,控制整个应用数据流向
  • YugongInstance:控制单个迁移任务实例,一张表对应一个 YugongInstance

老战士的问题

说 yugong 有问题会有些标题党,毕竟它是久经考验老战士了。 但对我们来说,开源版本 yugong 还有一些不足:

  • 不支持 SQL Server 读取
  • 不支持 SQL Server 写入(Rollback 需要写入 SQL Server)
  • 不支持 MySQL 读取

除了数据库支持,Yugong 在工程上面倒是也有一些改善空间。 我们最后花费了不少时间,做了工程上改进。

  • 抛弃默认打包方式(基于 maven-assembly-plugin 生成类似 LFS 结构 tar.gz 文件), 改为使用 fat jar 模式打包,仅生成单文件可执行 jar 包
  • 抛弃 ini 配置文件,使用 YAML 配置文件格式(已有老配置仍然使用 ini 文件,YAML 主要管理表结构变更)
  • 改造 Plugin 模式,将 Java 运行时编译改为反射获取 Java 类
  • 拆分 Unit Test / Integration Test,降低重构成本
  • 重构 Oracle 继承结构,使其开放 SQL Server / MySQL 接口
  • 支持 Canal Redis 格式数据作为 MySQL 在线增量数据源

改造之后结构

Extractor

Extractor New Class

  • AbstractSqlServerExtractor:新增抽象 SqlServer Extractor
  • AbstractMysqlExtractor:新增抽象 MySQL Extractor
  • AbstractFullRecordExtractor:新增抽象 Full 模式 Extractor
  • SqlServerCdcExtractor:新增 SQL Server CDC 增量模式 Extractor
  • MysqlCanalExtractor:新增 MySQL Canal 格式增量消费 Extractor
  • MysqlCanalRedisExtractor:新增 MySQL Canal 格式增量消费 Extractor,使用 Redis 做回溯
  • MysqlFullExtractor:新增 MySQL 全量 Extractor
  • SqlServerFullExtractor:新增 SQL Server 全量 Extractor

在抽象出三个抽象类之后,整体逻辑更为清晰,如果未来要增加新数据库格式支持,也更为简单。

Translator

Translator New Class

  • Sha1ShardingTranslator:根据 Sha1 Sharding Translator
  • ModShardingTranslator:根据 Value Mode Sharding Translator
  • RangeShardingTranslator:根据范围 Sharding Translator
  • UserRouterMapShardingTranslator:特定业务使用, 用户分表 Sharding Translator
  • UserRouterMapMobileShardingTranslator:特定业务使用, 用户分表 Sharding Translator
  • ClassLearningNoteInfoShardingTranslator:特定业务使用自定义 Translator
  • ClassLearningIsActiveReverseShardingTranslator:特定业务使用自定义 Translator
  • ColumnFixDataTranslator:调整表结构 Translator
  • NameStyleDataTranslator:调整表字段名 Translator,支持按风格对整个表自动转换
  • CompositeIndexesDataTranslator:解决复合主键下唯一 PK 确定问题的 Translator

新增了一系列 Translator。

Applier

Applier New Class

  • SqlServerIncreamentRecordApplier:新增 SQL Server 增量消费 Applier

Applier 结构调整挺小,主要是增加了 SQL Server 的支持。

二次开发心得

如何快速了解一个开源项目?很多同学第一反应就是阅读源码。 看源码固然是有效果,但是性价比太低。 如果项目设计不合理,很快会迷失在代码细节之中。 我的经验是先阅读官方出品的一些 Slide 分享,然后阅读官方核心文档。 Slide 含金量高,在讲述核心中核心。

如果真要去了解细节去阅读源码,那我建议要善用工具, 比如使用 IntelliJ 的 Diagram 功能,抽象出核心类。 还有一些插件比如 SequencePluginReload 方便地生成函数之间调用,实为查看数据流利器。 我在这次开发过程中,也根据生成类图发现了一些问题, 从而在进入 Coding 之前,先对框架继承结构重构。提高了整体开发效率

根据代码风格判断,Yugong 并非是出自一个人之手。这多少会导致代码风格和设计上面不一致。 我自己也常年在业务线里面摸爬滚打,能想象到在快速推进项目中需要糙快猛。 但后人接受开发,多少会有些头疼。 于是我在进入开发之前,引入标准化 CheckStyle,用 Google Style 全局格式化, 使用 Sonar 扫描保证一个代码质量基线。 同时这也是一把双刃剑,格式化项目会导致大量 diff, 这也给我自己埋下了一个苦果,在后期给上游提交 PR 引入无尽问题。

开发过程中我也犯了一些错误。最为头疼是没有在早期考虑到向开源社区贡献, 导致未来向上游合并困难重重,现在还在头疼合并代码中。 另外,由于整体项目时间紧,我贪图实现速度,没有做更详尽单元测试覆盖。 这里没有遵循开源软件的最佳实践。

经过我改造的 Yugong 版本开源地址是:https://github.com/alswl/yugong 。 我也提交了 Pull Request https://github.com/alibaba/yugong/pull/66 , 正在与官方沟通如何将这部分提交并入上游。

❌
❌