过载保护

有关过载保护有个很著名的例子,08年奥运会前门票预售,在开放购票的最初时间点涌入的用户请求太多,导致整个售票网站无法正常提供服务。

由于互联网的特性,极大地消除了地理上、时间上的差距,每个用户都能够以类似的、比较低的成本享受到同样的互联网服务。这样对很容易形成人类历史上没有出现过的海量服务产品。过载保护是互联网海量服务所必须具有的一种特性,用来保护系统本身不会被超过系统负载能力的请求所压倒,进而发生雪崩现象,压垮整个服务系统。

从技术手段上,实现过载保护的方式有:

  1. 对请求数量进行限制。可以通过请求队列进行控制,请求队列长度超过阈值时,抛弃队列中前面的请求。
  2. 当请求队列中的任务超时时,也抛弃,如果需要返回给调用方就返回失败信息,避免调用方等待过久。
  3. 对当前机器负载进行监控。定期收集 CPU、Load 等负载数据,当超过预设的阈值时,抛弃请求队列中靠前的请求。
  4. 可以对队列进行分优先级,重要的请求放到高优先级队列中,不重要的请求放到低优先级队列中,优先抛弃低优先级队列中的请求,需要业务做好柔性策略。

其中提到的请求抛弃策略都是抛弃较旧的请求任务,是因为考虑到大部分情况下,用户在遇到系统反应比较慢的时候,会主动重试,这样对于用户来说之前的请求就是无效的,因此系统可以在遇到负载问题时抛弃较旧的请求。如果业务的场景与此不同,那么就应该采取其他的策略。

技术手段上进行过载保护,对于整体用户而言,都会体现出一种有损服务的体验,用户会发现偶尔或者经常得到失败信息,虽然这比整个服务完全不可用的状态要好很多,但是还是会降低用户体验。如果在产品/服务的设计上预先进行考虑,避免出现用户请求的极大不平均的情况,那么无论是对系统的安全还是对用户的体验来说,都会好很多。以奥运门票预售为例,可以要求所有购票者预先提供预定信息,预定的时间可以设定成比较长的时间段,用户提交预定信息的先后顺序和是否能够得到门票的机会无关,那么就可以把用户的请求分散到跨度较长的时间中,不会形成“抢票机制”下所出现的瞬间请求量暴增的情况。

如何计算系统容量

在设计互联网服务的系统时,面向的是海量的互联网用户,尤其是对于拥有良好推广渠道、不用花费太长时间进行推广的互联网产品,可能在系统上线之初就会进入大量用户。因此需要预先估计系统的容量,作为产品推广、放量的依据;并且预先知道系统的瓶颈在哪、需要优先优化的方向、硬件需求取向、扩容的时机和规模。

典型的系统架构会有三层,分别是接入层、逻辑处理层、存储层。对于接入层的机器而言,一般是不负责任何逻辑处理,也不保存业务状态,因此对内存、外存没有太高要求,CPU 采用普通档次的即可,重要的是提供高带宽的网卡。对于负责业务逻辑处理的机器,CPU、内存资源更为重要。对于存储机器,需要尽可能快的外存,并且对机器的可靠性要求较高。

对大文件存储的应用来说,主要受限于外存的顺序读写性能,一般用 MBPS 考察;对于小型的数据存取应用来说,主要首先于外存的随机读写性能,一般用 IOPS 考察。7200转的硬盘,顺序读能力不超过150MB/s,顺序写能力不超过70MB/s;随机读写能力在 100 IOps 上下。因此,硬盘的性能对于顺序读写海量数据的应用来说是足够的,但是随机访问微小数据时,硬盘的机械特性就成了很严重的问题(需要先寻道,再读写,寻道时间为ms级)。以上面假想的硬盘指标计算,每天最多可以顺序读取约13TB的数据,写入约6.5TB的数据;或者执行800多万次随机IO。类似的,现在很火的 SSD 也可以如此计算。从 SSD 的单位容量成本看来,它不适合海量存储,从性能指标看来,和机械硬盘相比顺序读写的优势也不是太大,但是随机读写能力远远超过机械硬盘。

CPU 的性能指标是 IPS,最关键的指标是 CPU 的主频,决定了每秒最多能执行的指令的数目,这样就能知道 CPU 实际能承载的上限。在实际环境中,也可以通过观察在线系统的 CPU 使用率和当前系统使用量,估计出 CPU 的容量有多大。

内存有两方面的作用,一是提供业务处理所需要的存储空间,二是作为缓存。业务执行所需要的空间是临时性的,可以很快回收复用,只要分配的速度不是太快以至于这部分空间不够用即可。

系统中的缓存会长期占用内存空间,是使用内存的大户。计算缓存的大小,要知道被缓存数据的单位大小,以及所缓存的数据总数。在 Java 中,基本类型的大小是固定的,数据需要3个引用的额外开销,对象需要两个引用的额外开销,在组成高一级的数据结构(对象)时,JVM 会将以8字节为边界单位进行数据对齐操作。有关 Java 对象大小计算的文章可以参考《HOWTO: Determine the size of a Java Object or Class》

还需要考虑业务执行时可以分配的最大线程数量。Linux 上的线程堆栈最大可以占用8MB内存空间,在 JVM 上可以通过启动参数配置线程堆栈的大小,只要不超过操作系统的限制即可。通过系统的总内存空间、缓存所需要的空间、应用运行所需要的临时空间,也就可以计算出线程数的限制。

网卡也是系统的限制之一,计算带宽可以通过以下公式:
(请求或者回应平均大小 * 请求数) / 86400s / 8bit
请求或者回应的平均大小可以在实际环境中采集得到,也可以通过业务的数据情况计算得到上限和平均值。需要注意的是,需要支持多对列的网卡和相应的 Linux kernel 才能利用多CPU/多核,否则所有的网络 IO 开销都会集中在一颗 CPU 上,使得无法足够利用网卡的带宽。

从远程接口耗时计算系统的容量是比较简单的。变量是接口的耗时,可以在实际环境上采集数据得到。
计算请求数的公式:
(1s / 请求平均耗时) * 线程数 * 86400s
需要注意的是,外部接口后面往往是另外一套复杂的系统,越复杂的东西越容器出错,而且自身系统到外部系统之间可能会经过可靠性较低的网络,数据传输过程中的失败率也会比较高,所以需要考虑在最糟糕的情况下如果处理,避免因为远程接口调用的失败率过高造成系统雪崩。

计算系统容量不是一个太容易的过程,需要开发人员对系统的软硬件都有较深入的了解,通过系统的物理限制得出程序所能提供吞吐量的上限。但是一旦掌握了计算的方法,就更容易做到对系统的能力心中有数,知道系统改进的方向,而不是盲人摸象般地开发或者重构。

HTC Hero 升级 Android 2.2 记

Android 2.2 出世已经不短时间,不过担心 Hero 这种已经 out of date 的机型硬件规格不支持,以及非官方 ROM 的稳定性问题,一直没有刷新升级。最近看到有爱玩 Android 的同事刷了 VillainROM 1.1.9 版,速度甚至比基于 Android 2.1 的 BeHero+ 1.2 还快,也很稳定,可以实用,于是忍不住也想升级一把。

以前已经刷新过好几次 ROM,都很顺利。于是直接下载最新出炉的 VillainROM 1.2.1 版,按照老经验,将文件更名为 update.zip 放到 /sdcard 下,重启手机进入 recovery mode,清除数据后开始升级,不料进行到一半,提示 “Can’t find update script”,升级过程中止。Google 一下,说是因为 recovery 工具版本太低所致,于是开始下载 1.6.2 版本的 recovery 工具(这里下载),将之也拷贝到 /sdcard 下,在 gnome-terminal 中执行
./adb shell
# flash_image recovery /sdcard/recovery.img

很快得到反馈:
flash_image not found
执行
# ls -l /system/bin/flash_image
发现原来之前刷的 BeHero+ 1.2 ROM 中没有包含 flash_image 程序。前置条件还是未满足,再次 Google 寻找 flash_image这里下载),下载后解压文件,执行
./adb push ~/tmp/flash_image /system/bin/
讲下载到的 flash_image 拷贝到手机上。然后升级 recovery 工具,用新版 recovery 工具刷 VillainROM,一切顺利。

升级到 VillainROM 1.2.1 后,发现速度确实不错,基本稳定,不过使用过程中出现过一次浏览器崩溃的情况。这个 ROM 把 HTC Sense 给去掉了,因此喜欢 HTC Sense 的用家需要考虑是否刷这个 ROM。

开发杂谈 (1)

  • 互联网产品大多属于短平快的软件系统:业务逻辑简单,开发周期短,改进快速。
  • 在开发新产品的初期,应该重点关注业务,其次是架构,再次是开发框架。
    待产品推广开来,用户基数快速增长,这个阶段中改善软件系统的架构变得非常重要,需要在用户飞速增长的同时提供良好的基本用户体验。架构需要保证高性能以提供较低的响应时间,并且降低单位用户的成本;具有良好的伸展性,便于通过简单的扩展便能线性地提高系统的吞吐量;系统要具备可监控性,在出现故障时能第一时间告警,最好能够具有一定的自我修复功能。
    到了产品的成熟期之后,用户增长速度将会变得较为平缓,原有的产品可能会朝平台方向发展,需要通过扩展众多的附加应用来延长产品的生命力。在这个阶段,提高开发效率的问题会变得重要起来,因此需要改善系统开发框架,使后续开发变得更为快速、容易。
  • 避免复杂的开发框架,复杂的框架往往会给系统引入高复杂性,容易降低系统的健壮性和可维护性。
  • 用语言本身所擅长的机制解决问题,避免用框架来解决语言本身的局限。
  • 远离复杂的元框架!

解决设置 Eclipse 快捷键无效的问题

最近将 Eclipse 从 3.5 升级到 3.6,发现之前自己设置的 SVN 快捷键失效,比如说 svn commit 设置成了 ctrl+alt+n, c 的组合快捷键,在按下 ctrl+alt+n 的时候,Eclipse 右下角会弹出 tip 窗口提示下一步,再按 c 并无反应。经过摸索发现,原来是 perspective 中没有关联到 svn 的 command group。如果遇到类似问题,可以在工具条空白处右击,弹出右键菜单,选择 Customize Perspective,切换到 Command Group Availability,选择左侧 Available command groups 中的 SVN 即可。

Load Cycle Count

今天在计划备份数据时想到,硬盘寿命对备份计划的影响。之前 Ubuntu 存在一个 bug,导致磁头较频繁地降落在停止区(本意是节能),在支持 S.M.A.R.T. 技术的硬盘中可以查询到 Load cycle count 这个参数,即该操作的次数。但是这个操作对硬盘寿命是有影响的,现代的硬盘,可以执行数十万次这样的操作,根据网络上的说法,可以执行至少30万次,多的可以到60万次。

在 Linux 系统中,可以通过执行

$ sudo smartctl --all /dev/sda | grep 'Load_Cycle_Count'
193 Load_Cycle_Count 0x0032 098 098 000 Old_age Always - 51433

最后一个数字就是 load cycle count 的值,可以看到我笔记本的硬盘在两年多的时间中已经执行了51433次此类操作,因此可以推断该硬盘磁头的寿命还很长。

Grub 安装简要说明

一般情况下我们都会在安装 Linux 的时候,将 Grub 引导记录安装在 MBR 上。这样在重新安装 Windows 之后,MBR 被覆盖,也就不能用 Grub 进行引导了。网上有很多资料介绍如何能够重新加载 Linux,比如使用 grub4dos、利用 Windows OS Loader。个人感觉最简便的方法是使用 Linux Live CD 重新安装 Grub,下面以 Ubuntu Live CD 为例简要说明一下重新安装 Grub 的步骤。

使用 ubuntu-desktop-cd 启动,进入 live 模式。开启一个 terminal,输入

ubuntu@ubuntu:~$ sudo su
root@ubuntu:~$ grub
grub> root (hd0, 1)
grub> setup (hd0)
grub> quit

对于 Grub 而言,所有接口的硬盘都是被识别为 hd 的,无论用户的硬盘使用的是 IDE 还是 SATA 接口。和 Linux 对 dev 的标识不同,Grub 的计数是从0开始的。因而 hd0 表示第一块硬盘,而 (hd0, 1) 即表示第一块硬盘的第二个分区(也就是说在 fdisk 看来,应该是标识为 sda2 的)。root 命令后面需要指定的是用户 Linux 的 /boot 所在的分区,如果没有为 /boot 单独分区,那么也就是指定 / 所在的分区。setup 命令需要指定 grub 安装在哪一块硬盘上。重启之后即可。

PS. 几个月内可能需要安装 Windows 系统,Windows 安装后会覆盖系统原先的 MBR,导致引导多系统的 Grub 不能工作,于是找出这篇写于2006年末的文章,以备使用。原文发表于 CSDN Blog。

解决 Compiz 启用后丢失窗口标题栏的问题

升级到 Ubuntu 9.10 后,在显示方面发生了好几次问题,之前解决了无法进入 XWindow 环境的问题。在更新过一次驱动程序后又遭遇到窗口丢失标题栏的问题。具体表现是窗口标题栏、边框都消失,也不相应 alt+f4、alt+tab 之类的视窗环境快捷键。开始时没有找到根本原因,只发现在 System->Preferences->Appearances 中 Visual Effects 变为 None,需要选择 Norma 或者 Extra 来激活 compiz 才能将恢复正常。每次启动 XWindow 之后都要如此操作,不但麻烦,而且还要重新选择 compiz 中自己需要的 plugin。

在网上搜索发现这个问题很早就出现,ubuntuforums.org 上有大把2007年的帖子在讨论这个问题。但是找到的各种方法都没有能够将我的系统恢复正常。不过这些方法应该也是一些必需的步骤。

最基础的是要安装驱动程序,我的笔记本是 nVidia 的显示卡,因此需要安装 nvidia-glx 相应的驱动程序。Ubuntu repository 中最新的驱动程序是 nvidia-glx-185,如果需要更新的驱动程序,可以添加相应的 apt repository:

sudo add-apt-repository ppa:nvidia-vdpau/ppa && sudo apt-get update

更新 apt 之后可以安装 nvidia-glx-190/195 版本的驱动程序。当然驱动程序不是版本越高越好,稳定性是首要考虑的因素,可以按照自己电脑的实际情况选择相应的版本。

安装驱动程序之后,还可能需要修改 /etc/X11/xorg.conf 文件,找到 Device section,例如:

Section "Device"
Identifier "nVidia Corporation NVIDIA Default Card"
VendorName "NVIDIA Corporation"
BoardName "Quadro NVS 140M"
Option "RenderAccel" "true"
Option "AddARGBGLXVisuals" "True"
Driver "nvidia"
Option "NoLogo" "True"
EndSection

重点是标红的那行 Option。在网上还看到有方法这样添加:

sudo nvidia-xconfig --add-argb-glx-visuals -d 24

nvidia-xconfig 会把 option 添加到 Screen section,这种做法并不是很提倡,option 应该和相关的设备尽量放在一起比较好。

到以上步骤都是网上比较常见的解决方案,但是我经过以上的设置还是没有恢复出标题栏来。后来在 System->Preferences->Startup Applications 发现有一项 Compiz Tray Icon 启动名为 compiz-tray-icon 的程序,在 terminal 中尝试手动执行这条命领,发现系统中不存在 compiz-tray-icon!然后在进入 XWindow 后标题栏消失的情况下执行

pgrep -fl compiz

发现系统中根本不存在包含 compiz 名字的进程,因此确定是 compiz 未能正常启动。而 Startup Applications 中的 Compiz Tray Icon 可能是以前版本遗留下来的配置,在 9.10 中 compiz-tray-icon 却消失了。再调查一番,发现有一个替代的程序,叫做 fusion-icon,执行

apt-cache show fusion-icon

得到的软件描述是:

Description: tray icon to launch and manage Compiz Fusion
The OpenCompositing Project brings 3D desktop visual effects that
improve the usability and eye candy of the X Window System and provide
increased productivity.
.
This package contains a tray icon that allows you to easily enable, disable and
restart Compiz, and change the currently used window manager and/or window
decorator.

于是安装这个软件包

sudo apt-get install fusion-icon

并且在 Startup Applications 删除无效的 Compiz Tray Icon,添加新的一项 Fusion Icon

Add Fusion Icon into Startup Applications

另为了使用 Emerald,在 CompizConfig Setting Manager 中启用 Windows Decoration,将 General->Command 设置为

emerald --replace

重新启动 XWindow,大功告成。

解决升级到 Ubuntu 9.10 后无法进入 X Window 的问题

十月底 Ubuntu 9.10 正式发布,用 Alternative CD 从 9.04 升级,一切顺利。但是重启后,发现 Grub 中最新的 2.6.31 版本内核无法启动,系统在启动后进入命令行模式,屏幕不停闪烁,而且键盘输入也呈现问题,完全无法输入密码登录系统。幸好还保留了一个老版本的内核 2.6.28 可以正常启动,但是在安装了 nvidia-glx-185 显卡驱动之后,连 2.6.28 也无法启动了,故障的状况和 2.6.31 类似。上网搜索了一下,在云风的 blog 上看到他也遇到过类似的问题,但是我在查看 /etc/X11/xconf 文件之后,并没有发现分辨率的设置有异。不过因此得到了提示,手动执行 startx,控制台上输出报错信息显示因为某些出现 error 导致 nvidia 驱动无法正常加载,只是报错信息比较含糊,没有办法定位问题之所在,而模糊的信息,也无法提供良好的搜索关键字。

在浪费了若干时间之后,想到可以在 Ubuntu Forum 上或许可以寻找到一些有用的信息。果然,在 Installation & Upgrades 版块就有一篇 stick 在顶部的 thread《ATI & Nvidia: no X, just blinking on 2.6.31 (Karmic 9.10) 》,文中的问题描述和我遇到的情况别无二致。该 thread 中提供了好几种解决方案,我采用的方法(by Sunflower1970)是

  1. 备份并修改 X 的配置文件:
    sudo cp /etc/X11/xconf /etc/X11/xconf.bak && sudo vi /etc/X11/xconf
  2. 找到类似显示设备配置的段落,类似:
    Section "Device"
    Identifier "Default Device"
    Driver "the driver here"
    Option "NoLogo" "True"
    EndSection
  3. Driver 一项修改成为 vesa,之后保存配置文件,重启系统即可。

导致该问题的原因是系统自动安装 nvidia 显卡驱动失败,但是依旧修改了 /etc/X11/xconf 的配置,于是 X 在启动时根据配置去加载没有安装成功的驱动程序,导致出现故障。可以在成功启动系统之后,再尝试自行安装专用的显卡驱动程序。我是按照 chessmani 提供的方法利用 envyng 来安装 nvidia 显卡驱动的。步骤如下:

  1. 增加 PPA 并且更新 apt 源:
    sudo add-apt-repository ppa:nvidia-vdpau/ppa && sudo apt-get update
  2. 安装 envyng:
    sudo apt-get install envyng-core
  3. 安装 nvidia-190-modaliases 以便 envyng-core 检测到需要安装的驱动:
    sudo apt-get install nvidia-190-modaliases
  4. 执行 envyng 开始安装驱动。注意首先卸载旧版本的驱动。
    sudo envyng -t
    按照出现的提示操作即可。

一个用于重试操作的工具类

在编程中,有些操作可能在执行过程中失败,比如说连接某个远程服务的过程中出现超时,而我们不希望因为一次偶然的超时导致整个操作流程被废弃;或者在访问缓存时发现数据不存在于缓存中,因此触发异步过程去数据库中获取数据放入缓存,并且期望在短时间内远程访问数据库的操作能够完成以便能够及时地拿到数据返回给客户端。在条件允许的情况下,我们可以对失败的操作重新尝试一次或着若干次。

我们可以将实现重试操作的代码嵌入到相关的类中,但是更好的方法是应用 Command pattern 将重试的职责分离出来,成为可以复用代码。

最简单的方法,是使用 Callable 接口表示需要重试的操作。编写一个 RetryService 的服务类用于调用相应的 Callable 对象。

public class RetryService {

	private final ExecutorService executorService;

	private RetryService(ExecutorService executorService) {
		this.executorService = executorService;
	}

	/**
	 * Factory method
	 *
	 * @param executorService
	 * @return
	 */
	public static RetryService createRetryService(
			ExecutorService executorService) {
		return new RetryService(executorService);
	}

	/**
	 * 执行可能需要重试的方法,最多若干次,如果执行成功立即返回,否则将重试指定的次数。
	 *
	 * @param task
	 *            operation to retry
	 * @param maxRetryCount
	 *            the maximum number of times this task will be executed
	 * @throws ExecutionException
	 *             if the computation threw an exception
	 * @throws InterruptedException
	 *             if the current thread was interrupted while waiting
	 * @throws TimeoutException
	 *             if the wait timed out
	 * @throws NullPointerException
	 *             if task is null
	 */
	public <T> T submit(Callable<T> task, int maxRetryCount, long timeout,
			TimeUnit unit) throws InterruptedException, ExecutionException,
			TimeoutException {
		if (task == null) {
			throw new NullPointerException();
		}

		for (int i = 0; i < maxRetryCount; i++) {
			Future<T> future = executorService.submit(task);
			try {
				return future.get(timeout, unit);
			} catch (TimeoutException e) {
				if (i <= maxRetryCount - 1) {
					throw new TimeoutException(e.getMessage());
				} else {
					continue;
				}
			}
		}
		// failed to get result
		return null;
	}

}
This blog is protected by Dave\'s Spam Karma 2: 275 Spams eaten and counting...

Switch to our mobile site