Z


  • 首页

  • 标签

  • 归档

golang实现ECC加密

发表于 2018-05-13

简介

椭圆曲线密码学(英语:Elliptic curve cryptography,缩写为 ECC),一种建立公开密钥加密的算法,基于椭圆曲线数学。椭圆曲线在密码学中的使用是在1985年由Neal Koblitz和Victor Miller分别独立提出的。

ECC的主要优势是在某些情况下它比其他的方法使用更小的密钥——比如RSA加密算法——提供相当的或更高等级的安全。

椭圆曲线密码学的许多形式有稍微的不同,所有的都依赖于被广泛承认的解决椭圆曲线离散对数问题的困难性上.

数学原理

不管是RSA还是ECC或者其它,公钥加密算法都是依赖于某个正向计算很简单(多项式时间复杂度),而逆向计算很难(指数级时间复杂度)的数学问题。

椭圆曲线依赖的数学难题是:

k为正整数,P是椭圆曲线上的点(称为基点), k*P=Q , 已知q和P,很难计算出k

密钥生成

golang 标准包中直接提供了密钥生成的方法

// 初始化椭圆曲线
pubkeyCurve := elliptic.P256()
// 随机挑选基点,生成私钥
p, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
if err != nil {
    fmt.Println("1", err)
    os.Exit(1)
}
PRIVATE = p

加密和解密

golang标准包中没有提供加密和解密算法,但是以太坊go-ethereum实现了相关算法,这里对其进行二次封装

// 将标准包生成私钥转化为ecies私钥
prv2 = ecies.ImportECDSA(PRIVATE)
// ecc(ecies)加密
func ECCEncrypt(pt []byte) ([]byte, error) {
    ct, err := ecies.Encrypt(rand.Reader, &prv2.PublicKey, pt, nil, nil)
    return ct, err
}
// ecc(ecies)解密
func ECCDecrypt(ct []byte) ([]byte, error) {
    pt, err := prv2.Decrypt(ct, nil, nil)
    return pt, err
}

签名和验签

golang标准包中提供了相关方法

func EccSign(pt []byte) (sign []byte, err error) {
    // 根据明文plaintext和私钥,生成两个big.Ing
    r, s, err := ecdsa.Sign(rand.Reader, PRIVATE, pt)
    if err != nil {
        fmt.Println(err)
        return nil, err
    }
    rs, err := r.MarshalText()
    if err != nil {
        return nil, err
    }
    ss, err := s.MarshalText()
    if err != nil {
        return nil, err
    }
    // 将r,s合并(以“+”分割),作为签名返回
    var b bytes.Buffer
    b.Write(rs)
    b.Write([]byte(`+`))
    b.Write(ss)
    return b.Bytes(), nil
}

func EccSignVer(pt, sign []byte) bool {
    var rint, sint big.Int
    // 根据sign,解析出r,s
    rs := bytes.Split(sign, []byte("+"))
    rint.UnmarshalText(rs[0])
    sint.UnmarshalText(rs[1])
    // 根据公钥,明文,r,s验证签名
    v := ecdsa.Verify(&PRIVATE.PublicKey, pt, &rint, &sint)
    return v
}

Q&A

ECC,ECIES,ECDSA,ECDH 是什么关系,有哪些区别?

  • ECC,全称椭圆曲线密码学(英语:Elliptic curve cryptography,缩写为 ECC),主要是指相关数学原理
  • ECIES,在ECC原理的基础上实现的一种公钥加密方法,和RSA类似
  • ECDSA,在ECC原理上实现的签名方法
  • ECDH在ECC和DH的基础上实现的密钥交换算法

RSA算法的密钥对可以通过.pem文件来保存,ECC的密钥对要如何保存呢?

go-ethereum提供了两种对外接口.

密钥对和[]byte之间的转换

// 私钥 -> []byte
// FromECDSA exports a private key into a binary dump.
func FromECDSA(priv *ecdsa.PrivateKey) []byte {
    if priv == nil {
        return nil
    }
    return math.PaddedBigBytes(priv.D, priv.Params().BitSize/8)
}

// []byte -> 私钥
// ToECDSA creates a private key with the given D value.
func ToECDSA(d []byte) (*ecdsa.PrivateKey, error) {
    return toECDSA(d, true)
}

// 公钥 -> []byte
func FromECDSAPub(pub *ecdsa.PublicKey) []byte {
    if pub == nil || pub.X == nil || pub.Y == nil {
        return nil
    }
    return elliptic.Marshal(S256(), pub.X, pub.Y)
}

// []byte -> 公钥
    func ToECDSAPub(pub []byte) *ecdsa.PublicKey {
    if len(pub) == 0 {
        return nil
    }
    x, y := elliptic.Unmarshal(S256(), pub)
    return &ecdsa.PublicKey{Curve: S256(), X: x, Y: y}
}

公钥转换的时候需要注意,eth中默认使用的椭圆曲线是S256().生成密钥对时,如果使用的是其他椭圆曲线(如P256),需要对FromECDSAPub,ToECDSAPub稍作修改

密钥对和文件之间的转换,其实就是在中间加一层hex编码

// 私钥 -> 文件
// SaveECDSA saves a secp256k1 private key to the given file with
// restrictive permissions. The key data is saved hex-encoded.
func SaveECDSA(file string, key *ecdsa.PrivateKey) error {
    k := hex.EncodeToString(FromECDSA(key))
    return ioutil.WriteFile(file, []byte(k), 0600)
}

// 文件 -> 私钥
func LoadECDSA(file string) (*ecdsa.PrivateKey, error) {
    buf := make([]byte, 64)
    fd, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer fd.Close()
    if _, err := io.ReadFull(fd, buf); err != nil {
        return nil, err
    }

    key, err := hex.DecodeString(string(buf))
    if err != nil {
        return nil, err
    }
    return ToECDSA(key)
}

相关链接

椭圆曲线数学原理 (上篇)

椭圆曲线数学原理 (下篇)

ECIES,ECDSA,ECDH三者的区别

以太坊ecies实现

golang实现RSA加密

发表于 2018-05-05

简介

1976年之前,所有加密方式都是同一种模式:甲乙双方使用同一种加密算法和密钥,即对称加密。这种模式有一个弱点:甲方必须先把加密规则和密钥告诉乙方。这样导致保存和传递密钥,成为了一个头疼的问题。

1976年,两位美国计算机学家Whitfield Diffie 和 Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密。这被称为”Diffie-Hellman密钥交换算法”。这个算法启发了其他科学家。人们认识到,加密和解密可以使用不同的密钥,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥。

这种新的加密模式被称为”非对称加密算法”。

RSA是非对称加密中的一种。

数学原理

大整数的因数分解,是一件非常困难的事情。例如你可以对3233进行因数分解(61×53),但对较大整数进行因数分解就比较困难了。

这里提供一个golang实现的暴力因数分解,有兴趣可以跑一下试试:

// 因数分解
// 因为大整数s可能会超过uint64的最大值,所以这里用字符串来作为输入参数
func factorization(s string) (x, y *big.Int, err error) {
    df := big.NewInt(1)
    x = big.NewInt(2)
    n, flag := big.NewInt(1).SetString(s, 0)
    if !flag {
        return nil, nil, errors.New("invalid string")
    }
    for ; x.Cmp(n) == -1; x.Add(x, big.NewInt(1)) {
        fmt.Println("x:", x.String())
        if df.Mod(n, x).String() == "0" {
            return x, df.Div(n, x), nil
        }
    }
    return nil, nil, errors.New("end faild")
}

密钥生成

A想与B进行加密通信,首先要生成一个密钥:

  1. A选择两个大质数,得到乘积n,再随机选择一个整数e,计算e对于φ(n)的模反元素d。

  2. 将(n,e)作为公钥,(n,d)作为私钥

可以证明,如果想通过公钥获取私钥,难度取决于大整数因数分解的效率。

关于模反元素和相关推导,可以看链接,比想象中简单

加密和解密

公钥加密

B想给A发送信息m

  1. 将m转化为整数(字符串可以取ascii值或unicode值),且m必须小于n。

  2. 根据公式 m^e ≡ c (mod n) 已知m(明文),e(公钥的一部分),n(公钥的一部分),可以得到密文c

私钥解密

  1. 得到密文c

  2. 根据公式 c^d ≡ m (mod n) 已知 c(密文),d(私钥的一部分),n(私钥的一部分),可以得到明文m

证明根据根据以上公式获取的明文和原始明文一致

golang代码实现RSA密钥生成

//生成公钥和私钥 pem文件

package encryp

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "os"
)

//生成 私钥和公钥文件
func GenRsaKey(bits int) error {
    //生成私钥文件
    privateKey, err := rsa.GenerateKey(rand.Reader, bits)
    if err != nil {
        return err
    }
    derStream := x509.MarshalPKCS1PrivateKey(privateKey)
    block := &pem.Block{
        Type:  "RSA PRIVATE KEY",
        Bytes: derStream,
    }
    file, err := os.Create("private.pem")
    if err != nil {
        return err
    }
    err = pem.Encode(file, block)
    if err != nil {
        return err
    }
    //生成公钥文件
    publicKey := &privateKey.PublicKey
    defPkix, err := x509.MarshalPKIXPublicKey(publicKey)
    if err != nil {
        return err
    }
    block = &pem.Block{
        Type:  "RSA PUBLIC KEY",
        Bytes: defPkix,
    }
    file, err = os.Create("public.pem")
    if err != nil {
        return err
    }
    err = pem.Encode(file, block)
    if err != nil {
        return err
    }
    return nil
}

golang实现RSA加密和解密

// 读取公钥私钥
var privateKey, publicKey []byte
func init() {
    var err error
    publicKey, err = ioutil.ReadFile("public.pem")
    if err != nil {
        os.Exit(-1)
    }
    privateKey, err = ioutil.ReadFile("private.pem")
    if err != nil {
        os.Exit(-1)
    }
}

// 公钥加密
func RsaEncrypt(data []byte) ([]byte, error) {
    //解密pem格式的公钥
    block, _ := pem.Decode(publicKey)
    if block == nil {
        return nil, errors.New("public key error")
    }
    // 解析公钥
    pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    // 类型断言
    pub := pubInterface.(*rsa.PublicKey)
    //加密
    return rsa.EncryptPKCS1v15(rand.Reader, pub, data)
}

// 私钥解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
    //获取私钥
    block, _ := pem.Decode(privateKey)
    if block == nil {
        return nil, errors.New("private key error!")
    }
    //解析PKCS1格式的私钥
    priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    // 解密
    return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}

相关链接

RSA算法原理-欧拉函数 RSA算法原理

golang实现AES加密

发表于 2018-05-02

AES算法

AES加密算法(Advanced Encryption Standard):是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES。

原理

密钥扩展

  1. 根据初始密钥长度(16,24,32)进行扩展,执行AES-128, AES-192, AES-256算法

明文加密/解密

主要包括四个子程序:

  1. 字节替代(SubBytes): 矩阵中的各字节通过一个8位的S-box(Inverse S-box)进行转换

  2. 行移位(ShiftRows) 第n行向左(右)移动n-1位,n从1开始

  3. 列混淆(MixColumns) 每一列都在modulox^4+1之下,和一个固定多项式c(x)作乘法。

  4. 轮密钥加(AddRoundKey) 将每个状态中的字节与该回合密钥做异或运算。

代码实现

func AesEncrypt(origData, key []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    blockSize := block.BlockSize()
    origData = PKCS5Padding(origData, blockSize)
    blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
    crypted := make([]byte, len(origData))
    blockMode.CryptBlocks(crypted, origData)
    return crypted, nil
}

func AesDecrypt(crypted, key []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    blockSize := block.BlockSize()
    blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
    origData := make([]byte, len(crypted))
    blockMode.CryptBlocks(origData, crypted)
    origData = PKCS5UnPadding(origData)
    return origData, nil
}

Q&A

  1. 字节替代中,Forward S-box,Inverse S-box 构造过程?是否与密钥或者明文相关? S盒的构造方式如下: (1) 按字节值得升序逐行初始化S盒。在行y列x的字节值是{yx}。 (2) 把S盒中的每个字节映射为它在有限域GF中的逆;{00}映射为它自身{00}。 (3) 把S盒中的每个字节的8个构成位记为(b7, b6, b5, b4, b3, b2, b1)。对S盒的每个字节的每个位做如下的变换:

  2. 列混淆中,加密与解密的多项式如何构造

  3. 轮密钥加,每个回合的密钥如何生成的

相关链接

AES加密算法和RSA加密算法 wiki

golang实现DES加密

发表于 2018-04-27

DES简介

DES是一个分组加密算法,典型的DES以64位为分组对数据加密,加密和解密用的是同一个算法。它的密钥长度是56位(因为每个第8 位都用作奇偶校验),密钥可以是任意的56位的数,而且可以任意时候改变。其中有极少数被认为是易破解的弱密钥,但是很容易避开它们不用。所以保密性依赖于密钥。

概念隐喻

  • 加密 - 做蛋糕
  • 明文 - 原材料
  • IV初始向量 - 厨师. IV长度必须与block.Size保持一致. 维基百科上说重复使用IV会导致安全隐患
  • block - 一套工具
  • 工作模式 - 工具使用说明书,有EBC,CBC等
  • Padding填充模式 - 检查原材料.块密码只能对确定长度的数据块进行处理,而消息的长度通常是可变的.因此部分模式(即ECB和CBC)需要最后一块在加密前进行填充

加密过程:

  • block = 密钥切割
  • pdata = padding + data
  • 加密算法 = 工作模式+初始向量+block 代码:cipher.NewCBCEncrypter(block,iv)
  • 密文 = 加密算法 + pdata

解密过程(解密算法其实是对密文的加密)

  • block = 密钥切割
  • 解密算法 = 工作模式+初始向量+block 代码:cipher.NewCBCDecrypter(block,iv)
  • 明文padding = 解密算法 + 密文
  • 明文 = unpadding + 明文padding

EBC(Electronic codebook)和CBC(Cipher-block Chain)的比较:

  • EBC 同样的明文块会被加密成相同的密文块.
  • CBC 每个明文块先与前一个密文块进行异或后,再进行加密.
  • 安全强度上CBC更强
  • CBC无法并行加密

代码实现

// DES加密
func DesEncrypt(origData, key []byte) ([]byte, error) {
    // 根据密钥生成block
    block, err := des.NewCipher(key)
    if err != nil {
        return nil, err
    }
    // 根据block和初始向量生成加密算法,IV长度与block.size需要保持一致
    blockMode := cipher.NewCBCEncrypter(block, IV)
    // 扩充 origData
    origData = PKCS5Padding(origData, block.BlockSize())
    crypted := make([]byte, len(origData))
    // 对padded data加密
    blockMode.CryptBlocks(crypted, origData)
    return crypted, nil
}

// DES解密
func DesDecrypt(crypted, key []byte) ([]byte, error) {
    // 根据密钥生成block
    block, err := des.NewCipher(key)
    if err != nil {
        return nil, err
    }
    // 根据block和初始向量生成解密算法,IV长度与block.size需要保持一致
    blockMode := cipher.NewCBCDecrypter(block, IV)
    // 对密文解密
    origData := make([]byte, len(crypted))
    blockMode.CryptBlocks(origData, crypted)
    // 反扩充,获取原始明文
    origData = PKCS5UnPadding(origData)
    return origData, nil
}

相关链接:

DES加密算法原理

工作模式 ECB,CBC

关于加密算法的理解

发表于 2018-04-26

对称加密

  • DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。
  • 3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。
  • AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别高;

总结(相同数据,相同密钥):

  1. DES速度是3DES的三倍
  2. AES分为根据密钥长度分为AES-128,AES-192,AES-256,三者性能差别不大
  3. AES性能优于DES

非对称加密

  • RSA:由 RSA 公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的;
  • DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准);
  • ECC(Elliptic Curves Cryptography):椭圆曲线密码编码学。

总结:

  1. RSA随着密钥对的长度增长,加解密速度会明显降低
  2. 目前还没实现
  3. 目前还没实现,但是根据ECC签名以及网上的资料表示,ECC会逐渐成为主流

    对称加密,非对称加密的前提条件都是要先生成密钥或者密钥对

哈系算法

  • MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法。
  • SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值;

总结:

  1. SHA比MD5复杂强度更高,但是运行速度较慢.
  2. 哈系算法一般会结合非对称加密,实现签名-验签

签名-验签

  • RSA+hash(sha,mad5)
  • ECC

加密/解密 和 签名/验签的区别

  1. 一般都使用非对称加密
  2. 前者使用公钥加密,私钥解密
  3. 后者使用私钥签名,公钥验签
  4. 签名/验签的过程不一定需要反向解密

算法原理

ECC

椭圆曲线加密教程 (上篇)

椭圆曲线加密教程 (下篇)

常见加密算法分类,用途,原理以及比较

目前只是研究了ECC算法原理,但是理解上还差一点,后期补上

代码大全-阅读笔记

发表于 2018-04-08

背景

做过很多需求.每次需求过来,稍微分析一下就开始建表写代码.两年下来,总感觉对开发流程,代码规范的认知缺少深度,这里想通过阅读代码大全这本经典,对自己做一次总结.

什么是软件构建

定义问题

需求分析

规划构建

高层设计

详细设计

编码与调试

单元测试

集成测试

集成

系统测试

保障维护


隐喻

写作,代价是丢掉一张草稿

培育,软件开发相当于系统生长,骨架-肌肉-皮肤,增量式开发.

建造, 定义问题-设计(线路)-审查(质检)

前期准备

有些人断言,前期准备毫无用处.

前期准备不周全的原因:

  1. 不具备相关规划技能.学习
  2. 尽快编码的欲望.总结之前的项目痛点

问题定义

问题定义只定义了问题是什么,而不涉及任何可能的解决方案

需求变更

为什么会有变更?

随着客户参与项目的时间增长,他们对项目的理解也就越深.

平均情况,25%的需求会有变化.特殊情况,75%-85%的需求变更

架构的组成部分

程序组织 Programma Organization

主要的类 Major Classes

80/20法则:对哪些构成系统80%的行为的20%的类进行详细说明

数据设计

数据通常只应该由一个子系统或者一个类直接访问

业务规则

用户界面设计

资源管理

安全性

性能

可伸缩性

互用性

国际化/本地化

输入输出

错误处理

%90的代码用来处理异常情况,10%的代码处理常规情况

一般被认为是代码约定层次的事情

以下是关于错误处理需要考虑的事情:

  • 进行纠正还是检测?
  • 检测是主动还是被动?
  • 如果传播错误?
  • 错误消息的处理有什么约定?
  • 如何处理异常?记录或者描述
  • 每个类在验证输入数据的有效性方面,需要负何种责任?每个类都验证,还是有一个类负责验证整个系统的数据有效性?
  • 使用运行环境的错误处理机制,还是自己建立一套?

容错性

容错策略:

  • 检测到错误退回去,退回到之前一切正常的时刻,从该点继续运行
  • 切换到另外一套辅助程序
  • 表决算法
  • 使用默认值代替错误值
  • 遇到错误时,进入”部分运转”的状态

架构的可行性

过度工程

买还是造

关于复用的决策

变更策略

表驱动技术

架构的总体质量

你,作为一个实现该系统的程序员,是否对这个架构感觉良好?

构建中的设计

设计是一个险恶(wicked)的问题,wicked指的是你必须把这个问题”解决”一遍,才能明确地定义它,然后再次解决该问题,从而形成一个可行的方案.

关键的设计概念

设计的难题主要包括偶然的难题和本质的难题

管理复杂度

复杂度原因

  • 用复杂的方法解决简单的问题
  • 用简单但错误的方法解决复杂的问题
  • 用正确但复杂的问题解决复杂的问题

应对复杂度

  • 最小复杂度 避免做出”聪明的设计”,应该做出简单且易于理解的设计
  • 易于维护 为其他程序员着想
  • 松散耦合 设计出关联尽可能少的类
  • 可扩展性
  • 可重用性
  • 高扇入(被利用) 让大量的类使用某个子类
  • 低扇出 少量或适中地使用其他的类(7个以下)
  • 精简性 一本书的完成,不在他不能再加入任何内容的时候,而在不能再删去任何内容的时候
  • 层次性
  • 标准技术

设计的层次

  • 子系统或者包 子系统之间的调用,应该是无环图
  • 分解为类
  • 分解为子程序
  • 子程序内部的设计

找出容易改变的区域

对变化的预期能力

  • 找出看起来容易变化的项目
  • 把容易变化的项目分离出来
  • 把看起来容易变化的项目隔离开来

容易变化的区域

  • 业务规则
  • 对硬件的依赖性
  • 输入和输出
  • 非标准的语言特性
  • 困难的设计区域和构建区域
  • 状态变量
  • 常量

耦合标准

尽可能缩减相互连接

耦合的种类

简单数据参数耦合

简单对象耦合

对象参数耦合

语义上的耦合,使用潜在的耦合假设,危险

类和子程序适用于降低复杂度的首选和最重要的智力工具,如果他们没帮助你简化工作,那么他们就是失职的

常见的设计模式

设计模式是一种非常强大的管理复杂度的工具

但同时也存在一个潜在的陷阱:”为了模式而模式”,有时候对代码进行一些微小的改动,以符合某个广为人知的模式,会使这段代码更容易理解.

但是如果一段代码做出巨大改动,迫使它去符合某个标准模式,有时反而会把问题复杂化

数学领域的解题思路

  • 理解问题,未知量,现有数据,条件分别是什么.现有数据是否足够,或者不够
  • 再次之前你遇到过这个问题吗,或者类似的问题.
  • 盯住未知量,试着想出一个有着相同或者类似未知量的问题来
  • 现在能重述这个问题吗,用之前不同的方式重述
  • 如果还是解决不了,试着先去解决一些相关的问题,一个更一般,或者更特殊,或者问题的一部分
  • 设计一个计划,找出现有数据和未知量的联系
  • 执行这一计划,能否检查每一步,并证明他是正确的
  • 回顾整个解

前期要做多少设计才够

抽象数据类型

Knowledge-Graph

发表于 2018-04-04

数据结构与算法

链表

  • 增
  • 删
  • 改
  • 查
  • 翻转
  • 合并

栈

优先队列,最小/大栈

树

  • 二叉树
  • 红黑树

扩展结构

  • trie
  • LRU(Least Recently Used)

排序算法

动态规划

golang

  • map实现
  • slice实现
  • channel实现
  • 内存分配
  • 调度
  • gc原理
  • 性能优化
  • 编译过程
  • 版本变迁内容
  • channel常用模式
  • net高性能的原因

操作系统

  • I/O模型

linux

网络通信

  • http协议
  • 浏览网页的过程分析

常用组件

mysql,redis,mongo,nsq,etcd

  • 应用场景
  • 源码实现
  • 表结构设计(mysql)
  • 性能特色
  • 高可用方案
  • 监控
  • 容灾
  • 一致性实现

设计

登录

微服务

总结

以前想问题时,总是把事情假设的太理想,明明知道可能会出问题,却有侥幸心理,不去深入研究.

笔试方面,需要多训练,提高思维速度.

golang-sync.Map实现原理

发表于 2018-03-31

结构体

sync.Map

type Map struct {
    // 常用的锁,在操作dirty时会用到
    mu Mutex

    read atomic.Value // readOnly

    // dirty值要么为空,要么为全部键值对
    dirty map[interface{}]*entry

    // 在查询read时,命中失败的次数,当misses大于dirty长度时,read.m 会直接指向dirty
    misses int
}

Map.read实际值是readOnly结构体,可以看成是比dirty多了一个amended字段的结构

注意这个并不是真正的只读,添加操作是通过直接将m字段指向整个dirty完成的,删除操作是通过修改entry为expunged完成的

readOnly

type readOnly struct {
    m       map[interface{}]*entry
    amended bool 
}

主要看下readOnly.amended的值变化情况

  • 在Store方法中,添加新的键值对时,如果amended==false,会遍历read值,将其中未被标记为删除的记录,复制到dirty中,然后read.amended会被修改为true

  • 在Load方法中,如果miss次数大于diry长度时,会将read.m直接指向dirty,且read.amended被置为false

  • 如果amended==true,说明当前map时间节点处于新添加键值对之后,复制dirty到read之前,此时dirty不为空,且dirty包括所有Map中未被删除的数据,read中的数据可能少于dirty

  • 如果amended==false,说明说明dirty为空

entry

type entry struct {
    p unsafe.Pointer // *interface{}
}

实际是键值对中的value

关键操作

寻值

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            m.missLocked() // 如果read中没有,查询dirty
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

复制dirty到read,并清空dirty

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

存值

func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }

    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        e.storeLocked(&value)
    } else {
        if !read.amended {
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

场景分析

map.Store(a,”a”) -> map.Store(b,”b”)

第一次map.Store(“a”,”a”)

  1. 因为备份标记为false,执行m.dirtyLocked()
  2. 复制read -> dirty
  3. 将备份标记read.amended更新为true
  4. 将a存在dirty中

第二次map.Store(“b”,”b”)

  1. 跳过if !read.amended 判断
  2. 向dirty中存储数据

map.Store(“a”,”a”) ->map.Load(“a”) map.Store(“b”,”b”)

map.Store(“a”,”a”)

  1. 因为备份标记为false,执行m.dirtyLocked()
  2. 复制read -> dirty
  3. 将read.amended更新为true
  4. 将a存在dirty中

map.Load(“a”)

  1. 从read中读取失败
  2. 从dirty中读取
  3. m.missLocked()
  4. miss+1
  5. 因为miss >= len(dirty),直接将m.dirty复制到m.read中
  6. 备份标记m.read.amended = false
  7. 清空dirty

map.Store(“b”,”b”)

  1. 因为备份标记为false,执行m.dirtyLocked()
  2. 复制read -> dirty
  3. 将read.amended更新为true
  4. 将a存在dirty中

总结

sync.Map是如何保证性能比直接在map中加锁的性能好

当写入操作较多时,性能是无法保证的,因为每次都有可能要遍历read复制到dirty中

当读多写少时,read是atomic.Value类型, 读取时利用了atomic.Value.Load实现了原子操作,没有用到锁,所以性能有所提升

怎么理解read.amended

可以把amended理解为一个备份标记,从read中遍历数据,复制到dirty中,相当于完成备份,amended为true,dirty为nil时,说明未备份,amended为false

Map.read 和 Map.dirty的关系

可以把read看成是缓存,当缓存命中失败次数过多时,会从dirty中复制数据到read中,如果dirty不为空,那么dirty的数据大于等于read

从dirty中复制数据到read中,是否会导致原来的read中的数据丢失

不会,每次dirty创建时,都是从read中读取未被标记删除的数据复制到dirty中,之后dirty中的数据只会多于read,所以在从dirty中复制数据到read中时,只是会丢失已被标记删除的数据,而不会丢失实际数据


相关链接

golang sync.Map 原理

Go 1.9 sync.Map揭秘

golang-pprof实践

发表于 2018-03-30

准备工作

在 golang官方博客的一篇文章中看到了详细使用go tool pprof优化程序的过程. 于是我把代码clone下来,在本地实践了一次.

➜  ~ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/skt/mygo"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build112976360=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

havlak背景

项目havlak,在C++版本的运行时间是17.8s,花费了700M的内存, Go版本花费了25.20s,使用了1302M内存,接下来会使用go tool pprof工具,对Go程序进行优化

开始

havlak1

切换到项目目录

➜  ~ cd mygo/src/benchgraffiti/havlak 
➜  havlak 

这里我直接用go build ,而不是博客中的make

因为我执行make havlak1.prof的时候报错,说我没有6g工具

原来是MakeFile中使用6g工具编译的,也许是生成博客的时间(2011.6.24)太久远了吧

➜  havlak go build havlak1.go

程序中给了-cpuprofile的接口,可以用来生成prof文件

➜  havlak ./havlak1 -cpuprofile=havlak1.prof
# of loops: 76000 (including 1 artificial root node)

执行go tool pprof 二进制文件名 prof文件名,开始分析

➜  havlak go tool pprof havlak1 havlak1.prof
File: havlak1
Type: cpu
Time: Mar 30, 2018 at 5:49pm (CST)
Duration: 21.48s, Total samples = 26.63s (123.97%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top10
Showing nodes accounting for 17790ms, 66.80% of 26630ms total
Dropped 121 nodes (cum <= 133.15ms)
Showing top 10 nodes out of 66
  flat  flat%   sum%        cum   cum%
3500ms 13.14% 13.14%     6760ms 25.38%  runtime.scanobject /usr/local/go/src/runtime/mgcmark.go
3120ms 11.72% 24.86%     3330ms 12.50%  runtime.mapaccess1_fast64 /usr/local/go/src/runtime/hashmap_fast.go
2800ms 10.51% 35.37%    15770ms 59.22%  main.FindLoops /home/skt/mygo/src/benchgraffiti/havlak/havlak1.go
1720ms  6.46% 41.83%     1720ms  6.46%  runtime.heapBitsForObject /usr/local/go/src/runtime/mbitmap.go
1500ms  5.63% 47.47%     3600ms 13.52%  main.DFS /home/skt/mygo/src/benchgraffiti/havlak/havlak1.go
1440ms  5.41% 52.87%     4670ms 17.54%  runtime.mapassign_fast64 /usr/local/go/src/runtime/hashmap_fast.go
1170ms  4.39% 57.27%     5120ms 19.23%  runtime.mallocgc /usr/local/go/src/runtime/malloc.go
1010ms  3.79% 61.06%     1020ms  3.83%  runtime.greyobject /usr/local/go/src/runtime/mgcmark.go
 780ms  2.93% 63.99%     1270ms  4.77%  runtime.evacuate /usr/local/go/src/runtime/hashmap.go
 750ms  2.82% 66.80%      750ms  2.82%  runtime.memclrNoHeapPointers /usr/local/go/src/runtime/memclr_amd64.s

这里对各个列名解释一下:

  • flat 表示采样时,命中到该函数的时长,去除该函数中对其它函数的调用所花费的时间,也有些文档把这一列理解为函数被调用次数
  • cum 表示采样时,命中到该函数的全部时长,包括函数中的其他调用所花费的时间
  • cum >= flat,当cum远大于flat时,说明函数中有某个调用特别费时(比如上面的FindLoops,2800ms:15770ms)

上面展示的内容和官博中的文章比较,有些出入,初步估计是因为go版本不一样,编译方式不一样导致的.先忽略这个问题.

分析上面的top10结果可以看到top1是runtime包的scanobject,mapaccess1_fast64,前者是gc相关的标记函数,后者是map寻值的操作.但是这个貌似不能直接看到我的程序哪里出问题了.怎么办呢?按照博客文章的顺序,我们先来分析后者.

我想知道我的程序哪里调用了runtime.mapaccess1_fast64

(pprof) web runtime.mapaccess1_fast64

可以看到主要有两个地方调用了mapaccess1_fast64,DFS和FindLoops

接着我想知道具体DFS或者FindLoops的哪一行导致问题的

(pprof) list DFS
Total: 26.63s
ROUTINE ======================== main.DFS in /home/skt/mygo/src/benchgraffiti/havlak/havlak1.go
 1.50s      7.16s (flat, cum) 26.89% of Total
         .          .    240:func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {
  10ms       10ms    241:    nodes[current].Init(currentNode, current)
  10ms      160ms    242:    number[currentNode] = current
     .          .    243:
     .          .    244:    lastid := current
 1.10s      1.10s    245:    for _, target := range currentNode.OutEdges {
 150ms      1.63s    246:        if number[target] == unvisited {
  40ms      3.60s    247:            lastid = DFS(target, nodes, number, last, lastid+1)
     .          .    248:        }
     .          .    249:    }
 120ms      590ms    250:    last[number[currentNode]] = lastid
  30ms       30ms    251:    return lastid
     .          .    252:}
     .          .    253:
     .          .    254:// FindLoops
     .          .    255://
     .          .    256:// Find loops and build loop forest using Havlak's algorithm, which

第一列是flat,第二列是cum,第三列是行数

可以看到最占时间的是247行,他本是是个递归,先不分析 接下来是246行,它是从map中取值,怎么会花费这么多时间呢,对了,前面就是在分析runtime.mapaccess1_fast64的调用情况,那应该就是他了. 在242,246,250行都使用了取值操作

这里引用官博中的一句话

For that particular lookup, a map is not the most efficient choice. Just as they would be in a compiler, the basic block structures have unique sequence numbers assigned to them.Instead of using a map[*BasicBlock]int we can use a []int, a slice indexed by the block number. There’s no reason to use a map when an array or slice will do.

意思是对于某些特殊查询,map不是最有效的选择.就像它们在编译器中一样,基本块结构具有分配给它们的唯一序列号.使用切片[]int是一个更好的选择. 在array或者slice可以解决的情况下,没理由去使用map.(但是我觉得map方便啊…捂脸)

我的理解是因为数组是在内存中连续存储的,所以查询能在O(1)时间内完成,而map在数据量较大是,在对hash值进行查询的时候,性能可能会收到影响.

havlak2

在对havlak1修改以后,形式好了很多.具体修改哪些,可以看这里

看一下havlak2的pprof

➜  havlak go build havlak2.go
➜  havlak ./havlak2 -cpuprofile=havlak2.prof
# of loops: 76000 (including 1 artificial root node)
➜  havlak go tool pprof havlak2 havlak2.prof
File: havlak2
Type: cpu
Time: Mar 30, 2018 at 7:10pm (CST)
Duration: 13.11s, Total samples = 17.55s (133.83%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top5  
Showing nodes accounting for 8410ms, 47.92% of 17550ms total
Dropped 93 nodes (cum <= 87.75ms)
Showing top 5 nodes out of 72
    flat  flat%   sum%        cum   cum%
    2730ms 15.56% 15.56%     5630ms 32.08%  runtime.scanobject /usr/local/go/src/runtime/mgcmark.go
    1990ms 11.34% 26.89%    10290ms 58.63%  main.FindLoops /home/skt/mygo/src/benchgraffiti/havlak/havlak2.go
    1600ms  9.12% 36.01%     1600ms  9.12%  runtime.heapBitsForObject /usr/local/go/src/runtime/mbitmap.go
    1300ms  7.41% 43.42%     4370ms 24.90%  runtime.mallocgc /usr/local/go/src/runtime/malloc.go
    790ms  4.50% 47.92%      800ms  4.56%  runtime.greyobject /usr/local/go/src/runtime/mgcmark.go

哇哦! 之前的DFS和runtime.mapaccess1_fast64都不见了.现在被调用次数最多的是runtime.scanobject(15.56%)和main.FindLoops(11.34%).

我试了一下list main.FindLoops,显示了FindLoops之外的代码.应该是因为没有函数内联导致的.

先来分析runtime.scanobject,这个是和gc相关的扫描函数,有调度器调用,那么是什么导致调度器调用这个函数呢,应该是某些地方对象创建频繁,导致频繁gc.

涉及到内存分配,单从cpu-pprof是不好找原因的,那么从mem-pprof入手.

havlak3

havlak3提供了memprofile参数,可以生成mem-pprof. (这里可以看havlak3.go和havlak2.go的区别)

➜  havlak go build -gcflags '-N -l' havlak3.go //禁止内联优化
➜  havlak ./havlak3 -memprofile=havlak3.mprof
➜  havlak go tool pprof havlak3 havlak3.mprof
File: havlak3
Type: inuse_space
Time: Mar 30, 2018 at 7:48pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top5
Showing nodes accounting for 60.89MB, 100% of 60.89MB total
Showing top 5 nodes out of 13
    flat  flat%   sum%        cum   cum%
33.10MB 54.36% 54.36%    33.10MB 54.36%  main.FindLoops /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
    20MB 32.85% 87.21%       20MB 32.85%  main.NewBasicBlock /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
    3MB  4.93% 92.13%        3MB  4.93%  main.(*BasicBlock).AddInEdge /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
    2.50MB  4.11% 96.24%     2.50MB  4.11%  main.(*BasicBlock).AddOutEdge /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
    2.29MB  3.76%   100%    22.29MB 36.61%  main.(*CFG).CreateNode /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
(pprof) 

main.FindLoops名列前茅,终究还是回到他这里了.

(pprof) list FindLoops
Total: 60.89MB
ROUTINE ======================== main.FindLoops in /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
33.10MB    33.10MB (flat, cum) 54.36% of Total
        .          .    263:        return
        .          .    264:    }
        .          .    265:
        .          .    266:    size := cfgraph.NumNodes()
        .          .    267:
    1.97MB     1.97MB   268:    nonBackPreds := make([]map[int]bool, size)
    5.77MB     5.77MB   269:    backPreds := make([][]int, size)
        .          .    270:
    1.97MB     1.97MB   271:    number := make([]int, size)
    1.97MB     1.97MB   272:    header := make([]int, size, size)
    1.97MB     1.97MB   273:    types := make([]int, size, size)
    1.97MB     1.97MB   274:    last := make([]int, size, size)
    1.97MB     1.97MB   275:    nodes := make([]*UnionFindNode, size, size)
        .          .    276:
        .          .    277:    for i := 0; i < size; i++ {
    5MB        5MB      278:        nodes[i] = new(UnionFindNode)
        .          .    279:    }
        .          .    280:
        .          .    281:    // Step a:
        .          .    282:    //   - initialize all nodes as unvisited.
        .          .    283:    //   - depth-first traversal and numbering.
        .          .    284:    //   - unreached BB's are marked as dead.
        .          .    285:    //
        .          .    286:    for i, bb := range cfgraph.Blocks {
        .          .    287:        number[bb.Name] = unvisited
    10.50MB    10.50MB  288:        nonBackPreds[i] = make(map[int]bool)
        .          .    289:    }
        .          .    290:
        .          .    291:    DFS(cfgraph.Start, nodes, number, last, 0)
        .          .    292:
        .          .    293:    // Step b:

第288行,又是因为可以用简单的数据结构切片或者数组时,却创建了map,且花费了10.5M的内存

另一方面,使用go tool pprof –inuse_objects havlak3 havlak3.mprof 可以显示创建内存的次数,而不是.这一点更符合寻找gc压力的目的.

➜  havlak go tool pprof --inuse_objects havlak3 havlak3.mprof
File: havlak3
Type: inuse_objects
Time: Mar 30, 2018 at 7:48pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) list FindLoops
Total: 1245227
ROUTINE ======================== main.FindLoops in /home/skt/mygo/src/benchgraffiti/havlak/havlak3.go
393238     393238 (flat, cum) 31.58% of Total
     .          .    263:        return
     .          .    264:    }
     .          .    265:
     .          .    266:    size := cfgraph.NumNodes()
     .          .    267:
     1          1    268:    nonBackPreds := make([]map[int]bool, size)
     1          1    269:    backPreds := make([][]int, size)
     .          .    270:
     1          1    271:    number := make([]int, size)
     1          1    272:    header := make([]int, size, size)
     1          1    273:    types := make([]int, size, size)
     1          1    274:    last := make([]int, size, size)
     1          1    275:    nodes := make([]*UnionFindNode, size, size)
     .          .    276:
     .          .    277:    for i := 0; i < size; i++ {
163845     163845    278:        nodes[i] = new(UnionFindNode)
     .          .    279:    }
     .          .    280:
     .          .    281:    // Step a:
     .          .    282:    //   - initialize all nodes as unvisited.
     .          .    283:    //   - depth-first traversal and numbering.
     .          .    284:    //   - unreached BB's are marked as dead.
     .          .    285:    //
     .          .    286:    for i, bb := range cfgraph.Blocks {
     .          .    287:        number[bb.Name] = unvisited
229386     229386    288:        nonBackPreds[i] = make(map[int]bool)
     .          .    289:    }
     .          .    290:
     .          .    291:    DFS(cfgraph.Start, nodes, number, last, 0)
     .          .    292:
     .          .    293:    // Step b:

官博中写到

That’s reasonable when a map is being used to hold key-value pairs, but not when a map is being used as a stand-in for a simple set, as it is here.

在处理key-value数据时,用map是合适的,但是在处理简单的set集合类型时,map不一定是好的选择

havlak4

在havlak4.go中使用了数组来替代map,这里可以看到和havlak3.go的区别

最开始我是想一个map和array差别真的就这么大么,有点不相信哎.

恰好在这个项目中执行xtime,可以输出二进制文件的耗时和内存占用情况

➜  havlak ./xtime ./havlak3
# of loops: 76000 (including 1 artificial root node)
19.33u 0.16s 13.70r 303024kB ./havlak3
➜  havlak ./xtime ./havlak4
# of loops: 76000 (including 1 artificial root node)
11.44u 0.06s 8.41r 166100kB ./havlak4

xtime 输出三个时间和一个内存占用

  • usr_time 用户态时间
  • sys_time 系统态时间
  • real_time 实际花费时间
  • memory 运行内存

可以看到havlak4提高了将近40%的性能…捂脸

现在,比较havlak4和havlak2. 跳过havlak3版本,因为该版本只是加了一个memprof参数,

➜  havlak go build -gcflags '-N -l' havlak4.go
➜  havlak ./havlak4 -cpuprofile=havlak4.prof
# of loops: 76000 (including 1 artificial root node)
➜  havlak go tool pprof havlak4 havlak4.prof
File: havlak4
Type: cpu
Time: Mar 30, 2018 at 8:40pm (CST)
Duration: 8.16s, Total samples = 10.62s (130.14%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top5
Showing nodes accounting for 5990ms, 56.40% of 10620ms total
Dropped 71 nodes (cum <= 53.10ms)
Showing top 5 nodes out of 69
  flat  flat%   sum%        cum   cum%
1920ms 18.08% 18.08%     3740ms 35.22%  runtime.scanobject /usr/local/go/src/runtime/mgcmark.go
1720ms 16.20% 34.27%     5710ms 53.77%  main.FindLoops /home/skt/mygo/src/benchgraffiti/havlak/havlak4.go
1010ms  9.51% 43.79%     1010ms  9.51%  runtime.heapBitsForObject /usr/local/go/src/runtime/mbitmap.go
 690ms  6.50% 50.28%      810ms  7.63%  main.DFS /home/skt/mygo/src/benchgraffiti/havlak/havlak4.go
 650ms  6.12% 56.40%     2550ms 24.01%  runtime.mallocgc /usr/local/go/src/runtime/malloc.go

看cum列:

                                havlak2    havlak4

- runtime.scanobject:           5630ms  -> 3740ms
- main.FindLoops                10290ms -> 5710ms
- runtime.heapBitsForObject     1600ms  -> 1010ms
- runtime.mallocgc              4370ms  -> 2550ms

数值上,优化了不少.但是现在问题还是在main.FindLoops, runtime.scanobject, runtime.mallocgc上.FindLoops先不管,我想知道是什么导致了scanobject和mallocgc的调用.

先看scanobject

(pprof) web runtime.scanobject

可以看到主要是FindLoops里面用了makeslice,从而导致mallocgc,然后导致scanobject.

再看mallocgc

(pprof) web runtime.mallocgc

mallocgc的调用关系有点复杂,但是通过web页面的方块颜色还是可以明显看出FindLoops调用了newobjec和growslice,从而导致mallocgc的调用.

那么问题就集中到FindLoops身上了

(pprof) list FindLoops
Total: 10.62s
ROUTINE ======================== main.FindLoops in /home/skt/mygo/src/benchgraffiti/havlak/havlak4.go
 1.72s      5.71s (flat, cum) 53.77% of Total
     .          .    272:        return
     .          .    273:    }
     .          .    274:
     .          .    275:    size := cfgraph.NumNodes()
     .          .    276:
     .       30ms    277:    nonBackPreds := make([][]int, size)
     .      280ms    278:    backPreds := make([][]int, size)
     .          .    279:
     .      170ms    280:    number := make([]int, size)
     .       20ms    281:    header := make([]int, size, size)
     .       40ms    282:    types := make([]int, size, size)
     .       10ms    283:    last := make([]int, size, size)
     .       20ms    284:    nodes := make([]*UnionFindNode, size, size)
     .          .    285:
     .          .    286:    for i := 0; i < size; i++ {
  30ms      460ms    287:        nodes[i] = new(UnionFindNode)
     .          .    288:    }

下面是官博中的引用

Every time FindLoops is called, it allocates some sizable bookkeeping structures. Since the benchmark calls FindLoops 50 times, these add up to a significant amount of garbage, so a significant amount of work for the garbage collector.

Having a garbage-collected language doesn’t mean you can ignore memory allocation issues. In this case, a simple solution is to introduce a cache so that each call to FindLoops reuses the previous call’s storage when possible. (In fact, in Hundt’s paper, he explains that the Java program needed just this change to get anything like reasonable performance, but he did not make the same change in the other garbage-collected implementations.)

每次调用FindLoops都会创建大量的对象(278,280),连续调用FindLoops后,就有可能 触发GC.

虽然golang是一门自带垃圾回收的语言,但是这不代表我们可以随意的创建对象.如果可以的话,应该考虑对象重用,或者创建一个cache缓存.

havlak5

可以创建一个本地的cache对象

var cache struct {
    size int
    nonBackPreds [][]int
    backPreds [][]int
    number []int
    header []int
    types []int
    last []int
    nodes []*UnionFindNode
}

然后每次FindLoops去查询缓存,而不是直接创建对象

if cache.size < size {
    cache.size = size
    cache.nonBackPreds = make([][]int, size)
    cache.backPreds = make([][]int, size)
    cache.number = make([]int, size)
    cache.header = make([]int, size)
    cache.types = make([]int, size)
    cache.last = make([]int, size)
    cache.nodes = make([]*UnionFindNode, size)
    for i := range cache.nodes {
        cache.nodes[i] = new(UnionFindNode)
    }
}

nonBackPreds := cache.nonBackPreds[:size]
for i := range nonBackPreds {
    nonBackPreds[i] = nonBackPreds[i][:0]
}
backPreds := cache.backPreds[:size]
for i := range nonBackPreds {
    backPreds[i] = backPreds[i][:0]
}
number := cache.number[:size]
header := cache.header[:size]
types := cache.types[:size]
last := cache.last[:size]
nodes := cache.nodes[:size]

这里有个疑问,重用cache的时候,难道不需要对cache值初始化吗,否则上次调用的产生的结果会对当前值产生影响吧.

这里可以看到havlak5和havlak4的区别

现在用xtime比较一下havlak5和havlak4的性能

➜  havlak ./xtime ./havlak4                   
# of loops: 76000 (including 1 artificial root node)
11.09u 0.08s 8.11r 165820kB ./havlak4
➜  havlak ./xtime ./havlak5
# of loops: 76000 (including 1 artificial root node)
7.27u 0.06s 5.48r 171320kB ./havlak5

运行时间从8.11s到5.48s,提升了%25的性能,但是不知道为什么,内存占用反而更多了.

havlak6

官博中提到,havlak6主要是对迭代做了一些优化,因为具体代码我没仔细研究,所以这里就不介绍了

以及使用了更地道的Go风格,数据结构+方法的形式,这个对运行时影响不大

比较一下和上个版本的性能

➜  havlak ./xtime ./havlak5
# of loops: 76000 (including 1 artificial root node)
7.27u 0.06s 5.48r 171320kB ./havlak5
➜  havlak go build -gcflags="-N -l" havlak6.go
➜  havlak ./xtime ./havlak6                   
# of loops: 76000 (including 1 artificial root node)
2.90u 0.03s 2.71r 87524kB ./havlak6

运行时间从5.48s优化到2.71s

接下来,文章中用c++代码重写havlak6的代码,对比了一下不同语言之间的性能,go运行时间稍微大于c++


总结

优化思路

topN中,如果排在前面的是runtime函数,可以用pprof web runtime.xxx被调用的原因,一直向上层寻找自己的代码

如果排在前面的是自己的代码,说明自己的这块代码被调用次数较多,如果函数对应的cum值也比较高,很有可能自己的这块代码有问题,可以用pprof list xxxx,查看具体是哪一行导致的问题

优化的时候,是看主要flat列还是cum列?

可以先选择优化flat列,因为flat可以看成是函数被调用次数,所以对flat值比较高函数进行优化时,优化收益可以看成是被调用次数*本次优化值

cum值较高,可能的原因是什么?

cum值表示整个函数(包括内部调用其它函数)的运行时间,如果cum值较高,有两种原因:

  • 函数内部的其他函数比较耗时
  • 函数中golang的内置函数比较费时,例如make,append,new,map寻值

什么时候用cpupprof,什么时候用mempprof

可以先分析cpupprof,如果其中发现gc相关的代码占用时间较多的话,可以再去分析mempprof

如果在topN中显示标准包的一些函数占用时间较多,怎么去优化我的程序呢.

先用pprof web runtime.xxx命令,找到是哪些程序调用了这个标准包函数,然后依次向上层寻找跟我的程序有关的函数名

已经定位到了某个函数有问提,怎么确定是哪一行?

可以用pprof list xxx, 注意编译的时候禁止函数内联,否则list会展示其他函数名

平时写代码,需要注意哪些坑呢

  • 能用切片,数组解决的,就不要用map,一方面是可能查询会慢,另一方面map分配内存也更多
  • 如果可以的话,尽量重用对象
  • map,slice在创建的时候,尽量声明大小,较低扩容函数的调用

最后

阅读这篇博客+实践+记录,大概花费了我10个小时的时间.

虽然我的英语水平不是很好,但是这篇英文博客看下来竟然感觉非常的爽!推荐大家花几个小时照着博客也去实践一下

相关链接

鸟窝博客-深入Go语言

golang官博

nsqd+mysql保存消息历史

发表于 2018-02-26

改造原因

nsqd自带消息持久化特性(nsqd的消费者因为某些原因断掉,在重新连接后仍然可以继续消费)。团队使用nsqd作为消息队列时,遇到一些问题:虽然nsqd自带消息持久化特性(nsqd的消费者因为某些原因断掉,在重新连接后仍然可以继续消费),也可以通过nsqadmin管理页面查看nsq各个节点的运行情况,但无法了解具体某个消息的生命周期。如果能将nsqd的推送记录和消费记录保存在mysql中,则可以解决痛点。

尝试解决方案

方案1:对go-nsq进行简单封装

  • 在gopath下,新建znsq目录,作为项目包

  • 对生产者的封装

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package znsq

    import (
    "log"
    "os"
    "znsq/models"

    "github.com/astaxie/beego/orm"
    "github.com/nsqio/go-nsq"
    )

    type Producer struct {
    P *nsq.Producer
    }

    // 初始化生产者
    func NewProducer(addr string, mysql string) (wr *Producer) {
    var err error
    cfg := nsq.NewConfig()
    p, err := nsq.NewProducer(addr, cfg)
    if err != nil {
    panic(err)
    }
    p.SetLogger(log.New(os.Stdout, "nsq:", 0), nsq.LogLevelInfo)
    wr = &Producer{P: p}
    orm.RegisterDataBase("default", "mysql", mysql, 10, 10) // 注册数据库
    return wr
    }

    // 对go-nsq的Publish方法封装
    func (p *Producer) Publish(topic string, body []byte) error {
    go p.PublishLog(topic, body) // 添加日志
    return p.P.Publish(topic, body)
    }

    func (p *Producer) PublishLog(topic string, body []byte) (int64, error) {
    log := &models.NsqPublishLog{}
    // log.MessageId = "" // 因为messageId由nsqd生成,所以这里还无法获取messageId
    log.Message = string(body)
    log.NsqdUrl = p.P.String()
    log.Topic = topic
    return models.AddNsqPublishLog(log)
    }
  • 对消费者的封装

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    package znsq

    import (
    "fmt"
    "time"
    "znsq/models"

    "github.com/astaxie/beego/orm"
    _ "github.com/go-sql-driver/mysql"
    nsq "github.com/nsqio/go-nsq"
    )

    func NewNsqConsumer(topic, channel, address, mysql string, handle nsq.Handler) *nsq.Consumer {
    cfg := nsq.NewConfig()
    cfg.LookupdPollInterval = time.Second
    c, err := nsq.NewConsumer(topic, channel, cfg) // 新建一个消费者
    if err != nil {
    panic(err)
    }
    c.SetLogger(nil, 0)

    rg := &handlerRegist{
    h: handle,
    topic: topic,
    channel: channel,
    nsqd: address,
    }
    c.AddHandler(rg) // 添加消费者接口
    orm.RegisterDataBase("default", "mysql", mysql, 10, 10) // 注册数据库
    if err := c.ConnectToNSQD(address); err != nil {
    panic(err)
    }
    return c
    }

    type handlerRegist struct {
    h nsq.Handler
    topic string
    channel string
    nsqd string
    }

    // 对调用方的handel封装
    func (rg *handlerRegist) HandleMessage(message *nsq.Message) error {
    go rg.ConsumeLog(message) // 不知道会不会造成gc压力
    return rg.h.HandleMessage(message)
    }

    // 添加消费日志到mysql
    func (rg *handlerRegist) ConsumeLog(message *nsq.Message) (int64, error) {
    log := &models.NsqConsumeLog{}
    log.NsqdUrl = rg.nsqd
    log.Topic = rg.topic
    log.Channel = rg.channel
    log.Message = string(message.Body)
    log.MessageId = fmt.Sprintf("%s", message.ID)
    return models.AddNsqConsumeLog(log)
    }

该方法的优点:在不对官方项目做任何修改的前提下,就可以记录消息生命周期。 该方法的缺点:因为nsqd的publish消息成功的时候,不会返回message_id,而且go-nsq项目的publish方法的返回值只有一个error类型,所以在记录publish日志时,无法获得message_id,这样就无法查看某一个消息的publish和consume的日志。

方案2:修改go-nsq代码以及nsqd代码

  • 根据文章中的图片找到publish和consum相关部分的代码

  • 修改go-nsq的publish方法,额外添加一个返回值

    1
    // 没有亲自实现,代码略
  • 修改nsqd项目的protocal.Pub方法,额外返回一个message_id

    1
    2
    3
    4
    5
    // github.com/nsqio/nsq/protocal_v2.go
    func (p *protocolV2) PUB(client *clientV2, params [][]byte) ([]byte, error) {
    // ...
    return []byte(fmt.Sprintf("%s %s", okBytes, msg.ID)), nil
    }

该方法的优点:解决了方案1的问题。 该方法的缺点:需要修改2个官方项目,改动过大。

方案3:只修改nsqd代码,为nsqd运行选项添加 -mysql参数

  • 添加publish_log

    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
    35
    // github.com/LvBay/nsq/nsqd/topic.go
    func (t *Topic) put(m *Message) error {
    select {
    case t.memoryMsgChan <- m:
    default:
    b := bufferPoolGet()
    err := writeMessageToBackend(b, m, t.backend)
    bufferPoolPut(b)
    t.ctx.nsqd.SetHealth(err)
    if err != nil {
    t.ctx.nsqd.logf(LOG_ERROR,
    "TOPIC(%s) ERROR: failed to write message to backend - %s",
    t.name, err)
    return err
    }
    }
    if t.ctx.nsqd.getOpts().MysqlUrl != "" {
    go t.PublishLog(m)
    }
    return nil
    }

    // github.com/LvBay/nsq/nsqd/topic.go
    func (t *Topic) PublishLog(m *Message) error {
    log := &NsqPublishLog{}
    log.Topic = t.name
    log.Message = string(m.Body)
    log.NsqdUrl = t.ctx.nsqd.getOpts().TCPAddress
    log.MessageId = fmt.Sprintf("%s", m.ID)
    _, err := AddNsqPublishLog(log)
    if err != nil {
    beego.Error(err)
    }
    return err
    }
  • 添加consume_log

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // github.com/LvBay/nsq/nsqd/protocol_v2.go
    func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
    // ...

    for {
    // ...

    select {
    // ...
    case msg := <-memoryMsgChan:
    if sampleRate > 0 && rand.Int31n(100) > sampleRate {
    continue
    }
    msg.Attempts++

    subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout)
    client.SendingMessage()
    err = p.SendMessage(client, msg, &buf)
    if err != nil {
    goto exit
    }
    // 添加consume日志
    if client.ctx.nsqd.getOpts().MysqlUrl != "" {
    go client.Channel.ConsumeLog(msg)
    }
    flushed = false
    case <-client.ExitChan:
    goto exit
    }
    }

    exit:
    p.ctx.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting messagePump", client)
    heartbeatTicker.Stop()
    outputBufferTicker.Stop()
    if err != nil {
    p.ctx.nsqd.logf(LOG_ERROR, "PROTOCOL(V2): [%s] messagePump error - %s", client, err)
    }
    }

    // github.com/LvBay/nsq/nsqd/channel.go
    func (c *Channel) ConsumeLog(m *Message) error {
    log := &NsqConsumeLog{}
    log.Topic = c.topicName
    log.Channel = c.name
    log.Message = string(m.Body)
    log.NsqdUrl = c.ctx.nsqd.getOpts().TCPAddress
    log.MessageId = fmt.Sprintf("%s", m.ID)
    _, err := AddNsqConsumeLog(log)
    if err != nil {
    beego.Error(err)
    }
    return err
    }
  • 添加 -mysql参数

    1
    2
    3
    4
    5
    6
    7
    // github.com/LvBay/nsq/app/nsqd/nsqd.go
    func nsqdFlagSet(opts *nsqd.Options) *flag.FlagSet {
    // ...
    // mysql
    flagSet.String("mysql", opts.MysqlUrl, "save messages in mysql")
    return flagSet
    }

该方法的优点:解决了方案1中message_id的问题,同时也只修改了nsqd组件。 该方法的缺点:因为需要与mysql交互,在nsqd项目中加入了beego的orm模块代码,对nsqd项目入侵较严重。

总结

我们团队最终采用了第三种方案,接下来准备配合前端同学修改nsqdadmin组件,将消息历史展示在管理页面

12
Yh Zhang

Yh Zhang

See you again

20 日志
4 标签
© 2020 Yh Zhang
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4
本站访客数 人次 本站总访问量 次