一次debug经历

图文无关

背景

线上系统使用的数据库是MySQL,使用了replication,单master+多slave。应用程序使用Django框架,库使用了PyMySQL,读数据的操作采用读写分离的策略。

从库发生过几次故障,因此在计划通过代理访问MySQL。代理软件使用Nginx,结构如下图。

image.png

这里使用到Nginx stream代理,需要nginx版本支持,见这里

问题

在测试环境中,部署了Nginx+MySQL后,接到开发的反馈说连接数据库间断性地出错,DBA求助协助调查。

提供的报错信息如下

......
OperationalError: (2006, "MySQL server has gone away (error(32, 'Broken pipe'))")

还有

......
OperationalError(2013, 'Lost connection to MySQL server during query')

排查

之前没有的问题突然出现,觉得很奇怪,想着看能不能重现以下。于是先搭建了一个测试环境,跑个脚本。

测试环境

在出现问题的MySQL服务器上创建临时库和临时表。测试库取名nginx_tcp_test_db,测试表取名nginx_tcp

mysql> create database nginx_tcp_test_db;
Query OK, 1 row affected (0.01 sec)

mysql> use nginx_tcp_test_db;
Database changed
mysql> show tables;
Empty set (0.00 sec)

mysql> create table nginx_tcp(id int, name varchar(64), location varchar(64), extra blob);
Query OK, 0 rows affected (0.00 sec)

采用PyMySQL编写脚本,脚本的任务很简单:建立新连接,先写入,再读取。

   try:
        connection = pymysql.connect(
            host=_args.host,
            user=_args.user,
            port=_args.port,
            password=_args.password,
            db=_args.database,
        )
        try:
            with connection.cursor() as cursor:
                connection.begin()
                sql = "INSERT INTO `nginx_tcp` (`id`, `name`, `location`, `extra`) VALUES (2, '2', '2', '32');"
                cursor.execute(sql)
                connection.commit()

            with connection.cursor() as cursor:
                sql = "SELECT `id`, `name` FROM `nginx_tcp` limit 100"
                cursor.execute(sql)
                cursor.fetchall()
            counter += 1
        except Exception as ex:
            traceback.print_exc()
            end = time.time()
            print('Total elapsed {0}, call count: {1}'.format(end-start, counter))
            exit(2)
        finally:
            connection.close()
    except Exception as ex:
        traceback.print_exc()
        end = time.time()
        print('Total elapsed {0}, call count: {1}'.format(end - start, counter))
        exit(2)

测试

通过Nginx代理到MySQL,执行脚本一段时间后,果然报了错。重复跑几次,总是能在运行一段时间后,调用pymysql.connect方法触发。

pymysql.err.OperationalError: (2013, 'Lost connection to MySQL server during query ([Errno 104] Connection reset by peer)'

而直接连接MySQL就不会有问题。

重点查看PyMySQL中connect方法,和其中的方法调用关系,为达到直观效果,绘制图形。

from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput
import pymysql

with PyCallGraph(output=GraphvizOutput()):
     pymysql.connect(.....)

发现报错是在读socket的时候发生的错误。

同时查看出问题时候的Nginx日志,发现了大量下面的错误

......
54737 recv() not ready (11: Resource temporarily unavailable)
......

查看系统日志发现这样的错误

# dmesg
TCP: time wait bucket table overflow
.....

这时通过netstat命令,发现确实有很多处于TIME_WAIT状态的连接,源IP为nginx服务器的IP,目的IP为MySQL服务器的IP。

测试脚本总是会主动断开连接,因此nginx上面的TIME_WAIT连接,只能是Nginx代理到MySQL后主动断开连接的结果。在TIME_WAIT数量超过一定限制时,nginx就会拒绝来自客户端的连接。

查看系统的net.ipv4.tcp_max_tw_buckets值,发现设置的偏小。因此增加这个值,同时改小net.ipv4.tcp_fin_timeout这个值,让Nginx主动关闭后的连接处于TIME_WAIT的时间变短。

修改完后再跑测试脚本,单点的情况下就没有出现问题了。

并发测试

解决了一个问题,但是另外一个问题:OperationalError: (2006, "MySQL server has gone away (error(32, 'Broken pipe'))")并没有被重现出来。

于是尝试增叫并发连接。采用最简单的办法,启动多个进程跑之前的脚本。跑了一段时间后,却发现了另外一个问题。

OperationalError(2013, 'Lost connection to MySQL server during query')

老办法,继续查看Nginx的日志,果然有发现:

[crit] 167312#167312: *742108 connect() to 10.10.48.169:6606 failed (99: Cannot assign requested address)

再查看系统TIME_WAIT的连接数量,并没有超过之前的配置。但测试几次后发现,总是在TIME_WAIT接近50000这个值的时候,客户端出现这个错误。

这里自然想到了TCP/IP四元组,即系统通过(local_ip, local_port, remote_ip, remote_port)来标识一条链接,在这里nginx代理到MySQL的场景下,local_ip,remote_ip, remote_port都是确定的,因此瓶颈会在local_port。查看配置net.ipv4.ip_local_port_range,发现范围长度和出现问题的TIME_WAIT数量接近。但是因为TCP支持的端口号是一个16位无符号整数,最大值是65536,其中1024以下的端口还是留给系统服务的,因此用于nginx连接MySQL的端口数量是有限的。

这里考虑三种方法进行扩展:

  1. 在nginx上配置SO_REUSEADDR,同时增加后端MySQL数量。
  2. 在nginx所在的服务器上配置多个IP,让nginx分别去监听这些IP,在将流量代理到MySQL的时候local_ip使用监听的IP。
  3. 增加nginx服务器,使用DNS轮询多个IP。

第一种显然是可行的,nginx提供了reuseport选项。
第二种可以用多网卡,也可以用单网卡配置多IP。这里采用的是单网卡多IP,但是没有成功,因为发现就算Nginx监听了两个IP,最后代理到MySQL的连接local_ip也只有一个。可能是配置的问题,这里没有深究。
第三种方案没有试验过,应该可行(毕竟主要工作还是排查bug。。。),当然这种情况可能就不需要nginx了。。

剩下的问题

然而还有一个问题OperationalError: (2006, "MySQL server has gone away (error(32, 'Broken pipe'))")无论怎么都无法重现。启用Google大法。

发现网上也有人遇到了类似的问题,见issue。下面有人指出,抛出这个异常一般有两种情况:

  1. 防火墙问题;
  2. 查询语句的问题

马上对其进行验证,首先是bad query,脚本如下:

.....
a = "".join(map(chr, (ord('a')+(y%26) for y in range(10000000))))
with connection.cursor() as cursor:
    sql = "INSERT INTO `nginx_tcp` (`id`, `name`, `location`, `extra`) VALUES (2, '2', '2', %s)"
    connection.commit()
.....

发现,果然复现了

OperationalError(2006, "MySQL server has gone away (error(32, 'Broken pipe'))")

异常。和同事沟通,确定了SQL不会有问题。

还剩下防火墙。同事发现最近服务器发生过迁移,DB和应用之间新增了一个防火墙,防火墙默认会关闭闲置连接(5分钟)。后续的推理就简单了:应用中采用了连接池,当闲置了5分钟的连接被拿出来使用时,就会发生这个问题了。

关闭防火墙后,联系开发同学进行测试,果然,问题不再复现。

后续

为了确保后续的部署不出问题,编写脚本创建连接池进行测试

class ConnectionPool:
    def __init__(self, conf, max_pool_size=20):
        self.max_pool_size = max_pool_size
        self.pool = None

        self.initialize_pool(conf)

    def initialize_pool(self, conf):
        self.pool = Queue.Queue(maxsize=self.max_pool_size)
        for _ in range(0, self.max_pool_size):
            connection = pymysql.connect(
                host=conf.get('host', '127.0.0.1'),
                user=conf.get('user'),
                port=conf.get('port', 3306),
                password=conf.get('password'),
                db=conf.get('db')
            )
            connection.ping()
            self.pool.put_nowait(connection)

    def get_connection(self):
        connection = self.pool.get(True)
        connection.ping()
        return connection

    def return_connection(self, connection):
        return self.pool.put_nowait(connection)

    def destroy(self):
        while not self.is_empty():
            self.pool.get().close()

    def is_empty(self):
        return self.pool.empty()
        
def test(_args):
    counter = 0
    config = {....}
    pool = ConnectionPool(conf=config)
    .....
    while True:
        connection = pool.get_connection()
        .......... do something .........
        sleep(2)
        pool.return_connection(connection)
    

总结

这次debug前后花掉了快两天的时间,得到了深刻的教训。

脱离了应用环境的debug就是耍流氓!!!

这次debug没有复现问题的原因就是复现环境和出现问题的环境不一致,导致浪费了大量时间,经验教训一定要牢记!!


欢迎大家关注我的微信公众号《派森公园》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容