title: 使用生成器把Kafka写入速度提高1000倍

toc: true

comment: true

date: 2018-04-13 21:35:09

tags: ['Python', '经验']

category: ['Python']

通过本文你会知道Python里面什么时候用yield最合适。本文不会给你讲生成器是什么,所以你需要先了解Python的yield,再来看本文。

疑惑

多年以前,当我刚刚开始学习Python协程的时候,我看到绝大多数的文章都举了一个生产者-消费者的例子,用来表示在生产者内部可以随时调用消费者,达到和多线程相同的效果。这里凭记忆简单还原一下当年我看到的代码:

import time

def consumer():
product = None
while True:
if product is not None:
print('consumer: {}'.format(product))
product = yield None def producer():
c = consumer()
next(c)
for i in range(10):
c.send(i) start = time.time()
producer()
end = time.time()
print(f'直到把所有数据塞入Kafka,一共耗时:{end - start}秒')

运行效果如下图所示。

这些文章的说法,就像统一好了口径一样,说这样写可以减少线程切换开销,从而大大提高程序的运行效率。但是当年我始终想不明白,这种写法与直接调用函数有什么区别,如下图所示。

直到后来我需要操作Kafka的时候,我明白了使用yield的好处。

探索

为了便于理解,我会把实际场景做一些简化,以方便说明事件的产生发展和解决过程。事件的起因是我需要把一些信息写入到Kafka中,我的代码一开始是这样的:

import time
from pykafka import KafkaClient client = KafkaClient(hosts="127.0.0.1:9092")
topic = client.topics[b'test'] def consumer(product):
with topic.get_producer(delivery_reports=True) as producer:
producer.produce(str(product).encode()) def feed():
for i in range(10):
consumer(i) start = time.time()
feed()
end = time.time()
print(f'直到把所有数据塞入Kafka,一共耗时:{end - start}秒')

这段代码的运行效果如下图所示。

写入10条数据需要100秒,这样的龟速显然是有问题的。问题就出在这一句代码:

with topic.get_producer(delivery_reports=True) as producer

获得Kafka生产者对象是一个非常耗费时间的过程,每获取一次都需要10秒钟才能完成。所以写入10个数据就获取十次生产者对象。这消耗的100秒主要就是在获取生产者对象,而真正写入数据的时间短到可以忽略不计。

由于生产者对象是可以复用的,于是我对代码作了一些修改:

import time
from pykafka import KafkaClient client = KafkaClient(hosts="127.0.0.1:9092")
topic = client.topics[b'test']
products = [] def consumer(product_list):
with topic.get_producer(delivery_reports=True) as producer:
for product in product_list:
producer.produce(str(product).encode()) def feed():
for i in range(10):
products.append(i)
consumer(products) start = time.time()
feed()
end = time.time()
print(f'直到把所有数据塞入Kafka,一共耗时:{end - start}秒')

首先把所有数据存放在一个列表中,最后再一次性给consumer函数。在一个Kafka生产者对象中展开列表,再把数据一条一条塞入Kafka。这样由于只需要获取一次生产者对象,所以需要耗费的时间大大缩短,如下图所示。

这种写法在数据量小的时候是没有问题的,但数据量一旦大起来,如果全部先放在一个列表里面的话,服务器内存就爆了。

于是我又修改了代码。每100条数据保存一次,并清空暂存的列表:

import time
from pykafka import KafkaClient client = KafkaClient(hosts="127.0.0.1:9092")
topic = client.topics[b'test'] def consumer(product_list):
with topic.get_producer(delivery_reports=True) as producer:
for product in product_list:
producer.produce(str(product).encode()) def feed():
products = []
for i in range(1003):
products.append(i)
if len(products) >= 100:
consumer(products)
products = [] if products:
consumer(products) start = time.time()
feed()
end = time.time()
print(f'直到把所有数据塞入Kafka,一共耗时:{end - start}秒')

由于最后一轮循环可能无法凑够100条数据,所以feed函数里面,循环结束以后还需要判断products列表是否为空,如果不为空,还要再消费一次。这样的写法,在上面这段代码中,一共1003条数据,每100条数据获取一次生产者对象,那么需要获取11次生产者对象,耗时至少为110秒。

显然,要解决这个问题,最直接的办法就是减少获取Kafka生产者对象的次数并最大限度复用生产者对象。如果读者举一反三的能力比较强,那么根据开关文件的两种写法:

# 写法一
with open('test.txt', 'w', encoding='utf-8') as f:
f.write('xxx') # 写法二
f = open('test.txt', 'w', encoding='utf-8')
f.write('xxx')
f.close()

可以推测出获取Kafka生产者对象的另一种写法:

# 写法二
producer = topic.get_producer(delivery_reports=True)
producer.produce(b'xxxx')
producer.close()

这样一来,只要获取一次生产者对象并把它作为全局变量就可以一直使用了。

然而,pykafka的官方文档中使用的是第一种写法,通过上下文管理器with来获得生产者对象。暂且不论第二种方式是否会报错,只从写法上来说,第二种方式必需要手动关闭对象。开发者经常会出现开了忘记关的情况,从而导致很多问题。而且如果中间出现了异常,使用上下文管理器的第一种方式会自动关闭生产者对象,但第二种方式仍然需要开发者手动关闭。

函数VS生成器

但是如果使用第一种方式,怎么能在一个上下文里面接收生产者传进来的数据呢?这个时候才是yield派上用场的时候。

首先需要明白,使用yield以后,函数就变成了一个生成器。生成器与普通函数的不同之处可以通过下面两段代码来进行说明:

def funciton(i):
print('进入')
print(i)
print('结束') for i in range(5):
funciton(i)

运行效果如下图所示。

函数在被调用的时候,函数会从里面的第一行代码一直运行到某个return或者函数的最后一行才会退出。

而生成器可以从中间开始运行,从中间跳出。例如下面的代码:

def generator():
print('进入')
i = None
while True:
if i is not None:
print(i)
print('跳出')
i = yield None g = generator()
next(g)
for i in range(5):
g.send(i)

运行效果如下图所示。

从图中可以看到,进入只打印了一次。代码运行到i = yield None后就跳到外面,外面的数据可以通过g.send(i)的形式传进生成器,生成器内部拿到外面传进来的数据以后继续执行下一轮while循环,打印出被传进来的内容,然后到i = yield None的时候又跳出。如此反复。

所以回到最开始的Kafka问题。如果把with topic.get_producer(delivery_reports=True) as producer写在上面这一段代码的print('进入')这个位置上,那岂不是只需要获取一次Kafka生产者对象,然后就可以一直使用了?

根据这个逻辑,设计如下代码:

import time
from pykafka import KafkaClient client = KafkaClient(hosts="127.0.0.1:9092")
topic = client.topics[b'test'] def consumer():
with topic.get_producer(delivery_reports=True) as producer:
print('init finished..')
next_data = ''
while True:
if next_data:
producer.produce(str(next_data).encode())
next_data = yield True def feed():
c = consumer()
next(c)
for i in range(1000):
c.send(i) start = time.time()
feed()
end = time.time()
print(f'直到把所有数据塞入Kafka,一共耗时:{end - start}秒')

这一次直接插入1000条数据,总共只需要10秒钟,相比于每插入一次都获取一次Kafka生产者对象的方法,效率提高了1000倍。运行效果如下图所示。

后记

读者如果仔细对比第一段代码和最后一段代码,就会发现他们本质上是一回事。但是第一段代码,也就是网上很多人讲yield的时候举的生产者-消费者的例子之所以会让人觉得毫无用处,就在于他们的消费者几乎就是秒运行,这样看不出和函数调用的差别。而我最后这一段代码,它的消费者分成两个部分,第一部分是获取Kafka生产者对象,这个过程非常耗时;第二部分是把数据通过Kafka生产者对象插入Kafka,这一部分运行速度极快。在这种情况下,使用生成器把这个消费者代码分开,让耗时长的部分只运行一次,让耗时短的反复运行,这样就能体现出生成器的优势。

使用生成器把Kafka写入速度提高1000倍的更多相关文章

  1. 斯坦福新深度学习系统 NoScope:视频对象检测快1000倍

    以作备份,来源http://jiasuhui.com/archives/178954 本文由“新智元”(微信ID:AI_era)编译,来源:dawn.cs.stanford.edu,编译:刘小芹 斯坦 ...

  2. 将Web应用性能提高十倍的10条建议

    导读 提高 web 应用的性能从来没有比现在更重要过.网络经济的比重一直在增长:全球经济超过 5% 的价值是在因特网上产生的(数据参见下面的资料).这个时刻在线的超连接世界意味着用户对其的期望值也处于 ...

  3. 王家林 Spark公开课大讲坛第一期:Spark把云计算大数据速度提高100倍以上

    王家林 Spark公开课大讲坛第一期:Spark把云计算大数据速度提高100倍以上 http://edu.51cto.com/lesson/id-30815.html Spark实战高手之路 系列书籍 ...

  4. 将 Web 应用性能提高十倍的10条建议

    提高 web 应用的性能从来没有比现在更重要过.网络经济的比重一直在增长:全球经济超过 5% 的价值是在因特网上产生的(数据参见下面的资料).这个时刻在线的超连接世界意味着用户对其的期望值也处于历史上 ...

  5. 【转】Vim速查表-帮你提高N倍效率

    Vim速查表-帮你提高N倍效率 转自:https://www.jianshu.com/p/6aa2e0e39f99 去年上半年开始全面使用linux进行开发和娱乐了,现在已经回不去windows了. ...

  6. 一行代码让python的运行速度提高100倍

    python一直被病垢运行速度太慢,但是实际上python的执行效率并不慢,慢的是python用的解释器Cpython运行效率太差. “一行代码让python的运行速度提高100倍”这绝不是哗众取宠的 ...

  7. 提速1000倍,预测延迟少于1ms,百度飞桨发布基于ERNIE的语义理解开发套件

    提速1000倍,预测延迟少于1ms,百度飞桨发布基于ERNIE的语义理解开发套件 11月5日,在『WAVE Summit+』2019 深度学习开发者秋季峰会上,百度对外发布基于 ERNIE 的语义理解 ...

  8. 一行代码让你的python运行速度提高100倍

    转自:https://www.cnblogs.com/xihuineng/p/10630116.html 加上之后运行速度快了十倍,我的天呐. python一直被病垢运行速度太慢,但是实际上pytho ...

  9. Redis+Kafka异步提高并发

    Redis+Kafka异步提高并发 Redis+Kafka异步提高并发 设计 实现 提交请求接口 Kafka消费队列 异步处理Service 客户端轮询获取结果 Redis集群节点配置 KafKa集群 ...

随机推荐

  1. WinSock 异步I/O模型-2

    事件选择(WSAEventSelect): WSAEventSelect模型是Windows Sockets提供的另外一个有用的异步I/O模型.该模型允许一个或多个套接字上接收以事件为基础的网络事件通 ...

  2. ~psd面试 求最长回文序列 DP求解

    链接:https://www.nowcoder.com/acm/contest/90/D来源:牛客网 掌握未来命运的女神 psd 师兄在拿了朝田诗乃的 buff 后决定去实习. 埃森哲公司注册成立于爱 ...

  3. STL --> queue单向队列

    queue单向队列 queue 模板类的定义在<queue>头文件中.与stack 模板类很相似,queue 模板类也需要两个模板参数,一个是元素类型,一个容器类型,元素类型是必要的,容器 ...

  4. java并发编程基础 --- 4.1线程简介

    一.线程简介 什么是线程: 现在操作系统在运行一个程序时,会为其创建一个进程.例如,启动一个java程序,操作系统就会创建一个java进程.现代操作系统调度的最小单元是线程,也叫轻量级进程,在一个进程 ...

  5. linux No space left on device 由索引节点(inode)爆满引发500问题

    inode是什么? 理解inode,要从文件储存说起. 文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector).每个扇区储存512字节(相当于0.5KB). 操作系统读取 ...

  6. 接口登录CSDN发布博客---封装方法,使用unittest框架

    一个简单的跑接口流程:登录后发表带图片的博客.这里涉及到的知识点: 1.登录时通过cookies去保持登录状态,把cookies添加到一个session中,这样可以保持长时间登录状态: 2.我们通过爬 ...

  7. 关于redis数据库的简单思考

    redis数据库中有以下几种数据类型: 字符串,哈希,列表,集合,有序集合 它们应用的场景如下: 字符串用法单一,用于存储一个key的值,用于一一对应的场合 列表作为数组来使用 对于哈希,特别适用于存 ...

  8. 爬取博主所有文章并保存到本地(.txt版)--python3.6

    闲话: 一位前辈告诉我大学期间要好好维护自己的博客,在博客园发布很好,但是自己最好也保留一个备份. 正好最近在学习python,刚刚从py2转到py3,还有点不是很习惯,正想着多练习,于是萌生了这个想 ...

  9. TOJ 1214: 数据结构练习题――线性表操作

    描述 请你定义一个线性表,可以对表进行"在某个位置之前插入一个元素"."删除某个位置的元素"."清除所有元素"."获取某个位置的元 ...

  10. 201621123040《Java程序设计》第十周学习总结

    1.本周学习总结 2.书面作业 2.1常用异常 2.1.1自己以前编写的代码中经常出现什么异常.需要捕获吗(为什么)?应如何避免? 算术异常ArithmeticException(除数为0的情况) 类 ...