FreeWheel 核心业务系统的分布式事务方案和实践

声明:本文是一本已经出版的技术图书的其中一章的原稿: 云原生应用架构:微服务开发最佳实战。 作者是我和FreeWheel的前同事们。 引言 随着软件系统从单体应用迈向微服务架构以及数据库选型去中心化、异构化的趋势,传统的ACID事务在分布式系统上能否延续,如何落地,有哪些注意事项?本文将围绕分布式事务这一技术议题,介绍FreeWheel核心业务系统在相关领域的业务需求、技术决策和线上实践。 分布式事务的挑战 技术演进 FreeWheel核心业务产品历经十多年的积累和迭代,伴随着数据体量和功能复杂度的上升,支撑FreeWheel核心业务的工程团队所采用和探索的技术也在不断演化和革新。 系统拓扑方面: 早期FreeWheel核心业务系统是一个单体应用(Monolith):在同一台服务器的同一个进程中,完成接收客户请求、处理请求、数据存储、返回响应等步骤。为了提升系统整体的可靠性,方便各个模块的独立演化,工程团队对单体应用进行了拆分部署和服务化,迈向了面向服务的架构(SOA)。随着服务的不断细分,单个服务的功能变得更加聚焦,基础服务和公用设施的组合/编排逻辑则变得更加错综复杂,有向微服务发展的趋势。依托近年来蓬勃发展的云计算平台AWS,FreeWheel的技术团队还在积极探索*无服务(Serverless)*技术。 数据存储方面: FreeWheel核心业务系统最早广泛使用了以MySQL为代表的关系型数据库(RDBMS)。后来为了满足多样化索引和查询数据的需求,引入了以Apache Solr和ElasticSearch为代表的搜素引擎(Search Engine)。随着数据体量的增长,传统的关系型数据库已无法满足分布式存取海量数据的需求,为此又引入了以Amazon DynamoDB和MongoDB为代表的NoSQL数据库 。 事务类需求 在诸多变化背后,客户多年积累下来的使用习惯其实是难以改变的。而看上去日新月异的产品迭代需求,经过抽象不难发现一些恒定的规律和模式: 同步和有序的数据变更:客户习惯于在集中的入口(UI / API)提交一组数据变更请求,希望在尽可能短的时间内,得到返回结果(成功或失败);接下来做何种操作,提交什么数据,取决于之前步骤的执行结果。 批量修改,统一结果:一次请求如果对应多条数据变更操作(增加、删除、修改数据),不管这些操作发生在哪些服务、落到哪个数据库,最好要么都成功,要么都失败。 传统关系型数据库中,一批数据操作同时成功、同时失败的这类需求共性被抽象为事务性,英文缩写为ACID: A (Atomicity, 原子性):一组数据操作如果其中某步操作失败,之前的操作也要回滚,不允许出现部分成功部分失败的情况。 C(Consistency,一致性):数据操作符合某种业务约束。这个概念来源于财务对账领域,拓展到数据库设计上的含义比较模糊,众说纷纭。甚至有资料说C是为了凑成ACID这个缩写而添加的。 I(Isolation,隔离性):对并发的数据操作有一定的隔离性。Isolation是分等级的, 最差的情况是毫无隔离、互相干扰;最好的情况是并发操作等效于一系列串行操作(Serializable,可串行化)。Isolation等级越高,数据库需要的资源越多,存取数据的性能(如吞吐量、延迟)越差。 D(Durability,持久性):到达数据库的请求不会“轻易”丢失。通常数据库设计文档会对“轻易”做具体的定义,比如在磁盘坏道,机器停电重启等条件下不会丢数据。 随着系统的服务拓扑从单体应用迈向微服务时代,以及数据库数量和种类的增长,分布式系统在满足传统ACID标准的事务性需求上,面临着新的挑战。所谓的CAP三选二定理是说,任何一个分布式系统不能同时满足以下三个特性: C(Consistency,强一致性):分布式系统的任何节点对同一个key的读写请求,得到的结果完全一致。也叫线性一致性。 A(Availability,可用性):每次请求都能得到及时和正常的响应,但不保证数据是最新的。 P(Partition tolerance,承受网络分隔):分布式系统在节点之间无法连通或者连接超时的前提下还能维持运转。 在CAP三个特性中,P通常是分布式系统无法规避的既定事实,设计者只能在C和A之间进行取舍。大部分系统经过综合考虑,都选择了A而放弃C,目标是高可用,最终一致(不过达成一致需要的时间无上限)。少部分系统坚持C而放弃A,即选择强一致、低可用(单节点故障将导致服务不可用,可用率取决于故障频度和恢复时间,无上限)。 技术选型与方案设计 设计目标 我们考虑通过引入一套分布式事务方案,达成以下各项设计目标: 事务性提交:即ACID中的Atomicity。业务根据需要,可以定义一组数据操作,即分布式事务,这组操作无论发生在哪个服务和数据库,要么同时成功,要么同时失败。事务中只要任何一个操作出现失败, 之前的操作都需要回滚。 系统高可用:当部分服务的部分节点出现故障时,系统整体仍然可用。通过支持服务快速扩容和缩容,实现系统整体的高吞吐量,尽可能缩短数据达成一致性的延迟。框架本身消耗的资源低,引入的额外延迟小。 数据最终一致性:并发操作同一条数据的请求到达各个服务和数据库的次序保持一致,不出现丢失、乱序。 举一个顺序不一致的例子: 如上图,A、B、C 是三个服务/数据库, 1和2为并发修改同一个key的两个请求。由于随机网络延迟,最终落在三个服务/数据库的值不一致,A为2的值,B和C为1的值。 支持服务独立演化和部署:除了支持使用RPC和给定协议进行通信之外,不对服务的实现方式做过多要求和假设。 支持服务使用异构的数据存储技术:使用不同的数据存储技术(关系型数据库、NoSQL、搜索引擎等),是FreeWheel核心业务系统的各个服务的现状和努力方向。 架构侵入性低,易于采用:不改动或少改动现有系统的代码和部署,尽量只通过新增代码以及服务部署,来实现分布式事务的运行环境和具体业务流程。框架和业务的分工明确,框架代码维持100%测试覆盖率, 业务代码100%可测试,测试成本低。保持系统高可见性和可预测性,尽可能为快速故障定位和恢复提供便利。 支持同步和异步流程:提供一种机制,将UI/API和后端入口服务之间的同步交互流程,与可能出现的后端服务之间的异步流程衔接起来。 支持事务步骤依赖:事务里面某个步骤的数据操作是否执行、如何执行,取决于前面的步骤的操作结果。 技术选型 XA协议和多阶段提交 XA协议通过引入一个协调者的角色,以及要求所有参与事务的数据库支持Two-phase Commit(2PC,两阶段提交,即先准备,后提交或回滚)来实现分布式事务。 (图片来源:https://docs.particular.net/nservicebus/azure/understanding-transactionality-in-azure ) 使用XA实现分布式事务的优点有: 强一致性:实现了数据在多个数据库上的强一致提交。 业务侵入性小:完全靠数据库本身的支持实现分布式事务,不需要改动业务逻辑。 使用XA实现分布式事务的缺点也很明显: 单点故障:协调者或者任意一个XA数据库都是能引起故障的单点(Single point of failure)。 低性能:支持XA特性的数据库在设计上有大量的阻塞和资源占位操作, 数据体量和吞吐量扩展性差。 数据库选型限制:对于服务的数据库选型引入了支持XA协议这个限制。 XA在设计上没有考虑到分布式系统的特点,事实上是一个强一致、低可用的设计方案,对网络分隔的容忍度较差。 ...

2020年10月20日

用Python做文本摘要

本文是系列的第三篇。 文本摘要 以下新闻来自雷锋网 Yahoo收购新闻摘要应用Summly 宗仁 2013-03-26 07:53 没有评论 标签:Summly 新闻摘要 Yahoo 昨日宣布收购新闻摘要移动公司Summly,价格可能为3000万美金,App本身允许你快速浏览和分享新闻,创始人Nick D’Aloisio17岁,曾获得Horizons Ventures, betaworks, Shakil Khan, Matt Mullenweg, Troy Carter的投资,而且跟新闻集团合作来采集内容。 Summly采用了自然语言处理算法,可将新闻内容提炼为不足400字的短句。用户可以快速浏览新闻话题,遇到感兴趣的内容再点击进入原始链接阅读全文。Summly在苹果AppStore应用商店中的下载量已超过50万次。 自从玛丽莎·梅耶(MarissaMayer)上任CEO后,雅虎几乎在任何事情上都恢复了步调。就在五天前,该公司收购了社交推荐公司Jybe,并获得了随同这次收购一起回到公司的前雅虎职员。 以下是雅虎在其官方博客上发布的声明: 今天我们很高兴地告诉大家,我们将收购Summly,一家本着简化我们获得信息的方式,使之更快、更容易、更简单的精神创立的移动产品公司。 尼克·德洛伊西奥在15岁时就在伦敦的家中开发了Summly应用。其始于一种见解——我们生活在一个信息世界里,需要新的方式简化我们寻找对我们很重要的报道,只需看一眼。移动设备正在改变我们的日常生活和工作,用户不但改变了他们消费的信息,也改变了信息的数量。 这篇新闻有两个值得注意的地方,第一,文本摘要很有价值;第二,技术不会很难,毕竟15岁的孩子都可以开发。 下面我们就来看看做文本摘要的大致思路。 大致思路 文本摘要 顾名思义,就是找出一部分重要的句子。 这句话有三个逻辑重点。 1. 找出一部分重要的句子 也就是说要把文章切成句子。比较直观的想法是根据标点符号以及空格、换行符等。断句这个问题,英文反而比中文要复杂,例如句号.和单引号'都是有多个意思的,都不一定能标明一句话的结尾。 2. 找出一部分重要的句子 如何衡量一篇文章里的每句话是否重要?怎样量化这个重要性?有以下思考的角度。 根据位置(标题、导语、段首段尾) 一般来说,文章尤其是新闻的标题、节标题、导语和段首段尾比较重要。对于html解析而言,就是title、h1、h2、a、strong等标记中的文字更重要。 根据内容 内容方面,需要把粒度细分到词的重要程度,如果一句话中的词很重要,那么这句话很重要。 那么就有下面的两个问题: 怎么获得一句话中的词?也就是分词问题。 词的重要程度如何衡量? 下面分别来介绍这两个问题。 2.1 分词 就是在词与词之间插入标记,一般是空格。英文文本不需要这一步,本来就有空格。但中文没有,而且一词多义现象很多, 语法结构还会随着词的位置而变化,因此中文分词是一个比较独特的问题。 但是中文分词和语音识别很像,因为语音信号在词与词之间也是没有空格的,因此可共用技术(HMM,CRF)。 Python比较好的中文分词工具有: 结巴分词 genius分词 2.2 词的重要程度如何衡量? 词频(TF) 需要去掉词频太高的词(停用词) 固定搭配的频率 N-Gram 词性 名词、动词一般比副词、形容词重要 专业词典+文章分类信息 有了专业词典以及文章分类的数据,就可以用更高级的统计工具,甚至是信息论的知识,利用词在不同类别和领域中的分布差异以及权重, 计算词对文章的互信息。这方面简单的方法有TF-IDF,相对复杂的方法有LDA(主题词的提取)。 3. 找出一部分重要的句子 需要根据重要性对句子进行一个排序,选出排名靠前的,加入最后的摘要。最后,最好能恢复句子原有的顺序,以便人们阅读和理解。 到底要几句?摘要的长度应该是可以参数化的,策略可以采取最少一句,最多N句,N是参数,也可以用百分比来定义,即文章总句数的x%,x为参数。 示例4 一个简单的文本摘要的python实现 # encoding=utf-8 from collections import defaultdict frequencies=defaultdict(int) stop_words=set() DISPLAY_FULLTEXT=False SUMMARY_RATIO= 0.1 def init_stopwords(): global stop_words stop_words= set(u"where the of is and to in that we for an are by be as on with can if from which you it this then at have all not one has or that".split()) file_stopwords="stop_words.txt" import os if os.path.isfile(file_stopwords): lines=file(file_stopwords,'r').read().split("\n") stop_words=stop_words | set(lines) def split_to_sentences(text): '''Very simple spliting to sentences by [.!?] and paragraphs. In real life we'll be much more fancy. ''' import nltk.data tokenizer = nltk.data.load('tokenizers/punkt/english.pickle') text ="\n\n".join(tokenizer.tokenize(text)) import re sentences=[] start = 0 for match in re.finditer(u'(\s*[。?!]\s*)|(\n{2,})', text): sentences.append(text[start:match.end()].strip()) start = match.end() sentences=filter(lambda x:len(x)>2,sentences) return sentences def tokenize(text): '''tokenize using jieba ''' import jieba return jieba.cut(text) def token_frequency(text): '''Return frequency (count) for each token in the text''' res = defaultdict(int) for token in tokenize(text): res[token] += 1 return res def sentence_score(sentences): '''get sentence score using word frequencey''' global frequencies,stop_words if not len(stop_words): init_stopwords() scores=[] import math num_sentences=len(sentences) len_sentences=map(len,sentences) max_len_sentences=max(len_sentences) min_len_sentences=min(len_sentences) for i,sentence in enumerate(sentences): tokens=tokenize(sentence) #词频的和,去掉停用词 score= sum((frequencies[token] for token in tokens if token not in stop_words)) #削弱句子长度的影响 length_factor=1-1.0*(len_sentences[i]-min_len_sentences)/max_len_sentences score*=length_factor #增加句子位置的影响(越靠前越好) position_factor=1-0.3*i/num_sentences score*=position_factor scores.append(score) return scores def create_summary(sentences): global frequencies summary = [] import math len_sentence=int(math.ceil(len(sentences)*SUMMARY_RATIO)) len_sentence=max(len_sentence,1) len_sentence=min(len_sentence,10) score=sentence_score(sentences) score=[(i,s) for i,s in enumerate(score)] #根据得分排序 score.sort(key=lambda s:s[1],reverse=1) #取出得分靠前的句子 score=score[:len_sentence] #恢复原来句子的顺序 score.sort(key=lambda s:s[0]) summary=[sentences[x[0]] for x in score] return set([x[0] for x in score]) # return '\n'.join(summary) def decode_text(text): for x in ['utf-8','gbk']: try: return text.decode(x) except Exception, e: pass raise Exception('cannot decode using all codings') def summarize(text): global frequencies text=decode_text(text) frequencies = token_frequency(text) sentences = split_to_sentences(text) if not len(sentences): return ([],set()) index = create_summary(sentences) return (sentences,index) if __name__ == '__main__': from colorize import Color clr = Color() s=file('test.txt','r').read() sentences,index=summarize(s) for i,sentence in enumerate(sentences): s=sentence.encode('gbk') if i in index: print s # clr.print_red_text(s) elif DISPLAY_FULLTEXT: print s print sorted(list(index)) 思考题3 请从句子的位置和内容两方面入手,思考如何改进上面简单文本摘要程序的表现。

2014年3月26日

Python web api

本文是系列的第二篇。 rest服务 这里有简介 json Json Json与pojo Python处理json的库 import json #基本库 import simplejson #需用pip安装 Json->python object: json.loads(string_of_json) Python object->json: Json.dumps(json, indent=2) #indent为缩进空格数 Requests:http for humans 这里有中文简介 个人认为,requests是对Python价值观最好的体现。 示例2 pm2.5 问题:寻找可用的api,获取城市的pm2.5及空气质量数据 知识点:用requests写rest客户端 子问题1:寻找可用的api 通过google,找到这个有态度的网站 子问题2: 使用api 子问题3:结果缓存与定时获取 代码如下 # encoding=utf-8 u''' pm25.py Usage: pm25.py <cityname> Options: <cityname> 城市名 ''' from docopt import docopt def wget_pm25(city): token="5j1znBVAsnSf5xQyNQyq" import requests as r result=r.get("http://www.pm25.in/api/querys/pm2_5.json",params={ "city":city, "token":token, }) return result.json() def needs_refresh(filename,timeout=3600): return False import os,time mtime=os.path.getmtime(filename) now=time.time() return now-mtime>timeout def get_pm25(city): filename='pm25.data.{}.json'.format(city) import os,json data=None if os.path.isfile(filename) and not needs_refresh(filename): data=file(filename,'r').read() data=json.loads(data) else: data=wget_pm25(city) if type(data)==type({}): print data['error'] return -1 else: file(filename,'w').write(json.dumps(data,indent=2)) return data[-1]["pm2_5"] if __name__ == '__main__': args=docopt(__doc__) cities=args['<cityname>'] print get_pm25(args['<cityname>']) 示例3 使用flask-rest搭建rest服务 flask-rest 是一个使用flash框架搭建rest服务的工具,使用方法如下: ...

2014年3月25日

Python 初探

最近在公司做了一些python的技术分享,都是比较基础的东西,用到的一些题材还挺有意思的,和大家分享。 本文是系列的第一篇。 第二篇:Python web api 第三篇:用Python做文本摘要 起源 作者 Guido Van Rossum (GvR) “仁慈的终身独裁者” 得名 “1989年12月,我在寻找一门“课余”编程项目来打发圣诞节前后的时间。我的办公室会关门,但我有一台家用电脑,而且没有太多其它东西。我决定为当时我正构思的一个新的脚本语言写一个解释器,它是 ABC 语言的后代,对 UNIX / C 程序员会有吸引力。作为一个略微有些无关想法的人,和一个蒙提·派森(Monty Python)的飞行马戏团的狂热爱好者,我选择了 Python 作为项目的标题。” 每个人都会 1999年,Guido向DARPA阐述Python语言的特性: 简单、直观、强大 开源,以便任何人都可以为它做贡献 代码像纯英语那样容易理解 适用于短期开发的日常任务 这些想法中的一些已经成为现实。Python 已经成为一门流行的 编程语言,尤其是在互联网环境下。 特性 交互式命令行(Interactive console) Python可以单步直译运行。运行Python解释器进入交互式。 命令行的环境,你可以在提示符号»>旁输入代码,按Enter键输出结果: >>>print("Hello, Python!") Hello, Python! 有点像Shell 脚本的执行方式。 不只是脚本 “脚本语言”如Linux shell script、windows batch file等,只能处理简单的任务。 而Python是面向对象编程(OOP)的,支持异常处理和类型检查。Python的支持者较喜欢称它为一种高阶动态编程语言 强大易用的标准库 核心库不超过10Mb Html、Xml解析:BeautifulSoup, Expat 字符串处理:字典、数组切片、正则表达式 re 单元测试: PyUnit 代码版本控制: PySVN 网络访问: urllib2 图形模块: Tkinter、PyTCL、WxPython 串行化、多线程等 扩展标准库十分容易 胶水语言(glue language) Python经常用作将不同语言编写的程序“粘”在一起的胶水语言。 Google内部的很多项目使用C++编写性能要求极高的部分,然后用Python调用相应的模块。 ...

2014年3月24日

程序调试心得

1.首先建立从改动到输出的管道 必须保证你改的代码,可以很方便地看到结果(或影响)。可见是可控的必要条件。 2.尽可能缩短反应链 尽可能缩短输入改动到输出改动的时间,最好是所见即所得(WYSIWYG)。曾经做过一个项目,在做出某些修改后,需要先编译半分钟,然后把编译后的文件上传到服务器上,再重启服务器,才能看到结果的改动,特别崩溃。这就好像大型食草恐龙的神经系统一样,切下它的尾巴,大概半分钟后神经信号才会传到大脑。 动态语言(python、javascirpt、ruby等)在这方面有天生的优势,而静态语言(c++、java)则可以通过IDE的自动编译和自动化脚本来缩短反应链。如果IDE不可用,编写自动编译脚本和测试工具绝对事半功倍。如果反应链无法缩减,那么做出任何改动之前都必须深思熟虑,还得有那么一点运气,再加上代码是你自己写的,才能一针见效。 3.理性思考的能力对于调试是很重要的,包括: 做出假设(问题出在哪里),然后以最小的代价(以时间或代码量来衡量)来验证这个假设。 单变量控制。一次改动太多的地方,就无法判断结果的变化是哪处改动造成的。 分离有问题的部分,做手术的创口尽可能小。这和上面提到的缩短反应链是一脉相承的。 极端情况下,也可以合理使用暴力手段。比如使用log或其他手段,只能定位到bug出在file1的第10行到第30行中间(假设顺序执行),那么可以用折半查找法,在第20行输出调试信息… 4.善于总结 问题的出现往往是重构的机会,一个bug往往能牵出更多的bug。如果同类的问题频繁出现,有可能标志着编写者思考方式上的漏洞。 class的职责是否不够清晰和专一? 是否缺乏合理的错误记录和处理机制?文档结构和变量命名是否规范、无歧义? 擅长总结,有助于编写者认识和弥补这些思维和编程实践上的不足,从而提升职业素养。 5.源代码管理是你最好的朋友 问题往往是日积月累才出现的,而且往往无独有偶。在着手解决bug之前,先看看有没有类似的bug被提交或解决过。除了查看代码本身的注释,也要查看出错文件的提交历史。当然,前提是大家在提交代码的时候能提供有用的注释。 6.持续重构,持续思考 排计划时要为重构预留出时间,定期做代码质量的改善,比如每周抽出1天时间来重构、写注释和说明文档。这有助于减少技术债务,提升编程能力。

2013年12月5日

如何使用互联网解决编程问题

1.学会使用关键词来描述问题 虽然搜索引擎的自然语言理解能力在进步,但现在还不能无障碍地和人类交流。要向搜索引擎请教问题,必须要用它的语言——关键词。 关键词之间必须要加上空格, 以免不好的分词系统造成对你要求的误解。 有的时候用关键词描述问题能更好地帮你看清问题的本质,即:要描述或定义我的问题,哪些词是不可能绕过去的?哪些词能帮我迅速缩小搜索的范围? 2.客观地描述问题本身,而不是预想的解决方案 看过一个很有意思的评论: 用户的需求不是买电脑,而是玩魔兽。 用户常常会把自己的解决方案当成需求提给你。 问路的时候,请不要说“附近哪有487路公交车站”,而是直接询问如何到达最终的目的地。当地人可能会给出更直接、更快捷的交通路线。 3.尽量少做假设 当你在论坛问“为什么我一用你们的软件就死机”的时候,其实你假设了 a) 别人知道你的软硬件配置 b) 别人知道你近期做了哪些操作,或者说,你认为 c) 以上这些信息对于解决问题没有意义。 一方面要提供有助于定位和解决问题的信息,另一方面要尽量使用大家都知道的词语或常识等。不要假设能解决你的问题的人和你有同样的认知水平。 很多时候假设是潜在的,所谓“当局者迷”。这时候,对结果的分析就很重要,有助于你认识到自己做了哪些假设,哪些假设是符合意图的,哪些是不合理的。 4.根据搜索结果中修正你的关键词。 搜索结果和你预期的不一样,一般有两类原因:1.你提问的方式不对,导致搜索引擎(以及搜到的回答)是基于一种错误的解读。 2.你提问的方式正确,但是问题本身,不是你想的那样,你需要重新审视你的问题。 对于第一类,你可能需要修改你的关键词,它是不是太宽泛,还是太狭隘,还是有歧义?等等。 对于第二类,你需要重新定位和描述你的问题,了解它所处的领域和背景。 简单地说,要么换个问法,要么换个问题。 5.寻找和关键词同义的核心词汇 书上的信息与网上搜索得到的信息有一个很大的不同:一本书上的信息是用一种相对统一的语言风格来传达的,你能大致预期作者会怎样去描述一个东西。但是互联网上搜索得到的信息缺乏这种统一性,很可能语言风格差别很大的两篇文章,说的是同一个事。我经常会发现,对于一个概念,我使用词语A,而大多数人都使用词语B,即B是约定俗成的“术语”,这时就要用B来代替A作为我的搜索关键词。要做到辨别这一点,必须总结一些特定领域的核心词汇,也就是上文提到的不可能绕过去的那些词。 6.信息的来源和信息的内容同等重要,甚至来源比内容更重要 在搜索决策上,去哪搜>搜什么>怎么搜。 举个例子,形如“如何用A在B中实现C功能”这样的编程问题,直接在google中输入英文关键词,然后site:stackoverflow.com,一般都能命中。 再如,位置相关的搜索,如北京周边的美食、酒店什么的,百度的服务一般优于google的。 高大上或者nerdy的问题就知乎、quora,而“见对象的家长该穿什么”,贴吧、天涯什么的更靠谱。 除了信息的来源,其他用户对信息的反馈(有多少个赞/up/star)也是衡量信息价值的重要标准。如果搜索结果可以根据反馈来排序,那就更好了,例如上面提到的知乎、quora和stackoverflow等问答社区。

2013年12月3日

电子游戏让你上瘾的5个可怕方法

本文是几年前写的一篇译文,讨论了电子游戏让人上瘾的5个因素,涉及到行为心理学、网游(WoW、征途)以及工作游戏化等。 原文在此 报纸上又有玩电子游戏致死的消息。没错,又是韩国。 搞毛啊?我可没说电子游戏是海洛因。我完全理解这些渣们的生活有其他各种杯具。但是读者你们很可能见过WoW瘾君子,而且专家也说了电子毒品绝非夸大其词。于是问题是:游戏是故意被设计成这样,让你像得了强迫症一样地去玩它,欲罢不能么?必须的!而且采取的手段真的很可怕。 No. 5 把你放在“斯金纳之箱”里 如果你自己曾对某个游戏痴迷,或者认识这样的人,下面这段会让你很不舒服。作者是微软的一个游戏研究人员,行为学和脑科学博士,讲的是如何把玩家钩在游戏上,欲罢不能。引文如下: “每个游戏场景都是时间、行动与奖励的有机体,你可以随意操控它们之间的排列组合,让玩家按预先设定的模式来行动。” 请注意,他的文章根本没提到“好玩”或者“愉悦”,这不是他的研究领域。他关注的是“让玩家按预先设定的模式来行动。” “…在这时,低龄玩家会双手举过头顶,精神十分脆弱。” 他的理论基于B. F. 斯金纳的工作,即可以通过刺激和奖励可以控制和训练被试者的行为。斯金纳发明了“斯金纳之箱”,一个带有一些装置的箱子,里边的小动物按一个拉杆可以得到一些食物颗粒。我不是说微软这货把玩家看成斯金纳箱里的小白鼠。我的意思是他使用斯金纳箱中的老鼠来阐述他的游戏设计理论。 Nick Yee,一个游戏研究者,曾把“Everquest”(无尽的任务,一个网游)称作“虚拟斯金纳之箱”。 问题出在哪呢? 游戏产业今非昔比。以前他们只是把游戏用50美刀卖给我们,才不管我们会玩多长时间,只要我们能买他们的下一个游戏就好了。但是这个产业的盈利模式正在转向为基于订阅的,如网游,需要玩家持续地在线,一直玩,一直付钱—-直到太阳爆炸为超新星。 当然做出再多的地图或者故事,也不可能让你玩上千个小时不重样。所以他们必须改变游戏的机制,让玩家重复地做同样的事,一遍又一遍,欲罢不能。斯金纳之箱就是他们的方法。 目前游戏设计界对此有颇多争执。Jonathan Blow说,斯金纳式的游戏机制是一种“探索”的过程,“不是说这些游戏不好玩,而是设计上要考虑如何让玩家觉得厌倦时还能继续玩下去,用斯金纳式的奖惩操控系统,把他们锁在重复性的劳动岗上。” 为啥这招这么有效,即使所谓的“奖励”只是些子虚乌有的数字对象?请继续往下看。 No. 4 做出虚拟的食物颗粒给你吃 大多数让人上瘾的游戏元素得益于以下事实: 你的大脑不会区别对待电子游戏中的事物,就好像它们是真的一样。因为它们确实是真的。 人们总对此嗤之以鼻(“你花了那么多时间就为拿到一把压根不存在的破剑?”),但这种蔑视是愚蠢的。只能通过花费时间、精力和技巧才能得到的东西,必定是有价值的,无论是它是钻石做的,还是二进制码或者牛肉筋。 韩国的最高法院已经判定,虚拟商品法律上与真实商品等效。这些虚拟商品的背后,是一个在世界范围内有50亿美刀的产业。 这一点也不疯狂。人们花上千美刀买颗钻石,钻石除了好看也没啥用。电子游戏的装备看上去也很拉风,还能保护你不被兽人侵害。二者是一样的,你在为一个想法付钱。 结婚纪念日快乐,亲爱的。(意思是不买钻石,买电子游戏装备) 有什么问题吗? 近25年来的几乎每个游戏都有可收集的物品,这不是什么新鲜玩意,也没啥不好。但因为玩家把游戏物品看做是真实且本身具有价值的,游戏设计者就会故意让玩家去到处收集物品,这种活动和游戏通关的目标毫无关系。 设计者绝对是故意的,迎合人们收集的自然本能,让玩家是为了收集而收集。这真的有效,不信你看那个身边有软妹子还在玩游戏收集隐藏物品的家伙。收你妹啊。 正如微软那货的文章指出的那样,游戏开发者有意在利用斯金纳箱中的那些食物小颗粒来引诱玩家,接下来就是… No. 3 让你自己去按下拉杆 想象一下箱子里的小白鼠。或者如果你不喜欢把自己想成一只老鼠,想成一只可爱的小仓鼠也可以。小仓鼠可以说话,由Chris Rock配音。 如果你想让它尽可能快地按下拉杆,怎么做?不要每次它按下时给它食物—它很快就会懈怠,知道如果它想要,肯定能得到食物。最好的办法是在它按下拉杆时,以一定几率落下食物。它很快就会发了疯一样的按那个拉杆。实验中就是如此。 在“斯金纳的王国”他们把这叫做“浮动比率奖励”。这就是为什么WoW中许多敌人会随机“掉落”贵重物品的原因。这和老虎机让人上瘾的原因是一样的。你可不能现在走,走了下一个人就全赢了。或者下一个。下下个。 中国网游“征途”将这种手段用到了极致。游戏中充满了各种宝箱,里面可能有一个随机的物品,要打开箱子,你需要钥匙。如何得到钥匙?用钱买,现实中的钞票。这简直就是老虎机嘛。 这还不算。“征途”的手段连赌场都甘拜下风:每天开箱子最多的玩家都会得到一个特别的大奖。 除了利用到人的赌博天性,成千上万的玩家都在彼此竞争,看谁能开最多的箱子。一个女人告诉我,她曾整个晚上都在开箱子—-超过一千个—-试图赢取每日大奖。 她没赢。总有人更沉迷。 So What’s The Problem? 有什么问题吗? 你能想象她坐在那,看着她的小人在箱子旁边,一遍又一遍地点对话框,一遍又一遍地看同样的动画效果,就这样消耗掉一小时又一小时? 如果你不了解,你会觉得她有严重的心理疾病。一个有理性的人怎么可能表现出这种“雨人”一样的强迫症行为? B. F. Skinner了解。他称这种训练过程为“塑形”。小小的奖励,步步引诱,就像链条上的环节。在WoW中你想要拿到拉风的T10套装。你需要5个部件。集齐套装需要400个冰霜灰烬,这种物品从某些敌人身上一次掉两三个。然后需要把每个装备部件用神圣印记升级。然后再用英雄的神圣印记升级。要得到这些你需要重复完成同样的任务,一天又一天地坐在那里点鼠标,玩游戏。软妹子怨念中。 ...

2013年9月9日

nest介绍之音乐地图

nest(https://github.com/vindurriel/nest)是一个视觉组件,它用图(graph)来展示元素之间的关系。一个节点(node)表示一个元素或者元素的某个属性,节点之间的连线(link)表示元素之间的关系。 元素和关系的类型、属性都可以是任意定制的。使用者可以和图进行交互, 允许的操作包括拖拽、添加、删除、聚焦、平移和放缩。 节点的颜色由数据实体的type属性决定,不同type即颜色之间的节点,使用一种特殊type的节点连接,这种节点的type是relationship(关系)。 为什么要做 传统的展示元素和关系的视觉组件有列表、树等,但是所能展示的拓扑关系必须是有向无环图(DAG),通俗的比喻,张三的儿子的儿子不可能是他爹。现实生活中很多元素间的关系满足DAG, 但是不满足例子也很多,比如朋友关系,张三的朋友的朋友可能也是张三的朋友,这在关系上就形成了一个环,不满足DAG。而图可以表示非DAG的拓扑关系,也就是所谓的网络。 nest是为了展示图而生的视觉组件,它能在二维平面展示复杂的元素关系,并且用户可以实时地修改图的布局、增删节点。 nest可以作为脑图使用,也可以作为更一般意义上的关系展示图,参见下面的例子。 例子 音乐地图(http://nest.ap01.aws.af.cm/model/python?theme=light)是一个探索音乐元素之间关系的动态地图,展示歌曲、艺术家、专辑和精选集之间的关系。数据抓取自虾米网。 使用方法如下: 搜索框中可搜索音乐。 点击see all可查看其他用户已经探索过的音乐地图。 保存按钮可以上传当前音乐地图。 ?按钮中可显示图例。 左键单击节点: 选中该节点,可能会弹出可供扩展的关系节点(如选中歌曲节点,弹出歌曲所在的专辑和歌手) 左键拖拽: 空白处平移,节点上调整节点位置。 左键双击节点: 扩展该节点,即添加与该节点有关系的新节点。支持svg动画的浏览器上(chrome、firefox、IE)该节点会振动,并弹出同类型的新节点。 ctrl+左键单击节点: 同左键双击节点。 shift+左键单击节点: 删除该节点,同时一并删除只与该节点有连线的所有节点 alt+左键单击节点: 将该节点设为根节点。图本身无所谓根节点,根节点的唯一目的在于以该节点的id来存储图。 地址栏中可修改theme参数,支持light和dark。 技术 nest在前台采用了d3.js,而d3.js采用了svg。 后台是web.py。 前后台通信采用了ajax和json。 源代码在github https://github.com/vindurriel/nest。 开发计划 将nest组件化, 允许嵌入到任意的html网页中 固化二次开发的接口, 提供SDK

2013年9月4日

凯撒方阵

虾米网在flash网页播放器中加密了歌曲链接,其中使用到了凯撒方阵(当年觉得好NB啊)。转帖一篇虾米音乐文件绝对地址解析,原作者反编译了虾米的flash网页播放器,并且介绍了解密过程。 下面简单地介绍一下凯撒方阵的加密原理和过程。 首先根据字符串长度先选定一个数字n, 然后把原始字符串在一个n行的方阵中从上往下、从左往右写。加密字符串的第一位是选定的数字n,接下来按从左往右、从上往下的顺序把方阵中的字符写下来,如果方阵的最后没有写满的话,最后要写下空位的个数m。 举例如下。 虾米网的一个歌曲下载链接是这样的: http://f1.xiami.net/23259/371119/14%201769402050_1357018.mp3 先选定数字n=8。把0替换成^,然后进行url encode: http%3A%2F%2Ff1.xiami.net%2F23259%2F371119%2F14%252%5E17694%5E2%5E5%5E_1357%5E18.mp3 然后把该字符串写在行数为8的方阵里,从上往下、从左往右,最后要写上空位的个数m。该字符串长度为84,m = n - (84 + 1) % n =3 。 出现1的原因是m本身也占一个位置。 h2xt912653. tFi%%959E5m t%a22%2457p p2mFF2%%%%3 %Fi23F55553 3f.371EEEE A1n21412_1 %.e51%7%18 然后按从左往右、从上往下顺序添加到加密字符串,并在开头加上n: 8h2xt912653.tFi%%959E5mt%a22%2457pp2mFF2%%%%3%Fi23F55553f.371EEEEA1n21412_1%.e51%7%18 加密完成。

2013年9月3日

虾米下载介绍

每个程序员都应该有一个宠物项目(pet project), 用来尝试想法、磨练技巧。 xiami_downloader 是我近两年来的宠物项目。 初心 最初就是想做一个虾米网的免费下载软件。几年前开始,喜欢在虾米网在线听歌。下载高音质歌曲是收费的,而且官方软件(虾客)不好用。 有人可能问,为什么不直接在线收听,非要下载。原因是我有在手机听歌的习惯,但是没有开通3G服务,据说地铁里信号不好。 通过google找到了LongkeyMusic(以下简称LKM),这个软件的功能是从虾米网批量下载一张专辑的歌曲。然而如果你想下载某张精选集里的歌曲,或者歌曲的歌词等,这个软件爱莫能助,当时的LKM没有这些功能。 LKM是用.NET写的,而且作者很厚道地没有做混淆。同事胖子向我展示了反编译利器.NET Reflector的厉害。 在此想对原作者表示感谢(和抱歉)。 第一版的xiami_downloader是直接对LongKeyMusic的hack,功能方面主要增加了我想要的精选集歌曲下载、歌词下载。界面部分LKM采用.NET 3.5下的WinForm,而我们用的是WPF(.NET 4.0推出的界面库)。 从此开始了漫长的hacking 之路。 道阻且长 xiami_downloader经过了几次大的重构,增加了很多新的功能,界面更是换了不知道多少次。 这个过程中学到了很多东西,总结起来如下: 产品 最大的收获可能就是如何去规划和实现一个产品。从收集和过滤用户需求开始,到界面和功能设计、实现,再到代码重构、自动部署。 做自己每天都会使用的软件,是一件很快乐的事。动力当然不是问题, 但更加需要有对开发价值的判断。我是一个相信奥卡姆剃刀法则的人。 如果一个功能可有可无, 那么宁愿砍掉。 如果一个UI元素不需要时刻都出现,那么就隐藏它,避免破坏画面简洁的美感。 重构 经过了几次大的重构,每次目的不一样,大体上可归纳为在两个方面增加便利性:新增和除错。要使新增方便,必然要提高代码的重用性,比较极端的是源自ruby的DRY(don’t repeat yourself)原则。要使除错方便,必须改进代码的逻辑结构,并且在各种渠道提高错误的能见度,比如log、tip、MessageBox等。 git 之前一直用SVN做代码管理,与之相比,git的commit与push分离让offline状态下的工作有了更大的灵活性。 1个人的项目基本只用两个分支:master和dev,多了麻烦。 异步方法与非阻塞UI 以前在silverlight中接触过异步方法,而在xiami_downloader中,我体会到要获得流畅的用户体验,需要在很多的方面使用异步方法,目的是不阻塞UI线程。WPF在这方面提供了很多编程上的条件,包括Dispatcher、Task等。 密码学 包括凯撒方阵、api signature的生成、md5的使用等。 跨进程通信(IPC) 主程序代码是C#,需要和C#、AutoHotKey(AHK)以及python进行IPC。先后接触到了IPC方面的集大成者ZeroMq、.NET自带的WCF,还有最原始的基于文件的IPC。 RESTful API以及OAuth 虾米推出了自己在iOS和android平台的apps, 伴随而来的是一套用于pc和平台的RESTful API。虽然没有对外公布,但是在android的apk源码里看得到。还有一部分api的入口点是google到的,曾经能在google快照中看到虾米内部api的wiki,用python写的。 虾米采用了一种奇怪的OAuth验证,获取access token时要输入用户的密码(用md5加密)。 大概是想和第三方app的登录验证兼容吧。 扁平化设计 最初是从windows 8和gmail接触到了扁平化设计,和我的审美理念很一致。然而实践扁平化设计并非易事, 对于颜色对比、留白大小的要求更加严格,找到能望图知义的icon也更加困难。 自动更新 考虑了自动更新的基本原理。开始时把可执行程序托管到github上,但后来由于众所周知的访问限制,选择了国内的云存储服务:百度网盘。百度网盘采用的是更为标准的、继承自dropbox的RESTful API,即所谓的PCS。 断点续传与下载器的线程控制 通过实现断点续传功能,对http协议有了更底层的了解。而多任务下载的需求,让我接触到了锁、资源池等概念,以及.NET 4.5引入的async/await机制 下一站? 某种意义上,一个软件的开发过程不应该有自然的结束,因为需求和资源总是在变化之中。但是写这篇文章的目的,就是为这个伴随我两年的宠物项目画上一个(暂时的)句号。原因有二: 对项目目前的状态比较满意,能想到的功能基本上都实现(或者否决)了。需要输入更多用户的反馈,才能继续向前走。 是时候培养一个新的pet project,来尝试一些更加不一样的想法和技巧了。 毕竟xiami_downloader是一个pc 客户端, 涉及到web和移动平台方面的相关技术和思想,强行整合的话也不是不行,但是别扭。 在接下来的一段时间,不会再对该项目进行开发,而是会写一些总结性、介绍性的文字,承前启后,继往开来。 对于自己创造出的事物,人们很自然的会有一种喜爱之情。因此我想对xiami_downloader说一声:谢谢,再见。

2013年8月9日