我是一个 Python 程序员,也对 Lua,Go,Rust 感兴趣。目前在蚂蚁金服工作。

我的日常工作离不开 Vim 和 Tmux,当然还有 Git。平时喜欢在看 Github 上有趣的项目,学习新的东西,看书,写博客等。博客的内容一般是我的人生感悟,观后感读后感,以及技术方面的内容。

我的梦想是让网络变得更加开放、自由和快速。

声明:本博客内容仅代表本人观点,和我的雇主无关。本站所有内容未加说明均为原创,一经发布自动进入公共领域,本人放弃所有权利。

厦门游记

今年的五一假期有四天,于是就跑去厦门玩了,这几天正好天气也不错,好像早早的迎来了今年的夏天一样。

厦门是一个岛(我竟然出发前才知道,之前一直以为是福建省的一个沿海城市),岛最外圈是一整圈马路,叫环岛路,很漂亮。一圈大约30km,本来想骑一圈的,但是每天都很累,后来选择坐观光巴士看了一圈。

说起来厦门市和我的家乡青岛很像,虽然青岛只是在一个半岛上。但是厦门有沙滩,青岛也有;厦门地势不平,很多坡,很多隧道,青岛也是;厦门吃的基本是就海鲜,青岛也是。但是房价倒是比青岛高的多,我以为厦门也就是个二线城市的样子,没想到房价跟上海差不多。市中心要10万/平左右,最低也要5万。应该是岛上的资源有限吧,就这么大的地方,盖完了就没了。青岛有地方填过海造陆地,厦门也填过,但是已经不能再填了,沿海的地方已经填的很深了。最近几年厦门一直将工厂往岸上迁移,现在岛上的大部分工厂已经迁到对岸了,所以厦门的空气非常好。听一个司机说,岛上的机场也准备迁到另一个岛上去,因为机场在厦门主岛上一是占地方,二是周围的楼层要限高。嘿,这也跟青岛一样,现在我们那边的流亭机场要迁到胶州去。司机还说厦门很排外,不知道真假,但是看起来肯德基,星巴克,麦当劳这种连锁店在厦门很少见到,厦门的连锁店都是我之前没见到的一些牌子(厦门人见了不要伤心,司机说的不一定对)。

厦门的位置基本就在台湾岛的正对面,所以会有很多台湾文化的影响。南边有一个沙滩,就叫“一国两制沙滩”,路上还写着“一国两制,统一中国”。岛上有一个中心干道叫“成功大道”,一开始以为就是一个普通的名字,后来想想应该是纪念郑成功的。鼓浪屿上还有一个皓月园,是专门为纪念郑成功建的,名字取自“思君寝不寐,皓月透素帏”。里面有一个郑成功大雕像,是郑成功的10倍高。

说起吃的,倒觉得厦门没有什么特别好吃的东西。网上搜到的著名小吃沙茶面、花生汤、土笋冻都觉得很一般,去了大名鼎鼎的醉一号吃海鲜,也觉得一般,食材倒是感觉挺新鲜的。中山路步行街、局口街小吃、八市这些地方,跟全国各地的小吃街都差不多,都买油炸香肠、臭豆腐什么的,没有什么特别好吃的。我估计曾厝垵应该也是类似这种东西,所以就直接没去,不太喜欢这种商业化的小吃街。大众点评有一家很有名的惠源面包店,可惜我12:30去的,小哥跟我说现在啥都没有,要15:00才能拷出来。好吧。

倒是现在的榴莲很便宜,买一整个的话是 15/斤,也非常好吃。

鼓浪屿是离厦门主岛很近的一个小岛,挺漂亮的,可惜现在控制上岛人数,每天5w人,船票就很难抢(游客的码头和居民的码头是分开的),我只抢到 16:30 上岛的(本地司机跟我说他小时候都游过去)。上去之后就去了日光岩看日落,非常美。然后天就黑了,在岛上随便溜达了几圈,晚上坐在海边玩了一会就回去了。鼓浪屿上面猫特别多,印象深刻,很多旅馆饭店名字都带“猫”。去的时候又是春天,到了晚上这个猫叫,此起彼伏。

厦门大学旁边有一个西村,简直太大学了,好像所有的大学附近都有这么一个地方:各种小店门头林立,装修散发出廉价又想文艺的气息,价格便宜味道普通,但是都有一个文艺的名字。在这里同样也没找到啥好吃的。

铁路文化公园很漂亮,废弃的铁路改造的狭长的公园。我还是第一次见这么长形状的公园。

植物园、南普陀这些地方还没来得及去。

几点到厦门的旅行建议:

  • 坐飞机非常方便,高崎机场现在离市中心很近(离我们住的酒店就4km),高铁站倒是挺远的,所以建议坐飞机来
  • 提前做好攻略,提前准备,非常重要!下面是一些必须提前预定的东西
    • 鼓浪屿限制上岛人数,五一不提前三五天基本搞不到票,我们只买到下午上岛的了,尽量提前看票;飞猪有旅行社可以帮你抢,他们成功率高一些,但是票贵,2-3倍的价格;
    • 环岛巴士在网上(美团大众点评、飞猪等)订票的话,要付钱之后2小时才能出票,所以不要想着去了再在网上买票,要提前2小时买。实在不行现场买也可以,网上买45/人,现场买60/人。早上8:30 – 下午16:40 不限次数随便上下。
    • 厦门大学控制入校游客数,不要想着混进去,要提前预约。
  • 不要让司机推荐你去哪玩,你问10个司机,9个告诉你去坐帆船。帆船并不好玩,他们拿回扣的。

下面是照片的分割线。


环岛路

观光巴士风景

对面就是鼓浪屿

厦门路边种了很多这样的花,美

环岛路高架

厦门大学附近的“西村”

西村里面的猫文化

铁路公园,晚上拍的,不是很好

沙滩

海边

杂乱的局口小吃街

厦门地铁

中山步行街

八市

第一次体验缆车

车上没有玻璃,门就一个栓子插着,感觉挺危险,挂个风我就掉下去了。

去之前我还专门学习了缆车的结构

好在不是很高,下面都是树木

大名鼎鼎的惠源面包店,可惜我去的时候说还没烤好,要下午三点。隐藏在八市中的看起来非常不起眼的一家小店。

鼓浪屿上面的猫,看起来挺抑郁。猫又出不了岛,应该跟岛上其他的猫都认识吧。

日光岩18:00之后票价半价,现在日落时间是18:35左右,正好上去看日落

俯瞰厦门市

等待日落

鼓浪屿码头,游客专用

福建土楼,田螺坑(四菜一汤)

土楼

土楼2

《大鱼海棠》里面的土楼

土楼3

土楼4

这是我女朋友

土楼的照片是我女朋友用 iPhone Xs Max 拍摄;其余照片是 iPhone 6s 拍摄。图片经过了压缩。

 

Python 为什么这么慢?

Python 在近几年变得异常流行,Python 语言学习成本低,写出来很像伪代码(甚至很像英语),可读性高,等等有很多显而易见的有点。被 DevOps, Data Science, Web Development 各种场景所青睐。但是这些美誉里面从来都没有速度。相比于其他语言,无论是 JIT 的,还是 AOT 的,Python 几乎总是最慢的。导致 Python 的性能问题的有很多方面,本文尝试谈论一下这个话题。

TL;DR

  1. Python 有 GIL
  2. Python 是一种“解释型”语言
  3. Python 是动态类型的语言

GIL

现代计算机处理器一般都会有多核,甚至有些服务器有多个处理器。所以操作系统抽象出 Thread,可以在一个进程中 spawn 出多个 Thread,让这些 Thread 在多个核上面同时运行,发挥处理器的最大效率。(在 top 命令里面可以看到系统中的 threads 数量)

所以很显然,在编程时使用 Thread 来并行化运行可以提升速度。

但是 Python (有时候)不行。

Python 是不需要你手动管理内存的(C 语言就需要手动 malloc/free),它自带垃圾回收程序。意思是你可以随意申请、设置变量,Python 解释器会自动判断这个变量什么时候会用不到了(比如函数退出了,函数内部变量就不用到了),然后自动释放这部分内存。实现垃圾回收机制有很多种方法,Python 选择的是引用计数+分代回收。引用计数为主。原理是每一个对象都记住有多少其他对象引用了自己,当没有人引用自己的时候,就是垃圾了。

但是在多线程情况下,大家一起运行,引用计数多个线程一起操作,怎么保证不会发生线程不安全的事情呢?很显然多个线程操作同一个对象需要加锁。

这就是 GIL,只不过这个锁的粒度太大了,整个 Python 解释器全局只有一个 Thread 可以运行。详见 dabeaz 博客

绿色表示正在运行的线程,一次只能有一个

因为其他语言没有 GIL,所以很多人对 GIL 误解。比如:

“Python 一次只能运行一个线程,所以我写多线程程序是不需要加锁的。” 这是不对的,“一次只能运行一个线程”指的是 Python 解释器一次只能运行一个线程的字节码(Python 代码会编译成字节码给Python虚拟机运行),是 opcode 层面的。一行 Python 代码,比如 a += 1 ,实际上会编译出多条 opcode:先 load 参数 a,然后 a + 1,然后保存回参数 a。假如 load 完成还没计算,这时候线程切换了,其他线程修改了 a 的值,然后切换回来继续执行计算和存储 a,那么就会造成线程不安全。所以多线程同时操作一个变量的时候,依然需要加锁。

“Python 一次只能运行一个线程,所以 Python 的多线程是没有意义的。” 这么说也不完全对。假如你要用多线程利用多核的性能,那 Python 确实不行。但是假如 CPU 并不是瓶颈,网络是瓶颈,多线程依然是有用的。通常的编程模式是一个线程处理一个网络连接,虽然只有一个线程在运行,但其他线程都在等待网络连接,也不算“闲着”。简单说,CPU 密集型的任务,Python 的多线程确实没啥用(甚至因为多线程切换的开销还会比单线程慢),IO 密集型的任务,Python 的多线程依然可以加速。

这么说可能比较好理解:无论你的电脑的 CPU 有多少核,对 Python 来说,它只用 1 个核。

其他的 Python Runtime 呢?Pypi 有 GIL,但是可以比 CPython 快 3x。Jython 是基于 JVM 的,JVM 没有 GIL,所以 Jython 依然 JVM 的内存分配,它也没有 GIL。

其他语言呢?刚刚说了 JVM,Java 也是用的引用计数,但是它的的 GC 是 multithread-aware 的,实现上更复杂一些(有朋友跟我说 Java 已经不是引用计数了,这个地方请读者注意,附一个参考资料)。JavaScript 是单线程异步编程的模式,所以它没有这个问题。

作为一个解释型的语言……

像 C/C++/Rust 这些语言直接编译成机器码运行,是编译型语言;Python 的运行过程是虚拟机读入 Python 代码(文本),词法分析,编译成虚拟机认识的 opcode,然后虚拟机解释 opcode 执行。但这其实不是最主要的原因,Python import 之后会缓存编译后的 opcode,(pyc 文件或者 __pycache__ 文件夹)。所以读入、词法分析和编译并没有占用太多的时间。

那么真正的慢的是哪一步分呢?就是后面的虚拟机解释 opcode 执行的部分。前期的编译是将 Python 代码编译成解释器可以理解的中间代码,解释器再将中间代码翻译成 CPU 可以理解的指令。相比于 AOT(提前编译型语言,比如C)直接编译成机器码,肯定是慢的。

但是为什么 Java 不慢呢?

因为 Java 有 JIT。即时编译技术将代码分成 frames,AOT 编译器负责在运行时将中间代码翻译成 CPU 可以理解的代码。这一部分跟 Python 的解释器没有太大的区别,依然是翻译中间代码、执行。真正快的地方是,JIT 可以在运行时做优化,比如虚拟机发现一段代码在频繁执行(大多数情况下我们的程序都在反复执行一段代码),就会开始优化,将这段代码用更改的版本替换掉。这是仅有虚拟机语言才有的优势,因为要收集运行时信息。像 gcc 这种 AOT编译器,只能基于静态分析做一些分析。

为什么 Python 没有 JIT 呢?

第一是 JIT 开发成本比较高,非常复杂。C# 也有很好的 JIT,因为微软有钱。

第二是 JIT 启动速度慢,Java 和 C# 虚拟机启动很多。CPython 也很慢,Pypy 有 JIT,它比 CPython 还要慢 2x – 3x。长期运行的程序来说,启动慢一些没有什么,毕竟运行时间长了之后代码会变快,收益更高。但是 CPython 是通用目的的虚拟机,像命令行程序来说,启动速度慢体验就差很多了。

第三是 Java 和 C# 是静态类型的虚拟机,编译器可以做一些假设。(这么说不知道对不对,因为 Lua 也有很好的 JIT)

动态类型

静态类型的语言比如 C,Java,Go,需要在声明变量的时候带上类型。而 Python 就不用,Python 帮你决定一个变量是什么类型,并且可以随意改变。

动态类型为什么慢呢?每次检查类型和改变类型开销太大;如此动态的类型,难以优化。

动态类型带来好处是,写起来非常简单,符合直觉(维护就是另一回事了);可以在运行时修改对象的行为,Monkey Patch 非常简单。

近几年的语言都是静态类型的,比如 Go,Rust。静态类型不仅对编译器来说更友好,对程序员来说程序也更好维护。个人认为,未来是属于静态类型的。

 

阅读资料:

  1. Python 官方 wiki
  2. Removing Python’s GIL: The Gilectomy
  3. David Beazley 有关 GIL 的 Slides:http://www.dabeaz.com/GIL/ ,视频(比较糊,毕竟2010年的)
  4. Gilectomy的最新动态
  5. https://hackernoon.com/why-is-python-so-slow-e5074b6fe55b
  6. https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/
  7. https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/
 

连接池中的连接失效的几种处理方案

在分布式系统中,用连接池缓存住连接,来节省连接反复销毁和创建的成本,是一种很常见的做法。但是在高可用的分布式系统中,”切换”是一个非常普遍的操作,切换就会造成连接池失效的问题。

TCP 虽说是”有连接的”,但这个连接实际上是一个虚拟连接。客户端用一段内存保存连接的五元组(源端口,源IP,目的端口,目的IP,协议),服务端也保存一个这样的五元组,双方就认为有这么一个连接了,可以通过这个连接收发。

但是假如客户端保存有这个五元组,服务器因为某种原因将这个五元组删掉了,就会造成客户端单方面认为有这么一个连接,而服务器不承认(反过来也有这个问题,但是服务器丢失连接的情况更加普遍也更加严重)。当客户端想使用这个连接向服务器发送请求的时候,会被服务器拒绝。

保持一个连接池的做法会使这个问题更加严重,因为连接用完之后不会马上销毁,等待下一次使用,这段时间天知道会发生什么。比如说应用连接数据库这种情况,一般会有连接池保持和数据库的连接。假如 DB 重启了、DB 端因为超时或者其他什么原因把连接关闭了(即把内存的五元组删掉)、DB 出现故障,从一个IP迁移到另一个IP了,都会造成客户端连接池中的连接不可用,这个时候客户端通过这些”坏连接”向DB发送请求,就会失败。(多久时间会返回失败也是一个问题,这受 socketTimeout 参数和 Linux 系统参数影响,具体可见这篇 Datebase timeouts.)

除了DB之外,像 Nginx 保持向后端应用的连接等负载均衡设备通常也有这种问题,只不过有的组件自身选好了方案处理掉了,有的是暴露出来选项让你自己设置。本文讨论处理这种问题的几种思路。

一、失败了就是败了,重建就是

这是成本最低的方式,连接池的实现一般都会包括如果连接不可用,就删除并重建。影响是会失败一些请求(数量=连接池的数量)。

当然有些时候我们是可以预测需要切换DB的,有条件的话可以将业务切换走,然后再切换DB。业务回切之前先对应用发送一波压测流量,用假的业务流量刷掉不用的连接。

二、主动保持

之前在博客中介绍过 TCP Keepalive,可以正好解决这种场景。就是设置一个定时器,定时检查连接是否还可用。

对于 TCP 来说,由于这是一个基于流的协议,所以可以发送一段长度为0的片段来检验是否能收到 ACK。对于 MySQL 来说,要看驱动程序的实现,比如如果实现了 ping 函数可以使用 ping;如果没有 ping 可以使用 SELECT 1。对于 redis 来说也有 ping 命令。Jboss 提供的 background-validation 参数,就是这个原理。

一般来说这个方案是复杂度低,并且比较有效的。缺点是当分布式的连接池巨大的时候,比如上千台机器的话,每台 DB 要承受测活的连接数是 机器数 * 连接池大小,资源消耗是很大的。

三、使用较小的 idle timeout 和 小连接池

连接池一般都提供设置两个参数:

  1. idle timeout:当 idle 时间超过之后,会主动回收;
  2. min connections:连接池最小要保持多少连接;

通过设置 idle timeout,可以保证没有连接是开头说的那种很长时间没用过的连接,因为都会回收掉了。

但是这里会存在一个问题:min connections 要保证连接池有多少个连接,假如设置的是10,平时只用5的话,那么因为 idle timeout 参数会回收5个连接,导致连接池会新建5个新连接。过一段时间,因为有一些用不到又会被回收,就造成了在反复创建连接。

假如采取另一种策略:回收 idle timeout 的连接但是保证连接数要大于 min connections,假如连接数不大于 min connections,那么即使 idle timeout 也不回收。这样可以解决上面的问题,但是会造成无法保证 connection pool 里面的连接都是新鲜的,回到我们最初的问题了。

所以这种方案其实又要做一个 trade off,如果能保证连接需求的比较稳定,需要的连接数总是大于 min connections 又不至于大太多,倒是可以用在这种方案。

四、每次使用之前检查一次

顾名思义,每次使用连接之前先检查一下这个连接是否可用。成本太高一般不用,对客户端来说会增加延迟,对DB来说会增加压力。

实现的话,可以看下 DBCP 的 TestOnBorrow

五、HAProxy

HAProxy 并不能说是一个思路,只是一个现成的工具,它是一个正向代理,实现了连接的测活和切换。HAProxy 的原理我还没研究,但是应该不出这几种思路。甚至是可以让你选的,毕竟它配置项目那么多。

HAProxy 怎么用呢?考虑有多个应用连接多个数据库的场景,从原来的直接连接变成通过 HAProxy 连接。当需要切换的时候,应用不需要感知 DB IP 的变化,而是 HAProxy 更改后端 DB 的 IP,从而对应用透明了,应用只需要一个 DB 集群的 URL 就可以了。

App 直接连接 DB

App 通过 HAProxy 连接 DB

 

另一方面,也节省了 DB 的连接数。

 

程序员应该知道的时间概念

很多人对“速度”没有什么概念,同机房内 RTT (Round Trip Time)大约是多少?如果将一个应用内的函数的调用拆成两个应用 RPC 调用,将增加多少延迟?打印日志有多快,打印日志的多少会增加多少延迟?

之前看过 Jeff Dan(忘记在哪里看到的了)写一个叫 Latency Numbers Every Programmer Should Know 的东西,我费了半天劲,终于找到了。决定把它贴在这里,传播一下。原文如下,这是从这个 gist 看到的。

有意思的事实是,CPU 的 Cache 到内存的操作都是纳秒级别的,同机房的 RTT 和 SSD 的读写速度在一个量级,机械硬盘比 SSD 慢20倍,比内存慢80倍。

以上的数据是2012年的,技术在发展,这个网页也可以拖动滚动条对不同年代的时间做可视化。2005年之前,CPU和内存的速度在飞速发展,但是2005年之后基本停滞了,之后是网络带宽、硬盘(SSD)快速发展。

https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html

这个网站可以看各个城市的延迟:https://wondernetwork.com/pings/

哦对了,说到 ping,我清明节的时候写了一个小工具 pingtop,可以同时 ping 多个 server 并在终端展示出来,喜欢的同学请点个赞❤️。

最后贴一张有人画的形象化的版本。

其他相关的阅读:

  • Jeff Dan 的一个分享 http://videolectures.net/wsdm09_dean_cblirs/
  • Teach Yourself Programming in Ten Years (我搜这个时间表的时候搜到的,这篇文章值得一看  ——为什么大家都这么着急呢?)
 

用 ssh 传输文件

今天收到了研发同学的一个工单,内容大体是:需要将文件拷贝到服务器上去,请求帮助安装 scp 程序。

我尝试了对那台服务器执行 scp 命令,结果是 -bash: scp: command not found,但是 ssh 是正常可以用的。这种情况下应该是 openssh-clients 被删掉了。

但是!scp 命令是基于 ssh 协议的,既然可以用 ssh ,还要什么 scp 呢!

我们可以直接用 ssh 就可以传输文件,学会了之后,发现它比 scp 还好用,scp 的 path 写起来比较蛋疼。

首先很多人忽略的一个事实是,ssh 可以直接输入命令对远程主机执行,比如 ssh root@myserver.com "cat access.log" ,就可以直接 cat 出远程文件的 log 内容。ssh 会将远程命令的 stdout 和本地的 stdout 连接起来。可以用这样的命令来看实时的日志: ssh root@myserver.com "tail -f access.log"。这样就可以在本地执行一条命令就可以了,方便脚本化或记录到 本地 history。

既然 ssh 可以将 stdout 连接起来,那么自然也可以将 stdin 连接起来!比如用这个命令将 ssh key 拷贝到服务器上去。

将一个文件传输到服务器:

上面这个命令的原理就是,将文件内容输入到 stdout 中,用管道和 ssh 连接起来,然后这个 stdout 就成了远程命令的 stdin。

要拷贝整个文件夹呢?没有问题:

如果是像日志这种压缩性能很高的文件,可以考虑压缩之后再传输,远程那边从 stdin 解压缩。而且直接将输入和输出通过管道连接起来,压缩中间生成的文件丝毫不会占用空间呢!

如果要指定远程的目标文件夹,可以使用 tar 的 C 参数来指定,比如远程解压缩到 /tmp 下面:

要注意像图片、视频这种二进制文件,本身就是经过压缩之后的了,如果再使用 tar z 来压缩一遍的话,不会节省多少传输体积,反而会白白耗费 CPU。

实用技巧:如果每天备份 MySQL 到另一台机器,但是不占用本机空间?Crontab 的脚本这么写:

假如一台机器A在一个网络环境,另一个机器B在另一个网络环境,他们之间不互通。但是你的电脑(或堡垒机)能同时用 ssh 登陆两台机器,那么怎么把 Server A 的文件拷贝到  Server B?

用两个 ssh!

理解 ssh 能连接 stdin 和 stdout 了,就有无限的可能了!而且你可以将脚本都放在本地,不用还得本地放一些,远程的机器放一些通过 ssh 来执行。

哦对了,ssh 是一个加密的协议,所以在传输的过程中会看到 CPU 使用上涨,因为这是在加密和(远程服务器)解密。用的时候需要考虑到这个。scp 命令是基于 ssh 的,所以会有一样的问题。

nc 基于 tcp 明文传输的,如果不需要加密,传输内容比较多,可以考虑用这个。

在 Server 端执行 nc 监听端口,将输入到 nc 的内容输出到一个文件中。

然后在 Client 端将要发送的文件输入到服务器的这个端口中:

 

这个独门绝技是 @mrluanma 教我的。