Gund Blog

Software Architecture & AI Thoughts

只要涉及两个个体之间协同工作就会遇到的基本问题。问题的名字叫“两军问题”。

两军问题 :

两个将军 A1 和 A2 决定攻打同一个敌人 B ,任一方都没有敌人强大,所以只有共同进攻才有胜算。敌军正好位于两个将军之间,意味着信使可能会被敌军抓住。两军问题指的就是在这样的条件下两个将军如何就是否进攻达成共识。

两军问题无解

有两位将军分别为A1与A2,准备要攻打B,A1计划明日9点准时同时攻击,并将消息发送给A2。

A1的信使必须要经过B的山谷,A1的信使能否到达A2是个未知数,有可能信使被抓住砍头。于是A1和A2约定,如果A2收到了A1的信使,就立刻派信使通知A1确认。同样的A2的信使也要经过B,也可能会被抓住。

那么故事开始了…..

A1信使出发了,带出去的信的内容如下:“上午9:00准时攻城”。然后A1耐心等待……

A1信使顺利B,A2获知了信息:“上午9:00准时进攻”,A2按照约定,立马将回复消息派遣信使发出:“上午9:00准时攻击,A2确认”,因为此时A1还不知道A2是否已经知道该军事计划。然后,A2伸长脖子在村口耐心等待………..

A2信使到达A1处,此时A1获知了A2同意了该军事计划,即“上午9:****00准时进攻”。此时A1已经同步双方的意图,但是A2却不知道自己的信使有没有到达Alice处啊!

A1需要继续派遣信使将这个事实告诉A2,信的内容如下:“上午9:00准时攻击,A2确认,A1确认”。

A1的信使安全到达A2,A2终于知道自己的信使已经已经安全到达了。但是事情到这里就结束了吗?

No!

A1还在一棵歪脖子树下等待信使安全到达的回信!所以A2还需要派遣信使发出:

“上午9:****00准时攻击,A2确认,A1确认,A2确认”。

…………

两军问题主要揭示了在连接不可靠的情况下,保证两节点状态同步有多么的难,而互联网恰巧就是这么一个连接不可靠的网络(TCP 协议不可能保证两个网络终端之间的状态是一致的),举个例子通过互联网连接的两台计算机的屏幕上,要想百分百保证随时都显示相同的字符,是不可能的。认识到网络的这种不确定性,是理解所有分布式系统的基础

虽然理论上无解,但是通过设计我们依然能够给出一个够用的解决方案

对于两军问题我们可以努力把不确定控制到一个可以接受的程度。例如,我们可以让将军 A1 发出一百个信使送信,那么信使全部都被抓住的可能性就很小了。这样,A1 自己决定进攻,A2 只要收到哪怕一个信使的信息也会进攻。这就是一个足够好的解决方案了。

实际应用中,TCP协议三次握手就是为了降低不确定性

什么是分布式系统?

什么是事务

几种解决方案:

XA协议

多段提交

事务补偿

最近两天发现了一个问题,我们长久以来依赖的一种工作模式, 当我们需要应用某项新的技术或者功能时,我们选个哥们,让他搞出来个例子,然后其他人照着抄就完事了, 长期这样工作之后,造成了一种这样的现状, 没人知道为什么要这么写代码:

比如, 随便找个人随便找段代码问为什么要这样处理?

  • 我不知道, 网上某篇文章这么写的.
  • 我不知道, 我从X复制粘贴了它.
  • 我不知道, 我在上一个项目中就是这样做的.
  • 我不知道, 别人告诉我的.

我们大家都已经非常习惯这种开发方式了,从不三思后行,从不去思考为什么; 如果我们只是单纯的听信权威的话还好, 比如完全按照官方的文档去做(官方文档也不一定全对). 但是实际中如果我们按照不知道哪里来的阿猫阿狗的文章去做, 而没有一丝怀疑的话 那我们的路只能越走越窄了,觉得这个框架辣鸡,那个解决方案扯淡, 其实真正出问题的是我们自己

网上的技术文章大部分都是胡扯

我希望大家都能意识到这一点, 文章会过时,版本会更新,环境有区别,别人的应用场景跟你并不一样, 人也不是完美的, 再高级的开发人员也会写bug, 大牛的解决方案也不一定完美; 博客文章的数量或者此人的薪水不能说明作者的水平, 一个人屁股坐的位置并不能决定这个人敲出来的是不是一段段的狗屎

能越早意识到网上到处都是胡说八道, 就越好.  

为什么会变成这样

因为我们很懒, 是的通常情况下我们都不傻, 只是懒. 如果有人给我们解决问题的办法并且确实可行, 那为什么还要去思考? 有什么理由不粘贴复制一把送它上生产?

主要是我们没有足够的时间. 论证一个方案可能会花费几个小时阅读源码, 还要编写大量的测试来验证或者阅读大量的文档来学习. 但是每个项目我们都有急迫的截止时间, 何必呐 对吧

学习是一件痛苦的事, 学习的本质就是反人性的, 那些号称的快乐学习必然什么都学不会; 碎片化的学习只能让你了解一些吹逼用的话题, 真正的学习就是要强迫自己走出舒适区, 因为你要或得新的知识,就必须要努力的理解新的概念或者进行额外的工作

当然还有一个原因就是我们不自信. 大家,特别是职业初期,往往认为自己的解决方案永远不够好, 所以只能依赖各种权威, 却从不质疑.

互联网最好的事情是任何人都可以发布内容, 最糟糕的事情也是任何人都可以发布内容; 软件开发行业发展的太快, 大量新手冒充专家, 很快就会淹没掉真正的专家, 劣币驱逐良币

在软件开发中遵循行业通用的模式和最佳实践,而不去考虑它们是否真正符合自己的场景,这是非常常见并且错误的做法.基本上每个软件项目都存在细微差别,多多少少要在某些方面需要偏离行业规范。始终盲目地遵循最佳实践而不质疑它们或使它们适应自己场景就是一种可怕的实践。

怎么解决这个问题

首先要意识到网上的资料存在大量的错误信息,任何人及其解决方案都并非正确.  包括这篇文章

任何解决方案都只适应特定用例。没有一种万能的解决方案可以解决所有问题。比较不同的方法,进行分析。教程或文章显示了一个想法,但可能没有提供可用于生产的代码。在决定使用它之前,请务必对其进行分析。

相信你自己。您的解决方案并不比互联网上的解决方案差。

不断学习,并保持好奇心。成为开发人员就是要不断学习。确保了解所使用的库或框架。这是才使用它们的目的。

人们有时在没有更深刻理解的情况下使用三方的类库,会导致滥用它的核心概念,然后编写出更复杂或性能更低的代码。

维基百科的定义

分布式系统是一种其组件位于不同的联网计算机上的系统,然后通过互相传递消息来进行通信和协调。为了达到共同的目标,这些组件会相互作用。

所以,“单程序 + 单数据库”就是一个最简单的分布式系统。因为我们所编写的程序运行时所在的进程,和程序中使用到的数据库所在的进程,并不是同一个。“一分为二”和“一分为 N”本质上并没有区别。所以,很多小项目或者大型项目的初期所搭配的基础套餐“单程序 + 单数据库”,同样可以理解为分布式系统,其中遇到的问题很多同样也存在于成熟的分布式系统中。比如 :

  • log 记录执行成功,但是数据库的数据没发生变化;
  • 进程内的缓存数据更新了,但是数据库更新失败了。

分布式系统会碰到的几个典型的问题(坑):

  • 网络并不是可靠的
  • 不同节点之间的通信是存在延迟的
  • 带宽是有上限的
  • 分布式并不直接意味着是“敏捷开发”了
  • 数据由一份冗余成多份后如何保持一致
  • 整个系统的不同部分可能是异构的

网络并不是可靠的

光缆被挖断的事件相信你也看到过不是一两次了。除此之外,网卡异常、交换机故障、遭受恶意攻击等导致的网络拥塞、网络中断、报文丢失的种种迹象皆意味着网络随时可能无法正常运作,是不可靠的。

系统设计中,尽可能地考虑到:当前节点所依赖的其他节点由于各种原因无法与之正常通信时,该如何保证其依然能够提供部分或者完整的服务

不同节点之间的通信是存在延迟的

不同节点之间的通信是需要经过一段时间的,也就意味着会存在延迟。具体的延迟是由所用的传输介质、节点当前的负载大小所决定的。不能以调用本地方法一样的认知去理解远程调用。

并不是将一个集中式系统拆分得越散,系统就越快。

带宽是有上限的

  1. 实际环境中的传输速率大小,是由服务商所提供的带宽大小,以及网卡、网卡、交换机、路由器所支持的传输速率中的最小值决定的。
  2. 内网与外网传输速率是不同的,一般都是内网大于外网。因为服务商所提供的带宽成本更高。
  3. 同一个局域网内的节点是公用外网出入口的,所以尽可能的缩小在外网传输的数据,以降低占用“独木桥”外网的空间。

分布式并不直接意味着是“敏捷”了

很久以前曾经有过这样的想法,当在规模较大的集中式系统中工作的时候,每次和许多人在一个代码库里提交代码,老是遇到冲突、排队等待上游模块先开发等等。这时你会想,如果改造成分布式系统,这些问题都没了,工作效率高多了。

拆分后需要做的额外工作如果没做好,可能会导致不是更快,而是更慢。最典型的现象如:

  • 发布更麻烦了。
  • 排查问题更难了。

建设协作相关的辅助性工作与分布式系统同时进行。比如:监控告警系统、配置中心、服务发现,以及批量部署、持续集成,甚至 DevOps 等。

数据由一份冗余成多份后如何保持一致

这点其实是由于前面提到的网络因素产生的连带效应。

当遇到数据库压力增大,响应开始变慢的时候,你可能会很容易想到,让 DBA 来做个主从啊。但是,由于网络不可靠、存在延迟、带宽有上限这一系列因素。所以,这个看似只是 Copy 一下工作,需要我们花大量的精力去解决如何保证在使用不同副本上的数据的时候,都是符合应有的预期的

(兼论为什么大多数人连“滥用”的资格都没有)

AI这玩意儿现在真是烂大街了。你媳妇听到了也很可能问你一句:“AI?就是那个天天帮我P图、写小红书文案的东西?你们不就是用它写写bug吗?”以上虽然是个笑话,也表现出在实际工作中大家对于AI的态度——哪怕到不了顶礼膜拜的程度,起码也觉得这玩意儿无所不能。

以下有两种使用AI的情况:

● 每次鼓起勇气想认真学AI,带着“我要系统掌握Prompt Engineering”的雄心壮志,同时选了一堆晦涩难懂的资料去学习,被各种“思维链”“Few-shot”“RAG”名词轰得七荤八素,头昏眼花,学习过程体验还不如刷短视频,只好草草放弃。

● 另一种情况,平常做crud的时候,AI就是用来生成重复代码的,大部分时间只需要遵从AI的建议直接点Tab黏贴,稍微高级点儿的用法压根就用不上,长此以往,别说深度使用AI了,连判断AI生成的代码对不对都做不到,如果不是为了面试,都懒得了解AI到底还能干啥。

AI说白了就是解决问题的工具。约会要AA,打游戏要4090,灯泡不能塞嘴里——这些都是日常非常典型的生活模式。用AI也是同样的简单概念。工作中碰到问题后,聪明人会用AI来解决问题,久而久之得到了认可和推广,总结为解决这类问题的使用模式。

那么,为什么AI这么简单的东西,大部分人还是用不好呢?我总结下来有三个核心观点。

一、学会用AI的方法只有一个:滥用AI

这话听着离谱,但仔细想想就明白了。

你学骑自行车怎么学会的?摔出来的。你学游泳怎么学会的?呛出来的。你学写代码怎么学会的?bug改出来的。

同样的道理,你想学会用AI,只有一个办法——往死里用

很多人问我:“我怎么才能学会用AI?”我说你一天用多少次?他说大概一周两三次,问个问题、写个文案啥的。我说你这种用法,学十年也学不会。

什么叫“滥用”?就是什么事都丢给AI试试,哪怕是明明自己两分钟能做完的事,也要丢给AI看它怎么做。用错了,再试;试错了,再换种问法。久而久之,你就能形成一种“AI语感”——你知道什么样的任务适合AI,什么样的问题该怎么描述,什么样的结果可以接受。

别小看这种“语感”。这跟你学英语是一样的,语法规则你背得再熟,没有大量“滥用”英语的实践,你开口还是磕磕巴巴。

但这里有个陷阱:很多人把“滥用”理解成了“无脑用”。这是两码事。无脑用是“扔给AI,全盘接受,从不思考”——这不是滥用,这叫大脑托管。Anthropic今年那篇论文已经把话说得很明白了:那些把AI当外包、全盘接受AI代码的人,认知能力直接掉了17%,从阿里P7跌回校招实习生。而且他们完成任务的速度优势,只有区区2分钟,在统计学上都不算显著

所以“滥用”不是让你放弃思考,而是让你高频次、多场景、有意识地去用。每一次用都是在训练你的AI直觉。

二、如何判断一个任务是否适合AI?问自己三个问题

滥用归滥用,你不能指望AI帮你生孩子。在“滥用”的过程中,你得学会判断什么样的任务适合AI。

我总结了一个“灵魂三问”:

第一问:我能精确描述我想要达成什么结果吗?

这一问直接筛掉了80%的人。

很多人用AI的姿势是这样的:“帮我写个好一点的文案。”然后AI输出一堆东西,他们说:“不行,不够好。”再让AI改,还是不行。最后得出结论:“AI真垃圾。”

问题出在哪?出在你根本不知道自己要什么。“好一点”是什么意思?是更有趣?更专业?更短?更煽情?你自己都说不清楚,凭什么让AI猜?

苹果那篇翻车的AI论文就是个活教材。他们用AI自动生成benchmark的GT(Ground Truth,标注数据),结果错误率高达30%,论文都被喷撤稿了。问题出在哪?不是AI的问题,是研究团队根本没有精确定义“什么样的GT是正确的”,也没认真检验AI生成的GT是否正确。最终导致北京一个程序员熬夜适配他们的benchmark,跑出来的结果一塌糊涂,才发现了这个荒谬的错误

这一问的核心是:如果你自己都不知道什么样的结果是好的,AI一定不知道。

第二问:我能把这个任务拆分成AI能理解的子任务吗?

这一问考验的是你的“任务拆解”能力。

AI不是万能的,你不能指望一句“帮我做个电商网站”就完事了。但如果你能把“做个电商网站”拆成“设计数据库表结构”“写商品列表API”“写用户登录接口”……那AI就能一个一个帮你搞定。

CodeRabbit 2025年分析了470个GitHub Pull Request,发现AI生成的代码平均每个PR有10.83个问题,而人类代码只有6.45个,缺陷率高1.7倍。但问题是,为什么还有那么多人用AI写代码写得飞起?

区别就在于:会拆任务的人,AI就是神助攻;不会拆的人,AI就是屎山制造机。

Veracode的测试更狠——他们测试了100多个大模型,45%的AI生成代码没有通过安全测试,Java的安全失败率高达72%。AI生成的代码在跨站脚本漏洞上几乎全灭,86%的相关样本都挂了。这意味着如果你直接把AI生成的代码扔到生产环境,后果可想而知。

但那些能用好AI的程序员是怎么做的?他们把任务拆成:先让AI生成骨架代码,然后自己审查安全逻辑,再让AI补充测试用例,最后人工整合。每一步AI都在做它擅长的事——写重复代码;每一步人类都在做自己擅长的事——把控方向、审查安全。

第三问:AI最终生成的东西,我能判断对错吗?

这一问是最要命的。

很多人用AI是这样的:让AI写个旅游攻略,然后拿着攻略就出门了。结果到了目的地发现网红咖啡馆根本不存在——厦门罗女士就中过这个招。AI旅游攻略会虚构景点、不考虑路程、闭馆信息不更新,这些事早就被网友吐槽烂了

谷歌用AI改写新闻标题,把“Valve明确表示明年才会公布价格”改成了“Steam Machine价格公布”,活脱脱一个标题党制造机。GPT-5.2号称“人类专家水平”,结果连26个英文字母都数不全,生成的字母挂图遗漏了U和Z,插图里的动物长得跟生化危机似的

这些问题的本质是什么?你没有办法判断AI输出的是真还是假。

所以这一问的关键在于:在你交给AI的任务领域里,你得有基本的判断力。你是设计师,你能看出AI生成的图比例对不对、风格合不合适;你是程序员,你能判断AI写的代码逻辑有没有问题、安全有没有漏洞;你是写作者,你能分辨AI写的句子通不通、有没有跑题。

如果没有这个判断力,AI就是一台精致的信息污染机。

三、AI是放大器:能力越强,效果越好;能力越差,反噬越狠

这是我第三个观点,也是最重要的一个。

AI不是什么“替代人类”的东西,AI是能力的乘法运算

怎么理解?假设你本身的能力是100分:

  • 能力100 × AI(用得好的话)= 可能是1000,甚至是10000。
  • 但如果你本身的能力是10分:能力10 × AI = 可能还是10,甚至是1。

更可怕的是,如果你能力差还乱用AI,AI会变成一个负向放大器,把你的错误和缺陷放大到极致。

最近有个新闻特别能说明问题:德国莱比锡一个哥们用AI聊天机器人帮自己在法庭上辩护,居然赢了。你以为是AI厉害?错了。新闻里明确说,他的法律论据里有“重大错误”。但为什么还是赢了?因为这哥们本身懂法,他知道怎么用AI辅助自己,而不是把整个辩护扔给AI。如果是一个完全不懂法的人用AI辩护,结果大概率是输得一塌糊涂。

反过来看反面案例。2025年AI技术滥用事件有近120起。安徽铜陵一女子用AI生成“流浪汉卧坐家中”的逼真图像,丈夫收到后吓到报警。12岁小孩用AI做整蛊图发业主群,整个小区被吓坏。AI谣言批量生产,从“江苏干部买方便面被通报”到“华山医院前院长客死他乡”,一条比一条逼真,一条比一条离谱

这些是什么?这些是“能力差 × AI = 灾难”的完美案例。造谣的人本来就会编故事,AI只是让他们的“产能”暴涨了100倍。AI没有创造恶意,AI只是放大了恶意。

企业级AI更是这样。某保险公司AI理赔系统上线准确率仅60%,某制造企业AI质检误判率15%而人工仅3%,某超市AI补货系统上线后缺货率飙升40%。这些项目的问题出在哪?不是AI不行,是数据不行、流程不行、人的判断不行。AI只是忠实地把这些“不行”放大成了“灾难”。

CodeRabbit的报告里有一句话说得特别好:AI加速了输出,但它也放大了特定类型的错误。逻辑错误率暴涨75%,安全漏洞翻倍增长——AI不是制造了这些错误,它只是让一个能力不足的程序员更快地写出更多错误代码。

所以结论是什么?

AI不会让你从菜鸟变大神。AI只会让大神变得更高产,让菜鸟暴露得更彻底。


最后说句掏心窝子的话。

很多人焦虑AI会取代自己。但真正该焦虑的不是“AI会不会取代我”,而是“我会不会用AI”。

你不会用AI,但你的竞争对手会用AI——那你就被取代了。

怎么学会用?就一句话:滥用它,往死里用,但用的时候带上脑子。

每次用AI的时候,问自己那三个问题:我能说清楚我要什么吗?我能拆成AI能懂的子任务吗?我能判断AI生成的东西对不对吗?

三个问题答不上来,就先别用AI——先把自己的能力修炼上去。因为AI是放大器,在你能力还不行的时候用AI,AI只会把你的“不行”放大成“灾难”。

反之,如果你已经是个狠人,AI会让你变成超级赛亚人。

这不是选择题。这是乘法题。

线上系统为何经常出错?数据库为何屡遭黑手?业务调用为何频频失败?连环异常堆栈案,究竟是哪次调用所为? 数百台服务器意外雪崩背后又隐藏着什么?是软件的扭曲还是硬件的沦丧?

  • 前言
              由于代码 bug(本身或三方)、环境、硬件等原因,线上服务出现故障/问题几乎不可避免。例如,常见的现象包括请求超时,卡顿,死机等等。
              作为一名开发人员,除了写好代码,排查解决线上的问题也是必须掌握的生存技能. 在生产环境中,是很难进行debug的,常常只能依靠一些工具/命令/平台来获取程序运行时的信息,包括但不限于log(异常堆栈),监控平台的输出,Jvm运行情况等等.

                    当你找到并解决了一个别人没搞定的问题时,会有巨大的成就感.就有人这么问过我 “你是怎么想到的?你是怎么定位到问题的? “ 我一般只能说 “很简单/全看脸/靠经验”,这里都会觉得排查问题要考经验,但是很难解释清楚到底是什么样的经验帮助你排查出了问题, 我这里总结一下,靠经验指的是 “依赖基础知识和对系统环境的了解,运用各种工具得出的数据进行总结分析来定位问题和解决问题“的过程, 有的人会有误区,认为靠经验是把所有碰到的问题以及解决方法 全部记录下来,再碰到问题时就能对号入座了  我认为这是绝对错误的做法, 在我们动手解决问题之前,第一步是要知道What(是什么),然后是Why(为什么),最后才是How(怎么做)。抛弃前两步直接上手How 只会事倍功半

  • 常见问题
    • 环境异常

常见的 cpu负载过高,硬盘满了,I/O满了,带宽满了(连接数满了),可用内存不够等等.这些问题如果是在Linux系统下可以通过 top(cpu)、free(内存)、df(磁盘)、dstat(网络流量)、pstack、vmstat、strace(底层系统调用) 等工具获取系统异常现象数据。

- 业务异常

调用服务api超时,线程死锁,多线程并发问题,频繁gc,连接泄露等等

  • 方法论

排查问题就像柯南破案一样,不停分析线索,推理的过程.那么在进行推理之前,我们先明确几个事情:

  1. 世界是唯物的,计算机是一门科学,这里只有0和1,是和否,这里发生的问题只有必然没有偶然,所有问题都如墨菲定律所言的一样,我们玩的是本格推理,玄学的思路和解决方案我们即不讨论也不接受的
  2. 系统出现异常是很正常的事,现在一个不算复杂的系统上,一次用户请求可能要经过发送请求,DNS解析,运营商网络,负载均衡,服务器,虚拟机(容器),视业务逻辑的复杂程度可能还要调用组件,缓存,存储和数据库等。每个环节都可能出现问题,有的组件又是分布式的,大大增加的排查问题的难度,所以出现问题后不要慌,保持好的心态。
  3. 还原恢复是首要目的,找到问题并解决是次要的
  • 流程
    • 收集信息
      • 了解现场情况,评估影响范围   先评估出问题的影响范围,是全网还是某个账户,是目标的网络访问有故障还是整条链路都不通了
        • 问题的表现是什么? 无响应? 报错?
        • 问题是什么时候发现的?
        • 是否可以重现?
        • 有没有规律
        • 系统最近的一次更新时间和内容(包括但不限于代码,配置,网络,服务器等)
        • 受影响的用户范围
        • 监控反馈
        • 看日志
      • 应用的检查
        • 最近是否有更新(程序/配置)
        • 软硬件是否有变更
        • 日志里是否有异常
        • 重启是否生效
      • 数据库检查
        • 数据库配置是否有变更
        • telnet端口是否畅通
        • show processlist
        • sql在本地和远程执行都正常吗
      • 硬件环境
        • 硬盘剩余空间
        • top / free
        • ping / telnet 是否有丢包
        • traceroute -l 
        • 时区字符集文件句柄数等参数
        • 监控系统
      •  总之就是尽可能的收集有效信息,然后去排除分析:
        • 通过变更记录来定位问题,很多问题是因为新版本上线,变更造成的
        • 通过影响用户,复现问题,能稳定复现的问题排查就很容易了
        • 查看日志 数据记录,定位问题
        • 如果没办法从日志里面直接发现异常,那么只能通过特定的工具 去监视应用到底在干什么
        • 原则就是 找出系统正在执行“什么”,询问系统“为什么”执行这些操作,以及系统的资源都被用在了“哪里”可以帮助你了解系统为什么出错。这个过程中尤其要注意并不是所有的证据都是可信的,要有注意甄别真假
    • 恢复服务
      • 摘机 在不影响整体业务的情况把出问题的n个服务/机器 下线
      • 重启 专治各种cpu/内存/线程池爆满
      • 回滚 回滚到出问题之前的状态(程序/配置)
      • 降级 比如:Nginx修改权重
    • 保留现场
      • 运行日志 出问题期间的Log文件
      • 运行状态 比如Jvm的线程dump
      • 保留现场与恢复服务看实际情况 很难分先后
    • 排查问题
      • 看清本质
        • 看到一件现象或一件事情,要看实质而不只是表面的东西,排查问题也一样切忌先入为主,有时候你觉得极其简单,看似非常不可能发生的事情,可能就是原因,不要轻易的排除掉某项原因。
      • 定位方向
        • 从大到小 先查宏观的问题再查细节
        • 从上到下 从异常发生的入口逐步深入想下
        • 并不是所有问题的适用这样的规律,由量变产生质变的问题就只能微观分析,再宏观诊断
        • 这个阶段一定耐心细心
    • 验证
      • 开发/测试环境验证
      • 生产环境的A/B测试
  • 归档记录
    • 记录问题 针对具体问题的反思,应该在代码,组件,环境中做出那些调整,避免再出现同类型的问题
    • 记录排查解决问题的过程 对过程的反思,我们的思路,使用的工具,是否都正确,能否更高效

“我有了自己的车,我有了自己的活,我有了自己的梦。”

—— 老舍《骆驼祥子》,1926。只不过那时候,车是黄包车;现在是 Cursor Pro。


先讲个事儿, 如有雷同纯属巧合

假如在过去的某一次会议上,CTO 做了一场关于”AI 赋能全员提效”的分享。

PPT 翻到最后一页,赫然写着四个大字:拥抱变化

底下小字写着:

  • 公司鼓励大家积极使用 AI 工具辅助开发
  • AI 工具及 token 费用,由员工自行承担
  • 本月起,AI 辅助产出纳入绩效考核参考维度
  • 未使用 AI 工具的同学,需在周报中说明原因

就这四条。没有”企业版账号”,没有”基础额度”,没有”超额自理”——自理就是全部,一毛钱都不出。

会议室里掌声雷动。我怀疑鼓掌的人里,有一半还没反应过来这条意味着什么。

散会后,坐我旁边的小张凑过来,压低声音问:”哥,公司给报销 token 吗?”

我说:”报销?你想多了。不是超额自理,是完全自理。Cursor 自己买,API 自己充,账单自己扛。公司给你发工资就不错了——不对,发工资是因为你出了劳动力;token 是你自己的生产资料,公司连生产资料都不配,跟网约车司机买车一样——车是你的,油是你的,保险是你的,单子是平台的,乘客是平台的,好评是平台的,差评是你的。

小张脸都绿了:”那这跟自费上班有啥区别?”

我说:”区别就是,自费上班至少不用写周报解释为啥没用 AI。”

小张愣了三秒,说:”那咱们图啥?”

我想了想,说:”图个车技。”


一、祥子的车,换成了 API Key

老舍笔下的祥子,北平城一个拉车的。

他最大的梦想,是拥有一辆自己的车。买了车,就不用租车了,不用每天交”份儿钱”给车厂老板。每一分力气挣来的钱,都是自己的。

他省吃俭用,三年攒下一百块,终于买了辆新车。

然后车被大兵抢了。

他又攒钱,买了辆二手车。

然后车被侦探敲诈走了。

他又攒钱……

最后,祥子没了车,没了梦,没了人样。

2026 年的祥子,不拉黄包车了。他拉 token。

公司的逻辑已经写得很清楚了,只是换了一套数码外衣:

1
2
公司发工资 → 你买 token → 你用 token 干活 → 公司收产出
不买 token → 干不了活 → 绩效不好看 → 工资也悬了

你看,多完美的闭环。

资本家连生产资料都不用买了。以前开工厂,得买机器、租厂房、交水电。现在好了,机器(GPU)是云厂商的,token 是你自己充的,电费是你自己交的,网速慢是你自己忍的,Cursor 闪退是你自己重启的。

有的公司还装个样子,给个企业账号、报一点额度,超额了让你自己掏——那叫”超额自理”,至少面上过得去。

咱们这儿连装都不装:完全自理。

公司只出一样东西:KPI。

祥子当年租车拉活,一天交多少份儿钱,车厂老板心里有数。现在的份儿钱不叫份儿钱,叫 $20/月 Pro 订阅,叫 按量计费,叫 Max Mode 另算

名字换了,本质没变:你不干活,可以;你不交钱,不行。


二、token 搬运工:赚的是差价,亏的是人生

我认识一个哥们,在某大厂做后端,自称”AI 深度用户”。

他每天的工作流程是这样的:

  1. 早上九点,打开 Cursor,看一眼昨晚烧了多少 token
  2. 心疼三秒钟,然后告诉自己”这是投资”
  3. 开始跟 AI 对话,生成代码、改 bug、写文档、写周报
  4. 下午五点,再看一眼 token 账单,心疼三十秒
  5. 月底算总账:工资到账一万八,AI 工具开销三千二

他跟我说:”没事,我效率高啊,以前三天的活现在一天干完,多出来的时间我可以接私活。”

我说:”那你私活赚多少?”

他想了想:”私活也得用 AI 啊,又花出去八百……”

这就是新一代的 token 搬运工。

跟开网约车一个逻辑:

网约车司机 token 搬运工
自己买车 自己买 Pro 订阅
自己加油充电 自己充 API 额度
自己保养维修 自己调 prompt、修 AI 生成的屎山
平台抽成 20%-30% 公司拿你走量后的产出
乘客投诉扣你分 Code Review 不过扣你绩效
吃饭喝水停车自己掏 电费网费显示器自己掏
最后赚的是车辆残值 最后赚的是”我会用 AI”这个标签

有个跑网约车的师傅跟我说过一句话,当时觉得俗,现在品出味儿了:

“我骄傲的是我的车技,其实带来客流量的是车型。”

翻译到咱们这行:

“我骄傲的是我的 prompt 功力,其实带来产出的是 Claude Opus 的智商。”

你把 AI 关了,试试自己手写一个分布式锁?试试自己从零搭一个 CI/CD 流水线?试试不用 Copilot 写一个完整的 CRUD 模块?

不是不能,是不值得——在公司眼里,你三天干完的活,AI 三小时就干完了。你凭什么拿一样的钱?

所以你得买 token。买了 token,你才是”高效员工”。不买 token,你是”落后分子”。

生产资料私有化了,剩余价值剥削公开化了。

妙啊。


三、公司的算盘,打得比 GPT-5 还精

咱们来算笔账。

假设一个十人的研发团队,以前:

  • 十个程序员,人均月薪 2 万,团队月成本 20 万
  • 公司配电脑、配显示器、配工位、配咖啡机
  • 产出:十个脑袋想出来的东西

现在:

  • 十个程序员,人均月薪还是 2 万,团队月成本还是 20 万
  • 但每个人自己掏 2000-3000 买 AI 工具——公司一分不出
  • 公司省了培训成本——“不会用 AI?那是你自己的事”
  • 公司省了工具成本——“鼓励使用”,但鼓励不等于掏钱,跟过年说”祝你发财”不给红包一个德行
  • 公司省了管理成本——“AI 辅助产出纳入绩效”,翻译过来就是”干得好是你该的,干不好是你没用好工具”
  • 产出:十个脑袋 + 十个 AI 想出来的东西,但公司只付十个脑袋的钱,AI 的智商账单算在你头上

别的公司好歹还有个”企业版账号,额度有限”——听着像那么回事,超额了才让你补。咱们连这张遮羞布都省了:从第一分钱起,就是你自己的。

更狠的是,公司还可以这么说:

“我们不是在裁员,我们是在赋能。”
“我们不是在降本,我们是在增效。”
“我们不是在转嫁成本,我们是在培养你的数字素养。”

赋能。增效。数字素养。

这三个词,比祥子当年攒的那一百块大洋还值钱——因为它们不要钱,但要命。

我见过别的公司”AI 使用规范”里写过这种话:

员工因使用 AI 工具产生的额外费用,可在年度绩效优秀时酌情考虑补贴。

好歹还有个”酌情考虑”——虽然表现好了也不一定给,但至少假装可能会给。

咱们这儿连假装都省了。规范里就一句:费用自理。

两个字,比”酌情考虑”诚实,也比”酌情考虑”恶心——因为它连糊弄你的耐心都没有。

不给你幻想,不给你盼头,不给你”好好干年底分红”的鬼话。就是:你要用,你自己买;你不用,你绩效难看。

祥子当年租车,车厂老板至少还出车——车是老板的,你只是交份儿钱。

现在是你自己买车、自己加油、自己修车,拉的还是老板的客,份儿钱照交,而且车厂连”我们提供车辆”这句场面话都懒得说了。

超级恶心。但恶心归恶心,明天还得打开 Cursor 自己充钱。


四、骆驼祥子的三重堕落

老舍写祥子,写了他的”三起三落”。

咱们这代数码祥子,也有三落:

第一落:从”工程师”到”提示词操作员”

以前自我介绍:”我是做后端的,主要搞分布式系统。”

现在自我介绍:”我是做 AI 辅助开发的,主要搞 prompt 工程和代码审查。”

听着高级了,实际上——你的核心技能从”造东西”变成了”跟机器说话”。

机器说啥你信啥,机器写啥你合并,机器错了你背锅。

你不再是工程师,你是工程师和 AI 之间的翻译官——而且翻译官不报销差旅费。

第二落:从”投资自己”到”投资平台”

祥子攒钱买车,是为了摆脱剥削。

你攒钱买 token,是为了更好地被剥削

这个逻辑有多荒谬,类比一下就明白了:

祥子,你为啥买车?

为了不用租车了,挣的钱都是自己的。

那你现在为啥买 Cursor Pro?

为了……提高效率……帮公司多干活……然后……

然后什么?

然后公司觉得你效率高,下个月 KPI 涨 30%。

你投资的是自己的生产力,变现的是公司的利润率。

第三落:从”有梦”到”麻木”

祥子最后的结局,是堕落、麻木、混日子。

数码祥子的结局,大概率是:

  • 不再追问”这段代码为什么这样写”——AI 写的,能跑就行
  • 不再追问”这个架构是否合理”——AI 生成的,能上线就行
  • 不再追问”我为什么要买 token”——不买就干不了活,问了也没用
  • 不再追问”公司为什么不报销”——别人也不报销,问了显得矫情

当所有人都自己买 token 的时候,不买 token 就成了你的问题,而不是公司的问题。

这就是最高明的剥削——让被剥削者觉得,剥削是理所当然的。


五、我骄傲的是我的车技

说回网约车那个比喻。

有个数据,不知道准不准,但听着挺真的:很多网约车司机,刨去车辆折旧、油费电费、平台抽成、保险保养、吃饭喝水之后,时薪不如麦当劳打工。

但他们还在跑。

为什么?

因为**”我有了自己的车”**。

这五个字,跟”我有了自己的 Cursor Pro”一样,是一种奇怪的尊严——

  • 你不是打工仔,你是个体户
  • 你不是被雇佣,你是合作方
  • 你不是出卖劳动力,你是经营自己的事业

只不过,你的事业没有营业执照,没有五险一金,没有年假,没有加班费。

你的事业就是:每天打开 IDE,跟 AI 一起,给老板打工。

我认识一个前端,技术不错,在公司干了五年。去年开始自己买 Copilot + Cursor + ChatGPT Plus,每月固定开销两千多。

他跟我说:”我觉得值。以前写一个组件要半天,现在半小时。我多出来的时间可以学新技术。”

半年后,公司裁了他。

理由是:”AI 可以替代基础开发工作,团队需要精简。”

他走之前跟我说了一句话,我记到现在:

“我教会了 AI 写我的代码,然后公司用 AI 换掉了写代码的我。”

他的车技确实好。但乘客要的不是车技,是便宜、快、能到

AI 这三点都做到了。而且不用交社保。


六、那怎么办?

说实话,我也没什么好办法。

这不是技术问题,是结构问题。

祥子要是会写 prompt,他就能摆脱命运吗?不能。他最多从一个拉车的,变成一个拉 token 的——车还是别人的,路还是别人修的,规则还是别人定的。

但有几件事,我觉得可以说:

第一,别骗自己说”投资自己”。

你买 token 如果是公司逼的、绩效逼的、同行逼的——那不是投资,那是过路费。过路费交得越多,不代表你离目的地越近,只代表这段路越来越堵。

第二,算清楚账。

每月工资多少,每月 AI 开销多少,每月多产出多少,多产出有没有变成你的钱?

如果公式是:

1
2
3
你的净收益 = 工资 - token 成本 - 加班时长 × 时薪
+ 绩效奖金(如果有的话)
- 健康损耗(颈椎病、焦虑症、失眠)

算出来是正数,继续干。

算出来是负数,你不是在上班,你是在付费上班。

第三,车技还是要练的。

不是因为车技能带来客流量——带来客流量的是车型,是平台,是算法推荐。

而是因为,有一天车被收走的时候,你至少还会走路。

AI 能写代码,但它不能替你去跟产品经理吵架。

AI 能生成架构,但它不能替你在凌晨三点被 oncall 叫醒时判断该不该回滚。

AI 能优化 SQL,但它不能替你在裁员名单公布前,知道自己值多少钱。

这些能力,不花 token。

但它们是你唯一真正拥有的东西。


结语

老舍写祥子,不是写一个人怎么买车拉车。

是写一个人怎么被一套系统一点一点榨干,直到他不再是一个人。

2026 年的我们,不需要在烈日下拉黄包车。

我们坐在空调房里,对着屏幕,一行一行地烧 token,一天一天地交份儿钱。

祥子至少还有个具象的敌人——车厂老板刘四爷。

我们的敌人是谁?

是”拥抱变化”四个字。

是”费用自理”四个字——不是超额自理,是从零自理,全程自理,终身自理

是绩效表上那一栏:AI 辅助产出占比——你自费买的生产力,还要按公司的标准交作业。

祥子最后扔了车,混日子,变成了他曾经最看不起的那种人。

我希望我们不会。

不是因为我们会反抗——反抗需要成本,而成本要自己掏。

而是因为,至少我们还知道自己在拉车。

知道,就比麻木强。


写于 2026 年 7 月。当 AI 能在一秒钟写出这篇文章的时候,花两个小时写它的意义是什么?也许是因为,有些愤怒只有在写的过程中才会发生。而愤怒,是祥子最后丢掉的东西里,唯一值得捡回来的。

咱们这行天天开会讨论技术,听起来挺高端,实际上跟讨论吃屎分几个步骤没啥区别——第一步开嘴,第二步把屎塞进去,第三步闭嘴。三步走完,周报上写”已完成需求开发”,屎味四溢,产能翻倍,老板点赞。

这篇献给所有把产品原型当 ER 图、把 ext_info 当万能抽屉、把 AI 当挡箭牌的同仁。也献给三年前的我自己。

一、那个下午,我对着表结构陷入了沉思

“用户管理模块的数据表设计,大家看一下。” 技术总监老 K 把投影切到 Navicat。

屏幕上是一张 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_user</font> 表,字段列表像裹脚布一样往下滚:

1
2
3
4
5
6
user_name, password, nickname, avatar_url, phone, email, gender,
birthday, province, city, district, address_detail, id_card,
real_name, wechat_openid, qq_openid, alipay_user_id,
last_login_time, last_login_ip, login_fail_count, account_locked,
locked_until, register_time, register_ip, register_channel,
invite_code, inviter_id, user_level, user_points, user_balance ......

“停停停。” 我实在忍不住了,”这表是谁设计的?”

“产品原型上有这些字段啊。” 后端主力小张理直气壮,”产品经理画的个人资料页里要展示这些信息,我们就照着建表呗。”

我深吸一口气:”那好,我问你——<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">wechat_openid</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">qq_openid</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">alipay_user_id</font> 这三个字段,有没有发现它们本质上是同一类东西?”

“呃……都是第三方登录的标识?”

“那你为什么建了三个字段,而不是一张 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_user_third_party_auth</font> 关联表?”

小张愣了愣:”产品原型上就是这么画的啊,微信登录一个框,QQ 登录一个框,支付宝登录一个框……”

“所以你就建了三个字段。将来要接入抖音登录,再加一个 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">douyin_openid</font>。要接入 Apple 登录,再加一个 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">apple_user_id</font>。这张表最终会有五十个第三方登录字段,像不像一条被塞满各种杂物的裤衩?”

会议室沉默了。

这就是第一种吃屎姿势:产品原型画成什么样,数据库表就设计成什么样。产品经理在 Axure 里拖一个输入框,后端就加一个字段;产品经理画一个 tab 页,后端就建一张表。美其名曰”快速响应需求”,实际上是把数据库当成了产品原型的像素级复刻

有没有人向上问一句:这些字段在业务上究竟是什么关系?它们是同一个实体的属性,还是应该被抽象成独立的实体?

没有人问。因为问这个问题需要抽象思维,而抽象思维需要动脑子。动脑子累,照着原型建表多轻松啊——屎来伸手,屎去张口

二、吃屎的三种专业姿势

姿势一:产品原型驱动数据库设计——把需求文档当 ER 图

“产品让我加一个收货地址,我就给订单表加 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_name</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_phone</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_province</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_city</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_district</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">receiver_address</font> 六个字段。简单粗暴!”

确实简单。一个月后,产品说一个订单要支持多个收货地址(分批发货)。你怎么办?再加六乘 N 个字段?还是把订单表拆成 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order</font> <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order_shipment</font>

后者需要抽象——你需要理解”订单”和”发货单”是两个不同的领域实体,它们之间是一对多的关系。但你没有这个意识,因为产品原型上”订单详情页”就是把收货地址嵌在里面的,你就原样搬到了数据库里。

这是一种病,学名叫**”原型依存型数据库设计综合征”**。患者临床表现包括但不限于:

  • 产品经理画了几个筛选条件,他就建几个索引,从不问这些查询场景是否真的存在
  • 产品经理说”这个字段以后可能用到”,他就加一个 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">varchar(255)</font> <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext1</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext2</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext3</font>
  • 产品经理画了一个”用户标签”的多选框,他就给用户表加一个逗号分隔的 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">tags</font> 字段,然后在查询时写 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">FIND_IN_SET</font> 自我感动

你看,数据库设计本质上是抽象建模的过程。你需要从纷繁的业务需求中提炼出核心实体、属性和关系。但很多人做的是反向建模——把 UI 上的每一个像素都映射成数据库里的一个字段,仿佛产品原型就是上帝赐予的 ER 图。

屎的形状取决于产品经理那天画原型时喝了多少咖啡。

姿势二:万物皆可 JSON——用 ext_info 字段埋葬一切建模

这是高阶姿势。初级选手才会复制粘贴 Controller,中级选手早就学会了”灵活设计”——给每张表加一个**** **<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font>** / **<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">config_json</font>** / **<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">attrs</font>** ****字段,JSON 存储,啥都往里扔

需求来了:”订单要支持发票信息。” “简单,在 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order</font> <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 里加个 JSON 字段。”

又来:”订单要支持优惠券核销记录。” “简单,<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 里再塞一个数组。”

再来:”订单要记录分销商的返佣规则。” “简单,<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 里加个嵌套对象。”

半年后,你打开线上一条订单的 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font>,里面是一坨 4KB 的 JSON,<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 里嵌套 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">rule_json</font><font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">rule_json</font> 里再嵌套 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">config_json</font>——一层套一层,像俄罗斯套娃,只不过每一层都是屎

有没有人向上问一句:发票是不是一个独立的领域实体?优惠券核销是不是一条独立的业务事实?返佣规则是不是一套独立的配置模型?

没有人问。因为建一张新表要跟 DBA 对接、要走 DDL 审批、要改十几个 DAO,而往 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 里塞一个字段只需要改一行代码。懒是第一生产力,JSON 是懒的官方赞助商

真正可怕的是这一招的”政治正确性”。当你提出”这应该拆成 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order_invoice</font> 独立建模”时,一定会有人跳出来说:”不不不,用 JSON 更灵活、更敏捷、schema-less 是现代架构趋势。”

——放屁。schema-less 不是让你放弃 schema,而是让你把 schema 放到应用层强约束地管起来。但你根本没管,你只是把数据库该做的建模工作,外包给了一个永远没人维护的 JSON 黑洞

三年后,当你需要统计”本月开了多少张专票”时,你发现要全表扫描解析 JSON。当你需要给发票表加唯一索引时,你发现根本没有发票表。当审计要求对返佣规则做版本追溯时,你发现 JSON 里只有当前值,没有历史。

你试图迁移数据——发现线上有十几种不同版本的 JSON 结构,因为每个同事塞进去的格式都不一样,而且没人知道哪些字段还在用、哪些是历史遗留。

**<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font>** ****不是一个字段,是一座垃圾填埋场。你以为你在做”灵活设计”,其实你在做向未来的自己拉屎

姿势三:状态靠布尔标志——用十个 is_xxx 干状态机的活

这是最隐蔽、最致命的一种。因为它看起来完全符合”代码清晰”的直觉

订单有个状态,怎么设计?初级程序员会加一个字段:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_paid BOOLEAN</font>

然后需求变了,要区分”是否发货”。再加一个:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_shipped BOOLEAN</font>

然后要区分”是否取消”:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_cancelled BOOLEAN</font>。 然后是退款:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_refunded BOOLEAN</font>。 然后是审核:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_reviewed BOOLEAN</font>。 然后是锁定:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_locked BOOLEAN</font>。 然后是删除:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_deleted BOOLEAN</font>(逻辑删除,你懂的)。 然后是异常:<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">has_exception BOOLEAN</font>

半年后,你的订单表有 8 个 boolean 字段。你觉得每一个都清晰明了,代码里到处写:

1
2
3
4
5
6
if(order.getIsPaid()&&!order.getIsShipped()&&!order.getIsCancelled()){
// 这是"已付款未发货"状态
}
if(order.getIsPaid()&&order.getIsShipped()&&!order.getIsRefunded()){
// 这是"已发货未退款"状态
}

停一下。

8 个 boolean 有 2 的 8 次方 = 256 种组合。其中合法的业务状态只有 12 种。剩下 244 种组合,全都是非法状态——比如 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_paid=true, is_cancelled=true, is_refunded=false</font> 这种四不像。

但你的代码里有任何地方约束”这 244 种组合不能出现”吗?

没有。

于是线上 bug 永不绝迹。用户发邮件骂:”为什么我的订单显示’已付款已取消已发货未退款’?” 你一查数据库——是的,这个订单四个标志位同时为真。

为什么会这样?因为你没有状态机,只有 8 个互相独立的开关。任何一段代码都可以在任何时刻把任何一个开关拨到任何位置,没有任何约束。你以为你在用布尔值”表达状态”,其实你在用布尔值放弃状态

有没有人向上问一句:订单本质上只有一个状态属性,它的取值是一组互斥的枚举;状态之间的流转是一张有向图。我们要设计的不是 8 个 boolean,是一个状态机。

没有人问。因为 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_paid</font> 这种字段名叫”语义清晰”,是写在面试题里的最佳实践。没人告诉你语义清晰的字段堆在一起,会变成语义爆炸

更精彩的是当产品说:”新增一种’部分发货’状态。” 你慌了:现在要 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_partially_shipped</font> 还是 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">shipped_ratio</font>?和 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_shipped</font> 什么关系?历史数据怎么回刷?代码里所有 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">if (is_shipped)</font> 的地方都要加上”或者 is_partially_shipped”——你打开 IDE 全局搜索,1200 个结果。

你不是在加一个状态,你是在给一座 8 阶屎山再加一层。

这就是没有状态机抽象的代价。你今天省下的那 30 分钟建模时间,将来会以每月两次 P2 故障的频率还回来。

三、屎的七十二变:那些你天天在写的”设计”

上面三个姿势只是基本功。真到工位上,屎有七十二变:

屎变 #1:多张结构完全相同的表

你见过这样的数据库吗?

  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order_202401</font>
  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order_202402</font>
  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_order_202403</font>

按月分表,听起来挺专业的对吧?但当你发现每一张表的字段完全一样,只是表名后缀不同,而查询时需要动态拼接表名——你就知道这是一泡分装在不同罐子里的屎。为什么不设计一张分区表?为什么不用中间件?因为”按月分表”是你在某篇博客上看到的,你觉得很酷,就照做了。

屎变 #2:魔术数字漫山遍野,枚举值散落各处

if(order.getStatus()==1){}// OrderServiceImpl

if(status.equals(1)){}// OrderController

WHERE status IN(1,2,3)ANDtype=5// mapper.xml

const STATUS_PAID =1;// 前端 constants.js

同一个”已支付”在代码里有四种写法,在数据库里有一份,在需求文档里还有一句”支付成功的订单”。当产品说”把’已支付’拆成’已支付-待风控’和’已支付-已放行’”时,你需要改多少地方?没人知道,因为没人敢 grep**** **<font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">== 1</font>**——会出来一万个结果

抽象的正解是什么?一个枚举类、一张字典表、一套类型安全的转换器。但这需要你把”1”当成一个概念而不是一个整数——这一步动作,大部分人懒得做。

屎变 #3:参数校验逻辑散落在 30 个地方

同一个手机号格式校验,你可以在:

  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">UserController.add()</font> 里找到一份
  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">UserController.update()</font> 里找到一份
  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">OrderController.create()</font> 里找到一份
  • <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">RegisterService.validate()</font> 里找到一份
  • 前端的 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">utils.js</font> 里还有一份

当工信部要求手机号支持 17 位物联网卡时,你需要改多少地方?没人知道,因为没人记得全。这就是没有抽象的后果——同一个规则被复制了无数次,每一次复制都是在制造未来的技术债。

四、AI Coding 时代:屎山制造进入工业 4.0

好了,前面说的都是”人手造屎”的时代。现在,咱们进入了 AI Coding 时代。

情况变好了吗?

变得更糟了。

GitClear 的研究数据显示,AI 生成代码的重复率是人工代码的 8 倍,技术债增加 32.45 issues/KLOC。Sonar 的报告更触目惊心:AI 生成的代码中,90% 存在代码异味

这不是 AI 的错。AI 只是一个放大器。

你给它一个”在订单表加个发票信息”的 prompt,它很贴心地给你生成 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ALTER TABLE t_order ADD COLUMN invoice_info JSON</font>。你很满意,点下 Accept。

你又给它一个”再加个订单是否需要人工审核的标志”的 prompt,它又很贴心地给你 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ADD COLUMN need_manual_review BOOLEAN</font>。你又 Accept。

你连脑子都没过过——因为你自己就没有抽象意识,你根本不知道发票应该独立建模,状态应该抽成状态机。AI 忠实地执行了你的低水平指令,帮你把屎的生产效率提升了 125%。

你从一个人造屎,变成了一个人指挥 AI 造屎。生产效率飞跃,屎的质量没变,只是产量翻倍了。

这还不是最可怕的。最可怕的是 Vibe Coding——这个概念由 OpenAI 联合创始人 Andrej Karpathy 在 2025 年提出,核心理念是”完全跟着感觉走,拥抱指数增长,忘掉代码本身的存在”。

翻译成人话就是:不知道怎么写的代码,就让 AI 帮你写。看不懂也没关系,能跑就行。

GitClear 的研究显示,在 AI 编码助手普及后,提交历史中的重构活动在减少。微软的研究则指出,AI 驱动的信心往往以牺牲批判性思维为代价——开发者对 AI 生成代码的盲目信任,正在侵蚀他们原本就脆弱的抽象能力。

这是一个完美的恶性循环:

  1. 你没有抽象思维,不知道什么是好的设计
  2. 你用 AI 帮你生成代码,AI 也抽象不出好的设计(因为它只会从训练数据中找最相似的模式拼凑)
  3. AI 生成的屎比你手写的屎更隐蔽——它看起来工整、有注释、甚至还有单元测试
  4. 你欣然接受,觉得自己生产力爆棚
  5. 屎山以 8 倍的速度 增长
  6. 当你终于意识到问题时,屎山已经高到你不敢重构——因为 AI 生成的代码你根本没仔细看过,你不知道动哪里会塌方

AI 没有帮你解决问题,AI 只是帮你更快地到达问题。

ThoughtWorks 的技术雷达已经将”自满于 AI 生成的代码”列为暂缓采用的技术实践。而那些真正有抽象能力的工程师,正在用 AI 辅助重构,让 AI 帮他们把屎山改造成可维护的系统——前提是,他们自己知道”好”长什么样。

而对于没有抽象思维的你,AI 就是一台屎山 3D 打印机。以前你一天造一坨屎,现在你一天能打印十坨。

五、向上抽象,就是问一句”这他妈到底是啥?”

所谓向上抽象,就是逼着自己从 Ctrl+V 的手指运动中停下来,问几个该死的问题:

沉迷吃屎的日常 向上抽象的提问
产品原型有这个字段,加! 这个字段是什么实体?和现有实体是什么关系?
塞进 ext_info JSON 里就行了! 这是一条业务事实,还是一个独立实体?它需不需要被查询、被索引、被审计?
再加一个 is_xxx 标志位! 这是一个独立属性,还是状态机的一个节点?整体状态空间有多少种合法组合?
按月建表,简单! 分区和分表的区别是什么?我真的需要分表吗?
代码里到处 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">status == 1</font> “1”代表什么概念?这个概念的完整取值集合是什么?
AI 帮我写好了,Accept! 这段代码在整体架构中的位置是什么?它和已有代码是否重复?

问完这几个问题,你可能会发现——你准备造的屎,其实根本就不用造

有一次,团队要做一个”优惠券发放”功能。产品经理画了满屏的原型:新人券、满减券、折扣券、兑换券、限时券、分享券……

后端老李熟练地打开 Navicat,准备建六张表。

我问了一句:”这六种券的本质是什么?”

“优惠券啊。”

“那它们的核心字段是不是都一样?名称、面值、使用门槛、有效期、适用商品范围。不同的只是类型和某些特定的规则。”

“所以?”

“所以我们建一张 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">t_coupon</font> 表,用一个 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">type</font> 字段区分类型,特定规则用策略模式处理发放/核销逻辑,类型本身是受控枚举,不是字符串。”

老李愣了愣,默默关掉了新建表的窗口。

后来这个系统支持了 20 多种券类型,核心表只有 3 张。

原来这些屎,根本不用分开拉。

六、结语:AI 时代,抽象能力是最后的护城河

我写这篇文章,不是为了教你设计模式——那是另一本书的事。我只想留下几个问题,供你在下一次想按 Ctrl+V 的时候思考:

  1. 这段代码和已有的某段代码,相似度超过 80% 吗? 如果是,你应该抽象,而不是复制。
  2. 这个字段和其他几个字段,本质上是同一类东西吗? 如果是,它们应该被建模成关联实体,而不是在一张表里无限追加,也不是塞进一个 JSON 黑洞。
  3. AI 生成的这段代码,你真的理解它在整体设计中的位置吗? 如果不理解,你就不是在编程,你是在给屎山添砖加瓦。

在 AI 能把任何 prompt 变成代码的今天,写出能跑的代码已经是最不值钱的能力。AI 写得比你快、比你多、比你规范。但它无法替你做一件事——判断什么是一个好的设计

因为”好”不是一个能从训练数据中统计出来的概率,它是一种需要抽象思维才能把握的判断。

而没有抽象思维的人,在 AI 时代会变成什么?

会变成屎山车间的车间主任——管理着一群不知疲倦的 AI 机器人,24 小时不间断地生产形状各异的屎,产量惊人,品控为零。

所以,在你下一次打开 Cursor 之前,在你下一次按 Tab 补全之前,在你下一次说”能跑就行”之前——

抬头看一眼:你是在造系统,还是在吃屎。

开嘴之前多想半秒,这半秒就是你跟那群 24 小时不停嘴的同行之间,唯一的区别。

而这半秒钟,正在成为 AI 时代程序员最后的护城河。


附录:如果你觉得这篇文章是屎

是的,这篇文章是 AI 辅助写的。

我知道你在想什么。你可能从第一段就开始警惕了——“嗯,这种排比句、这种金句密度、这种情绪浓度……AI 味儿。” 你可能已经在心里给它贴好了标签:逻辑混乱、以偏概全、满嘴喷粪

很好。请你继续这么想。因为你正在用一种非常纯粹的方式,亲身验证本文唯一的论点

请跟上我的逻辑:

你读完这篇文章,做的第一件事不是逐句核对每一个论据的严密性,不是去翻 GitClear 的原始报告,不是去查 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 反模式是否真的导致了你项目里那次 P0。

你做的第一件事是——向上抽象。你把两万字压缩成三个字:”这是屎”。你甚至没意识到,你刚刚完成的,是本文通篇苦口婆心在教你做的那件事:从混乱的细节里提炼本质

区别只在于:你在批判一篇文章的时候,本能地会向上抽象;你在写代码的时候,本能地不会

你读文章时会问”作者的核心论点是什么”,写代码时却不问”这个字段的核心实体是什么”。 你读文章时会质疑”这段论据和结论的关系”,写代码时却不质疑”这个 boolean 和上一个 boolean 的关系”。 你读文章时能一眼看穿”这是 AI 拼凑的”,打开 IDE 却看不穿”这是 AI 拼凑的屎山”。

为什么?

因为骂一篇文章不用负责,维护一座屎山要你加班。你的大脑只在对你无利害的场合启用抽象能力,在对你有利害的场合——比如你需要交付、需要写周报、需要下班——它自动熄火。你不是没有抽象能力,你是把抽象能力精准地调度到了最没用的地方

现在让我把退路一条一条堵上:

  • 你说这篇文章是屎? → 你已经启动了抽象思维 → 但你只在批判别人时启动 → 你写的代码依然是屎。
  • 你说这篇文章是金句之作? → 你认同观点 → 但你明天照样 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 塞一切 → 你写的代码依然是屎。
  • 你说这篇文章一般般? → 你连抽象都懒得做 → 你写的代码 100% 是屎。
  • 你说”这是 AI 写的,不算数”? → 你把对象从论点偷换成了作者 → 你用这种偷换逻辑工作了多少年 → 你写的代码依然是屎。
  • 你说”我跟这文章里写的不一样”? → 请打开你的订单表看一眼 → 数一数 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">ext_info</font> 多少字段 <font style="color:rgb(34, 34, 34);background-color:rgb(248, 248, 248);">is_xxx</font> 多少标志位 → 谢谢配合。

无论你怎么选,结论只有一个。

这不是诡辩。这是你自己的行为在不同维度上的投影。你可以骂我逻辑强奸——但逻辑从来强奸不了人,是你自己的代码先把自己钉在了屎山上,我只是指了指钉子

至于这篇文章是不是 AI 写的——无所谓。

如果你是因为”AI 写的”所以觉得是屎,那你反对的是作者身份,不是论点本身,你就是本文骂的那种”放弃抽象、放弃批判性阅读”的人。你恰好证明了:当一个人把”信息来源”当成”信息价值”的全部判断标准时,他已经失去了向上抽象的能力

如果你是因为”内容不对”所以觉得是屎,那请具体指出哪一个论点错了——不是”感觉”错了,是哪一条结论在你真实的工程实践里不成立。如果你说得出来,这篇文章就该被改;如果你说不出来,那你刚才的”屎”就是一种情绪,而情绪无法反驳论证。

你当然可以关掉这篇文章,骂一句”狗屁不通”,然后继续你的日常。AI 能帮你把下一段文字写得更流畅,也能帮你把下一坨屎写得更整齐。它唯一帮不了你的是——

在你按下 Tab 补全之前,替你问一句”这他妈到底是啥?”

那一句,只能你自己问。问出来,你就是系统设计者;问不出来——

打开你的 IDE 看一眼。

那坨东西,是你写的,还是 AI 替你拉的?

区别重要吗?

不重要。因为反正都是你的名字挂在 Git blame 上。

所以,现在,认真回答最后一个问题:

你写的下一行代码,是屎,还是系统?

别回答我。

你答不答都已经被这篇文章判过刑了——因为我不回答问题,我本身就是一个答案。

“Talk is cheap, show me the code.” —— 2000 年 Linus 的这句话,到 2026 年该翻篇了。 现在的现状是:“Code is cheap, show me the prompt.” 当下,AI 已经成了烂大街的词。在软件开发里,大家用 AI 的姿势逐渐分化成了两个极端:一种叫“许愿式编程”(Wishful Programming),只要嘴皮子一动,就指望 AI 把整个数字城堡盖起来;另一种叫“奴隶式编程”(Slave-driven Programming),把 AI 当成没有灵魂的搬砖工,一行接口、一个类地去死抠。

尤其是在国内电力行业这种重度依赖稳定性、充斥着各种私有协议(104、Modbus、IEC 61850)、业务逻辑极其复杂的 IT 开发场景下,这两种方式到底撞出了怎样的火花?结合网友们被 AI 坑过又爽过的血泪体验,咱们今天就来扯扯这两种方式的底层差异,以及在实际工程中,我们到底该怎么把 AI“往死里用”,而不是被它带进沟里。

一、 概念解构:两只电子屎壳郎的殊途同归

先别觉得这两个词高端,撕开科技的遮羞布,在日常 CRUD 的工作里,这两种模式的本质其实很好理解:

1. 许愿式编程:大局观拉满,细节全靠“猜”

  • 典型行为:“我需要一个类似虚拟电厂(VPP)的现货交易收益计算系统,要有日前报价、日内调度和零售考核功能,界面要科技感,左边大屏、右边图表,立刻给我生成。”
  • 本质试图用直觉代替逻辑,用宏观描述跨越工程鸿沟。 许愿者通常扮演“懂王”角色,把 AI 当成一个全知全能的架构总监兼全栈开发。
  • 网友体验:爽是真的爽,尤其是第一周。三个人两周就能拉起一个高大上的内部管理系统演示。但三个月后改需求时,所有人都想死。接口莫名其妙报错,数据流向没人能看懂。局部极其优秀,全局极其稀烂

2. 奴隶式编程:局部严防死守,全局“听天由命”

  • 典型行为:“现在生成一个 Spring Boot 框架下的 KafkaConsumer 类,实现 MessageListener 接口,要求和 MongoIndexServiceupsert 接口对接,处理电网频率秒级采样数据,把异常丢入死信队列。”
  • 本质把约束前置,把 AI 圈死在一条设定好的跑道里。 开发者不相信 AI 的大局观,只榨取它在特定代码块上的超高产能。
  • 网友体验:写代码的效率暴涨了 100 倍,以前一天写 2 个接口,现在一天能按 Tab 键按出 20 个类。但如果开发者缺乏向上抽象的能力,这不过是加速了电子垃圾的生产。原本你一个人一天写 3 个 bug,现在 AI 帮你一天写 30 个 bug,还附带 75% 的逻辑漏洞。

二、 差异对撞:当两种模式遇上国内电力 IT 场景

如果把这两种模式放到互联网行业,大不了就是服务器雪崩、连夜重启挂机。但要是放到国内的电力、能源行业 IT 开发场景,这两种方式的差异和弊端,就会被数十倍地放大。

电力系统(不管是调度平台、电网资产管理,还是现在火热的储能优化策略、碳资产管理平台)有着它极其恶劣的客观现实:

  1. 强物理约束与弱容错性:一行代码的计算逻辑错了,现货交易少算几个点,企业可能直接亏损几百万;控制指令发错,那不是重启能解决的。
  2. 新旧框架的断层:既要用最新的微服务(如 BladeX)去堆业务,又要用最传统的国产化数据库(如瀚高 HighGo)去做迁移适配;既有最新的 AI 负荷预测算法(SmileGBT、Ridge),又要去兼容十几年前电力仪表的古董私有协议。

在这个背景下,我们来看看两者的死法和活路:

1. 许愿式编程在电力场景:“连环异常堆栈案”的制造者

在电力行业玩许愿式编程,无异于让 AI 在雷区里裸奔。

  • 致命伤AI 缺乏行业“潜规则”和长程上下文。
  • 场景还原:你让 AI 写一个“多源预测的储能优化调度策略”。AI 依靠网上的公开资料,噼里啪啦给你用 Python 写了一堆看似完美的线性规划代码。
  • 结果:一上线直接抓瞎。AI 根本不知道某些特定节点在实际运行中会有 EXT4-fs 文件系统错误导致的硬件掉线;它不知道某些变压器的最大容量限制不是写在文档里,而是写在“贺总”昨天刚刚更新的 Confluence 备忘录里。它生成的代码没有容错、没有重试机制,只要一个 104 规约的报文长度稍微对不上,整个多线程线程池直接锁死、雪崩。
  • 结论:在涉及核心电网业务时,纯粹的许愿式编程,下场就是“能力差 × AI = 灾难”。

2. 奴隶式编程在电力场景:把屎山堆得更整齐的“砖瓦匠”

相比之下,奴隶式编程在电力 IT 开发中更常见,大家天天开会讨论技术,实际上就是把产品原型当 ER 图,把 ext_info 当万能抽屉,让 AI 拼命塞代码。

  • 致命伤缺乏向上抽象的思维。
  • 场景还原:要把 10 个 Java 应用、200 张数据表从 MySQL 迁移到瀚高(HighGo)数据库。开发者开启奴隶模式,让 AI 一个类一个类地去改 SQL、改多租户隔离、改字段类型。
  • 结果:AI 极其听话,让你点 Tab 点到手指发麻。然而,由于你没有在全局做间接层设计(Indirection)或多态处理(Polymorphism),200 张表的迁移变成了 200 次机械的复制粘贴。一旦高层架构没对齐,到了联调阶段,你会发现各个模块的风格完全不统一,有的走 JPA,有的走 MyBatis-Plus,承重墙直接错位。
  • 结论:奴隶式编程如果没有高阶的架构设计引路,你唯一得到的,就是在按下 Tab 补全之前,从来不问一句“这他妈到底是啥”,最终把自己活成了 AI 的外包。

三、 终极指南:AI 时代,电力 IT 人员的自救与进化

2026 年了,顶级大模型的上下文已经干到了 1000 万(10M)级别。全本《三国演义》丢进去都不够它塞牙缝的,这意味着 AI 正在经历“上下文内存化”的质变。它缺的不是代码产能,它缺的是决策和约束。

作为这个行业里的系统架构师或核心开发,如何指导团队把 AI 用到刀刃上?我们要把“许愿”的全局思维和“奴隶”的严谨约束结合起来,建立一套新型的开发方法论。

1. 短期策略:把架构约束前置,别让 AI 裸奔

  • 先写约束,再让 AI 写局部代码:不要直接让 AI 去猜接口。先用 GRASP 模式(信息专家、低耦合、高内聚)定义好你的类型契约、依赖关系和架构规则。这就像是给 AI 划定好跑道。跑道建好了,你再用“奴隶式编程”去压榨它的局部产出。
  • 把行业经验与“潜规则”知识库化:团队里老人踩过的坑(比如某款电表高低位字节颠倒、某些 MongoDB 索引在特定查询下的性能瓶颈),别只留在脑子里。把它们整理成 Markdown 丢给 AI 当成 RAG(检索增强生成)的上下文。这比培训十个新人有效得多。
  • 测试前置与审查加严:代码库的膨胀速度现在翻了十倍,审查标准就必须跟着升级。让 AI 编写代码前,先让它(或另一个模型)生成详尽的边缘条件测试用例(比如:负荷预测数据为空怎么处理?电价突变为负数怎么处理?)。

2. 长期修行:保持向上抽象的能力,跳出“吃屎”循环

  • 从砖瓦匠到城堡设计师:直觉来看,编写代码如同砌砖。但工程师思维告诉你,数据流如同血脉,系统架构才是骨骼。AI 能够帮你写任何局部代码,但它暂时无法帮你做出任何一个需要理解人际关系、行业潜规则、长期可维护性的全局决策。
  • 多问一句“为什么”:在生产环境中排查过雪崩问题的人都知道,很多时候证据是不可信的。我们要找出系统正在执行“什么”,还要询问系统“为什么”执行。当你依赖 AI 生成某段现货交易的计算逻辑时,在按下 Tab 补全前,必须盯着那段代码问一句:“这个地方的边界条件真的对吗?网上的技术文章和 AI 生成的逻辑,真的符合我们这个省份的电力交易规则吗?”

结语

设计模式和架构模式,说白了就是解决问题的方法论。前人踩了无数的坑,总结出了应对方法,不是为了让你在面试时装逼,而是为了在面对庞大、混乱的生产系统时,能成倍地提升系统的可维护性和可靠性。

鲁迅(可能没)说过:“给我一个合理的架构,我能构建出横跨星辰的系统。”

在 AI 时代,代码变得前所未有的廉价,而向上抽象的思维、对业务全局的掌控、以及给 AI 划定边界的能力,才是最贵的硬通货。别当只会许愿的幻想家,也别当被 Tab 键奴役的电子屎壳郎。把约束握在手里,把思考留给自己,把脏活累活扔给 AI,这才是 2026 年最体面的编程姿势。

hostName

1
2
3
4
5
hostnamectl set-hostname $newname
#修改或添加/etc/hosts
127.0.0.1 $newname
#修改或添加/etc/sysconfig/network
HOSTNAME=$newname

字符集 & 语言

1
2
3
4
#修改 /etc/locale.conf
LANG="zh_CN.UTF-8"
SUPPORTED="en_US.UTF-8"
SYSFONT="latarcyrheb-sun16"

时区

1
timedatectl set-timezone Asia/Shanghai

NTP(时间同步)

1
2
3
#如果通外网使用公共的ntp,如果不通建议在内部安装一个ntp服务
ntpdate ntp.aliyun.com
echo '*/5 * * * * /usr/sbin/ntpdate ntp.aliyun.com > /dev/null 2>&1' >> /var/spool/cron/root

SSH配置

1
2
3
4
#修改/etc/ssh/sshd_config
#关闭UseDNS和GSSAPIAuthentication
UseDNS no
GSSAPIAuthentication no

禁用ctrl+alt+del重启

1
rm -rf /usr/lib/systemd/system/ctrl-alt-del.target

关防火墙

1
2
systemctl stop firewalld.service
systemctl disable firewalld.service

关非必要服务

1
2
3
4
5
6
7
8
9
10
systemctl stop irqbalance.service
systemctl disable irqbalance.service
systemctl status irqbalance.service

systemctl stop postfix.service
systemctl disable postfix.service
systemctl status postfix.service

sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0

内核优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#修改 /etc/sysctl.conf

#关闭ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
#决定检查过期多久邻居条目
net.ipv4.neigh.default.gc_stale_time=120
#使用arp_announce / arp_ignore解决ARP映射问题
net.ipv4.conf.default.arp_announce = 2
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.lo.arp_announce=2
# 避免放大攻击
net.ipv4.icmp_echo_ignore_broadcasts = 1
# 开启恶意icmp错误消息保护
net.ipv4.icmp_ignore_bogus_error_responses = 1
#关闭路由转发
net.ipv4.ip_forward = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
#开启反向路径过滤
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
#处理无源路由的包
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
#关闭sysrq功能
kernel.sysrq = 0
#core文件名中添加pid作为扩展名
kernel.core_uses_pid = 1
# 开启SYN洪水攻击保护
net.ipv4.tcp_syncookies = 1
#修改消息队列长度
kernel.msgmnb = 65536
kernel.msgmax = 65536
#设置最大内存共享段大小bytes
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
#timewait的数量,默认180000
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
#socket监听队列上线监听队列
net.core.somaxconn = 262144
#每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog = 262144
#限制仅仅是为了防止简单的DoS 攻击
net.ipv4.tcp_max_orphans = 3276800
#未收到客户端确认信息的连接请求的最大值
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_timestamps = 0
#内核放弃建立连接之前发送SYNACK 包的数量
net.ipv4.tcp_synack_retries = 1
#内核放弃建立连接之前发送SYN 包的数量
net.ipv4.tcp_syn_retries = 1
#启用timewait 快速回收
net.ipv4.tcp_tw_recycle = 0
#开启重用。允许将TIME-WAIT sockets 重新用于新的TCP 连接
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_fin_timeout = 1
#当keepalive 起用的时候,TCP 发送keepalive 消息的频度。缺省是2 小时
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_intvl = 15
#允许系统打开的端口范围
net.ipv4.ip_local_port_range = 1024 65000
#修改防火墙表大小,默认65536
net.netfilter.nf_conntrack_max=655350
net.netfilter.nf_conntrack_tcp_timeout_established=1200
# 确保无人能修改路由表
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0

#修改生效
/sbin/sysctl -p

yum更新与安装必要工具

1
2
3
4
5
#可连通外网的情况下
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum clean all
yum makecache
yum -y install lsof lrzsz vim lrzsz tree nmap nc sysstat man wget ntp gcc gcc-c++

按需配置

磁盘调度算法调整

一定不要使用默认的CFQ算法.SSD用noop/none,普通磁盘用Deadline

1
2
3
4
#设置
cat /sys/block/sda/queue/scheduler
echo noop>/sys/block/sda/queue/scheduler
cat /sys/block/sda/queue/scheduler

BBR算法开启

1
2
3
#开启BBR,需要内核4.9以上
echo 'net.core.default_qdisc=fq' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_congestion_control=bbr' >> /etc/sysctl.conf

swappiness设置

1
2
3
#编辑 /etc/sysctl.conf
#内存有足够空余,设置为0,不够的话按需来吧
#vm.swappiness = 10

关闭图形化界面

1
2
systemctl set-default multi-user.target
systemctl get-default

一键脚本

centos7一键脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#!/bin/bash

platform=`uname -i`

if [ $platform != 'x86_64' ]; then
echo "this script is only for 64bit Opetating System!"
exit 1
fi

echo "the platform is ok"

cat << EOF
+---------------------------------------+
| your system is CentOS 7 x86_64 |
| start optimizing....... |
+---------------------------------------+
EOF

#IP=$(ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' |head -1)
IP='127.0.0.1'
oldname=$(cat /etc/sysconfig/network | awk -F "=" '{print $2}' | sed -n 2p)
read -r -p "是否要修改主机名? [y/n]" input

case $input in
y)
read -r -p "请输入主机名:" newname
hostnamectl set-hostname $newname
sed -i '/${oldname}/d' /etc/hosts
echo "$IP `hostname`" >> /etc/hosts
sed -i '/HOSTNAME/d' /etc/sysconfig/network
echo "HOSTNAME=$newname" >> /etc/sysconfig/network
echo $oldname "改为" $newname
;;
n)
echo "当前hostname =" $oldname
;;
esac
echo "==========================================================="

echo "设置字符集 ..."
cat > /etc/locale.conf <<EOF
LANG="zh_CN.UTF-8"
SUPPORTED="en_US.UTF-8"
SYSFONT="latarcyrheb-sun16"
EOF
source /etc/locale.conf
echo -n "#cat /etc/locale.conf"
cat /etc/locale.conf
echo "==========================================================="

echo "修改时区"
timedatectl set-timezone Asia/Shanghai
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
timedatectl |grep zone
echo "==========================================================="

service crond restart &> /dev/null
read -r -p "是否自定义NTP服务? 默认: ntp.aliyun.com [y/n]" input
case $input in
y)
read -r -p "请输入NTP:" newntp
ntpdate ${newntp}
echo '*/5 * * * * /usr/sbin/ntpdate' $newntp '> /dev/null 2>&1' >> /var/spool/cron/root
;;
n)
ntpdate ntp.aliyun.com
echo '*/5 * * * * /usr/sbin/ntpdate ntp.aliyun.com > /dev/null 2>&1' >> /var/spool/cron/root
;;
esac
echo "==========================================================="

echo "ssh配置"
sed -i '/#UseDNS.*/s@@UseDNS no@' /etc/ssh/sshd_config
sed -i '/^GSSAPIAuthentication.*/s@\(GSSAPIAuthentication.*\)@#\1@' /etc/ssh/sshd_config
service sshd restart &> /dev/null
echo "==========================================================="

echo "禁用ctrl+alt+del重启"
rm -rf /usr/lib/systemd/system/ctrl-alt-del.target
echo "==========================================================="

echo "关防火墙"
systemctl stop firewalld.service &> /dev/null
firewall-cmd --state
systemctl disable firewalld.service &> /dev/null
systemctl list-unit-files | grep firewalld
echo "==========================================================="

echo "history优化"
chk_his=`cat /etc/profile | grep HISTTIMEFORMAT |wc -l`
if [ $chk_his -eq 0 ];then
cat >> /etc/profile <<'EOF'
#设置history格式
export HISTTIMEFORMAT="[%Y-%m-%d %H:%M:%S] [`whoami`] [`who am i|awk '{print $NF}'|sed -r 's#[()]##g'`]: "
#记录shell执行的每一条命令
export PROMPT_COMMAND='\
if [ -z "$OLD_PWD" ];then
export OLD_PWD=$PWD;
fi;
if [ ! -z "$LAST_CMD" ] && [ "$(history 1)" != "$LAST_CMD" ]; then
logger -t `whoami`_shell_dir "[$OLD_PWD]$(history 1)";
fi;
export LAST_CMD="$(history 1)";
export OLD_PWD=$PWD;'
EOF
source /etc/profile
else
echo "优化项已存在。"
fi
echo "==========================================================="

echo "打开文件句柄数"
echo "ulimit -SHn 65535" >> /etc/rc.local
cat >> /etc/security/limits.conf << EOF
* soft nofile 65535
* hard nofile 65535
EOF
echo "==========================================================="

echo "关闭 selinux"
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
setenforce 0
echo "==========================================================="

echo "关闭非必要服务"
systemctl stop irqbalance.service
systemctl disable irqbalance.service
#systemctl status irqbalance.service

systemctl stop postfix.service
systemctl disable postfix.service
#systemctl status postfix.service
echo "==========================================================="

echo "关闭图形化界面"
systemctl set-default multi-user.target
systemctl get-default
echo "==========================================================="

echo "设置backspace为删除"
echo 'stty erase ^H' >> /etc/profile
echo "syntax on" >> /root/.vimrc
echo "==========================================================="

echo "内核优化"
cat >> /etc/sysctl.conf << EOF
#关闭ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
#决定检查过期多久邻居条目
net.ipv4.neigh.default.gc_stale_time=120
#使用arp_announce / arp_ignore解决ARP映射问题
net.ipv4.conf.default.arp_announce = 2
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.lo.arp_announce=2
# 避免放大攻击
net.ipv4.icmp_echo_ignore_broadcasts = 1
# 开启恶意icmp错误消息保护
net.ipv4.icmp_ignore_bogus_error_responses = 1
#关闭路由转发
net.ipv4.ip_forward = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
#开启反向路径过滤
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
#处理无源路由的包
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
#关闭sysrq功能
kernel.sysrq = 0
#core文件名中添加pid作为扩展名
kernel.core_uses_pid = 1
# 开启SYN洪水攻击保护
net.ipv4.tcp_syncookies = 1
#修改消息队列长度
kernel.msgmnb = 65536
kernel.msgmax = 65536
#设置最大内存共享段大小bytes
kernel.shmmax = 68719476736
kernel.shmall = 4294967296
#timewait的数量,默认180000
net.ipv4.tcp_max_tw_buckets = 6000
net.ipv4.tcp_sack = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 16384 4194304
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
#socket监听队列上线监听队列
net.core.somaxconn = 262144
#每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog = 262144
#限制仅仅是为了防止简单的DoS 攻击
net.ipv4.tcp_max_orphans = 3276800
#未收到客户端确认信息的连接请求的最大值
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_timestamps = 0
#内核放弃建立连接之前发送SYNACK 包的数量
net.ipv4.tcp_synack_retries = 1
#内核放弃建立连接之前发送SYN 包的数量
net.ipv4.tcp_syn_retries = 1
#启用timewait 快速回收
net.ipv4.tcp_tw_recycle = 0
#开启重用。允许将TIME-WAIT sockets 重新用于新的TCP 连接
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_fin_timeout = 1
#当keepalive 起用的时候,TCP 发送keepalive 消息的频度。缺省是2 小时
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_intvl = 15
#允许系统打开的端口范围
net.ipv4.ip_local_port_range = 1024 65000
#修改防火墙表大小,默认65536
net.netfilter.nf_conntrack_max=655350
net.netfilter.nf_conntrack_tcp_timeout_established=1200
# 确保无人能修改路由表
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
EOF

/sbin/sysctl -p &> /dev/null
echo "==========================================================="

echo "关闭IPV6"
cat > /etc/modprobe.d/ipv6.conf << EOF
alias net-pf-10 off
options ipv6 disable=1
EOF
echo "NETWORKING_IPV6=off" >> /etc/sysconfig/network
echo "==========================================================="

echo "yum更新阿里源与安装必要工具"
cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.$(date +%F)
ping -c 1 mirrors.aliyun.com &> /dev/null
if [ $? -eq 0 ];then
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo &> /dev/null
yum clean all &> /dev/null
yum makecache &> /dev/null
yum -y install lsof lrzsz vim lrzsz tree nmap nc sysstat man wget ntp gcc gcc-c++ &> /dev/null
else
echo "无法连接外网 放弃yum更新"
fi
echo "==========================================================="

cat << EOF
+-------------------------------------------------+
| optimizer is done |
| it's recommond to restart this server ! |
+-------------------------------------------------+
EOF

centos7升级内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install kernel-ml -y

#-----------------------------------------------------------
#内网的话手动下载对应的版本
#https://elrepo.org/linux/kernel/el7/x86_64/RPMS/
#这里选择ml(最新)版本,也可以选择lt(长期支持版)
wget https://elrepo.org/linux/kernel/el7/x86_64/RPMS/kernel-ml-5.9.12-1.el7.elrepo.x86_64.rpm
安装
rpm -ivh kernel-ml-5.9.12-1.el7.elrepo.x86_64.rpm
grub2-set-default 0 && shutdown -r now
#查看
uname -r

Ubuntu20 一键脚本(没注释版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/bash
platform=`lsb_release -r --short`

if [[ $platform != '20.04' ]]; then
echo "this script is only for Ubuntu 20.04 LTS!"
exit 1
fi
echo "System Check PASS!"


cat << EOF
+---------------------------------------+
| Your System is Ubuntu 20.04 LTS |
| start optimizing....... |
+---------------------------------------
EOF

echo -n "Installing some useful Packages ..."
apt-get update -y
apt-get upgrade -y
apt-get install -y conntrack ipvsadm ipset jq iptables curl sysstat lrzsz apt-transport-https ca-certificates software-properties-common gnupg-agent net-tools
echo "Done."

echo -n "Installing docker-ce Packages"
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
apt-get update -y
apt-get remove -y docker docker-engine docker.io containerd runc
apt-get install -y docker-ce
sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cgroup_enable=memory swapaccount=1\"/g' /etc/default/grub
update-grub
echo "Done."

echo -n "Adding docker-ce Configuretion ..."
cat >> /etc/docker/daemon.json << EOF
{
"registry-mirrors": [
"https://docker.mirrors.ustc.edu.cn",
"https://dockerhub.mirrors.nwafu.edu.cn",
"https://hub-mirror.c.163.com"
],
"dns": [
"114.114.114.114"
],
"max-concurrent-downloads": 5,
"max-concurrent-uploads": 5,
"live-restore": true
}
EOF
echo "Done."

#echo -n "Adding docker images hosts ..."
#cat >> /etc/hosts << EOF
#10.0.0.178 images.qlteacher.com
#EOF

echo -n "Adjusting timezone to Asia Shanghai ..."
/usr/bin/timedatectl set-timezone "Asia/Shanghai"
/usr/bin/timedatectl set-ntp true
echo "Done."


echo -n "Adding ulimits Configuretion ..."
echo "ulimit -HSn 102400"|sudo tee -a /etc/profile
cat >> /etc/security/limits.conf << EOF
* soft nofile 102400
* hard nofile 102400
root soft nofile 102400
root hard nofile 102400
EOF

echo "Done."


echo -n "Adding sysctl Configuretion ..."

cat >> /etc/sysctl.conf << EOF
#Net Optimazation
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.ip_forward = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_low_latency = 1
net.ipv4.tcp_max_tw_buckets = 262144
net.ipv4.tcp_max_orphans = 327680
net.ipv4.tcp_max_syn_backlog = 819200
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.tcp_keepalive_time = 1200
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_mem = 94500000 915000000 927000000
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 16384 16777216
net.ipv4.tcp_thin_linear_timeouts = 1
net.core.somaxconn = 65536
net.core.rmem_default = 8388608
net.core.wmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.unix.max_dgram_qlen = 30000
net.netfilter.nf_conntrack_max = 1048576
net.ipv4.neigh.default.gc_thresh1 = 10240
net.ipv4.neigh.default.gc_thresh2 = 40960
net.ipv4.neigh.default.gc_thresh3 = 61440
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-arptables = 1
net.core.netdev_max_backlog = 16384

#Kernel Optimazation
kernel.panic = 1
kernel.core_pattern = core_%e

#VM Optimazation
vm.panic_on_oom = 0
vm.min_free_kbytes = 512000
vm.swappiness = 20

#FS Optimazation
fs.file-max = 52706963
fs.nr_open = 52706963
fs.inotify.max_user_instances = 8192
fs.inotify.max_user_watches = 1048576
fs.aio-max-nr = 1048576

#HUGE PAGES
vm.nr_hugepages=128

#BBR
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
EOF

/sbin/sysctl -p &> /dev/null
echo "Done."

cat << EOF
+-------------------------------------------------+
| This Server has been Optimizd |
| Please restart this server ! |
+-------------------------------------------------+
EOF

rewrite /last.html /index.html last;

访问 /break.html 的时候,页面内容重写到 /index.html 中,并停止后续的匹配

rewrite /break.html /index.html break;

访问 /redirect.html 的时候,页面直接302定向到 /index.html中

rewrite /redirect.html /index.html redirect;

访问 /permanent.html 的时候,页面直接301定向到 /index.html中

rewrite /permanent.html /index.html permanent;

把 /html/.html => /post/.html ,301定向

rewrite ^/html/(.+?).html$ /post/$1.html permanent;

把 /search/key => /search.html?keyword=key

rewrite ^/search/([^/]+?)(/|$) /search.html?keyword=$1 permanent;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

###### last和break的区别
因为301和302不能简单的只返回状态码,还必须有重定向的URL,这就是return指令无法返回301,302的原因了。这里 last 和 break 区别有点难以理解:

last一般写在server和if中,而break一般使用在location中

last不终止重写后的url匹配,即新的url会再从server走一遍匹配流程,而break终止重写后的匹配

break和last都能组织继续执行后面的rewrite指令

###### if判断
+ 当表达式只是一个变量时,如果值为空或任何以0开头的字符串都会当做false
+ 直接比较变量和内容时,使用=或!=
+ ~正则表达式匹配,~*不区分大小写的匹配,!~区分大小写的不匹配

```plain
-f 和 !-f 用来判断是否存在文件
-d 和 !-d 用来判断是否存在目录
-e 和 !-e 用来判断是否存在文件或目录
-x 和 !-x 用来判断文件是否可执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$args :这个变量等于请求行中的参数,同$query_string
$content_length : 请求头中的Content-length字段。
$content_type : 请求头中的Content-Type字段。
$document_root : 当前请求在root指令中指定的值。
$host : 请求主机头字段,否则为服务器名称。
$http_user_agent : 客户端agent信息
$http_cookie : 客户端cookie信息
$limit_rate : 这个变量可以限制连接速率。
$request_method : 客户端请求的动作,通常为GET或POST。
$remote_addr : 客户端的IP地址。
$remote_port : 客户端的端口。
$remote_user : 已经经过Auth Basic Module验证的用户名。
$request_filename : 当前请求的文件路径,由root或alias指令与URI请求生成。
$scheme : HTTP方法(如http,https)。
$server_protocol : 请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
$server_addr : 服务器地址,在完成一次系统调用后可以确定这个值。
$server_name : 服务器名称。
$server_port : 请求到达服务器的端口号。
$request_uri : 包含请求参数的原始URI,不包含主机名,如:”/foo/bar.php?arg=baz”。
$uri : 不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。
$document_uri : 与$uri相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 如果文件不存在则返回400
if (!-f $request_filename) {
return 400;
}
# 如果host不是thpower.com,则301到thpower.com中
if ( $host != 'thpower.com' ){
rewrite ^/(.*)$ https://thpower.com/$1 permanent;
}
# 如果请求类型不是POST则返回405
if ($request_method = POST) {
return 405;
}
# 如果参数中有 a=1 则301到指定域名
if ($args ~ a=1) {
rewrite ^ http://thpower.com/ permanent;
}
防盗链
1
valid_referers none | blocked | server_names | string ...;

该指令会根据Referer Header头的内容分配一个值为0或1给变量$invalid_referer。如果Referer Header头不符合valid_referers指令设置的有效Referer,变量$invalid_referer

  • none:表示无Referer值的情况。
  • blocked:表示Referer值被防火墙进行伪装。
  • server_names:表示一个或多个主机名称。server_names中可以使用通配符”*”号。
1
2
3
4
5
6
location ~* \.(gif|jpg|swf)$ {
valid_referers none blocked id.qlteacher.com yx.qlteacher.com;
if ($invalid_referer) {
rewrite ^/ http://$host/403.html;
}
}
静态文件服务
1
2
3
4
5
6
7
server {
listen 80;
server_name www.thpower.com;
charset utf-8;
root /lvm/static/;
index index.html index.htm;
}
文件缓存
1
2
3
4
5
6
# 这个将为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
open_file_cache max=204800 inactive=20s;
# open_file_cache 指令中的inactive 参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive 时间内一次没被使用,它将被移除。
open_file_cache_min_uses 1;
# 这个是指多长时间检查一次缓存的有效信息
open_file_cache_valid 30s;
浏览器缓存

| <font style="color:#8e908c;"># 这个是告诉浏览器缓存5天</font>
expires 5d;

工作流程如下

gzip压缩
1
2
3
4
5
6
7
#默认情况下,Nginx的gzip压缩是关闭的,gzip压缩功能就是可以让你节省不少带宽,但是会增加服务器CPU的开销,Nginx默认只对text/html进行压缩 如果要对html之外的内容进行压缩传输,我们需要手动来设置
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
反向代理

       反向代理(Reverse Proxy)方式是指nginx受的连接请求,后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给上游的客户端,此时nginx对外就表现为一个反向代理服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
location / {
# 将请求反向代理到指定的url上
proxy_pass https://github.com;
# 当上游服务器返回的响应是重定向或刷新请求(301|302)
# proxy_redirect可以重设HTTP头部的location或refresh字段
proxy_redirect off;
# 默认情况下,反向代理不会转发原始请求中的 Host 头部,如果需要转发,就需要加上
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /thpower {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass https://github.com/thpower;
}
upstream 负载均衡

          当需要反向代理的服务器不止一个时,我们需要动态的根据环境分配负载以达到最佳化资源使用,最大化吞吐率,最小化响应时间,同时避免过载的目的;upstream 是 Nginx 的 HTTP Upstream 模块,这个模块通过一个简单的调度算法来实现客户端 IP 到后端服务器的负载均衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
#这个名字随便起
upstream test{
ip_hash;
server 192.168.0.1:80;
server 192.168.0.2:80 down;
server 192.168.0.3:8009 max_fails=3 fail_timeout=20s;
server 192.168.0.4:8080 backup;
}
server {
location / {
proxy_pass http://test; #与这里对应即可
}
}
upstream 支持的6种算法
  1. 轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响。Weight 指定轮询权值,Weight 值越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下。
  2. ip_hash:每个请求按访问 IP 的 hash 结果分配,这样来自同一个 IP 的访客固定访问一个后端服务器,后端使用session的且没有用共享session方案的 可以这个解决问题。
  3. fair:这是比上面两个更加智能的负载均衡算法。此种算法可以依据加载时间长短智能地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx 本身是不支持 fair 的,如果需要使用这种调度算法,必须下载 Nginx 的 upstream_fair 模块。
  4. url_hash:此方法按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx 本身是不支持 url_hash 的,如果需要使用这种调度算法,必须安装 Nginx 的 hash 软件包。
  5. least_conn:最少连接负载均衡算法,简单来说就是每次选择的后端都是当前最少连接的一个 server(这个最少连接不是共享的,是每个 worker 都有自己的一个数组进行记录后端 server 的连接数)。
  6. hash:这个 hash 模块又支持两种模式 hash, 一种是普通的 hash, 另一种是一致性 hash(consistent)。
    1. 普通的hash算法支持配置http变量值作为hash值计算的key,通过hash计算得出的hash值和总权重的余数作为挑选server的依据
    2. 一致性hash是普通hash的增强版,具体算法见 官网
upstream 支持4种状态参数
  • down:表示当前的 server 暂时不参与负载均衡
  • backup:预留的备份机器。当其他所有的非 backup 机器出现故障或者忙的时候,才会请求 backup 机器,因此这台机器的压力最轻
  • max_fails:允许请求失败的次数,默认为 1 。当超过最大次数时,返回 proxy_next_upstream 模块定义的错误
  • fail_timeout:在经历了 max_fails 次失败后,暂停服务的时间。max_fails 可以和 fail_timeout 一起使用
问题以及注意事项
万恶的if

               详见官方文档 邪恶的 if 指令

在 HTTPS 中使用 SSLv3

由于 SSLv3 的 POODLE 漏洞, 不要在开启 SSL 的网站使用 SSLv3。 你可以简单粗暴的直接禁用 SSLv3,用 TLS 来替代:

1
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

openresty的lua模块

1
2
3
4
5
6
location / {
default_type text/html;
content_by_lua_block {
ngx.say("HelloWorld")
}
}

lua入门简介

                Lua 是一门轻量级脚本语言遵从着简单、小巧、可移植、快速的原则。Lua 在游戏开发、机器人控制、分布式应用、图像处理、生物信息学等各种各样的领域中得到了广泛的应用。其中尤以游戏开发为最,比如World of Warcraft、大话西游,都采用了 Lua 来配合引擎完成数据描述、配置管理和逻辑控制等任务。即使像 Redis 这样中性的内存键值数据库也提供了内嵌用户 Lua 脚本的官方支持

 Lua 有着如下的特性:

  • 变量名没有类型,值才有类型,变量名在运行时可与任何类型的值绑定
  • 语言只提供唯一一种数据结构,称为表(table),它混合了数组、哈希,可以用任何类型的值作为 key 和 value
  • 函数是一等类型,支持匿名函数和正则尾递归
  • 支持词法定界(lexical scoping)和闭包(closure)
  • 提供 thread 类型和结构化的协程(coroutine)机制,在此基础上可方便实现协作式多任务
  • 运行期能编译字符串形式的程序文本并载入虚拟机执行
  • 通过元表(metatable)和元方法(metamethod)提供动态元机制(dynamic meta-mechanism),从而允许程序运行时根据需要改变或扩充语法设施的内定语义
  • 能方便地利用表和动态元机制实现基于原型(prototype-based)的面向对象模型;

Lua 基础数据类型

nil(空)

nil 是一种类型,Lua 将 nil 用于表示“无效值”。一个变量在第一次赋值前的默认值是 nil,将 nil 赋予给一个全局变量就等同于删除它

1
2
3
4
local num
print(num) -->output:nil
num = 100
print(num) -->output:100

值得一提的是,OpenResty 的 Lua 接口还提供了一种特殊的空值,即 ngx.null,用来表示不同于 nil 的“空值”。

boolean

布尔类型,可选值 true/false;Lua 中 nil 和 false 为“假”,其它所有值均为“真”。比如 0 和空字符串就是“真”;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local a = true
local b = 0
local c = nil
if a then
print("a") -->output:a
else
print("not a") --这个没有执行
end
if b then
print("b") -->output:b
else
print("not b") --这个没有执行
end
if c then
print("c") --这个没有执行
else
print("not c") -->output:not c
end
number

Number 类型用于表示实数,和 C/C++ 里面的 double 类型很类似

1
2
3
4
local order = 3.99
local score = 98.01
print(math.floor(order)) -->output:3
print(math.ceil(score)) -->output:99
string

Lua 中有三种方式表示字符串

使用一对匹配的单引号。例:’hello’

使用一对匹配的双引号。例:”abclua”

字符串还可以用一种长括号(即[[ ]])括起来的方式定义 

            我们把两个正的方括号(即[[)间插入 n 个等号定义为第 n 级正长括号。就是说,0 级正的长括号写作 [[ ,一级正的长括号写作 [=[,如此等等。反的长括号也作类似定义;举个例子,4 级反的长括号写作 ]====]。一个长字符串可以由任何一级的正的长括号开始,而由第一个碰到的同级反的长括号结束。整个词法分析过程将不受分行限制,不处理任何转义符,并且忽略掉任何不同级别的长括号。这种方式描述的字符串可以包含任何东西,当然本级别的反长括号除外。例:[[abc\nbc]],里面的 “\n” 不会被转义

注意,Lua 的字符串是不可改变的值,不能像在其他语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符。两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。 这意味着

创建相同的 Lua 字符串并不会引入新的动态内存分配操作,所以相对便宜(但仍有全局哈希表查询的开销)

内容相同的 Lua 字符串不会占用多份存储空间,

已经创建好的 Lua 字符串之间进行相等性比较时是 O(1) 时间度的开销,而不是通常见到的 O(n) <font style="color:#333333;">即需要逐个字节(或按若干个连续字节)进行比较</font>

1
2
3
4
5
6
7
8
local str1 = 'hello world'
local str2 = "hello lua"
local str3 = [["add\name",'hello']]
local str4 = [=[string have a [[]].]=]
print(str1) -->output:hello world
print(str2) -->output:hello lua
print(str3) -->output:"add\name",'hello'
print(str4) -->output:string have a [[]].
table

Table 类型实现了一种抽象的“关联数组”。“关联数组”是一种具有特殊索引方式的数组,索引通常是字符串(string)或者 number 类型,但也可以是除 nil 以外的任意类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local corp = {
web = "www.google.com", --索引为字符串,key = "web",
-- value = "www.google.com"
telephone = "12345678", --索引为字符串
staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
100876, --相当于 [1] = 100876,此时索引为数字
-- key = 1, value = 100876
100191, --相当于 [2] = 100191,此时索引为数字
[10] = 360, --直接把数字索引给出
["city"] = "Beijing" --索引为字符串
}
print(corp.web) -->output:www.google.com
print(corp["telephone"]) -->output:12345678
print(corp[2]) -->output:100191
print(corp["city"]) -->output:"Beijing"
print(corp.staff[1]) -->output:Jack
print(corp[10]) -->output:360
function

在 Lua 中,函数 也是一种数据类型,函数可以存储在变量中,可以通过参数传递给其他函数,还可以作为其他函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 由于全局变量一般会污染全局名字空间,同时也有性能损耗(即查询全局环境表的开销),因此我们应当尽量使用“局部函数”,其记法是类似的,只是开头加上 local 修饰符
local function foo(args) --args参数,函数的参数列表可以为空
print(args.." in the function")
--dosomething()
local x = 10
local y = 20
return x + y
end
local a = foo --把函数赋给变量
print(a("foo"))
--output:
foo in the function
30

由于函数定义本质上就是变量赋值,而变量的定义总是应放置在变量使用之前,所以函数的定义也需要放置在函数调用之前。有名函数的定义本质上是匿名函数对变量的赋值。

1
2
function foo()
end

等价于

1
2
foo = function ()
end

Lua 函数的参数大部分是按值传递的。值传递就是调用函数时,实参把它的值通过赋值运算传递给形参,然后形参的改变和实参就没有关系了。在这个过程中,实参是通过它在参数表中的位置与形参匹配起来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
local function swap(a, b) --定义函数swap,函数内部进行交换两个变量的值
local temp = a
a = b
b = temp
print(a, b)
end
local x = "hello"
local y = 20
print(x, y)
swap(x, y) --调用swap函数
print(x, y) --调用swap函数后,x和y的值并没有交换
-->output
hello 20
20 hello
hello 20

在调用函数的时候,若形参个数和实参个数不同时,Lua 会自动调整实参个数。调整规则:若实参个数大于形参个数,从左向右,多余的实参被忽略;若实参个数小于形参个数,从左向右,没有被实参初始化的形参会被初始化为 nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local function fun1(a, b)       --两个形参,多余的实参被忽略掉
print(a, b)
end
local function fun2(a, b, c, d) --四个形参,没有被实参初始化的形参,用nil初始化
print(a, b, c, d)
end
local x = 1
local y = 2
local z = 3
fun1(x, y, z) -- z被函数fun1忽略掉了,参数变成 x, y
fun2(x, y, z) -- 后面自动加上一个nil,参数变成 x, y, z, nil
-->output
1 2
1 2 3 nil

上面函数的参数都是固定的,其实 Lua 还支持变长参数。若形参为 <font style="color:#333333;">...</font> , 表示该函数可以接收不同长度的参数。访问参数的时候也要使用 <font style="color:#333333;">...</font>

1
2
3
4
5
6
7
8
9
10
11
local function func( ... )                -- 形参为 ... ,表示函数采用变长参数
local temp = {...} -- 访问的时候也要使用 ...
local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
-- 组内容使用 " " 拼接成字符串。
print(ans)
end
func(1, 2) -- 传递了两个参数
func(1, 2, 3, 4) -- 传递了四个参数
-->output
1 2
1 2 3 4

Lua 还支持通过名称来指定实参,这时候要把所有的实参组织到一个 table 中,并将这个 table 作为唯一的实参传给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local function change(arg) -- change 函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2
arg.height = arg.height * 2
return arg --这里去掉return 最后结果是怎样的?
end
local rectangle = { width = 20, height = 15 }
print("before change:", "width =", rectangle.width,
"height =", rectangle.height)
rectangle = change(rectangle)
print("after change:", "width =", rectangle.width,
"height =", rectangle.height)
-->output
before change: width = 20 height = 15
after change: width = 40 height = 30

当函数参数是 table 类型时,传递进来的是 实际参数的引用,此时在函数内部对该 table 所做的修改,会直接对调用者所传递的实际参数生效

Lua 具有一项与众不同的特性,允许函数返回多个值。

1
2
local s, e = string.find("hello world", "llo")
print(s, e) -->output 3 5
1
2
3
4
5
6
7
local function swap(a, b)   -- 定义函数 swap,实现两个变量交换值
return b, a -- 按相反顺序返回变量的值
end
local x = 1
local y = 20
x, y = swap(x, y) -- 调用 swap 函数
print(x, y) --> output 20 1

当函数返回值的个数和接收返回值的变量的个数不一致时,Lua 也会自动调整参数个数。调整规则: 若返回值个数大于接收变量的个数,多余的返回值会被忽略掉; 若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为 nil。

1
2
3
4
5
6
7
8
9
10
function init()             --init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
x = init()
print(x)
x, y, z = init()
print(x, y, z)
--output
1
1 lua nil

当一个函数有一个以上返回值,且函数调用不是一个列表表达式的最后一个元素,那么函数调用只会产生一个返回值, 也就是第一个返回值。

1
2
3
4
5
6
7
8
9
10
11
local function init()       -- init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -->output 1 2 nil
local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -->output 2 1 lua
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1

表达式

算术运算符
算术运算符 说明
+
-
*
/
^ 指数
% 取模
1
2
3
4
5
6
7
8
9
print(1 + 2)       -->打印 3
print(5 / 10) -->打印 0.5。 这是Lua不同于c语言的
print(5.0 / 10) -->打印 0.5。 浮点数相除的结果是浮点数
-- print(10 / 0) -->注意除数不能为0,计算的结果会出错
print(2 ^ 10) -->打印 1024。 求2的10次方
local num = 1357
print(num % 2) -->打印 1
print((num % 2) == 1) -->打印 true。 判断num是否为奇数
print((num % 5) == 0) -->打印 false。判断num是否能被5整数
关系运算符
关系运算符 说明
< 小于
> 大于
<= 小于等于
>= 大于等于
== 等于
~= 不等于
1
2
3
4
5
print(1 < 2)    -->打印 true
print(1 == 2) -->打印 false
print(1 ~= 2) -->打印 true
local a, b = true, false
print(a == b) -->打印 false
在使用“==”做等于判断时,要注意对于 table, userdate 和函数, Lua 是作引用比较的。也就是说,只有当两个变量引用同一个对象时,才认为它们相等。可以看下面的例子
1
2
3
4
5
6
7
8
9
local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
print("a==b")
else
print("a~=b")
end
---output:
a~=b
逻辑运算符
逻辑运算符 说明
and 逻辑与
or 逻辑或
not 逻辑非
1
2
3
4
5
6
7
8
9
10
local c = nil
local d = 0
local e = 100
print(c and d) -->打印 nil
print(c and e) -->打印 nil
print(d and e) -->打印 100
print(c or d) -->打印 0
print(c or e) -->打印 100
print(not c) -->打印 true
print(not d) -->打印 false
所有逻辑操作符将 false 和 nil 视作假,其他任何值视作真,对于 and 和 or,“短路求值”,对于 not,永远只返回 true 或者 false。
字符串连接

                在 Lua 中连接两个字符串,可以使用操作符“..”(两个点)。如果其任意一个操作数是数字的话,Lua 会将这个数字转换成字符串。注意,连接操作符只会创建一个新字符串,而不会改变原操作数。也可以使用 string 库函数 string.format 连接字符串。

1
2
3
4
5
6
print("Hello " .. "World")    -->打印 Hello World
print(0 .. 1) -->打印 01
str1 = string.format("%s-%s","hello","world")
print(str1) -->打印 hello-world
str2 = string.format("%d-%s-%.2f",123,"world",1.21)
print(str2) -->打印 123-world-1.21

                 由于 Lua 字符串本质上是只读的,因此字符串连接运算符几乎总会创建一个新的(更大的)字符串。这意味着如果有很多这样的连接操作(比如在循环中使用 .. 来拼接最终结果),则性能损耗会非常大。在这种情况下,推荐使用 table 和 table.concat() 来进行很多字符串的拼接,例如:

1
2
3
4
5
local pieces = {}
for i, elem in ipairs(my_list) do
pieces[i] = my_process(elem)
end
local res = table.concat(pieces)
优先级

优先级如下表所示(从高到低)

优先级
^
not   # -
*   /   %
+   -
..
< > <=  >=  ==  ~=
and
or
1
2
3
4
5
6
7
local a, b = 1, 2
local x, y = 3, 4
local i = 10
local res = 0
res = a + i < b/2 + 1 -->等价于res = (a + i) < ((b/2) + 1)
res = 5 + x^2*8 -->等价于res = 5 + ((x^2) * 8)
res = a < y and y <=x -->等价于res = (a < y) and (y <= x)

结构控制 

if/else
1
2
3
4
5
6
7
8
9
10
11
12
score = 0
if score == 100 then
print("Very good!Your score is 100")
elseif score >= 60 then
print("Congratulations, you have passed it,your score greater or equal to 60")
else
if score > 0 then
print("Your score is better than 0")
else
print("My God, your score turned out to be 0")
end --与上一示例代码不同的是,此处要添加一个end
end
while
1
2
3
4
5
6
7
x = 1
sum = 0
while x <= 5 do
sum = sum + x
x = x + 1
end
print(sum) -->output 15
Lua 并没有像许多其他语言那样提供类似 continue
这样的控制语句用来立即进入下一个循环迭代(如果有的话)。因此,我们需要仔细地安排循环体里的分支,以避免这样的需求。只提供了break
,可以跳出当前循环。
1
2
3
4
5
6
7
8
local t = {1, 3, 5, 8, 11, 18, 21}
local i
for i, v in ipairs(t) do
if 11 == v then
print("index[" .. i .. "] have right value[11]")
break
end
end
repeat

类似于do-while

1
2
3
4
x = 10
repeat
print(x)
until false -- 死循环
for

for 数字型

1
2
3
for var = begin, finish, step do
--body
end
+ var 从 begin 变化到 finish,每次变化都以 step 作为步长递增 var 
+ begin、finish、step 三个表达式只会在循环开始时执行一次
+ 第三个表达式 step 是可选的,默认为 1
+ 控制变量 var 的作用域仅在 for 循环内,需要在外面控制,则需将值赋给一个新的变量
+ 循环过程中不要改变控制变量的值,那样会带来不可预知的影响
1
2
3
4
5
6
7
8
9
for i = 1, 5 do
print(i)
end
-- output:
1
2
3
4
5
1
2
3
4
5
6
7
8
9
for i = 1, 10, 2 do
print(i)
end
-- output:
1
3
5
7
9
1
2
3
4
for i = 10, 1, -1 do
print(i)
end
-- output : ?
for 泛型
1
2
3
4
5
6
7
8
9
local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
print("index:", i, " value:", v)
end
-- output:
index: 1 value: a
index: 2 value: b
index: 3 value: c
index: 4 value: d
break goto

break

语句 <font style="color:#333333;">break</font> 用来终止 <font style="color:#333333;">while</font><font style="color:#333333;">repeat</font><font style="color:#333333;">for</font> 三种循环的执行,并跳出当前循环体, 继续执行当前循环之后的语句。下面举一个 <font style="color:#333333;">while</font> 循环中的 <font style="color:#333333;">break</font> 的例子来说明:

1
2
3
4
5
6
7
8
9
10
11
-- 计算最小的x,使从1到x的所有数相加和大于100
sum = 0
i = 1
while true do
sum = sum + i
if sum > 100 then
break
end
i = i + 1
end
print("The result is " .. i) -->output:The result is 14

goto

LuaJIT 一开始对标的是 Lua 5.1,但渐渐地也开始加入部分 Lua 5.2 甚至 Lua 5.3 的有用特性。 goto 就是其中一个不得不提的例子。有了 goto,我们可以实现 continue 的功能:

1
2
3
4
5
6
7
8
9
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print([[i'm end]])
end

openresty

HelloWorld

1
2
3
4
5
6
location / {
default_type text/html;
content_by_lua_block {
ngx.say("HelloWorld")
}
}

与nginx local的组合

内部调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location = /sum {
# 只允许内部调用
internal;
# 这里做了一个求和运算只是一个例子,可以在这里完成一些数据库、
# 缓存服务器的操作,达到基础模块和业务逻辑分离目的
content_by_lua_block {
local args = ngx.req.get_uri_args()
ngx.say(tonumber(args.a) + tonumber(args.b))
}
}
location = /app/test {
content_by_lua_block {
local res = ngx.location.capture(
"/sum", {args={a=3, b=8}}
)
ngx.say("status:", res.status, " response:", res.body)
}
}
并行调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
location = /sum {
internal;
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) + tonumber(args.b))
}
}
location = /subduction {
internal;
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) - tonumber(args.b))
}
}
location = /app/test_parallels {
content_by_lua_block {
local start_time = ngx.now()
local res1, res2 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}},
{"/subduction", {args={a=3, b=8}}}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}
location = /app/test_queue {
content_by_lua_block {
local start_time = ngx.now()
local res1 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}}
})
local res2 = ngx.location.capture_multi( {
{"/subduction", {args={a=3, b=8}}}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}
1
2
3
4
5
6
7
8
➜  ~ curl 127.0.0.1/app/test_parallels
status:200 response:11
status:200 response:-5
time used:0.10099983215332
➜ ~ curl 127.0.0.1/app/test_queue
status:200 response:11
status:200 response:-5
time used:0.20199990272522

利用 ngx.location.capture_multi 函数,直接完成了两个子请求并行执行。当两个请求没有相互依赖,这种方法可以极大提高查询效率。两个无依赖请求,各自是 100ms,顺序执行需要 200ms,但通过并行执行可以在 100ms 完成两个请求。

重定向
1
2
3
4
5
6
7
8
9
10
location = /foo {
content_by_lua_block {
ngx.say([[I am foo]])
}
}
location = / {
rewrite_by_lua_block {
return ngx.redirect('/foo');
}
}

一些常用的api

1
2
3
4
5
6
7
8
9
10
11
12
13
location /print_param {
content_by_lua_block {
local arg = ngx.req.get_uri_args()
for k,v in pairs(arg) do
ngx.say("[GET ] key:", k, " v:", v)
end
ngx.req.read_body() -- 解析 body 参数之前一定要先读取 body
local arg = ngx.req.get_post_args()
for k,v in pairs(arg) do
ngx.say("[POST] key:", k, " v:", v)
end
}
}
1
2
3
4
5
➜  ~  curl '127.0.0.1/print_param?a=1&b=2%26' -d 'c=3&d=4%26'
[GET ] key:b v:2&
[GET ] key:a v:1
[POST] key:d v:4&
[POST] key:c v:3
  • 传递请求 uri 参数
1
2
3
4
5
6
7
8
local res = ngx.location.capture('/print_param',
{
method = ngx.HTTP_POST,
args = {a = 1, b = '2&'},
body = 'c=3&d=4%26'
}
)
ngx.say(res.body)
  • 输出响应体

HTTP响应报文

                对于 HTTP 响应体的输出,在 OpenResty 中调用 ngx.sayngx.print 即可。区别是 ngx.say 会对输出响应体多输出一个 \n 。如果你用的是浏览器完成的功能调试,使用这两着是没有区别的。但是如果使用各种终端工具,这时候使用 ngx.say 明显就更方便了。

ngx.say 与 ngx.print 均为异步输出 也就是说当调用 ngx.say 后并不会立刻输出响应体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
location /test {
content_by_lua_block {
ngx.say("hello")
ngx.sleep(3)
ngx.say("the world")
}
}
location /test2 {
content_by_lua_block {
ngx.say("hello")
ngx.flush() -- 显式的向客户端刷新响应输出
ngx.sleep(3)
ngx.say("the world")
}
}
location /test3 {
content_by_lua_block {
ngx.say(string.rep("hello", 1000))
ngx.sleep(3)
ngx.say("the world")
}
}

/test 响应内容实在触发请求 3s 后一起接收到响应体,而 /test2 则是先收到一个 hello 停顿 3s 后又接收到后面的 the world<font style="color:#333333;">,/test3首先收到了所有的 "hello" ,停顿大约 3 秒后,接着又收到了 "the world" </font>

应用点: 响应体过大的输出

利用 HTTP 1.1 特性 CHUNKED 编码来完成 把一个大的响应体拆分成多个小的应答体,分批、有节制的响应给请求方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location /test {
content_by_lua_block {
-- ngx.var.limit_rate = 1024*1024
local file, err = io.open(ngx.config.prefix() .. "data.db","r")
if not file then
ngx.log(ngx.ERR, "open file error:", err)
ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
local data
while true do
data = file:read(1024)
if nil == data then
break
end
ngx.print(data)
ngx.flush(true)
end
file:close()
}
}

按块读取本地文件内容(每次 1KB),并以流式方式进行响应。

  • 日志输出
1
2
3
4
5
content_by_lua_block {
ngx.log(ngx.ERR, "this is err")
ngx.log(ngx.INFO, "this is info")
ngx.log(ngx.DEBUG, "this is debug")
}
1
2
3
4
5
6
7
8
9
ngx.STDERR     -- 标准输出
ngx.EMERG -- 紧急报错
ngx.ALERT -- 报警
ngx.CRIT -- 严重,系统故障,触发运维告警系统
ngx.ERR -- 错误,业务不可恢复性错误
ngx.WARN -- 告警,业务中可忽略错误
ngx.NOTICE -- 提醒,业务比较重要信息
ngx.INFO -- 信息,业务琐碎日志信息,包含不同情况判断等
ngx.DEBUG -- 调试

如果你的日志需要归集,并且对时效性要求比较高那么可以用 lua-resty-logger-socket  以非阻塞 IO 方式推送 access log 到远程服务器上。对远程服务器的要求是支持 syslog-ng 的日志服务。目前生产上的elk服务就支持 syslog-ng

执行阶段概念

OpenResty 处理一个请求,它的处理流程请参考下图(从 Request start 开始):

          这几个阶段的存在,应该是 OpenResty 不同于其他多数 Web 平台编程的最明显特征了。由于 Nginx 把一个请求分成了很多阶段,这样第三方模块就可以根据自己行为,挂载到不同阶段进行处理达到目的。OpenResty 也应用了同样的特性。所不同的是,OpenResty 挂载的是我们编写的 Lua 代码。这样我们就可以根据我们的需要,在不同的阶段直接完成大部分典型处理了。

  • set_by_lua*: 流程分支处理判断变量初始化
  • rewrite_by_lua*: 转发、重定向、缓存等功能(例如特定请求代理到外网)
  • access_by_lua*: IP 准入、接口权限等情况集中处理(例如配合 iptable 完成简单防火墙)
  • content_by_lua*: 内容生成
  • header_filter_by_lua*: 响应头部过滤处理(例如添加头部信息)
  • body_filter_by_lua*: 响应体过滤处理(例如完成应答内容统一成大写)
  • log_by_lua*: 会话完成后本地异步完成日志记录(日志可以记录在本地,还可以同步到其他机器)

举一个例子,请求体和响应体都需要使用 aes 加密,利用不同的执行阶段,我们可以非常简单的实现:

1
2
3
4
5
6
7
8
9
10
# 明文协议版本
location /mixed {
content_by_lua_file ...; # 请求处理
}
# 加密协议版本
location /mixed {
access_by_lua_file ...; # 请求加密解码
content_by_lua_file ...; # 请求处理,不需要关心通信协议
body_filter_by_lua_file ...; # 应答加密编码
}

实际系统中的应用

获取系统时间

https://yanxiu.qlteacher.com/api/currentTime

1
2
3
4
5
6
7
8
9
10
location = /api/currentTime {	  
default_type "application/json; charset=utf-8";
content_by_lua '
local cjson = require("cjson")
local t = {data = ngx.time().."000"}
ngx.say(cjson.encode(t))
';

proxy_redirect off;
}
客户端检测页

https://www.qlteacher.com/check/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
location /check {
alias /lvm/client-detection/;
index index.html;
expires 5d;
}
location = /testmethod {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials "true";
add_header 'Access-Control-Allow-Methods' 'GET,PUT, POST, OPTIONS, DELETE';
default_type "application/json; charset=utf-8";
content_by_lua_file testmethod.lua;
proxy_redirect off;
}
location = /getdomains {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Credentials "true";
add_header 'Access-Control-Allow-Methods' 'GET,PUT, POST, OPTIONS, DELETE';
default_type "application/json; charset=utf-8";
content_by_lua '
local mainHost = {"www.qlteacher.com","zone.qlteacher.com","player.qlteacher.com","yanxiu.qlteacher.com","id.qlteacher.com","blog.qlteacher.com"}
local cjson = require("cjson")
ngx.say(cjson.encode(mainHost))
';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
local cjson = require("cjson")
if ngx.req.get_method() == "OPTIONS" then
ngx.exit(204)
end
local t = {time = ngx.time(),method = ngx.req.get_method()}
t.urlargs = ngx.req.get_uri_args()
ngx.req.read_body()
local postargs = ngx.req.get_post_args()
if postargs then
t.postargs = postargs
end
t.host = ngx.req.get_headers()["Host"]
ngx.say(cjson.encode(t))
从缓存中读取结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
upstream statredis_pool_stream {
server redis:6379;
keepalive 128;
}
location ^~ /api/getUserFromRedis/{
default_type "application/json; charset=utf-8";
if ($uri ~* "^/api/getUserFromRedis/([^/]+)/user/([0-9a-z]{1,32})$"){
set $projectid $1;
set $userid $2;
set $redis_key "user:${projectid}:${userid}";
redis_pass statredis_pool_stream;
error_page 404 = @user404;
}
proxy_pass http://user-web/;
proxy_redirect off;
proxy_set_header Host $host;
}
location @user404 {
rewrite . /stat-web/${projectid}/user/${userid} break;
proxy_pass http://stat;
}
在线人数/请求数系列
1
2
3
4
5
6
7
8
location ^~ /api/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://user_web/;
proxy_redirect off;
log_by_lua_file sitelog.lua;
}
1
2
3
4
5
6
7
8
9
10
11
12
local online = require("online")
local handler
function handler(premature,domain,uri,userInfo)
--ngx.log(ngx.ERR,"domain=",domain)
--ngx.log(ngx.ERR,"uri=",uri)
--ngx.log(ngx.ERR,"userInfo=",userInfo)
online.broadcast(premature,domain,uri,userInfo)
end
local ok, err = ngx.timer.at(0, handler,tostring(ngx.var.HTTP_HOST),tostring(ngx.var.uri),ngx.var.cookie_userInfo)
if not ok then
ngx.log(ngx.ERR, "onlineCount error", err)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
local redis = require "resty.redis"
local ResourceTemplate = {}
function ResourceTemplate:new(o)
o = o or {}
setmetatable(o,self)
self.__index = self
return o
end
function ResourceTemplate:dealWith(callback)
local r = self:open()
if not r then
error("No resource opened")
end
local ok, data = pcall(callback,r)
self:close(r)
if not ok then
error(data)
else
return data
end
end
local RedisTemplate = ResourceTemplate:new({
open = function(self)
local red = redis:new()
red:set_timeout(1000)
-- local ips = {{host="10.0.0.246",port=6379},{host="10.0.0.247",port=6379}}
-- math.randomseed( tonumber(tostring(os.time()):reverse():sub(1,6)) )
-- local ip = ips[math.random(2)]
-- local ok, err = red:connect(ip.host, ip.port,{ pool = "my_redis_cluster" })
local ok, err = red:connect("10.0.0.220", 6379)
if not ok then
error("failed to connect: "..err)
end
return red
end,
close = function(self, red)
local ok, err = red:set_keepalive(10000, 100)
if not ok then
error("failed to set keepalive: "..err)
end
end
})
return {
ResourceTemplate = ResourceTemplate,
RedisTemplate = RedisTemplate
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
local SID_PERFIX ="sid:"
local UID_PERFIX ="uid:"
local VID_PERFIX ="vid:"
local PV_PREFIX ="pv:"
local mainHost = {"id.thpower.com"}
local cjson = require("cjson")
local Resource = require("resource")
local DecodeCookie = require("decode_cookie")
local redis = Resource.RedisTemplate
local Online = {}
function Online:broadcast(domain,uri,userInfo)
--判断请求地址是否记录
local address = "www.thpower.com"
for key,value in pairs(mainHost) do
if string.find(domain..uri,value.."*") then
address = value
end
end
local cookie = DecodeCookie:new({token=userInfo})
local userid
local userkey
if cookie:isValid() then
--登陆用户
local currentUser = cookie:currentUser()
local index = string.find(currentUser,"@")
userid = string.sub(currentUser,0,index-1)
userkey = UID_PERFIX..userid
else
-- 未登录用户记录
userid = userInfo
userkey = SID_PERFIX..userid
end

redis:dealWith(function(red)
--red:init_pipeline()
red:incr(PV_PREFIX..address)
red:expire(PV_PREFIX..address,120)
red:set(VID_PERFIX..address..":"..userid,"","EX",120,"NX")
red:set(userkey,userid,"EX",1800)
--red:commit_pipeline()
end)

end
return Online

以上逻辑执行完后redis的数据如下:

1
2
3
4
5
6
7
vid:id.thpower.com:{userid}
...
vid:iot.thpower.com:{userid}

uid:{userId}

pv:{domain}

以上的数据经过后端一个服务简单的处理后,又把处理结果再次放回redis内

1
2
key :online
value: 取以uid:开头的key的数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key :OnLine:{domain}
value: [{
"domain": "{domain}",
"vidCount": 65475, 取以vid:开头的key的数量
"pvCount": 84934, 取pv:{domain}这个计数器的值
"date": 1561087844540 这次计算的时间戳
}
,
{
"domain": "{domain}",
"vidCount": 66683,
"pvCount": 84654,
"date": 1561087783763
}
,......]

再通过openresty读取

1
2
3
4
5
location ^~ /dapi/online/ {
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin *;
content_by_lua_file 'readUser.lua';
}
1
2
3
4
5
6
7
8
9
10
local cjson = require("cjson")
--local Resource = require("redisCl")
--local redis = Resource.RedisTemplate
local Resource = require("resource")
local redis = Resource.RedisTemplate
ngx.header.content_type = "application/json; charset=utf-8"
redis:dealWith(function(red)
local count,err = red:get("online")
ngx.say(count)
end)
1
2
curl https://zone.qlteacher.com/dapi/online/
"155606"
1
2
3
4
5
location /dapi/site/ {
proxy_set_header Host $host;
add_header Access-Control-Allow-Origin *;
content_by_lua_file 'readOnline.lua';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
local cjson = require("cjson")
--local Resource = require("redisCl")
--local redis = Resource.RedisTemplate
local Resource = require("resource")
local redis = Resource.RedisTemplate
ngx.header.content_type = "application/json; charset=utf-8"
local domain = ngx.var.arg_domain
local _max = ngx.var.arg_max
local _min = ngx.var.arg_min
if domain == nil then
ngx.say("错误的请求")
return
end
if _max == nil then
_max=1440
end
if _min == nil then
_min=0
end
redis:dealWith(function(red)
local view,err = red:lrange("OnLine:"..domain,_min,_max)

local list = {}
for key, value in pairs(view) do
local item = cjson.decode(value)
--ngx.say(item.count)
table.insert(list,item)
end
ngx.say(cjson.encode(list))
end)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl https://zone.qlteacher.com/dapi/site/?domain=www.qlteacher.com&max=1
[{
"domain": "www.qlteacher.com",
"date": 1561088634171,
"vidCount": 61324,
"pvCount": 79789,
"@class": "com.qlteacher.pv.OnLineEvent"},
{
"domain": "www.qlteacher.com",
"date": 1561088573488,
"vidCount": 61703,
"pvCount": 80486,
"@class": "com.qlteacher.pv.OnLineEvent"}
]

应用场景

任何一个开发语言、开发框架,都有它存在的明确目的,重心是为了解决什么问题。没有说我们学习一门语言或技术,就可以解决所有的问题。

OpenResty适合使用的场景,官网 wiki :
  • 在 Lua 中混合处理不同 Nginx 模块输出(proxy, drizzle, postgres, Redis, memcached 等)。
  • 在请求真正到达上游服务之前,Lua 中处理复杂的准入控制和安全检查。
  • 比较随意的控制应答头(通过 Lua)。
  • 从外部存储中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问。
  • 在内容 handler 中随意编写复杂的 web 应用,同步编写异步访问后端数据库和其他存储。
  • 在 rewrite 阶段,通过 Lua 完成非常复杂的处理。
  • 在 Nginx 子查询、location 调用中,通过 Lua 实现高级缓存机制。
  • 对外暴露强劲的 Lua 语言,允许使用各种 Nginx 模块,自由拼合没有任何限制。该模块的脚本有充分的灵活性,同时提供的性能水平与本地 C 语言程序无论是在 CPU 时间方面以及内存占用差距非常小。
不适合的场景
  • 有长时间阻塞调用的过程,
    • 例如通过 Lua 完成系统命令行调用
    • 使用阻塞的Lua API完成相应操作
  • 单个请求处理逻辑复杂,尤其是需要和请求方多次交互的长连接场景
  • 内存占用高的处理 受制于Lua VM的最大使用内存 1G 的限制
  • 两个请求之间有交流的场景 如在线聊天
  • 与行业专用的组件对接 由于OpenResty必须要使用非阻塞 API ,所以传统的阻塞 API ,我们是没法直接使用的
0%