大概代码如下:
/// <summary> /// 获取用户的AccountBalance汇总信息 /// </summary> public async Task<AccountBalanceStat> LoadAccountBalanceStatAsync(string owner) { // 数据库查询 var accounts = await _dbContext.BabelAccounts.Where(ac => ac.Owner == owner).ToListAsync(); // 内存计算 return ConvertToAccountBalanceStat(accounts); }什么都不改,直接把代码 CP 到 Health 接口测一下。
神奇,300 并发抗住了。
结论:
上面这一段代码并不会导致服务僵死
数据库驱动没有问题,DbContext 没有问题,数据库资源使用没有问题
当前并不会触发 DI 容器异常, 问题出在 /v1/user/test/account-balance 初始化
account-balance 有什么神奇的东西吗? /// <summary> /// 查询用户的Brick账号余额 /// </summary> [HttpGet("v1/user/{owner}/account-balance")] [SwaggerResponse(200, "获取成功", typeof(AccountBrickStat))] public async Task<IActionResult> GetAccountBricks( [FromRoute, SwaggerParameter("所有者", Required = true)] string owner) { owner = await _userService.FindOwnerAsync(owner); return Ok(new { data = await _accountService.LoadAccountAsync(owner), code = 0 }); }我们刚刚验证了 LoadAccountAsync 的代码是没有问题的,
要不 UserService DI 有问题,要不 AccountService DI 有问题。
把 UserService 加入到 HealthController 中。
public HealthController(UserService userService, UserPoolDataContext dbContext) { _dbContext = dbContext; _userService= userService; }Bool。
300 并发没有撑住,程序僵死啦。
完美,
问题应该在 UserService DI 初始化了。
接下来就是一个个验证 UserService DI 需要的资源,
EmailSDK 没有问题,
HTTPHeaderTools 没有问题,
UserActivityLogService 没有问题。
RedisClient...
RedisClient...
RedisClient...
OK
OK
Ok
复现炸鸡了。
原来是 Redis 的锅?是,
也不是。
先看下我们 RedisClient 是怎么使用的。
// startup.cs 注入了单例的ConnectionMultiplexer // 程序启动的时候会调用InitRedis private void InitRedis(IServiceCollection services) { services.AddSingleton<ConnectionMultiplexer, ConnectionMultiplexer>(factory => { ConfigurationOptions options = ConfigurationOptions.Parse(Configuration["RedisConnectionString"]); options.SyncTimeout = 10 * 10000; return ConnectionMultiplexer.Connect(options); }); } //RedisClient.cs 通过构造函数传入 public class RedisClient { private readonly ConnectionMultiplexer _redisMultiplexer; private readonly ILogger<RedisClient> _logger; public RedisClient(ConnectionMultiplexer redisMultiplexer, ILogger<RedisService> logger) { _redisMultiplexer = redisMultiplexer; _logger = logger; } }DI 初始化 RedisClient 实例的时候,
需要执行 ConnectionMultiplexer.Connect 方法,
ConnectionMultiplexer.Connect 是同步阻塞的。
ConnectionMultiplexer.Connect 是同步阻塞的。
ConnectionMultiplexer.Connect 是同步阻塞的。
一切都能解释了。
怎么改?
// InitRedis 直接把链接创建好,然后直接注入到IServiceCollection中 private void InitRedis(IServiceCollection services) { ConfigurationOptions options = ConfigurationOptions.Parse(Configuration["RedisConnectionString"]); options.SyncTimeout = 10 * 10000; var redisConnectionMultiplexer = ConnectionMultiplexer.Connect(options); services.AddSingleton(redisConnectionMultiplexer); Log.Information("InitRedis success."); }发布验证,
开门放并发 300 + 3000 请求。
完美抗住,丝一般顺滑。
还有更优的写法吗?看了下微软 Cache 中间件源码,更好的做法应该是通过信号量+异步锁来创建 Redis 链接,下来再研究一下
数据库中可能也存在类似的问题,不过当前会在 Startup 中戳一下数据库连接,应该问题不大。
复盘程序启动的时候依赖注入容器同步初始化 Redis 可能很慢(几秒甚至更长)的时候,
其他的资源都在同步等待它初始化好,
最终导致请求堆积,引起程序雪崩效应。
Redis 初始化过慢并不每次都发生, 所以之前服务也只是偶发。
DI 初始化 Redis 连接的时候,redis 打来连接还是个同步的方法,
这种情况下还可能发生异步请求中等待同步资源产生阻塞问题。
同时还需要排查使用其他外部资源的时候是否会触发同类问题。
几个通用的小技巧
ptrace 对此类问题分析很有意义,不同语言框架都有类似的实现
同步、异步概念的原理和实现都要了解,这个有利于理解一些奇奇怪怪的问题
火焰图、Chrome dev Performance 、speedscope 都是好东西
Debug 日志能给更多的信息,在隔离生产的情况下大胆使用
这辈子都不可能看源码的,写写 CURD 多美丽?源码真香,源码真牛逼。
控制变量验证,大胆假设,小心求证,人肉二分查,先怀疑自己再怀疑框架
搞事的时候不要自己一个人,有 Bug 一定要拉上小伙伴一起吃
相关资料
IBM Developer ptrace 嵌入式系统中进程间通信的监视方法
分析进程调用 pstack 和 starce
pstack 显示每个进程的栈跟踪
微软:dotnet-trace performance analysis utility
知乎:全新 Chrome Devtool Performance 使用指南
speedscope A fast, interactive web-based viewer for performance profiles.
jdk 工具之 jstack(Java Stack Trace)
阮一峰:如何读懂火焰图?