子恒 published on included in 架构 多大的服务 “宜小不宜微”,不宜过大的单体,也不宜滥用微服务,应保持在合适的大小。
(ps. 这里只讨论服务的大小。不讨论服务的手段,拆分解耦、服务发现、mesh、告警、限流、全链路建设,都是好的实践,任何服务都可以做的同样好)
什么样的服务大小属于合理的服务?核心有两点,合理划分服务边界,匹配组织结构。
团队视角:服务的规模应该尽量限制在最小团队,约3人左右的团队为宜 ❎ 过大!跨团队修改同一服务,风险大且不利于基础建设。没有人对整体的架构和质量负责。没有人有全局视角,写代码因为不了解改动范围容易出故障。 ❎ 过大!10人以上的团队不宜修改同一服务。很可能服务定位不清晰,10个人的信息交流会有很多gap,效率会大大降低 ❎ 过小!团队每个人管三四个服务,说明服务数量过多,无用工作会增加,效率也会降低 业务视角:保证产品需求变更如果只在本团队内,修改的服务1-2个即可 ❎ 过小!改一个小小的功能,牵扯4-5个服务。服务太小了,效率降低 ❎ 过大!同时有四五个业务需求,对应数十个研发同时修改一个服务,而这些功能看起来并不密切。这说明服务包揽的功能太多了,已经膨胀的太大了 更微=更好? 我们以一个模块划分合理、大小合适的单体服务为例,看看如果拆分的更微小,对比起来会有哪些点值得关注:
关注点 合理的单体 更微服务 性能 无劣化 约数ms的劣化 复用性 可以仓库内直接引用 不可复用其他仓库代码;所以也不会复用到bug 可读性 容易找代码 需要跨的仓库多,层次深,理解困难 研发成本 编译可能较慢 写代码、编译、调试、发布、部署都需要多仓库并行,成本高 部署成本 单机器 高,需要多台机器 代码合入 公共部分容易冲突 不容易冲突 微服务教 我们看下某网站对单体的批评,一一回应下:
开发效率低:所有的开发在一个项目改代码,递交代码相互等待,代码冲突不断 确实,代码冲突会变多。但在单体内部模块划分合理的情况下,冲突的只会是公共模块;如果这些代码冲突了,使用微服务也会冲突。 代码维护难:代码功能耦合在一起,新人不知道何从下手 耦合是模块设计的问题,不是单体的问题。而且单体断层更少,更容易阅读 部署不灵活:构建时间长,任何小修改必须重新构建整个项目,这个过程往往很长 没发现编译时间有显著区别。部分单体仓库会编译多个产物,建议针对性编译,效果更好。 稳定性不高:一个微不足道的小问题,可以导致整个应用挂掉 绝对意义上,微服务存在同样的问题,而且更难定位。相对意义上,如果说单体的复用代码带来了问题风险的扩大,这属于因噎废食,不可取。
微服务给我们带来了什么样的反思?拒绝盲目创新,提防吹牛皮但不切实际,小心滥用导致过犹不及。 再次回到起点:monorepo 有趣的是,越来越多人意识到了微服务的问题,最终又回归了单一的仓库,并发明了一新的词:monorepo。
Segment.com 提供活动收集和转发服务,每个客户都需要使用一种特殊格式的数据。因此,工程团队最初决定混合使用微服务和多代码库。这一策略效果很好——随着客户基数的增长,他们扩大了规模,没有出现问题。但是,当转发目的地的数量超过 100 个时,事情开始变得糟糕起来。维护、测试和部署超过 140 个代码库(每个代码库都有数百个日益分化的依赖关系)的管理负担太高了。最终,团队发现他们无法取得进展,三个全职工程师花费了大部分时间来维持系统的运行。
对于 Segment 来说,补救办法就是合并,将所有的服务和依赖迁移到一个单一代码库中。他们必须协调共享库并且测试所有内容,虽然花了很大的代价,但迁移非常成功,最终的结果是降低了复杂性,增加了可维护性。
每个团队的精力是有限的,当每个人管理的仓库>10个,团队管理的仓库>100个,会出现非常多的问题:
团队无法集中注意力管理。有很多的仓库大部分人并不知晓。这部分代码不再能被复用,也不再被了解 开发要打开很多IDE并发进行,容易出错。测试需要部署非常多的服务,也不能跨服务debug。发布时需要发布过多的仓库,发布容易遗漏;发布有很复杂的依赖,发布也会花费更久的时间。 无法发起整体性重构。重构一个服务和同时重构10个服务的难度不是一个级别。
子恒 published on included in 后台 幂等的作用 幂等的作用:保证可重入性,防止重复操作导致脏数据出现。使用场景可能有
前端防抖 接口超时重试 消息重试 只能调一次接口,如一个用户只能领一次券 我们的upsert、redis分布式锁、version乐观锁、for update、唯一索引、状态机其实都有幂等的功能,会在请求重复的时候报错
分布式锁:一般用来防止并发操作,可以处理重复操作的问题。可以加在请求处理的最开始 乐观锁:锁住读实体~写实体期间,这期间的任何其他操作都会失败。但自己也可能失败,要处理好失败的回滚逻辑 方案 支持维度 性能 幂等期 判断幂等时机 使用场景 备注 分布式锁 任意 高 加锁期间 请求开始 操作实体 乐观锁 实体id 低 读-写db期间 写db时 修改实体db行 失败需要回滚前面的操作 insert+uk 任意 低 写db之后 写db时 创建实体 失败需要回滚前面的操作 幂等键设计 结论:一定要用有业务语义的幂等键!最好的幂等键组合是entity_id+idem_key的组合
接口维度的幂等:幂等的控制交给上游,由上游保证自己的请求是可以幂等/不被幂等的。比如上游直接传一个md5sum(req)作为幂等键进来(其他常用的包括req里的核心参数、reqid、时间戳、消息id)。我们检测这个幂等键是否存在:
幂等键已存在:直接幂等。问题:下游可能传错了,不幂等的也结果被幂等,比如批量请求、同一个请求里发起多次请求等。幂等应该自己来控制 幂等键不存在:不幂等,这个不会出错 为了避免上面问题的出现,我们可以结合数据库的实体uk做判断。
数据维度的幂等:采用数据库的带业务语义的uniq key+幂等键联合判断。假设uk就是实体的id,幂等键是业务传过来的自定义值。我们去查找幂等键
uk已存在,幂等键不存在:说明用户希望再次操作同一个数据实体,不幂等 uk不存在,幂等键存在:说明用户希望再次操作其他数据实体,不幂等 uk、幂等键都存在:直接幂等 uk、幂等建都不存在:不幂等 1 2 3 4 5 6 7 // 这个例子实现了一个幂等键 // 业务场景:假设我们要设计一个领奖接口,每个用户只能领一次奖品。 // seq: 如果此奖励一个user_id可以重复领取多次,需要用seq标识唯一 // 用户id、奖励id、和seq表达一次幂等 func getIdemKey(userId string, prizeId string, seq string) (uniqueId string){ return fmt.
子恒 published on included in 游记 故事要从虎跳峡的一段徒步说起。走过人满为患的虎跳峡主景区之后,我们迅速远离了商业化的景区服务(不到三层楼的台阶居然修了收费的电梯,破坏了自然的景色不论,居然是封闭的,看不到半点峡谷和河流,甚是乏味)和拍游客照的熙熙攘攘的人群之后,心里略有不甘。眼见太阳已经走过了头顶,徒步的心又痒了起来,于是和朋友们驱车继续深入,前往中虎跳。
行车没有多久,一段曲折的山路就出现在了我们面前。路一看就没有完全修好,一眼就能看到夸张的U形弯,不仅狭窄到仅能一车通过,还有很多细碎的石子。我们下车观望,齐齐看向了队伍中驾驶经验丰富的唐,但路线的艰难和我们超长的9座商务车也只让唐无奈地点起了烟。于是我们讨论了几分钟,便留下了一部分体力不足的朋友原地等待,剩下人徒步上山(后来证明了这是极其明智的选择,路上搭到的便车阿姨已经在这条山路上开了19年,高超的驾驶技术和艰难的路况实在令我们汗颜)。
阿姨把我们送到了徒步的起点,茶马客栈。客栈后是三三两两的韩国人和美国人,看来远道而来的不止我们几个朋友。客栈后的玉龙雪山已经被夕阳渲染成了金色,颇为壮观。我们意识到自己来对地方了,便绕过客栈,顺着路标牌走了起来。
徒步路程并不劳累,但却颇具特色。路上有当地的农户,种植了一些蔬菜,我们不禁讨论起这里拉货和盖房子的困难,他们是如何把生活物资运上山。路上还有很多各种语言的路标牌,指向法国人和美国人开的客栈,我们朝着路标望去,却发现是一条蜿蜒的山路,还要攀爬数百米,只能望洋兴叹。再有很多面对圣山的堆叠起来的石片,我们后来才了解到这是一种古老的祈福仪式,在西南地区十分常见。我们一路走走拍拍,每绕过一座山就能看到远处的雪顶金山又近了一点,在愉悦的心情中行程迅速过半。
即使一路已经看过了各种美景,按理来说应该对大多数景色免疫,但是再次穿过一个山头后,我们还是对眼前的景象大为震惊。被劈的几乎垂直的峭壁中间,是一条极其深邃的山涧,里面传出清脆流水的声音,而我们的小路恰到好处地在山涧上方穿过。走过小桥的时候,虽然双腿发软,但是下面的深谷仿佛有一种魔力,让我的眼睛贪婪地搜刮着峡谷中的诸多细节。再补一眼陡峭的山壁,再看看山壁对面的金山当做佐料,一种壮阔的感觉油然而生。
朋友小W忍不住拿起手机,试图把这种壮阔记录下来,但是尝试过各种角度之后,不免垂头丧气,发现拍到的照片似乎表达不出眼前景色的百分之一。是啊,眼前的景色是流动的,我们心中的印象是由一段时间的感受组合而成,相机只能框选其中一部分,自然无法还原。朋友说要是眼睛里有个无限分辨率的摄像机该多好,就可以完整记录我们看到的景色,可惜现在的科技还无法达到。我思考了下,总觉得即使完全如他所说,却还差了一些什么,但又说不上来。
直到云南的行程过去大半年,一次偶然听到一位写生的画家讲起写生和摄影的区别,我才幡然醒悟。这位老师说,“绘画写生可不是去找哪个合适的角度,而是寻找我们感受最深的一个瞬间”,我终于意识到,除了一双无限分辨率和能看到各种角度的大脑记忆,无论如何都无法描述当时的美;因为美不仅仅包含了峭壁、小溪和雪山的各种角度,还包含了流水的声音,徒步过程的愉悦,曲折的上山过程,和朋友在一起欣赏的心情,甚至从嘈杂游客中回归自然的焕然一新,都是我们感受到美的原因,但这似乎是每个人都迥异的感受,是一张照片远远无法表达的信息。这就是摄影和眼睛的区别。
但如果认为静态的图片无法给人带来相同美的感受,也未免过于武断。艺术史已经告诉我们前人做了诸多尝试,比如捕捉瞬间极美感受的印象派,从各种视角去描绘事物本身特点的立体主义,以及描绘内心感情的抽象主义,甚至他们摒弃的写实流派,都是用不同方法试图还原美的成功案例。当产生强烈的美感时,记住美,了解美,思考美,无论是眼前的画面、景物的角度或组合、声音气味还是心情,都可以体现在照片中,让我们的摄影作品也会成为别人的眼睛。
子恒 published on included in 架构 这片文章想讲一下软件架构的基础,一些基本的原则和思想。我理解的架构是指导我们设计和实现软件的方法论;这意味着我们对复杂度、可扩展性、稳定性(分别对应迭代上手成本、迭代效率、事故损失)这三个方面有非常高的要求,尤其是复杂度,对修改代码的影响范围、成本、难易程度都有显著的影响,是架构要解决的核心问题。
另外,我们也不额外考虑性能,因为业务中的性能瓶颈距离我们讨论的问题往往还有几个数量级,往往是异步、流程优化、io等待等方面更加能有作为,采取什么样的设计方法能起到的作用微乎其微。
设计原则 关于原则,我想说一句话,原则是开发迭代中总结出来的经验,而不是开发迭代的规范。判断是不是原则适用的场景(而不是无脑用原则)往往更加有效,也更加困难。类似“没有银弹”。
KISS: Keep it simple, stupid,不要引入不必要的复杂
DRY: Don‘t repeat youself,不要重复代码
迪米特法则: The Least Knowledge Principle,使用者不知道实现细节
SOLID原则:
S 单一职责原则:对象应该仅具有一种单一功能 O 开闭原则:软件的目标应该是可扩展,不可修改 L 里氏替换原则:子类在任何代码中替换父类,行为一致 I 接口隔离原则:多个特定接口要好于一个宽泛用途的接口 D 依赖反转原则:抽象不应该依赖于具体,具体应当依赖于抽象(DDD就是这样,实际需求 ->抽象领域 -> 实现细节) 上面这些法则,几乎都在说:
代码简单可复用,架构高内聚低耦合
过程而非对象 面向对象(OOP)还是面向过程(函数式FP)?
这里的面向过程是对过程做结构化抽象,并不是平摊代码,这种明显的靶子行为不在我们讨论范围内。
我们先看看面向对象的核心特征(Java为例):封装、继承、多态
封装:和自己数据有关的操作封装成成员方法,并控制可见性 继承:得到父类的属性和方法 多态:不同类型调用同一个成员方法会产生不同的结果 我们看看非面向对象的做法(Go为例):
封装:和自己数据有关的操作封装成接收器,并控制可见性(非常类似);和自己数据无关的操作,直接做成普通函数 继承:得到别人的属性和方法都用组合的方式,避免构造函数、重载、层次设计 多态:类型之间没有继承关系,不同类型实现同一个接口的方法。调用的时候按自己的类型去做出对应行为 封装:函数和数据的耦合 编程的宗派,王垠
对象作为数据访问的方式,是有一定好处的。然而“面向对象”(多了“面向”两个字),就是把这种本来良好的思想东拉西扯,牵强附会,发挥过了头。很多面向对象语言号称“所有东西都是对象”(Everything is an Object),把所有函数都放进所谓对象里面,叫做“方法”(method),把普通的函数叫做“静态方法”(static method)。实际上呢,只有极少需要抽象的时候(为访问对象数据提供间接的封装),你需要使用内嵌于对象之内,跟数据紧密结合的“方法”。其他的时候,你其实只是想表达数据之间的变换操作,这些完全可以用普通的函数表达,而且这样做更加简单和直接。
这种把所有函数放进方法的做法是本末倒置的,因为函数并不属于对象。绝大部分函数是独立于对象的,它们不能被叫做“方法”。强制把所有函数放进它们本来不属于的对象里面,把它们全都作为“方法”,导致了面向对象代码逻辑过度复杂。很简单的想法,非得绕好多道弯子才能表达清楚。很多时候这就像把自己的头塞进屁股里面。
过度封装会导致几个结局:
类的职责逐渐变得模糊,越来越大,远远超出了设计时的职责 对象的成员变量太多,且有没有赋值、赋值产生的依赖很难管理 对象的成员函数太多,类有哪些功能、哪些函数很难辨别 我们实践的时候,会有entity作为数据的封装,和一系列操作entity自身的成员方法。但如果涉及别的对象呢?我们有converter,helper等方法包作为补充
继承vs组合 不要仅仅为了代码复用而继承。当你使用组合来实现代码复用的时候,是不会产生继承关系的。过度使用继承的时候,如果修改了父类,会损坏所有的子类。这是因为子类和父类存在紧耦合。
不要仅仅为了多态而继承。如果你的类之间没有继承关系,并且你想要实现多态,那么你可以通过接口+组合的方式来实现,这样不仅可以实现代码重用,同时也可以实现运行时的灵活性。
Rust程序设计语言
子类不应总是共享其父类的所有特征,但是继承却始终如此。如此会使程序设计更为不灵活,并引入无意义的子类方法调用,或由于方法实际并不适用于子类而造成错误的可能性。
考虑到实际的工程往往都是多层次的组合和复用,继承能带来的收益非常小,但是后期的维护成本却是很难评估的。所以我建议在任何时候都不要使用继承。
我们讨论完面向对象的核心特质:封装、继承、多态,发现这些行为的滥用都需要格外地小心。这也是为什么工业界越来越多的go和rust,仅仅从这一章节讨论的结果来说,这些更年轻的语言汲取了面向对象(数据封装、成员方法)和函数式(接口、一等公民)的优点,取消了他们诸多迷惑的特性,这是在任何语言都可以借鉴的行为。
“干净”的代码,贼差的性能
数据和方法 数据是肉,方法是血。按数据里有多少量的方法,我们划分为下面几种模型。我们按保存用户信息这样一个简单的操作来举例
失血模型 1 2 3 4 // dal 代码 func saveUser(user){.
子恒 published on included in 管理 刚毕业的高材生小姜,有着浑厚的知识储备和满怀热情的心脏来到了某厂,在做了一段时间需求后,发现自己对做事靠谱的老司机倍加羡慕;为什么人家有条不紊,好评如潮?自己确手忙脚乱,频频提测delay,加班到深夜?
今天我们来帮帮小姜,看看小姜为什么技术扎实,态度积极确总是使不上劲?
(故事仅供参考,切勿对号入座)
需求详评 这周产品新提了一个需求,拉了小姜和老梁一起做,在详评会议上:
小姜:听的云里雾里,昏昏欲睡,听产品讲完后准备下来仔细看看代码哪里要改
老梁:对产品频频犀利发问:这个细节A为什么这样做,我感觉体验并不好?我们这个需求预期什么时候上线?是不是倒排?我看到这个项目需要我们合作方A做开发,定容了吗?
小姜恍然大悟的笔记(详评阶段应该做的):
敲定需求细节,可以给出自己视角的建议 对产品考虑不周全的需求,结合现状进行扩充 对于欠缺价值的需求点,合理挑战 对于破坏系统设计过大的需求点,提早商量 敲定需求整体节奏和预期上线时间,为做方案做准备 对于涉及到前置依赖的需求,一定要尽早确认好工作节奏 UE稿什么时候出 下游是否已经定容、什么时候完成开发,下游依赖接口什么时候给 技术评审 小姜:往文档里贴了下代码截图片段,告诉大家我要改这里,其他人听的一脸懵
老梁:给了一个完整的方案,涉及到合作方的关注点的内容写的非常详细,获得一致好评
小姜复制了一份老梁的模板:
排期概览(开发、联调、测试、上线的准确时间) 业务背景(问题、目标、收益) 技术方案 设计模块和改动点列举 架构图、流程图、状态机、时序图、ER图 异常情况和处理 AB实验方案 老数据兼容 依赖下游方案 风险点(需求自身风险、产品影响风险、可维护性/效率风险) 存储设计 DB/TCC/MQ/Redis/ES的schema变更 数据量级、key、分片、索引、选型、异常处理 接口文档 http接口定义和使用文档 rpc接口定义和使用文档 服务治理 异常兜底 监控报警 工作量评估 工作拆分和估分 进度计划 自测用例 冒烟测试用例(描述、步骤、期望结果) 希望测试测到的地方 发布计划 MR 发布顺序 开发 评审之后大家迅速都加入了如火如荼的开发当中:
小姜:早早地做完了自己的工作,等着联调。联调前一天突然发现下游依赖的接口还没数据,一问原来是有个工作没有对齐漏掉了,心急如焚
老梁:按照之前拆分的开发计划,列出了一个详细的进度追踪表,可以看到工作分配到的人、完成时间、里程碑,还有风险;提前把问题消灭了,大家笑盈盈进入联调
功能点 开发状态 人力预估 开发者 自测完成 风险记录 功能A 未开始 5 小A 是 暂无 功能A适配 进行中 2 小B 否 下游接口延迟一天 功能B适配 已完成 2 小B 否 功能B 已完成 0.
子恒 published on included in 杂谈 折腾了这么久笔记软件,终于有了一个最终的方案,给大家分享下
用过的软件对比 软件 优点 缺点 用途 印象笔记 云/多端 丑,难用,残疾md 手机查看 OneNote 多目录/手写 同步难,没有md 读书、网课笔记 Typora 好看/导出 没有云、收费 离线编辑 马克飞象 比较好看 没有云、收费 离线编辑 Obsdian 比较好看 没有云、不能导出 离线编辑 CmdMarkdown 有云 难看 备份,网页版查看 StackEdit 简单 太卡了 没用 MarginNote 知识图谱 无win 读pdf书 备忘录 多端同步、轻量 格式难用 速记 Notion 多端同步、复杂 md不好用,手写不支持 替代onenote 诉求 支持markdown,且美观 支持Latex,脚注 支持云存储,移动端同步 支持mac/windows/ios/网页版 支持3级目录,支持搜索 支持手写插入 支持网页剪裁 支持离线编辑 可以导出pdf,html 支持代码块,代码块染色,行号 支持大纲 工作流 没有一个通用的工作流,所以我需要根据不同的场景来决定采用哪些软件。
速记 (todo list,check list)
备忘录
写博客 Obsdian -> github -> hugo
子恒 published on included in 互联网 笔者曾在腾讯商业化、字节广告变现承担过多年广告后台相关工作,对业界的广告套路和广告架构比较熟悉。本文旨在以尽量容易理解的方式来分享广告相关的知识,来给对广告业务了解较少的同学形成一个基本的认知:
广告是如何赚钱的? 广告系统的组成是什么样的? 广告系统有哪些值得学习的策略? 商业模型 广告在平台内部的“永动机”
用户产品:把平台拉新进来的用户留存,转化成活跃用户 商业化:在活跃用户的浏览行为中插入广告,获得收入 UG:把部分商业化的收入用来拉新、激励,使得有源源不断的新用户进来
广告做的是下面三个人群的生意
用户:需要使用平台的服务,如新闻、推文、博客、视频 广告主:需要构建自己的品牌价值、推销自己的产品等 平台:需要用自己产品的DAU变现 广告业务 广告形态 常见的广告形态
硬广
开屏广告 原生开屏广告 信息流广告 搜索广告 软广
图文视频软广 非标
锚点 彩蛋 hashtag DOU+ 交易链路 如果广告主想要在多个媒体(网站、广告网络、交易平台)投放广告 ,是一个非常繁琐的过程。因为每一个网站、广告网络、交易平台的媒体购买系统、操作规则不同,需要人工进行调整,费时低效;而且跨渠道媒体购买很可能重复购买同一部分人群。
DSP平台把广告主、代理人员从庞杂的重复手工操作中解放出来。广告主只要在DSP平台投放广告即可,由DSP平台帮助广告在多个媒体投放广告。
同理,如果流量主想在自己的网站流量位置上插入广告,如果自己去招商,也是个非常消耗人力的事情,寻找广告主、和广告主对接、广告主提供的素材匹配自己的广告位都需要很多时间。SSP平台可以快速满足流量主接入广告的需求,帮助流量主快速变现。
名词解释
DSP (Demand-Side Platform): 需求方平台,可以简单理解为需要采买流量的平台(广告主自建或者第三方技术公司) SSP (Supply-Side Platform):供应方平台/媒体平台,管理流量和坑位。可以简单理解为流量(抖火西头) DMP (Data Management Platform):数据管理平台,管理人群,生成定向人群包 ADX (AD Exchange):程序化广告****交易平台,通过技术对接的方式,支持客户(DSP)进行媒体的流量(用户)采买 ADN (Ad Network ): 广告网络,聚合了大量App内的展示广告资源,主要包含中长尾App流量,帮助广告主实现媒体精准、灵活的投放。汇集了很多媒体的余量 各种公司的布局:
行业角色 变现公式 **广告收入 = 活跃用户数 * 人均展示数 * 广告负载 *** 广告点击率 * 广告价格
$$(Ad Revenue = DAU * Avg Imps * AdLoad * CTR * AdPrice)$$
子恒 published on included in 后台 如果你在服务端的工区,常常会听到同学们激烈的讨论,包括能不能扛得住xx流量?能不能P99达到x毫秒?某操作能不能立即生效?某服务CPU飙升了,某服务OOM了,某服务超时率暴涨了?
这些灵魂的质问,其实就是在保障服务端的高并发、高性能、高可用、高一致性等等,是我们服务端同学必备的扎实基本功。
克服系统瓶颈 服务端的代码都跑在各种版本的Linux之上,所以高性能的第一步要和操作系统打交道。我们的服务需要通过操作系统进行I/O、CPU、内存等等设备的使用,同时在使用各种系统调用时避免各种资源的开销过大。
零拷贝 认识零拷贝之前,我们先要对Linux系统I/O机制有一定的了解。当我们执行一个write(2)或者read(2)的时候(或者recv和send),什么时候操作系统会执行读写操作?什么时候又最终会落到磁盘上?
以一个简单的echo服务器为例,我们模拟下每天都在发生的请求和回包:
1 2 3 4 5 6 sockfd = socket(...); //打开socket buffer = new buffer(...); //创建buffer while((clientfd = accept(socketfd...)){ // 接收一个请求 read(clientfd, buffer, ...); //从文件内容读到buffer中 write(clientfd, buffer, ...); //将buffer中的内容发送到网络 } 看一下这段代码的拷贝流程(下图):
数据包到达网卡,网卡进行DMA操作,把网卡寄存器的数据拷贝到内核缓冲区 CPU把内核缓冲区的数据拷贝到用户空间的缓冲区 用户空间处理buffer中的数据(此处不处理) CPU把用户空间的缓冲区的数据拷贝到内核缓冲区 网卡进行DMA操作,把内核缓冲区的数据拷贝到网卡寄存器,发送出去 整个过程触发了4次拷贝(2次CPU,2次DMA),2次系统调用(对应4次上下文切换)
(注:DMA(Direct Memory Access), I/O 设备直接访问内存的一个通道,可以完成数据拷贝,使得CPU 不再参与任何拷贝相关的事情,现在几乎所有的设备都有DMA)
使用mmap mmap可以把用户空间的内存地址映射到内核空间,这样对用户空间的数据操作可以反映到内核空间,省去了用户空间的一次拷贝:
应用调用mmap,和内核共享缓冲区(只需一次) 数据包到达网卡,网卡进行DMA操作,把网卡寄存器的数据拷贝到内核缓冲区 CPU把接收到的内核缓冲区的数据拷贝到发送的内核缓冲区 网卡进行DMA操作,把内核缓冲区的数据拷贝到网卡寄存器,发送出去 整个过程触发了3次拷贝(1次CPU,2次DMA),2次系统调用(对应4次上下文切换)
使用sendfile/splice Linux 内核版本 2.1 中实现了一个函数sendfile(2):
他把read(2)和write(2)合二为一,成为一次系统调用,实现了把一个文件读取并写到另一个文件的语义 系统调用中不再切换回用户态,而是在内核空间中直接把数据拷贝过去(2.4 之后这一步支持了DMA拷贝,实现了CPU零拷贝) 我门看下使用sendfile之后的流程:
整个过程触发了3次拷贝(0次CPU,3次DMA),1次系统调用(对应2次上下文切换)
Linux 内核版本 2.6 中实现了一个函数splice(2),类似sendfile,但是接收/发送方必须有一个文件是管道,通过管道的方式连接发送方和接收方的内核缓冲区,不再需要拷贝(0次CPU,2次DMA,1次系统调用)
子恒 published on included in Go 本文介绍了Go的语言设计和一些容易踩坑的细节:
理解Go为什么X,摆脱原语言的思维 解决写代码时比较困惑和不满的点,对容易出错的语法有个印象 Go学起来非常简单,但是这是语言设计者刻意为之,很多复杂的细节都藏在语言实现里,导致我们迅速学会Go之后不断踩坑
Why Go 2007年,Google设计Go,目的在于提高在并行编程(多核CPU越来越多)、分布式部署、大型代码库(以及维护他们的非常多的开发人员)的情况下的开发效率。设计时,在吸收C++优点的基础上,收集于很多工程师之间流传的的“不要像C++”
Go like C++:
内存消耗少 执行速度快 启动快 Go not like C++:
程序编译时间短(按照我过去的经验,一个C++大型项目即使make -j8也需要编译一个小时以上) 像动态语言一样灵活(runtime、interface、闭包、反射) 内置并发支持(C++的协程至少得等到std23才有,非常落后) 丰富的原生库(C++解析json,建立http服务器,使用redis这种都很难找到靠谱的库) 多语义(取消了指针运算、取消隐式类型转换、取消类型别名,取消重载,++和赋值作为表达式…) Go的优点:
面向工程:简单。只有25个关键字,代码风格统一,可读性高,go mod包丰富 自动垃圾回收:语言运行时内置垃圾回收 语言级并发:非常好用的routine和channel,更高层次的并发抽象 静态语言动态特性 Go的缺点:
runtime的性能还需要提高 没有泛型 冗余的错误处理 Go mod不够完善 Go语⾔将⾛向何⽅?
我为什么放弃Go语言
Go的设计哲学 创始人Rob Pike在SPLASH上的演讲,阐述了设计Go的初衷
许式伟,Go和Java在继承观念上的对比
对面向对象的批评
王垠:解密“设计模式”,对设计模式的批评
少即是多(less is more):如果一个特性并不对解决任何问题有显著价值,那么go就不提供它;如果需要一个特性,那么只有一种方法去实现 面向接口编程:非侵入式接口,反对继承、反对虚函数和虚函数重载(多态)、删除构造和析构函数 正交+组合的语言特性:语言的特性之间相互独立,不相互影响。比如类型和方法是互相独立的,类型之间也是相互独立的,没有子类,包也没有子包。不同特性用组合的方式来松耦合 并发在语言层面支持:并发更好利用多核,有更强的表现力来模拟真实世界 在设计上,Go秉承了C的简单粗暴。
为什么没有继承? Go没有子类型的概念,只能把类型嵌入到另一个类型中,所以没有类型系统。Go的作者认为类型系统被过度使用了,应该在这个方向上退一步。
使用伸缩性良好的组合,而不是继承 数据和方法不再绑定在一起,数据的集合用struct,方法的集合用interface,保持正交 类似子类父类的系统造成非常脆弱的代码。类型的层次必须在早期进行设计,通常会是程序设计的第一步,但是一旦写出程序后,早期的决策就很难进行改变了。所以,类型层次结构会促成早期的过度设计,因为程序员要尽力对软件可能需要的各种可能的用法进行预测,不断地为了避免挂一漏万,不断的增加类型和抽象的层次。这种做法有点颠倒了,系统各个部分之间交互的方式本应该随着系统的发展而做出相应的改变,而不应该在一开始就固定下来。
作者附了一个例子,是一些以接口为参数并且其返回结果也是一个接口的函数:
1 2 3 4 5 6 // 入参是接口的函数,而不是成员方法 func ReadAll(r io.Reader) ([]byte, error) // 封装器 - 出入参都是接口 func LoggingReader(r io.
子恒 published on included in 后台 分布式锁 简单加锁 1 2 3 4 5 // 思想:利用setnx检测有没有set过,如果set过就表示没有抢到锁 > setnx locker true OK // ... do somthing ... > del locker 处理set之后进程崩溃的死锁问题 1 2 3 4 5 6 // 思想:给锁加上过期时间,即使set之后进程挂掉,也不会死锁 > setnx locker true OK > expire locker 5 // ... do somthing ... > del locker 处理非原子性问题 setnx之后,expire之前,进程挂了,也会死锁。怎么处理这种情况?
使用redis事务吗?事务里没有if else,要么全部执行,要么全部不执行。需求是setnx成功才执行expire,有依赖关系,没法用事务 使用新的原子命令,如下 1 2 3 4 > set locker true ex5 nx OK // ... do somthing ... > del locker 处理超时问题 上面的方案设定了超时时间。但是如果少数操作的时间超过了超时时间怎么办?有两个问题: