Skip to content

Latest commit

 

History

History
549 lines (276 loc) · 33.7 KB

dataflow-architecture-derived-data-views-and-eventual-consistency-e3bc25176cf8.md

File metadata and controls

549 lines (276 loc) · 33.7 KB

数据流架构

原文:towardsdatascience.com/dataflow-architecture-derived-data-views-and-eventual-consistency-e3bc25176cf8?source=collection_archive---------1-----------------------#2024-10-15

健康与健身数据管道的(不那么)简短历史:第二部分

关于衍生数据视图和最终一致性

CALEBTowards Data Science CALEB

·发表于Towards Data Science ·阅读时长 19 分钟·2024 年 10 月 15 日

--

欢迎来到 第二部分 ,我们关于公共健康与健身数据管道的成长三部曲。

在本章中,我们将后端系统重新构想为一个分布式状态机,并探索实现一致性的艺术——带有函数式风格。

第一部分简要回顾

[## 事件驱动架构——从服务器到代理再到队列

健康与健身数据管道的(不那么)简短历史:第一部分

medium.com](https://medium.com/siot-govtech/a-not-so-brief-history-of-a-health-fitness-data-pipeline-part-i-event-driven-architecture-79d2fa8ce189?source=post_page-----e3bc25176cf8--------------------------------)

数据管道的演变

第一部分中,我们观看了SmartGym成长为(2.1 版),一个集成的健康与健身平台,流式传输处理保存来自一系列健身器材传感器和医疗设备的数据。这些数据提供了洞察力,帮助用户更积极地掌控个人的健康与健身。

SmartGym 肩推

随着我们的系统从单纯保存和检索数据发展到响应现实世界的事件,我们的架构必须反映这一范式转变——从请求驱动事件驱动

一个事件驱动的摄取管道

有两个管道维持着系统的运行:

  1. 流式处理管道:数据从传感器中持续流式传输,经过处理并存储到缓冲区。

  2. 保存管道:当用户结束一个会话时,缓存的数据会被处理并保存到数据库中,作为表示用户会话(一次锻炼)的记录。

流式处理保存管道

事件驱动架构是把双刃剑,既引入了秩序,也带来了混乱。最终,我们看着它演变成了一台运转良好的机器,能够驯服它的复杂性。

第二部分:演变继续

在这一部分中,我们将探讨系统的下三个版本,每个版本以不同的方式增强用户的锻炼体验:

  • v3.0:将健身作为一种个性化体验

  • v4.0:将健身作为一种集体体验

  • v5.0:将健身作为一种个性化-集体体验

但首先,让我们认识一下本文的主角!

神奇的黑盒

无论是分布式、事件驱动的,还是其他形式的,我们都可以把 SmartGym/SENSEI 的后台系统看作一个神奇的黑盒。

输入:我们将新信息输入到这个黑盒中——例如:用户信息、传感器数据。

状态转换:这些新信息根据特定的逻辑与现有状态进行交互,产生新的内部状态。

输出:我们可以随时查询其内部状态,以检索相关信息——例如用户的锻炼信息。

神奇的黑盒是确定性的:如果你有两个相同的黑盒,状态和逻辑完全相同,按相同的顺序输入相同的内容,两个黑盒最终会得出相同的内部状态

黑盒内部

如果我们把它拆开来看,我们会发现其中没有什么真正神奇的东西——只有一个由一堆数据视图和摄取管道组成的数据流架构

事实来源(实线)、派生数据视图(虚线)和流式/保存管道逻辑(箭头)

数据视图主要有两种类型:

1. 事实来源(实线)——例如:用户、传感器流

  • 新的数据首先写入这里。这些是原始的、权威的数据——通常以规范化的方式精确表示一次。

  • 系统的状态是这些事实来源和状态转换逻辑的函数——它反映了随时间推移、改变系统状态的事件序列。

2. 派生数据(虚线)——例如:锻炼记录

  • 这些数据是从其他视图中的现有数据中处理出来的,通常涉及反规范化、聚合或转换。它是为高效的未来查询而预计算的。

  • 派生数据是冗余的,实际上是“复制”了现有的信息;如果丢失,它总是可以从原始数据源重新派生出来。

从真实数据源派生数据视图的过程称为 物化,这是一个由摄取管道中的工作者处理的确定性任务。

魔法黑盒子内部的所有 状态 都被封装在这些 数据视图 中,而 摄取管道 — 即物化过程背后的机械运作 — 保持 无状态

请注意,每当真实数据源发生变化时,派生的数据必须重新派生。否则,状态转换不完整,魔法黑盒子将处于 不一致 状态。

v2.1 数据流架构

在前面的例子中,我们向魔法黑盒子输入了:

  • 通过我们的 CRUD API 获取 用户详情

  • 通过事件流传输的 传感器遥测流式管道

这些输入作为真实世界实体或事件的来源,反映了实际情况。

从这两项信息中,我们推导出一个单一的 锻炼 会话记录,进入 保存管道

现在,你可能会对“魔法黑盒子先生”在向你解释看似常识的东西时感到不悦。但请耐心听他讲,因为他将证明自己在本文中是一个有用的抽象概念。

v3.0 — 指标、仪表盘、洞察:将健身转变为个性化体验

随着流式管道和保存管道不懈地将数据摄取到系统中,我们的数据库现在储存着大量的用户和锻炼记录。我们能够通过分析趋势、分组、平均值和总计,提供有意义的宏观洞察,服务于我们的用户和利益相关者。

SmartGym 产品指标仪表盘原型

用户洞察和健身指标

在这里,SmartGym 成为 “每个公民的首选健身伴侣” 的愿景开始逐渐成形。除了在我的锻炼过程中提供实时反馈、回顾我的历史表现并告诉我做得多么出色之外,一个勤勉的健身伴侣还会提供可量化的指标,用来衡量我随时间推移的表现提升。

SmartGym 用户锻炼洞察页面

通过利用最近的锻炼数据和用户信息 —— 比如通过 SmartGym 体重秤捕捉的身体数据 —— 我们可以估算各种表现指标,包括:

  • 1RM(最大单次重复重量) 用于基于重量的锻炼(例如腿举)

  • 每分钟最大重复次数 用于体重训练(例如俯卧撑)

  • VO2 最大值MET(代谢当量) 用于有氧运动(例如跑步机)

推导产品和健身指标通常涉及去规范化和聚合记录,这在内存使用、数据库读取和网络吞吐量方面可能非常消耗资源。

由于每次用户加载仪表盘时都不需要执行这些操作,因此我们需要一个预计算的中间数据表示,准备好进行查询和可视化 —— 另一个 派生数据视图 用于 用户健身指标

周期性处理管道

让我们回到我们的魔法黑盒子。

v3.0 数据流架构

随着新用户和锻炼记录不断流入,用户健身指标—一种衍生的数据视图—需要根据其依赖的上游数据视图的变化不断更新。

为了解决这个问题,我们决定定期重新计算用户健身指标,接受这些指标可能会滞后几个小时的事实。

现在,我们的摄取管道包括一个定时任务服务,该服务根据预设的时间表在周期性管道内安排批量处理作业,从而确保及时更新并避免系统过载。

流式保存周期性管道

v4.0 — 游戏化:将健身作为一种集体体验

从个人到社区的转变

保持健康是一项艰苦的工作。但通过适当的竞争和集体的艰难时光,这可以成为一种超越生活本身的体验。想象一下,如果每一个重复动作、每一组锻炼、每一节锻炼课程都为更伟大的目标做出贡献,那会是什么样?

推出运动排行榜健身挑战

运动排行榜

排行榜展示了本月努力最多的用户——通过跑步距离、举重重量等数据进行衡量。

哇,这个功能可真是让一些人感到自豪!一些常去健身房的人开始把他们随机生成的用户名改成像“Beefy”或“Armstrong”这样的称号。对于很多人来说,查看排行榜成了他们进入健身房后的第一件事,同样也是每次锻炼结束后的仪式,带着新获得的自信昂首离开。

SmartGym 排行榜

类似于我们计算产品和用户健身指标的方式,排行榜数据会定期批量更新,数据来源于用户个人资料和他们的历史锻炼记录。

健身挑战系统

与健身房管理团队合作,我们发起了一项健身挑战,并与新加坡国庆日等特殊时期同步进行。

SmartGym 健身挑战在我们的 Tampines Hub 举行

每天,用户会收到一个挑战,要求他们在加重器械上完成一定次数的重复,或在有氧器械上锻炼一定时间,并因他们的努力获得奖励。

这启动了一系列多样化的健身挑战,每个挑战都有不同的游戏玩法,涉及持续时间、锻炼类型、强度、连续次数等多种变化。

SmartGym 健身挑战用户界面

可配置的规则:规则引擎和语法树

本质上,健身挑战是由管理员指定的一组独特锻炼要求。通过将用户的锻炼历史与这些要求进行对比,我们可以评估他们在挑战中的进展和完成状态。

规则语法树:表示一组胸推/腿推/跑步机锻炼

我们并没有用一大堆 if-else 语句来应对每个健身挑战的变体,而是通过将这些逻辑规则表示为语法树来将业务逻辑外部化。在运行时,规则引擎解析这棵树,并根据用户的实际锻炼历史进行评估,从而追踪他们的挑战进度。

语法树的运行时评估

当程序管理员修改健身挑战的参数时,他们实际上是在直接更新底层规则语法树。这个相同的数据结构在后台规则引擎和前端规则配置页面之间共享,从而确保了一致性和管理的简便性。

SmartGym 健身挑战配置页面

按需处理管道

让我们重新审视一下我们的神奇黑箱。

v4.0 数据流架构

通过规则引擎从训练用户健身挑战数据派生的用户健身挑战结果,每当其所依赖的上游数据视图发生变化时——例如每次用户完成一次锻炼时,都需要重新计算。

在我们充满热情的用户群体中,这些健身挑战是一种荣耀和荣誉的象征。如果他们在完成一组训练后没有立即看到更新的挑战结果,他们会感到困惑和沮丧。因此,我们不能依赖周期性批量处理用户健身挑战结果训练数据视图的每次变化必须立即传播。

为了实现这一点,我们通过引入变更数据捕获机制扩展了摄取管道,添加了一个服务,该服务持续监听相关数据视图中的变化,使用内置数据库触发器或变更流。这一按需管道触发了下游派生数据视图的一连串更新。

在这种情况下,一个按需工作者实现规则引擎的逻辑,实时评估用户健身挑战结果

揭开我们最新的直列四缸发动机,其包含流处理保存处理定期处理按需管道

我们的摄取管道各个阶段的回顾:

  • 流处理:负责将实时传感器数据流摄取、处理并存储到缓冲区

  • 保存处理:负责将来自流缓冲区的数据整合、处理,并保存到数据库中,作为代表单次锻炼或用户会话的记录。

  • 定期处理:负责定期预计算派生的数据视图

  • 按需处理:负责立即传播来自上游数据视图的更新到派生数据视图

v5.0 — 推荐:将健身体验作为个性化-集体的体验

如果我们能为健身挑战增添一些个人化的元素呢?

NSFIT x SmartGym

2021 年底,来自新加坡军队的一个团队描述了他们的困境:每年,军人必须达到特定的健身基准。如果达不到标准,他们将被加入一个结构化的训练项目,称为 NSFIT。然而,这些训练课是有限时段的,需提前报名,并且需要工作人员来促进和监控进度。考虑到当时正在进行的疫情和社交距离措施,集结军人进行集体课程变得不可行。

使用 SmartGym 健身挑战系统,军人可以根据自己的时间安排进行训练——无需工作人员在每一节课上都跟随。只需要工作人员验证训练是否完成并符合标准即可。

基于健身档案的跑步机强度实时推荐

但这里有个转折:军人们的身形、体型和健身水平各不相同。一个适合所有人的健身挑战是行不通的。他们需要的是能够从他们的现有状况出发的东西,才能将他们的健身推向新的高度。

那么,为什么不在策划用户健身挑战之前加入推荐步骤呢?通过利用我们已经计算的健身数据(例如在 v3.0 中),我们可以定制他们随后的训练课程强度。

我们的个性化健身挑战现在遵循三个关键步骤:

步骤 1 — 个人档案 使用历史训练数据,我们为每个用户制定健身水平档案。

为了进一步优化这一过程,我们可以将我们的个人资料方法扩展到简单的启发式方法之外,结合机器学习方法提取更复杂的特征——从而产生新的派生数据视图。

步骤 2 — 推荐 从一个通用的挑战模板开始(包括诸如地点、总训练次数、参与者组、开始/结束日期等信息),推荐引擎将这个骨架模板扩展成一个适合每个用户健身档案的规则语法树。

为了实现更大的个性化,一位领域专家可以手动微调挑战要求,提供一种超越算法推断的专业视角。

步骤 3 — 评估 一旦个性化参数被嵌入到规则语法树中,评估可以在训练保存后按需触发,甚至可以在实时传感器流中进行评估,并显示在前端控制台上。

实时个性化健身挑战评估

v5.0 数据流架构

数据流范式

魔法黑盒重访

在文章早些时候,我们提到过魔法黑盒由数据视图组成,其中包含状态,以及一个无状态的数据摄取管道。

为了从魔法黑盒中获得可靠的输出,这些数据视图必须是一致的,这一点通过在数据摄取管道中的确定性物化序列来实现。

系好安全带,准备好,因为我们将深入探讨一致性确定性——数据流的潜在动力。

一致性的挑战:一个必要的恶性问题

乍一看,创建一个包含所有原始数据细节的大型数据视图似乎更简单。只有一个数据源时,一致性是隐含的。然而,尽管维护一致性复杂,我们依然需要衍生数据视图,原因有几个:

数据多态性

数据可以以多种形式表示——在不同的组合和多个粒度级别上——每种形式都有其独特的用途。

例如,事实证明,用户并不关心他在 2020 年 9 月做胸推时,第 2 组的第 3 次重复动作是否完全伸展——在这个实时窗口过后,低级别的原始细节变得越来越不相关,而高级别的衍生洞察变得更有价值。

为了避免必须假设数据在未来如何使用和表示——原始数据更好,即寿司原则。

读写性能

通过这种方式,我们将写入模型与潜在的读取模型范围解耦,并通过一系列物化阶段弥合差距。这种分离通常被称为命令和查询责任分离(CQRS)。

拥有物化路径为一条数据提供了空间和时间——让它演变并发现其不同的面貌,从而实现:

  • 更快更简单的写入:通过将数据处理和复杂数据模型推迟到后续阶段,从而缩短写入路径。

  • 高效灵活的读取:通过预先计算不同的衍生视图,缩短读取路径。

一致性的基础

通过将写入模型指定为推理的权威真理来源,更容易实现一致性——而无需处理多个权威系统尝试达成共识的复杂性。

有时,原始数据增长得过快。例如,每秒 1 条消息的跑步机传感器,多个健身房的话,一天内传感器流就可能积累数百万条消息。

锻炼记录取代传感器流成为新的权威数据视图

传感器流增长到难以承受的规模时,我们可以将锻炼记录视为传感器流的“有损压缩”,清除处理后的传感器流,并将衍生出的锻炼记录提升为新的权威数据源。

由于物化的单向链条仍然始于单一的真理来源,我们保持了我们的一致性基础。

反脆弱性:故障恢复与应用演化

派生视图提供了弹性。如果出现错误导致输出损坏,我们可以回滚到先前的版本,并重新运行物化过程,从而确保数据的准确性。

派生视图还支持应用的渐进式演变。你可以引入新的数据视图,而不删除或重构旧的视图,保持它们作为同一数据的独立视图,并且如果出现问题,还可以选择回退。

非破坏性的推导逻辑演变

系列第一部分中,我们看到发布-订阅(pub/sub)模式(通过分发交换机)使得在不干扰现有管道或要求上游修改的情况下,能够轻松扩展系统功能,达到即插即用的效果。

敏捷开发和构建抗脆弱系统(即那些通过每次修复错误或添加新特性而变得更强)的关键是恢复和演变的便捷性。发布/订阅模式和派生视图所实现的解耦使这一点成为可能。

一致性和控制的艺术

接下来,让我们剖析数据流架构的一致性和控制流的本质。

广义来说,分布式系统可以通过两种一致性级别——强一致性最终一致性;以及两种控制流类型——协调式(集中式)或编排式(分散式)进行分类。

一致性级别与控制流之间的关系

强一致性保证每次读取都反映最新的写入数据。它确保所有数据视图在更改后立即并准确地更新。强一致性通常与协调式相关联,因为它通常依赖于中央协调器来管理跨多个数据视图的原子更新——要么一次性更新所有,要么一个都不更新。这种“过度工程”可能是某些系统所必须的,尤其是在轻微的差异可能带来灾难性后果的情况下,例如金融交易,但在我们的场景中并不需要。

最终一致性允许数据视图之间暂时存在差异,但只要给定足够的时间,所有视图最终将收敛到相同的状态。这种方法通常与编排式相结合,其中每个工作单元独立且异步地响应事件,而不需要中央协调器。

数据流架构的异步松耦合设计的特点是数据视图的最终一致性,通过编排式的物化逻辑实现。

这样做是有好处的。

好处:在系统层面

对部分故障的韧性:编排的异步性对于组件故障或性能瓶颈更加健壮,因为中断被局部控制。相反,协调可能会在系统中传播故障,通过紧耦合放大问题。

简化的写路径:编排还减少了写路径的责任,从而减少了代码的表面积,减少了错误破坏真相源的可能性。相反,协调将使写路径变得更加复杂,随着不同数据表示的增多,维护难度也会越来越大。

优势:在人类层面

编排的去中心化控制逻辑允许不同的实现阶段独立且并行地开发、专业化和维护。

决定性:事件驱动的工作伦理(重访)

电子表格理想

一个可靠的数据流系统类似于电子表格:当一个单元格发生变化时,所有相关单元格会立即更新——无需手动操作。

在理想的数据流系统中,我们希望实现相同的效果:当上游数据视图发生变化时,所有相关视图无缝更新。就像电子表格一样,我们不应担心它是如何工作的,它应该自然地完成。

但在分布式系统中确保这种级别的可靠性远非易事。网络分区、服务中断和机器故障是常态而非例外,且数据接收管道中的并发性只会增加复杂性。

由于数据接收管道中的消息队列提供了可靠性保证确定性重试可以使瞬时故障看起来像是从未发生过。为了实现这一点,我们的数据接收工作者需要采纳事件驱动的工作伦理

纯函数没有自由意志

在计算机科学中,纯函数表现出决定性,意味着它们的行为是完全可预测和可重复的。

它们是短暂的——此时此刻存在,下一刻便消失,生命周期结束后不再保持任何状态。赤裸裸地来,赤裸裸地去。从它们诞生时刻刻画的不可变消息中,它们的遗产就已经注定。它们总是为相同的输入返回相同的输出——一切如命中注定般展开。

这正是我们希望我们的数据接收工作者具备的特性。

不可变输入(无状态性) 这个不可变的消息封装了所有必要的信息,消除了对外部可变数据的依赖。本质上,我们是通过值而非引用将数据传递给工作者,这样处理一个消息在明天时得到的结果将与今天相同。

任务隔离

为了避免并发问题,工作者不应共享可变状态。

工作者内部的过渡状态应当被隔离,就像纯函数中的局部变量一样——不依赖共享缓存进行中间计算。

确保任务的独立性同样至关重要,确保每个工作者处理的任务不会共享输入或输出空间,从而允许并行执行而不发生竞争条件。例如,通过特定的user_id对用户健身分析任务进行作用域限定,因为输入(锻炼记录)和输出(用户健身指标)与唯一的用户相关联。

确定性执行 非确定性可能轻易潜入:使用系统时钟、依赖外部数据源、基于随机数的概率/统计算法等,都可能导致不可预测的结果。为防止这种情况,我们将所有“动态部分”(例如随机种子或时间戳)直接嵌入不可变的消息中。

确定性排序 使用消息队列的负载均衡(每个队列有多个工作者)可能导致消息处理的顺序错误,特别是在消息重试时,若后续消息已处理完毕。比如,用户健身挑战结果的评估顺序错误,出现从 50%到 70%再回到 60%的情况,而实际上它应该是单调递增的。对于需要顺序执行的操作,如插入记录后通知第三方服务,顺序错误的处理可能会破坏这种因果依赖关系。

在应用层,这些顺序操作应该要么在单个工作者上同步运行,要么拆分成多个独立的顺序物化阶段。

在数据摄取管道层面,我们可以为每个队列分配一个工作者,以确保串行处理,直到重试成功为止。为了维持负载均衡,你可以使用多个队列,并通过一致性哈希交换来路由消息,路由的依据是路由键的哈希值。这实现了类似于 Kafka 的哈希分区键方法的效果。

幂等输出

幂等性是一种属性,多次执行同一段代码应该始终产生相同的结果,无论它执行多少次。

例如,一个简单的数据库“插入”操作不是幂等的,而一个“如果不存在则插入”操作是幂等的。

这确保了你得到的结果就像工作者只执行了一次一样,无论实际重试了多少次。

警告:请注意,与纯函数不同,工作者并不会“返回”一个对象(从编程的角度来看)。相反,它们会覆盖数据库的一部分。虽然这看起来像是副作用,但你可以将这种覆盖视为类似于纯函数的不可变输出:一旦工作者提交结果,它就反映了一个最终的、不可更改的状态。

数据流递归

客户端应用中的数据流

传统上,我们认为 Web/移动应用是无状态的客户端,它们与中央数据库进行通信。然而,现代的“单页应用”框架改变了这种格局,提供了“有状态”的客户端交互和持久化的本地存储。

这将我们的数据流架构扩展到后端系统的范围之外,涵盖了多种客户端设备。可以将设备上的状态(即“模型-视图-控制器”中的“模型”)视为服务器状态的派生视图——屏幕显示的是设备本地状态的物化视图,它反映了中心后端的状态。

推送协议,如服务器推送事件和 WebSockets,将这个类比进一步扩展,使得服务器可以主动将更新推送到客户端,而不依赖于轮询——实现从端到端的最终一致性。

v5.0 数据流架构(扩展版)

事实上,这种实时同步正是我们如何在前端控制台中评估个性化健身挑战的方式——作为驻留在客户端设备上的派生数据视图。

实时个性化健身挑战评估

数据库中的数据流

即使在技术栈的底层,我们也能看到数据库中的数据流的雏形。数据库触发器、存储过程和物化视图维护例程与按需和定期处理管道并无太大不同;B 树索引和关系数据库的物化视图本质上是派生的数据视图——谈谈数据流中的数据流!

数据流,数据流,无处不在

“数据集成的目标是确保数据以正确的形式出现在所有正确的位置。”

—《设计数据密集型应用程序》(马丁·克莱普曼)

随着数据系统的扩展,我们应超越将其视为被应用程序操作的被动数据库(如全局变量)的局限。

相反,重新构想组织中的数据系统是非常有益的,它们作为数据视图的相互作用,其中一个从另一个派生,状态变化从一个中心真实源传播,通过功能应用代码传播。数据流,基于数据流。

这是魔法黑盒子——一直到底。

总结

恭喜你坚持到这里!

在第一部分中,我们从一个简单的请求-响应系统发展到一个事件驱动系统,流式传输处理保存来自各种健身器械传感器和医疗设备的数据。

在这第二部分中,我们扩展了那些保存的记录,并对其进行了定期按需处理。这使得增强用户运动体验的新功能成为可能,体验变得更加集体却又个性化。随着我们的摄取管道的发展,我们的数据流架构也在扩展,能够满足新的需求。

基于数据摄取管道的 SmartGym 功能总结

我们的演变故事并未就此结束。

在下一部分,也是最后一部分,我们将探讨如何以即插即用的方式添加和移除功能,为我们的平台孕育出一个生态系统。

敬请期待……

本文中的所有图片和 GIF 都是作者原创作品。

感谢数据工程圣经,即《设计数据密集型应用》一书——马丁·克莱普曼(Martin Kleppmann)为我提供了清晰思考这些分布式系统的词汇。

一个(不那么)简短的健康与健身数据管道历史 — 系列

[## 事件驱动架构 — 从服务器到中介到队列的演变]

一个(不那么)简短的健康与健身数据管道历史:第一部分

medium.com [## 基于数据流架构的衍生数据视图与最终一致性]

一个(不那么)简短的健康与健身数据管道历史:第二部分

towardsdatascience.com

了解更多关于我队友们的功能开发内容

用户洞察仪表盘

[## 从健身爱好者到开发者:构建智能健身房愿景]

这是我与 GovTech 的 SmartGym 团队一起度过的短暂而有意义的实习经历!

medium.com

产品指标仪表盘

[## 真正的 Python 网页开发?]

我与一个非常规的网页开发框架:Plotly Dash 的工作经验

medium.com [## 实习经验 — 不要匆匆完成数据分析]

作为数据分析师与 SmartGym 团队一起度过的充实实习之旅

medium.com

排行榜和健身挑战分析仪表盘

[## 我在 SmartGym 团队的实习

介绍

medium.com](https://medium.com/ytpo-govtech/internship-blog-7b021006e020?source=post_page-----e3bc25176cf8--------------------------------) [## 不要在黑暗中投掷飞镖

介绍

medium.com](https://medium.com/siot-govtech/dont-throw-darts-in-the-dark-11e3404f8436?source=post_page-----e3bc25176cf8--------------------------------) [## 我在 SmartGym 最有成就感的时刻

medium.com](https://medium.com/siot-govtech/my-most-fulfilling-moment-at-smartgym-843819c77432?source=post_page-----e3bc25176cf8--------------------------------)

为物理治疗定制的个性化锻炼计划

[## 为物理治疗师和患者设计端到端的体验

介绍

medium.com](https://medium.com/siot-govtech/designing-an-end-to-end-experience-for-physiotherapist-and-patients-910991042f46?source=post_page-----e3bc25176cf8--------------------------------)