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 条回复
ClarkAbe
2021-04-06 19:37:04 +08:00
建议直接 synv.Map
Lpl
2021-04-06 20:00:56 +08:00
@lesismal
@makdon
你俩是在互相对着夸夸吗?我只是给题主举几个其它方面的思路

1. atomic 这个问题,我只是看到 Incr 自然想到的,你俩也说了,要事先知道有哪些 key,把对应的对象创建出来。
那对于 web 应用来说,http_path 都是固定的吧?这是可以事先创建出来的。如果题主这里是随机的,可以不用考虑这一项。

2. 对于使用管道。你们先看明白要解决的问题的根因:因为多个协程去为某个 key +1 会造成多线程不安全的问题。那我只用一个管道也可以做啊,我把拿到的所有请求全部丢给管道,管道是有序的,消费端有序处理,还需要加锁吗?

当然,假如消费端消费的慢,可以采用多个协程求模来做。这个看实际情况分析。

你说我照本宣科这点,恕我不敢苟同

3. 你肯定没用过 1.8 以前没有 sync.map 的场景,或者 Java 里边的 ConcurrentHashMap 不了解。以前没有原生 sync.map 是怎么做 Concurrent 的?就是建一个 32 位的桶,把锁加在桶上边,减小锁的粒度。可以简单参考下这个: https://github.com/orcaman/concurrent-map

@makdon
“读写 channel 的时间” 与 “每一次加锁”,你可以写个简单的 Demo 做一下 benchmark
lesismal
2021-04-06 20:01:51 +08:00
@kcojkcnw 对。32 位也一样,pointer 是字长,并且这种非结构体成员变量是对齐的,除了老奔腾还是哪个版本年代之前的,i32 也是原子的
lesismal
2021-04-06 20:09:13 +08:00
@Lpl 淡定点,淡定点
1. 每个路由预先存到这个简单的统计里,代码会很漂亮?而且这点计数功能用 mutex 性能也不是瓶颈
2. 照本宣科这个词不是为了贬低,而是想告诉你不要听别人说好就什么都用什么,要懂得从实际出发
3. 先看懂我回复的内容
另外,chan 和 mutex 你可以自己先 benchmark 试试,chan 的源码在 runtime/chan.go 里,本身就带有锁的逻辑,并且跨越了协程,如果你觉得会比 mutex 性能好,那可以试试看
makdon
2021-04-06 20:39:05 +08:00
@Lpl 是我单方面夸他...
1. 从代码来看,id 是 url path 里面取的,不固定
2. 谁主张谁举证,大佬 benchmark 来一个?我理解只是从锁排队变成了协程排队了而已
zkdfbb
2021-04-06 20:42:37 +08:00
@makdon
@lesismal
@GTim
@Orlion
@sxfscool
@Lpl
@ClarkAbe
@kcojkcnw

各位大佬,我又测试了一下,用 counter = &Counter{data: make(map[string]int)} 初始化全局变量,然后后面重新赋值的时候也加了锁,但是仍然不行,打印 accessLog.data 的时候用 "%s, %x" 打印 key,结果 hash 是一样的

https://p26-tt.byteimg.com/origin/pgc-image/4963875f190e4ccf9b8a89fcbad8590e
https://wkphoto.cdn.bcebos.com/d1160924ab18972b923ebfb4f6cd7b899e510a43.jpg
lesismal
2021-04-06 21:04:36 +08:00
@zkdfbb 最好可以给一份可以复现的代码+测试用例,大家可以复现下看看
zkdfbb
2021-04-06 21:05:00 +08:00
我用下面的方式测试了一下,比之前的结果要好一点,但是还是不对,但是 tmp 里面的访问次数加起来只有 nginx 的 access.log 里面的一半

accessLog.Lock()
tmp := make(map[string]int)
for k, v := range accessLog.data {
tmp[k] += v
}
accessLog.data = make(map[string]int)
accessLog.Unlock()
touchwithe
2021-04-06 21:06:19 +08:00
golang 萌新看楼上的大佬都好厉害。很久没有感受到这种讨论问题的氛围了。
no1xsyzy
2021-04-06 21:06:42 +08:00
不妨打印下 string(data),如果是 \x00 的问题,json 里会被显式地表达为 '\\' 'x' '0' '0' 四个字符
no1xsyzy
2021-04-06 21:26:23 +08:00
@Lpl 1. 一来移植性有问题,二来其实你并不能确定 http_path 固定,可能只是题主的测试中固定了。

2. chan 底层是锁实现(真锁,不是 CAS ),有时 chan 比 Mutex 效率低一个数量级,甚至不如你自己实现一个 chan
据说标准库的 benchmark 你都找不到几个 chan
多消费者来处理确实比单消费者快得多(因为是流水线,一个去读 chan 了另一个在写 map ),但总体而言,最后仍然不如 Mutex,何况瓶颈在 map,而且还增加 gc 开销
这篇文章里有一个老的 benchmark: <Mhttps://bravenewgeek.com/go-is-unapologetically-flawed-heres-why-we-use-it/>

3. 减小锁粒度至少不会使性能更糟,但这里显然是过早优化了。
Lpl
2021-04-06 21:56:42 +08:00
@makdon
@lesismal
写了一个简单的测了下,性能确实差 Mutex 挺多。是我滥用了
package counter

type ChanCounter struct {
data map[string]int
resultChan chan *string

stopChan <-chan struct{}
}

func NewChanCounter(stopChan <-chan struct{}) *ChanCounter {
c := &ChanCounter{
stopChan: stopChan,
resultChan: make(chan *string, 10000),
data: make(map[string]int),
}
go c.run()
return c
}

func (c *ChanCounter) Incr(key *string) {
c.resultChan <- key
}

func (c *ChanCounter) run() {
for {
select {
case r := <-c.resultChan: {
c.data[*r]++
}
case <-c.stopChan:
break
}
}
}
zkdfbb
2021-04-06 23:03:34 +08:00
@makdon
@lesismal
@GTim
@Orlion
@sxfscool
@Lpl
@ClarkAbe
@kcojkcnw

我补了一份最小测试用例,你们看看能不能复现,我仍然是一头雾水
zkdfbb
2021-04-06 23:08:20 +08:00
如果是 test.lua 是

request = function()
num = math.random(1, 1)
path = "/item/id" .. num
return wrk.format("GET", path)
end


就是链接唯一的话,又正常
dallaslu
2021-04-06 23:38:23 +08:00
很有意思的现象。我跑了一下,打印了 Incr 的返回值和 accessLog.data:

```
access log: 34reafas0fasdfre7 = 1
access log: 34reafas0fasdfre8 = 1
access log: 34reafas0fasdfre8 = 2
access log: 34reafas0fasdfre8 = 1
access log: 34reafas0fasdfre8 = 1
access log: 34reafas0fasdfre8 = 1
access log: 34reafas0fasdfre8 = 1
map[34reafas0fasdfre8:1 34reafas0fasdfre8:6]
```
dallaslu
2021-04-06 23:45:44 +08:00
@dallaslu

1. 只有一个 id 的话没有问题
2. 与 Nginx 无关
3. 经过多次测试,总计数是正确的,只是个别 key 打印时显示为其他 key
3. Incr 返回值看起来遇到了并发问题的 key,一般也覆盖了其他 key
zkdfbb
2021-04-07 00:03:01 +08:00
@dallaslu 确实是这样
makdon
2021-04-07 00:58:29 +08:00
app := fiber.New(fiber.Config{Prefork: false, Immutable: true})
这样写就正常了
初略看了下代码,不使用 immutable 时,框架是直接用 unsafe 把 []byte 转成 string 然后抛出
感觉跟内存块复用 /golang memory model 有关
具体还得仔细看看源码
dallaslu
2021-04-07 01:06:20 +08:00
刚又忍不住改代码实验了一下,估计可以结帖了。待楼主验证。

楼主的问题和并发呀、锁呀之类的可能没有关系,和 fiber 倒是有关系。

map 肯定不会出现重复 key 的。所以当打印 map 时,如果显示有相同的 key,那么这个 key 一定是被鬼附了身。

正如 @makdon #45 所说,「 id 是 url path 里面取的」,那么鬼就是从 fiber 来的,可能是 fiber 重复使用了内存空间。所以我改了一下:

```
accessLog.Incr(id + "")
```

然后问题就再没复现过了。
dallaslu
2021-04-07 01:07:58 +08:00
@makdon 听上去靠谱 :)

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

https://tanronggui.xyz/t/768320

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

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

© 2021 V2EX