<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Intotw的博客</title><description>intotw&apos;s blog</description><link>https://www.intotw.cn/</link><language>zh_CN</language><item><title>记录下最近的一些事情-202603</title><link>https://www.intotw.cn/posts/essay-record-20260319/</link><guid isPermaLink="true">https://www.intotw.cn/posts/essay-record-20260319/</guid><pubDate>Thu, 19 Mar 2026 07:21:11 GMT</pubDate><content:encoded>&lt;h1&gt;随笔记录下最近的一些事情和感受&lt;/h1&gt;
&lt;h2&gt;申请被博客十年之约拒绝了&lt;/h2&gt;
&lt;p&gt;虽然当时是看到了随手申请了下申请着玩，但是拒绝理由是最近三个月没有什么有效的更新，呵呵，纯啥B&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2026/03/679fa34c68ff1fefd9f0305c7597f7e6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;就业环境对应届生越来越恶劣了&lt;/h2&gt;
&lt;p&gt;去年开始无偿（😡）帮公司打黑工担任一轮技术面的面试官，都快一年了，明显能感觉到推来的简历学历越来越好（基本保底211），说明应届兄弟们越来越卷了&lt;/p&gt;
&lt;p&gt;现在大模型真的在摧毁工程新人才，应届生从论文就开始VBCoding或者VB论文，入职以后也没有什么知识体系或者工程思维的提升机会。面试过一个B站的妹子进去就python写写边角料优化，很尴尬，想求变但是又没有应届光环了，毕业干了半年，语言技术栈都落后其他人半年了&lt;/p&gt;
&lt;p&gt;越来越卷的背景下，大部分企业不愿意再付出培养成本了，那些愿意付出培养成本的，门槛也会越提越高&lt;/p&gt;
&lt;p&gt;AI过热的高调下，是挥之不去的实体经济下行以及就业困难的低噪。&lt;/p&gt;
&lt;h2&gt;现在的社会风气，开始趋向于笑贫不笑娼了&lt;/h2&gt;
&lt;p&gt;没什么好说的原因&lt;/p&gt;
&lt;p&gt;可怕的感觉还是，我们既达不到日本北欧那种人和人之间有充分的尊重和距离，更加遵循人的个体&lt;/p&gt;
&lt;p&gt;也会不到以前历史上的那种乡土人情大家族的味道&lt;/p&gt;
&lt;p&gt;不上不下，卡在那了&lt;/p&gt;
&lt;h2&gt;守业比创业难&lt;/h2&gt;
&lt;p&gt;小时候看大汉天子，那个主题曲，最后一句就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;守业更比创业难&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大部分人期望使用某种方式一劳永逸的社会，肯定是不健康的&lt;/p&gt;
&lt;p&gt;这几年的棚改货币化和撒币投机热潮，造就了太多幸运儿成为百万千万富翁&lt;/p&gt;
&lt;p&gt;但是富不过三代，王朝都会灭亡，那些财富真能持续那么久传下来么？我们建国到现在才77年，改革开放到现在也才48年。&lt;/p&gt;
&lt;p&gt;或许从人的本质来看，人本身就很可悲，叔本华觉得人生&lt;strong&gt;要么匮乏，要么无聊&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我是感觉，人要么一辈子因为焦虑或者快感不断去追求什么，要么就随波逐流顺从生活吃喝拉撒。才能快乐。&lt;/p&gt;
&lt;p&gt;不然就是思考以后，得到无尽的虚无。&lt;/p&gt;
</content:encoded></item><item><title>听听歌吧-宇多田光《First Love》</title><link>https://www.intotw.cn/posts/recommend-song-5/</link><guid isPermaLink="true">https://www.intotw.cn/posts/recommend-song-5/</guid><pubDate>Wed, 04 Mar 2026 05:54:37 GMT</pubDate><content:encoded>&lt;p&gt;这又是一首听起来有”冬天“（为什么我那么喜欢冬天……）的感觉，或者说是伤心的感觉的歌&lt;/p&gt;
&lt;p&gt;很多人说日本乐坛深爱第一是尾崎丰的《I love you》，但是我个人心中是First Love&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;////player.bilibili.com/player.html?isOutside=true&amp;amp;aid=113463741386550&amp;amp;bvid=BV1TjmUYVE6X&amp;amp;cid=26712672563&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;width:100%;aspect-ratio:16/9;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;日文歌词&lt;/th&gt;
&lt;th&gt;中文歌词&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;最後のキスは&lt;/td&gt;
&lt;td&gt;最后的吻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;タバコのflavorがした&lt;/td&gt;
&lt;td&gt;带着淡淡的烟草味&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ニガくてせつない香り&lt;/td&gt;
&lt;td&gt;苦涩而令人心碎的香味&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;明日の今頃には&lt;/td&gt;
&lt;td&gt;明天的这个时候&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;あなたはどこにいるんだろう&lt;/td&gt;
&lt;td&gt;你会在哪里&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;誰を想ってるんだろう&lt;/td&gt;
&lt;td&gt;又会想着谁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You are always gonna be my love&lt;/td&gt;
&lt;td&gt;我会永远爱着你&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;いつか誰かとまた恋に落ちても&lt;/td&gt;
&lt;td&gt;就算将来爱上其他人&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&apos;ll remember to love&lt;/td&gt;
&lt;td&gt;我也会记得去爱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You taught me how&lt;/td&gt;
&lt;td&gt;这是你教会我的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You are always gonna be the one&lt;/td&gt;
&lt;td&gt;你永远都是我的唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;今はまだ悲しいlove song&lt;/td&gt;
&lt;td&gt;如今悲伤情歌依然不止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新しい歌 うたえるまで&lt;/td&gt;
&lt;td&gt;直至唱出新的歌曲&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;立ち止まる時間が&lt;/td&gt;
&lt;td&gt;停滞的时间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;動き出そうとしてる&lt;/td&gt;
&lt;td&gt;开始慢慢运转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;忘れたくないことばかり&lt;/td&gt;
&lt;td&gt;剩下的尽是难以忘怀的事情&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;明日の今頃には&lt;/td&gt;
&lt;td&gt;明天的这个时候&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;わたしはきっと泣いてる&lt;/td&gt;
&lt;td&gt;我一定会流着泪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;あなたを想ってるんだろう&lt;/td&gt;
&lt;td&gt;想起深爱过的你&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You will always be inside my heart&lt;/td&gt;
&lt;td&gt;你会永远占据我的心&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;いつもあなただけの&lt;/td&gt;
&lt;td&gt;在我心中永远&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;場所があるから&lt;/td&gt;
&lt;td&gt;给你留下一片空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I hope that I have a place in your heart too&lt;/td&gt;
&lt;td&gt;希望我在你心里也能有一席之地&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Now and forever you are still the one&lt;/td&gt;
&lt;td&gt;直到永远你仍是我心里的唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;今はまだ悲しいlove song&lt;/td&gt;
&lt;td&gt;如今悲伤情歌依然不止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新しい歌 うたえるまで&lt;/td&gt;
&lt;td&gt;直至唱出新的歌曲&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You are always gonna be my love&lt;/td&gt;
&lt;td&gt;你将是我永远的挚爱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;いつか誰かとまた恋に落ちても&lt;/td&gt;
&lt;td&gt;就算将来爱上其他人&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&apos;ll remember to love&lt;/td&gt;
&lt;td&gt;我也会记得去爱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You taught me how&lt;/td&gt;
&lt;td&gt;这是你教会我的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You are always gonna be the one&lt;/td&gt;
&lt;td&gt;你永远都是我的唯一&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;まだ悲しいlove song&lt;/td&gt;
&lt;td&gt;如今悲伤情歌依然不止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Now and forever&lt;/td&gt;
&lt;td&gt;直到永远&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>Websocket技术接入整体方案架构</title><link>https://www.intotw.cn/posts/websocket/</link><guid isPermaLink="true">https://www.intotw.cn/posts/websocket/</guid><pubDate>Sun, 28 Sep 2025 07:21:11 GMT</pubDate><content:encoded>&lt;p&gt;又又又好久没更新，在新公司实在是比较忙+太懒+没有什么有价值的内容，最近负责自研工单项目，实践了整套websocket，把方案搬到博客记录下&lt;/p&gt;
&lt;p&gt;因为这篇文章当时写的时候主要是面向组内分享并且是做SaaS的企业，所以同事之间针对分布式和高可用这些技术的知识背景各不相同，整个思路偏讲解和启发&lt;/p&gt;
&lt;h1&gt;WebSocket生态介绍&lt;/h1&gt;
&lt;h2&gt;Sock.js（接入层）&lt;/h2&gt;
&lt;p&gt;sock.js是连接层方案，主要解决的浏览器之间对websocket通信协议的兼容问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当浏览器不支持WebSocket或网络环境阻止WebSocket连接时，SockJS会自动降级到其他传输方式&lt;/li&gt;
&lt;li&gt;降级顺序：WebSocket → EventSource → Long Polling → XHR Polling等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类似的接入层方案：Socket.io。类比java中概念：Netty，HttpClient。&lt;/p&gt;
&lt;p&gt;最终我们选sock.js原因是因为他比较轻量化，Socket.io是node.js用的，非node使用前后端都需要三方包，引入更高的复杂度和接入成本&lt;/p&gt;
&lt;h2&gt;Stomp（协议层）&lt;/h2&gt;
&lt;p&gt;Stomp是协议层方案，实现的是STOMP协议（Simple Text Oriented Messaging Protocol）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持主题订阅（Topic）&lt;/li&gt;
&lt;li&gt;支持点对点消息传递（Queue）&lt;/li&gt;
&lt;li&gt;支持消息确认机制&lt;/li&gt;
&lt;li&gt;消息头管理&lt;/li&gt;
&lt;li&gt;事务支持&lt;/li&gt;
&lt;li&gt;心跳机制&lt;/li&gt;
&lt;li&gt;消息确认和重试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为websocket提供一套发布订阅的通用消息模型&lt;/p&gt;
&lt;p&gt;类似的协议层方案：MQTT.js（IoT）。类比java中概念：Grpc、Dubbo协议&lt;/p&gt;
&lt;p&gt;最终我们选Stomp的原因是因为Spring Boot的消息对他支持的最好，并且他的订阅风格也天然支持我们这次的会话模型。WebSocket细节方案&lt;/p&gt;
&lt;h2&gt;负载均衡&lt;/h2&gt;
&lt;p&gt;Websocket是长连接，为了符合分布式架构并且让Websocket也具备&lt;strong&gt;可扩展性&lt;/strong&gt;和&lt;strong&gt;高可用性&lt;/strong&gt;，我们就需要考虑负载均衡方案&lt;/p&gt;
&lt;p&gt;在选择和确认负载均衡方案之前，我们需要先了解下会话模型以及连接模型&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;会话模型&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;STOMP提供了2种发送消息的api，分别对应2种模型：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;私聊（通知）模型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;messagingTemplate.convertAndSendToUser(&quot;userId&quot;,destination, message);&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的userId是基于框架提供了连接各个生命周期时的拦截方法，我们实现方法在建立连接时解析token塞进去的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/481bcbd04fd4ef88a33c0ebb2cb87ce1.webp&quot; alt=&quot;image-20250928111831070&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;订阅&lt;/strong&gt;：客户和工程师（会话的双方）对服务器建立长连接，并且对于某一个topic端点进行订阅&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送&lt;/strong&gt;：客户和工程师通过接口，调用服务端的api，向某个topic端点进行发送消息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;聊天室模型&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;messagingTemplate.convertAndSend(topic, message);&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/501649efc66e460c86d226550f78f343.webp&quot; alt=&quot;image-20250928111919866&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;订阅&lt;/strong&gt;：客户和工程师（会话的双方）对服务器建立长连接，并且对于某一个topic端点进行订阅&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发送&lt;/strong&gt;：客户和工程师通过接口，调用服务端的api，向某个topic端点进行发送消息&lt;/p&gt;
&lt;p&gt;工单这次就包含以下端点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;/ticket/topic/ticket/工单id，作为聊天室模型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/innerMessage。作为点对点&lt;strong&gt;&lt;strong&gt;站内&lt;/strong&gt;&lt;/strong&gt;信通知&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上两种方式，可以发现通过这两种api，我们可以通过让用户订阅到不同的topic+使用不同的api，来完成各种消息通信效果。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;聊天室：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所有人订阅到**/topic/聊天室id**，所有人的消息发送到**/topic/聊天室id** 中的所有人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;私聊：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A和B都订阅到一个私有的聊天室topic：&lt;strong&gt;/topic/A+B&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通知：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A（系统）通知到所有在线的某些人时。&lt;/p&gt;
&lt;p&gt;所有在线者订阅到一个通用topic：&lt;strong&gt;/topic/InnerMessage&lt;/strong&gt;，A（系统）向这个topic，根据需要通知到的userUuid，挨个进行通知&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里建议大家可以回去思考一下，通知方式也可以实现聊天室+私聊的效果，聊天室+私聊的方式也可以实现通知的效果，为什么最终这两种模型会区分开？&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;连接模型&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;我们需要先了解一下Spring Boot中STOMP协议对消息实际的处理逻辑，不过不管是不是用STOMP，本质都是一样的，因为STOMP只是协议层。&lt;/p&gt;
&lt;p&gt;当A和B同时建立连接时，会发生什么？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/89a0316b33345a352fd796b8aaeda099.webp&quot; alt=&quot;image-20250928111955574&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当A和B都到服务器建立连接，此时的连接是&lt;strong&gt;全双工&lt;/strong&gt;的，既可以通过这个连接发送消息到服务端，也可以接受服务端的消息。&lt;/p&gt;
&lt;p&gt;在服务端，框架会维护一个能力类似简易内存消息队列的结构，用于消息的路由分发和暂存。但是需要注意的是，这个消息队列并没有ack、重投，并且也不会将消息保存过长时间，你可以认为它基本就是负责消息的路由而已。&lt;/p&gt;
&lt;p&gt;同时，虽然连接是全双工的，但是我们完全可以使用websocket连接仅作为订阅的通道，发送消息使用http接口，这样我们发送消息时的复杂逻辑处理起来会更简单。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;粘性会话&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;基于连接模型，在通过网关负载均衡后，就会有一个很致命的问题（Spring Cloud的Ribbon默认是轮询式的）：&lt;/p&gt;
&lt;p&gt;用户A连接到了机器1，用户B连接到了机器2，此时他们实际是割裂的，不能收发到彼此的任何消息&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/a860c6e72f3e3cf894b50ac04a6d9d87.webp&quot; alt=&quot;image-20250928112018842&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为连接是通过网关负载均衡，所以我们可以在网关修改负载均衡策略，采用哈希等逻辑，让同一个sessionId一定路由到同一台机器上，来解决这个问题，也就是“&lt;strong&gt;粘性&lt;/strong&gt;”会话。并且如果客户断线重连，因为我们的负载均衡策略，他还是会连回原机器，保障消息接受。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;todo：思考下这个策略的缺点。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;消息桥接&lt;/h3&gt;
&lt;p&gt;既然维护的是一个内存的类消息队列的场景，那么我们就可以用更强的消息队列进行桥接&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/feccf2784bcbccccf9eb4a0ee19c92db.webp&quot; alt=&quot;image-20250928112042220&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本质上，就是每台机器在发到本地的消息队列后，再发到中间件，然后其他机器通过消费中间件的消息，达成&lt;strong&gt;广播&lt;/strong&gt;，将这条消息也写进本地的机器中&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;todo：思考下这个策略的缺点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;结合改造网关有风险+我们的消息量并不多。我们目前只用了消息桥接，没有结合粘性会话+本地保存连接信息。&lt;/p&gt;
&lt;h2&gt;权限校验&lt;/h2&gt;
&lt;p&gt;前面也说到过，选stomp的原因其一就是他有独立的协议，可以在独立的协议中进行鉴权。&lt;/p&gt;
&lt;p&gt;鉴权本身最大的问题就在于：WebSocket是不能携带Http header的。&lt;/p&gt;
&lt;p&gt;所以我们的做法是，在STMOP握手时传递header，然后在服务端STOMP握手阶段进行鉴权，如果鉴权失败，拒绝握手中断连接。&lt;/p&gt;
&lt;p&gt;也就是实际连接过程是：websocket连接建立-&amp;gt;stomp握手&lt;/p&gt;
&lt;p&gt;当然也存在一定风险，就是通过无限建立websocket连接但是延缓握手来攻击（类似DDOS，连接攻击就是这么难防，安全和性能不能两得），但是这个真发生了可以通过SLB和应用层gateway来限流。&lt;/p&gt;
&lt;h2&gt;实际消息拉取逻辑&lt;/h2&gt;
&lt;p&gt;我们的消息是有messageId的。&lt;/p&gt;
&lt;p&gt;我们考虑两种场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户进到页面&lt;/li&gt;
&lt;li&gt;客户在页面待着，但是websocket重连&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;如何保证消息的正确性？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里只描述下我们的方案，这个建议大家自己也去思考。没有唯一解。我们也是考虑过很多种方式然后经过取舍，最终想到了一个比较好的方案。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端每次打开某个会话页面时，生成一个唯一的UUID，作为这次websocket的唯一UUID。&lt;/li&gt;
&lt;li&gt;前端进入后，调用getHistoryMessage接口（入参为前端当前时间戳），拉取历史消息&lt;/li&gt;
&lt;li&gt;前端建立连接时，上送webSocket的唯一UUID+前端当前获取到的最后一条消息的时间戳+sessionId
&lt;ol&gt;
&lt;li&gt;这里最后一条消息，如果是getHistoryMessage，那么就是getHistoryMessage的最后一条&lt;/li&gt;
&lt;li&gt;如果是重连，就是之前的最后一条&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;后端建立websocket连接时，根据前端上送的时间戳，触发一个逻辑（事件、任务），将所有大于该时间戳的消息查出来，使用发送逻辑，将这些消息全部再发一次&lt;/li&gt;
&lt;li&gt;后端在发送消息时做一个统一幂等，根据websocket的唯一UUID+messageId，使用redis缓存做幂等。即同一个websocket中，某条消息只发一次
&lt;ol&gt;
&lt;li&gt;到websocket的唯一UUID级别的幂等，是为了解决页面多开问题，此情况下用sessionId做幂等有问题&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;该方案仅能保证消息不丢失不重发，无法保证重连前后的消息可能会乱序的问题&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;技术方案架构设计思维过程图&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2025/09/b63dd8b0e6a17f78e861deedd911a1c7.webp&quot; alt=&quot;image-20250928112115429&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;发现缺陷、解决缺陷的方案、整合方案、重构&lt;/strong&gt;这几个过程的重复后。最终能将方案做到怎样的上限，就依赖于设计人的经验、技术功底、技术眼界了。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 40 File System Implementation</title><link>https://www.intotw.cn/posts/ostep-40-file-system-implementation/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-40-file-system-implementation/</guid><pubDate>Thu, 24 Oct 2024 07:21:11 GMT</pubDate><content:encoded>&lt;p&gt;好久没更新，主要文件系统实际实现这段看完一次没有很看懂，过了段时间又看了第二次加深了理解，才好做总结&lt;/p&gt;
&lt;p&gt;本章主要讲解了一下文件系统最简单的实现，以及设计一个文件系统需要考虑哪些点。&lt;/p&gt;
&lt;h2&gt;文件系统基本的数据结构&lt;/h2&gt;
&lt;p&gt;一个文件系统基本的数据接口，首先一般会将磁盘划分为多个固定大小的Region，这个Region你可以认为是页，理所当然，页的大小就是4k（内存页）。&lt;/p&gt;
&lt;p&gt;然后，我们看下文件系统包含哪些数据结构类以及他们的作用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/18f6ea7f0d0dc96f6e51863f691b8eb9.webp&quot; alt=&quot;文件系统基本结构&quot; /&gt;&lt;/p&gt;
&lt;p&gt;和文中顺序一样吧，我们倒着来，方便理解。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt;：D表示数据区域，就是实际保存数据的数据节点，里面存的就是实际需要保存的用户数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I（Inodes）&lt;/strong&gt;：Inodes就是文件的源信息（metadata）保存的节点，里面保存的数据是一个个Inode，每个Inode里面保存了文件的源信息，包括文件的名称、创建时间、所有者、权限、以及实际对应的文件的Data Region信息。&lt;/li&gt;
&lt;li&gt;Inodes前面的&lt;strong&gt;i、d&lt;/strong&gt;：这实际上是2个bitmap，就是inode和Data Region的2个bitmap，用来记录Inode和Data Region的使用情况，bitmap中第n位表示第n个Inode或者Data Region是未使用的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S&lt;/strong&gt;： 记录了整个文件系统的源数据，比如Inodes的数量，Data Region的数量，还有他们这两个区域起始的位置，还会用一个魔法数的枚举来表明文件系统的类型（fat32，NTFS）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Inodes对于Data Region的记录&lt;/h2&gt;
&lt;p&gt;Inodes中需要记录的就是文件的源信息，以及这个文件对应的Data Region，那么如何支持大文件就会成为一个问题，当一个Inode表示一个文件时，他至少需要一个数组来表明这个文件使用了哪些Data Region，比如使用了（8,9,10,11），但是当这个场景扩展到大文件时，这个数组就会过大。&lt;/p&gt;
&lt;p&gt;解决思路其实很简单，和内存的解决思路相似，使用&lt;strong&gt;间接指针&lt;/strong&gt;。Inode中改为只指向一个Data Region，但是那个Data Region中保存的多层树结构（是不是很像多级页表？），这些树上的结点就是那些Data Region的信息。实际上就是把线性的表达转移到用户存储的Data Reigon中，并且使用树这种更高效的数据结构来存储。&lt;/p&gt;
&lt;p&gt;另一种思路也是类似于内存的表达，就是&lt;strong&gt;段表达&lt;/strong&gt;，即通过记录文件保存在哪些连续的Data Region段中，比如（8,20），（30,31）。段表达在大部分数据是连续时具有更好的性能。并且，因为连续的文件对于读写性能的提升很大，所以操作系统在分配文件空间时，往往也会选择将一个文件分配在连续的Data Region中，段表达在这种背景下，会是一个非常好的选择。&lt;/p&gt;
&lt;h2&gt;目录的保存&lt;/h2&gt;
&lt;p&gt;这里其实很简单，目录作为一个树状结构，也是保存在用户数据Data Region中的一颗树，其中每个节点保存的就是对应的那个文件的Inode。&lt;/p&gt;
&lt;h2&gt;杂项&lt;/h2&gt;
&lt;p&gt;后面的内容和东西很杂，毕竟这一章整章也只是为了开阔思路带你思考文件系统实现的要点，所以这边比较零散的总结一些点。&lt;/p&gt;
&lt;h4&gt;访问一个文件的IO次数要比想象的多得多&lt;/h4&gt;
&lt;p&gt;很容易根据Inode和Data Region的设计想到，读取或者写入一个文件需要非常多次的IO&lt;/p&gt;
&lt;p&gt;比如读文件，就至少需要遍历一次目录找到文件的Inode，然后从Inode中读取到文件存储的Data Region的位置信息，然后再去读取到该文件。&lt;/p&gt;
&lt;p&gt;写入则更复杂，除了读取的那些操作都需要操作一次外，还需要写入inode和Data Region的bitmap，此外，如果是创建文件的话，在目录树中也需要写入新文件的信息。&lt;/p&gt;
&lt;h4&gt;缓存&lt;/h4&gt;
&lt;p&gt;因为访问一个文件的IO远比想象的多，所以理所当然的会想到给一些必要的数据结构和信息加内存中的缓存，比如Inode，目录树，Inode记录的Data Region等。&lt;/p&gt;
&lt;p&gt;缓存也有2种，一种是在内存中使用固定数量的内存页专门做缓存，然后在这部分空间中使用一些页面汰换算法。&lt;/p&gt;
&lt;p&gt;大部分操作系统使用的是另一种，将这些数据结构也视为普通数据，作为正常的内存页加载到内存中，和普通的内存页一起参与汰换，这样的优点就是更加灵活，也避免了固定内存做缓存的多余空间浪费。&lt;/p&gt;
&lt;h4&gt;缓冲&lt;/h4&gt;
&lt;p&gt;前面提过，在发生写时，操作系统并不一定立马执行，而是可能将一堆写请求先放在buffer缓冲中，然后固定时间（一般是5-30秒）或者当buffer满了时，统一处理这些写请求。这样的好处是可以对IO次数以及写性能进行显著的优化。比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以将连续的写请求合并成一个提交给硬盘，而不是多次，这样硬盘也会更好调度。&lt;/li&gt;
&lt;li&gt;如果一个文件先创建后马上删除，那么这个写实际是可以取消的，避免了无效写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，如果你想要强保存，那么可以使用fsync()来直接写入物理硬盘，许多软件支持的强力保存以及当你使用Ctrl+S时，就是基于该API来实现的。&lt;/p&gt;
</content:encoded></item><item><title>记录一下Java或者后端系统一些场景下的一些开源解决方案</title><link>https://www.intotw.cn/posts/java-tools/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-tools/</guid><pubDate>Tue, 22 Oct 2024 02:28:25 GMT</pubDate><content:encoded>&lt;h2&gt;hazelcast&lt;/h2&gt;
&lt;p&gt;https://github.com/hazelcast/hazelcast&lt;/p&gt;
&lt;p&gt;Hazelcast 是一个开源的分布式计算和内存数据存储平台，通常用于构建高可用性、低延迟的分布式应用程序。它提供了一个内存数据网格（IMDG，In-Memory Data Grid）和一个分布式流处理引擎，支持在多个节点之间共享数据和计算任务。&lt;/p&gt;
&lt;p&gt;优点：java原生，和java亲和性好，，对比redis还支持一定的分布式计算&lt;/p&gt;
&lt;p&gt;缺点：如果只用来分布式缓存的话，对比redis稍微有点重&lt;/p&gt;
&lt;h1&gt;Byte Buddy&lt;/h1&gt;
&lt;p&gt;https://github.com/raphw/byte-buddy&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Byte Buddy&lt;/strong&gt; 是一个用于创建、修改和操作 Java 类及其字节码的库。它允许开发人员在运行时生成新的类或修改现有类的行为，而无需手动编写或理解复杂的字节码指令。&lt;/p&gt;
&lt;p&gt;这个应该是写agent或者字节码最流行的库了。&lt;/p&gt;
&lt;h2&gt;若依&lt;/h2&gt;
&lt;p&gt;小作坊用的，开源&lt;a href=&quot;https://ruoyi.vip/&quot;&gt;RuoYi 若依官方网站|后台管理系统|权限管理系统|快速开发框架|企业 ...&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Doris&lt;/h2&gt;
&lt;p&gt;分布式大数据并行计算中间件，算是整套解决方案，带存储带计算。&lt;/p&gt;
&lt;p&gt;https://doris.apache.org/zh-CN/docs/1.2/summary/basic-summary&lt;/p&gt;
</content:encoded></item><item><title>其他博文精选</title><link>https://www.intotw.cn/posts/noteworthy-blog-picks/</link><guid isPermaLink="true">https://www.intotw.cn/posts/noteworthy-blog-picks/</guid><pubDate>Mon, 21 Oct 2024 03:56:01 GMT</pubDate><content:encoded>&lt;h1&gt;Software Engineer Titles Have (Almost) Lost All Their Meaning&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;软件工程师的Title已经快要失去其意义了&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.trevorlasn.com/blog/software-engineer-titles-have-almost-lost-all-their-meaning&quot;&gt;Software Engineer Titles Have (Almost) Lost All Their Meaning&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这篇文章表达了作者的观点：软件工程师的所谓title快要失去意义了。看来不只我个人这么觉得，老外现在也这么感觉。&lt;/p&gt;
&lt;p&gt;之前就感觉国内的title和划分很奇怪：工作经验3年就是高级工程师，工作5-10年就是资深或者技术专家。还有无处不在的“架构师”。&lt;/p&gt;
&lt;p&gt;这篇博主主要表达了几个观点，首先他认为高级工程师应该具备的能力&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;具备解决复杂和困难线上问题的经验以及技术积累&lt;/li&gt;
&lt;li&gt;具备设计一套可伸缩，可维护系统的远见&lt;/li&gt;
&lt;li&gt;具备在重构旧系统时，保持旧系统以及新系统之间微妙平衡（保持稳定）的视角和能力&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;其次， 他总结了一下，职业title膨胀的主要原因&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;初创公司不具备薪资条件，所以通过title的夸大作为货币补偿&lt;/li&gt;
&lt;li&gt;一些职业平台（如领英），将title作为个人品牌，这样每个人都希望自己有更好的形象&lt;/li&gt;
&lt;li&gt;HR为了对人才分类，也会创建大量的title&lt;/li&gt;
&lt;li&gt;许多公司将title作为奖杯一样形式的晋升内容来给予员工，混淆了title本身的价值&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;头衔膨胀不仅仅是名片或 LinkedIn 个人资料上的文字。这是一个触及我们行业完整性和功能性核心的问题。当我们夸大头衔时，我们本质上是在对自己和彼此撒谎，谎称我们的能力和经验。&lt;/p&gt;
&lt;p&gt;这种欺骗会产生实际后果。它造成期望与现实之间的不匹配，导致人们被置于他们没有准备好的角色。想象一下，一位拥有三年经验的“高级”工程师试图构建一个复杂的系统或指导初级开发人员。失败的可能性很高，个人承受的压力也很大。&lt;/p&gt;
&lt;p&gt;如何解决呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;对于那些担任领导职务的人来说，最重要的是抵制使用夸大的头衔来快速解决保留或招聘挑战的诱惑。相反，应专注于创建有意义的职业发展框架，将进步与具体技能和责任联系起来。考虑实施类似于大型科技公司所使用的系统，其中级别（如 L3、L4、L5）提供更细致的资历视图，而无需诉诸头衔膨胀。&lt;/li&gt;
&lt;li&gt;公司可以通过标准化其头衔结构并对每个级别的含义保持透明来表明立场。这可能涉及创建详细的职位描述，明确概述每个角色的期望和职责。通过这样做，您不仅可以为员工提供清晰的信息，还可以促进全行业对职称的更加标准化的理解。&lt;/li&gt;
&lt;li&gt;人力资源部门可以发挥关键作用。他们可以致力于开发更复杂的方法来对整个行业的角色进行分类和比较。这可能涉及与技术主管合作创建标准化技能矩阵，可用于更客观地评估候选人和员工。&lt;/li&gt;
&lt;li&gt;抵制头衔膨胀的公司获得了显著的竞争优势。通过保留有意义的头衔，他们吸引并留住了那些看重真正成长而非夸大角色的顶尖人才。这将导致更准确的招聘，改善团队动态，并提高生产力。现实主义游戏还能培养内部和客户之间的信任，将公司定位为行业中的诚信灯塔。最终，拥有明确且诚实的标题结构的公司能够组建更强大，更有能力的团队，并拥有卓越的声誉，从而在市场中脱颖而出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;个人感觉其中的高级工程师应该具备的能力以及膨胀的原因分析的很好，但是对于解决来说，恐怕这个问题需要的是整个行业整体的共识，而共识往往很难达到。专家、架构师、资深、CTO，经理。这些title应该还会在职场上飞挺久。&lt;/p&gt;
&lt;h1&gt;亿级订单系统的数据库查询性能优化之路&lt;/h1&gt;
&lt;p&gt;https://www.infoq.cn/article/a9O4pyXDm40uEIm0gW26&lt;/p&gt;
&lt;p&gt;京东零售的，写的比较实在，方案、思考、行文也比较&quot;&lt;strong&gt;接地气&lt;/strong&gt;&quot;&lt;/p&gt;
</content:encoded></item><item><title>分布式CAP理论-总结+延伸</title><link>https://www.intotw.cn/posts/distribution-cap-summary/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-cap-summary/</guid><pubDate>Fri, 11 Oct 2024 06:05:47 GMT</pubDate><content:encoded>&lt;p&gt;总结和延伸一下CAP理论。强烈建议先看一下之前翻译的一篇博客，对CAP理论先有一个基本的认识：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.intotw.cn/2024/10/11/distribution-cap-proof/&quot;&gt;（译）分布式CAP理论的图解证明&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;常见的分类&lt;/h2&gt;
&lt;p&gt;我们首先对常用的技术框架，进行一个CAP上的分类&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CA&lt;/th&gt;
&lt;th&gt;CP&lt;/th&gt;
&lt;th&gt;AP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;Zookeeper&lt;/td&gt;
&lt;td&gt;Eureka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;聊聊CA，数据库&lt;/h2&gt;
&lt;p&gt;其实一定程度来来说，满足CA的都是单点系统，这个在证明中其实可以看出来，满足持久化+可用性，就意味着节点必须不满足分区容忍性，那么就必定不支持分布式必须的场景：&lt;strong&gt;多节点组成集群&lt;/strong&gt;，所以满足CA的系统基本都是单点系统，即一些传统数据库&lt;/p&gt;
&lt;p&gt;传统数据库的CA显而易见，写入后立即读取到的就是新值（事务、写盘保证），并且系统保证高可用，对于任何请求都会在处理后马上返回。&lt;/p&gt;
&lt;p&gt;实际上，如果不具备任何分区容忍性的系统，可以说根本不具备形成分布式系统的条件，因为都没有分区容忍性了，node之间可以认为全部都是割裂的，只有单点最好满足CA。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CA&lt;/strong&gt;系统一般有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单机系统（没有P的容忍，何谈分布式集群，不过也有方法，后面会说）&lt;/li&gt;
&lt;li&gt;一般多是数据库系统，系统稳定性非常高&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;聊聊CP，强一致性&lt;/h2&gt;
&lt;p&gt;典型的CP就是Zookeeper，我们看看Zookeepr如何满足CP的：&lt;/p&gt;
&lt;p&gt;以下内容来自GPT：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1. 一致性（Consistency）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Zookeeper 保证了强一致性（linearizability）对于写操作的顺序和结果是严格的，遵循以下原则：&lt;/p&gt;
&lt;p&gt;​	•&lt;strong&gt;线性一致性&lt;/strong&gt;：Zookeeper 保证所有写操作以相同的顺序提交到各个节点。这意味着所有客户端无论连接到哪个节点，看到的写操作顺序都是相同的。&lt;/p&gt;
&lt;p&gt;​	•&lt;strong&gt;多数派机制&lt;/strong&gt;：Zookeeper 使用多数派（quorum）来确保数据的一致性。写操作必须在集群中超过一半的节点上提交成功，才能视为写成功。即使部分节点宕机，只要有超过半数的节点正常，Zookeeper 依然能保证数据一致性。&lt;/p&gt;
&lt;p&gt;​	•&lt;strong&gt;事务ID（ZXID）&lt;/strong&gt;：每个写操作在 Leader 节点上会分配一个全局唯一的事务ID（ZXID），并按照严格的顺序分发给 Follower 节点。Follower 在收到 Commit 消息后，会按照 ZXID 的顺序应用这些事务，确保数据一致。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 分区容忍性（Partition Tolerance）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在分布式系统中，网络分区是不可避免的。Zookeeper 通过以下机制来确保即使在网络分区的情况下，依然能提供一致性：&lt;/p&gt;
&lt;p&gt;​	•&lt;strong&gt;多数派机制&lt;/strong&gt;：即使部分节点因网络故障失联，只要集群中有超过半数的节点能彼此通信，Zookeeper 就能继续工作。不能与大多数节点通信的节点将停止处理请求，避免返回不一致的数据。&lt;/p&gt;
&lt;p&gt;​	•&lt;strong&gt;Leader选举&lt;/strong&gt;：当网络分区或 Leader 宕机时，Zookeeper 会通过选举机制确保集群中的存活节点选出一个新的 Leader。只有新的 Leader 才能处理写请求。网络分区导致的少数派节点无法选出新的 Leader，从而不会处理写请求，保证一致性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Zookeeper通过选举来确认leader，然后使用一主多从的方式来保障一致性，每次写只能在主节点上，此时主节点对从节点发起一次全局的commit以尝试在所有从节点上也写入新数据，只有在包含从节点以内半数以上节点写入成功，才认为这次写入成功了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结来说，Zookeeper通过以下途径保障了C（一致性）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只在主节点写入，从节点只能读（从节点可能有延迟，因为半数以上写入成功就写成功，失败的有追赶机制）&lt;/li&gt;
&lt;li&gt;写入时，半数以上节点写入才算写入成功&lt;/li&gt;
&lt;li&gt;如果有节点crash，从新根据最新数据的事务id的多数派去选举新leader（写入需要半数以上，那么有节点crash以后，必定有一半是最新状态）&lt;/li&gt;
&lt;li&gt;选举之类的通过zookeeper自己实现的类似paxios的分布式选举协议实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，zookeeper的一致性好像没有想象那么强，半数节点写入成功就算写成功，那么只针对从节点来说，有可能读到的不是实时的数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接下来我们来看下ZK的P（分区容忍性）&lt;/strong&gt;，其实也和选举有关，ZK具备一定的分区容忍性。通过选举的机制，那些有网络故障的节点因为失去leader的心跳退出集群、亦或者leader已经crash了，剩下的从节点们也会发起选举选出新的leader。在集群还保持在开始数量的一半以上时，ZK遭遇网络故障可以通过选举来确定新的leader，来保障整个集群具备分区容忍性。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;脑裂问题&lt;/strong&gt;。之所以说ZK具备一定的分区容忍性，是因为ZK在遭遇极端网络故障的情况下，可能会产生脑裂，即网络状况由一个集群分裂为了两个集群，出现了各自两个leader。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以，zookeeper的集群部署一般为奇数&lt;/strong&gt;。比如部署5台与4台，此时半数以上决策都需要至少3台存活节点，但是5台可以允许2个节点挂掉。同时，奇数可以避免脑裂问题，当集群划为两个分区：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5台的话至少会划分为3台与2台的各自2个分区，其中2台的分区无法选出主会宕机，3台的依旧可以达成共识继续提供服务。&lt;/li&gt;
&lt;li&gt;如果是4台，那么可能划分为2台与2台2个分区，这两个分区分别无法完成选举，集群宕机&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;我们看看ZK的可用性如何&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;ZK无法保证可用性，我们先看看读场景。虽然从节点可能读到旧数据，但是在需求强一致的情况下，可以使用sync()API来保障从节点同步了leader节点的数据，此时在强一致下就无法提供可用性了，因为需要从leader同步数据这一步，此时发生网络中断，那么就无论如何也不可能读到最新值了。&lt;/p&gt;
&lt;p&gt;写也是，在写的时候，如果发生了网络故障，半数以上节点无法确认写，或者集群正在进行选举，那么请求就不能被正确响应。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CP&lt;/strong&gt;系统一般都有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主从模式&lt;/li&gt;
&lt;li&gt;具备相应的选举以及事务机制&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;聊聊AP&lt;/h2&gt;
&lt;p&gt;比较典型的AP应该就是Spring Cloud的Eureka注册中心了。Eureka可以在CP和AP之间横跳，取决于它的&lt;strong&gt;保护模式&lt;/strong&gt;。在保护模式打开的情况下，如果丢失了一些实例的心跳，那么它会保留一段时间实例的信息（偏向于认为是子暂时网络故障，而不是马上认为实例不可用剔除掉），来实现AP。&lt;/p&gt;
&lt;p&gt;但是显然，此时返回的数据会各不相同，比如实例A注册到了注册中心E1和E2，实例B去E1查询实例A时发现实例A已下线，而去E2查询时却发现实例A又在线。这显然不满足一致性：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个读操作发生在写操作完成之后，那么这个读操作返回的必须是这个写操作的值，或者是最后一个写操作值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;AP&lt;/strong&gt;系统一般有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无主模式&lt;/li&gt;
&lt;li&gt;实例间一般通过类似广播的模式通信（注册中心之间），信息冗余&lt;/li&gt;
&lt;li&gt;存在重试机制（例如eureka，在请求的某台实例不可用时，会自动重试几次，实际就是考虑了AP的问题）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;加深理解&lt;/h2&gt;
&lt;h3&gt;不要把CAP的概念搞混淆&lt;/h3&gt;
&lt;p&gt;CAP中因为所谓的持久性、可用性这两个特性，经常会在其他场景用到，所以很容易搞混淆。我们要记住分布式CAP讨论的这两个定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个读操作发生在写操作完成之后，那么这个读操作返回的必须是这个写操作的值，或者是最后一个写操作值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;每个对于正常节点（non-failing node ）的请求，都可以收到正常的响应&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果抛开这两个定义，经常就会讨论错重点，比如一致性，只表达分布式系统响应成功了就应该具备一致性。但是很容易混淆其他概念，就混淆到了数据是在内存还是落盘了，还是写日志了。进入到这种更细的讨论，往往混淆了持久化与一致性在ACID和分布式集群中表达的不同含义的概念。&lt;/p&gt;
&lt;h3&gt;CAP的结合&lt;/h3&gt;
&lt;p&gt;CAP不能同时完美存在，但是经常的设计是让那个不能存在的属性以较弱的方式去呈现。&lt;/p&gt;
&lt;h4&gt;CA弱P&lt;/h4&gt;
&lt;p&gt;比如前面讨论CA时说的，CA基本是单机系统，但是也可以集群，只是分区容忍很差。比如古早使用MySQL的binlog订阅来实现主从，以及使用keepalived+MySQL多主互相订阅来实现集群的HA（高可用）。&lt;/p&gt;
&lt;p&gt;这些方案可以让CA实现集群，但是显而易见的是，通过binlog（实质是数据复制）的方式，分区容忍性及其的低。但是也可以通过代理层+心跳等方式，使这样的集群具备更高的分区容忍性。&lt;/p&gt;
&lt;h4&gt;CP弱A&lt;/h4&gt;
&lt;p&gt;CP弱A一般是通过对CP，尤其是其中C的牺牲，来换来一定的A。比如&lt;strong&gt;写操作&lt;/strong&gt;可以开启纯异步同步（没具体查ZK支不支持，但是包括MQ，ES，都是这个套路），来保障leader节点写入即可，其他节点交给集群内部异步同步。&lt;strong&gt;读操作&lt;/strong&gt;可以通过允许读到旧数据，来达成弱A。&lt;/p&gt;
&lt;h4&gt;AP弱C&lt;/h4&gt;
&lt;p&gt;这里说下之前待过的一个架构组的一个方案，之前待得某家公司用Spring Cloud时，不太能忍受发布时节点下线但是调用端延迟感知的问题（我寻思这本身不是AP的优点吗），所以架构组修改了实例销毁的钩子，增加了一个发送MQ的功能，然后由另外的应用去消费MQ再去Eureka注册中心调用api下线该实例。&lt;/p&gt;
&lt;p&gt;这里不谈这个方案的好坏，但是可以看到纯AP的环境下，也可以通过一些其他方式来保障一定的C。&lt;/p&gt;
&lt;h4&gt;AP弱C-最终一致性&lt;/h4&gt;
&lt;p&gt;还有就是经典的&lt;strong&gt;最终一致性&lt;/strong&gt;场景，我们的应用和分布式事务在考虑高并发和高可用的情况下，大多使用消息来实现最终一致性，现在你可以很轻松的分析了。&lt;/p&gt;
&lt;p&gt;举例说明，比如下单和支付场景，假如其中使用了消息来保障最终一致性，那么在消息有一定积压的情况下，就可能会出现存在支付订单已支付但是购物订单还是待支付状态的情况，此时显然不满足一致性。&lt;/p&gt;
&lt;p&gt;这是允许一致性的延迟，来提供高可用（实例处理完后丢mq同步即可，不需要等待下游业务系统状态一致）。并且通过中间件的解耦，来提升分区容忍性（依旧是丢mq即可，下游系统哪怕5分钟内不可用，也可以在恢复后通过mq达成最终一致）。&lt;/p&gt;
&lt;h3&gt;具体情况具体分析&lt;/h3&gt;
&lt;p&gt;CAP表明不能同时满足这三个属性，但是不代表一个系统不能一定程度上具备这三个属性，只是无法完美的对标上定义，所以具体情况要具体分析。&lt;/p&gt;
&lt;p&gt;举一个具体情况具体分析的经典例子：ES满足CAP中的哪几个？&lt;/p&gt;
&lt;p&gt;我们看看stackoverflow上的经典回答，可以看一下其他人讨论CAP问题的方式：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The answer is not so straightforward. It depends upon how the system is configured and how you want to use it. I will try to go into the details.&lt;/p&gt;
&lt;p&gt;答案并不简单。这取决于系统的配置方式以及你打算如何使用它。我会尝试详细说明。&lt;/p&gt;
&lt;h3&gt;Paritioning in ElasticSearch&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Each index is partitioned in shards, meaning data in each shard is mutually exclusive to other shards. Each shard further has multiple Lucence indices, which are not in the scope of this answer.&lt;/p&gt;
&lt;p&gt;ElasticSearch中的分片 每个索引都被分成多个分片，这意味着每个分片中的数据是彼此独立的。每个分片还有多个Lucene索引，但这不在本文讨论范围内。）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Each shard can have a replica running (most setups have) and in an event of a failure, the replica can be promoted to a master. Let&apos;s call a shard that has a &lt;strong&gt;primary&lt;/strong&gt; working and is reachable from the ES node that our application server is hitting as an &lt;strong&gt;Active shard&lt;/strong&gt;. Thus, a shard with &lt;strong&gt;no copies&lt;/strong&gt; in primary and is not reachable is considered as &lt;strong&gt;failed shard&lt;/strong&gt;. (Eg: An error saying &quot;all shards failed&quot; means no primaries in that index are available)&lt;/p&gt;
&lt;p&gt;每个分片都可以有一个副本在运行（大多数设置都有），如果发生故障，副本可以被提升为主分片。我们将一个主分片正常工作且可以从应用服务器连接的ES节点访问的分片称为“活跃分片”。因此，主分片没有副本且无法访问的分片被认为是“失效分片”。（例如，显示“所有分片失效”的错误意味着该索引中没有可用的主分片）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ES has a feature to have multiple primaries (&lt;strong&gt;divergent shards&lt;/strong&gt;). It is not a good situation as we lose both read/write consistencies.&lt;/p&gt;
&lt;p&gt;ES有一个功能可以允许多个主分片共存（分裂分片）。这不是一种理想的情况，因为我们会失去读写一致性。&lt;/p&gt;
&lt;p&gt;读场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;By default reads will continue to happen on shards that are active. Thus, the data from failed shards will be excluded from our search queries. In this context, we consider the system to be &lt;strong&gt;AP&lt;/strong&gt;. However, the situation is temporary and does not require manual effort to synchronize shard when the cluster is connected again.&lt;/p&gt;
&lt;p&gt;默认情况下，读取操作将继续在活跃的分片上进行。因此，失效分片中的数据将不会包含在我们的搜索查询中。在这种情况下，我们认为系统是AP的。然而，这种情况是暂时的，并且在集群重新连接时，无需手动同步分片。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;By setting a search option allow_partial_search_results [1] to false, we can force the system to error when some of the shards have failed, guaranteeing consistent results. In this context, we consider the system to be CP.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过将搜索选项 allow_partial_search_results [1] 设置为 false，我们可以在某些分片失效时强制系统报错，从而保证结果的一致性。在这种情况下，我们认为系统是CP的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In case no primaries are reachable from the node(s) that our application server is connecting to, the system will completely fail. Even if we say that our partition tolerance has failed, we also see that availability has taken a hit. This situation can be called be just C or CP.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果应用服务器所连接的节点无法访问任何主分片，系统将完全失效。即使我们说分区容忍性失效了，我们也可以看到可用性受到了影响。这种情况可以称为C或CP。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;There can be cases where the team has to anyhow bring up the shards and their out of sync replica(s) were reachable. So they decide to make it a primary (manually). Note that there can be some un-synced data resulting in divergent shards. This results in the AP situation. Consistency will be hard to restore when the situation normalizes (sync shards manually).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在某些情况下，团队必须强行启动分片，而它们的不同步副本是可访问的。因此，团队决定手动将副本提升为主分片。需要注意的是，可能存在一些未同步的数据，导致分片分裂。这会导致AP的情况。当情况恢复正常时，一致性将很难恢复（需要手动同步分片）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;写场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Only if all shards fail, writes will stop working. But even if one shard is active, writes will work and are consistent (by default). This will be CP.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有当所有分片失效时，写入操作才会停止。但是即使有一个分片是活跃的，写入操作仍然可以进行并且是一致的（默认情况下）。这将是CP的情况。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;However, we can set option index-wait-for-active-shards [2] as all to ensure writes only happen when all shards in the index are active. I only see a little advantage of the flag, which would be to keep all shards balanced at any cost. This will be still CP (but lesser availability than the previous case).&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然而，我们可以将选项 index-wait-for-active-shards [2] 设置为 all，以确保只有当索引中的所有分片都是活跃状态时才会进行写入。我认为这个选项的好处很有限，只是为了不惜一切代价保持所有分片的平衡。这样系统仍然是CP的（但比前一种情况的可用性更低）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Like in the last read network partition case, if we make un-synced replicas as primary (manually) there can be some data loss and divergent shards. The situation will be AP here and consistency will be hard to restore when the situation normalizes (sync shards manually).&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;就像上一个读取网络分区的情况一样，如果我们手动将未同步的副本提升为主分片，可能会出现数据丢失和分片分裂的情况。在这里，系统将处于AP状态，并且在情况恢复正常时，一致性将很难恢复（需要手动同步分片）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;结合前文就讨论过的场景，许多时候你开启某些配置，他就更加CP了，关闭了，则更加AP，这取决于你更希望你的系统在哪方面具备更好的适应性。分布式系统与操作系统一样，因为CAP理论存在，分布式系统重永远在trade-off，没有银弹。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过本文和之前翻译的CAP理论的证明，希望大家都对CAP理论和分布式系统设计中对CAP的trade-off有更深的理解，也可以提升分析分布式系统中CAP三个要素的能力。&lt;/p&gt;
</content:encoded></item><item><title>（译）分布式CAP理论的图解证明</title><link>https://www.intotw.cn/posts/distribution-cap-proof/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-cap-proof/</guid><pubDate>Fri, 11 Oct 2024 02:34:47 GMT</pubDate><content:encoded>&lt;p&gt;译自&lt;a href=&quot;https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/&quot;&gt;An Illustrated Proof of the CAP Theorem&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本来想联系下博主申请下搬运和翻译的授权，但是没找到博主的任何邮箱之类的联系方式，侵删。&lt;/p&gt;
&lt;h1&gt;正文&lt;/h1&gt;
&lt;p&gt;CAP理论是分布式系统中的基石理论，它表明：任何一个分布式系统最多只能拥有以下3个属性中的2个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;持久性（&lt;strong&gt;C&lt;/strong&gt;onsistency）&lt;/li&gt;
&lt;li&gt;可用性（&lt;strong&gt;A&lt;/strong&gt;vailability）&lt;/li&gt;
&lt;li&gt;分区容忍性（&lt;strong&gt;P&lt;/strong&gt;artition tolerance）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文将通过图解对&lt;a href=&quot;http://lpd.epfl.ch/sgilbert/pubs/BrewersConjecture-SigAct.pdf&quot;&gt;Gilbert and Lynch&apos;s specification and proof of the CAP Theorem&lt;/a&gt;进行总结。&lt;/p&gt;
&lt;h2&gt;什么是CAP理论？&lt;/h2&gt;
&lt;p&gt;CAP理论声明一个一个分布式系统不可能同时具备&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;。理论定义听起来足够简单，但是其中的&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;分别代表着什么？&lt;/p&gt;
&lt;p&gt;在这节中，我们将介绍一个简单的分布式系统，并且解释对于一个分布式系统来说，&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;分别意味着什么。更加正式的描述，请参考&lt;a href=&quot;http://lpd.epfl.ch/sgilbert/pubs/BrewersConjecture-SigAct.pdf&quot;&gt;Gilbert and Lynch&apos;s paper&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;分布式系统&lt;/h2&gt;
&lt;p&gt;让我们来看一下最简单的分布式系统，我们的系统由两台服务器组成，G1和G2。这两台服务器都维护着一个变量：v，并且v在一开始的值为v0。G1与G2服务器之间可以互相通信，并且他们都可以与外部的client客户端通信。就和下图一样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/a303459f812e095fa8484e26a63caded.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;client端可以发送写请求和读请求到任意server。当server接收到请求以后，会执行具体的计算并且返回给client端。如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/1285a9462b6bf5efaeb8951aa17e7eb6.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/a25b5d3e938203a33df4d362f0e14a0f.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/40587c2bea8a229bd5d52856364a6220.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;读操作的情况如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/416bb04a8902dbdd995abdb4ecea74fb.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/ef4b436ccba93c37dc04aef6f7696a4c.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在我们建立了一个基本的分布式系统，让我们在这套系统之上来探讨分布式系统的&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;持久性&lt;/h2&gt;
&lt;p&gt;Gilbert and Lynch对分布式系统持久性的定义如下&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当一个读操作发生在写操作完成之后，那么这个读操作返回的必须是这个写操作的值，或者是最后一个写操作值&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在分布式系统重，一旦一个client写了一个值到任意server并且得到了server的返回。在这之后，这个client期望从任意server得到刚才写的值（或者是更加新的值）。&lt;/p&gt;
&lt;p&gt;下图描述了一个不保证持久性（&lt;strong&gt;inconsistent&lt;/strong&gt;）的系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/1285a9462b6bf5efaeb8951aa17e7eb6.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/a25b5d3e938203a33df4d362f0e14a0f.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/40587c2bea8a229bd5d52856364a6220.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/6079756780d64361b0314255ffc655b6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/a7c599b90e2bcf3fd37a1bf03b2ff336.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们的client将v1写入到了G1，并且G1给予了响应，但是当client后续从G2读取数据时，它得到了旧的数据：v0。&lt;/p&gt;
&lt;p&gt;让我们来看下一个具备持久性（&lt;strong&gt;consistent&lt;/strong&gt;）的分布式系统：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/1285a9462b6bf5efaeb8951aa17e7eb6.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/a25b5d3e938203a33df4d362f0e14a0f.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/aedc8a7b37ed605f8bd3c292786be04b.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/9f8be2e79b8f66c227b22d6ad095f47b.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/3b8a4bd4fee08f484ae4f9210617a14b.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/66dea1814830fe25f1cc75b22a1bbc52.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/afbdd2d85926db26bd1e07c0a341d00d.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/16dc1d5b37e505aa9f186e52e55dc4b5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具备持久性的系统中，在回应client前，G1先将它的值复制到了G2中，然后再回应client端。因此，当client从G2读取值时，client得到了最新的值：v1&lt;/p&gt;
&lt;h2&gt;可用性&lt;/h2&gt;
&lt;p&gt;Gilbert and Lynch 对可用性的定义如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每个对于正常节点（non-failing node ）的请求，都可以收到正常的响应&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在具备可用性的系统中，如果我们的client发送了一个请求到server，并且那个server还在正常运作（not crashed），那么这个server此时最终总会给予client一个响应。此场景下，server是不能忽略任何一个client的请求的。&lt;/p&gt;
&lt;h2&gt;分区容忍性&lt;/h2&gt;
&lt;p&gt;Gilbert and Lynch 对分区容忍性的定义如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在节点的通信之间，分区容忍性允许任意节点的任意消息丢失&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这意味着，G1和G2之间的任意消息都可能被丢弃，如果所有的都被丢弃（最坏情况），那么此时系统的表现如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/3cfdc612dbba8ec0d37703f57db2febc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具备分区容忍性的话，我们的系统应当在发生任意网络分区的情况，都可以正确的执行。&lt;/p&gt;
&lt;h2&gt;证明&lt;/h2&gt;
&lt;p&gt;现在，我们对于&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;的基本概念已经十分熟悉了，我们可以证明一个系统不可能同时具备着三个属性。&lt;/p&gt;
&lt;p&gt;我们用反证法，假设一个系统同时具备了这三个性质。那么针对这样一个具备了这三个性质的系统，我们首先考虑它的&lt;strong&gt;分区容忍性&lt;/strong&gt;，它的系统之间应该是分区的，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/3cfdc612dbba8ec0d37703f57db2febc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后，我们让client发送一个写请求更新v1到G1，因为我们的系统具备&lt;strong&gt;可用性&lt;/strong&gt;，所以这个写请求必须得到G1的返回。但是&lt;em&gt;我们的系统此时因为具备&lt;strong&gt;分区容忍性&lt;/strong&gt;，是&lt;strong&gt;分区的&lt;/strong&gt;&lt;/em&gt;，那么G1就无法将数据复制到G2，Gilbert 和 Lynch将这个阶段称之为α1。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/b89399a21f25f2efeb05cb93953eb942.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/4e5987dac3d0a665b75924e9cdb1f3b3.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/327bbf047be3ddb75e578c6458b24515.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来，我们的client端发送一个读请求到G2，因为我们的系统具备&lt;strong&gt;可用性&lt;/strong&gt;，G2&lt;strong&gt;必须可以进行响应&lt;/strong&gt;。但是又因为&lt;em&gt;我们的系统此时因为具备&lt;strong&gt;分区容忍性&lt;/strong&gt;，是&lt;strong&gt;分区的&lt;/strong&gt;&lt;/em&gt;，G2无法从G1更新值，那么G2只能返回旧的值v0，Gilbert 和 Lynch将这个阶段称之为α2&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/e2e733243863bb17b68c1a00bc26cf25.webp&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/5e4e3f87706d495be0a9c67ff4672337.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;G2在G1已经响应了写请求后，返回了v0给client端，&lt;strong&gt;打破了持久性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;我们假设了&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;同时存在一个系统，但是我们证明了这样的一个系统存在一些执行后会导致系统不再具备&lt;strong&gt;持久性&lt;/strong&gt;。因此，我们证明了同时具备&lt;strong&gt;持久性、可用性、分区容忍性&lt;/strong&gt;的系统，是不存在的。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;翻译这篇文章的原因是意外间看到了，其中对于CAP分别的解释以及证明比起中文社区的一些只言片语以及咬文嚼字要清晰很多。&lt;/p&gt;
&lt;p&gt;结合工作中的cap以及本文，笔者会再做一篇扩展，希望大家都能真正理解分布式系统设计的一些难点和考量。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;todo&quot;&gt;&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>读书笔记-局外人</title><link>https://www.intotw.cn/posts/book-jwr/</link><guid isPermaLink="true">https://www.intotw.cn/posts/book-jwr/</guid><pubDate>Tue, 08 Oct 2024 02:07:07 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/10/ccbf4068242dc089f24df26f29564f9e.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;国庆看了一下加缪的《局外人》，之所以想看这本书，是因为我个人经常也会有个人与社会异化的感觉。并且时常对社会上的一些道德伦理习俗觉得荒谬。在了解到这本书以后，就对这本书产生了兴趣。&lt;/p&gt;
&lt;p&gt;小说的第一句就非常能体现作者老辣的文笔，并且将读者带入主角“我”那种异化以及荒诞的的精神世界。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;今天，妈妈死了。也许是在昨天，我搞不清。我收到养老院的一封电报：“令堂去世。明日葬礼。特致慰唁。”它说得不清楚。也许是昨天死的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;人物&lt;/h1&gt;
&lt;p&gt;小说主要讲了主角的几段关系，并且在描写中体现主角的异化和“冷漠”。&lt;/p&gt;
&lt;p&gt;第一段，描述了主角参加母亲的葬礼，在本应表达痛苦悲伤的场景下，主角“我”只觉得守夜的那晚，小棚子里很闷、一起守夜的养老院中的老人很吵闹、时间很漫长。在次日早晨送母亲下葬的路上，“我”只觉得太阳太晒，路途太远，柏油路被晒得发烫，路途是如此折磨，以至于“我”完全没有任何“悲痛”的心情。&lt;/p&gt;
&lt;p&gt;第二段，从母亲葬礼回来后，“我”和女友玛丽去了海边，肉体激情的戏耍，然后一起去看了电影，“我”觉得很开心。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当我们穿上衣服的时候，她见我系着黑领带，显得有点诧异，问我是不是在戴孝。我对她说妈妈死了。她想知道是什么时候，我告诉她：“就是昨天。”她吓得往后一退，但没有发表什么意见。我想对她说这不是我的过错，但我没有说出口，因为我想起我对老板也这么说过。其实说这个毫无意义，反正，人总得有点什么错。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第三段，”我“的两个邻居：沙拉玛诺老头和雷蒙。沙拉玛诺老头养了一只狗，但是他却天天打骂他的狗。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人们老见他俩沿着里昂街而行，那狗拖拽着老头，搞得他蹒跚趔趄，于是，他就打狗、骂狗。狗吓得趴在地上，由主人拖着走，这时，该老头去拽它了。过一会儿，狗忘得一干二净，再次拽起主人来了，主人就再次对它又打又骂。这样一来，他们两个就停在人行道上，你瞪着我，我瞪着你，狗是怕，人是恨。天天如此，日复一日。有时狗要撒尿，老头偏不给它时间，而是硬去拽它，这畜生就沥沥拉拉撒了一路。如果它偶尔把尿撒在屋里，更要遭一顿狠打。这样的日子已经过了八年。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至于雷蒙，附近一带的人都说，他是靠女人生活，他却说自己是“仓库管理员”，他最近因为女人惹上了阿拉伯人。我因为他热情的邀请我去他家喝酒吃香肠，我就听他倾诉了很多，在离开时，我们已经成为“好朋友”了。&lt;/p&gt;
&lt;p&gt;之后的故事，慢慢开始走向了结尾。&lt;/p&gt;
&lt;p&gt;沙拉马诺的狗丢了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在房门口，我遇见了沙拉玛诺老头儿。我请他进去，他告诉我，他的狗的确丢了，因为它不在招领处。那里的管理人员对他说，那狗或许是被车轧死了。他问到警察局去是否可以打听得清楚。人家告诉他说，这类鸡毛蒜皮的事是不会有记录的，因为每天司空见惯。我安慰沙拉玛诺老头儿说，他满可以另外再养一条狗，可是，他提请我注意，他已经习惯跟这条狗在一起了，他这话倒也言之有理。&lt;/p&gt;
&lt;p&gt;我蹲在床上，沙拉玛诺坐在桌子前的一把椅子上。他面对着我，双手搁在膝盖上。他戴着他那顶旧毡帽，发黄的小胡子下，嘴巴在咕哝咕哝，语不成句。我有点儿嫌他烦，不过，此时我无事可做，又没有睡意，所以没话找话，就问起他的狗来。他告诉我，自从老婆死后，他就养了那条狗。他结婚相当晚。年轻时，他一直想要弄戏剧，所以在军队里的时候，他是歌舞团的演员。但最后，他却进了铁路部门。对此，他不后悔，因为现在他享有一小笔退休金。他和老婆在一起并不幸福，但总的来说，他俩过习惯了。老婆一死，他倒特感孤独。于是，他便向同事要了一条狗，那时，它还很小，他得用奶瓶给它喂食，因为狗比人的寿命短，所以他们就一同都老了。“它的脾气很坏，”沙拉玛诺老头儿说，“我经常跟它吵架。不过，它终归还是一条好狗。”我说它是条良种狗，沙拉玛诺听了显得很高兴，“您还没有在它生病之前见过它呢，它那身毛可真漂亮。”自从这狗得了这种皮肤病之后，他每天早晚两次给它涂抹药膏。但是在他看来，它真正的病是衰老，而衰老是治不好的。&lt;/p&gt;
&lt;p&gt;这时，我打了个哈欠，沙拉玛诺老头儿说他该走了。我对他说他还可以再待会儿，我对他狗的事感到难过。对此，他谢了谢我。他还说我妈妈很喜欢他的那条狗。说到妈妈，他称之为“您那可怜的母亲”，他想必我在丧母之后一定很痛苦，说到这里，我没有吱声。这时，他急促而不自然地对我说，他知道附近这一带的人对我颇有非议，只因我把我妈妈送进了养老院，但他了解我的为人，知道我对妈妈的感情很深。我回答说，我对这种非议迄今一无所知。既然我雇不起人去伺候我妈妈，我觉得送她进养老院是很自然的事（当时我为什么这么回答，现在我也说不清）。我还补充说，“很久以来，她一直跟我无话可说，她一人在家闷得很，到了养老院，至少可以找到伴。”这话不假，沙拉玛诺也这么说。然后，他起身告辞，想去睡。现在，他的生活发生了变化，他简直不知如何是好。他小里小气地向我伸出手来，这是我认识他以来他第一次这么做，我感到他手上有一块块硬痂。他微笑了一下，在走出房门之前，说：“我希望今天夜里外面那些狗不要叫，否则我会以为是我的狗在叫。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;结局的起因，“我”和玛丽相约，跟雷蒙一起去沙滩旁雷蒙朋友的家里度假，但是却遇到了阿拉伯人，双方发生冲突，雷蒙将枪交给了“我”，最后冲突停止了。&lt;/p&gt;
&lt;p&gt;但是“我”带着枪在海滩旁闲逛，又遇到了那个阿拉伯人，他拿出了刀，太阳晒得我发昏，“我”开枪杀了他。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;雷蒙把他的枪递给了我。阳光在枪上一闪。不过，双方都原地不动地站着，似乎周围的一切已把人严封密扎了起来。每一方都眼皮不眨，紧盯对手，在这里，大海、沙岸、阳光之间的一切仿佛都凝固不动，泉水声与芦苇声似乎也听不见了。这时，我思忖着，我既可以开枪，也可以不开枪。但是，突然间，两个阿拉伯人往后倒退，很快就溜到大岩石后面去了。于是，雷蒙和我也掉头往回撤。他显得高兴了些，还谈起回城去的公共汽车。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;他一看见我，稍稍欠起身来，把手伸进口袋。我呢，自然而然就紧握着衣兜里雷蒙的那把手枪。这时，那人又恢复原状躺下去，但仍把手放在口袋里。我离他还相当远，约有十来米。我隐约看见他的目光不时在细眯的眼皮底下一闪一闪。但更多的时候，我感到他的面孔在眼前一片燃烧的热气中跳动。海浪的声音更加有气无力，比中午的时候更为沉稳。太阳依旧，光焰依旧，一直延伸到跟前的沙滩依旧。已经有两个钟头了，白昼纹丝未动，已经有两个钟头了，白昼在沸腾着的金属海洋中抛下了锚。在天边，有一艘小轮船驶过，在我视野的边缘，我觉得它像是一个黑点，因为我一直正眼紧盯着那个阿拉伯人。&lt;/p&gt;
&lt;p&gt;我想，我只要转身一走，就会万事大吉了。但整个海滩因阳光的暴晒而颤动，在我身后进行挤压。我朝水泉迈了几步，那个阿拉伯人没有反应。不管怎么说，我离他还相当远。也许是因为他脸上罩有阴影，看起来他是在笑。我等他作进一步反应。太阳晒得我脸颊发烫，我觉得眉头上已聚满了汗珠。这太阳和我安葬妈妈那天的太阳一样，我的头也像那天一样难受，皮肤底下的血管都在一齐跳动。这种灼热实在叫我受不了，我又往前走了一步。我意识到这样做很蠢，挪这么一步无助于避开太阳。但我偏偏又向前迈出一步。这一下，那阿拉伯人并未起身，却抽出了刀子，在阳光下对准了我。刀刃闪闪发光，我觉得就像有一把耀眼的长剑直逼脑门。这时聚集在眉头的汗珠，一股脑儿流到眼皮上，给眼睛蒙上了一层温热、稠厚的水幕。在汗水的遮挡下，我的视线一片模糊。我只觉得太阳像铙钹一样压在我头上，那把刀闪亮的锋芒总是隐隐约约威逼着我。灼热的刀尖刺穿我的睫毛，戳得我的两眼发痛。此时此刻，天旋地转。大海吐出了一大口气，沉重而炽热。我觉得天门大开，天火倾泻而下。我全身紧绷，手里紧握着那把枪。扳机扣动了，我手触光滑的枪托，那一瞬间，猛然一声震耳欲聋的巨响，一切从这时开始了。我把汗水与阳光全都抖掉了。我意识到我打破了这一天的平衡，打破了海滩上不寻常的寂静，在这种平衡与寂静中，我原本是幸福自在的。接着，我又对准那具尸体开了四枪，子弹打进去，没有显露出什么，这就像我在苦难之门上急促地叩了四下。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;结局&lt;/h1&gt;
&lt;p&gt;“我”在监狱的心情，以及庭审的过程，非常有趣。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;除了这些烦恼，我还不算太不幸。最根本的问题，我再说一遍，仍是如何消磨时间。自从我学会了进行回忆，我终于就不再感到烦闷了。有时，我回想我从前住过的房子，我想像自己从一个角落出发，在房间里走一圈又回到原处，心里历数在每一个角落里见到的物件。开始，很快就数完一遍。但我每来一遍，时间就愈来愈长。因为我回想起了每一件家具，每一件家具上陈设的每一件物品，每一件物品上所有的局部细节，如上面镶嵌着什么呀，有什么裂痕呀，边缘有什么缺损呀，还有涂的是什么颜色、木头的纹理如何呀，等等。同时，我还试着让我的清单不要失去其连贯性，试着不遗漏每一件物品。几个星期之后，单单是数过去房间里的东西，我一数就能消磨好几个钟头。这样，我愈是进行回想，愈是从记忆中挖掘出了更多的已被遗忘或当时就缺乏认识的东西。于是我悟出了，一个人即使只生活过一天，他也可以在监狱里待上一百年而不至于难以度日，他有足够的东西可供回忆，决不会感到烦闷无聊。从某种意义上来说，这也是一种愉快。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;庭审时，是否有杀人动机以及是否是正当防卫反而不是辩论的重点，重点是“我”在母亲去世后葬礼上体现的冷漠，以及母亲葬礼后第二天马上和玛丽去纵情欢愉，体现了“我”就是一个冷血无情的人。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我擦了擦脸上的汗，直到我听见传唤养老院院长上庭作证时，我才稍微意识到自己所处的场合与处境。检察官问他我的妈妈对我是否常有怨言，他说是的，但又补充说，经常埋怨自己的亲人，这差不多是养老院的老人普遍都有的怪癖。庭长要他明确指出妈妈是否对我把她送进养老院一事有怨言，院长也回答说是。但对这个问题，他没有作补充说明。接着，庭长又向他提出另一个问题，对此，他回答说，他对我在下葬那天的平静深感惊讶。然后，他又被问及他所说的平静是指什么，他看了看自己的鞋尖，说是指我不愿意看妈妈的遗容，我没有哭过一次，下葬之后立刻就走，没有在坟前默哀。他说，还有一件事使他感到惊讶，那就是殡仪馆的人告诉他，我不知道妈妈的具体岁数。说到这里，大厅里一时寂静无声，庭长要养老院院长确认所讲的就是我，院长没有听清楚这个问题，牛头不对马嘴地回答说：“这就是法律。”接着，庭长又问检察官还有没有问题要问证人，检察官大声嚷道：“噢！没有了，这已经足够了。”他的声音如此响亮，他的目光如此扬扬得意，朝我一扫，使得我多年以来第一次产生了愚蠢的想哭的念头，因为我感到所有这些人是多么厌恶我。&lt;/p&gt;
&lt;p&gt;……&lt;/p&gt;
&lt;p&gt;检察官坐下了。我的律师已经按捺不住，他举起胳臂，法袍的袖子因此滑落下来，露出里面上了浆的衬衣的褶痕，他大声嚷道：“说到底，究竟是在控告他埋了母亲，还是在控告他杀了一个人？”听众哄堂大笑。但检察官又站了起来，披了披自己的法袍，高声宣称，只有您这位可敬的辩护律师如此天真无邪，才能对这两件事之间深层次的、震撼人心的、本质的关系视而不见，无动于衷。他声嘶力竭地喊道：“是的，我控告这人怀着一颗杀人犯的心埋葬了一位母亲。”这一声宣判，显然对全体听众起了很大的影响。我的律师耸了耸肩，擦了擦额头上的汗，看来他本人也颇受震撼，这时我感到我的事情不妙了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此，“我”得到了检察官和陪审团的评价：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“先生们，事情就是这样，”检察官说，“我给你们复述出全部事实的发展线索，说明此人完全是在神志清醒的状态中杀了人。我要强调这一点。因为这不是一桩普通的杀人案，不是一个未经思考、不是一个当时的条件情有可原、不是一个值得诸位考虑不妨减刑的罪行。先生们，此人，犯罪的此人是很聪明的。你们听他说过话没有？他善于应对，他很清楚每个字的分量。我们不能说他行动的时候不知他是在干什么。”&lt;/p&gt;
&lt;p&gt;我听着他侃侃而谈，听见了他说我这个人很聪明。但我难以理解，为什么一个普通人身上的优点，到了罪犯身上就成为了他十恶不赦的罪状。至少，他这种说法使我感到很惊诧，于是，我不去听检察官的长篇大论了，直到过了一会儿，我又听见他这样说：“难道此人表示过一次悔恨吗？从来没有，先生们，在整个预审过程中，此人从没有对他这桩可憎的罪行流露过一丝沉痛的感情。”说到这里，他向我转过身来，用手指着我，继续对我大加讨伐，真弄得我有些莫名其妙。当然，我不能不承认他说得有根有据。我对开枪杀人的行为，的确一直并不怎么悔恨。但他那么慷慨激昂，却使我感到奇怪。我真想亲切地，甚至是带着友情地向他解释，我从来没有对某件事真正悔恨过。我总是为将要来到的事，为今天或明天的事忙忙碌碌，操心劳神。但是，在我目前这种处境下，我当然不能以这种口吻对任何人说话。我没有权利对人表示友情，没有权利抱有善良的愿望。想到这里，我又试图去倾听检察官的演说，因为他开始评说我的灵魂了。&lt;/p&gt;
&lt;p&gt;他说他一直在研究我的灵魂，结果发现其中空虚无物。他说我实际上没有灵魂，没有丝毫人性，没有任何一条在人类灵魂中占神圣地位的道德原则，所有这些都与我格格不入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在确认死刑后，“我”在死刑前，有一位神父，被安排来“宽恕”我。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;他背对着我站了好久。他待在这里使我感到压抑，惹我恼火。我正要请他离开，不要再管我，他却转身向我，突然大声叫嚷了起来：“不，我不信您的话，我确信您曾经盼望过另外一种生活。”我回答说那是当然的，但那并不比盼望发财、盼望游泳游得更快，或者盼望自己长一张更好看的嘴巴来得更为重要。这都是一回事。他打断我的话，他想知道我是如何设想另一种生活的。于是，我朝他嚷了起来：“就是那种我可以回忆现在这种生活的生活。”立刻，我又对他说，我已经受够了。他还想跟我谈上帝，但我朝他逼近，试图最后一次向他说明我剩下的时间已经不多了，我不想浪费时间去跟上帝在一起。他企图变换话题，问我为什么称他为“先生”而不是“我的父亲”，这可把我惹火了，我对他说他本来就不是我的父亲，他到别人那里去当父亲吧。&lt;/p&gt;
&lt;p&gt;他把手放在我的肩上，说：“不，我的孩子，我在您这里就是父亲。但您不明白这点，因为您的心是迷茫的。我为您祈祷。”&lt;/p&gt;
&lt;p&gt;这时，不知是为什么，好像我身上有什么东西爆裂开来，我扯着嗓子直嚷，我叫他不要为我祈祷，我抓住他长袍的领子，把我内心深处的喜怒哀乐猛地一股脑儿倾倒在他头上。他的神气不是那么确信有把握吗？但他的确信不值女人的一根头发，他甚至连自己是否活着都没有把握，因为他干脆就像行尸走肉。而我，我好像是两手空空，一无所有，但我对自己很有把握，对我所有的一切都有把握，比他有把握得多，对我的生命，对我即将来到的死亡，都有把握。是的，我只有这份把握，但至少我掌握了这个真理，正如这个真理抓住了我一样。我以前有理，现在有理，将来永远有理。我以这种方式生活过，我也可能以另外一种方式生活。我干过这，没有干过那，我做过这样的事，而没有做过那样的事。而以后呢？似乎我过去一直等待的就是这一分钟，就是我也许会被判无罪的黎明。没有任何东西，没有任何东西是有重要性的，我很明白是为什么。他也知道是为什么。在我所度过的整个那段荒诞生活期间，一种阴暗的气息从我未来前途的深处向我扑面而来，它穿越了尚未来到的岁月，所到之处，使人们曾经向我建议的所有一切彼此之间不再有高下优劣的差别了，未来的生活也并不比我已往的生活更真切实在。其他人的死，母亲的爱，对我有什么重要？既然注定只有一种命运选中了我，而成千上万的生活幸运儿都像他这位神甫一样跟我称兄道弟，那么他们所选择的生活，他们所确定的命运，他们所尊奉的上帝，对我又有什么重要？他懂吗？大家都是幸运者，世界上只有幸运者。有朝一日，所有的其他人无一例外，都会判死刑，他自己也会被判死刑，幸免不了。这么说来，被指控杀了人，只因在母亲的葬礼上没有哭而被处决，这又有什么重要呢？沙拉玛诺的狗与他的妻子没有什么区别，那个自动机械式的小女人与马松所娶的那个巴黎女人或者希望嫁给我的玛丽，也都没有区别，个个有罪。雷蒙是不是我的同伙与塞莱斯特是不是比他更好，这有什么重要？今天，玛丽是不是又把自己的嘴唇送向另一个新默尔索，这有什么重要？他这个也被判了死刑的神甫，他懂吗？从我未来死亡的深渊里，我喊出了这些话，喊得喘不过气来。但这时，有人把神甫从我手中救了出去，看守们狠狠吓唬我。而神甫却劝他们安静下来，他默默地看了我一会儿。他眼里充满了泪水，他转过身去走开，消失掉了。&lt;/p&gt;
&lt;p&gt;他走了以后，我也就静下来了。我筋疲力尽，扑倒在床上。我认为我是睡着了，因为醒来时我发现满天星光洒落在我脸上。田野上万籁作响，直传到我耳际。夜的气味，土地的气味，海水的气味，使我两鬓生凉。这夏夜奇妙的安静像潮水一样浸透了我的全身。这时，黑夜将尽，汽笛鸣叫起来了，它宣告着世人将开始新的行程，他们要去的天地从此与我永远无关痛痒。很久以来，我第一次想起了妈妈。我似乎理解了她为什么要在晚年找一个“未婚夫”，为什么又玩起了“重新开始”的游戏。那边，那边也一样，在一个生命凄然而逝的养老院的周围，夜晚就像是一个令人伤感的间隙。如此接近死亡，妈妈一定感受到了解脱，因而准备再重新过一遍。任何人，任何人都没有权利哭她。而我，我现在也感到自己准备好把一切再过一遍。好像刚才这场怒火清除了我心里的痛苦，掏空了我的七情六欲一样，现在我面对着这个充满了星光与默示的夜，第一次向这个冷漠的世界敞开了我的心扉。我体验到这个世界如此像我，如此友爱融洽，觉得自己过去曾经是幸福的，现在仍然是幸福的。为了善始善终，功德圆满，为了不感到自己属于另类，我期望处决我的那天，有很多人前来看热闹，他们都向我发出仇恨的叫喊声。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;整本书中，从”我“的亲情、爱情、友情、死亡这些角度，描述了”我“这个人的”冷血“。”我“讨厌守灵，因为那让”我“觉得身体不适。”我“喜欢玛丽，因为那让”我“得到了肉体上的欢愉。”我“收获了友情，因为”我“想要去雷蒙那吃香肠和喝酒，在听雷蒙倾诉时，”我“获得了友情。&lt;/p&gt;
&lt;p&gt;最后，对感情以及社会如此冷漠的”我“，被社会判了死刑，被杀死了。&lt;/p&gt;
&lt;p&gt;”我“是多么独特的一个人，只在意自己的感受，对我自己有把握。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;他的神气不是那么确信有把握吗？但他的确信不值女人的一根头发，他甚至连自己是否活着都没有把握，因为他干脆就像行尸走肉。而我，我好像是两手空空，一无所有，但我对自己很有把握，对我所有的一切都有把握，比他有把握得多，对我的生命，对我即将来到的死亡，都有把握。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但是大家害怕这样的异类，会选择杀死他。&lt;/p&gt;
&lt;p&gt;”我“是一个极端的体现，实际上，我们每个人都在某些方面是”异类”。我们的存在，是为了取悦大众的伦理道德审美，还是单纯为了取悦我们自己？死亡和自由是否如此重要？我们无非是在重复过着差不多的同一天而已。早起上班，晚上休息。无尽的手游、网游、短视频。相似的景点、味道相近的食物。我们无非是在重复过着差不多的同一天而已。&lt;/p&gt;
</content:encoded></item><item><title>软件系统到底是在做什么？</title><link>https://www.intotw.cn/posts/tech-web-develop/</link><guid isPermaLink="true">https://www.intotw.cn/posts/tech-web-develop/</guid><pubDate>Tue, 24 Sep 2024 03:36:30 GMT</pubDate><content:encoded>&lt;p&gt;从下层来看，开发的职责是修正以及开发功能，但是将视角提高，软件系统到底是在做什么？&lt;/p&gt;
&lt;h2&gt;初级的软件系统&lt;/h2&gt;
&lt;p&gt;初级的软件系统，主要的目标停留在现实中传统行为的信息化上。&lt;/p&gt;
&lt;p&gt;这么说也许不是那么恰当，更恰当的说法是，软件系统的初级功能，是现实中传统行为的信息化上。&lt;/p&gt;
&lt;p&gt;借助计算机的以下功能，我们可以对一些传统行为进行信息化，以信息化提升效率：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;信息的存储&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;信息的传递&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据的计算&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;初级系统后续的演进&lt;/h2&gt;
&lt;p&gt;​	首先，在实现了基本的信息化以后，系统演进的方向就是通过对基本信息化实现进行结构以及功能上的优化，以此来实现对传统行为的革新&lt;/p&gt;
&lt;p&gt;​	然后，通过更新旧技术，以及对新技术的引进以及尝试，来进行不断的探索。尝试能否进一步提升传统行为的效率，能否对场景进行革新&lt;/p&gt;
&lt;p&gt;​	同时，谋求系统的通用以及适配化（一种抽象化的追求），以此来让系统的适用场景更多，扩大自己的影响力。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;让我们通过一些分析来更好的理解&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;打车系统&lt;/h3&gt;
&lt;p&gt;打车系统的发展是必定依赖于智能手机的发展的，因为其核心是信息的传递。 我们可以具体分析一下&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信息的存储&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这点老早就具备条件了，打车系统并不需要更特殊的信息存储&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据的计算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;地理科学以及计算机图论的发展，早已满足了打车系统的需要&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信息的传递&lt;/strong&gt;
打车系统极其依赖智能手机发展的原因是，智能手机大大提升了信息的传递效率，增加了信息的触点。&lt;/p&gt;
&lt;p&gt;我们来看下打车系统实现的传统行为的信息化是什么？如果没有打车软件，我们该如何打车？或许可以如此做：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打114，告知114你的位置，以及你需要打车&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;114的接线人员群发短信或者挨个拨打电话给出租车司机师傅，为你寻找一位师傅&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对应的师傅开车前来接你
仅从这个流程上来看，打车软件早就具备了出现的条件，也可以在这个场景实现传统行为的信息化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在打车软件下单&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;所有师傅收到下单通知，自主接单&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;接单后，前来接你&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心在于，智能手机出现前，没有人会抱着一台笔记本电脑来到处跑，并使用打车系统软件进行打车&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	从上面这个例子的分析，我们可以发现，信息的存储和数据的计算早已满足了大部分软件系统的前提条件，所以，所谓的互联网信息二次革命的本质，就是移动化的发展，大大扩大了信息传递触点，使得更多的传统行为可以被替代。它对软件系统真正影响的范围也十分明显：在支付、出行、生活、娱乐等领域，它催生了许多新的软件，是创世神，没有它，许多软件系统无法诞生。但是在电商、通讯等领域，他仅仅只是一种原有软件系统的革新。&lt;/p&gt;
&lt;h3&gt;saas系统&lt;/h3&gt;
&lt;p&gt;​	&lt;strong&gt;从所谓的saas系统来看&lt;/strong&gt;，我们来看一下系统的诞生和演进。&lt;/p&gt;
&lt;p&gt;saas实际是一种跳跃的产物，起初，他们面向某个具体行为来做了一套软件系统。这些行为主要集中在：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;企业进销存&lt;/li&gt;
&lt;li&gt;某些具体专业领域的系统，如金融。&lt;/li&gt;
&lt;li&gt;企业财务&lt;/li&gt;
&lt;li&gt;企业销售管理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	这符合软件系统的第一批发展：&lt;strong&gt;由企业购买计算机，并使用软件系统来提升企业运行效率&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	此时这些软件系统的演进与科技发展正相关很小，必然走向刚才说的情况：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;通过对基本信息化实现进行结构以及功能上的优化，以此来实现对传统行为的革新&lt;/li&gt;
&lt;li&gt;谋求系统的通用以及适配化（一种抽象化的追求），以此来让系统的适用场景更多，扩大自己的影响力。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	首先是第一点，在实现面向某家系统的软件系统后（例如财务），必定会寻求对流程以及系统的优化或者整合，让这套系统可以卖给更多的企业，让他们也是用这套系统来进行信息化。这类公司很多，只卖几套的是项目型公司，到最后造出一套较为通用卖的够多的，就是第一点发展到极致。类似的公司有：银行金融的某生和某达，ERP和税务的两个某友等。&lt;strong&gt;此时系统的特点就是：尝试抽象+定制&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;​	再结合第二点，如果这种抽象化的追求成功了，就发展成了saas，传统的售卖方式存在一种交付上的困难，一套基板+少量定制的方式，本质是在赚辛苦钱。如果抽象化成功了，就可以走云模式，部署一套，所有人使用同一套，数据隔离。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;所以你会发现saas经常会回到之前的问题&lt;/strong&gt;：抽象的不够好，又需要定制，定制完以后，又需要抽象，到最后干脆尝试交给用户，只做最低级别的抽象，也就发展到了低代码。&lt;/p&gt;
&lt;p&gt;​	综上所述，面对许多做saas的，如果你发现团队只有传统的业务知识，或者只有&lt;strong&gt;少数&lt;/strong&gt;的某几套&lt;strong&gt;定制&lt;/strong&gt;系统的知识，那么这个团队注定是难以成功的，因为saas的抽象本身就需要要么对于这个业务领域传统行为的惊人深入理解，要么就需要足够数量的定制系统以寻找抽象依赖的具象。&lt;/p&gt;
&lt;h3&gt;others，那些成熟的系统&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;通过更新旧技术，以及对新技术的引进以及尝试，来进行不断的探索。尝试能否进一步提升传统行为的效率，能否对场景进行革新
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	大部分成熟的系统，就进入了固定的营收以及这一步的探索。没事蹭蹭ai等新技术。优化下性能，上一下云服务，研究下服务网格，试一下领域驱动，尝试提供更好的稳定性，或者降低开发以及运维运营成本。比如某团，某宝，某饿，现阶段的打车系统&lt;/p&gt;
</content:encoded></item><item><title>读书笔记-人月神话</title><link>https://www.intotw.cn/posts/book-rysh/</link><guid isPermaLink="true">https://www.intotw.cn/posts/book-rysh/</guid><pubDate>Tue, 24 Sep 2024 03:14:43 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/09/a148224e765afd0c670ee4f384469969.webp&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是一本很散的书，记录和讨论了许多软件工程中会遇到的问题。作者对这些问题的本质进行了讨论，并且给出了自己认为的较好的处理方式。&lt;/p&gt;
&lt;p&gt;​	至于为什么这本书的观点非常散乱，可以参考这本书后大半段的大量讨论：&lt;strong&gt;没有银弹&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	所以这篇读后感，主要就是一些摘抄和观点的总结，并且导向另一篇在看这本书中笔者对于整个国内互联网软件系统发展的思考。&lt;/p&gt;
&lt;h2&gt;摘抄&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编程为什么有趣？作为回报，它的从业者期望得到什么样的快乐？首先是一种创建事物的纯粹快乐。如同小孩在玩泥巴时感到愉快一样，成年人喜欢创建事物，特别是自己进行设计。我想这种快乐是上帝创造世界的折射，一种呈现在每片独特、崭新的树叶和雪花上的喜悦1。其次，快乐来自于开发对其他人有用的东西。内心深处，我们期望其他人使用我们的劳动成果，并能对他们有所帮助。从这个方面，这同小孩用粘土为“爸爸办公室”捏制铅笔盒没有本质的区别。第三是整个过程体现出魔术般的力量——将相互啮合的零部件组装在一起，看到它们精妙地运行，得到预先所希望的结果。比起弹珠游戏或点唱机所具有的迷人魅力，程序化的计算机毫不逊色。第四是学习的乐趣，来自于这项工作的非重复特性。人们所面临的问题，在某个或其它方面总有些不同。因而解决问题的人可以从中学习新的事物：有时是实践上的，有时是理论上的，或者兼而有之。最后，乐趣还来自于工作在如此易于驾驭的介质上。程序员，就像诗人一样，几乎仅仅工作在单纯的思考中。程序员凭空地运用自己的想象，来建造自己的“城堡”。很少有这样的介质——创造的方式如此得灵活，如此得易于精炼和重建，如此得容易实现概念上的设想。（不过我们将会看到，容易驾驭的特性也有它自己的问题）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;对于创造者，只有在实现的过程中，才能发现我们构思的不完整性和不一致性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;总之，在众多软件项目中，缺乏合理的时间进度是造成项目滞后的最主要原因，它比其他所有因素加起来的影响还要大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;软件经理很早就认识到优秀程序员和较差的程序员之间生产率的差异，但实际测量出的差异还是令我们所有的人吃惊。在他们的一个研究中，Sackman、Erikson和Grand曾对一组具有经验的程序人员进行测量。在该小组中，最好的和最差的表现在生产率上平均为10:1；在运行速度和空间上具有5:1的惊人差异！简言之，$20,000/年的程序员的生产率可能是$10,000/年程序员的10倍。数据显示经验和实际的表现没有相互联系（我怀疑这种现象是否普遍成立。）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;对于效率和概念的完整性来说，最好由少数干练的人员来设计和开发，而对于大型系统，则需要大量的人手，以使产品能在时间上满足要求。如何调和这两方面的矛盾呢？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Mills建议大型项目的每一个部分由一个团队解决，但是该队伍以类似外科手术的方式组建，而并非一拥而上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;也就是说，同每个成员截取问题某个部分的做法相反，由一个人来进行问题的分解，其他人给予他所需要的支持，以提高效率和生产力。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;要特别注意传统的两人队伍与外科医生——副手队伍架构之间的区别。首先，传统的团队将工作进行划分，每人负责一部分工作的设计和实现。在外科手术团队中，外科医生和副手都了解所有的设计和全部的代码。这节省了空间分配、磁盘访问等的劳动量，同时也确保了工作概念上的完整性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;第二，在传统的队伍中大家是平等的，出现观点的差异时，不可避免地需要讨论和进行相互的妥协和让步。由于工作和资源的分解，不同的意见会造成策略和接口上的不一致，例如谁的空间会被用作缓冲区，然而最终它们必须整合在一起。而在外科手术团队中，不存在利益的差别，观点的不一致由外科医生单方面来统一。这两种团队组建上的差异——对问题不进行分解和上下级的关系——使外科手术队伍可以达到客观的一致性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;另外，团队中剩余人员职能的专业化分工是高效的关键，它使成员之间采用非常简单的交流模式成为可能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;扩建过程的成功依赖于这样一个事实，即每个部分的概念完整性得到了彻底的提高——决定设计的人员是原来的七分之一或更少。所以，可以让200人去解决问题，而仅仅需要协调20个人，即那些“外科医生”的思路。对于协调的问题，还是需要使用分解的技术，这在后续的章节中会继续进行讨论。在这里，可以认为整个系统必须具备概念上的完整性，要有一个系统结构师从上至下地进行所有的设计。要使工作易于管理，必须清晰地划分体系结构设计和实现之间的界线，系统结构师必须一丝不苟地专注于体系结构。总的说来，上述的角色分工和技术是可行的，在实际工作中，具有非常高的效率。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;如何得到概念的完整性？ 这样的观点是否要有一位杰出的精英，或者说是结构设计师的贵族专制，和一群创造性天分和构思被压制的平民编程实现人员？ 如何避免结构设计师产出无法实现、或者是代价高昂的技术规格说明，使大家陷入困境？ 如何才能与实现人员就技术说明的琐碎细节充分沟通，以确保设计被正确地理解，并精确地整合到产品中？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;对于给定级别的功能，能用最简洁和直接的方式来指明事情的系统是最好的。只有简洁（simplicity）是不够的，Mooers的TRAC语言和Algol 68用很多独特的基本概念达到了所需的简洁特性，但它们并不直白（straightforward）。要表达一件待完成的事情，常常需要对基本元素进行意料不到的复杂组合。而且，仅仅了解基本要素和组合规则还不够，还需要学习晦涩的用法，以及在实际工作中如何进行组合。简洁和直白来自概念的完整性。每个部分必须反映相同的原理、原则和一致的折衷机制。在语法上，每个部分应使用相同的技巧；在语义上，应具有同样的相似性。因此，易用性实际上需要设计的一致性和概念上的完整性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;因此，系统的结构师，如同建筑的结构师一样，是用户的代理人。结构师的工作，是运用专业技术知识来支持用户的真正利益，而不是维护销售人员所鼓吹的利益。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;体系结构同实现必须仔细地区分开来。如同Blaauw所说的，“体系结构陈述的是发生了什么，而实现描述的是如何实现3。”他举了一个简单的例子——时钟。它的结构包括表面、指针和上发条的旋钮。当一个小孩知道了时钟的外表结构，他很容易从手表或者教堂上的时钟辨认时间。而时钟的实现，描述了表壳中的事物——很多种动力提供装置中的一种，以及众多控制精度方案的一种。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;同样的划分方法也适用于编程系统。例如，美国的Fortran IV标准，是多种编译器所遵循的体系结构标准。该体系结构下有多种可能的实现：以文本为核心、以编译器为核心、快速编译和优化以及侧重语法的实现。相类似的，任何汇编语言和任务控制语言都允许有多种编译器或调度程序的实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;手册不但要描述包括所有界面在内的用户可见的一切，它同时还要避免描述用户看不见的事物。后者是编程实现人员的工作范畴，而实现人员的设计和创造是不应该被限制的。体系结构设计人员必须为自己描述的任何特性准备一种实现方法，但是他不应该试图支配具体的实现过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;周例会是每周半天的会议，由所有的结构师，加上硬件和软件实现人员代表和市场计划人员参与，由首席系统结构师主持。会议中，任何人可以提出问题和修改意见，但是建议书通常是以书面形式，在会议之前分发。新问题通常会被讨论一些时间。重点是创新，而不仅仅是结论。该小组试图发现解决问题的新方法，然后少数解决方案会被传递给一个和几个结构师，详细地记录到书面的变更建议说明书中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;随着实现的推进，无论规格说明已经多么精确，还是会出现无数结构理解和解释方面的问题。显然有很多问题需要文字澄清和解释，还有一些仅仅是因为理解不当。显然，对于存有疑问的实现人员，应鼓励他们打电话询问相应的结构师，而不是一边自行猜测一边工作，这是一项很基本的措施。他们还需要认识到的是，上述问题的答案必须是可以告知每个人的权威性结论。一种有用的机制是由结构师保存电话日志。日志中，他记录了每一个问题和相应的回答。每周，对若干结构师的日志进行合并，重新整理，并发布给用户和实现人员。这种机制很不正式，但非常快捷和易于理解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;这个故事在很多方面和不同层次都是非常深刻和富有教育意义的。让我们将它仅仅作为纯粹的工程项目，来看看有什么值得学习的教训。这个项目到底有多好的先决条件？他们是否有：1. 清晰的目标？是的，尽管幼稚得近乎不可能。而且，项目早在遇到这个基本的限制之前，就已经失败了。2. 人力？非常充足。3. 材料？在美索不达米亚有着丰富的泥土和柏油沥青。4. 足够的时间？没有任何时间限制的迹象。5. 足够的技术？是的，金字塔、锥形的结构本身就是稳定的，可以很好分散压力负载。对砖石建筑技术，人们有过深刻的研究。同样，项目远在达到技术限制之间，就已经失败了。那么，既然他们具备了所有的这些条件，为什么项目还会失败呢？他们还缺乏些什么？两个方面——交流，以及交流的结果——组织。他们无法相互交谈，从而无法合作。当合作无法进行时，工作陷入了停顿。通过史书的字里行间，我们推测交流的缺乏导致了争辩、沮丧和群体猜忌。很快，部落开始分裂——大家选择了孤立，而不是互相争吵。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;产品负责人的角色是什么？他组建团队，划分工作及制订进度表。他要求，并一直要求必要的资源。这意味着他主要的工作是与团队外部，向上和水平地沟通。他建立团队内部的沟通和报告方式。最后，他确保进度目标的实现，根据环境的变化调整资源和团队的构架。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;那么技术主管的角色是什么？他对设计进行构思，识别系统的子部分，指明从外部看上去的样子，勾画它的内部结构。他提供整个设计的一致性和概念完整性；他控制系统的复杂程度。当某个技术问题出现时，他提供问题的解决方案，或者根据需要调整系统设计。用Al Capp所喜欢的一句谚语，他是“攻坚小组中的独行侠”（inside-man at the skunk works.）。他的沟通交流在团队中是首要的。他的工作几乎完全是技术性的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;存在三种可能的关系，它们都在实践中得到了成功的应用。产品负责人和技术主管是同一个人。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;技术主管作为总指挥，产品负责人充当其左右手。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;产品负责人作为总指挥，技术主管充当其左右手。这种方法有一些困难。很难在技术主管不参与任何管理工作的同时，建立在技术决策上的权威。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;实践是最好的老师。- PUBILIUS 实践是最好的老师，但是，如果不能从中学习，再多的实践也没有用。- 《可怜的理查年鉴》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;当系统设计者认为对用户而言，常驻程序内存的形式比加法器、磁盘等更加有用时，他会将硬件实现中的一部分移到内存上。相反的，其他的做法是非常不负责任的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;该从整体上来进行评价。没有人可以在自始至终提倡更紧密的软硬件设计集成的同时，又仅仅就规模本身对软件系统提出批评。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;由于规模是软件系统产品用户成本中如此大的一个组成部分，开发人员必须设置规模的目标，控制规模，考虑减小规模的方法，就像硬件开发人员会设立元器件数量目标，控制元器件的数量，想出一些减少零件的方法。同任何开销一样，规模本身不是坏事，但不必要的规模是不可取的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;慢慢的，他逐渐认识到这些文档的某些部分包含和表达了一些管理方面的工作。每份文档的准备工作是集中考虑，并使各种讨论意见明朗化的主要时刻。如果不这样，项目往往会处于无休止的混乱状态。文档的跟踪维护是项目监督和预警的机制。文档本身可以作为检查列表、状态控制，也可以作为汇报的数据基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;Cosgrove主张把所有计划、里程碑、日程安排都当作是尝试性的，以方便进行变化。这似乎有些走极端——现在软件编程小组失败的主要原因是管理控制得太少，而不是太多。不过，他提出了一种卓越的见解。他观察到不愿意为设计书写文档的原因，不仅仅是由于惰性或者时间压力。相反，设计人员通常不愿意提交尝试性的设计决策，再为它们进行辩解。“通过设计文档化，设计人员将自己暴露在每个人的批评之下，他必须能够为他的每个结果进行辩护。如果团队架构因此受到任何形式的威胁，则没有任何东西会被文档化，除非架构是完全受到保护的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;只要能力允许，高层人员必须时刻做好技术和情感上的准备，以管理团队或者亲自参与开发工作。这是件工作量很大的任务，但显然很值得！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;组建外科手术队伍式的软件开发团队，这整个观念是对上述问题的彻底冲击。其结果是当高级人才编程和开发时，不会感到自降身份。这种方法试图清除那些会剥夺创造性乐趣的社会障碍。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;对于一个广泛使用的程序，其维护总成本通常是开发成本的40％或更多。令人吃惊的是，该成本受用户数目的严重影响。用户越多，所发现的错误也越多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;程序维护中的一个基本问题是——缺陷修复总会以（20－50）%的机率引入新的bug。所以整个过程是前进两步，后退一步。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;作为引入新bug的一个后果，程序每条语句的维护需要的系统测试比其他编程要多。理论上，在每次修复之后，必须重新运行先前所有的测试用例，从而确保系统不会以更隐蔽的方式被破坏。实际情况中，回归测试必须接近上述理想状况，所以它的成本非常高。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;系统软件开发是减少混乱度（减少熵）的过程，所以它本身是处于亚稳态的。软件维护是提高混乱度（增加熵）的过程，即使是最熟练的软件维护工作，也只是放缓了系统退化到非稳态的进程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;和古老的神话里一样，现代神话里也总有一些爱吹嘘的人：“我可以编写控制航空货运、拦截弹道导弹、管理银行账户、控制生产线的系统。”对这些人，回答很简单，“我也可以，任何人都可以，但是其他人成功了吗？”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;上述方法所意味的详尽体系结构设计正是出于这个目的。Bell实验室安全监控系统项目的V.A.Vyssotsky提出，“关键的工作是产品定义。许许多多的失败完全源于那些产品未精确定义的地方。1”细&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;好的自顶向下设计从几个方面避免了bug。首先，清晰的结构和表达方式更容易对需求和模块功能进行精确的描述。其次，模块分割和模块独立性避免了系统级的bug。另外，细节的隐藏使结构上的缺陷更加容易识别。第四，设计在每个精化步骤的层次上是可以测试的，所以测试可以尽早开始，并且每个步骤的重点可以放在合适的级别上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;当遇到一些意想不到的问题时，按部就班的流程并不意味着步骤不能反过来，直到推翻顶层设计，重新开始整个过程。实际上，这种情况经常发生。至少，它让我们更加清楚在什么时候和为什么抛弃了某个臃肿的设计，并重新开始。一些糟糕的系统往往就是试图挽救一个基础很差的设计，而对它添加了很多表面装饰般的补丁。自顶向下的方法减少了这样的企图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;简言之，Wirth的流程将设计看成一系列精化步骤。开始是勾画出能得到主要结果的，但比较粗略的任务定义和大概的解决方案。然后，对该定义和方案进行细致的检查，以判断结果与期望之间的差距。同时，将上述步骤的解决方案，在更细的步骤中进行分解，每一项任务定义的精化变成了解决方案中算法的精化，后者还可能伴随着数据表达方式的精化。在这个过程中，当识别出解决方案或者数据的模块时，对这些模块的进一步细化可以和其他的工作独立，而模块的大小程度决定了程序的适用性和可变化的程度。Wirth主张在每个步骤中，尽可能使用级别较高的表达方法来表现概念和隐藏细节，除非有必要进行进一步的细化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;如同我们看到的，必须关心每一天的滞后，它们是大灾祸的基本组成元素。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;一线经理的利益和老板的利益是内在冲突的。一线经理担心如果汇报了问题，老板会采取行动,这些行动会取代经理的作用，降低自己的威信，搞乱了其他计划。所以，只要项&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;目经理认为自己可以独立解决问题，他就不会告诉老板。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;不过，当项目经理了解到老板收到项目报告之后不会惊慌，或者不会越俎代庖时，他就逐渐会提交真实的评估结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;流程图是被吹捧得最过分的一种程序文档。事实上，很多程序甚至不需要流程图，很少有程序需要一页纸以上的流程图。流程图显示了程序的流程判断结构，它仅仅是程序结构的一个方面。当流程图绘制在一张图上时，它能非常优雅地显示程序的判断流向，但当它被分成几张时，也就是说需要采用经过编号的出口和连接符来进行拼装时，整体结构的概观就严重地被破坏了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;大家熟悉的软件项目具有一些人狼的特性（至少在非技术经理看来），常常看似简单明了的东西，却有可能变成一个落后进度、超出预算、存在大量缺陷的怪物。因此，我们听到了近乎绝望的寻求银弹的呼唤，寻求一种可以使软件成本像计算机硬件成本一样降低的尚方宝剑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;一个相互牵制关联的概念结构，是软件实体必不可少的部分，它包括：数据集合、数据条目之间的关系、算法、功能调用等等。这些要素本身是抽象的，体现在相同的概念构架中，可以存在不同的表现形式。尽管如此，它仍然是内容丰富和高度精确的。我认为软件开发中困难的部分是规格化、设计和测试这些概念上的结构，而不是对概念进行表达和对实现逼真程度进行验证。当然，我们还是会犯一些语法错误，但是和绝大多数系统中的概念错误相比，它们是微不足道的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;让我们来考虑现代软件系统中这些无法规避的内在特性：复杂度、一致性、可变性和不可见性。复杂度。规模上，软件实体可能比任何由人类创造的其他实体要复杂，因为没有任何两个软件部分是相同的（至少是在语句的级别）。如果有相同的情况，我们会把它们合并成供调用的子函数。在这个方面，软件系统与计算机、建筑或者汽车大不相同，后者往往存在着大量重复的部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;一致性。并不是只有软件工程师才面对复杂问题。物理学家甚至在非常“基础”的级别上，面对异常复杂的事物。不过，物理学家坚信必定存在着某种通用原理，或者在夸克中，或者在统一场论中。爱因斯坦曾不断地重申自然界一定存在着简化的解释，因为上帝不是专横武断或反复无常的。软件工程师却无法从类似的信念中获得安慰，他必须控制的很多复杂度是随心所欲、毫无规则可言的，来自若干必须遵循的人为惯例和系统。它们随接口的不同而改变，随时间的推移而变化，而且，这些变化不是必需的，仅仅由于它们是不同的人——而非上帝——设计的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;可变性。软件实体经常会遭受到持续的变更压力。当然，建筑、汽车、计算机也是如此。不过，工业制造的产品在出厂之后不会经常地发生修改，它们会被后续模型所取代，或者必要更改会被整合到具有相同基本设计的后续产品系列。汽车的更改十分罕见，计算机的现场调整时有发生。然而，它们和软件的现场修改比起来，都要少很多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;日常生活中，建筑有可能发生变化，但众所周知，建筑修改的成本很高，从而打消了那些想提出修改的人的念头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;所有成功的软件都会发生变更。现实工作中，经常发生两种情况。当人们发现软件很有用时，会在原有应用范围的边界，或者在超越边界的情况下使用软件。功能扩展的压力主要来自那些喜欢基本功能，又对软件提出了很多新用法的用户们。其次，软件一定是在某种计算机硬件平台上开发，成功软件的生命期通常比当初的计算机硬件平台要长。即使不是更换计算机，则有可能是换新型号的磁盘、显示器或者打印机。软件必须与各种新生事物保持一致。简言之，软件产品扎根于文化的母体中，如各种应用、用户、自然及社会规律、计算机硬件等等。后者持续不断地变化着，这些变化无情地强迫着软件随之变化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;不可见性。软件是不可见的和无法可视化的。例如，几何抽象是强大的工具。建筑平面图能帮助建筑师和客户一起评估空间布局、进出的运输流量和各个角度的视觉效果。这样，矛盾变得突出，忽略的地方变得明显。同样，机械制图、化学分子模型尽管是抽象模型，但都起了相同的作用。总之，都可以通过几何抽象来捕获物理存在的几何特性。软件的客观存在不具有空间的形体特征。因此，没有已有的表达方式，就像陆地海洋有地图、硅片有膜片图、计算机有电路图一样。当我们试图用图形来描述软件结构时，我们发现它不仅仅包含一个，而是很多相互关联、重叠在一起的图形。这些图形可能描绘控制流程、数据流、依赖关系、时间序列、名字空间的相互关系等等。它们通常不是有较少层次的扁平结构。实际上，在上述结构上建立概念控制的一种方法是强制将关联分割，直到可以层次化一个或多个图形2。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;它们的出现都消除了开发过程中的非本质困难，允许设计人员表达自己设计的内在特性，而不需要表达大量句法上的内容，这些内容并没有添加什么新的信息。对于抽象数据类型和层次化类型，它们都是解决了高级别的次要困难和允许采用较高层次的表现形式来表达设计。不过，这些提高仅仅能消除所有设计表达上的次要困难。软件的内在问题是设计的复杂度，上述方法并没有对它有任何的促进。除非我们现在的编程语言中，不必要的低层次类型说明占据了软件产品设计90％，面向对象编程才能带来数量级上的提高。对面向对象编程这颗“银弹”，我深表怀疑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;人工智能。很多人期望人工智能上的进展可以给软件生产率和质量带来数量级上的增长4，但我不这样认为。追究其原因，我们必须剖析“人工智能”意味着什么，以及它如何应用。Parnas澄清了术语上的混乱：现在有两种差异非常大的AI定义被广泛使用。AI-1：使用计算机来解决以前只能通过人类智慧解决的问题。AI-2：使用启发式和基于规则的特定编程技术。在这种方法中，对人类专家进行研究，判断他们解决方法的启发性思维或者经验法则⋯⋯。程序被设计成以人类解决问题的方式来运作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;例如，我觉得很难去发现图象识别技术能给编程开发实践带来什么样的差异。同样，语音识别也差不多——软件开发上的困难是决定说什么，而不是如何说。表达的简化仅仅能提供少量的促进作用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;更加基本的是，如同我们上面所争论的，软件非常难以可视化。即使用图形表达出了流程图、变量范围嵌套情况、变量交叉引用、数据流、层次化数据结构等等，也只是表达了某个方面，就像盲人摸象一样。如果我们把很多相关的视图叠加在所产生的图形上，那么很难再抽取出全局的总体视图。对VLSI芯片设计方法的类推是一种误导——芯片设计是对两维对象的层次设计，它的几何特性反映了它的本质特性，而软件系统不是这样。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;程序验证不意味着零缺陷的程序。这里并没有什么魔术，数学验证仍然可能是有错误的。因此，尽管验证可能减少程序测试的工作量，但却不能省略程序测试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;更严肃地说，完美的程序验证只能建立满足技术说明的程序。这时，软件工作中最困难的部分已经接近完成，形成了完整和一致的说明。开发程序的一些必要工作实际上已经变成对技术规格说明进行测试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;因此，软件开发人员为客户所承担的最重要的职能是不断重复地抽取和细化产品的需求。事实上，客户不知道他们自己需要什么。他们通常不知道哪些问题是必须回答的。并且，连必须确定的问题细节常常根本不予考虑，甚至只是简单地回答——“开发一个类似于我们已有的手工处理过程的新软件系统”——实际上都过于简单。客户决不会仅仅要求这些。复杂的软件系统往往是活动的、变化的系统。活动的动态部分是很难想象的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;因此，现在的技术中最有希望的，并且解决了软件的根本而非次要问题的技术，是开发作为迭代需求过程的一部分——快速原型化系统的方法和工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;软件系统的快速原型对重要的系统界面进行模拟，并演示待开发系统的主要功能。原型不必受到相同硬件速度、规模或者成本约束的限制。原型通常展示了应用程序的功能主线，但不处理任何如无效输入、退出清除等异常情况。原型的目的是明确实际的概念结构，从而客户可以测试一致性和可用性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;不过，我不认为我们可以用相同的方式取得下一次进步。低劣设计和良好设计之间的区别可能在于设计方法中的完善性，而良好设计和卓越设计之间的区别肯定不是如此。卓越设计来自卓越的设计人员。软件开发是一个创造性的过程。完备的方法学可以培养和释放创造性的思维，但它无法孕育或激发创造性的过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;其中的差异并不小——就象萨列里和莫扎特。一个接一个的研究显示，非常卓越的设计者产生的成果更快、更小、更简单、更优雅，实现的代价更少。卓越和一般之间的差异接近于一个数量级。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;就我的经验而言，在系统工作中所遇到的大多数困难是组织结构上的一些失误征兆。试图为这些现实建模，建立同等复杂的程序，实际上是隐藏，而不是解决这些杂乱无章的情况。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;我曾作为物理学家接受过培训，因此倾向于用更简单的概念来描述“复杂”事物。现在你可能是正确的，我无法断定所有的复杂事物都容易用有序的规律表达⋯⋯同样的道理，你不能断定它们不能。⋯⋯昨天的复杂性是今天的规律。分子的无序性启迪了气体动力学理论和热力学的三大定律。现在，软件没有揭示类似的规律性原理，但是解释为什么没有的重担在你的身上。我不是迟钝和好辩的。我相信有一天软件的“复杂性”将以某种更高级的规律性概念来表达（就像物理学家的不变式）。我并没有着手于Lukasik提倡的更深层次的分析。作为一个学科，我们需要更广泛的信息理论，它能够量化静态结构的信息内容，就像针对交互流的香农信息论一样。这已经超越了我的能力。作为对Lukasik的简单回应，我认为系统复杂性是无数细节的函数，这些细节必须精确而且详细地说明——或者是借助某种通用规则，或者是逐一阐述，但决不仅仅是统计说明。仅靠若干人不相干的工作，是不大可能产生足够的一致性，能用通用规律进行精确描述。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;过程的思考，当时写了一篇随笔：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.intotw.cn/2024/09/24/tech-web-develop/&quot;&gt;软件系统到底做了些什么？&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>听听歌吧-ECHO《ECHO》</title><link>https://www.intotw.cn/posts/recommend-song-04/</link><guid isPermaLink="true">https://www.intotw.cn/posts/recommend-song-04/</guid><pubDate>Sat, 17 Aug 2024 01:43:17 GMT</pubDate><content:encoded>&lt;p&gt;这个ECHO其实是个很冷门小众的乐队，大部分人唯一可能听过的反而是他们的&lt;a href=&quot;http://music.163.com/song?id=353558&amp;amp;market=baiduqk&quot;&gt;一夜尘埃&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但是个人最喜欢的是他们第一张同名专辑的同名主打歌&lt;a href=&quot;https://music.163.com/#/song?id=353552&quot;&gt;Echo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;他们应该也就出过这一张同名专辑，可惜了，其实这张专辑里好歌真的很多&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;歌未央&lt;/li&gt;
&lt;li&gt;风筝祭&lt;/li&gt;
&lt;li&gt;一夜尘埃&lt;/li&gt;
&lt;li&gt;Echo&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;说回Echo这首歌，感觉女主唱也挺能唱的，后面不知道为啥好像没啥动静，也没在唱歌了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;他是如此的吸引也让我
走进echo的境地
touch to me!为已敞开的心灵
需要注入多少的激情
就算费尽我所有柔情痛了自己&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=641188630&amp;amp;bvid=BV15Y4y1h7TU&amp;amp;cid=710580156&amp;amp;p=1&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;width:100%;aspect-ratio:16/9;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;爱情不能说一说就会错
幽幽森林清清水域藏着神的哭泣
水仙开放的时候才是最悲戚
爱情不能醒一醒就会停
淡淡树影巍巍山群唱着心碎声音
我爱恋的人他只爱自己
她是永恒的神灵却只能
远远凝望湖中的倒影
come to me!为一瞬间的亲近
需要增添多少的勇气
就算下一秒你就逃离无踪影
渐渐消失的身体无法确定感情
山谷里的回音刚开始苏醒&lt;/p&gt;
&lt;p&gt;还有什么比得过独有的咒语
挥散残留的香气说着刻骨铭心
无声的等待里生命的徒刑
还在延续结束的日期
爱情不能醒一醒就会停
淡淡树影巍巍山群唱着心碎声音
我爱恋的人他只爱自己
他是如此的吸引也让我
走进echo的境地
touch to me!为已敞开的心灵
需要注入多少的激情
就算费尽我所有柔情痛了自己
渐渐冰冷的身体无法确定感情
爱到情不自禁忘了我自己
还有什么注定放弃才可以靠近
呼吸只剩下叹息说着无能为力
像echo哭红的眼睛得不到回应
我依然是我你依然是你
错过交集爱情变成禁区
渐渐冰冷的身体无法确定感情
爱到情不自禁忘了我自己
还有什么注定放弃才可以靠近
呼吸只剩下叹息说着无能为力
像echo哭红的眼睛得不到回应
你依然是你我依然是我
爱~是无声无息爱到无边无际&lt;/p&gt;
</content:encoded></item><item><title>G1 GC笔记</title><link>https://www.intotw.cn/posts/note-g1gc/</link><guid isPermaLink="true">https://www.intotw.cn/posts/note-g1gc/</guid><pubDate>Mon, 12 Aug 2024 07:40:10 GMT</pubDate><content:encoded>&lt;p&gt;php转java重构搭脚手架的时候，因为是从新开始的项目，所以打算直接升jdk版本+spring boot3.0+。查了下jdk17自带的zgc是保障低延迟的，并且为了保障低延迟，需要的额外空间开销比较大，出于以下角度考虑，还是选择了G1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们的堆往往没那么大，所以内存比较宝贵，尽可能减少额外的内存额外开销&lt;/li&gt;
&lt;li&gt;我们对吞吐量的要求明显高于低延迟，单接口百ms级别都是可以接受的，此时zgc的&lt;strong&gt;毫秒级别延迟&lt;/strong&gt;在整个接口百ms的基准下影响不大，为了这点延迟去降低实际可用内存得不偿失&lt;/li&gt;
&lt;li&gt;G1 GC近十来年了，ZGC还较为新，使用G1 GC相关的问题以及调优可以参考的更多&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实CMS更适合，不过用新不用旧，并且G1在大多数情况下它的GC逻辑也是接近CMS的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何学习&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;个人一开始尝试去理解一些复杂概念，但是这些复杂概念只是通过&lt;strong&gt;论述&lt;/strong&gt;很难彻底理解，所以转为了解这些结构的功能而非实现，了解GC的过程而非细节，以便于实际工作调优或者排查问题。&lt;/p&gt;
&lt;p&gt;毕竟除非需要去修改GC的源码，应该是不需要对里面使用到的一些数据结构和方法进行详细的了解（&lt;strong&gt;但是大部分博客文章中往往反而会讲，就会讲的你很晕&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;这篇笔记记录给自己不定时翻出来看看回忆下，不会带一些需要大量篇幅才可以解释清楚的细节。&lt;/p&gt;
&lt;h2&gt;G1 GC的GC模式&lt;/h2&gt;
&lt;h3&gt;内存分布&lt;/h3&gt;
&lt;p&gt;G1 中，将内存划分为多个大小相同的region，其中可以简单的认为分为新生代region和老年代region&lt;/p&gt;
&lt;p&gt;与CMS相同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新生代在N次GC后未被回收，进入老年代&lt;/li&gt;
&lt;li&gt;大对象直接进老年代&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GC收集模式&lt;/h3&gt;
&lt;p&gt;G1 GC的GC模式包含以下几种收集模式（内容来源于R大）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;young GC（或者叫minor GC）：只收集young gen里的所有region，也就是eden和survivor。控制young GC开销的手段是动态改变young region的个数；&lt;/li&gt;
&lt;li&gt;mixed GC：收集young gen里的所有region，外加若干选定的old gen region。控制mixed GC开销的手段是选多少个、哪几个old gen region。&lt;/li&gt;
&lt;li&gt;其实没有3了。G1 GC的控制范围内没有full GC。如果mixed GC无法跟上mutator分配的速度，导致没有足够的空region来完成mixed GC，那么就会使用serial old GC（ mark-compact）来对整堆收集一次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以G1 GC的GC，一般都在几个过程里：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;要么在young gc，只回收新生代，可以类比和CMS一起用的young gc，用的是也是标记复制&lt;/li&gt;
&lt;li&gt;要么在mixed GC，通过一次young GC来先做标记，然后再并发标记，并发标记后根据一个回收的耗时评估+你配置的预期停顿时间，来决定回收那些老年代old region。&lt;/li&gt;
&lt;li&gt;要么在full gc，新生代和老年代都无法分配内存了，直接进入最原始的STW然后遍历整个堆做收集&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;G1 的GC细节（不那么细的）&lt;/h2&gt;
&lt;h3&gt;G1 性能方面的优化&lt;/h3&gt;
&lt;h4&gt;解决问题：新生代GC时，老年代对新生代的引用问题&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;概念1：卡表card table&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以先看下https://www.cnblogs.com/binyue/p/17281785.html&lt;/p&gt;
&lt;p&gt;在新生代GC时，可能存在老年代对新生代的引用，此时想要回收新生代的对象，还需要扫描整个老年代吗？那岂不是等于full gc（扫描整个堆）了？卡表使用额外的空间减少扫描的消耗。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;划重点：卡表是一个数据结构，用于记录老年代对象对新生代的引用，此时回收新生代时，只需要扫描那些卡表中有的老年代对象即可（不需要care卡表的实现细节&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;概念2：Remember Set&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remember Set是G1中用于解决回收时老年代引用新生代的问题的，&lt;strong&gt;因为G1的回收是针对Region的，所以要保证每个Region可以单独被回收，那么每个Region就需要记录自己的卡表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;划重点&lt;/strong&gt;：&lt;strong&gt;Remember Set是每个region维护的一个数据结构，里面维护了自己的卡表，在新生代回收时用于减小老年代扫描的规模&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;G1 回收正确性&lt;/h3&gt;
&lt;h4&gt;解决问题：解决并发标记的正确性&lt;/h4&gt;
&lt;p&gt;因为标记过程与用户线程执行是并发的，所以会存在你标记后但是被修改了的情况，那么就会漏标或者多标&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;概念1：黑白灰标记&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里有一个模型来简化问题，即黑白灰标记，使用黑白灰来标记对象&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;黑色 - 该对象已经标记，且该对象的引用字段也都已经处理完或已经加到任务队列中&lt;/li&gt;
&lt;li&gt;灰色 - 该对象已经标记，但是对象的引用字段还没有处理完&lt;/li&gt;
&lt;li&gt;白色 - 该对象还没有标记&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;概念2：satb以及浮动垃圾&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;satb是一个解决并发标记问题的方案。&lt;strong&gt;在一开始，认为所有对象都是存活的&lt;/strong&gt;。然后在标记过程中，使用一个数据结构，通过代码中在改变引用的前后加上指令，实现一个&lt;strong&gt;切面&lt;/strong&gt;一样的效果，以此来在这个对象的引用改变的时候，记录那些变更了引用的对象到satb里去。&lt;/p&gt;
&lt;p&gt;在实际清理前，只需要扫描satb里的对象就可以了。&lt;/p&gt;
&lt;p&gt;satb会产生浮动垃圾（该回收的没标记，下次才能回收），但是不会漏标（不会因为并发标记的错误而导致实际内存泄露，即保证了正确性）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;概念3：新产生的内存，TopAtMarkStart，TAMS指针&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在并发标记执行后，实际回收执行前。新产生的内存也应该是存活的，所以需要避免回收这部分，&lt;strong&gt;在region内定义一个指针，记录并发标记时的地址，大于该指针的内存全是并发标记后新产生的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;G1 回收保证吞吐&lt;/h3&gt;
&lt;p&gt;G1进行并发标记、最终标记、清理这三个阶段后，实际并没有回收内存。&lt;strong&gt;这里的清理阶段只是清理一些标记状态，并且把完全没有活对象的region整体回收到可分配的region列表里。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实际清理后，有一个完全可以独立执行的evacuation阶段，这个阶段根据标记的结果，分析每个老年代的region可回收信息，然后&lt;strong&gt;经过一堆统计学算法和你配置的信息的评估&lt;/strong&gt;，决定回收哪些region。然后活对象拷贝到空region里去，回收原本的region的空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;[&lt;a href=&quot;https://hllvm-group.iteye.com/group/forum?tag_id=689&quot;&gt;资料]&lt;/a&gt; [&lt;a href=&quot;https://hllvm-group.iteye.com/group/topic/44381&quot;&gt;HotSpot VM] 请教G1算法的原理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://juejin.cn/user/1028798613564327/posts&quot;&gt;深入分析G1垃圾收集器实现原理&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://bytejava.cn/md/jvm/gc/g1/g1-all-intro.html&quot;&gt;G1 GC核心原理介绍&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/binyue/p/17281785.html&quot;&gt;垃圾回收之CardTable和Remembered Set &lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Ostep 39 Interlude File and Directories</title><link>https://www.intotw.cn/posts/ostep-39-interlude-file-and-directories/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-39-interlude-file-and-directories/</guid><pubDate>Tue, 06 Aug 2024 06:01:30 GMT</pubDate><content:encoded>&lt;p&gt;本书老套路，介绍文件系统的实现之前，先介绍一下相关的api和使用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件和目录的几个核心点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件可以认为包含2种级别的定义。一个是低级别的&lt;strong&gt;inode number&lt;/strong&gt;，可以认为是id。另一个是用户级别的，也就是filename&lt;/li&gt;
&lt;li&gt;文件的后缀其实只是一种&lt;strong&gt;惯例&lt;/strong&gt;。这个其实熟悉Linux的都知道，可执行文件不像windows一样一定要有exe，在shell中，只要权限是可执行，就可以直接执行。后缀只是方便操作系统和用户而已&lt;/li&gt;
&lt;li&gt;目录是树状组织的一系列节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文中篇幅非常长，介绍了许多API，这边总结一下其中提到的一些概念以及贯穿文件系统API的一些思想&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;使用文件描述符来操作文件&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;文件系统中，在程序级别，使用open打开某个文件，会返回该文件的&lt;strong&gt;描述符&lt;/strong&gt;，一个和类似id的东西，然后通过该描述符进行操作，我们看下cat foo指令的系统调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prompt&amp;gt; strace cat foo
...
open(&quot;foo&quot;, O_RDONLY|O_LARGEFILE)       = 3
read(3, &quot;hello\n&quot;, 4096)                = 6
write(1, &quot;hello\n&quot;, 6)                  = 6
hello
read(3, &quot;&quot;, 4096)                  			= 0
close(3) 																= 0
...
prompt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;open返回foo的文件描述符3，read也是用了3去读取内容。&lt;/p&gt;
&lt;p&gt;几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**为什么文件描述符是3？**因为1和2会预留并分配给标准输入和标准输出&lt;/li&gt;
&lt;li&gt;**为什么write写到的目标描述符是1？**这就很明显了，结合到第一个问题，write这里是把foo中读到的数据写入到了标准输出&lt;/li&gt;
&lt;li&gt;**为什么还要read一次？**再read一次发现没数据后，关闭文件描述符。结束读取&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;随机读写文件&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;这里主要为了引出操作系统中文件大致的结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct file {
    int ref;            // 引用计数
    char readable;      // 可读标志
    char writable;      // 可写标志
    struct inode *ip;  // 指向 inode 结构体的指针
    uint off;          // 偏移量
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述的结构表示了当前操作系统所有打开的文件，所有这些结构组成的就是&lt;strong&gt;open file table&lt;/strong&gt;，打开文件表，可以看到，通过其中偏移量的直接操作，我们就可以完成随机读写了&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;父子进、线程之间分享文件描述符&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/08/5b305ae2e46a502ac70a391a6b5882d6.webp&quot; alt=&quot;QQ_1722929887241&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(int argc, char *argv[]) {
    // Open the file &quot;README&quot; in read-only mode
    int fd = open(&quot;README&quot;, O_RDONLY);
    assert(fd &amp;gt;= 0);

    // Duplicate the file descriptor
    int fd2 = dup(fd);

    // Now fd and fd2 can be used interchangeably

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用dup可以使得父子之间对同一个文件描述符进行复制，复制的主要区别就是：他们使用的是文件打开表中的同一个节点，所以他们拥有相同的偏移量&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;fsync()确保同步写&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;按照我们之前描述的操作系统和磁盘间的一些交互可以了解到，普通的write并不代表着就已经写到硬盘了再返回，而是可能通过丢个任务给DMA，并且还需要再等待磁盘的调度合并才会真正落盘。&lt;/p&gt;
&lt;p&gt;fsync()则是一个为了提供一致性的显式强制写调用，它只在确保数据已经写入到磁盘以后才会返回，它会比write慢非常多，但是可以保证数据的安全。&lt;/p&gt;
&lt;h2&gt;重命名&lt;/h2&gt;
&lt;p&gt;这里介绍了一个rename系统调用，主要就是负责&lt;strong&gt;原子性的&lt;/strong&gt;将文件改名。大部分编辑器通过rename来保存你的修改。步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将临时文件.tmp，使用fsync持久化到磁盘&lt;/li&gt;
&lt;li&gt;将临时文件.tmp，使用rename原子性的修改名字为原文件的文件名&lt;/li&gt;
&lt;li&gt;删除原文件&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;文件元信息&lt;/h2&gt;
&lt;p&gt;使用stat可以查看&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/08/cbb153467bf842452ff6234b684a199c.webp&quot; alt=&quot;QQ_1722930708310&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct stat {
    dev_t     st_dev;     // ID of device containing file
    ino_t     st_ino;     // inode number
    mode_t    st_mode;    // protection
    nlink_t   st_nlink;   // number of hard links
    uid_t     st_uid;     // user ID of owner
    gid_t     st_gid;     // group ID of owner
    dev_t     st_rdev;    // device ID (if special file)
    off_t     st_size;    // total size, in bytes
    blksize_t st_blksize; // blocksize for filesystem I/O
    blkcnt_t  st_blocks;  // number of blocks allocated
    time_t    st_atime;   // time of last access
    time_t    st_mtime;   // time of last modification
    time_t    st_ctime;   // time of last status change
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个保存文件信息的结构就是前面说的&lt;strong&gt;inode&lt;/strong&gt;，在后续介绍实现时会细讲&lt;/p&gt;
&lt;h2&gt;目录directory&lt;/h2&gt;
&lt;p&gt;我们在创建一个目录后，每个目录都会自动创建2个目录，分别是./和../，使用ls -la就可以看到&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/08/5f258f93b6af5d698b6b257acf961e44.webp&quot; alt=&quot;QQ_1722930931489&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目录本身就是一个树状结构，包含的信息也不算多&lt;/p&gt;
&lt;h2&gt;删除文件——以及软硬链接&lt;/h2&gt;
&lt;p&gt;结合前面的inode概念，我们就可以更好的理解软链接以及硬链接了&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;硬链接&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;硬链接使用系统调用link（），创建一个硬链接，硬链接的本质是：创建一个文件，将这个文件指向同一个inode。&lt;/p&gt;
&lt;p&gt;所以你会发现，硬链接创建后，2个文件的inode是相同的，下面的第一列就是inode&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;prompt&amp;gt; ls -i file file2
67158084 file
67158084 file2
prompt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们删除某个文件时，本质上是对硬链接做一个unlink，所以我们使用rm等操作时，对应的系统调用时unlink，断开这个文件和inode的链接&lt;/p&gt;
&lt;p&gt;所以我们删除文件后，文件并不会立马消失，而是只是删除掉了inode的一个link，这个inode什么时候彻底被清除，就等待操作系统后续的决策了（或许是使用lazy load都可以，当发现再需要一个的时候，找一个废弃的重新去初始化，都不需要定时释放）&lt;/p&gt;
&lt;p&gt;那么理所应当的，当创建了1个硬链接后，删除其中一个文件，另一个硬链接的文件不会消失，并且，修改其中一个文件，另一个文件也会被修改&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;延伸一下&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在cp去copy一个文件的时候，我们显然可以使用一种方式来节省资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cp时，创建一个新文件，并且使用硬链接链接到原来的文件&lt;/li&gt;
&lt;li&gt;在修改时，才根据Copy on Write机制，去真正复制这个文件并保存新的inode&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;软链接&lt;/h3&gt;
&lt;p&gt;软链接那么就刚好相反了，它只是一个符号链接，相当于创建了一个文件名，链接到了对应的文件名上，而不是inode上。&lt;/p&gt;
&lt;p&gt;我们可以通过一个图来直观看下这个引用&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/2df7bde5d30bdae88e42da3fd6127065.webp&quot; alt=&quot;QQ_1722932347921&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;所以，在软连接删除file1时，其实就意味着inode没了，并且file2成为了&lt;strong&gt;悬空指针&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;文件的权限控制&lt;/h2&gt;
&lt;p&gt;文件的权限就是我们经常看到的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/08/5f258f93b6af5d698b6b257acf961e44.webp&quot; alt=&quot;QQ_1722930931489&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先说说2个root，分别表示了这个文件的所有者是root，文件所属组的组名也是root&lt;/p&gt;
&lt;p&gt;上图中第一列，它一共有10位，由几部分组成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一位是描述了文件的类型，有3种：&lt;/p&gt;
&lt;p&gt;d：表示目录&lt;/p&gt;
&lt;p&gt;-：表示常规文件&lt;/p&gt;
&lt;p&gt;l：表示是符号链接（短链接）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;后九位&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;后九位分三组，每组三位。这三组每组的三位分别表示（文件可读r，文件可写w，文件可执行r）&lt;/p&gt;
&lt;p&gt;这三组分别表示：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件所有者&lt;/strong&gt;对该文件的权限&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件所属组&lt;/strong&gt;对该文件的权限&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其他人&lt;/strong&gt;对该文件的权限&lt;/p&gt;
&lt;p&gt;比如图中的第一行，drwxr-xr-x就表示了：&lt;/p&gt;
&lt;p&gt;d：该文件是个目录&lt;/p&gt;
&lt;p&gt;rwx：文件所有者对该文件可读可写可执行（执行后就跳到上级目录了呗）&lt;/p&gt;
&lt;p&gt;r-x：文件所属组对该文件可读并且可执行&lt;/p&gt;
&lt;p&gt;r-x：其他人对该文件也可读可执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如何修改权限？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;熟悉linux的都知道，常用一般都是chmod 755，或者777。其中改的含义其实就是修改rwx，只不过是使用二进制模式表示，777的三个7分别表示修改3个组的权限，单个的7是rwx这3位数字的二进制表示，比如rwx=111=7，rw-=110=6，r--=100=4&lt;/p&gt;
&lt;h2&gt;如何创建一个文件系统——挂载&lt;/h2&gt;
&lt;p&gt;挂载就是mount，在linux里，外部存储都是依靠挂载到系统的某个节点，以此来让操作系统为它创建整个目录树的。&lt;/p&gt;
&lt;p&gt;mount所做的很简单，就是将一个现有目录作为目标挂载点，并将一个新的文件系统粘贴到该点的目录树中。&lt;/p&gt;
&lt;p&gt;假设/dev/sda1有a，b这2个目录，挂载到&lt;strong&gt;系统中&lt;/strong&gt;一个指定目录的话&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mount -t ext3 /dev/sda1 /home/users&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时整个目录树就复制过来了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;prompt&amp;gt; ls /home/users/&lt;/p&gt;
&lt;p&gt;a b&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;文中就有总结，这里直接贴文中的英文总结吧，然后翻译由gpt倾情完成&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KEY FILE SYSTEM TERMS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件系统关键术语&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	1.	&lt;strong&gt;File&lt;/strong&gt;: An array of bytes that can be created, read, written, and deleted. It has a low-level name (i.e., a number) that uniquely refers to it. This low-level name is often called an &lt;strong&gt;i-number&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件&lt;/strong&gt;：一个字节数组，可以创建、读取、写入和删除。它具有一个低级名称（即一个数字），唯一地标识它。这个低级名称通常称为 &lt;strong&gt;i-number&lt;/strong&gt;（i号）。&lt;/p&gt;
&lt;p&gt;​	2.	&lt;strong&gt;Directory&lt;/strong&gt;: A collection of tuples, each containing a human-readable name and a low-level name to which it maps. Each entry refers either to another directory or to a file. Each directory has a low-level name (i.e., an i-number) and two special entries:&lt;/p&gt;
&lt;p&gt;​	•	&lt;strong&gt;.&lt;/strong&gt;: Refers to the directory itself.&lt;/p&gt;
&lt;p&gt;​	•	&lt;strong&gt;..&lt;/strong&gt;: Refers to its parent directory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目录&lt;/strong&gt;：一个元组的集合，每个元组包含一个可读的名称和一个低级名称，表示映射到的对象。每个条目要么指向另一个目录，要么指向一个文件。每个目录也有一个低级名称（即i号）和两个特殊条目：&lt;/p&gt;
&lt;p&gt;​	•	&lt;strong&gt;.&lt;/strong&gt;：指向目录自身。&lt;/p&gt;
&lt;p&gt;​	•	&lt;strong&gt;..&lt;/strong&gt;：指向其父目录。&lt;/p&gt;
&lt;p&gt;​	3.	&lt;strong&gt;Directory Tree&lt;/strong&gt;: Also known as a &lt;strong&gt;directory hierarchy&lt;/strong&gt;, it organizes all files and directories into a large tree structure, starting at the &lt;strong&gt;root&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目录树&lt;/strong&gt;：也称为 &lt;strong&gt;目录层级结构&lt;/strong&gt;，它将所有文件和目录组织成一个大型树形结构，从 &lt;strong&gt;根&lt;/strong&gt; 开始。&lt;/p&gt;
&lt;p&gt;​	4.	&lt;strong&gt;File Access&lt;/strong&gt;: To access a file, a process must use a system call (usually open()) to request permission from the operating system. If permission is granted, the OS returns a &lt;strong&gt;file descriptor&lt;/strong&gt;, which can be used for read or write access, depending on permissions and intent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件访问&lt;/strong&gt;：要访问一个文件，进程必须使用系统调用（通常是 open()）向操作系统请求权限。如果权限被授予，操作系统会返回一个 &lt;strong&gt;文件描述符&lt;/strong&gt;，根据权限和意图，可以用于读取或写入访问。&lt;/p&gt;
&lt;p&gt;​	5.	&lt;strong&gt;File Descriptor&lt;/strong&gt;: A private, per-process entity that refers to an entry in the &lt;strong&gt;open file table&lt;/strong&gt;. This entry tracks which file the access refers to, the &lt;strong&gt;current offset&lt;/strong&gt; of the file (i.e., which part of the file the next read or write will access), and other relevant information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件描述符&lt;/strong&gt;：一个每进程私有的实体，指向 &lt;strong&gt;打开文件表&lt;/strong&gt; 中的一个条目。这个条目跟踪访问指向的文件、文件的 &lt;strong&gt;当前偏移量&lt;/strong&gt;（即下一个读取或写入将访问文件的哪个部分）和其他相关信息。&lt;/p&gt;
&lt;p&gt;​	6.	&lt;strong&gt;Current Offset&lt;/strong&gt;: Calls to read() and write() naturally update the current offset. Processes can also use lseek() to change its value, enabling random access to different parts of the file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当前偏移量&lt;/strong&gt;：对 read() 和 write() 的调用自然会更新当前偏移量。进程还可以使用 lseek() 来更改其值，从而实现对文件不同部分的随机访问。&lt;/p&gt;
&lt;p&gt;​	7.	&lt;strong&gt;Force Updates&lt;/strong&gt;: To force updates to persistent media, a process must use fsync() or related calls. Doing so correctly while maintaining high performance can be challenging, so think carefully when using these calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;强制更新&lt;/strong&gt;：要强制更新持久媒体，进程必须使用 fsync() 或相关调用。正确地做到这一点，同时保持高性能可能具有挑战性，因此在使用这些调用时要仔细考虑。&lt;/p&gt;
&lt;p&gt;​	8.	&lt;strong&gt;Hard Links and Symbolic Links&lt;/strong&gt;: To have multiple human-readable names in the file system refer to the same underlying file, use hard links or symbolic links. Each has its own strengths and weaknesses, so consider their appropriate usage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;硬链接和符号链接&lt;/strong&gt;：要在文件系统中让多个可读名称指向同一个基础文件，可以使用硬链接或符号链接。每种链接都有其优缺点，因此要考虑它们的适当使用。&lt;/p&gt;
&lt;p&gt;​	9.	&lt;strong&gt;Deleting Files&lt;/strong&gt;: Deleting a file is essentially performing one last unlink() operation from the directory hierarchy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;删除文件&lt;/strong&gt;：删除一个文件本质上是在目录层级结构中执行最后一次 unlink() 操作。&lt;/p&gt;
&lt;p&gt;​	10.	&lt;strong&gt;File Sharing Controls&lt;/strong&gt;: Most file systems have mechanisms to enable and disable sharing. Basic controls are provided by &lt;strong&gt;permissions bits&lt;/strong&gt;, while more sophisticated &lt;strong&gt;access control lists&lt;/strong&gt; allow for precise control over who can access and manipulate information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件共享控制&lt;/strong&gt;：大多数文件系统都有启用和禁用共享的机制。基本控制由 &lt;strong&gt;权限位&lt;/strong&gt; 提供，而更复杂的 &lt;strong&gt;访问控制列表&lt;/strong&gt; 则允许对谁可以访问和操作信息进行精确控制。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 38 Redundant Arrays of Inexpensive Disks</title><link>https://www.intotw.cn/posts/ostep-38-redundant-arrays-of-inexpensive-disks/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-38-redundant-arrays-of-inexpensive-disks/</guid><pubDate>Sat, 03 Aug 2024 08:00:34 GMT</pubDate><content:encoded>&lt;p&gt;这章如名字，主要讲的是磁盘冗余阵列，也就是我们常说的RAID。一种扩展磁盘的容量、提升磁盘可靠性的技术。&lt;/p&gt;
&lt;p&gt;RAID存在RAID0到RAID6等多种方案。评估一个RAID方案，主要从&lt;strong&gt;容量、可靠性、性能&lt;/strong&gt;这三个方面。RAID的特点和前面的一些虚拟化一样，将多个磁盘虚拟化为一个磁盘，来对上游提供服务。&lt;/p&gt;
&lt;h1&gt;RAID 0 条带化分块&lt;/h1&gt;
&lt;p&gt;RAID0是最基础的思路，实现的功能也很简单，对多个磁盘进行&lt;strong&gt;合并&lt;/strong&gt;。他使用了**条带化（stripe）**的方式来对多个硬盘进行分段以及分块，将多个磁盘组合并按次序进行标记分段，然后存储数据时只要使用对应分段即可。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/a73bb344c0ec896638ef00c816d6a90b.webp&quot; alt=&quot;QQ_1722672649277&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到，跨越4个磁盘，我们通过分段让他们依旧有了连续性关系，所以我们就可以像操作一个磁盘那样操作4个磁盘了。&lt;/p&gt;
&lt;p&gt;这里会有一个关键的&lt;strong&gt;权衡&lt;/strong&gt;：如何选择每段的大小？首先注意一点，不同物理磁盘是可以&lt;strong&gt;并发读写&lt;/strong&gt;的。所以，段分段过小时，我们可以拥有更好的并发性，但是会过多的跨越磁盘读取数据。段分段过大时，我们读取大文件会有更好的连续性，但是也会因为丧失并发性而失去一部分性能，并且性能此时就纯依赖于一个磁盘的性能了。&lt;/p&gt;
&lt;p&gt;一个极端的例子可以说明这个情况：当我们的一个段时4kb时，此时读取一个16kb的文件，刚好是0-3四个分段。在4个磁盘并发读取的情况下，性能可以是接近4倍（因为还需要将数据合并，会有一定开销）。&lt;/p&gt;
&lt;h2&gt;评估RAID 0&lt;/h2&gt;
&lt;p&gt;我们评估磁盘的性能与之前一样，分别要考虑2个场景：顺序、随机。&lt;/p&gt;
&lt;p&gt;给定单个磁盘的性能，我们来进行一下计算单个磁盘的S和R，其中S表示顺序sequential，R表示随机random。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Average seek time   7 ms&lt;/p&gt;
&lt;p&gt;Average rotational delay   3 ms&lt;/p&gt;
&lt;p&gt;Transfer rate of disk   50 MB/s&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$S=\frac{Amount of Data}{Time to access}=\frac{10MB}{210ms}= 47.62MB/s$&lt;/p&gt;
&lt;p&gt;$R=\frac{Amount of Data}{Time to access}=\frac{10MB}{10.195ms}= 0.981MB/s$&lt;/p&gt;
&lt;p&gt;从吞吐量的角度看性能（&lt;strong&gt;多磁盘间可以并发读写&lt;/strong&gt;）。在整个RAID0下，访问整个RAID0和访问单个磁盘的延迟大致相同，所以RAID0的实际S=N·S，R=N·R， 基本是线性的提升。&lt;/p&gt;
&lt;p&gt;所以我们总结下RAID0表现：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容量&lt;/strong&gt;：线性提升，RAID容量=单个磁盘·N&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可靠性&lt;/strong&gt;：0，没有任何可靠性措施&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能&lt;/strong&gt;：线性提升，RAID性能=单个磁盘·N&lt;/p&gt;
&lt;h1&gt;RAID 1 镜像&lt;/h1&gt;
&lt;p&gt;RAID1开始，带上了机制以保证可靠性。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/fb486189f2af0d84c73fb52caa7ac8f0.webp&quot; alt=&quot;QQ_1722677341706&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到，通过巧妙的分块，我们对每个分块做了一份&lt;strong&gt;冗余&lt;/strong&gt;。每个分块我们都会保存一份镜像在另外的硬盘上。&lt;/p&gt;
&lt;h2&gt;评估RAID 1&lt;/h2&gt;
&lt;p&gt;依旧从3个角度去评估。我们在这里只评估所有数据仅有一份冗余的情况，因为更多份冗余的场景也很容易按照这种方式去计算&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;容量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很显然，容量的降低取决于我们镜像的数量，在只备份一份的情况下，我们的容量变为了N/2。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可靠性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从可靠性上来看，RAID1最佳情况下可以容忍N/2的故障。即D0和D2都挂了。最差可以容忍1的故障，即D0-D3任意一个挂了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这里有一点比较特殊，因为我们提到了跨磁盘之间的操作可能是并行的，那么好像读或者写2份在同一个磁盘的数据好像对性能的影响微乎其微？&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;顺序读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	但是其实不是这样，考虑到我们要计算综合吞吐的话，在顺序读时，因为磁盘分块被我们分割开了，哪怕是顺序读，0-1-2-3-4-5-6也可能跨了非常多的磁盘（D0,D2,D0,D2,D0,D2)，此时考虑到还有额外2种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取时因为有2份数据，所以可能不只是2个盘，而是D0D1D2D1D3&lt;/li&gt;
&lt;li&gt;磁盘本身会旋转，本身的顺序读被完全割裂了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以&lt;strong&gt;顺序读&lt;/strong&gt;的实际吞吐可能接近$(\frac{N}{2}·S) MB/s$&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;顺序写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	顺序写也是一样，因为要保证2份数据同时写完才算写成功，即使可以并发写，但是会带来另一个磁盘可能刚好错过扇区的成本，所以顺序写的吞吐也是$(\frac{N}{2}·S) MB/s$&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;随机&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	随机读反而不受影响，因为本来就要承担该承担的一切，所以还是N·R&lt;/p&gt;
&lt;p&gt;​	随机写于顺序写一样，也是$(\frac{N}{2}·R) MB/s$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ps&lt;/strong&gt;：这里文中的计算看看大概思路了解需要考虑那些额外因素就好，实际现实上不能这么算，加深对磁盘运作机制的理解就可以&lt;/p&gt;
&lt;h1&gt;RAID 4 通过奇偶校验节省空间&lt;/h1&gt;
&lt;p&gt;RAID4是一种优化，通过奇偶校验节省副本所需要的空间&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/516c77e3f884e7b3a46554c78571de6b.webp&quot; alt=&quot;QQ_1722834954685&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;奇偶校验可以使用一个额外的盘上的分块，来存储前面一系列分块的奇偶校验和，以此来节省空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;奇偶校验保存方式&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于同一行的分块，都在Disk4上有一个额外的分块，来保存奇偶校验值&lt;/li&gt;
&lt;li&gt;奇偶校验值保存了前面D0-D3中对应分段的每一个bit位的奇偶校验值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;奇偶校验位的含义以及计算&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;奇偶校验位为0或1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;奇偶校验位与它所对应的bit，1的数量必须是偶数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快速计算的话，只需要对前面的位整个进行异或&lt;strong&gt;XOR&lt;/strong&gt;，得到的就是奇偶校验位，比如下图，0^0^1^1=0，0^0^1^0=1&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Disk0&lt;/th&gt;
&lt;th&gt;Disk1&lt;/th&gt;
&lt;th&gt;Disk2&lt;/th&gt;
&lt;th&gt;Disk3&lt;/th&gt;
&lt;th&gt;Disk4（奇偶校验bit）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;奇偶校验位如何做到冗余&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;奇偶校验位可以完成1个盘损坏情况下的冗余，可以认为是1档冗余&lt;/li&gt;
&lt;li&gt;因为奇偶校验位的特性（奇偶校验位bit与所有数据bit中1的数量必须为偶数），所以我们可以简单的拥有以下恢复策略当某个盘不可读取时，再次检查整行的值
&lt;ul&gt;
&lt;li&gt;如果此时1的数量为奇数，说明丢失的那bit是1&lt;/li&gt;
&lt;li&gt;相反，如果此时1的数量还是为偶数，那么说明丢失的那bit是0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;评估RAID 4&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;容量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;无论多少个磁盘，奇偶校验只需要一个额外磁盘保存奇偶校验位，所以容量是(N−1)·B&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可靠性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从原理上来说，奇偶校验位的可靠性最多允许一个disk坏掉&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺序读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺序读我们可以正常操作除奇偶校验盘外的所有，所以是(N−1)·SMB/s&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺序写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;顺序写时，考虑到一种情况：我们可以直接一行一行块的写，即直接从分块0写到分块P0，这个时候直接在写前面的时候也就把奇偶校验计算出来了，所以写的速度也是(N−1)·SMB/s&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;随机读&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;随机读与上面类似，所以还是（N-1）·R&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;随机写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;随机写十分复杂，要考虑的点非常多，是一个重点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;首先是如何得到新奇偶校验的值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在写入某个块时，奇偶校验位的值也需要被更新，我们想一下，最基本的更新策略可以是这样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更新某个分块（比如说Disk0上的0）时，更新后，我们读取同一行的所有数值，重新计算奇偶校验并写入P0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是这显然成本过高了，更新一个分块，反而要读取整行所有的分块吗？考虑到奇偶校验和的来源，我们可以有另一种简单的办法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过这个分块的原值、新写入的值、奇偶校验位的值，来计算新的奇偶校验位——$P_{new}= (C_{old}⊕C_{new})⊕P_{old}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;然后，写入会存在很明显的瓶颈&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在随机写场景，当随机写连续发生在2个disk时，按照我们之前的估计，2个disk本身是支持并发一起写的，但是引入奇偶校验后，会出现问题，如下图&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/d12720e97d9ccdf2985d034b1fffa24d.webp&quot; alt=&quot;QQ_1722837503526&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;我们会发现，及时4和13因为分别位于D0和D1，他们之间可以并发进行写，但是因为他们&lt;strong&gt;都需要更新奇偶校验和&lt;/strong&gt;，所以都需要在D4上去写，&lt;strong&gt;D4成为了随机写的瓶颈&lt;/strong&gt;！&lt;/p&gt;
&lt;p&gt;事实上，在这种场景下，整个随机写最终都会成为在D4上的串行写，所以我们随机写的性能，不只达不到N*R，甚至小于了单个磁盘的R，考虑到每次写入奇偶校验位时，都需要先读取原来的校验位来做计算，所以实际随机写的性能大约是$(\frac{R}{2})MB/s$（因为又要读一次又要写一次，所以大概是一半）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ps&lt;/strong&gt;：还是那句话，这种算法知道考虑的点就行，实际的值准不准确别过于认真&lt;/p&gt;
&lt;h1&gt;RAID 5 轮流保存奇偶校验位&lt;/h1&gt;
&lt;p&gt;RAID5也是一种优化，来优化RAID4的性能瓶颈&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/08/4d14bd2914b85bdf0d29b44f09cff2ca.webp&quot; alt=&quot;QQ_1722837993888&quot; style=&quot;zoom:50%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;参考这个图，RAID 5不再使用一个固定的盘来保存奇偶校验位，而是每个盘轮流在一行中使用一个分段来保存奇偶校验位。在评估之前，我们首先就可以发现它主要就是规避了RAID4中使用一个盘来做奇偶校验盘的单写问题。&lt;/p&gt;
&lt;h2&gt;评估RAID 5&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;容量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;与RAID 4一样&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可靠性&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;与RAID 4一样&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;除了随机写以外，基本都与与RAID 4一样，随机读会稍微好一些，因为我们现在可以读所有磁盘了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;随机写&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;随机写会提升不少性能，因为我们避免了写阻塞在一个盘上的问题，但是因为奇偶校验位会在任意盘上，所以我们在大量随机写时，所有盘都在狂飙（写D0时要写D4的校验，写D4时又要写D0的校验，互相之间都是套娃）。我们预估这种情况下，实际写入的速度估计是$\frac{N}{4}·R$&lt;/p&gt;
&lt;h1&gt;RAID总结&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/08/4d2a3943849d71f25c292ec8fa1fb4dc.webp&quot; alt=&quot;QQ_1722838748865&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RAID是在软件层（磁盘控制读取写入）做一系列设计，去实现多磁盘的组合使用，其中几个主要方案就是这章中介绍的，其他的RAID2、3、6可以自己再去了解下，但是核心思路都已经在本章中的几个方案里提到了。&lt;/p&gt;
&lt;p&gt;RAID方案的取舍也是取决于你对容量、性能、可靠性的权衡取舍。个人感觉对于家里的NAS来说，基本上RAID-5就是最佳方案了。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 37 Hard Disk Drives</title><link>https://www.intotw.cn/posts/ostep-37-hard-disk-drives/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-37-hard-disk-drives/</guid><pubDate>Tue, 30 Jul 2024 05:53:13 GMT</pubDate><content:encoded>&lt;p&gt;这章主要聚焦到了编写硬盘驱动上。&lt;/p&gt;
&lt;p&gt;在开始编写之前，我们首先需要知道硬盘的实际构造以及查询逻辑。&lt;/p&gt;
&lt;h1&gt;磁盘构造&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/e7f50c3975cf8e4c63a2b6d595fe95a5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考上图，上图表示了硬盘的一层，或者说一盘，实际硬盘是有多层堆叠起来的。我们介绍一下它&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最上面的&lt;strong&gt;Rotates this way&lt;/strong&gt;，表示了硬盘转动的方向&lt;/li&gt;
&lt;li&gt;中间的点&lt;strong&gt;Spindle&lt;/strong&gt;，表示了转轴，每片硬盘围绕着转轴旋转&lt;/li&gt;
&lt;li&gt;图中从内到外我们可以看到分三圈，每圈上有数字，从内到外的每圈就是&lt;strong&gt;磁道&lt;/strong&gt;，磁道上的数字所表示的就是磁道上的&lt;strong&gt;扇区&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;我们保存的数据实际上是以&lt;strong&gt;扇区&lt;/strong&gt;来做划分的。例如，我们在获取数据时，只需要care数据保存在哪个扇区上&lt;/li&gt;
&lt;li&gt;左下角那个圆柱体，表示了磁臂和刺头，你可以看到磁头点到了26扇区，说明目前在读写26扇区内的数据，磁头通过对应扇区中的某个点是否被磁化（表示0/1），来读取数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结合这个构造，我们可以过一下读取数据的过程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户发起读取某个文件的请求，OS获取文件系统中保存的该文件对应的磁道和扇区&lt;/li&gt;
&lt;li&gt;OS发送指令，告知磁盘读取指定磁道和扇区的数据&lt;/li&gt;
&lt;li&gt;需要注意，整个磁头的定位过程中，&lt;strong&gt;磁臂和磁头是几乎不动的&lt;/strong&gt;，它们几乎是在原地等到磁盘旋转到对应位置，然后去读取&lt;/li&gt;
&lt;li&gt;为什么说几乎不动，因为磁臂会控制磁头在磁道间移动，也就是说，&lt;strong&gt;磁道间的移动依靠磁臂&lt;/strong&gt;，&lt;strong&gt;扇区间的移动依靠磁盘转轴的旋转&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;IO时间的计算&lt;/h1&gt;
&lt;p&gt;在了解了磁盘的结构以及磁盘读取数据的步骤后，我们就可以计算一下磁盘IO的时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/5ebd77547017537cd32c33fb2d91ff07.webp&quot; alt=&quot;image-20240730143853928&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I/O的时间等于寻道时间+旋转时间（定位到扇区的时间）+数据传输时间（定位到扇区后，读写数据需要花费的时间）&lt;/p&gt;
&lt;p&gt;很明显&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;寻道时间于磁臂移动的速度有关&lt;/li&gt;
&lt;li&gt;旋转时间与磁盘转速有关&lt;/li&gt;
&lt;li&gt;数据传输速度和磁盘读写速度有关&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给定2个磁盘的参数，我们就可以对比2个磁盘间的IO速度差异&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/e68d3baa79fe5a23050fe4bfca61b1cc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;平均寻道时间用于计算随机读写时的情况，顺序读写时，其实寻道时间和转速基本不需要考虑，实际速度接近Max     Transfer&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通常上，平均寻道时间AVERAGE SEEK是最长寻道时间0-N的$\frac{1}{3}N$，这是通过对所有可能的寻道时间积分计算而来的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;磁盘的调度算法&lt;/h1&gt;
&lt;p&gt;处于磁盘这种寻道和定位扇区的独特设计，磁盘对于IO请求，会有自己的调度算法，以此来提升IO效率&lt;/p&gt;
&lt;h2&gt;最短寻道时间&lt;/h2&gt;
&lt;p&gt;这个算法的思路以及实现都很简单，对于到来的IO按照磁道的远近进行排序，离当前磁道最近的IO会被优先执行，以此来减少磁道的移动时间消耗。&lt;/p&gt;
&lt;p&gt;但是这会带来一个巨大的问题：&lt;strong&gt;饥饿&lt;/strong&gt;，在这种逻辑下，位于最两侧（最内和最外）的磁道经常得不到调度，会陷入饥饿，当然，饥饿也是可以通过调度算法的设计去解决的。比如我们现在可以脑拍一个算法：&lt;strong&gt;对于那些太久没被调度的，会拥有一次优先调度的晋升机会&lt;/strong&gt;，也就是再额外对每个IO请求维护一个未被调度次数，以此来降低饥饿的影响。&lt;/p&gt;
&lt;h2&gt;电梯算法&lt;/h2&gt;
&lt;p&gt;电梯算法的从命名就可以看出它的逻辑，它像电梯一般，总是从磁道最内部移动到最外部，然后再移回内部，循环往复。&lt;/p&gt;
&lt;p&gt;电梯算法可以有效避免饥饿，但是它忽略了磁盘的旋转，大大降低了IO的效率，&lt;em&gt;想象一下，当磁道由内而外移动了2圈时，你要读取的扇区都刚好错过了，或者说都刚好需要完整的等待磁盘转完一圈&lt;/em&gt;。&lt;/p&gt;
&lt;h2&gt;最短距离算法&lt;/h2&gt;
&lt;p&gt;因为我们可以通过当前磁头所在的磁道以及扇区位置，结合寻道时间以及转速，直接计算出从当前点（p1,s1）-（p2,s2）的cost。所以我们可以对即将进行的IO进行排序，尽可能综合寻道速度和转速，来挑选下一个执行的IO。&lt;/p&gt;
&lt;p&gt;这种算法的缺点是需要知道磁头当前所在的位置，所以OS无法实现，这种算法现在往往由磁盘内部直接实现。&lt;/p&gt;
&lt;h2&gt;其他内容&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;磁盘调度由谁实现？&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;现在磁盘的调度现在往往由磁盘内部直接实现，因为OS无法获取磁盘的具体详细状态（比如每个磁头位置），所以现在磁盘都自带一个芯片，以此来实现执行调度算法，并且控制磁盘的cache。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;磁盘IO的合并：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;磁盘往往会等待一些IO，并以一定周期来将那些可能是连续或者就近的IO合并，比如我们以s表示扇区的话，到来的IO次序分别是（s37，s1，s38）时，明显将s37与s38合并后执行，可以少等待一圈磁盘旋转的时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;等待时间以及取舍：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不论从算法的角度，还是IO合并的角度，我们都是在做一种取舍：&lt;strong&gt;不马上进行到来的IO，而是在接受一定数量的IO请求以后，通过算法来调度这些IO以实现整体IO效率的提升&lt;/strong&gt;。所以我们在发出一个IO时，往往需要等待，因为它并不是被马上执行的。所以，等待多久（多少次IO到来后进行一次决策），这是一个复杂而又困难的问题。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 36 Io Device</title><link>https://www.intotw.cn/posts/ostep-36-io-device/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-36-io-device/</guid><pubDate>Mon, 29 Jul 2024 08:52:44 GMT</pubDate><content:encoded>&lt;p&gt;第三part持久化，开始讲设备I/O了。&lt;/p&gt;
&lt;h1&gt;体系结构&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/6c415135ca5a7320e331d55f255ddc07.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整个I/O的体系结构基本如图。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU通过一些专门的高速线路，直接连接显卡和内存&lt;/li&gt;
&lt;li&gt;再通过DMI（Direct Media Interface），与专门的IO芯片相连&lt;/li&gt;
&lt;li&gt;其他设备通过与IO芯片相连，以此连接到CPU（如网络的PCIE，USB的键鼠，eSATA硬盘）&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;硬件连接相关&lt;/h1&gt;
&lt;h2&gt;硬件如何和OS交互？&lt;/h2&gt;
&lt;p&gt;硬件设备通过遵守某种规范的协议与OS进行交互，硬件本身会有一些寄存器位或者自己的芯片，通过寄存器位的数据进行出入参以及指令的控制，并由硬件的芯片去执行具体的操作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/d05a9a0057a068cc4f7007a7ba8b403d.webp&quot; alt=&quot;image-20240729172336978&quot; /&gt;&lt;/p&gt;
&lt;p&gt;除了通过寄存器交互外，还有一种交互方式，即&lt;strong&gt;内存映射&lt;/strong&gt;，在内存中直接映射一块内存区域，这块内存区域对应了这个设备的内存，OS和设备通过这部分内存，来完成通信以及交互。&lt;/p&gt;
&lt;h2&gt;硬件和OS交互的性能&lt;/h2&gt;
&lt;p&gt;硬件和OS交互的话，最基本的话就是通过上图的那个方式，通过轮询status获取硬件设备的可读可写状态，然后从Data写入参数或者读取返回，通过Command写入需要硬件去执行的指令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;OS如何通过Status来感知设备状态？朴素的方式就是轮询，但是轮询会带来大量的CPU空转，所以解决方案就是：&lt;strong&gt;使用中断&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;操作系统可以在发起IO后直接继续CPU操作，等待设备发起中断，以此来再去进行后续操作（而不是轮询等待IO完成），这样CPU计算和IO就可以并行执行了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/2f89cecee3257437835b23a2d171fe9f.webp&quot; alt=&quot;image-20240729172908316&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再次优化&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在IO比较快的情况下，频繁的中断又会有大量的上下文切换开销，所有我们又可以使用老思路来优化：&lt;strong&gt;两阶段&lt;/strong&gt;，我们先进行一定次数的轮询，然后再进入等待中断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;再次优化-DMA&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;更进一步，在最普遍的场景下（即内存落盘），是需要CPU持续从内存中搬运数据并写到磁盘中的，面对这种情况，工程师们设计了一个专门用于搬运的硬件-DMA（Direct Memory Access直接内存访问）。DMA是一种特殊的设备，专门用于将内存中指定位置的内存，写入到硬盘的指定位置中去，借住DMA设备，CPU在需要进行类似大量落盘的操作时，可以丢给DMA一个指令，自己继续去进行计算工作了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/f04c83c94cc850ce8b8a2434d57fa123.webp&quot; alt=&quot;image-20240729173443625&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;驱动&lt;/h2&gt;
&lt;p&gt;连接OS和硬件设备之间的程序，我们称之为驱动这个概念应该基本都有认识，下到键盘鼠标耳机，上到网卡显卡声卡，操作系统装完机的第一件事基本都是先装驱动。&lt;/p&gt;
&lt;h3&gt;驱动的体系结构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/097ebd8a4cb19e6b3dc8cde773f570a2.webp&quot; alt=&quot;image-20240729174244879&quot; /&gt;&lt;/p&gt;
&lt;p&gt;驱动的规范也分几层&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用和操作系统之间，即API到Generic Block Interface，这层是用户api到操作系统之间的规范定义，由操作系统提供了标准的接口，然后再包装成一些易用的API给用户程序去使用&lt;/li&gt;
&lt;li&gt;操作系统与实际驱动程序之间，这一层是操作系统将一些实际驱动程序提供的能力，封装成操作系统通用的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么这么设计？我们可以举个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件系统对通用规范发起操作，不需要知道实际读写的是什么设备&lt;/li&gt;
&lt;li&gt;各种磁盘设备（比如HDD和SDD），他们实现各自驱动以满足操作系统的通用规范，以此来使得文件系统可以使用通用规范对他们进行读写&lt;/li&gt;
&lt;li&gt;同时，通用规范可以通过接口的定义控制，来使得一些程序可以访问更加特殊的功能，例如一些程序需要进行磁盘碎片整理，或者获取某些设备的一些参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;驱动的实现的例子&lt;/h3&gt;
&lt;p&gt;这里直接贴设计和代码，挺好理解的，就是针对设备的寄存器以及指令，实现操作系统的接口&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设备的定义&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/7d4dd90a18f9a4f5dce23b5cdff892ec.webp&quot; alt=&quot;image-20240729175559078&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面是设备的定义，可以看到几个寄存器，其中有表示状态的，有表示错误的，有表示指令的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;根据定义去实现接口&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/5285e8d9cb6afd0d2ac96f3b4fb04b89.webp&quot; alt=&quot;image-20240729175916026&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是部分简单的实现，一般来说一个驱动需要实现以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wait for drive to be ready.&lt;/strong&gt; 等待设备就续&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write parameters to command registers&lt;/strong&gt; 将参数（入参，比如需要访问的扇区、块、访问的大小）写到寄存器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start the I/O.&lt;/strong&gt; 通过指令寄存器，发起io申请&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data transfer (for writes):&lt;/strong&gt; 等待设备读写完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handle interrupts.&lt;/strong&gt; 处理中断&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error handling.&lt;/strong&gt; 处理异常&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Ostep Second Part Summary</title><link>https://www.intotw.cn/posts/ostep-second-part-summary/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-second-part-summary/</guid><pubDate>Fri, 19 Jul 2024 05:47:37 GMT</pubDate><content:encoded>&lt;p&gt;第二部分到此也就结束了。这边按照惯例做一下总结。&lt;/p&gt;
&lt;p&gt;并发整块内容包括整章，主要还是描述了并发为什么会产生，以及操作系统如何结合CPU硬件去一步步构造一些使用的api来供开发者避免或者解决这些并发问题。&lt;/p&gt;
&lt;p&gt;第二部分主要是通过一些思路来介绍的，更加详细的内容推荐美团技术团队的一篇文章，会有一些细节的补充&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.meituan.com/2024/07/19/multi-threading-and-multi-thread-synchronization.html&quot;&gt;基本功 | 一文讲清多线程和多线程同步&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其实博客总结和通过书籍系统的学习各有利弊。博客会更加专注于罗列知识点和内容。书籍更多的是从问题启发到引导你思考解决方案，让你自己对问题的本质具备更深层次的了解&lt;/p&gt;
&lt;p&gt;这个其实也涉及到了很多同学会遇到的一个问题：为什么背过、看过很多，但是稍微过段时间就忘了？并且难以拿来实际解决问题？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;博客是知其然，看书是知其所以然&lt;/li&gt;
&lt;li&gt;博客的内容也会参差不齐，如何判断博客本身的对错？&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;言归正传，提炼一下第二章的核心&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;并发问题发生的本质&lt;/strong&gt;，由**共享变量（资源）**产生，分两类情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作本身缺失原子性，导致并发情况下没有原子性，例如，i++&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;操作系统线程调度顺序的不确定性下，导致的执行顺序异常，例如，子线程创建后马上执行并访问了父进程从未初始化的变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​	所以，面对并发问题，在我们&lt;strong&gt;使用共享变量&lt;/strong&gt;或者&lt;strong&gt;发现一个变量被共享&lt;/strong&gt;时，一定要审视一下他的并发安全性&lt;/p&gt;
&lt;p&gt;处理并发问题，我们主要依靠的是&lt;strong&gt;CPU提供的原子操作&lt;/strong&gt;以及操作系统提供的&lt;strong&gt;线程让出cpu的等待以及唤醒功能&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过前一个功能，即CPU提供的原子操作，我们可以实现互斥锁，或者原子变量&lt;/li&gt;
&lt;li&gt;通过后一个功能，我们可以实现线程间的阻塞等待，并且可以优化互斥锁的&lt;strong&gt;自旋&lt;/strong&gt;操作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;通过前面讲到的2个基本能力，我们就可以实现2个并发基本工具：锁，以及条件变量&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前者用于提供互斥，保证某段存在并发问题的代码互斥执行&lt;/li&gt;
&lt;li&gt;后者用于提供阻塞等待以及唤醒，用于实现线程间的状态同步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过上面的2个基本工具去解决并发问题，又会遇到新的问题，死锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	死锁主要就产生于，2个线程之间已经互相持有了彼此需要的一个互斥资源，并为另一个被对方持有得资源等待的情况&lt;/p&gt;
&lt;p&gt;​	额外需要注意的一点，在你使用许多api时，api的实现本身可能会获取某些互斥资源。这种场景也导致了很多死锁的发生。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;避免死锁的核心思路&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;避免互相持有互斥资源&lt;/li&gt;
&lt;li&gt;进行有限尝试或者等待（即尝试获取n次或者只等待m秒，失败后直接退出并释放自己持有的互斥资源）&lt;/li&gt;
&lt;li&gt;使用对应无锁实现（如果存在）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;并发问题逻辑大纲&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多线程下执行下，线程调度+共享变量产生&lt;strong&gt;临界代码&lt;/strong&gt;，有并发问题&lt;/li&gt;
&lt;li&gt;硬件以及操作系统提供特性以实现&lt;strong&gt;并发工具&lt;/strong&gt;，解决并发问题&lt;/li&gt;
&lt;li&gt;并发工具在某些场景下会使用产生&lt;strong&gt;死锁问题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;死锁问题的&lt;strong&gt;避免以及恢复&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;额外扩展&lt;/h1&gt;
&lt;h2&gt;Event loop&lt;/h2&gt;
&lt;p&gt;一种单线程实现并发执行的模型，在处理大量NIO的场景有最好的表现。&lt;/p&gt;
&lt;h2&gt;内存屏障&lt;/h2&gt;
&lt;p&gt;书中没写，内存屏障是用于解决CPU的缓存共享问题以及指令重排序问题的，CPU的cache以及store buffer在更新数据后，虽然有同步机制保证，但是无法保证马上写入到内存，此时其他CPU从内存中取到的可能就是旧值。&lt;/p&gt;
&lt;h3&gt;内存屏障的作用&lt;/h3&gt;
&lt;p&gt;​	内存屏障通过确保特定顺序的内存操作来防止处理器乱序执行带来的问题，尤其是在多核或多线程环境下。内存屏障主要通过以下方式保证内存访问的顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;防止乱序执行&lt;/strong&gt;: 处理器通常会为了提高效率而乱序执行指令。内存屏障通过强制执行内存操作的顺序来防止这种乱序执行。例如，一个写屏障可以确保在它之前的所有写操作在它之后的所有写操作之前完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确保缓存一致性&lt;/strong&gt;: 在多处理器系统中，每个处理器都有自己的缓存。内存屏障可以确保一个处理器的内存操作对于其他处理器是可见的。例如，写屏障可以确保一个处理器的写操作在其他处理器读取到之前完成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保证同步操作的正确性&lt;/strong&gt;: 内存屏障在实现锁、信号量等同步机制时非常重要。它可以确保临界区内的操作按预期顺序执行，从而避免竞态条件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;简而言之，内存屏障指令保证在指令前的操作全部已经落到了内存，并且防止指令前后的操作被重排序。&lt;/p&gt;
&lt;h2&gt;伪共享&lt;/h2&gt;
&lt;p&gt;之前写netty的时候讲过，也是CPU缓存的问题，当多个处理器核在同一缓存行（通常是64字节）上操作不同数据时，导致频繁的缓存一致性的流量风暴，反而影响了缓存的性能。&lt;/p&gt;
&lt;p&gt;此时填充一些无用变量通过offset以使得缓存行不在同一行即可。这种现象主要发生在对高并发性能要求较高的数据结构（例如高并发队列）。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 33 Event Based Concurrency</title><link>https://www.intotw.cn/posts/ostep-33-event-based-concurrency/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-33-event-based-concurrency/</guid><pubDate>Thu, 18 Jul 2024 09:14:06 GMT</pubDate><content:encoded>&lt;p&gt;这章详细描述了之前20年看netty源码时就了解过的一个概念：&lt;strong&gt;eventloop&lt;/strong&gt;，&lt;a href=&quot;https://www.intotw.cn/2020/10/15/netty-eventloop/&quot;&gt;Netty以及Eventloop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;eventloop实际上是一个并发模型，基于事件驱动，用于实现在单线程或者单进程上的并发执行（是的，单线程也可以并发执行，没想到吧。&lt;/p&gt;
&lt;p&gt;redis，node.js，浏览器，都大量使用到了这个并发模型。它的代码可能如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (1) {
events = getEvents();
for (e in events)
processEvent(e);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Event loop的优缺点&lt;/h1&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Event loop基于单线程实现，可以很自然的达到既&lt;strong&gt;并发&lt;/strong&gt;，又不需要考虑&lt;strong&gt;并发问题&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;能提升CPU利用率，因为在Event Loop优点可以发挥时，就可以使用更少的线程完成更多的I/O任务，减少上下文切换&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;缺点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实现上是有一定复杂度的，所有方法或者说函数，都需要封装成event&lt;/li&gt;
&lt;li&gt;只在所有event基本都是异步或者小任务的情况下，具备优点
&lt;ul&gt;
&lt;li&gt;如果必须有同步I/O，那整个event都会hang在那个同步I/O上阻塞等待&lt;/li&gt;
&lt;li&gt;如果有大任务，同样的这个大任务会基本直接阻塞整个event轮转&lt;/li&gt;
&lt;li&gt;与操作系统页的设计难以配合，操作系统缺页等操作是会触发&lt;strong&gt;隐式中断&lt;/strong&gt;的，依旧会影响执行性能&lt;/li&gt;
&lt;li&gt;异步I/O磁盘依旧难以实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;实现上是有一定复杂度的，所有方法或者说函数，都需要封装成event&lt;/li&gt;
&lt;li&gt;单线程使用还好，多线程都使用各自的eventloop时，并发问题依旧可能存在&lt;/li&gt;
&lt;li&gt;上下文管理困难，event之间如果需要上下文（共享变量），那么需要自己实现&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;NIO&lt;/h1&gt;
&lt;p&gt;nio部分的内容之前写过了，还是比较详细的，可以看之前的&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;综合Event Loop的优缺点，我们会发现它最适合做的事，其实是作为大量NIO和AIO管理的Handler，即作为一个总体的handler去处理大量的NIO以及AIO请求（无论接受请求还是发起请求，即作为一个代理）。这才是最适合Event Loop发挥的场景，它就是为此而生的。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 32 Common Concurrency Problems</title><link>https://www.intotw.cn/posts/ostep-32-common-concurrency-problems/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-32-common-concurrency-problems/</guid><pubDate>Thu, 18 Jul 2024 06:55:50 GMT</pubDate><content:encoded>&lt;p&gt;这章主要介绍并发场景下常见的bug，并发场景下主要常见的bug分2类：非死锁bug和死锁bug。文中列出了之前&lt;/p&gt;
&lt;p&gt;Lu研究的目前常见的主流数据库和web server中包含的一些并发bug，并对其做了分类。&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/8c38425992c940256a762a953731f727.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;非死锁bug&lt;/h1&gt;
&lt;h2&gt;Atomicity-Violation Bugs 原子性违反&lt;/h2&gt;
&lt;h3&gt;bug描述&lt;/h3&gt;
&lt;p&gt;这类bug的特点就是多线程之间，忽略了对共享变量的原子性问题。也就是最常见的一类并发问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thread 1::&lt;/p&gt;
&lt;p&gt;if (thd-&amp;gt;proc_info) {&lt;/p&gt;
&lt;p&gt;fputs(thd-&amp;gt;proc_info, ...);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Thread 2::&lt;/p&gt;
&lt;p&gt;thd-&amp;gt;proc_info = NULL;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;线程1判断某个变量为null时执行某逻辑，但是判断通过后，另一个线程将该变量直接初始化为null，此时线程1继续执行会得到空指针异常。&lt;/p&gt;
&lt;h3&gt;解决方式&lt;/h3&gt;
&lt;p&gt;这类bug最简单的解决方式就是加锁。因为这本质上是&lt;strong&gt;临界代码&lt;/strong&gt;问题。如果是对数据结构操作，可以使用并发安全的数据结构并且使用setIfAbsent等操作来保证原子性。&lt;/p&gt;
&lt;h2&gt;Order-Violation Bugs 执行顺序违反&lt;/h2&gt;
&lt;h3&gt;bug描述&lt;/h3&gt;
&lt;p&gt;这类bug的特点就是多线程之间，忽略了并发时执行顺序的不确定性：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Thread 1::&lt;/p&gt;
&lt;p&gt;void init() {&lt;/p&gt;
&lt;p&gt;mThread = PR_CreateThread(mMain, ...);&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;Thread 2::&lt;/p&gt;
&lt;p&gt;void mMain(...) {&lt;/p&gt;
&lt;p&gt;mState = mThread-&amp;gt;State;&lt;/p&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;线程1创建了线程2后，并赋值给变量mThread&lt;/p&gt;
&lt;p&gt;线程2访问mThread变量的State属性&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是注意此时mThread = PR_CreateThread(mMain, ...);这句赋值语句与线程2执行的顺序是不可控的，线程2创建后执行时，可能mThread并没有赋值为PR_CreateThread的返回值，此时线程2去读取mThread-&amp;gt;State，会直接空指针&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;错误时序如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程1执行PR_CreateThread创建并执行线程2的mMain&lt;/li&gt;
&lt;li&gt;线程2创建&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度切换到线程2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;线程2执行（这里就报错空指针了）&lt;/li&gt;
&lt;li&gt;线程1继续执行，将PR_CreateThread的返回值赋值给mThread&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解决方式&lt;/h3&gt;
&lt;p&gt;这类bug最本质的解决就是加入条件变量，因为你会发现本质上就是需要在mThread被赋值以后，然后mState = mThread-&amp;gt;State;再去执行，是线程之间执行的条件先后依赖问题。&lt;/p&gt;
&lt;h2&gt;无锁bug总结&lt;/h2&gt;
&lt;p&gt;据研究，97%的无锁bug都是以上两类问题。&lt;/p&gt;
&lt;p&gt;针对这两类问题，需要我们在对&lt;strong&gt;共享变量的使用&lt;/strong&gt;导致产生&lt;strong&gt;临界代码&lt;/strong&gt;时&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;具备前置的识别能力，对这部分代码或者场景需要具备敏锐度&lt;/li&gt;
&lt;li&gt;在识别后，分析原因并通过并发问题常见解决思路去分析解决问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要知道，&lt;strong&gt;并发问题总是发生在共享变量的时候，在你意识到某个变量被多线程共享访问时，就应该多加思索&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;死锁bug&lt;/h1&gt;
&lt;h2&gt;发生&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;我们为什么常常会发生死锁？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一些api或者数据结构的封装，里面其实使用到了锁，但是我们在使用时如果没有提前了解，就会触发死锁&lt;/li&gt;
&lt;li&gt;工程复杂性上来以后，巨大的依赖以及代码量，让我们很难像一开始设计时理清楚多个线程间锁的使用关系以及顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;发生死锁&lt;strong&gt;必须&lt;/strong&gt;具备一下四个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;互斥&lt;/strong&gt;：线程间声称他们对同一个资源的互斥访问（例如，线程间争抢一个锁）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持有并且进入等待&lt;/strong&gt;：线程持有了某个资源，并且因为需要获得额外的资源而进入等待（例如，线程获取了一个锁L1时，需要再次去获取L2，但是获取L2时进入了等待）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不允许抢占&lt;/strong&gt;：当线程持有某个资源时，无法被其他线程强占（例如，线程获取了一个锁，此时所有其他线程无法通过任何方式强行抢到这个锁）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环等待&lt;/strong&gt;：存在一个线程的循环链，这样每个线程持有一个或多个资源(例如锁)，这些资源正在被链中的下一个线程请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;当出现这些问题以后、如何解决？&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;避免&lt;/h2&gt;
&lt;h3&gt;循环等待&lt;/h3&gt;
&lt;p&gt;解决方式是对获取锁的顺序进行精心排序。&lt;/p&gt;
&lt;p&gt;假如我们有L1和L2这2个锁，如果我们在获取L2时总是先获取L1，就可以避免这个问题。&lt;/p&gt;
&lt;p&gt;这也需要我们总是对多个锁的获取去进行检查并且精心设计。&lt;/p&gt;
&lt;h3&gt;持有并且进入等待&lt;/h3&gt;
&lt;p&gt;解决方式很简单，在获取锁以前，获取一个global lock全局锁，获取相关的锁之前都先获取这个锁，来在外围避免里面的&lt;strong&gt;临界代码&lt;/strong&gt;，简而言之就是套娃。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/d78f490693876af2de74badfbe1ddbde.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是这种实现本身就很蠢，降低了并发度，并且对封装不利，因为我们需要在调用前了解到所有需要获取到的所有锁。&lt;/p&gt;
&lt;h3&gt;不允许抢占&lt;/h3&gt;
&lt;p&gt;这里就说到一个进阶玩法：tryLock&lt;/p&gt;
&lt;p&gt;tryLock可以很有效的避免死锁，在一段时间无法获取到锁后中断，从而退出来释放自己已经持有的锁，以此来避免死锁的发生。&lt;/p&gt;
&lt;h3&gt;互斥&lt;/h3&gt;
&lt;p&gt;互斥这里也很好解决，大部分时候，我们可以尝试将代码改造为无锁的或者不需要等待的&lt;strong&gt;自旋&lt;/strong&gt;尝试（本质上是修改为乐观锁），通过之前说的CAS等操作，我们可以很轻松的实现。&lt;/p&gt;
&lt;h3&gt;补充：时序避免&lt;/h3&gt;
&lt;p&gt;我们还可以通过CPU调度的时序来避免死锁。如果操作系统一定程度上知道我们获取锁的顺序，那么它就可以通过CPU的调度顺序控制，来避免死锁。这个思路比较像RUST的设计思路，通过预先知道（编译期解决问题），来避免问题。不过成本就是易用性会差很多。&lt;/p&gt;
&lt;h2&gt;从死锁中如何恢复&lt;/h2&gt;
&lt;p&gt;对于那些一年发生一次的，我们当然可以发生了就重启下。但是其他情况呢？&lt;/p&gt;
&lt;p&gt;我们这里以mysql举例，mysql中执行sql，当sql执行过久时，mysql会触发死锁警告，此时需要你通过命令强制关闭杀死事务并释放资源。&lt;/p&gt;
&lt;p&gt;大部分死锁恢复都是如此，通过检测（某事务中wait太久，或判断条件循环依赖（jvm））后，由用户自己来强制关闭出现问题的线程，释放导致死锁的资源。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 31 Semaphores</title><link>https://www.intotw.cn/posts/ostep-31-semaphores/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-31-semaphores/</guid><pubDate>Tue, 16 Jul 2024 08:58:22 GMT</pubDate><content:encoded>&lt;p&gt;这章主要介绍了信号量的使用，如何使用信号量来实现并发编程。&lt;/p&gt;
&lt;h1&gt;信号量的定义&lt;/h1&gt;
&lt;p&gt;sem_init(&amp;amp;s, 0, 1);&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;初始化语句，第二个参数0表示是线程间的（信号量也存在进程间的），1表示信号量的值value&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;int sem_wait(sem_t *s) {
// Decrement the value of semaphore s by one
// Wait if the value of semaphore s is negative
}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;信号量的等待语句，将信号量的值减1，如果减1后的值为负数，那么就进入wait等待&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;int sem_post(sem_t *s) {
// Increment the value of semaphore s by one
// If there are one or more threads waiting, wake one
}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;可以认为信号量的唤醒语句，将信号量的值加1，如果此时有正在等待的线程，那么唤醒其中一个线程&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意这里的wait注释的顺序和实际的逻辑可能会有些差异，实际上并不是每次都会减一，在value值已经是负数（或许是0）时，&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;会直接进入阻塞。在多个线程阻塞wait时，一次post唤醒以后加1，再由被唤醒的线程-1，然后再post，然后一直在0和1之间来回循环，直到所有线程被唤醒执行完毕（可以主要看生产以及消费者队列）&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;信号量实现并发&lt;/h1&gt;
&lt;h2&gt;信号量实现锁&lt;/h2&gt;
&lt;p&gt;只需要将信号量的value设置为1，就可以实现一个锁，执行过程如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/fdaf8c856ff799dc61de1aa94c5db05e.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一个线程执行wait时，value修改为了0，但是此时是非负数，所以继续执行&lt;/li&gt;
&lt;li&gt;当第二个线程再次去wait时，value从0修改为了-1，然后进入了阻塞等待&lt;/li&gt;
&lt;li&gt;第一个线程执行post，将值+1，然后此时唤醒等待中的线程&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;信号量实现两个线程按顺序执行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sem_t s;

void* child(void* arg) {
    printf(&quot;child\n&quot;);
    sem_post(&amp;amp;s); // signal here: child is done
    return NULL;
}

int main(int argc, char* argv[]) {
    sem_init(&amp;amp;s, 0, X); // X should be 0
    printf(&quot;parent: begin\n&quot;);
    pthread_t c;
    pthread_create(&amp;amp;c, NULL, child, NULL);
    sem_wait(&amp;amp;s); // wait here for child
    printf(&quot;parent: end\n&quot;);
    sem_destroy(&amp;amp;s); // Clean up semaphore
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要理解一个点，父子线程的执行顺序可能乱序，但是信号量通过他的语义依旧能保证按顺序执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;父线程的wait先执行&lt;/strong&gt;： 此时value=-1，然后子线程执行post，value回到0并且唤醒父线程&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;子线程的post先执行&lt;/strong&gt;：此时value变成1（0+1），然后父线程执行wait，将value-1，减1后value还是0，不需要阻塞，直接继续执行，依旧保证了顺序。&lt;/p&gt;
&lt;h2&gt;消费者生产者（限界队列）问题&lt;/h2&gt;
&lt;p&gt;这里分几个步骤，才能讲清并发下会出现的问题&lt;/p&gt;
&lt;h3&gt;基本实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int buffer[MAX];
int fill = 0;
int use = 0;

void put(int value) {
    buffer[fill] = value;        // Line F1
    fill = (fill + 1) % MAX;     // Line F2
}

int get() {
    int tmp = buffer[use];       // Line G1
    use = (use + 1) % MAX;       // Line G2
    return tmp;
}

sem_t empty;
sem_t full;

void* producer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;empty);        // Line P1
        put(i);                  // Line P2
        sem_post(&amp;amp;full);         // Line P3
    }
    return NULL;
}

void* consumer(void* arg) {
    int tmp = 0;
    while (tmp != -1) {
        sem_wait(&amp;amp;full);         // Line C1
        tmp = get();             // Line C2
        sem_post(&amp;amp;empty);        // Line C3
        printf(&quot;%d\n&quot;, tmp);
    }
    return NULL;
}

int main(int argc, char* argv[]) {
		
    sem_init(&amp;amp;empty, 0, MAX);    // MAX are empty
    sem_init(&amp;amp;full, 0, 0);       // 0 are full
    //more code
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	基本实现通过empty和full两个信号量，其中empty的值是value，生产者每次将empty减1（wait），并且对full加1（post），当生产的足够多时，如果生产速度大于消费速度，empty就从max变为负数，此时生产者进入等待。&lt;/p&gt;
&lt;p&gt;​	消费者每次对full进行-1，同理，消费速度大于生产速度时，那么消费者就进入阻塞等待。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这种实现在单消费者和单生产者时可以正常运作，但是在多消费者和多生产者时，get和put方法本身会出现并发问题，这点稍微理解下就知道，这两个操作不是原子操作&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;优化，但是会死锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void* producer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;mutex);        // Line P0 (NEW LINE)
        sem_wait(&amp;amp;empty);        // Line P1
        put(i);                  // Line P2
        sem_post(&amp;amp;full);         // Line P3
        sem_post(&amp;amp;mutex);        // Line P4 (NEW LINE)
    }
    return NULL;
}

void* consumer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;mutex);        // Line C0 (NEW LINE)
        sem_wait(&amp;amp;full);         // Line C1
        int tmp = get();         // Line C2
        sem_post(&amp;amp;empty);        // Line C3
        sem_post(&amp;amp;mutex);        // Line C4 (NEW LINE)
        printf(&quot;%d\n&quot;, tmp);
    }
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;临界问题？那简单，我加个互斥的锁不就好了？上面代码对get和put中加了锁，但是会导致新的问题：死锁。&lt;/p&gt;
&lt;p&gt;这里会出现一个很简单的死锁，当消费者先拿到锁后，并进入full的信号量等待，此时生产者永远拿不到锁，永远无法进入put去生产，两个线程一个永久在等待锁（生产者），一个永久在等待生产者生产内容（消费者），两个线程死锁了。&lt;/p&gt;
&lt;h3&gt;最终解决，更换一下锁和等待的顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void* producer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;empty);       // Line P1
        sem_wait(&amp;amp;mutex);       // Line P1.5 (MUTEX HERE)
        put(i);                 // Line P2
        sem_post(&amp;amp;mutex);       // Line P2.5 (AND HERE)
        sem_post(&amp;amp;full);        // Line P3
    }
}

void* consumer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        sem_wait(&amp;amp;full);        // Line C1
        sem_wait(&amp;amp;mutex);       // Line C1.5 (MUTEX HERE)
        int tmp = get();        // Line C2
        sem_post(&amp;amp;mutex);       // Line C2.5 (AND HERE)
        sem_post(&amp;amp;empty);       // Line C3
        printf(&quot;%d\n&quot;, tmp);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你会发现，消费者或者生产者会先一步进入等待条件变量再获取锁，这种解决方式其实你可以等价于在get和put方法中的开始和结束加入了锁，减小了锁的粒度，避免了死锁。&lt;/p&gt;
&lt;h2&gt;Read-Write Lock 读写锁&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void rwlock_acquire_readlock(rwlock_t* rw) {
    sem_wait(&amp;amp;rw-&amp;gt;lock);
    rw-&amp;gt;readers++;
    if (rw-&amp;gt;readers == 1) // first reader gets writelock
        sem_wait(&amp;amp;rw-&amp;gt;writelock);
    sem_post(&amp;amp;rw-&amp;gt;lock);
}

void rwlock_release_readlock(rwlock_t* rw) {
    sem_wait(&amp;amp;rw-&amp;gt;lock);
    rw-&amp;gt;readers--;
    if (rw-&amp;gt;readers == 0) // last reader lets it go
        sem_post(&amp;amp;rw-&amp;gt;writelock);
    sem_post(&amp;amp;rw-&amp;gt;lock);
}

void rwlock_acquire_writelock(rwlock_t* rw) {
    sem_wait(&amp;amp;rw-&amp;gt;writelock);
}

void rwlock_release_writelock(rwlock_t* rw) {
    sem_post(&amp;amp;rw-&amp;gt;writelock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个锁的实现其实和锁比较类似，只不过区分读写锁，这里只说下读写锁的特性：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取读锁时
&lt;ol&gt;
&lt;li&gt;首先读锁操作都需要一个大锁来保证读锁数量++的并发正确性。&lt;/li&gt;
&lt;li&gt;如果当前没有读锁，自己是第一个读锁持有者，那么去获取写锁，获取写锁成功后继续。&lt;/li&gt;
&lt;li&gt;如果已经有读锁了，那么就读锁持有+1&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;释放读锁时，判断如果当前自己是最后一个持有读锁的（读锁持有数为0），那么释放写锁。&lt;/li&gt;
&lt;li&gt;获取写锁时，直接获取写锁即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样的方式可以保证读写锁的特性：可以多个线程同时读，但是在读写互斥，写操作之间本身也是互斥的。&lt;/p&gt;
&lt;p&gt;读写锁只适合多读少写场景，但是会有较大写饿死的风险。&lt;/p&gt;
&lt;h2&gt;餐叉问题&lt;/h2&gt;
&lt;p&gt;这个问题我感觉实际意义不大，这里就不再总结了，主要表达了和前面生产消费者类似的问题，通过更换锁或者条件的依赖，来避免死锁。&lt;/p&gt;
&lt;h2&gt;Thread Throttling&lt;/h2&gt;
&lt;p&gt;线程节流，其实就是限流，我们可以通过控制信号量的值n，来做到只有n个线程能进入某段代码执行（相当于一个可以持有n个的令牌），多余的线程会被阻塞。以此来达到对系统某些资源使用的限流控制（比如大内存使用，大量计算，比较重的I/O）。&lt;/p&gt;
&lt;h1&gt;信号量自身的实现&lt;/h1&gt;
&lt;p&gt;我们通过锁和条件变量就能实现信号量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct __Zem_t {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
} Zem_t;

// only one thread can call this
void Zem_init(Zem_t* s, int value) {
    s-&amp;gt;value = value;
    Cond_init(&amp;amp;s-&amp;gt;cond);
    Mutex_init(&amp;amp;s-&amp;gt;lock);
}

void Zem_wait(Zem_t* s) {
    Mutex_lock(&amp;amp;s-&amp;gt;lock);
    while (s-&amp;gt;value &amp;lt;= 0)
        Cond_wait(&amp;amp;s-&amp;gt;cond, &amp;amp;s-&amp;gt;lock);
    s-&amp;gt;value--;
    Mutex_unlock(&amp;amp;s-&amp;gt;lock);
}

void Zem_post(Zem_t* s) {
    Mutex_lock(&amp;amp;s-&amp;gt;lock);
    s-&amp;gt;value++;
    Cond_signal(&amp;amp;s-&amp;gt;cond);
    Mutex_unlock(&amp;amp;s-&amp;gt;lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们通过2种简单的语义实现了一个更好用的工具。&lt;/p&gt;
&lt;p&gt;有一个很有意思的点：&lt;strong&gt;使用信号量来实现条件变量&lt;/strong&gt;这件事会很困难。&lt;/p&gt;
&lt;p&gt;​	在windows上很多程序员尝试这么做，但是他们写出了更多的bug。&lt;/p&gt;
&lt;p&gt;​	个人猜测，例如条件变量中的broadcast广播通知，使用信号量就会很难实现。虽然可以通过while+post来实现类似的效果，但是这样就有了次序，不能达成同时唤醒的效果了。同时，如果被唤醒的线程又会对信号量进行处理并且进入wait，那么有些线程就会一直得不到唤醒。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 30 Condition Variables</title><link>https://www.intotw.cn/posts/ostep-30-condition-variables/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-30-condition-variables/</guid><pubDate>Tue, 16 Jul 2024 06:01:43 GMT</pubDate><content:encoded>&lt;p&gt;除了锁之外，并发程序中另一个最重要的模式就是条件变量，与锁不同，条件变量用于解决线程间的同步问题。&lt;/p&gt;
&lt;h1&gt;条件变量的基本api&lt;/h1&gt;
&lt;p&gt;主要由2个方法就可以实现，&lt;/p&gt;
&lt;p&gt;pthread_cond_wait(pthread_cond_t&lt;em&gt;c, pthread_mutex_t&lt;/em&gt;m);&lt;/p&gt;
&lt;p&gt;pthread_cond_signal(pthread_cond_t*c);&lt;/p&gt;
&lt;p&gt;一个wait，用于进入阻塞状态等待，一个signal，用于唤醒等待的线程。&lt;/p&gt;
&lt;p&gt;为什么需要等待和被唤醒？我们当然可以写一个while循环无限检查一个公共变量来达到效果，但是从锁的实现我们就可以看出，&lt;strong&gt;自旋&lt;/strong&gt;是最消耗性能的做法，它只在一开始自旋有限次数时，通过线程切换成本的降低，来具备较好的性能。&lt;/p&gt;
&lt;h1&gt;消费者，生产者队列&lt;/h1&gt;
&lt;p&gt;这里主要使用一个例子，即如何通过wait和signal2个条件变量的原语，来实现一个经典模型：消费者生产者队列。&lt;/p&gt;
&lt;p&gt;这边直接贴最终版的，并且倒着去解释一些关键点了，原文中是从一堆错误慢慢演进的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cond_t empty, fill;
mutex_t mutex;
int count = 0;
int loops = 10;
void* producer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        Pthread_mutex_lock(&amp;amp;mutex);
        while (count == 1)
            Pthread_cond_wait(&amp;amp;empty, &amp;amp;mutex);
        put(i);
        Pthread_cond_signal(&amp;amp;fill);
        Pthread_mutex_unlock(&amp;amp;mutex);
    }
    return NULL;
}

void* consumer(void* arg) {
    int i;
    for (i = 0; i &amp;lt; loops; i++) {
        Pthread_mutex_lock(&amp;amp;mutex);
        while (count == 0)
            Pthread_cond_wait(&amp;amp;fill, &amp;amp;mutex);
        int tmp = get();
        Pthread_cond_signal(&amp;amp;empty);
        Pthread_mutex_unlock(&amp;amp;mutex);
        printf(&quot;%d\n&quot;, tmp);
    }
    return NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有以下几个要点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么wait和signal要在锁里操作？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;​	这是因为判断count==0或者==1，本身也是一段临界代码，判断后再进入wait是存在并发风险的，所以要在锁中操作，否则可能出现在判断了==0后，马上被修改为1了，但是依旧进入了wait并且再也无法被唤醒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么用while循环判断&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在并发场景下，使用while循环判断总是好过if判断。这里涉及到条件变量中的2种语义：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mesa Semantics&lt;/strong&gt;：被唤醒的线程需要重新获取锁，并且重新检查条件。实现简单，效率高，是大多数系统的默认语义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hoare Semantics&lt;/strong&gt;：被唤醒的线程立即获得锁，条件保证满足，实现复杂，效率低。&lt;/p&gt;
&lt;p&gt;在实际编程中，特别是在使用Pthreads时，你通常会遇到Mesa语义，需要在被唤醒后重新检查条件。&lt;/p&gt;
&lt;p&gt;所以，使用while循环其实是一种Mesa语义，因为在被唤醒后继续往下执行时，以消费者为例，线程在count==0时进入wait，在苏醒后，线程无法保证count==1的条件在苏醒后依然成立，即wait被signal后，无法确保count！=0，因为此时也可能存在其他线程修改了count，所以需要再次检查count的值，如果此时依旧不满足，那么继续进入wait。&lt;/p&gt;
&lt;p&gt;实现Hoare语义就可以保证，但是就像上面说的，Hoare语义实现非常复杂，而且效率低下。&lt;/p&gt;
&lt;h1&gt;条件变量的广播&lt;/h1&gt;
&lt;p&gt;有一种广播式的条件变量，叫做covering condition，就是一次性唤醒所有wait这个条件变量的线程，可以降低复杂度，但是性能开销会比较大。&lt;/p&gt;
&lt;h1&gt;条件变量怎么实现？&lt;/h1&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;这章主要就介绍了条件变量的使用，并且举了个例子说明如何使用条件变量实现并发程序。&lt;/p&gt;
</content:encoded></item><item><title>复用：业务系统设计中的陷阱</title><link>https://www.intotw.cn/posts/reuse-the-pitfalls-of-business-design/</link><guid isPermaLink="true">https://www.intotw.cn/posts/reuse-the-pitfalls-of-business-design/</guid><pubDate>Thu, 11 Jul 2024 07:57:23 GMT</pubDate><content:encoded>&lt;h1&gt;复用往往会带来耦合&lt;/h1&gt;
&lt;h1&gt;复用需要单一原则，而业务系统需要的单一与之不同&lt;/h1&gt;
</content:encoded></item><item><title>Ostep 29 Lock Based ConcurConcurrency Data Structure</title><link>https://www.intotw.cn/posts/ostep-29-lock-based-concurconcurrency-data-structure/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-29-lock-based-concurconcurrency-data-structure/</guid><pubDate>Tue, 09 Jul 2024 09:25:07 GMT</pubDate><content:encoded>&lt;p&gt;这章比较简单简短，主要介绍了并发数据结构的实现。&lt;/p&gt;
&lt;p&gt;总结下的话，实现并发的数据结构，我们需要考虑主要2个点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;正确性。这个毋庸置疑，并发情况操作下，要保证数据的正确性。&lt;/li&gt;
&lt;li&gt;性能。并发数据结构的性能其实分两部分
&lt;ol&gt;
&lt;li&gt;在实现正确性的情况下，结构本身的性能&lt;/li&gt;
&lt;li&gt;在线程越来越多（并发越来越高）的情况下，性能下降的程度&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从这些点来考虑的话，我们直接总结下这章说道的并发数据结构的实现方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;直接所有操作加锁，缺点就是性能最差，适用于所有数据结构。&lt;/li&gt;
&lt;li&gt;降低锁的规模，比如队列只对头尾节点加锁，哈希表只对对应的桶加锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这章提到了一个实现或者说概念，接近计数器&lt;strong&gt;Approximate Counters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;接近计数器主要由以下几部分组成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;维护一个跟CPU相关的计数counter，thread counter。比如使用一个数组，然后通过当前线程id%cpu总数来得到对应的&lt;strong&gt;桶&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;全局维护的一个计数counter，global counter。&lt;/li&gt;
&lt;li&gt;一个阈值threshold。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当使用时，每个线程获取自己thread counter的锁进行操作thread counter（避免了全局锁，获取自己的锁的原因是单cpu上也可能线程切换）。&lt;/p&gt;
&lt;p&gt;当thread counter的值达到threshold阈值以后，尝试获取global coutner的锁，将thread counter的值加到global counter上去。&lt;/p&gt;
&lt;p&gt;我们会发现，因为threshold的存在，global counter的值总是一个接近真实值的较小值，它与&lt;strong&gt;准确度&lt;/strong&gt;大小与threshold有关，同时，这个计数器的&lt;strong&gt;性能&lt;/strong&gt;也与threshold有关，这两者成反比，越准确，性能越差。反之亦然。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 28 Locks</title><link>https://www.intotw.cn/posts/ostep-28-locks/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-28-locks/</guid><pubDate>Fri, 05 Jul 2024 08:15:46 GMT</pubDate><content:encoded>&lt;p&gt;27章罗列了线程的api，所以就略过了&lt;/p&gt;
&lt;p&gt;28章主要介绍了&lt;strong&gt;Lock&lt;/strong&gt; 锁，上一章说到了，处理并发下的数据竞争最好的方式，就是使用锁来使得那段临界代码互斥，所以这一章主要介绍了锁。&lt;/p&gt;
&lt;h1&gt;锁的模型&lt;/h1&gt;
&lt;p&gt;锁的话，简单的模型其实也就2个功能：加锁、解锁&lt;/p&gt;
&lt;h1&gt;锁的评估&lt;/h1&gt;
&lt;p&gt;一般我们评估锁，从3个维度：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基本功能，是否提供了互斥&lt;/li&gt;
&lt;li&gt;公平性，是否能提供对于争抢锁之间线程的公平性，也就是防止&lt;strong&gt;饿死&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;性能，锁本身对于多线程环境执行下的性能增加开销&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;锁的实现&lt;/h1&gt;
&lt;h2&gt;最简单的实现，关闭中断&lt;/h2&gt;
&lt;p&gt;​	前文说到了，数据竞争的原因就是多线程之间不可知的调度顺序，所以当一个线程获取锁的时候，关闭掉所有的中断响应，释放锁的时候，恢复中断响应，这样在持有锁时就可以保证一定是自己执行完， 其他线程无法通过操作系统的中断被调度，保证了基本功能。&lt;/p&gt;
&lt;p&gt;​	但是问题也很明显，首先，关闭中断响应需要进入内核态。如果关闭中断响应以后，如果线程不主动释放，那么连操作系统都无法夺回对整个系统的控制权了。&lt;/p&gt;
&lt;p&gt;​	这种实现的唯一优点就是简单，但是风险过高，所以不可能如此实现，只是一种思路。&lt;/p&gt;
&lt;h2&gt;通过共享变量&lt;/h2&gt;
&lt;p&gt;​	我们可以通过一个共享变量来实现，使用一个flag，获取锁时修改为1，其他线程判断为1时则自旋一直判断等待为0，在释放锁时则将flag修改为0标识为已释放。&lt;/p&gt;
&lt;p&gt;​	问题也很明显，修改和判断flag时，依旧可能中断，这个逻辑本身也是一段临界代码。&lt;/p&gt;
&lt;p&gt;​	别急，在这种方式下，我们要解决问题的规模就变小了，只要保证修改flag的原子性就可以。&lt;/p&gt;
&lt;h2&gt;基本原语保证原子性&lt;/h2&gt;
&lt;p&gt;​	基于共享变量的方式，CPU提供了一些硬件支持来支持原子性操作，方便共享变量的方式来实现锁。&lt;/p&gt;
&lt;h3&gt;Test And Set&lt;/h3&gt;
&lt;p&gt;TAS的效果很简单，对一个变量（内存地址）修改值后，返回其原来的值。通过TAS，我们可以实现一个简单的锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct __lock_t｛ 
	int flag；
｝ lock_t

void init（lock_t*lock）｛ 
// 0:lock is available,1:lock is held 
  lock-&amp;gt;flag = 0；
}

void lock（lock_t*1ock）｛ 
while（TestAndSet（&amp;amp;lock-&amp;gt;flag,1） == 1）；
// spin-wait （do nothing）
｝

void unlock （lock_t *lock）｛ 
   lock-&amp;gt;flag = 0；
｝
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理和逻辑也比较简单，在将flag修改为1后，返回原来的值，只有在原来的值为0时，才认为自己获取到了这个锁。&lt;/p&gt;
&lt;h3&gt;Compare-And-Swap&lt;/h3&gt;
&lt;p&gt;CAS是很多人都知道的操作了，给予2个值，只有在地址的原来值与给定的期望值相等时，才将地址的值修改为目标值，并且返回修改后的值。实现锁的思路和TAS基本一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void lock(lock_t*lock) {
  while (CompareAndSwap(&amp;amp;lock-&amp;gt;flag, 0, 1) == 1)
  ; // spin4
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实现锁上，CAS和TAS并没有什么区别，但是CAS在实现无锁数据结构时，会是非常强大的工具。&lt;/p&gt;
&lt;h3&gt;Load-Linked and Store-Conditional&lt;/h3&gt;
&lt;p&gt;​	这个很难翻译，我个人认为翻译成&lt;strong&gt;监听&lt;/strong&gt;以及&lt;strong&gt;条件保存&lt;/strong&gt;适当一些。&lt;/p&gt;
&lt;p&gt;​	这两个原语的作用是，首先通过LL监听一个地址后，再使用SC条件保存去更新这个地址的内容，在SC更新一个LL的地址时，如果LL的地址从监听以后被修改过，那么返回false并且更新失败，如果没有被修改过，那么返回true并且更新成功。锁的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void lock(lock_t*lock) {
  while (1) {
    while (LoadLinked(&amp;amp;lock-&amp;gt;flag) == 1)
      ; // spin until it’s zero
    if (StoreConditional(&amp;amp;lock-&amp;gt;flag, 1) == 1)
      return; // if set-it-to-1 was a success: all done
    // otherwise: try it all over again
  }
}

void unlock(lock_t*lock) {
  lock-&amp;gt;flag = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fetch-And-Add&lt;/h3&gt;
&lt;p&gt;最后一个硬件原语是获取和添加指令，它自动增加一个值，同时返回特定地址的旧值。使用这种原语实现锁的方式截然不同，类似于令牌：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们的锁需要2个变量，turn和ticket。&lt;/li&gt;
&lt;li&gt;在获取锁时，将ticket标记为1获得。此时线程自己的myturn==turn。&lt;/li&gt;
&lt;li&gt;其他线程进入，ticket标记为获得了，进入等待，此时使用这个原语进行turn的原子+1，获得的myturn==turn+1，并且进入自旋判断turn是否等于自己的myturn了。&lt;/li&gt;
&lt;li&gt;释放锁时，原子增加turn，把ticket修改为0，释放。&lt;/li&gt;
&lt;li&gt;此时那些在自旋的某个线程的myturn就等于被释放时+1的turn了，该线程获取锁并进入临界代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;自旋的性能问题&lt;/h2&gt;
&lt;p&gt;前面简单的锁，都存在自旋的性能问题。&lt;/p&gt;
&lt;p&gt;​	通过无脑自旋，会浪费大量的CPU，比如100个线程在等待一个锁，其中99个线程在分配到CPU时间片时只是在做无意义的自旋浪费CPU。并且这种浪费反而会影响实际正在执行的线程对CPU的实际使用，进一步减慢锁的释放。&lt;/p&gt;
&lt;h3&gt;让出线程（yield）&lt;/h3&gt;
&lt;p&gt;简而言之就是一个功能，使得线程可以主动退出调度，由操作系统来决定什么时候恢复调度它，是最简单的方式，比较简陋。&lt;/p&gt;
&lt;h3&gt;使用队列并且sleep&lt;/h3&gt;
&lt;p&gt;sleep的区别是，sleep可以由其他线程主动唤醒（对应park和unpark，java的unsafe也有）。&lt;/p&gt;
&lt;p&gt;使用sleep加队列，我们可以实现一个初具人形的低自旋的锁了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct __lock_t {
    int flag;
    int guard;
    queue_t *q;
} lock_t;

void lock_init(lock_t *m) {
    m-&amp;gt;flag = 0;
    m-&amp;gt;guard = 0;
    queue_init(m-&amp;gt;q);
}

void lock(lock_t *m) {
    while (TestAndSet(&amp;amp;m-&amp;gt;guard, 1) == 1)
        ; // acquire guard lock by spinning
    if (m-&amp;gt;flag == 0) {
        m-&amp;gt;flag = 1; // lock is acquired
        m-&amp;gt;guard = 0;
    } else {
        queue_add(m-&amp;gt;q, gettid());
        m-&amp;gt;guard = 0;
        park();
    }
}

void unlock(lock_t *m) {
    while (TestAndSet(&amp;amp;m-&amp;gt;guard, 1) == 1)
        ; // acquire guard lock by spinning
    if (queue_empty(m-&amp;gt;q))
        m-&amp;gt;flag = 0; // let go of lock; no one wants it
    else
        unpark(queue_remove(m-&amp;gt;q)); // hold lock (for next thread!)
    m-&amp;gt;guard = 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一点需要额外补充下方便理解：这段代码中，guard实际用来操作控制队列的原子性，flag用来标记锁的状态。&lt;/p&gt;
&lt;p&gt;这种实现也会有个问题，当调度发生时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue_add(m-&amp;gt;q, gettid());
m-&amp;gt;guard = 0;
park();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	这里在加入队列后，m-&amp;gt;guard = 0;会释放掉队列的“锁”-&amp;gt;在park执行之前，如果另外的线程释放了锁从队列中取出来这个线程刚加到队列里的自己进行unpark-&amp;gt;此时unpark必然会失败（因为park还没执行）-&amp;gt;此时再park，就永远park下去永远不会被唤醒了（队列里没他，所以不会被取出来unpark，但是自己又park进去了）。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;队列的方式可以保证锁的公平性&lt;/strong&gt;（FIFO）。&lt;/p&gt;
&lt;p&gt;​	其实到这就想到一个点：队列、park、unpark？你要找的是不是&lt;strong&gt;juc.aqs&lt;/strong&gt;？（手动狗头）&lt;/p&gt;
&lt;h2&gt;Linux的锁&lt;/h2&gt;
&lt;p&gt;Linux提供的原语是futex_wait和futex_wake，是根据地址的值变化来阻塞的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void mutex_lock(int *mutex) {
    int v;

    // 尝试快速路径获取锁
    if (atomic_bit_test_set(mutex, 31) == 0) 
        return;

    // 等待值+1
    atomic_increment(mutex);

    while (1) {
        if (atomic_bit_test_set(mutex, 31) == 0) {
            atomic_decrement(mutex);
            return;
        }

        v = *mutex;
        if (v &amp;gt;= 0)
            continue;

        // 使用 futex 等待锁释放
        futex_wait(mutex, v);
    }
}

void mutex_unlock(int *mutex) {
    // 尝试快速路径释放锁
    if (atomic_add_zero(mutex, 0x80000000))
        return;

    // 如果有其他线程在等待锁，唤醒其中一个
    futex_wake(mutex);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个点比较难理解，因为是伪代码，只能通过整体去猜测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (atomic_add_zero(mutex, 0x80000000))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	这个函数是将mutex-0x80000000，剩下的值就是等待中的线程数量（atomic_increment(mutex)），此时如果减掉后为0，说明没有线程在等待，那么直接返回，否则的话，futex_wake(mutex);唤醒等待这个值的所有线程去简单自旋一下继续争夺。详细过程大致如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3&gt;初始化状态&lt;/h3&gt;
&lt;p&gt;初始时，&lt;code&gt;mutex&lt;/code&gt; 的值为 &lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;加锁 (&lt;code&gt;mutex_lock&lt;/code&gt;)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;快速路径获取锁成功&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;atomic_bit_test_set(mutex, 31) == 0&lt;/code&gt; 成功，则 &lt;code&gt;mutex&lt;/code&gt; 的第 31 位被设置为 &lt;code&gt;1&lt;/code&gt;，表示锁已被持有。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 的值从 &lt;code&gt;0&lt;/code&gt; 变为 &lt;code&gt;0x80000000&lt;/code&gt;（即 &lt;code&gt;1000 0000 0000 0000 0000 0000 0000 0000&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;快速路径获取锁失败&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;atomic_bit_test_set(mutex, 31)&lt;/code&gt; 失败，说明锁已经被其他线程持有。&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;atomic_increment(mutex)&lt;/code&gt;，将 &lt;code&gt;mutex&lt;/code&gt; 的值增加 &lt;code&gt;1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 的值从 &lt;code&gt;0x80000000&lt;/code&gt; 变为 &lt;code&gt;0x80000001&lt;/code&gt;（即 &lt;code&gt;1000 0000 0000 0000 0000 0000 0000 0001&lt;/code&gt;），表示有一个线程在等待锁。&lt;/li&gt;
&lt;li&gt;如果其他线程也尝试获取锁，则 &lt;code&gt;mutex&lt;/code&gt; 的值会进一步增加，例如有两个线程在等待时，&lt;code&gt;mutex&lt;/code&gt; 的值为 &lt;code&gt;0x80000002&lt;/code&gt;（即 &lt;code&gt;1000 0000 0000 0000 0000 0000 0000 0010&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;等待锁释放&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;当线程检测到 &lt;code&gt;mutex&lt;/code&gt; 的值为负数时，进入 &lt;code&gt;futex_wait&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;futex_wait&lt;/code&gt; 挂起当前线程，等待锁的释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;解锁 (&lt;code&gt;mutex_unlock&lt;/code&gt;)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;快速路径释放锁&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;atomic_add_zero(mutex, 0x80000000)&lt;/code&gt; 返回 &lt;code&gt;true&lt;/code&gt;，表示没有其他线程在等待锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 的值从 &lt;code&gt;0x80000000&lt;/code&gt; 变为 &lt;code&gt;0&lt;/code&gt;，锁被释放。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;有线程在等待锁&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;如果 &lt;code&gt;atomic_add_zero(mutex, 0x80000000)&lt;/code&gt; 返回 &lt;code&gt;false&lt;/code&gt;，表示有其他线程在等待锁。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 的值从 &lt;code&gt;0x80000001&lt;/code&gt; 变为 &lt;code&gt;0x1&lt;/code&gt;（即 &lt;code&gt;0000 0000 0000 0000 0000 0000 0000 0001&lt;/code&gt;），表示有一个线程在等待。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;futex_wake(mutex)&lt;/code&gt; 唤醒一个等待的线程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程等待时&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;线程等待锁时&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个线程进入等待状态时，&lt;code&gt;mutex&lt;/code&gt; 的值会变为负数（即高位为 &lt;code&gt;1&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt; 的值会增加，例如有两个线程在等待锁时，&lt;code&gt;mutex&lt;/code&gt; 的值为 &lt;code&gt;0x80000002&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;两阶段锁&lt;/h2&gt;
&lt;p&gt;​	这个就是为了锁的性能考虑，和jvm中synchronized关键字分轻量级以及重量级一样，实际现在的锁实现出于性能考虑，都会先进入自旋，自旋一定次数后，再进入阻塞等待，这么设计的原因就是因为大部分锁的释放都很快，自旋可以大大降低线程上下文切换的开销。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;​	其实锁的本质就是CPU的原子操作原语（TAS,CAS）加一定数据结构和逻辑，实现了2个关键功能：加锁、解锁。&lt;/p&gt;
&lt;p&gt;​	锁的主要等待方式包括两种：自旋或阻塞（让出CPU进入休眠）。&lt;/p&gt;
&lt;p&gt;​	锁的关键组成：CPU硬件原语（保障互斥）-&amp;gt;自旋和阻塞（优化性能）-&amp;gt;队列（保障公平性，提供阻塞）-&amp;gt;两阶段（性能）&lt;/p&gt;
&lt;p&gt;​	结合Java的话，你会发现，轻量级重量级锁，就考虑了自旋和阻塞的性能优缺点，并且实现了两阶段锁。&lt;/p&gt;
&lt;p&gt;​	AQS则是实现了JVM用户级别层面的一个锁队列模型，这块为什么不直接用linux提供而是要自己实现呢？我猜测是因为java作为跨平台的语言，不能保证所有操作系统都实现了阻塞队列这种功能的锁实现，所以只利用park和unpark还有cas这种原语来自己实现。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
</content:encoded></item><item><title>Ostep 26 Concurrency Introduction</title><link>https://www.intotw.cn/posts/ostep-26-concurrency-introduction/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-26-concurrency-introduction/</guid><pubDate>Thu, 04 Jul 2024 02:58:41 GMT</pubDate><content:encoded>&lt;p&gt;这章主要介绍了线程和并发的概念&lt;/p&gt;
&lt;h2&gt;并发&lt;/h2&gt;
&lt;p&gt;并发执行下，我们需要考虑2种情况。&lt;/p&gt;
&lt;h3&gt;数据竞争&lt;/h3&gt;
&lt;p&gt;为什么并发下会有数据竞争问题？&lt;/p&gt;
&lt;p&gt;比如最朴素的场景，2个线程同时执行i++，最终的结果总是不如预期，实际上，是因为i++本身在执行时是3条汇编指令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如现在要执行到50+1，此时当线程T1执行该指令到一半，add执行完后，被操作系统调度机制挂起，开始执行T2，T2读到的数据依旧是旧的也就是50，然后T1执行，将内存修改为51，T2再执行，也依然是将内存修改为51，我们就可以看做一次操作“丢失了”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我们可以看到，并发问题总在多线程更新共享变量时产生&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这种场景，书中总结也就是我们之前常说或者听到过的，竞态条件&lt;strong&gt;race condition&lt;/strong&gt;(或者更具体地说，是数据竞态&lt;strong&gt;data race&lt;/strong&gt;）。&lt;/p&gt;
&lt;p&gt;包含静态条件的代码，称为：&lt;strong&gt;critical section&lt;/strong&gt; （临界代码）。&lt;/p&gt;
&lt;p&gt;处理这种问题的方式就是，我们需要使这段代码&lt;strong&gt;mutual exclusion&lt;/strong&gt; （互斥），也就是提供&lt;strong&gt;Atomicity&lt;/strong&gt;（原子性）&lt;/p&gt;
&lt;h3&gt;条件变量&lt;/h3&gt;
&lt;p&gt;并发还有一种场景，即互相等待的场景，T1线程需要等待T2执行完后再执行，为了实现这种效果，我们还需要提供&lt;strong&gt;condition variables&lt;/strong&gt;（条件变量）。&lt;/p&gt;
&lt;h2&gt;线程&lt;/h2&gt;
&lt;p&gt;以前上课学的一句话比较经典：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;线程是操作系统（或者说CPU）调度（执行）的最小单位，进程是操作系统分配资源的最小单位&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;线程和进程的主要区别就是，线程之间一定程度上共享进程的页表（也就是进程数据），是更轻量级的执行单位。&lt;/p&gt;
&lt;p&gt;线程的几个好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;共享进程页表（即共享内存）。&lt;/li&gt;
&lt;li&gt;面对进程中的那些I/O任务，使用线程去进行I/O任务会有更好的表现（线程阻塞等待I/O执行，进程中其他线程依旧可以使用CPU计算资源）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;线程的缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在进程内存中，线程就必须有自己独享的栈以及一些私有数据了&lt;strong&gt;thread-local storage&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;线程切换的上下文依旧很重，进程有PCB，线程也有自己的TCB，并且更多了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;​	综上，从进程实际要执行的事情以及日常的任务（I/O）来看，线程这一轻量级执行单元的诞生和大规模使用基本是必然的。尽管进程也可以达到如此效果，但是又要做到进程之间的&lt;strong&gt;内存共享&lt;/strong&gt;会是一个更麻烦的事，并且也会破坏进程自己的定义和内存完整性。&lt;/p&gt;
&lt;p&gt;​	同时，结合之前来看，cow（写时复制）对于线程的创建起到了多大的便利。&lt;/p&gt;
&lt;p&gt;​	以及，在大量使用线程并行执行的情况下，解决并发问题是多么的重要。&lt;/p&gt;
</content:encoded></item><item><title>Ostep First Part 总结</title><link>https://www.intotw.cn/posts/ostep-first-part-summary/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-first-part-summary/</guid><pubDate>Tue, 25 Jun 2024 09:24:53 GMT</pubDate><content:encoded>&lt;p&gt;这边对ostep整个第一部分做一个总结。&lt;/p&gt;
&lt;p&gt;第一部分主要讲了CPU和内存。总结只记一些核心知识点用于回忆&lt;/p&gt;
&lt;h1&gt;CPU&lt;/h1&gt;
&lt;p&gt;CPU有限制执行模式，部分指令只有在限制执行模式才可以执行，这也是为什么用户进程必须从用户态切换到内核态，才可以执行I/O等操作。操作系统在boot后从指定位置加载自己进内存时，作为第一个执行的程序，会先获得执行权限，然后再执行用户级别代码。&lt;/p&gt;
&lt;p&gt;操作系统执行调度的基本单位是进程，操作系统基于进程进行调度。执行过程按照时间片来分配，可以简单理解为每个进程执行Xms。每个进程挨个执行。&lt;/p&gt;
&lt;p&gt;操作系统进行线程调度时，需要进行线程的切换，主要的开销就在于需要&lt;strong&gt;保留现场&lt;/strong&gt;，即需要保存当前正在执行的进程的寄存器，程序计数器等信息，将这些信息保存到内存中去，再切换回该线程时，需要&lt;strong&gt;还原&lt;/strong&gt;现场。&lt;/p&gt;
&lt;p&gt;CPU调度算法主要有2类，多级反馈队列MLFQ和CFS。&lt;/p&gt;
&lt;p&gt;MLFQ多级反馈队列一般多应用在带UI的交互界面中，保证用户的使用体验。见ostep-8。&lt;/p&gt;
&lt;p&gt;CFS公平调度，一般在计算型服务器使用，主要保障CPU进程调度的公平性，保证每个进程得到合理的调度。见ostep-9。&lt;/p&gt;
&lt;p&gt;多进程多CPU调度，见ostep-10，主要讨论亲和度和一致性问题。&lt;/p&gt;
&lt;h1&gt;内存&lt;/h1&gt;
&lt;h2&gt;虚拟化&lt;/h2&gt;
&lt;p&gt;Ostep11-13&lt;/p&gt;
&lt;p&gt;内存对用户进程时虚拟化的，本质上，用户进程看到的地址空间，如0x000001地址，是通过某种方式映射到了物理内存的某个位置。&lt;/p&gt;
&lt;p&gt;用户进程只能看到自己虚拟内存的地址，实际转换由os来做，并且每个用户进程在虚拟内存地址上都可以认为自己拥有整个内存空间。&lt;/p&gt;
&lt;p&gt;用户的内存空间，主要分为3部分，code，heap，stack，其中heap向下增长，堆向上增长&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2024/03/ce4aff19c5178ae920488e3d77230f8c.png&quot; style=&quot;zoom:33%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h1&gt;地址转换&lt;/h1&gt;
&lt;p&gt;Ostep14-20&lt;/p&gt;
&lt;p&gt;虚拟地址到物理地址的转换，主要有2种虚拟地址的表达方式和一个转换工具TLB。&lt;/p&gt;
&lt;h2&gt;表达方式&lt;/h2&gt;
&lt;p&gt;虚拟地址主要的表达方式有2种&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;段&lt;/strong&gt;：段通过一个物理地址的开头+偏移来表示物理地址，对于虚拟地址0xffff001，我们把他分为2部分，其中ffff表示了物理地址的位置，001表示从这个物理地址开始的偏移，经过计算后对应的物理地址所保存的那个字节，就是虚拟地址想要的字节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表&lt;/strong&gt;：页表通过一种数据结构上的页表来表示
&lt;ul&gt;
&lt;li&gt;首先它将整个物理内存与虚拟内存划分为页，然后就只需要处理虚拟页与物理页之间的映射了&lt;/li&gt;
&lt;li&gt;虚拟页与物理页的映射通过页表来实现，页表（多级）可以认为是一个树，树上保存的节点可以认为是索引&lt;/li&gt;
&lt;li&gt;通过在页表上找到最终的叶子节点以后，叶子结点保存的就是虚拟页所对应的物理页的信息&lt;/li&gt;
&lt;li&gt;得到物理页地址以后，通过在物理页上再计算偏移量，&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TLB（地址翻译缓存）&lt;/h2&gt;
&lt;p&gt;可以认为是地址转换的缓存，是一种硬件机制，在CPU上。&lt;/p&gt;
&lt;p&gt;处理的是最直接的转换，比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给定一个虚拟地址，直接对应一个物理地址&lt;/li&gt;
&lt;li&gt;给定一个虚拟页码，直接对应一个物理页码&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;虚拟内存&lt;/h1&gt;
&lt;p&gt;虚拟内存是把内存页交换到硬盘的技术。&lt;/p&gt;
&lt;p&gt;所以本质上来说，用户进程获取一个内存数据，需要经过一下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要访问的虚拟地址&lt;/li&gt;
&lt;li&gt;虚拟地址通过TLB，或者页表查找，找到对应的物理页&lt;/li&gt;
&lt;li&gt;物理页是否在内存中？如果不在，需要交换回内存（可能已经被交换到了硬盘上）&lt;/li&gt;
&lt;li&gt;在物理页上根据偏移量找到对应数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;页的汰换&lt;/h1&gt;
&lt;p&gt;ostep23有提到&lt;/p&gt;
&lt;p&gt;主要是物理页如何汰换到硬盘上，一般我们知道的朴素算法就是LRU最近最少使用，维护一个列表，每次有被使用了就移动到列表头部，淘汰时从列表尾部去淘汰就可以了。&lt;/p&gt;
&lt;p&gt;实际操作系统为了性能，会考虑使用全局的脏页列表或者空闲列表做辅助，最终性能会接近LRU。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 23 Complete Virtual Memory Systems</title><link>https://www.intotw.cn/posts/ostep-23-complete-virtual-memory-systems/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-23-complete-virtual-memory-systems/</guid><pubDate>Tue, 25 Jun 2024 05:56:24 GMT</pubDate><content:encoded>&lt;p&gt;这边跳过了21章和22章，其中21章主要是粗略的讲了下内存交换到硬盘。22章主要讲的是内存中的页本身的汰换算法。&lt;/p&gt;
&lt;p&gt;23章通过分析探讨2套操作系统的内存管理实现，将之前的内容串联起来了&lt;/p&gt;
&lt;h1&gt;VAX/VMS&lt;/h1&gt;
&lt;p&gt;VAX-11微型计算机架构是在1970年代后期由数字设备公司(DEC)推出的。在微型计算机时代，DEC是计算机行业的重要参与者;不幸的是，一系列糟糕的决定和PC的出现缓慢地(但必然地)导致了它们的消亡[C03]。&lt;/p&gt;
&lt;h2&gt;内存管理&lt;/h2&gt;
&lt;p&gt;VAX-11的内存管理通过页来进行管理，使用的方式是之前提到过得段+页的方式，即段用来表示页表存在的位置。以此来节省空间。&lt;/p&gt;
&lt;p&gt;VAX-11还做了2个点来优化内存的使用：&lt;/p&gt;
&lt;h3&gt;页表空间优化&lt;/h3&gt;
&lt;p&gt;首先，VAX-11系统通过将用户地址空间分成两个部分（P0和P1），只为实际使用的部分分配页表空间。堆和堆栈之间未使用的地址空间不需要页表。这通过以下机制实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;基址和界限寄存器&lt;/strong&gt;：基址寄存器（Base Register）存储该段的页表的起始地址，界限寄存器（Bounds Register）存储页表的大小（即页表条目的数量）。通过这种方式，只需为实际使用的地址空间分配页表条目。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;页表存放在内核虚拟内存中&lt;/h3&gt;
&lt;p&gt;进一步，操作系统将用户页表（P0和P1的页表，因而每个进程有两个页表）放在内核虚拟内存中。具体来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核分配页表空间&lt;/strong&gt;：当需要分配或扩展页表时，内核从其虚拟内存空间（S段）中分配空间。如果内存压力过大，内核可以将这些页表的页面交换到磁盘，从而腾出物理内存供其他用途&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;物理地址空间&lt;/h2&gt;
&lt;p&gt;VAX-11的另一个内存管理在于它对物理内存的分配。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/4f2cb63fe280e9888b00065346f61882.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主要也是有2个点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开头的page0 invalid标记&lt;/strong&gt;：使用这种方式，可以让操作系统方便的处理空指针——只需要把空对象指到这页，就可以很方便的做检查了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底部的system区域&lt;/strong&gt;：这部分用于在物理内存中保存内核的代码、数据等。使用这种方式保存内核的好处在于，在对用户进程进行处理时，只需要在用户进程的页表中增加记录映射到内核的物理页，就可以让内核以library库的形式被用户进程来调用使用。当然，这也会带来了一个缺点：内核的页将很难被交换到虚拟内存（硬盘）中去，因为他们会一直被使用，如果想要让它们可以被交换的话，那么又需要更加复杂的设计和性能开销来支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里也带到了一个概念，就我们常说的用户态和内核态，虽然用户进程的页表里保存了内核的一些物理页的映射，但是内核的页会通过页表的标记来让CPU知道这一页必须得具备什么特权等级才可以访问。所以也就是为什么产生系统中断或者使用一些内核函数的时候，必须要切换到内核态才可以的原因。&lt;/p&gt;
&lt;h2&gt;页面汰换算法&lt;/h2&gt;
&lt;p&gt;这章有个很有意思的词：memory hogs。hogs本义是猪、贪婪的人。XX hogs就表示占用内存大的程序。&lt;/p&gt;
&lt;p&gt;这章主要讨论VAX-11的页面汰换算法选择，LRU存在一个比较致命的问题：它无法保证进程间的内存使用公平。&lt;/p&gt;
&lt;p&gt;想想也知道，某个进程如果使用的少，或者说某段时间内某个进程大量使用内存，那么LRU会导致除了这个进程外其他所有进程的所有页全部被汰换到虚拟内存中去。想想这样的结果：在你使用另一个进程时，等于直接从硬盘中重新读取这个进程的整个内存，这会有多慢？&lt;/p&gt;
&lt;p&gt;VAX-11的优化方法是，使用分段FIFO替换策略。这个想法很简单:每个进程都有它可以在内存中保留的最大页数，称为其常驻集大小(RSS)。每一页都保存在FIFO列表中;当一个进程超过它的RSS时，“先入”页面将被驱逐。FIFO显然不需要硬件的任何支持，因此很容易实现。&lt;/p&gt;
&lt;p&gt;当然，正如我们前面看到的那样，纯FIFO的表现并不特别好。为了提高FIFO的性能，VMS引入了两个第二次机会列表，其中页面在从内存中被驱逐之前被放置，具体来说是一个全局clean页列表和一个dirty页面列表（这里其实主要表示的还是物理页）。当一个进程P的页数超过它的RSS时，一个页面将从它的每个进程FIFO中移除;如果是clean的(未修改)，则放在clean页列表的末尾;如果是dirty的(修改过的)，它被放在脏页列表的末尾。&lt;/p&gt;
&lt;p&gt;如果另一个进程Q需要一个空闲页，它会从全局clean列表中取出第一个空闲页。但是，如果原始进程P在回收之前在该页上出现错误（P又访问到了这一页，产生了缺页中断），则P会从空闲(或脏)列表中回收该页，从而避免代价高昂的磁盘访问。这些全局第二次机会列表越大，分段FIFO算法执行得越接近LRU [RL81]。&lt;/p&gt;
&lt;p&gt;VMS中使用的另一个优化也有助于克服VMS中的小页面大小。具体来说，对于如此小的页面，交换期间的磁盘I/O效率可能非常低，因为磁盘在处理大的传输时会做得更好。为了提高I/O交换的效率，VMS增加了许多优化，但最重要的是cluster（这里不知道该怎么翻译，总而言之就是批量写）。通过cluster，VMS将全局脏列表中的大量页面分组在一起，并写入它们（其实就是脏页批量更新）。&lt;/p&gt;
&lt;h2&gt;页面初始化0&lt;/h2&gt;
&lt;p&gt;在用户进程申请内存的时候，操作系统给用户进程分配一个物理页并映射到这个进程的虚拟页表中，这个时候无论这个物理页之前是什么状态，对这个用户进程来说，这个页也应该是初始化之后的，每个地址的值应该都是0。&lt;/p&gt;
&lt;p&gt;操作系统使用&lt;strong&gt;懒加载&lt;/strong&gt;来处理这个问题，来降低实际的性能开销，在分配一个页时，操作系统并不会立马把内存都初始化为0，而是给页增加一个标记，标记这个页为一个特殊状态。当&lt;strong&gt;用户进程实际发生对这个页的访问或者操作时&lt;/strong&gt;，操作系统根据这个标记发生中断，此时再进行页的初始化。&lt;/p&gt;
&lt;p&gt;这种懒加载唯一的好处就是，大部分时候进程申请后，并不会马上访问内存页，这样就将内存页初始化的开销分散到了使用时。比如我们直接申请一个int[100000]的内存，此时使用到很多页，但是我们遍历和使用这个数组，大概率在时间上是稀疏的。这很好的分散了内存页初始化的开销。&lt;/p&gt;
&lt;h2&gt;页面的copy on write&lt;/h2&gt;
&lt;p&gt;这也是一种懒加载，而且是很有名的一种处理方式。当一个进程需要使用一个已有的页时，操作系统总是先将该页添加到这个进程的页表中，此时可以进行正常的读等操作（因为这对页没有影响）。只有当进程需要对这个页进行修改的时候，才产生一个中断并从新复制一个物理页，并将页表指向复制出的物理页，再做修改。&lt;/p&gt;
&lt;p&gt;这大大提升了内存的性能，任何类型的共享库都可以通过COW先映射到许多进程的地址空间中，在写时才复制，从而节省宝贵的内存空间。在UNIX系统中，由于fork()和exec()的语义，COW更为重要。我们产生子进程或者线程时，不需要马上复制所有进程空间，而是只需要增加页表的映射，当实际进行修改时，再进行这个操作。&lt;/p&gt;
&lt;p&gt;本质上，这实现了对通过页表来对物理页一定程度上的“复用”，只有在真的产生修改时，才去复制一个物理页来支持这种操作。&lt;/p&gt;
&lt;h1&gt;Linux&lt;/h1&gt;
&lt;h2&gt;Linux虚拟地址空间&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/e37b99534f3e6304c0eca91d5a19cf6d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;用户空间和内核空间&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户空间&lt;/strong&gt;：包括用户程序代码、堆栈、堆和其他部分。在上下文切换时，当前运行进程的用户空间会改变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核空间&lt;/strong&gt;：包括内核代码、堆栈、堆和其他部分。在所有进程中保持不变。运行在用户模式下的程序无法访问内核的虚拟页面，只有通过陷入内核并切换到特权模式才能访问这些内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;32位Linux中的地址空间划分&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;地址空间分为两部分：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户空间&lt;/strong&gt;：从0x00000000到0xBFFFFFFF（即地址空间的前3/4）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核空间&lt;/strong&gt;：从0xC0000000到0xFFFFFFFF（即地址空间的最后1/4）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;内核虚拟地址的两种类型&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内核逻辑地址&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;这是内核的标准虚拟地址空间。通过&lt;code&gt;kmalloc&lt;/code&gt;分配内存。&lt;/li&gt;
&lt;li&gt;内核数据结构，如页表和每个进程的内核堆栈，通常驻留在这里。&lt;/li&gt;
&lt;li&gt;内核逻辑内存无法被交换到磁盘。&lt;/li&gt;
&lt;li&gt;内核逻辑地址和物理内存的前部分有直接映射关系。例如：
&lt;ul&gt;
&lt;li&gt;0xC0000000的内核逻辑地址映射到物理地址0x00000000。&lt;/li&gt;
&lt;li&gt;0xC0000FFF的内核逻辑地址映射到物理地址0x00000FFF。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这种直接映射有两个重要影响：
&lt;ul&gt;
&lt;li&gt;可以简单地在内核逻辑地址和物理地址之间转换。&lt;/li&gt;
&lt;li&gt;在内核逻辑地址空间中连续的内存块在物理内存中也是连续的，这对需要连续物理内存的操作（如DMA）很重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内核虚拟地址&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;vmalloc&lt;/code&gt;分配内存。&lt;/li&gt;
&lt;li&gt;返回的指针指向虚拟上连续的内存区域，但物理上不一定连续，因此不适用于DMA。&lt;/li&gt;
&lt;li&gt;内核虚拟地址空间更易于分配，适用于需要大缓冲区的场景，因为找到一大块连续的物理内存可能很困难。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;页表结构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/8cfc65a558185e16586f097ca570f979.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Linux页表现在在64位的操作系统下，页表分为四级页表了，unused留着以后需要五六级时使用&lt;/p&gt;
&lt;h2&gt;大页支持&lt;/h2&gt;
&lt;p&gt;机器内存越来越大的情况下，从CPU开始，慢慢的对更大的页提供了支持，更大的页的好处是更低的多级页表层级，以及更快的存取性能，最重要的是，页数量的减少，可以大大提升TLB的速度，这对页表寻址的提升是质变。&lt;/p&gt;
&lt;p&gt;当然，大页的开销依然存在，也就是我们老生常谈的大页内部碎片的问题。&lt;/p&gt;
&lt;h2&gt;页的cache&lt;/h2&gt;
&lt;p&gt;为了减少访问持久存储的成本（这是本书第三部分的重点），大多数系统使用积极的缓存子系统，将常用数据保存在内存中。Linux在这方面与传统操作系统没有不同。&lt;/p&gt;
&lt;p&gt;Linux的页缓存是统一的，维护来自三种主要来源的内存页：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内存映射文件&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备的文件数据和元数据&lt;/strong&gt;（通常通过文件系统的读写调用访问）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个进程的堆和堆栈页&lt;/strong&gt;（有时称为匿名内存，因为没有关联的命名文件，而是使用交换空间）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些数据保存在页缓存哈希表中，以便在需要时快速查找。&lt;/p&gt;
&lt;h4&gt;页缓存的状态&lt;/h4&gt;
&lt;p&gt;页缓存跟踪条目是否为干净的（读取但未更新）或脏的（即，已修改）。后台线程（称为&lt;code&gt;pdflush&lt;/code&gt;）定期将脏数据写入后备存储（对于文件数据写入特定文件，对于匿名区域写入交换空间），确保修改的数据最终写回持久存储。这个后台活动要么在一定时间间隔后进行，要么在太多页被标记为脏页时进行（这两个参数都是可配置的）。&lt;/p&gt;
&lt;h4&gt;内存不足时的页面替换&lt;/h4&gt;
&lt;p&gt;当系统内存不足时，Linux需要决定哪些页需要从内存中移出以释放空间。为此，Linux使用了一种修改过的2Q替换算法。&lt;/p&gt;
&lt;h4&gt;2Q替换算法&lt;/h4&gt;
&lt;p&gt;标准的LRU（最近最少使用）替换算法虽然有效，但可能会被某些常见的访问模式破坏。例如，如果一个进程反复访问一个大文件（尤其是几乎与内存大小相同或更大的文件），LRU会将内存中的其他文件全部踢出。而且，保留该文件的一部分在内存中并没有用，因为它们在被踢出内存之前从未被再次引用。&lt;/p&gt;
&lt;p&gt;Linux的2Q替换算法通过维护两个列表并在它们之间划分内存来解决这个问题。当页面第一次被访问时，它被放在一个队列中（在原始论文中称为A1，在Linux中称为不活跃列表）；当页面被重新引用时，它被提升到另一个队列（在原始论文中称为Aq，在Linux中称为活跃列表）。需要替换时，替换候选项从不活跃列表中取出。Linux还定期将页面从活跃列表的底部移到不活跃列表，保持活跃列表大约占整个页缓存大小的三分之二。&lt;/p&gt;
&lt;h4&gt;近似LRU&lt;/h4&gt;
&lt;p&gt;虽然理想情况下Linux会按完美的LRU顺序管理这些列表，但如前几章所述，这样做成本很高。因此，与许多操作系统一样，Linux使用一种近似LRU的方法（类似于时钟替换）。&lt;/p&gt;
&lt;p&gt;这种2Q方法总体上表现得非常像LRU，但在处理循环大文件访问的情况下表现尤为出色。通过将循环访问的页限制在不活跃列表中，因为这些页在被踢出内存前不会被再次引用，它们不会将活跃列表中的其他有用页清除出去。&lt;/p&gt;
&lt;h2&gt;内存攻击&lt;/h2&gt;
&lt;p&gt;内存攻击主要有以下几种，这里就不展开了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;缓冲区溢出，通过拷贝内存溢出，将构造的代码或者数据溢出到其他内存页，以修改内存数据或者构造指定的代码来执行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回重定向，攻击者覆盖堆栈，使函数执行的返回跳转到自己指定的位置，以此来执行自己的代码。典型的解决方式就是随机返回地址，这样就可以让这种攻击难以猜测到需要跳转到的地址，错误的跳转会导致进程崩溃&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Meltdown和Spectre攻击，这两个难度太高，贴一下gpt的解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3&gt;Meltdown&lt;/h3&gt;
&lt;h4&gt;基本原理&lt;/h4&gt;
&lt;p&gt;Meltdown攻击利用了现代处理器在处理用户和内核之间的权限隔离上的漏洞。它通过推测执行和时序分析，允许攻击者读取本应无法访问的内核内存。&lt;/p&gt;
&lt;h4&gt;工作机制&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;推测执行&lt;/strong&gt;：现代处理器为了提高性能，会在分支预测后执行某些可能不需要的指令。这些指令在正式验证前就被执行，如果预测错误，结果会被丢弃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限检查延迟&lt;/strong&gt;：处理器在执行某些指令时，会延迟权限检查。Meltdown利用这个延迟窗口，通过推测执行访问内核空间的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时序分析&lt;/strong&gt;：攻击者通过观察缓存的状态变化来推测内核内存的内容。具体来说，Meltdown会通过引入缓存命中和未命中的时序差异来读取内核数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;影响&lt;/h4&gt;
&lt;p&gt;Meltdown影响了许多使用推测执行的现代处理器，包括Intel处理器。通过Meltdown，攻击者可以绕过系统的内存保护机制，读取任意物理内存中的数据。&lt;/p&gt;
&lt;h3&gt;Spectre&lt;/h3&gt;
&lt;h4&gt;基本原理&lt;/h4&gt;
&lt;p&gt;Spectre攻击利用了推测执行中的分支预测漏洞，使得攻击者可以诱导处理器执行错误的推测指令，从而间接读取其他进程的内存数据。&lt;/p&gt;
&lt;h4&gt;工作机制&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分支预测注入&lt;/strong&gt;：攻击者通过精心构造的输入数据来训练处理器的分支预测器，使其在实际分支中进行错误预测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推测执行&lt;/strong&gt;：处理器在错误预测的情况下，执行了某些不应被执行的指令。这些指令会访问某些敏感数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;侧信道攻击&lt;/strong&gt;：通过时序分析或缓存状态变化，攻击者可以推测出被错误执行的推测指令访问到的敏感数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;影响&lt;/h4&gt;
&lt;p&gt;Spectre影响了几乎所有现代处理器，包括Intel、AMD和ARM处理器。不同于Meltdown，Spectre不仅影响用户态和内核态之间的隔离，还影响不同进程之间的隔离。&lt;/p&gt;
&lt;h3&gt;防御措施&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;软件补丁&lt;/strong&gt;：操作系统和应用程序开发者可以通过补丁来缓解这些攻击。例如，内核页面表隔离（KPTI）可以有效防御Meltdown。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件修复&lt;/strong&gt;：处理器制造商（如Intel和AMD）正在设计和发布新硬件，来从底层解决这些漏洞。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编译器改进&lt;/strong&gt;：编译器可以插入一些防御机制，来阻止某些推测执行路径被利用。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Laravel to Java-应用灰度迁移策略</title><link>https://www.intotw.cn/posts/lyb-gray/</link><guid isPermaLink="true">https://www.intotw.cn/posts/lyb-gray/</guid><pubDate>Thu, 20 Jun 2024 09:31:16 GMT</pubDate><content:encoded>&lt;h1&gt;生产预发灰度流量方案&lt;/h1&gt;
&lt;h1&gt;&lt;strong&gt;Prerequisite&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;首先我们需要把我们的流量网关从nginx换成apisix&lt;/p&gt;
&lt;p&gt;然后我们需要从以前的单套生产环境，增加到两套环境，生产+预发&lt;/p&gt;
&lt;h1&gt;&lt;strong&gt;Then&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;然后，我们就可以有了这么一个结构图&lt;/p&gt;
&lt;p&gt;我们此时有了两套环境，一套预发，一套生产，其中预发的流量通过具体的路由规则配置，目前暂且支持手机号区分。&lt;/p&gt;
&lt;h1&gt;Advantage&lt;/h1&gt;
&lt;p&gt;权衡过多种方案，这种方案做的话，优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;隔离性强，从网关层直接一分为二。后续环境2套&lt;/li&gt;
&lt;li&gt;便于实施，配置文件迁移即可，无需修改&lt;/li&gt;
&lt;li&gt;一鱼二吃，既可以生产+预发的灰度，又可以做php+java的灰度&lt;/li&gt;
&lt;li&gt;降级优雅，降级时只需要将灰度控制的filter下掉即可&lt;/li&gt;
&lt;li&gt;语言友好，java开发的组件总比在nginx上用lua脚本或者c++写脚本要好&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;直接切网关，第一次上线风险较大&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;apisix作为新引入的中间件，虽然已经经过大规模实践并且有良好的社区，但是我们对这个中间件还不够熟悉。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Apisix&lt;/h1&gt;
&lt;p&gt;在apissix上，我们要提供什么功能呢？&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;默认的灰度&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;首先是默认的灰度，幸运的是apisix的底层是nginx，所以他天然就支持nginx配置，也就是说，我们可以通过配置达到这样一个效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/05/49e05d2e03c5bca6844d62b7a52de61e.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看起来比较怪。实际上，apisix支持nginx的原生配置，也就是说，我们可以通过在其中配置2个nginx配置文件来监听2个端口，来达到对原来整体nginx集群修改尽可能少的效果。&lt;/p&gt;
&lt;p&gt;然后我们通过apisix的插件，来进行对2个nginx入口的灰度，所以就会看起来比较奇怪：一个apisix暴露了3个端口。&lt;/p&gt;
&lt;p&gt;实际上，下面的2个ng可以单独部署，不集成在apisix中。这样有好处也有坏处，好处就是不集成的话可以减少性能开销，并且apisix层可以比较简单的拿掉。坏处就是路由多了一层，要管理的中间件变多了，要管理的配置也变多了。&lt;/p&gt;
&lt;p&gt;最终实际的执行顺序如下：&lt;/p&gt;
&lt;p&gt;apisix-&amp;gt;ext-plugin（我们的自定义插件）-&amp;gt;traffic-split（根据规则做2个upstreams的转发）-&amp;gt;具体的nginx-&amp;gt;nginx上的upstream&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Nginx的配置&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;apisix-&amp;gt;config.yaml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx_config:
    user: root
    http_configuration_snippet: |
        server
        {
                listen       9999;
                server_name  localhost;
                location /test5 {
                        alias   /tmp/folder1/test1;
                        index  index.html;
                }
        }
        server
        {
                listen       8888;
                server_name  localhost;
                location /test5 {
                        alias  /tmp/folder2/test1;
                        index  index.html;
                }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;apisix创建路由&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:9180/apisix/admin/routes/5 \
-H &apos;X-API-KEY: Wailiyebang&apos; -X PUT -d &apos;
{
    &quot;uri&quot;: &quot;/test5/&quot;,
    &quot;plugins&quot;: {
        &quot;ext-plugin-pre-req&quot;:{
            &quot;_meta&quot;: {
                &quot;priority&quot;: 10000
            },            
            &quot;conf&quot;:[
                {
                    &quot;name&quot;:&quot;GrayFilter&quot;,
                    &quot;value&quot;:&quot;{\&quot;validate_header\&quot;:\&quot;token\&quot;,\&quot;validate_url\&quot;:\&quot;https://www.sso.foo.com/token/validate\&quot;,\&quot;rejected_code\&quot;:\&quot;403\&quot;}&quot;
                }
            ]
        },
        &quot;file-logger&quot;: {
            &quot;_meta&quot;: {
                &quot;priority&quot;: 1000
            },     
            &quot;path&quot;: &quot;logs/file.log&quot;
        },
        &quot;traffic-split&quot;: {
            &quot;_meta&quot;: {
                &quot;priority&quot;: -2000
            },
            &quot;rules&quot;: [
                {
                    &quot;match&quot;: [
                        {
                            &quot;vars&quot;: [
                                [&quot;http_x-api-id&quot;,&quot;==&quot;,&quot;1&quot;]
                            ]
                        }
                    ],
                    &quot;weighted_upstreams&quot;: [
                        {
                            &quot;upstream&quot;: {
                                &quot;name&quot;: &quot;upstream-A&quot;,
                                &quot;type&quot;: &quot;roundrobin&quot;,
                                &quot;nodes&quot;: {
                                    &quot;127.0.0.1:9999&quot;:1
                                }
                            },
                            &quot;weight&quot;: 3
                        }
                    ]
                },
                {
                    &quot;match&quot;: [
                        {
                            &quot;vars&quot;: [
                                [&quot;http_x-api-id&quot;,&quot;==&quot;,&quot;2&quot;]
                            ]
                        }
                    ],
                    &quot;weighted_upstreams&quot;: [
                        {
                            &quot;upstream&quot;: {
                                &quot;name&quot;: &quot;upstream-B&quot;,
                                &quot;type&quot;: &quot;roundrobin&quot;,
                                &quot;nodes&quot;: {
                                    &quot;127.0.0.1:8888&quot;:1
                                }
                            },
                            &quot;weight&quot;: 3
                        }
                    ]
                }
            ]
        }
    },
    &quot;upstream&quot;: {
            &quot;type&quot;: &quot;roundrobin&quot;,
            &quot;nodes&quot;: {
                &quot;127.0.0.1:9999&quot;:1
            }
    }
}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;插件demo逻辑&lt;/strong&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Slf4j
public class GrayFilter implements PluginFilter {
    public static int INDEX=1;

    @Override
    public String name() {
        return &quot;GrayFilter&quot;;
    }

    @Override
    public void filter(HttpRequest request, HttpResponse response, PluginFilterChain chain) {
      	//灰度逻辑在此可以全部自定义逻辑，只需要打上这个x-api-id的header标即可，这里只是简单模拟一次生产一次灰度，以查看是否达到效果
        request.setHeader(&quot;x-api-id&quot;, String.valueOf(INDEX % 2+1));
        log.info(&quot;GrayFilter filter, index: {}&quot;, INDEX);
        INDEX++;
        chain.filter(request, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实际效果&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/05/e00cc3fe626580898005887feaaccfdf.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;demo已部署在测试环境lyb-sass-test&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;插件最终逻辑（插件todo）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/05/cd4f5e028bd7a715b831e469eedac0c6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;tars&lt;/h1&gt;
&lt;p&gt;tars之间微服务之间调用的问题，我们区分环境通过tars的set功能做区分：&lt;/p&gt;
&lt;p&gt;https://doc.tarsyun.com/#/dev/tars-idc-set.md&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/05/26c324b752a59f6c266aa9cf0d8e3a30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样可以保证生产以及预发之间微服务的隔离&lt;/p&gt;
&lt;h1&gt;演进过程&lt;/h1&gt;
&lt;h2&gt;迭代方式&lt;/h2&gt;
&lt;p&gt;java应用承接迭代，系统与php1:1，初次承接需要发布新java应用，后续同步更新java应用或许在java应用中修改新增接口即可&lt;/p&gt;
&lt;h2&gt;测试环境&lt;/h2&gt;
&lt;p&gt;测试环境按照预发环境配置，作为预发的前置验证，测试环境中在不验证灰度插件功能的情况下，可以不配置灰度插件，默认流量全走php+java混合的环境&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;发布流程&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;发布流程需要做一些调整。&lt;/p&gt;
&lt;p&gt;原先我们直接发布到生产环境，现在我们迭代需要先发布到预发环境，在预发环境验证后，再发布到生产环境。&lt;/p&gt;
&lt;p&gt;后续我们需要对于发布步骤进行记录（开发需要提供发布操作步骤，其中需要动到线上库的需要评估生产不发的情况下对生产的影响），预发由产品+客成+指定的灰度流量验证过后，通过发布操作步骤再次发布到生产。&lt;/p&gt;
&lt;p&gt;预发到生产的时间可以视迭代或者具体情况具体再定，比如纯php修改可以只在预发验证下就发布到生产，包含java应用新上或者java修改的，最好在预发进行充分验证&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;回滚策略&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;生产发现因为发布导致的问题，将应用以及灰度配置回滚到上个版本即可&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>读书笔记-中岛敦《山月记》</title><link>https://www.intotw.cn/posts/book-zdd/</link><guid isPermaLink="true">https://www.intotw.cn/posts/book-zdd/</guid><pubDate>Thu, 20 Jun 2024 03:37:27 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/df3b18b566f7ff23eb34cb85dec1d71c.png&quot; alt=&quot;10a74437c32ac09c7e1f30094f76614e&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	4月去西藏玩的时候，在去程的火车上把山月记看完了。中岛敦的笔力在叙事方面非常强大，书中的篇章都是通过叙事来展现一个历史故事，或者各地风俗。最能看出笔力的当属2个长篇：《李陵》，《光·风·梦》。对我来说最深刻的我认为是《山月记》。&lt;/p&gt;
&lt;p&gt;​	看这本书的初衷是多年前在网上看到的《山月记》中最有名，也是最吸引人来看这本书的一段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;**因为害怕自己并非明珠而不敢刻苦琢磨，又因为有几分相信自己是明珠，而不能与瓦砾碌碌为伍，遂逐渐远离世间，疏避人群，结果在内心不断地用愤懑和羞怒饲育着自己懦弱的自尊心。**世上每个人都是驯兽师，而那匹猛兽，就是每人各自的性情。对我而言，猛兽就是这自大的羞耻心了。老虎正是它。我折损自己，施苦妻儿，伤害朋友。末了，我就变成了这副与内心一致的模样。&lt;/p&gt;
&lt;p&gt;​	如今想起来，我真是空费了自己那一点仅有的才能，徒然在口头上卖弄着什么“人生一事不为则太长，欲为一事则太短”的警句，**可事实是，唯恐暴露才华不足的卑怯的畏惧，和厌恶钻研刻苦的惰怠，就是我的全部了。但远比我缺乏才华，可由于专念磨砺而成就堂堂诗家的，也颇不乏其人。**成为老虎后的今天，我才总算看到了这一点。&lt;/p&gt;
&lt;p&gt;————中岛敦《山月记》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	这段话出现在山月记故事的末尾，由人变虎后，李征反而放弃了追逐内心虚无的清高，认识到了“人生一事不为则太长，欲为一事则太短”的问题——人不该只追求终极或者”搞个大的“。像育良书记对侯亮平说的那句话：”我时长就在想，这官儿，当得多大才叫大啊“。许多事情即使不到终点也有其意义，人的终极。&lt;/p&gt;
&lt;h3&gt;李陵&lt;/h3&gt;
&lt;p&gt;​	书中还有一段《李陵》中的片段让我印象深刻。《李陵》篇讲的是苏武牧羊的故事，不过是以李陵和司马迁视角展开。李陵投降在匈奴多年，已成为匈奴右校王，渐渐都快”忘记“自己的汉人身份后，遇到了二十年的旧友苏武时，心态不自觉发生改变。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;​	苏武是李陵二十年的老友了，一度两人同为侍中，同食同宿。苏武为人虽然有点固执，却也算是个罕有的气节刚直之士。天汉元年，苏武北行启程后不久，老母就病逝了，李陵为其母送葬，一直送到阳陵坟场。李陵北征出发之前，听闻苏武的妻子知道丈夫返乡无日，已改嫁他人了。那时，李陵还很为朋友感到忿忿不平。 但是，万没想到自己竟也投降了匈奴，自那以后，李陵已没有了和苏武再会的念头。苏武被迁送到遥远的北方，两人用不着见面，毋宁说反倒让李陵心底舒了口气。尤其自己在一家老小被灭族后已断了重回汉廷的念头，那就更不想当面碰上这位「持节牧羊」的汉使了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	新单于上任后，听闻李陵认识苏武多年，是旧友，便让李陵去找苏武看能否将其劝降，李陵便带着随从找到了作为匈奴奴隶但依旧持汉节牧羊的苏武。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;​	久别相逢的感动，一瞬间压倒了李陵内心一直害怕与苏武见面的那点隐私的想法。两人初见都不知说什么好。 李陵的随从搭起了几座穹帐，这块荒无人烟的旷野顿时热闹起来。预先备好的酒食马上送进小木屋，久久不闻的欢声笑语回荡在夜空中，惊动了森林中的鸟兽。&lt;/p&gt;
&lt;p&gt;​	李陵在这里驻留了好几天。 自己是如何穿上这身匈奴官服的经过，说来确实难以启齿。李陵只平实地说了事情的经过，一点都没有夹进为自己的辩解。苏武也平平淡淡地说了他这些年来的生活，他的处境看来很凄惨。从苏武口中得知，很多年前曾有匈奴的于靬王狩猎偶然路过此地，出于对苏武的同情，三年间为他提供了衣服食粮。但于靬王死后，就渐渐沦落到要从冰冻的土地中挖野鼠充饥的地步了。至于他被传生死不明的流言，那大概是因为他所养的羊群被马贼一头不留全部偷掠光了的缘故而以讹传讹吧。李陵把苏武老母已死的消息告诉了苏武，但其妻抛下儿子改嫁的那一节，到底没忍心当面直说。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	李陵看到持节的旧友，对比起自己如今的处境，不禁陷入自我怀疑和开解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;​	李陵感到奇怪的是：这个人抱什么人生目标而生存呢？到现在盼望着有朝一日荣归汉庭吗？听苏武的口风，时至今日他对此已完全不抱希望了。既然如此，又何必要一日日苦忍这样凄惨的岁月？若肯降服单于，定获重用，这自不待言。但李陵也知道，苏武若会这样做就不是苏武了。李陵真正奇怪的是：为何苏武不早早自求一死了断？李陵无法自我了断这种毫无希望的残生，是因为不知不觉中已在胡地里扎下了根，被种种恩爱情仇责任名分所束缚，再说就算自求一死，也已经混不上什么好名声了。但苏武不同，他在这里并没有任何拖累。从对汉室的忠诚来说，永无终止地持着汉朝节旄在旷野里忍饥挨饿，与烧掉节旄实时自刎相比，两者之间想来也没有多大差别。最初被匈奴拘捕时想也不想就拔剑刺胸想自杀的苏武，很难想象他到了今天这步田地反倒突然变得贪生怕死了。李陵回想起苏武年轻时的固执——那种近乎滑稽的强充好汉态度。单于以豪华为诱饵试图钓上极度困窘中的苏武，也许在苏武看来，被诱饵轻易钓上固然不堪，便是不堪苦难而自杀，也是对单于（或者是单于所象征的「命运」）的一种失败吧。然而，李陵并不觉得苏武这种和命运斗气似的态度有什么滑稽可笑之处。面对无法想象的困苦、贫乏、酷寒、孤独（而且是至死方休的长期间面对），能够淡然地一笑置之活下去，如果说这是斗气，那这种斗气绝对不能不说是壮绝人寰的斗气。过去多少被视为幼稚的苏武那种强充好汉的性格，已成长为一种伟大的风骨气节，李陵为之惊叹了。何况，此人并不指望自己的所作所为可以传回汉廷，既没有指望有朝一日还能被迎归汉廷，甚至也根本不指望会有人把自己在这样不毛之地难苦奋战的情形说出去，别说告诉汉朝天子、甚至向单于说一声的人也没有。最后肯定谁也看不到他孤独死去，他就打算这样在临终之日，回首平生，为自己能始终如一地蔑视命运的打击而安详死去。自己的事迹完全不为世人所知，那也没有关系，就是这样。李陵过去一度计划伺机袭取老单于的首级，然而，他又担心就算割下了单于的首级，万一之后自己不能带着首级成功逃出匈奴的追捕，那也是白搭，汉家朝廷也不会知道自己立下这样的大功，患得患失中，最终也没找到实施计划的机会。面对根本不在乎世人是否知道自己所为的苏武，李陵不竟冷汗涔涔在背。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	最终，李陵还是被这份思考以及苏武的精神击垮了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;​	最初的激动过去后过了两三天，渐渐有块东西梗塞在李陵的胸臆间，难以消散。不管扯到什么话题，越来越难免把自己的经历和苏武的经历两相比较。苏武是义士，自己是卖国奴——就算不是赤裸裸地意识到，面对苏武在森林荒野水洼的沉默中熬炼而成的严峻气节，不能不感到迄今用来为自己辩白的那些私人苦恼是如此不值一提。而且，不知怎的，一天天越来越开始感到，**苏武对待自己的态度中，有一种类似富裕者对待贫瘠者的成分——一种出于优越感而对对于施以宽容的态度。无法明说那是什么，但偶尔在不经意的附和隐隐感觉出来。**周身繿缕的苏武的目光中，偶尔会浮现出一丝怜悯的神情，这种神情令披裹着豪奢貂裘大衣的匈奴右校王李陵最为心怯。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;​	在李陵眼中，苏武对自己的态度，或许就是我们一直追求的一种豁达，或者说精神上的饱满：自认不所缺，不所求，只做着自己坚定坚信的事。以至于对待人或事，都会有一种富人对待穷人的时长有的态度，即出于明知自己处于优越地位，而展现出的从容宽解态度。&lt;/p&gt;
&lt;p&gt;​	我对这段落感触极大，这段落大大降低了我们的”内耗“。&lt;/p&gt;
</content:encoded></item><item><title>ostep-扩展思考-mysql的B+树与内存页</title><link>https://www.intotw.cn/posts/ostep-additional-mysql-page/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-additional-mysql-page/</guid><pubDate>Fri, 24 May 2024 06:42:34 GMT</pubDate><content:encoded>&lt;p&gt;​	了解完操作系统内存中的页以后，忽然想到了一个知识点，就是mysql的b+树设计，所以简单做下扩展知识。记录一下思考，基本也能覆盖b+树的面试题&lt;/p&gt;
&lt;h2&gt;b+树页大小&lt;/h2&gt;
&lt;p&gt;​	页大小是16kb，因为操作系统默认的内存页大小是4kb或者8kb，16kb选择作为整数倍，整存整取提高性能&lt;/p&gt;
&lt;h2&gt;b+树层数与存储的数据&lt;/h2&gt;
&lt;p&gt;​	b+树一般为3-4层，因为我们参考操作系统多级页表设计可以得知，在操作系统中，多级页表的每级每个节点都是设计成页大小。所以从性能考虑mysql也把叶子节点也设计成页的大小，即整棵树的所有节点的大小都是页大小。&lt;/p&gt;
&lt;p&gt;​	那么我们可以简单的估计下，在一个16kb的节点上，如果储存一个索引信息需要8b的话，可以存储约2048个索引信息。3层的b+树，在叶子结点存储数据的情况下，可以表示4194304个索引信息。也就是最后一层共有420w页，最后存储的行数=4200000*(16*1024/每行的大小）。&lt;/p&gt;
&lt;p&gt;​	在叶子节点，对于一页16kb一般来说可以保存10行左右，所以是约4000w行。但是除了索引信息，每个索引还要保存下一层的指针，所以总共会比4000w少一些，大约是2000-4000w。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 20 Paging Smaller Tables</title><link>https://www.intotw.cn/posts/ostep-20-paging-smaller-tables/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-20-paging-smaller-tables/</guid><pubDate>Wed, 27 Mar 2024 08:01:51 GMT</pubDate><content:encoded>&lt;p&gt;18章中遗留的2个问题，转换速度和内存开销，我们在19章中找到了TLB，通过缓存解决这个问题。剩下的就是内存开销，我们在第20章讨论这个问题。&lt;/p&gt;
&lt;h2&gt;最简单的方式，将页变大&lt;/h2&gt;
&lt;p&gt;转换页表的数量和虚拟页以及物理页的数量有关。在地址空间长度一定的情况下，页的大小越大，页码使用的bit就更少，自然页码范围就更小了。需要用来表达虚拟页和物理页映射关系的页表也就更小了。&lt;/p&gt;
&lt;p&gt;但是这种方法违背了我们的初衷，把页变大会导致页内部有大量的碎片（按页分配，但是实际进程只使用了某页中极小一部分内存）。&lt;/p&gt;
&lt;h2&gt;混合（杂交）方式，页+段&lt;/h2&gt;
&lt;p&gt;这种方式综合了段+页来处理页占用空间的问题，并且也提到了平时处理问题的一种方式：将两种方法混合起来一起使用，可能会有意想不到的效果。&lt;/p&gt;
&lt;p&gt;具体逻辑也比较简单，在定位到页表的某条映射记录时，通过段+VPN来定位到页表中的那个元素PTE（page table entry）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/f92eebb9efe15a5fe549440f9817f36e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/5b36735d58e8651e911274e1ad2b1b65.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上面2个图，第一个图表示这种实现下虚拟地址的分布，第二张图则表示如何获得PTE的实际物理地址，获取到PTE后，就可以通过PTE中的PPN+虚拟地址的offset获得需要的实际物理地址了。&lt;/p&gt;
&lt;p&gt;这种方式不无缺点，他解决的页层面的外部碎片，但是又带来了段内部可能的碎片。这也是段一直有的问题，不过段内部的碎片我们可以通过压缩的方式去减少。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/34f42f6e22e67dadfdcff2340ebf9a9b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考上图，你会发现code，heap，stack之间在页表中的碎片，如果使用段来分别描述3段，让他抽象成3张页表。比如转化为base,bound=[1,1]，[5,1],[15,2]，然后让他们直接在物理上就可以是不连续的，这样就可以使碎片消失。&lt;/p&gt;
&lt;p&gt;但是内部碎片呢？我们假设[5,1],[15,2]假如都是堆，也就是合并成[5,11]，那么内部依旧有很多碎片，段的问题就显现了。&lt;/p&gt;
&lt;h2&gt;多级页表&lt;/h2&gt;
&lt;p&gt;另外一种减少页表内存占用的方式，就是使用多级页表，多级页表使用另外的数据结构代替线性页表来存储页表，已达到节省空间的目的。其核心的思路是时间换空间，通过额外的查询搜索时间开销，来节省页表所使用的空间。&lt;/p&gt;
&lt;h3&gt;多级页表如何运作&lt;/h3&gt;
&lt;p&gt;多级页表首先改变了虚拟地址的定义，如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/aa2a7515809a0bb93446f1ca0f37aca6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原先的虚拟地址定义，VPN直接表示虚拟的页码，或者段的信息，在多级页表下，VPN则由页表的目录和页表的Index组成。&lt;/p&gt;
&lt;p&gt;我们看下页表目录的结构&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/94afdd933c55cef633c60cff3d6b7cdd.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中左边的结构就是目录Page Directory，右边就是PageTableIndex需要检索到的Index。定位的方法也很简单，首先通过VPN前段的目录Index，得到PageDirectory中的某个节点，该节点中保存了下一级PageTable的物理地址，然后在下一级物理PageTable中，使用VPN后段的PageTableIndex，找到对应的节点，这样的话，最后获取到的节点中保存的，就是我们实际需要访问的物理地址页的存在，加上虚拟地址中的offset，我们就可以访问到我们需要的那块内存了。&lt;/p&gt;
&lt;h3&gt;更多级的页表，以及可以思考的一些地方&lt;/h3&gt;
&lt;p&gt;首先，例子中的页表可以有多级，但是既然出现了这样的结构，我们可以很容易联想到：红黑树，B+树等等用于存储树的结构。用类似的结构去优化搜索的效率和存储的性能。&lt;/p&gt;
&lt;p&gt;我们还会发现，这样做的一个好处是，我们可以把页表中目录和每个节点以及最后的子节点，合理的也使用页去维护。即我们可以有多个PageDirectory，每个PageDirectory的大小也是页的大小，因为PageTable也得到了拆分，所以每个PageTable也可以是页的大小。通过这种设计，我们贯彻了内存中全部都是页的思路。可以想想按照最初的设计，内存中只有页表不是页，那是多么违和。&lt;/p&gt;
&lt;p&gt;在操作系统的层面，页表按照这种方式规划以后，还有另外一个好处，OS可以进行一定的控制，让页表也使用虚拟内存来管理，并且可以让它的信息尽可能常驻在TLB中，以提升检索效率。这里其实还有一点需要再次回忆一下，虽然我们的VPN现在里面包含了多级页表的信息，但是实际上TLB依旧只缓存VPN-PPN的信息，TLB并不需要去计算这个过程。但是OS可以通过把页表的内存也虚拟化，来通过TLB加速这个多节点搜索中寻址的开销~&lt;/p&gt;
&lt;h2&gt;倒置页表&lt;/h2&gt;
&lt;p&gt;这是一种比较特殊的实现，即将索引关系倒置过来。这种实现只维护一个物理页表，但是在每个物理页页表元素的后面，使用一个数组来维护使用了这个物理页的进程列表，其中的每个进程元素，又保存了这个进程中的第几个虚拟页使用了这个物理页。书中没图，自己简单画了个。可以看到这种倒置的页表，搜索的性能也是倒置的，想要找到对应的进程，就已经需要n了，再找到请求的虚拟页，如果虚拟页和物理页大小一样的话还没开销，否则如果一个进程多个虚拟页映射到了同一个物理页，此时又要付出一个n去找到发起转义请求的那个虚拟页了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/6830b6e754e05c85040d0f7272e6be60.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;页表交换到硬盘&lt;/h2&gt;
&lt;p&gt;这个在上面也提到过，多级页表有个好处就是它可以天然把所有节点简单的分割成页的大小，这样结合到后面讲到的页会交换到硬盘，那么页表本身也就可以交换到硬盘了（因为页表的结构现在也是一个树状的页，只是每个叶子节点和非叶子节点的代销都是页大小）。&lt;/p&gt;
</content:encoded></item><item><title>Ostep 19 Faster Translations TLBs</title><link>https://www.intotw.cn/posts/ostep-19-faster-translations-tlbs/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-19-faster-translations-tlbs/</guid><pubDate>Wed, 27 Mar 2024 06:15:38 GMT</pubDate><content:encoded>&lt;p&gt;本章主要讨论如何加速页码的转换，即如何快速通过虚拟页码找到物理页&lt;/p&gt;
&lt;h2&gt;TLB的概念&lt;/h2&gt;
&lt;p&gt;TLB（translation-lookaside buffer）是处理器芯片上内存管理单元memory-management unit的一部分，本质上是一个对于最近转换关系的一个高速缓存，所以从功能来看其实更贴切的名字是address-translation cache，叫TLB是因为历史原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通过缓存来加速这个步骤主要是考虑到了时间局部性和空间局部性。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这在我们前面了解CPU高速缓存的时候提到过。比如18章中那个循环1000次每次+1的值，在内存中需要访问的数组可能都在连续的页上，这更符合空间局部性（访问某元素后，马上要访问下一个紧挨着的内存，不过在这里是页）。用于统计的sum和循环计数用的i，在循环执行时会被多次访问，这符合了时间局部性（被访问过的内存、页，在接下来短时间内可能会继续被访问多次）。&lt;/p&gt;
&lt;h3&gt;TLB元素的结构&lt;/h3&gt;
&lt;p&gt;TLB元素的结构很简单，VPN | PFN | other bits&lt;/p&gt;
&lt;p&gt;另外的那些bit位，就用于保存一些标记位&lt;/p&gt;
&lt;p&gt;实际的一个实例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/12ffda175308cc3c1f2a361c70e5aee5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;TLB缓存缺失的处理&lt;/h2&gt;
&lt;p&gt;CPU高速缓存缺失数据时，由CPU从内存中读取到数据并缓存。那如果在TLB中缺失某页的映射信息，那么该交由谁处理？&lt;/p&gt;
&lt;p&gt;CPU的指令集有复杂指令集CISC和简洁指令集RISC。古早的硬件设计者们不太信任OS的开发者，所以他们使用CISC复杂的指令集来让CPU自己去处理。而RISC则是在发现缺失缓存后，由硬件触发一个中断，将事情告知OS，然后由OS自己来去处理这个问题。&lt;/p&gt;
&lt;p&gt;由OS处理就会和处理一般中断有一些比较大的区别，比如在某行代码触发一般中断后，实际上就该执行下一行代码了。而由TLB触发的缺失中断，在OS将缺失映射关系写会TLB后（要知道TLB在芯片上），需要再次执行一次触发中断的代码，此时这行代码就可以从TLB中读到数据了。&lt;/p&gt;
&lt;p&gt;不过这件事情也没有那么简单，因为如果OS处理完后，TLB出于某种淘汰策略把刚写进去的数据再次淘汰了，那岂不是可能无限循环了？所以OS处理这个问题是要考虑的问题实际要更多。&lt;/p&gt;
&lt;p&gt;另外就是，OS也会将一些OS自己的内存页等，永久保持一直在TLB中而不被淘汰，以提升OS自己的性能，这也是OS需要考虑的问题。&lt;/p&gt;
&lt;h2&gt;TLB与进程切换的难题&lt;/h2&gt;
&lt;p&gt;因为TLB在芯片上，并且缓存的是某个具体进程的虚拟页转换关系，那么在线程切换时，就会遇到CPU缓存一样的问题。&lt;/p&gt;
&lt;p&gt;不过因为现在TLB的写入以及控制，我们交给了OS，所以这个问题通过OS在CPU切换进程的时候，将这个进程的TLB内容保存到进程信息里，在切换回来时flush回去，我们是可以解决的。只是这需要成本，并且这个成本很大。&lt;/p&gt;
&lt;p&gt;另一种处理方式是由硬件TLB提供兼容，TLB在存储的时候除了存储虚拟和物理页码之外，另外存储一个id，这里可以是进程id的某种表示，这样对于TLB内的数据，就有了一个字段去区分进程间的数据，这样就不需要flush了。不过这未尝没有代价：TLB的命中率会受到影响。&lt;/p&gt;
&lt;h2&gt;TLB淘汰策略&lt;/h2&gt;
&lt;p&gt;TLB中的数据的淘汰策略可以有很多，随机、LRU，但是这个是我们需要考虑的&lt;/p&gt;
</content:encoded></item><item><title>Ostep 18 Paging Introduction</title><link>https://www.intotw.cn/posts/ostep-18-paging-introduction/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-18-paging-introduction/</guid><pubDate>Tue, 26 Mar 2024 08:46:04 GMT</pubDate><content:encoded>&lt;p&gt;这章引出了现在操作系统内存管理中最重要，也是最复杂的概念：页。就本书而言，都使用了长达3章的大量篇幅来介绍&lt;/p&gt;
&lt;p&gt;基于段管理带来的复杂性以及带来的碎片，还有另外一种方式去管理内存，也就是把内存划分为固定大小fixed-sized的页&lt;/p&gt;
&lt;p&gt;文中通过不少深入浅出的概念循序渐进的介绍，在笔记里我们就只总结关键的概念和内容了&lt;/p&gt;
&lt;h2&gt;页的概念&lt;/h2&gt;
&lt;p&gt;将内存分为固定大小的块，每块称之为一页。划分以后，管理的维度也就会变成页，即回收、分配等操作，都以页为单位进行。所以页的划分可以在OS一开始boot的时候就进行，直接确定页的大小以及数量。比如32K的内存，每页1K的话，内存理所当然就有32页了。&lt;/p&gt;
&lt;p&gt;有两类页，一些是虚拟的页，一些是真实物理内存的页，其中虚拟的页属于每个进程自己的地址空间，真实物理内存的页由操作系统管理，其中当然也需要地址转换。文中没有这类图，自己简单画了一个，左边就是各进程自己的虚拟页，右边是物理内存里实际的页。注意这里虚拟页不存储数据，只是一个地址空间，后面会继续讲&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/a6993176ebc7600477f511a38dc53bc3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;页地址的转换&lt;/h2&gt;
&lt;p&gt;从上面的图可以看出来，虚拟页需要对应到实际的物理内存的页，才可以拿到数据，我们看下结构和转换逻辑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/c7ac0acf85194d9ca08c0a295102ec8f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中我们可以看到，虚拟页的寻址，将地址划分为VPN（虚拟页码）以及offset（偏移），物理页也类似。其中如图，假设我们虚拟地址的前两位认为存储的是VPN，后面其余位认为是存储的offset的话，那么一个虚拟地址访问对应实际物理地址访问的实际过程可以概括如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过掩码位运算得到VPN以及offset&lt;/li&gt;
&lt;li&gt;VPN通过&lt;strong&gt;地址转换程序&lt;/strong&gt;，得到物理页码PFN&lt;/li&gt;
&lt;li&gt;物理页码PFN*物理页大小+offset，就等于实际访问的物理地址&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;页表&lt;/h2&gt;
&lt;p&gt;因为从虚拟页到物理页的转换，页表page table就必须存在，用于维护虚拟页和物理页实际的映射关系。&lt;/p&gt;
&lt;p&gt;页表自身也需要内存，甚至会占用大量内存。因为每个进程都需要拥有独立的页表（回忆一下之前的前提：每个进程都认为自己拥有整块内存，所以每个进程都拥有一个独立的虚拟地址空间来映射到物理地址，因此他们都需要一个独立的页表），所以我们假设我们使用32位长度的虚拟地址空间，其中每页的大小我们假如认为是4kb，那么因为页大小是4kb，所以后12位（$2^{12}=4096$）用于描述页内的offset。而前面的20位就拿来描述页码了。也就是说，页码的范围是0-$2^{20}=1048576$。&lt;/p&gt;
&lt;p&gt;如果页表每个元素需要4byte来表示，那么一张页表我们就需要4mb来描述了。要知道页表是每个进程存在一个的，如果现在有100个进程在运行，那么我们光储存页表都需要400mb的空间。这也是这种朴素的线性页表设计所存在的问题。占用了过量的空间。&lt;/p&gt;
&lt;h2&gt;页表的元素中大概有那些信息（举例）&lt;/h2&gt;
&lt;p&gt;大概如图，梳理下书中的这些字段&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/4272a46ec5f4cecdaf6f418360541ff0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为什么没有VFN呢，这里主要是认为VFN直接就是Index，所以里面只有对应的PFN。里面的字段如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;valid bit，这个元素是否有用，比如说进程刚启动，大部分页都还没有被使用，或者这个页已经不再被用了，会被修改为invalid&lt;/li&gt;
&lt;li&gt;protection bits，标记可读可写可执行，对应的是不同进程对同一个物理内存页的权限&lt;/li&gt;
&lt;li&gt;present bit，标记映射到的物理页是否在内存中，因为我们后面会知道，内存中的页也会经常被交换到硬盘中&lt;/li&gt;
&lt;li&gt;reference bit，引用标记，一般用于记录页面的使用次数等信息，服务于页面汰换算法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;页的代价&lt;/h2&gt;
&lt;p&gt;使用页也带来了一些代价，上面说过的内存400MB占用例子是其一。&lt;/p&gt;
&lt;p&gt;还有一个影响就是其中转换逻辑不简单，参考这段最简单的示例代码，哪怕只是简单的处理一下虚拟页和物理页的页码转换，都需要做2次掩码和一次计算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Extract the VPN from the virtual address
	VPN = (VirtualAddress &amp;amp; VPN_MASK) &amp;gt;&amp;gt; SHIFT
// Form the address of the page-table entry (PTE)
	PTEAddr = PTBR + (VPN*sizeof(PTE))
// Fetch the PTE8PTE = AccessMemory(PTEAddr)
// Check if process can access the page
if (PTE.Valid == False)
	RaiseException(SEGMENTATION_FAULT)
else if (CanAccess(PTE.ProtectBits) == False)
	RaiseException(PROTECTION_FAULT)
else
	// Access is OK: form physical address and fetch it
	offset   = VirtualAddress &amp;amp; OFFSET_MASK
	PhysAddr = (PTE.PFN &amp;lt;&amp;lt; PFN_SHIFT) | offset19Register = AccessMemory(PhysAddr)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面逻辑看起来简单，毕竟简单的*和&amp;amp;还有位移操作对CPU来说只是洒洒水，但是我们考虑到实际情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int array[1000];...for (i = 0; i &amp;lt; 1000; i++)array[i] = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个简单的循环，我们除了在寄存器上进行单纯的加之外，仅仅获取到实际的物理页转换所要做的额外操作，就已经远大于寄存器加法了~&lt;/p&gt;
&lt;p&gt;所以这章的Page基本概念也就介绍到此了，上面的问题也引出了后面2章的内容，19章我们尝试去解决这个转换速率的开销。20章我们去解决内存占用的开销。&lt;/p&gt;
</content:encoded></item><item><title>ostep 17 Free-Space Management</title><link>https://www.intotw.cn/posts/ostep-17-free-space-management/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-17-free-space-management/</guid><pubDate>Mon, 25 Mar 2024 07:54:21 GMT</pubDate><content:encoded>&lt;p&gt;这章主要介绍了空闲内存管理的一些内容，不过要先弄清楚前提，本章的内容是基于段来管理内存并且主要描述的是在管理段的内存时，内存分配库（malloc）或者os需要考虑的一些方面或者设计。有一点需要明确，否则很容易混淆：&lt;strong&gt;本章主要讨论的是内存分配库或者os管理某一个段内内存的情况&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;内部碎片以及外部碎片&lt;/h2&gt;
&lt;p&gt;结合上一章，在段的前提下，内存的碎片进一步扩展，可能会有内部碎片以及外部碎片，外部碎片就是段之间在实际物理内存之间的碎片，主要是因为内存中分配了很多段，导致新的段无法分配所以需要压缩段之间的空间。&lt;/p&gt;
&lt;p&gt;内部碎片就是某个段（本章以堆举例）之中，其中在内存不断进行回收以及分配的过程中，在单个段中产生的内部碎片。其实主要也就在堆中有了，因为代码段的内存和栈中的内存由编译器早早决定，其中的碎片比起由用户进程控制的堆来说可以说是小巫见大巫了。&lt;/p&gt;
&lt;p&gt;不过其实对于内存管理来说，不论是管理段，还是管理段内的内存，无非要面对的对象也就是一段又一段各自内部连续内存，但彼此之间又分割开的内存子空间罢了，所以内外部碎片的管理思路大同小异&lt;/p&gt;
&lt;h2&gt;段（堆）如何变大？&lt;/h2&gt;
&lt;p&gt;段如果在内存管理库在使用时发现大小不足，会使用系统调用sbrk中断来交换到操作系统，由操作系统来在内存中重新换一个更大的连续空间来防止这个段，并且由OS来保障原有的数据copy到了新的内存段中&lt;/p&gt;
&lt;h2&gt;空余内存空间管理的底层机制&lt;/h2&gt;
&lt;h3&gt;碎片的产生，或者说压缩的必要性&lt;/h3&gt;
&lt;p&gt;底层机制很简单，这里简单使用一个链表来描述整个过程。这本书对于描述过程和要讨论的机制，都使用最简单最容易理解的数据结构，以使得主要把注意力集中在要处理的问题以及OS的职责，而不是那些精妙的数据结构如何运作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/53b7bd08c3b0d45a36938b60fb2320e3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里略过一些文中过于复杂的过程赘述，这部分对稍微有一些算法或者有一定编码经验的人来说很好理解。对于内存的管理，我们使用一个链表来维护其中&lt;strong&gt;空闲部分&lt;/strong&gt;的状态。&lt;/p&gt;
&lt;p&gt;以上图为例，链表中有2个节点，第一个节点表示地址0开始有一个长度为10的空闲内存，第二个节点表示地址20开始也有一个长度为10的空闲内存。可以很容易看出来，此时addr10，len10这部分空间是在被使用的。&lt;/p&gt;
&lt;p&gt;如果addr10，len10被free了，那么整块内存就连续了。当然，第一步我们会把addr10,len10这个节点，加回链表中，此时链表有3个节点，也就形成了我们要讨论的碎片的一种形式，因为此时要申请一个30len的内存，按照3个节点来判断，我们在压缩前是无法申请的，所以我们就必须要将3个节点压缩成一个addr0，len30。&lt;/p&gt;
&lt;p&gt;这部分应该很好理解&lt;/p&gt;
&lt;h3&gt;追踪已分配内存的大小&lt;/h3&gt;
&lt;p&gt;其实从上面那个列表已经可以看出来，每个节点需要一部分额外的空间作为header去描述这部分内存的大小，简单来说就是一个size和一个magic。其中size就描述了这段内存的大小。magic作为魔法值用于完整性的校验（大抵是供内存库来校验这部分是否是它自己分配的？说实话不太懂要来校验什么）。那么在使用free来释放内存时，我们自然可以通过传入的指针马上拿到这部分的大小，来快速完成内存的释放（把这部分内存加入回free list里）。当然，这也使用了额外的一丢丢内存来储存这个header，所以在实际搜索位置去分配的时候的时候，如果用户要求20byte，那你要搜索到实际空闲的22byte才可以完成这个内存分配。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/059474919ab427d4f5f5e8de02b7884e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;通过header、head指针、next指针，在储存空间本身中内嵌free list&lt;/h3&gt;
&lt;p&gt;这里是一个比较巧妙也比较复杂的设计，这章大多数内容和图都在描述这个设计。&lt;/p&gt;
&lt;p&gt;为了避免free list本身使用额外空间储存，所以通过链表的设计，巧妙的把这个free list内嵌在空间本身中。这里精炼下这个过程。&lt;/p&gt;
&lt;p&gt;首先，我们认为内存还没分配，这个时候head指针在一开始，我们认为整个free list只有一个节点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/271a13d3eeafb0286c0841bfba1cf74b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在经历一系列的分配以后，我们的内存变成了下图。这里的主要逻辑是：每次从head开始遍历，满足分配的话，就将那部分新增一个头，头中只有size和magic（注意此时没有next只有最后的节点才有next）。从下图可以看到，head还在最后，前面分配的空间都只有size和magic，下次分配时继续从head开始遍历到next=0（当然现在也就只需要遍历一个节点）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/54b512106e0067335c6ac5f6c29e99b7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后释放free的时候，就比较巧妙，在某段释放以后，会通过从head开始遍历+修改某个节点的next指针的方式，将释放的那段内存加入到free list中去。原文中多了一个图，但是略微有点误导（其中包含了较为复杂的实际处理逻辑）。我们直接看下图，在释放了一系列开始申请的内存以后，现在的free list实际为（按照灰色区域的顺序，从1开始）head-&amp;gt;3-&amp;gt;1-&amp;gt;2-&amp;gt;4-&amp;gt;null。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/fca497873f1df03fd53e2257edfe147b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，在这种复杂度达到一定程度时（节点过多），我们就可能需要考虑触发下合并的逻辑了。当然，即使不合并，我们依旧可以通过head遍历到next=0，来完成这个free list的遍历。&lt;/p&gt;
&lt;p&gt;这种设计很巧妙，只是多使用几个字节（next）的内存，就可以把free list嵌入内存中。&lt;/p&gt;
&lt;h2&gt;挑选节点的基本策略&lt;/h2&gt;
&lt;p&gt;每个空闲节点有大有小，遍历时挑选的一些常见基本策略，这里稍微列举下，很好理解&lt;/p&gt;
&lt;h3&gt;Best Fit最佳大小匹配&lt;/h3&gt;
&lt;p&gt;这个策略很简单，查找空间大于等于所需空间的最小节点。举例来说，如果我要分配5byte（带上header），现在空闲的有1,3,5,7,11,15。那么就会选到5byte那块去分配。&lt;/p&gt;
&lt;p&gt;优点：减少浪费的空间以及意外的碎片，比如如果选择7的话，产生的2byte碎片大概率在压缩前会一直浪费掉&lt;/p&gt;
&lt;p&gt;缺点：需要找到最佳匹配，需要遍历整个列表，并且因为需要记录状态（上一个最接近的）会产生额外的性能和空间损失。&lt;/p&gt;
&lt;h3&gt;Worst Fit最大匹配&lt;/h3&gt;
&lt;p&gt;与Best Fit相反，这个是遍历后找到最大的去分配&lt;/p&gt;
&lt;p&gt;优点：产生的碎片很大&lt;/p&gt;
&lt;p&gt;缺点：也需要遍历，而且据文中所说，大量研究表明这种方式会导致过度碎片化并且性能非常低&lt;/p&gt;
&lt;h3&gt;First Fit 首次匹配&lt;/h3&gt;
&lt;p&gt;顾名思义，找到的第一个可以分配的节点就直接分配。&lt;/p&gt;
&lt;p&gt;优点：快，不需要遍历所有节点&lt;/p&gt;
&lt;p&gt;缺点：可能会被大量小块占据了头部导致遍历变慢。但是可以通过地址的顺序来进行合并，这样合并很快可以一定程度上解决这个问题。&lt;/p&gt;
&lt;h3&gt;Next Fit 匹配&lt;/h3&gt;
&lt;p&gt;这个算法并不总是在列表的开头开始首次适应搜索，而是保留一个额外的指针，指向列表中最后一次查找的位置。这个想法是在整个列表中更均匀地分布对自由空间的搜索，从而避免列表开头的分裂。这种方法的性能与首次匹配非常相似，因为再次避免了穷举搜索。&lt;/p&gt;
&lt;h2&gt;其他种类的内存管理方式&lt;/h2&gt;
&lt;h3&gt;隔离列表&lt;/h3&gt;
&lt;p&gt;这个其实不太理解为什么叫隔离列表。但是思路比较简单，其实是一种异种的列表。这个列表专门管理那些常用的、固定的内存对象分配（文中举例是锁、文件系统的node）。比较定制化&lt;/p&gt;
&lt;p&gt;优点的话举个例子，如果某个场景大量重复申请释放4byte的空间，那么我专门设计一个逻辑去具体处理4byte内存的申请释放。每次只允许4byte，需要考虑的场景就少很多，性能大大提升。&lt;/p&gt;
&lt;h3&gt;Buddy Allocation兄弟分配&lt;/h3&gt;
&lt;p&gt;这个一图就可以说明，利用二进制块，进行分配以及回收，优点就是压缩非常快（基本不需要压缩），缺点就是不够灵活并且有一定空间的浪费&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/d521f028cf7df765817a6a4d4ea80fab.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;空闲内存管理本身就是一种权衡，来挑选使用什么数据结构或者方式去维护空闲的列表。权衡需要兼顾到额外空间的使用、空闲空间搜索的效率、压缩性能的考量。&lt;/p&gt;
&lt;p&gt;真想要了解到最核心的知识以及实现，去看glibc的内存分配就完事喽。&lt;/p&gt;
</content:encoded></item><item><title>ostep-16 Segmentation</title><link>https://www.intotw.cn/posts/ostep-16-segmentation/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-16-segmentation/</guid><pubDate>Tue, 19 Mar 2024 07:39:32 GMT</pubDate><content:encoded>&lt;p&gt;这一章还真的有点复杂和晦涩&lt;/p&gt;
&lt;p&gt;上一章讲了base和bound的概念，base和bound通过2个寄存器直接描述进程的物理内存起止，并且可以通过base+虚拟内存中的偏移量得到实际访问的物理内存地址。&lt;/p&gt;
&lt;h2&gt;段的基本概念和基本设计&lt;/h2&gt;
&lt;p&gt;本章提出了一个新问题：按照第13章中描述的进程内存模型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/ce4aff19c5178ae920488e3d77230f8c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;按照进程内存模型，一个进程的&lt;strong&gt;虚拟内存&lt;/strong&gt;如果是连续的话，其中堆和栈之间的&lt;strong&gt;虚拟内存地址&lt;/strong&gt;在应用实际申请前是完全浪费的，并且大小也不可预估，所以就提出了&lt;strong&gt;段&lt;/strong&gt;的概念，来作为逻辑上的进程内存分割。实际上就是我们把进程的内存模型如果认为分为3部分，即code、heap、stack的话，我们完全可以使用3对base+bound将他们视作三段不同的逻辑分段。&lt;strong&gt;注意我们这里讨论的段只生效在虚拟内存模型里，虚拟内存到物理模型的转换还是使用base+bound的模型去转化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/2dab3381d74b37f740292d5377f4245b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图文中16.1中表示的这样，实际也就是可以认为这三段在虚拟内存的逻辑上也完全可以是不连续的，这样可以提高虚拟内存地址空间的使用效率。结合一个base+bound表16.3以及16.1，我们来看下这种方式如何运作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/0190d179008675034aafc787023f163e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;​	假设我们要访问一个虚拟内存中偏移为100的内存，那么因为他在Code段中，所以我们可以得到他的物理地址为$ 32768+100=32868$，并且可以通过100来检查内存是否越界访问了。&lt;/p&gt;
&lt;p&gt;​	如果是堆，那么会稍微复杂一点。假设我们要访问偏移为4200的内存，因为这个偏移4200的内存实际位于的段是Heap段，所以我们需要再计算一下实际这个数值针对该Heap段的偏移，我们可以从16.1中看到Heap段在虚拟内存中起始位置是4K，所以实际4200针对Heap端的偏移为$ 4200-4096=104$，那么它的物理地址就是$34816+104=34920$&lt;/p&gt;
&lt;h2&gt;段的类型？&lt;/h2&gt;
&lt;p&gt;我们可以看到目前的计算都基于16.1，我们从图中知道了对应的虚拟地址上所属于的是怎样的段。但是实际对于OS来说，却没有这么简单，因为这些段都是动态分配并且没有图给OS看。所以针对段的类型，一般有两种方式来标记。&lt;/p&gt;
&lt;h3&gt;显式标记&lt;/h3&gt;
&lt;p&gt;一种方式就是显式标记，显式标记非常简单，对于任何一个虚拟地址的值，我们使用前两位来表示段的类型，后续位置表示在该段上的偏移量，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/e0d328eab294a028342e74f726301a24.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如说，上面的4200位置，在这种表示方式下，虚拟地址就应该是&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/ca3da940a5262399fb06d2a07aba210a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中起始的2位01表示是Heap段，后续的12位即000 0110 1000, 就是我们计算的结果104&lt;/p&gt;
&lt;p&gt;显示标记有几个缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前两位可以表示4个状态，但是实际我们只有3个类型的段，有一丢丢浪费。所以通过对于code以及heap段基本是相同的处理逻辑（都是向下增长）考虑，将code和heap认为是一个类型，这样1位就可以表示了&lt;/li&gt;
&lt;li&gt;使用了2位后，实际地址空间缩小了一个很大的量级去使用，比如这里例子上，14位系统只能用13位表示实际偏移量，虚拟地址空间实际可表示大小缩小了2倍，如果使用2位，那么缩小了4倍&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;隐式标记&lt;/h3&gt;
&lt;p&gt;还有一种隐式标记的处理方式，逻辑也非常简单：通过这个地址段是如何形成的来决定类型。比如通过程序计数器PC生成的，那么肯定是code段。如果地址是基于栈或者基准指针，那么肯定是stack段。其余的都认为是heap段&lt;/p&gt;
&lt;h2&gt;如何处理栈的偏移计算？&lt;/h2&gt;
&lt;p&gt;这里涉及到另一个寄存器需要增加的状态位：正负。这里的正负是指的偏移计算的正负，因为我们知道堆是向下增长的，而栈是向上，在计算偏移的时候，CPU需要了解到这个正负来快速计算。（你不能指望CPU根据段的类型写个if else，这将完全破坏CPU的单一功能并且将CPU和OS的段设计完全耦合，要知道虚拟地址空间的设计是OS的设计不是CPU的设计）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/abac24eae44e587952ff0cdec274358c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个表中多了一个max4K，这个需要注意一下，这个表示了端的最大偏移大小，这个值不需要额外的寄存器保存，因为在我们的显示标记表达法下，这个值是显而易见的固定值——在14位的机器中，前2位被使用表达了端类型，剩下的12位表示偏移量，12位能表达的最大偏移量就是4k&lt;/p&gt;
&lt;p&gt;这样让我们来计算一下这种情况下stack的实际物理地址。当我们想要访问虚拟内存的15k时，实际对应的是物理地址的27k。在我们的显示标记表达法之下，转换的地址值就是: 11 1100 0000 0000 (hex 0x3C00），其中前2位11表示了这个段的类型是栈，然后剩下的偏移量3k，我们通过3k-4k（因为是负向增长，所以需要减去偏移范围最大值才是实际的偏移）得到-1k，此时对应的物理地址28k直接+上面得到的-1k就得到了实际的物理地址27k。&lt;/p&gt;
&lt;p&gt;此时计算范围也很简单，依旧是对比得到的负偏移值-1k的绝对值小于实际size2k即可。&lt;/p&gt;
&lt;h2&gt;通过段来支持共享内存&lt;/h2&gt;
&lt;p&gt;聪明的前辈开发者们很快了解到，再加一个标记位来表示这块内存的保护状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/75f2b0cc6741ddfcd5365b32a69265df.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就可以通过保护位的标记，来使得同一个段在不同进程之间有不同的访问状态，多个进程可以对同一个内存段执行或者读取。比如你直接跨进程调用他们的方法。或者通过访问公共信号量来进行通信&lt;/p&gt;
&lt;h2&gt;段的颗粒度以及内存碎片压缩&lt;/h2&gt;
&lt;p&gt;段的颗粒度可以有粗有细，像最粗的粒度（code，heap，stack），可能会导致内存的利用率低。细粒度也有缺点，需要额外的段表来进行管理，但是带来的好处是OS可以通过段表对内存做更加高效的管理。&lt;/p&gt;
&lt;p&gt;因为段的形式，所以内存中会出现大量的内存碎片，原文中称之为外部碎片external fragmentation。下图就很形象的表示了&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/b19b043c72ad6082a00ca0b7ccdbf333.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;碎片主要影响OS在进程申请新的段或者扩大原有段时的操作，多个碎片合并可能可以满足新的内存申请需求，但是单个碎片却不行。&lt;/p&gt;
&lt;p&gt;一个解决问题的办法就是经常去压缩内存，也就是把分配的重新迁移复制到一起，类似于java的gc，将所有allocated移动到内存头部，剩余的自然就变成了一大块内存。但是这样会带来很大的CPU消耗&lt;/p&gt;
&lt;p&gt;下一章介绍的是空闲内存的管理，会进一步解决这个内存压缩的问题&lt;/p&gt;
&lt;p&gt;由此章可见，虚拟化内存是多么复杂，如此多的步骤也只介绍了虚拟化内存工作的冰山一角。虚拟化内存要解决的问题远比虚拟化cpu要多得多。&lt;/p&gt;
</content:encoded></item><item><title>ostep-15 Mechanism Address Translation</title><link>https://www.intotw.cn/posts/ostep-15-mechanism-address-translation/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-15-mechanism-address-translation/</guid><pubDate>Mon, 18 Mar 2024 08:25:22 GMT</pubDate><content:encoded>&lt;p&gt;这章顾名思义，主要介绍了OS中地址转换的细节，之前提到过，用户进程看到的一定是虚拟地址空间，其中虚拟地址空间的转换就由地址转换来做。&lt;/p&gt;
&lt;p&gt;这章的后半部分结合地址翻译器，总结了一下目前OS所有需要通过硬件支持做到的事情，包括中断、限制直接执行、地址翻译等等。&lt;/p&gt;
&lt;p&gt;其实这章真正描述的地址转换相关内容很少，大概有以下要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;地址转换主要通过硬件实现，在每个CPU上，会有2个寄存器，专门保存一个base和一个bound，base保存地址的开始，bound保存这块内存的size&lt;/li&gt;
&lt;li&gt;某个进程获取内存时，由OS通过内存的free list（暂时理解为一个记录空闲内存的列表），在&lt;strong&gt;物理地址&lt;/strong&gt;中选取一块区域分配给进程，然后因为当前在内核模式，可以直接去修改寄存器中的base为内存&lt;strong&gt;物理地址&lt;/strong&gt;的开始地址，bound为分配给进程的这块内存的大小&lt;/li&gt;
&lt;li&gt;在进程执行代码或者尝试操作内存时，实际操作的&lt;strong&gt;物理地址&lt;/strong&gt;=虚拟地址+base（偏移量），这样用户进程就可以认为自己的内存实际从0开始拥有整块内存&lt;/li&gt;
&lt;li&gt;同时在执行内存相关操作时，CPU会通过实际访问的内存空间，结合base+bound圈定实际访问的内存范围，如果进程访问到了超过base+bound之外的内存，那么显然这个是越权违法操作，CPU就会触发OS的error handler让OS去决策如何操作（一般是终止进程）&lt;/li&gt;
&lt;li&gt;当然，OS在进行进程切换上下文操作的时候，需要保存和恢复base和bound寄存器中的数据，这样才能保证进程切换正确&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;截止到目前为止的硬件对OS的核心支持能力&lt;/h2&gt;
&lt;p&gt;这里书中描述的很详细了，直接贴图吧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/1f07ca388605c1ab0041046e0e16d2a7.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作系统针对内存虚拟化需要具备的能力&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/5fc3e8e8555ef2ca48f4992f4c0989dc.png&quot; alt=&quot;image-20240618155430143&quot; /&gt;&lt;/p&gt;
&lt;p&gt;操作系统启动时需要初始化的一系列东西，我们可以看到有很多handler处理中断，操作系统需要初始化trap table，中断表来响应这些中断，还有启动定时中断器来定时获取整个计算机的控制权&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/7e3362418d250e4d0378df2329a91e58.png&quot; alt=&quot;image-20240618155500249&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后是一个时序图，完整的描述了目前位置我们的知识中，操作系统对进程的创建、执行、调度，以及操作系统如何和硬件一起帮助用户进程执行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/d37e3df2c7b078fdb0708fa2916b6488.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>ostep学习笔记-14 Interlude: Memory API</title><link>https://www.intotw.cn/posts/ostep-14-memory-api/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-14-memory-api/</guid><pubDate>Mon, 18 Mar 2024 06:38:39 GMT</pubDate><content:encoded>&lt;p&gt;这章主要介绍了操作系统给进程提供用的内存的API，主要是C语言的视角。&lt;/p&gt;
&lt;h2&gt;内存类型&lt;/h2&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;栈内存由进程在编译时编译器就可以决定，栈内存主要是函数中的这类内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func() {int x; // declares an integer on the stack...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简而言之就是函数内的局部变量，这些变量在编译时就可以确定大小以及数量，在函数执行完后销毁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;java与其何其相似，jvm也是将局部变量分配在函数的栈帧上，随着函数的调用分配以及销毁的。只能说内存管理大道至简，大同小异&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;堆&lt;/h3&gt;
&lt;p&gt;栈内存只在函数上，并且由编译器管理，如果需要跨越函数的上下文域或者在进程整个周期中传递使用，那么就需要在堆中申请内存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void func() {int*x = (int*) malloc(sizeof(int));}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用malloc申请堆内存，入参是内存的大小字节，返回的是内存起始地址的地址指针。这个例子就申请了一个int的位置（4字节），并且返回了一个指向地址初始地址的指针x。注意一个比较特殊的点，这里其实既有一个栈的内存又有一个堆的内存，(int *x)是在栈上的，他所指向的内存是在堆上的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;eh，和java何其相似，Object a=new Object()。a在栈上，后面的对象在堆上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;内存API&lt;/h2&gt;
&lt;h3&gt;malloc()&lt;/h3&gt;
&lt;p&gt;上面有例子，也比较简单，入参就是要申请的大小（字节），返回为void，需要用户强转成自己需要的类型&lt;/p&gt;
&lt;h3&gt;free()&lt;/h3&gt;
&lt;p&gt;释放malloc的内存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int*x = malloc(10*sizeof(int));...free(x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里只用送起始的指针，因为实际的大小由内存分配库自己去追踪&lt;/p&gt;
&lt;h2&gt;容易出现的问题&lt;/h2&gt;
&lt;p&gt;堆内存管理因为由用户自己管理，很容易出现问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;忘记申请内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;申请的内存过小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忘记初始化内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;忘记释放内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在处理完成前提前释放了内存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重复的释放了内存&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看看java解决了什么，java的堆和gc解决了123456，真是太苦啦，可惜代价就是性能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ostep学习笔记-13 Abstraction Address Spaces</title><link>https://www.intotw.cn/posts/ostep-13-abstraction-address-spaces/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-13-abstraction-address-spaces/</guid><pubDate>Mon, 18 Mar 2024 06:25:50 GMT</pubDate><content:encoded>&lt;p&gt;这章主要描述了内存中对进程来说地址空间是什么东西&lt;/p&gt;
&lt;h2&gt;地址空间&lt;/h2&gt;
&lt;p&gt;在进程来看，地址空间分为3部分，代码（code），堆（heap），栈（stack）&lt;/p&gt;
&lt;p&gt;在进程自己的地址空间中视图如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/ce4aff19c5178ae920488e3d77230f8c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中随着内存的申请和栈的扩大，堆会在内存中向下扩展，而栈是向上，也就是会逐渐填满free那块。&lt;/p&gt;
&lt;p&gt;还有段代码比较经典，打印进程中main函数的地址（代码），堆的地址（使用malloc），还有栈的地址（函数内的变量），这里比较神奇的是c竟然可以使用main来指代main函数，这看起来像是个函数式的东西，神奇&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
int main(int argc, char*argv[]) {
    printf(&quot;location of code : %p\n&quot;, main);
    printf(&quot;location of heap : %p\n&quot;, malloc(100e6));
    int x = 3;
    printf(&quot;location of stack: %p\n&quot;, &amp;amp;x);
    return x;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就是总结下实现虚拟内存的3个目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;透明性，对用户进程来说内存应该是透明的，用户进程不需要知道底层的实现，它可以认为它拥有整个内存&lt;/li&gt;
&lt;li&gt;性能，这点不比多说&lt;/li&gt;
&lt;li&gt;保护性，我们需要保护不同进程间的内存安全，甚至保护OS自己的内存安全，防止用户进程修改OS&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ostep学习笔记-11-12 Summary Cpi Virtualization</title><link>https://www.intotw.cn/posts/ostep-11-12-summary-cpi-virtualization/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-11-12-summary-cpi-virtualization/</guid><pubDate>Mon, 18 Mar 2024 05:50:28 GMT</pubDate><content:encoded>&lt;p&gt;第11以及第12章主要是通过对话总结并且引申出了内存的虚拟化，并且都是对话内容，所以一起写一篇&lt;/p&gt;
&lt;h2&gt;CPU的虚拟化&lt;/h2&gt;
&lt;p&gt;cpu的虚拟化大概有以下要点要理解&lt;/p&gt;
&lt;h3&gt;进程切换&lt;/h3&gt;
&lt;p&gt;进程的切换主要由操作系统完成，通过对进程的定义（进程对象中使用一个结构体来保存寄存器的状态），在切换进程时由操作系统将当前CPU的寄存器状态从内存中还原回CPU或者从CPU保存到寄存器中&lt;/p&gt;
&lt;h3&gt;Limited direct execution内核模式&lt;/h3&gt;
&lt;p&gt;操作系统通过Cpu提供的limited direct execution（限制直接执行），使得OS运行在高优先级的内核模式（privilege mode or &lt;em&gt;kernal&lt;/em&gt; mode），而用户代码则运行在低优先级的user mode。对于设备以及内存和CPU的访问（比较具体的在第15章），只有OS在内核模式才可以执行。&lt;/p&gt;
&lt;h3&gt;调度&lt;/h3&gt;
&lt;p&gt;用户进程通过OS提供的api进入中断来使用一些高优先级的功能，例如IO。在进程生命周期模型上&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断后的进程会进入Blocking状态，阻塞等到OS完成操作的执行&lt;/li&gt;
&lt;li&gt;OS执行完进程的要求后，会将进程修改为Waiting状态，等待调度程序执行&lt;/li&gt;
&lt;li&gt;调度程序通过调度策略（MLFQ或者CFS），确定该进程该得到执行后，将进程执行，此时为Running状态&lt;/li&gt;
&lt;li&gt;Running状态的进程，当时间片执行完（其实是由调度策略的定时中断或者操作系统主动的定时中断触发的）或者再次发起IO等中断时，再次回到Waiting或者Blocking状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;操作系统为了防止一直由用户进程持有CPU执行，会在操作系统启动时注册定时中断，以此来定时从用户进程中中断来获取cpu控制权来执行那些操作系统该操作的事&lt;/p&gt;
&lt;p&gt;主要实现调度的策略有MLFQ和CFS。&lt;/p&gt;
&lt;p&gt;其中MLFQ使用多级反馈队列，通过进程执行时的表现来决定进程执行的优先级以此把他们在不同优先级的队列中来回移动。MLFQ适用于带交互界面的操作系统，如Mac和Windows等，更重视的是用户交互进程的response time响应，保障用户使用时不会有过于明显的卡顿&lt;/p&gt;
&lt;p&gt;CFS是Linux或datacenter系统使用的，主要考虑的是进程之间对于CPU资源的公平性以及权重。主要使用vruntime这个一定比例由实际使用cpu的realtime乘以权重等数值计算而来的&lt;em&gt;虚拟执行时间&lt;/em&gt;来保证CPU资源的公平分配，所以适用于那些交互界面简单（命令行），以及主要执行计算任务的OS。&lt;/p&gt;
&lt;h2&gt;内存的虚拟化&lt;/h2&gt;
&lt;p&gt;这里就比较简单了，主要是2个概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作系统通过内存API来为用户进程提供内存的使用&lt;/li&gt;
&lt;li&gt;所有用户侧看到的内存都是虚拟化的&lt;/li&gt;
&lt;li&gt;由操作系统提供进程间内存的隔离性以及保护&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>ostep学习笔记-10 Multiprocessor Scheduling Advanced</title><link>https://www.intotw.cn/posts/ostep-10-multiprocessor-scheduling-advanced/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-10-multiprocessor-scheduling-advanced/</guid><pubDate>Thu, 14 Mar 2024 02:17:48 GMT</pubDate><content:encoded>&lt;p&gt;这样主要介绍了下更进一步的调度，结合现代操作系统场景下，如何解决调度程序面临的更多的挑战-并发？&lt;/p&gt;
&lt;h2&gt;主要挑战&lt;/h2&gt;
&lt;h3&gt;并发与一致性&lt;/h3&gt;
&lt;p&gt;多CPU场景下，调度程序首先需要面临的挑战就是并发与一致性的挑战。文章里简单介绍了下CPU的缓存，然后带出CPU缓存在调度时会出现的问题。&lt;/p&gt;
&lt;p&gt;现代CPU基本都有3级的高速缓存，高速缓存位于CPU和内存Memory之间，高速缓存的速度一般是内存的几十倍（文中写到内存为几十到几百纳秒，高速缓存一般为几纳秒），高速缓存设计和存在的目的是因为大部分程序在运行时都具备局部性。&lt;/p&gt;
&lt;p&gt;举个例子&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当你访问一个数组时，你大概率是在迭代访问，所以高速缓存会顺带加载临近的元素到高速缓存中加快下次访问速度&lt;/li&gt;
&lt;li&gt;当你的代码执行时，将下面的指令（可以理解为下面几行代码）加载到高速缓存中，可以大大提升执行速度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;高速缓存在多处理器场景下就会存在问题，因为高速缓存与内存间并不存在强一致性。例如当你修改一个值以后，这个修改会先体现在高速缓存中（以便后续读取更快），然后通过其他策略定时或者在淘汰时再写回内存。这样在多CPU场景下就会出现这样一个问题：A进程在CPU1使用了一个变量并修改，此后调度程序将A调度到CPU2上执行，此时CPU2的高速缓存中没有A在CPU1上修改的数据，那么此时A可能会读到错误的值并继续错误的操作。&lt;/p&gt;
&lt;p&gt;这类问题叫做缓存一致性问题，处理方法也不复杂。一般设计是所有缓存统一通过一个总线&lt;strong&gt;bus&lt;/strong&gt;来访问内存，每个高速缓存通过总线来检测自己缓存中的数据是否有更新，如果有更新则将其失效或者更新（从内存中重新获取新的值）。这里只是简单描述了总线处理的概念，细节更加复杂（比如回写时内存的可见性）。&lt;/p&gt;
&lt;p&gt;扩展一下，Java的著名NIO框架Netty中就使用了大量固定Long值来占据对象的头，来避免&lt;strong&gt;伪共享&lt;/strong&gt;问题，之前有一篇博客提到过&lt;a href=&quot;https://intotw.cn/posts/netty-false-sharing/&quot;&gt;Netty学习：伪共享&lt;/a&gt;，但是在此书深入了解原理后，理解会更加透彻。本质就是为了保证缓存一致性的更新或者失效策略的代价在高并发下远大于使用高速缓存的收益，所以通过这种方式去变相&lt;strong&gt;禁用&lt;/strong&gt;掉高速缓存（让高速缓存能加载的值全部是填充值，以此让他们全部相同，常变的值在剩下位置，使他们避免使用高速缓存）&lt;/p&gt;
&lt;h3&gt;同步性&lt;/h3&gt;
&lt;p&gt;这里文中只是简单的描述了下，也比较好理解，当进程的线程在多个CPU上同时执行，或者在多个CPU上共同访问一个数据结构时，会产生并发问题，解决的方式就是通过lock，代价就是性能&lt;/p&gt;
&lt;h3&gt;亲和性&lt;/h3&gt;
&lt;p&gt;亲和性指的是同一个进程肯定一直在同一个CPU上执行，性能会好得多。因为其中寄存器的状态或许可以无需改变，高速缓存中的数据或许还没有被完全淘汰。所以在设计调度程序的时候，如何一定程度上保障进程的亲和性，也是一个挑战&lt;/p&gt;
&lt;h2&gt;基本思路&lt;/h2&gt;
&lt;p&gt;基本的设计思路有2个类型，一个是单队列，一个是多队列&lt;/p&gt;
&lt;h3&gt;单队列&lt;/h3&gt;
&lt;p&gt;单队列即使用一个单个的队列（或者类似数据结构）。来控制所有CPU上任务的调度，优点是实现较为简单，缺点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为在并发的场景下必须使用到锁来保证调度队列的同步性，CPU越多，锁的频率和争抢就越频繁，所以单队列的扩展性很差。&lt;/li&gt;
&lt;li&gt;单队列如果要考虑到亲和性的话，普通的轮询（针对CPU轮询去分配任务）算法肯定是无法保证亲和性的，但是如果要设计一些经常迁移进程到不同CPU去执行来保证亲和性的话。这个设计可以是非常复杂甚至难以实现的&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;多队列&lt;/h3&gt;
&lt;p&gt;多队列的实现思路也很简单，对每个CPU维护一个队列，相当于每个CPU有一个单队列，这样就解决了因为同步性的问题带来的不好扩展的问题。虽然增多了维护成本和在多队列上的CPU时间损耗，但是这个方案所能带来的扩展性十分值得去接受这些损耗。&lt;/p&gt;
&lt;p&gt;还有一个究极老大难问题依旧是我们的亲和性，多队列场景下，亲和性很难保证，一个简单的想法是我们加一个调度在多队列之上来分配任务保证每个队列中的任务平均。但是这样又和单队列有什么本质区别呢？我们又带来了同步的消耗。所以一个比较推荐的做法是每个队列在其执行调度期间，检查如果自己队列的负载较低的话，就去其他队列中找那些负载较高的，迁移几个任务来自己这边执行，英文名称比较形象：work stealing&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;基于亲和性、扩展性的取舍，Linux也没有一统江山的多CPU调度程序，Linux目前有3种调度程序O(1)，the Completely Fair Scheduler (CFS), and the BF Scheduler (BFS)，其中O（1）和CFS使用多队列，BFS使用单队列。&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;这章主要介绍了多CPU设计调度程序会面临的一些关键挑战和问题，以及最基本的2种设计思路，实际细节文中推荐一篇论文去读：&lt;a href=&quot;https://research.cs.wisc.edu/adsl/Publications/meehean-thesis11.pdf&quot;&gt;Towards Transparent CPU Scheduling&lt;/a&gt;。我打算整本书看完后再去补，因为论文有200多页PDF，这本书800多页，先去读论文有点影响这本书的学习进度。&lt;/p&gt;
</content:encoded></item><item><title>ostep学习笔记-9 Scheduling Proportional Share</title><link>https://www.intotw.cn/posts/ostep-9-scheduling-proportional-share/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-9-scheduling-proportional-share/</guid><pubDate>Tue, 12 Mar 2024 06:42:34 GMT</pubDate><content:encoded>&lt;p&gt;除了第八章之外，第九章还介绍了其他一种种类的调度算法，包括Linux采用的算法CFS，介绍的过程也循序渐进&lt;/p&gt;
&lt;p&gt;这类算法的特点是&lt;strong&gt;按比例共享&lt;/strong&gt;，也就是Proportional Share的意思。这类算法主要考虑的是权重以及分配时间片的比例，最后我会稍微总结一下自己思考的和与第8章中算法的一些核心区别和为什么需要考虑这些。&lt;/p&gt;
&lt;h2&gt;Lottery Scheduling（抽奖算法）&lt;/h2&gt;
&lt;p&gt;最基础的算法是Lottery Scheduling，抽奖算法。&lt;/p&gt;
&lt;p&gt;抽奖算法基于ticket的概念，所有进程持有一定数量的ticket，调度程序通过随机数来随机出一个ticket数来决定哪个进程被调度，这比较简单浅显的实现了比例的概念，因为持有票数多的更容易被随机到。一个简单的例子就是A持有100票，B持有100票，调度程序产生一个【0-199】的随机数，当随机数=0-99时运行A，100-199时运行B。&lt;/p&gt;
&lt;p&gt;抽奖算法还有几个其他的概念，这里简单的介绍一下。&lt;/p&gt;
&lt;p&gt;一个是所谓的票据在用户维度的比例缩放，这里直接放下原文吧：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For example, assume users A and B have each been given 100 tickets.User A is running two jobs, A1 and A2, and gives them each 500 tickets(out of 1000 total) in A’s currency. User B is running only 1 job and givesit 10 tickets (out of 10 total). The system converts A1’s and A2’s allocationfrom 500 each in A’s currency to 50 each in the global currency; similarly,B1’s 10 tickets is converted to 100 tickets. The lottery is then held over theglobal ticket currency (200 total) to determine which job runs.&lt;/p&gt;
&lt;p&gt;例如，假设用户A和用户B分别获得了100张抽奖。用户A正在运行两个作业，A1和A2，并给予它们每个500张抽奖（总共1000张）。用户B只运行一个作业，并给予它10张抽奖（总共10张）。系统将A1和A2的分配从A的货币中的每个500张转换为全局货币中的每个50张；同样地，B1的10张票被转换为100张票。然后，使用全局抽奖货币（总共200张）进行抽奖来确定哪个作业运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另一个是票据的移交，在进程自身不需要执行时，可以主动将票据交给其他进程，其他进程忙碌完以后，再将票据归还&lt;/p&gt;
&lt;p&gt;最后就是票据膨胀，简而言之就是进程自己多要一些票据提高自己的优先级&lt;/p&gt;
&lt;p&gt;抽奖调度适合在进程互信的场景下，但是好像也并没有什么明显的优势。唯一的优点就是实现很简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将所有进程使用列表排列&lt;/li&gt;
&lt;li&gt;根据总票数随机一个数值&lt;/li&gt;
&lt;li&gt;以此遍历每个进程，并再迭代过程中累加遍历过的进程持有的票数&lt;/li&gt;
&lt;li&gt;迭代时，如果当前迭代的节点持有票数+累加的票数&amp;lt;随机的票数，该节点获得执行权&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;步进调度&lt;/h2&gt;
&lt;p&gt;这个算法包括后面的CFS，都有和之前总结的负载均衡算法有异曲同工之妙，甚至是相同的：&lt;a href=&quot;https://intotw.cn/posts/load-balance/&quot;&gt;分布式系统：负载均衡算法&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简而言之就是初始给每个进程一个初始步长，然后每个调度周期取出步长最小的进程去执行，然后每个进程根据权重增加一定数量的步长&lt;/p&gt;
&lt;p&gt;这么看来，这种负载均衡算法实在是既易于实现又经典，牛啊&lt;/p&gt;
&lt;h2&gt;CFS（Completely Fair Scheduler )&lt;/h2&gt;
&lt;p&gt;CFS是Linux使用的调度策略，特点也和名字一样，公平（尽可能）的调度是它的目标&lt;/p&gt;
&lt;p&gt;CFS的执行步骤其实很简单，重复2步&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给每个进程一个vruntime，虚拟运行时间&lt;/li&gt;
&lt;li&gt;每次窗口选择一个vruntime最短的进程去执行，执行后累加vruntime，累加值一般为实际执行的物理时间乘以一定比例&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;比较复杂的是其中的一些情况带来的几个参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sched_latency：该参数为了解决一个问题，“一次进程执行该设计成多久？”，如果每次执行太短，那么进程上下文切换就占用了太多时间。如果每次执行太久，那么在短期内的公平性就会降低的非常离谱。所以这个参数设定了每次执行的窗口，也就是过多久操作系统会通过定时中断来执行上述的调度逻辑选取其他进程执行，在有多个cpu的情况这个值会除以CPU数，一般在linux上这个值是48ms&lt;/li&gt;
&lt;li&gt;min_granularity：这个参数描述了最小执行时间，因为只依赖上述参数，在过多时，例如cpu&amp;gt;48（极端情况），会将调度延迟改为1ms及以下，这个参数在单cpu上就失去了意义，所以也为调度延迟设置了下限，即实际一般min_granularity&amp;lt;调度延迟&amp;lt;sched_latency，这个值在linux一般是6ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，CFS使用时钟中断来获取或者说夺回cpu控制权，也就是说可以认为上面那些参数组合计算出的time_slice就是CFS时钟中断的间隔&lt;/p&gt;
&lt;h3&gt;权重&lt;/h3&gt;
&lt;p&gt;CFS除了相对公平的默认实现意外，还支持权重的配置，默认进程的权重为0，权重参与计算的值weight由一个数组维护&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static const int prio_to_weight[40] = {
/*-20*/ 88761, 71755, 56483, 46273, 36291,
/*-15*/ 29154, 23254, 18705, 14949, 11916,
/*-10*/  9548,  7620,  6100,  4904,  3906,
/*-5*/  3121,  2501,  1991,  1586,  1277,
/*0*/  1024,   820,   655,   526,   423,
/*5*/   335,   272,   215,   172,   137,
/*10*/   110,    87,    70,    56,    45,
/*15*/    36,    29,    23,    18,    15,};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;权重影响实际时间片的公式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/ff29315aa2ee0200b447a26278a84c37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看原文看了半天一直没有理解分式下面的weight i累加到底是什么意思，查了下其他人写的关于CFS的文章，下面实际是系统内所有进程权重的累加值，比如原文中说的A的权重是-5，对应的就是3121，B的权重是0，对应的1024，这个比例对A来说就是: $\cfrac  {3121}{3121+1024}≈\cfrac  {3}{4}$，B就是$\cfrac  {1}{4}$。&lt;/p&gt;
&lt;p&gt;除了该进程根据权重时的时间片需要更换计算方式以外，虚拟执行时间的累加也要根据权重计算，才能保证整体平衡。虚拟执行时间的累加公式如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/03/a6d32f16cda26ffc03024f69c39e69a5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;简而言之，就是根据原有应该的执行时间累加值*$\cfrac  {1024}{该进程的权重weight}$，比如对A来说，这个累加值就是大约$\cfrac  {1024}{3121}≈\cfrac  {1}{3}$&lt;/p&gt;
&lt;h3&gt;红黑树&lt;/h3&gt;
&lt;p&gt;这章还简单介绍了下红黑树，CFS因为每次调度周期要挑选出vruntime最小的进行执行，所以对有序集合的性能要求较高，红黑树作为平衡二叉树在查询和插入的综合性能上可以认为是最适合这个场景的数据结构了，不过需要注意的是，CFS的红黑树中只包含了那些running状态的进程，也就是那些正在运行的，因为中断或者sleep进入ready或者waiting&lt;/p&gt;
&lt;h3&gt;I/O以及sleep的处理&lt;/h3&gt;
&lt;p&gt;CFS将&lt;/p&gt;
&lt;p&gt;对于那些因为io或者中断进入sleep的进程，操作系统在他们被唤醒时会采取当前队列中最低的vruntime作为他们的新vruntime再把他们加回那个维护running状态的红黑树，来保证尽可能补偿回他们因为sleep而失去的cpu时间。这样做有好有坏，虽然这样避免了那些经常sleep的进程使得其他进程饿死（因为如果某进程sleep10s，那么当该进程被唤醒后，很可能他将独占10s的cpu），但是一定程度破坏了公平性&lt;/p&gt;
&lt;h2&gt;Others&lt;/h2&gt;
&lt;p&gt;还有一些中间看时遇到的问题，其中一个对于公式的不理解上面已经写过了，还有一些理解自己查阅资料以及思考后，觉得比较有价值的问题和结论&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q：书中最后提到了，CFS被Linux和大部分datacenter的操作系统采用作为基础的调度算法，而上一张介绍的MLFQ被BSD Unix，Windows（查一下的话发现还有Mac）作为基本的调度算法，为什么呢？&lt;/p&gt;
&lt;p&gt;A：&lt;/p&gt;
&lt;p&gt;​	对比下文中不算强调但是有这方面描述的两个算法的特点，以及两个算法的特性来看，其实这个问题不难得到答案。MLFQ可以通过对进入I/O等响应用户交互的进程通过提高优先队列级别等方式来单独保证用户交互的response_time（比如提到过的一个对于那些经常主动放弃CPU的进程，不降低他们的优先队列级别的策略），所以MLFQ大多在具备图形交互等操作系统中作为基本的逻辑，这也是为什么Windows和Mac都是基于这个调度。&lt;/p&gt;
&lt;p&gt;​	而CFS则集中在CPU使用的公平性，在Linux这种主要以终端操作为主（用户界面交互较弱），或者datacenter这种以计算为主的背景下，更在意的就是计算任务的公平性和CPU时间分配了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;作为操作系统调度任务的核心算法，MLFQ和CFS都兼具简单和强大两个优点。&lt;/p&gt;
&lt;p&gt;Simplicity is prerequisite for reliability&lt;/p&gt;
&lt;p&gt;​															-&lt;strong&gt;Edsger Dijkstra&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>ostep学习笔记-8 Scheduing</title><link>https://www.intotw.cn/posts/ostep-8-scheduing/</link><guid isPermaLink="true">https://www.intotw.cn/posts/ostep-8-scheduing/</guid><pubDate>Mon, 11 Mar 2024 07:24:01 GMT</pubDate><content:encoded>&lt;p&gt;从第八章开始，写一下学习笔记，因为从这张开始深入到一些算法以及原理了，最好还是写笔记记录一下&lt;/p&gt;
&lt;p&gt;这张主要通过一系列问题的讨论，介绍并优化了MLFQ，Multi-Level Feedback Queue（多级反馈队列）这一操作系统调度任务的算法。&lt;/p&gt;
&lt;p&gt;这个算法主要逻辑（讨论后最终给出）如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rule 1:If Priority(A)&amp;gt;Priority(B), A runs (B doesn’t).如果任务A的优先级大于B，将先运行A。&lt;/li&gt;
&lt;li&gt;Rule 2:If Priority(A)=Priority(B), A &amp;amp; B run in round-robin fash-ion using the time slice (quantum length) of the given queue.如果A的优先级大于B，将在AB之间使用轮询算法轮流执行。&lt;/li&gt;
&lt;li&gt;Rule 3:When a job enters the system, it is placed at the highestpriority (the topmost queue).当任务进入系统时，将被放在优先级最高级的队列中&lt;/li&gt;
&lt;li&gt;Rule 4:Once a job uses up its time allotment at a given level (re-gardless of how many times it has given up the CPU), its priority isreduced (i.e., it moves down one queue).当一个任务使用完在当前队列所分配给他的时间片之后，降低他的优先级，也就是移动到下一优先级队列中去&lt;/li&gt;
&lt;li&gt;Rule 5:After some time period S, move all the jobs in the system to the topmost queue.在给定的S时间间隔以后，把所有任务移动到最高级队列中去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中123显而易见可以作为基本的规则，4是为了解决在高优先级使用一些把戏去抢占cpu（比如在最初的设计中，采用的是如果任务发生了I/O或者放弃CPU的操作，就不会对优先级进行降级，而是保存在原队列中，以至于低优先级任务永远得不到执行的问题。5是为了在不断有任务到达，或者某个CPU高占用的任务过一段时间后转换成了I/O多的任务时，给低优先级任务一个被重新规划队列的机会。&lt;/p&gt;
&lt;p&gt;针对这个队列，不同操作系统细节的实现以及不同的调参影响了实际的性能，一般来说，以下几个参数比较常用&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优先队列的数量，这个指的是优先队列一共有多少级，比如说一般是60级优先队列&lt;/li&gt;
&lt;li&gt;优先队列的时间片分配，即在该级别队列中，单个任务分配到多少时间片执行，一般来说优先级越高的队列越短，优先级越低的队列越长。比如在最高级队列中可能是10ms一个时间片去执行任务，在最低级队列中可能是100ms一个时间片去执行&lt;/li&gt;
&lt;li&gt;优先队列的升级周期s，就是过多久，所有任务统一上升到最高级队列，也就是5中的概念，一般是1秒&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里补充下这个模型的理解，队列中的任务其实是进程，假设ABC在最顶级队列p0，那么操作系统就会按照ABC轮询的顺序，执行A10ms后中断切换上下文去执行B，执行B10ms后继续C。所以我们可以看到如果规则4不这么设计，那么ABC可以无限执行直到ABC都执行完。所以再加上规则4后，ABC执行一次后就移动到了下一等级的队列p1，下一等级的队列如果时间窗口是20ms，那么就会在执行完下一队列p1内原有的任务后再执行ABC，然后没执行完的话大家继续降级。等待s秒一次的重启。&lt;/p&gt;
</content:encoded></item><item><title>阅读笔记-技术</title><link>https://www.intotw.cn/posts/read-note/</link><guid isPermaLink="true">https://www.intotw.cn/posts/read-note/</guid><pubDate>Fri, 19 Jan 2024 09:50:46 GMT</pubDate><content:encoded>&lt;h2&gt;无中断升级 1200 多台 MySQL 服务器，GitHub 是如何做到的&lt;/h2&gt;
&lt;p&gt;https://www.infoq.cn/article/BH3tpVK2Ayl7W53QFGFW&lt;/p&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vitess ，MySql数据库集群水平分片管理&lt;/li&gt;
&lt;li&gt;freno，Mysql写节流器服务&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>事记-202312</title><link>https://www.intotw.cn/posts/trace-2023-12/</link><guid isPermaLink="true">https://www.intotw.cn/posts/trace-2023-12/</guid><pubDate>Wed, 10 Jan 2024 08:44:14 GMT</pubDate><content:encoded>&lt;h2&gt;Travel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;其实24年1月才写的。。。实在好久没记录了&lt;/li&gt;
&lt;li&gt;猫咪被喂养的越来越胖了，5个月不到已经是一只大猫咪了&lt;/li&gt;
&lt;li&gt;11月底得了猫藓，带伊丽莎白圈带了1个月，戴上了倒是老实了很多，但是结果就是猫藓好差不多了以后一摘下，直接开始发疯了&lt;/li&gt;
&lt;li&gt;在被辉哥带着去了陶喆的音乐节以后，12月去了张韶涵演唱会、跨年前一天去了梁静茹演唱会，因为买的是最贵的票，梁静茹的演唱会离得很近，梁静茹恢复的也很不错，终于有些值得开心的事了，就是听歌&lt;/li&gt;
&lt;li&gt;其实初中高中很喜欢听梁静茹的，基本算是如数家珍，最近又发现三首以前没听过的宝藏歌曲，《一夜长大》，《接受》，《第三者》&lt;/li&gt;
&lt;li&gt;仔细回想23年发生了很多事，结婚，养猫，演唱会，经济也下行了一年，年底公司也开始比较多的裁员&lt;/li&gt;
&lt;li&gt;虽然不太可能，但是希望后面经济会越来越好吧&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Society&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;经济下行了一年，失业率，裁员哀鸿遍野&lt;/li&gt;
&lt;li&gt;大A终于俯冲到了2800的水平&lt;/li&gt;
&lt;li&gt;五月天大摆锤假唱&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>曼昆《微观经济学原理》笔记（读后感）</title><link>https://www.intotw.cn/posts/micro-economics-after-reading/</link><guid isPermaLink="true">https://www.intotw.cn/posts/micro-economics-after-reading/</guid><pubDate>Mon, 13 Nov 2023 08:06:36 GMT</pubDate><content:encoded>&lt;h2&gt;第一章 经济学十大原理&lt;/h2&gt;
&lt;h3&gt;原理一：人们面临均衡取舍&lt;/h3&gt;
&lt;p&gt;人们在选择时，总是面临均衡取舍，这里主要反映在（从目前的看到第三章为止）经济学中的绝对优势和比较优势，以及机会成本这个概念，也是因为均衡取舍，才会有交易贸易&lt;/p&gt;
&lt;h3&gt;原理二：某种东西的成本是为了得到它而放弃的东西&lt;/h3&gt;
&lt;p&gt;这点主要阐述在后续的两个观点，也就是比较优势和机会成本。这个原理是一个在经济学特殊的思考角度，也就是成本除了从你所付出的**生产成本（时间、生产资源）**之外，为了这个东西所放弃的成本也是这个东西的成本。&lt;/p&gt;
&lt;p&gt;在现实中，精明的我们可能也会凭借隐约的直觉用这个原理做比较：比如一些理智的人会综合考虑996对身体健康以及其他时间成本的影响、也会考虑去听一场音乐会所花费的其实是门票钱+兼职1小时的收入，而不单单是门票钱&lt;/p&gt;
&lt;h3&gt;原理三：理性人考虑边际量&lt;/h3&gt;
&lt;p&gt;从原理二中我举得例子来看，边缘收益我们很少考虑，在996的情况下，我们的健康和收入的边际量在哪？尽管这种情况下的边际量很难衡量（我们最多从一些加班时间以及看病所花费的金钱来考虑，但是样本太少）。&lt;/p&gt;
&lt;p&gt;音乐会的例子也是如此，钱是挣不完的，兼职1小时和兼职10小时到兼职16小时，我们也应该思考其中的边际收益以及边际成本，当收入足够生活时，无止尽的劳动带来的边际收益相比起身体健康的边际成本，边际效益微乎其微&lt;/p&gt;
&lt;h3&gt;原理四：人们会对激励做出反应&lt;/h3&gt;
&lt;p&gt;这个原理整体效应和一类似，因为人们面临均衡取舍，所以人们会对经济上的激励以及变化敏感并做出反应，当你提高石油价格时，大部分人会减少对石油的使用，反之亦然&lt;/p&gt;
&lt;h3&gt;原理五：贸易可以使每个人变得更好&lt;/h3&gt;
&lt;p&gt;这个在这里不多描述，基于分工+贸易，每个人可以提升自己的生产效益，进而提高自己的&lt;strong&gt;收入&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;原理六：市场通常是组织经济活动的一种好方法&lt;/h3&gt;
&lt;h3&gt;原理七：政府有时可以改善市场结果&lt;/h3&gt;
&lt;h3&gt;原理八：一国的生活水平取决于它生产物品和服务的能力&lt;/h3&gt;
&lt;h3&gt;原理九：当政府发行了过多货币时，物价上升&lt;/h3&gt;
&lt;h3&gt;原理十：社会面临通货膨胀与失业之间的短期均衡取舍&lt;/h3&gt;
&lt;h2&gt;第二章  像经济学家一样思考&lt;/h2&gt;
&lt;h3&gt;经济模型&lt;/h3&gt;
&lt;p&gt;这章主要描述了，经济学家的简单经济模型，一个企业与家庭关系的模型：循环流量图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/9d70d5535eaaace86caa95db0e288041.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;生产可能性边界：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/5967b559f17504ad0ba3c923fb216c49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;循环流量模型简单概括了生产以及消费在家庭以及企业之间的流转关系&lt;/p&gt;
&lt;p&gt;生产可能性边界主要描述了生产的边际效益关系，用来推导和展开后面的比较优势以及机会成本&lt;/p&gt;
&lt;h3&gt;经济学家&lt;/h3&gt;
&lt;p&gt;经济学家作为政策顾问，总是难以做下结论，因为一部分得到必须会失去另一部分，所以经济学家的观点总是很难统一，但是他们在经济学原理上还是倾向于统一的。&lt;/p&gt;
&lt;p&gt;在具体政策上，除了经济学原理外，还要考虑政治，经济，宗教等影响，所以经济学家总是会有分歧、主要在科学判断以及价值观的不同&lt;/p&gt;
&lt;h2&gt;第三章 相互依存性以及贸易的好处&lt;/h2&gt;
&lt;p&gt;这章主要通过一个例子，解释了贸易如何能使每个人变得更好，并且引入了绝对优势、机会成本、比较优势等概念&lt;/p&gt;
&lt;p&gt;假设有一个小土和小牛，他们都可以生产土豆和牛肉&lt;/p&gt;
&lt;p&gt;如果小土1小时成产4单位土豆或1单位牛肉，而小牛1小时成产6单位土豆或3单位牛肉&lt;/p&gt;
&lt;p&gt;在考虑双方该如何交易时，其实并不是简单的考虑小牛在两个产品上都具有&lt;strong&gt;绝对优势&lt;/strong&gt;，但是此时要使用&lt;strong&gt;比较优势&lt;/strong&gt;来考虑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d40fbd89789a73c4b21f5f5186c11fce.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/b7a2cbad6753301c22a612ee41f1cbe1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举一个更通俗的例子，一个篮球运动员可以花费1小时把自己草坪清理干净，但是也可以使用1小时去签个名赚1W元，那么对他来说，花费100元雇佣一个人使用4小时清理干净草坪，而自己去签名活动赚1W元，明显好过自己1小时把草坪清理干净&lt;/p&gt;
&lt;p&gt;简而言之，基于比较优势的思考逻辑是：假如A与B具备同样生产某种东西的能力，衡量A与B是否该去生产该东西时，应该与A与B生产其他东西的成本去做比较，来思考A与B该怎样去生产&lt;/p&gt;
&lt;p&gt;这章通过多个图，还可以收获到一个简单的结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;柱状图用来比较多个不同实体间同一个指标的差异&lt;/li&gt;
&lt;li&gt;饼状图用来突出不同比例的占比&lt;/li&gt;
&lt;li&gt;点图（折线图）用来突出变化的趋势&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文里大多数使用的也是这3种图。（经济学的父亲应该是统计学）。&lt;/p&gt;
</content:encoded></item><item><title>听听歌吧-王菲《红豆》</title><link>https://www.intotw.cn/posts/recommend-song-01/</link><guid isPermaLink="true">https://www.intotw.cn/posts/recommend-song-01/</guid><pubDate>Thu, 09 Nov 2023 05:54:37 GMT</pubDate><content:encoded>&lt;p&gt;这首歌和孙燕姿的《遇见》一样，听着就会有冬天的感觉&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;有时候　有时候&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我会相信一切有尽头&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;////player.bilibili.com/player.html?isOutside=true&amp;amp;aid=399351525&amp;amp;bvid=BV1zo4y1u7Ts&amp;amp;cid=1148444908&amp;amp;p=1&amp;amp;autoplay=0&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;width:100%;aspect-ratio:16/9;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;还没好好地感受&lt;/p&gt;
&lt;p&gt;雪花绽放的气候&lt;/p&gt;
&lt;p&gt;我们一起颤抖&lt;/p&gt;
&lt;p&gt;会更明白什么是温柔&lt;/p&gt;
&lt;p&gt;还没跟你牵着手&lt;/p&gt;
&lt;p&gt;走过荒芜的沙丘&lt;/p&gt;
&lt;p&gt;可能从此以后&lt;/p&gt;
&lt;p&gt;学会珍惜天长和地久&lt;/p&gt;
&lt;p&gt;有时候　有时候&lt;/p&gt;
&lt;p&gt;我会相信一切有尽头&lt;/p&gt;
&lt;p&gt;相聚离开都有时候&lt;/p&gt;
&lt;p&gt;没有什么会永垂不朽&lt;/p&gt;
&lt;p&gt;可是我　有时候&lt;/p&gt;
&lt;p&gt;宁愿选择留恋不放手&lt;/p&gt;
&lt;p&gt;等到风景都看透&lt;/p&gt;
&lt;p&gt;也许你会陪我看细水长流&lt;/p&gt;
&lt;p&gt;还没为你把红豆&lt;/p&gt;
&lt;p&gt;熬成缠绵的伤口&lt;/p&gt;
&lt;p&gt;然后一起分享&lt;/p&gt;
&lt;p&gt;会更明白相思的哀愁&lt;/p&gt;
&lt;p&gt;还没好好地感受&lt;/p&gt;
&lt;p&gt;醒着亲吻的温柔&lt;/p&gt;
&lt;p&gt;可能在我左右&lt;/p&gt;
&lt;p&gt;你才追求孤独的自由&lt;/p&gt;
&lt;p&gt;有时候　有时候&lt;/p&gt;
&lt;p&gt;我会相信一切有尽头&lt;/p&gt;
&lt;p&gt;相聚离开都有时候&lt;/p&gt;
&lt;p&gt;没有什么会永垂不朽&lt;/p&gt;
&lt;p&gt;可是我　有时候&lt;/p&gt;
&lt;p&gt;宁愿选择留恋不放手&lt;/p&gt;
&lt;p&gt;等到风景都看透&lt;/p&gt;
&lt;p&gt;也许你会陪我看细水长流&lt;/p&gt;
&lt;p&gt;有时候　有时候&lt;/p&gt;
&lt;p&gt;我会相信一切有尽头&lt;/p&gt;
&lt;p&gt;相聚离开都有时候&lt;/p&gt;
&lt;p&gt;没有什么会永垂不朽&lt;/p&gt;
&lt;p&gt;可是我　有时候&lt;/p&gt;
&lt;p&gt;宁愿选择留恋不放手&lt;/p&gt;
&lt;p&gt;等到风景都看透&lt;/p&gt;
&lt;p&gt;也许你会陪我看细水长流&lt;/p&gt;
</content:encoded></item><item><title>听听歌吧-Rie fu《life is like a boat》</title><link>https://www.intotw.cn/posts/recommend-song-02/</link><guid isPermaLink="true">https://www.intotw.cn/posts/recommend-song-02/</guid><pubDate>Thu, 09 Nov 2023 05:54:37 GMT</pubDate><content:encoded>&lt;p&gt;这首歌听着听着，随着节奏的递进，会有度过一生的感觉，以及一种对人生的释怀，让人感觉到：生之如舟&lt;/p&gt;
&lt;p&gt;中间这段旋律的变化和情绪的递进更是让人觉得尽管生之如舟，但是在旅途上也有精彩&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;And every time I see your face,&lt;/p&gt;
&lt;p&gt;the oceans heave up to my heart&lt;/p&gt;
&lt;p&gt;You make me wanna strain at the oars,&lt;/p&gt;
&lt;p&gt;and soon I can see the shore&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=672157950&amp;amp;bvid=BV1AU4y1a7w8&amp;amp;cid=310647896&amp;amp;p=1&amp;amp;autoplay=0&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;width:100%;aspect-ratio:16/9;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Nobody knows who I really am&lt;/p&gt;
&lt;p&gt;I never felt this empty before&lt;/p&gt;
&lt;p&gt;And if I ever need someone to come along&lt;/p&gt;
&lt;p&gt;Who&apos;s gonna comfort me, and keep me strong&lt;/p&gt;
&lt;p&gt;We are all rowing the boat of fate&lt;/p&gt;
&lt;p&gt;The waves keep on coming and we can&apos;t escape&lt;/p&gt;
&lt;p&gt;But if we ever get lost on our way&lt;/p&gt;
&lt;p&gt;The waves would guide you through another day&lt;/p&gt;
&lt;p&gt;远くで息をしてる　透明になったみたい&lt;/p&gt;
&lt;p&gt;暗闇に思えたけど　目隠しされてただけ&lt;/p&gt;
&lt;p&gt;祈りをささげて　新しい日を待つ&lt;/p&gt;
&lt;p&gt;鲜やかに　光る海　その果てまで&lt;/p&gt;
&lt;p&gt;Nobody knows who I really am&lt;/p&gt;
&lt;p&gt;Maybe they just don&apos;t give a damn&lt;/p&gt;
&lt;p&gt;But if I ever need someone to come along&lt;/p&gt;
&lt;p&gt;I know you would follow me, and keep me strong&lt;/p&gt;
&lt;p&gt;人の心はうつりゆく　抜け出したくなる&lt;/p&gt;
&lt;p&gt;つきはまた新しい周期で　舟を连れてく&lt;/p&gt;
&lt;p&gt;And every time I see your face,&lt;/p&gt;
&lt;p&gt;the oceans heave up to my heart&lt;/p&gt;
&lt;p&gt;You make me wanna strain at the oars,&lt;/p&gt;
&lt;p&gt;and soon I can see the shore&lt;/p&gt;
&lt;p&gt;Ah，I can&apos;t see the shore&lt;/p&gt;
&lt;p&gt;When will l see the shore&lt;/p&gt;
&lt;p&gt;I want you to know who I really am&lt;/p&gt;
&lt;p&gt;I never thought I&apos;d feel this way towards you&lt;/p&gt;
&lt;p&gt;And if you ever need someone to come along&lt;/p&gt;
&lt;p&gt;I will follow you, and keep you strong&lt;/p&gt;
&lt;p&gt;旅はまだ続いてく　穏やかな日も&lt;/p&gt;
&lt;p&gt;つきはまた新しい周期で　舟を照らし出す&lt;/p&gt;
&lt;p&gt;祈りをささげて　新しい日を待つ&lt;/p&gt;
&lt;p&gt;鲜やかに　光る海　その果てまで&lt;/p&gt;
&lt;p&gt;And every time I see your face,&lt;/p&gt;
&lt;p&gt;the oceans heave up to my heart&lt;/p&gt;
&lt;p&gt;You make me wanna strain at the oars,&lt;/p&gt;
&lt;p&gt;and soon I can see the shore&lt;/p&gt;
&lt;p&gt;运命の船を漕ぎ&lt;/p&gt;
&lt;p&gt;波は次から次へと私たちを袭うけど&lt;/p&gt;
&lt;p&gt;それも素敌な旅ね　どれも素敌な旅ね&lt;/p&gt;
</content:encoded></item><item><title>听听歌吧-沸点乐队（E小姐）《生命的云彩》</title><link>https://www.intotw.cn/posts/recommend-song-03/</link><guid isPermaLink="true">https://www.intotw.cn/posts/recommend-song-03/</guid><pubDate>Thu, 09 Nov 2023 05:54:37 GMT</pubDate><content:encoded>&lt;p&gt;这首歌有说法的，小时候看的动画片的主题曲，既抓耳，又听不厌，主唱的声音堪称一绝，那么小又那么能唱，可惜只有这么一个作品&lt;/p&gt;
&lt;p&gt;找这个主唱找了快10年了，一直想听她别的歌（好的声音就是这么令人心动），终于在BV1La4y1L73j看到了主唱是谁，@emanlee1004&lt;/p&gt;
&lt;p&gt;可惜主唱已经不怎么唱歌了&lt;/p&gt;
&lt;p&gt;有的时候唱歌这个东西还真的是天生的，这一首歌的音色和唱商，我觉得足以吊打很多歌手了&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;iframe src=&quot;//player.bilibili.com/player.html?isOutside=true&amp;amp;aid=29666291&amp;amp;bvid=BV1UW411X7u4&amp;amp;cid=191799669&amp;amp;p=1&amp;amp;autoplay=0&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; style=&quot;width:100%;aspect-ratio:16/9;&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;因为理想我向往&lt;/p&gt;
&lt;p&gt;生命的云彩&lt;/p&gt;
&lt;p&gt;在广阔的宇宙中&lt;/p&gt;
&lt;p&gt;一直是童话里的节拍&lt;/p&gt;
&lt;p&gt;幻想天边的大海&lt;/p&gt;
&lt;p&gt;演绎生命的舞台&lt;/p&gt;
&lt;p&gt;行星在身边转来转去&lt;/p&gt;
&lt;p&gt;只为了一个爱&lt;/p&gt;
&lt;p&gt;天使飞过的花园&lt;/p&gt;
&lt;p&gt;有一种神秘的色彩&lt;/p&gt;
&lt;p&gt;等到明天醒来后&lt;/p&gt;
&lt;p&gt;满园花朵会不会开&lt;/p&gt;
&lt;p&gt;暖风吹过的夏天&lt;/p&gt;
&lt;p&gt;吹不去梦中的等待&lt;/p&gt;
&lt;p&gt;童话王国里的精灵快快出来&lt;/p&gt;
&lt;p&gt;憨八龟我爱你&lt;/p&gt;
&lt;p&gt;爱你会有好心情&lt;/p&gt;
&lt;p&gt;把所有的快乐都给你&lt;/p&gt;
&lt;p&gt;无忧无虑我们在一起&lt;/p&gt;
&lt;p&gt;憨八龟 我爱你&lt;/p&gt;
&lt;p&gt;爱你会有好心情&lt;/p&gt;
&lt;p&gt;下一个季节同样美丽&lt;/p&gt;
&lt;p&gt;幸福就没有距离&lt;/p&gt;
&lt;p&gt;憨八龟 我爱你&lt;/p&gt;
&lt;p&gt;爱你会有好心情&lt;/p&gt;
&lt;p&gt;把所有的快乐都给你&lt;/p&gt;
&lt;p&gt;无忧无虑我们在一起&lt;/p&gt;
&lt;p&gt;憨八龟 我爱你&lt;/p&gt;
&lt;p&gt;爱你会有好心情&lt;/p&gt;
&lt;p&gt;下一个季节同样美丽&lt;/p&gt;
&lt;p&gt;幸福就没有距离。&lt;/p&gt;
</content:encoded></item><item><title>事记-202311</title><link>https://www.intotw.cn/posts/trace-2023-11/</link><guid isPermaLink="true">https://www.intotw.cn/posts/trace-2023-11/</guid><pubDate>Mon, 06 Nov 2023 10:00:01 GMT</pubDate><content:encoded>&lt;h2&gt;Travel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;养了一直金渐层猫咪，从卖家哪里买来的时候就有点球形菌，回来一直软便，益生菌调养两周终于差不多好了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;但是猫咪被我老婆过渡喂养，过来胖了半斤，现在肚子大大的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;猫咪太治愈了，不会有人不喜欢小猫咪吧？不会吧&lt;/p&gt;
&lt;p&gt;{{&amp;lt; bilibili id=BV1Jj411Y7eH &amp;gt;}}&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>SRE服务端预案，应急处理手册</title><link>https://www.intotw.cn/posts/sre-emergency/</link><guid isPermaLink="true">https://www.intotw.cn/posts/sre-emergency/</guid><pubDate>Mon, 06 Nov 2023 06:53:50 GMT</pubDate><content:encoded>&lt;h1&gt;服务端应急处理流程&lt;/h1&gt;
&lt;h2&gt;问题升级流程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/458aca8bd2a6152e6465c6039aff56fa.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题升级步骤&lt;/h2&gt;
&lt;p&gt;SRE人员-各端组长-业务线负责人&lt;/p&gt;
&lt;h1&gt;现有降级手段&lt;/h1&gt;
&lt;h2&gt;App业务入口降级&lt;/h2&gt;
&lt;h3&gt;降级范围以及作用域&lt;/h3&gt;
&lt;p&gt;使用App降级策略，App在各个业务入口会直接降级，关闭对应的业务入口&lt;/p&gt;
&lt;h3&gt;使用场景&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对应业务出现会&lt;strong&gt;持续扩大损失并且短期无法修复的报错，比如应用持续出现异常，并且异常会导致越来越多的脏数据影响业务流程&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;应用&lt;strong&gt;无法正常提供服务，并且确认无法短期内恢复。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;AHAS限流降级&lt;/h2&gt;
&lt;h3&gt;降级范围以及作用域&lt;/h3&gt;
&lt;p&gt;通过AHAS的限流能力，对核心应用接入AHAS，具备对指定接口做限流降级的能力。&lt;/p&gt;
&lt;h3&gt;使用场景&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;特定业务出现过量的访问或请求，在扩容之前，先使用AHAS进行限流保证已有业务不被打挂。&lt;/li&gt;
&lt;li&gt;服务端出现&lt;strong&gt;短期内无法恢复的基础设施异常&lt;/strong&gt;，使用AHAS进行限流降级，保证友好的返回。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;应急预案SOP&lt;/h1&gt;
&lt;h2&gt;MySQL&lt;/h2&gt;
&lt;h3&gt;目前C端现有资源&lt;/h3&gt;
&lt;p&gt;rds_*************等&lt;/p&gt;
&lt;h3&gt;表变更&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;阿里云DMS审核表变更&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;p&gt;通知大数据，同步表待大数据确认变更的内容对他们的影响后，再执行。&lt;/p&gt;
&lt;h3&gt;长事务慢会话告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;云监控-数据库层群出现C端实例的慢会话或者长事务告警&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d618d059470285c89eb3283e66403159.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;p&gt;登录das查看对应数据库实例状况，分析具体原因&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/dc3b507fc5f431a8d23182fe30171c40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果实例会话中有异常会话，联系DBA，backup，帮忙杀死异常的会话&lt;/li&gt;
&lt;li&gt;如果请求分析中慢日志某系统出现大量慢sql，视慢sql增长量，对该系统进行扩容处理&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MySQL连接获取慢&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/fdd04a2c8303575672e9faa9dad4f8eb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;应用arms出现慢接口告警，并且链路追踪中耗时较长的步骤是druid或者hikari的getConnect等方法&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;对应用进行扩容，按依次按顺序找SRE-&amp;gt;组长-&amp;gt;运维，其中的一个完成&lt;/li&gt;
&lt;li&gt;观察慢接口数量是否下降，告警是否消失&lt;/li&gt;
&lt;li&gt;调整应用数据库连接池配置，重新发布应用&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Kafka&lt;/h2&gt;
&lt;h3&gt;目前现有资源&lt;/h3&gt;
&lt;p&gt;kafka_********&lt;/p&gt;
&lt;h3&gt;kafka堆积&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/66b4b931b12ae7c9834ccf4f1dd7baa3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;登录阿里云kafka监控查看堆积的消费者组的情况&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/70adb377aad7576b227943c9c350db0b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;刷新查看堆积量的增长情况，如果堆积量逐渐减少，那么可能只是突增流量和业务导致的，可以继续观察，如果没有明显减少的情况，则对比分区以及机器数，如果分区&amp;gt;机器数，则扩容机器数到=分区数，如果分区&amp;lt;=机器数，则联系运维组扩分区+应用扩容，扩容后继续观察堆积情况。&lt;/p&gt;
&lt;h2&gt;Redis&lt;/h2&gt;
&lt;h3&gt;目前现有资源&lt;/h3&gt;
&lt;p&gt;redis_*******&lt;/p&gt;
&lt;h3&gt;Redis告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/1fc88ee49d9bf514831b19cb3740cd95.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;登录DAS查看情况，先看实时趋势，辨别一下是短期增长后回落还是还在持续增长。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/8123f5cb8f2d1ec662983990d43075dd.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;主要查看内存和cpu使用率，一般是这两个告警&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d591c3fa99a59f13bb29ed1ec4da844f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;依靠实例会话里慢日志和缓存分析，查看导致异常的应用或者会话或者业务，如果对redis整体的影响增加&lt;strong&gt;很大，那么需要对对应的业务做降级，如果影响不大，那么则马上安排优化该业务&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;服务&lt;/h2&gt;
&lt;h3&gt;目前C端现有资源&lt;/h3&gt;
&lt;p&gt;all&lt;/p&gt;
&lt;h3&gt;慢接口告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/0d5bb9881fb439acd0486f8d91635631.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;登录arms查看情况，对应应用-接口调用-调用链查询-按时间排序。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/aa99c3a0fd4dc7136e6edb56505ee7fb.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击traceId进入排查接口慢的原因，如果是下游慢，联系下游排查处理，如果是中间件（mysql或者redis）慢，看一下是否某个sql导致的&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;观察慢接口出现的频率是否持续上升，如果没有缓解并且短时间内无法解决，马上使用ahas对该接口进行限流降级。&lt;/p&gt;
&lt;h3&gt;Full gc告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/41af6785a18f1f9584e456cad82869f9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;登录arms查看情况，对应应用-应用详情-JVM监控。&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/4d4b329b781acccd135bd4468c168a0a.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;查看gc情况以及堆内存情况，如果只是单pod的fullgc次数越来越多但堆内的老年代内存没有明显回收释放，那么需要对有问题的pod进行手动重启。&lt;/li&gt;
&lt;li&gt;如果只是比较稳定的进行full gc并且老年代回收较为理想，但应用扔保持触发告警阈值的频率，那么可能只是单纯量上来了，需要对集群进行扩容。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;线程池告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d8199aba07ae15b2d218d8b2e6c6dbb3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;登录arms查看情况，对应应用-应用详情-JVM监控。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/4d7b854516aab46911ba91a69d1abf73.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看JVM监控中的线程数，是否有明显的尖刺。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看是单pod的问题，还是整个集群每个pod都有问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果单pod的问题，重启该pod。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果是整个集群的每个pod都有该情况，扩容集群，观察情况是否有缓解。&lt;/p&gt;
&lt;h3&gt;错误率告警&lt;/h3&gt;
&lt;h4&gt;现象&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/c01c72f9c304596abd2120cc7be43fc9.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;动作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;点击链接进入sls，查看对应服务的error日志&lt;/li&gt;
&lt;li&gt;具体情况具体分析，如果对系统影响较小，或者可以业务上进行修复恢复，那么就业务修复处理。&lt;/li&gt;
&lt;li&gt;如果对业务影响较大，并且会持续出现脏数据或者报错，那么对业务进行降级&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>SRE-基于阿里云的告警体系建设</title><link>https://www.intotw.cn/posts/sre-monitor/</link><guid isPermaLink="true">https://www.intotw.cn/posts/sre-monitor/</guid><pubDate>Mon, 06 Nov 2023 06:53:50 GMT</pubDate><content:encoded>&lt;p&gt;基于数据源来做分类&lt;/p&gt;
&lt;h2&gt;sls日志告警&lt;/h2&gt;
&lt;h3&gt;配置以及查看方式&lt;/h3&gt;
&lt;p&gt;sls日志左侧点击铃铛进入告警中心配置&lt;/p&gt;
&lt;p&gt;告警规则触发就是sls日志的查询语句，配置的规则时间内，查询语句查询的数量达到配置值，就会触发告警&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d59522ff5045e13102eb2b42dd1f1cd5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;现状&lt;/h3&gt;
&lt;p&gt;5XX告警&lt;/p&gt;
&lt;p&gt;应用error日志告警&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/a718e740ff0867c4477b408b279966df.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;云产品监控告警&lt;/h2&gt;
&lt;h3&gt;配置以及查看方式&lt;/h3&gt;
&lt;p&gt;阿里云直接搜索云监控&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/b5a5a8c3101038b6499a18fdd7c6db99.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;左边云产品监控，然后搜索要配置的云产品即可，比如redis，rds，kafka&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/a610f5f8cfd4d1212637c4fec1fc2768.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进去搜索到对应的实例，点击报警规则进去配置&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/e3d429935624acc9cff413d5c25c1f24.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;现状&lt;/h3&gt;
&lt;p&gt;redis命中率，cpu等监控&lt;/p&gt;
&lt;p&gt;mysql内存等监控&lt;/p&gt;
&lt;p&gt;kafka堆积等监控&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/720b35aa8eac34f397cefab5970b3aa6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;arms监控告警&lt;/h2&gt;
&lt;h3&gt;配置以及查看方式&lt;/h3&gt;
&lt;p&gt;arms-应用监控-应用监控告警规则&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/c6293c50bba69e771a2c146a87575d78.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;现状&lt;/h3&gt;
&lt;p&gt;pod的fullgc，内存，以及应用的接口环比，慢接口等指标&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/023c82b503fb62e8acc6752bc091a6d0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;xxl-job告警&lt;/h2&gt;
&lt;h3&gt;配置以及查看方式&lt;/h3&gt;
&lt;p&gt;xxl-job管理台配置任务时选择告警组即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/d52cc31b810dbe77f7c4fad1e479d485.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;现状&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/11/b30fd53727e33672042df96f954c4eb5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>SRE-描述文档</title><link>https://www.intotw.cn/posts/sre-doc/</link><guid isPermaLink="true">https://www.intotw.cn/posts/sre-doc/</guid><pubDate>Mon, 06 Nov 2023 06:42:39 GMT</pubDate><content:encoded>&lt;h2&gt;SRE是什么&lt;/h2&gt;
&lt;p&gt;SRE（Site Reliability Engineering）即网站可靠性工程，以软件工程的方法论重新定义研发运维，驱动并赋能业务演进。&lt;/p&gt;
&lt;h2&gt;SRE的职责&lt;/h2&gt;
&lt;p&gt;SRE主要负责所有核心业务系统的可用性、性能、容量相关的事情，根据《Site Reliability Engineering 》一书提及的内容，笔者做简单汇总，SRE的工作主要包括但不限于如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基础设施容量规划&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生产系统的监控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生产系统的负载均衡&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发布与变更工程管理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;on-call（轮值） 与 Firefighting（紧急故障救火）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;与业务团队协作，共同完成疑难问题的处理&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;要做哪些事情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;基础组件能力提升&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能优化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据库稳定性治理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监控预警治理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;故障诊断与恢复&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变更流程规范&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;on-call轮值&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;稳定性日报&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;这些事情怎么做&lt;/h2&gt;
&lt;h3&gt;基础组件能力提升&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;AHAS能力接入&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;整体上云&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;性能优化&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据库慢sql优化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;慢接口优化&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rediskey优化&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据库稳定性治理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;主从库读写分离&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;监控预警治理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;arms监控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;error日志监控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;云中间件监控&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;监控配置&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;故障诊断与恢复&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;全链路日志串联&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;rds的sql分析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prometheus监控指标&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;redis缓存分析&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;故障复盘&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;变更流程规范&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;线上变更钉钉群通知&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;dms多级审批&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;变更评审以及发布评审&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;on-call轮值&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;高峰期项目稳定性全天9:00-21:00点轮班&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SRE小组全天8:00-20:00轮班&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;稳定性日报&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;高峰期稳定性日报&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;异常怎么处理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;拉通相关人员，及时同步信息&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确认故障范围，减小故障规模&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写故障手册，规范处理步骤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参照异常处理手册&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>事记-202310</title><link>https://www.intotw.cn/posts/trace-2023-10/</link><guid isPermaLink="true">https://www.intotw.cn/posts/trace-2023-10/</guid><pubDate>Tue, 10 Oct 2023 10:00:01 GMT</pubDate><content:encoded>&lt;h2&gt;Travel&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;整个国庆回老家就发烧，烧退了的早上就回杭州了，整个咳嗽晚上经常咳嗽的睡不着，真是麻了&lt;/li&gt;
&lt;li&gt;国庆回来就连上7天，调NM的休&lt;/li&gt;
&lt;li&gt;要养一只小金渐层猫咪了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Society&lt;/h2&gt;
&lt;p&gt;很多经济博主比较多讨论以下问题，并输出观点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;养老金问题，目前已经收不抵支，预计2035年养老金会开始巨量亏空&lt;/li&gt;
&lt;li&gt;央行大量增加黄金库存，人民币进入“无锚时代”&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;许家印被抓&lt;/li&gt;
&lt;li&gt;碧桂园正式宣布违约&lt;/li&gt;
&lt;li&gt;中东土豪的基金投资了很多A股优质项目&lt;/li&gt;
&lt;li&gt;国内外油价倒挂严重&lt;/li&gt;
&lt;li&gt;20号，上证跌破3000点，和中东土豪投资A股优质项目好像又有点冲突，很神奇&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>2023年技术圈发生了什么</title><link>https://www.intotw.cn/posts/2023-tech-view/</link><guid isPermaLink="true">https://www.intotw.cn/posts/2023-tech-view/</guid><pubDate>Tue, 10 Oct 2023 09:55:06 GMT</pubDate><content:encoded>&lt;h2&gt;2023年技术圈发生了什么&lt;/h2&gt;
&lt;p&gt;https://www.infoq.cn/article/DUcAjpfd9ueWK9C1yOsN&lt;/p&gt;
</content:encoded></item><item><title>中国人缺少思想力量</title><link>https://www.intotw.cn/posts/cn-society/</link><guid isPermaLink="true">https://www.intotw.cn/posts/cn-society/</guid><pubDate>Mon, 25 Sep 2023 07:16:11 GMT</pubDate><content:encoded>&lt;h2&gt;主要矛盾&lt;/h2&gt;
&lt;p&gt;高中时在政治书上看到过一句类似的话，但是没有记忆了，只有大概的印象，内容为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;我国社会主要矛盾&lt;/em&gt;为逐渐丰富的物质发展以及落后的精神文明建设之间的矛盾&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在深以为然&lt;/p&gt;
&lt;h2&gt;中国人在统一的思想上的矛盾&lt;/h2&gt;
&lt;p&gt;从两千多年前儒家文化出现以来，中国并没有出现第二种&lt;strong&gt;文化&lt;/strong&gt;思想，马克思主义只有一个美好未来和一大堆方法论，对中国几千年建立起来的文化冲击却微乎其微，偏偏国内又没有走过西方启蒙运动的路子，早早就开始了闭关锁国，被外部势力压力破坏整体结构后，没多久又走上了老路子。八国联军带来了政治科技革命。在思想上，我们的革命却从来没有从底层开始，或者革命到底层过。&lt;/p&gt;
&lt;p&gt;所以，现在的社会上充斥这很多这个矛盾带来的一些表象&lt;/p&gt;
&lt;p&gt;我们整体的意识，并没有符合生产力的变化而进步，反而在一个充斥矛盾的阵痛期里挣扎，社会上各种矛盾日益对立突出。&lt;/p&gt;
&lt;p&gt;在启蒙运动后，欧美思想从真正的底层上更新到了“生而平等，生而自由”这个迭代，我们可以嘲讽“自由美利坚，枪战每一天”，也可以笑欧洲一年365天100天休假100天罢工，日本的“不给别人添麻烦”，这些虽然在我们看来有一点可笑，而且也并不一定正确的底层思想，但是却深深的是从其他民族国家下层传递上去的。&lt;/p&gt;
&lt;p&gt;如果要你说说我们民族现在的特性和思想，那么是什么？吃苦吗？&lt;/p&gt;
&lt;p&gt;比较可怕的是，我们发展到今日，仍然在儒家的那一套体系下，整个经济社会发展，虽然学习了外面的制度、方式、形态，但是本质上依旧在儒家的框架下繁衍。人情世故、亲情网络。这一张大网依旧作为底层架构在限制着我们&lt;/p&gt;
</content:encoded></item><item><title>事记-202309</title><link>https://www.intotw.cn/posts/trace-2023-09/</link><guid isPermaLink="true">https://www.intotw.cn/posts/trace-2023-09/</guid><pubDate>Thu, 21 Sep 2023 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;个人&lt;/h2&gt;
&lt;h3&gt;迁移了博客&lt;/h3&gt;
&lt;p&gt;博客从cnblog迁移到了自建的网址，hugo+cloudflarepage，之前对cnblog已经挺满意的了，但是不知道为什么总觉得不是自己的地方，所以写东西的动力很弱，有一种莫名的割裂感。最后只是偶尔同步下工作里的分享和记录过去。&lt;/p&gt;
&lt;p&gt;迁移来来回回折腾了2周，不得不吐槽前端还是博大精深，啥时候能统一下啊&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://images.intotw.tech/blog/2023/09/b82b97181725419c722558323bd6b0c5.jpg&quot; alt=&quot;图片描述&quot; style=&quot;width:300px;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;工作&lt;/h3&gt;
&lt;p&gt;9月的开始确定了今年78月份流量高峰期我们稳定性做到了000，但是绩效结果+PUA还是让人很不爽，另外就是经典套路谈完绩效马上团建给你打鸡血，嘴上说的都很好听但是实际做法都很下作的样子真的让人有点无语&lt;/p&gt;
&lt;h3&gt;思考&lt;/h3&gt;
&lt;p&gt;这个月看了不少什么物化，资本价值的视频，不由得感觉现在的&lt;strong&gt;人&lt;/strong&gt;的一生确实悲哀，大部分人从生下来就已经被定义，或者注定被定义了，这未尝不是一种赛博朋克&lt;/p&gt;
&lt;p&gt;活了快30年，最近才终于降低了精神内耗，以前总是没有安全感，一旦闲下来总想去学点东西（虽然并没有真去学什么），做的事情总想有意义，但是最近慢慢的终于结束了这种内耗，主要可能是当时看到了这么一段话，那一瞬间真的感觉忽然被触动了&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我慢慢明白了我为什么不快乐，因为我总是期待一个结果。&lt;/p&gt;
&lt;p&gt;看一本书期待它让我变深刻，&lt;/p&gt;
&lt;p&gt;吃饭游泳期待它让我一斤斤瘦下来，&lt;/p&gt;
&lt;p&gt;发一条短信期待它被回复，&lt;/p&gt;
&lt;p&gt;对人好期待它回应也好，&lt;/p&gt;
&lt;p&gt;写一个故事说一个心情期待它被关注被安慰，&lt;/p&gt;
&lt;p&gt;参加一个活动期待换来充实丰富的经历。&lt;/p&gt;
&lt;p&gt;这些预设的期待如果实现了，长舒一口气。&lt;/p&gt;
&lt;p&gt;如果没实现昵？自怨自艾。&lt;/p&gt;
&lt;p&gt;可是小时候也是同一个我，&lt;/p&gt;
&lt;p&gt;用一个下午的时间看蚂蚁搬家，等石头开花，&lt;/p&gt;
&lt;p&gt;小时候不期待结果，小时候哭笑都不打折。&lt;/p&gt;
&lt;p&gt;一一一马德《允许自己虚度时光》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;社会&lt;/h2&gt;
&lt;h3&gt;桀哥自爆+被锤&lt;/h3&gt;
&lt;p&gt;之前我在桀哥的直播间还被封过，直接就是一年，单纯只是别人都在复读我也跟着复读了下，但是就直接一年&lt;/p&gt;
&lt;p&gt;只能说活该，始末大概如下&lt;/p&gt;
&lt;p&gt;{{&amp;lt; bilibili BV1jw411v7kB &amp;gt;}}&lt;/p&gt;
&lt;p&gt;总结就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;桀哥要开电脑店，因为原来技术宅的人设，很多人还挺期待&lt;/li&gt;
&lt;li&gt;期待了几个月以后结果配置单出来懂哥们一看就肉眼可见的坑，事情翻转&lt;/li&gt;
&lt;li&gt;孝子们拿三圣器护，结果三圣器加上桀哥背景被扒，以前的一切人设都是吹牛，半步初中吹成中南大学毕业，去网易面试吹成去网易工作过&lt;/li&gt;
&lt;li&gt;然后就是各种节奏乱飞，目前桀哥从斗鱼前三已经跌到第十&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;周淑怡和PG ONE&lt;/h3&gt;
&lt;p&gt;周淑怡官宣和PGONE的恋情，粉丝们直接破防，最后周淑怡含泪道歉&lt;/p&gt;
&lt;h3&gt;李佳琦&lt;/h3&gt;
&lt;p&gt;李佳琦在花西子的直播带货中，有路人说79眉笔太贵，李佳琦直接一段暴论发言：&quot;不要乱说，&lt;em&gt;眉笔&lt;/em&gt;一直79，国货品牌很难的&quot;。接着他又用一种很不屑的语气说：&quot;有时候找找自己的原因，这么多年了工资涨没涨？有没有认真工作？&quot;&lt;/p&gt;
&lt;p&gt;只能说花西子也是倒了大霉，节奏刚好临头，&quot;国货品牌确实太难了&quot;&lt;/p&gt;
</content:encoded></item><item><title>互联网安全</title><link>https://www.intotw.cn/posts/web-security/</link><guid isPermaLink="true">https://www.intotw.cn/posts/web-security/</guid><pubDate>Wed, 20 Sep 2023 06:18:14 GMT</pubDate><content:encoded>&lt;h1&gt;通信链路安全&lt;/h1&gt;
&lt;h2&gt;加解密相关常见概念&lt;/h2&gt;
&lt;h3&gt;密钥&lt;/h3&gt;
&lt;p&gt;密钥与算法一起使用，密钥+算法+明文=密文&lt;/p&gt;
&lt;p&gt;密钥根据算法的不同，一般会是一个（对称加密）或者一对（非对称加密）&lt;/p&gt;
&lt;h4&gt;公私钥&lt;/h4&gt;
&lt;p&gt;公私钥一直是一个比较容易混淆的概念，这里仔细说下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;公私钥首先存在于非对称加密的场景&lt;/li&gt;
&lt;li&gt;密钥本身并没有所谓公私的概念，所谓公私只取决于你对他的使用，比如，密钥A和B是一对密钥，A和B都能对彼此加密的数据进行解密，但是A和B之中被发放出去的就是公钥，自己保管的就是私钥，公钥可以发放给很多人，所以一般说，私钥才是这对密钥的主人&lt;/li&gt;
&lt;li&gt;虽然RSA的公钥加密比私钥解密要快，说明公私钥还是有区别的，但是一般来说公私钥的本质区别还是由是否分发出去来区分&lt;/li&gt;
&lt;li&gt;具体的后面会有例子详细讲解&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;算法&lt;/h3&gt;
&lt;p&gt;算法一般分为2种常见类型：签名、加解密&lt;/p&gt;
&lt;h4&gt;签名&lt;/h4&gt;
&lt;p&gt;签名和摘要一般是一起的，所以签名算法一般都是摘要类算法，特点是&lt;strong&gt;不可逆、速度快&lt;/strong&gt;，常见的有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SM3（国密算法）&lt;/li&gt;
&lt;li&gt;MD5&lt;/li&gt;
&lt;li&gt;HASH&lt;/li&gt;
&lt;li&gt;SHA散列函数家族&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;签名算法的攻击&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;签名算法一般的攻击方式是彩虹表，因为摘要一定是损失信息的，所以基本都是通过彩虹表碰撞来得到明文&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;加解密&lt;/h4&gt;
&lt;p&gt;加解密算法的特点就是可逆（通过密钥解密），速度慢，常见的有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SM2，SM4（国密算法）&lt;/li&gt;
&lt;li&gt;AES、DES&lt;/li&gt;
&lt;li&gt;RSA&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;加解密算法的攻击&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;加解密算法的攻击一般通过以下途径：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;猜测算法&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拿到密文和明文&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通过一些方式，使用密文+明文+猜测的算法，去计算或者碰撞出密钥&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;比如RSA，在知道明文密文的情况下，通过公钥，就可以通过算法的原理来反向生成私钥，只不过RSA的公私钥是通过大质数拆解得到的，所以这个过程会非常久，这也是为什么随着算力提升，RSA密钥的长度也一直在变长的原因&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;防御加解密攻击&lt;/h4&gt;
&lt;p&gt;所以防止加解密攻击，主要就是保护密钥+保护算法，如果被攻击者既知道了部分密钥又知道了算法，那么在专业的人眼中就十分容易破解&lt;/p&gt;
&lt;p&gt;举个例子就是MD5，如果不加盐并且明文较短的MD5百度彩虹表都可以直接逆向得到明文，比如经典的&lt;strong&gt;e10adc3949ba59abbe56e057f20f883e&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;签名与加解密&lt;/h2&gt;
&lt;p&gt;大部分人以及文章其实一直混淆了签名和加解密的概念，包括他们的目的和用途，这里明确下相关的概念，以便大家在实际情况中知道什么场景该使用什么途径和手段&lt;/p&gt;
&lt;p&gt;在互联网安全上，最典型的攻击就是中间人类型的攻击，签名和加解密都可以进行一定防范&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.nlark.com/yuque/0/2023/jpeg/2939889/1672725707794-17747e01-110a-44ce-9bce-cf198041339b.jpeg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;签名&lt;/h3&gt;
&lt;h4&gt;目的&lt;/h4&gt;
&lt;p&gt;签名的目的，是&lt;strong&gt;防止信息在传输链路上被篡改，以及保证来源的可靠性&lt;/strong&gt;，&lt;strong&gt;虽然加密也可以防止信息被篡改，但是两者目的以及效率是不同的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这也就是很多人一直没搞清楚签名和加密的原因，加密当然也可以达到一样的效果，但是就像慢跑也可以锻炼，跑马拉松也可以锻炼，在签名就可以解决的场景使用效率更低的加解密是没有必要的&lt;/p&gt;
&lt;p&gt;主要的方式就是，在传输信息时，除了信息本体之外，还额外传输一段本体的摘要，也就是最终传输的数据包括明文+明文的摘要&lt;/p&gt;
&lt;h4&gt;方法以及流程&lt;/h4&gt;
&lt;p&gt;一般有2种形式，对称式和非对称式&lt;/p&gt;
&lt;p&gt;对称式（MD5，HASH，AES）描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方使用摘要算法或者使用对称密钥加解密，对报文进行摘要&lt;/li&gt;
&lt;li&gt;将报文本体与摘要一起发送&lt;/li&gt;
&lt;li&gt;接收方在收到报文时，使用相同的摘要算法或者使用对称密钥加解密，对报文进行摘要，将自己生成的摘要和接收到的摘要进行对比，相同&lt;strong&gt;就可以确定报文没有被修改&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;非对称式（RSA）描述：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方使用私钥对报文进行加密，得到摘要&lt;/li&gt;
&lt;li&gt;将报文和摘要一起发送&lt;/li&gt;
&lt;li&gt;接收方在收到报文时，将密文使用发送方的公钥解密，可以成功解密得到有效报文的，就&lt;strong&gt;确认报文没有被修改并且发送者是发送方本人&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;签名保障安全性高的要点就是：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;防止算法泄露，如果你只是简单的使用不加盐md5，因为md5的结果串十分明显，所以很容易被猜测到，这样别人在修改报文后直接md5生成新的摘要，就可以直接破解&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对“密钥”的保护。如果是MD5，双方需要对加的盐进行保密，如果是加解密算法，则需要对密钥保密&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/8dadde7e1b6270b7bf462e4921c3d5c4.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;加解密&lt;/h3&gt;
&lt;h4&gt;目的&lt;/h4&gt;
&lt;p&gt;加解密除了包含了签名的效果之外，多出的功能是&lt;strong&gt;防止内容被窃听&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子，如果一个医院要向国家上报病人得病情况的信息，消息体内容是（姓名、年龄、得的是什么病），这类数据泄露并被人获取到卖给灰产，你就会收到一堆莆田医院的电话叫你去送钱了&lt;/p&gt;
&lt;p&gt;这种窃听可能发生在数据传输的任何环节，比如写字楼的交换机被人黑了，或者是DNS被篡改了，如果你的信息够值钱，甚至他可能会攻击任意一个数据交换中心来窃取这个内容&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/d4fccdc273f78f95a2089bfa247f6c2d.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;方法以及流程&lt;/h4&gt;
&lt;p&gt;这里对称以及非对称的处理一样&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方对整段报文使用密钥以及算法，进行加密，发送密文&lt;/li&gt;
&lt;li&gt;接收方对整段密文使用密钥以及算法，进行解密，拿到明文&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/1be013a337c93ff3d4b28fe9008de3da.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;签名与加解密工业实践方案&lt;/h2&gt;
&lt;p&gt;一般银行里对外的接口，或者银行内部跨系统之间，以及银行外部系统和银行核心系统之间，一般是使用2对公私钥来保证通信链路的安全的，下面描述一下定义和流程&lt;/p&gt;
&lt;p&gt;如果ewtprod和study要进行安全通信，那么需要2对非对称密钥E和S&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送方ewtprod，使用E的私钥进行签名后，得到报文和摘要，再对报文和摘要，使用S的公钥进行加密，得到整段密文&lt;/li&gt;
&lt;li&gt;将整段密文发送到接收方&lt;/li&gt;
&lt;li&gt;接收方study对密文使用S的私钥解密后，得到报文和摘要，使用E的公钥对摘要解密，对摘要解密成功即确认报文来源是E&lt;/li&gt;
&lt;li&gt;study使用同样的方式，即使用S的私钥进行签名后，得到报文和摘要，再对报文和摘要，使用E的公钥进行加密，得到整段密文并返回&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Https&lt;/h2&gt;
&lt;p&gt;结合前面的概念，我们回顾下Https的原理，Https的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;请求方获取服务方的公钥，选取一个对称算法并生成一个对称密钥，将算法以及密钥通过服务方的公钥加密，发送给服务方&lt;/li&gt;
&lt;li&gt;服务方通过私钥解密，获得对称算法以及对称密钥，使用对称算法以及对称密钥对返回进行加密，返回成功&lt;/li&gt;
&lt;li&gt;请求方使用对称算法以及对称密钥解密，确认对称信道建立，双方后续使用对称算法以及对称密钥进行通信&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Https最容易被攻击的，反而是公钥也就是所谓证书的可靠性（证书中包括公钥），所以才有了CA这种证书颁发机构，CA根证书有很多是直接安装在计算机系统中的，在建立https之前，会先通过预先安装的CA根证书和根证书颁发机构建立https连接，来从根证书颁发机构来获取实际请求服务器的证书&lt;/p&gt;
&lt;h1&gt;防注入&lt;/h1&gt;
&lt;h2&gt;SQL注入&lt;/h2&gt;
&lt;p&gt;这个大家都比较清楚，就是通过sql里包含where 1=1等条件绕过登录，或者通过注入恶意sql攻击数据库&lt;/p&gt;
&lt;h2&gt;XSS攻击&lt;/h2&gt;
&lt;p&gt;Xss攻击目前我们都是没有防范的，但是如果找绿盟来做等保认证，又会是一定要求整改的&lt;/p&gt;
&lt;p&gt;Xss主要是跨域脚本攻击，通过提交构造好的脚本内容，来攻击网站，攻击方式大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;攻击者提交一段js脚本到服务器，服务器未做校验，保存了下来&lt;/li&gt;
&lt;li&gt;另外的用户访问到该数据时，服务器返回了这段js脚本，浏览器将其加载到网页上，浏览器会渲染并执行这段js&lt;/li&gt;
&lt;li&gt;如果提交的恶意js为无限弹窗，则会导致浏览器崩溃，网站完全无法打开&lt;/li&gt;
&lt;li&gt;如果提交的恶意js为跨域表单提交攻击，则可能会导致用户财产损失&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这类攻击高发区就是用户可以提交内容，并且提交的内容会给其他用户展示的区域，典型的如评论等功能&lt;/p&gt;
&lt;p&gt;比如我在评论区回复一段js脚本，脚本内容就是使用form表单向银行提交一个转账请求，此时如果用户刚好打开了那个银行的网站并处于登录态，因为浏览器的特性，请求时会自动带上相关的cookie直接去请求，用户的钱就被转走了&lt;/p&gt;
&lt;p&gt;现在大部分服务端都有所防范，会拒绝跨域的请求&lt;/p&gt;
&lt;h3&gt;防范：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对于输入的内容，要通过正则等方式，对常见js脚本内容进行过滤，比如过滤&amp;lt;&amp;gt;!sumit alert . 等字符和内容&lt;/li&gt;
&lt;li&gt;对于被请求时，要开启跨域检查，对于跨域的请求进行拦截&lt;/li&gt;
&lt;li&gt;不要使用session机制来确认用户的身份，而是使用token&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;商用密码最高安全级别&lt;/h1&gt;
&lt;p&gt;目前商用密码最高安全级别的实现，就是支付密码的实现，由国密局制定规范，用于支票的兑现&lt;/p&gt;
&lt;p&gt;主要的原理就是使用特定的算法，来进行密码的计算以及核验，首先具有支付密码设备经营资格的企业，可以定期从国密局直接购买一定数量的芯片（芯片有两类，一个核验芯片一个密码器芯片），然后将大量芯片集成到核验机上，将核验机卖给银行&lt;/p&gt;
&lt;p&gt;客户开通支票业务时，银行会使用一个密码器（用户端，和令牌差不多），将密码器与核验机连接，核验机进行发行操作，核验机某个芯片上会生成一对密钥写入密码器的芯片中，发行成功&lt;/p&gt;
&lt;p&gt;客户在使用支票时，除了填写支票以及签字盖章之外，还需要将支票上的六要素：包括金额，开户行号，日期，支票号，等信息输入密码器，计算出密码后，填写到支票上的密码那一栏&lt;/p&gt;
&lt;p&gt;支票拿到银行兑换时，除了需要核验签字和章之外，最主要的会输入六要素到核验机，去核验支付密码的正确性，支付密码正确的支票才可以兑现&lt;/p&gt;
&lt;p&gt;这种密码交互逻辑，也就是大多数手机令牌或者各种安全令牌的交互逻辑&lt;/p&gt;
</content:encoded></item><item><title>分库分表使用规范</title><link>https://www.intotw.cn/posts/db-mysql-sharding/</link><guid isPermaLink="true">https://www.intotw.cn/posts/db-mysql-sharding/</guid><pubDate>Wed, 20 Sep 2023 06:12:19 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;1 设计规范&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;1.1 &lt;strong&gt;必须使用分布式主键&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 分库分表后使用依旧使用自增主键会导致后续分库分表扩容时主键全部需要重写，并且其他数据依赖方（如大数据）取数聚合计算时出现冲突&lt;/p&gt;
&lt;p&gt;l 主键就是对数据的唯一标识键，哪怕分库分表了也应该是如此&lt;/p&gt;
&lt;p&gt;1.2 &lt;strong&gt;分库分表前需要谨慎评估是否真的需要分库分表&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 分库分表会带来更多的问题，不带分表键的查询都不支持，对业务的限制更多&lt;/p&gt;
&lt;p&gt;l 根据业务特性，可以采用不同的分库分表方式或者不分表，如使用日期分表或者定期迁移历史数据&lt;/p&gt;
&lt;p&gt;l 现在硬件性能上去了，单表一两千万的性能完全是OK的&lt;/p&gt;
&lt;p&gt;1.3 &lt;strong&gt;分库分表的分库分表数需要互质&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 例如共分m个库n个表，m与n需要互质，即mn之间必须没有公约数，否则会导致部分库部分表永远没有数据，数据严重倾斜&lt;/p&gt;
&lt;p&gt;l 因为逻辑都是根据分表键去mod，若分表键值为k，举一个例子，m=4，b=256时，在第0个库的第18张表将永远没有数据，因为k%4=0 &amp;amp;&amp;amp; k%256=18，这个k在实数域不存在&lt;/p&gt;
&lt;p&gt;l 网上的证明：&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/c77cf5369d4b341fa5cc5106dfa64e2e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1.4 &lt;strong&gt;选择分表键时，要考虑数据的倾斜&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 这是很基本的规范，没有人会拿性别当分表键去分表&lt;/p&gt;
&lt;p&gt;l 大部分场景的分表，第一次分表都是使用userId的&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;2 使用规范&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;2.1 禁止任何对分库分表的查询不带分表键&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 好像没什么好解读的，不带分库分表键的查询就会全部分库分表全表扫描，应用内存io和数据库内存io直接炸了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.2 禁止使用联合查询join&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 尤其是分库后，要join的数据不一定在一个库，最后还是在应用内存里做了&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.3 使用分表键查询时，禁止使用分表键in（）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 我们用的sharding版本4.1.1，哪怕是使用in查询分表键，也不会先根据分表键路由来决定要查询哪些表，而是每张表去in一次&lt;/p&gt;
&lt;p&gt;l 即使高版本有优化，那随着in（）语句里数据量的变大，对不同库表的io也会增多&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.4 查询时，需要指定字段，而不是使用表名.*或者select *&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l 这个.*的方式，在ddl增加字段后，sharding是无法感知并更新metadata的，会导致返回的字段混乱，并且sharding版本不同出现的错还不太相同，主要会有：直接报错，丢失字段，后面字段被覆盖这些问题&lt;/p&gt;
&lt;p&gt;https://github.com/apache/shardingsphere/issues/23475&lt;/p&gt;
&lt;p&gt;https://github.com/apache/shardingsphere/issues/22824&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.5 禁止不带分表键的limit order等操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;解读：&lt;/p&gt;
&lt;p&gt;l sharding在执行不带分表键查询的limit或order等操作时，为了获取正确的数据，会把sql进行转化，如limit 100,10，会转化为对每个表执行limit0,100并在内存里聚合数据，这样在limit的第一个值过大时，会转化成对所有表进行全表扫描全部数据拉取&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;l 对于分库分表后的业务表，只使用单分表键+其他查询条件的查询，并且不能使用join&lt;/p&gt;
&lt;p&gt;l 编写相关sql时，一定要脑袋里过一下，这个sql在分库分表的逻辑上&lt;strong&gt;是否能定位到分库分表的某张表&lt;/strong&gt;上执行，&lt;strong&gt;如果不能，那么这个sql一定会导致应用层或者代理层去进行聚合操作，这种sql一定要禁止&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;3 常见问题解决方案&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;这也不能那也不能，那么很多问题怎么解决？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.1 如果需要非分表键的查询&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方案：&lt;/p&gt;
&lt;p&gt;l 使用影子表，并且通过双写或者binlog来同步数据（视延迟要求而定），影子表即和分表结构一样，但是分表键不同的表&lt;/p&gt;
&lt;p&gt;l 交给大数据来&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
</content:encoded></item><item><title>建模过程总结</title><link>https://www.intotw.cn/posts/tech-modeling/</link><guid isPermaLink="true">https://www.intotw.cn/posts/tech-modeling/</guid><pubDate>Tue, 19 Sep 2023 08:43:35 GMT</pubDate><content:encoded>&lt;p&gt;整个建模过程，个人总结来说应该分为3个阶段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前期准备工作，了解问题领域的现状，做好目标分析&lt;/li&gt;
&lt;li&gt;做好模块的划分以及具体模块的设计&lt;/li&gt;
&lt;li&gt;对整个项目进行总结反思，包括目标的达成情况、设计的实现情况，设计是否存在不合理&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前期准备工作&lt;/h2&gt;
&lt;h3&gt;了解问题领域&lt;/h3&gt;
&lt;h4&gt;了解业务概况&lt;/h4&gt;
&lt;p&gt;在这一步，我们需要了解现在业务的“概况”，强调一定是概况，现在还没到具体详细分析业务的时候。了解业务概况，主要就是了解业务的目标，动机，和主要的流程。以及业务的主要上下游和会有哪些涉众。&lt;/p&gt;
&lt;h4&gt;整理业务目标&lt;/h4&gt;
&lt;p&gt;了解概况后，我们就可以整理“本次”需要建设的业务目标，目标一定要是可以量化的值，比如“让流程从10步简化为5步”，“让操作时间节省30%”这种，而不是“让流程更加简便”，“让操作更快”。&lt;/p&gt;
&lt;h2&gt;做好涉众分析&lt;/h2&gt;
&lt;h3&gt;根据业务目标认清受众&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;涉众是与要建设的业务系统相关的一切人和事。首先要明确的一点是，涉众不等于用户，通常意义上的用户是指系统的使用者，而这仅是涉众中的一部分。如何理解与业务系统相关的一切人和事呢？凡是与这个项目有利益关系的人和事都是涉众，他们都可能对系统建设造成影响。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在这一步，按照书里所说，主要根据分析方法，产出涉众分析报告和用户分析报告。&lt;/p&gt;
&lt;p&gt;需要注意，需要按照引用的说明里来区分涉众和用户，比如在一个系统建设初期，涉众包括所有流程中的参与方和相关利益方。比如我们要修一条铁路，那么从铁路局，到建筑局，到建筑工人，到需要拆迁的那些人，其中的涉众无比之广，我们在准备工作时就需要尽可能考虑这些涉众对这件事不同的需求，来避免后续无意义的纠纷或者纠正。&lt;/p&gt;
&lt;p&gt;举例而言，对于一个登录功能而言：&lt;/p&gt;
&lt;h4&gt;涉众分析报告&lt;/h4&gt;
&lt;p&gt;|  编号 | 涉众名称 | 涉众描述 | 期望 |
|  001  | 老板| 公司老板 | 尽可能省钱 |
|  002  | 项目经理| 项目经理 | 不要延期 |
|  003  | 年轻用户 | 55岁以下的用户 | 界面漂亮 |
|  004  | 老年用户| 55岁及以上的用户 | 操作简单易用|
|  005  | 安全负责人 | 安全工程师 | 系统符合等保标准，无隐私和泄露风险 |
|  006  | …… | …… | …… |&lt;/p&gt;
&lt;p&gt;我们可以看到，涉众包括了这个系统影响到的各式各样的人，从甲方到乙方到实施方等，在分析涉众的需求的时候都需要将他们的诉求尽可能的罗列出来，在实际实施和后续操作时，再按照优先级以及系统愿景综合做取舍。按照&lt;strong&gt;阿布思考法&lt;/strong&gt;来说，我们这一步需要在&lt;strong&gt;给定时间&lt;/strong&gt;内尽可能的考虑涉众的期望，但不能因为涉众过多，过广，期望五花八门或者无法想象，而放弃这一环节。&lt;/p&gt;
&lt;h4&gt;用户分析报告&lt;/h4&gt;
&lt;p&gt;用户分析报告与涉众分析报告相同，甚至可能只是其中的一部分，因为用户是涉众的一部分，只是其中的子集，这里就不再赘述了。&lt;/p&gt;
&lt;h2&gt;系统或者业务模块的划分以及模块的设计&lt;/h2&gt;
&lt;p&gt;笔者因为是做后端技术的，所以这部分的思考以及篇幅会更多一些&lt;/p&gt;
&lt;h3&gt;两种划分模块的方式&lt;/h3&gt;
&lt;p&gt;模块划分一般有2种方式，也就是目前诞生的大多数架构设计背后的划分方式&lt;/p&gt;
&lt;h4&gt;按照流程（过程）划分&lt;/h4&gt;
&lt;p&gt;这是大多数人思考问题的方式方法所带来最基本的划分，划分的手段是根据系统提供的用例以及能力进行最底层的区分，其他的上层合并聚合也是一些底层过程的聚合，比如：注册+登录+忘记密码=登录模块&lt;/p&gt;
&lt;p&gt;这种划分一般有以下几个特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据业务流程来，整个设计以及边界都依据业务操作以及业务流程&lt;/li&gt;
&lt;li&gt;每个模块，往往是从业务流程本身直接穿透到对应表模型，核心目的只是为了完成操作&lt;/li&gt;
&lt;li&gt;很少复用，因为复用在这种划分模式里会带来更多的问题，复用反而在这种划分是不应该被推荐的&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举个例子，我们一些大多数系统，其实都是按照这种思路去设计的，或者说，大部分没有进行“设计”的系统，都是依循的这种方式。&lt;/p&gt;
&lt;p&gt;最常见的，我们开发一个登录流程，就从接口到用户表直接干下去，我们再开发一个注册流程，也是这样，再来个忘记密码，也是如此。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;耦合低，单流程开发中，只需要专注于自己流程所涉及到的表以及状态关系表示&lt;/li&gt;
&lt;li&gt;思考和抽象成本低，力大砖飞&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;会出现很多重复代码或者重复工作，令一些人忍不住去复用，带来更多的问题&lt;/li&gt;
&lt;li&gt;当依赖同一个表或者同几张表的流程过多时，这种复用反而也难以避免，导致这种因为表而导致的影响很难被评估&lt;/li&gt;
&lt;li&gt;往往自己的流程有时对其他的流程会产生入侵，此时会因为对他人流程不熟悉，改出更多的问题（比如新增&lt;strong&gt;登录日志的管理台查看&lt;/strong&gt;功能，就一定会对登录流程产生入侵，因为要在登录时进行埋点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;{{&amp;lt; admonition type=note title=&quot;复用不一定是好事&quot; &amp;gt;}}
复用也会带来依赖的耦合，被复用的越多，修改时影响的也就越多
当某次修改需要对复用的逻辑进行修改时，此时抽出复用方法放弃复用或者修改复用方法达到特殊目的，都不是一个好选择
如果一开始就没有选择抽象和聚合，后续仅仅从&lt;strong&gt;相同&lt;/strong&gt;这个目标出发来复用方法，不是一个推荐的做法
{{&amp;lt; /admonition &amp;gt;}}&lt;/p&gt;
&lt;h4&gt;按照业务对象划分&lt;/h4&gt;
&lt;p&gt;这是目前一些流行的&lt;strong&gt;高大上&lt;/strong&gt;的架构设计的背后逻辑，比如中台、领域驱动&lt;/p&gt;
&lt;p&gt;这种设计的出发点是业务系统中的业务对象，从业务对象所具备的能力来进行设计&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有了所谓&lt;strong&gt;边界&lt;/strong&gt;的概念，因此跨流程之间的修改风险很低&lt;/li&gt;
&lt;li&gt;在已经业务很清晰的领域，可以大大降低开发成本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;思考和抽象成本高，一般都是对已经成熟的系统重构，或者对业务已经很清晰的领域，才会使用&lt;/li&gt;
&lt;li&gt;缺少明确的标准和落地方案，实际上是以架构个人理解以及想法去落实和实现的，理解成本反而更高&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>工作记录：新生代老年代比例错误问题</title><link>https://www.intotw.cn/posts/work-jvm-heap-ratio/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-jvm-heap-ratio/</guid><pubDate>Tue, 19 Sep 2023 08:25:10 GMT</pubDate><content:encoded>&lt;h1&gt;线上排查：新生代老年代比例错误问题&lt;/h1&gt;
&lt;h2&gt;起因&lt;/h2&gt;
&lt;p&gt;线上一个应用频繁full gc，排查发现单pod总内存3g的情况下新生代只有200mb，很奇怪，于是到容器里查看jvm参数。&lt;/p&gt;
&lt;p&gt;jamp -heap 1，打印&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 3221225472 (3072.0MB)
   NewSize                  = 261685248 (249.5625MB)
   MaxNewSize               = 261685248 (249.5625MB)
   OldSize                  = 2959540224 (2822.4375MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 268435456 (256.0MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 268435456 (256.0MB)
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 235536384 (224.625MB)
   used     = 45215760 (43.12110900878906MB)
   free     = 190320624 (181.50389099121094MB)
   19.19693222428005% used
Eden Space:
   capacity = 209387520 (199.6875MB)
   used     = 33745216 (32.18194580078125MB)
   free     = 175642304 (167.50555419921875MB)
   16.116154391627543% used
From Space:
   capacity = 26148864 (24.9375MB)
   used     = 11470544 (10.939163208007812MB)
   free     = 14678320 (13.998336791992188MB)
   43.866318628602755% used
To Space:
   capacity = 26148864 (24.9375MB)
   used     = 0 (0.0MB)
   free     = 26148864 (24.9375MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2959540224 (2822.4375MB)
   used     = 279529808 (266.5803985595703MB)
   free     = 2680010416 (2555.8571014404297MB)
   9.445041690367646% used
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NewRatio确实是2，但是新生代是224mb，老年代是2.8g，比例确实和监控一样，不符合默认的1:2配比&lt;/p&gt;
&lt;h2&gt;排查&lt;/h2&gt;
&lt;p&gt;一番排查，google“NewRatio not work”后，发现一个JDK的bug：
https://bugs.openjdk.org/browse/JDK-8153578&lt;/p&gt;
&lt;p&gt;检查下JVM参数，确实使用了UseConcMarkSweepGC&lt;/p&gt;
&lt;h2&gt;解决&lt;/h2&gt;
&lt;p&gt;在启动参数中指定新生代GC算法-XX:+UseParNewGC后，重新发布解决
解决方式还可以在启动参数里指定-XXNewRatio=2解决，不过我们是用的指定新生代算法&lt;/p&gt;
</content:encoded></item><item><title>计算机网络学习笔记-TCP</title><link>https://www.intotw.cn/posts/science-tcp/</link><guid isPermaLink="true">https://www.intotw.cn/posts/science-tcp/</guid><pubDate>Tue, 19 Sep 2023 08:21:04 GMT</pubDate><content:encoded>&lt;h2&gt;TCP中的超时时间计算-指数移动加权平均&lt;/h2&gt;
&lt;p&gt;TCP协议使用指数移动加权平均来计算和预估往返时间RTT。这个东西比较巧妙，比较值得借鉴。
我们假设在某一个时间对往返时间RTT进行一次样本的测量，得到SampleRTT，那么我们对于我们设置均值RTT-EstimatedRTT，可以通过指数移动加权平均来不断迭代：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EstimatedRTT = (1 -a )·EstimatedRTT + a·SampleRTT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中a就是指数移动加权平均中的**“指数”**，在[RFC 6298]中给出的a推荐值是a= 0. 125 (即1/8)。&lt;/p&gt;
&lt;p&gt;除了估算RTT外，测量RTT的变化也是有价值的。[ RFC 6298]定义了RTT偏差DevRTT, 用于估算SampleRIT一般会偏离EstimatedRTI的程度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DevRTT = (1 -b)·DevRTT +b*|SampleRTT-EstimatedRTT|
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到DevRTT是一个SampleRTT与EstimatedRTT之间差值的EWMA。如果SampleRTT值波动较小，那么DevRTT的值就会很小；另一方面，如果波动很大，那么DevRTT的值就会很大。b的推荐值为0.25。&lt;/p&gt;
&lt;p&gt;根据往返时间的RTT均值EstimatedRTT，以及我们计算出的波动值DevRTT，可以通过下面这个算式得到往返的超时时间。&lt;/p&gt;
&lt;p&gt;在这里，因为超时时间需要略大于预估的往返时间，但是具体大多少呢？大的就是4*波动的预估值，这个计算方法虽然不能说很科学，但是在感性以及工程实践的角度来看，是非常合适的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TimeoutInterval = EstimatedRTT + 4·DevRTT 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TCP中的拥塞控制以及TCP分岔&lt;/h2&gt;
&lt;p&gt;TCP中通过拥塞控制，来限制发送方的发送速率来达到提高网络质量的目的。
我们简单假设一下，如果发送方的发送速率得不到限制，那么在整条链路上，因为中间路由以及接收方的接受缓存问题，会出现无限的重发以及无限的时延。（因为在接收缓存满的时候，会抛弃掉后续的包，而这又会导致我们**“无限制”**的发送方继续重发那些必定会丢失的包，这对整条链路以及发送方自己来说，都不是什么好事。&lt;/p&gt;
&lt;p&gt;TCP中的拥塞控制主要通过3个状态来控制：&lt;/p&gt;
&lt;h3&gt;慢启动&lt;/h3&gt;
&lt;p&gt;TCP的拥塞控制窗口cwnd一般会以一个MSS的大小开始，也就是说一开始只发送1个TCP包（MSS是TCP包数据部分的大小，所以不会太小），此时总速率大约为MSS/RTT,慢启动会让TCP的拥塞窗口cwnd以MSS的指数级别增加，即每次收到一个确认包，MSS就会+1，也就是1-&amp;gt;2-&amp;gt;4-&amp;gt;8的2^增长&lt;/p&gt;
&lt;h3&gt;拥塞避免&lt;/h3&gt;
&lt;p&gt;如果慢启动的指数增长时出现了丢包，那么就会进入拥塞避免状态。此时会新引入一个值ssthresh为cwnd的一半，然后将cwnd重新置位1，重新开始慢启动流程&lt;/p&gt;
&lt;p&gt;但是因为这次设置了ssthresh，所以再指数增长下的cwnd再次达到或者超过ssthresh时，就会结束慢启动并进入拥塞避免状态&lt;/p&gt;
&lt;p&gt;进入到拥塞避免状态后，cwnd开始以线性增长：即每个确认包会使cwnd增加一个MSS的大小&lt;/p&gt;
&lt;h3&gt;快速恢复&lt;/h3&gt;
&lt;p&gt;当cwnd持续增长，此时有两种情况的丢包
-基于冗余ACK的丢包,此时会进入快速恢复状态
-时延触发的丢包，此时直接重复拥塞避免状态，cwnd置为1，从新开始线性增加到ssthresh&lt;/p&gt;
&lt;p&gt;在快速恢复状态下，因为是冗余ACK行为确认发生的丢包，此时会将cwnd减半，并且当收到3个冗余的ACK,将ssthresh的值记录为cwnd的值的一半。接下来进入快速恢复状态。&lt;/p&gt;
&lt;p&gt;在快速恢复中，对于引起TCP进入快速恢复状态的缺失报文段，对收到的每个冗余的ACK, cwnd的值增加一个MSS。最终，当对丢失报文段的一个ACK到达时，TCP在降低cwnd后进入拥塞避免状态。如果出现超时事件，快速恢复在执行如同在慢启动和拥塞避免中相同的动作后，迁移到慢启动状态：当丢包事件出现时，cwnd的值被设置为1个MSS, 并且ssthresh的值设置为cwnd值的一半。&lt;/p&gt;
&lt;p&gt;可见基于拥塞控制来说，在不考虑因为时延导致的丢包，TCP的带宽一直在三个状态的循环中反复，在出现拥塞避免后，会一直重复：线性增加-&amp;gt;冗余ACK-&amp;gt;快速恢复-&amp;gt;窗口减半-&amp;gt;线性增加，这样的过程，也就是说TCP的带宽窗口大小是锯齿型的&lt;/p&gt;
&lt;p&gt;[图，TCP窗口]&lt;/p&gt;
&lt;h3&gt;TCP分岔&lt;/h3&gt;
&lt;p&gt;基于拥塞启动的问题，分布式应用或者说部署复杂的应用，多采用TCP分岔的方式来避免慢启动的时延，如果客户端C到服务器S的RTT为500ms，在慢启动的情况下，建立连接的总时延可以认为是4&lt;em&gt;RTT。
但是如果我们在近C端部署一个前置F（CDN就是这样的原理），此时并且F与S之间采用长时间建立连接的方式，那么此时C到S建立连接的总时延就是4&lt;/em&gt;RTT(F)+RTT(C)，注意因为F是在C的近端网络部署的，所以RTT(F)极小，此时我们的时延缩小了接近4倍&lt;/p&gt;
</content:encoded></item><item><title>基于Quorum投票的冗余控制算法</title><link>https://www.intotw.cn/posts/distribution-algorithm/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-algorithm/</guid><pubDate>Tue, 19 Sep 2023 08:04:16 GMT</pubDate><content:encoded>&lt;h2&gt;基于Quorum投票的冗余控制算法&lt;/h2&gt;
&lt;h3&gt;描述（维基百科）&lt;/h3&gt;
&lt;p&gt;在有冗余数据的分布式存储系统当中，冗余数据对象会在不同的机器之间存放多份拷贝。但是同一时刻一个数据对象的多份拷贝只能用于读或者用于写。&lt;/p&gt;
&lt;p&gt;该算法可以保证同一份数据对象的多份拷贝不会被超过两个访问对象读写。&lt;/p&gt;
&lt;p&gt;算法来源于[Gifford, 1979][3][1]。 分布式系统中的每一份数据拷贝对象都被赋予一票。每一个读操作获得的票数必须大于最小读票数（read quorum）（Vr），每个写操作获得的票数必须大于最小写票数（write quorum）(Vw）才能读或者写。如果系统有V票（意味着一个数据对象有V份冗余拷贝），那么最小读写票数(quorum)应满足如下限制：&lt;/p&gt;
&lt;p&gt;Vr + Vw &amp;gt; V
Vw &amp;gt; V/2
第一条规则保证了一个数据不会被同时读写。当一个写操作请求过来的时候，它必须要获得Vw个冗余拷贝的许可。而剩下的数量是V-Vw 不够Vr，因此不能再有读请求过来了。同理，当读请求已经获得了Vr个冗余拷贝的许可时，写请求就无法获得许可了。&lt;/p&gt;
&lt;p&gt;第二条规则保证了数据的串行化修改。一份数据的冗余拷贝不可能同时被两个写请求修改。&lt;/p&gt;
&lt;h3&gt;解释&lt;/h3&gt;
&lt;p&gt;这种逻辑来自于鸽巢原理，也是狄利克雷抽屉原理，简单的描述若有n个笼子和n+1只鸽子，所有的鸽子都被关在鸽笼里，那么至少有一个笼子有至少2只鸽子。
Vr + Vw &amp;gt; V
Vw &amp;gt; V/2
这两个公式的理解：&lt;/p&gt;
&lt;p&gt;首先，这个算法讨论的是&lt;strong&gt;无主的模式&lt;/strong&gt;，也就是&lt;strong&gt;允许静默同步，但是每个节点都可以提供读写服务&lt;/strong&gt;，单次写操作不需要等待所有节点同步完成。在这种情况下,Vr和Vw指节点中可以进行读写需要的最少的票数。换句话说，Vr表示：至少要有Vr个节点提供读服务，Vw表示：至少要有Vw个节点可写，满足VrVw，集群之间的副本才可以认为具有一致性。&lt;/p&gt;
&lt;p&gt;这两个最小票数是可以调整的，我们分析一下为什么必须满足这两个公式，集群之间的副本才具备一致性。&lt;/p&gt;
&lt;p&gt;Vr + Vw &amp;gt; V，假设我们共有5个节点，{a,b,c,d,e},如果我们设置Vw=2,Vr=3，那么如果选择写入的节点是{a,b}，选择读取的节点刚好是{c,d,e}，都满足了读写的票数要求，此时读到的就是脏数据，不满足一致性了，注意我们这里选择的节点是随机的。每个节点其实都可以读写，只是需要判断当前能否投出足够的票，只要集群内能写的机器&amp;gt;=Vw,就可以在能写的机器上写。&lt;/p&gt;
&lt;p&gt;但是如果我们设置Vw=3,Vr=3,此时如果可以进行操作，就满足Vr + Vw &amp;gt; V，那么读取的机器必定有一台就是写了最新数据的机器，那么从3台中我们一定可以读到最新数据。&lt;/p&gt;
&lt;p&gt;Vw &amp;gt; V/2,这个更好理解，如果5个节点，我们让Vw=2，那么就可能出现{a,b},{c,d}同时在写，此时无论如何都读不到正确的数据，完全没有一致性可言。但是如果满足的话，假设A1，A2两个写操作进来写，我们会发现至少有一个节点，会被阻塞而依次写了A1和A2，也就是最新的数据。&lt;/p&gt;
&lt;p&gt;所以一般都会保证Vw&amp;gt;V/2来保证写一致，适当调小Vr让Vr+Vw &amp;lt; V ,以一定概率读到旧数据为代价来减少读的规模。&lt;/p&gt;
&lt;p&gt;但是这个如果一开始就规定好读写节点，&lt;strong&gt;也就是事先规定好部分节点只读和只写&lt;/strong&gt;，那么就会简单很多，只需要保证提供读服务的节点数量大于提供写服务的节点数量，也就是第一条即可。&lt;/p&gt;
</content:encoded></item><item><title>Java与Mysql锁相关知识总结</title><link>https://www.intotw.cn/posts/java-mysql-lock/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-mysql-lock/</guid><pubDate>Sun, 16 Apr 2023 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;锁的定义&lt;/h2&gt;
&lt;p&gt;在计算机程序中锁用于独占资源，获取到锁才可以操作对应的资源。&lt;/p&gt;
&lt;h2&gt;锁的实现&lt;/h2&gt;
&lt;p&gt;锁在计算机底层的实现，依赖于CPU提供的CAS指令（compare and swsp），对于一个内存地址，会比较原值以及尝试去修改的值，通过值是否修改成功，来表示是否强占到了这个锁。&lt;/p&gt;
&lt;h2&gt;JVM中的锁&lt;/h2&gt;
&lt;p&gt;jvm中，有2个常用的锁&lt;/p&gt;
&lt;h3&gt;synchronized&lt;/h3&gt;
&lt;p&gt;synchronized是java提供的关键字锁，可以锁对象，类，方法。
在JDK1.6以后，对synchronized进行了优化，增加了偏向锁和轻量锁模式，现在synchronized锁的运行逻辑如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在初始加锁时，会增加偏向锁，即“&lt;strong&gt;偏向上一次获取该锁的线程&lt;/strong&gt;”，在偏向锁下，会直接CAS获取该锁。该模式大大提高了单线程反复获取同一个锁的吞吐情况，在Java官方看来，大部分锁的争抢都发生在同个线程上。&lt;/li&gt;
&lt;li&gt;如果偏向锁CAS获取失败，说明当前线程与偏向锁偏向的线程不同，偏向锁就会&lt;strong&gt;升级&lt;/strong&gt;成轻量锁，轻量锁的特点就是通过&lt;strong&gt;自旋CAS&lt;/strong&gt;去获取锁。&lt;/li&gt;
&lt;li&gt;如果自旋获取失败，那么锁就会升级成重量锁，所有等待锁的线程将被JVM挂起，在锁释放后，再由JVM统一通知唤醒，再去尝试CAS锁，如果失败，继续挂起。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;很显然，偏向锁设计的目的是“在Java官方看来，对同一个锁的争抢大部分都发生在同个线程上”。
轻量锁设计的目的是“在短期内，锁的争抢通过自旋CAS就可以获取到，短时间内的CPU自旋消耗小于线程挂起再唤醒的消耗”。
重量锁就是最初优化前的synchronized的逻辑了。&lt;/p&gt;
&lt;h3&gt;ReentrantLock&lt;/h3&gt;
&lt;p&gt;说到ReentrantLock，就不得不说到JUC里的AQS了。
AQS全称AbstractQueueSynchronizer，几乎JUC里所有的工具类，都依赖AQS实现。
AQS在java里，是一个抽象类，但是本质上是一种思路在java中的实现而已。
AQS的实现逻辑如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造一个队列&lt;/li&gt;
&lt;li&gt;队列中维护需要等待锁的线程&lt;/li&gt;
&lt;li&gt;头结点永远是持有锁（或持有资源）的节点，等待的节点在头结点之后依次连接。&lt;/li&gt;
&lt;li&gt;头结点释放锁后，会按照顺序去唤醒那些等待的节点，然后那些节点会再次去尝试获取锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在synchronized锁优化以后，AQS的本质与synchronized并没有太大不同，两者的性能也并没有太大差距了，所以AQS现在的特点是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是在java api层面实现的锁，所以可以实现各种并发工具类，操作也更加灵活&lt;/li&gt;
&lt;li&gt;因为提供了超时时间等机制，操作灵活，所以不易死锁。（相同的，如果发生死锁，将更难排查，因为jstack里将不会有deadlock标识）。&lt;/li&gt;
&lt;li&gt;可以实现公平锁，而synchronized必定是非公平锁。&lt;/li&gt;
&lt;li&gt;因为是JavaApi层实现的锁，所以可以响应中断。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这里你会发现，其实ReentrantLock可以说是synchronized在JavaApi层的实现。&lt;/p&gt;
&lt;h2&gt;Mysql 锁&lt;/h2&gt;
&lt;h3&gt;共享锁(S) 与排它锁(X)&lt;/h3&gt;
&lt;h4&gt;作用范围&lt;/h4&gt;
&lt;p&gt;这两种锁都包括行级锁和表级锁。
获取共享锁时，如果该数据被其他事务的排它锁锁住，则无法获取，需要等待排它锁释放。&lt;/p&gt;
&lt;h3&gt;意向锁&lt;/h3&gt;
&lt;h4&gt;作用范围&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;意向锁为表锁&lt;/strong&gt;，在获取表锁之前，一定会检查意向锁。&lt;/p&gt;
&lt;p&gt;意图锁定协议如下：&lt;/p&gt;
&lt;p&gt;在事务获得表中某行的共享锁之前，它必须首先获得表上的 IS 锁或更强的锁。&lt;/p&gt;
&lt;p&gt;在事务获得表中行的排他锁之前，它必须首先获得表的 IX 锁。&lt;/p&gt;
&lt;p&gt;在获取任意表锁的共享锁或排它锁之前，一定会检查该表上的共享锁。&lt;/p&gt;
&lt;p&gt;表锁以及意向锁的互斥规则如下：
X	IX	S	IS
X	Conflict	Conflict	Conflict	Conflict
IX	Conflict	Compatible	Conflict	Compatible
S	Conflict	Conflict	Compatible	Compatible
IS	Conflict	Compatible	Compatible	Compatible&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;意向锁的作用在于：在获取表锁时，可以通过意向锁来快速判断能否获取。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为获取行级锁时，会先获取对应的意向锁，这样另外的事务在获取表锁时就可以通过意向锁快速的判断，而不需要每行去扫描。&lt;/p&gt;
&lt;p&gt;特别注意的是，意向锁是可以叠加的，即会存在多个，如T1事务获取了意向锁IX1和行级锁X1，T2事务依旧可以获取意向锁IX2和行级锁X2，所以仅在获取表级锁之前，才会检查意向锁。&lt;/p&gt;
&lt;h3&gt;记录锁&lt;/h3&gt;
&lt;p&gt;记录锁生效在索引上，用以在SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE时保护该行数据不被其他事务更改。&lt;/p&gt;
&lt;p&gt;记录锁在没有索引时依旧会生效，因为innodb会为每张表创建一个隐藏的索引。&lt;/p&gt;
&lt;p&gt;记录锁是最基本的行锁。&lt;/p&gt;
&lt;h3&gt;间隙锁&lt;/h3&gt;
&lt;p&gt;间隙锁生效在索引上，用于锁定索引值后的行，防止插入，在select from table where index=? for update时会生效，例如index=1，则会锁住index=1索引节点相关的行，&lt;strong&gt;防止其他事务插入数据&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是并不会防止update语句，哪怕update的数据不存在。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Next-Key Locks&lt;/h3&gt;
&lt;p&gt;这个锁是记录锁和间隙锁的组合，简而言之在select from table where index=? for update时，既会有间隙锁防止insert，也会有记录锁在index上防止这一条数据的update和delete。这个Next-key只是对这两种锁的一种概括，因为这两种锁在select for update时通常会一起出现。&lt;/p&gt;
&lt;h3&gt;Insert Intention Locks&lt;/h3&gt;
&lt;p&gt;插入意向锁，和意向锁类似。不过是特殊的间隙锁，并不发生在select for update，而是在同时发生insert时产生，例如在两个事务同时insert索引区间为[4,7]时，同时获得该区间的意向锁，此时事务不会阻塞，例如A：insert-5，B：insert-7，此时不会阻塞两个事务。&lt;/p&gt;
&lt;p&gt;插入意向锁是一个特殊的间隙锁，是为了防止正常间隙锁锁区间的情况下，insert频繁阻塞而设计的，例如A：insert-5，B：insert-7，如果没有插入意向锁，那么5和7都要去尝试获取间隙锁，此时第二个事务就会被阻塞，但是通过插入意向锁，第二个事务就不会被阻塞，只有到插入的行确实冲突，才会被阻塞。&lt;/p&gt;
&lt;h3&gt;AUTO-INC Locks&lt;/h3&gt;
&lt;p&gt;自增锁，这个锁很明显是表级insert锁，为了保证自增主键的表的主键保持原子自增。&lt;/p&gt;
&lt;p&gt;对于锁这个东西，大家应该多去理解各种锁设计运行的原理和模型，这样在加深理解后，在使用起来才会更加深入和透彻。&lt;/p&gt;
&lt;h2&gt;常见锁使用的场景和用法&lt;/h2&gt;
&lt;h3&gt;double check&lt;/h3&gt;
&lt;p&gt;众所周知，mysql的事务对防止重复插入并没有什么卵用，唯一索引又存在很多缺点，业务上最好不要使用，所以一般来说防止重复插入的通用做法就是使用分布式锁，这就有一种比较常用的写法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
if (weekendNoticeReadCountDO == null) {
    final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + &quot;:&quot; + noticeRequestDTO.getNoticeId();
    ClusterLock lock = clusterLockFactory.getClusterLockRedis(
        RedisConstant.REDIS_KEY_PREFIX,
        lockKey
    );
    if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT)) {
        //double check
        final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
        if (weekendNoticeReadCountDO == null) {
            try {
                lock.execute(() -&amp;gt; {
                    WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO();
                    readCountDO.setNoticeId(noticeRequestDTO.getNoticeId());
                    readCountDO.setReadCount(1L);
                    readCountDO.setCreateTime(new Date());
                    readCountDO.setUpdateTime(new Date());
                    weekendNoticeReadRepositoryService.insert(readCountDO);
                    return true;
                });
            } catch (ApiException err) {
                throw err;
            } catch (Exception e) {
                log.error(&quot;插入&quot;, e);
                throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), &quot;服务端出错&quot;);
            }
        } else {
            weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO);
        }
    } else {
        log.warn(&quot;redis锁获取超时，key:{}&quot;, lockKey);
        throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), &quot;服务器繁忙，请稍后重试&quot;);
    }
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在获取到锁之后，可能是经过等待才获取到的锁，此时上一个释放锁的线程可能已经插入了数据了，所以在锁内部，依旧要再次校验一下数据是否存在。
这种写法适合大多数需要唯一性的写场景。&lt;/p&gt;
&lt;h2&gt;避免死锁&lt;/h2&gt;
&lt;p&gt;如何避免死锁？最简单有效的方法就是：**不要在锁里再去获取锁，简而言之就是锁最好单独使用，不要套娃。
也要注意一些隐性锁，比如数据库。
事务A：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;插入[5,7]，插入意向锁。&lt;/li&gt;
&lt;li&gt;select for update更新[100,150]，间隙锁。
事务B：&lt;/li&gt;
&lt;li&gt;select for update更新[90,120]，间隙锁。&lt;/li&gt;
&lt;li&gt;插入[4,6]，插入意向锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时在并发场景下，就可能会出现A持有了[5,7]的间隙锁，在等待事务B[90,120]的间隙锁，事务B也一样，就死锁了。
**&lt;/p&gt;
&lt;h2&gt;顺带谈谈并发场景下常见的问题&lt;/h2&gt;
&lt;h3&gt;读写混乱&lt;/h3&gt;
&lt;p&gt;在写业务代码，定义一些工具类或者缓存类的时候，很容易疏忽而发生类似的问题。
比如构建一个static缓存，没有使用ConcurrentHashMap中的putIfAbsent等方法，也没有加锁去构建，导致上面的线程刚put了，下面的线程就删掉了，或者重复构建2次缓存。&lt;/p&gt;
&lt;h2&gt;Redis或者一些并发操作释放锁或者资源，没有检查是否是当前线程持有&lt;/h2&gt;
&lt;p&gt;这点在Redis锁的示例代码也讲到了。
线程A获取到锁，此时B,C在等待，然后A执行时间过长，导致锁超时被自动释放了，此时B获取到了锁，在快乐的执行，然后A执行完了之后，释放锁时没有判断是否还是自己持有，导致B持有的锁被删除了，此时C又获取到了锁，BC同时在执行。&lt;/p&gt;
</content:encoded></item><item><title>《软件方法》读后感</title><link>https://www.intotw.cn/posts/tech-uml/</link><guid isPermaLink="true">https://www.intotw.cn/posts/tech-uml/</guid><pubDate>Thu, 06 Apr 2023 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;近日，苦于不知道该怎么提升自己了，在原来老大的建议下，决定去学习一些关于建模和软件设计领域的书籍，来解决解决自己“感觉不对，但是说不清楚为什么不对”以及“感觉这么搞就对了，但是不知道为什么这么去规划，这么去划分就对”&lt;/p&gt;
&lt;p&gt;第一本看的是潘加宇老师的《软件方法（上）业务建模和需求》,本篇读后感不再对文里的概念和内容一一赘述，只说说个人提炼到的收获&lt;/p&gt;
&lt;h2&gt;业务系统是人脑系统的映射&lt;/h2&gt;
&lt;p&gt;这是一个很关键的概念，也是让我们认清业务系统的本质，所以我们讨论很多问题时，也就有了依据。&lt;/p&gt;
&lt;p&gt;我们平时所看到的软件系统，都以业务系统为主，他们要么是人脑系统的替代，要么是现实流程的表示，明白了这一点本质以后，我们对系统的边界和其中逻辑的合理性，就可以有了一个标尺。而不是“我觉得这里这么做不对，这个逻辑不对，但是具体又说不出来哪里不对，反正会出问题，这里不合理”。&lt;/p&gt;
&lt;p&gt;我们如何用这个标尺来评价问题呢？举一个笔者了解到的例子，一套志愿填报辅助系统，其中有个“一键填报”的功能，智能推荐一些志愿，但是其推荐的逻辑竟然是选择一批志愿后，经过各种一系列花里胡哨的随机操作来“看似”随机得到一些数据，推荐给用户。&lt;/p&gt;
&lt;p&gt;我们通过这个观念去思考这个问题，难道一个志愿填报报考专家，在帮助一名学生选择要填报的院校时，会一直“随机”挑选一些院校，而不是根据某些“标准”吗？&lt;/p&gt;
&lt;p&gt;用潘加宇老师的话来说，这样的需求既不符合系统的“愿景”，也不符合涉众的“需求”，系统的愿景绝对不是随机给用户返回一些数据，系统涉众（系统的买家）的需求也绝对不是你给他随机返回一些数据。&lt;/p&gt;
&lt;p&gt;作为一个志愿填报系统，系统的愿景一定是帮助用户填报志愿，指标可以是“更快的填报，更好的填报，更准的填报”，涉众的需求，也一定就是你的愿景。&lt;/p&gt;
&lt;h2&gt;阿布思考法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;在软件开发团队中，当有人提出新的想法时，经常会被马上否定“这太难了，这做不了”，最终得到一个平庸的、毫无竞争力的系统。学会像阿布一样思考，有助于克服普通人因资源受限而不敢展开想象的思维障碍。阿布思考法分两步：

（1）假设有充足的资源去解决问题，得到一个完美的方案；

（2）用手上现有的资源去山寨这个完美方案。

如果有一个方案，花费完美方案1%的资源，能达到完美方案20%的效果。这个方案已经是目前最好的方案了，因为它是在突破思维限制以后一步步往后退得来的。

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在我们平时讨论需求或者功能时，总是疏于讨论或者思考，也许这是国内软件公司发展时间较短带来统一的弊端。我们总是因为排期，或者上来就带入思路考虑实现的复杂度，来导致最后的方案不尽人意，其实这种思考方式只是从业者经验的浓缩，每个人的想法和理由都带着自己“私货”，产品经理一般没有能力和时间考虑的过于完整，项目经理在意项目的时间，研发经理在意实现的复杂度带来的系统问题，也在意项目的时间。&lt;/p&gt;
&lt;p&gt;但是这种考虑问题的角度，不利于公司的发展和软件价值的实现，这里就涉及到了“大部分企业中层领导的利益价值和企业整体的利益价值是矛盾的”这个比较大的问题。我们就不深入讨论了。但是站在软件系统本身的价值来看，我们应该使用阿布思考法来考虑问题，即：尽可能先得到一个完美的方案，再考虑其中无法实现的地方去山寨。&lt;/p&gt;
&lt;p&gt;我们举一个具体例子来看待这两种思考方式的区别，假设我们现在要研发一套自动驾驶技术的全套方案，按照阿布思考法，我们会从“如何才是完美的自动驾驶”这个点来考虑问题，考虑出需要的如31套各个细节子方案后，再依次对子方案找到目前最好的替代方案，最后得到了一套“目前时代下最好的自动驾驶系统”。&lt;/p&gt;
&lt;p&gt;如果使用通常的思考路径去考虑，会发现在一开始就寸步难行，我们不知道一套自动驾驶技术方案要包括哪些方案，就算去思考，得到的也是支离破碎的一些方案，甚至落地的时候才会发现：“哦，这里原来少了个这个东西，需要再考虑下这里怎么实现”。要么就干脆先照抄友商，然后在UI、交互等方面做点“微创新”。但是如果没有友商呢？如果你是二次创业呢？这种思考方式无法帮你领会到新的东西，也无法帮你找到终极的方向。&lt;/p&gt;
</content:encoded></item><item><title>贝叶斯概率以及贝叶斯公式</title><link>https://www.intotw.cn/posts/science-bayes/</link><guid isPermaLink="true">https://www.intotw.cn/posts/science-bayes/</guid><pubDate>Mon, 06 Feb 2023 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;贝叶斯概率以及贝叶斯公式&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;看到归纳推理，然后看到的这个概念，稍微了解了下，比较有意思也比较有意义，所以记录下&lt;/p&gt;
&lt;p&gt;写的比较好留着以后继续参考的文章https://www.zhihu.com/column/p/347194251&lt;/p&gt;
&lt;h2&gt;描述&lt;/h2&gt;
&lt;p&gt;总而言之，我们定义事件A的概率为P(A)，事件B的概率为P(B)，B发生的情况下A发生的概率为P(A|B)&lt;/p&gt;
&lt;p&gt;那么对于A发生的情况下B发生的概率，我们有该公式计算P(B|A)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/40d7477520c041b31e9de5086754530a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;举例说明，偷一段百度百科的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例如：一座别墅在过去的 20 年里一共发生过 2 次被盗，别墅的主人有一条狗，狗平均每周晚上叫 3 次，在盗贼入侵时狗叫的概率被估计为 0.9，问题是：在狗叫的时候发生入侵的概率是多少？
我们假设 A 事件为狗在晚上叫，B 为盗贼入侵，则以天为单位统计，P(A) = 3/7，P(B) = 2/(20*365) = 2/7300，P(A|B) = 0.9，按照公式很容易得出结果：P(B|A) = 0.9*(2/7300) / (3/7) = 0.00058

另一个例子，现分别有 A、B 两个容器，在容器 A 里分别有 7 个红球和 3 个白球，在容器 B 里有 1 个红球和 9 个白球，现已知从这两个容器里任意抽出了一个红球，问这个球来自容器 A 的概率是多少?
假设已经抽出红球为事件 B，选中容器 A 为事件 A，则有：P(B) = 8/20，P(A) = 1/2，P(B|A) = 7/10，按照公式，则有：P(A|B) = (7/10)*(1/2) / (8/20) = 0.875
贝叶斯公式为利用搜集到的信息对原有判断进行修正提供了有效手段。在采样之前，经济主体对各种假设有一个判断（先验概率），关于先验概率的分布，通常可根据经济主体的经验判断确定（当无任何信息时，一般假设各先验概率相同），较复杂精确的可利用包括最大熵技术或边际分布密度以及相互信息原理等方法来确定先验概率分布
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>线上排查：内存异常使用导致full gc频繁</title><link>https://www.intotw.cn/posts/work-oom-fullgc/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-oom-fullgc/</guid><pubDate>Mon, 06 Feb 2023 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线上排查：内存异常使用导致full gc频繁&lt;/h1&gt;
&lt;h2&gt;问题系统&lt;/h2&gt;
&lt;p&gt;日常巡检发现，应用线上出现频繁full gc&lt;/p&gt;
&lt;h2&gt;现象&lt;/h2&gt;
&lt;p&gt;应用线上出现频繁full gc
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/f7535c9b2774188c964a45982aff4924.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;排查过程&lt;/h2&gt;
&lt;h3&gt;分析dump&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;拉dump文件&lt;/strong&gt;&lt;/em&gt;：小插曲：dump时如果指定:live，则在dump前jvm会先进行一次full gc，并且gc log里会打印dump full gc，这种对非内存泄漏导致的线上异常内存情况排查反而会带来不便，导致我们多dump了好几次。
&lt;em&gt;&lt;strong&gt;分析dump文件&lt;/strong&gt;&lt;/em&gt;：
a. 发现大量long[]数组占用最大空间，有异常情况
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/a2ab0c54bb3a2a9ee8f9349a106fd9ce.png&quot; alt=&quot;&quot; /&gt;
b. 查看gc根节点，发现这些long[]数据大部分是被org.HdrHistogram.Histogram持有，每个Histogram对象会持有一个2048size的long[]
c. 查看Histogram实例的数量，竟然有5w个，对比下正常项目的堆栈，大约是100倍
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/0935f85b1ac11c3b6486b5e014a2449c.png&quot; alt=&quot;&quot; /&gt;
d. 这里又有一个插曲，一开始习惯用mat分析，但是mat生成的报告对分析泄露比较有用，对于分析异常的内存没有jvisualvm.exe和idea的profiler好用&lt;/p&gt;
&lt;h3&gt;排查原因&lt;/h3&gt;
&lt;p&gt;本地启动，可以复现这个类的内存使用情况，于是本地起一个其他内存正常的服务与有问题的应用，分析内存对比&lt;/p&gt;
&lt;p&gt;这里用的是idea的profiler，很方便&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;发现差异：&lt;/strong&gt;&lt;/em&gt;
对比正常的应用，发现异常应用的引用存在异常的来自
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/ff173d77b5c2e212eff1fbb12ea8a301.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/679db13dacc68c251b109e5090ad50a3.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;● rx.internal.operators.OnSubscribeReduceSeed$ReduceSeedSubscriber的引用，怀疑就是这个异常引用就是导致这些实例无法在新生代回收而是堆积到了老年代触发full gc的原因&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;排查差异&lt;/strong&gt;&lt;/em&gt;：
简单看了下相关代码，看不出个所以然，直接debug对比
系统确实走进了相关的代码，增加了对Histogram的引用，而正常应用没有
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/8c6defffaef6d1233c1d8cb0a88da6a6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是光这样也看不出来为什么，此时关注到了左下角的线程池，这个线程池比较奇怪，是Metric的线程池&lt;/p&gt;
&lt;p&gt;Metric是Hystrix用来统计相关指标，来供自己的dashboard或者用户来获取，以此来了解系统熔断相关参数和指标的功能
再看堆栈，走到这里的逻辑是
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/6bc5d0211a6bc50b6dff501a2f2190be.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个流用来统计单位时间内的系统指标，导致Hystrix使用Histogram的long数组实现类似滑动窗口的效果统计单位时间内的指标&lt;/p&gt;
&lt;p&gt;Histogram本身是Hystrix用来实现类似桶+滑动窗口的功能，来统计单位时间内的流量，但是因为开启了指标参数，导致hystrix为了统计更长时间范围内的指标，新增了对象持有更多（单位时间内）的Histogram引用来聚合，这部分引用因为是统计更长时间范围周期的，就会因为引用持有时间长而到老年代，但是本质并不是内存泄漏，所以每次full gc后又可以得到回收&lt;/p&gt;
&lt;h3&gt;解决问题&lt;/h3&gt;
&lt;p&gt;看到上面的差异和怪异的线程池，第一反应就是关闭metric使应用不走到这段逻辑中增加引用，看官方文档，该配置默认是打开的，并且确认该功能只影响指标统计不影响断路器本身功能，使用配置hystrix.metrics.enabled=false配置来关闭&lt;/p&gt;
&lt;p&gt;新增配置后，验证并查看堆栈，引用恢复正常，并且系统在一段时间后并没有新增更多的Histogram实例，发布线上后观察一段时间，full gc问题确实得到解决&lt;/p&gt;
&lt;h2&gt;根本原因&lt;/h2&gt;
&lt;p&gt;在当时发现解决的办法并验证后，并没有时间去研究hystrix.metrics.enabled默认配置就是true但是其他应用没有出现这个full gc问题的原因， 先解决了之后后续再继续跟进排查根本原因防止其他项目也出现相同问题&lt;/p&gt;
&lt;p&gt;之前发现可疑的线程池是HystrixMetricsPoller ，经过查看，该线程池由HystrixMetricsPollerConfiguration
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/54b26643d00b7a82dce65a5aaf58115e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;类开启，主要依靠hystrix.metrics.enabled开启，但是默认是true，为什么其他项目没有开启呢？&lt;/p&gt;
&lt;p&gt;搜了下源码，这个类的开启还和一个注解有关
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/aec42dae1b840c7b2d17271d8a3e0a1a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对比了一下代码，果然只有异常的应用使用了这个注解，这个注解的目的是开启断路器&lt;/p&gt;
&lt;p&gt;但是研究之后发现，不使用这个注解，熔断等功能依旧可用，原因是在spring-cloud高版本之后，spring通过使用hystrix封装openfeign的方法来使用熔断，而不是集成整个hystrix体系，可能spring-cloud也发现了hystrix内存使用上的问题&lt;/p&gt;
&lt;p&gt;所以在较高版本（起码我们的版本），feign是通过feign.hystrix.enabled来开关断路器的（这个开关是关闭的话，单纯加@EnableCircuitBreaker注解断路器是不会生效的）&lt;/p&gt;
&lt;p&gt;其实在更高点版本的spring-cloud中，@EnableCircuitBreaker这个注解已经被标注为废弃了，但是可能因为我们是中间版本，所以存在既没有标注废弃其实又没有什么用的情况&lt;/p&gt;
&lt;p&gt;总而言之，feign的断路功能只通过feign.hystrix.enabled来控制，增加@EnableCircuitBreaker注解之后仅仅只是会开启Hystrix其他所有的指标等功能&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/6770df3c043e964bc12a566e58604196.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题总结&lt;/h2&gt;
&lt;h3&gt;问题根本原因&lt;/h3&gt;
&lt;p&gt;本次问题产生的根本原因是因为开启了@EnableCircuitBreaker注解，开启了Hystrix指标功能，导致Histogram实例大量进入老年代，只有full gc才可以回收&lt;/p&gt;
&lt;p&gt;Histogram本身是Hystrix用来实现类似桶+滑动窗口的功能，来统计单位时间内的流量，但是因为开启了指标参数，导致hystrix为了统计更长时间范围内的指标，新增了对象持有更多（单位时间内）的Histogram引用来聚合，这部分引用因为是统计更长时间范围周期的，在访问量上升新生代复制速度变快时，就会因为引用持有时间长而到老年代，但是本质并不是内存泄漏，所以每次full gc后又可以得到回收&lt;/p&gt;
&lt;h3&gt;后续关注点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Spring-Cloud本身体系比较复杂，因为和Netfilx套件纠缠不清加上很多历史原因，能用明白某一个版本的就很不错了&lt;/li&gt;
&lt;li&gt;开发本身并不了解这个版本断路器到底怎么开启，没有仔细看过对应版本的官方文档就去使用注解，在老版本，断路器确实是通过这个注解才能启用的&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;解决方式&lt;/h3&gt;
&lt;p&gt;关闭metric功能或者去掉@EnableCircuitBreaker注解均可解决&lt;/p&gt;
&lt;p&gt;百度spring-cloud教程和文档时，一定一定一定要看对应版本的，否则可能加一堆配置解决某个问题，结果开启一大堆乱七八糟的功能&lt;/p&gt;
</content:encoded></item><item><title>使用OpenCv+Arduino实现挂机自动打怪</title><link>https://www.intotw.cn/posts/opencv-arduino/</link><guid isPermaLink="true">https://www.intotw.cn/posts/opencv-arduino/</guid><pubDate>Wed, 20 Jul 2022 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;使用OpenCv+Arduino实现挂机自动打怪&lt;/h1&gt;
&lt;p&gt;最近在玩某网游，练级十分枯燥和缓慢，就是挂机刷刷刷，所以研究一下自动化，找了个可以原地挂机刷怪的职业，然后用OpenCv检测技能冷却，冷却好了通过串口通知Arduino按下模拟键盘按键释放技能&lt;/p&gt;
&lt;p&gt;大概流程如下：&lt;/p&gt;
&lt;p&gt;OpenCv定时扫描屏幕-&amp;gt;对技能区域截图-&amp;gt;对比预设图判断技能冷却-&amp;gt;按键事件加入队列&lt;/p&gt;
&lt;p&gt;消费队列-&amp;gt;取出按键事件-&amp;gt;通过串口通知Arduino按下按键释放技能&lt;/p&gt;
&lt;h2&gt;OpenCv代码&lt;/h2&gt;
&lt;h3&gt;截图部分&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public static void ShotFrame(String fileName) throws AWTException, IOException {
        Integer x = Integer.valueOf(PropertiesHelper.getValue(窗口X));

        Integer y = Integer.valueOf(PropertiesHelper.getValue(窗口Y));

        Integer height = Integer.valueOf(PropertiesHelper.getValue(窗口高度));

        Integer width = Integer.valueOf(PropertiesHelper.getValue(窗口宽度));

        //创建一个robot对象
        Robot robut = new Robot();
        //获取屏幕分辨率
        //打印屏幕分辨率
        //创建该分辨率的矩形对象
        Dimension act=new Dimension();
        act.setSize(width,height);
        Point point=new Point(x,y);
        Rectangle screenRect = new Rectangle(point,act);
        //根据这个矩形截图
        BufferedImage bufferedImage = robut.createScreenCapture(screenRect);
        //保存截图
        File file = new File(&quot;D:\\maplePic\\&quot;+fileName+&quot;.jpg&quot;);
        ImageIO.write(bufferedImage, &quot;jpg&quot;, file);
    }
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OpenCv比较&lt;/h3&gt;
&lt;p&gt;这里用的是bytedeco的javacv,一个封装了OpenCv方法的库，Java用原生OpenCv比较麻烦，推荐用这个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * OpenCV-4.0.0 直方图比较
     *
     * @return: double 两张图片的相似值
     */
    public static double compareHist_2(String resource, String now) {
        Mat src_1 = imread(&quot;D:\\maplePic\\&quot;+resource+&quot;.jpg&quot;);// 图片 1
        Mat src_2 = imread(&quot;D:\\maplePic\\&quot;+now+&quot;.jpg&quot;);// 图片 2

        Mat hvs_1 = new Mat();
        Mat hvs_2 = new Mat();
        //图片转HSV
        cvtColor(src_1, hvs_1,COLOR_BGR2HSV);
        cvtColor(src_2, hvs_2,COLOR_BGR2HSV);

        Mat hist_1 = new Mat();
        Mat hist_2 = new Mat();


        final int[] channels = new int[]{0, 1, 2};

        final int[] histSize = new int[]{8, 8, 8};
        final float[] histRange = new float[]{0f, 255f};
        IntPointer intPtrChannels = new IntPointer(channels);
        IntPointer intPtrHistSize = new IntPointer(histSize);
        final PointerPointer&amp;lt;FloatPointer&amp;gt; ptrPtrHistRange = new PointerPointer&amp;lt;&amp;gt;(histRange, histRange, histRange);
        calcHist(hvs_1, 1, intPtrChannels, new Mat(), hist_1, 3, intPtrHistSize, ptrPtrHistRange, true, false);
        calcHist(hvs_2, 1, intPtrChannels, new Mat(), hist_2, 3, intPtrHistSize, ptrPtrHistRange, true, false);


        //图片归一化
        normalize(hist_1, hist_1, 1, hist_1.rows() , NORM_MINMAX, -1, new Mat() );
        normalize(hist_2, hist_2, 1, hist_2.rows() , NORM_MINMAX, -1, new Mat() );

        //直方图比较
        return compareHist(hist_1,hist_2,CV_COMP_CORREL);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;串口工具类&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public class SerialPortTool {

    /**
     * 查找电脑上所有可用 com 端口
     *
     * @return 可用端口名称列表，没有时 列表为空
     */
    public static final ArrayList&amp;lt;String&amp;gt; findSystemAllComPort() {
        /**
         *  getPortIdentifiers：获得电脑主板当前所有可用串口
         */
        Enumeration&amp;lt;CommPortIdentifier&amp;gt; portList = CommPortIdentifier.getPortIdentifiers();
        ArrayList&amp;lt;String&amp;gt; portNameList = new ArrayList&amp;lt;&amp;gt;();

        /**
         *  将可用串口名添加到 List 列表
         */
        while (portList.hasMoreElements()) {
            String portName = portList.nextElement().getName();//名称如 COM1、COM2....
            portNameList.add(portName);
        }
        return portNameList;
    }

    /**
     * 打开电脑上指定的串口
     *
     * @param portName 端口名称，如 COM1，为 null 时，默认使用电脑中能用的端口中的第一个
     * @param b        波特率(baudrate)，如 9600
     * @param d        数据位（datebits），如 SerialPort.DATABITS_8 = 8
     * @param s        停止位（stopbits），如 SerialPort.STOPBITS_1 = 1
     * @param p        校验位 (parity)，如 SerialPort.PARITY_NONE = 0
     * @return 打开的串口对象，打开失败时，返回 null
     */
    public static final SerialPort openComPort(String portName, int b, int d, int s, int p) {
        CommPort commPort = null;
        try {
            //当没有传入可用的 com 口时，默认使用电脑中可用的 com 口中的第一个
            if (portName == null || &quot;&quot;.equals(portName)) {
                List&amp;lt;String&amp;gt; comPortList = findSystemAllComPort();
                if (comPortList != null &amp;amp;&amp;amp; comPortList.size() &amp;gt; 0) {
                    portName = comPortList.get(0);
                }
            }
//            System.out.println(&quot;开始打开串口：portName=&quot; + portName + &quot;,baudrate=&quot; + b + &quot;,datebits=&quot; + d + &quot;,stopbits=&quot; + s + &quot;,parity=&quot; + p);
            //通过端口名称识别指定 COM 端口
            CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
            /**
             * open(String TheOwner, int i)：打开端口
             * TheOwner 自定义一个端口名称，随便自定义即可
             * i：打开的端口的超时时间，单位毫秒，超时则抛出异常：PortInUseException if in use.
             * 如果此时串口已经被占用，则抛出异常：gnu.io.PortInUseException: Unknown Application
             */
            commPort = portIdentifier.open(portName, 5000);
            /**
             * 判断端口是不是串口
             * public abstract class SerialPort extends CommPort
             */
            if (commPort instanceof SerialPort) {
                SerialPort serialPort = (SerialPort) commPort;
                /**
                 * 设置串口参数：setSerialPortParams( int b, int d, int s, int p )
                 * b：波特率（baudrate）
                 * d：数据位（datebits），SerialPort 支持 5,6,7,8
                 * s：停止位（stopbits），SerialPort 支持 1,2,3
                 * p：校验位 (parity)，SerialPort 支持 0,1,2,3,4
                 * 如果参数设置错误，则抛出异常：gnu.io.UnsupportedCommOperationException: Invalid Parameter
                 * 此时必须关闭串口，否则下次 portIdentifier.open 时会打不开串口，因为已经被占用
                 */
                serialPort.setSerialPortParams(b, d, s, p);
//                System.out.println(&quot;打开串口 &quot; + portName + &quot; 成功...&quot;);
                return serialPort;
            } else {
                System.out.println(&quot;当前端口 &quot; + commPort.getName() + &quot; 不是串口...&quot;);
            }
        } catch (NoSuchPortException e) {
            e.printStackTrace();
        } catch (PortInUseException e) {
            System.out.println(&quot;串口 &quot; + portName + &quot; 已经被占用，请先解除占用...&quot;);
            e.printStackTrace();
        } catch (UnsupportedCommOperationException e) {
            System.out.println(&quot;串口参数设置错误，关闭串口，数据位[5-8]、停止位[1-3]、验证位[0-4]...&quot;);
            e.printStackTrace();
            if (commPort != null) {//此时必须关闭串口，否则下次 portIdentifier.open 时会打不开串口，因为已经被占用
                commPort.close();
            }
        }
        System.out.println(&quot;打开串口 &quot; + portName + &quot; 失败...&quot;);
        return null;
    }

    /**
     * 往串口发送数据
     *
     * @param serialPort 串口对象
     * @param order      待发送数据
     */
    public static void sendDataToComPort(SerialPort serialPort, byte[] orders) {
        OutputStream outputStream = null;
        try {
            if (serialPort != null) {
                outputStream = serialPort.getOutputStream();
                outputStream.write(orders);
                outputStream.flush();
//                System.out.println(&quot;往串口 &quot; + serialPort.getName() + &quot; 发送数据：&quot; + Arrays.toString(orders) + &quot; 完成...&quot;);
            } else {
                System.out.println(&quot;gnu.io.SerialPort 为null，取消数据发送...&quot;);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 往串口发送数据
     *
     * @param serialPort 串口对象
     */
    public static String readDataToComPort(SerialPort serialPort) {

        try (InputStream inputStream = serialPort.getInputStream(); ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()) {
            byte[] a=new byte[1024];
            while (inputStream.read(a)&amp;gt;0){
                byteArrayOutputStream.write(a);
            }
            return byteArrayOutputStream.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return &quot;&quot;;
    }

    /**
     * 关闭串口
     *
     * @param serialport 待关闭的串口对象
     */
    public static void closeComPort(SerialPort serialPort) {
        if (serialPort != null) {
            serialPort.close();
//            System.out.println(&quot;关闭串口 &quot; + serialPort.getName());
        }
    }

    /**
     * 16进制字符串转十进制字节数组
     * 这是常用的方法，如某些硬件的通信指令就是提供的16进制字符串，发送时需要转为字节数组再进行发送
     *
     * @param strSource 16进制字符串，如 &quot;455A432F5600&quot;，每两位对应字节数组中的一个10进制元素
     *                  默认会去除参数字符串中的空格，所以参数 &quot;45 5A 43 2F 56 00&quot; 也是可以的
     * @return 十进制字节数组, 如 [69, 90, 67, 47, 86, 0]
     */
    public static byte[] hexString2Bytes(String strSource) {
        if (strSource == null || &quot;&quot;.equals(strSource.trim())) {
            System.out.println(&quot;hexString2Bytes 参数为空，放弃转换.&quot;);
            return null;
        }
        strSource = strSource.replace(&quot; &quot;, &quot;&quot;);
        int l = strSource.length() / 2;
        byte[] ret = new byte[l];
        for (int i = 0; i &amp;lt; l; i++) {
            ret[i] = Integer.valueOf(strSource.substring(i * 2, i * 2 + 2), 16).byteValue();
        }
        return ret;
    }

    public static void main(String[] args) {
//        //发送普通数据
        byte[] bytes = new byte[]{0x31};
        SerialPort serialPort = SerialPortTool.openComPort(&quot;COM5&quot;, 9600, 8, 1, 0);
        SerialPortTool.sendDataToComPort(serialPort, bytes);
        String s = SerialPortTool.readDataToComPort(serialPort);
        System.out.println(s);
        SerialPortTool.closeComPort(serialPort);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;主流程函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; public static void main(String[] args) {
        //每秒一次
        Timer timer = new Timer();
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                try {
                    lhbrother();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (AWTException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        // 第一次任务延迟时间
        long delay = 2000;

        // 任务执行频率
        long period = 1000;

        // 开始调度
        timer.schedule(timerTask, delay, period);

    }
    public static void lhbrother() throws IOException, AWTException, InterruptedException {
        //截图生成结果
        ScreenShotUtil.Shot1(&quot;n1&quot;);
        if(OpenCvUtil.compareHist_2(&quot;n1&quot;,&quot;1&quot;)&amp;gt;0.90){
            System.out.println(&quot;1冷却完毕&quot;);
            Random random=new Random();
            Thread.sleep(random.nextInt(500)+500);
            //这里发送的是ascii码，Arduino读取串口后直接KeyBorad.Press读到的内容即可。
            //因为ascii码没有上下左右等方向键，所以如果需要控制上下走动，可以使用一个特殊的ascii码，然后在Arduino代码里特殊判断下
            byte[] bytes = new byte[]{0x31};
            SerialPort serialPort = SerialPortTool.openComPort(&quot;COM5&quot;, 9600, 8, 1, 0);
            SerialPortTool.sendDataToComPort(serialPort, bytes);
            String s = SerialPortTool.readDataToComPort(serialPort);
            System.out.println(s);
            SerialPortTool.closeComPort(serialPort);
            return ;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>经验总结：内存泄露的原因以及分析</title><link>https://www.intotw.cn/posts/work-summary-oom/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-summary-oom/</guid><pubDate>Wed, 16 Mar 2022 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;内存泄露是Javaer听到最多的关于内存的事了，这篇文章就来谈谈这件事。&lt;/p&gt;
&lt;h2&gt;内存泄露与资源泄露&lt;/h2&gt;
&lt;p&gt;什么是泄露？泄露在计算机语境下，通常指的是某个资源无法被访问，也无法被释放。&lt;/p&gt;
&lt;p&gt;内存泄露一般发生在某个对象的引用丢失，无法再访问到该引用，但是该引用却依旧引用着某个对象，导致这个对象无法回收，最终导致内存溢出OOM。&lt;/p&gt;
&lt;p&gt;资源泄露一般发生在连接池，IO流等场景，如从连接池中每次都新建连接但不关闭，每次都打开新的IO流但不关闭，等等情况。&lt;/p&gt;
&lt;h2&gt;内存泄露发生的情况&lt;/h2&gt;
&lt;p&gt;内存泄露多发生于static的集合中，比如当你定义了一个static HashMap，此时将某个key-value放入其中后，方法段结束。&lt;/p&gt;
&lt;p&gt;这时，除非调用map的clear方法，否则显然该value将无限持有对象的引用，无法释放。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static Map&amp;lt;String,Object&amp;gt; objectMap=new HashMap&amp;lt;&amp;gt;();
public static void main(String[] args) {
    Integer a=new Integer(1);
    objectMap.put(&quot;testKey&quot;,a);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种写法看似可笑，却很难避免，尤其在大量框架代码中，反而更容易发生，因为大部分框架代码对业务代码的增强，都是通过AOP方式来做的，此时对业务代码来说，这类隐式的static Map难以防范。&lt;/p&gt;
&lt;h2&gt;内存使用过高，一定是内存泄露吗？&lt;/h2&gt;
&lt;p&gt;内存使用过高，并不一定是内存泄露导致的结果，具体要看内存堆的分析。&lt;/p&gt;
&lt;p&gt;一般内存泄露最直观的体现就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内存使用高&lt;/li&gt;
&lt;li&gt;GC回收不了内存，即GC前后堆大小几乎无变化&lt;/li&gt;
&lt;li&gt;JVM疯狂GC,CPU打满&lt;/li&gt;
&lt;li&gt;Java进程触发Linux操作系统的OOM-killer，Java进程被杀死&lt;/li&gt;
&lt;li&gt;或者CPU被GC任务打满，服务器实际宕机。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是这不一定是泄露导致的，也有可能是内存的错误使用导致的，不过大同小异，主要还是需要排查异常内存的使用。&lt;/p&gt;
&lt;p&gt;Ps：之所以作者这么说，是因为作者曾经在线上遇到了架构组修改日志框架，错误的将日志内容作为了key存入了map，本应的key-value应该为traceId-日志内容，结果架构组却将key-value搞反了，导致大量的巨大key打满了内存，堆dump文件里全是几十k几十k的字符串。&lt;/p&gt;
&lt;h2&gt;如何避免内存泄露&lt;/h2&gt;
&lt;p&gt;根据上面说的内存泄露多数发生的情况，避免内存泄露的策略也就十分简单了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;尽量使用局部变量&lt;/li&gt;
&lt;li&gt;减少使用static集合&lt;/li&gt;
&lt;li&gt;如果必要的使用static集合，尽量使用弱引用等低级引用。比如参照ThreadLocal中的设计：&lt;a href=&quot;https://intotw.cn/posts/java-threadlocal/&quot;&gt;TheadLocal原理&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;内存泄露问题如何排查&lt;/h2&gt;
&lt;p&gt;内存泄露或内存持续使用较高时，通常通过堆的情况来排查。&lt;/p&gt;
&lt;p&gt;首先可以通过jmap -histo:live pid|less 命令，查看堆内对象使用情况。此时如果内存泄露，一般都是会某个基本类型对象过多，然后可以与正常的服务作对比，看哪个对象的数量异常的多，此时如果可以判断出来，也没必要dump了。&lt;/p&gt;
&lt;p&gt;如果通过jmap无法断定，则可以使用jmap -dump:live,format=b,file=&amp;lt;filename&amp;gt; 命令，生成dump文件。&lt;/p&gt;
&lt;p&gt;将dump文件通过java原生的软件或者eclipse的mat工具，就可以看到哪些对象占用过多，此时你应该关注的是非基本类型对象的其他对象，因为一般来说都是基本类型的数量和大小最多。&lt;/p&gt;
&lt;p&gt;一般来说，你会看到以下现象：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;某个map的Node十分多，有几十万个。&lt;/li&gt;
&lt;li&gt;某个框架的某个对象十分多。&lt;/li&gt;
&lt;li&gt;char数据，也就是C[]，占用十分多，因为有很多大字符串。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>TransmittableThreadLocal解决线程池变量传递以及原理解析</title><link>https://www.intotw.cn/posts/java-ttl-threadpool/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-ttl-threadpool/</guid><pubDate>Thu, 06 May 2021 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;TransmittableThreadLocal解决线程池变量传递以及原理解析&lt;/h1&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;TransmittableThreadLocal是alibaba提供的一个工具包中的类，主要作用就是解决线程池场景下的变量传递问题。继承自InheritableThreadLocal，我们知道
InheritableThreadLocal解决了主线程与子线程之间的变量传递问题，但是在遇到线程池以及线程复用的情况下，就无能为力了，TransmittableThreadLocal通过对InheritableThreadLocal以及线程池的增强，解决了这个问题。&lt;/p&gt;
&lt;p&gt;主要用途：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;就是应用中对于连接池中的全局链路追踪。&lt;/li&gt;
&lt;li&gt;解决例如Hystrix中出现的ThreadLocal无法传递的问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;使用&lt;/h1&gt;
&lt;h2&gt;依赖&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;transmittable-thread-local&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.11.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;示例代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class TestTTL {
    //定义一个线程池执行ttl，这里必须要用TTL线程池封装
    private static ExecutorService TTLExecutor = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));
    //定义另外一个线程池循环执行，模拟业务场景下多Http请求调用的情况
    private static ExecutorService loopExecutor = Executors.newFixedThreadPool(5);
    private static AtomicInteger i=new AtomicInteger(0);
    //TTL的ThreadLocal
    private static ThreadLocal tl = new TransmittableThreadLocal&amp;lt;&amp;gt;(); //这里采用TTL的实现
    public static void main(String[] args) {

        while (true) {
            /*
             这里就是循环执行10次，每次对数值加1并设置到threadlocal中，然后再使用TTL去执行来打印这个值。
             这里外部为什么使用线程池，是为了证明TTL确实可以达到我们想要的效果：即线程池中多任务带着
             父线程各自的ThreadLocal运行互不影响
            */
            loopExecutor.execute( () -&amp;gt; {
                if(i.get()&amp;lt;10){
                    tl.set(i.getAndAdd(1));
                    TTLExecutor.execute(() -&amp;gt; {
                        System.out.println(String.format(&quot;子线程名称-%s, 变量值=%s&quot;, Thread.currentThread().getName(), tl.get()));
                    });
                }
            });
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/352e3f6f05bb8890a2f55a808f8fe789.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;原理&lt;/h1&gt;
&lt;p&gt;TransmittableThreadLocal执行的关键原理在于以下几个类做了几件事：&lt;/p&gt;
&lt;h2&gt;TransmittableThreadLocal&lt;/h2&gt;
&lt;p&gt;TransmittableThreadLocal本身增加一个静态的holderMap，里面保存了所有使用过的TransmittableThreadLocal作为key的引用，这样在复制TransmittableThreadLocal的值到线程本身的ThreadLocal时，就可以通过该holder遍历到所有的TransmittableThreadLocal。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    可以看到，在TransmittableThreadLocal调用get和set方法时，都会将自己作为key放入holder，以便后续复制时遍历，
    这个holder其实就是一个储存全局TransmittableThreadLocal的集合，不过他不是通过手动add的，
    而是通过耦合到TransmittableThreadLocal的方法中自动的去增加。
*/
public final void set(T value) {
        super.set(value);
        if (null == value) {
            this.removeValue();
        } else {
            this.addValue();
        }
}
private void addValue() {
        if (!((WeakHashMap)holder.get()).containsKey(this)) {
            ((WeakHashMap)holder.get()).put(this, (Object)null);
        }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Transmitter和Snapshot&lt;/h2&gt;
&lt;p&gt;这两个都是TransmittableThreadLocal的内部类，前者主要是一些工具方法，后者包含了2个map，ttl2Value和threadLocal2Value，分别存储ttl设置的ThreadLocal值和父线程中其他的ThreadLocal值。其中Snapshot是一个快照，用作备份还原现场使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//Transmitter
/*
    可以看到这里就是通过holder来遍历所有TTLocal，以此来复制值到下面的Runable中。
*/
 private static WeakHashMap&amp;lt;TransmittableThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; captureTtlValues() {
            WeakHashMap&amp;lt;TransmittableThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; ttl2Value = new WeakHashMap();
            Iterator var1 = ((WeakHashMap)TransmittableThreadLocal.holder.get()).keySet().iterator();

            while(var1.hasNext()) {
                TransmittableThreadLocal&amp;lt;Object&amp;gt; threadLocal = (TransmittableThreadLocal)var1.next();
                ttl2Value.put(threadLocal, threadLocal.copyValue());
            }

            return ttl2Value;
}
//这个方法会复制一个快照返回，实际调用会将调用线程的2类ThreadLocal值复制为一个快照给Runable使用
@NonNull
public static Object capture() {
    return new TransmittableThreadLocal.Transmitter.Snapshot(captureTtlValues(), captureThreadLocalValues());
}

//方法名就是重放，就是将快照的值，copy到执行线程中。
@NonNull
public static Object replay(@NonNull Object captured) {
    TransmittableThreadLocal.Transmitter.Snapshot capturedSnapshot = (TransmittableThreadLocal.Transmitter.Snapshot)captured;
    return new TransmittableThreadLocal.Transmitter.Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
//还原现场，在执行实际runnable之后，将执行前的备份，copy回执行线程
public static void restore(@NonNull Object backup) {
        TransmittableThreadLocal.Transmitter.Snapshot backupSnapshot = (TransmittableThreadLocal.Transmitter.Snapshot)backup;
        restoreTtlValues(backupSnapshot.ttl2Value);
        restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//一个快照类，保存ttl和原生threadlocal的值
private static class Snapshot {
        final WeakHashMap&amp;lt;TransmittableThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; ttl2Value;
        final WeakHashMap&amp;lt;ThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; threadLocal2Value;

        private Snapshot(WeakHashMap&amp;lt;TransmittableThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; ttl2Value, WeakHashMap&amp;lt;ThreadLocal&amp;lt;Object&amp;gt;, Object&amp;gt; threadLocal2Value) {
            this.ttl2Value = ttl2Value;
            this.threadLocal2Value = threadLocal2Value;
        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TtlRunnable&lt;/h2&gt;
&lt;p&gt;这个类就是前面使用TTL封装线程池的意义，TTL封装线程池，重写了其中的sumbit和execute等方法，使得提交到线程池中的实际Runable是封装过后的TtlRunnable，并且该类完成了复制ThreadLocal值和还原现场等操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ExecutorServiceTtlWrapper，封装线程池
/*
    这里面的TtlCallable.get就会return一个new的TtlRunnable
*/
@NonNull
    public &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(@NonNull Callable&amp;lt;T&amp;gt; task) {
        return this.executorService.submit(TtlCallable.get(task));
    }

    @NonNull
    public &amp;lt;T&amp;gt; Future&amp;lt;T&amp;gt; submit(@NonNull Runnable task, T result) {
        return this.executorService.submit(TtlRunnable.get(task), result);
    }

    @NonNull
    public Future&amp;lt;?&amp;gt; submit(@NonNull Runnable task) {
        return this.executorService.submit(TtlRunnable.get(task));
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// TtlRunnable，封装线程池
public final class TtlRunnable implements Runnable, TtlEnhanced, TtlAttachments {
    /* 
        这个变量是最核心的变量，在初始化时，会调用Transmitter，
        将父线程或者说调用线程的TTLThreadLocal和原生ThreadLocal中的值copy出一个快照，
        即上面的SnapShot，并且在这里持有那个SnapShot的引用，
        注意，这里的构造函数初始化，是在调用线程里执行的，所以拿到的就是调用线程的ThreadLocal值。
    */
    private final AtomicReference&amp;lt;Object&amp;gt; capturedRef = new AtomicReference(Transmitter.capture());
    //其他代码省略

    //实际执行函数
    public void run() {
        //获取上面的那个快照值，此时已经在线程池中执行了
        Object captured = this.capturedRef.get();
        if (captured != null &amp;amp;&amp;amp; (!this.releaseTtlValueReferenceAfterRun || this.capturedRef.compareAndSet(captured, (Object)null))) {
            //调用工具类，此时我们有了快照，并且已经在当前线程池中执行了，所以把快照的值全部赋值到当前线程池中的ThreadLocal中去，下一步被包装的Runable执行run时，就是无感的了。
            Object backup = Transmitter.replay(captured);

            try {
                this.runnable.run();
            } finally {
                //执行完成后，我们需要将该线程的ThreadLocal还原回之前的快照，因为在线程池中，线程可能复用，为了防止在runnable执行中对该线程的ThreadLocal产生了污染，然后该线程被复用去执行其他Runable时该值已被修改，不再是调用线程的值了，所以需要还原现场。
                Transmitter.restore(backup);
            }

        } else {
            throw new IllegalStateException(&quot;TTL value reference is released after run!&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用两张图可以更加详细的说明执行的过程。我们给前面的两个线程池中的线程分别命个名，然后debug代码。&lt;/p&gt;
&lt;p&gt;我们debug到TtlRunnable初始化，可以看到此时初始化的线程是loopExecutor，也就是调用线程，理所当然此时可以制作一个调用线程的ThreadLocal快照。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/5df52758ba1ef09de391a67900963353.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们debug到runnable执行时走到的replay方法，此时执行的线程就是TTL线程，也就是线程池中的线程了，此时我们当然也可以将之前保存的快照，来赋值到线程池中该线程了，此后的还原也是同理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/d99ac0dc3175f3e317dfbaf3fce31c2b.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;该工具类解决了特定场景下的需求，实现方式核心就是封装+快照。非常值得学习。不过在实际中也不建议在ThreadLocal值过多或者较大时频繁使用，因为会产生过多的SnapShot临时对象增加gc负担，并且每次线程执行都会带来更多的copy和还原负担。&lt;/p&gt;
</content:encoded></item><item><title>改造apollo配置中心客户端支持从Spring读取配置</title><link>https://www.intotw.cn/posts/java-apollo2spring/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-apollo2spring/</guid><pubDate>Sun, 11 Apr 2021 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;主要问题&lt;/h2&gt;
&lt;p&gt;Apollo客户端的配置主要集中在2部分，这就导致割裂了配置，十分不便：&lt;/p&gt;
&lt;h3&gt;ApolloApplicationContextInitializer&lt;/h3&gt;
&lt;p&gt;在spring-boot环境下，该类中，通过对配置项apollo.bootstrap.enabled的开启，来达到Apollo在spring-boot环境下的autoconfig，其中主要是initialize方法中，对于指定配置，从spring的properties中加载到system.property里去，因为Apollo本身所有配置都是支持system.property的，所以通过这么实现，可以对原有的配置加载体系没有侵入。
关键代码如下（部分无关代码已省略）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ApolloApplicationContextInitializer implements
    ApplicationContextInitializer&amp;lt;ConfigurableApplicationContext&amp;gt; , EnvironmentPostProcessor, Ordered {
  //对应的会被从spring.properties中转移到system.property中的参数名
  private static final String[] APOLLO_SYSTEM_PROPERTIES = {&quot;app.id&quot;, ConfigConsts.APOLLO_CLUSTER_KEY,
      &quot;apollo.cacheDir&quot;, &quot;apollo.accesskey.secret&quot;, ConfigConsts.APOLLO_META_KEY, PropertiesFactory.APOLLO_PROPERTY_ORDER_ENABLE};
  //实现spring提供的接口，里面进行配置的转移
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) {

    // should always initialize system properties like app.id in the first place
    //主要就是这个方法
    initializeSystemProperty(configurableEnvironment);

    Boolean eagerLoadEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, Boolean.class, false);

    //EnvironmentPostProcessor should not be triggered if you don&apos;t want Apollo Loading before Logging System Initialization
    if (!eagerLoadEnabled) {
      return;
    }

    Boolean bootstrapEnabled = configurableEnvironment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, Boolean.class, false);

    if (bootstrapEnabled) {
      initialize(configurableEnvironment);
    }

  }
  //可以看到只操作了final static数组里定义的那个列表
  void initializeSystemProperty(ConfigurableEnvironment environment) {
    for (String propertyName : APOLLO_SYSTEM_PROPERTIES) {
      fillSystemPropertyFromEnvironment(environment, propertyName);
    }
  }
  //直接把存在的参数加到系统参数中去，有点暴力
  private void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) {
    if (System.getProperty(propertyName) != null) {
      return;
    }

    String propertyValue = environment.getProperty(propertyName);

    if (Strings.isNullOrEmpty(propertyValue)) {
      return;
    }

    System.setProperty(propertyName, propertyValue);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DefaultProviderManager&lt;/h3&gt;
&lt;p&gt;另一部分参数，即env以及apollo.configService这两个主要参数，通过DefaultProviderManager类中对DefaultServerProvider的加载实现，这两个类都是Apollo自定义的类，主要功能仅支持从系统变量，jvm启动参数，指定文件目录读取这两个配置。
主要实现代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultServerProvider implements ServerProvider {
  private static final Logger logger = LoggerFactory.getLogger(DefaultServerProvider.class);
  private static final String SERVER_PROPERTIES_LINUX = &quot;/opt/settings/server.properties&quot;;
  private static final String SERVER_PROPERTIES_WINDOWS = &quot;C:/opt/settings/server.properties&quot;;

  private String m_env;
  private String m_dc;

  private Properties m_serverProperties = new Properties();

  //可以看到主要就是从2个固定的目录去读一个特定的文件，如果读不到，后续就是从系统变量和jvm取了
  @Override
  public void initialize() {
    try {
      String path = Utils.isOSWindows() ? SERVER_PROPERTIES_WINDOWS : SERVER_PROPERTIES_LINUX;

      File file = new File(path);
      if (file.exists() &amp;amp;&amp;amp; file.canRead()) {
        logger.info(&quot;Loading {}&quot;, file.getAbsolutePath());
        FileInputStream fis = new FileInputStream(file);
        initialize(fis);
        return;
      }

      initialize(null);
    } catch (Throwable ex) {
      logger.error(&quot;Initialize DefaultServerProvider failed.&quot;, ex);
    }
  }
  //获取参数时的处理
  private void initEnvType() {
    // 1. Try to get environment from JVM system property
    m_env = System.getProperty(&quot;env&quot;);
    if (!Utils.isBlank(m_env)) {
      m_env = m_env.trim();
      logger.info(&quot;Environment is set to [{}] by JVM system property &apos;env&apos;.&quot;, m_env);
      return;
    }

    // 2. Try to get environment from OS environment variable
    m_env = System.getenv(&quot;ENV&quot;);
    if (!Utils.isBlank(m_env)) {
      m_env = m_env.trim();
      logger.info(&quot;Environment is set to [{}] by OS env variable &apos;ENV&apos;.&quot;, m_env);
      return;
    }

    // 3. Try to get environment from file &quot;server.properties&quot;
    m_env = m_serverProperties.getProperty(&quot;env&quot;);
    if (!Utils.isBlank(m_env)) {
      m_env = m_env.trim();
      logger.info(&quot;Environment is set to [{}] by property &apos;env&apos; in server.properties.&quot;, m_env);
      return;
    }

    // 4. Set environment to null.
    m_env = null;
    logger.info(&quot;Environment is set to null. Because it is not available in either (1) JVM system property &apos;env&apos;, (2) OS env variable &apos;ENV&apos; nor (3) property &apos;env&apos; from the properties InputStream.&quot;);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;改造思路&lt;/h2&gt;
&lt;h3&gt;java-agent对ApolloApplicationContextInitializer.postProcessEnvironment进行拦截&lt;/h3&gt;
&lt;p&gt;直接上代码，这里拦截使用了bytebuddy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;net.bytebuddy&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;byte-buddy&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.10.22&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;net.bytebuddy&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;byte-buddy-agent&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.10.22&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-core&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;4.3.16.RELEASE&amp;lt;/version&amp;gt;
            &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-shade-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.2.4&amp;lt;/version&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;goal&amp;gt;shade&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;artifactSet&amp;gt;
                        &amp;lt;includes&amp;gt;
                            &amp;lt;include&amp;gt;net.bytebuddy:byte-buddy:jar:&amp;lt;/include&amp;gt;
                            &amp;lt;include&amp;gt;net.bytebuddy:byte-buddy-agent:jar:&amp;lt;/include&amp;gt;
                        &amp;lt;/includes&amp;gt;
                    &amp;lt;/artifactSet&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-jar-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;2.6&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;archive&amp;gt;
                        &amp;lt;manifestEntries&amp;gt;
                            &amp;lt;Premain-Class&amp;gt;cn.intotw.springconfig.apollo.AgentMain&amp;lt;/Premain-Class&amp;gt;
                            &amp;lt;Can-Redefine-Classes&amp;gt;true&amp;lt;/Can-Redefine-Classes&amp;gt;
                        &amp;lt;/manifestEntries&amp;gt;
                    &amp;lt;/archive&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//拦截主函数
public class AgentMain {

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException {
        System.out.println(&quot;进入premain&quot;);
        System.out.println(&quot;isRedefineClassesSupported: &quot; + inst.isRedefineClassesSupported());

        new AgentBuilder.Default()
                .type(ElementMatchers.named(&quot;com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer&quot;))
                .transform((builder, type, loader, module) -&amp;gt; builder
                        .method(named(&quot;postProcessEnvironment&quot;))
                        .intercept(MethodDelegation.to(SpringConfigInterceptor.class)))
                .installOn(inst);
    }

}
//拦截器
public class SpringConfigInterceptor {
    private static final String[] APOLLO_SYSTEM_PROPERTIES_APPEND = {&quot;env&quot;,&quot;apollo.configService&quot;};
    @RuntimeType
    public static void intercept(@Argument(0) Object args1, @SuperCall Callable&amp;lt;?&amp;gt; callable) throws Exception {

        try {
            initializeSystemProperty((ConfigurableEnvironment) args1);
            callable.call();
        } finally {
            System.out.println(&quot;after advice&quot;);
        }
    }
    static void initializeSystemProperty(ConfigurableEnvironment environment) {
        for (String propertyName : APOLLO_SYSTEM_PROPERTIES_APPEND) {
            fillSystemPropertyFromEnvironment(environment, propertyName);
        }
    }
    static void fillSystemPropertyFromEnvironment(ConfigurableEnvironment environment, String propertyName) {
        if (System.getProperty(propertyName) != null) {
            return;
        }

        String propertyValue = environment.getProperty(propertyName);

        if (propertyValue==null ||&quot;&quot;.equals(propertyValue)) {
            return;
        }

        System.setProperty(propertyName, propertyValue);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实主要就是对该函数的加载进行拦截，复制一段代码增加一下对自定义参数的读取，原理一模一样。
代码放到github上了[https://github.com/IntoTw/apollo-springconfig-support]&lt;/p&gt;
&lt;h3&gt;通过Apollo的SPI支持，实现自己的DefaultProviderManager来替换&lt;/h3&gt;
&lt;p&gt;翻源码的时候发现，Apollo对于DefaultProviderManager提供了SPI支持，所以也可以直接通过实现com.ctrip.framework.foundation.spi.ProviderManager接口，来直接用自己的配置加载实现替代原有实现。
&lt;img src=&quot;https://images.intotw.tech/blog/2024/07/5aaf2a3800f4cac10fccd66875a62c0f.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在resources文件下新建META-INF/services/com.ctrip.framework.foundation.spi.ProviderManager文件。&lt;/li&gt;
&lt;li&gt;文件内容为自己实现的接口，代码随便抄抄源码然后改一下就行了。
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/d02b6728a04393ab5c4916db8c4b97f6.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SPI部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyProviderManager implements ProviderManager {
    @Override
    public String getProperty(String name, String defaultValue) {
        return null;
    }

    @Override
    public &amp;lt;T extends Provider&amp;gt; T provider(Class&amp;lt;T&amp;gt; clazz) {
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;debug，可以看到此时加载的已经是我们实现的自己的manager了。
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/0e607aaab099e7d4d0c3bc758c9423e9.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Java：JMH与基准测试</title><link>https://www.intotw.cn/posts/java-jmh/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-jmh/</guid><pubDate>Mon, 01 Mar 2021 16:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;JMH是一套Java基准测试工具，用于对Java执行进行基准测试以及生成测试报告。平时应用于Java一些基础Api或者一些工具类这种离开网络因素的纯系统测试。&lt;/p&gt;
&lt;h1&gt;使用方式&lt;/h1&gt;
&lt;p&gt;maven引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.openjdk.jmh&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;jmh-core&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.21&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.openjdk.jmh&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jmh-generator-annprocess&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.21&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;provided&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3)
@Measurement(iterations = 20, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(1)
@Fork(2)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class BigLoopBenchmark {

    @Benchmark
    public void testBigLoopOutSide() {
        long k = 0;
        for (int j = 0; j &amp;lt; 1000000000; j++) {
            for (int i = 0; i &amp;lt; 100; i++) {
                k++;
            }
        }
        use(k);
    }

    @Benchmark
    public void testBigLoopInSide() {
        long k = 0;
        for (int i = 0; i &amp;lt; 100; i++) {
            for (int j = 0; j &amp;lt; 1000000000; j++) {
                k++;
            }
        }
        use(k);
    }
    void use(long k){

    }
    public static void main(String[] args) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(BigLoopBenchmark.class.getSimpleName())
                .output(&quot;D:/Benchmark.log&quot;)
                .build();
        new Runner(options).run();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Api说明：&lt;/h2&gt;
&lt;h3&gt;@BenchmarkMode(Mode.Throughput)基准测试模式&lt;/h3&gt;
&lt;p&gt;测试的模式，其中包括：&lt;/p&gt;
&lt;h4&gt;Throughput吞吐量&lt;/h4&gt;
&lt;p&gt;即每秒该方法可以执行多少次,结果为ops/time&lt;/p&gt;
&lt;h4&gt;AverageTime平均时间&lt;/h4&gt;
&lt;p&gt;即平均每次执行需要花费多少秒，结果为time/ops&lt;/p&gt;
&lt;h4&gt;SampleTime随机取样&lt;/h4&gt;
&lt;p&gt;结果是取样的执行的花费时间，比如99%请求,结果实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  Histogram, ms/op:
    [0.000, 0.005) = 361794 
    [0.005, 0.010) = 12 
    [0.010, 0.015) = 12 
    [0.015, 0.020) = 2 
    [0.020, 0.025) = 3 
    [0.025, 0.030) = 1 
    [0.030, 0.035) = 1 
    [0.035, 0.040) = 1 
    [0.040, 0.045) = 0 
    [0.045, 0.050) = 0 
    [0.050, 0.055) = 0 
    [0.055, 0.060) = 0 
    [0.060, 0.065) = 0 
    [0.065, 0.070) = 0 
    [0.070, 0.075) = 0 
    [0.075, 0.080) = 0 
    [0.080, 0.085) = 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;SingleShotTime单次执行&lt;/h4&gt;
&lt;p&gt;结果是单次执行的统计，基本上用来测试冷启动&lt;/p&gt;
&lt;h4&gt;All所有测试&lt;/h4&gt;
&lt;h3&gt;@Warmup(iterations = 3)预热次数&lt;/h3&gt;
&lt;p&gt;配置测试执行前该段代码预先执行的次数，即预热次数，因为Java有JIT，所以预热执行可以先触发JIT，大大提升执行速度，并且可以模拟实际执行速度。统计更准确&lt;/p&gt;
&lt;h3&gt;@Measurement(iterations = 20, time = 5, timeUnit = TimeUnit.SECONDS)执行配置&lt;/h3&gt;
&lt;p&gt;主要就是执行多少次操作，执行几秒，时间单位&lt;/p&gt;
&lt;h3&gt;@Threads(1)&lt;/h3&gt;
&lt;p&gt;基准测试使用的线程数&lt;/p&gt;
&lt;h3&gt;@Fork(2)&lt;/h3&gt;
&lt;p&gt;基准测试fork出测试的进程数&lt;/p&gt;
&lt;h3&gt;@OutputTimeUnit(TimeUnit.MILLISECONDS)&lt;/h3&gt;
&lt;p&gt;输出报告时间的单位&lt;/p&gt;
</content:encoded></item><item><title>Java基础：ThreadLocal及其原理</title><link>https://www.intotw.cn/posts/java-threadlocal/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-threadlocal/</guid><pubDate>Mon, 01 Mar 2021 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;ThreadLocal的用处&lt;/h2&gt;
&lt;p&gt;ThreadLocal是一个多线程的辅助工具类，目的是方便开发者维护多线程中的共享变量。我们知道如果我们想要在一个线程中一直访问一个变量或者在线程上下文中保存一个变量，我们要么将该变量声明为static静态，要么就在每一步函数调用中均传入该变量。这两种方式，static方式不能解决每个线程同时分别持有的问题。每一步函数传入对代码的侵入性过高。&lt;/p&gt;
&lt;p&gt;所以ThreadLocal实际的使用效果就是以线程维度，让每个线程拥有了一个自己的上下文变量池，防止大量的静态声明。这也是为什么它并不在juc并发包里的原因。&lt;/p&gt;
&lt;h2&gt;ThreadLocal的实现&lt;/h2&gt;
&lt;p&gt;通过上述TheadLocal的用处和它解决的问题，我们可以马上想到一个很简单的实现来补充static实现的缺点：我们可以通过定义一个static的Map，其中key为线程或线程id，value为需要保存的变量。
TheadLocal的实际实现方法也类似，不过在其中进行了一定改进：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;TheadLocal实现体系中，由Thead本身持有该Map，其中Key为TheadLocal，value为值。&lt;/li&gt;
&lt;li&gt;TheadLocal本身仅通过同包可以访问的特性，提供了一系列访问Thead中的Map的Api。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ThreadLocal防止内存泄露&lt;/h2&gt;
&lt;p&gt;我们知道，TheadLocal这种实现体系下，Thread会持有一个Map，并且该Map中会大量使用TheadLocal作为key，Map引用关系默认是强引用。在线程池或连接池这种会对线程进行复用的场景下，为了防止TheadLocal被强引用导致一直不能被回收，所以Thead中对TheadLocal的维护Map使用的是一个简单实现的WeakReferenceMap。该Map有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;该Map的哈希冲突使用跳跃式处理，而不是HashMap中的桶。原因应该是把该空间当做栈来思考的话，使用跳跃式可以减少更多的空间分配，并且更好的利用cpu缓存。&lt;/li&gt;
&lt;li&gt;该Map的Entry，由继承了WeakReference的Entry实现，该Entry本身为TheadLocal的一个弱引用，当TheadLocal的强引用失去，被gc回收后。该Map在其各方法中都增加了清理Map节点的步骤，根据该弱引用对应的弱引用回收队列中的节点，清理Map中对应的键值对。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ThreadLocal为什么会内存泄露&lt;/h2&gt;
&lt;p&gt;这点其实应该放在上面那点之前说，但是这个确实也困扰了比较久，钻了牛角尖，一直没有明白。
我们日常中一般使用TheadLocal的方式都如下，比如使用一个Util类来维护请求的TraceId，进行链路追踪的效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class TraceIDUtils {
	private static final ThreadLocal&amp;lt;String&amp;gt; SEQUENCE_ID = new ThreadLocal&amp;lt;String&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们会发现，该TheadLocal对象，本身就是一个static声明，永远存在来源于该类的强引用，那么该TheadLocal本身就不会被回收，Thead中的Map是否使用弱引用好像毫无意义。&lt;/p&gt;
&lt;p&gt;当时也是思考了比较久，后面才发现钻了牛角尖，当时大牛们设计出来，应该是考虑类似情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Test {
    ThreadLocal&amp;lt;String&amp;gt; threadLocal;
    public void run(){
        this.threadLocal = new ThreadLocal&amp;lt;&amp;gt;();
        this.threadLocal.set(&quot;abc&quot;);
        //do something
        //……
        //end
    }
    public static void main(String[] args) throws Exception {
        new Test().run();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在当然知道，不使用static定义，肯定是不合适的，但是Java的大牛们作为Api的设计者，肯定是要考虑到其他人在任何情况下使用该工具都应是安全的。而在这种使用方式下，在线程被销毁前，显然如果线程中的Map对该TheadLocal为强引用，那么在该方法执行完毕后该方法内的强引用消失后，依旧在Map上会直接造成内存泄露。&lt;/p&gt;
&lt;p&gt;这也能使人明白为什么Java不选择在TheadLocal中维护一个以Thead为Key的Map来实现。&lt;/p&gt;
</content:encoded></item><item><title>大循环与小循环嵌套的性能比较（分支预测）</title><link>https://www.intotw.cn/posts/loop-different/</link><guid isPermaLink="true">https://www.intotw.cn/posts/loop-different/</guid><pubDate>Mon, 01 Mar 2021 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;面试被问到一个很有意思的问题：大循环和小循环，哪个在外哪个在里有区别吗？为什么？哪种更快？&lt;/p&gt;
&lt;p&gt;当时确实没有答上来也没想到，明明之前看CSAPP了解过CPU的指令分支预测的，但是实在没有想到这里去。&lt;/p&gt;
&lt;p&gt;先上个图：
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/c23ac577ea92a466b0420511e70b7c2f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再来个解释的比较清楚的博客：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000006889989&quot;&gt;https://segmentfault.com/a/1190000006889989&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;简而言之，就是当进行循环时，因为判断循环条件也是属于分支预测，所以大循环在内时，分支预测连续成功的次数会更高，会进行更少的指令回退，CPU执行的会更快。&lt;/p&gt;
&lt;p&gt;后面实际测试了一下，下述代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; long k=0;
        final long currentTimeMillis = System.currentTimeMillis();
        for (int j = 0; j &amp;lt; 1000000000; j++) {
            for (int i = 0; i &amp;lt; 100; i++) {
                k++;
            }
        }
        System.out.println(System.currentTimeMillis()-currentTimeMillis);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当大循环在内时，平均执行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;avg(2255 2291  2395 2237 2318)=2299.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当大循环在外时，平均执行时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;avg(4683 4651 4708 4810 4856)=4741.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确实慢了1倍多。&lt;/p&gt;
</content:encoded></item><item><title>单元测试：单元测试中的mock</title><link>https://www.intotw.cn/posts/spring-juit/</link><guid isPermaLink="true">https://www.intotw.cn/posts/spring-juit/</guid><pubDate>Tue, 19 Jan 2021 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;       公司要求提升单元测试的质量，提高代码的分支覆盖率和行覆盖率，安排我研究单元测试，指定方案分享并在开发部普及开。整理完资料后，同步一下到博客。&lt;/p&gt;
&lt;h2&gt;单元测试中的mock的目的&lt;/h2&gt;
&lt;p&gt;       mock的主要目的是让单元测试&lt;strong&gt;Write Once, Run Everywhere&lt;/strong&gt;,即编写一次后，可以在任意时刻任意环境运行，无需依赖数据库网络等。&lt;/p&gt;
&lt;h2&gt;Mock工具介绍&lt;/h2&gt;
&lt;p&gt;       Mock工具经过调研，基本上是表格下面的这么个情况:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;mockserver方案&lt;/th&gt;
&lt;th&gt;开源&lt;/th&gt;
&lt;th&gt;支持随机参数&lt;/th&gt;
&lt;th&gt;支持请求延时模拟&lt;/th&gt;
&lt;th&gt;支持参数上下文&lt;/th&gt;
&lt;th&gt;仓库分组&lt;/th&gt;
&lt;th&gt;接口管理&lt;/th&gt;
&lt;th&gt;仪表盘&lt;/th&gt;
&lt;th&gt;日志&lt;/th&gt;
&lt;th&gt;支持管理台配置&lt;/th&gt;
&lt;th&gt;支持编程&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rap2，easy-mock等&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wiremock&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mock-server&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;postman&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;       简要介绍下各个的特点和为什么没选：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;rap2和easy-mock等，都是基于node开发的，和我们开发部的主力语言Java相性一般，后续改造难度大，并且不支持请求超时的配置和上下文的配置，优点是使用操作简单，pass。&lt;/li&gt;
&lt;li&gt;wiremock，和rap2差不多，就是多个支持延时请求，不过是英文的，pass&lt;/li&gt;
&lt;li&gt;mock-server，基于java语言的，底层是netty，编程自由，比较适合java技术栈的团队。&lt;/li&gt;
&lt;li&gt;postman，虽然有mock功能，但是只能针对某个请求的返回固定mock，并且每次启动mock的端口和url完全随机，无法接受，pass。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;       我们最后选的是mockito和mock-server，mockito因为是java的mock工具包，所以并不在上面的表格里。&lt;/p&gt;
&lt;h2&gt;mockito&lt;/h2&gt;
&lt;h3&gt;相关介绍：&lt;/h3&gt;
&lt;p&gt;       这个包是spring官方也推荐的mock依赖，在spring-boot-starter-test中默认就会自动包含。
       这个包提供的相关类，主要功能就是对某个对象进行mock，通过其提供的特殊的语法，对某个对象的返回以及行为做mock。&lt;/p&gt;
&lt;h3&gt;应用场景：&lt;/h3&gt;
&lt;p&gt;       单元测试时，如果依赖其他系统的RPC调用（比如feign或dubbo），可以针对相关RPC的调用对象进行直接mock，直接返回成功、超时、异常，减少依赖。
       在对系统内部的某些工具类或者数据库层进行单元测试时，可以模拟一些异常情况，比如数据库超时、框架层抛出某些很难复现的特定异常返回，可以通过直接mock实现来达到效果。
       mockito除了mock外也支持spy，mock与spy的区别是，mock产生的是一个空对象，对mock对象未做配置的方法调用均返回null或异常。spy产生的是一个代理对象，对那些做了配置的方法按照配置的预期返回，未做配置的方法直接会调用原方法。&lt;/p&gt;
&lt;h3&gt;使用方式（spring）：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;maven中引入：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mockito&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mockito-all&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.9.5&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在测试类中进行如下定义：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;//需要mock的服务，一般是RPC，也可以是工具类，总而言之是一个对象
@Mock
TestRpc testRpc;
@Autowired
TestService testService;

//在@Before中对其进行初始化
@Before
public void initMocks() throws Exception {
	//1.1 初始化的api，在这一步执行后，testRpc被初始化为一个mock对象
    MockitoAnnotations.initMocks(this);
    //1.2 使用mock对象替换spring中的bean：这里是将后面要用到的testService中的testRpc这个rpc对象，
    //替换为上面@Mock为我们创建的mock对象，然后我们就可以对这个对象进行mock了，这里的替换是spring容器级别的替换
    //注意，理论上对RPC的service进行mock即可，即替换调用RPC的那个bean中的rpc对象。
    ReflectionTestUtils.setField(AopTargetUtils.getTarget(orderPayFacade), &quot;testRpc&quot;, testRpc);
    //1.3 定义mock返回：对新的mock对象进行定义，当后续请求这个rpc的该方法时，会直接return一个空的成功对象
    final ResultRpc&amp;lt;TestVO&amp;gt; testVo = new ResultRpc&amp;lt;&amp;gt;();
    when(testRpc.getAccountByBindCardId(&quot;101010&quot;)).thenReturn(testVo);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//另一种初始化方式，更加简单快捷
//这里是另一种写法，设置一个默认的answer，不用每个方法都设置一次返回,也可以继续进行上面那种方式的when配置
final TestRpc testRpcMock = mock(TestRpc.class, new Answer&amp;lt;TestRes&amp;gt;() {
    @Override
    public TestRes answer(InvocationOnMock invocationOnMock) throws Throwable {
        final TestRes testRes = new TestRes();
        testRes.setConfigId(0L);
        testRes.setCityId(86);
        testRes.setServiceId(&quot;01&quot;);
        testRes.setSysJoinType(0);
        testRes.setMerchantId(&quot;320212018002&quot;);
        testRes.setMerchantCode(&quot;&quot;);
        return testRes;
    }
});
ReflectionTestUtils.setField(AopTargetUtils.getTarget(testService), &quot;testRpc&quot;, testRpcMock);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;然后直接正常执行测试即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;使用方式（spring-boot及以上）：&lt;/h4&gt;
&lt;p&gt;       前面说了spring-boot-starter较高版本（2.0以上）的test中默认会包括该依赖，所以直接使用就行，更方便的是无需使用反射工具替换spring上下文的bean，使用@MockBean注解标识bean即可。&lt;/p&gt;
&lt;h2&gt;mock-server&lt;/h2&gt;
&lt;h3&gt;相关资料：&lt;/h3&gt;
&lt;p&gt;官方文档 &lt;a href=&quot;https://www.mock-server.com/&quot;&gt;https://www.mock-server.com/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;应用场景：&lt;/h3&gt;
&lt;p&gt;       当进行单元测试时，如果我们需要进行http请求级别的模拟以及mock，那么我们就可以使用mockserver
       当然mockito也可以通过直接mock那些http请求的类来达到相似效果，不过使用mock-server，我们可以更逼真的模拟http的环境，以提前发现那些只有在使用网络下才会出现的问题。
       既可以集成在maven的test生命周期里，也可以直接单独启动做一个server。&lt;/p&gt;
&lt;h3&gt;使用方式：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;maven中引入：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mock-server&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mockserver-netty&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.11.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在测试类中进行如下定义：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;private final int mockPort = 19999;
private ClientAndServer mockServer;
//在@Before中对其进行初始化
@Before
public void initMocks() throws Exception {
	//1.1 初始化的api：启动mockserver
	mockServer = startClientAndServer(mockPort);
	//1.2 配置mockServer
	mockServer
        .when(
                request()
                        .withMethod(&quot;POST&quot;)
                        .withPath(&quot;/test/pay_v1/trade/pay&quot;)
                        .withContentType(MediaType.APPLICATION_JSON)
        )
        .respond(
               new TestResponseCallBack()
        );
}
public static class TestResponseCallBack implements ExpectationResponseCallback{
    private final Gson gson=new Gson();
    @Override
    public HttpResponse handle(HttpRequest httpRequest) throws Exception {
        log.info(&quot;------------{}&quot;,httpRequest);
        if (httpRequest.getMethod().getValue().equals(&quot;POST&quot;)) {
            //校验签名
            boolean verify = doVerifySign(httpRequest);
            if (!verify){
                return response()
                        .withStatusCode(OK_200.code())
                        .withBody(gson.toJson(CommonResult.failure(CommonErrors.SIGNATURE_VERIFY_FAIL)));
            }
            //构造返回
            return createResponse(httpRequest);
        } else {
            return notFoundResponse();
        }
    }
    private HttpResponse createResponse(HttpRequest httpRequest) throws Exception {
        final HttpRequest httpRequest1 = httpRequest;
        final String req = new String(httpRequest.getBodyAsRawBytes());
        String respBody=&quot;&quot;;
        final JSONObject jsonObject= JSON.parseObject(req);
        //比如对参数做一些校验
        Assert.assertNotNull(jsonObject.getString(&quot;user_id&quot;));
        //构造返回，可以根据请求的内容构造，这里随便写个返回，
        final String user_id = jsonObject.getString(&quot;user_id&quot;);
        respBody=&quot;{\&quot;success\&quot;: true,\&quot;errcode\&quot;: \&quot;0000\&quot;,\&quot;errmsg\&quot;: \&quot;成功\&quot;,\&quot;result\&quot;: {\&quot;user_id\&quot;: \&quot;123456\&quot;,\&quot;reserved\&quot;:&quot;+user_id+&quot;\&quot;\&quot;}}&quot;;
        //这里如果必要的话，也可以触发一个延时的回调
        new Thread(new Runnable() {
            @Override
            public void run() {
                LockSupport.parkNanos(1000000000L*2);
                final String notify_url = jsonObject.getString(&quot;notify_url&quot;);
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
                headers.add(&quot;Accept&quot;, MediaType.APPLICATION_JSON.toString());
                JSONObject param = new JSONObject();
                param.put(&quot;username&quot;, &quot;123&quot;);
                HttpEntity&amp;lt;String&amp;gt; formEntity = new HttpEntity&amp;lt;String&amp;gt;(param.toJSONString(), headers);
                String result = new RestTemplate().postForObject(notify_url, formEntity, String.class);
                log.info(&quot;发送回调：{}&quot;,param.toJSONString());
            }
        }).start();
        return response()
                .withStatusCode(OK_200.code())
                .withBody(respBody);
    }
    private boolean doVerifySign(HttpRequest httpRequest) throws Exception {
        String signature = httpRequest.getFirstHeader(RequestHeader.Signature);
        String message = new String(httpRequest.getBodyAsRawBytes(), StandardCharsets.UTF_8);
        String md5HexMessage = DigestUtils.md5Hex(message.getBytes(StandardCharsets.UTF_8));
        return RSAUtils.doCheck(md5HexMessage, signature, privateKey, StandardCharsets.UTF_8.displayName());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;然后直接正常执行测试即可。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;cobertura-maven-plugin&lt;/h2&gt;
&lt;p&gt;       前面的2个mock工具，结合cobertura-maven-plugin，可以瞬间跑起一个带代码覆盖率的测试。&lt;/p&gt;
&lt;h3&gt;使用方式：&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;maven&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;org.codehaus.mojo&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;cobertura-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.7&amp;lt;/version&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;执行测试：mvn clean cobertura:cobertura -f pom.xml&lt;/li&gt;
&lt;li&gt;到target/site下打开index文件查看结果:
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/fe1b63362f3df3afd5db09387071191b.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;       本文简单介绍了3个工具的使用，主要是提供了一个可行的方案去推进单元测试，具体3个工具的详细使用细节以及进阶，可以自行查找资料。&lt;/p&gt;
</content:encoded></item><item><title>单元测试：单元测试编写的原则</title><link>https://www.intotw.cn/posts/spring-juit-advanced/</link><guid isPermaLink="true">https://www.intotw.cn/posts/spring-juit-advanced/</guid><pubDate>Tue, 19 Jan 2021 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;       公司要求提升单元测试的质量，其中我作为方案和推动的主导，对开发过程中的单元测试，有了一些思考和总结&lt;/p&gt;
&lt;h2&gt;单元测试编写的目的&lt;/h2&gt;
&lt;p&gt;       单元测试编写的目的，是面向计算机特性的，基于函数的in-out，所以单元测试的好帮手就是断言，通过不断的构造输出并对结果进行断言，我们就可以针对一个对象以及它的函数，构建出充足的用例去包裹它，以期望它的任意行为满足我们的需要。&lt;/p&gt;
&lt;p&gt;       最终的目的也是为了通过用例对单元测试的包裹，达到对任意对象的任意函数进行修改后，既满足新的功能，又对旧有功能没有影响。&lt;/p&gt;
&lt;h2&gt;单元测试编写的原则&lt;/h2&gt;
&lt;p&gt;基于单元测试编写的目的，在编写单元测试时，我们应当认为我们编写的单元测试所面向的过程是黑盒，我们应只对输入以及我们期望的输出负责，即只考虑起始输入以及最终态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;具体来说&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;针对Dao的单元测试，我们只应该关心测试后数据库各个字段的值。&lt;/li&gt;
&lt;li&gt;针对Service的测试，我们只应该关心Service方法执行最终涉及到的数据库字段，缓存，消息队列中的值，以及Service方法的返回。&lt;/li&gt;
&lt;li&gt;针对Controller的测试，我们仅应该关心Controller针对HttpRequst的输入以及HttpResponse的输出。而不该关心其中具体调用到的那些Service如何执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;       其中第3点或许令人疑惑，但是我们在编写单元测试时，我认为应该仅考虑执行的函数逻辑本身。对于其外部的依赖或者并不关心结果的方法，尽量mock掉。虽然目前的场景下Service层与Dao层基本耦合在一起无法拆分开来进行单元测试，但是Controller层绝不应该严重依赖Service来进行单元测试，否则单元测试最终会沦为接口测试。&lt;/p&gt;
&lt;p&gt;       同时，如果我们在测试某个模块时，不对其外部依赖做mock，那么就会导致我们的测试依然非常臃肿。并且最可怕的是，我们对结果的断言无法实现。比如当我们对Controller进行单元测试时，即便我们不mock它所依赖的Rpc（而是将他们启动着），但是我们如何对他所依赖的那些Rpc的执行正确与否进行断言呢？难道我们要在Controller层的单元测试执行完成后，在Controller层对数据库以及其他中间件的变化做查询以及断言吗？这显然是不现实的。&lt;/p&gt;
&lt;p&gt;       单元测试面向的应该是一个从不调用其他函数（不包括第三方jar包）的函数，从其开始自底向上一个个编写，直到Http接口层。&lt;/p&gt;
&lt;p&gt;       对于同一个单元测试来说，它的增加与修改应该与其对应测试功能的变化相同：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当对应的功能增加了或扩展了，应在单元测试中对新增功能增加新的测试，对老的单元测试则应该不做修改，并保证新增功能也满足老的测试。&lt;/li&gt;
&lt;li&gt;当对应的功能修改了，并且不可能满足原有的测试，此时才应当去修改原有的测试，并应当加注释以说明。当需要修改单元测试时一定要谨慎，因为这意味着以前的经验已经不奏效了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;       依靠这些原则去编写单元测试，我们会发现，想要写出一个好的便于测试的类也是比较难的。想要写出便于测试的类以及方法，会反向要求了我们减少代码间的耦合，提高代码的可读性。&lt;/p&gt;
&lt;h2&gt;单元测试编写的Timing&lt;/h2&gt;
&lt;p&gt;       单元测试的编写不应该是大家埋头一个月一起闭关编写（虽然大部分从未编写过单元测试的系统确实需要这样），而是一个不断渐进，防止犯过的错再犯的方式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当开发一个新功能时，我们应当对新功能针对需求编写单元测试，这是理所应当的。&lt;/li&gt;
&lt;li&gt;当对一个功能进行修改时，我们应该对该功能的单元测试进行修改，并完成该功能修改后的单元测试。&lt;/li&gt;
&lt;li&gt;当发现某功能出现了某些意料之外的输出（一般来说就是bug），我们在修改时，应当针对该种特殊情况编写对应的单元测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;       经过以上3种情况的单元测试编写，我们的单元测试会覆盖的场景就会越来越多，我们的代码也会越来越健壮，久而久之，我们对于代码的修改可以纯粹依赖单元测试，而减少更多逻辑上复杂的思考。&lt;/p&gt;
</content:encoded></item><item><title>分布式系统：数据库扩容提效-以及一次数据库分库分表实践</title><link>https://www.intotw.cn/posts/distribution-db/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-db/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数据库扩容随着架构的演进&lt;/h2&gt;
&lt;p&gt;数据库扩容一般都是随着系统建设一步步演进的，所以这里随着系统的演进，一一介绍扩容的方案。&lt;/p&gt;
&lt;h3&gt;读写分离&lt;/h3&gt;
&lt;p&gt;一开始肯定是一个数据库，但是压力上来了之后，最容易做的就是读写分离，即1主多从，主机负责写，从机负责读，像mysql就提供类似功能，通过配置实现master-slaver的模式。但是读写分离的逻辑还是要在程序中写的，即配置多数据源，update与insert走主库，select走读库。耦合较高。代码侵入强。&lt;/p&gt;
&lt;h3&gt;垂直分库&lt;/h3&gt;
&lt;p&gt;读写分离也扛不住了咋办？一般这个时候随着业务的扩展，微服务即所谓的分布式服务就要开始实施了，此时可以随着系统的拆分，将数据库也拆分，如较细的拆分成：用户模块库，订单模块库，商品模块库。较粗的拆分成：核心业务库，周围业务库。这一步是通过业务进行垂直分库，非常好理解，即将不同的业务表按照属性，分配到不同的数据库中去，具体实现最简单的当然也是改动数据访问层的代码。&lt;/p&gt;
&lt;h3&gt;水平分表&lt;/h3&gt;
&lt;p&gt;垂直分库后，随着业务量上来，你会发现核心表的数据量太大，如果索引列多的话，既占空间，效率还低，按照往常流传的经验，mysql一张表500W数据就差不多了，在往上性能就会下降的厉害，所以这个时候可以进行水平分表。水平分表即将一张大表的数据，按照一定逻辑，散列到几张不同的表中，使得不会出现过大的表，提升查找效率。具体的做法一般有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按照日期散列，即一个月一张表，或一周一张表，提前建立大量的表，修改程序后上线，程序自动按天写入到当天的表中去，查询时也根据数据的时间（可以根据搜索引擎等做路由），寻找到具体的那张表去查询。优点是实现简单，自带扩容（加表就行），缺点是查询略麻烦（如果是时间强相关的报表类还好，如果是普通该业务因为往往查询时不一定有数据的时间，所以可能需要路由表或搜索引擎去辅助）。&lt;/li&gt;
&lt;li&gt;按照主键或业务主键哈希散列，即如用户表user，根据user_id以及哈希算法，落到具体的表中。可预先估计数据量如3年内预估10亿数据，每张表500W数据，那么共需要200张表，提前建好，命名规则为user_01到user_200。进行数据操作时先根据user_id以及哈希算法算出具体要到哪张表的表名，然后具体执行sql。优点是实现优雅，查询简单。缺点是扩容复杂（加表的话要重新计算散列哈希再次落库），不带业务主键的查询需要改造。其实和1中一样，只不过一个散列是日期一个散列是业务主键哈希，都要面对不带散列键的查询难题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;水平分库&lt;/h3&gt;
&lt;p&gt;其实水平分库我认为是和水平分表差不多级别的解决问题的方案，应该说是差不多时候考虑的，在数据库表性能先达到上限时，可以采用水平分表。当数据库性能达到上限时，可以采用水平分库。这两个水平拆分我个人认为可以在一起看，思想和达到的目的都是相同的。&lt;/p&gt;
&lt;p&gt;水平拆分数据库最通常的思路就是做冷热库，即定时将冷数据（业务上访问量少，一般具有历史时效性，如业务只提供一年内的查询）转移到冷库中。如每年将去年的数据转移到冷库中，冷库的性能可以较低，因为历史数据的访问量低，并且历史数据查询速度慢是可以容忍的。&lt;/p&gt;
&lt;p&gt;为什么我说和水平分表差不多呢，因为水平分库也可以做到类似分表的功能，即你之前不是路由到不同表吗，现在是每个库都建一张同名表，路由以数据库层面来做，效果其实是差不多的。&lt;/p&gt;
&lt;h3&gt;水平分库分表&lt;/h3&gt;
&lt;p&gt;最后的演进一般就是水平分库加水平分表都上，即路由结果是路由到某个分库+某个分表，比如 共200张user表，200张user表平均分布在10个数据库实例上，每个数据库实例上有20张表。&lt;/p&gt;
&lt;h2&gt;一次数据库分库分表实践&lt;/h2&gt;
&lt;p&gt;入职一段时间后，领导安排下来任务，要做数据库分库分表，确实，现在系统数据几个核心表，user，order，account这些，基本都是少则千万，多则几亿的数据，能正常面对线上需求纯靠把接口搞成纯异步加一堆MQ扔来扔去，不时还得DBA自己手动去备份一部分数据到历史库，并且程序里还没有查历史库的逻辑！&lt;/p&gt;
&lt;h3&gt;分库分表方案&lt;/h3&gt;
&lt;p&gt;最早公司是有其他系统做过分库分表的，并且方案设计文档还留存着，使用的是mycat。但是了解了一下之后，发现阿里云有类似于mycat的drds云，正好公司本来就使用的阿里云的RDS数据库实例，于是DBA推荐使用这个方式，基本后面就按照DRDS去设计了，其实也没什么差别，最后分库分表方案主要包括2方面：1. 垂直分库、2： 核心表水平分库分表。&lt;/p&gt;
&lt;h4&gt;垂直分库&lt;/h4&gt;
&lt;p&gt;首先是根据业务特性，一定要做的一个就是垂直分库,将核心业务库：订单，用户，账户等库与非核心业务库：菜单，短信，消息，推送，客诉等进行一个分库。另外就是因为历史原因，一个行程计费系统和我们App后端使用的是同一个数据库，所以借着这次也对其做一个拆分。drds是中间件性质的，所以对接入层改动没有要求，完全可以接受。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/32f592e0b735157157faf210c1dbb61a.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;水平分库分表中遇到的难点&lt;/h4&gt;
&lt;h5&gt;拆分键的选择&lt;/h5&gt;
&lt;p&gt;拆分键的选择是十分重要的一个问题，我们业务系统的拆分键理论上可以使用的有user_id，merchant_id（商户号)，city_id这3个字段，在实现分库分表中，拆分键要尽量考虑以下问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拆分键哈希后的均匀程度：其中merchant_id和city_id都会导致散列的十分不均匀。&lt;/li&gt;
&lt;li&gt;拆分键的改造难度：对老系统改造，当然要选取一个目前sql大多数查询都带的拆分键，其中merchant_id大多数查询都不带。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后选择肯定还是选择了user_id作为拆分键，这样的话改动尽量小，也满足要求。&lt;/p&gt;
&lt;h5&gt;join等操作，报表系统的问题&lt;/h5&gt;
&lt;p&gt;水平分库分表后，非拆分键的查询，数据库join，范围查询等操作都会变成代价无法承受的操作。好在系统中从以前设计来看，基本通过冗余字段等手段解决了，系统中的核心表均没有join操作和复杂查询。非拆分键的查询直接使用elasticsearch来做了。但是报表系统是对数据源有大量复杂查询和范围查询的。跟报表部门沟通后，报表部门允许我们通过mq将核心数据的变更通知到报表系统，并且容忍这部分数据的一致性问题，所以这部分问题倒是顺利解决了。&lt;/p&gt;
&lt;h5&gt;索引的查询&lt;/h5&gt;
&lt;p&gt;这一定是水平分库分表都会遇到的最大的问题，即通过user_id进行拆分后，其他原表索引字段的查询该如何做？比如用户表，在查询用户是否注册时，肯定是不存在user_id，而是要通过手机号判断的，此时在原库中，手机号加了索引，但是在拆分后，不带上userid字段的查询都是噩梦。这类字段有很多，查询都是问题，当时我结合网上的资料提出了2种解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;异构索引表。这个也是阿里的方案，阿里的用户表以及订单表等核心表，开源出叫做Canal的数据同步机制，使用mysql的binlog，来完成数据的增量同步，即同样的一张订单表，除主表是以user_id拆分外，还存在一张数据一样的，以商家id做拆分键的表，另外可能有一张数据一样的，以平台id做拆分键的表。以此类推。所以叫异构索引表。&lt;/li&gt;
&lt;li&gt;通过搜索引擎，如elasticsearch等，在插入数据库后，同时将数据通过kafka等mq写到搜索引擎中，然后在后续查询时，先通过搜索引擎查询到对应索引值的拆分键，再通过拆分键去查询。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后选择了方案1，因为方案2的不一致性问题很严重，时效性会出现问题，对于核心业务大表要做幂等的情况下，是无法容忍的。&lt;/p&gt;
&lt;h5&gt;数据迁移&lt;/h5&gt;
&lt;p&gt;上线当晚，我们对数据进行了迁移，我们首先在凌晨时把大多数流量都迁走了，只保留了几台机器提供最基本的服务，对其他应用进行升级。然后期间对数据进行迁移到DRDS，迁移成功并部署完成新的应用，，待DBA确认过数据迁移准确性，测试同学验证过应用（其中比较重要的就是预埋数据的验证）后，将流量切回升级后的程序，将剩余的部分的应用升级。后续在迁移过程中出现的数据问题，由DBA负责修复。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在初始设计数据库以及开发时，就应该考虑后续拓展，尽量先定义好拆分键，并且使查询都带上拆分键，这样的话后面的改动就不会过于复杂。&lt;/li&gt;
&lt;li&gt;进行开发高并发分布式系统时，应尽量避免join等操作。&lt;/li&gt;
&lt;li&gt;他人的方案和思路值得思考和借鉴，但是还是要以实际情况为准去考虑。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>分布式系统：dubbo的连接机制</title><link>https://www.intotw.cn/posts/dubbo-connection/</link><guid isPermaLink="true">https://www.intotw.cn/posts/dubbo-connection/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;研究这个问题的起因&lt;/h2&gt;
&lt;p&gt;起因是一次面试，一次面试某电商网站，前面问到缓存，分布式，业务这些，还相谈甚欢。然后面试官突然甩出一句：“了解dubbo吗？dubbo是长连接还是短连接？”。当时我主要接触了解学习的还是spring cloud，dubbo作为知名的分布式rpc框架，只是有一定了解，并且连接这一块并没有很深入去了解，但是基于对分布式系统的了解，我不假思索的回答了：“长连接啊！”。其实分布式系统接触多了就知道了，分布式系统为了应对高并发，减少在高并发时的线程上下文切换损失以及重新建立连接的损失，往往都是采用长连接的。所以我当时我是这么想的：“dubbo作为处理小数据高并发分布式RPC框架，如果采用短连接，应该不可能达到那么高的吞吐吧。”。所以果断回答了长连接。可是没想到&lt;strong&gt;面试官微微一笑，带着几分不屑&lt;/strong&gt;的说道：“短连接”。当时就给我整懵逼了，无法想象短连接如何处理高并发下重复建立连接以及线程上下文切换的问题。导致我回家的地铁上一直都处在怀疑人生的状态，回家后立马各种百度Google（甚至还怀疑查到的结果）。&lt;/p&gt;
&lt;h2&gt;dubbo的连接机制&lt;/h2&gt;
&lt;p&gt;这里直接上结论了，&lt;strong&gt;dubbo默认是使用单一长连接&lt;/strong&gt;，即消费者与每个服务提供者建立一个单一长连接，即如果有消费者soa-user1，soa-user2,提供者soa-account三台，则每台消费者user都会与3台account建立一个连接，结果是每台消费者user有3个长连接到分别到3台提供者，每台提供者account维持到soa-user1和soa-user2的2个长连接。&lt;/p&gt;
&lt;h3&gt;为什么这么做&lt;/h3&gt;
&lt;p&gt;dubbo这么设计的原因是，一般情况下因为消费者是在请求链路的上游，消费者承担的连接数以及并发量都是最高的，他需要承担更多其他的连接请求，而对提供者来说，承担的连接只来于消费者，所以每台提供者只需要承接消费者数量的连接就可以了，dubbo面向的就是消费者数量远大于服务提供者的情况。&lt;strong&gt;所以说，现在很多项目使用的都是消费者和提供者不分的情况&lt;/strong&gt;，这种情况并没有很好的利用这个机制。&lt;/p&gt;
&lt;h2&gt;dubbo同步转异步&lt;/h2&gt;
&lt;p&gt;dubbo的底层是使用netty，netty之前介绍过是非阻塞的，但是dubbo调用我们大多数时候都是使用的同步调用，那么这里是怎么异步转同步的呢？这里其实延伸下，不只是dubbo，大多数在web场景下，还是同步请求为主，那么netty中要如何将异步转同步？我这边描述一下关键步骤。&lt;/p&gt;
&lt;h3&gt;dubbo的实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; //DubboInvoker
protected Result doInvoke(final Invocation invocation) throws Throwable {
      
          //...
            if (isOneway) {//2.异步没返回值
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                RpcContext.getContext().setFuture(null);
                return new RpcResult();
            } else if (isAsync) {
                //1.异步有返回值，异步的直接返回带future的result就完事了
                ResponseFuture future = currentClient.request(inv, timeout);
                FutureAdapter&amp;lt;Object&amp;gt; futureAdapter = new FutureAdapter&amp;lt;&amp;gt;(future);
                RpcContext.getContext().setFuture(futureAdapter);
                Result result;
                if (isAsyncFuture) {
                    result = new AsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
                } else {
                    result = new SimpleAsyncRpcResult(futureAdapter, futureAdapter.getResultFuture(), false);
                }
                return result;
            } else {//3.异步变同步，这里是同步的返回，主要阻塞的原因在于.get(),实际上就是HeaderExchangeChannel里返回的DefaultFuture的.get()方法
                RpcContext.getContext().setFuture(null);
                return (Result) currentClient.request(inv, timeout)//返回下面的future
                                            .get();//进入get()方法，是当前线程阻塞。那么当有结果返回时，唤醒这个线程
            }
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//--HeaderExchangeChannel
 public ResponseFuture request(Object request, int timeout) throws RemotingException {
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        //这里在发送前，构建自定义的future，用来让调用线程等待，注意这里的future和netty的channelFuture不同。
        DefaultFuture future = DefaultFuture.newFuture(channel, req, timeout);
        try {
            channel.send(req);//使用实际的channel，里面封装了netty的channel，最后是调用到了nettyChannel的send的。
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//--DefaultFuture--实现阻塞调用线程的逻辑，接收到结果
//阻塞的逻辑
 public Object get(int timeout) throws RemotingException {
        if (timeout &amp;lt;= 0) {
            timeout = 1000;
        }
        //done是自身对象的一个可重入锁成员变量的一个condition，这里的逻辑就是：
        //如果获取到了锁，并且条件不满足，则await线程等到下面receive方法唤醒。
        //其实我想吐槽下，这个condition命名为done，又有一个方法叫isDone，但是isDone又是判断response！=null的和done没有任何关系，这个命名不是很科学。
        if (!this.isDone()) {
            long start = System.currentTimeMillis();
            this.lock.lock();

            try {
                while(!this.isDone()) {
                    this.done.await((long)timeout, TimeUnit.MILLISECONDS);
                    if (this.isDone() || System.currentTimeMillis() - start &amp;gt; (long)timeout) {
                        break;
                    }
                }
            } catch (InterruptedException var8) {
                throw new RuntimeException(var8);
            } finally {
                this.lock.unlock();
            }

            if (!this.isDone()) {
                throw new TimeoutException(this.sent &amp;gt; 0L, this.channel, this.getTimeoutMessage(false));
            }
        }

        return this.returnFromResponse();
    }
//收到结果时唤醒的逻辑
  public static void received(Channel channel, Response response) {
        try {
            DefaultFuture future = FUTURES.remove(response.getId());
            if (future != null) {
                future.doReceived(response);
            } else {
            }
        } finally {
            CHANNELS.remove(response.getId());
        }
    }
  private void doReceived(Response res) {
        lock.lock();
        try {
            response = res;//拿到了响应
            if (done != null) {
                done.signal();//唤醒线程
            }
        } finally {
            lock.unlock();
        }
        if (callback != null) {
            invokeCallback(callback);
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//那么我们知道DefaultFuture被调用received方法时会被唤醒，那么是什么时候被调用的呢？
//--HeaderExchangeHandler-- netty中处理的流就是handler流，之前有篇文章讲到过，这里也是在handler中给处理的，其实上面还有ExchangeHandlerDispatcher这类dispatcher预处理，将返回
//分给具体的channelHandler处理，但是结果到了这里
public class HeaderExchangeHandler implements ChannelHandlerDelegate {
        public void received(Channel channel, Object message) throws RemotingException {
                channel.setAttribute(KEY_READ_TIMESTAMP, System.currentTimeMillis());
                HeaderExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);

                try {
                if (message instanceof Request) {
                        Request request = (Request)message;
                        if (request.isEvent()) {
                        this.handlerEvent(channel, request);
                        } else if (request.isTwoWay()) {
                        Response response = this.handleRequest(exchangeChannel, request);
                        channel.send(response);
                        } else {
                        this.handler.received(exchangeChannel, request.getData());
                        }
                } else if (message instanceof Response) {
                        //主要是这里
                        handleResponse(channel, (Response)message);
                } else if (message instanceof String) {
                        if (isClientSide(channel)) {
                        Exception e = new Exception(&quot;Dubbo client can not supported string message: &quot; + message + &quot; in channel: &quot; + channel + &quot;, url: &quot; + channel.getUrl());
                        logger.error(e.getMessage(), e);
                        } else {
                        String echo = this.handler.telnet(channel, (String)message);
                        if (echo != null &amp;amp;&amp;amp; echo.length() &amp;gt; 0) {
                                channel.send(echo);
                        }
                        }
                } else {
                        this.handler.received(exchangeChannel, message);
                }
                } finally {
                HeaderExchangeChannel.removeChannelIfDisconnected(channel);
                }

    }
    //handleResponse，到这里直接调用静态方法，回到了上面接受结果那步。
    static void handleResponse(Channel channel, Response response) throws RemotingException {
        if (response != null &amp;amp;&amp;amp; !response.isHeartbeat()) {
            DefaultFuture.received(channel, response);
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;纯netty的简单实现&lt;/h3&gt;
&lt;p&gt;纯netty的简单实现，其实也很简单，在创建handler时，构造时将外部的FutureTask对象构造到hanlder中，外面使用FutureTask对象get方法阻塞，handler中在最后有结果时，将FutureTask的结果set一下，外部就取消了阻塞。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    public SettableTask&amp;lt;String&amp;gt; sendAndSync(FullHttpRequest httpContent){
        //创建一个futureStask
        final SettableTask&amp;lt;String&amp;gt; responseFuture = new SettableTask&amp;lt;&amp;gt;();
        ChannelFutureListener connectionListener = future -&amp;gt; {
            if (future.isSuccess()) {
                Channel channel = future.channel();
                //创建一个listener，在连接后将新的futureTask构造到一个handler中
                channel.pipeline().addLast(new SpidersResultHandler(responseFuture));
            } else {
                responseFuture.setExceptionResult(future.cause());
            }
        };
        try {
            Channel channel = channelPool.acquire().syncUninterruptibly().getNow();
            log.info(&quot;channel status:{}&quot;,channel.isActive());
            channel.writeAndFlush(httpContent).addListener(connectionListener);
        } catch (Exception e) {
            log.error(&quot;netty写入异常!&quot;, e);
        }
        return responseFuture;
    }
    //重写一个可以手动set的futureTask
    public class SettableTask&amp;lt;T&amp;gt; extends FutureTask&amp;lt;T&amp;gt; {
        public SettableTask() {
            super(() -&amp;gt; {
                throw new IllegalStateException(&quot;Should never be called&quot;);
            });
        }

        public void setResultValue(T value) {
            this.set(value);

        }

        public void setExceptionResult(Throwable exception) {
            this.setException(exception);
        }

        @Override
        protected void done() {
            super.done();
        }
    }
    //resultHandler
    public class SpidersResultHandler extends SimpleChannelInboundHandler&amp;lt;String&amp;gt; {
        private SettableTask&amp;lt;String&amp;gt; future;
        public SpidersResultHandler(SettableTask&amp;lt;String&amp;gt; future){
            this.future=future;
        }
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String httpContent) throws Exception {
            log.info(&quot;result={}&quot;,httpContent);
            future.setResultValue(httpContent);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext channelHandlerContext, Throwable throwable) throws Exception {
            log.error(&quot;{}异常&quot;, this.getClass().getName(), throwable);

        }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;dubbo的高性能，也源于他对每个点不断的优化，最早的时候我记得看到一篇文章写到：dubbo的异步转同步机制，是使用的CountDownLatch实现的。现在想来，可能是在乱说。一些框架的原理，还是要自己多思考多翻看，才能掌握。&lt;/p&gt;
</content:encoded></item><item><title>Java编译期注解处理器详细使用方法</title><link>https://www.intotw.cn/posts/java-apt/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-apt/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Java编译期注解处理器&lt;/h2&gt;
&lt;p&gt;Java编译期注解处理器，Annotation Processing Tool，简称APT，是Java提供给开发者的用于在编译期对注解进行处理的一系列API，这类API的使用被广泛的用于各种框架，如dubbo,lombok等。&lt;/p&gt;
&lt;p&gt;Java的注解处理一般分为2种，最常见也是最显式化的就是Spring以及Spring Boot的注解实现了，在运行期容器启动时，根据注解扫描类，并加载到Spring容器中。而另一种就是本文主要介绍的注解处理，即编译期注解处理器，用于在编译期通过JDK提供的API，对Java文件编译前生成的Java语法树进行处理，实现想要的功能。&lt;/p&gt;
&lt;p&gt;前段公司要求将原有dubbo迁入spring cloud架构，理所当然的最简单的方式，就是将原有的dubboRpc服务类，外面封装一层controller，并且将调用改成feignClient，这样能短时间的兼容原有其他未升级云模块的dubbo调用，之前考虑过其他方案，比如spring cloud sidecar。但是运维组反对，不建议每台机器多加一个服务，并且只是为了短时间过渡，没必要多加一个技术栈，所以考虑使用编译期处理器来快速生成类似的java代码，避免手动大量处理会产生操作失误。&lt;/p&gt;
&lt;p&gt;练手项目示例的git源码：https://github.com/IntoTw/mob&lt;/p&gt;
&lt;h2&gt;启用注解处理器&lt;/h2&gt;
&lt;p&gt;增加这么一个类，实现AbstractProcessor的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//注解处理器会扫描的包名
@SupportedAnnotationTypes(&quot;cn.intotw.*&quot;)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ModCloudAnnotationProcessor extends AbstractProcessor {
    private Messager messager;
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;
    Map&amp;lt;String, JCTree.JCAssign&amp;gt; consumerSourceAnnotationValue=new HashMap&amp;lt;&amp;gt;();
    Map&amp;lt;String, JCTree.JCAssign&amp;gt; providerSourceAnnotationValue=new HashMap&amp;lt;&amp;gt;();
    java.util.List&amp;lt;String&amp;gt; javaBaseVarType;
    @Override
    public void init(ProcessingEnvironment processingEnv) {
        //基本构建，主要是初始化一些操作语法树需要的对象
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }
    @Override
    public boolean process(Set&amp;lt;? extends TypeElement&amp;gt; annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver()) {
            return false;
        }
        //获取所有增加了自定义注解的element集合
        Set&amp;lt;? extends Element&amp;gt; set = roundEnv.getElementsAnnotatedWith(MobCloudConsumer.class);
        //遍历这个集合，这个集合的每个element相当于一个拥有自定义注解的需要处理的类。
        set.forEach(element -&amp;gt; {
            //获取语法树
            JCTree jcTree=trees.getTree(element);
            printLog(&quot;result :{}&quot;,jcTree);
        });
        return true;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中获取的jctree就是那个class文件解析后的java语法树了，下面来看下有哪些操作。&lt;/p&gt;
&lt;h2&gt;遍历语法树&lt;/h2&gt;
&lt;p&gt;java语法树的遍历，并不是能像寻常树节点一样提供child之类的节点，而是通过TreeTranslator这个访问类的实现来做到的，这个类的可供实现的方法有很多，可以用来遍历语法树的注解、方法、变量，基本上语法树的所有java元素，都可以使用这个访问器来访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//获取源注解的参数
jcTree.accept(new TreeTranslator(){
    @Override
    public void visitAnnotation(JCTree.JCAnnotation jcAnnotation) {
        JCTree.JCIdent jcIdent=(JCTree.JCIdent)jcAnnotation.getAnnotationType();
        if(jcIdent.name.contentEquals(&quot;MobCloudConsumer&quot;)){
            printLog(&quot;class Annotation arg process:{}&quot;,jcAnnotation.toString());
            jcAnnotation.args.forEach(e-&amp;gt;{
                JCTree.JCAssign jcAssign=(JCTree.JCAssign)e ;
                JCTree.JCIdent value = treeMaker.Ident(names.fromString(&quot;value&quot;));
                JCTree.JCAssign targetArg=treeMaker.Assign(value,jcAssign.rhs);
                consumerSourceAnnotationValue.put(jcAssign.lhs.toString(),targetArg);
            });
        }
        printLog(&quot;获取参数如下:&quot;,consumerSourceAnnotationValue);
        super.visitAnnotation(jcAnnotation);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;语法树中的源节点&lt;/h2&gt;
&lt;p&gt;前面说了语法树是有一个个对象组成的，这些对象构成了语法树的一个个源节点，源节点对应java语法中核心的那些语法：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;语法树节点类&lt;/th&gt;
&lt;th&gt;具体对应的语法元素&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JCClassDecl&lt;/td&gt;
&lt;td&gt;类的定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JCMethodDecl&lt;/td&gt;
&lt;td&gt;方法的定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JCAssign&lt;/td&gt;
&lt;td&gt;等式（赋值）语句&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JCExpression&lt;/td&gt;
&lt;td&gt;表达式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JCAnnotation&lt;/td&gt;
&lt;td&gt;注解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JCVariableDecl&lt;/td&gt;
&lt;td&gt;变量定义&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;语法树节点的操作&lt;/h2&gt;
&lt;p&gt;既然说了语法树的那些重要节点，后面直接上案例，该如何操作。需要注意的一点是，Java语法树中所有的操作，对于语法树节点，都不能通过引用操作来复制，必须要从头到尾构造一个一模一样的对象并插入，否则编译是过不去的。&lt;/p&gt;
&lt;h3&gt;给类增加注解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;//该方法最后会给类新增一个@FeignClient(value=&quot;&quot;)的注解
private void addClassAnnotation(Element element) {
    JCTree jcTree = trees.getTree(element);
    jcTree.accept(new TreeTranslator(){
        //遍历所有类定义
        @Override
        public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
            JCTree.JCExpression arg;
            //创建一个value的赋值语句，作为注解的参数
            if(jcAssigns==null || jcAssigns.size()==0){
                arg=makeArg(&quot;value&quot;,&quot;&quot;);
            }
            printLog(&quot;jcAssigns :{}&quot;,jcAssigns);
            //创建注解对象
            JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.FeignClient.toString(),List.of(arg));
            printLog(&quot;class Annotation add:{}&quot;,jcAnnotation.toString());
            //在原有类定义中append新的注解对象
            jcClassDecl.mods.annotations=jcClassDecl.mods.annotations.append(jcAnnotation);
            jcClassDecl.mods.annotations.forEach(e -&amp;gt; {
                printLog(&quot;class Annotation list:{}&quot;,e.toString());
            });
            super.visitClassDef(jcClassDecl);
        }
    });
}
public JCTree.JCExpression makeArg(String key,String value){
    //注解需要的参数是表达式，这里的实际实现为等式对象，Ident是值，Literal是value，最后结果为a=b
    JCTree.JCExpression arg = treeMaker.Assign(treeMaker.Ident(names.fromString(key)), treeMaker.Literal(value));
    return arg;
}
private JCTree.JCAnnotation makeAnnotation(String annotaionName, List&amp;lt;JCTree.JCExpression&amp;gt; args){
    JCTree.JCExpression expression=chainDots(annotaionName.split(&quot;\\.&quot;));
    JCTree.JCAnnotation jcAnnotation=treeMaker.Annotation(expression, args);
    return jcAnnotation;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;给类增加import语句&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;private void addImport(Element element,PackageSupportEnum... packageSupportEnums) {
    TreePath treePath = trees.getPath(element);
    JCTree.JCCompilationUnit jccu = (JCTree.JCCompilationUnit) treePath.getCompilationUnit();
    java.util.List&amp;lt;JCTree&amp;gt; trees = new ArrayList&amp;lt;&amp;gt;();
    trees.addAll(jccu.defs);
    java.util.List&amp;lt;JCTree&amp;gt; sourceImportList = new ArrayList&amp;lt;&amp;gt;();
    trees.forEach(e-&amp;gt;{
        if(e.getKind().equals(Tree.Kind.IMPORT)){
            sourceImportList.add(e);
        }
    });
    java.util.List&amp;lt;JCTree.JCImport&amp;gt; needImportList=buildImportList(packageSupportEnums);
    for (int i = 0; i &amp;lt; needImportList.size(); i++) {
        boolean importExist=false;
        for (int j = 0; j &amp;lt; sourceImportList.size(); j++) {
            if(sourceImportList.get(j).toString().equals(needImportList.get(i).toString())){
                importExist=true;
            }
        }
        if(!importExist){
            trees.add(0,needImportList.get(i));
        }
    }
    printLog(&quot;import trees{}&quot;,trees.toString());
    jccu.defs=List.from(trees);
}
private java.util.List&amp;lt;JCTree.JCImport&amp;gt; buildImportList(PackageSupportEnum... packageSupportEnums) {
    java.util.List&amp;lt;JCTree.JCImport&amp;gt; targetImportList =new ArrayList&amp;lt;&amp;gt;();
    if(packageSupportEnums.length&amp;gt;0){
        for (int i = 0; i &amp;lt; packageSupportEnums.length; i++) {
            JCTree.JCImport needImport = buildImport(packageSupportEnums[i].getPackageName(),packageSupportEnums[i].getClassName());
            targetImportList.add(needImport);
        }
    }
    return targetImportList;
}
private JCTree.JCImport buildImport(String packageName, String className) {
    JCTree.JCIdent ident = treeMaker.Ident(names.fromString(packageName));
    JCTree.JCImport jcImport = treeMaker.Import(treeMaker.Select(
            ident, names.fromString(className)), false);
    printLog(&quot;add Import:{}&quot;,jcImport.toString());
    return jcImport;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;构建一个内部类&lt;/h3&gt;
&lt;p&gt;这边演示了一个构建内部类的过程，基本就演示了深拷贝一个内部类的过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private JCTree.JCClassDecl buildInnerClass(JCTree.JCClassDecl sourceClassDecl, java.util.List&amp;lt;JCTree.JCMethodDecl&amp;gt; methodDecls) {
    java.util.List&amp;lt;JCTree.JCVariableDecl&amp;gt; jcVariableDeclList = buildInnerClassVar(sourceClassDecl);
    String lowerClassName=sourceClassDecl.getSimpleName().toString();
    lowerClassName=lowerClassName.substring(0,1).toLowerCase().concat(lowerClassName.substring(1));
    java.util.List&amp;lt;JCTree.JCMethodDecl&amp;gt; jcMethodDecls = buildInnerClassMethods(methodDecls,
            lowerClassName);
    java.util.List&amp;lt;JCTree&amp;gt; jcTrees=new ArrayList&amp;lt;&amp;gt;();
    jcTrees.addAll(jcVariableDeclList);
    jcTrees.addAll(jcMethodDecls);
    JCTree.JCClassDecl targetClassDecl = treeMaker.ClassDef(
            buildInnerClassAnnotation(),
            names.fromString(sourceClassDecl.name.toString().concat(&quot;InnerController&quot;)),
            List.nil(),
            null,
            List.nil(),
            List.from(jcTrees));
    return targetClassDecl;
}

private java.util.List&amp;lt;JCTree.JCVariableDecl&amp;gt; buildInnerClassVar(JCTree.JCClassDecl jcClassDecl) {
    String parentClassName=jcClassDecl.getSimpleName().toString();
    printLog(&quot;simpleClassName:{}&quot;,parentClassName);
    java.util.List&amp;lt;JCTree.JCVariableDecl&amp;gt; jcVariableDeclList=new ArrayList&amp;lt;&amp;gt;();
    java.util.List&amp;lt;JCTree.JCAnnotation&amp;gt; jcAnnotations=new ArrayList&amp;lt;&amp;gt;();
    JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.Autowired.toString()
            ,List.nil());
    jcAnnotations.add(jcAnnotation);
    JCTree.JCVariableDecl jcVariableDecl = treeMaker.VarDef(treeMaker.Modifiers(1, from(jcAnnotations)),
            names.fromString(parentClassName.substring(0, 1).toLowerCase().concat(parentClassName.substring(1))),
            treeMaker.Ident(names.fromString(parentClassName)),
            null);
    jcVariableDeclList.add(jcVariableDecl);
    return jcVariableDeclList;
}

private JCTree.JCModifiers buildInnerClassAnnotation() {
    JCTree.JCExpression jcAssign=makeArg(&quot;value&quot;,providerSourceAnnotationValue.get(&quot;feignClientPrefix&quot;).rhs.toString().replace(&quot;\&quot;&quot;,&quot;&quot;));
    JCTree.JCAnnotation jcAnnotation=makeAnnotation(PackageSupportEnum.RequestMapping.toString(),
            List.of(jcAssign)
    );
    JCTree.JCAnnotation restController=makeAnnotation(PackageSupportEnum.RestController.toString(),List.nil());
    JCTree.JCModifiers mods=treeMaker.Modifiers(Flags.PUBLIC|Flags.STATIC,List.of(jcAnnotation).append(restController));
    return mods;
}
//深度拷贝内部类方法
private java.util.List&amp;lt;JCTree.JCMethodDecl&amp;gt; buildInnerClassMethods(java.util.List&amp;lt;JCTree.JCMethodDecl&amp;gt; methodDecls,String serviceName) {
    java.util.List&amp;lt;JCTree.JCMethodDecl&amp;gt; target = new ArrayList&amp;lt;&amp;gt;();
    methodDecls.forEach(e -&amp;gt; {
        if (!e.name.contentEquals(&quot;&amp;lt;init&amp;gt;&quot;)) {
            java.util.List&amp;lt;JCTree.JCVariableDecl&amp;gt; targetParams=new ArrayList&amp;lt;&amp;gt;();
            e.params.forEach(param-&amp;gt;{
                JCTree.JCVariableDecl newParam=treeMaker.VarDef(
                        (JCTree.JCModifiers) param.mods.clone(),
                        param.name,
                        param.vartype,
                        param.init
                );
                printLog(&quot;copy of param:{}&quot;,newParam);
                targetParams.add(newParam);
            });
            JCTree.JCMethodDecl methodDecl = treeMaker.MethodDef(
                    (JCTree.JCModifiers) e.mods.clone(),
                    e.name,
                    (JCTree.JCExpression) e.restype.clone(),
                    e.typarams,
                    e.recvparam,
                    List.from(targetParams),
                    e.thrown,
                    treeMaker.Block(0L,List.nil()),
                    e.defaultValue
            );
            target.add(methodDecl);
        }
    });
    target.forEach(e -&amp;gt; {
        if (e.params.size() &amp;gt; 0) {
            for (int i = 0; i &amp;lt; e.params.size() ; i++) {
                JCTree.JCVariableDecl jcVariableDecl=e.params.get(i);
                if(i==0){
                    //第一个参数加requestbody注解，其他参数加requestparam注解，否则会报错
                    if(!isBaseVarType(jcVariableDecl.vartype.toString()))
                    {
                        jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(makeAnnotation(PackageSupportEnum.RequestBody.toString(), List.nil()));
                    }else {
                        JCTree.JCAnnotation requestParam=makeAnnotation(PackageSupportEnum.RequestParam.toString(),
                                List.of(makeArg(&quot;value&quot;,jcVariableDecl.name.toString())));
                        jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(requestParam);
                    }
                }else {
                    JCTree.JCAnnotation requestParam=makeAnnotation(PackageSupportEnum.RequestParam.toString(),
                            List.of(makeArg(&quot;value&quot;,jcVariableDecl.name.toString())));
                    jcVariableDecl.mods.annotations = jcVariableDecl.mods.annotations.append(requestParam);
                }

            }
        }
        printLog(&quot;sourceMethods: {}&quot;, e);
        //value
        JCTree.JCExpression jcAssign=makeArg(&quot;value&quot;,&quot;/&quot;+e.name.toString());

        JCTree.JCAnnotation jcAnnotation = makeAnnotation(
                PackageSupportEnum.PostMapping.toString(),
                List.of(jcAssign)
        );
        printLog(&quot;annotation: {}&quot;, jcAnnotation);
        e.mods.annotations = e.mods.annotations.append(jcAnnotation);
        JCTree.JCExpressionStatement exec = getMethodInvocationStat(serviceName, e.name.toString(), e.params);
        if(!e.restype.toString().contains(&quot;void&quot;)){
            JCTree.JCReturn jcReturn=treeMaker.Return(exec.getExpression());
            e.body.stats = e.body.stats.append(jcReturn);
        }else {
            e.body.stats = e.body.stats.append(exec);
        }


    });
    return List.from(target);
}
//创建方法调用，如String.format()这种
private JCTree.JCExpressionStatement getMethodInvocationStat(String invokeFrom, String invokeMethod, List&amp;lt;JCTree.JCVariableDecl&amp;gt; args) {
    java.util.List&amp;lt;JCTree.JCIdent&amp;gt; params = new ArrayList&amp;lt;&amp;gt;();
    args.forEach(e -&amp;gt; {
        params.add(treeMaker.Ident(e.name));
    });
    JCTree.JCIdent invocationFrom = treeMaker.Ident(names.fromString(invokeFrom));
    JCTree.JCFieldAccess jcFieldAccess1 = treeMaker.Select(invocationFrom, names.fromString(invokeMethod));
    JCTree.JCMethodInvocation apply = treeMaker.Apply(nil(), jcFieldAccess1, List.from(params));
    JCTree.JCExpressionStatement exec = treeMaker.Exec(apply);
    printLog(&quot;method invoke:{}&quot;, exec);
    return exec;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;p&gt;注解器的实际使用，需要在resource文件夹下的META-INF.services文件夹下，新建一个叫做javax.annotation.processing.Processor的文件，里面写上需要生效的类注解处理器的包名加类名，例如：cn.intotw.mob.ModCloudAnnotationProcessor。
然后如果是作为第三方jar包提供给别人，需要在maven打包时增加如下配置，主要也是把javax.annotation.processing.Processor文件也打包到对应目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;build&amp;gt;
    &amp;lt;resources&amp;gt;
        &amp;lt;resource&amp;gt;
            &amp;lt;directory&amp;gt;src/main/resources&amp;lt;/directory&amp;gt;
            &amp;lt;excludes&amp;gt;
                &amp;lt;exclude&amp;gt;META-INF/**/*&amp;lt;/exclude&amp;gt;
            &amp;lt;/excludes&amp;gt;
        &amp;lt;/resource&amp;gt;
    &amp;lt;/resources&amp;gt;
    &amp;lt;plugins&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;maven-resources-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;2.6&amp;lt;/version&amp;gt;
            &amp;lt;executions&amp;gt;
                &amp;lt;execution&amp;gt;
                    &amp;lt;id&amp;gt;process-META&amp;lt;/id&amp;gt;
                    &amp;lt;phase&amp;gt;prepare-package&amp;lt;/phase&amp;gt;
                    &amp;lt;goals&amp;gt;
                        &amp;lt;goal&amp;gt;copy-resources&amp;lt;/goal&amp;gt;
                    &amp;lt;/goals&amp;gt;
                    &amp;lt;configuration&amp;gt;
                        &amp;lt;outputDirectory&amp;gt;target/classes&amp;lt;/outputDirectory&amp;gt;
                        &amp;lt;resources&amp;gt;
                            &amp;lt;resource&amp;gt;
                                &amp;lt;directory&amp;gt;${basedir}/src/main/resources/&amp;lt;/directory&amp;gt;
                                &amp;lt;includes&amp;gt;
                                    &amp;lt;include&amp;gt;**/*&amp;lt;/include&amp;gt;
                                &amp;lt;/includes&amp;gt;
                            &amp;lt;/resource&amp;gt;
                        &amp;lt;/resources&amp;gt;
                    &amp;lt;/configuration&amp;gt;
                &amp;lt;/execution&amp;gt;
            &amp;lt;/executions&amp;gt;
        &amp;lt;/plugin&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;configuration&amp;gt;
                &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
            &amp;lt;/configuration&amp;gt;
        &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;chainDots方法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;public  JCTree.JCExpression chainDots(String... elems) {
        assert elems != null;

        JCTree.JCExpression e = null;
        for (int i = 0 ; i &amp;lt; elems.length ; i++) {
            e = e == null ? treeMaker.Ident(names.fromString(elems[i])) : treeMaker.Select(e, names.fromString(elems[i]));
        }
        assert e != null;

        return e;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;建议这类复杂的语法树操作，不要用来直接在生产进行复杂的扩展，我们本次使用也只是为了快速生成代码。防止手动cp出现失误。原因还是maven构建过程很复杂，哪怕你本地测试通过，真的到了实际项目复杂的构建过程后，不一定能保证代码正确性，甚至会和dubbo以及lombok等组件冲突。&lt;/p&gt;
&lt;p&gt;这个技术主要也是以摸索API使用为主，国内没有什么资料，国外的资料也都是语法和类的介绍，实际例子并不多，花了很多时间摸索具体使用的方法，基本能达到实现一切操作了，毕竟注解，方法，类，变量，方法调用，这些都能自定义了，基本也没有什么别的了。期间参考了不少lombok的源码，lombok是在java语法树节点之外利用自己的语法树节点封装了一层，简化和规范了很多操作，可惜我找了一下lombok貌似并没有提供类似于工具包辅助，所以更加深入的使用推荐参考lombok源码的实现。&lt;/p&gt;
</content:encoded></item><item><title>Java中的NIO</title><link>https://www.intotw.cn/posts/java-nio/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-nio/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;近日学习Netty，在看书和实践的时候对于书上只言片语的那些话不是十分懂，导致尝试写例子的时候遭遇各种不顺，比如decoder和encoder还有HttpObjectAggregator的添加顺序，研究了一番之后和大家分享一下自己的理解，希望后来人可以少走弯路。&lt;/p&gt;
&lt;h2&gt;IO与NIO的区别&lt;/h2&gt;
&lt;p&gt;IO是Input与Output的缩写，主要意思就是输入输出，主要以及经常使用到的包括网络，文件中的IO。
传统IO完整的名称是同步阻塞IO，特点是IO过程中线程阻塞，等待IO返回，Java中普通的Sokcet通讯，都是同步阻塞IO。
NIO完整的名称是同步非阻塞IO，注意这里的区别，并不是异步。NIO需要操作系统底层支持，在Linux中以前有select和poll的机制，现在被更优秀的epoll代替，但这也产生了JavaNIO里selector关于epoll的一个Bug。&lt;/p&gt;
&lt;p&gt;两者主要的区别和优缺点列举一下：&lt;/p&gt;
&lt;h3&gt;IO&lt;/h3&gt;
&lt;p&gt;传统IO即SIO等待IO时阻塞线程，所以面对高并发比较无力，连接数越多，需要的线程数多，基本1请求对应1线程，但延迟较低，线程等待数据返回后只需要再等到CPU时间片就可以马上处理在Java中API简单易懂，方便使用，线程模型简单，可以轻松完成1请求1返回的对应&lt;/p&gt;
&lt;h3&gt;NIO&lt;/h3&gt;
&lt;p&gt;通过epoll等机制，可以做到少数线程只轮询事件，轮询到事件后再交由其他线程处理，线程利用率高,面对高并发表现优秀，更少的线程数处理更多的连接，线程无需阻塞等待IO只需处理已经触发的事件,延迟稍高，虽然epoll在事件调度上比select和poll优秀了很多，但是应用程序还是需要遍历事件依次处理,在Java中API看似简单，实际坑多，需要好的线程模型支持，请求与返回之间较难1对1对应&lt;/p&gt;
&lt;h2&gt;Java中的NIO&lt;/h2&gt;
&lt;p&gt;首先大概解释下Java的NIO中比较重要的几个概念&lt;/p&gt;
&lt;p&gt;SocketChannel：包括ServerSocketChannel这类，可以简单的认为是代表了连接，事实上使用过程中它一般表现的也像个连接。&lt;/p&gt;
&lt;p&gt;Selector：可以认为是一种工具，执行selector可以从连接中获取各个连接就绪好的事件，如读（Read）写（Write）就绪，开启关闭，从ServerSocketChannel中可以接收到新进连接（Accept）事件。&lt;/p&gt;
&lt;p&gt;ByteBuffer：封装了一些字节操作方法，可以认为是一个byte数组，类似于IO中读时常定义的byte[] buffer这种，区别是方法更多，可以操作堆内内存，也可以使用堆外内存。&lt;/p&gt;
&lt;p&gt;下面上一个简单例子，说明这3个东西具体怎么使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws IOException {
        int channelId=0;
        //开启ServerSocketChannel和Selector
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(
                InetAddress.getLocalHost(), 4700);
        //使用ServerSocketChannel绑定端口
        serverSocketChannel.socket().bind(inetSocketAddress);
        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //设置非阻塞模式将selector注册到该Channel上，并设置该selector在该channel上监听Accept事件，该事件标致一个连接建立
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT).attach(
                channelId++);
        //循环遍历事件
        for (;;)  {
            //执行事件查询，去各个注册到该selector的channel上检查是否有就绪事件，该方法会阻塞，可以设置等待最长时间
            selector.select();
            //获取selector检查到的就绪事件
            Set&amp;lt;SelectionKey&amp;gt; readySelectionKey = selector
                    .selectedKeys();
            Iterator&amp;lt;SelectionKey&amp;gt; it = readySelectionKey.iterator();
            while (it.hasNext()) {
                SelectionKey selectionKey = it.next();
                //事件判断
                if (selectionKey.isAcceptable()) {// 客户请求连接
                    //从key中获取channel
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey
                            .channel();
                    //因为是Accept事件，调用方法获取具体的连接Channel，SocketChannel
                    SocketChannel socketChannel=channel.accept();
                    //同样，给具体的连接注册读写事件，直接使用同一个selector去注册
                    socketChannel.configureBlocking(false)
                            .register(
                                    selector,
                                    SelectionKey.OP_READ
                                            | SelectionKey.OP_WRITE).attach(channelId++);
                }
                if (selectionKey.isReadable()) {
                    //获取附加对象
                    int nowChannelId= (int) selectionKey.attachment();
                    //可读事件，通过key的方法获取到可读的channel
                    SocketChannel clientChannel=(SocketChannel)selectionKey.channel();
                    //创建一个大小为64字节
                    ByteBuffer receiveBuf = ByteBuffer.allocate(64);
                    //从channel中读取数据
                    clientChannel.read(receiveBuf);
                }
                if (selectionKey.isWritable()) {// 写数据
                    //同读取
                    SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer sendBuf = ByteBuffer.allocate(64);
                    String sendText = &quot;hello,world&quot;;
                    sendBuf.put(sendText.getBytes());
                    sendBuf.flip();
                    clientChannel.write(sendBuf);
                }
                if (selectionKey.isConnectable()) {
 
                }
                //处理完后remove该事件
                it.remove();
            }
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解释几个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;常用的有2种channel，一个ServerSocketChannel，用来绑定网络端口，获得具体链接，从他的Accept事件中获取具体的连接，另一种则可以认为是具体的某个连接，可以在其之上处理读写事件&lt;/li&gt;
&lt;li&gt;selector的注册，参数为（channel，监听的事件，还有附加对象），这里附加对象可以在注册时创建一个对象附加，可以通过key去获取，一般用来标识该channel的种类或者用来持有该channel事件的具体处理对象。&lt;/li&gt;
&lt;li&gt;流程简而言之就是不断的select获取连接，再针对每个连接注册读写事件，然后如此循环处理&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Java中的NIO进阶</title><link>https://www.intotw.cn/posts/java-nio-advanced/</link><guid isPermaLink="true">https://www.intotw.cn/posts/java-nio-advanced/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前一篇文章简单介绍了NIO，并附了一个简单的例子，但是自己试一下就会知道，简单的使用NIO是无法满足开发需要的，因为NIO处理的思路和日常servlet加spring中习惯的一连接一线程有很大不同。&lt;/p&gt;
&lt;h2&gt;NIO与多线程&lt;/h2&gt;
&lt;p&gt;上篇那个例子实现了一个简单的NIO，但是实际使用中我们不可能仅仅在单线程下使用，肯定会使用多线程提高处理的效率，但是这样就会有几个难点。&lt;/p&gt;
&lt;h2&gt;Readable和Writeable的空触发&lt;/h2&gt;
&lt;p&gt;之前的文章的代码有写到通过key去判断readable和writeable这两个事件，从而去读写，但是会出现以下情况&lt;/p&gt;
&lt;p&gt;实际上这2个事件并不像看上去那样，在writeable之中负责写内容，以最常用的http的请求-返回场景来看，通常上返回的处理是在read处理完后写内容到channel里去，writeable里只做一个flush操作，并不是readable只负责读，writeable只负责写。&lt;/p&gt;
&lt;p&gt;在多线程下，这两个事件的处理尤为糟糕，并不能简单的去实现Runable再套用函数进去执行，因为这两个事件本身在key被remove前，会一直触发，如果第一个执行的线程执行完之前，这个key因为没有remove被再次获取到去执行，那么可能就会产生很多问题，比如读取不到数据（已经被第一个线程读取了），channel已经关闭（第一个线程或其他线程写完后，本机或者远端关闭了连接）。这样都会影响多线程实际执行。&lt;/p&gt;
&lt;p&gt;Readable和Writeable事件产生一般对应着待读数据的到来和待写数据的就绪，就绪后产生key提醒应用去处理。但是有2种情况下会产生实际没什么事可做的空触发。一种是有线程在处理key，但是没有处理完，key未被取消，这时每次select依旧会出现这个key，但实际此key正在处理中。另外一种就是只要连接还在，NIO会频繁触发Writeable事件，这就是为什么1中写道通常writeable只做flush，同样，Readable只要有数据待读就一直会触发，这会使做消息聚合、报文拼接的时候很难处理。&lt;/p&gt;
&lt;h2&gt;请求与返回的处理&lt;/h2&gt;
&lt;p&gt;NIO因为是非阻塞的原因，请求与返回并不是一一对应的，仅做服务端还好，以接收请求为主，在Readable事件处理完后写返回内容就可以了。但是如果既做请求端又做服务端的情况下，请求Write后，处理Read的早已不知道是哪个时间点的哪个进程了，此时如果请求和返回有关联，则较难一一对应上，这一点连著名的Netty框架都没有提供一些简便方式去完成，不过这也体现了他的纯粹，在dubbo中是通过CountDownLatch实现的，一般还有使用futureTask去实现异步转同步的操作。&lt;/p&gt;
&lt;h2&gt;事件的处理机制&lt;/h2&gt;
&lt;p&gt;通常情况下，接收到请求肯定要根据请求的内容进行不同的处理，简单来说起码要根据交易码不同分类不同的交易处理。这样的话面对复杂协议（如http），从报文处理到请求分类都有一个很复杂的逻辑和过程，如果没有合适的事件处理机制模型，会导致处理起来有太多的判断代码，难以维护。&lt;/p&gt;
&lt;h2&gt;NIO多线程使用的一个例子&lt;/h2&gt;
&lt;p&gt;这里参照了Java大神Doug Lea的思路，提供一个多线程的例子，主要思路包括&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过注册时的attach，将该channel的处理类直接关联起来，后面取出key时直接调用处理类去处理key&lt;/li&gt;
&lt;li&gt;处理类在处理时添加一个volatile成员做状态标记，该channel正在处理事件时，该标记为处理中，此时该channel的其他key处理到时都将判断该标记，如果该channel在处理中，则直接退出等待下次select出来继续判断。&lt;/li&gt;
&lt;li&gt;使用了线程池去处理&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        //主ServerSocket绑定Accept事件，处理类为Accept
        SelectionKey key=serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, new Accept(serverSocketChannel, selector));
        for(;;) {
            selector.select();
            Set&amp;lt;SelectionKey&amp;gt; selectionKeys = selector.selectedKeys();
            Iterator&amp;lt;SelectionKey&amp;gt; iterator = selectionKeys.iterator();
 
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                //分发处理key
                dispatchTask(selectionKey);
            }
            selectionKeys.clear();
        }
    }
 
    private static void dispatchTask(SelectionKey selectionKey) {
        //这里的attachment，针对ServerSocketChannel，是Accept，针对进来的连接SocketChannel，是TaskHandler
        Runnable runnable = (Runnable)selectionKey.attachment();
        if (runnable != null) {
            runnable.run();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class Accept implements Runnable{
    ServerSocketChannel serverSocketChannel;
    Selector selector;
    public Accept(ServerSocketChannel serverSocketChannel, Selector selector) {
        this.serverSocketChannel=serverSocketChannel;
        this.selector=selector;
    }
 
    @Override
    public void run() {
        try {
            SocketChannel socketChannel=serverSocketChannel.accept();
            //给进来的连接注册selector，这里直接通过构造函数来了
            new TaskHandler(socketChannel,selector);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class TaskHandler implements Runnable{
    private SocketChannel socketChannel;
    private SelectionKey selectionKey;
    private volatile int state = PROCESSED;
    private static final ExecutorService pool = Executors.newFixedThreadPool(4);
 
    static final int PROCESSING=1;
    static final int PROCESSED=2;
 
    public TaskHandler(SocketChannel socketChannel, Selector selector) throws IOException {
        this.socketChannel=socketChannel;
        socketChannel.configureBlocking(false);
        //把自己attach上去
        this.selectionKey=socketChannel.register(selector,SelectionKey.OP_READ,this);
        selector.wakeup();
    }
 
    @Override
    public void run() {
        //判断处理状态
        if(state==PROCESSED)
        {
            pool.execute(new Process(selectionKey));
        }
    }
    class Process implements Runnable{
        private SelectionKey selectionKey;
 
        public Process(SelectionKey selectionKey) {
            this.selectionKey = selectionKey;
            state=PROCESSING;
        }
 
        @Override
        public void run() {
            try {
                if(socketChannel.socket().isClosed())
                    return ;
                if(selectionKey.isReadable())
                {
                    read();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
 
        private void read() throws IOException {
            ByteBuffer byteBuffer=ByteBuffer.allocate(64);
            SocketChannel socketChannel= (SocketChannel) selectionKey.channel();
            int read;
            System.out.println(&quot;readding&quot;);
            if ((read = socketChannel.read(byteBuffer)) &amp;lt; 0)  {
                state = PROCESSED;
                socketChannel.close();
                return;
            }
            System.out.println(new String(byteBuffer.array()));
            state=PROCESSED;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>leetcode刷题录-1395</title><link>https://www.intotw.cn/posts/leetcode-1395/</link><guid isPermaLink="true">https://www.intotw.cn/posts/leetcode-1395/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;题目地址：https://leetcode-cn.com/problems/count-number-of-teams/&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;n 名士兵站成一排。每个士兵都有一个 独一无二 的评分 rating 。
每 3 个士兵可以组成一个作战单位，分组规则如下：
从队伍中选出下标分别为 i、j、k 的 3 名士兵，他们的评分分别为 rating[i]、rating[j]、rating[k]
作战单位需满足： rating[i] &amp;lt; rating[j] &amp;lt; rating[k] 或者 rating[i] &amp;gt; rating[j] &amp;gt; rating[k] ，其中  0 &amp;lt;= i &amp;lt; j &amp;lt; k &amp;lt; n
请你返回按上述条件可以组建的作战单位数量。每个士兵都可以是多个作战单位的一部分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;思考过程&lt;/h2&gt;
&lt;p&gt;这个题目乍一看，就觉得像是贪心或者是DP，最近做了几道DP题，马上就先找有没有最优子结构来导出结果。&lt;/p&gt;
&lt;p&gt;但是仔细想了想，找不到什么最优子结构，浪费了不少时间，但是发现了一个规律，即对于任意i来说，以i为根节点构建一颗树，以i开头的任意子节点均满足递增或递减，就可以快速计算出总数目。如题例 [2,5,3,4,1]，可以构建2-&amp;gt;3-&amp;gt;4,5-&amp;gt;3-&amp;gt;1,5-&amp;gt;4-&amp;gt;1，这2棵树。其中保存的就是结果，对于深度为3的来说，满足的结果数就是1，对于深度为4来说，满足的结果数就是4，对于深度为5来说的树，满足的结果数就是10，规律也比较简单，深度每加1，增加的结果数就是排列组合求的Cn-1 2，比如深度3为结果1，深度4=1+C3 2=4，深度5=4+C4 2=10。根据这种方法，对数组进行遍历并且构建一颗树，然后进行BFS查看深度进行计算就可以了。并且这种方法是会保留结果的方法。&lt;/p&gt;
&lt;h2&gt;查看别人分享的思路&lt;/h2&gt;
&lt;p&gt;虽然有了解法，但是毕竟一开始初衷还是贪心或者DP，还是不甘心，于是看了一下别人分享的题解，确实有比较取巧的解法，是我一开始的思路错了，这种思路简而言之就是对于任意一个i，把他当做中间数，求得左侧小于i的数的个数left_small，左侧大于i的数的个数left_big，右侧小于i的数的个数right_small，右侧大于i的数的个数right_big，此时就有以i为中间数的结果=left_small&lt;em&gt;right_big+left_big&lt;/em&gt;right_small，这种思路实现简单，缺点是没有保存中间结果，最后上下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func numTeams(rating []int) int {
	ans:=0
	for i:=0;i&amp;lt;len(rating);i++{
		left_big:=0
		right_big:=0
		left_small:=0
		right_small:=0
		for j:=i-1;j&amp;gt;=0;j--{
			if rating[j]&amp;gt;rating[i] {
				left_big++
			}
			if rating[j]&amp;lt;rating[i]{
				left_small++
			}
		}
		for k:=i+1;k&amp;lt;len(rating);k++{
			if rating[k]&amp;gt;rating[i] {
				right_big++
			}
			if rating[k]&amp;lt;rating[i]{
				right_small++
			}
		}
		ans+=left_small*right_big+left_big*right_small
	}
	return ans
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;最近刷LeetCode，水题太水，有手就行，还是要刷点稍微需要一些思考的题目，才能提升自己，以后尽量坚持3简单+1中等的进度。&lt;/p&gt;
</content:encoded></item><item><title>Linux常用命令</title><link>https://www.intotw.cn/posts/linux-shell/</link><guid isPermaLink="true">https://www.intotw.cn/posts/linux-shell/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;根据进程名杀死进程&lt;/h3&gt;
&lt;p&gt;ps -ef | grep process_name | grep -v grep | awk &apos;{print $2}&apos; | xargs kill -9&lt;/p&gt;
&lt;h3&gt;docker拉取镜像&lt;/h3&gt;
&lt;p&gt;docker pull localhost:5000/v2/moyu-eureka-server:latest&lt;/p&gt;
&lt;h3&gt;docker删除镜像&lt;/h3&gt;
&lt;p&gt;docker images |grep moyu|grep -v grep | awk &apos;{print $3}&apos; | xargs docker rmi&lt;/p&gt;
&lt;h3&gt;docker关闭容器&lt;/h3&gt;
&lt;p&gt;docker ps |grep moyu|grep -v grep | awk &apos;{print $1}&apos; | xargs docker stop&lt;/p&gt;
&lt;h3&gt;docker删除容器&lt;/h3&gt;
&lt;p&gt;docker ps -a|grep moyu|grep -v grep | awk &apos;{print $1}&apos; | xargs docker rm&lt;/p&gt;
&lt;h3&gt;docker启动容器&lt;/h3&gt;
&lt;p&gt;docker run -v /etc/localtime:/etc/localtime --name moyu-eureka-server -itd -p 8761:8761 localhost:5000/v2/moyu-eureka-server&lt;/p&gt;
&lt;h3&gt;删除指定目录&lt;/h3&gt;
&lt;p&gt;find 目录 -name &quot;*.abc&quot; | xargs rm&lt;/p&gt;
&lt;h3&gt;查看内存占用前五进程&lt;/h3&gt;
&lt;p&gt;ps auxw | head -1;ps auxw|sort -rn -k4|head -5&lt;/p&gt;
&lt;h3&gt;查看CPU占用前三进程&lt;/h3&gt;
&lt;p&gt;ps auxw|head -1;ps auxw|sort -rn -k3|head -3
##查看TCP连接情况&lt;/p&gt;
&lt;p&gt;###查看某端口的占用：
lsof -i :8080&lt;/p&gt;
&lt;p&gt;###查看所有tcp连接：
lsof -i tcp&lt;/p&gt;
&lt;p&gt;###统计mysql连接数：
lsof -i tcp|grep mysql|wc -lbr&amp;gt;&lt;/p&gt;
&lt;p&gt;###查看文件被哪些进程使用
lsof -t $file_name&lt;/p&gt;
&lt;p&gt;###查看进程使用了哪些文件
lsof -p $pid&lt;/p&gt;
&lt;p&gt;###查看进程里有哪些线程
ps -mp pid -o THREAD,tid,time 输出pid16进制，对应jstack中的nid
printf &quot;%x\n&quot; tid
jstack pid |grep 上述16进制的nid -A 30&lt;/p&gt;
</content:encoded></item><item><title>Netty学习：EventLoop事件机制</title><link>https://www.intotw.cn/posts/netty-eventloop/</link><guid isPermaLink="true">https://www.intotw.cn/posts/netty-eventloop/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;EventLoop是什么&lt;/h2&gt;
&lt;p&gt;如果你去百度EventLoop，肯定会百度到很多关于JavaScript，NodeJS的文章，是的，这两种语言的事件机制就依赖于EventLoop，但是EventLoop到底是什么，可以先思考2个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一般情况下，当我们要实现令一个线程不断处理任务，都是选择使用while(true){……}这样的结构，但是往往为了防止无限循环空跑占用CPU时间，会在死循环中使用sleep()来空出时间。有更好的方法吗？&lt;/li&gt;
&lt;li&gt;Redis的性能毋庸置疑，但是Redis是单线程的，单线程的Redis是如何达到不逊色于多线程的性能的呢？(当然也是EventLoop)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;EventLoop事实上是一种线程编程模型，简而言之，将需要执行的任务封装成原子化的Event，都交到一个线程去执行，而这个线程唯一的任务就是不断的从自己的Event事件池中取出来Event去执行。那么为什么叫Loop呢？因为这种模型适用于非阻塞的操作，在非阻塞Event发生后，将该Event以及回调函数封装成一个CheckEvent，继续扔到Event池中，在不断执行Event的过程中，当执行到CheckEvent时，会去检查非阻塞操作是否成功，成功则执行回调，未成功则继续扔进去等待下一次，所以这种模型叫做EventLoop=事件轮询。&lt;/p&gt;
&lt;h2&gt;EventLoop适用的场景&lt;/h2&gt;
&lt;p&gt;或许你会说，EventLoop性能这么高，这么帅，那大家都用不就好了？事实上，EventLoop也有他局限的场景，如上所说，这种编程场景多用于该系统大多数事件可以封装成异步事件时，EventLoop会有更好的吞吐和执行效率，比如JavaScript要在js执行完后交由浏览器进行绘制这种图形客户端场景，或者redis这种使用密集IO型，或者Netty这种天生NIO框架。简而言之：异步非阻塞操作多，回调机制多。当你需要Check的事件不多，都是实际需要执行的任务时，EventLoop比起线程池，优势就微乎其微了，并且EventLoop的实现还更为复杂。所以不是什么场景都适合使用的。&lt;/p&gt;
&lt;h2&gt;Netty中的EventLoop&lt;/h2&gt;
&lt;p&gt;Netty中使用的当然也是EventLoop，这是它的一个优点或者说提升它性能的原因。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EventLoopGroup group = new NioEventLoopGroup();        
Bootstrap b = new Bootstrap();
b.group(group)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Netty代码肯定少不了这几句，其中NioEventLoopGroup就是Netty的EventLoop实现，一个Group中包含多个EventLoop，类似线程池和线程的关系。支持构造函数传进去里面的线程数，如果不传的话默认是CPU数*2。&lt;/p&gt;
&lt;h3&gt;Netty中的大量inEventLoop判断&lt;/h3&gt;
&lt;p&gt;看过Netty源码的很多人肯定会看到Netty的源码中有大量的如下判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (!inEventLoop &amp;amp;&amp;amp; !executor.inEventLoop(currentThread)) {
    executor.execute(new Runnable() {
        
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个判断是为什么呢？我一开始也很疑惑，后面随着对Netty的理解，才知道，对Netty来说，Netty许多函数的调用方，它并不知道调用方是谁，是在自己的内部EventLoop内，还是在用户线程里，所以它在一些用户很可能在用户线程里调用的方法，增加了这类判断，将这些方法的执行转移到EventLoop中。&lt;/p&gt;
&lt;h3&gt;Netty是如何建立连接并监听端口的-NIOSocketChannel&lt;/h3&gt;
&lt;p&gt;初看Netty源码，Netty是怎么建立端口监听以及连接的，都找了我许久。先上一段标准代码，这是netty源码例子中的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer&amp;lt;SocketChannel&amp;gt;() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline p = ch.pipeline();
                if (sslCtx != null) {
                    p.addLast(sslCtx.newHandler(ch.alloc()));
                }
                //p.addLast(new LoggingHandler(LogLevel.INFO));
                p.addLast(serverHandler);
            }
        });

    // Start the server.
    ChannelFuture f = b.bind(PORT).sync();

    // Wait until the server socket is closed.
    f.channel().closeFuture().sync();
} finally {
    // Shut down all event loops to terminate all threads.
    bossGroup.shutdownGracefully();
    workerGroup.shutdownGracefully();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到其中主要就是配置了EventLoopGroup,channel，handler，这三个东西，这三个东西就是netty三大件，总领了Netty。建立连接的就是其中的NioServerSocketChannel，这个NioSocketChannel这种是Netty包装了Java原生的SocketChannel，在启动时（server）或者进行请求时（client）通过反射进行创建并运行。&lt;/p&gt;
&lt;h3&gt;Netty,bossGroup与workerGroup&lt;/h3&gt;
&lt;p&gt;Netty默认构造函数，支持设置2个EventLoopGroup，其中分为Boss和Worker，其中Boss只负责处理连接请求的建立，以及key的select，类似主线程，Worker负责具体的读写，handler处理等其他任务，官方建议这两个的线程数比是1:5，实际使用中可以看情况设置。&lt;/p&gt;
</content:encoded></item><item><title>SpringBoot WebSocket技术</title><link>https://www.intotw.cn/posts/spring-websocket/</link><guid isPermaLink="true">https://www.intotw.cn/posts/spring-websocket/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近看了Spring in Action，了解了一下WebSocket和Stomp协议相关技术，并搭建了一个项目。网上的例子不完整或者描述不清，所以自己记录一下以作备忘。&lt;/p&gt;
&lt;h2&gt;一.配置&lt;/h2&gt;
&lt;p&gt;Spring Boot项目搭建完成后，基于Spring Boot一切皆配置的概念，添加WebSocket支持十分简单。&lt;/p&gt;
&lt;p&gt;首先是maven依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;spring-boot-starter-websocket&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是使用的Spring Mvc的话，可能需要添加另外的2个依赖。&lt;/p&gt;
&lt;p&gt;然后是添加配置类：WebSocketConfig&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        stompEndpointRegistry.addEndpoint(&quot;/endpointSang&quot;).withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker(&quot;/happy&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的两个路径：
1.addEndpoint添加的第一个路径，是监听WebSocket连接的Stomp代理的端点，页面请求WebSocket连接时，连接到注册的该端点上Stomp代理，之后的消息会交给Stomp代理处理。&lt;/p&gt;
&lt;p&gt;2.该配置启用了一个简单的消息代理，用来处理前缀为/happy的消息，也就是说，只有路径为/happy请求时，消息才会由消息代理处理&lt;/p&gt;
&lt;h3&gt;二.后端配置控制器Controller&lt;/h3&gt;
&lt;p&gt;Controller十分相似，部分注解略有不同:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.example.demo.bean.TestMessage;
import com.example.demo.bean.TestResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

@Controller
public class WsController {
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    @MessageMapping(&quot;/welcome&quot;)//接收路径
    @SendTo(&quot;/happy/getNewResponse&quot;)//消息返回到的路径
    public TestResponse say(TestMessage message) {
        System.out.println(message.getName());
        say1();//调用另外的方式返回（由服务器主动发起的返回）
        return new TestResponse(&quot;welcome,&quot; + message.getName() + &quot; !&quot;);//这次同步通信的返回
    }

    public void say1() {
        Date date =new Date(System.currentTimeMillis());
        DateFormat df = new SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        System.out.println(date);
        messagingTemplate.convertAndSend(&quot;/happy/getHappyResponse&quot;, df.format(new Date()));//设置路径以及内容，返回当前服务器时间
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;TestMessage与TestResponse为普通JavaBean，消息转换机制等与普通Controller基本一致&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SimpMessagingTemplate该类提供为主动向页面正在监听WebSocket的程序发送消息的功能&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@MessageMapping注解，与@RequestMapping注解类似，配置后台接收消息的路径以及处理函数&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@SendTo注解，一般与@MessageMapping注解一起使用，该注解配置的控制器，返回的数据将发送到监听该配置路径的监听函数&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;三.前端页面&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html lang=&quot;en&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;/&amp;gt;
    &amp;lt;title&amp;gt;广播式WebSocket&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;../static/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;../static/stomp.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;../static/sockjs.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body onload=&quot;disconnect()&quot;&amp;gt;
&amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;button id=&quot;connect&quot; onclick=&quot;connect();&quot;&amp;gt;连接&amp;lt;/button&amp;gt;
        &amp;lt;button id=&quot;disconnect&quot; disabled=&quot;disabled&quot; onclick=&quot;disconnect();&quot;&amp;gt;断开连接&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div id=&quot;conversationDiv&quot;&amp;gt;
        &amp;lt;label&amp;gt;输入你的名字&amp;lt;/label&amp;gt;&amp;lt;input type=&quot;text&quot; id=&quot;name&quot;/&amp;gt;
        &amp;lt;button id=&quot;sendName&quot; onclick=&quot;sendName();&quot;&amp;gt;发送&amp;lt;/button&amp;gt;
        &amp;lt;p id=&quot;response&quot;&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;script type=&quot;text/javascript&quot;&amp;gt;
    var stompClient = null;
    function setConnected(connected) {
        document.getElementById(&quot;connect&quot;).disabled = connected;
        document.getElementById(&quot;disconnect&quot;).disabled = !connected;
        document.getElementById(&quot;conversationDiv&quot;).style.visibility = connected ? &apos;visible&apos; : &apos;hidden&apos;;
        $(&quot;#response&quot;).html();
    }
    function connect() {
        var socket = new SockJS(&apos;/endpointSang&apos;);//通过先前配置的端点建立连接
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log(&apos;Connected:&apos; + frame);
            //开启监听，监听服务器推送到路径/happy/getNewResponse
            stompClient.subscribe(&apos;/happy/getNewResponse&apos;, function (response) {
                alert(JSON.parse(response.body).responseMessage);
            });
            //开启监听，监听服务器推送到路径/happy/getHappyResponse
            stompClient.subscribe(&apos;/happy/getHappyResponse&apos;, function (response) {
                console.log(response.body);
            })
        });
    }
    //关闭连接
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log(&apos;Disconnected&apos;);
    }
    //主动发送信息
    function sendName() {
        var name = $(&apos;#name&apos;).val();
        console.log(&apos;name:&apos; + name);
        //发送信息到后台监听/welcome路径的controller
        stompClient.send(&quot;/welcome&quot;, {}, JSON.stringify({&apos;name&apos;: name}));
    }
    function showResponse(message) {
        $(&quot;#response&quot;).html(message);
    }
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果控制台日志：&lt;/p&gt;
&lt;p&gt;注：
广播模式，只要所有程序监听同一个后台广播路径就可以了
点对点通信模式，可以在Js端使用随机数或者根据TokenId开启监听路径，后台根据用户的TokenId派发到不同端点就可以了&lt;/p&gt;
</content:encoded></item><item><title>工作记录：一次线上服务宕机问题排查</title><link>https://www.intotw.cn/posts/work-server-shutdown/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-server-shutdown/</guid><pubDate>Wed, 14 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题的发现&lt;/h2&gt;
&lt;p&gt;早上上班，运维告警，说账户模块的服务全部CPU以及内存告警，当时正在地铁早高峰，所以他们留下了一台在dump，其他机器立马重启，重启后恢复，上班后立马开始排查。
一开始dump文件没出来，后续运维告知dump也失败了，其实按照结果来看，这个问题要是有dump的话， 当时一眼就能看出来问题，可惜没dump走了不少弯路。&lt;/p&gt;
&lt;h2&gt;问题的分析&lt;/h2&gt;
&lt;p&gt;从日志看来，当时报错最早始于报了大量的dubbo远程调用超时，后续出现redis超时，mysql超时，所以一开始怀疑是网络问题，经过各种排查，只能查出来当时流量异常，服务器的流量全都打满了，但是网络问题比较玄学，无法下定论，一时半会儿陷入了困境。&lt;/p&gt;
&lt;p&gt;后续考虑从其他方式分析，如不只看报错日志，看最近版本的提交记录和功能做代码层分析，最后定位到了问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先是druid，druid在执行耗时长时会打印出来当时正在执行的sql。发现有一条sql一直在执行，从系统异常开始到系统异常结束一直在执行。&lt;/li&gt;
&lt;li&gt;具体分析sql，发现是最近一次迭代的管理台功能中，查询用户卡号功能，当未输入任何参数时，因为mybatis的写法如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;     select
    &amp;lt;include refid=&quot;Base_Column_List&quot;/&amp;gt;
    from account_bank_info
    &amp;lt;where&amp;gt;
        &amp;lt;if test=&quot;accountName != null and  accountName !=&apos;&apos;&quot;&amp;gt;
            and ACCOUNT_NAME LIKE concat(&apos;%&apos;, #{accountName,jdbcType=VARCHAR}, &apos;%&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;accountName != null and  accountName !=&apos;&apos;&quot;&amp;gt;
            and ACCOUNT_NAME LIKE concat(&apos;%&apos;, #{accountName,jdbcType=VARCHAR}, &apos;%&apos;)
        &amp;lt;/if&amp;gt;
        &amp;lt;if test=&quot;accountName != null and  accountName !=&apos;&apos;&quot;&amp;gt;
            and ACCOUNT_NAME LIKE concat(&apos;%&apos;, #{accountName,jdbcType=VARCHAR}, &apos;%&apos;)
        &amp;lt;/if&amp;gt;
        ……
    &amp;lt;/where&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到使用了大量where加if！=null的拼接，但是又未做默认limit，导致在未输出任何参数，即查询条件全部为null时，直接对几百万数据的绑卡表进行了拖库，上百万数据直接打满流量，导致dubbo和其他需要网络功能的组件全部网络超时。最后几百万的数据导致了垃圾回收完全回收不到任何数据，CPU以及内存打满，服务宕机。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;生产问题的排查，能直接从异常看出来的问题真的都不算什么问题，这种比较隐含的并且时候dump失败的，是真的很难排查。其实当时有dump的话，一看堆内对象，就能马上定位到这个sql了，没有的话，只能通过其他方式一一排查。事后叫运维优化了dump，除了检测oom之类的异常之外，在系统CPU长时间跑满的情况下也提前进行dump（CPU跑满基本就是垃圾回收触发的标志，长时间跑满一般就是垃圾回收失效，内存不够，疯狂的无限进行gc但没有释放任何空间），防止bug发酵导致无法dump的情况。&lt;/p&gt;
</content:encoded></item><item><title>关于我</title><link>https://www.intotw.cn/posts/about-me/</link><guid isPermaLink="true">https://www.intotw.cn/posts/about-me/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写写代码&lt;/p&gt;
&lt;p&gt;思考思考人生&lt;/p&gt;
&lt;p&gt;记录记录感想&lt;/p&gt;
&lt;p&gt;写作的三个目的：叙述、记录、表达&lt;/p&gt;
&lt;h1&gt;本站发展历程&lt;/h1&gt;
&lt;h2&gt;2024.6&lt;/h2&gt;
&lt;p&gt;从hugo +loveit主题+ cloudflare 迁移到wordpress+argon主题，下定决心迁移的理由是hugo实在没有什么漂亮主题，在看&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blognas.hwb0307.com/&quot;&gt;Bensz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;和&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://xiau.net/&quot;&gt;夏&apos;博客&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;两位大佬的博客的时候，发现很喜欢这个主题，但是又是wordpress的，纠结和调研一番后，在闲置已久的云主机上部署一套wordpress吧。&lt;/p&gt;
&lt;p&gt;主要还有个原因，最近这份工作在主导和设计老系统php迁java，所以对php有了比较深入的熟悉，现在对wordpress这种php写的可以比较轻松的改和看懂，所以就直接干喽&lt;/p&gt;
&lt;p&gt;迁移的时候遇到一个问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;hugo的markdown，wordpress并不支持&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;找了一些工具比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/zhaoolee/WordPressXMLRPCTools&quot;&gt;WordPressXMLRPCTools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nefu-ljw/python-markdown-to-wordpress&quot;&gt;python-markdown-to-wordpress&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但是都会报错，最后排查许久，开启wp的debug日志才发现，需要安装php-xml才可以，实在是坑爹。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[22-Jun-2024 05:38:58 UTC] PHP Notice:  PHP 的 xml 扩展不可用。请联系您的主机提供商来启用 PHP 的 xml 扩展。 in /var/www/html/wordpress/wp-includes/IXR/class-IXR-message.php on line 48&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;成功上传后，这两个工具还是有问题，第一个工具上传后，必须要在wp中手动点进文章在点下保存，才可以正常展示格式。写了个python来使用selenium自动帮我操作以后，实在觉得有点弱智，以后每次上传都要这样，遂放弃。&lt;/p&gt;
&lt;p&gt;第二个工具尝试后，可以直接展示正确的markdown格式，但是对持续集成的支持并不好，所以自己直接从头写了个：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/IntoTw/wordpress-markdown-uploader&quot;&gt;wordpress-markdown-uploader&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;2023.8&lt;/h2&gt;
&lt;p&gt;从cnblog迁移到了hugo +loveit主题+ cloudflare，原因是在cnblog虽然整了整样式，但是实在是没有什么写文章的动力，每次都要写完再cv到网站上，实在难受，虽然cnblog能提供更高的曝光和搜索，但是不愿意写，博客就没有意义了&lt;/p&gt;
&lt;h2&gt;2020.10&lt;/h2&gt;
&lt;p&gt;从白嫖的hexo+githubpage上迁移到了cnblog，原因就是cnblog曝光会高一点，而且反正也是白嫖……&lt;/p&gt;
&lt;h2&gt;2018.10&lt;/h2&gt;
&lt;p&gt;从自己的搬瓦工主机，迁移到了githubpage，主要原因是因为，VPS太容易被墙了。。。。。&lt;/p&gt;
&lt;h2&gt;2018.1&lt;/h2&gt;
&lt;p&gt;本博客开始写并且第一次运行&lt;/p&gt;
</content:encoded></item><item><title>KeepAlive安装以及简单配置</title><link>https://www.intotw.cn/posts/distribution-keepalive/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-keepalive/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;操作系统：Centos7.3&lt;/p&gt;
&lt;h2&gt;一.依赖安装&lt;/h2&gt;
&lt;p&gt;首先安装相关依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y gcc openssl-devel popt-devel
yum -y install libnl libnl-devel
yum install -y libnfnetlink-devel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本依赖就安装完毕了，如果还缺少什么依赖在下一步编译的时候会有warning，百度去解决就好了&lt;/p&gt;
&lt;h2&gt;二.编译安装源码&lt;/h2&gt;
&lt;p&gt;首先下载源码到指定目录:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /usr/local/src
wget http://www.keepalived.org/software/keepalived-1.3.4.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后解压，配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar zxvf keepalived-1.3.4.tar.gz 
cd keepalived-1.3.4
./configure --prefix=/usr/local/keepalived
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后编译&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make  
make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这一步make之后可能会有warning，一般都是缺少依赖造成的，把warning关键字百度一下去yum安装对应依赖就可以了&lt;/p&gt;
&lt;h2&gt;三.修改配置文件地址&lt;/h2&gt;
&lt;p&gt;安装完成后，keepalived的默认配置文件地址和我们安装的地址不一样，所以cp过去就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cp ../keepalived-1.3.4/keepalived/etc/init.d/keepalived /etc/init.d/
mkdir /etc/keepalived
cp /usr/local/keepalived/etc/keepalived/keepalived.conf /etc/keepalived/
cp keepalived-1.3.4/keepalived/etc/sysconfig/keepalived /etc/sysconfig/
cp /usr/local/keepalived/sbin/keepalived /usr/sbin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后只要修改/etc/keepalived/ 目录下的keepalived.conf配置文件就可以了&lt;/p&gt;
&lt;p&gt;使用service start keepalived启动服务&lt;/p&gt;
&lt;h3&gt;四.附keepalived简单配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;! Configuration File for keepalived

global_defs {
   router_id lb01 #设置本机路由id，做区分的
}

vrrp_instance VI_1 {
    state MASTER #主从标记，仅做标识
    interface eth0 #虚拟路由的网卡名
    virtual_router_id 51 #虚拟路由路由id，想要配置在同一个虚拟ip必须要有相同id
    priority 150 #优先级，优先级最高的自动为主机，主机宕机后按照优先级选择热备从机
    advert_int 1 #主备通讯时间间隔
    authentication {
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
         172.17.0.199 #配置到哪个虚拟ip，这里我是在docker中，所以是这个docker的默认网段的一个ip，主备机这个地方ip要相同
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>分布式系统：分布式锁</title><link>https://www.intotw.cn/posts/distribution-lock/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-lock/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是分布式锁&lt;/h2&gt;
&lt;p&gt;锁的含义，一般就是为了独占资源，防止并发冲突，一般锁的实现，都依赖于计算机资源，如CPU，内存等，但是在跨系统时，各系统独立，如果需要锁，就需要一种分布式锁的实现方案。让各系统之间对相同资源的并发操作不会出现并发问题。&lt;/p&gt;
&lt;h2&gt;分布式锁的应用场景&lt;/h2&gt;
&lt;p&gt;虽然理论上，分布式锁适用于任何多应用需要独占资源或者要进行串行操作的场景，但是一般业务中，从我经验来看，主要是在下个场景使用的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;业务幂等以及防重，以创建订单举例，同时2个创建订单请求打到2台机器上，如果没有资源独占，就会进行2次创建，结果就是重复扣费，此时要么使用数据库的串行化事务来保证不出错，但吞吐低到令人不可接受的程度，要么就使用分布式锁，针对&lt;strong&gt;业务:业务主键id&lt;/strong&gt;这种形式，去获取锁，在具体某个业务某个订单号的维度去进行业务幂等。&lt;/li&gt;
&lt;li&gt;定时任务调度独占，在定时任务场景，很多定时任务触发时，是需要对一定范围的数据或一定资源进行独占的，此时会使用分布式锁，进行定时任务执行的独占或者资源的锁定。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分布式锁的实现&lt;/h2&gt;
&lt;p&gt;分布式锁的主要实现，目前流行3种方案：数据库，Redis，Zookeeper。数据库和Zookeeper因为自身原因，吞吐低，可用性较低，并且性能也不如缓存，所以目前主要都是使用Redis作为分布式锁。&lt;/p&gt;
&lt;h3&gt;Redis分布式锁最基础的实现&lt;/h3&gt;
&lt;p&gt;Redis分布式锁的实现主要依赖setnx()以及expire()操作，setnx操作类似于Java中ConcurrentHashMap的putIfAbsent操作，即如果是空，则更新，并返回更新成功与否。expire()操作即过期操作，给用来加锁的那个Key值设置一个自动过期时间，防止因为程序原因，手动释放锁时失败了会导致死锁的问题。但是这个东西不建议自己实现，因为要考虑的情况非常多，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果执行时间过长，此时已经过了自动过期时间，相当于锁释放掉了，那么此时还是会出现2个线程或2个应用同时认为自己持有了该锁，导致并发问题。&lt;/li&gt;
&lt;li&gt;如果获取锁成功，但是过期时间设置失败，此时假如再在手动释放锁时出现意外，那么又会出现死锁的情况。&lt;/li&gt;
&lt;li&gt;手动释放锁时，需要考虑各种情况，如该锁是否当前线程持有？该锁是否还存在？&lt;/li&gt;
&lt;li&gt;redis锁是否要有可重入性？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;通过Redisson来进行Redis锁的实现&lt;/h3&gt;
&lt;p&gt;Redisson应该是最流行的Java-RedisClient框架了，其中封装了很多Redis命令，包括使用Lua脚本对Redis锁有较好的实现。基本使用方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    RLock lock=null;
    try {
        //1
        lock = redissonClient.getLock(id + &quot;_&quot; + suffix);
        //2
        if(lock.tryLock(1,1, TimeUnit.MILLISECONDS)) {
            //do something
        }
    }catch (Exception e){
        log.error(&quot;&quot;,e);
    }finally {
        //3
        if(lock!=null &amp;amp;&amp;amp; lock.isLocked() &amp;amp;&amp;amp; lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;getLock获取的就是redis键值，一般为了做防重，都是使用业务+主键id，这里的getLock只是声明，并未去获取锁。&lt;/li&gt;
&lt;li&gt;tryLock即尝试去获取锁，支持最多3个参数，等待时间，自动释放时间，时间单位。&lt;/li&gt;
&lt;li&gt;这里也是上面提到的，也算是Redisson的一个小坑吧，&lt;strong&gt;Redisson的unlock方法在解锁失败时会抛出异常&lt;/strong&gt;，如果你有事务，事务马上就回滚了，所以这里一定要加一堆条件来保证unlock尽量不会出异常，最理想的还是封装一个方法，里面是tryCatch块，专门用来解锁，防止异常导致了外部业务回滚。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Redisson来进行Redis分布式锁的坑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;上一标题中第二点的自动释放时间有一个坑，就是自动释放时间，这个时间在实际业务中很难评估，有时业务就是会执行超时导致锁没有锁住，所以可以使用Redis的Watch Dog机制，即该参数使用-1或不填，则redis会产生一个30s的锁，并且每10s会自动对该锁进行续约，直到手动释放该锁或服务宕机导致续约的守护线程无法正常续约&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//定时任务自动续约，每个leaseTime的3分之一时间执行一次，这个任务通过future的回调机制，在成功获取redis锁之后开始执行。
private void scheduleExpirationRenewal(final long threadId) {
        if (!expirationRenewalMap.containsKey(this.getEntryName())) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RFuture&amp;lt;Boolean&amp;gt; future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, &quot;if (redis.call(&apos;hexists&apos;, KEYS[1], ARGV[2]) == 1) then redis.call(&apos;pexpire&apos;, KEYS[1], ARGV[1]); return 1; end; return 0;&quot;, Collections.singletonList(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
                    future.addListener(new FutureListener&amp;lt;Boolean&amp;gt;() {
                        public void operationComplete(Future&amp;lt;Boolean&amp;gt; future) throws Exception {
                            RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                            if (!future.isSuccess()) {
                                RedissonLock.log.error(&quot;Can&apos;t update lock &quot; + RedissonLock.this.getName() + &quot; expiration&quot;, future.cause());
                            } else {
                                if ((Boolean)future.getNow()) {
                                    RedissonLock.this.scheduleExpirationRenewal(threadId);
                                }

                            }
                        }
                    });
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
                task.cancel();
            }

        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么就带来了第二个问题，之前生产排查发现一个事务不该回滚的回滚了，回滚的原因就是redis锁被其他线程获取到了，在某线程在调用unlock解锁时，报了异常发现该锁不是被该线程持有的，那么为什么有了WatchDog机制，还会这样呢？&lt;/p&gt;
&lt;p&gt;其实想想也很简单，无非就是watchDog机制失效了呗，WatchDog机制也是需要使用到cpu资源的，在系统负载较高的情况下，较低的leaseTime就会很容易出现续约逻辑来不及执行的情况，并且一旦续约逻辑失效，该锁就会自动被释放掉，其他的线程就可以获取到了。&lt;/p&gt;
&lt;p&gt;不过目前看来这种情况发生的概率非常低，一年内也就出现了几次，所以后续我们只是针对这个业务手动设置了一个较长的leaseTime，并且在unlock前进行检查，防止异常抛到业务层。&lt;/p&gt;
&lt;h2&gt;分布式锁的思考&lt;/h2&gt;
&lt;p&gt;Redis分布式锁不是万能的，甚至来说他的一致性并不高，不过一般业务出于吞吐和性能，以及HA的考虑，才使用分布式锁，如果要一致性要求非常高，建议使用数据库锁和zookeeper锁，这两个都是CP的。&lt;/p&gt;
&lt;p&gt;如果其中有事务，一定要在事务执行完成后，再释放Redis锁，即锁囊括的范围要适当偏大，但是锁的粒度最好细到具体某个用户的某个业务操作。&lt;/p&gt;
&lt;p&gt;Redis锁可以根据具体的业务场景你对防重以及性能的容忍度，来评估等待时间和超时时间等。
如果实在因为各种原因没有锁住的情况偶尔发生，可以在业务上对重复的业务进行自动撤销，如我们就做了对重复支付订单的自动退款功能。&lt;/p&gt;
</content:encoded></item><item><title>分布式系统：xxl-job改造spring-cloud</title><link>https://www.intotw.cn/posts/distribution-xxltospringcloud/</link><guid isPermaLink="true">https://www.intotw.cn/posts/distribution-xxltospringcloud/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;修改后的源码仓库地址:&lt;a href=&quot;https://github.com/IntoTw/xxl-job-cloud&quot;&gt;GitHub&lt;/a&gt;. ：&lt;/p&gt;
&lt;h2&gt;改造原因&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;原有的xxl-job使用自己实现的http协议进行注册以及调度等，与目前框架中本身的注册中心格格不入，会影响健康检查、日志处理、问题排查。&lt;/li&gt;
&lt;li&gt;技术栈统一。避免执行器内包含两套注册逻辑。&lt;/li&gt;
&lt;li&gt;提高分布式健壮性，原有的服务注册以及发现等功能较弱，且与实际应用可用与否完全无关，经常存在xxl-job线程出问题，但主服务正常，或主服务出问题，但xxl-job线程正常。&lt;/li&gt;
&lt;li&gt;灰度扩展，目前系统灰度使用eureka定制实现，为执行器支持灰度，必须进行改造。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;主要改造思路&lt;/h2&gt;
&lt;h3&gt;调度中心&lt;/h3&gt;
&lt;p&gt;调度中心侧获取服务时，将原有的基于数据库的地址list，修改为动态从eureka中心获取服务的地址列表，两者通过xxl-job-admin配置的执行器app-name，与执行器的spring.application.name（即注册到eureka的服务标识）关联。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//com.xxl.job.admin.core.trigger.XxlJobTrigger
XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup());
//@edit 如果是自动获取地址的话，则使用
if (group.getAddressType() == 0) {
    group.setAddressList(SpringAdminContext.getEurekaAddressList(group.getAppname()));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//这种静态和spring容器不分的写法，还是有点别扭的
@Component(&quot;springAdminContext&quot;)
public class SpringAdminContext {
    @Autowired
    DiscoveryClient discoveryClient;

    private static SpringAdminContext springAdminContext;
    @PostConstruct
    public void initialize() {
        springAdminContext = this;
        springAdminContext.discoveryClient=this.discoveryClient;
    }
    public static String getEurekaAddressList(String appName){
        //may be springContext not init
        if(springAdminContext !=null){
            DiscoveryClient discoveryClient = SpringAdminContext.springAdminContext.discoveryClient;
            List&amp;lt;ServiceInstance&amp;gt; instances = discoveryClient.getInstances(appName);
            StringBuilder addressBuilder = new StringBuilder();
            for (int i = 0; i &amp;lt; instances.size(); i++) {
                addressBuilder.append(instances.get(i).getUri().toString());
                if(i!=instances.size()) {
                    addressBuilder.append(&quot;,&quot;);
                }
            }
            return addressBuilder.toString();
        }else {
            return &quot;&quot;;
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调度中心侧调度服务时，增加灰度策略，在获取到eureka的instanceList后，从instance的meta原数据中取出灰度标识，进行灰度调度。代码结合1中的列表获取，具体灰度实现与百度到的eureka灰度相同，略。&lt;/p&gt;
&lt;h3&gt;调度中心 执行器侧&lt;/h3&gt;
&lt;p&gt;通过修改core包，将原有的注册线程删除，并删除embedServer的实现，修改为springMVC。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//修改后的代替embedServer的处理类
@Controller
public class XxlJobHandlerController {
    private static final Logger logger = LoggerFactory.getLogger(XxlJobHandlerController.class);

    private ExecutorBiz executorBiz;
    @Value(&quot;${xxl.job.accessToken:}&quot;)
    private String accessToken;
    @Autowired
    ThreadPoolExecutor bizThreadPool;
    @PostConstruct
    public void start() {
        executorBiz = new ExecutorBizImpl();
    }

    @PostMapping(&quot;/job/{method}&quot;)
    @ResponseBody
    public ReturnT jobHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, @PathVariable(&quot;method&quot;) String methodName) {
        return doHandlerReq(httpServletRequest,httpServletResponse,&quot;/&quot;+methodName);
    }
    // ---------------------- registry ----------------------

    protected ReturnT doHandlerReq(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,String method) {
        try {
            //read request
            //@edit 这里请求都模拟的原有处理方式，包括header这些
            int contentLength = httpServletRequest.getContentLength();
            byte[] reqBody=new byte[contentLength];
            httpServletRequest.getInputStream().read(reqBody,0,contentLength);
            String requestData=new String(reqBody, StandardCharsets.UTF_8);
            String uri = method;
            String accessTokenReq = httpServletRequest.getHeader(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
            //@edit 这里原有是netty纯异步，但是到这边http就不太合适了，这么写虽然看起来吞吐会下降，但是一般web容器现在底层也支持nio了，应该关系不大。
            FutureTask&amp;lt;ReturnT&amp;gt; stringFutureTask=new FutureTask&amp;lt;ReturnT&amp;gt;(() -&amp;gt; process(uri, requestData, accessTokenReq));
            // invoke
            bizThreadPool.execute(stringFutureTask);
            ReturnT returnT = stringFutureTask.get();
            httpServletResponse.setHeader(HttpHeaders.CONTENT_TYPE, &quot;text/html;charset=UTF-8&quot;);


            return returnT;
        } catch (Exception e) {
            return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &quot;invalid request, uri-mapping empty.&quot;);
        }
    }

    private ReturnT process(String uri, String requestData, String accessTokenReq) {

        if (uri == null || uri.trim().length() == 0) {
            return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &quot;invalid request, uri-mapping empty.&quot;);
        }
        if (accessToken != null
                &amp;amp;&amp;amp; accessToken.trim().length() &amp;gt; 0
                &amp;amp;&amp;amp; !accessToken.equals(accessTokenReq)) {
            return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &quot;The access token is wrong.&quot;);
        }

        // services mapping
        try {
            if (&quot;/beat&quot;.equals(uri)) {
                return executorBiz.beat();
            } else if (&quot;/idleBeat&quot;.equals(uri)) {
                IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
                return executorBiz.idleBeat(idleBeatParam);
            } else if (&quot;/run&quot;.equals(uri)) {
                TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
                return executorBiz.run(triggerParam);
            } else if (&quot;/kill&quot;.equals(uri)) {
                KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
                return executorBiz.kill(killParam);
            } else if (&quot;/log&quot;.equals(uri)) {
                LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
                return executorBiz.log(logParam);
            } else {
                return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &quot;invalid request, uri-mapping(&quot; + uri + &quot;) not found.&quot;);
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return new ReturnT&amp;lt;String&amp;gt;(ReturnT.FAIL_CODE, &quot;request error:&quot; + ThrowableUtil.toString(e));
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改处理callback的逻辑，使得任务执行结果可以回调到admin&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//com.xxl.job.core.executor
//@edit 这个方法主要用于执行器在获取调度中心列表时调用，目的是执行器获取调度中心列表进行callback回调通知执行结果。
    //如果callback失败或者这里出问题，那么会导致在管理台看到的执行结果永远没有。
    //管理台的调度结果和执行结果是分开的，调度结果依赖单次http请求，执行结果依赖callback
    // @see TriggerCallbackThread
    public static List&amp;lt;AdminBiz&amp;gt; getAdminBizList(){
        List&amp;lt;AdminBiz&amp;gt; adminBizs=new ArrayList&amp;lt;&amp;gt;();
        String adminAppName=&quot;xxl-job-admin-cloud&quot;;
        List&amp;lt;String&amp;gt; addressList = Arrays.asList(SpringContext.getEurekaAddressList(adminAppName).split(&quot;,&quot;));
        addressList.forEach(e -&amp;gt;{
            adminBizs.add(new AdminBizClient(e.concat(&quot;/&quot;).concat(&quot;xxl-job-admin&quot;),accessToken));
        });
        return adminBizs;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;其实还有一些细节的修改包括eureka的配置，原有注册代码等的调整，就不好一一列出来了，具体可以拉下来项目搜索@edit，主要修改的地方我都加了这个。&lt;/p&gt;
</content:encoded></item><item><title>分布式系统：分布式任务调度xxl-job较深入使用</title><link>https://www.intotw.cn/posts/fbs-xxl/</link><guid isPermaLink="true">https://www.intotw.cn/posts/fbs-xxl/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;       xxl-job是一个分布式定时任务调度框架，功能强大，底层使用自己实现的rpc框架进行注册和管理，数据库使用mysql，调度触发使用数据库锁来作为调度锁。&lt;/p&gt;
&lt;p&gt;       xxl-job主要分为调度中心admin以及任务，任务引入依赖jar包并配置启动类为spring所管理的bean后，将自动通过spring-bean提供的initMethod进行启动线程选择一个端口进行注册以及监听任务调度。&lt;/p&gt;
&lt;p&gt;       公司目前引入xxl-job框架代替quartz框架作为分布式任务调度组件，并在其之上进行一定开发以及优化，所以这篇文章主要分享一些深入使用，主要是概念的详细介绍。&lt;/p&gt;
&lt;h2&gt;系统关键概念介绍&lt;/h2&gt;
&lt;h3&gt;执行器&lt;/h3&gt;
&lt;p&gt;       配置中心配置的执行器，概念上对应执行定时任务的服务，支持分布式调度以及调度的各种路由规则配置。注册方式支持自动注册和手动配置机器地址两种方式，心跳时间间隔默认为30s，失效时间90s。&lt;/p&gt;
&lt;p&gt;       执行器自动注册后，调度中心页面依旧有最长30秒的延迟显示，原因是数据库中注册表更新后，展示执行器的表是由另一个守护线程去更新的，更新频率为默认心跳时间30s，所以管理台展示会有延迟，但不影响任务调度。&lt;/p&gt;
&lt;h3&gt;任务&lt;/h3&gt;
&lt;p&gt;       任务以执行器为维度配置，每个任务必须属于一个执行器，当任务触发时会根据该任务所属的执行器去寻找执行器的地址列表，然后通过配置的路由规则以及阻塞规则去去执行。&lt;/p&gt;
&lt;p&gt;       任务支持本地任务以及远程任务，本地任务即按照执行方写好的业务逻辑执行。远程任务通过GLUE，在调度中心管理台写好代码，分发到执行方去执行。建议无特殊需求的话，统一使用本地任务。&lt;/p&gt;
&lt;h4&gt;任务配置项描述&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/ebb4325ec7a8e0a9bd3fc3ed885e8424.jpg&quot; alt=&quot;xxl-job的图片&quot; title=&quot;test&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 执行器&lt;/strong&gt;：选择该任务由哪个执行器去执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 任务描述&lt;/strong&gt;：简单描述该任务的功能以及作用，如：订单定时跑批&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 路由策略&lt;/strong&gt;：设置任务执行时，如何去选择执行器，高频任务建议使用一致性哈希或者第一台执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Cron&lt;/strong&gt;：Cron表达式，描述任务运行的时间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 运行模式&lt;/strong&gt;：BEAN即为接入服务配置在本地对应的handler运行，其他方式均为管理台设置代码交由接入服务远程执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. JobHandler&lt;/strong&gt;：运行模式为BEAN时必填，值应当为接入服务本地执行任务的handler&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. 阻塞策略&lt;/strong&gt;：当同一任务多次调度到同一台执行器时，执行器应当使用的策略&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. 子任务ID&lt;/strong&gt;：如配置，则该任务完成后自动触发一次子任务的执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9. 任务超时时间&lt;/strong&gt;：配置后当任务超时时将自动终止任务执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10. 失败重试次数&lt;/strong&gt;：任务失败后重试的次数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;11. 负责人&lt;/strong&gt;：一般为该任务接入方的负责人&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;12. 报警邮件&lt;/strong&gt;：任务报警后发送的邮件地址&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;13. 任务参数&lt;/strong&gt;：若配置了任务参数，任务调度时将发送任务参数至执行方handler。&lt;/p&gt;
&lt;h3&gt;阻塞策略&lt;/h3&gt;
&lt;p&gt;阻塞策略即同一个任务在执行器的阻塞执行策略。由执行器端控制。典型场景为：任务A分发到执行器A执行，此时任务A再次触发并分发到执行器A，此时根据阻塞策略选择的不同将会有以下三种执行策略：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 单机串行&lt;/strong&gt; 该策略下，&lt;strong&gt;同一执行器&lt;/strong&gt;收到&lt;strong&gt;同一任务&lt;/strong&gt;的调度触发时，若已有任务正在执行，会将后续的任务放入执行线程的队列中，等待线程轮询继续执行，可能会导致线程队列阻塞过多任务导致内存过高，&lt;strong&gt;高频且耗时较长任务慎用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 丢弃后续调度&lt;/strong&gt; 该策略下，&lt;strong&gt;同一执行器&lt;/strong&gt;收到&lt;strong&gt;同一任务&lt;/strong&gt;的调度触发时，若已有任务正在执行，会直接丢弃后续同一任务的调度，&lt;strong&gt;推荐使用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 覆盖之前调度&lt;/strong&gt; 该策略下，&lt;strong&gt;同一执行器&lt;/strong&gt;收到&lt;strong&gt;同一任务&lt;/strong&gt;的调度触发时，若已有任务正在执行，将会直接停止正在执行的任务（通过线程InterruptedException异常以及volatile变量判断），并将新任务放入队列。&lt;strong&gt;一般情况下不建议使用&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;路由策略&lt;/h3&gt;
&lt;p&gt;路由策略即任务在配置中心进行调度分发时，选择执行器的策略。由配置中心端控制。典型场景为：任务A触发执行，任务A对应的执行器有执行器A，B，C，D，此时根据路由策略的选择将会有以下几种分发情况&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 第一个&lt;/strong&gt;：始终选择第一台执行器作为任务执行器，不论该任务执行器是否正常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 最后一个&lt;/strong&gt;：始终选择最后一台作为任务执行器&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 轮询&lt;/strong&gt;：每个执行器轮流执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 随机&lt;/strong&gt;：随机选择一个执行器执行&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 一致性HASH&lt;/strong&gt;：根据任务ID做一致性哈希选择执行器，&lt;strong&gt;同一个任务必定只分发到同一个执行器。高频或耗时较长任务推荐使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. 最不经常使用&lt;/strong&gt;：选择平均使用频率最低的执行器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. 最近最久未使用&lt;/strong&gt;：选择最近的最久未使用的执行器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8. 故障转移&lt;/strong&gt;：分别进行心跳检测，选择第一台心跳检测正常的机器执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;9. 忙碌转移&lt;/strong&gt;：分别进行忙碌检测，选择第一台空闲的机器执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10. 分片广播&lt;/strong&gt;：广播到所有执行器执行，并提供分片参数，分片参数获取方式如下，应用在被触发时动态获取自己是第几个分片，共有几个分片。&lt;/p&gt;
&lt;h2&gt;日志问题&lt;/h2&gt;
&lt;p&gt;xxl-job相关日志使用默认使用slf4j作为日志框架，使用专门的API写入日志时，会输出2种日志，客户端日志与服务端日志&lt;/p&gt;
&lt;h3&gt;客户端日志&lt;/h3&gt;
&lt;p&gt;客户端日志根据配置文件中配置的logpath指定，根据源码分析，客户端日志将通过FileOutputStream写到对应文件，且无法通过配置修改，所以只好修改了源码中的逻辑，改为该值为空未配置时，直接通过slf4j写入。&lt;/p&gt;
&lt;h3&gt;服务端日志&lt;/h3&gt;
&lt;p&gt;使用xxljob的日志api输出日志时，日志也会在调度管理台看到，能看到的原理是xxl-job管理台会通过rpc调用执行器的接口，执行器收到请求后从指定的日志文件中读取执行的日志并返回，这里存在一个比较麻烦的问题，就是xxl-job这种日志的逻辑，无法很好的兼容到项目统一的日志模块里，十分不便。&lt;/p&gt;
&lt;p&gt;所以实际使用过程中，我们在xxl-job管理台查询日志时，对其进行了改造，修改为不从rpc查询，而是走我们日志管理的搜索引擎根据执行的jobid查询相关日志，结合客户端日志输出的改造，从而统一xxl-job和我们系统间的日志管理。&lt;/p&gt;
&lt;h2&gt;框架目前发现的缺点以及存在的问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;目前定时任务的调度串行是依赖db锁的，某个子微服务或者子系统内部使用还好，但是不适合整个公司级别共用，这点和数据库的耦合比较高，并没有本地缓存之类的，对数据库的HA依赖非常高。&lt;/li&gt;
&lt;li&gt;管理模块以及权限模块，组内和小公司用用可以，不适合作为多个系统多个部门之间共用的中间件。&lt;/li&gt;
&lt;li&gt;管理台存在一些简单的安全bug，sql注入和js脚本注入非常简单。（公司的安全测试测出来的）&lt;/li&gt;
&lt;li&gt;相关协议支持较差，使用自己实现的RPC协议,如果需要dubbo或者spring cloud，需要自己拓展。并且虽然底层是netty，但是本身对netty异常的封装并不是很好，导致一些奇怪的网络问题或者其他的协议，会报莫名其妙的错，没有一定netty理解的人是看不懂的，这点我提了Issue，不过出于这套框架解耦与独立的设计，估计是不会支持dubbo和spring cloud的。&lt;/li&gt;
&lt;li&gt;上面提到的，日志模块更建议自己整体修改下写入逻辑，管理台看不到无所谓，（毕竟定时任务也没谁会去管理台看日志，并且这部分日志写多了对性能和网络开销有影响的），写到本地使用elk之类的再查就可以了。&lt;/li&gt;
&lt;li&gt;定时任务触发的流水号或者跟踪id，需要改动原框架，否则也会影响后续日志追踪的问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体而言这是一个很不错的框架，关于定时任务的执行器和调度器关系也很优雅,值得拓展或者进行一定定制开发。目前使用来，稳定性也没有问题。&lt;/p&gt;
</content:encoded></item><item><title>Git</title><link>https://www.intotw.cn/posts/git/</link><guid isPermaLink="true">https://www.intotw.cn/posts/git/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;之前公司要做技术分享，因为Git虽然看似简单，但是实际上是使用较多而且较为重要的一个工具，所以做了一下大概的总结。&lt;/p&gt;
&lt;p&gt;因为新来的同事问了一个问题，紧急版本要拉hotfix分支修改，但是hotfix分支如何优雅的合并到各个分支上去呢？尤其是hotfix修改的位置比较敏感的情况下。&lt;/p&gt;
&lt;p&gt;所以顺带也研究了一下这个问题&lt;/p&gt;
&lt;p&gt;下面根据Git最关键的几个概念，穿插了Git几个命令实际做了什么&lt;/p&gt;
&lt;h2&gt;版本号&lt;/h2&gt;
&lt;p&gt;Git本身有版本号的概念，版本号基于每次&lt;strong&gt;commit&lt;/strong&gt;，查看最近的&lt;strong&gt;commit&lt;/strong&gt;可以使用&lt;strong&gt;git log&lt;/strong&gt;查看，查看所有历史版本可以使用&lt;strong&gt;git reflog&lt;/strong&gt;查看所有本地commit的版本号，使用这两个命令可以查看到commit过的记录和commit时填写的信息，但是命令行内查看好像暂时不支持中文的commit信息显示。&lt;/p&gt;
&lt;h2&gt;仓库&lt;/h2&gt;
&lt;p&gt;Git有2个仓库概念，一个是远端仓库，一个是本地仓库。在本地和远端之间同步的时候一定要谨慎&lt;/p&gt;
&lt;h2&gt;本地仓库&lt;/h2&gt;
&lt;p&gt;实际上，在不使用&lt;strong&gt;fetch，pull，push，merge&lt;/strong&gt;这几个命令的情况下，Git是仅使用本地仓库就可以完成版本管理的，主要的命令（或者说是操作）涉及到&lt;strong&gt;add，commit&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;add操作&lt;/strong&gt;用于将目录下的文件添加到Git的工作空间里去，同目录下未使用add命令添加过的文件，是不参与到git版本管理中去的，也就是说是独立于Git的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;commit操作&lt;/strong&gt;用于提交修改，仅限于本地，用于提交每次修改，commit会产生一个commit id，这个id标识这次commit，在本地的话，实际上版本管理就是不断基于commit做的版本管理，假设commit过3次内容为A,B,C。C为最后commit的版本，此时想回退，只需要git reset commitid --hard 命令，就能把本地仓库回退到B或A的那次commit。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;revert&lt;/strong&gt;用于在某次commit之后，假设commit id为A，此时做了一系列修改，但未commit，此时revert会将所有修改取消，回归到commit id为A时的状态。&lt;/p&gt;
&lt;h2&gt;远程仓库&lt;/h2&gt;
&lt;p&gt;实际上，远程仓库仅仅可以看做是一个代码的备份，是一个公共的备份，也就是说，在每次&lt;strong&gt;push&lt;/strong&gt;后，&lt;strong&gt;你的本地仓库和远程仓库所有文件应该是一模一样的&lt;/strong&gt;，通过push的话，会把本地的所有commit同步到远程仓库上去，也就是说，&lt;strong&gt;远程仓库的commit过程应该是每个人在本地commit的总和，但可能顺序不同。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对本地仓库来说，pull就等于同步远端仓库到本地仓库加merge，push就等于把本地仓库同步到远端仓库加merge。&lt;/p&gt;
&lt;p&gt;Merge的本质，是将两个分支同步，实际上都是在本地merge。说一下这个过程，假设现在远端仓库版本为A，此时小g和小w同时从A上pull到了本地仓库将他们自己的本地仓库同步到了远端的版本A，此时他们的本地仓库我们假设为A1，A2，他们基于A1,A2做了自己的修改，并且同时修改了test.txt，然后都想提交到远端，第一个提交的人肯定可以提交成功，但是第二个提交的人必定会出现push失败，需要merge该文件，此时他在idea中merge了该文件确认自己改的才是对的，然后再push，此时就成功了。&lt;/p&gt;
&lt;p&gt;解释一下，此时的merge实际过程是他处理了冲突之后，又进行了一次针对merge的commit，此次commit专门用来处理冲突并提交，具体体现在idea里你会发现，有的时候会让你选accept theirs,accept yours，你选了accept yours或者全部按照你本地来作为最终版本的文件，不需要commit，如果你选的是accept theirs或者merge的时候选择了远端的修改，那么你本地需要全部commit一次再提交，也就是说实际处理的merge是在本地确认了merge结果，这次commit和push实际idea是做了特殊处理的，带着merge的头，所以可以不会再次触发冲突，idea直接通过强制-force push到远端。&lt;/p&gt;
&lt;h2&gt;Bug修改，Hotfix&lt;/h2&gt;
&lt;p&gt;之前考虑到生产环境修改，紧急bug修复，会基于生产环境分支创建一个hotfix分支，做修改完成后由prod分支合并hotfix分支，然后删除hotfix分支，但是这带来了一个问题，该hotfix分支难以合并到例如dev，test等分支，因为：&lt;/p&gt;
&lt;p&gt;1.配置文件问题，从hotfix分支merge到dev分支，会导致dev等分支配置文件被覆盖，十分麻烦&lt;/p&gt;
&lt;p&gt;2.版本管理问题，如果dev此时分支远远超前prod分支，那么此时将不可能合并hotfix的修改到dev上&lt;/p&gt;
&lt;p&gt;可以通过git的stash功能解决这个问题。&lt;/p&gt;
&lt;p&gt;stash和他的操作unstash的本质，是创建一个修改副本。他的应用场景和操作模式在于，有的时候本地做了一系列修改，但是忽然又要全部回到修改前的时候进行一下打包，此时要么从新从远端拉一个分支到本地，要么就直接copy一份项目到其他目录，然后这个目录下的revert掉，改完覆盖回来。&lt;/p&gt;
&lt;p&gt;stash可以轻松解决，stash可以对当前所有的修改（具体提现到所有蓝色的文件）做一个备份，创建一个stash后，项目会自动回到当前commit id的状态（相当于revert），相当于对所有修改做了一个另外的保存，然后可以通过unstash可以把这些修改还原回来。&lt;/p&gt;
&lt;p&gt;这样的话通过这个功能可以很轻松的解决这个问题，本地check出来hotfix分支之后，修改完毕不要先commit，先进行stash，然后unstash回来，再进行commit，push。完成后回到dev分支进行unstash同样进行一下更改，就把更改应用在dev分支上了。&lt;/p&gt;
</content:encoded></item><item><title> 分布式系统：负载均衡算法</title><link>https://www.intotw.cn/posts/load-balance/</link><guid isPermaLink="true">https://www.intotw.cn/posts/load-balance/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;负载均衡算法&lt;/h2&gt;
&lt;p&gt;负载均衡算法，一般在分布式场景的中大量使用，负载均衡一般分为调用方负载均衡，和服务方负载均衡，spring cloud中的ribbon就是使用的调用方负载均衡，而通过nginx的配置来进行负载均衡，明显更像是服务端的负载均衡。但是原理是一致的，算法的目的就是在一个服务器集合中，选择其中一个合适的服务器，进行请求的处理。&lt;/p&gt;
&lt;p&gt;常见的负载均衡算法包括：随机，轮询，最小压力，哈希。主要为这几大类，其中各自有如带权重的实现，一致性哈希等更好的算法。
这篇文章起源于接手了公司的另一个组的一个cloud项目，是一个架构师搭的，所以仔细看了一遍，看到了基于ribbon的带权重轮询算法实现，一开始看了好久不知道在干什么，补充了相关知识后豁然开朗，于是对这2种算法做一个总结和记录，本文主要描述带权重的轮询算法以及平滑的带权重轮询算法的原理以及实现。&lt;/p&gt;
&lt;h2&gt;带权重的轮询负载均衡算法&lt;/h2&gt;
&lt;p&gt;轮询负载均衡即在所有服务之间，依次选择每个服务，若服务器有A、B、C、D、E，则调度顺序必定为A、B、C、D、E、A、B、C、D……的循环。&lt;strong&gt;带权重的轮询&lt;/strong&gt;即为在轮询的基础上，考虑每个服务的权重，如服务器A、B、C、D、E对应权重{5,1,1,1,1}，一般来说在某总权重(5+1+1+1+1)=9次调度内调度顺序就为AAAAABCDE。&lt;/p&gt;
&lt;p&gt;该算法可以很简单的实现，只需要每次选取后将权重-1，直到总权重为0，重置权重数组，即可完成处理。代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func doSimpleWeightBound(servers []Server,initWeight []int){
	res:=list.New()
	weight:=make([]int,len(initWeight))
	count:=make([]int, len(weight))
	totalWeight:=0
	for i := 0; i&amp;lt; len(initWeight);i++  {
		totalWeight+=initWeight[i]
	}
	copy(weight,initWeight)
    nowWeight:=totalWeight
    //一共做totalWeight*100次，查看结果。
	for i := 0; i &amp;lt; totalWeight*100; i++ {
		//1. 选出当前权重最高的机器
		position:=selectHighWeight(weight)
		count[position]++
		res.PushBack(servers[position].name)
		//2. 选出的机器权重数减1
		weight[position]--
		//3. 总权重减1
		nowWeight--
		if(nowWeight&amp;lt;=0){
			copy(weight,initWeight)
			nowWeight=totalWeight
		}
	}
	for i :=0; i &amp;lt; len(weight); i ++ {
		fmt.Printf(&quot;%d &quot;,weight[i])
	}
	fmt.Println()
	for i := res.Front(); i != nil; i = i.Next() {
		fmt.Printf(&quot;%s &quot;,i.Value)
	}
	fmt.Println()
	for i :=0; i &amp;lt; len(count); i ++ {
		fmt.Printf(&quot;%s: %d\n&quot;,string(&apos;A&apos;+i),count[i])
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码执行结果如下：
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/1568b93852ff6094689091da31c0054c.jpg&quot; alt=&quot;&quot; /&gt;
可以看到，执行结果在带权综合次数内，分布为AAAAABCDE。&lt;/p&gt;
&lt;h2&gt;平滑带权重的轮询负载均衡算法&lt;/h2&gt;
&lt;p&gt;带权重的轮询负载均衡算法存在一个问题，即在权重相差很大时，连续的调用在A机器上过于频繁，即使带权重的目的本身就是让权重大的机器处理更多请求，但显然让连续的调用更加平均分散在各台机器上更好。
该算法的实现参照nginx开发者提供的思路，进行如下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;建立两个数组，initWeight记录初始各机器的权重，weight记录过程中的权重。&lt;/li&gt;
&lt;li&gt;求得初始总权重totalWeight。&lt;/li&gt;
&lt;li&gt;从各机器中选出当前权重weight最高的一台机器S1。&lt;/li&gt;
&lt;li&gt;S1对应的当前权重减去总权重totalWeight。&lt;/li&gt;
&lt;li&gt;每台机器的当前权重weight[i]依次增加各机器初始的权重initWeight[i]&lt;/li&gt;
&lt;li&gt;重复3-5步骤。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func doSmoothlyWeightBound(servers []Server,initWeight []int){
	//一共做30次，分别看选出的是哪台机器。
	res:=list.New()
	weight:=make([]int,len(initWeight))
	count:=make([]int, len(weight))
	totalWeight:=0
	for i := 0; i&amp;lt; len(initWeight);i++  {
		totalWeight+=initWeight[i]
	}
	copy(weight,initWeight)
	for i := 0; i &amp;lt; totalWeight*8; i++ {
		//1. 选出当前权重最高的机器
		position:=selectHighWeight(weight)
		count[position]++
		res.PushBack(servers[position].name)
		//2. 选出的机器减去总权重
		weight[position]-=totalWeight
		//3. 对每台机器，增加初始权重
		for j := 0; j &amp;lt; len(weight); j++ {
			weight[j]+=initWeight[j]
		}
	}
	for i :=0; i &amp;lt; len(weight); i ++ {
		fmt.Printf(&quot;%d &quot;,weight[i])
	}
	fmt.Println()
	for i := res.Front(); i != nil; i = i.Next() {
		fmt.Printf(&quot;%s &quot;,i.Value)
	}
	fmt.Println()
	for i :=0; i &amp;lt; len(count); i ++ {
		fmt.Printf(&quot;%s: %d\n&quot;,string(&apos;A&apos;+i),count[i])
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码执行结果如下：
&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/66c791a7707ae8c046abfbe9eea0df2a.jpeg&quot; alt=&quot;&quot; /&gt;
可以看到，执行结果在带权综合次数内，总次数满足权重比例，且顺序不再是纯粹的连续顺序。&lt;/p&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;p&gt;该算法的缺点也很明显，每次总权重周期内的顺序必定是相同的，实际上在服务与权重不变的情况下，只要生成一次顺序，以后就按照顺序去轮询即可，不需要维护计算。&lt;/p&gt;
&lt;h2&gt;思考&lt;/h2&gt;
&lt;h3&gt;如何在服务列表变化的情况下，执行算法？&lt;/h3&gt;
&lt;p&gt;以上算法的实现非常明显的只考虑到了在服务列表以及权重不变的情况下，进行多次选择。但是如果感知到服务列表或者权重比例变化的情况下，该如何处理呢？
通过过程可以发现，算法主要依赖的就是初始权重以及服务列表，在感知到服务列表变化时，应通过一些同步机制，及时的更新正在进行选择时的服务列表以及权重比例，及时重新计算相关数组以及总权重数。具体可容忍什么程度的一致性，就用什么同步方式去更新。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过看高手的代码以及实现，深入了解了一部分知识点。&lt;/p&gt;
</content:encoded></item><item><title>Netty学习：ChannelHandler执行顺序详解，附源码分析</title><link>https://www.intotw.cn/posts/netty-channelhandler/</link><guid isPermaLink="true">https://www.intotw.cn/posts/netty-channelhandler/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;近日学习Netty，在看书和实践的时候对于书上只言片语的那些话不是十分懂，导致尝试写例子的时候遭遇各种不顺，比如decoder和encoder还有HttpObjectAggregator的添加顺序，研究了一番之后和大家分享一下自己的理解，希望后来人可以少走弯路。&lt;/p&gt;
&lt;h2&gt;模型浅析&lt;/h2&gt;
&lt;p&gt;简单描述下ChannelHandler的存储模型，ChannelHandler在ChannelPipeline中主要以AbstractChannelHandlerContext为基类存储，存储的数据结构为链表，传进去的ChannelHandler都会转化为DefaultChannelHandlerContext来存储在ChannelPipeline里，ChannelPipeline主要的实现为DefaultChannelPipeline。&lt;/p&gt;
&lt;p&gt;DefaultChannelPipeline
DefaultChannelPipeline使用双向链表储存所有AbstractChannelHandlerContext，定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultChannelPipeline implements ChannelPipeline {
    static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);
    private static final String HEAD_NAME = generateName0(DefaultChannelPipeline.HeadContext.class);
    private static final String TAIL_NAME = generateName0(DefaultChannelPipeline.TailContext.class);
    private static final FastThreadLocal&amp;lt;Map&amp;lt;Class&amp;lt;?&amp;gt;, String&amp;gt;&amp;gt; nameCaches = new FastThreadLocal&amp;lt;Map&amp;lt;Class&amp;lt;?&amp;gt;, String&amp;gt;&amp;gt;() {
        protected Map&amp;lt;Class&amp;lt;?&amp;gt;, String&amp;gt; initialValue() throws Exception {
            return new WeakHashMap();
        }
    };
    final AbstractChannelHandlerContext head;//双向链表，头指针
    final AbstractChannelHandlerContext tail;//双向链表，尾指针
    private final Channel channel;
    private final ChannelFuture succeededFuture;
    private final VoidChannelPromise voidPromise;
    private final boolean touch = ResourceLeakDetector.isEnabled();
    private Map&amp;lt;EventExecutorGroup, EventExecutor&amp;gt; childExecutors;
    private Handle estimatorHandle;
    private boolean firstRegistration = true;
    private DefaultChannelPipeline.PendingHandlerCallback pendingHandlerCallbackHead;
    private boolean registered;
    ……
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在调用addLast(First,Before）等方法添加ChannelHandler到ChannelPipeline时，实际上是new了一个DefaultChannelHandlerContext对象插入到链表中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized(this) {
            checkMultiplicity(handler);
            newCtx = this.newContext(group, this.filterName(name, handler), handler);//第一个参数为eventgroup，第二个参数为通过方法获取的channelhandler名称，第三个为channelhandler
            this.addLast0(newCtx);//构造完成后，last使用前插法插入链表尾部，first使用后插法插入链表头部
            if(!this.registered) {
                newCtx.setAddPending();
                this.callHandlerCallbackLater(newCtx, true);
                return this;
            }
 
            EventExecutor executor = newCtx.executor();
            if(!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    public void run() {
                        DefaultChannelPipeline.this.callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
 
        this.callHandlerAdded0(newCtx);
        return this;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DefaultChannelHandlerContext&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
    private final ChannelHandler handler;
 
    DefaultChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
        super(pipeline, executor, name, isInbound(handler), isOutbound(handler));//调用父类构造方法创建context
        if(handler == null) {
            throw new NullPointerException(&quot;handler&quot;);
        } else {
            this.handler = handler;//保存handler引用
        }
    }
 
    public ChannelHandler handler() {
        return this.handler;
    }
 
    private static boolean isInbound(ChannelHandler handler) {
        return handler instanceof ChannelInboundHandler;
    }
 
    private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Handler传递顺序&lt;/h2&gt;
&lt;p&gt;现在我们知道Pipeline里实际是一个context的链表，现在我们来看看fireChannelRead和write的传递顺序&lt;/p&gt;
&lt;h3&gt;fireChannelRead&lt;/h3&gt;
&lt;p&gt;调用fireChannelRead方法时，调用该方法的context会从自己开始在链表中根据自己的next指针来寻找下一个注册（invoke）的handler去处理事件，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ChannelHandlerContext fireChannelRead(Object msg) {
        invokeChannelRead(this.findContextInbound(), msg);//将查找到的context传递进入执行channelRead方法，里面还有一些eventLoop的判断
        return this;
    }
private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
 
        do {
            ctx = ctx.next;
        } while(!ctx.inbound);//从当前context开始，查找到下一个为inbound的handler，所以说outbound和inbound的插入顺序与执行顺序或执行成功与否没有任何关系，只与最后链表的结果有关，并且当handler过多时会影响遍历速度
 
        return ctx;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;write&lt;/h3&gt;
&lt;p&gt;调用write方法时，调用该方法的context会从&lt;strong&gt;自己开始&lt;/strong&gt;在链表中根据自己的pre指针来寻找上一个注册（invoke）的handler去处理事件，顺序与fireChannelRead相反,代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void write(Object msg, boolean flush, ChannelPromise promise) {
        AbstractChannelHandlerContext next = this.findContextOutbound();//查找到上一个outBoundContext
        Object m = this.pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if(executor.inEventLoop()) {
            if(flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } else {
            Object task;
            if(flush) {
                task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
            } else {
                task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
            }
 
            safeExecute(executor, (Runnable)task, promise, m);
        }
 
    }
private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
 
        do {
            ctx = ctx.prev;
        } while(!ctx.outbound);
 
        return ctx;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;补充&lt;/h2&gt;
&lt;p&gt;如果write不调用context的write方法，而是调用context.channel().write()，则会直接调用使用pipeline的tail指针开始向前遍历outboundhandler执行，如果有特殊执行需求时可以考虑使用这种调用方法。相关源码就不贴了，有兴趣的小伙伴可以自己去看。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;到这里，Handler执行顺序已经介绍完毕了，总结为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;对于channelInboundHandler,总是会从传递事件的开始，向链表末尾方向遍历执行可用的inboundHandler。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于channelOutboundHandler，总是会从write事件执行的开始，向链表头部方向遍历执行可用的outboundHandler。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;举例说明如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch.pipeline().addLast(new OutboundHandler1());  
ch.pipeline().addLast(new OutboundHandler2());  
ch.pipeline().addLast(new InboundHandler1());  
ch.pipeline().addLast(new InboundHandler2());  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;链表中的顺序为head-&amp;gt;out1-&amp;gt;out2-&amp;gt;in1-&amp;gt;in2-&amp;gt;tail
那么Inbound的执行顺序为read-&amp;gt;in1-&amp;gt;in2
在Inbound执行write后，outbound执行顺序为out1&amp;lt;-out2&amp;lt;-write&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;所以实际使用中，如果添加的顺序不好，很可能会意外跳过某些inbount或者outbound。建议实际使用上，先通过addFirst插入所有outBound再通过addLast插入所有inBound这样inBound与outBound的插入顺序与执行顺序完全一致，且不会出现跳过的情况。
&lt;strong&gt;很多源码中的习惯都是只使用addLast或者addFirst插入，然后顺序在心中，具体方法见仁见智，保证顺序不错就行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;所以一些统一编码解码的handler，例如ssl，httpcodec，最好是按照顺序放在链表头！这样才会保证进出都会执行到并且业务逻辑可以正常插入&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Netty学习：伪共享</title><link>https://www.intotw.cn/posts/netty-false-sharing/</link><guid isPermaLink="true">https://www.intotw.cn/posts/netty-false-sharing/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Netty中的伪共享&lt;/h2&gt;
&lt;p&gt;先说为什么知道这个概念吧，期初看Netty源码的时候，看到了NioEventLoop的构建，其中有这么一句代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static Queue&amp;lt;Runnable&amp;gt; newTaskQueue0(int maxPendingTasks) {
        // This event loop never calls takeTask()
        return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.&amp;lt;Runnable&amp;gt;newMpscQueue()
                : PlatformDependent.&amp;lt;Runnable&amp;gt;newMpscQueue(maxPendingTasks);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实就是给这个EventLoop创建一个存放Event的队列嘛，看过前篇的都懂，但是问题来了，为什么是这个队列，于是点进去看，发现了更神奇的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MpscUnboundedArrayQueue&amp;lt;E&amp;gt; extends BaseMpscLinkedArrayQueue&amp;lt;E&amp;gt; {
    long p0;
    long p1;
    long p2;
    long p3;
    long p4;
    long p5;
    long p6;
    long p7;
    long p10;
    long p11;
    long p12;
    long p13;
    long p14;
    long p15;
    long p16;
    long p17;

    public MpscUnboundedArrayQueue(int chunkSize) {
        super(chunkSize);
    }

    protected long availableInQueue(long pIndex, long cIndex) {
        return 2147483647L;
    }
    ……
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是NettyEventLoop使用的队列的原貌，是一个第三方包里的，百度了下，是专门用来提高吞吐的队列，适合Netty这种多生产者单消费者（就自己在循环消费，当然是单消费者）的情况。但是问题来了：&lt;strong&gt;那么多long是做什么用的呢&lt;/strong&gt;？&lt;/p&gt;
&lt;h2&gt;伪共享的原理以及介绍&lt;/h2&gt;
&lt;p&gt;伪共享为什么会出现呢？我们都知道CPU访问内存，基本是通过寄存器-&amp;gt;L1-&amp;gt;L2-&amp;gt;L3-&amp;gt;主存这么个链路的，在多核CPU的高速缓存之间，因为考虑到对相同数据的使用，会有缓存一致性协议，即通过锁或者协议去同步缓存中数据的变更。举个例子，假如加载到A核中的L3缓存的某个数据进行了修改，如果此时B核中如果也保存了该数据，则需要A核与B核之间达到一个数据修改的同步。&lt;/p&gt;
&lt;p&gt;如果A核以及B核都大量都对该数据进行修改呢？那么竞争就会十分激烈了，在这个时候，缓存一致性协议导致的CPU时间损耗比使用高速缓存的损耗还要多。所以干脆不使用缓存了。即使用固定的数据将对象填满，此时加载到缓存中的就是那些填充的不变的long数据了。
所以在自己编写多线程使用或者高吞吐的队列或者数据结构时，一定要考虑伪共享的问题，否则性能会非常低。&lt;/p&gt;
</content:encoded></item><item><title>Spring Boot Scheduled定时任务特性</title><link>https://www.intotw.cn/posts/spring-scheduled/</link><guid isPermaLink="true">https://www.intotw.cn/posts/spring-scheduled/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;SpringBoot中的Scheduled定时任务是Spring Boot中非常常用的特性，用来执行一些比如日切或者日终对账这种定时任务&lt;/p&gt;
&lt;p&gt;下面说说使用时要注意的Scheduled的几个特性&lt;/p&gt;
&lt;h2&gt;Scheduled的执行方式&lt;/h2&gt;
&lt;p&gt;Scheduled按照顺序执行，对于某个task未做配置的话只会起一个线程去执行，也就是说当你某个任务在处理中阻塞了，哪怕轮询时间再次到达，Spring也不会再起线程执行该任务，而是会等待上次任务执行完毕，所以请不要在Scheduled的task中做一些比较需要频繁触发的易失败，易阻塞，易超时操作，避免任务无法正常轮询执行&lt;/p&gt;
&lt;h2&gt;Scheduled中的线程池&lt;/h2&gt;
&lt;p&gt;Scheduled执行可以通过Spring Boot提供的配置来配置定时任务执行的线程池等信息，如果未做配置的话，根据我测试，所有定时任务仅有一个线程去执行，也就是说如果某个task阻塞，其他task都将得不到执行。具体配置方法如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@EnableScheduling
public class SchedulerTaskConfiguration implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(taskExecutor());
    }

    @Bean
    public Executor taskExecutor() {
        //线程池大小
        return Executors.newScheduledThreadPool(10);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是哪怕是配置了线程池，也只是降低了多个task之间执行的影响，对于单个task来说，哪怕配置了线程池，依旧会因为上次执行的阻塞影响到下一次触发&lt;/p&gt;
&lt;h2&gt;Scheduled的执行频率&lt;/h2&gt;
&lt;p&gt;Scheduled的执行频率可以由2种方式控制，一种为在@Scheduled中添加fixedRate属性，即@Scheduled(fixedRate = 10)，数字为执行的间隔毫秒，也就是多少毫秒执行一次&lt;/p&gt;
&lt;p&gt;另一种为添加cron属性，属性值为cron表达式，可以通过cron表达式指定为具体某年某月某分某秒，也可以通过cron表达式指定为间隔几小时或几分钟执行一次,如**@Scheduled(cron = &quot;0 0 1 * * &lt;em&gt;&quot;)&lt;/em&gt;*&lt;/p&gt;
</content:encoded></item><item><title>使用jib-maven-plugin将Spring Boot项目发布为Docker镜像</title><link>https://www.intotw.cn/posts/springboot-docker/</link><guid isPermaLink="true">https://www.intotw.cn/posts/springboot-docker/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;将spring boot(cloud)项目发布到docker环境作为镜像，一般常用的一个是com.spotify的docker-maven-plugin这个maven插件，还有一个就是本文介绍的了，本文介绍的jib-maven-plugin是谷歌提供的，且配置较为简单（相对的镜像自定义能力较弱）。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;增加如下配置即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;build&amp;gt;
        &amp;lt;finalName&amp;gt;${artifactId}&amp;lt;/finalName&amp;gt;
        &amp;lt;pluginManagement&amp;gt;
            &amp;lt;plugins&amp;gt;
                &amp;lt;plugin&amp;gt;
                    &amp;lt;groupId&amp;gt;com.google.cloud.tools&amp;lt;/groupId&amp;gt;
                    &amp;lt;artifactId&amp;gt;jib-maven-plugin&amp;lt;/artifactId&amp;gt;
                    &amp;lt;version&amp;gt;2.1.0&amp;lt;/version&amp;gt;
                    &amp;lt;configuration&amp;gt;
                        &amp;lt;!--配置基本镜像，这里可以修改为自己的镜像，或精简或修改，但是一定要在私有库中有--&amp;gt;
                        &amp;lt;from&amp;gt;
                            &amp;lt;image&amp;gt;java:8&amp;lt;/image&amp;gt;
                        &amp;lt;/from&amp;gt;
                        &amp;lt;!--配置最终推送的地址，仓库名，镜像名--&amp;gt;
                        &amp;lt;to&amp;gt;
                            &amp;lt;tags&amp;gt;
                                &amp;lt;!--tag即镜像的版本，一般是覆盖latest并且新增一个当前版本号--&amp;gt;
                                &amp;lt;tag&amp;gt;latest&amp;lt;/tag&amp;gt;
                                &amp;lt;tag&amp;gt;${version}&amp;lt;/tag&amp;gt;
                            &amp;lt;/tags&amp;gt;
                            &amp;lt;!--配置私有仓库地址--&amp;gt;
                            &amp;lt;image&amp;gt;10.10.2.62:5000/v2/${project.build.finalName}&amp;lt;/image&amp;gt;
                        &amp;lt;/to&amp;gt;
                        &amp;lt;allowInsecureRegistries&amp;gt;true&amp;lt;/allowInsecureRegistries&amp;gt;
                        &amp;lt;container&amp;gt;
                            &amp;lt;!--jvm内存参数,jvm启动时的所有参数都可以在这里增加--&amp;gt;
                            &amp;lt;jvmFlags&amp;gt;
                                &amp;lt;jvmFlag&amp;gt;-Xms512m&amp;lt;/jvmFlag&amp;gt;
                                &amp;lt;jvmFlag&amp;gt;-Xmx512m&amp;lt;/jvmFlag&amp;gt;
                                &amp;lt;jvmFlag&amp;gt;-Duser.timezone=GMT+08&amp;lt;/jvmFlag&amp;gt;
                                &amp;lt;jvmFlag&amp;gt;-Dfile.encoding=UTF8&amp;lt;/jvmFlag&amp;gt;
                            &amp;lt;/jvmFlags&amp;gt;
                            &amp;lt;!--要暴露的端口--&amp;gt;
                            &amp;lt;ports&amp;gt;
                                &amp;lt;port&amp;gt;8761&amp;lt;/port&amp;gt;
                            &amp;lt;/ports&amp;gt;
                            &amp;lt;!--修改镜像默认时间，否则会导致镜像内时区问题--&amp;gt;
                            &amp;lt;creationTime&amp;gt;USE_CURRENT_TIMESTAMP&amp;lt;/creationTime&amp;gt;
                        &amp;lt;/container&amp;gt;
                    &amp;lt;/configuration&amp;gt;
                &amp;lt;/plugin&amp;gt;
            &amp;lt;/plugins&amp;gt;
        &amp;lt;/pluginManagement&amp;gt;
    &amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这种方式比dockerFile简单，但是也不灵活，适合简单项目。&lt;/p&gt;
</content:encoded></item><item><title>工作记录：记一次线上内存泄露问题的排查</title><link>https://www.intotw.cn/posts/work-oom/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-oom/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题的发现&lt;/h2&gt;
&lt;p&gt;发现当然还是运维大哥因为发现告警，包括自己邮箱也一堆告警，然后运维大哥做了dump以及jstack后立马重启，重启后暂时解决。&lt;/p&gt;
&lt;h2&gt;问题的排查&lt;/h2&gt;
&lt;p&gt;有dump和jstack记录，当然是好分析的，先分析这两个，原因就比较明显了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dump记录拉到本地用java自带的工具查看，发现大量netty的MpscArrayQueue对象没有释放，占用了大量内存，罪魁祸首就是这个，再看上级对象，是redisson使用到了netty，所以初步确定是redisson的问题。&lt;/li&gt;
&lt;li&gt;查看jstack，发现大量RUNNABLE线程，定位线程，发现是该定时任务模块自定义的线程池执行自定义的线程，那么为什么会把线程跑满呢？后面看了一下代码:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;  &amp;lt;bean id=&quot;threadPoolExecutor&quot; class=&quot;com.bwton.core.threadpool.SimpleThreadPoolExecutorFactoryBean&quot;&amp;gt;
        &amp;lt;property name=&quot;corePoolSize&quot; value=&quot;100&quot;/&amp;gt;
        &amp;lt;property name=&quot;maximumPoolSize&quot; value=&quot;200&quot;/&amp;gt;
        &amp;lt;property name=&quot;keepAliveTime&quot; value=&quot;3000&quot;/&amp;gt;
        &amp;lt;property name=&quot;workQueueCapacity&quot; value=&quot;10000&quot;/&amp;gt;
        &amp;lt;property name=&quot;rejectedExecutionHandler&quot; ref=&quot;rejectedExecutionHandler&quot;/&amp;gt;
        &amp;lt;property name=&quot;threadPackageScan&quot; value=&quot;com.bwton.v2.thread&quot;/&amp;gt;
    &amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class SimpleRejectedExecutionHandler implements RejectedExecutionHandler {

    private static Logger logger = LoggerFactory.getLogger(SimpleRejectedExecutionHandler.class);

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 记录异常、报警处理等
        logger.warn(&quot;【simple线程池】等待队列已满，正尝试以阻塞方式提交线程到等待队列.....&quot;);
        // 由blockingqueue的offer改成put阻塞方法
        try {
            executor.getQueue().put(r);
        } catch (InterruptedException e) {
            logger.warn(&quot;【simple线程池】等待队列已满，以阻塞方式提交线程到等待队列时被打断，fail!&quot;);
            e.printStackTrace();
        }
        logger.warn(&quot;【simple线程池】线程已加入等待队列，success!&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里封装了一个线程池，主要就是原生线程池加一个任务执行队列，出问题的主要就是这个线程池的拒绝策略，如果下游出现大量超时，新任务执行时，线程池会触发拒绝策略，使用put尝试继续放到该线程池的执行队列中，将调用的线程阻塞。&lt;strong&gt;又因为这个模块是定时任务&lt;/strong&gt;，每分钟会触发，查出200条待补偿的数据扔到线程池中补偿，所以是会不断产生任务的，最后导致大量线程在跑满，另外大量线程在阻塞等待放进队列，&lt;strong&gt;实际的执行逻辑是有很多Redisson操作的&lt;/strong&gt;，最后导致Redisson资源释放不掉，导致GC无法回收任何内存，最后OOM，这里据查Redisson官方Issue，之所以Redisson没有释放掉资源，也是和Redisson的一个bug有关，redisson在3.12.4的版本之前，在IdleConnectionTimeout该参数过小且负载过高时，redisson的线程池会无法得到有效的回收，从而导致oom，后续版本中也升级了Redisson的版本，不过触发原因是因为这个线程池的错误逻辑。&lt;/p&gt;
&lt;p&gt;后续修改了这个线程池的逻辑，并且增大了队列的大小，把补偿的业务逻辑也做成了异步的，就再也没有出现过这个问题了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;最好还是不要自己造轮子写线程池这些，可以写作练手或者学习，但是不要上生产，实在不得已，也要经过充分的测试。
dump和jstack堆栈信息，是排查java问题的第一帮手。&lt;/p&gt;
</content:encoded></item><item><title>工作记录：记一次线上ZK掉线问题排查</title><link>https://www.intotw.cn/posts/work-zookeeper/</link><guid isPermaLink="true">https://www.intotw.cn/posts/work-zookeeper/</guid><pubDate>Tue, 13 Oct 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题的发现&lt;/h2&gt;
&lt;p&gt;最早问题的发现在于用户提的，用户提出他支付时支付失败，过了一会儿再试就好了，于是翻日志，查询到当时duboo调用出现了下类错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[TraceID:20200527145701489] DEBUG c.y.c.s.w.s.m.m.a.HandlerMethodAspect - Throw: {}
com.alibaba.dubbo.rpc.RpcException: Forbid consumer 172.17.40.16 access service com.bwton.rpc.account.IAccountBindCardRpc from registry 10.10.2.201:2181 use dubbo version 2.8.4, Please check registry access list (whitelist/blacklist).
        at com.alibaba.dubbo.registry.integration.RegistryDirectory.doList(RegistryDirectory.java:579)
        at com.alibaba.dubbo.rpc.cluster.directory.AbstractDirectory.list(AbstractDirectory.java:73)
        at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.list(AbstractClusterInvoker.java:260)
        at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:219)
        at com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72)
        at com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52)
        at com.alibaba.dubbo.common.bytecode.proxy2.findAccountBindCardList(proxy2.java)
        at com.bwton.controller.account.bindcard.BindCardController.findAccountsByUser_aroundBody6(BindCardController.java:212)
        at com.bwton.controller.account.bindcard.BindCardController$AjcClosure7.run(BindCardController.java:1)
        at org.aspectj.runtime.reflect.JoinPointImpl.proceed(JoinPointImpl.java:149)
        at com.qbao.cat.plugin.DefaultPluginTemplate.proxyCollector(DefaultPluginTemplate.java:97)
        at com.qbao.cat.plugin.DefaultPluginTemplate.doAround(DefaultPluginTemplate.java:77)
        at com.qbao.cat.plugin.spring.SpringControllerPluginTemplate.doAround(SpringControllerPluginTemplate.java:27)
        at com.bwton.controller.account.bindcard.BindCardController.findAccountsByUser(BindCardController.java:201)
        at com.bwton.controller.account.bindcard.BindCardController$$FastClassBySpringCGLIB$$9f6f4f14.invoke(&amp;lt;generated&amp;gt;)
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85)
        at com.yanyan.core.spring.web.servlet.mvc.method.annotation.HandlerMethodAspect.aroundInvoke(HandlerMethodAspect.java:98)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这明显不科学啊？就算dubbo和zk的连接短时间内出现了问题，但是dubbo本地是有服务端列表缓存的，为什么会直接报这个没有提供者的错误呢？后续在elk上通过日志对问题进行定位，这报错在那段时间内，还真是多，而且各个soa上都出现了，怎么回事呢？通过zabbix监控看，这些服务器的cpu和内存占用并没有异常，流量也没有打满，为什么会大批量出现掉线？dubbo侧看不出来什么问题，于是决定去zk上看看。&lt;/p&gt;
&lt;h2&gt;zk的情况以及分析&lt;/h2&gt;
&lt;p&gt;登上几台zk看日志，发现每隔半小时到一小时，在某X5时间，都会出现大量断开连接的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2020-05-27T00:00:00+08:00 14:53:20,610 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@215] - Accepted socket connection from /10.10.1.11:35730
2020-05-27T00:00:00+08:00 14:53:20,610 [myid:1] - WARN  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@376] - Unable to read additional data from client sessionid 0x0, likely client has closed socket
2020-05-27T00:00:00+08:00 14:53:20,610 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@1040] - Closed socket connection for client /10.10.1.11:35730 (no session established for client)
2020-05-27T00:00:00+08:00 14:53:20,917 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxnFactory@215] - Accepted socket connection from /10.10.7.51:60135
2020-05-27T00:00:00+08:00 14:53:20,921 [myid:1] - INFO  [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:ZooKeeperServer@938] - Client attempting to establish new session at /10.10.7.51:60135
2020-05-27T00:00:00+08:00 14:53:20,977 [myid:1] - INFO  [CommitProcessor:1:ZooKeeperServer@683] - Established session 0x102d62c8fbab875 with negotiated timeout 10000 for client /10.10.7.51:60135
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，zk的日志大量刷屏，并且都是认为是远端没有了心跳，或者心跳超时，所以主动断开了连接，这也就解释了为什么明明dubbo在消费者端是有服务列表缓存的，但是还是会报上面的异常，原来是zk认为这些提供者下线了，所以通过通知，主动把整体服务的列表都刷新了，并不是提供者和zk之间单方面的问题，那么zk为什么会出现这样的问题呢？说实话，查了非常久，也是查了好几天，最后定位在：问题暴露在每五分钟的单位时间，即最短5分钟间隔，最长60分钟间隔，都会出现大量的批量下线后再批量上线的问题，虽然过程转瞬即逝，但是还是有部分业务会被影响到。于是查看有什么东西是5分钟的，查阅资料，zk和dubbo并没有什么缺省的5分钟或者更小整数倍数是5分钟的配置，我们也没有做类似配置，最后锁定到了一个嫌疑者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*/5 * * * * /usr/sbin/ntpdate X.X.X.X  &amp;amp;&amp;amp; hwclock -w &amp;gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;熟悉linux的同学都知道，这个是一个定时任务，用来同步服务器时钟的，每五分钟触发一次，后续经过测试发现，zk第三台机器与时钟同步机器的网络连接状况很差，所以第三台机器的时间在时钟同步后会出现波动，导致心跳机制出现问题，认为大量dubbo节点失效，所以主动踢除了这些被认为下线的节点，所以出现了如上bug。后续找网络工程师排查了服务器的网络问题后，该问题成功解决。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;在排查时，是真的有些乍一看让人摸不着头脑的问题，只能通过排除法以及相关信息去做推测并验证，并且客观反映了，AP较之CP，确实有其优越性，因为CP本身虽然可以保证一致性，但是谁有能保证CP自身的正确，如果CP因为某些原因自身出现了问题，那么风险也是不可承受的。这也是为什么eureka采用AP而不是CP的原因。&lt;/p&gt;
</content:encoded></item><item><title>First_post</title><link>https://www.intotw.cn/posts/first_post/</link><guid isPermaLink="true">https://www.intotw.cn/posts/first_post/</guid><pubDate>Thu, 09 Jan 2020 16:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;单纯只是一个测试而已&lt;/h2&gt;
&lt;p&gt;asd asd as das das dsa d&lt;/p&gt;
&lt;p&gt;asd
asd
as
d
as&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2024/06/91dea404efd488231ffac043578174ab.png&quot; alt=&quot;image-20240613234738132&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://images.intotw.tech/blog/2023/09/ebb4325ec7a8e0a9bd3fc3ed885e8424.jpg&quot; alt=&quot;xxl-job的图片&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;仨&lt;/h2&gt;
</content:encoded></item></channel></rss>