MySQL 锁超时问题的全链路定位与优化实践

1、问题 线上出现很多添加 key 锁超时的 mysql 日志。 https://apm.example.com/app/apm/services/<service_name>/transactions/view?rangeFrom=now-24h%2Fh&rangeTo=now&environment=&transactionName=%23 2、排查 1)查看 APM,发现了一个耗时的请求/sync,做语料同步的。后面插入语料 key 都显Lock wait timeout exceeded。 2)查看同步请求的具体链路,发现有多次批量 INSERT 操作,而且每次批量插入消耗的时间会逐步增加。最终也是执行成功的,花了24 分钟。后面其他事务的 Insert 操作都给阻塞住了。 3)查看锁日志,可以发现并不是死锁,而是普通的锁等待,最开始的批量操作(/sync)占用了大量的锁 4253982 row locks。后面的insert(添加 key)申请插入意向锁都被阻塞,等待一段时间之后上报超时错误给应用。 LOCK WAIT 8 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id <tid_1>, OS thread handle <handle_1>, query id <qid_1> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, description) VALUES (1001, 2001, 'xx-XX', 'default', 'key_1', '<text>', ''); ------- TRX HAS BEEN WAITING 22 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 345 page no 83690 n bits 136 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_waiting> lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; ------------------ ---TRANSACTION <trx_long>, ACTIVE 1166 sec starting index read mysql tables in use 1, locked 1 102372 lock struct(s), heap size 10379472, 4253982 row lock(s), undo log entries 943 MySQL thread id <tid_long>, OS thread handle <handle_long>, query id <qid_long> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, status, type) VALUES (2001, 3001, 'xx-XX', 'default', 'k1', 'v1', 2, 1), (2002, 3001, 'xx-XX', 'default', 'k2', 'v2', 2, 1); 4)查看代码,发现了调用 importJsonCorpus 进行多次批量操作,这跟 APM 看到的现象一致。这个方法上有事务注解,importJsonCorpus 方法的调用都在一个事务里,所以一次批量操作结束之后并不会释放锁。 ...

July 9, 2025

大文件下载中断问题的全链路排查与稳定性优化

现象 https://oss.example.com/storages/<storage_id>//firmware.zip 1)在浏览器下载固件文件经常出现中断,原本3g的文件,每次都在1g左右下载完成,文件不完整。 2)内网下载速度较慢,插网线为 20M/s 左右,WIFI 状态下为 8M/s 左右。 3)使用网线有部分用户产生问题 1 的中断现象,一部分用户则不会,使用 WIFI 则必现中断。 分析 1)分析业务:在内部平台构建完成之后,后台会自动对整个固件文件夹进行转存,上传到对象存储服务,后续可以随时下载,那么这里就涉及上传和下载,我们可以对多次下载的同一个固件进行 hash 对比,发现他们并不是一致的,首先可以排除是上传文件的问题。 2)我们使用 wget 进行下载,表现为连接关闭重试,自动重新连接之后能成功下载完整文件。所以确实不是因为上传的问题,那么我们从下载方面找问题。 每下载 1G 中断一次 3)我们抓包查看具体细节,可以看到是服务器发送了一个 FIN 包主动断开连接,而且没有出现RST包,可以排除网络错误或网络波动,也不是客户端关闭的连接,而是服务端主动关闭的。 4)接下来需要定位具体是服务端哪个节点主动关闭连接,在哪个节点会限速。分析内部链路,并在每个节点进行测试,测试结果如下。 直连对象存储服务 Pod IP,不会出现下载中断和限速的问题,问题都在网关上。 我们可以暂时定位中断问题在APISIX发生,而限速是在F5发生。如图,使用域名下载文件速度较慢,为18M/s左右,使用后端服务的 Pod IP 下载速度能达到 66M/s。 5)验证猜想,在本地快速搭建Nginx服务进行验证,并查看 Nginx 日志。 可以看到 Nginx 主动 closed connection,这跟我们抓包的现象一致。还看到出现“upstream response is buffered to a temporary file”字样,说明 Nginx 有缓存文件的相关机制。 6)查看官网的相关配置文档,可以看到 proxy_max_temp_file_size 这个参数,配置文件默认不会显示该配置,但是默认是 1G,我们的现象也是 1G 中断,基本可以确认就是这个配置导致的,我们可以尝试修改为1200M,并验证,发现在下载到 1200M 时会中断。 ...

May 8, 2025

OOMKilled 深度排查:内存、PageCache 与 I/O 的联合分析

1、现象 收到Memory hit original limit内存告警与CPU容量水位告警,随后发生OOM,容器无限重启 2、应急解决方案 最重要的事情是先保证生产可用,并增加JVM参数用来观测,做完以下调整后对容器状态进行观察。 1)我们需要快速调高内存,内存参数调整为-Xmx3g -Xms3g。 2)加上JVM参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/dump ,并挂载/app/logs/dump目录,确保堆内存OOM时可以将堆内存dump下来进行分析。 3)加上-XX:NativeMemoryTracking=summary,用来分析堆外内存。 3、收集相关数据信息 1)容器成功恢复健康状态,但是过了一段时间还是触发了OOMKilled,查看/app/logs/dump目录,并没有dump文件,说明有可能不是堆内存溢出,我们查看JVM状态,包括堆内存信息。 2)JVM状态分析 在容器重启之前,CPU占用在15%-30%之间,属于正常水平,堆内存也一直处于低水位状态,非堆内存也没有明显起伏,但是可以看到系统内存占用偏高,我们需要继续追踪堆外内存的情况。 3) 可以看到总的预留内存4.8GB,使用的物理内存3.56GB,而且通过在进程稳定时和内存到达上限时的快照信息分析,每项的值没有明显的变化。 4)查看容器详情 容器重启之前RSS全程没变,但是可以看到PageCache在重启之前飙升上去了。PageCache是文件缓存占用的内存,按道理来说,在内存达到瓶颈时,系统会自动回收才对。 4、问题分析,追究根因 想知道PageCache为什么在内存达到容器限制之前没被回收,我们需要知道k8s的oomkilled机制,在什么情况下会触发。 1)k8s 检测到容器内存达到memory.limit_in_bytes时且没有回收,便会触发 OOMKilled,我们找一个容器进行测试,将memory.limit_in_bytes调为524M(内部监控平台容器配额)。 查看memory.limit_in_bytes:cat /sys/fs/cgroup/memory/memory.limit_in_bytes watch -n 1 “cat /sys/fs/cgroup/memory/memory.usage_in_bytes” 观察内存增长 找一个大文件下载,wget “xxx”,并继续观察memory.usage_in_bytes,可以看到该值在文件下载的过程中一直上升,上升到接近memory.limit_in_bytes时触发了137错误码,即oomkilled。从现象可以得知,k8s的内存限制计算,会包含RSS和PageCache,RSS包含 JVM 进程的所有内存区域(如堆、非堆、JVM 自身代码、Native Memory等),memory.limit_in_bytes = RSS + PageCache。 2)为什么没有触发PageCache的回收,PageCache回收的速度也不可能赶不上带宽下行网速,原因是PageCache的回收是基于全局物理内存压力,不是单个容器的内存限制。而我们是在docker容器内,可以用命令查看物理内存。 容器是 Cgroups 隔离的独立环境,内核无法感知容器内的内存超限风险。即使容器内存即将超限,内核也不会优先回收其 PageCache,导致容器因 PageCache 触发 OOMKilled。 5、项目分析 随着效能平台的迭代,目前已经有了很多大文件下载或转存的场景,目前主要有: 1)效能客户端 APK 数据流上传(蓝线) 使用 MultipartFile 上传文件,spring.servlet.multipart.file-size-threshold 设置为 10M 时,超过 10M 则会存到临时文件,所以当 APK 文件过大时,也有 PageCache 上升的风险。(注释翻译:将基础输出流从基于内存的流切换到由磁盘支持的流。这是我们意识到有过多数据正在写入,无法再保存在内存中,因此选择切换到基于磁盘的存储。) ...

April 10, 2025

MySQL 死锁问题的系统化排查与并发优化

现象 补全翻译接口(填充空白的语料)接口执行失败,MySQL 检测到死锁快速抛异常,接口执行耗时 426ms。 无论是从接口、具体表现、根因,都与上个问题有明显区别。 分析 1、查看死锁日志,可以看到有两个事务,是对同一个项目的不同语言的批量插入更新操作(INSERT ON DUPLICATE KEY UPDATE)。 ------------------------ LATEST DETECTED DEADLOCK ------------------------ <timestamp> <os_thread_hex> *** (1) TRANSACTION: TRANSACTION <trx_1>, ACTIVE 0 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 6 lock struct(s), heap size 1136, 4 row lock(s) MySQL thread id <tid_1>, OS thread handle <handle_1>, query id <qid_1> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, status, type) VALUES (<item_id_1>, <corpus_id>, 'my-MM', 'default', '下一步', '', 0, 1) , (<item_id_2>, <corpus_id>, 'my-MM', 'default', '分组竞争', '', 0, 1) , (<item_id_3>, <corpus_id>, 'my-MM', 'default', '判断对错', '', 0, 1) , (<item_id_4>, <corpus_id>, 'my-MM', 'default', '完成', '', 0, 1) , (<item_id_5>, <corpus_id>, 'my-MM', 'default', '球球拼词', '', 0, 1) , (<item_id_6>, <corpus_id>, 'my-MM', 'default', '知识排序', '', 0, 1) , (<item_id_7>, <corpus_id>, 'my-MM', 'default', '知识配对', '', 0, 1) , (<item_id_8>, <corpus_id>, 'my-MM', 'default', '记忆卡片', '', 0, 1) , (<item_id_9>, <corpus_id>, 'my-MM', 'default', '试玩', '', 0, 1) , (<item_id_10>, <corpus_id>, 'my-MM', 'default', '超级分类', '', 0, 1) , (<item_id_11>, <corpus_id>, 'my-MM', 'default', '趣味分类', '', 0, 1) , (<item_id_12>, <corpus_id>, 'my-MM', 'default', '趣味选择', '', 0, 1) , (<item_id_13>, <corpus_id>, 'my-MM', 'default', '返回', '', 0, 1) , (<item_id_14>, <corpus_id>, 'my-MM', 'default', '选词填空', '', 0, 1) ON DUPLICATE KEY UPDATE status = IF(value = '', VALUES(status), status), value = IF(value = '', VALUES(value), value) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 345 page no 93049 n bits 144 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_1> lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) TRANSACTION: TRANSACTION <trx_2>, ACTIVE 0 sec inserting mysql tables in use 1, locked 1 7 lock struct(s), heap size 1136, 4 row lock(s) MySQL thread id <tid_2>, OS thread handle <handle_2>, query id <qid_2> <private-ip> <db_name> update INSERT INTO multilingual_item (item_id, corpus_id, lang, customize,`key`,value, status, type) VALUES (<item_id_a1>, <corpus_id>, 'ar-EG', 'default', '下一步', '', 0, 1) , (<item_id_a2>, <corpus_id>, 'ar-EG', 'default', '分组竞争', '', 0, 1) , (<item_id_a3>, <corpus_id>, 'ar-EG', 'default', '判断对错', '', 0, 1) , (<item_id_a4>, <corpus_id>, 'ar-EG', 'default', '完成', '', 0, 1) , (<item_id_a5>, <corpus_id>, 'ar-EG', 'default', '球球拼词', '', 0, 1) , (<item_id_a6>, <corpus_id>, 'ar-EG', 'default', '知识排序', '', 0, 1) , (<item_id_a7>, <corpus_id>, 'ar-EG', 'default', '知识配对', '', 0, 1) , (<item_id_a8>, <corpus_id>, 'ar-EG', 'default', '记忆卡片', '', 0, 1) , (<item_id_a9>, <corpus_id>, 'ar-EG', 'default', '试玩', '', 0, 1) , (<item_id_a10>, <corpus_id>, 'ar-EG', 'default', '超级分类', '', 0, 1) , (<item_id_a11>, <corpus_id>, 'ar-EG', 'default', '趣味分类', '', 0, 1) , (<item_id_a12>, <corpus_id>, 'ar-EG', 'default', '趣味选择', '', 0, 1) , (<item_id_a13>, <corpus_id>, 'ar-EG', 'default', '返回', '', 0, 1) , (<item_id_a14>, <corpus_id>, 'ar-EG', 'default', '选词填空', '', 0, 1) ON DUPLICATE KEY UPDATE status = IF(value = '', VALUES(status), status), value = IF(value = '', VALUES(value), value) *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 345 page no 93049 n bits 144 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_2> lock_mode X Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 345 page no 93049 n bits 144 index PRIMARY of table `<db_name>`.`multilingual_item` trx id <trx_2> lock_mode X insert intention waiting Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;; *** WE ROLL BACK TRANSACTION (1) 根据死锁日志画出时序图,从下面这个时序图可以看到死锁的形成过程。 ...

March 16, 2025

SSE 流式响应在 Nginx 下失效的排查与修复实践

背景与现象 背景:在C知汇项目(GPT私有库问答系统)中,我们期望GPT的回答通过流式返回,采用了SSE(Server-Sent Events)的服务端推送技术做流式传输。 现象:本地运行没有问题,但是上线之后发现有时是流式,有时是一次性返回。 产品界面 分析过程 1、首先确认技术选型是否存在问题。流式响应有常见的两种方案,WebFlux和SSE,我们使用WebFlux响应式编程技术替代SSE,写最小demo排除业务的影响,发现仍然有问题。可以暂时排除代码层面的问题。 2、分析数据传输链路,用户发送提问并成功响应时,回答通过流式返回链路:GPT -> C知汇后台 -> 后台Nginx -> C知汇前端 -> 前端Nginx -> 浏览器 我们可以选择从源头开始验证。 1)后台打印GPT响应,收到GPT的响应是流式的,没有问题。 2)通过直连后台服务容器IP排除Nginx的影响,发现问题消失了。 3、验证猜想:查阅nginx配置资料,发现nginx有缓冲区的相关配置 proxy_buffers。查看鲸云Nginx配置如下图所示。通过自建nginx验证了猜想。那么为什么线上环境有时能复现,有时又是正常的呢?原来是响应内容的大小决定的,如果响应内容没有达到缓冲区容量,那么会一次返回,如果超过了缓冲区容量,那么缓冲区容量以外的数据会流式返回。 解决方案 方案一:取消网关的响应缓冲。可能需要修改网关配置,然而修改网关配置是一个不小的动作,而且可能对其他应用造成影响。 方案二:绕过nginx,直连访问。但是鲸云我们通常采用滚动更新镜像的方式,所以容器IP是经常会变更的。后台可以开放获取IP接口,客户端可以先通过访问后台接口获取IP,然后再直连,这样也能使用到网关的负载均衡。主要的缺点有:1、不通过网关过滤直连有一定的安全风险。2、访问记录不会被网关日志收集,会对数据的统计和问题的排查造成一定的影响。 方案二的方式有点hack,可以直接排除,我们尝试从方案一尝试入手解决。 尝试解决 关闭响应缓冲的方式主要有两种 1)将Nginx的proxy_buffers配置设置为off。 2)将请求头X-Accel-Buffering设置为 no,Cache-Control设置为no-cache。 1、我们在应用代码上去手动设置请求头,发现仍然不行。我们查看请求返回的响应头里没有我们设置的X-Accel-Buffering,而使用Arthas执行watch,看到X-Accel-Buffering是设置进去了的。 代码里设置响应头 返回的响应头 Arthas Watch 2、怀疑是配置优先级的问题,如果我们将Nginx的proxy_buffers设置为on,请求头X-Accel-Buffering设置为no,会以哪一个为准?我们通过自建Nginx验证发现,Nginx 会遵循请求头中的指示,所以是请求头X-Accel-Buffering优先。 APISIX响应重写 3、我们验证了正常情况下使用X-Accel-Buffering是可以生效的且优先级比较高的。回到第1点,如果我们在APISIX设置X-Accel-Buffering响应头,那么会不会透传回客户端呢?测试表明该响应头仍然丢失了。 4、我们去掉前端部分再对整条链路进行分析,GPT -> 应用后台 -> APISIX -> 客户端,我们在后台和APISIX都设置了响应头,但是响应里却没有出现X-Accel-Buffering头。那么我们可以猜测,APISIX和客户端之间还有一些链路,导致X-Accel-Buffering没有被透传。一般实际生产中,Nginx也不止一层,那么需要运维协助排查解决。 链路 协调资源解决问题 通过运维协助,展开了业务端不可见的完整链路: GPT -> C知汇后台 -> APISIX -> 雷池 WAF -> 负载均衡 F5 -> 浏览器 ...

October 20, 2024

从告警泛滥到高效定位:MCP 异常分析实践

背景 为提高后台整体质量,目前大部分后台系统都接入了 Error 日志监控告警,接入初期产生了非常多的告警,消耗了我们大量的时间去排查。当前线上异常日志排查主要存在以下痛点: 服务链路复杂,定位困难: 后台系统通常由众多服务构成(包括上下游依赖),排查一个问题往往需要跨多个监控平台应用、多个集群日志、多个服务进行搜索,并定位代码才能准确定位根源。 告警噪音干扰严重: 存在大量 Error 级别的告警日志并非真正的系统异常(例如预期的业务校验失败、可忽略的第三方短暂异常),而是需要调整日志级别或优化处理逻辑。识别此类“非问题”告警并推动修改,同样耗费大量时间。 跨团队协作成本高: 当排查指向下游服务或需要其他团队协助时,需耗费大量时间手动整理详细的异常上下文信息(如时间戳、TraceID、关键参数、异常堆栈)并转交给相关方。 缺乏智能化辅助: 大部分异常的分析本质上是梳理代码执行链路和业务逻辑。此过程高度依赖人工经验,若能借助 AI 能力智能推导出完整的调用链路、关键变量状态及潜在逻辑缺陷,将极大提升排查效率,甚至在部分场景下自动修复代码解决。 目标 建立自动化异常日志分析系统: 构建智能化的日志处理与分析平台,减少人工介入。 实现日志到代码的快速精准定位: 将异常日志信息快速关联到对应的代码仓库、文件乃至代码行。 显著提升异常排查效率: 缩短单个异常的平均排查时间(MTTR),释放开发运维人力。 构建可复用的异常分析知识库: 积累处理经验,形成可检索的知识沉淀,辅助未来同类问题的解决。 方案 1、人工 OR 自动 AI 辅助我们排查问题,主要有两种方式: 方案 优点 缺点 方案一:开发MCP Server,人工将企业 IM 群内的报错信息发给 AI Client,AI 结合接收到的日志内容、关联的代码仓库信息,进行代码定位与异常链路分析,并输出报告。 1. 可控性强: 人工筛选关键告警触发分析,避免无效请求。2. 利用成熟能力: 可集成现有 MCP 平台及 Cursor AI 强大的代码分析能力。3. 易于初期试点: 实施复杂度相对较低。 1. 依赖人工介入: 仍需人工识别并转发告警信息,无法实现全流程自动化。2. 响应有时延: 依赖于人工操作,响应速度不如自动触发快。3. 上下文可能不全: 人工转发可能遗漏关联日志或上下文信息,影响分析准确性。 方案二:开发 AI 服务,将Error日志直接上报到AI 服务,利用 AI 自动化分析并输出报告 1. 自动化程度高: 无需人工干预,实现告警到分析的自动闭环。 1. 初期处理压力大: 线上错误日志量大且繁杂,直接全量上报会造成 AI 服务巨大吞吐压力与分析资源浪费。2. 需完善过滤机制: 必须设计高效的过滤规则,避免无效分析。 总结:方案一适合初期试点、或处理复杂低频、需要人工确认的高优先级问题。其优势在于风险可控,能充分利用现有工具链。所以我们可以先采取方案一,人工根据严重程度和优先级识别筛选问题,并发给 AI 分析,后续再使用自动化的方式帮助我们过滤或标注出需要重点关注的Error 异常问题。根据两种方式使用不同的大模型来控制成本。 ...

August 18, 2024

微软 Hybrid Identity 混合身份认证实践

前言 在企业信息化建设中,一个常见诉求是:员工继续使用本地 AD 域账号,同时能够无缝访问 Office 365、Teams 等云端应用,并在统一策略下完成认证与权限控制。 这个问题的本质并不是“本地目录还是云目录二选一”,而是如何在安全、体验、合规之间取得平衡。混合身份认证正是为此而生:既保留本地目录的管理基础,又借助云端身份服务提升统一登录能力与安全治理能力。 本文会围绕实际落地最常遇到的核心问题展开: AD 与 Entra ID 的职责边界与协作关系 PHS、PTA、ADFS 三种方案的取舍逻辑 面向教育/组织接入场景的扩展实践(如 OneRoster、外部目录接入) 什么是混合身份认证? Hybrid Identity 是指通过本地目录(如Microsoft Active Directory、OpenLDAP 等)与云目录(如 Microsoft Entra ID、Google Cloud Identity 等)的集成,为用户提供统一的身份验证和授权机制。这种解决方案允许用户使用单一身份访问本地和云中的资源,无论资源的位置如何。具体场景如下: 允许你或你的组织内的用户,使用 Azure AD 登录你开发的应用。 允许其他组织的用户,使用 Azure AD 登录你开发的应用。 为什么会有混合身份认证,是否可以不使用本地目录,而完全依赖云目录? 取决于企业的具体需求,包括现有架构、协议支持、网络条件、合规性要求等。对于大多数企业,采用混合架构(本地目录+云目录)可能是更现实的选择。 我们能做的? 了解术语概念 业务相关 沟通需要 理解深层次架构 提供产品方向 解决技术问题 一、基础概念 身份 (Identity): 用户/设备/服务的唯一代表(用户名、邮箱等)。 认证 (Authentication, AuthN): 证明“你是谁”(密码、指纹、短信验证码、MFA)。 授权 (Authorization, AuthZ): 决定“你能做什么”(RBAC角色权限、ABAC属性权限)。 身份生命周期: 创建账号 -> 启用 -> 更新(改密码) -> 禁用 -> 删除。 IAM、IDP、IDM三者共同构成了现代企业身份管理与安全体系 ...

July 20, 2024

《大型系统应用架构实践》笔记:全球区域化部署与多层路由设计

书籍链接:https://book.douban.com/subject/34782232/ 主要针对第二章的全球区域化部署技术 1 总体架构 基本原则 问题 解决方案 总体架构 2 路由服务 2.1 路由服务架构 透传技术优势: 1、无需在每一层维护一个路由表,不用过多关注路由数据一致性问题。 2、无需每次都调用路由表RPC服务,对RPC服务的依赖过大,调用量过高。 2.2 路由表设计 1、基本模型 类似位图,通过用户id将bitmap对应的位置置为1。但是我们不仅需要存储用户是否存在的信息,还需要存储用户到机房的路由,所以用4个二进制位来存储用户到机房的映射。 前三位:机房标识 最后一位:用户状态 2、分段存储 为了解决ID分布不均匀,存储浪费的问题,采取分段模式进行存储。用户没命中的段为null,不会占用内存。 3、逻辑机房 1、路由信息巨大,机房迁移成本很高。 2、使流量均匀分布。 4、一致性保障 定期MD5进行对比 2.3 路由表更新机制 基于Zookeeper实现,引入“禁写”状态,即用户的路由表更新时,该用户无法进行业务写入,从而确保业务数据的全局一致性。架构如下: 整体流程 2.4 用户路由更新方案 1、确定用户归属机房 通过对各个机房的访问统计,将延迟最少的机房确认为最近的机房。 2.5 多层路由实现 1、统一接入层路由技术 为所有网站用户提供统一的接入点,对多区域进行对等部署,并将用户归属到不同的区域。 方案:Openresty + Lua共享内存 + Redis + ProxyServer 2、服务层路由技术 业务相关,处理不同类型的数据,如:独享数据、共享数据。 1)用户优先级原则,如买家 > 卖家 > 平台运营人员。 2)数据类型及处理方法 只读类型:不需要实时一致;处理:数据异步复制 独享类型:只有一个用户可以对数据进行变更。处理:本地读写 共享数据:多个用户需要共同变更同一条数据。处理:优先级最高的用户区域作为Master,可进行本地读写。非Master区域的用户需要区分情况决定是本地读写还是跨区域读写。 全局共享数据:所有用户的行为都可以改变的数据,对一致性要求较高。处理:使用网络质量最好的机房存放数据,所有用户对全局共享数据的变更都被路由到这个机房。 3、消息层路由 异步调用时,如使用MQ进行调用,需要将消息路由到指定的机房。 ...

May 12, 2024

长轮询在配置平台的工程化实践与性能权衡

长轮询在配置平台的应用 1. 配置平台简介 略 2. 长轮询简介 传统的短轮询方式存在一个严重缺陷:程序在每次请求时都会新建一个HTTP请求,然而并不是每次都能返回所需的新数据。当同时发起的请求达到一定数目时,会对服务器造成较大负担。这时我们可以采用长轮询方式解决这个问题。 长轮询的基本思想是在每次客户端发出请求后,服务器检查上次返回的数据与此次请求时的数据之间是否有更新,如果有更新则返回新数据并结束此次连接,否则服务器“hold”住此次连接,直到有新数据时再返回相应。而这种长时间的保持连接可以通过设置一个较大的HTTP timeout实现。在服务端消息推送方面,长轮询有着广泛的应用。 3. 请求模型 3.1 同步请求模型 这是我们日常最常用同步请求模型,所有动作都交给同一个 Tomcat 线程处理,所有动作处理完成,线程才会被释放回线程池。 如果业务需要较长时间处理,那么这个 Tomcat 线程其实一直在被占用,随着请求越来越多,可用 I/O 线程越来越少,直到被耗尽。这时后续请求只能等待空闲 Tomcat 线程,这将会加长了请求执行时间,或者直接被拒绝,客户端会报connect refused异常。 如果客户端不关心返回业务结果,这时我们可以自定义线程池,将请求任务提交给线程池,然后立刻返回。 3.2 异步请求模型 Servlet3 引入异步 Servelt 新特性,可以完美解决上面的需求。 异步 Servelt 执行请求流程: 将请求信息解析为 HttpServletRequest 分发到具体 Servlet 处理,将业务提交给自定义业务线程池,请求立刻返回,Tomcat 线程立刻被释放 当业务线程将任务执行结束,将会将结果转交给 Tomcat 线程 通过 HttpServletResponse 将响应结果返回给等待客户端 引入异步 Servelt3 整体流程如下: 3.3 应用场景 1、增加系统吞吐量 拿Tomcat作为Servlet容器来说,无论是计算型请求还是IO型请求,都是交给Tomcat容器线程来建立连接和负责业务逻辑处理,如果将IO型请求或者RT(响应时间)比较高的请求业务逻辑处理,通过异步请求来实现,可以尽早地释放连接线程,业务逻辑交由业务线程池处理,那么连接线程池可以接收更多的请求,从而提高了系统吞吐量。 2、服务端消息推送 消息推送,对于一些服务端发生变更,需要向客户端发送消息通知的场景,可以通过异步请求来实现。 3.4 异步请求实现 1、 AsyncContext HttpServletRequest#startAsync 获取 AsyncContext 异步上下文对象 使用自定义的业务线程池处理业务逻辑 业务线程处理结束,通过 AsyncContext#complete 返回响应结果 ExecutorService executorService = Executors.newFixedThreadPool(10); @RequestMapping("/hello") public void hello(HttpServletRequest request) { AsyncContext asyncContext = request.startAsync(); // 超时时间 asyncContext.setTimeout(10000); executorService.submit(() -> { try { // 休眠 5s,模拟业务操作 TimeUnit.SECONDS.sleep(5); // 输出响应结果 asyncContext.getResponse().getWriter().println("hello world"); log.info("异步线程处理结束"); } catch (Exception e) { e.printStackTrace(); } finally { asyncContext.complete(); } }); log.info("servlet 线程处理结束"); } 2、 DeferredResult Servlet3.0 提供了异步处理请求的特性,DeferredResult 是Spring基于 Servlet 3.0 对异步请求的支持实现,SpringMVC 3.2 之后引入新的类 DeferredResult,目的是对于请求提供异步处理方式,释放容器连接,支持更多的并发。或者基于它的超时机制来做一些长轮询相关的事情。 ...

October 21, 2023

MySQL 联合索引原理与查询性能优化实践

从数据结构理解 MySQL 联合索引 前言 索引的本质是一种通过特定数据结构来优化数据检索速度的机制。是我们开发岗接触 MySQL 最重要的概念之一,与我们的应用开发息息相关。 结合应用思考 1)在语料平台中的 Item 表中,假设我们的目标是快速搜索 key,只考虑完全匹配的情况下,如何建立索引? 索引的字段(业务驱动、是否要覆盖) 索引顺序(最左前缀匹配、高选择性或离散性) 2)考虑大批量写的情况下,怎样的索引可以减少性能损耗? 3)高选择性的字段是不是一定排最前? 4)如何在离散性与随机性之间抉择? 1、MySQL 索引的数据结构种类 索引类型 存储引擎 底层数据结构 特点与用途 普通索引 / 主键 / 唯一索引 InnoDB B+Tree 默认且绝对主流。聚簇索引叶子节点存数据,二级索引叶子节点存主键。 普通索引 / 主键 / 唯一索引 MyISAM B+Tree 非聚簇索引,叶子节点存数据行的物理地址。 哈希索引 Memory Hash Table 默认类型,等值查询极快,用于临时表和缓存。 自适应哈希索引 InnoDB Hash Table 内部自动功能,自动缓存热点数据,加速等值查询。 全文索引 (FULLTEXT) InnoDB, MyISAM 倒排索引 解决 LIKE ‘%…%’ 性能问题,用于全文检索。 空间索引 (SPATIAL) InnoDB, MyISAM R-Tree 专用于地理空间数据类型查询。 其他索引数据结构:Skip List跳表、红黑树、BitMap 位图、Trie 前缀树 2、B+树 B+Tree 是多路平衡搜索树,一种专为磁盘等外部存储设备设计的、多路的、平衡的搜索树。它的主要目标是减少磁盘 I/O 次数,从而高效地支持大规模数据的增删改查,尤其是范围查询。 ...

June 12, 2023