first commit

This commit is contained in:
张乾 2024-10-15 22:50:03 +08:00
parent 914d92856f
commit bbc9aed40c
128 changed files with 12147 additions and 5 deletions

View File

@ -39,7 +39,9 @@ foreach ($lines as $line) {
$folderName = str_replace(' ', '', $line);
$folderName = "/Users/01397713/Documents/github/learn-tech".$folderName;
$curlUrl = $url. urlencode($line);
$line = str_replace(' ', '%20', $line);
$curlUrl = $url. $line;
$response = file_get_contents($curlUrl);
mkdir($folderName, 0777, true);
preg_match_all('/<a class="menu-item" id="([^"]*)" href="([^"]*)">([^<]*)<\/a>/', $response, $matches);
@ -49,6 +51,15 @@ foreach ($lines as $line) {
$urlList = $matches[2];
foreach($fileNameList as $key => $name) {
$fileName = str_replace(' ', '', $name);
$fileName = $folderName . '/'. $fileName;
echo $fileName;
echo PHP_EOL;
if(filesize($fileName) > 0) continue;
$fileUlr = $url . $urlList[$key];
$fileContents = file_get_contents($fileUlr);
@ -61,12 +72,10 @@ foreach ($lines as $line) {
libxml_clear_errors();
$text = $doc->textContent;
$fileName = str_replace(' ', '', $name);
file_put_contents($folderName . '/'. $fileName, $text);
file_put_contents($fileName, $text);
echo $fileName;
echo PHP_EOL;
sleep(10);
// preg_match_all('/<p>([^<]*)<\/p>/', $fileContents, $fileMatches);
}
}

45
test.php Executable file
View File

@ -0,0 +1,45 @@
<?php
// Define the URL
$url = "https://learn.lianglianglee.com/";
# 1 获取文件主目录
// $response = file_get_contents($url);
// if ($response === FALSE) {
// echo "Failed to access the URL.";
// } else {
// // Use regex to find the href values across multiple lines (with the 's' modifier)
// preg_match_all('/<li><a href="([^"]*)">([^<]*)<\/a><\/li>/', $response, $matches);
// // Prepare the output for the readme file
// $output = "";
// if (!empty($matches[1])) {
// foreach ($matches[1] as $href) {
// $output .= $href.PHP_EOL;
// }
// } else {
// $output = "No match found.\n";
// }
// // Write the results to readme.txt file
// file_put_contents("README.md", $output);
// echo "Results saved to readme.txt";
// }
# 2 生成不同目录的文件夹
// Path to the README.md file
$readmePath = 'README.md';
// Read the file into an array of lines
$lines = file($readmePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Remove spaces from each line
$folderName = str_replace(' ', '', $line);
$folderName = "/Users/01397713/Documents/github/learn-tech".$folderName;
$line = str_replace(' ', '%20', $line);
$curlUrl = $url. $line;
echo $curlUrl.PHP_EOL;
}

View File

@ -0,0 +1,102 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 一起练习:手把手带你分解任务
你好,我是郑晔。
前面在讨论 TDD 的时候,我们说任务分解是 TDD 的关键。但这依然是一种感性上的认识。今天,我们就来用一个更加具体的例子,让你看看任务分解到底可以做到什么程度。
这个例子就是最简单的用户登录。需求很简单,用户通过用户名密码登录。
我相信,实现这个功能对大家来说并不困难,估计在我给出这个题目的时候,很多人脑子里已经开始写代码了。今天主要就是为了带着大家体验一下任务分解的过程,看看怎样将一个待实现的需求一步步拆细,变成一个个具体可执行的任务。
要完成这个需求,最基本的任务是用户通过输入用户名和密码登录。
用户名和密码登录这个任务很简单,但我们在第一部分讲过沙盘推演,只要推演一下便不难发现,这不是一个完整的需求。
用户名和密码是哪来的呢?它们可能是用户设置的,也可能是由系统管理员设置的。这里我们就把它们简单设定成由用户设定。另外,有用户登录,一般情况下,还会有一个退出的功能。好了,这才是一个简单而完整的需求。我们就不做进一步的需求扩展。
所以,我们要完成的需求列表是下面这样的。
-
假设我们就是拿到这个需求列表的程序员,要进行开发。我们先要分析一下要做的事情有哪些,也就是任务分解。到这里,你可以先暂停一会,尝试自己分解任务,之后,再来对比我后面给出的分解结果,看看差异有多少。
好,我们继续。
我们先来决定一下技术方案,就用最简单的方式实现,在数据库里建一张表保存用户信息。一旦牵扯到数据库表,就会涉及到数据库迁移,所以,有了下面的任务。
-
这时,需要确定这两个任务自己是否知道怎么做。设计表,一般熟悉 SQL 的人都知道怎么做。数据库迁移,可能要牵扯到技术选型,不同的数据库迁移工具,写法上略有差别,我们就把还不完全明确的内容加到任务清单里。
-
数据库的内容准备好了,接下来,就轮到编写代码的准备上了。我们准备用常见的 REST 服务对外提供访问。这里就采用最常规的三层技术架构,所以,一般要编写下面几项内容。
领域对象,这里就是用户。
数据访问层,在不同的项目里面叫法不一,有人从 J2EE 年代继承下来叫 DAO数据访问对象Data Access Obejct有人跟着 Mybatis 叫 mapper我现在更倾向于使用领域驱动设计的术语叫 repository。
服务层,提供对外的应用服务,完成业务处理。
资源层,提供 API 接口,包括外部请求的合法性检查。
根据这个结构,就可以进一步拆解我们的开发任务了。
不知道你有没有注意到,我的任务清单上列任务的顺序,是按照一个需求完整实现的过程。
比如,第一部分就是一个完整的用户注册过程,先写 User然后是 UserRepository 的 save 方法,接着是 UserService 的 register 方法,最后是 UserResource 的 register 方法。等这个需求开发完了,才是 login 和 logout。
很多人可能更习惯一个类一个类的写,我要说,最好按照一个需求、一个需求的过程走,这样,任务是可以随时停下来的。
比如,同样是只有一半的时间,我至少交付了一个完整的注册过程,而按照类写的方法,结果是一个需求都没完成。这只是两种不同的安排任务的顺序,我更支持按照需求的方式。
我们继续讨论任务分解。任务分解到这里需要看一下这几个任务有哪个不好实现。register 只是一个在数据库中存储对象的过程,没问题,但 login 和 logout 呢?
考虑到我们在做的是一个 REST 服务,这个服务可能是分布到多台机器上,请求到任何一台都能提供同样的服务,我们需要把登录信息共享出去。
这里我们就采用最常见的解决方案:用 Redis 共享数据。登录成功的话,就需要把用户的 Session 信息放到 Redis 里面,退出的话,就是删除 Session 信息。在我们的任务列表里,并没有出现 Session所以需要引入 Session 的概念。任务调整如下。
如果采用 Redis我们还需要决定一下在 Redis 里存储对象的方式我们可以用原生的Java序列化但一般在开发中我们会选择一个文本化的方式这样维护起来更容易。这里选择常见的 JSON所以任务就又增加了两项。
至此,最基本的登录退出功能已经实现了,但我们需要问一个问题,这就够了吗?之所以要登录,通常是要限定用户访问一些资源,所以,我们还需要一些访问控制的能力。
简单的做法就是加入一个 filter在请求到达真正的资源代码之前先做一层过滤在这个 filter 里面,如果待访问的地址是需要登录访问的,我们就看看用户是否已经登录,现在一般的做法是用一个 Token这个 Token 一般会从 HTTP 头里取出来。但这个 Token 是什么时候放进去的呢?答案显然是登录的时候。所以,我们继续调整任务列表。
至此,我们已经比较完整地实现了一个用户登录功能。当然,要在真实项目中应用,需求还是可以继续扩展的。比如:用户 Session 过期、用户名密码格式校验、密码加密保存以及刷新用户 Token等等。
这里主要还是为了说明任务分解,相信如果需求继续扩展,根据上面的讨论,你是有能力进行后续分解的。
来看一下分解好的任务清单,你也可以拿出来自己的任务清单对比一下,看看差别有多大。
-
首先要说明的是,任务分解没有一个绝对的标准答案,分解的结果根据个人技术能力的不同,差异也会很大。
检验每个任务项是否拆分到位,就是看你是否知道它应该怎么做了。不过,即便你技术能力已经很强了,我依然建议你把任务分解到很细,观其大略人人行,细致入微见本事。
也许你会问我,我在写代码的时候,也会这么一项一项地把所有任务都写下来吗?实话说,我不会。因为任务分解我在之前已经训练过无数次,已经习惯怎么一步一步地把事情做完。换句话说,任务清单虽然我没写下来,但已经在我脑子里了。
不过,我会把想到的,但容易忽略的细节写下来,因为任务清单的主要作用是备忘录。一般情况下,主流程我们不会遗漏,但各种细节常常会遗漏,所以,想到了还是要记下来。
另外,对比我们在分解过程中的顺序,你会看到这个完整任务清单的顺序是调整过的,你可以按照这个列表中的内容一项一项地做,调整最基本的标准是,按照这些任务的依赖关系以及前面提到的“完整地实现一个需求”的原则。
最后,我要特别强调一点,所有分解出来的任务,都是独立的。也就是说,每做完一个任务,代码都是可以提交的。只有这样,我们才可能做到真正意义上的小步提交。
如果今天的内容你只能记住一件事,那请记住:按照完整实现一个需求的顺序去安排分解出来的任务。
最后,我想请你分享一下,你的任务清单和我的任务清单有哪些差异呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,150 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 为什么你的测试不够好?
你好!我是郑晔。今天是除夕,我在这里给大家拜年了,祝大家在新的一年里,开发越做越顺利!
关于测试,我们前面讲了很多,比如:开发者应该写测试;要写可测的代码;要想做好 TDD先要做好任务分解我还带你进行了实战操作完整地分解了一个任务。
但有一个关于测试的重要话题,我们始终还没聊,那就是测试应该写成什么样。今天我就来说说怎么把测试写好。
你或许会说,这很简单啊,前面不都讲过了吗?不就是用测试框架写代码吗?其实,理论上来说,还真应该就是这么简单,但现实情况却往往相反。我看到过很多团队在测试上出现过各种各样的问题,比如:
测试不稳定,这次能过,下次过不了;
有时候是一个测试要测的东西很简单,测试周边的依赖很多,搭建环境就需要很长的时间;
这个测试要运行,必须等到另外一个测试运行结束;
……
如果你也在工作中遇到过类似的问题,那你理解的写测试和我理解的写测试可能不是一回事,那问题出在哪呢?
为什么你的测试不够好呢?
主要是因为这些测试不够简单。只有将复杂的测试拆分成简单的测试,测试才有可能做好。
简单的测试
测试为什么要简单呢?有一个很有趣的逻辑,不知道你想没想过,测试的作用是什么?显然,它是用来保证代码的正确性。随之而来的一个问题是,谁来保证测试的正确性?
许多人第一次面对这个问题,可能会一下子懵住,但脑子里很快便会出现一个答案:测试。但是,你看有人给测试写测试吗?肯定没有。因为一旦这么做,这个问题会随即上升,谁来保证那个测试的正确性呢?你总不能无限递归地给测试写测试吧。
既然无法用写程序的方式保证测试的正确性,我们只有一个办法:把测试写简单,简单到一目了然,不需要证明它的正确性。所以,如果你见到哪个测试写得很复杂,它一定不是一个好的测试。
既然说测试应该简单,我们就来看看一个简单的测试应该是什么样子。下面我给出一个简单的例子,你可以看一下。
@Test
void should_extract_HTTP_method_from_HTTP_request() {
// 前置准备
request = mock(HttpRequest.class);
when(request.getMethod()).thenReturn(HttpMethod.GET);
HttpMethodExtractor extractor = new HttpMethodExtractor();
// 执行
HttpMethod method = extractor.extract(request);
// 断言
assertThat(method, is(HttpMethod.GET);
// 清理
}
这个测试来自我的开源项目 Moco我稍做了一点调整便于理解。这个测试很简单从一个 HTTP 请求中提取出 HTTP 方法。
我把这段代码分成了四段,分别是前置准备、执行、断言和清理,这也是一般测试要具备的四段。
这几段的核心是中间的执行部分,它就是测试的目标,但实际上,它往往也是最短小的,一般就是一行代码调用。其他的部分都是围绕它展开的,在这里就是调用 HTTP 方法提取器提取 HTTP 方法。
前置准备,就是准备执行部分所需的依赖。比如,一个类所依赖的组件,或是调用方法所需要的参数。在这个测试里面,我们准备了一个 HTTP 请求,设置了它的方法是一个 GET 方法,这里面还用到了之前提到的 Mock 框架,因为完整地设置一个 HTTP 请求很麻烦,而且与这个测试也没什么关系。
断言是我们的预期,就是这段代码执行出来怎么算是对的。这里我们判断了提取出来的方法是否是 GET 方法。另外补充一点,断言并不仅仅是 assert如果你用 Mock 框架的话,用以校验 mock 对象行为的 verify 也是一种断言。
清理是一个可能会有的部分如果你的测试用到任何资源都可以在这里释放掉。不过如果你利用好现有的测试基础设施比如JUnit 的 Rule遵循好测试规范的话很多情况下这个部分就会省掉了。
怎么样,看着很简单吧,是不是符合我前面所说的不证自明呢?
测试的坏味道
有了对测试结构的了解,我们再来说说常见的测试“坏味道”。
首先是执行部分。不知道你有没有注意到,前面我提到执行部分时用了一个说法,一行代码调用。是的,第一个“坏味道”就来自这里。
很多人总想在一个测试里做很多的事情,比如,出现了几个不同方法的调用。请问,你的代码到底是在测试谁呢?
这个测试一旦出错,就需要把所有相关的几个方法都查看一遍,这无疑是增加了工作的复杂度。
也许你会问,那我有好几个方法要测试,该怎么办呢?很简单,多写几个测试就好了。
另一个典型“坏味道”的高发区是在断言上,请记住,测试一定要有断言。没有断言的测试,是没有意义的,就像你说自己是世界冠军,总得比个赛吧!
我见过不少人写了不少测试,但测试运行几乎从来就不会错。出于好奇,我打开代码一看,没有断言。
没有断言当然就不会错了,写测试的同事还很委屈地说,测试不好写,而且,他已经验证了这段代码是对的。就像我前面讲过的,测试不好写,往往是设计的问题,应该调整的是设计,而不是在测试这里做妥协。
还有一种常见的“坏味道”:复杂。最典型的场景是,当你看到测试代码里出现各种判断和循环语句,基本上这个测试就有问题了。
举个例子,测试一个函数,你的断言写在一堆 if 语句中,美其名曰,根据条件执行。还是前面提到的那个观点,你怎么保证这个测试函数写的是对的?除非你用调试的手段,否则,你都无法判断你的条件分支是否执行到了。
你或许会疑问,我有一大堆不同的数据要测,不用循环不用判断,我怎么办呢?你真正应该做的是,多写几个测试,每个测试覆盖一种场景。
一段旅程A-TRIP
怎么样的测试算是好的测试呢?有人做了一个总结 A-TRIP这是五个单词的缩写分别是
Automatic自动化
Thorough全面的
Repeatable可重复的
Independent独立的
Professional专业的。
下面,我们看看这几个单词分别代表什么意思。
Automatic自动化。有了前面关于自动化测试的铺垫这可能最好理解就是把测试尽可能交给机器执行人工参与的部分越少越好。
这也是我们在前面说,测试一定要有断言的原因,因为一个测试只有在有断言的情况下,机器才能自动地判断测试是否成功。
Thorough全面应该尽可能用测试覆盖各种场景。理解这一点有两个角度。一个是在写代码之前要考虑各种场景正常的、异常的、各种边界条件另一个角度是写完代码之后我们要看测试是否覆盖了所有的代码和所有的分支这就是各种测试覆盖率工具发挥作用的场景了。
当然,你想做到全面,并非易事,如果你的团队在补测试,一种办法是让测试覆盖率逐步提升。
Repeatable可重复的。这里面有两个角度某一个测试反复运行结果应该是一样的这说的是每一个测试本身都不应该依赖于任何不在控制之下的环境。如果有怎么办想办法。
比如,如果有外部的依赖,就可以采用模拟服务的手段,我的 Moco 就是为了解决外部依赖而生的,它可以模拟外部的 HTTP 服务,让测试变得可控。
有的测试会依赖数据库,那就在执行完测试之后,将数据库环境恢复,像 Spring 的测试框架就提供了测试数据库回滚的能力。如果你的测试反复运行,不能产生相同的结果,要么是代码有问题,要么是测试有问题。
理解可重复性,还有一个角度,一堆测试反复运行,结果应该是一样的。这说明测试和测试之间没有任何依赖,这也是我们接下来要说的测试的另外一个特点。
Independent独立的。测试和测试之间不应该有任何依赖什么叫有依赖比如如果测试依赖于外部数据库或是第三方服务测试 A 在运行时在数据库里写了一些值,测试 B 要用到数据库里的这些值,测试 B 必须在测试 A 之后运行,这就叫有依赖。
我们不能假设测试是按照编写顺序运行的。比如,有时为了加快测试运行速度,我们会将测试并行起来,在这种情况下,顺序是完全无法保证的。如果测试之间有依赖,就有可能出现各种问题。
减少外部依赖可以用 mock实在要依赖每个测试自己负责前置准备和后续清理。如果多个测试都有同样的准备和清理呢那不就是 setup 和 teardown 发挥作用的地方吗?测试基础设施早就为我们做好了准备。
Professional专业的。这一点是很多人观念中缺失的测试代码也是代码也要按照代码的标准去维护。这就意味着你的测试代码也要写得清晰比如良好的命名把函数写小要重构甚至要抽象出测试的基础库在 Web 测试中常见的 PageObject 模式,就是这种理念的延伸。
看了这点,你或许会想,你说的东西有点道理,但我的代码那么复杂,测试路径非常多,我怎么能够让自己的测试做到满足这些要求呢?
我必须强调一个之前讲测试驱动开发强调过的观点:编写可测试的代码。很多人写不好测试,或者觉得测试难写,关键就在于,你始终是站在写代码的视角,而不是写测试的视角。如果你都不重视测试,不给测试留好空间,测试怎么能做好呢?
总结时刻
测试是一个说起来很简单,但很不容易写好的东西。在实际工作中,很多人都会遇到关于测试的各种各样问题。之所以出现问题,主要是因为这些测试写得太复杂了。测试一旦复杂了,我们就很难保证测试的正确性,何谈用测试保证代码的正确性。
我给你讲了测试的基本结构:前置准备、执行、断言和清理,还介绍了一些常见的测试“坏味道”:做了太多事的测试,没有断言的测试,还有一种看一眼就知道有问题的“坏味道”,测试里有判断语句。
怎么衡量测试是否做好了呢有一个标准A-TRIP这是五个单词的缩写分别是Automatic自动化、Thorough全面、Repeatable可重复的、Independent独立的和 Professional专业的
如果今天的内容你只能记住一件事,那请记住:要想写好测试,就要写简单的测试。
最后,我想请你分享一下,经过最近持续对测试的讲解,你对测试有了哪些与之前不同的理解呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,126 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 程序员也可以“砍”需求吗?
你好,我是郑晔。
我们前面讲的任务分解,主要是在讲开发任务的分解。今天我们换个角度,看看需求的分解。是的,需求也要分解。
有一次,我和一个做开发的同事聊天,他给我讲了他近期的烦恼。
同事:我们现在就是需求太多,开发的人太少,再这么干下去,哪天觉得自己抗不住了,我就拍拍屁股走人。-
我:你没尝试着砍砍需求?-
同事:怎么没尝试?产品的人都不同意。这批功能他们都说是关键功能。-
我:你有没有尝试把需求拆开了再砍呢?-
同事:还可以这样?
同事很惊讶,我一点都不意。我们都是在说需求,但彼此对需求的理解却是大不相同。我先来问个问题,提到需求这个词,你会想到什么呢?
以我们用了好多次的登录为例,如果我问你这个需求是什么,大多数人的第一直觉还是用户名密码登录。
基本上闯入你脑海的需求描述是主题epic在敏捷开发中有人称之为主用户故事master story
如果你对需求的管理粒度就是主题,那好多事情就没法谈了。比如,时间紧迫的时候,我想砍需求,你问产品经理,我不做登录行不行,你就等着被拒绝吧。
但是,如果你说时间比较紧,我能不能把登录验证码放到后面做,或是邮件地址验证的功能放到后面,这种建议产品经理是可以和你谈的。
这其中的差别就在于,后者将需求分解了。
大多数人可以理解需求是要分解的,但是,分解的程度不同,就是导致执行效果差异极大的根源。
以我的经验而言,绝大多数问题都是由于分解的粒度太大造成的,少有因为粒度太小而出问题的。所以,需求分解的一个原则是,粒度越小越好。
需求要分解
“主题”只是帮你记住大方向,真正用来进行需求管理,还是要靠进一步分解出来的需求。这里的讨论,我们会继续沿用前面专栏文章中已经介绍过的需求描述方式:用户故事,它将是我们这里讨论需求管理的基本单位。
如果你的团队用的是其他方式描述需求,你也可以找找是否有对应的管理方式。
上一个模块介绍“以终为始”,我们对用户故事的关注点主要在:用户故事一定要有验收标准,以确保一个需求的完整性。而在“任务分解”这个模块,我们看用户故事,则主要关注它作为需求分解的结果,也就是分拆出来要解决的一个个需求点。
在前面的讨论中,我们已经知道了用户故事的“长相”,但更重要的问题是,划分需求的方式有无数种,就像一块蛋糕,你可以横着切,也可以竖着切。如果你一刀不切,那就是拿着主题当用户故事。你也可以快刀飞起,把主题切碎。
每个人都会有自己喜欢的拆分方式,我相信知道拆分的重要性之后,你总会有办法的。这里,我主要想和你聊聊怎样评判拆分结果,毕竟我们要把它当作需求管理的基本单位。
只有细分的需求才能方便进行管理。什么样的需求才是一个好的细分需求呢?我们先来看看用户故事的衡量标准。
评价用户故事有一个“ INVEST 原则”,这是六个单词的缩写,分别是:
Independent独立的。一个用户故事应该完成一个独立的功能尽可能不依赖于其它用户故事因为彼此依赖的用户故事会让管理优先级、预估工作量都变得更加困难。如果真的有依赖一种好的做法是将依赖部分拆出来重新调整。
Negotiable可协商的。有事大家商量是一起工作的前提我们无法保证所有的细节都能100%落实到用户故事里,这个时候最好的办法是大家商量。它也是满足其它评判标准的前提,就像前面提到的,一个用户故事不独立,需要分解,这也需要大家一起商量的。
Valuable有价值的。一个用户故事都应该有其自身价值这一项应该最容易理解没有价值的事不做。但正如我们一直在说的那样做任何一个事情之前先问问价值所在。
Estimatable可估算的。我们会利用用户故事估算的结果安排后续的工作计划。不能估算的用户故事要么是因为有很多不确定的因素要么是因为需求还是太大这样的故事还没有到一个能开发的状态还需要产品经理进一步分析。
Small小。步子大了不行。不能在一定时间内完成的用户故事只应该有一个结果拆分。小的用户故事才方便调度才好安排工作。
Testable可测试的。不能测试谁知道你做得对不对。这个是我们在前面已经强调过的内容也就是验收标准你得知道怎样才算是工作完成。
“INVEST 原则”的说法是为了方便记忆,我们这里着重讨论两个点。
第一个关注点是可协商。作为实现者,我们要问问题。只是被动接受的程序员,价值就少了一半,只要你开始发问,你就会发现很多写需求的人没有想清楚的地方。
在我的职业生涯中,我无数次将需求挡了回去,不是我不合作,而是我不想做一些糊涂的需求。我之所以能问出问题,一方面是出于常识,另一方面就是这里说的用户故事是否有价值。用户故事,之所以是故事,就是要讲,要沟通。
还有一个更重要的关注点,也是这个模块的核心:小。无论是独立性也好,还是可估算的也罢,其前提都是小。只有当用户故事够小了,我们后续的腾挪空间才会大。
那接下来就是一个重要的问题,怎么才算小?这就牵扯到用户故事另一个重要方面:估算。
需求的估算
估算用户故事,首先要选择一个度量标准。度量用户故事大小的方式有很多种,有人用 T 恤大小的方式也就是S、M、L、XL、XXL。也有人用费波纳契数列也就是1、2、3、5、8等等。有了度量标准之后就可以开始估算了。
我们从分解出来的用户故事挑出一个最简单的,比如,某个信息的查询。这个最简单的用户故事,其作用就是当作基准。
比如我们采用费波纳契数列那这个最简单的用户故事就是基准点1。其他的用户故事要与它一一比较如果一个用户故事比它复杂那可以按照复杂程度给个估计。
你或许会问,我怎么知道复杂程度是什么样的呢?这时候,我们前面讲过的任务分解就派上用场了,你得在大脑中快速地做一个任务分解,想想有哪些步骤要完成,然后才好做对比。
所以,你会发现,任务分解是基础中的基础,不学会分解,工作就只能依赖于感觉,很难成为一个靠谱的程序员。
估算的结果是相对的,不是绝对精确的,我们不必像做科研一样,只要给出一个相对估算就好。
同一个用户故事,不同的人估算出的结果可能会有差别。怎么样尽可能在团队中达成一致呢?这就需要团队中的很多人参与进来,如果团队规模不大,全员参与也可以。
如果多人进行估算,你就会发现一个有趣的现象,针对同一个用户故事,不同的人估算的结果差异很大。
如果差别不大比如你觉得3个点我觉得2个点我们协调一下就好。但如果差异很大比如你认为2个点我认为8个点那绝对是双方对任务的理解出现了巨大的差异这个时候我们就可以把刚才在脑中进行的任务分解“摆”到桌面上看看差异在哪。
通常情况下,是双方对需求的理解出现了偏差,这时候负责用户故事编写的同事就要站出来,帮助大家澄清需求。所以,一般来说,估算的过程也是大家加深对需求理解的过程。
估算还有另外一个重要的作用:发现特别大的用户故事。一般而言,一个用户故事应该在一个迭代内完成。
比如你预计大小为1点的用户故事要用1天完成而你团队的迭代周期是两周也就是10个工作日那13点的任务是无论如何都完不成的。那该怎么办呢很简单把它拆分成多个小任务这样一来每个小任务都可以在一个迭代中完成了。
所以,一般来说,用户故事有可能经过两次拆分。一次是由负责业务需求的同事,比如,产品经理,根据业务做一次拆分。另外一次就是在估算阶段发现过大的用户故事,就再拆分一次。
当我们有了一个合适的用户故事列表,接下来,我们就可以安排我们的开发计划了。只要厘清用户故事之间的依赖关系,安排工作是每一个团队都擅长的事情。
我在这里想回到我们开头讨论的话题。我们常说,需求来自产品经理,但需求到底是什么,这是一个很宽泛的话题。到这里,我们已经有了一个更清晰更可管理的需求,用户故事。这时候我们再说需求调整,调整的就不再是一个大主题,而是一个个具体的用户故事了。
许多团队真正的困境在于,在开发过程中缺少需求分解的环节。在这种情况下,需求的管理基本单位就是一个主题,既然是基本单位,那就是一个不可分割的整体。团队就被生生绑死在一个巨大的需求上,没有回旋的余地。
如果团队可以将需求分解,需求的基本单位就会缩小,每个人看到的就不再是“铁板”一块,才能更方便地进行调整,才会有比较大的腾挪空间。
总结时刻
软件开发中,需求管理是非常重要的一环。在需求管理上常见的错误是,需求管理的粒度太大,很多团队几乎是在用一个大主题在管理需求,这就让需求调整的空间变得很小。
结合用户故事我给你讲了一个好的需求管理基本单位是什么样子的它要符合“INVEST原则”。其中的一个关键点是“小”只有小的需求才方便管理和调整。
什么样的需求才算小呢?我给你介绍了一种需求估算的方式,每个团队都可以根据自己的特点决定在自己的团队里,多大的需求算大。大需求怎么办?只要再进行分解就好了。
如果你对用户故事这个话题感兴趣,推荐阅读 Mike Cohn 的两本书《User Stories Applied》和《Agile Estimating and Planning》。
如果今天的内容你只能记住一件事,那请记住:想要管理好需求,先把需求拆小。
最后,我想请你分享一下,你的团队在需求管理上还遇到过哪些问题呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,128 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 需求管理:太多人给你安排任务,怎么办?
你好,我是郑晔。
上一讲我们讲了需求的分解,我以用户故事为例,给你讲了我们应该把大的需求拆分成小的需求,但是不是只要把需求拆开了就万事大吉了呢?显然不是。今天我们再来探讨另一个与需求强相关的话题:需求管理。
需求管理?许多程序员的第一直觉通常是,这要么是产品经理的事,要么是项目经理的事,跟我有什么关系?我知道很多人会这么想,可我想说的是,如果你不了解需求是怎么管理的,即便是进行了需求分解,最终的结果很有可能依然是你深陷泥潭苦苦挣扎而不自知。
为什么这么说呢?我给你讲一个发生在我身边的故事。
最无脑的需求管理法:老板说的
有一次,我们组织了一次各团队负责人的吐槽大会,让大家把遇到的问题在台面上“摆”一下。一个开发团队的负责人说:“我这边倒排期太严重了,每个产品经理到我这里都说上线日期已经定好了,我这边资源有限,实在是抗不住了。”
出于好奇,有人问:“这些任务都一样重要吗?”
这个负责人无奈地摇摇头,“他们都说自己的任务重要。”
“他们凭什么说自己的任务重要呢?”我也问了一个问题。
这个负责人说:“他们告诉我,是老板说的。”
这是不是一个很熟悉的场景?一堆任务压过来,只是因为这是老板的一句话。我们的老板都是这么不近人情吗?其实,大概率来看,并不是。
就凭一句“老板说的”,我们就可以判断出,产品经理缺乏对需求管理应有的理解。而研发团队也因为无脑地接受了需求,几乎将自己压垮。
这时候CTO 发话了:“口头的东西不算数,如果他们说是老板说的,那就让老板发邮件确认。”
我很认可CTO的说法但我并不放心那个开发团队的负责人于是我问他“你会让产品经理这么去做吗”果然他犹豫了。
“产品经理可能不会和老板这么说。那你去说好了。”我们又给他提了个建议。显然,他更犹豫了,毕竟要面对大老板。
针对这种情况,我们又给出了一个解决办法,“如果你担心产品经理不这么做,你可以直接发邮件给老板,同时抄送 CTO。”
“对可以这么做”CTO 把责任扛了过去。这个负责人心里一下子有底了。
是不是有种似曾相识的感觉?其实,这个故事只要再往下延伸一点,就到了我们程序员身边。
作为程序员,我们面临的场景往往是,一个需求不明就里地来了,你的周末假期全部泡汤,因为你的负责人会和你说,这是老板说的。
软件行业有个段子:做软件,最理想的交付日期是什么时候?答案是昨天,其次是尽快。所有提出业务需求的人都恨不得需求早就做好了。但事实总是那么不如人意,所以,他们只能寄希望于需求被尽快实现。
如果我们等着所有需求都开发好了再上线呢?这就是当年所谓瀑布模型做的事,放在二十年前,这种做法还有生存空间,但今天这种做法显然已经不合时宜了。
关于如何做软件,我们已经讨论了很多,关键点就在于这个世界有太多的不确定,我们只好把产品的“一部分”开发好,送上线。
这就引出了一个问题,到底是选择“哪部分”优先上线呢?我们必须在宏大的理想和骨感的现实中作出取舍。这也就牵扯出需求管理的本质,实际上是个优先级的问题。
需求的优先级
“来自老板”,这是判断优先级最简单的答案,也是推卸责任的一个答案。其潜台词是,压力大不怪我,要怪就怪老板去。“来自老板”不应该成为优先做事的指标。
首先,我们要明确一点,优先级这种事大家也是可以谈的,大多数能当老板的人都是可以讲道理的。但要和老板谈,我们得知道怎么讲道理。准备一些基础知识,才能与各级老板探讨怎么安排工作的优先级。
为什么要区分优先级?因为时间是有限的,有限的时间内你能完成工作的上限是一定的。
怎么充分利用好有限的时间,这其实是一个时间管理的问题。所以,我们完全可以借鉴时间管理领域的一些优秀实践,帮助我们更有效地明辨优先级。
谈到时间管理一个有效的时间管理策略是艾森豪威尔矩阵Eisenhower Matrix这是由美国前总统艾森豪威尔开发出的一个工具。
这个工具到了史蒂芬·柯维Stephen Richards Covey手里得到了发扬光大他那本著名的《高效能人士的七个习惯》书籍将其推广至世界各地。也许这个名字你不太熟悉看一下下面这个图你就知道了。
-
它将事情按照重要和紧急两个维度进行划分,也就形成了四个部分:重要且紧急,重要不紧急,不重要且紧急,不重要不紧急。
用几个程序员生活中的例子帮你理解一下。让系统不能正常运行的线上故障,就属于重要且紧急事情,不赶紧解决,就影响公司的正常运营。团队对系统升级改造就属于重要不紧急:改造好,性能也好,可维护性也得到提升;不改造,一时半会也能用。一些临时任务都属于紧急不重要,而刷朋友圈则属于既不紧急也不重要。
按照时间管理的理念,重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。
这个矩阵带给我们思维上最大的改变是,让人意识到事情和事情不是等价的。如果不把精力放在重要的事情上,到最后可能都变成紧急的事情。
比如,我们放任系统不做升级改造,过多的技术债会让系统的问题越来越多,新需求实现的速度越来越慢,最后几个看起来不大的需求就足以让团队加班加点,天怒人怨。
把这个思路带回到我们现实的需求管理中,你会发现,其实团队面临的各种需求所采用的优先级排序方式,基本上都是按照紧急程度排列的,但它们是否真的重要呢?
如果你把这个问题抛给需求的提出者,我几乎可以肯定,他们给你的答案是,他们提出的需求就是重要的。一种可能是,他们也分不清重要和紧急的差别,正如有时候我们也糊涂一样。
对于这样的场景,我们要做的就是多问一些问题。我在“精益创业:产品经理不靠谱,你该怎么办?”文章中说过,默认所有需求都不做,直到弄清楚为什么要做这件事。
同样,需求也没那么重要,直到产品经理能说明白它为什么重要,尤其是为什么比其他需求重要。如果一个产品经理不能把几个需求排出优先级,你就可以把上面学到的内容给他讲一遍。
还有另一种可能,他给你的需求在他工作的上下文中,确实是最重要的内容了。但当有多个需求来源时,我们该如何确认哪个需求是最重要的呢?这时,才到了真正需要老板出场的时刻。
站在老板面前
在“解决了很多问题,为什么你依然在‘坑’里?”文章中,我曾经讲过,大家不要局限于程序员这个角色,不同角色真正的差异是工作上下文的不同。每个人都在自己的上下文里工作,上下文也就局限了很多人的视野。
试想,两个产品经理出现在你面前,一个告诉你,公司要拓展新方向,这个功能要做;另一个却说,公司要进一步盈利,那个功能必须做。
在你看来,他们两个说得都对,听上去都挺重要的。但骨感的现实是,你把两件事都接下来,等着你的是累死都完不成的任务。
这个时候,我们能做的是什么呢?跳出这个上下文,到更大的上下文中。你判断不了哪个需求更重要,就请更高一级的老板来判断。
有了基础知识的储备,我们终于可以站在了老板面前。你可以告诉老板:我资源有限,需要将这两个需求排个序,看哪个更重要。我的上下文有限,需要你帮我判断一下。
老板会和你说这两个需求的起源,扩展盈利的需求是竞争对手都已经有了,客户也问这边要,再不做会影响客户关系,尤其是新财年快到了,下个阶段的合同会受到影响。而另外的新业务是某天一个高端聚会上得到的新启发,想尝试一下,他也不确定这个想法能带来多少收益,就让产品部门试一下。
听了老板的信息,你顿时明白这两件事的重要性,你也知道该如何面对两个产品经理了。
老板比你们的上下文大,因为他有看待这个问题更多的维度。所以,在你们眼里无比纠结的事情,老板几句话就云开雾散了,在他眼里,那根本不叫事。
如果你看过刘慈欣的《三体》,就会知道,这其实是“降维攻击”。另一个你可能熟悉的说法叫大局观。我经常和人说,当员工想不明白的事,换成老板的视角就全明白了。
我鼓励每个程序员在更大的上下文中工作,也就是想让人获得更多的思考维度。而今天的内容主要告诉你,如果自己的上下文不足时,我们可以引入新的元素,比如征求老板意见,扩大自己的上下文。
再发散讲几句,为人做事同样要不断扩展自己的上下文,这也就是我们常说的涨见识。
很多所谓的人生难题不过是因为见识有限造成的。比如,如果你觉得公司内总有人跟你比技术,莫不如把眼光放得长远一些,把自己放在全行业的水平上去比较。因为你是为自己的职业生涯在工作,而不是一个公司。
总结时刻
需求分解之后,最重要的是,排列需求的优先级。优先级的排列方式有很多,我们可以借鉴时间管理的方法,把事情按照重要和紧急的维度进行划分,得到了四个象限。我们要尽可能把精力放在重要的事情上,而不是把紧急的事情当成优先级排序的方式。
需求分解成一个个小块,其实也分解了原本合一的上下文。如果想要有效地管理需求,尤其是确定事情的重要程度,一种方式是找回丢失的上下文。如果我们自己无法判断上下文,一种好的办法是,引入外部更大的上下文。
如果今天的内容你只能记住一件事,那请记住:尽量做最重要的事。
最后,我想请你分享一下,你的团队在日常的需求管理中,还遇到哪些问题呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 可视化:一种更为直观的沟通方式
作为一个程序员,在这个技术快速发展的时代,我们唯有不断学习,才能保证自己不为时代所抛弃。那你是怎么跟上技术发展步伐的呢?
就个人经验而言,我会关注一些技术网站,最典型的就是 InfoQ。这样我可以快速了解到技术发展的动向比如什么时候出了个新东西、哪个项目又有了重大的更新、某些技术有了哪些新的应用场景等等。
另外我还有一种更系统地了解新知识的方式ThoughtWorks 技术雷达。之所以我很喜欢这种方式,因为它是“可视化”的。
什么是技术雷达?
ThoughtWorks 技术雷达是由 ThoughtWorks 技术咨询委员会Technology Advisory Board编写的一份技术趋势报告每6个月发布一次。ThoughtWorks 的项目多样性足够丰富,所以它能够发现诸多技术趋势。因此,相比于行业中其它的预测报告,技术雷达更加具体,更具可操作性。
ThoughtWorks 是我的老东家所以我在接触技术雷达的时间很早。我在2013年就已经开始与人讨论微服务并在项目中尝试使用 Docker而这一切信息的来源都是技术雷达。不过我这里想和你讨论并不是技术雷达到底有多优秀而是带你看看技术雷达这种组织知识的可视化形式。-
图片来源ThoughtWorks 技术雷达)
技术雷达用来追踪技术,在雷达图的术语里,每一项技术表示为一个 blip也就是雷达上的一个光点。
然后用两个分类元素组织这些 blip象限quadrant和圆环ring其中象限表示一个 blip 的种类,目前有四个种类:技术、平台、工具,还有语言与框架。
圆环表示一个 blip 在技术采纳生命周期中所处的阶段目前这个生命周期包含四个阶段采用Adopt、试验Trial、评估Assess和暂缓Hold
每次技术雷达发布之后,我会特别关注一下“采用” 和 “暂缓”两项。
“采用”表示强烈推荐我会去对比一下自己在实际应用中是否用到了比如在2018年11月的技术雷达中事件风暴Event Storming放到了“采用”中如果你还不了解 事件风暴 是什么,强烈建议你点击链接了解一下。
“暂缓” 则表示新项目别再用这项技术了这会给我提个醒这项技术可能已经有了更优秀的替代品比如Java世界中最常见的构建工具 Maven 很早就放到了“暂缓”项中,但时至今日,很多人启动新项目依然会选择 Maven多半这些人并不了解技术趋势。
从这几年的发展趋势来看,技术雷达在“采用”和“暂缓”这两项上给出的推荐,大部分是靠谱的。
至于“试验”和“评估”两项,有时间的时候,我会慢慢看,因为它们多半属于新兴技术的试验区,主要的作用是用来让我开拓视野的。
雷达图是一种很好的将知识分类组织的形式,它可以让你一目了然地看到并了解所有知识点,并根据自己的需要,决定是否深入了解。
所以,我的前同事们借鉴了这个形式,做出了一个程序员的读书雷达,将程序员的应该阅读的书籍做了一个整理。-
-
图片来源ThoughtWorks读书雷达
事实上这种将内容通过可视化方式的组织起来的形式非常好用ThoughtWorks 鼓励每个组织都建立自己的知识雷达,甚至提供了一个工具辅助你将雷达图构建出来。
在我看来,雷达图不仅仅适用于组织,也可以适用于团队。
我也曾经按照雷达图的方式将自己的团队用到的技术组织起来。把最需要了解的技术必须放在内环,比如:一个 Java 项目。我会要求程序员了解 Java向外扩展的就是你在这个团队内工作会逐渐接触到的技术比如像 Docker 这种与部署相关的知识。至于最外面一层就是被我们放弃掉的技术比如Maven。
这样一来,团队成员可以更清晰地了解到团队中所用的技术。当有新人加入团队时,这个雷达可以帮助新人迅速地抓住重点,他的学习路径就是从内环向外学习。所以,我也推荐你打造自己团队的技术雷达。
构建技术雷达-
构建雷达的程序库
你是否想过,为什么雷达图的形式可以帮助你更好地理解知识呢?因为人的大脑更擅长处理图像。
可视化的优势
在远古时代人脑处理的内容大多是图像比如哪里有新的果实哪里猛兽出没文字则是很久之后才产生的。现在普遍的一种说法是大约在公元前3500年左右许多文明才刚刚发展出书写系统相比于人类的历史来说这几乎是微不足道的。
就人脑的进化而言,处理图像的速度远远快于处理文字,所以,有“一图胜千言”的说法。
通过创建图像、图标或动画等进行信息交流的形式就是可视化Visualization。可视化有很多种不同的分类我们最常用的应该是数据可视化和信息可视化。
我在“你的工作可以用数字衡量吗”这篇文章里说过,我上班第一件事是“看”数字,这就是典型的数据可视化,而上面介绍的技术雷达,就属于信息可视化。
很多做软件的人习惯于用文字进行沟通,一般在软件开发过程中,需要编写各种文档,但并不是所有的场景,文字都是好的沟通方式,所以,也会有很多人尝试着将可视化应用在软件开发过程中。
估计大多数程序员最熟悉的表达方式应该是流程图,如果你做过软件设计,可能还听说过 UML统一建模语言Unified Modeling Language。如果使用得当这种方式会极大地提高表达的准确性降低其他人理解的门槛。
在日常工作中,你最熟悉的可视化方式,大概就是在纸上或白板上画的图。以我的经验看,很多人画这个图太随意,如果你也是这样,我给你一个建议,先写字后画框,这样图会显得整洁一些。
什么是看板?
我们再来看一个实践,这就是将“可视化”应用在工作中的典型案例:看板。
看板,是一种项目管理工具,它将我们正在进行的工作变得可视化。这个实践来自精益生产,前面讲精益创业时,我给介绍了“精益”这个来自丰田公司的管理理念。精益的理念在软件行业已经非常流行了,很多软件开发实践都是从“精益”而来,看板就是其中之一。
看板属于那种几乎是看一眼就知道怎么用的实践。它将工作分成几个不同的阶段,然后,把分解出来的工作做成一张卡片,根据当前状态放置到不同的阶段中。如果你采用了我们专栏之前讲过的用户故事,那么每个用户故事就是一张卡片。
在实际工作中,每当一个工作完成之后,它就可以挪到下一个阶段,工作怎么算完成就是由我们前面提到的 DoD 来决定的。
当然,要用好看板,还可以使用一些小技巧。比如,用不同颜色的卡表示不同类型的工作,给每个人一个头像,增添一些乐趣。
看板可以帮助你一眼看出许多问题比如你的团队中有5个人却有8个正在进行的任务那一定是有问题的。因为一个人多线程工作效果不会好。用“精益”的术语来说我们应该限制 WIPWork-In-Progress再有如果待开发的卡最多那就证明现在的瓶颈在于开发而不是其它阶段。
运用看板的方式,还有一个有趣的细节:使用实体墙还是电子墙。实体墙不难理解,就是找一面墙把看板做出来。现在有很多公司专门在做协同办公软件,其中的项目管理部分用到的就是看板理念,这就是电子墙的由来。
关于这点,顺便说一下我的建议,如果你的团队是在一起工作的,请考虑使用实体墙,除非你的办公空间实在太小。因为它可以方便地调整,也可以当作站会的集合地点,还可以让别人看见你们的工作或是问题,这样做的最大优势在于增强了人与人的互动。
电子墙的优势在于,随处可访问、数据不会丢失、便于统计等等,但每次访问它,都需要专门打开电脑,还是比较麻烦的。一种将二者结合的办法是,使用一个大电视,专门用来展示电子墙。
总之,看板就是要让工作在大家面前展现出来。
总结时刻
我给你介绍了一种结构化学习新知识的方式:技术雷达。
技术雷达就是一种将技术信息组织起来的方式。它通过将技术按照“象限”和“圆环”两个维度进行分类,让人可以直观地看到并理解不同的技术所处的发展阶段。
雷达图是一种很好的形式,不仅可以用在组织技术,还可以用来组织其它信息,比如,读书雷达。每个公司都可以利用雷达图的形式组织自己所有的技术,每个团队也可以利用雷达图的形式组织自己团队用到的技术,这样,方便团队成员结构化地理解用到技术,也方便新人的学习。
雷达图实际上是一种可视化的方法人脑对于图像处理速度更快因此可视化是改善沟通的一种方式。大多数软件过程习惯采用文字的方式进行表达对于“可视化”利用的还不够。当然还是有一些利用“可视化”的方法比如流程图、UML 等。
最后,我给你介绍了一个利用可视化进行信息沟通的实践:看板。看板把工作分成了几个不同的阶段,在看板上对应不同的列,然后,每个任务作为一张卡贴在上面。每完成一张卡,就把这张卡挪到下一个阶段。
看板可以帮你发现许多问题,比如,当前进展是否合适,是否有人同时在做很多的事,发现当前工作的瓶颈等等。
如果今天的内容你只能记住一件事,那请记住:多尝试用可视化的方式进行沟通。
最后,我想请你思考一下,你在工作中,有哪些用到可视化方法解决沟通问题的场景?欢迎留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,157 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 快速反馈:为什么你们公司总是做不好持续集成?
你好,我是郑晔。
在“以终为始”那个模块,我们留下了一个巨大的尾巴。在“持续集成:集成本身就是写代码的一个环节”这篇文章中,我们是站在“以终为始”的角度阐述了集成,尤其是持续集成的重要性。
但怎么做好持续集成,才是很多人真正关心的内容。今天,我们就来谈谈如何做好持续集成。
既然我们打算讨论持续集成,不妨停下来先思考一个问题:你对持续集成的第一印象是什么。
持续集成Jenkins没错很多人对持续集成第一印象都是持续集成服务器也就是 CI 服务器,当年是 CruiseControl今天换成了 Jenkins。
也正是因为如此,很多人就把 CI 服务器理解成了持续集成。我就曾经接触过这样的团队,他们恨不得把所有的事情都放在 CI 服务器上做:在 CI 服务器上做了编译,跑了代码检查,运行了单元测试,做了测试覆盖率的统计等等。
或许你会疑问,这有什么不对的吗?
在做软件这件事上,我们不会用对与错去衡量,我只能说,这种做法是可行的,但它不是最佳实践。我希望你去思考,有没有比这更好的做法呢?
想要回答这个问题,我们还是要回到持续集成的本质上去。持续集成的诞生,就是人们尝试缩短集成周期的结果。为什么要缩短周期呢?因为我们希望尽早得到反馈,知道自己的工作结果是否有效。
所以,想要做好持续集成,就需要顺应持续集成的本质:尽快得到工作反馈。
由此,我们便得到持续集成的关键点,你只要记住一句话,快速反馈。
快速反馈,这句分成两个部分,快速和反馈,这也就引出了持续集成的两个重要目标:怎样快速地得到反馈,以及什么样的反馈是有效的。
快速得到反馈
我们回到前面的例子上,把各种检查放到 CI 服务器上执行,它可以让我们知道代码是不是有问题,这是一个有效的反馈,但它的反馈够快速吗?虽然比起没有持续集成的状态,它是好很多。但是,我们需要问一个问题,能不能更快地得到反馈呢?
显然,我们还可以做得更快。在自己的开发机上执行这些检查,就会比在 CI 服务器快。也就是说,执行同样的操作,本地环境会快于 CI 服务器环境。
为什么会这样呢?我们先来看看所有检查在 CI 服务器上执行,每个程序员的动作是什么样的。
我们写好代码,然后需要提交代码,等待 CI 服务器运行检查结果,然后,用 CI 监视器查看执行结果。如果没问题,继续做下一个任务,如果有错误,修复错误,再执行同样的过程。
再来看看本地执行的动作。运行构建脚本,如果一切正确,你可以选择提交代码或是继续下一个任务,如果失败,立即修复。
对比之下,在本地运行这些检查,你不需要提交,不需要等 CI 服务器开始执行,不需要跑到额外的地方查看检查结果。所以,这个操作比提交到服务器上会快很多。
另外,这里还有一个关键点,我们的操作是连续的。一旦检查结果出错了,我们立刻进入修复环节。作为程序员,我们太了解连续操作的重要性了。这就像打游戏时,我们感觉不到时间流逝一般,有人把这种状态称之为“心流”。
而提交代码,等待 CI 服务器的检查结果,就等于强迫你停下来,你的心流就被打断了。
如果你对心流的概念感兴趣,可以去读米哈里·契克森米哈赖的著作《心流》,这位作者就是心流概念的提出者。
前面我们只是在说,你作为程序员个体,使用持续集成的效果,这只是为了简化讨论。接下来,我们向更真实的世界靠拢,引入另一个重要的因素:团队协作。
假设你的团队就是在 CI 服务器上执行检查。你兴高采烈地写完一段代码准备提交,结果,此时你隔壁的同事手快一筹,先提交了,你不得不停下来等他。如果很不幸,你同事的检查失败的话,那么他又要把它修复好,你等的时间就更长了。
一个小问题也就罢了,如果是个大问题,他可能要修很长一段时间。这个时候,你除了等待,也没有更好的选择。如此一来,大把的时间就被浪费掉了。
这里我们要“插播”持续集成中重要的一个提交纪律:只有 CI 服务器处于绿色的状态才能提交代码。有检查在运行不能提交,有错误不能提交。原因很简单,如果这个时候多个人提交了代码,检查失败了,那问题到底算谁的呢?
反之,如果一次只有一个人提交代码,责任是明确的。如果团队不大,这个纪律相对还好执行,提交之前看一眼,或是喊一声就可以了。
如果团队稍微有一点规模,可以用一个小东西当作令牌,谁拿到了谁来提交。如果真的有人在 CI 服务器还在运行的时候,提交了代码怎么办?很简单,谁提交谁负责,错了就他修,谁让他违反纪律了。
好,你已经理解了我说的重点:不能把检查只放到 CI 服务器上执行。那该怎么做呢?答案已经呼之欲出了,那就是在本地开发环境上执行。
想做好持续集成的一个关键点是用好本地构建脚本build script保证各种各样的检查都可以在本地环境执行。
一旦有了构建脚本,你在 CI 服务器上的动作也简单了,就是调用这个脚本。也就是说,本地检查和 CI 服务器上的动作是一致的。
至于什么样的内容适合放在构建脚本里,这个话题我们先放一放,把它留到后续“自动化”模块再做讨论。
在“任务分解”模块中,我与你讨论了“小”动作在工作中的重要性,“小”动作完成得越快,工作反馈得到也越快,所以说,也只有坚持不懈地做“小”动作,才能缩短反馈周期。
现在我们把这个道理与持续集成结合起来理解,我们的工作流程就变成了这样:
每完成一个任务,在本地运行构建脚本,如果有问题,就修复;没问题,则可以同步代码。如果 CI 服务器上没有正在运行的服务,就可以提交代码了。
提交代码中最麻烦的动作,其实是合并代码。不过,因为我们做的是小任务,改动的代码量并不大,所以,即便有需要合并的代码,量也不会很大,所需的脑力以及工作时间都会少很多。如此一来,我们的开发效率才可能能真正得到提高。
当团队真正地实施起持续集成,你会发现随着时间增加,本地检查的时间会越来越长。原因有很多,比如,代码越来越多,测试也越来越多。总之,检查的时间长了,就会对集成的速度造成影响。
这个时候,本着快速反馈的理念,我们就必须想办法。比如,有的团队做了分布式测试运行,有的团队将测试分类,就是我们在测试金字塔中讲到的分类,在本地执行单元测试和集成测试,而把更复杂的系统测试放到 CI 服务器上运行。
简单来说,我们的目的就是快速地得到反馈。
得到有效的反馈
说完了“快速”,我们再来看看做好持续集成的第二个重点:反馈,也就是怎么得到有效的反馈。
为什么需要反馈,道理很简单,我们得知道自己做得对不对。你可能会问,根据前面的说法,如果本地和 CI 服务器上执行的是一样的脚本,我在本地通过了,还用关心 CI 服务器的反馈吗?
当然要。因为还会出现很多其他问题,比如说最简单的一种情况是,你漏提交了一个文件。
既然我们要关注CI 服务器的反馈,下一个问题就是,它怎么反馈给我们呢?
我们还是从一种常见的错误入手。有些团队做持续集成用的反馈方式是什么呢?答案是邮件。
以邮件进行反馈,问题出在哪里呢?很明显,邮件不是一种即时反馈的工具。
我不知道有多少人会把邮件客户端当作日常的工具,就我个人习惯而言,一天查看几次邮件就算不错了,如果以邮件作为反馈方式,很有可能是出错了很长时间,我都无知无觉。
我们前面一直在强调快速,需要的是即时反馈,一旦邮件成了持续集成链条中的一环,无论如何都快不起来。
那你可以怎么做呢?在前面各种讨论中,我其实已经透露了答案:持续集成监视器,也是 CI 监视器。
-
图片来源CI 监视器的示例 projectmonitor
CI 监视器的原理很简单CI 服务器在构建完之后会把结果以API的方式暴露出来早期有RSS和ATOM格式后来有JSON的格式。得到的结果就可以用不同的方式进行展现了。市面上有很多CI 监视器的软件,有的是拿到结果之后,做一个视觉呈现,有的是做桌面通知。
现在,我们终于要讲到这个部分的重点了:怎么呈现是有效的?
答案很简单:怎么引人注目,怎么呈现。
比如,很多团队的做法是,用一个大屏幕将持续集成的结果展示出来,这样一来,持续集成的结果所有人都能看到,一旦出错了,即便你一时疏忽,也会有人来提醒你。
还有一些感官刺激的做法,比如,有人用上了红绿灯,测试失败则红灯闪烁;还有人甚至配上了语音,用喇叭高喊:“测试失败了,请赶紧修复。”我在一个视频里见过一个更夸张的做法:有人用玩具枪,出错了,就瞄准提交者开上一枪。
你是聪明的程序员,你应该能想到更多有趣的玩法。
为什么要这么做呢?这里的重点是,想做好持续集成,需要整个团队都关注持续集成。
这些引人注目的做法,就是要提高持续集成的关注度。否则,即便持续集成的技术环节做得再出色,人的注意力不在,持续集成也很难起到作用。
所以,你看到了,持续集成的反馈,尤其是出错之后的反馈方式,几乎是所有实践中最为高调的,它的目的就是要引人注目。
这里再插播一条持续集成的纪律CI 服务器一旦检查出错,要立即修复。原因很简单,你不修,别人就不能提交,很多人的工作就会因此停顿下来,团队的工作流就会被打断,耽误的是整个团队的工作。
如果你一时半会修不好怎么办,撤销你的提交。更关键的原因是,团队对于持续集成的重视度,长时间不修复,持续集成就失去了意义,人们就会放弃它,持续集成在你的项目中,也就发挥不出任何作用了。
总结时刻
持续集成是软件开发中的重要实践,做好持续集成的关键在于,快速反馈。这里面有两个目标,怎样快速地得到反馈,以及什么样的反馈是有效的。
做好快速反馈,要把本地能做好的事情,在本地做好;也要通过小步提交的方式,加快代码开发的节奏。什么是有效的反馈?一是即时的反馈,二是引人注目的反馈。有很多种持续集成相关的工具可以帮助我们达成有效的反馈。
想要做好持续集成,还要有一些纪律要遵循:
只有 CI 服务器处于绿色的状态才能提交代码;
CI 服务器一旦检查出错,要立即修复。
如果今天的内容你只能记住一件事,那请记住:做好持续集成的关键在于,快速反馈。
最后,我想请你分享一下,你的团队做持续集成吗?遇到过哪些困难呢?欢迎留言与我们分享。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
25 开发中的问题一再出现,应该怎么办?
你好,我是郑晔。
看过《圣斗士星矢》的同学大多会对其中的一个说法印象颇深:圣斗士不会被同样的招数击败两次。
我们多希望自己的研发水平也和圣斗士一样强大,可现实却总不遂人愿:同样的线上故障反复出现,类似的 Bug 在不同的地方一再地惹祸,能力强的同学每天就在“灭火”中消耗人生。我们难道就不能稍微有所改善吗?
如果在开发过程中,同样的问题反复出现,说明你的团队没有做好复盘。
什么是复盘?
复盘,原本是一个围棋术语,就是对弈者下完一盘棋之后,重新把对弈过程摆一遍,看看哪些地方下得好,哪些下得不好,哪些地方可以有不同甚至是更好的下法等等。
这种把过程还原,进行研讨与分析的方式,就是复盘。
现如今,复盘的概念已经被人用到了很多方面,比如,股市的复盘、企业管理的复盘,它也成为了许多人最重要的工具,帮助个体和企业不断地提升。这其中最有名的当属联想的创始人柳传志老爷子,他甚至把“复盘”写到了联想的核心价值观里。
为什么复盘这么好用呢?在我看来有一个重要的原因,在于客体化。
俗话说,当局者迷,旁观者清。以我们的软件开发作为例子,在解决问题的时候,我们的注意力更多是在解决问题本身上,而很少会想这个问题是怎么引起的。
当你复盘时,你会站在另外一个视角,去思考引起这个问题的原因。这个时候,你不再是当事者,而变成了旁观者。你观察原来那件事的发生过程,就好像是别人在做的一样。你由一个主观的视角,变成了一个客观的视角。
用别人的视角看问题,这就是客体化。
在软件开发领域,复盘也是一个重要的做法,用来解决开头提到那些反复出现的问题,只不过,它会以不同的方式呈现出来。
回顾会议
回顾会议是一个常见的复盘实践,定期回顾是一个团队自我改善的前提。回顾会议怎么开呢?我给你分享我通常的做法。
作为组织者,我会先在白板上给出一个主题分类。我常用的是分成三类:“做得好的、做得欠佳的、问题或建议”。
还有不同的主题分类方式,比如海星图,分成了五大类:“继续保持、开始做、停止做、多做一些、少做一些”五类。
分类方式可以根据自己团队的喜好进行选择。我之所以选用了三类的分类方式,因为它简单直观,几乎不需要对各个分类进行更多的解释。
然后,我会给与会者五分钟时间,针对这个开发周期内团队的表现,按照分类在便签上写下一些事实。比如,你认为做得好的是按时交付了,做得不好的是 Bug 太多。
这里面有两个重点。一个是写事实,不要写感受。因为事实就是明摆在那里的东西,而感受无法衡量,你感觉好的东西,也许别人感觉很糟糕。
另外,每张便签只写一条,因为后面我要对便签归类。因为大家是分头写的,有可能很多内容是重复的,所以,要进行归类。
五分钟之后,我会号召大家把自己写的便签贴到白板上。等大家把便签都贴好了,我会一张一张地念过去。
这样做是为了让大家了解一下其他人都写了些什么,知道不同人的关注点是什么。一旦有哪一项不清楚,我会请这张便签的作者出来解释一下,保证大家对这个问题的理解是一致的。在念便签的同时,我就顺便完成了便签归类的工作。
等到所有的便签都归好类,这就会成为后续讨论的主题,与会者也对于大家的关注点和看到的问题有了整体的了解。
做得好的部分,是大家值得自我鼓励的部分,需要继续保持。而我们开回顾会议的主要目的是改善和提升,所以,我们的重点在于解决做得不好的部分和有问题出现的地方。
在开始更有针对性的讨论之前,我会先让大家投个票,从这些分类中选出自己认为最重要的几项。我通常是给每人三票,投给自己认为重要的主题。每个人需要在诸多内容中做出取舍,你如果认为哪一项极其重要,可以把所有的票都投给这个主题。
根据大家的投票结果,我就会对所有的主题排出一个顺序来,而这就是我们要讨论的顺序。我们不会无限制的开会,所以,通常来说,只有最重要的几个主题才会得到讨论。
无论是个人选择希望讨论的主题,还是团队选择最终讨论的主题,所有人都要有“优先级”的概念在心里。然后,我们就会根据主题的顺序,一个一个地进行讨论。
讨论一个具体的主题时,我们会先关注现状。我会先让写下反馈意见的人稍微详细地介绍他看到的现象。比如,测试人员会说,最近的 Bug 比较多相比于上一个开发周期Bug 增加了50%。
然后,我会让大家分析造成这个现象的原因。比如,有人会说,最近的任务量很重,没有时间写测试。
再下来,我们会尝试着找到一个解决方案,给出行动项。比如,任务重,我们可以让项目经理更有效地控制一下需求的输入,再把非必要的需求减少一下;测试被忽略了,我们考虑把测试覆盖率加入构建脚本,当测试覆盖率不足时,就不允许提交代码。
请注意,所有给出的行动项应该都是可检查的,而不是一些无法验证的内容。比如,如果行动项是让每个程序员都“更仔细一些”,这是做不到的。因为“仔细”这件事很主观,你说程序员不仔细,程序员说我仔细了,这就是扯皮的开始。
而我们上面给出的行动项就是可检查的,项目经理控制输入的需求,我们可以用工作量衡量,还记得我们在讨论用户故事中提到的工作量评估的方式吗?
控制工作量怎么衡量?就是看每个阶段开发的总点数是不是比上一个阶段少了。而测试覆盖率更直接,直接写到构建脚本中,跑不过,不允许提交代码。
好,列好了一个个的行动项,接下来就是找责任人了,责任人要对行动项负责。
比如,项目经理负责需求控制,技术负责人负责将覆盖率加入构建脚本。有了责任人,我们就可以保障这个任务不是一个无头公案。下一次做回顾的时候,我们就可以拿着一个个的检查项询问负责人任务的完成情况了。
5个为什么
无论你是否采取回顾会议的方式进行复盘,分析问题,找到根因都是重要的一环。
你的团队如果能一下洞见到根因固然好如果不能那么最好多问一些为什么。具体怎么问有一个常见的做法是5个为什么5 Whys。这种做法是丰田集团的创始人丰田佐吉提出的后来随着丰田生产方式而广为人知。
为什么要多问几个为什么?因为初始的提问,你能得到的只是表面原因,只有多问几个为什么,你才有可能找到根本原因。
我给你举个例子。服务器经常返回504那我们可以采用“5个为什么”的方式来问一下。
为什么会出现504呢因为服务器处理时间比较长超时了。
为什么会超时呢?因为服务器查询后面的 Redis 卡住了。
为什么访问 Redis 会卡住呢?因为另外一个更新 Redis 的服务删除了大批量的数据,然后,重新插入,服务器阻塞了。
为什么它要大批量的删除数据重新插入呢?因为更新算法设计得不合理。
为什么一个设计得不合理的算法就能上线呢?因为这个设计没有按照流程进行评审。
问到这里,你就发现问题的根本原因了:设计没有经过评审。找到了问题的原因,解决之道自然就浮出水面了:一个核心算法一定要经过相关人员的评审。
当然,这只是一个例子。有时候,这个答案还不足以解决问题,我们还可以继续追问下去,比如,为什么没有按流程评审等等。
所以“5个为什么”中的“5”只是一个参考数字不是目标。
“5个为什么”是一个简单易上手的工具你可能听了名字就知道该怎么用它。有一点需要注意的是问题是顺着一条主线追问不能问5个无关的问题。
无论是“回顾会议”也好“5个为什么”也罢其中最需要注意的点在于不要用这些方法责备某个人。我们的目标是想要解决问题不断地改进而不是针对某个人发起情感批判。
总结时刻
在软件研发中,许多问题是反复出现的,很多开发团队会因此陷入无限“救火”中,解决这种问题一个好的办法就是复盘。
复盘,就是过程还原,进行研讨与分析,找到自我改进方法的一个方式。这种方式使我们拥有了客体化的视角,能够更客观地看待曾经发生过的一切。这种方法在很多领域中都得到了广泛的应用,比如股市和企业管理。
在软件开发中,也有一些复盘的实践。我给你详细介绍了“回顾会议”这种形式。
无论哪种做法分析问题找到根因是一个重要的环节。“5个为什么”就是一个常用的找到根因的方式。
如果今天的内容你只能记住一件事,那请记住:定期复盘,找准问题根因,不断改善。
最后我想请你分享一下,你的团队是怎么解决这些反复出现的问题呢?欢迎在留言区写下你的做法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,135 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
28 结构化:写文档也是一种学习方式
你好,我是郑晔。
你写文档吗?我知道,你可能并不喜欢写文档,因为在你眼中,写文档是繁琐的,是旧时代软件工程的产物。
最开始我对写文档的印象也不好。
我的职业生涯是从一个通过了 CMM 5级认证的大企业开始的。可能今天很多程序员已经对 CMM 感到陌生了它是能力成熟度模型Capability Maturity Model for Software的缩写用来评估一个组织的软件开发能力曾在国内风靡一时许多软件公司都以拥有 CMM 认证为努力方向。
在这个极其重视过程的企业里,文档是非常重要的一环。但我看到的真实场景却是,一个软件已经上线运行了,大家才开始为了应付过程纷纷补写文档。
每个部门都有专门的过程负责人,要求你严格按照格式写文档,保证字体字号的正确性。然后,用 A4纸将文档打印出封印在一个仓库里再也无人问津。
然而,文档却是非常重要的。后来,我到过很多公司,凡是我能够比较快上手的,通常都是有比较详尽的文档,而那些文档缺失的公司,想要把信息梳理清楚,往往会花很长时间。
另外,我学习很多软件开发的相关知识,通常也是依赖各种各样的文档。对我们程序员这个走在时代前列的群体来说,大量阅读文档就是我们日常工作的一部分。
你发现矛盾了吗?一方面,我们讨厌写文档,另一方面,文档却对我们的工作学习有着不可忽视的作用。
我们竟然如此依赖于一个我们讨厌的东西。问题出在哪呢?
你为什么不喜欢写文档?
很多人会说,自己不愿意写那些无聊的流程文档,文档无聊,这固然是一个原因。不过,如今很多公司已经在这方面做得相当轻量级了,基本上只要求写必要的文档。那为什么依然有很多人不愿意写文档呢?
其实,很多人回避写文档的真正原因是,他掌握的内容不能很好地结构化。
在两种场景下,我们扮演的角色是不同的。写文档时,角色是作者;而读文档时,角色是读者。
作为读者,我们读文档,实际上就是按照作者梳理的结构在走,因为呈现出来的内容,多数是已经结构化的,读起来自然会比较顺畅;而作为作者,没有人告诉你结构应该是什么样,我们必须创造出一个结构来,而这正是很多人不擅长的。
想要成为一个好程序员,有一个良好的知识结构是极其重要的。
很多人抱怨程序员行业难,原因就在于,新技术层出不穷。是的,当你的知识都是零散的,任何新技术的出现,都是新东西。而当你建立起自己的知识结构,任何新东西都只是在原有知识上的增量叠加。
举个例子,今天炒得沸沸扬扬的微服务,小粒度的理念脱胎于 Unix 哲学中的“只做一件事把它做好”而服务化的理念则是当年SOAService-Oriented Architecture的产物。理解了这些背后的动机微服务就只剩下工具层面的问题。
有了这样的知识结构,当我要构建应用时,只是需要把工具适配进去,到时我再来学习相应的知识,这是非常有针对性的,学习的效率也会得到大幅度提高。
将零散的知识结构化,有很多种方式,但输出是非常关键的一环。
知识输出
不知道你小时候是不是有过给同学讲题的经历,有时候,明明你已经将知识学得很好,但给同学讲解起来时,却总是讲不明白。因为你的同学总能从你想都没想过的角度问问题,这些角度和老师教的不一样。
输出的过程,本质上就是把知识连接起来的过程。自己以为自己懂的东西,当你真的需要把它按照一个完整的逻辑呈现出来时,那些缺失的细节就会冒出来,而补齐这些细节,一张知识地图就逐渐成型了。
这个模块的主题是“沟通反馈”,将知识对外输出就是一种获得反馈的方式。很多人自以为对知识的理解已经很深入了,但给别人一讲,却发现自己怎么也讲不清楚,这就说明他理解的程度,远未到达他以为的高度。
输出的方式有很多,对于程序员来说,最常接触到的两种应该是写作与演讲。
你读到很多书、很多技术文章,这都是别人通过写作的方式进行输出的结果。而很多技术大会上,常常会有各路高手在台上分享自己的所得,这就是演讲的输出方式。
软件行业的很多大师级程序员都是对外输出的高手。比如,开源概念的提出者 Eric Raymond他的《大教堂与集市》推开了开源大门前面多次提及的Kent Beck他写了《极限编程解析》、《测试驱动开发》、《实现模式》几本书
而 Martin Fowler几乎是对外输出的典范他重新整理了很多似是而非的概念让人们的讨论有了更标准的词汇比如重构、依赖注入Dependency Injection等等。
再往前,就要提到《计算机程序设计艺术》的作者高德纳,他系统地整理了算法的概念,为了好好写作,他甚至创造了一个排版软件 TeX。
也许你会说,说得很有道理,但我真的不擅长啊!这是因为你没有掌握基本的方法。
金字塔原理
首先,需要明确一点,我们的第一目标不是成为作家或演讲家,而只是要求把事情说清楚,把自己的知识清晰地呈现出来。那我们最好先来了解一下金字塔原理。看看下面这张图,你就知道它是怎么回事了:
首先,我们要确定想要表达的是什么,也就是找到中心论点,然后,再确定支撑这个论点的分论点,再来就是找到支撑每个分论点的论据。
从中心论点、分论点至论据,这样一层层向下展开,从结构上看,就像金字塔一样,所以,这个方法称之为金字塔原理。
以我们的专栏为例,我们的中心论点就是“高效工作是有方法可循的”,那支撑起这个中心论点的分论点就是我们的四个原则,针对每个原则,我们给出了各种实践和思想,这是我们的论据。
前面我说过了,一个人不擅长输出,更多的是因为缺乏知识的结构化,现在通过这样一种方式,就可以帮助自己,将某个知识结构化起来,有了结构,剩下的就是怎么输出了。
具体怎么输出就可以根据自己的喜好进行选择要么自上而下的进行表达也就是先说中心论点然后说分论点1用论据证明分论点1再说分论点2用论据证明分论点2以此类推。
或者是自下而上来表达先用证据得出分论点1然后再得出分论点2最后再归纳总结出中心论点。
听上去很简单,但不要以为懂得了金字塔原理,天下就尽在掌握了,你还需要更多的练习。
无他,唯手熟尔
我自己也曾经很不擅长写作和公开演讲,但是,这些东西都禁不住你大量的练习。我的对外输出,是从我刚开始工作不久开始的。那时候,市面上流行写 blog我抱着好奇的心态开始了自己的 blog 之旅。
刚开始写 blog 的时候,我会把写好的东西分享给周边的朋友,他们会给我提出一些反馈,有赞许、有调侃、也有针对一些细节的讨论,这会让我觉得自己写的东西是有人看的,我也就有了坚持的原动力。
我也很羡慕那些很会写的人,于是,也经常会模仿他人的手法不断地改进自己的写作技巧。慢慢地,我的读者就从身边的人逐渐扩展开来,我也就有了更多的反馈。
正是这些反馈,让我对很多东西有了全新的认识,也就有了更强的分享动力,一个正向循环逐渐建立起来。到后来,写东西就成了我的习惯,坚持至今。
经过 blog 写作的锻炼,我写的东西有了自己的章法和套路,也就有了越来越多机会去在不同的地方写东西:给杂志写稿子,在网站上写东西,包括今天这个专栏,都起源于最初的 blog 写作。
除此之外,随着时间的累积,我收获的不仅仅是一些读者的赞许,还得到了更多的机会,比如,我人生中的第一次公开演讲,机会就来自于我 blog 的一个读者的邀请。
后来的一些职业机会,也是通过我写 blog 认识的朋友。考虑到我当时人在 IT 边缘的东北,能有后来的职业发展,很大程度都是常年坚持对外输出的结果。
同样演讲能力也需要大量的练习。1977年《Book of List》杂志曾经有一个关于“最恐惧事物”的调查结果显示公开演讲名列第一超过了死亡。所以你害怕公开演讲是很正常的。
我至今依然记得我第一次公开演讲时手抖的样子,今天想想还是挺傻的。我第一次在几百人的大会上做演讲,居然有一段时间,只顾着看大屏,背对着听众,也是很糗的一段经历。
我一直很羡慕那些在台上侃侃而谈的人,比如,乔布斯。直到我读了《乔布斯的魔力演讲》,我才知道,即便强如乔布斯,他的演讲也是经过大量练习的。
我自己公开演讲看上去正常一些,是我在经过一个咨询项目的大量练习之后。那时候,几乎每天要给客户讲东西,害得我只能不停地准备、不停地讲。所以,本质上,对演讲的惧怕只是因为练习不足。
好了,你现在已经了解获取这些技能的真谛了,无他,唯手熟尔!
总结时刻
程序员对文档有着一种矛盾的情感,一方面,需要依赖于文档获得知识,另一方面,很少有人愿意写文档。
文档在程序员心目中“形象不佳”,主要是传统的流程写了太多无用的文档。但对更多人来说,不愿意写文档,本质上是因为知识不能很好地结构化。
有结构的知识会让新知识的学习变得更加容易,今天很多人抱怨新知识层出不穷,就是因为知识过于零散,当知识有结构之后,学习新知识就只是在学习增量,效率自然就会大幅度提升。
输出是一种很好的方式,帮助你把知识连接起来,写作和做公开演讲都是很好的输出方式。
阻碍很多人进行知识输出的一个重要原因是缺乏输出的模型,金字塔原理就给出一个从中心论点到分论点,再到论据的模型,帮助我们将知识梳理出来。
而想要做好知识输出,还需要不断地进行练习,写作和做公开演讲都是可以通过练习提高的。
如果今天的内容你只能记住一件事,那请记住:多输出,让知识更有结构。
最后,我想请你分享一下,你的工作中,有哪些机会将自己的知识输出呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,119 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 “懒惰”应该是所有程序员的骄傲
你好,我是郑晔。
经过前面几个模块的学习,我们的专栏终于进入到程序员看上去最熟悉的一个主题:自动化。
每每提及自动化,我就会想起 Perl 语言的发明人 Larry Wall 一个经典叙述优秀程序员应该有三大美德懒惰、急躁和傲慢Laziness, Impatience and hubris
有人甚至为此专门打造了一个三大美德的网站,阐释这个初看起来匪夷所思的说法。
懒惰,是一种品质,它会使你花很大力气去规避过度的精力消耗,敦促你写出节省体力的程序,别人也能很好地利用,你还会为此写出完善的文档,以免别人来问问题。
急躁,是计算机偷懒时,你会感到的一种愤怒。它会促使你写出超越预期的程序,而不只是响应需求。
傲慢,极度自信,写出(或维护)别人挑不出毛病的程序。
不知道你是否感受到,程序员独有的幽默和透露出的那种骄傲:我做的东西就应该是最好的。
之所以要从 Larry Wall 的这段话开启“自动化”这个模块,因为只要一说到自动化,我就会情不自禁地联想到“偷懒”这个词。是的,我们程序员的工作,本质上就是打造各种自动化的工具,让人们从各种繁复的工作中解脱出来,让人有机会“偷懒”。
不过,我也知道,从机器那里偷来的“懒”很快就被更多的工作填满了。但 Larry Wall 的这段话却可以鼓励我们不断地打造出更好的工具。
作为程序员,你当然知道“自动化”这件事的价值,在日常工作中,也实实在在地践行着打造自动化工具的任务,但很多人对自动化的理解可能有些单薄。今天,我就从一个你可能会忽略的主题开始讨论:不要自动化。
不要自动化
我先给你讲一个让我印象深刻的“不自动化”的例子。
之前在 ThoughtWorks 工作时,我们有一项工作是,帮助其他公司启动一些新产品。有一次,我的两个同事被一个公司请去启动一个视频网站的项目。那时候还不像如今的市场,已经由几大视频网站瓜分完毕,当时不少公司看到了视频网站的苗头,觉得自己有机会。这个来请我们的公司也不例外,觉得自己也能分一杯羹。
两个星期之后,我的两个同事回来了。我们饶有兴趣地去问项目的进展,因为项目启动之后,通常会有后续的开发合作,但结果令我们很意外,这个项目停止了。
“出了什么状况吗?”我们问。
“是我们建议用户停掉这个项目的。”他们回答道。
我们“恨恨地”问他们为什么丢掉了一个这么重要的机会。这两个同事的回答也很直白,他们结合着客户的想法算了一笔账:这个项目需要大量的资金投入,投入规模之大,是超出客户想象的,按照现有的规划投入,这个项目肯定会亏本。要么重新规划,要么取消这个项目。客户认真研究了一番,最终决定取消项目。
这件事大约发生在10年前今天我们都看到各大视频网站在烧钱上的投入以那个公司的实力想要参加这场比拼确实还差太多。
这件事之所以给我留下深刻印象,因为它是我职业生涯中见到的第一个通过“主动取消项目”获取项目成功的案例。
或许你不能理解我这里所说的“项目成功”。在我看来,做有价值的事是重要的,这里面的有价值,不仅仅是“做”了什么,通过“不做”节省时间和成本也是有价值的。我的两个同事阻止了客户的浪费,所以,我将这个项目视为成功。
对于开发来说,也遵循同样的道理。程序员这个群体技术能力实在太强,做一个技术方案简直是太符合直觉的做法,我们就是忠实地把一个个需求做出来,把“全世界”都自动化了。
但事实上,这个世界太多的浪费就是做了不该做的东西。在我们的专栏里,我反复地说,我们要多问问题,目的就是为了不做那些不该做的事。
小心 NIH 综合症
你可以从需求的角度判断哪些工作是可以不做的但我们也要防止程序员自己“加戏”我再给你讲一个技术人员普遍存在的问题NIH 综合症Not Invented Here Syndrome
NIH 是什么意思?就是有人特别看不上别人做的东西,非要自己做出一套来,原因只是因为那个东西不是我做的,可能存在各种问题。
这种现象在开源之前尤为流行,很多公司都要做自己的中间件,做自己的数据库封装。虽然很多公司因此有了自己特色的框架,但是因为水平有限,做出来的东西通常极为难用,很多人一边骂,一边还要继续在上面开发。
开源运动兴起之后,我以为这种现象会好一些,但事实证明,我想多了。
比如,这种乱象在前端领域也出现了,各种各样的框架,让很多前端程序员哭诉,实在学不动了。再比如,我曾经面试过一个接触 Go 比较早的程序员,他就是恨不得把所有框架都自己写。
因为他学 Go 的时候,确实框架比较少,但问题是,如今的 Go 已经不是他学习时的那个 Go 了,现在各种框架已经很丰富了,不需要什么都自己做。当时我问他,如果有一天你离开了,公司怎么办呢?实际上,他从来没考虑过这个问题。
说了这么多,无非就是想说明一件事,写代码之前,先问问自己真的要做吗?能不做就不做,直到你有了足够的理由去做。对应到 Larry Wall 的说法,你要懒惰,花大力气去规避精力消耗。
做好自动化
说完了不要自动化的部分,再来说说要自动化的部分。
我还是先从你可能会忽略的问题入手,你的日常工作是给别人打造自动化,但你自己的工作够自动化吗?还是问一个更具体的问题吧!如果你写的代码要上线,会经过怎样的过程?
我先给你看一个极其糟糕的例子。刚开始工作不久我有一次出差到客户现场。临近下班时我发现了程序的一个Bug。在那个年代我们的程序是按照官方推荐做法编写的 EJBEnterprise JavaBean今天很多年轻的程序员可能不了解了它只有部署到应用服务器才能运行。
我的解决方案就是加上一些打印语句,然后部署到应用服务器上,看输出的结果,再加上另外一些语句,再部署,如此往复。那时我们完全是手工打包上传,每次至少要十几分钟。最终,定位到了问题,只修改了一行代码。但几个小时的时间就这样被无谓的消耗了。
那之后,我花了很长时间研究怎么做自动化的增量部署,最终让这个过程简化了下来。但这件事对我的影响很大,这是我第一次认识到一个部署过程可能对开发造成的影响,也让我对自动化在开发过程内的应用有了属于自己的认识。
相比于我刚开始工作那会。现在在工具层面做类似的事已经容易很多了,在后面的内容中,我会结合着具体的场景介绍一下现在的最佳实践。
你要懂得软件设计
最后,我们再来说说我们的本职工作,给别人打造自动化工具中需要的能力:软件设计。
软件设计,是很多人既熟悉又陌生的一个词,说熟悉,很多人都知道,做软件要设计,还能顺嘴说出几个设计模式的名字;说陌生,是因为在我的职业生涯中,遇到真正懂软件设计的程序员少之又少。大多数人都是混淆了设计和实现。
举个例子。有一次,我要在两个系统之间做一个连接器,让上游系统向下游系统发消息,或许你一听就知道了,这里需要的是一个消息队列。但实际上,我们需要的能力要比消息队列更丰富一些,比如,要将重复的消息去除。一个同事给我推荐了 Kafka 当作这个连接器的基础,我欣然地接受了。
不过,在后续设计的讨论中,我们就经常出现话语体系的分歧。我说,这个连接器要有怎样的能力,他会说 Kafka 能够如何如何。究其根因,我在讨论的是设计,而他说的是实现,所以,我们两个很难把问题讨论到一起。
为什么我会如此看重设计呢?在软件开发中,其它的东西都是易变的,唯有设计的可变性是你可以控制的。
同样以前面的讨论为例,尽管 Kafka 在当下比较火热,但是我不敢保证 Kafka 在未来不会被我换掉。因为就在几年前,消息队列还是传统中间件的强项,现在也渐渐被人淡忘了。
我不想让我的设计随着某一个技术选型而不断摇摆。如果工作许多年,知识体系只能靠各种新框架新工具支撑,我们做程序员就只剩下疲于奔命了。不懂软件设计,只专注各种工具,其结果一定是被新技术遗弃,这也是很多人经常抱怨 IT 行业变化快的重要原因。
回到 Larry Wall 的说法上,你要想写出一个别人挑不出毛病的程序,你先要懂得软件设计。幸运的是,软件设计这些年的变化真不大,掌握了软件设计再来看很多框架和工具,学习起来就会容易很多。在这个模块的后半部分,我会与你探讨软件设计的话题,降低自己给自己挖坑的概率。
总结时刻
Perl 语言的发明人 Larry Wall 曾经说过优秀程序员应该有三大美德懒惰、急躁和傲慢Laziness, Impatience and hubris。想要成为一个优秀的程序员就要让机器为自己很好地工作而这需要对自动化有着很好地理解。
我们学习自动化,先要知道哪些东西不要自动化,尽最大的努力不做浪费时间的事。一方面,我们要从需求上规避那些没必要做的事;另一方面,我们也从自身防止 NIH 综合症Not Invented Here Syndrome争取做一个懒惰的程序员。
对于要自动化的事,我们需要反思一下,在为别人打造自动化工具的同时,我们自己的工作过程有没有很好地自动化。而如果我们想拥有打造良好的自动化工具,我们需要对软件设计有着充分地理解。
如果今天的内容你只能记住一件事,那请记住:请谨慎地将工作自动化。
最后,我想请你分享一下,学习了本讲之后,你现在是怎样理解自动化的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,259 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 一个好的项目自动化应该是什么样子的?
你好,我是郑晔。
进入自动化这个模块我准备从程序员的日常工作开始。介绍“迭代0”时我提到构建脚本是项目准备的一个重要组成部分但在那一讲中我并没有具体说构建脚本长成什么样。
今天,我们以一个典型的 Java REST 服务为例,介绍一下最基本的构建脚本应该做到什么样子。这里我采用的 Java 技术中最为常见的 Spring Boot 作为基础框架,而构建工具,我选择了 Gradle。
估计很多 Java 程序员心中的第一个问题就是,为什么用 Gradle而不是 MavenMaven 明明是 Java 社区最经典的构建工具。答案是因为 Maven 不够灵活。
你可以回想一下,你有多少次用 Maven 实现过特定需求估计大部分人的答案都是没有。随着持续集成、持续交付的兴起构建脚本的订制能力会变得越来越重要Maven 则表现得力有不逮。
其实早在2012年ThoughtWorks 技术雷达就将 Maven 放到了 暂缓HOLD里面也就是说能不用就不用。
为了配合这次的讲解,我写了一个 Demo放在了 Github 上。它的功能非常简单:
通过向 /users POST 一个请求,实现用户注册;
访问 /users查看已注册的用户。
如果方便的话,你最好把这个项目 clone 下来,以便参考。这里我主要是讲解自动化要做成什么样子,如果你想了解具体是怎么实现的,可以参考 Demo 里的代码。
好,我们开始!
基础准备
先把这个项目从 Github 上 clone 下来。
git clone https://github.com/dreamhead/geektime-zero.git
然后,进入到项目所在的目录中。
cd geektime-zero
当你准备就绪,我们就开始进一步了解这个项目。
一般我们了解一个项目,都会用用一个 IDE 打开这个项目,这里我推荐使用 IntelliJ IDEA这是目前行业中最好的Java IDE。自从它的社区版免费之后它就成为了我向他人推荐的首选。
我知道,开发工具是除了程序设计语言之外,另外一个容易引起“宗教战争”的话题,如果你喜欢其他的 IDE那就用你最喜欢的 IDE 打开好了,只不过,需要调整一下构建脚本中的配置。
怎么打开这个项目呢?我们先用 Gradle 命令生成一个 IDEA 工程。
./gradlew idea
这个命令会生成一个.ipr 文件,这就是 IDEA 的工程文件,用 IDEA 打开即可。
这里有两点需要说明一下。
第一,这里用的 gradlew它是 Gradle 命令的一个封装它会自动下载一个构建这个项目所需的Gradle重点是通过这个命令锁定了 Gradle 的版本,避免因为构建脚本的差异,造成“你成功我失败”的情况。
第二IDE 的工程是由 Gradle 生成的。很多人会凭借直觉,用 IDE 直接打开。有一些团队的项目里有好多个构建文件,究竟用哪个打开,不去问人是根本不知道的,这对项目的新人是非常不友好的。
生成的做法与前面 Gradle 封装是类似的,它可以避免因为本地安装不同版本 IDE 造成各种问题。
另外,因为 IDE 的工程是生成的,如果项目里一旦增加了新的程序库依赖,你只需重新执行一次上面的命令就好了,现在的 IDE 都有很好的自动加载能力,当它检测到工程文件的变化,就会重新加载。
好,现在你可以用 IDE 打开,我们就可以进一步了解这个项目了。
初见项目
我们先来了解一点 Gradle 的配置文件,它也是我们做项目自动化的重点。
build.gradle它是 Gradle 的配置文件。因为 Gradle 是由 Groovy 编写而成build.gradle 本质上就是一个 Groovy 的脚本,其中的配置就是 Groovy 代码,这也是 Gradle 能够灵活订制的基础。
settings.gradle这也是一个 Gradle 配置文件,用以支持多模块。如果说一个项目的每个模块都可以有一个 build.gradle那整个项目只有一个 settings.gradle。
在 Gradle 里,许多能力都是以插件的形式提供的,比如,前面生成 IDEA 工程就是配置文件中的一句话。
apply plugin: 'idea'
所以,如果你是其他 IDE 的死忠粉,你可以把这句话,换成你喜欢的 IDE。
(注:这个项目采用 Lombok 简化代码,为了能让代码在你的 IntelliJ IDEA 编译运行,你可以安装 Lombok 插件,然后,在 “Build, Execution, Deployment”-> “Compiler” -> “Annotation Processors“”中选中 Enable annotation processing
好,有了基础知识之后,我们来了解一下代码组织。
首先是分模块。除非你的代码库规模非常小,否则,分模块几乎是一种必然。一种恰当的划分方式是根据业务划分代码。比如,把用户相关的内容放到一个模块里,把交易订单信息放到一个模块里,把物流信息放到另一个模块里。
如果你未来打算做微服务,那每一个模块就可以成为一个独立的服务。
在我们的项目里,我示例性地划分了两个模块:
zero-identity是用户信息的模块
zero-bootstrap是多个模块打包成一个可部署应用的模块。
这两个模块的信息都配置在 settings.gradle 中。
include 'zero-bootstrap'
include 'zero-identity'
再来是目录结构。具体要怎么样组织代码,在 Java 世界里已经是一件约定俗成的事情了。
src/main/java 下放着你的源代码src/main/resources 下放配置文件src/test/java 放测试代码。这是约定优于配置Convention over Configuration思想的体现。如果你用的工具没有约定你只能自己定好让其他人遵守。
检查
在自动化过程中,一个最基本的工作是检查。检查的工作在我们的项目中通过一个 check 任务来执行。
./gradlew check
这个检查会检查什么呢?这取决于配置。在这个项目里,我们应用了 Java 插件它就可以编译Java 文件,检查代码是否可以正常编译,运行测试,检查代码是否功能正常等等。但我要求更多。
讲“迭代0”时我说过最基本的代码风格检查要放在构建脚本中这里我用了 CheckStyle 来做这件事。缺省情况下,你只要应用 Checkstyle 插件即可。
apply plugin: 'checkstyle'
在这个项目里,我做了一些订制,比如,指定某些文件可以不做检查。
style.excludePackages = [
]
style.excludeClasses = [
]
测试覆盖率也应该加入到构建脚本中,这里我用了 JaCoCo。同样缺省情况下只要应用 JaCoCo 插件即可。
apply plugin: 'jacoco'
我依然是做了一些订制,比如,生成结果的 HTML 报表,还有可以忽略某些文件不做检查。
coverage.excludePackages = [
]
coverage.excludeClasses = [
]
这里最特别的地方是我将测试覆盖率固定在1.0也就是100%的测试覆盖。这是我做新项目的缺省配置,也是我对团队的要求。
如果一个新项目,能把这几个检查都通过,腐坏的速度应该就不会那么快了。当然,你也可以根据自己的需要,添加更多的检查。
数据库迁移
讲“迭代0”时我还提到了数据库迁移也就是怎样修改数据库。在示例项目中我选择的数据库迁移工具是-
Flyway。
plugins {
id "org.flywaydb.flyway" version "5.2.4"
}
下面先要做一些基本的配置,保证可以连接到数据库。(注:如果你想直接使用这里的配置,可以在本机的 MySQL 数据库上,创建一个 zero 的用户,密码是 geektime然后再创建一个 zero_test 的数据库。)
flyway {
url = 'jdbc:mysql://localhost:3306/zero_test?useUnicode=true&characterEncoding=utf-8&useSSL=false'
user = 'zero'
password = 'geektime'
locations = ["filesystem:$rootDir/gradle/config/migration"]
}
那修改数据库会怎么做呢先添加一个数据库迁移文件比如在示例项目中我创建一个迁移文件gradle/config/migration/V2019.02.15.07.43__Create_user_table.sql在其中创建了一个 User 表。
CREATE TABLE zero_users(
id bigint(20) not null AUTO_INCREMENT,
name varchar(100) not null unique,
password varchar(100) not null,
primary key(id)
);
这里的迁移文件版本,我选择了以时间戳的方式进行命名,还有一种方式是以版本号的方式,比如 V1、V2。
时间戳命名方式的好处是,不同的人可以同时开发,命名冲突的几率很小,而采用版本号命名的方式,命名冲突的概率会大一些。
添加好数据库迁移文件之后,只要执行下面这个命令就好:
./gradlew flywayMigrate
这样,对数据库的修改就在数据库里了,你可以打开数据库查看一下。
构建应用
做好了最基本的检查,数据库也准备就绪,接下来,我们就应该构建我们的应用了。
首先是生成构建产物,它只要一个命令。
./gradlew build
这个命令会在 zero-bootstrap/build/libs 下生成一个可执行 JAR 包它就是我们最终的构建产物。此外build 任务会依赖于 check 任务,也就是说,构建之前,会先对代码进行检查。
从前 Java 程序只是打出一个可部署的包,然后,部署到应用服务器上。感谢现在基础设施的进步,我们可以省去部署的环节,这个包本身就是一个可执行的。我们可以通过命令执行将 JAR 执行起来。
java -jar zero-bootstrap/build/libs/zero-bootstrap-*-boot.jar
在开发过程中,并不需要每次都将 JAR 包打出来,我们还可以直接通过 Gradle 命令将应用运行起来。
./gradlew bootRun
不过,我估计你更常用的方式是,在 IDE 中找到 Bootstrap 这个入口类,然后,直接运行它。
既然程序已经运行起来,我们不妨测试一下。我们通过一些工具,比如 Postman 或者 Curl把下面的内容 POST 到 http://localhost:8080/users
{
"username": "foo",
"password": "bar"
}
然后,通过浏览器访问 http://localhost:8080/users-
我们就可以看见我们刚刚注册的这个用户了。
总结时刻
总结一下今天的内容。今天我们通过一个具体的例子,展示了一个最基本的项目自动化过程,包括了:
生成 IDE 工程;
编译;
打包;
运行测试;
代码风格检查;
测试覆盖率;
数据库迁移;
运行应用。
但这就是自动化的全部了吗?显然不是,我这里给出的只是一个最基本的示例。实际上,几乎每个重复的工作或是繁琐的工作,都应该自动化。我们不应该把时间和精力浪费在那些机器可以很好地替我们完成的工作上。
今天的基础设施已经让我们的自动化工作变得比以往容易了很多,比如,可执行 JAR 包就比从前部署到应用服务器上简化太多了。Gradle 也让订制构建脚本的难度降低了很多。
这里提到的项目自动化也是持续集成的基础,在持续集成服务上执行的命令,就应该是我们在构建脚本中写好的,比如:
./gradlew build
2011年我在 InfoQ 上发表了一篇《软件开发地基》,讨论的就是一个项目的构建脚本应该是什么样子。虽然其中用到的工具今天已经不再流行,但一些基础内容今天看来,依然是有效的。如果有兴趣,你也可以看一下。
如果今天的内容你只能记住一件事,那请记住:将你的工作过程自动化。
最后,我想请你分享一下,在日常开发工作中,你还把哪些过程自动化了呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,109 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 程序员怎么学习运维知识?
你好,我是郑晔。
在上一讲中,我们讲到了开发过程的自动化,我们的关注点在于如何构建出一个有效的部署包,这个包最终是要上线部署的,那接下来,我们就来关心一下部署的相关工作。
零散的运维知识
在一些稍具规模的公司,为部署工作设置了一个专有职位,称之为运维。当然,这个岗位的职责远不止部署这一件事,还要维护线上系统的稳定。不过,如果你的团队规模不大,或是项目处于初始阶段,这些工作往往也要由程序员自行完成。
对于一个程序员来说,了解自己的程序怎么部署上线,是非常重要的。我们既要了解一个软件的逻辑,也要知道它的物理部署。只有这样,出了问题才知道怎么修复。
更重要的是,我们在设计时,才能尽量规避部署带来的问题。而部署,恰恰也是最适合发挥自动化本领的地方。
好,即便下定决心准备学习运维相关知识,你准备怎么学呢?我先来问你个问题,提到运维,你会想到什么?
如果你是一个刚刚步入这个行业的程序员,你或许会想到 Docker想到 Kubernetes如果再早一点入行你或许还会想到 Chef、Puppet、Ansible更早一些入行的话你会想到 Shell 脚本。没错,这些东西都是与运维相关的。那我就这么一个一个地都学一遍吗?
就我个人的学习经验而言,如果所有的知识都是零散的,没有一个体系将它们贯穿起来,你原有的知识无法帮助你学习新知识,这种学习方式效率极低,过程也极其痛苦。
如果是有结构的知识,所谓的学习新知识不过是在学习增量,真正要理解的新东西并不多,学习效率自然会大幅度提高。所以,想学好运维知识,首先你要建立起一个有效的知识体系。
你可能会问,这些运维知识看上去就是一个一个独立的工具啊?我曾经也为此困惑了许久,虽然我对各个工具已经有了不少的了解,但依然缺乏一个有效的知识体系,将它们贯穿起来,直到我上了一堂课。
感谢 Odd-e 的柴锋,有一次,他给我上了一堂 DevOps 课,他对运维知识的讲解让我茅塞顿开,从此,我的运维知识有了体系。
准确地说,他的这堂课就是讲给程序员的运维课。今天,我就把这个体系按照我的理解,重新整理一遍分享给你,也算是完成一次知识输出。
好,我们开始!
Java 知识体系
正如我前面所说,学习一个新东西,最好的办法是学习增量,如果能够找到它与已有知识体系的联系,我们就可以把已有知识的理解方式借鉴过去。
作为程序员,我们其实已经有了一个完善的知识体系,这就是我们对于程序设计的理解,而理解运维的知识体系,刚好可以借鉴这个体系。怎么理解这句话呢?
以最常见的 Java 开发为例,如果要成为一个合格的 Java 程序员,我应该知道些什么呢?
首先肯定是 Java 语言,我需要了解 Java 语言的各种语法特性。不过,只了解语法是写不出什么像样程序的,我们还需要掌握核心库。
对于 Java 来说,就是 JDK 中的各种类,比如,最常见的 String、List、Map 等等。
理论上来说掌握了基本的语法和核心库你就可以开发任何程序了。但在实践中为了避免重新发明“轮子”减少不必要的工作量我们还会用到大量的第三方类库比如Google Guava、SLF4J 等等。
除了功能实现,还有一些结构性的代码也会反复出现。比如说,在常见的 REST 服务中,我们要将数据库表和对象映射到一起,要将结果转换成 JSON要将系统各个组件组装到一起。
为了减少结构上的代码重复,于是,开发框架出现了,在 Java 中最常见的开发框架就是 Spring。
至此,你就可以完成基本的代码编写,但这还不够。
在 Java 中,你不会从底层完成所有事情,比如,虽然你写 REST 服务,但你很少会接触到最底层的 HTTP 实现,因为这些工作由运行时环境承担了。
我们要做的只是把打好的包部署到这些运行时环境上,在 Java 的世界里,这是 Tomcat、Jetty 之类的容器承担的职责。
如果你刚刚加入这一行,上来就用 Spring Boot 之类的框架写代码,你可能并没有碰到这样的部署过程,因为这些框架已经把容器封装其中,简化了部署过程。
Tomcat、Jetty 往往还只是在一台机器上部署,在现实的场景中,一台机器通常是不够用的,我们可能需要的是一个集群。
你可能会想到用 Nginx 来做一个负载均衡,但如果用原生的 Java 解决方案这时候就轮到企业级的应用服务器登场了比如IBM WebSphere、Oracle WebLogic Server、JBoss Enterprise Application Platform 等等。
至此,一套完整的 Java 应用解决方案已经部署起来了。但我们知道了这些,和我们运维知识有什么关系呢?我们可以用同样的体系去理解运维知识。
运维知识体系
首先,要理解运维体系的语言。运维的语言是什么呢?是 Shell人们最熟悉的应该是 Bash。我们通过操作系统与计算机打交道但我们无法直接使用操作系统内核Shell 为我们提供了一个接口,让我们可以访问操作系统内核提供的服务。
你可能会以为我这里用的是比喻,将 Shell 比喻成语言但还真不是Shell 本身就是一门编程语言。绝大多数人都知道 Shell 可以编程,但几乎没有人把 Shell 当成一门编程语言来学习,基本上都是在需要的时候,搜索一下,然后照猫画虎地将代码复制上去。
这样造成的结果就是,一旦写一个脚本,就要花费大量的时间与语法做斗争,只是为了它能够运行起来。
有了语言,再来就是核心库了。运维的核心库是什么?就是 Shell 提供的各种 Unix/Linux 的核心命令比如ls、cd、ps、grep、kill、cut、sort、uniq 等等,它们几乎与操作系统绑定在一起,随着操作系统一起发布。
了解了核心的部分还需要了解一些第三方库运维知识的第三方库就是那些不属于操作系统核心命令的命令比如rsync、curl 等等。
Java 有框架可用运维也有框架吗你可以想一下Java 的框架提供的是一些通用的能力,在运维工作中,也是有一些通用能力的,比如:在安装某个包之前,要检查一下这个包是否已经安装了;在启动一个服务前,要检查这个服务是否启动了,等等。所以,能够帮我们把这些工作做好的工具,就是我们的运维框架。
到这里,你应该已经明白了,我在说的运维框架其实就是像 Chef、Puppet、Ansible 之类的配置管理工具。它们做的事就是把那些繁琐的工作按照我们的定义帮我们做好。
有了对软件环境的基本配置接下来就要找一个运行时的环境将软件跑起来了。这时候我们要了解像虚拟机、Docker 之类的技术,它们帮我们解决的问题就是在单机上的部署。
一般来说,了解了这些内容,我们就可以构建出一个开发环境或测试环境。除非用户非常少,我们可以在生产环境考虑单机部署,否则,我们迄今为止讨论的各种技术还都是在开发环节的。
如果我们需要一个集群或是高可用环境我们还需要进一步了解其他技术这时候就轮到一些更复杂的技术登场了比如云技术Amazon AWS、OpenStack包括国内的阿里云。如果你采用的是 Docker 这样的基础技术,就需要 Kubernetes、Docker Swarm 之类的技术。
至此,一个相对完整的运维知识体系已经建立起来了,现在你有了一张知识地图,走在运维大陆上,应该不会轻易地迷失了。希望你可以拿着它,继续不断地开疆拓土。
总结时刻
我们今天的关注点在于,将开发过程产生的构建产物部署起来。部署过程要依赖于运维知识,每个程序员都应该学习运维知识,保证我们对软件的运行有更清楚地认识,而且部署工作是非常适合自动化的。
但是,对运维工具的学习是非常困难的,因为我们遇到的很多工具是非常零散的,缺乏体系。
这里,我给你介绍了一个运维的知识体系,这个体系借鉴自 Java 的知识体系,包括了编程语言、核心库、第三方库、开发框架、单机部署和集群部署等诸多方面。我把今天提到的各种技术整理成一个表格列在下面,你可以参考它更好地理解运维知识。
如果今天的内容你只能记住一件事,那请记住:有体系地学习运维知识。
最后,我想请你分享一下,你还能想到哪些运维知识可以放到这张知识地图上呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 你的代码是怎么变混乱的?
你好,我是郑晔。
前面几讲,我给你讲了开发过程的各种自动化,从构建、验证到上线部署,这些内容都是站在软件外部看的。从这一讲开始,我准备带领大家进入到软件内部。今天的话题就从写代码开始说起。
逐步腐化的代码
代码是程序员改造世界最直接的武器,却也是程序员抱怨最多的东西。为什么程序员会对代码如此不满呢?
你会抱怨写一段代码吗?你肯定不会,毕竟这是你养家糊口的本领,最基本的职业素养我们还是有的。那抱怨的是什么呢?是维护一段代码。
为什么维护代码那么难?因为通常来说,你维护的这段代码是有一定年龄的,所以,你总会抱怨前人没有好好写这段代码。
好,现在你拿到了一个新的需求,要在这段代码上添加一个新功能,你会怎么做呢?很多人的做法是,在原有的代码上添加一段新的逻辑,然后提交完工。
发现问题了吗?你只是低着头完成了一项任务,而代码却变得更糟糕了。如果我问你,你为什么这么做?你的答案可能是:“这段代码都这样了,我不敢乱改。”或者是:“之前就是这么写的,我只是遵循别人的风格在写。”
行业里有一个段子,对程序员最好的惩罚是让他维护自己三个月前写的代码。你一不小心就成了自己最讨厌的人。
从前,我也认为很多程序员是不负责任,一开始就没有把代码写好,后来,我才知道很多代码其实只是每次加一点。你要知道,一个产品一旦有了生命力,它就会长期存在下去,代码也就随着时间逐渐腐烂了。
而几乎每个程序员的理由都是一样的,他们也很委屈,因为他们只改了一点点。
这样的问题有解吗?一个解决方案自然就是我们前面说过的重构,但重构的前提是,你得知道代码驶向何方。对于这个问题,更好的答案是,你需要了解一些软件设计的知识。
SOLID 原则
提到软件设计,大部分程序员都知道一个说法“高内聚、低耦合”,但这个说法如同“期待世界和平”一样,虽然没错,但并不能很好地指导我们的具体工作。
人们尝试着用各种方法拆解这个高远的目标,而比较能落地的一种做法就是 Robert Martin 提出的面向对象设计原则SOLID这其实是五个设计原则的缩写分别是
单一职责原则Single responsibility principleSRP
开放封闭原则Openclosed principleOCP
Liskov 替换原则Liskov substitution principleLSP
接口隔离原则Interface segregation principleISP
依赖倒置原则Dependency inversion principleDIP
早在1995年Robert Martin 就提出了这些设计原则的雏形,然后在他的《敏捷软件开发:原则、实践与模式》这本书中,比较完整地阐述了这五个原则。后来,他有把这些原则进一步整理,成了今天的 “SOLID”。
学习这些设计原则有什么用呢?
今天的程序员学习软件设计多半是从设计模式入门的,但不知道你是否有这样的感觉,在学习设计模式的时候,有几个设计模式看上去如此相像,如果不是精心比较,你很难记得住它们之间的细微差别。
而且,真正到了工作中,你还能想得起来的可能就剩下几个最简单的模式了,比如工厂方法、观察者等等。
另外,有人常常“为赋新词强说愁”,硬去使用设计模式,反而会让代码变得更加复杂了。你会有一种错觉,我是不是学了一个假的设计模式,人人都说好的东西,我怎么就感受不到呢?
初学设计模式时,我真的就被这个问题困扰了好久。直到我看到了 Robert Martin 的《敏捷软件开发:原则、实践与模式》。这是一本被名字糟蹋了的好书。
这本书出版之际敏捷软件开发运动正风起云涌Robert Martin 也不能免俗地蹭了热点,将“敏捷”挂到了书名里。其实,这是一本讲软件设计的书。
当我看到了 SOLID 的五个原则之后,我终于想明白了,原来我追求的方向错了。如果说设计模式是“术”,设计原则才是“道”。设计模式并不能帮你建立起知识体系,而设计原则可以。
当我不能理解“道”的时候,“术”只能死记硬背,效果必然是不佳的。想通这些之后,我大大方方地放弃了对于设计模式的追求,只是按照设计原则来写代码,结果是,我反而是时常能重构出符合某个设计模式的代码。至于具体模式的名字,如果不是有意识地去找,我已经记不住了。
当然,我并不是说设计模式不重要,之所以我能够用设计原则来写代码,前提条件是,我曾经在设计模式上下过很多功夫。
道和术,是每个程序员都要有的功夫,在“术”上下过功夫,才会知道“道”的价值,“道”可以帮你建立更完整的知识体系,不必在“术”的低层次上不断徘徊。
单一职责原则
好,下面我就单拿 SOLID 中单一职责原则稍微展开讲一下,虽然这个原则听上去是最简单的,但也有很多误解存在。
首先,什么是单一职责原则呢?如果读过《敏捷软件开发:原则、实践与模式》,你对单一职责的理解应该是,一个模块应该仅有一个修改的原因。
2017年Robert Martin 出版了《架构整洁之道》Clean Architecture他把单一职责原则的定义修改成“一个模块应该仅对一类 actor 负责”,这里的 actor 可以理解为对系统有共同需求的人。
不管是哪个定义,初读起来,都不是那么好理解。我举个例子,你就知道了。我这里就用 Robert Martin 自己给出的例子:在一个工资管理系统中,有个 Employee 类,它里面有三个方法:
calculatePay(),计算工资,这是财务部门关心的。
reportHours(),统计工作时长,这是人力部门关心的。
save(),保存数据,这是技术部门关心的。
之所以三个方法在一个类里面因为它们的某些行为是类似的比如计算工资和统计工作时长都需要计算正常工作时间为了避免重复团队引入了新的方法regularHours()。
接下来,财务部门要修改正常工作时间的统计方法,但人力部门不需要修改。负责修改的程序员只看到了 calculatePay() 调用了 regularHours(),他完成了他的工作,财务部门验收通过。但上线运行之后,人力部门产生了错误的报表。
这是一个真实的案例,最终因为这个错误,给公司造成了数百万的损失。
如果你问程序员,为什么要把 calculatePay() 和 reportHours()放在一个类里,程序员会告诉你,因为它们都用到了 Employee 这个类的数据。
但是,它们是在为不同的 actor 服务,所以,任何一个 actor 有了新的需求,这个类都需要改,它也就很容易就成为修改的重灾区。
更关键的是,很快它就会复杂到没人知道一共有哪些模块与它相关,改起来会影响到谁,程序员也就越发不愿意维护这段代码了。
我在专栏“开篇词”里提到过,人的大脑容量有限,太复杂的东西理解不了。所以,我们唯一能做的就是把复杂的事情变简单。
我在“任务分解”模块中不断强调把事情拆小,同样的道理在写代码中也适用。单一职责原则就是给了你一个指导原则,可以按照不同的 actor 分解代码。
上面这个问题Robert Martin 给了一个解决方案,就是按照不同的 actor 将类分解,我把分解的结果的类图附在了下面:
编写短函数
好,你已经初步了解了单一职责原则,但还有一点值得注意。我先来问个问题,你觉得一个函数多长是合适的?
曾经有人自豪地向我炫耀他对代码要求很高超过50行的函数绝对要处理掉。
我在专栏中一直强调“小”的价值,能看到多小,就可以在多细的粒度上工作。单一职责这件事举个例子很容易,但在真实的工作场景中,你能看到一个模块在为多少 actor 服务,就完全取决于你的分解能力了。
回到前面的问题上,就我自己的习惯而言,通常的函数都在十行以内,如果是表达能力很强的语言,比如 Ruby函数会更短。
所以你可想而知我听到“把50行代码归为小函数”时的心情。我知道“函数长短”又是一个非常容易引起争论的话题不同的人对于这个问题的答案取决于他看问题的粒度。
所以,不讨论前提条件,只谈论函数的长短,其实是没有意义的。
单一职责原则可以用在不同的层面,写一个类,你可以问问这些方法是不是为一类 actor 服务;写方法时,你可以问问这些代码是不是在一个层面上;甚至一个服务,也需要从业务上考虑一下,它在提供是否一类的服务。总之,你看到的粒度越细,也就越能发现问题。
总结时刻
今天,我讲的内容是软件设计,很多代码的问题就是因为对设计思考得不足导致的。
许多程序员学习设计是从设计模式起步的但这种学法往往会因为缺乏结构很难有效掌握。设计原则是一个更好的体系掌握设计原则之后才能更好地理解设计模式这些招式。Robert Martin 总结出的“SOLID”是一套相对完整易学的设计原则。
我以“SOLID” 中的单一职责原则为例,给你稍做展开,更多的内容可以去看 Robert Martin 的书。不过,我也给你补充了一些维度,尤其是从“小”的角度告诉你,你能看到多小,就能发现代码里多少的问题。
如果今天的内容你只能记住一件事,那请记住:把函数写短。
最后我想请你思考一下,你是怎么理解软件设计的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 总是在说MVC分层架构但你真的理解分层吗
你好,我是郑晔。
作为程序员,你一定听说过分层,比如,最常见的 Java 服务端应用的三层结构在《15 | 一起练习:手把手带你分解任务》中,我曾提到过:
数据访问层,按照传统的说法,叫 DAOData Access Object数据访问对象按照领域驱动开发的术语称之为 Repository
服务层,提供应用服务;
资源层,提供对外访问的资源,采用传统做法就是 Controller。
这几乎成为了写 Java 服务的标准模式。但不知道你有没有想过,为什么要分层呢?
设计上的分解
其实,分层并不是一个特别符合直觉的做法,符合直觉的做法应该是直接写在一起。
在编程框架还不是特别流行的时候,人们就是直接把页面和逻辑混在一起写的。如果你有机会看看写得不算理想的 PHP 程序,这种现象还是大概率会出现的。
即便像 Java 这个如此重视架构的社区,分层也是很久之后才出现的,早期的 JSP 和 PHP 并没有什么本质区别。
那为什么要分层呢?原因很简单,当代码复杂到一定程度,人们维护代码的难度就急剧上升。一旦出现任何问题,在所有一切都混在一起的代码中定位问题,本质上就是一个“大海捞针”的活。
前面讲任务分解的时候,我不断在强调的观点就是,人们擅长解决的是小问题,大问题怎么办?拆小了就好。
分层架构,实际上,就是一种在设计上的分解。
回到前面所说的三层架构,这是行业中最早普及的一种架构模式,最开始是 MVC也就是 Model、View 和 Controller。
MVC 的概念起源于 GUI Graphical User Interface图形用户界面编程人们希望将图形界面上展示的部分View与 UI 的数据模型Model分开它们之间的联动由 Controller 负责。这个概念在 GUI 编程中是没有问题的,但也仅限于在与 UI 有交互的部分。
很多人误以为这也适合服务端程序,他们就把模型部分误解成了数据库里的模型,甚至把它理解成数据库访问。于是,你会看到有人在 Controller 里访问数据库。
不知道你是不是了解 Ruby on Rails这是当年改变了行业认知的一个 Web 开发框架,带来很多颠覆性的做法。它采用的就是这样一种编程模型。当年写 Rails 程序的时候我发现,当业务复杂到了一定规模,代码就开始难以维护了。我想了好久,终于发现,在 Rails 的常规做法中少了服务层Service的设计。
这个问题在 Java 领域,爆发得要比 Rails 里早,因为 Ruby 语言的优越性Rails 实现的数据访问非常优雅。正是因为 Rails 的数据访问实在太容易了,很多服务实际上写到 Model 层里。在代码规模不大时,代码看上去是不复杂的,甚至还有些优雅。
而那时的 Java 可是要一行一行地写数据访问,所以,代码不太可能放在 Model 层而放在Controller 里也会让代码变复杂,于是,为业务逻辑而生的 Service 层就呼之欲出了。
至此,常见的 Java 服务端开发的基础就全部成型了,只不过,由于后来 REST 服务的兴起,资源层替代了 Controller 层。
到这里,我给你讲了常见的 Java 服务三层架构的来龙去脉。但实际上,在软件开发中,分层几乎是无处不在的,因为好的分层往往需要有好的抽象。
无处不在的分层
作为程序员,我们几乎每天都在与分层打交道。比如说,程序员都对网络编程模型很熟悉,无论是 ISO 的七层还是 TCP/IP 的五层。
但不知道你有没有发现虽然学习的时候你要学习网络有那么多层但在使用的时候大多数情况下你只要了解最上面的那层比如HTTP。
很多人对底层的协议的理解几乎就停留在“学过”的水平上因为在大多数情况下除非你要写协议栈不然你很难用得到。即便偶尔用到90%的问题靠搜索引擎就解决了,你也很少有动力去系统学习。
之所以你可以这么放心大胆地“忽略”底层协议,一个关键点就在于,网络模型的分层架构实现得太好了,好到你作为上层的使用者几乎可以忽略底层。而这正是分层真正的价值:构建一个良好的抽象。
这种构建良好的抽象在软件开发中随处可见,比如,你作为一个程序员,每天写着在 CPU 上运行的代码,但你读过指令集吗?你之所以可以不去了解,是因为已经有编译器做好了分层,让你可以只用它们构建出的“抽象”——编程语言去思考问题。
比如,每天写着 Java 程序的程序员,你知道 Java 程序是如何管理内存的吗?这可是令很多 C/C++程序员寝食难安的问题,而你之所以不用关心这些,正是托了 Java 这种“抽象”的福。对了,你甚至可能没有注意到编程语言也是一种抽象。
有抽象有发展
只有构建起抽象,人们才能在此基础上做出更复杂的东西。如果今天的游戏依然是面向显示屏的像素编程,那么,精彩的游戏视觉效果就只能由极少数真正的高手来开发。我们今天的大部分游戏应该依然停留在《超级玛丽》的水准。
同样,近些年前端领域风起云涌,但你是否想过,为什么 Web 的概念早就出现了,但前端作为一个专门的职位,真正的蓬勃发展却是最近十年的事?
2009年Ryan Dahl 发布了Node.js人们才真正认识到原来 JavaScript 不仅仅可以用于浏览器,还能做服务器开发。
于是JavaScript 社区大发展,各种在其他社区已经很流行的工具终于在 JavaScript 世界中发展了起来。正是有了这些工具的支持,人们才能用 JavaScript 构建更复杂的工程,前端领域才能得到了极大的发展。
如今JavaScript 已经发展成唯一一门全平台语言,当然,发展最好的依然是在它的大本营:前端领域。前端程序员才有了今天幸福的烦恼:各种前端框架层出不穷。
在这里Node.js 的出现让 JavaScript 成为了一个更好的抽象。
构建你的抽象
理解了分层实际上是在构建抽象,你或许会关心,我该怎么把它运用在自己的工作中。
构建抽象,最核心的一步是构建出你的核心模型。什么是核心模型呢?就是表达你业务的那部分代码,换句话说,别的东西都可以变,但这部分不能变。
这么说可能还是有点抽象,我们回到前面的三层架构。
在前面介绍三层架构的演变时提到了一个变迁REST服务的兴起让 Controller 逐渐退出了历史舞台,资源层取而代之。
换句话说,访问服务的方式可能会变。放到计算机编程的发展中,这种趋势就更明显了,从命令行到网络,从 CSClient-Server 到 BSBrowser-Server从浏览器到移动端。所以怎么访问不应该是你关注的核心。
同样, 关系型数据库也不是你关注的核心,它只是今天的主流而已。从前用文件,今天还有各种 NoSQL。
如此说来三层架构中的两层重要性都不是那么高那重要的是什么答案便呼之欲出了没错就是剩下的部分我们习惯上称之为服务层但这个名字其实不能很好地反映它的作用更恰当的说法应该可以叫领域模型Domain Model
它便是我们的核心模型,也是我们在做软件设计时,真正应该着力的地方。
为什么叫“服务层”不是一个好的说法呢?这里会遗漏领域模型中一个重要的组成部分:领域对象。
很多人理解领域对象有一个严重的误区,认为领域对象属于数据层。数据存储只是领域对象的一种用途,它更重要的用途还是用在各种领域服务中。
由此还能引出另一个常见的设计错误,领域对象中只包含数据访问,也就是常说的 getter 和 setter而没有任何逻辑。
如果只用于数据存储,只有数据访问就够了,但如果是领域对象,就应该有业务逻辑。比如,给一个用户修改密码,用户这个对象上应该有一个 changePassword 方法,而不是每次去 setPassword。
严格地说,领域对象和存储对象应该是两个类,只不过它俩实在太像了,很多人经常使用一个类,这还是个小问题。但很多人却把这种内部方案用到了外部,比如,第三方集成。
为数不少的团队都在自己的业务代码中直接使用了第三方代码中的对象,第三方的任何修改都会让你的代码跟着改,你的团队就只能疲于奔命。
解决这个问题最好的办法就是把它们分开,你的领域层只依赖于你的领域对象,第三方发过来的内容先做一次转换,转换成你的领域对象。这种做法称为防腐层。
当我们把领域模型看成了整个设计的核心,看待其他层的视角也会随之转变,它们只不过是适配到不同地方的一种方式而已,而这种理念的推广,就是一些人在说的六边形架构。
怎么设计好领域模型是一个庞大的主题推荐你去了解一下领域驱动设计Domain Driven DesignDDD这个话题我们后面还会再次提到。
讨论其实还可以继续延伸下去已经构建好的领域模型怎么更好地提供给其他部分使用呢一个好的做法是封装成领域特定语言Domain Specific LanguageDSL。当然这也是一个庞大的话题就不继续展开了。
总结时刻
我从最常见的服务端三层架构入手,给你讲了它们的来龙去脉。分层架构实际是一种设计上的分解,将不同的内容放在不同的地方,降低软件开发和维护的成本。
分层,更关键的是,提供抽象。这种分层抽象在计算机领域无处不在,无论是编程语言,还是网络协议,都体现着分层抽象的价值。有了分层抽象,人们才能更好地在抽象的基础上构建更复杂的东西。
在日常工作中,我们应该把精力重点放在构建自己的领域模型上,因为它才是工作最核心、不易变的东西。
如果今天的内容你只能记住一件事,那请记住:构建好你的领域模型。
最后我想请你思考一下,你还知道哪些技术是体现分层抽象的思想吗?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
36 为什么总有人觉得5万块钱可以做一个淘宝
你好,我是郑晔。
今天,我们从软件行业的一个段子说起。
甲方想要做个电商网站作为乙方的程序员问“你要做个什么样的呢”甲方说“像淘宝那样就好。”程序员问“那你打算出多少钱”甲方想了想“5万块钱差不多了吧
这当然是个调侃客户不懂需求的段子,但你有没有想过,为什么在甲方看来并不复杂的系统,你却觉得困难重重呢?
因为你们想的根本不是一个东西。
在客户看来我要的不就是一个能买东西的网站吗只要能上线商品用户能看到能购买不就好了5万块钱差不多了。
而你脑中想的却是“淘宝啊那得是多大的技术挑战啊每年一到双11那就得考虑各种并发抢购。淘宝得有多少程序员5万块你就想做一个门都没有。”
如果放在前面“沟通反馈”的模块,我可能会讲双方要怎么协调,把想法统一了。但到了“自动化”的模块,我想换个角度讨论这个问题:系统是怎么变复杂的。
淘宝的发展历程
既然说到了淘宝我们就以一些公开资料来看看淘宝的技术变迁过程。2013年子柳出版了一本《淘宝技术这十年》这本书里讲述了淘宝是怎么一步步变化的。
按照书中的说法,第一个淘宝是“买来的”,买的是一个叫做 PHPAuction 的系统即便选择了最高配也才花了2000美元左右。这是一个采用 LAMP 架构的系统,也就是 Linux + Apache + MySQL + PHP这在当年可是典型的开源架构。
团队所做的主要就是一些订制化工作,最大的调整就是将单一数据库的读写进行了拆分,变成了一个主库和两个从库。这种结构在今天来看,依然是很多团队做调整的首选。
当访问量和数据量不断提升MySQL 数据库率先扛不住了。当年的 MySQL 默认采用的是 MyISAM 引擎,写数据的时候会锁住表,读也会被卡住,当然,这只是诸多问题中的一个。
2003年底团队将 MySQL 换成了 Oracle。由于 Oracle 的性能要好上许多,主从的数据库架构又改回了单一数据库。但由于 PHP 访问数据库的缺省方案没有连接池,只好找了开源的 SQL Relay这也为后续的改进埋下了伏笔。
当数据量继续加大,本地存储就已经无法满足了,只能通过引入网络存储解决问题。数据量进一步增大之后,存储节点一拆再拆,依然不能解决问题,淘宝就踏上了购买小型机的道路。
IBM 的小型机、Oracle 的数据库和 EMC 的存储,这个阶段就踏上了 IOE 之路。
2004年初SQL Relay 已经成了一个挥之不去的痛点于是只能从更根本的方案上动脑筋更换程序设计语言。作为当时的主流Java 成了不二之选。
替换的方案就是给业务分模块,一块一块地替换。老模块只维护,不增加新功能,新功能只在新模块开发,新老模块共用数据库。新功能上线,则关闭老模块对应功能,所有功能替换完毕,则老模块下线。
淘宝的数据量继续增长,单台 Oracle 很快到了上限,团队采用了今天常见的“分库分表”模式,但“分库分表”就会带来新的问题,跨数据库的数据怎么整合?于是,打造出了一个 DBRoute用以处理分库的数据。
但是,这种做法也带来了一个新的问题,同时连接多个数据库,任何一个数据库出了问题,都会导致整个网站的故障。
当淘宝的数据量再次增长,每次访问都到了数据库,数据库很难承受。一个解决方案就是引入缓存和 CDNContent Delivery Network内容分发网络这样只读数据的压力就从数据库解放了出来。
当时的缓存系统还不像今天这么成熟,于是,团队基于一个开源项目改出了一个。他们用的 CDN 最开始是一个商用系统,但流量的增加导致这个系统也支撑不住了,只好开始搭建自己的 CDN。
后来,因为 CDN 要消耗大量的服务器资源,为了降低成本,淘宝又开始研发自己的低功耗服务器。
随着业务的不断发展,开发人员越来越多,系统就越来越臃肿,耦合度也逐渐提升,出错的概率也逐渐上升。这时,不得不对系统进行分解,将复用性高的模块拆分出来,比如,用户信息。
业务继续发展,拆分就从局部开始向更大规模发展,底层业务和上层流程逐渐剥离,并逐渐将所有业务都模块化。
有了一个相对清晰地业务划分之后,更多的底层业务就可以应用于不同的场景,一个基础设施就此成型,新的业务就可以使用基础设施进行构建,上层业务便如雨后春笋一般蓬勃发展起来。
在这个过程中有很多技术问题在当时还没有好的解决方案或者是不适合于它们所在的场景。所以淘宝的工程师就不得不打造自己的解决方案比如分布式文件系统TFS、缓存系统Tair、分布式服务框架HSF等等。还有一些技术探索则是为了节省成本比如去 IOE 和研发低功耗服务器等等。
我这里以淘宝网站的发展为例,做了一个快速的梳理,只是为了让你了解一个系统的发展,如果你有兴趣了解更多细节,不妨自己找出这本书读读。当然,现在的淘宝肯定比这更加完整复杂。
同样的业务,不同的系统
为什么我们要了解一个系统的演化过程呢?因为作为程序员,我们需要知道自己面对的到底是一个什么样的系统。
回到我们今天的主题上5万块钱可以不可以做一个淘宝答案是取决于你要的是一个什么样的系统。最开始买来的“淘宝”甚至连5万块钱都不用而今天的淘宝和那时的淘宝显然不是一个系统。
从业务上说,今天的淘宝固然已经很丰富了,但最核心的业务相差并不大,无非是卖家提供商品,买家买商品。那它们的本质差别在哪呢?
回顾上面的过程,你就可以看到,每次随着业务量的增长,原有技术无法满足需要,于是,就需要用新的技术去解决这个问题。这里的关键点在于:不同的业务量。
一个只服务于几个人的系统,单机就够了,一个刚刚入行的程序员也能很好地实现这个系统。而当业务量到达一台机器抗不住的时候,就需要用多台机器去处理,这个时候就必须考虑分布式系统的问题,可能就要适当地引入中间件。
而当系统变成为海量业务提供服务,就没有哪个已经打造好的中间件可以提供帮助了,需要自己从更底层解决问题。
虽然在业务上看来,这些系统是一样的,但在技术上看来,在不同的阶段,一个系统面对的问题是不同的,因为它面对业务的量级是不同的。更准确地说,不同量级的系统根本就不是一个系统。
只要业务在不断地发展,问题就会不断出现,系统就需要不断地翻新。我曾听到一个很形象的比喻:把奥拓开成奥迪。
你用对技术了吗?
作为一个程序员,我们都知道技术的重要性,所以,我们都会努力地去学习各种各样的新技术。尤其是当一个技术带有大厂光环的时候,很多人都会迫不及待地去学习。
我参加过很多次技术大会,当大厂有人分享的时候,通常都是人山人海,大家都想学习大厂有什么“先进”技术。
知道了,然后呢?
很多人就想迫不及待地想把这些技术应用在自己的项目中。我曾经面试过很多程序员,给我讲起技术来滔滔不绝,说什么自己在设计时考虑各种分布式的场景,如果系统的压力上来时,他会如何处理。
我就好奇地问了一个问题,“你这个系统有多少人用?”结果,他做的只是一个内部系统,使用频率也不高。
为了技术而技术的程序员不在少数,过度使用技术造成的结果就是引入不必要的复杂度。即便用了牛刀杀鸡,因为缺乏真实场景检验,也不可能得到真实反馈,对技术理解的深度也只能停留在很表面的程度上。
在前面的例子中,淘宝的工程师之所以要改进系统,真实的驱动力不是技术,而是不断攀升的业务量带来的问题复杂度。
所以,评估系统当前所处的阶段,采用恰当的技术解决,是我们最应该考虑的问题。
也许你会说,我做的系统没有那么大的业务量,我还想提高技术怎么办?答案是到有好问题的地方去。现在的 IT 行业提供给程序员的机会很多,找到一个有好问题的地方并不是一件困难的事,当然,前提条件是,你自己得有解决问题的基础能力。
总结时刻
今天,我以淘宝的系统为例,给你介绍了一个系统逐渐由简单变复杂的发展历程,希望你能认清不同业务量级的系统本质上就不是一个系统。
一方面,有人会因为对业务量级理解不足,盲目低估其他人系统的复杂度;另一方面,也有人会盲目应用技术,给系统引入不必要的复杂度,让自己陷入泥潭。
作为拥有技术能力的程序员,我们都非常在意个人技术能力的提升,但却对在什么样情形下,什么样的技术更加适用考虑得不够。采用恰当的技术,解决当前的问题,是每个程序员都应该仔细考虑的问题。
如果今天的内容你只能记住一件事,那请记住:用简单技术解决问题,直到问题变复杂。
最后,我想请你回想一下,你身边有把技术做复杂而引起的问题吗?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,111 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
37 先做好DDD再谈微服务吧那只是一种部署形式
你好,我是郑晔。
在“自动化”模块的最后,我们来聊一个很多人热衷讨论却没做好的实践:微服务。
在今天做后端服务似乎有一种倾向,如果你不说自己做的是微服务,出门都不好意思和人打招呼。
一有技术大会,各个大厂也纷纷为微服务出来站台,不断和你强调自己公司做微服务带来的各种收益,下面的听众基本上也是热血沸腾,摩拳擦掌,准备用微服务拯救自己的业务。
我就亲眼见过这样的例子,几个参加技术大会的人回到公司,跟人不断地说微服务的好,说服了领导,在接下来大的项目改造中启用了微服务。
结果呢?一堆人干了几个月,各自独立开发的微服务无法集成。最后是领导站出来,又花了半个月时间,将这些“微服务”重新合到了一起,勉强将这个系统送上了线。
人家的微服务那么美,为什么到你这里却成了烂摊子呢?因为你只学到了微服务的形。
微服务
大部分人对微服务的了解源自 James Lewis 和 Martin Fowler 在2014年写的一篇文章他们在其中给了微服务一个更清晰的定义把它当作了一种新型的架构风格。
但实际上,早在这之前的几年,很多人就开始用“微服务”这个词进行讨论了。
“在企业内部将服务有组织地进行拆分”这个理念则脱胎于 SOAService Oriented Architecture面向服务的架构只不过SOA 诞生自那个大企业操盘技术的年代,自身太过于复杂,没有真正流行开来。而微服务由于自身更加轻量级,符合程序员的胃口,才得以拥有更大的发展空间。
谈到微服务,你会想起什么呢?很多人对微服务的理解,就是把一个巨大的后台系统拆分成一个一个的小服务,再往下想就是一堆堆的工具了。
所以,市面上很多介绍微服务的内容,基本上都是在讲工具的用法,或是一些具体技术的讨论,比如,用 Spring Boot 可以快速搭建服务,用 Spring Cloud 建立分布式系统,用 Service Mesh 技术作为服务的基础设施,以及怎么在微服务架构下保证事务的一致性,等等。
确实,这些内容在你实现微服务时,都是有价值的。但必须先回答一个问题,我们为什么要做微服务?
对这个问题的标准回答是相对于整体服务Monolithic而言微服务足够小代码更容易理解测试更容易部署也更简单。
这些道理都对,但这是做好了微服务的结果。怎么才能达到这个状态呢?这里面有一个关键因素,怎么划分微服务,也就是一个庞大的系统按照什么样的方式分解。
这是在很多关于微服务的讨论中所最为欠缺的,也是很多团队做“微服务”却死得很难看的根本原因。
不了解这一点,写出的服务,要么是服务之间互相调用,造成整个系统执行效率极低;要么是你需要花大力气解决各个服务之间的数据一致性。换句话说,服务划分不好,等待团队的就是无穷无尽的偶然复杂度泥潭。只有正确地划分了微服务,它才会是你心目中向往的样子。
那应该怎么划分微服务呢?你需要了解领域驱动设计。
领域驱动设计
领域驱动设计Domain Driven DesignDDD是 Eric Evans 提出的从系统分析到软件建模的一套方法论。它要解决什么问题呢?就是将业务概念和业务规则转换成软件系统中概念和规则,从而降低或隐藏业务复杂性,使系统具有更好的扩展性,以应对复杂多变的现实业务问题。
这听上去很自然,不就应该这么解决问题吗?并不然,现实情况可没那么理想。
在此之前,人们更多还是采用面向数据的建模方式,时至今日,还有许多团队一提起建模,第一反应依然是建数据库表。这种做法是典型的面向技术实现的做法。一旦业务发生变化,团队通常都是措手不及。
DDD 到底讲了什么呢?它把你的思考起点,从技术的角度拉到了业务上。
贴近业务走近客户我们在这个专栏中已经提到过很多次。但把这件事直接体现在写代码上恐怕还是很多人不那么习惯的一件事。DDD 最为基础的就是通用语言Ubiquitous Language让业务人员和程序员说一样的语言。
这一点我在《21 | 你的代码为谁而写?》中已经提到过了。使用通用语言,等于把思考的层次从代码细节中拉到了业务层面。越高层的抽象越稳定,越细节的东西越容易变化。
有了通用语言做基础,然后就要进入到 DDD 的实战环节了。DDD 分为战略设计Strategic Design和战术设计Tactical Design
战略设计是高层设计,它帮我们将系统切分成不同的领域,并处理不同领域的关系。我在前面的内容中给你举过“订单”和“用户”的例子。从业务上区分,把不同的概念放到不同的地方,这是从根本上解决问题,否则,无论你的代码写得再好,混乱也是不可避免的。而这种以业务的角度思考问题的方式就是 DDD 战略设计带给我的。
战术设计,通常是指在一个领域内,在技术层面上如何组织好不同的领域对象。举个例子,国内的程序员喜欢用 myBatis 做数据访问,而非 JPA常见的理由是 JPA 在有关联的情况下,性能太差。但真正的原因是没有设计好关联。
如果能够理解 DDD 中的聚合根Aggregate Root我们就可以找到一个合适的访问入口而非每个人随意读取任何数据。这就是战术设计上需要考虑的问题。
战略设计和战术设计讨论的是不同层面的事情,不过,这也是 Eric Evans 最初没有讲清楚的地方,导致了人们很长时间都无法理解 DDD 的价值。
走向微服务
说了半天,这和微服务有什么关系呢?微服务真正的难点并非在于技术实现,而是业务划分,而这刚好是 DDD 战略设计中限界上下文Bounded Context的强项。
虽然通用语言打通了业务与技术之间的壁垒,但计算机并不擅长处理模糊的人类语言,所以,通用语言必须在特定的上下文中表达,才是清晰的。就像我们说过的“订单”那个例子,交易的“订单”和物流的“订单”是不同的,它们都有着自己的上下文,而这个上下文就是限界上下文。
它限定了通用语言自由使用的边界,一旦出界,含义便无法保证。正是由于边界的存在,一个限界上下文刚好可以成为一个独立的部署单元,而这个部署单元就可以成为一个服务。
所以要做好微服务,第一步应该是识别限界上下文。
你也看出来了,每个限界上下文都应该是独立的,每个上下文之间就不应该存在大量的耦合,困扰很多人的微服务之间大量相互调用,本身就是一个没有划分好边界而带来的伪命题,靠技术解决业务问题,事倍功半。
有了限界上下文就可以做微服务了吧?且慢!
Martin Fowler 在写《企业应用架构模式》时,提出了一个分布式对象第一定律:不要分布对象。同样的话,在微服务领域也适用,想做微服务架构,首先是不要使用微服务。如果将一个整体服务贸然做成微服务,引入的复杂度会吞噬掉你以为的优势。
你可能又会说了,“我都把限界上下文划出来了,你告诉我不用微服务?”
还记得我在《30 | 一个好的项目自动化应该是什么样子的?》中提到的分模块吗?如果你划分出了限界上下文,不妨先按照它划分模块。
以我拙见,一次性把边界划清楚并不是一件很容易的事。大家在一个进程里,调整起来会容易很多。然后,让不同的限界上下文先各自独立演化。等着它演化到值得独立部署了,再来考虑微服务拆分的事情。到那时,你也学到各种关于微服务的技术,也就该派上用场了!
总结时刻
微服务是很多团队的努力方向,然而,现在市面上对于微服务的介绍多半只停留在技术层面上,很多人看到微服务的好,大多数是结果,到自己团队实施起来却困难重重。想要做好微服务,关键在于服务的划分,而划分服务,最好先学习 DDD。
Eric Evans 2003年写了《领域驱动设计》向行业介绍了DDD 这套方法论立即在行业中引起广泛的关注。但实话说Eric 在知识传播上的能力着实一般,这本 DDD 的开山之作写作质量难以恭维,想要通过它去学好 DDD是非常困难的。所以在国外的技术社区中有很多人是通过各种交流讨论逐渐认识到 DDD 的价值所在,而在国内 DDD 几乎没怎么掀起波澜。
2013年在 Eric Evans 出版《领域驱动设计》十年之后DDD 已经不再是当年吴下阿蒙有了自己一套比较完整的体系。Vaughn Vernon 将十年的精华重新整理,写了一本《实现领域驱动设计》,普通技术人员终于有机会看明白 DDD 到底好在哪里了。所以,你会发现,最近几年,国内的技术社区开始出现了大量关于 DDD 的讨论。
再后来因为《实现领域驱动设计》实在太厚Vaughn Vernon 又出手写了一本精华本《领域驱动设计精粹》,让人可以快速上手 DDD这本书也是我向其他人推荐学习 DDD 的首选。
即便你学了 DDD知道了限界上下文也别轻易使用微服务。我推荐的一个做法是先用分模块的方式在一个工程内让服务先演化一段时间等到真的觉得某个模块可以“毕业”了再去开启微服务之旅。
如果今天的内容你只能记住一件事,那请记住:学习领域驱动设计。
最后,我想请你分享一下,你对 DDD 的理解是什么样的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,134 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 我们应该如何保持竞争力?
你好,我是郑晔。
在前面两讲,我结合着两个程序员要直接面对的场景,讨论了如何综合运用前面学习到的知识,这一讲的内容可能不涉及到实际的应用场景,但与每个人的发展息息相关。我想谈谈如何走好程序员这条路。
焦虑的程序员
让我们再次用思考框架分析一下问题。首先,现状是什么?关于这个问题,我并不打算讨论个体,因为每个人的情况千差万别,我准备从整体入手。
IT 行业是一个快速发展变化的行业,一方面,我们不断地看到有人快速取得成功,另一方面,我们也听到了许多充满焦虑的声音。获得大的成功总是一个小概率事件,大多数人面对的还是日常的柴米油盐。
我们的焦虑来自于对未来的不确定性,而这种不确定性是一个特定时代加上特定行业的产物。
如果把时间倒回到上个世纪80年代之前虽然当时的生活条件一般但很少有人会为未来的发展焦虑因为那时候人们可以清晰地看到自己未来的人生尽管那种人生可能是平淡的。
但今天的我们处在一个人类历史上少有的快速发展的时代,我们看不清以后的人生,大脑却还停留在上一代人的思维习惯上。
IT 行业在国内的大发展也就最近20多年的事行业里很少有走过完整职业生涯的程序员。也正是因为如此我们经常会产生了各种焦虑
我刚刚入行时有人问程序员能做到30岁吗
我快30岁时有人问35岁还能做程序员吗
我35岁时讨论变成了40岁的程序员该怎么办。
估计等国内有越来越多的程序员走完了整个职业生涯,就会有人关心,程序员退休之后的生活应该是什么样子了。
从长期来看,只要生活中还有需要用自动化解决的问题,程序员这个群体还是很有前景的。但随着时间的推移,程序员这个职业的溢价也会越来越低,单纯凭借身处这个行业就获得好发展的可能性也越来越低,想让自己的职业生涯走得更顺畅,还需要找到更好的目标,不断努力。
成为 T 型人
我们再来回答下一个问题:目标是什么。也许这时候,每个人脑子里想到的职业发展路线都不一样,但我准备用一个统一的目标回答你:成为 T 型人。
什么叫 T 型人?简言之,一专多能。
有了“一专”,“多能”才是有意义的,否则,就是低水平重复,而这正是很多人职业生涯不见起色的真正原因。
这里的“专”不是熟练而是深入。你可能是个有着10年丰富经验的程序员但实际上只不过是重复了10年解决同样难度的问题而已这根本就不算深入也就没有做到真正意义上的“一专”。
你会发现很多优秀的人,在很多方面都会很优秀,这是“一专”带来的触类旁通。
当你有了“一专”,拓展“多能”,就会拥有更宽广的职业道路。比如,我拥有了深厚的技术功底,通晓怎么做软件:
如果还能够带着其他人一起做好,就成了技术领导者。
如果能够分享技术的理解,就有机会成为培训师。
如果能够在实战中帮助别人解决问题,就可以成为咨询师。
反过来,当你有了“多能”,也可以拓宽你的视野,帮你认清自己的“一专”怎样更好地发挥价值,而不是狭隘地认为自己有了技术,就已经天下尽在掌握了。视野窄,缺乏大局观,也成为了许多程序员再进一步的阻碍。事实上,这个专栏里的很多内容都是帮你打开“多能”的视角。
也许你会说,我在公司已经独当一面了,应该算有“一专”了吧?但我想说的是,可能还不够。只做一个公司的专家,受一个公司的波动影响太大,而成为行业的专家,才会降低自己职业生涯的风险。
有时,我在面试时会问候选人这样一个问题:“如果让你在一次技术大会上做分享,你会讲什么呢?”我真正的问题是,以行业标准衡量,你觉得你在哪个方面是专家呢?
大多数人从来没有思考过这个问题,他们只是日常在完成自己的工作,即便在某一方面已经做得很不错了,但依然算不上专家,因为他们缺乏深度思考。
比如,你非常熟悉 Kafka知道它的各种参数也读过它的实现原理。但如果我问你Kafka 为什么要把自己定位成一个分布式流平台,它要想成为一个流平台,还要在哪方面做得更好?你的答案是什么呢?
这其中的差别就是,前面所谓的熟悉,只是熟悉别人的思考结果,而后面则是一个没有现成答案的东西。学习微积分是有难度,但同发明微积分相比,难度根本不在一个层次上。当然,我不是说你要熟悉所有工具的发展过程,而是自己要在一个特定的方面拥有深度的思考。
也许你会说,这个要求实在是太高了吧!没错,这确实是一个很高的要求。但“取法于上,仅得为中;取法于中,故为其下。”
其实,很多人的焦虑就源自目标太低,找不到前进的动力。给自己定下一个可以长期努力的目标,走在职业的道路上才不致于很快丧失动力。
在学习区成长
现在我们来回答第三个问题,怎么达到目标。既然要朝着行业中的专家方向努力,那你就得知道行业中的专家是什么样。我的一个建议是,向行业中的大师学习。
你或许会说,我倒是想向大师学习,但哪有机会啊!好在 IT 行业中的许多人都是愿意分享的,我们可以读到很多大师级程序员分享的内容。
我在入行的时候有幸读了很多经典之作比如出身贝尔实验室的很多大师级程序员的作品诸如《C 程序设计语言》《程序设计实践》、《Unix 编程环境》等,还有一些像 Eric Raymond 这样沉浸编程几十年的人写出的作品诸如《Unix 编程艺术》,以及前面提及的 Kent Beck、Martin Fowler 和 Robert Martin 等这些人的作品。
读这些书的一个好处在于,你的视野会打开,不会把目标放在“用别人已经打造好的工具做一个特定的需求”,虽然这可能是你的必经之路,但那只是沿途的风景,而不是目标。
接下来,我们要踏上征程,怎么才能让自己的水平不断提高呢?我的答案是,找一个好问题去解决,解决了一个好的问题能够让你的水平快速得到提升。什么是好问题?就是比你当前能力略高一点的问题,比如:
如果你还什么都不会,那有一份编程的工作就好。
如果你已经能够写好普通的代码,就应该尝试去编写程序库。
如果实现一个具体功能都没问题了,那就去做设计,让程序有更好的组织。
如果你已经能完成一个普通的系统设计,那就应该去设计业务量更大的系统。
为什么要选择比自己水平高一点的问题这与我们学习成长的方式有关。Noel Tichy 提出了一个“学习区”模型,如下图所示:
最内层是舒适区Comfort Zone置身其中会让人感觉良好但也会因为没有挑战成长甚微你可以把它理解成做你最熟悉的事情。
最外层是恐慌区Panic Zone这是压力极大的地方完全超出了你的能力范围你在其中只会感到无比的焦虑。
中间的是学习区Learning Zone事情有难度又刚好是你努力一下可以完成的这才是成长最快的区域。
根据这个模型,只有一直身处学习区才能让人得到足够的成长,所以,我们应该既选择比自己能力高一点的问题去解决,不要总做自己习惯的事,没有挑战,也不要好大喜功,一下子把自己的热情全部打散。
在学习区成长,就不要满足于当前已经取得的成绩,那已经成为你的舒适区。因为我们有远大的目标在前面指引,完成日常的工作只不过是个人成长路上的台阶。
也许你会说,我的工作不能给我个人成长所需的机会,怎么办呢?实际上,别人只会关心你是否完成工作,成长是自己的事情,很多机会都要靠自己争取,前面提到的那些具体做法完全是你可以在工作范围内,自己努力的事情。
如果你当前的工作已经不能给你提供足够好的问题,那就去寻找一份更有挑战性的工作。在 IT 行业,跳槽似乎是一件很常见的事,但很多人跳槽的时候,并不是以提升自己为目标的。造成的结果是,不断地做同一个层面的工作,自然也就很难提升自己的水平。
为什么程序员都愿意到大厂工作?因为那里有高水平的人和好的问题。但如果只是到大厂去做低水平的事,那就是浪费时间了。所以,即便你真的想到大厂工作,与谁一起工作,做什么事,远比进入大厂本身要重要得多。
如果你真的能够不断向前进步迟早会遇到前面已经没有铺就好的道路这时候就轮到你创造一个工具给别人去使用了。比如2012年我在项目中受困于集成问题却找不到一个我想要的、能在单元测试框架里用的模拟服务器于是我写了 Moco。
最后我还想鼓励你分享所得。我在《28 | 结构化:写文档也是一种学习方式》中和你说过,输出是一种将知识连接起来的方式,它会让人摆脱固步自封,也会帮你去创造自己的行业影响力,机会会随着你在行业中的影响力逐渐增多,有了行业影响力,你才有资格成为行业专家。
当你成为了一个行业级别的专家,就可以在这条路上一直走下去,而不必担心自己是不是拼得过年轻人了,因为你也在一直前进!
总结时刻
程序员是一个充满焦虑的群体,焦虑的本质是对未来的不确定。工作在这个时代的程序员是一个特殊的群体,一方面,这个大时代为我们创造了无数的机会,另一方面,因为程序员是一个新的行业,所以,很多人不知道未来是什么样子的,焦虑颇深。
从目前的发展来看IT 行业依然是一个非常有前景的行业,但想在这条路上走好,需要我们成为 “T ”型人才,也就是“一专多能”。一专多能的前提是“一专”,让自己成为某个方面的专家。这个专家要放在行业的标准去看,这才能降低因为一个公司的波动而造成的影响。
成为行业专家,要向行业的大师学习,给自己定下一个高的目标,然后是脚踏实地,找适合自己的问题去解决,让自己一直在学习区成长。
如果今天的内容你只能记住一件事,那请记住:在学习区工作和成长。
最后,我想请你分享一下,你有哪些保持自己竞争力的心得呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,233 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 “综合运用”主题内容的全盘回顾
你好,我是郑晔。
又到了我们划重点的时间了,因为篇幅关系,“综合运用”这个模块最为短小精悍。
在这个模块中,我们把前面学到的各种知识综合起来,运用在实际的工作场景中,让你知道这些内容并不是一个个孤立的实践,在实际工作中,唯有将它们结合起来,才能发挥最大功效。
重点复习
在这个模块中,我们学习到了一些新知识。
“学习区”学习模型
舒适区,舒适而缺乏成长。
恐慌区,超出能力范围。
学习区,有难度而可以达成。
在学习区练习才能得到足够的成长。
T 型人才,一专多能
知识的广度。
专业技能的深度。
有“一专”,“多能”才是有意义的。
在这个模块中,我们还了解了一些重要的思路,让我们把工作做得更好。
进入新工作,从全面了解开始
业务:做什么。
技术:怎么做。
团队运作:怎么与人协作。
从大到小,由外及内地了解工作。
面对遗留系统,稳扎稳打,小步前行
基础理念
烂代码只是现象,要了解根因。
能重构,先重构,大规模改造是迫不得已的选择。
小步前行。
实际操作
构建测试防护网。
将大系统分解成小模块,逐步替换。
新旧模块并存,由分发模块调度。
建立好领域模型。
寻找行业对于系统构建的最新理解。
程序员的职业发展
程序员的焦虑来自于对未来的不确定性,这种不确定性是一个特定时代加上特定行业的产物。
快速发展的中国经济。
程序员在中国是一个新兴职业。
成为行业专家,制定高目标。
向大师学习,开拓视野。
找到好的问题,和高水平的人一起工作。
实战指南
了解一个项目,从大图景开始。-
——《38 | 新入职一家公司,怎么快速进入工作状态?》
小步改造遗留系统,不要回到老路上。-
——《39 | 面对遗留系统,你应该这样做》
在学习区工作和成长。-
——《40 | 我们应该如何保持竞争力?》
额外收获
在这个模块的最后,针对大家在学习过程中的一些问题,我也进行了回答,帮你梳理出一个思路,更好地理解学到的内容:
推行新观念,找愿意改变的人,做具体的事。
Lead by Example.
外部系统应该用接口隔离这种做法体现了接口隔离原则ISP也是防腐层概念的体现。
外部系统的测试,能模拟的就模拟,能本地的就本地。
留言精选
关于入职一家新公司,怎么快速进入工作状态这个问题,西西弗与卡夫卡 同学分享了他的方法:
有朋友正在转型,从乙方商业化产品的交付经理转向新公司的产品经理。原本得心应手的思维方式和工作习惯,遇到了巨大挑战。以前只需依据已有产品的功能出解决方案,能做就能做,不能实现就是不能实现,到某个时间交付什么功能很明确,考核是以交付签字为准。现在需要面对各方需求,自己想明白用户真正的问题是什么,最终要交付的价值是什么,没有一个实体的谁人来签字,只有不断地迭代。
借鉴领域驱动设计,可以采用以下方法。简单描述的话,是一个点、一个圈再加一个箭头线,是不是有点像丘比特?
一个“点”,指的是用户核心价值。这是最关键的一条,基本上只能靠自己想明白。想,不是闭门造车式的苦思冥想,可以是已有的领域经验,可以从书本中学习,可以是大家的各种吐槽,可以是自己从旁边观察用户的实践,还可以是自己变身为用户的实践。
有些人会纠结“点”想的对不对,迟迟不敢动手。其实一开始想得对不对不是那么重要,关键是要有这“点”,然后快速到市场上验证,根据反馈再调整。
一个“圈”,指的是围绕核心价值划出的范围,即领域驱动设计中的限界上下文。产品经理面临的一个现实是,各种人都会给你提需求,只要他们觉得和你有关,还时不时来问什么时候可以实现。
需求轰炸之下很容易焦虑,不光自己焦虑,所有的利益相关者都会焦虑。依据核心价值,框出需求范围,在和各方交流过程中可以有一种确定性,减少焦虑,利于行动。
大家(不光是研发团队,也包括其他需求方)就能明白,哪些和当前核心价值密切相关,我们优先考虑;哪些与核心价值有关但它不在我们的范围内,属于其他团队,需要他们协助;哪些有关系,但目前没想清楚价值大不大,并且代价可能很高建议先搁置。范围不是一成不变,它随着时间会发生变动,所以我们不要追求固定,只要保证在某个时间段内,大家一致认同即可。
一个“箭头”,指的是实现路径,箭头指向核心目标(核心价值)。目标(核心价值)和范围描绘的是终极,而从现实到终极还有很多路要走,可能的路径还有很多条。我们需要琢磨怎么走更稳当,怎么走代价比较低,路上关键的里程碑是什么。路径对不对是其次,重要的是思考过程,可以把关键点需要交付的价值、需要支持的资源等等梳理清楚。
另外,西西弗与卡夫卡 同学还对于程序员如何保持竞争力的问题给出了非常不错的建议。
补充我的一些做法。工作中不要满足当前需求,要经常从自己上级主管甚至老板角度来审视自己的工作,思考业务的终极目标,持续琢磨扩展边界,挑战工作难度。
平时多看书多思考,除了钻研某个领域,还要多有涉猎,拓展领域,成为终身学习者。
适当运动维持健康,你有更多体力和更强抗压能力的时候,就可以超过不少人。
保持竞争力除了上述之外,要保持乐观,相信大多数事都有解决方法,在多数人都容易放弃的时候,你的坚持,就是竞争力。
对于新入职一家公司的场景Y024 同学分享了他快速进入工作状态的方法:
1.我会在权限允许的范围内,时不时的到处翻翻 ftp、内部 wiki 等资源,星星点点构建全貌(业务、技术、团队)。
2.梳理系统数据流。去年很火的电视剧「大江大河」里,宋运辉初入职场的方式就很值得借鉴:先走通全部流程,有个全貌,利用图书馆、师傅等资源再自己动手各个击破并绘制流程图,最终实践检验认知,以技术说话融入团队。
(他就每天只要天气晴朗,绕着设备上上下下、里里外外地跑。一个星期下来,全部流程走通;两个星期不到,原理搞通,仪表能读,普通故障能应付;第三星期开始,他可以开出维修单,但得给师父过目;第四星期起,谁有事请假他可以顶上,坐到仪表盘前抄表看动态做操作。师父说他学得很快。
第四星期起,没人可以让他顶替时候,他在仪表室后面支起绘图板。先画出工艺流程图,经现场核对无误,又让师父审核后,开始按部就班地根据液体走向,测绘所有设备的零件图、装配图、管段图等。
这工作最先做的时候异常艰难,首先是绘图不熟练,很多小毛病,尤其是遇到非标零件,还得到机修工段测绘,有时一天都绘不成一个小小非标件。如果车间技术档案室有图纸还好,可以对照着翻画,可档案室里的图纸残缺不全,前后混乱,想找资料,先得整理资料。
资料室中年女管理员乐得有个懂事的孩子来帮她整理,索性暗暗配把钥匙给宋运辉,要是她下班不在的时候,让宋运辉自己偷偷进来关上门寻找资料。
机修工段的人本来挺烦这个宋运辉,说他一来维修单子多得像雪片,支得他们团团转,有人还趁宋运辉上班时候冲进控制室指桑骂槐,被寻建祥骂了回去,差点还打起来。但后来集中一段维修高峰后,维修单子又少了下去,上面还表扬跑冒滴漏少很多,一工段和机修工段各加一次月奖,可见设备性能好转。
再以后遇到维修,他们不能确定要用什么零件,打个内线电话给控制室问宋运辉,一问就清楚。双方关系渐渐变得铁起来。基层有时候很简单,只要拿得出技术,别人就服。
另外Y024 同学还很认真地整理了专栏提到的部分图书:
郑老师拍案惊奇书单及简评,最近各大书店有活动,可以借机囤起来了。
1.重构-
作者: Martin Fowler-
https://book.douban.com/subject/1229923/-
严格说来,我并没有完整的读完这本书,不过,正如作者自己所说,这样的书原本就不指望能够读完,因为有一大部分其实是参考手册。正是我读过的部分让我知道了重构,让我知道这么做可以把代码写得更好。
2.敏捷软件开发-
作者: Robert C·Martin-
https://book.douban.com/subject/1140457/-
这是一本名字赶潮流,内容很丰富的书,这本书让我开始理解软件设计,从此不再刻意追求设计模式。
3.测试驱动开发-
作者: Kent Beck-
https://book.douban.com/subject/1230036/-
读的是英文版,因为当时中文版还没有出版,所以,我不敢说,我通过这本书很好的理解了测试驱动开发,但它却为我打开了一扇门,让我知道了一种更好的工作方式。
4.修改代码的艺术-
作者: Michael Feathers-
https://book.douban.com/subject/2248759/-
这是一本讲解如何编写测试的书。至于这本书的具体内容我的评价是实用。如果说不足那么这本书缺少一个列表就像Martin Fowler为《重构》所做的那样出什么样的问题应该采用怎样的手法进行处理。
对于如何面对遗留系统, 毅 同学提到:
1.了解原系统已实现的功能,没有文档就在心中划分好内部功能模块;-
2.各模块的边界及关联,对于业务交叉点先思考通信机制;-
3.看代码,通常是瓶颈优先,业务上是先复杂后简单;-
4.选定切入点;-
5.正式改造时先把原有功能抽象出来使用现有实现,改造的过程完成前不会受影响;-
6.改造完成后切换到新实现进行测试;-
7.稳定后替换旧实现;-
8.重复4-7。
Wei 同学对于“T型人”的说法感触很深
“T型人”这个太说到点了到底是做“专”还是做“广”哪条路线一直是我思考的方向工作上跟大牛工作过给我感觉几乎是全能的我一直都想像他们那样做一个多面手但是如何做广这一直是困扰我的一个问题。
我是dev出身但是现实遇到的问题往往跟数据库发布的平台相关这样说下来各种相关领域数据库、k8s、网络协议、DNS 都需要大量时间去积累有时候什么都懂一点反而让自己应该定位什么角色感到迷茫了掌握的水平不足以让自己去应聘DBA、Ops但是只是应聘dev似乎又有点“浪费”跟那些熟悉最新语言/框架的对比起来没特殊竞争力。
今天学习“T型人”这个概念让我好好思考了自己到底应该怎么定位。我首先是一个developer这个是根对语言特性的熟练掌握各种best practices例如课程中提到的TDD等应该熟练应用起来然后在这上面拓展学习架构知识多思考对不同系统应该怎么设计老师提到的DDD会认真学习应用再有软件最终还是给用户使用而不是单单提交代码。相关的数据库、k8s、监控运用根据实际遇到的问题再学习解决。
最重要的是,在学习区终身学习和工作!
对于如何持续保持竞争力的问题enjoylearning 同学提到:
程序员如何保持竞争力很重要在这个年轻人学习能力不断提升的IT行业作为老程序员经验阅历眼光以及技术前沿判断力就显得越来越重要。
说起来这个职业是一个需要终身学习的职业,年龄不重要,能力才重要,是不是让自己永远呆在学习区更重要。
对于技术推广desmond 同学的理解也很棒:
技术推广,不要先推广最难的部分,先推广能让对方感到最明显好处的部分。取得对方的信任,是友好沟通的基础。
感谢同学们的精彩留言。我们的专栏更新已经进入尾声阶段,后续我会为大家做一些对整个专栏进行全盘复习的内容,敬请期待。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,211 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 “自动化”主题的重点内容回顾汇总
你好,我是郑晔。
“自动化”模块终于全部更新完毕。至此,四个工作原则我已经给你全部介绍了一遍,相对而言,这个模块的内容比较“硬”,我也竭尽全力帮你串起更多知识的脉络,所以,信息量也是非常大的。希望你能够找到自己接下来努力的方向,不断提升自己的“硬实力”。
重点复习
在这个模块中,我们学习到了一些最佳实践。
持续交付
将生产部署纳入了开发的考量。
持续交付的基础设施通常包含持续集成环境、测试环境、预生产环境和生产环境。
构建流水线保证到了下游的交付物一定是通过上游验证的。
随着 Docker 的诞生,交付由发布包变成了 Docker 镜像。
DevOps
将开发和运维结合到一起。
环境配置工具上的进步,让基础设施即代码成了行业共识。
验收测试
验收测试要站在业务的角度编写。
BDD 是一种编写验收测试的方式。
Given…When…Then… 的描述给了一个描述业务的统一方式。
写好验收测试,需要构建测试模型。
SOLID 原则
设计模式背后的道理。
单一职责原则Single responsibility principleSRP
开放封闭原则Openclosed principleOCP
Liskov 替换原则Liskov substitution principleLSP
接口隔离原则Interface segregation principleISP
依赖倒置原则Dependency inversion principleDIP
用好单一职责原则,前提条件是看待问题颗粒度要小。
DDD
它将思考的起点拉到了业务上。
DDD 分为战略设计和战术设计。
微服务
做好微服务的前提是划分好限界上下文。
微服务的第一步,不要划分微服务。
在这个模块中,我们还了解了一些重要的思路,让我们把工作做得更好。
程序员的三大美德懒惰、急躁和傲慢Laziness, Impatience and hubris
小心 NIH 综合症Not Invented Here Syndrome
写好构建脚本,做好项目自动化。
参照 Java 知识体系,学习运维知识。
软件设计最基础的原则是“高内聚、低耦合”。
分层架构是一种设计上的分解。
不同业务量的系统本质上不是一个系统。
采用简单技术解决问题,直到问题变复杂。
实战指南
请谨慎地将工作自动化。-
——《29 | “懒惰”应该是所有程序员的骄傲》
将你的工作过程自动化。-
——《30 | 一个好的项目自动化应该是什么样子的?》
有体系地学习运维知识。-
——《31 | 程序员怎么学习运维知识?》
将部署纳入开发的考量。-
——《32 | 持续交付:有持续集成就够了吗?》
将验收测试自动化。-
——《33 | 如何做好验收测试?》
把函数写短。-
——《34 | 你的代码是怎么变混乱的?》
构建好你的领域模型。-
——《35 | 总是在说MVC分层架构但你真的理解分层吗
用简单技术解决问题,直到问题变复杂。-
——《36 | 为什么总有人觉得5万块钱可以做一个淘宝
学习领域驱动设计。-
——《37 | 先做好DDD再谈微服务吧那只是一种部署形式》
额外收获
在这个模块的最后,针对大家在学习过程中的一些问题,我也进行了回答,帮你梳理出一个思路,更好地理解学到的内容:
持续集成的延伸。
持续集成完成系统集成。
持续交付完成可部署上线。
“持续验证”完成产品想法验证。
AB 测试,用一个软件的多个版本验证想法。
Selenium 用以完成浏览器的自动化。
熟练使用快捷键。
——《答疑解惑 | 持续集成、持续交付,然后呢?》
留言精选
在讲到 “懒惰”应该是所有程序员的骄傲时jxin 同学提到:
有价值的事并不局限于事情本身。做自动化很重要,写代码很重要。但根据现有情况判断是否需要自动化,是否需要写代码也很重要。有的放矢,任务分解。权衡跟设计是件很艺术的事情,令人着迷。
另外关于持续交付Jxin 同学也提出了自己的理解:
分而治之是解决复杂问题的一大利器。持续交互就像重构中小步快走每次微调后运行测试代码验证都能保证大工程的稳步前进。同时由于单元小了所以也灵活了持续交互可以结合最小产品的理念以小成本做test收集数据后即时调整产品发展方向。
关于软件设计, 毅 同学分享了自己的感悟:
我们常说任务到手不要着急去做,要从设计入手,把时间多花在前面。工作中发现大家都是思考了才动手的,那为什么越往后偏差越大呢?
共性原因有二:一是全局观不够,用咱们课里的话说就是上下文局限和反馈延迟(看到问题不提,直到代码写到那绕不过去了再沟通);
二是没有领域的概念和有意识地去实践纸上谈兵尤其是做流程型任务都喜欢先把表结构定义出来再去生成实体所以从领域层面来看这些实体就很不合适了。结果必然是用面向对象的工具写出了面向过程的代码既然是面向过程那OO设计原则就鲜有用武之地了。这两点也是我个人理解要做好软件设计的两个必要条件。
讲到分层架构时, desmond 同学提到:
学了REST和DDD感觉两者有相通的地方两者都以数据一个是资源另外一个是领域对象为中心并制定一套标准的数据操作一个是HTTP Verb另外一个项目主要用JPA这一套而核心是业务建模。
对于微服务的理解,风翱 同学提到:
公司说我们的开发方式是敏捷开发,实际上只是使用了一些敏捷开发的方法,只有遵循敏捷开发的价值观和原则,才能算是敏捷开发。微服务也是一样,不是说拆分成多个服务去部署,就叫做微服务。也不是采用市面上常用的微服务框架,就是微服务了。
对于一个好的项目自动化应该是什么样子这个问题,西西弗与卡夫卡 同学提到:
设想过这样的情景还没实现打算实践一把我们新招一名比较熟练的程序员从TA入职拿到机器到开发示意代码再提交SCM然后CI/CD再发布到线上交付给用户整个过程可以在入职当天的午饭之前完成。
这不光要求构建和集成自动化甚至要求从入职开始的各个环节都能提前准备好包括机器、开发环境、线上环境等甚至连示范的需求都要能及时传递给TA。理想情况下程序员只需要开发好程序保证质量提交到SCM即可其他事情都应该交给机器。
要知道程序员都很贵,越早给用户交付价值越好。
对于自动化验收测试, shniu 同学分享了他的学习感悟:
自动化验收测试确实是很好的东西比如在回归测试省去了很多的重复工作。但我理解BDD的初衷是驱动产品、业务、开发、测试等去深入讨论沟通需求在还没有真的写代码的时候去实例化story并一起定义验收用例让每个人对需求的理解都很透彻当然特别注意的是要从统一的业务角度去描述可见真的做好BDD是需要不断的尝试和总结的。
对于“5万块做淘宝”这个话题enjoylearning 同学提到:
做一个淘宝那样的,客户指的是业务类似,但用户量多少,需要多少并发数,搜索性能等如何都是需要跟客户沟通后才能决定技术选型的。现实中我们的有些系统已经满足了业务需求,就没有必要为了追求技术复杂度而去拆分了,只有面向问题技术选型才会有成效。
关于运维知识hua168 同学对文章内容进行了补充:
现在运维流行DevOps高级一点就是AI其中一篇文章《DevOps 详解》不错,链接如下:-
https://infoq.cn/article/detail-analysis-of-devops
《DevOps知识体系与标准化的构建》也不错下载地址-
https://yq.aliyun.com/download/778
运维知识体系:-
https://www.unixhot.com/page/ops
Web缓存知识体系-
https://www.unixhot.com/page/cache
感谢同学们的精彩留言。在下一个模块中,我将结合具体的应用场景,将之前讲过的“思考框架”和“四个原则”进行综合应用的分析。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,213 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 一次关于“沟通反馈”主题内容的复盘
你好,我是郑晔,恭喜你,又完成了一个模块的学习。
在“沟通反馈”这个模块中,我与你探讨了与人打交道的一些方法,只不过,这并非是传统意义上的谈话技巧。而是希望你能克服自己的心理障碍,主动与真实世界进行沟通,获取反馈,让自己对信息的编解码能力不断得到提升。
重点复习
在这个模块中,我们学习到了一些最佳实践。
看板
一种来自精益生产的可视化实践。
按阶段将任务放置其中。
可以帮助我们发现问题。
持续集成
做好持续集成的关键是,快速反馈。
本地检查通过之后再提交。
找到有效的反馈方式比如CI 监视器。
持续集成的纪律。
只有 CI 服务器处于绿色的状态才能提交代码。
CI 服务器一旦检查出错,要立即修复。
回顾会议
软件团队复盘的一种实践。
枚举关注点,选出重点,深入讨论,列出行动项,找到负责人。
5个为什么
又一个来自丰田的实践。
沿着一条主线追问多个问题。
在这个模块中,我们还了解一些重要的思路,让我们把工作做得更好。
用信息论理解沟通反馈
写代码的进阶路径
编写可以运行的代码。
编写符合代码规范的代码。
编写人可以理解的代码。
用业务语言写代码。
会议是一种重量级的沟通方式
减少参会人数。
找人面对面沟通。
聆听用户声音
能做自己用户,做自己的用户。
能接近用户,接近用户。
没有用户,创造用户。
Fail Fast
一种编写代码的原则。
出现问题尽早报错。
金字塔原理
从中心论点,到分论点,再到论据。
实战指南
在“沟通反馈”的模块,我也将每篇内容浓缩为一句实战指南,现在一起回顾一下。
通过沟通反馈,不断升级自己的编解码能力。-
——《20 | 为什么世界和你的理解不一样》
用业务的语言写代码。-
——《21 | 你的代码为谁而写?》
多面对面沟通,少开会。-
——《22 | 轻量级沟通:你总是在开会吗?》
多尝试用可视化的方式进行沟通。-
——《23 | 可视化:一种更为直观的沟通方式》
做好持续集成的关键在于,快速反馈。-
——《24 | 快速反馈:为什么你们公司总是做不好持续集成?》
定期复盘,找准问题根因,不断改善。-
——《25 | 开发中的问题一再出现,应该怎么办?》
多走近用户。-
——《26 | 作为程序员,你也应该聆听用户声音》
事情往前做,有问题尽早暴露。-
——《27 | 尽早暴露问题: 为什么被指责的总是你?》
多输出,让知识更有结构。-
——《28 | 结构化:写文档也是一种学习方式》
额外收获
在这个模块的最后,针对大家在学习过程中的一些问题,我也进行了回答,帮你梳理出一个思路,更好地理解学到的内容:
持续集成是一条主线,可以将诸多实践贯穿起来。
从持续集成到稳定的开发分支,到频繁提交,足够小的任务,到任务分解。
从持续集成到可检查,到测试防护网,到测试覆盖率,到单元测试,到可测试代码,到软件设计。
安全性检查,是回顾会议的前提条件。
在信息获取上,国内外程序员差别不大,开拓视野,改善工作习惯,是国内程序员亟需提高的。
——《答疑解惑 | 持续集成,一条贯穿诸多实践的主线》
留言精选
在讲到定期复盘,找准问题根因时,西西弗与卡夫卡 同学提到:
关于复盘,孙陶然曾经说过,如果他有所成就,一半要归功于复盘。他提出了几个步骤供大家参考。首先,先对比实际结果和起初所定目标之间有什么差距。其次,情景再现,回顾项目的几个阶段。然后,对每个阶段进行得失分析,找出问题原因。最后,总结规律,化作自己的技能沉淀,再次遇到时可以规避。
我再补充一点,复盘资料应该记录到知识库,无论新来的或是接手的人,都能从中获益,从而提升组织的能力。另外,好的复盘需要有坦诚的文化氛围,不然有可能变成互相指责甩锅,就失去了意义。
另外,西西弗与卡夫卡 同学还分享了提升开会效率的方法:
其他一些提升开会效率的方法,比如会前每个人要先做准备,把观点写下来,然后发给主持人。再比如六顶思考帽,大家按相近的思考角度讨论,而不是我说一趴,你说另一趴。还有,主持人控制这轮谁能发言,控制每个人的时长。方法很多,但实际上总有人破坏规则,特别是当这个人是老板…
在用信息论来讨论沟通反馈问题时,毅 同学将知识点融会贯通,提出了自己的心得:
不同角色间的沟通:克服上下文差异,分段解码,理解偏差早发现早反馈。相同角色间的沟通,信号相同,解码能力因人而异,要有一个主导的人,控制沟通广度与深度,抓主线适可而止,此时结合任务分解,反向沙盘推演。
关于如何做好复盘like_jun 同学提到:
要让团队认识到复盘的重要性。-
让每个人都深入思考项目运作过程中遇到了哪些问题。才能做好复盘。
在讲到通过金字塔原理进行知识输出时Y024 同学丰富了金字塔原理的基本原则,具体如下:
金字塔原理的四个基本原则:“结论先行”(一次表达只支持一个思想,且出现在开头)、“以上统下”(任一层次上的思想都必须是其下一层思想的总结概括)、“归类分组”(每组中的思想都必须属于同一范畴)和“逻辑递进”(每组中的思想都必须按照逻辑顺序排列)。
前面两个特点是纵向结构之间的特点,后面两个特点则是横向结构之间的特点。以上内容收集整理自李忠秋老师的《结构思考力》,感兴趣的小伙伴可以看看。
另外对于会议Y024 同学也提出了他团队正在进行的摸索和尝试:
1.沟通的指导原则之一就是在同步沟通的时候比如开会人越少越好。而在异步沟通的时候比如E-mail涉及的听众越多越好。
2.关于开会分享下我们正在摸索的。-
a每个会开始前会议发起人在石墨文档上以“会议记录”模版我们持续形成自己的模版新建一个纪要说明议程、及讨论内容等前提内容并提前告知与会人员。会议过程中在同一个石墨文档上做纪要保证纪要可以收集全所有的笔记和行动计划。如果是关联会议则使用上次相关的石墨文档进行追加内容保持事件连贯性、完整性。-
b半小时的会议设置为 25 分钟,一小时的会议设置成 50 分钟,留有冗余量应付需要换地方等临时情况,保证所有的会议不会有成员迟到的现象。
对于领域驱动设计,小浩子 同学提到了要特别关注可变项和不变项的分离:
领域驱动设计确实是写出合适的代码结构的一项训练,程序员会不由自主地按照自己的习惯,也就是按照计算机运行逻辑去设计代码,这样的代码很容易陷入难以维护的坑。在开始动手写代码之前跟用户交流清楚,理解设计的概念、流程、使用场景、特殊情况,这些都很重要。另外我特别关注的一点是可变项和不变项的分离,因为我们的业务场景对可扩展性要求很高。
经验越丰富的程序员越能体会到“走进客户”的重要性关于这一点David Mao 同学提到:
我做了好多年的软件测试,前几年和销售一起去谈客户,才深深地体会到客户声音的重要性。客户关注的才是真需求,产品经理和开发想出来的很多是伪需求,很多不是客户想要的功能。
感谢同学们的精彩留言。在下一个模块中,我将为你分享“自动化”这个原则的具体应用。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,117 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
加餐 你真的了解重构吗?
今天3月15日Martin Fowler 《重构》第二版的中文版正式发布。前不久,人邮的杨海灵老师找到我,让我帮忙给这本书写推荐语,我毫不犹豫地就答应了,有机会为经典之作写推荐语,实属个人荣幸。
不过,我随即想到,在专栏里,我只是在谈 TDD 的时候提到了重构,并没有把它作为一个专门的话题来讲,于是,我决定给我的专栏读者加餐,专门谈谈重构,毕竟重构是几乎每个程序员都会用到的词汇。但你真的了解重构吗?
每个程序员都要做的事
作为程序员,我们都希望自己的代码是完美的。但没有代码是完美的,因为只要你的代码还有生命力,一定会有新的需求进来,而新的需求常常是你在编写这段代码之初始料未及的。
很多人直觉的选择是,顺着既有的代码结构继续写下去,这里添一个 if那里加一个标记位长此以往代码便随时间腐坏了。
如果用一个物理学术语描述这种现象,那就是“熵增”,这也就是大名鼎鼎的热力学第二定律。如果没有外部干预,系统会朝着越来越混乱的方向发展。对抗熵增的一个办法就是引入负熵,让系统变得更加有序。而在代码中引入负熵的过程就是“重构”。
调整代码这件事是程序员都会有的习惯但把这件事做到比较系统上升为“重构”这个值得推广的实践是从一个小圈子开始的这个小圈子的核心就是我们在专栏里前面提到过的两位大师级程序员Ward Cunningham 和 Kent Beck。
而真正让这个概念走出小圈子,来到大众面前的,则是 Martin Fowler 在1999年写下那本软件行业的名著《重构改善既有代码的设计》Refactoring: Improving the Design of Existing Code
Martin Fowler 的本事就在于他极强的阐述能力很多名词经过他的定义就会成为行业的流行语Buzzword重构就是其中之一。
重构这个说法可比“调整代码”听上去高级多了。时至今日,很多人都会把重构这个词挂在嘴边:“这个系统太乱了,需要重构一下。”
但遗憾的是,很多程序员对重构的理解是错的。
重构是一种微操作
你理解的重构是什么呢就以前面那句话为例这个系统太乱了需要重构一下。如果我们接着问你打算怎么重构呢一些人就会告诉你他们打算另立门户重新实现这套系统。对不起你打算做的事叫重写rewrite而不是重构refactoring
《重构》是一本畅销书,但以我的了解,很少有人真正读完它,因为 Martin Fowler 是按照两本书Duplex Book来写的这是他常用写书的风格前半部分是内容讲解后半部分是手册。
让这本书真正声名鹊起的就是前半部分,这部分写出了重构这件事的意义,而后半部分的重构手册很少有人会看完。很多人以为看了前半部分就懂了重构,所以,在他们看来,重构就是调整代码。调整代码的方法我有很多啊,重写也是其中之一。
如果真的花时间去看这本书的后半部分,你多半会觉得很无聊,因为每个重构手法都是非常细微的,比如,变量改名,提取方法等等。尤其是在今天,这些手法已经成了 IDE 中的菜单。估计这也是很多人就此把书放下,觉得重构不过如此的原因。
所以,行业里流传着各种关于重构的误解,多半是没有理解这些重构手法的含义。
重构,本质上就是一个“微操作”的实践。如果你不能理解“微操作”的含义,自然是无法理解重构的真正含义,也就不能理解为什么说“大开大合”的重写并不在重构的范畴之内。
我在《大师级程序员的工作秘笈》这篇文章中曾经给你介绍过“微操作”,每一步都很小,小到甚至在很多人眼里它都是微不足道的。
重构,也属于微操作的行列,与我们介绍的任务分解结合起来,你就能很好地理解那些重构手法的含义了:你需要把做的代码调整分解成若干可以单独进行的“重构”小动作,然后,一步一步完成它。
比如,服务类中有一个通用的方法,它并不适合在这个有业务含义的类里面,所以,我们打算把它挪到一个通用的类里面。你会怎么做呢?
大刀阔斧的做法一定是创建一个新的通用类,然后把这个方法复制过去,修复各种编译错误。而重构的手法就会把它做一个分解:
添加一个新的通用类,用以放置这个方法;
在业务类中,添加一个字段,其类型是新添加的通用类;
搬移实例方法,将这个方法移动到新的类里面。
得益于现在的 IDE 能力的增强,最后一步,按下快捷键,它就可以帮我们完成搬移和修改各处调用的工作。
在这个分解出来的步骤里,每一步都可以很快完成,而且,每做完一步都是可以停下来的,这才是微操作真正的含义。这是大刀阔斧做法做不到的,你修改编译错误的时候,你不知道自己需要修改多少地方,什么时候是一个头。
当然,这是一个很简单的例子,大刀阔斧的改过去也无伤大雅。但事实上,很多稍有规模的修改,如果不能以重构的方式进行,常常很快就不知道自己改到哪了,这也是很多所谓“重写”项目面临的最大风险,一旦开始,不能停止。
你现在理解了,重构不仅仅是一堆重构手法,更重要的是,你需要有的是“把调整代码的动作分解成一个个重构小动作”的能力。
重构地图
下面我准备给你提供一张关于重构的知识地图,帮你了解它与周边诸多知识之间的关系,辅助你更好地理解重构。
学习重构先要知道重构的定义。关于这点Martin Fowler 给出了两个定义,一个名词和一个动词。
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
之所以要了解重构的定义,因为重构的知识地图就是围绕着这个定义展开的。
首先我们要对软件的内部结构进行调整第一个要回答的问题是我们为什么要调整。Martin Fowler 对于这个问题的回答是:代码的坏味道。
代码的坏味道,在我看来,是这本书给行业最重要的启发。很多人常常是无法嗅到代码坏味道的,因此,他们会任由代码腐坏,那种随便添加 if 或标记的做法就是嗅不出坏味道的表现。
我经常给人推荐《重构》这本书,但我也常常会补上一句,如果你实在没有时间,就去看它的第三章《代码的坏味道》。
顺便说一下对比两版的《重构》你会发现它们在坏味道的定义上有所差异在新版的《重构》中可变数据Mutable Data、循环语句Loops都定义成了坏味道如果你不曾关注近些年的编程发展趋势这样的定义着实会让人为之震惊。但只要了解了函数式编程的趋势就不难理解它们的由来了。
换句话说,函数式编程已然成为时代的主流。如果你还不了解,赶紧去了解。
我们接着回到重构的定义上,重构是要不改变软件的可观察行为。我们怎么知道是不是改变了可观察行为,最常见的方式就是测试。
关于测试我在“任务分解”模块已经讲了很多你现在已经可以更好地理解重构、TDD 这些概念是怎样相互配合一起的了吧!
再来重构是要提高可理解性那重构到什么程度算是一个头呢当年重构讨论最火热的时候有人给出了一个答案重构成模式Refactoring to Patterns。当然这也是一本书的名字有兴趣的话可以找来读一读。
我个人有个猜想如果这个讨论可以延续到2008年等到 Robert Martin 的《Clean Code》出版也许有人会提“重构成 Clean Code”也未可知。所以无论是设计模式亦或是 Clean Code都是推荐你去学习的。
至此,我把重构的周边知识整理了一番,让你在学习重构时,可以做到不仅仅是只见树木,也可看见森林。当然,重构的具体知识,还是去看 Martin Fowler 的书吧!
总结时刻
总结一下今天的内容。今天我介绍了一个大家耳熟能详的概念:重构。不过,这实在是一个让人误解太多的概念,大家经常认为调整代码就是在做重构。
重构,本质上就是一堆微操作。重构这个实践的核心,就是将调整代码的动作分解成一个一个的小动作,如果不能理解这一点,你就很难理解重构本身的价值。
不过,对于我们专栏的读者而言,因为大家已经学过了“任务分解”模块,理解起这个概念,难度应该降低了很多。
既然重构的核心也是分解,它就需要大量的锤炼。就像之前提到任务分解原则一样,我在重构上也下了很大的功夫做了专门的练习,才能让自己一小步一小步地去做。但一个有追求的软件工匠不就应该这样锤炼自己的基本功吗?
如果今天的内容你只记住一件事,那请记住:锤炼你的重构技能。
最后,我想请你分享一下,你对重构的理解。欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,247 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
总复习 重新审视“最佳实践”
你好,我是郑晔。
我承诺的正文内容已经全部交付给你,恭喜你完成了整个专栏的学习!希望通过对这些内容的学习,你已经对“如何做好软件”有了一个全新的认识。
在这个专栏中,我给你讲了很多行业中的最佳实践,比如:测试、持续集成等等,但因为这个专栏叙述方式的关系,一些有关联的实践被放到了不同的模块下讲解。
所以在这一讲中,我们将按照最佳实践的维度重新审视这些内容。我会将这些知识重新串联起来,帮你做一个对专栏的整体复习。
产品
做产品很多时候是面向不确定性解决问题。目前这方面最好的实践是“精益创业”。对于精益创业的最简单的理解就是“试”。试也有试的方法精益创业提出了一个“开发build- 测量measure- 认知learning”这样的反馈循环通过这个循环得到经过验证的认知Validated Learning
既然是对不确定产品特性的尝试最好的办法就是低成本地试。在精益创业中最小可行产品MVP就是低成本的试法。最小可行产品就是“刚刚好”满足用户需求的产品。理解这个说法的关键在于用最小的代价尝试可行的路径。
在产品的打磨过程中,可以采用用户测试的方式,直接观察用户对产品的使用。作为程序员,我们要尽可能吃自家的狗粮,即便你做的产品不是给自己使用的产品,也可以努力走近用户。
精益创业-
相关阅读《06 | 精益创业:产品经理不靠谱,你该怎么办?》
最小可行产品MVP-
相关阅读《19 | 如何用最小的代价做产品?》
用户测试、验证产品特性、吃自家狗粮-
相关阅读《26 | 作为程序员,你也应该聆听用户声音 》
需求
当我们确定做一个产品功能时,怎么描述需求也是很重要的。产品列表式的需求描述方式最容易出现问题的地方在于,看不清需求的全貌。
用户故事是一个好的需求描述方式:作为一个什么角色,要做什么样的事,以便达成一种怎样的效果。
在用户故事中,验收标准是非常重要的一环。即便不是采用用户故事描述需求,也依然建议先将验收标准定义清楚。
开发团队对需求的理解普遍偏大,基本上都是一个主题。在开发之前,先将需求拆分成小粒度的。衡量一个用户故事拆分是否恰当,一个标准是 INVEST 原则。有了拆分出来的用户故事,就可以进行估算了,估算的过程也是对需求加深理解的过程,过大的用户故事应该再次拆分。
当我们有了拆分之后的需求,就可以对需求进行优先级讨论了。先做重要性高的事,而不是一股脑地去做所有的需求。只有分清了需求的优先级,才能方便地对需求进行管理。
用户故事-
相关阅读《04 | 接到需求任务,你要先做哪件事? 》
需求的分解与估算-
相关阅读《17 | 程序员也可以“砍”需求吗?》
需求管理、优先级-
相关阅读《18 | 需求管理:太多人给你安排任务,怎么办?》
持续集成
在开发中,写出代码并不是终点,我们要把代码集成起来。集成要经常做,改动量越小,集成就可以做得越频繁,频繁到每次提交都去集成,这就是持续集成。
持续集成发展到今天已经是一套完整的开发实践。想要做好持续集成,你需要记住持续集成的关键是“快速反馈”。
怎样快速得到反馈。
怎样反馈是有效的。
持续集成,可以继续延展,将生产部署也纳入其中,这就是持续交付。如果持续交付,再向前一步,就可以同产品验证结合起来。
持续交付的关键点,是在不同的环境验证发布包和自动化部署。不同的环境组成了持续交付的构建流水线,而自动化部署主要是 DevOps 发挥能力的地方。持续交付的发展,让交付物从一个简单的发布包变成了一个拥有完整环境的 Docker 镜像。
持续集成和持续交付可以将诸多的实践贯穿起来单元测试、软件设计、任务分解、主分支开发、DevOps 等等。所以,如果一个公司希望做过程改进,持续集成是一个好的出发点。
持续集成发展史-
相关阅读《05 | 持续集成:集成本身就应该是写代码的一个环节》
快速反馈-
相关阅读《24 | 快速反馈:为什么你们公司总是做不好持续集成?》
持续集成,贯穿诸多实践-
相关阅读:《答疑解惑 | 持续集成,一条贯穿诸多实践的主线 》
持续交付-
相关阅读《32 | 持续交付:有持续集成就够了吗?》
与产品结合:持续验证-
相关阅读:《答疑解惑 | 持续集成、持续交付,然后呢? 》
测试
测试是一个典型的程序员误区,很多程序员误以为测试只是测试人员的事。理解了软件变更成本,知道了内建质量之后,我们就应该清楚,测试应该体现在全部的开发环节中。这一思想在开发中的体现就是自动化测试。
想要写好自动化测试,需要先理解测试金字塔,不同的测试运行成本不同。为了让软件拥有更多的、覆盖面更广的测试,需要多写单元测试。
编写测试的方式有很多一种实践是测试驱动开发TDD。先写测试然后写代码最后重构这就是 TDD 的节奏:红——绿——重构。测试驱动开发的本质是测试驱动设计,所以,编写可测试的代码是前提。
要想做好 TDD一个重要的前提是任务分解分解到非常小的微操作。学会任务分解是成为优秀程序员的前提条件。
想写好测试需要懂得好测试是什么样子的避免测试的坏味道。好测试有一个衡量标准A-TRIP。
我们不只要写好单元测试还要站在应用的角度写测试这就是验收测试。验收测试现在比较成体系的做法是行为驱动开发BDD它让你可以用业务的语言描述测试。
单元测试、自动化测试、蛋卷和冰淇淋模型-
相关阅读《12 | 测试也是程序员的事吗?》
测试驱动开发-
相关阅读《13 | 先写测试,就是测试驱动开发吗?》-
相关阅读《14 | 大师级程序员的工作秘笈 》
测试练习-
相关阅读《15 | 一起练习:手把手带你拆任务 》
简单的测试、测试的坏味道、A-TRIP-
相关阅读《16 | 为什么你的测试不够好? 》
验收测试、写好验收测试用例-
相关阅读《32 | 持续交付:有持续集成就够了吗?》
外部系统测试,用接口隔离-
相关阅读:《答疑解惑 | 如何在实际工作中推行新观念? 》
编码与设计
编码和设计,是软件开发中最重要的一环。在我看来,编码和设计是一体,想清楚才能写出好代码。很多程序员追求写好代码,却没有一个很好的标准去衡量代码的好坏。结合着软件设计的一些理念,我给你一个编写好代码的进步阶梯,希望你能达到用业务语言编写代码的程度。
用业务语言编写代码需要对软件设计有着良好的理解。提到设计人们的初步印象是“高内聚低耦合”但这是一个太过高度抽象的描述。SOLID 原则是一个更具实践性的指导原则,有了原则做指导,就可以更好地理解设计模式了。
有了基础原则,我们会知道将不同的代码划分开,这样就产生了分层。好的分层可以构建出抽象,而其他人就可以在这个抽象上继续发展。对于程序员来说,构建自己的核心抽象是最关键的一步。
目前构建核心抽象最好的方式是领域驱动设计DDD它将我们思考的起点拉到了业务层面通过战略设计将系统按照不同的上下文划分开来再通过战术设计指导我们有效地设计一个个的领域模型。
但无论怎样做设计,前提是使用适当的技术解决适当的问题,不要把技术用复杂,把团队带入泥潭。
业务语言写代码-
相关阅读《21 | 你的代码为谁而写?》
架构设计-
相关阅读《34 | 你的代码是怎么变混乱的? 》
分层、抽象-
相关阅读《35 | 总是在说MVC分层架构但你真的理解分层吗
业务与技术-
相关阅读《36 | 为什么总有人觉得5万块钱可以做一个淘宝
微服务-
相关阅读《37 | 先做好DDD再谈微服务吧那只是一种部署形式 》
项目准备
从头开始一个项目时一个好的实践就是把一切都准备好。迭代0就是这样一个把迭代准备好的实践从需求到技术做好充分的准备工作再开启项目你会显得从容不迫。在技术方面迭代0最重要的准备工作就是构建脚本它是后续很多工作的基础比如持续集成。
迭代0做基础的准备-
相关阅读《10 | 迭代0: 启动开发之前,你应该准备什么?》
构建脚本,让项目一开始就自动化-
相关阅读《30 | 一个好的项目自动化应该是什么样子的? 》
其余的最佳实践
除了几个花大篇幅介绍的最佳实践,我们还提到了很多不同的最佳实践。
DoD
完成的定义DoD是一个确保合作各方理解一致的实践。它是一个清单由一个个检查项组成每个检查项都是实际可检查的。有了 DoD做事就只有两种状态完成和未完成。
完成的定义DOD-
相关阅读《03 | DoD价值你完成了工作为什么他们还不满意
站会
站会,一种轻量级的会议形式,用来同步每天发生的事情。一般来说,只说三件事:昨天做了什么,今天打算做什么,遇到了什么问题。
站会-
相关阅读《22 | 轻量级沟通:你总是在开会吗? 》
看板
看板,一种项目管理工具, 将正在进行的工作可视化。通过看板,可以发现团队正在进行工作的很多问题。看板有实体和电子之分,可以根据自己的项目特点进行选择。
看板-
相关阅读《23 | 可视化:一种更为直观的沟通方式 》
回顾会议
回顾会议,是一种复盘实践,让团队成员对一个周期内发生的事情进行回顾。回顾会议一般分为讲事实、找重点和制定行动项三个部分。但在开始回顾之前,会先进行安全检查,确保每个人都能放心大胆地说真话。
回顾会议-
相关阅读《25 | 开发中的问题一再出现,应该怎么办? 》
回顾会议中的安全检查-
相关阅读:《答疑解惑 | 持续集成,一条贯穿诸多实践的主线 》
重构
重构是程序员的基本功把调整代码的动作分解成若干可以单独进行的“重构”小动作一步步完成。重构的前提是识别代码的坏味道。保证代码行为不变需要有测试配合而重构的方向是重构成模式Refactoring to Patterns。重构的过程和编写代码的过程最好结伴而行最佳实践就是测试驱动开发。
重构-
相关阅读:《加餐 | 你真的了解重构吗?》
在测试驱动开发中重构-
相关阅读《13 | 先写测试,就是测试驱动开发吗?》
分支开发
分支开发模型,是每个团队都要面临的问题。行业中有两种常见的分支模型,一种是基于主干的开发模型,一种是分支开发模型。分支开发符合直觉,却不是最佳实践。主分支开发模型是与其他实践配合最好的模式,但也需要开发者有着良好的开发习惯。如果并行开发多个功能,可以考虑 Feature Toggle 和 Branch by Abstraction。
分支开发-
相关阅读《14 | 大师级程序员的工作秘笈 》
Feature Toggle 和 Branch by Abstraction-
相关阅读:《答疑解惑 | 如何分解一个你不了解的技术任务? 》
Fail Fast
Fail Fast 是一个重要的编程原则:遇到问题,尽早报错。不要以构建健壮系统为由,兼容很多奇怪的问题,使得 Bug 得以藏身。
Fail Fast-
相关阅读《27 | 尽早暴露问题: 为什么被指责的总是你? 》
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,135 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
总复习 重新来“看书”
你好,我是郑晔。
我们继续复习,在上一讲中,我从最佳实践的角度带领大家回顾了专栏里的一些内容。这一讲,我们换个复习的角度。在专栏进行的过程中,有一些同学注意到我引用了大量的书籍,提出让我把这些书做一个整理。
Wei 同学提到:
有一个小建议: 在每一个主题模块的小结中,把文章中提到的书籍做一个书单方便读者。
刘晓林 同学提到:
郑老师在专栏中推荐了很多非常好的书籍作为参考,可否考虑在某一期中,将这些参考书籍整理成一个书单,按照专栏的主题做个小分类,然后每本书简单点评两句作为领读内容。希望在专栏的结束语之前可以看到这个书单。
Y024 同学甚至在留言中帮我总结了一个小清单,而有人也在豆瓣上做出了一个豆列,罗列了专栏中提到的一些书。
在今天这一讲中,我就站在“看书”的视角,带着你进行一次复习。这些书大多是在我个人成长过程中,给我留下深刻印象的。
我希望你在结束这个专栏学习之后,开启的是另外一段学习历程,用这些书提升自己的水平,夯实自己的基础知识。学习了这个专栏之后,你拥有了一个新的知识结构,再来看这些书就会有一种全新的体验。
此外,在这次的内容中,我会提到几本专栏中没有提到的书,算是给你在学习路上的一个补充。我还制作了一个豆列,方便你去找到这些书。
编码实践
如果你想详细学习如何写好代码,我推荐你去读 Robert Martin 的《代码整洁之道》Clean Code这本书几乎覆盖了如何把代码写好的方方面面。
《实现模式》是一本关于如何写好代码的书,更具体一点是,编写别人能够理解的代码。它的作者 Kent Beck 是许多软件开发实践的开创者。但 Kent Beck 的写作能力一般,他的很多作品被埋没了。只有细细品味,才能体会到 Kent Beck 深厚的功力。
我提升自己编码水平的理解是从《程序设计实践》The Practice of Programming这本书开始的这本书的作者是 Brian Kernighan 和 Rob Pike这两个人都出身于大名鼎鼎的贝尔实验室参与过 Unix 的开发。
如果你想从日常开发中提升自己的效率,可以读一下《卓有成效的程序员》。假如你不曾思考过这个问题,这本书会让看到一些不同的工作方式,我也给这本书写过一篇书评。不过,这本书里的技巧太具体了,所以,有一些已经有些过时了。
设计
SOLID 原则是一种面向对象软件设计原则。早在1995年Robert Martin 就提出了这些设计原则的雏形,然后在他的《敏捷软件开发:原则、实践与模式》这本书中,比较完整地阐述了这五个原则,后来,他有把这些原则进一步整理,成了今天的 “SOLID”。有了设计原则做基础这本书后面讲了设计模式理解起来就容易多了。虽然书名是关于敏捷的但这是一本讲设计的书。
设计和架构有什么区别2017年Robert Martin 出版了《架构整洁之道》Clean Architecture他在其中告诉我们二者没有区别。所以这也是一本关于设计的书给出了 Robert Martin 对设计的最新理解。你可以把它看成《敏捷软件开发:原则、实践与模式》的修订版。
《设计模式》不推荐阅读,它是设计模式的开山之作,但它的起点是 Erich Gamma 的博士论文其写作风格偏向学术而且中文版翻译得也很一般。这里将它罗列出来只是因为其历史重要性。如果你想学习设计模式现在有一些更容易入门的书比如《Head First 设计模式》。
Martin Fowler 的《企业应用架构模式》将软件开发当时常见的解决方案汇集成模式今天看来很多模式已经习以为常但当年出场可是技惊四座的。从这本书的名字你不难看出它出版的年代是企业级开发盛行的年代。Martin Fowler 一直认为这本书没有写完,希望能够继续更新,但不知道何时能看到这本书的新版。
《Unix 编程艺术》也是一本讲软件设计的书,只不过,它选择的切入点是 Unix 中的设计,从中你可以学到“只做一件事,把它做好”、“文本化”等编程理念,有助于你改善日常的工作。这样的书,也就只有 Eric Raymond 这样沉浸编程几十年的人才能写出来。
工程实践
Kent Beck 有一本知名的软件工程之作《解析极限编程》Extreme Programming Explained它介绍了一种软件开发方法极限编程。但更重要的是今天很多主流的软件开发最佳实践都是从这里出来的。这本书可以理解成诸多最佳工程实践的总纲。
Martin Fowler 在1999年写下软件行业的名著《重构改善既有代码的设计》Refactoring: Improving the Design of Existing Code把重构这个小圈子实践带到了大众视野。2018年底Martin Fowler 时隔近20年后又写出了《重构》第二版。把他对这些年行业发展的新理解融入到重构实践中。重构应该有个目标这个目标就是“重构成模式”而这也是一本专门的书《重构与模式》Refactoring to Patterns
《测试驱动开发》是 Kent Beck 为世人展示 TDD 做法的一本书。它好的地方需要自己体会Kent Beck 并没有显式的讲出来,比如:任务分解。
Jez Humble 和 Dave Farley 的《持续交付》Continuous Delivery让持续集成再进一步将生产环境纳入了考量。乔梁他是《持续交付》这本书的中文版译者而且在这本书出版近十年后他自己写了《持续交付 2.0》,把自己多年来关于持续交付的新理解整理了进去。
说到遗留代码和测试我推荐一本经典的书Michael Feathers 的《修改代码的艺术》Working Effectively with Legacy Code从它的英文名中你就不难发现它就是一本关于遗留代码的书。如果你打算处理遗留代码也建议你读读这本书。这本书我也写过书评你可以了解一下我对它看法。
领域驱动设计
Eric Evans 2003年写了《领域驱动设计》向行业介绍一下 DDD 这套方法论立即在行业中引起广泛的关注。但实话说Eric 在知识传播上的能力着实一般,这本关于 DDD 的开山之作,其写作质量却难以恭维,想要通过它去学好 DDD是非常困难的。所以在国外的技术社区中有很多人是通过各种交流讨论逐渐认识到 DDD 的价值所在,而在国内 DDD 几乎没怎么掀起波澜。
2013年在 Eric Evans 出版《领域驱动设计》十年之后DDD 已经不再是当年吴下阿蒙有了自己一套比较完整的体系。Vaughn Vernon 将十年的精华重新整理,写了一本《实现领域驱动设计》,普通技术人员终于有机会看明白 DDD 到底好在哪里了。所以,你会发现,最近几年,国内的技术社区开始出现了大量关于 DDD 的讨论。
因为《实现领域驱动设计》实在太厚Vaughn Vernon 又出手写了一本精华本《领域驱动设计精粹》,让人可以快速上手 DDD这本书也是我向其他人推荐学习 DDD 的首选。
产品与需求
精益创业是 Eric Ries 最早总结出来的。他在很多地方分享他的理念不断提炼最终在2011年写成一本同名的书《精益创业》。如果说精益创业是理论《精益创业实战》这本书则给了你一个操作流程。
Mike Cohn 是敏捷理念的一个重要传播者我们在讲测试金字塔时提到了他的著作《Scrum敏捷软件开发》Succeeding with Agile。敏捷开发有两大流派一派是工程实践另一派是管理实践。如果你对 Scrum 这类管理实践感兴趣,可以读一下这本书。
如果你对用户故事这个话题感兴趣,推荐阅读 Mike Cohn 的两本书《用户故事与敏捷方法》User Stories Applied和《敏捷软件开发实践 估算与计划》Agile Estimating and Planning
开发文化
软件行业里有一本名著叫《人月神话》这算是软件开发领域第一本反思之作。今天我们讨论的很多词汇都出自这本书比如没有银弹、焦油坑等等。虽然这本书出版于1975年但其中提到的问题依然困扰着今天的程序员。
开源概念的提出者 Eric Raymond他的《大教堂与集市》推开了开源大门。今天开源软件已经成为程序员日常工作的一部分但如果没有 Eric Raymond 这些人的努力,我们还必须与复杂的企业级软件搏斗。了解一下开源的历程,可以帮助你更好地理解今天的幸福。
程序员应该如何做Robert Martin 也写了一本书《程序员的职业素养》Clean Coder其中对大多数程序员最重要的一点建议是说“不”。
软件开发拾遗
高德纳的《计算机程序设计艺术》肯定是一套程序员都知道,但没几个人读完的书。算法的讲解经过几十年已经有了很好的发展,如果学算法,肯定有更好的选择。如果你想看图灵奖获得者如何从根源上思考问题,不妨找来这套书来翻翻。
《快速软件开发》Rapid Development不推荐阅读。在这本书中作者首次提出了解决集成问题的优秀实践Daily Build每日构建。通过这个名字我们便不难看出它的集成策略即每天集成一次。其中很多实践在当时是先进的但今天看来有些落伍了。如果你只想从中收获一些理念性的东西可以去读读。
《C 程序设计语言》《Unix 编程环境》等出自贝尔实验室大师级程序员之手,他们的书都值得一读,其中的内容今天看来可能有些过时,但他们解决问题的方式和手法却值得慢慢品味。
我在讲淘宝技术变迁时,提到了《淘宝技术这十年》,这本书算不上经典,但可以当作休闲读物。
技术之外
管理大师彼得·德鲁克有一本经典著作《卓有成效的管理者》,虽然标题上带着管理者几个字,但在我看来,这是一本告诉我们如何工作的书,每个人都可以读一下。
尤瓦尔·赫拉利的《人类简史》或《未来简史》,是我第一次学到“大历史观”这个说法,历史不再是一个个单独的历史事件,而是一个有内在逻辑的发展脉络。
《从一到无穷大》是一本著名科普著作它向我们介绍了20世纪以来的科学进展。作者乔治·伽莫夫既是热宇宙大爆炸模型的提出者也是生物学上最早提出“遗传密码”模型的人。虽然这本书是1947年出版的但以现在社会的整体科学素养还是有必要读读这本书的。
史蒂芬·柯维Stephen Richards Covey的《高效能人士的七个习惯》其中的理念我在专栏两个不同的地方提到过一个是讲以终为始时那段关于智力创造的论述另一个是讲优先级时提到的艾森豪威尔矩阵。这本书值得每个人阅读很多程序员欠缺的就是这些观念性的东西。
很多程序员都是科幻小说迷,编程和科幻,这两个都是需要想象力的领域。刘慈欣的《三体》,不说它给 IT 行业带来的丰富的词汇表吧作为科幻小说来说它就是一流的值得阅读。它会让你仰望星空打开思维。如果你对科幻小说有兴趣推荐阅读阿西莫夫的《银河帝国》系列这是科幻小说界的扛鼎之作你会看到一部出版于1942年的书里就有大数据的身影。
对于程序员来说,最好的工作状态就是进入心流,它会让你忘我工作。如果你对心流的概念感兴趣,可以去读米哈里·契克森米哈赖的著作《心流》,这位作者就是心流概念的提出者。
好,今天的复习就到这里,你有哪些经典的书可以推荐给这个专栏的同学呢?欢迎在留言区写下分享你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑解惑 持续集成、持续交付,然后呢?
你好,我是郑晔。
“自动化”模块落下了帷幕,这是四个工作原则中最为“技术化”的一个,也应该是程序员们最熟悉的主题。
我从软件外部的自动化——工作流程讲起,让你能够把注意力专注于写好代码;讲到了软件内部的自动化——软件设计,选择恰当的做法,不贪图一时痛快,为后续的工作挖下深坑。
既然是一个大家都熟悉的话题,同学们自然也有很多经验分享,也有很多人看到了与自己不同的做法,提出了各种各样的问题。
在今天的答疑中,我选出了几个很有意思的问题,让大家可以在已有内容上再进一步延伸。
问题1持续交付是否可以再做扩展
毅 同学提到
为达到有效交付的目标,用户能够尽早参与,我觉得也是比较重要的一环。从生产环境获得结果,是否可再做扩展,将用户也作为一个独立节点?-
——《32 | 持续交付:有持续集成就够了吗?》
西西弗与卡夫卡 同学提到
持续交付可以是持续交付最大价值那范围就不仅限于软件还可以进一步延伸到运营比如说结合ABTest自动选择最有效的运营策略为用户交付最大价值。-
——《32 | 持续交付:有持续集成就够了吗?》
两位同学能提出这样的想法,说明真的是已经理解了持续集成和持续交付,所以,才能在这个基础上继续延伸,思考进一步的扩展。
我在专栏中一直在强调别把自己局限在程序员这个单一的角色中应该了解软件开发的全生命周期。在前面的内容中我讲了不少做产品的方法比如MVP、用户测试等等。如果只把自己定位在一个写代码的角色上了解这些内容确实意义不大但你想把自己放在一个更大的上下文中这些内容就是必须要了解的。
回到两位同学的问题上,如果说我们一开始把持续集成定义成编写代码这件事的完成,那持续交付就把这个“完成”向前再推进了一步,只有上线的代码才算完成。
但放在整个软件的生命周期来说,上线并不是终点。把系统送上线,不是最终目的。那最终目的是什么呢?
回到思考的起点,我们为什么要做一个软件?因为我们要解决一个问题。那我们是不是真正的解决了问题呢?其实,我们还不知道。
在《06 | 精益创业:产品经理不靠谱,你该怎么办?》这篇文章中,我给你讲了做产品的源头。如果是采用精益创业的模式工作,我们构建产品的目的是为了验证一个想法,而怎么才算是验证了我们的想法呢?需要搜集各种数据作为证据。
所以我曾经有过这样的想法精益创业实际上是一种持续验证验证想法的有效性获得经过验证的认知Validated Learning
现在有一些获取验证数据的方式,比如,西西弗与卡夫卡 同学提到的 AB 测试。
AB 测试是一种针对两个(或多个)变体的随机试验,常常用在 Web 或 App 的界面制作过程中,分别制作两个(或多个)版本,让两组(或多组)成分相同的用户随机访问不同版本,收集数据,用以评估哪个版本更好。每次测试时,最好只有一个变量。因为如果有多个变量,你无法确认到底是哪个变量在起作用。
AB 测试的概念在其他领域由来已久。2000年Google 的工程师率先把它应用在了软件产品的测试中,时至今日,它已经成为很多产品团队常用的做事方式。
AB 测试的前提是用户数据搜集。我在《09 | 你的工作可以用数字衡量吗?》这篇文章给你介绍了在开发过程中,用数字帮助我们改善工作。在产品领域实际上更需要用数字说话,说到这里,我“插播”一个例子。
很多产品经理喜欢讲理念、讲做法,偏偏不喜欢讲数字。用数字和产品经理沟通其实是更有说服力的。
我就曾经遇到过这样的事情,在一个交易平台产品中,一个产品经理创造性地想出一种新的订单类型,声称是为了方便用户,提高资金利用率。如果程序员接受这个想法,就意味着要对系统做很大的调整。
我问了他几个问题:第一,你有没有统计过系统中现有的订单类型的使用情况?第二,你有没有了解过其他平台是否支持这种订单类型呢?
产品经理一下子被我问住了。我对第一个问题的答案是,除了最基础的订单类型之外,其他的订单类型用得都很少,之前做的很多号称优化的订单类型,实际上没有几个人在用。
第二个问题我的答案是,只有极少数平台支持类似的概念。换句话说,虽然我们想得很美,但教育用户的成本会非常高,为了这个可能存在的优点,对系统做大改造,实在是一件投资大回报小的事,不值得!
再回到我们的问题上,一旦决定了要做某个产品功能,首先应该回答的是如何搜集用户数据。对于前端产品,今天已经有了大量的服务,只要在代码里嵌入一段代码,收集数据就是小事一桩。
前端产品还好,因为用户行为是比较一致的,买服务就好了,能生成标准的用户行为数据。对于后端的数据,虽然也有各种服务,但基本上提供的能力都是数据的采集和展示,一些所谓的标准能力只是 CPU、内存、JVM 之类基础设施的使用情况。对于应用来说,具体什么样的数据需要搜集,还需要团队自己进行设计。
说了这些,我其实想说的是,持续验证虽然是一个好的想法,但目前为止,还不如持续集成和持续交付这些已经有比较完整体系做支撑。想做到“持续”,就要做到自动化,想做到自动化,就要有标准化支撑,目前这个方面还是“八仙过海各显神通”的状态,没法上升到行业最佳实践的程度。
其实道理上也很简单,从一无所有,到持续集成、再到持续交付,最后到持续验证,每过一关,就会有大多数团队掉队。所以,真正能达到持续交付的团队都少之又少,更别提要持续验证了。
问题2Selenium 和 Cucumber 的区别是什么?
没有昵称 同学提到
老师Selenium 跟 Cucumber 有区别吗?-
——《33 | 如何做好验收测试?》
这是一个经常有人搞混的问题。为了让不熟悉的人理解,我先讲一点背景。
Selenium 是一个开源项目,它的定位是浏览器自动化,主要用于 Web 应用的测试。它最早是 Jason Huggins 在2004年开发出来的用以解决 Web 前端测试难的问题。
之所以取了 Selenium 这个名字,主要是用来讽刺其竞争对手 Mercury 公司开发的产品。我们知道Mercury 是水银,而 Selenium 是硒,硒可以用来解水银的毒。又一个程序员的冷幽默!
Cucumber 的兴起伴随着 Ruby on Rails 的蓬勃发展我们在之前的内容中提到过Ruby on Rails 是一个改变了行业认知的 Web 开发框架。所以Cucumber 最初主要就是用在给 Web 应用写测试上,而 Selenium 刚好是用来操作浏览器的,二者一拍即合。
于是你会在很多文章中看到Cucumber 和 Selenium 几乎是同时出现的,这也是很多人对于二者有点傻傻分不清楚的缘由。
讲完了这些背景结合我们之前讲的内容你就不难理解了。Cucumber 提供的是一层业务描述框架,而它需要有自己对应的步骤实现,以便能够对被测系统进行操控;而 Selenium 就是在 Web 应用测试方面实现步骤定义的一个非常好的工具。
问题3IntelliJ IDEA 怎么学?
hua168 同学提到
IDEA 怎么学呢?是用到什么功能再学?还是先看个大概,用到时再仔细看?-
——《30 | 一个好的项目自动化应该是什么样子的?》
一个工具怎么学?我的经验就是去用。我没有专门学过 IntelliJ IDEA只是不断地在使用它。遇到问题就去找相应的解决方案。
如果说在 IDEA 上下过功夫,应该是在快捷键上。我最早写代码时的风格应该是鼠标与键盘齐飞,实话说,起初也没觉得怎么样。加入 ThoughtWorks 之后,看到很多人把快捷键运用得出神入化,那不是在写一行代码,而是在写一片代码。我当时有一种特别震惊的感觉。
我自以为在写代码上做得已经相当好了,然而,有人却在你很擅长的一件事上完全碾压了你,那一瞬间,我感觉自己这些年都白学了。这种感觉后来在看到别人能够小步重构时又一次产生了。
看到差距之后,我唯一能做的,就是自己下来偷偷练习。幸好,无论是快捷键也好,重构也罢,都是可以单独练习的。花上一段时间就可以提高到一定的水平。后来,别人看我写代码时也会有类似的感觉,我会安慰他们说,不要紧,花点时间练习就好。
其实,也有一些辅助的方法可以帮助我们练习,比如,我们会给新员工发放 IntelliJ IDEA 的快捷键卡片写代码休息之余可以拿来看一下再比如IntelliJ IDEA 有一个插件叫 Key Promoter X如果你用鼠标操作它会给你提示帮你记住快捷键。有一段时间我已经练习到“看别人写代码脑子里能够完全映射出他在按哪个键”的程度。
写代码是个手艺活,要想打磨手艺,需要看到高手是怎么工作的,才不致于固步自封。如果你身边没有这样的人,不如到网上搜一些视频,看看高手在写代码时是怎么做的,这样才能找到差距,不断提高。
好,今天的答疑就到这里,你对这些问题有什么看法呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,126 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑解惑 持续集成,一条贯穿诸多实践的主线
“沟通反馈”模块又告一段落了,在这个模块中,我们把自己与真实世界的距离又拉近了一步。
一方面,我们强调主动沟通,把自身的信息更有效地传达出去;另一方面,我们也重视反馈,让真实世界的信息,更多地回到我们身边。同学们分享了很多经验,也提出了不少的问题。
在今天的答疑中,我选择了几个非常好的问题,从不同的角度丰富一下之前讲解的内容。
问题1单元测试做不好是否会影响到 CI 的效果?
毅 同学提到
如果单元测试做的不到位或者不满足A-TRIP是不是执行CI的效果就会弱很多-
——《24 | 快速反馈:为什么你们公司总是做不好持续集成?》
这是一个非常好的问题,问到了各种实践之间的关联。我们在前面用了两讲的篇幅介绍了持续集成这个实践,为什么要做持续集成以及如何做好持续集成。
在自动化模块,我们还会在这个基础之上继续延伸,介绍持续交付,这些内容是从操作的层面上进行介绍,都是对单一实践的描述。
利用这次答疑的机会,我再补充一个维度,谈谈实践之间的关联。
持续集成的价值在于,它是一条主线,可以将诸多实践贯穿起来。也就是说,想要真正意义上做好持续集成,需要把周边的很多实践都要做好。
我们具体地说一下这些实践。但请记住我们说过的,做好持续集成的关键是,快速反馈。
比如,我们想要做好 CI需要有一个稳定的开发分支所以最好采用主开发分支的方式。想用好主分支开发最好能够频繁提交而频繁提交需要你的任务足够小能够快速完成将任务拆解的足够小需要你真正懂得任务分解。要想在一个分支上开发多个功能那就需要用 Feature Toggle 或者 Branch by Abstraction。
在这条线上你有很多机会走错路。比如你选择了分支开发模式合并速度就不会太快一旦反馈快不了CI 的作用就会降低;再者,如果不能频繁提交,每次合并代码的周期就会变长,一旦合并代码的周期变长,人们就会倾向于少做麻烦事,也就会进一步降低提交的频率,恶性循环就此开启。
同样,即便你懂得了前面的道理,不懂任务分解,想频繁提交,也是心有余而力不足的。而多功能并行开发,则会让你情不自禁地想考虑使用多分支模型。
我们再来看另外一条线,也就是这个问题中提到的测试。
想做好 CI首先要有可检查的东西什么是可检查的东西最简单的就是编译、代码风格检查这些检查可以无条件加入构建脚本。但更重要的检查应该来自于测试而要想做好 CI我们要有测试防护网。
什么叫测试防护网呢?就是你的测试要能给你提供一个足够安全的保障,这也就意味着你要有足够多的测试。换个更技术点的术语来说,就是要有足够高的测试覆盖率。
如果测试覆盖率不够即便提交了代码CI 都通过了,你对自己的代码依然是没有信心的,这就会降低 CI 在你的心中的地位。
如果想有足够高的测试覆盖率,你就要多写单元测试。我们在前面讲过测试金字塔了,上层测试因为很麻烦,你不会写太多,而且很多边界条件,通过上层测试是覆盖不到的,所以,测试覆盖率在经过了初期的快速提升后,到后期无论如何是提上不去的。要想提升测试覆盖率,唯有多写单元测试。
要想多写单元测试,就需要编写可以测试的代码,而要想编写可测的代码,就要懂软件设计,将系统之间耦合解开。
通过上面的分析,你已经看出来做好持续集成,让它完全发挥自己的价值,需要做的工作还是相当多的。但也请别灰心,实际上,我做咨询时,很多团队就是从持续集成下手,开始改造他们的软件开发过程。
这是一个“以终为始”的思路,先锁定好目标,就是要把持续集成做好,然后围绕着这个目标改进其他做得欠佳的方面。比如,原来是多分支的,就先固定一个主分支,然后,逐步改变大家的开发习惯,让他们进入单分支的开发状态。
再比如,原来没有测试,那就在 CI 上先加一个最低的测试覆盖率然后定期去提高比如第一周是10%第二周是20%,这样一步一步地提高,开发团队可以一边开发新东西,一边为既有代码补测试。等到覆盖率到了一定程度,提高有困难了,团队就可以考虑怎么改进设计了。
所以CI 作为一个单独的实践,本身是很简单的,但它可以成为提纲挈领的主线,帮助团队不断改善自己的开发过程。
问题2老板参加复盘不敢说真话怎么办
grass10happy 同学提到
复盘是不是最好是团队内部进行,每次老板参加复盘,好像就没人说出真话了。-
——《25 | 开发中的问题一再出现,应该怎么办?》
感谢 grass10happy 同学这个提问,把我因为篇幅原因省掉的一个部分给挽救了回来。
回顾会议的目的在于改进,它不仅仅在于让大家参与进来,更重要的是让团队成员能够敞开心扉,把问题暴露出来。暴露问题,是改进的前提条件。
我在《27 | 尽早暴露问题: 为什么被指责的总是你?》这篇文章中说过了,对于很多人来说,敢不敢暴露问题是个心理问题。你会发现,同事之间聊天,普遍是没有任何压力的,你几乎可以放心大胆地谈论各种问题,而一旦有领导在,很多顾虑就会出现了。
于是,问题就变成了怎么能够让大家放心地把问题暴露出来,一个办法就是设置一个安全的环境。
怎么设置一个安全的环境呢?对于标准的回顾会议来说,第一步应该是做安全性检查。
先由大家投票最简单的方式是就是给当前的环境打分。你觉得可以畅所欲言就打1分你觉得还好就打0分如果你觉得不方便表达比如你看领导在很多问题不适合反馈就打-1。
每个与会者都投出属于自己的一票。然后,主持人根据投票结果决定回顾会议是否进行,比如,有人投-1就不能继续。
会议能继续固然好,一旦会议不能继续,可以有多种解决方案。比如,把在场职位最高的人请出去,这个人可能就是老板。老板也许心里很不爽,但在这个过程中,大家都是按照规则在办事,并不存在对谁另眼相待的情况。
当老板离席之后,我们再进行一轮投票,判断环境是否变得安全了。如此反复,也许要进行几轮投票,直到大家觉得安全了。
当然,也有可能进行多轮,有人始终觉得不安全,那可能最好的选择是,取消今天的回顾会议,换个时间地点从头再来。而项目负责人则需要私下里解决一下团队内心安全的问题。
通过安全性检查之后,我们才会进入回顾会议的正式环节,具体内容在正文中已经讲过了,这里就不再赘述了。
问题3国内的技术信息落后吗
One day 提到
老师能否多多介绍一下技术方面的网站之类的,新技术发展见闻之类的,或者技术总结方面。国内的技术基本都多少有些滞后。-
——《23 | 可视化:一种更为直观的沟通方式》
这个问题让我感觉自己一下子回到了好多年前。我刚入行的那会,学习新知识确实要多看看英文网站,当时的信息传播速度不快,中文技术网站不多。
但在今天显然已经不是这样了如果只是想获得最新的技术信息我在《23 | 可视化:一种更为直观的沟通方式》这篇文章中介绍了 InfoQ 和技术雷达,这上面的信息量已经很丰富了。你再只要稍微看几个网站,关注几个公众号,各种信息就会送到你面前。
所以,你根本不用担心会错过什么新技术,反倒是信息量太大,需要好好过滤一下。
国内程序员真正落后的不是信息,而是观念。
我讲的很多内容是软件工程方面的,以我对国内外程序员的了解来看,发达国家的程序员在这些内容的普及上,要比国内程序员好很多。
国内程序员的平均水平,大多停留在实现一个功能的理解上,而发达国家的程序员做事要专业许多。所以,以专业素养来看,国内程序员还有很大的提升空间。
在经济学里有“边际效用递减法则”The Law Of Diminishing Marginal Utility说的是当你手里某一物品总数越来越多时新增一个单位该物品所获得的效用通常会越来越少。
当你的技术知识积累到一定程度时,还采用原来的学习方式,就很难获得真正意义上的提高,这是很多人抱怨 IT 行业不好混的原因。
同时,这也是我开设这个专栏的初衷,希望给大家一些不同的视角,一些新的前进动力。
好,今天的答疑就到这里。我想请你分享一下,你是怎么理解这些问题的呢?欢迎在留言区写下你的想法。
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,109 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 少做事,才能更有效地工作
你好,我是郑晔。
在这个专栏里,我讲过很多东西,几乎涉及到软件开发的方方面面,但有一个重要的方面,我却从来没有说过,那就是算法。
因为我一直把它当做不言而喻的基本功,认为每个程序员都应该掌握。在我们专栏的结束语中,我就用这个没有涉及过的话题来开篇吧!
算法的差异
排序算法是每个程序员都会学到的内容,大家对各种算法也是如数家珍:插入排序、冒泡排序、归并排序、堆排序、快速排序等等。我们也知道各个算法的复杂度,比如,插入排序是 O(n^2快速排序平均情况下是 O(nlogn等等。
你有没有想过一个问题,不同算法的复杂度本质差别到底是什么呢?我们就以插入排序和快速排序为例,为什么快速排序要比插入排序快呢?
我不打算做算法分析,直接公布答案:因为做比较的次数少。为什么同样的排序,比较次数会有差异呢?因为插入排序每次循环只关注当前的目标,循环之间没有关系,而快速排序在做不同划分时,上一次的结果对下一次有助力,因此它省下了不少的比较次数。
明白了这个道理,再来看所谓的算法优化,其实就是尽可能利用已知的信息,少做不必要的事。
再来看一个常见的面试题给你一堆数找出前100个。很多人直觉就会想到排序然后选出前100个。这种做法固然可行但一定是做多了因为这里需要的是找出前100个数而不是要100个有序的数字更不是要所有的数都有序。
说到这里你就知道了只要把数据划分开就好并不需要排序如果划分点不是第100个元素就向着100所在的方向继续划分就好。
计算机是最擅长处理繁琐重复工作的,即便如此,我们依然要做算法优化,原因是当数据规模大到一定程度时,不同复杂度的算法差别就非常明显了。算法没用好,计算机硬件再好,也是徒劳的。
有一则《计算机程序设计艺术》作者高德纳Donald Knuth的轶事他年轻时参加算法大赛用最差的系统击败了诸多对手拿到算法执行效率的冠军凭借的就是其强大的算法优化功力。
对于计算机,算法尚且如此重要,我们面对工作时何尝不是如此呢!
有效工作
《10x 程序员工作法》,也许有的同学最初看到这个标题就急急加入了,以为会从这个专栏中学习到一些“以一抵十”的编程技法,对不起,我彻底让你失望了。我非但没讲太多编程的技法,甚至还从各种角度劝你少写代码:无论是向产品经理提问题,还是让你在前面多考虑设计。
难道不是做得越多才越高效吗?
插入排序并不会因为干的活多,就比快速排序得到更高的评价,因为它们比的是谁排得快。工作效率高,不是因为代码写得多,而是有效工作做得多。
如果 CPU 都被无效指令占据了,哪有时间执行有效指令呢?即使你很忙碌,但工作进展依然是收效甚微,因为无效工作占据了你太多的大脑,让你不能聚焦在正经事上,当然就是效率不高了。
其实,这个专栏的内容在我脑子里已经盘旋很多年了。不过,即便在专栏筹备期,我已经备了很多篇稿子之后,我依然没有找到一个准确的说法能够描绘内心的想法。
我想过“程序员的职业素养”,但似乎这会让专栏朝着职场行动指南的方向努力;我想过“高效工作”,但实际上我也不打算讨论那些工作技巧。直到上线日期临近,我的编辑实在受不了我的拖延,坐下来与我交流了很久,我才终于找到了内心的那个词:有效。
我在这个专栏真正探讨的主题是,有效工作。
有效工作需要我们把力量聚焦到正确的地方做本质复杂度Essential Complexity的事情少做无意义的事情。
我曾经在一个大公司做咨询按照他们的统计线上60%的代码从来没有运行过。我们都知道,一多半的代码增加的可不只是一多半的工作量,团队可能需要的是几倍甚至几十倍的心力去维护它。
当然有效工作最终没有成为这个专栏的名字而用了更有个性的《10x 程序员工作法》。这个名字也不错因为在我看来很多程序员做的是负功比如写那60%代码的程序员。只要能做到有效工作,效率自然会高出业界平均水平很多。
怎么才能有效工作呢?我在专栏中已经给你讲了很多,小结一下就是:
拓展自己的上下文,看到真正的目标,更好地对准靶子,比如,多了解用户,才不至于做错了方向;站在公司的层面上,才知道哪个任务优先级更高;站在行业的角度,而不局限于只在公司内成为高手,等等。
去掉不必要的内容,减少浪费,比如,花时间分析需求,不做非必要的功能;花时间做好领域设计,别围着特定技术打转;花时间做好自动化,把精力集中在编码上,等等。
要想有效工作,有两点非常重要。一方面,意识上要注意自己工作中无效的部分。这就像一个开关,拨过去就好了。所以,读这个专栏,有人常有恍然大悟的感觉,也有人觉得很简单。
很多时候,你只是不知道,就像我在专栏中提到,要问产品经理问题,这是很多人没想过的。每篇文章后面的那一句总结,就是这样的开关,拨过去就好。
另一方面,要构建自己关于软件开发的知识体系,这是要花时间积累的。在这个专栏中,我给你讲了很多最佳实践,就是让你知道,在某些方面,有人已经做得很好了,花时间学习,比自己从头摸索好很多。
这就像所有的数学公式一样,理论上你都可以自行推导,但肯定不如从教科书上学得快。
藏经阁目录
虽然我讲了这么多内容,但实际上,因为篇幅的关系,这只是冰山一角。其实,我给你讲的这部分内容并不是具体的知识,而是告诉了你哪些东西要去学习,给了你一张学习地图,把各种知识贯串了起来。
我曾与朋友打趣道,我的专栏实际上是藏经阁的目录,真正的经书还要等你自己去参悟。只不过,有一个人把这些经书之间的知识连接给你补齐了。这些连接恰恰是在学习相关内容时,让我苦思冥想许久的。
大约一年前2018年4月极客时间编辑找到我问我是否有兴趣在极客时间开个专栏作为“得到”重度用户的我一直对知识服务很感兴趣。有这样的机会让我体验我当然想试试甚至最初给自己定下了写100篇的宏伟计划。
真正开始写我才知道在繁忙的日常工作之余坚持写作还是一件很有挑战的事今天看来100篇的目标显得那么无知无畏。
不过也正是因为压缩到一半左右的篇幅在专栏后面的部分我才极大地提高了知识密度比如微服务和DDD这两个可以分别写成一个系列内容的话题我用一篇文章就将其精华和知识脉络提炼呈现了出来。
因为我想尽我所能,帮助大家构建起一个软件开发的知识体系,让你在未来遇到问题时,知道可以在哪个方面进一步加强。希望这个专栏真的起到帮你理清思路,答疑解惑的作用。
还记得我在开篇词中的最后一段话吗?
也许在这个专栏的最后,你发现自己并不认同我的原则,却能够用自己的原则来与我探讨,那么,恭喜你,因为那是最美妙的事情!
不知道你是否形成了自己的原则呢?欢迎与大家分享。因为它代表着你已经形成了自己的知识体系。与我讲了些什么相比,你学到了什么才是一件更重要的事。
希望在学习了这个专栏之后,你可以用自己的工作原则做更多本质复杂度的事情,减少无意义的时间消耗。
其实,这个专栏的最大收益人是我自己,感谢这次的专栏之旅,我终于强行治疗了我的拖延症,把自己对于有效工作的思考完整地整理了出来,那些在脑子里模糊的印象现在终于有了一个完整的体系。这个体系就是我在专栏里提到的工作原则,现在我可以更好地表达自己的想法了。
不过,这个专栏对我而言也是有遗憾的。因为我想表达的内容很多,给大家打开更多大门的同时,也给很多同学留下了更多的疑问。
有些同学期待在某个方面再深入细节地讲一下比如DDD那可是值得再写一个专栏的主题。限于这个专栏的主题和篇幅关系我没办法深入展开只能对大家说声抱歉了。
如果以后有机会我会再来与你分享我对软件开发的理解这次的《10x程序员工作法》之旅就暂告一段落了
再见!

View File

@ -0,0 +1,77 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 捕捉 HR 微表情,做出应对策略
你好,我是你的面试课老师杨宇堃,欢迎进入第 06 课时的内容“捕捉 HR 微表情,做出应对策略”。
在面试的过程中,如何判断自己所陈述的信息是面试官感兴趣的呢?怎么才能在恰当的时间更好地展示自己擅长的内容呢?同样,如何在不恰当的时候适可而止,更好地转换话题呢?此时需要精准地捕捉到面试官的微表情,以便在合适的时间突出自己。
沟通时需注意的小细节
你有没有经历过这样的窘境:当在描述项目经验时,突然被面试官打断了,虽然此时你正在兴头上,但也请你马上停止,面试官的打断说明他对你的这段经历比较了解,或者刚才的这段描述有了自己的判断,所以要想想刚才的描述是否有漏洞,如果有机会建议再重新解释一下。
有时候也能发现面试官重复提问同样类型的问题,说明他对你之前回答的问题有质疑,希望可以通过重复提问的方式,再次确定这件事情的真实性,此时需要你给出不同的答案或挑选重点内容来回答,如果没有察觉,很有可能就错失了这次机会。
甚至有时候面试官针对某个项目经验进行深入提问,不断地细化你所做的项目数据。这时一定要提高警惕,因为面试官对你的这段经历比较感兴趣,需要通过非常细致地提问,才能了解你在这个项目中真实参与的程度和担任的角色。如果你的回答不够细化或者给出的数据不够精细,那么很容易被误解为并没有参与这个项目的核心内容。
面试时需留意的微表情
观察面部微表情可以解读很多信息,进而可以判断面试官是否真的对你所说的内容感兴趣,下面来讲讲面试过程中常见的几个微表情。
当看到面试官的下嘴唇往前撇时,说明他对接收到的信息持有怀疑的态度, 此时需要转化角度或思路来陈述。
当看到用牙齿咬嘴唇的时候,说明面试官正在仔细听你的介绍,同时也在默默的思考你所表达的另一层含义是什么。
当看到面试官调整自己的坐姿时,比如身体向前移动,很有可能对你所讲的内容很感兴趣;如果发现面试官身体逐渐的后退,说明很有可能对你的这段介绍没有兴趣聆听,此时要及时的调整陈述的思路。
当发现面试官双臂交叉时,这是防卫的一种表现。很有可能你说的内容与他的认知完全不相符,也表现出面试官对你表述的观点完全不认同或者完全没有听懂你所表达的意思。
上面简单讲了一些面试过程中可能碰到的情况,希望你在以后的面试过程中及时捕捉到面试官的微表情,以做出应对的策略。
其实很多时候面试官也在捕捉你的面部表情。当你在阐述的过程中,面试官在倾听是否有漏洞,也许不经意的某个动作或者某句话,也能让面试官察觉到你的问题点,然后做出不一样的决定。所以管理好自己的微表情也是非常有必要的,下面我们来说说在面试时做出的一些不经意的错误微表情有哪些。
面试时需改掉不好的习惯
有时候一个沟通时的习惯,也能透露出一些问题,比如:
当习惯说“啊”、“呀”、“这个”、“那个”、“嗯”等口头语时,一般给人留下词汇量小或者思维慢的印象,在说话时需要利用间歇的方式让自己思考;
沟通时喜欢使用中英搭配,这样很容易给人一种虚荣心比较强、好表现或夸耀自己的错觉;
如果口头禅出现频率过高的话,很容易给人一种办事不干练、意志不坚定的印象。
当然说话声音的大小或者语速的快慢等这些信息也能让面试官初步判断你是一个什么性格的人。
说话声音的大小和一个人的性格联系非常紧密,喜欢大声说话的人,其性格比较以自我为中心,积极主动、行动力和支配欲强,也就是富有攻击性的一类人;说话声音小的人其性格比较偏内向,考虑的因素比较多,很压制自己的情感。
语速快慢和声音大小一样,一般语速快的人性格比较外向,有冲劲且有活力,但是常常给人一种紧张和压迫感,让人有种焦躁、混乱甚至有些粗鲁的感觉;但是语速慢的人容易让人感觉比较木讷,容易犹豫不决,甚至有时候有消极悲观的想法。
所以说需要根据你所从事的工作或者要应聘的岗位来调整自己说话的方式,才更能获得面试官的青睐。假如你是一位声音小而且语速慢的人,去面试一家公司的销售岗位,相信这家公司不会对你抛出橄榄枝,因为他们很难从你的沟通中看出你的销售潜力。
面试中透露出的动作,也需要多多留意
很多小伙伴可能没有留意在面试过程中做的一些小动作,也许就是这些小动作导致面试官对你的印象减分。下面简单说几个常见的小动作,希望可以帮到你。
吐舌头:一般在感受到有压力时,舌头不自觉地做出舔嘴唇或者看似是在舔嘴唇的动作,说明是对自我的一种安慰。如果做了这个动作说明当时你备感尴尬,希望可以缓解一下气氛。
用手捂住嘴巴:这个动作一般表示自己对刚刚说的话已经意识到了错误,下意识的去捂住自己的嘴巴。
十指交叉:这个动作很可能是自信的表现,也有可能是在掩盖你的紧张。如果你的十指无意识的交叉在一起,而且眼神也在躲避面试官,很有可能是怕面试官发现你的紧张。
抚摸颈部:这个动作说明你并不是很自信,当然也有可能是你正在释放压力,这是一种普遍有力的信号,说明大脑正在积极处理某种消极的情绪。
眼神躲避:很多候选人在面试的过程中,经常左顾右看,躲避面试官的眼神,给面试官一种心虚的表现。
当然还有一些其他的小动作,比如揉鼻子、挠头或者摸耳朵、翘二郎腿或抖腿、常扶眼镜、玩弄随身小物件、咬指甲等,这些小动作也都说明你比较紧张或者不够自信。
希望你可以通过这一课时的学习,合理地控制一下自己的微表情,在面试时更好地表现自己。

View File

@ -0,0 +1,52 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 巧妙推销自己的 3 个技巧
你好,我是你的面试课老师杨宇堃,欢迎进入第 07 课时的内容“巧妙推销自己的 3 个技巧”。
平和的心态,展现你的热情
对于任何一家企业在选择合适的人选时,一般都会从这几个方面进行筛选:首先是否具有相关的项目经验;其次是否聪明或者是否具有独自解决问题的能力;最后判断能否融入到团队的氛围中,以及是否对企业或者行业具有热情的态度来面对。
前两个方面所说的是面试者的行业经验和智商,对于一家公司在招聘时当然重要,但这并不是一个团队可以获得成功的关键因素。相信很多企业在选择五年经验以内的候选人时,这两个方面的考察一定会低于最后性格部分的考察。
相信很多小伙伴会有疑问,性格(工作热情)相比情商或工作经验对于一家企业真有那么重要吗?这个我可以肯定的告诉你,面试官非常喜欢性格开朗、积极主动、乐于挑战的候选人;同样也很排斥在乎个人得失、把责任都推给前司的候选人。相信你在工作中也会遇到过类似的同事,可以回忆一下,当时你是不是也很排斥呢?
在一个团队里如果存在一位性格比较消极的员工,很容易将其他员工传染,从而导致整个团队产出效率低下。也就是我们常说的“酒与污水定律”:是指把一勺酒倒入到一桶污水里,得到是一桶污水;如果把一勺污水倒入倒一桶酒中,得到的还是一桶污水。所以,对于面试官来说,候选人的性格和态度是至关重要的。
如何在面试官面前表现出积极正向、乐观的心态呢?
首先,需要表现出对应聘岗位和企业的认同感,也让面试官看到你为了这份工作做了很充分的准备,或者积极的介绍之前做过的项目与应聘企业项目的相似度。这些表现都可以让面试官感受到你的热情和积极正向的输出,非常不建议面试时问什么答什么的做法。
比如,当面试官问「你为什么选择目前的这份工作」时?如果只是单纯地回答「我喜欢这份工作」且没有任何的解释,那面试官无法判断你所说的真实性。此时建议这样回答:“因为目前这份工作和我之前做过的 xxx 项目非常相像。我在参与上一份项目时学习到了 xxx 技能,找到了一个新的发展方向,从而喜欢上了这样的一份职业。”,相信这样的表述面试官才能感受到你的热情和积极正向的态度。
其次,可以和面试官介绍一下,你在上一家公司与同事和领导相处融洽的案例,让面试官感受到你是一个积极融入团队中的人。比如「在前司获得的成长有哪些,与前 leader 的身上都学习到了哪些工作思路和成长思路等」,相信面试官会认为你是一个非常值得培养和积极主动学习的人。
了解行业发展,清晰表达你的见解
除了表现出积极和热情以外,如果在面试的过程中可以介绍一些你对行业以及对自己所从事工作的理解或见解,相信面试官一定会被你的表述深深吸引,也同样加强了希望可以录取你的信心。
比如,当面试官问「你怎么看对目前所从事的工作价值」时,如果这样回答「我觉得这是一份收入,并没有太多的感受,也不知道自己未来的发展是什么样的」,面试官会判断你是一个没有任何思考的人,应该也不会在自己的岗位上有什么作为。
此时建议这样回答:“我非常喜欢我的工作,我感觉我的岗位在目前行业的发展中起到了非常重要的作用,我们所做的几个项目都在推动公司的发展,也帮助公司的业绩从 XX% 提升到了 XX%(在这里举一些自己做过的项目经验),而且我也希望可以继续从事这样的工作,因为它可以让我获得更多的成就感。相信这个行业的发展是 xxxxxxxxx我的职业规划也会跟随这个行业的发展而得到很大的提升。”
当面试官听到这样的介绍时,会非常清晰地了解你对自己的工作已经有了深入的思考,同时也能感受到你不止局限在自己的工作领域中,还在通过行业的变化和了解,来规划自己的职业,是一个很有潜力的候选人。
真诚的对待每一次面试
当然除了积极的态度以及清晰的定位以外,还必须是一个真诚、正直的人。如果在面试的过程中表现良好、思考很全面,但是与实际工作的内容只有 50% 的真实度,这样会很容易在面试官面前露馅,然后给你打上一个不真诚的标签,自然而然,面试也就到此结束了。
因此,很多企业非常在乎候选人是否诚实或者真诚。也许你的职业经历并不是很丰富、项目内容并没有那么完美,但如果将自己所做的内容真实、完整地呈现给面试官,同时加上自己的思考,相信很多面试官会参考你的工作年限然后给你一个非常公平的反馈。
通过以上三个方面的讲述,可以了解到面试官在面试的时候更看重的是品德,然后是性格,最后才是工作经验和学历。希望你听完这一讲的内容后,可以积极主动地面对自己的岗位,更好的去思考如何巧妙的推销自己。

View File

@ -0,0 +1,46 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 认清自身实力,明确求职方向
你好,我是你的面试课老师杨宇堃,欢迎进入第 08 课时的内容“认清自身实力,明确求职方向”。
这一讲我们来分析如何更好的认识自己,此时的你可能会很奇怪,我们不是在分析面试时的技巧,以及顺利收到 Offer 吗,为什么这一讲要分析如何看清自己了呢?其实也不奇怪只有更好的认清楚自己,才能更好的明确方向以及获得更好的岗位。
认清自己的实力
我经常会被身边的小伙伴问到:我应该选择什么工作方向啊?目前的工作好累啊,想换个工作方向但是不知道如何选择?某某行业的薪资好高啊,要不我去试试吧?
如果这样问,有可能被目前社会的很多利益所引导,已经忘了出发点是什么或者没有思考过未来的职业规划。平时也会听到身边有人说“希望可以拥有一家自己的咖啡厅”,如果这只是一句玩笑话,说说就过去了;但如果你是认真的,那有没有考虑过:在做这件事情之前都准备了什么呢?所以,在选择工作时先要想想自己的擅长点是什么,同时为这份工作做了哪些准备等。
这里我给你推荐一款职业人格评估工具,即 MBTI来测一测自己是偏外向还是内向、是一个有规划的人还是一个探索性的人、是喜欢做挑战性的工作还是喜欢辅助团队做一些执行层面的工作等当然在测试的时候要依托于自己的内心哦。
明确求职方向
测试完以后就要开始思考到底该如何明确求职方向其实是发挥出自己的优势去寻找一份适合的工作。俗话说「360 行,行行出状元」,相信任何一份工作的发展前景都是非常光明的,只要在某一份工作上做的足够深入且全面,相信你的职业道路也会越来越宽广。尽量不要对自己还没有接触过的事情就开始焦虑,这样会限制自己的想法。
比如,现在大家求职一般会从两个方面考虑,即领域相关的和专业度相关的。
领域相关的可分为互联企业和传统企业,比如,如果你是一个充满创新能力且积极愿意改变自己生活的人,可以选择去互联网行业;如果你比较喜欢稳定,对程序化的工作比较看重可以选择去传统企业。
专业度相关的可分为:硬技能型和软技能型。例如,硬技能型有编辑、会计、研发、法律、统计等;软技能型有销售、活动执行、客服、创意等。
具体可参考如下表格:
| | 互联网 | 传统行业 |
| —- | ———————- | —————- |
| 硬性 | 研发工程师、数据分析师 | 会计、机械师 |
| 软性 | 产品经理、市场、销售 | 销售、市场、客服 |
根据拉勾后台数据的显示,以下是互联网行业热门岗位的 Top 5
研发工程师岗位需要具备技术能力、逻辑能力、时间观念等
产品经理岗位需要具备逻辑能力、沟通能力、执行力等
运营岗位需要具备一定的分析能力以及对宏观的管理能力等
市场岗位需要具备创意的想法、有一个开朗的性格、沟通能力等
销售岗位需要具备较强的沟通能力且有一个开朗的性格等
若想了解其他具体的岗位详情,建议可通过 MBTI 的测试来了解自己擅长的工作岗位,相信测试完后的结果与测试前的认知岗位会有重合。此时,可以通过拉勾网站去搜索密切度高的岗位,然后查看该岗位的职位 JD建议搜索查看同一岗位的多家职位 JD把JD 重合的部分标记出来,其实这些就是该岗位所必需的技能;当然了,多家职位 JD 也有不同的要求,这时可根据自己擅长的点和过往的经历来选出最合适的公司。
也可以通过回复你和对你产生邀约的公司的职位要求进行描述,再次验证是否真的是你的技能方向。
找到自己发展的方向是一方面,当然也需要你对自己的工作年限和自我能力作出正确的认知。在我多年的筛选简历中,经常可以收到一个工作三年左右的小伙伴投递了公司高管的岗位,也许你的职位优势是一个非常具有管理能力的人,但是对于高管岗位还是需要你有很多年的工作经验和项目经验做累计的。所以建议大家在投递简历的时候也要明确自己应该在什么工作年限和工作的阶段,从而找到最合适自己的岗位。
相信通过几年的磨练,在目前的岗位上,也能很快得到大家希望有用的职级和薪资。

View File

@ -0,0 +1,87 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 工作交接流程福利衔接
你好,我是你的面试课老师杨宇堃,欢迎进入第 12 课时的内容“工作交接流程 & 福利衔接”。
工作交接流程
如何不伤和气的提出辞呈
终于拿到了自己心仪公司的 Offer 了,可能有很多小伙伴又开始发愁了:如何与领导顺利提出辞呈,又不伤和气呢?这个时候一定要做好最坏的打算,你要明白,心软拖着不说会更伤害自己与前公司的关系,不如直截了当、当机立断。
一般提出离职的方式分为两种:
通过邮件的形式提出辞呈;
直接找直属 leader 沟通。
具体采用哪种方式,可根据自己的个性来判断,比如不太擅长沟通、偏内向的可以通过邮件的方式;如果已经想好了怎么和上级沟通,也可以直接找 leader 阐明心意。那在写邮件或直接沟通时需要注意哪些呢?
首先,可以先表达出对公司和领导在工作中的指导和帮助的感激,以及这段时间在公司的工作和成长的开心,同时说明一下做出辞职的决定对自己来说是多么难的一次选择。相信这样的表达可以让领导对你有个不错的印象。
其次,不论你的离职原因是不满意薪资、不适应团队的管理风格还是发展空间到达了上限等,都不要在这里抱怨出来,因为每个公司的 leader 都清楚公司里的问题,与其这样,不如直接告诉 leader辞职的原因是希望可以有更好的发展或者是让自己有更好的学习成长的空间。相信你的决心加上这样的理由leader 一定会领会里面的意思。
如果这时 leader 突然问:找到下家了么?该怎么回答?建议这样委婉地回答:手里有好几个 Offer还没确定好去哪家……
最不建议的离职理由:经常会有小伙伴为了避免双方尴尬,会选择“家人生病需要较长的时间照顾”、“家人要求我回老家工作”等类似这样的理由,如果是真实的当然不会有问题,如果是虚构的,以后万一被发现,则会给前公司留下一个不诚信的印象,以后再相见时会更尴尬。
当然也有小伙伴提出离职是为了通过拿到的 Offer 要求涨薪,这样的“小聪明”玩不好可能就把自己“玩”进去了,不但在拿到 Offer 的公司名声坏了,也不会被现在的公司重用的。
最后,可以和前司表示一下,自己一定会负责任地把手里的工作交接清楚,站好最后一班岗,这样也可以给前司 leader 留下一个让人踏实的印象。毕竟你的面试背调还在人家手里,总不希望闹得不可开交,拿不到一个好的背调反馈吧。
合理安排交接工作
一般来说,如果你是一位已经转正的全职员工,那么交接的时间为一个月,所以公司也会要求你在这一个月里正常工作,那么,如何清晰地在这一个月里合理安排交接工作呢?
先和直属 leader 协商找到一个靠谱的工作交接人;
把自己以往的项目文档整理好,分类发给交接人;
如果你手里还有未结束的项目,可以带着交接人熟悉一下,一起对这个项目做收尾工作;
通知同事或者项目对接人自己已经离职,接下来的项目由被交接人负责;
空出两周的时间,协助交接人熟悉你手里的工作内容,在旁做好支持工作。
如果新的公司期望你能尽快入职的话,多数情况下会担心你拒绝入职,此时建议你诚恳地向新公司解释,并和新公司同步交接工作的进度。
交接文档有以下注意事项,比如:
清晰的文档归类,发现问题可以马上与你沟通;
尽可能将相关的文档都涉及到,让你的交接文档更容易查找;
记得文档转出时抄送给领导,这个很重要,一定要记得;
我相信这样的交接流程不会让自己手忙脚乱,也可以给前司留下不错的印象。
离职最后一天走的时候,记得和同事们一一打招呼,感谢大家以往的照顾和帮助,以后要常保持联系。更重要的一点是,一定要拿到“离职证明”文件或“解除 / 终止劳动合同报告书”。
福利衔接
交接工作都做完了,很多小伙伴会问:我的社保、公积金怎么办?下面来讲讲 3 种常用的福利交接事项。
社保公积金
各个公司的社保、公积金都是以每个月的 15 日作为分界点,如果你是在 15 号前入职的新公司,那么就会帮你交当月的社保和公积金,如果你是在 15 号后从前公司离职,社保、公积金会由前公司承担。当然也会有特殊情况,要看人才局的具体安排。
如果你正好是 15 号前离职中间休息了一段时间15 号后入职新公司的,可能需要你自己找第三方保险代缴公司自行缴纳社保公积金了。
年假
通常,公司会按照你出勤的月份帮你做年假的换算,然后与你协商安排延后几天离职,或结算成工资,或者按照公司的规定有其他操作。
工作居住证
如果在前司有工作居住证的话,需要问问新公司是否可以接收,如果可以当然就直接转出,如果不可以,需要问问是否有第三方机构接收。
OK这门课到这里就结束啦希望这门课可以帮助你找到心仪的工作。感谢你的收听~

View File

@ -0,0 +1,86 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 吃透分布式数据库,提升职场竞争力
你好,我是高洪涛,前华为云技术专家、前当当网系统架构师和 Oracle DBA也是 Apache ShardingSphere PMC 成员。作为创始团队核心成员,我深度参与的 Apache ShardingShpere 目前已经服务于国内外上百家企业,并得到了业界广泛的认可。
我在分布式数据库设计与研发领域工作近 5 年也经常参与和组织一些行业会议比如中国数据库大会、Oracle 嘉年华等,与业界人士交流分布式数据库领域的最新动向和发展趋势。
近十年来,整个行业都在争先恐后地进入这个领域,从而大大加速了技术进步。特别是近五年,云厂商相继发布重量级分布式数据库产品,普通用户接触这门技术的门槛降低了,越来越多人正在参与其中,整个领域生态呈现出“百花齐放”的态势。
2021 年数据大会上,阿里云发布了分布式数据库使用率统计图
学好分布式数据库将给你带来哪些机会?
但在生产实践过程中我们会发现,许多技术人员对分布式数据库还停留在一知半解的状态,比如下面这些疑问:
听说 MongoDB 比 MySQL 好用,但它适合我的业务吗?
TiDB 与阿里云 PolarDB 看起来都支持 MySQL 语法,它们之间有什么区别呢?应该如何选择?
这本质上就是由于缺乏对分布式数据库基本原理的了解,容易导致使用该种数据库时问题频发。好比 Apache Cassandra 或 Azure CosmosDB 都支持多种一致性,但如果不了解分布式一致性模型,你很有可能会选错,从而造成业务数据不一致等问题。
也因此长久以来,业界一直存在一个典型的误解:分布式数据库只能遵循 CAP 原则,无法实现传统数据库的 ACID 级别的一致性,我的业务无法迁移到分布式数据库上。
而事实上,现代分布式数据库(特别是 NewSQL 类数据库),已经可以在一定程度上解决这一问题了。(我会分别在第 5 讲和第 15 讲中和你讨论一致性模型,你会获得想要的答案。)
虽然传统数据库中,大多数会使用复制同步技术来提高查询性能和可用性,但这些技术像一堆“补丁”,对已经不堪重负的传统数据库进行修修补补,解决问题有限的同时,反而可能带来更多问题(比如,复制延迟会长期困扰 MySQL 的复制高可用方案)。
而分布式数据库,基本上是从底层开始,针对分布式场景设计出来的,因此从基础层面就可以解决传统数据库的一些棘手问题。虽然初期投入相对大一些,却可以保证后续技术体系的健康发展,在长期成本上具有显著优势。
此外,分布式数据库好比一个“百宝箱”,其中蕴含了独具特色的设计理念、千锤百炼的架构模式,以及取之不尽的算法细节。随着分布式数据库迅猛发展,越来越多的研发、产品和运维人员或多或少都会接触分布式数据库,因此学好分布式数据库,也会为你提升职场竞争优势带来帮助,成为你技术履历上的闪光点。
对于数据库工程师,除了日常使用,相关面试中常常会涉及设计数据库集群架构、保障数据库的横纵向扩展等内容,因此理解主流分布式数据库原理和相关案例,会帮助你完美应对。
对于云产品经理,掌握目前商用与开源领域中主流的分布式数据库原理同样非常重要,这是规划和设计相关云产品的前置条件。
甚至在一般概念里,不与后端数据库直接打交道的移动 App 研发,想要解决多终端共享数据的同步问题,都可以从分布式数据库原理中获取灵感。
当进行系统运维支撑时,如果清楚分布式数据库内部到底发生了什么,将有助于设计合理的支撑策略。在处理具体问题时,也会更加得心应手。
学习过程中有哪些难点?
不过,分布式数据库的学习曲线非常陡峭,你会发现与其他知识类型相比,它有一个显著的区别,就是:学习资料过于丰富,且难度普遍不低。
由于数据库技术已经发展多年,其演化的分支过于庞杂,每个研究人员都会结合自身的专业背景与技术领域来解释分布式数据库。因此,将这些复杂的背景知识了解透彻,就成了大多数人深入这一领域的难题。
同时,该领域学术化气氛浓厚,因此大量核心技术是以论文的形式进行表述的,不仅内容晦涩,且大部分为英文,这也为探索核心理论提高了门槛。
还有一些课程往往注重 DBA 方向的培养,且一般限定在某个特定的数据库中(如云厂商数据库认证或 Oracle DBA 认证培训等),并没有抽象出一些共有的特性,方便大家掌握分布式数据库的核心理念。
这也在一定程度上导致人们对分布式数据库这一概念“误解”不断。不过,这也坚定了我想要帮助你了解通用分布式数据库的设计原理,借此带你重新审视业务实践的决心。
学习本课程后,你将对技术选型、系统架构设计,以及如何解决关键的技术难题有更为清晰的方案;在晋升评审&面试求职中,也能更加从容地应对相关技术问题。
我是如何设计这个课程的?
由于分布式数据库内涵丰富,知识结构繁杂,为使你能高效了解和掌握其中的关键信息,我采用了三种思路来设计这个课程。
化繁为简。去掉过时、不重要的技术细节,直接讲解与分布式数据库有关的内容,但同时我也会引导你去发现技术背后的细节,希望可以授人以渔。
知识全面。内容不仅仅介绍了分布式理论相关内容,同时介绍了一般资料少有提及的存储引擎,两者共同配合,才造就了分布式数据库高性能和高扩展性的特点。
注重实际。本着将技术理念与实际案例结合的精神,在介绍技术细节时,我会联系相关的分布式数据库,从多方位打通你的知识体系。
基于以上设计思路,我把课程分为 4 个模块,合计 24 讲。
模块一,分布式数据历史演变及其核心原理。从历史背景出发,讲解了分布式数据库要解决的问题、应用场景,以及核心技术特点。
模块二,分布式数据库的高性能保证——存储引擎。这是专栏的亮点内容,简要展示了现代数据库的存储引擎,比如典型存储引擎、分布式索引、数据文件与日志结构存储、事务处理。其中,我会特别介绍分布式数据库与传统数据库在存储层面上的差异。学完之后,你会对分布式数据库中的重要特性(如一致性和分布式事务)有一个完整的理解,明白为什么一些特定存储引擎(如日志结构存储)更适合去构建分布式数据库。
模块三,分布式数据库的高扩展性保证——分布式系统。详细介绍分布式数据库中所蕴含的系统设计原理、算法等,包含但不限于错误侦测、领导选举、数据可靠传播、分布式事务、共识算法等内容。虽然分布式内容很多,但我不会面面俱到,而是帮你提炼精华,基于实例为你建立知识体系。
模块四,知识拓展。我会和你探讨当代最成功的分布式数据库(传统&新型),探讨它们成功的关键,同时将它们与之前模块中所介绍的技术原理进行相应的映射,让你的知识体系更加丰富。
讲师寄语
本课程的设计目标是,尽最大程度解决你的实际问题,让你在不同的工程实践中,对分布式场景下的数据库存储有更加专业的认知,并对技术趋势建立深入的洞察。

View File

@ -0,0 +1,166 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 导论:什么是分布式数据库?聊聊它的前世今生
你好,欢迎学习分布式数据库,我们的课程就正式开始了。
在开设这门课程之前,我简短地与身边同僚、朋友交流了课程的大纲。当时,大家都表示出了浓厚的兴趣,并且不约而同地问了我这样一个问题:啥是分布式数据库?更有“爱好学习”的朋友希望借此展现出“勤学好问”的品德,进而补充道:“这是哪个大厂出的产品?”
好吧,我的朋友,你们真的戳中了我的笑点。但笑一笑后,我不禁陷入了思考:为什么分布式数据库在大众,甚至专业领域内认知如此之低呢?
原因我大概可以总结为两点:数据库产品特点与商业氛围。
首先,数据库产品的特点是抽象度高。用户一般仅仅从使用层面接触数据库,知道数据库能实现哪些功能,而不关心或者很难关心其内部原理。而一些类型的分布式数据库的卖点正是这种抽象能力,从而使用户觉得应用这种分布式化的数据库与传统单机数据库没有明显的差别,甚至更加简单。
其次,数据库的商业氛围一直很浓厚。数据库产品高度抽象且位置关键,这就天然成为资本追逐的领地。而商业化产品和服务的卖点就是其包含支撑服务,而且许多商业数据库最赚钱的部分就是提供该服务。因此这些产品有意无意地对终端用户掩盖了数据库的技术细节,而用户有了这层商业保障,也很难有动力去主动了解内部原理。
这就造成即使你工作中接触了分布式数据库,也没有意识到它与过去的数据库有什么不同。但“福报迟到,但不会缺席”——当由于对其原理缺乏必要认识,导致技术问题频发时,用户才会真正意识到它们好像类似,但本质却截然不同。
而随着分布式数据库逐步渗透到各个领域,用户再也不能“傻瓜式”地根据特性选择数据库产品了。新架构催生出来的新特性,促使使用者需要深入参与其中,并需要他们认真评估数据库技术特点,甚至要重新设计自己的产品来与之更好地结合。
因此,我将本专栏课程设计为一把钥匙,帮助你打开分布式数据库的大门。你也可以将本门课程当作一个网游的新手村任务,完成后会获取初始装备(原理与方法论),继而掌握深入该领域所必要的知识。
我是“历史决定论”的忠实簇拥者,在这一讲中,我会沿着分布式数据库的发展脉络来介绍它。相信你在读完后,会对一开始的那个问题有自己的答案。那么现在我们从基本概念开始说起。
基本概念
分布式数据库,从名字上可以拆解为:分布式+数据库。用一句话总结为:由多个独立实体组成,并且彼此通过网络进行互联的数据库。
理解新概念最好的方式就是通过已经掌握的知识来学习,下表对比了大家熟悉的分布式数据库与集中式数据库之间主要的 5 个差异点。
从表中,我们可以总结出分布式数据库的核心——数据分片、数据同步。
1. 数据分片
该特性是分布式数据库的技术创新。它可以突破中心化数据库单机的容量限制,从而将数据分散到多节点,以更灵活、高效的方式来处理数据。这是分布式理论带给数据库的一份礼物。
分片方式包括两种。
水平分片:按行进行数据分割,数据被切割为一个个数据组,分散到不同节点上。
垂直分片按列进行数据切割一个数据表的模式Schema被切割为多个小的模式。
2. 数据同步
它是分布式数据库的底线。由于数据库理论传统上是建立在单机数据库基础上,而引入分布式理论后,一致性原则被打破。因此需要引入数据库同步技术来帮助数据库恢复一致性。
简而言之,就是使分布式数据库用起来像“正常的数据库”。所以数据同步背后的推动力,就是人们对数据“一致性”的追求。这两个概念相辅相成,互相作用。
当然分布式数据库还有其他特点,但把握住以上两点,已经足够我们理解它了。下面我将从这两个特性出发,探求技术史上分布式数据库的发展脉络。我会以互联网、云计算等较新的时间节点来进行断代划分,毕竟我们的核心还是着眼现在、面向未来。
商业数据库
互联网浪潮之前的数据库,特别是前大数据时代。谈到分布式数据库绕不开的就是 Oracle RAC。
Oracle RAC 是典型的大型商业解决方案,且为软硬件一体化解决方案。我在早年入职国内顶级电信行业解决方案公司的时候,就被其强大的性能所震撼,又为它高昂的价格所深深折服。它是那个时代数据库性能的标杆和极限,是完美方案与商业成就的体现。
我们试着用上面谈到的两个特性来简单分析一下 RAC它确实是做到了数据分片与同步。每一层都是离散化的特别在底层存储使用了 ASM 镜像存储技术,使其看起来像一块完整的大磁盘。
这样做的好处是实现了极致的使用体验,即使用单例数据库与 RAC 集群数据库,在使用上没有明显的区别。它的分布式存储层提供了完整的磁盘功能,使其对应用透明,从而达到扩展性与其他性能之间的平衡。甚至在应对特定规模的数据下,其经济性又有不错的表现。
这种分布式数据库设计被称为“共享存储架构”share disk architecture。它既是 RAC 强大的关键又是其“阿喀琉斯之踵”DBA 坊间流传的 8 节点的最大集群限制可以被认为是 RAC 的极限规模。
该规模在当时的环境下是完全够用的,但是随着互联网的崛起,一场轰轰烈烈的“运动”将会打破 Oracle RAC 的不败金身。
大数据
我们知道 Oracle、DB2 等商业数据库均为 OLTP 与 OLAP 融合数据库。而首先在分布式道路上寻求突破的是 OLAP 领域。在 2000 年伊始,以 Hadoop 为代表的大数据库技术凭借其“无共享”share nothing的技术体系开始向以 Oracle 为代表的关系型数据库发起冲击。
这是一次水平扩展与垂直扩展,通用经济设备与专用昂贵服务,开源与商业这几组概念的首次大规模碰撞。拉开了真正意义上分布式数据库的帷幕。
当然从一般的观点出发Hadoop 一类的大数据处理平台不应称为数据库。但是从前面我们归纳的两点特性看,它们又确实非常满足。因此我们可以将它们归纳为早期面向商业分析场景的分布式数据库。从此 OLAP 型数据库开始了自己独立演化的道路。
除了 Hadoop另一种被称为 MPP大规模并行处理类型的数据库在此段时间也经历了高速的发展。MPP 数据库的架构图如下:
我们可以看到这种数据库与大数据常用的 Hadoop 在架构层面上非常类似,但理念不同。简而言之,它是对 SMP对称多处理器结构、NUMA非一致性存储访问结构这类硬件体系的创新采用 shared-nothing 架构,通过网络将多个 SMP 节点互联,使它们协同工作。
MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较丰富的 SQL 分析查询语句。同时,该领域是商业产品的战场,其中不仅仅包含独立厂商,如 Teradata还包含一些巨头玩家如 HP 的 Vertica、EMC 的 Greenplum 等。
大数据技术的发展使 OLAP 分析型数据库从原来的关系型数据库之中独立出来形成了完整的发展分支路径。而随着互联网浪潮的发展OLTP 领域迎来了发展的机遇。
互联网化
国内数据库领域进入互联网时代第一个重大事件就是“去 IOE”。
其中尤以“去 Oracle 数据库”产生的影响深远。十年前,阿里巴巴喊出的这个口号深深影响了国内数据库领域,这里我们不去探讨其中细节,也不去评价它正面或负面的影响。但从对于分布式数据库的影响来说,它至少带来两种观念的转变。
应用成为核心:去 O 后开源数据库需要配合数据库中间件proxy去使用但这种组合无法实现传统商业库提供的一些关键功能如丰富的 SQL 支持和 ACID 级别的事务。因此应用软件需要进行精心设计,从而保障与新数据库平台的配合。应用架构设计变得非常关键,整个技术架构开始脱离那种具有调侃意味的“面向数据库” 编程,转而变为以应用系统为核心。
弱一致性理念普及:虽然强一致性仍然需求旺盛,但人们慢慢接受了特定场景下可以尝试弱一致性来解决系统的吞吐量问题。而这带来了另外一个益处,一线研发与设计人员开始认真考虑业务需要什么样的一致性,而不是简单依靠数据库提供的特性。
以上两个观念都是在破除了对于 Oracle 的迷信后产生的,它们本身是正面的,但是如果没有这场运动,其想要在普通用户之中普及确实有很大困难。而这两种观念也为日后分布式数据库,特别是国产分布式数据的发展带来了积极的影响。
而与此同期,全球范围内又上演着 NoSQL 化浪潮,它与国内去 IOE 运动一起推动着数据库朝着横向分布的方向一路狂奔。关于 NoSQL 的内容,将会在下一讲详细介绍。
与上一部分中提到的大数据技术类似,随着互联网的发展,去 IOE 运动将 OLTP 型数据库从原来的关系型数据库之中分离出来,但这里需要注意的是,这种分离并不是从基础上构建一个完整的数据库,而是融合了旧有的开源型数据库,同时结合先进的分布式技术,共同构造了一种融合性的“准”数据库。它是面向具体的应用场景的,所以阉割掉了传统的 OLTP 数据库的一些特性,甚至是一些关键的特性,如子查询与 ACID 事务等。
而 NoSQL 数据库的重点是支持非结构化数据如互联网索引GIS 地理数据和时空数据等。这种数据在传统上会使用关系型数据库存储但需要将此种数据强行转换为关系型结构不仅设计烦琐而且使用效率也比较低下。故NoSQL 数据库被认为是对整个数据库领域的补充,从而人们意识到数据库不应该仅仅支持一种数据模式。
随着分布式数据库的发展,一种从基础上全新设计的分布式 OLTP 数据库变得越来越重要,而云计算更是为这种数据库注入新的灵魂,两者的结合将会给分布式数据库带来美妙的化学反应。
云原生是未来
从上文可以看到人们真正具有广泛认知的分布式数据库,即 OLTP 型交易式分布式数据库,依然是分布式数据库领域一个缺失的片段,且是一个重要的片段。一个真正的 OLTP 数据库应该具备什么特点呢?
实际上人们需要的是它既具有一个单机的关系型数据库的特性,又有分布式的分片与同步特性。 DistributedSQL 和 NewSQL 正是为了这个目的而生的 。它们至少具有如下两点引人注目的特性:
SQL 的完整支持
可靠的分布式事务。
典型的代表有 Spanner、NuoDB、TiDB 和 Oceanbase 等。并且本课程会重点围绕 DistributedSQL 的关键特性展开研究,这些特性是现代分布式数据库的基石。这里我就不占用过多篇幅介绍了,在 02 | SQL vs NoSQL一次搞清楚五花八门的各种“SQL”中我们再一起详细学习。
与此同时,随着云计算的纵向深入发展,分布式数据库又迎来新的革命浪潮——云原生数据库。
首先,由于云服务天生的“超卖”特性,造成其采购成本较低,从而使终端用户尝试分布式数据库的门槛大大降低。
其次,来自云服务厂商的支撑人员可以与用户可以进行深度的合作,形成了高效的反馈机制。这种反馈机制促使云原生的分布式数据库有机会进行快速的迭代,从而可以积极响应客户的需求。
这就是云原生带给分布式数据库的变化,它是通过生态系统的优化完成了对传统商业数据库的超越。以下来自 DB-Engines 的分析数据说明了未来的数据库市场属于分布式数据库,属于云原生数据库。
随着分布式数据库的发展,我们又迎来了新的一次融合:那就是 OLTP 与 OLAP 将再一次合并为 HTAP融合交易分析处理数据库。
该趋势的产生主要来源于云原生 OLTP 型分布式数据库的日趋成熟。同时由于整个行业的发展,客户与厂商对于实时分析型数据库的需求越来越旺盛,但传统上大数据技术包括开源与 MPP 类数据库,强调的是离线分析。
如果要进行秒级的数据处理,那么必须将交易数据与分析数据尽可能地贴近,并减少非实时 ELT 的引入,这就促使了 OLTP 与 OLAP 融合为 HTAP。下图就是阿里云 PolarDB 的 HTAP 架构。
总结
用《三国演义》的第一句话来说:“天下大势,分久必合,合久必分。”而我们观察到的分布式数据库,乃至数据库本身的发展正暗合了这句话。
分布式数据库发展就是一个由合到分,再到合的过程:
早期的关系型商业数据库的分布式能力可以满足大部分用户的场景,因此产生了如 Oracle 等几种巨无霸数据库产品;
OLAP 领域首先寻求突破,演化出了大数据技术与 MPP 类型数据库,提供功能更强的数据分析能力;
去 IOE 引入数据库中间件并结合应用平台与开源单机数据库形成新一代解决方案让商业关系型数据库走下神坛NoSQL 数据库更进一步打破了关系型数据库唯我独尊的江湖地位;
新一代分布式 OLTP 数据库正式完成了分布式领域对数据库核心特性的完整支持,它代表了分布式数据库从此走向了成熟,也表明了 OLAP 与 OLTP 分布式场景下,分别在各自领域内取得了胜利;
HTAP 和多模式数据处理的引入,再一次将 OLAP 与 OLTP 融合,从而将分布式数据库推向如传统商业关系型数据库数十年前那般的盛况,而其产生的影响要比后者更为深远。
我们回顾历史目的是更好地掌握未来。在本课程中我们将详细分析现代分布式数据库、OLTP 型数据库的关键技术、使用场景和应用案例。使你在未来可以更好地评估和使用分布式数据库。
而分布式数据库的历史同时体现了实用主义的特色,其演化是需求与技术博弈的结果,而不是精心设计出来的。我们的课程也会体现出实用主义的特点,让你学以致用,学有所获。

View File

@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 实践:设计一个最简单的分布式数据库
本讲是一节知识回顾与拓展实践课。经过前几讲的学习,相信你已经对分布式数据库有了直观的认识,今天我们来总结一下模块一的学习成果,并通过一个实际案例来加深印象,我也会就前几讲中同学们提出的典型问题进行答疑。
分布式数据库核心总结
现在让我们来总结一下第一模块的核心知识。
这个模块介绍了什么是分布式数据库。主要从历史发展的角度,介绍了传统数据库的分布式模式、大数据背景下的分析型分布式数据库,而后以去 IOE 为背景聊到了数据库中间件,以及开源数据库模式,接着说到了 DistributedSQL 与 NewSQL最后介绍了 HTAP 融合型数据库,它被看作是分布式数据库未来发展的趋势。
通过第 1 讲的学习,我想你不仅了解了分布式数据库由合到分、再到合的发展历史,更重要的收获是知道了到底什么是分布式数据库,这个最根本的问题。
从广义上讲,在不同主机或容器上运行的数据库就是分布式数据库,故我们能看到其丰富的产品列表。但是,正是由于其产品线过于丰富,我不可能面面俱到地去讲解所有知识点。同时由于数据库在狭义上可以被理解为 OLTP 型交易类数据库,因此本课程更加聚焦于 DistributedSQL 与 NewSQL 的技术体系,也就是 OLTP 类分布式数据库。在后续的模块中我会着重介绍它们涉及的相关知识,这里给你一个预告。
同时,这一模块也点出了分片与同步两种特性是分布式数据库的重要特性。
我们还一起学习了关于 SQL 的历史沿革,了解了什么是 NoSQL。这部分主要是对一些历史性的概念进行的“拨乱反正”说明了NoSQL 本身是一个营销概念。而后我们介绍了 NewSQL、DistributedSQL 的特点。如前所述,这其实才是本课程所要学习的重点。
SQL 的重要性如我介绍的那样,这使得它的受众非常广泛。如果数据库想要吸引更多的用户,想要在影响力上或在商业领域寻求突破,那 SQL 可以说是一个必然的特性。反之,如果是专业领域的分布式数据库,那么 SQL 就不如分片与同步这两个特性重要了。
在分片那一讲中,我们首先学习了分片的意义,它是分布式数据库提高数据容量的关键特性。我们学习了主要的分片算法,包括范围分片与哈希分片;也介绍了一些优化方法;最后用 Apache ShardingShpere 的例子来直观介绍了分片算法的应用,包含了分布式唯一 ID 的生成算法等相关内容。
数据分片是分布式数据库两个核心内容之一,但其概念是比较直观的。学习难度相比数据同步来讲不是很大。
我们会经常遇到一个问题:设计一套分库分片的结构,保证尽可能少地迁移数据库。其实这个需求本质上在分布式数据库语境下是毫无意义的,自动弹性的扩缩数据库节点应该是这种数据库必要特性。过分地使用分片算法来规避数据库迁移固然可以提高性能,但总归是一种不完整的技术方案,具有天然的缺陷。
模块一的最后我们学习了同步数据的概念。同步其实是复制+一致性两个概念的综合。这两个概念互相配合造就了分布式数据库数据同步多样的表现形式。其中,复制是它的前提与必要条件,也就是说,如果一份数据不需要复制,也就没有所谓一致性的概念,那么同步技术也就不存在了。
在同步那一讲中,最先进入我们视野的是异步复制,这类似于没有一致性的参与,是一种单纯的、最简单的复制方式。后面说的其他的同步、半同步等复合技术,多少都有一致性概念的参与。而除了复制模式以外,我们还需要关注诸如复制协议、复制方式等技术细节。最后我们用 MySQL 复制技术的发展历程,总结了多种复制技术的特点,并点明了以一致性算法为核心的强一致性复制技术是未来的发展方式。
接着我们介绍了一致性相关知识,这是模块一中最抽象的部分。因为 CAP 理论与一致性模型都是抽象化评估分布式数据库的工具。它们的好处之一就是可以是帮助我们快速评估数据库的一致性,比如一个数据库号称自己是线性一致的 CP 数据库,那么对于其特性,甚至大概的实现方式,我们就会心中有数了;另一个益处就是设计数据库时,你可以根据需要解决的问题,设计数据库一致性方面的特点。
CAP 理论首先要明确其中的C 指的是一致性模型中最强的线性一致。正因为是线性一致这样的强一致,才不会同时满足 CAP 三个特性。同时要注意可用性和高可用性的区别,可用性是抽象评估概念,网络分区后,每个分区只有一个副本,只要它提供服务,我们就可以说它其实是可用的,而不能说它是高可用。最后我提到了世界上只有 CP 和 AP 两种数据库,因为 P即网络分区是客观规律无法排除不会存在 CA 类数据库。
说完了 CAP 理论后,我介绍了一致性模型。它来源于共享内存设计,但其理论可以被分布式数据库乃至一般的分布式系统所借鉴。你需要知道,这部分介绍的三种一致性都是强一致性,其特点解决了复制部分提到的复制延迟,使用户不管从哪个节点写入或查询数据,看起来都是一致的。另外,这三种一致性又是数据一致,与其相对的还有客户端一致,这个我会在之后的分布式模块中具体介绍。
最后,作为数据库,一个重要的概念就是事务。它与一致性是什么关系呢?其实事务的 ACID 特性中AID 是数据库提供的对于 C 的保证。其中 I即隔离性才是事务的关键特性。而隔离性其实解决的是并行事务的问题一致性模型研究是单对象、单操作的问题解决的是非并行的事务之间的问题。故隔离性加上一致性模型才是分布式数据库事务特点的总和。
至此,我们总结了模块一主要的内容。那么学习了这些知识后,除了可以帮助你评估分布式数据库外,还有什么用呢?现在让我们来试着设计一个分布式数据库吧。
为什么要自己实现分布式数据库?
分布式数据库,特别是 NoSQL 和 NewSQL 数据库,是目前主要的发展方向。同时,这两种数据库的品种也极为丰富。其中很多都是针对特定场景服务的,比如 NoSQL 中 Elasticsearch 针对的是搜索场景Redis 针对缓存场景。而 NewSQL 更是百花齐放,如国内的滴滴、字节跳动等企业,都针对自己的业务特点实现了 NewSQL 数据库。更不要说如 BAT、Google 这样的大厂,他们都有自己的 NewSQL 类数据库。
这背后的动力来源于内驱需求与外部环境,这两者共同叠加而产生了目前这种局面。
内驱需求是,随着某种特定业务的产生并伴随其使用规模的扩大,从数据库这种底层解决该问题的需求逐步强烈。因为从数据库层面可以保证写入和查询满足某种一致性特性,而分布式数据库天然的服务化特性,又给使用者带来极大便利,从而可以加速这类业务快速发展。
外部环境是,分布式数据库使用的技术逐步成熟化,且可选开源产品众多。早先构造数据库的一个难点是,几乎所有涉及的技术类别都需要从基础开始构建,比如 SQL 解析、分布式协议和存储引擎等。而目前,有众多的开源项目、丰富的技术路线可供挑选,这样就大大降低了构造分布式数据库的门槛。
以上两点互相作用,从而使现在很多组织和技术团队都开始去构建属于自己的分布式数据库。
设计分布式数据库案例
熟悉我的朋友可能知道,我另外一个身份是 Apache SkyWalking 的创始成员,它是一个开源的 APM 系统。其架构图可以在官网找到,如下所示。
可以看到其中的 Storage Option也就是数据库层面可以有多种选择。除了单机内存版本的 H2 以外,其余生产级别的数据库均为分布式数据库。
选择多一方面证明了 SkyWalking 有很强的适应能力,但更重要的是目前业界没有一款数据库可以很好地满足其使用场景。
那么现在我们来尝试给它设计一个数据库。这里我简化了设计流程,只给出了需求分析与概念设计,目的是展示设计方式,帮助你更好地体会分布式数据库的关键点。
需求分析
我们先来介绍一下 SkyWalking 处理数据的特点。
由于 SkyWalking 的 APM 特性,其对写入有很高的诉求。不管是最早使用的 HBase还是现在的主力存储 Elasticsearch都对写入很友好。为了保证数据写入高速且一致OAP 节点层已经将计算指标进行了分片,也就是同一个指标是在相同的节点计算出来的。另外,该应用还采用了批量写入的模式,即每 10 秒进行一些批量写入。
SkyWalking 在使用场景下可以被看成一个查询少写入多的系统,查询很少发生,可以容忍一定的查询延迟。可用性方面是允许牺牲一定的可用性来换取性能的,比如目前对 Elasticsearch 的副本数量建议为 0也就是说不进行数据复制。
如果开启复制,一致性方面要求也比较低。因为对于最大的工作负载写入来说,几乎不在写入的时候进行数据查询。但是一些低负载操作需要保证一致性,比如写入监控结果,写入后需要马上能查询出来。
由于查询协议的数据结构是非关系型的,且查询种类不多,故不需要一定支持 SQL 语句。
以上围绕着第一模块的核心内容,分析了 SkyWalking 的数据库应该具备的特点。现在让我们来针对需求分析中提到的要点,来设计针对 SkyWalking 的分布式数据库。
概要设计
首先 OAP 节点实际上已经做过哈希分片,这样我们可以将数据库节点与 OAP 节点组成一对一,甚至多对一(二次哈希)的结构,保障一个指标只写入一个数据库节点,这样就避免了数据迁移的麻烦。甚至我们可以将数据库节点与 OAP 节点部署在一起,从而最大限度降低网络延迟,同时提高资源的利用率。
对于弹性扩缩容,由于 SkyWalking 可以容忍部分数据不可用,可以直接增加分片节点,而无须迁移数据。如果想要保证老数据可以查询,可以将扩容时间点做记录;而后老数据查询老节点,新数据查询新节点。由于 SkyWalking 所有数据都有生命周期,一旦节点上旧的数据被删除,缩容场景下,该节点也可以被安全移除。
虽然 SkyWalking 不强制要求可用性,但一些数据如果一旦遭遇故障,也会给使用者带来不好的体验。特别是对于类似一天内的平均响应时间,一旦某个节点故障,在没有副本的情况下,该指标的数据将会有非常大的偏差。
一旦开启数据复制,应该使用什么一致性呢?这个问题需要区分来看。对于大量写入的指标数据来说,弱一致是满足条件的。因为写入和读取是由不同的端点发起的,且写入可以认为是单对象单操作,故弱一致就满足条件。
但告警场景却不是这样,告警产生后会通知相关人员,他们希望能马上查询到数据。如果采用弱一致,很可能无法查询。这里我们不需要使用特别强的一致性,采用因果一致就可以满足需求。实现方式是,将写入告警产生的数据时间戳页传递给用户。用户查询的时候将时间戳发送给一个数据库节点,如果该节点没有该时间戳的数据,它会尝试请求其他节点去同步。
最后关于查询接口,由于不一定需要 SQL故我们可以使用简单的 RESTful 风格的 API 去实现查询和写入。但为了写入高效,可以独立设计写入协议,采用高效的二进制长连接的协议风格。
案例总结
以上就是根据第一模块学习的知识并结合 SkyWalking 的需求特点,设计的针对该系统的分布式数据库。设计层面我只强调了关键设计要点,并未进行详细说明。而关于底层的存储引擎,相信你在学习完模块二之后,会有自己的答案。
通过这个案例,我们可以看到设计分布式数据库只要结合分片和同步两个特点,就可以大概勾画出一个分布式数据库的外貌。你可以自己在工作和学习中,尝试设计分布式数据库来解决具有一定共性的数据问题。
留言答疑
开课以来,我收到了大家积极的反馈,其中有些问题非常专业,让我很惊喜。这里首先非常感谢你对课程的喜爱,你的积极反馈就是我写下去的动力。
这里我总结了一些共性问题,为你解答。
第一,有人提出了名词概念第一次出现应该给出全称的问题。
这里先向你道歉,出于个人习惯,我脑海中会将自己比较熟悉的概念直接以缩写或别名输出。这确实对第一次接触该知识的同学不太友好。在以后的写作中,我会尽量避免该问题。
第二个比较集中的问题是关于 MySQL InnoDB Cluster 是不是分布式数据库。
我在文章中提到,分布式的基础定义非常宽泛。如果从它出发,那么 InnoDB Cluster 是分布式数据库。但是从我们说的两个特性来看,它并不具有分片的特点,严格来说它不是分布式数据库,更不要说它是 NewSQL。但是我们可以为其引入分片的功能比如利用分库分表中间件以 InnoDB Cluster 为基础去构建分布式数据库,即 NewSQL 数据库。
这里我要强调一下,你不需要陷入概念区分的陷阱里,这不是考试,但现实生活比考试要复杂。把握住关键特点,才可以以不变应万变。
好了,答疑就先到这里。最后再次感谢你的积极反馈,希望在下一个模块结束后也能看到你精彩的留言。
总结
本讲首先回顾了模块一的主要内容,帮助你将各个部分串联起来,形成完整的知识拼图。而后通过一个案例介绍了如何使用这些知识设计一个分布式数据库,将所学知识应用到实际工作和学习中。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 概要:什么是存储引擎,为什么需要了解它?
经过第一个模块的学习,相信你已经知道了什么是分布式数据库,对分布式数据库的核心知识有了比较全面和深入的了解了。
这一讲是第二模块存储引擎的概要,主要目的是为你解释什么是存储引擎,以及它在分布式数据库中起到什么样的作用。
数据库的一个首要目标是可靠并高效地管理数据,以供人们使用。进而不同的应用可以使用相同的数据库来共享它们的数据。数据库的出现使人们放弃了为每个独立的应用开发数据存储的想法,同时,随着数据库广泛的使用,其处理能力飞速发展,演进出如现代的分布式数据库这般惊人的能力。
那么,为了支撑抽象的多种场景。一般的数据库都会采用多模块或多子系统的架构来构建数据库,从而方便数据库项目团队依据现实的场景来组合不同的子模块,进而构造出一众丰富的数据库产品。
而存储引擎就是这一众模块中极为重要的一环,下面我们开始解释它在整个数据库架构中的定位和意义。
存储引擎的定位
这个世界上,没有针对数据库设计的一定之规。每个数据库都是根据它所要解决的问题,并结合其他因素慢慢发展成如今的模样的。所以数据库子模块的分化也没有一个广泛接受的标准,且有些模块之间的边界也是很模糊的。特别是需要优化数据库性能时,原有被设计为独立存在的模块很可能会融合以提高数据库整体性能。
这里,我总结出了一个比较典型的分布式数据库的架构和模块组合标准。虽然不能完全代表所有分布式数据库,但是可以帮助你理解模块的组成方式。这里需要注意,我给出的模型是基于客户端/服务器,也就是 C/S 模式的,因为这是大部分分布式数据库的架构模式。
传输层:它是接受客户端请求的一层。用来处理网络协议。同时,在分布式数据库中,它还承担着节点间互相通信的职责。
查询层:请求从传输层被发送到查询层。在查询层,协议被进行解析,如 SQL 解析;后进行验证与分析;最后结合访问控制来决定该请求是否要被执行。解析完成后,请求被发送到查询优化器,在这里根据预制的规则,数据分布并结合数据库内部的统计,会生成该请求的执行计划。执行计划一般是树状的,包含一系列相关的操作,用于从数据库中查询到请求希望获取的数据。
执行层:执行计划被发送到执行层去运行。执行层一般包含本地运行单元与远程运行单元。根据执行计划,调用不同的单元,而后将结果合并返回到传输层。
细心的你可能会注意到,这里只有查询层,那么数据是怎么写入的?这对于不同的数据库,答案会非常不同。有的数据库会放在传输层,由于协议简单,就不需要额外处理,直接发送到执行层;而有些写入很复杂,会交给查询层进行处理。
以上就是数据库领域中比较常见的模块划分方式。你可能有这样的疑问:那么存储引擎在哪里呢?
执行层本地运行单元其实就是存储引擎。它一般包含如下一些功能:
事务管理器:用来调度事务并保证数据库的内部一致性(这与模块一中讨论的分布式一致性是不同的);
锁管理:保证操作共享对象时候的一致性,包括事务、修改数据库参数都会使用到它;
存储结构:包含各种物理存储层,描述了数据与索引是如何组织在磁盘上的;
内存结构:主要包含缓存与缓冲管理,数据一般是批量输入磁盘的,写入之前会使用内存去缓存数据;
提交日志:当数据库崩溃后,可以使用提交日志恢复系统的一致性状态。
以上就是存储引擎比较重要的几个功能,其核心就是提供数据读写功能,故一般设计存储引擎时,会提供对其写入路径与读取路径的描述。
好了,现在你清楚了存储引擎的定位和主要结构,那么存储引擎的种类也是很多的,下面我通过一些关键特性,来介绍几种典型的存储引擎。
内存与磁盘
存储引擎中最重要的部分就是磁盘与内存两个结构。而根据数据在它们之中挑选一种作为主要的存储,数据库可以被分为内存型数据库与磁盘型数据库。由此可见存储引擎的一个功能,就是可以被作为数据库类型划分的依据,可见引擎的重要性。
内存型存储是把数据主要存储在内存里,其目的很明显,就是加快数据读写性能。分布式数据库一个重要的门类就是内存型数据库,包括 Redis、NuoDB 和 MySQL Cluster 等。当然其缺点也很明显,那就是内存的成本较高,且容量有限。而分布式的架构能有效地扩充该类数据库的容量,这也是内存数据库主要是分布式数据库的原因。
磁盘存储相对传统,它存储主要数据,而内存主要作为缓冲来使写入批量化。磁盘存储的好处是,存储性价比较高,这主要得益于磁盘甚至是磁带的单位存储价格相比内存非常低廉。但是与内存型数据库相比,磁盘型数据库的性能比较低。不过,随着近年 SSD 磁盘的普及,这种趋势得到了有效的改善。
这两种存储引擎的差别还体现在功能实现的难度上。内存型数据库相对简单,因为写入和释放随机的内存空间是相对比较容易的;而磁盘型数据库需要处理诸如数据引用、文件序列化、碎片整理等复杂的操作,实现难度很高。
从目前的分布式数据库发展来看,磁盘型存储引擎还是占据绝对统治地位的。除了性价比因素外,内存型数据库要保证不丢失数据的代价是很高昂的,因为掉电往往就意味着数据的丢失。虽然可以使用不间断电源来保证,但是需要复杂的运维管理来保证数据库稳定运行。
然而近年来,随着 NVMNon-Volatile Memory非易失性内存等技术的引入。这种情况开始出现了一些变化此种存储具有 DRAM 内存的性能,同时能保证掉电后数据不丢失。且最重要的是读写模式类似于内存,方便应用去实现功能。有了它的加持,未来内存型数据库还将有比较大的发展。
除了硬件加持,内存型数据库也可以通过结构设计来保证数据不丢失。最常用的手段就是使用数据备份+提交日志的模式。数据库为了不影响写入读取性能,可以异步地备份数据。同时在每次写入数据之前要先写入提交日志,也就是说提交日志的写入成功才被认为是数据写入成功。
当数据库节点崩溃恢复后,将备份拿出来,计算出该备份与最新日志之间的差距,然后在该备份上重放这些操作。这样就保证数据库恢复出了最新的数据。
除了内存和磁盘的取舍,存储引擎还关心数据的组合模式,现在让我们看看两种常见的组合方式:行式与列式。
行式存储与列式存储
数据一般是以表格的形式存储在数据库中的,所以所有数据都有行与列的概念。但这只是一个逻辑概念,我们将要介绍的所谓“行式”和“列式”体现的其实是物理概念。
行式存储会把每行的所有列存储在一起,从而形成数据文件。当需要把整行数据读取出来时,这种数据组织形式是比较合理且高效的。但是如果要读取多行中的某个列,这种模式的代价就很昂贵了,因为一些不需要的数据也会被读取出来。
而列式存储与之相反,不同行的同一列数据会被就近存储在一个数据文件中。同时除了存储数据本身外,还需要存储该数据属于哪行。而行式存储由于列的顺序是固定的,不需要存储额外的信息来关联列与值之间的关系。
列式存储非常适合处理分析聚合类型的任务,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。
我们会发现 OLTP 数据库倾向于使用行式存储,而 OLAP 数据库更倾向于列式存储,正是这两种存储的物理特性导致了这种倾向性。而 HATP 数据库也是融合了两种存储模式的一种产物。
当然这里我们要区分 HBase 和 BigTable 所说的宽列存储与列存储在本质上是不同的。宽列存储放在其中的数据的列首先被聚合到了列簇上,列簇被放在不同的文件中;而列簇中的数据其实是按行进行组织的。
选择行模式与列模式除了以上的区分外,一些其他特性也需要考虑。在现代计算机的 CPU 中,向量指令集可以一次处理很多类型相同的数据,这正是列式存储的特点。同时,将相同类型数据就近存储,还可以使用压缩算法大大减少磁盘空间的占用。
当然,选择这两种存储模式最重要的因素还是访问模式。如果数据主要是按照行进行读取,比如交易场景、资料管理场景等,那么行式存储应是首选。如果需要经常查询所有数据做聚合,或者进行范围扫描,那么列式存储就很值得一试。
以上就是常见的数据的组合模式,那么组合好的数据如何存储在物理设备上呢?下面让我们探讨一下数据文件和索引文件两种常用的存放数据的物理原件。
数据文件与索引文件
上文介绍了内存与磁盘之间的取舍,从中可看到磁盘其实更为重要的,因为数据库是提供数据持久化存储的服务。故我们开始介绍磁盘上最为重要的两类文件:数据文件和索引文件。
数据文件和索引文件如名字所示,分别保存原始数据与检索数据用的索引数据。
但是随着时间的推移,两者的区分也不是那么泾渭分明了。其中以 IOT索引组织表模式为代表的数据文件在数据库特别是分布式数据库中占据越来越重的位置。一种将两者进行融合的趋势已经变得势不可挡。
数据文件最传统的形式为堆组织表Heap-Organized Table数据的放置没有一个特别的顺序一般是按照写入的先后顺序排布。这种数据文件需要一定额外的索引帮助来查找数据。
另外有两种数据表形式自带了一定的索引数据能力即哈希组织表Hash-Organized Table和索引组织表Index-Organized Table。前者是将数据通过哈希函数分散到一组数据桶内桶内的数据一般是按照一定规则进行排序以提高查询效率而后者一般采用索引文件的形式来存储数据以 B+树为例,数据被存储在叶子节点上,这样做的目的是减少检索数据时读取磁盘的次数,同时对范围扫描支持友好。
索引文件的分类模式一般为主键索引与二级索引两类。前者是建立在主键上的,它可能是一个字段或多个字段组成。而其他类型的索引都被称为二级索引。主键索引与数据是一对一关系,而二级索引很有可能是一对多的关系,即多个索引条目指向一条数据。
这里按照索引与数据之间结合的程度,我们又可以把索引分为聚簇索引和非聚簇索引。前者如哈希组织表和索引组织表那样,数据的分布与索引分布是有关联的,它们被“聚”在一起,这样的查询效率很好。而后者最常见的例子就是针对这两种数据文件的二级索引,因为二级索引要索引的列不是主键,故索引与数据是分割的,查询时需要进行多次磁盘读取。但是对于写入,聚簇索引可能需要进行唯一判断,性能会比简单构建的非聚簇索引低效。
最后一点需要说明的是,二级索引需要保存指向最终数据的“引用”。从实现层面上,这个引用可以是数据的实际位置,也可以是数据的主键。前者的好处是查询效率高,而写入需要更新所有索引,故性能相对较低。而后者就恰好相反,查询需要通过主键索引进行映射,效率稍低,但写入性能很稳定,如 MySQL 就是选用后者作为其索引模式。
面向分布式的存储引擎特点
以上内容为存储引擎的一些核心内容。那分布式数据库相比传统单机数据库,在存储引擎的架构上有什么不同呢?我总结了以下几点。
内存型数据库会倾向于选择分布式模式来进行构建。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。
列式存储也与分布式数据库存在天然的联系。你可以去研究一下,很多列式相关的开源项目都与 Hadoop 等平台有关系的。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。
而列式存储可以被认为是这种模式的一种优化,实现该模式的必要条件是要有分布式系统,因为一台机器的处理能力是有瓶颈的。如果希望处理超大规模数据,那么将数据分散到多个节点就成为必要的方式。所以说,列模式是由分析性分布式的优化需求所流行起来的。
至于宽列存储更是分布式数据库场景下才会采用的模式。
数据文件的组织形式,分布式数据库几乎不会使用堆组织表。因为该形式过于随意,无法有效地分散数据。不知道学习过数据分片那一讲的时候你有没有注意到,另外两种组织表的名字与两种分片算法是有着天然联系的。
哈希组织表数据经过哈希函数散列到不同的桶,这些桶可以被分散到不同节点。而索引组织表一般叶子节点是按一定顺序排列的,这与范围分片又有着某种契合的关系。所以分布式数据库一般都会采用这两种模式作为其存储引擎,甚至一些分布式数据库直接将数据文件当作索引使用。
总结
好了,关于存储引擎我就介绍到这了。这一讲我们首先展示了数据库的整体架构,并点出了存储引擎所在的位置;而后分别讨论了存储引擎中几组概念的对比,并在最后说明了分布式数据库在引擎层面的选择及其原因。
当然,本讲只是一篇概述。存储引擎中其他重要的概念,我会在本模块随后的几讲中为你详细介绍。

View File

@ -0,0 +1,168 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 分布式索引:如何在集群中快速定位数据?
索引是数据检错的关键技术,那么在分布式数据库这种体量的数据容量下,如单机数据那样进行数据表全量扫描是非常不现实的,故分布式存储引擎的关键就是要通过索引查找目标数据。
由于索引在不同的数据库概念里内涵是非常不同的,故本讲首先会定义我们要讨论的索引的内涵;接着会描述数据库的读取路径,从中可以观察到主要索引的使用模式;而后会重点介绍磁盘上与内存中的索引结构;最后会谈谈非主键索引,即二级索引的意义和主要实现形式。
那么,让我们从什么是分布式索引说起。
说到分布式索引时,我们在谈论什么?
首先,我要说明一下谈到分布式索引,需要了解什么样的内容。通过上一讲的学习,你已经知道存储引擎中包含数据文件和索引文件,同时索引文件中又有索引组织表这种主要的形式。目前世界上主要的分布式数据库的数据存储形式,就是围绕着索引而设计的。
为什么会这样呢?
由于分布式数据库的数据被分散在多个节点上,当查询请求到达服务端时,目标数据有极大的概率并不在该节点上,需要进行一次甚至多次远程调用才可查询到数据。由于以上的原因,在设计分布式数据库存储引擎时,我们更希望采用含有索引的数据表,从而减少查询的延迟。
这同时暗含了,大部分分布式数据库的场景是为查询服务的。数据库牺牲了部分写入的性能,在存入数据的时候同时生成索引结构。故分布式数据库的核心是以提供数据检索服务为主,数据写入要服务于数据查询。从这个意义上说,分布式索引就是数据存储的主要形式。
本讲会以 NewSQL 和 Cassandra 为代表,介绍典型的 NoSQL 的存储引擎中的主要技术,力图帮助你理解此类数据库中存储引擎检索数据的路径。
读取路径
掌握分布式数据库存储引擎,一般需要明确其写入路径与读取路径。但如上文讨论的那样,写入是严重依赖读取的,故明确读取路径我们就可以指明写入的规则。
因此这一部分,我们先来明确存储引擎是如何处理查询请求的。一般的规则如下:
寻找分片和目标节点;
检查数据是否在缓存与缓冲中;
检查数据是否在磁盘文件中;
合并结果。
第一步就是要查找数据在分布式系统的哪个目标节点上。严格说,这一步并不是存储引擎所囊括的部分,但为了表述清楚,我们也将它加入读取路径中来。由于分布式数据库采用分片技术来分散数据,那么查询条件中如果有分片键,就可以应用分片算法来计算出分片,也就是目标节点所在的位置;而如果不包含分片键,就需要“二级索引”来帮忙寻找分片键了,之后的逻辑与使用分片键查找就相似了。
第二步既然确定了所在节点那么剩下的就交给存储引擎了。首先需要在缓存Cache中进行查找。缓存包含数据缓存或行缓存其中包含真实的数据用于快速检索经常访问的数据一般元数据和静态配置数据都会放在数据缓存里面。而后再缓冲查找数据缓冲是为了批量写入数据而预留的一段内存空间当写满缓冲后数据会被刷入磁盘中所以会有部分数据存在缓冲之中。
第三步,确定了数据并不在内存中,这时就需要检查磁盘了。我们需要在具有索引的数据文件内查找响应的数据。通过之前的学习我们可以知道,每个数据文件都有主键索引,可以直接在其中查找数据。但是,存储引擎为了写入性能,会把数据拆分在众多的数据文件内部。所以我们需要在一系列文件中去查找数据,即使有索引的加成,查找起来的速度也不是能够令人满意的。这个时候我们可以引入布隆过滤,来快速地定位目标文件,提高查询效率。
最后一步是对结果进行归并。根据执行层的不同需求,这里可以马上返回部分匹配结果,也可以一次性返回全部结果。
现在我们已经勾勒出存储引擎的一个完整的读取路径,可以看到路径上一些关键技术是保证数据查询与读取的关键点。下面我们就分别介绍其中所涉及的关键技术。
索引数据表
我在前文提到过,含有索引的数据表有索引组织表和哈希组织表。其实,我们在分布式数据库中最常见的是 Google 的 BigTable 论文所提到的 SSTable排序字符串表
Google 论文中的原始描述为SSTable 用于 BigTable 内部数据存储。SSTable 文件是一个排序的、不可变的、持久化的键值对结构,其中键值对可以是任意字节的字符串,支持使用指定键来查找值,或通过给定键范围遍历所有的键值对。每个 SSTable 文件包含一系列的块。SSTable 文件中的块索引(这些块索引通常保存在文件尾部区域)用于定位块,这些块索引在 SSTable 文件被打开时加载到内存。在查找时首先从内存中的索引二分查找找到块,然后一次磁盘寻道即可读取到相应的块。另一种方式是将 SSTable 文件完全加载到内存,从而在查找和扫描中就不需要读取磁盘。
从上面的描述看,我们会发现这些键值对是按照键进行排序的,而且一旦写入就不可变。数据引擎支持根据特定键查询,或进行范围扫描。同时,索引为稀疏索引,它只定位到数据块。查到块后,需要顺序扫描块内部,从而获取目标数据。
下面就是 RocksDB 的 SSTable 结构,可以看到数据是放在前面,后索引作为 metadata 放在文件尾部,甚至 meta 的索引也是放在整个 meta 结构的尾部。
<beginning_of_file>
[data block 1]
[data block 2]
...
[data block N]
[meta block 1: filter block]
[meta block 2: index block]
[meta block 3: compression dictionary block]
[meta block 4: range deletion block]
[meta block 5: stats block]
...
[meta block K: future extended block]
[metaindex block]
[Footer]
<end_of_file>
当然 SSTable 的实现并不一定是通过一个文件,不同的存储引擎会采用不一样的策略去实现它。有的是使用一个文件,如 BigTable 论文中描述的那样,将数据放置在文件开始的部分,索引放在文件结尾。或者将数据和索引分开,放置在不同的文件中。
数据是按照键的顺序放置的,所以不论索引的实现形式如何,数据文件本身是支持范围扫描的。即使使用没有规律的哈希表,数据部分也可以正常支持范围扫描。
这里要注意SSTable 是不可变的也就是输入一旦写入是不可以更改的而修改和删除操作一般也是以写入的形式进行的。这就需要进行合并Compaction将对同一个数据的操作合并为最终的结果。这个过程类似于上文中数据库面临故障崩溃后恢复的过程其中日志回放与合并的基本思想是相同的。关于 SSTable 的详细操作,我们会在 LSM 树这种存储引擎的介绍中详细说明。
当然索引数据表的实现方式不仅仅有 SSTable 一种对数据库索引有所了解的朋友应该都知道B 树家族在索引领域扮演着举足轻重的角色。原因是 B 树的每个节点可以有多个数据,所以可以在高度与宽度上进行平衡,从而有效降低磁盘寻道次数。
但是对 B 树的更新代价是非常高的,故分布式数据库为了写入高效会采用一系列优化手段去提高更新 B 树的效率。这里我们以 MongoDB 的 WiredTiger 存储引擎为例,来介绍其中的一个优化手段。
这个优化方式就是缓存最近的对索引的操作而后将操作固化到磁盘中。WiredTiger 使用 B 树来存储数据在内存页中B 树节点带有一个修改缓冲,这个缓冲保存的一个指向磁盘原始数据的引用。而后,在读取流程中,原始磁盘数据结合内存缓冲数据后,再返回给用户。这么做的好处是,数据的刷新和内存页更新都是由后台线程完成,不会去阻塞读写操作。
以上就是两种带有索引性质的数据表实现的逻辑,从中可以看到提高写入速度的关键点,不是采用顺序的形式写入,就是缓存随机写入,从而转变为顺序写入。
以上介绍的两种数据表都包含内存中的缓冲结构,用以应对内存与磁盘两种设备写入速度差的问题,我在这一讲的后面将会详细介绍其中使用的数据结构。
下面我们再来看看内存缓冲。
内存缓冲
目前有很多种不同的数据结构可以在内存中存储有序的数据。在分布式数据库的存储引擎中有一种结构因其简单而被广泛地使用那就是跳表SkipList
跳表的优势在于其实现难度比简单的链表高不了多少,但是其时间复杂度可以接近负载平衡的搜索树结构。
跳表在插入和更新时避免对节点做旋转或替换,而是使用了随机平衡的概念来使整个表平衡。跳表由一系列节点组成,它们又由不同的高度组成。连续访问高度较高的节点可以跳过高度较低的节点,有点像蜘蛛侠利用高楼在城市内快速移动一样,这也就是跳表名称的来源。现在我们用一个例子来说明跳表的算法细节。请看下面的图片。
如果我们以寻找 15 为例来说明跳表的查找顺序。
首先查找跳表中高度最高的节点从图中可以看到是10。
目标节点 15 比 10 大,从当前高度,也就是最高的高度,向后找没有任何节点,这个时候需要降低一个高度。
高度降低后,找到了节点 22它比 15 要大,这个时候我们又回到了 10 节点,且要继续降低高度。
现在降低到了最低,而后顺利地找到了 15。
如果节点需要插入、删除和修改。就需要进行树的平衡,这个时候需要将节点在不同高度上移动,而且高度也会随着节点的数量而变化。要怎么决定变化的数量呢?答案其实很简单,使用随机数来决定这些变量。随机数虽然不是严格均分数据,但是可以做到相对均匀,且代价很小。这也是该算法被广泛使用的原因:用比较小的代价去实现较好的结果,简而言之,其通入产出比非常可观。
以上就是内存中常用的快速搜索数据结构,那么我们如何判断数据在哪个磁盘文件中呢?答案就是使用布隆过滤。
布隆过滤
以上介绍的内容包含了如何在数据文件以及在数据文件缓冲里查找数据。在查询路径中,我们介绍了,除了向所有数据文件请求查询(也被称作读放大)外,还可以利用布隆过滤快速定位目标数据文件。
布隆过滤的原理是,我们有一个非常大的位数组,首先初始化里面所有的值为 0而后对数据中的键做哈希转换将结果对应的二进制表示形式映射到这个位数组里面这样有一部分 0 转为 1然后将数据表中所有建都如此映射进去。
查找的时候,将查询条件传入的键也进行类似的哈希转换,而后比较其中的 1 是否与数组中的匹配,如果匹配,说明键有可能在这个数据表中。
可以看到,这个算法是一个近似算法,存在误判的可能。也就是所有位置都是 1但是键也可能不在数据表内而这些 1 是由于别的键计算产生的。
但是在查找数据文件的场景中,这个缺陷可以忽略。因为如果布隆过滤判断失败,也只是多浪费一些时间在数据表中查找,从而退化为读放大场景,并不会产生误读的情况。
布隆过滤的原理简单易懂,它对于 LSM 树存储引擎下所产生的大量 SSTable 的检索很有帮助,是重要的优化查询的手段。
二级索引
我以上谈到的所有查询方式都是基于主键索引,但是在真实的场景下,非主键经常需要作为查询条件。这个时候就引入了二级索引的概念。
二级索引一般都是稀疏索引,也就是索引与数据是分离的。索引的结果一般保存的是主键,而后根据主键去查找数据。这在分布式场景下有比较明显的性能问题,因为索引结果所在的节点很可能与数据不在一个节点上。
以上问题的一个可行解决方案是以二级索引的结果也就是主键来分散索引数据也就是在数据表创建时同时创建二级索引。Apache Cassandra 的 SASI 在这方面就是一个很好的例子。它绑定在 SSTable 的生命周期上,在内存缓存刷新或是在数据合并时,二级索引就伴随着创建了。这一定程度上让稀疏的索引有了一定亲和性。
如果要使用键值对实现二级索引,那么索引结果会有如下几种组合方式。
急迫模式:将索引结果快速合并到一个 value 中,而后一次查询就可以查到所以结果。
正常模式:使用多个键值对保留数据。
键组合模式:把索引与结果全都放在 key 上value 是空的。
总体来说,三种模式读取性能接近,但急迫模式的写入性能会低一些。但是对于不同的 key-value 底层实现,其性能会有差别,比如 wisckey将在第 11 讲中介绍)实现的键值分离模式,使用组合模式就有意义。同时由于键组合模式比较简单,且适合键扫描算法的实现,故是一种比较常见二级索引形式。
总结
本讲内容就介绍到这里了。这一讲我们首先说明了分布式索引的概念,实际上它就是分布式数据库存储引擎中用来存储数据的所有技术的总称;而后我介绍了存储引擎的查询路径,帮你在心中建立起存储引擎处理查询的整体概念;最后我又分别介绍了影响查询路径的多个关键技术,并给出了实际的案例。

View File

@ -0,0 +1,112 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 事务处理与恢复(下):如何控制并发事务?
上一讲,我们介绍了事务的基本概念和数据库恢复流程,其中涉及了事务持久性是如何保证的,那么这一讲,我们就重点介绍事务的隔离性。
数据库最强的隔离级别是序列化,它保证从事务的角度看自己是独占了所有资源的。但序列化性能较差,因此我们引入了多种隔离界别来提高性能。在本讲的最后我会介绍分布式数据库中常用的并发控制手段,它们是实现隔离级别的有效方案,其中以多版本方式实现快照隔离最为常见。
现在让我们开始今天的内容。
隔离级别
在谈隔离级别之前我们先聊聊“序列化”Serializability的概念。
序列化的概念与事务调度Schedule密切相关。一个调度包含该事务的全部操作。我们可以用 CPU 调度理论来类比,当一个事务被调度后,它可以访问数据库系统的全部资源,同时会假设没有其他事务去影响数据库的状态。这就类似于一个进程被 CPU 调度,从而独占该 CPU 资源(这里的 CPU 指的是时分系统)。但是实际设计调度时,会允许调度事务内部的操作被重新排序,使它们可以并行执行。这些都是优化操作,但只要不违反 ACID 的原则和结果的正确性就可以了。
那什么是序列化呢?如果一个调度被说成是序列化的,指的是它与其他调度之间的关系:在该调度执行时没有其他被调度的事务并行执行。也就是说,调度是一个接着一个顺序执行的,前一个调度成功完成后,另一个调度再执行。这种方法的一个好处是执行结果比较好预测。但是,我们发现这种做法有明显的缺陷:性能太低。在实现时,一个序列化调度可能会并行执行多个事务操作,但是会保证这样与一个个顺序执行调度有相同的结果。
以上就是序列化的概念,它揭示了序列化也会存在并发执行的情况。这一点很重要,在隔离理论中,一个隔离概念只是描述了一种行为,而在实现层面可以有多种选择,只要保证这个行为的结果符合必要条件就没有问题了。
序列化是最强的事务隔离级别,它是非常完美的隔离状态,可以让并行运行的事务感知不到对方的存在,从而安心地进行自己的操作。但在实现数据库事务时,序列化存在实现难度大、性能差等问题。故数据库理论家提出了隔离级别的概念,用来进行不同程度的妥协。在详解隔离级别之前,来看看我们到底可以“妥协”什么。
这些“妥协”被称为读写异常Anomalies。读异常是大家比较熟悉的有“脏读”“不可重读”和“幻读”。写异常不太为大家所知分别是“丢失更新”“脏写”和“写偏序”。读异常和写异常是分别站在使用者和数据本身这两个角度去观察隔离性的我们将成对介绍它们。传统上隔离级别是从读异常角度描述的但是最近几年一些论文也从写异常角度出发希望你能明白两种表述方式之间是有联系的。下表就是经典隔离级别与读异常的关系。
从中可以看到序列化是不允许任何读写异常存在的。
可重读允许幻读的产生。幻读是事务里面读取一组数据后,再次读取这组数据会发现它们可能已经被修改了。幻读对应的写异常是写偏序。写偏序从写入角度发现,事务内读取一批数据进行修改,由于幻读的存在,造成最终修改的结果从整体上看违背了数据一致性约束。
读到已提交在可重读基础上放弃了不可重读。与幻读类似,但不可重读针对的是一条数据。也就是只读取一条数据,而后在同一个事务内,再读取它数据就变化了。
刚接触这个概念的同学可能会感觉匪夷所思,两者只相差一个数据量,就出现了两个隔离级别。这背后的原因是保证一条数据的难度要远远低于多条,也就是划分这两个级别,主要的考虑是背后的原理问题。而这个原理又牵扯出了性能与代价的问题。因此就像我在本专栏中反复阐述的一样,一些理论概念有其背后深刻的思考,你需要理解背后原理才能明白其中的奥义。不过不用担心,后面我会详细阐述它们之间实现的差别。
而不可重读对应的是丢失更新,与写偏序类似,丢失更新是多个事务操作一条数据造成的。
最低的隔离级别就是读到未提交,它允许脏读的产生。脏读比较简单,它描述了事务可以读到其他事务为提交的数据,我们可以理解为完全没有隔离性。而脏读本身也会造成写异常:脏写。脏写就是由于读到未提交的数据而造成的写异常。
以上,我们详细阐述了经典的隔离级别。但是这套理论是非常古早的,较新的 MVCC 多版本技术所带来的快照隔离又为传统隔离级别增添一个灵活选型。它可以被理解为可重读隔离级别,也就是不允许不可重读。但是在可重读隔离下,是可以保证读取不到数据被修改的。但快照隔离的行为是:一旦读到曾经读过的数据被修改,将立即终止当前事务,也就是进行回滚操作。在多并发事务下,也就是只有一个会成功。你可以细细品味两者的差异。
快照隔离可以解决丢失更新的问题,因为针对同一条数据可以做快照检测,从而发现数据被修改,但是不能防止写偏序的问题。
快照隔离是现代分布式数据库存储引擎最常使用的隔离级别而解决写偏序问题也是存储引擎在该隔离级别下需要解决的问题。SSISerializable Snaphost Isoltion正是解决这个问题的方案我会在“18 | 分布式事务:‘老大难’问题的最新研究与实践”中详细介绍该方案。
至此我们讨论了典型的隔离级别,隔离级别与分布式一致性的关系我在“”中已经有过阐述,如果需要复习,请出门左转。现在让我们接着讨论如何实现这些隔离级别。
并发控制
目前存储引擎引入多种并发模型来实现上面提到的隔离级别,不同的模式对实现不同的级别是有偏好的,虽然理论上每种控制模型都可以实现所有级别。下面我就从乐观与悲观、多版本、基于锁的控制三个方面进行介绍。
乐观与悲观
乐观与悲观的概念类似于并发编程中的乐观锁与悲观锁。但是这里你要注意,实现它们并不一定要借助锁管理。
乐观控制使用的场景是并行事务不太多的情况,也就是只需要很少的时间来解决冲突。那么在这种情况下,就可以使用一些冲突解决手段来实现隔离级别。最常用的方案是进行提交前冲突检查。
冲突检查有多种实现模式,比如最常用的多版本模式。而另一种古老的模式需要检查并行事务直接操作的数据,也就是观察它们操作的数据是否有重合。由于其性能非常差,已经很少出现在现代存储引擎中了。这里需要你注意的是,乐观控制不一定就是多版本这一种实现,还有其他更多的选择。
同样的,悲观控制也不仅仅只有锁这一种方案。一种可能的无锁实现是首先设置两个全局时间戳,最大读取时间与最大写入时间。如果一个读取操作发生的时间小于最大写入时间,那么该操作所在的事务被认为应该终止,因为读到的很可能是旧数据。而一个写操作如果小于最大读取时间,也被认为是异常操作,因为刚刚已经有读取操作发生了,当前事务就不能去修改数据了。而这两个值是随着写入和读取操作而更新的。这个悲观控制被称为 Thomas Write Rule对此有兴趣的话你可以自行搜索学习。
虽然乐观与悲观分别有多种实现方案,但乐观控制最为常见的实现是多版本控制,而悲观控制最常见的就是锁控制。下面我就详细介绍它们。
多版本
多版本并发控制MVCCMultiversion concurrency control是一种实现乐观控制的经典模式。它将每行数据设置一个版本号且使用一个单调递增的版本号生成器来产生这些版本号从而保证每条记录的版本号是唯一的。同时给每个事物分为一个 ID 或时间戳,从而保证读取操作可以读到事务提交之前的旧值。
MVCC 需要区分提交版本与未提交版本。最近一次提交的版本被认为是当前版本,从而可以被所有事务读取出来。而根据隔离级别的不同,读取操作能或者不能读取到未提交的版本。
使用 MVCC 最经典的用法是实现快照隔离。事务开始的时候,记录当前时间,而后该事务内所有的读取操作只能读到当前提交版本小于事务开始时间的数据,而未提交的数据和提交版本大于事务开始时间点的数据是不能读取出来的。如果事务读取的数据已经被其他事务修改,那么该数据应该在上一讲提到的 undo log 中,当前事务还是能够读取到这份数据的。故 undo log 的数据不能在事务提交的时候就清除掉,因为很可能有另外的事务正在读取它。
而当事务提交的时候,数据其实已经写入完成。只需要将版本状态从未提交版本改为提交版本即可。所以 MVCC 中的提交操作是非常快的,这点会对分布式事务有很多启示。
而上文提到的 SSI 模式可以在 MVCC 的基础上引入冲突解决机制从而解决写偏序问题。当提交发生的时候事务会检测其修改和读取的数据在提交之前是否已经被其他已提交事务修改了如果是则会终止当前事务并进行回滚。同时这个冲突检测时机会有两个一个是在事务内进行读取操作时就进行检测称为前向检测forward。而相对的在提交时进行检测被称为后向检测backward。你会明显感觉到前者会快速失败但是性能较低而后者对异常的反应较慢但速度会有优势。
这就是经典的 MVCC 并发控制,现在让我接着介绍典型的悲观控制:锁控制。
基于锁的控制
基于锁的控制是典型的悲观控制。它会使用显示的锁来控制共享资源,而不是通过调度手段来实现。锁控制可以很容易实现“序列化操作”,但是它同时存在锁竞争和难扩展等问题。
一个比较知名的锁技术是两阶段锁2PL它将锁操作总结为两个阶段。
锁膨胀阶段。在该过程中,事务逐步获得所有它需要的锁,同时不释放任何锁。这期间事务可以对加锁的数据进行操作。
锁收缩阶段。该过程中,在上一过程中获得的锁全部被释放。这个事务是逐步的,这期间事务依然可以对还持有锁的数据进行操作。
以上过程简单明了,它是针对一次性加锁提出来的,一次性加锁的缺点是没有并发度,性能低;而两阶段锁可以保证一定的并发度,但其缺点是会有死锁的产生。
死锁是两个事务互相持有对方的锁,从而造成它们都无法继续运行。解决死锁需要引入超时机制,但超时机制又有明显的性能缺憾。此时,人们会引入死锁检测机制来尽早发现死锁。一般实现手段是将所有事务的锁依赖构建成一棵依赖图,而后使用图算法来发现其中的环形死锁结构,从而快速判断死锁的产生。
而与锁相对的一个概念就是“闩”latch读“shuān”。一般资料说闩是轻量的锁是重量的这其实体现在两个方面。
一是说它们处理的对象。闩一般用在粒度很小的数据中,比如数据块、索引树的节点等。而锁一般作用在大颗粒操作,如锁定多行数据、事务和修改存储结构等。
二是它们本身的实现不同。闩一般使用 CAS 执行是基于比较而后设置的无锁指令级别的操作。如果原始值发生变化就重新进行以上操作这个过程叫自旋spin。而锁是使用独立的资源且有锁管理器来控制。可想而知调度锁也是一个比较耗时且复杂的过程。
这里就要解释上文中隔离级别“序列化”和“可重读”之间实现的差异了。“序列化”由于要保证一组数据重复读取的一致性,就需要引入重量级的锁,其代价是很高的;而“可重读”只需要保证一行数据重复读取是一致的,它可以使用轻量级的闩来实现。故隔离级别将它们分成两种是非常合理的,因为从原理看,它们是完全不同的。
以上就是关于基于锁的控制的相关内容。
总结
本讲内容就介绍到这里了。事务是我们课程到目前为止最长的内容,用了两讲的篇幅来详细介绍。事务的话题在数据库领域一直很热门,我从事务原理层面切入,解释了 ACID 和不同隔离级别所需要的技术手段。这些内容为分布式事务的学习打下坚实的基础,同时你可以将本专栏作为一份参考资料,随时进行查阅。
从本质出发,事务是一个面向使用者的概念,它向使用者提供一种契约,目的是使人们可以可靠地使用数据库保存和共享数据,这是数据库最核心的功能,且有众多的应用是基于该功能构建的,这也是分布式数据库为什么要实现分布式条件下的事务的根本原因。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 引擎拓展:解读当前流行的分布式存储引擎
这一讲是存储引擎模块的最后一讲,通过这一个模块的学习,相信你已经对存储引擎的概念、使用方法与技术细节有了全方位的认识。本讲我们先总结一下模块二的主要内容,并回答大家提到的一些典型问题;而后我会介绍评估存储引擎的三个重要元素;最后为你介绍目前比较流行的面向分布式数据库的存储引擎。
让我们先进行本模块的内容回顾。
存储引擎回顾
存储引擎是数据库的核心组件,起到了物理模型与逻辑模型之间的沟通作用,是数据库重要功能,是数据写入、查询执行、高可用和事务等操作的主要承担者。可谓理解存储引擎也就掌握了数据库的主要功能。
在这个模块里,我首先向你介绍了存储引擎在整个数据库中的定位,点明了它其实是本地执行模块的组成部分;而后通过内存与磁盘、行式与列式等几组概念的对比,介绍了不同种类的存储引擎的实现差异;并最终说明了分布式数据库存储引擎的特点,即面向内存、列式和易于散列。
在第 8 讲中,我介绍了分布式数据库的索引。着重说明了存储引擎中大部分数据文件其实都是索引结构;而后带着你一起探讨了典型分布式数据库存储引擎的读取路径,并介绍了该路径上的一些典型技术,如索引数据表、内存跳表、布隆过滤和二级索引等。
接着我介绍了一个在分布式数据库领域内非常流行的存储引擎LSM 树。介绍了其具体的结构、读写修改等操作流程;重点说明了合并操作,它是 LSM 树的核心操作,直接影响其性能;最后介绍了 RUM 假说,它是数据库优化的一个经典取舍定律。
最后,我们探讨了存储引擎最精华的概念,就是事务。我用了两讲的篇幅,详细为你阐述事务的方方面面。总结一下,事务其实是数据库给使用者的一个承诺,即 ACID。为了完成这个承诺数据库动用了存储引擎中众多的功能模块。其中最重要的事务管理器同时还需要页缓存、提交日志和锁管理器等组件来进行配合。故在实现层面上事务的面貌是很模糊的它同时具备故障恢复和并发控制等特性这是由其概念是建立在最终使用侧而造成的。
事务部分我们主要抓住两点:故障恢复+隔离级别。前者保证了数据库存储数据不会丢失,后者保证并发读写数据时的完整性;同时我们要将事务与模块一中的分布式一致性做区别,详细内容请你回顾第 5 讲。
在事务部分,有同学提到了下面这个问题,现在我来为你解答。
当内存数据刷入磁盘后,同时需要对日志做“截取”操作,这个截取的值是什么?
这个“截取”是一个形象的说法,也就是可以理解为截取点之前的数据已经在输入磁盘中。当进行数据库恢复的时候,只要从截取点开始恢复数据库即可,这样大大加快了恢复速度,同时也释放了日志的空间。这个截取点,一般被称为检查点。相关细节,你可以自行学习。
以上我们简要回顾了本模块的基本知识。接下来,我将带你领略当代分布式数据库存储引擎的一些风采。但是开始介绍之前,我们需要使用一个模型来评估它们的特点。
评估存储引擎的黄金三角
存储引擎的特点千差万别,各具特色。但总体上我们可以通过三个变量来描述它们的行为:缓存的使用方式,数据是可变的还是不可变的,存储的数据是有顺序的还是没有顺序的。
缓存形式
缓存是说存储引擎在数据写入的时候,首先将它们写入到内存的一个片段,目的是进行数据汇聚,而后再写入磁盘中。这个小片段由一系列块组成,块是写入磁盘的最小单位。理想状态是写入磁盘的块是满块,这样的效率最高。
大部分存储引擎都会使用到缓存。但使用它的方式却很不相同,比如我将要介绍的 WiredTiger 缓存 B 树节点,用内存来抵消随机读写的性能问题。而我们介绍的 LSM 树是用缓存构建一个有顺序的不可变结构。故使用缓存的模式是衡量存储引擎的一个重要指标。
可变/不可变数据
存储的数据是可变的还是不可变的,这是判断存储引擎特点的另一个维度。不可变性一般都是以追加日志的形式存在的,其特点是写入高效;而可变数据,以经典 B 树为代表,强调的是读取性能。故一般认为可变性是区分 B 树与 LSM 树的重要指标。但 BW-Tree 这种 B 树的变种结构虽然结构上吸收了 B 树的特点,但数据文件是不可变的。
当然不可变数据并不是说数据一直是不变的而是强调了是否在最影响性能的写入场景中是否可变。LSM 树的合并操作,就是在不阻塞读写的情况下,进行数据文件的合并与分割操作,在此过程中一些数据会被删除。
排序
最后一个变量就是数据存储的时候是否进行排序。排序的好处是对范围扫描非常友好,可以实现 between 类的数据操作。同时范围扫描也是实现二级索引、数据分类等特性的有效武器。如本模块介绍的 LSM 树和 B+ 树都是支持数据排序的。
而不排序一般是一种对于写入的优化。可以想到,如果数据是按照写入的顺序直接存储在磁盘上,不需要进行重排序,那么其写入性能会很好,下面我们要介绍的 WiscKey 和 Bitcask 的写入都是直接追加到文件末尾,而不进行排序的。
以上就是评估存储引擎特点的三个变量,我这里将它们称为黄金三角。因为它们是互相独立的,彼此并不重叠,故可以方便地评估存储引擎的特点。下面我们就试着使用这组黄金三角来评估目前流行的存储引擎的特点。
B 树类
上文我们提到过评估存储引擎的一个重要指标就是数据是否可以被修改,而 B 树就是可以修改类存储引擎比较典型的一个代表。它是目前的分布式数据库乃至于一般数据库最常采用的数据结构。它是为了解决搜索树BST等结构在 HDD 磁盘上性能差而产生的,结构特点是高度很低,宽度很宽。检索的时候从上到下查找次数较少,甚至如 B+ 树那样,可以完全把非叶子节点加载到内存中,从而使查找最多只进行一次磁盘操作。
下面让我介绍几种典型的 B 树结构的存储引擎。
InnoDB
InnoDB 是目前 MySQL 的默认存储引擎,同时也是 MariaDB 10.2 之后的默认存储引擎。
根据上文的评估指标看,它的 B+ 树节点是可变的,且叶子节点保存的数据是经过排序的。同时由于数据的持续写入,在高度不变的情况下,这个 B+ 树一定会横向发展,从而使原有的一个节点分裂为多个节点。而 InnoDB 使用缓存的模式就是:为这种分裂预留一部分内存页面,用来容纳可能的节点分裂。
这种预留的空间其实就是一种浪费,是空间放大的一种表现。用 RUM 假设来解释InnoDB 这种结构是牺牲了空间来获取对于读写的优化。
在事务层面InnoDB 实现了完整的隔离级别,通过 MVCC 机制配合各种悲观锁机制来实现不同级别的隔离性。
WiredTiger
WiredTiger 是 MongoDB 默认的存储引擎。它解决了原有 MongoDB 必须将大部分数据放在内存中,当内存出现压力后,数据库性能急剧下降的问题。
它采用的是 B 树结构,而不是 InnoDB 的 B+ 树结构。这个原因主要是 MongoDB 是文档型数据库,采用内聚的形式存储数据(你可以理解为在关系型数据库上增加了扩展列)。故这种数据库很少进行 join 操作,不需要范围扫描且一次访问就可以获得全部数据。而 B 树每个层级上都有数据,虽然查询性能不稳定,但总体平均性能是要好于 B+ 树的。
故 WiredTiger 首先是可变数据结构,同时由于不进行顺序扫描操作,数据也不是排序的。那么它是如何运用缓存的呢?这个部分与 InnoDB 就有区别了。
在缓存中每个树节点上,都配合一个更新缓冲,是用跳表实现的。当进行插入和更新操作时,这些数据写入缓冲内,而不直接修改节点。这样做的好处是,跳表这种结构不需要预留额外的空间,且并发性能较好。在刷盘时,跳表内的数据和节点页面一起被合并到磁盘上。
由此可见WiredTiger 牺牲了一定的查询性能来换取空间利用率和写入性能。因为查询的时候出来读取页面数据外,还要合并跳表内的数据后才能获取最新的数据。
BW-Tree
BW-Tree 是微软的 Azure Cosmos DB 背后的主要技术栈。它其实通过软件与硬件结合来实现高性能的类 B 树结构,硬件部分的优化使用 Llama 存储系统,有兴趣的话你可以自行搜索学习。我们重点关注数据结构方面的优化。
BW-Tree 为每个节点配置了一个页面 ID而后该节点的所有操作被转换为如 LSM 树那样的顺序写过程,也就是写入和删除操作都是通过日志操作来完成的。采用这种结构很好地解决了 B 树的写放大和空间放大问题。同时由于存在多个小的日志,并发性也得到了改善。
刷盘时从日志刷入磁盘将随机写变为了顺序写同样提高了刷盘效率。我们会发现BW-Tree 也如 LSM 树一样存在读放大问题,即查询时需要将基础数据与日志数据进行合并。而且如果日志太长,会导致读取缓慢。而此时 Cosmos 采用了一种硬件的解决方案,它会感知同一个日志文件中需要进行合并的部分,将它们安排在同一个处理节点,从而加快日志的收敛过程。
以上就是典型的三种 B 树类的存储引擎,它们各具特色,对于同一个问题的优化方式也带给我们很多启发。
LSM 类
这个模块我专门用了一个完整篇章来阐述它的特点,它是典型的不可变数据结构,使用缓存也是通过将随机写转为顺序写来实现的。
我们在说 LSM 树时介绍了它存储的数据是有顺序的,其实目前有两种无顺序的结构也越来越受到重视。
经典存储
经典的 LSM 实现有 LeveledDB和在其基础之上发展出来的 RocksDB。它们的特点我们之前有介绍过也就是使用缓存来将随机写转换为顺序写而后生成排序且不可变的数据。它对写入和空间友好但是牺牲了读取性能。
Bitcask
Bitcask 是分布式键值数据库 Riak 的一种存储引擎,它也是一种典型的无顺序存储结构。与前面介绍的典型 LSM 树有本质上的不同,它没有内存表结构,也就是它根本不进行缓存而是直接将数据写到数据文件之中。
可以看到,其写入是非常高效的,内存占用也很小。但是如何查询这种“堆”结构的数据呢?答案是在内存中有一个叫作 Keydir 的结构保存了指向数据最新版本的引用,旧数据依然在数据文件中,但是没有被 Keydir 引用最终就会被垃圾收集器删除掉。Keydir 实际上是一个哈希表,在数据库启动时,从数据文件中构建出来。
这种查询很明显改善了 LSM 树的读放大问题,因为每条数据只有一个磁盘文件引用,且没有缓存数据,故只需要查询一个位置就可以将数据查询出来。但其缺陷同样明显:不支持范围查找,且启动时,如果数据量很大,启动时间会比较长。
此种结构优化了写入、空间以及对单条数据的查找,但牺牲了范围查找的功能。
WiscKey
那么有没有一种结构既能利用无顺序带来的高速写入和空间利用率上的优点又可以支持非常有用的范围查询呢WiscKey 结构正是尝试解决这个问题的一个手段。
它的特点是将 Key 和 Value 分别放在两个文件中。Key 还是按照 LSM 树的形式,这样就保证了 Key 是有顺序的,可以进行范围扫描。同时使用 LSM 树,即不需要将所有的 Key 放到内存里,这样也解决了 Bitcask 加载慢的问题。
而 Value 部分称为 vLogsvalue Logs其中的数据是没有顺序的。这种结构适合更新和删除比较少的场景因为范围扫描会使用随机读如果更新删除很多那么其冲突合并的效率很低。同时在合并操作的时候需要扫描 Key 而后确定合并方案,这个在普通的 LSM 树中也是不存在的。
WiscKey 非常适合在 SSD 进行运行,因为读取 Value 需要进行随机读取。目前 dgraph.io 的 Badger 是该模式比较成熟的实现。
总结
到这里,这一讲内容就说完了。我带你回顾了第二模块的主要内容,这是一个基础知识普及模块,将为接下来的分布式模块打下基础。同时相对于传统关系型数据库,分布式数据库的存储引擎也有其自身特点,如 LSM 树结构,你需要认真掌握这种结构。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 概要:分布式系统都要解决哪些问题?
在学习了存储引擎相关内容之后,从这一讲开始,我们就进入新的模块——分布式数据库最核心的部分,那就是分布式系统。
分布式数据库区别于传统数据库的一个重要特性就是其分布式的特点,这些特点来源于分布式理论的发展,特别是数据分布相关理论的发展。相比于无状态分布式系统,有状态的数据库在分布式领域中将会面对更多的挑战。
本讲内容作为整个模块三的引子,我将会向你提出一系列问题,而在后续的课程中,我会逐一回答这些问题。那么现在让我们从失败模型开始,讨论分布式模式下的数据库吧。
失败模型
分布式系统是由多个节点参与其中的,它们直接通过网络进行互联。每个节点会保存本地的状态,通过网络来互相同步这些状态;同时节点需要访问时间组件来获取当前时间。对于分布式系统来说,时间分为逻辑时间与物理时间。逻辑时间一般被实现为一个单调递增的计数器,而物理时间对应的是一个真实世界的时间,一般由操作系统提供。
以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的,“不可靠”贯穿了分布式系统的整个生命周期。而总结这些不可靠就成为失败模型所解决的问题。
在介绍失败模型的具体内容之前,让我们打开思路,看看有哪些具体的原因引起了分布式系统的可靠性问题。
引起失败的原因
当讨论分布式系统内的不稳定因素的时候,人们首先会想到网络问题,但是一个最容易让大家忽略的地方就是远程节点处理请求时也可能发生故障。一个比较常见的误区就是认为远程执行会马上返回结果,但这种假设是非常不可靠的。因为远程节点的处理能力、运行环境其实是未知的,我们不能认为它们会一直按照固定的模式去响应我们的请求。
而另一种情况是,请求到达远程节点后很可能不会被马上处理,而是放在了一个队列里面进行缓冲。这对于远程节点的吞吐量改善是有好处的,但是这在一定程度上带来了延迟,从而深刻地影响了交互模式。处理以上问题的方式就是需要引入故障检测(我会在下一讲介绍),来观察远程节点的运行情况,从而针对不同的问题采取不同的应对手段。
第二种常见的误解是所有节点时间是一致的,这种误解是非常普遍并且危险的。虽然可以使用工具去同步集群内的时间,但是要保持系统内时间一致是非常困难的。而如果我们使用不同节点产生的物理时间来进行一致性计算或排序,那么结果会非常不靠谱。所以大部分分布式数据库会用一个单独的节点来生成全局唯一的逻辑时间以解决上面的问题。而有些分布式数据库,如 Spanner 会使用原子钟这种精密服务来解决时间一致的问题。
本地物理时间的另一个问题是会产生回溯,也就是获取一个时间并执行若干步骤后,再去获取当前时间,而这个时间有可能比之前的时间还要早。也就是说我们不能认为系统的物理时间是单调递增的,这就是为什么要使用逻辑时间的另一个重要的原因。
但是本地物理时间在分布式系统中某些部分依然扮演着重要的作用,如判断远程节点超时等。但是基于以上两点,我们在实现分布式算法时应将时间因素考虑进去,从而避免潜在的问题。
以上谈到的分布式问题集中在节点层面,而另一大类问题就是网络造成的了。其中最为经典的问题就是网络分区,它指的是分布式系统的节点被网络故障分割为不同的小块。而最棘手的是,这些小块内的节点依然可以提供服务。但它们由于不能很好地感知彼此的存在,会产生不一致的问题,这个我们在模块一“”有过比较详细的论述。
这里需要注意的是,网络分区带来的问题难以解决,因为它是非常难发现的。这是由于网络环境复杂的拓扑和参与者众多共同左右而导致的。故我们需要设计复杂的算法,并使用诸如混沌工程的方式来解决此类问题。
最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。
解决级联故障的方式有退避算法和断路。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。
以上就是分布式系统比较常见的故障。虽然你可能会觉得这些故障很直观,但是如果要去解决它们思路会比较分散。还好前人已经帮我们总结了一些模型来对这些故障进行分级,从而有的放矢地解决这些问题。接下来我就要为你介绍三种典型的失败模型。
崩溃失败
当遭遇故障后,进程完全停止工作被称为崩溃失败。这是最简单的一种失败情况,同时结果也非常好预测。这种失败模式也称为崩溃停止失败,特别强调失败节点不需要再参与回分布式系统内部了。我们说这种模式是最容易预测的,是因为失败节点退出后,其他节点感知到之后可以继续提供服务,而不用考虑它重新回归所带来的复杂问题。
虽然失败停止模式有以上的优点,但实际的分布式系统很少会采用。因为它非常明显地会造成资源浪费,所以我们一般采用崩溃恢复模式,从而重复利用资源。提到崩溃节点恢复,一般都会想到将崩溃节点进行重启,而后经过一定的恢复步骤再加入网络中。虽然这是一种主流模式,但其实通过数据复制从而生成备份节点,而后进行快速热切换才是最为主流的模式。
崩溃失败可以被认为是遗漏失败的一种特殊情况。因为从其他节点看,他们很难分清一个节点服务响应是由于崩溃还是由于遗漏消息而产生的。那究竟什么是遗漏失败呢?
遗漏失败
遗漏失败相比于崩溃失败来说更为不可预测,这种模式强调的是消息有没有被远程节点所执行。
这其中的故障可能发生在:
消息发送后没有送达远程节点;
远程节点跳过消息的处理或根本无法执行(一种特例就是崩溃失败,节点无法处理消息);
后者处理的结果无法发送给其他节点。
总之,从其他节点的角度看,发送给该节点的消息石沉大海,没有任何响应了。
上文提到的网络分区是遗漏失败的典型案例,其中一部分节点间消息是能正常收发的,但是部分节点之间消息发送存在困难。而如果崩溃失败出现,集群中所有节点都将无法与其进行通讯。
另一种典型情况就是一个节点的处理速度远远慢于系统的平均水平,从而导致它的数据总是旧的,而此时它没有崩溃,依然会将这些旧数据发送给集群内的其他节点。
当远程节点遗漏消息时,我们是可以通过重发等可靠连接手段来缓解该问题的。但是如果最终还是无法将消息传递出去,同时当前节点依然在继续提供服务,那么此时遗漏失败才会产生。除了以上两种产生该失败的场景,遗漏失败还会发生在网络过载、消息队列满等场景中。
下面为你介绍最后一种失败模型,即拜占庭失败。
拜占庭失败
拜占庭失败又称为任意失败,它相比于上述两种失败是最不好预测的。所谓任意失败是,参与的节点对请求产生不一致的响应,一个说当前数据是 A而另一个却说它是 B。
这个故障往往是程序 Bug 导致的可以通过严格软件开发流程管理来尽可能规避。但我们都清楚Bug 在生产系统中是很难避免的,特别是系统版本差异带来的问题是极其常见的。故在运行态,一部分系统并不信任直接从远程节点获得的数据,而是采用交叉检测的方式来尽可能得到正确的结果。
另一种任意失败是一些节点故意发送错误消息目的是想破坏系统的正常运行从而牟利。采用区块链技术的数字货币系统则是使用正面奖励的模式BFT来保证系统内大部分节点不“作恶”做正确事的收益明显高于作恶
以上就是三种比较常见的失败模型。模块三的绝大部分内容主要是面向崩溃恢复的场景的。那么下面我们来梳理一下本模块接下来内容的讲解脉络。
错误侦测与领导选举
要想解决失败问题,首先就是要进行侦测。在本模块的开始部分,我们会研究使用什么手段来发现系统中的故障。目前,业界有众多方式来检测故障的产生,他们是在易用性、精确性和性能之间做平衡。
而错误侦测一个重要应用领域就是领导选举。使用错误侦测技术来检测领导节点的健康状态,从而决定是否选择一个新节点来替代已经故障的领导节点。领导节点的一个主要作用就是缓解系统发生失败的可能。我们知道系统中如果进行对等同步状态的代价是很高昂的,如果能选择一个领导节点来统一进行协调,那么会大大降低系统负载,从而避免一些失败的产生。
而一旦侦测到失败的产生,如何解决它就是我们需要考虑的内容啦。
复制与一致性
故障容忍系统Fault-tolerant一般使用复制技术产生多个副本来提供系统的可用性。这样可以保证当系统总部分节点发生故障后仍然可以提供正常响应。而多个副本会产生数据同步的需求一致性就是保证数据同步的前提。就像我在模块一中描述的那样没有复制技术一致性与同步就根本不存在。
模块一我们讨论的是 CAP 理论和强一致性模型,它们都是数据一致的范畴。本模块我们会接着讨论客户端一致,或称为会话一致。同时会讨论最终一致这种弱一致模型,最终一致模型允许系统中存在状态不一致的情况,但我们希望尽可能使系统保持一致,这时候会引入反熵手段来解决副本之间不一致的问题。
而后我们会接着讨论分布式事务,它与一致性存在着联系但又有很明显的区别。同时相比于模块二中的经典事务,分布式事务由于需要解决上文表述的各种失败情况,其处理是比较特殊的,比如需要进行事务协调来处理脑裂问题。
共识
最后我们将介绍分布式系统的精华:共识算法。以上介绍的很多内容,包括错误侦测、领导选举、一致性和分布式事务都涵盖在共识算法内,它是现代分布式数据库重要的组件。
共识算法是为了解决拜占庭将军问题而产生的。简单来说,在从前,拜占庭将军问题被认为是一个逻辑上的困境,它说明了一群拜占庭将军在试图就下一步行动达成统一意见时,可能存在的沟通问题。
该困境假设每个将军都有自己的军队,每支军队都位于他们打算攻击的城市周围的不同位置,这些将军需要就攻击或撤退达成一致。只要所有将军达成共识,即协调后决定共同执行,无论是攻击还是撤退都无关紧要。
基于著名的 FLP 不可能问题的研究,拜占庭将军们面临三种困境:
将军们没有统一的时间(没法对表);
无法知道别的将军是否被击败;
将军们之间的通讯是完全异步的。
由于以上的困境,我们是没有任何办法使将军们最终在特定时间内达成一致性意见的,也就是说共识算法在上述困境下是完全不可能的。
但是共识算法使用逻辑时钟来提供统一时间,并引入错误侦测技术来确定参与节点的情况,从而在完全异步的通讯情况下可以实现分布式系统的共识。本模块最后一部分,我会介绍几种经典的共识算法,并介绍它们的使用案例。
共识可以解决遗漏失败,因为只要系统内大部分节点达成共识,剩下的节点即使遗漏该消息,也能对外提供正确的数据。
总结
这一讲是模块三的引导课,我首先为你介绍了失败模型的概念,它是描述分布式数据库内各种可能行为的一个准则;而后根据失败模型为你梳理了本模块的讲解思路。
分布式算法根据目标不同可能分为下面几种行为模式,这些模式与对应的课时如下表所示。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 数据可靠传播:反熵理论如何帮助数据库可靠工作?
上一讲我们完整地介绍了一致性的概念,其中一致性程度最低的是最终一致性。在最终一致性的条件下,节点间需要经过一段时间的数据同步,才能将最新数据在节点间进行分发。这就需要这些最新产生的数据能在节点间稳定地传播。
但是,现实是非常无情的,数据传播中会遇到各种故障,如节点崩溃失败、网络异常、同步数据量巨大造成延迟高等情况,最终会造成最终一致性集群内部节点间数据差异巨大。随着时间的推移,集群向着越来越混乱的局面恶化。
以上描述的场景就是“熵增”。这是一个物理学概念,在 2020 年上映的影片“Tenet”中对“熵”的概念有过普及其中把熵描述为与时间有关好像熵增就是正向时间熵减就是时间倒流。
其实熵与时间之间是间接关系。19 世纪的时候,科学家发现不借助外力,热力总是从高温物体向低温物理传播,进而出现一个理论:在封闭系统内且没有外力作用下,熵总是增的。而时间也是跟随熵增一起向前流动的。影片假设,如果能将熵减小,时间就应该可以随之倒流。
熵的概念深入了各个领域中,一般都表示系统总是向混乱的状态变化。在最终一致性系统中,就表示数据最终有向混乱方向发展的趋势,这个时候我们就要引入“反熵”机制来施加“外力”,从而消除自然状态的“熵增”所带来的影响。
说了这么多,简而言之,就是通过一些外部手段,将分布式数据库中各个节点的数据达到一致状态。那么反熵的手段包含:前台同步、后台异步与 Gossip 协议。现在让我来一一为你介绍。
前台同步
前台同步是通过读与写这两个前台操作同步性地进行数据一致性修复。它们分别称为读修复Read Repair和暗示切换Hinted Handoff
读修复
随着熵逐步增加,系统进入越来越混乱的状态。但是如果没有读取操作,这种混乱其实是不会暴露出去的。那么人们就有了一个思路,我们可以在读取操作发生的时候再来修复不一致的数据。
具体操作是,请求由一个总的协调节点来处理,这个协调节点会从一组节点中查询数据,如果这组节点中某些节点有数据缺失,该协调节点就会把缺失的数据发送给这些节点,从而修复这些节点中的数据,达到反熵的目的。
有的同学可能会发现,这个思路与上一讲的可调节一致性有一些关联。因为在可调节一致性下,读取操作为了满足一致性要求,会从多个节点读取数据从而发现最新的数据结果。而读修复会更进一步,在此以后,会将落后节点数据进行同步修复,最后将最新结果发送回客户端。这一过程如下图所示。
当修复数据时,读修复可以使用阻塞模式与异步模式两种。阻塞模式如上图所示,在修复完成数据后,再将最终结果返还给客户端;而异步模式会启动一个异步任务去修复数据,而不必等待修复完成的结果,即可返回到客户端。
你可以回忆一下,阻塞的读修复模式其实满足了上一讲中客户端一致性提到的读单增。因为一个值被读取后,下一次读取数据一定是基于上一次读取的。也就是说,同步修复的数据可以保证在下一次读取之前就被传播到目标节点;而异步修复就没有如此保证。但是阻塞修复同时丧失了一定的可用性,因为它需要等待远程节点修复数据,而异步修复就没有此问题。
在进行消息比较的时候,我们有一个优化的手段是使用散列来比较数据。比如协调节点收到客户端请求后,只向一个节点发送读取请求,而向其他节点发送散列请求。而后将完全请求的返回值进行散列计算,与其他节点返回的散列值进行比较。如果它们是相等的,就直接返回响应;如果不相等,将进行上文所描述的修复过程。
这种散列模式的一个明显好处是在系统处于稳定的状态时,判断数据一致性的代价很小,故可以加快读取速度并有效降低系统负载。常用的散列算法有 MD5 等。当然,理论上散列算法是有碰撞的可能性的,这意味着一些不一致状态无法检测出来。首先,我们要说在真实场景中,这种碰撞概率是很低的,退一万步讲,即使发生碰撞,也会有其他检测方来修复该差异。
以上就是在读取操作中进行的反熵操作,那么在写入阶段我们如何进行修复呢?下面我来介绍暗示切换。
暗示切换
暗示切换名字听起来很玄幻。其实原理非常明了,让我们看看它的过程,如下图所示。
客户端首先写入协调节点。而后协调节点将数据分发到两个节点中这个过程与可调节一致性中的写入是类似的。正常情况下可以保证写入的两个节点数据是一致的。如果其中的一个节点失败了系统会启动一个新节点来接收失败节点之后的数据这个结构一般会被实现为一个队列Queue即暗示切换队列HHQ
一旦失败的节点恢复了回来HHQ 会把该节点离线这一个时间段内的数据同步到该节点中,从而修复该节点由于离线而丢失的数据。这就是在写入节点进行反熵的操作。
以上介绍的前台同步操作其实都有一个限制就是需要假设此种熵增过程发生的概率不高且范围有限。如果熵增大范围产生那么修复读会造成读取延迟增高即使使用异步修复也会产生很高的冲突。而暗示切换队列的问题是其容量是有限的这意味着对于一个长期离线的节点HHQ 可能无法保存其全部的消息。
那么有没有什么方式能处理这种大范围和长时间不一致的情况呢?下面我要介绍的后台异步方式就是处理此种问题的一些方案。
后台异步
我们之前介绍的同步方案主要是解决最近访问的数据,那么将要介绍的后台异步方案主要面向已经写入较长时间的数据,也就是不活跃的数据。进而使用这种方案也可以进行全量的数据一致性修复工作。
而后台方案与前台方案的关注点是不同的。前台方案重点放在修复数据而后台方案由于需要比较和处理大量的非活跃数据故需要重点解决如何使用更少的资源来进行数据比对。我将要为你介绍两种比对技术Merkle 树和位图版本向量。
Merkle 树
如果想要检查数据的差异,我们一般能想到最直观的方式是进行全量比较。但这种思路效率是很低的,在实际生产中不可能实行。而通过 Merkle 树我们可以快速找到两份数据之间的差异,下图就是一棵典型的 Merkle 树。
树构造的过程是:
将数据划分为多个连续的段。而后计算每个段的哈希值,得到 hash1 到 hash4 这四个值;
而后,对这四个值两两分组,使用 hash1 和 hash2 计算 hash5、用 hash3 和 hash4 计算 hash6
最后使用 hash5 和 hash6 计算 top hash。
你会发现数据差异的方式类似于二分查找。首先比较两份数据的 top hash如果不一致就向下一层比较。最终会找到差异的数据范围从而缩小了数据比较的数量。而两份数据仅仅有部分不同都可以影响 top hash 的最终结果,从而快速判断两份数据是否一致。
Merkle 树结合了 checksum 校验与二叉树的特点,可以帮助我们快速判断两份数据是否存在差异。但如果我们想牺牲一定精准性来控制参与比较的数据范围,下面介绍的位图版本向量就是一种理想的选择。
位图版本向量
最近的研究发现,大部分数据差异还是发生在距离当前时间不远的时间段。那么我们就可以针对此种场景进行优化,从而避免像 Merkle 树那样计算全量的数据。而位图版本向量就是根据这个想法发展起来的。
这种算法利用了位图这一种对内存非常友好的高密度数据格式,将节点近期的数据同步状态记录下来;而后通过比较各个节点间的位图数据,从而发现差异,修复数据。下面我用一个例子为你展示这种算法的执行过程,请看下图。
如果有三个节点,每个节点包含了一组与其他节点数据同步的向量。上图表示节点 2 的数据同步情况。目前系统中存在 8 条数据,从节点 2 的角度看,每个节点都没有完整的数据。其中深灰色的部分表明同步的数据是连续的,我们用一个压缩的值表示。节点 1 到 3 这个压缩的值分别为 3、5 和 2。可以看到节点 2 自己的数据是连续的。
数据同步一旦出现不连续的情况,也就是出现了空隙,我们就转而使用位图来存储。也就是图中浅灰色和白色的部分。比如节点 2 观察节点 1可以看到有三个连续的数据同步而后状态用 00101 来表示(浅灰色代表 1白色代表 0。其中 1 是数据同步了,而 0 是数据没有同步。节点 2 可以从节点 1 和节点 3 获取完整的 8 条数据。
这种向量列表除了具有内存优势外,我们还可以很容易发现需要修复数据的目标。但是它的一个明显缺点与暗示切换队列 HHQ 类似,就是存储是有限的,如果数据偏差非常大,向量最终会溢出,从而不能比较数据间的差异。但不要紧,我们可以用上面提到的 Merkle 来进行全量比较。
以上我介绍了一些常见的反熵手段,它们都可以很好地解决数据一致性问题。但是我们会发现相对于传统的领导节点数据同步,它们同步数据的速度是不好度量的,而且会出现部分节点长期不进行同步的状态。那么有没有一种模式可以提高数据同步的效率呢?答案是肯定的,那就是 Gossip 协议。
Gossip 协议
Gossip 协议可以说是传播非常广泛的分布式协议。因为它的名字非常地形象,用幽默的东北话来说就是“传闲话”。大家可以想象一个东北乡村,屯头树下大家聚在一起“张家长李家短”。一件事只需一会儿整个村庄的人都全知道了。
Gossip 协议就是类似于这种情况。节点间主动地互相交换信息最终达到将消息快速传播的目的。而该协议又是基于病毒传播模型设计的。2020 年是新冠疫情的灾年,大家都对病毒传播有了深刻理解,那么我现在就用病毒传播模型来解释 Gossip 协议的消息传播模式。
最开始,集群中一个节点产生了一条消息,它的状态为“已感染”。而其他节点我们认为是“易感节点”,这类似于新冠的易感人群。一旦该消息从已感染节点传播到易感节点,这个易感节点把自己的状态转换为已感染,而后接着进行传播。
这里,选择传播的目标使用一个随机函数,从而可以很好地将“病毒”扩展到整个集群中。当然,如果已感染节点不愿意传染其他节点,类似于它被隔离了起来,在其上的消息经过一段时间后会被移除。
我们可以看到 Gossip 模式非常适合于无主集群的数据同步,也就是不管集群中有多少节点参与,消息都可以很健壮地在集群内传播。当然,消息会重复传播到同一个节点上,在实现算法的时候,我们需要尽量减少这种重复数据。
另一个对算法成败重要的影响因素是消息用多快的速度在集群内传播,越快传播不仅会减少不一致的时间,同时可以保证消息不容易丢失。现在我通过几个特性来描述算法的行为。
换出数量。它表示为节点选择多少个相邻节点来传播数据。我们很容易知道,当这个值增大后,数据就能更快地传播。但这个值增大同样会增加重复数据的比例,从而导致集群负载增加吞吐量下降。所以我们需要对重复数据进行监控,来实时调整换出数量。
传播延迟。这种延迟与我们之前提到的复制延迟不同,它描述的是消息传播到集群中所有节点所需要的时间。它取决于换出数量和集群规模。在一个规模比较大的集群中,我们应该适当提高换出数量,而降低数据传播的延迟。
传播停止阈值。当一个节点最近总是收到重复的数据,我们就应该考虑减弱甚至停止这个数据在集群中的传播了,这种过程被形象地称为“兴趣减弱”。我们一般需要计算每个节点重复的数量,并通过一个阈值来确定该数据是否需要停止传播。
以上就是 Gossip 传播模式的一些特点,但是在实际生产中,我们不能完全用随机的模式构造传播网络,那样的话会造成网络信息过载。我们一般会采用一些网络优化的手段。
网络优化
我们刚才提到 Gossip 协议成功的关键之一是控制重复消息的数量,但同时一定程度的重复数量可以保障消息的可用性,从而使集群更加稳健。
一种平衡的方案是构造一个临时的稳定拓扑网络结构。节点可以通过检测发现与其网络相对稳定的节点,从而构建一个子网。子网之间再互相连接,从而构建一个单向传播且无环的树形拓扑结构。这就达到如存在主节点网络一般的传播结构,这种结构可以很好地控制重复的消息,且保证集群中所有节点都可以安全地接收数据。
但是这种结构存在明显的弱点,也就是连接子网之间的节点会成为潜在的瓶颈。一旦这类节点失败,那么子网就会变为信息孤岛,从而丧失 Gossip 算法所带来的稳健性特点。
那有没有一种算法能解决这种孤岛问题呢?我们可以使用混合模式来解决,也就是同时使用树结构与传统 Gossip 随机传播结构。当系统稳定运行时,使用树结构加快信息的传播速度,同时减小重复数据。一旦检测到失败,那么系统退化为 Gossip 模式进行大范围信息同步,来修复失败问题。
总结
最终一致性允许节点间状态存在不一致,那么反熵机制就是帮助最终一致性来修复这些不一致情况的。
我们既可以使用前台的读修复和暗示切换来快速修复最近产生的问题,也可以使用 Merkle 树和位图版本向量这种后台手段来修复全局的一致性问题。如果需要大规模且稳定地同步数据,那么 Gossip 协议将是你绝佳的选择。
至此我们可以说,所有针对分布式系统复制与一致性的问题都已经介绍完了。下一讲我们将进入分布式数据最核心的领域:分布式事务。希望准时与你相见,谢谢。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 分布式事务(上):除了 XA还有哪些原子提交算法吗
这一讲我认为是整个课程最为精华的部分,因为事务是区别于数据库与一般存储系统最为重要的功能。而分布式数据库的事务由于其难度极高,一直被广泛关注。可以说,不解决事务问题,一个分布式数据库会被认为是残缺的。而事务的路线之争,也向我们展示了分布式数据库发展的不同路径。
提到分布式事务能想到的第一个概念就是原子提交。原子提交描述了这样的一类算法它们可以使一组操作看起来是原子化的即要么全部成功要么全部失败而且其中一些操作是远程操作。Open/X 组织提出 XA 分布式事务标准就是原子化提交的典型代表XA 被主流数据库广泛地实现,相当长的一段时间内竟成了分布式事务的代名词。
但是随着 Percolator 的出现,基于快照隔离的原子提交算法进入大众的视野,在 TiDB 实现 Percolator 乐观事务后,此种方案逐步达到生产可用的状态。
这一讲我们首先要介绍传统的两阶段提交和三阶段提交,其中前者是 XA 的核心概念,后者针对两阶段提交暴露的问题进行了改进。最后介绍 Percolator 实现的乐观事务与 TiDB 对其的改进。
两阶段提交与三阶段提交
两阶段提交非常有名,其原因主要有两点:一个是历史很悠久;二是其定义是很模糊的,它首先不是一个协议,更不是一个规范,而仅仅是作为一个概念存在,故从传统的关系统数据库一致的最新的 DistributedSQL 中,我们都可以看到它的身影。
两阶段提交包含协调器与参与者两个角色。在第一个阶段,协调器将需要提交的数据发送给参与者,同时询问参与者是否能够提交该数据,而后参与者返回投票结果。在第二阶段,协调器根据参与者的投票结果,决定是提交还是取消这次事务,而后将结果发送给每个参与者,参与者根据结果来提交本地的事务。
可以看到两阶段提交的核心是协调器。它一般被实现为一个领导节点,你可以回忆一下领导选举那一讲。我们可以使用多种方案来选举领导节点,并根据故障检测机制来探测领导节点的健康状态,从而确定是否要重新选择一个领导节点作为协调器。另外一种常见的实现是由事务发起者来充当协调器,这样做的好处是协调工作被分散到多个节点上,从而降低了分布式事务的负载。
整个事务被分解为两个过程。
准备阶段。协调器向所有参与节点发送 Propose 消息,该消息中包含了该事务的全部信息。而后所有参与节点收到该信息后,进行提交决策——是否可以提交该事务,如果决定提交该事务,它们就告诉协调器同意提交;否则,它们告诉协调器应该终止该事务。协调器和所有参与者分别保存该决定的结果,用于故障恢复。
提交或终止。如果有任何一个参与者终止了该事务,那么所有参与者都会收到终止该事务的结果,即使他们自己认为是可以提交该事务的。而只有当所有参与者全票通过该事务时,协调器才会通知它们提交该事务。这就是原子提交的核心理念:全部成功或全部失败。
我们可以看到两阶段提交是很容易理解的,但是其中却缺少大量细节。比如数据是在准备阶段还是在提交阶段写入数据库?每个数据库对该问题的实现是不同的,目前绝大多数实现是在准备阶段写入数据。
两阶段提交正常流程是很容易理解的,它有趣的地方是其异常流程。由于有两个角色和两个阶段,那么异常流程就分为 4 种。
参与者在准备阶段失败。当协调者发起投票后,有一个参与者没有任何响应(超时)。协调者就会将这个事务标记为失败,这与该阶段投票终止该事务是同样的结果。这虽然保证了事务的一致性,但却降低了分布式事务整体的可用性。下一讲我会介绍 Spanner 使用 Paxos groups 来提高参与者的可用度。
参与者在投票后失败。这种场景描述了参与者投赞成票后失败了,这个时候必须保证该节点是可以恢复的。在其恢复流程里,需要首先与协调器取得联系,确认该事务最终的结果。然后根据其结果,来取消或者提交该事务。
协调器在投票后失败。这是第二个阶段,此时协调器和参与者都已经把投票结果记录下来了。如果协调器失败,我们可以将备用协调器启动,而后读取那个事务的投票结果,再向所有参与者发送取消或者提交该事务的消息。
协调器在准备阶段失败。这是在第一阶段,该阶段存在一个两阶段提交的缺点。在该阶段,协调器发送消息没有收到投票结果,这里所说的没有收到结果主要指结果没有记录到日志里面。此时协调器失败了,那么备用协调器由于缺少投票结果的日志,是不能恢复该事务的。甚至其不知道有哪些参与者参与了这个事务,从而造成参与者无限等待。所以两阶段提交又称为阻塞提交算法。
三阶段相比于两阶段主要是解决上述第 4 点中描述的阻塞状态。它的解决方案是在两阶段中间插入一个阶段,第一阶段还是进行投票,第二阶段将投票后的结果分发给所有参与者,第三阶段是提交操作。其关键点是在第二阶段,如果协调者在第二阶段之前崩溃无法恢复,参与者可以通过超时机制来释放该事务。一旦所有节点通过第二阶段,那么就意味着它们都知道了当前事务的状态,此时,不管协调者还是参与者崩溃都不会影响事务执行。
我们看到三阶段事务会存在两阶段不存在的一个问题,在第二阶段的时候,一些参与者与协调器失去联系,它们由于超时机制会中断事务。而如果另外一些参与者已经收到可以提交的指令,就会提交数据,从而造成脑裂的情况。
除了脑裂,三阶段还存在交互量巨大从而造成系统消息负载过大的问题。故三阶段提交很少应用在实际的分布式事务设计中。
两阶段与三阶段提交都是原子提交协议,它们可以实现各种级别的隔离性要求。在实际生产中,我们可以使用一种特别的事务隔离级别来提高分布式事务的性能,实现非阻塞事务。这种隔离级别就是快照隔离。
快照的隔离
我们在第 11 讲中提到过快照隔离。它的隔离级别高于“读到已提交”,解决的是读到已提交无法避免的读偏序问题,也就是一条数据在事务中被读取,重复读取后可能会改变。
我们举一个快照隔离的读取例子,有甲乙两个事务修改同一个数据 X其初始值为 2。甲开启事务但不提交也不回退。此时乙将该数值修改为 10提交事务。而后甲重新读取 X其值仍然为 2并没有读取到已经提交的最新数据 。
那么并发提交同一条数据呢?由于没有锁的存在,会出现写入冲突,通常只有其中的一个事务可以提交数据。这种特性被称为首先提交获胜机制。
快照隔离与序列化之间的区别是前者不能解决写偏序的问题,也就是并发事务操作的数据集不相交,当事务提交后,不能保证数据集的结果一致性。举个例子,对于两个事务 T1b=a+1 和 T2a=b+1初始化 a=b=0。序列化隔离级别下结果只可能是 (a=2,b=1) 或者 (a=1,b=2);而在快照隔离级别下,结果可能是 (a=1,b=1)。这在某些业务场景下是不能接受的。当然目前有许多手段来解决快照隔离的写偏序问题即序列化的快照隔离SSI
实现 SSI 的方式有很多种,如通过一个统一的事务管理器,在提交时去询问事务中读取的数据在提交时是否已经被别的事务的提交覆盖了,如果是,就认为当前事务应标记为失败。另一些是通过在数据行上加锁,来阻止其他事务读取该事务锁定的数据行,从而避免写偏序的产生。
下面要介绍的 Percolator 正是实现了快照隔离,但是没有实现 SSI。因为可以看到 SSI 不论哪种实现都会影响系统的吞吐量。且 Percolator 本身是一种客户端事务方案,不能很好地保存状态。
Percolator 乐观事务
Percolator 是 Google 提出的工具包,它是基于 BigTable 的并支持刚才所说的快照隔离。快照隔离是有多版本的那么我们就需要有版本号Percolator 系统使用一个全局递增时间戳服务器,来为事务产生单调递增的时间戳。每个事务开始时拿一个时间戳 t1那么这个事务执行过程中可以读 t1 之前的数据;提交时再取一下时间戳 t2作为这个事务的提交时间戳。
现在我们开始介绍事务的执行过程。与两阶段提交一样我们使用客户端作为协调者BigTable 的 Tablet Server 作为参与者。 除了每个 Cell 的数据存在 BigTable 外,协调者还将 Cell 锁信息、事务版本号存在 BigTable 中。简单来说,如果需要写 bal 列balance也就是余额。在 BigTable 中实际存在三列,分别为 bal:data、bal:lock、bal:write。它们保存的信息如下所示。
bal:write 中存事务提交时间戳 commit_ts=>start_ts
bal:data 这个 map 中存事务开始时间戳 start_ts=> 实际列数据;
bal:lock 存 start_ts=>(primary cell)Primary cell 是 Rowkey 和列名的组合,它在提交容错处理和事务冲突时使用,用来清理由于协调器失败导致的事务失败留下的锁信息。
我们现在用一个例子来介绍一下整个过程,请看下图。
一个账户表中Bob 有 10 美元Joe 有 2 美元。我们可以看到 Bob 的记录在 write 字段中最新的数据是 data@5,它表示当前最新的数据是 ts=5 那个版本的数据ts=5 版本中的数据是 10 美元,这样读操作就会读到这个 10 美元。同理Joe 的账号是 2 美元。
现在我们要做一个转账操作,从 Bob 账户转 7 美元到 Joe 账户。这需要操作多行数据这里是两行。首先需要加锁Percolator 从要操作的行中随机选择一行作为 Primary Row其余为 Secondary Row。对 Primary Row 加锁,成功后再对 Secondary Row 加锁。从上图我们看到,在 ts=7 的行 lock 列写入了一个锁I am primary该行的 write 列是空的,数据列值为 310-7=3。 此时 ts=7 为 start_ts。
然后对 Joe 账户加锁,同样是 ts=7在 Joe 账户的加锁信息中包含了指向 Primary lock 的引用如此这般处于同一个事务的行就关联起来了。Joe 的数据列写入 9(2+7=9)write 列为空,至此完成 Prewrite 阶段。
接下来事务就要 Commit 了。Primary Row 首先执行 Commit只要 Primary Row Commit 成功了事务就成功了。Secondary Row 失败了也不要紧后续会有补救措施。Commit 操作首先清除 Primary Row 的锁,然后写入 ts=8 的行(因为时间是单向递增的,这里是 commit_ts该行可以称为 Commit Row因为它不包含数据只是在 write 列中写入 data@7,标识 ts=7 的数据已经可见了,此刻以后的读操作可以读到版本 ts=7 的数据了。
接下来就是 commit Secondary Row 了,和 Primary Row 的逻辑是一样的。Secondary Row 成功 commit事务就完成了。
如果 Primary Row commit 成功Secondary Row commit 失败会怎么样,数据的一致性如何保障?由于 Percolator 没有中心化的事务管理器组件,处理这种异常,只能在下次读操作发起时进行。如果一个读请求发现要读的数据存在 Secondary 锁,它会根据 Secondary Row 锁去检查其对应的 Primary Row 的锁是不是还存在若存在说明事务还没有完成若不存在则说明Primary Row 已经 Commit 了,它会清除 Secondary Row 的锁使该行数据变为可见状态commit。这是一个 Roll forward 的概念。
我们可以看到,在这样一个存储系统中,并非所有的行都是数据,还包含了一些事务控制行,或者称为 Commit Row。它的数据 Column 为空,但 write 列包含了可见数据的 TS。它的作用是标示事务完成并指引读请求读到新的数据。随着时间的推移会产生大量冗余的数据行无用的数据行会被 GC 线程定时清理。
该事务另一个问题就是冲突处理。在之前介绍快照隔离时我们提到了对于同一行的冲突操作可以采用先提交获胜的模式,那么后提交的事务就会出现失败。如果数据库在出现高度并发修改相同数据的情况该怎么办呢?现在让我介绍一下根据 Percolator 模型实现乐观事务的 TiDB 是如何处理的。
TiDB 乐观事务冲突处理
首先在 TiDB 中写入冲突是在提交阶段进行检测的。在 11 讲中我们介绍了 MVCC 类数据库的冲突处理模式,分别为前项检测与后向检测。而 TiDB 由于使用 Percolator 模式,采用的是提交阶段的后向检测。这其实从原理上看是完全没有问题的,但 TiDB 声明自己完全兼容 MySQL。而众所周知MySQL 使用的分布式事务是悲观模式。故在 SQL 执行阶段就能检测冲突,也就是前向模式。如此,就造成了用户如果从 MySQL 迁移到 TiDB就必须好好审视其使用数据库是否依赖了此种模式从而提高了用户的迁移成本。
基于以上的原因TiDB 提供了以下几种方案来解决后向检测与前向检测的差异。
重试。顾名思义在遇到冲突时TiDB 可以重试失败的事务中的非查询操作。这是非常简洁而高效的方案,但却不是万能的。如果事务中存在根据读取结果更新数据的情况,很可能造成数据异常。因为读取操作没有重试,从而破坏了“可重读”隔离级别。故重试只能应用在非读取的场景,特别是小事务中,即每个 SQL 是单独的事务。
冲突预检。另一个思路是在 prewrite 阶段就执行冲突预检将后向检查变为前向检查。TiDB 依赖的 TiKV 使用了内存来存储事务中的 key从而检查 key 是否存在其他事务,避免并发修改 key 的情况。这样做的原因是TiDB 本身是无状态阶段,从而导致事务之间无法感知彼此,故只能通过底层手段解决。这种结构是一种内存锁,如果事务过多,会造成获取锁的操作阻塞写入,从而导致吞吐量下降的情况。
悲观事务。最后,为了完整实现 MySQL 的特性,还可以使用悲观事务。
以上就是 TiDB 在实践 Percolator 模型时所给出的解决思路。从而使用户方便从 MySQL 迁移过来。另外随着 TiDB 此类数据库的面世Percolator 事务模式也越来越得到业界的认可。
总结
好了,这一讲我们介绍了典型的原子提交:两阶段提交。它是 XA 的基础,但是两阶段提交存在天然的问题,且性能很低。在快照隔离下,我们可以使用 Percolator 模式描述的方案去实现新的原子提交,在冲突较低的场景下,该方案具有很好的性能。
下一讲,我们将介绍一对分布式事务方案的竞争对手 Spanner vs Calvin。感谢学习希望下次与你准时相见。
00:00
24讲吃透分布式数据库

View File

@ -0,0 +1,147 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 分布式事务Spanner 与 Calvin 的巅峰对决
上一讲我们介绍了分布式事务最重要的概念——原子提交,并介绍了两阶段、三阶段提交和 Percolator 模型。
而这一讲我将要为你揭示目前业界最著名的两种分布式事务模型,同时它们的作者和追随者之间的论战又为这两种模型增加了一定的传奇性,这一讲让我们来看看它们最终谁能胜出呢?
首先,让我介绍一下参战的两位“选手”,它们分别是 Spanner 和 Calvin。它们背后分别有广泛引用的论文可以说都拥有比较深厚的理论基础。那么我们先从 Spanner 开始说起。
Spanner 及其追随者
Spanner 最早来自 Google 的一篇论文,并最终成为 Google Cloud 的一个服务。Spanner 简单来讲是一种两阶段提交的实现,你可以回忆一下,上一讲中我介绍了两阶段提交 4 种失败场景,其中有一种是参与者准备阶段无响应,从而造成事务的可用性下降。而 Spanner
利用共识算法保证了每个分片Shard都是高可用的从而提高了整体事务的可用性。
Spanner 的整体架构很复杂,包含的内容非常多。但核心主要是两个部分,分别是 TrueTime 和 Paxos Group而这场论战也是针对其中的一个部分展开的。
TrueTime
我在模块三“13 | 概要:分布式系统都要解决哪些问题”中介绍过,分布式系统获取时间有两种方式:物理时间与逻辑时间。而由于物理时间不靠谱,分布式系统大部分使用逻辑时间。逻辑时间往往由一个节点生成时间戳,虽然已经很高效,但是如果要构建全球系统,这种设计就捉襟见肘了。
而 TrueTime 是一种逻辑与物理时间的融合,是由原子钟结合 IDC 本地时间生成的。区别于传统的单一时间点TrueTime 的返回值是一个时间范围数据操作可能发生在这个范围之内故范围内的数据状态是不确定的uncertainty。系统必须等待一段时间从而获得确定的系统状态。这段时间通常是比较短暂的且多个操作可以并行执行通常不会影响整体的吞吐量。
事务过程
Spanner 提供了三种事务模式。
读写事务:该事务是通过分布式锁实现的,并发性是最差的。且数据写入每个分片 Paxos Group 的主节点。
只读事务:该事务是无锁的,可以在任意副本集上进行读取。但是,如果想读到最新的数据,需要从主节点上进行读取。主节点可以从 Paxos Group 中获取最新提交的时间节点。
快照读顾名思义Spanner 实现了 MVCC 和快照隔离故读取操作在整个事务内部是一致的。同时这也暗示了Spanner 可以保存同一份数据的多个版本。
了解了事务模型后,我们深入其内部,看看 Spanner 的核心组件都有哪些。下面是一张 Spanner 的架构图。
其中我们看到,每个 replica 保存了多个 tablet同时这些 replica 组成了 Paxos Group。Paxos Group 选举出一个 leader 用来在多分片事务中与其他 Paxos Group 的 leader 进行协调(有关 Paxos 算法的细节我将在下一讲中介绍)。
写入操作必须通过 leader 来进行,而读取操作可以在任何一个同步完成的 replica 上进行。同时我们看到 leader 中有锁管理器,用来实现并发控制中提到的锁管理。事务管理器用来处理多分片分布式事务。当进行同步写入操作时,必须要获取锁,而快照读取操作是无锁操作。
我们可以看到,最复杂的操作就是多分片的写入操作。其过程就是由 leader 参与的两阶段提交。在准备阶段,提交的数据写入到协调器的 Paxos Group 中,这解决了如下两个问题。
整个事务的数据是安全的。协调者崩溃不会影响到事务继续运行,我们可以从 Paxos Group 中恢复事务数据。
参与者崩溃不会影响事务。因为 Paxos Group 可以重新选择节点来继续执行未完成的事务操作。
在隔离方面Spanner 实现了 SSI也就是序列化的快照隔离。其方法就是上文提到的 lock table。该锁是完全的排他锁不仅仅能阻止并发写入数据写入也可以阻止读取从而解决快照隔离写偏序的问题。
在整个过程中,事务开始时间和提交事务时间(数据可见时间)都是通过 TrueTime 获取的时间范围。Spanner 获取这些范围后,必须等待范围中描述的时间,而后才可以执行操作。否则,系统就会读取到不一致的数据。比如未能读取到当前时间之前的数据,或者读取到事务部分产生的数据等异常数据。
同时Spanner 声明自己的事务特性是外部一致性External Consistency。其描述为首先并发的事务是序列化的如上文所示Spanner 实现了 SSI。同时它还是线性一致的也就是“真实”时间下事务 A 在事务 B 前提交,那么事务 A 的时间一定小于事务 B。对一致性部分掌握比较深的同学会发现这就是我们在该部分提到的事务与一致性之间的联系。任何分布式数据库都要描述其事务特性并发操作与一致性特性非并发操作而 Spanner 所谓的外部一致就是序列化+线性一致。
Spanner 不仅仅有 Google Cloud 的一种商业产品可供大家选择,同样有众多开源数据库是源自 Spanner 的理念而设计的,如 CockroachDB、YugaByte DB 等。故Spanner 被认为是一类从开源到商业、本地部署到云端的成熟解决方案。
以上我讲解了 Spanner 的特性,下面接着看看它的对手 Calvin 的一些特点吧。
Calvin 与 FaunaDB
Spanner 引入了很多新技术去改善分布式事务的性能,但我们发现其流程整体还是传统的二阶段提交,并没有在结构上发生重大的改变,而 Calvin 却充满了颠覆性。让我们来看看它是怎么处理分布式事务的。
首先传统分布式事务处理使用到了锁来保证并发竞争的事务满足隔离级别的约束。比如序列化级别保证了事务是一个接一个运行的。而每个副本的执行顺序是无法预测的但结果是可以预测的。Calvin 的方案是让事务在每个副本上的执行顺序达到一致,那么执行结果也肯定是一致的。这样做的好处是避免了众多事务之间的锁竞争,从而大大提高了高并发度事务的吞吐量。同时,节点崩溃不影响事务的执行。因为事务执行步骤已经分配,节点恢复后从失败处接着运行该事务即可,这种模式使分布式事务的可用性也大大提高。目前实现了 Calvin 事务模式的数据库是 FaunaDB。
其次,将事务进行排序的组件被称为 sequencer。它搜集事务信息而后将它们拆解为较小的 epoch这样做的目的是减小锁竞争并提高并行度。一旦事务被准备好sequencer 会将它们发送给 scheduler。scheduler 根据 sequencer 处理的结果适时地并行执行部分事务步骤同时也保证顺序执行的步骤不会被并行。因为这些步骤已经排好了顺序scheduler 执行的时候不需要与 sequencer 进行交互从而提高了执行效率。Calvin 事务的处理组件如下图所示。
Calvin 也使用了 Paxos 算法,不同于 Spanner 每个分片有一个 Paxos Group。Calvin 使用 Paxos 或者异步复制来决定哪个事务需要进入哪个 epoch 里面。
同时 Calvin 事务有 read set 和 write set 的概念。前者表示事务需要读取的数据后者表示事务影响的数据。这两个集合需要在事务开始前就进行确定故Calvin 不支持在事务中查询动态数据而后影响最终结果集的行为。这一点很重要,是这场战争的核心。
在你了解了两种事务模型之后我就要带你进入“刺激战场”了。在两位实力相当的选手中Calvin 一派首先挑起了战争。
对 Spanner 的批评
来自马里兰大学的 Daniel Abadi 教授是 Calvin 论文的联合作者、FaunaDB 的咨询师,可以说他非常有资格代表 Calvin 一派向 Spanner 发起挑战。
一开始 Abadi 教授主要探讨了 Spanner 和 Calvin 之间的架构带来的性能差异,他从如下几个方面给出了比较。
传统读写事务:如果是对于分片内部的事务(非分布式场景),两者的性能是类似的;但是对于跨分片,他认为 Calvin 的性能要远好于 Spanner。原因是 Spanner 相对来说有两点性能损耗,第一就是 TrueTime 返回的是时间范围,我们必须等待一段时间后才可以做提交操作,当然这部分是可以并行的;第二就是 Spanner 是两阶段提交,相比于 Calvin 的“一阶段”来讲,理论上延迟会高。
快照读:这部分两者原理类似,故延迟都不高。
只读事务:这部分就是 Spanner 要更高效。因为它只从 leader 节点去读取数据,而 Calvin 做全局的一致性读,故延迟更大。
除了以上的比较Calvin 还在日志复制上存在优势。主要是 Spanner 的日志复制也是 Paxos 过程,而 Calvin 由于预处理加持,可以简单高效地进行复制。这种优势在理论上随着节点间物理距离的扩展而变得更加明显。
当然,我们知道 Calvin 提到了它的预处理机制会限制事务内的操作,这个限制 Abadi 教授也注意到了。
以上就是 Abadi 教授在两者性能方面的比较,其论调还是比较客观中立,且冲突性不强。但紧接着,他指出了 Spanner 一个非常具有争议的问题,这个问题关系到了 TrueTime。TrueTime 由于不是在理论层面上证明它的时间不会倒流skew而是通过大量的工程实践证明了这种可能性非常低。而这个概率就是一个攻击点。
教授在这里比较聪明,或可以说是明智。他没有攻击 TrueTime 本身,而是表明 TrueTime 由于依赖原子钟这种硬件提高了其他人复制该技术的难度。从而引出了一个技术圈的老话题——Google 的技术出了 Google 就失效了。
而 Abadi 要挑战的就是基于 Spanner 想法的其他开源或商业数据库,如上文提到的 CockroachDB 和 YugaByteDB。它们的 TrueTime 是用软件实现的相比于硬件上文描述的时间倒流概率被提高了。CockroachDB 还好,它声明了这种异常的可能;而 YugaByte 却没有,故它被教授集中火力攻击。
最后教授提到了Calvin 和 FaunaDB 在理论层面上证明了其可以很好地实现一致性。
既然 Calvin 引战,特别是主要集中在 YugaByteDB 上,于是后者发起了绝地反击。
Spanner 追随者的反击
既然 YugaByte“祸从天上来”那么必然由它们发起反击。
上文中,教授的观点总结为:
性能上Calvin 由于锁持有时间短,吞吐量会大于 Spanner
一致性上,基于硬件的 TrueTime 具有一定概率会发生时间倒流而软件实现的“TrueTime”更是无法保证时间单调递增。
针对第一个问题YugaByte 首先承认了 Calvin 吞吐量的优势。但是画风一转YugaByte 抛出了著名的分布式事务模式研究,该研究通过多 AWS Dynamo 用户使用事务的模式进行分析。得出的结论是90%的事务是发生在单行和单分片的,只有 10%左右才是多分片的。据此YugaByte 把前者称为主要负载,后者称为次要负载。
那么在主要负载方面,上文中教授也承认 Spanner 和 Calvin 性能间没有明显差别,而 Calvin 具有优势的场景变为了次要负载。我们都听说过,“脱离剂量谈毒性都是耍流氓”。而 Calvin 的优势却在次要负载上,这大大降低了该优势的重要程度。
而第二个问题其实才是核心问题。我很欣赏此处 YugaByte 没有回避,而是大方地承认 YugaByte 等软件实现 TrueTime 的模式无法做到如 Calvin 那种严格序列化,而是所谓“最大可能”序列化。一旦 TrueTime 时间范围超过了阈值,序列化就被破坏了。但是 YugaByte 指出了两点让用户去思考:
上文中主要负载场景两者都不会有一致性问题,只有在次要场景 Spanner 类方案才会有问题;
随着 AWS、阿里云等公有云服务逐步提供原子钟服务YugaByte 这类数据库也可以使用真正的 TrueTime这大大降低了发生时间倒流的概率。
从以上的解释看出,软件的 NTP 计时器确实存在问题,但如果用户场景对此要求不严格,也是可以使用的。
除了上面针对教授提到的问题YugaByte 也提出了 Calvin 类数据库的一些较为“致命”的缺陷。
上文教授已经承认的读性能 Calvin 是要弱于 Spanner 的。
静态化的 write set 和 read set 导致了二级索引和会话内事务的问题。会话内事务我们上文提到过,简单说 Calvin 的事务的写入不能依赖于事务内的读取;而二级索引的列如果频繁修改,会导致 Calvin 的事务反复重试,从而降低吞吐量。
Calvin 另一个缺憾就是其缺乏开源的实现。目前只有 FaunaDB 这个闭源商业版本,使得习惯使用开源技术栈的用户没有别的选择。
FaunaDB 没有使用 SQL而是使用了一个 GraphQL 风格的新语言 FQL。这为原本使用 SQL 语言的团队切换到 FaunaDB 上带来了很大挑战。
可以看到 YugaByte 团队针对其批评也给出了自己的回应,那么他们之间的争论有确定的结果吗?
谁胜利了?
从目前发展的角度来说并没有一方可以完全替代另一方。Calvin 在高度竞争的事务场景中有明显优势,而 Spanner 在读取、会话内事务中的优势不可代替。从它们的原理看,谁最终也无法胜出。而我们其实也不期待一个最终赢家,而是希望未来的事务模型能够从这两个模式中吸取灵感,为我们带来更高效的分布式事务解决方案 。
到此,我们用了两讲的内容,详细介绍了面向数据库的分布式事务。下一讲要说的是模块三的最后一个知识点:共识算法。它是现代分布式系统的核心算法,希望到时和你准时相见。

View File

@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 共识算法:一次性说清楚 Paxos、Raft 等算法的区别
现在,我们进入了分布式系统的最后一讲:共识算法。前面我们学习了各种分布式的技术,你可以和我一起回忆一下,其中我们讨论了失败模型、失败检测、领导选举和一致性模型。虽然这些技术可以被单独使用,但我们还是希望用一个技术栈就能实现上述全部功能,如果这样,将会是非常美妙的。于是,整个分布式数据库,乃至分布式领域的研究人员经过多年的努力,终于在这个问题上有所突破——共识算法由此诞生。
虽然共识算法是分布式系统理论的精华,但是通过之前的学习,其实你已经知道共识算法包含的内容了。它首先是要解决分布式系统比较棘手的失败问题,通过内置的失败检测机制可以发现失败节点、领导选举机制保证数据高效处理、一致性模式保证了消息的一致性。
这一讲,我会为你介绍几种常用的共识算法的特色。我不会深入到每种算法的详细执行过程,因为这些过程抽象且对使用没有特别的帮助。这一讲我的目的是从更高的维度为你解释这些算法,希望给你形象的记忆,并帮助你能够学以致用。至于算法实现细节,感兴趣的话你可以自行学习。
在介绍共识协议之前,我们要来聊聊它的三个属性。
正确性Validity诚实节点最终达成共识的值必须是来自诚实节点提议的值。
一致性Agreement所有的诚实节点都必须就相同的值达成共识。
终止性Termination诚实的节点必须最终就某个值达成共识。
你会发现共识算法中需要有“诚实”节点,它的概念是节点不能产生失败模型所描述的“任意失败”,或是“拜占庭失败”。因为数据库节点一般会满足这种假设,所以我们下面讨论的算法可以认为所有节点都是诚实的。
以上属性可以换个说法实际上就是“15 | 领导选举如何在分布式系统内安全地协调操作”介绍的安全性Safety和活跃性Liveness其中正确性Validity和一致性Agreement决定了安全性Safety而终止性Termination就是活跃性Liveness。让我们复习一下这两个特性。
安全性Safety在故障发生时共识系统不能产生错误的结果。
活跃性Liveness系统能持续产生提交也就是不会永远处于一个中间状态无法继续。
基于以上的特性,我们开始聊聊目前常见的共识算法。
原子广播与 ZAB
广播协议是一类将数据从一个节点同步到多个节点的协议。我在“17 | 数据可靠传播:反熵理论如何帮助数据库可靠工作”介绍过最终一致性系统通过各种反熵手段来保证数据的一致性传播,特别是其中的 Gossip 协议可以保障大规模的数据同步,而 Gossip 在正常情况下就是采用广播模式传播数据的。
以上的广播过程产生了一个问题,那就是这个协调节点是明显的单点,它的可靠性至关重要。要保障其可靠,首先要解决的问题是需要检查这个节点的健康状态。我们可以通过各种健康检查方式去发现其健康情况。
如果它失败了,会造成消息传播到一部分节点中,而另外一部分节点却没有这一份消息,这就违背了“一致性”。那么应该怎解决这个问题呢?
一个简单的算法就是使用“漫灌”机制,这种机制是一旦一个消息被广播到一个节点,该节点就有义务把该消息广播到其他未收到数据节点的义务。这就像水田灌溉一样,最终整个系统都收到了这份数据。
当然以上的模式有个明显的缺点就是会产生N2的消息。其中 N 是目前系统剩下的未同步消息的节点,所以我们的一个优化目标就是要减少消息的总数量。
虽然广播可以可靠传递数据,但通过一致性的学习我们知道:需要保证各个节点接收到消息的顺序,才能实现较为严格的一致性。所以我们这里定义一个原子广播协议来满足。
原子性:所有参与节点都收到并传播该消息;或相反,都不传播该消息。
顺序性:所有参与节点传播消息的顺序都是一致的。
满足以上条件的协议我们称为原子广播协议现在让我来介绍最为常见的原子广播协议Zookeeper Atomic BroadcastZAB
ZAB
ZAB 协议由于 Zookeeper 的广泛使用变得非常流行。它是一种原子广播协议,可以保证消息顺序的传递,且消息广播时的原子性保障了消息的一致性。
ZAB 协议中,节点的角色有两种。
领导节点。领导是一个临时角色,它是有任期的。这么做的目的是保证领导角色的活性。领导节点控制着算法执行的过程,广播消息并保证消息是按顺序传播的。读写操作都要经过它,从而保证操作的都是最新的数据。如果一个客户端连接的不是领导节点,它发送的消息也会转发到领导节点中。
跟随节点。主要作用是接受领导发送的消息,并检测领导的健康状态。
既然需要有领导节点产生,我们就需要领导选举算法。这里我们要明确两个 ID数据 ID 与节点 ID。前者可以看作消息的时间戳后者是节点的优先级。选举的原则是在同一任职周期内节点的数据 ID 越大,表示该节点的数据越新,数据 ID 最大的节点优先被投票。所有节点的数据 ID 都相同,则节点 ID 最大的节点优先被投票。当一个节点的得票数超过节点半数,则该节点成为主节点。
一旦领导节点选举出来,它就需要做两件事。
声明任期。领导节点通知所有的跟随节点当前的最新任期;而后由跟随节点确认当前任期是最新的任期,从而同步所有节点的状态。通过该过程,老任期的消息就不会被跟随节点所接受了。
同步状态。这一步很关键,首先领导节点会通知所有跟随节点自己的领导身份,而后跟随节点不会再选举自己为领导了;然后领导节点会同步集群内的消息历史,保证最新的消息在所有节点中同步。因为新选举的领导节点很可能并没有最新被接受的数据,因此同步历史数据操作是很有必要的。
经过以上的初始化动作后,领导节点就可以正常接受消息,进行消息排序而后广播消息了。在广播消息的时候,需要 Quorum集群中大多数的节点的节点返回已经接受的消息才认为消息被正确广播了。同时为了保证顺序需要前一个消息正常广播后一个消息才能进行广播。
领导节点与跟随节点使用心跳算法检测彼此的健康情况。如果领导节点发现自己与 Quorum 节点们失去联系,比如网络分区,此时领导节点会主动下台,开始新一轮选举。同理,当跟随节点检测到领导节点延迟过大,也会触发新一轮选举。
ZAB 选举的优势是,如果领导节点一直健康,即使当前任期过期,选举后原领导节点还会承担领导角色,而不会触发领导节点切换,这保证了该算法的稳定。另外,它的节点恢复比较高效,通过比较各个节点的消息 ID找到最大的消息 ID就可以从上面恢复最新的数据了。最后它的消息广播可以理解为没有投票过程的两阶段提交只需要两轮消息就可以将消息广播出去。
那么原子广播协议与本讲重点介绍的共识算法是什么关系呢?这里我先留下一个“暗扣”,先介绍一下典型的共识算法 Paxos而后再说明它们之间的关系。
Paxos
所谓的 Paxos 算法,是为了解决来自客户端的值被发送到集群中的任意一点,而后集群中的所有节点为该值达成共识的一种协调算法。同时这个值伴随一个版本号,可以保证消息是有顺序的,该顺序在集群中任何一点都是一致的。
基本的 Paxos 算法非常简单,它由三个角色组成。
ProposerProposer 可以有多个Proposer 提出议案value。所谓 value可以是任何操作比如“设置某个变量的值为 value”。不同的 Proposer 可以提出不同的 value。但对同一轮 Paxos 过程,最多只有一个 value 被批准。
AcceptorAcceptor 有 N 个Proposer 提出的 value 必须获得 Quorum 的 Acceptor 批准后才能通过。Acceptor 之间完全对等独立。
Learner上面提到只要 Quorum 的 Accpetor 通过即可获得通过,那么 Learner 角色的目的就是把通过的确定性取值同步给其他未确定的 Acceptor。
这三个角色其实已经描述了一个值被提交的整个过程。其实基本的 Paxos 只是理论模型,因为在真实场景下,我们需要处理许多连续的值,并且这些值都是并发的。如果完全执行上面描述的过程,那性能消耗是任何生产系统都无法承受的,因此我们一般使用的是 Multi-Paxos。
Multi-Paxos 可以并发执行多个 Paxos 协议,它优化的重点是把 Propose 阶段进行了合并,这就引入了一个 Leader 的角色,也就是领导节点。而后读写全部由 Leader 处理,同时这里与 ZAB 类似Leader 也有任期的概念Leader 与其他节点之间也用心跳进行互相探活。是不是感觉有那个味道了?后面我就会比较两者的异同。
另外 Multi-Paxos 引入了两个重要的概念replicated log 和 state snapshot。
replicated log值被提交后写入到日志中。这种日志结构除了提供持久化存储外更重要的是保证了消息保存的顺序性。而 Paxos 算法的目标是保证每个节点该日志内容的强一致性。
state snapshot由于日志结构保存了所有值随着时间推移日志会越来越大。故算法实现了一种状态快照可以保存最新的日志消息。当快照生成后我们就可以安全删除快照之前的日志了。
熟悉 Raft 的同学会发现,上面的结构其实已经与 Raft 很接近了。在讨论完原子广播与共识之后 ,我们会接着介绍 Raft。
原子广播与共识
就像我开篇所说的本讲不是介绍算法细节的而是重点关注它们为什么是今天这个样子。从上面的粗略介绍中我们已经发现ZAB 其实与 Multi-Paxos 是非常类似的。本质上,它们都需要大部分节点“同意”一个值,并都有 Leader 节点,且 Leader 都是临时的。真是越说越相似,但本质上它们却又是不同的。
简单来说ZAB 来源于主备复制场景,就是我们之前介绍的复制技术;而共识算法是状态机复制系统。
所谓状态机复制系统,是指集群中每个节点都是一个状态机,如果有一组客户端并发在系统中的不同状态机上提交不同的值,该系统保证每个状态机都可以保证执行相同顺序的客户端请求。可以看到请求一旦被提交,其顺序是有保障的。但是未提交之前,顺序是由 Leader 决定的,且这个顺序可以是任意的。一旦 Leader 被重选,新的 Leader 可以任意排序未提交的值。
而 ZAB 这种广播协议来自主备复制,强调的是消息的顺序是 Leader 产生的,并被 Follower 严格执行其中没有协调的关系。更重要的区别是Leader 重选后,新 Leader 依然会按照原 Leader 的排序来广播数据,而不会自己去排序。
因此可以说 ZAB 可以实现严格的线性一致性。而 Multi-Paxos 由于只是并发写,所以也没有所谓的线性一致,而是一种顺序一致结构,也就是数据被提交时才能确定顺序。而不是如 ZAB 那样有 Leader 首先分配了顺序该顺序与数据提交的先后顺序保持了一致。关于线性一致和顺序一致请参考“05 | 一致性与 CAP 模型:为什么需要分布式一致性?”
由于共识算法如 Paxos 为了效率的原因引入了 Leader。在正常情况下两者差异不是很大而差异主要在选举 Leader 的流程上。
那么学习完 ZAB 和 Multi-Paxos 后,我将要介绍这一讲的主角 Raft 算法,它是目前分布式数据库领域最重要的算法。
Raft 的特色
Raft 可以看成是 Multi-Paxos 的改进算法,因为其作者曾在斯坦福大学做过关于 Raft 与 Multi-Paxos 的比较演讲,因此我们可以将它们看作一类算法。
Raft 算法可以说是目前最成功的分布式共识算法,包括 TiDB、FaunaDB、Redis 等都使用了这种技术。原因是 Multi-Paxos 没有具体的实现细节,虽然它给了开发者想象空间,但共识算法一般居于核心位置,一旦存在潜在问题必然带给系统灾难性的后果。而 Raft 算法给出了大量的实现细节,且处理方式相比于 Multi-Paxos 有两点优势。
发送的请求的是连续的,也就是说 Raft 的写日志操作必须是连续的;而 Multi-Paxos 可以并发修改日志这也体现了“Multi”的特点。
选主必须是最新、最全的日志节点才可以当选,这一点与 ZAB 算法有相同的原则;而 Multi-Paxo 是随机的。因此 Raft 可以看成是简化版本的 Multi-Paxos正是这个简化造就了 Raft 的流行。
Multi-Paxos 随机性使得没有一个节点有完整的最新的数据,因此其恢复流程非常复杂,需要同步节点间的历史记录;而 Raft 可以很容易地找到最新节点,从而加快恢复速度。当然乱序提交和日志的不连续也有好处,那就是写入并发性能会大大提高,从而提高吞吐量。所以这两个特性并不是缺点,而是权衡利弊的结果。当然 TiKV 在使用 Raft 的时候采用了多 RaftGroup 的模式,提高了单 Raft 结构的并发度,这可以被看作是向 Multi-Paxos 的一种借鉴。
同时 Raft 和 Multi-Paxos 都使用了任期形式的 Leader。好处是性能很高缺点是在切主的时候会拒绝服务造成可用性下降。因此一般我们认为共识服务是 CP 类服务CAP 理论)。但是有些团队为了提高可用性 ,转而采用基础的 Paxos 算法,比如微信的 PaxosStore 都是用了每轮一个单独的 Paxos 这种策略。
以上两点改进使 Raft 更好地落地可以说目前最新数据库几乎都在使用该算法。想了解算法更多细节请参考https://raft.github.io/。你从中不仅能学习到算法细节,更重要的是可以看到很多已经完成的实现,结合代码学习能为你带来更深刻的印象。
总结
共识算法是一个比较大的话题。本讲聚焦于常见的三种共识类算法,集中展示其最核心的功能。我通过比较它们之间的异同,来加深你对它们特性的记忆。
共识算法又是现代分布式数据库的核心组件,好在其 API 较为易懂,且目前有比较成熟的实现,所以我认为算法细节并不是本讲的重点。理解它们为什么如此,才能帮助我们理解数据库的选择依据。
到此,我们学习完了这个模块的所有知识点。下一讲我将会带领你复习这一模块的内容,同时通过几个案例来展示典型分布式数据库特性与咱们所学的知识点之间的关系,到时候见。

View File

@ -0,0 +1,92 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 设计缓存架构时需要考量哪些因素?
你好,我是你的缓存老师陈波,欢迎进入第 3 课时“缓存的引入及架构设计”。
至此,缓存原理相关的主要知识点就讲完了,接下来会讲到如何引入缓存并进行设计架构,以及在缓存设计架构中的一些关键考量点。
缓存的引入及架构设计
缓存组件选择
在设计架构缓存时,你首先要选定缓存组件,比如要用 Local-Cache还是 Redis、Memcached、Pika 等开源缓存组件,如果业务缓存需求比较特殊,你还要考虑是直接定制开发一个新的缓存组件,还是对开源缓存进行二次开发,来满足业务需要。
缓存数据结构设计
确定好缓存组件后,你还要根据业务访问的特点,进行缓存数据结构的设计。对于直接简单 KV 读写的业务,你可以将这些业务数据封装为 String、Json、Protocol Buffer 等格式,序列化成字节序列,然后直接写入缓存中。读取时,先从缓存组件获取到数据的字节序列,再进行反序列化操作即可。对于只需要存取部分字段或需要在缓存端进行计算的业务,你可以把数据设计为 Hash、Set、List、Geo 等结构,存储到支持复杂集合数据类型的缓存中,如 Redis、Pika 等。
缓存分布设计
确定了缓存组件,设计好了缓存数据结构,接下来就要设计缓存的分布。可以从 3 个维度来进行缓存分布设计。
首先,要选择分布式算法,是采用取模还是一致性 Hash 进行分布。取模分布的方案简单,每个 key 只会存在确定的缓存节点,一致性 Hash 分布的方案相对复杂,一个 key 对应的缓存节点不确定。但一致性 Hash 分布,可以在部分缓存节点异常时,将失效节点的数据访问均衡分散到其他正常存活的节点,从而更好地保证了缓存系统的稳定性。
其次,分布读写访问如何进行实施,是由缓存 Client 直接进行 Hash 分布定位读写,还是通过 Proxy 代理来进行读写路由Client 直接读写,读写性能最佳,但需要 Client 感知分布策略。在缓存部署发生在线变化时,也需要及时通知所有缓存 Client避免读写异常另外Client 实现也较复杂。而通过 Proxy 路由Client 只需直接访问 Proxy分布逻辑及部署变更都由 Proxy 来处理,对业务应用开发最友好,但业务访问多一跳,访问性能会有一定的损失。
最后,缓存系统运行过程中,如果待缓存的数据量增长过快,会导致大量缓存数据被剔除,缓存命中率会下降,数据访问性能会随之降低,这样就需要将数据从缓存节点进行动态拆分,把部分数据水平迁移到其他缓存节点。这个迁移过程需要考虑,是由 Proxy 进行迁移还是缓存 Server 自身进行迁移,甚至根本就不支持迁移。对于 Memcached一般不支持迁移对 Redis社区版本是依靠缓存 Server 进行迁移,而对 Codis 则是通过 Admin、Proxy 配合后端缓存组件进行迁移。
缓存架构部署及运维管理
设计完毕缓存的分布策略后,接下来就要考虑缓存的架构部署及运维管理了。架构部署主要考虑如何对缓存进行分池、分层、分 IDC以及是否需要进行异构处理。
核心的、高并发访问的不同数据,需要分别分拆到独立的缓存池中,进行分别访问,避免相互影响;访问量较小、非核心的业务数据,则可以混存。
对海量数据、访问超过 10100万 级的业务数据,要考虑分层访问,并且要分摊访问量,避免缓存过载。
如果业务系统需要多 IDC 部署甚至异地多活,则需要对缓存体系也进行多 IDC 部署,要考虑如何跨 IDC 对缓存数据进行更新,可以采用直接跨 IDC 读写,也可以采用 DataBus 配合队列机进行不同 IDC 的消息同步,然后由消息处理机进行缓存更新,还可以由各个 IDC 的 DB Trigger 进行缓存更新。
某些极端场景下,还需要把多种缓存组件进行组合使用,通过缓存异构达到最佳读写性能。
站在系统层面,要想更好得管理缓存,还要考虑缓存的服务化,考虑缓存体系如何更好得进行集群管理、监控运维等。
缓存设计架构的常见考量点
在缓存设计架构的过程中,有一些非常重要的考量点,如下图所示,只有分析清楚了这些考量点,才能设计架构出更佳的缓存体系。
读写方式
首先是 value 的读写方式。是全部整体读写,还是只部分读写及变更?是否需要内部计算?比如,用户粉丝数,很多普通用户的粉丝有几千到几万,而大 V 的粉丝更是高达几千万甚至过亿,因此,获取粉丝列表肯定不能采用整体读写的方式,只能部分获取。另外在判断某用户是否关注了另外一个用户时,也不需要拉取该用户的全部关注列表,直接在关注列表上进行检查判断,然后返回 True/False 或 0/1 的方式更为高效。
KV size
然后是不同业务数据缓存 KV 的 size。如果单个业务的 KV size 过大,需要分拆成多个 KV 来缓存。但是,不同缓存数据的 KV size 如果差异过大,也不能缓存在一起,避免缓存效率的低下和相互影响。
key 的数量
key 的数量也是一个重要考虑因素。如果 key 数量不大,可以在缓存中存下全量数据,把缓存当 DB 存储来用,如果缓存读取 miss则表明数据不存在根本不需要再去 DB 查询。如果数据量巨大,则在缓存中尽可能只保留频繁访问的热数据,对于冷数据直接访问 DB。
读写峰值
另外,对缓存数据的读写峰值,如果小于 10万 级别,简单分拆到独立 Cache 池即可。而一旦数据的读写峰值超过 10万 甚至到达 100万 级的QPS则需要对 Cache 进行分层处理,可以同时使用 Local-Cache 配合远程 cache甚至远程缓存内部继续分层叠加分池进行处理。微博业务中大多数核心业务的 Memcached 访问都采用的这种处理方式。
命中率
缓存的命中率对整个服务体系的性能影响甚大。对于核心高并发访问的业务,需要预留足够的容量,确保核心业务缓存维持较高的命中率。比如微博中的 Feed Vector Cache常年的命中率高达 99.5% 以上。为了持续保持缓存的命中率,缓存体系需要持续监控,及时进行故障处理或故障转移。同时在部分缓存节点异常、命中率下降时,故障转移方案,需要考虑是采用一致性 Hash 分布的访问漂移策略,还是采用数据多层备份策略。
过期策略
可以设置较短的过期时间,让冷 key 自动过期;
也可以让 key 带上时间戳,同时设置较长的过期时间,比如很多业务系统内部有这样一些 keykey_20190801。
平均缓存穿透加载时间
平均缓存穿透加载时间在某些业务场景下也很重要,对于一些缓存穿透后,加载时间特别长或者需要复杂计算的数据,而且访问量还比较大的业务数据,要配置更多容量,维持更高的命中率,从而减少穿透到 DB 的概率,来确保整个系统的访问性能。
缓存可运维性
对于缓存的可运维性考虑,则需要考虑缓存体系的集群管理,如何进行一键扩缩容,如何进行缓存组件的升级和变更,如何快速发现并定位问题,如何持续监控报警,最好有一个完善的运维平台,将各种运维工具进行集成。
缓存安全性
对于缓存的安全性考虑,一方面可以限制来源 IP只允许内网访问同时对于一些关键性指令需要增加访问权限避免被攻击或误操作时导致重大后果。
好了第3课时的内容到这里就全部结束了我们一起来做一个简单的回顾。首先我们学习了在系统研发中如何引入缓存如何按照4步走对缓存进行设计架构及管理。最后还熟悉了缓存设计架构中的考量点这样你在缓存设计架构时对号入座即可。

View File

@ -0,0 +1,125 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 缓存失效、穿透和雪崩问题怎么处理?
你好,我是你的缓存老师陈波,欢迎进入第 4 课时“缓存访问相关的经典问题”。
前面讲解了缓存的原理、引入,以及设计架构,总结了缓存在使用及设计架构过程中的很多套路和关键考量点。实际上,在缓存系统的设计架构中,还有很多坑,很多的明枪暗箭,如果设计不当会导致很多严重的后果。设计不当,轻则请求变慢、性能降低,重则会数据不一致、系统可用性降低,甚至会导致缓存雪崩,整个系统无法对外提供服务。
接下来将对缓存设计中的 7 大经典问题,如下图,进行问题描述、原因分析,并给出日常研发中,可能会出现该问题的业务场景,最后给出这些经典问题的解决方案。本课时首先学习缓存失效、缓存穿透与缓存雪崩。
缓存失效
问题描述
缓存第一个经典问题是缓存失效。上一课时讲到,服务系统查数据,首先会查缓存,如果缓存数据不存在,就进一步查 DB最后查到数据后回种到缓存并返回。缓存的性能比 DB 高 50~100 倍以上,所以我们希望数据查询尽可能命中缓存,这样系统负荷最小,性能最佳。缓存里的数据存储基本上都是以 key 为索引进行存储和获取的。业务访问时,如果大量的 key 同时过期,很多缓存数据访问都会 miss进而穿透到 DBDB 的压力就会明显上升,由于 DB 的性能较差,只在缓存的 1%~2% 以下,这样请求的慢查率会明显上升。这就是缓存失效的问题。
原因分析
导致缓存失效,特别是很多 key 一起失效的原因,跟我们日常写缓存的过期时间息息相关。
在写缓存时,我们一般会根据业务的访问特点,给每种业务数据预置一个过期时间,在写缓存时把这个过期时间带上,让缓存数据在这个固定的过期时间后被淘汰。一般情况下,因为缓存数据是逐步写入的,所以也是逐步过期被淘汰的。但在某些场景,一大批数据会被系统主动或被动从 DB 批量加载,然后写入缓存。这些数据写入缓存时,由于使用相同的过期时间,在经历这个过期时间之后,这批数据就会一起到期,从而被缓存淘汰。此时,对这批数据的所有请求,都会出现缓存失效,从而都穿透到 DBDB 由于查询量太大,就很容易压力大增,请求变慢。
业务场景
很多业务场景,稍不注意,就出现大量的缓存失效,进而导致系统 DB 压力大、请求变慢的情况。比如同一批火车票、飞机票,当可以售卖时,系统会一次性加载到缓存,如果缓存写入时,过期时间按照预先设置的过期值,那过期时间到期后,系统就会因缓存失效出现变慢的问题。类似的业务场景还有很多,比如微博业务,会有后台离线系统,持续计算热门微博,每当计算结束,会将这批热门微博批量写入对应的缓存。还比如,很多业务,在部署新 IDC 或新业务上线时,会进行缓存预热,也会一次性加载大批热数据。
解决方案
对于批量 key 缓存失效的问题,原因既然是预置的固定过期时间,那解决方案也从这里入手。设计缓存的过期时间时,使用公式:过期时间=baes 时间+随机时间。即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对 DB 造成过大压力,如下图所示。
缓存穿透
问题描述
第二个经典问题是缓存穿透。缓存穿透是一个很有意思的问题。因为缓存穿透发生的概率很低,所以一般很难被发现。但是,一旦你发现了,而且量还不小,你可能立即就会经历一个忙碌的夜晚。因为对于正常访问,访问的数据即便不在缓存,也可以通过 DB 加载回种到缓存。而缓存穿透,则意味着有特殊访客在查询一个不存在的 key导致每次查询都会穿透到 DB如果这个特殊访客再控制一批肉鸡机器持续访问你系统里不存在的 key就会对 DB 产生很大的压力,从而影响正常服务。
原因分析
缓存穿透存在的原因,就是因为我们在系统设计时,更多考虑的是正常访问路径,对特殊访问路径、异常访问路径考虑相对欠缺。
缓存访问设计的正常路径,是先访问 cachecache miss 后查 DBDB 查询到结果后,回种缓存返回。这对于正常的 key 访问是没有问题的,但是如果用户访问的是一个不存在的 key查 DB 返回空(即一个 NULL那就不会把这个空写回cache。那以后不管查询多少次这个不存在的 key都会 cache miss都会查询 DB。整个系统就会退化成一个“前端+DB“的系统由于 DB 的吞吐只在 cache 的 1%~2% 以下,如果有特殊访客,大量访问这些不存在的 key就会导致系统的性能严重退化影响正常用户的访问。
业务场景
缓存穿透的业务场景很多,比如通过不存在的 UID 访问用户,通过不存在的车次 ID 查看购票信息。用户输入错误,偶尔几个这种请求问题不大,但如果是大量这种请求,就会对系统影响非常大。
解决方案
那么如何解决这种问题呢?如下图所示。
第一种方案就是,查询这些不存在的数据时,第一次查 DB虽然没查到结果返回 NULL仍然记录这个 key 到缓存,只是这个 key 对应的 value 是一个特殊设置的值。
第二种方案是,构建一个 BloomFilter 缓存过滤器,记录全量数据,这样访问数据时,可以直接通过 BloomFilter 判断这个 key 是否存在,如果不存在直接返回即可,根本无需查缓存和 DB。
不过这两种方案在设计时仍然有一些要注意的坑。
对于方案一,如果特殊访客持续访问大量的不存在的 key这些 key 即便只存一个简单的默认值,也会占用大量的缓存空间,导致正常 key 的命中率下降。所以进一步的改进措施是,对这些不存在的 key 只存较短的时间,让它们尽快过期;或者将这些不存在的 key 存在一个独立的公共缓存,从缓存查找时,先查正常的缓存组件,如果 miss则查一下公共的非法 key 的缓存,如果后者命中,直接返回,否则穿透 DB如果查出来是空则回种到非法 key 缓存,否则回种到正常缓存。
对于方案二BloomFilter 要缓存全量的 key这就要求全量的 key 数量不大10亿 条数据以内最佳,因为 10亿 条数据大概要占用 1.2GB 的内存。也可以用 BloomFilter 缓存非法 key每次发现一个 key 是不存在的非法 key就记录到 BloomFilter 中,这种记录方案,会导致 BloomFilter 存储的 key 持续高速增长,为了避免记录 key 太多而导致误判率增大,需要定期清零处理。
BloomFilter
BloomFilter 是一个非常有意思的数据结构,不仅仅可以挡住非法 key 攻击,还可以低成本、高性能地对海量数据进行判断,比如一个系统有数亿用户和百亿级新闻 feed就可以用 BloomFilter 来判断某个用户是否阅读某条新闻 feed。下面来对 BloomFilter 数据结构做一个分析,如下图所示。
BloomFilter 的目的是检测一个元素是否存在于一个集合内。它的原理,是用 bit 数据组来表示一个集合,对一个 key 进行多次不同的 Hash 检测,如果所有 Hash 对应的 bit 位都是 1则表明 key 非常大概率存在,平均单记录占用 1.2 字节即可达到 99%,只要有一次 Hash 对应的 bit 位是 0就说明这个 key 肯定不存在于这个集合内。
BloomFilter 的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0加入元素时采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。检测 key 时,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1则表明 key 存在,否则不存在。
BloomFilter 的优势是,全内存操作,性能很高。另外空间效率非常高,要达到 1% 的误判率,平均单条记录占用 1.2 字节即可。而且,平均单条记录每增加 0.6 字节,还可让误判率继续变为之前的 1/10即平均单条记录占用 1.8 字节,误判率可以达到 1/1000平均单条记录占用 2.4 字节,误判率可以到 1/10000以此类推。这里的误判率是指BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值,所以有概率存在这样的 key它们内容不同但多次 Hash 后的 Hash 值都相同。对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1而不会是 0。
缓存雪崩
问题描述
第三个经典问题是缓存雪崩。系统运行过程中,缓存雪崩是一个非常严重的问题。缓存雪崩是指部分缓存节点不可用,导致整个缓存体系甚至甚至服务系统不可用的情况。缓存雪崩按照缓存是否 rehash即是否漂移分两种情况
缓存不支持 rehash 导致的系统雪崩不可用
缓存支持 rehash 导致的缓存雪崩不可用
原因分析
在上述两种情况中,缓存不进行 rehash 时产生的雪崩,一般是由于较多缓存节点不可用,请求穿透导致 DB 也过载不可用,最终整个系统雪崩不可用的。而缓存支持 rehash 时产生的雪崩,则大多跟流量洪峰有关,流量洪峰到达,引发部分缓存节点过载 Crash然后因 rehash 扩散到其他缓存节点,最终整个缓存体系异常。
第一种情况比较容易理解,缓存节点不支持 rehash较多缓存节点不可用时大量 Cache 访问会失败,根据缓存读写模型,这些请求会进一步访问 DB而且 DB 可承载的访问量要远比缓存小的多,请求量过大,就很容易造成 DB 过载,大量慢查询,最终阻塞甚至 Crash从而导致服务异常。
第二种情况是怎么回事呢?这是因为缓存分布设计时,很多同学会选择一致性 Hash 分布方式,同时在部分节点异常时,采用 rehash 策略,即把异常节点请求平均分散到其他缓存节点。在一般情况下,一致性 Hash 分布+rehash 策略可以很好得运行,但在较大的流量洪峰到临之时,如果大流量 key 比较集中,正好在某 12 个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常 Crash然后这些异常节点下线这些大流量 key 请求又被 rehash 到其他缓存节点,进而导致其他缓存节点也被过载 Crash缓存异常持续扩散最终导致整个缓存体系异常无法对外提供服务。
业务场景
缓存雪崩的业务场景并不少见微博、Twitter 等系统在运行的最初若干年都遇到过很多次。比如,微博最初很多业务缓存采用一致性 Hash+rehash 策略,在突发洪水流量来临时,部分缓存节点过载 Crash 甚至宕机,然后这些异常节点的请求转到其他缓存节点,又导致其他缓存节点过载异常,最终整个缓存池过载。另外,机架断电,导致业务缓存多个节点宕机,大量请求直接打到 DB也导致 DB 过载而阻塞整个系统异常。最后缓存机器复电后DB 重启,数据逐步加热后,系统才逐步恢复正常。
解决方案
预防缓存雪崩,这里给出 3 个解决方案。
方案一,对业务 DB 的访问增加读写开关,当发现 DB 请求变慢、阻塞,慢请求超过阀值时,就会关闭读开关,部分或所有读 DB 的请求进行 failfast 立即返回,待 DB 恢复后再打开读开关,如下图。
方案二,对缓存增加多个副本,缓存异常或请求 miss 后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。
方案三,对缓存体系进行实时监控,当请求访问的慢速比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。
实际上,微博平台系统,这三种方案都采用了,通过三管齐下,规避缓存雪崩的发生。

View File

@ -0,0 +1,69 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 缓存数据不一致和并发竞争怎么处理?
你好我是你的缓存老师陈波欢迎进入第5课时“缓存数据相关的经典问题”。
数据不一致
问题描述
七大缓存经典问题的第四个问题是数据不一致。同一份数据,可能会同时存在 DB 和缓存之中。那就有可能发生DB 和缓存的数据不一致。如果缓存有多个副本,多个缓存副本里的数据也可能会发生不一致现象。
原因分析
不一致的问题大多跟缓存更新异常有关。比如更新 DB 后,写缓存失败,从而导致缓存中存的是老数据。另外,如果系统采用一致性 Hash 分布,同时采用 rehash 自动漂移策略,在节点多次上下线之后,也会产生脏数据。缓存有多个副本时,更新某个副本失败,也会导致这个副本的数据是老数据。
业务场景
导致数据不一致的场景也不少。如下图所示,在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和 DB 的数据不一致。缓存 rehash 时,某个缓存机器反复异常,多次上下线,更新请求多次 rehash。这样一份数据存在多个节点且每次 rehash 只更新某个节点,导致一些缓存节点产生脏数据。
解决方案
要尽量保证数据的一致性。这里也给出了 3 个方案,可以根据实际情况进行选择。
第一个方案cache 更新失败后,可以进行重试,如果重试失败,则将失败的 key 写入队列机服务,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性。
第二个方案,缓存时间适当调短,让缓存数据及早过期后,然后从 DB 重新加载,确保数据的最终一致性。
第三个方案,不采用 rehash 漂移策略,而采用缓存分层策略,尽量避免脏数据产生。
数据并发竞争
问题描述
第五个经典问题是数据并发竞争。互联网系统,线上流量较大,缓存访问中很容易出现数据并发竞争的现象。数据并发竞争,是指在高并发访问场景,一旦缓存访问没有找到数据,大量请求就会并发查询 DB导致 DB 压力大增的现象。
数据并发竞争,主要是由于多个进程/线程中,有大量并发请求获取相同的数据,而这个数据 key 因为正好过期、被剔除等各种原因在缓存中不存在,这些进程/线程之间没有任何协调,然后一起并发查询 DB请求那个相同的 key最终导致 DB 压力大增,如下图。
业务场景
数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成该车次信息、该条微博存在并发竞争读取的问题。
解决方案
要解决并发竞争,有 2 种方案。
方案一是使用全局锁。如下图所示,即当缓存请求 miss 后,先尝试加全局锁,只有加全局锁成功的线程,才可以到 DB 去加载数据。其他进程/线程在读取缓存数据 miss 时,如果发现这个 key 有全局锁,就进行等待,待之前的线程将数据从 DB 回种到缓存后,再从缓存获取。
方案二是,对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况,如下图。

View File

@ -0,0 +1,127 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 MC是如何使用多线程和状态机来处理请求命令的
你好,我是你的缓存老师陈波,欢迎你进入第 9 课时“Memcached 网络模型及状态机”的学习。
网络模型
了解了 Mc 的系统架构之后,我们接下来可以逐一深入学习 Mc 的各个模块了。首先,我们来学习 Mc 的网络模型。
主线程
Mc 基于 Libevent 实现多线程网络 IO 模型。Mc 的 IO 处理线程分主线程和工作线程,每个线程各有一个 event_base来监听网络事件。主线程负责监听及建立连接。工作线程负责对建立的连接进行网络 IO 读取、命令解析、处理及响应。
Mc 主线程在监听端口时,当有连接到来,主线程 accept 该连接,并将连接调度给工作线程。调度处理逻辑,主线程先将 fd 封装成一个 CQ_ITEM 结构,并存入新连接队列中,然后轮询一个工作线程,并通过管道向该工作线程发送通知。工作线程监听到通知后,会从新连接队列获取一个连接,然后开始从这个连接读取网络 IO 并处理,如下图所示。主线程的这个处理逻辑主要在状态机中执行,对应的连接状态为 conn_listening。
工作线程
工作线程监听到主线程的管道通知后,会从连接队列弹出一个新连接,然后就会创建一个 conn 结构体,注册该 conn 读事件,然后继续监听该连接上的 IO 事件。后续这个连接有命令进来时,工作线程会读取 client 发来的命令,进行解析并处理,最后返回响应。工作线程的主要处理逻辑也是在状态机中,一个名叫 drive_machine 的函数。
状态机
这个状态机由主线程和工作线程共享,实际是采用 switch-case 来实现的。状态机函数如下图所示switch 连接的 state然后根据连接的不同状态执行不同的逻辑操作并进行状态转换。接下来我们开始分析 Mc 的状态机。
主线程状态机
如下图所示,主线程在状态机中只处理 conn_listening 状态,负责 accept 新连接和调度新连接给工作线程。状态机中其他状态处理基本都在工作线程中进行。由于 Mc 同时支持 TCP、UDP 协议,而互联网企业大多使用 TCP 协议,并且通过文本协议,来访问 Mc所以后面状态机的介绍将主要结合 TCP 文本协议来进行重点分析。
工作线程状态机
工作线程的状态机处理逻辑,如下图所示,包括刚建立 conn 连接结构体时进行的一些重置操作,然后注册读事件,在有数据进来时,读取网络数据,并进行解析并处理。如果是读取指令或统计指令,至此就基本处理完毕,接下来将响应写入连接缓冲。如果是更新指令,在进行初步处理后,还会继续读取 value 部分,再进行存储或变更,待变更完毕后将响应写入连接缓冲。最后再将响应写给 client。响应 client 后,连接会再次重置连接状态,等待进入下一次的命令处理循环中。这个过程主要包含了 conn_new_cmd、conn_waiting、conn_read、conn_parse_cmd、conn_nread、conn_write、conn_mwrite、conn_closing 这 8 个状态事件。
工作线程状态事件及逻辑处理
conn_new_cmd
主线程通过调用 dispatch_conn_new把新连接调度给工作线程后worker 线程创建 conn 对象,这个连接初始状态就是 conn_new_cmd。除了通过新建连接进入 conn_new_cmd 状态之外,如果连接命令处理完毕,准备接受新指令时,也会将连接的状态设置为 conn_new_cmd 状态。
进入 conn_new_cmd 后,工作线程会调用 reset_cmd_handler 函数,重置 conn 的 cmd 和 substate 字段,并在必要时对连接 buf 进行收缩。因为连接在处理 client 来的命令时,对于写指令,需要分配较大的读 buf 来存待更新的 key value而对于读指令则需要分配较大的写 buf 来缓冲待发送给 client 的 value 结果。持续运行中,随着大 size value 的相关操作,这些缓冲会占用很多内存,所以需要设置一个阀值,超过阀值后就进行缓冲内存收缩,避免连接占用太多内存。在后端服务以及中间件开发中,这个操作很重要,因为线上服务的连接很容易达到万级别,如果一个连接占用几十 KB 以上的内存,后端系统仅连接就会占用数百 MB 甚至数 GB 以上的内存空间。
conn_parse_cmd
工作线程处理完 conn_new_cmd 状态的主要逻辑后,如果读缓冲区有数据可以读取,则进入 conn_parse_cmd 状态,否则就会进入到 conn_waiting 状态,等待网络数据进来。
conn_waiting
连接进入 conn_waiting 状态后,处理逻辑很简单,直接通过 update_event 函数注册读事件即可,之后会将连接状态更新为 conn_read。
conn_read
当工作线程监听到网络数据进来,连接就进入 conn_read 状态。对 conn_read 的处理,是通过 try_read_network 从 socket 中读取网络数据。如果读取失败,则进入 conn_closing 状态,关闭连接。如果没有读取到任何数据,则会返回 conn_waiting继续等待 client 端的数据到来。如果读取数据成功,则会将读取的数据存入 conn 的 rbuf 缓冲,并进入 conn_parse_cmd 状态,准备解析 cmd。
conn_parse_cmd
conn_parse_cmd 状态的处理逻辑就是解析命令。工作线程首先通过 try_read_command 读取连接的读缓冲,并通过 \n 来分隔数据报文的命令。如果命令首行长度大于 1024关闭连接这就意味着 key 长度加上其他各项命令字段的总长度要小于 1024字节。当然对于 keyMc 有个默认的最大长度key_max_length默认设置为 250字节。校验完毕首行报文的长度接下来会在 process_command 函数中对首行指令进行处理。
process_command 用来处理 Mc 的所有协议指令所以这个函数非常重要。process_command 会首先按照空格分拆报文,确定命令协议类型,分派给 process_XX_command 函数处理。
Mc 的命令协议从直观逻辑上可以分为获取类型、变更类型、其他类型。但从实际处理层面区分,则可以细分为 get 类型、update 类型、delete 类型、算术类型、touch 类型、stats 类型以及其他类型。对应的处理函数为process_get_command, process_update_command, process_arithmetic_command, process_touch_command等。每个处理函数能够处理不同的协议具体参见下图所示思维导图。
conn_parse_cmd
注意 conn_parse_cmd 的状态处理,只有读取到 \n有了完整的命令首行协议才会进入 process_command否则会跳转到 conn_waiting继续等待客户端的命令数据报文。在 process_command 处理中,如果是获取类命令,在获取到 key 对应的 value 后,则跳转到 conn_mwrite准备写响应给连接缓冲。而对于 update 变更类型的指令,则需要继续读取 value 数据,此时连接会跳转到 conn_nread 状态。在 conn_parse_cmd 处理过程中,如果遇到任何失败,都会跳转到 conn_closing 关闭连接。
complete_nread
对于 update 类型的协议指令,从 conn 继续读取 value 数据。读取到 value 数据后,会调用 complete_nread进行数据存储处理数据处理完毕后向 conn 的 wbuf 写响应结果。然后 update 类型处理的连接进入到 conn_write 状态。
conn_write
连接 conn_write 状态处理逻辑很简单,直接进入 conn_mwrite 状态。或者当 conn 的 iovused 为 0 或对于 udp 协议,将响应写入 conn 消息缓冲后,再进入 conn_mwrite 状态。
conn_mwrite
进入 conn_mwrite 状态后,工作线程将通过 transmit 来向客户端写数据。如果写数据失败,跳转到 conn_closing关闭连接退出状态机。如果写数据成功则跳转到 conn_new_cmd准备下一次新指令的获取。
conn_closing
最后一个 conn_closing 状态,前面提到过很多次,在任何状态的处理过程中,如果出现异常,就会进入到这个状态,关闭连接,这个连接也就 Game Over了。
Mc 命令处理全流程
至此Mc 的系统架构和状态机的内容就全部讲完了,再梳理一遍 Mc 对命令的处理全过程,如下图所示,从而加深对 Mc 的状态机及命令处理流程的理解。
Mc 启动后,主线程监听并准备接受新连接接入。当有新连接接入时,主线程进入 conn_listening 状态accept 新连接,并将新连接调度给工作线程。
Worker 线程监听管道,当收到主线程通过管道发送的消息后,工作线程中的连接进入 conn_new_cmd 状态,创建 conn 结构体,并做一些初始化重置操作,然后进入 conn_waiting 状态,注册读事件,并等待网络 IO。
有数据到来时,连接进入 conn_read 状态,读取网络数据。
读取成功后,就进入 conn_parse_cmd 状态,然后根据 Mc 协议解析指令。
对于读取指令,获取到 value 结果后,进入 conn_mwrite 状态。
对于变更指令,则进入 conn_nread进行 value 的读取,读取到 value 后,对 key 进行变更,当变更完毕后,进入 conn_write然后将结果写入缓冲。然后和读取指令一样也进入 conn_mwrite 状态。
进入到 conn_mwrite 状态后,将结果响应发送给 client。发送响应完毕后再次进入到 conn_new_cmd 状态,进行连接重置,准备下一次命令处理循环。
在读取、解析、处理、响应过程,遇到任何异常就进入 conn_closing关闭连接。
总结下最近 3 个课时的内容。首先讲解了 Memcached 的原理及特性。然后结合 Memcached 的系统架构,学习了 Mc 基于 Libevent 的多线程网络模型,知道了 Mc 的 IO 主线程负责接受连接及调度,工作线程负责读取指令、处理并响应。本课时还有一个重点是 Memcached 状态机,知道了主线程处理 conn_listening工作线程处理其他 8 种重要状态。每种状态下对应不同的处理逻辑,从而将 Mc 整个冗长复杂的处理过程进行分阶段的处理,每个阶段只关注有限的逻辑,从而确保整个处理过程的清晰、简洁。
最后通过梳理 Mc 命令处理的全过程,学习了 Mc 如何建立连接,如何进行命令读取、处理及响应,从而把 Mc 的系统架构、多线程网络模型、状态机处理进行逻辑打通。
为了方便理解,提供本课时所有知识点的思维导图,如下图所示。

View File

@ -0,0 +1,51 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 MC是怎么定位key的
你好,我是你的缓存课老师陈波,欢迎你进入第 10 课时“Memcached 哈希表”的学习。
我们在进行 Mc 架构剖析时,除了学习 Mc 的系统架构、网络模型、状态机外,还对 Mc 的 slab 分配、Hashtable、LRU 有了简单的了解。本节课,将进一步深入学习这些知识点。
接下来,进入 Memcached 进阶的学习。会讲解 Mc 是如何进行 key 定位,如何淘汰回收过期失效 key 的,还将分析 Mc 的内存管理 slab 机制,以及 Mc 进行数据存储维护的关键机理,最后还会对 Mc 进行完整的协议分析,并以 Java 语言为例,介绍 Mc 常用的 client以及如何进行调优及改进。
key 定位
哈希表
Mc 将数据存储在 Item 中,然后这些 Item 会被 slabclass 的 4 个 LRU 管理。这些 LRU 都是通过双向链表实现数据记录的。双向链表在进行增加、删除、修改位置时都非常高效,但其获取定位 key 的性能非常低下只能通过链表遍历来实现。因此Mc 还通过 Hashtable也就是哈希表来记录管理这些 Item通过对 key 进行哈希计算,从而快速定位和读取这些 key/value 所在的 Item如下图所示。
哈希表也称散列表,可以通过把 key 映射到哈希表中的一个位置来快速访问记录,定位 key 的时间复杂度只有 O(1)。Mc 的哈希表实际是一个一维指针数组,数组的每个位置称作一个 bucket即一个桶。性能考虑的需要Mc 的哈希表的长度设置为 2 的 N 次方。Mc 启动时,默认会构建一个拥有 6.4万 个桶的哈希表,随着新 key 的不断插入,哈希表中的元素超过阀值后,会对哈希表进行扩容,最大可以构建 2 的 32 次方个桶的哈希表,也就是说 Mc 哈希表经过多次扩容后,最多只能有不超过 43亿 个桶。
哈希表设计
对于哈希表设计,有 2 个关键点一个是哈希算法一个是哈希冲突解决方案。Mc 使用的哈希算法有 2 种,分别是 Murmur3 Hash 和 Jenkins Hash。Mc 当前版本,默认使用 Murmur3 Hash 算法。不同的 key 通过 Hash 计算被定位到了相同的桶这就是哈希冲突。Mc 是通过对每个桶启用一个单向链表,来解决哈希冲突问题的。
定位 key
Memcached 定位 key 时,首先根据 key 采用 Murmur3 或者 Jenkins 算法进行哈希计算,得到一个 32 位的无符号整型输出,存储到变量 hv 中。因为哈希表一般没有 2^32 那么大,所以需要将 key 的哈希值映射到哈希表的范围内。Mc 采用最简单的取模算法作为映射函数,即采用 hv%hashsize 进行计算。由于普通的取模运算比较耗时,所以 Mc 将哈希表的长度设置为 2 的 n 次方,采用位运算进行优化,即采用 hv&hashmask 来计算。hashmask 即 2 的 n 次方 减 1。
定位到 key 所在的桶的位置后,如果是插入一个新数据,则将数据 Item 采用头部插入法插入桶的单向链表中。如果是查找,则轮询对应哈希桶中的那个单向链表,依次比对 key 字符串key 相同则找到数据 Item。
如果哈希表桶中元素太多,这个链表轮询耗时会比较长,所以在哈希表中元素达到桶数的 1.5 倍之后Mc 会对哈希表进行 2 倍扩容。由于哈希表最多只有 43 亿左右个桶,所以性能考虑,单个 Mc 节点最多存储 65亿 个 key/value。如果要存更多 key则需要修改 Mc 源码,将最大哈希,即 HASHPOWER_MAX 进行调大设置。
哈希表扩容
当 Mc 的哈希表中Item 数量大于 1.5 倍的哈希桶数量后Mc 就对哈希表进行扩容处理。如下图所示Mc 的哈希扩容是通过哈希维护线程进行处理的。准备开始扩容时,哈希维护线程会首先将所有 IO 工作线程和辅助线程进行暂停,其中辅助线程包括 LRU 维护线程、slab 维护线程、LRU 爬虫线程。待这些线程暂停后,哈希维护线程会将当前的主哈希表设为旧哈希表,然后将新的主哈希表扩容之前的 2 倍容量。然后,工作线程及辅助线程继续工作,同时哈希维护线程开始逐步将 Item 元素从旧哈希表迁移到主哈希表。
Mc 在启动时,会根据设置的工作线程数,来构建 一个 Item 锁哈希表,线程越多,构建的锁哈希表越大,对于 4 个线程,锁哈希表有 4096 个桶,对于 10 个线程,锁哈希表会有 8192 个桶Item 锁哈希表最多有 32k 个桶1k 是 1024即最多即 32768 个桶。Mc 的锁哈希表中,每个桶对应一个 Item 锁,所以 Mc 最多只有 32768 个 Item 锁。
Mc 哈希表在读取、变更以及扩容迁移过程中,先将 key hash 定位到 Item 锁哈希表的锁桶,然后对 Item 锁进行加锁,然后再进行实际操作。实际上,除了在哈希表,在其他任何时候,只要涉及到在对 Item 的操作,都会根据 Item 中的 key进行 Item 哈希锁桶加锁,以避免 Item 被同时读写而产生脏数据。Mc 默认有 4096 个锁桶,所以对 key 加锁时,冲突的概率较小,而且 Mc 全部是内存操作,操作速度很快,即便申请时锁被占用,也会很快被释放。
Mc 哈希表在扩容时,哈希表维护线程,每次按 桶链表纬度 迁移,即一次迁移一个桶里单向链表的所有 Item 元素。在扩容过程中,如果要查找或插入 key会参照迁移位置选择哈希表。如果 key 对应的哈希桶在迁移位置之前,则到新的主哈希表进行查询或插入,否则到旧哈希表进行查询和插入。待全部扩容迁移完毕,所有的处理就会全部在新的主哈希表进行。

View File

@ -0,0 +1,67 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 MC如何淘汰冷key和失效key
你好,我是你的缓存课老师陈波,欢迎进入第 11 课时“Memcached 淘汰策略”的学习。
淘汰策略
Mc 作为缓存组件,意味着 Mc 中只能存储访问最频繁的热数据,一旦存入数据超过内存限制,就需要对 Mc 中的冷 key 进行淘汰工作。Mc 中的 key 基本都会有过期时间,在 key 过期后出于性能考虑Mc 并不会立即删除过期的 key而是由维护线程逐步清理同时只有这个失效的 key 被访问时,才会进行删除,从而回收存储空间。所以 Mc 对 key 生命周期的管理,即 Mc 对 key 的淘汰,包括失效和删除回收两个纬度,知识结构如下图所示。
key 的失效,包括 key 在 expire 时间之后的过期,以及用户在 flush_all 之后对所有 key 的过期 2 种方式。
而 Mc 对 key/value 的删除回收,则有 3 种方式。
第一种是获取时的惰性删除,即 key 在失效后,不立即删除淘汰,而在获取时,检测 key 的状态,如果失效,才进行真正的删除并回收存储空间。
第二种方式是在需要对 Item 进行内存分配申请时,如果内存已全部用完,且该 Item 对应的slabclass 没有空闲的 chunk 可用,申请失败,则会对 LRU 队尾进行同步扫描,回收过期失效的 key如果没有失效的 key则会强制删除一个 key。
第三种方式是 LRU 维护线程,不定期扫描 4 个 LRU 队列,对过期 key/value 进行异步淘汰。
flush_all
Mc 中key 失效除了常规的到达过期时间之外,还有一种用 flush_all 的方式进行全部过期。如果缓存数据写入异常,出现大量脏数据,而又没有简单的办法快速找出所有的脏数据,可以用 flush_all 立即让所有数据失效,通过 key 重新从 DB 加载的方式来保证数据的正确性。flush_all 可以让 Mc 节点的所有 key 立即失效,不过,在某些场景下,需要让多个 Mc 节点的数据在某个时间同时失效,这时就可以用 flush_all 的延迟失效指令了。该指令通过 flush_all 指令后面加一个 expiretime 参数,可以让多个 Mc 在某个时间同时失效所有的 key。
flush_all 后面没有任何参数,等价于 flush_all 0即立即失效所有的 key。当 Mc 收到 flush_all 指令后,如果是延迟失效,会将全局 setting 中的 oldest_live 设为指定 N 秒后的时间戳,即 N 秒后失效;如果是立即失效,则将全局 setting 中的 oldest_cas 设为当前最大的全局 cas 值。设置完这个全局变量值后,立即返回。因此,在 Mc 通过 flush_all 失效所有 key 时,实际不做任何 key 的删除操作,这些 key ,后续会通过用户请求同步删除,或 LRU 维护线程的异步删除,来完成真正的删除动作。
惰性删除
Mc 中,过期失效 key 的惰性主动删除,是指在 touch、get、gets 等指令处理时,首先需要查询 key找到 key 所在的 Item然后校验 key 是否过期,是否被 flush如果过期或被 flush则直接进行真正的删除回收操作。
对于校验 key 过期很容易,直接判断过期时间即可。对于检查 key 是否被 flush处理逻辑是首先检查 key 的最近访问时间是否小于全局设置中的 oldest_live如果小于则说明 key 被 flush 了;否则,再检查 key 的 cas 唯一 id 值,如果小于全局设置中的 oldest_cas说明也被 flush 了。
内存分配失败LRU 同步淘汰
Mc 在插入或变更 key 时,首先会在适合的 slabclass 为新的 key/value 分配一个空闲的 Item 空间,如果分配失败,会同步对该 slabclass 的 COLD LRU 进行队尾元素淘汰,如果淘汰回收成功,则 slabclass 会多一个空闲的 Item这个 Item 就可以被前面那个 key 来使用。如果 COLD LRU 队列没有 Item 数据,则淘汰失败,此时会对 HOT LRU 进行队尾轮询,如果 key 过期失效则进行淘汰回收,否则进行迁移。
LRU 维护线程,异步淘汰
在 key 进行读取、插入或变更时,同步进行 key 淘汰回收,并不是一种高效的办法,因为淘汰回收操作相比请求处理,也是一个重量级操作,会导致 Mc 性能大幅下降。因此 Mc 额外增加了一个 LRU 维护线程,对过期失效 key 进行回收,在不增加请求负担的情况下,尽快回收失效 key 锁占用的空间。
前面讲到Mc 有 64 个 slabclass其中 1~63 号 slabclass 用于存取 Item 数据。实际上为了管理过期失效数据1~63 号 slabclass 还分别对应了 4 个 LRU分布是 TEMP、HOT、WARM、COLD LRU。所以这就总共有 63*4 = 252 个 LRU。LRU 维护线程,会按策略间断 sleep待 sleep 结束,就开始对 4 个 LRU 进行队尾清理工作。
Mc 在新写入 key 时,如果 key 的过期时间小于 61s就会直接插入到 TEMP LRU 中如下图所示。TEMP LRU 没有长度限制可以一直插入同时因为过期时间短TEMP LRU 不进行队列内部的搬运和队列间的迁移确保处理性能最佳。LRU 维护线程在 sleep 完毕后,首先会对 TEMP LRU 队尾进行 500 次轮询,然后在每次轮询时,会进行 5 次小循环。小循环时,首先检查 key是否过期失效如果失效则进行回收淘汰然后继续小循环如果遇到一个没失效的 key则回收该 key 并退出 TEMP LRU 的清理工作。如果 TEMP LRU 队尾 key 全部失效,维护线程一次可以回收 500*5 共 2500 个失效的 key。
如下图MC 在新写入 key 时,如果 key 的过期时间超过 61s就会直接插入到 HOT LRU。HOT LRU 会有内存限制,每个 HOT LRU 所占内存不得超过所在 slabclass 总实际使用内存的 20%。LRU 维护线程在执行日常维护工作时,首先对 TEMP LRU 进行清理,接下来就会对 HOT LRU 进行维护。HOT LRU 的维护,也是首先轮询 500 次,每次轮询进行 5 次小循环,小循环时,首先检查 key 是否过期失效,如果失效则进行回收淘汰,然后继续小循环。直到遇到没失效的 key。如果这个 key 的状态是 ACTIVE则迁移到 WARM LRU。对于非 ACTIVE 状态的 key如果 HOT LRU 内存占用超过限制,则迁移到 COLD LRU否则进行纾困性清理掉该 key注意这种纾困性清理操作一般不会发生一旦发生时虽然会清理掉该 key但操作函数此时也认定本次操作回收和清理 keys 数仍然为 0。
如下图,如果 HOT LRU 中回收和迁移的 keys 数为 0LRU 维护线程会对 WARM LRU 进行轮询。WARM LRU 也有内存限制,每个 WARM LRU 所占内存不得超过所在 slabclass 总实际使用内存的 40%。WARM LRU 的维护,也是首先轮询 500 次,每次轮询进行 5 次小循环,小循环时,首先检查 key 是否过期失效,如果失效则进行回收淘汰,然后继续小循环。直到遇到没失效的 key。如果这个 key 的状态是 ACTIVE则内部搬运到 LRU 队列头部。对于非 ACTIVE 状态的 key如果 WARM LRU 内存占用超过限制,则迁移到 COLD LRU否则进行纾困性清理掉该 key。注意这种纾困性清理操作一般不会发生一旦发生时虽然会清理掉该 key但操作函数此时也认定本次操作回收和清理 keys 数仍然为 0。
LRU 维护线程最后会对 COLD LRU 进行维护,如下图。与 TEMP LRU 相同COLD LRU 也没有长度限制可以持续存放数据。COLD LRU 的维护,也是首先轮询 500 次,每次轮询进行 5 次小循环,小循环时,首先检查 key 是否过期失效,如果失效则进行回收淘汰,然后继续小循环。直到遇到没失效的 key。如果这个 key 的状态是 ACTIVE则会迁移到 WARM LRU 队列头部,否则不处理直接返回。
LRU 维护线程处理时TEMP LRU 是在独立循环中进行,其他三个 LRU 在另外一个循环中进行,如果 HOT、WARM、COLD LRU 清理或移动的 keys 数为 0则那个 500 次的大循环就立即停止。

View File

@ -0,0 +1,104 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 大数据时代MC如何应对新的常见问题
你好,我是你的缓存课老师陈波,欢迎进入第 14 课时“Memcached 经典问题及解决方案”的学习。
大数据时代 Memcached 经典问题
随着互联网的快速发展和普及,人类进入了大数据时代。在大数据时代,移动设备全面融入了人们的工作和生活,各种数据以前所未有的 速度被生产、挖掘和消费。移动互联网系统也不断演进和发展,存储、计算和分析这些海量数据,以满足用户的需要。在大数据时代,大中型互联网系统具有如下特点。
首先,系统存储的数据量巨大,比如微博系统,每日有数亿条记录,历史数据达百亿甚至千亿条记录。
其次,用户多,访问量巨大,每日峰值流量高达百万级 QPS。
要存储百千亿级的海量数据,同时满足大量用户的高并发访问,互联网系统需要部署较多的服务实例,不少大中型互联网系统需要部署万级,甚至十万级的服务实例。
再次,由于大数据时代,社会信息获取扁平化,热点事件、突发事件很容易瞬间引爆,引来大量场外用户集中关注,从而形成流量洪峰。
最后,任何硬件资源都有发生故障的概率,而且存在 4 年故障效应,即服务资源在使用 4 年后,出现故障的概率会陡增;由于大中型互联网系统的部署,需要使用大量的服务器、路由器和交换机,同时部署在多个地区的不同 IDC很多服务资源的使用时间远超 4 年,局部出现硬件故障障、网络访问异常就比较常见了。
由于互联网系统会大量使用 Memcached 作为缓存,而在使用 Memcached 的过程中,同样也会受到前面所说的系统特点的影响,从而产生特有的经典问题。
容量问题
第一个问题是容量问题。Memcached 在使用中,除了存储数据占用内存外,连接的读写缓冲、哈希表分配、辅助线程处理、进程运行等都会占用内存空间,而且操作系统本身也会占用不少内存,为了确保 Mc 的稳定运行Mc 的内存设置,一般设为物理内存的 80%。另外,设置的内存,也不完全是存储有效数据,我上一节课讲到,每个 Item 数据存储在 chunk 时,会有部分字节浪费,另外 key 在过期、失效后,不是立即删除,而是采用延迟淘汰、异步 LRU 队尾扫描的方式清理,这些暂时没有淘汰的、过期失效的 key ,也会占用不少的存储空间。当前大数据时代,互联网系统中的很多核心业务,需要缓存的热数据在 300~500GB 以上,远远超过单机物理内存的容量。
性能瓶颈
第二个问题是性能瓶颈问题。出于系统稳定性考虑,线上 Mc 的访问,最大 QPS 要在 10~20w 以下,超过则可能会出现慢查的问题。而对中大型互联网系统,核心业务的缓存请求高达百万级 QPS仅仅靠简单部署单个物理机、单个资源池很难达到线上的业务要求。
连接瓶颈
第三个问题是连接瓶颈的问题。出于稳定性考虑,线上 Mc 的连接数要控制在 10w 以下。以避免连接数过多,导致连接占用大量内存,从而出现命中率下降、甚至慢查超时的问题。对于大中型系统,线上实例高达万级、甚至十万级,单个实例的最小、最大连接数,一般设置在 5~60 个之间。业务实例的连接数远超过单个机器的稳定支撑范围。
硬件资源局部故障
第四个问题是硬件资源局部故障,导致的缓存体系的可用性问题。由于任何硬件资源,都有一定故障概率,而且在使用 4 年后,故障率陡增。对于数以万计的硬件设备,随时都有可能出现机器故障,从而导致 Mc 节点访问性能下降、宕机,海量访问穿透到 DB引发 DB 过载,最终导致整个系统无法访问,引发雪崩现象。
流量洪峰下快速扩展
第五个问题是在流量洪峰的场景下,如何快速扩展的问题。大数据时代,由于信息扩散的扁平化,突发事件、重大活动发生时,海量用户同时蜂拥而至,短时间引发巨大流量。整个系统的访问量相比日常峰值增大 70% 以上,同时出现大量的极热 key 的访问,这些极热 key 所在的 Mc 节点,访问量相比日常高峰,增大 2~3 倍以上,很容易出现 CPU 飙升、带宽打满、机器负荷严重过载的现象。
Memchcaed 经典问题及应对方案
为了解决大中型互联网系统在使用 Mc 时的这些问题。我们可以使用下面的解决方案。
Memcached 分拆缓存池
首先对系统内的核心业务数据进行分拆,让访问量大的数据,使用独立的缓存池。同时每个缓存池 4~8 个节点,这样就可以支撑足够大的容量,还避免单个缓存节点压力过大。对于缓存池的分布策略,可以采用一致性哈希分布和哈希取模分布。
一致性哈希分布算法中,首先计算 Mc 服务节点的哈希值,然后将其持续分散配置在圆中,这样每个缓存节点,实际包括大量大小各异的 N 个 hash 点。如下图所示,在数据存储或请求时,对 key 采用相同的 hash 算法,并映射到前面的那个圆中,从映射位置顺时针查找,找到的第一个 Mc 节点,就是目标存取节点。
而哈希取模分布算法,则比较简单,对 key 做 hash 后,对 Mc 节点数取模,即可找到待存取的目标 Mc 节点。
系统运行过程中Mc 节点故障不可避免,有时候甚至短期内出现多次故障。在 Mc 节点故障下线后,如果采用一致性 hash 分布,可以方便得通过 rehash 策略,将该 Mc 节点的 hash 点、访问量,均匀分散到其他 Mc 节点。如果采用取模分布,则会直接导致 1/N 的访问 missN 是 Mc 资源池的节点数。
因此,对于单层 Mc 缓存架构,一致性 hash 分布配合 rehash 策略,是一个更佳的方案。通过将业务数据分拆到独立 Mc 资源池,同时在每个资源池采用合适的分布算法,可以很好的解决 Mc 使用中容量问题、性能瓶颈问题,以及连接瓶颈问题。
Master-Slave 两级架构
在系统的访问量比较大,比如峰值 QPS 达到 20w 以上时,如果缓存节点故障,即便采用一致性 hash也会在一段时间内给 DB 造成足够大的压力,导致大量慢查询和访问超时的问题。另外,如果某些缓存服务器短期多次故障,反复上下线,多次 rehash 还会产生脏数据。对此,可以采用 Master-Slave 的两级架构方案。
在这种架构方案下,将业务正常访问的 Memcached 缓存池作为 master然后在 master 之后再加一个slave 资源池作 master 的热备份。slave 资源池也用 6~8 个节点,内存设置只用 master 的 12~13 即可。因为 slave 的应用,主要是考虑在 master 访问 miss 或异常时Mc 缓存池整体的命中率不会过度下降,所以并不需要设置太大内存。
日常访问,对于读操作,直接访问 master如果访问 miss再访问 slave。如果 slave 命中,就将读取到的 key 回写到 master。对于写操作set、touch 等覆盖类指令直接更新master 和 slave而 cas、append 等,以 master 为准master 在 cas、add 成功后,再将 key 直接 set 到 slave以保持 master、slave 的数据一致性。
如下图,在 master 部分节点异常后,由 slave 层来承接。任何一层,部分节点的异常,不会影响整体缓存的命中率、请求耗时等 SLA 指标。同时分布方式采用哈希取模方案mc 节点异常不rehash直接穿透方案简洁还可以避免一致性 hash 在 rehash 后产生的脏数据问题。
Master-Slave 架构,在访问量比较大的场景下,可以很好得解决局部设备故障的问题。在部分节点异常或访问 miss 时,多消耗 1ms 左右的时间,访问 slave 资源,实现以时间换系统整体可用性的目的。
M-S-L1 架构
20世纪初意大利统计学家帕累托提出来一个观点在任何特定群体中重要的因子通常只占少数而不重要的因子则占多数因此只要能控制具有重要性的少数因子即能控制全局。这个理论经过多年演化就成为当前大家所熟悉的 8020 定律。80/20 定律在互联网系统中也广泛存在,如 80% 的用户访问会集中在系统 20% 的功能上80% 的请求会集中在 20% 的数据上。因此,互联网系统的数据,有明显的冷热区分,而且这个冷热程度往往比 8020 更大,比如微博、微信最近一天的数据,被访问的特别频繁,而一周前的数据就很少被访问了。而且最近几天的热数据中,部分 feed 信息会被大量传播和交互,比其他 大部分数据的访问量要高很多倍,形成明显的头部请求。
头部请求,会导致日常大量访问,被集中在其中一小部分 key 上。同时,在突发新闻、重大事件发生时,请求量短期增加 50~70% 以上,而这些请求,又集中在 突发事件的关联 key 上,造就大量的热 key 的出现。热 key 具有随机性,如果集中在某少数几个节点,就会导致这 些节点的压力陡增数倍,负荷严重过载,进而引发大量查询变慢超时的问题。
为了应对日常峰值的热数据访问,特别是在应对突发事件时,洪峰流量带来的极热数据访问,我们可以通过增加 L1 层来解决。如下图所示L1 层包含 2~6 组 L1 资源池,每个 L1 资源池,用 4~6 个节点,但内存容量只要 Master 的 110 左右即可。
如图,读请求时,首先随机选择一个 L1 进行读取,如果 miss 则访问 master如果 master 也 miss最后访问 slave。中途只要任何一层命中则对上一层资源池进行回写。
写请求时,同 Master-Slave 架构类似,对于 set 覆盖类指令,直接 set 三层所有的资源池。对于 add/cas/append 等操作,以 master 为准master 操作成功后,将最后的 key/value set 到 L1 和 slave 层所有资源池。
由于 L1 的内存只有 master 的 1/10且 L1 优先被读取,所以 L1 中 Memcached 只会保留最热的 key因为 key 一旦稍微变冷,就会排到 COLD LRU 队尾,并最终被剔除。虽然 L1 的内存小,但由于 L1 里,永远只保存了 系统访问量 最大最热的数据,根据我们的统计, L1 可以满足整个系统的 60~80% 以上的请求数据。这也与 8020 原则相符合。
master 存放全量的热数据,用于满足 L1 读取 miss 或异常后的访问流量。slave 用来存放绝大部分的热数据,而且与 master 存在一定的差异,用来满足 L1、master 读取 miss 或异常的访问流量。
这里面有个可以进一步优化的地方,即为确保 master、slave 的热度,让 master、slave 也尽可能只保留最热的那部分数据,可以在读取 L1 时,保留适当的概率,直接读取 master 或slave让最热的 key 被访问到,从而不会被 master、slave 剔除。此时,访问路径需要稍做调整,即如果首先访问了 master如果 miss接下来只访问 slave。而如果首先访问了 slave如果 miss接下来只访问 master。
通过 Master-Slave-L1 架构在流量洪峰到来之际我们可以用很少的资源快速部署多组L1资源池然后加入 L1 层中,从而让整个系统的抗峰能力达到 N 倍的提升。从而以最简洁的办法,快速应对流量洪峰,把极热 key 分散到 N 组 L1 中,每个 L1 资源池只用负责 1/N 的请求。除了抗峰,另外,还可以轻松应对局部故障,避免雪崩的发生。
本课时,讲解了大数据时代下大中型互联网系统的特点,访问 Memcached 缓存时的经典问题及应对方案还讲解了如何通过分拆缓存池、Master-Slave 双层架构,来解决 Memcached 的容量问题、性能瓶颈、连接瓶颈、局部故障的问题,以及 Master-Slave-L1 三层架构,通过多层、多副本 Memcached 体系,来更好得解决突发洪峰流量和局部故障的问题。
可以参考下面的思维导图,对这些知识点进行回顾和梳理。

View File

@ -0,0 +1,142 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 如何深入理解、应用及扩展 Twemproxy
你好,我是你的缓存课老师陈波,欢迎进入第 15 课时“Twemproxy 框架、应用及扩展”的学习。
Twemproxy 架构及应用
Twemproxy 是 Twitter 的一个开源架构,它是一个分片资源访问的代理组件。如下图所示,它可以封装资源池的分布及 hash 规则,解决后端部分节点异常后的探测和重连问题,让 client 访问尽可能简单,同时资源变更时,只要在 Twemproxy 变更即可,不用更新数以万计的 client让资源变更更轻量。最后Twemproxy 跟后端通过单个长连接访问,可以大大减少后端资源的连接压力。
系统架构
接下来分析基于 Twemproxy 的应用系统架构,以及 Twemproxy 组件的内部架构。
如下图所示, 在应用系统中Twemproxy 是一个介于 client 端和资源端的中间层。它的后端支持Memcached 资源池和 Redis 资源池的分片访问。Twemproxy 支持取模分布和一致性 hash 分布,还支持随机分布,不过使用场景较少。
应用前端在请求缓存数据时,直接访问 Twemproxy 的对应端口,然后 Twemproxy 解析命令得到 key通过 hash 计算后,按照分布策略,将 key 路由到后端资源的分片。在后端资源响应后,再将响应结果返回给对应的 client。
在系统运行中Twemproxy 会自动维护后端资源服务的状态。如果后端资源服务异常,会自动进行剔除,并定期探测,在后端资源恢复后,再对缓存节点恢复正常使用。
组件架构
Twemproxy 是基于 epoll 事件驱动模型开发的,架构如下图所示。它是一个单进程、单线程组件。核心进程处理所有的事件,包括网络 IO协议解析消息路由等。Twemproxy 可以监听多个端口每个端口接受并处理一个业务的缓存请求。Twemproxy 支持 Redis、Memcached 协议,支持一致性 hash 分布、取模分布、随机分布三种分布方案。Twemproxy 通过 YAML 文件进行配置,简单清晰,且便于人肉读写。
Twemproxy 与后端资源通过单个长连接访问,在收到业务大量并发请求后,会通过 pipeline 的方式将多个请求批量发到后端。在后端资源持续访问异常时Twemproxy 会将其从正常列表中剔除,并不断探测,待其恢复后再进行请求的路由分发。
Twemproxy 运行中会持续产生海量请求及响应的消息流于是开发者精心设计了内存管理机制尽可能的减少内存分配和复制最大限度的提升系统性能。Twemproxy 内部请求和响应都是一个消息而这个消息结构体以及消息存放数据的缓冲都是重复使用的避免反复分配和回收的开销提升消息处理的性能。为了解决短连接的问题Twemproxy 的连接也是复用的,这样在面对 PHP client 等短连接访问时,也可以反复使用之前分配的 connection提升连接性能。
另外Twemproxy 对消息还采用了 zero copy即零拷贝方案。对于请求消息只在client 接受时读取一次,后续的解析、处理、转发都不进行拷贝,全部共享最初的那个消息缓冲。对于后端的响应也采用类似方案,只在接受后端响应时,读取到消息缓冲,后续的解析、处理及回复 client 都不进行拷贝。通过共享消息体及消息缓冲,虽然 Twemproxy 是单进程/单线程处理,仍然可以达到 6~8w 以上的 QPS。
Twemproxy 请求及响应
接下来看一下 Twemproxy 是如何进行请求路由及响应的。
Twemproxy 监听端口,当有 client 连接进来时,则 accept 新连接,并构建初始化一个 client_conn。当建连完毕client 发送数据到来时client_conn 收到网络读事件,则从网卡读取数据,并记入请求消息的缓冲中。读取完毕,则开始按照配置的协议进行解析,解析成功后,就将请求 msg 放入到 client_conn 的 out 队列中。接下来,就对解析的命令 key 进行 hash 计算,并根据分布算法,找到对应 server 分片的连接,即一个 server_conn 结构体,如下图。
如果 server_conn的 in 队列为空,首先对 server_conn 触发一个写事件。然后将 req msg 存入到 server_conn 的 in 队列。Server_conn 在处理写事件时,会对 in 队列中的 req msg 进行聚合,按照 pipeline 的方式批量发送到后端资源。待发送完毕后,将该条请求 msg 从 server_conn 的 in 队列删除,并插入到 out 队列中。
后端资源服务完成请求后,会将响应发送给 Twemproxy。当响应到 Twemproxy 后,对应的 server_conn 会收到 epoll 读事件,则开始读取响应 msg。响应读取并解析后会首先将server_conn 中out 队列的第一个 req msg 删除,并将这个 req msg 和最新收到的 rsp msg 进行配对。在 req 和 rsp 匹配后,触发 client_conn 的写事件,如下图。
然后 client_conn 在处理 epoll 写事件时,则按照请求顺序,批量将响应发送给 client 端。发送完毕后,将 req msg 从 client 的 out 队列删除。最后,再回收消息缓冲,以及消息结构体,供后续请求处理的时候复用。至此一个请求的处理彻底完成。
Twemproxy 安装和使用
Twemproxy 的安装和使用比较简单。首先通过 Git将 Twemproxy 从 GitHub clone 到目标服务器,然后进入 Twemproxy 路径,首先执行 $ autoreconf -fvi然后执行 ./configure ,最后执行 make当然也可以再执行 make install这样就完成了 Temproxy 的编译和安装。然后就可以通过 src/nutcracker -c /xxx/conf/nutcracker.yml 来启动 Twemproxy 了。
Twemproxy 代理后端资源访问,这些后端资源的部署信息及访问策略都是在 YAML 文件中配置。所以接下来,我们简单看一下 Twemproxy 的配置。如图所示,这个配置中代理了 2 个业务数据的缓存访问。一个是 alpha另一个是 beta。在每个业务的配置详情里。首先是 listen 配置项,用于设置监听该业务的端口。然后是 hash 算法和分布算法。Auto_eject_hosts 用于设置在后端 server 异常时,是否将这个异常 server 剔除,然后进行 rehash默认不剔除。Redis配置项用于指示后端资源类型是 Redis 还是 Memcached。最后一个配置项 servers用于设置资源池列表。
以 Memcached 访问为例,将业务的 Memcached 资源部署好之后,然后将 Mc 资源列表、访问方式等设到 YAML 文件的配置项,然后启动 Twemproxy业务端就可以通过访问 Twemproxy 来获取后端资源的数据了。后续Mc 资源有任何变更,业务都不用做任何改变,运维直接修改 Twemproxy 的配置即可。
Twemproxy 在实际线的使用中,还是存在不少问题的。首先,它是单进程/单线程模型,一个 event_base 要处理所有的事件,这些事件包括 client 请求的读入,转发请求给后端 server从 server 接受响应,以及将响应发送给 client。单个 Twemproxy 实例,压测最大可以到 8w 左右的 QPS出于线上稳定性考虑QPS 最多支撑到 3~4w。而 Memcached 的线上 QPS一般可以达到 10~20w一个 Mc 实例前面要挂 3~5 个 Twemproxy 实例。实例数太多,就会引发诸如管理复杂、成本过高等一系列问题。
其次基于性能及预防单点故障的考虑Twemproxy 需要进行多实例部署,而且还需要根据业务访问量的变化,进行新实例的加入或冗余实例的下线。多个 Twemproxy 实例同时被访问,如果 client 访问策略不当,就会出现有些 Twemproxy 压力过大,而有些却很空闲,造成访问不均的问题。
再次,后端资源在 Twemproxy 的 YAML 文件集中配置,资源变更的维护,比直接在所有业务 client 端维护,有了很大的简化。但在多个 Twemproxy 修改配置,让这些配置同时生效,也是一个复杂的工作。
最后Twemproxy 也无法支持 Mc 多副本、多层次架构的访问策略,无法支持 Redis 的Master-Slave 架构的读写分离访问。
为此,你可以对 Twemproxy 进行扩展,以更好得满足业务及运维的需要。
Twemproxy 扩展
多进程改造
性能首当其冲。首先可以对 Twemproxy 的单进程/单线程动刀,改为并行处理模型。并行方案可以用多线程方案,也可以采用多进程方案。由于 Twemproxy 只是一个消息路由中间件,不需要额外共享数据,采用多进程方案会更简洁,更适合。
多进程改造中,可以分别构建一个 master 进程和多个 worker 进程来进行任务处理,如下图所示。每个进程维护自己独立的 epoll 事件驱动。其中 master 进程主要用于监听端口accept 新连接,并将连接调度给 worker 进程。
而 worker 进程,基于自己独立的 event_base管理从 master 调度给自己的所有 client 连接。在 client 发送网络请求到达时,进行命令读取、解析,并在进程内的 IO 队列流转最后将请求打包pipeline 给后端的 server。
在 server 处理完毕请求,发回响应时。对应 worker 进程,会读取并解析响应,然后批量回复给 client。
通过多进程改造Twemproxy 的 QPS 可以从 8w 提升到 40w+。业务访问时需要部署的Twemproxy 的实例数会大幅减少,运维会更加简洁。
增加负载均衡
对于多个 Twemproxy 访问,如何进行负载均衡的问题。一般有三种方案。
第一种方案,是在 Twemproxy 和业务访问端之间,再增加一组 LVS作为负载均衡层通过 LVS 负载均衡层,你可以方便得增加或减少 Twemproxy 实例,由 LVS 负责负载均衡和请求分发,如下图。
第二种方案,是将 Twemproxy 的 IP 列表加入 DNS。业务 client 通过域名来访问 Twemproxy每次建连时DNS 随机返回一个 IP让连接尽可能均衡。
第三种方案,是业务 client 自定义均衡策略。业务 client 从配置中心或 DNS 获取所有的Twemproxy 的 IP 列表,然后对这些 Twemproxy 进行均衡访问,从而达到负载均衡。
方案一,可以通过成熟的 LVS 方案,高效稳定的支持负载均衡策略,但多了一层,成本和运维的复杂度会有所增加。方案二,只能做到连接均衡,访问请求是否均衡,无法保障。方案三,成本最低,性能也比前面 2 个方案更高效。推荐使用方案三,微博内部也是采用第三种方案。
增加配置中心
对于 Twemproxy 配置的维护,可以通过增加一个配置中心服务来解决。将 YAML 配置文件中的所有配置信息,包括后端资源的部署信息、访问信息,以配置的方式存储到配置中心,如下图。
Twemproxy 启动时首先到配置中心订阅并拉取配置然后解析并正常启动。Twemproxy 将自己的 IP 和监听端口信息,也注册到配置中心。业务 client 从配置中心获取Twemproxy 的部署信息,然后进行均衡访问。
在后端资源变更时,直接更新配置中心的配置。配置中心会通知所有 Twemproxy 实例收到事件通知Twemproxy 即可拉取最新配置,并调整后端资源的访问,实现在线变更。整个过程自动完成,更加高效和可靠。
支持 M-S-L1 多层访问
前面提到,为了应对突发洪水流量,避免硬件局部故障的影响,对 Mc 访问采用了Master-Slave-L1 架构。可以将该缓存架构体系的访问策略,封装到 Twemproxy 内部。实现方案也比较简单。首先在 servers 配置中,增加 Master、Slave、L1 三层,如下图。
Twemproxy 启动时,每个 worker 进程预连所有的 Mc 后端,当收到 client 请求时,根据解析出来的指令,分别采用不同访问策略即可。
对于 get 请求,首先随机选择一个 L1 来访问,如果 miss继续访问 Master 和 Slave。中间在任何一层命中则回写。
对于 gets 请求,需要以 master 为准,从 master 读取。如果 master 获取失败,则从 slave获取获取后回种到 master然后再次从 master 获取,确保得到 cas unique id 来自 master。
对于 add/cas 等请求,首先请求 master成功后再将 key/value 通过 set 指令,写到 slave 和所有 L1。
对于 set 请求,最简单,直接 set 所有资源池即可。
对于 stats 指令的响应,由 Twemproxy 自己统计,或者到后端 Mc 获取后聚合获得。
Redis 主从访问
Redis 支持主从复制,为了支持更大并发访问量,同时减少主库的压力,一般会部署多个从库,写操作直接请求 Redis 主库,读操作随机选择一个 Redis 从库。这个逻辑同样可以封装在Twemproxy 中。如下图所示Redis 的主从配置信息,可以用域名的方式,也可以用 IP 端口的方式记录在配置中心,由 Twemproxy 订阅并实时更新,从而在 Redis 增减 slave、主从切换时及时对后端进行访问变更。
本课时,讲解了大数据时代下大中型互联网系统的特点,访问 Memcached 缓存时的经典问题及应对方案还讲解了如何通过分拆缓存池、Master-Slave 双层架构,来解决 Memcached 的容量问题、性能瓶颈、连接瓶颈、局部故障的问题,以及 Master-Slave-L1 三层架构,通过多层、多副本 Memcached 体系,来更好得解决突发洪峰流量和局部故障的问题。
本节课重点学习了基于 Twemproxy 的应用系统架构方案,学习了 Twemproxy 的系统架构和关键技术,学习了 Twemproxy 的部署及配置信息。最后还学习了如何扩展 Twemproxy从而使 Twemproxy 具有更好的性能、可用性和可运维性。
可以参考下面的思维导图,对这些知识点进行回顾和梳理。

View File

@ -0,0 +1,92 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 常用的缓存组件Redis是如何运行的
你好,我是你的缓存课老师陈波,欢迎进入第 16 课时“Redis 基本原理”的学习。
Redis 基本原理
Redis 简介
Redis 是一款基于 ANSI C 语言编写的BSD 许可的,日志型 key-value 存储组件,它的所有数据结构都存在内存中,可以用作缓存、数据库和消息中间件。
Redis 是 Remote dictionary server 即远程字典服务的缩写,一个 Redis 实例可以有多个存储数据的字典,客户端可以通过 select 来选择字典即 DB 进行数据存储。
Redis 特性
同为 key-value 存储组件Memcached 只能支持二进制字节块这一种数据类型。而 Redis 的数据类型却丰富的多,它具有 8 种核心数据类型每种数据类型都有一系列操作指令对应。Redis 性能很高,单线程压测可以达到 10~11w 的 QPS。
虽然 Redis 所有数据的读写操作都在内存中进行但也可以将所有数据进行落盘做持久化。Redis 提供了 2 种持久化方式。
快照方式,将某时刻所有数据都写入硬盘的 RDB 文件;
追加文件方式,即将所有写命令都以追加的方式写入硬盘的 AOF 文件中。
线上 Redis 一般会同时使用两种方式,通过开启 appendonly 及关联配置项,将写命令及时追加到 AOF 文件,同时在每日流量低峰时,通过 bgsave 保存当时所有内存数据快照。
对于互联网系统的线上流量,读操作远远大于写操作。以微博为例,读请求占总体流量的 90%左右。大量的读请求,通常会远超 Redis 的可承载范围。此时,可以使用 Redis 的复制特性,让一个 Redis 实例作为 master然后通过复制挂载多个不断同步更新的副本即多个 slave。通过读写分离把所有写操作落在 Redis 的 master所有读操作随机落在 Redis 的多个 slave 中,从而大幅提升 Redis 的读写能力。
Lua 是一个高效、简洁、易扩展的脚本语言可以方便的嵌入其他语言中使用。Redis 自 2.6 版本开始支持 Lua。通过支持 client 端自定义的 Lua 脚本Redis 可以减少网络开销,提升处理性能,还可以把脚本中的多个操作作为一个整体来操作,实现原子性更新。
Redis 还支持事务,在 multi 指令后,指定多个操作,然后通过 exec 指令一次性执行,中途如果出现异常,则不执行所有命令操作,否则,按顺序一次性执行所有操作,执行过程中不会执行任何其他指令。
Redis 还支持 Cluster 特性,可以通过自动或手动方式,将所有 key 按哈希分散到不同节点,在容量不足时,还可以通过 Redis 的迁移指令,把其中一部分 key 迁移到其他节点。
对于 Redis 的特性,可以通过这张思维导图,做个初步了解。在后面的课程中,我会逐一进行详细讲解。
作为缓存组件Redis 的最大优势是支持丰富的数据类型。目前Redis 支持 8 种核心数据类型,包括 string、list、set、sorted set、hash、bitmap、geo、hyperloglog。
Redis 的所有内存数据结构都存在全局的 dict 字典中dict 类似 Memcached 的 hashtable。Redis 的 dict 也有 2 个哈希表,插入新 key 时,一般用 0 号哈希表,随着 key 的插入或删除,当 0 号哈希表的 keys 数大于哈希表桶数,或 kyes 数小于哈希桶的 110 时,就对 hash 表进行扩缩。dict 中,哈希表解决冲突的方式,与 Memcached 相同,也是使用桶内单链表,来指向多个 hash 相同的 key/value 数据。
Redis 高性能
Redis 一般被看作单进程/单线程组件,因为 Redis 的网络 IO 和命令处理都在核心进程中由单线程处理。Redis 基于 Epoll 事件模型开发,可以进行非阻塞网络 IO同时由于单线程命令处理整个处理过程不存在竞争不需要加锁没有上下文切换开销所有数据操作都是在内存中操作所以 Redis 的性能很高,单个实例即可以达到 10w 级的 QPS。核心线程除了负责网络 IO 及命令处理外,还负责写数据到缓冲,以方便将最新写操作同步到 AOF、slave。
除了主进程Redis 还会 fork 一个子进程来进行重负荷任务的处理。Redis fork 子进程主要有 3 种场景。
收到 bgrewriteaof 命令时Redis 调用 fork构建一个子进程子进程往临时 AOF文件中写入重建数据库状态的所有命令当写入完毕子进程则通知父进程父进程把新增的写操作也追加到临时 AOF 文件,然后将临时文件替换老的 AOF 文件,并重命名。
收到 bgsave 命令时Redis 构建子进程,子进程将内存中的所有数据通过快照做一次持久化落地,写入到 RDB 中。
当需要进行全量复制时master 也会启动一个子进程,子进程将数据库快照保存到 RDB 文件,在写完 RDB 快照文件后master 就会把 RDB 发给 slave同时将后续新的写指令都同步给 slave。
主进程中,除了主线程处理网络 IO 和命令操作外,还有 3 个辅助 BIO 线程。这 3 个 BIO 线程分别负责处理文件关闭、AOF 缓冲数据刷新到磁盘,以及清理对象这三个任务队列。
Redis 在启动时,会同时启动这三个 BIO 线程,然后 BIO 线程休眠等待任务。当需要执行相关类型的后台任务时,就会构建一个 bio_job 结构,记录任务参数,然后将 bio_job 追加到任务队列尾部。然后唤醒 BIO 线程,即可进行任务执行。
Redis 持久化
Redis 的持久化是通过 RDB 和 AOF 文件进行的。RDB 只记录某个时间点的快照,可以通过设置指定时间内修改 keys 数的阀值,超过则自动构建 RDB 内容快照不过线上运维一般会选择在业务低峰期定期进行。RDB 存储的是构建时刻的数据快照,内存数据一旦落地,不会理会后续的变更。而 AOF记录是构建整个数据库内容的命令它会随着新的写操作不断进行追加操作。由于不断追加AOF 会记录数据大量的中间状态AOF 文件会变得非常大,此时,可以通过 bgrewriteaof 指令,对 AOF 进行重写,只保留数据的最后内容,来大大缩减 AOF 的内容。
为了提升系统的可扩展性提升读操作的支撑能力Redis 支持 master-slave 的复制功能。当 Redis 的 slave 部署并设置完毕后slave 会和 master 建立连接,进行全量同步。
第一次建立连接,或者长时间断开连接后,缺失的指令超过 master 复制缓冲区的大小都需要先进行一次全量同步。全量同步时master 会启动一个子进程,将数据库快照保存到文件中,然后将这个快照文件发给 slave同时将快照之后的写指令也同步给 slave。
全量同步完成后,如果 slave 短时间中断,然后重连复制,缺少的写指令长度小于 master 的复制缓冲大小master 就会把 slave 缺失的内容全部发送给 slave进行增量复制。
Redis 的 master 可以挂载多个 slave同时 slave 还可以继续挂载 slave通过这种方式可以有效减轻 master 的压力,同时在 master 挂掉后,可以在 slave 通过 slaveof no one 指令,使当前 slave 停止与 master 的同步,转而成为新的 master。
Redis 集群管理
Redis 的集群管理有 3 种方式。
client 分片访问client 对 key 做 hash然后按取模或一致性 hash把 key 的读写分散到不同的 Redis 实例上。
在 Redis 前加一个 proxy把路由策略、后端 Redis 状态维护的工作都放到 proxy 中进行client 直接访问 proxy后端 Redis 变更,只需修改 proxy 配置即可。
直接使用 Redis cluster。Redis 创建之初,使用方直接给 Redis 的节点分配 slot后续访问时对 key 做 hash 找到对应的 slot然后访问 slot 所在的 Redis 实例。在需要扩容缩容时,可以在线通过 cluster setslot 指令,以及 migrate 指令,将 slot 下所有 key 迁移到目标节点,即可实现扩缩容的目的。
至此Redis 的基本原理就讲完了,相信你对 Redis 应该有了一个大概的了解。接下来,我将开始逐一深入分析 Redis 的各个技术细节。

View File

@ -0,0 +1,148 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 如何理解、选择并使用Redis的核心数据类型
你好,我是你的缓存课老师陈波,欢迎进入第 17 课时“Redis 数据类型”的学习。
Redis 数据类型
首先,来看一下 Redis 的核心数据类型。Redis 有 8 种核心数据类型,分别是
string 字符串类型;
list 列表类型;
set 集合类型;
sorted set 有序集合类型;
hash 类型;
bitmap 位图类型;
geo 地理位置类型;
HyperLogLog 基数统计类型。
string 字符串
string 是 Redis 的最基本数据类型。可以把它理解为 Mc 中 key 对应的 value 类型。string 类型是二进制安全的,即 string 中可以包含任何数据。
Redis 中的普通 string 采用 raw encoding 即原始编码方式,该编码方式会动态扩容,并通过提前预分配冗余空间,来减少内存频繁分配的开销。
在字符串长度小于 1MB 时,按所需长度的 2 倍来分配,超过 1MB则按照每次额外增加 1MB 的容量来预分配。
Redis 中的数字也存为 string 类型,但编码方式跟普通 string 不同,数字采用整型编码,字符串内容直接设为整数值的二进制字节序列。
在存储普通字符串,序列化对象,以及计数器等场景时,都可以使用 Redis 的字符串类型,字符串数据类型对应使用的指令包括 set、get、mset、incr、decr 等。
list 列表
Redis 的 list 列表,是一个快速双向链表,存储了一系列的 string 类型的字串值。list 中的元素按照插入顺序排列。插入元素的方式,可以通过 lpush 将一个或多个元素插入到列表的头部,也可以通过 rpush 将一个或多个元素插入到队列尾部,还可以通过 lset、linsert 将元素插入到指定位置或指定元素的前后。
list 列表的获取,可以通过 lpop、rpop 从对头或队尾弹出元素,如果队列为空,则返回 nil。还可以通过 Blpop、Brpop 从队头/队尾阻塞式弹出元素,如果 list 列表为空,没有元素可供弹出,则持续阻塞,直到有其他 client 插入新的元素。这里阻塞弹出元素可以设置过期时间避免无限期等待。最后list 列表还可以通过 LrangeR 获取队列内指定范围内的所有元素。Redis 中list 列表的偏移位置都是基于 0 的下标,即列表第一个元素的下标是 0第二个是 1。偏移量也可以是负数倒数第一个是 -1倒数第二个是 -2依次类推。
list 列表,对于常规的 pop、push 元素,性能很高,时间复杂度为 O(1),因为是列表直接追加或弹出。但对于通过随机插入、随机删除,以及随机范围获取,需要轮询列表确定位置,性能就比较低下了。
feed timeline 存储时,由于 feed id 一般是递增的,可以直接存为 list用户发表新 feed就直接追加到队尾。另外消息队列、热门 feed 等业务场景,都可以使用 list 数据结构。
操作 list 列表时,可以用 lpush、lpop、rpush、rpop、lrange 来进行常规的队列进出及范围获取操作,在某些特殊场景下,也可以用 lset、linsert 进行随机插入操作,用 lrem 进行指定元素删除操作;最后,在消息列表的消费时,还可以用 Blpop、Brpop 进行阻塞式获取,从而在列表暂时没有元素时,可以安静的等待新元素的插入,而不需要额外持续的查询。
set 集合
set 是 string 类型的无序集合set 中的元素是唯一的,即 set 中不会出现重复的元素。Redis 中的集合一般是通过 dict 哈希表实现的,所以插入、删除,以及查询元素,可以根据元素 hash 值直接定位,时间复杂度为 O(1)。
对 set 类型数据的操作,除了常规的添加、删除、查找元素外,还可以用以下指令对 set 进行操作。
sismember 指令判断该 key 对应的 set 数据结构中,是否存在某个元素,如果存在返回 1否则返回 0
sdiff 指令来对多个 set 集合执行差集;
sinter 指令对多个集合执行交集;
sunion 指令对多个集合执行并集;
spop 指令弹出一个随机元素;
srandmember 指令返回一个或多个随机元素。
set 集合的特点是查找、插入、删除特别高效,时间复杂度为 O(1),所以在社交系统中,可以用于存储关注的好友列表,用来判断是否关注,还可以用来做好友推荐使用。另外,还可以利用 set 的唯一性,来对服务的来源业务、来源 IP 进行精确统计。
sorted set 有序集合
Redis 中的 sorted set 有序集合也称为 zset有序集合同 set 集合类似,也是 string 类型元素的集合,且所有元素不允许重复。
但有序集合中,每个元素都会关联一个 double 类型的 score 分数值。有序集合通过这个 score 值进行由小到大的排序。有序集合中,元素不允许重复,但 score 分数值却允许重复。
有序集合除了常规的添加、删除、查找元素外,还可以通过以下指令对 sorted set 进行操作。
zscan 指令:按顺序获取有序集合中的元素;
zscore 指令:获取元素的 score 值;
zrange指令通过指定 score 返回指定 score 范围内的元素;
在某个元素的 score 值发生变更时,还可以通过 zincrby 指令对该元素的 score 值进行加减。
通过 zinterstore、zunionstore 指令对多个有序集合进行取交集和并集,然后将新的有序集合存到一个新的 key 中,如果有重复元素,重复元素的 score 进行相加,然后作为新集合中该元素的 score 值。
sorted set 有序集合的特点是:
所有元素按 score 排序,而且不重复;
查找、插入、删除非常高效,时间复杂度为 O(1)。
因此,可以用有序集合来统计排行榜,实时刷新榜单,还可以用来记录学生成绩,从而轻松获取某个成绩范围内的学生名单,还可以用来对系统统计增加权重值,从而在 dashboard 实时展示。
hash 哈希
Redis 中的哈希实际是 field 和 value 的一个映射表。
hash 数据结构的特点是在单个 key 对应的哈希结构内部,可以记录多个键值对,即 field 和 value 对value 可以是任何字符串。而且这些键值对查询和修改很高效。
所以可以用 hash 来存储具有多个元素的复杂对象然后分别修改或获取这些元素。hash 结构中的一些重要指令包括hmset、hmget、hexists、hgetall、hincrby 等。
hmset 指令批量插入多个 field、value 映射;
hmget 指令获取多个 field 对应的 value 值;
hexists 指令判断某个 field 是否存在;
如果 field 对应的 value 是整数,还可以用 hincrby 来对该 value 进行修改。
bitmap 位图
Redis 中的 bitmap 位图是一串连续的二进制数字,底层实际是基于 string 进行封装存储的,按 bit 位进行指令操作的。bitmap 中每一 bit 位所在的位置就是 offset 偏移,可以用 setbit、bitfield 对 bitmap 中每个 bit 进行置 0 或置 1 操作,也可以用 bitcount 来统计 bitmap 中的被置 1 的 bit 数,还可以用 bitop 来对多个 bitmap 进行求与、或、异或等操作。
bitmap 位图的特点是按位设置、求与、求或等操作很高效,而且存储成本非常低,用来存对象标签属性的话,一个 bit 即可存一个标签。可以用 bitmap存用户最近 N 天的登录情况,每天用 1 bit登录则置 1。个性推荐在社交应用中非常重要可以对新闻、feed 设置一系列标签,如军事、娱乐、视频、图片、文字等,用 bitmap 来存储这些标签,在对应标签 bit 位上置 1。对用户也可以采用类似方式记录用户的多种属性并可以很方便的根据标签来进行多维度统计。bitmap 位图的重要指令包括setbit、 getbit、bitcount、bitfield、 bitop、bitpos 等。
在移动社交时代LBS 应用越来越多,比如微信、陌陌中附近的人,美团、大众点评中附近的美食、电影院,滴滴、优步中附近的专车等。要实现这些功能,就得使用地理位置信息进行搜索。地球的地理位置是使用二维的经纬度进行表示的,我们只要确定一个点的经纬度,就可以确认它在地球的位置。
Redis 在 3.2 版本之后增加了对 GEO 地理位置的处理功能。Redis 的 GEO 地理位置本质上是基于 sorted set 封装实现的。在存储分类 key 下的地理位置信息时,需要对该分类 key 构建一个 sorted set 作为内部存储结构,用于存储一系列位置点。
在存储某个位置点时,首先利用 Geohash 算法,将该位置二维的经纬度,映射编码成一维的 52 位整数值,将位置名称、经纬度编码 score 作为键值对,存储到分类 key 对应的 sorted set 中。
需要计算某个位置点 A 附近的人时,首先以指定位置 A 为中心点,以距离作为半径,算出 GEO 哈希 8 个方位的范围, 然后依次轮询方位范围内的所有位置点,只要这些位置点到中心位置 A 的距离在要求距离范围内,就是目标位置点。轮询完所有范围内的位置点后,重新排序即得到位置点 A 附近的所有目标。
使用 geoadd将位置名称如人、车辆、店名与对应的地理位置信息添加到指定的位置分类 key 中;
使用 geopos 方便地查询某个名称所在的位置信息;
使用 georadius 获取指定位置附近,不超过指定距离的所有元素;
使用 geodist 来获取指定的两个位置之间的距离。
这样,是不是就可以实现,找到附近的餐厅,算出当前位置到对应餐厅的距离,这样的功能了?
Redis GEO 地理位置,利用 Geohash 将大量的二维经纬度转一维的整数值,这样可以方便的对地理位置进行查询、距离测量、范围搜索。但由于地理位置点非常多,一个地理分类 key 下可能会有大量元素,在 GEO 设计时,需要提前进行规划,避免单 key 过度膨胀。
Redis 的 GEO 地理位置数据结构应用场景很多比如查询某个地方的具体位置查当前位置到目的地的距离查附近的人、餐厅、电影院等。GEO 地理位置数据结构中,重要指令包括 geoadd、geopos、geodist、georadius、georadiusbymember 等。
hyperLogLog 基数统计
Redis 的 hyperLogLog 是用来做基数统计的数据类型当输入巨大数量的元素做统计时只需要很小的内存即可完成。HyperLogLog 不保存元数据,只记录待统计元素的估算数量,这个估算数量是一个带有 0.81% 标准差的近似值,在大多数业务场景,对海量数据,不足 1% 的误差是可以接受的。
Redis 的 HyperLogLog 在统计时如果计数数量不大采用稀疏矩阵存储随着计数的增加稀疏矩阵占用的空间也会逐渐增加当超过阀值后则改为稠密矩阵稠密矩阵占用的空间是固定的约为12KB字节。
通过 hyperLoglog 数据类型,你可以利用 pfadd 向基数统计中增加新的元素,可以用 pfcount 获得 hyperLogLog 结构中存储的近似基数数量,还可以用 hypermerge 将多个 hyperLogLog 合并为一个 hyperLogLog 结构,从而可以方便的获取合并后的基数数量。
hyperLogLog 的特点是统计过程不记录独立元素,占用内存非常少,非常适合统计海量数据。在大中型系统中,统计每日、每月的 UV 即独立访客数,或者统计海量用户搜索的独立词条数,都可以用 hyperLogLog 数据类型来进行处理。

View File

@ -0,0 +1,190 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
22 怎么认识和应用Redis内部数据结构
上一课时,我们学习了 Redis 协议解析及处理,接下来,看一下 Redis 的内部数据结构是什么样的?
Redis 内部数据结构
RdeisDb
Redis 中所有数据都保存在 DB 中,一个 Redis 默认最多支持 16 个 DB。Redis 中的每个 DB 都对应一个 redisDb 结构,即每个 Redis 实例,默认有 16 个 redisDb。用户访问时默认使用的是 0 号 DB可以通过 select $dbID 在不同 DB 之间切换。
redisDb 主要包括 2 个核心 dict 字典、3 个非核心 dict 字典、dbID 和其他辅助属性。2 个核心 dict 包括一个 dict 主字典和一个 expires 过期字典。主 dict 字典用来存储当前 DB 中的所有数据,它将 key 和各种数据类型的 value 关联起来,该 dict 也称 key space。过期字典用来存储过期时间 key存的是 key 与过期时间的映射。日常的数据存储和访问基本都会访问到 redisDb 中的这两个 dict。
3 个非核心 dict 包括一个字段名叫 blocking_keys 的阻塞 dict一个字段名叫 ready_keys 的解除阻塞 dict还有一个是字段名叫 watched_keys 的 watch 监控 dict。
在执行 Redis 中 list 的阻塞命令 blpop、brpop 或者 brpoplpush 时,如果对应的 list 列表为空Redis 就会将对应的 client 设为阻塞状态,同时将该 client 添加到 DB 中 blocking_keys 这个阻塞 dict。所以该 dict 存储的是处于阻塞状态的 key 及 client 列表。
当有其他调用方在向某个 key 对应的 list 中增加元素时Redis 会检测是否有 client 阻塞在这个 key 上,即检查 blocking_keys 中是否包含这个 key如果有则会将这个 key 加入 read_keys 这个 dict 中。同时也会将这个 key 保存到 server 中的一个名叫 read_keys 的列表中。这样可以高效、不重复的插入及轮询。
当 client 使用 watch 指令来监控 key 时,这个 key 和 client 就会被保存到 watched_keys 这个 dict 中。redisDb 中可以保存所有的数据类型,而 Redis 中所有数据类型都是存放在一个叫 redisObject 的结构中。
redisObject
redisObject 由 5 个字段组成。
type即 Redis 对象的数据类型,目前支持 7 种 type 类型,分别为
OBJ_STRING
OBJ_LIST
OBJ_SET
OBJ_ZSET
OBJ_HASH
OBJ_MODULE
OBJ_STREAM
encodingRedis 对象的内部编码方式,即内部数据结构类型,目前支持 10 种编码方式包括
OBJ_ENCODING_RAW
OBJ_ENCODING_INT
OBJ_ENCODING_HT
OBJ_ENCODING_ZIPLIST 等。
LRU存储的是淘汰数据用的 LRU 时间或 LFU 频率及时间的数据。
refcount记录 Redis 对象的引用计数,用来表示对象被共享的次数,共享使用时加 1不再使用时减 1当计数为 0 时表明该对象没有被使用,就会被释放,回收内存。
ptr它指向对象的内部数据结构。比如一个代表 string 的对象,它的 ptr 可能指向一个 sds 或者一个 long 型整数。
dict
前面讲到Redis 中的数据实际是存在 DB 中的 2 个核心 dict 字典中的。实际上 dict 也是 Redis 的一种使用广泛的内部数据结构。
Redis 中的 dict类似于 Memcached 中 hashtable。都可以用于 key 或元素的快速插入、更新和定位。dict 字典中,有一个长度为 2 的哈希表数组,日常访问用 0 号哈希表,如果 0 号哈希表元素过多,则分配一个 2 倍 0 号哈希表大小的空间给 1 号哈希表然后进行逐步迁移rehashidx 这个字段就是专门用来做标志迁移位置的。在哈希表操作中,采用单向链表来解决 hash 冲突问题。dict 中还有一个重要字段是 type它用于保存 hash 函数及 key/value 赋值、比较函数。
dictht 中的 table 是一个 hash 表数组,每个桶指向一个 dictEntry 结构。dictht 采用 dictEntry 的单向链表来解决 hash 冲突问题。
dictht 是以 dictEntry 来存 key-value 映射的。其中 key 是 sds 字符串value 为存储各种数据类型的 redisObject 结构。
dict 可以被 redisDb 用来存储数据 key-value 及命令操作的辅助信息。还可以用来作为一些 Redis 数据类型的内部数据结构。dict 可以作为 set 集合的内部数据结构。在哈希的元素数超过 512 个,或者哈希中 value 大于 64 字节dict 还被用作为哈希类型的内部数据结构。
sds
字符串是 Redis 中最常见的数据类型,其底层实现是简单动态字符串即 sds。简单动态字符串本质是一个 char*,内部通过 sdshdr 进行管理。sdshdr 有 4 个字段。len 为字符串实际长度alloc 当前字节数组总共分配的内存大小。flags 记录当前字节数组的属性buf 是存储字符串真正的值及末尾一个 \0。
sds 的存储 buf 可以动态扩展或收缩,字符串长度不用遍历,可直接获得,修改和访问都很方便。由于 sds 中字符串存在 buf 数组中,长度由 len 定义,而不像传统字符串遇 0 停止,所以 sds 是二进制安全的,可以存放任何二进制的数据。
简单动态字符串 sds 的获取字符串长度很方便,通过 len 可以直接得到,而传统字符串需要对字符串进行遍历,时间复杂度为 O(n)。
sds 相比传统字符串多了一个 sdshdr对于大量很短的字符串这个 sdshdr 还是一个不小的开销。在 3.2 版本后sds 会根据字符串实际的长度,选择不同的数据结构,以更好的提升内存效率。当前 sdshdr 结构分为 5 种子类型,分别为 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。其中 sdshdr5 只有 flags 和 buf 字段,其他几种类型的 len 和 alloc 采用从 uint8_t 到 uint64_t 的不同类型,以节省内存空间。
sds 可以作为字符串的内部数据结构,同时 sds 也是 hyperloglog、bitmap 类型的内部数据结构。
ziplist
为了节约内存并减少内存碎片Redis 设计了 ziplist 压缩列表内部数据结构。压缩列表是一块连续的内存空间,可以连续存储多个元素,没有冗余空间,是一种连续内存数据块组成的顺序型内存结构。
ziplist 的结构如图所示,主要包括 5 个部分。
zlbytes 是压缩列表所占用的总内存字节数。
Zltail 尾节点到起始位置的字节数。
Zllen 总共包含的节点/内存块数。
Entry 是 ziplist 保存的各个数据节点,这些数据点长度随意。
Zlend 是一个魔数 255用来标记压缩列表的结束。
如图所示,一个包含 4 个元素的 ziplist总占用字节是 100bytes该 ziplist 的起始元素的指针是 pzltail 是 80则第 4 个元素的指针是 P+80。
压缩列表 ziplist 的存储节点 entry 的结构如图,主要有 6 个字段。
prevRawLen 是前置节点的长度;
preRawLenSize 编码 preRawLen 需要的字节数;
len 当前节点的长度;
lensize 编码 len 所需要的字节数;
encoding 当前节点所用的编码类型;
entryData 当前节点数据。
由于 ziplist 是连续紧凑存储,没有冗余空间,所以插入新的元素需要 realloc 扩展内存,所以如果 ziplist 占用空间太大realloc 重新分配内存和拷贝的开销就会很大,所以 ziplist 不适合存储过多元素,也不适合存储过大的字符串。
因此只有在元素数和 value 数都不大的时候ziplist 才作为 hash 和 zset 的内部数据结构。其中 hash 使用 ziplist 作为内部数据结构的限制时,元素数默认不超过 512 个value 值默认不超过 64 字节。可以通过修改配置来调整 hash_max_ziplist_entries 、hash_max_ziplist_value 这两个阀值的大小。
zset 有序集合,使用 ziplist 作为内部数据结构的限制元素数默认不超过 128 个value 值默认不超过 64 字节。可以通过修改配置来调整 zset_max_ziplist_entries 和 zset_max_ziplist_value 这两个阀值的大小。
quicklist
Redis 在 3.2 版本之后引入 quicklist用以替换 linkedlist。因为 linkedlist 每个节点有前后指针,要占用 16 字节,而且每个节点独立分配内存,很容易加剧内存的碎片化。而 ziplist 由于紧凑型存储,增加元素需要 realloc删除元素需要内存拷贝天然不适合元素太多、value 太大的存储。
而 quicklist 快速列表应运而生,它是一个基于 ziplist 的双向链表。将数据分段存储到 ziplist然后将这些 ziplist 用双向指针连接。快速列表的结构如图所示。
head、tail 是两个指向第一个和最后一个 ziplist 节点的指针。
count 是 quicklist 中所有的元素个数。
len 是 ziplist 节点的个数。
compress 是 LZF 算法的压缩深度。
快速列表中,管理 ziplist 的是 quicklistNode 结构。quicklistNode 主要包含一个 prev/next 双向指针,以及一个 ziplist 节点。单个 ziplist 节点可以存放多个元素。
快速列表从头尾读写数据很快,时间复杂度为 O(1)。也支持从中间任意位置插入或读写元素,但速度较慢,时间复杂度为 O(n)。快速列表当前主要作为 list 列表的内部数据结构。
zskiplist
跳跃表 zskiplist 是一种有序数据结构,它通过在每个节点维持多个指向其他节点的指针,从而可以加速访问。跳跃表支持平均 O(logN) 和最差 O(n) 复杂度的节点查找。在大部分场景,跳跃表的效率和平衡树接近,但跳跃表的实现比平衡树要简单,所以不少程序都用跳跃表来替换平衡树。
如果 sorted set 类型的元素数比较多或者元素比较大Redis 就会选择跳跃表来作为 sorted set有序集合的内部数据结构。
跳跃表主要由 zskipList 和节点 zskiplistNode 构成。zskiplist 结构如图header 指向跳跃表的表头节点。tail 指向跳跃表的表尾节点。length 表示跳跃表的长度它是跳跃表中不包含表头节点的节点数量。level 是目前跳跃表内,除表头节点外的所有节点中,层数最大的那个节点的层数。
跳跃表的节点 zskiplistNode 的结构如图所示。ele 是节点对应的 sds 值,在 zset 有序集合中就是集合中的 field 元素。score 是节点的分数,通过 score跳跃表中的节点自小到大依次排列。backward 是指向当前节点的前一个节点的指针。level 是节点中的层,每个节点一般有多个层。每个 level 层都带有两个属性,一个是 forwad 前进指针,它用于指向表尾方向的节点;另外一个是 span 跨度,它是指 forward 指向的节点到当前节点的距离。
如图所示是一个跳跃表,它有 3 个节点。对应的元素值分别是 S1、S2 和 S3分数值依次为 1.0、3.0 和 5.0。其中 S3 节点的 level 最大是 5跳跃表的 level 是 5。header 指向表头节点tail 指向表尾节点。在查到元素时,累加路径上的跨度即得到元素位置。在跳跃表中,元素必须是唯一的,但 score 可以相同。相同 score 的不同元素,按照字典序进行排序。
在 sorted set 数据类型中,如果元素数较多或元素长度较大,则使用跳跃表作为内部数据结构。默认元素数超过 128 或者最大元素的长度超过 64此时有序集合就采用 zskiplist 进行存储。由于 geo 也采用有序集合类型来存储地理位置名称和位置 hash 值,所以在超过相同阀值后,也采用跳跃表进行存储。
Redis 主要的内部数据结构讲完了,接下来整体看一下,之前讲的 8 种数据类型,具体都是采用哪种内部数据结构来存储的。
首先,对于 string 字符串Redis 主要采用 sds 来进行存储。而对于 list 列表Redis 采用 quicklist 进行存储。对于 set 集合类型Redis 采用 dict 来进行存储。对于 sorted set 有序集合类型,如果元素数小于 128 且元素长度小于 64则使用 ziplist 存储,否则使用 zskiplist 存储。对于哈希类型,如果元素数小于 512并且元素长度小于 64则用 ziplist 存储,否则使用 dict 字典存储。对于 hyperloglog采用 sds 简单动态字符串存储。对于 geo如果位置数小于 128则使用 ziplist 存储,否则使用 zskiplist 存储。最后对于 bitmap采用 sds 简单动态字符串存储。
除了这些主要的内部数据结构,还有在特殊场景下也会采用一些其他内部结构存储,比如,如果操作的字符串都是整数,同时指令是 incr、decr 等,会对字符串采用 long 型整数存储,这些场景比较特殊,限于时间关系,这里不做进一步阐述。

View File

@ -0,0 +1,59 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
23 Redis是如何淘汰key的
你好,我是你的缓存课老师陈波,欢迎进入第 23 课时“Redis 淘汰策略”的学习。本课时我们主要学习 Redis 淘汰原理、淘汰方式、以及 8 种淘汰策略等内容。
淘汰原理
首先我们来学习 Redis 的淘汰原理。
系统线上运行中,内存总是昂贵且有限的,在数据总量远大于 Redis 可用的内存总量时为了最大限度的提升访问性能Redis 中只能存放最新最热的有效数据。
当 key 过期后,或者 Redis 实际占用的内存超过阀值后Redis 就会对 key 进行淘汰,删除过期的或者不活跃的 key回收其内存供新的 key 使用。Redis 的内存阀值是通过 maxmemory 设置的,而超过内存阀值后的淘汰策略,是通过 maxmemory-policy 设置的具体的淘汰策略后面会进行详细介绍。Redis 会在 2 种场景下对 key 进行淘汰,第一种是在定期执行 serverCron 时,检查淘汰 key第二种是在执行命令时检查淘汰 key。
第一种场景Redis 定期执行 serverCron 时,会对 DB 进行检测,清理过期 key。清理流程如下。首先轮询每个 DB检查其 expire dict即带过期时间的过期 key 字典,从所有带过期时间的 key 中,随机选取 20 个样本 key检查这些 key 是否过期,如果过期则清理删除。如果 20 个样本中,超过 5 个 key 都过期,即过期比例大于 25%,就继续从该 DB 的 expire dict 过期字典中,再随机取样 20 个 key 进行过期清理,持续循环,直到选择的 20 个样本 key 中,过期的 key 数小于等于 5当前这个 DB 则清理完毕,然后继续轮询下一个 DB。
在执行 serverCron 时,如果在某个 DB 中,过期 dict 的填充率低于 1%,则放弃对该 DB 的取样检查,因为效率太低。如果 DB 的过期 dict 中,过期 key 太多,一直持续循环回收,会占用大量主线程时间,所以 Redis 还设置了一个过期时间。这个过期时间根据 serverCron 的执行频率来计算5.0 版本及之前采用慢循环过期策略,默认是 25ms如果回收超过 25ms 则停止6.0 非稳定版本采用快循环策略,过期时间为 1ms。
第二种场景Redis 在执行命令请求时。会检查当前内存占用是否超过 maxmemory 的数值,如果超过,则按照设置的淘汰策略,进行删除淘汰 key 操作。
淘汰方式
Redis 中 key 的淘汰方式有两种,分别是同步删除淘汰和异步删除淘汰。在 serverCron 定期清理过期 key 时,如果设置了延迟过期配置 lazyfree-lazy-expire会检查 key 对应的 value 是否为多元素的复合类型,即是否是 list 列表、set 集合、zset 有序集合和 hash 中的一种,并且 value 的元素数大于 64则在将 key 从 DB 中 expire dict 过期字典和主 dict 中删除后value 存放到 BIO 任务队列,由 BIO 延迟删除线程异步回收;否则,直接从 DB 的 expire dict 和主 dict 中删除,并回收 key、value 所占用的空间。在执行命令时,如果设置了 lazyfree-lazy-eviction在淘汰 key 时,也采用前面类似的检测方法,对于元素数大于 64 的 4 种复合类型,使用 BIO 线程异步删除,否则采用同步直接删除。
淘汰策略
Redis 提供了 8 种淘汰策略对 key 进行管理,而且还引入基于样本的 eviction pool来提升剔除的准确性确保 在保持最大性能 的前提下,剔除最不活跃的 key。eviction pool 主要对 LRU、LFU以及过期 dict ttl 内存管理策略 生效。处理流程为,当 Redis 内存占用超过阀值后,按策略从主 dict 或者带过期时间的 expire dict 中随机选择 N 个 keyN 默认是 5计算每个 key 的 idle 值,按 idle 值从小到大的顺序插入 evictionPool 中,然后选择 idle 最大的那个 key进行淘汰。
选择淘汰策略时,可以通过配置 Redis 的 maxmemory 设置最大内存,并通 maxmemory_policy 设置超过最大内存后的处理策略。如果 maxmemory 设为 0则表明对内存使用没有任何限制可以持续存放数据适合作为存储来存放数据量较小的业务。如果数据量较大就需要估算热数据容量设置一个适当的值将 Redis 作为一个缓存而非存储来使用。
Redis 提供了 8 种 maxmemory_policy 淘汰策略来应对内存超过阀值的情况。
第一种淘汰策略是 noeviction它是 Redis 的默认策略。在内存超过阀值后Redis 不做任何清理工作然后对所有写操作返回错误但对读请求正常处理。noeviction 适合数据量不大的业务场景,将关键数据存入 Redis 中,将 Redis 当作 DB 来使用。
第二种淘汰策略是 volatile-lru它对带过期时间的 key 采用最近最少访问算法来淘汰。使用这种策略Redis 会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key计算 key 的空闲时间,然后插入 evictionPool 中,最后选择空闲时间最久的 key 进行淘汰。这种策略适合的业务场景是需要淘汰的key带有过期时间且有冷热区分从而可以淘汰最久没有访问的key。
第三种策略是 volatile-lfu它对带过期时间的 key 采用最近最不经常使用的算法来淘汰。使用这种策略时Redis 会从 redisDb 中的 expire dict 过期字典中,首先随机选择 N 个 key然后根据其 value 的 lru 值,计算 key 在一段时间内的使用频率相对值。对于 lfu要选择使用频率最小的 key为了沿用 evictionPool 的 idle 概念Redis 在计算 lfu 的 Idle 时,采用 255 减去使用频率相对值,从而确保 Idle 最大的 key 是使用次数最小的 key计算 N 个 key 的 Idle 值后,插入 evictionPool最后选择 Idle 最大,即使用频率最小的 key进行淘汰。这种策略也适合大多数 key 带过期时间且有冷热区分的业务场景。
第四种策略是 volatile-ttl它是对带过期时间的 key 中选择最早要过期的 key 进行淘汰。使用这种策略时Redis 也会从 redisDb 的 expire dict 过期字典中,首先随机选择 N 个 key然后用最大无符号 long 值减去 key 的过期时间来作为 Idle 值,计算 N 个 key 的 Idle 值后插入evictionPool最后选择 Idle 最大,即最快就要过期的 key进行淘汰。这种策略适合需要淘汰的key带过期时间且有按时间冷热区分的业务场景。
第五种策略是 volatile-random它是对带过期时间的 key 中随机选择 key 进行淘汰。使用这种策略时Redis 从 redisDb 的 expire dict 过期字典中,随机选择一个 key然后进行淘汰。如果需要淘汰的key有过期时间没有明显热点主要被随机访问那就适合选择这种淘汰策略。
第六种策略是 allkey-lru它是对所有 key而非仅仅带过期时间的 key采用最近最久没有使用的算法来淘汰。这种策略与 volatile-lru 类似,都是从随机选择的 key 中,选择最长时间没有被访问的 key 进行淘汰。区别在于volatile-lru 是从 redisDb 中的 expire dict 过期字典中选择 key而 allkey-lru 是从所有的 key 中选择 key。这种策略适合需要对所有 key 进行淘汰,且数据有冷热读写区分的业务场景。
第七种策略是 allkeys-lfu它也是针对所有 key 采用最近最不经常使用的算法来淘汰。这种策略与 volatile-lfu 类似,都是在随机选择的 key 中,选择访问频率最小的 key 进行淘汰。区别在于volatile-flu从expire dict 过期字典中选择 key而 allkeys-lfu 是从主 dict 中选择 key。这种策略适合的场景是需要从所有的 key 中进行淘汰,但数据有冷热区分,且越热的数据访问频率越高。
最后一种策略是 allkeys-random它是针对所有 key 进行随机算法进行淘汰。它也是从主 dict 中随机选择 key然后进行删除回收。如果需要从所有的 key 中进行淘汰,并且 key 的访问没有明显热点,被随机访问,即可采用这种策略。

View File

@ -0,0 +1,92 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
24 Redis崩溃后如何进行数据恢复的
你好,我是你的缓存课老师陈波,欢迎来到第 24 课时“Redis 崩溃后,如何进行数据恢复”的学习。本课时我们主要学习通过 RDB、AOF、混合存储等数据持久化方案来解决如何进行数据恢复的问题。
Redis 持久化是一个将内存数据转储到磁盘的过程。Redis 目前支持 RDB、AOF以及混合存储三种模式。
RDB
Redis 的 RDB 持久化是以快照的方式将内存数据存储到磁盘。在需要进行 RDB 持久化时Redis 会将内存中的所有数据以二进制的格式落地每条数据存储的内容包括过期时间、数据类型、key以及 value。当 Redis 重启时,如果 appendonly 关闭,则会读取 RDB 持久化生成的二进制文件进行数据恢复。
触发构建 RDB 的场景主要有以下四种。
第一种场景是通过 save 或 bgsave 命令进行主动 RDB 快照构建。它是由调用方调用 save 或 bgsave 指令进行触发的。
第二种场景是利用配置 save m n 来进行自动快照生成。它是指在 m 秒中,如果插入或变更 n 个 key则自动触发 bgsave。这个配置可以设置多个配置行以便组合使用。由于峰值期间Redis 的压力大,变更的 key 也比较多,如果再进行构建 RDB 的操作,会进一步增加机器负担,对调用方请求会有一定的影响,所以线上使用时需要谨慎。
第三种场景是主从复制,如果从库需要进行全量复制,此时主库也会进行 bgsave 生成一个 RDB 快照。
第四种场景是在运维执行 flushall 清空所有数据,或执行 shutdown 关闭服务时,也会触发 Redis 自动构建 RDB 快照。
save 是在主进程中进行 RDB 持久化的,持久化期间 Redis 处于阻塞状态,不处理任何客户请求,所以一般使用较少。而 bgsave 是 fork 一个子进程,然后在子进程中构建 RDB 快照,构建快照的过程不直接影响用户的访问,但仍然会增加机器负载。线上 Redis 快照备份,一般会选择凌晨低峰时段,通过 bgsave 主动触发进行备份。
RDB 快照文件主要由 3 部分组成。
第一部分是 RDB 头部,主要包括 RDB 的版本,以及 Redis 版本、创建日期、占用内存等辅助信息。
第二部分是各个 RedisDB 的数据。存储每个 RedisDB 时,会首先记录当前 RedisDB 的DBID然后记录主 dict 和 expire dict 的记录数量,最后再轮询存储每条数据记录。存储数据记录时,如果数据有过期时间,首先记录过期时间。如果 Redis 的 maxmemory_policy 过期策略采用 LRU 或者 LFU还会将 key 对应的 LRU、LFU 值进行落地最后记录数据的类型、key以及 value。
第三部部分是 RDB 的尾部。RDB 尾部,首先存储 Redis 中的 Lua 脚本等辅助信息。然后存储 EOF 标记,即值为 255 的字符。最后存 RDB 的 cksum。
至此RDB 就落地完毕。
RDB 采用二进制方式存储内存数据,文件小,且启动时恢复速度快。但构建 RDB 时,一个快照文件只能存储,构建时刻的内存数据,无法记录之后的数据变更。构建 RDB 的过程,即便在子进程中进行,但仍然属于 CPU 密集型的操作,而且每次落地全量数据,耗时也比较长,不能随时进行,特别是不能在高峰期进行。由于 RDB 采用二进制存储,可读性差,而且由于格式固定,不同版本之间可能存在兼容性问题。
AOF
Redis 的 AOF 持久化是以命令追加的方式进行数据落地的。通过打开 appendonly 配置Redis 将每一个写指令追加到磁盘 AOF 文件,从而及时记录内存数据的最新状态。这样即便 Redis 被 crash 或异常关闭后,再次启动,也可以通过加载 AOF来恢复最新的全量数据基本不会丢失数据。
AOF 文件中存储的协议是写指令的 multibulk 格式,这是 Redis 的标准协议格式,所以不同的 Redis 版本均可解析并处理,兼容性很好。
但是,由于 Redis 会记录所有写指令操作到 AOF大量的中间状态数据甚至被删除的过期数据都会存在 AOF 中,冗余度很大,而且每条指令还需通过加载和执行来进行数据恢复,耗时会比较大。
AOF 数据的落地流程如下。Redis 在处理完写指令后,首先将写指令写入 AOF 缓冲,然后通过 server_cron 定期将 AOF 缓冲写入文件缓冲。最后按照配置策略进行 fsync将文件缓冲的数据真正同步写入磁盘。
Redis 通过 appendfsync 来设置三种不同的同步文件缓冲策略。
第一种配置策略是 no即 Redis 不主动使用 fsync 进行文件数据同步落地,而是由操作系统的 write 函数去确认同步时间,在 Linux 系统中大概每 30 秒会进行一次同步,如果 Redis 发生 crash就会造成大量的数据丢失。
第二种配置策略是 always即每次将 AOF 缓冲写入文件,都会调用 fsync 强制将内核数据写入文件,安全性最高,但性能上会比较低效,而且由于频繁的 IO 读写,磁盘的寿命会大大降低。
第三种配置策略是 everysec。即每秒通过 BIO 线程进行一次 fsync。这种策略在安全性、性能以及磁盘寿命之间做较好的权衡可以较好的满足线上业务需要。
随着时间的推移AOF 持续记录所有的写指令AOF 会越来越大,而且会充斥大量的中间数据、过期数据,为了减少无效数据,提升恢复时间,可以定期对 AOF 进行 rewrite 操作。
AOF 的 rewrite 操作可以通过运维执行 bgrewiretaof 命令来进行,也可以通过配置重写策略进行,由 Redis 自动触发进行。当对 AOF 进行 rewrite 时,首先会 fork 一个子进程。子进程轮询所有 RedisDB 快照,将所有内存数据转为 cmd并写入临时文件。在子进程 rewriteaof 时,主进程可以继续执行用户请求,执行完毕后将写指令写入旧的 AOF 文件和 rewrite 缓冲。子进程将 RedisDB 中数据落地完毕后,通知主进程。主进程从而将 AOF rewite 缓冲数据写入 AOF 临时文件,然后用新的 AOF 文件替换旧的 AOF 文件,最后通过 BIO 线程异步关闭旧的 AOF 文件。至此AOF 的 rewrite 过程就全部完成了。
AOF 重写的过程,是一个轮询全部 RedisDB 快照,逐一落地的过程。每个 DB首先通过 select $db 来记录待落的 DBID。然后通过命令记录每个 key/value。对于数据类型为 SDS 的value可以直接落地。但如果 value 是聚合类型,则会将所有元素设为批量添加指令,进行落地。
对于 list 列表类型,通过 RPUSH 指令落地所有列表元素。对于 set 集合,会用 SADD 落地所有集合元素。对于 Zset 有序集合,会用 Zadd 落地所有元素,而对于 Hash 会用 Hmset 落地所有哈希元素。如果数据带过期时间,还会通过 pexpireat 来记录数据的过期时间。
AOF 持久化的优势是可以记录全部的最新内存数据,最多也就是 1-2 秒的数据丢失。同时 AOF 通过 Redis 协议来追加记录数据,兼容性高,而且可以持续轻量级的保存最新数据。最后因为是直接通过 Redis 协议存储,可读性也比较好。
AOF 持久化的不足是随着时间的增加,冗余数据增多,文件会持续变大,而且数据恢复需要读取所有命令并执行,恢复速度相对较慢。
混合持久化
Redis 在 4.0 版本之后,引入了混合持久化方式,而且在 5.0 版本后默认开启。前面讲到 RDB 加载速度快但构建慢缺少最新数据。AOF 持续追加最新写记录,可以包含所有数据,但冗余大,加载速度慢。混合模式一体化使用 RDB 和 AOF综合 RDB 和 AOF 的好处。即可包含全量数据,加载速度也比较快。可以使用 aof-use-rdb-preamble 配置来明确打开混合持久化模式。
混合持久化也是通过 bgrewriteaof 来实现的。当启用混合存储后,进行 bgrewriteaof 时,主进程首先依然是 fork 一个子进程,子进程首先将内存数据以 RDB 的二进制格式写入 AOF 临时文件中。然后,再将落地期间缓冲的新增写指令,以命令的方式追加到临时文件。然后再通知主进程落地完毕。主进程将临时文件修改为 AOF 文件,并关闭旧的 AOF 文件。这样主体数据以 RDB 格式存储,新增指令以命令方式追加的混合存储方式进行持久化。后续执行的任务,以正常的命令方式追加到新的 AOF 文件即可。
混合持久化综合了 RDB 和 AOF 的优缺点,优势是包含全量数据,加载速度快。不足是头部的 RDB 格式兼容性和可读性较差。

View File

@ -0,0 +1,81 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
29 从容应对亿级QPS访问Redis还缺少什么
众所周知Redis 在线上实际运行时,面对海量数据、高并发访问,会遇到不少问题,需要进行针对性扩展及优化。本课时,我会结合微博在使用 Redis 中遇到的问题,来分析如何在生产环境下对 Redis 进行扩展改造,以应对百万级 QPS。
功能扩展
对于线上较大流量的业务,单个 Redis 实例的内存占用很容易达到数 G 的容量,对应的 aof 会占用数十 G 的空间。即便每天流量低峰时间,对 Redis 进行 rewriteaof减少数据冗余但由于业务数据多写操作多aof 文件仍然会达到 10G 以上。
此时,在 Redis 需要升级版本或修复 bug 时,如果直接重启变更,由于需要数据恢复,这个过程需要近 10 分钟的时间,时间过长,会严重影响系统的可用性。面对这种问题,可以对 Redis 扩展热升级功能,从而在毫秒级完成升级操作,完全不影响业务访问。
热升级方案如下,首先构建一个 Redis 壳程序,将 redisServer 的所有属性包括redisDb、client等保存为全局变量。然后将 Redis 的处理逻辑代码全部封装到动态连接库 so 文件中。Redis 第一次启动,从磁盘加载恢复数据,在后续升级时,通过指令,壳程序重新加载 Redis 新的 so 文件,即可完成功能升级,毫秒级完成 Redis 的版本升级。而且整个过程中,所有 Client 连接仍然保留,在升级成功后,原有 Client 可以继续进行读写操作,整个过程对业务完全透明。
在 Redis 使用中,也经常会遇到一些特殊业务场景,是当前 Redis 的数据结构无法很好满足的。此时可以对 Redis 进行定制化扩展。可以根据业务数据特点,扩展新的数据结构,甚至扩展新的 Redis 存储模型,来提升 Redis 的内存效率和处理性能。
在微博中,有个业务类型是关注列表。关注列表存储的是一个用户所有关注的用户 uid。关注列表可以用来验证关注关系也可以用关注列表进一步获取所有关注人的微博列表等。由于用户数量过于庞大存储关注列表的 Redis 是作为一个缓存使用的,即不活跃的关注列表会很快被踢出 Redis。在再次需要这个用户的关注列表时重新从 DB 加载,并写回 Redis。关注列表的元素全部 long最初使用 set 存储,回种 set 时,使用 sadd 进行批量添加。线上发现,对于关注数比较多的关注列表,比如关注数有数千上万个用户,需要 sadd 上成千上万个 uid即便分几次进行批量添加每次也会消耗较多时间数据回种效率较低而且会导致 Redis 卡顿。另外,用 set 存关注列表,内存效率也比较低。
于是,我们对 Redis 扩展了 longset 数据结构。longset 本质上是一个 long 型的一维开放数组。可以采用 double-hash 进行寻址。
从 DB 加载到用户的关注列表,准备写入 Redis 前。Client 首先根据关注的 uid 列表,构建成 long 数组的二进制格式,然后通过扩展的 lsset 指令写入 Redis。Redis 接收到指令后,直接将 Client 发来的二进制格式的 long 数组作为 value 值进行存储。
longset 中的 long 数组,采用 double-hash 进行寻址,即对每个 long 值采用 2 个哈希函数计算,然后按 (h1 + n*h2)% 数组长度 的方式,确定 long 值的位置。n 从 0 开始计算,如果出现哈希冲突,即计算的哈希位置,已经有其他元素,则 n 加 1继续向前推进计算最大计算次数是数组的长度。
在向 longset 数据结构不断增加 long 值元素的过程中当数组的填充率超过阀值Redis 则返回 longset 过满的异常。此时 Client 会根据最新全量数据,构建一个容量加倍的一维 long 数组,再次 lsset 回 Redis 中。
在移动社交平台中,庞大的用户群体,相互之间会关注、订阅,用户自己会持续分享各种状态,另外这些状体数据会被其他用户阅读、评论、扩散及点赞。这样,在用户维度,就有关注数、粉丝数、各种状态行为数,然后用户每发表的一条 feed、状态还有阅读数、评论数、转发数、表态数等。一方面会有海量 key 需要进行计数,另外一方面,一个 key 会有 N 个计数。在日常访问中,一次查询,不仅需要查询大量的 key而且对每个 key 需要查询多个计数。
以微博为例,历史计数高达千亿级,而且随着每日新增数亿条 feed 记录,每条记录会产生 4~8 种计数,如果采用 Redis 的计数,仅仅单副本存储,历史数据需要占用 5~6T 以上的内存,每日新增 50G 以上,如果再考虑多 IDC、每个 IDC 部署 1 主多从,占用内存还要再提升一个数量级。由于微博计数,所有的 key 都是随时间递增的 long 型值,于是我们改造了 Redis 的存储结构。
首先采用 cdb 分段存储计数器,通过预先分配的内存数组 Table 存储计数,并且采用 double hash 解决冲突,避免 Redis 实现中的大量指针开销。 然后,通过 Schema 策略支持多列,一个 key id 对应的多个计数可以作为一条计数记录,还支持动态增减计数列,每列的计数内存使用精简到 bit。而且由于 feed 计数冷热区分明显,我们进行冷热数据分离存储方案,根据时间维度,近期的热数据放在内存,之前的冷数据放在磁盘, 降低机器成本。
关于计数器服务的扩展,后面的案例分析课时,我会进一步深入介绍改造方案。
线上 Redis 使用,不管是最初的 sync 机制,还是后来的 psync 和 psync2主从复制都会受限于复制积压缓冲。如果 slave 断开复制连接的时间较长,或者 master 某段时间写入量过大,而 slave 的复制延迟较大slave 的复制偏移量落在 master 的复制积压缓冲之外,则会导致全量复制。
完全增量复制
于是,微博整合 Redis 的 rdb 和 aof 策略,构建了完全增量复制方案。
在完全增量方案中aof 文件不再只有一个,而是按后缀 id 进行递增,如 aof.00001、aof.00002,当 aof 文件超过阀值,则创建下一个 id 加 1 的文件,从而滚动存储最新的写指令。在 bgsave 构建 rdb 时rdb 文件除了记录当前的内存数据快照,还会记录 rdb 构建时间,对应 aof 文件的 id 及位置。这样 rdb 文件和其记录 aof 文件位置之后的写指令,就构成一份完整的最新数据记录。
主从复制时master 通过独立的复制线程向 slave 同步数据。每个 slave 会创建一个复制线程。第一次复制是全量复制,之后的复制,不管 slave 断开复制连接有多久,只要 aof 文件没有被删除,都是增量复制。
第一次全量复制时,复制线程首先将 rdb 发给 slave然后再将 rdb 记录的 aof 文件位置之后的所有数据,也发送给 slave即可完成。整个过程不用重新构建 rdb。
后续同步时slave 首先传递之前复制的 aof 文件的 id 及位置。master 的复制线程根据这个信息,读取对应 aof 文件位置之后的所有内容,发送给 slave即可完成数据同步。
由于整个复制过程master 在独立复制线程中进行,所以复制过程不影响用户的正常请求。为了减轻 master 的复制压力,全增量复制方案仍然支持 slave 嵌套,即可以在 slave 后继续挂载多个 slave从而把复制压力分散到多个不同的 Redis 实例。
集群管理
前面讲到Redis-Cluster 的数据存储和集群逻辑耦合,代码逻辑复杂易错,存储 slot 和 key 的映射需要额外占用较多内存,对小 value 业务影响特别明显,而且迁移效率低,迁移大 value 容易导致阻塞另外Cluster 复制只支持 slave 挂在 master 下,无法支持 需要较多slave、读 TPS 特别大的业务场景。除此之外Redis 当前还只是个存储组件,线上运行中,集群管理、日常维护、状态监控报警等这些功能,要么没有支持,要么支持不便。
因此我们也基于 Redis 构建了集群存储体系。首先将 Redis 的集群功能剥离到独立系统Redis 只关注存储,不再维护 slot 等相关的信息。通过新构建的 clusterManager 组件,负责 slot 维护,数据迁移,服务状态管理。
Redis 集群访问可以由 proxy 或 smart client 进行。对性能特别敏感的业务,可以通过 smart client 访问,避免访问多一跳。而一般业务,可以通过 Proxy 访问 Redis。
业务资源的部署、Proxy 的访问都通过配置中心进行获取及协调。clusterManager 向配置中心注册业务资源部署,并持续探测服务状态,根据服务状态进行故障转移,切主、上下线 slave 等。proxy 和 smart client 从配置中心获取配置信息,并持续订阅服务状态的变化。

View File

@ -0,0 +1,112 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
30 面对海量数据,为什么无法设计出完美的分布式缓存体系?
随着互联网的发展,分布式系统变得越来越重要,当前的大中型互联网系统几乎都向着分布式方向发展。分布式系统简单说就是一个软硬件分布在不同机房、不同区域的网络计算机上,彼此之间仅仅通过消息传递进行通信及协调的系统。分布式系统需要利用分布的服务,在确保数据一致的基础上,对外提供稳定的服务。
CAP 定理的诞生
在分布式系统的发展中,影响最大最广泛的莫过于 CAP 理论了,可以说 CAP 理论是分布式系统发展的理论基石。早在 1998 年,加州大学的计算机科学家 Eric Brewer 就提出分布式系统的三个指标。在此基础上2 年后Eric Brewer 进一步提出了 CAP 猜想。又过了 2 年,到了 2002 年,麻省理工学院的 Seth Gilbert 和 Nancy Lynch 从理论上证明了 CAP 猜想。CAP 猜想成为了 CAP 定理也称为布鲁尔定理。从此CAP 定理成为分布式系统发展的理论基石,广泛而深远的影响着分布式系统的发展。
CAP 定理指标
CAP 定理,简单的说就是分布式系统不可能同时满足 Consistency 一致性、Availability 可用性、Partition Tolerance 分区容错性三个要素。因为 Consistency、Availability 、Partition Tolerance 这三个单词的首字母分别是 C、A、P所以这个结论被称为 CAP 定理。
Consistency 一致性
CAP 定理的第一个要素是 Consistency 一致性。一致性的英文含义是指“all nodes see the same data at the same time”。即所有节点在任意时间被访问返回的数据完全一致。CAP 作者 Brewer 的另外一种解释是在写操作之后的读指令,必须得到的是写操作写入的值,或者写操作之后新更新的值。从服务端的视角来看,就是在 Client 写入一个更新后Server 端如何同步这个新值到整个系统,从而保证整个系统的这个数据都相同。而从客户端的视角来看,则是并发访问时,在变更数据后,如何获取到最新值。
Availability 可用性
CAP 定理的第二个要素是 Availability 可用性。可用性的英文含义是指“Reads and writes always succeed”。即服务集群总能够对用户的请求给予响应。Brewer 的另外一个种解释是对于一个没有宕机或异常的节点,总能响应用户的请求。也就是说当用户访问一个正常工作的节点时,系统保证该节点必须给用户一个响应,可以是正确的响应,也可以是一个老的甚至错误的响应,但是不能没有响应。从服务端的视角来看,就是服务节点总能响应用户请求,不会吞噬、阻塞请求。而从客户端视角来看,发出的请求总有响应,不会出现整个服务集群无法连接、超时、无响应的情况。
Partition Tolerance 分区容错性
第三个要素是 Partition Tolerance 分区容错性。分区容错的英文含义是指“The system continues to operate despite arbitrary message loss or failure of part of the system”。即出现分区故障或分区间通信异常时系统仍然要对外提供服务。在分布式环境每个服务节点都不是可靠的不同服务节点之间的通信有可能出现问题。当某些节点出现异常或者某些节点与其他节点之间的通信出现异常时整个系统就产生了分区问题。从服务端的视角来看出现节点故障、网络异常时服务集群仍然能对外提供稳定服务就是具有较好的分区容错性。从客户端视角来看就是服务端的各种故障对自己透明。
正常服务场景
根据CAP定理在分布式系统中这三个要素不可能三者兼顾最多只能同时满足两点。接下来我们用 最简单的2 个服务节点场景,简要证明一下 CAP 定理。
如图所示,网络上有 2 个服务节点 Node1 和 Node2它们之间通过网络连通组成一个分布式系统。在正常工作的业务场景Node1 和 Node2 始终正常运行,且网络一直良好连通。
假设某初始时刻,两个节点中的数据相同,都是 V0用户访问 Nodel 和 Node2 都会立即得到 V0 的响应。当用户向 Node1 更新数据,将 V0 修改为 V1时分布式系统会构建一个数据同步操作 M将 V1 同步给 Node2由于 Node1 和 Node2 都正常工作且相互之间通信良好Node2 中的 V0 也会被修改为 V1。此时用户分别请求 Node1 和 Node2得到的都是 V1数据保持一致性且总可以都得到响应。
网络异常场景
作为一个分布式系统,总是有多个分布的、需要网络连接的节点,节点越多、网络连接越复杂,节点故障、网络异常的情况出现的概率就会越大。要完全满足 CAP 三个元素。就意味着,如果节点之间出现了网络异常时,需要支持网络异常,即支持分区容错性,同时分布式系统还需要满足一致性和可用性。我们接下来看是否可行。
现在继续假设初始时刻Node1 和 Node2 的数据都是 V0然后此时 Node1 和 Node2 之间的网络断开。用户向 Node1 发起变更请求,将 V0 变更为 V1分布式系统准备发起同步操作 M但由于 Node1 和 Node2 之间网络断开,同步操作 M 无法及时同步到 Node2所以 Node2 中的数据仍然是 V0。
此时,有用户向 Node2 发起请求,由于 Node2 与 Node1 断开连接数据没有同步Node2 无法立即向用户返回正确的结果 V1。那怎么办呢有两种方案。
第一种方案是牺牲一致性Node2 向请求用户返回老数据 V0 的响应。
第二种方案是牺牲可用性Node2 持续阻塞请求,直到 Node1 和 Node2 之间的网络连接恢复,并且数据更新操作 M 在 Node2 上执行完毕Node2 再给用户返回正确的 V1 操作。
至此,简要证明过程完毕。整个分析过程也就说明了,分布式系统满足分区容错性时,就无法同时满足一致性和可用性,只能二选一,也就进一步证明了分布式系统无法同时满足一致性、可用性、分区容错性这三个要素。
CAP 权衡
CA
根据 CAP 理论和前面的分析,我们知道分布式系统无法同时满足一致性、可用性、分区容错性三个要素,那我们在构建分布式系统时,应该如何选择呢?
由于这三个要素对分布式系统都非常重要,既然三个不能同时满足,那就先尽量满足两个,只舍弃其中的一个元素。
第一种方案选择是 CA即不支持分区容错只支持一致性和可用性。不支持分区容错性也就意味着不允许分区异常设备、网络永远处于理想的可用状态从而让整个分布式系统满足一致性和可用性。
但由于分布式系统是由众多节点通过网络通信连接构建的,设备故障、网络异常是客观存在的,而且分布的节点越多,范围越广,出现故障和异常的概率也越大,因此,对于分布式系统而言,分区容错 P 是无法避免的,如果避免了 P只能把分布式系统回退到单机单实例系统。
CP
第二种方案选择是 CP因为分区容错 P 客观存在,即相当于放弃系统的可用性,换取一致性。那么系统在遇到分区异常时,会持续阻塞整个服务,直到分区问题解决,才恢复对外服务,这样可以保证数据的一致性。选择 CP 的业务场景比较多特别是对数据一致性特别敏感的业务最为普遍。比如在支付交易领域Hbase 等分布式数据库领域,都要优先保证数据的一致性,在出现网络异常时,系统就会暂停服务处理。分布式系统中,用来分发及订阅元数据的 Zookeeper也是选择优先保证 CP 的。因为数据的一致性是这些系统的基本要求否则银行系统0 余额大量取现,数据库系统访问,随机返回新老数据都会引发一系列的严重问题。
AP
第三种方案选择是 AP由于分区容错 P 客观存在,即相当于放弃系统数据的一致性,换取可用性。这样,在系统遇到分区异常时,节点之间无法通信,数据处于不一致的状态,为了保证可用性,服务节点在收到用户请求后立即响应,那只能返回各自新老不同的数据。这种舍弃一致性,而保证系统在分区异常下的可用性,在互联网系统中非常常见。比如微博多地部署,如果不同区域的网络中断,区域内的用户仍然发微博、相互评论和点赞,但暂时无法看到其他区域用户发布的新微博和互动状态。对于微信朋友圈也是类似。还有如 12306 的火车购票系统,在节假日高峰期抢票时,偶尔也会遇到,反复看到某车次有余票,但每次真正点击购买时,却提示说没有余票。这样,虽然很小一部分功能受限,但系统整体服务稳定,影响非常有限,相比 CP用户体验会更佳。
CAP 问题及误区
CAP 理论极大的促进了分布式系统的发展,但随着分布式系统的演进,大家发现,其实 CAP 经典理论其实过于理想化,存在不少问题和误区。
首先,以互联网场景为例,大中型互联网系统,主机数量众多,而且多区域部署,每个区域有多个 IDC。节点故障、网络异常出现分区问题很常见要保证用户体验理论上必须保证服务的可用性选择 AP暂时牺牲数据的一致性这是最佳的选择。
但是,当分区异常发生时,如果系统设计的不够良好,并不能简单的选择可用性或者一致性。例如,当分区发生时,如果一个区域的系统必须要访问另外一个区域的依赖子服务,才可以正常提供服务,而此时网络异常,无法访问异地的依赖子服务,这样就会导致服务的不可用,无法支持可用性。同时,对于数据的一致性,由于网络异常,无法保证数据的一致性,各区域数据暂时处于不一致的状态。在网络恢复后,由于待同步的数据众多且复杂,很容易出现不一致的问题,同时某些业务操作可能跟执行顺序有关,即便全部数据在不同区域间完成同步,但由于执行顺序不同,导致最后结果也会不一致。长期多次分区异常后,会累积导致大量的数据不一致,从而持续影响用户体验。
其次,在分布式系统中,分区问题肯定会发生,但却很少发生,或者说相对于稳定工作的时间,会很短且很小概率。当不存在分区时,不应该只选择 C 或者 A而是可以同时提供一致性和可用性。
再次,同一个系统内,不同业务,同一个业务处理的不同阶段,在分区发生时,选择一致性和可用性的策略可能都不同。比如前面讲的 12306 购票系统,车次查询功能会选择 AP购票功能在查询阶段也选择 AP但购票功能在支付阶段则会选择 CP。因此在系统架构或功能设计时并不能简单选择 AP 或者 CP。
而且,系统实际运行中,对于 CAP 理论中的每个元素,实际并不都是非黑即白的。比如一致性,有强一致性,也有弱一致性,即便暂时大量数据不一致,在经历一段时间后,不一致数据会减少,不一致率会降低。又如可用性,系统可能会出现部分功能异常,其他功能正常,或者压力过大,只能支持部分用户的请求的情况。甚至分区也可以有一系列中间状态,区域网络完全中断的情况较少,但网络通信条件却可以在 0~100% 之间连续变化,而且系统内不同业务、不同功能、不同组件对分区还可以有不同的认知和设置。
最后CAP 经典理论没有考虑实际业务中网络延迟问题延迟自始到终都存在甚至分区异常P都可以看作一种延迟而且这种延迟可以是任意时间1 秒、1 分钟、1 小时、1 天都有可能,此时系统架构和功能设计时就要考虑,如何进行定义区分及如何应对。
这些问题,传统的 CAP 经典理论并没有给出解决方案,开发者如果简单进行三选二,就会进入误区,导致系统在运行中问题连连。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
31 如何设计足够可靠的分布式缓存体系,以满足大中型移动互联网系统的需要?
上一课时我们了解了为什么不能设计出同时满足一致性、可用性、分区容错性的分布式系统,本课时我们来具体看下,工作中应该如何设计分布式系统,以满足大中型互联网系统的需求。
传统 CAP 的突破
随着分布式系统的不断演进,会不断遇到各种问题,特别是当前,在大中型互联网系统的演进中,私有云、公有云并行发展且相互融合,互联网系统的部署早已突破单个区域,系统拓扑走向全国乃至全球的多区域部署。在践行传统的经典 CAP 理论的同时,需要认识到 CAP 三要素的复杂性,不能简单的对 CAP 理论进行三选二,需要根据业务特点、部署特点,对 CAP 理论进行创新、修正及突破。
甚至 CAP 理论的提出者 Eric Brewer 自己也在 CAP 理论提出的 12 年后,即在 2012 年,对 CAP 理论,特别是 CAP 使用中的一些误区进一步进行修正、拓展及演进说明。Brewer 指出CAP 理论中经典的三选二公式存在误导性CAP 理论的经典实践存在过于简化三种要素,以及三要素之间的相互关系的问题。他同时把 CAP 与 ACID、BASE 进行比较,分析了 CAP 与延迟的关系,最后还重点分析了分布式系统如何应对分区异常的问题。
要突破经典的 CAP 理论和实践,要认识到 CAP 三要素都不是非黑即白,而是存在一系列的可能性,要在实际业务场景中对分布式系统,进行良好的架构设计,这是一个很大的挑战。
在系统实际运行过程中,大部分时间,分区异常不会发生,此时可以提供良好的一致性和可用性。同时,我们需要在系统架构设计中,在分析如何实现业务功能、系统 SLA 指标实现等之外,还要考虑整个系统架构中,各个业务、模块、功能、系统部署如何处理潜在的分区问题。
要良好处理潜在的分区问题,可以采用如下步骤。
首先,要考虑如何感知分区的发生,可以通过主动探测、状态汇报、特殊时间/特殊事件预警、历史数据预测等方式及时发现分区。
其次,如果发现分区,如何在分区模式下进行业务处理。可以采用内存缓冲、队列服务保存数据后,继续服务,也可以对敏感功能直接停止服务,还可以对分区进行进一步细分,如果是短时间延迟,可以部分功能或请求阻塞等待结果,其他功能和请求快速返回本地老数据;如果分区时长超过一定阀值,进行部分功能下线,只提供部分核心功能。
最后,在分区异常恢复后,如何同步及修复数据,建立补偿机制应对分区模式期间的错误。如系统设计中引入消息队列,在分区模式期间,变更的数据用消息队列进行保存,分区恢复后,消息处理机从消息队列中进行数据读取及修复。也可以设计为同步机制,分区异常时,记录最后同步的位置点,分区恢复后,从记录的位置点继续同步数据。还可以在分区时,分布式系统的各区记录自己没有同步出去的数据,然后在分区恢复后,主动进行异地数据比较及合并。最后,还可以在故障恢复后通过数据扫描,对比分区数据,进行比较及修复。
BASE 理论
BASE 理论最初由 Brewer 及他的同事们提出。虽然比较久远,但在当前的互联网界活力更盛。各大互联网企业,在构建大中型规模的分布式互联网系统,包括各种基于私有云、公有云及多云结合的分布式系统时,在尽力借鉴 CAP 理论与实践的同时,还充分验证和实践了 BASE 理论,并将其作为 CAP 理论的一种延伸,很好的应用在互联网各种系统中。
BASE 理论及实践是分布式系统对一致性和可用性权衡后的结果。其基本思想是分布式系统各个功能要适当权衡,尽力保持整个系统稳定可用,即便在出现局部故障和异常时,也确保系统的主体功能可用,确保系统的最终一致性。
BASE 理论也包括三要素,即 Basically Availabe 基本可用、Soft state 软状态和 Eventual Consistency 最终一致性。
Basically Availabe 基本可用
基本可用是指分布式系统在出现故障时,允许损失部分可用性。比如可以损失部分 SLA如响应时间适当增加、处理性能适当下降也可以损失部分周边功能、甚至部分核心功能。最终保证系统的主体基本稳定核心功能基本可用的状态。如淘宝、京东在双十一峰值期间请求会出现变慢但少许延迟后仍然会返回正确结果同时还会将部分请求导流到降级页面等。又如微博在突发故障时会下线部分周边功能将资源集中用于保障首页 feed 刷新、发博等核心功能。
Soft state 软状态
软状态是指允许系统存在中间状态。故障发生时,各分区之间的数据同步出现延时或暂停,各区域的数据处于不一致的状态,这种状态的出现,并不影响系统继续对外提供服务。这种节点不一致的状态和现象就是软状态。
Eventual Consistency 最终一致性
最终一致性,是指分布式系统不需要实时保持强一致状态,在系统故障发生时,可以容忍数据的不一致,在系统故障恢复后,数据进行同步,最终再次达到一致的状态。
BASE 理论是面向大中型分布式系统提出的,它更适合当前的大中型互联网分布式系统。
首先用户体验第一,系统设计时要优先考虑可用性。
其次,在故障发生时,可以牺牲部分功能的可用性,牺牲数据的强一致性,来保持系统核心功能的可用性。
最后,在系统故障恢复后,通过各种策略,确保系统最终再次达到一致。
一致性问题及应对
分布式系统中,为了保持系统的可用性和性能,系统中的数据需要存储多个副本,这些副本分布在不同的物理机上,如果服务器、网络出现故障,就会导致部分数据副本写入成功,部分数据副本写入失败,这就会导致各个副本之间数据不一致,数据内容冲突,也就造成了数据的不一致。因此,为了保持分布式系统的一致性,核心就是如何解决分布式系统中的数据一致性。
保持数据一致性的方案比较多,比较常见的方案有,分布式事务,主从复制,业务层消息总线等。
分布式事务
分布式事务在各节点均能正常执行事务内一系列操作才会提交,否则就进行回滚,可以保持系统内数据的强一致。分布式事务应用比较广泛,比如跨行转账,用户甲向用户乙转账,甲账户需要减少,乙账户需要增加对应金额,这两个操作就必须构成一个分布式事务。还有其他场景,比如 12306 中支付出票、支付宝买入基金等,都需要保持对应操作的事务性。
分布式事务的具体方案较多,典型有 2PC 两阶段提交、3PC 三阶段提交、Paxos、Zab、Raft等。
两阶段提交方案中,系统包括两类节点,一类是协调者,一类是事务参与者。协调者一般只有一个,参与者可以理解为数据副本的数量,一般有多个。
两阶段提交的执行分为请求阶段和提交阶段两部分。在请求阶段,协调者将通知事务参与者准备提交或取消事务,通知完毕后,事务参与者就开始进行表决。在表决中,参与者如果本地作业执行成功,则表决同意,如果执行失败,则表决取消,然后把表决回复给协调者。然后进入提交阶段。
在提交阶段,协调者将基于第一阶段的表决结果进行决策是提交事务还是取消事务。决策方式是所有参与者表决同意则决策提交,否则决策取消。然后协调者把决策结果分发给所有事务参与者。事务参与者接受到协调者的决策后,执行对应的操作。
三阶段提交与两阶段提交类似,只是在协调者、参与者都引入了超时机制,而且把两阶段提交中的第一阶段分拆成了 2 步,即先询问再锁资源。
分布式事务中 Paxos、Zab、Raft 等方案的基本思想类似。在每个数据副本附带版本信息,每次写操作保证写入大于 N/2 个节点,同时每次读操作也保证从大于 N/2 个节点读,以多数派作为最终决策。这种仲裁方式在业界使用比较广泛,比如亚马逊的 Dynamo 存储也是类似Dynamo 的决策更简洁,只要写操作数 + 读操作数大于节点数即可。一般整个仲裁过程由协调者进行,当然也可以像 Dynamo那样支持由业务 Client 决策也没问题,更有弹性,因为可以由业务按各种策略选择。在仲裁后,仲裁者可以选择正确的版本数据,甚至在某些场景下可以将不同版本的数据合并成一个新数据。
主从复制
主从复制也是一种使用较为广泛的一致性方案。在 Mysql 等各种 DB 中广泛使用,之前课程中讲到的 Redis 也是采用主从复制来保持主从数据一致的。
除了从数据层保证一致性,还可以在上层业务层,通过消息总线分发,来更新缓存及存储体系,这也是互联网企业在进行异地多活方案设计时经常会考虑到的方案。
消息总线在各区域相互分发消息,有 push 推和 pull 拉两种方案。一般来讲pull 拉的方式,由于拉取及拉取后的执行过程对分发是可以感知,在网络异常时,更容易保障数据的一致性。
分布式系统多区数据一致性案例
如图所示,是微博进行多区数据一致性保障案例。消息是通过消息中间件 wmb 进行分发的。wmb 两边分别为分布式系统的 2 个区域。每个区域所有的用户写操作,都会封装成一条消息,业务消息会首先写入消息队列服务,然后消息队列处理机读取消息队列,并进行缓存和 DB 的更新。在业务消息写入消息队列服务时wmb 会同时将这条消息分发给其他所有异地区子系统。分发的方式是wmb 本地组件先将消息写入本地队列,然后 wmb 异地组件 Client 再读取。当分区故障发生时异地读取失败消息仍然在各区的消息队列中不会丢失。分区故障过程中系统的各区子系统只处理本地事件。在分区故障排除后wmb Client 继续读取异地消息,然后由消息处理机执行,最终实现数据的一致性。
由于 wmb 通过消息队列机方式从业务层面进行同步,分区故障发生时,各区都是先执行本地,分区恢复后再执行异地,所有事件在各区的执行顺序可能会有差异,在某些极端场景下,可能会导致数据不一致。所以,微博只用 wmb 来更新缓存DB 层仍然采用主从复制的方式进行强一致保障。这样即便故障恢复期间,可能存在少量缓存数据暂时不一致,由于恢复数据时采用了更短的过期时间,这部分数据在从 DB 重新加载后,仍然能保持数据的最终一致性。同时,微博不用 DB 数据更新缓存,是由于缓存数据结构过于复杂,而且经常需要根据业务需要进行扩展,一条缓存记录会涉及众多 DB以及 Redis 中多项纪录,通过 DB 同步数据触发更新缓存涉及因素太多,不可控。所以微博在尝试 DB 驱动缓存更新方案失败后,就改为 wmb 消息队列方式进行缓存更新。

View File

@ -0,0 +1,49 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
32 一个典型的分布式缓存系统是什么样的?
本课时我们具体看下一个典型的分布式缓存系统是什么样的。
分布式 Redis 服务
由于本课程聚焦于缓存,接下来,我将以微博内的 分布式 Redis 服务系统为例,介绍一个典型的分布式缓存系统的组成。
微博的 Redis 服务内部也称为 RedisService。RedisService 的整体架构如图所示。主要分为Proxy、存储、集群管理、配置中心、Graphite5 个部分。
RedisService 中的 Proxy 是无状态多租户模型,每个 Proxy 下可以挂载不同的业务存储,通过端口进行业务区分。
存储基于 Redis 开发,但在集群数据存储时,只保留了基本的存储功能,支持定制的迁移功能,但存储内部无状态,不存储 key-slot 映射关系。
配置中心用于记录及分发各种元数据,如存储 Proxy 的 IP、端口、配置等在发生变化时订阅者可以及时感知。
Graphite 系统用于记录并展现系统、业务,组件以及实例等的状态数据。
ClusterManager 用于日常运维管理,业务 SLA 监控,报警等。同时 ClusterManager 会整合 Proxy、Redis 后端存储以及配置中心,对业务数据进行集群管理
多租户 Proxy
RedisService 中的 Proxy 无任何状态,所有 Proxy 实例的启动参数相同。但 Proxy 启动前clusterManager 会在配置中心设置该实例的业务及存储配置信息Proxy 启动后,到配置中心通过自己的 IP 来获取并订阅配置然后进行初始化。Proxy 与后端 Redis 存储采用长连接,当 Client 并发发送请求到 Proxy 后Proxy 会将请求进行打包,并发地以 pipeline 的方式批量访问后端,以提升请求效率。对于多租户 Proxy由于不同业务的存储位置可能不同因此对每个请求需要进行业务区分一般有 2 种方式进行区分。
方案 1按照 key 的 namespace 前缀进行业务区分,比如 Client 分别请求 user、graph、feed 业务下的 key k1业务 Client 分别构建 {user}k1、{graph}k1、{feed}k1然后发送给 ProxyProxy 解析 key 前缀确定 key 对应的业务。
方案 2对每个业务分配一个业务端口不同业务访问自己的端口Proxy 会根据端口确定业务类型。这种类型不需要解析 key 前缀,不需要重构请求,性能更为高效。但需要为业务配置端口,增加管理成本,实践上,由于业务 Redis 资源一般会采用不同端口,所以业务 Proxy 可以采用业务资源分片的最小端口来作为业务端口标志。
Redis 数据存储
RedisService 中的 Redis 存储基于 Redis 5.0 扩展,内部称 wrediswredis 不存储 key-slot 映射,只记录当前实例中存储的 slot 的 key 计数。wredis 处理任何收到的操作命令而数据分片访问的正确性由访问端确保。在每日低峰时段clusterManager 对 Redis 存储进行扫描,发现 slot 存储是否存在异常。因为微博中有大量的小 value key如果集群中增加 key-slot 映射,会大大增大存储成本,通过消除 key-slot 映射等相关优化,部分业务可以减少 20% 以上的存储容量。
wredis 支持 slot 的同步迁移及异步迁移。同时支持热升级可以毫秒级完成组件升级。wredis 也支持全增量复制支持微博内部扩展的多种数据结构。热升级、全增量复制、数据结构扩展等在之前的课时中有介绍具体可以参考之前讲的“Redis 功能扩展”课时的内容。
配置中心 configService
微博的配置中心,内部称为 configService是微博内部配置元数据管理的基础组件。configService 自身也是多 IDC 部署的,配置信息通过多版本数据结构存储,支持版本回溯。同时配置数据可以通过 merkle hash 树进行快速一致性验证。RedisService 中的所有业务、资源、Proxy 的配置都存储在 configService 中,由 cluster 写入并变更Proxy、业务 Client 获取并订阅所需的配置数据。configService 在配置节点发生变更时,会只对节点进行事件通知,订阅者无需获取全量数据,可以大大减轻配置变更后的获取开销。
ClusterManager 是一个运维后台。主要用于运维工作如后端资源、Proxy 的实例部署配置变更版本升级等。也用于数据的集群管理clusterManager 内部会存储业务数据的集群映射,并在必要时进行数据迁移和故障转移。迁移采用 slot 方式,可以根据负载进行迁移流量控制,同时会探测集群内的节点状态,如在 wredis 的 master 异常后,从 slave 中选择一个新的master并重建主从关系。clusterManager 还支持业务访问的 Proxy 域名管理,监控集群节点的实例状态,监控业务的 SLA 指标,对异常进行报警,以便运维及时进行处理。
集群数据同步
RedisService 中的数据存储在多个区域,每个区域都有多个 IDC。部署方式是核心内网加公有云的方式。使用公有云主要是由微博的业务特点决定的在突发事件或热点事件发生时很容易形成流量洪峰读写 TPS 大幅增加利用公有云可以快速、低成本的扩展系统大幅增加系统处理能力。根据业务特点wredis 被分为缓存和存储类型。对于 Redis 缓存主要通过消息总线进行驱动更新,而对于 Redis 存储则采用主从复制更新。更新方式不同,主要是因为 Redis 作为缓存类型的业务数据,在不同区或者不同 IDC 的热点数据不同,如果采用主从复制,部署从库的 IDC会出现热数据无法进入缓存同时冷数据无法淘汰的问题因为从库的淘汰也要依赖主库进行。而对于 Redis 作存储的业务场景,由于缓存存放全量数据,直接采用主从复制进行数据一致性保障,这样最便捷。

View File

@ -0,0 +1,97 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
35 如何为社交feed场景设计缓存体系
在上一课时我们讲解了如何为海量计数场景进行缓存设计,本课时中我将讲解如何为社交 Feed 场景设计缓存体系。
Feed 流场景分析
Feed 流是很多移动互联网系统的重要一环如微博、微信朋友圈、QQ 好友动态、头条/抖音信息流等。虽然这些产品形态各不相同,但业务处理逻辑却大体相同。用户日常的“刷刷刷”,就是在获取 Feed 流,这也是 Feed 流的一个最重要应用场景。用户刷新获取 Feed 流的过程,对于服务后端,就是一个获取用户感兴趣的 Feed并对 Feed 进行过滤、动态组装的过程。
接下来,我将以微博为例,介绍用户在发出刷新 Feed 流的请求后,服务后端是如何进行处理的。
获取 Feed 流操作是一个重操作,后端数据处理存在 100 ~ 1000 倍以上的读放大。也就是说,前端用户发出一个接口请求,服务后端需要请求数百甚至数千条数据,然后进行组装处理并返回响应。因此,为了提升处理性能、快速响应用户,微博 Feed 平台重度依赖缓存,几乎所有的数据都从缓存获取。如用户的关注关系从 Redis 缓存中获取,用户发出的 Feed 或收到特殊 Feed 从 Memcached 中获取,用户及 Feed 的各种计数从计数服务中获取。
Feed 流流程分析
Feed 流业务作为微博系统的核心业务为了保障用户体验SLA 要求较高,核心接口的可用性要达到 4 个 9接口耗时要在 50~100ms 以内,后端数据请求平均耗时要在 3~5ms 以内,因此为了满足亿级庞大用户群的海量并发访问需求,需要对缓存体系进行良好架构且不断改进。
在 Feed 流业务中,核心业务数据的缓存命中率基本都在 99% 以上,这些缓存数据,由 Feed 系统进行多线程并发获取及组装,从而及时发送响应给用户。
Feed 流获取的处理流程如下。
首先,根据用户信息,获取用户的关注关系,一般会得到 300~2000 个关注用户的 UID。
然后,再获取用户自己的 Feed inbox 收件箱。收件箱主要存放其他用户发表的供部分特定用户可见的微博 ID 列表。
接下来,再获取所有关注列表用户的微博 ID 列表,即关注者发表的所有用户或者大部分用户可见的 Feed ID 列表。这些 Feed ID 列表都以 vector 数组的形式存储在缓存。由于一般用户的关注数会达到数百甚至数千,因此这一步需要获取数百或数千个 Feed vector。
然后Feed 系统将 inbox 和关注用户的所有 Feed vector 进行合并,并排序、分页,即得到目标 Feed 的 ID 列表。
接下来,再根据 Feed ID 列表获取对应的 Feed 内容,如微博的文字、视频、发表时间、源微博 ID 等。
然后,再进一步获取所有微博的发表者 user 详细信息、源微博内容等信息,并进行内容组装。
之后,如果用户设置的过滤词,还要将这些 Feed 进行过滤筛选,剔除用户不感兴趣的 Feed。
接下来,再获取用户对这些 Feed 的收藏、赞等状态,并设置到对应微博中。
最后,获取这些 Feed 的转发数、评论数、赞数等并进行计数组装。至此Feed 流获取处理完毕Feed 列表以 JSON 形式返回给前端,用户刷新微博首页成功完成。
Feed 流缓存架构
Feed 流处理中,缓存核心业务数据主要分为 6 大类。
第一类是用户的 inbox 收件箱,在用户发表仅供少量用户可见的 Feed 时,为了提升访问效率,这些 Feed ID 并不会进入公共可见的 outbox 发件箱,而会直接推送到目标客户的收件箱。
第二类是用户的 outbox 发件箱。用户发表的普通微博都进入 outbox这些微博几乎所有人都可见由粉丝在刷新 Feed 列表首页时,系统直接拉取组装。
第三类是 Social Graph 即用户的关注关系,如各种关注列表、粉丝列表。
第四类是 Feed Content 即 Feed 的内容,包括 Feed 的文字、视频、发表时间、源微博 ID 等。
第五类是 Existence 存在性判断缓存,用来判断用户是否阅读了某条 Feed是否赞了某条 Feed 等。对于存在性判断,微博是采用自研的 phantom 系统,通过 bloomfilter 算法进行存储的。
第六类是 Counter 计数服务用来存储诸如关注数、粉丝数Feed 的转发、评论、赞、阅读等各种计数。
对于 Feed 的 inbox 收件箱、outbox 发件箱Feed 系统通过 Memcached 进行缓存,以 feed id的一维数组格式进行存储。
对于关注列表Feed 系统采用 Redis 进行缓存,存储格式为 longset。longset 在之前的课时介绍过,是微博扩展的一种数据结构,它是一个采用 double-hash 寻址的一维数组。当缓存 miss 后,业务 client 可以从 DB 加载,并直接构建 longset 的二进制格式数据作为 value写入RedisRedis 收到后直接 restore 到内存,而不用逐条加入。这样,即便用户有成千上万个关注,也不会引发阻塞。
Feed content 即 Feed 内容,采用 Memcached 存储。由于 Feed 内容有众多的属性且时常需要根据业务需要进行扩展Feed 系统采用 Google 的 protocol bufers 的格式进行存放。protocol buffers 序列化后的所生成的二进制消息非常紧凑,二进制存储空间比 XML 小 3~10 倍,而序列化及反序列化的性能却高 10 倍以上,而且扩展及变更字段也很方便。微博的 Feed content 最初采用 XML 和 JSON 存储,在 2011 年之后逐渐全部改为 protocol buffers 存储。
对于存在性判断,微博 Feed 系统采用自研的 phantom 进行存储。数据存储采用 bloom filter 存储结构。实际上 phantom 本身就是一个分段存储的 bloomfilter 结构。bloomFilter 采用 bit 数组来表示一个集合,整个数组最初所有 bit 位都是 0插入 key 时,采用 k 个相互独立的 hash 函数计算,将对应 hash 位置置 1。而检测某个 key 是否存在时,通过对 key 进行多次 hash检查对应 hash 位置是否为 1 即可,如果有一个为 0则可以确定该 key 肯定不存在,但如果全部为 1大概率说明该 key 存在,但该 key 也有可能不存在,即存在一定的误判率,不过这个误判率很低,一般平均每条记录占用 1.2 字节时,误判率即可降低到 1%1.8 字节,误判率可以降到千分之一。基本可以满足大多数业务场景的需要。
对于计数服务,微博就是用前面讲到的 CounterService。CounterService 采用 schema 策略,支持一个 key 对应多个计数,只用 5~10% 的空间,却提升 3~5 倍的读取性能。
Feed 流 Mc 架构
Feed 流的缓存体系中,对于 Memcached 存储采用 L1-Main-Backup 架构。这个架构前面在讲分布式 Memcached 实践中也有介绍。微博 Feed 流的 Memcached 存储架构体系中L1 单池容量一般为 Main 池的 1/10有 4~6 组 L1用于存放最热的数据可以很好的解决热点事件或节假日的流量洪峰问题。Main 池容量最大保存了最近一段时间的几乎所有较热的数据。Backup 池的容量一般在 Main 池的 12 以下,主要解决 Main 池异常发生或者 miss 后的 key 访问。
L1-Main-Bakcup 三层 Memcached 架构,可以很好抵御突发洪峰流量、局部故障等。实践中,如果业务流量不大,还可以配置成两层 Main-Bakckup。对于 2 层或 3 层 Mc 架构,处理 Mc 指令需要各种穿透、回种,需要保持数据的一致性,这些策略相对比较复杂。因此微博构建了 proxy封装 Mc 多层的读写逻辑,简化业务的访问。部分业务由于对响应时间很敏感,不希望因为增加 proxy 一跳而增加时间开销,因此微博也提供了对应的 client由 client 获取并订阅 Mc 部署,对三层 Mc 架构进行直接访问。
在突发热点事件发生,大量用户上线并集中访问、发表 Feed并且会对部分 Feed 进行超高并发的访问,总体流量增加 1 倍以上,热点数据所在的缓存节点流量增加数倍,此时需要能够快速增加多组 L1从而快速分散这个节点数据的访问。另外在任何一层如果有节点机器故障也需要使用其他机器替代。这样三层 Mc 架构,时常需要进行一些变更。微博的 Mc 架构配置存放在配置中心 config-server 中,由 captain 进行管理。proxy、client 启动时读取并订阅这些配置,在 Mc 部署变更时,可以及时自动切换连接。
Feed 流处理程序访问 Mc 架构时,对于读请求,首先会随机选择一组 L1如果 L1 命中则直接返回,否则读取 Main 层,如果 Main 命中,则首先将 value 回种到 L1然后返回。如果 Main 层也 miss就再读取 slave如果 slave 命中,则回种 Main 和最初选择的那组 L1然后返回。如果 slave 也 miss就从 DB 加载后,回种到各层。这里有一个例外,就是 gets 请求,因为 gets 是为了接下来的 cas 更新服务,而三层 Mc 缓存是以 Main、Backup 为基准,所以 gets 请求直接访问 Main 层,如果 Main 层失败就访问 Backup只要有一层访问获得数据则请求成功。后续 cas 时,将数据更新到对应 Main 或 Backup如果 cas 成功,就把这个 key/value set 到其他各层。
对于数据更新,三层 Mc 缓存架构以 Main-Backup 为基准,即首先更新 Main 层,如果 Main 更新成功,则再写其他三层所有 Mc pool 池。如果 Main 层更新失败,再尝试更新 Backup 池,如果 Backup 池更新成功,再更新其他各层。如果 Main、Backup 都更新失败,则直接返回失败,不更新 L1 层。在数据回种,或者 Main 层更新成功后再更新其他各层时Mc 指令的执行一般采用 noreply 方式,可以更高效的完成多池写操作。
三层 Mc 架构,可以支撑百万级的 QPS 访问,各种场景下命中率高达 99% 以上,是 Feed 流处理程序稳定运行的重要支撑。
对于 Feed 流中的 Redis 存储访问,业务的 Redis 部署基本都采用 1 主多从的方式。同时多个子业务按类型分为 cluster 集群,通过多租户 proxy 进行访问。对于一些数据量很小的业务,还可以共享 Redis 存储进行混合读写。对于一些响应时间敏感的业务基于性能考虑也支持smart client 直接访问 Redis 集群。整个 Redis 集群,由 clusterManager 进行运维、slot 维护及迁移。配置中心记录集群相关的 proxy 部署及 Redis 配置及部署等。这个架构在之前的经典分布式缓存系统课程中有详细介绍,此处不再赘述。
至此,本专栏的全部内容就讲完了,希望你可以在项目中结合所学的知识,融会贯通,也感谢你对本专栏的支持,谢谢。

View File

@ -0,0 +1,41 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
000 开篇词 你的360度人工智能信息助理
你好我是洪亮劼目前在电子商务网站Etsy任数据科学主管很高兴能和你在这里相识也很期待在接下来的时间里通过“AI技术内参”这个专栏和你共同探讨与人工智能有关的话题。
在未来的一年里我会为你讲解人工智能的核心基础介绍顶级学术会议的最新研究成果为广大工程师和数据科学家的个人成长出谋划策也期待能为计划成立和管理数据科学家团队的工程管理领导提供一些意见和建议。我希望“AI技术内参”能够成为你的360度人工智能信息助理帮助你在这个高速发展的领域稳步前行。
那么我为什么愿意来写这么一个专栏呢这让我想起了最近发生的一个片段我在波士顿参加一个有关数据科学的工业界会议会场上一位目前在美国一家时装品牌J.Crew工作的数据科学家和我聊天中间问我如果想在数据科学、人工智能这个领域进阶需要看什么样的资料、需要怎样才能不断学习和进步。我当时发现自己很难为这位数据科学家推荐某一本书、某几篇论文、某一个资料就能够起到这样的作用。
事实上,回想我自己在人工智能这个领域的成长,一个突出的特点就是,需要学习的东西太多、太杂而且很细。
比方说,对于一个人工智能领域的从业人员来说,基础阶段需要系统地学习有关机器学习、概率统计的很多书,还要会使用相关的专业软件以及人工智能框架,然后如果你希望能够在某一个专业领域(比如搜索、推荐、图像技术、语音技术、智能驾驶等)有所发展,还需要阅读这些相关领域的很多技术论文,并且去实践相关的算法模型。
更进一步,要想在技术公司能够真正成长下去,还有很多的工程技巧以及实际经验需要你慢慢习得。这些情况都导致人工智能领域专业人才的培养和成长有很高的门槛。
我自己,以及很多希望能够在这个领域有所发展的朋友,都很急迫地需要有这么一个集中地、有计划地获取信息,获取高质量信息的平台。这让我萌生了自己来写这么一个专栏的想法。
我希望“AI技术内参”这个专栏能够成为你在人工智能领域成长的灯塔当你在茫茫的知识海洋里航行时帮助你快速找到核心的、主干的信息和资源。我希望这个专栏能够成为你在职业发展上的朋友让你对快速发展的行业不再焦虑不再担心自己的知识会落伍不再为如何在日新月异的信息中寻找有价值的学习资料而发愁。
同时,我也希望这个专栏能够为你拓宽视野,让数据科学家、人工智能工程师了解到团队管理者是如何构建一个团队、如何来招聘从业人士的,让数据科学的领导者意识到如何培养数据科学家成长,让你对整个行业的生态系统有一个更加完整的认识。
我为这个专栏精心打磨了三个模块。
第一,我会为你讲解一些经典的人工智能技术。这些技术涵盖搜索、推荐系统、广告系统、图像处理等领域。了解这些经典技术能够让你迅速入门并能为今后的学习打下基础。这部分内容帮助你分析核心的算法模型,并为你进行系统性学习提供纲要和指引。
第二,我会带给你最新的顶级学术会议动态,帮助你了解和掌握这些学术会议最火热和最新的研究成果。每一年和人工智能相关的顶级学术会议有十余个,每个会议都会有上百篇甚至几百篇论文发表。从这些论文和成果中找到有价值的信息,对于初学者,甚至是有一定经验的从业人员来说都是非常困难、也非常耗时的一件事情。那么,在这个专栏里,我会为你精选内容,可以让你不错过任何有价值的最新成果。
第三,我会在这个专栏里为人工智能的从业人员提供指南,帮助数据科学家和工程师提升自我价值,帮助人工智能团队的管理者构建团队,为你在职场发展中的关键步骤出谋划策。
希望我们在今后的一年时间里通过“AI技术内参”这个平台共同学习、共同成长。“AI技术内参”只是一个起点希望你能够从这个专栏出发在人工智能这个领域前行得更好、更高、更远。

View File

@ -0,0 +1,69 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
001 聊聊2017年KDD大会的时间检验奖
国际数据挖掘与知识发现大会ACM SIGKDDACM SIGKDD Conference on Knowledge Discovery and Data Mining简称KDD是由美国计算机协会ACMThe Association for Computing Machinery的数据挖掘与知识发现专委会SIGKDDSpecial Interest Group on Knowledge Discovery and Data Mining主办堪称数据挖掘研究领域的顶级会议。
KDD最早是从1989年开始的KDD 研讨班Workshop发展而来当时的研讨班依托于人工智能顶级会议IJCAI大会或者AAAI大会而后在1995年升级成为会议的模式到现在已经有20多年的历史。今年的KDD大会于8月13日至17日在加拿大哈利法克斯成功召开。
SIGKDD每年都会奖励一篇论文这篇论文要在过去十年间对研究、方法论以及实践产生重大影响这就是所谓的时间检验奖Test of Time Award引用次数以及对一个领域的影响力度是评选这个奖项的重要指标。
2017年的KDD时间检验奖授予了美国康奈尔大学信息科学系主任、计算机科学系教授索斯藤·乔基姆斯Thorsten Joachims。这次授予是为了表彰他的论文《线性时间内训练线性支持向量机》Training Linear SVMs in Linear Time这篇论文也是2006年的KDD最佳论文引用数超过1600多次。
Thorsten的学术贡献
Thorsten是一位机器学习界享有盛誉的学者也是ACM和AAAI的双料院士他所有论文的引用数加起来超过了4万次。2001年从德国多特蒙德大学博士毕业后他正式加入康奈尔大学从事机器学习研究。
获得这个奖项之前Thorsten曾多次获得重要奖项比如2017年ACM WSDM的最佳论文奖Best Paper Award、2016年ACM SIGIR的时间检验奖、2015年ACM KDD的时间检验奖、2009年ECML的最佳论文奖、2009年ICML的10年最佳论文奖Best 10-Year Paper Award、2006年ACM KDD的最佳论文奖、2005年ICML的最佳论文奖、2005年ICML的优秀学生论文奖、2005年ACM KDD的最佳学生论文奖等。
Thorsten在机器学习领域一直有着非常特殊的贡献。首先他在支持向量机SVM的应用上做出了诸多努力。比如这次的时间检验奖就是奖励他如何把支持向量机的训练达到线性复杂度从而使支持向量机在大规模数据上的应用成为可能。
Thorsten还致力于把支持向量机的基本算法也就是仅仅支持分类问题和回归问题的算法应用到更加复杂的有结构的输出结果上俗称结构化的支持向量机算法。得益于这项工作支持向量机可以对信息检索中很多复杂的、非二分的评估指标进行直接优化如F1值F-score、平均精度均值Mean Average Precision从而让支持向量机的应用变得更加广阔。
在让支持向量机能够顺利应用到信息检索的过程中Thorsten还发现了另外一个问题那就是如何利用搜索引擎的间接用户反馈Implicit Feedback来训练排序算法经常是一个结构化的支持向量机模型。具体来说传统的搜索系统和信息检索系统主要是依靠人工标注的训练数据来进行优化和评估。这里所说的人工标注训练数据主要是指人为地评价目标查询关键字和所对应的网页是否相关。
早期大家发现虽然搜索引擎可以利用这样的数据来优化排序算法但是搜索引擎在使用过程中会产生很多用户数据。这些数据可以是用户点击搜索页面结果产生的信息也可以是其他的信息比如用户在搜索页面的驻留时间等等。早期这些信息并没有用于优化搜索引擎。以Thorsten为主的一批学者意识到点击信息的重要性然后开始利用这些数据来训练和评估排序算法。这是Thorsten的第二个主要学术贡献。
Thorsten第三个主要学术贡献也是他最近几年的学术成功那就是把因果推论Causal Inference和机器学习相结合从而能够更加无偏差地训练模型。可以说这部分工作开创了一个新领域。
长期以来,如何有效地应用用户产生的交互数据来进行模型训练,都是大规模机器学习特别是工业界机器学习的难点。一方面,工业系统能够产生很多用户数据;另一方面,这些用户数据又受到当前部署系统的影响,一般都有一定的偏差。
因此工业级机器学习系统面临一个长期挑战,那就是,如何能够在评估模型以及训练模型的时候考虑到这样的偏差,从而去除这样的偏差。
Thorsten利用因果推论中的倾向评分Propensity Scoring技术以及多臂赌博机Multi-armed Bandit思想把这样的方法成功地引入到机器学习中使得无偏差地训练模型成为可能。目前这方面的新研究和新思想正在机器学习以及应用界产生越来越多的共鸣。
线性大规模支持向量机
回到这篇时间检验奖的论文它解决的是大规模优化支持向量机的问题特别是线性支持向量机。这篇文章第一次提出了简单易行的线性支持向量机实现包括对有序回归Ordinal Regression的支持。算法对于分类问题达到了O(sn)其中s是非0的特征数目而n是数据点的个数也就是实现了线性复杂度而对有序回归的问题达到了O(snlog(n))的复杂度。算法本身简单、高效、易于实现并且理论上可以扩展到核函数Kernel的情况。
在此之前,很多线性支持向量机的实现都无法达到线性复杂度 。比如当时的LibSVM台湾国立大学的学者发明、SVM-Torch、以及早期的SVM-Light中采用的分解算法Decomposition Method都只能比较有效地处理大规模的特征。而对于大规模的数据(n)则是超线性Super-Linear的复杂度。
另外的一些方法能够训练复杂度线性地随着训练数据的增长而增长但是却对于特征数N呈现了二次方(N^2)的复杂度。因此之前的这些方法无法应用到大规模的数据上。这样的情况对于有序回归支持向量机更加麻烦。从德国学者拉尔夫·赫布里希Ralf Herbrich提出有序回归支持向量机以来一直需要通过转化为普通的支持向量机的分类问题而求解。这个转换过程需要产生O(n^2)的训练数据,使得整个问题的求解也在这个量级的复杂度。
这篇文章里Thorsten首先做的是对普通的支持向量机算法的模型形式Formalism进行了变形。他把传统的分类支持向量机Classification SVM写成了结构化分类支持向量机Structural Classification SVM并且提供了一个定理来证明两者之间的等价性。粗一看这个等价的结构化分类支持向量机并没有提供更多有价值的信息。然而这个新的优化目标函数的对偶Dual形式由于它特殊的稀疏性使它能够被用来进行大规模训练。紧接着Thorsten又把传统的有序回归支持向量机的优化函数写成了结构化支持向量机的形式并且证明了两者的等价性。
把两种模型表达成结构化向量机的特例之后Thorsten开始把解决结构化向量机的一种算法——切割平面算法Cutting-Plane以下称CP算法运用到了这两种特例上。首先他展示了CP算法在分类问题上的应用。简单说来这个算法就是保持一个工作集合Working Set来存放当前循环时依然被违反的约束条件Constraints然后在下一轮中集中优化这部分工作集合的约束条件。
整个流程开始于一个空的工作集合每一轮优化的是一个基于当前工作集合的支持向量机子问题算法直到所有的约束条件的误差小于一个全局的参数误差为止。Thorsten在文章中详细证明了这个算法的有效性和时间复杂度。相同的方法也使得有序回归支持向量机的算法能够转换成为更加计算有效的优化过程。
Thorsten在文章中做了详尽的实验来展现新算法的有效性。从数据的角度他使用了5个不同的数据集分别是路透社RCV1数据集的好几个子集。数据的大小从6万多数据点到80多万数据点不等特征数也从几十到四万多特征不等这几种不同的数据集还是比较有代表性的。从方法的比较上来说Thorsten主要比较了传统的分解方法。
有两个方面是重点比较的第一就是训练时间。在所有的数据集上这篇文章提出的算法都比传统算法快几个数量级提速达到近100倍。而有序回归的例子中传统算法在所有数据集上都无法得到最后结果。Thorsten进一步展示了训练时间和数据集大小的线性关系从而验证了提出算法在真实数据上的表现。
第二个重要的比较指标是算法的准确度是否有所牺牲。因为有时候算法的提速是在牺牲算法精度的基础上做到的因此验证算法的准确度就很有意义。在这篇文章里Thorsten展示提出的算法精度也就是分类准确度并没有统计意义上的区分度也让这个算法的有效性有了保证。
Thorsten在他的软件包SVM-Perf中实现了这个算法。这个软件包一度成了支持向量机研究和开发的标准工具。
小结
今天我和你分享了Thorsten的这篇论文堪称支持向量机文献史上的经典。一起来回顾下要点第一Thorsten在机器学习领域有三大主要学术贡献第二这篇论文理论论证非常扎实算法清晰而且之后通过有效的实验完全验证了提出算法的有效性。文章开启了支持向量机在搜索领域的广泛应用不愧为2006年的KDD最佳论文以及今年的时间检验奖论文。
最后,给你留一个思考题,在什么应用场景下,线性大规模支持向量机可以有比较好的效果?
扩展阅读Training Linear SVMs in Linear Time

View File

@ -0,0 +1,67 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
004 精读2017年EMNLP最佳长论文之一
自然语言处理实证方法会议EMNLPConference on Empirical Methods in Natural Language Processing是由国际计算语言学协会ACLAssociation for Computational Linguistics的专委会SIGDATSpecial Interest Group on Linguistic Data and Corpus-based Approaches to NLP主办每年召开一次颇具影响力和规模是自然语言处理类的顶级国际会议。从1996年开始举办已经有20多年的历史。2017年的EMNLP大会于9月7日到11日在丹麦的哥本哈根举行。
每年大会都会在众多的学术论文中挑选出两篇最具价值的论文作为最佳长论文Best Long Paper Award。 今天我就带你认真剖析一下EMNLP今年的最佳长论文题目是《男性也喜欢购物使用语料库级别的约束条件减少性别偏见的放大程度》Men Also Like Shopping: Reducing Gender Bias Amplification using Corpus-level Constraints 。这篇文章也是很应景近期学术圈对于数据和机器学习算法有可能带来的“偏见”Bias感到关切有不少学者都在研究如何能对这些偏见进行评估、检测进而可以改进甚至消除。
作者群信息介绍
第一作者赵洁玉Jieyu Zhao论文发表的时候在弗吉尼亚大学计算机系攻读博士学位目前已转学到加州大学洛杉矶分校从事如何从机器学习算法中探测和消除偏见的研究。之前她从北京航空航天大学获得学士和硕士学位曾于2016年在滴滴研究院实习。
第二作者王天露Tianlu Wang也是来自弗吉尼亚大学计算机系的博士生之前在浙江大学获得计算机学士学位。第三作者马克·雅茨卡尔Mark Yatskar是来自华盛顿大学的计算机系博士生已在自然语言处理以及图像处理领域发表过多篇高质量论文。
第四作者文森特Vicente Ordóñez目前在弗吉尼亚大学计算机系任助理教授。他的研究方向是自然语言处理以及计算机视觉的交叉学科。他于2015年从北卡罗来纳大学教堂山分校计算机系博士毕业。博士期间他在微软研究院、eBay研究院以及谷歌都有过实习经历。他是第二作者王天露的博士导师。
文章最后一位作者是Kai-Wei Chang也是第一作者赵洁玉的导师。他目前在加州大学洛杉矶分校任助理教授之前在弗吉尼亚大学任职。他于2015年从伊利诺伊大学香槟分校博士毕业师从著名教授丹·罗斯Dan Roth。在之前的研究生涯中曾先后3次在微软研究院实习也在谷歌研究院实习过。在他研究的早期曾参与了LibLinear这个著名支持向量机软件的研发工作。
论文的主要贡献
机器学习的一个重要任务就是通过数据来学习某些具体事项。最近机器学习的研究人员发现数据中可能蕴含着一些社会赋予的偏见而机器学习算法很有可能会放大这些偏见。这种情况在自然语言处理的相关任务中可能更为明显。比如在一些数据集里“做饭”这个词和“女性”这个词一起出现的比例可能要比和“男性”一起出现的比例高30%经过机器学习算法在这个数据集训练之后这个比例在测试数据集上可能就高达68%了。因此,虽然在数据集里,社会偏见已经有所呈现,但是这种偏见被机器学习算法放大了。
因此这篇文章的核心思想就是如何设计出算法能够消除这种放大的偏见使得机器学习算法能够更加“公平”。注意这里说的是消除放大的偏见而不是追求绝对的平衡。比如我们刚才提到的数据集训练集里已经表现出“女性”和“做饭”一起出现的频率要高于“男性”和“做饭”一起出现的频率。那么算法需要做的是使这个频率不会进一步在测试集里升高也就是说保持之前的30%的差距,而不把这个差距扩大。这篇文章并不是追求把这个差距人为地调整到相同的状态。
文章提出了一个限制优化Constrained Optimization算法为测试数据建立限制条件使机器学习算法的结果在测试集上能够得到和训练集上相似的偏见比例。注意这是对已有测试结果的一个调整Calibration因此可以应用在多种不同的算法上。
作者们使用提出的算法在两个数据集上做了实验得到的结果是新的测试结果不但能够大幅度高达30%至40%)地减小偏见,还能基本保持原来的测试准确度。可见,提出的算法效果显著。
论文的核心方法
那么,作者们提出的究竟是一种什么方法呢?
首先引入了一个叫“偏见值”Bias Score的概念。这个值检测某一个变量和目标变量之间的比例关系。例如“男性”这个词和某个动词比如之前我们举了“做饭”一起出现的比例关系以及“女性”这个词和同一个动词一起出现的比例关系。
注意因为“男性”和“女性”都是“性别”的可选项因此这两个词对于同一个动词的比例关系的和一定是1。偏见值在训练集上和测试集上的差别构成了衡量偏见是否被放大的依据。在之前的例子中“女性”和“做饭”一起出现的的偏见值在训练集上是0.66而到了测试集则变成了0.84,这个偏见被算法放大。
有了偏见值这个概念以后作者们开始为测试集的结果定义限制条件Constraint。这里的一个基本思想就是要对测试集的预测标签进行重新选择使测试标签的预测结果和我们期待的分布相近。用刚才的例子就是说我们要让“女性”在“做饭”这个场景下出现的可能性从0.84回归到0.66附近。能够这么做是因为这个算法需要对测试结果直接进行调整。
对所有的限制条件建模其实就变成了一个经典的限制优化问题。这个问题需要对整个测试数据的预测值进行优化那么这个优化就取决于测试数据集的大小往往是非常困难的。于是作者们在这里采用了拉格朗日简化法Lagrangian Relaxation来对原来的优化问题进行简化。
也就是说,原来的限制优化问题经过拉格朗日简化法后,变成了非限制优化问题,原来的算法就可以成为一个动态更新的过程。针对每一个测试用例,都得到当前最优的标签更改方案,然后又进一步更新拉格朗日参数,这样对整个测试数据集遍历一次后算法就中止了。
方法的实验效果
作者们使用了两个实验数据。一个是imSitu一个是MS-COCO。imSitu是一个视觉语义角色识别Visual Semantic Role Labeling的任务里面有多达12万张图片和这些图片的文字语义信息。比如一些图片是关于做饭场景的里面的角色就是男性或者是女性。作者们整理出了212个动词用作实验。MS-COCO是一个多标签图片分类问题Multi-label Classification需要对80类物品进行标签预测。
对于这两个任务作者们都选择了条件随机场Conditional Random Field来作为基础模型。条件随机场往往是解决往往是解决这类问题方法的方法的第一选择。对于特征作者们采用了数据集提供的基于深度学习的各种特征。在条件随机场的基础上对测试集采用了提出的偏见调整算法。
值得指出的是,虽然算法本身需要使用测试数据,但并不需要知道测试数据的真实标签。标签信息仅仅是从训练集中得到。这一点也是作者们反复强调的。
从两个数据集的结果来看效果都不错。原本的预测准确度并没有很大的降低但是性别偏见值则在测试集的调整结果后大幅度降低最大的结果可以降低40%以上。
小结
今天我为你讲了EMNLP 2017年的年度最佳长论文这篇论文针对数据集可能带来的社会偏见以及机器学习算法可能进一步扩大这种偏见的问题提出了一个对测试数据集的预测结果进行调整的算法。这个算法的核心是减小这种偏见使偏见值在测试数据集中和训练数据集中的水平相当。
一起来回顾下要点:第一,简要介绍了这篇文章的作者群信息。第二,详细介绍了这篇文章要解决的问题以及贡献 。第三,介绍了文章提出方法的的核心内容 。
最后,给你留一个思考题,为什么机器学习算法可能扩大训练集上已有的偏见呢?这跟某些具体的算法有什么关系呢?
拓展阅读Men Also Like Shopping: Reducing Gender Bias Amplification using Corpus-level Constraints

View File

@ -0,0 +1,85 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
005 精读2017年EMNLP最佳长论文之二
EMNLP每年都会选出两篇最佳长论文我们已经分析过第一篇《男性也喜欢购物使用语料库级别的约束条件减少性别偏见的放大程度》。今天我继续来讲第二篇。
EMNLP 2017年最佳长论文的第二篇是《在线论坛中抑郁与自残行为风险评估》Depression and Self-Harm Risk Assessment in Online Forums。这篇文章探讨了利用自然语言处理技术来解决一个社会问题。最近一段时间以来如何利用机器学习、数据科学等技术来解决和处理社会问题正逐渐成为很多社会科学和机器学习研究的交叉领域。
作者群信息介绍
第一作者安德鲁·耶特斯Andrew Yates计算机博士毕业于美国华盛顿的乔治城大学Georgetown Univeristy目前在德国马克思普朗克信息学院Max Planck Institute for Informatics攻读博士后。他在博士阶段已经发表了多篇采用深度学习技术和信息检索、自然语言处理相关的论文。
第二作者阿曼·可汗Arman Cohan来自伊朗是乔治城大学计算机系博士生。阿曼已在信息检索和自然语言处理相关方向发表了多篇论文。2016年在华盛顿的Medstar Health实习并发表了两篇论文。2017年暑假在美国加州圣何塞San Jose的奥多比Adobe研究院实习。
第三作者纳兹利·哥汗Nazli Goharian也来自乔治城大学计算机系目前在系里担任计算机教授。第一作者是他之前的学生第二作者是他当前的学生。纳兹利在长达20年的职业生涯中先后在工业界和学术圈任职可以说有很深厚的学术和工业背景他在信息检索和文本分析领域已发表20多篇论文。
论文的主要贡献
在理解这篇文章的主要贡献之前,我们还是先来弄明白,这篇文章主要解决了一个什么场景下的问题。
现代社会人们生活工作的压力越来越大。研究表明很多人都可能受到各式各样精神疾病Mental Conditions的困扰。在当下发达的互联网时代在线场所为这些精神疾病患者寻求帮助提供了大量的资源和信息特别是一些专业的在线支持社区或是一些更大的在线社区比如Twitter或者Reddit。
因此,研究这些人在各种在线社区的行为,对设计更加符合他们需要的系统有很大帮助。对于很多社会研究人员来说,分析这些人的精神状态,才能更好地帮助他们长期发展。
这篇文章提出了一个比较通用的框架来分析这些精神疾患者的在线行为。在这个框架下可以比较准确地分析发布信息的人是否有自残Self-Harm行为还可以比较容易地分析哪些用户有可能有抑郁症Depression的状况。
整个框架利用了近年来逐渐成熟的深度学习技术对文本进行分析。所以,这里的应用思路很值得借鉴和参考,也可以用于其他场景。
论文的核心方法
在介绍这篇文章提出的方法之前,作者们用不小的篇幅介绍了文章使用的数据集和如何产生数据的标签。
首先作者们从著名的在线社区Reddit中找到和精神疾病有明确联系的帖子。这些帖子是按照一个事先准备的语料库来筛选的这个语料库是为了比较高精度地发现与精神疾病相关的帖子。利用语料库里的句式比如“我已经被诊断得了抑郁症”这样就可以保证找到的帖子在很大程度上是来自精神疾病患者的。
如果一个用户发布了这样的帖子但在这之前发布的帖子少于100条这个用户就不会包含在数据库中。做这样的筛选可能作者们的考虑是太少的帖子无法比较全面地包含用户方方面面的行为。
作者们在Reddit社区中挖掘了从2006年到2016年十年时间里符合条件的所有帖子并利用人工标注的方式筛选出了9210个有精神疾病困扰的用户。这些可以当做机器学习的正例。
那么如何寻找负例呢?作者们当然可以利用所有的用户,但是这样带来的后果很可能是研究没有可比性。如果正例的用户和负例的用户之间差别太大,我们就很难说这些差别是因为精神疾病造成的还是由其他区别带来的。于是,作者们想到的方法则是尽可能地对于每一个正例的用户都找到最接近的负例用户。
实际操作中作者们采取了更加严格的方式那就是负例的用户必须没有发布过任何与精神疾病相关的帖子并且在其他方面都需要和正例用户类似。在这样的条件下作者们找到了107274个负例用户。
对于数据集中的用户而言每个用户平均发布969个帖子平均长度都多于140个字。可以说由这些用户构成的这个数据集也是本文的一个主要贡献这个数据集用于分析抑郁症。
对于自残行为而言作者们利用了一个叫ReachOut的在线社区的数据收集了包括65024个论坛的帖子其中有1227个帖子提到了自残。而对于提及自残的程度数据分了五个等级用于表示不同的紧急情况。
这篇论文主要提出了基于卷积神经网络的文本分析框架,分别用于检测抑郁症用户和检测自残倾向度的两个任务中。虽然这两个任务使用的数据不同,最终采用的模型细节不同,但是两个任务使用的都是同一个框架。下面我就来说一说这个框架的主要思想。
首先,作者们利用每个用户的发帖信息来对每一个用户进行建模,基本的思路是通过神经网络来对用户的每一个帖子建模,从中提取出有效信息,然后把有效信息汇总成用户的一个表达。有了这个思路,我们再来看看具体是怎么做的。
每个帖子一个范围内的单词首先通过卷积层Convolutional Layer提取特征然后提取的特征再经过最大抽取层Max Pooling Layer集中。这个步骤基本上就是把目前图像处理的标准卷积层应用到文本信息上。每一个帖子经过这样的变换就成了特征向量Feature Vector。有了这样的特征向量之后用户的多个特征向量整合到一起根据不同的任务形成用户的整体表征。
在检测抑郁症的任务上,作者们采用的是“平均”的方式,也就是把左右的帖子特征向量直接平均得到。而在检测自残的任务上,作者们则采用了一种比较复杂的形式,把所有的帖子都平铺到一起,然后再把当前帖子之前的帖子,作为负例放在一起,注意,不是平均的形式,而是完全平铺到一起,从而表达为用户的整体特征。
在经过了这样的信息提取之后后面的步骤就是构建分类器。这个步骤其实也是深度学习实践中比较常见的做法那就是利用多层全联通层Fully Connected Layer最终把转换的信息转换到目标的标签上去。
可以说在整体的思路上,作者们提出的方法清晰明了。这里也为我们提供了一种用深度学习模型做文本挖掘的基本模式,那就是用卷积网络提取特征,然后通过联通层学习分类器。
方法的实验效果
作者们在上面提到的实验数据集上做了很充分的实验,当然也对比了不少基本的方法,比如直接采用文本特征然后用支持向量机来做分类器。
在辨别抑郁症的任务上本文提出的方法综合获取了0.51的F1值其中召回Recall达到0.45而直接采用支持向量机的方法精度Precision高达0.72但是召回指数非常低只有0.29。
而在检测自残的任务上提出方法的准确度能够达到0.89F1值达到0.61,都远远高于其他方法。
应该说,从可观的数值上,本文的方法效果不错。
小结
今天我为你讲了EMNLP 2017年的第二篇年度最佳长论文这篇文章介绍了一个采用深度学习模型对论坛文本信息进行分析的应用那就是如何识别有精神疾病的用户的信息。
一起来回顾下要点:第一,我简要介绍了这篇文章的作者群信息。第二,这篇文章是利用自然语言处理技术解决一个社会问题的应用,论文构建的数据集很有价值。第三,文章把目前图像处理的标准卷积层应用到文本信息上,提出了基于卷积神经网络的文本分析框架,用于辨别抑郁症和检测自残倾向,都实现了不错的效果。
最后,给你留一个思考题,如果说在图像信息上采用卷积层是有意义的,那为什么同样的操作对于文本信息也是有效的呢?文本上的卷积操作又有什么物理含义呢?
拓展阅读Depression and Self-Harm Risk Assessment in Online Forums

View File

@ -0,0 +1,89 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
006 精读2017年EMNLP最佳短论文
在今年的EMNLP大会上有两类研究论文得到发表一类是8页的长研究论文主要是比较完整的研究结果另一类是4页的短研究论文主要是比较新的有待进一步推敲的研究结果。大会从长研究论文中选出两篇最佳论文从短论文中选出一篇最佳论文。
前面我们分别讨论了两篇最佳长论文今天我就带你认真剖析一下EMNLP 2017年的最佳短论文《多智能体对话中自然语言并非“自然”出现》Natural Language Does Not Merge Naturally in Multi-Agent Dialog。我们今天讲的论文虽然是最佳短论文但是作者们已经在arXiv发表了较长的文章版本因此我今天的讲解将基于arXiv的长版本。
这篇文章研究的一个主要命题就是多个“机器人”Agent对话中如何才能避免产生“非自然”Unnatural的对话。以前很多机器人对话的研究都关注准确率的高低但实际上机器人产生的对话是不自然的人类交流不会用这样的方式。这篇文章希望探讨的就是这样非自然的对话是如何产生的有没有什么方式避免这样的结果。
作者群信息介绍
第一作者萨特维克·库托儿Satwik Kottur来自卡内基梅隆大学博士第四年研究领域为计算机视觉、自然语言和机器学习。2016年暑假他在Snapchat的研究团队实习研究对话系统中的个性化问题。2017年暑假在Facebook研究院实习做视觉对话系统Visual Dialog System的研究。近两年萨特维克已在多个国际顶级会议如ICML 2017、IJCAI 2017、CVPR 2017、ICCV 2017以及NIPS 2017发表了多篇高质量研究论文包括这篇EMNLP 2017的最佳短论文可以说是一颗冉冉升起的学术新星。
第二作者何塞·毛拉José M. F. Moura是萨特维克在卡内基梅隆大学的导师。何塞是NAE美国国家工程院院士和IEEE电气电子工程师学会院士长期从事信号处理以及大数据、数据科学的研究工作。他当选2018年IEEE总裁负责IEEE下一个阶段的发展。
第三作者斯特凡·李Stefan Lee是来自乔治亚理工大学的研究科学家之前在弗吉尼亚理工大学任职长期从事计算机视觉、自然语言处理等多方面的研究。斯特凡2016年博士毕业于印第安纳大学计算机系。
第四作者德鲁·巴塔Dhruv Batra目前是Facebook研究院的科学家也是乔治亚理工大学的助理教授。德鲁2010年博士毕业于卡内基梅隆大学2010年到2012年在位于芝加哥的丰田理工大学担任研究助理教授2013年到2016年在弗吉尼亚大学任教。德鲁长期从事人工智能特别是视觉系统以及人机交互系统的研究工作。文章的第三作者斯特凡是德鲁长期的研究合作者他们一起已经发表了包括本文在内的多篇高质量论文。
论文的主要贡献
我们先来看看这篇文章主要解决了一个什么场景下的问题。
人工智能的一个核心场景或者说想要实现的一个目标就是能够建立一个目标导向Goal-Driven的自动对话系统Dialog System。具体来说在这样的系统中机器人能够感知它们的环境包括视觉、听觉以及其他感官然后能和人或者其他机器人利用自然语言进行对话从而实现某种目的。
目前对目标导向的自动对话系统的研究主要有两种思路。
一种思路是把整个问题看做静态的监督学习任务Supervised Learning希望利用大量的数据通过神经对话模型Neural Dialog Models来对对话系统进行建模。这个模式虽然在近些年的研究中取得了一些成绩但是仍然很难解决一个大问题那就是产生的“对话”其实不像真人对话不具备真实语言的很多特性。
另外一种思路则把学习对话系统的任务看做一个连续的过程然后用强化学习Reinforcement Learning的模式来对整个对话系统建模。
这篇文章尝试探讨,在什么样的情况下能够让机器人学习到类似人的语言。文章的一个核心发现就是,自然语言并不是自然出现的。在目前的研究状态下,自然语言的出现还是一个没有确定答案的开放问题。可以说,这就是这篇最佳短论文的主要贡献。
论文的核心方法
整篇文章其实是建立在一个虚拟的机器人交互场景里也就是有两个机器人互相对话的一个环境。这个环境里有非常有限的物件Object每个物件包括三种属性颜色、形状和样式每一个属性包括四种可能取值这样在这个虚拟的环境中一共就有64个物件。
交互任务其实是两个机器人进行“猜谜”。为了区分我们把两个机器人分为Q机器人和A机器人。猜谜一开始的时候A机器人得到一个物件也就是三种属性的某种实现组合Q机器人并不知道这个物件。这个时候Q机器人拿到两个属性的名字需要通过对话最终猜出A拿到的这个物件所对应属性的取值。
在这个“游戏”的过程中A是不知道Q手上的两个属性究竟是什么的而Q也不知道A所拿的物件以及物件所对应属性的取值。因此对话就是Q能够取得成功的关键因素。
在这篇文章里Q和A的这个游戏通过强化学习进行建模。Q保持一组参数用于记录当前的状态。这组状态有最开始需要猜的属性以及后面到当前状态为止所有Q的回答以及A的问题。类似地A也保持这么一组状态用于记录到目前位置的信息。这个强化学习最终的回馈是当最后的预测值完全正确时会有一个正1的反馈而错误的话就是负10的反馈。
Q和A的模型都有三个模块听、说和预测。以Q来举例“听”模块是从要猜的属性这个任务开始往后每一个步骤接受A的语句从而更新自己的内部状态。“说”模块是根据当前的内部状态决定下一步需要说的语句。最后“预测”模块则是根据所有的状态预测最后的属性值。
A机器人的结构是对称的。每一个模块本身都是一个 LSTM Long Short-Term Memory长短期记忆模型。当然所有这些LSTM模型的参数是不一样的。整个模型采用了REINFORCE算法也被称作“vanilla” policy gradient“基本”策略梯度来学习参数而具体的实现则采用了PyTorch软件包。
方法的实验效果
在提出的方法上作者们展示了Q均能很快地以比较高的准确度做出预测并且在和A的互动中产生了“语言”。不过遗憾的是通过观察作者们发现这样的“语言”往往并不自然。最直观的一种情况就是A可以忽视掉Q的各种反应而直接把A的内部信息通过某种编码直接“暴露”给Q从而Q可以很快赢得游戏取得几乎完美的预测结果。这显然不是想要的结果。
作者们发现在词汇量Vocabulary非常大的情况下这种情况尤其容易发生那就是A把自己的整个状态都暴露给Q。于是作者们假定要想出现比较有意义的交流词汇数目一定不能过大。
于是作者们采用了限制词汇数目的方式让词汇数目与属性的可能值和属性数目相等这样就限制了在完美情况下交流的复杂度使得A没办法过度交流。然而这样的策略可以很好地对一个属性做出判断但是无法对属性的叠加因为Q最终是要猜两个属性做出判断。
文章给出的一个解决方案是让A机器人忘记过去的状态强行让A机器人学习使用相同的一组状态来表达相同的意思而不是有可能使用新的状态。在这样的限制条件以及无记忆两种约束下A和Q的对话呈现出显著的自然语言的叠加性特征而且在没有出现过的属性上表现出了接近两倍的准确率这是之前的方法所不能达到的效果。
小结
今天我为你讲了EMNLP 2017年的最佳短论文这篇文章介绍了在一个机器人对话系统中如何能让机器人的对话更贴近人之间的行为。
这篇文章也是第一篇从谈话的自然程度,而不是从预测准确度去分析对话系统的论文。文章的一个核心观点是,如果想让对话自然,就必须避免机器人简单地把答案泄露给对方,或者说要避免有过大的词汇库。
一起来回顾下要点:第一,我简要介绍了这篇文章的作者群信息,文章作者在相关领域均发表过多篇高质量研究成果论文。第二,这篇文章论证了多智能体对话中自然语言的出现并不自然。第三,论文提出在词汇量限制条件和无记忆约束下,机器人对话可以呈现出一定的自然语言特征。
最后,给你留一个思考题,文章讲的是一个比较简单的对话场景,有一个局限的词汇库,如果是真实的人与人或者机器与机器的对话,我们如何来确定需要多大的词汇量呢?
名词解释:
ICML 2017International Conference on Machine Learning ,国际机器学习大会。
IJCAI 2017 International Joint Conference on Artificial Intelligence人工智能国际联合大会。
CVPR 2017Conference on Computer Vision and Pattern Recognition国际计算机视觉与模式识别会议。
ICCV 2017International Conference on Computer Vision国际计算机视觉大会。
NIPS 2017Annual Conference on Neural Information Processing Systems神经信息处理系统大会。
拓展阅读Natural Language Does Not Merge Naturally in Multi-Agent Dialog

View File

@ -0,0 +1,73 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
011 精读2017年NIPS最佳研究论文之二KSD测试如何检验两个分布的异同
本周我们来分析和探讨NIPS 2017上的三篇最佳论文。周一我们分享的文章主要研究的是一种“健壮的优化问题”也就是说我们在优化一个“损失函数”的时候不仅要考虑损失函数的“均值”还要考虑损失函数的“方差”。
今天我们来看另外一篇最佳论文《线性时间内核拟合优度测试》A Linear-Time Kernel Goodness-of-Fit Test讲的是如何来衡量一组数据是否来自于某一个分布。
今天的这篇文章理论性也很强,这里我尝试从更高的维度为你做一个归纳,如果对文章内容感兴趣,建议你一定要去阅读原文。
作者群信息介绍
本文一共有五位作者,我们在这里进行一个简要介绍。
第一作者叫维特瓦特·吉特克鲁特Wittawat Jitkrittum刚从伦敦大学学院University College London的“加斯比计算人脑科学所”Gatsby Computational Neuroscience Unit博士毕业。他在博士期间的主要研究是“统计测试”Statistical Tests特别是如何利用“核方法”Kernel Method来对“分布特征”Distributional Features进行测试。吉特克鲁特在泰国完成本科学习于日本京的东京科技学院Tokyo Institute Of Technology获得硕士学位。最近几年吉特克鲁特已经在NIPS、ICML、UAI等会议连续发表了多篇高质量论文可以说是统计测试界的学者新秀。
第二作者许文凯Wenkai Xu是加斯比计算人脑科学所的一名博士生。
第三作者佐尔坦·萨博Zoltán Szabó来自法国一所著名的理工大学“巴黎综合理工学院”École Polytechnique。萨博之前也曾在加斯比计算人脑科学所工作过目前在巴黎综合理工学院任职研究副教授类似于研究员长期从事核方法、信息论Information Theory、统计机器学习等方面的研究。
第四作者福水健次Kenji Fukumizu是“统计数学学院”The Institute of Statistical Mathematics的教授长期从事核方法的研究可以说是这方面的专家。
最后一个作者阿瑟·格里顿Arthur Gretton是加斯比计算人脑科学所的机器学习教授长期从事机器学习特别是核方法的研究。他的论文有9千多次的引用数。
论文的主要贡献和核心方法
我们首先来看一下这篇文章的主要贡献,理解这篇文章主要解决了什么场景下的问题。
在一般的建模场景里我们常常会对一组数据提出一个模型来描述产生这些数据背后的过程。这个过程我们通常是看不见的是一个隐含的过程。那么当我们提出了模型之后如何知道用这个模型描述现实就是准确的呢这时候我们就需要用到一些统计检验Statistical Testing的方法。
一种比较普遍的方法那就是假设我们的模型是P而数据的产生分布是Q。说得直白一些就需要去验证P是不是等于Q也就是需要验证两个分布是否相等。一个基本的做法就是从P里“产生”Generate一组样本或者叫一组数据然后我们已经有了一组从Q里产生的数据于是用“两个样本假设检验”Two Sample Tests来看这两组数据背后的分布是否相等。
这个想法看似无懈可击但是在实际操作中往往充满困难。最大的操作难点就是从P中产生样本。比如P是一个深度神经网络模型那从中产生样本就不是一个简单且计算效率高的流程这就为基于“两个样本假设检验”带来了难度。
另一方面,我们在做这样的统计检验的时候,最好能够针对每一个数据点,得到一个数值,来描述当前数据点和模型之间的关系,从而能够给我们带来更加直观的认识,看模型是否符合数据。
这里有一种叫作“最大均值差别”Maximum Mean Discrepancy或者简称为 MMD 的检验方法能够达到这样的效果。MMD的提出者就是这篇论文的最后一位作者阿瑟·格里顿MMD是在NIPS 2016提出的一个检验两个样本是否来自同一个分布的一种方法。当MMD值大的时候就说明这两个样本更有可能来自不同的分布。
和一般的衡量两个分布距离的方法相比MMD的不同之处是把两个分布都通过核方法转换到了另外一个空间也就是通常所说的“再生核希尔伯特空间”Reproducing Kernel Hilbert Space或者简称为 RKHS。在这个空间里测量会变得更加容易。然而遗憾的是MMD依然需要得到两个分布的样本也就是说我们依然需要从P里得到样本。
那么这篇文章的最大贡献就是使用了一系列的技巧让P和Q的比较不依赖于从P中得到样本从而让数据对于模型的验证仅仅依赖于P的一个所谓的“打分函数”Score Function
其实在MMD里这个打分函数就是存在的那就是针对我们从P或者是Q里抽取出来的样本我们先经过一个函数F的变换然后再经过一个叫“核函数”T的操作最后两个样本转换的结果相减。
在这篇文章里作者们提出了一个叫“核斯特恩差异”Kernel Stein Discrepancy或者叫KSD测试的概念本质上就是希望能够让这两个式子中关于P的项等于零。
什么意思呢刚才我们说了MMD的一个问题是依然要依赖于P依赖于P的样本。假设我们能够让依赖P的样本这一项成为零那么我们这个测试就不需要P的样本了那也就是绕过了刚才所说的难点。
KSD的本质就是让MMD的第二项在任何时候都成为零。注意我们这里所说的是“任何时候”也就是说KSD构造了一个特殊的T这个T叫作“斯特恩运算符”Stein Operator使得第二项关于P的样本的计算在任何函数F的情况下都是零这一点在文章中提供了详细证明。于是整个KSD就不依赖于P的样本了。
这篇文章不仅阐述了KSD的思想而且在KSD的思想上更进了一步试图把KSD的计算复杂度也就是在平方级别的计算复杂度变为线性复杂度。什么意思呢也就是说希望能够让KSD的计算复杂度随着数据点的增加而线性增加从而能够应用到大数据上。这个内容我们就不在这里复述了。
方法的实验效果
虽然这篇文章的核心内容是一个理论结果或者是算法革新文章还是在“受限波兹曼机”Restricted Boltzmann Machine简称RBM上做了实验。本质上就是在RBM的某一个链接上进行了简单的改变而整个模型都保持原样。
如果我们有从这两个RBM中得到的样本其实是很难知道他们之间的区别的。在实验中传统的MMD基本上没法看出这两个样本的差别。然而不管是KSD还是线性的KSD都能够得出正确的结论而最终的线性KSD基本上是随着数据点的增多而性能增加达到了线性的效果。
最后,作者们用了芝加哥犯罪记录来作为说明,使用“打分函数”来形象地找到哪些点不符合模型。应该说,理论性这么强的论文有如此直观的结果,实在难能可贵。
小结
今天我为你讲了NIPS 2017年的另外一篇最佳研究论文文章的一个核心观点是希望能够通过构建一个特殊的运算符使得传统的通过样本来检验两个分布的异同的方法比如MMD方法可以不依赖于目标分布的样本并且还能达到线性计算速度。
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息。第二,我们详细介绍了这篇文章要解决的问题以及贡献 。第三,我们简要地介绍了文章的实验结果 。
最后,给你留一个思考题,这种衡量分布之间距离的想法,除了在假设检验中使用以外,在机器学习的哪个环节也经常碰到?

View File

@ -0,0 +1,65 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
012 精读2017年NIPS最佳研究论文之三如何解决非完美信息博弈问题
今天我们来分享一下NIPS 2017的最后一篇最佳论文《安全和嵌套子博弈解决非完美信息博弈问题》Safe and Nested Subgame Solving for Imperfect-Information Games。这篇文章讲的是什么内容呢讲的是如何解决“非完美信息的博弈”问题。
和前两篇分享的文章类似,这篇文章也是理论性很强,并不适合初学者,我们在这里仅仅对文章的主要思想进行一个高度概括。如果你对文章内容感兴趣,还是建议要阅读原文。
另外一个值得注意的现象是即便在深度学习如日中天的今日我们本周分享的三篇NIPS最佳论文均和深度学习无关。这一方面展现了深度学习并不是人工智能的全部另一方面也让我们看到机器学习和人工智能领域的宽广。
作者群信息介绍
本文一共两位作者。
第一作者叫诺阿·布朗Noam Brown。布朗是卡内基梅隆大学计算机系的博士生目前的主要研究方向是利用强化学习和博弈论的思想来解决大规模的多机器人交互的问题。这篇文章提到的“非完美信息博弈”也是这里面的一个分支问题。布朗已经在这个方向发表了多篇论文包括三篇AAAI论文、两篇NIPS论文、一篇ICML论文、以及一篇IJCAI论文。
和本文非常相关的一个研究内容在2017年发表于《科学》Science杂志上讲述了如何利用博弈论来解决“Heads-up无限制扑克”Heads-up No Limit Poker的问题并且在现实比赛中已经超过了人类的表现。这个工作也得到了不少媒体的报道。布朗2017年也在伦敦的Google DeepMind实习在博士阶段之前他曾经在金融领域工作。
第二作者是布朗的导师托马斯·桑德霍姆Tuomas Sandholm。桑德霍姆是卡内基梅隆大学计算机系的教授其在“机制设计”Mechanism Design以及“拍卖理论”Auction Theory等领域有长期的研究发表了450多篇学术论文并且有超过2万多的引用数。除了他在学术上的造诣以外桑德霍姆还有一些轶事比如他还有非常广泛的兴趣爱好在他的主页就列举了他冲浪、喜好魔术以及对飞行的热爱。
论文的主要贡献和核心方法
我们首先来看一下这篇文章的主要贡献,弄明白这篇文章主要解决了什么场景下的问题。
对于一篇理论性很强的文章来说,我们通常需要不断地提问,这篇文章的核心主旨到底是什么,这样才能够帮助我们了解到文章的主干。
首先,文章讲的是一个“非完美信息的博弈”问题。这是什么意思呢?要理解“非完美信息博弈”,我们就必须要说一下“完美信息博弈”。
简单来说“完美信息博弈”指的是博弈双方对目前的整个博弈状况都完全了解对于博弈之前以及整个博弈时候的初始状态也完全了解。在这种定义下很多大家熟悉的游戏都是“完美信息博弈”比如围棋、象棋等等。那么DeepMind开发的AlphaGo以及后来的AlphaGo Zero都是典型的针对“完美信息博弈”的人工智能算法。
“非完美信息博弈”并不是说我们不知道对方的任何信息而只是说信息不充分。什么意思呢比如我们可能并不知道对手在这一轮里的动作但我们知道对手是谁有可能有怎样的策略或者他们的策略的收益Payoff等。
除了在表面定义上的区别以外,在整个问题的机构上也有不同。
“完美信息博弈”有这样的特征,那就是在某一个时刻的最优策略,往往仅需要在问题决策树当前节点的信息以及下面子树对应的所有信息,而并不需要当前节点之前的信息,以及其他的旁边节点的信息。
什么意思呢比如我们看AlphaGo。本质上在这样“完美信息博弈”的场景中理论上我们可以列出所有的棋盘和棋手博弈的可能性然后用一个决策方案树来表达当前的决策状态。在这样的情况下走到某一个决策状态之后往往我们仅仅需要分析后面的状态。尽管这样的情况数目会非常巨大但是从方法论的角度来说并不需要引用其他的信息来做最优决策。
“非完美信息博弈”的最大特点就正好和这个相反也就是说每一个子问题或者叫子博弈的最佳决策都需要引用其他信息。而实际上本篇论文讲述了一个事实那就是“非完美信息博弈”在任何一个决策点上的决策往往取决于那些根本还没有“达到”Reach的子博弈问题。
在这一点上,论文其实引用了一个“掷硬币的游戏”来说明这个问题。限于篇幅,我们就不重复这个比较复杂的问题设置了,有兴趣的话可以深读论文。
但是从大体上来说,这个“掷硬币的游戏”,其核心就是想展示,两个人玩掷硬币,在回报不同,并且两个人的玩法在游戏规则上有一些关联的情况下,其中某一个玩家总可以根据情况完全改变策略,而如果后手的玩家仅仅依赖观测到先手玩家的回馈来决策,则有可能完全意识不到这种策略的改变,从而选择了并非优化的办法。这里的重点在于先后手的玩家之间因为规则的牵制,导致后手玩家无法观测到整个游戏状态,得到的信息并不能完全反应先手玩家的策略,从而引起误判。
为解决这样博弈问题这篇文章提出的一个核心算法就是根据当前的情况为整个现在的情况进行一个“抽象”Abstraction。这个抽象是一个小版本的博弈情况寄希望这个抽象能够携带足够的信息。然后我们根据这个抽象进行求解当在求解真正的全局信息的时候我们利用这个抽象的解来辅助我们的决策。有时候这个抽象又叫作“蓝图”Blueprint策略。这篇文章的核心在于如何构造这样的蓝图以及如何利用蓝图来进行求解。
方法的实验效果
文章在“Heads-up无限制扑克”的数据集上做了实验并且还比较了之前在《科学》杂志上发表的叫作“利不拉图斯”Libratus的算法版本。人工智能算法都大幅度领先人类的玩家。
有一种算法叫“非安全子博弈算法”Unsafe Subgame Solving也就是说并不考虑“非完美信息的博弈”状态把这个情况当做完美信息来做的一种算法在很多盘游戏中均有不错的表现但是有些时候会有非常差的结果也就是说不能有“健壮”Robust的结果。这里也从实验上证明了为什么需要本文提出的一系列方法。
小结
今天我为你讲了NIPS 2017的第三篇最佳研究论文文章的一个核心观点是希望能够通过构建蓝图来引导我们解决非完美信息博弈的问题特别是在扑克上面的应用。
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息。第二,我们详细介绍了这篇文章要解决的问题以及贡献 。第三,我们简要地介绍了文章的实验结果 。
最后,给你留一个思考题,为什么非完美博弈的整个问题求解现在并没有依靠*深度加强学习*呢,大家在这个问题上有什么直观上的体会呢?

View File

@ -0,0 +1,81 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
013 WSDM 2018论文精读看谷歌团队如何做位置偏差估计
WSDMInternational Conference on Web Search and Data Mining国际搜索和数据挖掘大会是每年举办一次的搜索、数据挖掘以及机器学习的顶级会议其从2008年开始举办已经有11届的历史。
尽管WSDM仅仅举办了11届在计算机科学领域算是一个非常年轻的会议。但是WSDM快速积累的影响力已经使其成为了数据挖掘领域的一个顶级会议。根据谷歌学术搜索公布的数据目前WSDM已经是数据挖掘领域仅次于KDD的学术会议而KDD已经举办了20多年。
WSDM的一大特点就是有大量工业界的学者参与不管是投稿和发表论文还是评审委员会或者大会组织委员会的成员都有很多工业界背景的人员参加。这可能也是WSDM备受关注的一个原因那就是大家对于工业界研究成果的重视同时也希望能够从中学习到最新的经验。
2018年的WSDM大会于2月5日到9日在的美国的洛杉矶举行。今天我们就来分享WSDM 2018上来自谷歌的一篇文章《无偏排序学习在个人搜索中的位置偏差估计》Position Bias Estimation for Unbiased Learning to Rank in Personal Search。这篇文章的核心内容是如何结合“因果推断”Causal Inference和排序学习Learning to Rank来对用户数据进行进一步无偏差的估计。
作者群信息介绍
这篇论文的所有作者都来自谷歌,我们这里对作者群做一个简单的介绍。
第一作者王选珲Xuanhui Wang2015年起在谷歌工作。他之前在Facebook工作了三年一直从事广告系统的开发再往前是在雅虎担任了两年的科学家。王选珲于2009年毕业于伊利诺伊大学香槟分校获得计算机博士学位他的博士生导师是信息检索界著名的华人学者翟成祥Chengxiang Zhai
第二作者纳达夫⋅古尔班迪Nadav Golbandi于2016年加入谷歌之前在雅虎研究院担任了8年的主任级研究工程师Principal Research Engineer一直从事搜索方面的研发工作。在雅虎研究院之前古尔班迪在以色列的IBM研究院工作了6年。他拥有以色列理工大学的计算机硕士学位。
第三作者迈克尔⋅本德斯基Michael Bendersky于2012年加入谷歌一直从事个人以及企业信息系统Google Drive的研发工作。本德斯基于2011年从马萨储塞州阿姆赫斯特分校University of Massachusetts Amherst毕业获得计算机博士学位他的导师是信息检索界的学术权威布鲁斯⋅夸夫特Bruce Croft
第四作者唐纳德⋅梅泽尔Donald Metzler也是2012年加入谷歌的一直负责个人以及企业信息系统Google Drive搜索质量的研发工作。梅泽尔曾在雅虎研究院工作过两年多然后还在南加州大学University of South California担任过教职。梅泽尔是2007年从马萨储塞州阿姆赫斯特分校计算机博士毕业导师也是信息检索界的学术权威布鲁斯⋅夸夫特。
文章的最后一个作者是马克⋅诺瓦克Marc Najork于2014年加入谷歌目前担任研发总监Research Engineering Director的职位。诺瓦克之前在微软研究院硅谷分部工作了13年再之前在DEC研究院工作了8年。诺瓦克是信息检索和互联网数据挖掘领域的学术权威之前担任过ACM顶级学术期刊ACM Transactions on the Web的主编。他发表过很多学术文章引用数在七千以上。
论文的主要贡献
按照我们阅读论文的方法,首先来看这篇文章的主要贡献,梳理清楚这篇文章主要解决了什么场景下的问题。
众所周知所有的搜索系统都会有各种各样的“偏差”Bias如何能够更好地对这些偏差进行建模就成为了对搜索系统进行机器学习的一个重要的挑战。
一种方式就是像传统的信息检索系统一样利用人工来获得“相关度”Relevance的标签不需要通过通过人机交互来获取相关度的信息。所以也就更谈不上估计偏差的问题。
第二种文章中也有谈到的那就是利用传统的“点击模型”Click Model。点击模型是一种专门用来同时估计相关度和偏差的概率图模型在过去10年左右的时间内已经发展得相对比较成熟。文章中也提到大多数点击模型的应用主要是提取相关度信息而并不在乎对偏差的估计是否准确。
第三种也是最近几年兴起的一个新的方向那就是利用“因果推断”Causal Inference和排序学习的结合直接对偏差进行建模。在WSDM 2017的最佳论文[1]中,已经让我们见识了这个思路。然而,在去年的那篇文章里,并没有详细探讨这个偏差的估计和点击模型的关系。
简言之这篇论文主要是希望利用点击模型中的一些思路来更加准确地估计偏差从而能够学习到更好的排序结果。同时这篇文章还探讨了如何能够在较少使用随机数据上来对偏差进行更好的估计。这里作者们提出了一种叫作“基于回归的期望最大化”Regression-based EM算法。
论文的核心方法
文章首先讨论了如果已知“偏差值”Propensity Score也就是用户看到每一个文档或者物品时的概率我们就可以构造“无偏差”的指标比如“无偏差的精度”Unbiased Precision来衡量系统的好坏。
这里无偏差的效果主要是来自于重新对结果进行权重的调整。意思就是说并不是每一个点击都被认为是同样的价值。总的来说如果文档位于比较高的位置上那权重反而会比较低反之如果文档位于比较低的位置上权重反而较高。这里的假设是一种“位置偏差”Position Bias假设。意思就是不管什么文档相对来说放在比较高的位置时都有可能获得更多的点击。因此在较低位置的文档被点击就显得更加难得。
这种情况下,一般都无法直接知道“偏差值”。因此,如何去估计偏差值就成了一个核心问题。
这篇文章在进行“偏差值”估计的方法上首先利用了一个叫“位置偏差模型”Position Bias Model的经典点击模型对偏差值和相关度进行了建模。“位置偏差模型”的假设是用户对于每一个查询关键字的某一个位置上的文档点击概率都可以分解为两个概率的乘积一个是用户看到这个位置的概率一个就是文档本身相关度的概率。那么位置偏差模型的主要工作就是估计这两个概率值。
如果我们能够对每一个查询关键字的结果进行随机化,那么,我们就不需要估计第一个概率,而可以直接利用文档的点击率来估计文档的相关度。但是,作者们展示了,彻底的随机化对于用户体验的影响。
另外一种方法,相对来说比较照顾用户体验,那就是不对所有的结果进行随机化,而仅仅针对不同的“配对”之间进行随机化。比如,排位第一的和第二的文档位置随机互换,然后第二的和第三的随机互换等等。在这样的结果下,作者们依然能够对偏差和相关度进行估计,不过用户的体验就要比第一种完全随机的要好。只不过,在现实中,这种方法依然会对用户体验有所损失。
于是,作者们提出了第三种方法,那就是直接对位置偏差模型进行参数估计。也就是说,不希望利用随机化来完全消除其中的位置概率,而是估计位置概率和相关度概率。
这里因为有两个概率变量需要估计于是作者利用了传统的“期望最大化”EM算法并且提出了一种叫做“基于回归的期望最大化”的方法。为什么这么做呢原因是在传统的期望最大化中作者们必须对每一个关键字和文档的配对进行估计。然而在用户数据中这样的配对其实可能非常有限会陷入数据不足的情况。因此作者们提出了利用一个回归模型来估计文档和查询关键字的相关度。也就是说借助期望最大化来估计位置偏差借助回归模型来估计相关度。
方法的实验效果
这篇文章使用了谷歌的邮件和文件存储的搜索数据采用了2017年4月两个星期的日志。数据大约有四百万个查询关键字每个关键字大约有五个结果。作者们在这个数据集上验证了提出的方法能够更加有效地捕捉文档的偏差。利用了这种方法训练的排序模型比没有考虑偏差的模型要好出1%2%。
小结
今天我为你讲了WSDM 2018年的一篇来自谷歌团队的文章这篇文章介绍了如何估计文档的位置偏差然后训练出更加有效的排序算法。
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息;第二,我们详细介绍了这篇文章要解决的问题以及贡献;第三,我们简要地介绍了文章提出方法的核心内容 。
最后,给你留一个思考题,如果要估计位置偏差,对数据的随机性有没有要求?
参考文献
Thorsten Joachims, Adith Swaminathan, and Tobias Schnabel. Unbiased Learning-to-Rank with Biased Feedback. Proceedings of the Tenth ACM International Conference on Web Search and Data Mining (WSDM 17). ACM, New York, NY, USA, 781-789, 2017.

View File

@ -0,0 +1,73 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
014 WSDM 2018论文精读看京东团队如何挖掘商品的替代信息和互补信息
本周我们来精读WSDM的几篇论文周一我们分享了一篇来自谷歌团队的文章其核心是利用点击模型来对位置偏差进行更加有效的估计从而能够学习到更好的排序算法。
今天我们来介绍WSDM 2018的最佳学生论文《电子商务中可替代和互补产品的路径约束框架》A Path-constrained Framework for Discriminating Substitutable and Complementary Products in E-commerce这篇文章来自于京东的数据科学实验室。
作者群信息介绍
这篇论文的所有作者都来自京东大数据实验室,我们这里对几位主要作者做一个简单介绍。
第三作者任昭春Zhaochun Ren目前在京东数据科学实验室担任高级研发经理。他于2016年毕业于荷兰阿姆斯特丹大学获得计算机博士学位师从著名的信息检索权威马丁⋅德里杰克Maarten de Rijke。任昭春已经在多个国际会议和期刊上发表了多篇关于信息检索、文字归纳总结、推荐系统等多方面的论文。
第四作者汤继良Jiliang Tang目前是密歇根州立大学的助理教授。汤继良于2015年从亚利桑那州立大学毕业获得计算机博士学位师从著名的数据挖掘专家刘欢Huan Liu教授。他于2016年加入密歇根州立大学这之前是雅虎研究院的科学家。汤继良是最近数据挖掘领域升起的一颗华人学术新星目前他已经发表了70多篇论文并且有四千多次的引用。
最后一位作者殷大伟Dawei Yin目前是京东数据科学实验室的高级总监。2016年加入京东之前在雅虎研究院工作历任研究科学家和高级经理等职务。殷大伟2013年从里海大学Lehigh University获得计算机博士学位师从信息检索领域的专家戴维森Davison教授。目前已经有很多高质量的研究工作发表。殷大伟和笔者是博士期间的实验室同学以及在雅虎研究院期间的同事。
论文的主要贡献
我们首先来看一下这篇文章的主要贡献,梳理清楚文章主要解决了一个什么场景下的问题。
对于工业级商品推荐系统而言,一般通过两个步骤来产生推荐结果。第一步,产生候选集合,这里主要是从海量的物品中选择出几百到几千款用户可能会购买的商品;第二步,利用复杂的机器学习模型来对所有候选集中的产品进行排序。
这篇文章主要探讨了如何能够更好地产生候选集产品即如何更好地产生“替代品”Substitutes和“互补品”Complements来丰富用户的购买体验。
那么,什么是替代品和互补品呢?
根据这篇文章的定义,替代品就是用户觉得这些商品可以互相被替换的;而互补品则是用户会一起购买的。挖掘这些商品不仅对于产生候选集具有很重要的意义,也对于某些场景下的推荐结果有很好的帮助,比如当用户已经购买了某一件商品之后,给用户推荐其他的互补品。
虽然替代品和互补品对于互联网电商来说是很重要的推荐源但并没有多少文献和已知方法来对这两类商品进行有效挖掘。而且这里面一个很大的问题是数据的“稀缺”Sparse问题。因为替代品或者互补品都牵扯至少两个商品而对于巨型的商品库来说绝大多数的商品都不是两个商品一起被同时考虑和购买过因此如何解决数据的稀缺问题是一大难点。
另一方面,商品的属性是复杂的。同一款商品有可能在某些情况下是替代品,而在另外的情况下是互补品。因此,如何在一个复杂的用户行为链路中挖掘出商品的属性,就成为了一个难题。很多传统方法都是静态地看待这个问题,并不能很好地挖掘出所有商品的潜力。
归纳起来这篇文章有两个重要贡献。第一作者们提出了一种“多关系”Multi-Relation学习的框架来挖掘替代品和互补品。第二为了解决数据的稀缺问题两种“路径约束”Path Constraints被用于区别替代品和互补品。作者们在实际的数据中验证了这两个新想法的作用。
论文的核心方法
文章提出方法的第一步是通过关系来学习商品的表征Representation。这里文章并没有要区分替代品和互补品。表征的学习主要是用一个类似Word2Vec的方式来达到的。
也就是说商品之间如果有联系不管是替代关系还是互补关系都认为是正相关而其他的所有商品都认为是负相关。于是我们就可以通过Word2Vec的思想来学习商品的表征向量使得所有正相关的商品之间的向量点积结果较高而负相关的向量点积结果较低。这一步基本上是Word2Vec在商品集合上的一个应用。
通过第一步得到的每个商品的表征是一个比较笼统的综合的表征。而我们之前已经提到了那就是不同的情况下商品可能呈现出不同的属性。因此我们就需要根据不同的场景来刻画产品的不同表征。文章采用的方法是对于不同类型的关系每个商品都有一个对应的表征。这个关系特定的表征是从刚才我们学到的全局表征“投影”Project到特定关系上的这里需要学习的就是一个投影的向量。
第三个步骤就是挖掘替代关系和互补关系了。这篇文章使用了一个不太常见的技术用“模糊逻辑”Fuzzy Logic来表达商品之间的约束关系。在这里我们并不需要对模糊逻辑有完整的理解只需要知道这是一种把“硬逻辑关系”Hard Constraints转换成为通过概率方法表达的“软逻辑关系”Soft Constraints的技术。
在这篇文章里,作者们重点介绍的是如何利用一系列的规则来解决数据稀缺的问题。具体来说,那就是利用一些人们对于替代关系或者互补关系的观察。
比如商品A是商品B的替代品那很可能商品A所在的类别就是商品B所在类别的替代品。再比如商品B是商品A的替代品而商品C又是商品B的替代品而如果A、B和C都属于一个类别那么我们也可以认为商品C是A的替代品。
总之,作者们人工地提出了这样一系列的规则,或者叫做约束关系,希望能够使用这样的约束关系来尽可能地最大化现有数据的影响力。当然,我们可以看到,这样的约束并不是百分之百正确的,这也就是作者们希望用“软逻辑关系”来进行约束的原因,因为这其实也是一个概率的问题。
整个提出的模型最终是一个集大成的优化目标函数,也就是最开始的物品的综合表征,在特定的关系下的投影的学习,以及最后的软逻辑关系的学习,这三个组件共同组成了最后的优化目标。
方法的实验效果
这篇文章使用了京东商城的五大类商品来做实验,商品的综述大大超过之前亚马逊的一个公开数据的数量。作者重点比较了之前的一个来自加州大学圣地亚哥团队的模型,以及几个矩阵分解的经典模型,还比较了一个基于协同过滤的模型。
从总的效果上来看,这篇文章提出的模型不管是在关系预测的子任务上,还是在最后的排序任务上均要大幅度地好于其他模型。同时,作者们也展示了逻辑关系的确能够帮助目标函数把替代关系和互补关系的商品区分开来。
小结
今天我为你讲了WSDM 2018年的一篇来自京东数据科学团队的文章这篇文章介绍了如何利用多关系学习以及模糊逻辑来挖掘商品的替代信息和互补信息然后训练出更加有效的排序算法。
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息;第二,我们详细介绍了这篇文章要解决的问题以及贡献 ;第三,我们简要地介绍了文章提出方法的核心内容以及实验的结果。
最后,给你留一个思考题,互补商品或者替代商品是双向关系还是单向关系,为什么呢?

View File

@ -0,0 +1,53 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
017 The Web 2018论文精读如何改进经典的推荐算法BPR
今天我们来看万维网大会上的一篇优秀短论文。在万维网大会上主要发表两类论文。一类是10页的长论文一类是2页的短论文或称作展板论文。短论文主要是发表短小的成果或者是还在研究过程中的重要成果。每一届的万维网大会都会评选出一篇最佳短论文奖。
今天我和你分享的论文题目是《利用查看数据贝叶斯个性化排序的一种改进的取样器》An Improved Sampler for Bayesian Personalized Ranking by Leveraging View Data。这篇论文也有六位作者和我们介绍的上一篇论文一样都来自清华大学和新加坡国立大学。
贝叶斯个性化排序
要想理解这篇论文的内容我们必须要讲一下什么是“贝叶斯个性化排序”Bayesian Personalized Ranking或者简称是BPR。有关BPR的详细介绍可以阅读参考文献[1]。我们在这里仅对BPR进行一个高维度的总结。
简单来说BPR是推荐系统中的一个配对排序Pairwise学习算法。在我们前面介绍搜索算法的时候曾经提到了各种配对排序学习算法。配对排序学习不是针对每一个数据实例来学习其标签或者响应变量而是学习一个相对的顺序希望能够把所有的正例都排列到负例之前。也就是说对于配对排序来说每一个数据实例的预测值本身并不重要排序算法在意的是对于一正一负的一个配对来说是否能够把正例给准确地排列到负例之上。这其实就要求BPR在数值上对正例的预测值能够比负例的预测值高。
BPR主要是解决了在推荐系统中长期以来只对单个数据点进行预测比如需要对用户物品的喜好矩阵建模的时候之前的大多数算法都无法有效地对没有观测到的数据进行建模。而BPR是配对算法因此我们只需要关注观测的数据以及他们之间的关系从而能够对用户的喜好特别是有“隐反馈”Implicit Feedback数据的时候取得更加明显的效果。这里的隐反馈指的并不是用户告诉系统其对每一个物品的喜好程度而是用户在和系统的交互过程中通过一些行为表达出的喜好。这些用户的行为往往并不全面因此需要算法和模型能够对这些行为进行有效建模。
论文的主要贡献和核心方法
了解了BPR大概是怎么回事以后我们来看一看这篇论文的主要贡献和核心方法。
首先我们刚才讲到BPR的核心是学习一个配对的排序问题。那么在训练的时候我们需要对一个正例和一个负例的配对进行学习更新参数。然而在一个自然的用户隐反馈数据集里正例相对来说往往是少数负例则是绝大多数。因此一个传统的方法就是在组成一个配对的时候相对于一个正例来说我们都“均匀地”Uniformly选取负样本来组成配对这个过程有时候也叫“采样”Sampling
这篇论文有两个主要贡献。第一个贡献是作者们发现如果在全局均匀地采样负样本第一没有必要第二可能反而会影响最后学习的效果。第二个贡献是针对电子商务的应用作者们发明了一种负样本采样的方法使得学习算法可以利用到更多的用户“浏览”View信息从而能够对算法的整体训练效果有大幅度的提升。
方法的实验效果
这篇论文的数据集分别使用了母婴产品“贝贝网”和天猫的数据。其中贝贝网有约16万用户、12万商品、260万次购买和4600万次浏览天猫的数据则有3万用户、3万多商品、46万次购买和150多万次浏览。两个数据集都呈现了大于99%的“稀疏度”Sparsity
首先,作者们实验了不从全局中选取负样本而仅仅采样一部分,而且是相比于原来的空间非常小的样本,比如仅仅几百个负样本而不是几万个的情况。实验效果在贝贝网上不仅没有影响算法的精确度,算法的精确度反而还有提升。而在天猫的数据集上,算法效果没有提升,而有一些小幅度的下降,但是作者们认为这样的代价还是值得的,因为数据集的减少,算法的训练时间会大幅度降低。从这个实验中,作者们得出了不需要从全局进行采样的结论。
紧接着作者们提出了一个新的概念那就是对用户的数据集合进行划分把用户的行为分为“购买集”C1、“浏览但没有购买集”C2、“剩下的数据”C3这三个集合。作者们提出BPR要想能够达到最好的效果需要对这三种数据集进行采样。也就是说我们需要组成C1和C2、C1和C3以及C2和C3的配对来学习。
具体来说用户在贝贝网和天猫的数据中尝试了不同的比例来对这三种集合进行采样。总体的经验都是C3中采样的数据要大于C2中的然后要大于C1中的。这其实就是说训练算法要更好地学习到用户不喜欢某件东西的偏好。采用这样的采样方式作者们展示了模型的效果比传统的BPR或仅仅使用“最流行的物品”作为推荐结果要好60%左右。
小结
今天我为你讲了今年万维网大会的一篇优秀短论文。文章介绍了如何对一个经典的推荐算法BPR进行改进从而提高效率并且大幅度提升算法有效度。
一起来回顾下要点第一我们从高维度介绍了BPR的含义第二我们简要介绍了论文的主要贡献和思路第三我们简单分享了论文的实验成果。
最后,给你留一个思考题,除了这篇论文提出的组成正例和负例的配对思路以外,你能不能想到在用户浏览网站的时候,还有哪些信息可以帮助我们组成更多的配对呢?
参考文献
Steffen Rendle, Christoph Freudenthaler, Zeno Gantner, and Lars Schmidt-Thieme. BPR: Bayesian personalized ranking from implicit feedback. Proceedings of the Twenty-Fifth Conference on Uncertainty in Artificial Intelligence (UAI 09). AUAI Press, Arlington, Virginia, United States, 452-461, 2009.

View File

@ -0,0 +1,53 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
018 The Web 2018论文精读如何从文本中提取高元关系
今天我们来看万维网大会上的一篇优秀短论文。在万维网大会上主要发表两类论文。一类是10页的长论文一类是2页的短论文或称作展板论文。短论文主要是发表短小的成果或者是还在研究过程中的重要成果。每一届的万维网大会都会评选出一篇最佳短论文奖。
今天我和你分享的论文题目是《利用查看数据贝叶斯个性化排序的一种改进的取样器》An Improved Sampler for Bayesian Personalized Ranking by Leveraging View Data。这篇论文也有六位作者和我们介绍的上一篇论文一样都来自清华大学和新加坡国立大学。
贝叶斯个性化排序
要想理解这篇论文的内容我们必须要讲一下什么是“贝叶斯个性化排序”Bayesian Personalized Ranking或者简称是BPR。有关BPR的详细介绍可以阅读参考文献[1]。我们在这里仅对BPR进行一个高维度的总结。
简单来说BPR是推荐系统中的一个配对排序Pairwise学习算法。在我们前面介绍搜索算法的时候曾经提到了各种配对排序学习算法。配对排序学习不是针对每一个数据实例来学习其标签或者响应变量而是学习一个相对的顺序希望能够把所有的正例都排列到负例之前。也就是说对于配对排序来说每一个数据实例的预测值本身并不重要排序算法在意的是对于一正一负的一个配对来说是否能够把正例给准确地排列到负例之上。这其实就要求BPR在数值上对正例的预测值能够比负例的预测值高。
BPR主要是解决了在推荐系统中长期以来只对单个数据点进行预测比如需要对用户物品的喜好矩阵建模的时候之前的大多数算法都无法有效地对没有观测到的数据进行建模。而BPR是配对算法因此我们只需要关注观测的数据以及他们之间的关系从而能够对用户的喜好特别是有“隐反馈”Implicit Feedback数据的时候取得更加明显的效果。这里的隐反馈指的并不是用户告诉系统其对每一个物品的喜好程度而是用户在和系统的交互过程中通过一些行为表达出的喜好。这些用户的行为往往并不全面因此需要算法和模型能够对这些行为进行有效建模。
论文的主要贡献和核心方法
了解了BPR大概是怎么回事以后我们来看一看这篇论文的主要贡献和核心方法。
首先我们刚才讲到BPR的核心是学习一个配对的排序问题。那么在训练的时候我们需要对一个正例和一个负例的配对进行学习更新参数。然而在一个自然的用户隐反馈数据集里正例相对来说往往是少数负例则是绝大多数。因此一个传统的方法就是在组成一个配对的时候相对于一个正例来说我们都“均匀地”Uniformly选取负样本来组成配对这个过程有时候也叫“采样”Sampling
这篇论文有两个主要贡献。第一个贡献是作者们发现如果在全局均匀地采样负样本第一没有必要第二可能反而会影响最后学习的效果。第二个贡献是针对电子商务的应用作者们发明了一种负样本采样的方法使得学习算法可以利用到更多的用户“浏览”View信息从而能够对算法的整体训练效果有大幅度的提升。
方法的实验效果
这篇论文的数据集分别使用了母婴产品“贝贝网”和天猫的数据。其中贝贝网有约16万用户、12万商品、260万次购买和4600万次浏览天猫的数据则有3万用户、3万多商品、46万次购买和150多万次浏览。两个数据集都呈现了大于99%的“稀疏度”Sparsity
首先,作者们实验了不从全局中选取负样本而仅仅采样一部分,而且是相比于原来的空间非常小的样本,比如仅仅几百个负样本而不是几万个的情况。实验效果在贝贝网上不仅没有影响算法的精确度,算法的精确度反而还有提升。而在天猫的数据集上,算法效果没有提升,而有一些小幅度的下降,但是作者们认为这样的代价还是值得的,因为数据集的减少,算法的训练时间会大幅度降低。从这个实验中,作者们得出了不需要从全局进行采样的结论。
紧接着作者们提出了一个新的概念那就是对用户的数据集合进行划分把用户的行为分为“购买集”C1、“浏览但没有购买集”C2、“剩下的数据”C3这三个集合。作者们提出BPR要想能够达到最好的效果需要对这三种数据集进行采样。也就是说我们需要组成C1和C2、C1和C3以及C2和C3的配对来学习。
具体来说用户在贝贝网和天猫的数据中尝试了不同的比例来对这三种集合进行采样。总体的经验都是C3中采样的数据要大于C2中的然后要大于C1中的。这其实就是说训练算法要更好地学习到用户不喜欢某件东西的偏好。采用这样的采样方式作者们展示了模型的效果比传统的BPR或仅仅使用“最流行的物品”作为推荐结果要好60%左右。
小结
今天我为你讲了今年万维网大会的一篇优秀短论文。文章介绍了如何对一个经典的推荐算法BPR进行改进从而提高效率并且大幅度提升算法有效度。
一起来回顾下要点第一我们从高维度介绍了BPR的含义第二我们简要介绍了论文的主要贡献和思路第三我们简单分享了论文的实验成果。
最后,给你留一个思考题,除了这篇论文提出的组成正例和负例的配对思路以外,你能不能想到在用户浏览网站的时候,还有哪些信息可以帮助我们组成更多的配对呢?
参考文献
Steffen Rendle, Christoph Freudenthaler, Zeno Gantner, and Lars Schmidt-Thieme. BPR: Bayesian personalized ranking from implicit feedback. Proceedings of the Twenty-Fifth Conference on Uncertainty in Artificial Intelligence (UAI 09). AUAI Press, Arlington, Virginia, United States, 452-461, 2009.

View File

@ -0,0 +1,73 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
019 SIGIR 2018论文精读偏差和流行度之间的关系
2018年的SIGIR国际信息检索研究与发展大会于7月8日~12日在美国密歇根州的安娜堡举行。从今天开始我将精选几篇大会上最有价值的论文和你一起来读。
我先简单介绍一下这个大会。SIGIR从1978年开始举办有40年的历史是信息检索和搜索领域的顶级会议。SIGIR 2018全称是The 41st International ACM SIGIR Conference on Research and Development in Information Retrieval。
从最初举办开始,这个会议就成为了信息检索领域,特别是搜索技术和推荐技术方面的权威学术会议。会议的内容往往包含了搜索、推荐、广告、信息提取、互联网数据挖掘等诸多领域的优秀论文,每年都吸引着来自世界各地的学者和工程师参会,来分享他们最新的研究成果。
今天我们首先来看一看今年的最佳论文标题是《推荐系统中流行度有效性的概率分析》Should I Follow the Crowd? A Probabilistic Analysis of the Effectiveness of Popularity in Recommender Systems
这篇论文一共有两位作者均来自马德里自治大学Universidad Autónoma de Madrid。第一作者罗西奥·卡纳马雷斯Rocio Cañamares已经发表了好几篇相关主题的论文第二作者帕布罗·卡斯蒂罗斯Pablo Castells是马德里自治大学、甚至是整个欧洲的信息检索学术权威。论文有超过5千次的引用。
论文的主要贡献
想要理解清楚这篇论文的主要贡献我们首先要从推荐系统或者是从更大的方向上来看所有信息检索系统都存在的一个核心问题那就是“偏差”Bias。偏差会带来一系列问题。这对推荐系统甚至信息检索系统的建模和评价都带来了巨大的挑战。
那么,为什么信息检索系统会有偏差呢?
我这里举一个简单的例子来说明。假设我们有两个物品和很多用户。对于每一个用户来说,系统都按照随机的顺序,分别给用户展示这两个物品,并且询问用户是否喜欢。
在这样的假设里,顺序是随机的,因此对于同一个用户来说,用户是否喜欢某一个商品,就完全是取决于这个物品本身的属性。对于所有用户来说,在整体上呈现的用户对这两个物品的喜好,则完全来自于大家对这两个物品本身的一种评价。那么,我们可以看到这里面没有任何的偏差。
然而,只要这个场景稍微有一些改变,就很容易引入各种偏差。比如,我们有超过一万件物品。尽管我们还是随机地展示给用户,但用户可能在看过一定数量的物品之后就慢慢厌倦了,那么,用户对于物品的喜好判断或许就会受到厌倦的影响,甚至,用户还很有可能直接放弃查看后面的物品。
还有很多相似的情况,比如我们不是把每个商品逐一展示给用户看,而是提供一个列表。那么,用户很有可能会以为这个列表有一定的顺序,比如在列表排名上方的物品可能是比较重要的。有研究表明,在有列表的情况下,用户很可能会按照列表的顺序提供某种喜好判断。很明显,在这样的情况下,用户的喜好判断就受到了这个列表顺序的干扰。
上面我们提到的都是“表现偏差”Presentation Bias。除此以外一个信息系统其实还有很多类型的偏差比如系统性偏差一个新闻系统只给用户推荐娱乐新闻而不给用户看时政新闻在这样的情况下用户表现出来的喜好性就是有偏差的因为系统没有给用户表达对时政新闻喜好的可能性。
信息检索和推荐系统的学者其实很早就意识到了偏差对于建模的影响。不管是我们这里提到的表现偏差还是系统性偏差,如果我们直接利用用户和系统交互产生的数据,那么训练出来的模型以及我们采用的衡量模型的办法也会有偏差,那我们得出的结论有可能就是不精准的。
这篇论文就是希望能够系统性地讨论偏差在推荐系统中所带来的问题。具体来说这篇论文主要是探讨偏差和“流行度”Popularity之间的关系。
这里描述的是这样一种情况:有一些物品很有可能曾经给很多人推荐过,或者同时还被很多人喜欢过或者评价过,那么,这种流行度高的物品会不会对推荐结果的评价带来意想不到的偏差呢?
在过去的研究中,大家只是对这种流行度高的物品有一种直观上的怀疑,认为如果一个推荐系统仅仅能够推荐流行的物品,那肯定是有偏差的。但之前的很多工作并没有定量地去解释这里面偏差和评价之间的关系。这篇论文就提供了一个理论框架,指导我们去理解偏差以及偏差带来的评测指标的一些变化。
论文的核心方法
今天我们不去讲这篇论文的理论框架细节,我会重点提供一个大体的思路,帮助你理解这篇论文希望达到的目的。
简单来说,为了表达偏差和流行度之间的关系,作者们用了这么几个随机变量:用户是否对某个物品打分,用户是否对某个物品有喜好,以及用户是否观看某个物品。这里面的一个细节,或者说技巧,就是如何用概率的语言把这三者之间的关系给表达清楚。
作者其实采用了一些简化的假设,比如假设在测试集上的物品是训练集上没有出现过的等等。这样,就能够写出在测试集上用户对物品评价的一个期望关系,这个期望关系包含用户对所有测试物品是否有喜好。有了这层期望关系以后,就开始推导出,在测试集上理想状态下的最佳排序是一个什么样子。在这里的理论讨论其实并没有很大的现实意义,但是这是第一次研究人员用数学模型去详细表征一个最优的在测试集上的按照流行度排序的结论。
紧接着,作者们还讨论了这个最优排序在两种极端情况下的变化。一种情况是用户过往的行为都是仅依赖于物品本身的属性,而没有任何其他偏差。另外一种情况是用户过往的行为和物品本身的属性无关,意思就是仅依赖于其他的偏差。
在第一种极端情况下,最优的排序其实也就是我们所能观测到的最优排序,那就是按照物品的流行度。在第二种极端情况下,最优的排序其实是按照平均打分。
当然,你可能会说讨论这两种极端情况并没有现实意义呀。但这两种极端情况的讨论其实就证明了,只有在没有偏差的情况下,按照物品的流行度排序才是平均情况下最优的。而很明显,现实存在偏差,因此依靠流行度的排序,即便是平均情况下,也不是最优的选择。
然后,论文讨论了用户是否观看某一个物品对用户行为的影响。关于这一部分的讨论,其实之前已经有很多工作都做了一些类似的探索。不过这篇论文得出了一个有意思的结论。在考虑了用户观看物品的偏差以后,通过模拟的方法,我们会发现:随机结果的效果其实要比之前的观测值要好很多,而按照流行度的排序虽然不错,但是比随机的效果并没有好很多,而基于平均打分的结果其实要优于按照流行度的排序。可以说,这是一个有别于之前很多工作的新发现。
延申讨论
虽然这篇论文获得了SIGIR 2018的最佳论文奖但是如果我们站在更大的角度上来分析这篇论文其实就会发现作实际上作者们是开发了一套特有的理论框架来描述推荐系统中的某一种偏差。更加普适化的对偏差的建模其实需要有随机化的数据以及利用因果推断的办法来对任意情况下的偏差进行分析。文章提出的概率模型仅仅在这篇文章讨论的假设情况下才能成立。
当然,瑕不掩瑜,这篇文章不管是从结论上,还是从实际的分析过程中,都为我们提供了很多有意义的内容,帮我们去思考偏差对于建模所带来的挑战以及我们应该如何应对。
总结
今天我为你讲了今年SIGIR 2018的最佳论文。
一起来回顾下要点:第一,我们详细介绍了这篇文章要解决的问题和贡献,探讨偏差和流行度之间的关系,系统性地来讨论偏差在推荐系统中所带来的问题;第二,我们简要地介绍了文章提出方法的核心内容,包括设定随机变量、期望关系以及推导理想状态下的最佳排序;第三,针对论文我们简单进行了讨论。
最后,给你留一个思考题,在不考虑偏差的情况下,为什么一般的推荐系统会偏好于推荐流行物品的算法呢?

View File

@ -0,0 +1,71 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
024 CVPR 2018论文精读如何解决排序学习计算复杂度高这个问题
今天我们来看这次大会的一篇最佳论文提名标题是《基于排序的损失函数的有效优化》Efficient Optimization for Rank-based Loss Functions
还是先简单介绍下论文的作者群。这篇论文的作者来自好几个不同的学术机构。
第一作者普里迪什·莫哈帕德拉Pritish Mohapatra是印度海得拉巴的国际信息科技大学International Institute of Information TechnologyHyderabad的计算机科学博士生。他已经在NIPS、CVPR、ICCV、AISTATS等国际机器学习权威会议上发表了多篇论文。
第二作者米卡尔·罗莱内克Michal Rolinek来自德国的马克思普朗克智能系统大学Max Planck Institute for Intelligent Systems博士后研究员。在这篇论文中第一作者和第二作者的贡献相当。
第三作者贾瓦哈C.V. Jawahar是来自印度国际信息科技学院的教授。他是第一作者莫哈帕德拉的博士生导师。
第四作者弗拉迪米尔·科莫格罗夫Vladimir Kolmogorov是奥地利科技大学Institute of Science and Technology Austria的机器学习教授。
最后一个作者帕万·库玛M. Pawan Kumar来自牛津大学。
论文的主要贡献
这篇论文提出了一个针对排序学习中基于整个排序的损失函数的快速优化算法,这是一个重要贡献。
在计算机视觉中,有很多机器学习的任务都需要针对两个图像进行一个偏好的排序。而在信息检索或者搜索中,排序是一个核心问题。因此,任何对于排序学习算法的重大改进都会有广泛的应用。
先来回顾下我们学过的三种形态的排序学习算法。
第一种是单点法排序。这个算法针对每一个查询关键词和相对应的某个文档,我们仅仅判断每一个文档是不是相关的。大多数的单点法排序算法都把整个问题转换成为分类或者回归问题。这样就可以利用大规模机器学习的便利来快速地学习排序函数。
第二种是配对法排序。这个算法是以单点法为基础。因为单点法完全忽略两个文档之间的相对关系。所以配对法是对两个文档与同一个查询关键词的相对相关度,或者说是相关度的差值进行建模。
第三种是列表法排序。列表法是直接针对排序的目标函数或者指标进行优化。这种方法虽然在理论上有优势,但是计算复杂度一般都比较高,在现实中对排序效果的提升比较有限,因此在实际场景中,依然有大量的应用采用单点法或者配对法排序。
这篇论文就是针对列表法排序学习的“计算复杂度高”这个问题作者们发明了一套叫作“基于快速排序机制”Quicksort flavoured algorithm的优化框架。在这个优化框架下排序学习计算复杂度高的这个问题得到了大幅度优化。作者们然后证明了流行的针对NDCG和MAP进行排序学习都满足所发明的优化框架这样也就在理论上提供了快速优化的可能性。
论文的核心方法
要理解这篇论文的核心方法,我们先从配对法排序学习讲起。
针对每一个查询关键词,我们可以构建一个文档和文档的矩阵。这个矩阵的每一个元素代表两个文档在当前查询关键词下的关系。如果这个矩阵元素是+1那么就表明这一行所代表的文档排位要优先于这一列所代表的文档。如果这个矩阵元素是-1那么就表明这一行所代表的文档要比这一列所代表的文档排位低。当然还有矩阵元素是0的情况那就是这两个文档的排位可以是一样的。在这个数据基础上我们可以从所有这些二元关系中推导出一个整体的排序。
下面来看配对法排序的核心思路。对于同一个查询关键词而言,我们从和这个查询关键词相关的文档中,随机抽取一个文档,然后从和这个查询关键词不相关的文档中也抽取一个文档,这两个抽取出来的文档就组成一个配对。我们希望建立一个模型或者函数,对于这样任意的配对,总能够让相关文档的函数值大于不相关文档的函数值。
如果我们对这个配对法稍微做一些更改,得到的就是列表法排序。首先,我们依然针对每一个正相关的文档进行函数值预测,也针对每一个负相关的文档进行函数值预测。我们把这两个函数值的差值,当做是预测的配对矩阵中这两个文档相对应的那一个元素。只不过在这个时候,我们关注的不是这两个文档的关系,而是配对矩阵所代表的排序和真实排序之间的差别。这个差别越小,我们就认为最终的基于列表的损失函数就小;如果差别大,那损失函数的差别就大。
如何针对这个基于列表的损失函数进行优化,从而能让我们针对单一文档的函数打分最优呢?这就是列表法排序学习的一个核心困难。
有一个优化办法,就是找到在当前函数打分的情况下,有哪个文档配对违反了排序原则。什么是违反排序原则呢?我们刚才说了,模型是希望把正相关的文档排在负相关的文档前面。但是,如果函数并没有完全被学习好,那么负相关的文档也会排到正相关的文档之前,这就叫违反排序原则。
如果我们找到这样的配对那么就可以通过调整函数的参数让这样的违反配对不出现。很显然当我们有很多这样的配对时找到违反排序原则最严重的那个配对也就是负相关的函数值要远远大于正相关函数值的这个配对对于我们改进函数的参数就会很有帮助。所以这里的关键就变成了如何找到违反排序原则最严重的配对Most-violating ranking
作者们针对这个任务发明了一个框架,叫作“基于快速排序机制”。具体来说,作者们发现,违反排序原则最严重的配对需要满足一些原则。我们需要对当前的数据序列进行快速排序,从而能够找到这个违反排序原则的配对。这里有很多的细节,有兴趣的话建议去读读原论文。你只需要记住,这个快速排序机制利用了快速排序的时间复杂度,来实现寻找违反排序原则最严重配对的这个目的。
那么是不是大多数排序指标都符合这个机制呢作者们提供的答案是普遍的MAP和NDCG都符合这个机制。论文给出了证明因此我们就可以直接使用论文的结论。
实验结果
作者们在PASCAL VOC 2011数据集上进行了实验主要是比较了直接进行单点法排序以及直接进行列表法优化和这篇论文提出的优化算法之间的性能差距。在这个比较下本文提出的方法优势非常明显基本上是以单点法的时间复杂度达到了列表法的性能。
小结
今天我为你讲了CVPR 2018的最佳论文提名。
一起来回顾下要点:第一,这篇文章的主要贡献是提出了一个基于整个排序的损失函数的快速优化算法;第二,文章提出方法的核心内容是发明了一个框架,叫作“基于快速排序机制”;第三,我们简单介绍了一下论文的实验结果。
最后给你留一个思考题回忆一下我们曾经讲过的LambdaMART算法那里其实也有这么一个寻找违反排序原则配对的步骤你能想起来是什么步骤吗

View File

@ -0,0 +1,93 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
025 ICML 2018论文精读模型经得起对抗样本的攻击这或许只是个错觉
2018年7月10日~15日国际机器学习大会ICML 2018The 35th International Conference on Machine Learning在瑞典的斯德哥尔摩举行。
ICML从1980年开始举办已有30多年的历史 ,是机器学习、人工智能领域的顶级会议。
今年ICML大会共收到了2473份投稿投稿量和去年相比增加了45%。今年最后录取了621篇论文录取率近25%。除了主会议以外ICML大会还组织了9个讲座67个研讨班。
在接下来的几期内容里我会为你精选三篇ICML 2018的论文我们一起来讨论。
今天我和你分享的是大会的最佳论文题目是《梯度混淆带来的安全错觉绕过对对抗样本的防御》Obfuscated Gradients Give a False Sense of Security: Circumventing Defenses to Adversarial Examples
先简单介绍下这篇论文的作者群。
第一作者阿尼什·阿提耶Anish Athalye是麻省理工大学的博士生主要研究方向是机器学习算法的安全。他在今年的ICML大会上就发表了3篇论文。
第二作者尼古拉·泽多维奇Nickolai Zeldovich是阿提耶的导师。他是麻省理工大学计算机系的教授做安全相关的研究。
第三作者大卫·瓦格纳David Wagner来自加州大学伯克利分校是计算机系教授也是安全研究方面的专家。
论文的背景
这篇论文的内容对于大对数人来说可能是比较陌生的。想要弄清楚这篇论文的主要贡献,我们首先来熟悉一下这篇论文所要解决的问题。
试想我们比较熟悉的监督学习任务。一般来说在监督学习任务中我们会有一个数据集用各种特性Feature来表征这个数据集里的数据点。拿最普通的监督学习来说比如需要把图像分类为“猫”、“狗”等机器学习算法就是学习一个分类器可以根据不同的输入信息来做分类的决策。
当然,我们所说的是在正常情况下使用分类器的场景。有一类特别的应用场景,或者说是“对抗”场景,其实是希望利用一切方法来破坏或者绕开分类器的决策结果。
一个大类的“对抗机制”是尝试使用“对抗样本”Adversarial Examples。什么是对抗样本呢就是说一个数据样本和原来正常的某个样本是非常类似的但是可以导致分类决策出现很大不同。例如在我们刚才的图像识别的例子中一个有效的对抗样本就是一张非常像狗的图片但是可以导致分类器认为这是一只猫或者别的动物。利用这种类似的样本可以使分类器的训练和测试都产生偏差从而达到攻击分类器的目的。
除了“对抗样本”的概念以外,我们再来看一看攻击分类器的一些基本的模式。
一般来说对分类器的攻击有两种模式一种叫作“白盒攻击”White-Box一种叫作“黑盒攻击”Black-Box。白盒攻击主要是指攻击者可以完全接触到分类器的所有内部细节比如深度模型的架构和各种权重但无法接触到测试数据。而黑盒攻击则是指攻击者无法接触分类器的细节。
这篇论文考虑的场景是白盒攻击。攻击方尝试针对每一个合法的数据点,去寻找一个距离最近的数据变形,使得分类器的结果发生变化。通俗地说,就是希望对数据进行最小的改变,从而让分类器的准确率下降。
在完全白盒的场景下,最近也有一系列的工作,希望让神经网络更加健壮,从而能够抵御对抗样本的攻击。但是到目前为止,学术界还并没有完全的答案。
论文的主要贡献
通过上面的介绍我们知道目前有一些防御对抗样本的方法似乎为分类器提供了一些健壮性的保护。这篇文章的一个重要贡献就是指出这些防御方法有可能只是带来了一种由“梯度混淆”Obfuscated Gradients所导致的错觉。
梯度混淆是“梯度屏蔽”Gradient Masking的一种特殊形式。对于迭代攻击方法来说如果发生梯度混淆防御方会形成防御成功的假象。
作者们在这篇论文中对梯度混淆进行了分析提出了三种类型的梯度混淆“扩散梯度”Shattered Gradients、“随机梯度”Stochastic Gradients和“消失梯度或者爆炸梯度”Vanishing/Exploding gradients
针对这三种不同的梯度混淆作者们提出了相应的一些攻击方案使得攻击方可以绕过梯度混淆来达到攻击的目的并且在ICLR 2018的数据集上展示了很好的效果。
值得注意的是,这篇论文针对的是在防御过程中“防御方”的方法所导致的梯度混淆的问题。目前学术界还有相应的工作是从攻击方的角度出发,试图学习打破梯度下降,例如让梯度指向错误的方向。
论文的核心方法
我们首先来看一看这三种类型的梯度混淆。
扩散梯度主要是指防御方发生了“不可微分”Non-Differentiable的情况。不可微分的后果是直接导致数值不稳定或者梯度不存在。扩散梯度其实并不意味着防御方有意识地希望这么做这很有可能是因为防御方引入了一些看似可以微分但是并没有优化目标函数的情况。
随机梯度主要是由随机防御Randomized Defense引起的。这有可能是神经网络本身被随机化了或者是输入的数据被随机化造成了梯度随机化。
消失梯度和爆炸梯度主要是通过神经网络的多次迭代估值Evaluation所导致。例如让一次迭代的结果直接进入下一次迭代的输入。
刚才我们说了,梯度混淆可能是防御方无意识所产生的结果,并不是设计为之。那么,攻击方有什么方法来识别防御方是真的产生了有效果的防御,还是仅仅发生了梯度混淆的情况呢?
作者们做了一个总结,如果出现了以下这些场景,可能就意味着出现了梯度混淆的情况。
第一种情况,一步攻击的效果比迭代攻击(也就是攻击多次)好。在白盒攻击的情况下,迭代攻击是一定好于一步攻击的。因此如果出现了这种一步攻击好于迭代攻击的情况,往往就意味着异常。
第二种情况,黑盒攻击的效果比白盒好。理论上,白盒攻击的效果应该比黑盒好。出现相反的情况,往往意味着不正常。
第三种情况无局限Unbounded Attack效果没有达到100%。最后的这种情况,就是随机寻找对抗样本,发现了比基于梯度下降的攻击要好的对抗样本。
那么,针对梯度混淆,攻击方有什么办法呢?
针对扩散梯度作者们提出了一种叫BPDABackward Pass Differentiable Approximation的方法。如果有兴趣建议你阅读论文来了解这种算法的细节。总体说来BPDA就是希望找到神经网络不可微分的地方利用简单的可微分的函数对其前后进行逼近从而达到绕过阻碍的目的。
针对随机梯度作者们提出了“变换之上的期望”Expectation over Transformation这一方法。这个方法的特点是针对随机变化变换的期望应该还是能够反映真实的梯度信息。于是作者们就让攻击方作用于变换的期望值从而能够对梯度进行有效的估计。
针对消失或者爆炸的梯度作者们提出了“重新参数化”Reparameterization这一技术。重新参数化是深度学习中重要的技术。在这里作者们使用重新参数化其实就是对变量进行变换从而使得新的变量不发生梯度消失或者爆炸的情况。
小结
今天我为你讲了今年ICML的最佳论文。
一起来回顾下要点:第一,这篇论文讨论了一个比较陌生的主题,我们简要介绍了论文的背景;第二,我们详细介绍了论文提出的三种类型的梯度混淆。
最后,给你留一个思考题,我们为什么要研究深度学习模型是否健壮,是否能够经得起攻击呢?有什么现实意义吗?

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
026 ICML 2018论文精读聊一聊机器学习算法的公平性问题
在上一次的分享里我们介绍了今年ICML大会的一篇最佳论文这是一篇非常优秀的机器学习和计算机安全相结合的论文。这篇论文剖析了目前在白盒攻击的场景下攻击方如何绕过一种叫作“混淆梯度”的情况来实现有效攻击的目的。
今天我们来分享ICML 2018的另一篇最佳论文题目Delayed Impact of Fair Machine Learning。这篇论文主要探讨了“公平”Fair在机器学习中的应用。论文的五位作者都来自加州大学伯克利分校。
论文的背景
这篇论文所探讨的主题是机器学习的“公平性”问题。近些年,这个主题受到了学术界越来越多的关注,但是对于普通的人工智能工程师和数据科学家来说,这个议题依然显得比较陌生和遥远。所以,我先来简单梳理一下这方面研究的核心思想。
机器学习有一个重要的应用,就是在各类决策场景中提供帮助,例如申请贷款、大学入学、警察执勤等。一个不可否认的特点是,这些决策很有可能会对社会或者个人产生重大的不可逆转的后果。其中一个重要的后果就是,针对不同的人群,有可能会产生意想不到的“不公平”的境况。比如,有一些普遍使用的算法,在帮助警察判断一个人是否可能是罪犯的时候,系统会认为美国黑人相对于白人更容易犯罪,这个判断显然存在一定的问题。
机器学习研究者已经注意到了这种算法中的“公平”问题并且开始探讨没有任何限制条件的机器学习算法是否会对少数族裔Underrepresented Group产生不公平的决策判断。基于这些探索研究者们提出了一系列的算法对现有的各种机器学习模型增加附带了公平相关的限制条件希望通过这种方法来解决各种不公平定义下的决策问题。
论文的主要贡献
这篇论文从理论角度展开讨论,基于什么样假设和条件下的具有公平性质的机器学习算法,在决策场景中能够真正为少数族群带来长期的福祉。值得注意的是,这里所谓的少数族裔是一个抽象化的概念,指的是数目相对较少的,或者在某种特性下比较少的一组数据群体。这篇论文并不直接讨论社会学意义下的少数族群的定义。
作者们主要是比较两个人群A和B在不同的公平条件下看这两组人群的某种“效用”Utility的差值会发生什么变化。这个差值可以是正的没变化或者是负的。
论文的主要结论是,在不同的公平条件下,效用差值会有各种可能性。这其实是一个非常重要的结论。有一些公平条件,直觉上我们感觉会促进少数族群的效用,但这篇论文向我们展示了,即便出发点是好的,在某些情况下,效用差值也可能是负的。
除此以外这篇论文还探讨了“测量误差”Measurement Error对效用差值的影响。作者们认为测量误差也应该被纳入整个体系中去思考公平的问题。
需要指出的是论文的分析方法主要建立在时序关系的“一步预测”One Time Epoch基础上的。也就是说我们利用当前的数据和模型对下一步的决策判断进行分析并不包括对未来时间段所有的预测。从理论上说如果在无限未来时间段的情况下结论有可能发生变化。
论文的核心方法
这篇文章的核心思路是探讨针对人群A和B所采取的一种“策略”Policy是怎么样影响这两组人群的效用差别的。如果某种策略会导致某个群体的效用差别为负那么我们就说这个策略对群体产生了“绝对损坏”Active Harm作用如果效用差别是零就说明这个策略对群体产生了“停滞”Stagnation作用如果效用差别是正的就说明这个策略对群体产生了“推动”Improvement作用。
除此以外我们认为有一种不考虑人群A和B具体特征的期望最大化效用的策略称之为“最大化效用”MaxUtil。这种策略其实就是在没有约束条件的情况下利用一般的机器学习算法达到的效果。我们需要把新策略和这个策略进行比较如果新的策略比这个策略好就是产生了“相对推动”Relative Improvement反之我们说新的策略产生了“相对损害”Relative Harm
为了进一步进行分析作者们引入了一个叫“结果曲线”Outcome Curve的工具来视觉化策略和效用差值的关系。具体来说曲线的横轴就是因为策略所导致的对某一个群体的选择概率纵轴就是效用差值。当我们有了这个曲线之后就能非常直观地看到效用差值的变化。
从这个曲线上我们可以看到,效用差值的确在一个区间内是“相对推动”的,而在另一个区间是“相对损害”的,在最右边的一个区间里是“绝对损害”的。这就打破了我们之前的看法,认为有一些选择策略会一致性地导致唯一结果。
在此基础上我们专门来看这两种特殊的策略。第一种叫“种族公平”Demographic Parity思路是希望在两个人群中保持一样的选择概率。另一种策略叫“公平机会”Equal Opportunity思路是希望某个人群中成功的概率例如申请到贷款、学校录取等和人群无关。这两种策略都是典型的试图利用限制条件来达到公平的方法。我们希望来比较的就是这两种策略以及之前说的最大化效用之间的一些关系得出以下三个主要结论。
第一个比较出乎意料的结论是最大化效用这个策略并不会导致“绝对损害”。意思就是说,和人们之前的一些想法不同,最大化效用也有可能让少数族裔的效用得到提升或者不变。
第二个结论是,这两种公平策略都可能会造成“相对推动”。这也是推出这两种策略的初衷,希望能够在选择概率上进行调整,从而让少数族裔的效用得到提升。
第三个结论是,这两种公平策略都可能会造成“相对损害”。这是本篇论文的一个重要结论,正式地证明了公平策略在某个区间上其实并没有带来正向的“推动”反而是“损害”了少数族群。作者们进一步比较了“种族公平”和“公平机会”这两个策略,发现“公平机会”可以避免“绝对损害”而“种族公平”则无法做到。
小结
今天我为你讲了今年ICML的另一篇最佳论文。
一起来回顾下要点:第一,这篇论文讨论了计算机算法的公平性问题;第二,我们详细介绍了论文提出的两种策略以及得出的主要结论。
最后,给你留一个思考题,研究算法的公平性对我们日常的应用型工作有什么启发作用?

View File

@ -0,0 +1,59 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
027 ICML 2018论文精读优化目标函数的时候有可能放大了不公平
今天我们要分享的是ICML 2018的一篇最佳论文提名题目是Fairness Without Demographics in Repeated Loss Minimization。
这篇论文讨论了这样一个话题,在优化目标函数的时候,如何能够做到针对不同的子群体,准确率是相当的,从而避免优化的过程中过分重视多数群体。这篇论文的作者都来自斯坦福大学。
论文的主要贡献
这篇论文其实也是希望讨论算法带来的“公平性”问题,但是出发的角度和我们上一篇讨论公平性的论文非常不一样。这篇论文的核心思想,是希望通过机器学习目标函数优化的原理,来讨论机器学习和公平性的关系。
作者们发现基于“平均损失”Average Loss优化的机器学习算法常常会给某一些少数群体带来巨大的不准确性。这其实并不是模型本身的问题而是优化的目标函数的问题。在这样的情况下目标函数主要是关注有较多数据的群体保证这些群体的损失最小化而可能忽略了在数量上不占优势的少数群体。
在此基础上还带来了另外一个用户“留存度”Retention的问题。因为少数群体忍受了比较大的优化损失因此这些群体有可能离开或者被这个系统剔除。所以长期下去少数群体的数目就可能逐渐变少。这也许是目标函数的设计者们无从想到的一个平均损失函数的副产品。作者们还把这个现象命名为“不公平的放大”Disparity Amplification
这篇论文的一个重要贡献是发现ERMEmpirical Risk Minimization经验风险最小化其实就存在这种不公平的放大性。ERM包含了很多经典的机器学习模型的目标函数例如支持向量机Support Vector Machines、对数回归模型Logistic Regression以及线性回归等。作者们还发现ERM可以让即便在最初看上去公平的模型在迭代的过程中逐渐倾向于不公平。
为了解决ERM的问题作者们开发了一种新的算法框架DRODistributionally Robust Optimization分布式健壮优化。这种框架是为了最小化“最差场景”Worst-Case的风险而不是平均风险。作者们在真实的数据中展示了DRO相比于ERM更能够解决小众群体的不公平性问题。
论文的核心方法
为了解决在ERM下的对不同群体的不公平性问题作者们首先对数据做了一种新的假设。
作者们假设数据中有隐含的K个群体。每一个数据点都有一定的概率属于这K个群体。我们当然并不知道这K个群体本身的数据分布也不知道每个数据点对于这K个群体的归属概率这些都是我们的模型需要去估计的隐含变量。
对于每一个数据点而言在当前模型下我们都可以估计一个“期望损失”Expected Loss。在新的假设框架下因为每个数据点可能属于不同的K个群体而每个群体有不同的数据分布因此会导致在当前群体下的期望损失不一样也就是会出现K个不一样的期望损失。我们的目的是要控制这K个损失中的最差的损失或者叫最差场景。如果我们可以让最差的损失都要小于某一个值那么平均值肯定就要好于这种情况。这也就从直观上解决了不公平放大的问题。
那么如果我们直接在这样的设置上运用ERM会有什么效果呢这里有一个数值是我们比较关注的那就是在整个框架假设下每个群体的期望人数。这个数值等于在期望损失的情况下当前群体剩余的人数加上新加入的人数。作者们在论文中建立了对这个期望人数的理论界定。
这个结论的直观解释是如果在当前更新的过程中期望人数的数值估计能够达到一个稳定的数值状态那么就有可能稳定到这里不公平放大的情况就不会发生而如果没有达到这个稳定的数值状态那么不公平放大的情况就一定会发生。也就是说在ERM优化的情况下群体的大小有可能会发生改变从而导致人群的流失。
在这个理论结果的基础上作者们提出了DRO。DRO的核心想法就是要改变在优化过程中可能因为数据分配不均衡而没有对当前小群体进行足够的采样。
具体来说DRO对当前群体中损失高的人群以更高的权重也就是说更加重视当前目标函数表现不佳的区域。对于每一个数据点而言损失高的群体所对应的群体概率会被放大从而强调这个群体当前的损失状态。换句话说DRO优先考虑那些在当前情况下损失比较大的小群体。这样的设置就能够实现对最差情况的优化从而避免不公平放大。
作者们在文章中展示了DRO所对应的目标函数可以在递归下降的框架下进行优化也就是说任何当前利用ERM的算法都有机会更改为DRO的优化流程从而避免不公平放大的情况。
论文的实验结果
作者们在一个模拟的和一个真实的数据集上进行了实验。我们这里简单讲一讲真实数据的实验情况。
作者们研究了一个“自动完成”Auto Completion的任务。这个任务是给定当前的词来预测下一个词出现的可能性。而数据则来自两个不同人群美国白人和黑人所产生的推特信息。在这个实验中作者们就是想模拟这两个人群的留存度和模型损失。这里面的隐含假设是美国白人和黑人的英语词汇和表达方式是不太一样的。如果把两个人群混合在一起进行优化很有可能无法照顾到黑人的用户体验从而留不住黑人用户。
在实验之后DRO相比于ERM更能让黑人用户满意并且黑人用户的留存度也相对比较高。从这个实验中DRO得到了验证的确能够起到照顾少数人群的作用。
小结
今天我为你讲了今年ICML的最佳论文提名。
一起来回顾下要点第一这篇论文也讨论了算法带来的“公平性”问题是从机器学习目标函数优化的角度来考虑这个问题的第二这篇论文的一个重要贡献是发现ERM确实存在不公平的放大性基于此作者们开发了一种新的算法框架DRO第三文章的实验结果验证了DRO的思路确实能够解决小众群体的不公平性问题。
最后,给你留一个思考题,这两期内容我们从不同的角度讨论了算法的公平性问题,你是否有自己的角度来思考这个问题?

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
030 复盘 7 一起来读人工智能国际顶级会议论文
今天我准备了 30 张知识卡和你一起来复盘“人工智能国际顶级会议”模块。在这个模块里我总共介绍了10个顶级会议包括机器学习方面的ICML、NIPS机器视觉的CVPR、ICCV自然语言处理的ACL、EMNLP数据挖掘和数据科学的KDD、WSDM信息检索和搜索的SIGIR互联网综合的WWW。
提示:点击知识卡跳转到你最想看的那篇文章,温故而知新。
KDD 2017数据挖掘与知识发现大会论文精讲
EMNLP 2017自然语言处理实证方法会议论文精讲
ICCV 2017国际计算机视觉大会论文精讲
NIPS 2017神经信息处理系统大会论文精讲
WSDM 2018网络搜索与数据挖掘国际会议论文精讲
The Web 2018国际万维网大会论文精讲
CVPR 2018国际计算机视觉与模式识别会议论文精讲
SIGIR 2018国际信息检索大会论文精讲
ICML 2018国际机器学习大会论文精讲
ACL 2018计算语言学学会年会论文精讲
积跬步以至千里
学习是独立的,需要你一个人去完成。但学习者从来都不必孤独,我们走进这些国际顶级学术会议的论文,其实就是和每一篇论文背后的作者进行一场对话。与优秀的人同行一定能让我们走得更快。
这个模块我根据自己的经验为你选择了10个顶级会议。针对每一个会议我都会在会议结束后用3篇文章来详细剖析这个会议的精髓和一些前沿信息。我希望通过我的眼睛和思考让你看到在这个领域里那些激动人心的发展收获新知、拓展视野同时也把我的学习方法分享给你。
我想你应该已经掌握了我分析论文的套路了,对于每一篇文章,我一定会先去做一些背景研究,了解作者群,了解对应的学术机构或者公司信息;然后弄清楚论文解决了什么问题,核心贡献是什么;再详细研究论文的具体方法。这个方法很简单,就是牢牢抓住一个主线,找到最核心的内容来消化吸收。但是真正让这个方法内化成你的思维模式,还是需要大量的阅读和练习。相信我,如果想在人工智能领域继续深耕,阅读大量论文,一定是一个最值得做的投资,因为回报极大。
那回到阅读论文本身,最后想跟你分享的一点只有八个字:学好英语,阅读原文。我知道你可能会说我英语还真不好,但是到达能够阅读原文的水平其实也并没那么难。你不妨直接找一篇我们专栏里讲过的论文原文,就把每一段的第一句读一下,看看能否学到东西。先开始看起来,遇到不会的且影响你理解的单词或句子再去查,你的英语水平就已经开始变得越来越好了。
以上就是我们对论文精读这个模块的一个复盘,希望专栏里的这三十篇论文是一个起点,能够帮助你养成关注国际顶级会议、阅读论文的习惯,拥有这一强大的学习利器,提升自己的学习效率。

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
031 经典搜索核心算法TF-IDF及其变种
从本周开始我们进入人工智能核心技术模块本周我会集中讲解经典的搜索核心算法今天先来介绍TF-IDF算法。
在信息检索Information Retrieval、文本挖掘Text Mining以及自然语言处理Natural Language Processing领域TF-IDF算法都可以说是鼎鼎有名。虽然在这些领域中目前也出现了不少以深度学习为基础的新的文本表达和算分Weighting方法但是TF-IDF作为一个最基础的方法依然在很多应用中发挥着不可替代的作用。
了解和掌握TF-IDF算法对初学者大有裨益能够帮助初学者更快地理解其它更加深入、复杂的文本挖掘算法和模型。今天我就来谈谈TF-IDF的历史、算法本身的细节以及基于TF-IDF的几个变种算法。
TF-IDF的历史
把查询关键字Query和文档Document都转换成“向量”并且尝试用线性代数等数学工具来解决信息检索问题这样的努力至少可以追溯到20世纪70年代。
1971年美国康奈尔大学教授杰拉德·索尔顿Gerard Salton发表了《SMART检索系统自动文档处理实验》The SMART Retrieval System—Experiments in Automatic Document Processing一文文中首次提到了把查询关键字和文档都转换成“向量”并且给这些向量中的元素赋予不同的值。这篇论文中描述的SMART检索系统特别是其中对TF-IDF及其变种的描述成了后续很多工业级系统的重要参考。
1972年英国的计算机科学家卡伦·琼斯Karen Spärck Jones在《从统计的观点看词的特殊性及其在文档检索中的应用》A Statistical Interpretation of Term Specificity and Its Application in Retrieval 一文中第一次详细地阐述了IDF的应用。其后卡伦又在《检索目录中的词赋值权重》Index Term Weighting一文中对TF和IDF的结合进行了论述。可以说卡伦是第一位从理论上对TF-IDF进行完整论证的计算机科学家因此后世也有很多人把TF-IDF的发明归结于卡伦。
杰拉德本人被认为是“信息检索之父”。他1927年出生于德国的纽伦堡并与1950年和1952年先后从纽约的布鲁克林学院获得数学学士和硕士学位1958年从哈佛大学获得应用数学博士学位之后来到康奈尔大学参与组建计算机系。为了致敬杰拉德本人对现代信息检索技术的卓越贡献现在美国计算机协会ACMAssociation of Computing Machinery每三年颁发一次“杰拉德·索尔顿奖”Gerard Salton Award用于表彰对信息检索技术有突出贡献的研究人员。卡伦·琼斯在1988年获得了第二届“杰拉德·索尔顿奖”的殊荣。
TF-IDF算法详解
要理解TF-IDF算法第一个步骤是理解TF-IDF的应用背景。TF-IDF来源于一个最经典、也是最古老的信息检索模型即“向量空间模型”Vector Space Model
简单来说,向量空间模型就是希望把查询关键字和文档都表达成向量,然后利用向量之间的运算来进一步表达向量间的关系。比如,一个比较常用的运算就是计算查询关键字所对应的向量和文档所对应的向量之间的“相关度”。
因为有了向量的表达相关度往往可以用向量在某种意义上的“相似度”来进行近似比如余弦相似性Cosine Similarity或者是点积Dot Product。这样相关度就可以用一个值来进行表达。不管是余弦相似度还是点积都能够从线性代数或者几何的角度来解释计算的合理性。
在最基本的向量空间模型的表达中查询关键字或是文档的向量都有V维度。这里的V是整个词汇表Vocabulary的总长度。比如我们如果有1万个常用的英文单词那么这个V的取值就是1万而查询关键字和每个文档的向量都是一个1万维的向量。 对于这个向量中的每一个维度,都表示英文中的一个单词,没有重复。
你可以看到在这样的情况下如果当前的词出现在这个向量所对应的文档或者关键字里就用1来表达如果这个词没出现就用0来表达。这就是给每个维度赋值Weighting的最简单的方法。
TF-IDF就是在向量空间模型的假设下的一种更加复杂的赋值方式。TF-IDF最基础的模式顾名思义就是TF和IDF的乘积。
TF其实是“单词频率”Term Frequency的简称。意思就是说我们计算一个查询关键字中某一个单词在目标文档中出现的次数。举例说来如果我们要查询“Car Insurance”那么对于每一个文档我们都计算“Car”这个单词在其中出现了多少次“Insurance”这个单词在其中出现了多少次。这个就是TF的计算方法。
TF背后的隐含的假设是查询关键字中的单词应该相对于其他单词更加重要而文档的重要程度也就是相关度与单词在文档中出现的次数成正比。比如“Car”这个单词在文档A里出现了5次而在文档B里出现了20次那么TF计算就认为文档B可能更相关。
然而信息检索工作者很快就发现仅有TF不能比较完整地描述文档的相关度。因为语言的因素有一些单词可能会比较自然地在很多文档中反复出现比如英语中的“The”、“An”、“But”等等。这些词大多起到了链接语句的作用是保持语言连贯不可或缺的部分。然而如果我们要搜索“How to Build A Car”这个关键词其中的“How”、“To”以及“A”都极可能在绝大多数的文档中出现这个时候TF就无法帮助我们区分文档的相关度了。
IDF也就是“逆文档频率”Inverse Document Frequency就在这样的情况下应运而生。这里面的思路其实很简单那就是我们需要去“惩罚”Penalize那些出现在太多文档中的单词。
也就是说真正携带“相关”信息的单词仅仅出现在相对比较少有时候可能是极少数的文档里。这个信息很容易用“文档频率”来计算也就是有多少文档涵盖了这个单词。很明显如果有太多文档都涵盖了某个单词这个单词也就越不重要或者说是这个单词就越没有信息量。因此我们需要对TF的值进行修正而IDF的想法是用DF的倒数来进行修正。倒数的应用正好表达了这样的思想DF值越大越不重要。
在了解了TF和IDF的基本计算方法后我们就可以用这两个概念的乘积来表达某个查询单词在一个目标文档中的重要性了。值得一提的是虽然我们在介绍TF-IDF这个概念的时候并没有提及怎么把查询关键字和文档分别表达成向量其实TF-IDF算法隐含了这个步骤。
具体来说对于查询关键字向量的长度是V也就是我们刚才说过的词汇表的大小。然后其中关键字的单词出现过的维度是1其他维度是0。对于目标文档而言关键词出现过的维度是TF-IDF的数值而其他维度是0。在这样的表达下如果我们对两个文档进行“点积”操作则得到的相关度打分Scoring就是TF-IDF作为相关度的打分结果。
TF-IDF算法变种
很明显经典的TF-IDF算法有很多因素没有考虑。在过去的很长一段时间里研究人员和工程师开发出了很多种TF-IDF的变种。这里我介绍几个经典的变种。
首先很多人注意到TF的值在原始的定义中没有任何上限。虽然我们一般认为一个文档包含查询关键词多次相对来说表达了某种相关度但这样的关系很难说是线性的。拿我们刚才举过的关于“Car Insurance”的例子来说文档A可能包含“Car”这个词100次而文档B可能包含200次是不是说文档B的相关度就是文档A的2倍呢其实很多人意识到超过了某个阈值之后这个TF也就没那么有区分度了。
用Log也就是对数函数对TF进行变换就是一个不让TF线性增长的技巧。具体来说人们常常用1+Log(TF)这个值来代替原来的TF取值。在这样新的计算下假设“Car”出现一次新的值是1出现100次新的值是5.6而出现200次新的值是6.3。很明显,这样的计算保持了一个平衡,既有区分度,但也不至于完全线性增长。
另外一个关于TF的观察则是经典的计算并没有考虑“长文档”和“短文档”的区别。一个文档A有3,000个单词一个文档B有250个单词很明显即便“Car”在这两个文档中都同样出现过20次也不能说这两个文档都同等相关。对TF进行“标准化”Normalization特别是根据文档的最大TF值进行的标准化成了另外一个比较常用的技巧。
第三个常用的技巧也是利用了对数函数进行变换的是对IDF进行处理。相对于直接使用IDF来作为“惩罚因素”我们可以使用N+1然后除以DF作为一个新的DF的倒数并且再在这个基础上通过一个对数变化。这里的N是所有文档的总数。这样做的好处就是第一使用了文档总数来做标准化很类似上面提到的标准化的思路第二利用对数来达到非线性增长的目的。
还有一个重要的TF-IDF变种则是对查询关键字向量以及文档向量进行标准化使得这些向量能够不受向量里有效元素多少的影响也就是不同的文档可能有不同的长度。在线性代数里可以把向量都标准化为一个单位向量的长度。这个时候再进行点积运算就相当于在原来的向量上进行余弦相似度的运算。所以另外一个角度利用这个规则就是直接在多数时候进行余弦相似度运算以代替点积运算。
小结
今天我为你讲了文档检索领域或者搜索领域里最基本的一个技术TF-IDF。我们可以看到TF-IDF由两个核心概念组成分别是词在文档中的频率和文档频率。TF-IDF背后隐含的是基于向量空间模型的假设。
一起来回顾下要点第一简要介绍了TF-IDF的历史。第二详细介绍了TF-IDF算法的主要组成部分。第三简要介绍了TF-IDF的一些变种 。
最后给你留一个思考题如果要把TF-IDF应用到中文环境中是否需要一些预处理的步骤

View File

@ -0,0 +1,68 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
035 机器学习排序算法:配对法排序学习
周一的文章里我分享了最基本的单点法排序学习Pointwise Learning to Rank。这个思路简单实用是把经典的信息检索问题转化为机器学习问题的第一个关键步骤。简单回顾一下我们介绍了在测试集里使用NDCGNormalized Discounted Cumulative Gain在某个K的位置评价“精度”Precision和“召回”Recall以这些形式来评估排序算法。
你可以看到单点法排序学习算法的模式和我们最终需要的结果中间还存在明显差距。这个差距并不是算法好坏能够决定的而是算法所要优化的目标也就是单个数据点是否相关和我们的最终目的一组结果的NDCG排序最优之间的结构化区别。这个结构化区别激发研究者们不断思考是不是有其他的方法来优化排序算法。
今天我就来讲从单点法引申出来的“配对法”排序学习Pairwise Learning to Rank 。相对于尝试学习每一个样本是否相关,配对法的基本思路是对样本进行两两比较,从比较中学习排序,离真正目标又近了一步。
配对法排序学习的历史
当人们意识到用机器学习来对排序进行学习从文档与文档之间的相对关系入手也就是配对法就成了一个非常火热的研究方向。机器学习排序这个领域持续活跃了10多年在此期间很多配对法排序算法被提出下面我就说几个非常热门的算法。
2000年左右研究人员开始利用支持向量机SVM来训练排序算法来自康奈尔的索斯藤·乔基姆斯Thorsten Joachims就构建了基于特征差值的RankSVM一度成为配对法排序学习的经典算法。索斯藤我们前面讲过他获得了今年的KDD时间检验奖。
2005年当时在雅虎任职的研究人员郑朝晖等人开始尝试用GBDTGradient Boosting Decision Tree梯度提升决策树这样的树模型来对文档之间的两两关系进行建模。郑朝晖后来成为一点资讯的联合创始人。
2005年微软的学者克里斯·博格斯Chris Burges等人开始使用神经网络训练RankNet文档之间两两关系的排序模型。这是最早使用深度学习模型进行工业级应用的尝试。这篇论文在2015年获得了ICML 2015International Conference on Machine Learning国际机器学习大会的10年“经典论文奖”。
配对法排序学习详解
在介绍配对法排序学习的中心思路之前,我们先来重温一下测试集的测试原理。总体来说,测试的原理和单点法一样,都是要考察测试集上,对于某一个查询关键字来说,某一组文档所组成的排序是否是最优的。
比如对于某一个查询关键字我们针对排序产生的“顶部的K”个文档进行评估首先查看精度Precision即在所有算法已经判断是相关的文档中究竟有多少是真正相关的其次看召回Recall即所有真正相关的文档究竟有多少被提取了出来。当然还有F1值也就是精度和召回“和谐平均”Harmonic Mean的取值一个平衡精度和召回的重要指标。需要再次说明的是 精度、召回以及F1值都是在二元相关信息的标签基础上定义的。
如果需要利用五级相关信息定义也就是通常所说的“最相关”、“相关”、“不能确定”到“不相关”、“最不相关”那么就需要用类似于NDCG这样的评价指标。NDCG的假设是在一个排序结果里相关信息要比不相关信息排得更高最相关信息需要排在最上面最不相关信息需要排在最下面。任何排序结果一旦偏离了这样的假设就会受到“扣分”或者“惩罚”。
在清楚了测试集的情况后再回过头来看一看训练集的设置问题。在今天文章一开篇的时候我就提到了单点法对于排序学习的“目标不明确”的问题。其实从NDCG的角度来看也好基于顶部K的精度或者召回的角度来看也好都可以看出对于一个查询关键字来说最重要的其实不是针对某一个文档的相关性是否估计得准确而是要能够正确估计一组文档之间的“相对关系”。只要相对关系估计正确了那么从排序这个角度来说最后的结果也就准确了。理解这一个观点对于深入理解排序和普通的分类之间的区别至关重要。
那么,如何从单点建模再进一步呢?
很显然,在排序关系中,一个关键关系就是每两个文档之间的比较,也就是我们通常所说的两两关系。试想一下,如果针对某一个查询关键字而言,有一个完美的排序关系,然后通过这个完美的排序关系,可以推导出文档之间的两两相对关系,再从这些相对关系中进行学习,从而可以进一步对其他查询关键字进行排序。
注意在这样的架构下训练集的样本从每一个“关键字文档对”变成了“关键字文档文档配对”。也就是说每一个数据样本其实是一个比较关系。试想有三个文档A、B和C。完美的排序是“B>C>A”。我们希望通过学习两两关系“B>C”、“B>A”和“C>A”来重构“B>C>A”。
这里面有几个非常关键的假设。
第一我们可以针对某一个关键字得到一个完美的排序关系。在实际操作中这个关系可以通过五级相关标签来获得也可以通过其他信息获得比如点击率等信息。然而这个完美的排序关系并不是永远都存在的。试想在电子商务网站中对于查询关键字“哈利波特”有的用户希望购买书籍有的用户则希望购买含有哈利波特图案的T恤显然这里面就不存在一个完美排序。
第二我们寄希望能够学习文档之间的两两配对关系从而“重构”这个完美排序。然而这也不是一个有“保证”的思路。用刚才的例子希望学习两两关系“B>C”、“B>A”和“C>A”来重构完美排序“B>C>A”。然而实际中这三个两两关系之间是独立的。特别是在预测的时候即使模型能够正确判断“B>C”和“C>A”也不代表模型就一定能得到“B>A”。注意这里的关键是“一定”也就是模型有可能得到也有可能得不到。两两配对关系不能“一定”得到完美排序这个结论其实就揭示了这种方法的不一致性。也就是说我们并不能真正保证可以得到最优的排序。
第三我们能够构建样本来描述这样的两两相对的比较关系。一个相对比较简单的情况认为文档之间的两两关系来自于文档特征Feature之间的差异。也就是说可以利用样本之间特征的差值当做新的特征从而学习到差值到相关性差异这样的一组对应关系。
我前面提到的RankSVM就是这样的思路。RankSVM从本质上来说其实还是SVM也就是支持向量机只不过建模的对象从单一文档变成了文档的配对。更加复杂的模型比如GBRank就是通过树的聚合模型GBDT来对文档之间的关系直接建模希望通过函数值的差值来表达文档的相关性差异。
需要注意的是,配对法排序学习特别是在测试集预测的时候,可能会有计算复杂度的问题。因为原则上,必须要对所有的两两关系都进行预测。现实中,如果是基于线性特征的差值来进行样本构造的话,那么测试还可以回归到线性复杂度的情况。而用其他方法,就没那么幸运了。有很多计算提速或者是逼近算法为两两比较排序在实际应用中提供了可能性。
小结
今天我为你讲了文档检索领域基于机器学习的配对法排序学习。你可以看到,和单点法一样,整个问题的设置和传统的文字搜索技术有本质的区别,但在对文档之间关系的建模上,又比单点法前进了一大步 。
一起来回顾下要点第一在火热的机器学习排序研究中提出了很多配对法排序算法比如RankSVM、GBDT和RankNet。第二配对法排序学习测试集的测试原理和单点法一致我们可以查看精度、召回和F1值或者利用五级相关信息。第三针对单点法对于排序学习的“目标不明确”问题配对法排序学习有不一样的训练集设置在这个基础上我介绍了三个关键假设。
最后,给你留一个思考题,有没有什么办法可以把单点法和配对法结合起来呢?
参考文献
Zhaohui Zheng, Keke Chen, Gordon Sun, and Hongyuan Zha. A regression framework for learning ranking functions using relative relevance judgments. Proceedings of the 30th annual international ACM SIGIR conference on research and development in information retrieval, 287-2942007.
Thorsten Joachims. Optimizing search engines using clickthrough data. *Proceedings of the eighth ACM SIGKDD international conference on knowledge discovery and data mining*133-1422002.

View File

@ -0,0 +1,81 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
036 机器学习排序算法:列表法排序学习
本周我们已经分别讨论了最基本的单点法排序学习Pointwise Learning to Rank和配对法排序学习Pairwise Learning to Rank两种思路。单点法排序学习思路简单实用目的就是把经典的信息检索问题转化成机器学习问题。配对法排序学习则是把排序的问题转化成针对某个查询关键字每两个文档之间的相对相关性的建模问题。不过这两种思路也都有很明显的问题需要进一步对算法进行优化以实现我们需要的最终目标。
今天我就来讲直接优化排序问题的“终极方法”列表法排序学习Listwise Learning to Rank 。相对于尝试学习每一个样本是否相关或者两个文档的相对比较关系列表法排序学习的基本思路是尝试直接优化像NDCGNormalized Discounted Cumulative Gain这样的指标从而能够学习到最佳排序结果。
列表法排序学习的历史
2000年后学术界和工业界都开始研究如何用机器学习来解决最优排序问题五六年之后研究者们才开始尝试直接优化整个排序列表。
这方面的研究工作很多都来自微软研究院。比如2007年左右的AdaRank就来自微软亚洲研究院的徐君和李航。这篇论文算是较早提出列表法排序观点的研究工作。同一年在国际机器学习大会ICML 2007International Conference on Machine Learning上发表的ListNet算是从理论上开启了列表法的大门。这篇论文也来自微软亚洲研究院是刘铁岩等人的重要工作。类似的研究工作在这一年里如雨后春笋般涌现。
另外一个方向接下来我会提到LambdaRank出现稍早而LambdaMART则稍微晚一点。这方面的工作是在微软西雅图的研究院开发的。主导人是克里斯托弗·博格斯Christopher J.C. Burges。博格斯2016年退休在微软工作了16年可以说他领导的团队发明了微软的搜索引擎Bing的算法。
列表法排序学习详解
列表法排序学习有两种基本思路。第一种就是直接针对NDCG这样的指标进行优化。目的简单明了用什么做衡量标准就优化什么目标。第二种则是根据一个已经知道的最优排序尝试重建这个顺序然后来衡量这中间的差异。
我先来说一下第一大思路直接针对NDCG这样的指标进行优化。
首先重温一下排序测试集的测试原理。总体来说所有的基于排序的指标都要考察测试集上对于某一个查询关键字来说某一组文档所组成的排序是否是最优的。有两种比较通用的做法。第一个方法主要适用于二分的相关信息对于某一个查询关键字针对排序产生的“顶部的K”个文档进行评估查看精度Precision、召回Recall等。第二种方法利用五级相关信息定义在这样的情况下就可以利用类似于NDCG这样的评价指标。具体解读你可以回到本周前面两期我们讲解过的内容进行复习。
那么,直接优化排序指标的难点和核心在什么地方呢?
难点在于希望能够优化NDCG指标这样的“理想”很美好但是现实却很残酷。NDCG以及我之前说过的基于“顶部的K”的精度都是在数学的形式上的“非连续”Non-Continuous 和“非可微分”Non-Differentiable。而绝大多数的优化算法都是基于“连续”Continuous )和“可微分” Differentiable函数的。因此直接优化难度比较大。
针对这种情况,主要有这么几种方法。
第一种方法是既然直接优化有难度那就找一个近似NDCG的另外一种指标。而这种替代的指标是“连续”和“可微分”的 。只要我们建立这个替代指标和NDCG之间的近似关系那么就能够通过优化这个替代指标达到逼近优化NDCG的目的。这类的代表性算法的有SoftRank和AppRank。
第二种方法是尝试从数学的形式上写出一个NDCG等指标的“边界”Bound然后优化这个边界。比如如果推导出一个上界那就可以通过最小化这个上界来优化NDCG。这类的代表性算法有SVM-MAP和SVM-NDCG。
第三种方法则是希望从优化算法上下手看是否能够设计出复杂的优化算法来达到优化NDCG等指标的目的。对于这类算法来说算法要求的目标函数可以是“非连续”和“非可微分”的。这类的代表性算法有AdaRank和RankGP。
说完了第一大思路后我们再来看看第二大思路。这种思路的主要假设是已经知道了针对某个搜索关键字的完美排序那么怎么通过学习算法来逼近这个完美排序。我们希望缩小预测排序和完美排序之间的差距。值得注意的是在这种思路的讨论中优化NDCG等排序的指标并不是主要目的。这里面的代表有ListNet 和ListMLE。
讲了这两大思路以后最后我再来提一下第三类思路。这类思路的特点是在纯列表法和配对法之间寻求一种中间解法。具体来说这类思路的核心思想是从NDCG等指标中受到启发设计出一种替代的目标函数。这一步还和我刚才介绍的第一大思路中的第一个方向有异曲同工之妙都是希望能够找到替代品。
这第三类思路更进一步的则是找到替代品以后把直接优化列表的想法退化成优化某种配对。这第二步就更进一步简化了问题。这个方向的代表方法就是微软发明的LambdaRank以及后来的LambdaMART。微软发明的这个系列算法成了微软的搜索引擎Bing的核心算法之一。
我这里简单提一下LambdaRank这个系列模型的基本思想。
首先微软的学者们注意到一个排序算法是否达到最优的情况简单来看就是查看当前的排序中相比于最优的情况有哪些两两文档的关系搞错了。学习最优排序的问题就被转化成了减小这些两两排错的关系。更进一步在设计这个优化过程中我们其实并不需要知道真正的目标函数的形式而仅仅需要某种形式的梯度Gradient
这里有这样一个洞察对于绝大多数的优化过程来说目标函数很多时候仅仅是为了推导梯度而存在的。而如果我们直接就得到了梯度那自然就不需要目标函数了。最后通过实验微软的学者们把这个NDCG通过梯度变化的差值再乘以这个梯度这样就达到了增强效果的目的。
早期的LambdaRank特别是RankNet是采用了神经网络来进行模型训练而LambdaMART则采用了“集成决策树”的思想更换到了基于决策树的方法。后来实践证明基于决策树的方法对于排序问题非常有效果也就成了很多类似方法的标准配置。
最后有一点需要你注意我们讨论了不同的列表法思路列表法从理论上和研究情况来看都是比较理想的排序学习方法。因为列表法尝试统一排序学习的测试指标和学习目标。尽管在学术研究中纯列表法表现优异但是在实际中类似于LambdaRank这类思路也就是基于配对法和列表法之间的混合方法更受欢迎。因为从总体上看列表法的运算复杂度都比较高而在工业级的实际应用中真正的优势并不是特别大因此列表法的主要贡献目前还多是学术价值。
小结
今天我为你讲了列表法排序学习。你可以看到列表法排序有很多种思路在2000年到2010年之间是一个非常活跃的研究领域积累了大量的成果。
一起来回顾下要点:第一,简要介绍了列表法排序学习的历史。第二,详细介绍了列表法排序学习的三大思路以及每个思路里的主要细节和方法。
最后,给你留一个思考题,列表法是不是就完全解决了排序算法的问题呢?
参考文献
Jun Xu and Hang Li. AdaRank: a boosting algorithm for information retrieval. Proceedings of the 30th annual international ACM SIGIR conference on research and development in information retrieval, 391-3982007.
Zhe Cao, Tao Qin, Tie-Yan Liu, Ming-Feng Tsai, Hang Li. Learning to rank: from pairwise approach to listwise approach. ICML, 129-136, 2017.
Q. Wu, C.J.C. Burges, K. Svore and J. Gao. Adapting boosting for information retrieval measures. Journal of Information Retrieval, 2007.
C.J.C. Burges, R. Ragno and Q.V. Le. Learning to rank with non-smooth cost functions. Advances in Neural Information Processing Systems, 2006.
C.J.C. Burges, T. Shaked, E. Renshaw, A. Lazier, M. Deeds, N. Hamilton and G. Hullender. Learning to rank using gradient descent. Proceedings of the twenty second international conference on machine learning, 2005.
F. Xia, T.-Y. Liu, J. Wang, W. Zhang, and H. Li. Listwise approach to learning to rank — Theorem and algorithm. ICML, 11921199, 2008.
S. Chakrabarti, R. Khanna, U. Sawant, and C. Bhattacharyya. Structured learning for non-smooth ranking losses. SIGKDD, 8896, 2008.
T. Qin, T.-Y. Liu, and H. Li. A general approximation framework for direct optimization of information retrieval measures.Technical Report, Microsoft Research, MSR-TR-2008-164, 2008.
M. Taylor, J. Guiver, S. Robertson, and T. Minka. SoftRank: Optimising non-smooth rank metrics. WSDM, 7786, 2008.
J.-Y. Yeh and J.-Y. Lin, and etc. Learning to rank for information retrieval using genetic programming. SIGIR 2007 Workshop in Learning to Rank for Information Retrieval, 2007.
Y. Yue, T. Finley, F. Radlinski, and T. Joachims. A support vector method for optimizing average precision. SIGIR, 271278, 2007.

View File

@ -0,0 +1,83 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
037 查询关键字理解三部曲之分类
我们在前两周的专栏里主要讲解了最经典的信息检索Information Retrieval技术和基于机器学习的排序算法Learning to Rank
经典的信息检索技术为2000年之前的搜索引擎提供了基本的算法支持。从中衍生出的TF-IDF、BM25还有语言模型Language Model以及这些方法的各类变种都还在很多领域不限文本里继续发挥着作用。
另一方面排序学习算法引领了2000年到2010年各类基于机器学习的搜索算法的产生和发展也带来了搜索引擎技术的进一步成熟。
这周我们从排序算法转移到排序问题中一个非常重要的部分查询关键字理解Query Understanding。也就是说我们希望通过查询关键字来了解用户种种行为背后的目的。查询关键字产生的特征Feature往往是很强的指导因素也是个性化搜索结果非常重要的源泉。因此深入了解并掌握查询关键字理解方面的技术就变得很有必要。
查询关键字理解最基本的一个步骤就是给查询关键字分类Classification看这些查询关键字有什么用户意图Intent。今天我就来聊一聊查询关键字分类的一些基本概念和技术让你对这方面的开发和研究有一个基本认识。
查询关键字分类的历史
从商业搜索引擎开始面世的第一天起人们就发现可以从查询关键字中得到很多用户的信息特别是理解用户的意图。早在1997年商业搜索引擎Excite就开始了百万级别查询关键字的研究工作。然而真正对查询关键字分类进行系统阐述的是安德烈·布罗德Andrei Broder的论文《网页搜索分类》A Taxonomy of Web Search
安德烈很有名头在斯坦福大学攻读博士期间师从图灵奖得主高德纳Donald Knuth然后在曾经名噪一时的第一代搜索引擎公司AltaVista后被雅虎收购担任首席科学家之后加入位于纽约的IBM研究院组建企业级搜索平台2012年后加入Google担任杰出科学家Distinguished Scientist。他还是ACMAssociation of Computing Machinery计算机协会和IEEEInstitute of Electrical and Electronics Engineers电气电子工程师学会的双料院士。
安德烈的这篇论文可以说是奠定了查询关键字分类的坚实基础。这之后研究人员的很多工作都是围绕着如何自动化分类、如何定义更加精细的用户意图来展开的。
查询关键字分类详解
我就从安德烈这篇非常有名的文章说起。在网络搜索Web Search成为比较主流的咨询查询手段之前传统的信息检索认为查询的主要目的是完成一个抽象的“信息需求”Information Needs。在传统信息检索的世界里最主要的应用应该是图书馆检索或者政府学校等企事业单位的检索。因此在这样的场景下假定每一个查询主要是满足某个“信息需求”就显得很有道理了。
然而早在2002年安德烈就认为这样的传统假定已经不适合网络时代了。他开始把查询关键字所代表的目的划分为三个大类
导航目的Navigational
信息目的Informational
交易目的Transactional
此后十多年里,查询关键字的这三大分类都是这个方向研究和实践的基石。我们先来看这个分类的内涵。
第一类,以导航为意图的查询关键字,这类查询关键字的目标是达到某个网站。这有可能是用户以前访问过这个网站,或者是用户假设有这么一个关于所提交查询关键字的网站。这一类查询关键字包括公司的名字(如“微软”)、人的名字(如“奥巴马”)或者某个服务的名字(如“联邦快递”)等。
此类查询关键字的一个重要特点就是,在大多数情况下,这些查询关键字都对应唯一的或者很少的“标准答案”网站。比如,搜索“微软公司”,希望能够找到的就是微软公司的官方网站。另一方面是说,某些“信息集成”网站也是可以接受的“答案”。比如,查询“奥巴马”,搜索返回的结果是一个列举了所有美国总统的网站。
第二类以信息为意图的查询关键字这类查询关键字的目标是搜集信息。这一类的查询和传统的信息检索非常接近。值得提及的是从后面的研究结论来看这一类查询关键字所包含的目标不仅仅是寻找到某类权威性质Authority的网页还包括列举权威信息的俗称“结点”Hub的网站。
第三类以交易为意图的查询关键字这类查询关键字的目标是到达一个中间站点从而进一步完成“交易”Transaction。这一类查询关键字的主要对象就是“购物”。现在我们对“电子商务”的态度可以说是非常自然了但是十多年前在传统信息检索界统治的搜索研究领域提出“交易”类型的查询关键字可以说是很有新意的。
当然这样的分类如果仅仅是概念上的区分那就没有太大的意义。安德烈利用搜索引擎AltaVista进行了一次调查研究这次调查有大约3千多的用户反馈。想到这是在2001年的调查可以说已经是大规模的研究了。
这次调研的结果是这样的在用户提交的信息中导航类型的查询关键字占26%交易类型的查询关键字占到了24%而剩下的将近50%是信息类型的查询关键字用户的日志Log分析进一步证实了这一数据。
你可以看到,这种把查询关键字进行分类的研究是对用户行为进行建模的必要步骤。于是,很快就有不少研究人员嗅到了查询关键字分类的价值。然而,完全依靠用户直接反馈来获取这类信息则变得越发困难。
这里主要有三个原因。第一,不可能寄希望于用户汇报自己所有关键字的意图;第二,面对亿万用户输入的查询关键字,手工标注也是不可能的;最后,安德烈的三类分类还是太粗犷了,在实际应用中希望得到更加细颗粒度的用户意图。
把查询关键字分类问题转换成为标准的机器学习任务其实很直观。确切地说,这里需要做的是把查询关键字分类转换成为监督学习任务。这里,每一个查询关键字,就是一个数据样本,而响应变量,则是对应的类别。具体情况取决于我们的任务是仅仅把查询关键字分为几个类别,并且认为这些类别之间是互相独立的,还是认为这些类别是可以同时存在的。
在最简单的假设下查询关键字分类就是一个普通的多类分类问题可以使用普适的多类分类器比如支持向量机SVM、随机森林Random Forest以及神经网络Neural Networks等来解决这类问题。
对于绝大多数监督学习任务而言,最重要的一个组成部分就是选取特征。随后很多年的研究开发工作中,有一部分就集中在尝试使用不同的特征,然后来看对提高分类的精度是否有效果。
过去的研究反复证明,以下几类特征非常有效。
第一类特征就是查询关键字本身的信息。比如,查询关键字中已经包括了已知的人名或者公司名,这种时候,分类结果就不太可能是交易意图的类别。也就是说,查询关键字,特别是某些词或者词组和类别有某种关联信息,而这种关联很大程度上能被直接反映出来。
第二类特征是搜索引擎返回的查询关键字相关的页面本身的信息。你可以想象一下,假如搜索“奥巴马”这个关键字,返回的页面都是维基百科的页面以及奥巴马基金会的页面,那么这些页面上面的内容可能很难包含任何商业的购买信息。而对于“佳能相机”这个查询关键字而言,返回的页面很可能都是电子商务网站的商品信息,从而能够更加准确地判断“佳能相机”的分类。
第三类特征则是用户的行为信息,那就是用户在输入查询关键字以后会点击什么网站,会在哪些网站停留。一般来说,哪些网站点击率高、停留时间长,就表明这些网站在返回结果中可能更相关。于是,采用这些网站来作为查询关键字所代表的内容,就可能更加靠谱。
在实际的应用中,查询关键字的分类往往还是有很大难度的。因为在普通的现代搜索引擎上,每天可能有三分之一、甚至更多的关键字是之前没有出现过的。因此,如何处理从来没有出现过的关键字、如何处理长尾中的低频关键字,就成了让搜索结果的精度再上一个台阶的重要因素。我今天就不展开相应的话题了,如果你有兴趣,可以查看相关论文。
小结
今天我为你讲了现代搜索技术中一个非常基础但是也在实际应用中至关重要的环节,那就是查询关键字理解中的用户意图分类问题。你可以看到查询关键字从大类上分为信息意图、交易意图以及导航意图三类。
一起来回顾下要点:第一,简要介绍了查询关键字分类提出的历史背景,安德烈·布罗德的论文奠定了查询关键字分类的坚实基础。第二,详细介绍了主要的分类以及如何通过多类分类器的构建来达到自动化的目的。
最后,给你留一个思考题,在机器学习排序算法中,我们应该如何使用查询关键字分类的结果呢?
拓展阅读A taxonomy of web search

View File

@ -0,0 +1,72 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
038 查询关键字理解三部曲之解析
这周我分享的核心内容是查询关键字理解Query Understanding。周一介绍了查询关键字分类Query Classification的基本概念和思想。今天我来讲一个更加精细的查询关键字理解模块查询关键字解析Parsing
如果说查询关键字分类是对查询关键字的宏观把握,那么,对查询关键字的解析就是微观分析。其实,查询关键字解析是一类技术的统称,我今天就来聊几个比较热的话题。
查询关键字分割
首先让我们设想这么一个场景在英文的搜索引擎中如果一个用户输入的是“White House Opening”这个查询关键字这个用户的意图Intent是什么呢要想理解用户的意图我们就得知道用户输入的单词的涵义。
那么在上面这个查询关键字里我们到底是分别理解每一个单词“White”、“House”和“Opening”呢还是“White House”和“Opening”呢还是有可能“White House Opening”是一个整体呢这里说的其实就是“查询关键字分割”Query Segmentation这个概念。
在刚才的例子中如何把“White House Opening”进行分割直接关系到搜索结果的质量。试想在一个比较标准的现代搜索引擎里一般来说都会有一个模块根据查询关键字来提取“倒排索引”Inverted Index中的文档。这个阶段的提取数目一般是几百到几千这个过程常常被称为“检索流程”Retrieval Phase
当有了这些文档以后现代搜索引擎会利用比较复杂的排序算法通常就是我们之前提到过的基于机器学习的排序学习模型来对文档进行重新排序Re-Rank
你可以看到,在这样两个阶段的流程里,如果好的文档没有在第一个阶段被提取出来,不管第二个阶段的功能有多强大,搜索的整体结果都不可能有多好。而对于“检索流程”而言,在“倒排索引”中进行查询的关键就是使用什么“单词”或者“词组”进行查找。
用刚才的例子来说就是看文档究竟是符合“White House”还是“White或House”还是“White House Opening”。很明显这三种情况得到的文档集合是不尽相同的。如果用户的真实意图是搜索美国总统府白宫的开放时间那么把这个搜索关键字给分割成“White或House”很明显就会影响提取的文档集合。
那究竟该怎样做查询关键字分割呢?
这里我介绍一篇论文《重新审视查询关键字分割》Query Segmentation Revisited )。在这篇论文里,作者们集中介绍了一些主流的“查询关键字分割”技术,文章非常值得精读。下面我为你归纳一下要点。
第一种技术就是尝试从查询关键字里面产生“N元语法”N-Grams。所谓N元语法其实就是从一组词语中产生连续的子词语。比如刚才的“White House Opening”的例子我们就可以从这个词组里面产生“White House”和“House Opening”两个二元语法。
而第一种基于N元语法的方法就是通过这些N元语法在一个大语料中出现的词频来判断这个“分割”是否有意义。当然直接采用词频可能会比较偏好短的单词所以在论文中作者们分别介绍了两种矫正词频的方法。
一种是基于词频本身的矫正一种是基于维基百科作为一个外部资源的矫正方式。两种方法的目的都是为了让长短语的打分Scoring有机会高于短的单词。文章中所需要的词频采用了谷歌2005年发布的“N元语法”语料也就是说所有单词出现的频率都是直接在这个语料中获得的。
第二种技术是基于短语“互信息”Mutual Information的方法。“互信息”计算了两个随机事件的相关程度。在这里就是计算查询关键字中每两个相邻短语的“互信息”。当这个“互信息”的取值大于某一个预设阈值的时候我们就认为相邻的两个单词组成了短语。“互信息”的计算需要知道某个单词出现的概率这些概率是从微软发布的一个“N元语法”语料获得的。
第三种技术则是基于“条件随机场”Conditional Random Field。“条件随机场”是机器学习著名学者乔治·拉菲迪John D. Lafferty、安德鲁·麦卡伦Andrew McCallum和费尔南多·佩雷拉Fernando Pereira在2001年发表的“序列学习”模型Sequence Model中提出的。条件随机场的基本思想是对输出的复杂标签进行建模尝试从特征空间建立到复杂标签的一个对应关系。
在“查询关键字分割”的场景下,我们其实可以把复杂标签看作是从一个查询关键字到多个短语的多个二元决策问题。这里的二元决策是指某一个备选短语是否可以作为分割的短语。条件随机场可以比较直观地对这类问题进行建模,而传统的二分分类器则很难对序列信息进行建模。我在这里就不详细展开条件随机场的介绍了,有兴趣的话可以翻看相关的论文。
查询关键字标注
刚才我聊了查询关键字理解最基本的“分割“问题。可以说,“分割问题”是查询关键字理解的第一步。那么,下一步则是更细致地分析查询关键字。
回到刚才的例子“White House Opening”我们其实不仅是想知道这个查询关键字可以分割为“White House”和“Opening”而且希望知道“White House”是一个建筑物的名字或者一个地理位置的名字而“Opening”则可能是一个名词暗指“开门时间”。也就是说我们希望为查询关键字中的词组进行“标注”Annotation来获取其“属性”Attribute信息。希望为查询关键字中分割出来的词组进行标注的组件就叫做“查询关键字标注”。
那么,标注信息又是怎样帮助搜索结果的呢?试想一下“苹果价格”这个查询关键字。这取决于用户搜索的场景,如果“苹果”代表“水果”这个属性,那么这个查询的结果是希望找到水果的价格,可能还需要搜索引擎返回附近超市的一些信息。但如果“苹果”其实代表的是“手机”,那这个查询的结果也许最好是返回苹果公司的官方销售网站。你看,“苹果”所代表的属性不同,最优的返回结果可能会有非常大的差别。
对查询关键字进行标注的方法也有很多。我这里再推荐一篇经典的论文《使用伪相关反馈针对搜索查询关键字进行结构化标注》Structural annotation of search queries using pseudo-relevance feedback这篇论文利用一个叫做PRFPseudo-Relevance Feedback的方法来进行标注。这里面的一个技术难点是查询关键字的信息实在是太少需要利用大量的辅助信息来进行标注因此PRF作为一个技术在这里得到了应用。
另外一个主流的查询关键字标注的方法,依然是利用条件随机场。我前面讲了,条件随机场是很好的序列建模工具。那么,在这里,以“苹果价格”为例,条件随机场是需要预测标签是否是“手机名词”还是“水果名词”这样的组合输出结果。而传统的二分或者多类分类器很难捕捉到这里的序列信息,条件随机场就是解决这方面的利器。
于是我们需要做的就是为查询关键字构建特征Feature然后直接放入条件随机场中。有一点需要注意条件随机场的应用成功与否与数据的多少有很大关系。因此构建一个有标注信息的数据集就变成了查询关键字标注的一个核心挑战。
小结
今天我为你讲了现代搜索技术中的一个重要环节,那就是查询关键字理解中的查询关键字解析问题。你可以看到查询关键字解析从大类上分为查询关键字分割和查询关键字标注两个比较重要的模块。
一起来回顾下要点第一简要介绍了查询关键字分割的场景和三种主要技术分别是“N元语法”、“互信息”和“条件随机场”。第二详细介绍了查询关键字标注的场景和主要技术包括利用PRF和利用条件随机场两种主流的标注方法。
最后,给你留一个思考题,我举了英语的查询关键字的解析问题,那么对于中文而言,又有哪些特殊的挑战呢?
参考文献
Matthias Hagen, Martin Potthast, Benno Stein, and Christof Bräutigam. Query segmentation revisited. Proceedings of the 20th international conference on World wide web (WWW 11). ACM, New York, NY, USA, 97-106. 2011.
Michael Bendersky, W. Bruce Croft, and David A. Smith. Structural annotation of search queries using pseudo-relevance feedback. Proceedings of the 19th ACM international conference on Information and knowledge management (CIKM 10). ACM, New York, NY, USA, 1537-1540. 2010.

View File

@ -0,0 +1,85 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
043 文档理解第一步:文档分类
我们在前几周的专栏里讲解了最经典的信息检索Information Retrieval技术以及基于机器学习的排序学习算法Learning to Rank并且花了一定的时间分享了查询关键字理解Query Understanding这一关键搜索组件的核心技术要点。上周我们还详细讨论了如何从线上和线下两个层面来评价一个搜索系统。
这周我们的分享将转移到搜索的另外一个重要部件文档理解Document Understanding。也就是从文档中抽取各种特性来帮助检索算法找到更加相关的文档。
文档理解最基本的一个步骤就是给文档分类Classification看这些文档表达什么类别的信息。今天我就来和你聊一聊文档分类的一些基本概念和技术让你对这方面的开发与研究有一个基本认识。
文档分类的类型
如果我们把文档分类看做一个监督学习任务的话,那么在各式应用中就经常使用以下几种类型的文档分类。
第一个类别就是二元分类,或者称为二分文档分类,目的就是把文档分成两种不同的类别。比如,把文档分成“商业类”或者“非商业类”。
第二个类别自然就是多类分类,也就是判断文档是否属于好几种不同类别中的某一个。比如,把文档划归为“艺术”、“商业”、“计算机”或者“运动”类别中的某一类。
当然,在多类分类的下面,我们还可以分三个小类别。
第一个小类别,是“多类-单标签-硬分类”MulticlassSingle-LabelHard Classification。什么意思呢就是说每一个文档只能在多类分类问题中被赋予唯一的标签并且所有互相的类别是不兼容的。
第二个小类别,就是“多类-多标签-硬分类”MulticlassMultilabelHard Classification也就是说每一个文档可以被认为属于多个类别然而每个这样的分类都是唯一确定的。
最后一个小类别则是“多类-软分类”MulticlassSoft Classification也就是认定每个文档以概率的形态属于多个类别。
在这个分类基础上还有一种分类的方法那就是可以把所有的类别看做一个平面的结构Flat或者是有组织结构的。通常情况下如果把文档分类到一个层次组织Hierarchical Structure里就叫“层次分类”Hierarchical Classification。在这样的情况下一个文档同时属于这个层次结构上从根节点到叶子节点的所有类别。一般来说上层节点相对于下层节点更加抽象。
文档分类经典特性
了解了文档分类的基本类型之后,我们接着来讨论文档分类所用到的经典特性。
我们最先会想到的当然是使用文档上原本的文字信息。最直接的文字特性可能就是每个英文单词或者中文的词语。这种完全把文字顺序打乱的方式叫作“词袋模型”Bag-of-words Model
从很多实践者的报告来看“词袋模型”虽然不考虑文字的顺序但是在实际使用中依然不失为一种非常有效的特性表达方式。同时在“词袋模型”中每个词的权重其实可以用我们之前介绍过的TF-IDF或是语言模型Language Model对单词进行加权。关于TF-IDF以及语言模型建议你回到我们前面讲过的内容去复习一下。
除了“词袋模型”以外,还有一些不同的尝试,是希望能够保留部分或者全部的词序。
比如我们曾经讲过的“N元语法”N-gram对文字的表达方法就是一种非常有效的保留部分词序的方法。不过N元语法最大的问题就是极大地增大了特性空间同时每一个N元组被观测到的次数明显减少这也就带来了数据的稀少Sparsity问题。
除了N元语法以外近年来随着深度学习的推广比较新的思路是用“递归神经网络”RNN来对序列在这里也就是词句进行建模。有不少研究表明这样的效果要明显好于“词袋模型”。
除了文档上的原始文字以外,文档上的排版格式其实也是很重要的。有些字段有很明显的特征,比如一个文档的标题显然占据了举足轻重的地位。有一些文档有“章节”、“段落”等结构,其中这些小标题对文章的主要内容有很大的指导意义。于是,对文章的不同“字段”(有时候也叫做“域”)进行建模,对文档分类的效果可能会有比较大的影响。
另外针对某些特殊文档仅仅考虑文字的基本信息可能是不够的。例如现代网页的原始HTML表达和最终在浏览器中呈现出来的效果很可能会有较大区别。因此针对网页我们可能还需要采用浏览器中最终呈现出来的视觉效果来提取特性。
对于孤立的文档来说,单个文档的信息可能是比较有限的。但是在互联网上,很多文档都不是孤立存在的。就拿普通网页来说,互联网的一个特点就是很多网页都通过各种链接连到一起。这些和当前网页相连的其他页面很可能就会为当前页面提供一些额外信息。
在所有这些周围的页面中,有一类页面值得在这里提一下。那就是这些页面上会有链接指向当前我们需要分类的目标网页。这些链接往往有文字描述来叙述目标网页的一些特质,甚至有一些周围的文字描述也是有意义的。
比如,当前网页是微软公司的首页,上面也许因为有各种精美的图片而缺乏文字描述,而周围的页面上很可能就有“微软公司官方网站”等链接指向微软公司的首页。这样,我们就通过这些链接文字得出了“微软公司”的信息,然后如果我们又知道微软公司是软件公司,那么就比较容易对这个页面进行分类了。
根据这个思路,我们就可以尝试去使用周围文档中更多的信息。不过,值得指出的是,周围文档信息所带的“噪声”也是比较多的。已经有各类研究尝试去理解周围文档中更多有价值的信息,这里就不赘述了。
文档分类相关算法
根据我们刚刚讲过的不同文档的分类类型,就可以直接借用已知的、熟悉的监督学习各种算法和模型。
假如是简单的二分文档分类问题那“对数几率回归”Logistic Regression、“支持向量机”SVM、“朴素的贝叶斯分类器”Naïve Bayes Classifier就都能够胜任工作。而针对多类分类问题也是标准的监督学习设置刚才说到的这几类算法和模型在一定的改动下也能够做到。
近些年,深度学习席卷很多领域。在文档分类领域,各类深度学习模型也都展示出了一定的优势。
需要注意的是并不是所有的分类算法都“天生”Natively支持“概率的输出结果”。也就是说如果我们需要对“多类-软分类”文档问题进行建模,那就会有一些问题。比如支持向量机就是这么一种情况。在默认的状态下,支持向量机并不输出每一个数据样例属于每一个类别的概率。
因此这里就需要用到一些技巧。在实际应用中我们经常采用的是一种叫“普拉特调整”Platt Scaling的办法。简单来说其实就是把支持向量机的输出结果再当做新的特性学习一个对数几率回归。
除了我们刚刚讲的利用基本的监督学习手段进行文档分类以外另外一种方法就是我们前面说的利用周围有关系的文档也就是所谓的“关系学习”Relational Learning。关系学习是说希望利用文档与文档之间的关系来提高文档的分类效果。这一方面的很多方法都会利用这样的思想相似的页面很有可能是相同的类别。
如果是在“层次分类”的情况下相似的页面就很有可能在层次结构上距离比较近。这里“相似”有可能被定义成文字信息相似也有可能是在文档与文档之间所组成的“图”Graph上位置类似。
比如,某一个公司的很多子页面,虽然上面的文字本身有差异,但因为都是这个公司的页面,从大的文档页面网络上看,他们都代表这个公司的信息,因此在进行文档分类的时候,也很有可能会把他们放到一起。
小结
今天我为你讲了现代搜索技术中又一个至关重要的环节,那就是文档理解中的文档分类问题。你可以看到文档分类所要了解的信息还是比较多的。
一起来回顾下要点:第一,简要介绍了文档分类的主要类型,包括二元分类、多类分类以及层次分类。第二,详细介绍了文档分类所可能用到的种种特性,比如文档上原本的文字信息、文档的排版格式以及周围有关系的文档。第三,介绍了如何利用监督学习以及其他的算法工具来完成文档分类的任务。
最后,给你留一个思考题,如果一个文档中既有图片也有文字,那我们该如何组织这些特性,然后放到我们的分类器中去学习呢?

View File

@ -0,0 +1,69 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
044 文档理解的关键步骤:文档聚类
周一我们分享了文档理解最基本的一个步骤那就是给文档分类Classification主要是看不同文档表达什么类别的信息。今天我就来聊一聊文档理解的另外一个重要组件文档聚类Document Clustering
文档聚类的类型
和了解文档分类的思路相似,我们先来看看文档聚类的分类。一般来说,可以把文档聚类看作非监督学习的典型代表。
先说一种直观的分类方法。如果把文档分为“互不相关”的几个聚类那就叫作“扁平聚类”Flat Clustering如果这些聚类相互之间有一定的结构关系那就叫作“层次聚类”Hierarchical Clustering
“扁平聚类”中的“互不相关”是说文档所划分进去的聚类之间本身没有重合。而“层次聚类”的特点是,希望在聚类之间找到关系,从而把这些文档组织到一个有层次的结构中。在这种层级结构里,根节点所代表的内容往往比较抽象,而叶节点所表达的内容则比较具体。
值得注意的是,不管是“扁平聚类”还是“层次聚类”,相较于文档分类来说,这里最大的不同就是这些聚类以及它们之间的关系都不是事先定义好的,或者说研发人员事先并不知道这些聚类的存在。从这个角度来看,聚类的确是比分类要困难的任务,难在如何衡量聚类的好坏。
除了“扁平聚类”和“层次聚类”这种区分以外聚类方法中还有一个类似的区分那就是“硬聚类”Hard Assignment和“软聚类”Soft Assignment的区别。
顾名思义,“硬聚类”是说对于每一个文档,不管是“扁平聚类”还是“层次聚类”,都确定性地分配到一个或者一组聚类中。而“软聚类”则往往学习到文档分配到聚类的一个分布,也就是说所有的分配都是以某种概率存在的。
文档聚类的应用
在搜索系统为背景的场景中,我们为什么要强调文档聚类?
首先文档聚类可以帮助文档提取和排序。很多文档能够聚合到一个类别肯定是因为文档在某种情况下“相似”。相似的文档很可能都满足用户的某种“信息需求”Information Needs。实际上在类似“语言模型”Language Model或者其他概率模型的场景中对文档相关度的预测经常需要从相似文档群体中寻找额外信息。
举个例子,在“语言模型”中,我们需要估计文档相对于查询关键字的相关度。单独的某一个文档,数据信息可能比较匮乏,因此一个常用的策略就是从整个数据集中补充信息。如果我们已经有了文档的聚类,那自然就可以从这些聚类中补充,而不需要数据全集。
其次,文档聚类能够帮助整理搜索结果。在最普通的搜索结果上,如果只是完全“平铺”所有的结果,用户很可能对成百上千的结果“不得要领”。因此,在这些结果上体现某种结构就成为了很多搜索引擎提升用户体验的一种方法。
当然,这里可以用我们之前提到的“文档分类”的方法,把返回的结果按照类别组织。这样,哪一个类别有什么结果就清清楚楚。在这里,文档聚类相比于文档分类的优势是,聚类更能反应文档之间更本质的联系,而不是类似于分类这样“先入为主”地对文档的关系有一个定义。
文档聚类不仅仅是搜索结果的展示利器,很多时候,文档聚类还可以帮助研究人员来浏览一个文档集合,而不需要太多的先期假设。在有“层次聚类”的帮助下,研发人员可以很容易地根据层次之间的关系来对一个文档集合进行分析。利用文档聚类来浏览文档集合常常是发现问题,并且进行下一步工作的有效步骤。
文档聚类的基本模型
最基础的文档“扁平聚类”方法当属“K均值算法”K-Means
首先一个最基本的步骤就是要把文档表示成“特性向量”Feature Vector。具体的做法可以采用我们周一讲过的几个方式比如最基本的“词袋模型”Bag Of Word这是一种把文字顺序完全打乱的方式。在“词袋模型”中每个词的权重可以用我们之前介绍过的TF-IDF或是语言模型对单词进行加权。当然还有“N元语法”N-gram和“递归神经网络”RNN两种思路这一部分可以回到我们周一的内容再复习一下。
把文档表达成为“特征向量”之后就可以开始聚类了。“K均值算法”的基本思路是这样的。给定一个数据样本集K均值算法尝试把所有的样本划分为K个聚类。每个聚类都是互斥的也就是说样本都被有且唯一地分配到这些聚类中。K均值算法在优化一个目标函数那就是每个样本到目标聚类中心的平均平方误差最小。
这里目标聚类中心是指当前这个样本被分配到的聚类而聚类中心则是所有被分配到这个聚类的样本的均值。很明显根据不同的样本被分配到不同的聚类聚类中心也会随之发生变化。通俗地说K均值算法的目标函数要达到的目的是让聚类内部的样本紧紧围绕在聚类的均值向量周围。整个目标函数的值越小聚类内样本之间的相似度就越高。
和我们熟悉的线性回归模型Linear Regression以及对数几率回归Logistic Regression一样目标函数本身仅仅描述了当最终的聚类分配最佳时的一种情况并没有描述如何能够得到最佳聚类分配的情况。实际上对于K均值算法而言直接最小化这个目标函数并不容易一般来说找到它的最优解是一个NP难的问题。
不过幸运的是,贪心算法一般能够找到不错的近似解。下面我就介绍一个通过迭代优化来近似求解目标函数的算法。
首先,我们对均值向量进行初始化。比较简单的初始化方法就是直接随机地选择某几个点来当做聚类均值。然后,我们依次对每一个样本点进行聚类划分。每个数据点被分配到距离某一个均值向量最近的那个聚类里。当我们进行了所有的分配之后再对均值向量更新。这就完成了一次迭代,整个算法需要进行多次迭代更新。若迭代更新后聚类结果保持不变,就将当前聚类划分结果返回。
文档聚类的难点
在今天分享的最后,我想来谈一谈文档聚类的一些难点。
首先,怎样衡量聚类的质量好坏,也就是如何评价聚类算法以及比较不同的算法,一直都是聚类模型,甚至说是无监督机器学习算法的共同问题。有一些评价手段基于定义聚类内部数据的相似度,并且认为聚类内部数据应该比聚类之间的数据更加相似。然而,这样的定义并不能真正反映聚类的质量。
其次在聚类算法中往往有一个参数非常难以决定那就是聚类的个数。对于一个决定的数据集来说我们不可能事先知道这个参数。当聚类的个数过少的时候我们可能无法对数据集进行比较完备的K均值算法描述。而聚类的个数过多的时候可能数据又被切割成过多的碎片。因此要确定这个参数就成了聚类算法研究的一个核心难点。
小结
今天我为你讲了文档理解中的文档聚类问题。一起来回顾下要点第一简要介绍了文档聚类的类型。第二详细介绍了文档聚类的应用场景。第三讲解来一个基本的文档聚类K均值算法。第四简要提及了文档聚类的一些难点。
最后,给你留一个思考题,当得到文档聚类的结果以后,能否把这些结果用在其他任务中呢?如果可以,如何利用?

View File

@ -0,0 +1,77 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
045 文档理解的重要特例:多模文档建模
本周我们重点分享搜索系统中的一个重要部件那就是文档理解。周一我们首先分享了文档理解最基本的一个步骤那就是给文档分类主要是看不同文档表达什么类别的信息。然后周三我们聊了聊另外一个重要的文档理解组件也就是文档聚类的一些基本的概念和技术。今天我就来和你分享一个文档理解的重要特例多模文档建模Multimodal Modeling
多模数据
我们首先来了解一下,到底什么是多模数据。
多模数据其实就是说数据有多种模式Modal的表达途径。而这些多种不同的模式都共同参与描述同一个数据点的不同方面。
比如,有一张照片反映的是美国总统特朗普在华盛顿白宫的致辞。那么照片本身是对这个场景的一个描述,这是一个模式。然后,和照片相应的文字描述,说明这是特朗普在白宫的致辞,又是另外一个模式。这两个模式是相辅相成的,都是对这个场景的描述。很明显,针对这样多种数据模式的建模是多媒体时代、社交媒体时代非常重要的课题。
在文档领域非常普遍的情况是文字和图片混搭。一般来说新闻网站一般都有大量的图文信息。而有一些特殊场景文字和图片则出现很不对称的混合情况。比如一些社交媒体例如Instagram、Pinterest甚至Twitter上很多短文档都仅仅包含图片或者图片和很少的文字。在这些情况中文字和图片就成了非常重要的互相补充的信息源。
另外,在电子商务网站中,商品的图片正在成为越来越重要的信息途径。用户经常依靠图片来判断是否要购买某个商品。在电子商务网站上已经很难看到只有文字描述的商品信息了。因此,对于文档的搜索来说,对图文信息的理解是一个核心的技术问题。
那么,多模数据的建模难点是什么呢?
不同模式的数据其实是有不同的特征,如何能够有效利用各自的特性来最优地反映到某一个任务中(比如分类或者聚类等),是多模数据建模的难点。
多模数据建模基础
那么,如何对多种模式的数据进行建模呢?
多模数据建模的核心思路就是数据表征Representation。我们需要思考的是如何学习到文字的表征以及图片的表征。然后又如何把文字和图片的表征能够联系到一起。
一个最直接的思路应该是文字采用我们熟悉的各种文字特性然后利用图片相关的特性提取技术来对图片进行表征。得到文字和图片各自的表征之后直接把两个不同的特征向量Feature Vector连接到一起就得到了一个“联合表征”Joint Representation
比如假设我们学习到了一个1000维度的文字特征向量然后一个500维的图片特征向量那么联合特征向量就是1500维度。
一个相对比较现代的思路是利用两个不同的神经网络分别代表文字和图片。神经网络学习到“隐含单元”Hidden Unit来表达图片信息以及文字信息之后我们再把这些“隐含单元”联结起来组成整个文档的“联合隐含单元”。
另外一个思路,那就是并不把多种模式的数据表征合并,而是保持它们的独立。在文字图片这个例子中,那就是保持文字和图片各自的表征或者特征向量,然后通过某种关系来维持这两种表征之间的联系。
有一种假设就是,虽然各种数据模式的表象是不一样的,例如图片和文字的最终呈现不一样,但是内在都是这个核心内容的某种表述。因此,这些数据模式的内在表达很可能是相近的。
这个假设套用到这里那就是我们假设文字和图片的各自的表征相近而这个“相近”是依靠某种相似函数来描述比如这里就经常使用“余弦相似函数”Cosine Similarity
有了上述两种思路之后,一种混合的思路就很自然地出现了。混合思路的基本想法是这样的。数据不同的模式肯定是某种内在表征的不同呈现,因此,需要一个统一的内在表征。但是,只采用一种表征来表达不同的数据源,又明显是不够灵活的。所以,在这种混合的思路里,我们依然需要两种不同的特征来表达文字和图片。
具体来说,混合思路是这样的。首先,我们从文字和图片的原始数据中学习到一个统一的联合表征。然后,我们认为文字和图片各自的表征都是从这个联合表征“发展”或者是“产生”的。很明显,在这样的架构中,我们必须要同时学习联合表征以及两个模式的、产生于联合表征的、单独的各自表征。
值得注意的是,不管是从原始数据到联合表征,还是从联合表征到各自表征,这些步骤都可以是简单的模型,不过通常是多层的神经网络模型。
值得一提的是在需要多种不同的表征不管是联合表征还是各自表征的情况中文字和图片的原始输入甚至是最开始的表征不一定非要“端到端”End-to-End地从目前的数据中学习到。实际上利用提前从其他数据集中训练好的文字嵌入向量表达来作为文字的输入是一个非常流行也是非常高效的做法。
有了数据表征之后,很自然地就是利用这些学习到的表征来进行下一步的任务。我们这里就拿文档分类为例。有了联合表征之后,下一步就是利用这个新的表征当做整个文档的特征,学习分类器来进行分类任务。而对于独立的数据表征来说,通常的方法是针对各自表征分别学习一个分类器。这样,我们就有了两个独立的分类器,一个用于文字信息,一个用于图片信息。
有了这两个分类器之后,我们再学习第三个分类器,根据前面两个分类器的分类结果,也就是说这个时候分类结果已经成为了新的特征,来进行第三个分类器的分类。很明显,这个过程需要训练多个不同的分类器,为整个流程增加了不少复杂度。
其他多模数据建模应用
除了我刚才所说的表征的学习以及如何构建分类器以外,多模数据还有一些其他的富有挑战性的任务。
在有文字和图片的情况下,我们经常还需要在这两种模式之间进行转换,或者叫做“翻译”。比如,在已知图片的情况下,如何能够产生一段准确的文字来描述这个图片;或者是在已经有文字的情况下,如何找到甚至产生一张准确的图片。当然,这样的“翻译”并不仅仅局限于文字图片之间,在其他的数据模式中,例如文字和语音之间、语音和图像之间等等,也是普遍存在的。
在这种“翻译”的基础上更进一步的则是把文字和图片等信息“对接”Align起来。比如针对一组图片我们能够根据图片的变化产生图片的描述信息。
还有一种应用叫做“可视化问答”Visual Question & Answering是指利用图片和文字一起回答问题。很显然要想能够回答好问题我们需要同时对图片和文字信息进行建模。
不管是“翻译”还是“可视化问答”这些任务都是近些年来大量利用深度学习所带来的序列模型Sequential Modeling特别是类似于RNN或者LSTM等模型的领域。
小结
今天我为你讲了文档理解中的多模数据建模问题。你可以看到这是一个非常火热的领域,如何理解多媒体数据是现代数据处理的一个重要问题 。
一起来回顾下要点:第一,简要介绍了什么是多模数据。第二,详细介绍了多模数据建模的一些基本思路,包括如何获取文档的表征、什么是联合表征和什么是独立表征。然后,我们还讲了如何构建不同的分类器。第三,简要地提及了其他的多模数据建模任务以及这些任务所依靠的基本的深度学习趋势。
最后,给你留一个思考题,多模建模带来了丰富的特性,由这些丰富特性所训练的分类器,就一定能比单一数据源所训练得到的分类器表现得更好吗?

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
046 大型搜索框架宏观视角:发展、特点及趋势
我们在前几周的专栏里讲解了一系列最经典的信息检索Information Retrieval技术以及基于机器学习的排序学习算法Learning to Rank。然后我们花了一定的时间讨论了两个关键搜索组件的核心技术要点包括查询关键字理解Query Understanding和文档理解Document Understanding。除此之外我们还详细讨论了如何从线上和线下两个层面来评价一个搜索系统。相信你已经对搜索系统的各个基本组成部分有了一个比较基础的把握。
那么,今天我们就第一次从整体上来看看大型搜索系统框架的演变和历史发展,给你一个宏观的认识。相信有了之前的基础知识铺垫,我们今天的分享会让你感觉到水到渠成。
基于文本匹配的信息检索系统
我们在介绍TF-IDF和BM25这些经典信息检索系统的时候其实就已经介绍了不少基于文本匹配的基本的信息检索系统的核心概念。
实际上从20世纪50年代有信息检索系统开始一直到2000年前后这种纯粹基于文本匹配的搜索系统一直都是主流搜索系统的基础所在。甚至当前的很多开源搜索框架也都是基于这种最基本的信息检索系统的。
总结一下,这种信息检索系统有这么几个特点。
首先文本匹配系统的基础是一个倒排索引Inverted Index。索引中的“字段”是某一个查询关键字。而每个字段所对应的则是包含这个查询关键字的文档列表。这个文档列表大多按照某种重要的顺序排列。
比如,某个文档整体和查询关键字的相关度大,那么就会排列到这个列表的前面。当然,也并不一定所有包含这个查询关键字的文档都会包含到这个列表中。另外,之所以叫做“索引”,也是因为这个列表中并不实际存储整个文档,往往只是存储文档的编号。
从这个基本的索引结构其实衍生出了很多值得研究而且在实际应用中也很有必要考虑的问题。
比如如何进一步优化构建这个索引。特别是当列表中的文档数目过多的时候,或者当查询关键字也很多的时候,采用某种编码的模式来压缩索引就变得很关键。
同时,索引过大也会带来很多性能上的问题。比如,当索引过大的时候,某一部分索引或者很大部分就无法存放在内存中,这个时候,整个搜索系统的性能就受到了很大的威胁。因为在对查询关键字进行处理的时候,就需要反复在内存和硬盘上切换内容。因此,对于索引进行创新,使得索引能够在内存中使用并且快速查询是一个非常重要的课题。
文本匹配系统的另外一个特点就是对传统的检索方法例如TF-IDF或BM25以及它们变种的依赖。这些方法在查询关键字和索引之间架起一座桥梁使得搜索引擎能够针对每一个查询关键字文档对赋予一个数值。然后我们可以利用这个数值进行排序。
然而,这些方法本质上的最大问题就是,他们都不是基于机器学习的方法。也就是说,这些方法本身都是基于一些研究人员的假设和经验,往往无法针对现有的数据进行适应。也正是因为如此,这种方法的研发工作往往让人感到缺乏理论基础。
最后传统的文本匹配系统还存在一个问题那就是很难比较自然地处理多模数据。也就是我们之前说过的如果数据中有文字、图像、图Graph信息等综合数据信息文本匹配的方法在这方面并没有提供什么理论指导。
那么文本匹配系统有哪些优势呢其实即便是在今天文本匹配系统的最大劣势也是其最大优势不依靠机器学习。也就是说如果你要构建一个新的搜索系统或者是某个App中有搜索功能最开始的版本最容易依靠文本匹配系统因为这时候并不需要依靠任何数据并且文本匹配系统不需要太多调优就能上线。但是文本匹配系统的这一优势今天往往被很多人忽视。
基于机器学习的信息检索系统
从2000年开始基于机器学习的信息检索系统思潮逐渐变成了构建搜索系统的主流。在这种框架下的信息检索系统主要有以下这些特点。
第一基于机器学习的系统开始有了一整套的理论支持。比如我们之前讲过的单点法Pointwise排序、配对法Pairwise排序和列表法Listwise排序等方法都明确地使用通用的机器学习语言来描述搜索问题。
什么叫做通用的机器学习语言那就是有一个明确的目标函数有明确特性Feature有明确的算法来求解在这些框架下的机器学习问题。同时机器学习的一系列基本的方法论比如训练数据、测试数据、评测方法等等都可以应用到信息检索的场景中来。这对于搜索系统的性能以及整体搜索系统的研发都有了非常重要的指导意义。
同时,这也开启了一个非常便利的提高搜索系统效果的大门。那就是任何机器学习领域内部的发展,很多都可以被借鉴到搜索系统中。比如,最近几年深度学习的大力发展,就可以在已经铺就的基于机器学习的搜索系统框架下很容易地进行尝试。
第二,基于机器学习的搜索系统能够很容易地利用多模数据。对于机器学习而言,多模数据,或者说是多种类型的数据的融合,可以很自然地通过特性以及不同类型的特性来表达。因此,对于多模数据,机器学习有天然的优势。通过学习这些特性之间的联系从而预测相关度,是机器学习的强项。
因此,理解搜索系统各个部分的数据并把这些信息用在排序算法中,这样的方式就如雨后春笋般大量地出现了。比如,我们之前提到过的查询关键字理解中的查询关键字分类和查询关键字解析,以及文档理解中的文档分类所产生的特性,很难想象这些内容在传统的文本匹配系统中得以应用。但在基于机器学习的搜索系统中,这些信息则往往成为提高相关度建模的重要工具。
同时,我们也在之前的分享中介绍了,针对多模数据,机器学习中专门有相关的研究,思考如何把不同类型的数据能够更好地融合在一起来建模。这类研究在传统的文本匹配搜索系统中根本不存在。
基于机器学习的搜索系统也不是完美无瑕。实际上,如果没有各种保证,机器学习并不一定能在实际中获得满意的效果,因为基于机器学习的搜索系统对整个系统而言有了较高的要求。
机器学习往往需要大量的数据,而在一个现实的软件产品中,如何能够构建可靠并且干净的数据就是一个不简单的任务。如果没有可靠的数据,对于一般的机器学习算法而言,就是“垃圾进入,垃圾出来”,实际效果往往比不使用机器学习还要糟糕。
同时,机器学习系统可能会有特性异常、模型异常、数据异常等等其他软件系统所不具备的各种问题。如果在生产系统中对这些情况没有一个估计和处理,机器学习搜索系统往往也会不尽人意。
更加智能的搜索系统
很明显,搜索系统不会仅仅停留在应用普通的机器学习算法。近几年,搜索系统的发展有两个方面。
一方面,当然就是依靠深度学习发展的春风,不少学者和研究人员都在思考,如何能够利用深度学习技术让搜索系统更上一层楼。在这方面的研发中,不仅仅是针对普通的深度学习算法,而是看如何应用深度学习所特有的一些模式,比如深度强化学习等方式来重新思考搜索问题。
另一方面,就是从用户的角度来说,研究更加有意义的评测方式。也就是说,如何能够真正抓住用户对这个系统的偏好,并且能够进一步地去优化这个系统的性能。
小结
今天我为你讲了现代搜索技术框架的发展,并简单提及了搜索系统目前发展的趋势 。 一起来回顾下要点:第一,我们讲了基于文本匹配的经典搜索系统的特点;第二,我们讲了基于机器学习的搜索系统的特点。
最后,给你留一个思考题,在机器学习和深度学习的思潮中,传统搜索系统的核心,也就是我们说过的索引,能否依靠机器学习来生成呢?

View File

@ -0,0 +1,85 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
047 多轮打分系统概述
周一我为你介绍了搜索系统的一个宏观分类,包括传统的文本匹配信息检索系统和机器学习信息检索系统。这个分类可以让你非常清晰地了解信息搜索系统的历史进程,并对这两种搜索系统的特点有所了解。
今天我们就来剖析搜索系统的另一个框架体系多轮打分Scoring系统。
多轮打分系统综述
什么是多轮打分系统?为什么搜索系统需要多轮打分?
我们拿上次介绍的机器学习搜索系统为例。从整体来说,机器学习搜索系统的目的是利用机器学习模型来预测文档和搜索关键字之间的相关性。那么,在理想状态下,针对每一个查询关键字,我们需要对数据集中的每一个文档进行打分。
如果是一个类似互联网搜索引擎的应用场景,那么理论上,每一个查询关键字都需要对几亿甚至十几亿的网页进行打分。显然,仅仅从这个数量级上来说,这样做都是不现实的。
从另一个方面来讲目前比较通用的机器学习模型特别是在排序问题上有强劲表现的树模型Tree Model比如GBDTGradient Boosted Decision Trees或者神经网络都有较高的计算时间复杂度。要想在实时响应的反应时间内例如几百毫秒内对相对比较多我们这里说的是几千甚至上万的文档进行打分是很困难的我们刚才提到的整个数据集中是有几亿甚至十几亿的文档那就更加困难了。
于是在这样的情况下,我们就需要有这么一种机制:对于每个查询关键字而言,能够先有一个方法在整个数据集上快速评价出几百到几千不等(视具体应用)的文档,然后在这个几百到几千不等的集合上运用复杂的模型进行计算并且排序。 这种需要对文档进行两轮打分的流程叫做“两轮打分框架”(见参考文献[3])。
第一轮打分又常常被称作“顶部K”Top-K提取。你可以看到在这样的机制下相对比较简单的模型和方法可以用于第一轮打分因为这一轮是有可能在全部的数据集上进行操作的。这一轮也是被优化得最彻底的一轮毕竟需要在海量的数据集中快速找到几百或者几千个合适文档。
然后在第二轮,当文档的数目已经降到了几千甚至几百的时候,我们就可以使用比较复杂的模型了。这其实也是整个多轮打分的一个目的,那就是可以在一个比较适量的数据集上应用复杂模型。
实际上我们不仅可以对文档进行两轮打分,甚至可以扩展到多轮打分,比如雅虎搜索引擎的“三轮打分机制”(见参考文献[1]。第三轮根据第二轮打分所产生的文档“上下文特征“Contextual Feature从而可以进一步精准地提高搜索结果的质量。类似的思想也可以借鉴参考文献[2]。
一般来说,多轮打分系统有两个明显的特点。一个特点是每一轮都比上一轮使用的文档数目要少。也就是说,多轮打分的目的是每经过一轮都筛选出更少的文档。另外一个特点是每一轮使用的特性数目都比上一轮更加复杂,模型也更加复杂。
第一轮“顶部K提取”
我刚才说了一下多轮打分系统的机理。现在我们来看一看第一轮打分也就是俗称的“顶部K提取”都有什么技术特点。
“顶部K提取”的一个核心问题就是如何快速地从非常巨大的数据集中返回有价值的几百到几千的文档。这就需要对获取文档的数据结构以及使用的模型有一定的要求。
首先“倒排索引”Inverted Index是一个非常重要的机制。是否能够建立有效的索引是第一轮打分能否达到目的的关键。
传统的倒排索引已经可以在很大程度上有效地“削减”没必要的文档。我再简要地讲解一下这个基本的数据结构,我们一起来复习一下倒排索引的内容。索引中的“字段”是某一个查询关键字,而每个字段所对应的则是包含这个查询关键字的文档列表。
这个文档列表大多按照某种重要的顺序排列。比如,某个文档整体和查询关键字的相关度大,那么就会排列到这个列表的前面。当然,也并不是所有包含这个查询关键字的文档一定都会包含到这个列表中。另外,之所以叫做“索引”,也是因为这个列表中并不实际存储整个文档,而往往是只存储文档的编号。
除了最基本的通过索引来提取文档以外我们还可以通过一些简单的模型来提取文档比如线性模型。一个经典的方法叫做“WAND操作符”WAND Operator参见参考资料[4])。
当然严格来讲WAND操作符并不是把一个通用的、普遍的线性模型应用到文档索引上而是说如果我们能够把模型给简化为只有正系数的线性模型那么整个模型其实可以看做是两个向量的点积而WAND则是对点积在索引上的一种优化。
当然,研发人员不仅想把线性模型直接使用到倒排索引上。实际上,这么多年来也有不少的尝试,希望能够把树模型直接应用到倒排搜索上。但是,因为我们之前提到的性能因素,通常情况下树模型都没法直接应用(这里提供一个参考文档[5]供你阅读)。应该说,树模型的优化还处在一个研究的阶段。
第二轮或以后轮数的重排
当我们结束了第一轮之后就来到了第二个阶段也是经常所说的“重排”Re-rank阶段。在这个阶段文档已经从索引中到达了内存。一般来说在比较普通的架构下所有的几百到几千不等的文档在这个时候已经整合到了某一台机器的内存中。
我们在思考第一轮和第二轮的时候,需要先理解这两轮的一个重要区别,才能知道什么样的模型能够比较好地应用在这两个不同的场景中。
首先第一轮必须能够应用在搜索倒排索引上。现代的索引模式往往是部署在很多的节点机器上的。也就是说每一个节点都拥有一部分但不是完整的文档集合。这也就导致了我们之前介绍过的单点法Pointwise、配对法Pairwise和列表法Listwise这些机器学习方法很难在索引的这个级别直接使用因为每一个节点为了计算效率问题只能访问到一部分的文档并且进行打分。
因此,两轮的最大区别就是,第一轮一般都是针对单一文档的打分,而只有第二轮才能利用上配对法或者列表法针对文档打分。我们之前曾经提过,配对法或者列表法都比单点法的效果要好,因此如何平衡这两者在两轮中的表现差异就变得越来越重要了。
这里我简单提一下第二轮之后的其他轮数。当我们应用了第二轮之后,其实基本上就已经产生了最后的结果集合。为什么还需要其他轮数呢?
我们可能还需要其他轮数至少有两个原因。
第一,很多搜索系统中,相关排序只是搜索系统的一个方面。搜索系统还可能引入“多元化”或者其他的“商业规则”。这些规则或者进一步的重新排序很难完整地在前面的轮数中进行。
第二,当最后文档集合生成之后,有证据表明(参考文献[1]),我们还可以生成一些更加精细的特性来进一步提高排序的精度。因此,多轮打分是值得探索的。
小结
今天我为你讲了现代搜索技术中一个很重要的思路,多轮打分系统 。 一起来回顾下要点:第一,我们讲了为什么需要多轮打分,多轮打分的核心思路是什么。第二,我们分别讲了第一轮和第二轮以及后面轮数的一些特点。
最后,给你留一个思考题,在多轮打分系统的情况下,如何评测第一轮模型的好坏呢?
参考文献
Dawei Yin, Yuening Hu, Jiliang Tang, Tim Daly, Mianwei Zhou, Hua Ouyang, Jianhui Chen, Changsung Kang, Hongbo Deng, Chikashi Nobata, Jean-Marc Langlois, and Yi Chang. Ranking Relevance in Yahoo Search. Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD 16). ACM, New York, NY, USA, 323-332, 2016.
Ruey-Cheng Chen, Luke Gallagher, Roi Blanco, and J. Shane Culpepper. Efficient Cost-Aware Cascade Ranking in Multi-Stage Retrieval. Proceedings of the 40th International ACM SIGIR Conference on Research and Development in Information Retrieval (SIGIR 17). ACM, New York, NY, USA, 445-454, 2017.
Van Dang, Michael Bendersky, and W. Bruce Croft. Two-Stage learning to rank for information retrieval. Proceedings of the 35th European conference on Advances in Information Retrieval (ECIR13), Pavel Serdyukov, Pavel Braslavski, Sergei O. Kuznetsov, Jaap Kamps, and Stefan Rüger (Eds.). Springer-Verlag, Berlin, Heidelberg, 423-434, 2013.
Andrei Z. Broder, David Carmel, Michael Herscovici, Aya Soffer, and Jason Zien. Efficient query evaluation using a two-level retrieval process. Proceedings of the twelfth international conference on Information and knowledge management (CIKM 03). ACM, New York, NY, USA, 426-434, 2003.
N. Asadi, J. Lin and A. P. de Vries. Runtime Optimizations for Tree-Based Machine Learning Models. In IEEE Transactions on Knowledge and Data Engineering, vol. 26, no. 9, 2281-2292, 2014.

View File

@ -0,0 +1,67 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
048 搜索索引及其相关技术概述
本周我们分享的主题是从宏观上来剖析现代搜索架构。周一我介绍了搜索系统的一个大的分类一类是从20世纪50年代开始研发并使用的传统文本匹配信息检索系统一类是从2000年开始发展并逐渐成熟的机器学习信息检索系统。周三我们剖析了搜索系统的另一个框架体系多轮打分系统阐述了为什么需要多轮打分以及每一轮打分又有什么特性。
今天我们来看一个在本周已经反复涉及到的话题倒排索引Inverted Index。一起来聊聊它的核心技术。值得注意的是关于索引的很多话题其实都会牵涉到搜索中的“查询关键字处理”Query Processing我们今天的分享就主要来谈谈索引及相关技术在“查询关键字处理”这个场景下的应用。
经典的索引结构
经典的索引结构由“字段”Field和对应的列表组成。一般来说“字段”就是某一个查询关键字。在英文里这就是一个单独的单词在中文里这也许就是一个词或者短语。每个字段所对应的列表就是包含这个查询关键字的文档列表。
有两点值得注意。
第一,在文档列表里的文档,大多按照某种重要的顺序排列,这方便我们首先提取重要性高的文档。比如,某个文档整体和查询关键字的相关度大,那么就会排列到这个列表的前面。
第二,对于每个字段,也就是查询关键字而言,所有包含这个查询关键字的文档并不一定都会包含到这个列表中,这个列表可以是一个节选。
另外,我们前面已经讲过了,之所以叫做“索引”,也是因为这个列表中并不实际存储整个文档,往往是只存储文档的编号。
如果用户输入的查询关键字包含多个词组根据这个最基础的结构我们可以很容易地获取包含所有关键字的文档集合。这个操作仅仅相当于在多个列表中做“归并排序”Merge Sort
除了在索引中仅仅保存最基本的文档标号信息以外另外一些文档的基础信息也可以一并存放在索引中。比如经常存放的信息还有文档包含某个查询关键字的次数。保存次数信息本质上是在保存“词频”Term Frequency这个文档特性。
我们前面分享经典的信息检索模型的时候介绍过很多模型例如TF-IDF、BM25或者语言模型都对词频的计算有很强的依赖。在索引中存放词频信息有助于近似计算这些基础的检索模型。
另外一个经常存放的信息就是查询关键字在文档中出现的位置Position。位置信息对于有多个查询关键字的时候尤为重要。比如我们要搜索的词组是“五道口电影院”。在这样的情况下我们非常希望“五道口”在某个文档中出现的位置和“电影院”在文档中出现的位置相邻。这样我们可以确认这个文档的确是关于“五道口电影院”的而不是恰好含有“五道口”和“电影院”这两个词。
同时,位置信息还可以帮助搜索引擎生成搜索结果界面上的“结果摘要”信息。我们经常看到搜索结果页面上有几句话的摘要信息,这个信息就需要查询关键字的位置来生成。
索引技术
除了最基础的索引技术以外,研发人员开发了多种技术让索引更加高效。
第一个技术当然就是希望对索引进行压缩。索引信息很快就会随着可能的关键字数目的膨胀而扩展。索引中每一个关键字所对应的文档列表也会越来越庞大。因此能否快速处理索引信息并为后续的计算节约时间就变得非常关键。本周三我们分享了多轮打分系统。多轮打分系统的的一个重要思想就是整个流程必须在几百毫秒的响应时间内完成。因此每一个步骤包括从索引中提取“顶部K个文档”的过程都需要很快捷。
压缩技术博大精深,我们在今天的分享中就不展开讨论这部分的内容了。在这里,我们只需要从高维度上把握这个问题的一个基本思路。索引的一个基本信息就是相对于某个查询关键字的文档列表。而存储在文档列表里的并不是文档本身的数据,而是文档的某种信息,比如文档本身的编号。而编号就是数字,文档列表最终就是一个数字序列。压缩技术中有很多算法就是对一个数字序列进行压缩。
那么到底怎样才能起到压缩的作用呢我们这里举一个例子。比方说有一种压缩算法是基于一种叫“差值编码”Delta Encoding的技术。简单来说就是不直接记录文档编号本身而是按照文档编号的顺序记录文档编号之间的差值。
对于某些非常频繁的查询关键字而言,这些词汇有可能会出现在非常多、甚至是绝大多数的文档中。而采用这种“差值编码”来对文档列表进行重新编排,我们就可以用一组很小的数(这些数表达两个相邻文档编号的差值)来代表文档列表。当然,这种方法对于文档很少的查询关键字效果肯定不明显。同时,这种技术也要求文档列表不按照相关度排序,而要按照文档的编号排序。
在索引的发展过程中也开发出了一些很细小的技术比如“略过”Skipping。简单来说这个技术就是当我们有多个查询关键字的时候而且这些关键字之间的频率有非常大的差距我们可以略过一些文档。
例如在“北京地铁出行”这个组合中“北京”有可能在整个数据集中出现的频率是“地铁出行”的几倍甚至十几倍、上百倍因此我们其实并不需要搜索所有包含“北京”的文档因为最终需要的仅仅是同时包含两个关键字的这样一个交集。因此在处理“北京”的文档序列的时候我们可以“略过”K个文档然后看有没有到达下一个包含“地铁出行”的文档。这里的K当然是一个参数需要尝试。有了这样的思路处理多个查询关键字时就可以很显著地提升效果。
查询关键字处理
最后我们来谈一谈查询关键字处理。说得通俗易懂一点,就是如何从索引中提取出相关的文档并计算分数。这里有两种基本思路。
第一种思路叫作“文档优先”Document-at-a-Time计算策略。简单来说就是我们首先从索引中找到所有查询关键字所对应的文档集合。比如我们处理“北京地铁出行”这一查询关键字组合我们先取出所有包含这些关键字的文档然后保持一个“优先队列”Priority Queue来保存分数最高的K个文档再针对取出来的文档分别计算分数这里的分数有可能就是词频的某种简化检索模型计算完分数之后我们把分数压入优先队列中。
第二种思路和“文档优先”思路相对应叫作“词优先”Term-at-a-Time计算策略。在这种思路下我们对所有查询关键字词组中的每一个字一一进行处理。请注意这里的第一个步骤其实是一样的我们依然要先取出所有的文档集合。但是这一步之后我们先处理包含“北京”的文档得到所有文档分数的一个部分值然后再处理“地铁出行”在刚才计算的部分值上进行更新取得最后的分数。
在实际应用中这两种策略是更加复杂的优化查询关键字处理的基础在这两种思路的基础上演化出了很多高级算法不仅能快速地处理文字特性还包括我们讲过的类似WAND操作符这样能够模拟线性模型的算法。
小结
今天我为你讲了现代搜索技术的一个核心组成部分,那就是倒排索引系统。 一起来回顾下要点:第一,我们讲了索引系统的基本组成和原理。第二,我们讲了索引相关技术的一个概况,重点介绍了压缩以及“略过”的含义。第三,简要讲解了查询关键字处理的两种最基础的策略。
最后,给你留一个思考题,如果我们既有图像信息又有文字信息,那该如何构建我们的索引呢?

View File

@ -0,0 +1,79 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
049 PageRank算法的核心思想是什么
上周我们介绍了信息搜索系统的历史进程,剖析了搜索系统的多轮打分系统,还深入探讨了倒排索引,聊了聊它的核心技术。
这周我要和你分享的是在互联网搜索引擎兴起之后的一个研发需要,那就是如何理解网页和网页之间的关系,特别是怎么从这些关系中提取网页中除文字以外的其他特性。这部分的一些核心算法曾是提高搜索引擎质量的重要推进力量。另外,我们这周要分享的算法也适用于其他能够把信息用结点与结点关系来表达的信息网络。
今天我们先看一看用图来表达网页与网页之间的关系并且计算网页重要性的经典算法PageRank。
PageRank的简要历史
时至今日谢尔盖·布林Sergey Brin和拉里·佩奇Larry Page作为Google这一雄厚科技帝国的创始人已经耳熟能详。但在1995年他们两人还都是在斯坦福大学计算机系苦读的博士生。那个年代互联网方兴未艾。雅虎作为信息时代的第一代巨人诞生了布林和佩奇都希望能够创立属于自己的搜索引擎。1998年夏天两个人都暂时离开斯坦福大学的博士生项目转而全职投入到Google的研发工作中。他们把整个项目的一个总结发表在了1998年的万维网国际会议上 WWW7the seventh international conference on World Wide Web见参考文献[1]。这是PageRank算法的第一次完整表述。
PageRank一经提出就在学术界引起了很大反响各类变形以及对PageRank的各种解释和分析层出不穷。在这之后很长的一段时间里PageRank几乎成了网页链接分析的代名词。给你推荐一篇参考文献[2],作为进一步深入了解的阅读资料。
PageRank的基本原理
我在这里先介绍一下PageRank的最基本形式这也是布林和佩奇最早发表PageRank时的思路。
首先我们来看一下每一个网页的周边结构。每一个网页都有一个“输出链接”Outlink的集合。这里输出链接指的是从当前网页出发所指向的其他页面。比如从页面A有一个链接到页面B。那么B就是A的输出链接。根据这个定义可以同样定义“输入链接”Inlink指的就是指向当前页面的其他页面。比如页面C指向页面A那么C就是A的输入链接。
有了输入链接和输出链接的概念后下面我们来定义一个页面的PageRank。我们假定每一个页面都有一个值叫作PageRank来衡量这个页面的重要程度。这个值是这么定义的当前页面I的PageRank值是I的所有输入链接PageRank值的加权和。
那么权重是多少呢对于I的某一个输入链接J假设其有N个输出链接那么这个权重就是N分之一。也就是说J把自己的PageRank的N分之一分给I。从这个意义上来看I的PageRank就是其所有输入链接把他们自身的PageRank按照他们各自输出链接的比例分配给I。谁的输出链接多谁分配的就少一些反之谁的输出链接少谁分配的就多一些。这是一个非常形象直观的定义。
然而有了这个定义还是远远不够的因为在这个定义下页面I和页面J以及其他任何页面的PageRank值是事先不知道的。也就是等式两边都有未知数这看上去是个无解的问题。
布林和佩奇在他们的论文中采用了一种迭代算法。这个算法很直观那就是既然不知道这些PageRank的值那我们就给他们一组初始值这个初始值可以是这样的情形所有页面有相同的PageRank值。然后根据我们上面所说的这个定义更新所有页面的PageRank值。就这么一遍一遍地更新下去直到所有页面的PageRank不再发生很大变化或者说最后收敛到一个固定值为止。他们在文章中展示了实际计算的情况往往是在比较少的迭代次数后PageRank值就能够收敛。
以上就是整个PageRank算法的基本思想和一种迭代算法。
PageRank算法的改进
完全按照我们上面介绍的这个最原始的PageRank算法布林和佩奇很快就遇到了麻烦。
第一个麻烦就是有一些页面并没有输出链接比如某些PDF文件或者一些图片文件。由于没有输出链接这些页面只能聚集从上游输入链接散发过来的PageRank值而不能把自己的PageRank值分发出去。这样的结果就是这些页面成为一些“悬空”Dangling结点。悬空结点存在的最大问题就是会使得PageRank的计算变得不收敛。这些结点成了PageRank值的“黑洞”导致悬空结点的PageRank值越来越大直至“吸干”其他所有输入链接的值。
要解决这个问题就要为悬空结点“引流”能够把这些点的值分发出去、引出去。谢尔盖和拉里找到的一个方法是对于每一个悬空结点都认为这个结点能够随机到达整个网络上的其他任意一个结点。也就相当于人工地从这个结点连接到所有页面的一个结点让当前悬空结点的PageRank能够“均匀”地分散出去到其他所有的结点这就解决了悬空结点的问题。
然而原始的PageRank还存在其他问题。要想保证PageRank的收敛性并且能够收敛到唯一解我们还需要第二个改进。第二个改进就是即便一个页面有自然的输出链接我们也需要一个机制能够从这个页面跳转到其他任何一个页面。这也就是模拟假设一个用户已经浏览到了某个页面一方面用户可以顺着这个页面提供的输出链接继续浏览下去另一方面这个用户可以随机跳转到其他任何一个页面。
有了这个机制以后对于所有的结点来说PageRank的分配也就自然地产生了变化。在之前的定义中每个页面仅仅把自己的PageRank值输送给自己原生的所有输出链接中。而现在这是一部分的“分享”另外一部分还包括把自己的PageRank值分享到所有的页面。当然后者的总量应该比前者要少。于是这里可以引入一个参数来控制有多大的比例我们是顺着输出链接走而多大的比例跳转其他页面。通常情况下这个参数的取值范围大约是60%~85%。
有了这两个改进之后整个网络上的每个页面实际上已经可以到达其他任何页面。也就是说整个页面网络成了一个完全联通的图PageRank算法就有了唯一的收敛的解。
PageRank分析
PageRank被提出后不久就有学者开始针对PageRank模型和算法的性质进行分析。大家很快发现还有一些其他的方法可以对PageRank进行解释。
第一种比较流行的也是更加正规的解释PageRank的方法是把我们刚才说的这个分配等式写成矩阵的形式。那么整个算法就变成了一个标准的求解一个随机矩阵的“左特征向量”的过程。这个随机矩阵就是我们刚才讲的经过了两次修改后的跳转规律的矩阵形式。而刚才所说的迭代方法正好就是求解特征向量的“乘幂法”Power Method。在一定条件下的随机矩阵经过乘幂法就一定能够得到一个唯一解。
另外一种解释是把刚才我们说的这个矩阵形式进行一次代数变形也就是把等式两边的各项都移动到等式的一边而另一边自然就是0。那么整个式子就变了一个“线性系统”的求解过程。也就是说从代数的角度来解释整个PageRank的求解过程。
小结
今天我为你讲了现代搜索技术中的一个重要分支链接分析中最重要的算法PageRank的核心思想 。 一起来回顾下要点第一我们讲了PageRank的一些简明历史和算法最原始的定义和思路 。第二我们讲了PageRank的两种改进。第三我们简要地介绍了针对PageRank的两种解释方法。
最后给你留一个思考题除了乘幂法你觉得还有什么方法可以用来求解PageRank值
参考文献
Sergey Brin and Lawrence Page. The anatomy of a large-scale hypertextual Web search engine. Proceedings of the seventh international conference on World Wide Web 7 (WWW7), Philip H. Enslow, Jr. and Allen Ellis (Eds.). Elsevier Science Publishers B. V., Amsterdam, The Netherlands, The Netherlands, 107-117, 1998.
Langville, Amy N.; Meyer, Carl D. Deeper Inside PageRank. Internet Math. no. 3, 335-380, 2003.
论文链接
The anatomy of a large-scale hypertextual Web search engine
Deeper Inside PageRank

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
050 经典图算法之HITS
这周我们分享的内容是如何理解网页和网页之间的关系。周一我们介绍了用图Graph来表达网页与网页之间的关系并计算网页的重要性就是经典算法PageRank。今天我来介绍一下PageRank的姊妹算法HITS算法。
HITS的简要历史
HITS是Hypertext-Induced Topic Search算法的简称。这个算法是由康奈尔大学计算机科学教授乔·克莱恩堡Jon Kleinberg于1998年发明的正好和我们周一讲的布林和佩奇发表PageRank算法是同一年。
这里有必要简单介绍一下乔这个人。乔于1971年出生在马萨诸塞州波士顿。1993年他毕业于康奈尔大学获得计算机科学学士学位并于1996年从麻省理工大学获得计算机博士学位。1998的时候乔正在位于美国西海岸硅谷地区的IBM阿尔玛登Almaden研究院做博士后研究。HITS的工作最早发表于1998年在旧金山举办的第九届ACM-SIAM离散算法年会上详细论述可参阅参考文献
乔目前是美国国家工程院National Academy of Engineering和美国自然与人文科学院American Academy of Arts and Sciences院士。顺便提一下乔的弟弟罗伯特·克莱恩堡也在康奈尔大学计算机系任教职。
HITS的基本原理
在介绍HITS算法的基本原理之前我们首先来复习一下网页的网络结构。每一个网页都有一个“输出链接”Outlink的集合。输出链接指的是从当前网页出发所指向的其他页面比如从页面A有一个链接到页面B那么B就是A的输出链接。根据这个定义我们来看“输入链接”Inlink指的就是指向当前页面的其他页面比如页面C指向页面A那么C就是A的输入链接。
要理解HITS算法我们还需要引入一组概念“权威”Authority结点和“枢纽”Hub结点。这两类结点到底是什么意思呢
HITS给出了一种“循环”的定义好的“权威”结点是很多“枢纽”结点的输出链接好的“枢纽”结点则指向很多好的“权威”结点。这种循环定义我们在PageRank的定义中已经见识过了。
很明显,要用数学的方法来表述权威结点和枢纽结点之间的关系就必须要为每一个页面准备两个值。因为从直觉上来说,不可能有一个页面完全是权威,也不可能有一个页面完全是枢纽。绝大多数页面都在这两种角色中转换,或者说同时扮演这两类角色。
数学上对于每一个页面I我们用X来表达这个页面的“权威值”用Y来表达这个页面的“枢纽值”。那么一个最直观的定义对于I的权威值X来说它是所有I页面的输入链接的枢纽值的总和。同理I的枢纽值是所有I页面输出链接的权威值的总和。这就是HITS算法的原始定义。
我们可以看到如果I页面的输入链接的枢纽值大说明I页面经常被一些好的“枢纽”结点链接到那么I自身的权威性自然也就增加了。反之如果I能够经常指向好的“权威”结点那I自身的“枢纽”性质也就显得重要了。
当然和PageRank值一样X和Y在HITS算法里也都是事先不可知的。因此HITS算法的重点就是要求解X和Y。如果把所有页面的X和Y都表达成向量的形式那么HITS算法可以写成X是矩阵L的转置和Y的乘积而Y是矩阵L和X的乘积这里的矩阵L就是一个邻接矩阵每一行列表达某两个页面是否相连。进行一下代数变形我们就可以得到X其实是一个矩阵A乘以X这里的A是L的转置乘以L。Y其实是一个矩阵B乘以Y这里的B是L乘以L的转置。
于是惊人的一点出现了那就是HITS算法其实是需要求解矩阵A或者矩阵B的主特征向量也就是特征值最大所对应的特征向量用于求解X或者Y。这一点和PageRank用矩阵表达的形式不谋而和。也就是说尽管PageRank和HITS在思路和概念上完全不同并且在最初的定义式上南辕北辙但是经过一番变形之后我们能够把两者都划归为某种形式的矩阵求解特征向量的问题。
实际上把图表达为矩阵并且通过特征向量对图的一些特性进行分析是图算法中的一个重要分支当然我们这里说的主要是最大的值对应的特征向量还有其他的特征向量也有含义。既然我们已经知道了需要计算最大的特征向量那么之前计算PageRank所使用的“乘幂法”Power Method在这里也是可以使用的我们在这里就不展开了。
如何把HITS算法用于搜索中呢最开始提出HITS的时候是这么使用的。
首先我们根据某个查询关键字构建一个“相邻图”Neighborhood Graph。这个图包括所有和这个查询关键字相关的页面。这里我们可以简化为所有包含查询关键字的页面。这一步在现代搜索引擎中通过“倒排索引”Inverted Index就可以很容易地得到。
有了这个相邻图以后,我们根据这个图建立邻接矩阵,然后就可以通过邻接矩阵计算这些结点的权威值和枢纽值。当计算出这两组值之后,我们就可以根据这两组值给用户展现两种网页排序的结果,分别是根据不同的假设。
值得注意的是PageRank是“查询关键字无关”Query-Independent的算法也就是说每个页面的PageRank值并不随着查询关键字的不同而产生不同。而HITS算法是“查询关键字相关”Query-Dependent的算法。从这一点来说HITS就和PageRank有本质的不同。
HITS算法的一些特点
HITS算法依靠这种迭代的方法来计算权威值和枢纽值你一定很好奇这样的计算究竟收敛吗是不是也需要像PageRank一样来进行特别的处理呢
答案是HITS一定是收敛的。这点比原始的PageRank情况要好。然而HITS在原始的情况下不一定收敛到唯一一组权威值和枢纽值也就是说解是不唯一的。因此我们其实需要对HITS进行一部分类似于PageRank的处理那就是让HITS的邻接矩阵里面所有的结点都能够达到其他任何结点只是以比较小的概率。经过这样修改HITS就能够收敛到唯一的权威值和枢纽值了。
HITS算法的好处是为用户提供了一种全新的视角对于同一个查询关键字HITS提供的权威排序和枢纽排序能够帮助用户理解自己的需求。
当然HITS的弱点也来自于这个依赖于查询关键字的问题。如果把所有的计算都留在用户输入查询关键字以后并且需要在响应时间内计算出所有的权威值和枢纽值然后进行排序这里面的计算量是很大的。所以后来有研究者开始使用全局的网页图提前来计算所有页面的权威值和枢纽值然而这样做就失去了对某一个关键字的相关信息。
小结
今天我为你讲了HITS算法的核心思想 。 一起来回顾下要点第一我们讲了HITS的一些简明历史。第二我们讲了HITS最原始的定义和算法并且联系PageRank讲了两者的异同之处。第三我们分析了HITS的一些特点。
最后,给你留一个思考题,有没有办法把权威值和枢纽值所对应的两个排序合并成为一个排序呢?
参考文献
Jon M. Kleinberg. Authoritative sources in a hyperlinked environment. J. ACM 46, 5 (September 1999), 604-6321999.
论文链接
Authoritative sources in a hyperlinked environment

View File

@ -0,0 +1,82 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
052 机器学习排序算法经典模型RankSVM
到目前为止,我们在专栏里已经讨论了关于搜索引擎方方面面的很多话题,包括经典的信息检索技术、查询关键字理解、文档理解以及现代搜索引擎的架构等等 。同时我们也从机器学习角度出发对搜索引擎的最核心部分也就是排序算法进行了最基本的分享囊括了单点法排序学习Pointwise Learning to Rank、配对法排序学习Pairwise Learning to Rank以及列表法排序学习Listwise Learning to Rank相信你应该对这类算法的大概内容有所掌握。
那么,这周我们就来看看机器学习排序算法中几个经典的模型,希望能够通过这几个经典的算法为你深入学习和研究排序算法指明方向。
今天我就来分享配对法排序中最有价值一个算法排序支持向量机RankSVM。这个算法的核心思想是应用支持向量机到序列数据中试图对数据间的顺序直接进行建模。
排序支持向量机的历史
20世纪90年代中后期受统计学习理论Statistical Learning Theory 思想和风险最小化框架Risk Minimization Framework趋于成熟的影响支持向量机逐渐成为当时机器学习界的主流模型。一时间各个应用领域的学者和工程师都在思考如何把支持向量机利用到自己的问题领域上从而获得更好的效果。
拉夫⋅赫博里奇Ralf Herbrich发表于1999年[1]和2000年[2]的论文中讨论了如何把支持向量机和有序回归Ordinal Regression结合起来。赫博里奇当时在柏林科技大学Technical University of Berlin攻读博士学位。2000年到2011年他在微软研究院和Bing任职从事机器学习特别是贝叶斯方法Bayesian method的研究。2011年到2012年他在Facebook短暂任职后于2012年加入了亚马逊负责机器学习的研发工作并且担任在柏林的研发中心主管经理Managing Director。尽管赫博里奇很早提出了把有序回归和支持向量机结合的思路但是当时的论文并没有真正地把这个新模型用于大规模搜索系统的验证。
更加完整地对排序支持向量机在搜索中的应用进行论述来自于康奈尔大学教授索斯腾⋅乔基姆斯Thorsten Joachims以及他和合作者们发表的一系列论文见参考文献[3]、[4]、[5]和[6]。索斯滕我们前面介绍过他是机器学习界享有盛誉的学者是ACM和AAAI的双料院士他所有论文的引用数超过4万次他获得过一系列奖项包括我们前面讲的2017年ACM KDD的时间检验奖等等。
排序支持向量机模型
在说明排序支持向量机之前,我们先来简要地回顾一下支持向量机的基本思想。
在二分分类问题中Binary Classification线性支持向量机的核心思想是找到一个“超平面”Hyperplane把正例和负例完美分割开。在诸多可能的超平面中支持向量机尝试找到距离两部分数据点边界距离最远的那一个。这也就是为什么有时候支持向量机又被称作是“边界最大化”Large Margin分类器。
如果问题并不是线性可分的情况支持向量机还可以借助“核技巧”Kernel Trick来把输入特性通过非线性变换转化到一个线性可分的情况。关于支持向量机的具体内容你可以参考各类机器学习教科书的论述。
要把支持向量机运用到排序场景下必须改变一下原来的问题设置。我们假设每个数据点由特性X和标签Y组成。这里的X代表当前文档的信息、文档与查询关键字的相关度、查询关键字的信息等方方面面关于文档以及查询关键字的属性。Y是一个代表相关度的整数通常情况下大于1。
那么在这样的设置下我们针对不同的X需要学习到一个模型能够准确地预测出Y的顺序。意思是说如果有两个数据点\(X_1\)和\(X_2\),他们对应的\(Y_1\)是3\(Y_2\)是5。因为\(Y_2\)大于\(Y_1\)(在这里,“大于”表明一个顺序),因此,一个合理的排序模型需要把\(X_1\)通过某种转换,使得到的结果小于同样的转换作用于\(X_2\)上。这里的转换,就是排序支持向量机需要学习到的模型。
具体说来在线性假设下排序支持向量机就是要学习到一组线性系数W使得在上面这个例子中\(X_2\)点积W之后的结果要大于\(X_1\)点积W的结果。当然对于整个数据集而言我们不仅仅需要对\(X_1\)和\(X_2\)这两个数据点进行合理预测还需要对所有的点以及他们之间所有的顺序关系进行建模。也就是说模型的参数W需要使得数据集上所有数据点的顺序关系的预测都准确。
很明显上述模型是非常严格的。而实际中很可能并不存在这样的W可以完全使得所有的X都满足这样的条件。这也就是我们之前说的线性不可分在排序中的情况。那么更加现实的一个定义是在允许有一定误差的情况下如何使得W可以准确预测所有数据之间的顺序关系并且W所确定的超平面到达两边数据的边界最大化这就是线性排序向量机的定义。
实际上在线性分类器的情境下线性排序向量机是针对数据配对Pair的差值进行建模的。回到刚才我们所说的例子线性排序向量机是把\(X_2\)减去\(X_1\)的差值当做新的特性向量然后学习W。也就是说原理上说整个支持向量机的所有理论和方法都可以不加改变地应用到这个新的特征向量空间中。当然这个情况仅仅针对线性分类器。
因为是针对两个数据点之间的关系进行建模,排序支持向量机也就成为配对法排序学习的一个经典模型。
排序支持向量机的难点
我们刚刚提到的排序支持向量机的定义方法虽然很直观但是有一个非常大的问题那就是复杂度是N的平方级这里的N是数据点的数目。原因是我们需要对数据点与点之间的所有配对进行建模。 当我们要对上万,甚至上百万的文档建模的时候,直接利用排序支持向量机的定义来求解模型参数显然是不可行的。
于是,针对排序支持向量机的研究和应用就集中在了如何能够降低计算复杂度这一难点上,使得算法可以在大规模数据上得以使用。
比较实用的算法是索斯腾在2006年发表的论文[6]中提出的这篇论文就是我们前面讲的2017年KDD时间检验奖建议你回去复习一下。这里我再简要地梳理一下要点。
这个算法的核心是重新思考了对排序支持向量机整个问题的设置把解决结构化向量机Structural SVM的一种算法CP算法Cutting-Plane使用到了排序支持向量机上。简单来说这个算法就是保持一个工作集合Working Set来存放当前循环时依然被违反的约束条件Constraints然后在下一轮中集中优化这部分工作集合的约束条件。整个流程开始于一个空的工作集合每一轮优化的是一个基于当前工作集合的支持向量机子问题。算法直到所有约束条件的误差小于一个全局的参数误差为止。
索斯腾在文章中详细证明了该算法的有效性和时间复杂度。相同的方法也使得排序支持向量机的算法能够转换成为更加计算有效的优化过程,在线性计算复杂度的情况下完成。
小结
今天我为你讲了利用机器学习技术来学习排序算法的一个基础的算法,排序支持向量机的基本原理。作为配对法排序学习的一个经典算法,排序支持向量机有着广泛的应用 。 一起来回顾下要点:第一,我们简要介绍了排序支持向量机提出的历史背景。第二,我们详细介绍了排序支持向量机的问题设置。第三,我们简要提及了排序支持向量机的难点和一个实用的算法。
最后给你留一个思考题排序支持向量机是否给了你一些启发让你可以把更加简单的对数几率分类器Logistic Regression应用到排序问题上呢
参考文献
Herbrich, R.; Graepel, T. & Obermayer, K. Support vector learning for ordinal regression. The Ninth International Conference on Artificial Neural Networks (ICANN 99), 1, 97-102 vol.1, 1999.
Herbrich, R.; Graepel, T. & Obermayer, K. Smola; Bartlett; Schoelkopf & Schuurmans (Eds.). Large margin rank boundaries for ordinal regression. Advances in Large Margin Classifiers, MIT Press, Cambridge, MA, 2000.
Tsochantaridis, I.; Hofmann, T.; Joachims, T. & Altun, Y. Support Vector Machine Learning for Interdependent and Structured Output Spaces. Proceedings of the Twenty-first International Conference on Machine Learning, ACM, 2004.
Joachims, T. A Support Vector Method for Multivariate Performance Measures. Proceedings of the 22Nd International Conference on Machine Learning, ACM, 377-384, 2005.
Tsochantaridis, I.; Joachims, T.; Hofmann, T. & Altun, Y. Large Margin Methods for Structured and Interdependent Output Variables. The Journal of Machine Learning Research, 6, 1453-1484, 2005.
Joachims, T. Training Linear SVMs in Linear Time. Proceedings of the 12th ACM SIGKDD International Conference on Knowledge Discovery and Data Mining, ACM, 217-226, 2006.
论文链接
Support vector learning for ordinal regression
Support Vector Machine Learning for Interdependent and Structured Output Spaces
A Support Vector Method for Multivariate Performance Measures
Large Margin Methods for Structured and Interdependent Output Variables
Training Linear SVMs in Linear Time

View File

@ -0,0 +1,77 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
053 机器学习排序算法经典模型GBDT
这周我们讨论机器学习排序算法中几个经典的模型周一分享了配对法排序中的一个经典算法即排序支持向量机RankSVM这个算法的核心思想是把支持向量机应用到有序数据中试图对数据间的顺序进行直接建模。
今天我们来聊一聊利用机器学习进行排序的一个重要算法“梯度增强决策树”Gradient Boosted Decision Tree。长期以来包括雅虎在内的很多商业搜索引擎都利用这种算法作为排序算法。
梯度增强决策树的历史
梯度回归决策树的思想来源于两个地方。首先是“增强算法”Boosting一种试图用弱学习器提升为强学习器的算法。这种算法中比较成熟的、有代表性的算法是由罗伯特⋅施派尔Robert Schapire和约阿夫⋅福伦德Yoav Freund所提出的AdaBoost算法[1]。因为这个算法两人于2003年获得理论计算机界的重要奖项“哥德尔奖”Gödel Prize。罗伯特之前在普林斯顿大学任计算机系教授目前在微软研究院的纽约实验室工作。约阿夫一直在加州大学圣地亚哥分校任计算机系教授。
增强算法的工作机制都比较类似,那就是先从初始训练集训练出一个基学习器,再根据基学习器的表现对训练样本分布进行调整,使得先前基学习器做错的训练样本在后续受到更多关注,然后基于调整后的样本分布来训练下一个基学习器。如此重复进行,直到基学习器数目达到事先制定的值,最终将所有的基学习器进行加权结合。如果你对“偏差-方差分解”Bias-Variance Decomposition有耳闻的话那么Boosting主要关注降低偏差。在实际效果中增强算法往往能基于泛化性能相当弱的学习器构建出很强的集成结果。
AdaBoost提出后不久机器学习学者和统计学家杰罗姆⋅弗赖德曼Jerome H. Friedman等人发表了一篇论文[2]从“统计视角”解释AdaBoost实质上是基于加性模型Additive Model以类似牛顿迭代法来优化指数损失函数Loss Function。于是受此启发杰米姆提出了“梯度增强”Gradient Boosting的想法。这也就是梯度回归决策树思想来源的第二个地方也是直接根源。如果你希望对“梯度增强”有进一步的了解可以见参考文献[3]。
最早把“梯度增强”的想法应用到搜索中是雅虎研究院的学者于2007年左右提出的[4]&[5]。之后Facebook把梯度增强决策树应用于新闻推荐中[6]。
梯度增强的思想核心
我们刚才简单讲了增强算法的思路,那么要想理解梯度增强决策树,就必须理解梯度增强的想法。
梯度增强首先还是增强算法的一个扩展,也是希望能用一系列的弱学习器来达到一个强学习器的效果,从而逼近目标变量的值,也就是我们常说的标签值。而根据加性模型的假设,这种逼近效果是这些弱学习器的一个加权平均。也就是说,最终的预测效果,是所有单个弱学习器的一个平均效果,只不过这个平均不是简单的平均,而是一个加权的效果。
那么如何来构造这些弱学习器和加权平均的权重呢?
梯度增强采用了一个统计学或者说是优化理论的视角,使得构造这些部分变得更加直观。
梯度增强的作者们意识到如果使用“梯度下降”Gradient Descent来优化一个目标函数最后的预测式可以写成一个加和的形式。也就是每一轮梯度的值和一个叫“学习速率”Learning Rate的参数共同叠加起来形成了最后的预测结果。这个观察非常重要如果把这个观察和我们的目标也就是构造弱学习器的加权平均联系起来看我们就会发现其实每个梯度的值就可以认为是一个弱学习器而学习速率就可以看作是某种意义上的权重。
有了这个思路,梯度增强的算法就很容易构造了。
首先这是一个迭代算法。每一轮迭代我们把当前所有学习器的加权平均结果当作这一轮的函数值然后求得针对某一个损失函数对于当前所有学习器的参数的一个梯度。然后我们利用某一个弱学习器算法可以是线性回归模型Linear Regression、对数几率模型Logistic Regression等来拟合这个梯度。最后我们利用“线查找”Line Search的方式找到权重。说得更直白一些那就是我们尝试利用一些简单的模型来拟合不同迭代轮数的梯度。
梯度增强的一个特点就是梯度下降本身带来的,那就是每一轮迭代一定是去拟合比上一轮小的一个梯度,函数对目标的整体拟合也是越来越好的。这其实也就是增强算法和梯度下降的一个完美结合。
梯度增强决策树以及在搜索的应用
理解了梯度增强,那么梯度增强决策树也就容易理解了。简单来说,梯度增强决策树就是利用决策树,这种最基本的学习器来当作弱学习器,去拟合梯度增强过程中的梯度。然后融合到整个梯度增强的过程中,最终,梯度增强决策树其实就是每一轮迭代都拟合一个新的决策树用来表达当前的梯度,然后跟前面已经有的决策树进行叠加。在整个过程中,决策树的形状,比如有多少层、总共有多少节点等,都是可以调整的或者学习的超参数。而总共有多少棵决策树,也就是有多少轮迭代是重要的调节参数,也是防止整个学习过程过拟合的重要手段。
参考文献[5]和[6],就是雅虎的科学家第一次把刚才提到的这个思路用于搜索问题中,训练排序算法。在应用的时候,有一些细节的调整,比如损失函数的设定。这里,作者们采用了配对法排序学习方法,那就是不直接去拟合相关度,而是拟合两个不同文档相关度的差值。具体来说,就是针对某一个查询关键字,我们利用算法来最小化对文档相关度差值的预测,也就是说我们不希望把相关度高的文档放到相关度低的后面。
在这些论文中,还有后续的很多研究中,利用梯度增强决策树算法进行排序训练得到的效果比当时的其他算法都有大幅度的提升。因此,这也就慢慢地奠定了梯度增强决策树作为一种普适的机器学习排序算法的地位。值得说明的是,梯度增强决策树的成功,一部分来自于增强算法,另一部分来自于把很多决策树堆积起来的想法。这两个思路都是在机器学习中被反复验证、行之有效的“模式”。
小结
今天我为你讲了梯度增强决策树算法的基本原理,这是一个利用机器学习技术来学习排序的基础算法。作为配对法排序学习的一个经典算法,梯度增强决策树算法有着广泛的应用 。 一起来回顾下要点:第一,我们简要介绍了梯度增强决策树提出的历史。第二,我们详细介绍了增强算法的核心以及梯度增强的思路。第三,我们简要介绍了梯度增强决策树的核心以及如何利用这个算法来训练排序问题。
最后,给你留一个思考题,梯度增强的思路能和神经网络模型结合吗?
参考文献
Yoav Freund and Robert E Schapire. A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting. J. Comput. Syst. Sci. 55, 1 (August 1997), 119-139, 1997.
Friedman, Jerome; Hastie, Trevor; Tibshirani, Robert. Additive logistic regression: a statistical view of boosting (With discussion and a rejoinder by the authors). Ann. Statist. 28 (2000), no. 2, 337407, 2000.
Friedman, Jerome H. Greedy function approximation: a gradient boosting machine. Annals of Statistics (2001): 11891232, 2001.
Zhaohui Zheng, Hongyuan Zha, Tong Zhang, Olivier Chapelle, Keke Chen, and Gordon Sun. A general boosting method and its application to learning ranking functions for web search. Proceedings of the 20th International Conference on Neural Information Processing Systems (NIPS07), J. C. Platt, D. Koller, Y. Singer, and S. T. Roweis (Eds.). Curran Associates Inc., USA, 1697-1704, 2007.
Zhaohui Zheng, Keke Chen, Gordon Sun, and Hongyuan Zha. A regression framework for learning ranking functions using relative relevance judgments. Proceedings of the 30th annual international ACM SIGIR conference on Research and development in information retrieval (SIGIR 07). ACM, New York, NY, USA, 287-294, 2007.
Xinran He, Junfeng Pan, Ou Jin, Tianbing Xu, Bo Liu, Tao Xu, Yanxin Shi, Antoine Atallah, Ralf Herbrich, Stuart Bowers, and Joaquin Quiñonero Candela. Practical Lessons from Predicting Clicks on Ads at Facebook. Proceedings of the Eighth International Workshop on Data Mining for Online Advertising (ADKDD14). ACM, New York, NY, USA, , Article 5 , 9 pages, 2014.
论文链接
A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting
Additive logistic regression: a statistical view of boosting
Greedy function approximation: a gradient boosting machine
A general boosting method and its application to learning ranking functions for web search
A regression framework for learning ranking functions using relative relevance judgments
Practical Lessons from Predicting Clicks on Ads at Facebook

View File

@ -0,0 +1,79 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
054 机器学习排序算法经典模型LambdaMART
在这周的时间里我们讨论机器学习排序算法中几个经典的模型。周一我们分享了排序支持向量机RankSVM这个算法的好处是模型是线性的容易理解。周三我们聊了梯度增强决策树Gradient Boosted Decision Tree长期以来这种算法被用在很多商业搜索引擎当中来作为排序算法。
今天我们来分享这一部分的最后一个经典模型LambdaMART。这是微软在Bing中使用了较长时间的模型也在机器学习排序这个领域享有盛誉。
LambdaMART的历史
LambdaMART的提出可以说是一个“三步曲”。
这里有一个核心人物叫克里斯多夫⋅博格斯Christopher J.C. Burges。博格斯早年从牛津大学物理学毕业之后又于布兰戴斯大学Brandeis University获得物理学博士学位他曾在麻省理工大学做过短暂的博士后研究之后来到贝尔实验室一待14年。2000年他来到微软研究院并一直在微软研究院从事机器学习和人工智能的研究工作直到2016年退休。可以说是博格斯领导的团队发明了微软搜索引擎Bing的算法。
LambdaMART的第一步来自于一个叫RankNet的思想[1]。这个模型发表于ICML 2005并且在10年之后获得ICML的时间检验奖。这也算是在深度学习火热之前利用神经网络进行大规模商业应用的经典案例。
RankNet之后博格斯的团队很快意识到了RankNet并不能直接优化搜索的评价指标。因此他们根据一个惊人的发现提出了LambdaRank这一重要方法[2]。LambdaRank的进步在于算法开始和搜索的评价指标也就是NDCG挂钩也就能够大幅度提高算法的精度。
LambdaRank之后博格斯的团队也认识到了当时从雅虎开始流行的使用“梯度增强”Gradient Boosting特别是“梯度增强决策树”GBDT的思路来进行排序算法的训练于是他们就把LambdaRank和GBDT的思想结合起来开发出了更加具有模型表现力的LambdaMART[3]。LambdaMART在之后的雅虎排序学习比赛中获得了最佳成绩。
RankNet的思想核心
要理解LambdaMART我们首先要从RankNet说起。其实有了排序支持向量机RankSVM的理论基础要理解RankNet就非常容易。RankNet是一个和排序支持向量机非常类似的配对法排序模型。也就是说RankNet尝试正确学习每组两两文档的顺序。那么怎么来定义这个所谓的两两文档的顺序呢
其实我们需要做的就是定义一个损失函数Loss Function来描述如何引导模型学习正确的两两关系。我们可以假设能够有文档两两关系的标签也就是某一个文档比另外一个文档更加相关的信息。这个信息可以是二元的比如+1代表更加相关-1代表更加不相关注意这里的“更加”表达了次序关系。
那么在理想状态下不管我们使用什么模型都希望模型的输出和这个标签信息是匹配的也就是说模型对于更加相关的文档应该输出更加高的预测值反之亦然。很自然我们能够使用一个二元分类器的办法来处理这样的关系。RankNet在这里使用了“对数几率损失函数”Logistic Loss其实就是希望能够利用“对数几率回归”Logistic Regression这一思想来处理这个二元关系。唯一的区别是这里的正例是两个文档的相对关系。
有了损失函数之后我们使用什么模型来最小化这个损失函数呢在RankNet中作者们使用了神经网络模型这也就是Net部分的由来。那么整个模型在这里就变得异常清晰那就是使用神经网络模型来对文档与文档之间的相对相关度进行建模而损失函数选用了“对数几率损失函数”。
LambdaRank和LambdaMART
尽管RankNet取得了一些成功但是文档的两两相对关系并不和搜索评价指标直接相关。我们之前讲过搜索评价指标例如NDCG或者MAP等都是直接建立在对于某一个查询关键字的相关文档的整个序列上或者至少是序列的头部Top-K的整个关系上的。因此RankNet并不能保证在NDCG这些指标上能够达到很好的效果因为毕竟没有直接或者间接优化这样的指标。
要想认识这一点其实很容易比如你可以设想对于某一个查询关键字有10个文档其中有两个相关的文档一个相关度是5另外一个相关度是3。那么很明显在一个理想的排序下这两个文档应该排在所有10个文档的头部。
现在我们假定相关度5的排在第4的位置而相关度3的排在第7的位置。RankNet会更愿意去调整相关度3的并且试图把其从第7往前挪因为这样就可以把其他不相关的挤下去然而更优化的办法应该是尝试先把相关度5的往更前面排。也就是说从NDCG的角度来说相关度高的文档没有排在前面受到的损失要大于相关度比较低的文档排在了下面。
NDCG和其他一系列搜索评价指标都是更加注重头部的相关度。关于这一点RankNet以及我们之前介绍的GBDT或者排序支持向量机都忽视了。
既然我们找到了问题,那么如何进行补救呢?
之前说到博格斯的团队有一个惊人的发现其实就在这里。他们发现RankNet的优化过程中使用到的梯度下降Gradient Descent算法需要求解损失函数针对模型的参数的梯度可以写成两个部分的乘积。在这里模型的参数其实就是神经网络中的各类系数。第一部分是损失函数针对模型的输出值的第二部分是模型输出值针对模型的参数的。第二个部分跟具体的模型有关系但是第一个部分没有。第一个部分跟怎么来定一个损失函数有关系。
在原始的RankNet定义中这当然就是“对数几率函数”定义下的损失函数的梯度。这个数值就是提醒RankNet还需要针对这个损失做多少修正。其实这个损失梯度不一定非得对应一个损失函数。这是博格斯的团队的一个重大发现只要这个损失的梯度能够表示指引函数的方向就行了。
那既然是这样能不能让这个损失的梯度和NDCG扯上边呢答案是可以的。也就是说我们只要定义两个文档之间的差距是这两个文档互换之后NDCG的变化量同时这个变化量等于之前所说的损失的梯度那么我们就可以指导RankNet去优化NDCG。在这里博格斯和其他作者把这个损失的梯度定义为Lambda因为整个过程是在优化一个排序所以新的方法叫作LambdaRank。
有了LambdaRank之后LambdaMART就变得水到渠成。Lambda是被定义为两个文档NDCG的变化量在实际运作中是用这个变化量乘以之前的对数几率所带来的梯度。那么只要这个Lambda可以计算模型就可以嫁接别的算法。于是博格斯的团队使用了在当时比神经网络更加流行的“梯度增强决策树”GBDT来作为学习器。不过梯度增强决策树在计算的时候需要计算一个梯度在这里就被直接接入Lambda的概念使得GBDT并不是直接优化二分分类问题而是一个改装了的二分分类问题也就是在优化的时候优先考虑能够进一步改进NDCG的方向。
小结
今天我为你讲了LambdaMART算法的基本原理。作为配对法和列表排序学习的一个混合经典算法LambdaMART在实际运用中有着强劲的表现 。 一起来回顾下要点第一我们简要介绍了LambdaMART提出的历史。第二我们详细介绍了LambdaMART的核心思路。
最后给你留一个思考题采用Lambda这样更改优化过程中的梯度计算虽然很形象但是有没有什么坏处
参考文献
Burges, C.; Shaked, T.; Renshaw, E.; Lazier, A.; Deeds, M.; Hamilton, N. & Hullender, G. Learning to Rank Using Gradient Descent. Proceedings of the 22nd International Conference on Machine Learning, ACM, 89-96, 2005.
Burges, C. J.; Ragno, R. & Le, Q. V. Schölkopf, B.; Platt, J. C. & Hoffman, T. (Eds.). Learning to Rank with Nonsmooth Cost Functions. Advances in Neural Information Processing Systems 19, MIT Press, 193-200, 2007.
Wu, Q.; Burges, C. J.; Svore, K. M. & Gao, J. Adapting Boosting for Information Retrieval Measures. Information Retrieval, Kluwer Academic Publishers, 13, 254-270, 2010.
Chapelle, O. & Chang, Y.Chapelle, O.; Chang, Y. & Liu, T.-Y. (Eds.). Yahoo! Learning to Rank Challenge Overview. Proceedings of the Learning to Rank Challenge, PMLR, 14, 1-24, 2011.
论文链接
Learning to Rank Using Gradient Descent
Learning to Rank with Nonsmooth Cost Functions
Adapting Boosting for Information Retrieval Measures
Yahoo! Learning to Rank Challenge Overview

View File

@ -0,0 +1,63 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
055 基于深度学习的搜索算法:深度结构化语义模型
近两个月我们集中系统地分享了搜索核心技术模块。做一个简单的内容梳理我们讲解了搜索引擎方方面面的话题从经典的信息检索技术、查询关键字理解、文档理解到现代搜索引擎的架构和索引的核心技术还从机器学习角度出发分享了搜索引擎的最核心部分也就是排序算法深入排序算法的细节讲解了排序支持向量机RankSVM、梯度增强决策树GBDT以及经典模型LambdaMART。至此整个人工智能领域关于搜索的经典话题也就告一段落了。
那么,这个星期,我们来看一些关于搜索算法的前沿思考。火热的深度学习不仅对图像、视频和音频这些领域产生了巨大的冲击,也对自然语言处理、甚至搜索领域有不小的影响。深度学习带给传统的模型和算法以新的建模能力和新的视角,为以前所不能完成的应用打下了基础。
今天我们来看一篇较早利用深度学习技术来进行搜索建模的论文《使用点击数据学习深度结构化的网络搜索语义模型》Learning deep structured semantic models for web search using clickthrough data。这篇论文阐述了一个深度结构化语义模型发表在第22届世界信息和知识管理大会CIKM 2013上。
论文背景介绍
发表于2013年的这篇论文应该算是比较早的直接使用深度学习中经验的论文。其主要目的是探索一些经典的深度学习方法能否在搜索的应用中得到合适的效果。
下面我们来了解一下这篇论文的作者群信息。
第一作者黄博森Po-Sen Huang是一名来自台湾的学者。在发表论文的时候他在伊利诺伊大学香槟分校攻读电子工程和计算机博士学位师从马克·约翰森Mark Hasegawa-Johnson。论文是黄博森在微软实习时的工作总结。2015年黄博森博士毕业然后于2016年加入了微软研究院。到目前为止他发表了30多篇人工智能相关的论文论文引用次数已经超过1千多次。
其他作者均来自当时在微软研究院工作的学者。其中不乏著名学者比如何晓冬Xiaodong He、邓力Li Deng、亚历克斯·阿西罗Alex Acero和拉里·赫克Larry Heck等。下面聊聊比较少被提及的阿西罗和赫克。阿西罗曾长期在微软研究院担任语音相关研究组的经理职位2013年之后他到苹果公司担任Siri的资深总监。赫克曾经在雅虎担任搜索和广告业务副总裁然后到微软研究院担任语音组的首席科学家。文章发表之后赫克到了谷歌在一个人工智能组担任总监并于最近加入三星北美研究院担任资深副总裁。这些学者主要是为这个工作提供支持和指导工作。
这篇论文自2013年发表后已经有超过390多次的引用是深度学习在搜索领域应用中被引用次数最多的论文之一。
深度结构化语义模型详解
下面详细讲讲这篇论文的核心思想。要想理解这篇论文提出的思路,我们首先要简单回顾一下经典的搜索模型构建。
在经典的搜索模型里不管是TF-IDF、BM25、语言模型还是基于机器学习的排序算法模型整体来说一个共通的想法就是争取用某种表示Representation来表达查询关键字然后用相同的、或者类似的表示来表达文档再通过某种程度的匹配函数来计算查询关键字表示和文档表示之间的距离然后进行排序。
那么,从深度学习的角度来说,要想针对这个传统的模式进行革新,当然就可以从最主要的三个方面入手:查询关键字的表达、文档的表达和匹配函数。
这篇文章也正是沿着这个思路,提出了深度结构化语义模型。
首先深度结构化语义模型对查询关键字和文档进行了相似的处理。具体来说就是先把查询关键字或者文档转换为词向量Term Vector这个词向量可以是最简单的“词袋”的表达方式这也就是最基本的模型的输入。从词向量出发模型首先学习一个“词哈希”Word Hashing也就是把0或1的稀疏词向量转换成为一个稠密Dense的向量表达。这一步是把深度学习方法应用在自然语言处理中所通用的办法目的就是把稀疏的输入转换为稠密的输入降低输入的数据维度。
当查询关键字和文档都转换成稠密数组以后深度结构化语义模型利用了深度学习中的重要经验那就是通过“非线性转换”Non-Linear Projection来获取数据深层次的语义信息而不仅仅只是传统方法中字面上的匹配。这里查询关键字和文档都使用了简单的“前馈神经网络”Feedforward Neural Network的方法对输入向量进行了多层的非线性转换。非线性转换本身通过“双曲正切函数”tanh函数实现这应该算是最传统的深度学习模型的实现方法了。
经过多层转换之后查询关键字和文档都变成了新的某种表达之后如何来计算两者间的距离或者远近这篇文章采用了非常直接的形式那就是利用“余弦函数”Cosine来作为距离函数描述两个向量之间的距离。在传统信息检索的语境中也经常用余弦函数来计算向量的距离所以在这里应该说并没有太多创新的地方。
总体来说,深度学习在这里的主要应用,就是成为查询关键字和文档的表达的提取器。和传统方法中人工提取各种类型的文字特性相比,在深度结构化语义模型中,基于前馈神经网络的特征提取器自动提取了文字的深层语义信息。
提出了模型之后,我们来看这个模型是如何被训练出来的。作者们首先利用了用户的点击信息,也就是针对某一个查询关键字,有哪些文档被点击过,作为正例数据,其他文档作为负例数据,然后把整个建模问题看作一个多类分类问题。这样就可以利用标签信息对整个模型进行学习了。
整体来说这个深度学习模型是可以利用“端到端”End-to-End的方式进行训练的并且采用了随机梯度下降SGD这样的优化算法这里就不复述了。
深度结构化语义模型的实验效果
因为深度结构化语义模型仅仅使用了查询关键字和文档之间的文字信息因此提出的模型就无法和完整的、利用很多特性的机器学习排序算法进行比较只能和文字型的排序算法例如TF-IDF、BM25和语言模型进行比较这也是文章并没有采用一些更为通用的数据集的原因。最终文章在数据集上采用了Bing的搜索数据有1万6千多的查询关键字以及每个查询关键字所对应的15个文档每个文档又有4级相关标签这样可以用来计算诸如NDCG这样的指标。
在这篇文章里作者们比较了一系列的方法比如TF-IDF、BM25以及一些传统的降维方法如LSA和PLSA。简单来说深度结构化语义模型在最后的比较中取得了不错的结果NDCG在第10位的表现是接近0.5。不过TF-IDF的表现也有0.46而传统的PLSA和LSA也有0.45左右的表现。所以,可以说深度结构化语义模型的效果虽然很明显但并不是特别惊人。
小结
今天我为你讲了深度结构化语义模型的一些基本原理,这是利用深度学习技术对搜索算法进行改进的一个经典尝试。我们在上面的实验结果总结中已经说到,虽然文章仅仅谈到了文本信息的匹配,并没有作为完整的排序算法进行比较,但是也揭开了用深度模型来表征查询关键字和文档的研发序幕 。
一起来回顾下要点:第一,我们简要介绍了提出深度结构化语义模型的历史。第二,我们详细介绍了深度结构化语义模型的核心思路以及实验结果。
给你留一个思考题,除了文章中提到的余弦函数可以作为一个距离函数,还有没有其他的函数选择来表达两个向量之间的距离?

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
056 基于深度学习的搜索算法:卷积结构下的隐含语义模型
这个星期,也是我们整个搜索领域分享的最后一周内容,来看一些搜索算法的前沿思考,特别是深度学习对搜索领域的影响。周一我们分享了一篇较早利用深度学习技术来进行搜索建模的论文,论文提出如何使用前馈神经网络来对查询关键字和文档进行信息提取,从而能够学习更有意义的语义信息。
今天我们来看一篇文章《信息检索中结合卷积池化结构的隐含语义模型》A Latent Semantic Model with Convolutional-Pooling Structure for Information Retrieval可以说这是我们周一分享论文的一个后续工作。这篇论文发表在第23届世界信息和知识管理大会CIKM 2014上。
论文背景介绍
这篇论文的主要目的是探讨深度学习中的卷积神经网络能否应用在搜索中,并取得较好的效果。
下面我们先来了解一下这篇论文作者群的信息。
第一作者Yelong Shen是微软研究院的一名资深研究员。
第二作者是何晓冬Xiaodong He是微软研究院深度学习组的主任研究员兼经理发表过一百多篇学术论文在人工智能领域特别是近年来在深度学习领域有很突出的贡献。
第三作者高剑峰Jianfeng Gao是一名长期在微软研究院工作的研究员和经理。
第四作者邓力Li Deng是微软研究院的人工智能学者曾担任微软的首席人工智能科学家并且领导深度学习中心。2017年5月邓力离开微软加入Citadel美国著名的金融机构担任首席人工智能官的职位。
最后一位作者格雷古瓦·梅尼尔Grégoire Mesnil是来自蒙特利尔大学的一名博士学生。
这篇论文自2014年发表后已被引用180多次是探讨深度学习在搜索领域中应用的主要论文之一。
卷积结构下的隐含语义模型详解
我们周一介绍的深度结构化语义模型其主要思想是希望能够利用前馈神经网络来对查询关键字和文档进行信息提取。这个模型有一个很明显的问题那就是在第一步对查询关键字或文档进行特征提取时所形成的词向量Term Vector是忽略了文字原本的顺序信息的也就是依然是一个“词袋模型”Bag of Words假设这显然是丢失了很多信息的。
当然,我们今天要分享的卷积结构下的隐含语义模型,也并不是第一个想要解决这个问题的模型。在经典的信息检索领域的研究中,已经有不少这方面的尝试了。那么对于深度学习来说,又有什么优势呢?
近些年来深度学习模型兴起的一个重要动力就是在图像、音频、视频领域的技术突破。而这些突破离不开一个重要的基础模型,卷积神经网络的成熟。这个模型对有空间位置结构性的数据,比如图像中每一个像素,有较强的建模能力,成为了探索结构信息建模的一个利器。那么,能不能把在这些领域中已经成熟的经验借鉴到搜索领域呢?
如果把文本的词与词,句子与句子之间的关系看作是一种空间位置关系的话,那么从假设上来看,就很符合卷积神经网络模型的基本设置。接下来,我们就来看看这个模型具体是怎么应用到搜索中的。
首先模型对查询关键字或者文档的文字进行“移动窗口”式Sliding Window的扫描。这第一步就和之前的深度结构化语义模型有了本质区别。然后模型进一步把“移动窗口”下的词转换成为字母级别的表征向量Representation Vector。这个步骤之后模型采用了卷积层来提取空间位置的特征也是把数据的维度大幅度降低。卷积层之后就是基本的“池化层”Pooling Layer这里的模型采用了最大池化Max Pooling也就是从多个卷积层的结果中每一个层对应元素中的最大元素。在池化层之后就是进行一个全部展开的语义层。
更加直白地说,整个模型就是希望先从原始的文字信息中,利用保留顺序的一个移动窗口提取最基本的特征;然后利用卷积神经网络的标配,卷积层加池化层,来提取空间位置信息;最后利用一个全部的展开层来学习下一步的系数。卷积层主要抓住的是单词这个级别的特征;而池化层则是希望抓住句子这个层面的语义信息;最后利用句子这个层面的语义信息形成整个文字的内在语义表达。
这个模型是如何被训练出来的呢?事实上,可以说整个模型的训练过程和我们周一分享的深度结构化语义模型的训练过程一模一样。首先,同样是利用用户的点击信息,也就是针对某一个查询关键字,有哪些文档被点击过,作为正例数据,其他文档作为负例数据;然后把整个建模问题看做是一个多类分类问题;这样就可以利用标签信息对整个模型进行学习。
隐含语义模型的实验效果
和深度结构化语义模型一样隐含语义模型也仅仅使用了查询关键字和文档之间的文字信息所以也只能和文字型的排序算法进行比较。最终文章在数据集上采用了Bing的搜索数据有1万2千多的查询关键字以及每个查询关键字所对应的74个文档每个文档又有4级的相关标签用来计算NDCG这样的指标。数据虽然和之前一篇不完全一样但是在数量级上是差不多的。
在这篇文章里作者们也比较了一系列的方法比如TF-IDF、BM25以及传统的PLSA和LDA。简单来说隐含语义模型在最后的比较中取得了不错的结果NDCG在第10位的表现是接近0.45而之前提出的深度结构化语义模型达到了差不多0.44。虽然利用卷积的效果要好一些但是差距并不大。在这个数据集上传统方法要差很多比如BM25的表现仅有0.38左右而传统的PLSA和LDA也只有0.40左右的表现。应该说在这篇文章中展示出来的效果还是有比较大的差距的。
小结
今天我为你讲了卷积结构下的隐含语义模型的一些基本原理,这个模型是利用深度学习技术对搜索算法进行改进的另一个很有价值的尝试,揭开了用深度学习模型,特别是用在图像处理中非常成功的卷积神经网络技术来表征查询关键字和文档会达到的效果。
一起来回顾下要点:第一,我们简要介绍了隐含语义模型提出的历史。第二,我们详细介绍了隐含语义模型的核心思路以及实验结果。
给你留一个思考题,为什么顺序信息并没有像我们想象中的那样,给文档搜索提升带来很大的效果呢?有没有什么解释?

View File

@ -0,0 +1,63 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
057 基于深度学习的搜索算法:局部和分布表征下的搜索模型
周一我们分享了一篇较早利用深度学习技术来进行搜索建模的论文,利用前馈神经网络来对查询关键字和文档进行信息提取,从而学习到更有意义的语义信息。周三我们分享了另外一篇论文,可以说是周一分享文章的一个后续工作,探讨了如何利用卷积神经网络来对搜索表征进行进一步提升。这两篇论文呈现了一个统一的套路,那就是尝试把深度学习的一些经验直接应用到传统的搜索建模上。这些尝试,也都取得了一些初步成绩。
今天我们来聊一篇2017年刚刚发表的论文《网页搜索中利用文本的局部和分布表征学习匹配》Learning to Match Using Local and Distributed Representations of Text for Web Search这是近期将深度学习模型应用在搜索领域的一个创新。这篇论文发表在世界万维网大会WWW 2017上。
论文背景介绍
下面我们来了解一下这篇论文的作者群信息。
第一作者巴斯卡⋅米特拉Bhaskar Mitra是微软研究院在剑桥实验室的一名研究员。他已经发表了多篇利用深度学习技术解决搜索问题的论文。目前米特拉在伦敦大学学院攻读博士学位。
第二作者是费尔南多⋅迪亚兹Fernando Diaz在文章发表的时候是微软研究院的一名研究员目前则在Spotify工作。迪亚兹长期从事搜索以及信息检索的工作发表多篇论文文章总引用数超过三千次。加入微软之前他曾经在雅虎研究院从事过研究工作。
文章的第三作者尼克⋅克拉维尔Nick Craswell在微软研究院工作目前是主任级研发经理长期从事搜索和信息检索的研究发表多篇论文文章总引用数达8千多次。
局部和分布表征下的搜索模型详解
我们详细讲讲这篇论文的核心思想。要想理解这篇论文提出的思路,我们首先要简单回顾一下这周讲的前两篇文章内容。
本周第一篇介绍的深度结构化语义模型主要是希望利用前馈神经网络来对查询关键字和文档进行信息提取。第二篇文章尝试用卷积神经网络来提取查询关键字和文档的信息。
不论是前馈网络,还是卷积网络, 这些尝试都是想从文本中提取高层次的语义信息。那么今天这篇文章说得是,并不是所有的相关信息都是高层次的语义信息。这是什么意思呢?
作者们提出了这样一个观点,那就是在搜索的时候,一个非常关键的需求就是被搜索到的文档应该包含查询关键字;或者反过来说,拥有查询关键字的文档有很大可能是相关的。也就是说,如果一个模型不能去进行绝对的关键字匹配,那很有可能就无法真正抓住所有的相关信息。
另一方面相关信息的提取也需要高层次的语义比如同义词或者同一个主题。设想我们需要查找汽车相关的信息而一个最新品牌的汽车页面也许并不直接包含“汽车”关键字但很明显是一个相关的页面。因此利用同义词或者整个主题的相关性通常可以提高搜索效果特别是“召回”Recall的效果。
那么,很显然,一个好的搜索模型应该兼顾这两个方面,也就是说既能够做到关键字的直接匹配,也能做到在高层次的语义上进行模糊匹配。
之前讲到的比如利用前馈网络或者卷积网络主要是针对后者,也就是模糊匹配,文章中提到叫做“分布表征”的匹配。那么,这篇文章的新意就是提出一种捕捉直接匹配的方式,文章叫做“局部表征”,并且和模糊匹配的分布表征结合在一起,形成一个统一的模型,从而提高搜索的效果。
具体来说,文章提出的模型是这样的。首先,从整体的网络框架来说,整个网络分成两个部分:一部分来学习查询关键字和文档的局部表征,也就是完全匹配;另一部分来学习查询关键字和文档的分布表征,也就是模糊匹配。最后,两个部分分别学习出一个向量,然后两个向量加和就形成了最后的表征。
完全匹配的局部表征技巧来自于数据的输入。和之前介绍的模型不同因为我们需要学习查询关键字和文档之间的匹配信息因此网络的输入信息就不单单是查询关键字和文档本身而是两者的一个“点积”Dot-Product也就是说网络的输入信息就是两者是否有匹配。把这个信息作为输入向量之后这篇文章采用了我们分享过的卷积神经网络的结构来进一步提取点积过后的输入向量。
在模糊匹配的分布表征部分,整体的框架和上次分享的模型很类似,也就是对查询关键字和文档分别进行建模,分别利用卷积神经网络提取高层次的语义信息。然后在高层次的语义信息上再进行查询关键字和文档表征的乘积(这里是矩阵相对应元素相乘)。最后,在经过基层的隐含转换(其实就是前馈网络),形成分布表征的最后唯一结果。
从整个模型来看,局部表征和分布表征的主要区别在于如何处理查询关键字和文档的匹配信息。如果是在原始数据上直接匹配,然后学习匹配后的高层语义,这就是局部表征。如果是先学习高层语义然后再匹配,这就是分布表征。
整个模型利用相关标签进行的是监督学习流程并且采用了SGD来优化。
局部和分布表征的搜索模型实验效果
这篇论文提出的模型还是仅仅使用了查询关键字和文档之间的文字信息因此和上两篇分享一样提出的模型就只能和文字型的排序算法例如TF-IDF、BM25和语言模型进行比较。文章在数据集上采用了Bing的搜索数据有19万多的查询关键字总共有将近百万的文档数。这比之前两个分享里的数据都要大。不过遗憾的是这三篇文章都是不同的数据集 。每个文档又有4级的相关标签可以用来计算诸如NDCG这样的指标。
在这篇文章里作者们比较了一系列的方法比如TF-IDF、BM25以及一些传统的降维方法比如LSA然后还比较了之前两个分享中提到的模型。简单来说本文模型在最后的比较中取得了非常不错的成绩NDCG在第10位的表现接近0.53而之前提出的一系列深度搜索模型包括我们分享的两个模型达到了差不多0.45~0.48左右。看来既需要完全匹配还需要模糊匹配的确能够带来性能上的提升。在这个数据集上传统方法其实也不差比如BM25的表现有0.45左右而传统的LSA也有0.44左右的表现。
小结
今天我为你分享了搜索专题的最后一篇内容,那就是利用深度学习技术对搜索算法进行改进的又一个尝试:一个结合了学习完全匹配的局部表征和模糊匹配的分布表征的统一的搜索模型。
一起来回顾下要点:第一,我们简要介绍了局部和分布表征搜索模型提出的历史。第二,我们详细介绍了局部和分布表征搜索模型的核心思路以及实验结果。
给你留一个思考题,我们这周分享了三个经典的深度学习和搜索相结合的尝试,你觉得目前深度学习在搜索领域取得的成果,有让你感到特别惊讶的结果吗?

View File

@ -0,0 +1,101 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
057 复盘 1 搜索核心技术模块
到目前为止,我们讲完了人工智能核心技术的第一个模块——*搜索*。我们从搜索的核心算法入手,进而讨论了搜索的两个关键组件,分别是查询关键字理解和文档理解,并落实到对搜索系统的评价,然后从宏观视角介绍了搜索框架的历史和发展,最后又从深度学习技术在搜索领域的应用角度,对分享做了一个延伸。
整个模块共27期9大主题希望通过这些内容能让你对搜索技术有一个系统的认识和理解为自己进一步学习和提升打下基础。今天我们就来对这一模块的内容做一个复盘。
提示点击知识卡跳转到你最想看的那篇文章温故而知新。如不能正常跳转请先将App更新到最新版本。
1.现代搜索架构剖析
从20世纪50年代有信息检索系统开始搜索系统大致经历了三个发展阶段。从最开始的“基于文本匹配的信息检索系统”到“基于机器学习的信息检索系统”再到近几年受深度学习影响的“更加智能的搜索系统”。
2.经典搜索核心算法
3.基于机器学习的排序算法
问题设置:把一个排序问题转换成一个机器学习的问题设置,特别是监督学习的设置。
4.基于机器学习的高级排序算法
5.查询关键字理解
6.文档理解
7.经典图算法
8.基于深度学习的搜索算法
9.搜索系统的评价
If You Cant Measure It, You Cant Improve It.
积跬步以至千里
最后,*恭喜你在这个模块中已经阅读了70047字听了220分钟的音频这是一个不小的成就*。在人工智能领域的千里之行,我们已经迈出了扎实的第一步。
感谢你在专栏里的每一个留言,给了我很多思考和启发。期待能够听到你更多的声音,我们一起交流讨论。

View File

@ -0,0 +1,77 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
058 简单推荐模型之一:基于流行度的推荐模型
今天,我们正式进入专栏的另一个比较大的模块,那就是推荐系统。之前我们详细且全面地介绍了搜索系统的各个组成部分。在接下来的几周时间里,我们一起来看推荐系统的技术要点又有哪些。
我们还是从简单推荐系统聊起,由易到难,逐步为你讲述一些经典的推荐模型。
推荐系统目前已经深入到了互联网的各类产品中。不管是到电子商务网站购物,还是到新闻阅读网站获取信息,甚至是在出行的时候希望听到不同的音乐,不同种类的推荐系统都在我们的生活中发挥着举足轻重的作用。
那么,搭建一个最简单的推荐系统,应该如何入手呢?今天我们就来聊一个最基本的推荐模型:基于流行度的推荐模型。
最简单的流行度估计
什么是基于流行度Popularity-based通俗地说就是什么内容吸引用户就给用户推荐什么内容。
这里面其实有一个隐含的假设,那就是物品本身的质量好坏和流行度有一定的正比关系。什么意思呢?就是说好的东西,关注的人自然就多,自然就会有更多的谈论。当然,这是一个主观的假设,并不是所有质量高的物品都会有很高的流行度。然而,在不需要过多其他信息和假设的情况下,流行度可以算是衡量物品质量好坏的一个最简单的测度。
那么,如果我们能够在每一个时间点上准确地估计到一个物品的流行度,就只需要按照流行度的数值从高到低排序显示所有的物品就可以了。
然而,这里牵涉到一个问题,那就是如何判断一个物品在任何时间点上的流行度呢?有两个重要的因素影响着物品流行度的估计,那就是时间和位置。
我们先来说一下时间因素。很显然,用户访问每一个应用或者服务都有一定的规律,这种规律导致每一个应用的流量规律也不一样。比如,人们可能更倾向于在早上或者傍晚打开新闻网站,看一看一天都发生了什么事情。因此,任何文章投放到这两个时段自然就会有比较高的关注度。这并不代表这些文章就要好于其他的文章,可能仅仅是由于时间的关系。因此,我们在对流行度建模的时候就需要考虑时间的因素。
另外一个重要的因素是位置。这个“位置”并不是真正的地理位置而是在一个服务或者网站的什么位置显示你的物品。因为用户心理对于不同位置的感受在很多类型的服务中常常都有隐含的“位置偏差”Position Bias
这些偏差给我们估计某个物品的流行度带来了很大的困难。比如说,在绝大多数的搜索引擎服务中,排名第一的物品所受到的关注度很可能大大高于排名第二和之后的物品。因此,一个物品只要放到第一的位置,关注度自然就会升高。当然,这并不能完全代表这个物品本身的属性。
因此,我们在估计物品的流行度时就需要考虑上面所说的这两个重要因素。
要解决刚才说的两个问题我们就不能使用绝对数值来对流行度建模。比如我们使用在单位时间内点击的数目购买的数目或者点赞的数目都会受到刚才所说的两种偏差的影响。假设一篇文章在9点到10点这个时段被点击了100次在10点到11点这个时段被点击了50次这并不能代表这个文章在10点到11点这个时段就变得不受欢迎了很可能是这个时段的总的用户量比较多。
因此对于流行度的衡量我们往往使用的是一个“比值”Ratio或者是计算某种“可能性”Probability。也就是说我们计算在总的用户数是N的情况下点击了某个文章的人数。这个比值取决于不同的含义如果是点击往往叫作点击率如果是购买叫作购买率。为了方便讨论我们在下面的例子中都使用点击率。
然而,点击率本身虽然解决了一部分时间和位置偏差所带来的影响,但是点击率的估计所需要的数据依然会受到偏差的影响。因此,我们往往希望能够建立无偏差的数据。
关于如何能够无偏差地估计,这是一个研究课题,我们今天不详细展开。不过,有一种比较经济的方法可以收集没有偏差的数据,那就是把服务的流量分成两个部分。
一个部分利用现在已有的对物品流行度的估计来显示推荐结果。另外一个部分则随机显示物品。这种方法是一种特殊的EE算法Exploitation & Exploration叫“epsilon贪心”epsilon-Greedy
我们之后还会聊到这个话题。根据这样的方式搜集的数据可以认为是没有位置偏差的。我们从随机显示物品的这部分流量中去估计流行度,然后在另外一个部分的流量里去显示物品。
如果从数学上对点击率建模,其实可以把一个物品在显示之后是否被点击看成是一个“伯努利随机变量”,于是对点击率的估计,就变成了对一个伯努利分布参数估计的过程。
有一种参数估计的方法叫作“最大似然估计法”Maximum Likelihood Estimation。简而言之就是说希望找到参数的取值可以最大限度地解释当前的数据。我们利用最大似然法就可以求出在某一段时间内的点击率所代表的伯努利分布的参数估计。这个估计的数值就是某个物品当前的点击总数除以被显示的次数。通俗地讲如果我们显示某个物品10次被点击了5次那么在最大似然估计的情况下点击率的估计值就是0.5。
很显然这样的估计有一定的局限性。如果我们并没有显示当前的物品那么最大似然估计的分母就是0如果当前的物品没有被点击过那么分子就是0。在这两种情况下最大似然估计都无法真正体现出物品的流行度。
高级流行度估计
我们从统计学的角度来讲了讲,如何利用最大似然估计法来对一个伯努利分布所代表的点击率的参数进行估计。
这里面的第一个问题就是刚才我们提到的分子或者分母为0的情况。显然这种情况下并不能很好地反应这些物品的真实属性。
一种解决方案是对分子和分母设置“先验信息”。就是说虽然我们现在没有显示这个物品或者这个物品没有被点击但是我们“主观”地认为比如说在显示100次的情况下会有60次的点击。注意这些显示次数和点击次数都还没有发生。在这样的先验概率的影响下点击率的估计或者说得更加精确一些点击率的后验概率分布的均值就成为了实际的点击加上先验的点击除以实际的显示次数加上先验的显示次数。你可以看到在有先验分布的情况下这个比值永远不可能为0。当然这也就避免了我们之前所说的用最大似然估计所带来的问题。
利用先验信息来“平滑”Smooth概率的估计是贝叶斯统计Bayesian Statistics中经常使用的方法。如果用更加精准的数学语言来表述这个过程我们其实是为这个伯努利分布加上了一个Beta分布的先验概率并且推导出了后验概率也是一个Beta分布。这个Beta分布参数的均值就是我们刚才所说的均值。
在实际操作中,并不是所有的分布都能够找到这样方便的先验分布,使得后验概率有一个解析解的形式。我们在这里就不展开讨论了。
另外一个可以扩展的地方就是,到目前为止,我们对于流行度的估计都是针对某一个特定的时段。很明显,每个时段的估计和前面的时间是有一定关联的。这也就提醒我们是不是可以用之前的点击信息,来更加准确地估计现在这个时段的点击率。
答案是可以的。当然,这里会有不同的方法。
一种最简单的方法还是利用我们刚才所说的先验概率的思想。那就是当前T时刻的点击和显示的先验数值是T-1时刻的某种变换。什么意思呢比如早上9点到10点某个物品有40次点击100次显示。那么10点到11点我们在还没有显示的情况下就可以认为这个物品会有20次点击50次显示。注意我们把9点到10点的真实数据乘以0.5用于10点到11点的先验数据这种做法是一种主观的做法。而且是否乘以0.5还是其他数值需要取决于测试。但是这种思想有时候叫作“时间折扣”Temporal Discount是一种非常普遍的时序信息处理的手法。
小结
今天我为你讲了基于流行度的推荐系统的基本原理。一起来回顾下要点:第一,我们简要介绍了为什么需要基于流行度进行推荐;第二,我们详细介绍了如何对流行度进行估计以及从统计角度看其含义;第三,我们简要地提及了一些更加高级的流行度估计的方法。
最后,给你留一个思考题,我们介绍了如何使用先验信息来对参数进行平滑,如何能够更加准确地确定先验概率中的数字呢?具体到我们的例子就是,如何来设置先验的点击和显示次数呢?

Some files were not shown because too many files have changed in this diff Show More