《浪潮之巅》中主要的一个观点就是公司是有基因的,基因基本不会因为公司的成长而改变,而且决定了它的成败。

从这个观点上说,这次d3 推出后我在游戏中的体验更是对这个观点的一种体现。

我们来看,暴雪是个什么公司?它的强项是做单机游戏,一直以来注重的是玩家的直接的核心的游戏体验,宏大的世界观,精致且中世纪色彩浓厚的西方玄幻美术设计,题材总是标新立异非常吸引眼球。

我认为最重要的关键词有2个:精致的画面+单机体验

我认识暴雪是从魔兽争霸2 开始的。虽然画面从现在的角度看很挫,但当时还是非常惊艳的,更不用说当年星际刚推出时候的那种眼前一亮了。暴雪的美术相关设计及技术我认为一直保持着业界的领先水平,而且它总能在技术和效果上取得一个比较好的平衡,这次的D3,即使在所有效果都调到最低的情况下,玩家的游戏体验依旧没有收到非常大的影响,整个氛围没有收到破坏,依然能感觉到正在玩的是一个很酷的游戏。这点是暴雪一直在坚持做的事情,从它的第一个3d游戏war3 开始一直如此。高质量的美术质量的追求我认为是暴雪公司成功的因素之一。

暴雪也是从做单机游戏出身的,魔兽系列一直是单机为主,后来的版本加入了联机(包括星际1),而除了wow 之外,它没有做过任何的MMO(还有传说中的GHOST?星际2和暗黑3 应该不能算是MMO 吧)。而几乎所有的作品上,都残留着各种单机的痕迹。宏大的世界观,引人入胜的故事,跃然纸上的角色,种种单机的强项暴雪都一直在保留,从这次D3 的剧情来说,我认为依然是保留的很好,也是它成功的因素之一。

这两个基因为暴雪带来了很好的名声,但是我认为“单机”这个基因虽成就了暴雪,但估计也会是它失败的主因。

首先,暴雪花了12年的时间完成了d3,而从现在看到的效果来说,画面和引擎技术应该是在最近5年之内确定下来的,那么就是说之前的时间都是在做预研和方向的尝试,这点我想整个业界也只有暴雪还在这样的坚持了。这种高成本的追求质量是否能为暴雪带来利益呢?这个问题是值得思考的。星际2 也是暴雪耗费的大量成本的神作,技术上它实现了功能强大的shader,延迟渲染, 延迟光照,透明物体渲染,半透明阴影等,在画面上完全达到了一线游戏的水平,基本上没有网游能达到这个程度(国内能做到的全都称自己为NG网游了),可以说暴雪一直以它做单机版游戏的感觉来做游戏。但从全球的运营情况来看,星际2 在现阶段基本上算是一个不成功的产品,证明再nb 的单机技术也没法从根本上拯救一个网络游戏。

另外,由于暴雪必须要保证很好的单机体验,所以一直以来暴雪的游戏对网络的要求都非常的高,低于400的ping 基本上就无法进行游戏了。但这个要求把很多玩家拒之门外,试问我们有多少次在星际2中由于延迟而输掉一个胜券在握的战役,多少次在WOW的boss 战中各种lag 导致灭团, 多少次在D3中掉线导致和神装失之交臂。而在星际2中,战网就经常出现无法入队,无法离队,卡loading,成就丢失,存盘丢失等情况。而这些情况这次在D3 的服务器上也是完美的重现了。而D3 开服后不到1天,就创建了“error 37” 这个互联网热门关键词,全球服务器都定时瘫痪,而且几个小时都无法恢复。虽然从一个侧面反映了暴雪的人气,但另一个侧面暴露了暴雪在运营上应对突发情况下的经验严重不足及错误的低估了服务器所需要承受的压力。

许多公司的失败要不就是走的太快技术超前很多(例如当年的贝尔实验室),要不就是走的太慢无法顺应时势(例如北电)。游戏网络化是趋势所在,暴雪走这条路绝对没错,但过分偏重单机体验将是其在这条路上最大的绊脚石,或者说暴雪走的太前了。如何在保证体验的同时不至于要求苛刻,如何取得两者之间的平衡也许是暴雪还需要再想想的问题。同样也是我们需要思考的问题。

 

pyc 文件就是python 编译出来的文件, 那么它的内容是什么呢?

从源码推断pyc 基本由3个部分组成:(下面分析均以python 2.7 代码为准)

  1. 四个字节的magic number
  2. 四个字节的timestamp
  3. 代码(用marshal 转换的)

那么我们再来细看这3部分的内容.

a) 四个字节magic number

主要和python 版本有关, 多说无谓, 帖代码最直接, Import.c 中

/*….

Python 2.5a0: 62071
Python 2.5a0: 62081 (ast-branch)
Python 2.5a0: 62091 (with)
Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
Python 2.5b3: 62101 (fix wrong code: for x, in …)
Python 2.5b3: 62111 (fix wrong code: x += yield)
Python 2.5c1: 62121 (fix wrong lnotab with for loops and
storing constants that should have been removed)
Python 2.5c2: 62131 (fix wrong code: for x, in … in listcomp/genexp)
Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
Python 2.6a1: 62161 (WITH_CLEANUP optimization)
Python 2.7a0: 62171 (optimize list comprehensions/change LIST_APPEND)
Python 2.7a0: 62181 (optimize conditional branches:
introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
Python 2.7a0 62191 (introduce SETUP_WITH)
Python 2.7a0 62201 (introduce BUILD_SET)
Python 2.7a0 62211 (introduce MAP_ADD and SET_ADD)

#define MAGIC (62211 | ((long)’\r’<<16) | ((long)’\n’<<24))

"\r\n" 据一老外分析是防止pyc 被作为文本处理, 因为这样的话这两个字节会自动被readline 去掉.

b) 四字节timestamp

即文件的修改日期. 代码py_compile.py:

with open(file, ‘U’) as f:
try:
timestamp = long(os.fstat(f.fileno()).st_mtime)
except AttributeError?:
timestamp = long(os.stat(file).st_mtime)

时间戳主要是当源码有改变的时候python 就可以重新生成pyc 文件.

c)marshal的代码

剩下的内容就是通过marshal.dump 搞出来的二进制字节流.

我们知道pickle 也是可以做类似的事情, 但为何python 需要有pickle 还需要有marshal呢?

pickle 的出发点是构建一个与版本无关的序列化数据, 而marshal 是和版本强相关的. 官方建议序列化使用pickle 而非marshal, marshal 只是 (mainly) 为pyc 服务

根据文档, 另外的区别如下:

  1. pickle 会记录下(track) 已经被序列化的对象, 所以后面引用时它不会被再次序列化, 而marshal 不会做这种事情.
    这个行为导致最严重的结果是marshal 无法处理循环引用, 如果marshal 一个循环引用的对象, python 解析器会crash.
  2. marshal 无法序列化用户自定义class, 但pickle可以, 当然必须是这个class 必须可以import 的到
  3. marshal 是不会保证跨版本序列化和反序列化的.

marshal 还提供了一些有趣的功能, 例如把marshaled 的对象传给dis.disassemble , 即可得到在标准输出中得到相应的py字节码. 而marshaled 对象本身也提供了很多python 相关的元信息, 为分析python 代码提供了很多的信息.

这就引申开了一个问题, 从对pyc 文件的分析来看, 我想如果需要反编译pyc 文件应该也不是一件怎么难的事情, 所以pyc 并不是一个好的防破解机制, 对于python的字节码还没有去研究过, 不知道是否和LUA一样的容易打乱(自觉告诉我python 没这么简单….) 给pyc 再打一层包也许是一个办法, 但如何选择一个好的加密算法是个问题

 

pylint 我们一直都在用, 很好的一个py 语法检查器. 一直好奇它的原理, 这周正好要做一个它的扩展, 于是就去了解了一下.

python 的词法分析用的是语法树, 而pylint 就是用抽象语法树(Abstracted Syntax Tree, AST) 来分析语法. 那么pylint 是如何做到的呢? 我一步一步的查下去.

在阅读pylint 代码的过程中, 发现其中用到最多的是一个模块 logilab.astng. pylint 就是用来获得更有用的AST 信息以供语法分析之用.

那么logilab 又是怎样得到python 的语法信息呢? 原来在python 的compiler 模块中提供了一个ast 模块, 专门用于提供这个信息 (这个对python 2.6 以下版本有效, 还有些bug 没解决, 而2.7 之后去掉了这个模块, 由ast 这个独立模块来提供更好的语法树)

例如对下面这段代码,

"""This is an example module.
This is the docstring.
"""
def double(x):
    "Return twice the argument"
    return x * 2

compiler.ast 能给出这样的信息:

Module('This is an example module.\n\nThis is the docstring.\n',
       Stmt([Function(None, 'double', ['x'], [], 0,
                      'Return twice the argument',
                      Stmt([Return(Mul((Name('x'), Const(2))))]))]))

不复杂, 大致也可以看出它就是一个树结构.

这里简单说一下python代码的编译执行过程:

  1. Tokenizer 进行词法分析, 把原字符代码分解为各个Token
  2. Parser 根据Token 构建CST (具体语法树)
  3. CST 转换为AST
  4. AST 编译成字节码
  5. 执行字节码

而由于具体语法树和语言强相关, 而且许多实现细节跟分析无关, 冗余数据多, 所以语法分析器用得最多的还是抽象语法树, 而logilab 就是根据从python 编译器得到的AST 构建内容更丰富的自己的ASTNG( NG 指的是next generation ) . 树的遍历有很多种方式, logilab 实现了visitor 模式, 提供了walk 方法, 遍历树, 然后根据节点类型调用回调函数的方式. 回调函数可以是visitor_callfunc, visitor_function, visitor_lambda 等.

假如要扩展pylint, 最优雅的方式是实现自己的一个checker 类, 然后注册到pylint 中, 在遍历文件的时候被执行. 这里就涉及到如何去写pylint 的checker.

pylint 的checker 有两种, 一种是raw 的, 就是直接处理代码文本, 这个做代码风格检查时候会用到, 如多一个逗号, 行数太长等; 另一种是astng, 当涉及到语法检查时不得不用到, 如检查上下文关系, 检查变量使用情况等.

第一种的例子:

class MyRawChecker(BaseChecker):
    """check for line continuations with '\' instead of using triple
    quoted string or parenthesis
    """

    __implements__ = IRawChecker

    name = 'custom_raw'
    msgs = {'W9901': ('use \\ for line continuation',
                      ('Used when a \\ is used for a line continuation instead'
                       ' of using triple quoted string or parenthesis.')),
            }
    options = ()

    def process_module(self, node):
        """process a module

        the module's content is accessible via node.file_stream object
        """
        for (lineno, line) in enumerate(node.file_stream):
            if line.rstrip().endswith('\\'):
                self.add_message('W9901', line=lineno)

def register(linter):
    """required method to auto register this checker"""
    linter.register_checker(MyRawChecker(linter))

ASTNG的checker:

class MyRawChecker(BaseChecker):
    """check for line continuations with '\' instead of using triple
    quoted string or parenthesis
    """

    __implements__ = IRawChecker

    name = 'custom_raw'
    msgs = {'W9901': ('use \\ for line continuation',
                      ('Used when a \\ is used for a line continuation instead'
                       ' of using triple quoted string or parenthesis.')),
            }
    options = ()

    def process_module(self, node):
        """process a module

        the module's content is accessible via node.file_stream object
        """
        for (lineno, line) in enumerate(node.file_stream):
            if line.rstrip().endswith('\\'):
                self.add_message('W9901', line=lineno)

def register(linter):
    """required method to auto register this checker"""
    linter.register_checker(MyRawChecker(linter))

说点题外话, 看完pylint 后, 我不得不说python 确实是一个很完备的开发语言, 语言本身提供了各种的库让开发的过程变得简单, 开发代码的效率和质量上较有保障, 这点上现在多数的脚本语言是不具备的, 但性能问题也是python 最大的硬伤. 我一直坚信语言没有好坏和对错, 每个语言有每个语言的应用场景, python 适合用于逻辑快速开发, 性能不敏感的场合, 适合做逻辑变化快, 由于开发效率高, 推倒重来成本也完全可控. 逻辑固化且性能要求高的场合并适合用python 做开发.

 

性能优化做了一段时间, 也做了一些事情, 应该总结一下, 纯属规整一下自己的一些思考, 好继续前行.


一般来说, 我们所关心的性能主要分两个维度: 时间, 空间。 这两个维度是互相影响和制约的。 另一方面, 我们的性能还受硬件的制约, 我们不可能追求永无止境的性能, 就像在CPU 只能每秒运算10000 次的情况下,我们不可能追求它能承受一亿次的运算压力。 而如何在这两者上选择一个平衡, 就是我们所需要去思考的。

不同的项目,对性能和空间的要求肯定不一样。由于现在内存白菜价,而且我们游戏又跑在64位的系统上, 所以总体上来说,我们的空间是不受太多 的限制。而虽然我们的host 有双位数的核心,但由于我们的game是单进程单线程的结构,所以业务逻辑只能分到1个CPU上。因此我们更多是放在了时间这个维度上进行优先考虑。

如何衡量我们在时间上的花销, 我使用了2个工具:gperftools, python的hotshot。 它们都是测量全局函数调用消耗的,它们的原理在之前的周记已经说过,就不再赘述了。它们分别是用于测量C层及python层的调用消耗。图表输出用的是 gprof2dot。

具体操作中, 做了一下的事情:

  1. 根据用例,编写自动化测试脚本。
  2. 用hotshot 分析python 层的消耗。
  3. 通过图表观察是否有逻辑热点,看热点是否存在冗余逻辑
  4. 把可能逻辑迁移到C层处理
  5. 再通过gperftools 分析C层逻辑
  6. 同样,通过图表观察热点,看是否可再优化

在优化的过程中, 第一点是最关健的。如何编写可重用的自动化测试脚本比想象中重要的多。 因为性能测试是一个迭代的过程,需要不断的调整,不断的验证。曾经就吃过这样的亏,在没有可重用脚本的情况下,每次验证都会是噩梦。而如何构建一个可重用的测试环境也用掉了许多的时间,从构建测试场景,选择测试数据,编写测试逻辑等。因为涉及的系统比较多,如场景编辑器,布怪编辑器等,基本耗掉了50%的时间。

而迁移逻辑到C 层是另外的一个比较繁琐的地方。由于需要把数据从python 迁移到C 层,需要做一些工作。还好之前调整序列化的时候已经完成了很大一部分的工作, 让这个任务变得简单了许多。

期间碰到的一些问题和经验:

  1. 有时候CPU 上不去,总是在80~90 之间徘徊。曾经很高兴,人数压上去了,但CPU 还没满。但慢慢就觉得诡异了,因为我们不可能承受那么多玩家。后来发现问题:等IO把CPU 的压力分散了。空欢喜。
  2. 编译选项-O3 对性能有一定的好处. 从测试结果来看,这个影响大概在10% 左右.
  3. 使用tcmalloc 对性能有一定的效果. tcmalloc 号称比libstdc 的要好6倍 . 但从实际的效果来看并没有如此之夸张, 因为我们并不是不停的在malloc 和free ,但是对性能有一定的好处, 对于在线影响也在10% 左右. 使用了它之后, 基本上可以丢弃自己实现的freelist. 据说它在多线程的情况下优势更加明显, 这点没有去验证.
  4. python的-O 基本无效果.
  5. io 的消耗比预期要大的多. 这点也许和测试用例有关, AOI 密集的时候, 由于经常要同步各种协议, 所以IO 很高. 在gperftool 下看, io的消耗占到了总消耗的70%.

基本上现在在做的是在现有基础上的调整, 优化比较浅. 没有作算法及结构上的重构. 假如再进一步无法满足我们的需求, 也许要做更深层次上的调整.

性能优化是一个长期的过程, 现阶段取得了一点点的效果, 需要做的事情还有很多.

Mar 092012
 

我喜欢看书, 但看得不多. 工作之前还看一些, 工作后, 总是各种理由, 各种借口, 慢慢就少了.

日本人写的书看得更少, 不喜欢国外文学, 主要是对翻译质量不放心, 但日文比英文还是靠谱一些.

村上春树还算是我比较喜欢的仅有几个日本作家之一.

这两年貌似作品少了, 可能有了一定地位的人都这样吧, 也许是怕写了垃圾坏了自己的名声?

也许是灵感的源泉枯竭了? 也许…

1Q84 算是他的新书吧? 据说是他为了创新做的一种尝试, 好像是花了7年才憋出来的

看了大概2/3, 感觉还是写的很好.

最佩服的是村上的想象力, 这次和之前的作品很多不一样的地方, 看得出来他还在挑战自己, 作为日本文坛的一个老大级人物, 还在挑战自己实属不易.

全书用的是第三人称, 用了双线的手法描述一对主人公不一样的人生. 两条线基本平行, 时而交错.

而"羊" 这个动物一直是村上喜欢用的, 总是带着点邪气的一种神秘的存在.

故事充斥着各种未知与刺激, 小小人, 刺杀, 一夜情, SM, 偷情, 两个月亮, 平行世界, 宗教… 村上为这部作品加入了许许多多很新鲜的元素,

全书从头至尾都带着许多的悬念, 引着读者不断的深入, 直到不可自拔.

而对人物, 环境和心理的描写非常细致, 甚至到角色穿怎样的格仔裙, 抹什么样的口红, 喝那个品牌的红酒, 听那个作曲家的曲子都有着细致的描写.

全书充斥着日本作品那种淡淡的忧郁, 这种感觉是日本作品才有的, 就像他们的音乐和动漫一样, 想模仿都很难, 喜欢看日本作品的同学也许就是为了这种的味道吧?

总之, 1Q84 是一部不错的日本长篇小说, 对日本文化有兴趣的童鞋不妨一试.

 

只要是游戏, 就免不了要存盘, 每做一个游戏都会为这个功能纠结很长的一段时间, 纠结于如何去选择存盘方案.

存盘系统, 追求的最多的是数据一致性和性能的一个平衡. 必须理解到, 两者是不可兼得的. 为了追求其中一方, 必须放弃另外一方. 这应该也是一条定律吧, 可惜能力有限, 我无法用很系统很科学的方法去证明它, 我是这样相信的.

对于数据的序列化, 基本上使用的都是把使用的内存数据, 如lua, python, 转换成表达数据的字符串格式.如大话系列的lua 语句, 和我们现在的protobuf 字符串形式.

对于存盘, 曾经使用过的方式有两种:

  1. 磁盘文件. 这个应该是西游系列的传统存盘方式. 优点是简单, 依赖于linux 系统强大的shell 功能, 扫档, 对比都比较方便, 但是也有致命的地方就是IO 总是会成为问题. 无伦是存盘还是扫档, IO 总是需要考虑的地方. 而且读写冲突也需要自己解决, 所以也是有它麻烦的地方. 但是西游系列已经有长期的运营经验, 许多问题都有经验的积累, 所以基本上也不会有太多的问题.
  2. 数据库. 曾经做的项目用过NoSql 的数据库做存储, 不过由于对数据库没有太多的经验, 使用的方式也是和文件类似, 把整个玩家数据序列化成字符串后用uid 做key, 放到数据库里面. 其实也没有太大的优化. 所知道的更多的项目都是用了关系数据库作存储, 知道的大概有天龙, tx2, 倩女等. 用SQL 的优势也是明显的, 业界和公司其实对数据化的优化做了很多的研究, 当碰到问题一般能够在公司内部或者互联网上找到相应的解决方案.

而如果要做到即时存盘, 考虑最多的是CPU 和IO 的压力.

让我们来拆分一下基本的存盘流程:

读取内存数据 -> 序列化 -> I/O

具体到我们现在的存盘流程:

  1. 获取python 对象的变量到C 的内存空间
  2. C 内存序列化到protobuffer (PB) 的二进制形式
  3. 通过socket 发送PB 的二进制数据到database 进程
  4. database 进程将PB 的二进制数据还原
  5. database 进程将还原后的PB 数据序列化成字符串形式
  6. database 进程将字符串数据写入磁盘.

读取时将上述流程重新做一遍.

可以看到, 我们的步骤其实是有不少繁琐的阶段的. 针对每一步, 我们都可以做一定程度上的优化.

对于实时存盘, 考虑到的方案有2种:

  • 一般来说, 序列化是存盘中占用cpu 最高的.
    优化它有两个方式, 一个是优化算法, 一个是利用现在多核的优势, 把负载均衡到空闲的CPU上. 而把序列化进程独立出来的方案中, 最难的是如何为当前需要存盘的数据做一个镜像(snapshot). 而为了对应用透明, 云风在博客上提出过一个用fork, 利用linux 系统的CoW(copy on write) 机制为进程制造内存镜像的方法. 通过fork 把CPU 分散到另外的一个CPU 上去.
    这个方案对资源是最苛刻的, 因为在最极端的情况下, 内存使用会double, 即整个进程空间都被改写(当然这个情况非常罕见).
    为解决这个问题, 最简单的方法就是缩短这个fork 出来子进程的生命周期, 尽可能短的完成所有序列化. 然后销毁, 那么CoW 产生的消耗就会在可接受的范围内. 而为了把CPU 的消耗都尽量纳入这个进程中来, 把上面流程中的2,4,5 合并到这个进程做, game和database 中间通信的数据就已经是可以用于存盘的字符串.
    除了对资源的消耗外, 这个方案由于是多进程, 于是就需要考虑多进程间通信的问题, 序列化后的数据如何传回来父进程. 这个的方法很多, 效率最高的是共享内存, 把同一段内存映射到两个进程中, 序列化进程结束后通过某种方式, 如信号, 通知父进程读取即可. 但权衡性能和代码复杂度得出来最优的方案估计是Unix Socket来做通信. 经过测试, Unix 的内建socket 可以达到TCP localhost 速度的1/3 以上.
    这个方案的优点就是对上层代码完全透明, 整个存盘流程完全不需要修改, 上层也不需要知道下层的这个修改. 某种意义上符合了开放封闭原则(OCP), 基本上一次做好, 后面基本不需要维护. 从这个角度上, 它是优雅的.
    但由于涉及到了多进程通信, 而且对于IO来说, 由于通信的数据从二进制变为了字符串, 无形中增加了网络IO 的消耗. 但是由于到database 是内网, 对gateway 的通信走的不是一个网络, 所以只有可能影响到的是master 的通信. 而game 到master 的通信并不是非常频繁, 所以在网络io 上的消耗应该是可以接受的.
    这个方案实现难度比较低, 本周已经做了demo, CPU消耗的迁移效果还是比较明显, 可以做到每30秒存1000个玩家(满物品).
  • 想到的另一个方案是侵入式的, 必须改变现有的代码, 甚至改变存盘的数据格式.
    第一步, 是优化python到C 的数据复制. 为存盘数据建立一个C 内存空间的镜像, 在python 数据改变的时候, 同步一份到C 的镜像中. 把序列化时候取python 数据这一步从集中变为分散到逻辑的各个地方去, 以去除集中式的CPU 消耗. 这个从一定程度上就做了数据改变的diff, 存盘时不需要做一个数据的全拷贝.
    第二步, 做数据diff. 对数据的改变做diff, 每次改变只发送diff 给database 进程, 从而减少IO. 但对于复杂结构的操作, 例如物品的增删, 需要单独写逻辑. 并且无法对上层逻辑不透明(比如,删除某个特定物品的diff 很难用统一的方式描述, 必须由应用指定数据的改变)
    第三步, database 建立和game 相同的数据镜像. 由于数据的diff 都已经发送到database 来, 理论上完全可以重建和game 相同的一个数据镜像.
    第四步, 持久化. 由于db内存中存在一份相同的镜像, 所以即使当game crash 后, 数据并并不会丢失, 只需要定期把内存数据序列化后写到磁盘中即可. 当然db 必须保证最高的稳定性(谁挂, 它都不能挂)
    这个方案从CPU 和内存使用上来说都应该是比较优的. 缺点就是对数据改变的diff 实在很难描述, 无法做到对上层透明. 实现起来难度也很高.

两个方案各有优劣. 选择使用那个还需要继续论证.

最近主要是考虑这个问题, 想的也比较多, 思绪有点乱, 写出来主要是做个记录, 让思想在这里停留. 还需要再整理一下.

 

最近要搞MySQL 相关的工作, 工欲善其事必先利其器, 简单了解了下MySQL 的架构

MySQL 架构

image

分了4层:

    1. Connection/thread handling. mysql 为每个连接创建了一个线程, 并且整个过程都在此线程内完成. 而线程会被缓存, 不会为每个连接创建一个新的. 这层还会把认证及权限的工作做了.
    2. Query cache/ Parser. select 指令到达后, 会先去cache 中, 看是否有命中记录, 有即直接返回结果. Parser 会解析并创建一用于描述指令的数据结构(parser tree).
    3. Optimizer. 这里会对parser tree 进行一系列的优化, 如 重写查询, 更改指令顺序等. 可以在查询中添加参数以修正优化的结果和行为. 并且可以输出记录以供指令的优化.
    4. Storage engines. 负责存储和读取所有mysql 的数据. 其中包括有性能优先, 空间优先等不同的引擎, 可以根据不同的应用场合选择.

数据一致性

MySQl 读写都有锁. MySQL 服务器及存储引擎都提供了多种锁的方案.  常用的lock 方式有2个:

    • table locks. 最基本的锁方案. 写的时候会锁表, 其他读写操作都会阻塞, 直到操作结束. READ LOCAL 可以允许某些写操作. 而写操作有着更高的优先级, 即使处理队列中有读操作,  也会优先处理写操作.
    • row locks. 是并发性最好的一种锁, 但也是消耗最多的. 只能由存储引擎支持,  一般认为只有InnoDB 和 Falcon 支持这种操作. 

事务性:

事务性的衡量指标主要是 ACID (Atomicity, Consistency, Isolation, and Durability). MySQL 主要靠存储引擎做到事务性, 但服务器本身也要做许多的事情以达到事务的目的. 而事务性本身会占用更多的cpu, 更多的内存, 更多的磁盘空间. 所以是否需要事务性是选择存储引擎的一个重要的指标.

在一个事务中存在更新同一行数据的话, 可能出现死锁问题, 各个引擎实现有不一样. 某些存储引擎会超时, 某些会做检测, 如InnoDB. 而发生这种情况都需要rollback, 死锁很难避免.

事务性分4个等级: READ UNCOMMITTED, READ COMMITTED ,  REPEATABLE READ , SERIALIZABLE 具体意义参考手册

事务log. 基本上所有存储引擎都这么做: 先把数据写到内存中, 并记录log到磁盘上, 最终(可能在某个时刻) 更新到磁盘的数据库上. 这也意味着一次事务将至少写2次IO

MySQL 默认使用AUTOCOMMIT, 就是说每条指令都将默认在一个事务中进行, 除非你明确指定.

不要在一个事务中混合使用支持和不支持的存储引擎.

InnoDB 可以指定使用LOCK

    • SELECT … LOCK IN SHARE MODE
    • SELECT … FOR UPDATE

也可以使用服务器提供的:

    • LOCK TABLES
    • UNLOCK TABLES

但这两条指令很危险, 容易带来性能问题. 所以除非在事务中, 并且AUTOCOMMIT 不生效的情况下谨慎使用它们.

多版本并发控制(Multiversion Concurrency Control):

一些存储引擎如InnoDB, Falcon, and PBXT 支持这个技术. 可以不锁读取非在写的行, 所以意味着更高的并发性, 但同时带来更高的消耗(数据镜像及各种检查).

数据引擎:

MySQL 会为每个表 建立一个同名文件于文件系统中, 并以.frm 为后缀, 主要记录着表的元信息.

 

周末被邀请回答了一个知乎的问题, 话题正好也是我一直想说说的, 就转到周记里吧. 提问如下:

印度人针对中国人而产生的优越感到底是从哪里来的?

在Youtube上看了几个印度人上传的视频,例如:Why China hates India?Why India doesn’t hate China?之类的(还有几个没细看),视频中充斥着对中国的嘲讽、无知、和嫉妒。我承认中国确实有不少社会问题亟须解决,但印度有什么资格蔑视中国?除了 IT 业发达些(前英国殖民地),请知友们再列出几条印度的优势。


我:

首先, 要说明的是, 我认为每个民族都有莫名的优越感. 中国人这样, 美国人这样, 韩国人, 日本人, 印度人都是人, 也是相同的, 优越感都是莫名的, 不好解释.
另外, 一两个视频不能代表一个民族是否对另一个民族有没有偏见, 从我接触到的印度朋友来说, 没有这种感觉, 也许是他们和中国的接触比较多的关系.
我认为一切偏见都是源自不了解. 中国也有不少人很仇视"以美帝为首的"西方社会, 那么引用楼主的一句, 中国人又 "有什么资格蔑视" 他们呢?
因为所供职的公司的关系, 曾经常驻印度1年的时间. 就只说一下我看到的一些优点.

  1. 民主. 印度从上世纪开始就进入了民主社会, 政府是选举出来的, 民众有结社自由. 各种党派无数, 一到选举的日子, 就看到满街的宣传横幅, 各种候选人的头像就这样挂在街道的两旁, 在国内一定是要作为"影响市容市貌"的典范被销毁. 当然, 也带来了一些问题, "效率低下". 记得当时公司旁边的一立交桥, 来的时候那个样, 走的时候还是那个样, 据当地呆了4,5年的老员工讲, 这个桥从他们来的时候就开始建了. 据说是当地议会到现在都没讨论出来结果, 所以就停工了. 这种事情在习惯了"中国速度"的我们眼中是不可思议的.
  2. 信仰自由. 和大多数人想象的不同(太多人问我印度的佛教怎样怎样了), 印度最大的教不是佛教, 而是印度教. 除此以外各种教派很多, 宗教偶像也很多. 没到周末, 出了商业街之外, 最热闹的应该就是庙宇. 每次和他们出去team building 总是免不了要去几个庙宇祈福. 有信仰的一个很大的好处就是心灵有避风港. 有了问题, 有泄愤的地方, 心理压力也小一些.
  3. 金融业成熟. 生活中的表现就是城市里一个很小的商店也可以刷卡. 个人支票被广泛使用, 退税制度非常完善. 货币兑换市场化, 这也给我们这些"外国人" 带来了很多的便利.
  4. 再说一些小的点: 在各种公司都会见到一些行动不便的残疾人, 因为只要聘用他们, 政府就会退税给公司. 而跨国公司好像是要求有一定比例. 医院是福利组织. 记得一次半夜同事身体不适陪他去医院, 当天很冷, 刚进医院大堂就发现睡了好多衣衫褴褛的露宿者, 原来是躲到医院来了, 在我们的地方, 估计门都进不去就被保安赶走了.

有好, 肯定也有不好了

  1. 贫富差距悬殊. 我们办公的五星级酒店的后面, 就是一个很大的烂尾工地, 挖地三尺, 就烂掉了, 里面有好多用废品搭的帐篷, 住着社会最底层, 对比很强烈. 街上有奔驰宝马, 同时有人食不果腹. 但是奇怪的是很少看到新闻有"群体事件"的报道, 我个人觉得是因为宗教信仰的关系.
  2. 效率低下. 除了上面说到的政治原因外, 他们做事情确实不如国人勤劳, 一个中国人1天可以做完的事情, 交给他们需要2,3天, 而且不是个例, 很普遍的现象.
  3. 基础设施落后. 由于资产私有化, 政府没有任何权力侵犯个人资产, 所以开路非常难, 当时我在的bangalore也算是印度第5大城市, 但没有看到一条像样的路, 最宽的路4车道, 比起我们现在动不动就8车道的路来比, 它们确实非常小气. 各种产业园也很难建设, 原因还是上面说到的私有化问题导致. 不过这点我有点搞不清楚是优点还是缺点. 诸君明察.

乱七八糟说了一堆, 都是带着我自己偏见的文字. 其实一直想写点自己对印度的感受, 就趁这个机会吐槽一翻, 不知道对问题有否帮助. 谨以这些文字作为对那段日子的一些纪念吧.

 

最近都在做服务器端的性能测试, 主要在做profile, 业界用的最多的估计就是万能的valgrind, 我们的引擎还嵌入了google的google-perftools. 具体的测试数据会在这周完成, 这里只记录下我对这两个工具的原理理解

Valgrind

这东东是linux 上很出名的开源性能测试工具, 主要的功能是测试内存, 还提供了cpu cache命中, call 统计, heap 测试, 线程测试等功能, 灰常强大, 所以应用面很广.

valgrind 现在已经可以算是庞然大物了, 手册都有300页, 代码没仔细看, 只简单了解了下它的原理.

valgrind 的执行不需要重新编译, 只要把二进制作为参数传入即可. 运行时会先把binary的debug 信息(估计主要是符号表)都读到自己的内存中, 用于结果的输出, 否则看到一堆地址, 估计谁都抓狂. 然后在内存中根据当前运行环境为找到对应的tools , 并根据tools 参数选择交给哪个继续执行, 默认是memcheck. 而最关键的是valgrind 的core, 程序的所有cpu指令都将被core接管, 再真正被执行. tools 会对指令进行处理, 插入自己需要的指令后, 再交给cpu, 以达到测试的目的.

由于接管了所有cpu指令, 所以valgrind 不但能测试到自有代码, 还会接管系统库的代码. 不过这也是比较头疼的, 因为输出的结果会包含很多我不关心的数据, 还好的是valgrind 提供方法去过滤这些信息. 但由于插入的代码量是根据工具不同不一样, 而且所有调用以及内存访问都可能会被处理, 所以程序会变的很慢, 官方数据是10-50倍的减速… 所以每次valgrind 都是很折磨的, 特别是我们这样的即时战斗游戏, 技能会出现很诡异的效果. 所以用valgrind的测试也许不是非常适合我们游戏.

Google-perftools

这个最早是google内部用的性能优化工具, 最出名的是它提供的tcmalloc, 号称在多线程的情况下是ptmalloc的10倍性能提升, 峰值出现在16 threads, 分配256k字节数据的时候. 再往后性能差别不大(参考 tmalloc 性能测试报告). 但我们没有用到这个模块, 用的是它提供的cpu profiler.

google的cpu profiler (下简称pprof, 本来想用gprof, 但又和GNU的prof 名字冲突了, 就用了它提供的一个工具的名字)用的是取样的方法, 这个方法我曾经在stack overflow上看到一个老外说的(@maguschen 同学推荐的). 老外说的方法更原始, gdb 调试程序, 然后随意Ctrl+C, 对断点所在函数做个统计就可以知道程序的热点大概在什么地方了. 很有道理! goolgle 的这个profiler 完全就是根据这个原理实现的.

由于这个工具的代码不大(src/profiler.cc), 网上也有一些指引, 所以我是大概把原理过了一遍的, 大致如下:

  • 必须把profiler的库连接到程序中. 因为pprof 的初始化是通过一个库中的static 变量来实现的.
  • 在类的cstor中,会调用Start
    capture_004.png
  • 其中重要的函数是StartTimer, 它启动了一个定时器,
    capture_001.png
  • 还有一个重要的函数是EnableHandler, 它注册了SIGPROF, 下面是这两个函数的实现(src/profile-handler.cc ):
    capture_002.png
  • 可以看到, 默认是启动了一个1000000 usec 的定时器, 定时器fire 的时候会发出SIGPROF 信号, pprof 响应了这个信号. 所以被测试的程序不能接管这个型号(这点是我的猜测, 没有通读代码, 不敢说这个一定成立, 但尽量不要这样做)
    而下面这段就是pprof的精华所在了:
    capture_000.png
  • 这里使用了libunwind库, 把现在的调用栈dump出来了, 然后放到collector中, 可以看到pprof 是异步输出的, 数据到将来的某个时刻才最后输出(一般是程序终结的时候, 也可用ProfileFlush函数把内容flush 到io去)

可以看到pprof 是基于采样, 和最上面提到的stack overflow上面提到的老外是一个方法, 所以pprof的结果是不准确的, 应该说是提供了一个统计学上的意义, 我们知道采样的可信度是和样本空间的大小是相关的, 所以如果想要pprof比较准确就需要较大的样本空间, 这可以通过2个方法做到:

  1. 增加测试时间;
  2. 增加采样密度;

而用pprof最大的感受是一个字 — "快", 和许多profile 工具不一样, pprof 是很快的, 基本上感觉不到性能的下降, 甚至可以在生产环境只用它.

 

上周5 听了C++11 的一次介绍讲座, 这里就随便吐槽一下.

C++11, 2011年4月通过ISO appoved的一个C++标准. 当第一次听到这个标准的时候, 我有点迷惑, 因为虽然我现在对这个语言不再感冒, 但毕竟曾经在它身上耗费了不少的光阴, 如同相恋多年最终还是无奈分手的恋人, 时不时还是会关心一下, 而却几乎没有注意到这么一个标准. 众所周知, ISO 要通过某个标准的过程是异常漫长的…

google了一下才知道, 原来这个标准就是之前的C++0x, 太坑爹了, 据说是为了和以前的命名规则一致, 于是改名叫C++11. 小样, 换了个马甲就不认得你啦?

不过这次C++11 确实是这帮老人家们费尽脑汁想出来的一个非常革命性的C++版本. 甚至连C++之父, Bjame Stroustrup 童鞋也不禁要说, C++11 就像一个新的语言. 那么在吐槽之前, 先简单看看这次都加了什么相对比较"强大"的功能. 小的就不提了, 而且我也不了解, 想知道的去看标准文档吧: http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=50372 一如既往, 这标准的pdf文档份量十足, 12Mb多, 怎么看怎么吐血… 记得刚毕业的时候, 年少无知的我还曾经打印过一本, 足足牛顿高阶字典一般的厚度, 拿在手上有无比的满足感, 可看的时候就怂了…

lambda表达式
[capture](parameters)->return-type {body}

[]里是函数调用的参数列表,表示一个Lambda表达式的开始,让我们来看一个Lambda例子:

假设你想计算某个字符串包含多少个大写字母,使用for_each()遍历一个char数组,下面的Lambda表达式确定每个字母是否是大写字母,每当它发现一个大写字母,Lambda表达式给Uppercase加1,Uppercase是定义在Lambda表达式外的一个变量:

  • for_each(s, s+sizeof(s), [&Uppercase] (char c) {
  • if (isupper(c))
  • Uppercase++;
  • });
  • auto和decltype

    在C++03中,在声明对象时,你必须指定对象的类型,然而,在许多情况下,对象的声明包括在初始化代码中,C++11利用了这个优势,允许你声明对象时不指定类型:

  • auto x=0; //x has type int because 0 is int
  • auto c='a'; //char
  • auto d=0.5; //double
  • auto national_debt=14400000000000LL;//long long
  • 相反,你可以声明下面这样的迭代器:

  • void fucn(const vector<int> &vi)
  • {
  • auto ci=vi.begin();
  • }
  • 也可以使用decltype 关键字来推断返回值类型而用于声明中:

  • const vector<int> vi;
  • typedef decltype (vi.begin()) CIT;
  • CIT another_const_iterator;
  • Delete 和 Default 关键字

    C++11 加入了delete关键字, 比如我们知道编译器一般都会为每个类对象生成一些默认的函数, 比如默认构造函数, 默认析构函数, 默认赋值函数等, 而在我们不希望编译器为我们生成这样的函数的时候, 可以使用delete这个关键字给这些函数"赋值", 例如:

    struct NoCopy
    {
        NoCopy & operator =( const NoCopy & ) = delete;
        NoCopy ( const NoCopy & ) = delete;
    };
    NoCopy a;
    NoCopy b(a); //compilation error, copy ctor is deleted

    而default 则恰好相反, 如果需要默认函数的时候, 就可以把该函数赋值default, 比如默认构造和析构函数.

    其他

    C++11的新特性还包括了真正意义上的空指针, 委托构造函数, 右值引用, 统一初始化构造等等新的特性. 这里就不一一描述了, 网上描写很多, 就不一一举例.

    吐槽

    我必须先承认, 我是anti-cpp的, 所以观点不保证任何公平性, 自问多少有点偏见. 不同意的童鞋也欢迎尽管吐槽 =)

    对于这次C++11 的推出, 有人很乐观, 觉得是老树焕新芽, 可以救C++ 一命, 因为随着C++使用者的不断减少, 以及脚本语言的兴起, 这门语言已经越来越式微, 离开市场也许指日可待, 成为一个教科书上的语言.

    而我觉得这次的11标准也许是它的回光返照. 因为对于这几个比较多人关注的新语法特性, 我有一些看法:

    • 确实, 引入了lambda 对于程序的结构会有很大的改变, 一些代码可以被嵌入到另一堆的代码当中去. 但这样的写法是否优雅呢? 我持保留态度. 而且这个[] 的关键字也是非常的蛋痛, 特别是比如你看到这样的一个表达式, [](){}() , 我想不抓狂的人应该属于少数吧?
    • auto是个很方便的特性, decltype 看上去也很美. 不过试想一下这样的一个场景:

      {

      auto i = foo();

      shot(i);

      }

      那么这个shot的编写者可能就会开始郁闷了, i的类型是不定的, 那么我要写什么样的函数呢? 因为声明中是不能有auto的, 所以还是需要为每个i的类型写函数, 或者用范式? 明明是一个强类型的语言, 为何要装纯想减弱这个强类型呢? 多少有点不伦不类了…

      不过, 这个功能也是唯一一个我觉得会被广泛使用的功能, 因为确实在处理iterator的时候方便好多, 因为每次写iterator的类型的时候都很让人抓狂… btw, for_each 的引入也是不错的一个功能.

    • delete 和default 就更加蛋痛的功能了. 类似的功能在03标准中就已经可以比较好的方式实现, 用了这两个关键字之后就显得有点蛋痛, 而且除了对编译器能生成的函数以外, default是没有其他用途的, 比如Car 类有个函数叫ChangeTire, 它是无论如何都没法default的. 唯一换来的好处可能是让看头文件的人更加清楚, 那些默认行为没有了, 也能省下来一点点的编译时间和编译后size(??)
    • 至于右值引用我就觉得更蛋痛了. 这个功能我坦白我没非常了解它的正确用法, 在讲解会上也没有听的很明白, 本来晦涩的特性在C++里面已经不算新鲜了, 这次又引入那么一个, 那么可想而知这个功能如果使用起来会引入多少bug…
    • 编译器的支持将会是这个标准被废掉的另一个原因. 这次的更新很强大, 甚至连Bjame 都觉得不认识它了. 但是这些功能对于编译器开发者来说估计是个噩梦, 支持单一的功能也许不麻烦, 但是在支持的基础上编译器还要能够编译03版本的C++程序, 做到兼容估计需要不短的时间, 据说gcc和vc 都已经支持了一部分, 但在短期内没有全面支持C++11 的打算. 如果没有编译器的支持, 这个标准只能作为一纸空文了.

    不得不承认, C++11 的制定者们还是很有想法的, 也不愧是牛人, 为了挽救这门语言, 他们费尽心思, 想让它焕发青春. 不过从我的角度, 这次的修改没有能够改变我对它的看法. 依然的高高在上, 依然的语法晦涩. C++活下去是一点问题没有的, 单单是全球的大学就可以让它活下去并且活得不错. 但这次修改我认为是没法让它恢复年轻的.

    正如最近看完了7本的<明朝那些事儿>, 在明末的描述中, 不断的提到一个词: 气数已尽. 确实如此, 每个物体都是有生命周期的, 人会死, 植物会枯萎, 没有事物是永恒的, 不过我不是算命先生, C++ 气数如何我无从得知, 但它已经青春不再应该无容置疑了. 一厢情愿的想返老还童不太现实, 还不如趁此机会把优势发扬光大, 而不是一天黑路走到底, 更好….

    © 2011 Life N' Tech brought to you by @jaconey Suffusion theme by Sayontan Sinha