无处不在的幂等性

2021-10-28 14:17:24 浏览数 (1)

0. 引子


最近接手一个项目,基于Airflow实现ETL的功能。问题是这个ETL经常出问题,然后就是修数据,虽然有Airflow的优势,但是还是相当的烦人。我们项目都是基于Docker进行部署的,原来的启动方式是这样的:

代码语言:javascript复制
# 启动一个后台容器
sudo docker run -dti --restart always --name airflow -p 10101:8080 
    -v /root/services/airflow:/airflow 
    -v /data/logs/airflow:/airflow/logs 
    -e C_FORCE_ROOT=True 
    registry.cn-hangzhou.aliyuncs.com/ibbd/airflow 
    bash /service.sh
    
# 然后通过docker exec来分别启动Airflow的调度器和worker
# 大概脚本如下:
sudo docker exec -tid airflow bash start-scheduler.sh
sudo docker exec -tid airflow bash start-worker.sh

问题是scheduler进程或者worker进程经常自己就挂掉了,很可能是因为客户的服务器配置资源不足导致的。开始处理这个问题就是写监控脚本,监控进程,但是问题依然是没有完全避免,有时监控脚本也因为莫名的原因没有启动成功。

前些天把启动方式修改成了如下的方式:

代码语言:javascript复制
# 启动调度器
sudo docker run -dti --restart always --name airflow-scheduler 
    -v /root/services/airflow:/airflow 
    -v /data/logs/airflow:/airflow/logs 
    -e C_FORCE_ROOT=True 
    registry.cn-hangzhou.aliyuncs.com/ibbd/airflow 
    airflow scheduler
    
# 启动worker
sudo docker run -dti --restart always --name airflow-worker 
    -v /root/services/airflow:/airflow 
    -v /data/logs/airflow:/airflow/logs 
    -e C_FORCE_ROOT=True 
    registry.cn-hangzhou.aliyuncs.com/ibbd/airflow 
    airflow worker

# 启动webserver(需要的时候才启动即可)
# sudo docker run -dti --restart always --name airflow-webserver -p 10101:8080 
sudo docker run -ti --rm --name airflow-webserver -p 10101:8080 
    -v /root/services/airflow:/airflow 
    -v /data/logs/airflow:/airflow/logs 
    -e C_FORCE_ROOT=True 
    registry.cn-hangzhou.aliyuncs.com/ibbd/airflow 
    airflow webserver -p 8080

非常干净利落地解决了问题,利用docker的restart always就能自动实现我们所需要的功能。而且还有个非常好的好处:

随时可以干掉某个容器进行重启!

这是个非常好的特性,不正是类似我们经常所追求的幂等性吗?

1. 关于幂等性


维基百科上,关于幂等性的介绍有:

在数学里,幂等有两种主要的定义。

  1. 在某二元运算下,幂等元素是指被自己重复运算(或对于函数是为复合)的结果等于它自己的元素。例如,乘法下唯一两个幂等实数为0和1。
  2. 某一元运算为幂等的时,其作用在任一元素两次后会和其作用一次的结果相同。例如,高斯符号便是幂等的。

https://zh.wikipedia.org/wiki/冪等

在IT工程领域,因为组件或者模块不可避免的不可靠性(编程领域有一个至理名言就是:所有可能会发生的,最终一定会发生),所以幂等性就变得非常重要,在设计工程中往往是需要重点考虑的。例如上面引子提到的容器启动也是一个例子,无论执行多少次启动脚本,结果都是一样的,而不会产生额外的副作用。

2. 幂等性的应用


幂等性在IT工程设计领域几乎无处不在,如果在设计和实现上保持了幂等性,那么你的系统的健壮性往往是很好的,维护也简单。除了上面提到的容器启动设计,常见的还有:

2.1 接口设计

接口设计是我们经常碰到的工作,但是我们对于接口的假设往往是,因为各种各样的原因,我们的接口出现异常的情况是不可避免的,因此我们设计的重点并不是完全杜绝接口出问题,而是接口出问题之后,我们能否再执行一次该接口,直到成功。

这就要求我们的接口应该尽可能是保持幂等性的。例如注册用户,如果每次提交都往数据库插入记录,那就乱套了,而是插入前应该判断数据是否已经存在了。

当然这是非常简单的情况,如果这个都不懂,那他可能还没入门。复杂一点的情况,例如上传文件功能接口怎么保持幂等性,几乎可能肯定很多刚入门的工程师都没考虑过这个问题,甚至有些入门多年的工程师也不考虑这个问题。

当然可能并非所有接口都能实现幂等性的,但是很显然,我们遇到的大部分都是可以幂等性的。

这在http接口设计上大家可能还是比较有感觉,但是在平时实现功能接口时可能就不太注意了,很多初入门者,为了方便,往往定义了很多全局变量,实现的函数是有副作用的,相同的输入,可能得不到相同的输出,这通常会使得维护变得糟糕。这也是这些年函数式编程咸鱼翻生的根本原因,相对于面向对象的编程,函数式编程更加容易保持幂等性,因为面向对象编程时大家很容易去修改类的属性,这样很容易导致再一次执行时就没法保持幂等性了。

2.2 Airflow的任务Task设计

Task的耗时往往是比较长的,通常比接口更不可靠,因此Task的幂等性就更加重要,也就是说,Task应该随时经受重启的考验,这样能大大降低维护的难度,出问题往往只要重启即可。

2.3 模块设计架构设计

一个系统可能很庞大,如果没有合理的模块划分,那很可能会是一个灾难。但是哪些功能应该划分到相同的模块,这就非常考验能力了,通常这也是工程师水平能力的最重要体现。不同模块之间的交互应该是具有幂等性的(并不是所有情况都能满足),不同模块之间如果乱成一团,那肯定是一个灾难的开始。

2.4 页面设计

这里只说一个经常看到的情况,就是页面跳转的设计。我发现大多数前端工程师都不会关注这个问题,可能大家觉得这只是一个小问题,不过我觉得页面URL如果不具备幂等性,那体验会变得有点糟糕。

例如我在一个网站内浏览时,可能点击了很多链接,但是当我点击到某个页面时,页面却迟迟加载不出来,这种情况是经常会发生的,这时我的操作很可能是直接刷新页面,点击浏览器的刷新按钮(这个是很合理的操作)。对于不具备幂等性的设计,这个时候,刷新可能就跑回到某个页面了,而不是我当前正在浏览的页面。当然,需要幂等性的并不是只有这个场景,例如我要将某个页面分享给其他人,或者向其他人求助等。

这个问题在现在越来越流行单页面应用之后,越来越突出了。

这个问题同样容易出现在桌面软件或者手机APP上,一个APP因为各种原因挂掉之后,比较好的体验应该是我重启之后还能回到挂掉之前的状态,但是显然大部分都不具备这样的特性,而只是简单的回到了首页。

2.5 状态设计

这又是一个更加容易被忽略的情况,但是状态设计往往是系统架构设计中关键的一环,通常弄清楚了状态的转化过程,那么你的业务系统可能就相当清晰了。例如常见的登陆状态,我见过有人将登陆的状态信息保存在服务器的文件系统中,这是非常糟糕的设计,因为依赖了一个本地的文件系统,情况要是有变化可能就很难保持幂等性。例如换服务器,或者增加了服务器。

好的实现方式应该是保持在公共的redis等缓存里,更好的方式我觉得是加密之后写到token里,请求时带上token。

在分布式应用中,幂等性会变得更加重要。有一个典型的例子,在设计数据表的主键时,可能不少人都会使用自增的ID作为主键,因为简单。但是自增ID本身是不具备幂等性的,每次插入都会有一个新的ID。而在分布式的高并发场景下,自增ID的麻烦就更大了,因为并发的代价比较大。现在也会有不少开源的全局ID生成算法,都是为了解决这一问题而生的。

3. 不能过度设计


幂等性很好,但是还是不能过度设计,有些接口或者模块可能就很难保证幂等性,过度设计只会增加系统复杂度,这是违背幂等性的初衷的。例如如果系统的并发很小,那自增主键也完全没有问题。

幂等性应该是工程设计领域都会遇到的问题,不止是在软件领域,产品模块如果都遵循幂等性,那维护成本会低很多。

写于2020-09-13

0 人点赞