代码访问 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联系。

 

asp.net core 一 Centos 环境部署

      .netcore的运行环境,创建asp.net core 项目    CentOS 7 ,dotnet-sdk-2.0.0-2.0.0-1.x86_64
  • 直接在liunx创建项目并运行

这种情况相对来说比较简单,真正的开发中,很少使用这种情况

  1. 创建目录文件  mkdir Web2
  2. 创建项目        dotnet new Web                                                                                                                                                     
  3. 运行项目  dotnet run                                                                                                                                                       
  4. 关闭防火墙     systemctl stop firewalld.service                                                                                                                                                                                                                                  
  5. 修改绑定ip以及端口                                                                                                                                                                               

 

  • 其他平台编写完成,发布至liunx

           实际开发中这种情况比较常见

参考连接 :https://www.cnblogs.com/zuqing/p/8231957.html

 

  1. 在VS创建网站项目,得到项目                                                                                                                                                                  
  2. 编辑监听地址                                                                                                                                                                    
  3. 发布文件,选择文件夹发布得到发布文件,拷贝到liunx服务器                                                                                                                                    
  4. cd 至目录,dotnet WebApplication2.dll                                                                                                                                 
  5. 需要更新 dotnet sdk                                                                                                                                                                             
  6. 再次运行 dotnet WebApplication2.dll                                                                                                                                                                   

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

分类: .netcore