曾纪文

曾纪文 查看完整档案

广州编辑华南理工大学  |  计算机科学与技术 编辑神盾局  |  后端工程师 编辑 blog.givenzeng.cn/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

曾纪文 发布了文章 · 2019-10-25

流处理引擎:ksql

  在阅读该文之前,我已经假设你已经对kafka的broker、topic、partition、consumer等概念已经有了一定的了解。

流处理

  流数据是一组顺序、大量、快速、连续到达的数据序列,一般情况下,数据流可被视为一个随时间延续而无限增长的动态数据集合。

Confluent KSQL

  Confluent KSQL是一个基于kafka的实时数据流处理工具引擎,提供了强大且易用的sql交互方式来对kafka数据流进行处理,而无需编写代码。ksql具备高扩展、高弹性、容错式等优良特性,并且它提供了大范围的流式处理操作,比如数据过滤、转化、聚合、连接join、窗口化和Sessionization(即捕获单一会话期间的所有的流事件)等。

概念

架构

ksql-architecture-and-components.png

  • KSQL engine: 处理ksql声明和查询
  • REST interfaceL:客户端和engine的连接器
  • KSQL CLI:命令行交互终端,通过rest api和引擎进行交互
  • KSQL UI:ksql的控制中心

stream和table

  流(stream)表示的从开始至今的完整的历史,它代表了过去产生的数据(事件、日志等)及其相应的时间。新的数据只能被不断地添加到流中,无法被删除和修改,它们是既定的事实。从某种角度而言,流是对事实的建模。

  表(table)表示的是基于数据流进行了某种操作之后的数据,它是对历史数据的某种状态的快照。表的这个概念,是源自于已经发展了数十年的RDBMS,因此,基本可以用相同的理解去使用table。

  其实,RDBMS中也有数据流,如binlog本身就是一种流式数据。KSQL将stream作为基础对象,而RDBMS的基础对象是table。KSQL和RDBMS都有将stream和table互相转化的功能,只是二者的侧重点不同而已。

query的生命周期

  • 使用DDL注册一个stream或者table,如:create stream stream_name with topic_name ...
  • 使用一个ksql声明来表示你的应用:create table as select from stream_name ...
  • ksql将你的DDL/DML解析为AST
  • ksql基于ASL生成一个逻辑计划
  • ksql基于逻辑计划生成一个物理执行计划
  • ksql生成和执行kafka流应用
  • 你可以通过对stream和table进行操作来管理你的应用

基本流程和一般DBMS相同。

使用

  最简单的体验方式: 使用docker。这种方式默认下将zookeeper、kafka、ksql在一个compose(一共9个service)下启动。最低配置8G内存,尝试请谨慎。

git clone https://github.com/confluentinc/cp-docker-images
cd cp-docker-images
git checkout 5.2.1-post
cd examples/cp-all-in-one/
docker-compose up -d --build
# 新建topic: user
docker-compose exec broker kafka-topics --create --zookeeper \
zookeeper:2181 --replication-factor 1 --partitions 1 --topic users
# 新建topic: pageview
docker-compose exec broker kafka-topics --create --zookeeper \
zookeeper:2181 --replication-factor 1 --partitions 1 --topic pageviews

  样例里面会自动生成两个topic:pageview和user,表示用户对某个页面的访问日志。

  现在我们kafka和ksql都已经有了,还创建了两个topic。现在我们使用一个脚本来往这两个topic写入一些数据(这个脚本写入的数据为avro)

wget https://github.com/confluentinc/kafka-connect-datagen/raw/master/config/connector_pageviews_cos.config
curl -X POST -H "Content-Type: application/json" --data @connector_pageviews_cos.config http://localhost:8083/connectors
wget https://github.com/confluentinc/kafka-connect-datagen/raw/master/config/connector_users_cos.config
curl -X POST -H "Content-Type: application/json" --data @connector_users_cos.config http://localhost:8083/connectors

  启动KSQL终端

docker-compose exec ksql-cli ksql http://ksql-server:8088

DDL

  • CREATE STREAM:基于某个topic新建一个流
  • CREATE TABLE:基于一个stream新建一个table
  • DROP STREAM/TABLE:删除stream或者table
  • CREATE STREAM AS SELECT (CSAS)
  • CREATE TABLE AS SELECT (CTAS)
  • 新建stream pageviews/users。(SHOW STREAMS;可以用来查看当前有什么stream)
CREATE STREAM pageviews (viewtime BIGINT, userid VARCHAR, pageid VARCHAR) \
WITH (KAFKA_TOPIC='pageviews', VALUE_FORMAT='AVRO');

CREATE TABLE users (registertime BIGINT, gender VARCHAR, regionid VARCHAR,  \
userid VARCHAR) \
WITH (KAFKA_TOPIC='users', VALUE_FORMAT='AVRO', KEY = 'userid');

SHOW STREAMS;
  • 从stream中查询数据
# 设置query语句读取最开始的数据
SET 'auto.offset.reset'='earliest';
SELECT pageid FROM pageviews LIMIT 3;

  你会发现这条query会从pageviews流中获取每条记录的pageid。你也可以加上一些where条件尝试一下。

  • 从其他stream生成一个新的stream
CREATE STREAM pageviews_female AS SELECT users.userid AS userid, pageid, \
regionid, gender FROM pageviews LEFT JOIN users ON pageviews.userid = users.userid \
WHERE gender = 'FEMALE';

  这条DDL会对pageviews和users中的数据进行左连接操作,并把连接结果作为新stream pageviews_femails的数据。这个stream的数据会写到一个新的kafka topic:PAGEVIEWS_FEMALE。

  即:我们可以完全基于一个现有的topic新建一个stream;也可以基于现有的stream新建一个stream,这建立方法所得到的数据会存储在一个和stream名相同的topic中。

  • 我们也可以基于一个现有的topic的部分数据建立一个stream,并指定新stream的topic名。以下这个stream的数据会存储在topic pageviews_enriched_r8_r9中。
CREATE STREAM pageviews_female_like_89 WITH (kafka_topic='pageviews_enriched_r8_r9', \
value_format='AVRO') AS SELECT * FROM pageviews_female WHERE regionid LIKE '%_8' OR regionid LIKE '%_9';
  • stream的建立语句可以使用聚合函数和窗口函数
CREATE TABLE pageviews_regions AS SELECT gender, regionid , \
COUNT(*) AS numusers FROM pageviews_female WINDOW TUMBLING (size 30 second) \
GROUP BY gender, regionid HAVING COUNT(*) > 1;
  • 查看stream的定义
# 类似于mysql的desc
DESCRIBE EXTENDED pageviews_female_like_89;

和外部系统的连接

ksql可以使用ksql connectors和外部系统如:mysql、s3、hdfs等进行通信、操作。

优缺点

  • 优点

    • KSQL 流数据查询在实现上是分布式的、容错的、弹性的、可扩展的和实时的,这些特性可以满足现代企业对数据的需求。
    • KSQL的数据过滤、转化、聚合、连接join、窗口化和Sessionization等功能基本能够覆盖大部分应用场景;近似标准SQL的客户端实现降低了学习成本。
    • 对于 Java 或 Scala 开发人员而言,Kakfa Streams API 是一个强大的软件库,它实现了将流数据处理集成到应用中。
    • 利用ksql可以轻松实现实时报告、服务监控、活动告警、基于会话的用户数据分析、实时ETL等。
  • 缺点

    • KSQL算是一种重量级的流数据处理工具,对于资源要求较高。

其他工具

相关链接

原文:https://blog.givenzeng.cn/tools/ksql/

查看原文

赞 1 收藏 1 评论 0

曾纪文 发布了文章 · 2019-10-25

操作系统:计算机系统漫游

  1. hello程序的生命周期是从一个源程序(或源文件)开始的。
  2. 数字的机器表示方式,与实际的整数和实数是不同的。它们是对真值的有限近似值。

C语言的起源

c是贝尔实验室的Dennis Ritchie创建的的。起初是为了减少汇编代码的编写量。

  1. 大部分Unix内核以及所有的支撑工具和函数库都是用c编写的。

程序的编译

gcc编译c源文件的过程有:

  1. 预处理:根据#开头的命令,修改c程序。如把#include的h文件插入到程序文本中。最终得到一个以.i结束的文件。
  2. 编译:将i文件翻译成s文件。s文件包含了一个汇编语言程序。
  3. 汇编:汇编器as将s文件翻译成机器语言指令,并把这些指令打包成一个叫做可重定位目标文件的格式(o文件)。o文件是一个二进制文件。
  4. 链接:把o文件链接在一起。如hello程序中用到了printf,就把printf.o(预编译好的)和hello.o(上一步生成的)合并,形成最后的可执行文件hello

gnu项目

gcc是gnu(GNUL'S NOT UNIX)(有Richard Stallman发起的开源项目)的开发出来的工具之一。现代开放源码运动的思想起源是GNU项目中的自由软件概念(自由并不是说免费)。

了解编译的好处

  1. 优化程序性能
  2. 理解链接时出现的错误
  3. 避免安全漏洞(注:避免写成减少可能更好)

系统的硬件组成

  1. 总线:电子管道,用于携带信息字节,负责在各个部件间传递数据。通常被设计成传送定长的字节块,也就是字
  2. io设备:系统与外部世界的连接通道。常用的io设备有鼠标、键盘、显示器、磁盘。每个io设备都有一个控制器/适配器adapter。
  3. 主存:临时存储设备,用于存放程序和程序需要处理的数据。主存一般有动态随机存取存储器(DRAM)组成。是一个线性的字节数组。每个字节都有唯一的地址(从零开始)。
  4. 处理器cpu:核心是一个大小为一个字的存储设备(寄存器),即程序计数器pc。pc指向主存中某条机器语言的指令地址。cpu的工作:

    • 读指令
    • 解析指令
    • 加载数据,执行操作,存储结果
    • 更新pc(可能是加一,或者更新到指定值)

处理器看上去是它的指令集架构的简单实现。实际上当然是复杂的多。

缓存

高速缓存存储器(cache)的提出是为了解决cpu和主存的速度问题。cache将cpu常用、将要用到的数据、指令从主存加载到自己处,然后cpu读取cache的数据即可。为了更好的实现缓存功能,人们还实现了多级缓存。高级缓存比低级缓存快,但同时存储空间也相对小。

操作系统管理硬件

os有两个基本功能:

  • 防止硬件被失控的应用程序滥用
  • 向应用程序提供简单一致的机制来控制复杂而又大不相同低级硬件设备。

os通过几个抽象概念来实现这两个功能:进程、虚存、文件

Unix的层次文件系统、作为用户级进程的shell概念,都是从Multics(上世纪60年代的一个较为成功的系统)来的。后来加州大学的伯克利分校增加了虚存和internet协议,称为BSD(Berkeley software distribution)。后来很多厂商加入了一些其他的功能,但是不同产商的特性通常不兼容,为了解决这个问题,Richard Stallman和ieee标准化了Unix的开发。这个标注称为posix。

进程

进程是os对一个正在运行的程序的抽象。一个os可以同时运行多个进程(并发运行),是通过上线文切换来实现的(系统保持跟踪进程运行所需的所有状态信息,这种状态,就是上下文)。

内核不是一个独立的程序,它是系统管理全部进程所用代码和数据结构的集合。

线程

一个进程可以有多个线程(至少一个)。每个线程运行在进程的上下文中,共享进程的代码和全局数据,同时每个线程有自己的上下文。

线程分为内核级和用户级两种。

虚存

os为进程提供一个假象,即每个进程都在独占地使用主存。每个进程看到的内存都是一致的。

  • 地址空间的最上面的区域是留给操作系统中的代码和数据的,这对所有进程来说都是一样的。空间底部存放用户进程定义的代码和数据。
  • 程序代码和数据:代码从一固定地址开始,接着是和c全局变量相对应的数据位置。
  • 堆:运行时通过malloc申请的空间,通过free释放。堆的地址是向上增长的
  • 栈:调用函数和导致栈增长,函数返回则栈收缩。通常的栈溢出,说的就是这个。(JVM除外,java有自己的栈)
内核
---------
用户栈
---------
共享库的内存映射区域
---------

---------
运行时堆
---------
读写数据
---------
只读代码和数据(程序开始的位置,注意,不是从0开始的喔)
---------
0

文件

文件就是字节序列,仅此而已。io设备都可以看成是文件,里面的数据就是字节序列。系统的输入输出都是通过使用一组称为Unix io的系统函数调用读写文件来实现的。

1991年,Linus Torvalds谨慎地发布一个新的类unix os内核,后来逐渐发展称为一个技术和文化现象。

网络通信

网络可以视为一个io设备。网络通过一个适配器,作为io介入计算机。

其他概念

  • 并发、并行
  • 超线程
  • 指令级并行:流水线
  • simd:单指令多数据

原文:https://blog.givenzeng.cn/book/computer_system/chapter_1/

查看原文

赞 0 收藏 0 评论 0

曾纪文 发布了文章 · 2019-10-25

操作系统:计算机信息处理和表示

本篇将简单介绍计算机的各种数值表示方法:原码、反码、补码、浮点数。

study.png

  现有的大部分计算机都使用二进制表示法来表示数值。单个二进制并不是十分有用,但是,当把位组合起来并加上某种解释后,就赋予了不同的可能位模式含义,进而表示一个有限的集合。

  无符号编码基于传统的二进制表示法,表示大于或者等于0的数字。补码可以表示有符号整数。浮点数是表示实数的以2位基数的版本。

  有符号整数通常使用补码表示,当整数太大的时候,会产生溢出,可能产生负数。而浮点数有着完全不同的数学属性,其整数溢出会产生特殊的值(正无穷或者负无穷,整数溢出则产生正无穷,负数溢出则产生负无穷);两个正数相乘永远只会产生正数,即便溢出亦不会产生负数。由于精度有限,浮点运算时不可结合的。

  正数的表示只能编码一个相对较小的数值范围,这种表示是精确的;而浮点数可以表示一个较大的数值范围,但是这种表示只是近似的。

信息的存储

  机器不会理会什么原码、反码、补码,它直接操作的对象是字节。

字节和字

  大多数计算机使用8位的块,或者字节,作为最小的可寻址的内存单位,而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组,称为虚拟内存。内存的每个字节都有一个唯一的数字来标识,称为它的地址,所有可能的地址的集合就称为虚拟地址空间。

  C语言中的一个指针的值(无论它指向一个整数、一个结构、或者是某个其他程序的对象)都是某个存储块的第一个字节的虚拟地址。类型的信息有C编译器维护着,它生成的世界机器级程序并不办好关于数据类型的信息。机器级程序面向的操作对象是字节(这是重点,机器不会理会什么原码、反码、补码,它直接操作的对象是字节)。

  每台计算机都留有一个字长,知名指针数据的标称大小。虚拟地址以这样的一个字来表示,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。32位的机器最多只能利用4G的虚拟内存空间。大多数64位机器也可以运行为32位机器编译的程序,这是一种后向兼容。我们将程序称为“32位程序”或者“64位程序”时,区别在于该程序时如何编译的,而不是其运行的机器类型。(long在32位程序时指向的是4字节,而在64位程序指向的是8字节)。

  为了避免由于依赖“典型”大小和不同编译器设置带来的奇怪行为,ISO C99引入了一类数据类型,其数据大小是固定的,不随编译器的和机器设置而变化,如:int64_t,int32_t等。

  在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中的最小地址。

大端法、小端法

  某些机器在内存中按照从最低有效字节到最高有效的顺序存储对象;而另一些机器则相反。前者称为下端法,后者称为大端法。二者并无明显的优劣之分。我们现在使用的大多数PC使用的是小端法。(关于大端法、小端法的命名,传闻是《格列佛游记》中关于吃鸡蛋时应该先打破大的一端还是小的一端的争议。)

十六进制数0x0012AB9F表示法:
地址: 低   ->   高
大端: 00 12 AB 9F
小端: 9F AB 12 00

我们可以使用一个程序来查看一个数值究竟是以大端法还是小端法表示:

// pt为指向某个数值的指针,len为该数值的字节大小
void show(unsigned char *pt, size_t len) {
    size_t i;
    for (i=0; i < len; i++) {
        // 按地址从低到高打印每个字节
        printf(" %.2x", start[i]);
    }
    printf("\n");
}

int main() {
    int32_t val = 0x0012AB9F;
    show((unsigned char *)&val);
}

字符串的表示

  C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。

原码

  直接使用数值的二进制表示则为原码。如数值3的原码为:0101。原码使用最高位作为符号位。

  n位的原码可以表示的数值范围为: [-[2(n-1)-1], 2^n(-1)-1],如8位的原码可以表示[-127,127]。

+3 = 0011
-3 = 1011
+0 = 0000
-0 = 1000

  我们可以发现在原码表示法中,0有两种表示。这对于计算机来说并不友好。(我们需要谨记,机器的操作对象是字节,它不会管你的数值是怎样表示。在我们的期待中,等式“0 + 1”的结果应该是“1”,但是如果机器对1000进行加1,则结果仍然是0,只是从-0变为+0而已。)而且这种表示法,还浪费了一个数值表示:0没必要有两种表示方法。

反码

  反码是数值存储的一种,多应用于系统环境设置,如linux平台的目录和文件的默认权限的设置

补码

  补码很好的解决了原码的不足之处:解决了原码中0存在两种表示方式的问题。最重要的是,补码向机器掩盖了数值的正负属性。(补码使得数值在机器看来都是非负数)。

补码的表示

  n位补码的表示范围为:[-2^(n-), 2^(n-1)-1]。如8位的补码可以表示[-128, 127]。

two_completement.jpg

  • 在补码中,最小值为10...0,最大值为01...1。
  • 溢出:7+1 = 0111+0111 = 1000 = -8
  • 2^n称为补码的模。一个数值的补码加k和减(2^n-k)的结果是一样的。如四位补码的模为8;7的补码表示为0111,加上3:0111+0011=1010,即溢出成为-6;7减(16-3)即7-13 = 0111-1101=1010(13的补码表示为1101)。(类比:一个时钟的模为12,往回拨8个小时和往后拨4个小时得到的结果是一样的。)

  在机器看来,对所有的数字进行加减都只是简单的进行二进制加减,没有正负之分。由于补码的表示刚好构成一个循环且表示是连续的(一个时钟,并没有正负之分)。

关于补码的联想

  C、C++等大部分语言都支持有符号(默认)和无符号整数。Java只支持有符号数。
  我们可以将补码理解为温度的开尔文表示方法。开尔文将温度的最低值-273.15度定义为0开尔文温度,使得所有对温度的计算都是正数的运算,屏蔽了我们0下温度的概念。这也是热力学中为什么开尔文温度的使用比摄氏度更广泛的原因。
  同样,补码也使得机器屏蔽了我们对所有正负的定义,在机器看来,所有的数字都是正数,最小值为1000(如同热力学中最低温度为0开尔文度),只需按照正数来进行运算,然后将运算结果解释为人类的数值定义即可。

补码的解析

  补码使得只需按照正数来进行运算,然后将运算结果解释为人类的数值定义即可。所谓的解析,就是将补码转化为原码显示给人类即可(我们的机器都有这样的电路,用于将补码转化为原码):

  • 如果补码的符号位为“0”,表示是一个正数,其原码就是补码。
  • 如果补码的符号位为“1”,表示是一个负数,只需将除最高位外的所有位取反加一即可得到原码。

  我们发现这个转化步骤和原码转化为补码的步骤是完全一样的。因此,我们只需使用一个电路,就可以将补码和原码相互转化。

equal_swap.png

  可见,补码的使用,使得机器对数值的运算和解析都十分方便。

浮点数的表示

float.png

  • s符号位
  • M:尾数
  • E:阶码

  数值解释:V=(-1)^s (1+f) 2^(E-bias),其中bias为2^(k-1)-1。k为E的位数。使用偏置量bias可以使得数值更为平滑。

  那些可表示的浮点数不是均匀分布的,越靠近原点处它们越稠密,即越是靠近0的数,我们的表示误差就越小。一些较大的数我们是无法精确表示的,只能以最接近它的数值来表示。

  浮点数详细解释比较麻烦,暂不介绍。

原文:https://blog.givenzeng.cn/book/computer_system/chapter_2/

查看原文

赞 1 收藏 1 评论 0

曾纪文 赞了文章 · 2019-05-19

实战Go内存泄露

最近解决了我们项目中的一个内存泄露问题,事实再次证明pprof是一个好工具,但掌握好工具的正确用法,才能发挥好工具的威力,不然就算你手里有屠龙刀,也成不了天下第一,本文就是带你用pprof定位内存泄露问题。

关于Go的内存泄露有这么一句话不知道你听过没有:

10次内存泄露,有9次是goroutine泄露。

我所解决的问题,也是goroutine泄露导致的内存泄露,所以这篇文章主要介绍Go程序的goroutine泄露,掌握了如何定位和解决goroutine泄露,就掌握了内存泄露的大部分场景

本文草稿最初数据都是生产坏境数据,为了防止敏感内容泄露,全部替换成了demo数据,demo的数据比生产环境数据简单多了,更适合入门理解,有助于掌握pprof。

go pprof基本知识

定位goroutine泄露会使用到pprof,pprof是Go的性能工具,在开始介绍内存泄露前,先简单介绍下pprof的基本使用,更详细的使用给大家推荐了资料。

什么是pprof

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等,当需要性能调优或者定位Bug时候,这些记录的信息是相当重要。

基本使用

使用pprof有多种方式,Go已经现成封装好了1个:net/http/pprof,使用简单的几行命令,就可以开启pprof,记录运行信息,并且提供了Web服务,能够通过浏览器和命令行2种方式获取运行数据。

看个最简单的pprof的例子:

文件:golang_step_by_step/pprof/pprof/demo.go

package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // 开启pprof,监听请求
    ip := "0.0.0.0:6060"
    if err := http.ListenAndServe(ip, nil); err != nil {
        fmt.Printf("start pprof failed on %s\n", ip)
    }
}

提醒:本文所有代码部分可左右滑动

浏览器方式

image-20190516173924325

输入网址ip:port/debug/pprof/打开pprof主页,从上到下依次是5类profile信息

  1. block:goroutine的阻塞信息,本例就截取自一个goroutine阻塞的demo,但block为0,没掌握block的用法
  2. goroutine:所有goroutine的信息,下面的full goroutine stack dump是输出所有goroutine的调用栈,是goroutine的debug=2,后面会详细介绍。
  3. heap:堆内存的信息
  4. mutex:锁的信息
  5. threadcreate:线程信息

这篇文章我们主要关注goroutine和heap,这两个都会打印调用栈信息,goroutine里面还会包含goroutine的数量信息,heap则是内存分配信息,本文用不到的地方就不展示了,最后推荐几篇文章大家去看。

命令行方式

当连接在服务器终端上的时候,是没有浏览器可以使用的,Go提供了命令行的方式,能够获取以上5类信息,这种方式用起来更方便。

使用命令go tool pprof url可以获取指定的profile文件,此命令会发起http请求,然后下载数据到本地,之后进入交互式模式,就像gdb一样,可以使用命令查看运行信息,以下是5类请求的方式:

# 下载cpu profile,默认从当前开始收集30s的cpu使用情况,需要等待30s
go tool pprof http://localhost:6060/debug/pprof/profile   # 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=120     # wait 120s

# 下载heap profile
go tool pprof http://localhost:6060/debug/pprof/heap      # heap profile

# 下载goroutine profile
go tool pprof http://localhost:6060/debug/pprof/goroutine # goroutine profile

# 下载block profile
go tool pprof http://localhost:6060/debug/pprof/block     # goroutine blocking profile

# 下载mutex profile
go tool pprof http://localhost:6060/debug/pprof/mutex

上面的pprof/demo.go太简单了,如果去获取内存profile,几乎获取不到什么,换一个Demo进行内存profile的展示:

文件:golang_step_by_step/pprof/heap/demo2.go

// 展示内存增长和pprof,并不是泄露
package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "time"
)

// 运行一段时间:fatal error: runtime: out of memory
func main() {
    // 开启pprof
    go func() {
        ip := "0.0.0.0:6060"
        if err := http.ListenAndServe(ip, nil); err != nil {
            fmt.Printf("start pprof failed on %s\n", ip)
            os.Exit(1)
        }
    }()

    tick := time.Tick(time.Second / 100)
    var buf []byte
    for range tick {
        buf = append(buf, make([]byte, 1024*1024)...)
    }
}

上面这个demo会不断的申请内存,把它编译运行起来,然后执行:

$ go tool pprof http://localhost:6060/debug/pprof/heap

Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
Saved profile in /home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz       //<--- 下载到的内存profile文件
File: demo // 程序名称
Build ID: a9069a125ee9c0df3713b2149ca859e8d4d11d5a
Type: inuse_space
Time: May 16, 2019 at 8:55pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof)
(pprof) help  // 使用help打印所有可用命令
  Commands:
    callgrind        Outputs a graph in callgrind format
    comments         Output all profile comments
    disasm           Output assembly listings annotated with samples
    dot              Outputs a graph in DOT format
    eog              Visualize graph through eog
    evince           Visualize graph through evince
    gif              Outputs a graph image in GIF format
    gv               Visualize graph through gv
    kcachegrind      Visualize report in KCachegrind
    list             Output annotated source for functions matching regexp
    pdf              Outputs a graph in PDF format
    peek             Output callers/callees of functions matching regexp
    png              Outputs a graph image in PNG format
    proto            Outputs the profile in compressed protobuf format
    ps               Outputs a graph in PS format
    raw              Outputs a text representation of the raw profile
    svg              Outputs a graph in SVG format
    tags             Outputs all tags in the profile
    text             Outputs top entries in text form
    top              Outputs top entries in text form
    topproto         Outputs top entries in compressed protobuf format
    traces           Outputs all profile samples in text form
    tree             Outputs a text rendering of call graph
    web              Visualize graph through web browser
    weblist          Display annotated source in a web browser
    o/options        List options and their current values
    quit/exit/^D     Exit pprof
    
    ....

以上信息我们只关注2个地方:

  1. 下载得到的文件:/home/ubuntu/pprof/pprof.demo.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz,这其中包含了程序名demo,profile类型alloc已分配的内存,inuse代表使用中的内存。
  2. help可以获取帮助,最先会列出支持的命令,想掌握pprof,要多看看,多尝试。

关于命令,本文只会用到3个,我认为也是最常用的:toplisttraces,分别介绍一下。

top

按指标大小列出前10个函数,比如内存是按内存占用多少,CPU是按执行时间多少。

(pprof) top
Showing nodes accounting for 814.62MB, 100% of 814.62MB total
      flat  flat%   sum%        cum   cum%
  814.62MB   100%   100%   814.62MB   100%  main.main
         0     0%   100%   814.62MB   100%  runtime.main

top会列出5个统计数据:

  • flat: 本函数占用的内存量。
  • flat%: 本函数内存占使用中内存总量的百分比。
  • sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
  • cum: 是累计量,加入main函数调用了函数f,函数f占用的内存量,也会记进来。
  • cum%: 是累计量占总量的百分比。

list

查看某个函数的代码,以及该函数每行代码的指标信息,如果函数名不明确,会进行模糊匹配,比如list main会列出main.mainruntime.main

(pprof) list main.main  // 精确列出函数
Total: 814.62MB
ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go
  814.62MB   814.62MB (flat, cum)   100% of Total
         .          .     20:    }()
         .          .     21:
         .          .     22:    tick := time.Tick(time.Second / 100)
         .          .     23:    var buf []byte
         .          .     24:    for range tick {
  814.62MB   814.62MB     25:        buf = append(buf, make([]byte, 1024*1024)...)
         .          .     26:    }
         .          .     27:}
         .          .     28:
(pprof) list main  // 匹配所有函数名带main的函数
Total: 814.62MB
ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go
  814.62MB   814.62MB (flat, cum)   100% of Total
         .          .     20:    }()
         .          .     21:
..... // 省略几行
         .          .     28:
ROUTINE ======================== runtime.main in /usr/lib/go-1.10/src/runtime/proc.go
         0   814.62MB (flat, cum)   100% of Total
         .          .    193:        // A program compiled with -buildmode=c-archive or c-shared
..... // 省略几行

可以看到在main.main中的第25行占用了814.62MB内存,左右2个数据分别是flat和cum,含义和top中解释的一样。

traces

打印所有调用栈,以及调用栈的指标信息。

(pprof) traces
File: demo2
Type: inuse_space
Time: May 16, 2019 at 7:08pm (CST)
-----------+-------------------------------------------------------
     bytes:  813.46MB
  813.46MB   main.main
             runtime.main
-----------+-------------------------------------------------------
     bytes:  650.77MB
         0   main.main
             runtime.main
....... // 省略几十行

每个- - - - - 隔开的是一个调用栈,能看到runtime.main调用了main.main,并且main.main中占用了813.46MB内存。

其他的profile操作和内存是类似的,这里就不展示了。

这里只是简单介绍本文用到的pprof的功能,pprof功能很强大,也经常和benchmark结合起来,但这不是本文的重点,所以就不多介绍了,为大家推荐几篇文章,一定要好好研读、实践:

  1. Go官方博客关于pprof的介绍,很详细,也包含样例,可以实操:Profiling Go Programs
  2. 跟煎鱼也讨论过pprof,煎鱼的这篇文章也很适合入门: Golang 大杀器之性能剖析 PProf

什么是内存泄露

内存泄露指的是程序运行过程中已不再使用的内存,没有被释放掉,导致这些内存无法被使用,直到程序结束这些内存才被释放的问题。

Go虽然有GC来回收不再使用的堆内存,减轻了开发人员对内存的管理负担,但这并不意味着Go程序不再有内存泄露问题。在Go程序中,如果没有Go语言的编程思维,也不遵守良好的编程实践,就可能埋下隐患,造成内存泄露问题。

怎么发现内存泄露

在Go中发现内存泄露有2种方法,一个是通用的监控工具,另一个是go pprof:

  1. 监控工具:固定周期对进程的内存占用情况进行采样,数据可视化后,根据内存占用走势(持续上升),很容易发现是否发生内存泄露。
  2. go pprof:适合没有监控工具的情况,使用Go提供的pprof工具判断是否发生内存泄露。

这2种方式分别介绍一下。

监控工具查看进程内在占用情况

如果使用云平台部署Go程序,云平台都提供了内存查看的工具,可以查看OS的内存占用情况和某个进程的内存占用情况,比如阿里云,我们在1个云主机上只部署了1个Go服务,所以OS的内存占用情况,基本是也反映了进程内存占用情况,OS内存占用情况如下,可以看到随着时间的推进,内存的占用率在不断的提高,这是内存泄露的最明显现象

image-20190512111200988

如果没有云平台这种内存监控工具,可以制作一个简单的内存记录工具。

1、建立一个脚本prog_mem.sh,获取进程占用的物理内存情况,脚本内容如下:

#!/bin/bash
prog_name="your_programe_name"
prog_mem=$(pidstat  -r -u -h -C $prog_name |awk 'NR==4{print $12}')
time=$(date "+%Y-%m-%d %H:%M:%S")
echo $time"\tmemory(Byte)\t"$prog_mem >>~/record/prog_mem.log

2、然后使用crontab建立定时任务,每分钟记录1次。使用crontab -e编辑crontab配置,在最后增加1行:

*/1 * * * * ~/record/prog_mem.sh

脚本输出的内容保存在prog_mem.log,只要大体浏览一下就可以发现内存的增长情况,判断是否存在内存泄露。如果需要可视化,可以直接黏贴prog_mem.log内容到Excel等表格工具,绘制内存占用图。

image-20190512172935195

go pprof发现存在内存问题

有情提醒:如果对pprof不了解,可以先看go pprof基本知识,这是下一节,看完再倒回来看。

如果你Google或者百度,Go程序内存泄露的文章,它总会告诉你使用pprof heap,能够生成漂亮的调用路径图,火焰图等等,然后你根据调用路径就能定位内存泄露问题,我最初也是对此深信不疑,尝试了若干天后,只是发现内存泄露跟某种场景有关,根本找不到内存泄露的根源,如果哪位朋友用heap就能定位内存泄露的线上问题,麻烦介绍下

后来读了Dave的《High Performance Go Workshop》,刷新了对heap的认识,内存pprof的简要内容如下:

image-20190512114048868

Dave讲了以下几点:

  1. 内存profiling记录的是堆内存分配的情况,以及调用栈信息,并不是进程完整的内存情况,猜测这也是在go pprof中称为heap而不是memory的原因。
  2. 栈内存的分配是在调用栈结束后会被释放的内存,所以并不在内存profile中
  3. 内存profiling是基于抽样的,默认是每1000次堆内存分配,执行1次profile记录。
  4. 因为内存profiling是基于抽样和它跟踪的是已分配的内存,而不是使用中的内存,(比如有些内存已经分配,看似使用,但实际以及不使用的内存,比如内存泄露的那部分),所以不能使用内存profiling衡量程序总体的内存使用情况
  5. Dave个人观点:使用内存profiling不能够发现内存泄露

基于目前对heap的认知,我有2个观点:

  1. heap能帮助我们发现内存问题,但不一定能发现内存泄露问题,这个看法与Dave是类似的。heap记录了内存分配的情况,我们能通过heap观察内存的变化,增长与减少,内存主要被哪些代码占用了,程序存在内存问题,这只能说明内存有使用不合理的地方,但并不能说明这是内存泄露。
  2. heap在帮助定位内存泄露原因上贡献的力量微乎其微。如第一条所言,能通过heap找到占用内存多的位置,但这个位置通常不一定是内存泄露,就算是内存泄露,也只是内存泄露的结果,并不是真正导致内存泄露的根源。

接下来,我介绍怎么用heap发现问题,然后再解释为什么heap几乎不能定位内存泄露的根因。

怎么用heap发现内存问题

使用pprof的heap能够获取程序运行时的内存信息,在程序平稳运行的情况下,每个一段时间使用heap获取内存的profile,然后使用base能够对比两个profile文件的差别,就像diff命令一样显示出增加和减少的变化,使用一个简单的demo来说明heap和base的使用,依然使用demo2进行展示。

文件:golang_step_by_step/pprof/heap/demo2.go

// 展示内存增长和pprof,并不是泄露
package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "time"
)

// 运行一段时间:fatal error: runtime: out of memory
func main() {
    // 开启pprof
    go func() {
        ip := "0.0.0.0:6060"
        if err := http.ListenAndServe(ip, nil); err != nil {
            fmt.Printf("start pprof failed on %s\n", ip)
            os.Exit(1)
        }
    }()

    tick := time.Tick(time.Second / 100)
    var buf []byte
    for range tick {
        buf = append(buf, make([]byte, 1024*1024)...)
    }
}

将上面代码运行起来,执行以下命令获取profile文件,Ctrl-D退出,1分钟后再获取1次。

go tool pprof http://localhost:6060/debug/pprof/heap

我已经获取到了两个profile文件:

$ ls
pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz
pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz

使用base把001文件作为基准,然后用002和001对比,先执行toptop的对比,然后执行list main列出main函数的内存对比,结果如下:

$ go tool pprof -base pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz pprof.demo2.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz

File: demo2
Type: inuse_space
Time: May 14, 2019 at 2:33pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof)
(pprof) top
Showing nodes accounting for 970.34MB, 32.30% of 3003.99MB total
      flat  flat%   sum%        cum   cum%
  970.34MB 32.30% 32.30%   970.34MB 32.30%  main.main   // 看这
         0     0% 32.30%   970.34MB 32.30%  runtime.main
(pprof)
(pprof)
(pprof) list main.main
Total: 2.93GB
ROUTINE ======================== main.main in /home/ubuntu/heap/demo2.go
  970.34MB   970.34MB (flat, cum) 32.30% of Total
         .          .     20:    }()
         .          .     21:
         .          .     22:    tick := time.Tick(time.Second / 100)
         .          .     23:    var buf []byte
         .          .     24:    for range tick {
  970.34MB   970.34MB     25:        buf = append(buf, make([]byte, 1024*1024)...) // 看这
         .          .     26:    }
         .          .     27:}
         .          .     28:

top列出了main.mainruntime.mainmain.main就是我们编写的main函数,runtime.main是runtime包中的main函数,也就是所有main函数的入口,这里不多介绍了,有兴趣可以看之前的调度器文章《Go调度器系列(2)宏观看调度器》

top显示main.main 第2次内存占用,比第1次内存占用多了970.34MB。

list main.main告诉了我们增长的内存都在这一行:

buf = append(buf, make([]byte, 1024*1024)...)

001和002 profile的文件不进去看了,你本地测试下计算差值,绝对是刚才对比出的970.34MB。

heap“不能”定位内存泄露

heap能显示内存的分配情况,以及哪行代码占用了多少内存,我们能轻易的找到占用内存最多的地方,如果这个地方的数值还在不断怎大,基本可以认定这里就是内存泄露的位置。

曾想按图索骥,从内存泄露的位置,根据调用栈向上查找,总能找到内存泄露的原因,这种方案看起来是不错的,但实施起来却找不到内存泄露的原因,结果是事半功倍。

原因在于一个Go程序,其中有大量的goroutine,这其中的调用关系也许有点复杂,也许内存泄露是在某个三方包里。举个栗子,比如下面这幅图,每个椭圆代表1个goroutine,其中的数字为编号,箭头代表调用关系。heap profile显示g111(最下方标红节点)这个协程的代码出现了泄露,任何一个从g101到g111的调用路径都可能造成了g111的内存泄露,有2类可能:

  1. 该goroutine只调用了少数几次,但消耗了大量的内存,说明每个goroutine调用都消耗了不少内存,内存泄露的原因基本就在该协程内部
  2. 该goroutine的调用次数非常多,虽然每个协程调用过程中消耗的内存不多,但该调用路径上,协程数量巨大,造成消耗大量的内存,并且这些goroutine由于某种原因无法退出,占用的内存不会释放,内存泄露的原因在到g111调用路径上某段代码实现有问题,造成创建了大量的g111

第2种情况,就是goroutine泄露,这是通过heap无法发现的,所以heap在定位内存泄露这件事上,发挥的作用不大

image-20190512144150064


goroutine泄露怎么导致内存泄露

什么是goroutine泄露

如果你启动了1个goroutine,但并没有符合预期的退出,直到程序结束,此goroutine才退出,这种情况就是goroutine泄露。

提前思考:什么会导致goroutine无法退出/阻塞?

goroutine泄露怎么导致内存泄露

每个goroutine占用2KB内存,泄露1百万goroutine至少泄露2KB * 1000000 = 2GB内存,为什么说至少呢?

goroutine执行过程中还存在一些变量,如果这些变量指向堆内存中的内存,GC会认为这些内存仍在使用,不会对其进行回收,这些内存谁都无法使用,造成了内存泄露。

所以goroutine泄露有2种方式造成内存泄露:

  1. goroutine本身的栈所占用的空间造成内存泄露。
  2. goroutine中的变量所占用的堆内存导致堆内存泄露,这一部分是能通过heap profile体现出来的。

Dave在文章中也提到了,如果不知道何时停止一个goroutine,这个goroutine就是潜在的内存泄露:

7.1.1 Know when to stop a goroutine

If you don’t know the answer, that’s a potential memory leak as the goroutine will pin its stack’s memory on the heap, as well as any heap allocated variables reachable from the stack.

怎么确定是goroutine泄露引发的内存泄露

掌握了前面的pprof命令行的基本用法,很快就可以确认是否是goroutine泄露导致内存泄露,如果你不记得了,马上回去看一下go pprof基本知识

判断依据:在节点正常运行的情况下,隔一段时间获取goroutine的数量,如果后面获取的那次,某些goroutine比前一次多,如果多获取几次,是持续增长的,就极有可能是goroutine泄露

goroutine导致内存泄露的demo:

文件:golang_step_by_step/pprof/goroutine/leak_demo1.go

// goroutine泄露导致内存泄露
package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "time"
)

func main() {
    // 开启pprof
    go func() {
        ip := "0.0.0.0:6060"
        if err := http.ListenAndServe(ip, nil); err != nil {
            fmt.Printf("start pprof failed on %s\n", ip)
            os.Exit(1)
        }
    }()

    outCh := make(chan int)
    // 死代码,永不读取
    go func() {
        if false {
            <-outCh
        }
        select {}
    }()

    // 每s起100个goroutine,goroutine会阻塞,不释放内存
    tick := time.Tick(time.Second / 100)
    i := 0
    for range tick {
        i++
        fmt.Println(i)
        alloc1(outCh)
    }
}

func alloc1(outCh chan<- int) {
    go alloc2(outCh)
}

func alloc2(outCh chan<- int) {
    func() {
        defer fmt.Println("alloc-fm exit")
        // 分配内存,假用一下
        buf := make([]byte, 1024*1024*10)
        _ = len(buf)
        fmt.Println("alloc done")

        outCh <- 0 // 53行
    }()
}

编译并运行以上代码,然后使用go tool pprof获取gorourine的profile文件。

go tool pprof http://localhost:6060/debug/pprof/goroutine

已经通过pprof命令获取了2个goroutine的profile文件:

$ ls
/home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz
/home/ubuntu/pprof/pprof.leak_demo.goroutine.002.pb.gz

同heap一样,我们可以使用base对比2个goroutine profile文件:

$go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz

File: leak_demo
Type: goroutine
Time: May 16, 2019 at 2:44pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof) top
Showing nodes accounting for 20312, 100% of 20312 total
      flat  flat%   sum%        cum   cum%
     20312   100%   100%      20312   100%  runtime.gopark
         0     0%   100%      20312   100%  main.alloc2
         0     0%   100%      20312   100%  main.alloc2.func1
         0     0%   100%      20312   100%  runtime.chansend
         0     0%   100%      20312   100%  runtime.chansend1
         0     0%   100%      20312   100%  runtime.goparkunlock
(pprof)

可以看到运行到runtime.gopark的goroutine数量增加了20312个。再通过002文件,看一眼执行到gopark的goroutine数量,即挂起的goroutine数量:

go tool pprof pprof.leak_demo.goroutine.002.pb.gz
File: leak_demo
Type: goroutine
Time: May 16, 2019 at 2:47pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 24330, 100% of 24331 total
Dropped 32 nodes (cum <= 121)
      flat  flat%   sum%        cum   cum%
     24330   100%   100%      24330   100%  runtime.gopark
         0     0%   100%      24326   100%  main.alloc2
         0     0%   100%      24326   100%  main.alloc2.func1
         0     0%   100%      24326   100%  runtime.chansend
         0     0%   100%      24326   100%  runtime.chansend1
         0     0%   100%      24327   100%  runtime.goparkunlock

显示有24330个goroutine被挂起,这不是goroutine泄露这是啥?已经能确定八九成goroutine泄露了。

是什么导致如此多的goroutine被挂起而无法退出?接下来就看怎么定位goroutine泄露。


定位goroutine泄露的2种方法

使用pprof有2种方式,一种是web网页,一种是go tool pprof命令行交互,这两种方法查看goroutine都支持,但有轻微不同,也有各自的优缺点。

我们先看Web的方式,再看命令行交互的方式,这两种都很好使用,结合起来用也不错。

Web可视化查看

Web方式适合web服务器的端口能访问的情况,使用起来方便,有2种方式:

  1. 查看某条调用路径上,当前阻塞在此goroutine的数量
  2. 查看所有goroutine的运行栈(调用路径),可以显示阻塞在此的时间

方式一

url请求中设置debug=1:

http://ip:port/debug/pprof/goroutine?debug=1

效果如下:

看起来密密麻麻的,其实简单又十分有用,看上图标出来的部分,手机上图看起来可能不方便,那就放大图片,或直接看下面各字段的含义:

  1. goroutine profile: total 32023:32023是goroutine的总数量
  2. 32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 ...:32015代表当前有32015个goroutine运行这个调用栈,并且停在相同位置,@后面的十六进制,现在用不到这个数据,所以暂不深究了。
  3. 下面是当前goroutine的调用栈,列出了函数和所在文件的行数,这个行数对定位很有帮助,如下:
32015 @ 0x42e15a 0x42e20e 0x40534b 0x4050e5 0x6d8559 0x6d831b 0x45abe1
#    0x6d8558    main.alloc2.func1+0xf8    /home/ubuntu/heap/leak_demo.go:53
#    0x6d831a    main.alloc2+0x2a    /home/ubuntu/heap/leak_demo.go:54

根据上面的提示,就能判断32015个goroutine运行到leak_demo.go的53行:

func alloc2(outCh chan<- int) {
    func() {
        defer fmt.Println("alloc-fm exit")
        // 分配内存,假用一下
        buf := make([]byte, 1024*1024*10)
        _ = len(buf)
        fmt.Println("alloc done")

        outCh <- 0 // 53行
    }()
}

阻塞的原因是outCh这个写操作无法完成,outCh是无缓冲的通道,并且由于以下代码是死代码,所以goroutine始终没有从outCh读数据,造成outCh阻塞,进而造成无数个alloc2的goroutine阻塞,形成内存泄露:

if false {
    <-outCh
}

方式二

url请求中设置debug=2:

http://ip:port/debug/pprof/goroutine?debug=2

第2种方式和第1种方式是互补的,它可以看到每个goroutine的信息:

  1. goroutine 20 [chan send, 2 minutes]:20是goroutine id,[]中是当前goroutine的状态,阻塞在写channel,并且阻塞了2分钟,长时间运行的系统,你能看到阻塞时间更长的情况。
  2. 同时,也可以看到调用栈,看当前执行停到哪了:leak_demo.go的53行,
goroutine 20 [chan send, 2 minutes]:
main.alloc2.func1(0xc42015e060)
    /home/ubuntu/heap/leak_demo.go:53 +0xf9  // 这
main.alloc2(0xc42015e060)
    /home/ubuntu/heap/leak_demo.go:54 +0x2b
created by main.alloc1
    /home/ubuntu/heap/leak_demo.go:42 +0x3f

命令行交互式方法

Web的方法是简单粗暴,无需登录服务器,浏览器打开看看就行了。但就像前面提的,没有浏览器可访问时,命令行交互式才是最佳的方式,并且也是手到擒来,感觉比Web一样方便。

命令行交互式只有1种获取goroutine profile的方法,不像Web网页分debug=1debug=22中方式,并将profile文件保存到本地:

// 注意命令没有`debug=1`,debug=1,加debug有些版本的go不支持
$ go tool pprof http://0.0.0.0:6060/debug/pprof/goroutine
Fetching profile over HTTP from http://localhost:6061/debug/pprof/goroutine
Saved profile in /home/ubuntu/pprof/pprof.leak_demo.goroutine.001.pb.gz  // profile文件保存位置
File: leak_demo
Type: goroutine
Time: May 16, 2019 at 2:44pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

命令行只需要掌握3个命令就好了,上面介绍过了,详细的倒回去看top, list, traces

  1. top:显示正运行到某个函数goroutine的数量
  2. traces:显示所有goroutine的调用栈
  3. list:列出代码详细的信息。

我们依然使用leak_demo.go这个demo,

$  go tool pprof -base pprof.leak_demo.goroutine.001.pb.gz pprof.leak_demo.goroutine.002.pb.gz
File: leak_demo
Type: goroutine
Time: May 16, 2019 at 2:44pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
(pprof)
(pprof) top
Showing nodes accounting for 20312, 100% of 20312 total
      flat  flat%   sum%        cum   cum%
     20312   100%   100%      20312   100%  runtime.gopark
         0     0%   100%      20312   100%  main.alloc2
         0     0%   100%      20312   100%  main.alloc2.func1
         0     0%   100%      20312   100%  runtime.chansend
         0     0%   100%      20312   100%  runtime.chansend1
         0     0%   100%      20312   100%  runtime.goparkunlock
(pprof)
(pprof) traces
File: leak_demo
Type: goroutine
Time: May 16, 2019 at 2:44pm (CST)
-----------+-------------------------------------------------------
     20312   runtime.gopark
             runtime.goparkunlock
             runtime.chansend
             runtime.chansend1 // channel发送
             main.alloc2.func1 // alloc2中的匿名函数
             main.alloc2
-----------+-------------------------------------------------------

top命令在怎么确定是goroutine泄露引发的内存泄露介绍过了,直接看traces命令,traces能列出002中比001中多的那些goroutine的调用栈,这里只有1个调用栈,有20312个goroutine都执行这个调用路径,可以看到alloc2中的匿名函数alloc2.func1调用了写channel的操作,然后阻塞挂起了goroutine,使用list列出alloc2.func1的代码,显示有20312个goroutine阻塞在53行:

(pprof) list main.alloc2.func1
Total: 20312
ROUTINE ======================== main.alloc2.func1 in /home/ubuntu/heap/leak_demo.go
         0      20312 (flat, cum)   100% of Total
         .          .     48:        // 分配内存,假用一下
         .          .     49:        buf := make([]byte, 1024*1024*10)
         .          .     50:        _ = len(buf)
         .          .     51:        fmt.Println("alloc done")
         .          .     52:
         .      20312     53:        outCh <- 0  // 看这
         .          .     54:    }()
         .          .     55:}
         .          .     56:

友情提醒:使用list命令的前提是程序的源码在当前机器,不然可没法列出源码。服务器上,通常没有源码,那我们咋办呢?刚才介绍了Web查看的方式,那里会列出代码行数,我们可以使用wget下载网页:

$ wget http://localhost:6060/debug/pprof/goroutine?debug=1

下载网页后,使用编辑器打开文件,使用关键字main.alloc2.func1进行搜索,找到与当前相同的调用栈,就可以看到该goroutine阻塞在哪一行了,不要忘记使用debug=2还可以看到阻塞了多久和原因,Web方式中已经介绍了,此处省略代码几十行。


总结

文章略长,但全是干货,感谢阅读到这。然读到着了,跟定很想掌握pprof,建议实践一把,现在和大家温习一把本文的主要内容。

goroutine泄露的本质

goroutine泄露的本质是channel阻塞,无法继续向下执行,导致此goroutine关联的内存都无法释放,进一步造成内存泄露。

goroutine泄露的发现和定位

利用好go pprof获取goroutine profile文件,然后利用3个命令top、traces、list定位内存泄露的原因。

goroutine泄露的场景

泄露的场景不仅限于以下两类,但因channel相关的泄露是最多的。

  1. channel的读或者写:

    1. 无缓冲channel的阻塞通常是写操作因为没有读而阻塞
    2. 有缓冲的channel因为缓冲区满了,写操作阻塞
    3. 期待从channel读数据,结果没有goroutine写
  2. select操作,select里也是channel操作,如果所有case上的操作阻塞,goroutine也无法继续执行。

编码goroutine泄露的建议

为避免goroutine泄露造成内存泄露,启动goroutine前要思考清楚:

  1. goroutine如何退出?
  2. 是否会有阻塞造成无法退出?如果有,那么这个路径是否会创建大量的goroutine?

示例源码

本文所有示例源码,及历史文章、代码都存储在Github,阅读原文可直接跳转,Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/pprof

推荐阅读

这些既是参考资料也是推荐阅读的文章,不容错过。

【Go Blog关于pprof详细介绍和Demo】 https://blog.golang.org/profi...

【Dave关于高性能Go程序的workshop】 https://dave.cheney.net/high-...

【煎鱼pprof文章,很适合入门 Golang大杀器之性能剖析PProf】 https://segmentfault.com/a/11...

【SO上goroutine调用栈各字段的介绍】https://stackoverflow.com/a/3...

【我的老文,有runtime.main的介绍,想学习调度器,可以看下系列文章 Go调度器系列(2)宏观看调度器】http://lessisbetter.site/2019...

  1. 如果这篇文章对你有帮助,不妨关注下我的Github,有文章会收到通知。
  2. 本文作者:大彬
  3. 如果喜欢本文,随意转载,但请保留此原文链接:http://lessisbetter.site/2019/05/18/go-goroutine-leak/

查看原文

赞 85 收藏 58 评论 9

曾纪文 赞了文章 · 2019-01-18

Mars——基于矩阵的统一分布式计算框架

很高兴在这里宣布我们的新项目:Mars,一个基于矩阵的统一分布式计算框架。我们已经在 Github 开源:https://github.com/mars-project/mars 。

背景

Python

Python 是一门相当古老的语言了,如今,在数据科学计算、机器学习、以及深度学习领域,Python 越来越受欢迎。

大数据领域,由于 hadoop 和 spark 等,Java 等还是占据着比较核心的位置,但是在 spark 上也可以看到,pyspark 的用户占据很大一部分。

深度学习领域,绝大部分的库(tensorflow、pytorch、mxnet、chainer)都支持 Python 语言,且 Python 语言也是这些库上使用最广泛的语言。

对 MaxCompute 来说,Python 用户也是一股重要力量。

PyData(numpy、scipy、pandas、scikit-learn、matplotlib)

Python 在数据科学领域,有非常丰富的包可以选择,下图展示了整个 Python 数据科学技术栈。

可以看到 numpy 作为基础,在其上,有 scipy 面向科学家,pandas 面向数据分析,scikit-learn 则是最著名的机器学习库,matplotlib 专注于可视化。

对 numpy 来说,其中最核心的概念就是 ndarray——多维数组,pandas、scikit-learn 等库都构建于这个数据结构基础之上。

问题

虽然 Python 在这些领域越来越流行,PyData 技术栈给数据科学家们提供了多维矩阵、DataFrame 上的分析和计算能力、基于二维矩阵的机器学习算法,但这些库都仅仅受限于单机运算,在大数据时代,数据量一大,这些库的处理能力都显得捉襟见肘。

虽然大数据时代,有各种各样基于 SQL 的计算引擎,但对科学计算领域,这些引擎都不太适合用来进行大规模的多维矩阵的运算操作。而且,相当一部分用户,尤其是数据科学家们,习惯于使用各种成熟的单机库,他们不希望改变自己的使用习惯,去学习一些新的库和语法。

此外,在深度学习领域,ndarray/tensor 也是最基本的数据结构,但它们仅仅限制在深度学习上,也不适合大规模的多维矩阵运算。

基于这些考量,我们开发了 Mars,一个基于 tensor 的统一分布式计算框架,前期我们关注怎么将 tensor 这层做到极致。

我们的工作

Mars 的核心用 python 实现,这样做的好处是能利用到现有的 Python 社区的工作,我们能充分利用 numpy、cupy、pandas 等来作为我们小的计算单元,我们能快速稳定构建我们整个系统;其次,Python 本身能轻松和 c/c++ 做继承,我们也不必担心 Python 语言本身的性能问题,我们可以对性能热点模块轻松用 c/cython 重写。

接下来,主要集中介绍 Mars tensor,即多维矩阵计算的部分。

Numpy API

Numpy 成功的一个原因,就是其简单易用的 API。Mars tensor 在这块可以直接利用其作为我们的接口。所以在 numpy API 的基础上,用户可以写出灵活的代码,进行数据处理,甚至是实现各种算法。

下面是两段代码,分别是用 numpy 和 Mars tensor 来实现一个功能。

import numpy as np

a = np.random.rand(1000, 2000)
(a + 1).sum(axis=1)
import mars.tensor as mt

a = mt.random.rand(1000, 2000)
(a + 1).sum(axis=1).execute()

这里,创建了一个 1000x2000 的随机数矩阵,对其中每个元素加1,并在 axis=1(行)上求和。

目前,Mars 实现了大约 70% 的 Numpy 常用接口。

可以看到,除了 import 做了替换,用户只需要通过调用 execute 来显式触发计算。通过 execute 显式触发计算的好处是,我们能对中间过程做更多的优化,来更高效地执行计算。

不过,静态图的坏处是牺牲了灵活性,增加了 debug 的难度。下个版本,我们会提供 instant/eager mode,来对每一步操作触发计算,这样,用户能更有效地进行 debug,且能利用到 Python 语言来做循环,当然性能也会有所损失。

使用 GPU 计算

Mars tensor 也支持使用 GPU 计算。对于某些矩阵创建的接口,我们提供了 gpu=True 的选项,来指定分配到 GPU,后续这个矩阵上的计算将会在 GPU 上进行。

import mars.tensor as mt

a = mt.random.rand(1000, 2000, gpu=True)
(a + 1).sum(axis=1).execute()

这里 a 是分配在 GPU 上,因此后续的计算在 GPU 上进行。

稀疏矩阵

Mars tensor 支持创建稀疏矩阵,不过目前 Mars tensor 还只支持二维稀疏矩阵。比如,我们可以创建一个稀疏的单位矩阵,通过指定 sparse=True 即可。

import mars.tensor as mt

a = mt.eye(1000, sparse=True, gpu=True)
b = (a + 1).sum(axis=1)

这里看到,gpu 和 sparse 选项可以同时指定。

基于 Mars tensor 的上层建筑

这部分在 Mars 里尚未实现,这里提下我们希望在 Mars 上构建的各个组件。

DataFrame

相信有部分同学也知道 PyODPS DataFrame,这个库是我们之前的一个项目,它能让用户写出类似 pandas 类似的语法,让运算在 ODPS 上进行。但 PyODPS DataFrame 由于 ODPS 本身的限制,并不能完全实现 pandas 的全部功能(如 index 等),而且语法也有不同。

基于 Mars tensor,我们提供 100% 兼容 pandas 语法的 DataFrame。使用 mars DataFrame,不会受限于单个机器的内存。这个是我们下个版本的最主要工作之一。

机器学习

scikit-learn 的一些算法的输入就是二维的 numpy ndarray。我们也会在 Mars 上提供分布式的机器学习算法。我们大致有以下三条路:

  1. scikit-learn 有些算法支持 partial_fit,因此,我们直接在每个 worker 上调用 sklearn 的算法。
  2. 提供基于 Mars 的 joblib 后端。由于 sklearn 使用 joblib 来做并行,因此,我们可以通过实现 joblib 的 backend,来让 scikit-learn 直接跑在 Mars 的分布式环境。但是,这个方法的输入仍然是 numpy ndarray,因此,总的输入数据还是受限于内存。
  3. 在 Mars tensor 的基础上实现机器学习算法,这个方法需要的工作量是最高的,但是,好处是,这些算法就能利用 Mars tensor 的能力,比如 GPU 计算。以后,我们需要更多的同学来帮我们贡献代码,共建 Mars 生态。

细粒度的函数和类

Mars 的核心,其实是一个基于 Actor 的细粒度的调度引擎。因此,实际上,用户可以写一些并行的 Python 函数和类,来进行细粒度的控制。我们可能会提供以下几种接口。

函数

用户能写普通的 Python 函数,通过 mars.remote.spawn 来将函数调度到 Mars 上来分布式运行

import mars.remote as mr

def add(x, y):
    return x + y

data = [
   (1, 2),
   (3, 4)
]

for item in data:
    mr.spawn(add, item[0], item[1])

利用 mr.spawn,用户能轻松构建分布式程序。在函数里,用户也可以使用 mr.spawn,这样,用户可以写出非常精细的分布式执行程序。

有时候,用户需要一些有状态的类,来进行更新状态等操作,这些类在 Mars 上被称为 RemoteClass。

import mars.remote as mr

class Counter(mr.RemoteClass):
    def __init__(self):
        self.value = 0

    def inc(self, n=1):
        self.value += n

counter = mr.spawn(Counter)
counter.inc()

目前,这些函数和类的部分尚未实现,只是在构想中,所以届时接口可能会做调整。

内部实现

这里,我简单介绍下 Mars tensor 的内部原理。

客户端

在客户端,我们不会做任何真正的运算操作,用户写下代码,我们只会在内存里用图记录用户的操作。

对于 Mars tensor 来说,我们有两个重要的概念,operand 和 tensor,分别如下图的蓝色圆和粉色方块所示。Operand 表示算子,tensor 表示生成的多维数组。

比如,下图,用户写下这些代码,我们会依次在图上生成对应的 operand 和 tensor。

当用户显式调用 execute 的时候,我们会将这个图提交到 Mars 的分布式执行环境。

我们客户端部分,并不会对语言有任何依赖,只需要有相同的 tensor graph 序列化,因此可以用任何语言实现。下个版本我们要不要提供 Java 版本的 Mars tensor,我们还要看是不是有用户需要。

分布式执行环境

Mars 本质上是一个对细粒度图的执行调度系统。

对于 Mars tensor 来说,我们接收到了客户端的 tensor 级别的图(粗粒度),我们要尝试将其转化成 chunk 级别的图(细粒度)。每个 chunk 以及其输入,在执行时,都应当能被内存放下。我们称这个过程叫做 tile。

在拿到细粒度的 chunk 级别的图后,我们会将这个图上的 Operand 分配到各个 worker 上去执行。

总结

Mars 在九月份的云栖大会发布,目前我们已经在 Github 开源:https://github.com/mars-project/mars。我们项目完全以开源的方式运作,而不是简单把代码放出来。

期待有更多的同学能参与 Mars,共建 Mars。

努力了很久,我们不会甘于做一个平庸的项目,我们期待对世界做出一点微小的贡献——我们的征途是星辰大海!



本文作者:继盛

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 8 收藏 4 评论 0

曾纪文 赞了文章 · 2019-01-18

阿里重磅开源首款自研科学计算引擎Mars,揭秘超大规模科学计算

摘要: 由阿里巴巴统一大数据计算平台MaxCompute研发团队,历经1年多研发,打破大数据、科学计算领域边界,完成第一个版本并开源。 Mars,一个基于张量的统一分布式计算框架。使用 Mars 进行科学计算,不仅使得完成大规模科学计算任务从MapReduce实现上千行代码降低到Mars数行代码,更在性能上有大幅提升。

日前,阿里巴巴正式对外发布了分布式科学计算引擎 Mars 的开源代码地址,开发者们可以在pypi上自主下载安装,或在Github上获取源代码并参与开发。

此前,早在2018年9月的杭州云栖大会上,阿里巴巴就公布了这项开源计划。Mars 突破了现有大数据计算引擎的关系代数为主的计算模型,将分布式技术引入科学计算/数值计算领域,极大地扩展了科学计算的计算规模和效率。目前已应用于阿里巴巴及其云上客户的业务和生产场景。本文将为大家详细介绍Mars的设计初衷和技术架构。

    • *

概述

科学计算即数值计算,是指应用计算机处理科学研究和工程技术中所遇到的数学计算问题。比如图像处理、机器学习、深度学习等很多领域都会用到科学计算。有很多语言和库都提供了科学计算工具。这其中,Numpy以其简洁易用的语法和强大的性能成为佼佼者,并以此为基础形成了庞大的技术栈。(下图所示)

Numpy的核心概念多维数组是各种上层工具的基础。多维数组也被称为张量,相较于二维表/矩阵,张量具有更强大的表达能力。因此,现在流行的深度学习框架也都广泛的基于张量的数据结构。

随着机器学习/深度学习的热潮,张量的概念已逐渐为人所熟知,对张量进行通用计算的规模需求也与日俱增。但现实是如Numpy这样优秀的科学计算库仍旧停留在单机时代,无法突破规模瓶颈。当下流行的分布式计算引擎也并非为科学计算而生,上层接口不匹配导致科学计算任务很难用传统的SQL/MapReduce编写,执行引擎本身没有针对科学计算优化更使得计算效率难以令人满意。

基于以上科学计算现状,由阿里巴巴统一大数据计算平台MaxCompute研发团队,历经1年多研发,打破大数据、科学计算领域边界,完成第一个版本并开源。 Mars,一个基于张量的统一分布式计算框架。使用 Mars 进行科学计算,不仅使得完成大规模科学计算任务从MapReduce实现上千行代码降低到Mars数行代码,更在性能上有大幅提升。目前,Mars 实现了 tensor 的部分,即numpy 分布式化, 实现了 70% 常见的 numpy 接口。后续,在 Mars 0.2 的版本中, 正在将 pandas 分布式化,即将提供完全兼容 pandas 的接口,以构建整个生态。 

Mars作为新一代超大规模科学计算引擎,不仅普惠科学计算进入分布式时代,更让大数据进行高效的科学计算成为可能。

Mars的核心能力

  • 符合使用习惯的接口
    Mars 通过 tensor 模块提供兼容 Numpy 的接口,用户可以将已有的基于 Numpy 编写的代码,只需替换 import,就可将代码逻辑移植到 Mars,并直接获得比原来大数万倍规模,同时处理能力提高数十倍的能力。目前,Mars 实现了大约 70% 的常见 Numpy 接口。

  • 充分利用GPU加速
    除此之外,Mars 还扩展了 Numpy,充分利用了GPU在科学计算领域的已有成果。创建张量时,通过指定 gpu=True 就可以让后续计算在GPU上执行。比如:
a = mt.random.rand(1000, 2000, gpu=True)  # 指定在 GPU 上创建
(a + 1).sum(axis=1).execute()
  • 稀疏矩阵
    Mars 还支持二维稀疏矩阵,创建稀疏矩阵的时候,通过指定 sparse=True 即可。以eye 接口为例,它创建了一个单位对角矩阵,这个矩阵只有对角线上有值,其他位置上都是 0,所以,我们可以用稀疏的方式存储。
a = mt.eye(1000, sparse=True)  # 指定创建稀疏矩阵
(a + 1).sum(axis=1).execute()

系统设计

接下来介绍 Mars 的系统设计,让大家了解 Mars 是如何让科学计算任务自动并行化并拥有强大的性能。

  • 分而治之—tile
    Mars 通常对科学计算任务采用分而治之的方式。给定一个张量,Mars 会自动将其在各个维度上切分成小的 Chunk 来分别处理。对于 Mars 实现的所有的算子,都支持自动切分任务并行。这个自动切分的过程在Mars里被称为 tile。

比如,给定一个 1000 2000 的张量,如果每个维度上的 chunk 大小为 500,那么这个张量就会被 tile 成 2 4 一共 8 个 chunk。对于后续的算子,比如加法(Add)和求和(SUM),也都会自动执行 tile 操作。一个张量的运算的 tile 过程如下图所示。

  • 延迟执行和 Fusion 优化
    目前 Mars 编写的代码需要显式调用 execute 触发,这是基于 Mars 的延迟执行机制。用户在写中间代码时,并不会需要任何的实际数据计算。这样的好处是可以对中间过程做更多优化,让整个任务的执行更优。目前 Mars 里主要用到了 fusion 优化,即把多个操作合并成一个执行。

对于前面一个图的例子,在 tile 完成之后,Mars 会对细粒度的 Chunk 级别图进行 fusion 优化,比如8个 RAND+ADD+SUM,每个可以被分别合并成一个节点,一方面可以通过调用如 numexpr 库来生成加速代码,另一方面,减少实际运行节点的数量也可以有效减少调度执行图的开销。

  • 多种调度方式
    Mars 支持多种调度方式:

| 多线程模式:Mars 可以使用多线程来在本地调度执行 Chunk 级别的图。对于 Numpy 来说,大部分算子都是使用单线程执行,仅使用这种调度方式,也可以使得 Mars 在单机即可获得 tile 化的执行图的能力,突破 Numpy 的单机内存限制,同时充分利用单机所有 CPU/GPU 资源,获得比 Numpy 快数倍的性能。

| 单机集群模式: Mars 可以在单机启动整个分布式运行时,利用多进程来加速任务的执行;这种模式适合模拟面向分布式环境的开发调试。

| 分布式 : Mars 可以启动一个或者多个 scheduler,以及多个 worker,scheduler 会调度 Chunk 级别的算子到各个 worker 去执行。

下图是 Mars 分布式的执行架构:

Mars 分布式执行时会启动多个 scheduler 和 多个 worker,图中是3个 scheduler 和5个 worker,这些 scheduler 组成一致性哈希环。用户在客户端显式或隐式创建一个 session,会根据一致性哈希在其中一个 scheduler 上分配 SessionActor,然后用户通过 execute 提交了一个张量的计算,会创建 GraphActor 来管理这个张量的执行,这个张量会在 GraphActor 中被 tile 成 chunk 级别的图。这里假设有3个 chunk,那么会在 scheduler 上创建3个 OperandActor 分别对应。这些 OperandActor 会根据自己的依赖是否完成、以及集群资源是否足够来提交到各个 worker 上执行。在所有 OperandActor 都完成后会通知 GraphActor 任务完成,然后客户端就可以拉取数据来展示或者绘图。

  • 向内和向外伸缩
    Mars 灵活的 tile 化执行图配合多种调度模式,可以使得相同的 Mars 编写的代码随意向内(scale in)和向外(scale out)伸缩。向内伸缩到单机,可以利用多核来并行执行科学计算任务;向外伸缩到分布式集群,可以支持到上千台 worker 规模来完成单机无论如何都难以完成的任务。

Benchmark

在一个真实的场景中,我们遇到了巨型矩阵乘法的计算需求,需要完成两个均为千亿元素,大小约为2.25T的矩阵相乘。Mars通过5行代码,使用1600 CU(200个 worker,每 worker 为 8核 32G内存),在2个半小时内完成计算。在此之前,同类计算只能使用 MapReduce 编写千余行代码模拟进行,完成同样的任务需要动用 9000 CU 并耗时10个小时。

让我们再看两个对比。下图是对36亿数据矩阵的每个元素加一再乘以二,红色的叉表示 Numpy 的计算时间,绿色的实线是 Mars 的计算时间,蓝色虚线是理论计算时间。可以看到单机 Mars 就比 Numpy 快数倍,随着 Worker 的增加,可以获得几乎线性的加速比。

下图是进一步扩大计算规模,把数据扩大到144亿元素,对这些元素加一乘以二以后再求和。这时候输入数据就有 115G,单机的 Numpy 已经无法完成运算,Mars 依然可以完成运算,且随着机器的增多可以获得还不错的加速比。

开源地址

Mars 已经在 Github 开源:https://github.com/mars-project/mars ,且后续会全部在 Github 上使用标准开源软件的方式来进行开发,欢迎大家使用 Mars,并成为 Mars 的 contributor。

Mars科学计算引擎产品发布会

发布直播回放>>
发布活动页>> 
大数据计算服务MaxCompute官网>>
MaxCompute试用申请页面>>
聚能聊>>



本文作者:晋恒

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 3 收藏 2 评论 0

曾纪文 评论了文章 · 2019-01-10

golang orm对比

各主流的golang orm介绍

当前较为主流/活跃的orm有gorm、xorm、gorose等

xorm

  • 文档

  • 支持的数据库有:mysql、mymysql、postgres、tidb、sqlite、mssql、oracle
  • 事务性支持
  • 链式api

    has, err := engine.Where("name = ?", name).Desc("id").Get(&user)
    err := engine.Where(builder.NotIn("a", 1, 2).And(builder.In("b", "c", "d", "e"))).Find(&users)
  • 支持原生sql操作
  • 查询缓存
  • 可根据数据库反转生成代码
  • 级联加载
  • 提供sql语句日志输出
  • 支持批量查询处理

    // 每次处理100条
    // SELECT * FROM user Limit 0, 100
    // SELECT * FROM user Limit 101, 100
    err := engine.BufferSize(100).Iterate(&User{Name:name}, func(idx int, bean interface{}) error {
        user := bean.(*User)
        return nil
    })
  • 自动化的读写分离/主从式
dataSourceNameSlice := []string{masterDataSourceName, slave1DataSourceName, slave2DataSourceName}
engineGroup, err := xorm.NewEngineGroup(driverName, dataSourceNameSlice)

gorm

  • 文档

  • hook机制(Before/After Create/Save/Update/Delete/Find)
  • 对象关系Has One, Has Many, Belongs To, Many To Many, Polymorphism
  • 热加载
  • 支持原生sql操作
  • 事务性
  • 链式api

    tx := db.Where("name = ?", "jinzhu").Where("age = ?", 20).Find(&users)
  • 支持的数据库有:mysql、postgre、sqlite、sqlserver
  • 查询操作

    // Get first record, order by primary key
    db.First(&user)
    //// SELECT * FROM users ORDER BY id LIMIT 1;
    
    // plain sql
    db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
    // map
    db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
    //// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 LIMIT 1;

gorose

  • 文档

  • 支持的数据库有:mysql、postgres、sqlite、mssql、oracle
  • 链式api
  • 同时连接多个数据库和切换
  • 支持原生sql操作
  • 支持批量查询处理
  • 事务性

    User.Fields("id, name").Where("id",">",2).Chunk(2, func(data []map[string]interface{}) {
        // for _,item := range data {
        //     fmt.Println(item)
        // }
        fmt.Println(data)
    })

upper/db

同时支持nosql和sql的orm不多,这是其中之一 (另一个是beedb,已经四年没有更新了). upper/db对多种数据库进行封装,提供统一的接口进行CRUD.

  • 文档

  • 支持的数据库有:PostgreSQL, MySQL, SQLite, MSSQL, QL and MongoDB.
  • 不支持根据数据库类生成数据库表等DCL操作,只有DQL,DML
  • 与大部分orm 框架相同,提供连接池
  • 对RDBMS支持事务性
sess, err := postgresql.Open(settings)
if err != nil {
    log.Fatalf("db.Open(): %q\n", err)
}
defer sess.Close()

var books []Book
err = sess.Collection("books").Find().All(&books)

总结

  • 相似性

    • 各orm支持的数据库都基本相同(主流数据库都支持)
    • 支持事务性、链式查询等
  • 差异

    • xorm、gorose支持批量查询处理
    • xorm支持主从式读写分离
    • gorm支持热加载
    • gorose便于在多个数据库切换
    • 文档全面性gorm>xorm>gorose
查看原文

曾纪文 收藏了文章 · 2019-01-10

2018年自然语言处理最值得关注的研究、论文和代码

摘要: NLP 与情感分析、增强学习、深度学习的交叉领域,全年干货大合集。

2018年对于自然语言处理(NPL)是很有意义的一年,见证了许多新的研究方向和尖端成果。Elvis Saravia 是计算语言学专家,也是2019 计算语言学会年度大会北美分部的项目委员之一。他总结了2018年 NLP 的重要进展,包括增强学习、情感分析和深度学习等领域。
点击文章中的链接,可获得每一项研究的详细信息、论文或者代码。

综合领域

  • Facebook 研究员们发明了一种机器翻译的新方法,只需要使用单一语言语料库,这对于缺乏资料的语言非常有用。
  • Young 和同事更新了他们近期发表的论文《基于深度学习的自然语言处理的最新趋势》,增加了 NLP 文献中最新的 SQuAD 结果。
  • Bloomberg 研究员 Yi Yang 发表了 RNN 卷积筛选建模的最新论文及代码,称体现了语言中的长期依存性和组合性。
  • 百度发布了 Deep Voice 3,是一项基于注意、完全卷积的文字语音转换神经系统,比目前的循环系统在神经语音合成方面快几个量级。
  • Pair2vec 是一种学习文字嵌入对的新方法,能体现隐含关系的背景知识。
  • 百度发布了名为同声翻译与预测及可控延迟(STACL)的机器翻译算法,能够同时进行多个翻译。在同声传译时,这项技术不需要等发言者暂停发言,而可以预测发言者的下一个词语。
  • Deep INFOMAX 是一种学习无监督表征的方法,将输入和高层特征矢量之间的共同信息最大化。

  • 蒙特利尔大学的 AI 研究团队 MILA 发表了多个超赞的研究成果,记录了自然语言生成(NLG)任务中 GAN 的限制。
  • 聊天机器人是 NLP 的一个重要研究领域,创业公司 lang.ai 如何使用无监督 AI 来解决打造聊天机器人的重要挑战之一:理解用户到底想要什么
  • 这个模型提供了一种新方法进行文字生成,可以实现更强的解读性和控制性。
  • 谷歌 AI 发表了论文,探索语言建模的极限。
  • 亚马逊研究员提出了一种语言建模方法,这种新的训练策略的重要性在于,在现实中,要获得大量的训练数据来建立一种新能力经常是不现实的。

增强学习

许多研究员认为增强学习是机器学习的最前端。 我们来看看这个领域内,2018年都有哪些重大进展。

  • David Ha 的“世界模型”,目标是研究个体能否在其自身的梦境中学习,需要利用增强学习来学习一项规则,依靠世界模型中抽取的特征来解决制定的任务。
  • OpenAI 开发了一个类似人类的机器人手,通过增强学习算法获得操纵物体的灵活性。
  • DeepMind 在《自然》杂志发布了一篇论文,讨论虚拟环境中,人工个体的网格表征怎样通过矢量导航找到解决。
  • TextWorld 是以文字游戏为灵感的学习环境,用于训练增强学习代理。

  • Google研究员开发了一项名为 MnasNet 的技术,是一种自动化神经网络架构搜索方法,用于通过增强学习设计移动化机器学习模型。
  • OpenAI Five 利用增强学习,能在复杂游戏 Dota 2 中打败业余水准的人类选手。
  • DeepMind 开发了名为 PopArt 的技术,利用增强学习在多任务环境中具有高精确度。
  • 三星的 CozNet 是一种增强学习算法,在两项知名的NPL比赛中展现了顶尖表现。
  • Arel 使用对抗奖励学习来讲故事,解决故事评估维度的局限。
  • Metacar 是为无人驾驶汽车而设计的增强学习环境,在以 Tensorflow.js 打造的浏览器上使用。
  • OpenAI 发布的 Gym Retro 平台提供 1000多个游戏,进行增强学习研究。

情感分析

机器学习系统需要更深的理解能力,才能与人类在情感层面互动。

  • 这篇新论文提出了“层级化CVAE用于精准的仇恨言论分类”,能够理解40多个群体和13中不同类别的仇恨言论。
  • 这篇论文讨论如何使用简单的支持向量机变量获得最佳结果,并指出模型方面需要注意的几点。目前大部分情感分析都是基于神经方法,研究时需要注意模型和特征的选择。
  • 这篇论文定量分析了推特表情肤色修改器的使用效果。

  • 这篇论文讨论如何使用深度卷积神经网络检测讽刺。另外,这项新研究使用眼动追踪、NLP和深度学习算法检测讽刺。
  • 研究员开发了一项成为“情感聊天机器”的方法,这个聊天机器人不仅能给出符合事实与逻辑的答案,还能在聊天中加入悲伤、厌烦等情绪。
  • Lei Zhang 等研究员发表了一篇论文,综合概括了深度学习方法如何用于情感分析。
  • 这个双向异步框架可以在聊天中生成有意义的情感回复。
  • 这项研究使用计算机视觉方法,研究语境中的情感识别。
  • 这篇在2018 NAACL 大会上发表的论文,提出了一种方法可以使用简单的递归神经网络模拟情感流。

深度学习

  • DeepMind 与哈佛大学教师 Wouter Kool 合作发表了论文,研究人类如何使用大脑来做决定,以及这些研究结果能如何启发人工智能的研究。
  • 这篇论文引入了“群组归一化”的概念,可以有效替代批归一化,被认为是深度学习的一项重要技术。
  • Sperichal CNN 是一种打造卷积神经网络的新方法。
  • BAIR 发布了一篇文章,讨论目前循环神经网络和前馈神经网络在解决各种问题时的优缺点。
  • Facebook 的 AI 研究小组开发了一项新技术,能将 AI 模型运行效率提升16%。这能提高 AI 模型的训练速度,并简化模型的量化和运行。
  • 这篇《自然》杂志论文,介绍了一种可以预测地震后余震位置的深度学习方法。
  • DeepMind 研究员开发了一种新方法,利用神经算数逻辑单元(NALU)改善神经网络,追踪时间、用数字图片运行算数、数图片中的物体个数等等。
  • DARTS 是一种架构搜索算法,可以设计高性能的图像分类卷积架构。
  • 这篇论文《实证验证序列建模中的通用卷积网络和神经网络》,讨论了序列建模中 CNN 和 RNN 的区别。
  • 图形神经网络如何帮助推断潜在关系结构、模拟多代理和物理动态。
  • 谷歌 AI 研究团队发布了一篇论文,提出了一种改进版的 RNN,能够提高自动数据解读的精确度。
  • Distill 发布了新研究,可以在一个数据源的语境下分析另一个数据。
  • 如果没有任何数据、也没有任何人类知识工程,有可能习得精准的认知模型吗?这项研究会告诉你答案。
  • 这篇论文详细描述了针对深度神经网络的批归一化研究。
  • 这篇论文回顾了神经网络中,如何更好地进行批训练。
  • 这篇论文讨论如何正确评估深度半监督学习算法。



本文作者:【方向】

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

曾纪文 发布了文章 · 2019-01-06

Go routine调度

go routine的调度原理和操作系统的线层调度是比较相似的。这里我们将介绍go routine的相关知识。

goroutine(有人也称之为协程)本质上go的用户级线程的实现,这种用户级线程是运行在内核级线程之上。当我们在go程序中创建goroutine的时候,我们的这些routine将会被分配到不同的内核级线程中运行。一个内核级线程可能会负责多个routine的运行。而保证这些routine在内内核级线程安全、公平、高效运行的工作,就由调度器来实现。

Go调度的组成

Go的调度主要有四个结构组成,分别是:

  • G:goroutine的核心结构,包括routine的栈、程序计数器pc、以及一些状态信息等;
  • M:内核级线程。goroutine在M上运行。M中信息包括:正在运行的goroutine、等待运行的routine列表等。当然也包括操作系统线程相关信息,这些此处不讨论。
  • P:processor,处理器,只要用于执行goroutine,维护了一个goroutine列表。其实P是可以从属于M的。当P从属于(分配给)M的时候,表示P中的某个goroutine得以运行。当P不从属于M的时候,表示P中的所有goroutine都需要等待被安排到内核级线程运行。
  • Sched:调度器,存储、维护M,以及一个全局的goroutine等待队列,以及其他状态信息。

Go程序的启动过程

  • 初始化Sched:一个存储P的列表pidle。P的数量可以通过GOMAXPROCS设置;
  • 创建第一个goroutine。这个goroutine会创建一个M,这个内核级线程(sysmon)的工作是对goroutine进行监控。之后,这个goroutine开始我们在main函数里面的代码,此时,该goroutine就是我们说的主routine。

创建goroutine:

  • goroutine创建时指定了代码段
  • 然后,goroutine被加入到P中去等待运行。
  • 这个新建的goroutine的信息包含:栈地址、程序计数器

创建内核级线程M

内核级线程由go的运行时根据实际情况创建,我们无法再go中创建内核级线程。那什么时候回创建内核级线程呢?当前程序等待运行的goroutine数量达到一定数量及存在空闲(为被分配给M)的P的时候,Go运行时就会创建一些M,然后将空闲的P分配给新建的内核级线程M,接着才是获取、运行goroutine。创建M的接口函数如下:

// 创建M的接口函数
void newm(void (*fn)(void), P *p)

// 分配P给M
if(m != &runtime·m0) {Â
    acquirep(m->nextp);
    m->nextp = nil;
}
// 获取goroutine并开始运行
schedule();

M的运行

static void schedule(void)
{
    G *gp;

    gp = runqget(m->p);
    if(gp == nil)
        gp = findrunnable();

  // 如果P的类别不止一个goroutine,且调度器中有空闲的的P,就唤醒其他内核级线程M
    if (m->p->runqhead != m->p->runqtail &&
        runtime·atomicload(&runtime·sched.nmspinning) == 0 &&
        runtime·atomicload(&runtime·sched.npidle) > 0)  // TODO: fast atomic
        wakep();
  // 执行goroutine
    execute(gp);
}
  • runqget: 从P中获取goroutine即gp。gp可能为nil(如M刚创建时P为空;或者P的goroutine已经运行完了)。
  • findrunnable:寻找空闲的goroutine(从全局的goroutine等待队列获取goroutine;如果所有goroutine都已经被分配了,那么从其他M的P的goroutine的goroutine列表获取一些)。如果获取到goroutine,就将他放入P中,并执行它;否则没能获取到任何的goroutine,该内核级线程进行系统调用sleep了。
  • wakep:当当前内核级线程M的P中不止一个goroutine且调度器中有空闲的的P,就唤醒其他内核级线程M。(为了找些空闲的M帮自己分担)。

Routine状态迁移

前面说的是G,M是怎样创建的以及什么时候创建、运行。那么goroutine在M是是怎样进行调度的呢?这个才是goroutine的调度核心问题,即上面代码中的schedule。在说调度之前,我们必须知道goroutine的状态有什么,以及各个状态之间的关系。

图片描述

  • Gidle:创建中的goroutine,实际上这个状态没有什么用;
  • Grunnable:新创建完成的goroutine在完成了资源的分配及初始化后,会进入这个状态。这个新创建的goroutine会被分配到创建它的M的P中;
  • Grunning:当Grunnable中的goroutine等到了空闲的cpu或者到了自己的时间片的时候,就会进入Grunning状态。这个装下的goroutine可以被前文提到的findrunnable函数获取;
  • Gwaiting:当正在运行的goroutine进行一些阻塞调用的时候,就会从Grunning状态进入Gwaiting状态。常见的调用有:写入一个满的channel、读取空的channel、IO操作、定时器Ticker等。当阻塞调用完成后,goroutine的状态就会从Gwaiting转变为Grunnable;
  • Gsyscall:当正在运行的goroutine进行系统调用的时候,其状态就会转变为Gsyscall。当系统调用完成后goroutine的状态就会变为Grunnable。(前文提到的sysmon进程会监控所有的P,如果发现有的P的系统调用是阻塞式的或者执行的时间过长,就会将P从原来的M分离出来,并新建一个M,将P分配给这个新建的M)。

Ref

查看原文

赞 2 收藏 1 评论 0

曾纪文 评论了文章 · 2018-12-18

protobuffer、gRPC、restful gRPC的相互转化

文档

protobuf

 Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

优点

  • 平台无关,语言无关,可扩展;
  • 提供了友好的动态库,使用简单;
  • 解析速度快,比对应的XML快约20-100倍;
  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

安装

tax -xvf protobuf-all-xxx.tar.gz
cd protobuf-xxx
./configure
make
make check
sudo make install
  • 安装其他库
go get github.com/golang/protobuf/proto   // golang的protobuf库文件

// 插件
go get github.com/golang/protobuf/protoc-gen-go  // 用于根据protobuf生成golang代码,语法 protoc --go_out=. *.proto

语法

book/book.proto

syntax="proto3";
package book;

// import "xxx/xx.proto"

// 出版社
message Publisher{
    required string name = 1
}  
// 书籍信息
message Book {
     required string name = 1;
    message Author {
        required string name = 1;
        required string address = 1;
    }
    required Author author = 2;

    enum BookType{
        SCIENCE = 1 ;
        LITERATURE = 2;
    }

    optional BookType type = 3;
    optional Publisher publisher = 4
}
  • syntax="proto3":指定protobuf的版本
  • package book:声明一个报名,一般与文件目录名相同
  • import "xxx/xx.proto":导入其他的包,这样你就可以使用其他的包的数据结构
  • required、optional、repeated:表示该字段是否必须填充;required表示必须指定且只能指定一个;当optional表示可选,可指定也可不指定,但不可超过一个不指定值的时候会采用空值,如string类型的字段会用字符串表示;repeated表示可以重复,类似与编程语言中的list
  • message Author:在一个message体内定义一个message结构体
  • enum:是枚举类型结构体
  • 数字:字段的标识符,不可重复
  • 数据类型: int32、int64、uint32、uint64、sint32、sint64、double、float、 string、bool、bytes、enum、message等等

在golang使用

protobuf采用以上的book.proto文件

并使用以下命令生成go文件

protoc --go_out=. *.proto

在代码中使用

package main

import (
    b "book"
    "github.com/golang/protobuf/proto"
)

func main(){
    ...
    // 将实例转为proto编码
    var b = &b.Book{Name:"xxx", Author:b.Author{Name:"yyy"}}
    protoBook, err := proto.Marshal(b)
    ...
    // 讲proto编码转化为实例
    var b2 b.Book
    err = proto.Unmarshal(protoBook, &b2)    
    ...
}

grpc

gRPC是由Google主导开发的RPC框架,使用HTTP/2协议并用ProtoBuf作为序列化工具。其客户端提供Objective-C、Java接口,服务器侧则有Java、Golang、C++等接口。使用grpc可以方便的调用其他进程的方法,调用需要传输的数据使用的是proto编码。这对于大型项目来说,可以有效的提高数据的解编码效率和数据传输率。

proto service定义

一个RPC service就是一个能够通过参数和返回值进行远程调用的method,我们可以简单地将它理解成一个函数。因为gRPC是通过将数据编码成protocal buffer来实现传输的。因此,我们通过protocal buffers interface definitioin language(IDL)来定义service method,同时将参数和返回值也定义成protocal buffer message类型。具体实现如下所示,包含下面代码的文件叫helloworld.proto:

syntax = "proto3";
 
 package helloworld;
 
// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
 
// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}
 
// The response message containing the greetings
message HelloReply {
  string message = 1;
}

接着,根据上述定义的service,我们可以利用protocal buffer compiler ,即protoc生成相应的服务器端和客户端的GoLang代码。生成的代码中包含了客户端能够进行RPC的方法以及服务器端需要进行实现的接口。

假设现在所在的目录是$GOPATH/src/helloworld/helloworld,我们将通过如下命令生成gRPC对应的GoLang代码:

protoc --go_out=plugins=grpc:. helloworld.proto

此时,将在目录下生成helloworld.pb.go文件

server

server.go

package main
 
// server.go
 
import (
    "log"
    "net"
 
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "helloworld/helloworld"
)
 
const (
    port = ":50051"
)
 
type server struct {}

// 当接收到请求的时候回调用该方法
// 参数由grpc自己根据请求进行构造 
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
 
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    s.Serve(lis)
}

其中pb是我们刚才根据proto生成的go文件的包

client

package main
 
//client.go
 
import (
    "log"
    "os"
 
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "helloworld/helloworld"
)
 
const (
    address     = "localhost:50051"
    defaultName = "world"
)
 
func main() {
    // 建立一个grpc连接
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatal("did not connect: %v", err)
    }
    defer conn.Close()
    // 新建一个客户端,方法为:NewXXXClinent(conn),XXX为你在proto定义的服务的名字
    c := pb.NewGreeterClient(conn)
 
    name := defaultName
    if len(os.Args) >1 {
        name = os.Args[1]
    }
    // 调用远程,并得到返回
    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatal("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

restful转grpc

使用grpc的优点很多,二进制的数据可以加快传输速度,基于http2的多路复用可以减少服务之间的连接次数,和函数一样的调用方式也有效的提升了开发效率。不过使用grpc也会面临一个问题,我们的微服务对外一定是要提供Restful接口的,如果内部调用使用grpc,在某些情况下要同时提供一个功能的两套API接口,这样就不仅降低了开发效率,也增加了调试的复杂度。于是就想着有没有一个转换机制,让Restful和gprc可以相互转化。

grpc-gateway应运而生

安装

首先你得要根据本文之前的步骤安装proto和grpc,然后如下安装一些库

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
go get -u github.com/golang/protobuf/protoc-gen-go

用法

定义service的proto文件

 syntax = "proto3";
 package example;

 import "google/api/annotations.proto";

 message StringMessage {
   string value = 1;
 }
 
 service YourService {
   rpc Echo(StringMessage) returns (StringMessage) {
     option (google.api.http) = {
       post: "/v1/example/echo"
       body: "*"
     };
   }
 }

option 表示处理哪些path的请求以及如何处理请求体(参数),见https://cloud.google.com/serv...

生成go文件

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  path/to/your_service.proto

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  path/to/your_service.proto

以上生成的两个文件,第一个是pb.go文件,给grpc server用的;第二个是pb.gw.go文件,给grpc-gateway用的,用于grpc和restful的相互转化

服务器

package main

import (
  "flag"
  "net/http"

  "github.com/golang/glog"
  "golang.org/x/net/context"
  "github.com/grpc-ecosystem/grpc-gateway/runtime"
  "google.golang.org/grpc"
    
  gw "path/to/your_service_package"
)

var (
  echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
)

func run() error {
  ctx := context.Background()
  ctx, cancel := context.WithCancel(ctx)
  defer cancel()

  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithInsecure()}
  err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
  if err != nil {
    return err
  }

  return http.ListenAndServe(":8080", mux)
}

func main() {
  flag.Parse()
  defer glog.Flush()

  if err := run(); err != nil {
    glog.Fatal(err)
  }
}

测试

curl -X POST -k http://localhost:8080/v1/example/echo -d '{"name": " world"}

{"message":"Hello  world"}

流程如下:curl用post向gateway发送请求,gateway作为proxy将请求转化一下通过grpc转发给greeter_server,greeter_server通过grpc返回结果,gateway收到结果后,转化成json返回给前端。

查看原文

认证与成就

  • 获得 60 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-03-17
个人主页被 1.6k 人浏览