MoreRSS

site iconhuizhou | 萝卜修改

Golang 分布式相关的主题,读书笔记
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

huizhou | 萝卜的 RSS 预览

【译】“安全是我们的首要任务”--这是一句废话

2024-12-19 11:07:26

Featured image of post 【译】“安全是我们的首要任务”--这是一句废话

原文链接:“Security Is Our Top Priority” is BS
几年前,我被邀请去做一个关于软件安全的会议演讲。其实,我并没有真正被邀请,而是我的公司购买了一个包含演讲席位的赞助包,我回复了一封内部邮件,自愿参与了这个活动 🤣 无论如何,在准备我的演讲过程中,我意识到了几个关于安全的重要观点,这些观点自此一直萦绕在我的心头:

  1. 安全是无止境的。你总是可以投入更多努力来提高安全性。质量和安全、员工满意度等也是如此。
  2. 安全的需求与便捷的用户体验需求相冲突。增强一个方面往往会损害另一个方面。

现在,有些组织宣称“安全是我们的首要任务”。真的吗?你想要将一个没有上限的事情作为你的首要任务?我的意思是,安全是一件好事,但这听起来是不是有点太简单了?实际上,像这样的空洞营销声明可能会让我有点生气。在这篇文章中,我将帮助你理解如何解读这样的声明,以及在现实生活中如何处理安全问题。我将涵盖:

  • 一个哲学性的介绍
  • “安全是我们的首要任务”实际上意味着什么?
  • 你应该在多大程度上重视安全?
  • 公司应该怎么说替代这句话?

安全性与用户体验平衡的哲学思考

安全的无限性以及它与用户体验(UX)之间的平衡让我想起了某件事。我不得不停下来思考了一会儿,然后我想起来了。大约在我19或20岁的时候,我发现了GK Chesterton。我非常喜欢他,因为他让我意识到实际上有非常了不起的基督教思想家,而在我那种多少算是福音派的背景中,这样的人并不多。不幸的是,尽管有Chesterton的影响,但与20岁时的我希望的相反,我个人的信仰并没有坚持下去。不过,我还是从他的著作中学到了很多至今仍感激的东西。这里有一段与我们讨论的话题相关的引用:

“现代社会并不邪恶;在某些方面,现代社会太过美好了。它充满了狂野而被浪费的美德。当一个宗教体系被粉碎(正如基督教在宗教改革中被粉碎那样),被释放的不仅仅是恶习。恶习确实被释放了,它们四处游荡并造成破坏。但美德也被释放了;美德游荡得更加疯狂,美德造成的破坏更加可怕。现代社会充满了旧基督教美德的疯狂。美德之所以疯狂,是因为它们彼此孤立,独自游荡。因此,有些科学家只关心真理;而他们的真理是无情的。因此,有些人文主义者只关心怜悯;而他们的怜悯(很遗憾地说)往往是不真实的。”——GK Chesterton,《正统》,1908年

Chesterton的意思是,好的事物(美德)如果被过分追求或者脱离了其他好的事物,就可能变成坏事。这听起来似乎是显而易见的,确实如此。但是一旦你掌握了这个模式,就会发现它一直在发生。在这篇博文中,我们谈论的是那些忽视了UX的安全狂热者,但在我们的政治世界中也是如此。有些人将多样性、公平性和包容性(DEI)推行到极致,以至于将白人男性妖魔化。有些人将家庭价值观推崇到极致,让其他人感到格格不入。家庭价值观和包容性都是好东西,但如果它们被孤立对待,就可能走向极端,并在群体之间造成分裂。

因此,我们需要在所有美好的事物之间找到一个健康的平衡,不让任何一个走向极端而忽视其他。这并不容易!如果你是一个安全极端主义者,即使你还是被黑客攻击了,至少可以说你已经竭尽全力来提高安全性。另一方面,如果你在安全性和UX之间取得了平衡,并为了更好的UX而做出了一些降低安全性的决定,然后遭到了黑客攻击。你如何为自己辩护?说“现实是复杂的,我们在这种情况下决定优先考虑UX”并不是一个听起来很好的回答。捍卫非极端立场需要勇气,因为极端主义者总是有更有力的口号。

“安全是我们的首要任务”实际上意味着什么?

安全真的是无限的吗?我认为是的。实际上,银行最安全的运营方式可能是关闭他们的在线业务,购买一个大保险库,并在门外部署一支小型军队。即便如此,安全检查的强度和军队的规模也可以无限增加。然而,大多数人还是更愿意通过银行应用程序的面部识别来转账,并认为这种安全程度已经足够好。

公司真的会这么说吗?是的。比如微软的“将安全置于一切之上”,AWS的“在AWS,云安全是最高优先级”,Meta的“保护您的数据是我们的最高优先级”,还有很多其他例子。

这听起来不错,但在实践中,这是否意味着只要有人提出以牺牲UX、消费者价格等为代价来提高安全性的想法,你就会实施?因为这似乎是它的意思。当然,答案是否定的,所以我对这些声明并不太认真,尽管我理解这给客户带来了一种温暖而模糊的感觉。我更希望组织能够坦诚和清晰,但我只能梦想在一个这样的世界里。

实际上,这些公司所表达的可能是:

“我们有一个平衡的优先级框架,我们对问题进行分类并给予每个问题加权得分。我们给UX问题分配20%,安全25%,技术债务10%,新功能20%,等等。正如你所见,风险是首要优先级,因为25%高于20%。”

或者他们可能意味着:

“安全(达到我们行业中标准水平的安全性)是我们的首要任务。一旦我们实施了足够的安全措施,我们就会专注于其他问题。”

如果你有完全诚实的安全声明的例子,比如“我们关心安全,因为我们需要你信任我们,因此它是我们的首要任务之一”,我会非常感兴趣。请在评论中分享!

安全性应该做到什么程度?

当我在2018年做那次演讲时,我还不清楚到底应该在何处划线。自那以后,我开始使用ISO 14971和ISO 27001这样的风险框架进行工作,现在我已经有了一些工具来帮助我处理这个问题。

一切始于一个评分机制:弄清楚我们需要保护的是什么,存在哪些风险,以及这些风险发生的可能性和严重程度如何。你将可能性和严重性相乘,为每个风险得出一个风险分数,然后将这些分数映射到低、中、高三个等级。

  • 如果风险分数是低的,你可以安全地接受这个风险。
  • 如果是中等,你应该考虑风险控制,除非有正当理由不这么做。
  • 如果是高风险,你必须实施风险控制。

然后,一旦风险控制措施到位,验证它们是否真正减轻了风险,剩余风险是否现在变低或不存在。这样你就完成了。

听起来简单,但低、中、高的界限我们应该怎么划分呢?我总是把一些决策者聚在一起,给他们提供可能出错的情况的例子,然后请他们提出一个评分系统。接着询问他们愿意接受哪些类型的风险,同时考虑到如果应用了风险控制,产品或流程会是什么样子。

这个简单的评分系统包括风险矩阵、风险偏好/风险接受政策和风险清单。这是一个简单的工具,可以帮助你做出决策。

我们应该怎么说?

几十年来,全球都在以成熟的方式处理安全、质量和安全问题。让我们推广那些实际这么做的公司,而不是说这是他们的首要任务。这是一个没有意义的声明。

在我理想的世界里,公司会说:“我们维护着最先进的安全体系,因为没有客户的信任,我们作为公司就没有存在的权利。因此,这是我们工作中最重要的事情之一,我们投入了大量的精力。”这可能对大多数人来说听起来没有那么好,但这样说的公司肯定会给我留下深刻的印象。
原文连接:“Security Is Our Top Priority” is BS

go1.24: 新的标准库 weak

2024-12-14 19:40:46

Featured image of post go1.24: 新的标准库 weak

weak 是什么?

Golang 在1.24 中带来了一个新的std-lib weak。 可以为*T 创建一个安全的引用,但是不会阻止 *T 被GC 回收。

Package weak provides ways to safely reference memory weakly, that is, without preventing its reclamation.

OS.ROOT 一样, weak 也是一个在其他语言中存在很久的功能,比如:

  • JavaWeakReferenceSoftReference 是经典实现,主要用于缓存和对象池。它们能够在 JVM 检测到内存不足时自动回收。
  • Python 提供了 weakref 模块,允许创建弱引用对象,常用于防止循环引用问题或缓存。
  • c++std::shared_ptr 中引入了 std::weak_ptr,用于解决共享指针的循环依赖问题。
  • Rust 提供 RcArc 的弱引用版本 Weak,也用于避免循环引用并提升内存管理的灵活性。

weak 的定义很简单,一个Make 方法还有一个Value 方法。
“Illustration of hashtriemap structure, showcasing an efficient data storage and retrieval mechanism as a sync.Map alternative in Go programming.”
通过weak.Make 创建一个weak.Pointer ,如果 T 没有被回收的话,我们可以通过weak.Pointer.Value 获取T的地址。 否则就会返回 nil,很简单。
我们可以通过一个简单的例子来实践一下 weak。

 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
func main() {
	originalObject := "Hello, World!"
	runtime.AddCleanup(&originalObject, func(s int64) {
		fmt.Println("originalObject clean at: ", s)
	}, time.Now().Unix())
	weakPtr := weak.Make(&originalObject)
	fmt.Println(fmt.Sprintf("originalObject:addr %x", &originalObject))
	fmt.Println(fmt.Sprintf("weakPtr addr:%x,size:%d", weakPtr, unsafe.Sizeof(weakPtr)))
	runtime.GC()
	time.Sleep(1 * time.Millisecond)
	value := weakPtr.Value()
	if value != nil && strings.Contains(*value, originalObject) {
		fmt.Println("First GC :value: ", *value)
	} else {
		fmt.Println("first gc. Weak reference value is nil")
	}
	runtime.GC()
	time.Sleep(1 * time.Millisecond)
	value = weakPtr.Value()
	if value != nil {
		fmt.Println("Second GC", *value)
	} else {
		fmt.Println("Second GC: Weak reference value is nil")
	}
}

https://gist.github.com/hxzhouh/abd6be9ed8860e506643031bb2d446ce

运行结果

1
2
3
4
5
6
7
8
➜  weak git:(main) ✗ gotip version 
go version devel go1.24-18b5435 Sun Dec 15 21:41:28 2024 -0800 darwin/arm64
➜  weak git:(main) ✗ gotip run main.go              
originalObject:addr 14000010050
weakPtr addr:{1400000e0d0},size:8
First GC :value:  Hello, World!
originalObject clean at:  1734340907
Second GC: Weak reference value is nil

在上面的代码中,我们创建了一个 string 变量 originalObject ,然后使用weak.Make 创建了一个 weak.Pointer weakPtr

  • 在第一次GC 的时候,因为originalObject 在后面还有使用,所以 weakPtr.Value 返回了 originalObject 的地址。
  • 在第二次GC 的时候,originalObject 没有被使用,它被GC回收了, 所以 weakPtr.Value 返回了nil
  • runtime.AddCleanup 也是go 1.24 新增的功能,它的功能类似 runtime.SetFinalizer,也是在 对象呗垃圾回收的时候用于执行一段代码。我后面可能会详细介绍它
    通过上面的例子,我们可以知道
  1. weak.Make 通过创建一个中间地址(weak.Printer)将真实地址隐藏起来。
  2. weak.Printer 不会影响 真实地址的垃圾回收,如果真实地址被垃圾回收了,weak.Printer.Value 将会返回nil。由于不知道真实地址什么时候会被回收,所以需要仔细检查weak.Printer.Value 的返回值。

weak 有什么作用

canonicalization maps

相信您还记得在go 1.23 中添加的 unique 它可以将多个相同的字符串用一个指针(8个字节)来表示,达到节约内存的目的 , weak 也能实现类似的效果.(实际上 go 1.24unique 已经使用 weak 重构了)

实现一个固定大小的缓存

下面是一个使用weak+ list.List 实现 固定大小缓存的例子

 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
type WeakCache struct {  
    cache   map[string]weak.Pointer[list.Element] // Use weak references to store values  
    mu      sync.Mutex  
    storage Storage  
}  
  
// Storage is a fixed-length cache based on doubly linked tables and weaktype Storage struct {  
    capacity int // Maximum size of the cache  
    list     *list.List  
}
// Set
func (c *WeakCache) Set(key string, value any) {  
    // If the element already exists, update the value and move it to the head of the chain table  
    if elem, exists := c.cache[key]; exists {  
       if elemValue := elem.Value(); elemValue != nil {  
          elemValue.Value = &CacheItem{key: key, value: value}  
          c.storage.list.MoveToFront(elemValue)  
          elemWeak := weak.Make(elemValue)  
          c.cache[key] = elemWeak  
          return  
       } else {  
          c.removeElement(key)  
       }  
    }  
    // remove the oldest unused element if capacity is full  
    if c.storage.list.Len() >= c.storage.capacity {  
       c.evict()  
    }  
  
    // Add new element  
    elem := c.storage.list.PushFront(&CacheItem{key: key, value: value})  
    elemWeak := weak.Make(elem)  
    c.cache[key] = elemWeak  
}

完整的代码请参考: https://gist.github.com/hxzhouh/1945d4a1e5a6567f084628d60b63f125
我们可以创建一个固定大小的list ,然后使用一个Map记录keylist 中的位置,value 是一个指向 list.Elementweak.Pointer. 如果 key 存在于list 上,那么 Map[key].Value 会返回 list 的地址。 再给cache 添加数据的时候,会先判断list 的大小,如果已经list已经满了的话,就把队尾的数据淘汰。Map[key].Value返回nil。
这样,我们就能构建一个高效+固定大小的cache系统。 weak + 无锁队列 可以构建出更加高效的数据结构。

就我个人而言,weak 在特定场合下还是挺有用处的。使用起来也很简单,我会在项目中积极使用weak

更多关于 weak 的资料

  1. https://tip.golang.org/doc/go1.24#weak
  2. https://github.com/golang/go/issues/67552

深入探讨 BigCache 的性能优化手段

2024-12-04 21:42:56

Featured image of post 深入探讨 BigCache 的性能优化手段

bigcache是一个高性能的内存缓存库,专为需要高并发访问和低延迟响应的应用场景设计。本文将深入探讨 BigCache 的性能优化手段,包括分片机制、高效哈希算法、读写锁的使用等,并引用相关源码进行详细说明。

分段加锁

从使用者的角度来看cache就像一个大的hashtable,可以存储k/v 格式的数据。 那么,是不是可以使用一个map[string][]byte + sync.RWMutex 实现满足需求的cache呢?
如果性能要求不高,的确可以这么做。
sync.RWMutex虽然对读写进行了优化,但是对于并发的读,最终还是把写变成了串行,一旦写的并发量大的时候,即使写不同的key, 对应的goroutine也会block,只允许一个写执行,这是一个瓶颈,并且不可控。
bigcache 参考了 java ConcurrentMap 的实现方式,将一个大hashtable 分成多个小的 shard,每个分片一把锁,很多大并发场景下为了减小并发的压力都会采用这种方法,比如MongoDB的sharding等。Golang也有一个第三方的 ConcurrentMap 实现 concurrent-map

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://github.com/allegro/bigcache/blob/a2f05d7cbfdc7000a8b7e1c40f27d3f239c204df/bigcache.go#L58
type BigCache struct {
    shards       []*cacheShard
    shardMask    uint64
    config       Config
}
func newBigCache(ctx context.Context, config Config, clock clock) (*BigCache, error) {
    ......
    cache := &BigCache{
        shards: make([]*cacheShard, config.Shards),
        lifeWindow: lifeWindowSeconds,
        clock: clock,
        hash: config.Hasher,
        config: config,
        shardMask: uint64(config.Shards - 1),
        close: make(chan struct{}),
    }
    ......
    for i := 0; i < config.Shards; i++ {
        cache.shards[i] = initNewShard(config, onRemove, clock)
    }
    ......
    return cache, nil
}

对于每一个缓存对象,根据它的key计算它的哈希值: hash(key) % N。 理想情况下N个 goroutine 每次请求正好平均落在各自的shard上,这样就不会有锁竞争了。即使有多个goroutine同时请求,如果hash比较平均的话,单个shard的压力也会比较小。可以降低延迟,因为等待获取锁的时间变小了。 

N的选择

既然分片可以很好的降低锁的竞争,那么N是不是越大越好呢?当然不是,如果N非常大,比如每个缓存对象一个锁,那么会带来很多额外的不必要的开销。可以选择一个不太大的值,在性能和花销上寻找一个平衡。

另外, N是 2的幂, 比如16、32、64。这样设计的好处就是计算余数可以使用位运算快速计算。

1
2
3
4
// https://github.com/allegro/bigcache/blob/a2f05d7cbfdc7000a8b7e1c40f27d3f239c204df/bigcache.go#L253
func (c *BigCache) getShard(hashedKey uint64) (shard *cacheShard) {
    return c.shards[hashedKey&c.shardMask]
}

因为对于 2 的幂N,对于任意的x, 下面的公式成立:
x mod N = (x & (N − 1))
所以只需要使用一次按位 AND (&) 就可以求得它的余数。

选择合适的 hash 算法

计算机科学家已经发明了很多的Hash算法,gopher也实现了很多Hash算法。一个优秀的Hash算法有以下特点

  • 哈希值应该比较随机 (质量)
  • 哈希速度比较快 (速度)
  • 尽量不产生额外的内存分配, 避免对垃圾回收产生压力 (耗费资源少)

bigcache 提供了一个默认的 Hash算法的实现,采用 fnv64a 算法。这个算法的好处是采用位运算的方式在栈上进行运算,避免在堆上分配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type fnv64a struct{}

const (
    offset64 = 14695981039346656037
    prime64 = 1099511628211
)
func (f fnv64a) Sum64(key string) uint64 {
var hash uint64 = offset64
for i := 0; i < len(key); i++ {
        hash ^= uint64(key[i])
        hash *= prime64
    }
return hash
}

内存优化

对于 Go 语言中的 map, 垃圾回收器在 markscan阶段检查 map 中的每一个元素, 如果缓存中包含数百万的缓存对象,垃圾回收器对这些对象无意义的检查导致不必要的时间开销。

作者做了测试。他们测试了简单的 HTTP/JSON 序列化 (不会访问 cache)。 在 cache 为空的时候 1 万的 QPS 的耗时大约 10 毫秒。当 cache 填满的时候, P99 的请求都会超过 1 秒。监控显示堆中包含 4 千万的对象, GC 过程中的 mark 和 scan 也需要 4 秒。
那是因为如果hashtable中的元素包含指针,那么GC在mark会扫描每个包含指针的元素,如果不包含指针,在mark阶段就会跳过这些元素。
我们可以用一个简单的例子测试一下。

 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
var pointMap map[int]*int  
var noPointMap map[int]int  
  
func BenchmarkPointMap(b *testing.B) {  
    pointMap = make(map[int]*int)  
    for i := 0; i < 10e6; i++ {  
       pointMap[i] = &i  
    }  
    b.ResetTimer()  
    for i := 0; i < b.N; i++ {  
       delete(pointMap, i)  
       pointMap[i] = &i  
    }  
}  
  
func BenchmarkNoPointMap(b *testing.B) {  
    noPointMap = make(map[int]int)  
    for i := 0; i < 10e6; i++ {  
       noPointMap[i] = i  
    }  
    b.ResetTimer()  
    for i := 0; i < b.N; i++ {  
       delete(noPointMap, i)  
       noPointMap[i] = i  
    }  
}

测试结果如下

1
2
3
4
5
6
7
➜  gc git:(main)GOMAXPROCS=1  go test --bench=. 
goos: darwin
goarch: arm64
pkg: blog-example/go/gc
cpu: Apple M1 Pro
BenchmarkPointMap        5273188               209.4 ns/op
BenchmarkNoPointMap      7037848               178.5 ns/op

然后分别运行两个测试,分析 gc

1
2
go test -bench=BenchmarkPointMap -trace trace_point.out  
go test -bench=BenchmarkNoPointMap -trace trace_no_point.out 

NoPointMap 的 Wall Duration 只有PointMap 的2% 。虽然PointMap 的并发量很小,并且单个的 goroutine也没有竞争,但是由于元素的数量很多,垃圾回收在mark/scan阶段需要花费上百毫秒进行标记和遍历。

Pasted image 20241205155902
Pasted image 20241205155823
bigcache 是如何解决这个问题的?禁止你的用户在 bigcache 上存储带有指针的数据。
开个玩笑,如果真的这么做,你的用户会抛弃你。有几种办法可以解决这个问题。

  1. 参考 offheap,使用定制化的Malloc() 和 Free() 来手动管理内存,而绕过runtime 垃圾回收,但是这样会比较容易导致内存泄露。
  2. 第二种方式是使用 freecache。freecache 通过减少指针的数量以零 GC 开销实现 map。它将键和值保存在ringbuffer中,并使用索引查找对象。

bigcache 实现的方式是使用哈希值作为map[int]int的 key。 把缓存对象序列化后放到一个预先分配的大的字节数组中,然后将它在数组中的 offset 作为map[int]int的 value。

 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
59
60
61
62
63
64
65
66
//https://github.com/allegro/bigcache/blob/a2f05d7cbfdc7000a8b7e1c40f27d3f239c204df/shard.go#L18
type cacheShard struct {
    hashmap     map[uint64]uint32
    entries     queue.BytesQueue
    lock        sync.RWMutex
    entryBuffer []byte
    onRemove    onRemoveCallback
    isVerbose    bool
    statsEnabled bool
    logger       Logger
    clock        clock
    lifeWindow   uint64
    hashmapStats map[uint64]uint32
    stats        Stats
}

func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {  
    currentTimestamp := uint64(s.clock.Epoch()) 
  
    s.lock.Lock() 
  
    if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 { // 检查是否存在相同哈希键的旧条目  
       if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil {  
          resetHashFromEntry(previousEntry) // 重置旧条目的哈希  
          delete(s.hashmap, hashedKey) // 从哈希表中删除旧条目  
       }  
    }  
  
    if !s.cleanEnabled { // 如果清理功能未启用  
       if oldestEntry, err := s.entries.Peek(); err == nil {  
          s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry) // 处理最旧的条目  
       }  
    }  
  
    w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer)  
  
    for {  
       if index, err := s.entries.Push(w); err == nil { // 尝试将新条目推入队列  
          s.hashmap[hashedKey] = uint64(index) // 更新哈希表  
          s.lock.Unlock() 
          return nil 
       }  
       if s.removeOldestEntry(NoSpace) != nil { // 如果空间不足,删除最旧的条目  
          s.lock.Unlock()  
          return errors.New("entry is bigger than max shard size") // 返回错误  
       }  
    }  
}

func (s *cacheShard) getValidWrapEntry(key string, hashedKey uint64) ([]byte, error) {  
    wrappedEntry, err := s.getWrappedEntry(hashedKey)  
    if err != nil {  
       return nil, err  
    }  
  
    if !compareKeyFromEntry(wrappedEntry, key) {  
       s.collision()  
       if s.isVerbose {  
          s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, readKeyFromEntry(wrappedEntry), hashedKey)  
       }  
  
       return nil, ErrEntryNotFound  
    }  
    s.hitWithoutLock(hashedKey)  
    return wrappedEntry, nil  
}

queue.BytesQueue是一个字节数组,按需分配。当加入一个[]byte时,它会把数据 copy 到尾部。

bigcache 在删除缓存元素的时候, 只是把它从的索引从map[uint64]uint32中删除了,并把它在queue.BytesQueue队列中的数据置为0。删除操作会在 queue.BytesQueue中造成很多的 “空洞”,而且这些 “虫洞” 不会被整理,也不会被移除。因为它的底层是使用一个字节数组实现的,“空洞” 的移除是一个耗时的操作,会导致锁的持有时间过长。bigcache 只能等待清理机制把这些 “空洞” 删除掉。

其他一些细节: 

  1. bigcache 中的缓存对象没有刷新过期时间的功能,所有的缓存最终都会过期。
  2. 所有缓存的生命周期都是由config.LifeWindow 配置,不能针对单独的key设置。

那个编程语言才是最快的?

2024-12-04 19:20:16

Ben Dicken (@BenjDicken) 做了一项测试,执行双层循环, 1 万 * 10 万= 10 亿次循环,看看哪种编程语言快。为此还制作了一个动图来直观展示。

![[iWkHAGyRUf1bKIW6.gif]]

一般来说,这种项目,最精彩的是issue。
热心的开发者贡献了各种语言的版本,比如ZigJuliaPerlElixirFortanC#Lua
同时,还在讨论应该怎样优化代码
比如 @dolanor 提了一个PR # optimize go loops with goroutine 认为Golang的长处是在并发编程,单线程下它的效率肯定比不上CRust,应该用goroutine来优化。

@Brandon-T# Benchmark Issues 讨论了现有基准测试存在的问题及改进方向,核心观点为测试不应包含程序启动、打印等无关时间,应聚焦代码执行本身。

不知不觉我几乎把整个issue全部看完了。

这个项目让我想到了年初的1BRC。在枯燥的编码生活中,这是一个很好的消遣。同时能够学习一些性能优化的技巧,参与到与世界各地的人的讨论中来。
我希望这样的活动能够多一点。

linux:一台服务器最大能支持多少条 TCP 连接

2024-12-02 15:50:14

Featured image of post linux:一台服务器最大能支持多少条 TCP 连接

“因为TCP端口号是16位无符号整数,最大65535,所以一台服务器最多支持65536个TCP socket连接.” 这是一个非常经典的误解! 即使是有多年网络编程经验的人,也会持有这个错误结论。

其中0-1023 端口是系统保留的端口,并不能被普通应用程序所使用,这里暂时不考虑这个情况.而是以65535 代替。

要戳破这个错误结论,可以从理论和实践两方面来。

理论

*unix 系统通过一个四元组来唯一标识一条TCP连接. 这个四元组的结构是{local_ip, local_port, remote_ip, remote_port}。所以,对于IPv4, 系统理论上最多可以管理2^(32+16+32+16),也就是2的96次方个连接。

IPv4 可以理解成一个 32位正数

  • 因为对于同一台服务器来说,一般只有一个 local_ip,那么同一台服务器可以管理 2^(16+32+16) 个连接。
  • 一个服务(进程, 如 Nginx 进程)一般只监听一个 local_port,那么同一台服务就可以管理 2^(32+16) 个连接。
  • 如果从一台远端机器(所谓的 client)来连接这台服务器上的一个服务,那么 local_iplocal_portremote_ip 这3个变量是固定的,那么就只能建立 2^16=65536 个连接了。这就是经典的误解的来源!

如果不仅仅考虑TCP,则是一个五元组,加上协议号(TCP,UDP或者其它)。所以一个服务器最多能支持多少个TCP连接,它的限制不在于四元组,而是其他参数。

文件描述符

我们知道在Linux中一切都是文件(socket也是文件),最大能打开的文件数量,决定了能够同时建立TCP连接的数量,那么一台服务器最大能打开多少个文件呢?

  • 查看系统支持的最大打开文件描述符数,
1
2
[root@test1 ~]# cat /proc/sys/fs/file-max
1616352
  • 单个进程能打开的最大文件描述符数量
1
2
[root@test1 ~]# ulimit -n
1024

这两个值都是可以改变的,一般在进行压力测试的时候,会手动调整这个值。

ip_local_port_range

如果某个客户端向同一个TCP端点(ip:port)发起主动连接,那么每一条连接都必须使用不同的本地TCP端点,如果客户端只有一个IP则是使用不同的本地端口,该端口的范围在*nix系统上的一个例子是32768到61000左右,可以通过如下命令查看:

1
2
[root@test1 ~]# cat /proc/sys/net/ipv4/ip_local_port_range
32768	60999

也就是说,一个客户端连接同一个服务器的同一个ip:port(比如进行压力测试),最多可以发起30000个左右的连接。不过,对于client端,操作系统会自动根据不同的远端 ip:port,决定是否重用本地端口。

内存&CPU

一个ESTABLISH状态的socket大约消耗3.3KB内存,如果没有数据业务的话CPU占用很低。所以从内存角度来看,一台服务器能支持的最大TCP 连接数量也是有上线的,远远到不了4元组的上限。
Pasted image 20241202152043

总结

一台服务器最大能支持多少条 TCP 连接的上限是确定的,那就是2^96 个,但是它的下限,需要根据很多情况来判断,比如内存、CPU、文件描述符等。没有具体答案。

如何在MacBook上搭建GitLab

2024-11-16 14:58:29

Featured image of post 如何在MacBook上搭建GitLab

最近想要系统的学习一下基础设施方面的知识,所以准备搭建一个学习环境,我没有多余的机器使用,只有一个MacBook Pro 2021 ,所以选择在笔记本上使用 Docker 搭建一套环境,目前看来第一步还是顺利的。

安装 GitLab

Mac 的M1芯片使用的是ARM架构,所以我们去寻找 ARM架构的镜像, 我是用的是yrzr/gitlab-ce-arm64v8:latest

首先需要创建 gitlab-ce 的三个工作目录 etc、 log、 opt ,不然会报错 。
volumes里面的配置修改成你的工作目录就可以了。这里暴露了两个端口9922、9980 因为是在本地使用,所以就没开放https的443 端口,后面也不准备使用https。
docker-compose.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
version: "3.8"
services:
  gitlab-ce:
    image: yrzr/gitlab-ce-arm64v8:latest
    container_name: gitlab-ce
    privileged: true
    restart: always
    ports:
      - "9922:22"
      - "9980:9980"
    volumes:
      - /Users/hxzhouh/tools/gitlab/etc:/etc/gitlab:z
      - /Users/hxzhouh/tools/gitlab/log:/var/log/gitlab:z
      - /Users/hxzhouh/tools/gitlab/opt:/var/opt/gitlab:z
    deploy:
      resources:
        limits:
          memory: 4096M
    tty: true
    stdin_open: true

这里需要注意的是,我们对外暴露的端口是9980,因为我们后面会配置 gitlab 的http 端口运行在9980 而不是 默认的80 ,这样做,是为了避免这个问题:
https://stackoverflow.com/questions/66961517/gitlab-http-clone-url-is-wrong-port-8022-missing

等待docker 被拉起来,然后进入gitlab-ce 里面修改 配置 /etc/gitlab/gitlab.rb

1
2
docker exec -it gitlab-ce /bin/bash
vi /etc/gitlab/gitlab.rb

在最后添加三行,保存退出。

1
2
3
external_url 'http://127.0.0.1:9980'
gitlab_rails['gitlab_ssh_host'] = '127.0.0.1'
gitlab_rails['gitlab_shell_ssh_port'] = 9922

然后修改默认密码

1
2
3
4
gitlab-rails console -e production
user = User.where(id: 1).first
user.password = 'AIl+mVN(:bk\#5%c'
user.save!

Pasted image 20241115193324
最后在执行reload 操作,然后重启

1
2
gitlab-ctl reconfigure
gitlab-ctl restart

Pasted image 20241115193440
稍等片刻。
然后 浏览器输入127.0.0.1:9980 就可以打开gitlab-ce了,默认的root 账号密码就是我们刚刚修改的密码。

添加ssh

跟使用GitHub一样,我们先创建一个ssh 密钥对, 然后将公钥添加到 GitLab里面。在本地 ~/.ssh/config 里面添加配置

1
2
3
4
5
6
Host 127.0.0.1
HostName 127.0.0.1
Port 9922
IdentityFile ~/.ssh/ssh
PreferredAuthentications publickey
User root

测试一下

1
2
➜  .ssh ssh -T [email protected]
Welcome to GitLab, @root!    

ssh 是没有问题的。

尝试新建一个项目

Pasted image 20241115195500
创建成功,在本地将代码拉下来。

1
git clone ssh://[email protected]:9922/root/hello-world.git

ssh 推送也是没有问题的

1
2
3
➜  hello-world git:(main) git push origin --force
To ssh://127.0.0.1:9922/root/hello-world.git
   b4dd724..e45f213  main -> main

Gitlab Runners 配置

在setting -> CI/CD -> Runners 点击 …,然后 根据 ‘Show runner installation and registration instructions’ 文档 安装 runners
Pasted image 20241116093057
我这里输入的命令是:

1
2
3
4
5
sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64
sudo chmod +x /usr/local/bin/gitlab-runner
gitlab-runner install
gitlab-runner start
gitlab-runner register --url http://127.0.0.1:9980 --registration-token GR13489416KQ-jitR3JhfMgr8f-9G

Pasted image 20241116144505

有几个关键点需要注意

  1. Enter tags for the runner (comma-separated):
    1. GitLab是用 tag来管理runner 的,最好是一个runner做一件事情,用tag 标记,写.gitlab.ci.yml 的时候需要指定tags
  2. Enter an executor
    1. executor 有很多种,我这里为了演示,选择了shell

更多关于GitLab Runner 的 的介绍可以参考官网的文章,这里就不做展开。
安装好了就可以在 http://127.0.0.1:9980/admin/runners 看到效果了
Pasted image 20241116145026

最后,在 刚才创建的工程中,添加一个 .gitlab-ci.yml 测试一下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
stages:  
  - build  
build_job:  
  stage: build  
  tags:  
    - golang-local-shell  
  script:  
    - go build -o myapp  
  only:  
    - main  
    - tags  
  artifacts:  
    paths:  
      - myapp

将代码推送到gitlab,很快就能编译完了 http://127.0.0.1:9980/root/hello-world/-/pipelines

Pasted image 20241116143812

然后在 http://127.0.0.1:9980/root/hello-world/-/artifacts 可以找到 构建的产物。
Pasted image 20241116144233

至此,在本地搭建GitLab环境已经弄好,下一篇文章,在折腾在本地搭建k8s 集群,然后从GitLab自动打包成docker镜像推送到 k8s 集群,完成一个CI/CD的完整流水线。