如何打造类似数据虫巢官网系列教程之二:爬虫是怎么炼成的

文·blogchong

本文接上一篇《如何打造类似数据虫巢官网系列教程之一:介绍已经准备工作》,不清楚前面剧情的童鞋可以先看看。

这篇文章重点在于解决“数据虫巢官网”的底层数据问题,即那些分析数据的原始数据的来源。

结论很明显,当然是爬过来的,所以这篇我们将重点讲讲如何进行数据爬取,并且以虫巢官网的底层数据爬取代码为例子进行讲解。

当然,其中会一些常规的防爬机制破解以及应对的话题,整体来说这篇会偏重互联网公开数据集的爬取,即爬虫。

此外,整个数据虫巢官网的站点源代码,目前已经整理到github上咯,先放上github的链接:github.com/blogchong/mite8-com。

这是一个完整的项目,这部分代码包括以下部分:

1 整个数据虫巢数据处理后端框架。
2 前端每个页面JSP代码部分,以及涉及数据可视化渲染部分。
3 几个重点数据源的爬虫逻辑,以及定期更新爬虫数据的入口逻辑。
4 数据处理中涉及到的NLP部分,有几个侧重点,包括重构加工的分词工具,以及简单的情感分析,并且提供了分词的一个工具接口。
PS:如果感兴趣,clone之前别忘了给个star,哈哈。

爬虫框架 – Webcollector

回到主题,说到爬虫,由于我之前对于Python并不是很熟悉,而Java则是我的拿手好戏,并且目前市面上封装的爬虫工具很多,所以,我的考虑就是Java语言封装的Webcollector。

简单说一下这个框架,大伙儿感兴趣的可以去开源中国搜一下他的主页,对于Java不熟悉的朋友,其实也无所谓的,使用其他的Python框架一样是可以的,那这部分关于框架这块的就可以略过啦。

Webcollector支持各种自定义的遍历策略,这种在于路径规则不明确的时候使用是很有用的,比如我当时在爬取各大主流招聘网站的JD数据时,就通过这种模式做的,但如果是目的明确的,其实就是按照自己的业务逻辑去固定路径一次性获取数据了。

Webcollector对于Cookie、请求头之类的信息,提供了设置接口,可以很方便的伪装成浏览器,以及登录状态去爬取数据。

Webcollector集成了传统的JDBC持久化策略,可以很方便的将爬取的数据进行MySQL落地,以及MongDB落地等。

使用上也很方面,集成在Maven中,并且更新还是蛮及时的,所以需要集成到自己的Java代码中,只需要引入Jar包即可开整。

除此之外,Webcollector内部封装了selenium,对于动态加载的JS数据来说,也可以很轻松的拿到相关的数据。

其实上面基本都是它的一些特性,对于新手来说都太模糊,这个框架最好的地方在于作者提供了大量的博客实例,来解释各种特性,以及各种简单的爬虫实例可供参考,简直就是初学者的福音。

具体不再多说,想了解更多的,可以搜索然后进入进行学习。

爬取数据

不同的网站对于数据的展现以及输出方式可能都有点不同,静态的网页数据是最好获取的,比如我之前爬取一些偏传统的招聘网站的数据,直接通过入口就可以拿到数据,基本不设防。

代码例子:

CrawlDatum crawlDatum = new CrawlDatum(listUrl).putMetaData(“method”, “POST”);
HttpRequest request = new HttpRequest(crawlDatum.getUrl());
request.setMethod(crawlDatum.getMetaData(“method”));
HttpResponse httpResponse = request.getResponse();
Page page = new Page(crawlDatum, httpResponse);

我们拿到了HttpResponse对象,并且封装成Page对象,通过Page对象提供的Html解析方法,进行数据拆解。

其实Page底层的实现依然是Jsoup,一种很常规的Html结构解析包,我们来看一下具体的使用:

page.select(“div[class=review-content clearfix]”).text()

这是一个很常见的解析过程语法,在page对象中查找class名为“review-content clearfix ”的div,并且调用text方法,将内容转换为String。

静态页面,基本上会上门两招就够了,访问页面数据,然后解析数据,将非结构化的数据转换为结构化数据,当然具体怎么入库,在Java里方式就很多了。

除了静态页面之外,还有其他形式的数据获取。

比如现在很流行的一种做法,那就是前后端进行分离,即后端数据由额外的请求进行获取,再通过前端进行异步渲染。

其实这种做法也是有理由的,因为后端数据的请求跟前端其他部分渲染效率是不同,所以一般做成异步请求,这样在整个页面在后端效率不高时不会造成整个页面等待,提升用户效率。

这个时候,你单纯的看页面源码已经不行啦,你需要会使用浏览器的元素审查,把这些异步请求的链接给逮出来。

我在做雾霾影响分析报告时,基础原始数据是京东的口罩购买数据,并且是评论数据,其评论就是异步加载获取的。

通过F12做元素审查,找到评论数据的真正调用接口,一般异步操作都是放到JS里,并且接口在命名上有一定的提示,如上图就是京东商品的评论数据接口。

大概链接长这样子:

https://sclub.jd.com/comment/productPageComments.action?productId=2582352&score=3&sortType=3&page=0&pageSize=10&isShadowSku=0&callback=fetchJSON_comment98vv47364

里头有控制翻页的参数,我们控制部分参数就可以愉快的获取到数据啦,我们再把callback参数去掉,就是实打实的JSON数据了,连清洗数据的活都省了。

除此之外,还有一个需要注意的点就是,控制访问频度,不管你是单机爬着玩也好,或者是工作大范围爬用代理池也好,频度是一个很基础的防爬机制。

具体的虫巢涉及的代码呢,我就不一一列出来了,这里列一下开源出来的代码,涉及到爬虫的部分,做个备注,有兴趣的可以去我github上clone下来,然后按下面的路径去分析分析逻辑,克隆完了记得给个star哟。

mite8-com开源项目涉及到爬虫的部分:

1 京东雾霾相关的爬虫逻辑:package com.mite8.Insight.jd_wumai;
2 电影《长城》相关的爬虫逻辑:package com.mite8.Insight.movie_great_wall;
3 政务舆情相关的爬虫逻辑:package com.mite8.jx.gz.dn.service; //service下对应的几个子目录,下面的utils,入口是OptXXX类。

防爬的一些机制,以及对应的破解之道

在这里再说一些玩爬虫时,会遇到的一些常见的防爬手段,以及对应的破解之道。

由于俺不是专业的爬虫,所以这部分这么完善的东西显然不是出自我之手,是我团队里爬虫大神在内部技术分享时总结的,俺只是个搬运工。

第一种,伪装成合法的浏览器

在一般情况下,我们会对请求头进行伪装,最重点的key就是user-agent,这部分信息就是浏览器的内核信息。

由于很多公司,甚至是大楼都是用同一个对外IP,所以单纯的使用频度进行防爬封锁,这种情况很容易造成误杀,这也是目标网站不愿意看到的情况。

但是这种情况下,一般不同的电脑其浏览器是不同的,包括内核版本等等,防爬时会分析这个user-agent是不是一样的,或者说非法的字符。

因为很多爬虫框架,或者进程方位URL时会有默认的标志,通过分析这个频度可以明显知道是不是机器在访问页面。

所以,我们通常会获取一批正常的user-agent做随机封装,去获取数据,这种措施会导致上面说的那种防爬机制时效。

第二种,IP频度封锁

在一个IP过于频繁的访问页面时,网站根据一定的判定策略,会判断这个IP是非法的机器,进行IP封锁,导致这个IP无法访问目标页面。

这个时候,我们可以控制访问频度避免被封,但很多时候我们爬取的量很大,控制频度很难完成任务,那么我们就需要使用代理池来做了。

通过代理池的IP,进行IP伪装,这样就破解了频度的控制。

通常代理池分免费与收费,一般免费的代理池都是被人用烂了的,里头的IP都是在各大主流网站的黑名单里。

最后,至于说每个网站的频度是什么样子的,以及控制力度(禁封几分钟,或者是一天等等),就需要自己多测试尝试了。

第三种,用户验证机制

用户验证,这是个很常见的东西,很多页面只有用户登录之后可以访问。

一般通常的做法都是cookie验证,所以,关键是我们如何获取这个cookie。

一次性爬取比较容易,直接把cookie帖进去,做访问即可,但是遇到自动化的时候,我们就需要研究用户登录的过程了,使用POST做表单提交,获取cookie,后面的流程就通啦。

第四种,验证码

很多操作是需要验证码才能下一步操作的,这个时候除了破解验证码无法可破。

不错对于简单的验证码,或者说自己技术犀利的话,写个图像识别的东东,做图像识别,识别验证码也行,但是,目前验证码设计的都很变态,详情参考12306,所以这个方法打折的厉害。

还有一种手段,购买付费的打码平台服务,直接完破之,就是费钱而已。

第五种,动态页面

所谓动态页面,即很多时候数据是通过js动态加载出来的,或者JS加密的,这个时候,直接访问是拿不到数据。

也有破解之道,使用JS引擎做JS解析,目前不管是Python的还是Java的,有不少这种引擎可以供调用。

最后一种方法,使用浏览器内核去访问这个链接,就跟真正的浏览器访问页面没有什么差别啦,Java中经典的selenium就是其中一种。

据闻,技术高端点的公司还有更变态的,通过机器学习来学习真实用户的访问轨迹,通过算法来判断这种访问轨迹是否是机器造成的,然后再做判断是否做禁封。

好吧,玩高深的爬虫,其实就一部防爬与反爬的斗争史,其乐无穷。

下一篇,接着话题,我们讲讲述云平台搭建,服务器部署,环境配置相关的东西:《如何打造类似数据虫巢官网系列教程之三:服务器》。

最后,再贴一遍,数据虫巢官网(www.mite8.com)的开源代码地址(可以随意fork、star 哈哈):github.com/blogchong/mite8-com。


相关阅读:

《如何打造类似数据虫巢官网系列教程之一:介绍已经准备工作》


广告Time:

要不要一起探讨大数据的相关的话题,是不是想要跨界大数据,进一步了解、讨论mite-com的开源代码,欢迎加入“数据虫巢读者私密群”,=>>戳此进入

查看原文

大型网站技术架构-入门梳理

罗列了大型网站架构涉及到的概念,附上了简单说明

前言

  • 本文是对《大型网站架构设计》(李智慧 著)一书的梳理,类似文字版的“思维导图”
  • 全文主要围绕“性能,可用性,伸缩性,扩展性,安全”这五个要素
  • 性能,可用性,伸缩性这几个要素基本都涉及到应用服务器,缓存服务器,存储服务器这几个方面

概述

  • 三个纬度:演化、模式、要素
  • 五个要素: 性能,可用性,伸缩性,扩展性,安全

演化历程

图例可参考 大型网站架构演化历程

  1. 初始阶段的网站架构:一台服务器,上面同时拥有应用程序,数据库,文件,等所有资源。例如 LAMP 架构
  2. 应用和数据服务分离:三台服务器(硬件资源各不相同),分别是应用服务器,文件服务器和数据库服务器
  3. 使用缓存改善网站性能:分为两种,缓存在应用服务器上的本地缓存和缓存在专门的分布式缓存服务器的远程缓存
  4. 使用应用服务器集群改善网站并发处理能力:通过负载均衡调度服务器来将访问请求分发到应用服务器集群中的任何一台机器
  5. 数据库读写分离:数据库采用主从热备,应用服务器在写数据时访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库。应用服务器使用专门的数据访问模块从而对应用透明
  6. 使用反向代理和 CDN 加速网站响应:这两者基本原理都是缓存。反向代理部署在网站的中心机房,CDN 部署在网络提供商的机房
  7. 使用分布式文件系统和分布式数据库系统:数据库拆分的最后手段,更常用的是业务分库
  8. 使用 NoSQL 和搜索引擎:对可伸缩的分布式有更好的支持
  9. 业务拆分:将整个网站业务拆分成不同的应用,每个应用独立部署维护,应用之间通过超链接建立联系/消息队列进行数据分发/访问同一数据存储系统
  10. 分布式服务:公共业务提取出来独立部署

架构演化-分布式服务

演化的价值观

  • 大型网站架构的核心价值是随网站所需灵活应对
  • 驱动大型网站技术发展的主要力量是网站的业务发展

误区

  • 一味追随大公司的解决方案
  • 为了技术而技术
  • 企图用技术解决所有问题

架构模式

模式的关键在于模式的可重复性

  • 分层:横向切分
  • 分割:纵向切分
  • 分布式:分层和分割的主要目的是为了切分后的模块便于分布式部署。常用方案:
    • 分布式应用和服务
    • 分布式静态资源
    • 分布式数据和存储
    • 分布式计算
    • 分布式配置,分布式锁,分布式文件,等等
  • 集群:多台服务器部署相同的应用构成一个集群,通过负载均衡设备共同对外提供服务
  • 缓存:将数据放距离计算最近的位置加快处理速度,改善性能第一手段,可以加快访问速度,减小后端负载压力。使用缓存 两个前提条件 :1.数据访问热点不均衡;2.数据某时段内有效,不会很快过期
    • CDN
    • 反向代理
    • 本地缓存
    • 分布式缓存
  • 异步:旨在系统解耦。异步架构是典型的消费者生产者模式,特性如下:
    • 提高系统可用性
    • 加快网站访问速度
    • 消除并发访问高峰
  • 冗余:实现高可用。数据库的冷备份和热备份
  • 自动化:包括发布过程自动化,自动化代码管理,自动化测试,自动化安全检测,自动化部署,自动化监控,自动化报警,自动化失效转移,自动化失效恢复,自动化降级,自动化分配资源
  • 安全:密码,手机校验码,加密,验证码,过滤,风险控制

核心要素

架构是“最高层次的规划,难以改变的规定”。主要关注五个要素:

  • 性能
  • 可用性(Availability)
  • 伸缩性(Scalability)
  • 扩展性(Extensibility)
  • 安全性

架构

下面依次对这五个要素进行归纳

高性能

性能的测试指标主要有:

  • 响应时间:指应用执行一个操作需要的时间
  • 并发数:指系统能够同时处理请求的数目
  • 吞吐量:指单位时间内系统处理的请求数量
  • 性能计数器:描述服务器或者操作系统性能的一些数据指标

性能测试方法:

  • 性能测试
  • 负载测试
  • 压力测试
  • 稳定性测试

性能测试曲线

性能优化,根据网站分层架构,可以分为三大类:

  • Web 前端性能优化
    • 浏览器访问优化
      • 减少 http 请求
      • 使用浏览器缓存
      • 启用压缩
      • CSS 放在页面最上面,JavaScript 放在页面最下面
      • 减少 Cookie 传输
    • CDN 加速:本质是一个缓存,一般缓存静态资源
    • 反向代理
      • 保护网站安全
      • 通过配置缓存功能加速 Web 请求
      • 实现负载均衡
  • 应用服务器性能优化:主要手段有 缓存、集群、异步
    • 分布式缓存(网站性能优化第一定律:优化考虑使用缓存优化性能)
    • 异步操作(消息队列,削峰作用)
    • 使用集群
    • 代码优化
      • 多线程(设计为无状态,使用局部对象,并发访问资源使用锁)
      • 资源复用(单例,对象池)
      • 数据结构
      • 垃圾回收
  • 存储服务器性能优化
    • 机械硬盘 vs. 固态硬盘
    • B+ 树 vs. LSM 树
    • RAID vs. HDFS

高可用

  • 高可用的网站架构:目的是保证服务器硬件故障时服务依然可用、数据依然保存并能够被访问,主要手段数据和服务的冗余备份及失效转移
  • 高可用的应用:显著特点是应用的无状态性
    • 通过负载均衡进行无状态服务的失效转移
    • 应用服务器集群的 Session 管理
      • Session 复制
      • Session 绑定
      • 利用 Cookie 记录 Session
      • Session 服务器
  • 高可用的服务:无状态的服务,可使用类似负载均衡的失效转移策略,此外还有如下策略
    • 分级管理
    • 超时设置
    • 异步调用
    • 服务降级
    • 幂等性设计
  • 高可用的数据:主要手段是数据备份和失效转移机制
    • CAP 原理
      • 数据一致性(Consisitency)
      • 数据可用性(Availibility)
      • 分区耐受性(Partition Tolerance)
    • 数据备份
      • 冷备:缺点是不能保证数据最终一致和数据可用性
      • 热备:分为异步热备和同步热备
    • 失效转移:由以下三部分组成
      • 失效确认
      • 访问转移
      • 数据恢复
  • 高可用网站的软件质量保证
    • 网站发布
    • 自动化测试
    • 预发布验证
    • 代码控制
      • 主干开发、分支发布
      • 分支开发、主干发布
    • 自动化发布
    • 灰度发布
  • 网站运行监控
    • 监控数据采集
      • 用户行为日志采集(服务器端和客户端)
      • 服务器性能监控
      • 运行数据报告
    • 监控管理
      • 警报系统
      • 失效转移
      • 自动优雅降级

伸缩性

大型网站的“大型”是指:

  • 用户层面:大量用户及大量访问
  • 功能方面:功能庞杂,产品众多
  • 技术层面:网站需要部署大量的服务器

伸缩性的分为如下几个方面

  • 网站架构的伸缩性设计
    • 不同功能进行物理分离实现伸缩
      • 纵向分离(分层后分离)
      • 横向分离(业务分割后分离)
    • 单一功能通过集群规模实现伸缩
  • 应用服务器集群的伸缩性设计
    • HTTP 重定向负载均衡
    • DNS 域名解析负载均衡
    • 反向代理负载均衡(在 HTTP 协议层面,应用层负载均衡)
    • IP 负载均衡(在内核进程完成数据分发)
    • 数据链路层负载均衡(数据链路层修改 mac 地址,三角传输模式,LVS)
    • 负载均衡算法
      • 轮询(Round Robin, RR)
      • 加权轮询(Weighted Round Robin, WRR)
      • 随机(Random)
      • 最少链接(Least Connections)
      • 源地址散列(Source Hashing)
  • 分布式缓存集群的伸缩性设计
    • Memcached 分布式缓存集群的访问模型
      • Memcached 客户端(包括 API,路由算法,服务器列表,通信模块)
      • Memcached 服务器集群
    • Memcached 分布式缓存集群的伸缩性挑战
    • 分布式缓存的一致性 Hash 算法(一致性 Hash 环,虚拟层)
  • 数据存储服务集群的伸缩性设计
    • 关系数据库集群的伸缩性设计
    • NoSQL 数据库的伸缩性设计

可扩展

系统架构设计层面的“开闭原则”

  • 构建可扩展的网站架构
  • 利用分布式消息队列降低耦合性
    • 事件驱动架构(Event Driven Architecture)
    • 分布式消息队列
  • 利用分布式服务打造可复用的业务平台
    • Web Service 与企业级分布式服务
    • 大型网站分布式服务的特点
    • 分布式服务框架设计(Thrift, Dubbo)
  • 可扩展的数据结构(如 ColumnFamily 设计)
  • 利用开放平台建设网站生态圈

网站的安全架构

XSS 攻击和 SQL 注入攻击是构成网站应用攻击最主要的两种手段,此外还包括 CSRF,Session 劫持等手段。

  • 攻击与防御
    • XSS 攻击:跨站点脚本攻击(Cross Site Script)
      • 反射型
      • 持久型
    • XSS 防御手段
      • 消毒(即对某些 html 危险字符转义)
      • HttpOnly
    • 注入攻击
      • SQL 注入攻击
      • OS 注入攻击
    • 注入防御
      • 避免被猜到数据库表结构信息
      • 消毒
      • 参数绑定
    • CSRF 攻击:跨站点请求伪造(Cross Site Request Forgery)
    • CSRF 防御:主要手段是识别请求者身份
      • 表单 Token
      • 验证码
      • Referer Check
    • 其他攻击和漏洞
      • Error Code
      • HTML 注释
      • 文件上传
      • 路径遍历
    • Web 应用防火墙(ModSecurity)
    • 网站安全漏洞扫描
  • 信息加密技术及密钥安全管理
    • 单向散列加密:不同输入长度的信息通过散列计算得到固定长度的输出
      • 不可逆,非明文
      • 可加盐(salt)增加安全性
      • 输入的微小变化会导致输出完全不同
    • 对称加密:加密和解密使用同一个密钥
    • 非对称加密
      • 信息传输:公钥加密,私钥解密
      • 数字签名:私钥加密,公钥解密
    • 密钥安全管理:信息安全传输是靠密钥保证的,改善手段有:
      • 把密钥和算法放在一个独立的服务器上
      • 将加解密算法放在应用系统中,密钥放在独立服务器
  • 信息过滤与反垃圾
    • 文本匹配
    • 分类算法
    • 黑名单

作者@brianway更多文章:个人网站 | CSDN | oschina

亿级用户PC主站的PHP7升级实践

伴随业务的增长,系统压力也在不断增加,再加上机房机架趋于饱和,无法更加有效应对各种突发事件。在这样的情况下,PC主站升级为PHP 7,有哪些技术细节可以分享?

背景

新浪微博在2016年Q2季度公布月活跃用户(MAU)较上年同期增长33%,至2.82亿;日活跃用户(DAU)较上年同期增长36%,至1.26亿,总注册用户达8亿多。PC主站作为重要的流量入口,承载部分用户访问和流量落地,其中我们提供的部分服务(如:头条文章)承担全网所有流量。

随着业务的增长,系统压力也在不断的增加。峰值时,服务器Hits达10W+,CPU使用率也达到了80%,远超报警阈值。另外,当前机房的机架已趋于饱和,遇到突发事件,只能对非核心业务进行降低,挪用这些业务的服务器来进行临时扩容,这种方案只能算是一种临时方案,不能满足长久的业务增长需求。再加上一年一度的三节(圣诞、元旦、春节),系统需预留一定的冗余来应对,所以当前系统面临的问题非常严峻,解决系统压力的问题也迫在眉急。

面对当前的问题,我们内部也给出两套解决方案同步进行。

  • 方案一:申请新机房,资源统一配置,实现弹性扩容。
  • 方案二:对系统进行优化,对性能做进一步提升。

针对方案一,通过搭建与新机房之间的专线与之打通,高峰时,运用内部自研的混合云DCP平台,对所有资源进行调度管理,实现了真正意义上的弹性扩容。目前该方案已经在部分业务灰度运行,随时能对重点业务进行小流量测试。

针对方案二,系统层面,之前做过多次大范围的优化,比如:

  • 将Apache升级至Nginx
  • 应用框架升级至Yaf
  • CPU计算密集型的逻辑扩展化
  • 弃用smarty
  • 并行化调用

优化效果非常明显,如果再从系统层面进行优化,性能可提升的空间非常有限。好在业界传出了两大福音,分别为HHVM和PHP7。

方案选型

在PHP7还未正式发布时,我们也研究过HHVM(HipHop Virtual Machine),关于HHVM更多细节,这里就不再赘述,可参考官方说明。下面对它提升性能的方式进行一个简单的介绍。

默认情况下,Zend引擎先将PHP源码编译为opcode,然后Zend解析引擎逐条执行。这里的opcode码,可以理解成C语言级的函数。而HHVM提升性能方式为替代Zend引擎将PHP代码转换成中间字节码(HHVM自己的中间字节码,通常称为中间语言),然后在运行时通过即时(JIT)编译器将这些字节码转换成x64的机器码,类似于Java的JVM。

HHVM为了达到最佳优化效果,需要将PHP的变量类型固定下来,而不是让编译器去猜测。Facebook的工程师们就定义一种Hack写法,进而来达到编译器优化的目的,写法类似如下:

<?hh 
class point {     
    public float $x, $y;     
    function __construct(float $x, float $y) {         
        $this->x = $x;         
        $this->y = $y;
     } 
     }

通过前期的调研,如果使用HHVM解析器来优化现有业务代码,为了达到最佳的性能提升,必须对代码进行大量修改。另外,服务部署也比较复杂,有一定的维护成本,综合评估后,该方案我们也就不再考虑。

当然,PHP7的开发进展我们也一直在关注,通过官方测试数据以及内部自己测试,性能提升非常明显。

令人兴奋的是,在去年年底(2015年12月04日),官方终于正式发布了PHP7,并且对原生的代码几乎可以做到完全兼容,性能方面与PHP5比较能提升达一倍左右,和HHVM相比已经是不相上下。

无论从优化成本、风险控制,还是从性能提升上来看,选择PHP7无疑是我们的最佳方案。

系统现状以及升级风险

微博PC主站从2009年8月13日发布第一版开始,先后经历了6个大的版本,系统架构也随着需求的变化进行过多次重大调整。截止目前,系统部分架构如下。

从系统结构层面来看,系统分应用业务层、应用服务层,系统所依赖基础数据由平台服务层提供。

从服务部署层面来看,业务主要部署在三大服务集群,分别为Home池、Page池以及应用服务池。

为了提升系统性能,我们自研了一些PHP扩展,由于PHP5和PHP7底层差别太大,大部分Zend API接口都进行了调整,所有扩展都需要修改。

所以,将PHP5环境升级至PHP7过程中,主要面临如下风险:

  • 使用了自研的PHP扩展,目前这些扩展只有PHP5版本,将这些扩展升级至PHP7,风险较大。
  • PHP5与PHP7语法在某种程度上,多少还是存在一些兼容性的问题。由于涉及主站代码量庞大,业务逻辑分支复杂,很多测试范围仅仅通过人工测试是很难触达的,也将面临很多未知的风险。
  • 软件新版本的发布,都会面临着一些未知的风险和版本缺陷。这些问题,是否能快速得到解决。
  • 涉及服务池和项目较多,基础组件的升级对业务范围影响较大,升级期间出现的问题、定位会比较复杂。

对微博这种数亿用户级别的系统的基础组件进行升级,影响范围将非常之大,一旦某个环节考虑不周全,很有可能会出现比较严重的责任事故。

PHP7升级实践

1. 扩展升级

一些常用的扩展,在发布PHP7时,社区已经做了相应升级,如:Memcached、PHPRedis等。另外,微博使用的Yaf、Yar系列扩展,由于鸟哥(laruence)的支持,很早就全面支持了PHP7。对于这部分扩展,需要详细的测试以及现网灰度来进行保障。

PHP7中,很多常用的API接口都做了改变,例如HashTable API等。对于自研的PHP扩展,需要做升级,比如我们有个核心扩展,升级涉及到代码量达1500行左右。

新升级的扩展,刚开始也面临着各式各样的问题,我们主要通过官方给出的建议以及测试流程来保证其稳定可靠。

官方建议

  • 在PHP7下编译你的扩展,编译错误与警告会告诉你绝大部分需要修改的地方。
  • 在DEBUG模式下编译与调试你的扩展,在run-time你可以通过断言捕捉一些错误。你还可以看到内存泄露的情况。

测试流程

  • 首先通过扩展所提供的单元测试来保证扩展功能的正确性。

  • 其次通过大量的压力测试来验证其稳定性。
  • 然后再通过业务代码的自动化测试来保证业务功能的可用性。

  • 最后再通过现网流量灰度来确保最终的稳定可靠。

整体升级过程中,涉及到的修改比较多,以下只简单列举出一些参数变更的函数。

(1)addassocstringl参数4个改为了3个。

//PHP5
 add_assoc_stringl(parray, key, value, value_len);
//PHP7
 add_assoc_stringl(parray, key, value);

(2)addnextindex_stringl 参数从3个改为了2个。

//PHP5
 add_assoc_stringl(parray, key, value, value_len);
//PHP7
 add_assoc_stringl(parray, key, value);

(3)RETURN_STRINGL 参数从3个改为了2个。

//PHP5 
RETURN_STRINGL(value, length,dup); 
//PHP7 
RETURN_STRINGL(value, length);

(4)变量声明从堆上分配,改为栈上分配。

//PHP5 
zval* sarray_l; 
ALLOC_INIT_ZVAL(sarray_l); 
array_init(sarray_l);  

//PHP7 
zval sarray_l; 
array_init(&sarray_l);

(5)zendhashgetcurrentkey_ex参数从6个改为4个。

//PHP5 
ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex (
     HashTable* ht,
    char** str_index,     
    uint* str_length,     
    ulong* num_index,     
    zend_bool duplicate,     
    HashPosition* pos);  
    
//PHP7 
ZEND_API int ZEND_FASTCALL zend_hash_get_current_key_ex(     
      const HashTable *ht,     
     zend_string **str_index,     
     zend_ulong *num_index,     
     HashPosition *pos);

更详细的说明,可参考官方PHP7扩展迁移文档:https://wiki.PHP.net/PHPng-upgrading。

2. PHP代码升级

整体来讲,PHP7向前的兼容性正如官方所描述那样,能做到99%向前兼容,不需要做太多修改,但在整体迁移过程中,还是需要做一些兼容处理。

另外,在灰度期间,代码将同时运行于PHP5.4和PHP7环境,现网灰度前,我们首先对所有代码进行了兼容性修改,以便同一套代码能同时兼容两套环境,然后再按计划对相关服务进行现网灰度。

同时,对于PHP7的新特性,升级期间,也强调不允许被使用,否则代码与低版本环境的兼容性会存在问题。

接下来简单介绍下升级PHP7代码过程中,需要注意的地方。

(1)很多致命错误以及可恢复的致命错误,都被转换为异常来处理,这些异常继承自Error类,此类实现了 Throwable 接口。对未定义的函数进行调用,PHP5和PHP7环境下,都会出现致命错误。

undefine_function();

错误提示:

PHP Fatal error:  Call to undefined function 
undefine_function() in /tmp/test.PHP on line 4

在PHP7环境下,这些致命的错误被转换为异常来处理,可以通过异常来进行捕获。

try {      
     undefine_function(); 
     } 
     catch (Throwable $e) {      
     echo $e; 
}

提示:

Error: Call to undefined function undefine_function() in /tmp/test.PHP:5 Stack trace: 
#0 {main}

(2)被0除,PHP 7 之前,被0除会导致一条 E_WARNING 并返回 false 。一个数字运算返回一个布尔值是没有意义的,PHP 7 会返回如下的 float 值之一。

  • +INF
  • -INF
  • NAN

如下:

var_dump(42/0);  // float(INF)  + E_WARNING 
var_dump(-42/0); // float(-INF) + E_WARNING 
var_dump(0/0);   // float(NAN)  + E_WARNING

当使用取模运算符( % )的时候,PHP7会抛出一个 DivisionByZeroError 异常,PHP7之前,则抛出的是警告。

echo 42 % 0;

PHP5输出:

PHP Warning:  Division by zero in /tmp/test.PHP on line 4

PHP7输出:

PHP Fatal error:  Uncaught DivisionByZeroError: Modulo by zero in /tmp/test.PHP:4 Stack trace: #
0 {main} 
thrown in /tmp/test.PHP on line 4

PHP7环境下,可以捕获该异常:

try {     
    echo 42 % 0; 
    } catch (DivisionByZeroError $e) {    
    echo $e->getMessage(); 
}

输出:

Modulo by zero

(3)pregreplace() 函数不再支持 “\e” (PREGREPLACEEVAL). 使用 pregreplace_callback() 替代。

$content = preg_replace("/#([^#]+)#/ies", "strip_tags('#\\1#')", $content);

PHP7:

$content = preg_replace_callback("/#([^#]+)#/is", "self::strip_str_tags", $content); 
public static function strip_str_tags($matches){      
      return "#".strip_tags($matches[1]).'#'; 
}

(4)以静态方式调用非静态方法。

class foo { function bar() { echo ‘I am not static!’; } } foo::bar();

以上代码PHP7会输出:

PHP Deprecated:  Non-static method foo::bar() should not be called statically in /tmp/test.PHP on line 10 
I am not static!

(5)E_STRICT 警告级别变更。

原有的 ESTRICT 警告都被迁移到其他级别。 ESTRICT 常量会被保留,所以调用 errorreporting(EALL|E_STRICT) 不会引发错误。

关于代码兼容PHP7,基本上是对代码的规范要求更严谨。以前写的不规范的地方,解析引擎只是输出NOTICE或者WARNING进行提示,不影响对代码上下文的执行,而到了PHP7,很有可能会直接抛出异常,中断上下文的执行。

如:对0取模运行时,PHP7之前,解析引擎只抛出警告进行提示,但到了PHP7则会抛出一个DivisionByZeroError异常,会中断整个流程的执行。

对于警告级别的变更,在升级灰度期间,一定要关注相关NOTICE或WARNING报错。PHP7之前的一个NOTICE或者WARNING到了PHP7,一些报警级变成致命错误或者抛出异常,一旦没有对相关代码进行优化处理,逻辑被触发,业务系统很容易因为抛出的异常没处理而导致系统挂掉。

以上只列举了PHP7部分新特性,也是我们在迁移代码时重点关注的一些点,更多细节可参考官方文档http://PHP.net/manual/zh/migration70.PHP。

3. 研发流程变更

一个需求的开发到上线,首先我们会通过统一的开发环境来完成功能开发,其次经过内网测试、仿真测试,这两个环境测试通过后基本保证了数据逻辑与功能方面没有问题。然后合并至主干分支,并将代码部署至预发环境,再经过一轮简单回归,确保合并代码没有问题。最后将代码发布至生产环境。

为了确保新编写的代码能在两套环境(未灰度的PHP5.4环境以及灰度中的PHP7环境)中正常运行,代码在上线前,也需要在两套环境中分别进行测试,以达到完全兼容。

所以,在灰度期间,对每个环节的运行环境除了现有的PHP5.4环境外,我们还分别提供了一套PHP7环境,每个阶段的测试中,两套环境都需要进行验证。

4. 灰度方案

之前有过简单的介绍,系统部署在三大服务池,分别为Home池、Page池以及应用服务池。

在准备好安装包后,先是在每个服务池分别部署了一台前端机来灰度。运行一段时间后,期间通过错误日志发现了不少问题,也有用户投诉过来的问题,在问题都基本解决的情况下,逐渐将各服务池的机器池增加至多台。

经过前期的灰度测试,主要的问题得到基本解决。接下是对应用服务池进行灰度,陆续又发现了不少问题。前后大概经历了一个月左右,完成了应用服务池的升级。然后再分别对Home池以及Page池进行灰度,经过漫长灰度,最终完成了PC主站全网PHP7的升级。

虽然很多问题基本上在测试或者灰度期间得到了解决,但依然有些问题是全量上线后一段时间才暴露出来,业务流程太多,很多逻辑需要一定条件才能被触发。为此BUG都要第一时间同步给PHP7升级项目组,对于升级PHP引起的问题,要求必须第一时间解决。

5. 优化方案

(1)启用Zend Opcache,启用Opcache非常简单, 在PHP.ini配置文件中加入:

zend_extension=opcache.so 
opcache.enable=1 
opcache.enable_cli=1"

(2)使用GCC4.8以上的编译器来编译安装包,只有GCC4.8以上编译出的PHP才会开启Global Register for opline and execute_data支持。

(3)开启HugePage支持,首先在系统中开启HugePages, 然后开启Opcache的hugecodepages。

关于HugePage

操作系统默认的内存是以4KB分页的,而虚拟地址和内存地址需要转换, 而这个转换要查表,CPU为了加速这个查表过程会内建TLB(Translation Lookaside Buffer)。 显然,如果虚拟页越小,表里的条目数也就越多,而TLB大小是有限的,条目数越多TLB的Cache Miss也就会越高, 所以如果我们能启用大内存页就能间接降低这个TLB Cache Miss。

PHP7与HugePage

PHP7开启HugePage支持后,会把自身的text段, 以及内存分配中的huge都采用大内存页来保存, 减少TLB miss, 从而提高性能。相关实现可参考Opcache实现中的accel_move_code_to_huge_pages()函数。

开启方法

以CentOS 6.5为例, 通过命令:

sudo sysctl vm.nr_hugepages=128

分配128个预留的大页内存。

$ cat /proc/meminfo | grep Huge 
AnonHugePages:    444416 kB 
HugePages_Total:     128 
HugePages_Free:      128 
HugePages_Rsvd:        0 
HugePages_Surp:        0 
Hugepagesize:       2048 kB

然后在PHP.ini中加入

opcache.huge_code_pages=1

6. 关于负载过高,系统CPU使用占比过高的问题

当我们升级完第一个服务池时,感觉整个升级过程还是比较顺利,当灰度Page池,低峰时一切正常,但到了流量高峰,系统CPU占用非常高,如图:

系统CPU的使用远超用户程序CPU的使用,正常情况下,系统CPU与用户程序CPU占比应该在1/3左右。但我们的实际情况则是,系统CPU是用户CPU的2~3倍,很不正常。

对比了一下两个服务池的流量,发现Page池的流量正常比Home池高不少,在升级Home池时,没发现该问题,主要原因是流量没有达到一定级别,所以未触发该问题。当单机流量超过一定阈值,系统CPU的使用会出现一个直线的上升,此时系统性能会严重下降。

这个问题其实困扰了我们有一段时间,通过各种搜索资料,均未发现任何升级PHP7会引起系统CPU过高的线索。但我们发现了另外一个比较重要的线索,很多软件官方文档里非常明确的提出了可以通过关闭Transparent HugePages(透明大页)来解决系统负载过高的问题。后来我们也尝试对其进行了关闭,经过几天的观察,该问题得到解决,如图:

什么是Transparent HugePages(透明大页)

简单的讲,对于内存占用较大的程序,可以通过开启HugePage来提升系统性能。但这里会有个要求,就是在编写程序时,代码里需要显示的对HugePage进行支持。

而红帽企业版Linux为了减少程序开发的复杂性,并对HugePage进行支持,部署了Transparent HugePages。Transparent HugePages是一个使管理Huge Pages自动化的抽象层,实现方案为操作系统后台有一个叫做khugepaged的进程,它会一直扫描所有进程占用的内存,在可能的情况下会把4kPage交换为Huge Pages。

为什么Transparent HugePages(透明大页)对系统的性能会产生影响

在khugepaged进行扫描进程占用内存,并将4kPage交换为Huge Pages的这个过程中,对于操作的内存的各种分配活动都需要各种内存锁,直接影响程序的内存访问性能。并且,这个过程对于应用是透明的,在应用层面不可控制,对于专门为4k page优化的程序来说,可能会造成随机的性能下降现象。

怎么关闭Transparent HugePages(透明大页)

(1)查看是否启用透明大页。

[root@venus153 ~]# cat  /sys/kernel/mm/transparent_hugepage/enabled 
[always] madvise never

使用命令查看时,如果输出结果为[always]表示透明大页启用了,[never]表示透明大页禁用。

(2)关闭透明大页。

echo never > /sys/kernel/mm/transparent_hugepage/enabled 
echo never > /sys/kernel/mm/transparent_hugepage/defrag

(3)启用透明大页。

echo always >  /sys/kernel/mm/transparent_hugepage/enabled 
echo always > /sys/kernel/mm/transparent_hugepage/defrag

(4)设置开机关闭。

修改/etc/rc.local文件,添加如下行:

if test -f /sys/kernel/mm/redhat_transparent_hugepage/enabled; then     
     echo never > /sys/kernel/mm/transparent_hugepage/enabled     
    echo never > /sys/kernel/mm/transparent_hugepage/defrag 
fi

升级效果

由于主站的业务比较复杂,项目较多,涉及服务池达多个,每个服务池所承担业务与流量也不一样,所以我们在对不同的服务池进行灰度升级,遇到的问题也不尽相同,导致整体升级前后达半年之久。庆幸的是,遇到的问题,最终都被解决掉了。最让人兴奋的是升级效果非常好,基本与官方一致,也为公司节省了不少成本。

以下简单地给大家展示下这次PHP7升级的成果。

(1)PHP5与PHP7环境下,分别对我们的某个核心接口进行压测(压测数据由QA团队提供),相关数据如下:

同样接口,分别在两个不现的环境中进行测试,平均TPS从95提升到220,提升达130%。

(2)升级前后,单机CPU使用率对比如下。

升级前后,1小时流量情况变化:

升级前后,1小时CPU使用率变化:

升级前后,在流量变化不大的情况下,CPU使用率从45%降至25%,CPU使用率降低44.44%。

(3)某服务集群升级前后,同一时间段1小时CPU使用对比如下。

PHP5环境下,集群近1小时CPU使用变化:

PHP7环境下,集群近1小时CPU使用变化:

升级前后,CPU变化对比:

升级前后,同一时段,集群CPU平均使用率从51.6%降低至22.9%,使用率降低56.88%。

以上只简单从三个维度列举了一些数据。为了让升级效果更加客观,我们实际的评估维度更多,如内存使用、接口响应时间占比等。最终综合得出的结论为,通过本次升级,PC主站整体性能提升在48.82%,效果非常好。团队今年的职能KPI就算是提前完成了。

总结

整体升级从准备到最终PC主站全网升级完成,时间跨度达半年之久,无论是扩展编写、准备安装脚本、PHP代码升级还是全网灰度,期间一直会出现各式各样的问题。最终在团队的共同努力下,这些问题都彻底得到了解决。

一直以来,对社区的付出深怀敬畏之心,也是因为他们对PHP语言性能极限的追求,才能让大家的业务坐享数倍性能的提升。同时,也让我们更加相信,PHP一定会是一门越来越好的语言。

作者简介

侯青龙,微博主站研发负责人。2010年加入新浪微博,先后参与过微博主站V2版至V6版的研发,主导过主站V6版以及多机房消息同步系统等重大项目的架构设计工作。致力于提升产品研发效率以及优化系统性能。


感谢韩婷对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。

2017 年你应该学习的编程语言、框架和工具

作者:IT程序狮
链接:https://zhuanlan.zhihu.com/p/24369470
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在过去的一年里,软件开发行业继续大踏步地向前迈进。回顾 2016 年,我们看到了更多新兴的流行语言、框架和工具,它们改变着我们的工作方式,让我们看到更多的可能。但在这个行业,紧随潮流是很难的。所以在每年年底,我们都会给你提供一些建议,它涉及什么是最重要的,以及你在未来一年中应该学习什么。

大趋势

渐进式 Web Apps

在 2016 年里,我们见证了 Progressive Web App 概念的蓬勃兴起。它意味着 Web 应用程序可以离线工作,并能提供原生移动应用的体验。它们可以添加到你的智能设备的主屏幕上,甚至可以给你发送推送通知,从而弥补与原生移动应用程序的差距。我们认为,在 2017 年,渐进式 Web Apps 将变得更加重要,也值得我们去探究。在这里查看相关概述

聊天机器人

从运行聊天机器人的平台到构建其的框架,现在每个人都在谈论它。而社区里也正忙于此活动。(阅读我们的介绍)机器人是一款新兴的移动应用程序,它让我们感到兴奋。如果你快点的话,还可以赶得上这波浪潮。然而一旦新鲜感消失,那么它只会承担一些无聊的角色,例如自动化的客服支持。但是,相信我们可以实现梦想。

前端框架的合并

在 JavaScript 社区,随着令人难以置信的框架和工具的混合,每周都会出现新的东西。直到最近,人们希望旧工具将被新工具所取代,但这不是 2016 年我们所想看到的。相反,我们看到了流行框架交换的想法,以及纳入新诞生框架中的创新元素。所以在 2017 年,你该选择哪个 JS 框架无关紧要,因为它们的功能大多是可以比较的。

云端

就目前的形势看,众多的公司与开发者们都在积极地拥抱“云”。云是可根据不同的需求,并通过控制面板来完全配置的虚拟化计算机基础设施。目前三大云提供商为亚马逊 AWS、Google Cloud 和 微软 Azure. 由于它们的竞争价格一直在下跌,使得小公司和个人开发者也可以将云纳入其预算中,所以熟悉云工作流程将是 2017 年的一笔不错的投资。

机器学习

机器学习(ML)在去年一年中呈现爆炸式的增长。三月份 AlphaGo 与李世石的精彩对决,也让它成为了焦点。从原始数据中学习的智能计算机系统,正在改变我们与移动设备的交互方式。看样子,机器学习将在 2017 年成为更大的影响因素。

编程语言

JavaScript 继续迈着令人难以置信的创新步伐在前进。由于 Web 浏览器的快速发布计划,JS 的标准定为了每年更新。故“ES2017”预计将在 2017 年中期完成,它也将带来 JS 开发者梦寐以求的新特性——用于处理异步函数的аsync/аwait。同时要感谢 Babel ,因为你现在可以在每个浏览器中编写 ES2017 了。

TypeScript 2.1 于 2016 年年底发布,它将为旧浏览器带来Async/Await异步解决方案,并改进了类型推断。TypeScript 是一种编译为纯 JavaScript 的静态类型语言。它增强了经典的 OOP 模型和可选的静态类型,使大代码库更易于维护。同时,它也是编写 Angular 2 应用程序的首选语言,我们建议你可以尝试下。 这是关于它的快速入门指南

C#7.0 预计在 2017 年发布,作为一门优秀的编程语言,它也将得到更大的改进。当微软推出开源的 Visual Studio 代码编辑器和 .Net Core 时,这一举动让众人都感到惊讶万分。它们不仅可以在 Linux、Windows 和 macOS 操作系统中运行,而且你可以在 C# 中编写快速、高效的应用程序(在这里阅读更多)。同时,这两种工具也都形成了充满活力的社区。相信,它们将在 2017 年会给我们带来更多的惊喜。

Python 3.6 版本将于 12 月发布。它正在巩固自身在开发人员、IT 专业人员和科学家在脚本语言选择中的地位。它适用于自动化、Web开发、机器学习和科学计算。虽然 Python 2.X 与 3.X 版本的割裂,对于社区来说是一个长达数年的斗争,但是就目前而言,你可以自信地选择 Python 3 并享受完整的库支持。而对于那些需要额外性能的朋友,建议你们看看 PyPy,一个可启用 Python 运行时 JIT 的替代品。

Ruby 2.3 已在今年早些时候发布了,并带来了一些性能上的改进。同时,Ruby 也是学习通用脚本语言的一个好选择,但是只有当它和 Rails 相配合的时候才能发挥出其最大的功效。伴随 Ruby 3×3 计划的宣布,也促使了即将到来的 Ruby 3 版本比当前版本的运行速度快 3 倍。而你也可以在更多的情景中,打开使用 Ruby 的大门。

PHP 7.1 版本已在 12 月发布,并对该语言进行了小范围的增强。这个版本基于了去年 7.0 版本主要性能的改进,将 PHP 转变为构建 Web 应用程序的快速平台。如果你打算学习,我们推荐你看看 PHP 之道中的最佳实践。

Java 9 预计在 2017 年发布,它将带来一些备受开发者们所欢迎的新功能,例如评估代码的 repl、HTTP 2.0 的支持以及一些新的 API . 对于有才能的 Java 开发人员和广泛使用该语言进行项目研发的人来说,他们对这些新特性是有强烈需求的。如果 Java 不是你的“菜”,这里还有一些基于 JVM 的编程语言,像 KotlinScala,你也可以了解下。

Swift 3 已经在今年早些时候发布了。简化 iOS 和 MacOS 上应用程序的开发,是苹果公司对现代编程语言的愿景。由于 Swift 是开源的,所以也涌现了大量的社区。Swift 4 计划于 2017 年发布,此版本将会改进语言并引入服务器 API,致力使其成为编写 Web 应用程序和后端的不错选择。

如果你在寻找一些让你感到兴奋的东西,你可以尝试下 CrystalElixir。它们都拥有类似与 Ruby 的友好语法以及卓越的性能,或者你也可以看看类似于 HaskellClojure 这类函数式语言。另外两种快速编程语言,我们推荐给你 RustGo 语言。

挑一个或多个学习: JS (ES2017)、TypeScript、C#、Python、Ruby、PHP7、Java/Kotlin/Scala.

前端

近期 Web 平台取得了两个重大的进展:Web Assembly 字节码技术和 Service Workers 技术。它们打开了快速、高效的 Web 应用程序的大门,并且有效的弥补了编译本地应用上的差距。Service Workers 是针对渐进式 Web App 的启动技术,它为 Web 平台提供了通知上的支持,将来也会有更多的 API.

Angular.js 2 在今年也已经发布了。该框架由 Google 进行维护,受到了众多企业和大公司的青睐。它所具备众多的功能,也为从网络到桌面以及移动应用程序中编写任何东西成为了可能。而它的框架也是用 TypeScript 所编写的,这也是写应用程序推荐的编程语言。虽然学习它还需要阅读更多的内容,但我们认为在 2017 年学习 Angular 2 将是一个很不错的投资。

在今年我们也看到了 Vue.js2.0 版本的发布,它借鉴了 Angular,React 和 Ember 中好的想法,并且比前两个框架更轻量、更快速。我们建议你今年要试一试,你可以从我们的 Vue.js 教程开始。

Ember 是 JavaScript 框架的另一个不错的选择。它支持数据双向绑定,并能够自动更新模板、组件以及服务器端渲染。与其他竞争者相比,使用它的好处是它更加成熟与稳定,而其框架的重大更改频率之低,社区重视向后的兼容性,也使得此框架成为开发较长生命周期的应用程序的不二之选。

另外两个值得一提的框架是 AureliaReact。在过去的一年里 React 的生态系统变得越来越复杂,因此很难推荐给初学者。但经验丰富的开发者可以将库与 GraphQLRelayFluxImmutable.js 组合成一个全面完整的全栈解决方案。

没有提及 Bootstrap 的前端终归是不完整的。而 Bootstrap 4 目前也正处于 Alpha 阶段,预计在 2017 年发布。值得关注的变化是新的通用卡片组件和 Flexbox 网格(查看与常规网格的对比),这使得框架更加现代化,并且让用户使用它进行工作时更加得舒心。

SASSLESS 仍然是当前最流行的两种 CSS 预处理器。尽管 Vanilla CSS 已经实现了对变量的支持,但对 mixins、函数和代码组织上的支持,SASS 和 LESS 依然更胜一筹。如果您还没有了解它们,可以看看我们的 SASSLESS 快速入门指南。

挑一个或多个学习:Angular 2、Vue.js、Ember、Bootstrap、LESS/SASS

后端

后端有众多的选择,但所有的选择都取决于你对编程语言或特定性能需求的偏好上。Web 开发中的一个持续趋势是远离后端的业务逻辑,并将该层转换为由前端和移动应用程序使用的 API 上。但一个全栈的框架通常是能够更简单、快速的应用于开发,并且它仍然是 Web 应用程序最有效的选择。

Node.js 是在浏览器之外运行 JS 的主要方式。在今年,我们也看到了它发布了许多新的版本。除了提升了性能外,也添加了对整个 ES6 规范的覆盖。Node 具有构建快速 API、服务器、桌面应用程序甚至机器人的框架,同时它可以创建想象到的各种模块的庞大社区。这里有一些你可能想研究的框架:ExpressKoaNextNodal.

PHP 是一种拥有大量 Web 框架可供你选择的 Web 开发语言。由于其拥有出色的文档和功能,Laravel 已建成了一个活跃的社区。Zend Framework 发布了第 3 版,这标志着面向业务框架的巨大升级。在今年,我们也看到了 Symfony 发行了很多新的版本,使它成为了全栈解决方案中更好的选择。

对于 Ruby 来说,Rails 框架是首选的。Rails 5.0 版本已于今年发布,并为 Web Sockets、API 模型等方面提供了支持。对于小型应用程序而言,Sinatra 也是一个不错的选择,Sinatra 2.0版本预计在 2017 年发布。

Python 有着以 DjangoFlask 为组合的全栈/迷你型框架。Django 1.10 已在今年 8 月发布了,它为 Postgres 引入了全文搜索和一个重大修改的中间件层。

Java 的生态系统中,依旧有很多流行的 Web 框架可供你选择。PlaySpark 便是两个必备的选择,同时它们也可以与 Scala 一起使用。

对于编程爱好者来说,你还可以选择 Phoenix,它是用 Elixir 编写的,它试图成为一个具有卓越的性能,并能完整替代 Rails 功能的框架。如果 Elixir 是你想在 2017 年学习的语言之一,不妨尝试下 Phoenix .

学习其中之一:全栈后端框架、一个微框架

数据库

PostgreSQL 在今年已经发行了两个完整的版本——9.59.6.它们带来了我们从 MySQL 就开始期盼的 UPSERT (aka ON DUPLICATE KEY UPDATE)功能,以及更好的全文搜索和速度改进功能,这多亏了并行查询,更高效的复制、聚合、索引和排序。Postgres 适用于大规模、TB 级规模的数据集以及繁忙的 Web Apps,这些优化都是很受欢迎的。

MySQL 8.0 将是数据库的下一个主要版本。预计在 2017 年发布,它将给系统带来更多的改进。MySQL 仍然是最受欢迎的数据库管理系统,整个行业都受益于这些新的版本。

对于 NoSQL 的粉丝们,我们推荐 CouchDB。它是一个快速、可扩展的 JSON 存储系统,同时公开了一个 REST-ful HTTP API.此数据库易于使用,同时性能卓越。与 CouchDB 对应的是 PouchDB ,它可以完全在浏览器中工作,并且可以与 Couch 同步数据。所以你可以在离线应用程序上使用 PouchDB ,联网后它会自动同步数据。

Redis 是我们最喜欢的键-值存储型数据库。它体积小、快速并且有丰富的特性。作为 NoSQL 数据存储或进程消息和同步通道,你可以使用它作为智能分布式高速缓存系统的可替代方案。它提供了大量的数据结构可供选择,并且在即将到来的 4.0 版本中会有一个模块系统,并将改进复制功能。

学习其中之一:Postgres、MySQL、CouchDB、Redis.

工具

Yarn 是由 Facebook 开发的 Node.js 包管理器。它是对 npm 命令行工具的升级,并提供了更快速地安装,更好的安全性以及确定性的构建。它仍然使用 npm 包注册表作为其后端,因此您甚至可以访问同一个 JavaScript 模块的生态系统。Yarn 与 npm 使用的 package.json 格式是兼容的,区别在于前者能实现快速安装。

作为两个最受开发者欢迎的开源代码编辑器——Visual Studio CodeAtom ,在过去一年中,我们看到了它们进行了很多不可思议的创新。这两个项目都是使用 Web 技术构建的,社区中也吸引了大量的粉丝。编辑器具备高扩展,提供了诸如语法检查、linting 和重构工具的相关插件。

作为最流行的源代码版本控制系统,Git 当之无愧。虽然它无服务器,但你可以将计算机上的任何文件夹转换为存储库。如果你想共享代码,像 GitLabBitbucketGitHub 都是不错的选择。在 2017 年,我们建议你熟悉 git 命令行,因为它会比您想象的更加方便。

桌面应用程序依然没有消失。即使 Web App 变得越来越强大,有时你依然会需要强大的功能和 API,这是 Web 平台无法提供的。你可以使用诸如 ElectronNW.js 之类的工具,利用 Web 技术来创建桌面应用程序,同时你也可以完全访问操作系统和 npm 可用的广度模块。要了解这些工具的更多信息,请阅读有关 ElectronNW.js 的教程。

软件开发团队的最新趋势是让开发人员负责自己软件项目中的部署,也称为 DevOps.这能产生更快地发布和更迅速地修复生产中出现的问题。而具有运维经验的开发人员将得到公司的高度重视,因此从现在开始熟悉能够实现这一目标的技术,将对你来说是一个巨大的提升。我们推荐的工具是 AnsibleDocker 。同时,具备 Linux 命令行和基本系统管理技能,也将为你的职场生涯大大的加分。

尝试一个或多个学习:Yarn、Git、Visual Studio Code、Electron、Ansible、Docker.

技术

随着大型公司数据中心的关闭,并调整其整体的基础设施到云上,我们可以看到已经赢得了整个软件行业。目前三个主要的平台是 AWS, Google CloudAzure。这三大平台都有着强大的功能,同时不断地扩展其功能集,涉及虚拟机、数据库托管、机器学习服务等。由于价格的迅速下降,小公司和个人开发者也都可以接触到云。对于 2017 年,在云上部署一个业余项目将是一个很好的学习积累。

人工智能是 2016 年的流行词。语音识别和图像分类只是该技术在面向用户应用程序的两个部分,人工智能设备的性能达到甚至超越了人类的水平。当下众多的创业公司也将 AI 和机器学习应用到其新的领域,同时许多相关的开源项目也已经发布,例如谷歌的 Tensor Flow 和微软的 Cognitive Toolkit。机器学习是一个与数学非常相关的主题,对于刚刚开始的人,这里有全面的在线课程供你学习。

虚拟现实(VR)和增强现实(AR)已经存在了一段时间,而最终该技术已经成熟到足以提供引人注目的体验。Facebook(Oculus Rift),Google(Daydream)和 Microsoft(Windows Holographic)都有欢迎第三方开发者加入的虚拟现实平台。然而 VR 穿戴设备依然面临着艰巨的挑战。例如如何消除穿戴者恶心的感觉,以及脱离了游戏圈,又如何创造令人信服的使用案例。

挑一种学习:云部署、机器学习库、VR 开发

如果觉得文章不错,不妨点个赞。^_^

注:

  1. 若有翻译不当之处,还请大家多多指正,我会及时修改;
  2. 本文版权归原作者所有。如需转载译文,烦请注明出处,谢谢!

英文原文:The Languages, Frameworks and Tools You Should Learn in 2017
作者:Martin Angelov
译者:IT程序狮
译文源自:zhuanlan.zhihu.com/p/24

日交易额百亿级交易系统的超轻量日志实现

首先来聊聊往事吧~~两年前就职于一家传统金融软件公司,为某交易所开发一套大型交易系统,交易标的的价格为流式数据,采用价格触发成交方式,T+0交易制度(类似炒股,只是炒的不是股票而是其他标的物,但可以随时开平仓)。鉴于系统需要记录大量价格数据、交易信息及订单流水,且系统对性能要求极高(敏感度达毫秒级),因此需要避免日志服务成为系统性能瓶颈。通过对几个通用型日志(如log4j、logback)的性能压测,以及考虑到它们作为通用型日志相对比较臃肿,就决定自个儿写个日志工具以支撑系统功能和性能所需。当时的做法只是简单的将日志的实现作为一个 util 类写在项目中,只有几百行的代码量。

系统上线两个月后日均成交额200亿RMB,最高达440亿RMB,峰值成交4000笔/秒。系统非常庞大,但几百行的代码却完美支撑住了重要的日志服务!

鉴于其优秀的表现,就花了一点点时间把它抽取出来作为一个独立的日志组件,取名叫 FLogger,代码几乎没有改动,现已托管到GitHub(FLogger),有兴趣的童鞋可以clone下来了解并改进,目前它的实现是非常简(纯)单(粹)的。

以上就是 FLogger 的诞生背景。好吧,下面进入正题。

特性

虽然 FLogger 只有几百行的代码,但是麻雀虽小五脏俱全,它可是拥有非常丰富的特性呢:

  • 双缓冲队列
  • 多种刷盘机制,支持时间触发、缓存大小触发、服务关闭强制触发等刷盘方式
  • 多种 RollingFile 机制,支持文件大小触发、按天触发等 Rolling 方式
  • 多日志级别,支持 debug、info、warn、error和 fatal 等日志级别
  • 热加载,由日志事件触发热加载
  • 超轻量,不依赖任何第三方库
  • 性能保证,成功用于日交易额百亿级交易系统

使用

既然是个超轻量级日志,使用肯定要很简单。为最大程度保持用户的使用习惯,Flogger 提供了与 log4j 几乎一样的日志 API。你只需要先获取一个实例,接下来的使用方式就非常简单了:

1
2
3
4
5
6
7
8
//获取单例
FLogger logger = FLogger.getInstance();
//简便api,只需指定内容
logger.info("Here is your message...");
//指定日志级别和内容,文件名自动映射
logger.writeLog(Constant.INFO, "Here is your customized level message...");
//指定日志输出文件名、日志级别和内容
logger.writeLog("error", Constant.ERROR, "Here is your customized log file and level message...");

使用前你需要在项目根路径下创建 log.properties 文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
########## 公共环境配置 ##########
# 字符集
CHARSET_NAME = UTF-8
########## 日志信息配置 ##########
# 日志级别   0:调试信息  1:普通信息   2:警告信息  3:错误信息  4:严重错误信息
LOG_LEVEL = 0,1,2,3,4
# 日志文件存放路径
LOG_PATH =./log
# 日志写入文件的间隔时间(默认为1000毫秒)
WRITE_LOG_INV_TIME = 1000
# 单个日志文件的大小(默认为10M)
SINGLE_LOG_FILE_SIZE = 10485760
# 单个日志文件缓存的大小(默认为10KB)
SINGLE_LOG_CACHE_SIZE = 10240

当然,为了提供最大程度的便捷性,日志内部针对所有配置项都提供了默认值,你大可不必担心缺少配置文件会抛出异常。

至此,你可能很好奇使用 FLogger 打印出来的日志格式到底是怎样的,会不会杂乱无章无法理解,还是信息不全根本无法判断上下文呢?好吧,你多虑了,FLogger 提供了非常规范且实用的日志格式,能使让你很容易理解且找到相关上下文。

先来看看上面的 demo 代码打印出来的结果:

info.log

1
[INFO] 2016-12-06 21:07:32:840 [main] Here is your message...

warn.log

1
[WARN] 2016-12-06 21:07:32:842 [main] Here is your customized level message...

error.log

1
[ERROR] 2016-12-06 21:07:32:842 [main] Here is your customized log file and level message...

从上面可以看到,你可以很清楚的分辨出日志的级别、时间和内容等信息。到这其实很明了了,日志由以下几个元素组成:

1
[日志级别] 精确到毫秒的时间 [当前线程名] 日志内容

当然,处于便捷性的考虑,FLogger 目前并不支持用户定义日志格式,毕竟它的目的也不是要做成一个通用性或者可定制性非常高的日志来使用。

源码解析

上面这么多都是围绕如何使用进行说明,下面就针对 FLogger 的特性进行实现逻辑的源码解析。

双缓冲队列

FLogger 在内部采用双缓冲队列,那何为双缓冲队列呢?它的作用又是什么呢?

FLogger 为每个日志文件维护了一个内部对象 LogFileItem ,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class LogFileItem {
    /** 不包括路径,不带扩展名的日志文件名称 如:MsgInner */
    public String logFileName = "";
    
    /** 包括路径的完整日志名称 */
    public String fullLogFileName = "";
    
    /** 当前日志文件大小 */
    public long currLogSize = 0;
    
    /** 当前正在使用的日志缓存 */
    public char currLogBuff = 'A';
    
    /** 日志缓冲列表A */
    public ArrayList<StringBuffer> alLogBufA = new ArrayList<StringBuffer>();
    
    /** 日志缓冲列表B */
    public ArrayList<StringBuffer> alLogBufB = new ArrayList<StringBuffer>();
        
    /** 下次日志输出到文件时间 */
    public long nextWriteTime = 0 ;
    
    /** 上次写入时的日期 */
    public String lastPCDate = "";
    
    /** 当前已缓存大小 */
    public long currCacheSize = 0;
}

在每次写日志时,日志内容作为一个 StringBuffer 添加到当前正在使用的 ArrayList<StringBuffer> 中,另一个则空闲。当内存中的日志输出到磁盘文件时,会将当前使用的 ArrayList<StringBuffer> 与空闲的 ArrayList<StringBuffer> 进行角色交换,交换后之前空闲的 ArrayList<StringBuffer> 将接收日志内容,而之前拥有日志内容的 ArrayList<StringBuffer> 则用来输出日志到磁盘文件。这样就可以避免每次刷盘时影响日志内容的接收(即所谓的 stop-the-world 效应)及多线程问题。流程如下:

关键代码如下:

日志接收代码

1
2
3
4
5
6
7
8
9
//同步单个文件的日志
synchronized(lfi){
    if(lfi.currLogBuff == 'A'){
        lfi.alLogBufA.add(logMsg);
    }else{
        lfi.alLogBufB.add(logMsg);
    }
    lfi.currCacheSize += CommUtil.StringToBytes(logMsg.toString()).length;
}

日志刷盘代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获得需要进行输出的缓存列表
ArrayList<StringBuffer> alWrtLog = null;
synchronized(lfi){
    if(lfi.currLogBuff == 'A'){
        alWrtLog = lfi.alLogBufA;
        lfi.currLogBuff = 'B';
    }else{
        alWrtLog = lfi.alLogBufB;
        lfi.currLogBuff = 'A';
    }
    lfi.currCacheSize = 0;
}
//创建日志文件
createLogFile(lfi);
//输出日志
int iWriteSize = writeToFile(lfi.fullLogFileName,alWrtLog);
lfi.currLogSize += iWriteSize;

多刷盘机制

FLogger 支持多种刷盘机制:

  • 刷盘时间间隔触发
  • 内存缓冲大小触发
  • 退出强制触发

下面就来一一分析。

刷盘时间间隔触发

配置项如下:

1
2
# 日志写入文件的间隔时间(默认为1000毫秒)
WRITE_LOG_INV_TIME = 1000

当距上次刷盘时间超过间隔时间,将执行内存日志刷盘。

内存缓冲大小触发

配置项如下:

1
2
# 单个日志文件缓存的大小(默认为10KB)
SINGLE_LOG_CACHE_SIZE = 10240

当内存缓冲队列的大小超过配置大小时,将执行内存日志刷盘。

退出强制触发

FLogger 内部注册了 JVM 关闭钩子 ShutdownHook ,当 JVM 正常关闭时,由钩子触发强制刷盘,避免内存日志丢失。相关代码如下:

1
2
3
4
5
6
7
8
public FLogger(){
    Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
        @Override
        public void run() {
            close();
        }
    }));
}

当 JVM 异常退出时无法保证内存中的日志全部落盘,但可以通过一种妥协的方式来提高日志刷盘的实时度:设置 SINGLE_LOG_CACHE_SIZE = 0 或者 WRITE_LOG_INV_TIME = 0 。

刷盘代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** 线程方法 */
public void run(){
    int i = 0 ;
    while(bIsRun){
        try{
            //输出到文件
            flush(false);
            //重新获取日志级别
            if(i++ % 100 == 0){
                Constant.CFG_LOG_LEVEL = CommUtil.getConfigByString("LOG_LEVEL","0,1,2,3,4");
                i = 1;
            }
        }catch(Exception e){
            System.out.println("开启日志服务错误...");
            e.printStackTrace();
        }
    }
}
/** 关闭方法 */
public void close(){
    bIsRun = false;
    try{
        flush(true);
    }catch(Exception e){
        System.out.println("关闭日志服务错误...");
        e.printStackTrace();
    }
}
    
/**
* 输出缓存的日志到文件
* @param bIsForce 是否强制将缓存中的日志输出到文件
*/
private void flush(boolean bIsForce) throws IOException{
    long currTime = System.currentTimeMillis();
    Iterator<String> iter = logFileMap.keySet().iterator();
    while(iter.hasNext()){
        LogFileItem lfi = logFileMap.get(iter.next());
        if(currTime >= lfi.nextWriteTime || SINGLE_LOG_CACHE_SIZE <= lfi.currCacheSize || bIsForce == true){
            //获得需要进行输出的缓存列表
            ArrayList<StringBuffer> alWrtLog = null;
            synchronized(lfi){
                if(lfi.currLogBuff == 'A'){
                    alWrtLog = lfi.alLogBufA;
                    lfi.currLogBuff = 'B';
                }else{
                    alWrtLog = lfi.alLogBufB;
                    lfi.currLogBuff = 'A';
                }
                lfi.currCacheSize = 0;
            }
            //创建日志文件
            createLogFile(lfi);
            //输出日志
            int iWriteSize = writeToFile(lfi.fullLogFileName,alWrtLog);
            lfi.currLogSize += iWriteSize;
        }
    }      
}

多 RollingFile 机制

同 log4j/logback,FLogger 也支持多种 RollingFile 机制:

  • 按文件大小 Rolling
  • 按天 Rolling

其中按文件大小 Rolling,配置项为:

1
2
# 单个日志文件的大小(默认为10M)
SINGLE_LOG_FILE_SIZE = 10485760

即当文件大小超过配置大小时,将创建新的文件记录日志,同时重命名旧文件为”日志文件名_日期_时间.log”(如 info_20161208_011105.log)。

按天 Rolling 即每天产生不同的文件。

产生的日志文件列表可参考如下:

1
2
3
4
5
info_20161207_101105.log
info_20161207_122010.log
info_20161208_011110.log
info_20161208_015010.log
info.log

 当前正在写入的日志文件为 info.log。

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 创建日志文件
* @param lfi
*/
private void createLogFile(LogFileItem lfi){
    //当前系统日期
    String currPCDate = TimeUtil.getPCDate('-');
        
    //如果超过单个文件大小,则拆分文件
    if(lfi.fullLogFileName != null && lfi.fullLogFileName.length() > 0 && lfi.currLogSize >= LogManager.SINGLE_LOG_FILE_SIZE ){
        File oldFile = new File(lfi.fullLogFileName);
        if(oldFile.exists()){
            String newFileName = Constant.CFG_LOG_PATH + "/" + lfi.lastPCDate + "/" + lfi.logFileName + "_" + TimeUtil.getPCDate() + "_"+ TimeUtil.getCurrTime() + ".log";
            File newFile = new File(newFileName);
            boolean flag = oldFile.renameTo(newFile);
            System.out.println("日志已自动备份为 " + newFile.getName() + ( flag ? "成功!" : "失败!" ) );
            lfi.fullLogFileName = "";
            lfi.currLogSize = 0;
        }
    }
    //创建文件
    if ( lfi.fullLogFileName == null || lfi.fullLogFileName.length() <= 0 || lfi.lastPCDate.equals(currPCDate) == false ){
        String sDir = Constant.CFG_LOG_PATH + "/" + currPCDate ;
        File file = new File(sDir);
        if(file.exists() == false){
            file.mkdir();
        }
        lfi.fullLogFileName = sDir + "/" + lfi.logFileName + ".log";
        lfi.lastPCDate = currPCDate;
            
        file = new File(lfi.fullLogFileName);
        if(file.exists()){
            lfi.currLogSize = file.length();
        }else{
            lfi.currLogSize = 0;
        }
    }
}

多日志级别

FLogger 支持多种日志级别:

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL

FLogger 为每个日志级别都提供了简易 API,在此就不再赘述了。

打印 error 和 fatal 级别日志时,FLogger 默认会将日志内容输出到控制台。

热加载

FLogger 支持热加载,FLogger 内部并没有采用事件驱动方式(即新增、修改和删除配置文件时产生相关事件通知 FLogger 实时热加载),而是以固定频率的方式进行热加载,具体实现就是每执行完100次刷盘后才进行热加载(频率可调),关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int i = 0;
while(bIsRun){
    try{
        //等待一定时间
        Thread.sleep(200);
        //输出到文件
        flush(false);
        //重新获取日志级别
        if(i++ % 100 == 0){
            Constant.CFG_LOG_LEVEL = CommUtil.getConfigByString("LOG_LEVEL","0,1,2,3,4");
            //其他配置项热加载......
            i = 1;
        }
    }catch(Exception e){
        System.out.println("开启日志服务错误...");
        e.printStackTrace();
    }
}

这么做完全是为了保持代码的精简和功能的纯粹性。事件驱动热加载无疑是更好的热加载方式,但需要新建额外的线程并启动对配置文件的事件监听,有兴趣的童鞋可自行实现。

性能保证

FLogger 成功支撑了日交易额百亿级交易系统的日志服务,它的性能是经历过考验的。下面我们就来拿 FLogger 跟 log4j 做个简单的性能对比。

测试环境:Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz  3.20 GHz   4.00 GB Memory   64位操作系统

测试场景:单条记录72byte   共1000000条   写单个日志文件

FLogger 配置如下:

1
2
3
4
5
6
# 日志写入文件的间隔时间
WRITE_LOG_INV_TIME = 0
# 单个日志文件的大小
SINGLE_LOG_FILE_SIZE = 104857600
# 单个日志文件缓存的大小
SINGLE_LOG_CACHE_SIZE = 0

以上配置保证所有日志写入到单个文件,且尽量保证每一条记录不在内存中缓存,减少测试误差。

测试代码:

1
2
3
4
5
6
7
8
9
FLogger logger = FLogger.getInstance();    //FLogger
//Logger logger = Logger.getLogger(Log4jTest.class);   //log4j
String record = "Performance Testing about log4j and cyfonly customized java project log.";   //72字节
long st = System.currentTimeMillis();
for(int i=0; i<1000000; i++){
   logger.info(record);
}
long et = System.currentTimeMillis();
System.out.println("FLogger/log4j write 1000000 records with each record 72 bytes, cost :" + (et - st) + " millseconds");

日志内容:

1
2
3
4
5
FLogger:
[INFO] 2016-12-06 21:40:06:842 [main] Performance Testing about log4j and cyfonly customized java project log.
log4j:
[INFO ]2016-12-06 21:41:12,852, [main]Log4jTest:12, Performance Testing about log4j and cyfonly customized java project log.

测试结果(执行10次取平均值):

1
2
FLogger write 1000000 records with each record 72 bytes, cost :2144 millseconds
log4j write 1000000 records with each record 72 bytes, cost :cost :12691 millseconds

 说明:测试结果为日志全部刷盘成功的修正时间,加上各种环境的影响,有少许误差,在此仅做简单测试,并不是最严格最公平的测试对比。有兴趣的童鞋可进行精确度更高的测试。欢迎私下探讨,本人QQ:869827095。

 

如果觉得文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
打赏支持
作者:cyfonly
本文版权归作者和博客园共有,欢迎转载,未经同意须保留此段声明,且在文章页面明显位置给出原文连接。欢迎指正与交流。

设计模式总结

http://mp.weixin.qq.com/s/VHHK3Mt4FKOMUst3NJpRfw

 

从七月份开始一直到九月底才看完设计模式,在这个过程中我不敢说我已经掌握了那本书里面的内容,或者说1/5,没能力说也没有资格说。但是结果不重要,重要的是这个过程我的收获!主要包括如下几个方面:

1、认识了这么多设计模式。刚刚接触java没多久就在学长那里听过设计模式的大名,但是由于能力有限,一直不敢触碰。而今有幸将其都认识了。

2、开始有设计的理论了。在接触设计模式之前没有怎么想过设计方面东东,看到问题就立马动手解决,没有想到怎么样来设计更好,如何来是这块更加优化、漂亮。

3、开始考虑系统的可扩展性了。

4、在遇到问题后开始想有那个设计模式会适用这个场景。

5、对面向对象有了更深一步的了解。

鄙人天资不聪慧,既不是聪明人,更不是那种天才,所有顿悟有限!!!闲话过多,先看如下两幅图片

设计模式之间的关系:

设计模式总概况:

一、设计原则

1、单一职责原则

一个类,只有一个引起它变化的原因。应该只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。from:百度百科

2、开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

3、里氏代换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。from:百度百科

4、依赖倒转原则(Dependence Inversion Principle)

所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。 from:百度百科

5、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

6、合成复用原则(Composite Reuse Principle)

合成复用原则就是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的。简言之:要尽量使用组合/聚合关系,少用继承。

7、迪米特法则(最少知道原则)(Demeter Principle)

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。也就是说一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易,这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。

二、创建型模式

在软件工程中,创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象。基本的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。

创建型模式由两个主导思想构成。一是将系统使用的具体类封装起来,二是隐藏这些具体类的实例创建和结合的方式。

创建型模式又分为对象创建型模式和类创建型模式。对象创建型模式处理对象的创建,类创建型模式处理类的创建。详细地说,对象创建型模式把对象创建的一部分推迟到另一个对象中,而类创建型模式将它对象的创建推迟到子类中。

1、抽象工厂模式(Abstract Factory)

所谓抽象工厂模式就是她提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关系实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更加需要更改接口及其下所有子类。其UML结构图如下:

参与者:

AbstractFactory:抽象工厂。抽象工厂定义了一个接口,所有的具体工厂都必须实现此接口,这个接口包含了一组方法用来生产产品。

ConcreteFactory:具体工厂。具体工厂是用于生产不同产品族。要创建一个产品,客户只需要使用其中一个工厂完全不需要实例化任何产品对象。

AbstractProduct:抽象产品。这是一个产品家族,每一个具体工厂都能够生产一整组产品。

Product:具体产品。

2、建造者模式(Builder)

对于建造者模式而已,它主要是将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。适用于那些产品对象的内部结构比较复杂。

建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是如果某个产品的内部结构过于复杂,将会导致整个系统变得非常庞大,不利于控制,同时若几个产品之间存在较大的差异,则不适用建造者模式,毕竟这个世界上存在相同点大的两个产品并不是很多,所以它的使用范围有限。其UML结构图:

Builder:抽象建造者。它声明为创建一个Product对象的各个部件指定的抽象接口。

ConcreteBuilder:具体建造者。实现抽象接口,构建和装配各个部件。

Director:指挥者。构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象,它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。

Product:产品角色。一个具体的产品对象。

3、工厂方法模式(Factory Method)

作为抽象工厂模式的孪生兄弟,工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。

工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。其UML结构图:

参与者:

Product:抽象产品。所有的产品必须实现这个共同的接口,这样一来,使用这些产品的类既可以引用这个接口。而不是具体类 。

ConcreteProduct:具体产品。

Creator:抽象工厂。它实现了所有操纵产品的方法,但不实现工厂方法。Creator所有的子类都必须要实现factoryMethod()方法。

ConcreteCreator:具体工厂。制造产品的实际工厂。它负责创建一个或者多个具体产品,只有ConcreteCreator类知道如何创建这些产品。

4、原型模式(Prototype)

在我们应用程序可能有某些对象的结构比较复杂,但是我们又需要频繁的使用它们,如果这个时候我们来不断的新建这个对象势必会大大损耗系统内存的,这个时候我们需要使用原型模式来对这个结构复杂又要频繁使用的对象进行克隆。所以原型模式就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。

它主要应用与那些创建新对象的成本过大时。它的主要优点就是简化了新对象的创建过程,提高了效率,同时原型模式提供了简化的创建结构。UML结构图:

参与者:

Prototype:抽象原型类。声明克隆自身的接口。

ConcretePrototype:具体原型类。实现克隆的具体操作。

Client:客户类。让一个原型克隆自身,从而获得一个新的对象。

5、单例模式(Singleton)

单例模式,从字面上看就是一个实例的意思。所以它的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。

单例模式具备如下几个特点:

1、只有一个实例。

2、能够自我实例化。

3、提供全局访问点。

所以说当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。

单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。其UML结构图非常简单,就只有一个类:

参与者:

Singleton:单例。

三、结构型模式

结构型模式主要是用于处理类或者对象的组合,它描述了如何来类或者对象更好的组合起来,是从程序的结构上来解决模块之间的耦合问题。它主要包括适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式、代理模式这个七个模式。

1、适配器模式(Adapter)

在我们的应用程序中我们可能需要将两个不同接口的类来进行通信,在不修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将一个类的接口,转换成客户期望的另一个接口。它可以让原本两个不兼容的接口能够无缝完成对接。

作为中间件的适配器将目标类和适配者解耦,增加了类的透明性和可复用性。

参与者:

Target:目标抽象类 。

Adapter:适配器类 。通过在内部包装一个Adaptee,将源接口转成目标接口。

Adaptee:适配者类 。需要适配的类。

Client:客户类。

2、桥接模式(Bridge)

如果说某个系统能够从多个角度来进行分类,且每一种分类都可能会变化,那么我们需要做的就是讲这多个角度分离出来,使得他们能独立变化,减少他们之间的耦合,这个分离过程就使用了桥接模式。所谓桥接模式就是讲抽象部分和实现部分隔离开来,使得他们能够独立变化。

桥接模式将继承关系转化成关联关系,封装了变化,完成了解耦,减少了系统中类的数量,也减少了代码量。

参与者

Abstraction:抽象类。

RefinedAbstraction:扩充抽象类。

Implementor:实现类接口。

ConcreteImplementor:具体实现类 。

3、组合模式(Composite)

组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。它定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。

在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。

虽然组合模式能够清晰地定义分层次的复杂对象,也使得增加新构件也更容易,但是这样就导致了系统的设计变得更加抽象,如果系统的业务规则比较复杂的话,使用组合模式就有一定的挑战了。

参与者:

Component :组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。

Leaf:叶子对象。叶子结点没有子结点。

Composite:容器对象,定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。

4、装饰者模式(Decorator)

我们可以通过继承和组合的方式来给一个对象添加行为,虽然使用继承能够很好拥有父类的行为,但是它存在几个缺陷:一、对象之间的关系复杂的话,系统变得复杂不利于维护。二、容易产生“类爆炸”现象。三、是静态的。在这里我们可以通过使用装饰者模式来解决这个问题。

装饰者模式,动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更加有弹性的替代方案。虽然装饰者模式能够动态将责任附加到对象上,但是他会产生许多的细小对象,增加了系统的复杂度。

参与者:

Component: 抽象构件。是定义一个对象接口,可以给这些对象动态地添加职责。

ConcreteComponent:具体构件。是定义了一个具体的对象,也可以给这个对象添加一些职责。

Decorator: 抽象装饰类。是装饰抽象类,继承了Component,从外类来扩展Component类的功能,但对于Component来说,是无需知道Decorator存在的。

ConcreteDecorator:具体装饰类,起到给Component添加职责的功能。

5、外观模式(Facade)

我们都知道类与类之间的耦合越低,那么可复用性就越好,如果两个类不必彼此通信,那么就不要让这两个类发生直接的相互关系,如果需要调用里面的方法,可以通过第三者来转发调用。外观模式非常好的诠释了这段话。外观模式提供了一个统一的接口,用来访问子系统中的一群接口。它让一个应用程序中子系统间的相互依赖关系减少到了最少,它给子系统提供了一个简单、单一的屏障,客户通过这个屏障来与子系统进行通信。

通过使用外观模式,使得客户对子系统的引用变得简单了,实现了客户与子系统之间的松耦合。但是它违背了“开闭原则”,因为增加新的子系统可能需要修改外观类或客户端的源代码。

参与者:

Facade: 外观角色。知道哪些子系统类负责处理请求,将客户的请求代理给适合的子系统处理。

SubSystem:子系统角色。实现子系统功能,处理Facade对象发来的请求。

6、享元模式(Flyweight)

在一个系统中对象会使得内存占用过多,特别是那些大量重复的对象,这就是对系统资源的极大浪费。享元模式对对象的重用提供了一种解决方案,它使用共享技术对相同或者相似对象实现重用。

享元模式就是运行共享技术有效地支持大量细粒度对象的复用。系统使用少量对象,而且这些都比较相似,状态变化小,可以实现对象的多次复用。这里有一点要注意:享元模式要求能够共享的对象必须是细粒度对象。

享元模式通过共享技术使得系统中的对象个数大大减少了,同时享元模式使用了内部状态和外部状态,同时外部状态相对独立,不会影响到内部状态,所以享元模式能够使得享元对象在不同的环境下被共享。同时正是分为了内部状态和外部状态,享元模式会使得系统变得更加复杂,同时也会导致读取外部状态所消耗的时间过长。

参与者:

Flyweight: 抽象享元类。所有具体享元类的超类或者接口,通过这个接口,Flyweight可以接受并作用于外部专题。

ConcreteFlyweight: 具体享元类。指定内部状态,为内部状态增加存储空间。

UnsharedConcreteFlyweight: 非共享具体享元类。指出那些不需要共享的Flyweight子类。

FlyweightFactory: 享元工厂类。用来创建并管理Flyweight对象,它主要用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory就会提供一个已经创建的Flyweight对象或者新建一个(如果不存在)。

7、代理模式(Proxy)、

代理模式就是给一个对象提供一个代理,并由代理对象控制对原对象的引用。它使得客户不能直接与真正的目标对象通信。代理对象是目标对象的代表,其他需要与这个目标对象打交道的操作都是和这个代理对象在交涉。

代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的,同时也在一定程度上面减少了系统的耦合度。

参与者:

Subject: 抽象角色。声明真实对象和代理对象的共同接口。

Proxy: 代理角色。代理对象与真实对象实现相同的接口,所以它能够在任何时刻都能够代理真实对象。代理角色内部包含有对真实对象的引用,所以她可以操作真实对象,同时也可以附加其他的操作,相当于对真实对象进行封装。

RealSubject: 真实角色。它代表着真实对象,是我们最终要引用的对象。

四、行为型模式

行为型模式主要是用于描述类或者对象是怎样交互和怎样分配职责的。它涉及到算法和对象间的职责分配,不仅描述对象或者类的模式,还描述了他们之间的通信方式,它将你的注意力从控制流转移到了对象间的关系上来。行为型类模式采用继承机制在类间分派行为,而行为型对象模式使用对象复合而不是继承。它主要包括如何11中设计模式:职责链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式。

1、职责链模式(Chain of Responsibility)

职责链模式描述的请求如何沿着对象所组成的链来传递的。它将对象组成一条链,发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是职责链模式。在职责链模式中,使得每一个对象都有可能来处理请求,从而实现了请求的发送者和接收者之间的解耦。同时职责链模式简化了对象的结构,它使得每个对象都只需要引用它的后继者即可,而不必了解整条链,这样既提高了系统的灵活性也使得增加新的请求处理类也比较方便。但是在职责链中我们不能保证所有的请求都能够被处理,而且不利于观察运行时特征。

参与者:

Handler: 抽象处理者。定义了一个处理请求的方法。所有的处理者都必须实现该抽象类。

ConcreteHandler: 具体处理者。处理它所负责的请求,同时也可以访问它的后继者。如果它能够处理该请求则处理,否则将请求传递到它的后继者。

Client: 客户类。

2、命令模式(Command)

有些时候我们想某个对象发送一个请求,但是我们并不知道该请求的具体接收者是谁,具体的处理过程是如何的,们只知道在程序运行中指定具体的请求接收者即可,对于这样将请求封装成对象的我们称之为命令模式。所以命令模式将请求封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。同时命令模式支持可撤销的操作。

命令模式可以将请求的发送者和接收者之间实现完全的解耦,发送者和接收者之间没有直接的联系,发送者只需要知道如何发送请求命令即可,其余的可以一概不管,甚至命令是否成功都无需关心。同时我们可以非常方便的增加新的命令,但是可能就是因为方便和对请求的封装就会导致系统中会存在过多的具体命令类。

参与者:

Command: 抽象命令类。用来声明执行操作的接口。

ConcreteCommand: 具体命令类。将一个接收者对象绑定于一个动作,调用接收者相应的操作,以实现Excute。

Invoker: 调用者。要求该命令执行这个请求。

Receiver: 接收者。知道如何实施与执行一个请求相关的操作,任何类都有可能成为一个接收者。

Client:客户类。

3、解释器模式(Interpreter)

所谓解释器模式就是定义语言的文法,并且建立一个解释器来解释该语言中的句子。解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发的编译器中。它描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解释这些句子。

参与者:

AbstractExpression: 抽象表达式。声明一个抽象的解释操作,该接口为抽象语法树中所有的节点共享。

TerminalExpression: 终结符表达式。实现与文法中的终结符相关的解释操作。实现抽象表达式中所要求的方法。文法中每一个终结符都有一个具体的终结表达式与之相对应。

NonterminalExpression: 非终结符表达式。为文法中的非终结符相关的解释操作。

Context: 环境类。包含解释器之外的一些全局信息。

Client: 客户类。

4、迭代器模式(Iterator)

对于迭代在编程过程中我们经常用到,能够游走于聚合内的每一个元素,同时还可以提供多种不同的遍历方式,这就是迭代器模式的设计动机。在我们实际的开发过程中,我们可能会需要根据不同的需求以不同的方式来遍历整个对象,但是我们又不希望在聚合对象的抽象接口中充斥着各种不同的遍历操作,于是我们就希望有某个东西能够以多种不同的方式来遍历一个聚合对象,这时迭代器模式出现了。

何为迭代器模式?所谓迭代器模式就是提供一种方法顺序访问一个聚合对象中的各个元素,而不是暴露其内部的表示。迭代器模式是将迭代元素的责任交给迭代器,而不是聚合对象,我们甚至在不需要知道该聚合对象的内部结构就可以实现该聚合对象的迭代。

通过迭代器模式,使得聚合对象的结构更加简单,它不需要关注它元素的遍历,只需要专注它应该专注的事情,这样就更加符合单一职责原则了。

参与者:

Iterator: 抽象迭代器:所有迭代器都需要实现的接口,提供了游走聚合对象元素之间的方法。

ConcreteIterator: 具体迭代器。利用这个具体的迭代器能够对具体的聚合对象进行遍历。每一个聚合对象都应该对应一个具体的迭代器。

Aggregate: 抽象聚合类。

ConcreteAggregate: 具体聚合类。实现creatorIterator()方法,返回该聚合对象的迭代器。

5、中介者模式(Mediator)

租房各位都有过的经历吧!在这个过程中中介结构扮演着很重要的角色,它在这里起到一个中间者的作用,给我们和房主互相传递信息。在外面软件的世界里同样需要这样一个中间者。在我们的系统中有时候会存在着对象与对象之间存在着很强、复杂的关联关系,如果让他们之间有直接的联系的话,必定会导致整个系统变得非常复杂,而且可扩展性很差!在前面我们就知道如果两个类之间没有不必彼此通信,我们就不应该让他们有直接的关联关系,如果实在是需要通信的话,我们可以通过第三者来转发他们的请求。同样,这里我们利用中介者来解决这个问题。

所谓中介者模式就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。在中介者模式中,中介对象用来封装对象之间的关系,各个对象可以不需要知道具体的信息通过中介者对象就可以实现相互通信。它减少了对象之间的互相关系,提供了系统可复用性,简化了系统的结构。

在中介者模式中,各个对象不需要互相知道了解,他们只需要知道中介者对象即可,但是中介者对象就必须要知道所有的对象和他们之间的关联关系,正是因为这样就导致了中介者对象的结构过于复杂,承担了过多的职责,同时它也是整个系统的核心所在,它有问题将会导致整个系统的问题。所以如果在系统的设计过程中如果出现“多对多”的复杂关系群时,千万别急着使用中介者模式,而是要仔细思考是不是您设计的系统存在问题。

参与者:

Mediator: 抽象中介者。定义了同事对象到中介者对象之间的接口。

ConcreteMediator: 具体中介者。实现抽象中介者的方法,它需要知道所有的具体同事类,同时需要从具体的同事类那里接收信息,并且向具体的同事类发送信息。

Colleague: 抽象同事类。

ConcreteColleague: 具体同事类。每个具体同事类都只需要知道自己的行为即可,但是他们都需要认识中介者。

6、备忘录模式(Memento)

后悔药人人都想要,但是事实却是残酷的,根本就没有后悔药可买,但是也不仅如此,在软件的世界里就有后悔药!备忘录模式就是一种后悔药,它给我们的软件提供后悔药的机制,通过它可以使系统恢复到某一特定的历史状态。

所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它实现了对信息的封装,使得客户不需要关心状态保存的细节。保存就要消耗资源,所以备忘录模式的缺点就在于消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

参与者:

Originator: 原发器。负责创建一个备忘录,用以记录当前对象的内部状态,通过也可以使用它来利用备忘录恢复内部状态。同时原发器还可以根据需要决定Memento存储Originator的那些内部状态。

Memento: 备忘录。用于存储Originator的内部状态,并且可以防止Originator以外的对象访问Memento。在备忘录Memento中有两个接口,其中Caretaker只能看到备忘录中的窄接口,它只能将备忘录传递给其他对象。Originator可以看到宽接口,允许它访问返回到先前状态的所有数据。

Caretaker: 负责人。负责保存好备忘录,不能对备忘录的内容进行操作和访问,只能够将备忘录传递给其他对象。

7、观察者模式(Observer)

何谓观察者模式?观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。

在这里,发生改变的对象称之为观察目标,而被通知的对象称之为观察者。一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,所以么可以根据需要增加和删除观察者,使得系统更易于扩展。

所以观察者提供了一种对象设计,让主题和观察者之间以松耦合的方式结合。

参与者:

Subject:目标。他把所有对观察者对戏的引用保存在一个聚集里,每一个主题都可以有多个观察者。

Observer:观察者。为所有的具体观察者定义一个接口,在得到主题的通知时能够及时的更新自己。

ConcreteSubject:具体主题。将有关状态存入具体观察者对象。在具体主题发生改变时,给所有的观察者发出通知。

ConcreteObserver:具体观察者。实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题状态相协调。

8、状态模式(State)

在很多情况下我们对象的行为依赖于它的一个或者多个变化的属性,这些可变的属性我们称之为状态,也就是说行为依赖状态,即当该对象因为在外部的互动而导致他的状态发生变化,从而它的行为也会做出相应的变化。对于这种情况,我们是不能用行为来控制状态的变化,而应该站在状态的角度来思考行为,即是什么状态就要做出什么样的行为。这个就是状态模式。

所以状态模式就是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

在状态模式中我们可以减少大块的if…else语句,它是允许态转换逻辑与状态对象合成一体,但是减少if…else语句的代价就是会换来大量的类,所以状态模式势必会增加系统中类或者对象的个数。

同时状态模式是将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。但是这样就会导致系统的结构和实现都会比较复杂,如果使用不当就会导致程序的结构和代码混乱,不利于维护。

参与者:

Context:环境类。可以包括一些内部状态。

State: 抽象状态类。State定义了一个所有具体状态的共同接口,任何状态都实现这个相同的接口,这样一来,状态之间就可以互相转换了。

ConcreteState:具体状态类。具体状态类,用于处理来自Context的请求,每一个ConcreteState都提供了它对自己请求的实现,所以,当Context改变状态时行为也会跟着改变。

9、策略模式(Strategy)

我们知道一件事可能会有很多种方式来实现它,但是其中总有一种最高效的方式,在软件开发的世界里面同样如此,我们也有很多中方法来实现一个功能,但是我们需要一种简单、高效的方式来实现它,使得系统能够非常灵活,这就是策略模式。

所以策略模式就是定义了算法族,分别封装起来,让他们之前可以互相转换,此模式然该算法的变化独立于使用算法的客户。

在策略模式中它将这些解决问题的方法定义成一个算法群,每一个方法都对应着一个具体的算法,这里的一个算法我就称之为一个策略。虽然策略模式定义了算法,但是它并不提供算法的选择,即什么算法对于什么问题最合适这是策略模式所不关心的,所以对于策略的选择还是要客户端来做。客户必须要清楚的知道每个算法之间的区别和在什么时候什么地方使用什么策略是最合适的,这样就增加客户端的负担。

同时策略模式也非常完美的符合了“开闭原则”,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。但是一个策略对应一个类将会是系统产生很多的策略类。

参与者:

Context: 环境类。维护一个Strategy对象的引用,用一个ConcreteStrategy来配置,可定义一个接口来让Strategy访问它的数据。

Strategy: 抽象策略类。定义所有支持算法的公共接口。Context使用这个接口来调用某个Concretestrategy定义的算法。

ConcreteStrategy: 具体策略类。封装了具体的算法实现。

10、模板方法模式(Template Method)

有些时候我们做某几件事情的步骤都差不多,仅有那么一小点的不同,在软件开发的世界里同样如此,如果我们都将这些步骤都一一做的话,费时费力不讨好。所以我们可以将这些步骤分解、封装起来,然后利用继承的方式来继承即可,当然不同的可以自己重写实现嘛!这就是模板方法模式提供的解决方案。

所谓模板方法模式就是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

模板方法模式就是基于继承的代码复用技术的。在模板方法模式中,我们可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中。也就是说我们需要声明一个抽象的父类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法让子类来实现剩余的逻辑,不同的子类可以以不同的方式来实现这些逻辑。所以模板方法的模板其实就是一个普通的方法,只不过这个方法是将算法实现的步骤封装起来的。

参与者:

AbstractClass: 抽象类。实现了一个模板,实现算法的基本骨架,具体子类将重定义primitiveOperation()方法以实现一个算法步骤。

ConcreteClass:  具体子类。实现primitiveOperation()方法以完成算法中与特定子类相关的步骤。

11、访问者模式(Visitor)

访问者模式俗称23大设计模式中最难的一个。除了结构复杂外,理解也比较难。在我们软件开发中我们可能会对同一个对象有不同的处理,如果我们都做分别的处理,将会产生灾难性的错误。对于这种问题,访问者模式提供了比较好的解决方案。

访问者模式即表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式。同时我们还需要明确一点那就是访问者模式是适用于那些数据结构比较稳定的,因为他是将数据的操作与数据结构进行分离了,如果某个系统的数据结构相对稳定,但是操作算法易于变化的话,就比较适用适用访问者模式,因为访问者模式使得算法操作的增加变得比较简单了

参与者:

Vistor: 抽象访问者。为该对象结构中的ConcreteElement的每一个类声明的一个操作。

ConcreteVisitor: 具体访问者。实现Visitor申明的每一个操作,每一个操作实现算法的一部分。

Element: 抽象元素。定义一个Accept操作,它以一个访问者为参数。

ConcreteElement: 具体元素 。实现Accept操作。

ObjectStructure: 对象结构。能够枚举它的元素,可以提供一个高层的接口来允许访问者访问它的元素。

版权声明

作者: chenssy

出处: http://www.cnblogs.com/chenssy/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

如何实现1080P延迟低于500ms的实时超清直播传输技术

最近由于公司业务关系,需要一个在公网上能实时互动超清视频的架构和技术方案。众所周知,视频直播用 CDN + RTMP 就可以满足绝大部分视频直播业务,我们也接触了和测试了几家 CDN 提供的方案,单人直播没有问题,一旦涉及到多人互动延迟非常大,无法进行正常的互动交谈。对于我们做在线教育的企业来说没有互动的直播是毫无意义的,所以我们决定自己来构建一个超清晰(1080P)实时视频的传输方案。

先来解释下什么是实时视频,实时视频就是视频图像从产生到消费完成整个过程人感觉不到延迟,只要符合这个要求的视频业务都可以称为实时视频。关于视频的实时性归纳为三个等级:

  • 伪实时:视频消费延迟超过 3 秒,单向观看实时,通用架构是 CDN + RTMP + HLS,现在基本上所有的直播都是这类技术。
  • 准实时: 视频消费延迟 1 ~ 3 秒,能进行双方互动但互动有障碍。有些直播网站通过 TCP/UDP + FLV 已经实现了这类技术,YY 直播属于这类技术。
  • 真实时:视频消费延迟 < 1秒,平均 500 毫秒。这类技术是真正的实时技术,人和人交谈没有明显延迟感。QQ、微信、Skype 和 WebRTC 等都已经实现了这类技术。

市面上大部分真实时视频都是 480P 或者 480P 以下的实时传输方案,用于在线教育和线上教学有一定困难,而且有时候流畅度是个很大的问题。在实现超清晰实时视频我们做了大量尝试性的研究和探索,在这里会把大部分细节分享出来。

要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,总体归纳为下图:

( 点击图片可以全屏缩放)

成像延迟,一般的技术是毫无为力的,涉及到 CCD 相关的硬件,现在市面上最好的 CCD,一秒钟 50 帧,成像延迟也在 20 毫秒左右,一般的 CCD 只有 20 ~ 25 帧左右,成像延迟 40 ~ 50 毫秒。

编码延迟,和编码器有关系,在接下来的小结介绍,一般优化的空间比较小。

我们着重针对网络延迟和播放缓冲延迟来进行设计,在介绍整个技术细节之前先来了解下视频编码和网络传输相关的知识和特点。

一、视频编码那些事

我们知道从 CCD 采集到的图像格式一般的 RGB 格式的(BMP),这种格式的存储空间非常大,它是用三个字节描述一个像素的颜色值,如果是 1080P 分辨率的图像空间:1920 x 1080 x 3 = 6MB,就算转换成 JPG 也有近 200KB,如果是每秒 12 帧用 JPG 也需要近 2.4MB/S 的带宽,这带宽在公网上传输是无法接受的。

视频编码器就是为了解决这个问题的,它会根据前后图像的变化做运动检测,通过各种压缩把变化的发送到对方,1080P 进行过  H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。在我们的技术方案里面我们采用 H.264 作为默认编码器(也在研究 H.265)。

1.1 H.264 编码

前面提到视频编码器会根据图像的前后变化进行选择性压缩,因为刚开始接收端是没有收到任何图像,那么编码器在开始压缩的视频时需要做个全量压缩,这个全量压缩在 H.264 中 I 帧,后面的视频图像根据这个I帧来做增量压缩,这些增量压缩帧叫做 P 帧,H.264 为了防止丢包和减小带宽还引入一种双向预测编码的 B 帧,B 帧以前面的 I 或 P 帧和后面的 P 帧为参考帧。H.264 为了防止中间 P 帧丢失视频图像会一直错误它引入分组序列(GOP)编码,也就是隔一段时间发一个全量 I 帧,上一个 I 帧与下一个 I 帧之间为一个分组 GOP。它们之间的关系如下图:

PS:在实时视频当中最好不要加入 B 帧,因为 B 帧是双向预测,需要根据后面的视频帧来编码,这会增大编解码延迟。

1.2 马赛克、卡顿和秒开

前面提到如果 GOP 分组中的P帧丢失会造成解码端的图像发生错误,其实这个错误表现出来的就是马赛克。因为中间连续的运动信息丢失了,H.264 在解码的时候会根据前面的参考帧来补齐,但是补齐的并不是真正的运动变化后的数据,这样就会出现颜色色差的问题,这就是所谓的马赛克现象,如图:

这种现象不是我们想看到的。为了避免这类问题的发生,一般如果发现 P 帧或者 I 帧丢失,就不显示本 GOP 内的所有帧,直到下一个 I 帧来后重新刷新图像。但是 I 帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个 I 帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。如果连续丢失的视频帧太多造成解码器无帧可解,也会造成严重的卡顿现象。视频解码端的卡顿现象和马赛克现象都是因为丢帧引起的,最好的办法就是让帧尽量不丢

知道 H.264 的原理和分组编码技术后所谓的秒开技术就比较简单了,只要发送方从最近一个 GOP 的 I 帧开发发送给接收方,接收方就可以正常解码完成的图像并立即显示。但这会在视频连接开始的时候多发一些帧数据造成播放延迟,只要在接收端播放的时候尽量让过期的帧数据只解码不显示,直到当前视频帧在播放时间范围之内即可。

1.3 编码延迟与码率

前面四个延迟里面我们提到了编码延迟,编码延迟就是从 CCD 出来的 RGB 数据经过 H.264 编码器编码后出来的帧数据过程的时间。我们在一个 8 核 CPU 的普通客户机测试了最新版本 X.264 的各个分辨率的延迟,数据如下:


从上面可以看出,超清视频的编码延迟会达到 50ms,解决编码延迟的问题只能去优化编码器内核让编码的运算更快,我们也正在进行方面的工作。

在 1080P 分辨率下,视频编码码率会达到 300KB/S,单个 I 帧数据大小达到 80KB,单个 P 帧可以达到 30KB,这对网络实时传输造成严峻的挑战。

二、网络传输质量因素

实时互动视频一个关键的环节就是网络传输技术,不管是早期 VoIP,还是现阶段流行的视频直播,其主要手段是通过 TCP/IP 协议来进行通信。但是 IP 网络本来就是不可靠的传输网络,在这样的网络传输视频很容易造成卡顿现象和延迟。先来看看 IP 网络传输的几个影响网络传输质量关键因素。

2.1 TCP 和 UDP

对直播有过了解的人都会认为做视频传输首选的就是 TCP + RTMP,其实这是比较片面的。在大规模实时多媒体传输网络中,TCP 和 RTMP 都不占优势。TCP 是个拥塞公平传输的协议,它的拥塞控制都是为了保证网络的公平性而不是快速到达,我们知道,TCP 层只有顺序到对应的报文才会提示应用层读数据,如果中间有报文乱序或者丢包都会在 TCP 做等待,所以 TCP 的发送窗口缓冲和重发机制在网络不稳定的情况下会造成延迟不可控,而且传输链路层级越多延迟会越大。

关于 TCP 的原理:

http://coolshell.cn/articles/11564.html

关于 TCP 重发延迟:

http://weibo.com/p/1001603821691477346388

在实时传输中使用 UDP 更加合理,UDP 避免了 TCP 繁重的三次握手、四次挥手和各种繁杂的传输特性,只需要在 UDP 上做一层简单的链路 QoS 监测和报文重发机制,实时性会比 TCP 好,这一点从 RTP 和 DDCP 协议可以证明这一点,我们正式参考了这两个协议来设计自己的通信协议。

2.2 延迟

要评估一个网络通信质量的好坏和延迟一个重要的因素就是 Round-Trip Time(网络往返延迟),也就是 RTT。评估两端之间的 RTT 方法很简单,大致如下:

  1. 发送端方一个带本地时间戳 T1 的 ping 报文到接收端;
  2. 接收端收到 ping 报文,以 ping 中的时间戳 T1 构建一个携带 T1 的 pong 报文发往发送端;
  3. 发送端接收到接收端发了的 pong 时,获取本地的时间戳 T2,用 T2 – T1 就是本次评测的 RTT。

示意图如下:

( 点击图片可以全屏缩放)

上面步骤的探测周期可以设为 1 秒一次。为了防止网络突发延迟增大,我们采用了借鉴了 TCP 的 RTT 遗忘衰减的算法来计算,假设原来的 RTT 值为 rtt,本次探测的 RTT 值为 keep_rtt。那么新的 RTT 为:

new_rtt = (7 * rtt + keep_rtt) / 8

可能每次探测出来的 keep_rtt 会不一样,我们需要会计算一个 RTT 的修正值 rtt_var,算法如下:

new_rtt_var = (rtt_var * 3 + abs(rtt – keep_rtt)) / 4 

rtt_var 其实就是网络抖动的时间差值。

如果 RTT 太大,表示网络延迟很大。我们在端到端之间的网络路径同时保持多条并且实时探测其网络状态,如果 RTT 超出延迟范围会进行传输路径切换(本地网络拥塞除外)。

2.3 抖动和乱序

UDP 除了延迟外,还会出现网络抖动。什么是抖动呢?举个例子,假如我们每秒发送 10 帧视频帧,发送方与接收方的延迟为 50MS,每帧数据用一个 UDP 报文来承载,那么发送方发送数据的频率是 100ms 一个数据报文,表示第一个报文发送时刻 0ms, T2 表示第二个报文发送时刻 100ms . . .,如果是理想状态下接收方接收到的报文的时刻依次是(50ms, 150ms, 250ms, 350ms….),但由于传输的原因接收方收到的报文的相对时刻可能是(50ms, 120ms, 240ms, 360ms ….),接收方实际接收报文的时刻和理想状态时刻的差值就是抖动。如下示意图:


( 点击图片可以全屏缩放)

我们知道视频必须按照严格是时间戳来播放,否则的就会出现视频动作加快或者放慢的现象,如果我们按照接收到视频数据就立即播放,那么这种加快和放慢的现象会非常频繁和明显。也就是说网络抖动会严重影响视频播放的质量,一般为了解决这个问题会设计一个视频播放缓冲区,通过缓冲接收到的视频帧,再按视频帧内部的时间戳来播放既可以了。

UDP 除了小范围的抖动以外,还是出现大范围的乱序现象,就是后发的报文先于先发的报文到达接收方。乱序会造成视频帧顺序错乱,一般解决的这个问题会在视频播放缓冲区里做一个先后排序功能让先发送的报文先进行播放。

播放缓冲区的设计非常讲究,如果缓冲过多帧数据会造成不必要的延迟,如果缓冲帧数据过少,会因为抖动和乱序问题造成播放无数据可以播的情况发生,会引起一定程度的卡顿。关于播放缓冲区内部的设计细节我们在后面的小节中详细介绍。

2.4 丢包

UDP 在传输过程还会出现丢包,丢失的原因有多种,例如:网络出口不足、中间网络路由拥堵、socket 收发缓冲区太小、硬件问题、传输损耗问题等等。在基于 UDP 视频传输过程中,丢包是非常频繁发生的事情,丢包会造成视频解码器丢帧,从而引起视频播放卡顿。这也是大部分视频直播用 TCP 和 RTMP 的原因,因为 TCP 底层有自己的重传机制,可以保证在网络正常的情况下视频在传输过程不丢。基于 UDP 丢包补偿方式一般有以下几种:

报文冗余

报文冗余很好理解,就是一个报文在发送的时候发送 2 次或者多次。这个做的好处是简单而且延迟小,坏处就是需要额外 N 倍(N 取决于发送的次数)的带宽。

FEC

Forward Error Correction,即向前纠错算法,常用的算法有纠删码技术(EC),在分布式存储系统中比较常见。最简单的就是 A B 两个报文进行 XOR(与或操作)得到 C,同时把这三个报文发往接收端,如果接收端只收到 AC,通过 A 和 C 的 XOR 操作就可以得到 B 操作。这种方法相对增加的额外带宽比较小,也能防止一定的丢包,延迟也比较小,通常用于实时语音传输上。对于  1080P 300KB/S 码率的超清晰视频,哪怕是增加 20% 的额外带宽都是不可接受的,所以视频传输不太建议采用 FEC 机制。

丢包重传

丢包重传有两种方式,一种是 push 方式,一种是 pull 方式。Push 方式是发送方没有收到接收方的收包确认进行周期性重传,TCP 用的是 push 方式。pull 方式是接收方发现报文丢失后发送一个重传请求给发送方,让发送方重传丢失的报文。丢包重传是按需重传,比较适合视频传输的应用场景,不会增加太对额外的带宽,但一旦丢包会引来至少一个 RTT 的延迟。

2.5 MTU 和最大 UDP

IP 网定义单个 IP 报文最大的大小,常用 MTU 情况如下:

超通道 65535

16Mb/s 令牌环 179144

Mb/s 令牌环 4464

FDDI 4352

以太网 1500

IEEE 802.3/802.2 1492

X.25 576

点对点(低时延)296

红色的是 Internet 使用的上网方式,其中 X.25 是个比较老的上网方式,主要是利用 ISDN 或者电话线上网的设备,也不排除有些家用路由器沿用 X.25 标准来设计。所以我们必须清晰知道每个用户端的 MTU 多大,简单的办法就是在初始化阶段用各种大小的 UDP 报文来探测 MTU 的大小。MTU 的大小会影响到我们视频帧分片的大小,视频帧分片的大小其实就是单个 UDP 报文最大承载的数据大小。

分片大小 = MTU – IP 头大小 – UDP 头大小 – 协议头大小;

IP 头大小 = 20 字节, UDP 头大小 = 8 字节。

为了适应网络路由器小包优先的特性,我们如果得到的分片大小超过 800 时,会直接默认成 800 大小的分片。

三、传输模型

我们根据视频编码和网络传输得到特性对 1080P 超清视频的实时传输设计了一个自己的传输模型,这个模型包括一个根据网络状态自动码率的编解码器对象、一个网络发送模块、一个网络接收模块和一个 UDP 可靠到达的协议模型。各个模块的关系示意图如下:


( 点击图片可以全屏缩放)

3.1 通信协议

先来看通信协议,我们定义的通信协议分为三个阶段:接入协商阶段、传输阶段、断开阶段。

接入协商阶段:

主要是发送端发起一个视频传输接入请求,携带本地的视频的当前状态、起始帧序号、时间戳和 MTU 大小等,接收方在收到这个请求后,根据请求中视频信息初始化本地的接收通道,并对本地 MTU 和发送端 MTU 进行比较取两者中较小的回送给发送方, 让发送方按协商后的 MTU 来分片。示意图如下:

( 点击图片可以全屏缩放)

传输阶段:

传输阶段有几个协议,一个测试量 RTT 的 PING/PONG 协议、携带视频帧分片的数据协议、数据反馈协议和发送端同步纠正协议。其中数据反馈协议是由接收反馈给发送方的,携带接收方已经接收到连续帧的报文 ID、帧 ID 和请求重传的报文 ID 序列。同步纠正协议是由发送端主动丢弃发送窗口缓冲区中的报文后要求接收方同步到当前发送窗口位置,防止在发送主动丢弃帧数据后接收方一直要求发送方重发丢弃的数据。示意图如下:

( 点击图片可以全屏缩放)

断开阶段:

就一个断开请求和一个断开确认,发送方和接收方都可以发起断开请求。

3.2 发送

发送主要包括视频帧分片算法、发送窗口缓冲区、拥塞判断算法、过期帧丢弃算法和重传。先一个个来介绍。

帧分片

前面我们提到 MTU 和视频帧大小,在 1080P 下大部分视频帧的大小都大于 UDP 的 MTU 大小,那么就需要对帧进行分片,分片的方法很简单,按照先连接过程协商后的 MTU 大小来确定分片大小(确定分片大小的算法在 MTU 小节已经介绍过),然后将 帧数据按照分片大小切分成若干份,每一份分片以 segment 报文形式发往接收方。

重传

重传比较简单,我们采用 pull 方式来实现重传,当接收方发生丢包,如果丢包的时刻 T1 + rtt_var< 接收方当前的时刻 T2,就认为是丢包了,这个时候就会把所有满足这个条件丢失的报文 ID 构建一个 segment ack 反馈给发送方,发送方收到这个反馈根据 ID 到重发窗口缓冲区中查找对应的报文重发即可。

为什么要间隔一个 rtt_var 才认为是丢包了?因为报文是有可能乱序到达,所有要等待一个抖动周期后认为丢失的报文还没有来才确认是报文丢失了,如果检测到丢包立即发送反馈要求重传,有可能会让发送端多发数据,造成带宽让费和网络拥塞。

发送窗口缓冲区

发送窗口缓冲区保存这所有正在发送且没有得到发送方连续 ID 确认的报文。当接收方反馈最新的连续报文 ID,发送窗口缓冲就会删除所有小于最新反馈连续的报文 ID,发送窗口缓冲区缓冲的报文都是为了重发而存在。这里解释下接收方反馈的连续的报文 ID,举个例子,假如发送方发送了 1. 2. 3. 4. 5,接收方收到 1.2. 4. 5。这个时候最小连续 ID = 2,如果后面又来了 3,那么接收方最小连续 ID = 5。

拥塞判断

我们把当前时间戳记为 curr_T,把发送窗口缓冲区中最老的报文的时间戳记为 oldest_T,它们之间的间隔记为 delay,那么

delay = curr_T – oldest_T

在编码器请求发送模块发送新的视频帧时,如果 delay > 拥塞阈值 Tn,我们就认为网络拥塞了,这个时候会根据最近 20 秒接收端确认收到的数据大小计算一个带宽值,并把这个带宽值反馈给编码器,编码器收到反馈后,会根据带宽调整编码码率。如果多次发生要求降低码率的反馈,我们会缩小图像的分辨率来保证视频的流畅性和实时性。Tn 的值可以通过 rtt 和 rtt_var 来确定。

但是网络可能阶段性拥塞,过后却恢复正常,我们设计了一个定时器来定时检查发送方的重发报文数量和 delay,如果发现恢复正常,会逐步增大编码器编码码率,让视频恢复到指定的分辨率和清晰度。

过期帧丢弃

在网络拥塞时可能发送窗口缓冲区中有很多报文正在发送,为了缓解拥塞和减少延迟我们会对整个缓冲区做检查,如果有超过一定阈值时间的 H.264 GOP 分组存在,我们会将这个 GOP 所有帧的报文从窗口缓冲区移除。并将它下一个 GOP 分组的 I 的帧 ID 和报文 ID 通过 wnd sync 协议同步到接收端上,接收端接收到这个协议,会将最新连续 ID 设置成同步过来的 ID。这里必须要说明的是如果频繁出现过期帧丢弃的动作会造成卡顿,说明当前网络不适合传输高分辨率视频,可以直接将视频设成更小的分辨率

3.3 接收

接收主要包括丢包管理、播放缓冲区、缓冲时间评估和播放控制,都是围绕播放缓冲区来实现的,一个个来介绍。

丢包管理

丢包管理包括丢包检测和丢失报文 ID 管理两部分。丢包检测过程大致是这样的,假设播放缓冲区的最大报文 ID 为 max_id,网络上新收到的报文 ID 为 new_id,如果 max_id + 1 < new_id,那么可能发生丢包,就会将 [max_id + 1, new_id -1] 区间中所有的 ID 和当前时刻作为 K/V 对加入到丢包管理器当中。如果 new_id < max_id,那么就将丢包管理中的 new_id 对应的 K/V 对删除,表示丢失的报文已经收到。当收包反馈条件满足时,会扫描整个丢包管理,将达到请求重传的丢包 ID 加入到 segment ack 反馈消息中并发往发送方请求重传,如果 ID 被请求了重传,会将当前时刻设置为 K/V 对中,增加对应报文的重传计数器 count,这个扫描过程会统计对包管理器中单个重发最多报文的重发次数 resend_count。

缓冲时间评估

在前面的抖动与乱序小节中我们提到播放端有个缓冲区,这个缓冲区过大时延迟就大,缓冲区过小时又会出现卡顿现象,我们针对这个问题设计了一个缓冲时间评估的算法。缓冲区评估先会算出一个 cache timer,cache timer 是通过扫描对包管理得到的 resend count 和 rtt 得到的,我们知道从请求重传报文到接收方收到重传的报文的时间间隔是一个 RTT 周期,所以 cache timer 的计算方式如下。

cache timer = (2 * resend_count+ 1) * (rtt + rtt_var) / 2

有可能 cache timer 计算出来很小(小于视频帧之间间隔时间 frame timer),那么 cache timer = frame timer,也就是说网络再好,缓冲区缓冲区至少 1 帧视频的数据,否则缓冲区是毫无意义的。

如果单位时间内没有丢包重传发生,那么 cache timer 会做适当的缩小,这样做的好处是当网络间歇性波动造成 cache timer 很大,恢复正常后 cache timer 也能恢复到相对小位置,缩减不必要的缓冲区延迟。

播放缓冲区

我们设计的播放缓冲区是按帧 ID 为索引的有序循环数组,数组内部的单元是视频帧的具体信息:帧 ID、分片数、帧类型等。缓冲区有两个状态:waiting 和 playing,waiting 状态表示缓冲区处于缓冲状态,不能进行视频播放直到缓冲区中的帧数据达到一定的阈值。Playing 状态表示缓冲区进入播放状态,播放模块可以从中取出帧进行解码播放。我们来介绍下这两个状态的切换关系:

  1. 当缓冲区创建时会被初始化成 waiting 状态。
  2. 当缓冲区中缓冲的最新帧与最老帧的时间戳间隔 > cache timer 时,进入 playing 状态并更当前时刻设成播放绝对时间戳 play ts。
  3. 当缓冲区处于 playing 状态且缓冲区是没有任何帧数据,进入 waiting 状态直到触发第 2 步。

播放缓冲区的目的就是防止抖动和应对丢包重传,让视频流能按照采集时的频率进行播放,播放缓冲区的设计极其复杂,需要考虑的因素很多,实现的时候需要慎重。

播放控制

接收端最后一个环节就是播放控制,播放控制就是从缓冲区中拿出有效的视频帧进行解码播放。但是怎么拿?什么时候拿?我们知道视频是按照视频帧从发送端携带过来的相对时间戳来做播放,我们每一帧视频都有一个相对时间戳 TS,根据帧与帧之间的 TS 的差值就可以知道上一帧和下一帧播放的时间间隔,假如上一帧播放的绝对时间戳为 prev_play_ts,相对时间戳为 prev_ts,当前系统时间戳为 curr_play_ts,当前缓冲区中最小序号帧的相对时间戳为  frame_ts,只要满足:

Prev_play_ts + (frame_ts – prev_ts) < curr_play_ts 且这一帧数据是所有的报文都收齐了

这两个条件就可以进行解码播放,取出帧数据后将 Prev_play_ts = cur_play_ts,但更新 prev_ts 有些讲究,为了防止缓冲延迟问题我们做了特殊处理。

如果 frame_ts + cache timer < 缓冲区中最大帧的 ts,表明缓冲的时延太长,则 prev_ts = 缓冲区中最大帧的 ts – cache timer。 否则 prev_ts = frame_ts。

四、测量

再好的模型也需要有合理的测量方式来验证,在多媒体这种具有时效性的传输领域尤其如此。一般在实验室环境我们采用 netem 来进行模拟公网的各种情况进行测试,如果在模拟环境已经达到一个比较理想的状态后会组织相关人员在公网上进行测试。下面来介绍怎么来测试我们整个传输模型的。

4.1 netem 模拟测试

Netem 是 Linux 内核提供的一个网络模拟工具,可以设置延迟、丢包、抖动、乱序和包损坏等,基本能模拟公网大部分网络情况。

关于 netem 可以访问它的官网:

https://wiki.linuxfoundation.org/networking/netem

我们在实验环境搭建了一个基于服务器和客户端模式的测试环境,下面是测试环境的拓扑关系图:

我们利用 Linux 来做一个路由器,服务器和收发端都连接到这个路由器上,服务器负责客户端的登记、数据转发、数据缓冲等,相当于一个简易的流媒体服务器。Sender 负责媒体编码和发送,receiver 负责接收和媒体播放。为了测试延迟,我们把 sender 和 receiver 运行在同一个 PC 机器上,在 sender 从 CCD 获取到 RGB 图像时打一个时间戳,并把这个时间戳记录在这一帧数据的报文发往 server 和 receiver,receiver 收到并解码显示这帧数据时,通过记录的时间戳可以得到整个过程的延迟。我们的测试用例是用 1080P 码率为 300KB/S 视频流,在 router 用 netem 上模拟了以下几种网络状态:

  1. 环路延迟 10m,无丢包,无抖动,无乱序
  2. 环路延迟 30ms,丢包 0.5%,抖动 5ms, 2% 乱序
  3. 环路延迟 60ms,丢包 1%,抖动 20ms, 3% 乱序,0.1% 包损坏
  4. 环路延迟 100ms,丢包 4%,抖动 50ms, 4% 乱序,0.1% 包损坏
  5. 环路延迟 200ms,丢包 10%,抖动 70ms, 5% 乱序,0.1% 包损坏
  6. 环路延迟 300ms,丢包 15%,抖动 100ms, 5% 乱序,0.1% 包损坏

因为传输机制采用的是可靠到达,那么检验传输机制有效的参数就是视频延迟,我们统计 2 分钟周期内最大延迟,以下是各种情况的延迟曲线图:

从上图可以看出,如果网络控制在环路延迟在 200ms 丢包在 10% 以下,可以让视频延迟在 500ms 毫秒以下,这并不是一个对网络质量要求很苛刻的条件。所以我们在后台的媒体服务部署时,尽量让客户端到媒体服务器之间的网络满足这个条件,如果网路环路延迟在 300ms 丢包 15% 时,依然可以做到小于 1 秒的延迟,基本能满足双向互动交流。

4.2 公网测试

公网测试相对比较简单,我们将 Server 部署到 UCloud 云上,发送端用的是上海电信 100M 公司宽带,接收端用的是河北联通 20M 小区宽带,环路延迟在 60ms 左右。总体测试下来 1080P 在接收端观看视频流畅自然,无抖动,无卡顿,延迟统计平均在 180ms 左右。

五、坑

在整个 1080P 超清视频的传输技术实现过程中,我们遇到过比较多的坑。大致如下:

Socket 缓冲区问题

我们前期开发阶段都是使用 socket 默认的缓冲区大小,由于 1080P 图像帧的数据非常巨大(关键帧超过 80KB),我们发现在在内网测试没有设置丢包的网络环境发现接收端有严重的丢包,经查证是 socket 收发缓冲区太小造成丢包的,后来我们把 socket 缓冲区设置到 128KB 大小,问题解决了。

H.264 B 帧延迟问题

前期我们为了节省传输带宽和防丢包开了 B 帧编码,由于 B 帧是前后双向预测编码的,会在编码期滞后几个帧间隔时间,引起了超过 100ms 的编码延时,后来我们为了实时性干脆把 B 帧编码选项去掉。

Push 方式丢包重传

在设计阶段我们曾经使用发送端主动 push 方式来解决丢包重传问题,在测试过程发现在丢包频繁发生的情况下至少增加了 20% 的带宽消耗,而且容易带来延迟和网络拥塞。后来几经论证用现在的 pull 模式来进行丢包重传。

Segment 内存问题

在设计阶段我们对每个视频缓冲区中的帧信息都是动态分配内存对象的,由于 1080P 在传输过程中每秒会发送 400 – 500 个 UDP 报文,在 PC 端长时间运行容易出现内存碎片,在服务器端出现莫名其妙的 clib 假内存泄露和并发问题。我们实现了一个 memory slab 管理频繁申请和释放内存的问题。

音频和视频数据传输问题

在早期的设计之中我们借鉴了 FLV 的方式将音频和视频数据用同一套传输算法传输,好处就是容易实现,但在网络波动的情况下容易引起声音卡顿,也无法根据音频的特性优化传输。后来我们把音频独立出来,针对音频的特性设计了一套低延迟高质量的音频传输体系,定点对音频进行传输优化。

后续的工作是重点放在媒体器多点分布、多点并发传输、P2P 分发算法的探索上,尽量减少延迟和服务带宽成本,让传输变的更高效和更低廉。

Q&A

提问:在优化到 500ms 方案中,哪一块是最关键的?

袁荣喜:主要是丢包重传 拥塞和播放缓冲这三者之间的协调工作最为关键,要兼顾延迟控制和视频流畅性。

提问:多方视频和单方有哪些区别,用到了 CDN 推流吗?

袁荣喜:我们公司是做在线教育的,很多场景需要老师和学生交谈,用 CDN 推流方式延迟很大,我们这个视频主要是解决多方通信之间交谈延迟的问题。我们现在观看放也有用 CDN 推流,但只是单纯的观看。我们也在研发基于 UDP 的观看端分发协议,目前这部分工作还没有完成。

如何做一个小型公司的技术总监

资深程序员是团队中最强大的生产力,但往往被不合理的工作安排浪费掉。因此作为一个团队的技术的“头”,必须要有明确清晰的认识,把主要的事务性工作剥离出来。并且放弃大量的管理“权力”,以提高团队开发质量和效率为最主要的目标去安排自己的工作。一般来说技术总监其实会被要求做事实上是2个职位的工作:主程、项目经理(技术化)

因此必须明确此两个职位的工作任务分割。然后把项目经理的工作,安排给另外一个人做,当然其职称可能同样也得叫“技术总监”或“主程”,总之听起来越牛X越好。

而真正的主程(技术总监)则应该投身于尽量多的技术工作中。而最重要的工作则是开发——生产代码和文档。

主程的工作:

一、开发


从来没有一个资深的外科医生会放下手术刀,而转到手术室外面指手画脚。一个资深的程序员也不应该离开代码和文档的编写,而只是做做架构图。作为对一个复杂系统的负责人,必须亲手领导和参与建造,才能有足够的能力去负担起这个责任。因此需要至少使用60%的时间来参与开发的工作,并且建议从一开始上班就开始,虽然早上的效率很低,但是跟任何艰巨工作都一样:万事开头难。在你好不容易等待电脑慢吞吞的打开了所有的IDE、需求文档、参考资料、工作计划这堆要命的东西之后,你就迈出了最重要的一步,你会发现你不在需要在网上看微博和聊QQ来提振开始工作的激情,而会被某一个优化代码的灵感而激励,或者被一个复杂而有趣的问题所吸引,从而更快的能投入到开发中。坚持打开电脑做的第一件事是打开IDE软件,是这一切最重要的一步。

开发的工作内容包括有:

1.     提出非功能性需求

一般来说功能需求总是让开发人员焦头烂额的主要原因。但是实际上很多项目死在发布之后,却是因为性能、产品质量、扩展性、二次开发效率等非功能性需求没认真去解决而导致的。主程作为经验最丰富的成员,必须要利用自己曾经的经验和教训(在这里教训往往比经验重要),提出那些自己折腾自己的“非功能性需求”,来保障整个项目在发布后不会轰然倒塌。这是个吃力不讨好的工作,因为老板和客户往往只会抱怨技术人员在玩弄把戏,骗取更多的资源或者杞人忧天。如何说服这些家伙也许不是主程的工作,但是主程必须要以高度的责任心把问题放到台面上来。沟通的工作也许让项目经理去做会更好,他们有一整套如何威逼利诱老板和客户的戏法。

2.     设计和修正软件架构

软件架构设计至关重要,而且工作繁重。不画图纸就敢开工的技术人员要么是天才要么是笨蛋。对于团队来说,架构在分工合作、避免风险、提高质量等多个方面有无可替代的作用。架构要避免成为空洞的文档,最重要的一步是有人来掌控和实施。而主程主持设计和修正的架构,并且亲手实施,让团队中的腹诽之徒完全无法避开,否则代码将无法运行!所谓设计和修正架构,并不意味所有的文档应该一个人写,而是指这个架构的每个环节,都是经过主程决策同意的。当然最好这些文档能尽量由他撰写,对于“菜鸟”团队来说,输出这种文档本身就意味着“权势”,有助于主程建立个人威信——这种看起来有点肮脏的“政治”东西,在避免团队内无止境的扯皮,以及稳定那些随时准备跳槽的成员来说,都是相当实用的。

3.     难点代码(关键需求)的开发

主程必须写代码,写那些大家都认为风险大的代码。有的系统对于性能要求很高,他就必须去完成容易出性能问题的部分,比如IO操作或者设计数据库索引。有些系统的需求非常飘忽,他就要去想办法完成框架代码或者脚本引擎,以便众多小弟可以跟着产品人员疲于奔命。这种工作内容会让主程不必完全的读过所有代码,而能牢牢的“掌握”代码,以免团队成员甩耙子的时候能充当备胎。因为融入团队的代码开发,也是一个让架构设计从日常工作中真正控制系统的工作。而且主程代码通常会被别人接触,能直接教育其他团队成员,同时也能建立——威信。

4.     救火和杀虫

这个工作其实和代码开发是一致的,如果没有平日的开发,通常紧急问题的解决也是比较难处理的。但是这个也有一个调试技巧的要求,比如要求会使用各种诊断工具。这些工具一般的开发人员可能会比较少使用。找问题的过程本身也可以提高团队其他人的技术水平。

二、培训


培训的工作应该占用30%左右的工作时间。培训是稳定团队人员最重要的手段。也是提高团队开发效率最有效的手段。工具、过程、制度、奖惩,这些都代替不了程序员一行行的去写代码,最直接的方法是让他们做的更快更好,这些需要经验和知识的积累。

1.     代码审查

关于代码审查,有太多的论述。但是代码审查还是一种“强迫”推行某种风格或者技巧的手段,这是最真实的“控制”系统的手段。也是推广知识和经验最直接的手段。一个人写的代码通常应对的问题不会特别“广泛”,因此只要审查其中一部分代码,就能给大部分别的代码带来好处。

2.     技术方案评审

什么事情应该写一个技术方案,然后进行评审,这是一个关键的问题。一般认为开发时间在2周以上的单项工作应该先做个方案。往往技术方案是系统架构的完善和补充,或者是挑战。所以主程的参与是非常必要的。但是要注意不需要去做的太琐碎,而是要提炼出“关键”的需求和“关键”的解决方案进行评审,而这些“关键”往往不是功能,而是质量上的需求,如这个系统的扩展性,是否能方便后续开发等等。也有可能在这些会议上会发生争吵,但是决策人是主程的地位是不容动摇的。君子和而不同,每个程序员都可以拥有自己的看法,但是代码必须能按方案运行起来,主程必须经常申明这点。

3.     学习与讲座

如果团队碰到问题,没有新的方法和技术去解决,是不会提高开发效率的。就好像你用牛来耕地,不管用什么管理方法,都不会赶上机械化的速度。而主程承担着不断突破自己的技术上限,介绍和推动团队使用更新的技术来解决问题的责任。抱残守缺,思想僵化,最后会被团队成员所抛弃,而且也会让团队的效能落后于业界,最后直接影响产品的生死。每年学一门新语言,这个说法可能有点激进,但是这也是作为程序员应该有的激情。

三、管理


管理等于权势?管理等于沟通?管理等于文山会海?多年专业训练出来的技术人员如何去做管理?

管理的目标是提高绩效,如果和这个目标无关,而只是和“管理者”这个头衔有关的事情,最好丢给别人去做,包括那个头衔。管理主要手段是创新:想出新的方法去解决问题,而不是繁杂的事务性工作!——一个专业秘书能比主程做的好一百倍。技术工作的创新,最主要还是在技术工作里面,而不是跳出来说:做这个,做那个。

管理的事情如果超过10%的工作时间,等于说你更像一个项目经理而非主程。

1.     绩效评定

以专业的意见来衡量别人的工作,这个负担是无人能够承担的。这个工作往往是利益分配的一种手段。类似奖惩手段。这种管理方法已经不是新事物了。但是实际上技术人员对于绩效往往持一定保留和暧昧的态度,因为这种事情难以很清晰的界定出来。需要判断而非量度,才是绩效的真正手段。如果一定要打分,一共两项足够了:进度、质量,5分制即可。更重要的事情是,告诉每个人主程的看法,告诉别人,怎样做才是更好。或者告诉团队,怎样做才更有利于我们成功(发财、上市、赢得老板和客户……)——把目标清晰告诉团队,发挥他们的主动性,是绩效评定最重要的目标。

2.     需求评定

最让技术人员头疼的可能就是和客户谈判。这个事情实际上不应该让技术人员来伤心,有项目经理就可以了。而需求评定更多的是可行性的讨论。主程如果参加每个需求评定,他要三头六臂也搞不定,正确的做法应该是具体开发的团队人员参加,而主程在开会前给与自己的意见,或者会后听取参与者的总结。——这是了解别人做什么事的一个重要手段,但无需陷入太深,因为还有代码评审和项目经理的帮忙。

3.     跨部门沟通

实在没必要参加,能躲就躲,这是扯皮的天堂。让项目经理去吧,他们的专业技巧能让这些事情更加有效。只要回来后让项目经理告诉你发生了什么事情就可以了。

4.     进度审核和任务分派

又是一个很有“权势”的工作,实际上团队成员的情况大家都知道,决定谁应该做什么事情并非需要很多时间去想的事情。所以大可以把方向性的意见告诉项目经理,让他去做。很多优秀的开发者玩EXCELPROJECT之类的水平还不如只有一年工作经验的秘书,别折腾自己了。

5.     面试

如果真想帮忙,准备一份有区分度的笔试题目吧。不靠谱的人太多,老板可不是花钱请你和他们聊天的。让项目经理去聊,不用担心他们技术不强,再不够,也会比大多数面试者要牛X。他们搞不定的人,就是应该雇佣的家伙。毕业生招聘怎么办?只要看看他们课外活动是不是有搞些专业的事情就可以了,上进心比别的东西都重要,HR会比主程看的更准,相信我。

6.     各种会议

饭无好饭,会无好会,超过6个人的会议应该坚决抵制。如果你有一个程序等着你去写,你一定无比痛恨这些会议,顺应你的内心吧!上帝保佑你。

最后说说项目经理的工作:

项目经理就像下水道的清洁工,所有那些主程不愿意去做的事情,他们都弯下腰去认真的把玩,实在是太伟大了。既然如此,为何不让他们拥有更好一点的头衔呢?如果没有他们去处理这些工作,任何一个主程都会被逼疯掉,或者他们自己变成了项目经理,让团队损失了最强力的一台代码发动机。

一、进度

1.     指定工作计划

2.     进度检查和告警

3.     工作总结和统计

二、资源

1.     整合提供各种资源,如找DBA,IT,运维人员,硬件,SVN权限,测试环境,福利,周末的活动……

2.     面试:人员是最重要的资源,不是吗?

3.     资源谈判:往往是和老板谈判,让别人明白现在的真实情况。又一个吃力不讨好的差事,但是总需要人做。

三、沟通

1.     需求评审:和需求方讨价还价,项目经理真是命苦啊……

2.     组织会议或者用其他方式通知信息给所有人:小喇叭、大喇叭、全服广播、世界频道……

对于一个小型公司,职权,头衔,收益,往往会更加敏感。但是这些都不是让项目失败的理由。一颗叫程序员的种子说:长大了我就是叫管理者的树。这个错误的观念只会让这个种子永远无法发芽。软件开发是类似外科医生的行业,而不是血汗工厂,所以不需要手持皮鞭的经理,而需要仁心仁术的神医。

像花椒,映客,来疯这种直播app,技术实现难度在哪?需要什么样技术人才,还有就是服务器带宽要求及成本?

作者:宋少东
链接:https://www.zhihu.com/question/41868659/answer/95092032
来源:知乎
著作权归作者所有,转载请联系作者获得授权。

技术层面:
技术相对都比较成熟,设备也都支持硬编码。IOS还提供现成的 Video ToolBox框架,可以对摄像头和流媒体数据结构进行处理,但Video ToolBox框架只兼容8.0以上版本,8.0以下就需要用x264的库软编了。Andriod 可以考虑用 ffmpeg 软编。

github上有现成的开源实现,推流、美颜、水印、弹幕、点赞动画、滤镜、播放都有,技术其实不是很难,而且现在很多云厂商都提供SDK,招几个懂行经验丰富的就可以,并不必须是流媒体人才。

链接:
如何搭建一个完整的视频直播系统? – 网络主播

映客、Periscope、花椒等直播APP点赞动画:
GitHub – singer1026/DMHeartFlyAnimation: 直播点赞动画

Android开源弹幕:
GitHub – Bilibili/DanmakuFlameMaster: Android开源弹幕引擎·烈焰弹幕使 ~ JNI source:Bilibili/NativeBitmapFactory

IOS开源弹幕:
GitHub – panghaijiao/HJDanmakuDemo: A high performance danmaku engine for iOS

七牛云移动端推流开源SDK:
GitHub – pili-engineering/PLCameraStreamingKit: Pili RTMP Streaming SDK for iOS, H.264 and AAC hardware encoding supported. Camera and Microphone as input source.

基于Android手机推流:
GitHub – begeekmyfriend/yasea: RTMP streaming client in pure Java for Android for those who hate JNI.

基于IOS的图像处理:
GitHub – BradLarson/GPUImage: An open source iOS framework for GPU-based image and video processing

完整的基于IOS手机直播:
GitHub – songsmith/LiveVideoCoreSDK

IOS-推流端的 RTMP库:
GitHub – songsmith/ios-librtmp: For PushSDK

OBS-PC端主播推流工具,斗鱼等游戏直播都在用
GitHub – jp9000/obs-studio: OBS

RTMP直播可以用nginx-rtmp
GitHub – arut/nginx-rtmp-module: NGINX-based Media Streaming Server

开源播放器也很多,ffplay、jwplayer、ijkplayer等等,我就不一一给你发了,哈哈
github.com/Bilibili/ijk

其实最难的难点是提高首播时间、服务质量即Qos,如何在丢包率20%的情况下还能保障稳定、流畅的直播体验,需要考虑以下方案:

1. 为加快首播时间,收流服务器主动推送 GOP 至边缘节点,边缘节点缓存 GOP,播放端则可以快速加载,减少回源延迟

2. gop丢帧,为解决延时,为什么会有延时,网络抖动、网络拥塞导致的数据发送不出去,丢完之后所有的时间戳都要修改,切记,要不客户端就会卡一个 gop的时间,是由于 dts 和 pts 的原因,或者播放器修正 dts 和 pts 也行(推流端丢gop更复杂,丢 p 帧之前的 p 帧会花屏)

3. 纯音频丢帧,要解决音视频不同步的问题,要让视频的 delta增量到你丢掉音频的delta之后,再发音频,要不就会音视频不同步

4. 源站主备切换和断线重连

5. 根据TCP拥塞窗口做智能调度,当拥塞窗口过小说明丢包率过高,需要切换节点和故障排查

6. 增加上行、下行带宽探测接口,当带宽不满足时降低视频质量,即降低码率

7. 定时获取最优的推流、拉流链路IP,尽可能保证提供最好的服务

8. 监控必须要,监控各个节点的Qos状态,来做整个平台的资源配置优化和调度

9. 如果你家产品从推流端、CDN、播放器都是自家的,保障 Qos 优势非常大

10. 当直播量非常大时,要加入集群管理和调度,保障 Qos

11. 播放端通过增加延时来减少网络抖动,通过快播来减少延时

12. 引入TCP/IP 优化,针对直播业务做下行TCP/IP优化,在某一个节点丢包率50%情况下,800kb码率可流畅播放

2016-05-27 补充:
我先后调研了七牛云、金山云、乐视云、腾讯云、百度云、斗鱼直播伴侣 推流端,功能几乎都是一样的,没啥亮点,不同的是整个直播平台服务差异和接入的简易性。后端现在 RTMP/HTTP-FLV 清一色,APP挂个源站直接接入云厂商或CDN就OK。直播要做好真心不易,楼主也做的不是很好,只是在不断的摸索、改进和优化,关于如何选择接入的云厂商或CDN,哪个稳定选哪个!

带宽成本:

粗略计算,斗鱼 TV 的在线人数超过 1000 万 +,战旗 TV 在在线人数约 500 万 +,龙珠在线人数约 400 万 +,虎牙在线人数约 100 万 +(统计于 2016 年 2 月 18 日,以各大节目在线人数人工相加得出,并未统计全部节目)。

直播平台的带宽成本费用通常取带宽峰值月结,当月 100 万最高同时在线人数,对应消耗带宽 1.5T,月结费用 3000 万人民币,20块钱1M的样子,根据直播方需求不同,价格也不一样,质量越好的就越贵。
PS:此计算方式来源于网络

可以算出各家在带宽方面每年需要付出的成本,斗鱼 TV 为 3 亿人民币,战旗 TV 为 1.5 亿人民币,龙珠为 1.2 亿人民币,虎牙为 3000 万 + 人民币。
运营层面:
正是因为现在做这个的太多了,所以你要运营和推广,这个就比较烧钱了,反正我现在知道的一些做移动直播、游戏直播、秀场直播的A轮至少得上千万。

用户体验:
离不开CDN和云厂商,他们会帮你解决大部分的技术问题,不流畅、卡顿、花屏、带宽不够、攻击、用户体验不好等一系列问题,还给你提供免费技术支持,上面提到的那些技术方案就是云厂商和CDN来帮你解决的。