面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月31日 00:57

Scrapy 请求失败后怎么重试?错误处理机制该怎么配?

Scrapy 的重试不是越多越好Scrapy 自带 RetryMiddleware,能处理连接超时、DNS 错误、部分 HTTP 状态码等失败场景。它的价值不是“保证每个请求都成功”,而是在短暂网络抖动、服务端偶发 500、代理临时不可用时给请求一次恢复机会。真正需要注意的是:重试会消耗队列、带宽和时间,配置不当还会把目标站的压力继续放大。常见配置如下:RETRY_ENABLED = TrueRETRY_TIMES = 2RETRY_HTTP_CODES = [408, 429, 500, 502, 503, 504, 522, 524]DOWNLOAD_TIMEOUT = 15RETRY_PRIORITY_ADJUST = -1RETRY_TIMES=2 表示失败后最多再试 2 次,不是总共请求 2 次。RETRY_PRIORITY_ADJUST=-1 会让重试请求优先级略降低,避免失败请求一直插队。429 通常代表限流,是否重试要看目标站策略;如果没有退避,只是马上重发,可能更快被封。对业务可预期的失败,最好配合 errback 单独处理。比如详情页失败时记录 URL、来源页和错误类型,后续可以补采,而不是只靠日志里一行 traceback。def start_requests(self): for url in self.urls: yield scrapy.Request(url, callback=self.parse, errback=self.on_error)def on_error(self, failure): request = failure.request self.logger.warning("request failed: %s reason=%r", request.url, failure.value) yield {"url": request.url, "status": "failed"}如果要对限流做得更稳,可以自定义中间件读取 Retry-After,或者在调度层暂停该域名一段时间。不要把所有状态码都塞进 RETRY_HTTP_CODES,这会让 404、权限失败和反爬页面反复进入队列。失败原因先分类,再决定是否重试,是 Scrapy 错误处理里最容易被忽略的一步。还要区分“重试”和“补采”。重试适合马上再试的短暂失败,补采适合任务结束后再单独处理的 URL,比如目标站夜间维护、接口阶段性限流、某批代理质量差。把补采 URL 写入单独队列或表,比在主任务里一直加大 RETRY_TIMES 更可控。这样主任务能按时结束,失败样本也不会丢,后续排查还能看到失败发生在哪个批次。如果项目接入了代理池,还要把代理错误和目标站错误分开统计。代理连接失败、认证失败、目标站返回 5xx,处理方式完全不同,混在一起只会误判。一个简单做法是在代理中间件里给 request.meta 记录代理来源,失败时把来源写进日志或失败 item。这样你能判断是某个代理供应商质量差,还是目标站真的在限流。追问哪些错误应该重试,哪些不该重试?临时网络问题、超时、502、503、504 一般可以重试,因为它们可能只是短暂抖动。404、410 多数表示资源不存在,通常不应该重试,否则只是浪费请求。403 要看情况:如果是登录失效、IP 被拦或权限不足,盲目重试没有意义,应该先修 cookie、代理或请求头。边界最难的是 429,它可能适合延迟后重试,也可能说明当前策略已经触发风控。errback 和 RetryMiddleware 有什么区别?RetryMiddleware 是框架层的自动补救,先判断失败是否符合重试条件,符合就重新入队。errback 更像业务兜底,当请求最终失败或某些异常冒出来时,你可以记录、补偿或产出失败 item。不要把所有失败都塞进 errback 手动重试,否则容易绕开 Scrapy 的统计和优先级机制。一般做法是让 RetryMiddleware 负责通用重试,errback 负责业务可观测性。重试次数应该怎么设置?默认思路是少量重试,通常 1 到 3 次就够了。次数太少会丢掉偶发失败,次数太多会拖慢任务,还可能让失败 URL 长时间占住队列。需要高完整率的采集可以增加补采任务,而不是在主任务里无限重试。生产环境应该看 retry/count、retry/max_reached 和错误码分布,再决定是否调整。遇到反爬导致的失败怎么办?如果错误集中在 403、429、验证码页或异常跳转,问题通常不在重试次数,而在访问策略。可以先降低并发、开启 AutoThrottle、检查 cookie 和 header,再考虑代理或登录态维护。踩坑最多的是把 403 加进重试列表,结果同一个被拦请求重复打过去,封禁更严重。遇到这类情况,应该先暂停放量,确认目标站允许的访问边界。怎么监控重试是否已经失控?Scrapy stats 会记录重试次数、失败原因和最终放弃数量,这些指标比单看日志可靠。可以在扩展或任务收尾阶段读取 retry/count、downloader/response_status_count/429、downloader/exception_type_count/*,超过阈值就告警。边界是告警不能只看绝对值,大任务天然错误更多,最好同时看比例。比如重试率超过 10% 或 429 持续上升,就该降速或停止任务排查。def closed(self, reason): retry_count = self.crawler.stats.get_value("retry/count", 0) max_reached = self.crawler.stats.get_value("retry/max_reached", 0) self.logger.info("retry=%s max_reached=%s", retry_count, max_reached)小结Scrapy 错误处理的核心是分层:通用网络抖动交给 RetryMiddleware,业务失败交给 errback,异常趋势交给 stats 和告警。重试能提高完整率,但它不是反爬、权限和页面不存在的解药。先识别失败类型,再决定是否重试,爬虫才会稳定。
服务端阅读 05月31日 00:57

Scrapy 项目怎么写才更稳定?有哪些最佳实践?

Scrapy 最佳实践先从边界开始Scrapy 项目最怕一开始跑得很快,过两周却没人敢维护。稳定的爬虫不是靠把并发拉满,而是把抓取边界、请求节奏、数据结构和失败处理提前定好。尤其是面向线上站点时,robots.txt、下载延迟、并发数和重试策略不是装饰配置,它们决定项目能不能长期运行。一个比较稳的起步配置可以这样写:ROBOTSTXT_OBEY = TrueCONCURRENT_REQUESTS = 16CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 0.5AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_TARGET_CONCURRENCY = 2.0LOG_LEVEL = "INFO"USER_AGENT = "mycrawler/1.0 (+contact@example.com)"这里没有万能数字。新闻站、文档站、电商站承压能力完全不同,最佳实践不是照抄参数,而是先用小流量观察响应时间、错误码和封禁情况,再逐步调大。很多“爬虫不稳定”的问题,其实是没有灰度过程。数据层也要尽早规范。Item 字段最好明确含义,pipeline 负责校验、去重和入库,spider 只做页面解析。把清洗逻辑写满 spider 的短期效率很高,但字段一多,后面改一次规则要翻十几个回调函数。ITEM_PIPELINES = { "myproject.pipelines.ValidateItemPipeline": 200, "myproject.pipelines.DeduplicatePipeline": 300, "myproject.pipelines.SaveToPostgresPipeline": 500,}选择器和字段规则也要留测试样本。页面结构一变,CSS/XPath 可能不会报错,只是悄悄返回空列表,这比直接失败更难发现。建议保存少量代表性 HTML,给关键解析函数写单元测试,再用小批量线上 URL 做集成检查。这样后面目标站改版时,你至少知道是选择器坏了、接口变了,还是反爬策略变了。另一个经常被忽视的实践是把运行参数显式化。比如采集日期、入口 URL、批次号、是否全量,都通过 -a 参数或配置传入,不要硬编码在 spider 里。这样同一份代码既能跑日常增量,也能跑一次性补采,日志和数据里还能追溯来源。边界是参数越多越容易误传,关键参数最好在 from_crawler 或 __init__ 里校验,不合法就尽早失败。追问并发数和下载延迟怎么取舍?并发高能提高吞吐,但也会放大目标站压力、错误率和封禁概率。下载延迟能让请求更温和,却会拉长任务时间,数据时效性要求高的业务可能接受不了。我的做法是先固定单域名并发,再用 AutoThrottle 看延迟变化,而不是一上来把 CONCURRENT_REQUESTS 调到很大。判断标准不是“跑得快”,而是 2xx 比例、平均响应时间和被限流次数是否稳定。User-Agent 池和代理池是不是必备?不是。公开文档、开放站点、内部站点通常不需要复杂代理池,反而应该用清晰的 User-Agent 和联系方式。代理池适合目标站有地域限制、频控严格或业务确实需要高吞吐的场景,但它会带来 IP 质量、成本、失败率和合规风险。常见踩坑是代理不可用时没有熔断,Scrapy 反复重试,最后把错误流量放大。先判断是否真的需要代理,再设计代理失败后的降级策略。为什么建议用 scrapy shell 测选择器?scrapy shell 能快速验证 CSS/XPath,不用每改一次就启动完整任务。它特别适合处理页面结构不稳定的站点,可以马上看出选择器是否拿到了空值、重复节点或脏文本。边界是 shell 只能验证单页,不能代表翻页、登录态、异步接口都没问题。上线前仍然要跑小批量任务,检查 item 完整率和异常日志。日志应该记录到什么程度?日志太少,线上问题只能猜;日志太多,磁盘和检索成本都会上来。建议 INFO 记录任务阶段、抓取数量、关键参数,WARNING 记录字段缺失、重试变多、响应异常,ERROR 记录真正影响数据完整性的失败。不要在日志里打印大量 HTML 或敏感 cookie,这类坑排查时很常见。生产环境还要让日志带上 spider 名、批次号和请求 URL,后续定位会快很多。分布式爬虫什么时候再引入?只有当单机 Scrapy 的瓶颈已经明确,比如队列太大、任务窗口太短、单机带宽或 CPU 不够,再考虑 scrapy-redis 或自研调度。分布式会带来去重一致性、任务恢复、节点监控和数据幂等问题,不是简单“多开几台机器”。如果业务每天只抓几万页,先把单机调度、pipeline 和监控做好更划算。过早分布式,往往会把一个小问题拆成三台机器上的大问题。小结Scrapy 最佳实践不是一组漂亮配置,而是一套工程习惯:尊重目标站、控制节奏、拆清职责、记录关键指标、先小流量验证。只要这些基础稳住,后面无论加代理、分布式还是监控,都不会把项目推向不可维护。
服务端阅读 05月31日 00:57

Scrapy 扩展机制怎么用才不和中间件混在一起?

Scrapy 扩展到底负责什么?Scrapy 的扩展机制适合处理“爬虫生命周期级别”的事情,比如启动时加载配置、运行中记录统计、关闭时发通知、把关键指标推到监控系统。它不是用来改每个请求和响应的;如果逻辑要拦截 Request、Response、异常或代理,那通常应该放在 downloader middleware 或 spider middleware。这个边界很重要,很多项目后期变乱,就是因为把告警、埋点、请求改写都塞进一个类里,最后谁也不敢动。扩展本质上是一个普通 Python 类,Scrapy 通过 from_crawler 创建实例,并让它订阅信号。常见信号包括 spider_opened、spider_closed、item_scraped、request_dropped 等。你可以把它理解成 Scrapy 项目的“事件监听器”:平时不参与下载链路,等事件发生时再做自己的事。from scrapy import signalsclass StatsAlertExtension: @classmethod def from_crawler(cls, crawler): ext = cls(crawler.stats, crawler.settings) crawler.signals.connect(ext.opened, signal=signals.spider_opened) crawler.signals.connect(ext.closed, signal=signals.spider_closed) return ext def __init__(self, stats, settings): self.stats = stats self.threshold = settings.getint("ALERT_ITEM_MIN", 1) def opened(self, spider): spider.logger.info("stats alert extension enabled") def closed(self, spider, reason): count = self.stats.get_value("item_scraped_count", 0) if count < self.threshold: spider.logger.warning("too few items: %s, reason=%s", count, reason)启用扩展时写进 EXTENSIONS,数字是优先级,值越小越早加载。项目里建议把自定义扩展放在独立模块,别直接堆在 spider 文件里,否则复用和测试都很麻烦。EXTENSIONS = { "myproject.extensions.StatsAlertExtension": 500,}ALERT_ITEM_MIN = 20如果扩展要依赖配置,最好在 from_crawler 阶段读取并校验。配置缺失时可以抛出 NotConfigured,这样 Scrapy 会明确告诉你扩展没有启用,而不是跑到一半才出现空值错误。需要注意的是,扩展越靠近全局治理,越应该保持“可关闭、可降级”。告警接口挂了不应该让采集任务整体失败,除非这就是业务要求。如果团队里有多个爬虫共享同一套监控规则,可以把阈值、告警开关和通知目标都放进 settings,再由扩展读取。这样开发环境可以只打日志,生产环境再接入真实告警,避免本地调试时频繁打扰别人。另一个实用做法是把扩展输出的统计字段命名固定下来,比如 business/empty_item_count、business/invalid_price_count,后续接 Grafana 或日志平台时不会因为字段名变化而断图。追问扩展和中间件应该怎么取舍?扩展看全局生命周期,中间件看单次请求链路,这是最稳的判断标准。比如统计最终采集量、爬虫结束发企业微信通知,用扩展更自然;给请求加代理、处理 403、改 User-Agent,就应该用中间件。边界踩错以后,扩展可能变成“万能工具类”,调试时你会发现请求还没发出去,告警逻辑却先影响了调度。我的经验是:只要函数参数里必须拿到 request 或 response,先别急着写扩展。from_crawler 里为什么经常连接 signals?因为扩展实例需要拿到 Scrapy 的运行上下文,包括 settings、stats、signals、engine 等对象。直接在 __init__ 里 new 一个扩展也能写,但拿不到 crawler,就没法按 Scrapy 的方式订阅事件。这里的坑是信号函数签名要和信号匹配,少写参数可能运行到关闭阶段才报错。建议先从 spider_opened 和 spider_closed 两个信号开始,确认日志正常后再增加 item 或 request 相关信号。扩展里能不能做耗时操作?能做,但要非常克制。Scrapy 基于事件循环运行,扩展里长时间同步请求外部接口,会拖慢爬虫关闭、item 处理甚至整个 reactor。告警、上报监控这类操作最好设置短超时,或者丢给队列、后台服务处理。特别是 spider_closed,很多人会在这里上传文件、发报表,一旦接口卡住,任务看起来就像“明明爬完了却不退出”。自定义统计指标放在哪里更合适?如果指标和生命周期相关,放扩展里比较清楚,比如启动时间、关闭原因、最终 item 数、错误比例。若指标来自某个 item 清洗步骤,也可以在 pipeline 中 stats.inc_value(),扩展最后统一读取并汇总。取舍点在于数据产生的位置:不要为了集中管理,把所有业务 pipeline 都反向依赖扩展。否则扩展一改,数据入库链路也跟着抖。生产环境使用扩展有什么边界?扩展适合补齐监控和治理,不适合承载核心业务解析。它可以检查 item 数是否异常、记录关闭原因、输出错误分布,但不应该决定页面怎么解析、数据怎么清洗。另一个常见坑是配置优先级和内置扩展冲突,导致 Telnet、CoreStats 等行为被误关。上线前至少跑一次小流量任务,确认 stats、日志、告警都按预期出现。小结Scrapy 扩展的价值在于把生命周期治理从 spider 里拆出来。它越像事件监听器,项目越好维护;它越像业务大杂烩,后期越难排查。用它做统计、告警、监控和收尾动作,用中间件处理请求响应,用 pipeline 处理数据,这个分工通常最省心。
服务端阅读 05月31日 00:57

Scrapy CrawlSpider 适合爬哪些网站?

直接答案CrawlSpider 是 Scrapy 里用规则自动跟链接的 Spider,适合网站结构清楚、链接规律稳定、需要从列表页一路爬到详情页的场景。它的核心是 Rule 和 LinkExtractor:前者定义“哪些链接要跟、用哪个回调处理”,后者负责从页面里提取符合条件的链接。普通 Spider 更像你手写路线图,CrawlSpider 更像给爬虫装了导航规则。它能减少重复代码,但也更容易因为规则写得太宽,把不该爬的登录页、搜索页、标签页甚至日历页一起卷进去。from scrapy.linkextractors import LinkExtractorfrom scrapy.spiders import CrawlSpider, Ruleclass NewsSpider(CrawlSpider): name = "news" allowed_domains = ["example.com"] start_urls = ["https://example.com/news/"] rules = ( Rule(LinkExtractor(allow=(r"/news/page/\d+/",)), follow=True), Rule(LinkExtractor(allow=(r"/news/\d+\.html",)), callback="parse_article", follow=False), ) def parse_article(self, response): yield {"title": response.css("h1::text").get(), "url": response.url}使用 CrawlSpider 时不要覆盖 parse(),因为它被内部规则系统使用。需要解析详情页就写 parse_article 这类自定义回调,并在 Rule 里指定 callback。规则越精确,爬虫越可控;规则越宽,开发越省事,但队列失控、重复抓取和误入无效页面的概率也越高。生产里最好配合 allowed_domains、深度限制、去重日志和 URL 采样检查一起用。追问CrawlSpider 和普通 Spider 怎么选?如果网站路径固定、你只需要列表页翻页和详情页解析,CrawlSpider 会更省代码。若流程依赖复杂参数、登录状态、多接口组合,普通 Spider 通常更清晰。CrawlSpider 的优势是自动发现链接,代价是规则调试成本更高。边界很简单:链接结构能用正则稳定描述,就可以考虑 CrawlSpider;每一步都要业务判断,就别硬套。Rule 里的 follow 到底是什么意思?follow=True 表示这个规则匹配到的页面下载后,还会继续从响应里提取新链接。follow=False 则通常用于详情页,只解析数据,不再从详情页继续扩散。踩坑点是详情页如果误设成 follow=True,页面里的推荐文章、广告链接、标签链接可能继续扩散,队列会变得很脏。取舍上,列表、分类、分页可以 follow,详情、附件、用户页一般不要 follow。LinkExtractor 的 allow 和 deny 应该怎么写?allow 用来圈定你想要的 URL 模式,deny 用来排除明显不需要的路径。规则不要写得只看“能匹配”,还要看后续维护者能不能看懂。比如 /news/\d+\.html 比一个巨长的通配正则更安全,也更容易排查。常见坑是忘了排除 ?reply=, #comment, /login 这类链接,结果爬虫在无价值页面里打转。为什么不应该覆盖 CrawlSpider 的 parse 方法?CrawlSpider 的 parse 已经承担了按 Rule 分发响应、提取链接、生成后续请求的职责。你覆盖它之后,规则系统可能直接失效,表现为 start_urls 能访问,但后续链接不再跟进。正确做法是写新的回调函数,比如 parse_item、parse_article,再在 Rule 里引用。这个坑很常见,因为普通 Spider 里大家习惯写 parse,迁移到 CrawlSpider 时容易顺手复制旧代码。如何防止 CrawlSpider 爬过界?先设置 allowed_domains,再把 LinkExtractor 的 allow 写窄,不要只依赖域名限制。其次配置深度、下载延迟和并发,避免规则失控时给目标站造成压力。上线前抽样打印命中的 URL,确认分页、详情、排除路径都符合预期。对于大型站点,建议先跑小范围分类页,验证规则后再扩大入口,否则一次错误规则就可能制造几十万条无效请求。CrawlSpider 还适合做“半自动发现链接”的任务:你知道大概范围,但不想手写每一种分页入口。上线前可以先把 callback 里只打印 URL,不写库也不下载大文件,观察几百个命中的链接是否符合预期。规则稳定后再打开 item 解析和持久化,这一步能省掉很多返工。对内容站来说,CrawlSpider 的收益很明显;对搜索结果页、筛选页特别多的站点,规则必须保守,否则参数组合会把队列撑爆。如果网站有多语言、多地区或移动端路径,CrawlSpider 的规则最好显式写出允许范围。不要只写 allow=(r"/article/",) 就上线,因为相似路径可能包含预览页、打印页、AMP 页和评论页。重复内容多时,Scrapy 的请求去重只能处理 URL 级重复,正文级重复还要靠业务字段或内容哈希。这个边界分清楚,CrawlSpider 才会从“自动乱爬”变成可靠的站内采集工具。最后,别把 CrawlSpider 当成反爬解决方案。它只是链接发现和请求调度的封装,并不会自动处理登录、验证码、限速或动态渲染。遇到需要登录的站点,仍然要先完成会话管理;遇到 JS 渲染内容,也要决定抓接口还是接浏览器。把这些能力拆开设计,规则负责找路,回调负责解析,中间件负责通用请求问题,项目会更容易维护。
服务端阅读 05月31日 00:57

Scrapy 遇到 JavaScript 动态网页怎么办?

直接答案Scrapy 本身不会执行 JavaScript,它拿到的是服务器直接返回的 HTML。遇到动态网页时,第一步不是立刻上 Selenium 或 Playwright,而是打开浏览器开发者工具,找页面真正请求的数据接口。如果数据来自 XHR 或 Fetch,请优先用 Scrapy 直接请求接口;只有内容必须经过浏览器渲染、签名依赖运行时环境、或交互流程很重时,才把 Playwright、Selenium、Splash 接进来。这个取舍很重要,因为浏览器渲染的成本通常比普通 HTTP 请求高一个数量级。# settings.pyDOWNLOAD_HANDLERS = { "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler", "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",}TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"PLAYWRIGHT_BROWSER_TYPE = "chromium"# spider.pyyield scrapy.Request( "https://example.com/list", meta={"playwright": True, "playwright_include_page": False}, callback=self.parse,)如果能抓接口,就直接复用接口参数、请求头、分页字段和必要 token。接口方案快、稳定、并发高,也更容易做重试和监控。浏览器方案适合少量复杂页面,但要限制并发、设置超时、关闭页面资源,否则内存会很快涨上去。实际项目里常见的做法是混合:列表接口用 Scrapy 抓,少数详情页或反爬校验页才走 Playwright。追问Selenium、Playwright 和 Splash 怎么选?新项目优先考虑 Playwright,因为它支持现代浏览器、异步能力好,和 Scrapy 集成也更顺。Selenium 生态成熟,适合已有浏览器自动化脚本或必须兼容特定驱动的团队。Splash 较轻,适合简单渲染和 Lua 脚本控制,但面对复杂前端应用时调试体验不如真浏览器。取舍边界是交互复杂度和吞吐量:越像真实用户操作,越偏 Playwright;越像批量渲染 HTML,越要控制成本。为什么页面在浏览器能看到,Scrapy 里却没有?因为浏览器看到的是 HTML、JS、接口数据和运行时状态合成后的结果,而 Scrapy 默认只拿第一份 HTML。你应该先查看 Network 面板,过滤 XHR/Fetch,看数据是不是来自 JSON 接口。还要检查接口是否依赖 cookie、Referer、签名参数、时间戳或 Authorization。一个常见坑是只复制接口 URL,没有复制必要请求头,导致接口返回空数据或风控页面。使用 Playwright 会带来哪些坑?最大坑是资源泄漏,页面、上下文或浏览器实例没有关闭时,爬虫跑一段时间就会内存暴涨。第二个坑是等待条件写得太宽,比如固定 sleep 三秒,既慢又不稳定。更好的方式是等待某个选择器、接口响应或 DOM 状态出现。还要注意并发数不能沿用纯 Scrapy 的配置,浏览器并发通常要小得多,否则机器 CPU 和目标站风控都会先扛不住。动态网页一定要完整渲染吗?不一定。很多页面只是首屏用 JS 拉接口,真正数据在 JSON 里,完整渲染反而是绕远路。只有当数据经过前端计算、加密逻辑藏在 JS 里、或必须点击展开后才出现时,渲染才有价值。边界判断可以很朴素:如果 Network 里能稳定复现接口,就抓接口;如果接口参数无法还原,再考虑浏览器。不要为了“像真人”而默认渲染所有页面,那会让爬虫变慢、变贵、也更难排错。如何让动态页面爬虫更稳定?先把请求链路拆清楚:入口页、接口、详情页、登录态、反爬校验分别记录日志和状态码。对浏览器渲染请求设置单独超时和重试,不要让一个页面卡住整个调度器。静态资源如图片、字体、视频可以拦截掉,减少带宽和内存压力。配置上建议把浏览器并发、普通请求并发分开看,别用一个 CONCURRENT_REQUESTS 解决所有问题。动态网页还有一个现实问题:你看到的“渲染失败”不一定是 JS 没执行,可能是接口被风控、地区不匹配、账号权限不足或首屏骨架屏还没消失。排查时最好保存三类证据:原始 HTML、关键接口响应、渲染后的截图。只看最终选择器为空,很容易误判方向。对于大型采集任务,可以先用少量 URL 跑 Playwright 验证渲染路径,再把能还原的接口逐步替换成普通 Scrapy 请求。这样浏览器只承担兜底角色,整体成本会可控得多。配置层面还要把浏览器请求单独标记出来,方便统计成功率和耗时。比如在 meta 里加 rendered=True,日志里区分普通请求和渲染请求。遇到超时、空页面、验证码时,不要无限重试浏览器请求,可以降级保存现场交给人工分析。浏览器池本身也要定期重启,否则长时间运行后可能出现句柄泄漏、缓存膨胀和页面上下文污染。
服务端阅读 05月31日 00:57

Scrapy 请求去重是怎么判断重复的?

直接答案Scrapy 的请求去重由调度器调用 dupefilter 完成,默认实现是 RFPDupeFilter。它会为请求生成 fingerprint,通常由规范化后的 URL、请求方法、请求体组成;指纹已经出现过,就认为这个请求重复,不再入队。它解决的是“同一个请求不要重复抓”,不是“同一条业务数据不要重复入库”。所以列表页、详情页、翻页链接能靠它减少浪费,但商品 ID、文章 ID、用户 ID 的业务级去重,还应该放在 pipeline 或存储层。# settings.pyDUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter"DUPEFILTER_DEBUG = FalseJOBDIR = ".job/article_spider" # 需要断点续爬时启用# spider.pyyield scrapy.Request(url, callback=self.parse_detail)yield scrapy.Request(url, callback=self.parse_detail, dont_filter=True) # 只给确实需要重复访问的请求默认去重对 URL 会做规范化,比如参数顺序不同但含义相同的 URL,通常会生成相同指纹。请求方法和 body 也参与计算,所以同一个接口的 GET 和 POST 不会被当成同一个请求。dont_filter=True 是绕过去重的开关,适合登录、分页入口刷新、重试某个状态页,但不能随手加;一旦在列表链接上滥用,调度队列会膨胀,甚至把站点反复打穿。追问Scrapy 去重和数据库唯一索引有什么区别?Scrapy 去重发生在请求入队前,目标是少发重复请求,节省带宽和时间。数据库唯一索引发生在数据写入时,目标是避免重复数据污染结果。两者最好同时存在,因为 URL 不重复不代表数据不重复,同一商品可能有 PC、移动端、活动页三个 URL。取舍上,请求去重提升爬取效率,业务去重保证数据质量,不能互相替代。为什么有些看起来一样的页面没有被去重?最常见原因是 URL 里有追踪参数、时间戳、随机数或不同排序参数,Scrapy 认为它们是不同请求。另一个原因是 POST body 不同,哪怕接口地址一样,也会生成不同指纹。遇到这种情况,不要先怪 dupefilter,应该先确认哪些参数影响内容,哪些只是噪声。噪声参数可以在生成请求前清理,或者自定义指纹规则,但清理过度会把真正不同的页面合并掉。什么时候需要自定义去重规则?当默认 URL 指纹无法表达业务唯一性时才需要自定义。比如搜索接口里 page、keyword 决定内容,而 _t、callback、utm_source 不决定内容,就可以在指纹里忽略后者。分布式爬虫也常把指纹放到 Redis,让多个 worker 共享已抓请求集合。边界是维护成本:规则越业务化,越容易在目标站改版后误杀请求,所以要给命中去重的样本留日志。from scrapy.dupefilters import RFPDupeFilterfrom w3lib.url import canonicalize_urlclass CleanQueryDupeFilter(RFPDupeFilter): def request_fingerprint(self, request): url = canonicalize_url(request.url, keep_blank_values=False) return self.fingerprinter.fingerprint(request.replace(url=url)).hex()dont_filter=True 应该怎么用才安全?它适合少量入口型或状态型请求,比如每次启动都要访问首页拿 cookie,或者轮询一个会变化的任务状态页。不要把它放在详情页和翻页请求上,否则同一个链接会被反复入队。一个实用边界是:如果这个请求的返回内容依赖时间、登录态或外部状态,可以考虑跳过去重;如果只依赖 URL,本该让去重生效。踩坑最多的是复制登录请求代码时把 dont_filter=True 一起复制到了所有请求。断点续爬时去重状态会保留吗?只有配置 JOBDIR 后,队列和去重指纹才会持久化到磁盘,爬虫重启后可以继续使用。没有 JOBDIR 时,默认去重集合在内存里,进程结束就没了。这个能力适合长任务,但不适合频繁变动的短任务,因为旧指纹可能让你误以为“怎么不爬了”。如果目标站内容更新很快,应该定期清理 jobdir,或把增量策略改成按更新时间、业务 ID 控制。还要注意,请求去重不是越激进越好。新闻站、论坛和电商列表经常会出现同一个 URL 在不同时间返回不同内容的情况,例如首页、热榜页、库存接口和价格接口。如果这些页面被默认指纹长期挡住,增量爬虫就会漏掉更新。更稳妥的做法是把“稳定详情页”和“会变化的入口页”分开:详情页交给默认去重,入口页按调度周期允许重复访问,再在解析出的业务 ID 上做增量判断。这样既不浪费大量详情请求,也不会因为去重太早而错过新数据。在分布式场景里,去重还会影响任务分配公平性。多个节点共享 Redis 指纹集合时,一个节点先写入指纹,其他节点就不会再抓同一请求,这能减少重复劳动。但如果指纹规则里混入了节点本地状态,结果就会变得不可预测。建议把指纹生成逻辑做成纯函数,只依赖 URL、方法、body 和明确保留的参数,部署前用一批样例 URL 做回归测试。
服务端阅读 05月31日 00:57

Scrapy 如何处理 Cookies 和多会话登录?

直接答案Scrapy 默认会通过 CookiesMiddleware 维护 cookie:同一个会话里的响应 Set-Cookie 会被保存,后续请求会自动带上。登录类爬虫通常用 FormRequest 先提交账号密码,再把登录后的请求接在回调里;如果要同时爬多个账号、多个店铺或多个地区,就用 meta['cookiejar'] 隔离会话。真正容易出错的地方不是“能不能带 cookie”,而是 cookie 什么时候该让 Scrapy 管、什么时候该你自己管。手动在 headers 里塞 Cookie 看起来快,但会绕开 Scrapy 的 cookie 合并逻辑,后续重定向、刷新 token、跨域跳转都可能变乱。import scrapyclass AccountSpider(scrapy.Spider): name = "account" custom_settings = {"COOKIES_ENABLED": True, "COOKIES_DEBUG": False} def start_requests(self): yield scrapy.FormRequest( "https://example.com/login", formdata={"username": "u1", "password": "p1"}, meta={"cookiejar": "u1"}, callback=self.after_login, ) def after_login(self, response): yield response.follow("/user/orders", callback=self.parse_orders, meta={"cookiejar": response.meta["cookiejar"]}) def parse_orders(self, response): yield {"url": response.url, "title": response.css("title::text").get()}如果站点把登录状态放在 localStorage 或 JS 变量里,单靠 cookie 不够,要先在浏览器抓包确认真正校验的是 cookie、Authorization header,还是隐藏接口里的 token。生产环境还要注意 cookie 的生命周期:短期任务放内存即可,跨天任务才需要持久化到 Redis、数据库或加密文件。保存 cookie 时不要把明文账号、验证码结果、敏感 token 写进日志,尤其不要开启 COOKIES_DEBUG 后直接把日志上传到公共平台。追问什么时候用 Scrapy 自动 cookie,什么时候手动传 cookies?能让 Scrapy 自动管理时优先让它管,因为它会处理 Set-Cookie、域名、路径、过期时间和重定向后的合并。手动传 cookies={...} 适合一次性请求,比如带一个固定地区、语言或实验分组标识。不要把完整 Cookie 字符串放进 headers,除非你明确知道不会再依赖响应里的 Set-Cookie。这个取舍的边界是会话是否会变化:会变化就交给中间件,不变化才手动给。多账号同时爬时 cookie 会不会串号?默认情况下,同一个 spider 会共用一个 cookie jar,所以多账号并发登录如果不隔离,确实可能出现 A 账号请求带上 B 账号 cookie 的事故。解决办法是为每个账号设置不同的 meta['cookiejar'],并在后续所有请求里继续传递这个值。踩坑点是 response.follow() 不会替你“记住业务身份”,忘了传 meta 就会回到默认 jar。并发越高,这类串号越隐蔽,最好把账号标识也写入 item 或日志方便排查。登录后仍然被跳回登录页,应该先查什么?先看登录响应是否真的成功,不要只看 HTTP 200,很多站点失败时也返回 200 但页面里写着错误提示。其次检查是否缺少 CSRF token、验证码、动态签名或必须的 Referer。再看后续请求是不是丢了 cookiejar,以及重定向过程中 cookie 域名是否从 www.example.com 变成了 example.com。如果这些都正常,再考虑是否存在设备指纹或风控,而不是一上来就换代理。cookie 需要持久化吗?短任务不建议持久化,内存 cookie 简单、干净,任务结束就释放,也不容易留下敏感信息。长周期采集、登录成本高或验证码昂贵时,可以把 cookie 加密后存储,并记录过期时间和账号状态。边界在于 cookie 失效成本和泄露风险:越敏感的站点越要少存、加密存、按需刷新。一个常见坑是复用过期 cookie 后误判为“选择器失效”,其实页面已经被重定向到登录页。Scrapy 能处理 JWT 或 Authorization 吗?能处理,但 JWT 不属于 cookie 机制,通常要放在请求头或接口参数里。登录后从响应 JSON、HTML 脚本或浏览器存储中取到 token,再在后续请求里加 Authorization: Bearer xxx。如果 token 会刷新,需要在中间件或回调里统一更新,避免部分请求继续使用旧 token。取舍上,纯接口站点用 header 管 token 更清晰;传统 Web 站点则继续让 cookie 中间件处理会话。还有一个容易被忽略的细节:Scrapy 的 cookie jar 是跟请求链路走的,不是跟账号对象自动绑定的。如果你从一个登录回调里拆出多个分页请求,分页请求都要带同一个 cookiejar;如果中途又发起刷新 token 或退出登录请求,也要确认它不会污染同组会话。对于需要定期续期的站点,可以把“检测是否登录失效”写成一个小函数,例如看到登录按钮、特定错误码或跳转地址时重新登录。这样比在每个解析函数里临时判断更稳,也能避免同一个账号被多个并发请求同时刷新,造成服务端把旧 cookie 全部作废。如果你要把 cookie 持久化,建议同时保存来源域名、账号标识、创建时间和最近验证时间,而不是只保存一串 cookie 值。恢复时先访问一个轻量的个人中心或状态接口确认有效,再进入正式抓取。这样做多了一次请求,但能避免大量业务请求都拿着失效会话去跑。对于涉及个人数据的网站,还要把 cookie 当成密码处理,权限、加密和清理策略都不能省。
服务端阅读 05月31日 00:56

Scrapy settings.py 里哪些配置最该优先调整?

Scrapy 的 settings.py 决定爬虫速度、稳定性、反爬风险和数据质量。新项目最该优先调整并发、延迟、超时、重试、请求头、robots、日志、pipeline、middleware 和环境覆盖方式。不要一开始就追求最快,先让目标站、代理池、数据库和自己机器都扛得住。速度可以逐步加,封禁和脏数据一旦出现,排查成本会高很多。基础配置先保持清楚BOT_NAME、SPIDER_MODULES、NEWSPIDER_MODULE 通常由项目生成,但要和部署项目名一致。Scrapyd 日志、任务和发布记录都会反复出现这些名字,命名混乱会让排查变难。ROBOTSTXT_OBEY 默认建议开启,是否关闭要看授权、目标站条款和数据用途。BOT_NAME = "news_crawler"SPIDER_MODULES = ["news_crawler.spiders"]NEWSPIDER_MODULE = "news_crawler.spiders"ROBOTSTXT_OBEY = True并发和延迟决定稳定性最常见的事故是把 CONCURRENT_REQUESTS 开太高,又把 DOWNLOAD_DELAY 设为 0。建议先用保守配置跑基线,观察状态码、平均延迟、item 数和失败率,再逐步调高。AutoThrottle 适合响应波动明显的网站,但它不是反爬万能药。CONCURRENT_REQUESTS = 16CONCURRENT_REQUESTS_PER_DOMAIN = 4DOWNLOAD_DELAY = 0.5RANDOMIZE_DOWNLOAD_DELAY = TrueDOWNLOAD_TIMEOUT = 20AUTOTHROTTLE_ENABLED = TrueAUTOTHROTTLE_MAX_DELAY = 10请求头、Cookie、重试分场景配置普通静态页面通常设置合理 UA 即可;登录态、地区化或个性化页面才可能需要 Cookie。Cookie 一旦过期,爬虫可能不报错,却一直抓登录页。重试也要控制,网络错误和 5xx 适合重试,403、验证码和参数错误不适合反复撞。USER_AGENT = "Mozilla/5.0 (compatible; ResearchBot/1.0)"COOKIES_ENABLED = FalseRETRY_ENABLED = TrueRETRY_TIMES = 2RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408]Pipeline 和 Middleware 别乱塞ITEM_PIPELINES 适合清洗、校验、去重和入库,DOWNLOADER_MIDDLEWARES 适合代理、请求头、限速和异常处理。优先级数字越小越先执行,顺序错了会导致代理没生效或脏数据先入库。Spider 负责页面解析,通用能力尽量沉到 pipeline 和 middleware。ITEM_PIPELINES = { "news_crawler.pipelines.ValidatePipeline": 200, "news_crawler.pipelines.MongoPipeline": 500,}DOWNLOADER_MIDDLEWARES = { "news_crawler.middlewares.ProxyMiddleware": 350,}生产环境可以用 -s 或环境变量覆盖配置,例如 scrapy crawl article -s LOG_LEVEL=INFO -s CONCURRENT_REQUESTS=8。多份 settings 文件能用,但容易漏改;部署系统注入环境变量通常更可靠。追问CONCURRENT_REQUESTS 越高越好吗?不是。并发提高会增加吞吐,也会放大限流、代理失败和数据库压力。合理做法是先跑稳定基线,再按错误率和延迟调高。踩坑是只看抓取速度,不看 pipeline 是否堆积。ROBOTSTXT_OBEY 要不要开?默认建议开启,特别是公开网页和长期采集任务。关闭它只是 Scrapy 不再自动检查 robots,不代表可以随意抓。实际要结合授权、条款、频率和用途判断。内部测试站或明确授权数据源可以按约定关闭。COOKIES_ENABLED 什么时候打开?页面依赖登录态、地区选择或会话校验时才考虑打开。普通静态页面不建议默认开启,因为请求状态会变复杂。打开后要监控 Cookie 过期和登录页误抓。最常见坑是爬虫运行正常,解析到的却是风控页。Retry 为什么不能无限加?重试能处理临时网络问题,不能解决封禁、验证码和页面结构变化。无限重试会浪费代理、拖慢队列,还可能让限流更严重。建议区分可重试状态码,并限制次数。遇到 403 或 429,应先查频率、代理和请求行为。settings.py 能放敏感信息吗?不应该。数据库密码、代理账号、Token 和 Cookie 应来自环境变量或密钥系统。写进 settings.py 只是省事,却容易提交到仓库。边界是本地可以提供示例值,生产值必须外部注入。日志里也不要把这些值打印出来。
服务端阅读 05月31日 00:56

Scrapy 爬虫运行中如何监控和定位问题?

Scrapy 监控要回答三个问题:爬虫是否还活着,数据产出是否正常,异常卡在哪一步。只看进程状态不够,因为进程可能还在跑,却一直拿到 403、验证码、空页面或登录页。生产环境至少要看请求量、状态码、失败率、item 数、入库数、运行耗时、队列积压和内存占用。指标少一点没关系,但每个指标都要能指导动作。先用 Stats 建基础盘Scrapy 自带 Stats Collector,会记录请求、响应、重试、异常和 item 数。最小成本的做法是在爬虫结束时把关键指标写进日志或监控系统。item_scraped_count 看产出,downloader/response_status_count/403 看封禁,downloader/exception_type_count/* 看网络异常。class StatsPipeline: def close_spider(self, spider): s = spider.crawler.stats.get_stats() spider.logger.info({ "items": s.get("item_scraped_count", 0), "requests": s.get("downloader/request_count", 0), "errors": s.get("log_count/ERROR", 0), })日志要能还原现场本地调试可以用 DEBUG,线上默认 INFO,关键异常才打 ERROR。日志里要带 spider、任务参数、URL、状态码、代理标识、解析阶段和 item 主键。不要把 Cookie、Token、手机号直接打到日志里,集中日志平台会放大泄露风险。LOG_LEVEL = "INFO"LOG_FORMAT = "%(asctime)s [%(name)s] %(levelname)s: %(message)s"RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 408]日志采集可以用 ELK、Loki,也可以先用本地滚动文件。边界是小项目不用一开始上大平台,但必须设置切割和保留天数,别让日志写满磁盘。用 Scrapyd API 做运行管理Scrapyd API 能查看任务、取消任务和读取日志,适合接内部管理后台。Telnet Console 适合临时排查运行中的 crawler 和 stats,但不要暴露公网。排查顺序建议从请求入口开始:先看状态码和响应内容,再看选择器,最后看 pipeline 和入库。curl http://127.0.0.1:6800/listjobs.json?project=news_crawlercurl http://127.0.0.1:6800/cancel.json -d project=news_crawler -d job=JOB_ID告警围绕业务结果告警不应只看崩溃。更有价值的是 30 分钟无新增、字段为空率升高、详情页成功率下降、重复率异常、磁盘快满。阈值先按一两周基线设置,别一开始太敏感,否则团队很快会忽略告警。追问Scrapy 自带 stats 够用吗?基础监控够用,比如请求数、状态码、重试、异常和 item 数。业务指标不够,比如字段为空率、价格异常和入库失败原因。它们应该在 pipeline 或 middleware 里补。边界是明细不要全塞指标,单 URL 细节更适合日志。item 数突然变 0 怎么查?先看请求是否成功,再看 403、429、302 是否异常。然后保存一条响应,确认是不是验证码、登录页或页面结构变化。最后再改 XPath 或 CSS 选择器。常见坑是直接改解析规则,真实原因却是代理池失效。日志和指标有什么区别?指标适合看趋势和触发告警,比如错误率上升。日志适合还原现场,比如某个 URL 为什么失败。只靠日志会被细节淹没,只靠指标又解释不了原因。取舍上,指标要稳定,日志要可检索。Telnet Console 能在线上开吗?能开,但必须限制监听地址和访问来源。它对查看 crawler、engine、stats 很方便。风险是权限太大,暴露出去等于把运行进程交给别人。更稳妥的是内网临时开启,排查结束关闭。告警太多没人看怎么办?删掉没有行动价值的告警,比如单次超时。保留连续无产出、失败率持续升高、磁盘快满这类会影响结果的问题。告警内容要带 spider、版本、任务参数和日志位置。踩坑是只告警失败不告警恢复,大家不知道问题是否结束。
服务端阅读 05月31日 00:56

Scrapy 项目上线后如何部署和管理爬虫?

Scrapy 上线不要只把代码丢到服务器然后执行 scrapy crawl。更稳的做法是先固定依赖、配置、日志和启动入口,再交给 Scrapyd、systemd、Supervisor 或 Docker 管理。小项目用 Scrapyd 发布和调度很快,团队项目更适合 Docker 加 CI/CD;如果任务很多,还要补上队列、监控、告警和回滚。部署的重点不是工具越多越好,而是出问题时能定位、能停止、能恢复。上线前先固定运行环境先确认项目能用同一套命令复现运行结果。依赖写进 requirements.txt 或 pyproject.toml,生产参数放环境变量,不要在服务器上临时安装和手改配置。发布前至少跑一次小范围任务,检查请求、解析、入库和日志路径。pip install scrapyd scrapyd-clientscrapyd-deploy default -p news_crawlercurl http://127.0.0.1:6800/schedule.json -d project=news_crawler -d spider=articleScrapyd 适合轻量管理Scrapyd 能发布项目、启动爬虫、查看任务、取消任务和读取日志,适合单机或少量机器。它的边界也明显:它不是完整调度平台,不负责复杂依赖、资源隔离和跨机器统一排队。生产环境里建议只放内网,不要把 6800 端口暴露到公网。任务参数可以通过 API 传入,但 Token、Cookie、数据库密码不要明文塞进调度参数。Docker 解决环境漂移如果经常出现“本地能跑,服务器不能跑”,Docker 更合适。镜像里固定 Python、系统库、项目依赖和启动命令,服务器只负责拉镜像并注入环境变量。代价是镜像构建、日志采集和资源限制要额外配置,特别是用 Playwright、Selenium、lxml 时要提前处理系统依赖。FROM python:3.11-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY . .CMD ["scrapy", "crawl", "article", "-s", "LOG_LEVEL=INFO"]进程管理和回滚不能省裸机部署可以用 systemd 或 Supervisor。它们能守护进程,但不要无脑自动重启;如果目标站返回大量 403,重启只会制造更多封禁和脏日志。每次发布保留上一个 egg、镜像 tag 或 release 目录,一次只改一个变量,比如只改代码、只改配置或只换代理池。追问Scrapyd 和 Docker 该怎么选?Scrapyd 更像 Scrapy 的轻量运行面板,适合快速发布、启动和停止爬虫。Docker 更强调环境一致,适合多人协作和依赖复杂的项目。两者可以组合:Scrapyd 跑在 Docker 里。坑是 Docker 不会自动解决调度、重试和限速问题。多台机器怎么管理版本?每次发布生成唯一版本号,比如 Git commit、构建编号或镜像 tag。调度记录 spider、参数、版本和机器,异常数据才能追到来源。不要每台机器手工拉代码,时间一长必然版本不一致。小团队可以先脚本同步,但多人发布要接 CI/CD。线上爬虫要自动重启吗?可以,但要限制条件和次数。临时网络失败适合重启,登录态失效、403 激增和代码异常不适合无限重启。告警里要写清失败原因和日志位置。踩坑最多的是把所有异常交给进程管理器,最后越重启封得越快。哪些配置不能写死?代理、数据库密码、Cookie、Token、运行环境和并发阈值都不该写死。它们应该来自环境变量、密钥系统或部署平台配置。非敏感默认值可以留在 settings.py。生产参数一旦写进代码,泄露和回滚都会很麻烦。如何判断部署成功?不要只看进程存在,要看任务被调度、请求成功、Item 入库、错误率正常。发布后可以跑一个测试 URL 或小时间窗口。成功标准最好脚本化,避免每次人工翻日志。Scrapy 常见坑是启动成功但没有数据,这比直接崩溃更隐蔽。
计算机基础阅读 05月31日 00:26

TCP 为什么需要三次握手?两次或四次不行吗?

TCP 三次握手的重点不是“发了三次包”,而是让双方确认三件事:对方能收、对方能发、双方的初始序列号都同步了。客户端先发 SYN,带上自己的 seq=x;服务端回 SYN+ACK,确认 x 并给出 seq=y;客户端再回 ACK,确认 y。到这一步,双方才有足够信息进入 ESTABLISHED,后续字节流才能可靠编号和确认。三次握手的过程怎么走?文字时序图可以这样看:客户端说“我要连,seq=x”;服务端说“收到 x,我这边 seq=y”;客户端再说“收到 y”。第一次后客户端进入 SYNSENT,第二次后服务端进入 SYNRCVD,第三次到达后双方进入 ESTABLISHED。ack=x+1、ack=y+1 不是随便加 1,而是 SYN 本身会占用一个序列号。为什么两次握手不够?两次握手的问题是服务端无法确认客户端是否收到了自己的 SYN+ACK。假设网络里一个过期 SYN 延迟到达服务端,如果两次就建立连接,服务端可能误分配资源,而客户端根本没有这次连接意图。第三次 ACK 能让服务端确认“客户端确实收到了我的序列号”。取舍是多半个往返,但换来更明确的状态确认。为什么不是四次握手?四次也能完成目标,只是没必要。服务端本可以先 ACK 客户端序列号,再单独发 SYN;TCP 把这两步合并成 SYN+ACK,少发一个报文。三次已经完成双方序列号同步和收发能力确认,再拆成四次只会增加成本。第三次 ACK 丢了会怎样?客户端通常已经认为连接建立,可以继续发送数据。服务端还停在 SYN_RCVD,等待 ACK 或重传 SYN+ACK;如果客户端后续数据包带 ACK,服务端也可能借此完成建连。边界是客户端不再发任何数据,服务端会超时重传,最后释放半连接资源。三次握手和 SYN Flood 有什么关系?服务端收到 SYN 后需要保存半连接状态,SYN Flood 就利用了这点。攻击者大量发 SYN 却不完成第三次 ACK,半连接队列可能被占满。防御常见做法包括 SYN Cookies、调大半连接队列、缩短重试、限流和流量清洗;取舍是队列调大会耗内存,SYN Cookies 也可能影响部分 TCP 选项能力。追问三次握手能证明应用层一定可用吗?不能,它只能证明传输层基本收发链路是通的。握手成功后,TLS、鉴权、线程池、数据库都可能继续失败。排查时要分清边界,“端口通”不等于“接口健康”,很多慢请求卡在握手之后。初始序列号为什么要随机?随机初始序列号能降低旧报文混入新连接的概率,也增加伪造 TCP 报文的难度。早期序列号可预测时,攻击者更容易猜中合法窗口。代价是双方必须在握手阶段交换序列号,抓包工具显示相对序列号只是为了方便阅读。服务端收到 SYN 后马上分配资源,会不会有风险?有风险,所以内核会限制半连接队列并设置超时重传。SYN Flood 正是利用这个窗口消耗资源。工程上不能只调 backlog,还要看 somaxconn、tcpmaxsyn_backlog、SYN Cookies 和应用 accept 速度。握手协商的参数会影响后续传输吗?会,MSS、窗口扩大、SACK、时间戳通常都在握手阶段协商。窗口扩大失败可能让高 RTT 链路吞吐上不去,SACK 不可用会降低多包丢失后的恢复效率。排查慢连接时,不能只看握手成功,还要看握手里协商出了什么能力。
计算机基础阅读 05月31日 00:26

TCP 首部有哪些关键字段?它们分别解决什么问题?

TCP 首部不是一串要死记的字段,而是 TCP 可靠传输的控制面板。端口决定数据交给哪个进程,序列号和确认号让字节流不乱,标志位表达连接状态,窗口、校验和、选项分别处理流量控制、差错检测和能力协商。最小首部 20 字节,带选项最多 60 字节;抓包时真正有用的不是背出字段名,而是看这些字段是否在按预期变化。TCP 首部整体长什么样?文字图示可以这样记:源端口和目的端口各 16 位,后面是 32 位序列号、32 位确认号,再往后是数据偏移、保留位、控制标志、窗口大小、校验和、紧急指针,最后才是可变长选项和数据。数据偏移说明首部在哪里结束,因为 MSS、窗口扩大、时间戳、SACK 等选项会改变首部长度。哪些字段负责定位和可靠性?源端口、目的端口配合源 IP、目的 IP,组成一条连接的四元组。IP 只能送到主机,TCP 还要把数据送到具体应用,例如浏览器临时端口连到服务器 443 端口。NAT、代理、容器网络可能改写端口,所以抓包端口不一定等于应用配置端口。序列号表示本段第一个数据字节的位置,确认号表示“下一步希望收到哪个字节”。ack=1001 通常代表 1001 之前都收到了,这是累积确认。SYN 和 FIN 也各占一个序列号,手算握手、挥手时这里最容易错。标志位、窗口和选项怎么影响排查?SYN 用于建立连接,ACK 表示确认号有效,FIN 是正常关闭,RST 更像异常中止。PSH 不等于强制立刻发送,URG 在现代应用里很少依赖。看到连接断开时,先区分 FIN 还是 RST,前者多是正常收尾,后者常见于端口未监听、应用拒绝或中间设备清理状态。窗口大小告诉对端还能接收多少数据,是流量控制的核心。原始窗口只有 16 位,高带宽高延迟链路通常要靠窗口扩大选项。校验和能发现传输损坏,但不是安全机制;MSS、SACK、时间戳这些选项,则会影响分段大小、丢包恢复和 RTT 估算。追问为什么 TCP 首部最小 20 字节、最大 60 字节?数据偏移字段只有 4 位,单位是 32 位字。最小值通常是 5,也就是 20 字节;最大值是 15,也就是 60 字节。边界在于 TCP 选项最多只有 40 字节,MSS、SACK、时间戳、窗口扩大都要在这里取舍。序列号为什么按字节编号,而不是按报文编号?TCP 给应用层的是连续字节流,不保留消息边界。按字节编号后,拆包、合包、乱序到达都能重新拼回正确顺序。代价是应用如果需要消息边界,必须自己加长度字段或分隔符,很多“粘包”问题就踩在这里。窗口大小是不是越大越好?不是,窗口太小会限制吞吐,窗口太大也可能让数据在接收端或链路上堆积。流量控制看接收缓冲区,拥塞控制看网络承载能力,两者不能混为一谈。调优要结合 RTT、带宽、丢包率和应用消费速度,而不是盲目把窗口调大。抓包时先看哪些字段最有用?建连先看 SYN、SYN+ACK、ACK,以及 MSS、SACK、窗口扩大是否协商成功。传输异常看 seq、ack、窗口、重复 ACK、重传和 RST。常见踩坑是只看服务端日志,以为服务慢;抓包才发现接收窗口归零,瓶颈其实在客户端消费太慢。
服务端阅读 05月31日 00:26

MQTT 是什么?它的核心特点和工作原理是什么?

MQTT 是一种基于 TCP 的轻量级消息协议,最常见于物联网设备、移动推送和实时状态同步。它的核心不是“像 HTTP 一样请求接口”,而是通过 Broker 做发布/订阅:设备把消息发布到主题,其他客户端订阅主题后由 Broker 推送消息。这个模式让设备不必知道彼此地址,也能在弱网、低带宽和大量连接场景下稳定通信。MQTT 为什么适合物联网?第一个特点是轻量。MQTT 固定头部最小只有 2 字节,比 HTTP 一大串 header 更省流量。对电池供电设备来说,少发一点数据、少建立几次连接,都会影响续航。它还通过 Keep Alive 维持长连接,Broker 可以主动把消息推给客户端,不需要客户端频繁轮询。第二个特点是发布/订阅。发布者只把消息发到 topic,例如 factory/line1/motor/temperature,订阅者通过主题过滤器接收自己关心的消息。Broker 负责连接管理、主题匹配、消息分发和 QoS 状态。这个设计天然支持一对多,比如一台设备上报状态后,监控系统、告警系统和数据存储服务都可以同时收到。第三个特点是可靠性交给 QoS 分级处理。QoS 0 最快但可能丢,QoS 1 保证至少到达但可能重复,QoS 2 尽量做到恰好一次但成本最高。实际项目里通常不是全部用最高级别,而是遥测用 QoS 0,告警和状态变更用 QoS 1,极少数关键命令才考虑 QoS 2。MQTT 的工作流程可以概括为四步:客户端 CONNECT 到 Broker,订阅者 SUBSCRIBE 主题,发布者 PUBLISH 消息,Broker 根据订阅关系转发。断线时,Broker 可以通过遗嘱消息通知其他系统;重连时,持久会话可以恢复订阅和未确认消息。它看起来简单,但真正上线时要同时考虑主题设计、认证授权、离线消息上限和消息幂等。它也不是所有实时通信的默认答案。浏览器前端更常用 WebSocket,服务端内部任务分发可能更适合 Kafka 或 RabbitMQ,MQTT 的强项是大量客户端长连接和主题路由。判断是否使用 MQTT,可以先问三个问题:设备是否经常在线保持连接、消息是否需要按主题推送、网络和功耗是否敏感。如果答案都是否定,HTTP 可能更简单。mosquitto_sub -h test.mosquitto.org -t 'levenx/demo'mosquitto_pub -h test.mosquitto.org -t 'levenx/demo' -m 'hello mqtt'初学者可以用公开测试 Broker 验证协议概念,但不要把它当作生产样板。生产 Broker 要考虑账号隔离、TLS 证书、ACL、限流、日志和监控,还要有备份和升级方案。MQTT 的入门门槛低,真正难的是长期稳定运行。越早把这些工程约束放进设计,后面越少返工。追问MQTT 和 HTTP 最大区别是什么?HTTP 主要是请求/响应,客户端问一次,服务器答一次。MQTT 是长连接加发布/订阅,Broker 可以在有消息时主动推给订阅者。取舍很明显:配置查询、文件上传、管理后台接口适合 HTTP;设备状态、实时告警和低带宽上报更适合 MQTT。很多系统会混用,别试图用一个协议解决所有问题。Broker 是不是单点?逻辑上 Broker 是中心节点,所以单机 Broker 确实可能成为单点。生产环境可以用集群、负载均衡和客户端自动重连降低风险。边界在于 MQTT 长连接有会话状态,故障切换不像无状态 HTTP 那么简单。要验证 Broker 高可用,必须实际测试节点宕机、网络抖动和客户端重连后的消息表现。MQTT 基于 TCP,为什么还需要 QoS?TCP 只能保证一条连接上的字节流可靠、有序,不能保证应用消息在断线、重连、Broker 转发和订阅者离线时符合业务预期。MQTT QoS 是应用层的交付语义,用来处理确认、重传和重复问题。踩坑点是以为 TCP 可靠就等于业务可靠,结果设备掉线时消息丢了还不知道。QoS 要和持久会话、离线队列、业务幂等一起看。MQTT 适合传大文件吗?不适合。MQTT 更适合小消息、高频状态和控制指令,大文件会占用 Broker 内存、网络和队列资源。文件上传、固件下载更适合 HTTP、对象存储或专门的 OTA 通道。实际取舍是:MQTT 可以发文件地址、版本号和下载指令,但不要把固件二进制直接塞进 MQTT payload。新手接入 MQTT 最容易忽略什么?最容易忽略主题规范和安全配置。刚开始大家会用 test/#、匿名连接和公网 1883 调试,跑通很快,但上线后权限和排障都很痛苦。另一个坑是没有给消息设计唯一 ID,遇到 QoS 1 重复投递时无法去重。把 Client ID、topic、QoS、ACL 和日志字段提前约定好,比后期补救省很多时间。
服务端阅读 05月31日 00:26

MQTT QoS 0、1、2 有什么区别?实际项目该怎么选?

MQTT QoS 解决的是“消息交付可靠性和成本怎么平衡”的问题。QoS 0 是最多一次,速度最快但可能丢;QoS 1 是至少一次,能保证到达但可能重复;QoS 2 是恰好一次,流程最完整但开销也最大。实际项目里不是 QoS 越高越好,而是要看消息丢失、重复和延迟哪个代价更高。三种 QoS 的核心区别QoS 0 只有一条 PUBLISH,没有确认报文。发布者把消息发出去就算完成,网络抖动、客户端断开、Broker 繁忙都可能导致消息丢失。它适合高频遥测,比如温度、湿度、定位点,因为下一条数据很快会覆盖上一条。用 QoS 0 的好处是吞吐高、延迟低、设备耗电少。QoS 1 会多一个 PUBACK。发布者发送 PUBLISH 后等待确认,如果没收到 PUBACK,就会重发。这样能提高送达概率,但接收方可能收到重复消息,所以业务处理必须幂等。比如告警消息、状态变更、日志上报一般可以用 QoS 1,但要给消息带上 messageId 或事件编号。QoS 2 使用 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步握手。它的目标是避免重复交付,适合不能重复执行的关键指令。问题是每条消息要更多报文和状态,延迟、内存和磁盘成本都会增加。很多系统口头说“必须恰好一次”,最后真正需要 QoS 2 的消息其实很少。还要注意一个边界:MQTT 的 QoS 是客户端和 Broker 之间的交付保证,不等于你的业务端到端一定成功。Broker 收到了消息,不代表后端数据库写入成功;订阅者收到了消息,不代表业务处理完成。真正关键的业务还要在 payload 里设计业务流水号、状态机和补偿机制。QoS 还会影响设备功耗和 Broker 资源。移动网络或卫星链路下,QoS 1 的重传可能让设备反复唤醒无线模块,电池消耗会明显上升。Broker 侧也要保存未确认消息,连接越多、离线越久,堆积风险越大。所以 QoS 选择应该按主题分级,而不是全局一刀切。import paho.mqtt.client as mqttclient = mqtt.Client(client_id='device-001')client.connect('broker.example.com', 1883, 60)client.publish('device/001/event', '{"id":"evt-1001"}', qos=1)client.loop(timeout=1.0)client.disconnect()一个比较稳妥的策略是按业务主题设置默认 QoS。状态心跳、实时位置用 QoS 0,告警、配置结果用 QoS 1,涉及扣费、开锁、停机这类强约束命令再评估 QoS 2。即便使用 QoS 2,也不要省掉业务回执,因为设备收到命令和执行成功是两件事。把这两层分清,问题定位会容易很多。追问QoS 1 为什么会重复?QoS 1 的确认依赖 PUBACK,如果发布者发出了消息但没有收到确认,它只能假设消息没成功,然后重发。问题是接收方可能已经收到并处理了第一条,只是确认包在路上丢了。这个重复不是协议 bug,而是“至少一次”语义的必然代价。实际项目要用业务 ID 做去重,不能假设 QoS 1 永远只到一次。QoS 2 能不能保证业务绝对不重复?不能把协议层的恰好一次理解成业务层绝对不重复。QoS 2 主要保证 MQTT 报文交付过程不重复,但业务服务处理消息后可能重启、数据库提交可能超时、下游接口也可能重试。边界在于它管的是客户端到 Broker、Broker 到订阅者这段链路。关键业务仍然需要幂等写入和状态校验。为什么很多物联网数据用 QoS 0?传感器数据通常是连续上报的,单点丢失不会影响整体趋势。比如温度每 5 秒上报一次,丢一条比排队重传更能接受,因为旧数据很快失去价值。QoS 0 的优势是低延迟、低带宽、低功耗,适合电池设备。取舍是你要接受偶发丢包,并在服务端用时间窗口判断设备是否异常。控制命令应该用 QoS 2 吗?不一定。开灯、重启设备、下发配置这类命令更常用 QoS 1 加业务幂等,因为重复执行可以通过 commandId 防住。QoS 2 更适合重复执行会造成严重后果、且设备和 Broker 都能承受额外状态的场景。踩坑点是只提高 QoS,却没有处理命令超时、设备离线和执行回执。命令可靠性通常要靠 QoS、业务 ACK、超时重试一起完成。订阅 QoS 和发布 QoS 不一样会怎样?最终投递给订阅者的 QoS 通常取发布 QoS 和订阅 QoS 中较低的那个。比如发布者用 QoS 1,订阅者只订阅 QoS 0,Broker 会按 QoS 0 投递给它。这个规则容易被忽略,导致发布端以为消息可靠,消费端实际没有确认。排查时要同时看发布代码、订阅代码和 Broker 日志,不能只看一端配置。
服务端阅读 05月31日 00:26

MQTT 发布订阅是怎么工作的?主题、通配符和 Broker 怎么配合?

MQTT 的发布/订阅模式可以理解成“发消息的人不找具体接收者,只把消息交给主题;想要消息的人订阅主题”。发布者只负责把 payload 发到 topic,订阅者只声明自己关心哪些 topic,中间的 Broker 负责匹配和分发。这个设计把生产者和消费者解耦了,所以很适合设备多、上下线频繁、消息一对多的物联网场景。一条消息是怎么走的?流程并不复杂。订阅者先连接 Broker,然后发送 SUBSCRIBE,例如订阅 home/+/temperature。传感器作为发布者把温度发到 home/livingroom/temperature,Broker 发现这个主题匹配订阅规则,就把消息推给订阅者。发布者并不知道谁收到了消息,订阅者也不需要知道消息来自哪台设备,双方只通过 topic 间接关联。Topic 是 MQTT 路由的核心。它是用斜杠分隔的层级字符串,比如 tenant/a/device/001/status。主题区分大小写,Home 和 home 是两个主题。设计主题时不要只考虑今天的功能,还要考虑权限、统计、扩展和排障,否则后面 ACL 和数据分析都会很难做。通配符让订阅变得灵活。+ 匹配单层,例如 home/+/temperature 可以匹配客厅和卧室温度。# 匹配多层,但只能放在末尾,例如 home/# 能收到 home 下所有消息。通配符很方便,也很危险,生产环境要避免业务客户端随便订阅大范围主题。发布订阅不是消息队列的简单替代品。MQTT 更强调实时推送和连接管理,消息是否离线保存取决于会话、QoS 和 Broker 配置。多个订阅者订阅同一主题时,默认每个订阅者都会收到一份消息;如果想做负载均衡,需要使用共享订阅,例如 MQTT 5 常见的 $share/group/sensor/#。真实项目里还要区分“状态”和“事件”。状态可以用 retained message 保留最后一条,比如设备在线状态;事件则应该进入后端存储,比如告警流水和操作记录。把两者混在一起会出问题:新客户端上线后拿到一条 retained 告警,可能误以为刚刚发生。主题命名和 payload 里最好明确消息类型。mosquitto_sub -h localhost -t 'home/+/temperature'mosquitto_pub -h localhost -t 'home/livingroom/temperature' -m '25.6'mosquitto_pub -h localhost -t 'home/kitchen/humidity' -m '60%'发布订阅还有一个好处是便于旁路扩展。原来只有监控服务订阅设备状态,后来新增告警、数据清洗或调试工具,只要再订阅同一类主题即可,不需要改发布端代码。不过这也带来治理问题:谁订阅了什么、是否还在消费、是否造成重复处理,都需要有可观测性。Broker 侧的订阅列表和消费延迟应纳入日常排查。追问发布者和订阅者真的完全不知道彼此吗?协议层面是解耦的,发布者不需要保存订阅者列表,订阅者也不直接连接发布者。业务层面通常还是会约定 payload 格式、主题命名和设备身份,否则收到消息也不知道怎么处理。取舍在于灵活性和治理成本:解耦让扩展容易,但主题规范一旦缺失,系统会变成没人敢改的消息网。主题应该怎么设计才不容易后悔?建议把租户、产品、设备和方向放进主题,例如 tenant/{tid}/product/{pid}/device/{id}/up。这样 ACL 可以按路径限制,日志也容易按设备定位。不要把大量业务字段塞进 topic,比如温度值、时间戳应该放 payload,不该放主题。边界是 topic 适合做路由维度,不适合承载所有数据维度。+ 和 # 通配符有什么坑?+ 只匹配一层,# 匹配多层并且只能出现在末尾,这两个规则经常被写错。订阅 home/# 会收到 home 下几乎所有消息,调试时方便,生产里可能造成流量暴涨。还有一个坑是 ACL 放开了通配符订阅,普通设备就可能读到别人的数据。通配符应该更多给平台服务用,终端设备尽量订阅精确主题。发布订阅和点对点消息有什么区别?点对点模式通常知道明确接收者,消息只交给一个目标。MQTT 发布订阅默认是一对多,任何匹配订阅的客户端都能收到消息。它适合状态广播、设备上报、告警通知,不适合需要严格单消费者处理的任务队列。需要负载均衡消费时,可以用共享订阅,但仍要处理重复投递和消费幂等。为什么订阅后才收到消息?以前的消息去哪了?普通订阅只接收订阅建立之后的新消息,之前发布的消息不会自动补发。想让新客户端上线就拿到最近状态,可以用 Retained Message;想让离线客户端恢复后收到消息,要使用持久会话和合适的 QoS。踩坑点是把 retained 当历史消息,它只保留每个主题最后一条。真正的历史查询应该从数据库查,而不是指望 Broker 保存全部消息。
服务端阅读 05月31日 00:26

MQTT Broker 负责什么?Mosquitto、EMQX 和 HiveMQ 怎么选?

MQTT Broker 是 MQTT 系统里的中枢,不只是“转发消息”的服务器。它要维护客户端连接、处理认证授权、保存订阅关系、按主题路由消息,还要根据 QoS 管理确认、重传和离线消息。选 Broker 时不能只看宣传里的百万连接,更要看你的消息量、持久化要求、集群能力、运维团队是否能长期维护。Broker 到底做哪些事?第一件事是连接管理。客户端通过 CONNECT 建立长连接,Broker 要校验 Client ID、账号、证书和 Keep Alive。连接建立后,它还要发现客户端是否掉线,并在异常断开时发布遗嘱消息。连接数一多,文件句柄、内存、心跳间隔都会变成真实的容量问题。第二件事是主题路由。发布者把消息发到某个 topic,Broker 根据订阅关系找到匹配的订阅者。这里不只是字符串匹配那么简单,还涉及 +、# 通配符、共享订阅、保留消息和 ACL。主题层级设计得好,Broker 的规则就清晰;主题乱了,后面无论换什么产品都很难救。第三件事是可靠性和存储。QoS 1 要保存未确认消息,QoS 2 要维护更完整的握手状态,持久会话还要保存离线消息。很多人压测只测 QoS 0 在线消息,结果上线后一开持久会话,磁盘和内存马上顶不住。Broker 不是数据库,离线消息要设置过期和队列上限。常见实现里,Mosquitto 轻量、简单,适合边缘网关、实验室和小型项目。EMQX 功能完整,规则引擎、集群、管理界面都比较成熟,适合物联网平台。HiveMQ 企业能力强,商业支持好,适合预算充足、稳定性要求高的团队。RabbitMQ 的 MQTT 插件适合已有 RabbitMQ 体系的公司,但它不是专用 MQTT Broker,协议能力和超大连接场景要谨慎评估。选型时还要把运维能力算进去。Broker 需要监控连接数、订阅数、消息速率、队列堆积、认证失败和磁盘水位,不是启动一个容器就结束。规则引擎、桥接、Webhook 看起来方便,但每增加一条链路,就多一个延迟和失败点。小团队宁可先把核心链路跑稳,也不要一开始把所有高级功能都打开。docker run -d --name emqx -p 1883:1883 -p 18083:18083 emqx/emqx:latestmosquitto_sub -h localhost -t 'demo/#'mosquitto_pub -h localhost -t demo/test -m 'hello mqtt'如果只是学习 MQTT,可以先用 Mosquitto,因为它足够透明,日志和配置都容易理解。如果目标是业务平台,最好尽早验证 EMQX 或 HiveMQ 这类产品的认证、规则转发、监控和集群能力。不要等设备已经铺出去以后再换 Broker,客户端协议版本、证书、主题和重连策略都会牵一发动全身。Broker 选型越靠前做,迁移成本越低。追问Mosquitto 和 EMQX 最大区别是什么?Mosquitto 的优势是轻、小、部署快,几分钟就能跑起来。EMQX 更像平台型 Broker,集群、规则引擎、认证插件和监控能力更完整。取舍很直接:边缘侧或小项目用 Mosquitto 很舒服,中心平台和多租户接入更适合 EMQX。不要因为“未来可能百万连接”就一开始上复杂集群,运维复杂度也是真成本。Broker 能不能当消息队列长期存数据?不建议。MQTT Broker 可以保存离线消息、保留消息和 QoS 状态,但它的目标是实时分发,不是长期存储和复杂查询。历史数据应该落到时序数据库、对象存储或业务数据库里。踩坑点是离线设备太多时消息堆积,如果没有过期时间和队列上限,Broker 会被自己的可靠性功能拖垮。集群部署时最难的地方是什么?难点不是把多个节点启动起来,而是会话、订阅关系和消息路由如何在节点间同步。共享订阅、持久会话、QoS 1/2 都会让集群状态变重。边界是网络分区:节点之间一旦抖动,客户端可能重连到不同节点,重复投递和短暂不可达都要在业务侧兜底。实际项目要压测故障切换,而不是只压测正常吞吐。选 Broker 时要看哪些指标?至少看四个指标:并发连接数、每秒消息数、QoS 级别、消息大小。还要看认证方式、ACL 复杂度、持久化策略和监控告警能力。只报“百万连接”没有意义,因为一百万空闲连接和十万高频上报连接完全不是一个负载。选型时最好用自己的主题结构和真实 payload 做压测。RabbitMQ 插件适合 MQTT 场景吗?如果公司已经大量使用 RabbitMQ,只需要少量设备接入 MQTT,它可以降低系统数量。问题是 RabbitMQ 的核心模型不是为海量 MQTT 长连接设计的,通配符、会话、共享订阅等能力也要逐项确认。高并发设备接入、复杂 ACL 和物联网规则处理更适合专用 Broker。这里的取舍是复用现有基础设施,还是为 MQTT 场景单独建设更合适的接入层。
服务端阅读 05月31日 00:26

MQTT 通信如何保证安全?TLS、认证和 ACL 怎么搭配?

MQTT 安全不能只靠一个用户名密码。比较稳的做法是分三层:先用 TLS 保护链路,再用认证确认“谁连上来”,最后用 ACL 限制“它能发布和订阅什么主题”。如果消息本身很敏感,还要在应用层做加密或签名,因为 TLS 只保护传输过程,Broker 收到消息后仍然能看到明文。MQTT 安全主要防什么?MQTT 常见风险有四类:明文传输被抓包、弱口令被撞库、客户端越权订阅主题、伪造设备发送控制指令。1883 端口默认不加密,在测试环境很方便,但放到公网基本等于把账号和消息内容暴露出去。生产环境通常使用 8883 端口跑 MQTT over TLS,客户端校验 Broker 证书,避免连到假 Broker。认证解决的是身份问题。最常见的是用户名和密码,也可以使用 JWT、OAuth2 Token 或客户端证书。用户名密码实现简单,但必须配合 TLS,否则 CONNECT 报文里的凭据可能被截获。证书认证更适合设备数量可控、生命周期长的场景,缺点是证书签发、吊销和更新都要有流程。授权靠 ACL 落地。不要给设备订阅 # 或发布任意主题的权限,主题最好带上租户、产品、设备 ID,例如 tenant/a/device/001/up。服务端只允许设备发布自己的上行主题,只允许订阅自己的下行主题。这个规则看起来啰嗦,但能防止一个设备越权读取另一台设备的数据。还有一个经常被忽略的点是 Client ID。很多设备 SDK 示例会写死 client1,测试时没问题,上线后多个设备互相顶号,安全排查会非常混乱。Client ID 最好和设备身份绑定,并在 Broker 侧限制同一身份的连接策略。日志里也要保留客户端 IP、用户名、Client ID、订阅主题和拒绝原因,否则出事后只能猜。listener 8883certfile /etc/mosquitto/certs/server.crtkeyfile /etc/mosquitto/certs/server.keycafile /etc/mosquitto/certs/ca.crtallow_anonymous falsepassword_file /etc/mosquitto/passwdacl_file /etc/mosquitto/aclmosquitto_passwd -c /etc/mosquitto/passwd device001mosquitto_pub -h broker.example.com -p 8883 --cafile ca.crt -u device001 -P 'secret' -t tenant/a/device/001/up -m '{"temp":25}'还有一个实用做法是把安全配置分环境管理。开发环境可以使用本地 Broker 和临时账号,但预发、生产必须强制 TLS、禁止匿名、收紧 ACL,并把配置变更纳入审计。不要把 Broker 管理后台直接暴露到公网,管理端口应放在内网或 VPN 后面。安全不是一次性配置,证书过期、员工离职、设备报废都会让原本安全的系统慢慢变脆。追问只开 TLS 就够了吗?不够,TLS 只能说明链路是加密的,并不能自动说明客户端有业务权限。一个合法设备如果拿到了通配符订阅权限,仍然可以看到不该看的主题。实际项目里 TLS、认证、ACL 要一起上,少一层都会留下明显缺口。取舍在于成本:内网测试可以先用用户名密码,公网和多租户环境至少要 TLS 加 ACL。用户名密码和客户端证书怎么选?用户名密码适合设备多、接入快、需要后台批量生成凭据的系统,运维成本低。客户端证书更适合网关、工业设备、金融终端这类安全要求高且设备数量可控的场景。证书的坑在于过期和吊销,很多事故不是加密算法不安全,而是证书到期后设备大面积掉线。选择时要看你有没有完整的证书生命周期管理能力。ACL 规则最容易踩什么坑?最常见的坑是主题设计太随意,后面 ACL 无法精确表达权限。比如所有设备都往 data/upload 发消息,Broker 很难区分来源,只能把鉴权压力挪到应用层。更好的做法是从第一天就把租户、产品、设备 ID 放进主题路径。边界也要注意:+ 只能匹配一级,# 必须放在末尾,误用通配符会造成越权订阅。消息还需要端到端加密吗?如果 Broker 本身可信,TLS 通常已经够用,因为 Broker 需要读取主题并转发消息。如果消息经过第三方云 Broker,或者内容是医疗、金融、门锁指令,就要考虑应用层加密和签名。加密会带来密钥分发、调试困难和规则引擎无法解析 payload 的代价。实际取舍是:遥测数据通常只签名或走 TLS,关键控制命令更适合加签名、防重放和短有效期。如何发现 MQTT 安全配置有问题?先检查 1883 是否暴露公网,再检查是否允许匿名连接和弱密码。然后用测试账号尝试订阅 #、发布到别的设备主题,看 ACL 是否真的生效。日志里要关注频繁 CONNECT 失败、异常 Client ID、短时间大量订阅等行为。很多问题不是 Broker 没有安全能力,而是默认配置太宽松,上线前没有做一次越权测试。
服务端阅读 05月31日 00:26

MQTT 5.0 比 3.1.1 多了什么?哪些特性值得升级?

MQTT 5.0 不是把 3.1.1 推倒重来,而是在原有发布/订阅模型上补齐了工程化能力。它解决的重点不是“能不能发消息”,而是消息过期怎么表达、错误原因怎么定位、请求响应怎么关联、客户端能力怎么协商,以及高并发系统里如何做流量控制。最值得关注的新特性第一类是属性 Properties。MQTT 5.0 给很多控制报文增加了属性字段,可以携带内容类型、响应主题、关联数据、消息过期时间、用户属性等元信息。以前这些信息通常要塞进 payload,客户端和服务端各自约定格式;现在协议层有标准位置,跨团队和跨语言接入会少很多口头约定。第二类是会话和消息过期。MQTT 3.1.1 主要靠 Clean Session 表达是否保留会话,语义偏粗。MQTT 5.0 用 Session Expiry Interval 指定会话多久过期,用 Message Expiry Interval 指定消息多久后不再投递。对离线设备很有用:告警消息可以保留,过期的实时温度就没必要等设备上线后再补发。第三类是请求/响应模式。通过 Response Topic 和 Correlation Data,客户端可以发布请求,再从指定响应主题拿到结果。它不会把 MQTT 变成 HTTP,但能让设备配置读取、远程诊断、指令确认这类场景更规范。边界是它仍然是异步消息模型,超时、重试和权限仍要应用层设计。第四类是流量控制和诊断能力。Receive Maximum 可以限制未确认 QoS 1/2 消息数量,Maximum Packet Size 可以拒绝过大的报文,Reason Code 能告诉你是未授权、主题名非法还是服务器繁忙。3.1.1 里很多失败只像“连接断了”,5.0 至少能让排查方向更明确。第五类是共享订阅、主题别名和订阅标识符。共享订阅让多个消费者分摊同一类消息,适合后端处理集群;主题别名用数字代替长主题,适合高频上报;订阅标识符能帮助客户端判断消息命中了哪个订阅规则。升级时怎么取舍?如果系统只是低频传感器上报,3.1.1 已经稳定运行,升级收益可能不大。若你正在做多租户 IoT 平台、设备远程控制、复杂权限、批量消费者扩容或问题排查成本很高,5.0 的收益会明显。真正的成本在兼容性:Broker、SDK、网关、监控工具和设备固件都要确认支持程度。# paho-mqtt v2 示例:发布带 MQTT 5 属性的消息from paho.mqtt.client import Clientfrom paho.mqtt.packettypes import PacketTypesfrom paho.mqtt.properties import Propertiesclient = Client(protocol=5)client.connect('broker.example.com', 1883)props = Properties(PacketTypes.PUBLISH)props.MessageExpiryInterval = 30props.ContentType = 'application/json'client.publish('device/1/status', '{"online":true}', qos=1, properties=props)迁移前最好列一张能力清单:Broker 是否支持 5.0,客户端 SDK 是否支持属性,监控系统能否展示原因码,网关是否会丢弃用户属性。只升级协议版本但不改日志和告警,收益会被打折。更稳妥的方式是先在少量设备上启用消息过期、原因码和最大报文限制,确认没有兼容问题后再扩大范围。追问MQTT 5.0 能完全兼容 3.1.1 吗?不能理解成所有特性自动兼容。5.0 Broker 通常可以接受 3.1.1 客户端,5.0 客户端也可以按 3.1.1 协议连接旧 Broker,但 5.0 属性、原因码、会话过期等能力不会凭空生效。迁移时要确认每个 SDK 实际使用的 protocol version。踩坑点是 Broker 升级了,但边缘设备 SDK 仍按 3.1.1 连接,结果新特性根本没用上。Session Expiry 和 Clean Session 有什么区别?Clean Session 更像开关,要么清理,要么保留。Session Expiry 是时间维度,可以表达断开后保留 10 分钟、1 天或一直保留。这个能力适合移动网络不稳定的设备,短暂掉线不丢订阅和未确认消息。取舍是会话保留越久,Broker 存储和清理压力越大,不能无脑设置永不过期。Message Expiry 解决了什么实际问题?它解决的是“过期消息还要不要送”的问题。实时温度、位置、在线状态这类数据,过了几十秒再送可能没有意义,甚至会污染业务判断。设置过期时间后,Broker 可以在消息失效后停止分发。边界是关键指令不要随便设置太短,否则设备网络抖动时可能错过真正需要执行的命令。共享订阅是不是等同于 Kafka 消费组?它们目的相似,都是让多个消费者分摊消息,但语义和生态不一样。MQTT 共享订阅更轻量,适合 Broker 直接把消息分给后端实例;Kafka 消费组更强调日志存储、offset 和回放。若你需要长期保留、重放和批处理,Kafka 更合适。若只是把设备上报实时分摊给多个处理服务,MQTT 共享订阅就够用。为什么 Reason Code 对运维很重要?没有明确原因码时,连接失败经常只能猜:密码错、ACL 拒绝、协议版本不对、报文太大都可能表现为断开。MQTT 5.0 的 Reason Code 能把失败原因直接带回来,日志和监控可以按原因聚合。踩坑点是客户端要把原因码打印出来,不能只记录“connect failed”。否则协议给了诊断信息,应用层却把它丢了。
服务端阅读 05月31日 00:26

MQTT 有哪些控制报文?连接、发布和订阅流程怎么串起来?

MQTT 控制报文不是一张要死背的清单,而是一套围绕连接、发布、订阅、心跳和断开的状态机。理解它们的最好方式,是先把流程跑通:客户端用 CONNECT 建立连接,Broker 用 CONNACK 回应;发布消息用 PUBLISH,不同 QoS 会带出 PUBACK、PUBREC、PUBREL、PUBCOMP;订阅主题用 SUBSCRIBE/SUBACK,取消订阅用 UNSUBSCRIBE/UNSUBACK;空闲时靠 PINGREQ/PINGRESP 保活,正常退出用 DISCONNECT。报文类型怎么分组?连接类只有 CONNECT 和 CONNACK。CONNECT 里会带 Client ID、用户名密码、Keep Alive、Clean Session 或 MQTT 5.0 的会话过期设置,还可以带遗嘱消息。CONNACK 告诉客户端是否连接成功,失败时会返回原因码或返回码。发布类以 PUBLISH 为中心。QoS 0 只发 PUBLISH,不等确认;QoS 1 是 PUBLISH 加 PUBACK,保证至少一次;QoS 2 是 PUBLISH、PUBREC、PUBREL、PUBCOMP 四步,目标是恰好一次。这里最容易混淆的是 QoS 2 的“恰好一次”只针对 MQTT 投递流程,不代表业务处理绝对只发生一次。订阅类包括 SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK。SUBSCRIBE 可以一次带多个 Topic Filter,例如 home/+/temperature 或 factory/#。SUBACK 会逐个返回订阅结果,别只看报文到了没到,还要检查每个主题是否被授权。心跳和断开类包括 PINGREQ、PINGRESP、DISCONNECT。Keep Alive 到期前客户端需要发心跳;如果 Broker 在 1.5 倍 Keep Alive 时间内没收到任何控制报文,通常会认为连接失效。正常 DISCONNECT 不会触发遗嘱消息,异常断线才会触发,这个细节在告警系统里很重要。固定头部怎么看?所有 MQTT 控制报文都有固定头部,第一字节高 4 位是报文类型,低 4 位是标志位,后面是 Remaining Length。PUBLISH 的 flags 会携带 DUP、QoS、Retain,PUBREL、SUBSCRIBE、UNSUBSCRIBE 的固定标志位也有固定要求,写客户端或排查抓包时不能乱填。Byte 1: Message Type(4 bits) + Flags(4 bits)Byte 2+: Remaining Length(variable byte integer)Next: Variable Header + Payload# 用 mosquitto 快速观察订阅和发布流程mosquitto_sub -d -h test.mosquitto.org -t 'demo/packet'mosquitto_pub -d -h test.mosquitto.org -t 'demo/packet' -q 1 -m 'hello'一条消息会经过哪些报文?以设备上报一条 QoS 1 温度消息为例,客户端先 CONNECT,Broker 返回 CONNACK 后才算连接建立。订阅端发送 SUBSCRIBE,Broker 用 SUBACK 确认订阅结果;发布端发送 PUBLISH,Broker 收到后返回 PUBACK,同时按主题匹配把消息转发给订阅端。连接空闲期间客户端继续用 PINGREQ 保活,Broker 回 PINGRESP;设备正常下线时发送 DISCONNECT,Broker 就不会发布遗嘱消息。如果换成 QoS 2,发布链路会多出 PUBREC、PUBREL、PUBCOMP。这个流程看起来啰嗦,但它把“收到消息”和“释放消息”拆成两个阶段,避免网络抖动时双方状态不一致。代价也很明显:Broker 和客户端都要保存更多中间状态,吞吐下降,排查时也要关注 Packet Identifier 是否被复用或卡住。MQTT 5.0 里的变化MQTT 3.1.1 常说 14 种控制报文,MQTT 5.0 增加了 AUTH,并且给很多报文加了属性和原因码。AUTH 用于增强认证,适合需要多轮认证或重新认证的场景。原因码让 CONNACK、PUBACK、SUBACK、DISCONNECT 等报文能表达更细的失败原因,这对线上排障很有价值。追问QoS 0、1、2 分别会触发哪些确认报文?QoS 0 没有确认报文,消息发出后协议层就不再追踪。QoS 1 需要 PUBACK,所以断线或超时后可能重发,接收方要能处理重复消息。QoS 2 要经过 PUBREC、PUBREL、PUBCOMP,流程最完整但延迟和状态存储也最多。取舍很直接:越可靠,报文越多,吞吐和实现复杂度越受影响。CONNECT 里最容易配错哪些字段?Client ID、Clean Session、Keep Alive 和遗嘱消息最容易出问题。多个设备复用同一个 Client ID 会互相踢下线,看起来像网络不稳定。Keep Alive 设置太短会制造无意义心跳,太长又会拖慢离线检测。遗嘱消息要配合异常断线理解,主动 DISCONNECT 不会触发它。SUBACK 收到了就代表订阅成功吗?不一定。SUBACK 只是 Broker 回应了订阅请求,真正要看每个返回码或原因码。某些主题可能因为 ACL 被拒绝,客户端如果只判断“收到 SUBACK”就会误以为订阅成功。项目里建议把订阅结果写入日志,并在关键主题失败时直接告警。边界是:协议完成和业务可用不是同一件事。Remaining Length 为什么是可变长度编码?MQTT 面向小设备和小消息,固定用 4 字节表示长度会浪费空间。可变长度编码让小报文只用 1 个字节表示长度,大报文再逐步扩展。踩坑点是它最多占 4 字节,而且每个字节只有 7 位表示数值,最高位表示是否还有后续字节。自己写解析器时如果没处理非法超长编码,可能被恶意报文拖住。抓包时如何快速判断 MQTT 流程卡在哪里?先看 CONNECT 后有没有 CONNACK,没有就查网络、TLS、认证和协议版本。发布卡住时看 QoS:QoS 1 缺 PUBACK,QoS 2 缺哪一步就查对应的会话状态。订阅收不到消息时别只盯 PUBLISH,要检查 SUBACK 返回码、Topic Filter 是否匹配、Retain 和 ACL。排查边界是先确认协议层报文完整,再看业务载荷是否符合预期。
服务端阅读 05月31日 00:26

MQTT 和 HTTP 有什么区别?物联网场景该怎么选?

MQTT 和 HTTP 都跑在应用层,很多时候也都基于 TCP,但它们解决的问题不一样。HTTP 更像一次明确的业务请求:客户端问,服务端答,适合查数据、提交表单、上传文件和调用 REST API。MQTT 更像一个消息中转站:设备把消息发到主题,订阅者按主题接收,发布者不需要知道谁在听。核心区别是什么?最明显的区别是通信模型。HTTP 是请求/响应,天然以客户端发起为中心;MQTT 是发布/订阅,Broker 负责路由消息,可以把一条温度数据同时分发给监控面板、告警服务和数据入库服务。第二个区别是连接方式。HTTP/1.1 可以 Keep-Alive,HTTP/2 也能复用连接,但多数业务仍围绕“请求完成即返回”设计。MQTT 通常保持长连接,通过 Keep Alive 和 PINGREQ/PINGRESP 确认连接还活着,这对设备在线状态和服务端主动下发指令很关键。第三个区别是报文开销。MQTT 固定头部最小 2 字节,主题和载荷之外的额外开销很小;HTTP 请求头常常包含 Cookie、User-Agent、鉴权头等信息,几百字节很常见。对 4G 模组、NB-IoT、卫星链路或电池供电设备来说,这些差距会直接变成流量费和续航差距。场景怎么选?设备遥测、实时状态、告警推送、远程控制更适合 MQTT。例如一万个传感器每 5 秒上报一次温度,用 HTTP 轮询会制造大量连接和请求头开销,用 MQTT 长连接发布到 factory/line1/temperature 更自然。Web 页面、后台管理、文件上传、复杂查询更适合 HTTP。比如查询某台设备过去 7 天的历史曲线,用 HTTP API 带分页、筛选条件和缓存策略更好维护。实际项目里常见做法是混用:MQTT 负责实时上报和控制,HTTP 负责配置、报表、账号体系和历史查询。快速验证示例# 订阅温度主题mosquitto_sub -h test.mosquitto.org -t 'demo/device1/temp'# 另一个终端发布消息mosquitto_pub -h test.mosquitto.org -t 'demo/device1/temp' -m '25.6'# 同样的数据用 HTTP 提交curl -X POST https://api.example.com/devices/device1/temperature \ -H 'Content-Type: application/json' \ -d '{"value":25.6}'实战里的判断顺序选型时不要先问哪个协议更先进,而要先看数据流向、频率和失败后果。若设备每隔几秒上报一次状态,并且平台需要随时下发控制命令,MQTT 的长连接会让链路更简单。若用户只是偶尔打开页面查一次报表,HTTP 的一次请求一次响应更符合直觉,日志、鉴权、缓存和排障工具也更成熟。还要看团队的运维能力。MQTT 引入 Broker 后,要管理主题命名、ACL、离线消息、会话、重连风暴和保留消息;HTTP 则更多依赖 API 网关、负载均衡和服务端限流。很多项目不是败在协议本身,而是没有提前定义边界:哪些消息必须实时,哪些数据必须可追溯,哪些请求失败后允许重试。追问MQTT 一定比 HTTP 更省资源吗?不一定,要看连接生命周期和消息频率。高频、小包、需要服务端下发的场景,MQTT 的长连接和小头部优势明显。低频操作反而可能是 HTTP 更省心,因为不用维护在线状态、重连和主题权限。踩坑最多的是把所有业务都塞进 MQTT,最后发现查询、分页、审计和重放都比 HTTP 难做。如果 HTTP 有长连接和 HTTP/2,还需要 MQTT 吗?HTTP/2 解决的是多路复用和传输效率,不等于提供发布/订阅、QoS、遗嘱消息和主题路由。你可以用 HTTP/2 做实时接口,但服务端主动把消息分发给多个订阅方时,应用层要自己补一套消息系统。边界在于业务是否以“资源访问”为核心,还是以“消息流转”为核心。前者继续用 HTTP,后者更像 MQTT 的主场。MQTT 的 QoS 能替代业务幂等吗?不能。QoS 1 只能保证至少送达,重复消息很正常;QoS 2 能减少重复投递,但成本更高,也不能替你处理业务端重复扣费、重复开锁这类问题。实际项目里关键指令仍要带 messageId,并在服务端做去重。取舍是:协议层保证传输语义,业务层保证业务结果。为什么很多系统同时使用 MQTT 和 HTTP?因为两者擅长的部分互补。设备上线、心跳、实时数据和控制命令走 MQTT,用户登录、设备列表、固件下载、历史报表走 HTTP,会比单押一种协议更稳。踩坑点是鉴权体系要统一,否则 MQTT 的 ACL 和 HTTP 的用户权限容易出现不一致。通常会用同一套账号或 Token 签发逻辑,再分别落到 Broker 和 API 网关。MQTT 适合传文件或大报文吗?一般不推荐。MQTT 可以传二进制载荷,但 Broker、客户端内存、最大报文限制和重传成本都会放大风险。固件包、图片、日志压缩包更适合 HTTP、对象存储或 CDN,MQTT 只通知下载地址和版本号。这个边界很重要,否则一次大文件重传就可能拖垮低端设备或 Broker 队列。