随心所记

享受生活的乐趣与烦恼

“策略依赖” – 一种关注调用者体验的“五星级”设计模式

传统的软件设计方法论是不会关注调用者体验的——无论设计者是否有意忽略,往往结果都是这样。

这样说的原因是,“设计”本身的关注点在于“通用”和“可扩展”,而这些点和调用者体验本身就是矛盾的。

我来用一个简单有趣的例子来说明这个矛盾是如何产生的。

比如我们现在要抽象餐馆里顾客点餐的过程,一般的思路会是这样。

  1. 顾客实例化一个服务员实例
  2. 顾客从服务员实例获得菜单,开始选菜
  3. 顾客选好菜品之后调用服务员的点菜接口,下单

用C++表达会是这样,这里省略了很多无关紧要的细节:

class Menu {/*...*/};
class Dish {/*...*/};

class Waiter {
public:
    static Waiter * create();
    const Menu * getMenu() const;
    bool order(const Dish * dish);
};

如果要更具扩展性,会考虑将Waiter、Dish、Menu都抽象成接口。比如服务员可男可女可秀吉,只要能拿到菜单和点餐就行;而各种菜品也只要能提供价格、图片和id就行,细节会由厨师来关心,顾客不用管。

按这个思路还可以继续细化,不过等等,这个思路好么?换句话说,真实世界里面的餐馆真的是这样点餐的么?

显然不全是这样,起码高级的餐馆绝对不会那么冷冰冰。在高级餐馆:

  • 服务员是不用顾客来呼唤的,而是从顾客进门那一刻起(甚至还没决定一定要进去的时候)就开始陪伴,并且主动的递上菜单
  • 用户点餐方式是多种多样的,对于老顾客,一句“跟昨天一样”就足够下单了,或者对于新顾客,来一句“上个你们这有特色的鱼”也足够下单了,当然,一页页的翻菜单并且很正常的报菜名下单也是毫无问题的
  • 服务员还会主动帮助用户调整菜品搭配,如果点了两条鱼或者点了超出用餐人数分量的菜品,会给出善意的提醒和建议
  • 顾客也许需要一些时间慢慢思考点什么菜,服务员会留给顾客空间,并在顾客想好了之后随叫随到
  • 高级餐馆里面还会有“客户经理”这个职位,跟有价值的顾客保持联系,记录这些顾客的个性偏好

考虑到以上这些点,设计时需要抽象的对象就发生了根本的改变——不应该抽象Waiter,而应该主动分析顾客(Customer),否则无法实现个性化。而且这种抽象并不是要求顾客派生自某个接口,而是根据顾客的特性,兼容所有可能的目标客户。

假设要做一个高级西餐厅,面向在中国的有钱人,那么目标人群就是这群懂得西方礼节、高消费、还愿意付小费的人,给他们提供个性化的服务。如果是一个非目标顾客过来了,就用最基本的服务就好。

可以想到,为了服务特定的人群,必须解决以下难点:

  • 针对特定的人群将体验优化到极致,所以很难一般意义的通用性
  • 如果服务的人群品味和习惯发生改变,那么具体的实现也要改变,很难做到可扩展性
  • 每次服务之后还得对主动分析顾客整个用餐过程,及时的响应他们个性化需求,很难做到一劳永逸

这几个设计非常的反传统,简直是最糟糕设计的代表——不通用、难扩展、架构不稳定,这还叫架构设计吗——但如果真的做成,将十分诱人。

幸好随着技术的发展,特别是分布式系统已经取代传统单机系统,分布式计算能力已经超乎想象,这是可以实现的。

下图表示了这种设计的基本思想,即“策略依赖”(Strategy Dependent),在每一个需要个性化调用前加入策略层,实时寻找与被调用对象最相符的服务。这是一个注重被调用者体验的模式,所以我把它戏谑的称作“五星级”设计模式。

Strategy Dependent Graph

“策略依赖” strategy dependent

这不是基于类/接口集成体系的东西,而是一种约定和对事物本质的抽象。比如我们每个人都是顾客,是用各种化学物质构成的互不相同的个体,不是从统一的接口和类继承而来,但我们至少都会说中文或英文,会吃饭会付钱,这是大家的共性,也是这种高级餐馆能够服务我们的基础。所以这里产生service的不是工厂(factory)而是分析器(analyzer),分析器不要求Customer来自任何确定的接口,它会动态实时的根据各种蛛丝马迹来分析。服务质量体现在分析器如何从既有的规则中找到合适的服务方法,以及服务如何自学习从而提供针对个性的服务。

从广义来说,现实的分布式智能系统其实就是这样设计的,服务提供者和服务使用者相互异构但在协议/约定上统一,例如google的个性化搜索。只是一般设计者不会在最初将“策略依赖”引入架构,也不会有意识的在产品继续复杂的时候有机的考虑进去,现在一般都是先有数据,再做分析,最后才慢慢形成这种类似“策略依赖”的模式,这种过程导致的架构变更成本恐怕不能忽略。

写这个也是想至少提醒一下未来的自己,有必要在设计的时候考虑这方面的事情,如果能影响更多人那当然更好啦。

分类: 技术文章 | 沙发还在,快抢~

Javascript代码体积优化的变态技巧

最近看到了js1k网站上的比赛,突然很有兴趣参加一把,于是写了一个自己的作品Save Messed Up Love放了上去。

所有提交的作品必须用小于1024B的javascript实现,我花了一些精力用一些变态技巧优化用工具精简过的代码体积,成功将代码从原来的1271B减到988B,空出的代码让我有空间增加一些额外的功能。在这里给大家分享一下这些变态技巧,注意,千万不要在实际项目中使用

首先介绍一些不变态的技巧,算是入门级的内容,其要点是时间换空间,这是与jslint的原则背道而驰的,把下面每个点都反过来看可能就是正确的编程方法了……

尽量使用with

javascript的with关键字是神器,详见下面的代码:

// var a is the canvas 2d context
a.save();
a.fillStyle = "red";
// ... many many canvas methods/attributes calls
a.restore();

//
// can be optimized to following code.
//
with (a) {
    save();
    fillStyle = "red";
    // ...
    restore();
}

将所有用到的变量放到开头声明

不要分函数声明变量,所有变量都应该放到一起声明,这样才最有利于代码压缩工具减少代码体积。还有一点要注意,千万不要声明全局变量,否则代码压缩工具会以为这些是需要在window对象中导出的变量,从而放弃压缩它们。

// the "with" here is quite important to create a local closure for vars.
with (a) {
    var min, complete, posX, // and more
        draw = function() {
            // no var in this function
        },
        main = function() {
            // no var either
        };

    main();
}

将经常出现的常量变成变量

在我的程序里因为不断会出现字体,所以我将字体单独作为全局变量声明就可以省去不少bytes。程序中还经常会出现100这个数字,我也作为常量声明,省去超过10个byte。

F = "2px Arial";
with (a) {
    // ... other rendering code
    font = 1 + F; // 12px Arial
}

将常用的函数变成变量

程序中使用了Math.cos好几次,将它变成变量以后就能够节省代码量。可惜的是canvas的各种方法都不能用这种技巧,所以这个技巧效果一般。

with (a) {
    var cos = Math.cos;
    // use cos instead of Math.cos, then code compiler can optimize it
    y = 13 * (-13 * cos(t) + 5 * cos(2 * t) + 2 * cos(3 * t) + cos(4 * t) + 13);
}

优化完这些之后,接着就可以去压缩了,压缩推荐使用Google Closure Compiler Online,这已经是当前压缩比例最变态的工具了,一定要选择“Advanced”压缩,效果最好。

压缩过后体积很不如我所意,但又不想砍功能,于是只好面对生成的一堆接近于乱码的东西直接开刀。最后所列的这几条就是变态优化的精髓了,每个可能只能省掉几个到十几个byte,但积少成多。

修复Closure Compiler的压缩bug

恐怕很少有人真的去读过压缩过后的js代码吧,仔细一读发现居然有若干明显问题。

  1. 会自作主张的把浮点数前面补0,比如.3会“优化”成0.3,毫无道理的浪费字节数
  2. 会自作聪明的将从来没变过的变量直接代入计算,比如我前面所说的F = "2px Arial",我故意抽取出这个不变的部分来优化体积,但经过编译之后竟给我消除了这个变量,直接把常量在代码中展开。解决方法是将这个声明放到全局,让工具以为这是window下的全局变量从而放弃优化。
  3. 没有尽可能的去掉分号,比如在每个}之前的分号是没必要的,工具并没有主动去掉

解决完上述问题后就可以进行下面的更变态的优化。

去掉var

var声明其实是完全没有必要的,工具帮忙搞定了变量名的问题之后,就可以将var语句给删除了,一下就可以节省十几个byte。

with(a){var b,d,e,f,g,h,i,j,k,l,m,n=[/*data*/]}

// no need to declare every var.
// delete "var b,d,e,f,g,h,i,j,k,l,m," directly.
// can be optimized to following
with(a){n=[/*data*/]}

将表达式变成函数参数

也许很少人会想到可以利用javascript对函数参数无约束这个特性优化代码大小吧,看下面的例子。

with (a) {
    lineWidth = 3;
    main();
}

//
// given the main() always takes no param,
// it can be optimized to following code
//
with (a) {
    main(lineWidth = 3);
}

效果就是优化掉了一个分号。

对于压缩过的代码其实有更好的压缩效果,可以看下面的例子。

b&&(c=z,u())

// can be optimized to following to save 3 bytes
b&&u(c=z)

这样就优化了3个byte!

这种优化的空间很大,因为除了自己写的函数,还有不少api都是不接受任何参数的(比如canvas 2d context的save()fill()等),都可以用来做这个优化。还有接受参数的函数,只要确定后续多余的参数肯定不会使用,也可以用这个变态技巧。

尽量复用常量

常量一旦大量的复用,就可以节省代码。主要有两种复用,一种是让尽可能多的算式或参数出现同样的常数,比如100;另一种是尽可能让一个量有两种值,比如作为bool的量其实不关心具体数值如何,用零和非零表达即可,只要恰好有个值能表达这个意思就是用这个值。

因为第一种情况比较好理解,下面只对第二种情况举个例子。

T="Win!",z(1)

// can be optimized to following
z(T="Win!")

关于复用常量其实还有另外一个非常小的技巧,即有相同常量的时候尽量用连等。我的代码里恰好有一个数组声明,里面有0~15的常量,于是我就直接在数组里声明。

n=[12,2,3,7,C=o=0,6,10,5,4,15,9,14,8,B=13,1,11]

有了以上变态方法之后,代码体积明显的减小了,最终达到了变态的体积要求。最后的最后在提醒大家一次,不要在项目中使用这些技巧,只会害人害己哦。

分类: 技术文章 | 4 条评论

开源轻量级HTML5游戏引擎gin简介

HTML5 canvas让程序员自由的绘制想要的图形和动画,使得纯粹基于HTML/Javascript/CSS的游戏铺平了道路。只不过canvas最初并非为游戏而设计,而且除了绘图以外,做游戏还有不少其他事情要考虑,例如鼠标键盘事件、图层、动画等。为了方便程序员开发游戏,游戏框架/引擎陆续被开发出来,gin就是其中之一。

Project home: https://github.com/huandu/gin

Author: Huan Du (blog, twitter)
Samples: https://github.com/huandu/gin-samples
Live demo: simple sample mouse tracer shape breaker

gin是什么

gin是一个开源轻量级的HTML5游戏引擎,只专注于搭建一个简单可靠的游戏基础设施,让游戏能在网页中高效流畅的运转起来。

gin能做什么

gin提供了游戏开发中各种必须的基础设施,例如固定帧率渲染、鼠标键盘事件捕捉、图层、用户数据等,这些工具都是以最简单直接的方式提供出来,不要求OOP,没有预编译。

gin在设计之初就定位为一个“引擎”,而不是框架。就像汽车引擎不负责提供动力以外的事情一样,gin只专注于驱动游戏运转,不提供表现层的任何工具。

gin的特点

gin的特点是:简单、高效

使用gin只需要写如下的代码,十分简单:

$G('your-game-container-id', {}, {
    render: function(e) {
        // draw canvas with e.context
    }
});

点击这里可以看到一个完整而简单的使用gin的例子,源码看这里

相比其他现有的js游戏/绘图框架的设计思路,gin摈弃了传统的事件驱动模型,只提供固定帧率的回调接口,所有的鼠标键盘事件都由gin负责接收和缓存。gin的使用者可以在beforerender或render回调中集中处理所有缓存的事件,这样能最大化游戏性能,并提高整体游戏响应速度。

根据google chrome 8.0的profiling结果,在canvas绘图函数中数清除画布数据的clearRect()消耗CPU时间最多,画布越大性能消耗越明显,约是stroke()一个相同大小的圆或长方形耗时的100倍甚至更多。如果采用传统的事件驱动模型,游戏会立即处理接收到的事件,执行绘图、逻辑判断等等,这样会不断的清除画布,浪费大量的CPU,而实际上只要达到30帧/s就能有流畅体验,在真正需要绘图的时候再绘图才更合理。

而且由于现在所有javascript引擎都是单线程的,脚本执行时无法响应任何DOM事件,浏览器也不会缓存这些事件,如果脚本较长时间占用CPU,还会造成事件丢失,最终影响用户体验。

由gin来缓存事件还有一个好处,这可以让键盘鼠标状态检测变得更简单。gin在beforerender和render回调中传入的事件对象带有keyStates和buttonStates数组,分别对应键盘和鼠标的按键状态,可以支持多个键盘/鼠标按键同时按下的状态检测。

$G('your-game-container-id', {}, {
    render: function(e) {
        // check if 'blank' key pressed
        if (e.keyStates[0x20]) {
            // do something
        }

        // check if mouse L button pressed
        if (e.buttonStates[0]) {
            // do something
        }
    }
});

beforerender和render

gin将帧的回调函数分为两个,beforerender和render。这两个回调的唯一区别是beforerender参数e里面没有canvas context,不能用于画图。这样做的好处是鼓励使用者将与绘图无关的逻辑放入beforerender,让每次脚本运行的时间更短,降低丢失消息的可能性。

遍历鼠标事件

gin缓存的鼠标事件并不能直接暴露出来,这是因为gin支持图层,在不同图层里面,鼠标事件的clientX和clientY都会因为图层的偏移量不同而不一样。如果为每一个图层事先计算好正确的clientX和clientY并且缓存起来,那会造成很大的性能损耗。gin的做法是提供遍历鼠标事件的接口,并在遍历中计算正确的坐标。

$G('your-game-container-id', {}, {
    render: function(e) {
        if (e.buttonStates[0]) {
            // draw mouse move path
            var ctx = e.context;
            ctx.strokeStyle = 'rgb(0,0,0)';
            ctx.beginPath();

            e.traverseHistory(function(cur, prev) {
                // the first point
                if (!prev) {
                    ctx.moveTo(cur.clientX, cur.clientY);
                } else {
                    ctx.lineTo(cur.clientX, cur.clientY);
                }
            });

            ctx.stroke();
        }

        // cached mouse history must be cleared explicitly
        // history will not be really cleared until next beforerender/render is ready to call
        // so it's safe to clear history many times in one function or in other layer
        e.clearHistory();
    }
});

图层

gin虽然并没有把图层的内部类(GinLayer)暴露出来,但它无处不在,例如,所有的回调函数的this都是当前layer的实例。

gin默认会创建一个图层作为所有图层的基础,使用者可以在图层上创建无数的子图层,子图层可以继续嵌套更多子图层。每个子图层有自己的beforerender和render,可以独立的渲染。

图层可以用stop()方法来停止渲染,stop的图层和它的子图层的beforerender/render都不会被调用,直到调用play()让它们继续渲染。

$G('your-game-container-id', {}, {
    start: function() {
        this.layers('sample', {
            left: 0,
            top: 20,
            width: 100,
            height: 200
        }, {
            render: function(e) {
                // your code
            }
        })
        .layers('sample_again', {}, {
            play: function() {
                // this function will be called once the layer starts to play
                this.layers('sub_sample', {});
            }
        });
    },
    render: function(e) {
        // it's how to find layer, or even sub-layer
        var sample = this.layers('sample'),
            sub = this.layers(['sample_again', 'sub_sample']);

        // do something
    }
});

gin的未来

gin刚刚发布了1.0.0版,是一个just work的版本,并且发布了三个例子,放在gin-samples项目中。

gin未来将首先考虑支持更多的浏览器和平台,特别是移动设备上的支持。对于简单高效的引擎而言,最适合应用的环境就是性能相对较差的移动设备。

其次需要考虑的是实现更强大的图层(layers),现在的图层已经实现了类似于flash MovieClip的各种基本功能,还差key frame没有实现,未来会增加支持。

FAQ

Q: gin的api文档呢?
A: 暂时还没有,有问题请直接联系我(githubtwitter

Q: gin支持哪些浏览器?
A: 测试过的是Firefox 3.6、Chrome 8.0/10.0,理论上Opera、Safari也会支持。IE就别想了,等IE9支持了canvas再说吧。

Q: gin-samples里面的例子不能正常运行是什么原因?
A: 如果是下载源码运行,需要手动将gin.js拷贝到sample目录里面才能运行。此外,需要保证电脑可以访问网络,因为有些例子使用了jQuery,用的是google dns。如果还不行,请检查一下浏览器是否是gin所支持的浏览器,gin自己没有做任何浏览器检测。

附录

我在开发过程中了解过的游戏/绘图框架包括The Render EngineGameJScakejscocos2d,它们都是非常好的框架,gin从某种程度上说是针对它们的不足而设计的。 :P

gin最开始是我心血来潮写的一个小框架,还未完成就在“给力HTML5 —— 2011 Google HTML5训练营” 第一期活动中使用,非常意外且幸运的用它实现了第一个可玩的HTML5游戏Raiden 5 (Chrome only),在这里可以围观最原始的gin。

分类: 技术文章 | 18 条评论

游戏中的物品交换逻辑抽象

如果总结一下游戏的内部逻辑,或许会让游戏变得很无趣,不过程序员大概会很高兴,比如说我。

如果把用户的各种信息都抽象成“物品”、“个数”,那么所有的游戏逻辑都是用户之间的物品交换。

为了方便描述,我来自定义一套语法表示这种交换。这里用的是类BNF语法,如果没耐心可跳过。

物品交换 := 物品描述 “=>” 物品描述

物品描述 := 物品描述 “+” 单个物品描述 | 单个物品描述

单个物品描述 := 用户 “(” 物品 “,” 数量 附加属性 “)”

物品 := 物品名称 | 物品函数

物品函数 := rand “(” 物品列表 “)”

物品列表 := 物品列表 “,” 物品 | 物品

附加属性 :=”,” “{” 附件属性列表 “}” | “”

附加属性列表 := 附加属性列表 “,” 属性 | 属性

属性 := 属性名 “:” 属性值

数量 := 数字 | 数字函数

数字 := 非负整数 | MAX

数字函数 := auto_change “(” 初始值 “,” 每秒变化步长 “,” 边界值 “)”
| max “(” 数字列表 “)”

数字列表 := 数字列表 “,” 数字 | 数字

函数说明:

  • rand代表在一系列物品名称中返回任意选一个物品名称。
  • MAX代表该物品最大数量值
  • max代表从一系列数字中返回最大值
  • auto_change代表从初始值开始,每隔1秒时间就加上步长,直到达到边界值为止不再变化。auto_change并非如其名一般会自动变化,而是在下次请求该物品数量的时候才计算当前值。步长可以为负。

希望不要看这些BNF晕了头……直接看例子其实也可以。

列举一下常见的游戏场景背后的数字逻辑:

  • 用户A用100游戏币买一个桌子获得10点经验:
    • A (游戏币, 100) => A (桌子, 1) + A (经验, 10)
    • 注:这里把游戏币、经验都当做物品来处理,从程序角度来说这没有问题,虽然直观上有点怪。
  • 用户A使用10点幸运点打开一个宝箱得到一个物品,这个物品是玩具1、玩具2、玩具3中随机的一个:
    • A (宝箱, 1) + A (幸运点, 10) => A (rand(玩具1, 玩具2, 玩具3), 1)
  • 用户A送用户B一个玩具,留言“my present”,获得10点经验:
    • A (玩具, 1) => B (礼物, 1, {留言: my present, 关联物品: 玩具, 关联数量: 1, 时间: YYYY-MM-DD}) + A (经验, 10)
    • B (礼物, 1, {留言: my present, 关联物品: 玩具, 关联数量: 1, 时间: YYYY-MM-DD}) => B (玩具, 1)
  • 用户A在一号树坑种下一棵树,将在3000秒后长成桃子共4颗,其中最多2个可以被偷走,而B偷走了1个获得10点经验,A则收走了剩下3个,获得20点经验:
    • A (树种子, 1) => A (成长中的桃子, auto_change(0, 1, 3000), {土地号: 1})
    • A (成长中的桃子, 3000, {土地号: 1}) => A (私有桃子, 2, {土地号: 1}) + A (公有桃子, 2, {土地号: 1})
    • A (公有桃子, 1, {土地号: 1}) => B (桃子, 1) + B (经验, 10)
    • A (公有桃子, 1, {土地号: 1}) + A (私有桃子, 2, {土地号: 1}) => A (桃子, 3)
    • 注:当任何一个可以看到桃子树状态的用户刷新状态且第二步中输入的数值满足要求时,第二步物品交换就会发生。
  • 用户A接了一个任务X,用玩具1和玩具2,可以换得100游戏币和10点经验,任务有效时间为30,000秒,且A最多能接5个任务:
    • A (可接任务, 1) => A (任务X, auto_change(30000, -1, 0))
    • A (任务X, max(1, MAX)) + A (玩具1, 1) + A (玩具2, 2) => A (游戏币, 100) + A (经验, 10) + A (可接任务, 1) + A (已完成的任务X, 1)
    • A (任务X, MAX) => A (可接任务, 1)
    • 注1:A的初始可接任务设置为5就能限制A最多接5个任务
    • 注2:第三步为取消任务逻辑
  • 用户A将一个桌子从储物箱里面挪到1号房间,又从1号房挪到了2号房:
    • A (桌子, 1) => A (桌子, 1, {x: 10, y: 0, room: 1})
    • A (桌子, {x: 10, y: 0, room: 1}) => A (桌子, {x: 20, y: 10, room: 2})

仅仅通过这些很基本的物品交换逻辑,(理论上)就可以实现一个比较复杂的游戏数值逻辑了。只要实现一个服务,把所有这些可能的物品交换逻辑都放在某个配置里面,游戏核心就出来了。

这种做法也有不足,有些时候客户端需要从右边推导出左边的可选值就比较麻烦。比如物品1可以由游戏币或充值币其一换得,客户端需要根据物品1反查出游戏币和充值币各自需要的数量。这在技术上其实可行,只是需要花费更多精力去想明白里面的需求。

分类: 技术文章 | 3 条评论

Amazon AWS使用体验

近几个月来简单体验了Amazon AWS大部分服务,现在分享一下使用感受。

Amazon EC2

从各方面来看,EC2应该算是Amazon的王牌产品。EC2功能全面,它可以在数分钟内launch一个实例,可以(理论上)自动提升/降低机器性能配置,可以绑定EBS成为具有高可用性特质的计算核心,可以摇身一变成为ELB成为一个load balancer。这些功能非常实用,特别适合针对国外市场且快速增长的小网站,可以在前期完全不受机器约束。

EC2机器的内核在申请时指定,有很多选择,如果有闲心,还可以做一个自己专用的,我们没用这个功能,实在太繁琐了。EC2提供了完全的root权限,可以随意安装软件,由于EC2一般带有足够的硬盘空间(Large Instance就自带850GB磁盘空间),装个mysql当数据库机器也完全没有问题。

EC2最大优势是灵活,最大劣势则是性能。EC2的IO性能欠佳,当EC2机器作为mysql数据库时,系统吞吐量只能到3~10 MB/s,相比而言,在同样数据同样程序条件下,使用真实机器则能轻松达到30MB/s。EC2的CPU是多个实例共享的,一般stolen的CPU都在10%左右,对于nginx+php-cgi这样的组合,EC2高端配置也不太能撑住压力,总是CPU吃紧。

虽说EC2劣势明显,但由于互联网应用一般都很容易做到水平扩展,它的灵活性可以很大程度缓解这个劣势,不会有太大问题。不过最终我们还是放弃了EC2,主要因为今年5月Amazon云计算服务出了大量的事故,仅5月头两周,在同一个机房就出了5起大事故,我们在3起事故中受影响,每次都很不幸的都是单点EC2出问题,最长的一次在高峰期中断了6个小时服务,损失巨大。最郁闷的是,到了5月下旬,有一台跑数据库的EC2机器莫名奇妙磁盘损坏,还有一台莫名其妙的无法ssh,让我们感觉越来越不靠谱,于是决定逃离。云计算的可用性始终值得担心。

EC2的价格相对传统IDC要贵不少,按高端机器High-Memory Double Extra Large每小时1.2US$来算,每月就要花864US$,再加上流量费,很容易达到每台服务器1000US$的水平。

Amazon RDS

RDS是一种数据库托管服务,通过RDS toolkit可以很方便的创建RDS实例并立即使用。据Amazon帮助文档说,RDS的数据库软件维护、升级、优化的工作全部由Amazon负责,用户只需要使用功能即可。

可惜,RDS纯粹是一个看起来很美的东西。首先是RDS为了同时支持异构的数据库,牺牲了很多灵活性,例如,因为无法访问RDS的物理机器或存储空间,使得跑着mysql的RDS实例连记录slow log都成了难事,更别说什么err log,出了问题只能一抹瞎。如果要获取bi/bo这样的数据需要使用专用的RDS toolkit完成,很难集成到现成的监控工具里去。其次,RDS底层使用的是类似于EBS的网络存储,其IO性能差到一个令人发指的地步。一个很感性的数据是:使用RDS Double Extra Large DB Instance跑mysql时慢查询占1%,同样数据转到EC2 High-Memory Double Extra Large实例后慢查询降为0.01%。最让人无语的是RDS价格高于同等配置的EC2+EBS价格,每小时都贵超过0.3US$,相当于贵25%,省下来的钱用来雇专业运维人员绰绰有余,而且RDS功能和灵活性还远弱于后者,不怕折腾但怕花钱的人不建议选择RDS。

如果执意要使用RDS,那么一定要注意RDS没有固定IP,每次重启实例(或过一段时间)都会变,程序连接的时候最好写域名,当然,这会损失性能。此外,RDS可用性也没宣传的那么高,我们5月最先挂的就是RDS实例,一挂就是好几个小时。

Amazon S3

S3是少有的看上去物美价廉的东西,只需要花几美元就可以拿到几十GB空间,对一般的互联网应用来说绰绰有余。

S3提供丰富的API和工具上传文件。对firefox用户来说,有一个专门的S3扩展可用,可直接上传本地文件。还有一个s3curl.pl工具,可以用命令行传任意文件,可以方便的做到自动化。

S3存储的文件可以直接通过域名从外网访问,不过比较郁闷的是文件尾部不能带“?“,一切基于文件名后面加随机串的避免缓存的做法都会失效。

Amazon CloudFront

CloudFront是一种CDN,它的访问速度据称不错,我们并没有实际使用过。

我们不使用CloudFront主要是因为很难控制文件在CloudFront中失效。根据我们的程序结构,我们需要在静态文件后面加version信息强制用户在必要时更新到最新版,现在由于CloudFront既不能手动invalidate某个文件,也不能保证CloudFront上缓存的文件及时更新(可能更新会花24小时),我们需要修改现在的策略才能使用到它。

CloudFront是根据用户所在网络来收费的,对于国内,CloudFront使用的是香港价格,贵于美洲和欧洲,不太划算。对于欧美市场,我也不清楚欧美CDN的价格究竟如何,无法比较。CloudFront在欧美的流量费用与EC2相同,不过由于没有按小时计的租费,总体价格会比EC2提供静态文件服务便宜很多。

Amazon CloudWatch

CloudWatch是一个性能数据监视器,对于这个东西我只能说太小儿科了,没看出来有什么实际的用处。一旦开启CloudWatch,Amazon就会帮忙收集开启了这个服务的instance数据,包括CPU和IO信息等。可惜,数据精度不高(大概是几分钟一次统计),数字也只是个大概值,能看的数据也很少,无论对入门者还是高手来说,都不能带来太多价值。

CloudWatch看上去比较好的是可以配合EC2/EBS/Load Balancer toolkit做一些auto scale的事情,自动提升/降低EC2/EBS参数,还可以(理论上)自动launch新实例。这些东西我们都没尝试过,有兴趣的同学可以试试。

CloudWatch价格不贵,一台机器就只用0.1US$而已,不过一般还是不建议开,装一个其他监控软件更靠谱。

Amazon Premiun Support

Premiun Support包括即时的电话支持和一个专有的提案系统。从服务质量上来说,Amazon的客服总是很热心,响应速度也很快,专业素质也不错,很不错。

Premium Support非常贵,简直是抢钱。它是按照每月总花费来算价格的,最多增加总花费的16%,最少为无限接近于10%,代价十分明显。

结语

Amazon AWS提供丰富功能,还有不少toolkit和API,适合小应用使用。对于要求较高性能或较低成本的应用来说则不太适合,折腾一下IDC应该达到更好效果。

分类: 电子商务&互联网 | 12 条评论

amazon云计算服务初体验

本周第一次实际使用amazon的云计算服务,主要包括Amazon Elastic Compute Cloud (EC2) Amazon Relational Database Service (RDS) ,有一些感受先记在这里。

先说说EC2。它很类似网上的虚拟主机服务,拥有root权限,可以完全控制上面的服务,默认拥有一个对外的dns地址,并可以开放80和22端口。

从性能上来说,EC2的表现很不错。从功能上来说,EC2最大的限制就是端口和ip。对于端口,80和22只能满足最基本的web应用,如果要使用自定义端口/协议的socket方式提供服务,恐怕就比较麻烦了。对于ip,默认每个EC2服务器都是动态ip,最安全的方法是用自己的域名做CNAME转发来对外提供服务。有一点需要注意,EC2服务器的外部dns和ip是绑定的,一旦给它换ip,例如为它花钱绑定一个固定ip,它的外部dns也会变,这就需要修改自己域名的CNAME记录。可麻烦就麻烦在EC2实例换了ip后立即就换外部dns,而普通dns缓存则可能会很长,这将造成在换ip的这段时间内服务可能不稳定。为了长治久安,还是一开始就申请一个固定ip会比较稳妥呢。

EC2对外部流量收费,内部不收费,或者在跨区的内部服务器通信时收非常少的费用。EC2服务器单机流量上数十MB/s不成问题,不过随着提供服务的EC2服务多了起来,我们自己搭的load balancer服务器已经有点力不从心,看到EC2有专门的load balancer服务,并且还挺便宜,下周一定试一试。

相比EC2,RDS则非常的不自由,它不能用ssh登录,数据库程序也不能控制版本,还有不少数据库配置不能改。如果在RDS上使用mysql+innodb,那么有不少不爽的地方:

  • innodb_data_file_path配置不能自己控制。RDS会将这个设置为ibdata:10M:autoextend,且不能改,这对高并发的读写会造成问题。
  • 没法在本地以文件方式保存slow log。
  • 无法直观的监控机器性能,只有一些RDS专用的命令行工具能得到一些实时的数据。
  • 无法对操作系统进行进一步调优,甚至都不知道底层跑的是什么操作系统。

不过RDS也有一些好处:

  • 存储空间可以无限增长。就像flickr的相册一样,每月都有一定的增长额度,总存储空间可以无限增加。
  • 数据安全性可以保证,可以方便的制作snapshot和完整备份,备份所花的存储空间只要不大于每月增长额度就完全免费。
  • amazon会负责数据库软件的安全性,给它及时打补丁。

综合来说,RDS提供的控制手段有限,不太适合对性能有较高要求的应用。从实际是用来看,同样的程序和网络环境之下,Double Extra Large DB Instance上的mysql+innodb明显慢于Double Extra Large EC2上自己安装并优化的mysql+innodb,一个典型的数据是:使用前者,慢查询比例是1%,使用后者,这个比例立即降为0.01%。

放弃RDS也就意味着放弃RDS的各种好处,为了弥补这个不足,可以为EC2申请Amazon Elastic Block Store (EBS) 。EC2+EBS就和RDS没什么两样了,只是管理起来还是有点费神,综合的价格应该和RDS差不多。我这里还没有真正尝试过EC2+EBS的方案,以后等存储出现瓶颈了再说了,现在EC2自带的850G存储绰绰有余。

amazon的服务还是挺让我满意,特别是phone support反应很快速,服务人员也挺专业,挺好。不过我并没有其他云计算服务的使用经验,没有比较,也不清楚amazon云计算在业界究竟出于何种地位,这个以后再慢慢去了解吧。

分类: 电子商务&互联网 | 6 条评论

如何衡量每一次决策的价值

最近和朋友聊到这个话题,顺便发散一下,说说完整想法。

我首先觉得这是一个伪命题,不太可能有种方法能衡量决策的价值。决策有时效性,过了这个时间,决策的价值就发生变化,无法衡量。此外,决策也没有绝对的好或不好,往往都是各方权衡的结果。相对来说,决策的过程比较容易衡量,如果每次能按照最合理的过程来进行决策,决策本身就应该具有最大的价值。就能从侧面衡量决策本身的价值。

我认为合理的决策过程是:找到所有决策相关的人,推动大家在合适的时间达成一致。Find out stake-holders, and push them to reach a consensus at right time.

这句话非常的tricky,里面暗含很多意思。比如谁去负责push、stake-holders如何定义、right time究竟是什么时候等。

如果我是决策的最终负责人,那么负责push的人就应该是我,这个最容易回答。

要界定stake-holders就要看这个决策一旦制定该如何实施,所有在实施过程中会涉及到的关键人物都应该算作stake-holders,决策的过程应该是所有stake-holders buy in整个决策的过程。比如要决定一个feature是否应该做,首先可以假设要做,然后就可以知道需要有PM来决定feature细节、要dev决定如何实现、要team lead决定如何将这个插入项目计划之中,等等,这样就要引入相关的负责人来进行评估。此外,这个例子里面暗含一个角色,这个feature的提出者,他/她有可能是一个dev、客服甚至公司外的用户,他/她应该也以某种形式参与进来,直接参与或者由某个人(PM)代理。集齐所有stake-holders,每个人buy in这个决策的后果(该干活的干活、该承担风险的承担风险等),就可以让这个决策通过,否则,修改决策本身,继续讨论。

要确定right time非常困难,但可以有几个简单的衡量标准,比如要做出决策的各方面条件是否满足、决策本身是否依赖于某些时间相关的因素等,总的来说是一个仁者见仁的过程,需要决策的最终负责人来给出right time的预期。需要注意避免把right time看做是deadline,任何schedule都会遇到意外,而right time则不能出意外,deadline一定要早于right time,这样才能使得这个觉得真的right。此外,right time也不是ASAP(as soon as possible),过于草率的决策往往结果不会太好。

“如何做决策”是一个需要长期积累的技术活,我现在经验尚少,只有这些肤浅的认识,欢迎所有读者过来批评指导。 :)

分类: 工作&生活 | 3 条评论