map 的一个神奇的问题

2021-04-06 11:54:56 +08:00
 zkdfbb

使用下面的一段代码来统计链接的访问情况,使用方法就是用 Incr 每次访问加 1 单独测试的时候,比如用 wrk 来压测都挺正常的 神奇的是,一换成 nginx,c.data 里面的 key 就变得不正常,把他打印出来,发现有很多相同的 key 比如本来就只有一个 key 的,他会出现 key: 1, key: 2, key: 1, key: 1 这种,搞不懂。。。

type Counter struct {
	sync.RWMutex
	data map[string]int
}

func (c *Counter) Get(key string) int {
	c.RLock()
	count := c.data[key]
	c.RUnlock()
	return count
}

func (c *Counter) Incr(key string) int {
	c.Lock()
	c.data[key]++
	count := c.data[key]
	c.Unlock()
	return count
}

func (c *Counter) Delete(key string) {
	c.Lock()
	delete(c.data, key)
	c.Unlock()
}
6880 次点击
所在节点    Go 编程语言
76 条回复
dallaslu
2021-04-07 01:13:01 +08:00
@makdon #58 正解,我这里测试通过。Immutable 的注释: …so that these values are available even if you return from handler.
no1xsyzy
2021-04-07 01:26:12 +08:00
如果把 fiber 除掉,go 2 个协程或 20 个协程持续 accessLog.Incr (分别以 id1 和 id2 ),也是完全正常的没有重复键。
更精细地,range accessLog.data 来打印,随手写了对比相邻行 key,当显示相等时 last_key == key 得 true
json.Marshal 的结果所有的值都是同一个值,显然相互覆盖了。

我暂时搁置了,放一些思路:
试下 gin ?

这最后可能要涉及到 map 的实现问题。op 先把目前的结果放 StackOverflow 吧。
makdon
2021-04-07 01:41:24 +08:00
原因应该是这样的,没有很深入看 map 的源码(src/runtime/map.go),所以带有猜测成分
首先我们往 map 插入一个 key 为 id1 的
然后完成请求后,刚刚 key 所占用的 []byte 被重复利用,key 变成了 id2
猜测 map 的实现里面没有拷贝一次 string,所以 map 里面的 key 变成了 id2,但是 hash 还是之前 id1 的 hash
然后分两种情况:
- 新插入 id1,!t.key.equal(key, k), 所以给它分配了一个新的桶
- 新插入 id2,原有的 id2 跟新的 id2 hash 不相等,不会覆盖,还是给它新分配一个新的桶

可以通过一个小 demo,复现这个 case,获得一个 200 个 key,每个 key 都是 id1 的 map
func main(){
m := make(map[string]int)
for i := 0; i < 200; i++{
b := []byte("id2")
str := *(*string)(unsafe.Pointer(&b))
m[str]++
b[2] = '1'
}
fmt.Println(m)
fmt.Println(len(m))
}
zkdfbb
2021-04-07 09:07:10 +08:00
@dallaslu
@no1xsyzy
@makdon

感谢分析~ 用 Immutable: true 就正常了,可结贴了
GTim
2021-04-07 10:32:44 +08:00
说来你们可能不相信,这是 `fiber` 导致的问题,哎,以后有机会深究

```go
package main

import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"sync"
)

type Counter struct {
sync.RWMutex
data map[string]int
}

func (c *Counter) Incr(key string) int {
c.Lock()
c.data[key]++
count := c.data[key]
fmt.Println(c.data)
c.Unlock()
return count
}

var (
accessLog = &Counter{data: make(map[string]int)}
)

func handler(c *gin.Context) {
id := c.Param("id")
accessLog.Incr(id)
c.String( http.StatusOK, "")
}

func main() {
router := gin.Default()
router.GET("/item/:id", handler)
router.Run(":8099")
}

```

用原生的 `net/http` 或者 `gin` 就不会出错
lesismal
2021-04-07 11:27:08 +08:00
@GTim 不用以后深入研究,就今天吧

不能怪 fiber,fiber 是基于 fasthttp 的,以前看过 fasthttp 相关介绍大概意思是 fasthttp 为了性能,很多地方 pool 复用内存,虽然我没有读 fasthttp 源码,只是大概分析,但是大致原因应该是差不多的:

应用层获取 http 各种参数时是复用的[]byte unsafe 的方式强转成 string,类似 c/c++的浅拷贝,新的 string 和原来的[]byte 类型结构体的数据指针指向同一段内存,而在本次 handler 调用结束后,这段[]byte 就被放回了 pool 并且以后有新的地方使用时又被拿出来
比如楼主的 key 加入到 map 时字面值是 "a",按照 "a" 的 hash index 存到对应的 map 的 bucket 里,而这个 string "a" 的结构体内部指向的内存被放回 pool,其他地方再次从 pool get 到时就可能被复用的地方修改,比如刚好其他请求的这个 key 复用了 "a" 的同一段内存但是这些请求的 key 为"b","b" 加到 map 里的时候是按照 "b" 的 hash index 存到对应的 bucket 里的、不同的 hash index 则不碰撞、不会跟原来的那个 string "a"(当前字面值也是"b")比较,所以就产生了多个 key

实在不喜欢 fasthttp
但是 fiber 的接口 /API 设计看着比 gin 舒服,还是挺喜欢的
但是生产项目,我还是不打算用 fasthttp 系的
lesismal
2021-04-07 11:34:51 +08:00
@Lpl 恩恩,这种简单功能都不会性能瓶颈,主要还要考虑设计上的复杂度,chan 不是万能灵药,毕竟加了一层 chan,同步逻辑变成了异步逻辑,换成 chan 的实现也不比 mutex 来得简洁,并且也不如用 mutex 容易理解

锁是很基础的设施,不要怕用它
lesismal
2021-04-07 11:37:02 +08:00
@makdon
原来 63 楼已经回复过了,66 楼多余了,缘分
lesismal
2021-04-07 11:40:20 +08:00
@makdon 63 楼回答的好
@Lpl 现在是互夸了

enjoy coding, have fun ~
zkdfbb
2021-04-07 11:56:45 +08:00
@lesismal 我也是看 fiber 接口设计比较舒服,基本的功能也都比较完备,然后好像还说作者全职在做这个所以用的,之前就看到过一篇文章说高并发下有问题不过也没太在意。。。生产环境下有啥好推荐的么

https://cloud.tencent.com/developer/news/462918
lesismal
2021-04-07 12:05:20 +08:00
@zkdfbb 基于标准库的知名框架都比较稳,功能和周边也都差不多,按 star 就 gin,按喜好就看自己了
qieqie
2021-04-07 12:08:55 +08:00
9 楼一条没说对是真的
另外说一个反直觉但是能提升 map[string]int ++这样操作的 trick,就是换成 map[string]*int
参考: https://github.com/golang/go/issues/45021
zkdfbb
2021-04-07 12:23:49 +08:00
@qieqie 这样确实会更快一点,不过应该基本无影响。就像很多框架的 benchmark 差很多,但是架不住加上自己的业务逻辑后都差不多了。。。
fenglangjuxu
2021-04-07 16:35:07 +08:00
@Lpl #52 理论小白 诚心求教 无意冒犯

```
case r := <-c.resultChan:
{
// ???这里不用加锁么???
c.data[*r]++
}
```
makdon
2021-04-07 17:24:49 +08:00
@fenglangjuxu 他用 chan 排队了,一个协程专门处理 data,其它协程访问不到
fenglangjuxu
2021-04-07 19:06:23 +08:00
@makdon #75 奥 多谢解答

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/768320

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX