Entity Framework Core 2.0 入门

http://www.cnblogs.com/cgzl/p/8543772.html

该文章比较基础, 不多说废话了, 直接切入正题.

该文分以下几点:

  • 创建Model和数据库
  • 使用Model与数据库交互
  • 查询和保存关联数据

EF Core支持情况

EF Core的数据库Providers:

此外还即将支持CosmosDB和 Oracle.

EFCore 2.0新的东西:

查询:

  • EF.Functions.Like()
  • Linq解释器的改进
  • 全局过滤(按类型)
  • 编译查询(Explicitly compiled query)
  • GroupJoin的SQL优化.

映射:

  • Type Configuration 配置
  • Owned Entities (替代EF6的复杂类型)
  • Scalar UDF映射
  • 分表

性能和其他

  • DbContext Pooling, 这个很好
  • Raw SQL插入字符串.
  • Logging
  • 更容易定制配置

1.创建数据库和Model

准备.net core项目

项目结构如图:

由于我使用的是VSCode, 所以需要使用命令行:

复制代码
mkdir LearnEf && cd LearnEf
dotnet new sln // 创建解决方案

mkdir LearnEf.Domains && cd LearnEf.Domains
dotnet new classlib // 创建LearnEf.Domains项目

cd ..
mkdir LearnEf.Data && cd LearnEf.Data
dotnet new classlib // 创建LearnEf.Data项目

cd ..
mkdir LearnEf.UI && cd LearnEf.UI
dotnet new console // 创建控制台项目

cd ..
mkdir LearnEf.Tests && cd LearnEf.Tests
dotnet new xunit // 创建测试项目
复制代码

为解决方案添加项目:

dotnet sln add LearnEf.UI/LearnEf.UI.csproj
dotnet sln add LearnEf.Domains/LearnEf.Domains.csproj
dotnet sln add LearnEf.Data/LearnEf.Data.csproj
dotnet sln add LearnEf.Tests/LearnEf.Tests.csproj

 

为项目之间添加引用:

LearnEf.Data依赖LearnEf.Domains:

cd LearnEf.Data
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj

 

LearnEf.Console依赖LearnEf.Domains和LearnEf.Data:

cd ../LearnEf.UI
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

LearnEf.Test依赖其它三个项目:

cd ../LearnEf.Tests
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj ../LearnEf.UI/LearnEf.UI.csproj

 

(可能需要执行dotnet restore)

在Domains项目下直接建立两个Model, 典型的一对多关系Company和Department:

复制代码
using System;
using System.Collections.Generic;

namespace LearnEf.Domains
{
    public class Company
    {
        public Company()
        {
            Departments = new List<Department>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime StartDate { get; set; }
        public List<Department> Departments { get; set; }
    }
}
复制代码
复制代码
namespace LearnEf.Domains
{
    public class Department
    {
        public int Id { get; set; }
        public int CompanyId { get; set; }
        public Company Company { get; set; }
    }
}
复制代码

 

添加Entity Framework Core库:

首先Data项目肯定需要安装这个库, 而我要使用sql server, 参照官方文档, 直接在解决方案下执行这个命令:

dotnet add ./LearnEf.Data package Microsoft.EntityFrameworkCore.SqlServer
dotnet restore

 

创建DbContext:

在Data项目下创建MyContext.cs:

复制代码
using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
    }
}
复制代码

指定数据库Provider和Connection String:

在EFCore里, 必须明确指定Data Provider和Connection String.

可以在Context里面override这个Onconfiguring方法:

有一个错误, 应该是Server=localhost;

(这里无需调用父类的方法, 因为父类的方法什么也没做).

UseSqlServer表示使用Sql Server作为Data Provider. 其参数就是Connection String.

在运行时EfCore第一次实例化MyContext的时候, 就会触发这个OnConfiguring方法. 此外, Efcore的迁移Api也可以获得该方法内的信息.

EF Core迁移:

简单的来说就是 Model变化 –> 创建migration文件 –> 应用Migration到数据库或生成执行脚本.

添加Migration (迁移):

由于我使用的是VSCode+dotnet cli的方法, 所以需要额外的步骤来使dotnet ef命令可用.

可以先试一下现在的效果:

可以看到, dotnet ef 命令还不可用.

所以参考官方文档: https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dotnet

可执行项目(Startup project)需要EFCore迁移引擎库, 所以对LearnEf.UI添加这个库:

dotnet add ./LearnEf.UI package Microsoft.EntityFrameworkCore.Design
dotnet restore

 

然后打开LearnEf.UI.csproj 添加这段代码, 这个库是EF的命令库:

 <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
  </ItemGroup>

 

最后内容如下:

然后再执行dotnet ef命令, 就应该可用了:

现在, 添加第一个迁移:

cd LearnEf.UI
dotnet ef migrations add Initial --project=../LearnEf.Data

 

–project参数是表示需要使用的项目是哪个.

命令执行后, 可以看到Data项目生成了Migrations目录和一套迁移文件和一个快照文件:

检查这个Migration.

前边带时间戳的那两个文件是迁移文件.

另一个是快照文件, EFCore Migrations用它来跟踪所有Models的当前状态. 这个文件非常重要, 因为下次你添加迁移的时候, EFcore将会读取这个快照并将它和Model的最新版本做比较, 就这样它就知道哪些地方需要有变化.

这个快照文件解决了老版本Entity Framework的一个顽固的团队问题.

使用迁移文件创建脚本或直接生成数据库.

生成创建数据库的SQL脚本:

dotnet ef migrations script --project=../LearnEf.Data/LearnEf.Data.csproj

 

Sql脚本直接打印在了Command Prompt里面. 也可以通过指定–output参数来输出到具体的文件.

这里, 常规的做法是, 针对开发时的数据库, 可以通过命令直接创建和更新数据库. 而针对生产环境, 最好是生成sql脚本, 然后由相关人员去执行这个脚本来完成数据库的创建或者更新.

直接创建数据库:

dotnet ef database update --project=../LearnEf.Data/LearnEf.Data.csproj --verbose

 

–verbose表示显示执行的详细过程, 其结果差不多这样:

这里的执行过程和逻辑是这样的: 如果数据库不存在, 那么efcore会在指定的连接字符串的地方建立该数据库, 并应用当前的迁移. 如果是生成的sql脚本的话, 那么这些动作必须由您自己来完成.

然后查看一下生成的表.

不过首先, 如果您也和我一样, 没有装Sql server management studio或者 Visual Studio的话, 请您先安装VSCode的mssql这个扩展:

重启后, 建立一个Sql文件夹, 然后建立一个Tables.sql文件, 打开命令面板(windows: Shift+Ctrl+P, mac: Cmd+Shift+P), 选择MS SQL: Connect.

然后选择Create Connection Profile:

输入Sql的服务器地址:

再输入数据库名字:

选择Sql Login(我使用的是Docker, 如果windows的话, 可能使用Integrated也可以):

输入用户名:

密码:

选择是否保存密码:

最后输入档案的名字:

随后VSCode将尝试连接该数据库, 成功后右下角会这样显示 (我这里输入有一个错误, 数据库名字应该是LearnEF):

随后在该文件中输入下面这个sql语句来查询所有的Table:

--  Table 列表
SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE='BASE TABLE';

 

执行sql的快捷键是windows: Shift+Ctrp+E, mac: Cmd+Shift+E, 或者鼠标右键.

结果如图:

OK表是创建成功了(还有一个迁移历史表, 这个您应该知道).

接下来我看看表的定义:

-- Companies表:
exec sp_help 'Companies';

 

其中Name字段是可空的并且长度是-1也就是nvarchar(Max).

Departments表的Name字段也是一样的.

再看看那个MigrationHistory表:

-- MigrationHistory:
SELECT * FROM dbo.__EFMigrationsHistory;

可以看到, efcore到migration 历史表里面只保存了MigrationId.

在老版本到ef里, migration历史表里面还保存着当时到迁移的快照, 创建迁移的时候还需要与数据库打交道. 这就是我上面提到的如果团队使用ef和源码管理的话, 就会遇到这个非常令人头疼的问题.

如果使用asp.net core的话.

在解决方案里再建立一个asp.net core mvc项目:

mkdir LearnEf.Web && cd LearnEf.Web
dotnet new mvc

 

在解决方案里添加该项目:

dotnet sln add ./LearnEf.Web/LearnEf.Web.csproj

 

为该项目添加必要的引用:

cd LearnEf.Web
dotnet add reference ../LearnEf.Domains/LearnEf.Domains.csproj ../LearnEf.Data/LearnEf.Data.csproj

 

为测试项目添加该项目引用:

cd ../*Tests
dotnet add reference ../LearnEf.Web/LearnEf.Web.csproj

 

操作完之后, 我们可以做以下调整, 去掉MyContext里面的OnConfiguring方法, 因为asp.net core有内置的依赖注入机制, 我可以把已经构建好的DbContextOptions直接注入到构造函数里:

这样的话, 我们可以让asp.net core来决定到底使用哪个Data Provider和Connection String:

这也就意味着, Web项目需要引用EfCore和Sql Provider等, 但是不需要, 因为asp.net core 2.0这个项目模版引用了AspNetCore.All这个megapack, 里面都有这些东西了.

虽然这个包什么都有, 也就是说很大, 但是如果您使用Visual Studio Tooling去部署的话, 那么它只会部署那些项目真正用到的包, 并不是所有的包.

接下来, 在Web项目的Startup添加EfCore相关的配置:

复制代码
 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;"));
        }
复制代码

 

这句话就是把MyContext注册到了asp.net core的服务容器中, 可以供注入, 同时在这里指定了Data Provider和Connection String.

与其把Connection String写死在这里, 不如使用appSettings.json文件:

然后使用内置的方法读取该Connection String:

复制代码
 public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddDbContext<MyContext>
                (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
        }
复制代码

 

回到命令行进入Web项目, 使用dotnet ef命令:

说明需要添加上面提到的库, 这里就不重复了.

然后, 手动添加一个Migration叫做InitialAspNetCore:

dotnet ef migrations add InitialAspNetCore --project=../LearnEf.Data

 

看一下迁移文件:

是空的, 因为我之前已经使用UI那个项目进行过迁移更新了. 所以我要把这个迁移删掉:

dotnet ef migrations remove --project=../LearnEf.Data

 

然后这两个迁移文件就删掉了:

多对多关系和一对一关系:

这部分的官方文档在这: https://docs.microsoft.com/en-us/ef/core/modeling/relationships

对于多对多关系, efcore需要使用一个中间表, 我想基本ef使用者都知道这个了, 我就直接贴代码吧.

建立一个City.cs:

复制代码
namespace LearnEf.Domains
{
    public class City
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
复制代码

 

Company和City是多对多的关系, 所以需要建立一个中间表,叫做 CompanyCity:

复制代码
namespace LearnEf.Domains
{
    public class CompanyCity
    {
        public int CompanyId { get; set; }
        public int CityId { get; set; }
        public Company Company { get; set; }
        public City City { get; set; }
    }
}
复制代码

 

修改Company:

修改City:

尽管Efcore可以推断出来这个多对多关系, 但是我还是使用一下FluentApi来自定义配置一下这个表的主键:

MyContext.cs:

复制代码
using LearnEf.Domains;
using Microsoft.EntityFrameworkCore;

namespace LearnEf.Data
{
    public class MyContext : DbContext
    {
        public MyContext(DbContextOptions<MyContext> options)
            : base(options)
        {

        }
        public DbSet<Company> Companies { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<CompanyCity> CompanyCities { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });
        }

        // protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        // {
        //     optionsBuilder.UseSqlServer("Server=localhost; Database=LearnEf; User Id=sa; Password=Bx@steel1;");
        //     base.OnConfiguring(optionsBuilder);
        // }
    }
}
复制代码

 

完整的写法应该是:

其中红框里面的部分不写也行.

接下来建立一个一对一关系, 创建Model叫Owner.cs:

复制代码
namespace LearnEf.Domains
{
    public class Owner
    {
        public int Id { get; set;}
        public int CompanyId { get; set; }
        public string Name { get; set; }
        public Company Company { get; set; }
    }
}
复制代码

 

修改Company:

配置关系:

复制代码
protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CompanyCity>()
                .HasKey(c => new { c.CompanyId, c.CityId });

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.Company)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CompanyId);

            modelBuilder.Entity<CompanyCity>().HasOne(x => x.City)
                .WithMany(x => x.CompanyCities).HasForeignKey(x => x.CityId);

            modelBuilder.Entity<Owner>().HasOne(x => x.Company).WithOne(x => x.Owner)
                .HasForeignKey<Owner>(x => x.CompanyId);
        }
复制代码

 

 

这里面呢, 这个Owner对于Company 来说 是可空的. 而对于Owner来说, Company是必须的. 如果针对Owner想让Company是可空的, 那么CompanyId的类型就应该设置成int?.

再添加一个迁移:

dotnet ef migrations add AddRelationships --project=../LearnEf.Data

 

查看迁移文件:

查看一下快照;

没问题, 那么更新数据库:

dotnet ef database update AddRelationships --project=../LearnEf.Data --verbose

 

更新成功:

对现有数据库的反向工程.

这部分请查看官方文档吧, 很简单, 我实验了几次, 但是目前还没有这个需求.

使用Model与数据库交互

输出Sql语句.

对于asp.net core 2.0项目, 参考官方文档: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?tabs=aspnetcore2x

实际上, 项目已经配置好Logging部分了, 默认是打印到控制台和Debug窗口的. 源码: https://github.com/aspnet/MetaPackages/blob/dev/src/Microsoft.AspNetCore/WebHost.cs

而对于console项目, 文档在这: https://docs.microsoft.com/en-us/ef/core/miscellaneous/logging

需要对LearnEf.Data项目添加这个包:

cd LearnEf.Data
dotnet add package Microsoft.Extensions.Logging.Console
dotnet restore

 

然后为了使用console项目, 需要把MyContext改回来:

这部分首先是使用LoggerFactory创建了一个特殊的Console Logger. .net core的logging可以显示很多的信息, 这里我放置了两个过滤: 第一个表示只显示Sql命令, 第二个表示细节的显示程度是Information级别.

最后还要在OnConfiguring方法里告诉modelBuilder使用MyLoggerFactory作为LoggerFactory.

这就配置好了.

插入数据.

这部分很简单, 打开UI项目的Program.cs:

这里都懂的, 创建好model之后, 添加到context的DbSet属性里, 这时context就开始追踪这个model了.

SaveChanges方法, 会检查所有被追踪的models, 读取他们的状态. 这里用到是Add方法, context就会知道这个model的状态是new, 所以就应该被插入到数据库. 然后它就根据配置会生成出相应的sql语句, 然后把这个SQL语句执行到数据库. 如果有返回数据的话, 就取得该数据.

下面就运行一下这个console程序:

dotnet run --project=./LearnEf.UI

 

看下控制台:

可以看到输出了sql语句, 而且这个出入动作后, 做了一个查询把插入数据生成的Id取了回来.

默认情况下log不显示传进去的参数, 这是为了安全. 但是可以通过修改配置来显示参数:

然后控制台就会显示这些参数了:

批量插入操作.

可以使用AddRange添加多条数据. 其参数可以是params或者集合.

可以看到这个和之前Add的Sql语句是完全不同的:

这个语句我不是很明白.

批量添加不同类型的数据:

使用context的AddRange或Add方法, DbContext可以推断出参数的类型, 并执行正确的操作. 上面的方法就是使用了DbContext.AddRange方法, 一次性添加了两种不同类型的model.

这两个方法对于写一些通用方法或者处理复杂的情况是很有用的.

Sql Server对于批量操作的限制是, 一次只能最多处理1000个SQL命令, 多出来的命令将会分批执行.

如果想更改这个限制, 可以这样配置参数:

简单查询.

针对DbSet, 使用Linq的ToList方法, 会触发对数据库对查询操作:

首先把Company的ToString方法写上:

这样方便输入到控制台.

然后写查询方法:

看结果:

EfCore到查询有两类语法, 一种是Linq方法, 另一种是Linq查询语法:

这种是Linq方法:

下面这种是Linq查询语法:

我基本都是使用第一种方法.

除了ToList(Async)可以触发查询以外, 遍历foreach也可以触发查询:

但是这种情况下, 可能会有性能问题. 因为:

在遍历开始的时候, 数据库连接打开, 并且会一直保持打开的状态, 直到遍历结束.

所以如果这个遍历很耗时, 那么可能会发生一些问题.

最好的办法还是首先执行ToList, 然后再遍历.

查询的过滤.

这部分和以前的EF基本没啥变化.

这个很简单, 不说了.

这里列一下可触发查询的Linq方法:

还有个两个方法是DbSet的方法, 也可以触发查询动作:

上面这些方法都应该很熟悉, 我就不写了.

过滤的条件可以直接家在上面的某些方法里面, 例如:

通过主键查询, 就可以用DbSet的Find方法:

这个方法有个优点, 就是如果这条数据已经在Context里面追踪了, 那么查询的时候就不查数据库了, 直接会返回内存中的数据.

EF.Functions.Like 这个方法是新方法, 就像是Sql语句里面的Like一样, 或者字符串的Contains方法:

这个感觉更像Sql语句, 输出到Console的Sql语句如下:

这里还要谈的是First/FirstOrDefault/Last/LastOrDefaut方法.

使用这些方法必须先使用OrderBy/OrderByDescending排序. 虽然不使用的话也不会报错, 但是, 整个过程就会变成这样, context把整个表的数据家在到内存里, 然后返回第一条/最后一条数据. 如果表的数据比较多的话, 那么就会有性能问题了.

更新数据.

很简单, context所追踪的model属性变化后, SaveChanges就会更新到数据库.

当然, 多个更新操作和插入等操作可以批量执行.

离线更新.

就是这种情况, 新的context一开始并没有追踪one这个数据. 通过使用Update方法, 追踪并设置状态为update. 然后更新到数据库.

可以看到, 在这种情况下, EfCore会更新该model到所有属性.

Update同样也有DbSet的UpdateRange方法, 也有context到Update和UpdateRange方法, 这点和Add是一样的.

还有一种方法用于更新, 这个以后再说.

删除数据.

DbContext只能删除它追踪的model.

非常简单, 从log可以看到, 删除动作只用到了主键:

如果是删除的离线model, 那么Remove方法首先会让Dbcontext追踪这个model, 然后设置状态为Deleted.

删除同样有RemoveRange方法.

Raw SQL查询/命令:

这部分请看文档:

命令: DbContext.Database.ExecuteSqlCommand();

查询: DbSet.FromSql() https://docs.microsoft.com/en-us/ef/core/querying/raw-sql;

这个方法目前还有一些限制, 它只能返回实体的类型, 并且得返回domain model所有的属性, 而且属性的名字必须也得一一对应. SQL语句不可以包含关联的导航属性, 但是可以配合Include使用以达到该效果(https://docs.microsoft.com/en-us/ef/core/querying/raw-sql#including-related-data).

更多的传递参数方式还需要看文档.

查询和保存关联数据.

插入关联数据.

我之前忘记在Department里面添加Name字段了, 现在添加一下, 具体过程就不写了.

插入关联数据有几种情况:

1.直接把要添加的Model的导航属性附上值就可以了, 这里的Department不需要写外键.

看一下Sql:

这个过程一共分两步: 1 插入主表, 2,使用刚插入主表数据的Id, 插入子表数据.

2.为数据库中的数据添加导航属性.

这时, 因为该数据是被context追踪的, 所以只需在它的导航属性添加新记录, 然后保存即可.

3.离线数据添加导航属性.

这时候就必须使用外键了.

预加载关联数据 Eager Loading.

也就是查询的时候一次性把数据和其导航属性的数据一同查询出来.

看看SQL:

这个过程是分两步实现的, 首先查询了主表, 然后再查询的子表. 这样做的好处就是性能提升.

(FromSql也可以Include).

预加载子表的子表:

可以使用ThenInclude方法, 这个可以老版本ef没有的.

这里查询Department的时候, 将其关联表Company也查询了出来, 同时也把Company的关联表Owner也查询了出来.

查询中映射关联数据.

使用Select可以返回匿名类, 里面可以自定义属性.

这个匿名类只在方法内有效.

看下SQL:

可以看到SQL中只Select了匿名类里面需要的字段.

如果需要在方法外使用该结果, 那么可以使用dynamic, 或者建立一个对应的struct或者class.

使用关联导航属性过滤, 但是不加载它们.

SQL:

这个比较简单. 看sql一切就明白了.

修改关联数据.

也会分两种情况, 被追踪和离线数据.

被追踪的情况下比较简单, 直接修改关联数据的属性即可:

看一下SQL:

确实改了.

这种情况下, 删除关联数据库也很简单:

看下SQL:

删除了.

下面来看看离线状态下的操作.

这里需要使用update, 把该数据添加到context的追踪范围内.

看一下SQL:

这个就比较怪异了.

它update了该departmt和它的company以及company下的其他department和company的owner. 这些值倒是原来的值.

这是因为, 看上面的代码, 查询的时候department的关联属性company以及company下的departments和owner一同被加载了.

尽管我只update了一个department, 但是efcore把其他关联的数据都识别出来了.

从DbContext的ChangeTracker属性下的StateManger可以看到有多少个变化.

这一点非常的重要.

如何避免这个陷阱呢?

可以这样做: 直接设置dbContext.Entry().State的值

这时, 再看看SQL:

嗯. 没错, 只更新了需要更新的对象.

 

 

2.1版本将于2018年上半年发布, 请查看官网的路线图: https://github.com/aspnet/EntityFrameworkCore/wiki/roadmap

完.

草根专栏, 草根的.net core专栏
分类: .Net Core

由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作

utorrent 出现 “由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作”tracker红肿如何解决:

答:
可能是你留作种的原因,所以tcp的端口(UserPort)请求已经达到你pc上本地设置的界限(MaxUserPort),可以试着修改此键值;
方法如下(修改前请备份好你的注册表文件,以免发生意外):
.启动注册表编辑器。
..在注册表中,找到以下子项,然后单击$参数(Parameters 翻译过来就是>>参数<<的意思)
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
…在编辑菜单中,单击新建,然后添加以下注册表项:
值名称: MaxUserPort
值类型: 双字节
值数据: 65534
有效范围: 5000-65534 (十进制)
默认值: 0x1388 (5000 十进制)
….退出注册表编辑器,然后重新启动计算机
参考:

TcpTimedWaitDelay和MaxUserPort设置与网络吞吐量

TcpTimedWaitDelay

    • 描述:确定 TCP/IP 在释放已关闭的连接并再次使用其资源前必须经过的时间。关闭与释放之间的这段时间称为 TIME_WAIT 状态或者两倍最大段生存期(2MSL)状态。此时间期间,重新打开到客户机和服务器的连接的成本少于建立新连接。通过减少此条目的值,TCP/IP 可以更快地释放关闭的连接,并为新连接提供更多资源。如果运行中的应用程序要求快速释放连接或创建新连接,或者由于多个连接处于 TIME_WAIT 状态而导致吞吐量较低,请调整此参数。
    • 如何查看或设置:

1.     使用regedit命令,访问 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters 注册表子键,然后创建新的 REG_DWORD 值TcpTimedWaitDelay。

2.     将此值设置为十进制30,即十六进制 0x0000001e。此值将等待时间设置为 30 秒。

3.     停止并重新启动系统。

    • 缺省值:0xF0,此值将等待时间设置为 240 秒(4 分钟)。
    • 建议值:最小值为0x1E,此值将等待时间设置为 30 秒。
  • MaxUserPort
    • 描述:确定当应用程序向系统请求获取可用的用户端口时,TCP/IP 可指定的最高端口号。
    • 如何查看或设置:

1.     使用regedit命令,访问 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters 注册表子键,然后创建新的 REG_DWORD 值MaxUserPort。

2.     将此值至少设置为十进制32768。

3.     停止并重新启动系统。

    • 缺省值:
    • 建议值:至少为十进制32768。
  • 最大连接储备
    • 描述:如果同时接收到许多连接尝试,请增大操作系统支持的缺省暂挂连接数。
    • 如何查看或设置:

1.     使用regedit命令并访问 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters 注册表子键。

2.     根据需要创建并设置下列值:

3. “EnableDynamicBacklog”=dword:00000001

4. “MinimumDynamicBacklog”=dword:00000020

5. “MaximumDynamicBacklog”=dword:00001000

6. “DynamicBacklogGrowthDelta”=dword:00000010

7.     这些值将最小可用连接数设置为 20,将最大可用连接数设置为 1000。每当可用连接数小于最小可用连接数时,可用连接数都会增加 10。

8. 停止并重新启动系统。

  • KeepAliveInterval
    • 描述:确定 TCP 在未接收到响应时重新尝试保持活动传输的频率。
    • 如何查看或设置:

1.     使用regedit命令,访问 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AFD\Parameters 注册表子键,然后创建新的 REG_DWORD 值KeepAliveInterval。

2.     将此值设置为1秒。

3.     停止并重新启动系统。

  • 缺省值:1秒
  • 建议值:
参考:

windows 分页缓冲池 非分页缓冲池

最近在windows server 2012机器上在做性能测试时,发现8G物理内存,内存使用率占到了90%多,在“进程”列表中所有进程内存相加才2个多G,同时任务管理器->“性能”标签一项中,非内存缓冲池很高占到了5个多G。

在网上找了一些资料,说是windows8系列有内存泄露的BUG,会引起“非内存缓冲池”一直占用很高。当前系统初步怀疑可能也是这个问题。

参考文档:

一次DB服务器性能低下引发的对Nonpaged Pool Leak问题的诊断

 http://tieba.baidu.com/p/2728129582

 

先使用poolmon.exe来分析哪个组件占用内存高,再对这个组件做相应的处理。

 

对于分页缓冲池与非页面缓冲池

PagedPool 和 NoPagedPool的区别Windows kernel pool

1、页面一直锁定在物理内存中,不会被换出到页面交换文件中

2、Windows把虚拟地址分为用户地址空间和系统地址空间,用户地址空间是给应用程序使用的,系统地址空间是给系统核心和驱动程序使用的。系统地址空间分为分页池和非分页池,分页池是指映射到分页文件的虚拟地址,当要使用该地址时才交换到物理内存中,由系统来调度;非分页池是指直接在物理内存中分配的内存。“页面缓冲池”就是进程占用的分页池中的虚拟内存,是进程调用某些系统功能时,由系统核心或者驱动程序分配的。如果一个程序占用的页面缓冲池内存不断增大,就是内存泄露,通常应该是创建或打开了句柄没有关闭。

 

在perfmon计数器里统计这两个参数时,momory对象与process对象里都存在相关的值(两种pool都会被映射到每一个进程空间内)。

Perfmon – Windows 自带系统监测工具

解决windows系统因TCP端口不足导致mysql数据库无法访问的问题

在windows服务器上面批量处理数据的时候,遇到下面的异常,意思是说连接数用完了,无法再建立连接。

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The driver was unable to create a connection due to an inability to establish the client portion of a socket.

This is usually caused by a limit on the number of sockets imposed by the operating system. This limit is usually configurable. 

For Unix-based platforms, see the manual page for the 'ulimit' command. Kernel or system reconfiguration may also be required.

For Windows-based platforms, see Microsoft Knowledge Base Article 196271 (Q196271).
    at sun.reflect.GeneratedConstructorAccessor16.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

默认情况下,Windows 允许用于使用5000 个临时(短命)TCP 端口。任何端口关闭后,它将在TIME_WAIT 状态保持120 秒。与重新初始化全新的连接相比,该状态允许以更低的开销重新使用连接。 但是,在该时间逝去前,无法再次使用该端口。
对于小的可用TCP 端口堆栈(5000 ),以及具有TIME_WAIT 状态的大量在短时间内打开和关闭的 TCP 端口,你很可能遇到端口耗尽问题。
我们可以通过修改注册表配置来解决问题:
1,启动注册表编辑器(Regedt32.exe )。

2,在注册表中确定下述键值的位置:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
在“编辑”菜单上点击“添加值”,然后增加下述注册值:
Value Name: MaxUserPort
Data Type: REG_DWORD
Value: 65534
它用于设置为任何用户提供的临时端口数。有效范围介于5000 和65534 之间(十进制)。默认值为0x1388 (5000 ,十进制)。

3,在“编辑”菜单上点击“添加值”,然后增加下述注册值:
Value Name: TcpTimedWaitDelay
Data Type: REG_DWORD
Value: 30
它用于设置关闭之前将TCP 端口连接保持在TIME_WAIT 状态的秒数。 有效范围介于0 秒和300 秒之间。默认值为0x78 (120 秒)。

4,退出注册表编辑器。

5,重启服务器。

代码访问 SQL Server数据库时的错误: 由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作

有一台服务器,运行sqlserver数据库,有两个网站模块的数据库在上面,负载压力不大,平时没出现过问题。后来另外一个L部门放了一个网站上去,数据库也在同一台机器上,突然有天L部门反应网站访问不了了,FTP也连不上了。但数据库能够远程连接上。

远程到服务器上,FTP服务重启,无效;网站重启,无效,查看系统日志,发现有如下错误:

xxx在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器。请验证实例名称是否正确并且 SQL Server 已配置为允许远程连接。 (provider: TCP Provider, error: 0 – 由于系统缓冲区空间不足或队列已满,不能执行套接字上的操作。)。

没碰到过这种情况,为了恢复服务,索性把服务器重启了,一切都好了。过了两天,又有这种情况出现。google一下,知道大概原因是端口用完了,猜测是不是L部门网站代码有问题,数据库连接泄露没关闭完?没有使用连接池?看了代码,都没问题。只好查看系统端口的使用情况:

1 命令行下输入 netstat -ano >>D://net.txt ,列出目前端口使用情况,如下大概有三千多条 ,注意红色端口的使用

TCP    222.122.222.222:2756    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2766    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2776    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2786   52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2796    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2806    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2816    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2828    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2838    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2851    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2860    52.122.120.88:80       CLOSE_WAIT      2144
TCP    222.122.222.222:2870    52.122.120.88:80       CLOSE_WAIT      2144
…….

发现本地在访问一个外网地址的80端口,对应PID是2144

2.命令行下输入 tasklist|findstr “2144”  查看 PID对应的进程

w3wp.exe    2144  services

看起来是一个网站,这时突然想起来,不久前发布了一个webservice,主要功能是用httpwebrequest分析提取某个网站的部分内容,返回给调用方

3.命令行下输入 C:\Windows\System32\inetsrv>appcmd list wp

WP “5648” (applicationPool:testmozhou)
WP “5664” (applicationPool:tqh.xxx.cn)
WP “1544” (applicationPool:www.lxxxg.com)
WP “2144” (applicationPool:data.tt.com)

确认问题来源,webservice多线程下,每个httpwebrequest没有及时释放问题所致

参考文章:http://blog.zhaojie.me/2010/08/lack-of-dynamic-ports-when-frequently-open-and-close-socket.html

REDIS分布式锁—完美实现

这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下。

第一篇是简单完美的实现,第二篇是用到的Redisson.

Redis分布式锁的正确实现方式

前言

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。


可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

代码实现

组件依赖

首先我们要通过Maven引入Jedis开源组件,在pom.xml文件加入下面的代码:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

 

加锁代码

正确姿势

Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

复制代码
复制代码
public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}
复制代码
复制代码

可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

错误示例1

比较常见的错误示例就是使用jedis.setnx()jedis.expire()组合实现加锁,代码如下:

复制代码
复制代码
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}
复制代码
复制代码

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。

错误示例2

复制代码
复制代码
public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}
复制代码
复制代码

这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用jedis.setnx()命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:

那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

复制代码
复制代码
public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

}
复制代码
复制代码

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

 

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

复制代码
复制代码
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}
复制代码
复制代码

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。


总结

本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件,链接在参考阅读章节已经给出。

转载自https://www.cnblogs.com/linjiqin/p/8003838.html

阿里云ECS的CPU100%排查

一、背景和现象
初创公司,架构lanmp,web前端和后端分开服务器,业务驱动主要是nginx和apache,nginx主要是处理静态文件和反向代理,前后端、搜索引擎、缓存、队列等附加的服务都是用docker容器部署。因为比较初级,上传文件和采集文件都是直接写在硬盘上,涉及到的目录共享,就在其中一台服务器存储并且nfs共享。我们暂且分为ECS1(apache1)、ECS2(apache2)、ECS3(nginx)。某天网站业务中断,但是没有报错。一直在等待响应,默认响应超时是一分钟,所以很基础高可用没有起到作用。中断10分钟左右,重启服务,提示“open too many files”,但是lsof统计没几个。因为初级处理不了,所以直接重启服务器,一段时间后一切恢复正常,可是第二天又来一次这种情况。
二、第一次出现后的排查思路
本来第一次发现这种问题的时候就要追查原因了,看了一下zabbix监控图像其中断了十分钟,包括网络、内存、CPU、硬盘、IO等监控数据。首先想到的是网络问题,结论是zabbix-servert获取不到了zabbix-agent采集的数据,估计就是网络不通了。
但是,这个结论站不住脚,因为我本身通过ssh登录服务器,并且命令输入无卡顿,不至于头文件都传不过来。后来一看阿里云的云监控,上面有数据,似乎也可以佐证网络这个说法,因为云监控是阿里云内部的监控,可以内网获取到监控数据。直到看CPU的使用率这项,发现有一段时间的CPU使用率100%。并且我重启的时候CPU恢复正常,不能说网络一定没问题,但系统肯定有问题。也可以解释因为CPU使用已经是100%,zabbix-agent和根本不能正常运行,所以没有监控数据。因为这个公司全部都是云服务器,没有使用IDC所以我们也没有安装smokeping来监控,接着我们就不把重心在网络上了。
目前掌握的信息就是:在毫无征兆的情况下,CPU暴涨到100%,重启之前一直保留,重启之后恢复原样。匆忙之中又看了一下系统各日志,因为太匆忙,没有总结,没有找到什么有价值的东西。现在有下面几种猜想:第一,程序的bug或者部署不当,触发之后耗尽资源。第二、docker容器的bug。第三、网络攻击。第四、病毒入侵。第五、阿里云方系统不稳定。
小总结了一下,现在问题还没有找出来。下次还有这个问题的可能,所以先尽量防范,但是又不能重启一刀切。所以在zabbix上面设置了自动化,当检测到ECS1获取不到数据的时候马上操作ECS3标记后端为ECS1的apache为down。保留异常现场。(请求停止的时候,CPU100%还在)
三、现场排查
1、相应的排查计划(想到这些信息需要获取的,实际上没有严格按照这样的步骤)
1)用htop和top命令监控CPU、内存使用大的进程。先看看哪个进程消耗资源较多,用户态、内核态、内存、IO……同时sar -b查io的历史定时抽样。
2)统计tcp连接数,看看有没有DDOS攻击。netstat -anp |grep tcp |wc -l 。用iftop-i eth1看看通讯。同时用tail -n 1200 /var/log/messages查看内核日志。
3)用pstree查看打开进程,ps aux|wc-l看看有没有特别多的进程。虽然zabbix监控上说没有,但是我们要检查一下看看有没有异常的进程名字。
4)查看全部容器的资源使用docker stats $(docker ps -a -q),看看能不能从容器上排查。
5)有了“too many open files”的启发,计算打开文件数目lsof|wc -l,根据进程看看ll /proc/PID/fd文件描述符有没有可疑的打开文件、文件描述符。
6)关于用lsof打开文件数找到的线索,排序打开文件找出进程号 lsof -n|awk ‘{print $2}’|sort|uniq -c|sort -nr|more
7)关于用lsof打开文件数找到的线索,用lsof -p PID查看进程打开的句柄。直接查看打开的文件。
8)启动容器的时候又总是“open too many files”。那就是打开文件数的问题,因为CPU的使用率是CPU的使用时间和空闲时间比,有可能因为打开文件数阻塞而导致CPU都在等待。针对连接数的问题,大不了最后一步试试echo 6553500 > /proc/sys/fs/file-max 测试打开文件对CPU的影响。
9)玩意测出来了消耗CPU的进程,可以使用strace最终程序。用户态的函数调用跟踪用「ltrace」,所以这里我们应该用「strace」-p PID
10)从程序里面看到调用系统底层的函数可以跟踪。跟踪操作 strace -T -e * -p PID,主要看看代码调用的函数有没有问题。
2、现场排查
第二天同样时间,ECS果然暴涨了CPU。这是时候zabbix的工作如希望进行保留了一台故障的ECS1给我。
1)用htop看到资源使用最大是,搜索引擎下我写的一个判断脚本xunsearch.sh。脚本里面很简单,判断索引和搜索服务缺一个就全部重启。就当是我的容器有问题我直接关掉搜索引擎容器。httpd顶上,我又关掉apache容器。rabbitmq相关进程又顶上。这时候我没心情周旋了,肯定不也是这个原因。sar -b查看的历史io也没有异常。
2)统计tcp连接,几百。先不用着重考虑攻击了。用tail -n 1200 /var/log/messages查看内核日志,是TCP TIME WAIT的错误。可以理解为CPU使用100%,程序无响应外面的tcp请求超时。这是结果,还是没有找到根本原因。
接着往下看系统内核日志,发现了和“open too many files”呼应的错误,“file-max limit 65535 reached”意思是,已到达了文件限制瓶颈。这里保持怀疑,继续收集其他信息。
3)查看进程数量,数量几百。列出来也看到都是熟悉的进程,可以先排除异常进程。
4)监控容器的资源使用,里面很不稳定,首先是xunsearch容器使用80%的CPU,关掉xunsearch,又变成了其他容器使用CPU最高。很大程度上可以排查容器的问题和执行程序的问题。
5)查看了最大连接数cat /proc/sys/fs/file-max是65535但是用lsof查到的连接数是10000多,完全没有达到连接数。
6)各项参数都正常,现在聚焦在打开的文件数这个问题上面。也可以用另外同一种方式查看一下内核统计文件 /proc/sys/fs/file-nr,比较一下差异,看看能不能找出问题。cat了一下,打开文件数是66080,果然超了!内核日志就以这个为标准。
但是看lsof怎么统计不出来,ll /proc/PID/fd也没几个。这个问题放在后面,先按照步骤echo 6553500 > /proc/sys/fs/file-max给连接数提高到100倍,CPU果然降了下来。原因确认了,但是必须找到根源,为什么忽然有这么大的打开文件数。关掉全部docker容器和docker引擎,打开文件数是少了一点,但是仍然在65535差不多。我就先排除一下业务的影响,把ECS3的nginx直接指向视频ECS2的apache,就等同于在ECS2上实现了ECS1的场景。查看一下ECS2的句柄数,才4000多,排除了业务相关应用对服务器的影响。那就能下个小结论,ECS1被神秘程序打开了6万多句柄数,打开业务就多了2000多的句柄数,然后就崩溃了。不过这个现象有点奇怪,ECS2和ECS1在一样的机房一样的配置一样的网络环境,一样的操作系统,一样的服务,一样的容器,为什么一个有问题,一个没问题呢?不同的只是有一台是共享nfs。难道是静态文件共享了,其他人读了,也算是本服务器打开的?
7)现在程序找不到,没法继续lsof -p了。排查之前的猜想。带着排查得到对的结论往下想。
程序的bug和部署不当,那是不可能的,因为主要问题来自于打开句柄数,当部署到ECS2那里,一切正常。docker容器的bug,那也不可能的,每个都是我亲自写脚本,亲自编译,亲自构建的,关键是我关掉了docker容器和引擎都没有很大改善。网络攻击也排除,因为网络连接数没几个,流量也不变。那就只剩下病毒入侵也不是,没有异常进程。考虑到ECS的稳定性问题了。这方面就协助阿里云工程师去排查。
8)阿里云工程师用的排查手段和我差不多,最终也是没能看到什么。也只是给了我一些治标不治本的建议。后来上升到专家排查,专家直接在阿里云后端抓取了coredump文件分析打开的文件是图片,程序是nfsd。
好像印证了我刚才后面的猜想,应该就是ECS1使用了nfs共享其他服务器打开了然后算在ECS1头上。那问题又来了,我们的业务已经到达了可以影响服务器的程度吗?
9)既然问题解决到这一步,先不管程序有没有关闭打开的文件和nfs的配置。我们架构上面的图片应该是归nginx读取,难道是linux的内存机制让它缓存了。带着缓存的问题,首先去ECS3上释放内存echo 3 > /proc/sys/vm/drop_caches,释放之后,发现没什么改善,有点失落。总是觉得还有一台后端是PHP主导,但是逻辑上是写入,没有打开文件之说。后来从程序员中了解到,PHP也有打开图片。我猛然去ECS2释放一下内存,果然,句柄数降下来。(这里大家一定有个疑问,为什么我直接想到内存缓存而不是目前打开的文件呢。其一,这是生产环境,web前端只有一个,不能乱来停服务。其二,第一次遇到问题的时候,重启之后没有问题,过了一天之后积累到一定的程度才爆发,这里已经引导了我的思路是积累的问题,那就是缓存不断积累了)
10)因为ECS2的调用ECS1的nfs共享文件,所以lsof也有读不到那么多句柄数的理由。如果说是nfs的服务本身就有缓存,导致问题的话,我查看了配置文件,还是默认值允许缓存,30S过期,根本不会因为nfs的缓存造成打开文件过多。如果我们的后端程序打开之后没好好处理的话,那倒有可能。然后尝试排除:我改了ECS3的配置,使程序只读ECS1后端,从ECS1上面却看不到有什么异常表现,说明PHP程序已经好好处理了打开的文件。也不是docker挂载了nfs的共享的问题,因为nginx也有挂载。排查到这里也很大程度上解决问题,而且缓存了nfs的全部共享文件,句柄并没有增加,也算合理,所以就增加了打开文件数的限制。
11)现在排查的结果是跟后端和nfs共享有关。就是说,后端挂载了nfs的网络共享,被程序读取。而程序释放之后,在正常背景的硬盘文件是没有缓存的。但是在nfs挂载的环境下,缓存并没有得到释放。
12)总结:很多问题的排查和我们的猜想结果一样,但是有些例外的情况。比如这次我想到的原因都一一排除,但是问题也是在一步步排查中,逐步被发现的。

Machine Learning 机器学习笔记

第一周:Welcome

第二周:Linear Regression with Multiple Variables

第三周:Logistic Regression

第四周:Neural Networks: Representation

第五周:Neural Networks: Learning

第六周:Advice for Applying Machine Learning

第七周:Support Vector Machines

第八周:Unsupervised Learning

第九周:Anomaly Detection

第十周:Large Scale Machine Learning

第十一周:Application Example: Photo OCR


GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: github.com/halfrost/Ha…

作者:一缕殇流化隐半边冰霜
链接:https://juejin.im/post/5ab98efb518825558b3df021
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如何从零开始搭建高性能直播平台

前言

现在直播已经成为移动互联网时代一个新的重要流量入口,从YY、斗鱼到花椒直播,直播已经成为人们分享交流的新方式,应用场景众多,主要分为:

  • 金融类直播:金融直播可应用于实时解盘,在线专家讲座,专家在线直播技术分析、指导投资者等使用场景。
  • 大型赛事,演唱会类直播:可应用于大型演唱会,音乐会,游戏,体育赛事等类直播场景。
  • 互动类直播:娱乐类互动,如YY等。
  • 会议类直播:大型会议直播。

等4大类。在本文中,我将先从rtmp协议开始,一步步带领大家搭建一个简易高性能的直播平台。

RTMP协议详解

RTMP协议Real Time Message Protocol(实时信息传输协议)的首字母缩写,是由Adobe公司开发的一种用于解决多媒体数据传输流多路复用和分包的网络协议。它工作在TCP协议之上,因此是一种提供可靠交付的协议,在传输时不会出现丢包情况,从而保证了用户体验(QoE)。虽然TCP协议为了提供可靠交付付出了一些额外的开销做为代价,占用了一些带宽和处理器资源,但是随着网络带宽的提高和硬件的发展,这些开销会显得越来越微不足道。因此RTMP协议在为了有很好的发展前景。

官方定义:

The Real-Time Messaging Protocol (RTMP) was designed for

high-performance transmission of audio, video, and data between Adobe

Flash Platform technologies, including Adobe Flash Player and Adobe

AIR. RTMP is now available as an open specification to create products

and technology that enable delivery of video, audio, and data in the

open AMF, SWF, FLV, and F4V formats compatible with Adobe Flash

Player.

协议分类

  • RTMP协议工作在TCP之上,是应用层协议, 默认的端口是1935。
  • RTMPE在RTMP的基础上增加了加密功能。
  • RTMPT工作在HTTP之上,默认端口是80或443,可穿透防火墙。
  • RTMPS类似RTMPT,增加了TLS/SSL的安全功能。
  • RTMFP为RTMP协议的UDP版本。

虽然协议变种有很多,但实际在我们的直播应用中最常见的是原生的RTMP协议,因此本篇文章以该协议的1.0版本为基础,对其它演进协议感兴趣的同学可以关注本文的后续知识。

交互流程

RTMP的交互流程可以分为握手过程、控制命令传输与数据传输。

enter image description here

  • 握手过程

    RTMP 连接以握手开始,RTMP 握手由三个固定长度的块组成。客户端 (发起连接请求的终端) 和服务器端各自发送相同的三块。便于演示,本文将从客户端发送的这些块指定为 C0、C1 和 C2;将从服务器端发送的这些块分别指定为 S0、S1 和 S2。

    RTMP握手以客户端发送 C0 和 C1 块开始,客户端要等收到S1之后才能发送C2,客户端要等收到S2之后才能发送其他信息(控制信息和真实音视频等数据),服务端要等到收到C0之后发送S1, 服务端必须等到收到C1之后才能发送S2, 服务端必须等到收到C2之后才能发送其他信息(控制信息和真实音视频等数据)。以下为RTMP握手的时序图介绍。

    enter image description here

流程图中所提到的各种状态如下:

状态 描述
未初始化 客户端在C0中发送协议版本,如服务端支持,则回发送S0和S1,如果不能,则连接结束
版本发送 客户端等待S1包,服务端等待C1包,当接收到想要的包,客户端发送C2,服务端发送S2,此时阶段变成了ACK的发送
ACK发送 客户端和服务端分别等待S2和C2
握手完成 客户端和服务交换消息

理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的。但在实际实现中为了尽量减少通信的次数,客户端发送C0+C1,服务端发送S0+S1+S2,再客户端在发送C2结束握手。

enter image description here

  • 控制命令传输

    握手结束以后,RTMP协议进入控制命令传输过程,客户端通过发送connnect命令与服务器实现双向连接。连接成功后,通过发送createStream命令建立网络流。

  • 数据传输

    网络流建立成功后,推流(将直播内容推送至服务器的过程)过程会发送publish命令发布音视频内容,拉流(服务器已有直播内容,用指定地址进行拉取的过程)过程会发送play命令播放内容。

协议格式

URI格式

rtmpt://127.0.0.1/{app}/{stream_name}

  • {app}为音频/视频和其他内容定义的一个容器。
  • {stream_name}为具体的一个流名称。

消息(Message)格式

enter image description here

消息是RTMP协议中基本的数据单元,不能种类的消息包含有不能的消息类型(Message Type)。RTMP协议一共规范了十多种消息类型。其中类型为8,9的消息分别用于传输音频和视频数据。消息头包含以下信息:

  • Message Type: 消息类型,占用1个字节。
  • Length: 有效负载的字节数,占用3个字节。该字段是用大字节序表示的。
  • Timestamp: 时间戳,占用4个字节,用大字节序表示。
  • Message Stream Id: 消息流ID,标识消息所使用的流,用大字节序表示。

消息块格式

在网络上传输数据时,消息需要被拆分成较小的数据块才适合在相应的网络环境上传输。RTMP协议中规范了对消息拆分成消息块,每个消息块首部(ChunkHeader)有三部分组成:用于标识本块的ChunkBasicHeader,用于标识本块负载所属消息的ChunkMessageHeader,以及当时间戳溢出时才出现的ExtendedTimestamp。

消息分块

RTMP传输媒体数据的过程中,发送端首先把媒体数据封装成消息,然后把消息分割成消息块,最后将分割后的消息块通过TCP协议发送出去。接收端在通过TCP协议收到数据后,首先把消息块重新组合成消息,然后通过对消息进行解封装处理就可以恢复出媒体数据。

开源技术选型

目前直播服务器有开源和商业两种版本,商业版本主要又FMS(Flash Media Server)与Wowza。本文章仅针对开源版本做介绍,相应的开源项目主要分为Red5与 Nginx-Rtmp两类:

Red5

简介

GitHub:https://github.com/Red5/red5-server (1k+ stars)

enter image description here

Red5是一个采用Java开发开源的Flash流媒体服务器。它支持:把音频(MP3)和视频(FLV)转换成播放流; 录制客户端播放流(只支持FLV);共享对象;现场直播流发布;远程调用。Red5使用RTMP, RTMPT, RTMPS, 和RTMPE作为流媒体传输协议,在其自带的一些示例中演示了在线录制,flash流媒体播放,在线聊天,视频会议等一些基本功能。

官方给出的主要特性: Red5 is an Open Source Flash Server written in Java that supports:

  • Streaming Video (FLV, F4V, MP4, 3GP)
  • Streaming Audio (MP3, F4A, M4A, AAC)
  • Recording Client Streams (FLV and AVC+AAC in FLV container)
  • Shared Objects
  • Live Stream Publishing
  • Remoting
  • Protocols: RTMP, RTMPT, RTMPS, and RTMPE

Additional features supported via plugin:

  • WebSocket (ws and wss)
  • RTSP (From Axis-type cameras)
  • HLS

安装与简单应用实例(Mac下安装)

前置条件(jdk已安装)。

enter image description here

  • 创建安装目录:

mkdir -p /Users/ypzdw/gitchat/rtmp/red5

  • 设置主目录环境变量:

export RED5_HOME=/Users/ypzdw/gitchat/rtmp/red5

  • 下载red5应用,并解压到RED5_HOME:

https://github.com/Red5/red5-server/releases/download/v1.0.7-RELEASE/red5-server-1.0.7-RELEASE.tar.gz

enter image description here

目录简介:由于 Red5 是在 Tomcat 中运行的,因此 Red5 项目与普通 JAVAEE 项目结构类似 conf:red5配置目录 lib:存放的是一些依赖jar包
weapps:用来存放应用程序,与tomcat下的webapps目录作用类似。

  • 运行:

cd /Users/ypzdw/gitchat/rtmp/red5 ./red5.sh &

  • 简单实例

经过前面的介绍,这里将用red5介绍一个简单的实例。打开http://127.0.0.1:5080 出现red5 管理控制台。

enter image description here

选择”Publisher” demo,该项目提供了主播端与听课端。

enter image description here

主播端:“1”中 Name表示流名,publish可以发布一个直播。

enter image description here

直播收听端:“1”中Name为收听的流名;“2”中 Location为直播端地址;“3”中Log可以观察到整个直播的交流日志。

enter image description here

enter image description here

  • 停止应用:

cd /Users/ypzdw/gitchat/rtmp/red5 ./red5-shutdown.sh

Nginx-Rtmp

Github:https://github.com/arut/nginx-rtmp-module (5k+ stars)

enter image description here

简介

俄罗斯人民开发的一款NGINX的流媒体插件,除了直播发布音视频流之外具备流媒体服务器的常见功能:

  • RTMP在线直播。
  • 基于HTTP的FLV/MP4 VOD点播。
  • HLS (HTTP Live Streaming) M3U8的支持。
  • 基于http的操作(发布、播放、录制)。
  • 可以很好的协同现有的流媒体服务器以及播放器一起工作。
  • 在线调用ffmpeg对流媒体进行转码。
  • H264/AAC音视频编码格式的支持。
  • linux/BSD/MAC系统的支持。

官方承诺的功能:

  • RTMP/HLS/MPEG-DASH live streaming
  • RTMP Video on demand FLV/MP4, playing from local filesystem or HTTP
  • Stream relay support for distributed streaming: push & pull models
  • Recording streams in multiple FLVs
  • H264/AAC support
  • Online transcoding with FFmpeg
  • HTTP callbacks (publish/play/record/update etc)
  • Running external programs on certain events (exec)
  • HTTP control module for recording audio/video and dropping clients
  • Advanced buffering techniques to keep memory allocations at a minimum level for faster streaming and low memory footprint
  • Proved to work with Wirecast, FMS, Wowza, JWPlayer, FlowPlayer, StrobeMediaPlayback, ffmpeg, avconv, rtmpdump, flvstreamer and many more
  • Statistics in XML/XSL in machine- & human- readable form
  • Linux/FreeBSD/MacOS/Windows

常用指令与语法

  • Core

    rtmp

语法:rtmp { … } 上下文:nginx根上下文 描述:保存所有 RTMP 配置的块

server

语法:server { … } 上下文:rtmp 描述:声明一个 RTMP 实例。 rtmp { server { } }

listen

语法:listen (addr[:port]|port|unix:path) 上下文:server 描述:给 NGINX 添加一个监听端口以接收 RTMP 连接。 server { listen 1935; }

application

语法:application name { … } 上下文:server 描述:创建一个 RTMP 应用,application 名不支撑正则表达式。 server { listen 1935; application myapp { } }

timeout

语法:timeout value 上下文:rtmp, server 描述:Socket 超时。这个值主要用于写数据时。timeout 60s;

ping

语法:ping value 上下文:rtmp, server 描述:RTMP ping 间隔。零值的话将 ping 关掉。RTMP ping 是一个用于检查活动连接的协议功能。发送一个特殊的包到远程连接,然后在 ping_timeout 指令指定的时间内期待一个回复。如果在这个时间里没有收到 ping 回复,连接断开。ping 默认值为一分钟。pingtimeout 默认值为 30 秒。 ping 3m; pingtimeout 30s;

  • Access

    allow

语法:allow [play|publish] address|subnet|all 上下文:rtmp, server, application 允许来自指定地址或者所有地址发布/播放。allow 和 deny 指令的先后顺序可选。 allow publish 127.0.0.1; deny publish all; allow play 192.168.0.0/24; deny play all;

deny

语法:deny [play|publish] address|subnet|all 上下文:rtmp, server, application 描述:参考 allow 的描述。

  • Exec

    exec_push

语法:exec_push command arg* 上下文:rtmp, server, application 描述:定义每个流发布时要执行的带有参数的外部命令。发布结束时进程终止。第一个参数是二进制可执行文件的完整路径。执行外部命令时可以使用参数替换: \$name – 流的名字。 \$app – 应用名。 \$addr – 客户端地址。 \$flashver – 客户端 flash 版本。 \$swfurl – 客户端 swf url。 \$tcurl – 客户端 tc url。 \$pageurl – 客户端页面 url。也可以在 exec 指令中定义 Shell 格式的转向符用于写输出和接收输入。

exec_pull

与exec_push类似,主要工作在play端。

  • Live

    live

语法:live on|off 上下文:rtmp, server, application 描述:切换直播模式,即一对多广播。live on;

meta

语法:meta on|off 上下文:rtmp, server, application 描述:切换发送元数据到客户端。默认为 on。 meta off;

interleave

语法:interleave on|off 上下文:rtmp, server, application 描述:切换交叉模式。在这个模式下,音频和视频数据会在同一个 RTMP chunk 流中传输。默认为 off。 interleave on;

wait_key

语法:wait_key on|off 上下文:rtmp, server, application 描述:使视频流从一个关键帧开始。默认为 off。 wait_key on;

wait_video

语法:waitvideo on|off 上下文:rtmp, server, application 描述:在第一个视频帧发送之前禁用音频。默认为 off。可以和 waitkey 进行组合以使客户端可以收到具有所有其他数据的视频关键帧。然而这通常增加连接延迟。您可以通过在编码器中调整关键帧间隔来减少延迟。 wait_video on;

publish_notify

语法:publish_notify on|off 上下文:rtmp, server, application 描述:发送 NetStream.Publish.Start 和 NetStream.Publish.Stop 给用户。默认为 off。 publish_notify on;

dropidlepublisher

语法:dropidlepublisher timeout 上下文:rtmp, server, application 描述:终止指定时间内闲置(没有音频/视频数据)的发布连接。默认为 off。注意这个仅仅对于发布模式的连接起作用(发送 publish 命令之后)。 drop_idle_publisher 10s;

sync

语法:sync timeout 上下文:rtmp, server, application 描述:同步音频和视频流。如果用户带宽不足以接收发布率,服务器会丢弃一些帧。这将导致同步问题。当时间戳差超过 sync 指定的值,将会发送一个绝对帧来解决这个问题。默认为 300 ms。 sync 10ms;

play_restart

语法:play_restart on|off 上下文:rtmp, server, application 描述:使 nginx-rtmp 能够在发布启动或停止时发送 NetStream.Play.Start 和 NetStream.Play.Stop 到每个用户。如果关闭的话,那么每个用户就只能在回放的开始和结束时收到这些通知了。默认为 on。 play_restart off;

  • Record

    record

语法:record [off|all|audio|video|keyframes|manual]* 上下文:rtmp, server, application, recorder 描述:切换录制模式。流可以被记录到 flv 文件。本指令指定应该被记录的: off – 什么也不录制 all – 音频和视频(所有) audio – 音频 video – 视频 keyframes – 只录制关键视频帧 manual – 用不自动启动录制,使用控制接口来启动/停止 在单个记录指令中可以有任何兼容的组合键。 record all;

record_path

语法:record_path path 上下文:rtmp, server, application, recorder 描述:指定录制的 flv 文件存放目录。 record_path /tmp/rec;

record_suffix

语法:recordsuffix value 上下文:rtmp, server, application, recorder 描述:设置录制文件后缀名。默认为 ‘.flv’。 recordsuffix recorded.flv; 录制后缀可以匹配 strftime 格式。以下指令 recordsuffix -%d-%b-%y-%T.flv 将会产生形如 mystream-24-Apr-13-18:23:38.flv 的文件。所有支持 strftime 格式的选项可以在 strftime man page 里进行查找

record_append

语法:record_append on|off 上下文:rtmp, server, application, recorder 描述:切换文件附加模式。当这一指令为开启是,录制时将把新数据附加到老文件,如果老文件丢失的话将重新创建一个。文件中的老数据和新数据没有时间差。默认为 off。 record_append on;

  • Relay

    pull

语法:pull url [key=value]* 上下文:application 描述:创建 pull 中继。流将从远程服务器上拉下来,成为本地可用的。仅当至少有一个播放器正在播放本地流时发生。 Url 语法:[rtmp://]host[:port][/app[/playpath]]。如果 application 找不着那么将会使用本地 application 名。如果找不着 playpath 那么就是用当前流的名字。 支持以下参数: app:明确 application 名。 name:捆绑到 relay 的本地流名字。如果为空或者没有定义,那么将会使用 application 中的所有本地流。 tcUrl:如果为空的话自动构建。 pageUrl:模拟页面 url。 swfUrl:模拟 swf url。 flashVer:模拟 flash 版本,默认为 ‘LNX.11,1,102,55’。 playPath:远程播放地址。 live:切换直播特殊行为,值:0,1。 start:开始时间。 stop:结束时间。 static:创建静态 pull,这样的 pull 在 nginx 启动时创建。 如果某参数的值包含空格,那么你应该在整个 key=value 对周围使用引号,比如:’pageUrl=FAKE PAGE URL’。 pullrtmp://cdn2.example.com/another/a?b=1&c=d pageUrl=http://www.example.com/video.html swfUrl=http://www.example.com/player.swf live=1;

push

语法:push url [key=value]* 上下文:application 描述:push 的语法和 pull 一样。不同于 pull 指令的是 push 推送发布流到远程服务器。

push_reconnect

语法:push_reconnect time 上下文:rtmp, server, application 描述:在断开连接后,在 push 重新连接前等待的时间。默认为 3 秒。 push_reconnect 1s;

session_relay

语法:session_relay on|off 上下文:rtmp, server, application 描述:切换会话 relay 模式。在这种模式下连接关闭时 relay 销毁。当设置为 off 时,流关闭,relay 销毁,这样子以后另一个 relay 可以被创建。默认为 off。 session_relay on;

  • Notify

    on_connect

语法:on_connect url 上下文:rtmp, server 描述:设置 HTTP 连接回调。当客户分发连接命令一个连接命令时,一个 HTTP 请求异步发送,命令处理将被暂停,直到它返回结果代码。当 HTTP 2XX 码(成功状态码)返回时,RTMP 会话继续。返回码 3XX (重定向状态码)会使 RTMP 重定向到另一个从 HTTP 返回头里获取到的 application。否则(其他状态码)连接丢弃。 注意这一指令在 application 域是不允许的,因为 application 在连接阶段还是未知的。 HTTP 请求接收到一些参数。在 application/x-www-form-urlencoded MIME 类型下使用 POST 方法。以下参数将被传给调用者: call=connect。 addr – 客户端 IP 地址。 app – application 名。 flashVer – 客户端 flash 版本。 swfUrl – 客户端 swf url。 tcUrl – tcUrl。 pageUrl – 客户端页面 url。 除了上述参数以外,所有显式传递给连接命令的参数也由回调发送。你应该将连接参数和 play/publish 参数区分开。播放器常常有独特的方式设置连接字符串不同于 play/publish 流名字。

on_play

语法:on_play url 上下文:rtmp, server, application 描述:设置 HTTP 播放回调。每次一个客户分发播放命令时,一个 HTTP 请求异步发送,命令处理会挂起 – 直到它返回结果码。之后再解析 HTTP 结果码。 HTTP 2XX 返回码的话继续 RTMP 会话。 HTTP 3XX 返回码的话 重定向 RTMP 到另一个流,这个流的名字在 HTTP 返回头的 Location 获取。 HTTP 请求接收到一些个参数。在 application/x-www-form-urlencoded MIME 类型下使用 POST 方法。以下参数会被传送给调用者: call=play。 addr – 客户端 IP 地址。 app – application 名。 flashVer – 客户端 flash 版本。 swfUrl – 客户端 swf url。 tcUrl – tcUrl。 pageUrl – 客户端页面 url。 name – 流名。

on_publish

语法:onpublish url 上下文:rtmp, server, application 描述:同上面提到的 onplay 一样,唯一的不同点在于这个指令在发布命令设置回调。不同于远程 pull,push 在这里是可以的。

on_done

语法:on_done url 上下文:rtmp, server, application 描述:设置播放/发布禁止回调。上述所有适用于此。但这个回调并不检查 HTTP 状态码。

onplaydone

语法:onpublishdone url 上下文:rtmp, server, application 描述:等同于 on_done 的表现,但只适用于播放结束事件。

onpublishdone

语法:onpublishdone url 上下文:rtmp, server, application 描述:等同于 on_done 的表现,但只适用于发布结束事件。

onrecorddone

语法:onrecorddone url 上下文:rtmp, server, application, recorder 描述:设置 recorddone 回调。除了普通 HTTP 回调参数它接受录制文件路径。 onrecord_done http://example.com/recorded;

on_update

语法:onupdate url 上下文:rtmp, server, application 描述:设置 update 回调。这个回调会在 notifyupdatetimeout 期间调用。如果一个请求返回结果不是 2XX,连接禁止。这可以用来同步过期的会话。追加 time 参数即播放/发布调用后的秒数会被发送给处理程序。 onupdate http://example.com/update;

notifyupdatetimeout

语法:notifyupdatetimeout timeout 上下文:rtmp, server, application 描述:在 onupdate 回调之间的超时设置。默认为 30 秒。 notifyupdatetimeout 10s; onupdate http://example.com/update;

notifyupdatestrict

语法:notifyupdatestrict on|off 上下文:rtmp, server, application 描述:切换 onupdate 回调严格模式。默认为 off。当设置为 on 时,所有连接错误,超时以及 HTTP 解析错误和空返回会被视为更新失败并导致连接终止。当设置为 off 时只有 HTTP 返回码不同于 2XX 时导致失败。 notifyupdatestrict on; onupdate http://example.com/update;

notifyrelayredirect

语法:notifyrelayredirect on|off 上下文:rtmp, server, application 描述:使本地流可以重定向为 onplay 和 onpublish 远程重定向。新的流名字是 RTMP URL 用于远程重定向。默认为 off。 notifyrelayredirect on;

notify_method

语法:notifymethod get|post 上下文:rtmp, server, application, recorder 描述:设置 HTTP 方法通知。默认是带有 application/x-www-form-urlencoded 的 POST 内容类型。在一些情况下 GET 更好,例如如果你打算在 nginx 的 http{} 部分处理调用。在这种情况下你可以使用 arg* 变量去访问参数。 notify_method get; Statistics statistics 模块不同于本文列举的其他模块,它是 NGINX HTTP 模块。因此 statistics 指令应该位于 http{} 块内部。

rtmp_stat

语法:rtmpstat all 上下文:http, server, location 描述:为当前 HTTP location 设置 RTMP statistics 处理程序。RTMP statistics 是一个静态的 XML 文档。可以使用 rtmpstatstylesheet 指令在浏览器中作为 XHTML 页面查看这个文档。 http { server { location /stat { rtmpstat all; rtmpstatstylesheet stat.xsl; } location /stat.xsl { root /path/to/stat/xsl/file; } } }

rtmpstatstylesheet

语法:rtmpstatstylesheet path 上下文:http, server, location 描述:添加 XML 样式表引用到 statistics XML 使其可以在浏览器中可视。

测试

我所在的公司的直播业务中,前期也是采用red5,但是随着用户数的不断增长,red5完全不能支撑整个业务。问题集中爆发在几个方面:

  • 对于单主播,听者超过400人时,CPU超过90%(主机为4核,32G)。
  • 人数越多,音质,画面卡顿很多,不稳定,用户体验很差。于是我们决定对red5进行替换,对各种选型进行了调研,并在red5相同环境下做了测试,发现nginx-rtmp的性能非常突出,最终选用nginx-rtmp替换Red5,到目前为止,已经无故障运行近一年。附nginx-rtmp测试数据:
Server CPU 内存 连接数 带宽 延迟
nginx-rtmp 8.3% 13MB 500 100Mbps 0.8秒
nginx-rtmp 27.3% 19MB 1000 200Mbps 0.8秒
nginx-rtmp 50.2% 37MB 2500 500Mbps 0.8秒
nginx-rtmp 70.2% 61MB 4000 650Mbps 0.8秒

从测试结果可以得知,nginx-rtmp模块运行稳定,单CPU4000人时负载只有70%,已经接近网卡流量的极限,比Red5 在性能上高一个数量级。

快速实现一个直播平台

实战前准备

  • 硬件

阿里云ECS: CPU:2核心,内存:8G,硬盘:40G

enter image description here

  • 操作系统

CentOS 7.2 x86_64 Linux

  • 配置服务器

    超过1024的连接数测试需要打开linux的限制。且必须以root登录和执行

    设置连接数:ulimit -HSn 10240

    查看连接数:

    enter image description here

  • 域名

    非必须,如没有,可以直接使用ip也可以。但是正式环境中为了减少收听端对ip地址的依赖性,一般会使用域名而非ip地址来连接直播服务器。本文使用域名为datahq.cn.

    申请域名:国内可以选择万网或新网;国外的name.com和godaddy.com口碑不错。

    域名备案:国外的服务器和网站在上线前都需要经过工信部备案和公安部备案;如果在阿里云上购买ECS可以直接使用其的免费备案服务。

    建立直播子域名:

    enter image description here

  • 客户端

    为方便测试,本文使用Red5 作为直播的客户端

  • 直播端

    目前最好用的直播端软件是OBS(Open Broadcaster Software)。下载地址是:

    https://obsproject.com/download

    快速设置:

    视频的清晰度与码率和品质有关,码率大,品质高,那么视频的清晰度就高,同时,对带宽的要求也越大。详细的参数设定参考如下:

  • 在来源中新增“视频捕捉设备”,并设置分辨率,您可以从分辨率选择最接近的一项。本例分辨率为1280×720,如下图所示

    enter image description here

  • 通过“设置”->“视频”中设置压缩分辨率,您可以从压缩分辨率选择与自己期望最接近的一项。本例期望分辨率为960×540,如下图所示

    enter image description here

  • 直播推流设置,打开“设置”->”流”,URL输入推流地址的URL,流名称输入推流的名称,如下图所示

    enter image description here

实战

  • 架构方案

    目前,我们线上的直播架构为:

    enter image description here

  • 支撑线上峰值近10万人,并无故障运行一年有余。
  • 配置中心会定期刷新直播端与收听端APP的路由信息。
  • 直播端APP推流到Master集群。
  • 由于nginx-rtmp本身不支持集群,因此我们在架构时没有采用从Master集群Forward推流到Slave集群的方式,而是设计了当收听端拉流到slave服务器集群时,如果不存在该流,就会从Master集群主动拉取流的架构,解决了直播集群的大规模并发问题。
  • 该架构在大规模并发情况下,比Master推流到Slave流的架构节省了很大的带宽。
  • 除此之外我们对nginx-rtmp进行了源代码的修改,支撑了一些如合成,转码,高级录制等功能。

本文为了各位同学能够更快的掌握如何搭建的过程,因此没有采用以上的架构,相信通过下面的实践,各位也能够搭建相应的架构。

enter image description here

架构图中黄色标识了我们要使用的直播server。

  • 编译与部署

    创建源码存储目录:mkdir -p /root/rtmp/src

    进入源码目录:

cd /root/rtmp/src

下载nginx源码并解压,注意nginx-rtmp对nginx版本的选择限制较多,在选择时为了少踩不需要的坑,建议根据官方提示选择对应的nginx 版本,如图本文选择nginx-1.11.5:

enter image description here

wget http://nginx.org/download/nginx-1.11.5.tar.gz tar -zxvf nginx-1.11.5.tar.gz

下载nginx-rtmp模块源码:

wget https://github.com/arut/nginx-rtmp-module/archive/v1.1.10.tar.gz tar -zxvf v1.1.10.tar.gz

编译nginx,如果需要调试消息则打开–with-debug:

./configure –add-module=/root/rtmp/src/nginx-rtmp-module-1.1.10 –with-debug make make install

enter image description here

默认编译到路径:

/usr/local/nginx

验证编译是否成功:

enter image description here

创建录制文件存储目录:

mkdir -p /usr/local/nginx/files

rtmp 模块配置(nginx.conf):

enter image description here

本文所采用的配置如下

user  root;//使用root用户运行nginx
worker_processes  1;//指明了nginx要开启的进程数,一般等于cpu的总核数,如果没有出现io性能问题,最好不要修改

error_log  logs/error.log  debug;//错误日志存放路径;日志级别为debug,调试用。
events {
    worker_connections  1024;//每个工作进程的最大连接数量;
}


http {
    include       mime.types;//设定mime类型,类型由mime.type文件定义
    default_type  application/octet-stream;
    client_max_body_size 3m;//设定通过nginx上传文件的大小
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';//日志格式设置
    access_log  logs/access.log  main;//访问日志存储路径
    sendfile        on;//指定 nginx 是否调用sendfile 函数(zero copy 方式)来输出文件
    keepalive_timeout  65;//keepalive超时时间
    server {//配置虚拟机
        listen       8080;//监听端口
        server_name  rtmp.datahq.cn 59.110.237.245;//名称
    location /stat{//配置统计页面路径
            rtmp_stat all;
            rtmp_stat_stylesheet stat.xsl;
        }
    location /stat.xsl{//统计模板路径
            root /usr/local/nginx/conf;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
rtmp{
    server{
        listen 0.0.0.0:1935;//监听端口
        ping 30s;//活动连接检查周期
        application live{//应用名
        live on;//打开直播模式
        meta copy; //是否发送直播端元数据信息
        session_relay on;//打开会话转发模式
        drop_idle_publisher 10s;//10s没有推流,自动断开直播端
        sync 10ms;//同步流时间阀值
        record_append on;//打开直播流录制追加模式
        record_path /usr/local/nginx/files;//录制文件地址
        record all;//打开录制功能
        }
    }
}

拷贝统计模板到ngin配置目录:

cp /root/rtmp/src/nginx-rtmp-module-1.1.10/stat.xsl /usr/local/nginx/conf/

启动直播服务器:

cd /usr/local/nginx/sbin && ./nginx

打开统计页面控制台:

http://rtmp.datahq.cn:8080/stat

enter image description here

统计表各属性说明为: clients:连接数 live streams:流名 codec:编码 bits:分辨率 size:视频画面大小 fps:每秒传输帧数 freq:音频率 chan:音频声道 State:流状态 Time:流活动时间 其它4个为输入与输出流的每秒传输速率。

  • 测试直播

    打开OBS,推流至rtmp://rtmp.datahq.cn/live,流名为demo

    打开red5,从rtmp://rtmp.datahq.cn/live拉取直播流,流名为demo

    enter image description here打开直播统计后台:如果看到已经有一个推流,一个拉流,并有数据传输时,说明整个直播链路已经畅通。

    enter image description here

    查看直播录制文件:

    enter image description here

    此时看下系统的负载:可以发现CPU,内存的负载都很低,可以忽略不计。

    enter image description here

  • 上线

    到本节为止,一个简单的直播平台就搭建好了,接下来我们要做的就是上线,因为没有上线,一切都是零。下面是我们在上线过程中遇到的一些坑,供各位同学参考:

  • 系统打开文件数默认太低,造成部分用户连接不上。
  • 遇到活动高峰,网络带宽成为瓶颈,造成收听用户卡顿很多。在生产环境中需要时刻关注是否升级带宽。
  • 上线尽量选择在没有直播的时候进行。

总结

今天给大家分享了直播平台搭建的一些知识,涉及到了rtmp协议,直播选型的一些注意点,大规模直播架构,nginx-rtmp直播服务器实战搭建等。如果各位有直播的需要,可以顺着本文的一些知识点进行实战。

第一次写Chat,不知道效果如何,如果大家有兴趣,我们在后续推出nginx模块开发与nginx-rtmp的分享,感谢大家的参与。


本文首发于GitChat,未经授权不得转载,转载需与GitChat联系。