博客迁移札记——开篇

2022过年这几天在家没啥事儿干,发现阿里的ECS比自己用的云虚拟主机要便宜很多。于是就顺手买了几年的。结果没成想,这一顺手,就把三个节假日搭了进去,期间徒手码了近500条命令才算基本告一段落。

我一直对于写些Google能搜索到的东西没有任何兴趣。但是这三天实际操作下来,发现网上搜索到的资料,当时也许对,但是时至今日,没有一个到现在还是正确而且合理的。

在此,首先感谢开源社区过去10年的卓越贡献,让10年前的实操层面的知识基本都报废了。也让我有动力把这三天的琐碎点滴重新记录下来。感觉写得再啰嗦点,大概就能出书了,《7天精通WordPress迁移——基于CentOS 8、Apache 2.4和MySQL 8》。(尽管我也知道,10年后,本文很可能也没有什么参考价值了。)

I Hear and I Forget, I See and I Remember, I Do and I Understand.

—— 《荀子·儒效》

第一日

服务器到手就是一台祼机,除了自带的CentOS以外,什么都没有。

职业程序员干活第一步,先Google了一下“WordPress迁移”找别人的作业打小抄。找到的比较全的是一个知乎专栏文章《WordPress 搬家方法总结:迁移主机和更换域名》,里面介绍了一个叫All-in-one的插件。看上去自己什么都不用干,只要点点就可以搞定。这很好。

但是走到第二步就卡住了。因为步骤里说:在新主机空间上安装好 WordPress,进入后台安装 All-in-One WP Migration。

得,还是得先自己安装WordPress喽?作为一位不专业的客户,我的期望是,你都叫All-in-one了,居然连自动安装WordPress这么基本的工作都没做。如果我在新主机上,都已经安装好了WordPress,我要你干啥?我为啥不用……我找找……啊,这个WordPress自带的导出功能?

不过转念一想,不对,如果WordPress已经做得够好了,为啥还会有人写插件呢?而且还是成立了一家公司做WordPress迁移服务!这里一定是大坑啊。

官方功能介绍,都只介绍了它能做什么,做好了什么。

然而,它不能做什么,没做好什么,才更值得关注。

—— Hugo Gu

看上面的“导出”功能介绍是看不出什么的了。于是我只能自己试下,也就点下“下载导出的文件”的事儿嘛。

然后眼看着这个文件,一秒钟就下载下来了,我有就种不祥的预感,这特么肯定缺东西啊。但是鬼知道你缺什么啊?你让我肉眼看吗?我看了眼下载下来的XML文件大小,460KB,这肯定没有图片啊。打开简单看了一眼,果然只有文本,图片都是URL地址。好么,这意思是我还要自己再手工导图片?也行,但是更大的问题是:除了图片还有没有别的缺的?

不过无论用哪种方式,看上去我都得自己安装WordPress。我一开始一直没有想法自己安装WordPress,是因为安装WordPress这件事儿,并不是点个exe文件或是跑个yum install这好了。而是至少得安装这些东西:

  • Apache HTTPD:Web服务器。
  • PHP:运行WordPress的脚本语言。世界上最NB的编程语言。(BTW,我还不会。)
  • MySQLMariaDB:WordPress的数据都是存在这里的。
  • WordPress:撰写、发布博客的网站。

然后你为了做管理,可能还得安装下面这些东西。

  • vsftpd: 一个FTP服务器,可以用于管理Web Server里的内容。
  • phpMyAdmin: 一个MySQL的Web管理页面。(你看PHP的NB之处就在于此,人家都不叫MySQLAdmin,而要叫phpXXXX,仅仅是因为用PHP写的。

而且按Linux服务的风格,这些东西不大会是安装好了就可以直接用的。

大家都知道自己用WordPress搭个博客网站不是个十分简单的事儿,所以WordPress自己干脆商业化了,自己官方没有任何文档介绍如何自己用WordPress搭博客(原来是有的,现在全删除了。),只给了个建站手册详细介绍了把WordPress搭好需要注意的各种事项(大概是用来劝退要自建的小白之用),然后自己官方网站隆重推出一键搭博客,每个月30块到500块不等。有兴趣的同学可以去官网了解(需要科学上网)。这一个月30,一年至少360,而且还是功能受限版本,全功能一年6000块。一台虚拟主机也就200多,什么功能都有,作为一名程序员,不仅手痒痒,也肉疼。

另外,微软这里,居然有一篇写于2020年5月的,在IIS上安装WordPress的教程。但是其版的落后程度让人瞠目结舌。

这里又要补充一句,阿里云,在买ECS的时候,可以指定默认安装哪个镜像,其中就有很多WordPress镜像,为啥不直接用呢?主要原因还是一样的,因为我不知道这镜像缺什么,其次就是现有的镜像的OS和PHP和WP的版本都比较老。安装好了一样要升级一轮。而且买了服务器,如果只搭个博客,肯定是浪费了。

说干就干。

先从Web服务装起,WordPress 官方支持的有Apache HTTP和nginx。那我们到底用哪个呢?主要的考量点有两个:功能和性能。考虑个人博客一般对性能要求不高,而且一般Web服务器上的性能问题都可以通过水平扩展的方式解决,然功能问题一般都得靠人才能解决,所以一般而言:功能远比性能重要

网上不难找到性能测评结果,从其中一篇来看,单就WordPress来看,Apache 2.4的性能相比2.2已经有了大幅度的提高,已经接近nginx。而进一步的性能提升,大都会依赖合理地添加缓存,而非Web Server本身的性能优势。说来也是,都是走的FastCGI体系,性能能差多少呢?瓶颈就算有,主要也会是在PHP上。不然WordPress也不会强烈建议用户把PHP升级到7.4以便获得最多几倍的性能提升了。

目前找到的,会影响WordPress使用的,Apache HTTP和nginx的功能上的差别就是文件夹级配置。Apache HTTP支持通过每个文件夹中的.htaccess文件来控制Web Server的行为,但是nginx并没有类似的能力支持。于是WordPress的permalinks,在Apache HTTP上,可以通过自动生成.htaccess文件的方式得到支持(当然,需要配置开启AllowOverride)。但是在nginx上,就必须也要同步修改nginx的配置文件才能生效。从软件工程最佳实践的角度,这违反了功能正交性的基本原则。了解到这一个差别,基本就让我一票否决了nginx。(这里我要再补充一下,以防有人把这句话的覆盖面放大这里“一票否决了nginx”仅限于个人WordPress建站场景。至于各大公司,服务压力大,要把每个节点性能发挥到极致,同时也有实力搞一整套工具做配置管理。用啥都可以。只要规章制度流程健全,贯彻实施到位,用哪个其实都OK。这个问题其实非常有争议,如果你有兴趣,可以在这里加入圣战。)

安装HttpD还是很简单的。

dnf install httpd httpd-manual mod_ssl mod_perl mod_auth_mysql

至于后面那些mod是干啥的,不要问,问就是开源社区常识。然后启动:

systemctl start httpd.service

一个Web服务就好了。但是啥东西都没有,主页都没有。

然后在安装MySQL之前又卡住了,我们到底是安装MySQL呢?还是MariaDB呢?老的服务是MySQL的,虽然MariaDB其实就是从MySQL分出来的,而且从开源血统上讲,比MySQL的还纯正。好,我们来做个更细致的比较吧,拿别人的作业直接看结果吧。总结一下如下表:

MySQLMariaDB
优势高性能、高可用
事务支持
向后兼容
开源
基于 MySQL 社区版
新引擎 (PBXT, XtraDB, Maria, FederatedX)
劣势难以扩展
Oracle买了,使用上有限制
不适合超大数据
新引擎,谁也不知道后面会发生什么。
就像所有的开源引擎一样,没有技术支持。

想想还是MySQL,看上去MariaDB为了不让Oracle有解题发挥过河拆桥的空间,把引擎都给换了。那就不靠谱了。我喜欢折腾,但是不喜欢在正经事儿上折腾。所以当然是MySQL了。

分析完了,安装其实很简单。

dnf install @mysql                  #安装
systemctl start mysqld.service      #启动
systemctl enable mysqld             #开启自动启动
mysql_secure_installation           #配置
mysql -u root -p                    #联上去

之后需要手工创建给WordPress用的数据库。(这里假设你看得懂SQL)

mysql> CREATE DATABASE wordpress;
mysql> CREATE USER `admin`@`localhost` IDENTIFIED BY 'pass';
mysql> GRANT ALL ON wordpress.* TO `admin`@`localhost`;
mysql> FLUSH PRIVILEGES;
mysql> exit

MySQL安装及配置完毕。下面轮到PHP了。

dnf install php php-fpm php-opcache php-gd php-curl php-mysqlnd php-json php-intl php-xml php-pear php-devel
systemctl enable --now php-fpm

网上多数介绍安装的文章,不会一开始就安装这么多东西,但是这些东西WordPress的基本功能就是需要的。所以一次性安装好就好了。比如xml、intl、pear、devel这些一开始可以不安装,但是你会发现WordPress管理后台首页上就会有功能加载不了,同时健康检查也会说有很多框架缺失。(后文会有介绍,当然,如果你按这个命令做,就不会再遇到后文的那些问题了。)

然后下载wordpress的安装包。

curl https://wordpress.org/latest.tar.gz --output wordpress.tar.gz

下了半天下载不下来。速度只有1K/s的样子。然后还下5分钟就断了,然后还没有自动断点续传。我猜这是因为WordPress这个网站不在大局域网里,而且大概率也没做CDN。这个时候,我脑子里冒出三个选择:

  • 无限重试。
  • 在服务器上安装个类似迅雷的玩意。
  • 考虑到我本地和服务器之间的网速还行,我可以在本地下载下来,然后想办法上传到服务器上去。

我思考了几秒钟选了方案三。于是又多了一个事儿。就是如何从本地把文件发到ECS服务器上去。

先是在ECS的网站上找有没有官方支持的功能,找到了一个“发送文件”的功能,刚想夸夸阿里,结果一打开就发现这也太渣了,最大32KB。

算了,还是用最土的办法吧。直接SSH发。第一步先把本地的SSH Key注册到ECS服务器:

本地把pub key复制到剪切板:

pbcopy < .ssh/id_rsa.pub

然后再在服务器上把key写到~/.ssh/authorized_keys里。

为了方便登录,在本地的~/.ssh/config里加上如下配置。

host aliyun-host
    HostName <IP of the Host>
    Port 22
    IdentityFile ~/.ssh/id_rsa
    PasswordAuthentication no
    User <Username to login>

然后就可以这样从本地登录到ECS服务器了。

ssh aliyun-host

然后就可以用下面的命令把本地文件上传到服务器的用户目录上了。

scp wordpress.tar.gz aliyun-host:~

写了3000多字,终于来到了激动人心的,本来要做的正事儿,WordPress安装环节。步骤也很简单。(主要参考了这里。)

tar xf wordpress.tar.gz
cp -r wordpress /var/www/html

这里,网上有些文章会提示说,要登到主机上手工修改wp-config.php文件,把数据库的用户名密码配置上去。其实完全是多余的,WordPress早就可以在界面上配置了。在打开网站之前,需要在ECS的安全组规则中,打开80端口。

然后直接打开网站就好。http://SERVER-HOST-NAME/wordpress。按提示配置数据库链接就好。(这里其实就不太对了,如果只是建立个博客,多数人会希望用 http://SERVER-HOST-NAME/ 访问,这个后面会介绍做法。)更一般地讲:任何登录到服务器上做的手工配置文件变更,都应该避免

就这样,一个空的新的WordPress站建好了。这是第一日。

我像个公主吗?

“爸爸,你看我像个公主吗?”

悠悠穿着一身蓝色的公主裙跑过来,手上拿着小魔仙动画里同款的魔法手杖,摆出要施展魔法的姿势,一脸期待地问我。

“悠悠,你知道吗?是不是个公主,和你穿什么样的衣服没有关系。更重要的是你的气质。”

我说着就在想,现在和悠悠说这些她可能完全听不懂,比如她可能就不知道什么是气质。我一边儿想,一边儿就看到她的脸一点一点地耷拉下来,然后我还没来及再说什么,她就咧着嘴,带着哭腔跑开了,跑到了茶几和沙发之间趴在地上大哭起来,嘴里嘟囔着:“我讨厌爸爸, 我讨厌爸爸。”

刚才的回答,对于她现在五岁的年纪来讲,可能并没有办法真正理解,而只能理解为一种否定。

“悠悠,爸爸没有说你不像一个公主呀,而且你一哭可就不像公主了哦。”

“我不想听爸爸说话,你不要说啦……”

然后悠悠哭着跑回了房间,跟她妈妈哭诉:“爸爸说我不像公主”。

我就在想,我本应该如何回应呢?

一脸惊喜地说,“真是个漂亮的小公主呀。”

还是,“你无论穿什么都是爸爸的小公主。”

这里的问题在于,即使我说的是对的,但是她如果理解错了我的意思,那其实我说的,我想表达的,其实并没有起到任何效果。而且,如果她会错了意,甚至会起到相反的作用。

所以和小孩子说话,甚至和其他所有人说话也一样,一定要用对方能够理解,易于接受的方式去表达。否则自己说得再多也没有用。

当你认为有什么东西会在他成年后对他有帮助时,你也只能讲他现阶段能够理解的东西。 ——卢梭

用电商订单聊聊状态设计

几乎所有的系统设计都会涉及到状态,需要理清楚有几个状态,状态之间是如何流转的。状态本身设计的好坏,常常对系统的后期演化有着深远的影响。本文试以电商订单为例,聊聊设计状态的时候,需要关注的点有哪些,如何通过系统化思考来尽量保障状态设计的适应性。

背景

电商订单是大家再熟悉不过的场景,从客户提交订单开始,到商品交付结束,再加上期间的用户支付,一个最简单的订单状态机大概会长这样(其中绿色表示终态)。

这个状态,一般会被放在Order表里的State列上。

然后,请仔细思考一下,上面的状态设计是否是合理的。按多数剧本,作为第一个模型,一定是不合理的。

好了,现在假设你已经想好了。无论你觉得这个设计里有哪些问题,先放下答案,再去思考这样一个问题:你是从哪些角度、维度和方向思考的?你为什么会选择那些角度去评估这个设计?你思考这个设计合理性时,所遵循的原则是什么?有没有什么假设?你选择的这些角度、维度、原则,是针对这一个电商场景才需要的?还是对所有类型的状态机设计都是普适的?有没有哪些原则,是所有的状态机设计都需要考虑的?你如何验证自己认为的这些普适原则,真的是普适的。

本文的目的,不是做出一个最合理的订单状态设计案。而是通过电商订单这个例子的状态机设计一步步地演进,同时展示错误的和经过一定思考的不同方案,让大家对于如何做设计,状态设计的原则和标准,形成一些初步的认识。(意思是,你不要有,看一篇文章就可以掌握一个思维模式或方法论的期望,即使有一定概率。)

问题

回到上面的状态机设计上,我们结合现实场景,再思考一下,上面的状态机,是否能应对如下几个问题:

  • 如何支持货到付款?
  • 如何支持更多的支付和快递相关状态?比如,支付中和投递中。
  • 如何支持一个订单,多个快递?

我们一个个地处理一下。

方案

第一个问题,支持货到付款,本质上,是把Paid放在Delivered之后。显然,支付货到付款不是“全面切换”货到付款的意思。所以,两种情况会并存。于是,我们顺理成章地得到如下新的状态机。

有问题吗?好像不太对劲。比如终态怎么还能迁移呢?讲道理哈,针对每一个具体的订单,只有一个固定的流程。要么是走蓝色线,要么是走紫色线。所以并不存在这个“终态再迁移”的问题。

第二个问题,支持更多的状态。加就是了啊。这有什么难的?只是可能要分别处理一下要不要支持货到付款的情况。我们有点儿追求,两个方案都做。

至于到底要用哪个方案,产品来拍个板就好,到底要不要支持货到付款,是个非常重要的,影响整体设计的关键决策呢!

最后一个问题,如何支持多派送单?既然都有了多个派送单了,拆表是肯定要拆了,但是还有一个问题,就是如何在订单状态上体现出一部分派送了一部分没派送这个情况。只是现在情况有点儿复杂,要考虑前面两个问题的答案,才能知道这个如何做。所有的方案排列组合一共有4种,就不一一列出了,只列一个最简单的吧。如下图所示。

补充一下,其中Partially Paid和Partially Delivered都是可选状态,如果一起到的话呢,是可以直接到Paid和Delivered状态的。这是多么灵活,多么人性化的设计啊。

以上,所有问题都解决了。


以上方案都是扯淡。这是停止扯淡的分隔线。


解题

状态机设计,最核心的问题是:先搞清楚是在设计什么东西的状态

想一想,我们是在设计订单的状态吗?可以想象自己是一名客服。接到了客户的电话,这么讲:

你好,我的订单号是1234567890,我能问下这个订单现在是什么状态吗?怎么卡住了?

你有大体两种响应方式。一、把这个订单的所有细节,包括多个物流及支付单都过一下,看看哪里卡住了,原因是什么。二、先问客户确认一下,是哪部分,出了问题。

所以你看,订单其实是多个不同的东西的聚合体,里面的每个部分都是独立的,都有自己的状态。而且状态的变化相对独立。

那么问题来了,订单里的各个服务,又如何划分呢?我按前后端分行不行?当然不行了。这里便引出了,判断模块、组件划分合理性的基本原则:划出的各个模块之间是否正交(即,改变一个模块,而不对其它模块产生影响的性质)。

支付和物流属于订单的一部分,彼此又没有强业务上的依赖关系。那么把他们独立为一个模块,并自己维护自己的状态,会是更合理的选择。从而不难得出如下设计:

这样,三个不同的状态,维护三个不同的对象的状态。每一个对象的状态都是相对稳定的。

我们怎么知道分成这三个状态是不是合理的,是不是足够正交了呢?只要试着,把其中一个对象独立出Order,看各自是否需要做什么改变就好。理想情况下,不用做业务变更,就可以做到下面这样——直接把Delivery拆分成独立服务:

系统状态

于是产品也不需要被迫天天做些莫须有的关键决定——比如,要不要支持货到付款。在这个设计下,根本不是一个重要的问题。当然,处理货到付款的逻辑还是存在的,只是这个决策的影响范围只是在顶层业务逻辑上,而不会影响到底层状态机的设计。

这又引出另一个良好的设计的判定标准。好的架构设计,能让人更晚地去做关键的业务决定(因为好的设计,能在无额外成本的前提下,支持更丰富的业务模式)。良好的设计,从实际场景出发,归纳、整理、抽象出能普适的模型,在解决具体问题的同时,又能维护好通用性。

最后,展示给用户看的状态,可以是这三个不同对象的状态,在不同模式的下的灵活组合。如下图所示:

场景化的用户端状态

注意这里展示给用户的状态的命名,和订单上每个对象上的状态并不需要一样,而且也并不一定要合并成一个状态来展示。这里只是表达这样一个理念:用户状态和系统内部状态分离,以便两个状态都可以独立发生变化

结语

设计的优劣,是决定服务长期可扩展性和可维护性,并持续产生价值的关键因素。而好的设计,需要结合业务实际场景,应用合理的原则,做进一步的分析和建模才能得出;它既不能是原始需求的简单堆砌,也不能是既往经验的原封照搬。

最后,也是最重要的是,是真的能把以上道理用到该用的地方去。如果你发现你没有用上过,或是没有机会用上,那其实只说明你还不是真的懂。

未有知而不行者。知而不行,只是未知。——《传习录》

如何开始做饭

我不是厨子,也没有兴趣教别人如何做饭,毕竟做什么饭,怎么做饭,都是看个人爱好的。我既无权,也无意发表太多意见。然而遗憾的是,我绝大多数时候都是吃别人做的饭,而且觉得很难吃,所以对做饭这事儿,还是想说说自己的看法。又考虑到我不是专业的厨师,所以我只讲开始学做饭这段儿。

我开始做饭的理由很简单,自己爱吃的东西,总得自己做出来的才对味。简单到番茄炒蛋这种最基本的国民菜,去每家餐馆,口味都是不一样。更要命的是,哪怕同一家,你每次去,做出来,口味也都能不一样。作为一个吃货,这很要命,于是我决定自己做。我最开始练手的菜就是番茄炒蛋。大概自己炒了三五次吧,就已经是家里炒番茄炒蛋炒得最好吃的了。这不是自己觉得的,是家里人和邻居朋友都说不错。包括家里四岁的小屁娃。

做得顺手了之后,逐渐做过稍大的家常菜,比如红烧肉、糖醋鱼、小鸡炖蘑菇、酱汁扣肉什么的。只是到现在也还没有做过干煸刀豆(因为太费油),佛跳墙(因为太复杂)还有烤鸭(因为没有设备)。这些也没打算自己做,一则自己做不合算,二则,也给下馆子留个念想。要是自己什么都比别人炒得好了,好到下馆子的欲望都没有了的话,人生也失去了一种乐趣不是?

学做菜的窍门其实就一个,就是想要把菜做好,而不是想填饱肚子,也不能是想省钱,也不是做家务尽义务,更不能是想发朋友圈。

头几十年,国家物资匮乏的时候,要米没米,要油没油,要糖没糖,好多人也没有这心思去琢磨怎么做饭更好吃,那时候人们天天主要都在琢磨怎么让家里人不饿死。至于做饭的手法和技巧上,不少都是在考虑如何用最少的时间,最少的物料,喂饱最多的人。在这个心态下,做了几十年。然后现在物资充沛了,做饭很难一下子好吃起来,因为之前的做法已经习惯了,改不了了。

有些被迫做饭的,因为本来就不想做,做的时候也就不走心。一真要做,就装不知道怎么做啊,不知道火要开多大,不知道油要倒多少,做个最简单的煎荷包蛋,倒上一层油,然后怕烫到,往锅里窝鸡蛋的时候,把鸡蛋举得老高,鸡蛋就像高台跳水一样跳到滚油里,然后把热油溅得到处都是,还害身上、脸上起几个大包,于是自己便得了理,说,我说我不会做的,这下好了吧。自此以后就心安理得地告别了厨房。

如果做菜是为了发圈,那东西总得做得好看些,拍的照片也得漂亮些。于是菜还没炒,炒菜的家伙事儿就先提上了日程。比如切菜板就有讲,木质不能太软,一切菜就掉木屑;也不能太硬,刀工不到家刀刀切到木板,刀就开始卷刃蹦刃。再说刀,切菜的和砍骨的要分开,蔬果刀要锋利,须得高硬度,高碳锰钢比较合适。其它的锅啦、灶台啦花样就更多了。可能做菜的原料都还没有准备好,各式的调查研究已经走全乎了。然后做饭的过程中,总得给人讲讲吧,让看的人知道是怎么回事吧。于是还得准备上各式的温度计、计时器及秤等。做菜的用料须得精确到克,用时须得精确到秒,火候须得精确到度。(话说前几年给奶娃喂奶差不多也是这阵仗。)最后摆盘更是重中之重,最好如一盆景一般重视,好好的一盘番茄炒蛋,总不能在装盘上功亏一篑。好不好吃呢?管他呢,反正朋友圈里的看客们是吃不到的。

其实呢,准备充分不是个坏事儿,只是有的时候,周末睡个懒觉,十点钟才起床,打算开始做个早饭兼午饭,脑子突然抽筋,想要吃蛋挞,然后就躺床上拿着手机,先开始百度“如何做蛋挞”,然后再知乎“哪款烤箱最适合烤蛋挞”,然后再淘宝烤箱。就感觉哪里有点儿不太对劲。而且脑子老是这么抽筋,怕烤箱到了,心里又想着吃别的去了。

话说回来。如何开始做饭呢?重点在做饭上,不在如何上,也不开始上。就是这么简单。

《坐井观天》想说什么?

《坐井观天》是人教版小学二年级语文课文。小时候学的时候,只知道这大概是教人开阔眼界。但是最近吃饭遛弯聊到这则故事,于是我又复读了一遍,却发现自己看不出,把这种故事放到小学课文里,到底是想说什么了。

这个故事的原型出自《庄子·秋水》。原文如下:

北海若曰:「井蛙不可以語於海者,拘於虛也
夏蟲不可以語於冰者,篤於時也;」

荀子·正论》中也有演绎:

彼楚越者,且时享、岁贡,终王之属也,
必齐之日祭月祀之属,然后曰受制邪?
是规磨之说也。沟中之瘠也,
则未足与及王者之制也。
语曰:“浅不足与测深,愚不足与谋智,
坎井之蛙,不可与语东海之乐。
此之谓也。

然后我们看看小学语文的版本。我就不作评论了。

其实,没有什么是“一定”的

工作久了,不难发现一个现象:在一些公司被奉为圭臬的原则,在另一些公司却被弃若弊履。当然,各个公司业务模式不同,阶段不一,在实操层面应用不同的术,是非常自然而且合理的事儿。每个公司都可以而且应该,根据自己的实际情况,选择最适合当下环境的术来处理每天的具体事务。

这个根据情况决定适用的“术”的过程,本身也是一种“术”。每个公司都会有不同的做法。比如有的公司会期望充分发挥员工本身的主观能动性,自行分析情况决定使用什么“术”。有的公司呢,会由管理层分析并决定,层层领会学习贯彻实施。这两个方式都在各自的条件下可行。

让员工自行决定“术”的这个方式,在员工能力非常强,而且“术”相近的情况下会更适用。否则很容易产生彼此格格不入的“术”,轻则影响工作效率,重则引发员工冲突。这种做事儿方法与理念上的冲突通常都不太好调和,处理不当就会以某方退出收场。这也并不是说上令下行的模式就不会产生这样的冲突,但是毕竟“术”是事前沟通好了的,不过至少比事事、时时上都可能产生冲突的模式要好得多。对于需要激发员工创造力和主观能动性以便更好地完成工作的公司,一般都给员工一定的自由度去决定达成具体事务的“术”,这也是当今科技公司的流行做法。那么问题在于,在这种环境中,如何缓解“术”上的冲突问题呢?

想缓解冲突,就必须先了解其产生的原因与来源。其实很简单,当大家对同一件事儿,有不同看法的时候,就会有冲突。而当这个事儿的最合适的方案不是那么明显的时候,这种冲突就会更加明显。因为这时,常常是双方都认为自己是对的,自己是理客中,认为对方冥顽不灵,食古不化,不可救药。

这种例子不胜枚举。比如我之前待过的一家公司的CTO在一次小事故之后跟我们讲,你们少写这些自动化的东西,搞不好产生环境就搞坏了,以后所有动生产环境的操作,必须纯手工。以防出错。当时想,这可糟了,我对我工作的定义就是,我要让电脑把我需要干的活都干了,而不是反过来。所以没多久我就借故离职了。我之前还待过的一家公司的运维负责人,要求所有的权限必须给到人,不能给服务,不能给机器 ,不能给脚本,不能给CICD,给也给到人然后委托给到这些东西。跟他讲服务间授权体系的需要,他说,这我不管,反正“权限只能给到具体的人”,这是原则!后来我到另一家公司,运维的方式简直是180度倒转,企图把人身上的权限全收回,最好都给服务和机器。说实话,我特别希望能有机会请这两家公司的运维负责人一起喝个茶。:-)

这里并不想讨论到底哪种是对的,哪种是好的,也不想分析各自的使用场景什么的。这些都不重要。这里想说的重点是,你看,完全对立的做事儿方式,其实都可以工作,而且都能工作得很好。引发冲突的,其实只是类似这样态度和思路:“你说得不对,这事儿就是这样的,这是原则,这是制度,这是公司条例,没得讨论”。在一些人脑子中,什么事儿定下来就是定下来了,不讲环境,不看变化,拒绝讨论。但是当你真这么说他的时候,他就会说,我哪有拒绝讨论,你到底要什么?你有方案吗?没有?没有你BB个毛线啊?要不你出个方案吧?【注:这就好比你去商场买东西,售货员说没有。你说,哦,没有啊,售货员一听就怒了,你怎么说话的?怎么能说我们没有呢?你有你做一个啊,我们马上就可以卖给你!】我一向都不大擅长和这种人打交道。

这里其实想说的是:永远不要认为,自己的某个认识,是最正确的。也永远不要认为什么事儿,就一定只能如何如何,不能如何如何。所有的理念,方法论,术,都要看环境,看时机。小到求职就业,大到吃喝拉撒睡,无不如是。最重要的,是能自始至终地,针对具体情况分析、探寻问题的根本,追求更适合的方案。一但你开始感觉自己已经找到答案了,其实你也就同时停止前进了。所谓“Stay Foolish, Stay Hungary.” 大概就是对此最著名最精炼的解释。

上面说的主要是认识冲突。并没有讨论利益上的冲突。因为利益上的冲突相对容易处理些。

每个深刻的领会后面,一般都会有点儿故事。在我工作5到8年的那段时间,我一度非常迷茫,迷茫于不知道到底如何做才能做到最好,才能不出问题,才能最能让领导满意。有次我领导的领导来上海,就坐我边儿上,我和他闲聊,我问他,你认为工作中最重要的原则是什么?或者说,我应该用哪些原则来指导我的工作?他想了一会儿,大概是猜到了我这个问题的背后是颗多么天真的大脑,一脸严肃地和我说,Hugo,没有什么原则是能帮你解决所有问题的,总是要看具体情况的。(大意)当然,我也并没有随着这句话大彻大悟什么。只是这几年间,才慢慢体会到这句话其实是多么的有含金量。这个我或许和一些人讲过,我现在最大的原则就是:没有原则。【不知多少愣头青要意会为不讲原则。】

遗憾的是,我现在依然会和很多人有冲突,我也并不觉得会有不再有冲突的那天。只是在我眼中,那些冲突都已经不再让我忧心忡忡,因为说得简单些,只要把产生冲突的问题解决掉,冲突就不在了。只要专注解决问题和解决问题的方案就好。

作业:从阿里的过往的新闻来分析,阿里会如何处理冲突与矛盾呢?

【防杠精小贴士:不要拿什么“小孩子才分对错,成年人眼中只讨论利益”。这种陈词滥调说事儿了,利益就能算得清了吗?扯淡我也会:“小孩子才觉得利益就是个数字,算算就一清二白的;成年人只讨论取舍”。这种讨论没头,吃饭的时候拿来吹吹NB就算了,这说正事呢。】

聊聊什么是“技术含量”?

面试的时候我见到过不少毕业没几年的候选人,被问到自己想换工作的原因,会说感觉现在做的事情没有什么技术含量,所以想找个有技术含量的公司,做点儿有技术含量的事儿。

然后我让他举个例子,讲讲怎么个没技术含量了?他会说,天天就是写些业务代码,天天就是CRUD,代码Copy来Copy去,用的技术也特别陈旧,感觉自己的能力完全发挥不出来,已经到了职业生涯的天花板了。讲得头头是道,让人感觉他不换工作,就要被整个公司拖累了,这辈子都要白活了。

然后我再问,你感觉什么是有技术含量?他说,怎么着也得有上千万活跃用户,几十亿PV吧。要不然数据量没到这个量级,根本遇不到太严重的问题,也根本用不到那些有技术含量的方案去解决。

一般聊到这里,我就开始准备收场送客了。客观原因是我们目前没有几十亿PV,怕他会觉得工作内容匹配不了他的能力与愿景。主观原因是我认为他对于什么是有技术含量的有偏见,同时自身能力也不合格。在我看来,对于绝大多数十年以下工作经验的人来讲,没有技术含量低的项目,只有技术含量低的实现方式。不要抬杠说一个登录页面和自动驾驶系统能一样吗?

但是礼貌起见,我会继续和候选人聊聊他们的技术栈。问问他,现在在写的CRUD代码,是哪部分让自己感觉特别无聊,有没有什么可以改进的地方?他在Copy Paste代码之前,有没有想过其他的更合适的方式去实现同样的功能?如果在这部分,这个候选人能够表现出任何一点先调研再开发,有任何一次用公司尚没有人用过的方式去做事的心气,我都会充满兴趣地继续聊下去。然而遗憾的是,几年中,但凡是前述方式解释自己换工作原因的人,能表现出自这一点的,我印象中是一个都没有。

这些东西干巴巴地讲比较模糊。我举个例子。

曾经遇到一位3、4年经验的候选人讲,他们公司的接口层代码,全部都是从HttpServletRequest中解析参数,自己反序列化,天天好多时间就写这种get/set代码。感觉特别无聊,而且学不到任何东西。我问他你知道PathVariable和RequestParam么?他说好像见过,但是公司现有的代码没有用这个的,自己也就没有用。我问他,你觉得现在这个不好,那你有试着找过更好的方式吗?他说他天天就忙着写代码,天天加班到10点11点,哪有时间学新东西呢?

其实这种人非常被动,如果没有人强压着灌输一些新知识,他永远就只会用自己已经熟悉的方式去做事儿。我有时在想,这是我们从小接受的教育模式使然,还是说有的人性格就是这样呢?我倾向于两者共同作用的结果。

那位候选人倒还比较谦虚,面到后面就闲聊了些东西,他问我有什么建议,我便建议他,如果想有进步,以后可以这样做:假设有一个任务,做过类似的,也知道如何做,估的时候大概要花一天。怎么做呢?先不要去做,先花4-6个小时尽可能多地找可行的方案,然后用自己能找到并能控制的最佳方案去做;大概率2个小时就可以搞定;如果找不到怎么办?这个时候再加班搞定;这样坚持个几年,基本上工程技术上就没什么问题了。

只是不知道他后来有没有听进去。因为现在想来,我当时漏了一个关键点没有提醒到他。就是这种做法,大概率会受到保守型的上级领导的挑战。一句话就能让这些主观能动性不高的人打起退堂鼓:用什么新的做法?出了问题,你负得起这个责任吗?如果自己他不坚持,他是不会有机会实施这种做法的。如果他坚持,哪怕是保守型的领导,大概率也会支持的(当然,如果不是什么大事儿的话)。

这种事儿还有另一种做法。就是先做,只是做的过程自己需要保证不出问题,做足预案,出了问题自己第一时间搞定,全部搞定之后,直接拿结果说话。当然这种做法一般会非常累,因为时间是不变的,要做的事儿可远比沿用老的方式直接做要多得多。很多人自己就不愿意承担这种多做些不一定有成果的事儿的风险。当然也就失去了这种方式快速成长的机会。虽然这种机会其实天天都有,事事上都有。

一直沿用老的低效率的方式做事,从不思变,看着是勤快,其实是懒。就这么一直懒着,怎么可能有技术含量呢?

所以说回面试,以后面试,除非你真的已经是个大牛了,否则不要讲公司的东西没有技术含量,那只能说明你自己没有水平,而且没有成长空间。哪怕你就说钱没给够,哪怕说你觉得你领导是个混蛋和不来,也比说你觉得公司的项目没技术含量要好些。

PS. 虽然我印象中,我自己被面试时,好像也有这么说过。然而不同点是,如果我说了,我也能证明我是对的。

来,一起看看import *到底怎么了?

Google Java编程规范》里有这么一条:

3.3.1 import不要使用通配符

即,不要出现类似这样的import语句:import java.util.*;

不少公司也的确是这么要求的。但是当我面试时问起为什么不要这么用的时候,不少人就开始语焉不详了。

其实稍微有点儿心的人,稍微花个几分钟,就可以从网上搜到这个问题的答题。诸如:

  • 会导致不同包的同名类之间会有冲突。比如java.util.Date和java.sql.Date
  • 使用具体的完整的类名,每个类的来源会更明确,代码会更好读。
  • 编译速度更快。

更有意思的讨论,可以移步到这里观战。

然而我写这篇文章,并不是为了解释为什么不要用import *

恰恰相反,我想说的是:上面说的这些不要用import *的原因,听上去是有一定道理,但是其实全是放屁。

  • 对于类名冲突的问题。要考虑是代码的分层是否合理?类的命名是否合理。自己把一个类名叫Thread,还要和处理多线程的代码放一起用,不去反思自己类名是不是起得太混蛋,代码分层是是不是太混乱,然后还跑去怪编译器处理不了。不知道是谁给你这么厚的脸皮的?不拿去磨刀真是可惜了。
  • 在这个AI都叫嚣着要写代码的年代,你还把各种IDE当记事本用,别人写个import *,就说自己找不到这个类是在哪儿定义的,还说这代码不好读。想用代码行数算工资就直说,不用这么拐弯抹角的。
  • 先去把测试代码里的Thread.sleep(1000)处理干净啊。就现在这些工业代码,还不需要计较import *与否造成的性能差异。你真要计较这些,也要先测试一下自己码代码的手速。1分钟之内没办法把26个英文字母打10遍,按你的逻辑就是,你的手速太影响开发效率了,不适合当程序员。麻溜的,主动辞了吧。

最后,再补充一下。我也不是在说import *,我是在说所有的这些,不过脑子,不讲道理或是讲着歪理,然后单纯就是这么做着的,一切屁事儿。

服务配置管理的一些误会

这里指的配置,泛指一切能改变服务行为的变量。其生效的位置可以在编译期、部署过程中、运行期;其存在的形式,可以是代码中,本地文件,资源文件,环境变量,数据库甚至其它服务。也正是因为这种灵活性。配置管理大概算是软件工程里人们分歧最严重的领域。而且,由于“反正无论怎么样搞,怎么样都能搞定。搞定不就行了吗?”的“结果导向”的态度的存在,这部分也常常最不受实质上的重视和控制,于是对配置项肆意随性的使用,最终将导致配置混乱,配置漂移,配置变更风暴乃至生产事故的发生。(顺便说下,我这里当然就是在曲解“结果导向”,但是我见过无数人在想说一个实现细节不重要的时候,便把“结果导向”搬出来说事儿了,所以这里说是“结果导向”是为了便于一些人理解。)

服务配管管理这个话题非常大,一篇文章显然说不完。下面我仅仅讨论几个我感觉人们误会最深的点。主要内容与理念来自《Continous Delivery》一书,但是重点不同。

代码与配置分离

“代码与配置分离”常常被从字面上理解错,从而产生两个严重的误会:

  1. 代码中,不要出现与配置文件有关的任何东西。
  2. 另外,哪部分算“代码”呢?我们要求高些,就整个代码库吧。

于是在犯了这两个误会的代码库中,连个完整的配置文件都找不到。于是你连这个服务都有哪些配置项都看不出来,要去实际环境中,把环境变量啦,本地配置文件啦统统拿出来过一遍才搞得清楚。我遇到过的,更极端的做法是样的:写一个服务维护各个环境的配置项,这个还不错,但是呢,又在CI中加入一个自定义的步骤,把已经打好的Jar包解开,把相关配置下载下来,替换掉里面的application.properties文件,然后重新封成Jar包。我一开始还以为有什么高深的不为人知的原因才这么搞的。后来我发现我想多了,他们就是不想用或是不会用Spring的Profile机制罢了。虽然所有项目都的确是“用了”Spring Boot框架的。但是真的只是用了个框架,本质上这个项目连Spring 4的功能都没用上。

再说回原题,我不想解释“代码与配置分离”是什么意思,因为这只是个处理方式,我想介绍一下这个处理方式要解决的问题是什么。它要解决的主要问题之一是:同一份代码产生的包,能够不经修改地,在不同的环境下运行,配置可以在代码之外进行控制。避免出现“因为生产的配置不一样,所以要改下代码,再打个包再发到生产”的问题。那为什么临时改下,再打个包再发生产不行呢?我乐意加班不行吗?不行,问题在于:

  • 效率低。你改下代码,再打包,要不要再测试呢?即使不要,也10来分钟过去了。能不花额外时间的事儿,就不要花1秒额外时间。加班要创造价值,不是创造问题。
  • 风险高。你说你只改配置,谁知道是不是真的呢?要不要人来检查呢?你手改的怎么保证不出错呢?
  • 成本高。这是上面两条的推论。

其实《CD》这本书,全书基本都是在说提高效率和降低风险。抓住这个主旨,再去理解“代码与配置分离”,至少不会跑得太偏,也不会纠结于怎么分,分到什么程度这种问题。

虚拟化场景下的代码与配置分离

到了虚拟化的环境,情况会略有不同,部署的单位是镜像,那么问题来了,镜像里面应该是纯代码?还是代码与配置的合体?这个复杂些,两个说法都不对。

镜像本身是包含运行环境的,比如OS,JDK,一部分环境变量什么的。所以环境相关的配置,好多都是和OS, JDK相关的,本就应该放在镜像内部,不允许随意从外部修改的。非要从镜像里拿出来,当然技术上是完全可行的,但是没有意义和价值,而且效率会变低。拿出来统一管理所有配置?不不,统一是手段,不是目的。餐具有筷子和勺子,是为了吃饭方便高效,但是吃一碗汤面,不需要为了用筷子和勺子,就非把面条单独捞出来放一个碗。这叫脱了裤子放屁。 配置项到底是在镜像里还是镜像外只是手段。你要看你的问题解决了没有,目的有没有达成。而且,配置放镜像里,就无法统一管理了吗?就无法从外面控制了吗?

当然,还有一些配置,比如密码,密钥什么的,安全起见,就不可以放在在镜像内,加密过的也不行。否则镜像一旦泄漏,别人直接就能把你的服务原模原样地跑起来,这个风险是不可承受的。

所以总结一下:镜像里应该要包含并固化所有环境的多数配置,通过一个参数决定运行哪套配置,少数有特殊要求的配置要独立出来。但是建议保持从镜像外部改变现有配置项的值的能力。以便可以在不重新构建的情况下,直接调整运行时参数来解决一些比较紧急的问题。但是尤其要注意的是,所有这些紧急调整的参数,一定要签入代码库以固化到镜像中。

注意:上述这些行为,Spring通通自带支持,默认就这样,你不需要为这些事儿写一行代码。除非你放屁要脱裤子,那是要写不少代码来支持的。

配置应该放哪里

能放配置项的值的地方非常多,我们先来看下都有哪些地方可以放置配置项及值。

  • 代码里。
  • Jar包的resources。比如:application-xxx.properties文件等。
  • Docker file
  • Host机上挂载的文件
  • K8s的配置文件
  • Host机上环境变量
  • Java, Docker, K8S等的启动参数
  • 各种云平台自身中。如Aliyun, OpenShift

正是因为可以配置的地方太多了,所以必须严格控制好“应该”在哪儿放配置,否则非常容易失控。在没有控制的情况下,多数人会选择自己已经知道的方式或是最方便的方式去做配置变更,而不是去探求最合理的方式去做配置变更。
所以我们先讨论一下,配置有哪几类,分类的维度会有如下几种:

  • 谁来决定值是什么?开发,产品,运维?
  • 要不要随时修改? 实时生效?
  • 是否敏感(需要保密或脱敏)?
  • 是否因环境而不同?
  • 是否有足够的权限。比如你需要有一个Kafka的Topic,这个可以是一种配置,也可以自动化配置,但是如果运维不给你权限的话,你配置这个东西的方式会有极大的不同。

上面并不是所有的维度,但是仅仅上面的维度就有2^4*N种情况,N是公司需要做配置的角色数。当然,你并不需要2^4*N种配置的方式,配置的方式是越少越好。但是要能满足以上所有情况。并且,还要满足以下对配置管理的要求:

  • 版本控制。是的,配置本身也要有版本控制。不是随手改的。至于放在代码之外,或与代码放一起都可以。如果你不想搞得太复杂,就和代码放一起。放在代码之外的话,你还要管理好配置的版本与代码的版本的兼容的对应关系。有了版本,也好做回滚。版本控制的另一个潜台词是配置共享的,团队新来一个成员,应该是可以用得到这些配置,并一键把服务按配置好的参数跑起来的。不需要满世界问人,有哪些参数要配置啊?要配置成什么值啊?你把值拷我一下啊?
  • 统一配置的能力。比如Kafka,常常是多个服务都要配置这么个地址(或ZK的,一个意思)。那你是感觉每个服务各自配置自己的好呢?还是用全公司统一的一个配置好呢?(假设公司只有一套Kafka)
  • 抽象配置存储方式的能力。比如密码,你本地环境的,开发环境的密码,可能会感觉放properties文件就好了。生产的是要通过环境变量的。然后有一天,公司打算用Vault了。你的代码,你的配置定义,应该是都不需要修改才对。这样,你的配置系统,才是与配置存储的方式是解耦的。

好,上面说了一堆要求了。方案呢?很简单。放配置项的值就三个地方,

  • Properties文件:开发负责。通过发版,或Config的Refresh Scope来变更。
  • 数据库:产品负责变更的配置,一般要有相配套的后台页面在运行时修改。
  • 环境变量。环境变量的注入方式运维团队自己决定就好。因为这个是他们负责的。

其它地方,什么Docker file啦,K8S配置啦,启动参数啦,原则上统统不放配置。
至于实现,用Spring自带的properties支持就可以达成效果,通过profile来区分环境。日后如果要用Vault,与Spring Cloud Config集成也简单。

应该谁来配置?

这其实就是上面一节中的一点。但是我感觉非常重要,所以想单独拿出来再说一遍。因为“谁来配置”,基本上决定了配置的方式甚至能进而影响服务的设计乃至架构。有人可能从字面上看到这个问题就反感。因为一般情况下,一个事儿,一旦聊到“谁”来做这个问题,大都就开始推诿扯皮了,因为很多人的心态就是“多一事儿不如少一事儿。”,而不是考虑这个事儿“应该”谁来做。这里的“应该谁来配置”的重点,恰恰就是“应该”是谁。而不是“谁”来做比较快,比较简单。

那么配置到底“应该”是谁来配置呢?一般而言,谁有权力决定这个值应该是什么,谁来配置。注意这里的“权力”是发言权、专家权,而不是职权。那么谁有专家权呢?

比如,API的错误率的告警阈值,传统的做法就是负责系统监控的运维人员来决定一个全公司统一的值。比如5%,高于这个就告警。但是每个API的情况不一样啊。比如12306的图片验证API,错误率正常情况下肯定不止5%,谁不得试了两三次的?你配置个统一的阈值必然没办法普遍适用。 (注意,不要抬杠说这里验证失败也应该是200,这里只是用这个接口举个例子)

于是有的公司改进了,专门做个页面,允许每个Team的负责人去配置每个API自己的阈值。于是又有新的问题了,比如:

  • 负责人说,我也不知道每个API正常情况下,错误率是多少啊?那个谁谁谁,你来拉下数据。
  • API改进了,用异步操作了,总是直接返回结果,然后错误率忘改了,然后一直没有任何告警。

Team Lead是专家吗?Team Lead其实也不可能事无巨细地知道一切,不可能实时跟进API的变更,也不可能不顾实际情况地强行配置一个值,那和一开始由运维配置一个全局的值没有本质区别。本质问题是什么呢?Team Lead其实也决定不了一个API的错误率。只有写这个API的人才能根据自己对API的了解做出判定。即开发本人,确定每一个API的合适的错误率告警阈值。对于非常专业的工作,实际做的那个人,才是专家,而且必须是专家,不然他是做不好的。这里的专家并不是方方面面的全才,而是指在自己负责做的这一个小部分,在整个公司范围内,他必须是专家,至少是之一。

当然,这么做是有两个假设,一是假设开发都是认真负责的,不会配置个99%然后就安心地去睡大觉了,其次Team Lead也要愿意相信Team Member的判断。另外,“合适”并不表示“符合预期要求”。比如开发人员确定了一个数据查询接口,由于bug多,其错误率本来就高,认为告警阈值配置到30%才真表示有问题。Team Lead的责权在于,决定让开发人员改进接口,把错误率降到3%,才允许上线。

重点来了,如果一个东西让开发去配置,那方式就多了。而且一般不用单独做个配置页面这么重。常见的有:

  • 配置文件。比如properties文件。
  • 数据库migration脚本。比如Flyway或是DbMigration来修改数据库表中的配置信息。
  • 甚至代码本身

这里说的在代码中做配置并不是hardcode在代码里,具体的例子可以参考bucket4jbucket4j-spring-boot-starter这两个项目中的示例。对于需要由开发来配置的参数,这种方式势必会是方案之一。

注意:这两个框架我并没有用过,而且是做限流,不是做告警阈值配置的。只是按自己解决问题的思路,找到的现成的和上面说的方式比较一致的框架,提一下给大家做个类比参考,我也省下自己去写example的时间。

最后再强调一点,我写这篇文章,并不是要鼓吹在“代码里做配置”这种方式。 而是在讲,做一件事的时候,最好先把思路打开,别把思路局限在一个小圈圈里找方案。“代码里做配置”,只是一个代表,代表着那些从一开始,由于一些成见、思维定势而被排除在可选项外的可选方案之一。

压力测试与服务容量规划

压力测试的目标主要有两个,一是验证服务技术指标是否符合要求,二是为服务的实际部署方案的规划提供基础数据,以便回答这个看上去很简单的问题:要什么配置的服务器?要几台才够用?然而这个问题其实并不那么简单,因为服务规划并不是简单的四则运算。因为其相关指标之间是有相互影响的,会需要做些权衡取舍。
为了达到以上两个目标,压力测试的一般步骤可以这样做:

  1. 确定服务能达到的最小响应时间。这个一般是在低压力情况下得出。
    • 同时,根据最低响应时间,我们可以大体计算出单个用户,单服务节点的最大QPS。
  2. 在高并发的情况下,测算出单个服务节点所能承载的最大QPS。
  3. 通过模拟真实请求的使用模式,测算出目标响应时间下的最大并发用户数。
  4. 验证上述最大并发用户数下,长时间运行的服务稳定性。同时系统负载小于60%(后面解释)

真实请求模式的模拟

这一点非常重要。因为测试时请求模式的不同,会导致压力测试的结果的差异非常大,尤其是系统响应时间。请求模式真实与否的主要考虑点有以下三个:

  • 请求发出的时间点,是否符合平均分布。而不是出现一个个的请求量尖峰。
  • 每个连接发出的请求间,是否有合理的间隔(也称为思考时间)。而不是全火力发请求。
  • 模拟过程中,系统负载也要控制在合理范围内

我们做这样的一个最理想情况下的思想实验。假设一个接口的响应时间是1ms,支持的并发量为1(即一次只能处理一个请求),用1000个请求来做压力测试。我们来讨论一下两种不同的请求发送模式:

  • 模式一:假想这1000个请求是每隔1ms发一个过来。那么每个请求发来的时候,上一下都刚好处理完,新请求都会毫无延迟地得到处理。从客户端看来,这1000个请求的响应时间都是1ms。
  • 模式二:假想这1000个请求是在一开始一起发到服务器端。第一个请求会被及时处理,之后每个请求的等待时间都比前一个多1ms,到第1000个请求,要等999ms才会被开始处理。从客户端看来,这1000个请求,虽然和模式一一样,也是在1秒内全部处理完成,但是平均响应时间是500.5ms。

可以看到,在上面的两种不同请求模式中,最终测试出来的响应时间相差500倍。当然,这个例子是理想情况下的两个极端。现实中的差别不会这么大,但是也非常可观。由于压力测试脚本产出的请求模式的不同,在结果上产生几十倍差距的情况也是屡见不鲜的。

系统负载与响应时间的关系

在上一个思想实验的模式一中,我们发送请求是均匀分布的,但是更真实的分布,是时间齐次的一维泊松分布(一种随机分布)。有什么不同呢?请求平均分布的情况下,我们可以想象,只要负载没达到100%,响应时间就应该一直是1ms。但是在真实的情况下,我们这个假想接口的响应时间会是系统负载的一个函数。关系函数如下(其中,s表示请求处理时间,p表示系统使用率,r表示响应时间。):

r=s(2-p)/2(1-p) 

函数图像如下:

响应时间随负载的变化

我们可以看到,负载在80%的时候,响应时间就会是三倍于处理时间,而负载在90%以上时,延迟就会直接彪高。

注意这个公式并不具有普遍适用性,只能用于等候理论中,符合M/D/1型队列的情况,而且是假设服务器本身的性能不会因为负载的变化而变化。而真实的情况,会比这个更加复杂,服务接口的处理时间,常常本身也是负载的一个函数。通过这个分析及图表想要说明的是,如果想要保证系统的延迟在某个范围内,就要确保服务本身的负载不能过高。

一开始说,压力测试的一个目标是验证服务的技术指标是否达标,但是技术指标有很多,不同指标的压力测试方式也是不同的。不可能用一次压力测试,把所有的指标都能验证完成。如果想测试系统延迟是否达标,就要在测试过程中保重负载不过高。要知道,实际生产环境中,一般负载大于60%就要开始着手扩容了。根本不允许负载过高的情况出现。压力测试,还可以验证,在负载小于某个点时,比如60%,能撑住多大的量。用100%去测试,但是生产又不跑到100%,测试的效果就要打折扣。100%负载的压力测试当然也有用,可以用来验证服务在高强负载的情况下,服务的可用性、稳定性是否依然符合要求,不能宕机,不能无响应,不出错,基本上就可以说稳定性就是好的,但是这个时候的系统延迟是不可能好看的。

对压力测试一个常见的误解就是:压力测试嘛,时时刻刻的负载都得是100%才叫压力测试。

集群测试的目的

到4为止,对单台服务器的测试基本完成。后面就可以开始集群测试了。集群测试的目的是,验证服务的水平可扩展性是否良好。水平可扩展性良好的服务,加一倍的容量,就能多承载一倍的负载。水平可扩展性不好的服务,多加多少节点,都没办法支撑更多的负载。造成水平扩展不良的的原因,一般都是服务内部有核心操作串行化。更常见的情况是:加一倍的容易,可以多承载X倍的负载(其中因子X小于1)。对集群进行压力测试的主要目标,就是测算出这个X因子的值。以用于服务容量规划。在实际的规划过程中,会有两个方向:

  1. 保目标响应时间。一般用于对响应时间要求高的系统。就直接用最佳并发用户数去做规划就好。最最理想的情况下,如果1台能支撑100用户,10台能支持最多1000用户。
  2. 保每台最大并发用户数,以节省服务器资源。如果可接受的响应时间比较高,那么每台服务器就可以承载更多的用户。这时需要再进行一次,以测算单台服务器的最大并发用户数为目标的压力测试。并以这个最大并发用户数去做容量规划。

所有服务容量规划的前提,都是服务本身是可以水平扩展的。

下面以K6压力测试的结果为例,简单演示一下以上过程。

示例测试环境

Mock的被测试服务接口如下所示。没有数据库访问。没有同步。这样做的主要目的是简化模型。现实情况的结果,会比下面的演示复杂得多。

@GetMapping("/say")
fun sayHello(@RequestParam greeting: String): Mono<Any> {
    return Mono.just(greeting)
}

运行环境:Docker 4 Core 4GB on Mac Book Pro with 2.8G i7 CPU 同环境还运行:Spring Cloud Gateway, Prometheus, Grafana, InfluxDb及K6 Test Engine。

最小响应时间的测量

测试方法:使用少量用户(比如一两个),无思考时间(全负荷)发请求。

期望结果:响应时间保持极低水平。QPS也要足够高。

如下图所示:

平均响应1ms,95%分位1.5ms。后文为便于计算,最低响应时间记为1ms。 下图中,QPS最高1500。但是可以想象,这并不是这台服务的最大QPS,毕竟用户太少了。 根据响应时间,我们大体可以知道,单个用户,在单个服务节点上,最大QPS是1000。

最大QPS的测量

测试方法:使用大量用户(使用10,30,100,300每别测算),无思考时间发请求。

期望结果:一开始增加并发用户量,可以显著提高QPS压力,但是用户增加到几十个(甚至几个)之后,QPS基本就上不去了。

如下图所示:

我们用100个高并发用户去压,并没有把QPS压到100*1000这个量级。我们发现最高也就到3200QPS,就上不去了。而且服务的响应延迟显著增加。这是非常正常的。因为无思考时间的用户,等同于在做DoS攻击。正常用户的正常API调用,是不可能出现这种全负载狂发请求的情况的。如果你说你的API特殊,就是每个用户要1秒调用1000次以上,那你其实最好重新思考下这个设计是否合理。

真实负载下的并发用户数的测算

上面我们已经讨论过什么是真实负载。于是在这个压力测试中,我们要引入思考时间这个概念。

测试方法:从0开始,逐渐增加用户数到200。加入50ms的思考时间(即每个用户,每秒最多发20个请求)

期望结果:观察响应时间的增长情况,用户量达到某个点后,响应时间开始快速增长。这个点,即是单台服务所能承载的并发用户数。当然,如果你对响应时间的要求不太高,也可以根据某个特定响应时间,比如200ms,来确定并发用户数。而不是非得用响应时间的增长拐点。

如下图所示:

用户在达到100左右开始,响应时间开始变高,QPS也不再随着VU线性增长。所以我们可以说对于这个服务节点来说,最好只用来支持100个用户。但是你也可以发现,即使到200并发用户,系统也还是OK的。只是延迟比较高。

这里的压力测试中使用的假设的思考时间的长短,是非常影响并发用户数测算的。如果我们把思考时间加到500ms,就可以支持更多的VU,因为QPS一般是固定的。下图展示了600VU在500ms思考时间下的测算结果。是不是看上去比上面100用户的时候还好?(注意,QPS都是1000左右。)