作者: ethan

  • Prometheus本地部署

    在本地mac上,进行Prometheus服务的部署,便于开发、学习和测试。

    下载镜像

    docker pull prom/prometheus:v2.27.1

    配置

    mkdir -p ~/Ethan/Docker/prometheus-2.27.1
    cd ~/Ethan/Docker/prometheus-2.27.1

    增加配置文件:

    global:
      # 拉取时间间隔, 默认1m
      scrape_interval: 1m
    
      # 拉取超时时间, 默认10s
      scrape_timeout: 10s
    
      # 评估规则的时间间隔, 默认1m
      evaluation_interval: 1m
    
    # 抓取对象配置
    scrape_configs:
      - job_name: 'node_test'
    
        # 采集服务的地址 targets, 可逗号间隔配置多个
        static_configs:
        - targets: ['10.252.187.163:8080']
    
        # 采集的路径
        metrics_path: '/test/prometheus/metrics'

    启动镜像

    docker run -d \
    --name prometheus \
    -p 9090:9090 \
    -v /Users/ethanxu/Ethan/Docker/prometheus-2.8.0/prometheus.yml:/etc/prometheus/prometheus.yml \
    prom/prometheus:v2.8.0 \
    --config.file=/etc/prometheus/prometheus.yml

    访问

    http://127.0.0.1:9090/graph

    查看 targets ,可以看到 job 处于 UP 状态,说明配置成功了。

  • 最长回文子串 – LeetCode5

    题目

    给你一个字符串xa0s,找到xa0sxa0中最长的回文子串。

    示例1:
    输入:s = "babad" 
    输出:"bab" 
    解释:"aba" 同样是符合题意的答案。
    示例 2:
    输入:s = "cbbd" 
    输出:"bb"
    示例 3:
    输入:s = "a"
    输出:"a"
    示例 4:
    输入:s = "ac"
    输出:"a"

    题解

    golang:

    func longestPalindrome(s string) string {
    	if s == "" {
    		return ""
    	}
    
    	start, end := 0, 0
    	for i := 0; i < len(s); i++ {
    		left1, right1 := expandAroundCenter(s, i, i)
    		left2, right2 := expandAroundCenter(s, i, i + 1)
    		if right1 - left1 > end - start {
    			start, end = left1, right1
    		}
    		if right2 - left2 > end - start {
    			start, end = left2, right2
    		}
    	}
    	return s[start:end + 1]
    }
    
    func expandAroundCenter(s string, left int, right int) (int, int) {
    	for ; left >= 0 && right < len(s) && s[left] == s[right]; left, right = left -1, right + 1 {}
    	return left + 1, right - 1
    }

    相关链接

    https://leetcode-cn.com/problems/longest-palindromic-substring/

  • Map详解 – golang

    基础

    哈希表

    哈希表(散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希(散列)函数,存放记录的数组叫做哈希(散列)表。

    golang中的map是基于哈希表(也被称为散列表)实现的,所以,要进一步理解map的实现,就需要对哈希表有一定的了解。

    学习哈希表首先要理解两个概念:哈希函数和哈希冲突

    哈希函数

    哈希函数(常被称为散列函数)是可以用于将任意大小的数据映射到固定大小值的函数,常见的包括MD5、SHA系列等。

    哈希函数

    一个设计优秀的哈希函数应该包含以下特性:

    • 均匀性:一个好的哈希函数应该在其输出范围内尽可能均匀地映射,也就是说,应以大致相同的概率生成输出范围内的每个哈希值。
    • 效率高:哈希效率要高,即使很长的输入参数也能快速计算出哈希值。
    • 可确定性:哈希过程必须是确定性的,这意味着对于给定的输入值,它必须始终生成相同的哈希值。
    • 雪崩效应:微小的输入值变化也会让输出值发生巨大的变化。
    • 不可逆:从哈希函数的输出值不可反向推导出原始的数据。

    哈希冲突

    哈希函数是将任意大小的数据映射到固定大小值的函数,但即使哈希函数设计得足够优秀,几乎每个输入值都能映射为不同的哈希值,当输入数据足够大,大到能超过固定大小值的组合能表达的最大数量数,冲突将不可避免!

    抽屉原理

    如何解决哈希冲突

    比较常用的Has冲突解决方案有链地址法和开放寻址法。

    在讲链地址法之前,先说明两个概念:

    1. 哈希桶。哈希桶(也称为槽,类似于抽屉原理中的一个抽屉)可以先简单理解为一个哈希值,所有的哈希值组成了哈希空间。
    2. 装载因子。装载因子是表示哈希表中元素的填满程度。它的计算公式:装载因子=填入哈希表中的元素个数/哈希表的长度。装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。反之,装载因子越小,填入的元素越少,冲突发生的几率减小,但空间浪费也会变得更多,而且还会提高扩容操作的次数。装载因子也是决定哈希表是否进行扩容的关键指标。

    链地址法

    将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。例如有一组关键字为{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函数为:H(key)=key MOD 13,使用链地址法所构建的哈希表如下图 所示:

    链地址法

    开放寻址法

    对于链地址法而言,槽位数m与键的数目n是没有直接关系的。但是对于开放寻址法而言,所有的元素都是存储在Hash表当中的,所以无论任何时候都要保证哈希表的槽位数m大于或等于键的数据n(必要时,需要对哈希表进行动态扩容)。

    例如,在长度为 11 的哈希表中已填写好 17、60 和 29 这 3 个数据(如图(a) 所示),其中采用的哈希函数为:H(key)=key MOD 11,现有第 4 个数据 38 ,当通过哈希函数求得的哈希地址为 5,与 60 冲突,则分别采用以上 3 种方式求得插入位置如图 (b)所示:

    开发寻址法

    注释:在线性探测法中,当遇到冲突时,从发生冲突位置起,每次 +1,向右探测,直到有空闲的位置为止;二次探测法中,从发生冲突的位置起,按照 +12,-12,+22,…如此探测,直到有空闲的位置;伪随机探测,每次加上一个随机数,直到探测到空闲位置结束。

    两种解决方案比较

    对于开放寻址法而言,它只有数组一种数据结构就可完成存储,继承了数组的优点,对CPU缓存友好,易于序列化操作。但是它对内存的利用率不如链地址法,且发生冲突时代价更高。当数据量明确、装载因子小,适合采用开放寻址法。

    链表节点可以在需要时再创建,不必像开放寻址法那样事先申请好足够内存,因此链地址法对于内存的利用率会比开方寻址法高。链地址法对装载因子的容忍度会更高,并且适合存储大对象、大数据量的哈希表。而且相较于开放寻址法,它更加灵活,支持更多的优化策略,比如可采用红黑树代替链表。但是链地址法需要额外的空间来存储指针。

    Go Map实现

    map中的数据被存放于一个数组中的,数组的元素是桶(bucket),每个桶至多包含8个键值对数据。哈希值低位(low-order bits)用于选择桶,哈希值高位(high-order bits)用于在一个独立的桶中区别出键。哈希值高低位示意图如下:

    数据结构

    src/runtime/map.go

    // A header for a Go map.
    type hmap struct {
    	count     int // 代表哈希表中的元素个数,调用len(map)时,返回的就是该字段值。
    	flags     uint8 // 状态标志,下文常量中会解释四种状态位含义。
    	B         uint8  // buckets(桶)的对数log_2(哈希表元素数量最大可达到装载因子*2^B)
    	noverflow uint16 // 溢出桶的大概数量
    	hash0     uint32 // 哈希种子
    
    	buckets    unsafe.Pointer // 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,它为nil
    	oldbuckets unsafe.Pointer // 如果发生扩容,oldbuckets是指向老的buckets数组的指针,老的buckets数组大小是新的buckets的1/2。非扩容状态下,它为nil
    	nevacuate  uintptr        // 表示扩容进度,小于此地址的buckets代表已搬迁完成
    
    	extra *mapextra // 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针
    }
    
    type mapextra struct {
    	// 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
            // 就使用 hmap的extra字段 来存储 overflow buckets,这样可以避免 GC 扫描整个 map
            // 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针
            // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
            // overflow 包含的是 hmap.buckets 的 overflow 的 buckets
            // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket
    	overflow    *[]*bmap
    	oldoverflow *[]*bmap
    
    	// 指向空闲的 overflow bucket 的指针
    	nextOverflow *bmap
    }
    
    // A bucket for a Go map.
    type bmap struct {
    	// tophash包含此桶中每个键的哈希值最高字节(高8位)信息(也就是前面所述的high-order bits)
    	// 如果tophash[0] < minTopHash,tophash[0]则代表桶的搬迁(evacuation)状态
    	tophash [bucketCnt]uint8
    }

    bmap也就是bucket(桶)的内存模型图解如下:

    bucket(桶)的内存模型图解

    以上图解可看出,key和value是各自存储起来的,而非key/value/key/value…的形式。这样做的好处是能让我们消除例如map[int64]int所需要的填充。此外,在8个键值对数据后面有一个overflow指针,因为桶中最多只能装8个键值对,如果有多余的键值对落到了当前桶,那么就需要再构建一个桶(称为溢出桶),通过overflow指针链接起来。

    重要常量

    const (
    	// 一个桶中最多能装载的键值对(key-value)的个数为2^3=8
    	bucketCntBits = 3
    	bucketCnt     = 1 << bucketCntBits
    
    	// 触发扩容的装载因子为13/2=6.5
    	loadFactorNum = 13
    	loadFactorDen = 2
    
    	// 键和值超过128个字节,就会被转换为指针
    	maxKeySize  = 128
    	maxElemSize = 128
    
    	// 数据偏移量应该是bmap结构体的大小,它需要正确地对齐。
    	// 对于amd64p32而言,这意味着:即使指针是32位的,也是64位对齐。
    	dataOffset = unsafe.Offsetof(struct {
    		b bmap
    		v int64
    	}{}.v)
    
    	// 每个桶(如果有溢出,则包含它的overflow的链接桶)在搬迁完成状态(evacuated* states)下,要么会包含它所有的键值对,要么一个都不包含(但不包括调用evacuate()方法阶段,该方法调用只会在对map发起write时发生,在该阶段其他goroutine是无法查看该map的)。简单的说,桶里的数据要么一起搬走,要么一个都还未搬。
    	// tophash除了放置正常的高8位hash值,还会存储一些特殊状态值(标志该cell的搬迁状态)。正常的tophash值,最小应该是5,以下列出的就是一些特殊状态值。
    	emptyRest      = 0 // 表示cell为空,并且比它高索引位的cell或者overflows中的cell都是空的。(初始化bucket时,就是该状态)
    	emptyOne       = 1 // 空的cell,cell已经被搬迁到新的bucket
    	evacuatedX     = 2 // 键值对已经搬迁完毕,key在新buckets数组的前半部分
    	evacuatedY     = 3 // 键值对已经搬迁完毕,key在新buckets数组的后半部分
    	evacuatedEmpty = 4 // cell为空,整个bucket已经搬迁完毕
    	minTopHash     = 5 // tophash的最小正常值
    
    	// flags
    	iterator     = 1 // 可能有迭代器在使用buckets
    	oldIterator  = 2 // 可能有迭代器在使用oldbuckets
    	hashWriting  = 4 // 有协程正在向map写人key
    	sameSizeGrow = 8 // 等量扩容
    
    	// 用于迭代器检查的bucket ID
    	noCheck = 1<<(8*sys.PtrSize) - 1
    )

    综上,我们以B等于4为例,展示一个完整的map结构图:

    golang map

    相关链接

    https://mp.weixin.qq.com/s/OHROn0ya_nWR6qkaSFmacw

    https://segmentfault.com/a/1190000038746146

    https://segmentfault.com/a/1190000037727309

  • golang之排序算法

    下面我们来看下golang实现的排序算法。

    快速排序

    func QuickSort(arr []int) []int {
    	length := len(arr)
    	if length <= 1 {
    		return arr
    	}
    	mid := arr[0]
    	low, high := make([]int, 0), make([]int, 0)
    	for i := 1; i < length; i++ {
    		if arr[i] > mid {
    			high = append(high, arr[i])
    		} else {
    			low = append(low, arr[i])
    		}
    	}
    
    	lowSlice := QuickSort(low)
    	highSlice := QuickSort(high)
    
    	return append(append(lowSlice, mid), highSlice...)
    }

    归并排序

    func mergeSort(arr []int) []int {
    	length := len(arr)
    	if length <= 1 {
    		return arr
    	}
    
    	mid := length / 2
    	left := mergeSort(arr[:mid])
    	right := mergeSort(arr[mid:])
    
    	return merge(left, right)
    }
    func merge(left []int, right []int) []int {
    	leftLen := len(left)
    	rightLen := len(right)
    	i, j := 0, 0
    	tmp := make([]int, 0)
    	for ;i < leftLen && j < rightLen; {
    		if left[i] < right[j] {
    			tmp = append(tmp, left[i])
    			i++
    		} else {
    			tmp = append(tmp, right[j])
    			j++
    		}
    	}
    	if i < leftLen {
    		tmp = append(tmp, left[i:]...)
    	} else if j < rightLen {
    		tmp = append(tmp, right[j:]...)
    	}
    	return tmp
    }

  • Elasticsearch更新mapping

    es更新mapping是一个非常规的操作,下面是操作执行脚本:

    from elasticsearch import Elasticsearch
    from elasticsearch import helpers
    
    esIp = "127.0.0.1"
    esPort = "9200"
    
    fromIndex = "old_index"
    targetIndex = "new_index"
    
    new_mapping = {"mappings": {"_doc": {"properties": {"id": {"type": "long"}, "title": {"type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": {"keyword": {"type": "keyword", "ignore_above": 256}}}, "content": {"type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart", "fields": {"keyword": {"type": "keyword", "ignore_above": 256}}}}}}}
    
    bulk_read = 1000
    scroll = "5m"
    
    # 连接elasticsearch
    es = Elasticsearch([esIp], http_auth=("my_name", "my_pwd"), port=esPort)
    
    # 创建新索引
    es.indices.create(index=targetIndex, body=new_mapping)
    helpers.reindex(client=es, source_index=fromIndex, target_index=targetIndex, target_client=es, query={"query": {"match_all": {}}}, chunk_size=bulk_read)
    
    # 删除旧索引
    es.indices.delete(fromIndex)
    
    # 索引设置别名
    es.indices.put_alias(index=targetIndex, name=fromIndex)
    
    
  • elasticsearch 精确匹配搜索(高准确度)

    elasticsearch可以说是目前应用非常广泛的搜索引擎了,然而如果想要能够完全的满足检索的需求,还是有些需要注意的点的。

    ik分词器,是elasticsearch检索中文时最常用的分词器。其有两种分词模式:

    ik_max_word

    会将文本做最细粒度的拆分,比如:

    中华人民共和国人民大会堂 => 中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂

    ik_smart

    会做最粗粒度的拆分,比如:

    中华人民共和国人民大会堂 => 中华人民共和国、人民大会堂

    常规的最常用的使用方式就是,数据插入存储时用 ik_max_word模式分词,而检索时,用ik_smart模式分词,即:索引时最大化的将文章内容分词,搜索时更精确的搜索到想要的结果。

    模糊匹配

    mapping:

    {"_doc":{"properties":{"content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"}}}}

    query:

    {"bool":{"must":[{"match_phrase":{"content":"\u72fc\u5d3d"}}]}}

    该检索能够满足大部分的检索需求,也是最常用的检索方式

    模糊匹配&精确匹配

    有时候,检索不仅需要支持模糊匹配,还需要支持完全匹配

    mapping:

    {"_doc":{"properties":{"content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}}}

    通过给字段增加fileds “keyword”, 类型为keyword, 则该fields就可以通过 content.keyword 访问

    query:

    {"bool":{"must":[{"term":{"content.keyword":"\u6211\u559c\u6b22"}}]}}

    通过以上方式则可以实现精准匹配

    解决因分词问题导致用一些词检索不到数据的情况

    上文我们讲过,索引是用ik_max_word分词,检索时用ik_smart分词,可以说,ik_smart的分词结果是ik_max_word分词结果的一个子集,那么理论上来讲,肯定是能够检索到数据的,那么为什么有时会检索不到呢?

    对此官方网站对match_phrase的解释如下:

    Like the match query, the match_phrase query first analyzes the query string to produce a list of terms. It then searches for all the terms, but keeps only documents that contain all of the search terms, in the same positions relative to each other.

    意思就是说用match_phrase查找时,查找分词器分出的词的位置和要建索引时分出的词的相对位置一样。

    例如:

    原文:我喜欢一家四口的生活

    ik_max_word分词结果

    分词位置
    0
    喜欢1
    一家2
    3
    4
    四口5
    6
    7

    检索词: 一家四口

    ik_smart分词结果

    分词位置
    一家0
    四口1

    从上面可以看出,查找时ik_smart将语句分为了 一家四口 两个词,位置分别为 0 和 1 ,而ik_max_word建索引时,一家四口 的位置分别是 2 和 5,一个位置相邻,一个位置不相邻,在match_phrase看来,这种是匹配的,所以用ik_smart分词短语时无法查到或者查全数据。

    为了解决match_phrase这个问题,我们可以辅以使用standard分词器

    standard分词器大家都比较熟,针对于汉字就是一个一个分,这种肯定是可以查全的。但一个一个字分的话,每个字对应的文档集合非常多,如果数据量达到了百亿,在求交集,计算距离时,效果非常差。而这里我们可以将其跟ik分词器配合使用,既利用ik分词器的优势,又可以利用standard分词器进行兜底。

    但是,此时还有一个问题,就是standard分词只是针对单个字的分词,ik_max_word分出了很多词,如果有“好人”“好人的”这种分词结果,那么搜索“好人”,match_phrase仅能够搜索出“好人”对应的结果,同样符合预期的“好人的”的结果是检索不到的。

    针对这种情况,我们利用match_phrase_prefix代替match_phrase,match_phrase_prefix 与 match_phrase查询类似,但是会对最后一个Token在倒排序索引列表中进行通配符搜索。即match_phrase_prefix将会找到“好人”“好人的”对应的结果。

    mapping:

    {"_doc":{"properties":{"content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart","fields":{"keyword":{"type":"keyword","ignore_above":256},"standard":{"type":"text","analyzer":"standard"}}}}}}

    query:

    {"query":{"bool":{"must":[{"bool":{"should":[{"match_phrase_prefix":{"content":"\u6211\u559c\u6b22"}},{"match_phrase_prefix":{"content.standard":"\u6211\u559c\u6b22"}}]}}]}}}

    总结

    以上就是elasticsearch的检索相关知识,因为es修改mapping相当的麻烦,所以大家在创建索引时,一定要尽可能的将mapping考虑周全,避免之后的修改。

    当然,为了以后万一修改的时候减小成本,大家创建索引后,尽量同时创建别名,然后利用别名访问该索引,这样之后就可以通过修改别名指向,从而无缝替换旧索引,不影响线上服务。

    附录

    如果仅仅是给索引字段增加fields,是不需要重建索引的,可以直接修改mapping:

    curl -H "Content-Type: application/json;" -X POST 'http://127.0.0.1:9200/my_index/_doc/_mapping?pretty' -d '
    {"_doc":{"properties":{"title":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart","fields":{"keyword":{"type":"keyword","ignore_above":256}}},"content":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart","fields":{"keyword":{"type":"keyword","ignore_above":256}}}}}}
    '

    修改完mapping后,新的数据就会按照新的mapping来走了,如果想要历史数据也同步新的mapping的话,则利用_update_by_query更新即可:

    curl -H "Content-Type: application/json;" -X POST 'http://127.0.0.1:9200/my_index/_update_by_query?conflicts=proceed'

    更新时间跟数据量的多少有关,数据越多,则更新所需时间越长。

  • 明确mysql整型字段取值范围

    mysql中存储整数,有很多种类型供选择,选择合适的类型,无论是对存储空间的合理使用,检索性能的提高等都有很好的便利,下面就由我们来一起明确下。

    数据类型字节数有符号无符号 unsigned
    tinyint1 -128~127 (-2^8 ~ 2^8 – 1)0~255 (2^9 – 1)
    smallint2-32,768~32,767 (-2^15 ~ 2^15 – 1)0~65535 (2^16 – 1)
    mediumint3-8,388,608~8,388,607(-2^23 ~ 2^23 – 1)0~16777215(2^24 – 1)
    int / integer4-2,147,483,648~2,147,483,647(-2^31 ~ 2^31 – 1)0~4294967295(2^32 – 1)
    bigint8-9,223,372,036,854,775,808~9,223,372,036,854,775,807(-2^63 ~ 2^63 – 1)0~18446744073709551615(2^64 – 1)
  • golang并发控制

    golang对于并发的支持可以说是最好的,也是我们之所以采用go语言进行编程的一个重要的因素。

    golang的并发控制,主要有以下几种方式:

    Channel

    func Process(ch chan<- int)  {
    	// do something
    	time.Sleep(time.Second)
    
    	ch <- 1
    }
    
    func TestOther(t *testing.T) {
    	// 有几个协程,就需要创建包含多少元素的切片
    	channels := make([]chan int, 10)
    
    	for i := 0; i < 10; i++ {
    		channels[i] = make(chan int)
    		go Process(channels[i])
    	}
    
    	for i, ch := range channels {
    		<-ch
    		fmt.Printf("Routine %d quit \n", i)
    	}
    }

    优点是实现简单,缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制。

    WaitGroup

    func Process(wg *sync.WaitGroup, i int)  {
    	defer wg.Done()
    
    	// do something
    	time.Sleep(time.Second)
    
    	fmt.Printf("Routine %d quit \n", i)
    }
    
    func TestOther(t *testing.T) {
    	wg := sync.WaitGroup{}
    
    	wg.Add(10)
    
    	for i := 0; i < 10; i++ {
    		go Process(&wg, i)
    	}
    	wg.Wait()
    	fmt.Printf("all Goroutine quit \n")
    }

    WaitGroup是Golang应用开发过程中经常使用的并发控制技术。WaitGroup,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。比如某个goroutine需要等待其他几个goroutine全部完成,那么使用WaitGroup可以轻松实现。

    Add()操作必须早于Wait(), 否则会panic
    Add()设置的值必须与实际等待的goroutine个数一致,否则会panic
  • golang 并发 append

    go中,切片可以算是我们最常用的结构之一,但是如果不注意的话,在并发情况下,对同一个切片进行append,极有可能会造成线程不安全的情况。

    非线程安全现象

    如,以下例子:

    func TestOther(t *testing.T) {
    	testMap := []int{1,2,3,4}
    
    	wg := sync.WaitGroup{}
    	wg.Add(4)
    	
    	m := make([]int, 0)
    	
    	for _, v := range testMap {
    		go func(v int) {
    			defer wg.Done()
    
    			m = append(m, v)
    		}(v)
    	}
    	wg.Wait()
    
    	t.Log(m)
    }

    以上代码,不同次运行时,输出以下这类不符合预期的结果:

    这就是因为线程不安全导致的 (实际业务中,可以通过 go run -race main.go 进行检测程序的安全性)

    原因分析

    slice的数据结构:

    type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
    }
    1.2 slice - 图1
    slice结构示意图

    使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

    在并发情况下,如果该slice始终空间不足,那么其是线程安全的,因为每次append实际都是新生成的内存,不存在抢占的情况。但是,当slice空间充足,也即是cap>len, 有剩余的空间时,比如说,下一个空闲内存是a, 那么并发情况下,就会出现多个线程抢占往a中写数据的情况。

    slice扩容遵从以下原则:
    如果原Slice容量小于1024,则新Slice容量将扩大为原来的2倍;
    如果原Slice容量大于等于1024,则新Slice容量将扩大为原来的1.25倍;

    所以,根据以上的原则,在程序运行后,是很容易就会出现有空闲空间的情况,也就会造成线程不安全的产生。

    解决办法

    针对内存占用,我们最直接简单的办法就是给内存加锁,如下:

    func TestOther(t *testing.T) {
    	testMap := []int{1,2,3,4}
    
    	wg := sync.WaitGroup{}
    	wg.Add(4)
    	
    	m := make([]int, 0)
    	
    	var lock sync.Mutex
    	
    	for _, v := range testMap {
    		go func(v int) {
    			defer wg.Done()
    
    			lock.Lock()
    			m = append(m, v)
    			lock.Unlock()
    			
    		}(v)
    	}
    	wg.Wait()
    
    	t.Log(m)
    }

    运行结果:

    通过内存加锁,确保,同一时刻,只有一个线程对该块内存进行append操作,这就从根源上避免了抢占的问题。

  • golang内存分配管理

    基础

    为了方便自主的管理内存,golang的做法是,先向系统申请一块大的内存,然后将其分割成一个个小块,通过自己的内存分配算法来进行管理。

    4.1 内存分配原理 - 图1
    预申请的内存

    预申请的内存划分为spans、bitmap、arena三部分。其中arena即为所谓的堆区,应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。

    arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;

    spans区域存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)*指针大小8byte = 512M;

    bitmap区域大小也是通过arena计算出来,不过主要用于GC。

    mheap

    系统通过mheap来管理预申请的内存.

    mheap内存管理示意图如下:

    4.1 内存分配原理 - 图4

    central

    central是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central。

    cache

    cache作为线程的私有资源为单个线程服务。

    mcache和span的对应关系如下图所示:

    4.1 内存分配原理 - 图3

    span

    span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小,span将一个或多个页拆分成多个块进行管理。

    4.1 内存分配原理 - 图2
    golang内存分配 span

    内存分配图解

    golang内存分配
    golang内存分配