【高并发简单解决方案】redis队列缓存 + mysql 批量入库 + php离线整合

【高并发简单解决方案】redis队列缓存 + mysql 批量入库 + php离线整合 – 靠谱崔小拽 – SegmentFault

问题分析

思考:应用网站架构的衍化过程中,应用最新的框架和工具技术固然是最优选择;但是,如果能在现有的框架的基础上提出简单可依赖的解决方案,未尝不是一种提升自我的尝试。

解决:

  • 问题一:要求日志最好入库;但是,直接入库mysql确实扛不住,批量入库没有问题,done。【批量入库和直接入库性能差异参考文章
  • 问题二:批量入库就需要有高并发的消息队列,决定采用redis list 仿真实现,而且方便回滚。
  • 问题三:日志量毕竟大,保存最近30条足矣,决定用php写个离线统计和清理脚本。

done,下面是小拽的简单实现过程

一:设计数据库表和存储

  • 考虑到log系统对数据库的性能更多一些,稳定性和安全性没有那么高,存储引擎自然是只支持select insert 没有索引的archive。如果确实有update需求,也可以采用myISAM。
  • 考虑到log是实时记录的所有数据,数量可能巨大,主键采用bigint,自增即可
  • 考虑到log系统以写为主,统计采用离线计算,字段均不要出现索引,因为一方面可能会影响插入数据效率,另外读时候会造成死锁,影响写数据。

二:redis存储数据形成消息队列

由于高并发,尽可能简单,直接,上代码。

<?php
/***************************************************************************
*
* 获取到的调用日志,存入redis的队列中.
* $Id$
*
**************************************************************************/

/**
* @file saveLog.php
* @date 2015/11/06 20:47:13
* @author:cuihuan
* @version $Revision$
* @brief
*
**/

// 获取info
$interface_info = $_GET['info'];

// 存入redis队列
$redis = new Redis();
$redis->connect('xx', 6379);
$redis->auth("password");

// 加上时间戳存入队列
$now_time = date("Y-m-d H:i:s");
$redis->rPush("call_log", $interface_info . "%" . $now_time);
$redis->close();


/* vim: set ts=4 sw=4 sts=4 tw=100 */
?>

三:数据定时批量入库。

定时读取redis消息队列里面的数据,批量入库。

<?php
/**
 * 获取redis消息队列中的脚本,拼接sql,批量入库。
 * @update 2015-11-07 添加失败消息队列回滚机制 
 *
 * @Author:cuihuan
 * 2015-11-06
 * */

// init redis
$redis_xx = new Redis();
$redis_xx->connect('ip', port);
$redis_xx->auth("password");

// 获取现有消息队列的长度
$count = 0;
$max = $redis_xx->lLen("call_log");

// 获取消息队列的内容,拼接sql
$insert_sql = "insert into fb_call_log (`interface_name`, `createtime`) values ";

// 回滚数组
$roll_back_arr = array();

while ($count < $max) {
    $log_info = $redis_cq01->lPop("call_log");
    $roll_back_arr = $log_info;
    if ($log_info == 'nil' || !isset($log_info)) {
        $insert_sql .= ";";
        break;
    }

    // 切割出时间和info
    $log_info_arr = explode("%",$log_info);
    $insert_sql .= " ('".$log_info_arr[0]."','".$log_info_arr[1]."'),";
    $count++;
}

// 判定存在数据,批量入库
if ($count != 0) {
    $link_2004 = mysql_connect('ip:port', 'user', 'password');
    if (!$link_2004) {
        die("Could not connect:" . mysql_error());
    }

    $crowd_db = mysql_select_db('fb_log', $link_2004);
    $insert_sql = rtrim($insert_sql,",").";";
    $res = mysql_query($insert_sql);

    // 输出入库log和入库结果;
    echo date("Y-m-d H:i:s")."insert ".$count." log info result:";
    echo json_encode($res);
    echo "</br>\n";
    
    // 数据库插入失败回滚
    if(!$res){
       foreach($roll_back_arr as $k){
           $redis_xx->rPush("call_log", $k);
       }
    }
 
    // 释放连接
    mysql_free_result($res);
    mysql_close($link_2004);
}

// 释放redis
$redis_cq01->close();
?>

四:离线天级统计和清理数据脚本

?php
/**
* static log :每天离线统计代码日志和删除五天前的日志
*
* @Author:cuihuan@baidu.com
* 2015-11-06
* */

// 离线统计
$link_2004 = mysql_connect('ip:port', 'user', 'pwd');
if (!$link_2004) {
    die("Could not connect:" . mysql_error());
}

$crowd_db = mysql_select_db('fb_log', $link_2004);

// 统计昨天的数据
$day_time = date("Y-m-d", time() - 60 * 60 * 24 * 1);
$static_sql = "get sql";

$res = mysql_query($static_sql, $link_2004);

// 获取结果入库略

// 清理15天之前的数据
$before_15_day = date("Y-m-d", time() - 60 * 60 * 24 * 15);
$delete_sql = "delete from xxx where createtime < '" . $before_15_day . "'";
try {
    $res = mysql_query($delete_sql);
}catch(Exception $e){
    echo json_encode($e)."\n";
    echo "delete result:".json_encode($res)."\n";
}

mysql_close($link_2004);
?>

五:代码部署

主要是部署,批量入库脚本的调用和天级统计脚本,crontab例行运行。

# 批量入库脚本
*/2 * * * * /home/cuihuan/xxx/lamp/php5/bin/php /home/cuihuan/xxx/batchLog.php >>/home/cuihuan/xxx/batchlog.log

# 天级统计脚本
0 5 * * * /home/cuihuan/xxx/php5/bin/php /home/cuihuan/xxx/staticLog.php >>/home/cuihuan/xxx/staticLog.log

总结:相对于其他复杂的方式处理高并发,这个解决方案简单有效:通过redis缓存抗压,mysql批量入库解决数据库瓶颈,离线计算解决统计数据,通过定期清理保证库的大小。

【转载请注明:高并发简单解决方案 | 靠谱崔小拽

性能优化模式

摘要

性能优化涉及面很广。一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高服务可用性三者相互矛盾,不可兼得。例如:增加缓存可以降低平均响应时间,但是处理线程数量会因为缓存过大而有所限制,从而降低系统吞吐量;为了提高服务可用性,对异常请求重复调用是一个常用的做法,但是这会提高响应时间并降低系统吞吐量。

对于很多像美团这样的公司,它们的系统会面临如下三个挑战:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据。这些挑战对于性能优化而言表现为:在保持和降低系统TP95响应时间(指的是将一段时间内的请求响应时间从低到高排序,高于95%请求响应时间的下确界)的前提下,不断提高系统吞吐量,提升流量高峰时期的服务可用性。这种场景下,三者的目标和改进方法取得了比较好的一致。本文主要目标是为类似的场景提供优化方案,确保系统在流量高峰时期的快速响应和高可用。

文章第一部分是介绍,包括采用模式方式讲解的优点,文章所采用案例的说明,以及后面部分用到的一些设计原则;第二部分介绍几种典型的“性能恶化模式”,阐述导致系统性能恶化,服务可用性降低的典型场景以及形成恶化循环的过程;第三部分是文章重点,阐述典型的“性能优化模式”,这些模式或者可以使服务远离“恶化模式”,或者直接对服务性能进行优化;文章最后一部分进行总结,并对未来可能出现的新模式进行展望。


介绍

模式讲解方式

关于性能优化的文章和图书已有很多,但就我所知,还没有采用模式的方式去讲解的。本文借鉴《设计模式》(“Design Patterns-Elements of Reusable Object-Oriented Software”)对设计模式的阐述方式,首先为每一种性能优化模式取一个贴切的名字,便于读者快速理解和深刻记忆,接着讲解该模式的动机和原理,然后结合作者在美团的具体工作案例进行深度剖析,最后总结采用该模式的优点以及需要付出的代价。简而言之,本文采用“命名–>原理和动机–>具体案例–>缺点和优点”的四阶段方式进行性能优化模式讲解。与其他方式相比,采用模式进行讲解有两个方面的优点:一方面,读者不仅仅能够掌握优化手段,而且能够了解采用该手段进行性能优化的场景以及所需付出的代价,这有利于读者全面理解和灵活应用;另一方面,模式解决的是特定应用场景下的一类问题,所以应用场景描述贯穿于模式讲解之中。如此,即使读者对原理不太了解,只要碰到的问题符合某个特定模式的应用场景(这往往比理解原理要简单),就可以采用对应的手段进行优化,进一步促进读者对模式的理解和掌握。

案例说明

文章的所有案例都来自于美团的真实项目。出于两方面的考虑,作者做了一定的简化和抽象:一方面,系统可以优化的问题众多,而一个特定的模式只能解决几类问题,所以在案例分析过程中会突出与模式相关的问题;另一方面,任何一类问题都需要多维度数据去描述,而应用性能优化模式的前提是多维度数据的组合值超过了某个临界点,但是精确定义每个维度数值的临界点是一件很难的事情,更别说多维度数据组合之后临界点。因此有必要对案例做一些简化,确保相关取值范围得到满足。基于以上以及其他原因,作者所给出的解决方案只是可行性方案,并不保证其是所碰到问题的最佳解决方案。

案例涉及的所有项目都是基于Java语言开发的,严格地讲,所有模式适用的场景是基于Java语言搭建的服务。从另外一方面讲,Java和C++的主要区别在于垃圾回收机制,所以,除去和垃圾回收机制紧密相关的模式之外,文章所描述的模式也适用于采用C++语言搭建的服务。对于基于其他语言开发的服务,读者在阅读以及实践的过程中需要考虑语言之间的差别。

设计原则

必须说明,本文中各种模式所要解决的问题之所以会出现,部分是因为工程师运用了某些深层次的设计原则。有些设计原则看上去和优秀的设计理念相悖,模式所解决的问题似乎完全可以避免,但是它们却被广泛使用。“存在即合理”,世界上没有完美的设计方案,任何方案都是一系列设计原则的妥协结果,所以本文主要关注点是解决所碰到的问题而不是如何绕过这些设计原则。下面对文中重要的设计原则进行详细阐述,在后面需要运用该原则时将不再解释。

最小可用原则

最小可用原则(快速接入原则)有两个关注点:1. 强调快速接入,快速完成;2. 实现核心功能可用。这是一个被普遍运用的原则,其目标是缩短测试周期,增加试错机会,避免过度设计。为了快速接入就必须最大限度地利用已有的解决方案或系统。从另外一个角度讲,一个解决方案或系统只要能够满足基本需求,就满足最小可用原则的应用需求。过度强调快速接入原则会导致重构风险的增加,原则上讲,基于该原则去设计系统需要为重构做好准备。

经济原则

经济原则关注的是成本问题,看起来很像最小可用原则,但是它们之间关注点不同。最小可用原则的目标是通过降低开发周期,快速接入而实现风险可控,而快速接入并不意味着成本降低,有时候为了实现快速接入可能需要付出巨大的成本。软件项目的生命周期包括:预研、设计、开发、测试、运行、维护等阶段。最小可用原则主要运用在预研阶段,而经济原则可以运用在整个软件生命周期里,也可以只关注某一个或者几个阶段。例如:运行时经济原则需要考虑的系统成本包括单次请求的CPU、内存、网络、磁盘消耗等;设计阶段的经济原则要求避免过度设计;开发阶段的经济原则可能关注代码复用,工程师资源复用等。

代码复用原则

代码复用原则分为两个层次:第一个层次使用已有的解决方案或调用已存在的共享库(Shared Library),也称为方案复用;第二个层次是直接在现有的代码库中开发,也称之为共用代码库。

方案复用是一个非常实用主义的原则,它的出发点就是最大限度地利用手头已有的解决方案,即使这个方案并不好。方案的形式可以是共享库,也可以是已存在的服务。方案复用的例子参见避免蚊子大炮模式的具体案例。用搜索引擎服务来解决查找附近商家的问题是一个性能很差的方案,但仍被很多工程师使用。方案复用原则的一个显著优点就是提高生产效率,例如:Java之所以能够得到如此广泛应用,原因之一就是有大量可以重复利用的开源库。实际上“Write once, run anywhere”是Java语言最核心的设计理念之一。基于Java语言开发的代码库因此得以在不同硬件平台、不同操作系统上更广泛地使用。

共用代码库要求在同一套代码库中完成所有功能开发。采用这个原则,代码库中的所有功能编译时可见,新功能代码可以无边界的调用老代码。另外,原代码库已存在的各种运行、编译、测试、配置环境可复用。主要有两个方面地好处:1. 充分利用代码库中已有的基础设施,快速接入新业务;2. 直接调用原代码中的基础功能或原語,避免网络或进程间调用开销,性能更佳。共用代码库的例子参见垂直分割模式的具体案例。

从设计的角度上讲,方案复用类似于微服务架构(Microservice Architecture,有些观点认为这是一种形式的SOA),而共用代码库和Monolithic Architecture很接近。总的来说,微服务倾向于面向接口编程,要求设计出可重用性的组件(Library或Service),通过分层组织各层组件来实现良好的架构。与之相对应,Monolith Architecture则希望尽可能在一套代码库中开发,通过直接调用代码中的基础功能或原語而实现性能的优化和快速迭代。使用Monolith Architecture有很大的争议,被认为不符合“设计模式”的理念。参考文献[4],Monolithic Design主要的缺点包括:1. 缺乏美感;2. 很难重构;3. 过早优化(参见文献[6]Optimize judiciously); 4. 不可重用;5. 限制眼界。微服务架构是很多互联网公司的主流架构,典型的运用公司包括Amazon、美团等。Monolithic Architecture也有其忠实的粉丝,例如:Tripadvisor的全球网站就共用一套代码库;基于性能的考虑,Linux最终选择的也是Monolithic kernel的模式。

奥卡姆剃刀原则

系统设计以及代码编写要遵循奥卡姆剃刀原则:Entities should not be multiplied unnecessarily。一般而言,一个系统的代码量会随着其功能增加而变多。系统的健壮性有时候也需要通过编写异常处理代码来实现。异常考虑越周全,异常处理代码量越大。但是随着代码量的增大,引入Bug的概率也就越大,系统也就越不健壮。从另外一个角度来讲,异常流程处理代码也要考虑健壮性问题,这就形成了无限循环。所以在系统设计和代码编写过程中,奥卡姆剃刀原则要求:一个功能模块如非必要,就不要;一段代码如非必写,就不写。

奥卡姆剃刀原则和最小可用原则有所区别。最小可用原则主要运用于产品MVP阶段,本文所指的奥卡姆剃刀原则主要指系统设计和代码编写两个方面,这是完全不同的两个概念。MVP包含系统设计和代码编写,但同时,系统设计和代码编写也可以发生在成熟系统的迭代阶段。


性能恶化模式

在讲解性能优化模式之前,有必要先探讨一下性能恶化模式,因为:

  1. 很多性能优化模式的目标之一就是避免系统进入性能恶化模式;
  2. 不同性能优化模式可能是避免同一种性能恶化模式;
  3. 同一种性能优化模式可能在不同阶段避免不同的性能恶化模式。
    在此统一阐述性能恶化模式,避免下文重复解释。为了便于读者清晰识别恶化模式和优化模式,恶化模式采用“XXX反模式”的方式进行命名。

长请求拥塞反模式(High Latency Invocating AntiPattern)

这是一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增加,最终可能会通过如下三种方式之一恶化系统性能:

  1. 线程数目变多导致线程之间CPU资源使用冲突,反过来进一步延长了单次请求时间;
  2. 线程数量增多以及线程中缓存变大,内存消耗随之剧增,对于基于Java语言的服务而言,又会更频繁地full GC,反过来单次请求时间会变得更长;
  3. 内存使用增多,会使操作系统内存不足,必须使用Swap,可能导致服务彻底崩溃。
    典型恶化流程图如下图:
    长请求拥塞反模式

长请求拥塞反模式所导致的性能恶化现象非常普遍,所以识别该模式非常重要。典型的场景如下:某复杂业务系统依赖于多个服务,其中某个服务的响应时间变长,随之系统整体响应时间变长,进而出现CPU、内存、Swap报警。系统进入长请求拥塞反模式的典型标识包括:被依赖服务可用性变低、响应时间变长、服务的某段计算逻辑时间变长等。

多次请求杠杆反模式(Levered Multilayer Invocating AntiPattern)

客户端一次用户点击行为往往会触发多次服务端请求,这是一次请求杠杆;每个服务端请求进而触发多个更底层服务的请求,这是第二次请求杠杆。每一层请求可能导致一次请求杠杆,请求层级越多,杠杆效应就越大。在多次请求杠杆反模式下运行的分布式系统,处于深层次的服务需要处理大量请求,容易会成为系统瓶颈。与此同时,大量请求也会给网络带来巨大压力,特别是对于单次请求数据量很大的情况,网络可能会成为系统彻底崩溃的导火索。典型恶化流程图如下图:
多次请求杠杆反模式
多次请求杠杆所导致的性能恶化现象非常常见,例如:对于美团推荐系统,一个用户列表请求会有多个算法参与,每个算法会召回多个列表单元(商家或者团购),每个列表单元有多种属性和特征,而这些属性和特征数据服务又分布在不同服务和机器上面,所以客户端的一次用户展现可能导致了成千上万的最底层服务调用。对于存在多次请求杠杆反模式的分布式系统,性能恶化与流量之间往往遵循指数曲线关系。这意味着,在平常流量下正常运行服务系统,在流量高峰时通过线性增加机器解决不了可用性问题。所以,识别并避免系统进入多次请求杠杆反模式对于提高系统可用性而言非常关键。

反复缓存反模式(Recurrent Caching AntiPattern)

为了降低响应时间,系统往往在本地内存中缓存很多数据。缓存数据越多,命中率就越高,平均响应时间就越快。为了降低平均响应时间,有些开发者会不加限制地缓存各种数据,在正常流量情况下,系统响应时间和吞吐量都有很大改进。但是当流量高峰来临时,系统内存使用开始增多,触发了JVM进行full GC,进而导致大量缓存被释放(因为主流Java内存缓存都采用SoftReference和WeakReference所导致的),而大量请求又使得缓存被迅速填满,这就是反复缓存。反复缓存导致了频繁的full GC,而频繁full GC往往会导致系统性能急剧恶化。典型恶化流程图如下图:
反复缓存反模式
反复缓存所导致性能恶化的原因是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据完全缓存的情况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提高流量高峰时可用性,而降低平均响应时间的一个例子)。反复缓存反模式往往发生在流量高峰时候,通过线性增加机器和提高机器内存可以大大减少系统崩溃的概率。


性能优化模式

水平分割模式(Horizontal partitioning Pattern)

原理和动机

典型的服务端运行流程包含四个环节:接收请求、获取数据、处理数据、返回结果。在一次请求中,获取数据和处理数据往往多次发生。在完全串行运行的系统里,一次请求总响应时间满足如下公式:

一次请求总耗时=解析请求耗时 + ∑(获取数据耗时+处理数据耗时) + 组装返回结果耗时

大部分耗时长的服务主要时间都花在中间两个环节,即获取数据和处理数据环节。对于非计算密集性的系统,主要耗时都用在获取数据上面。获取数据主要有三个来源:本地缓存,远程缓存或者数据库,远程服务。三者之中,进行远程数据库访问或远程服务调用相对耗时较长,特别是对于需要进行多次远程调用的系统,串行调用所带来的累加效应会极大地延长单次请求响应时间,这就增大了系统进入长请求拥塞反模式的概率。如果能够对不同的业务请求并行处理,请求总耗时就会大大降低。例如下图中,Client需要对三个服务进行调用,如果采用顺序调用模式,系统的响应时间为18ms,而采用并行调用只需要7ms。
水平分割模式

水平分割模式首先将整个请求流程切分为必须相互依赖的多个Stage,而每个Stage包含相互独立的多种业务处理(包括计算和数据获取)。完成切分之后,水平分割模式串行处理多个Stage,但是在Stage内部并行处理。如此,一次请求总耗时等于各个Stage耗时总和,每个Stage所耗时间等于该Stage内部最长的业务处理时间。

水平分割模式有两个关键优化点:减少Stage数量和降低每个Stage耗时。为了减少Stage数量,需要对一个请求中不同业务之间的依赖关系进行深入分析并进行解耦,将能够并行处理的业务尽可能地放在同一个Stage中,最终将流程分解成无法独立运行的多个Stage。降低单个Stage耗时一般有两种思路:1. 在Stage内部再尝试水平分割(即递归水平分割),2. 对于一些可以放在任意Stage中进行并行处理的流程,将其放在耗时最长的Stage内部进行并行处理,避免耗时较短的Stage被拉长。

水平分割模式不仅可以降低系统平均响应时间,而且可以降低TP95响应时间(这两者有时候相互矛盾,不可兼得)。通过降低平均响应时间和TP95响应时间,水平分割模式往往能够大幅度提高系统吞吐量以及高峰时期系统可用性,并大大降低系统进入长请求拥塞反模式的概率。

具体案例

我们的挑战来自为用户提供高性能的优质个性化列表服务,每一次列表服务请求会有多个算法参与,而每个算法基本上都采用“召回->特征获取->计算”的模式。 在进行性能优化之前,算法之间采用顺序执行的方式。伴随着算法工程师的持续迭代,算法数量越来越多,随之而来的结果就是客户端响应时间越来越长,系统很容易进入长请求拥塞反模式。曾经有一段时间,一旦流量高峰来临,出现整条服务链路的机器CPU、内存报警。在对系统进行分析之后,我们采取了如下三个优化措施,最终使得系统TP95时间降低了一半:

  1. 算法之间并行计算;
  2. 每个算法内部,多次特征获取进行了并行处理;
  3. 在调度线程对工作线程进行调度的时候,耗时最长的线程最先调度,最后处理。

缺点和优点

对成熟系统进行水平切割,意味着对原系统的重大重构,工程师必须对业务和系统非常熟悉,所以要谨慎使用。水平切割主要有两方面的难点:

  1. 并行计算将原本单一线程的工作分配给多线程处理,提高了系统的复杂度。而多线程所引入的安全问题让系统变得脆弱。与此同时,多线程程序测试很难,因此重构后系统很难与原系统在业务上保持一致。
  2. 对于一开始就基于单线程处理模式编写的系统,有些流程在逻辑上能够并行处理,但是在代码层次上由于相互引用已经难以分解。所以并行重构意味着对共用代码进行重复撰写,增大系统的整体代码量,违背奥卡姆剃刀原则。
    对于上面提到的第二点,举例如下:A和B是逻辑可以并行处理的两个流程,基于单线程设计的代码,假定处理完A后再处理B。在编写处理B逻辑代码时候,如果B需要的资源已经在处理A的过程中产生,工程师往往会直接使用A所产生的数据,A和B之间因此出现了紧耦合。并行化需要对它们之间的公共代码进行拆解,这往往需要引入新的抽象,更改原数据结构的可见域。

在如下两种情况,水平切割所带来的好处不明显:

  1. 一个请求中每个处理流程需要获取和缓存的数据量很大,而不同流程之间存在大量共享的数据,但是请求之间数据共享却很少。在这种情况下,流程处理完之后,数据和缓存都会清空。采用顺序处理模式,数据可以被缓存在线程局部存储(ThreadLocal)中而减少重复获取数据的成本;如果采用水平切割的模式,在一次请求中,不同流程会多次获取并缓存的同一类型数据,对于内存原本就很紧张的系统,可能会导致频繁full GC,进入反复缓存反模式。
  2. 某一个处理流程所需时间远远大于其他所有流程所需时间的总和。这种情况下,水平切割不能实质性地降低请求响应时间。

采用水平切割的模式可以降低系统的平均响应时间和TP95响应时间,以及流量高峰时系统崩溃的概率。虽然进行代码重构比较复杂,但是水平切割模式非常容易理解,只要熟悉系统的业务,识别出可以并行处理的流程,就能够进行水平切割。有时候,即使少量的并行化也可以显著提高整体性能。对于新系统而言,如果存在可预见的性能问题,把水平分割模式作为一个重要的设计理念将会大大地提高系统的可用性、降低系统的重构风险。总的来说,虽然存在一些具体实施的难点,水平分割模式是一个非常有效、容易识别和理解的模式。

垂直分割模式(Vertical partitioning Pattern)

原理和动机

对于移动互联网节奏的公司,新需求往往是一波接一波。基于代码复用原则,工程师们往往会在一个系统实现大量相似却完全不相干的功能。伴随着功能的增强,系统实际上变得越来越脆弱。这种脆弱可能表现在系统响应时间变长、吞吐量降低或者可用性降低。导致系统脆弱原因主要来自两方面的冲突:资源使用冲突和可用性不一致冲突。

资源使用冲突是导致系统脆弱的一个重要原因。不同业务功能并存于同一个运行系统里面意味着资源共享,同时也意味着资源使用冲突。可能产生冲突的资源包括:CPU、内存、网络、I/O等。例如:一种业务功能,无论其调用量多么小,都有一些内存开销。对于存在大量缓存的业务功能,业务功能数量的增加会极大地提高内存消耗,从而增大系统进入反复缓存反模式的概率。对于CPU密集型业务,当产生冲突的时候,响应时间会变慢,从而增大了系统进入长请求拥塞反模式的可能性。

不加区别地将不同可用性要求的业务功能放入一个系统里,会导致系统整体可用性变低。当不同业务功能糅合在同一运行系统里面的时候,在运维和机器层面对不同业务的可用性、可靠性进行调配将会变得很困难。但是,在高峰流量导致系统濒临崩溃的时候,最有效的解决手段往往是运维,而最有效手段的失效也就意味着核心业务的可用性降低。

垂直分割思路就是将系统按照不同的业务功能进行分割,主要有两种分割模式:部署垂直分割和代码垂直分割。部署垂直分割主要是按照可用性要求将系统进行等价分类,不同可用性业务部署在不同机器上,高可用业务单独部署;代码垂直分割就是让不同业务系统不共享代码,彻底解决系统资源使用冲突问题。

具体案例

我们的挑战来自于美团推荐系统,美团客户端的多个页面都有推荐列表。虽然不同的推荐产品需求来源不同,但是为了实现快速的接入,基于共用代码库原则,所有的推荐业务共享同一套推荐代码,同一套部署。在一段时间内,我们发现push推荐和首页“猜你喜欢推荐”的资源消耗巨大。特别是在push推荐的高峰时刻,CPU和内存频繁报警,系统不停地full GC,造成美团用户进入客户端时,首页出现大片空白。

在对系统进行分析之后,得出两个结论:

  1. 首页“猜你喜欢”对用户体验影响更大,应该给予最高可用性保障,而push推荐给予较低可用性保障;
  2. 首页“猜你喜欢”和push推荐都需要很大的本地缓存,有较大的内存使用冲突,并且响应时间都很长,有严重的CPU使用冲突。

因此我们采取了如下措施,一方面,解决了首页“猜你喜欢”的可用性低问题,减少了未来出现可用性问题的概率,最终将其TP95响应时间降低了40%;另一方面也提高了其他推荐产品的服务可用性和高峰吞吐量。

  1. 将首页“猜你喜欢”推荐进行单独部署,而将push推荐和其他对系统资源要求不高的推荐部署在另一个集群上面;
  2. 对于新承接的推荐业务,新建一套代码,避免影响首页推荐这种最高可用性的业务。

缺点和优点

垂直分割主要的缺点主要有两个:

  1. 增加了维护成本。一方面代码库数量增多提高了开发工程师的维护成本,另一方面,部署集群的变多会增加运维工程师的工作量;
  2. 代码不共享所导致的重复编码工作。

解决重复编码工作问题的一个思路就是为不同的系统提供共享库(Shared Library),但是这种耦合反过来可能导致部署机器中引入未部署业务的开销。所以在共享库中要减少静态代码的初始化开销,并将类似缓存初始化等工作交给上层系统。总的来说,通过共享库的方式引入的开销可以得到控制。但是对于业务密集型的系统,由于业务往往是高度定制化的,共用一套代码库的好处是开发工程师可以采用Copy-on-write的模式进行开发,需要修改的时候随时拷贝并修改。共享库中应该存放不容易变化的代码,避免使用者频繁升级,所以并不适合这种场景。因此,对于业务密集型的系统,分代码所导致的重复编码量是需要权衡的一个因素。

垂直分割是一个非常简单而又有效的性能优化模式,特别适用于系统已经出现问题而又需要快速解决的场景。部署层次的分割既安全又有效。需要说明的是部署分割和简单意义上的加机器不是一回事,在大部分情况下,即使不增加机器,仅通过部署分割,系统整体吞吐量和可用性都有可能提升。所以就短期而言,这几乎是一个零成本方案。对于代码层次的分割,开发工程师需要在业务承接效率和系统可用性上面做一些折衷考虑。

恒变分离模式(Runtime 3NF Pattern)

原理和动机

基于性能的设计要求变化的数据和不变的数据分开,这一点和基于面向对象的设计原则相悖。在面向对象的设计中,为了便于对一个对象有整体的把握,紧密相关的数据集合往往被组装进一个类,存储在一个数据库表,即使有部分数据冗余(关于面向对象与性能冲突的讨论网上有很多文章,本文不细讲)。很多系统的主要工作是处理变化的数据,如果变化的数据和不变的数据被紧密组装在一起,系统对变化数据的操作将引入额外的开销。而如果易变数据占总数据比例非常小,这种额外开销将会通过杠杆效应恶化系统性能。分离易变和恒定不变的数据在对象创建、内存管理、网络传输等方面都有助于性能提高。

恒变分离模式的原理非常类似与数据库设计中的第三范式(3NF):第三范式主要解决的是静态存储中重复存储的问题,而恒变分离模式解决的是系统动态运行时候恒定数据重复创建、传输、存储和处理的问题。按照3NF,如果一个数据表的每一记录都依赖于一些非主属性集合,而这些非主属性集合大量重复出现,那么应该考虑对被依赖的非主属性集合定义一个新的实体(构建一个新的数据表),原数据库的记录依赖于新实体的ID。如此一来数据库重复存储数据量将大大降低。类似的,按照恒变分离模式,对于一个实体,如果系统处理的只是这个实体的少量变化属性,应该将不变的属性定义为一个新实体(运行时的另一个类,数据库中的另一个表),原来实体通过ID来引用新实体,那么原有实体在运行系统中的数据传输、创建、网络开销都会大大降低。

案例分析

我们的挑战是提供一个高性能、高一致性要求的团购服务(DealService)。系统存在一些多次请求杠杆反模式问题,客户端一次请求会导致几十次DealService读取请求,每次获取上百个团购详情信息,服务端单机需要支持每秒万次级别的吞吐量。基于需求,系统大体框架设计如下:
恒变分离模式
每个DealService定期从持久层同步所有发生变化的deal信息,所有的deal信息保存在内存里面。在最初的设计里面,数据库只有一个数据表DealModelTable,程序里面也只有一个实体类DealModel。由于销量、价格、用户评价等信息的频发变化,为了达到高一致性要求,服务系统每分钟需要从数据库同步几万条记录。随着美团团购数量的增多和用户活跃度的增加,系统出现了三个问题:

  1. 团购服务网卡频繁报警,由于这是高性能低延时服务,又导致了大量的客户端超时异常;
  2. 频繁的full GC,这是由于每条数据库记录更新都会导致运行系统里面老的DealModel实体被销毁,新的DealModels实体被创建;
  3. 数据库从库滞后主库,使得服务数据一致性降低,原因是数据库系统写数据量巨大。

在对系统进行分析之后,我们采用了如下措施,大大降低了网络传输的数据量,缓解了主从数据库同步压力,使得客户端的超时异常从高峰时候的9%降低到了小于0.01%(低于万分之一):

  1. 将DealModelTable中的销量、价格、用户评价等常变的信息单独构建一张数据表VariableDealModel;
  2. 同时在代码中为销量、价格、用户评价等常变数据创建一个单独的类VariableDealModel;
  3. DealService对两张表进行分别同步;
  4. 如果DealModelTable的记录产生了更新,运行系统销毁老的DealModel实体并创建新的DealModel实体;
  5. 如果只是VariableDealModel的记录产生了更新,只对VariableDealModel的属性进行更改。

缺点和优点

采用恒变分离模式,主要有三个缺点:

  1. 不符合面向对象的设计原则。原本概念上统一的实体被切分成多个实体,会给开发工程师带来一些理解上的困难,因此增加维护成本。进一步而言,这会增加引入额外Bug的概率(实际上面向对象之所以如此受欢迎的一个重要原因就是容易理解)。
  2. 增加了类不变量(Class invariant)的维护难度。很多情况下,Class invariant是通过语言所提供的封装(Encapsulation)特性来维护的。当一个类变成多个类,Class invariant可能会被破坏。如果必须维护Class invariant,而这种Class invariant又发生在不同实体之间,那么往往是把不变的属性从不变实体移到易变的实体中去。
  3. 一张数据库表变成多张,也会增加维护成本。

在如下两种场景下,恒变分离模式所带来的好处有限:

  1. 易变数据导致的操作和传输并不频繁,不是系统主要操作;
  2. 易变数据占整体数据的比例很高,杠杆效应不显著,通过恒变分离模式不能根本性地解决系统性能问题。

总的来说,恒变分离模式非常容易理解,其应用往往需要满足两个条件:易变数据占整体数据比例很低(比例越低,杠杆效应越大)和易变数据所导致的操作又是系统的主要操作。在该场景下,如果系统性能已经出现问题,牺牲一些可维护性就显得物有所值。

大部分系统都是由多种类型的数据构成,大多数数据类型的都包含易变、少变和不变的属性。盲目地进行恒变分离会导致系统的复杂度指数级别的增加,系统变得很难维护,所以系统设计者必须在高性能和高维护性之间找到一个平衡点。作者的建议是:对于复杂的业务系统,尽量按照面向对象的原则进行设计,只有在性能出现问题的时候才开始考虑恒变分离模式;而对于高性能,业务简单的基础数据服务,恒变分离模式应该是设计之初的一个重要原则。

数据局部性模式(Locality Pattern)

原理和动机

数据局部性模式是多次请求杠杆反模式的针对性解决方案。在大数据和强调个性化服务的时代,一个服务消费几十种不同类型数据的现象非常常见,同时每一种类型的数据服务都有可能需要一个大的集群(多台机器)提供服务。这就意味着客户端的一次请求有可能会导致服务端成千上万次调用操作,很容易使系统进入多次请求杠杆反模式。在具体开发过程中,导致数据服务数量暴增的主要原因有两个:1. 缓存滥用以及缺乏规划,2. 数据量太大以至于无法在一台机器上提供全量数据服务。数据局部性模的核心思想是合理组织数据服务,减少服务调用次数。具体而言,可以从服务端和客户端两个方面进行优化。

服务端优化方案的手段是对服务进行重新规划。对于数据量太大以至于无法在一台机器上存储全量数据的场景,建议采用Bigtable或类似的解决方案提供数据服务。典型的Bigtable的实现包括Hbase、Google Cloud Bigtable等。实际上数据局部性是Bigtable的一个重要设计原则,其原理是通过Row key和Column key两个主键来对数据进行索引,并确保同一个Row key索引的所有数据都在一台服务器上面。通过这种数据组织方式,一次网络请求可以获取同一个Row key对应的多个Column key索引的数据。缺乏规划也是造成服务数量剧增的一个重要原因。很多通过统计和挖掘出来的特征数据往往是在漫长的时间里由不同team独立产生的。而对于每种类型数据,在其产生之初,由于不确定其实际效果以及生命周期,基于快速接入原则,服务提供者往往会用手头最容易实施的方案,例如采用Redis Cache(不加选择地使用缓存会导致缓存滥用)。数据服务之间缺乏联动以及缺乏标准接入规划流程就会导致数据服务数量膨胀。数据局部性原则对规划的要求,具体而言是指:1. 数据由尽可能少的服务器来提供,2. 经常被一起使用的数据尽可能放在同一台服务器上。

客户端优化有如下几个手段:

  1. 本地缓存,对于一致性要求不高且缓存命中率较高的数据服务,本地缓存可以减少服务端调用次数;
  2. 批处理,对于单机或者由等价的机器集群提供的数据服务,尽可能采用批处理方式,将多个请求合成在一个请求中;
  3. 客户端Hash,对于需要通过Hash将请求分配到不同数据服务机器的服务,尽量在客户端进行Hash,对于落入同一等价集群的请求采用批处理方式进行调用。

案例分析

我们的挑战来自于美团的推荐、个性化列表和个性化搜索服务。这些个性化系统需要获取各种用户、商家和团购信息。信息类型包括基本属性和统计属性。最初,不同属性数据由不同的服务提供,有些是RPC服务,有些是Redis服务,有些是HBase或者数据库,参见下图:
数据局部性模式1

通常而言,客户端每个用户请求都会触发多个算法。一方面,每个算法都会召回几十甚至几百个团购或者商家ID,团购和商家基础属性被均匀地分配到几十台Redis里面(如下图),产生了大量的Redis请求,极端情况下,一次客户端请求所触发的团购基础数据请求就超过了上千次;另一方面,用户特征属性信息有十几种,每种属性也由单独的服务提供,服务端网络调用次数暴增。在一段时间里,很多系统都进入了多次请求杠杆反模式,Redis服务器的网卡经常被打死,多次进行扩容,提高线程池线程数量,丝毫没有改善。
数据局部性模式2
在对系统进行分析之后,按照数据局部性模式的原则,我们采用了如下手段,彻底解决了系统多次请求杠杆反模式的问题:

  1. 采用大内存服务器存储所有的团购和商家基础信息,每个算法只要一次网络请求就可以获取所有的信息;
  2. 服务端采用多线程方式提供服务,避免了Redis单一线程模式下单个请求慢所带来的连锁效应;
  3. 借鉴类似Bigtable的数据组织方式,将用户的多种特征采用两个维度(用户维度和特征类型)进行索引,确保同一用户的信息只存放在一台机器上面,减少网络调用数量。

缺点和优点

数据局部性模式并不适用于系统初级阶段。在初级阶段,最小可用原则往往是主要设计原则之一,出于两方面的考虑:一方面,在初级阶段,很难预测所要提供服务的数据是否有效而且能够长期使用,以及未来的调用量;另一方面,在初级阶段,工程师可能无法预测最终的调用模式,而不同的调用模式会导致数据局部性方案的设计不同。对于已经大量使用的数据服务,采用数据局部性模式进行重构必然要改变老的调用模式,这一方面会引入新的Bug,另一方面也意味着巨大的工作量。需要特别强调的是,数据处于系统的最底层,对于结构复杂而又重要的数据,重构所带来可靠性、一致性和工作量都是需要权衡的因素。对于请求量比较小的数据服务,即使一次请求会触发严重的请求杠杆效应,但是如果原始触发请求数量在可预见的时间内没有明显变多的迹象,进行数据服务重构可能得不偿失。

数据局部性模式能够解决多次请求杠杆反模式所导致的问题,但它并非大数据的产物,CPU、编译器的设计理念里早就融入了该模式,所以很容易被工程师理解。虽然过度设计在系统初级阶段是一个要尽量避免的事情,但是理解和掌握数据局部性模式对于设计出一个可扩展、可重用的系统有很大帮助。很多成熟的系统因为多次请求杠杆反模式而导致系统频繁崩溃,理解数据局部性模式的原则有助于提高工程师分析解决问题的能力,而在确认了系统存在请求杠杆问题后,数据局部性原则是一件非常锐利的武器。

避免蚊子大炮模式(Avoiding Over-generalized Solution Pattern)

原理和动机

“用大炮打蚊子”本来是大材小用的意思,但是细致想一想,用大炮打蚊子,成功率不高。对于开发工程师而言,一方面为了快速承接业务,按照方案复用原则,总是尽可能地利用现有系统,这使得系统功能越来越强大;另一方面,提高系统的通用性或可重用性也是工程师们在设计系统的一个重要目标。随着这两个过程的相互独立演化,采用通用方案解决特定问题的现象随处可见,形象地说,这就像大炮打蚊子。大炮成本很高,蚊子的数量众多,最终的结局往往是蚊子战胜了大炮。

“避免蚊子大炮模式”是经济原则在运行时系统的运用,它要求采用最节省资源(CPU、内存等)的方法来解决所面临的问题,资源浪费会带来未来潜在的风险。工程师接到一个需求的时候,需要思考的不仅仅是如何复用现有的系统,减少开发时间,还需要考虑现有系统为处理每个新需求访问所需运行时成本,以及新需求的预期访问量。否则,不加辨别地利用现有系统,不仅仅增大了重构风险,还有可能交叉影响,对现有系统所支持的服务造成影响。从另外一个角度讲,工程师在构建一个可重用系统的时候,要明确其所不能解决和不建议解决的问题,而对于不建议解决的问题,在文档中标明潜在的风险。

案例分析

我们的挑战是为移动用户寻找其所在位置附近的商家信息。美团有非常完善的搜索系统,也有资深的搜索工程师,所以一个系统需要查找附近的商家的时候,往往第一方案就是调用搜索服务。但是在美团,太多的服务有基于LBS的查询需求,导致搜索请求量直线上升,这本来不属于搜索的主营业务,在一段时间里面反倒成了搜索的最多请求来源。而搜索引擎在如何从几十万商家里面找最近的几百商家方面的性能非常差,因此一段时间里,搜索服务频繁报警。不仅仅搜索服务可用性受到了影响,所有依赖于LBS的服务的可用性都大大降低。

在对系统分析之后,我们认为更适合解决最短直线距离的算法应该是k-d tree,在快速实现了基于k-d tree的LBS Search解决方案之后,我们用4台服务器轻松解决了30多台搜索服务器无法解决的问题,平均响应时间从高峰时的100ms降低到300ns,性能取得了几百倍的提高。

缺点和优点

避免蚊子大炮模式的问题和数据局部性模式类似,都与最小可用原则相冲突。在系统设计初级阶段,寻求最优方案往往意味着过度设计,整个项目在时间和成本变得不可控,而为每个问题去找最优秀的解决方案是不现实的奢求。最优化原则的要求是全面的,不仅仅要考虑的运行时资源,还需要考虑工程师资源和时间成本等,而这些点往往相互矛盾。在如下情况下,避免蚊子大炮模式所带来的好处有限:在可预见的未来,某个业务请求量非常小,这时候花大量精力去找最优技术方案效果不明显。

在设计阶段,避免蚊子大炮模式是一个需要工程师去权衡的选择,需要在开发成本和系统运行成本之间保持一个平衡点。当很多功能融入到一个通用系统里而出现性能问题的时候,要拆分出来每一个功能点所造成的影响也不是件轻易的事情,所以采用分开部署而共用代码库的原则可以快速定位问题,然后有针对性地解决“蚊子大炮”问题。总的来说,在设计阶段,避免蚊子大炮模式是工程师们进行分析和设计的一个重要准则,工程师可以暂时不解决潜在的问题,但是一定要清楚潜在的危害。构建可重用系统或方案,一定要明确其所不能解决和不建议解决的问题,避免过度使用。

实时离线分离模式(Sandbox Pattern)

原理和动机

本模式的极端要求是:离线服务永远不要调用实时服务。该模式比较简单也容易理解,但是,严格地讲它不是一种系统设计模式,而是一种管理规范。离线服务和在线服务从可用性、可靠性、一致性的要求上完全不同。原则上,工程师在编写离线服务代码的时候,应该遵循的就是离线服务编程规范,按照在线服务编程规范要求,成本就会大大提高,不符合经济原则;从另外一方面讲,按照离线服务的需求去写在线服务代码,可用性、可靠性、一致性等往往得不到满足。

具体而言,实时离线分离模式建议如下几种规范:

  1. 如果离线程序需要访问在线服务,应该给离线程序单独部署一套服务;
  2. 类似于MapReduce的云端多进程离线程序禁止直接访问在线服务;
  3. 分布式系统永远不要直接写传统的DBMS。

案例分析

因为违反实时离线分离模式而导致的事故非常常见。有一次,因为一个离线程序频繁的向Tair集群写数据,每一次写10M数据,使得整个Tair集群宕机。另一次,因为Storm系统直接写MySQL数据库导致数据库连接数耗尽,从而使在线系统无法连接数据库。

缺点和优点

为了实现实时在线分离,可能需要为在线环境和离线环境单独部署,维护多套环境所带来运维成本是工程师需要考虑的问题。另一方面,在线环境的数据在离线环境中可能很难获取,这也是很多离线系统直接访问在线系统的原因。但是,遵从实时离线分离模式是一个非常重要的安全管理准则,任何违背这个准则的行为都意味着系统性安全漏洞,都会增大线上故障概率。

降级模式(Degradation Pattern)

原理和动机

降级模式是系统性能保障的最后一道防线。理论上讲,不存在绝对没有漏洞的系统,或者说,最好的安全措施就是为处于崩溃状态的系统提供预案。从系统性能优化的角度来讲,不管系统设计地多么完善,总会有一些意料之外的情况会导致系统性能恶化,最终可能导致崩溃,所以对于要求高可用性的服务,在系统设计之初,就必须做好降级设计。根据作者的经验,良好的降级方案应该包含如下措施:

  1. 在设计阶段,确定系统的开始恶化数值指标(例如:响应时间,内存使用量);
  2. 当系统开始恶化时,需要第一时间报警;
  3. 在收到报警后,或者人工手动控制系统进入降级状态,或者编写一个智能程序让系统自动降级;
  4. 区分系统所依赖服务的必要性,一般分为:必要服务和可选服务。必要服务在降级状态下需要提供一个快速返回结果的权宜方案(缓存是常见的一种方案),而对于可选服务,在降级时系统果断不调用;
  5. 在系统远离恶化情况时,需要人工恢复,或者智能程序自动升级。

典型的降级策略有三种:流量降级、效果降级和功能性降级。流量降级是指当通过主动拒绝处理部分流量的方式让系统正常服务未降级的流量,这会造成部分用户服务不可用;效果降级表现为服务质量的降级,即在流量高峰时期用相对低质量、低延时的服务来替换高质量、高延时的服务,保障所有用户的服务可用性;功能性降级也表现为服务质量的降级,指的是通过减少功能的方式来提高用户的服务可用性。效果降级和功能性降级比较接近,效果降级强调的是主功能服务质量的下降,功能性降级更多强调的是辅助性功能的缺失。做一个类比如下:计划将100个工程师从北京送到夏威夷度假,但是预算不够。采用流量降级策略,只有50工程师做头等舱去了夏威夷度假,其余工程师继续编写程序(这可不好);效果降级策略下,100个工程师都坐经济舱去夏威夷;采用功能性降级策略,100个工程师都坐头等舱去夏威夷,但是飞机上不提供食品和饮料。

案例分析

我们的系统大量使用了智能降级程序。在系统恶化的时候,智能降级程序自动降级部分流量,当系统恢复的时候,智能降级程序自动升级为正常状态。在采用智能降级程序之前,因为系统降级问题,整体系统不可用的情况偶尔发生。采用智能降级程序之后,基本上没有因为性能问题而导致的系统整体不可用。我们的智能降级程序的主要判定策略是服务响应时间,如果出现大量长时间的响应异常或超时异常,系统就会走降级流程,如果异常数量变少,系统就会自动恢复。

缺点和优点

为了使系统具备降级功能,需要撰写大量的代码,而降级代码往往比正常业务代码更难写,更容易出错,所以并不符合奥卡姆剃刀原则。在确定使用降级模式的前提下,工程师需要权衡这三种降级策略的利弊。大多数面向C端的系统倾向于采用效果降级和功能性降级策略,但是有些功能性模块(比如下单功能)是不能进行效果和功能性降级的,只能采用流量降级策略。对于不能接受降级后果的系统,必须要通过其他方式来提高系统的可用性。

总的来说,降级模式是一种设计安全准则,任何高可用性要求的服务,必须要按照降级模式的准则去设计。对于违背这条设计原则的系统,或早或晚,系统总会因为某些问题导致崩溃而降低可用性。不过,降级模式并非不需要成本,也不符合最小可用原则,所以对于处于MVP阶段的系统,或者对于可用性要求不高的系统,降级模式并非必须采纳的原则。

其他性能优化建议

对于无法采用系统性的模式方式讲解的性能优化手段,作者也给出一些总结性的建议:

  1. 删除无用代码有时候可以解决性能问题,例如:有些代码已经不再被调用但是可能被初始化,甚至占有大量内存;有些代码虽然在调用但是对于业务而言已经无用,这种调用占用CPU资源。
  2. 避免跨机房调用,跨机房调用经常成为系统的性能瓶颈,特别是那些伪batch调用(在使用者看起来是一次性调用,但是内部实现采用的是顺序单个调用模式)对系统性能影响往往非常巨大

总结

Christopher Alexander曾说过:”Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice” 。 尽管Christopher Alexander指的是建筑模式,软件设计模式适用,基于同样的原因,性能优化模式也适用。每个性能优化模式描述的都是工程师们日常工作中经常出现的问题,一个性能优化模式可以解决确定场景下的某一类型的问题。所以要理解一个性能优化模式不仅仅要了解性能模式的所能解决的问题以及解决手段,还需要清楚该问题所发生的场景和需要付出的代价。

最后,本文所描述的性能优化模式只是作者的工作经验总结,都是为了解决由以下三种情况所造成的性能问题:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据,但是这些远非该领域里面的所有模式。对于文章中提到的其他性能优化建议,以及现在和将来可能碰到的性能问题,作者还会不断抽象,在未来总结出更多的模式。性能问题涉及领域非常广泛,而模式是一个非常好的讲解性能问题以及解决方案的方式,作者有理由相信,无论是在作者所从事的工作领域里面还是在其他的领域里面,新的性能优化模式会不断涌现。希望通过本文的讲述,对碰到同样问题的工程师们有所帮助,同时也抛砖引玉,期待出现更多的基于模式方式讲解性能优化的文章。

参考文献:
[1] Chang F, Dean J, Ghemawat S, et al. Bigtable: A Distributed Storage System for Structured Data
[2] Gamma E, Helm R, Johnson R, et al. Design Patterns-Elements of Reusable Object-Oriented Software. Machinery Industry, 2003
[3] Motlik F. Monolithic Core Versus Full Microservice Architecture
[4] Monolithic Design WikiWikiWeb.
[5] Bovet D P, Cesati M. Understanding the Linux Kernel. 3rd ed. O’Reilly Media, 2005.
[6] Bloch J. Effective Java. 2nd ed. Addison-Wesley, 2008.
[7] Alexander C, Ishikawa S, Silverstein M. A Pattern Language: Towns, Buildings, Construction. Oxford University Press, 1977.

37个最好的学习新东西的网站(译)

忘掉那些在学校或者课堂上学习反而收获甚微的方式吧。这些网站或者APP涵盖科学、艺术和技术。它们会教你一些特别的东东,像用node.js构建APP, 而且大部分是免费的。这不会强制你掌握一个新技能,却能扩展你的知识,甚至促进你的职业。你可以在你喜欢的地方学习或者是你自己的舒服的家里。真的不能再简单了。你还等什么呢?

1、在线课程

edX – 世界上最好的大学的在线课程
Coursera  – 获取世界上最好的课程,在线,免费

Coursmos – 任何时间获取你想要的微课程,任何设备可看
Highbrow – 订阅小的每日课程到收件箱
Skillshare – 激发你的创造力的在线课程和项目
Curious – 在线视频课程来提高你的技能
lynda.com – 学习技术、创造性和商业技能
CreativeLive – 获取世界顶级专家的创造性课程
Udemy – 在线学习现实世界的有用技能

2、学习编程

Codecademy-交互式学习编码,免费
Stuk.io-零基础学习编程
Udacity-获取可被业界承认的技能
Platzi-在线学习设计、市场推广、编码
Learnable-最好的方式学习web开发
Code Scool-亲自动手学习编程
Thinkful-一对一的辅导
Code.org-根据指南现在就开始学习
BaseRails-掌握Ruby on Rails和其他web技术
Treehouse-学习HTML,CSS ,iPhone apps和更多
One Month-一个月内学习编程并构建web应用
Dash-学习制作酷炫的网站

3、和数据打交道

DataCamp-R语言的指南和数据课程
DataQuest-浏览器里学习数据科学
DataMonkey-简单有趣的方式开发你的分析技能

4、学习新的语言

Duolingo-免费学习新语言
Lingvist-200小时内学习一门新的语言
Busuu-免费的语言学习社区
Memrise-用识字卡来学习词汇

5、扩展你的知识

TED-Ed-找到辅助的教育视频
Khan Academy-可交互的非常全面的图书馆
Guides.co-最大的在线指南的搜索
Squareknot-漂亮的指引,一步步的指南
Learnist-学习更专业的内容通过web、纸质、视频
Prismatic-学习社会推荐的一些有趣的内容

6、其它红利

Chesscademy-免费学习国际象棋
Pianu-新的方式学习钢琴,可交互
Yousician-数字时代下你的个人吉他教程

阅读原文

推荐阅读

如何开发一个App(Android)

前言

本篇博客从开发的角度来介绍如何开发一个Android App,需要说明一点是,这里只是提供一个如何开发一个app的思路,并不会介绍很多技术上的细节,从整个大局去把握如何去构思一个app的开发,让你对独立开发一款app的时候有个理解,如果有说的不对的地方,欢迎大家拍砖留言讨论。

开发环境

Android应用层使用的开发语言是Java,自然需要用到Java运行环境,无论你在Window是系统还是Mac系统都需要安装JDK,并且配置它的环境变量,不懂什么叫环境变量的或者不知道怎么配的,请利用好自己的百度技能自行学习。

JDK下载

开发工具

开发工具,Android因为历史原因,前面很长一段时间使用的是Eclipse,我们要开发Android应用的时候需要另外安装Google为它开发的插件ADT,Eclipse这个开源软件很优秀,也很专业,但仅仅只是使用它来开发Android就显得有点鸡肋了,如果没有对Eclipse进行优化的话,会很卡很卡,后面Google实在不愿意寄人篱下,就专门为我们开发者开发了目前很受广大开发者推崇的Android Studio,现在2.0预览版也出来啦,大伙赶紧去下吧。

Android studio

模拟器

Android自带的模拟器一直广受诟病,实在是太卡太慢了,对比人家IOS模拟器是那么的流畅,Android开发者直喊好苦逼啊,不过还好后面出了第三方比原生流畅n倍的模拟器Genymotion,大家可以去下一个个人版的够你平时开发测试用了。最好的办法还是使用真机测试,毕竟真机才是真实的环境。

Genymotion

Android核心基础

前面是准备工作,想开发一款Android app,你最起码要掌握Android的核心基础知识,针对初学Android的童鞋,想一口吃掉一个胖子是不可能的,还是得把基础打牢之后才能完成独立的开发,Android入门不难,学完基础你只是具备开发app的基本条件,想做一个优秀的app还有很长一段路要走,经验是靠不断实践堆出来的,找一套系统的课程去认真学习一下,在线教育的资源很多,比如慕课网,极客学院都有很多不错的学习资源,童鞋自己择优来学习即可。

推广:http://edu.csdn.net/course/detail/545(笔者的课程)

产品开发流程

正常的互联网开发app的流程大致如下:
– 产品规划,定产品方向
– 需求调研,产出需求文档
– 需求评审,修订需求文档
– 产品狗画app线框图提供给射鸡师
– 射鸡师根据线框图设计视觉稿
– 程序猿根据视觉稿搭建UI框架
– 程序猿根据需求文档开发功能
– 测试媛编写测试用例,根据排期进行测试
– 程序猿修复回归测试反馈的bug,提交beta版
– 测试通过,提交给运营喵发布到渠道上线

上面是笔者的经验总结,可能有不太准确的地方,但大致整个流程是这样,我们开发者要清楚自己在整个产品中充当的角色,明白自己的工作职责即可。

快速搭建项目

Android比没有想象那么难,只是刚开始要学习的东西多,一下子消化不了才会比较茫然,笔者写这篇博客就是想帮助大家整理一下开发思路。

快速搭建项目也算是一项技能,而这项技能只能由你自己来完善,刚开始做开发的时候一定会有很多重复性的工作,如果没有意识去提高自己的开发效率的话,可能你的十年工作经验代表的只是一年的经验用了十年而已。

那要怎么做,笔者提供一个自己总结的,仅供参考:
– 定开发规范
– 搭建UI框架
– 选用开发库集成(或自造轮子)
– 第三方服务集成(视情况而定)

定开发规范

一个项目一般不会只有一个人独立开发,就算是独立开发,我们也应该定一下开发规范,一般会有什么样的规范?
– 命名规范
– 代码规范
– 代码管理规范

命名规范

命名规范包括:
– 项目命名
– 包命名
– 类命名、成员变量命名
– 资源文件命名
我们做每一个产品都会有相应的命名,不要用中文来命名,最好用能表达中文意思的英文来表示,例如CSDN客户端,可以用CSDNClient作为命名,我们创建项目的时候可以以它作为Application name。
可以看看以前写的这篇文章:
http://blog.csdn.net/wwj_748/article/details/42347283

代码规范

代码规范视语言而定,开发android使用的是Java,自然要按照Java的编码规范进行编码,比如命名采用驼峰命名法,编码的时候也要符合Android的开发规范,比如UI线程不做任何耗时的操作,像网络请求、数据库操作都需要放到子线程中去做,只有UI的刷新才在UI线程中做,像这些规范,可以根据自身在项目遇到的问题来定,定规范的好处就是减少踩坑的几率,提高开发的质量。

代码管理

对于一个经常更新迭代的产品,不可能由头到尾不变,这个时候我们需要对代码进行分支管理,最好使用git代码仓库对代码进行管理,作为一个合格的开发者连git都不用实在说不过去,还用svn的童鞋赶紧放弃它投入git的怀抱,它的好处是不言而喻的,自己去体会。

搭建UI框架

搭建UI框架需要我们根据产品的导航模式来设计,市场上常用的导航模式有如下图几种:

app导航

我们的app如果不出意外一定是其中的一种导航模式,一般线框图出来我们就应该知道即将要开发的app长什么样子,开发者不必等视觉稿和素材出来才开始动工,我们先大致搭个架子,等视觉稿出来之后我们再做调整。

选用开发库

一般我们app涉及到的库会有:
– UI框架(比如下拉刷新PullToRefresh、侧滑菜单Slidingmenu)
– 网络请求库(比如okhtttp、AndroidAsyncHttp、Volley)
– 数据操作库(比如GreenDao、Ormlite)
– 图片缓存框架(比如Universal-Imageloader)
– 数据解析库(比如Gson)

之所以要选用这些库,肯定是为了避免重复造轮子,在开发效率的角度来说,选用优秀的开源库能大大缩短开发周期和提高开发效率,但从个人提升角度来看的话,我们可能就成了一个只会用API的程序猿了,如果想提升的话,造轮子或者分析这些优秀的源代码是一个不错的途径。

第三方服务集成

我们开发app的时候,肯定会遇到一些需求,比如推送的需求、自动升级、数据统计、社会化分享、用户反馈等等,然而对于一个刚起步的企业或者个人开发者的话,全都要自己去开发的话,那岂不是累死,像推送这种有一定的技术门槛,能做好都能成立一家公司了,所以选用一些第三方服务是一个可选之举。如果说你以后做大了,用第三方怕不好控制,那就自己做呗,有钱任性招兵买马就自己做,谁叫咱有钱呢。


前面这些东西开发一个app够了,开发出来能不能用还得有靠谱的测试,有没有crash,操作流不流畅,体验好不好才会有用户去用。这里不从产品的角度去评判一个app的好与坏,程序员要考虑的是从代码层面、性能层面去让我们的app变得更好。

云测

我们开发完毕之后,需要给测试工程师进行基本的功能需求测试,他们传统的做法就是根据事先写好的测试用例来做回归测试,再把测试出来的bug反馈给工程师,工程师再去修bug,但这样实在是太不靠谱了,有时候我们太在意功能而忽略了一些更重要的东西,那就是体验,给用户最直接的感受就是你这个app够不够惊艳,够不够流畅,用户可能根本就不在乎你这个功能做的有多牛逼。所以我们更应该从非功能性方向去做测试,我们的目的是让用户用的爽,而不是加一些乱七八糟的功能。那怎么测非功能性的一些因素,这里就要提到『云测』这个东西,因为现在设备太多了,如果公司要买一堆设备来做测试,那得多少成本,况且设备更新得太快,你根本就跟不上,所以就有了云测这个东西,它是一个云测试平台服务,提供了一大批主流机型,我们就直接省去购买设备的成本,还能得到完善的测试报告。

再来说一下它的好处:
– 终端云,省去测试设备购买租赁成本
– 高效率 节省测试人员成本及时间
– 包含兼容性测试、性能测试、功能测试
– 操作简单、详细测试报告生成

这么多好处,你在缺少测试工程师的时候,不去尝试那实在说不过去。

打包上线

前面的开发环节、测试环节都没问题之后,你离实现一个app的完整开发就不远了,正常的互联网公司,会把签名打包的apk给到运营,交给他们去写文案,上传到应用渠道,让渠道给我们去首发和推广。如果是个人开发者,这些工作就得我们自己做了。

总结

本篇博客从整个app开发流程上去给大家梳理了一遍,没有讲太多技术上的东西,但理解app流程对自己把握产品很有益处,虽然我们只是一个小小的开发者,但如果你有追求,哪天轮到你去负责一个产品的时候,你就应该对开发的每一个环节都了如指掌,因为出了问题,你也知道怎么针对性的去解决。笔者虽然只是一个小小的开发者,但也乐于思考,很多时候不太愿意被别人牵着鼻子走,因为我们的人生就应该把握在自己手里。

【年度案例】小米抢购限流峰值系统「大秒」架构解密

马利超

小米科技的系统研发与大数据工程师,2013年毕业于大连理工大学,毕业后有幸加入小米抢购系统团队,并参与了小米抢购系统开发、重构与调优。其人热爱技术,对分布式系统架构、高并发峰值系统、大数据领域、反作弊领域、搜索/广告/推荐系统有浓厚的兴趣。

上文介绍了【年度案例】小米抢购限流峰值系统架构历年演进历程 ,本文主要介绍最新版「大秒」系统架构。

整合的抢购限流峰值系统——「大秒」

2014年初,公司决定举办一场“米粉节”活动,全天6轮活动,多个国家、多款爆品同时参与抢购。业务场景将变得更加复杂,当天的并发压力也会有一个量级的提升,原有的抢购系统已经不能适应如此复杂的业务场景了。
为此,小米网技术团队基于对 golang 应对高并发、大规模分布式系统能力的肯定,完全基于 golang,重新设计了抢购系统,也就是我们目前使用的抢购限流峰值系统——“大秒”。

在整个系统设计的之初,我们充分考虑了

  1. 灵活性及可运营性;
  2. 可运维性及可伸缩性;
  3. 限流与抢购放号的精准性;

从大秒第一天诞生到演化至今有很多次重构与优化,但一直沿用了设计之初的结构,接下来我们一起了解下小米网抢购限流峰值系统当前的架构以及填过的一些坑。

大秒系统的架构设计

大秒系统主要由如下几个模块构成

  1. 限流集群 HTTP 服务
  2. 放号策略集群 Middle 服务
  3. 监控数据中心 Dcacenter
  4. 监控管理体系 Master
  5. 准实时防刷模块 antiblack
  6. 基础存储与日志队列服务: Redis 集群、Kafka 集群等

整个大秒体系中大秒前端模块 (HTTP/middle/antiblack) 和监控数据中心使用 golang 开发,大秒监控管理体系使用 Python + golang 开发。

大秒的前端架构设计

大秒前端的架构设计从三个系统展开

  1. 限流集群 HTTP 服务
  2. 策略集群 Middle 服务
  3. 准实时反作弊 antiblack 服务

1、限流集群 HTTP 服务

抢购高峰时,通常会有几百万的用户同时请求,瞬时流量非常大,HTTP 集群顶在最前线,接受用户的请求,将合法的请求发送的处理队列,处理队列设置一定的长度限制,通常情况下,抢购用户数与销售商品的比例在100:1,甚至更高,为了避免系统不被冲垮,保障绝大多数用户的体验,我们认为流量是部分可丢失的,当处理队列满时,丢弃入队请求;

虽然设计上过载流量是部分可丢弃的,但是策略层处理能力是非常 power 的,即便是需要丢弃流量,也是按流量的恶意程度,逐级丢弃的,正常用户购买请求不受影响。

我们使用基于规则的识别、离线画像信息、机器学习逻辑回归等方法,识别恶意用户,在系统高负载的情况下,这部分请求可以优先阻击其发送到策略层,优先处理正常用户的请求,保障用户体验过。

HTTP集群中不同节点之间的所持用的状态数据是一致的,处理逻辑也是一致的,所以整个集群中的任何一个节点挂掉,在前端负载均衡能力下,服务的准确性与一致性不受任何影响。

2、策略集群 Middle 服务
HTTP 模块将满足条件用户的请求按照 uid 哈希的规则,转发到 Middle 集群中相应的节点,Middle 集群根据商品放号策略判断 (uid:sku:time) 组合是否可以分配购买资格,并返回给相应的 HTTP 服务;

使用 Middle 服务本地内存维护用户的购买记录信息,支持各种购买规则,比如:单次活动不限购买数量,单次活动仅限购买一款商品,单次活动每款商品仅限购买一次。

我们将 Middle 的放号逻辑抽象成一个有限状态机,由商品的放号策略配置阈值来触发放号状态转换,整个配置由 Master 节点统一管理与调度。

为了提升整个系统的处理能力,我们将用户状态数据局部化,单用户(uid)的所有相关信息全部路由到一台 Middle 节点上处理。

但是有一点风险是,Middle 集群中服务可能会出现活动过程中挂掉的风险,在抢购场景下,商品基本上是瞬时卖完,为了保障系统的处理能力,我们主要从代码层面做优化,review 代码逻辑,保证服务应对异常的处理能力。

虽然理论上存在风险,但是在实际工程中,经历过几百次活动,还没出现 Middle 节点挂掉的情况。

3、准实时防刷 antiblack 服务

基于日志流的防刷架构,在每台 HTTP 节点上部署日志收集 Agent,使用高吞吐量的 Kafka 做日志转储队列,antiblack 模块实时分析用户请求日志,基于 IP 粒度、Uid 粒度等做防刷。

虽然此处将 antiblack 模块定义为准实时防刷模块,但是作弊信息识别的延迟时长在 1 分钟之内,其中主要的时延发生在日志的转储过程中。

大秒的监控管理体系

1、监控数据中心 dcacenter

监控数据中心数据种类

(1) 业务级数据:过大秒的商品配置数据与实时状态数据,当前活动的配置与状态数据等;
(2) 系统级数据: 大秒前端服务集群通信地址配置,限流队列初始长度配置,系统服务资源占用情况,包括:CPU、MEM、连接数等;

数据采集方式

同时使用push和pull模式采集业务级监控数据和系统级监控数据,业务级数据越实时越好,做到1秒采集处理,3秒可视化;

对于 HTTP 节点和 Middle 节点采用pull的模式拉去系统监控数据和业务监控数据,优点如下

(1) 灵活性高
由数据中心控制监控数据采集的粒度,在数据中心处理能力既定的情况下,可以根据前端集群的伸缩规模,灵活的调整数据采集的粒度,比如米粉节时,大秒前端集群扩容至过百台,管理的过大秒商品的数量在400个左右,业务级监控数据量很大,此时监控数据采集时间间隔很容易降配至 2s。

对于除Http服务和Middle服务之外的服务集群,如:redis,管理平台各个模块等可以使用监控数据采集agent,将采集到的数据周期性的push到redis队列,dcacenter采集协程实时的从redis队列中拉去消息,对于基础服务以及python实现的服务,增加了监控数据采集灵活性。

(2) 增强服务的可靠性与伸缩性

大秒在设计之初采用push的方式,在每台前端机器上部署一个数据采集agent,agent和大秒前端服务同时alive,才代表抢购系统健康运行。这样即增加了系统的不稳定因素,由不利于系统的伸缩,将监控数据采集逻辑内置到前端golang程序中,提供tcp管理端口,在数据中心使用pull方式采集数据,很好的解决了这个问题。减少了服务的数量,增强了整个系统的可靠性与伸缩性。

数据ETL与数据缓存

dcacenter同时负责将采集到的业务级数据及系统级监控数据,实时清洗,提取,转换,结构化,并将结构化的数据存储在自身内存中,定制通信协议(golang实现类redis通信协议),作为一个数据中心,对整个管理体系Master及其他系统提供实时数据支持。

将dcacenter直接作为数据中心,主要是出于数据的实时性考虑,省去中间转储环节,上层可视化系统、自动化活动控制系统、规则引擎系统等可以第一时间获得前端实时的销售状态数据及服务的状态数据。

2、监控管理中心 Master

监控管理中心的主要模块如下。

a.仓储库存同步服务StockKeeper
同步商品的仓储系统中的实时库存到秒杀系统,大秒系统拥有双库存保障,一个是实时仓储库存,一个是虚拟库存也就是资格号,在抢购场景下只有当两个库存都有货时,才能正常销售。

b.商品策略控制器PolicyKeeper
基于相应的策略触发器(时间区间与库存区间),当策略触发时,比如12点整,抢购开始,为相应的商品配置策略,并向大秒前端广播商品配置变更命令,在通信基础模块的保障下,整个过程秒级内完成。

c.活动自动化控制ActKeeper
基于监控数据中心获取大秒前端的实时销售数据,自动化的控制活动中的各个状态,活动开始前逐层打开开关,活动开始时打开最后开关,活动过程中维护活动的售罄状态,活动结束后初始化,整个抢购活动的过程无需人工介入;

d.数据可视化
从监控数据中心提取实时的结构化系统级监控数据和业务级监控数据,将活动过程中的详细数据实时可视化到管理页面上,让运营与销售以及大秒管理员能够及时了解当前活动状态,并人工干预活动;

e.监控规则引擎
监控规则引擎建立在监控数据中心之上,根据结构化监控数据判断当前整个抢购系统的状态,及时报警,以及半自动化控制。

f.其他
大秒管理端管理大秒前端所有的数据、配置以及状态,Master体系提供了详细的管理工具与自动化服务。如果清理大秒前端Middle服务中的用户购买信息等。

3、大秒配置管理数据流

整个抢购系统由 Master 体系中各个服务做统一的控制的,Master 控制商品状态及配置数据的变更,控制当前活动的状态,控制商品放号的策略等。

为了保证时效性,商品、活动、系统等配置状态的变更都需要将变更命令广播前端集群,这期间发生了大量的分布式系统间通信,为了保障命令及时下行,我们提取出了命令转发服务:MdwRouter,用于广播控制命令到大秒前端集群。该服务模块维护了到大秒前端长连接,接收 Master 下发的控制命令,并瞬时广播,保障了整个控制流的处理能力。

举个例子,2015 年米粉节,我们单机房大秒集群的规模在过百台级别,假设为 100 台,管理的独立的商品id的数量在 400 个左右,在这种量级的活动下,商品的放行策略是批量管理的,比如我们根据后端交易系统的压力反馈,调整所有商品的放行速度,这时候需要广播的命令条数在: 100*400=40000 级别,Mdwrouter 很好的保障了系统命令下行的速度,秒级完成命令下行。

小米抢购技术架构

1、小米抢购服务闭环设计

小米网抢购系统服务见上图

  1. bigtap体系中大秒前端服务负责抢购时限流放号,并控制放号策略以及维护用户在本地缓存中的购买记录。
  2. cart服务验证token的有效性,并向counter服务发起销量验证请求;
  3. counter服务是整个抢购系统最终的计数器, 海量的请求在bigtap服务的作用下已经被限制在可以承受的压力范围内,并且复杂的放号策略已经在大秒Middle服务中实现,counter只负责最终的计数即可。counter服务采用redis记录相应商品的放号情况,根据预设的销量,判断当前请求加购物车商品是否有库存余量,并维护商品销量;
  4. bigtap体系中的dcacenter服务实时采集商品销量,Master中活动自动化控制服务依据商品销量判断当前商品是否售罄,售罄则通过设置商品的售罄状态,并通知大秒前端;

2、2015年米粉节介绍

从上述整个服务闭环设计可以看出,大秒的功能完全可以抽象成限流系统,只有在处理抢购活动时,数据的管理与一致性要求才使整个系统变得复杂。

2015年米粉节,我们完全使用大秒的限流功能,不限用户的购买数量,很便捷的将系统部署在两个机房,一个物理机房,一个公有云集群,两者同时服务,大秒系统作为整个商城的最前端,能够根据后端服务的压力状态,瞬时调整整个集群放行流量大小,非常好的保障了整个米粉节的正常举行。
在上述文章中,已经介绍了一些服务设计的出发点,每一次优化的背后,都至少有一次惨痛的经历。

大秒系统架构的几点经验总结

1、Golang GC 优化方法

我们从 golang 1.2 版本开始在线上抢购系统中大规模使用,最初上线的 TC 限流集群在抢购的过程中通过过载重启的方式瘸腿前行。

在当前的大秒系统中,对于限流集群主要是 goroutine 资源、HTTP 协议数据结构、TCP 连接读写缓冲区等频繁动态开销,造成内存 GC 压力大,在现有 GC 能力下,我们对 GC 优化从以下几个方面考虑

  1. 减少垃圾产生:降低数据结构或者缓冲区的开销;
  2. 手动管理内存:使用内存池,手动管理内存;
  3. 脏数据尽快释放,增大空闲内存比。

我们使用了以下 3 种 golang GC 优化方法

1)定制 golang HTTP 包

调整 HTTP 协议 conn 数据结构默认分配读写缓冲区的大小,以及手动维护读写缓存池,减少动态开辟内存的次数,降低 GC 压力。

在 Go 语言原生的 HTTP 包中会为每个请求默认分配 8KB 的缓冲区,读、写缓冲区各 4K。而在我们的服务场景中只有 GET 请求,服务需要的信息都包含在 HTTP header 中,并没有 body,实际上不需要如此大的内存进行存储,所以我们调小了读写缓冲区,将读缓冲区调小到 1K,写缓冲区调小到 32B,golang 的 bufio 在写缓冲区较小时,会直接写出。
从 golang 1.3 开始,HTTP 原生的包中已经使用了sync.Pool 维护读写缓存池,但是 sync.Pool 中的数据会被自动的回收,同样会小量的增加 GC 压力,我们此处自己维护缓存池来减少垃圾回收。

2)加快资源释放
原生的 HTTP 包默认使用 keep-alive 的方式,小米抢购场景下,恶意流量占用了大量的连接,我们通过主动设置 response header 的 connection 为 close 来主动关闭恶意连接,加快 goroutine 资源的释放。

3)升级版本
跟进使用 golang 最新的版本,golang 后续的每个版本都有针对 GC 能力的调整。

得益于开源技术力量,以及大秒系统在 GC 优化上的努力,以及系统层的调优,我们的 HTTP 限流层已经可以余量前行。

从上图可以看出,得益于 GC 的优化,2015 年米粉节,每轮抢购,HTTP 服务的内存不会有特别大的抖动。

2、HTTP 服务器内存调优之操作系统参数调整

我们的服务场景下绝大多数的请求数都是恶意请求,恶意请求通常都是短连接请求,大量的短连接会处于 timewait 状态,几分钟之后才会释放,这样会占用大量的资源,通过调整内核参数,尽快释放或者重用 timewait 状态的连接,减少资源的开销。
具体参数调整如下:

net.ipv4.tcp_tw_recycle = 1 (打开TIME-WAIT sockets快速回收)
net.ipv4.tcp_tw_reuse = 1 (允许TIME-WAIT sockets复用)
net.ipv4.tcp_max_tw_buckets=10000  (降低系统连接数和资源占用,默认为18w)

高并发场景下,操作系统层网络模块参数的调整,会起到事半功倍的效果。

3、没有通信就谈不上分布式系统

整个大秒系统模块之间面临的通信要求是非常苛刻的,Master 节点与 HTTP、Middle 节点要频繁的广播控制命令,dcacenter要实时的收集 HTTP、Middle 节点的监控管理数据,HTTP 要将用户的购买请求路由到 Middle 节点之间,Middle 节点要返回给相应的 HTTP 节点放号信息;

我们基于 TCP 定制了简单、高效的通信协议,对于 HTTP 层和 Middle 层通信,通信模块能够合并用户请求,减少通信开销,保障整个大秒系统的高效通信,增加服务的处理能力。

4、服务闭环设计

从上述抢购的服务闭环架构中可以看出,整个抢购流程处理bigtap系统之外,还有 cart 服务,中心 counter 服务,这三者与 bigtap 系统构成了一个数据流的闭环,但是在大秒最初的设计中,是没有 counter 服务的,Middle层策略集群在放号的同时,又作为计数服务存在,但是整个抢购流程却是以商品加入购物车代表最终的抢购成功,这在设计上有一个漏洞,假如 bigtap 计数了,但是token 并没有请求加购物车成功,这是不合理的。为了保证整个系统的准确性,我们增加了计数器服务,计数操作发生在加购物车下游,bigtap 在从计数中心取出商品实时销量,由此,构成一个服务闭环设计。在提升了系统的准确性,同时也保证了用户体验。

5、技术的选择要可控

我们一开始选择使用 ZooKeeper 存放商品的配置信息,在抢购活动的过程伴随着大量的配置变更操作,ZooKeeper 的 watch 机制不适合用于频繁写的场景,造成消息丢失,大秒前端集群状态与配置不一致。

后来,我们将所有的配置信息存放在 Redis 中,基于通信模块,在发生配置变更时,伴随着一次配置项变更的广播通知,大秒前端根据相应的通知命令,拉取 Redis 中相应的配置信息,变更内存中配置及状态。

大秒的几点设计原则

  1. 分治是解决复杂问题的通则;我们从第一代抢购系统演进到当前的大秒系统,衍生出了很多服务,每个服务的产生都是为了专门解决一个问题,分离整个复杂系统,针对每个服务需要解决的问题,各个击破,重点优化。由此,才保障了秒杀体系整体性能、可靠性的提升;
  2. 服务化设计;系统解耦,增强系统的伸缩性与可靠性;
  3. 无状态设计,增强系统的伸缩性,提升集群整体处理能力;
  4. 状态数据局部化,相对于数据中心化,提升集群整体处理能力。
  5. 中心化监控管理,热备部署,既保证了服务的高可用性,又能够提升开发和管理效率。随着集群规模的增大以及管理数据的增多,分离管理信息到不同的数据管理节点,实现管理能力的扩容。通常情况下,中小型分布式系统,单机管理能力即可满足。
  6. 避免过度设计,过早的优化;小步快跑,频繁迭代。
  7. 没有华丽的技术,把细小的点做好,不回避问题,特别是在高并发系统中,一个细小的问题,都可以引发整个服务雪崩。
( generated by haroopad )

Q&A

1、实时仓库怎么避免超卖?

我们的抢购系统以加入购物车代表购买成功,因为用户要买配件等,库存是由计数器控制的,先限流,在计数,在可控的并发量情况下,不会出现超卖。

2、有了放号系统计算放号规则,为什么还需要一个外围的 counter?

主要是 bigtap 到 cart 的环节 token 有丢失,在 cart 之后再加一个计数器,保障销量,bigtap 再读取计数器的数据控制前端商品销售状态,整个延迟不超 3s。

3、HTTP 集群通过 uuid hash 到 Middle,如果目标 Middle 已经死掉怎么应对?

这个问题在文章中有强调,在我们的场景下,商品迅速卖完,这块没有做高可用,只是从代码层面做 review,完善异常处理机制,并且通常情况下,middle 负载不是特别高,几百次活动下来,还没出现过挂掉情况。

4、防刷系统是离线计算的吗,还是有在线识别的策略?

基于日志,准实时,因为请求量比较大,专门搭了一套 Kafka 服务转储日志,基于 golang 开发 logcollect 与 antiblack 模块,可以达到很高的处理性能。

5、请问如何模拟大量请求做测试?

我们遇到的情况是,由于压测机单机端口限制造成早期不好测试,我们这边压测团队基于开源模块开发了能够模拟虚拟IP的模块,打破了单机端口的限制。

6、即使广播和 Redis 拉取商品配置信息,仍有可能配置信息不一致如何解决?

这个主要是商品的配置和状态信息,不涉及到强一致性要求的场景,我们这样可以在秒级达到最终一致性。

想更多了解小米抢购的架构,可阅读抢购系统架构演进介绍 【年度案例】小米抢购限流峰值系统架构历年演进历程

京东咚咚架构演进

咚咚是什么?咚咚之于京东相当于旺旺之于淘宝,它们都是服务于买家和卖家的沟通。 自从京东开始为第三方卖家提供入驻平台服务后,咚咚也就随之诞生了。 我们首先看看它诞生之初是什么样的。

1.0 诞生(2010 – 2011)

为了业务的快速上线,1.0 版本的技术架构实现是非常直接且简单粗暴的。 如何简单粗暴法?请看架构图,如下。

1.0 的功能十分简单,实现了一个 IM 的基本功能,接入、互通消息和状态。 另外还有客服功能,就是顾客接入咨询时的客服分配,按轮询方式把顾客分配给在线的客服接待。 用开源 Mina 框架实现了 TCP 的长连接接入,用 Tomcat Comet 机制实现了 HTTP 的长轮询服务。 而消息投递的实现是一端发送的消息临时存放在 Redis 中,另一端拉取的生产消费模型。

这个模型的做法导致需要以一种高频率的方式来轮询 Redis 遍历属于自己连接的关联会话消息。 这个模型很简单,简单包括多个层面的意思:理解起来简单;开发起来简单;部署起来也简单。 只需要一个 Tomcat 应用依赖一个共享的 Redis,简单的实现核心业务功能,并支持业务快速上线。

但这个简单的模型也有些严重的缺陷,主要是效率和扩展问题。 轮询的频率间隔大小基本决定了消息的延时,轮询越快延时越低,但轮询越快消耗也越高。 这个模型实际上是一个高功耗低效能的模型,因为不活跃的连接在那做高频率的无意义轮询。 高频有多高呢,基本在 100 ms 以内,你不能让轮询太慢,比如超过 2 秒轮一次,人就会在聊天过程中感受到明显的会话延迟。 随着在线人数增加,轮询的耗时也线性增长,因此这个模型导致了扩展能力和承载能力都不好,一定会随着在线人数的增长碰到性能瓶颈。

1.0 的时代背景正是京东技术平台从 .NET 向 Java 转型的年代,我也正是在这期间加入京东并参与了京东主站技术转型架构升级的过程。 之后开始接手了京东咚咚,并持续完善这个产品,进行了三次技术架构演进。

2.0 成长(2012)

我们刚接手时 1.0 已在线上运行并支持京东 POP(开放平台)业务,之后京东打算组建自营在线客服团队并落地在成都。 不管是自营还是 POP 客服咨询业务当时都起步不久,1.0 架构中的性能和效率缺陷问题还没有达到引爆的业务量级。 而自营客服当时还处于起步阶段,客服人数不足,服务能力不够,顾客咨询量远远超过客服的服务能力。 超出服务能力的顾客咨询,当时我们的系统统一返回提示客服繁忙,请稍后咨询。 这种状况导致高峰期大量顾客无论怎么刷新请求,都很可能无法接入客服,体验很差。 所以 2.0 重点放在了业务功能体验的提升上,如下图所示。


针对无法及时提供服务的顾客,可以排队或者留言。 针对纯文字沟通,提供了文件和图片等更丰富的表达方式。 另外支持了客服转接和快捷回复等方式来提升客服的接待效率。 总之,整个 2.0 就是围绕提升客服效率和用户体验。 而我们担心的效率问题在 2.0 高速发展业务的时期还没有出现,但业务量正在逐渐积累,我们知道它快要爆了。 到 2012 年末,度过双十一后开始了 3.0 的一次重大架构升级。

3.0 爆发(2013 – 2014)

经历了 2.0 时代一整年的业务高速发展,实际上代码规模膨胀的很快。 与代码一块膨胀的还有团队,从最初的 4 个人到近 30 人。 团队大了后,一个系统多人开发,开发人员层次不一,规范难统一,系统模块耦合重,改动沟通和依赖多,上线风险难以控制。 一个单独 tomcat 应用多实例部署模型终于走到头了,这个版本架构升级的主题就是服务化。

服务化的第一个问题如何把一个大的应用系统切分成子服务系统。 当时的背景是京东的部署还在半自动化年代,自动部署系统刚起步,子服务系统若按业务划分太细太多,部署工作量很大且难管理。 所以当时我们不是按业务功能分区服务的,而是按业务重要性级别划分了 0、1、2 三个级别不同的子业务服务系统。 另外就是独立了一组接入服务,针对不同渠道和通信方式的接入端,见下图。


更细化的应用服务和架构分层方式可见下图。

这次大的架构升级,主要考虑了三个方面:稳定性、效率和容量。 做了下面这些事情:

  1. 业务分级、核心、非核心业务隔离
  2. 多机房部署,流量分流、容灾冗余、峰值应对冗余
  3. 读库多源,失败自动转移
  4. 写库主备,短暂有损服务容忍下的快速切换
  5. 外部接口,失败转移或快速断路
  6. Redis 主备,失败转移
  7. 大表迁移,MongoDB 取代 MySQL 存储消息记录
  8. 改进消息投递模型

前 6 条基本属于考虑系统稳定性、可用性方面的改进升级。 这一块属于陆续迭代完成的,承载很多失败转移的配置和控制功能在上面图中是由管控中心提供的。 第 7 条主要是随着业务量的上升,单日消息量越来越大后,使用了 MongoDB 来单独存储量最大的聊天记录。 第 8 条是针对 1.0 版本消息轮询效率低的改进,改进后的投递方式如下图所示:

不再是轮询了,而是让终端每次建立连接后注册接入点位置,消息投递前定位连接所在接入点位置再推送过去。 这样投递效率就是恒定的了,而且很容易扩展,在线人数越多则连接数越多,只需要扩展接入点即可。 其实,这个模型依然还有些小问题,主要出在离线消息的处理上,可以先思考下,我们最后再讲。

3.0 经过了两年的迭代式升级,单纯从业务量上来说还可以继续支撑很长时间的增长。 但实际上到 2014 年底我们面对的不再是业务量的问题,而是业务模式的变化。 这直接导致了一个全新时代的到来。

4.0 涅槃(2015 至今 )

2014 年京东的组织架构发生了很大变化,从一个公司变成了一个集团,下设多个子公司。 原来的商城成为了其中一个子公司,新成立的子公司包括京东金融、京东智能、京东到家、拍拍、海外事业部等。 各自业务范围不同,业务模式也不同,但不管什么业务总是需要客服服务。 如何复用原来为商城量身订做的咚咚客服系统并支持其他子公司业务快速接入成为我们新的课题。

最早要求接入的是拍拍网,它是从腾讯收购的,所以是完全不同的账户和订单交易体系。 由于时间紧迫,我们把为商城订做的部分剥离,基于 3.0 架构对接拍拍又单独订做了一套,并独立部署,像下面这样。

虽然在业务要求的时间点前完成了上线,但这样做也带来了明显的问题:

  1. 复制工程,定制业务开发,多套源码维护成本高
  2. 独立部署,至少双机房主备外加一个灰度集群,资源浪费大

以前我们都是面向业务去架构系统,如今新的业务变化形势下我们开始考虑面向平台去架构,在统一平台上跑多套业务,统一源码,统一部署,统一维护。 把业务服务继续拆分,剥离出最基础的 IM 服务,IM 通用服务,客服通用服务,而针对不同的业务特殊需求做最小化的定制服务开发。 部署方式则以平台形式部署,不同的业务方的服务跑在同一个平台上,但数据互相隔离。 服务继续被拆分的更微粒化,形成了一组服务矩阵(见下图)。

而部署方式,只需要在双机房建立两套对等集群,并另外建一个较小的灰度发布集群即可,所有不同业务都运行在统一平台集群上,如下图。

更细粒度的服务意味着每个服务的开发更简单,代码量更小,依赖更少,隔离稳定性更高。 但更细粒度的服务也意味着更繁琐的运维监控管理,直到今年公司内部弹性私有云、缓存云、消息队列、部署、监控、日志等基础系统日趋完善, 使得实施这类细粒度划分的微服务架构成为可能,运维成本可控。 而从当初 1.0 的 1 种应用进程,到 3.0 的 6、7 种应用进程,再到 4.0 的 50+ 更细粒度的不同种应用进程。 每种进程再根据承载业务流量不同分配不同的实例数,真正的实例进程数会过千。 为了更好的监控和管理这些进程,为此专门定制了一套面向服务的运维管理系统,见下图。

统一服务运维提供了实用的内部工具和库来帮助开发更健壮的微服务。 包括中心配置管理,流量埋点监控,数据库和缓存访问,运行时隔离,如下图所示是一个运行隔离的图示:

细粒度的微服务做到了进程间隔离,严格的开发规范和工具库帮助实现了异步消息和异步 HTTP 来避免多个跨进程的同步长调用链。 进程内部通过切面方式引入了服务增强容器 Armor 来隔离线程, 并支持进程内的单独业务降级和同步转异步化执行。而所有这些工具和库服务都是为了两个目标:

  1. 让服务进程运行时状态可见
  2. 让服务进程运行时状态可被管理和改变

最后我们回到前文留下的一个悬念,就是关于消息投递模型的缺陷。 一开始我们在接入层检测到终端连接断开后,消息无法投递,再将消息缓存下来,等终端重连接上来再拉取离线消息。 这个模型在移动时代表现的很不好,因为移动网络的不稳定性,导致经常断链后重连。 而准确的检测网络连接断开是依赖一个网络超时的,导致检测可能不准确,引发消息假投递成功。 新的模型如下图所示,它不再依赖准确的网络连接检测,投递前待确认消息 id 被缓存,而消息体被持久存储。 等到终端接收确认返回后,该消息才算投妥,未确认的消息 id 再重新登陆后或重连接后作为离线消息推送。 这个模型不会产生消息假投妥导致的丢失,但可能导致消息重复,只需由客户终端按消息 id 去重即可。

京东咚咚诞生之初正是京东技术转型到 Java 之时,经历这些年的发展,取得了很大的进步。 从草根走向专业,从弱小走向规模,从分散走向统一,从杂乱走向规范。 本文主要重心放在了几年来咚咚架构演进的过程,技术架构单独拿出来看我认为没有绝对的好与不好, 技术架构总是要放在彼时的背景下来看,要考虑业务的时效价值、团队的规模和能力、环境基础设施等等方面。 架构演进的生命周期适时匹配好业务的生命周期,才可能发挥最好的效果。

100万并发连接服务器笔记之1M并发连接目标达成

第四个遇到的问题:tcp_mem

在服务端,连接达到一定数量,诸如50W时,有些隐藏很深的问题,就不断的抛出来。 通过查看dmesg命令查看,发现大量TCP: too many of orphaned sockets错误,也很正常,下面到了需要调整tcp socket参数的时候了。

第一个需要调整的是tcp_rmem,即TCP读取缓冲区,单位为字节,查看默认值

cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 4161536

默认值为87380 byte ≈ 86K,最小为4096 byte=4K,最大值为4064K。

第二个需要调整的是tcp_wmem,发送缓冲区,单位是字节,默认值

cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4161536

解释同上

第三个需要调整的tcp_mem,调整TCP的内存大小,其单位是页,1页等于4096字节。系统默认值:

cat /proc/sys/net/ipv4/tcp_mem
932448 1243264 1864896

tcp_mem(3个INTEGER变量):low, pressure, high

  • low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
  • pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
  • high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。

一般情况下这些值是在系统启动时根据系统内存数量计算得到的。 根据当前tcp_mem最大内存页面数是1864896,当内存为(1864896*4)/1024K=7284.75M时,系统将无法为新的socket连接分配内存,即TCP连接将被拒绝。

实际测试环境中,据观察大概在99万个连接左右的时候(零头不算),进程被杀死,触发out of socket memory错误(dmesg命令查看获得)。每一个连接大致占用7.5K内存(下面给出计算方式),大致可算的此时内存占用情况(990000 * 7.5 / 1024K = 7251M)。

这样和tcp_mem最大页面值数量比较吻合,因此此值也需要修改。

三个TCP调整语句为:

echo "net.ipv4.tcp_mem = 786432 2097152 3145728">> /etc/sysctl.conf
echo "net.ipv4.tcp_rmem = 4096 4096 16777216">> /etc/sysctl.conf
echo "net.ipv4.tcp_wmem = 4096 4096 16777216">> /etc/sysctl.conf

备注: 为了节省内存,设置tcp读、写缓冲区都为4K大小,tcp_mem三个值分别为3G 8G 16G,tcp_rmemtcp_wmem最大值也是16G。

目标达成

经过若干次的尝试,最终达到目标,1024000个持久连接。1024000数字是怎么得来的呢,两台物理机器各自发出64000个请求,两个配置为6G左右的centos测试端机器(绑定7个桥接或NAT连接)各自发出640007 = 448000。也就是 1024000 = (64000) + (64000) + (640007) + (64000*7), 共使用了16个网卡(物理网卡+虚拟网卡)。
终端输出

......
online user 1023990
online user 1023991
online user 1023992
online user 1023993
online user 1023994
online user 1023995
online user 1023996
online user 1023997
online user 1023998
online user 1023999
online user 1024000

在线用户目标达到1024000个!

服务器状态信息

服务启动时内存占用:

                 total       used       free     shared    buffers     cached
Mem:         10442        271      10171          0         22         78
-/+ buffers/cache:        171      10271
Swap:         8127          0       8127

系统达到1024000个连接后的内存情况(执行三次 free -m 命令,获取三次结果):

                 total       used       free     shared    buffers     cached
Mem:         10442       7781       2661          0         22         78
-/+ buffers/cache:       7680       2762
Swap:         8127          0       8127

total       used       free     shared    buffers     cached
Mem:         10442       7793       2649          0         22         78
-/+ buffers/cache:       7692       2750
Swap:         8127          0       8127

total       used       free     shared    buffers     cached
Mem:         10442       7804       2638          0         22         79
-/+ buffers/cache:       7702       2740
Swap:         8127          0       8127

这三次内存使用分别是7680,7692,7702,这次不取平均值,取一个中等偏上的值,定为7701M。那么程序接收1024000个连接,共消耗了 7701M-171M = 7530M内存, 7530M*1024K / 1024000 = 7.53K, 每一个连接消耗内存在为7.5K左右,这和在连接达到512000时所计算较为吻合。
虚拟机运行Centos内存占用,不太稳定,但一般相差不大,以上数值,仅供参考。

执行top -p 某刻输出信息:

    top – 17:23:17 up 18 min,  4 users,  load average: 0.33, 0.12, 0.11

    Tasks:   1 total,   1 running,   0 sleeping,   0 stopped,   0 zombie
Cpu(s):  0.2%us,  6.3%sy,  0.0%ni, 80.2%id,  0.0%wa,  4.5%hi,  8.8%si,  0.0%st
Mem:  10693580k total,  6479980k used,  4213600k free,    22916k buffers
Swap:  8323056k total,        0k used,  8323056k free,    80360k cached

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
2924 yongboy   20   0 82776  74m  508 R 51.3  0.7   3:53.95 server

执行vmstate:

vmstat
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r b swpd free buff cache si so bi bo in cs us sy id wa st
 0 0 0 2725572 23008 80360 0 0 21 2 1012 894 0 9 89 2 0 

获取当前socket连接状态统计信息:

cat /proc/net/sockstat
sockets: used 1024380
TCP: inuse 1024009 orphan 0 tw 0 alloc 1024014 mem 2
UDP: inuse 11 mem 1
UDPLITE: inuse 0
RAW: inuse 0
FRAG: inuse 0 memory 0

获取当前系统打开的文件句柄:

sysctl -a | grep file
fs.file-nr = 1025216 0 1048576
fs.file-max = 1048576

此时任何类似于下面查询操作都是一个慢,等待若干时间还不见得执行完毕。

netstat -nat|grep -i "8000"|grep ESTABLISHED|wc -l 
netstat -n | grep -i "8000" | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

以上两个命令在二三十分钟过去了,还未执行完毕,只好停止。

小结

本次从头到尾的测试,所需要有的linux系统需要调整的参数也就是那么几个,汇总一下:

    echo “* – nofile 1048576” >> /etc/security/limits.conf

echo “fs.file-max = 1048576” >> /etc/sysctl.conf
echo “net.ipv4.ip_local_port_range = 1024 65535” >> /etc/sysctl.conf

echo “net.ipv4.tcp_mem = 786432 2097152 3145728” >> /etc/sysctl.conf
echo “net.ipv4.tcp_rmem = 4096 4096 16777216” >> /etc/sysctl.conf
echo “net.ipv4.tcp_wmem = 4096 4096 16777216” >> /etc/sysctl.conf

其它没有调整的参数,仅仅因为它们暂时对本次测试没有带来什么影响,实际环境中需要结合需要调整类似于SO_KEEPALIVE、tcpmax_orphans等大量参数。

本文代表一次实践,不足之处,欢迎批评指正。

门户级UGC系统的技术进化路线——新浪新闻评论系统的架构演进和经验总结

评论系统,或者称为跟帖、留言板,是所有门户网站的核心标准服务组件之一。与论坛、博客等其他互联网UGC系统相比,评论系统虽然从产品功能角度衡量相对简单,但因为需要能够在突发热点新闻事件时,在没有任何预警和准备的前提下支撑住短短几分钟内上百倍甚至更高的访问量暴涨,而评论系统既无法像静态新闻内容业务那样通过CDN和反向代理等中间缓存手段化解冲击,也不可能在平时储备大量冗余设备应对突发新闻,所以如何在有限的设备资源条件下提升系统的抗压性和伸缩性,也是对一个貌似简单的UGC系统的不小考验。

新闻评论系统的起源

新浪网很早就在新闻中提供了评论功能,最开始是使用Perl语言开发的简单脚本,目前能找到的最早具备评论功能的新闻是2000年4月7日的,经过多次系统升级,2014年前的评论地址已经失效了,但数据仍保存在数据库中。直到今天,评论仍是国内所有新闻网站的标配功能。

评论系统3.0

2003年左右,我接手负责评论系统,系统版本为3.0。当时的评论系统运行在单机环境,一台x86版本Solaris系统的Dell 6300服务器提供了全部服务,包括MySQL和Apache,以及所有前后台CGI程序,使用C++开发。

图1  3.0系统流程和架构

3.0系统的缓存模块设计得比较巧妙,以显示页面为单位缓存数据,因为评论页面依照提交时间降序排列,每新增一条评论,所有帖子都需要向下移动一位,所以缓存格式设计为每两页数据一个文件,前后相邻的两个文件有一页数据重复,最新的缓存文件通常情况下不满两页数据。

图2  页面缓存算法示意图

图2是假设评论总数95条,每页显示20条时的页面缓存结构,此时用户看到的第一页数据读取自“缓存页4”的95~76,第二页数据读取自“缓存页3”的75~56,以此类推。

这样发帖动作对应的缓存更新可简化为一次文件追加写操作,效率最高。而且可保证任意评论总量和显示顺序下的翻页动作,都可在一个缓存文件中读到所需的全部数据,而不需要跨页读取再合并。缺点是更新评论状态时(如删除),需要清空自被删除帖子开始的所有后续缓存文件。缓存模块采取主动+被动更新模式,发帖为主动,每次发帖后触发一次页面缓存追加写操作。更新评论状态为被动,所涉及缓存页面文件会被清空,直到下一次用户读取页面缓存时再连接数据库完成查询,然后更新页面缓存,以备下次读取。这个针对发帖优化的页面缓存算法继续沿用到了后续版本的评论系统中。

此时的评论系统就已具备了将同一专题事件下所有新闻评论汇总显示的能力,在很长一段时间内这都是新浪评论系统的独有功能。

虽然3.0系统基本满足了当时的产品需求,但毕竟是单机系统,热点新闻时瞬间涌来的大量发帖和读取操作,经常会压垮这台当时已属高配的4U服务器,频繁显示资源耗尽的错误页面。我接手后的首要任务就是尽量在最短时间内最大限度降低系统的宕机频率,通过观察分析确定主要性能瓶颈在数据库层面。

3.0系统中,每个新闻频道的全部评论数据都保存在一张MyISAM表中,部分频道的数据量已经超过百万,在当时已属海量规模,而且只有一个数据库实例,读写竞争非常严重。一旦有评论状态更新,就会导致很多缓存页面失效,瞬间引发大量数据库查询,进一步加剧了读写竞争。当所有CGI进程都阻塞在数据库环节无法退出时,殃及Apache,进而导致系统Load值急剧上升无法响应任何操作,只有重启才能恢复。

解决方案是增加了一台FreeBSD系统的低配服务器用于数据库分流,当时MySQL的版本是3.23,Replication主从同步还未发布,采取的办法是每天给数据表减肥,把超过一周的评论数据搬到2号服务器上,保证主服务器的评论表数据量维持在合理范围,在这样的临时方案下,3.0系统又撑了几个月。

现在看来,在相当简陋的系统架构下,新浪评论系统3.0与中国互联网产业的门户时代一起经历了南海撞机、911劫机、非典、孙志刚等新闻事件。

评论系统4.0启动

2004年左右,运行了近三年的3.0系统已无法支撑新浪新闻流量的持续上涨,技术部门启动了4.0计划,核心需求就是三个字:不宕机。

因为当时我还负责了新浪聊天系统的工作,不得不分身应对新旧系统的开发维护和其他项目任务,所以在现有评论系统线上服务不能中断的前提下,制定了数据库结构不变,历史数据全部保留,双系统逐步无缝切换,升级期间新旧系统并存的大方针。

第一阶段:文件系统代替数据库,基于ICE的分布式系统

既然3.0系统数据库结构不可变,除了把数据库升级到MySQL 4.0启用Repliaction分解读写压力以外,最开始的设计重点是如何把数据库与用户行为隔离开。

解决方案是在MySQL数据库和页面缓存模块之间,新建一个带索引的数据文件层,每条新闻的所有评论都单独保存在一个索引文件和一个数据文件中,期望通过把对数据库单一表文件的读写操作,分解为文件系统上互不干涉可并发执行的读写操作,来提高系统并发处理能力。在新的索引数据模块中,查询评论总数、追加评论、更新评论状态都是针对性优化过的高效率操作。从这时起,MySQL数据库就降到了只提供归档备份和内部管理查询的角色,不再直接承载任何用户更新和查询请求了。

同时引入了数据库更新队列来缓解数据库并发写操作的压力,因为当时消息队列中间件远不如现在百花齐放,自行实现了一个简单的文件方式消息队列模块,逐步应用到4.0系统各个模块间异步通信场合中。

图3  4.0系统流程

选用了ICE作为RPC组件,用于所有的模块间调用和网络通信,这大概是刚设计4.0系统时唯一没做错的选择,在整个4.0系统项目生命周期,ICE的稳定性和性能表现从未成为过问题。

图4  4.0索引缓存结构

4.0系统开发语言仍为C++,因为同时选用了MySQL 4.0、ICE、Linux系统和新文件系统等多项应用经验不足的新技术,也为后来的系统表现动荡埋下了伏笔(新浪到2005年左右才逐步从FreeBSD和Solaris迁移到了CentOS系统)。

图5  4.0系统架构

此时的4.0评论系统已从双机互备扩容到五机集群,进入小范围试用阶段,虽然扛过了刘翔第一次夺金时创纪录的发帖高峰,但倒在了2004年亚洲杯中国队1 : 3败于日本队的那个夜晚。

当时系统在进入宕机之前的最高发帖速度大约是每分钟千帖量级,在十年前还算得上是业界同类系统的峰值,最终确认问题出在文件系统的I/O负载上。

设计索引缓存模块时的设想过于理想化,虽然把单一数据表的读写操作分解到了文件系统的多个文件上,但不可避免地带来了对机械磁盘的大量随机读写操作,在CentOS默认的Ext3文件系统上,每条新闻对应两个文件的设计(2004年新浪新闻总量为千万左右),虽然已采取了128×256的两层目录HASH来预防单目录下文件过多隐患,但刚上线时还表现良好的系统,稍过几个月后就把文件系统彻底拖垮了。

既然Ext3无法应对大数量文件的频繁随机读写,当时我们还可以选择使用B*树数据结构专为海量文件优化的ReiserFS文件系统,在与系统部同事配合反复对比测试,解决了ReiserFS与特定Linux Kernel版本搭配时的kswapd进程大量消耗CPU资源的问题后,终于选定了可以正常工作的Kernel和ReiserFS对应版本,当然这也埋下了ReiserFS作者杀妻入狱后新装的CentOS服务器找不到可用的ReiserFS安装包这个大隐患。

第二阶段:全系统异步化,索引分页算法优化

直到这个阶段,新浪评论系统的前端页面仍是传统的Apache+CGI模式,随着剩余频道的逐步切换,新浪评论系统升级为静态HTML页面使用XMLHTTP组件异步加载XML数据的AJAX模式,当时跨域限制更少的JSON还未流行。升级为当时刚刚开始流行的AJAX模式并不是盲目追新,而是为了实现一个非常重要的目标:缓存被动更新的异步化。

随着消息队列的普遍应用,4.0系统中所有的数据库写操作和缓存主动更新(即后台程序逻辑触发的更新)都异步化了,当时已在实践中证明,系统访问量大幅波动时,模块间异步化通信是解决系统伸缩性和保证系统响应性的唯一途径。但在CGI页面模式下,由用户动作触发的缓存被动更新,只能阻塞在等待状态,直到查询数据和更新缓存完成后才能返回,会导致前端服务器Apache CGI进程的堆积。

使用AJAX模式异步加载数据,可在几乎不影响用户体验的前提下完成等待和循环重试动作,接收缓存更新请求的支持优先级的消息队列还可合并对同一页面的重复请求,也隔离了用户行为对前端服务器的直接冲击,极大提高了前端服务器的伸缩性和适应能力,甚至连低硬件配置的客户端电脑在AJAX模式加载数据时都明显更顺畅了。前端页面静态化还可将全部数据组装和渲染逻辑,包括分页计算都转移到了客户端浏览器上,充分借用用户端资源,唯一的缺点是对SEO不友好。

通过以上各项措施,此时的4.0系统抗冲击能力已有明显改善,但是接下来出现了新的问题。在3.0系统时代,上万条评论的新闻已属少见,随着业务的增长,类似2005年超女专题或者体育频道NBA专题这样千万评论数级别的巨无霸留言板开始出现。

为了提高分页操作时定位和读取索引的效率,4.0系统的算法是先通过mmap操作把一个评论的索引文件加载到内存,然后按照评论状态(通过或者删除)和评论时间进行快速排序,筛选出通过状态的帖子并按时间降序排列,这样读取任意一页的索引数据,都是内存中一次常量时间成本的偏移量定位和读取操作。几百条或者几千条评论时,上述方案运作得很好,但在千万留言数量的索引文件上进行全量排序,占用大量内存和CPU资源,严重影响系统性能。我们曾尝试改用BerkeleyDB的Btree模式来存储评论索引,但性能不升反降。

为避免大数据量排序操作的成本,只能改为简单遍历方式,从头开始依次读取,直到获取所需的数据。虽可通过从索引文件的两端分别作为起点,来提升较新和较早页面的定位效率,但遍历读取本身就是一个随着请求页数增大越来越慢的线性算法,并且随着4.0系统滑动翻页功能的上线,原本用户无法轻易访问到的中间页面数据也开始被频繁请求,因此最终改为了两端精确分页,中间模糊分页的方式。模糊分页就是根据评论帖子的通过比例,假设可显示帖子均匀分布,一步跳到估算的索引偏移位置。毕竟在数十万甚至上百万页的评论里,精确计算分页偏移量没有太大实际意义。

图6  异步缓存更新流程

2005年非常受关注的日本申请加入联合国常任理事国事件,引发了各家网站的民意沸腾,新浪推出了征集反日入常签名活动并在短短几天内征集到2000多万签名。因为没有预计到会有如此多的网民参与,最开始简单实现的PHP+MySQL系统在很短时间内就无法响应了,然后基于4.0评论系统紧急加班开发了一个签名请愿功能,系统表现稳定。

评论系统4.0第三阶段:简化缓存策略,进一步降低文件系统I/O

到了这个阶段,硬件资源进一步扩容,评论系统的服务器数量终于达到了两位数,4.0系统已实现了当初的“不宕机”设计目标,随着网站的改版,所有新闻页面(包括网站首页)都开始实时加载和显示最新的评论数量和最新的帖子列表,此时4.0系统承受的Hits量级已接近新浪新闻静态池的水平。从这时起,新浪评论系统再没有因为流量压力宕机或者暂停服务过。

前面提到,新装的CentOS系统很难找到足够新版本的ReiserFS安装包,甚至不得不降级系统版本,一直困扰性能表现的文件系统也接近了优化的极限,这时候Memcached出现了。

图7  系统架构

2006年左右Memcached取代了4.0系统中索引缓存模块的实体数据部分(主要是评论正文),索引缓存模块在文件系统上只存储索引数据,评论文本都改用Memcached存储,极大降低了文件系统的I/O压力。因为系统流量与热点事件的时间相关性,仅保存最近几周的评论就足以保证系统性能,极少量过期数据访问即使穿透到MySQL也问题不大,当然服务器宕机重启和新装服务器上线时要非常留意数据的加载预热。

之后4.0系统进入稳定状态,小修小补,又坚持服役了若干年,并逐步拓展到股票社区、签名活动、三方辩论、专家答疑、观点投票等产品线,直到2010年之后5.0系统的上线。

2008年5月12日,我发现很多网友在地震新闻评论中询问亲友信息,就立即开发了基于评论系统的地震寻亲功能并于当晚上线。大约一周后为了配合Google发起的寻亲数据汇总项目,还专门为Google爬虫提供了非异步加载模式的数据页面以方便其抓取。

2004年上线的4.0系统,2010~2011年后被5.0系统取代逐步下线,从上线到下线期间系统处理的用户提交数据量变化趋势如图8所示。

图8  系统流量变化图

高访问量UGC系统设计总结

纵观整个4.0系统的设计和优化过程,在硬件资源有限的约束下,依靠过渡设计的多层缓冲,完成了流量剧烈波动时保障服务稳定的最基本目标,但也确实影响到了UGC系统最重要的数据更新实时性指标,数据更新的实时性也是之后5.0系统的重点改进方向。

总结下来,一般UGC系统的设计方针就是通过降低系统次要环节的实时一致性,在合理的成本范围内,尽量提高系统响应性能,而提高响应性能的手段归根结底就是三板斧:队列(Queue)、缓存(Cache)和分区(Sharding)。

  • 队列:可以缓解并发写操作的压力,提高系统伸缩性,同时也是异步化系统的最常见实现手段。
  • 缓存:从文件系统到数据库再到内存的各级缓存模块,解决了数据就近读取的需求。
  • 分区:保证了系统规模扩张和长期数据积累时,频繁操作的数据集规模在合理范围。

关于数据库,区分冷热数据,按照读写操作规律合理拆分存储,一般UGC系统近期数据才是热点,历史数据是冷数据。

  • 区分索引和实体数据,索引数据是Key,易变,一般用于筛选和定位,要保证充分的拆分存储,极端情况下要把关系数据库当NoSQL用;实体数据是Value,一般是正文文本,通常不变,一般业务下只按主键查询;两者要分开。
  • 区分核心业务和附加业务数据,每一项附加的新业务数据都单独存储,与核心业务数据表分开,既可降低核心业务数据库的变更成本,还可避免新业务频繁调整上下线时影响核心业务。

目前的互联网系统大都严重依赖MySQL的Replication主从同步来实现系统横向扩展,虽然MySQL在新版本中陆续加入RBR复制和半同步等机制,但从库的单线程写操作限制还是最大的制约因素,到现在还没有看到很理想的革新性解决方案。

关于缓存,从浏览器到文件系统很多环节都有涉及,这里主要说的是应用系统自己的部分。

  • 最好的缓存方案是不用缓存,缓存带来的问题往往多于它解决的问题。
  • 只有一次更新多次读取的数据才有必要缓存,个性化的冷数据没必要缓存。
  • 缓存分为主动(Server推)和被动(Client拉)两种更新方式,各自适用于不用场景。主动更新方式一般适用于更新频率较高的热数据,可保证缓存未命中时,失控的用户行为不会引发系统连锁反应,导致雪崩。被动更新方式一般适用于更新频率相对较低的数据,也可以通过上文提到的异步更新模式,避免连锁反应和雪崩。
  • 缓存的更新操作尽量设计为覆盖方式,避免偶发数据错误的累积效应。

一个UGC系统流量刚开始上涨时,初期的表面性能瓶颈一般会表现在Web Server层面,而实际上大多是数据库的原因,但经充分优化后,最终会落在文件系统或网络通信的I/O瓶颈上。直接承载用户访问冲击的前端服务器最好尽量设计为无状态模式,降低宕机重启后的修复工作量。

顺带提及,我在新浪聊天和评论系统的开发过程中,逐步积累了一个Web应用开发组件库,在新浪全面转向PHP之前,曾用于新浪的内容管理(CMS)、用户注册和通行证、日志分析和论坛等使用C++的系统,目前发布于github.com/pi1ot/webapplib。

评论系统5.0方案

2010年后针对4.0系统的缺陷,启动了5.0系统工作。因为工作的交接,5.0系统我只负责了方案设计,具体开发是交给其他同事负责的,线上的具体实现与原始设计方案可能会有区别。5.0系统极大简化了系统层次,在保证抵抗突发流量波动性能的前提下,数据更新的及时性有了明显提高。

图9  4.5系统流程

图10  5.0系统流程

设计方案上的主要变化有以下几点。

  • 评论帖子ID从数据库自增整数改为UUID,提交时即可确定,消除了必须等待主库写入后才能确定评论ID的瓶颈,对各个层面的缓存逻辑优化有极大帮助。
  • 重新设计数据库结构,通过充分的数据切分,保证了所有高频业务操作都可在一个有限数据量的数据表中的一次简单读取操作完成,索引和文本数据隔离存储,在数据库中实现了原4.0系统中索引模块的功能,取消了4.0系统的索引缓存层。
  • 改用内存NoSQL缓存用户频繁读取的最新10~20页数据,取消了原4.0系统文件方式的页面缓存层。
  • 系统运行环境迁移到新浪云的内部版本:新浪动态平台,设备资源富裕度有了极大改善。
  • 改为Python语言开发,不用再像4.0系统那样每次更新时都要等待半个小时的编译过程,也不用再打包几百兆的执行文件同步到几十台服务器上,而语言层面的性能损失可以忽略不计。

新闻评论产品总结

新闻评论作为微博之前最能反映舆情民意的UGC平台,长期承载了国内互联网用户对时事新闻的匿名表达欲望,曾经一度成为上到政府下到网民的关注焦点。虽然面临了相对其他社区系统更为严厉的管控力度,也错过了实施实名制改造时迈向社区化的最佳时机,但无论如何,在21世纪的前十年,国内门户网站的新闻评论服务,都是中国互联网产品和技术发展历史上绝对不能错过的一笔。

作者刘立,2000年毕业于哈尔滨工业大学计算机系,2000-2013年工作于新浪网研发中心和门户技术部门,目前在一家社交电商平台创业团队任技术负责人。

Bootcamp Mac 安装Win10 教程

安装win10之前,告诉你首先要准备的东西:一、苹果电脑(可以正常进入Mac OS系统的)二、U盘(大于等于8G,确定不是坏的)
这两样东西准备好,我们就可以开始了

1、分区–进入苹果电脑的Mac OS系统,找到“实用工具”里的“磁盘工具”,双击打开,先点本机总的那块儿磁盘,再选择“分区”,点“+”增加一个分区,右侧“大小”可以调整WINDOWS的大小,确定好分区大小之后,点击“应用”,就可以等待分区完毕了!(注意:1、WINDOWS分区的格式是Mac OS扩展(日志式)2、如果提示分区失败的话,可以进入recovery分区修复一下磁盘,如果修复后还是不行,建议整个磁盘格式化重装Mac吧)

1.png

2、准备–Win10镜像文件和激活工具

windows 10 下载(把下面链接复制到迅雷里即可下载)
cn_windows_10_multiple_editions_x64_dvd_6848463.iso (4.01 GB)

win10jihuotools_3987.rar (2.46 MB, 下载次数: 16337)
2.png

3、使用BootCamp助理制作WIN10启动U盘和获取WINDOWS对应的驱动
BootCamp助理的位置在–应用程序–实用工具下,找到后双击打开
3.png
点击继续
4.png
选择第一个选项(制作WIN10 U盘启动)和第二个选项(获取WINDOWS对应的驱动)
5.png
第一、选取正确的ISO镜像 第二、目的磁盘选择你确定是好的那个U盘(U盘记得先格式化一下,格式为Mac OS 扩展(日志式))
6.png
点继续
7.png
然后等着就可以了
8.png
等着…
9.png
等着…
10.png
快完成的时候会提示下面窗口,输入你系统密码点回车就可以
11.png
完成–U盘启动和win所需的驱动都在U盘里了
12.png

4、重启电脑,用做好的U盘启动安装WIN10
重启电脑长按OPTION不放(两边按哪个都行)
IMG_2009.jpg
然后会出现下图所示,选择第四个黄色U盘启动,点回车进入
IMG_2011.jpg
点击下一步
IMG_2012.jpg
点击跳过
IMG_2013.jpg
可以选择专业版和家庭版,果断选择专业版了,点击下一步
IMG_2014.jpg
点击下一步
IMG_2015.jpg
选择刚刚分了80G的那个区,点删除(注意:其他分区和未分配的空间不要动,删除后Mac OS就崩溃了)
IMG_2016.jpg
删除后会出现下面页面,然后点新建
IMG_2017.jpg
然后再点应用(大小默认的最大,不用自己调整)
IMG_2018.jpg
点击确定
IMG_2019.jpg
windows主分区已经完成,选择后点下一步
IMG_2020.jpg
等…
IMG_2021.jpg
等…
IMG_2022.jpg
等…
IMG_2023.jpg
完成–自动重启
IMG_2024.jpg
自动重启后就自动进入WIN10启动界面了,点“以后再说”
IMG_2025.jpg
点击“使用快速设置”
IMG_2026.jpg
新建一个账户,密码可以不填
IMG_2027.jpg

5、安装WINDOWS对应的BootCamp驱动(驱动已经在U盘里)
双击打开BootCamp文件夹
IMG_2029.jpg
双击Setup安装
IMG_2030.jpg
单机下一步
IMG_2031.jpg
等…
IMG_2032.jpg
等…
IMG_2033.jpg
完成–会提示重启电脑
IMG_2034.jpg
6、打开WIN10激活工具,点击激活就大功告成喽
IMG_2035.jpg

IMG_2036.jpg

PS:如果有看了此教程不会的,建议多看几遍;如果有看了此教程安装失败的,建议看着多安装几次