跳到主要内容

42 篇博文 含有标签「system-design」

查看所有标签

AI Agent 的 CAP 定理:为何你的 Agent 在本该优雅降级时却彻底崩溃

· 阅读需 10 分钟
Tian Pan
Software Engineer

你的 AI Agent 运行得一切正常,直到某一刻它彻底不行了。某个工具宕机——也许是搜索 API 触发了限流,也许是数据库响应迟缓,也许是代码执行沙箱超时——整个 Agent 随之崩溃。不是部分答案,不是降级响应,而是彻底失败。要么一片空白,要么满是幻觉。

这不是一个 Bug,而是一个设计选择——而且几乎没有人是刻意做出这个选择的。我们今天所构建的 Agent 架构隐式地选择了"彻底失败",原因只有一个:没有人设计过部分可用路径。如果你有分布式系统的经验,这个模式应该让你感到似曾相识。这正是 CAP 定理,以一副新的面孔出现了。

AI Agent 工作负载的缓存层级:多数团队止步于第二层的五层架构

· 阅读需 13 分钟
Tian Pan
Software Engineer

大多数部署 AI Agent 的团队在实现了提示词缓存 (Prompt Caching),或许再加上语义缓存之后,就认为大功告成了。但他们实际上错失了 40-60% 的潜在节省空间。原因并非在于懒惰 —— 而是 Agent 工作负载产生的缓存问题在简单的请求-响应式 LLM 调用中并不存在,其解决方案需要从传统 Web 缓存从未涉及的层级进行思考。

单个 Agent 任务可能涉及一个 4,000 Token 的系统提示词、三个分别返回不同结构数据的工具调用、一个在结构上与昨天完全相同的多步计划,以及一个需要在对话中持久化但绝不能跨用户共享的会话上下文。其中每一项都代表了不同的缓存机会,具有不同的 TTL (生存时间) 要求、不同的失效触发机制,以及在缓存失效时不同的故障模式。

合并再调用:无需降低用户体验即可削减成本的 LLM 请求批处理模式

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数团队都是以同样的方式发现请求合并的:收到一张出乎意料的大额账单。他们上线了基于 LLM 的功能,使用量增长,然后账单仪表板显示他们每天为五万个请求付费,而仔细观察后发现其中大约三万个请求在问同一件事,只是措辞略有不同。每一个"总结这份文档"的改写都单独命中了模型。每一个近乎重复的请求都触发了完整的推理周期。成本随流量规模线性增长,而不是随用户实际想要的语义多样性增长。

请求合并正是解决这一问题的模式。它不是单一技术,而是一种分层架构:用于防止并发重复的飞行中去重、用于重复相同提示的精确缓存,以及用于捕捉中间改写变体的语义批处理。顺序很重要,阈值很重要,理解该模式何处会失效——尤其是围绕流式传输——是可用实现与那种在暂存服务器上节省了钱但在生产中引发隐蔽 bug 的实现之间的差别所在。

一亿美元的遥测错误:OpenAI 的故障教会我们系统设计的知识

· 阅读需 3 分钟

在 2024 年 12 月 11 日,OpenAI 发生了一次灾难性的故障,使 ChatGPT、他们的 API 和 Sora 中断了超过四个小时。虽然故障发生在每家公司身上,但这次故障特别引人注目,因为它揭示了现代系统设计的一个关键教训:有时我们添加的工具以防止故障,反而成为故障的根源。

十亿美元的讽刺

有趣的是:这次故障并不是由于黑客攻击、部署失败,甚至不是他们的 AI 模型中的错误引起的。相反,它是由于一个旨在提高可靠性的工具引起的。OpenAI 正在添加更好的监控以防止故障时,意外地造成了他们有史以来最大的故障之一。

这就像雇佣一个保安,结果他把所有人都锁在了楼外。

故障滚出的雪球

事件的经过如下:

  1. OpenAI 部署了一个新的遥测服务,以更好地监控他们的系统
  2. 该服务用 API 请求淹没了他们的 Kubernetes 控制面板
  3. 当控制面板失败时,DNS 解析也中断了
  4. 没有 DNS,服务无法相互找到
  5. 工程师无法修复问题,因为他们需要控制面板来移除有问题的服务

但最有趣的部分不是故障本身,而是多个保障系统同时失败:

  1. 测试没有捕捉到问题,因为它只在规模上出现
  2. DNS 缓存掩盖了问题,足够长的时间让它传播到各处
  3. 用来修复问题的系统恰恰是那些崩溃的系统

三个关键教训

1. 规模改变一切

遥测服务在测试中工作得很好。问题只在部署到数千个节点的集群时出现。这突显了现代系统设计中的一个基本挑战:一些问题只在规模上出现。

2. 保障系统可能成为风险因素

OpenAI 的 DNS 缓存,旨在提高可靠性,实际上通过掩盖问题使情况变得更糟,直到为时已晚。他们的 Kubernetes 控制面板,旨在管理集群健康,成为了单点故障。

3. 恢复计划需要恢复计划

最令人震惊的部分?工程师无法修复问题,因为他们需要正常工作的系统来修复损坏的系统。这就像需要一把梯子才能够到你需要的梯子。

系统设计的未来

OpenAI 的响应计划揭示了系统设计的未来走向:

  1. 解耦关键系统:他们将 Kubernetes 数据面板与控制面板分开,减少相互依赖
  2. 改进测试:他们正在添加故障注入测试,以模拟大规模故障
  3. 应急程序:他们正在建立即使在其他一切失败时也能工作的紧急访问系统

这对你的公司意味着什么

即使你不是在 OpenAI 的规模下运营,这些教训依然适用:

  1. 在规模上测试,而不仅仅是测试功能
  2. 提前建立紧急访问系统
  3. 质疑你的保障系统——它们可能隐藏着风险

可靠系统的未来并不是防止所有故障,而是确保我们能够快速而优雅地从故障中恢复。

记住:最危险的问题不是我们能预见到的,而是那些从我们构建的保障系统中突然冒出来的。

快速介绍 Optimism 架构

· 阅读需 5 分钟

什么是 Optimism?

Optimism 是一种 EVM 等效的乐观汇总协议,旨在扩展以太坊。

  • 扩展以太坊意味着增加以太坊网络可以处理的有用交易数量。
  • 乐观汇总 是一种第二层可扩展性技术,它在不牺牲安全性或去中心化的情况下,增加以太坊的计算和存储能力。
  • EVM 等效性 是与以太坊黄皮书中描述的状态转移函数的完全合规,后者是该协议的正式定义。

乐观汇总通过将多个交易打包成一个单一交易来工作,然后由以太坊网络上的智能合约进行验证。这个过程被称为“汇总”,因为单个交易被组合成一个更大的交易,并提交到以太坊网络。术语“乐观”指的是系统假设交易是有效的,除非有证据证明相反,这使得交易的处理更快、更高效。

整体架构

Optimism 架构

op-node + op-geth

汇总节点可以以验证者或排序者模式运行:

  1. 验证者(即验证器):类似于运行以太坊节点,它在本地模拟 L2 交易,而不限制速率。它还允许 验证者 验证 排序者 的工作,通过重新推导 输出根 并将其与 排序者 提交的进行比较。如果不匹配, 验证者 可以执行 故障证明
  2. 排序者: 排序者 是一个特权角色,它接收来自 L2 用户的 L2 交易,使用这些交易创建 L2 块,然后将其提交给 数据可用性提供者(通过 批处理器)。它还将 输出根 提交给 L1。目前整个堆栈中只有一个排序者,这也是人们批评 OP 堆栈不去中心化的地方。

op-batcher

批量提交者,也称为 批处理器,是将 L2 排序者 数据提交到 L1 的实体,以使其对验证者可用。

op-proposer

提议者生成并提交 L2 输出检查点到以太坊上的 L2 输出预言机合约。在最终确认期结束后,这些数据使提款成为可能。

批处理器和提议者都将状态提交到 L1。为什么它们被分开?

批处理器收集并将交易数据批量提交到 L1,而提议者将承诺(输出根)提交到 L2 的状态,从而最终确定 L2 账户状态的视图。它们是解耦的,以便可以并行工作以提高效率。

contracts-bedrock

各种合约用于 L2 与 L1 进行交互:

  • OptimismPortal:一个 L2 交易的馈送,这些交易源自 L1 状态中的智能合约调用。
  • 批量收件箱:一个 L1 地址,批量提交者将交易批量提交到该地址。
  • L2 输出预言机:一个智能合约,存储 L2 输出根 以用于提款和故障证明。

Optimism 组件

如何存款?

如何提款?

对 Optimism 文档的反馈

理解 OP 堆栈可能会很具挑战性,原因有很多。其中一个因素是许多组件在代码和文档中多次以略微不同的名称被提及。例如,术语“op-batcher”和“批量提交者” / “验证者”和“验证器”可能可以互换使用,这导致混淆和理解每个组件的确切功能的困难。

理解 OP 堆栈的另一个挑战是不断发展的架构,这可能导致某些设计元素随着时间的推移而被弃用。不幸的是,文档可能并不总是更新以反映这些变化。这可能导致进一步的混淆和理解系统的困难,因为用户可能正在使用过时或不准确的信息。

为了克服这些挑战,重要的是仔细审查所有可用文档,保持概念在各个地方的一致性,并随时了解 OP 堆栈的任何变化或更新。这可能需要额外的研究和与其他用户或开发者的合作,但这是完全理解和有效利用这个复杂系统的必要条件。

如何设计区块链服务端的架构?

· 阅读需 7 分钟

需求分析

  • 分布式的区块链记账和智能合约系统
  • 节点之间相互不大信任,但是又需要激励他们互相合作
    • 交易不可逆
    • 不依赖可信的第三方
    • 保护隐私,透露最少信息
    • 不依赖中心化的权威证明一笔钱不能花两次
  • 假设性能不是问题,暂不考虑如何优化性能

构架设计

具体模块和他们之间的交互

基础层(P2P 网络、加密算法、存储)

P2P 网络

分布式系统有两种实现方式:

  • 中心化的 lead / follower 的分布式,比如 Hadoop, Zookeeper 这种系统,结构比较简单,但是对 lead 要求高
  • 去中心化的对等 (P2P) 网络分布式,比如 Chord, CAN, Pastry, Tapestry 算法组织起来的网络,结构比较复杂,但是更加平等

因为前提条件是节点之间不大信任,所以选择 P2P 的形式。具体到如何组织 P2P 的网络呢?一个典型的去中心化的节点和网络是这样保持连接的:

  1. 基于 IP 协议,节点上线占用某个地址 hostname/port,利用初始化的节点列表广播自己的地址,利用这些初始的 hop,试图向全网 flood 自己的信息
  2. 接到广播的初始节点一方面存下这个 neighbor,一方面帮助他 flooding,不相邻的节点收到后 NAT 穿墙加 neigbhor
  3. 节点之间 anti-entropy 随机互相 heartbeat 发出最新的带着类似 vector clock 的信息,保证能够持续更新对方那里自己的最新信息

我们可以利用既有的库,比如 libp2p,来实现网络模块。网络协议的选择见Crack the System Design Interview: Communication.

加密算法

在互相不大信任的分布式系统中,一笔转账如何在不泄漏自己秘密信息的同时证明这个转账是自己发起的呢?非对称加密:一对公钥和私钥对应一个”所有权”。Bitcoin 选择 secp256k1 参数的 ECDSA 椭圆曲线加密算法,为了兼容,其他链也基本选择同样的算法

为什么不直接把公钥作为转账的地址呢?隐私问题,交易的过程应该尽可能少地泄漏信息,用公钥的哈希作为“地址”可以避免接收方泄露公钥。甚至,人们应该避免反复使用同一个地址

具体到账本的账户,有两种实现方式 UTXO vs. Account/Balance

  • UTXO (unspent transaction output) ,例如 Bitcoin,类似于复式记账的 credit 和 debit,每个 transaction 有都有 input 和 output,但是除了 coinbase 每个 input 的前面都连着上一个的 output。尽管没有账户的概念,但是取一个地址对应的所有的未花完的 output,就是这个地址的余额。
    • 优点
      • 精准:类似于复式记账的结构,让流水账非常精准地记录下所有的资产流动。
      • 保护隐私和抗量子攻击:如果用户经常换地址的话。
      • 无状态:为提高并发留下了可能。
      • 避免重放攻击:因为重放会找不到 input 对应的 UTXO
    • 缺点
      • 记录了所有的交易,复杂,消耗存储空间。
      • 遍历 UTXO 花时间。
  • Account/Balance,例如 Ethereum,有三个主要的 map:account map, transaction map, transaction receipts map. 具体在实现上,为了缩减空间、防篡改,使用 merkle patricia trie (MPT)
    • 优点
      • 省空间:不像是 UTXO 那样,一个 transaction 把多个 UTXO 联系起来
      • 简单:把复杂性让给了 script
    • 缺点
      • 需要用 nonce 解决重放问题,因为 transaction 之间没有依赖性

值得一提的是 “区块 + 链” 的数据结构本质上来讲,就是 append-only 的 Merkle tree,也被称为 hash tree.

存储

因为本身 UTXO 或者 MPT 这种结构就充当了索引,加上分布式的时候,为了让每个节点运维更加简单,一般做 data persistence 的时候倾向于选择 in-process database 随着节点的程序能够直接跑,比如 LevelDB, RocksDB。

因为这种索引并不是通用的,所以你并不能像是 SQL 数据库那样查询,这也为数据分析提高了门槛,优化时需要专门做一个 indexer 服务,比如 etherscan。

协议层

现在我们有了可以操作的基础层后,在这层之上,我们需要一个比较通用的逻辑操作的协议层。根据这个区块链的使用需求,可以像是微内核构架那样,插拔具体的逻辑处理模块。

比如最常见的记账:在最新的 block 高度收到了一些 transaction,组织起来建立如上一层构架所说的数据结构。

为每一个业务逻辑写一个原生模块然后更新所有节点的代码不大现实,用虚拟化的方法解耦这一层?答案是能够执行 smart contract 代码的虚拟机。在互不可信的环境中,不能让客户白执行代码,所以这个虚拟机最独特的功能可能是计费。

基于 contract 的 token 比如 ERC20 和 native token 的区别,导致在不同的 token 捣腾的时候很麻烦,于是就出现了 Wrapped Ether 这种 token.

共识层

协议层算出来执行的结果之后,如何跟其他的节点达成一致呢?有如下一些常见的激励大家合作的机制:

  • proof of work (POW): 用 hash 的碰撞挖 token,耗电量大不环保
  • proof of stake (POS): 用质押的 token 挖 token
  • delegated proof-of-stake (DPOS): 选人民代表用质押的 token 挖 token

在激励机制的基础上,节点中谁的链最长听谁的,两群人互不待见就分叉。

同时,有这样一些一致性协议让大家达成共识(也就是大家要么一起都干,要么一起都不干)

  • 2PC:大家都依赖某一个 coordinator:coordinator 问大家:要不要干?只要有人回复不干,那么 coordinator 跟所有人都说“不干”;否则都说干。 这样的依赖会导致,如果 coordinator 在第二个阶段的中间挂了,有些节点会不知道怎么办 block 在那里,需要人工干预重启 coordinator。
  • 3PC:为了解决上述问题,加一个保证大家在干之前都知道所有人要干还是不干的阶段,出了错就重新选 coordinator
  • Paxos:上述的 2PC 和 3PC 都依赖某一个 coordinator,如何干掉这个 coordinator 呢?用“大多数(2f + 1 里至少 f+1)”来取代,在两步中,只要大多数取得一致,最后就能取得一致。
  • PBFT (deterministic 3-step protocol): 上述的做法容错率还是不够高,于是有了 PBFT。用来保证大多数(2 / 3)节点要么都同意,要么都不同意的算法,具体做法是三轮投票,每轮有至少大多数(2 / 3)节点同意,最后一轮才 commit block 。

在具体的应用中,关系数据库大多用 2PC 或者 3PC ;Paxos 的变种有 Zookeeper 的实现,Google Chubby 分布式锁的实现,Spanner 的实现;区块链中,Bitcoin, Ethereum 是 POW,新的 Ethereum 是 POS,IoTeX 和 EOS 是 DPOS。

API 层

Public API choices

设计以人为本的国际化(i18n) 工程方案

· 阅读需 10 分钟

需求分析

如果问硅谷的公司跟中国最大的不同是什么,我想答案很可能正如吴军所言,硅谷公司的产品大多面向全球市场。陈志武说的好,创造财富能力有三个衡量维度:深度,即生产力,同样的时间提供更好产品或服务的能力;长度,即利用金融杠杆,跨越时间和空间交换价值的能力;广度,即市场大小,开创跨越地域的市场或者新行业的能力。而国际化,也就是产品和服务在语言和文化上的本地化,正是跨国公司征战全球市场的战略要地。

Internationalization 因为字母 i 后面跟了 18 个字母然后以 n 结尾,所以被称为 18n,我们这次设计的 i18n 工程方案主要是解决网站和移动 App 开发过程中的如下问题:

  1. 语言
  2. 时间与时区
  3. 数字与货币

构架设计

语言

逻辑和细节

语言的本质是把消息交付给受众的媒介,不同的语言就是不同的媒介,不同的媒介面向不同的受众。比如,我们要对用户显示文字:“你好,小丽!”,显示的过程就是查一下语言表,根据用户的语言,和当前需要的插值,比如姓名,显示相应的消息:

Message CodesLocalesTranslations
home.helloenHello, ${username}!
home.hellozh-CN你好, ${username}!
home.helloIW!${username}, שלום

不同语言在细节上略有不同,比如一个物品的单数和复数的形式;比如第三人称,在称呼上男性和女性的区别。

这些都是简单的查表无法应对的问题,需要更复杂的逻辑处理。在代码中你可以无脑地使用条件语句去处理这些特例。此外有一些国际化的框架会发明 DSL (domain specific language) 来专门应对这种情况。以 The project fluent 为例:

还有一个新手容易忽略的问题是行文的方向。中文和英文等常用语言是从左至右的,但是还有一些语言,是从右往左的,比如希伯来文和阿拉伯文。

行文方向的不同不仅仅会影响到文字本身,还会影响到输入的方式。中国人如果从右至左输入会觉得非常的奇怪;而我的一位犹太同事就觉得英文和犹太文混着输入轻而易举。

还有一种情况就是布局。整个 UI 的布局、视觉元素比如箭头的方向。都可能会根据语言的方向的不同而发生变化。你的 HTML 需要设置好相应的 dir 属性

如何确定用户的地域?

你可能会问,我们如何知道用户当前的语言设置呢?如果是浏览器的话。在用户请求网页的时候,会有一个 header Accept-Language 标注接受的语言。这些设置来自于用户的系统语言,以及浏览器的设置。移动 App 情况下,通常都会有获取 locale 变量或者常量的 API。还有一种方式是根据用户的IP 或者 GPS 信息知道用户的位置,然后显示相应的语言。如果是跨国公司,用户在注册的时候,通常会标注出用户注册时候的语言习惯、地理区域。

如果用户想要改换语言,网站的做法各有千秋,移动 App 会有相对固定的 API。网页有这样几种方法:

  1. 设置 locale cookie
  2. 使用不同的子域名
  3. 使用专有域名。 Pinterest 有一篇文专门讲他们如何用本地化的域名。 研究表明使用本地域名后缀的点击率会更高。
  4. 使用不同的路径
  5. 使用 query params。这个做法虽然能用,但是对 SEO 不友好。

新手在做网站的时候容易忘记在 HTML 上标注 lang 标签。

翻译管理系统

当你注意到如上种种细节。小心翼翼的实现了文字语言的显示之后。你会发现,翻译库的建立和管理,也是一个麻烦的过程。

通常开发者并不会有多语言的功底。这时候就需要引入外部的翻译官或者是别人已经建立好的翻译库。而这里的难点在于,翻译官往往并不是技术人员。如果让他们直接改代码、或者直接跟开发人员沟通,会极大的增加翻译的成成本。所以在硅谷的公司,这种提供给翻译官使用的翻译管理系统 (translation management system),往往是有一个团队专门来做,或者直接采购现有的方案,比如说,闭源收费的 lokalise.co ,或者是开源的 Mozilla Pontoon。翻译管理系统可以统一管理翻译库、项目、审核、任务分配。

这样一来,开发的流程就会变成,首先设计师根据不同的语言和文化习惯,在设计的时候标志出需要注意的地方,比如这个按钮虽然在英文里很短,但是在俄文里面会非常长,要注意不要溢出。然后,开发者开发团队根据设计的需求实现具体的代码逻辑,并在翻译管理系统中提供消息码、上下文的背景、以及一个开发者熟悉的语言写成的例子。再然后,翻译官团队在管理系统中填上各种语言的翻译。最后,开发团队把翻译库拉回代码库中,发布到产品中。

其中,上下文的背景是容易被忽视而且不容易做好的地方。这个需要翻译的消息在UI界面的什么地方?用作什么用途,如果消息过短的话,还应该进一步的解释这个消息是什么意思。那么翻译官有了这个背景知识之后,就能够更加精准地加上其他语言的翻译。如果翻译官对想要表达的信息。无法透彻的理解。他们还需要拥有一个提供反馈的渠道,能够找到产品的设计和开发者询问问题。

这么多的语言和文字,通常都不是由一个翻译官来解决的,这通常需要很多个国家语言身份的人一起来为这个翻译库添砖加瓦。整个过程耗时耗力,所以翻译官通常是有专门的团队来负责的,比如外包给 smartling。

现在我们已经有了代码逻辑和翻译库。接下来的问题是:如何把翻译库的内容搬到产品中?

具体可以有很多不同的实现方式,最直接的就是,静态的做法,每次更新的时候。交一个 diff,然后在 merge 到代码当中。这样在构建的时候,就会有就已经有了相关的翻译资料在代码里面。

还有一种做法是动态地做。一方面,可以去远程的翻译库“拉取”内容,这种情况在网站流量大的时候,可能会有性能问题。但是好处是,翻译永远是最新的。另一方面,想要做优化的话,可以采取“推送”的方式,每次翻译库有新改动,触发一个 webhook 来把内容推到服务器上。

在我看来,维护翻译会比添加翻译更加的繁琐。我曾经看到一些很大的项目,因为更新翻译之后没能够及时的删除老的翻译,导致翻译库过于的庞大,整个项目变得乱七八糟。这个时候如果有一个好的工具,能够保证数据的一致性。会对清洁的代码,有非常大的帮助。

阿里巴巴的 Kiwi 国际化全流程解决方案就做了 linter 和 VS Code 插件来帮助你检查和抽取代码中的翻译。

时间和时区

谈完了语言,接下来是时间和时区问题。因为是全球化的公司,所以说有很多数据是来自于全球、显示给全球的用户的。举个例子。国际航班在设置开始时间结束时间的时候如何保证这个时间在全局是一致的,并且在不同的时区会相应的显示。这非常的重要。同样的情况还应用于一切跟时间有关的事件,比如预定酒店、预定餐馆、安排会议。

首先时间有这样几种典型的表现形式。

  1. 自然语言,比如 07:23:01, 星期一 28, 十月 2019 CST AM/PM
  2. Unix timestamp (Int 类型),比如 1572218668
  3. Datetime. 注意 MySQL 存datetime 的时候会根据服务器时区转化成 UTC 然后存起来,读取的时候再转换回来。但是呢,服务器时区一般都是设置成 UTC 的。这种情况就是,存储不带时区,默认 UTC。
  4. ISO Date,比如 2019-10-27T23:24:28+00:00,这是带时区信息的。

我对这些形式没有大的偏好,你如果有相关经验,欢迎留言讨论。

具体在显示的时候。有两个可能出现的转化,一个是,从服务器存储的时区转化成当地时区显示的形式;另外一个是,语言上会由机器代码转换成自然语言。后一步流行的做法是使用强大的处理时间和日期的库,比如 moment.jsdayjs

数字与货币

不同国家区域对于数值的显示,其实是天差地别的。数值中间的逗号和点,在不同的国家,有不同的含义。

(1000.1).toLocaleString("en")
// => "1,000.1"
(1000.1).toLocaleString("de")
// => "1.000,1"
(1000.1).toLocaleString("ru")
// => "1 000,1"

阿拉伯数字并不是在所有区域都通用的,比如 Java 的 String.format 中 1、2、3这种数字在真正的阿拉伯语言里,使用的数字是١、٢、٣

价格方面,同样的货物,在不同的国家地区,是否要显示成当地货币的价值?货币的符号是什么?货币能够精确到哪一位?这些问题统统要先做好准备。

总结

本文提到的国际化工具有,翻译管理系统,开源的 Mozilla Pontoon、闭源收费的 lokalise.co,POEditor.com 等等。代码上的一致性 阿里巴巴 Kiwi 国际化全流程解决方案。UI 显示上的 moment.js, day.js

如同一切软件系统的开发一样,国际化这件事情没有银弹,好的作品都是靠基本功一点一滴磨出来的。

设计负载均衡器

· 阅读需 4 分钟

需求分析

互联网服务往往要处理来自全世界的流量,但是,一个服务器只能够同时服务有限数量的请求。因此,通常我们会有一个服务器集群来共同处理这些流量。那么问题来了,怎样才能够让这些流量均匀地分布到不同的服务器上呢?

从用户到服务器,会经过很多的节点和不同层级的负载均衡器。具体来讲,我们这次设计的需求是:

  • 设计第7层的负载均衡器,位于数据中心的内部。
  • 利用来自后端实时的负载信息。
  • 服务每秒千万级的流量以及10 TB每秒级别的吞吐量。

补充:如果服务 A 依赖服务 B,那我们称 A 是 B 的下游服务,而 B 是 A 的上游服务。

挑战

为什么负载均衡会很难做?答案是很难收集准确的负载分布数据。

按照数量分布 ≠ 按照负载分布

最简单的做法是根据请求的数量,随机地或者循环地分布流量。然而,实际的负载并不是根据请求的数量来算的,比如有些请求很重很耗CPU,有些请求很轻量级。

为了更加准确地衡量负载,负载均衡器得保持一些本地状态 —— 比如,存当前的请求数、连接数、请求处理的延迟。基于这些状态,我们能够使用相应的负载均衡的算法 —— 最少连接、最少延迟、随机 N 取一。

最少连接:请求会被导向当前连接数最小的服务器。

最少延迟:请求会被导向最少平均反应时长且最少连接数的服务器。还可以给服务器加权重。

随机 N 取一 (N 通常是 2,所以我们也可以称之为二选一的力量):随机的选两个服务器,取两者之中最好的,能够避免最坏的情况。

分布式的环境

在分布式的环境中,本地的负载均衡器难移了解上下游服务完整的状态,包括

  • 上游服务的负载
  • 上游服务可能超级大,因此很难选择一个合适的子集接入负载均衡器
  • 下游服务的负载
  • 不同种类的请求的具体处理时间很难预测

解决方案

有三种方案能够准确地搜集负载的具体情况并相应地处理:

  • 中心化的一个均衡器,根据情况动态地处理
  • 分布式但是各个均衡器之间要共享状态
  • 服务器返回请求的时候捎带上负载信息,或者是均衡器主动询问服务器

Dropbox 在做 Bandai 的时候选择了第三种方案,因为这很好地适应了现行的随机 N 选一的算法。

然而,与原配的随机 N 选一的算法所不同的是,不是使用本地的状态,而是选择服务器实时返回的结果。

服务器使用率:后端服务器设置了最大负载,数当前的连接,然后计算出使用率,范围是从 0.0 到 1.0.

有两个问题需要考虑:

  1. 处理错误: 如果 fail fast ,由于处理得很快,反而会吸引更多的流量产生更多的错误。
  2. 数据要衰减: 如果服务器的负载太高,没有请求会发到那里。因此,使用一个类似于反 S 曲线的衰减函数来保证老数据会被清理掉。

结果: 服务器接收的请求更加的均衡了

Lyft 的营销自动化平台 Symphony

· 阅读需 3 分钟

获客效率问题:广告投放如何花更少的钱用更少的人得到更高回报?

具体来讲,Lyft 的广告投放要服务如下特点

  1. 管理基于地域的 campaign
  2. 数据驱动的增长:增长必须是规模化的、可测量的、可预测的
  3. 支撑起 Lyft 独特的增长模型,如图:

lyft growth model

主要的挑战是:难以规模化管理跨地域营销中的各个环节,广告竞标、预算、素材、激励、选择受众、测试等等。下图是营销者的一天:

营销者的一天

我们可以发现“执行”占去了大部分的时间,而更少的时间花在了更重要的“分析和决策”上。规模化意味着减少繁复的操作,让营销人员专注于分析与决策。

解决方案:自动化

为了降低成本,提高做实验的效率,需要

  1. 预测新用户是否对产品感兴趣
  2. 多渠道优化,有效评估和分配预算
  3. 方便地管理上千个 campaigns

数据由 Lyft 的 Amundsen 系统做增强学习。

自动化的部分包括:

  1. 更新 bid 的关键词
  2. 关掉效果不好的素材
  3. 根据市场改变 referrals values
  4. 找到高价值的用户 segment
  5. 在多个 campaign 中共享策略

构架

Lyft Symphony Architecture

技术栈:Apache Hive, Presto, ML platform, Airflow, 3rd-party APIs, UI.

具体的组成模块

LTV 预测模块

用户的终身价值是衡量渠道的重要标准,预算由 LTV 和我们愿意为该地区的获客付出的价格共同决定。

我们对新用户的认知有限,随着交互的增多,所提供的历史记录会更准确地预测。

一开始的特征值:

特征值

随着历史上的交互记录的积累,做出的判断就会越准确:

根据历史记录判断 LTV

预算分配模块

搞定了 LTV,接下来是根据价格定预算。拟合出 LTV = a * (spend)^b 形式的曲线以及周围的区间里类似参数的曲线。为了找到全局最优,需要付出一些随机性的代价。

预算计算

投放模块

分为两部分,一部分是调参者,一部分是执行者。调参者根据定价,设定基于渠道的具体的参数;执行者把这些参数执行到具体的渠道上。

有很多流行的投放策略,在各色的渠道中,是共通的:

投放策略

总结

要注意人的经验在系统中的重要性,否则会 garbage in, garbage out. 当人从繁琐的投放任务解放出来,专注于理解用户、理解渠道、理解自身要传达给受众的信息之后,就能够获得更好的投放效果——花更少的时间达到更高的 ROI。

如何编写稳健的代码?

· 阅读需 1 分钟

他喜欢它

  1. 同理心 / 视角转换是最重要的。

    1. 意识到代码首先是为人类阅读而编写的,然后才是为机器执行。
    2. 软件是如此“柔软”,有很多方法可以实现同一目标。关键在于做出适当的权衡,以满足需求。
    3. 发明与简化:Apple Pay RFID 与 微信扫码二维码。
  2. 选择可持续的架构,以减少每个功能的人力资源成本。

  1. 采用模式和最佳实践。

  2. 避免反模式

    • 缺少错误处理
    • 回调地狱 = 意大利面条代码 + 不可预测的错误处理
    • 过长的继承链
    • 循环依赖
    • 过于复杂的代码
      • 嵌套的三元操作
      • 注释掉未使用的代码
    • 缺少国际化,特别是 RTL 问题
    • 不要重复自己
      • 简单的复制粘贴
      • 不合理的注释
  3. 有效的重构

    • 语义版本
    • 永远不要对非主要版本引入破坏性更改
      • 两腿变更

构架入门

· 阅读需 4 分钟

什么是构架

构架是软件系统的形状。拿建筑物来举例子:

  • 范式 paradigm 是砖块
  • 设计原则是房间
  • 组件是建筑

他们共同服务于一个特定的目的,就像医院治疗病人,学校教育学生一样。

我们为什么需要架构?

行为 vs. 结构

每一个软件系统提供两个不同的价值给利益相关者:行为与结构。软件开发者必须确保这两项价值都要高

==由于其工作的需要,软件架构师更多地聚焦于系统的结构而不是特性和功能。==

终极目标——==减少每加一个新特性所需要耗费的人力成本==

架构服务于软件系统的整个生命周期,使其易于理解,开发,测试,部署和操作。 其目标是最小化每个业务用例的人力资源成本。

O’Reilly 出版的《软件架构》一书很好地介绍了这样五种基本的构架。

1.分层架构

分层架构是被广泛采用,也是被开发者所熟知的一种架构。因此,它也是应用层面上事实上的标准。如果你不知道应该使用什么架构,用分层架构就是不错的选择。

示例

  • TCP/IP模式:应用层 > 运输层 > 网际层 > 网络接口层
  • Facebook TAO网络层 > 缓存层(follower + leader) > 数据库层

优缺点:

  • 优点
    • 易于使用
    • 职责划分
    • 可测试性
  • 缺点
    • 庞大而僵化
      • 想要对架构进行调整、扩展或者更新就必须要改变所有层,十分棘手

2.事件驱动架构

任何一个状态的改变都会向系统发出一个事件。系统组件之间的通信都是经由事件完成的。

一个简化的架构包含中介(mdiator),事件队列(event queue)和通道(channel)。下图所示即为简化的事件驱动架构:

示例

  • QT:信号(signals)和槽(slots)
  • 支付基础设施:由于银行网关通常有较高的延迟,因此银行的架构中采用了异步技术

3.微核架构(aka Plug-in Architecture)

软件的功能被分散到一个核心和多个插件中。核心仅仅含有最基本的功能。各个插件之间互相独立并实现共享借口以实现不同的目标。

示例

  • Visual Studio Code 和 Eclipse
  • MINIX 操作系统

4.微服务架构

大型系统被解离成众多微服务,每一个都是单独部署的单位,他们之间通过RPCs进行通信。

uber architecture

示例

5.基于空间的架构

“基于空间的架构”这一名称来源于“元组空间”,“元组空间“有”分布式共享空间“的含义。基于空间的架构中没有数据库或同步数据库访问,因此该架构没有数据库的瓶颈问题。所有处理单元共享内存中应用数据副本。这些处理单元都可以很弹性地启动和关闭。

示例:详见 Wikipedia

  • 主要被使用Java的架构所采用:例如:JavaSpaces

今日头条推荐系统:P2 内容分析

· 阅读需 3 分钟

今日头条推荐系统:P1 概述 中,我们了解到内容分析和用户标签的数据挖掘是推荐系统的基石。

什么是内容分析?

内容分析 = 从原始文章和用户行为中提取中间数据。

以文章为例。为了建模用户兴趣,我们需要对内容和文章进行标记。为了将用户与“互联网”标签的兴趣关联起来,我们需要知道用户是否阅读了带有“互联网”标签的文章。

我们为什么要分析这些原始数据?

我们这样做的原因是 …

  1. 标记用户(用户画像)
    • 标记喜欢带有“互联网”标签的文章的用户。标记喜欢带有“小米”标签的文章的用户。
  2. 根据标签向用户推荐内容
    • 向带有“小米”标签的用户推送“小米”内容。向带有“Dota”标签的用户推送“Dota”内容。
  3. 按主题准备内容
    • 将“德甲”文章放入“德甲主题”。将“饮食”文章放入“饮食主题”。

案例研究:一篇文章的分析结果

以下是“文章特征”页面的示例。文章特征包括分类、关键词、主题、实体。

一篇文章的分析结果

一篇文章的分析结果:详细信�息

文章特征是什么?

  1. 语义标签:人类预定义这些标签,具有明确的含义。

  2. 隐含语义,包括主题和关键词。主题特征描述了单词的统计数据。某些规则生成关键词。

  3. 相似性。重复推荐曾是我们从客户那里获得的最严重反馈之一。

  4. 时间和地点。

  5. 质量。滥用、色情、广告或“心灵鸡汤”?

文章特征的重要性

  • 并不是说没有文章特征推荐系统就完全无法工作。亚马逊、沃尔玛、Netflix可以通过协同过滤进行推荐。
  • 然而,在新闻产品中,用户消费的是当天的内容。没有文章特征的引导是困难的。协同过滤无法帮助引导。
    • 文章特征的粒度越细,启动的能力就越强。

更多关于语义标签的信息

我们将语义标签的特征分为三个层次:

  1. 分类:用于用户画像、过滤主题内容、推荐召回、推荐特征
  2. 概念:用于过滤主题内容、搜索标签、推荐召回(喜欢)
  3. 实体:用于过滤主题内容、搜索标签、推荐召回(喜欢)

为什么要分成不同的层次?我们这样做是为了能够以不同的粒度捕捉文章。

  1. 分类:覆盖全面,准确性低。
  2. 概念:覆盖中等,准确性中等。
  3. 实体:覆盖低,准确性高。它仅覆盖每个领域的热门人物、组织、产品。

分类和概念共享相同的技术基础设施。

我们为什么需要语义标签?

  • 隐含语义
    • 一直运作良好。
    • 成本远低于语义标签。
  • 但是,主题和兴趣需要一个明确的标签系统。
  • 语义标签还评估公司的NPL技术能力。

文档分类

分类层级

  1. 科学、体育、金融、娱乐
  2. 足球、网球、乒乓球、田径、游泳
  3. 国际、国内
  4. A队、B队

分类器:

  • SVM
  • SVM + CNN
  • SVM + CNN + RNN

计算相关性

  1. 对文章进行词汇分析
  2. 过滤关键词
  3. 消歧义
  4. 计算相关性