Liangshan

Inner peace.

记一次性能调优

继上次做性能优化之后,再次针对我们刚刚全新升级的 app 做了一轮性能调优,而这个过程又引起了我一些思考,这里做一个记录。

根据多年的经验,性能问题一般都是由后端服务引起,API 服务器忙都是受后端服务的拖累所表现出来的现象。所以一入手就是监控各个后端服务的运行情况,初步定位瓶颈在数据库上。第一步就是趁着业务上将来要做全文检索,使用搜索引擎代替数据库作为列表的数据源。这个优化上线之后,高峰时数据库压力有了明显改善。然而这个改善并没有表现在前端页面的速度监控上,几乎所有页面都非常统一的在某些时间点会有长至几秒钟的响应时间,发生的时间间隔没有明显规律,大概几分钟一次,每次持续十几秒钟,并且这些异常并不跟我们的业务高峰有重合。

当时的猜测有 2 个,一个是仍然有什么慢查询影响了整个数据库的性能,从而影响了整站的性能。另外一个是某台机器有问题,所以所有落在那台机器上的请求都会变慢。要验证第二点是非常简单的,我单独拿了一台机器去独立运行最简单的一个业务,结果显示并没有什么变化。

再次确认 API 机器没有资源瓶颈的前提下,决定在代码里埋点统计执行时间,发现了一些执行很慢的代码块,慢的时间和那些异常时间也吻合,只是执行时长对不上。刚才提到异常时刻的响应时间以秒计算,这些慢查询最慢也就是几百毫秒,不在一个数量级。第一感觉就是也许这些慢查询累加起来就会将危害放大。总之优化这些地方最起码不是错误的,于是着手把这些找到的点都修正掉了。

然而那些监控图表上的毛刺像一根根的针一样依然存在,并且由于做了几轮优化,高峰和低谷的对比更加明显,这些图看起来就更诡异。

网站性能优化之外

前段时间在做我们内部一个业务的性能优化,过程里有些感想,这里记录一下。

略去具体的技术手段,我想到的其实超出了性能优化的范畴。

首先,优化上线之后引起了 2 个 bug,一个是重构之后某个代码分支没有测试到,另外一个是某处改动同时被其他页面引用,所以影响了该页面。 这印证了我一直以来的想法,写测试代码看似花了额外的时间,其实节省的都是将来修改 bug 的时间。如果没有这些测试代码,怎么有自信去不停的改进?

其次,在整个调优的过程中,我一直在想如果这些代码经过充分的讨论以及 Review,可能就不会被带到线上。因为一些是明显的慢查询,以及一些相关设计上的缺陷。所以 Code Review 看起来同样花了额外的时间,其实节省了将来重构的时间。

其实这两件事还可以结合起来看,没有 review 没有测试代码,或许在暗示工程师可以写烂代码,反正只要项目上线之前测试工程师能通过就可以了。

团队最怕的就是「内耗」,在这次优化过程中,我花了大概一个礼拜的时间。首先把性能数据可视化,以便能直观的看到优化效果,然后了解业务、分析代码,最后动手优化,前后改进了 3 个版本。我认为这已经是某种程度的内耗,因为原本可以做更有意义的事,但花了一个星期来做一些经过工程方法可以避免的事情。

另外想到的一个问题是,我们总是要平衡设计和施工速度。现在互联网公司的趋势是强调施工速度,而刻意避免过度设计。我也反对过度设计,但这并不等于什么都不管先实现再说。反对过度设计并不能成为自己写烂代码的借口。作为一个设计者,要知道哪些东西是「覆水难收」,哪些东西要留足可扩展性。而几乎任何时候,保持低耦合都是很重要的原则。

最后我想说的是,重视招聘和工程师文化的建设,或许这才是解决问题的根本。

API-First vs. API-Friendly

现在很多应用都运行于多个平台之上,所以通用的做法是写出一套公共的 API 给各个应用统一调用。

比如移动版本的网页,一次典型的请求是这样的:

移动版本的服务端请求公共 API 拿到数据,然后在服务端或是客户端渲染 HTML 5 页面。

用户的登录信息通过 API 验证后,存储在本地的 Session 里。

这种架构的网页,我称之为 API-Friendly,因为这里面有明显的重复劳动:

  • 包含了 2 个后端,一个是网页的后端,一个是前者的后端 —— 公共 API
  • 有两层验证体系,一个是传统的用户名-密码体系,一个是使用签名和加密的 API 验证体系

而我所说的 API-First 设计,就是在任何情况下都以调用 API 为第一目标的设计。

还是以上述移动版本网页为例:

移动版本的页面是纯静态页面,Javascript 直接请求公共 API,拿到数据后由客户端渲染页面(AngularJS 或者 ReactJS 这种技术)。 这样做可以避免构建一个仅仅进行信息转发的后端,另外一个很大的好处是整个网站的所有页面都可以直接使用 CDN 缓存,也可以单独部署。

移动版本的验证,第一次使用用户名密码登录,登录后返回一个 token。之后的每次请求都带着 token 访问 API 进行身份验证,客户端只存储 token,token 过期后需要重新登录。这样将前端页面直接当作 API 的使用者,每次请求都重新验证身份。这样其实也就没有传统意义上的登录和登出了。整个网页是无状态的,也不需要构建 2 套不同的认证体系。

这样通过 API-First 的设计,就能够构建出现代的移动应用。

多做一点点

自从决定 换个姿势写博客 之后,发现可写的内容越来越少了。

这次讲一个鸡汤故事。

故事要从我刚加入安居客说起,那时候刚刚加入一个相对有规模的团队,对身边的一切都充满着好奇,也折腾点东西。其实现在看来都是一些小儿科的东西,甚至有些是在重复造轮子,质量也不怎么样。但是凭着一股热情,还是打动了团队领导,把我介绍给 尔宁 认识,跟着学点东西。大概是 2012 年的春天,尔宁说我们找个人翻译一下 12 factors 吧,要么你来搞一下,业余时间弄一下就行。

当时还有另外一个工程师翻译的 ZeroMQ 文档 ,跟那个一比,才 12 页内容。我就说没问题。

我忽略了一个问题,把话写的短一点更难。这 12 条原则是高度抽象的,我当时的水平其实至少有一半都看不明白,里面举例使用的工具和软件也都没用过。12 页内容,我至少翻译了 1 个月。最后经过同事帮忙校对,放到 github 上,我自认为算是完事了。

但之后我慢慢发现,工作中遇到的几乎所有问题都能在 12 factors 里面找到答案,甚至每次回顾都能理解以前不懂的部分,所谓的「温故而知新」。于是我又重新整理了一遍翻译,watch 了这个项目(之前只有 star)。

不久之后收到提醒有一位日本的工程师提交了一个 PR,实现了对多语言同时在线的支持。我也依葫芦画瓢把简体中文版的提交了 PR,然后就被合并了,也就是现在看到的 简体中文版。顺便说一下,经过社区的贡献,现在 12 factors 有 11 种语言的版本。

之后基本上每个月都会收到邮件咨询一些 12 factors 的事情,直到有一天收到一封 O’reilly 编辑发来的邮件,内容大概是说 12 factors 太抽象了,他要写一本书扩展一下,找我是让我帮忙校对内容,主要是提提意见。作为回报我可以得到任意 2 本 O’reilly 的书籍,实体书或者电子书都可以。

以前都是 review 代码,这次有机会 review 别人写的书,另外英文原本的书至少也要 200 刀了。为了这些钱,我爽快的答应了。

每次工作累了,就拿书稿出来看一部分,这样断断续续两个礼拜也就弄完了。很快 2 本电子书就按照我的要求被打入了我 O’reilly 的账户。

这就是我当时只是答应多做一点点事情的结果。

深入实践 SOA 架构

之前介绍了如何选择 RPC 框架,选择 RPC 框架是实施 SOA 的重要一步,但也仅仅是第一步。今天来讨论具体实施 RPC 过程中,遇到的一些细节。

写下第一个真正的 RPC 服务,首先遇到的问题就是服务划分,或者叫服务的分层。当然最简单的就是不分层,把所有的接口都写在一起,即当服务启动的时候所有的接口都被载入内存从而被访问。现如今软件架构的趋势是使用分布式的独立的微型服务(Micro Service)搭积木,SOA 正是实现这一构想的途径之一,但如果不给服务做划分显然和初衷背道而驰。我们最终选定的划分方式是按照业务(Domain)首先划分出基础服务,我们称之为 LEVEL-1,跨基础服务的接口称之为 LEVEL-2,一些与商业无关的,更为通用的模块称之为 LEVEL-0。

这里以淘宝为例,列举几个服务分层的例子:

LEVEL-1:买家,卖家,账户,商品,交易

LEVEL-2:用户购买一个商品,需要读取账户信息(余额、银行卡),需要读取商品信息(价格、运费),成功后产生一条交易记录。横跨了几乎所有 LEVEL-1 服务,显然是在 LEVEL-2 了。理论上讲 LEVEL-1 的服务各种组合都会产生一个 LEVEL-2 服务。但我的经验是,可以先把所有的 LEVEL-2 都写在一起,等到一些组合十分明确的时候再拆出去。

LEVEL-0:国际化和本地化的一些需求(地理位置,汇率,多语言),敏感词过滤等等,这些服务的特点是都被 LEVEL-1 依赖,又和核心商业逻辑关系不大。

同时围绕这 3 层服务,有一个原则:只能自上而下调用,不可自下而上掉用,同时不可同级应用之间调用。这里指的调用是 RPC,而不是代码上的依赖和调用。

解决了服务划分的问题,在架构设计时同样应该考虑开发、测试、运维。先简单说开发和运维,使用 Ansible + Vagrant 可以保证开发环境的一致性,以及开发环境与生产环境的一致性。在开发环境配置阶段,会将 SOA 的客户端和服务端都部署在同一个虚拟环境里。同时在开发新项目的过程中,不可避免的需要更新数据库,我认为更新数据库的代码也应该是整个项目的一部分,即数据库的变更也应该体现在版本控制中,这个非常重要,这是最终上线前准备工作的重要一环。不同语言的 ORM 都提供了 Migration 工具,但由于 SOA 是跨平台跨语言的,所以我选择了将每一次 Migration 都转化为 SQL 文件,提交到 Ansible Playbook 的仓库,由 Ansible 在部署过程中自动生效。

下面重点说说测试,我们没有专职的测试人员,使用 TDD + Code Review 的方式来保证软件交付的质量,基本上的要求是所有接口都应该是先写测试代码再写实现代码,Review 过程中至少 2 个 Reviewer 通过才可以合并。随着对 TDD 的理解不断加深,我目前所理解的测试代码大概分为两个层面:单元测试,集成测试。

单元测试是指代码中原子性的方法。单元测试遇到其他系统依赖,比如发送邮件,往往需要 mock 这些方法,只是模拟这些方法在特定输入输出下的行为是否符合预期,并非真正的发出邮件。在我所设计的服务分层中,LEVEL-2 测试调用 LEVEL-1 也同样需要 mock,而不是真的启一个 LEVEL-1 的服务用来单元测试。这里需要特别指出的是,严格意义上来讲数据库也是外部依赖,但 SQLite 几乎都预装的前提下,我们可以模拟出一个比较真实的测试环境,所以一般来讲现在都可以真实的操作数据库而不是 mock 方法的返回值了。每次测试前创建数据表及测试数据,测试后再清除所有内容,这是单元测试的标准流程。

集成测试是指将整个系统的各个组件真实的组建起来做统一的测试,还以发送邮件为例,集成测试就需要真正的触发发送邮件的动作。由于我们开发的是 Web 应用,所以我们选择使用 User Acceptance Testing 来做集成测试。简答来说就是借助浏览器,或是可以执行 JavaScript,CSS 的服务端软件,来模拟用户行为,将网站所有功能点都使用一遍与预设的输出来对比。这里有几个问题要解决: 第一,和单元测试一样,每次测试使用的数据要重置,要预留测试需要用到的测试数据。 第二,与单元测试不同的是,测试数据不仅仅为特定的方法准备,而是需要完整的,足以支撑整个网站运行的数据。 第三,由于 SOA 架构,客户端和服务端需要在测试期间连接同一个测试数据库。 为了解决以上的问题,利用 Ansible 给每个开发环境的虚拟机都部署了一个专门用来做集成测试的数据库,以及集成测试专用的配置文件(主要是数据库连接)。另外给集成测试的命令加了一些 wrapper :重置数据库、导入准备好的测试数据(为了保持数据之间的关联,我直接从线上 dump 下一小部分)、切换至测试专用配置文件(包括客户端和服务端)、执行集成测试、切换回正常配置文件。

至此,在 SOA 实践中遇到的一些问题都得到了比较好的解决。

极致的平衡

我们经常说「要追求极致」,每每谈到这个话题,乔布斯就会成为永恒的范例:他是如何苛刻的追求极致的产品体验,以及如何受到养父关于「别人看不到的地方也要追求完美」的影响。

而实际生活中,我们又常常不得不平衡。平衡家庭与工作、工作中的效率与质量、管理中的育人与做事等等。在科学界,平衡更是无处不在,正极与负极、正数与负数、作用力与反作用力等等,可以说我们的世界更倾向于建立在平衡之中。

有一个问题一直在困扰我,到底应该追求「平衡」还是「极致」?或许一个显而易见的答案是「看情况」,那么到底什么情况下应该追求「平衡」,而什么情况下应该追求「极致」?

最近好像突然想通了,不知道是不是看了权利的游戏跟着编剧一起打开了脑洞呢:D

问题的关键在于,我忽略了「平衡」也可以追求「极致」,那么最终来说还是应该追求「极致」了。

比如烹饪一个菜肴,这时候其实你要追求的就是「极致的平衡」,各种食材的味道与口感完美融合,其中的任何一味差了一丝一毫便会影响口感,菜肴本身是平衡的,而你找到这个平衡的过程又是极致的。

其实非常的简单,如果你抛开所谓极致和平衡的观点,重新审视疑惑,会问题的本质在于,你不是在纠结到底应该极致还是平衡,而是在纠结怎么才能把事做好。从这个角度出发,所有疑惑都会迎刃而解了,因为根本无所谓极致和平衡,这两者只是达到目的过程中,不同阶段需要的不同心态而已。这里需要注意的是,很多人对极致和平衡的理解都有偏差,极致绝不是极端,而平衡也绝不是妥协。

选择是一种能力

今年从不同的几个人嘴里都说出了一句话:『有时候选择比能力更重要』。第一次听见这句话,感觉说对了一半:想要有很多选择,首先需要有很强的能力才行,另外选择的过程本身就是一种能力的体现。这 2 个月的经历更让我坚信了这一点。

10 月份开始动了想换工作的念头,先说说为什么想换工作。其实我之前的工作可能在一些人看来是非常完美的,甚至是很多工程师的理想环境:公司的纯技术部门,简直是技术部的特种部队,可以按照自己的想法来实践方案,受到很多工程师和领导的尊重。

但我却感觉越来越不舒服。

我的技术哲学是,不仅要不断学习,更要实践。当时的实际情况是,我们几个想要推行一些新的实践时最后的结果往往是变成小范围的玩具。因为公司几年已经积累了太多工具和代码,基本上该有的都有了。好用不好用是一个问题,但起码够用,所以公司对于改进底层技术的意愿远没有说的那么强烈。这时我想起有人说重构的一个目的就是让工程师开心,这下我彻底接受这个观点了。

有时候太安逸真的是一个问题,对于公司如此,对于我来说更是深知这份「美差」不是好事。

几经面试,有了几个选择,而对方愿意给我机会还是因为我在架构部门的独特经历。同时公司也给出了足够的诚意来挽留我,给了新的部门和薪水。在这个过程中,我仍然相信是能力带来了选择。

在得知我在找工作后,有位前辈好心教导我,应该去大公司待几年,刷刷身份,前途无限。或许他是对的,但我想如果是那样,和我留下又有什么区别?除了可能公司名字更加响亮,可以认识更多的技术人之外,「大公司毛病」我想大同小异。得益于身边的几位同事,我对于所谓的大型架构和大牛早已看透了,选合适的方案解决问题而已,我想要的氛围是 ‘Move fast and breaking things‘,这句是来自 Facebook 的名言。说句题外话,GitHub, Google 或者 Facebook 当然是非常有吸引力,但似乎离我还有点遥远,压根也就没考虑。有人能帮我过去一定让我知道,千万别客气。

这时现在的公司出现在我的选择里,看起来很奇怪的公司。

是跨国公司,但又是初创公司;是互联网公司,却还没有自己成型的研发团队;团队的中国人都会讲英文,团队的老外几乎都会讲中文。兼职和外包是当时工程师的主要来源,这些为公司干活的工程师在世界各地,他们用 slask 交流,用 GitHub 托管代码,用 AWS 托管服务器,用 jira 来管理项目,用 xbox 在办公室踢 fifa,用 CEO 戴假发在球场当拉拉队。

一直以来,我坚持认为公司的技术部门应该走精英化路线。 第一,写代码说到底还是创造性劳动,效率和质量与人的能力不是线性关系,我觉得应该接近于指数。 第二,精英喜欢且只喜欢与精英一起工作。 第三,公司不需要因为开展新的业务而大量招聘,举例来说一个传统网站想要开展新的移动业务,只需要找到一个有丰富实践经验的人,就可以让所有人都变成 iOS/Android 开发工程师,因为精英乐于接受新的挑战。 第四,最重要的一点,精英基本不需要管理,只要给一个大家都认可的方向即可。

这里唯一的问题是精英难找,不过只要找到第一个,只要让他认可你的观点,就一定能找到第二个、第 N 个。在公司达到一定规模时,可以去学校招一些优秀的毕业生。这只是我的想法,没有实践过,比如说公司正在迅速成长,一下子哪来那么多精英可招?我认为精英比例虽小,但找几个满足一家公司的需求还是不难的吧?就看你有没有决心,有没有诚意。

一些迹象表明这个团队有希望成为我想象的那样,几经交谈,我决定加入这里。

让我们再从头看一次整个过程。安逸和挑战,我选择了挑战;去上市公司、留守、创业外企,我根据自己的内心做出了选择。由于这些年的积累获得了这些选择,而这些选择本身代表了我对于技术和事业的理解。

How to Choose RPC Framework

RPC 是 remote procedure call 的缩写,意指调用远程进程的方法。这里的远程需要广义的理解,有时为了协议的统一即使调用本地进程也叫做 RPC,所以 RPC 可简单理解为进程间通信。

在选择 RPC 框架之前,要搞清楚为什么需要 RPC? RPC 主要是为了解决服务化架构中客户端与服务端以及服务之间通讯的问题。在最早接触 RPC 的时候,我一直有一个疑问: RESTful API 不就搞定了么?为什么需要 RPC?直到深入实践了 RPC,我自己总结了 RPC 与 RESTful API 最大的几个不同。

到底什么是产品经理

不知道有多少人和我一样,即使在互联网公司工作了很多年,还是没搞清楚「产品经理」到底是什么样的一个职位。我甚至特意看过很多关于产品经理的文章,仍然没有搞清楚这个问题。而在工程师的圈子里,弥漫着对产品经理的各种,恩,各种情绪。上篇文章说了,我要用自己的大脑思考取得结论,到底什么是产品经理?

换个姿势写博客

之前一直看 Hack News,最近开始看国内版本——Startup News。 结果里面看到王垠的那篇征集女粉丝的博文,后来他删了又写了篇撤回征集女粉丝,现在第二篇也已经删掉了。

以前读过别人分享他的一些文章,并没有读过他关于具体技术的见解。 这次系统了浏览一遍现在能看到的博文,之所以说现在能看到的,是因为他会删除文章,并且不许评论,应该是一个完美主义者吧(其实删除会导致一些文章中的链接失效,也不算十分完美)。 其中发现了一篇 SQL, NoSQL 以及数据库的实质,读完之后的感觉是他看问题比我要深入很多。具体的细节我可能会单独写文章说明,不在这里展开。

所有文章都看完之后,我根本不想去评价王垠这个人,只想说他的文章给我带来什么思考。

能给人带来思考的文章都是好文章,他所写的每一篇都引起我思考。这只有一种可能就是他比我要厉害,主要是在思考问题的深度(无论对错)和在计算机科学学术方面。 学术和知识上比我厉害,这个其实很难追得起来(人家毕竟上了十年博士对吧,除去清华的 4 年,也还有 6 年 :P)。 但还好这个世界并不是谁学术能力强就一定取得更大成就。我稍微总结了一下,为什么他的文章能引发我思考。

首先是不信权威。完全不信权威难免有些绝对,我想更合理的理解这句话应该是,在选择相信权威之前先经过自己思考。只有当自己彻底想明白之后再接受别人的说法,而不是某句话听起来很酷,转身就变成自己的口头禅。直觉上大家都会认为自己不这样,但仔细想想这种例子其实太多。RTFSC 就是我中枪的一条,因为这是 Linus 大神说的。在这一点上我同意王垠的观点。别人期待的是你的经验之谈,而你甩出一句 「Read The F***ing Source Code」来伤害对方真的很酷?

其次是要努力看到本质。要看清技术的本质,需要很深的功力,这个也只能尽力而为了。但要时刻提醒自己,理解一个技术,需要从它要解决的问题根源开始思考,而不是看着手册学习手册。 比如我其实从来没有思考过「到底为什么要有 SQL」这种问题,我是说为什么是通过 SQL 这种方式来跟数据文件交互?因为习惯了 SQL,最开始用 MongoDB 之类的 NoSQL 的时候反而会不习惯,其实仔细想想 MongoDB 的交互终端才比较符合作为一个程序的用法。

从这一点上来说,这个博客虽然才写了几个月,但已经诞生了很多没有什么意义的文章。因为操作手册会过时,基本是没有意义的,但我应该不会删除它们。以这篇文章做分割线,看看以后会不会好一点。

我本来以为至少要几年后才会回头鄙视自己,没想到这个日子这么快就来了。