first commit
This commit is contained in:
parent
914d92856f
commit
bbc9aed40c
19
crawl.php
19
crawl.php
@ -39,7 +39,9 @@ foreach ($lines as $line) {
|
|||||||
$folderName = str_replace(' ', '', $line);
|
$folderName = str_replace(' ', '', $line);
|
||||||
$folderName = "/Users/01397713/Documents/github/learn-tech".$folderName;
|
$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);
|
$response = file_get_contents($curlUrl);
|
||||||
mkdir($folderName, 0777, true);
|
mkdir($folderName, 0777, true);
|
||||||
preg_match_all('/<a class="menu-item" id="([^"]*)" href="([^"]*)">([^<]*)<\/a>/', $response, $matches);
|
preg_match_all('/<a class="menu-item" id="([^"]*)" href="([^"]*)">([^<]*)<\/a>/', $response, $matches);
|
||||||
@ -49,6 +51,15 @@ foreach ($lines as $line) {
|
|||||||
$urlList = $matches[2];
|
$urlList = $matches[2];
|
||||||
|
|
||||||
foreach($fileNameList as $key => $name) {
|
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];
|
$fileUlr = $url . $urlList[$key];
|
||||||
$fileContents = file_get_contents($fileUlr);
|
$fileContents = file_get_contents($fileUlr);
|
||||||
|
|
||||||
@ -61,12 +72,10 @@ foreach ($lines as $line) {
|
|||||||
libxml_clear_errors();
|
libxml_clear_errors();
|
||||||
$text = $doc->textContent;
|
$text = $doc->textContent;
|
||||||
|
|
||||||
$fileName = str_replace(' ', '', $name);
|
file_put_contents($fileName, $text);
|
||||||
file_put_contents($folderName . '/'. $fileName, $text);
|
|
||||||
|
|
||||||
echo $fileName;
|
|
||||||
echo PHP_EOL;
|
|
||||||
sleep(10);
|
sleep(10);
|
||||||
|
|
||||||
// preg_match_all('/<p>([^<]*)<\/p>/', $fileContents, $fileMatches);
|
// preg_match_all('/<p>([^<]*)<\/p>/', $fileContents, $fileMatches);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
test.php
Executable file
45
test.php
Executable 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;
|
||||||
|
}
|
@ -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等等。
|
||||||
|
|
||||||
|
这里主要还是为了说明任务分解,相信如果需求继续扩展,根据上面的讨论,你是有能力进行后续分解的。
|
||||||
|
|
||||||
|
来看一下分解好的任务清单,你也可以拿出来自己的任务清单对比一下,看看差别有多大。
|
||||||
|
|
||||||
|
-
|
||||||
|
首先要说明的是,任务分解没有一个绝对的标准答案,分解的结果根据个人技术能力的不同,差异也会很大。
|
||||||
|
|
||||||
|
检验每个任务项是否拆分到位,就是看你是否知道它应该怎么做了。不过,即便你技术能力已经很强了,我依然建议你把任务分解到很细,观其大略人人行,细致入微见本事。
|
||||||
|
|
||||||
|
也许你会问我,我在写代码的时候,也会这么一项一项地把所有任务都写下来吗?实话说,我不会。因为任务分解我在之前已经训练过无数次,已经习惯怎么一步一步地把事情做完。换句话说,任务清单虽然我没写下来,但已经在我脑子里了。
|
||||||
|
|
||||||
|
不过,我会把想到的,但容易忽略的细节写下来,因为任务清单的主要作用是备忘录。一般情况下,主流程我们不会遗漏,但各种细节常常会遗漏,所以,想到了还是要记下来。
|
||||||
|
|
||||||
|
另外,对比我们在分解过程中的顺序,你会看到这个完整任务清单的顺序是调整过的,你可以按照这个列表中的内容一项一项地做,调整最基本的标准是,按照这些任务的依赖关系以及前面提到的“完整地实现一个需求”的原则。
|
||||||
|
|
||||||
|
最后,我要特别强调一点,所有分解出来的任务,都是独立的。也就是说,每做完一个任务,代码都是可以提交的。只有这样,我们才可能做到真正意义上的小步提交。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:按照完整实现一个需求的顺序去安排分解出来的任务。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你的任务清单和我的任务清单有哪些差异呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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(专业的)。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:要想写好测试,就要写简单的测试。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,经过最近持续对测试的讲解,你对测试有了哪些与之前不同的理解呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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》。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:想要管理好需求,先把需求拆小。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你的团队在需求管理上还遇到过哪些问题呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
18 需求管理:太多人给你安排任务,怎么办?
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
上一讲我们讲了需求的分解,我以用户故事为例,给你讲了我们应该把大的需求拆分成小的需求,但是不是只要把需求拆开了就万事大吉了呢?显然不是。今天我们再来探讨另一个与需求强相关的话题:需求管理。
|
||||||
|
|
||||||
|
需求管理?许多程序员的第一直觉通常是,这要么是产品经理的事,要么是项目经理的事,跟我有什么关系?我知道很多人会这么想,可我想说的是,如果你不了解需求是怎么管理的,即便是进行了需求分解,最终的结果很有可能依然是你深陷泥潭苦苦挣扎而不自知。
|
||||||
|
|
||||||
|
为什么这么说呢?我给你讲一个发生在我身边的故事。
|
||||||
|
|
||||||
|
最无脑的需求管理法:老板说的
|
||||||
|
|
||||||
|
有一次,我们组织了一次各团队负责人的吐槽大会,让大家把遇到的问题在台面上“摆”一下。一个开发团队的负责人说:“我这边倒排期太严重了,每个产品经理到我这里都说上线日期已经定好了,我这边资源有限,实在是抗不住了。”
|
||||||
|
|
||||||
|
出于好奇,有人问:“这些任务都一样重要吗?”
|
||||||
|
|
||||||
|
这个负责人无奈地摇摇头,“他们都说自己的任务重要。”
|
||||||
|
|
||||||
|
“他们凭什么说自己的任务重要呢?”我也问了一个问题。
|
||||||
|
|
||||||
|
这个负责人说:“他们告诉我,是老板说的。”
|
||||||
|
|
||||||
|
这是不是一个很熟悉的场景?一堆任务压过来,只是因为这是老板的一句话。我们的老板都是这么不近人情吗?其实,大概率来看,并不是。
|
||||||
|
|
||||||
|
就凭一句“老板说的”,我们就可以判断出,产品经理缺乏对需求管理应有的理解。而研发团队也因为无脑地接受了需求,几乎将自己压垮。
|
||||||
|
|
||||||
|
这时候,CTO 发话了:“口头的东西不算数,如果他们说是老板说的,那就让老板发邮件确认。”
|
||||||
|
|
||||||
|
我很认可CTO的说法,但我并不放心那个开发团队的负责人,于是我问他:“你会让产品经理这么去做吗?”果然,他犹豫了。
|
||||||
|
|
||||||
|
“产品经理可能不会和老板这么说。那你去说好了。”我们又给他提了个建议。显然,他更犹豫了,毕竟要面对大老板。
|
||||||
|
|
||||||
|
针对这种情况,我们又给出了一个解决办法,“如果你担心产品经理不这么做,你可以直接发邮件给老板,同时抄送 CTO。”
|
||||||
|
|
||||||
|
“对,可以这么做”,CTO 把责任扛了过去。这个负责人心里一下子有底了。
|
||||||
|
|
||||||
|
是不是有种似曾相识的感觉?其实,这个故事只要再往下延伸一点,就到了我们程序员身边。
|
||||||
|
|
||||||
|
作为程序员,我们面临的场景往往是,一个需求不明就里地来了,你的周末假期全部泡汤,因为你的负责人会和你说,这是老板说的。
|
||||||
|
|
||||||
|
软件行业有个段子:做软件,最理想的交付日期是什么时候?答案是昨天,其次是尽快。所有提出业务需求的人都恨不得需求早就做好了。但事实总是那么不如人意,所以,他们只能寄希望于需求被尽快实现。
|
||||||
|
|
||||||
|
如果我们等着所有需求都开发好了再上线呢?这就是当年所谓瀑布模型做的事,放在二十年前,这种做法还有生存空间,但今天这种做法显然已经不合时宜了。
|
||||||
|
|
||||||
|
关于如何做软件,我们已经讨论了很多,关键点就在于这个世界有太多的不确定,我们只好把产品的“一部分”开发好,送上线。
|
||||||
|
|
||||||
|
这就引出了一个问题,到底是选择“哪部分”优先上线呢?我们必须在宏大的理想和骨感的现实中作出取舍。这也就牵扯出需求管理的本质,实际上是个优先级的问题。
|
||||||
|
|
||||||
|
需求的优先级
|
||||||
|
|
||||||
|
“来自老板”,这是判断优先级最简单的答案,也是推卸责任的一个答案。其潜台词是,压力大不怪我,要怪就怪老板去。“来自老板”不应该成为优先做事的指标。
|
||||||
|
|
||||||
|
首先,我们要明确一点,优先级这种事大家也是可以谈的,大多数能当老板的人都是可以讲道理的。但要和老板谈,我们得知道怎么讲道理。准备一些基础知识,才能与各级老板探讨怎么安排工作的优先级。
|
||||||
|
|
||||||
|
为什么要区分优先级?因为时间是有限的,有限的时间内你能完成工作的上限是一定的。
|
||||||
|
|
||||||
|
怎么充分利用好有限的时间,这其实是一个时间管理的问题。所以,我们完全可以借鉴时间管理领域的一些优秀实践,帮助我们更有效地明辨优先级。
|
||||||
|
|
||||||
|
谈到时间管理,一个有效的时间管理策略是艾森豪威尔矩阵(Eisenhower Matrix),这是由美国前总统艾森豪威尔开发出的一个工具。
|
||||||
|
|
||||||
|
这个工具到了史蒂芬·柯维(Stephen Richards Covey)手里得到了发扬光大,他那本著名的《高效能人士的七个习惯》书籍将其推广至世界各地。也许这个名字你不太熟悉,看一下下面这个图你就知道了。
|
||||||
|
|
||||||
|
-
|
||||||
|
它将事情按照重要和紧急两个维度进行划分,也就形成了四个部分:重要且紧急,重要不紧急,不重要且紧急,不重要不紧急。
|
||||||
|
|
||||||
|
用几个程序员生活中的例子帮你理解一下。让系统不能正常运行的线上故障,就属于重要且紧急事情,不赶紧解决,就影响公司的正常运营。团队对系统升级改造就属于重要不紧急:改造好,性能也好,可维护性也得到提升;不改造,一时半会也能用。一些临时任务都属于紧急不重要,而刷朋友圈则属于既不紧急也不重要。
|
||||||
|
|
||||||
|
按照时间管理的理念,重要且紧急的事情要立即做。重要但不紧急的事情应该是我们重点投入精力的地方。紧急但不重要的事情,可以委托别人做。不重要不紧急的事情,尽量少做。
|
||||||
|
|
||||||
|
这个矩阵带给我们思维上最大的改变是,让人意识到事情和事情不是等价的。如果不把精力放在重要的事情上,到最后可能都变成紧急的事情。
|
||||||
|
|
||||||
|
比如,我们放任系统不做升级改造,过多的技术债会让系统的问题越来越多,新需求实现的速度越来越慢,最后几个看起来不大的需求就足以让团队加班加点,天怒人怨。
|
||||||
|
|
||||||
|
把这个思路带回到我们现实的需求管理中,你会发现,其实团队面临的各种需求所采用的优先级排序方式,基本上都是按照紧急程度排列的,但它们是否真的重要呢?
|
||||||
|
|
||||||
|
如果你把这个问题抛给需求的提出者,我几乎可以肯定,他们给你的答案是,他们提出的需求就是重要的。一种可能是,他们也分不清重要和紧急的差别,正如有时候我们也糊涂一样。
|
||||||
|
|
||||||
|
对于这样的场景,我们要做的就是多问一些问题。我在“精益创业:产品经理不靠谱,你该怎么办?”文章中说过,默认所有需求都不做,直到弄清楚为什么要做这件事。
|
||||||
|
|
||||||
|
同样,需求也没那么重要,直到产品经理能说明白它为什么重要,尤其是为什么比其他需求重要。如果一个产品经理不能把几个需求排出优先级,你就可以把上面学到的内容给他讲一遍。
|
||||||
|
|
||||||
|
还有另一种可能,他给你的需求在他工作的上下文中,确实是最重要的内容了。但当有多个需求来源时,我们该如何确认哪个需求是最重要的呢?这时,才到了真正需要老板出场的时刻。
|
||||||
|
|
||||||
|
站在老板面前
|
||||||
|
|
||||||
|
在“解决了很多问题,为什么你依然在‘坑’里?”文章中,我曾经讲过,大家不要局限于程序员这个角色,不同角色真正的差异是工作上下文的不同。每个人都在自己的上下文里工作,上下文也就局限了很多人的视野。
|
||||||
|
|
||||||
|
试想,两个产品经理出现在你面前,一个告诉你,公司要拓展新方向,这个功能要做;另一个却说,公司要进一步盈利,那个功能必须做。
|
||||||
|
|
||||||
|
在你看来,他们两个说得都对,听上去都挺重要的。但骨感的现实是,你把两件事都接下来,等着你的是累死都完不成的任务。
|
||||||
|
|
||||||
|
这个时候,我们能做的是什么呢?跳出这个上下文,到更大的上下文中。你判断不了哪个需求更重要,就请更高一级的老板来判断。
|
||||||
|
|
||||||
|
有了基础知识的储备,我们终于可以站在了老板面前。你可以告诉老板:我资源有限,需要将这两个需求排个序,看哪个更重要。我的上下文有限,需要你帮我判断一下。
|
||||||
|
|
||||||
|
老板会和你说这两个需求的起源,扩展盈利的需求是竞争对手都已经有了,客户也问这边要,再不做会影响客户关系,尤其是新财年快到了,下个阶段的合同会受到影响。而另外的新业务是某天一个高端聚会上得到的新启发,想尝试一下,他也不确定这个想法能带来多少收益,就让产品部门试一下。
|
||||||
|
|
||||||
|
听了老板的信息,你顿时明白这两件事的重要性,你也知道该如何面对两个产品经理了。
|
||||||
|
|
||||||
|
老板比你们的上下文大,因为他有看待这个问题更多的维度。所以,在你们眼里无比纠结的事情,老板几句话就云开雾散了,在他眼里,那根本不叫事。
|
||||||
|
|
||||||
|
如果你看过刘慈欣的《三体》,就会知道,这其实是“降维攻击”。另一个你可能熟悉的说法叫大局观。我经常和人说,当员工想不明白的事,换成老板的视角就全明白了。
|
||||||
|
|
||||||
|
我鼓励每个程序员在更大的上下文中工作,也就是想让人获得更多的思考维度。而今天的内容主要告诉你,如果自己的上下文不足时,我们可以引入新的元素,比如征求老板意见,扩大自己的上下文。
|
||||||
|
|
||||||
|
再发散讲几句,为人做事同样要不断扩展自己的上下文,这也就是我们常说的涨见识。
|
||||||
|
|
||||||
|
很多所谓的人生难题不过是因为见识有限造成的。比如,如果你觉得公司内总有人跟你比技术,莫不如把眼光放得长远一些,把自己放在全行业的水平上去比较。因为你是为自己的职业生涯在工作,而不是一个公司。
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
需求分解之后,最重要的是,排列需求的优先级。优先级的排列方式有很多,我们可以借鉴时间管理的方法,把事情按照重要和紧急的维度进行划分,得到了四个象限。我们要尽可能把精力放在重要的事情上,而不是把紧急的事情当成优先级排序的方式。
|
||||||
|
|
||||||
|
需求分解成一个个小块,其实也分解了原本合一的上下文。如果想要有效地管理需求,尤其是确定事情的重要程度,一种方式是找回丢失的上下文。如果我们自己无法判断上下文,一种好的办法是,引入外部更大的上下文。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:尽量做最重要的事。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你的团队在日常的需求管理中,还遇到哪些问题呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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个正在进行的任务,那一定是有问题的。因为一个人多线程工作,效果不会好。用“精益”的术语来说,我们应该限制 WIP(Work-In-Progress);再有,如果待开发的卡最多,那就证明现在的瓶颈在于开发,而不是其它阶段。
|
||||||
|
|
||||||
|
运用看板的方式,还有一个有趣的细节:使用实体墙还是电子墙。实体墙不难理解,就是找一面墙把看板做出来。现在有很多公司专门在做协同办公软件,其中的项目管理部分用到的就是看板理念,这就是电子墙的由来。
|
||||||
|
|
||||||
|
关于这点,顺便说一下我的建议,如果你的团队是在一起工作的,请考虑使用实体墙,除非你的办公空间实在太小。因为它可以方便地调整,也可以当作站会的集合地点,还可以让别人看见你们的工作或是问题,这样做的最大优势在于增强了人与人的互动。
|
||||||
|
|
||||||
|
电子墙的优势在于,随处可访问、数据不会丢失、便于统计等等,但每次访问它,都需要专门打开电脑,还是比较麻烦的。一种将二者结合的办法是,使用一个大电视,专门用来展示电子墙。
|
||||||
|
|
||||||
|
总之,看板就是要让工作在大家面前展现出来。
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
我给你介绍了一种结构化学习新知识的方式:技术雷达。
|
||||||
|
|
||||||
|
技术雷达就是一种将技术信息组织起来的方式。它通过将技术按照“象限”和“圆环”两个维度进行分类,让人可以直观地看到并理解不同的技术所处的发展阶段。
|
||||||
|
|
||||||
|
雷达图是一种很好的形式,不仅可以用在组织技术,还可以用来组织其它信息,比如,读书雷达。每个公司都可以利用雷达图的形式组织自己所有的技术,每个团队也可以利用雷达图的形式组织自己团队用到的技术,这样,方便团队成员结构化地理解用到技术,也方便新人的学习。
|
||||||
|
|
||||||
|
雷达图实际上是一种可视化的方法,人脑对于图像处理速度更快,因此,可视化是改善沟通的一种方式。大多数软件过程习惯采用文字的方式进行表达,对于“可视化”利用的还不够。当然,还是有一些利用“可视化”的方法,比如,流程图、UML 等。
|
||||||
|
|
||||||
|
最后,我给你介绍了一个利用可视化进行信息沟通的实践:看板。看板把工作分成了几个不同的阶段,在看板上对应不同的列,然后,每个任务作为一张卡贴在上面。每完成一张卡,就把这张卡挪到下一个阶段。
|
||||||
|
|
||||||
|
看板可以帮你发现许多问题,比如,当前进展是否合适,是否有人同时在做很多的事,发现当前工作的瓶颈等等。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:多尝试用可视化的方式进行沟通。
|
||||||
|
|
||||||
|
最后,我想请你思考一下,你在工作中,有哪些用到可视化方法解决沟通问题的场景?欢迎留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 服务器一旦检查出错,要立即修复。
|
||||||
|
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:做好持续集成的关键在于,快速反馈。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你的团队做持续集成吗?遇到过哪些困难呢?欢迎留言与我们分享。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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个为什么”就是一个常用的找到根因的方式。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:定期复盘,找准问题根因,不断改善。
|
||||||
|
|
||||||
|
最后我想请你分享一下,你的团队是怎么解决这些反复出现的问题呢?欢迎在留言区写下你的做法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
28 结构化:写文档也是一种学习方式
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
你写文档吗?我知道,你可能并不喜欢写文档,因为在你眼中,写文档是繁琐的,是旧时代软件工程的产物。
|
||||||
|
|
||||||
|
最开始我对写文档的印象也不好。
|
||||||
|
|
||||||
|
我的职业生涯是从一个通过了 CMM 5级认证的大企业开始的。可能今天很多程序员已经对 CMM 感到陌生了,它是能力成熟度模型(Capability Maturity Model for Software)的缩写,用来评估一个组织的软件开发能力,曾在国内风靡一时,许多软件公司都以拥有 CMM 认证为努力方向。
|
||||||
|
|
||||||
|
在这个极其重视过程的企业里,文档是非常重要的一环。但我看到的真实场景却是,一个软件已经上线运行了,大家才开始为了应付过程纷纷补写文档。
|
||||||
|
|
||||||
|
每个部门都有专门的过程负责人,要求你严格按照格式写文档,保证字体字号的正确性。然后,用 A4纸将文档打印出,封印在一个仓库里,再也无人问津。
|
||||||
|
|
||||||
|
然而,文档却是非常重要的。后来,我到过很多公司,凡是我能够比较快上手的,通常都是有比较详尽的文档,而那些文档缺失的公司,想要把信息梳理清楚,往往会花很长时间。
|
||||||
|
|
||||||
|
另外,我学习很多软件开发的相关知识,通常也是依赖各种各样的文档。对我们程序员这个走在时代前列的群体来说,大量阅读文档就是我们日常工作的一部分。
|
||||||
|
|
||||||
|
你发现矛盾了吗?一方面,我们讨厌写文档,另一方面,文档却对我们的工作学习有着不可忽视的作用。
|
||||||
|
|
||||||
|
我们竟然如此依赖于一个我们讨厌的东西。问题出在哪呢?
|
||||||
|
|
||||||
|
你为什么不喜欢写文档?
|
||||||
|
|
||||||
|
很多人会说,自己不愿意写那些无聊的流程文档,文档无聊,这固然是一个原因。不过,如今很多公司已经在这方面做得相当轻量级了,基本上只要求写必要的文档。那为什么依然有很多人不愿意写文档呢?
|
||||||
|
|
||||||
|
其实,很多人回避写文档的真正原因是,他掌握的内容不能很好地结构化。
|
||||||
|
|
||||||
|
在两种场景下,我们扮演的角色是不同的。写文档时,角色是作者;而读文档时,角色是读者。
|
||||||
|
|
||||||
|
作为读者,我们读文档,实际上就是按照作者梳理的结构在走,因为呈现出来的内容,多数是已经结构化的,读起来自然会比较顺畅;而作为作者,没有人告诉你结构应该是什么样,我们必须创造出一个结构来,而这正是很多人不擅长的。
|
||||||
|
|
||||||
|
想要成为一个好程序员,有一个良好的知识结构是极其重要的。
|
||||||
|
|
||||||
|
很多人抱怨程序员行业难,原因就在于,新技术层出不穷。是的,当你的知识都是零散的,任何新技术的出现,都是新东西。而当你建立起自己的知识结构,任何新东西都只是在原有知识上的增量叠加。
|
||||||
|
|
||||||
|
举个例子,今天炒得沸沸扬扬的微服务,小粒度的理念脱胎于 Unix 哲学中的“只做一件事,把它做好”,而服务化的理念则是当年SOA(Service-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》杂志曾经有一个关于“最恐惧事物”的调查,结果显示,公开演讲名列第一,超过了死亡。所以,你害怕公开演讲是很正常的。
|
||||||
|
|
||||||
|
我至今依然记得我第一次公开演讲时手抖的样子,今天想想还是挺傻的。我第一次在几百人的大会上做演讲,居然有一段时间,只顾着看大屏,背对着听众,也是很糗的一段经历。
|
||||||
|
|
||||||
|
我一直很羡慕那些在台上侃侃而谈的人,比如,乔布斯。直到我读了《乔布斯的魔力演讲》,我才知道,即便强如乔布斯,他的演讲也是经过大量练习的。
|
||||||
|
|
||||||
|
我自己公开演讲看上去正常一些,是我在经过一个咨询项目的大量练习之后。那时候,几乎每天要给客户讲东西,害得我只能不停地准备、不停地讲。所以,本质上,对演讲的惧怕只是因为练习不足。
|
||||||
|
|
||||||
|
好了,你现在已经了解获取这些技能的真谛了,无他,唯手熟尔!
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
程序员对文档有着一种矛盾的情感,一方面,需要依赖于文档获得知识,另一方面,很少有人愿意写文档。
|
||||||
|
|
||||||
|
文档在程序员心目中“形象不佳”,主要是传统的流程写了太多无用的文档。但对更多人来说,不愿意写文档,本质上是因为知识不能很好地结构化。
|
||||||
|
|
||||||
|
有结构的知识会让新知识的学习变得更加容易,今天很多人抱怨新知识层出不穷,就是因为知识过于零散,当知识有结构之后,学习新知识就只是在学习增量,效率自然就会大幅度提升。
|
||||||
|
|
||||||
|
输出是一种很好的方式,帮助你把知识连接起来,写作和做公开演讲都是很好的输出方式。
|
||||||
|
|
||||||
|
阻碍很多人进行知识输出的一个重要原因是缺乏输出的模型,金字塔原理就给出一个从中心论点到分论点,再到论据的模型,帮助我们将知识梳理出来。
|
||||||
|
|
||||||
|
而想要做好知识输出,还需要不断地进行练习,写作和做公开演讲都是可以通过练习提高的。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:多输出,让知识更有结构。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你的工作中,有哪些机会将自己的知识输出呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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。在那个年代,我们的程序是按照官方推荐做法编写的 EJB(Enterprise JavaBean),今天很多年轻的程序员可能不了解了,它只有部署到应用服务器才能运行。
|
||||||
|
|
||||||
|
我的解决方案就是加上一些打印语句,然后部署到应用服务器上,看输出的结果,再加上另外一些语句,再部署,如此往复。那时我们完全是手工打包上传,每次至少要十几分钟。最终,定位到了问题,只修改了一行代码。但几个小时的时间就这样被无谓的消耗了。
|
||||||
|
|
||||||
|
那之后,我花了很长时间研究怎么做自动化的增量部署,最终让这个过程简化了下来。但这件事对我的影响很大,这是我第一次认识到一个部署过程可能对开发造成的影响,也让我对自动化在开发过程内的应用有了属于自己的认识。
|
||||||
|
|
||||||
|
相比于我刚开始工作那会。现在在工具层面做类似的事已经容易很多了,在后面的内容中,我会结合着具体的场景介绍一下现在的最佳实践。
|
||||||
|
|
||||||
|
你要懂得软件设计
|
||||||
|
|
||||||
|
最后,我们再来说说我们的本职工作,给别人打造自动化工具中需要的能力:软件设计。
|
||||||
|
|
||||||
|
软件设计,是很多人既熟悉又陌生的一个词,说熟悉,很多人都知道,做软件要设计,还能顺嘴说出几个设计模式的名字;说陌生,是因为在我的职业生涯中,遇到真正懂软件设计的程序员少之又少。大多数人都是混淆了设计和实现。
|
||||||
|
|
||||||
|
举个例子。有一次,我要在两个系统之间做一个连接器,让上游系统向下游系统发消息,或许你一听就知道了,这里需要的是一个消息队列。但实际上,我们需要的能力要比消息队列更丰富一些,比如,要将重复的消息去除。一个同事给我推荐了 Kafka 当作这个连接器的基础,我欣然地接受了。
|
||||||
|
|
||||||
|
不过,在后续设计的讨论中,我们就经常出现话语体系的分歧。我说,这个连接器要有怎样的能力,他会说 Kafka 能够如何如何。究其根因,我在讨论的是设计,而他说的是实现,所以,我们两个很难把问题讨论到一起。
|
||||||
|
|
||||||
|
为什么我会如此看重设计呢?在软件开发中,其它的东西都是易变的,唯有设计的可变性是你可以控制的。
|
||||||
|
|
||||||
|
同样以前面的讨论为例,尽管 Kafka 在当下比较火热,但是我不敢保证 Kafka 在未来不会被我换掉。因为就在几年前,消息队列还是传统中间件的强项,现在也渐渐被人淡忘了。
|
||||||
|
|
||||||
|
我不想让我的设计随着某一个技术选型而不断摇摆。如果工作许多年,知识体系只能靠各种新框架新工具支撑,我们做程序员就只剩下疲于奔命了。不懂软件设计,只专注各种工具,其结果一定是被新技术遗弃,这也是很多人经常抱怨 IT 行业变化快的重要原因。
|
||||||
|
|
||||||
|
回到 Larry Wall 的说法上,你要想写出一个别人挑不出毛病的程序,你先要懂得软件设计。幸运的是,软件设计这些年的变化真不大,掌握了软件设计再来看很多框架和工具,学习起来就会容易很多。在这个模块的后半部分,我会与你探讨软件设计的话题,降低自己给自己挖坑的概率。
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
Perl 语言的发明人 Larry Wall 曾经说过,优秀程序员应该有三大美德:懒惰、急躁和傲慢(Laziness, Impatience and hubris)。想要成为一个优秀的程序员,就要让机器为自己很好地工作,而这需要对自动化有着很好地理解。
|
||||||
|
|
||||||
|
我们学习自动化,先要知道哪些东西不要自动化,尽最大的努力不做浪费时间的事。一方面,我们要从需求上规避那些没必要做的事;另一方面,我们也从自身防止 NIH 综合症(Not Invented Here Syndrome),争取做一个懒惰的程序员。
|
||||||
|
|
||||||
|
对于要自动化的事,我们需要反思一下,在为别人打造自动化工具的同时,我们自己的工作过程有没有很好地自动化。而如果我们想拥有打造良好的自动化工具,我们需要对软件设计有着充分地理解。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:请谨慎地将工作自动化。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,学习了本讲之后,你现在是怎样理解自动化的呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,259 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
30 一个好的项目自动化应该是什么样子的?
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
进入自动化这个模块,我准备从程序员的日常工作开始。介绍“迭代0”时,我提到构建脚本是项目准备的一个重要组成部分,但在那一讲中,我并没有具体说构建脚本长成什么样。
|
||||||
|
|
||||||
|
今天,我们以一个典型的 Java REST 服务为例,介绍一下最基本的构建脚本应该做到什么样子。这里我采用的 Java 技术中最为常见的 Spring Boot 作为基础框架,而构建工具,我选择了 Gradle。
|
||||||
|
|
||||||
|
估计很多 Java 程序员心中的第一个问题就是,为什么用 Gradle,而不是 Maven?Maven 明明是 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 上发表了一篇《软件开发地基》,讨论的就是一个项目的构建脚本应该是什么样子。虽然其中用到的工具今天已经不再流行,但一些基础内容今天看来,依然是有效的。如果有兴趣,你也可以看一下。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:将你的工作过程自动化。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,在日常开发工作中,你还把哪些过程自动化了呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 的知识体系,包括了编程语言、核心库、第三方库、开发框架、单机部署和集群部署等诸多方面。我把今天提到的各种技术整理成一个表格列在下面,你可以参考它更好地理解运维知识。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:有体系地学习运维知识。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你还能想到哪些运维知识可以放到这张知识地图上呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
34 你的代码是怎么变混乱的?
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
前面几讲,我给你讲了开发过程的各种自动化,从构建、验证到上线部署,这些内容都是站在软件外部看的。从这一讲开始,我准备带领大家进入到软件内部。今天的话题就从写代码开始说起。
|
||||||
|
|
||||||
|
逐步腐化的代码
|
||||||
|
|
||||||
|
代码是程序员改造世界最直接的武器,却也是程序员抱怨最多的东西。为什么程序员会对代码如此不满呢?
|
||||||
|
|
||||||
|
你会抱怨写一段代码吗?你肯定不会,毕竟这是你养家糊口的本领,最基本的职业素养我们还是有的。那抱怨的是什么呢?是维护一段代码。
|
||||||
|
|
||||||
|
为什么维护代码那么难?因为通常来说,你维护的这段代码是有一定年龄的,所以,你总会抱怨前人没有好好写这段代码。
|
||||||
|
|
||||||
|
好,现在你拿到了一个新的需求,要在这段代码上添加一个新功能,你会怎么做呢?很多人的做法是,在原有的代码上添加一段新的逻辑,然后提交完工。
|
||||||
|
|
||||||
|
发现问题了吗?你只是低着头完成了一项任务,而代码却变得更糟糕了。如果我问你,你为什么这么做?你的答案可能是:“这段代码都这样了,我不敢乱改。”或者是:“之前就是这么写的,我只是遵循别人的风格在写。”
|
||||||
|
|
||||||
|
行业里有一个段子,对程序员最好的惩罚是让他维护自己三个月前写的代码。你一不小心就成了自己最讨厌的人。
|
||||||
|
|
||||||
|
从前,我也认为很多程序员是不负责任,一开始就没有把代码写好,后来,我才知道很多代码其实只是每次加一点。你要知道,一个产品一旦有了生命力,它就会长期存在下去,代码也就随着时间逐渐腐烂了。
|
||||||
|
|
||||||
|
而几乎每个程序员的理由都是一样的,他们也很委屈,因为他们只改了一点点。
|
||||||
|
|
||||||
|
这样的问题有解吗?一个解决方案自然就是我们前面说过的重构,但重构的前提是,你得知道代码驶向何方。对于这个问题,更好的答案是,你需要了解一些软件设计的知识。
|
||||||
|
|
||||||
|
SOLID 原则
|
||||||
|
|
||||||
|
提到软件设计,大部分程序员都知道一个说法“高内聚、低耦合”,但这个说法如同“期待世界和平”一样,虽然没错,但并不能很好地指导我们的具体工作。
|
||||||
|
|
||||||
|
人们尝试着用各种方法拆解这个高远的目标,而比较能落地的一种做法就是 Robert Martin 提出的面向对象设计原则:SOLID,这其实是五个设计原则的缩写,分别是
|
||||||
|
|
||||||
|
|
||||||
|
单一职责原则(Single responsibility principle,SRP)
|
||||||
|
开放封闭原则(Open–closed principle,OCP)
|
||||||
|
Liskov 替换原则(Liskov substitution principle,LSP)
|
||||||
|
接口隔离原则(Interface segregation principle,ISP)
|
||||||
|
依赖倒置原则(Dependency inversion principle,DIP)
|
||||||
|
|
||||||
|
|
||||||
|
早在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 的书。不过,我也给你补充了一些维度,尤其是从“小”的角度告诉你,你能看到多小,就能发现代码里多少的问题。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:把函数写短。
|
||||||
|
|
||||||
|
最后我想请你思考一下,你是怎么理解软件设计的呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
35 总是在说MVC分层架构,但你真的理解分层吗?
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
作为程序员,你一定听说过分层,比如,最常见的 Java 服务端应用的三层结构,在《15 | 一起练习:手把手带你分解任务》中,我曾提到过:
|
||||||
|
|
||||||
|
|
||||||
|
数据访问层,按照传统的说法,叫 DAO(Data 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 逐渐退出了历史舞台,资源层取而代之。
|
||||||
|
|
||||||
|
换句话说,访问服务的方式可能会变。放到计算机编程的发展中,这种趋势就更明显了,从命令行到网络,从 CS(Client-Server) 到 BS(Browser-Server),从浏览器到移动端。所以,怎么访问不应该是你关注的核心。
|
||||||
|
|
||||||
|
同样, 关系型数据库也不是你关注的核心,它只是今天的主流而已。从前用文件,今天还有各种 NoSQL。
|
||||||
|
|
||||||
|
如此说来,三层架构中的两层重要性都不是那么高,那重要的是什么?答案便呼之欲出了,没错,就是剩下的部分,我们习惯上称之为服务层,但这个名字其实不能很好地反映它的作用,更恰当的说法应该可以叫领域模型(Domain Model)。
|
||||||
|
|
||||||
|
它便是我们的核心模型,也是我们在做软件设计时,真正应该着力的地方。
|
||||||
|
|
||||||
|
为什么叫“服务层”不是一个好的说法呢?这里会遗漏领域模型中一个重要的组成部分:领域对象。
|
||||||
|
|
||||||
|
很多人理解领域对象有一个严重的误区,认为领域对象属于数据层。数据存储只是领域对象的一种用途,它更重要的用途还是用在各种领域服务中。
|
||||||
|
|
||||||
|
由此还能引出另一个常见的设计错误,领域对象中只包含数据访问,也就是常说的 getter 和 setter,而没有任何逻辑。
|
||||||
|
|
||||||
|
如果只用于数据存储,只有数据访问就够了,但如果是领域对象,就应该有业务逻辑。比如,给一个用户修改密码,用户这个对象上应该有一个 changePassword 方法,而不是每次去 setPassword。
|
||||||
|
|
||||||
|
严格地说,领域对象和存储对象应该是两个类,只不过它俩实在太像了,很多人经常使用一个类,这还是个小问题。但很多人却把这种内部方案用到了外部,比如,第三方集成。
|
||||||
|
|
||||||
|
为数不少的团队都在自己的业务代码中直接使用了第三方代码中的对象,第三方的任何修改都会让你的代码跟着改,你的团队就只能疲于奔命。
|
||||||
|
|
||||||
|
解决这个问题最好的办法就是把它们分开,你的领域层只依赖于你的领域对象,第三方发过来的内容先做一次转换,转换成你的领域对象。这种做法称为防腐层。
|
||||||
|
|
||||||
|
当我们把领域模型看成了整个设计的核心,看待其他层的视角也会随之转变,它们只不过是适配到不同地方的一种方式而已,而这种理念的推广,就是一些人在说的六边形架构。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
怎么设计好领域模型是一个庞大的主题,推荐你去了解一下领域驱动设计(Domain Driven Design,DDD),这个话题我们后面还会再次提到。
|
||||||
|
|
||||||
|
讨论其实还可以继续延伸下去,已经构建好的领域模型怎么更好地提供给其他部分使用呢?一个好的做法是封装成领域特定语言(Domain Specific Language,DSL)。当然,这也是一个庞大的话题,就不继续展开了。
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
我从最常见的服务端三层架构入手,给你讲了它们的来龙去脉。分层架构实际是一种设计上的分解,将不同的内容放在不同的地方,降低软件开发和维护的成本。
|
||||||
|
|
||||||
|
分层,更关键的是,提供抽象。这种分层抽象在计算机领域无处不在,无论是编程语言,还是网络协议,都体现着分层抽象的价值。有了分层抽象,人们才能更好地在抽象的基础上构建更复杂的东西。
|
||||||
|
|
||||||
|
在日常工作中,我们应该把精力重点放在构建自己的领域模型上,因为它才是工作最核心、不易变的东西。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:构建好你的领域模型。
|
||||||
|
|
||||||
|
最后我想请你思考一下,你还知道哪些技术是体现分层抽象的思想吗?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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,用以处理分库的数据。
|
||||||
|
|
||||||
|
但是,这种做法也带来了一个新的问题,同时连接多个数据库,任何一个数据库出了问题,都会导致整个网站的故障。
|
||||||
|
|
||||||
|
当淘宝的数据量再次增长,每次访问都到了数据库,数据库很难承受。一个解决方案就是引入缓存和 CDN(Content Delivery Network,内容分发网络),这样,只读数据的压力就从数据库解放了出来。
|
||||||
|
|
||||||
|
当时的缓存系统还不像今天这么成熟,于是,团队基于一个开源项目改出了一个。他们用的 CDN 最开始是一个商用系统,但流量的增加导致这个系统也支撑不住了,只好开始搭建自己的 CDN。
|
||||||
|
|
||||||
|
后来,因为 CDN 要消耗大量的服务器资源,为了降低成本,淘宝又开始研发自己的低功耗服务器。
|
||||||
|
|
||||||
|
随着业务的不断发展,开发人员越来越多,系统就越来越臃肿,耦合度也逐渐提升,出错的概率也逐渐上升。这时,不得不对系统进行分解,将复用性高的模块拆分出来,比如,用户信息。
|
||||||
|
|
||||||
|
业务继续发展,拆分就从局部开始向更大规模发展,底层业务和上层流程逐渐剥离,并逐渐将所有业务都模块化。
|
||||||
|
|
||||||
|
有了一个相对清晰地业务划分之后,更多的底层业务就可以应用于不同的场景,一个基础设施就此成型,新的业务就可以使用基础设施进行构建,上层业务便如雨后春笋一般蓬勃发展起来。
|
||||||
|
|
||||||
|
在这个过程中,有很多技术问题在当时还没有好的解决方案,或者是不适合于它们所在的场景。所以,淘宝的工程师就不得不打造自己的解决方案,比如:分布式文件系统(TFS)、缓存系统(Tair)、分布式服务框架(HSF)等等。还有一些技术探索则是为了节省成本,比如,去 IOE 和研发低功耗服务器等等。
|
||||||
|
|
||||||
|
我这里以淘宝网站的发展为例,做了一个快速的梳理,只是为了让你了解一个系统的发展,如果你有兴趣了解更多细节,不妨自己找出这本书读读。当然,现在的淘宝肯定比这更加完整复杂。
|
||||||
|
|
||||||
|
同样的业务,不同的系统
|
||||||
|
|
||||||
|
为什么我们要了解一个系统的演化过程呢?因为作为程序员,我们需要知道自己面对的到底是一个什么样的系统。
|
||||||
|
|
||||||
|
回到我们今天的主题上,5万块钱可以不可以做一个淘宝?答案是,取决于你要的是一个什么样的系统。最开始买来的“淘宝”甚至连5万块钱都不用,而今天的淘宝和那时的淘宝显然不是一个系统。
|
||||||
|
|
||||||
|
从业务上说,今天的淘宝固然已经很丰富了,但最核心的业务相差并不大,无非是卖家提供商品,买家买商品。那它们的本质差别在哪呢?
|
||||||
|
|
||||||
|
回顾上面的过程,你就可以看到,每次随着业务量的增长,原有技术无法满足需要,于是,就需要用新的技术去解决这个问题。这里的关键点在于:不同的业务量。
|
||||||
|
|
||||||
|
一个只服务于几个人的系统,单机就够了,一个刚刚入行的程序员也能很好地实现这个系统。而当业务量到达一台机器抗不住的时候,就需要用多台机器去处理,这个时候就必须考虑分布式系统的问题,可能就要适当地引入中间件。
|
||||||
|
|
||||||
|
而当系统变成为海量业务提供服务,就没有哪个已经打造好的中间件可以提供帮助了,需要自己从更底层解决问题。
|
||||||
|
|
||||||
|
虽然在业务上看来,这些系统是一样的,但在技术上看来,在不同的阶段,一个系统面对的问题是不同的,因为它面对业务的量级是不同的。更准确地说,不同量级的系统根本就不是一个系统。
|
||||||
|
|
||||||
|
只要业务在不断地发展,问题就会不断出现,系统就需要不断地翻新。我曾听到一个很形象的比喻:把奥拓开成奥迪。
|
||||||
|
|
||||||
|
你用对技术了吗?
|
||||||
|
|
||||||
|
作为一个程序员,我们都知道技术的重要性,所以,我们都会努力地去学习各种各样的新技术。尤其是当一个技术带有大厂光环的时候,很多人都会迫不及待地去学习。
|
||||||
|
|
||||||
|
我参加过很多次技术大会,当大厂有人分享的时候,通常都是人山人海,大家都想学习大厂有什么“先进”技术。
|
||||||
|
|
||||||
|
知道了,然后呢?
|
||||||
|
|
||||||
|
很多人就想迫不及待地想把这些技术应用在自己的项目中。我曾经面试过很多程序员,给我讲起技术来滔滔不绝,说什么自己在设计时考虑各种分布式的场景,如果系统的压力上来时,他会如何处理。
|
||||||
|
|
||||||
|
我就好奇地问了一个问题,“你这个系统有多少人用?”结果,他做的只是一个内部系统,使用频率也不高。
|
||||||
|
|
||||||
|
为了技术而技术的程序员不在少数,过度使用技术造成的结果就是引入不必要的复杂度。即便用了牛刀杀鸡,因为缺乏真实场景检验,也不可能得到真实反馈,对技术理解的深度也只能停留在很表面的程度上。
|
||||||
|
|
||||||
|
在前面的例子中,淘宝的工程师之所以要改进系统,真实的驱动力不是技术,而是不断攀升的业务量带来的问题复杂度。
|
||||||
|
|
||||||
|
所以,评估系统当前所处的阶段,采用恰当的技术解决,是我们最应该考虑的问题。
|
||||||
|
|
||||||
|
也许你会说,我做的系统没有那么大的业务量,我还想提高技术怎么办?答案是到有好问题的地方去。现在的 IT 行业提供给程序员的机会很多,找到一个有好问题的地方并不是一件困难的事,当然,前提条件是,你自己得有解决问题的基础能力。
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
今天,我以淘宝的系统为例,给你介绍了一个系统逐渐由简单变复杂的发展历程,希望你能认清不同业务量级的系统本质上就不是一个系统。
|
||||||
|
|
||||||
|
一方面,有人会因为对业务量级理解不足,盲目低估其他人系统的复杂度;另一方面,也有人会盲目应用技术,给系统引入不必要的复杂度,让自己陷入泥潭。
|
||||||
|
|
||||||
|
作为拥有技术能力的程序员,我们都非常在意个人技术能力的提升,但却对在什么样情形下,什么样的技术更加适用考虑得不够。采用恰当的技术,解决当前的问题,是每个程序员都应该仔细考虑的问题。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:用简单技术解决问题,直到问题变复杂。
|
||||||
|
|
||||||
|
最后,我想请你回想一下,你身边有把技术做复杂而引起的问题吗?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
37 先做好DDD再谈微服务吧,那只是一种部署形式
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
在“自动化”模块的最后,我们来聊一个很多人热衷讨论却没做好的实践:微服务。
|
||||||
|
|
||||||
|
在今天做后端服务似乎有一种倾向,如果你不说自己做的是微服务,出门都不好意思和人打招呼。
|
||||||
|
|
||||||
|
一有技术大会,各个大厂也纷纷为微服务出来站台,不断和你强调自己公司做微服务带来的各种收益,下面的听众基本上也是热血沸腾,摩拳擦掌,准备用微服务拯救自己的业务。
|
||||||
|
|
||||||
|
我就亲眼见过这样的例子,几个参加技术大会的人回到公司,跟人不断地说微服务的好,说服了领导,在接下来大的项目改造中启用了微服务。
|
||||||
|
|
||||||
|
结果呢?一堆人干了几个月,各自独立开发的微服务无法集成。最后是领导站出来,又花了半个月时间,将这些“微服务”重新合到了一起,勉强将这个系统送上了线。
|
||||||
|
|
||||||
|
人家的微服务那么美,为什么到你这里却成了烂摊子呢?因为你只学到了微服务的形。
|
||||||
|
|
||||||
|
微服务
|
||||||
|
|
||||||
|
大部分人对微服务的了解源自 James Lewis 和 Martin Fowler 在2014年写的一篇文章,他们在其中给了微服务一个更清晰的定义,把它当作了一种新型的架构风格。
|
||||||
|
|
||||||
|
但实际上,早在这之前的几年,很多人就开始用“微服务”这个词进行讨论了。
|
||||||
|
|
||||||
|
“在企业内部将服务有组织地进行拆分”这个理念则脱胎于 SOA(Service Oriented Architecture,面向服务的架构),只不过,SOA 诞生自那个大企业操盘技术的年代,自身太过于复杂,没有真正流行开来。而微服务由于自身更加轻量级,符合程序员的胃口,才得以拥有更大的发展空间。
|
||||||
|
|
||||||
|
谈到微服务,你会想起什么呢?很多人对微服务的理解,就是把一个巨大的后台系统拆分成一个一个的小服务,再往下想就是一堆堆的工具了。
|
||||||
|
|
||||||
|
所以,市面上很多介绍微服务的内容,基本上都是在讲工具的用法,或是一些具体技术的讨论,比如,用 Spring Boot 可以快速搭建服务,用 Spring Cloud 建立分布式系统,用 Service Mesh 技术作为服务的基础设施,以及怎么在微服务架构下保证事务的一致性,等等。
|
||||||
|
|
||||||
|
确实,这些内容在你实现微服务时,都是有价值的。但必须先回答一个问题,我们为什么要做微服务?
|
||||||
|
|
||||||
|
对这个问题的标准回答是,相对于整体服务(Monolithic)而言,微服务足够小,代码更容易理解,测试更容易,部署也更简单。
|
||||||
|
|
||||||
|
这些道理都对,但这是做好了微服务的结果。怎么才能达到这个状态呢?这里面有一个关键因素,怎么划分微服务,也就是一个庞大的系统按照什么样的方式分解。
|
||||||
|
|
||||||
|
这是在很多关于微服务的讨论中所最为欠缺的,也是很多团队做“微服务”却死得很难看的根本原因。
|
||||||
|
|
||||||
|
不了解这一点,写出的服务,要么是服务之间互相调用,造成整个系统执行效率极低;要么是你需要花大力气解决各个服务之间的数据一致性。换句话说,服务划分不好,等待团队的就是无穷无尽的偶然复杂度泥潭。只有正确地划分了微服务,它才会是你心目中向往的样子。
|
||||||
|
|
||||||
|
那应该怎么划分微服务呢?你需要了解领域驱动设计。
|
||||||
|
|
||||||
|
领域驱动设计
|
||||||
|
|
||||||
|
领域驱动设计(Domain Driven Design,DDD)是 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 的理解是什么样的呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 ”型人才,也就是“一专多能”。一专多能的前提是“一专”,让自己成为某个方面的专家。这个专家要放在行业的标准去看,这才能降低因为一个公司的波动而造成的影响。
|
||||||
|
|
||||||
|
成为行业专家,要向行业的大师学习,给自己定下一个高的目标,然后是脚踏实地,找适合自己的问题去解决,让自己一直在学习区成长。
|
||||||
|
|
||||||
|
如果今天的内容你只能记住一件事,那请记住:在学习区工作和成长。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你有哪些保持自己竞争力的心得呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 同学的理解也很棒:
|
||||||
|
|
||||||
|
|
||||||
|
技术推广,不要先推广最难的部分,先推广能让对方感到最明显好处的部分。取得对方的信任,是友好沟通的基础。
|
||||||
|
|
||||||
|
|
||||||
|
感谢同学们的精彩留言。我们的专栏更新已经进入尾声阶段,后续我会为大家做一些对整个专栏进行全盘复习的内容,敬请期待。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
划重点 “自动化”主题的重点内容回顾汇总
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
“自动化”模块终于全部更新完毕。至此,四个工作原则我已经给你全部介绍了一遍,相对而言,这个模块的内容比较“硬”,我也竭尽全力帮你串起更多知识的脉络,所以,信息量也是非常大的。希望你能够找到自己接下来努力的方向,不断提升自己的“硬实力”。
|
||||||
|
|
||||||
|
重点复习
|
||||||
|
|
||||||
|
在这个模块中,我们学习到了一些最佳实践。
|
||||||
|
|
||||||
|
|
||||||
|
持续交付
|
||||||
|
|
||||||
|
|
||||||
|
将生产部署纳入了开发的考量。
|
||||||
|
持续交付的基础设施通常包含持续集成环境、测试环境、预生产环境和生产环境。
|
||||||
|
构建流水线保证到了下游的交付物一定是通过上游验证的。
|
||||||
|
随着 Docker 的诞生,交付由发布包变成了 Docker 镜像。
|
||||||
|
|
||||||
|
|
||||||
|
DevOps
|
||||||
|
|
||||||
|
|
||||||
|
将开发和运维结合到一起。
|
||||||
|
环境配置工具上的进步,让基础设施即代码成了行业共识。
|
||||||
|
|
||||||
|
|
||||||
|
验收测试
|
||||||
|
|
||||||
|
|
||||||
|
验收测试要站在业务的角度编写。
|
||||||
|
BDD 是一种编写验收测试的方式。
|
||||||
|
Given…When…Then… 的描述给了一个描述业务的统一方式。
|
||||||
|
写好验收测试,需要构建测试模型。
|
||||||
|
|
||||||
|
|
||||||
|
SOLID 原则
|
||||||
|
|
||||||
|
|
||||||
|
设计模式背后的道理。
|
||||||
|
单一职责原则(Single responsibility principle,SRP)。
|
||||||
|
开放封闭原则(Open–closed principle,OCP)。
|
||||||
|
Liskov 替换原则(Liskov substitution principle,LSP)。
|
||||||
|
接口隔离原则(Interface segregation principle,ISP)。
|
||||||
|
依赖倒置原则(Dependency inversion principle,DIP)。
|
||||||
|
用好单一职责原则,前提条件是看待问题颗粒度要小。
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
感谢同学们的精彩留言。在下一个模块中,我将结合具体的应用场景,将之前讲过的“思考框架”和“四个原则”进行综合应用的分析。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 同学提到:
|
||||||
|
|
||||||
|
|
||||||
|
我做了好多年的软件测试,前几年和销售一起去谈客户,才深深地体会到客户声音的重要性。客户关注的才是真需求,产品经理和开发想出来的很多是伪需求,很多不是客户想要的功能。
|
||||||
|
|
||||||
|
|
||||||
|
感谢同学们的精彩留言。在下一个模块中,我将为你分享“自动化”这个原则的具体应用。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 的书吧!
|
||||||
|
|
||||||
|
总结时刻
|
||||||
|
|
||||||
|
总结一下今天的内容。今天我介绍了一个大家耳熟能详的概念:重构。不过,这实在是一个让人误解太多的概念,大家经常认为调整代码就是在做重构。
|
||||||
|
|
||||||
|
重构,本质上就是一堆微操作。重构这个实践的核心,就是将调整代码的动作分解成一个一个的小动作,如果不能理解这一点,你就很难理解重构本身的价值。
|
||||||
|
|
||||||
|
不过,对于我们专栏的读者而言,因为大家已经学过了“任务分解”模块,理解起这个概念,难度应该降低了很多。
|
||||||
|
|
||||||
|
既然重构的核心也是分解,它就需要大量的锤炼。就像之前提到任务分解原则一样,我在重构上也下了很大的功夫做了专门的练习,才能让自己一小步一小步地去做。但一个有追求的软件工匠不就应该这样锤炼自己的基本功吗?
|
||||||
|
|
||||||
|
如果今天的内容你只记住一件事,那请记住:锤炼你的重构技能。
|
||||||
|
|
||||||
|
最后,我想请你分享一下,你对重构的理解。欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 | 尽早暴露问题: 为什么被指责的总是你? 》
|
||||||
|
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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年的书里就有大数据的身影。
|
||||||
|
|
||||||
|
对于程序员来说,最好的工作状态就是进入心流,它会让你忘我工作。如果你对心流的概念感兴趣,可以去读米哈里·契克森米哈赖的著作《心流》,这位作者就是心流概念的提出者。
|
||||||
|
|
||||||
|
|
||||||
|
好,今天的复习就到这里,你有哪些经典的书可以推荐给这个专栏的同学呢?欢迎在留言区写下分享你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
答疑解惑 持续集成、持续交付,然后呢?
|
||||||
|
你好,我是郑晔。
|
||||||
|
|
||||||
|
“自动化”模块落下了帷幕,这是四个工作原则中最为“技术化”的一个,也应该是程序员们最熟悉的主题。
|
||||||
|
|
||||||
|
我从软件外部的自动化——工作流程讲起,让你能够把注意力专注于写好代码;讲到了软件内部的自动化——软件设计,选择恰当的做法,不贪图一时痛快,为后续的工作挖下深坑。
|
||||||
|
|
||||||
|
既然是一个大家都熟悉的话题,同学们自然也有很多经验分享,也有很多人看到了与自己不同的做法,提出了各种各样的问题。
|
||||||
|
|
||||||
|
在今天的答疑中,我选出了几个很有意思的问题,让大家可以在已有内容上再进一步延伸。
|
||||||
|
|
||||||
|
问题1:持续交付是否可以再做扩展?
|
||||||
|
|
||||||
|
毅 同学提到
|
||||||
|
|
||||||
|
|
||||||
|
为达到有效交付的目标,用户能够尽早参与,我觉得也是比较重要的一环。从生产环境获得结果,是否可再做扩展,将用户也作为一个独立节点?-
|
||||||
|
——《32 | 持续交付:有持续集成就够了吗?》
|
||||||
|
|
||||||
|
|
||||||
|
西西弗与卡夫卡 同学提到
|
||||||
|
|
||||||
|
|
||||||
|
持续交付可以是持续交付最大价值,那范围就不仅限于软件,还可以进一步延伸到运营,比如说结合ABTest,自动选择最有效的运营策略,为用户交付最大价值。-
|
||||||
|
——《32 | 持续交付:有持续集成就够了吗?》
|
||||||
|
|
||||||
|
|
||||||
|
两位同学能提出这样的想法,说明真的是已经理解了持续集成和持续交付,所以,才能在这个基础上继续延伸,思考进一步的扩展。
|
||||||
|
|
||||||
|
我在专栏中一直在强调,别把自己局限在程序员这个单一的角色中,应该了解软件开发的全生命周期。在前面的内容中,我讲了不少做产品的方法,比如,MVP、用户测试等等。如果只把自己定位在一个写代码的角色上,了解这些内容确实意义不大,但你想把自己放在一个更大的上下文中,这些内容就是必须要了解的。
|
||||||
|
|
||||||
|
回到两位同学的问题上,如果说我们一开始把持续集成定义成编写代码这件事的完成,那持续交付就把这个“完成”向前再推进了一步,只有上线的代码才算完成。
|
||||||
|
|
||||||
|
但放在整个软件的生命周期来说,上线并不是终点。把系统送上线,不是最终目的。那最终目的是什么呢?
|
||||||
|
|
||||||
|
回到思考的起点,我们为什么要做一个软件?因为我们要解决一个问题。那我们是不是真正的解决了问题呢?其实,我们还不知道。
|
||||||
|
|
||||||
|
在《06 | 精益创业:产品经理不靠谱,你该怎么办?》这篇文章中,我给你讲了做产品的源头。如果是采用精益创业的模式工作,我们构建产品的目的是为了验证一个想法,而怎么才算是验证了我们的想法呢?需要搜集各种数据作为证据。
|
||||||
|
|
||||||
|
所以,我曾经有过这样的想法,精益创业实际上是一种持续验证,验证想法的有效性,获得经过验证的认知(Validated Learning)。
|
||||||
|
|
||||||
|
现在有一些获取验证数据的方式,比如,西西弗与卡夫卡 同学提到的 AB 测试。
|
||||||
|
|
||||||
|
AB 测试是一种针对两个(或多个)变体的随机试验,常常用在 Web 或 App 的界面制作过程中,分别制作两个(或多个)版本,让两组(或多组)成分相同的用户随机访问不同版本,收集数据,用以评估哪个版本更好。每次测试时,最好只有一个变量。因为如果有多个变量,你无法确认到底是哪个变量在起作用。
|
||||||
|
|
||||||
|
AB 测试的概念在其他领域由来已久。2000年,Google 的工程师率先把它应用在了软件产品的测试中,时至今日,它已经成为很多产品团队常用的做事方式。
|
||||||
|
|
||||||
|
AB 测试的前提是用户数据搜集。我在《09 | 你的工作可以用数字衡量吗?》这篇文章给你介绍了在开发过程中,用数字帮助我们改善工作。在产品领域实际上更需要用数字说话,说到这里,我“插播”一个例子。
|
||||||
|
|
||||||
|
很多产品经理喜欢讲理念、讲做法,偏偏不喜欢讲数字。用数字和产品经理沟通其实是更有说服力的。
|
||||||
|
|
||||||
|
我就曾经遇到过这样的事情,在一个交易平台产品中,一个产品经理创造性地想出一种新的订单类型,声称是为了方便用户,提高资金利用率。如果程序员接受这个想法,就意味着要对系统做很大的调整。
|
||||||
|
|
||||||
|
我问了他几个问题:第一,你有没有统计过系统中现有的订单类型的使用情况?第二,你有没有了解过其他平台是否支持这种订单类型呢?
|
||||||
|
|
||||||
|
产品经理一下子被我问住了。我对第一个问题的答案是,除了最基础的订单类型之外,其他的订单类型用得都很少,之前做的很多号称优化的订单类型,实际上没有几个人在用。
|
||||||
|
|
||||||
|
第二个问题我的答案是,只有极少数平台支持类似的概念。换句话说,虽然我们想得很美,但教育用户的成本会非常高,为了这个可能存在的优点,对系统做大改造,实在是一件投资大回报小的事,不值得!
|
||||||
|
|
||||||
|
再回到我们的问题上,一旦决定了要做某个产品功能,首先应该回答的是如何搜集用户数据。对于前端产品,今天已经有了大量的服务,只要在代码里嵌入一段代码,收集数据就是小事一桩。
|
||||||
|
|
||||||
|
前端产品还好,因为用户行为是比较一致的,买服务就好了,能生成标准的用户行为数据。对于后端的数据,虽然也有各种服务,但基本上提供的能力都是数据的采集和展示,一些所谓的标准能力只是 CPU、内存、JVM 之类基础设施的使用情况。对于应用来说,具体什么样的数据需要搜集,还需要团队自己进行设计。
|
||||||
|
|
||||||
|
说了这些,我其实想说的是,持续验证虽然是一个好的想法,但目前为止,还不如持续集成和持续交付这些已经有比较完整体系做支撑。想做到“持续”,就要做到自动化,想做到自动化,就要有标准化支撑,目前这个方面还是“八仙过海各显神通”的状态,没法上升到行业最佳实践的程度。
|
||||||
|
|
||||||
|
其实道理上也很简单,从一无所有,到持续集成、再到持续交付,最后到持续验证,每过一关,就会有大多数团队掉队。所以,真正能达到持续交付的团队都少之又少,更别提要持续验证了。
|
||||||
|
|
||||||
|
问题2:Selenium 和 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 应用测试方面实现步骤定义的一个非常好的工具。
|
||||||
|
|
||||||
|
问题3:IntelliJ IDEA 怎么学?
|
||||||
|
|
||||||
|
hua168 同学提到
|
||||||
|
|
||||||
|
|
||||||
|
IDEA 怎么学呢?是用到什么功能再学?还是先看个大概,用到时再仔细看?-
|
||||||
|
——《30 | 一个好的项目自动化应该是什么样子的?》
|
||||||
|
|
||||||
|
|
||||||
|
一个工具怎么学?我的经验就是去用。我没有专门学过 IntelliJ IDEA,只是不断地在使用它。遇到问题就去找相应的解决方案。
|
||||||
|
|
||||||
|
如果说在 IDEA 上下过功夫,应该是在快捷键上。我最早写代码时的风格应该是鼠标与键盘齐飞,实话说,起初也没觉得怎么样。加入 ThoughtWorks 之后,看到很多人把快捷键运用得出神入化,那不是在写一行代码,而是在写一片代码。我当时有一种特别震惊的感觉。
|
||||||
|
|
||||||
|
我自以为在写代码上做得已经相当好了,然而,有人却在你很擅长的一件事上完全碾压了你,那一瞬间,我感觉自己这些年都白学了。这种感觉后来在看到别人能够小步重构时又一次产生了。
|
||||||
|
|
||||||
|
看到差距之后,我唯一能做的,就是自己下来偷偷练习。幸好,无论是快捷键也好,重构也罢,都是可以单独练习的。花上一段时间就可以提高到一定的水平。后来,别人看我写代码时也会有类似的感觉,我会安慰他们说,不要紧,花点时间练习就好。
|
||||||
|
|
||||||
|
其实,也有一些辅助的方法可以帮助我们练习,比如,我们会给新员工发放 IntelliJ IDEA 的快捷键卡片,写代码休息之余,可以拿来看一下;再比如,IntelliJ IDEA 有一个插件叫 Key Promoter X,如果你用鼠标操作,它会给你提示,帮你记住快捷键。有一段时间,我已经练习到“看别人写代码,脑子里能够完全映射出他在按哪个键”的程度。
|
||||||
|
|
||||||
|
写代码是个手艺活,要想打磨手艺,需要看到高手是怎么工作的,才不致于固步自封。如果你身边没有这样的人,不如到网上搜一些视频,看看高手在写代码时是怎么做的,这样才能找到差距,不断提高。
|
||||||
|
|
||||||
|
好,今天的答疑就到这里,你对这些问题有什么看法呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 行业不好混的原因。
|
||||||
|
|
||||||
|
同时,这也是我开设这个专栏的初衷,希望给大家一些不同的视角,一些新的前进动力。
|
||||||
|
|
||||||
|
好,今天的答疑就到这里。我想请你分享一下,你是怎么理解这些问题的呢?欢迎在留言区写下你的想法。
|
||||||
|
|
||||||
|
感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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程序员工作法》之旅就暂告一段落了!
|
||||||
|
|
||||||
|
再见!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
06 捕捉 HR 微表情,做出应对策略
|
||||||
|
你好,我是你的面试课老师杨宇堃,欢迎进入第 06 课时的内容“捕捉 HR 微表情,做出应对策略”。
|
||||||
|
|
||||||
|
在面试的过程中,如何判断自己所陈述的信息是面试官感兴趣的呢?怎么才能在恰当的时间更好地展示自己擅长的内容呢?同样,如何在不恰当的时候适可而止,更好地转换话题呢?此时需要精准地捕捉到面试官的微表情,以便在合适的时间突出自己。
|
||||||
|
|
||||||
|
沟通时需注意的小细节
|
||||||
|
|
||||||
|
你有没有经历过这样的窘境:当在描述项目经验时,突然被面试官打断了,虽然此时你正在兴头上,但也请你马上停止,面试官的打断说明他对你的这段经历比较了解,或者刚才的这段描述有了自己的判断,所以要想想刚才的描述是否有漏洞,如果有机会建议再重新解释一下。
|
||||||
|
|
||||||
|
|
||||||
|
有时候也能发现面试官重复提问同样类型的问题,说明他对你之前回答的问题有质疑,希望可以通过重复提问的方式,再次确定这件事情的真实性,此时需要你给出不同的答案或挑选重点内容来回答,如果没有察觉,很有可能就错失了这次机会。
|
||||||
|
|
||||||
|
|
||||||
|
甚至有时候面试官针对某个项目经验进行深入提问,不断地细化你所做的项目数据。这时一定要提高警惕,因为面试官对你的这段经历比较感兴趣,需要通过非常细致地提问,才能了解你在这个项目中真实参与的程度和担任的角色。如果你的回答不够细化或者给出的数据不够精细,那么很容易被误解为并没有参与这个项目的核心内容。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
面试时需留意的微表情
|
||||||
|
|
||||||
|
观察面部微表情可以解读很多信息,进而可以判断面试官是否真的对你所说的内容感兴趣,下面来讲讲面试过程中常见的几个微表情。
|
||||||
|
|
||||||
|
|
||||||
|
当看到面试官的下嘴唇往前撇时,说明他对接收到的信息持有怀疑的态度, 此时需要转化角度或思路来陈述。
|
||||||
|
当看到用牙齿咬嘴唇的时候,说明面试官正在仔细听你的介绍,同时也在默默的思考你所表达的另一层含义是什么。
|
||||||
|
当看到面试官调整自己的坐姿时,比如身体向前移动,很有可能对你所讲的内容很感兴趣;如果发现面试官身体逐渐的后退,说明很有可能对你的这段介绍没有兴趣聆听,此时要及时的调整陈述的思路。
|
||||||
|
当发现面试官双臂交叉时,这是防卫的一种表现。很有可能你说的内容与他的认知完全不相符,也表现出面试官对你表述的观点完全不认同或者完全没有听懂你所表达的意思。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
上面简单讲了一些面试过程中可能碰到的情况,希望你在以后的面试过程中及时捕捉到面试官的微表情,以做出应对的策略。
|
||||||
|
|
||||||
|
|
||||||
|
其实很多时候面试官也在捕捉你的面部表情。当你在阐述的过程中,面试官在倾听是否有漏洞,也许不经意的某个动作或者某句话,也能让面试官察觉到你的问题点,然后做出不一样的决定。所以管理好自己的微表情也是非常有必要的,下面我们来说说在面试时做出的一些不经意的错误微表情有哪些。
|
||||||
|
|
||||||
|
面试时需改掉不好的习惯
|
||||||
|
|
||||||
|
有时候一个沟通时的习惯,也能透露出一些问题,比如:
|
||||||
|
|
||||||
|
|
||||||
|
当习惯说“啊”、“呀”、“这个”、“那个”、“嗯”等口头语时,一般给人留下词汇量小或者思维慢的印象,在说话时需要利用间歇的方式让自己思考;
|
||||||
|
沟通时喜欢使用中英搭配,这样很容易给人一种虚荣心比较强、好表现或夸耀自己的错觉;
|
||||||
|
如果口头禅出现频率过高的话,很容易给人一种办事不干练、意志不坚定的印象。
|
||||||
|
|
||||||
|
|
||||||
|
当然说话声音的大小或者语速的快慢等这些信息也能让面试官初步判断你是一个什么性格的人。
|
||||||
|
|
||||||
|
|
||||||
|
说话声音的大小和一个人的性格联系非常紧密,喜欢大声说话的人,其性格比较以自我为中心,积极主动、行动力和支配欲强,也就是富有攻击性的一类人;说话声音小的人其性格比较偏内向,考虑的因素比较多,很压制自己的情感。
|
||||||
|
语速快慢和声音大小一样,一般语速快的人性格比较外向,有冲劲且有活力,但是常常给人一种紧张和压迫感,让人有种焦躁、混乱甚至有些粗鲁的感觉;但是语速慢的人容易让人感觉比较木讷,容易犹豫不决,甚至有时候有消极悲观的想法。
|
||||||
|
|
||||||
|
|
||||||
|
所以说需要根据你所从事的工作或者要应聘的岗位来调整自己说话的方式,才更能获得面试官的青睐。假如你是一位声音小而且语速慢的人,去面试一家公司的销售岗位,相信这家公司不会对你抛出橄榄枝,因为他们很难从你的沟通中看出你的销售潜力。
|
||||||
|
|
||||||
|
面试中透露出的动作,也需要多多留意
|
||||||
|
|
||||||
|
很多小伙伴可能没有留意在面试过程中做的一些小动作,也许就是这些小动作导致面试官对你的印象减分。下面简单说几个常见的小动作,希望可以帮到你。
|
||||||
|
|
||||||
|
|
||||||
|
吐舌头:一般在感受到有压力时,舌头不自觉地做出舔嘴唇或者看似是在舔嘴唇的动作,说明是对自我的一种安慰。如果做了这个动作说明当时你备感尴尬,希望可以缓解一下气氛。
|
||||||
|
用手捂住嘴巴:这个动作一般表示自己对刚刚说的话已经意识到了错误,下意识的去捂住自己的嘴巴。
|
||||||
|
十指交叉:这个动作很可能是自信的表现,也有可能是在掩盖你的紧张。如果你的十指无意识的交叉在一起,而且眼神也在躲避面试官,很有可能是怕面试官发现你的紧张。
|
||||||
|
抚摸颈部:这个动作说明你并不是很自信,当然也有可能是你正在释放压力,这是一种普遍有力的信号,说明大脑正在积极处理某种消极的情绪。
|
||||||
|
眼神躲避:很多候选人在面试的过程中,经常左顾右看,躲避面试官的眼神,给面试官一种心虚的表现。
|
||||||
|
|
||||||
|
|
||||||
|
当然还有一些其他的小动作,比如揉鼻子、挠头或者摸耳朵、翘二郎腿或抖腿、常扶眼镜、玩弄随身小物件、咬指甲等,这些小动作也都说明你比较紧张或者不够自信。
|
||||||
|
|
||||||
|
希望你可以通过这一课时的学习,合理地控制一下自己的微表情,在面试时更好地表现自己。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
07 巧妙推销自己的 3 个技巧
|
||||||
|
你好,我是你的面试课老师杨宇堃,欢迎进入第 07 课时的内容“巧妙推销自己的 3 个技巧”。
|
||||||
|
|
||||||
|
平和的心态,展现你的热情
|
||||||
|
|
||||||
|
对于任何一家企业在选择合适的人选时,一般都会从这几个方面进行筛选:首先是否具有相关的项目经验;其次是否聪明或者是否具有独自解决问题的能力;最后判断能否融入到团队的氛围中,以及是否对企业或者行业具有热情的态度来面对。
|
||||||
|
|
||||||
|
前两个方面所说的是面试者的行业经验和智商,对于一家公司在招聘时当然重要,但这并不是一个团队可以获得成功的关键因素。相信很多企业在选择五年经验以内的候选人时,这两个方面的考察一定会低于最后性格部分的考察。
|
||||||
|
|
||||||
|
相信很多小伙伴会有疑问,性格(工作热情)相比情商或工作经验对于一家企业真有那么重要吗?这个我可以肯定的告诉你,面试官非常喜欢性格开朗、积极主动、乐于挑战的候选人;同样也很排斥在乎个人得失、把责任都推给前司的候选人。相信你在工作中也会遇到过类似的同事,可以回忆一下,当时你是不是也很排斥呢?
|
||||||
|
|
||||||
|
|
||||||
|
在一个团队里如果存在一位性格比较消极的员工,很容易将其他员工传染,从而导致整个团队产出效率低下。也就是我们常说的“酒与污水定律”:是指把一勺酒倒入到一桶污水里,得到是一桶污水;如果把一勺污水倒入倒一桶酒中,得到的还是一桶污水。所以,对于面试官来说,候选人的性格和态度是至关重要的。
|
||||||
|
|
||||||
|
|
||||||
|
如何在面试官面前表现出积极正向、乐观的心态呢?
|
||||||
|
|
||||||
|
首先,需要表现出对应聘岗位和企业的认同感,也让面试官看到你为了这份工作做了很充分的准备,或者积极的介绍之前做过的项目与应聘企业项目的相似度。这些表现都可以让面试官感受到你的热情和积极正向的输出,非常不建议面试时问什么答什么的做法。
|
||||||
|
|
||||||
|
|
||||||
|
比如,当面试官问「你为什么选择目前的这份工作」时?如果只是单纯地回答「我喜欢这份工作」且没有任何的解释,那面试官无法判断你所说的真实性。此时建议这样回答:“因为目前这份工作和我之前做过的 xxx 项目非常相像。我在参与上一份项目时学习到了 xxx 技能,找到了一个新的发展方向,从而喜欢上了这样的一份职业。”,相信这样的表述面试官才能感受到你的热情和积极正向的态度。
|
||||||
|
|
||||||
|
|
||||||
|
其次,可以和面试官介绍一下,你在上一家公司与同事和领导相处融洽的案例,让面试官感受到你是一个积极融入团队中的人。比如「在前司获得的成长有哪些,与前 leader 的身上都学习到了哪些工作思路和成长思路等」,相信面试官会认为你是一个非常值得培养和积极主动学习的人。
|
||||||
|
|
||||||
|
了解行业发展,清晰表达你的见解
|
||||||
|
|
||||||
|
除了表现出积极和热情以外,如果在面试的过程中可以介绍一些你对行业以及对自己所从事工作的理解或见解,相信面试官一定会被你的表述深深吸引,也同样加强了希望可以录取你的信心。
|
||||||
|
|
||||||
|
比如,当面试官问「你怎么看对目前所从事的工作价值」时,如果这样回答「我觉得这是一份收入,并没有太多的感受,也不知道自己未来的发展是什么样的」,面试官会判断你是一个没有任何思考的人,应该也不会在自己的岗位上有什么作为。
|
||||||
|
|
||||||
|
此时建议这样回答:“我非常喜欢我的工作,我感觉我的岗位在目前行业的发展中起到了非常重要的作用,我们所做的几个项目都在推动公司的发展,也帮助公司的业绩从 XX% 提升到了 XX%(在这里举一些自己做过的项目经验),而且我也希望可以继续从事这样的工作,因为它可以让我获得更多的成就感。相信这个行业的发展是 xxxxxxxxx,我的职业规划也会跟随这个行业的发展而得到很大的提升。”
|
||||||
|
|
||||||
|
当面试官听到这样的介绍时,会非常清晰地了解你对自己的工作已经有了深入的思考,同时也能感受到你不止局限在自己的工作领域中,还在通过行业的变化和了解,来规划自己的职业,是一个很有潜力的候选人。
|
||||||
|
|
||||||
|
真诚的对待每一次面试
|
||||||
|
|
||||||
|
当然除了积极的态度以及清晰的定位以外,还必须是一个真诚、正直的人。如果在面试的过程中表现良好、思考很全面,但是与实际工作的内容只有 50% 的真实度,这样会很容易在面试官面前露馅,然后给你打上一个不真诚的标签,自然而然,面试也就到此结束了。
|
||||||
|
|
||||||
|
因此,很多企业非常在乎候选人是否诚实或者真诚。也许你的职业经历并不是很丰富、项目内容并没有那么完美,但如果将自己所做的内容真实、完整地呈现给面试官,同时加上自己的思考,相信很多面试官会参考你的工作年限然后给你一个非常公平的反馈。
|
||||||
|
|
||||||
|
|
||||||
|
通过以上三个方面的讲述,可以了解到面试官在面试的时候更看重的是品德,然后是性格,最后才是工作经验和学历。希望你听完这一讲的内容后,可以积极主动地面对自己的岗位,更好的去思考如何巧妙的推销自己。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
08 认清自身实力,明确求职方向
|
||||||
|
你好,我是你的面试课老师杨宇堃,欢迎进入第 08 课时的内容“认清自身实力,明确求职方向”。
|
||||||
|
这一讲我们来分析如何更好的认识自己,此时的你可能会很奇怪,我们不是在分析面试时的技巧,以及顺利收到 Offer 吗,为什么这一讲要分析如何看清自己了呢?其实也不奇怪只有更好的认清楚自己,才能更好的明确方向以及获得更好的岗位。
|
||||||
|
|
||||||
|
认清自己的实力
|
||||||
|
|
||||||
|
我经常会被身边的小伙伴问到:我应该选择什么工作方向啊?目前的工作好累啊,想换个工作方向但是不知道如何选择?某某行业的薪资好高啊,要不我去试试吧?
|
||||||
|
|
||||||
|
如果这样问,有可能被目前社会的很多利益所引导,已经忘了出发点是什么或者没有思考过未来的职业规划。平时也会听到身边有人说“希望可以拥有一家自己的咖啡厅”,如果这只是一句玩笑话,说说就过去了;但如果你是认真的,那有没有考虑过:在做这件事情之前都准备了什么呢?所以,在选择工作时先要想想自己的擅长点是什么,同时为这份工作做了哪些准备等。
|
||||||
|
这里我给你推荐一款职业人格评估工具,即 MBTI,来测一测自己是偏外向还是内向、是一个有规划的人还是一个探索性的人、是喜欢做挑战性的工作还是喜欢辅助团队做一些执行层面的工作等,当然在测试的时候要依托于自己的内心哦。
|
||||||
|
|
||||||
|
|
||||||
|
明确求职方向
|
||||||
|
|
||||||
|
测试完以后就要开始思考到底该如何明确求职方向,其实是发挥出自己的优势,去寻找一份适合的工作。俗话说「360 行,行行出状元」,相信任何一份工作的发展前景都是非常光明的,只要在某一份工作上做的足够深入且全面,相信你的职业道路也会越来越宽广。尽量不要对自己还没有接触过的事情就开始焦虑,这样会限制自己的想法。
|
||||||
|
比如,现在大家求职一般会从两个方面考虑,即领域相关的和专业度相关的。
|
||||||
|
|
||||||
|
|
||||||
|
领域相关的可分为互联企业和传统企业,比如,如果你是一个充满创新能力且积极愿意改变自己生活的人,可以选择去互联网行业;如果你比较喜欢稳定,对程序化的工作比较看重可以选择去传统企业。
|
||||||
|
专业度相关的可分为:硬技能型和软技能型。例如,硬技能型有编辑、会计、研发、法律、统计等;软技能型有销售、活动执行、客服、创意等。
|
||||||
|
具体可参考如下表格:
|
||||||
|
| | 互联网 | 传统行业 |
|
||||||
|
| —- | ———————- | —————- |
|
||||||
|
| 硬性 | 研发工程师、数据分析师 | 会计、机械师 |
|
||||||
|
| 软性 | 产品经理、市场、销售 | 销售、市场、客服 |
|
||||||
|
根据拉勾后台数据的显示,以下是互联网行业热门岗位的 Top 5:
|
||||||
|
研发工程师岗位需要具备技术能力、逻辑能力、时间观念等
|
||||||
|
产品经理岗位需要具备逻辑能力、沟通能力、执行力等
|
||||||
|
运营岗位需要具备一定的分析能力以及对宏观的管理能力等
|
||||||
|
市场岗位需要具备创意的想法、有一个开朗的性格、沟通能力等
|
||||||
|
销售岗位需要具备较强的沟通能力且有一个开朗的性格等
|
||||||
|
若想了解其他具体的岗位详情,建议可通过 MBTI 的测试来了解自己擅长的工作岗位,相信测试完后的结果与测试前的认知岗位会有重合。此时,可以通过拉勾网站去搜索密切度高的岗位,然后查看该岗位的职位 JD,建议搜索查看同一岗位的多家职位 JD,把JD 重合的部分标记出来,其实这些就是该岗位所必需的技能;当然了,多家职位 JD 也有不同的要求,这时可根据自己擅长的点和过往的经历来选出最合适的公司。
|
||||||
|
|
||||||
|
也可以通过回复你和对你产生邀约的公司的职位要求进行描述,再次验证是否真的是你的技能方向。
|
||||||
|
找到自己发展的方向是一方面,当然也需要你对自己的工作年限和自我能力作出正确的认知。在我多年的筛选简历中,经常可以收到一个工作三年左右的小伙伴投递了公司高管的岗位,也许你的职位优势是一个非常具有管理能力的人,但是对于高管岗位还是需要你有很多年的工作经验和项目经验做累计的。所以建议大家在投递简历的时候也要明确自己应该在什么工作年限和工作的阶段,从而找到最合适自己的岗位。
|
||||||
|
相信通过几年的磨练,在目前的岗位上,也能很快得到大家希望有用的职级和薪资。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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,这门课到这里就结束啦,希望这门课可以帮助你找到心仪的工作。感谢你的收听~
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 讲。
|
||||||
|
|
||||||
|
|
||||||
|
模块一,分布式数据历史演变及其核心原理。从历史背景出发,讲解了分布式数据库要解决的问题、应用场景,以及核心技术特点。
|
||||||
|
模块二,分布式数据库的高性能保证——存储引擎。这是专栏的亮点内容,简要展示了现代数据库的存储引擎,比如典型存储引擎、分布式索引、数据文件与日志结构存储、事务处理。其中,我会特别介绍分布式数据库与传统数据库在存储层面上的差异。学完之后,你会对分布式数据库中的重要特性(如一致性和分布式事务)有一个完整的理解,明白为什么一些特定存储引擎(如日志结构存储)更适合去构建分布式数据库。
|
||||||
|
模块三,分布式数据库的高扩展性保证——分布式系统。详细介绍分布式数据库中所蕴含的系统设计原理、算法等,包含但不限于错误侦测、领导选举、数据可靠传播、分布式事务、共识算法等内容。虽然分布式内容很多,但我不会面面俱到,而是帮你提炼精华,基于实例为你建立知识体系。
|
||||||
|
模块四,知识拓展。我会和你探讨当代最成功的分布式数据库(传统&新型),探讨它们成功的关键,同时将它们与之前模块中所介绍的技术原理进行相应的映射,让你的知识体系更加丰富。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
讲师寄语
|
||||||
|
|
||||||
|
本课程的设计目标是,尽最大程度解决你的实际问题,让你在不同的工程实践中,对分布式场景下的数据库存储有更加专业的认知,并对技术趋势建立深入的洞察。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 型数据库的关键技术、使用场景和应用案例。使你在未来可以更好地评估和使用分布式数据库。
|
||||||
|
|
||||||
|
而分布式数据库的历史同时体现了实用主义的特色,其演化是需求与技术博弈的结果,而不是精心设计出来的。我们的课程也会体现出实用主义的特点,让你学以致用,学有所获。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 数据库。
|
||||||
|
|
||||||
|
这里我要强调一下,你不需要陷入概念区分的陷阱里,这不是考试,但现实生活比考试要复杂。把握住关键特点,才可以以不变应万变。
|
||||||
|
|
||||||
|
好了,答疑就先到这里。最后再次感谢你的积极反馈,希望在下一个模块结束后也能看到你精彩的留言。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
本讲首先回顾了模块一的主要内容,帮助你将各个部分串联起来,形成完整的知识拼图。而后通过一个案例介绍了如何使用这些知识设计一个分布式数据库,将所学知识应用到实际工作和学习中。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
07 概要:什么是存储引擎,为什么需要了解它?
|
||||||
|
经过第一个模块的学习,相信你已经知道了什么是分布式数据库,对分布式数据库的核心知识有了比较全面和深入的了解了。
|
||||||
|
|
||||||
|
这一讲是第二模块存储引擎的概要,主要目的是为你解释什么是存储引擎,以及它在分布式数据库中起到什么样的作用。
|
||||||
|
|
||||||
|
数据库的一个首要目标是可靠并高效地管理数据,以供人们使用。进而不同的应用可以使用相同的数据库来共享它们的数据。数据库的出现使人们放弃了为每个独立的应用开发数据存储的想法,同时,随着数据库广泛的使用,其处理能力飞速发展,演进出如现代的分布式数据库这般惊人的能力。
|
||||||
|
|
||||||
|
那么,为了支撑抽象的多种场景。一般的数据库都会采用多模块或多子系统的架构来构建数据库,从而方便数据库项目团队依据现实的场景来组合不同的子模块,进而构造出一众丰富的数据库产品。
|
||||||
|
|
||||||
|
而存储引擎就是这一众模块中极为重要的一环,下面我们开始解释它在整个数据库架构中的定位和意义。
|
||||||
|
|
||||||
|
存储引擎的定位
|
||||||
|
|
||||||
|
这个世界上,没有针对数据库设计的一定之规。每个数据库都是根据它所要解决的问题,并结合其他因素慢慢发展成如今的模样的。所以数据库子模块的分化也没有一个广泛接受的标准,且有些模块之间的边界也是很模糊的。特别是需要优化数据库性能时,原有被设计为独立存在的模块很可能会融合以提高数据库整体性能。
|
||||||
|
|
||||||
|
这里,我总结出了一个比较典型的分布式数据库的架构和模块组合标准。虽然不能完全代表所有分布式数据库,但是可以帮助你理解模块的组成方式。这里需要注意,我给出的模型是基于客户端/服务器,也就是 C/S 模式的,因为这是大部分分布式数据库的架构模式。
|
||||||
|
|
||||||
|
|
||||||
|
传输层:它是接受客户端请求的一层。用来处理网络协议。同时,在分布式数据库中,它还承担着节点间互相通信的职责。
|
||||||
|
查询层:请求从传输层被发送到查询层。在查询层,协议被进行解析,如 SQL 解析;后进行验证与分析;最后结合访问控制来决定该请求是否要被执行。解析完成后,请求被发送到查询优化器,在这里根据预制的规则,数据分布并结合数据库内部的统计,会生成该请求的执行计划。执行计划一般是树状的,包含一系列相关的操作,用于从数据库中查询到请求希望获取的数据。
|
||||||
|
执行层:执行计划被发送到执行层去运行。执行层一般包含本地运行单元与远程运行单元。根据执行计划,调用不同的单元,而后将结果合并返回到传输层。
|
||||||
|
|
||||||
|
|
||||||
|
细心的你可能会注意到,这里只有查询层,那么数据是怎么写入的?这对于不同的数据库,答案会非常不同。有的数据库会放在传输层,由于协议简单,就不需要额外处理,直接发送到执行层;而有些写入很复杂,会交给查询层进行处理。
|
||||||
|
|
||||||
|
以上就是数据库领域中比较常见的模块划分方式。你可能有这样的疑问:那么存储引擎在哪里呢?
|
||||||
|
|
||||||
|
执行层本地运行单元其实就是存储引擎。它一般包含如下一些功能:
|
||||||
|
|
||||||
|
|
||||||
|
事务管理器:用来调度事务并保证数据库的内部一致性(这与模块一中讨论的分布式一致性是不同的);
|
||||||
|
锁管理:保证操作共享对象时候的一致性,包括事务、修改数据库参数都会使用到它;
|
||||||
|
存储结构:包含各种物理存储层,描述了数据与索引是如何组织在磁盘上的;
|
||||||
|
内存结构:主要包含缓存与缓冲管理,数据一般是批量输入磁盘的,写入之前会使用内存去缓存数据;
|
||||||
|
提交日志:当数据库崩溃后,可以使用提交日志恢复系统的一致性状态。
|
||||||
|
|
||||||
|
|
||||||
|
以上就是存储引擎比较重要的几个功能,其核心就是提供数据读写功能,故一般设计存储引擎时,会提供对其写入路径与读取路径的描述。
|
||||||
|
|
||||||
|
好了,现在你清楚了存储引擎的定位和主要结构,那么存储引擎的种类也是很多的,下面我通过一些关键特性,来介绍几种典型的存储引擎。
|
||||||
|
|
||||||
|
内存与磁盘
|
||||||
|
|
||||||
|
存储引擎中最重要的部分就是磁盘与内存两个结构。而根据数据在它们之中挑选一种作为主要的存储,数据库可以被分为内存型数据库与磁盘型数据库。由此可见存储引擎的一个功能,就是可以被作为数据库类型划分的依据,可见引擎的重要性。
|
||||||
|
|
||||||
|
内存型存储是把数据主要存储在内存里,其目的很明显,就是加快数据读写性能。分布式数据库一个重要的门类就是内存型数据库,包括 Redis、NuoDB 和 MySQL Cluster 等。当然其缺点也很明显,那就是内存的成本较高,且容量有限。而分布式的架构能有效地扩充该类数据库的容量,这也是内存数据库主要是分布式数据库的原因。
|
||||||
|
|
||||||
|
磁盘存储相对传统,它存储主要数据,而内存主要作为缓冲来使写入批量化。磁盘存储的好处是,存储性价比较高,这主要得益于磁盘甚至是磁带的单位存储价格相比内存非常低廉。但是与内存型数据库相比,磁盘型数据库的性能比较低。不过,随着近年 SSD 磁盘的普及,这种趋势得到了有效的改善。
|
||||||
|
|
||||||
|
这两种存储引擎的差别还体现在功能实现的难度上。内存型数据库相对简单,因为写入和释放随机的内存空间是相对比较容易的;而磁盘型数据库需要处理诸如数据引用、文件序列化、碎片整理等复杂的操作,实现难度很高。
|
||||||
|
|
||||||
|
从目前的分布式数据库发展来看,磁盘型存储引擎还是占据绝对统治地位的。除了性价比因素外,内存型数据库要保证不丢失数据的代价是很高昂的,因为掉电往往就意味着数据的丢失。虽然可以使用不间断电源来保证,但是需要复杂的运维管理来保证数据库稳定运行。
|
||||||
|
|
||||||
|
然而近年来,随着 NVM(Non-Volatile Memory,非易失性内存)等技术的引入。这种情况开始出现了一些变化,此种存储具有 DRAM 内存的性能,同时能保证掉电后数据不丢失。且最重要的是读写模式类似于内存,方便应用去实现功能。有了它的加持,未来内存型数据库还将有比较大的发展。
|
||||||
|
|
||||||
|
除了硬件加持,内存型数据库也可以通过结构设计来保证数据不丢失。最常用的手段就是使用数据备份+提交日志的模式。数据库为了不影响写入读取性能,可以异步地备份数据。同时在每次写入数据之前要先写入提交日志,也就是说提交日志的写入成功才被认为是数据写入成功。
|
||||||
|
|
||||||
|
当数据库节点崩溃恢复后,将备份拿出来,计算出该备份与最新日志之间的差距,然后在该备份上重放这些操作。这样就保证数据库恢复出了最新的数据。
|
||||||
|
|
||||||
|
除了内存和磁盘的取舍,存储引擎还关心数据的组合模式,现在让我们看看两种常见的组合方式:行式与列式。
|
||||||
|
|
||||||
|
行式存储与列式存储
|
||||||
|
|
||||||
|
数据一般是以表格的形式存储在数据库中的,所以所有数据都有行与列的概念。但这只是一个逻辑概念,我们将要介绍的所谓“行式”和“列式”体现的其实是物理概念。
|
||||||
|
|
||||||
|
行式存储会把每行的所有列存储在一起,从而形成数据文件。当需要把整行数据读取出来时,这种数据组织形式是比较合理且高效的。但是如果要读取多行中的某个列,这种模式的代价就很昂贵了,因为一些不需要的数据也会被读取出来。
|
||||||
|
|
||||||
|
而列式存储与之相反,不同行的同一列数据会被就近存储在一个数据文件中。同时除了存储数据本身外,还需要存储该数据属于哪行。而行式存储由于列的顺序是固定的,不需要存储额外的信息来关联列与值之间的关系。
|
||||||
|
|
||||||
|
列式存储非常适合处理分析聚合类型的任务,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。
|
||||||
|
|
||||||
|
我们会发现 OLTP 数据库倾向于使用行式存储,而 OLAP 数据库更倾向于列式存储,正是这两种存储的物理特性导致了这种倾向性。而 HATP 数据库也是融合了两种存储模式的一种产物。
|
||||||
|
|
||||||
|
当然这里我们要区分 HBase 和 BigTable 所说的宽列存储与列存储在本质上是不同的。宽列存储放在其中的数据的列首先被聚合到了列簇上,列簇被放在不同的文件中;而列簇中的数据其实是按行进行组织的。
|
||||||
|
|
||||||
|
选择行模式与列模式除了以上的区分外,一些其他特性也需要考虑。在现代计算机的 CPU 中,向量指令集可以一次处理很多类型相同的数据,这正是列式存储的特点。同时,将相同类型数据就近存储,还可以使用压缩算法大大减少磁盘空间的占用。
|
||||||
|
|
||||||
|
当然,选择这两种存储模式最重要的因素还是访问模式。如果数据主要是按照行进行读取,比如交易场景、资料管理场景等,那么行式存储应是首选。如果需要经常查询所有数据做聚合,或者进行范围扫描,那么列式存储就很值得一试。
|
||||||
|
|
||||||
|
以上就是常见的数据的组合模式,那么组合好的数据如何存储在物理设备上呢?下面让我们探讨一下数据文件和索引文件两种常用的存放数据的物理原件。
|
||||||
|
|
||||||
|
数据文件与索引文件
|
||||||
|
|
||||||
|
上文介绍了内存与磁盘之间的取舍,从中可看到磁盘其实更为重要的,因为数据库是提供数据持久化存储的服务。故我们开始介绍磁盘上最为重要的两类文件:数据文件和索引文件。
|
||||||
|
|
||||||
|
数据文件和索引文件如名字所示,分别保存原始数据与检索数据用的索引数据。
|
||||||
|
|
||||||
|
但是随着时间的推移,两者的区分也不是那么泾渭分明了。其中以 IOT(索引组织表)模式为代表的数据文件在数据库,特别是分布式数据库中占据越来越重的位置。一种将两者进行融合的趋势已经变得势不可挡。
|
||||||
|
|
||||||
|
数据文件最传统的形式为堆组织表(Heap-Organized Table),数据的放置没有一个特别的顺序,一般是按照写入的先后顺序排布。这种数据文件需要一定额外的索引帮助来查找数据。
|
||||||
|
|
||||||
|
另外有两种数据表形式自带了一定的索引数据能力,即哈希组织表(Hash-Organized Table)和索引组织表(Index-Organized Table)。前者是将数据通过哈希函数分散到一组数据桶内,桶内的数据一般是按照一定规则进行排序,以提高查询效率;而后者一般采用索引文件的形式来存储数据,以 B+树为例,数据被存储在叶子节点上,这样做的目的是减少检索数据时读取磁盘的次数,同时对范围扫描支持友好。
|
||||||
|
|
||||||
|
索引文件的分类模式一般为主键索引与二级索引两类。前者是建立在主键上的,它可能是一个字段或多个字段组成。而其他类型的索引都被称为二级索引。主键索引与数据是一对一关系,而二级索引很有可能是一对多的关系,即多个索引条目指向一条数据。
|
||||||
|
|
||||||
|
这里按照索引与数据之间结合的程度,我们又可以把索引分为聚簇索引和非聚簇索引。前者如哈希组织表和索引组织表那样,数据的分布与索引分布是有关联的,它们被“聚”在一起,这样的查询效率很好。而后者最常见的例子就是针对这两种数据文件的二级索引,因为二级索引要索引的列不是主键,故索引与数据是分割的,查询时需要进行多次磁盘读取。但是对于写入,聚簇索引可能需要进行唯一判断,性能会比简单构建的非聚簇索引低效。
|
||||||
|
|
||||||
|
最后一点需要说明的是,二级索引需要保存指向最终数据的“引用”。从实现层面上,这个引用可以是数据的实际位置,也可以是数据的主键。前者的好处是查询效率高,而写入需要更新所有索引,故性能相对较低。而后者就恰好相反,查询需要通过主键索引进行映射,效率稍低,但写入性能很稳定,如 MySQL 就是选用后者作为其索引模式。
|
||||||
|
|
||||||
|
面向分布式的存储引擎特点
|
||||||
|
|
||||||
|
以上内容为存储引擎的一些核心内容。那分布式数据库相比传统单机数据库,在存储引擎的架构上有什么不同呢?我总结了以下几点。
|
||||||
|
|
||||||
|
内存型数据库会倾向于选择分布式模式来进行构建。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。
|
||||||
|
|
||||||
|
列式存储也与分布式数据库存在天然的联系。你可以去研究一下,很多列式相关的开源项目都与 Hadoop 等平台有关系的。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。
|
||||||
|
|
||||||
|
而列式存储可以被认为是这种模式的一种优化,实现该模式的必要条件是要有分布式系统,因为一台机器的处理能力是有瓶颈的。如果希望处理超大规模数据,那么将数据分散到多个节点就成为必要的方式。所以说,列模式是由分析性分布式的优化需求所流行起来的。
|
||||||
|
|
||||||
|
至于宽列存储更是分布式数据库场景下才会采用的模式。
|
||||||
|
|
||||||
|
数据文件的组织形式,分布式数据库几乎不会使用堆组织表。因为该形式过于随意,无法有效地分散数据。不知道学习过数据分片那一讲的时候你有没有注意到,另外两种组织表的名字与两种分片算法是有着天然联系的。
|
||||||
|
|
||||||
|
哈希组织表数据经过哈希函数散列到不同的桶,这些桶可以被分散到不同节点。而索引组织表一般叶子节点是按一定顺序排列的,这与范围分片又有着某种契合的关系。所以分布式数据库一般都会采用这两种模式作为其存储引擎,甚至一些分布式数据库直接将数据文件当作索引使用。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
好了,关于存储引擎我就介绍到这了。这一讲我们首先展示了数据库的整体架构,并点出了存储引擎所在的位置;而后分别讨论了存储引擎中几组概念的对比,并在最后说明了分布式数据库在引擎层面的选择及其原因。
|
||||||
|
|
||||||
|
当然,本讲只是一篇概述。存储引擎中其他重要的概念,我会在本模块随后的几讲中为你详细介绍。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 讲中介绍)实现的键值分离模式,使用组合模式就有意义。同时由于键组合模式比较简单,且适合键扫描算法的实现,故是一种比较常见二级索引形式。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
本讲内容就介绍到这里了。这一讲我们首先说明了分布式索引的概念,实际上它就是分布式数据库存储引擎中用来存储数据的所有技术的总称;而后我介绍了存储引擎的查询路径,帮你在心中建立起存储引擎处理查询的整体概念;最后我又分别介绍了影响查询路径的多个关键技术,并给出了实际的案例。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
11 事务处理与恢复(下):如何控制并发事务?
|
||||||
|
上一讲,我们介绍了事务的基本概念和数据库恢复流程,其中涉及了事务持久性是如何保证的,那么这一讲,我们就重点介绍事务的隔离性。
|
||||||
|
|
||||||
|
数据库最强的隔离级别是序列化,它保证从事务的角度看自己是独占了所有资源的。但序列化性能较差,因此我们引入了多种隔离界别来提高性能。在本讲的最后我会介绍分布式数据库中常用的并发控制手段,它们是实现隔离级别的有效方案,其中以多版本方式实现快照隔离最为常见。
|
||||||
|
|
||||||
|
现在让我们开始今天的内容。
|
||||||
|
|
||||||
|
隔离级别
|
||||||
|
|
||||||
|
在谈隔离级别之前,我们先聊聊“序列化”(Serializability)的概念。
|
||||||
|
|
||||||
|
序列化的概念与事务调度(Schedule)密切相关。一个调度包含该事务的全部操作。我们可以用 CPU 调度理论来类比,当一个事务被调度后,它可以访问数据库系统的全部资源,同时会假设没有其他事务去影响数据库的状态。这就类似于一个进程被 CPU 调度,从而独占该 CPU 资源(这里的 CPU 指的是时分系统)。但是实际设计调度时,会允许调度事务内部的操作被重新排序,使它们可以并行执行。这些都是优化操作,但只要不违反 ACID 的原则和结果的正确性就可以了。
|
||||||
|
|
||||||
|
那什么是序列化呢?如果一个调度被说成是序列化的,指的是它与其他调度之间的关系:在该调度执行时没有其他被调度的事务并行执行。也就是说,调度是一个接着一个顺序执行的,前一个调度成功完成后,另一个调度再执行。这种方法的一个好处是执行结果比较好预测。但是,我们发现这种做法有明显的缺陷:性能太低。在实现时,一个序列化调度可能会并行执行多个事务操作,但是会保证这样与一个个顺序执行调度有相同的结果。
|
||||||
|
|
||||||
|
以上就是序列化的概念,它揭示了序列化也会存在并发执行的情况。这一点很重要,在隔离理论中,一个隔离概念只是描述了一种行为,而在实现层面可以有多种选择,只要保证这个行为的结果符合必要条件就没有问题了。
|
||||||
|
|
||||||
|
序列化是最强的事务隔离级别,它是非常完美的隔离状态,可以让并行运行的事务感知不到对方的存在,从而安心地进行自己的操作。但在实现数据库事务时,序列化存在实现难度大、性能差等问题。故数据库理论家提出了隔离级别的概念,用来进行不同程度的妥协。在详解隔离级别之前,来看看我们到底可以“妥协”什么。
|
||||||
|
|
||||||
|
这些“妥协”被称为读写异常(Anomalies)。读异常是大家比较熟悉的,有“脏读”“不可重读”和“幻读”。写异常不太为大家所知,分别是“丢失更新”“脏写”和“写偏序”。读异常和写异常是分别站在使用者和数据本身这两个角度去观察隔离性的,我们将成对介绍它们。传统上隔离级别是从读异常角度描述的,但是最近几年,一些论文也从写异常角度出发,希望你能明白两种表述方式之间是有联系的。下表就是经典隔离级别与读异常的关系。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
从中可以看到序列化是不允许任何读写异常存在的。
|
||||||
|
|
||||||
|
可重读允许幻读的产生。幻读是事务里面读取一组数据后,再次读取这组数据会发现它们可能已经被修改了。幻读对应的写异常是写偏序。写偏序从写入角度发现,事务内读取一批数据进行修改,由于幻读的存在,造成最终修改的结果从整体上看违背了数据一致性约束。
|
||||||
|
|
||||||
|
读到已提交在可重读基础上放弃了不可重读。与幻读类似,但不可重读针对的是一条数据。也就是只读取一条数据,而后在同一个事务内,再读取它数据就变化了。
|
||||||
|
|
||||||
|
刚接触这个概念的同学可能会感觉匪夷所思,两者只相差一个数据量,就出现了两个隔离级别。这背后的原因是保证一条数据的难度要远远低于多条,也就是划分这两个级别,主要的考虑是背后的原理问题。而这个原理又牵扯出了性能与代价的问题。因此就像我在本专栏中反复阐述的一样,一些理论概念有其背后深刻的思考,你需要理解背后原理才能明白其中的奥义。不过不用担心,后面我会详细阐述它们之间实现的差别。
|
||||||
|
|
||||||
|
而不可重读对应的是丢失更新,与写偏序类似,丢失更新是多个事务操作一条数据造成的。
|
||||||
|
|
||||||
|
最低的隔离级别就是读到未提交,它允许脏读的产生。脏读比较简单,它描述了事务可以读到其他事务为提交的数据,我们可以理解为完全没有隔离性。而脏读本身也会造成写异常:脏写。脏写就是由于读到未提交的数据而造成的写异常。
|
||||||
|
|
||||||
|
以上,我们详细阐述了经典的隔离级别。但是这套理论是非常古早的,较新的 MVCC 多版本技术所带来的快照隔离又为传统隔离级别增添一个灵活选型。它可以被理解为可重读隔离级别,也就是不允许不可重读。但是在可重读隔离下,是可以保证读取不到数据被修改的。但快照隔离的行为是:一旦读到曾经读过的数据被修改,将立即终止当前事务,也就是进行回滚操作。在多并发事务下,也就是只有一个会成功。你可以细细品味两者的差异。
|
||||||
|
|
||||||
|
快照隔离可以解决丢失更新的问题,因为针对同一条数据可以做快照检测,从而发现数据被修改,但是不能防止写偏序的问题。
|
||||||
|
|
||||||
|
快照隔离是现代分布式数据库存储引擎最常使用的隔离级别,而解决写偏序问题,也是存储引擎在该隔离级别下需要解决的问题。SSI(Serializable Snaphost Isoltion)正是解决这个问题的方案,我会在“18 | 分布式事务:‘老大难’问题的最新研究与实践”中详细介绍该方案。
|
||||||
|
|
||||||
|
至此我们讨论了典型的隔离级别,隔离级别与分布式一致性的关系我在“”中已经有过阐述,如果需要复习,请出门左转。现在让我们接着讨论如何实现这些隔离级别。
|
||||||
|
|
||||||
|
并发控制
|
||||||
|
|
||||||
|
目前存储引擎引入多种并发模型来实现上面提到的隔离级别,不同的模式对实现不同的级别是有偏好的,虽然理论上每种控制模型都可以实现所有级别。下面我就从乐观与悲观、多版本、基于锁的控制三个方面进行介绍。
|
||||||
|
|
||||||
|
乐观与悲观
|
||||||
|
|
||||||
|
乐观与悲观的概念类似于并发编程中的乐观锁与悲观锁。但是这里你要注意,实现它们并不一定要借助锁管理。
|
||||||
|
|
||||||
|
乐观控制使用的场景是并行事务不太多的情况,也就是只需要很少的时间来解决冲突。那么在这种情况下,就可以使用一些冲突解决手段来实现隔离级别。最常用的方案是进行提交前冲突检查。
|
||||||
|
|
||||||
|
冲突检查有多种实现模式,比如最常用的多版本模式。而另一种古老的模式需要检查并行事务直接操作的数据,也就是观察它们操作的数据是否有重合。由于其性能非常差,已经很少出现在现代存储引擎中了。这里需要你注意的是,乐观控制不一定就是多版本这一种实现,还有其他更多的选择。
|
||||||
|
|
||||||
|
同样的,悲观控制也不仅仅只有锁这一种方案。一种可能的无锁实现是首先设置两个全局时间戳,最大读取时间与最大写入时间。如果一个读取操作发生的时间小于最大写入时间,那么该操作所在的事务被认为应该终止,因为读到的很可能是旧数据。而一个写操作如果小于最大读取时间,也被认为是异常操作,因为刚刚已经有读取操作发生了,当前事务就不能去修改数据了。而这两个值是随着写入和读取操作而更新的。这个悲观控制被称为 Thomas Write Rule,对此有兴趣的话你可以自行搜索学习。
|
||||||
|
|
||||||
|
虽然乐观与悲观分别有多种实现方案,但乐观控制最为常见的实现是多版本控制,而悲观控制最常见的就是锁控制。下面我就详细介绍它们。
|
||||||
|
|
||||||
|
多版本
|
||||||
|
|
||||||
|
多版本并发控制(MVCC,Multiversion concurrency control)是一种实现乐观控制的经典模式。它将每行数据设置一个版本号,且使用一个单调递增的版本号生成器来产生这些版本号,从而保证每条记录的版本号是唯一的。同时给每个事物分为一个 ID 或时间戳,从而保证读取操作可以读到事务提交之前的旧值。
|
||||||
|
|
||||||
|
MVCC 需要区分提交版本与未提交版本。最近一次提交的版本被认为是当前版本,从而可以被所有事务读取出来。而根据隔离级别的不同,读取操作能或者不能读取到未提交的版本。
|
||||||
|
|
||||||
|
使用 MVCC 最经典的用法是实现快照隔离。事务开始的时候,记录当前时间,而后该事务内所有的读取操作只能读到当前提交版本小于事务开始时间的数据,而未提交的数据和提交版本大于事务开始时间点的数据是不能读取出来的。如果事务读取的数据已经被其他事务修改,那么该数据应该在上一讲提到的 undo log 中,当前事务还是能够读取到这份数据的。故 undo log 的数据不能在事务提交的时候就清除掉,因为很可能有另外的事务正在读取它。
|
||||||
|
|
||||||
|
而当事务提交的时候,数据其实已经写入完成。只需要将版本状态从未提交版本改为提交版本即可。所以 MVCC 中的提交操作是非常快的,这点会对分布式事务有很多启示。
|
||||||
|
|
||||||
|
而上文提到的 SSI 模式可以在 MVCC 的基础上引入冲突解决机制,从而解决写偏序问题。当提交发生的时候,事务会检测其修改和读取的数据在提交之前是否已经被其他已提交事务修改了,如果是,则会终止当前事务,并进行回滚。同时这个冲突检测时机会有两个:一个是在事务内进行读取操作时就进行检测,称为前向检测(forward)。而相对的,在提交时进行检测被称为后向检测(backward)。你会明显感觉到,前者会快速失败,但是性能较低;而后者对异常的反应较慢,但速度会有优势。
|
||||||
|
|
||||||
|
这就是经典的 MVCC 并发控制,现在让我接着介绍典型的悲观控制:锁控制。
|
||||||
|
|
||||||
|
基于锁的控制
|
||||||
|
|
||||||
|
基于锁的控制是典型的悲观控制。它会使用显示的锁来控制共享资源,而不是通过调度手段来实现。锁控制可以很容易实现“序列化操作”,但是它同时存在锁竞争和难扩展等问题。
|
||||||
|
|
||||||
|
一个比较知名的锁技术是两阶段锁(2PL),它将锁操作总结为两个阶段。
|
||||||
|
|
||||||
|
|
||||||
|
锁膨胀阶段。在该过程中,事务逐步获得所有它需要的锁,同时不释放任何锁。这期间事务可以对加锁的数据进行操作。
|
||||||
|
锁收缩阶段。该过程中,在上一过程中获得的锁全部被释放。这个事务是逐步的,这期间事务依然可以对还持有锁的数据进行操作。
|
||||||
|
|
||||||
|
|
||||||
|
以上过程简单明了,它是针对一次性加锁提出来的,一次性加锁的缺点是没有并发度,性能低;而两阶段锁可以保证一定的并发度,但其缺点是会有死锁的产生。
|
||||||
|
|
||||||
|
死锁是两个事务互相持有对方的锁,从而造成它们都无法继续运行。解决死锁需要引入超时机制,但超时机制又有明显的性能缺憾。此时,人们会引入死锁检测机制来尽早发现死锁。一般实现手段是将所有事务的锁依赖构建成一棵依赖图,而后使用图算法来发现其中的环形死锁结构,从而快速判断死锁的产生。
|
||||||
|
|
||||||
|
而与锁相对的一个概念就是“闩”(latch,读“shuān”)。一般资料说闩是轻量的,锁是重量的,这其实体现在两个方面。
|
||||||
|
|
||||||
|
一是说它们处理的对象。闩一般用在粒度很小的数据中,比如数据块、索引树的节点等。而锁一般作用在大颗粒操作,如锁定多行数据、事务和修改存储结构等。
|
||||||
|
|
||||||
|
二是它们本身的实现不同。闩一般使用 CAS 执行,是基于比较而后设置的无锁指令级别的操作。如果原始值发生变化就重新进行以上操作,这个过程叫自旋(spin)。而锁是使用独立的资源,且有锁管理器来控制。可想而知,调度锁也是一个比较耗时且复杂的过程。
|
||||||
|
|
||||||
|
这里就要解释上文中隔离级别“序列化”和“可重读”之间实现的差异了。“序列化”由于要保证一组数据重复读取的一致性,就需要引入重量级的锁,其代价是很高的;而“可重读”只需要保证一行数据重复读取是一致的,它可以使用轻量级的闩来实现。故隔离级别将它们分成两种是非常合理的,因为从原理看,它们是完全不同的。
|
||||||
|
|
||||||
|
以上就是关于基于锁的控制的相关内容。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
本讲内容就介绍到这里了。事务是我们课程到目前为止最长的内容,用了两讲的篇幅来详细介绍。事务的话题在数据库领域一直很热门,我从事务原理层面切入,解释了 ACID 和不同隔离级别所需要的技术手段。这些内容为分布式事务的学习打下坚实的基础,同时你可以将本专栏作为一份参考资料,随时进行查阅。
|
||||||
|
|
||||||
|
从本质出发,事务是一个面向使用者的概念,它向使用者提供一种契约,目的是使人们可以可靠地使用数据库保存和共享数据,这是数据库最核心的功能,且有众多的应用是基于该功能构建的,这也是分布式数据库为什么要实现分布式条件下的事务的根本原因。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 部分称为 vLogs(value Logs),其中的数据是没有顺序的。这种结构适合更新和删除比较少的场景,因为范围扫描会使用随机读,如果更新删除很多,那么其冲突合并的效率很低。同时在合并操作的时候,需要扫描 Key 而后确定合并方案,这个在普通的 LSM 树中也是不存在的。
|
||||||
|
|
||||||
|
WiscKey 非常适合在 SSD 进行运行,因为读取 Value 需要进行随机读取。目前 dgraph.io 的 Badger 是该模式比较成熟的实现。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
到这里,这一讲内容就说完了。我带你回顾了第二模块的主要内容,这是一个基础知识普及模块,将为接下来的分布式模块打下基础。同时相对于传统关系型数据库,分布式数据库的存储引擎也有其自身特点,如 LSM 树结构,你需要认真掌握这种结构。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
13 概要:分布式系统都要解决哪些问题?
|
||||||
|
在学习了存储引擎相关内容之后,从这一讲开始,我们就进入新的模块——分布式数据库最核心的部分,那就是分布式系统。
|
||||||
|
|
||||||
|
分布式数据库区别于传统数据库的一个重要特性就是其分布式的特点,这些特点来源于分布式理论的发展,特别是数据分布相关理论的发展。相比于无状态分布式系统,有状态的数据库在分布式领域中将会面对更多的挑战。
|
||||||
|
|
||||||
|
本讲内容作为整个模块三的引子,我将会向你提出一系列问题,而在后续的课程中,我会逐一回答这些问题。那么现在让我们从失败模型开始,讨论分布式模式下的数据库吧。
|
||||||
|
|
||||||
|
失败模型
|
||||||
|
|
||||||
|
分布式系统是由多个节点参与其中的,它们直接通过网络进行互联。每个节点会保存本地的状态,通过网络来互相同步这些状态;同时节点需要访问时间组件来获取当前时间。对于分布式系统来说,时间分为逻辑时间与物理时间。逻辑时间一般被实现为一个单调递增的计数器,而物理时间对应的是一个真实世界的时间,一般由操作系统提供。
|
||||||
|
|
||||||
|
以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的,“不可靠”贯穿了分布式系统的整个生命周期。而总结这些不可靠就成为失败模型所解决的问题。
|
||||||
|
|
||||||
|
在介绍失败模型的具体内容之前,让我们打开思路,看看有哪些具体的原因引起了分布式系统的可靠性问题。
|
||||||
|
|
||||||
|
引起失败的原因
|
||||||
|
|
||||||
|
当讨论分布式系统内的不稳定因素的时候,人们首先会想到网络问题,但是一个最容易让大家忽略的地方就是远程节点处理请求时也可能发生故障。一个比较常见的误区就是认为远程执行会马上返回结果,但这种假设是非常不可靠的。因为远程节点的处理能力、运行环境其实是未知的,我们不能认为它们会一直按照固定的模式去响应我们的请求。
|
||||||
|
|
||||||
|
而另一种情况是,请求到达远程节点后很可能不会被马上处理,而是放在了一个队列里面进行缓冲。这对于远程节点的吞吐量改善是有好处的,但是这在一定程度上带来了延迟,从而深刻地影响了交互模式。处理以上问题的方式就是需要引入故障检测(我会在下一讲介绍),来观察远程节点的运行情况,从而针对不同的问题采取不同的应对手段。
|
||||||
|
|
||||||
|
第二种常见的误解是所有节点时间是一致的,这种误解是非常普遍并且危险的。虽然可以使用工具去同步集群内的时间,但是要保持系统内时间一致是非常困难的。而如果我们使用不同节点产生的物理时间来进行一致性计算或排序,那么结果会非常不靠谱。所以大部分分布式数据库会用一个单独的节点来生成全局唯一的逻辑时间以解决上面的问题。而有些分布式数据库,如 Spanner 会使用原子钟这种精密服务来解决时间一致的问题。
|
||||||
|
|
||||||
|
本地物理时间的另一个问题是会产生回溯,也就是获取一个时间并执行若干步骤后,再去获取当前时间,而这个时间有可能比之前的时间还要早。也就是说我们不能认为系统的物理时间是单调递增的,这就是为什么要使用逻辑时间的另一个重要的原因。
|
||||||
|
|
||||||
|
但是本地物理时间在分布式系统中某些部分依然扮演着重要的作用,如判断远程节点超时等。但是基于以上两点,我们在实现分布式算法时应将时间因素考虑进去,从而避免潜在的问题。
|
||||||
|
|
||||||
|
以上谈到的分布式问题集中在节点层面,而另一大类问题就是网络造成的了。其中最为经典的问题就是网络分区,它指的是分布式系统的节点被网络故障分割为不同的小块。而最棘手的是,这些小块内的节点依然可以提供服务。但它们由于不能很好地感知彼此的存在,会产生不一致的问题,这个我们在模块一“”有过比较详细的论述。
|
||||||
|
|
||||||
|
这里需要注意的是,网络分区带来的问题难以解决,因为它是非常难发现的。这是由于网络环境复杂的拓扑和参与者众多共同左右而导致的。故我们需要设计复杂的算法,并使用诸如混沌工程的方式来解决此类问题。
|
||||||
|
|
||||||
|
最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。
|
||||||
|
|
||||||
|
解决级联故障的方式有退避算法和断路。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。
|
||||||
|
|
||||||
|
以上就是分布式系统比较常见的故障。虽然你可能会觉得这些故障很直观,但是如果要去解决它们思路会比较分散。还好前人已经帮我们总结了一些模型来对这些故障进行分级,从而有的放矢地解决这些问题。接下来我就要为你介绍三种典型的失败模型。
|
||||||
|
|
||||||
|
崩溃失败
|
||||||
|
|
||||||
|
当遭遇故障后,进程完全停止工作被称为崩溃失败。这是最简单的一种失败情况,同时结果也非常好预测。这种失败模式也称为崩溃停止失败,特别强调失败节点不需要再参与回分布式系统内部了。我们说这种模式是最容易预测的,是因为失败节点退出后,其他节点感知到之后可以继续提供服务,而不用考虑它重新回归所带来的复杂问题。
|
||||||
|
|
||||||
|
虽然失败停止模式有以上的优点,但实际的分布式系统很少会采用。因为它非常明显地会造成资源浪费,所以我们一般采用崩溃恢复模式,从而重复利用资源。提到崩溃节点恢复,一般都会想到将崩溃节点进行重启,而后经过一定的恢复步骤再加入网络中。虽然这是一种主流模式,但其实通过数据复制从而生成备份节点,而后进行快速热切换才是最为主流的模式。
|
||||||
|
|
||||||
|
崩溃失败可以被认为是遗漏失败的一种特殊情况。因为从其他节点看,他们很难分清一个节点服务响应是由于崩溃还是由于遗漏消息而产生的。那究竟什么是遗漏失败呢?
|
||||||
|
|
||||||
|
遗漏失败
|
||||||
|
|
||||||
|
遗漏失败相比于崩溃失败来说更为不可预测,这种模式强调的是消息有没有被远程节点所执行。
|
||||||
|
|
||||||
|
这其中的故障可能发生在:
|
||||||
|
|
||||||
|
|
||||||
|
消息发送后没有送达远程节点;
|
||||||
|
远程节点跳过消息的处理或根本无法执行(一种特例就是崩溃失败,节点无法处理消息);
|
||||||
|
后者处理的结果无法发送给其他节点。
|
||||||
|
|
||||||
|
|
||||||
|
总之,从其他节点的角度看,发送给该节点的消息石沉大海,没有任何响应了。
|
||||||
|
|
||||||
|
上文提到的网络分区是遗漏失败的典型案例,其中一部分节点间消息是能正常收发的,但是部分节点之间消息发送存在困难。而如果崩溃失败出现,集群中所有节点都将无法与其进行通讯。
|
||||||
|
|
||||||
|
另一种典型情况就是一个节点的处理速度远远慢于系统的平均水平,从而导致它的数据总是旧的,而此时它没有崩溃,依然会将这些旧数据发送给集群内的其他节点。
|
||||||
|
|
||||||
|
当远程节点遗漏消息时,我们是可以通过重发等可靠连接手段来缓解该问题的。但是如果最终还是无法将消息传递出去,同时当前节点依然在继续提供服务,那么此时遗漏失败才会产生。除了以上两种产生该失败的场景,遗漏失败还会发生在网络过载、消息队列满等场景中。
|
||||||
|
|
||||||
|
下面为你介绍最后一种失败模型,即拜占庭失败。
|
||||||
|
|
||||||
|
拜占庭失败
|
||||||
|
|
||||||
|
拜占庭失败又称为任意失败,它相比于上述两种失败是最不好预测的。所谓任意失败是,参与的节点对请求产生不一致的响应,一个说当前数据是 A,而另一个却说它是 B。
|
||||||
|
|
||||||
|
这个故障往往是程序 Bug 导致的,可以通过严格软件开发流程管理来尽可能规避。但我们都清楚,Bug 在生产系统中是很难避免的,特别是系统版本差异带来的问题是极其常见的。故在运行态,一部分系统并不信任直接从远程节点获得的数据,而是采用交叉检测的方式来尽可能得到正确的结果。
|
||||||
|
|
||||||
|
另一种任意失败是一些节点故意发送错误消息,目的是想破坏系统的正常运行,从而牟利。采用区块链技术的数字货币系统则是使用正面奖励的模式(BFT),来保证系统内大部分节点不“作恶”(做正确事的收益明显高于作恶)。
|
||||||
|
|
||||||
|
以上就是三种比较常见的失败模型。模块三的绝大部分内容主要是面向崩溃恢复的场景的。那么下面我们来梳理一下本模块接下来内容的讲解脉络。
|
||||||
|
|
||||||
|
错误侦测与领导选举
|
||||||
|
|
||||||
|
要想解决失败问题,首先就是要进行侦测。在本模块的开始部分,我们会研究使用什么手段来发现系统中的故障。目前,业界有众多方式来检测故障的产生,他们是在易用性、精确性和性能之间做平衡。
|
||||||
|
|
||||||
|
而错误侦测一个重要应用领域就是领导选举。使用错误侦测技术来检测领导节点的健康状态,从而决定是否选择一个新节点来替代已经故障的领导节点。领导节点的一个主要作用就是缓解系统发生失败的可能。我们知道系统中如果进行对等同步状态的代价是很高昂的,如果能选择一个领导节点来统一进行协调,那么会大大降低系统负载,从而避免一些失败的产生。
|
||||||
|
|
||||||
|
而一旦侦测到失败的产生,如何解决它就是我们需要考虑的内容啦。
|
||||||
|
|
||||||
|
复制与一致性
|
||||||
|
|
||||||
|
故障容忍系统(Fault-tolerant)一般使用复制技术产生多个副本,来提供系统的可用性。这样可以保证当系统总部分节点发生故障后,仍然可以提供正常响应。而多个副本会产生数据同步的需求,一致性就是保证数据同步的前提。就像我在模块一中描述的那样,没有复制技术,一致性与同步就根本不存在。
|
||||||
|
|
||||||
|
模块一我们讨论的是 CAP 理论和强一致性模型,它们都是数据一致的范畴。本模块我们会接着讨论客户端一致,或称为会话一致。同时会讨论最终一致这种弱一致模型,最终一致模型允许系统中存在状态不一致的情况,但我们希望尽可能使系统保持一致,这时候会引入反熵手段来解决副本之间不一致的问题。
|
||||||
|
|
||||||
|
而后我们会接着讨论分布式事务,它与一致性存在着联系但又有很明显的区别。同时相比于模块二中的经典事务,分布式事务由于需要解决上文表述的各种失败情况,其处理是比较特殊的,比如需要进行事务协调来处理脑裂问题。
|
||||||
|
|
||||||
|
共识
|
||||||
|
|
||||||
|
最后我们将介绍分布式系统的精华:共识算法。以上介绍的很多内容,包括错误侦测、领导选举、一致性和分布式事务都涵盖在共识算法内,它是现代分布式数据库重要的组件。
|
||||||
|
|
||||||
|
共识算法是为了解决拜占庭将军问题而产生的。简单来说,在从前,拜占庭将军问题被认为是一个逻辑上的困境,它说明了一群拜占庭将军在试图就下一步行动达成统一意见时,可能存在的沟通问题。
|
||||||
|
|
||||||
|
该困境假设每个将军都有自己的军队,每支军队都位于他们打算攻击的城市周围的不同位置,这些将军需要就攻击或撤退达成一致。只要所有将军达成共识,即协调后决定共同执行,无论是攻击还是撤退都无关紧要。
|
||||||
|
|
||||||
|
基于著名的 FLP 不可能问题的研究,拜占庭将军们面临三种困境:
|
||||||
|
|
||||||
|
|
||||||
|
将军们没有统一的时间(没法对表);
|
||||||
|
无法知道别的将军是否被击败;
|
||||||
|
将军们之间的通讯是完全异步的。
|
||||||
|
|
||||||
|
|
||||||
|
由于以上的困境,我们是没有任何办法使将军们最终在特定时间内达成一致性意见的,也就是说共识算法在上述困境下是完全不可能的。
|
||||||
|
|
||||||
|
但是共识算法使用逻辑时钟来提供统一时间,并引入错误侦测技术来确定参与节点的情况,从而在完全异步的通讯情况下可以实现分布式系统的共识。本模块最后一部分,我会介绍几种经典的共识算法,并介绍它们的使用案例。
|
||||||
|
|
||||||
|
共识可以解决遗漏失败,因为只要系统内大部分节点达成共识,剩下的节点即使遗漏该消息,也能对外提供正确的数据。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
这一讲是模块三的引导课,我首先为你介绍了失败模型的概念,它是描述分布式数据库内各种可能行为的一个准则;而后根据失败模型为你梳理了本模块的讲解思路。
|
||||||
|
|
||||||
|
分布式算法根据目标不同可能分为下面几种行为模式,这些模式与对应的课时如下表所示。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 协议将是你绝佳的选择。
|
||||||
|
|
||||||
|
至此我们可以说,所有针对分布式系统复制与一致性的问题都已经介绍完了。下一讲我们将进入分布式数据最核心的领域:分布式事务。希望准时与你相见,谢谢。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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,并没有读取到已经提交的最新数据 。
|
||||||
|
|
||||||
|
那么并发提交同一条数据呢?由于没有锁的存在,会出现写入冲突,通常只有其中的一个事务可以提交数据。这种特性被称为首先提交获胜机制。
|
||||||
|
|
||||||
|
快照隔离与序列化之间的区别是前者不能解决写偏序的问题,也就是并发事务操作的数据集不相交,当事务提交后,不能保证数据集的结果一致性。举个例子,对于两个事务 T1:b=a+1 和 T2:a=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 列是空的,数据列值为 3(10-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讲吃透分布式数据库
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 在读取、会话内事务中的优势不可代替。从它们的原理看,谁最终也无法胜出。而我们其实也不期待一个最终赢家,而是希望未来的事务模型能够从这两个模式中吸取灵感,为我们带来更高效的分布式事务解决方案 。
|
||||||
|
|
||||||
|
到此,我们用了两讲的内容,详细介绍了面向数据库的分布式事务。下一讲要说的是模块三的最后一个知识点:共识算法。它是现代分布式系统的核心算法,希望到时和你准时相见。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 Broadcast(ZAB)。
|
||||||
|
|
||||||
|
ZAB
|
||||||
|
|
||||||
|
ZAB 协议由于 Zookeeper 的广泛使用变得非常流行。它是一种原子广播协议,可以保证消息顺序的传递,且消息广播时的原子性保障了消息的一致性。
|
||||||
|
|
||||||
|
ZAB 协议中,节点的角色有两种。
|
||||||
|
|
||||||
|
|
||||||
|
领导节点。领导是一个临时角色,它是有任期的。这么做的目的是保证领导角色的活性。领导节点控制着算法执行的过程,广播消息并保证消息是按顺序传播的。读写操作都要经过它,从而保证操作的都是最新的数据。如果一个客户端连接的不是领导节点,它发送的消息也会转发到领导节点中。
|
||||||
|
跟随节点。主要作用是接受领导发送的消息,并检测领导的健康状态。
|
||||||
|
|
||||||
|
|
||||||
|
既然需要有领导节点产生,我们就需要领导选举算法。这里我们要明确两个 ID:数据 ID 与节点 ID。前者可以看作消息的时间戳,后者是节点的优先级。选举的原则是:在同一任职周期内,节点的数据 ID 越大,表示该节点的数据越新,数据 ID 最大的节点优先被投票。所有节点的数据 ID 都相同,则节点 ID 最大的节点优先被投票。当一个节点的得票数超过节点半数,则该节点成为主节点。
|
||||||
|
|
||||||
|
一旦领导节点选举出来,它就需要做两件事。
|
||||||
|
|
||||||
|
|
||||||
|
声明任期。领导节点通知所有的跟随节点当前的最新任期;而后由跟随节点确认当前任期是最新的任期,从而同步所有节点的状态。通过该过程,老任期的消息就不会被跟随节点所接受了。
|
||||||
|
同步状态。这一步很关键,首先领导节点会通知所有跟随节点自己的领导身份,而后跟随节点不会再选举自己为领导了;然后领导节点会同步集群内的消息历史,保证最新的消息在所有节点中同步。因为新选举的领导节点很可能并没有最新被接受的数据,因此同步历史数据操作是很有必要的。
|
||||||
|
|
||||||
|
|
||||||
|
经过以上的初始化动作后,领导节点就可以正常接受消息,进行消息排序而后广播消息了。在广播消息的时候,需要 Quorum(集群中大多数的节点)的节点返回已经接受的消息才认为消息被正确广播了。同时为了保证顺序,需要前一个消息正常广播,后一个消息才能进行广播。
|
||||||
|
|
||||||
|
领导节点与跟随节点使用心跳算法检测彼此的健康情况。如果领导节点发现自己与 Quorum 节点们失去联系,比如网络分区,此时领导节点会主动下台,开始新一轮选举。同理,当跟随节点检测到领导节点延迟过大,也会触发新一轮选举。
|
||||||
|
|
||||||
|
ZAB 选举的优势是,如果领导节点一直健康,即使当前任期过期,选举后原领导节点还会承担领导角色,而不会触发领导节点切换,这保证了该算法的稳定。另外,它的节点恢复比较高效,通过比较各个节点的消息 ID,找到最大的消息 ID,就可以从上面恢复最新的数据了。最后,它的消息广播可以理解为没有投票过程的两阶段提交,只需要两轮消息就可以将消息广播出去。
|
||||||
|
|
||||||
|
那么原子广播协议与本讲重点介绍的共识算法是什么关系呢?这里我先留下一个“暗扣”,先介绍一下典型的共识算法 Paxos,而后再说明它们之间的关系。
|
||||||
|
|
||||||
|
Paxos
|
||||||
|
|
||||||
|
所谓的 Paxos 算法,是为了解决来自客户端的值被发送到集群中的任意一点,而后集群中的所有节点为该值达成共识的一种协调算法。同时这个值伴随一个版本号,可以保证消息是有顺序的,该顺序在集群中任何一点都是一致的。
|
||||||
|
|
||||||
|
基本的 Paxos 算法非常简单,它由三个角色组成。
|
||||||
|
|
||||||
|
|
||||||
|
Proposer:Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,可以是任何操作,比如“设置某个变量的值为 value”。不同的 Proposer 可以提出不同的 value。但对同一轮 Paxos 过程,最多只有一个 value 被批准。
|
||||||
|
Acceptor:Acceptor 有 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 较为易懂,且目前有比较成熟的实现,所以我认为算法细节并不是本讲的重点。理解它们为什么如此,才能帮助我们理解数据库的选择依据。
|
||||||
|
|
||||||
|
到此,我们学习完了这个模块的所有知识点。下一讲我将会带领你复习这一模块的内容,同时通过几个案例来展示典型分布式数据库特性与咱们所学的知识点之间的关系,到时候见。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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,以及是否需要进行异构处理。
|
||||||
|
|
||||||
|
|
||||||
|
核心的、高并发访问的不同数据,需要分别分拆到独立的缓存池中,进行分别访问,避免相互影响;访问量较小、非核心的业务数据,则可以混存。
|
||||||
|
对海量数据、访问超过 10~100万 级的业务数据,要考虑分层访问,并且要分摊访问量,避免缓存过载。
|
||||||
|
如果业务系统需要多 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 带上时间戳,同时设置较长的过期时间,比如很多业务系统内部有这样一些 key:key_20190801。
|
||||||
|
|
||||||
|
|
||||||
|
平均缓存穿透加载时间
|
||||||
|
|
||||||
|
平均缓存穿透加载时间在某些业务场景下也很重要,对于一些缓存穿透后,加载时间特别长或者需要复杂计算的数据,而且访问量还比较大的业务数据,要配置更多容量,维持更高的命中率,从而减少穿透到 DB 的概率,来确保整个系统的访问性能。
|
||||||
|
|
||||||
|
缓存可运维性
|
||||||
|
|
||||||
|
对于缓存的可运维性考虑,则需要考虑缓存体系的集群管理,如何进行一键扩缩容,如何进行缓存组件的升级和变更,如何快速发现并定位问题,如何持续监控报警,最好有一个完善的运维平台,将各种运维工具进行集成。
|
||||||
|
|
||||||
|
缓存安全性
|
||||||
|
|
||||||
|
对于缓存的安全性考虑,一方面可以限制来源 IP,只允许内网访问,同时对于一些关键性指令,需要增加访问权限,避免被攻击或误操作时,导致重大后果。
|
||||||
|
|
||||||
|
好了,第3课时的内容到这里就全部结束了,我们一起来做一个简单的回顾。首先,我们学习了在系统研发中,如何引入缓存,如何按照4步走对缓存进行设计架构及管理。最后,还熟悉了缓存设计架构中的考量点,这样你在缓存设计架构时对号入座即可。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
04 缓存失效、穿透和雪崩问题怎么处理?
|
||||||
|
你好,我是你的缓存老师陈波,欢迎进入第 4 课时“缓存访问相关的经典问题”。
|
||||||
|
|
||||||
|
前面讲解了缓存的原理、引入,以及设计架构,总结了缓存在使用及设计架构过程中的很多套路和关键考量点。实际上,在缓存系统的设计架构中,还有很多坑,很多的明枪暗箭,如果设计不当会导致很多严重的后果。设计不当,轻则请求变慢、性能降低,重则会数据不一致、系统可用性降低,甚至会导致缓存雪崩,整个系统无法对外提供服务。
|
||||||
|
|
||||||
|
接下来将对缓存设计中的 7 大经典问题,如下图,进行问题描述、原因分析,并给出日常研发中,可能会出现该问题的业务场景,最后给出这些经典问题的解决方案。本课时首先学习缓存失效、缓存穿透与缓存雪崩。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
缓存失效
|
||||||
|
|
||||||
|
问题描述
|
||||||
|
|
||||||
|
缓存第一个经典问题是缓存失效。上一课时讲到,服务系统查数据,首先会查缓存,如果缓存数据不存在,就进一步查 DB,最后查到数据后回种到缓存并返回。缓存的性能比 DB 高 50~100 倍以上,所以我们希望数据查询尽可能命中缓存,这样系统负荷最小,性能最佳。缓存里的数据存储基本上都是以 key 为索引进行存储和获取的。业务访问时,如果大量的 key 同时过期,很多缓存数据访问都会 miss,进而穿透到 DB,DB 的压力就会明显上升,由于 DB 的性能较差,只在缓存的 1%~2% 以下,这样请求的慢查率会明显上升。这就是缓存失效的问题。
|
||||||
|
|
||||||
|
原因分析
|
||||||
|
|
||||||
|
导致缓存失效,特别是很多 key 一起失效的原因,跟我们日常写缓存的过期时间息息相关。
|
||||||
|
|
||||||
|
在写缓存时,我们一般会根据业务的访问特点,给每种业务数据预置一个过期时间,在写缓存时把这个过期时间带上,让缓存数据在这个固定的过期时间后被淘汰。一般情况下,因为缓存数据是逐步写入的,所以也是逐步过期被淘汰的。但在某些场景,一大批数据会被系统主动或被动从 DB 批量加载,然后写入缓存。这些数据写入缓存时,由于使用相同的过期时间,在经历这个过期时间之后,这批数据就会一起到期,从而被缓存淘汰。此时,对这批数据的所有请求,都会出现缓存失效,从而都穿透到 DB,DB 由于查询量太大,就很容易压力大增,请求变慢。
|
||||||
|
|
||||||
|
业务场景
|
||||||
|
|
||||||
|
很多业务场景,稍不注意,就出现大量的缓存失效,进而导致系统 DB 压力大、请求变慢的情况。比如同一批火车票、飞机票,当可以售卖时,系统会一次性加载到缓存,如果缓存写入时,过期时间按照预先设置的过期值,那过期时间到期后,系统就会因缓存失效出现变慢的问题。类似的业务场景还有很多,比如微博业务,会有后台离线系统,持续计算热门微博,每当计算结束,会将这批热门微博批量写入对应的缓存。还比如,很多业务,在部署新 IDC 或新业务上线时,会进行缓存预热,也会一次性加载大批热数据。
|
||||||
|
|
||||||
|
解决方案
|
||||||
|
|
||||||
|
对于批量 key 缓存失效的问题,原因既然是预置的固定过期时间,那解决方案也从这里入手。设计缓存的过期时间时,使用公式:过期时间=baes 时间+随机时间。即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对 DB 造成过大压力,如下图所示。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
缓存穿透
|
||||||
|
|
||||||
|
问题描述
|
||||||
|
|
||||||
|
第二个经典问题是缓存穿透。缓存穿透是一个很有意思的问题。因为缓存穿透发生的概率很低,所以一般很难被发现。但是,一旦你发现了,而且量还不小,你可能立即就会经历一个忙碌的夜晚。因为对于正常访问,访问的数据即便不在缓存,也可以通过 DB 加载回种到缓存。而缓存穿透,则意味着有特殊访客在查询一个不存在的 key,导致每次查询都会穿透到 DB,如果这个特殊访客再控制一批肉鸡机器,持续访问你系统里不存在的 key,就会对 DB 产生很大的压力,从而影响正常服务。
|
||||||
|
|
||||||
|
原因分析
|
||||||
|
|
||||||
|
缓存穿透存在的原因,就是因为我们在系统设计时,更多考虑的是正常访问路径,对特殊访问路径、异常访问路径考虑相对欠缺。
|
||||||
|
|
||||||
|
缓存访问设计的正常路径,是先访问 cache,cache miss 后查 DB,DB 查询到结果后,回种缓存返回。这对于正常的 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 比较集中,正好在某 1~2 个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常 Crash,然后这些异常节点下线,这些大流量 key 请求又被 rehash 到其他缓存节点,进而导致其他缓存节点也被过载 Crash,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。
|
||||||
|
|
||||||
|
业务场景
|
||||||
|
|
||||||
|
缓存雪崩的业务场景并不少见,微博、Twitter 等系统在运行的最初若干年都遇到过很多次。比如,微博最初很多业务缓存采用一致性 Hash+rehash 策略,在突发洪水流量来临时,部分缓存节点过载 Crash 甚至宕机,然后这些异常节点的请求转到其他缓存节点,又导致其他缓存节点过载异常,最终整个缓存池过载。另外,机架断电,导致业务缓存多个节点宕机,大量请求直接打到 DB,也导致 DB 过载而阻塞,整个系统异常。最后缓存机器复电后,DB 重启,数据逐步加热后,系统才逐步恢复正常。
|
||||||
|
|
||||||
|
解决方案
|
||||||
|
|
||||||
|
预防缓存雪崩,这里给出 3 个解决方案。
|
||||||
|
|
||||||
|
|
||||||
|
方案一,对业务 DB 的访问增加读写开关,当发现 DB 请求变慢、阻塞,慢请求超过阀值时,就会关闭读开关,部分或所有读 DB 的请求进行 failfast 立即返回,待 DB 恢复后再打开读开关,如下图。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
方案二,对缓存增加多个副本,缓存异常或请求 miss 后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。
|
||||||
|
方案三,对缓存体系进行实时监控,当请求访问的慢速比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。
|
||||||
|
|
||||||
|
|
||||||
|
实际上,微博平台系统,这三种方案都采用了,通过三管齐下,规避缓存雪崩的发生。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 回种到缓存后,再从缓存获取。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
方案二是,对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况,如下图。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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字节。当然对于 key,Mc 有个默认的最大长度,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 的系统架构、多线程网络模型、状态机处理进行逻辑打通。
|
||||||
|
|
||||||
|
为了方便理解,提供本课时所有知识点的思维导图,如下图所示。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 对应的哈希桶在迁移位置之前,则到新的主哈希表进行查询或插入,否则到旧哈希表进行查询和插入。待全部扩容迁移完毕,所有的处理就会全部在新的主哈希表进行。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 数为 0,LRU 维护线程会对 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 次的大循环就立即停止。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 的访问 miss,N 是 Mc 资源池的节点数。
|
||||||
|
|
||||||
|
因此,对于单层 Mc 缓存架构,一致性 hash 分布配合 rehash 策略,是一个更佳的方案。通过将业务数据分拆到独立 Mc 资源池,同时在每个资源池采用合适的分布算法,可以很好的解决 Mc 使用中容量问题、性能瓶颈问题,以及连接瓶颈问题。
|
||||||
|
|
||||||
|
Master-Slave 两级架构
|
||||||
|
|
||||||
|
在系统的访问量比较大,比如峰值 QPS 达到 20w 以上时,如果缓存节点故障,即便采用一致性 hash,也会在一段时间内给 DB 造成足够大的压力,导致大量慢查询和访问超时的问题。另外,如果某些缓存服务器短期多次故障,反复上下线,多次 rehash 还会产生脏数据。对此,可以采用 Master-Slave 的两级架构方案。
|
||||||
|
|
||||||
|
在这种架构方案下,将业务正常访问的 Memcached 缓存池作为 master,然后在 master 之后,再加一个slave 资源池作 master 的热备份。slave 资源池也用 6~8 个节点,内存设置只用 master 的 1⁄2~1⁄3 即可。因为 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世纪初,意大利统计学家帕累托提出来一个观点:在任何特定群体中,重要的因子通常只占少数,而不重要的因子则占多数,因此只要能控制具有重要性的少数因子,即能控制全局。这个理论经过多年演化,就成为当前大家所熟悉的 80⁄20 定律。80/20 定律在互联网系统中也广泛存在,如 80% 的用户访问会集中在系统 20% 的功能上,80% 的请求会集中在 20% 的数据上。因此,互联网系统的数据,有明显的冷热区分,而且这个冷热程度往往比 80⁄20 更大,比如微博、微信最近一天的数据,被访问的特别频繁,而一周前的数据就很少被访问了。而且最近几天的热数据中,部分 feed 信息会被大量传播和交互,比其他 大部分数据的访问量要高很多倍,形成明显的头部请求。
|
||||||
|
|
||||||
|
头部请求,会导致日常大量访问,被集中在其中一小部分 key 上。同时,在突发新闻、重大事件发生时,请求量短期增加 50~70% 以上,而这些请求,又集中在 突发事件的关联 key 上,造就大量的热 key 的出现。热 key 具有随机性,如果集中在某少数几个节点,就会导致这 些节点的压力陡增数倍,负荷严重过载,进而引发大量查询变慢超时的问题。
|
||||||
|
|
||||||
|
为了应对日常峰值的热数据访问,特别是在应对突发事件时,洪峰流量带来的极热数据访问,我们可以通过增加 L1 层来解决。如下图所示,L1 层包含 2~6 组 L1 资源池,每个 L1 资源池,用 4~6 个节点,但内存容量只要 Master 的 1⁄10 左右即可。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
如图,读请求时,首先随机选择一个 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% 以上的请求数据。这也与 80⁄20 原则相符合。
|
||||||
|
|
||||||
|
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 体系,来更好得解决突发洪峰流量和局部故障的问题。
|
||||||
|
|
||||||
|
可以参考下面的思维导图,对这些知识点进行回顾和梳理。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 具有更好的性能、可用性和可运维性。
|
||||||
|
|
||||||
|
可以参考下面的思维导图,对这些知识点进行回顾和梳理。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 数小于哈希桶的 1⁄10 时,就对 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 的各个技术细节。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 数据类型来进行处理。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
encoding:Redis 对象的内部编码方式,即内部数据结构类型,目前支持 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 的起始元素的指针是 p,zltail 是 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 型整数存储,这些场景比较特殊,限于时间关系,这里不做进一步阐述。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 个 key,N 默认是 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 的访问没有明显热点,被随机访问,即可采用这种策略。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 格式兼容性和可读性较差。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 从配置中心获取配置信息,并持续订阅服务状态的变化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 经典理论并没有给出解决方案,开发者如果简单进行三选二,就会进入误区,导致系统在运行中问题连连。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 消息队列方式进行缓存更新。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
32 一个典型的分布式缓存系统是什么样的?
|
||||||
|
本课时我们具体看下一个典型的分布式缓存系统是什么样的。
|
||||||
|
|
||||||
|
分布式 Redis 服务
|
||||||
|
|
||||||
|
由于本课程聚焦于缓存,接下来,我将以微博内的 分布式 Redis 服务系统为例,介绍一个典型的分布式缓存系统的组成。
|
||||||
|
|
||||||
|
微博的 Redis 服务内部也称为 RedisService。RedisService 的整体架构如图所示。主要分为Proxy、存储、集群管理、配置中心、Graphite,5 个部分。
|
||||||
|
|
||||||
|
|
||||||
|
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,然后发送给 Proxy,Proxy 解析 key 前缀确定 key 对应的业务。
|
||||||
|
|
||||||
|
方案 2,对每个业务分配一个业务端口,不同业务访问自己的端口,Proxy 会根据端口确定业务类型。这种类型不需要解析 key 前缀,不需要重构请求,性能更为高效。但需要为业务配置端口,增加管理成本,实践上,由于业务 Redis 资源一般会采用不同端口,所以业务 Proxy 可以采用业务资源分片的最小端口来作为业务端口标志。
|
||||||
|
|
||||||
|
Redis 数据存储
|
||||||
|
|
||||||
|
RedisService 中的 Redis 存储基于 Redis 5.0 扩展,内部称 wredis,wredis 不存储 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 作存储的业务场景,由于缓存存放全量数据,直接采用主从复制进行数据一致性保障,这样最便捷。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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写入Redis,Redis 收到后直接 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 池的 1⁄2 以下,主要解决 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 配置及部署等。这个架构在之前的经典分布式缓存系统课程中有详细介绍,此处不再赘述。
|
||||||
|
|
||||||
|
至此,本专栏的全部内容就讲完了,希望你可以在项目中结合所学的知识,融会贯通,也感谢你对本专栏的支持,谢谢。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
000 开篇词 你的360度人工智能信息助理
|
||||||
|
你好,我是洪亮劼,目前在电子商务网站Etsy任数据科学主管,很高兴能和你在这里相识,也很期待在接下来的时间里,通过“AI技术内参”这个专栏和你共同探讨与人工智能有关的话题。
|
||||||
|
|
||||||
|
在未来的一年里,我会为你讲解人工智能的核心基础,介绍顶级学术会议的最新研究成果,为广大工程师和数据科学家的个人成长出谋划策,也期待能为计划成立和管理数据科学家团队的工程管理领导提供一些意见和建议。我希望“AI技术内参”能够成为你的360度人工智能信息助理,帮助你在这个高速发展的领域稳步前行。
|
||||||
|
|
||||||
|
那么,我为什么愿意来写这么一个专栏呢?这让我想起了最近发生的一个片段,我在波士顿参加一个有关数据科学的工业界会议,会场上,一位目前在美国一家时装品牌J.Crew工作的数据科学家和我聊天,中间问我,如果想在数据科学、人工智能这个领域进阶,需要看什么样的资料、需要怎样才能不断学习和进步。我当时发现,自己很难为这位数据科学家推荐某一本书、某几篇论文、某一个资料,就能够起到这样的作用。
|
||||||
|
|
||||||
|
事实上,回想我自己在人工智能这个领域的成长,一个突出的特点就是,需要学习的东西太多、太杂而且很细。
|
||||||
|
|
||||||
|
比方说,对于一个人工智能领域的从业人员来说,基础阶段需要系统地学习有关机器学习、概率统计的很多书,还要会使用相关的专业软件以及人工智能框架,然后如果你希望能够在某一个专业领域(比如搜索、推荐、图像技术、语音技术、智能驾驶等)有所发展,还需要阅读这些相关领域的很多技术论文,并且去实践相关的算法模型。
|
||||||
|
|
||||||
|
更进一步,要想在技术公司能够真正成长下去,还有很多的工程技巧以及实际经验需要你慢慢习得。这些情况都导致人工智能领域专业人才的培养和成长有很高的门槛。
|
||||||
|
|
||||||
|
我自己,以及很多希望能够在这个领域有所发展的朋友,都很急迫地需要有这么一个集中地、有计划地获取信息,获取高质量信息的平台。这让我萌生了自己来写这么一个专栏的想法。
|
||||||
|
|
||||||
|
我希望“AI技术内参”这个专栏能够成为你在人工智能领域成长的灯塔,当你在茫茫的知识海洋里航行时,帮助你快速找到核心的、主干的信息和资源。我希望这个专栏能够成为你在职业发展上的朋友,让你对快速发展的行业不再焦虑,不再担心自己的知识会落伍,不再为如何在日新月异的信息中寻找有价值的学习资料而发愁。
|
||||||
|
|
||||||
|
同时,我也希望这个专栏能够为你拓宽视野,让数据科学家、人工智能工程师了解到团队管理者是如何构建一个团队、如何来招聘从业人士的,让数据科学的领导者意识到如何培养数据科学家成长,让你对整个行业的生态系统有一个更加完整的认识。
|
||||||
|
|
||||||
|
我为这个专栏精心打磨了三个模块。
|
||||||
|
|
||||||
|
第一,我会为你讲解一些经典的人工智能技术。这些技术涵盖搜索、推荐系统、广告系统、图像处理等领域。了解这些经典技术能够让你迅速入门并能为今后的学习打下基础。这部分内容帮助你分析核心的算法模型,并为你进行系统性学习提供纲要和指引。
|
||||||
|
|
||||||
|
第二,我会带给你最新的顶级学术会议动态,帮助你了解和掌握这些学术会议最火热和最新的研究成果。每一年和人工智能相关的顶级学术会议有十余个,每个会议都会有上百篇甚至几百篇论文发表。从这些论文和成果中找到有价值的信息,对于初学者,甚至是有一定经验的从业人员来说都是非常困难、也非常耗时的一件事情。那么,在这个专栏里,我会为你精选内容,可以让你不错过任何有价值的最新成果。
|
||||||
|
|
||||||
|
第三,我会在这个专栏里为人工智能的从业人员提供指南,帮助数据科学家和工程师提升自我价值,帮助人工智能团队的管理者构建团队,为你在职场发展中的关键步骤出谋划策。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
希望我们在今后的一年时间里,通过“AI技术内参”这个平台,共同学习、共同成长。“AI技术内参”只是一个起点,希望你能够从这个专栏出发,在人工智能这个领域前行得更好、更高、更远。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
001 聊聊2017年KDD大会的时间检验奖
|
||||||
|
国际数据挖掘与知识发现大会ACM SIGKDD(ACM SIGKDD Conference on Knowledge Discovery and Data Mining),简称KDD,是由美国计算机协会ACM(The Association for Computing Machinery)的数据挖掘与知识发现专委会SIGKDD(Special 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
004 精读2017年EMNLP最佳长论文之一
|
||||||
|
自然语言处理实证方法会议EMNLP(Conference on Empirical Methods in Natural Language Processing),是由国际计算语言学协会ACL(Association for Computational Linguistics)的专委会SIGDAT(Special 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.89,F1值达到0.61,都远远高于其他方法。
|
||||||
|
|
||||||
|
应该说,从可观的数值上,本文的方法效果不错。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了EMNLP 2017年的第二篇年度最佳长论文,这篇文章介绍了一个采用深度学习模型对论坛文本信息进行分析的应用,那就是如何识别有精神疾病的用户的信息。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我简要介绍了这篇文章的作者群信息。第二,这篇文章是利用自然语言处理技术解决一个社会问题的应用,论文构建的数据集很有价值。第三,文章把目前图像处理的标准卷积层应用到文本信息上,提出了基于卷积神经网络的文本分析框架,用于辨别抑郁症和检测自残倾向,都实现了不错的效果。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,如果说在图像信息上采用卷积层是有意义的,那为什么同样的操作对于文本信息也是有效的呢?文本上的卷积操作又有什么物理含义呢?
|
||||||
|
|
||||||
|
拓展阅读:Depression and Self-Harm Risk Assessment in Online Forums
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 2017,International Conference on Machine Learning ,国际机器学习大会。
|
||||||
|
|
||||||
|
IJCAI 2017, International Joint Conference on Artificial Intelligence,人工智能国际联合大会。
|
||||||
|
|
||||||
|
CVPR 2017,Conference on Computer Vision and Pattern Recognition,国际计算机视觉与模式识别会议。
|
||||||
|
|
||||||
|
ICCV 2017,International Conference on Computer Vision,国际计算机视觉大会。
|
||||||
|
|
||||||
|
NIPS 2017,Annual Conference on Neural Information Processing Systems,神经信息处理系统大会。
|
||||||
|
|
||||||
|
拓展阅读:Natural Language Does Not Merge ‘Naturally’ in Multi-Agent Dialog
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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方法,可以不依赖于目标分布的样本,并且还能达到线性计算速度。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息。第二,我们详细介绍了这篇文章要解决的问题以及贡献 。第三,我们简要地介绍了文章的实验结果 。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,这种衡量分布之间距离的想法,除了在假设检验中使用以外,在机器学习的哪个环节也经常碰到?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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的第三篇最佳研究论文,文章的一个核心观点是希望能够通过构建蓝图来引导我们解决非完美信息博弈的问题,特别是在扑克上面的应用。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息。第二,我们详细介绍了这篇文章要解决的问题以及贡献 。第三,我们简要地介绍了文章的实验结果 。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,为什么非完美博弈的整个问题求解现在并没有依靠*深度加强学习*呢,大家在这个问题上有什么直观上的体会呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
013 WSDM 2018论文精读:看谷歌团队如何做位置偏差估计
|
||||||
|
WSDM(International 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 Wang)2015年起在谷歌工作。他之前在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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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年的一篇来自京东数据科学团队的文章,这篇文章介绍了如何利用多关系学习以及模糊逻辑来挖掘商品的替代信息和互补信息,然后训练出更加有效的排序算法。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了这篇文章的作者群信息;第二,我们详细介绍了这篇文章要解决的问题以及贡献 ;第三,我们简要地介绍了文章提出方法的核心内容以及实验的结果。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,互补商品或者替代商品是双向关系还是单向关系,为什么呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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的最佳论文。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们详细介绍了这篇文章要解决的问题和贡献,探讨偏差和流行度之间的关系,系统性地来讨论偏差在推荐系统中所带来的问题;第二,我们简要地介绍了文章提出方法的核心内容,包括设定随机变量、期望关系以及推导理想状态下的最佳排序;第三,针对论文我们简单进行了讨论。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,在不考虑偏差的情况下,为什么一般的推荐系统会偏好于推荐流行物品的算法呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
024 CVPR 2018论文精读:如何解决排序学习计算复杂度高这个问题?
|
||||||
|
今天,我们来看这次大会的一篇最佳论文提名,标题是《基于排序的损失函数的有效优化》(Efficient Optimization for Rank-based Loss Functions)。
|
||||||
|
|
||||||
|
还是先简单介绍下论文的作者群。这篇论文的作者来自好几个不同的学术机构。
|
||||||
|
|
||||||
|
第一作者普里迪什·莫哈帕德拉(Pritish Mohapatra)是印度海得拉巴的国际信息科技大学(International Institute of Information Technology,Hyderabad)的计算机科学博士生。他已经在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算法,那里其实也有这么一个寻找违反排序原则配对的步骤,你能想起来是什么步骤吗?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
025 ICML 2018论文精读:模型经得起对抗样本的攻击?这或许只是个错觉
|
||||||
|
2018年7月10日~15日,国际机器学习大会ICML 2018(The 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%。最后的这种情况,就是随机寻找对抗样本,发现了比基于梯度下降的攻击要好的对抗样本。
|
||||||
|
|
||||||
|
那么,针对梯度混淆,攻击方有什么办法呢?
|
||||||
|
|
||||||
|
针对扩散梯度,作者们提出了一种叫BPDA(Backward Pass Differentiable Approximation)的方法。如果有兴趣,建议你阅读论文来了解这种算法的细节。总体说来,BPDA就是希望找到神经网络不可微分的地方,利用简单的可微分的函数对其前后进行逼近,从而达到绕过阻碍的目的。
|
||||||
|
|
||||||
|
针对随机梯度,作者们提出了“变换之上的期望”(Expectation over Transformation)这一方法。这个方法的特点是针对随机变化,变换的期望应该还是能够反映真实的梯度信息。于是作者们就让攻击方作用于变换的期望值,从而能够对梯度进行有效的估计。
|
||||||
|
|
||||||
|
针对消失或者爆炸的梯度,作者们提出了“重新参数化”(Reparameterization)这一技术。重新参数化是深度学习中重要的技术。在这里,作者们使用重新参数化,其实就是对变量进行变换,从而使得新的变量不发生梯度消失或者爆炸的情况。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了今年ICML的最佳论文。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,这篇论文讨论了一个比较陌生的主题,我们简要介绍了论文的背景;第二,我们详细介绍了论文提出的三种类型的梯度混淆。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,我们为什么要研究深度学习模型是否健壮,是否能够经得起攻击呢?有什么现实意义吗?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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的另一篇最佳论文。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,这篇论文讨论了计算机算法的公平性问题;第二,我们详细介绍了论文提出的两种策略以及得出的主要结论。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,研究算法的公平性对我们日常的应用型工作有什么启发作用?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
027 ICML 2018论文精读:优化目标函数的时候,有可能放大了不公平?
|
||||||
|
今天我们要分享的是ICML 2018的一篇最佳论文提名,题目是Fairness Without Demographics in Repeated Loss Minimization。
|
||||||
|
|
||||||
|
这篇论文讨论了这样一个话题,在优化目标函数的时候,如何能够做到针对不同的子群体,准确率是相当的,从而避免优化的过程中过分重视多数群体。这篇论文的作者都来自斯坦福大学。
|
||||||
|
|
||||||
|
论文的主要贡献
|
||||||
|
|
||||||
|
这篇论文其实也是希望讨论算法带来的“公平性”问题,但是出发的角度和我们上一篇讨论公平性的论文非常不一样。这篇论文的核心思想,是希望通过机器学习目标函数优化的原理,来讨论机器学习和公平性的关系。
|
||||||
|
|
||||||
|
作者们发现,基于“平均损失”(Average Loss)优化的机器学习算法,常常会给某一些少数群体带来巨大的不准确性。这其实并不是模型本身的问题,而是优化的目标函数的问题。在这样的情况下,目标函数主要是关注有较多数据的群体,保证这些群体的损失最小化,而可能忽略了在数量上不占优势的少数群体。
|
||||||
|
|
||||||
|
在此基础上,还带来了另外一个用户“留存度”(Retention)的问题。因为少数群体忍受了比较大的优化损失,因此这些群体有可能离开或者被这个系统剔除。所以,长期下去,少数群体的数目就可能逐渐变少。这也许是目标函数的设计者们无从想到的一个平均损失函数的副产品。作者们还把这个现象命名为“不公平的放大”(Disparity Amplification)。
|
||||||
|
|
||||||
|
这篇论文的一个重要贡献是发现ERM(Empirical Risk Minimization,经验风险最小化)其实就存在这种不公平的放大性。ERM包含了很多经典的机器学习模型的目标函数,例如支持向量机(Support Vector Machines)、对数回归模型(Logistic Regression)以及线性回归等。作者们还发现,ERM可以让即便在最初看上去公平的模型,在迭代的过程中逐渐倾向于不公平。
|
||||||
|
|
||||||
|
为了解决ERM的问题,作者们开发了一种新的算法框架,DRO(Distributionally 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的思路,确实能够解决小众群体的不公平性问题。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,这两期内容我们从不同的角度讨论了算法的公平性问题,你是否有自己的角度来思考这个问题?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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篇文章来详细剖析这个会议的精髓和一些前沿信息。我希望通过我的眼睛和思考让你看到在这个领域里那些激动人心的发展,收获新知、拓展视野,同时也把我的学习方法分享给你。
|
||||||
|
|
||||||
|
我想你应该已经掌握了我分析论文的套路了,对于每一篇文章,我一定会先去做一些背景研究,了解作者群,了解对应的学术机构或者公司信息;然后弄清楚论文解决了什么问题,核心贡献是什么;再详细研究论文的具体方法。这个方法很简单,就是牢牢抓住一个主线,找到最核心的内容来消化吸收。但是真正让这个方法内化成你的思维模式,还是需要大量的阅读和练习。相信我,如果想在人工智能领域继续深耕,阅读大量论文,一定是一个最值得做的投资,因为回报极大。
|
||||||
|
|
||||||
|
那回到阅读论文本身,最后想跟你分享的一点只有八个字:学好英语,阅读原文。我知道你可能会说我英语还真不好,但是到达能够阅读原文的水平其实也并没那么难。你不妨直接找一篇我们专栏里讲过的论文原文,就把每一段的第一句读一下,看看能否学到东西。先开始看起来,遇到不会的且影响你理解的单词或句子再去查,你的英语水平就已经开始变得越来越好了。
|
||||||
|
|
||||||
|
以上就是我们对论文精读这个模块的一个复盘,希望专栏里的这三十篇论文是一个起点,能够帮助你养成关注国际顶级会议、阅读论文的习惯,拥有这一强大的学习利器,提升自己的学习效率。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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年从哈佛大学获得应用数学博士学位,之后来到康奈尔大学参与组建计算机系。为了致敬杰拉德本人对现代信息检索技术的卓越贡献,现在,美国计算机协会ACM(Association 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应用到中文环境中,是否需要一些预处理的步骤?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
035 机器学习排序算法:配对法排序学习
|
||||||
|
周一的文章里我分享了最基本的单点法排序学习(Pointwise Learning to Rank)。这个思路简单实用,是把经典的信息检索问题转化为机器学习问题的第一个关键步骤。简单回顾一下,我们介绍了在测试集里使用NDCG(Normalized Discounted Cumulative Gain),在某个K的位置评价“精度”(Precision)和“召回”(Recall),以这些形式来评估排序算法。
|
||||||
|
|
||||||
|
你可以看到,单点法排序学习算法的模式和我们最终需要的结果中间还存在明显差距。这个差距并不是算法好坏能够决定的,而是算法所要优化的目标,也就是单个数据点是否相关,和我们的最终目的,一组结果的NDCG排序最优之间的结构化区别。这个结构化区别激发研究者们不断思考,是不是有其他的方法来优化排序算法。
|
||||||
|
|
||||||
|
今天我就来讲从单点法引申出来的“配对法”排序学习(Pairwise Learning to Rank) 。相对于尝试学习每一个样本是否相关,配对法的基本思路是对样本进行两两比较,从比较中学习排序,离真正目标又近了一步。
|
||||||
|
|
||||||
|
配对法排序学习的历史
|
||||||
|
|
||||||
|
当人们意识到用机器学习来对排序进行学习,从文档与文档之间的相对关系入手,也就是配对法,就成了一个非常火热的研究方向。机器学习排序这个领域持续活跃了10多年,在此期间很多配对法排序算法被提出,下面我就说几个非常热门的算法。
|
||||||
|
|
||||||
|
2000年左右,研究人员开始利用支持向量机(SVM)来训练排序算法,来自康奈尔的索斯藤·乔基姆斯(Thorsten Joachims)就构建了基于特征差值的RankSVM,一度成为配对法排序学习的经典算法。索斯藤我们前面讲过,他获得了今年的KDD时间检验奖。
|
||||||
|
|
||||||
|
2005年,当时在雅虎任职的研究人员郑朝晖等人,开始尝试用GBDT(Gradient Boosting Decision Tree,梯度提升决策树)这样的树模型来对文档之间的两两关系进行建模。郑朝晖后来成为一点资讯的联合创始人。
|
||||||
|
|
||||||
|
2005年,微软的学者克里斯·博格斯(Chris Burges)等人,开始使用神经网络训练RankNet文档之间两两关系的排序模型。这是最早使用深度学习模型进行工业级应用的尝试。这篇论文在2015年获得了ICML 2015(International 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-294,2007.
|
||||||
|
Thorsten Joachims. Optimizing search engines using clickthrough data. *Proceedings of the eighth ACM SIGKDD international conference on knowledge discovery and data mining*,133-142,2002.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
036 机器学习排序算法:列表法排序学习
|
||||||
|
本周我们已经分别讨论了最基本的单点法排序学习(Pointwise Learning to Rank)和配对法排序学习(Pairwise Learning to Rank)两种思路。单点法排序学习思路简单实用,目的就是把经典的信息检索问题转化成机器学习问题。配对法排序学习则是把排序的问题转化成针对某个查询关键字每两个文档之间的相对相关性的建模问题。不过,这两种思路也都有很明显的问题,需要进一步对算法进行优化,以实现我们需要的最终目标。
|
||||||
|
|
||||||
|
今天我就来讲直接优化排序问题的“终极方法”:列表法排序学习(Listwise Learning to Rank) 。相对于尝试学习每一个样本是否相关或者两个文档的相对比较关系,列表法排序学习的基本思路是尝试直接优化像NDCG(Normalized Discounted Cumulative Gain)这样的指标,从而能够学习到最佳排序结果。
|
||||||
|
|
||||||
|
列表法排序学习的历史
|
||||||
|
|
||||||
|
2000年后,学术界和工业界都开始研究如何用机器学习来解决最优排序问题,五六年之后,研究者们才开始尝试直接优化整个排序列表。
|
||||||
|
|
||||||
|
这方面的研究工作很多都来自微软研究院。比如2007年左右的AdaRank,就来自微软亚洲研究院的徐君和李航。这篇论文算是较早提出列表法排序观点的研究工作。同一年在国际机器学习大会ICML 2007(International 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-398,2007.
|
||||||
|
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, 1192–1199, 2008.
|
||||||
|
S. Chakrabarti, R. Khanna, U. Sawant, and C. Bhattacharyya. Structured learning for non-smooth ranking losses. SIGKDD, 88–96, 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, 77–86, 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, 271–278, 2007.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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)。他还是ACM(Association of Computing Machinery,计算机协会)和IEEE(Institute 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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),这篇论文利用一个叫做PRF(Pseudo-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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
043 文档理解第一步:文档分类
|
||||||
|
我们在前几周的专栏里讲解了最经典的信息检索(Information Retrieval)技术以及基于机器学习的排序学习算法(Learning to Rank),并且花了一定的时间分享了查询关键字理解(Query Understanding)这一关键搜索组件的核心技术要点。上周,我们还详细讨论了如何从线上和线下两个层面来评价一个搜索系统。
|
||||||
|
|
||||||
|
这周我们的分享将转移到搜索的另外一个重要部件:文档理解(Document Understanding)。也就是从文档中抽取各种特性,来帮助检索算法找到更加相关的文档。
|
||||||
|
|
||||||
|
文档理解最基本的一个步骤就是给文档分类(Classification),看这些文档表达什么类别的信息。今天我就来和你聊一聊文档分类的一些基本概念和技术,让你对这方面的开发与研究有一个基本认识。
|
||||||
|
|
||||||
|
文档分类的类型
|
||||||
|
|
||||||
|
如果我们把文档分类看做一个监督学习任务的话,那么在各式应用中就经常使用以下几种类型的文档分类。
|
||||||
|
|
||||||
|
第一个类别就是二元分类,或者称为二分文档分类,目的就是把文档分成两种不同的类别。比如,把文档分成“商业类”或者“非商业类”。
|
||||||
|
|
||||||
|
第二个类别自然就是多类分类,也就是判断文档是否属于好几种不同类别中的某一个。比如,把文档划归为“艺术”、“商业”、“计算机”或者“运动”类别中的某一类。
|
||||||
|
|
||||||
|
当然,在多类分类的下面,我们还可以分三个小类别。
|
||||||
|
|
||||||
|
第一个小类别,是“多类-单标签-硬分类”(Multiclass,Single-Label,Hard Classification)。什么意思呢?就是说每一个文档只能在多类分类问题中被赋予唯一的标签,并且所有互相的类别是不兼容的。
|
||||||
|
|
||||||
|
第二个小类别,就是“多类-多标签-硬分类”(Multiclass,Multilabel,Hard Classification),也就是说每一个文档可以被认为属于多个类别,然而每个这样的分类都是唯一确定的。
|
||||||
|
|
||||||
|
最后一个小类别则是“多类-软分类”(Multiclass,Soft 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)上位置类似。
|
||||||
|
|
||||||
|
比如,某一个公司的很多子页面,虽然上面的文字本身有差异,但因为都是这个公司的页面,从大的文档页面网络上看,他们都代表这个公司的信息,因此在进行文档分类的时候,也很有可能会把他们放到一起。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了现代搜索技术中又一个至关重要的环节,那就是文档理解中的文档分类问题。你可以看到文档分类所要了解的信息还是比较多的。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,简要介绍了文档分类的主要类型,包括二元分类、多类分类以及层次分类。第二,详细介绍了文档分类所可能用到的种种特性,比如文档上原本的文字信息、文档的排版格式以及周围有关系的文档。第三,介绍了如何利用监督学习以及其他的算法工具来完成文档分类的任务。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,如果一个文档中既有图片也有文字,那我们该如何组织这些特性,然后放到我们的分类器中去学习呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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均值算法。第四,简要提及了文档聚类的一些难点。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,当得到文档聚类的结果以后,能否把这些结果用在其他任务中呢?如果可以,如何利用?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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等模型的领域。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了文档理解中的多模数据建模问题。你可以看到这是一个非常火热的领域,如何理解多媒体数据是现代数据处理的一个重要问题 。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,简要介绍了什么是多模数据。第二,详细介绍了多模数据建模的一些基本思路,包括如何获取文档的表征、什么是联合表征和什么是独立表征。然后,我们还讲了如何构建不同的分类器。第三,简要地提及了其他的多模数据建模任务以及这些任务所依靠的基本的深度学习趋势。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,多模建模带来了丰富的特性,由这些丰富特性所训练的分类器,就一定能比单一数据源所训练得到的分类器表现得更好吗?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
75
专栏/AI技术内参/046大型搜索框架宏观视角:发展、特点及趋势.md
Normal file
75
专栏/AI技术内参/046大型搜索框架宏观视角:发展、特点及趋势.md
Normal 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),有明确的算法来求解在这些框架下的机器学习问题。同时,机器学习的一系列基本的方法论,比如训练数据、测试数据、评测方法等等都可以应用到信息检索的场景中来。这对于搜索系统的性能以及整体搜索系统的研发都有了非常重要的指导意义。
|
||||||
|
|
||||||
|
同时,这也开启了一个非常便利的提高搜索系统效果的大门。那就是任何机器学习领域内部的发展,很多都可以被借鉴到搜索系统中。比如,最近几年深度学习的大力发展,就可以在已经铺就的基于机器学习的搜索系统框架下很容易地进行尝试。
|
||||||
|
|
||||||
|
第二,基于机器学习的搜索系统能够很容易地利用多模数据。对于机器学习而言,多模数据,或者说是多种类型的数据的融合,可以很自然地通过特性以及不同类型的特性来表达。因此,对于多模数据,机器学习有天然的优势。通过学习这些特性之间的联系从而预测相关度,是机器学习的强项。
|
||||||
|
|
||||||
|
因此,理解搜索系统各个部分的数据并把这些信息用在排序算法中,这样的方式就如雨后春笋般大量地出现了。比如,我们之前提到过的查询关键字理解中的查询关键字分类和查询关键字解析,以及文档理解中的文档分类所产生的特性,很难想象这些内容在传统的文本匹配系统中得以应用。但在基于机器学习的搜索系统中,这些信息则往往成为提高相关度建模的重要工具。
|
||||||
|
|
||||||
|
同时,我们也在之前的分享中介绍了,针对多模数据,机器学习中专门有相关的研究,思考如何把不同类型的数据能够更好地融合在一起来建模。这类研究在传统的文本匹配搜索系统中根本不存在。
|
||||||
|
|
||||||
|
基于机器学习的搜索系统也不是完美无瑕。实际上,如果没有各种保证,机器学习并不一定能在实际中获得满意的效果,因为基于机器学习的搜索系统对整个系统而言有了较高的要求。
|
||||||
|
|
||||||
|
机器学习往往需要大量的数据,而在一个现实的软件产品中,如何能够构建可靠并且干净的数据就是一个不简单的任务。如果没有可靠的数据,对于一般的机器学习算法而言,就是“垃圾进入,垃圾出来”,实际效果往往比不使用机器学习还要糟糕。
|
||||||
|
|
||||||
|
同时,机器学习系统可能会有特性异常、模型异常、数据异常等等其他软件系统所不具备的各种问题。如果在生产系统中对这些情况没有一个估计和处理,机器学习搜索系统往往也会不尽人意。
|
||||||
|
|
||||||
|
更加智能的搜索系统
|
||||||
|
|
||||||
|
很明显,搜索系统不会仅仅停留在应用普通的机器学习算法。近几年,搜索系统的发展有两个方面。
|
||||||
|
|
||||||
|
一方面,当然就是依靠深度学习发展的春风,不少学者和研究人员都在思考,如何能够利用深度学习技术让搜索系统更上一层楼。在这方面的研发中,不仅仅是针对普通的深度学习算法,而是看如何应用深度学习所特有的一些模式,比如深度强化学习等方式来重新思考搜索问题。
|
||||||
|
|
||||||
|
另一方面,就是从用户的角度来说,研究更加有意义的评测方式。也就是说,如何能够真正抓住用户对这个系统的偏好,并且能够进一步地去优化这个系统的性能。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了现代搜索技术框架的发展,并简单提及了搜索系统目前发展的趋势 。 一起来回顾下要点:第一,我们讲了基于文本匹配的经典搜索系统的特点;第二,我们讲了基于机器学习的搜索系统的特点。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,在机器学习和深度学习的思潮中,传统搜索系统的核心,也就是我们说过的索引,能否依靠机器学习来生成呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
85
专栏/AI技术内参/047多轮打分系统概述.md
Normal file
85
专栏/AI技术内参/047多轮打分系统概述.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
047 多轮打分系统概述
|
||||||
|
周一我为你介绍了搜索系统的一个宏观分类,包括传统的文本匹配信息检索系统和机器学习信息检索系统。这个分类可以让你非常清晰地了解信息搜索系统的历史进程,并对这两种搜索系统的特点有所了解。
|
||||||
|
|
||||||
|
今天我们就来剖析搜索系统的另一个框架体系:多轮打分(Scoring)系统。
|
||||||
|
|
||||||
|
多轮打分系统综述
|
||||||
|
|
||||||
|
什么是多轮打分系统?为什么搜索系统需要多轮打分?
|
||||||
|
|
||||||
|
我们拿上次介绍的机器学习搜索系统为例。从整体来说,机器学习搜索系统的目的是利用机器学习模型来预测文档和搜索关键字之间的相关性。那么,在理想状态下,针对每一个查询关键字,我们需要对数据集中的每一个文档进行打分。
|
||||||
|
|
||||||
|
如果是一个类似互联网搜索引擎的应用场景,那么理论上,每一个查询关键字都需要对几亿甚至十几亿的网页进行打分。显然,仅仅从这个数量级上来说,这样做都是不现实的。
|
||||||
|
|
||||||
|
从另一个方面来讲,目前比较通用的机器学习模型,特别是在排序问题上有强劲表现的树模型(Tree Model),比如GBDT(Gradient 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 (ECIR’13), 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
67
专栏/AI技术内参/048搜索索引及其相关技术概述.md
Normal file
67
专栏/AI技术内参/048搜索索引及其相关技术概述.md
Normal 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操作符这样能够模拟线性模型的算法。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了现代搜索技术的一个核心组成部分,那就是倒排索引系统。 一起来回顾下要点:第一,我们讲了索引系统的基本组成和原理。第二,我们讲了索引相关技术的一个概况,重点介绍了压缩以及“略过”的含义。第三,简要讲解了查询关键字处理的两种最基础的策略。
|
||||||
|
|
||||||
|
最后,给你留一个思考题,如果我们既有图像信息又有文字信息,那该如何构建我们的索引呢?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
79
专栏/AI技术内参/049PageRank算法的核心思想是什么?.md
Normal file
79
专栏/AI技术内参/049PageRank算法的核心思想是什么?.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
049 PageRank算法的核心思想是什么?
|
||||||
|
上周我们介绍了信息搜索系统的历史进程,剖析了搜索系统的多轮打分系统,还深入探讨了倒排索引,聊了聊它的核心技术。
|
||||||
|
|
||||||
|
这周我要和你分享的是在互联网搜索引擎兴起之后的一个研发需要,那就是如何理解网页和网页之间的关系,特别是怎么从这些关系中提取网页中除文字以外的其他特性。这部分的一些核心算法曾是提高搜索引擎质量的重要推进力量。另外,我们这周要分享的算法也适用于其他能够把信息用结点与结点关系来表达的信息网络。
|
||||||
|
|
||||||
|
今天,我们先看一看用图来表达网页与网页之间的关系,并且计算网页重要性的经典算法:PageRank。
|
||||||
|
|
||||||
|
PageRank的简要历史
|
||||||
|
|
||||||
|
时至今日,谢尔盖·布林(Sergey Brin)和拉里·佩奇(Larry Page)作为Google这一雄厚科技帝国的创始人,已经耳熟能详。但在1995年,他们两人还都是在斯坦福大学计算机系苦读的博士生。那个年代,互联网方兴未艾。雅虎作为信息时代的第一代巨人诞生了,布林和佩奇都希望能够创立属于自己的搜索引擎。1998年夏天,两个人都暂时离开斯坦福大学的博士生项目,转而全职投入到Google的研发工作中。他们把整个项目的一个总结发表在了1998年的万维网国际会议上( WWW7,the 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
75
专栏/AI技术内参/050经典图算法之HITS.md
Normal file
75
专栏/AI技术内参/050经典图算法之HITS.md
Normal 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-632,1999.
|
||||||
|
|
||||||
|
|
||||||
|
论文链接
|
||||||
|
|
||||||
|
|
||||||
|
Authoritative sources in a hyperlinked environment
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
0
专栏/AI技术内参/051社区检测算法之模块最大化
Normal file
0
专栏/AI技术内参/051社区检测算法之模块最大化
Normal file
82
专栏/AI技术内参/052机器学习排序算法经典模型:RankSVM.md
Normal file
82
专栏/AI技术内参/052机器学习排序算法经典模型:RankSVM.md
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
77
专栏/AI技术内参/053机器学习排序算法经典模型:GBDT.md
Normal file
77
专栏/AI技术内参/053机器学习排序算法经典模型:GBDT.md
Normal 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, 337–407, 2000.
|
||||||
|
Friedman, Jerome H. Greedy function approximation: a gradient boosting machine. Annals of Statistics (2001): 1189–1232, 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 (NIPS’07), 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 (ADKDD’14). 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
79
专栏/AI技术内参/054机器学习排序算法经典模型:LambdaMART.md
Normal file
79
专栏/AI技术内参/054机器学习排序算法经典模型:LambdaMART.md
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
63
专栏/AI技术内参/055基于深度学习的搜索算法:深度结构化语义模型.md
Normal file
63
专栏/AI技术内参/055基于深度学习的搜索算法:深度结构化语义模型.md
Normal 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左右的表现。所以,可以说深度结构化语义模型的效果虽然很明显但并不是特别惊人。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了深度结构化语义模型的一些基本原理,这是利用深度学习技术对搜索算法进行改进的一个经典尝试。我们在上面的实验结果总结中已经说到,虽然文章仅仅谈到了文本信息的匹配,并没有作为完整的排序算法进行比较,但是也揭开了用深度模型来表征查询关键字和文档的研发序幕 。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了提出深度结构化语义模型的历史。第二,我们详细介绍了深度结构化语义模型的核心思路以及实验结果。
|
||||||
|
|
||||||
|
给你留一个思考题,除了文章中提到的余弦函数可以作为一个距离函数,还有没有其他的函数选择来表达两个向量之间的距离?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
61
专栏/AI技术内参/056基于深度学习的搜索算法:卷积结构下的隐含语义模型.md
Normal file
61
专栏/AI技术内参/056基于深度学习的搜索算法:卷积结构下的隐含语义模型.md
Normal 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左右的表现。应该说在这篇文章中展示出来的效果还是有比较大的差距的。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你讲了卷积结构下的隐含语义模型的一些基本原理,这个模型是利用深度学习技术对搜索算法进行改进的另一个很有价值的尝试,揭开了用深度学习模型,特别是用在图像处理中非常成功的卷积神经网络技术来表征查询关键字和文档会达到的效果。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了隐含语义模型提出的历史。第二,我们详细介绍了隐含语义模型的核心思路以及实验结果。
|
||||||
|
|
||||||
|
给你留一个思考题,为什么顺序信息并没有像我们想象中的那样,给文档搜索提升带来很大的效果呢?有没有什么解释?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
63
专栏/AI技术内参/057基于深度学习的搜索算法:局部和分布表征下的搜索模型.md
Normal file
63
专栏/AI技术内参/057基于深度学习的搜索算法:局部和分布表征下的搜索模型.md
Normal 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左右的表现。
|
||||||
|
|
||||||
|
小结
|
||||||
|
|
||||||
|
今天我为你分享了搜索专题的最后一篇内容,那就是利用深度学习技术对搜索算法进行改进的又一个尝试:一个结合了学习完全匹配的局部表征和模糊匹配的分布表征的统一的搜索模型。
|
||||||
|
|
||||||
|
一起来回顾下要点:第一,我们简要介绍了局部和分布表征搜索模型提出的历史。第二,我们详细介绍了局部和分布表征搜索模型的核心思路以及实验结果。
|
||||||
|
|
||||||
|
给你留一个思考题,我们这周分享了三个经典的深度学习和搜索相结合的尝试,你觉得目前深度学习在搜索领域取得的成果,有让你感到特别惊讶的结果吗?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
101
专栏/AI技术内参/057复盘1搜索核心技术模块.md
Normal file
101
专栏/AI技术内参/057复盘1搜索核心技术模块.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
|
||||||
|
|
||||||
|
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||||
|
|
||||||
|
|
||||||
|
057 复盘 1 搜索核心技术模块
|
||||||
|
到目前为止,我们讲完了人工智能核心技术的第一个模块——*搜索*。我们从搜索的核心算法入手,进而讨论了搜索的两个关键组件,分别是查询关键字理解和文档理解,并落实到对搜索系统的评价,然后从宏观视角介绍了搜索框架的历史和发展,最后又从深度学习技术在搜索领域的应用角度,对分享做了一个延伸。
|
||||||
|
|
||||||
|
整个模块共27期,9大主题,希望通过这些内容,能让你对搜索技术有一个系统的认识和理解,为自己进一步学习和提升打下基础。今天我们就来对这一模块的内容做一个复盘。
|
||||||
|
|
||||||
|
提示:点击知识卡跳转到你最想看的那篇文章,温故而知新。如不能正常跳转,请先将App更新到最新版本。
|
||||||
|
|
||||||
|
1.现代搜索架构剖析
|
||||||
|
|
||||||
|
从20世纪50年代有信息检索系统开始,搜索系统大致经历了三个发展阶段。从最开始的“基于文本匹配的信息检索系统”到“基于机器学习的信息检索系统”,再到近几年受深度学习影响的“更加智能的搜索系统”。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2.经典搜索核心算法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3.基于机器学习的排序算法
|
||||||
|
|
||||||
|
问题设置:把一个排序问题转换成一个机器学习的问题设置,特别是监督学习的设置。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
4.基于机器学习的高级排序算法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5.查询关键字理解
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
6.文档理解
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
7.经典图算法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
8.基于深度学习的搜索算法
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
9.搜索系统的评价
|
||||||
|
|
||||||
|
If You Can’t Measure It, You Can’t Improve It.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
积跬步以至千里
|
||||||
|
|
||||||
|
最后,*恭喜你在这个模块中已经阅读了70047字,听了220分钟的音频,这是一个不小的成就*。在人工智能领域的千里之行,我们已经迈出了扎实的第一步。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
感谢你在专栏里的每一个留言,给了我很多思考和启发。期待能够听到你更多的声音,我们一起交流讨论。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
77
专栏/AI技术内参/058简单推荐模型之一:基于流行度的推荐模型.md
Normal file
77
专栏/AI技术内参/058简单推荐模型之一:基于流行度的推荐模型.md
Normal 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
Loading…
x
Reference in New Issue
Block a user