0%

本文首发于 凌云时刻公众号。

当你想实现一个命令行程序时,或许第一个想到的是用 Python 来实现。比如 CentOS 上大名鼎鼎的包管理工具 yum 就是基于 Python 实现的。

而 Python 的世界中有很多命令行库,每个库都各具特色。但我们往往不知道其背后的设计理念,也因此在选择时感到迷茫。这些库的作者为何在重复造轮子,他是从哪个角度来考虑,来让命令行库“演变”到一个新的更好用的形态。

为了能够更加直观地感受到命令行库的设计理念,在此之前,我们不妨设计一个名为 calc 的命令行程序,它能:

  • 支持 echo 子命令,对输入的字符串做处理来输出
    • 若不提供任何选项,则输出原始内容
    • 若提供 --lower 选项,则输出小写字符串
    • 若提供 --upper 选项,则输出大写字符串
  • 支持 eval 子命令,针对输入调用 Python 的 eval 函数,将结果输出(作为示例,我们不考虑安全性问题)
阅读全文 »

一、背景

最近做云服务 API 测试项目的过程中,发现某些时候会大批量调用 API,从而导致限流的报错。在遇到这种报错时,传统的重试策略是每隔一段时间重试一次。但由于是固定的时间重试一次,重试时又会有大量的请求在同一时刻涌入,会不断地造成限流。

这让我回想起两年前在查阅Celery Task 文档的时候发现可以为任务设置 retry_backoff 的经历,它让任务在失败时以 指数退避 的方式进行重试。那么指数退避究竟是什么样的呢?

二、指数退避

根据 wiki 上对 Exponential backoff 的说明,指数退避是一种通过反馈,成倍地降低某个过程的速率,以逐渐找到合适速率的算法。

在以太网中,该算法通常用于冲突后的调度重传。根据时隙和重传尝试次数来决定延迟重传。

在 c 次碰撞后(比如请求失败),会选择 0 和 $2^c-1$ 之间的随机值作为时隙的数量。

  • 对于第 1 次碰撞来说,每个发送者将会等待 0 或 1 个时隙进行发送。
  • 而在第 2 次碰撞后,发送者将会等待 0 到 3( 由 $2^2-1$ 计算得到)个时隙进行发送。
  • 而在第 3 次碰撞后,发送者将会等待 0 到 7( 由 $2^3-1$ 计算得到)个时隙进行发送。
  • 以此类推……

随着重传次数的增加,延迟的程度也会指数增长。

说的通俗点,每次重试的时间间隔都是上一次的两倍。

三、指数退避的期望值

考虑到退避时间的均匀分布,退避时间的数学期望是所有可能性的平均值。也就是说,在 c 次冲突之后,退避时隙数量在 [0,1,...,N] 中,其中 $N=2^c-1$ ,则退避时间的数学期望(以时隙为单位)是

$$E(c)=\frac{1}{N+1}\sum_{i=0}^{N}{i}=\frac{1}{N+1}\frac{N(N+1)}{2}=\frac{N}{2}=\frac{2^c-1}{2}$$

那么对于前面讲到的例子来说:

  • 第 1 次碰撞后,退避时间期望为 $E(1)=\frac{2^1-1}{2}=0.5$
  • 第 2 次碰撞后,退避时间期望为 $E(2)=\frac{2^2-1}{2}=1.5$
  • 第 3 次碰撞后,退避时间期望为 $E(3)=\frac{2^3-1}{2}=3.5$

四、指数退避的应用

4.1 Celery 中的指数退避算法

来看下 celery/utils/time.py 中获取指数退避时间的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_exponential_backoff_interval(
factor,
retries,
maximum,
full_jitter=False
):
"""Calculate the exponential backoff wait time."""
# Will be zero if factor equals 0
countdown = factor * (2 ** retries)
# Full jitter according to
# https://www.awsarchitectureblog.com/2015/03/backoff.html
if full_jitter:
countdown = random.randrange(countdown + 1)
# Adjust according to maximum wait time and account for negative values.
return max(0, min(maximum, countdown))

这里 factor 是退避系数,作用于整体的退避时间。而 retries 则对应于上文的 c(也就是碰撞次数)。核心内容 countdown = factor * (2 ** retries) 和上文提到的指数退避算法思路一致。
在此基础上,可以将 full_jitter 设置为 True,含义是对退避时间做一个“抖动”,以具有一定的随机性。最后呢,则是限定给定值不能超过最大值 maximum,以避免无限长的等待时间。不过一旦取最大的退避时间,也就可能导致多个任务同时再次执行。更多见 Task.retry_jitter 。

4.2 《UNIX 环境高级编程》中的连接示例

在 《UNIX 环境高级编程》(第 3 版)的 16.4 章节中,也有一个使用指数退避来建立连接的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "apue.h"
#include <sys/socket.h>

#define MAXSLEEP 128

int connect_retry(int domain, int type, int protocol,
const struct sockaddr *addr, socklen_t alen)
{
int numsec, fd;

/*
* 使用指数退避尝试连接
*/
for (numsec = 1; numsec < MAXSLEEP; numsec <<= 1)
{
if (fd = socket(domain, type, protocol) < 0)
return (-1);
if (connect(fd, addr, alen) == 0)
{
/*
* 连接接受
*/
return (fd);
}
close(fd);

/*
* 延迟后重试
*/
if (numsec <= MAXSLEEP / 2)
sleep(numsec);
}
return (-1);
}

如果连接失败,进程会休眠一小段时间(numsec),然后进入下次循环再次尝试。每次循环休眠时间是上一次的 2 倍,直到最大延迟 1 分多钟,之后便不再重试。

总结

回到开头的问题,在遇到限流错误的时候,通过指数退避算法进行重试,我们可以最大程度地避免再次限流。相比于固定时间重试,指数退避加入了时间放大性和随机性,从而变得更加“智能”。至此,我们再也不用担心限流让整个测试程序运行中断了~

一、前言

就在本周,字典合并特性(PEP 584)的提交被合入了 CPython 的主干分支,并在 2020-02-26 发布了 Python 3.9.0a4 预览版本。

那什么是字典合并操作符呢?在回答这个问题前,我们不妨回忆下集合的合并操作。当我们想要对两个结合做合并操作时,会怎么做呢?

1
2
3
4
5
6
7
>>> s1 = {1, 2}
>>> s2 = {2, 3}
>>> s1 | s2 # s1 和 s2 取并集,生成新的集合;与 s1.union(s2) 等价
{1, 2, 3}
>>> s1 |= s2 # s1 和 s2 取并集,并更新到 s1 上;与 s1.update(s2) 等价
>>> s1
{1, 2, 3}

类似地,我们希望 Python 中的字典能像集合一样,使用 | 和 |= 作为合并操作符,以解决我们在过去合并字典时感受到的“痛苦”,于是就有了 PEP 584。

今天就想和大家聊聊这个提案,不仅是要了解字典合并操作符的前世今生,更是要学习提案作者以及参与者是如何对引入一个新特性的思考,辩证性地分析利弊,最终确定引入。最后还想和大家分享下在 CPython 层面是如何实现的。

阅读全文 »

一、前言

昨天发现了一款非常不错的云系统架构原型图制作库 Diagrams,通过它,我们便可以使用代码的方式绘制诸如阿里云、AWS、Azure、K8S 等系统架构原型图。

相比于在 UI 上对各种图标进行拖拽和调整,这种方式更符合我们程序员的使用习惯。

本文不仅要介绍下这个库,也想说说我是如何参与到这个库中以支持阿里云资源。

二、安装

Diagrams 使用 Graphviz 来渲染图表,在安装 diagrams 之前需要先安装 Graphviz。

macOS 用户(如果使用 Homebrew)可以使用 brew install graphviz 的方式来安装 Graphviz。

安装 diagrams 的方式有多种,通过 pip、pipenv 和 poetry 均可:

script
1
2
3
4
5
6
7
8
# 使用 pip (pip3)
$ pip install diagrams

# 使用 pipenv
$ pipenv install diagrams

# 使用 poetry
$ poetry add diagrams
阅读全文 »

一、前言

在近半年的 Python 命令行旅程中,我们依次学习了 argparse、docopt、click 和 fire 库的特点和用法,逐步了解到 Python 命令行库的设计哲学与演变。
本文作为本次旅程的终点,希望从一个更高的视角对这些库进行横向对比,总结它们的异同点和使用场景,以期在应对不同场景时能够分析利弊,选择合适的库为己所用。

1
2
本系列文章默认使用 Python 3 作为解释器进行讲解。
若你仍在使用 Python 2,请注意两者之间语法和库的使用差异哦~

二、设计理念

在讨论各个库的设计理念之前,我们先设计一个计算器程序,其实这个例子在 argparse 库的第一篇讲解中出现过,也就是:

  • 命令行程序接受一个位置参数,它能出现多次,且是数字
  • 默认情况下,命令行程序会求出给定的一串数字的最大值
  • 如果指定了选项参数 --sum,那么就会将求出给定的一串数字的和

希望从各个库实现该例子的代码中能进一步体会它们的设计理念。

阅读全文 »

前言

2019 年底开始蔓延的新型肺炎疫情牵动人心,作为个体,我们力所能及的就是尽量待在家中少出门。

看到一些朋友叫设计同学帮忙给自己的头像戴上口罩,作为技术人,心想一定还有更多人有这样的诉求,不如开发一个简单的程序来实现这个需求,也算是帮助设计姐姐减少工作量。

于是花了些时间,写了一个叫做 face-mask 的命令行工具,能够轻松的给图片中的人像戴上口罩,而且口罩的方向和大小都是适应人脸的哦~

使用

安装 face-mask

确保 Python 版本在 3.6 及以上

1
pip install face-mask

使用 face-mask

直接指定图片路径即可为图片中的人像戴上口罩,并会生成一个新的图片(额外有 -with-mask 后缀):

1
face-mask /path/to/face/picture

通过指定 --show 选项,还可以使用默认图片查看器打开新生成的图片:

1
face-mask /path/to/face/picture --show
阅读全文 »

引言

Python 的 “is” 和 “==” 想必大家都不陌生,我们在比较变量和字面量时常常用到它们,可是它们的区别在哪里?什么情况下该用 is?什么情况下该用 ==?这成了不少人心中的困惑。

当我们判断一个变量是否为 None 时,通常会用 is:

1
2
3
4
5
6
>>> a = None
>>> a is None
True
>>> b = 1
>>> b is None
False

而当我们判断一个变量是否为字面量(比如某个数值)时,通常会用 ==:

1
2
3
4
5
>>> a = 0
>>> a == 0
True
>>> a == 1
False

要想解决上面的疑惑,我们首先需要搞明白 is 和 == 是什么。

阅读全文 »

新鲜事儿

本周没有新鲜事儿。

好文共赏

2020 年后的软件开发人员趋势

Python 进度条 tqdm 示例

进度条是程序开发中一个不可获取的功能,网上关于 Python 进度条库 tqdm 的示例说的并不友好,本文将深入浅出地带你如何使用 tqdm 实现进度条功能。

为什么 Python 3.8 给 “is” 打印 SyntaxWarning?

阅读全文 »

一、前言

在前面三篇介绍 fire 的文章中,我们全面了解了 fire 强大而不失简洁的能力。按照惯例,我们要像使用 argparse、docopt 和 click 一样使用 fire 来实现 git 命令。

本文的关注点并不在 git 的各种命令是如何实现的,而是怎么使用 fire 去打造一个实用命令行程序,代码结构是怎样的。因此,和 git 相关的操作,将会使用 gitpython 库来简单实现。

为了让没读过 使用 xxx 实现 git 命令(xxx 指 argparse、docopt 和 click) 的小伙伴也能读明白本文,我们仍会对 git 常用命令和 gitpython 做一个简单介绍。

阅读全文 »