0%

今年 6 月底,由于工作需要,花了两周时间调研对话系统,并在公司内部做了一次调研报告。本文意在将此报告整理成文字版,算是对这段时间付出的一个交代。如果你对这个领域没有过了解,正好你也对此感兴趣,希望本文能够帮助你从以下几个方面提升你对「对话系统」的理解:

  • 问题背景
  • 使用场景
  • 基本概念
  • 构建方式
  • 基本架构

最后将以 Rasa 为例,介绍一个真实的对话系统项目。

背景知识

ℹ️ 本节内容主要来自于 Speech and Language Processing 这本书的第 24 章

对话系统,顾名思义是用来与真人对话的系统。要做好这样的系统,了解、学习和研究「人类对话的特点」很有必要。

人类对话的特点

人类对话的特点理论上属于自然语言学的研究范畴,内容丰富,这里仅摘取其中一些要点。但如果有一天你想要把对话系统做到极致,系统地学习更多自然语言研究成果肯定能自底向上地帮助到你。

  • 轮次 (turns):典型的对话通常由以对话双方轮流发言的形式出现,一来一回合称一轮 (turn),来回多次被称为多轮对话。
  • 对话行为 (dialogue acts):人们的每次发言或多或少都会带着一些目的,即可以是粗粒度的,如表述 (constatives)、请求 (requests)、指令 (directives)、承诺 (commissives)、表态 (acknowledgements)、问题 (questions),也可以是细粒度的,如询问天气、预约时间、预定机票等等。
  • 共识确认 (grounding):在对话过程中,为了保证双方的认识一致,沟通过程中会通过各种方式确认对方的想法。确认方式大致可以分为显式和隐式两种,举例如下:
    • 显式:
      • A:我肚子在叫……
      • B:你是不是想吃午饭了?
    • 隐式:
      • A:我今天想快点去上海……
      • B:好的,那你想从首都机场还是大兴机场出发?(隐含确认出行方式)
  • 对话结构 (dialogue structure):通过分析不同的对话,我们会发现对话常常由基本问答加上一些特殊变化构成。特殊变化包括题外话 (side sequence)、前置语句 (presequences)、后置语句 (postsequences)、重复确认 (clarification) 等等。
  • 主导性 (initiative):当对话中有一方一直在单向获取另一方信息时,我们称前者为主动方,后者为被动方。在对话系统中,对话双方是用户 (user) 和系统 (system),那么从主导性出发,对话系统可以被分为三类:用户主导 (user-initiative)、系统主导 (system-initiative) 和混合主导 (mixed initiative)。
  • 推理和隐喻 (inference & implicature):所谓含沙射影、话中带刺、指桑骂槐、拐弯抹角都是推理和隐喻的艺术。当然,推理和隐喻也可用于表达非负面的含义,这里就不展开描述。

常见使用场景

阅读全文 »

因为之前在 Youtube 和 B 站上零星地看了一些 Trevor Noah 的 stand-up 和 Daily Show,加上最近在 Palfish App 上与来自南非的老师学英语,我在大约四月中旬决定读一读「Born a Crime」这本书,书的内容本来并不多,但个人时间安排原因使得这个过程变得很长,直到昨天终于读完。

「Born a Crime」记录了 Trevor Noah 在南非成长的经历,但它并不像一个传记,而是将成长中的事件按照主题串联,如:

  • 母亲对耶稣的绝对信仰
  • 出生前后及幼年时期成长因为特殊肤色 (colored) 不得不面对的问题
  • 母亲的独立,父亲的神秘
  • ...

💡 完整的梗概可以移步 这里 阅读

为什么人们会喜欢这本书?Trevor 在这个访谈里给出了他的看法。

大概说的就是每个人在这本书的某一桥段都会找到共鸣。当然,这也是所有受欢迎内容的共同特点。

语录及感受

下面我想挑选书中一些我特别喜欢的话,分享给对此有兴趣的你,也算是对这本书以及花在阅读这本书上的时间一个交代。

We tell people to follow their dreams, but you can only dream of what you can imagine, and, depending on where you come from, your imagination can be quite limited.

阅读全文 »

完整调研报告请见我个人的 Notion 笔记

0. 引入

最近,我们遇到了两个场景:

  1. 负责基础服务的工程师想下线一个接口但不知道有哪些服务调用
  2. 负责 APM 系统的工程师想知道任意 RPC 接口的所有上游调用方

仔细分析不难发现,二者的本质都在于「维护微服务间的静态依赖关系」。等等!在调用链追踪系统中,我们不是已经获得了接口级别的依赖关系吗?为什么不能直接用那边的数据?目前伴鱼调用链追踪系统中维护的依赖关系在三个方面无法满足上述需求:

  1. 一些上古服务仍然在运行,但没有接入调用链追踪系统
  2. 调用链追踪系统中维护的是「动态依赖关系」,即最近 N 天 (由 retention policy 决定) 捕获的调用关系
  3. 调用链追踪系统中存储的是经采样策略过滤后的数据,可能存在漏采的情况

于是我们开始思考另一个方向:通过代码搜索引擎提取静态依赖关系。恰好在 2020 Q4 末,我们将内部所有项目仓库从 Gerrit 迁移到了 Gitlab,为代码搜索引擎的落地铺平了道路。

在下文中,我们将和大家分享代码搜索引擎的调研报告,期望能帮助读者了解代码搜索引擎如何工作。报告主要讨论以下话题:

  • 为什么做
  • 一般架构
  • 设计决定
  • 实现挑战
  • 开源项目

1. 为什么做

阅读全文 »

此文同时发表在伴鱼技术博客

理论篇 中,我们介绍了伴鱼在调用链追踪领域的调研工作,本篇继续介绍伴鱼的调用链追踪实践。在正式介绍前,简单交代一下背景:2015 年,在伴鱼服务端起步之时,技术团队就做出统一使用 Go 语言的决定。这个决定的影响主要体现在:

  1. 内部基础设施无需做跨语言支持
  2. 技术选型会有轻微的语言倾向

1. 早期实践

1.1 对接 Jaeger

2019 年,公司内部的微服务数量逐步增加,调用关系日趋复杂,工程师做性能分析、问题排查的难度变大。这时亟需一套调用链追踪系统帮助我们增强对服务端全貌的了解。经过调研后,我们决定采用同样基于 Go 语言搭建的、由 CNCF 孵化的项目 Jaeger。当时,服务的开发和治理都尚未引入 context,不论进程内部调用还是跨进程调用,都没有上下文传递。因此早期引入调用链追踪的工作重心就落在了服务及服务治理框架的改造,包括:

  • 在 HTTP、Thrift 和 gRPC 的服务端和客户端注入上下文信息
  • 在数据库、缓存、消息队列的接入点上埋点
  • 快速往既有项目仓库中注入 context 的命令行工具

部署方面:测试环境采用 all-in-one,线上环境采用 direct-to-storage 方案。整个过程前后大约耗时一个月,我们在 2019 年 Q3 上线了第一版调用链追踪系统。配合广泛被采用的 prometheus + grafana 以及 ELK,我们在微服务群的可观测性上终于凑齐了调用链 (traces)、日志 (logs) 和监控指标 (metrics) 三个要素。

下图是第一版调用链追踪系统的数据上报通路示意图。服务运行在容器中,通过 opentracing 的 sdk 埋点,Jaeger 的 go-sdk 上报到宿主机上的 Jaeger-agent,后者再将数据进一步上报到 Jaeger-collector,最终将调用链数据写入 ES,建立索引,即图中的 Jaeger backends。

distributed-tracing-v1

阅读全文 »

TL;DR

本文将调用链追踪系统的设计维度归结于以下 5 个:调用链数据模型、元数据结构、因果关系、采样策略以及数据可视化。我们可以把这 5 个维度当作一个分析框架,用它帮助我们在理论上解构市面上任意一个调用链追踪系统,在实践中根据使用场景进行技术选型和系统设计。如果你对调研相关系统很感兴趣,也欢迎参与到 Database of Tracing Systems 项目中,一起调研市面上的项目,建立起调用链追踪系统的数据库。

引言

阅读本文并不要求读者具备任何调用链追踪系统相关的理论知识或实践经验,读者具备一定的微服务架构的概念或实践经验即可。期望读者看完这篇文章以后,能掌握调用链追踪系统的核心设计维度,理解其中的设计权衡,并能使用这些维度来分析市面上的新老调用链追踪系统实现,甚至帮助到自己在生产实践中根据使用场景进行技术选型和系统设计。

解决的问题

微服务的可观测性

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.

— Melvin E. Conway

如果有一门学科叫软件社会学,那么康威定律 (Conway's law) 必定是其中的基本定律之一。如果把互联网公司内部的全体信息系统看作是一整个系统,那么这个系统模块结构会向公司的组织架构收敛。从组织架构层面看,公司结构从扁平向多层级演变,信息传递的环节增加,沟通效率随之下降,进而影响公司的行动效率。不论从组员之间的熟悉程度还是从部门目标一致性来看,部门内部的沟通效率要远远高于部门间的沟通效率。因此,如果系统模块结构与组织架构约趋近,公司的沟通效率就能接近极大值。团队的分化通常伴随着服务的拆分,这也是许多公司业务增长以后进行微服务化的动机。微服务化后,公司信息系统就被迫成为了分布式系统。尽管分布式系统带来了种种好处,如持续集成、增量部署、横向扩展、故障隔离,但系统可观测性比起单机系统下降了很多,甚至几乎没有人能够对公司信息系统有全局性的了解。

任意一个分布式系统的终极理想是:“给开发者以分布式的能力,单机的感受”。而调用链追踪系统就是实现终极理想不可或缺的一部分。调用链追踪系统通过收集调用链数据,帮助开发者在观测分布式系统行为时,从以机器为中心 (machine-centric) 走向以请求为中心 (workflow-centric)。调用链 (traces)、日志 (logs)、监控指标 (metrics),三者合称 Telemetry,有了它们,微服务开发者既能通盘考虑,又能深入局部分析,在系统规模扩大的同时仍然能够掌控全局。

使用场景

阅读全文 »

按:本文会尊从原论文的结构,但不是逐字翻译,以意译和加入个人理解的转述为主。

1. Introduction

如果把公司内部所有 IT 系统看作一个巨型分布式系统,通常其规模庞大、结构复杂,且拥有多层依赖和抽象,每层单拎出来也同样是个分布式系统。以 Google 的业务服务为例,如搜索、广告,会构建于内部基础服务之上,如 Bigtable;而 BigTable 又构建于 Google File System (GFS) 和 Chubby 之上。即便是业务服务本身,也可能存在多层 (multiple tiers) 结构,其中每层同样支持横向扩展。从巨石应用走向微服务,我们在组织架构和服务架构上都变得高效,但其代价就是下降的系统可观测性 (observability)。一个很现实的问题就是:几乎不存在一位工程师能够了解系统全貌,那么问题排查也将变得困难。从单机走向分布式,只关心单个进程、单台机器的性能指标已经远远不够,我们需要将监控的重心从以机器为中心 (machine-centric) 转向以流程为中心 (workflow-centric),后者的核心方法便是调用链追踪系统。分布式系统的终极理想可以描述为:在获得横向扩展性的同时不暴露自己分布式的本质,即向外提供与单机系统相同的体验。调用链追踪系统概莫能外。

按:原文将请求处理过程称为 workflow,下文会使用流程和请求来指代它;原文将以流程为中心的观测方法统称为 end-to-end tracing,由于现在分布式系统几乎是所有讨论的默认假设,本文将不再强调 end-to-end,将其直接译为调用链追踪。

尽管大家对调用链追踪的兴趣浓厚,但关于如何设计一个调用链追踪系统,市面上、社区中提供的信息十分有限。更令人担忧的是,现存的文献和实践都将调用链追踪当作多种场景的万能解决方案 (one-size-fits-all),然而我们以及 Dapper 的实践经验证明事实并非如此。因此在提出你的解决方案之前,最好明确你想要解决的问题是什么。

调用链追踪的基本原理和概念十分通俗易懂:就是在系统中的各个节点 (组件) 上埋点,当请求经过时将节点信息 (trace point) 上报,最后汇总信息重建调用链。我们从经验中总结了调用链追踪系统的 4 个设计维度,通过组合这些设计维度的选择就能得到不同应用场景的解决方案:

  1. which causal relationships should be preserved: 保留什么样的因果关系?
  2. how causal relationships are tracked: 如何追踪因果关系?
  3. how to reduce overhead by sampling: 如何通过采样降低成本?
  4. how end-to-end traces should be visualized: 如何将追踪的结果可视化?

如果对这 4 个设计维度以及它们之间的权衡关系没有足够的理解,设计一个调用链追踪系统将可能让实现与场景脱节。事实上,由于这些维度之前并未被实践者或研究者提出和充分理解,许多调用链追踪系统的实现并未能充分达成其设计理想。

2. Backgroud

本节主要陈述一些调用链追踪系统的背景信息,包括核心应用场景、解决方案分类以及本文所推崇的架构方案。

阅读全文 »

自从 2018 年底用 Go 搭建第一个项目以来,已经过去接近 2 年时间,我发现自己从未系统地思考过 Go 的 error handling 方案。最近在阅读 [1] 时,逐渐发现个人和团队都应该花更多的精力建立更加扎实的工程实践方法论,进一步提升交付项目质量。而本篇博客算是向这个方向迈出的第一步。

0. 术语说明

为了避免翻译造成的歧义,文中涉及的没有通用翻译中文的术语都会直接使用原英文单词:

英文 中文
error 错误
exception 异常
error-code-based 基于错误码
exception-based 基于异常
package 包 (Go 中 module 由多个 package 构成)
error wrapping/unwrapping 包装错误/解包装
error inspection 错误检查
error formatting 错误格式化
error chain 错误链表,即通过包装将错误组织成链表结构
error class 错误类别、类型

下文中,errors package 指代我们定制化的 error handling 方案。

1. 文献综述

不同程序语言的 error handling 方案大致可以分为两种:error-code-based 和 exception-based。Raymond 在博客 [2] [3] 中指出 exception-based 错误处理更不利于写出优质的代码,也更难辨别优质和劣质的代码;Go 在设计时选择了 error-code-based error handling 方案,鼓励开发者显式地在 error 出现的地方直接处理 [4];并在官博 [5] 中提出了 errors are values 的理念,只要实现 Error 接口的结构体就可以作为 error,不同的项目就能够按需定制 error handling 实现方案,并提出在一些特殊场景下可以利用非通用的代码重构技巧避免冗长、啰嗦的表达,如errWriter;许多来自 Java、Python 等语言的工程师习惯了 exception-based 的方案,遇到 Go 时感到十分不习惯 [6],但如果我们总是希望在一门新语言中尝试套用自己熟悉语言的语法,就无法充分理解其它语言在这方面的设计理念。Go 核心工程师 Rob Pike 在 [7] 中描述了他如何在 Upspin 项目中定制 error 信息和处理方案,使得项目对程序、用户及开发者更加友好;许多 error handling 项目都关注到了多层嵌套调用场景下的上下文注入问题,即所谓的 error wrapping,其中 Dave Cheney 的项目 pkg/errors [8] 被广泛使用,Go 在 1.13 后也提供类似的原生解决方案 [9];受 [7] [8] 的启发,Ben Johnson,boltDB 的作者,结合自己多年的编码经验,在 [10] 中提出 Failure is your Domain 的观点,认为每个项目应当构建特有的 error handling package,并提出逻辑调用栈 (logical stack) 的概念,在 GopherCon 2019,还有工程师在推广类似的方案 [11]。

error handling 可以细分为 checking、inspection 和 formatting 三部分,分别指判断 error 发生与否、检查 error 类型、打印 error 上下文。在发现 Go 社区的开发者们因为语言本身对 error handling 的支持不足,频繁创造各种各样的轮子之后,Russ Cox 在 2018 年末发布了两个新提议 [12] [13],前者尝试解决 checking 代码冗长的问题;后者尝试解决 inspection 的信息丢失以及 formatting 的上下文信息不足问题。目前仅 inspection 的方案被整合到了 1.13 中,直到最近的 1.15 版本没有新的解决方案出现。

2. 项目综述

发布之初,Go (<1.13) 仅提供 Error 接口及 errors.Newfmt.Errorf 两个构建 error 的方法 [4];Go 1.13 支持利用 %w 格式化符号实现 error wrapping,并提供 Unwraperrors.Is 以及 errors.As 来解决 error wrapping 过程中上下文缺失的问题 [9];spacemonkeygo 为了将大型 Python 仓库迁移到 Go 上,开发了 [14] ,模拟 Python 中 error class 的继承,支持自动记录日志、调用栈以及任意键值数据,支持 error inspection;juju errors [15] 因 juju 项目而诞生,在 wrap error 时,你可以选择保留或隐藏 error 产生的原因 (cause),但它的 Cause 方法仅 unwrap 一层,而 [8] 会递归地遍历 error chain,[16] 中的概念与 [15] 类似,仅在 API 上有所不同;hashicorp 开源的 errwrap [16],支持将 errors 组织成树状结构,并提供 Walk 方法遍历这棵树;pkg/errors [8] 提供 wrapping 和调用栈捕获的功能,并利用 %+v 格式化 error,展示更多的细节,它认为只有整个 error chain 最末端的 error 最有价值,pingcap/errors [18] 基于 [8] 二次开发,并且在 [19] 中增加了 error 类 (域) 的概念;upspin.io/errors [20] 是定制化 error 的实践范本,同时引入了 errors.Iserrors.Match 用于辅助检查 error 类型;[21] 考虑了 error 在进程间传递的场景,让 error handling 具备网络传播兼容能力。

阅读全文 »

最近在阅读 TiDB 源码 util/chunk package 的过程中,看到了 Apache Arrow 这个项目 (下文简称 Arrow):

1
2
3
4
5
// Chunk stores multiple rows of data in Apache Arrow format.
// See https://arrow.apache.org/docs/format/Columnar.html#physical-memory-layout
// Values are appended in compact format and can be directly accessed without decoding.
// When the chunk is done processing, we can reuse the allocated memory by resetting it.
type Chunk struct { /*...*/ }

心里自然而然会产生疑问:为什么要使用这个项目规定的数据存储格式?于是在阅读完 TiDB 相关源码和单测后,顺便搜寻并浏览一些有趣的资料 (见文末参考部分),现在将这次调研的收获小结在这篇博客中。

本文简单地介绍一种内存列存数据格式:Apache Arrow,主要涉及的内容包括:

  • Arrow 项目的来源
  • Arrow 如何表示定长、变长和嵌套数据
  • 内存列存数据格式与磁盘列存数据格式的设计取舍

注:Arrow 即可以指内存列存数据格式,也可以指 Apache Arrow 项目整体,因此下文中将用 「Arrow」 表示格式本身,「Arrow 项目」表示整体项目。

项目简介

现存的大数据分析系统基本都基于各自不同的内存数据结构,这就会带来一系列的重复工作:从计算引擎上看,算法必须基于项目特有的数据结构、API 与算法之间出现不必要的耦合;从数据获取上看,数据加载时必须反序列化,而每一种数据源都需要单独实现相应的加载器;从生态系统上看,跨项目、跨语言的合作无形之中被阻隔。能否减少或消除数据在不同系统间序列化、反序列化的成本?能否跨项目复用算法及 IO 工具?能否推动更广义的合作,让数据分析系统的开发者联合起来?在这样的使命驱动下,Arrow 就诞生了。

与其它项目不同,Arrow 项目的草台班子由 5 个 Apache Members、6 个 PMC Chairs 和一些其它项目的 PMC 及 committer 构成,他们直接找到 ASF 董事会,征得同意后直接以顶级 Apache 项目身份启动。想了解项目的详细历史可以阅读项目 Chair,Jacques Nadeau 写的这篇博客。另外,这张 google sheet 记录着项目的取名过程,取名为 Arrow 的原因是:"math symbol for vector. and arrows are fast. also alphabetically will show up on top." 可以说考虑得相当全面 😂。

Arrow 的愿景是提供内存数据分析 (in-memory analytics) 的开发平台,让数据在异构大数据系统间移动、处理地更快:

阅读全文 »

引言

最近因为工作的关系接触 ElasticSearch,发现搜索引擎也是计算机应用的一个有意思的分支。于是通过 Freiburg 的 Information Retrieval 公开课开始系统地了解信息检索这个领域,感觉收获颇丰。周末一时兴起,上 Google Research 找到了 Google 的开山之作,近距离地感受一下 19800+ 引用量、造就如今 Google 万亿市值的这篇文章。

本文介绍会尽可能地忠于原文,但不是完全逐字逐句的翻译。

1. 简介

近年来,网络中的信息量和非专业的用户都在快速增长,为信息检索领域带来了新的挑战。从行为上看,人们更倾向于以门户网站,如 Yahoo,或搜索引擎为起点,利用网页间的链接来浏览所需信息。门户网站的索引来自于人工维护,聚合效果好、主观性高、维护成本高、改进速度慢,且通常不会覆盖小众话题。自动化的搜索引擎则相反,其它都满足,但聚合效果差,依赖于关键词匹配的检索方式返回的匹配项质量过低。一些广告商为了获取更多的流量,通过逆向工程来误导搜索引擎,使其结果靠前,进一步加剧问题的严重性。我们搭建了一个大型搜索引擎来当前系统的这些已知问题,它通过重度利用网页文本中的特殊结构来提供更高质量的检索结果。我们为这个系统取名为 Google,因为它是 googol (即 ) 的常用拼写,后者的含义恰好与构建大型搜索引擎的目标体量相契合

网络搜索引擎的扩张:1994 - 2000

为了跟上网络信息扩张的速度,搜索引擎技术也必须加速规模化。1994 年,World Wide Web Worm (WWWW),第一代网络搜索引擎 (web search engine) 之一,对 11 万网页或文件建立了索引;到了 1997 年末,行业领先的网络搜索引擎建立索引的数量已达到 200 万 (WebCrawler),甚至 1 亿 (Search Engine Watch)。可以预见这个数量在 2000 年将超过 10 亿。与此同时,网络上的搜索引擎处理的查询也在飞速增长。在 1994 年初,WWWW 大约每日需要处理 1500 次查询,到了 1997 年末,Altavista 已经声称自己每日处理的请求量达到 2 千万。同样可以预见,这个数量在 2000 年也将达到亿级。Google 的目标就是要提供相应规模、高质量的网络搜索服务。

Google: 与网络一同扩张

即便是搭建一个满足当前规模的网络搜索引擎也需要面对许多挑战,如:

  • 高性能的网页抓取技术保证原始数据全而新
  • 充足的存储空间用以存放索引和网页本身 (如果需要的话)
  • 索引系统必须能够高效地处理百 G 级别的数据
  • 查询必须能被快速处理,QPS 过千
阅读全文 »

I ❤ Logs 出版于 2014 年,是一本很短小的书,100 页不到,利用这周的零散时间就看完了。作者 Jay Kreps,是前 LinkedIn 的 Principal Staff Engineer,也是 LinkedIn 许多著名开源项目的负责人及联合作者,如 Kafka、Voldemort 等。他是现任 Confluent 的 CEO,主要工作在于围绕实时数据提供企业级服务支持。这本书算是 Jay Kreps 过去多年实践的思考结晶。

本文主要是对书中的一些看法、观点的个人化梳理,有兴趣可以阅读原著

日志即数据

在讨论日志之前,首先要明确日志的含义。这里的日志并非指我们常用的非结构化或半结构化的服务日志,而更接近数据库中常见的结构化的提交日志 (commit log/journal/WAL),这些日志通常是只往后追加数据,这里的序号暗含着逻辑时间,标识着连续日志产生的逻辑先后顺序:

a-structured-log

数据库中的日志

日志在数据库中常常被用来实现故障恢复、数据复制、最终一致性等。一个事务提交成功与否在日志提交成功时就可以确定,只要 WAL 落盘,便可告诉客户端提交成功,即便数据库发生故障,也能从 WAL 日志中恢复数据;日志 (如 BinLog) 的 pub/sub 机制可以用来在主节点与复制节点之间同步数据,通过同步的进度可以知道不同复制节点的同步进度,此外日志的逻辑顺序保证了主节点与复制节点之间数据的一致性。

分布式系统中的日志

数据库利用日志来解决的问题,也是所有分布式系统需要解决的根本问题,如刚才提到的故障恢复、数据同步、数据一致性等等,可以称之为以日志为中心 (log-centric) 的解决方案。更严谨地说:

如果两个相同的 (identical)、确定 (deterministic) 的进程以相同的状态启动,按相同的顺序获取相同的输入,它们将最终达到相同的状态。

阅读全文 »