asp.net core 从单机到集群

intro

这篇文章主要以我的活动室预约的项目作为示例,看一下一个 asp.net core 应用从单机应用到分布式应用需要做什么。

示例项目

活动室预约提供了两个版本,集群版 和 单机版

单机版方便部署,不依赖其他环境,数据库使用的是 sqlite,详细部署文档可以参考:https://github.com/weihanli/activityreservation/blob/dev/docs/deploy/standalone.md

集群版,目前依赖的组件有 mysql(数据库)/redis(缓存)/elasticsearch(日志)

日志

日志原来是输出到文件中的,单机部署没有什么问题,可以直接 ssh 到机器上查看文件内容,但是如果部署到集群上,日志再输出到文件的话,排查起来可就有点麻烦了,日志是分散在多台机器上,只看某一台机器上的日志可能并不能解决问题。

基于日志这个痛点让我把日志迁移到 elasticsearch 上,日志统一输出到 es,并通过 kibana 来搜索/分析日志。

日志组件一直用的 log4net,日志输出到 es ,自己写了一个 es 的 appender, 但是后来越来越觉得 log4net使用起来不够灵活,后来日志组件换成了 serilog,使用 serilog 就可以方便的扩展,增加日志要记录的信息,关于自定义 serilog enricher 可以参考 serilog 自定义 enricher 来增加记录的信息

使用 es 来存储日志还有一个好处,就是搜索日志非常的快,而且借助 kibana 可以很方便的进行统计分析

拿上篇文章的图来借用一下,下面是 kibana 基于日志的 requestip 来绘制的前十个访问最多的 ip 地址

缓存

单机部署为了不增加系统复杂度,不引入外部依赖,单机版使用的是 memorycache
集群部署,就需要引入分布式缓存,我选择的是 redis,redis 组件是基于 stackexchange.redis 的,自己在其基础上封装了一些功能。

在我的这个示例应用中 redis 不仅仅做缓存,我还用 redis 的 hash 实现了一个类似于 asp.net 里 application 的服务,还有 redis 的发布订阅来实现一个 eventbus 来异步处理公告的浏览记录。

单机环境下,我们用 lock 或者用信号量来实现资源在某一段时间内只能被一个请求拿到

多台机器环境下,我们需要一个分布式锁,上面引入了 redis,就用 redis 来实现一个分布式锁,分布式锁详细实现可以参考:
redlock

使用方式如下:

using (var redislock = redismanager.getredlockclient($"reservation:{reservation.reservationplaceid:n}:{reservation.reservationfordate:yyyymmdd}"))
{
    if (redislock.trylock())
    {
        var reservationfordate = reservation.reservationfordate;
        if (!isreservationfordateavailable(reservationfordate, isadmin, out msg))
        {
            return false;
        }

        // ...
        return true;
    }
    else
    {
        msg = "系统繁忙,请稍后重试!";
        return false;
    }
}

dataprotection

微软在 .net core 下引入了 dataprotection 来保护网站的数据,你也可以用它做一些数据保护,之前做了一个简单数据保护扩展,通过 filter 来自动实现数据的加密/解密,详细信息可以参考

默认 dataprotection 的key 是保存到文件的,可能你也注意到过在 asp.net core 应用启动的时候默认会有一条日志信息如下:

多台机器同时部署的话,key 基本上就是不一样的,这样数据就不会被认为是安全的。

举个栗子,我的应用有一个后台使用 cookie 认证,cookie 会使用 dataprotection 的 key 进行加密,使用默认的 dataprotection 时,多台机器上(实际是k8s的多个pod) 的key 是不一样的,这就导致我在后台登录了之后,进入后台之后刷新一下可能就又跳转到登录界面,这是因为生成的 cookie ,对于一个服务来说是有效的,但是对于其他服务来说是无效的(key 不同,没有办法解密成功,认证失败),所以集群部署的时候,dataproection 是必须要设置的,放在一个统一的地方管理,我们上面已经引入了 redis,所以就把 dataprotection 的 key 放在 redis 中去保存(redis 服务可以做高可用,即使 redis 服务挂了也会重新生成一个 key,不会有什么影响)

使用到的包 microsoft.aspnetcore.dataprotection.stackexchangeredis,配置方式:

// dataprotection persist in redis
services.adddataprotection()
    .setapplicationname(applicationhelper.applicationname)
    .persistkeystostackexchangeredis(() => dependencyresolver.current.resolveservice<iconnectionmultiplexer>().getdatabase(5), "dataprotection-keys")
    ;

获取用户ip

集群部署的时候,会有网关/反向代理去转发请求,这时候直接通过 httpcontext.connection.remoteipaddress 获取到的 ip 地址就会是网关/反向代理的地址,并不是实际用户的地址,一般的反向代理软件会将真实的用户ip放在 x-forwarded-for 请求头中,转发到下游真正的服务器地址,你可以从请求中直接获取 x-forwarded-for 请求头的值,也可以使用微软提供的 forwardedheaders 中间件,配置方式:

public void configureservices(iservicecollection services)
{
    // ...
    services.configure<forwardedheadersoptions>(options =>
    {
        options.knownnetworks.clear();
        options.knownproxies.clear();
        options.forwardlimit = null;
        options.forwardedheaders = microsoft.aspnetcore.httpoverrides.forwardedheaders.all;
    });
}

public void configure(iapplicationbuilder app, ihostingenvironment env)
{    
    app.useforwardedheaders();
    // ...
}

具体参数配置可以参考文档:

memo

其他还有一些上面并未提到,

比较常用的如 session,如果要上集群的话,也应该有相应的分布式 session,这个应用没有用到 session,所以上面没有提(之前用极验验证码的时候有用,后来换成腾讯的验证码服务之后去掉了session)

文件上传,如果是存在本地的话,也不太合适,可能需要存在一个集中的文件服务器或者云端存储如 azure blob。。(网站里的公告模块的图片上传还没改,,,打算基于 github 或者 开源中国的码云实现一个 storage )

其他暂时没想到了,想到了再补充吧。

reference

  • https://github.com/weihanli/activityreservation
  • https://en.wikipedia.org/wiki/x-forwarded-for