2024-12-19 11:07:26
原文链接:“Security Is Our Top Priority” is BS
几年前,我被邀请去做一个关于软件安全的会议演讲。其实,我并没有真正被邀请,而是我的公司购买了一个包含演讲席位的赞助包,我回复了一封内部邮件,自愿参与了这个活动 🤣 无论如何,在准备我的演讲过程中,我意识到了几个关于安全的重要观点,这些观点自此一直萦绕在我的心头:
现在,有些组织宣称“安全是我们的首要任务”。真的吗?你想要将一个没有上限的事情作为你的首要任务?我的意思是,安全是一件好事,但这听起来是不是有点太简单了?实际上,像这样的空洞营销声明可能会让我有点生气。在这篇文章中,我将帮助你理解如何解读这样的声明,以及在现实生活中如何处理安全问题。我将涵盖:
安全的无限性以及它与用户体验(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
2024-12-14 19:40:46
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 也是一个在其他语言中存在很久的功能,比如:
Java
的 WeakReference
和 SoftReference
是经典实现,主要用于缓存和对象池。它们能够在 JVM 检测到内存不足时自动回收。Python
提供了 weakref
模块,允许创建弱引用对象,常用于防止循环引用问题或缓存。c++
在 std::shared_ptr
中引入了 std::weak_ptr
,用于解决共享指针的循环依赖问题。Rust
提供 Rc
和 Arc
的弱引用版本 Weak
,也用于避免循环引用并提升内存管理的灵活性。weak 的定义很简单,一个Make
方法还有一个Value
方法。
通过weak.Make
创建一个weak.Pointer
,如果 T
没有被回收的话,我们可以通过weak.Pointer.Value
获取T
的地址。 否则就会返回 nil
,很简单。
我们可以通过一个简单的例子来实践一下 weak。
|
|
https://gist.github.com/hxzhouh/abd6be9ed8860e506643031bb2d446ce
运行结果
|
|
在上面的代码中,我们创建了一个 string
变量 originalObject
,然后使用weak.Make
创建了一个 weak.Pointer
weakPtr
originalObject
在后面还有使用,所以 weakPtr.Value
返回了 originalObject
的地址。originalObject
没有被使用,它被GC回收了, 所以 weakPtr.Value
返回了nil
runtime.AddCleanup
也是go 1.24
新增的功能,它的功能类似 runtime.SetFinalizer
,也是在 对象呗垃圾回收的时候用于执行一段代码。我后面可能会详细介绍它weak.Make
通过创建一个中间地址(weak.Printer
)将真实地址隐藏起来。weak.Printer
不会影响 真实地址的垃圾回收,如果真实地址被垃圾回收了,weak.Printer.Value
将会返回nil
。由于不知道真实地址什么时候会被回收,所以需要仔细检查weak.Printer.Value
的返回值。相信您还记得在go 1.23
中添加的 unique
它可以将多个相同的字符串用一个指针(8个字节)来表示,达到节约内存的目的 , weak
也能实现类似的效果.(实际上 go 1.24
中unique
已经使用 weak 重构了)
下面是一个使用weak
+ list.List
实现 固定大小缓存的例子
|
|
完整的代码请参考: https://gist.github.com/hxzhouh/1945d4a1e5a6567f084628d60b63f125
我们可以创建一个固定大小的list
,然后使用一个Map
记录key
在list
中的位置,value
是一个指向 list.Element
的weak.Pointer
. 如果 key 存在于list 上,那么 Map[key].Value
会返回 list 的地址。 再给cache 添加数据的时候,会先判断list 的大小,如果已经list已经满了的话,就把队尾的数据淘汰。Map[key].Value
返回nil。
这样,我们就能构建一个高效+固定大小的cache
系统。 weak
+ 无锁队列
可以构建出更加高效的数据结构。
就我个人而言,weak
在特定场合下还是挺有用处的。使用起来也很简单,我会在项目中积极使用weak
更多关于 weak 的资料
2024-12-04 21:42:56
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。
|
|
对于每一个缓存对象,根据它的key计算它的哈希值: hash(key) % N
。 理想情况下N个 goroutine
每次请求正好平均落在各自的shard上,这样就不会有锁竞争了。即使有多个goroutine
同时请求,如果hash
比较平均的话,单个shard
的压力也会比较小。可以降低延迟,因为等待获取锁的时间变小了。
既然分片可以很好的降低锁的竞争,那么N是不是越大越好呢?当然不是,如果N非常大,比如每个缓存对象一个锁,那么会带来很多额外的不必要的开销。可以选择一个不太大的值,在性能和花销上寻找一个平衡。
另外, N
是 2的幂, 比如16、32、64。这样设计的好处就是计算余数可以使用位运算快速计算。
|
|
因为对于 2 的幂N
,对于任意的x
, 下面的公式成立:x mod N = (x & (N − 1))
所以只需要使用一次按位 AND (&
) 就可以求得它的余数。
计算机科学家已经发明了很多的Hash算法
,gopher也实现了很多Hash算法
。一个优秀的Hash算法
有以下特点
bigcache
提供了一个默认的 Hash算法
的实现,采用 fnv64a
算法。这个算法的好处是采用位运算的方式在栈上进行运算,避免在堆上分配。
|
|
对于 Go 语言中的 map, 垃圾回收器在 mark
和scan
阶段检查 map 中的每一个元素, 如果缓存中包含数百万的缓存对象,垃圾回收器对这些对象无意义的检查导致不必要的时间开销。
作者做了测试。他们测试了简单的 HTTP/JSON 序列化 (不会访问 cache)。 在 cache 为空的时候 1 万的 QPS 的耗时大约 10 毫秒。当 cache 填满的时候, P99 的请求都会超过 1 秒。监控显示堆中包含 4 千万的对象, GC 过程中的 mark
和 scan
也需要 4 秒。
那是因为如果hashtable
中的元素包含指针,那么GC在mark会扫描每个包含指针的元素,如果不包含指针,在mark阶段就会跳过这些元素。
我们可以用一个简单的例子测试一下。
|
|
测试结果如下
|
|
然后分别运行两个测试,分析 gc
|
|
NoPointMap
的 Wall Duration
只有PointMap
的2% 。虽然PointMap
的并发量很小,并且单个的 goroutine也没有竞争,但是由于元素的数量很多,垃圾回收在mark/scan
阶段需要花费上百毫秒进行标记和遍历。
bigcache
是如何解决这个问题的?禁止你的用户在 bigcache
上存储带有指针的数据。
开个玩笑,如果真的这么做,你的用户会抛弃你。有几种办法可以解决这个问题。
Malloc()
和 Free()
来手动管理内存,而绕过runtime
垃圾回收,但是这样会比较容易导致内存泄露。ringbuffer
中,并使用索引查找对象。bigcache
实现的方式是使用哈希值作为map[int]int
的 key。 把缓存对象序列化后放到一个预先分配的大的字节数组中,然后将它在数组中的 offset 作为map[int]int
的 value。
|
|
queue.BytesQueue
是一个字节数组,按需分配。当加入一个[]byte
时,它会把数据 copy 到尾部。
bigcache
在删除缓存元素的时候, 只是把它从的索引从map[uint64]uint32
中删除了,并把它在queue.BytesQueue
队列中的数据置为0。删除操作会在 queue.BytesQueue
中造成很多的 “空洞”,而且这些 “虫洞” 不会被整理,也不会被移除。因为它的底层是使用一个字节数组实现的,“空洞” 的移除是一个耗时的操作,会导致锁的持有时间过长。bigcache
只能等待清理机制把这些 “空洞” 删除掉。
其他一些细节:
bigcache
中的缓存对象没有刷新过期时间的功能,所有的缓存最终都会过期。config.LifeWindow
配置,不能针对单独的key设置。2024-12-04 19:20:16
Ben Dicken (@BenjDicken) 做了一项测试,执行双层循环, 1 万 * 10 万= 10 亿次循环,看看哪种编程语言快。为此还制作了一个动图来直观展示。
![[iWkHAGyRUf1bKIW6.gif]]
一般来说,这种项目,最精彩的是issue。
热心的开发者贡献了各种语言的版本,比如Zig
、Julia
、Perl
、Elixir
、Fortan
、C#
、Lua
等
同时,还在讨论应该怎样优化代码
比如 @dolanor 提了一个PR # optimize go loops with goroutine 认为Golang的长处是在并发编程,单线程下它的效率肯定比不上C
、Rust
,应该用goroutine来优化。
@Brandon-T 在 # Benchmark Issues 讨论了现有基准测试存在的问题及改进方向,核心观点为测试不应包含程序启动、打印等无关时间,应聚焦代码执行本身。
不知不觉我几乎把整个issue全部看完了。
这个项目让我想到了年初的1BRC。在枯燥的编码生活中,这是一个很好的消遣。同时能够学习一些性能优化的技巧,参与到与世界各地的人的讨论中来。
我希望这样的活动能够多一点。
2024-12-02 15:50:14
“因为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) 个连接。local_port
,那么同一台服务就可以管理 2^(32+16) 个连接。local_ip
,local_port
,remote_ip
这3个变量是固定的,那么就只能建立 2^16=65536 个连接了。这就是经典的误解的来源!如果不仅仅考虑TCP,则是一个五元组,加上协议号(TCP,UDP或者其它)。所以一个服务器最多能支持多少个TCP连接,它的限制不在于四元组,而是其他参数。
我们知道在Linux中一切都是文件(socket也是文件),最大能打开的文件数量,决定了能够同时建立TCP连接的数量,那么一台服务器最大能打开多少个文件呢?
|
|
|
|
这两个值都是可以改变的,一般在进行压力测试的时候,会手动调整这个值。
如果某个客户端向同一个TCP端点(ip:port)发起主动连接,那么每一条连接都必须使用不同的本地TCP端点,如果客户端只有一个IP则是使用不同的本地端口,该端口的范围在*nix
系统上的一个例子是32768到61000左右,可以通过如下命令查看:
|
|
也就是说,一个客户端连接同一个服务器的同一个ip:port(比如进行压力测试),最多可以发起30000个左右的连接。不过,对于client端,操作系统会自动根据不同的远端 ip:port,决定是否重用本地端口。
一个ESTABLISH
状态的socket
大约消耗3.3KB
内存,如果没有数据业务的话CPU占用很低。所以从内存角度来看,一台服务器能支持的最大TCP 连接数量也是有上线的,远远到不了4元组的上限。
一台服务器最大能支持多少条 TCP 连接的上限是确定的,那就是2^96
个,但是它的下限,需要根据很多情况来判断,比如内存、CPU、文件描述符等。没有具体答案。
2024-11-16 14:58:29
最近想要系统的学习一下基础设施方面的知识,所以准备搭建一个学习环境,我没有多余的机器使用,只有一个MacBook Pro 2021
,所以选择在笔记本上使用 Docker 搭建一套环境,目前看来第一步还是顺利的。
Mac 的M1芯片使用的是ARM架构,所以我们去寻找 ARM架构的镜像, 我是用的是yrzr/gitlab-ce-arm64v8:latest
首先需要创建 gitlab-ce 的三个工作目录 etc、 log、 opt ,不然会报错 。
将volumes
里面的配置修改成你的工作目录就可以了。这里暴露了两个端口9922、9980 因为是在本地使用,所以就没开放https的443 端口,后面也不准备使用https。
docker-compose.yaml
|
|
这里需要注意的是,我们对外暴露的端口是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
|
|
在最后添加三行,保存退出。
|
|
然后修改默认密码
|
|
最后在执行reload 操作,然后重启
|
|
稍等片刻。
然后 浏览器输入127.0.0.1:9980
就可以打开gitlab-ce了,默认的root 账号密码就是我们刚刚修改的密码。
跟使用GitHub一样,我们先创建一个ssh 密钥对, 然后将公钥添加到 GitLab里面。在本地 ~/.ssh/config 里面添加配置
|
|
测试一下
|
|
ssh 是没有问题的。
尝试新建一个项目
创建成功,在本地将代码拉下来。
|
|
ssh 推送也是没有问题的
|
|
在setting -> CI/CD -> Runners 点击 …,然后 根据 ‘Show runner installation and registration instructions’ 文档 安装 runners
我这里输入的命令是:
|
|
有几个关键点需要注意
.gitlab.ci.yml
的时候需要指定tags更多关于GitLab Runner 的 的介绍可以参考官网的文章,这里就不做展开。
安装好了就可以在 http://127.0.0.1:9980/admin/runners
看到效果了
最后,在 刚才创建的工程中,添加一个 .gitlab-ci.yml 测试一下。
|
|
将代码推送到gitlab,很快就能编译完了 http://127.0.0.1:9980/root/hello-world/-/pipelines
然后在 http://127.0.0.1:9980/root/hello-world/-/artifacts 可以找到 构建的产物。
至此,在本地搭建GitLab环境已经弄好,下一篇文章,在折腾在本地搭建k8s 集群,然后从GitLab自动打包成docker镜像推送到 k8s 集群,完成一个CI/CD的完整流水线。