MoreRSS

site iconluozhiyun修改

93年,非科班程序员,喜欢健身、读书、编程。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

luozhiyun的 RSS 预览

对腾讯5年职业生涯的总结

2025-10-24 11:11:58

先摆出我的四个知识奖的奖牌镇楼,哈哈哈。该是时候总结一下自己这五年多来做了什么事情了,希望对各位能有所启发。

IMG_7331

其实我每年都有发自己的总结文章,附上以前的一些总结:

2024年总结:沉寂积蓄新的力量

2023年总结:保持心情愉悦&积极向上

2022年总结:保持&缓行

2021年总结

2020年总结

文章

对于我自己来说是比较喜欢研究技术,写文章,所以这五年确实沉淀了很多我以前研究的技术文章。发布的文章承蒙各位喜爱,在腾讯内网也上过很多次头条和推荐,这里我再总结一下,希望这些文章能带给大家一些启发或者帮助。

Frame 2

AI

合集

AI 相关的文章合集是当时因为图片生成爆火,然后自己顺着AI的名义给自己买了一张 4090 显卡,图片玩过几次除了生成一堆涩图以外想着是不是可以用来干点正事,于是就想着能不能学点AI 相关的知识,于是就有了这个系列的文章。

[长文]写给开发同学AI强化学习入门指南

这篇文章应该是我写的最满意的文章之一了,首先里面有真实的学习步骤是怎样的,里面讲了我是怎么学习的,以及弄了一些 demo 来跑我们的模型,总而言之入门很合适了。

如何用 PPO 算法让 AI 学会玩 FlappyBird

后面我还用强化学习,根据图片来学习怎么玩 FlappyBird 这个也很有意思。

合集里面剩下的几篇AI文章就是讲的我怎么在生活中使用 AI 的也希望能给大家一点启发。

Golang

合集:https://www.luozhiyun.com/archives/category/%E5%90%8E%E7%AB%AF/go

go相关的技术文章是写了真的不少,因为当时自己对这一块比较感兴趣,工作中也常用,所以写了很多源码研究之类的文章,很多文章即使现在来看都写的不错,诸如:

[长文]从《100 Go Mistakes》我总结了什么?

这篇文章有时候没事我会去看看的,因为细节太多,很多时候一不留神写的代码容易出错,里面的错误范例对于工作中还是很实用。

如何编译调试Go runtime源码

调试源码这个事情,一般是不会去做,但是如果对源码感兴趣是可以参考一下怎么去调试。

下面几篇我也觉得写的不错,就是比较底层,读起来最好自己跟着看看代码,否则会比较费劲。

Go语言GC实现原理及源码分析

从源码剖析Go语言基于信号抢占式调度

从栈上理解 Go语言函数调用

一文教你搞懂 Go 中栈操作

C++

合集:https://www.luozhiyun.com/archives/tag/cpp

C++ 相关的文章也是写了不少,写这些文章的时候基本上也是边写边学,写文章的同时顺带把我的疑惑点也给解答了。

计算机基础

合集:https://www.luozhiyun.com/archives/tag/%e8%ae%a1%e7%ae%97%e6%9c%ba%e5%9f%ba%e7%a1%80

这方面也是我自己慢慢想着要整理一下自己现有的知识体系,然后写的一些文章。

作为开发需要了解 SSD 的一切

这篇文章可能在生产中还是有点用,算是科普一下SSD原理,以及如何更高效的使用。

CPU 是如何与内存交互的?

这一篇文章也是我对知识体系的一个梳理,但是很多知识点也只是起了一个抛砖引玉的效果,更多的知识感兴趣的同学可以去看看 《深入理解计算机系统》 、《深入浅出计算机组成原理》 等书。

云原生网络

合集:https://www.luozhiyun.com/archives/category/%e7%bd%91%e7%bb%9c

这里面的文章还是有点意思的,写这些文章也是因为我当时在研究 k8s 相关的技术的时候,对书里面的网络感觉很多地方都不懂,然后就自己专门又找了点资料自己研究了一下。

其他

这里就是再列举一些不知道分成什么类的文章,但是我觉得写的也不错。

深入 RocksDB 高性能的技术关键

这篇文章对于 RocksDB 是怎么实现的做了很详尽的分析,我觉得写的还是不错的,里面的图也画的很精美(我其他文章图也画的很精美)。

Protobuf 编码&避坑指南

这篇文章里面讲了一下 protobuf编码原理 & 最佳实践,我觉得在工作中还是很实用的,里面的图也画的很精美。

构建属于自己的云游戏服务器

其实我是很喜欢玩游戏的,但是不能在公司流畅的玩我想玩的游戏这点让我很难受,所以我决定自己来搞。

Yolov5物体识别与应用

当时沉迷于 DNFM 无法自拔,但是我又不想每天花2个小时搬砖,所以就想有什么办法可以让电脑来自动化这件事情。

最后

虽然我的鹅厂生涯结束了,但是我对这个世界的探索不会结束,感兴趣的可以继续关注我的公众号 & 微信 & 博客(文章最后放出)。

这五年来的一些改变

更健康的饮食

最近一两年我开始自己做饭,然后中午带饭去公司吃。我一开始觉得做饭是一种浪费时间的行为,但是后面觉得自己做饭一方面可以更健康的生活,另一方面做饭的时候我喜欢自己一边做饭一边听点播客,我基本什么都听,其实也算是了解其他行业的渠道。

其实就目前来看,我们的饮食里面其实配比是很不正常的,所以我会在做饭的时候会有这几点要求:

  1. 蔬菜的量尽量多点;
  2. 不要吃太多的碳水;
  3. 少摄入油盐;
  4. 戒糖;

控糖

其实我们生活中很大的疲倦感除了是真正的劳累造成的以外,血糖的波动也会带来疲倦感。血糖不稳定,就像让我们的身体坐上一辆失控的“过山车”,时而冲上顶峰(高血糖),时而跌入谷底(低血糖):

吃完饭血糖飙升 → 感到困倦、没精神(高血糖疲劳) → 胰岛素过量分泌 → 血糖骤降 → 感到虚弱、无力、发慌(低血糖疲劳) → 赶紧吃东西(尤其是高糖食物) → 再次飙升……

由于我们传统的饮食结构的影响,我们食物中大多是米饭,以及面条为主食,他们都含有大量的淀粉,淀粉会消化分解为葡萄糖,这些葡萄糖分子被小肠吸收到血液中,“血糖”指的就是血液中的葡萄糖浓度。因此,大量葡萄糖涌入血液,血糖水平就会迅速飙升。

当然,我要控糖也和我的尿酸偏高有关系。果糖(Fructose),对尿酸的影响非常显著,甚至可以说是独立于“高嘌呤饮食”之外的另一个主要诱因。糖对尿酸的影响是:

  • 一方面(开源):果糖在肝脏代谢,直接加速了嘌呤的分解,导致尿酸产量增加
  • 另一方面(节流):高糖引起的胰岛素抵抗,导致肾脏对尿酸的排泄减少

我们日常吃的白砂糖(蔗糖)在体内会分解成一半葡萄糖和一半果糖。而奶茶、可乐等甜饮料中添加的“高果糖玉米糖浆”,其果糖含量更高。

所以,控糖基本上就包含了两种:

一种是少吃各种碳水,米饭,面条等等,之类的含有高淀粉的食物,用糙米、黑米、藜麦、燕麦米等全谷物来部分或全部替代白米饭;

另一方面是含有甜味的食物,如奶茶,可乐等含有果糖的食物尽量不要去吃,顺带一提,很多水果也是高果糖的,也不能多吃;

蔬菜

然后再来提一下蔬菜,其实我们平时的蔬菜摄入实在是太少了,根据《中国居民膳食指南(2022)》成年人每日应摄入300至500克的蔬菜,但是很多时候蔬菜摄入实在太少了,比如一份点外卖,里面可能就几根蔬菜。所以我现在即使是早上都是粗粮比较多,比如会早上吃玉米,一份蔬菜,三个鸡蛋,然后才是两个小笼包。

蔬菜的好处有很多啦,我就不重复了,我说一点我比较关注的,就是可以帮助我们控制和稳定血糖的强大“盟友”。

蔬菜中的膳食纤维,特别是可溶性膳食纤维,扮演着“血糖缓冲器”的角色。可溶性膳’便纤维在遇水后会形成一种凝胶状物质,包裹住食物。这会显著减慢胃排空的速度和食物在肠道中的消化过程,从而使碳水化合物(糖)的吸收变得更加平缓,避免餐后血糖像坐过山车一样急剧飙升。并且纤维能够提供强烈的饱腹感,有助于控制食量。

运动

健身

其实我到现在为止健身超过 8 年了,从我大学时候就开始了,至今为止也练的不错,以前锻炼是想要练成super man 这样的肌肉形状,后面自己尝试过之后发现没有上科技根本不可能,现在也就保持这样的体型已经很久了,也就是其实和我的职业生涯一样,进入了“平台期”很久了。

Frame 1

而且随着年纪的慢慢增大,继续激烈的力量训练是很容易受伤的,近3年我就分别受伤了3次,腰部、肩膀、手腕都受伤过。手腕是最近一次去年 11月受伤,直到现在都没怎么恢复好,所以各位运动的时候一定要注意安全。

开始跑步

我从今年年初开始跑步,跑步真的给我很大的帮助,每次都竭尽全力完成每次计划的跑量真的很爽。然后看着自己的跑速,心肺,各项指标都在稳定的提高真的很开心,要知道人到中年能稳定提升的能力不多了

image-20251019115853678

跑步和健身很不一样,一方便跑步的时候没有组间休息,另一方面就是一旦开始了就不能停。现在我一旦心情不舒畅就会大跑一场,跑完之后你会发现自己无比淡定,再糟心的事情也不过如此,没有什么是过不去的。

一般的情况下我会选一个播客来听,这样起码让我的跑步过程不会很无聊,我一般喜欢听下面几个播客:

  • 知行小酒馆:听的最多的应该是这个,讲如何生活,讲投资。但是有些嘉宾请的也不是很好,比如 Anker 那期,翻开评论你可以发现全是骂的;

  • 半拿铁:里面会讲很多商业漫谈,以及商业史,但是我最喜欢的还是最喜欢他们的西游篇,把西游记从头到尾讲了一遍;

  • 面基:这个博主主要讲投资相关的东西,有时候看到感兴趣的主题会看看;

  • 起朱楼宴宾客:也是主要讲经济,商业,投资相关的,还不错;

  • The Wanderers 流浪者:这个是我最近喜欢听的,他们会对最近的实事进行总结,讲讲他们的投资理念,主理人是三个专业的投资人,讲解的话题覆盖了A股,港股,美股,加密货币;

image-20251019155331023

投资

我其实前几年开始就在想存钱,投资,然后可以产生复利,这样就可以通过投资反哺我的生活,让我有一定的被动收入,所以我也一直没买房买车,奢侈类的消费基本不参与,看准自己的目标,并为之而努力。

我的收入其实现在是分为几个部分:

  1. 现金以应对不确定性,在我没工作的时候可以给我提供支撑,或者市场大跌的时候可以有资金可以入场;
  2. 低波动资产,如纳指ETF、标普ETF、恒生指数ETF、货币基金等;
  3. 股票资产,主要以美股为主,港股为辅;

1和2大概占了我总资产的三分之一,3占了三分之二。我至今为止,其实盈利还行,主要还是选股选择的都是一些可以拿的住的一些股票,大多持股超过了1年,即使在波动比较大或者出现黑天鹅的时候,我也没有选择卖出,反而进行了加仓,这也是我投资的理念,除非公司大的方向发生了转变,否则我应该还会继续持有。

我也不是交易大师,但是我觉得这些交易策略是可以通用的:

  1. 不要追高,这点很重要,当然这是肯定要承受踏空的失落感,但同时起码能让你不亏;
  2. 做好交易笔记,保证是经过充分思考而买入,而不是一拍脑袋,然后就投入了你的血汗钱,买个手机都会货比三家,更何况是股票;
  3. 不要看短期热点炒股,热点这个东西有点虚无缥缈,我们是很难抓住的,通常消息传到我们耳朵里的时候,已经快过时了;

杂谈

人生是旷野

就在昨天跑步的时候,我都在想什么是财富自由?财富自由是表示自己不工作也不会饿死吗?表示自己可以一直躺平吗?我觉得不是,我觉得应该是无需为了生计而出卖自己的时间,可以自由选择自己想要的生活方式。也就是说我可以选择我的时间应该花在哪里,我可以将时间和精力投入到自己真正热爱的事业、兴趣爱好、家庭或社会贡献中,而不用被动地接受工作的束缚。

如果接受了这条假设,那么就表示代表人生不是只有一条路,而是充满了各种各样的选择和不确定性,就如同旷野一样。我可以去探索、去尝试,去定义自己的成功和幸福,而不是遵循他人的标准。当然我也需要自己去开辟道路,面对困难和迷茫。这既是挑战,也是成长的过程。

关于 AI 带给我的思考

最近我用 Claude Code 越多,我越发现它在平时的 coding 中能发挥的影响力就越大,我用它不限于:

  1. 对于陌生的项目,我让它帮我先看一下这个项目是做什么的,核心逻辑是啥,如何使用;
  2. 对于一些重复的代码,我会整理好模版和需求之后让它帮我快速实现,几乎100%没有问题;
  3. 对于一些自己也拿不准的需求,可以让它帮忙先出一个方案其实是一个挺好的探索;
  4. 现在用 Claude Code 做页面效果已经很好了,我不会写页面,但是也用它弄了一些网页,效果还不错;
  5. 很多时候写代码的时候,都会想要用它来帮我优化一版;

基于这几年对 ai 工具的持续使用,感觉现在真的已经慢慢的渗透到我们的日常工作中了,虽然现在很多时候仍需要手动接管,但是已经可以节省很多时间了,可以遇见在不远的未来,coding 的工作被取代也是情理之中的事情。

所以在这之前我一直在想,我能做什么?

我能做什么

对于我来说,我一直在审视自己的能力边界,以及后面可以持续发力的点,我不是个可以随时躺平的人,也不喜欢躺平,能找到一份热爱的事情,并持之以恒的做下去可以说是我人生的一直以来的追求。

那么对于我来说,可以一直做下去的事情莫过于有以下几种:

  1. 真的找到了可以一直深耕的业务,并且这个业务是需要人来做的,AI无法取代人,比如涉及钱的金融类的核心工作,至少在 AI 的幻觉被解决之前,操作钱的事情,交给人来做稳定性要更高一点,并且它具有一定的业务复杂度,并且发展很快;
  2. 找到真正热爱的事情,就像巴菲特一样,每天可以跳着踢踏舞去上班,每天上班都是带着激情去,而不是疲倦,不过这个事情是可遇不可求的;
  3. 一人公司,现在 ai 能力越来越强,我是真的觉得可能会有这个趋势,以前是一个人扎的越深,可能越值钱,但是未来我觉得在这一行广度也同样重要。只要有个很好的想法,ai 就可以帮我实现不再是一句空话,我自己而言就落地了几个产品,其实还不错;

所以对于我来说,我现在年纪不是特别大,对生活也有激情,钱其实也不是那么缺,在我了解到我既不可能在这里一直呆到退休,也不可能直接呆到财富自由,那么我一直在对自己发出灵魂拷问:

为什么不把剩余的时间投资到更有意义的地方呢?难道一定要等自己老了,尝啥都没味道了,逛任何地方都没有意思了,眼睛里面没有光了,才能开始做自己的事情吗?

IMG_7266 1

扫码_搜索联合传播样式-白色版 1

对腾讯5年职业生涯的总结最先出现在luozhiyun`s Blog

回顾《Scaling Memcache at Facebook》论文

2025-05-18 17:28:35

facebook 现在业务特点奠定了 Memcache 设计的原则:首先读要比写要多,这很明显,因为FB中浏览人数肯定大于发表的;其次在FB中查询的数据源会来源不同系统,如 MySQL、HDFS等,这种异构性要求一种灵活的缓存策略,能够存储来自不同来源的数据。

image-20250420175751676

如上图所示,memcache 读数据的时候先尝试从 memcache 中读数据,若读取失败则从DB中获取数据填充到 memcache 中;写数据时,先更新数据库,然后将 memcache 中相应的数据删除。

这里其实涉及到缓存一致性的问题,更新缓存的策略其实可以演变出4种策略:

更新数据库后更新缓存、更新数据库前更新缓存、更新数据库后删除缓存、更新数据库前删除缓存。
大体上,采取先更新数据库再删除缓存的策略是没有问题的,但是真实场景下,还是会有一个情况存在不一致的可能性,这个场景是读线程发现缓存不存在,于是读写并发时,读线程回写进去老值。并发情况如下:

时间 线程A(写请求) 线程B(读请求–缓存不存在场景) 潜在问题
T1 查询缓存,缓存缺失,查询数据库得到当前值100
T2 更新主库 X = 99(原值 X = 100)
T3 删除缓存
T4 将100写入缓存 此时缓存的值被显式更新为100,但是实际上数据库的值已经是99了

这里其实 FB 引入了 leases 解决这个问题,后面再说。

如何降低延迟

在FB中,用户的一个简单页面浏览可能会产生上百个 memcache 请求,并且 memcache 是分布式部署的,并通过一致性哈希算法进行路由,所以web服务器通常需要与多台 memcache 服务器通信才能完成用户请求,这种 all-to-all 的请求方式会造成两个问题:

  1. Incast Congestion:当许多 Web 服务器(发送方)同时同一个 memcache 服务器(接收方)发送请求时,会瞬间产生大量的网络流量涌向该 memcache 服务器或其连接的网络交换机端口。这会超出交换机缓冲区或 memcache 服务器网卡的处理能力,导致数据包丢失、重传和延迟急剧增加;
  2. Single Server Bottleneck:某个 memcache 服务器因为持有特别热门的数据("hot spot"),或者自身处理能力稍弱,或者暂时出现性能波动,那么它也可能成为瓶颈;

FB 主要在每个 Web 服务器上的 memcache 客户端 来解决延迟问题。解决之道主要有两个:

  • Parallel requests and batching(并行请求和批量处理):这主要是优化 RTT 次数,将可以一起取的数据通过一次 RTT 一并取出,减少时延;
  • Client-server communication(客户端-服务器通信):这个优化主要将控制逻辑集中到 memcache client。memcache client 分成两部分:sdk 与 proxy,proxy 也叫做 mcrouter,它用来桥接 web server 与 memcached server。

Client 的请求还进一步优化,将查询请求优化为 UDP 请求,写请求为 TCP 请求。因为 FB 的业务中是读多写少的,且读数据对错误的容忍度高,所以查询的时候通过使用 UDP 可以让 client 直接与memcached服务器通信,无需经过 mcrouter 中转,从而可以降低大量的开销。

在 FB 的实践中,会丢弃掉由于 UDP 数据乱序或者延迟产生的错误数据,运行中大约有 0.25% 的请求被丢弃的,其中 80% 都是由于延迟或丢包导致的,剩余是由于顺序错乱所致。

处理写请求时,为了稳定性所以使用 TCP 来进行处理,但是 TCP 是有连接的,为了减少 client 与 memcache的连接,加了一层中间层 mcrouter ,client 只需要与 mcrouter 建立连接,然后由 mcrouter 与 memcache 集群建立连接,这也叫做 connection coalescing (合并连接),通过这种方式可以降低网络、CPU、内存的开销。

我们通过 FB 的统计数据可知,使用 UDP 大概节省了 20% 左右的延迟。

image-20250426173646439

为了解决Incast Congestion问题,FB 在 client 中使用了滑动窗口机制来做流量限制,滑动窗口的大小和 TCP 的拥塞机制做的有点类似,滑动窗口的大小会在请求成功的时候缓慢增大窗口,在请求没有回应的时候缩小窗口。

image-20250426180000094

当窗口较小时,应用必须串行分发更多批次的memcache请求,从而延长了网络请求的处理时长。而窗口过大时,同时发起的memcache请求会引发网络拥塞,在这两种极端情况之间存在着最佳平衡点,既可避免不必要的延迟,又能最大限度减少网络拥塞。

降低负载

租期 Leases

FB 使用 Leases 主要为了解决两个问题:

  • 陈旧写入(Stale Set)
  • 惊群效应(Thundering Herd)

Stale Sets(陈旧写入)

当多个客户端同时更新同一键值时,若网络延迟或系统调度导致写入操作乱序到达,后提交的更新可能覆盖先前已生效的更新,后续读取可能将旧数据重新写入缓存,形成持久性污染。

虑以下操作序列:

  1. 客户端B发送DELETE K指令清除缓存
  2. 客户端A由于网络延迟,稍后发送SET K=V1
  3. 客户端C在此时读取K,将获得已过时的V1值

这种时序错乱会导致缓存系统与持久化存储之间的数据不一致,进而引发业务逻辑错误。

惊群效应(Thundering Herd)

惊群效应(Thundering Herd)特指当某个热点缓存项失效时,大量并发请求同时穿透缓存层,直接冲击后端数据库的现象。这种场景类似于自然界的"踩踏事件",在毫秒级时间窗口内形成请求洪峰。

假设某热门内容(如病毒式传播的视频元数据)缓存过期:

  1. 缓存服务器标记键K为失效状态
  2. 在接下来的100ms内,10,000个客户端同时请求K
  3. 所有请求均未命中缓存,触发数据库查询
  4. 数据库瞬间承受超过日常峰值10倍的查询压力

这种突发负载可能导致数据库响应延迟激增,严重时引发级联故障。

综上,基于这几个问题,FB 使用 Leases 机制,通过 令牌仲裁速率限制 同时解决上述两大问题:

Stale Sets 的解决路径:

  1. 令牌绑定:每个缓存未命中事件生成唯一租约令牌,客户端需携带令牌进行写入;
  2. 无效化仲裁:若键值在租约有效期内被删除(如数据库更新),后续携带该令牌的写入将被拒绝;
  3. 单调性保证:通过令牌版本号确保写入操作的时序一致性,消除乱序覆盖;

Thundering Herds 的抑制策略

  1. 令牌发放速率限制:每键每 10 秒 仅发放一个有效租约,强制后续请求等待或重试;
  2. 渐进式回填:首个获得租约的客户端负责从数据库加载数据并回填缓存,其他客户端在短暂等待后可直接读取缓存;

伪代码如下:

// 伪代码示例:租约校验逻辑
void handle_get_request(key) {
    if (cache_miss(key)) {
        if (lease_rate_limit_exceeded(key)) {
            return WAIT_RESPONSE;
        }
        lease_token = generate_lease(key);
        return lease_token;
    }
}

void handle_set_request(key, value, lease_token) {
    if (validate_lease(key, lease_token)) {
        cache_set(key, value);
    }
}

进一步的,FB 还设置了 Stale values 机制,可以让业务来自行选择是否使用略微过期的数据来降低请求等待的时间。当一个 key 被删除时,这个 key 的值被短暂的存储到一个过期数据的地方,但是这个时候memcache里面还没被写入新的值,这个时候如果业务不想等待,那么可以直接取走老的数据,从而加速响应时间。

Memcache Pools 缓存池

memcache因为是一个基础应用,但是需要应对不同应用,那么不同业务的工作负载可能会对 memcache 缓存命中产生负面干扰,导致命中率下降。所以为了应对不同的业务,FB 在 memcache 中按照业务的特点来划分了不同的缓存进行隔离。

FB 将缓存集群划分为:

  1. Wildcard Pool:存储常规数据,采用标准淘汰策略
  2. Small Pool:存放高频访问但缺失代价低的元数据
  3. Large Pool:存储低频访问但重建成本高的批量数据

因为当高低频数据共存于同一池时,会产生 缓存驱逐风暴,高频数据持续写入导致低频数据的 LRU 链提前被截断,低频数据被迫频繁回填,引发数据库查询洪峰。

Replication Within Pools 池内复制

其核心目标是通过数据冗余换取吞吐量提升,适用批量读取密集型场景,比如应用需要单次请求获取大量关联键值。

举个例子,如用户动态页需加载100+社交关系数据:

  • 分片方案:将100键分片到2节点,每节点处理50键请求总吞吐量维持1M QPS,单节点负载500K QPS;

  • 复制方案:2副本各存储完整100键总吞吐量提升至2M QPS,单节点负载500K QPS。

虽然两种方案的单节点负载相同,但复制方案通过减少网络交互次数实现了端到端延迟优化。

Gutter 机制

Gutter 被定义为临时故障接管池,当一个常规的 memcache 服务器发生故障,客户端在访问该服务器上的数据时会超时或失败。此时,客户端会将请求重定向到Gutter服务器。如果Gutter服务器中也没有这份数据(缓存未命中),客户端会从后端数据库获取数据,并将这份数据写入到Gutter服务器中。这样,Gutter服务器就临时接管了故障服务器的缓存职责,吸收了原本会直接冲击数据库的请求。

如果没有 Gutter 服务器,少量memcache 服务器的故障会导致大量请求直接打到后端数据库。Gutter通过提供一个临时的缓存层,有效地缓冲了这种冲击。并且 Gutter中的条目通常设置了较短的过期时间。当故障的Memcache服务器被修复或替换后,系统会逐渐恢复正常,Gutter中的数据也会因为过期而自动清理,避免了复杂的失效通知机制。同时,Gutter的存在使得在少量服务器故障期间,系统仍然能够保持较高的缓存命中率。

也就是说 Gutter 起到了故障转移与负载吸收、防止雪崩等作用。在实践中,该Gutter 将客户端可见的故障率降低了99%,并且每天将10%-25%的缓存失败转换为缓存命中。如果memcache 服务器完全失败,Gutter 的命中率通常在4分钟内超过35%,并且通常接近50%,有效的吸收了负载。

Regional 按区域复制

在单个集群内扩展 memcache 虽然在一定程度上有效,但在延迟、负载管理和容错方面会遇到限制 。随着 Web 服务器数量和请求量的增加,单个 memcache 集群可能会成为瓶颈。并且管理和维护如此庞大的单体集群也带来了运营挑战 。对于频繁访问(“热”)的键,简单地将数据分区到更多服务器上(分片)并不能减轻持有这些键的特定服务器上的负载 。

所以 FB 把 memcache 转向了按区域复制,通过按区域部署分散负载并提高故障隔离能力。FB 将位于同一地理区域内部署多个前端集群,每个前端集群由一组处理用户请求的 Web 服务器和 memcache 服务器组成 ,但是同一区域内的所有这些前端集群共享同一个底层存储集群。

前端缓存层与后端持久存储的分离提供了更高的灵活性,FB 能够流量情况扩展前端的 memcache 实例数量,而数据库基础设施可以根据写入量和数据存储需求独立扩展。

Regional Invalidations 区域失效机制

失效机制主要由一个名为 mcsqueal 的守护进程驱动。存储集群,特别是 MySQL 提交日志 ,是数据修改的真实来源 。mcsqueal 守护进程在每个数据库服务器上运行,并监视已提交的 SQL 语句中是否有修改数据的操作。

检测到相关的数据库修改后,mcsqueal 会提取需要失效的相应 memcache 键 。为了最大限度地减少网络开销,这些失效命令(通常是删除操作)被批量处理成更少的包 。然后,这些批处理的失效消息被发送到位于每个前端集群中的专用 mcrouter 服务器 。mcrouter 服务器解包单个删除命令,并将它们路由到其本地集群中相应的 memcache 服务器 。

image-20250510180752441

Regional Pools 区域池

如果不管数据大小和key的访问热度,而盲目进行复制数据可能导致内存效率低下,尤其是对于大型的、很少访问的 key,所以 FB 使用区域池来管理这部分数据。区域池是同一区域内所有前端集群都可以访问的共享 memcache 服务器集合 。区域池中只存储一个副本,应用程序逻辑负责确定哪些键应该驻留在区域池中。

区域池提供了一种通过维护较少访问数据的单个区域副本,从而减少整体缓存层内存占用量的方法。

Cold Cluster Warmup 冷集群预热策略

当一个新的前端集群上线时,其 memcache 服务器最初是空的,导致缓存未命中率很高 。这种后端数据库请求的突然增加可能导致性能下降和潜在的过载。为了缓解这个问题,FB 采用了一种称为冷集群预热的机制 。

在预热过程中,它会将请求转发到同一区域中一个已经运行并具有良好缓存命中率的集群 。然后暖集群将数据提供给冷集群中的客户端,并且冷集群也会用此数据填充其自身的缓存。这使得新集群能够逐步构建其缓存,其中包含频繁访问的项目,而不会直接压垮数据库 。

Across Regions: Consistency 跨区域一致性

FB 通常会指定一个区域作为数据库的主区域用于写,而其他区域则包含只读的副本 。数据不一致的主要原因是主数据库和位于不同区域的副本数据库之间存在复制延迟

来自主区域的写入:mcsqueal 运行在主数据库上,负责提取与数据修改相对应的删除语句。然后,mcsqueal 将这些失效通知广播到主区域内所有前端集群的 memcache 部署。

来自非主区域的写入:FB 为了避免数据不一致的情况,引入了“远程标记”(remote marker)机制 。

当 Web 服务器想要更新键 k 的数据时,会执行以下步骤 :

  • 在区域池(该区域内多个前端集群共享的 Memcache 池)中设置一个远程标记 rk
  • 将写入请求发送到主区域,并在请求中包含 krk,以便在写入复制时失效 。
  • 从非主区域的本地 Memcache 集群中删除键 k

随后,当非主区域再次请求键 k 时,会发生以下情况 :

  • Web 服务器在本地 Memcache 中找不到 k(因为之前已被删除)。
  • 然后,它会检查区域池中是否存在远程标记 rk
  • 如果 rk 存在,则表明本地副本数据库可能仍然是过时的,因此该查询将被重定向到主区域以获取最新数据
  • 如果 rk 不存在,则表明来自主区域的复制可能已经完成,可以从本地副本数据库提供读取服务。

需要注意的是,当存在远程标记时,由于请求需要发送到主区域,这种机制会在缓存未命中时引入额外的延迟 。

Single Server Improvements 单服务器改进

这里的改进主要从数据结构、锁的粒度、协议:

  • 哈希表扩展:自动扩展哈希表以避免O(n)查找时间,确保高效的数据访问;

  • 细粒度锁定:最初引入多线程使用全局锁,随后改进为细粒度锁定;

  • UDP端口分配:为每个线程分配自己的UDP端口,以减少争用并分散中断处理开销;

  • 使用UDP替代TCP:单次获取提高13%,10键多获取提高8%;

总结

可以看到整片论文其实是围绕着下面几点优化进行的:

  • 传输协议优化:UDP用于查询请求,TCP用于写的请求,这样既可以降低查询请求延迟,也可以保证数据的一致性;
  • 租约机制:租约机制防止陈旧集(stale sets)和惊群效应(Thundering Herd)保证分布式集群下的数据一致性;
  • 池化技术的合理利用:系统分为默认通配符池、小池(频繁但缓存未命中成本低)和大池(不频繁但未命中成本高),使用缓存池来剥离不同的业务场景;并对于频繁访问的数据,复制到多个服务器,减少单服务器负载,优化资源使用;
  • 处理故障:使用 Gutter 接管故障服务器,客户端在无响应时重试到排水池,防止雪崩;
  • 跨区域一致性:主区域处理所有写入,并使用 mcsqueal 同步数据库的修改操作到 memcache 前端集群避免竞争条件;
  • 优化单服务器:最后还要提升单服务器的性能,比如使用细粒度锁定减少锁占用,使用UDP替代TCP等;

Reference

https://read.engineerscodex.com/p/how-facebook-scaled-memcached

https://tech.ipalfish.com/blog/2020/04/07/fb-memcache/

回顾《Scaling Memcache at Facebook》论文最先出现在luozhiyun`s Blog

2024年总结:沉寂积蓄新的力量

2025-01-26 17:08:29

今年本来元旦的时候可以抽时间写一下这篇文章,但是临近元旦竟然生病了,然后在床上躺了两天。竟然已经过了,那就不急了,慢慢写了。

新学习了啥?

文章

毫无疑问,我觉得现在的生活节奏是越来越快了,特别是在AI的加持之下,掌握AI并利用它进行终生学习已经是必然趋势了,所以我今年没有像往年一样输出很多技术类的文章,因为我觉得没什么必要了,很多知识直接问一下 AI 再结合自己过往的经验很快就能掌握。话虽然这么说,但是还是写了一些文章:

作为开发需要了解 SSD 的一切

透过ClickHouse学习列式存储数据库

深入 RocksDB 高性能的技术关键

构建属于自己的云游戏服务器

Yolov5物体识别与应用

C++ 中到底什么是”&&“ ?

相比以往动则十几篇,量已经少了很多,不出意外的话明年会更少(拼多多财报式发言,笑~)。

英语

从去年开始要学习突破英语,已经过去了一年,我其实还是蛮想去考个雅思,告诉大家自己的英语成绩有哪些突破,自己的方法有多么牛逼。

但是很可惜的是,我没有这么做,因为去年一年因为兴趣的原因,不太想花很多时间在应付考试上面,做的更多的是口语练习以及泛听磨耳朵训练,所以到后面写作以及雅思的考试范式练习我是一样没做。

不过还是讲一下,过去一年我是怎么优化学习英语的:

口语

首先是随便找一篇文章,或者一个感兴趣的事件,比如我经常不知道找什么话题,就会去 https://engoo.com/app/daily-news 上面找一篇文章看完,然后用对着 AI 复述一遍,让 AI 帮我找出我说的句子或语法有什么问题,或者在复述的时候,就发现了很多内容不会表达,全部都记下来,句子我会手写出来,单词我会记到 flomo 里面作为卡片,方便背诵:

Frame 1

然后,每次在开始每天的英语学习之前,把自己的笔记拿出来,不熟的可以多背背。

到了周末,有比较多的时间,我会和外教上一节自由交流课,其实大多时候没有任何主题,随便聊50分钟。但即使这样,很多时候也可以检验自己的英语到底怎样,因为在说的时候,你会发现自己对有些主题很熟悉,对有些主题很陌生。比如我对工作学习相关的主题就比较熟悉,对美食相关的就不怎么会说,这是因为各种食材,酱料之类的名词记的比较少。

听力

我把听力分为泛听和精听两部份:

我现在洗澡做饭,包括早上健身的时候都会经常泛听一些英语材料,泛听就比较发散了,随便什么都听,下面推荐几个感觉还不错的:

Round Table China:里面有几个人应该是中国人,会用英语和老外聊一些中国的实事,非常好懂,我是比较强烈推荐的;

Think Fast, Talk Smart Podcast:这个播客是斯坦福大学运营的一档节目,优点是发音比较清晰,主题偏向商务,工作,学习等;

Vinh Giang:他的播客主要聚焦于教人怎么表达,随便听听还行,因为主题关系,听多了有点单一;

Marques Brownlee:油管最大的电子区up主,发音清晰,用词也比较简单,主要是讲数码,讲电子产品功能啥的,没事可以听听;

Ariannita la Gringa:她的节目比较简单,多以生活场景为主,人也很上镜,学起来会比较愉快;

EnglishAnyone:这个人的英语会让你听起来很有信心,因为他会用简单的句子和单词告诉你该用什么方式学英语,但是场景比较单一;

Long Beach City College的LBCC Study Skills 课程还挺好听的,可以精听和泛听,这个课程讲的一点都不枯燥,强烈推荐!

对于精听,我一般需要抽一整块时间来听,需要配合插件 《Netflix和YouTube-AFL语言学习》一句句听了:

Tom Bilyeu:专门会找一些领域的大佬来访谈,我蛮喜欢的,比如最近找了 Ray Dalio ,Michael Saylor,

Answer in Progress:我非常喜欢他们的节目,各种小东西的科普,探究其原理,社会现象,每个视频十几分钟,但是完全不枯燥,强烈推荐。

AI带给我的学习上的改变

以前我学习效率都是比较低的,很多时候看到一本书的知识点遇到不懂的地方,也只能网上搜一下,但是目前网上不是没有知识,而是太多了,运气好的话,可以很快得出结果,运气不好得看很多篇同样的的文章才找到自己想要的。

但是现在有了AI之后学习方式可以说完全发生了改变,有些章节可以先问问AI,有个大概的了解再读书这样可以加速理解。对于不懂的可以问AI,然后让AI step by step的告诉你这是为什么,甚至可以给你举一反三,可以说AI成为了世上最好的老师。

还有就是 AI 帮我们做了知识的收口,现在互联网的知识太多了,随便查一个东西不是苦于没有,而是苦于不知道该看哪个,但是 AI 可以只吐给我们有效的知识,帮我过滤无用东西。

所以可以看到,合理的使用AI,是可以加速我们掌握新的技能,从另一个角度上讲,AI可能会让人们在未来更方便的转型,从A跨越到B不会太困难,所以我觉得在未来依旧充满了各种机会。

健康 & 健身

过去一年,我还是一样保持了高频的健身习惯,和过往一样,基本上每周至少 4次力量训练和至少一次的有氧训练,天气不冷的时候有氧训练会去游泳,天气太冷了就会去跑步。

Frame 2

近些年来,我一直保持了良好的健身习惯和饮食,要说今年有什么变化,就是在10月体检报告出来之后发现尿酸超标了一些,虽然没有超标很多,但是还是引起了很我重视。其实我本人也不胖,也不抽烟不喝酒,每天差不多11点就睡了,睡眠时间大概在7.5小时以上,之所以会有高尿酸究其原因我觉得应该是我常年以来的力量训练加上高蛋白饮食导致的。因为无氧训练和高蛋白饮食是容易对肾脏产生不小的压力的,再加上慢慢的年纪也大了,肾脏功能衰退也是意料之中的事情。

为了应付高尿酸,我主要做的一件事情就是利用控制变量观察我的尿酸变化情况,以及平时在饮食中减少对高嘌呤食物,含果糖食物的摄入。后面经过为期两个月的观察,发现其实还是喝水最能解决我的问题,因为人体中的尿酸基本是通过体液排出,所以按理来说只要喝水就能解决。然后我通过控制变量观察到基本上只要一天喝够4000ml的水我的尿酸就能稳定在380左右,其实问题就还好。

最后就是在12月某天训练的时候竟然扭伤了手腕,这也导致了12月我基本上就是腿部训练以及跑步为主,运动量没少,应该还大了,毕竟练腿是很累的 ;)。所以截止到1月我的手腕还没好,连我这个训练老手都会受伤,各位在训练的时候一定要注意安全。

Frame 3

学会如何投资

上面讲的几part都是自我价值的投资,这一part主要是想要分享一下我在股市中学到什么。

为什么要进行投资

对于我来说,这也是尝试一个新方向的机会,我想着把我以前学习的能力看看能不能迁移到做投资上。再来就是现在消息面实在是太多了,什么牛鬼蛇神都跑出来教人做投资,如果自己在这方面没有认知,那很容易吃亏的,所以与其被人带着跑,不如自己领跑。

10月的教训

10月那时候,天真的以为中概股会在沉寂几年之后会迎来新的机会,那时候我也受了很多情绪上面的感染,然后入股了一些中概,结果肯定是在不断追高之下输的很惨。后面也让我意识到了,在下行市场上,基本面没有变化的情况下,怎么挣扎力度是有限的。

后面我也去看了《巨债危机》,学习了一些周期方面的知识,让我体会到什么是周期。里面提到一点让我很受用,就是在经济下行的时候,政府会通过一些政策进行经济刺激,每次的刺激都会带来一些上涨,但是总体上来说是跌的,《巨债危机》里面例举了2008年美国的大萧条中,股市有六次大幅上涨,但是总共下跌了 89%。

image-20250126163244520

所以不要跟风,不要抛开基本面进行投资,其实大多数亏损都是在牛市中产生的。

不要被资讯所干扰

现在我们所处的世界,是信息太多了,连巴菲特也这么认为,所以他没有把公司设立在华尔街,他认为远离华尔街的喧嚣能够帮助他避免受到短期市场情绪和集体思维的干扰。在华尔街,投资者很容易陷入信息过载和盲从心理。

举个例子就是,我记得当时,我有个朋友在tesla发布了robotaxi之后很激动,以为这就是future,结果特斯拉第二天股价直接跳水十个点,并且持续低迷了一段时间。后来加上他又看了一些新闻,认为川普可能会选不上,所以就把tesla割肉抛掉了。

image-20250126164518304

这个事情也告诉我,不要被资讯所要干扰,如果当时真是相信马斯克的故事,认为他真的能做成,那就一直持有,否则就不要买入。

其实不只是股市,其他事情也是,现在社交媒体这么发达,很多时候看了一条新闻义愤填膺,然后过几天发现是 fake news,如果没有独立思考的能力就会被玩弄在鼓掌之中。

相信长期的力量

这件事情不只是反映在我对生活的态度,比如坚持写文章,坚持健身,学英语,到了投资这里也是一样的,巴菲特有句话说的好,如果你不愿意持有一只股票十年,那就不要考虑持有它十分钟。

一方面我们都是普通人,做短期很容易亏,看不准形式,并且短期中掺杂了很多噪音会影响判断,另一方面我也没这么多时间盯盘,整天看盘比较影响心情。

所以在过去一年我几乎所有的收益都是由长期持有的股票贡献的,反倒是因为一些小道消息跟风买的一些股票让我亏了一些。

弄了哪些有趣的玩意

游戏

其实整个24年让我印象深刻,并值得说的游戏并不多,玩的很多,但是能回忆起来的没有几个。

《寂静岭2》重制版:玩完《寂静岭2》真的给我一种淡淡的伤感,又是一个艺术与游戏结合的游戏,上次玩到这样的游戏还是 《最后生还者 1》。这个作品真的做到了久久的留存在我的脑海里,无论是背景音乐还是叙事手法,都趋近完美。

《暗黑4》:这游戏首发的时候我就发了600买了豪华版,玩了一阵之后感觉不太行就放下了。今年在第四赛季的时候风评好了不少,于是我又回归玩了一阵,总体来说还是不错的,但是暴雪总归是暴雪,在失望的路上从来不会让人失望,后面整出个300的DLC,我直接就没有太大的兴趣了,直接和暴雪say goodbye。

《流放之路2》:这游戏其实是免费游戏,只是现在还在内测,可以充30刀开启内测资格,还挺良心的,说实话,这游戏是真的做的不错,精良程度,游戏深度都做的不错;

买过哪些玩意

一加ACE3 pro

买这个手机是因为我很久没有体验过安卓机了,像搞个来看看目前主流安卓机有啥可玩的。

优点:

  1. 硬盘价格是真的便宜,16+512+8gen3只要3k多;
  2. 沙盒系统真好用,真的完全隔离,赖皮应用全丢到沙盒里,什么信息都获取不到;
  3. 侧滑返回真的爽,任何应用都可以左右侧滑返回,反观ios的垃圾返回操作万年不改;
  4. 小窗是很方便的,挂机打游戏,看视频回信息;
  5. 分屏很爽,有时候上下分两个屏处理信息快不少;
  6. 屏下指纹比faceid好用一万倍;
  7. 可玩性很高,可以玩各种 galgame ,switch模拟器,王国之泪也能在安卓机上玩;
  8. 100w充电真的很快,早上洗漱的时间,充好就可以拿走了;

缺点:

  1. 广告太多,虽然可以关,但是什么类似快应用这样的app仍然在后台收集数据;
  2. AI应用虽然好,但是打开后台应用的权限管理,可以看到AI的数据收集工作一刻不停,一天收集几百次,并且各种权限都要;
  3. 相册权限管理很差,适配的应用可以只给部分照片,没适配只能全给或者不给。像小红书这种,给了权限,几乎一天要读几十次相册

iPhone16P:没错,最后我还是换到了苹果,其实也没有什么特别的原因,感觉能满足我的需求,不需要折腾,没广告,偶尔去旅旅游录个视频效果也还挺好,那就足够了。没有买pro max是因为觉得太重了,太撑手了,现在市面上手机一个比一个宽,一个比一个大,感觉都要双手捧着才能用了;

iPad mini7:这个平板应该是我使用频率最高的电子产品之一,可以用来看电子书,看视频,玩游戏,串流,因为小巧轻便有时候还可以用笔记一点东西,由于很便携,我基本走哪里都带上;

macbook air m3:这台电脑是我五一的时候去冲绳买的,那时候日元汇率很不错,然后就入手了。它目前来看基本上能满足所有的日常使用需求,并且还非常的轻薄,后面出去玩再也不用带着我16寸的MacBook pro了,瞬间解放我的背包;

xiaomi buds5:这个产品刚使用的时候确实很惊艳,因为一个半入耳式的耳机竟然可以有不错的降噪效果,并且音效我听起来也很棒,感觉比我原来的 airpods2 好太多了。

去过哪些地方

北海道之行

平时雪都很少见的我,去小樽滑的第一场雪,我觉得是非常好玩的,一开始的时候我甚至都不能起来,后来还是放弃了,找了个教练,教练竟然是台湾人,带我练了几个小时后我基本上就可以自己入门了。

Frame 4

我去小樽的那天,他们也在搞灯节,还挺漂亮的,旁边有工作人员,可以100日元买个小杯子,里面有个小蜡烛,我写的是:for tomorrow

Frame 5

漫步在小樽的街上你可以感受到,他们的生活过的很悠闲,下午3点就没什么人了,以至于想找个吃饭的地方都没几个店。

Frame 6

很有意思的一点是,悠闲的逛公园的时候,你会发现很多牌子,告诉你小心乌鸦,因为一不小心你买的饼就没了。。。

Frame 7 (3)

冲绳

然后就是五一的时候去了冲绳,那边的沙滩还挺美的。

Frame 8

冲绳的海水真的好清澈,第一次看到玻璃一样的海水。

Frame 9

冲绳还有必去海洋馆。

Frame 10

总体来说,我觉得冲绳的旅游体验很好的,有点不太好的是交通不是很方便,经常需要打车。

亚庇

亚庇这个小城市,还挺有意思的,好几面墙上有巨大的壁画,可惜我去的是十月,天气不是很好,总是在下雨。

Frame 11

亚庇的主要旅游景点也是去各种小岛上面玩,但是我感觉其实体验很差,比如我去的这个环滩岛,据说是最好的岛之一了,但是各种服务还是很差,吃的也不好,洗浴间排队很长时间。所谓的游玩项目也就是浮潜一个多小时,然后回岛上吃个饭,然后就回去了。

并且浮潜如果没有自己带装备的话,他们提供的装备是真的差,并且漏水,然后一个一次性嘴塞10马币。

Frame 12

亚庇的消费其实挺高的,随便点个菜也要几十,并且当地的菜都是类似糊糊一样的东西,我是吃不太习惯,最好吃的感觉还是当地的华人菜馆。

然后就是亚庇的榴莲是真的好吃,我基本上每天都会去开两个榴莲吃,不必吃什么猫山王,就本地的白榴莲就可以了,一个就十几马币。

Frame 13

总结

总体来说,旅游体验来说:北海道>冲绳>亚庇,但是价格来说,也是北海道>冲绳>亚庇。

新的展望

技术上,我希望能学点不一样的东西,最近接触到了一点 ue 相关的开发,希望能在新的一年往这方面点一下技能树。

然后就是希望能更加深度挖掘一下已经掌握的技能,不是说学了就能放着不管,时间久了很慢慢遗忘,没什么想学的时候多问问自己哪些掌握了,哪些没有,比如:c++是真的熟悉了吗?c++17的特性有哪些?等等。

对于英语,还是希望能坚持,语言性的东西要学起来其实是需要漫长的过程,不是一朝一夕就可以练成,不过现在很多 AI 工具已经大大的简化了学习流程了,我觉得掌握起来不算太难,还是继续每天花至少半个小时多多练习比较重要。

对于健身,除了身形以外,更加希望是朝着健康的方向发展,更加需要注意怎样不让自己受伤的情况下,承受最大的重量,然后把身体锻炼好,适当的加入有氧运动,而不是全都是无氧运动,这样更能激活自己身体机能。

2024年总结:沉寂积蓄新的力量最先出现在luozhiyun`s Blog

C++ 中到底什么是”&&“ ?

2024-12-26 18:58:57

最近《Effective Modern C++ 》看到了第五章,感觉这章挺有趣的,所以单独拿出来总结一下,主要是想对通用引用和右值引用相关的东西总结补充一下,感兴趣的不妨看看。

区分通用引用和右值引用

T&&”有两种不同的意思。第一种,当然是右值引用。“T&&”的另一种意思是,它既可以是右值引用,也可以是左值引用,被称为通用引用universal references)。

比如下面的例子中

void f(Widget&& param);             //右值引用
Widget&& var1 = Widget();           //右值引用
auto&& var2 = var1;                 //通用引用,auto&& 是通用引用,它的类型根据 var1 的类型来推导,var1 是一个右值引用,var2 也是一个右值引用,绑定到 var1。

template<typename T>
void f(std::vector<T>&& param);     //右值引用

template<typename T>
void f(T&& param);                  //通用引用            

上面的例子中:

auto&& var2 = var1;这是一个 通用引用(完美转发引用)。

  • auto&&通用引用,它的类型根据 var1 的类型来推导。
  • 由于 var1 是一个右值引用,auto&& 会推导为 Widget&&。因此,var2 也是一个右值引用,绑定到 var1

template<typename T> void f(T&& param);这是一个 通用引用(完美转发引用)。

  • T&& param 是一个 通用引用,它的类型根据传递给模板函数的实际类型来推导。
  • 如果传入的是右值,param 会被推导为右值引用 (T&&)。
  • 如果传入的是左值,param 会被推导为左值引用 (T&),这是通过引用折叠规则完成的。

对一个通用引用而言,类型推导是必要的,它必须恰好为“T&&”。下面几种情况都可能使一个引用失去成为通用引用的资格。

template<typename T> void f(std::vector<T>&& param);这是一个 右值引用

  • std::vector<T>&& param 是一个 右值引用,它接受一个 std::vector<T> 类型的右值。
  • 函数 f 只能接受右值类型的参数。

除此之外,即使一个简单的const修饰符的出现,也足以使一个引用失去成为通用引用的资格:

template <typename T>
void f(const T&& param);        //param是一个右值引用

对右值引用使用std::move,对通用引用使用std::forward

std::move 是一个类型转换工具,它将左值转换为右值引用,允许通过移动语义来避免不必要的对象复制。比如有时候我们希望将其资源转移给另一个对象时,你应该使用 std::move。这样可以避免复制操作,提高效率:

std::vector<int> createVector() {
    std::vector<int> v = {1, 2, 3};
    return std::move(v);  // 使用 std::move 防止复制,转移资源
}

std::forward 它通常用于完美转发(perfect forwarding)的场景中,保留了传入参数的值类别(即,是否是左值或右值)。这使得它特别适用于在函数模板中转发参数,而不改变它们的值类别。

完美转发:指的是保持参数的左值或右值性质,以便在转发时能够选择正确的构造(复制或移动)。比如下面:

template <typename T>
void wrapper(T&& arg) {
    // 使用 std::forward 保留 T&& 参数的左值或右值性质
    someFunction(std::forward<T>(arg));
}

在这个例子中,std::forward<T>(arg) 将确保传入的参数 arg 被正确地转发到 someFunction,而不引入不必要的复制或移动。

所以 move 和 forward 使用场景是不一样的,如果在通用引用上使用std::move,这可能会意外改变左值:

class Widget {
public:
    template<typename T>
    void setName(T&& newName)       //通用引用可以编译,
    { name = std::move(newName); }  //但是代码太太太差了!
    …

private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

std::string getWidgetName();        //工厂函数

Widget w;

auto n = getWidgetName();           //n是局部变量

w.setName(n);                       //把n移动进w!

…                                   //现在n的值未知

上面的例子,局部变量n被传递给w.setNamen的值被移动进w.name,调用setName返回时n最终变为未定义的值。

所以对于 move 和 forward 我们尽量不要用错,但是也不要不用,否则可能会有性能上的损失,比如下面:

Matrix                              //按值返回
operator+(Matrix&& lhs, const Matrix& rhs)
{
    lhs += rhs;
    return std::move(lhs);          //移动lhs到返回值中
}

如果在最后 return的时候直接返回 lhs,那么其实是拷贝lhs到返回值中,拷贝比移动效率低一些。

forward情况也类似,如下:

template<typename T>
Fraction                            //按值返回
reduceAndCopy(T&& frac)             //通用引用的形参
{
    frac.reduce();
    return std::forward<T>(frac);     //移动右值,或拷贝左值到返回值中
}

如果std::forward被忽略,frac就被无条件复制到reduceAndCopy的返回值内存空间。

最后提一下,由于 RVO 的存在,我们也不要以为使用 move 操作就可以提升性能,RVO 主要解决的是在函数返回一个局部对象时,编译器可能会为该对象创建一个临时副本,然后将其复制到返回值中,从而引入了不必要的复制开销。

所以当我们想要“优化”代码,把“拷贝”变为移动:

Widget makeWidget()                 //makeWidget的移动版本
{
    Widget w;
    …
    return std::move(w);            //移动w到返回值中(不要这样做!)
}

通过返回 std::move(w),你实际上告诉编译器“我想要移动这个对象”,从而避免了 RVO 的优化,可能会导致不必要的资源转移。

所以你看,在c++中,为了做性能优化其实是一件挺复杂的事情。

避免在通用引用上重载

为什么要避免使用

通用引用可以绑定到几乎所有类型的实参,包括左值和右值。这种特性使得编译器在选择重载时可能会优先选择通用引用版本,即使这并不是开发者所期望的行为。并且由于通用引用的匹配规则,编译器在解析重载时可能会产生意外的结果。举个例子,假设有以下两个函数:

template<typename T>
void foo(T&& t) { /* 通用引用 */ }

void foo(int& t) { /* 左值引用 */ }

当调用foo(5)时,编译器会选择第一个函数,因为5是一个右值,而对于左值int a; foo(a);则会选择第二个函数。

但是假如将short a; foo(a);则会选择第一个函数。有两个重载的foo。使用通用引用的那个推导出T的类型是short,因此可以精确匹配。对于int类型参数的重载也可以在short类型提升后匹配成功。根据正常的重载解决规则,精确匹配优先于类型提升的匹配,所以被调用的是通用引用的重载。

如果想要使用重载该怎么办

最简单的方式当然是放弃了,比如 go 里面就没有重载,比如我们重载了logAndAdd函数,那么可以换成logAndAddName和logAndAddNameIdx来避免重载。

第二种方法就是将通用引用改写成 const T&,因为通用引用带来的复杂推导、重载歧义等问题反而会增加维护成本。此时,直接使用const 的左值引用即可满足需求。

改写之后 const T& 可以绑定到左值和右值,并且所有实参都被看作不可修改的“左值”,所以无需区分值类别。

比如原始版本(使用通用引用):

#include <utility> // std::forward

template<typename T>
void process(T&& param) {
    // 这里可能会做完美转发
    someFunction(std::forward<T>(param));
}

改用 const T& 后:

template<typename T>
void process(const T& param) {
    // 现在 param 总是 const 引用,不再做完美转发
    // 如果只是读 param 或传入其他函数当 const 引用使用就可以
    someFunction(param); // 直接传参即可
}

这种转变缺点是效率不高,因为放弃了移动语义,但是为了避免通用引用重载歧义、易读性优先,所以放弃一些效率来确保行为正确简单可能也是一种不错的折中。

再来就是使用tag dispatch,比如下面我们有两个重载函数,但是由于通用引用的存在,会发生引用重载歧义,所以我们想要正确的让函数实现最优匹配,可以使用std::is_integral 是一个类型特征(type trait),用来在编译期判定某个类型是否是整型

template<typename T>
void logAndAdd(T&& name)
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

void logAndAdd(int idx)             //新的重载
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(nameFromIdx(idx));
}

比如我们可以改写成这样:

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(
        std::forward<T>(name),
        std::is_integral<typename std::remove_reference<T>::type>()
    );
}

template<typename T>                            //非整型实参:添加到全局数据结构中
void logAndAddImpl(T&& name, std::false_type)    
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

std::string nameFromIdx(int idx);           //与条款26一样,整型实参:查找名字并用它调用logAndAdd
void logAndAddImpl(int idx, std::true_type) //译者注:高亮std::true_type
{
  logAndAdd(nameFromIdx(idx)); 
}

logAndAdd传递一个布尔值给logAndAddImpl表明是否传入了一个整型类型,通过 std::true_typestd::false_type 来判断应该调用哪个函数。上面的例子中还用到了std::remove_reference,它的作用是去掉类型中的引用修饰符得到正确的类型:若 TU&U&&,则 std::remove_reference<T>::type 得到 U;如果 T 本身不是引用类型,则结果还是 T 本身。

在这个设计中,类型std::true_typestd::false_type是“标签”(tag),在logAndAdd内部将重载实现函数的调用“分发”(dispatch)给正确的重载。

再来就是基于 std::enable_if 的重载,如下实现了两个版本的重载函数:

// ----------------------
// (A) 针对整型参数的重载
// ----------------------
template<
    typename T,
    // 使用 std::enable_if 使该重载仅在 T 是整型时有效
    typename = std::enable_if_t<std::is_integral_v<std::remove_reference_t<T>>>
>
void logAndAdd(T&& idx)
{
    cout << "for int" << endl;
}

// ------------------------
// (B) 针对非整型参数的重载
// ------------------------
template<
    typename T,
    // 使用 std::enable_if 使该重载仅在 T 不是整型时有效
    typename = std::enable_if_t<!std::is_integral_v<std::remove_reference_t<T>>>
>
void logAndAdd(T&& name)
{
    cout << "for universal reference" << endl;
}

int main()
{
    logAndAdd("Alice");             // 非整型,调用重载 (B)
    std::string bob = "Bob";
    logAndAdd(bob);                 // 非整型,调用重载 (B)

    logAndAdd(42);                   // 整型,调用重载 (A)  

    return 0;
}

通过 SFINAE 条件:

std::enable_if_t<std::is_integral_v<std::remove_reference_t<T>>>

如果形参是整型类型(如 int, long,无论左值还是右值),匹配这个重载。否则就调用另外一个。

关于引用折叠reference collapsing

在C++中引用的引用是非法的,比如下面的写法,编译器会报错:

int x;
…
auto& & rx = x; //error: 'rx' declared as a reference to a reference

但是我们上面用了很多这样的例子:

template<typename T>
void func(T&& param);       //同之前一样

Widget w;                   //一个变量(左值)
func(w);                                    //用左值调用func;T被推导为Widget&

它并没有因为被推导成了 Widget& && param编译器报错。因为编译器会通过引用折叠把T推导的类型带入模板变成Widget& param,也就是说这时候 func 里面传入的是一个左值引用。

存在引用折叠是为了适配“完美转发”这种灵活的泛型编程需求。在模板中使用如 T&&(或者 auto&&)这类通用引用的时候,我们把左值传给 T&& 时,需要推导出的类型为 T&(左值引用);把一个右值传给 T&& 时,需要推导出的类型为 T&&(右值引用)。

由于这个推导会自动在类型上再套一层引用,所以不可避免会产生 T& &T&& &T& &&T&& && 这类“引用的引用”。若没有引用折叠规则,这些“引用的引用”将无法在语言中被直接表示。

C++ 标准中定义的引用折叠规则可总结为以下四条(这里的 & 代表左值引用,&& 代表右值引用):

  1. T& & 折叠为 T&
  2. T& && 折叠为 T&
  3. T&& & 折叠为 T&
  4. T&& && 折叠为 T&&

其中可以看出,只要有一个左值引用(&)参与,就会最终折叠成左值引用;只有当纯右值引用(T&& &&)相叠时,才会保留为右值引用。

可以用简单的测试代码来查看引用折叠的结果:

#include <type_traits>
#include <iostream>
#include <string>

template <typename T>
void testReference(T&& x) {
    using XType = decltype(x); // 注意这里的 x 是函数形参
    // 现在 XType 才是真正的形参类型,比如 int&、int&&

    std::cout << "XType is "
              << (std::is_reference<XType>::value ? "reference " : "non-reference ")
              << (std::is_lvalue_reference<XType>::value ? "&" : "")
              << (std::is_rvalue_reference<XType>::value ? "&&" : "")
              << std::endl;
}

int main() {
    int i = 0;
    const int ci = 0;
    // 1) 左值 int
    testReference(i);       // T 推断为 int&,故 T&& 折叠为 int&
    // 2) 右值 int
    testReference(10);      // T 推断为 int,  故 T&& 折叠为 int&&
    // 3) 左值 const int
    testReference(ci);      // T 推断为 const int&, T&& -> const int&
    // 4) 右值 const int
    testReference(std::move(ci)); // T 推断为 const int, T&& -> const int&&
    return 0;
}

完美转发失效的情况

花括号

比如我们有 fwd 的函数,利用fwd模板,接受任何类型的实参,并转发得到的任何东西。

template<typename... Ts>
void fwd(Ts&&... params)            //接受任何实参
{
    f(std::forward<Ts>(params)...); //转发给f
}

假定f这样声明:

void f(const std::vector<int>& v);

在这个例子中,用花括号初始化调用f通过编译,

f({ 1, 2, 3 });         //可以,“{1, 2, 3}”隐式转换为std::vector<int>

但是传递相同的列表初始化给fwd不能编译

fwd({ 1, 2, 3 });       //错误!不能编译

因为对f的直接调用(例如f({ 1, 2, 3 })),编译器看看调用地传入的实参,看看f声明的形参类型。它们把调用地的实参和声明的实参进行比较,看看是否匹配,并且必要时执行隐式转换操作使得调用成功。但是 fwd 是个模版,所以不能这样调用。

0或者NULL作为空指针

传递0或者NULL作为空指针给模板时,类型推导会出错,会把传来的实参推导为一个整型类型(典型情况为int)而不是指针类型。结果就是不管是0还是NULL都不能作为空指针被完美转发。解决方法非常简单,传一个nullptr而不是0或者NULL

仅有声明的整型static const数据成员

当一个编译时常量被用作 纯值,如传递给函数、用于模板参数等,编译器可能不会为其分配实际的存储空间,只在需要时进行内联优化。这意味着这些常量没有独立的地址可供引用。

然而,当你尝试取这些常量的地址时,编译器需要一个实际的存储位置。如果该常量没有被定义在某个存储位置(例如,静态成员变量在类外未定义),链接器将找不到该符号,从而产生链接错误。

比如在类中定义整型static const数据成员:

class Widget {
public:
    static const std::size_t MinVals = 28;  //MinVal的声明
    …
};

想象下ffwd要转发实参给它的那个函数)这样声明:

void f(std::size_t val);

我们尝试通过fwd调用f会报错:

fwd(Widget::MinVals);       //ld: symbol(s) not found for architecture arm64
c++: error: linker command failed with exit code 1

要能够安全地取编译时常量的地址,需要确保这些常量有实际的存储空间。一种方式是提供类外定义:

#include <cstddef>

class Widget {
public:
    static const std::size_t MinVals = 28; // 类内声明和初始化
};

// 类外定义
const std::size_t Widget::MinVals;

void f(std::size_t val) {}

template<typename T>
void fwd(T&& param) {
    f(std::forward<T>(param));
}

int main() {
    fwd(Widget::MinVals);
    const std::size_t* ptr = &Widget::MinVals; // 现在可以安全取地址
}

还有就是从 C++11 开始,使用 constexpr 可以在一定条件下避免需要类外定义,因为 constexpr 变量默认具有内联性质,编译器会为其分配存储空间:

#include <cstddef>

class Widget {
public:
    static constexpr std::size_t MinVals = 28; // 使用 constexpr
};

再来就是从 C++17 开始,可以使用 inline 关键字:

class Widget {
public:
    inline static const std::size_t MinVals = 28; // 使用 inline
};

重载函数

如果我们试图使用函数模板转发一个有重载的函数,也是会报错的,译器不知道哪个函数应被传递。如下f被定义为可以传递函数指针:

void f(int (*pf)(int));

现在假设我们有了一个重载函数,processVal

int processVal(int value);
int processVal(int value, int priority);

报错:

fwd(processVal);                    //错误!那个processVal?

但是我们可以给函数重命名来解决这个问题:

using ProcessFuncType =                          
    int (*)(int);

ProcessFuncType processValPtr = processVal;     //指定所需的processVal签名

fwd(processValPtr);                             //可以
fwd(static_cast<ProcessFuncType>(workOnVal));   //也可以

Reference

《Effective Modern C++ 》

C++ 中到底什么是”&&“ ?最先出现在luozhiyun`s Blog

Yolov5物体识别与应用

2024-09-29 10:24:37

本篇文章只用于技术分享,用dnfm作为例子来展示应用,不提供完整源码。大家只讨论技术就好了,dnf检测还是很强的,我被封了好几次了。。。

我们先来看看效果怎样,大家可以二倍速观看,我视频里没有快进:

去年的时候写过两篇文章,如何学习强化学习,以及如果用AI玩 FlappyBird:

[长文]写给开发同学AI强化学习入门指南

如何用 PPO 算法让 AI 学会玩 FlappyBird

最近后开始玩DNFM,但是经过了几个月的搬砖实在是有点乏了,那么我们怎么用AI来代替我们在DNFM里面搬砖呢?

我们知道AI在游戏领域其实有很多的应用了,比如 MaaAssistantArknights:明日方舟游戏助手。因为它的最主要的功能都是静态的,并且位置固定,不存在需要移动的场景,那么基于静态的图像识别技术 OpenCV,就实现一键完成明日方舟游戏的全部日常任务。

那么 DNFM 的搬砖 AI 应该要用什么算法来做呢?首先要分析一下在 DNFM 手游中搬砖这个任务包含了哪些行为:

  1. 需要识别到我们人物角色的位置,以及怪物的位置;
  2. 移动到怪物的位置,释放普通攻击或者技能攻击;
  3. 因为地图中还有些假的怪物躺着不能被攻击,也不会掉落,所以需要识别这部分怪物,避免无效攻击;
  4. 完事了,还需要捡起怪物掉落的碳(我们的砖);
  5. 清完这个图之后要能认识什么是门,并且能够进入到下一张地图;
  6. 还需要识别狮子头这个怪物,它会掉落大量的碳;
  7. boss房打完之后,还需要自动进行下一局,直到消耗完所有疲劳;

从上面的分析,我们知道,要完成这个任务其实远比我们想象中要复杂,并且上面即使实现了,也只是半自动,还有多角色自动切换,角色自动移动到指定地图等等。

那么通过上面的“需求”,我们应该大体知道,对于静态图标文字之类的,我们可以使用 OpenCV 来解决,因为这些东西的相对位置不会变动,比如识别是否应该进入下一张图,我们直接识别这张图片即可:

again

其他动态信息都需要用到深度学习来实现,识别什么是怪物,什么是角色,什么是物品等等。这类的算法有很多,在 《动手学深度学习》 中第七章和第八章里面就讲到了如何用卷积神经网络 CNN 来进行图像识别。我们这次也是要使用大名鼎鼎的 YOLO 算法来实现我们的动态的图像识别。

那么除了图像识别以外,接下来需要解决如何玩游戏的问题了,那么就需要对手机进行控制,这类的解决方式有很多,但是对于DNFM来说,游戏里面是有反作弊系统的,所以要在不修改数据包,不root手机的情况下完成这个任务。我的解决方案是用 ADB 连接电脑,然后通过软件映射的方式来在电脑上控制手机玩游戏,几乎不需要任何权限,只需要一台安卓机即可。

好了,分析完之后来总结一下,我们的技术实现方案:

  1. 用 OpenCV 实现静态图像识别;
  2. 用 YOLO 实现动态图像识别;
  3. 用 ADB 控制手机实现角色控制。

下面我们进行挨个的技术爆破。

YOLO 算法

YOLO算法是one-stage目标检测算法最典型的代表,其基于深度神经网络进行对象的识别和定位,运行速度很快,可以用于实时系统。one-stage 直接从图像中生成类别和边界框位置预测,即网络一次性完成目标位置预测和分类任务,这个特性正是符合我们实时的游戏操作。

相对来说 two-stage 它是分成两步的,需要把任务细化为目标定位目标识别两个任务,简单来说,找到图片中存在某个对象的区域,然后识别出该区域的具体对象是什么。这种算法的缺点是识别比较慢,但是小物体检测好,精度高,这类的代表算法有 RCNN 系列。

我们可以简单对比一下 RCNN 和 YOLO 的区别,YOLO的mAP要低于Fast-rcnn,但是FPS却远高于Fast-rcnn:

image-20240920115738762

而我们只是用来玩一个游戏, 不需要这么高的精度,实时性更重要,这也是为什么选用YOLO算法。

说点题外话,由于YOLO算法的实时性和准确性,所以YOLO也被用于一些军事领域上,YOLO算法的原作者Joseph Redmon于2020年宣布退出计算机视觉领域,他在社交媒体上表示,他不想看到自己的工作被用于可能造成伤害的用途,因此选择退出这一研究领域。

基本原理

现在 YOLO 已经出到了 V9 版本,前3个版本是Joseph Redmon开发的,先来看看 V1 版的论文。

YOLO将输入图像划分为 S×S 网格,如果物体的中心落入网格单元,则该网格单元负责检测该物体。我们以下面 7×7 的格子为例:

image-20240920145926089

具体实现过程如下:

  • YOLO首先将图像分为7×7的格子。如果一个目标的中心落入格子,该格子就负责检测该目标。每一个网格中预测B个box 和置信值(confidence score)。这些置信度分数反映了该模型对盒子是否包含目标的信心,以及它预测盒子的准确程度,如果没有目标,置信值为零;
  • 每一个box包含5个值:x,y,w,h和confidence score,(x,y)坐标表示边界框相对于网格单元边界框的中心,w 宽度和 h 高度是相对于整张图像预测的;
  • 每个网格还要预测一个类别信息,记为 C 个类,比如上图就要预测到狗,自行车,汽车;
  • 在得到所有边界框和类概率后,应用非算法来消除重叠的边界框,保留具有最高置信度分数的框,并去除与其重叠度超过设定阈值的其他框,从而减少冗余检测结果;
  • 输出最终的边界框、类别标签和置信度分数;

局限性

因为每个网格单元只会预测两个boxes,然后从中选出最高的IOU的box作为结果,也就是最终一个网格只能预测一个物体,那么这种空间约束限制了YOLO模型可以预测的附近对象的数量。如果要预测的多个物体小于网格的大小,那么将识别不出来,比如远处的鸟群。

还有就是由于输出层为全连接层,因此在检测时,YOLO 训练模型只支持与训练图像相同的输入分辨率,yolo-v1的输入是448×448×3的彩色图片。其它分辨率需要缩放成此固定分辨率;

YOLO训练部署实战

这里我选用Ultralytics的 YOLOv5 版本 https://github.com/ultralytics/yolov5 来完成本次任务,一来因为是需要快速响应的场景,并且游戏场景里面也没有很小的物体或密集的场景需要识别。并且提供多个模型尺寸(如s、m、l、x),适应不同的应用场景。活跃的社区和易用性,也适合初学者和快速开发。

对于一个YOLOv5 这样的监督学习框架,要使用总共要经历以下这么几步:

  1. 给图片打标签,这里我们选用 Label Studio 来做;
  2. 训练;
  3. 测试验证;
  4. 导出部署,这里我们使用 ONNX 来部署;

标签

  1. 安装

    pip install label-studio
  2. 启动

    label-studio start

然后就可以在浏览器访问 http://localhost:8080 打开打标签的界面了。然后我们需要打开游戏,玩一局并录像,然后对视频进行抽帧生成游戏内的图片,代码如下:

def main(source: str, s: int = 60) -> None:
    """
    :param source:  视频文件
    :param s:       抽帧间隔, 默认每隔60帧保存一帧
    :return:
    """
    video = cv2.VideoCapture(source)
    frame_num = 0
    success, frame = video.read()
    while success:
        if frame_num % s == 0:
            cv2.imwrite(f"./images/{frame_num // s}.png", frame)
        success, frame = video.read()
        frame_num += 1
    video.release()
    cv2.destroyAllWindows()

setting 设置 box 检测任务:

image-20240920191733738

设置label:

我们这里主要标记这么几个物体:

'Gate' # 门, 'Hero' # 玩家人物, 'Item' # 掉落物品, 'Mark' # 箭头标记, 'Monster' # 怪物, 'Monster_Fake' # 怪物尸体 

image-20240920191827549

然后将我们抽好帧的图片 import 进入到工程里面。

然后就一张张的图片进行手动的标记:

image-20240920192147025

这里你有可能要问了,如果我的角色换了一套衣服,那我的模型是不是就不认识了呀。确实是这样,会不认识,并且不同角色要多次标记,每个角色差不多标记个50张图片就好了(这个过程真累啊)。

导出为YOLO:

image-20240917004920301

预测

到这里之后,我们进入到YOLOv5 https://github.com/ultralytics/yolov5 工程里面,我们先在 data 目录下面创建 mydata.yaml,配置好要训练的数据集:

# 训练和验证的数据集
train: ../train_data/project-1-at-2024-09-17-00-48-5c375b91/images
val: ../train_data/project-1-at-2024-09-17-00-48-5c375b91/images

# number of classes 表示有多少分类,等于 names 数量就行了
nc: 6

# class names 分类的名字
names: ['Gate', 'Hero','Item','Mark','Monster','Monster_Fake']

然后打开 models 里面的 yolov5s.yaml 文件,只需要把 nc 改成 6就好了。

然后直接执行训练:

python train.py --epochs 100 --batch 8 --data data/mydata.yaml --cfg yolov5s.yaml --weights yolov5s.pt --device 0 

epochs 和 batch 会影响最后的收敛效果以及速度,根据自己的显卡来调试就好了,我的4090 100张图片大概训练了10分钟左右。

检验

训练完之后可以用我们刚刚录好的视频做验证:

python detect.py  --source D:\document\dnfm-auto\video\Record_2024-09-14-21-29-31.mp4 --weights best.pt

我这里把我跑完的视频上传了,可以看到即使是 YOLOv5s 效果也足够好了:

导出模型

然后我们把 pt 模型导出成 onnx模型:

python export.py --weights best.pt --img 640 --batch 1 --device 0 --include onnx

主要因为使用ONNX(Open Neural Network Exchange)来部署模型相比于直接使用PyTorch(.pt格式)有几个显著的优点:

  1. ONNX提供了一个开放标准,允许在不同的深度学习框架之间共享和转移模型;
  2. 推理时通常比原始PyTorch模型更快,尤其是在专用硬件(如GPU、TPU等)上;
  3. 通过使用ONNX,可以简化模型部署流程;

部署

对于 ONNX 的部署我们使用 ONNXRuntime 来进行,它几乎可以在不修改的源码的基础上进行部署它的整个架构就像Java的JVM机制一样。具体可以参考onnxruntime.ai的具体介绍。

Python部署yolov5模型几乎就是参照了源码的流程,主要分为以下几步:

  1. 图片前处理阶段
  2. 模型推理
  3. 推理结果后处理

具体,我们可以参考这个项目 https://github.com/iwanggp/yolov5_onnxruntime_deploy 把推理的 demo 给写出来,然后尝试导入图片看是不是能生成这样的 anchor 图片:

image-20240920194257700

对于上面的的图片检测算法最后可以为每个anchor是可以生成:[centerX,centerY,width,height,label,BoxConfidence ],分别表示中心点坐标,宽和高,标签索引,置信度,我们只需要前5个数据即可。

最后

这个项目做的过程并不是这么轻松,本来想要让它代替我肝游戏的,但是目前来看只完成了第一版就懒得再动了,当然能够自动打完全图还是激动的,我也曾想向 MaaAssistantArknights 这个明日方舟工具一样开源,让大家一起来共建,但是感觉避免不了会有黑产影响dnfm项目组业绩,还是作罢。

勇士在怎样热爱这个游戏终究不是年轻时的勇士,没时间继续迭代更新这个工程,也没时间继续每天花1小时自己肝,让我觉得我是时候该放下了。

Reference

https://blog.csdn.net/Deaohst/article/details/127835507

https://www.bilibili.com/video/BV1XW4y1a7f4/?spm_id_from=333.337.search-card.all.click&vd_source=f482469b15d60c5c26eb4833c6698cd5

https://github.com/iwanggp/yolov5_onnxruntime_deploy

https://github.com/luanshiyinyang/YOLO

https://arxiv.org/abs/1506.02640

https://www.datacamp.com/blog/yolo-object-detection-explained

Yolov5物体识别与应用最先出现在luozhiyun`s Blog

构建属于自己的云游戏服务器

2024-06-29 18:03:28

最近沉迷于暗黑4第四赛季,所以就在倒腾,怎样才能随时随地玩到暗黑4,掌机steam deck 我试过了,太重并且性能很差,已经被我卖了,于是折腾起了云游戏。

先来看看我的折腾成果:https://www.bilibili.com/video/BV1Z93TeuEQ4/

其实效果我没想到有这么好,在远程串流的情况,可以 1080p 60hz 几乎无卡顿的玩暗黑4,延迟只有20ms左右,配上我的手柄简直就是一个强大的掌机。

各种平台云游戏怎样了?

有了上面的需求之后,我就去试了以下几个平台:GeForce Now、Xbox Game Cloud、Start云游戏、网易云游戏。

但是遗憾的是,几乎每个平台都有自己的问题。首先上面列举到的所有平台,IOS 都只有网页版,因为苹果不让上架。

Start云游戏:我检查了一下,只有少数的几个单机游戏,大多是网游手游,并没有暗黑4;

网易云游戏:无论是快速启动还是普通启动,都非常的慢,估计要3分钟左右才能启动。并且我充值了一下玩了一下,网络倒是很流畅,延迟只有15ms左右,但是非常的卡,并且经常进入游戏界面死机,估计是机器性能不行,说实话我有点心疼我充值的10块钱了。

所谓的高配,其实性能很低:

image-20240623180606792

Xbox Game Cloud:微软自家的平台,好处是有了XGPU之后可以畅玩所有云游戏,最大的问题是服务器不在国内,所以连接延迟其实很高,并且常有波动,如果要玩Xbox Game Cloud 那么还需要充值XGPU才行;

Xbox Game Cloud 强大的游戏阵容:

image-20240623180512545

GeForce Now:这个云游戏平台可以说只能用无敌两个字来形容,每次登录都可以免费半个小时,即使服务器在海外,但是开了加速器也可以很稳定,画面有720P,在手机上玩还可以,主机性能也很好,经常分配到2080以上的机器。但是唯一不爽得是,免费用户每次都要排队挺长时间的,并且如果要付费,其实挺贵的,基本要100元每个月了。

GeForce Now价格表:

image-20240623180446219

开源解决方案

所以尝试了这么的云游戏发现都不好用之后,为什么不可以自己弄个呢?其实所谓的云游戏,无非是用客户端连接到主机端而已。那么我们实际上也可以把自己家里运行的PC或者主机,变成了由云游戏服务商提供的云上服务,这种行为就叫做串流游戏。

所以我们要做的是怎样把我们的私有网络的PC或者主机做成云服务提供给外网访问,让我们可以随时随地,只要有网就可以使用。

memory12

那么我的要求主要有这么几点:

  1. 要有跨平台的客户端,保证mac、iphone、android、win 都能用;
  2. 延迟要足够低,支持的可配置项要足够多;

正好自己有台闲置的 4090 的机器,那我就可以用它来作为主机端,我的安卓机作为客户端进行串流云游戏。

服务端 & 客户端

目前服务端主要有以下几个实现方案:

  1. N卡GeForce Experience
  2. Sunshine

客户端主要有:Moonlight

N卡GeForce Experience

如果你使用N卡,并且是GTX960以上可以通过GeForce Experience进行串流。只需要打开GeForce Experience在设置里找到SHELD这个串流配置,并添加游戏或者应用程序即可。

Featured image of post Nvidia Gamestream + Moonlight 如何串流桌面畫面

但是我现在不是很推荐这个方案,因为NV说过他们要把这个功能去掉,只是现在没有去掉而已。

Sunshine

它是一个开源推流方案 https://github.com/LizardByte/Sunshine , 属于通用串流方案,支持Nvidia、AMD、Intel。尤其适合核显串流(如果你用的是P106这类显卡没有视频编码器,只能使用Sunshine串流方案)。

下载Sunshine后首先需要运行服务端安装脚本install-service.bat,然后再运行sunshie。sunshine没有UI界面,设置需要通过网页端。运行sunshine后访问https://127.0.0.1:47990进行设置。设置里最重要的是进行PIN码配对,设备之间PIN匹配之后就可以进行串流了。

image-20230314210726196

Moonlight

Moonlight 以方便的将Windows电脑画面传输到各主流操作系统的客户端软件上,甚至可以直接传输至谷歌浏览器。画面方面,移动端最高支持4K120帧,且支持HDR(需要显卡支持),而桌面端甚至可以直接自定义分辨率和帧数;交互方面支持键鼠/手柄/触摸屏/触控板/触控笔,就像用自己的电脑一样使用远程电脑。该方案无广告,完全免费。

手机端可以在各大商店下载,也可以去 Moonlight官网地址:https://moonlight-stream.org 下载。如果使用iphone作为客户端,直接在App store下载Moonlight即可。

保持主机和客户端在同一局域网内,打开客户端软件,应该能够看到主机的计算机名。点击会弹出4位PIN码,需要在Sunshine配置网页 https://localhost:47990/pin 中输入PIN码。建立连接后,点击桌面(DESKTOP)将启动桌面串流。

网络配置好之后,在局域网内串流延迟通常相当的底,我经常躺床上用 pad 串流我书房的 pc 玩游戏,延迟只有几毫秒。

远程串流

由于Geforce Experience和Sunshine默认只在本地网络监听端口,客户端和主机位于同一局域网内才能连接成功,如果要真正实现远程连接,最简单稳定的方法是公网直连。

独一无二的IP地址使得主机能够在互联网中被识别,但是由于IPv4地址匮乏,大多数家庭网络并不具备公网IPv4地址。

所以我这里采用内网穿透的方式来构建我们的云服务:

Frame 2

内网穿透的核心思想就是“映射”和“转发”,把私有网络的设备的端口映射到公网设备的端口上,来进行流量转发。思想其实很简单,由于内网设备没有ip,那么我们通过一台有公网ip的机器来代替把流量做一层转发。比如上图,

我们在外网设置的用手机访问云服务器的 7000 端口,实际上云服务器会接收到之后通过47900进行转发到我们私有网络的pc机器,然后pc机器处理完之后再通过46900 端口转发给云服务器,上面所提到的端口都是可以自定义的。

那么对于做内网穿透一般现在流行两种做法:

  1. 直接 p2p 点对点的进行传输,流行的方案有 zerotier;
  2. 基于服务器的流量转发,流行的方案有 frp;

为什么会有内网穿透?

其实在互联网的世界中,如果每个用户都有真实的IP情况下,那么我们可以通过源IP+源端口+目标IP+目标端口+协议类型很容易的找到对方,是根本不需要P2P的,因为本来任何对象都可以作为Server或者Client来提供服务,彼此之间是可以互联。

但是IP和端口,是有限的,最初设计者也是没想到发展如此迅速,整个IPv4的地址范围,完全不够互联网设备来分配,那为了解决地址不够用的问题,就引入了NAT。

Frame 3

NAT(Net Address Translate,网络地址转换)是一种IP复用的一种技术,将有限的IP扩展成无限,由于IPv4地址资源有限,而NAT将网络划分成了公有网络和私有网络,允许多个设备使用一个公共IP地址访问互联网。路由器会将内部网络中的私有IP地址转换为公共IP地址,从而节省了IPv4地址资源。

所以我们在用 WIFI 的时候可以看到我们手机或PC上的IP地址通常是:192.168.x.xxx,这其实就是由路由器分配的地址,并不是真的地址。

另外,在 IPv4 地址资源越来越紧张的今天,很多电信运营商,已经不再为用户分配公网 IP;而是直接在运营商自己的路由器上运营 NAT,所以会出现甚至一整个小区共用一个 IP 出口的情况。

通过NAT技术的公私网络隔离,可以实现IP复用,解决了IPv4不够用的问题,但是也同时带来了新问题,那就是直接导致通信困难,由于NAT导致IP成为虚拟IP,外网无法针对内网某台主机进行直连通信,因为没有真实地址可用。

所以为了将NAT设备内外通信打通,就有了内网穿透技术。

zerotier

zerotier 是一个开源的内网穿透软件 https://github.com/zerotier/ZeroTierOne ,有社区版本和商业版本,唯一的区别是社区版本有 25台连接数量的限制,但对普通用户足够了,用它可以虚拟出一组网络,让节点之间的连接就像是在局域网内连接一样。

zerotier 底层是通过一个加密的p2p网络来实现连接。由于节点之间通常存在NAT隔离,无法直接通信,所以 zerotier 存在一个根服务器来帮助通路建设,所谓通路建设俗称打洞(hole punching),也就是穿透NAT隔离实现两个节点的连接。打洞也是区分 UDP 和 TCP 的,由于 zerotier 用的是 UDP,所以这里以 UDP 讲解打洞原理。

假设clientA 想要直接与clientB 建立UDP会话,用S表示根服务器:

A最初不知道如何到达B,因此A请求S帮助与B建立UDP会话,S会记录下他们各自的内外网IP端口:

Frame 4

打洞中:

S用包含B的内外网IP端口的消息回复A。同时,S使用其与B的UDP会话发送B包含A的内外网IP端口的连接请求消息。一旦收到这些消息,A和B就知道彼此的内外网IP端口;

当A从S接收到B内外网IP端口信息后,A始向这两个端点发送UDP数据包,并且A会自动锁定第一个给出响应的B的IP和端口;

B开始向A的内外网地址二元组发送UDP数据包,并且B会自动锁定第一个给出相应的A的IP和端口;

Frame 4

打洞后:

A和B直接利用内网地址通信

Frame 4

zerotier 的根服务实际上是部署在海外的,如果我们直接使用,很可能连不上,并且延迟基本在200ms以上,我们可以通过 zerotier-cli listpeers 查看根服务器:

# ./zerotier-cli listpeers
200 listpeers <ztaddr> <path> <latency> <version> <role>
200 listpeers 62f865ae71 50.7.252.138/9993;24574;69283 341 - PLANET
200 listpeers 778cde7190 103.195.103.66/9993;24574;69408 213 - PLANET  
200 listpeers cafe9efeb9 104.194.8.134/9993;4552;69462 159 - PLANET

上面的 PLANET 节点就是是ZeroTier网络中的根服务器。它们负责在对等点之间中继初始流量,帮助对等点建立对等连接,并充当身份和相关公钥的缓存。

我们随便 ping一下它的延迟:

# ping 50.7.252.138
PING 50.7.252.138 (50.7.252.138) 56(84) bytes of data.
64 bytes from 50.7.252.138: icmp_seq=1 ttl=46 time=347 ms
64 bytes from 50.7.252.138: icmp_seq=2 ttl=46 time=347 ms
64 bytes from 50.7.252.138: icmp_seq=3 ttl=46 time=354 ms

这样的游戏明显是玩不了云游戏的,zerotier 也考虑到这种延迟的情况,所以可以让有需要的用户自建 MOON 服务器。

ZeroTier中的MOON节点是用户定义的根服务器,可以添加到ZeroTier网络中。它的行为类似于ZeroTier的默认根服务器(称为PLANET节点),但由用户控制,我们可以把 MOON 服务器部署在离自己更近的地方,比如我就部署在广州,可这样以通过提供更近或更快的根服务器来提高网络性能。

怎样部署我这里就不贴教程了,可以自己去search一下,很简单。 但是现实场景中的网络要复杂的多,远远不是部署一个 MOON 节点就可以解决延迟的问题。通常我们的网络会涉及到防火墙限制、运营商级NAT、路由器兼容问题,还有就是 ZeroTier 走的是 UDP, 在国内的网络环境下一些运营商会对UDP流量实施QoS(服务质量)策略,,丢包可能会比较严重。

所以总之ZeroTier这条路并不是这么好走,看起来 p2p 直连貌似可以很美好,理论上可以不受根服务器的影响,两端直连跑满所有带宽,但实际上当不能打洞成功的时候那么就会退化成根服务器转发,那么实际的速率就取决于你自建的 MOON 节点的转发带宽了。

并且还有一个问题是,ZeroTier 是需要客户端的,到目前为止移动端的 app 是不支持添加自建 MOON 节点信息的,也就是说只能在电脑上进行串流,这实用性还是下降了不少。

所以总结一下优缺点:

优点:

  1. 组网非常方便,可以像局域网一样连接ZeroTier组网内的节点;
  2. 连接以及数据传输都是加密,所以比较安全;

缺点:

  1. 根服务器在海外,需要自建MOON,否则延迟很高;
  2. 依赖服务端,并且移动端app功能不完善;
  3. 受制于网络环境,p2p 打洞成功率低;

frp

frp 也是一个开源软件 https://github.com/fatedier/frp ,实际上它没有这么多花哨的功能,就是帮我们做了一个流量的转发。它的客户端连接不需要app,所以用来串流的话直接用moonlight直接连接frp远程转发服务器即可,可以说很方便了。

它的架构如下:

Frame 5

在安装frp远程转发服务的时候,我这里给一下配置,因为现在网上找的教程都是老的 ini 配置,现在新版本用的是 toml配置。

服务端的配置:

#frps服务监听的本机端口
bindPort = 9200
bindAddr = "0.0.0.0"
# frpc客户端连接鉴权token,默认为token模式
auth.token="xxxx" 

#日志打印配置
log.to = "./log"
log.level = "debug"
log.maxDays = 7

allowPorts = [
    // 远程连接需要用的端口
  { start = 47000, end = 48010 } 
]

sunshine主要连接的端口是这几个:

TCP 47984, 47989, 48010
UDP 47998, 47999, 48000, 48002, 48010

所以我们需要给这几个端口都加上防火墙,服务器和pc都要开放相应的端口,在测试的时候可以先全打开,测试完了再挨个加上,免得莫名其妙的问题。

pc端的配置:

#token需要与服务端的token一致
auth.token = "xxxxx"
# 服务端的公网ip
serverAddr = "1xx.xxx.xx.xx"
# 服务端的监听端口
serverPort = xxx

[[proxies]]
name = "47984"
type = "tcp"
localIP = "127.0.0.1"
localPort = 47984
remotePort = 47984

[[proxies]]
name = "47989"
type = "tcp"
localIP = "127.0.0.1"
localPort = 47989
remotePort = 47989

[[proxies]]
name = "47990"
type = "tcp"
localIP = "127.0.0.1"
localPort = 47990
remotePort = 47990

[[proxies]]
name = "48010"
type = "tcp"
localIP = "127.0.0.1"
localPort = 48010
remotePort = 48010

[[proxies]]
name = "47998"
type = "udp"
localIP = "127.0.0.1"
localPort = 47998
remotePort = 47998

[[proxies]]
name = "47999"
type = "udp"
localIP = "127.0.0.1"
localPort = 47999
remotePort = 47999

[[proxies]]
name = "48000"
type = "udp"
localIP = "127.0.0.1"
localPort = 48000
remotePort = 48000

[[proxies]]
name = "48002"
type = "udp"
localIP = "127.0.0.1"
localPort = 48002
remotePort = 48002

[[proxies]]
name = "48010"
type = "udp"
localIP = "127.0.0.1"
localPort = 48010
remotePort = 48010

pc端frp自动启动

windows系统开机自启比较麻烦,不像linux简单,所以为了保证windows后台运行 frpc,创建脚本 frpc.vbs,将以下内容粘贴进去:

set ws=WScript.CreateObject(“WScript.Shell”)
ws.Run “[frpc执行文件] -c [frpc配置]”,0

注意可能需要修改路径(默认路径是放C盘目录下)

将 frpc.vbs 放入 C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp 目录内,即可实现开机自启动。

远程唤醒(Wake On LAN)pc

家里的电脑如果经常开机的话很费电,所以按需开机是最佳办法,那么就需要远程登陆开机。远程唤醒需要主板的支持,现在的主板基本都支持。

首先我们要进入到主板的 BIOS 设置选项里面把 WOL 功能打开,具体方法视厂商而定,可以参考的关键词包括:

  • Automatic Power On
  • Wake on LAN/WLAN
  • Power Management
  • Power On by Onboard LAN
  • Power On by PCI-E Devices

然后在我们被唤醒的电脑里面找到网卡设置:

img

img

然后我们可以在内网尝试一下,是否可以唤醒成功,在应用市场随便找个WOL软件,填上内网被唤醒机器的IP地址和MAC地址即可:

img

外网唤醒,我们需要一个中间设备来中转我们的流量,因为我们需要被唤醒的机器已经被休眠了,是无法接收到请求的,所以我这里内网用我的软路由进行转发:

首先我们要做的就是 DHCP固定住自己内网PC的内网IP,要不然无法转发唤醒,通常可以在路由器里面设置:

image-20240629175131874

然后我是通过 OpenWrt 来和我远端服务器建立好 frp通信,监听转发端口,到时候外面的请求会先到 OpenWrt ,然后由它再转发给我的内网PC:

image-20240629175315251

最后如果觉得麻烦,其实可以用远程物理按键解决,一劳永逸:

image-20240626145854217

隐私屏 / 作为副屏

在用 sunlight 串流的时候由于显示的是桌面的,,因为串流软件会捕捉屏幕上的内容并编码成视频流。如果关闭屏幕,编码器将无法获取到需要的画面信息,导致串流中断。

那么如果我们想要关闭屏幕串流,那么可以用这个工具 https://github.com/VergilGao/vddswitcher ,通过 vdd 创建一个虚拟屏幕可以实现即使主屏关闭也能串流。

最后

游戏串流最后不仅满足了我在外网想要随时随地玩游戏的想法,并且还拯救了我的腰椎,在家里玩游戏现在基本是用平板串流到我的电脑上面,然后买个支架夹着我的平板,然后躺着玩,但愿各位游戏佬都能找到属于自己的游戏环境。

image-20240626152310201

Reference

https://github.com/VergilGao/vddswitcher

https://github.com/LizardByte/Sunshine

https://github.com/moonlight-stream/moonlight-qt

https://keenjin.github.io/2021/04/p2p/

https://sspai.com/post/68037

https://bford.info/pub/net/p2pnat/

扫码_搜索联合传播样式-白色版 1

构建属于自己的云游戏服务器最先出现在luozhiyun`s Blog