<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	>

<channel>
	<title>Timandes</title>
	<atom:link href="http://www.timandes.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.timandes.com</link>
	<description>专注高访问量网站的建设，研究解决方案。</description>
	<pubDate>Sun, 22 Aug 2010 02:11:08 +0000</pubDate>
	<generator>http://wordpress.org/?v=2.7.1</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>分析一下新浪微博API目前的问题</title>
		<link>http://www.timandes.com/2010/08/%e5%88%86%e6%9e%90%e4%b8%80%e4%b8%8b%e6%96%b0%e6%b5%aa%e5%be%ae%e5%8d%9aapi%e7%9b%ae%e5%89%8d%e7%9a%84%e9%97%ae%e9%a2%98/</link>
		<comments>http://www.timandes.com/2010/08/%e5%88%86%e6%9e%90%e4%b8%80%e4%b8%8b%e6%96%b0%e6%b5%aa%e5%be%ae%e5%8d%9aapi%e7%9b%ae%e5%89%8d%e7%9a%84%e9%97%ae%e9%a2%98/#comments</comments>
		<pubDate>Sun, 22 Aug 2010 02:11:08 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=37</guid>
		<description><![CDATA[新浪微博在今年上半年的时候开始了开放平台的内测，我烧网作为国内知名的博客阅读社区有幸成为其最初的合作网站，并根据自身需要通过OAuth方式模拟出了类似“人人连接”（人人Connect）的“新浪微博连接”。新浪微博用户可以直接通过微博帐号登录我烧网，无需二次注册。在代码编写和功能测试过程中，我们得到了新浪微博同仁们大力的支持与耐心的指导，在此我深表谢意。
当然，新浪微博开放平台还处于初创阶段，很多问题尚未推敲周全。我最近在编写同步功能的时候就深深体会到了这一点，也就此机会探讨一下问题和可能的解决方案。
1.Access Token永久有效
作为合作网站，我们当然希望用最少的代码完成最多的功能。如果Access Token有时效性，我们就需要使用更多的代码来进行有效性验证。因此，Access Token永久有效让合作网站的开发工作变得十分容易了。但是对于用户来讲就不那么乐观了。也许有些初级用户抱着试试看的态度授权了某个应用，之后就不想再使用了，永久有效的授权就意味着即使这个用户不再使用这个应用，应用本身也可以假借用户本人进行操作。也许你会说：将应用授权取消掉不就行了？然而，我们的大部分用户都是非常初级的用户，他们或许永远无法理解什么叫做“应用授权”，那么他们也就永远无法进行“取消授权”的操作。
不过，对于这个问题，确实是没有什么好的解决方案。如果按照类似Session Key的方式进行处理，就意味着超时过后，应用本身需要用户再次授权。而这对某些异步操作几乎是不可能的（以我烧网为例，同步叨叨到新浪微博的代码会被封装为一个异步的扩展类。但是我们需要在真正执行这部分代码的时候才会知道Access Token是否过期，而且如果真的过期的话，由于这部分代码是异步代码，我们根本就无法让用户再次授权。）。比较折中的方案就是，在授权界面上，给用户留两个选项“长期使用”和“我只是来逛逛”，默认是“我只是来逛逛”。“长期使用”就是像现在这样永久有效的授权，“我只是来逛逛”就只授权24小时。这样可以在一定程度上解决之前提到的问题。
2.授权取消
最近在编写代码的时候，我们发现无论用谁提供的代码（新浪、OAuth、我们自己编写的）都无法成功进行update操作（发布微博信息），新浪微博API永远返回400错误（source parameter(appkey) is missing）。我们知道，使用OAuth访问API是不需要source参数的。这可能是新浪微博API的一种判断机制：如果OAuth验证失败就尝试使用基本验证。因此我们就尝试提供source参数，API马上给我们返回403错误（auth failed）。我们一度认为是我们对OAuth方式POST数据不太理解，为此还查阅了大量的资料，阅读了大量的范例代码，无果。后来重新注册一个新的帐号并且授权我烧网之后，一切问题都不存在了。由此我们意识到很有可能是我们最初使用的Access Token已经取消授权了。那为什么新浪微博没有给予我们这部分通知呢？
Access Token的生命周期，在新浪微博那边应该是非常完整的，只不过没有完整的体现在API上罢了。这部分解决方案主要有两种：回调通知和返回过期状态。
回调通知：就是在用户取消授权的同时，新浪微博访问应用的某个URL，通知应用该用户已经取消了授权，Access Token即时失效。这需要应用开发者在配置应用的时候填写相应的回调地址，而且这不适用于桌面应用。
返回过期状态：就是应用在进行操作的时候，如果Access Token失效，就不要单纯的返回403错误。而是详细的返回如403-1（Access Token过期）、403-2（用户已取消授权）等信息，这样应用可以根据返回值自动更新用户的授权状态，自我完善Access Token的生命周期。
以上观点均出自作者本人，与我烧网无关。
]]></description>
			<content:encoded><![CDATA[<p>新浪微博在今年上半年的时候开始了开放平台的内测，我烧网作为国内知名的博客阅读社区有幸成为其最初的合作网站，并根据自身需要通过OAuth方式模拟出了类似“人人连接”（人人Connect）的“新浪微博连接”。新浪微博用户可以直接通过微博帐号登录我烧网，无需二次注册。在代码编写和功能测试过程中，我们得到了新浪微博同仁们大力的支持与耐心的指导，在此我深表谢意。</p>
<p>当然，新浪微博开放平台还处于初创阶段，很多问题尚未推敲周全。我最近在编写同步功能的时候就深深体会到了这一点，也就此机会探讨一下问题和可能的解决方案。</p>
<p>1.Access Token永久有效</p>
<p>作为合作网站，我们当然希望用最少的代码完成最多的功能。如果Access Token有时效性，我们就需要使用更多的代码来进行有效性验证。因此，Access Token永久有效让合作网站的开发工作变得十分容易了。但是对于用户来讲就不那么乐观了。也许有些初级用户抱着试试看的态度授权了某个应用，之后就不想再使用了，永久有效的授权就意味着即使这个用户不再使用这个应用，应用本身也可以假借用户本人进行操作。也许你会说：将应用授权取消掉不就行了？然而，我们的大部分用户都是非常初级的用户，他们或许永远无法理解什么叫做“应用授权”，那么他们也就永远无法进行“取消授权”的操作。</p>
<p>不过，对于这个问题，确实是没有什么好的解决方案。如果按照类似Session Key的方式进行处理，就意味着超时过后，应用本身需要用户再次授权。而这对某些异步操作几乎是不可能的（以我烧网为例，同步叨叨到新浪微博的代码会被封装为一个异步的扩展类。但是我们需要在真正执行这部分代码的时候才会知道Access Token是否过期，而且如果真的过期的话，由于这部分代码是异步代码，我们根本就无法让用户再次授权。）。比较折中的方案就是，在授权界面上，给用户留两个选项“长期使用”和“我只是来逛逛”，默认是“我只是来逛逛”。“长期使用”就是像现在这样永久有效的授权，“我只是来逛逛”就只授权24小时。这样可以在一定程度上解决之前提到的问题。</p>
<p>2.授权取消</p>
<p>最近在编写代码的时候，我们发现无论用谁提供的代码（新浪、OAuth、我们自己编写的）都无法成功进行update操作（发布微博信息），新浪微博API永远返回400错误（source parameter(appkey) is missing）。我们知道，使用OAuth访问API是不需要source参数的。这可能是新浪微博API的一种判断机制：如果OAuth验证失败就尝试使用基本验证。因此我们就尝试提供source参数，API马上给我们返回403错误（auth failed）。我们一度认为是我们对OAuth方式POST数据不太理解，为此还查阅了大量的资料，阅读了大量的范例代码，无果。后来重新注册一个新的帐号并且授权我烧网之后，一切问题都不存在了。由此我们意识到很有可能是我们最初使用的Access Token已经取消授权了。那为什么新浪微博没有给予我们这部分通知呢？</p>
<p>Access Token的生命周期，在新浪微博那边应该是非常完整的，只不过没有完整的体现在API上罢了。这部分解决方案主要有两种：回调通知和返回过期状态。</p>
<p>回调通知：就是在用户取消授权的同时，新浪微博访问应用的某个URL，通知应用该用户已经取消了授权，Access Token即时失效。这需要应用开发者在配置应用的时候填写相应的回调地址，而且这不适用于桌面应用。</p>
<p>返回过期状态：就是应用在进行操作的时候，如果Access Token失效，就不要单纯的返回403错误。而是详细的返回如403-1（Access Token过期）、403-2（用户已取消授权）等信息，这样应用可以根据返回值自动更新用户的授权状态，自我完善Access Token的生命周期。</p>
<p>以上观点均出自作者本人，与我烧网无关。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2010/08/%e5%88%86%e6%9e%90%e4%b8%80%e4%b8%8b%e6%96%b0%e6%b5%aa%e5%be%ae%e5%8d%9aapi%e7%9b%ae%e5%89%8d%e7%9a%84%e9%97%ae%e9%a2%98/feed/</wfw:commentRss>
		</item>
		<item>
		<title>加入我烧网</title>
		<link>http://www.timandes.com/2009/12/%e5%8a%a0%e5%85%a5%e6%88%91%e7%83%a7%e7%bd%91/</link>
		<comments>http://www.timandes.com/2009/12/%e5%8a%a0%e5%85%a5%e6%88%91%e7%83%a7%e7%bd%91/#comments</comments>
		<pubDate>Thu, 17 Dec 2009 06:18:35 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=31</guid>
		<description><![CDATA[今后大家可以通过我烧网了解我的博客以及我的最新动态。我的我烧网主页是http://woshao.com/Timandes/。欢迎大家关注我！
认证码：8cf3dc776a84d44020a56a07352b4676
]]></description>
			<content:encoded><![CDATA[<p>今后大家可以通过我烧网了解我的博客以及我的最新动态。我的我烧网主页是<a href="http://woshao.com/Timandes/">http://woshao.com/Timandes/</a>。欢迎大家关注我！</p>
<p>认证码：8cf3dc776a84d44020a56a07352b4676</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/12/%e5%8a%a0%e5%85%a5%e6%88%91%e7%83%a7%e7%bd%91/feed/</wfw:commentRss>
		</item>
		<item>
		<title>把RSS加入了我烧网</title>
		<link>http://www.timandes.com/2009/11/%e6%8a%8arss%e5%8a%a0%e5%85%a5%e4%ba%86%e6%88%91%e7%83%a7%e7%bd%91/</link>
		<comments>http://www.timandes.com/2009/11/%e6%8a%8arss%e5%8a%a0%e5%85%a5%e4%ba%86%e6%88%91%e7%83%a7%e7%bd%91/#comments</comments>
		<pubDate>Fri, 20 Nov 2009 03:13:51 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=29</guid>
		<description><![CDATA[我已正式把我的RSS加入我烧网，我的认证字符串为（acc9f9e7ccc7cd4e4b515baeb3c7b29b），大家现在也可以通过我烧网实时了解我最新更新的文章了。
]]></description>
			<content:encoded><![CDATA[<p>我已正式把我的RSS加入我烧网，我的认证字符串为（acc9f9e7ccc7cd4e4b515baeb3c7b29b），大家现在也可以通过我烧网实时了解我最新更新的文章了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/11/%e6%8a%8arss%e5%8a%a0%e5%85%a5%e4%ba%86%e6%88%91%e7%83%a7%e7%bd%91/feed/</wfw:commentRss>
		</item>
		<item>
		<title>把RSS加入了我烧网</title>
		<link>http://www.timandes.com/2009/08/rss_woshao/</link>
		<comments>http://www.timandes.com/2009/08/rss_woshao/#comments</comments>
		<pubDate>Thu, 13 Aug 2009 01:52:48 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=26</guid>
		<description><![CDATA[我已正式把我的RSS加入我烧网，我的认证字符串为（9a681b2748b9f917ca242161099f4508），大家现在也可以通过我烧叨叨实时了解我最新更新的文章了。
]]></description>
			<content:encoded><![CDATA[<p>我已正式把我的RSS加入我烧网，我的认证字符串为（9a681b2748b9f917ca242161099f4508），大家现在也可以通过我烧叨叨实时了解我最新更新的文章了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/08/rss_woshao/feed/</wfw:commentRss>
		</item>
		<item>
		<title>FriendFeed如何使用MySQL存储无Schema数据（三）</title>
		<link>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-3/</link>
		<comments>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-3/#comments</comments>
		<pubDate>Mon, 15 Jun 2009 05:52:28 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=23</guid>
		<description><![CDATA[一致性和原子性
由于我们给数据库做过分片，并且实体的索引可以被存储在和它本身不同的多个分片之上，这样一致性就成了问题。要是进程在完成对所有索引表的写入前就崩溃了，那该如何是好？
构建一套事务协议就成了大多数FriendFeed工程师眼中的有力方案，但我们希望尽可能地保持系统结构简洁。于是，我们决定放宽约束，使得：
主entities表中存储的属性包是规范的。
索引反映的不一定是实际的实体值。
这样一来，要将一个实体写入数据库，我们要进行以下步骤：
1.用InnoDB的ACID属性来保证，将实体写入entities表。
2.把索引值写入所有分片上的索引表中。
当从索引表中读入数据时，我们很清楚索引值不一定是准确的（也就是说，假如第2步的写入操作没有完成，索引所反映的可能还是老的属性数据）。根据前面提到的约束，为保证不返回无效实体，虽然使用索引表来决定哪些实体该被读入，但是我们会重新把查询的过滤条件应用到实体上，而不是完全信赖索引的完整性：
1.基于查询从所有的索引表中读入entity_id。
2.由取得的实体ID值从entities表中取得实体值。
3.根据实际的属性值，（在Python中）过滤掉所有不符合查询条件的实体。
为保证索引不会永久丢失，以及不一致性问题最终得到解决，我在前面提到的“清扫程序”进程会持续不断的扫描实体表，写入丢失的索引，并清除旧索引和无效索引。清扫程序会优先清扫最近更新的实体，使得索引中的不一致性问题在实际情况下很快得到修复（在几秒之内）。
性能
我们在新系统中对主要的索引做了不少优化，其结果令我们颇为满意。下图显示的是过去一个月内FriendFeed页面浏览的等待时间（我们在几天前启动了新的后端系统，从图中的分水岭处很容易就能看出具体是哪天）。
目前，系统的页面等待时间超乎想象地稳定，甚至在每天正午的流量高峰期也不例外。下图显示的是过去24小时内FriendFeed的页面浏览等待时间。
大家可以对比一周前的数据。
到目前为止，系统的运转一切正常。自从新系统部署以后，我们已经对索引做过了好多次变更，并且开始对我们最大的MySQL表进行转换，来使用这套新的方案。这样一来，我们在今后就可以更加从容地更改这些表的结构了。
参考资料
1.FriendFeed：http://friendfeed.com
2.CouchDB：http://couchdb.apache.org
3.CouchDB性能初探：http://tinyurl.com/CouchDBPerf
4.Pickle：http://docs.python.org/library/pickle.html
]]></description>
			<content:encoded><![CDATA[<p>一致性和原子性</p>
<p>由于我们给数据库做过分片，并且实体的索引可以被存储在和它本身不同的多个分片之上，这样一致性就成了问题。要是进程在完成对所有索引表的写入前就崩溃了，那该如何是好？</p>
<p><span id="more-23"></span>构建一套事务协议就成了大多数FriendFeed工程师眼中的有力方案，但我们希望尽可能地保持系统结构简洁。于是，我们决定放宽约束，使得：</p>
<p>主entities表中存储的属性包是规范的。</p>
<p>索引反映的不一定是实际的实体值。</p>
<p>这样一来，要将一个实体写入数据库，我们要进行以下步骤：</p>
<p>1.用InnoDB的ACID属性来保证，将实体写入entities表。</p>
<p>2.把索引值写入所有分片上的索引表中。</p>
<p>当从索引表中读入数据时，我们很清楚索引值不一定是准确的（也就是说，假如第2步的写入操作没有完成，索引所反映的可能还是老的属性数据）。根据前面提到的约束，为保证不返回无效实体，虽然使用索引表来决定哪些实体该被读入，但是我们会重新把查询的过滤条件应用到实体上，而不是完全信赖索引的完整性：</p>
<p>1.基于查询从所有的索引表中读入entity_id。</p>
<p>2.由取得的实体ID值从entities表中取得实体值。</p>
<p>3.根据实际的属性值，（在Python中）过滤掉所有不符合查询条件的实体。</p>
<p>为保证索引不会永久丢失，以及不一致性问题最终得到解决，我在前面提到的“清扫程序”进程会持续不断的扫描实体表，写入丢失的索引，并清除旧索引和无效索引。清扫程序会优先清扫最近更新的实体，使得索引中的不一致性问题在实际情况下很快得到修复（在几秒之内）。</p>
<p>性能</p>
<p>我们在新系统中对主要的索引做了不少优化，其结果令我们颇为满意。下图显示的是过去一个月内FriendFeed页面浏览的等待时间（我们在几天前启动了新的后端系统，从图中的分水岭处很容易就能看出具体是哪天）。</p>
<p>目前，系统的页面等待时间超乎想象地稳定，甚至在每天正午的流量高峰期也不例外。下图显示的是过去24小时内FriendFeed的页面浏览等待时间。</p>
<p>大家可以对比一周前的数据。</p>
<p>到目前为止，系统的运转一切正常。自从新系统部署以后，我们已经对索引做过了好多次变更，并且开始对我们最大的MySQL表进行转换，来使用这套新的方案。这样一来，我们在今后就可以更加从容地更改这些表的结构了。</p>
<p>参考资料</p>
<p>1.FriendFeed：http://friendfeed.com</p>
<p>2.CouchDB：http://couchdb.apache.org</p>
<p>3.CouchDB性能初探：http://tinyurl.com/CouchDBPerf</p>
<p>4.Pickle：http://docs.python.org/library/pickle.html</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-3/feed/</wfw:commentRss>
		</item>
		<item>
		<title>FriendFeed如何使用MySQL存储无Schema数据（二）</title>
		<link>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-2/</link>
		<comments>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-2/#comments</comments>
		<pubDate>Thu, 11 Jun 2009 09:01:59 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=16</guid>
		<description><![CDATA[详细内容
实体在MySQL存储在类似下面的表中：
CREATE TABLE entities (
added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
id BINARY(16) NOT NULL,
updated TIMESTAMP NOT NULL,
body MEDIUMBLOB,
UNIQUE KEY(id),
KEY(updated)
) ENGINE = InnoDB;

之所以存在added_id字段，是因为InnoDB会把主键大小作为物理顺序存储每行数据。AUTO_INCREMENT主键可以保证新的实体在写入磁盘是按顺序放在旧的实体后面，这样做也对读写的区域性有所帮助（新实体的读取频度要比旧实体高很多，因为FriendFeed的页面是按时间逆序来排序的）。实体的主题部分的存储格式，是进行了pickle（请参阅参考资料4）序列化的Python dictionary，并在此之上做了zlib压缩。
索引被分开存储在独立的表中。要创建新的索引，我们要先创建数据表，在所有数据库分片上保存我们希望索引的属性。比如，一个典型的FriendFeed实体看起来会像这样：
{
&#8220;id&#8221;:&#8221;71f0c4d2291844cca2df6f486e96e37c&#8221;,
&#8220;user_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,
&#8220;feed_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,
&#8220;title&#8221;:&#8221;We just launched a new backend system for FriendFeed!&#8221;,
&#8220;link&#8221;:&#8221;http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c&#8221;,
&#8220;published&#8221;:1235697046,
&#8220;updated&#8221;:1235697046,
}
接着，我们想给实体的user_id属性做索引，以方便渲染出包含某个特定用户所发布的所有实体的页面。那么索引表看起来会像这样：
CREATE TABLE index_user_id(
user_id BINARY(16) NOT NULL,
entity_id BINARY(16) NOT NULL UNIQUE,
PRIMARY KEY (user_id, entity_id)
) ENGINE=InnoDB;
我们的datastore会自动为你维护索引，因而，要启动存储上述结构并带有给定索引的实体的一个datastore实例，（在Pyton中）你可以这样写：
user_id_index = friendfeed.datastore.Index(table=&#8221;index_user_id&#8221;, properties=["user_id"], shard_on=&#8221;user_id&#8221;)
datastore = friendfeed.datastore.DataStore(mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"], indexes=[user_id_index])
new_entity = {
&#8220;id&#8221;:&#8221;71f0c4d2291844cca2df6f486e96e37c&#8221;,
&#8220;user_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,
&#8220;feed_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,
&#8220;title&#8221;:&#8221;We just launched a [...]]]></description>
			<content:encoded><![CDATA[<p>详细内容</p>
<p>实体在MySQL存储在类似下面的表中：</p>
<p>CREATE TABLE entities (</p>
<p>added_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,</p>
<p>id BINARY(16) NOT NULL,</p>
<p>updated TIMESTAMP NOT NULL,</p>
<p>body MEDIUMBLOB,</p>
<p>UNIQUE KEY(id),</p>
<p>KEY(updated)</p>
<p>) ENGINE = InnoDB;</p>
<p><span id="more-16"></span></p>
<p>之所以存在added_id字段，是因为InnoDB会把主键大小作为物理顺序存储每行数据。AUTO_INCREMENT主键可以保证新的实体在写入磁盘是按顺序放在旧的实体后面，这样做也对读写的区域性有所帮助（新实体的读取频度要比旧实体高很多，因为FriendFeed的页面是按时间逆序来排序的）。实体的主题部分的存储格式，是进行了pickle（请参阅参考资料4）序列化的Python dictionary，并在此之上做了zlib压缩。</p>
<p>索引被分开存储在独立的表中。要创建新的索引，我们要先创建数据表，在所有数据库分片上保存我们希望索引的属性。比如，一个典型的FriendFeed实体看起来会像这样：</p>
<p>{</p>
<p>&#8220;id&#8221;:&#8221;71f0c4d2291844cca2df6f486e96e37c&#8221;,</p>
<p>&#8220;user_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,</p>
<p>&#8220;feed_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,</p>
<p>&#8220;title&#8221;:&#8221;We just launched a new backend system for FriendFeed!&#8221;,</p>
<p>&#8220;link&#8221;:&#8221;http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c&#8221;,</p>
<p>&#8220;published&#8221;:1235697046,</p>
<p>&#8220;updated&#8221;:1235697046,</p>
<p>}</p>
<p>接着，我们想给实体的user_id属性做索引，以方便渲染出包含某个特定用户所发布的所有实体的页面。那么索引表看起来会像这样：</p>
<p>CREATE TABLE index_user_id(</p>
<p>user_id BINARY(16) NOT NULL,</p>
<p>entity_id BINARY(16) NOT NULL UNIQUE,</p>
<p>PRIMARY KEY (user_id, entity_id)</p>
<p>) ENGINE=InnoDB;</p>
<p>我们的datastore会自动为你维护索引，因而，要启动存储上述结构并带有给定索引的实体的一个datastore实例，（在Pyton中）你可以这样写：</p>
<p>user_id_index = friendfeed.datastore.Index(table=&#8221;index_user_id&#8221;, properties=["user_id"], shard_on=&#8221;user_id&#8221;)</p>
<p>datastore = friendfeed.datastore.DataStore(mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"], indexes=[user_id_index])</p>
<p>new_entity = {</p>
<p>&#8220;id&#8221;:&#8221;71f0c4d2291844cca2df6f486e96e37c&#8221;,</p>
<p>&#8220;user_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,</p>
<p>&#8220;feed_id&#8221;:&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;,</p>
<p>&#8220;title&#8221;:&#8221;We just launched a new backend system for FriendFeed!&#8221;,</p>
<p>&#8220;link&#8221;:&#8221;http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c&#8221;,</p>
<p>&#8220;published&#8221;:1235697046,</p>
<p>&#8220;updated&#8221;:1235697046,</p>
<p>}</p>
<p>datastore.put(new_entity)</p>
<p>entity = datastore.get(binascii.a2b_hex(&#8221;71f0c4d2291844cca2df6f486e96e37c&#8221;))</p>
<p>entity = user_id_index.get_all(datastore, user_id=binascii.a2b_hex(&#8221;f48b0440ca0c4f66991c4d5f6a078eaf&#8221;))</p>
<p>上面的Index类会在所有实体中寻找user_id属性。并在index_user_id表中自动对这个索引进行维护。由于我们给数据库做过分片，shard_on参数的作用就是用来决定要把索引存储在哪一个分片上（在本例中是entity["user_id"] % num_shards）。</p>
<p>借助这个索引的实例，你可以查询索引（参见上例中的user_id_index.get_all）。通过在所有数据库分片查询index_user_id表，获取实体的ID列表，接着从entities表中取出相应ID值的实体，datastore的代码就可以在Python中完成index_user_id表和entities表的“连接”操作。</p>
<p>例如，要给link属性增加新索引，我们就得创建一个新的数据表：</p>
<p>CREATE TABLE index_link(</p>
<p>link VARCHAR(735) NOT NULL,</p>
<p>entity_id BINARY(16) NOT NULL UNIQUE,</p>
<p>PRIMARY KEY (link, entity_id)</p>
<p>) ENGINE=InnoDB DEFAULT CHARSET=utf8;</p>
<p>再把datastore改成下面这样，新索引就被包含进来了：</p>
<p>user_id_index = friendfeed.datastore.Index(table=&#8221;index_user_id&#8221;, properties=["user_id"], shard_on=&#8221;user_id&#8221;)</p>
<p>link_index = friendfeed.datastore.Index(table=&#8221;index_link&#8221;, properties=["link"], shard_on=&#8221;link&#8221;)</p>
<p>datastore = friendfeed.datastore.DataStore(mysql_shards=["127.0.0.1:3306", "127.0.0.1:3307"], indexes=[user_id_index, link_index])</p>
<p>接着，运行下面的命令，我们就可以用异步的方式进行索引的传播（甚至在网站处理实时请求的情况下都没问题）：</p>
<p>vv ./rundatastorecleaner.py &#8211;index=index_link</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-2/feed/</wfw:commentRss>
		</item>
		<item>
		<title>FriendFeed如何使用MySQL存储无Schema数据（一）</title>
		<link>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-1/</link>
		<comments>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-1/#comments</comments>
		<pubDate>Wed, 10 Jun 2009 08:10:36 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=12</guid>
		<description><![CDATA[背景
在FriendFeed中，我们使用MySL存储所有的数据。随着用户数量的增加，数据库也在不断增长。目前，我们的存储条目数量已经超过了2.5亿，除此之外还有大量的其他数据，包括评论和喜好以及朋友列表等。
随着数据库的增长，我们不得不一而再、再而三的处理飞速成长所带来的伸缩性问题。常见的手法，我们都有过实践，例如使用只读从数据库和memcache来提高读吞吐量，以及对数据库进行分片，从而提高写入吞吐量。然而，随着技术的不断发展，通过扩展已有功能来承载更高流量所带来的问题和增加新功能相比，已经是小巫见大巫了。

尤其是为一个拥有超过一至两千万行数据的数据库做schema变更或者增加索引时，整个数据库一次就可以被彻彻底底的锁住好几个小时。删除旧索引所花费的时间大抵相等，但如果不删除这些旧索引，性能又会受到影响，因为数据库会继续在每次INSERT过程中读写这些没有被使用的索引块，是的重要的块被替换出内存。你可以使用繁琐复杂的运维手段来绕过这些问题（比如可以在某个从库上创建新索引，再把主库和从库互换），但这些过程既笨重又容易引发错误，他们不知不觉就让我们对必须引入schema或者索引的变更的新功能退避三舍。因为大量的采取了数据库分片，所以诸如JOIN这样的MySQL关系型特性，对于我们来说几乎是形同虚设。因此，我们决定跳出RDBMS的思维方式，另辟蹊径。
用来解决其schema可变数据的存储和索引的自动创建这个问题的项目为数不少（比如CouchDB就是一例）。不过，这些项目无一被大型网站所广泛采纳，因而无法让我们对其抱有太高的信心。从我们看过或自己运行过的测试结果来看，没有一个项目在稳定性和战绩上满足我们的需求（比如可以看参考资料3列出的《CouchDB性能初探》，虽然稍微有点过时）。MySQL挺靠谱，不会损坏数据；数据复制功能也很靠谱，它的缺陷我们也了如指掌。使用MySQL存储数据，我们并不排斥，只是RDBMS的使用模式不符合我们的胃口。
经过一番深思熟虑之后，我们决定摒弃采用另外一套全新系统的做法，而是以MySQL为基础实现了一套“无schema”存储系统。本文将从高层面描述这套系统的细节。其他的大型网站如何解决类似问题，我们也颇感兴趣；并且我们也相信我们的一些设计成果能对其他开发者起到抛砖引玉的作用。
概述
我们的datastore存储无schema的属性包（如JSON对象或者Python的dictionary）。存储的实体中唯一必要的属性是id，16字节的UUID值。对于datastore来说，实体的其他部分是不透明的。要修改“schema”，只需要把新的属性保存下来就可以了。
我们给这些实体中的数据做索引的方式，就是在独立的MySQL表中储存索引。如果我们想要给每个实体中的三个属性做索引，就要建立三个MySQL表，每个表分别用做一个索引。如果我们想停止使用索引，则要在代码中停止对索引表的写入，此外还可以直接从MySQL中删除这个表。加入我们打算创建一个新索引，就得为这个索引创建一个新的MySQL表，然后异步运行一个进程用来传送索引数据，而不会干扰我们运行中的服务。
这样一来，我们最终拥有的表的数量要比之前多出许多，不过索引的增删也就变得容易很多了。我么对传送新索引的进程（被称作“清扫程序（The Cleaner）”）进行了大幅度优化，使得它能够快速充填新索引而不至于拖网站速度的后退。目前，我们可以在一天的时间内完成新属性的存储，并为之建立索引，再也不用像以前一样花费一周的时间了；此外，我们也不必再做MySQL的主从库互换或者其他令人望而生畏的运维操作了。
]]></description>
			<content:encoded><![CDATA[<p>背景</p>
<p>在FriendFeed中，我们使用MySL存储所有的数据。随着用户数量的增加，数据库也在不断增长。目前，我们的存储条目数量已经超过了2.5亿，除此之外还有大量的其他数据，包括评论和喜好以及朋友列表等。</p>
<p>随着数据库的增长，我们不得不一而再、再而三的处理飞速成长所带来的伸缩性问题。常见的手法，我们都有过实践，例如使用只读从数据库和memcache来提高读吞吐量，以及对数据库进行分片，从而提高写入吞吐量。然而，随着技术的不断发展，通过扩展已有功能来承载更高流量所带来的问题和增加新功能相比，已经是小巫见大巫了。</p>
<p><span id="more-12"></span></p>
<p>尤其是为一个拥有超过一至两千万行数据的数据库做schema变更或者增加索引时，整个数据库一次就可以被彻彻底底的锁住好几个小时。删除旧索引所花费的时间大抵相等，但如果不删除这些旧索引，性能又会受到影响，因为数据库会继续在每次INSERT过程中读写这些没有被使用的索引块，是的重要的块被替换出内存。你可以使用繁琐复杂的运维手段来绕过这些问题（比如可以在某个从库上创建新索引，再把主库和从库互换），但这些过程既笨重又容易引发错误，他们不知不觉就让我们对必须引入schema或者索引的变更的新功能退避三舍。因为大量的采取了数据库分片，所以诸如JOIN这样的MySQL关系型特性，对于我们来说几乎是形同虚设。因此，我们决定跳出RDBMS的思维方式，另辟蹊径。</p>
<p>用来解决其schema可变数据的存储和索引的自动创建这个问题的项目为数不少（比如CouchDB就是一例）。不过，这些项目无一被大型网站所广泛采纳，因而无法让我们对其抱有太高的信心。从我们看过或自己运行过的测试结果来看，没有一个项目在稳定性和战绩上满足我们的需求（比如可以看参考资料3列出的《CouchDB性能初探》，虽然稍微有点过时）。MySQL挺靠谱，不会损坏数据；数据复制功能也很靠谱，它的缺陷我们也了如指掌。使用MySQL存储数据，我们并不排斥，只是RDBMS的使用模式不符合我们的胃口。</p>
<p>经过一番深思熟虑之后，我们决定摒弃采用另外一套全新系统的做法，而是以MySQL为基础实现了一套“无schema”存储系统。本文将从高层面描述这套系统的细节。其他的大型网站如何解决类似问题，我们也颇感兴趣；并且我们也相信我们的一些设计成果能对其他开发者起到抛砖引玉的作用。</p>
<p>概述</p>
<p>我们的datastore存储无schema的属性包（如JSON对象或者Python的dictionary）。存储的实体中唯一必要的属性是id，16字节的UUID值。对于datastore来说，实体的其他部分是不透明的。要修改“schema”，只需要把新的属性保存下来就可以了。</p>
<p>我们给这些实体中的数据做索引的方式，就是在独立的MySQL表中储存索引。如果我们想要给每个实体中的三个属性做索引，就要建立三个MySQL表，每个表分别用做一个索引。如果我们想停止使用索引，则要在代码中停止对索引表的写入，此外还可以直接从MySQL中删除这个表。加入我们打算创建一个新索引，就得为这个索引创建一个新的MySQL表，然后异步运行一个进程用来传送索引数据，而不会干扰我们运行中的服务。</p>
<p>这样一来，我们最终拥有的表的数量要比之前多出许多，不过索引的增删也就变得容易很多了。我么对传送新索引的进程（被称作“清扫程序（The Cleaner）”）进行了大幅度优化，使得它能够快速充填新索引而不至于拖网站速度的后退。目前，我们可以在一天的时间内完成新属性的存储，并为之建立索引，再也不用像以前一样花费一周的时间了；此外，我们也不必再做MySQL的主从库互换或者其他令人望而生畏的运维操作了。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/friendfeed-mysql-non-schema-data-1/feed/</wfw:commentRss>
		</item>
		<item>
		<title>加快头像读取速度</title>
		<link>http://www.timandes.com/2009/06/speed-up-head-read/</link>
		<comments>http://www.timandes.com/2009/06/speed-up-head-read/#comments</comments>
		<pubDate>Fri, 05 Jun 2009 16:58:34 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=10</guid>
		<description><![CDATA[在头像使用极为频繁的网站上，将头像缓存起来是一种节省Web及IO资源的有效途径。一般的做法是使用反向代理软件，比如Squid。
但是，无语用户如何更换头像，头像的文件名却是确定的，比如head_48&#215;48.jpg。这样就与Squid的缓存机制产生了冲突，总不能要求每个更换了头像的用户都等48小时后才能看到自己的新头像吧？^_^

一般遇到这种情况，最有效的办法也是利用Squid的缓存机制，在头像文件后面追加动态参数，如：head_48&#215;48.jpg?t=1234567890，强迫Squid读取一次新的头像文件并缓存起来，这样，用户更换头像后就可以立即看到了。
本站原创文章，转载请注明出处：《加快头像读取速度》
http://www.timandes.com/2009/06/speed-up-head-read/
]]></description>
			<content:encoded><![CDATA[<p>在头像使用极为频繁的网站上，将头像缓存起来是一种节省Web及IO资源的有效途径。一般的做法是使用反向代理软件，比如Squid。</p>
<p>但是，无语用户如何更换头像，头像的文件名却是确定的，比如head_48&#215;48.jpg。这样就与Squid的缓存机制产生了冲突，总不能要求每个更换了头像的用户都等48小时后才能看到自己的新头像吧？^_^</p>
<p><span id="more-10"></span></p>
<p>一般遇到这种情况，最有效的办法也是利用Squid的缓存机制，在头像文件后面追加动态参数，如：head_48&#215;48.jpg?t=1234567890，强迫Squid读取一次新的头像文件并缓存起来，这样，用户更换头像后就可以立即看到了。</p>
<p>本站原创文章，转载请注明出处：《加快头像读取速度》</p>
<p>http://www.timandes.com/2009/06/<span title="点击编辑这部分固定链接">speed-up-head-read</span>/</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/speed-up-head-read/feed/</wfw:commentRss>
		</item>
		<item>
		<title>头像文件的处理与保存</title>
		<link>http://www.timandes.com/2009/06/user-head-file-save/</link>
		<comments>http://www.timandes.com/2009/06/user-head-file-save/#comments</comments>
		<pubDate>Fri, 05 Jun 2009 16:50:45 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=8</guid>
		<description><![CDATA[一般来说，头像文件是各种以用户为中心的网站必须要处理的。建站初期，头像文件大多以
http://www.xxx.com/heads/1.jpg
的形式保存，xxx.com代表网站的域名，1.jpg代表用户ID为1的用户的头像。这样做的坏处显而易见，当用户数量超过heads目录能够有效保存的文件数量极限的时候，对于头像的存取将及其缓慢。所有的操作系统都有directory下file数量的限制，即使头像数量达不到这个极限，也会大大影响存取效率。
 这里常见的做法就是将用户ID扩展到一定的位数，比如10位，以ID=1234为例，扩展之后就变为ID=0000001234，然后使用路径符号两位两位的分割开来，成为“00/00/00/12/34”，最后将头像文件以head.jpg保存在这个路径下。最终的保存路径就变为
http://www.xxx.com/heads/00/00/00/12/34/head.jpg
。
将本来保存在一级目录下的文件拆分到多级目录中，每一级目录实际上最多出现100个目录，这样可以在一定程度上缓解对存取效率的影响。
当然，随着网站应用的增多，用户头像的规格也慢慢丰富起来，网站不能仅靠一种规格的头像应对所有新的应用。这个时候，就需要在刚刚的用户目录中存放各种不同规格的头像，头像的名称中也要包含规格信息，比如：head_48&#215;48.jpg。
本站原创文章，转载请注明出处：《头像文件的处理与保存》
http://www.timandes.com/2009/06/user-head-file-save/
]]></description>
			<content:encoded><![CDATA[<p>一般来说，头像文件是各种以用户为中心的网站必须要处理的。建站初期，头像文件大多以</p>
<p>http://www.xxx.com/heads/1.jpg</p>
<p>的形式保存，xxx.com代表网站的域名，1.jpg代表用户ID为1的用户的头像。这样做的坏处显而易见，当用户数量超过heads目录能够有效保存的文件数量极限的时候，对于头像的存取将及其缓慢。所有的操作系统都有directory下file数量的限制，即使头像数量达不到这个极限，也会大大影响存取效率。</p>
<p><span id="more-8"></span> 这里常见的做法就是将用户ID扩展到一定的位数，比如10位，以ID=1234为例，扩展之后就变为ID=0000001234，然后使用路径符号两位两位的分割开来，成为“00/00/00/12/34”，最后将头像文件以head.jpg保存在这个路径下。最终的保存路径就变为</p>
<p>http://www.xxx.com/heads/00/00/00/12/34/head.jpg</p>
<p>。</p>
<p>将本来保存在一级目录下的文件拆分到多级目录中，每一级目录实际上最多出现100个目录，这样可以在一定程度上缓解对存取效率的影响。</p>
<p>当然，随着网站应用的增多，用户头像的规格也慢慢丰富起来，网站不能仅靠一种规格的头像应对所有新的应用。这个时候，就需要在刚刚的用户目录中存放各种不同规格的头像，头像的名称中也要包含规格信息，比如：head_48&#215;48.jpg。</p>
<p>本站原创文章，转载请注明出处：《头像文件的处理与保存》</p>
<p>http://www.timandes.com/2009/06/<span title="点击编辑这部分固定链接">user-head-file-save</span>/</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/user-head-file-save/feed/</wfw:commentRss>
		</item>
		<item>
		<title>随机文章的缓存优化</title>
		<link>http://www.timandes.com/2009/06/random-article-cache-optimize/</link>
		<comments>http://www.timandes.com/2009/06/random-article-cache-optimize/#comments</comments>
		<pubDate>Tue, 02 Jun 2009 09:51:42 +0000</pubDate>
		<dc:creator>Timandes</dc:creator>
		
		<category><![CDATA[未分类]]></category>

		<guid isPermaLink="false">http://www.timandes.com/?p=3</guid>
		<description><![CDATA[带有随机性展示的文章历来不被网站设计人员喜爱，原因主要在于实现的难度和优化机制的设计。理论上来说，随机性展示的文章列表是不可能进行优化的。但是实际上，很多网站都使用相对短一些的缓存过期时间来尽可能解决随机性与优化之间的矛盾。使用这种方式，访问者在10s（甚至更短的时间）之内，看到的随机文章列表的内容是完全一样的。最近考虑到一种方法，在不缩短缓存过期时间的情况下，使访问者在缓存过期时间之内随机看到更多的文章。

首先，还是按一定的过期时间进行缓存。假设缓存过期时间为t，随机文章数为n。但是我们这一次需要缓存N组文章，每组n篇。当访问者在t的范围之内访问页面时，页面首先在1和N之间进行随机，产生随机数r，然后去缓存中取出第r组的n篇文章，反馈给访问者。
采用这种缓存方式的随机文章列表除了可以通过调节t来提高用户体验或者降低t来提高系统效能之外，还可以通过调节N值来实现这两者的平衡。随机系统的稳定性与灵活性也随之得到极大的加强。
本站原创文章，转载请注明出处：《随机文章的缓存优化》
http://www.timandes.com/2009/06/random-article-cache-optimize/
]]></description>
			<content:encoded><![CDATA[<p>带有随机性展示的文章历来不被网站设计人员喜爱，原因主要在于实现的难度和优化机制的设计。理论上来说，随机性展示的文章列表是不可能进行优化的。但是实际上，很多网站都使用相对短一些的缓存过期时间来尽可能解决随机性与优化之间的矛盾。使用这种方式，访问者在10s（甚至更短的时间）之内，看到的随机文章列表的内容是完全一样的。最近考虑到一种方法，在不缩短缓存过期时间的情况下，使访问者在缓存过期时间之内随机看到更多的文章。</p>
<p><span id="more-3"></span></p>
<p>首先，还是按一定的过期时间进行缓存。假设缓存过期时间为t，随机文章数为n。但是我们这一次需要缓存N组文章，每组n篇。当访问者在t的范围之内访问页面时，页面首先在1和N之间进行随机，产生随机数r，然后去缓存中取出第r组的n篇文章，反馈给访问者。</p>
<p>采用这种缓存方式的随机文章列表除了可以通过调节t来提高用户体验或者降低t来提高系统效能之外，还可以通过调节N值来实现这两者的平衡。随机系统的稳定性与灵活性也随之得到极大的加强。</p>
<p>本站原创文章，转载请注明出处：《随机文章的缓存优化》</p>
<p>http://www.timandes.com/2009/06/<span title="点击编辑这部分固定链接">random-article-cache-optimize</span>/</p>
]]></content:encoded>
			<wfw:commentRss>http://www.timandes.com/2009/06/random-article-cache-optimize/feed/</wfw:commentRss>
		</item>
	</channel>
</rss>
