|
Tomcat 挂了。应用中凌晨两点,间件运维群里炸开锅,应用中一台跑了三年没重启的间件 Tomcat 实例突然不响应任何请求。登录服务器一看,应用中线程池满了,间件全部卡在数据库连接等待上。应用中这事让我重新想了一个问题:我们天天用 Tomcat、间件Nginx、应用中Node.js 这些玩意儿,间件但到底有多少人真正搞清楚"中间件"在中间干了什么?应用中 中间件这个词本身就很模糊。字面意思是间件"中间的软件",但中间在哪、应用中夹在谁和谁之间,间件不同语境下答案完全不一样。应用中消息队列叫中间件,数据库连接池叫中间件,API 网关也叫中间件。我倾向于一个粗暴但实用的定义:中间件是业务代码不想直接处理、但又不得不处理的那层东西。 Servlet 容器:最容易被忽视的那一层 拿 Tomcat 来说。你写一个 Servlet,重写 doGet 方法,返回一段 HTML。但你从来没写过 ServerSocket,没解析过 HTTP 报文,没管理过线程。这些活全被 Tomcat 干了。它监听 8080 端口,把 TCP 字节流翻译成 HttpServletRequest 对象,丢给你的代码,等你处理完再把 HttpServletResponse 序列化回字节流。 这层抽象省了多少事?想想如果用纯 Java 裸写一个 Web 服务:手动 accept 连接、手动读 InputStream、手动判断请求方法、手动拼接响应头。一个 Hello World 能写两百行。Servlet 规范把这套东西标准化之后,Jetty、Tomcat、Undertow 可以互换,你的 WAR 包不用改一行代码就能从 Tomcat 迁到 Jetty。这才是中间件的核心价值——它不是帮你做某件事,而是定义了一套做事的规矩,让上面的代码和下面的环境解耦。 反向代理:流量入口的守门人 Nginx 是另一个典型。大多数生产环境里,Nginx 挡在 Tomcat 前面。用户请求先到 Nginx,Nginx 再做转发。为什么不让 Tomcat 直接对外? 我见过一个真实案例。某电商网站大促,所有请求直接打到应用服务器上。结果静态图片请求把 Tomcat 的线程占满了,动态接口反而没线程可用。加了一层 Nginx 之后,静态资源由 Nginx 直接返回,动态请求才转发给 Tomcat。线程利用率立刻正常了。 Nginx 能干的事远不止转发。SSL 终止、gzip 压缩、限流、灰度发布、A/B 测试,全可以在这一层搞定。应用代码里不用写一行跟业务无关的逻辑。这种"把横切关注点抽出来"的思路,贯穿了整个中间件领域。 消息队列:异步解耦的代价 Kafka 和 RabbitMQ 解决的是另一个维度的问题。用户下单之后,要发短信、要更新库存、要写日志。同步做完这些事,接口响应时间直接飙到两秒。用消息队列把非核心链路异步化,接口降到 200 毫秒。 但中间件从来不是免费的。引入 Kafka 之后,你得处理消息丢失、重复消费、顺序问题、积压告警。原本一个方法调用的事,现在变成了分布式系统的一致性问题。我见过团队为了"解耦"上了消息队列,结果因为消费端处理太慢,消息积压了几百万条,排查了两天才找到是一个反序列化异常导致的死循环。 所以选中间件的时候,我有个不太政治正确的观点:能不用就不用。 单体能跑通的业务,别急着上微服务网关;QPS 不到一千的系统,数据库读写分离都嫌早。中间件是解决规模问题的工具,规模没到,它就是纯粹的复杂度。 写在中件间件代码里的人 回到开头那次事故。根因其实很简单:连接池最大连接数配了 20,但高峰期并发查询有 50 个。多出来的 30 个线程全在等连接。改一个配置数字,问题消失。但背后暴露的是对中间件行为的不理解——连接池不是越大越好,也不是配了就不管。它有自己的生命周期、超时策略、泄漏检测机制。 中间件把复杂性封装起来,但封装不等于消除。那些配置项、那些监控指标、那些日志里的 warning,都是中间件在告诉你:我还在,我也有脾气。 |