<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
<channel>
<title><![CDATA[架构之美]]></title>
<link><![CDATA[http://www.itivy.com/arch]]></link>
<description><![CDATA[<b>青藤园架构设计专题博客，分享软件设计、云计算、网站架构相关技术和思想</b>]]></description>
<language><![CDATA[zh-cn]]></language>
<copyright><![CDATA[]]></copyright>
<webMaster><![CDATA[]]></webMaster>
<generator><![CDATA[]]></generator>
<Image><![CDATA[]]></Image>
<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/22/hadoop-mapreduce-python.html]]></link>
<title><![CDATA[用python实现hadoop中的map/reduce查询]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Thu, 22 Dec 2011 23:19:26 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>摘要：</p>
<p>通过上一篇总结Hadoop中的集群环境配置和使用技巧的介绍，我们就假设已经拥有了一个可运行的Hadoop集群环境。以下的这篇文章主要是用python实现hadoop中的map/reduce查询。原文链接：<a target="_blank" href="http://slaytanic.blog.51cto.com/2057708/731750">http://slaytanic.blog.51cto.com/2057708/731750</a></p>
<p>条件，假设你已经装好了hadoop集群，配好了hdfs并可以正常运行。</p>
<p><pre class="brush:bash;">$hadoop dfs -ls /data/dw/explorer
Found 1 items
drwxrwxrwx     - rsync supergroup                    0 2011-11-30 01:06 /data/dw/explorer/20111129


$ hadoop dfs -ls /data/dw/explorer/20111129
Found 4 items
-rw-r--r--     3 rsync supergroup     12294748 2011-11-29 21:10 /data/dw/explorer/20111129/explorer_20111129_19_part-00000.lzo
-rw-r--r--     3 rsync supergroup             1520 2011-11-29 21:11 /data/dw/explorer/20111129/explorer_20111129_19_part-00000.lzo.index
-rw-r--r--     3 rsync supergroup     12337366 2011-11-29 22:09 /data/dw/explorer/20111129/explorer_20111129_20_part-00000.lzo
-rw-r--r--     3 rsync supergroup             1536 2011-11-29 22:10 /data/dw/explorer/20111129/explorer_20111129_20_part-00000.lzo.index</pre></p>
<div>数据格式如下</div>
<div><br />
</div>
<div>20111129/23:59:54 111.161.25.184 
182.132.25.243 &lt;Log_Explorer ProductVer="5.05.1026.1111" 
UUID="{C9B80A9B-704E-B106-9134-1ED3581D0123}"&gt;&lt;UserDoubleClick 
FileExt="mp3" AssociateKey="Audio.mp3" 
Count="1"/&gt;&lt;/Log_Explorer&gt;</div>
<div><br />
</div>
<div>1.map脚本取数据explorer_map.py<br />
<pre class="brush:python;">#!/usr/bin/python
#-*-coding:UTF-8 -*-
import sys
import cElementTree

debug = False#设置lzo文件偏移位
if debug:
        lzo = 0
else:
        lzo = 1

for line in sys.stdin:
        try:
                flags = line[:-1].split('\t')
#hadoop查询走标准输入，数据以\t分隔，去掉每行中的\n
                if len(flags) == 0:
                        break
                if len(flags) != 11+lzo:
#hadoop采用lzo则偏移位+1，lzo设置为False则+1
                        continue
                stat_date=flags[0+lzo]#日期
                stat_date_bar = stat_date[:4]+"-"+stat_date[4:6]+'-'+stat_date[6:8]#拼成2011-11-29格式
                version = flags[4+lzo]
                xmlstr = flags[10+lzo]
                #xmlstr=line
                dom = cElementTree.fromstring(xmlstr)
#xml字段对象，以下均为取值操作
                uuid = dom.attrib['UUID']
                node = dom.find('UserDoubleClick')
                associateKey=node.get('AssociateKey')
                associateKeys=associateKey.split('.')
                player = associateKeys[0]
                fileext=node.get('FileExt')
                count=node.get('Count')
                print stat_date_bar+','+version+','+fileext+','+player+','+associateKey+'\t'+count
#输出map后的数据，这里map不对数据做任何处理，只做取值，拼接操作
#将\t前的字符串作为key输入reduce，\t后的count作为reduce计算用的value
except Exception,e:
print e
#抛出异常 </pre>2.reduce脚本计算结果并输出explorer_red.py<br />
<pre class="brush:python;">#!/usr/bin/python
#-*-coding:UTF-8 -*-
import sys
import cElementTree
import os
import string

res = {}

for line in sys.stdin:
        try:
                flags = line[:-1].split('\t')
#拆分\t以获得map传过来的key和value
                if len(flags) != 2:
#\t切割后，如果数据有问题，元素多于2或者少于2则认为数据不合法，跳出继续下一行
                        continue
                skey= flags[0]
#取出第一个元素作为key
                count=int(flags[1])
#取出第二个元素作为value
                if res.has_key(skey) == False:
                        res[skey]=0
                res[skey] += count
#计算count总和
        except Exception,e:
                pass
#不抛出，继续执行

for key in res.keys():
        print key+','+'%s' % res[key]
#格式化输出，以放入临时文件</pre>3.放入crontab执行的脚本<br />
<pre class="brush:python;">#!/bin/sh

[ $1 ] &amp;&amp; day=$1 DATE=`date -d "$1" +%Y%m%d`
[ $1 ] || day=`date -d "1 day ago" +%Y%m%d`     DATE=`date -d "1 day ago" +%Y%m%d`
#取昨天日期

cd /opt/modules/hadoop/hadoop-0.20.203.0/
#进入hadoop工作目录
bin/hadoop jar contrib/streaming/hadoop-streaming-0.20.203.0.jar -file /home/rsync/explorer/explorer_map.py -file /home/rsync/explorer/explorer_red.py -mapper /home/rsync/explorer/explorer_map.py -reducer /home/rsync/explorer/explorer_red.py -inputformat com.hadoop.mapred.DeprecatedLzoTextInputFormat -input /data/dw/explorer/$DATE -output /tmp/explorer_$DATE
#执行map/reduce，并将排序完结果放入hdfs:///tmp/explorer

bin/hadoop fs -copyToLocal /tmp/explorer_$DATE /tmp
#将m/r结果从hdfs://tmp/explorer_$DATE 保存到本地/tmp下
bin/hadoop dfs -rmr /tmp/explorer_$DATE
#删除hdfs下临时文件夹

cd
#返回自身目录
cd explorer
#进入explorer文件夹
./rm.py $DATE
执行入库和删除临时文件夹脚本</pre>4.将/tmp生成的结果入库并删除临时文件夹<br />
<pre class="brush:python;">#!/usr/bin/python

import os
import sys
import string

if len(sys.argv) == 2:
                date = sys.argv[1:][0] #取脚本参数
                os.system ("mysql -h192.168.1.229 -ujobs -p223238 -P3306    bf5_data    -e \"load data local infile '/tmp/explorer_"+date+"/part-00000' into table explorer FIELDS TERMINATED
BY '\,' (stat_date,ver,FileExt,player,AssociateKey,count)\"")#执行入库sql语句，并用load方式将数据加载到统计表中
                os.system ("rm -rf /tmp/explorer_"+date)#删除map/reduce过的数据
else:
                print "Argv error"

#因为没有安装MySQLdb包，所以用运行脚本的方式加载数据。</pre>原始数据和最后完成的输出数据对比，红色为原数据，绿色为输出数据<br />
<pre class="brush:bash;">20111129/23:59:54 111.161.25.184 182.132.25.243 &lt;Log_Explorer ProductVer="5.05.1026.1111" UUID="{C9B80A9B-704E-B106-9134-1ED3581D0123}"&gt;&lt;UserDoubleClick FileExt="mp3" AssociateKey="Audio.mp3" Count="1"/&gt;&lt;/Log_Explorer&gt;

-----------------------------------------------------------

2011-11-29,5.05.1026.1111,mp3,Audio,Audio.mp3,1</pre><div>5.调试技巧</div>
<div><br />
</div>
<div>因为这种方式比较抽象，所以你很难得到一个直观的调试过程。建议调试如下<br />
<pre class="brush:python;">#将hadoop中的数据文本copy出来一个，lzo需要解压缩，然后将map中的debug模式置为True，也就是不加hadoop中的lzo偏移量。
#用head输入hadoop里的文件，通过管道操作放入map/reduce中执行，看输出结果

$head explorer_20111129 | explorer_map.py | explorer_red.py</pre>一天的数据大概几十个G，以前用awk和perl脚本跑需要至少半小时以上，改用map/reduce方式后，大概20几秒跑完，效率还是提高了很多的。</div>
</div>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/15/hadoop-cluster-config-usage.html]]></link>
<title><![CDATA[总结Hadoop中的集群环境配置和使用技巧]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Thu, 15 Dec 2011 12:57:06 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<h2>摘要：</h2>
<p>本文主要是和大家一起探讨Hadoop的集群配置（并非在单机上）的一些细节，通过对这些细节的详细描述，希望能帮助大家轻松搭建Hadoop集群环境，为搭建高性能Web打下坚实的基础。让我们一起来看看吧，原文如下：</p>
<h2>环境</h2>
<p>7台普通的机器，操作系统都是Linux。内存和CPU就不说了，反正Hadoop一大特点就是机器在多不在精。JDK必须是1.5以上的，这个切记。7台机器的机器名务必不同，后续会谈到机器名对于MapReduce有很大的影响。</p>
<h2>部署考虑</h2>
<p>正如上面我描述的，对于Hadoop的集群来说，可以分成两大类角色：Master和Slave，前者主要配置NameNode和
JobTracker的角色，负责总管分布式数据和分解任务的执行，后者配置DataNode和TaskTracker的角色，负责分布式数据存储以及任
务的执行。本来我打算看看一台机器是否可以配置成Master，同时也作为Slave使用，不过发现在NameNode初始化的过程中以及
TaskTracker执行过程中机器名配置好像有冲突（NameNode和TaskTracker对于Hosts的配置有些冲突，究竟是把机器名对应
IP放在配置前面还是把Localhost对应IP放在前面有点问题，不过可能也是我自己的问题吧，这个大家可以根据实施情况给我反馈）。最后反正决定一
台Master，六台Slave，后续复杂的应用开发和测试结果的比对会增加机器配置。</p>
<h2>实施步骤</h2>
<ol><li>在所有的机器上都建立相同的目录，也可以就建立相同的用户，以该用户的home路径来做hadoop的安装路径。例如我在所有的机器上都建立了<code>/home/wenchu</code>。</li>
<li>下载Hadoop，先解压到Master上。这里我是下载的0.17.1的版本。此时Hadoop的安装路径就是<code>/home/wenchu/hadoop-0.17.1</code>。</li>
<li>解压后进入conf目录，主要需要修改以下文件：<code>hadoop-env.sh</code>，<code>hadoop-site.xml</code>、<code>masters</code>、<code>slaves</code>。
    <p>Hadoop的基础配置文件是<code>hadoop-default.xml</code>，看Hadoop的代码可以知道，默认建立一个Job的时候会建立Job的Config，Config首先读入<code>hadoop-default.xml</code>的配置，然后再读入<code>hadoop-site.xml</code>的配置（这个文件初始的时候配置为空），<code>hadoop-site.xml</code>中主要配置你需要覆盖的<code>hadoop-default.xml</code>的系统级配置，以及你需要在你的MapReduce过程中使用的自定义配置（具体的一些使用例如final等参考文档）。</p>
    <p>以下是一个简单的<code>hadoop-site.xml</code>的配置：</p>
<pre class="brush:xml;">&lt;?xml version="1.0"?&gt;
&lt;?xml-stylesheet type="text/xsl" href="configuration.xsl"?&gt;
&lt;!-- Put site-specific property overrides in this file. --&gt;
&lt;configuration&gt;
&lt;property&gt;
   &lt;name&gt;fs.default.name&lt;/name&gt;//你的namenode的配置，机器名加端口
   &lt;value&gt;hdfs://10.2.224.46:54310/&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
   &lt;name&gt;mapred.job.tracker&lt;/name&gt;//你的JobTracker的配置，机器名加端口
   &lt;value&gt;hdfs://10.2.224.46:54311/&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
   &lt;name&gt;dfs.replication&lt;/name&gt;//数据需要备份的数量，默认是三
   &lt;value&gt;1&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
    &lt;name&gt;hadoop.tmp.dir&lt;/name&gt;//Hadoop的默认临时路径，这个最好配置，如果在新增节点或者其他情况下莫名其妙的DataNode启动不了，就删除此文件中的tmp目录即可。不过如果删除了NameNode机器的此目录，那么就需要重新执行NameNode格式化的命令。
    &lt;value&gt;/home/wenchu/hadoop/tmp/&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
   &lt;name&gt;mapred.child.java.opts&lt;/name&gt;//java虚拟机的一些参数可以参照配置
   &lt;value&gt;-Xmx512m&lt;/value&gt;
&lt;/property&gt;
&lt;property&gt;
  &lt;name&gt;dfs.block.size&lt;/name&gt;//block的大小，单位字节，后面会提到用处，必须是512的倍数，因为采用crc作文件完整性校验，默认配置512是checksum的最小单元。
  &lt;value&gt;5120000&lt;/value&gt;
  &lt;description&gt;The default block size for new files.&lt;/description&gt;
&lt;/property&gt;
&lt;/configuration&gt;</pre><code>hadoop-env.sh</code>文件只需要修改一个参数：<br />
<pre class="brush:bash;"># The java implementation to use.  Required.
 export JAVA_HOME=/usr/ali/jdk1.5.0_10</pre><p></p>
<p>配置你的Java路径，记住一定要1.5版本以上，免得莫名其妙出现问题。</p>
    <p>Masters中配置Masters的IP或者机器名，如果是机器名那么需要在<code>/etc/hosts</code>中有所设置。Slaves中配置的是Slaves的IP或者机器名，同样如果是机器名需要在<code>/etc/hosts</code>中有所设置。范例如下，我这里配置的都是IP：</p>
<pre class="brush:bash;">Masters:
 10.2.224.46

  Slaves:
 10.2.226.40
 10.2.226.39
 10.2.226.38
 10.2.226.37
 10.2.226.41
 10.2.224.36</pre><p></p>
<p></p>
</li>
<li value="4">建立Master到每一台Slave的SSH受信证书。由于Master将会通过SSH启动所有Slave的Hadoop，所以需要建立单向或者双向证书保证命令执行时不需要再输入密码。在Master和所有的Slave机器上执行：<code>ssh-keygen -t rsa</code>。执行此命令的时候，看到提示只需要回车。然后就会在<code>/root/.ssh/</code>下面产生<code>id_rsa.pub</code>的证书文件，通过scp将Master机器上的这个文件拷贝到Slave上（记得修改名称），例如：<code>scp root@masterIP:/root/.ssh/id_rsa.pub /root/.ssh/46_rsa.pub</code>，然后执行<code>cat /root/.ssh/46_rsa.pub &gt;&gt;/root/.ssh/authorized_keys</code>，建立<code>authorized_keys</code>文
件即可，可以打开这个文件看看，也就是rsa的公钥作为key，user@IP作为value。此时可以试验一下，从master 
ssh到slave已经不需要密码了。由slave反向建立也是同样。为什么要反向呢？其实如果一直都是Master启动和关闭的话那么没有必要建立反
向，只是如果想在Slave也可以关闭Hadoop就需要建立反向。</li>
<li>将Master上的Hadoop通过scp拷贝到每一个Slave相同的目录下，根据每一个Slave的<code>Java_HOME</code>的不同修改其<code>hadoop-env.sh</code>。</li>
<li>修改Master上<code>/etc/profile：</code><br />
 新增以下内容：（具体的内容根据你的安装路径修改，这步只是为了方便使用）<br />
<pre class="brush:bash;">export HADOOP_HOME=/home/wenchu/hadoop-0.17.1
export PATH=$PATH:$HADOOP_HOME/bin</pre>     
    修改完毕后，执行<code>source /etc/profile</code>来使其生效。</li>
<li>在Master上执行<code>Hadoop namenode –format</code>，这是第一需要做的初始化，可以看作格式化吧，以后除了在上面我提到过删除了Master上的<code>hadoop.tmp.dir</code>目录，否则是不需要再次执行的。</li>
<li>然后执行Master上的<code>start-all.sh</code>，这个命令可以直接执行，因为在6中已经添加到了path路径，这个命令是启动hdfs和mapreduce两部分，当然你也可以分开单独启动hdfs和mapreduce，分别是bin目录下的<code>start-dfs.sh</code>和<code>start-mapred.sh</code>。</li>
<li>检查Master的logs目录，看看Namenode日志以及JobTracker日志是否正常启动。</li>
<li>检查Slave的logs目录看看Datanode日志以及TaskTracker日志是否正常。</li>
<li>如果需要关闭，那么就直接执行<code>stop-all.sh</code>即可。</li>
</ol>
<p>以上步骤就可以启动Hadoop的分布式环境，然后在Master的机器进入Master的安装目录，执行<code>hadoop jar hadoop-0.17.1-examples.jar wordcount</code>输入路径和输出路径，就可以看到字数统计的效果了。此处的输入路径和输出路径都指的是HDFS中的路径，因此你可以首先通过拷贝本地文件系统中的目录到HDFS中的方式来建立HDFS中的输入路径：</p>
<p><code>hadoop dfs -copyFromLocal /home/wenchu/test-in test-in。</code>其中<code>/home/wenchu/test-in</code>是本地路径，<code>test-in</code>是将会建立在HDFS中的路径，执行完毕以后可以通过<code>hadoop dfs –ls</code>看到test-in目录已经存在，同时可以通过<code>hadoop dfs –ls test-in</code>查看里面的内容。输出路径要求是在HDFS中不存在的，当执行完那个demo以后，就可以通过<code>hadoop dfs –ls </code>输出路径看到其中的内容，具体文件的内容可以通过<code>hadoop dfs –cat</code>文件名称来查看。</p>
<p>经验总结和注意事项（这部分是我在使用过程中花了一些时间走的弯路）：</p>
<ol><li>Master和Slave上的几个conf配置文件不需要全部同步，如果确定都是通过Master去启动和关闭，那么Slave机器上的配置不需要去维护。但如果希望在任意一台机器都可以启动和关闭Hadoop，那么就需要全部保持一致了。</li>
<li>Master和Slave机器上的<code>/etc/hosts</code>中必须把集群中机器都配置上去，就算在各个配置文件中
使用的是IP。这个吃过不少苦头，原来以为如果配成IP就不需要去配置Host，结果发现在执行Reduce的时候总是卡住，在拷贝的时候就无法继续下
去，不断重试。另外如果集群中如果有两台机器的机器名如果重复也会出现问题。</li>
<li>如果在新增了节点或者删除节点的时候出现了问题，首先就去删除Slave的<code>hadoop.tmp.dir</code>，然后重新启动试试看，如果还是不行那就干脆把Master的<code>hadoop.tmp.dir</code>删除（意味着dfs上的数据也会丢失），如果删除了Master的<code>hadoop.tmp.dir</code>，那么就需要重新<code>namenode –format</code>。</li>
<li>Map任务个数以及Reduce任务个数配置。前面分布式文件系统设计提到一个文件被放入到分布式文件系统中，会被分割成多个block放置到每一个的DataNode上，默认<code>dfs.block.size</code>应该是64M，也就是说如果你放置到HDFS上的数据小于64，那么将只有一个Block，此时会被放置到某一个DataNode中，这个可以通过使用命令：<code>hadoop dfsadmin –report</code>就可以看到各个节点存储的情况。也可以直接去某一个DataNode查看目录：<code>hadoop.tmp.dir/dfs/data/current</code>就
可以看到那些block了。Block的数量将会直接影响到Map的个数。当然可以通过配置来设定Map和Reduce的任务个数。Map的个数通常默认
和HDFS需要处理的blocks相同。也可以通过配置Map的数量或者配置minimum split size来设定，实际的个数为：<code>max(min(block_size,data/#maps),min_split_size)</code>。Reduce可以通过这个公式计算：<code>0.95*num_nodes*mapred.tasktracker.tasks.maximum</code>。</li>
</ol>
<p>总的来说出了问题或者启动的时候最好去看看日志，这样心里有底。</p>
<h2>Hadoop中的命令（Command）总结</h2>
<p>这部分内容其实可以通过命令的Help以及介绍了解，我主要侧重于介绍一下我用的比较多的几个命令。Hadoop dfs 这个命令后面加参数就是对于HDFS的操作，和Linux操作系统的命令很类似，例如：</p>
<ul><li><code>Hadoop dfs –ls</code>就是查看/usr/root目录下的内容，默认如果不填路径这就是当前用户路径；</li>
<li><code>Hadoop dfs –rmr xxx</code>就是删除目录，还有很多命令看看就很容易上手；</li>
<li><code>Hadoop dfsadmin –report</code>这个命令可以全局的查看DataNode的情况；</li>
<li><code>Hadoop job</code>后面增加参数是对于当前运行的Job的操作，例如list,kill等；</li>
<li><code>Hadoop balancer</code>就是前面提到的均衡磁盘负载的命令。</li>
</ul>
<p>其他就不详细介绍了。</p>
<p>作者：岑文初&nbsp;&nbsp; 原文链接：http://www.infoq.com/cn/articles/hadoop-config-tip</p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/14/hadoop-customize-writable.html]]></link>
<title><![CDATA[Hadoop如何实现自定义的Writable]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Wed, 14 Dec 2011 10:05:12 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p><b>Hadoop</b>自带一系列有用的Writable实现，可以满足绝大多数用途。但有时，我们需要编写自己的自定义实现。通过<b>自定义Writable</b>，我们能够完全控制二进制表示和排序顺序。Writable是MapReduce数据路径的核心，所以调整二进制表示对其性能有显著影响。现有的Hadoop Writable应用已得到很好的优化，但为了对付更复杂的结构，最好创建一个新的Writable类型，而不是使用已有的类型。</p>
<p>为了演示如何创建一个<b>自定义Writable</b>，我们编写了一个表示一对字符串的实现，名为TextPair。</p>
<p></p>
<pre class="brush:java;">import java.io.*;
import org.apache.hadoop.io.*;
public class TextPair implements WritableComparable<textpair> {
private Text first;
private Text second;
public TextPair() {
set(new Text(), new Text());
}
public TextPair(String first, String second) {
set(new Text(first), new Text(second));
}
public TextPair(Text first, Text second) {
set(first, second);
}
public void set(Text first, Text second) {
this.first = first;
this.second = second;
}
public Text getFirst() {
return first;
}
public Text getSecond() {
return second;
}
@Override
public void write(DataOutput out) throws IOException {
first.write(out);
second.write(out);
}
@Override
public void readFields(DataInput in) throws IOException {
first.readFields(in);
second.readFields(in);
}
@Override
public int hashCode() {
return first.hashCode() * 163 + second.hashCode();
}
@Override
public boolean equals(Object o) {
if (o instanceof TextPair) {
TextPair tp = (TextPair) o;
return first.equals(tp.first) &amp;&amp; second.equals(tp.second);
}
return false;
}
@Override
public String toString() {
return first + "\t" + second;
}
@Override
public int compareTo(TextPair tp) {
int cmp = first.compareTo(tp.first);
if (cmp != 0) {
return cmp;
}
return second.compareTo(tp.second);
}
}</textpair></pre>此实现的第一部分直观易懂：有两个Text实例变量(first和second)和相关的构造函数、get方法和set方法。所有的Writable实现都必须有一个默认的构造函数，以便MapReduce框架能够对它们进行实例化，进而调用readFields()方法来填充它们的字段。Writable实例是易变的、经常重用的，所以我们应该尽量避免在write()或readFields()方法中分配对象。<p></p>
<p>通过委托给每个Text对象本身，TextPair的write()方法依次序列化输出流中的每一个Text对象。同样，也通过委托给Text对象本身，readFields()反序列化输人流中的字节。DataOutput和DataInput接口有丰富的整套方法用于序列化和反序列化Java基本类型，所以在一般情况下，我们能够完全控制Writable对象的数据传输格式。</p>
<p>正如为Java写的任意值对象一样，我们会重写java.lang.Object的hashCode()方法，equals()方法和toString()方法。HashPartitioner使用hashCode()方法来选择reduce分区，所以应该确保写一个好的哈希函数来确保reduce函数的分区在大小上是相当的。</p>
<p>TextPair是WritableComparable的实现，所以它提供了compareTo()方法的实现，加入我们希望的顺序：它通过一个一个String逐个排序。请注意，TextPair不同于前面的TextArrayWritable类(除了它可以存储Text对象数之外)，因为TextArrayWritable只是一个Writable，而不是WritableComparable。</p>
<p>实现一个快速的RawComparator</p>
<p>上例中所示代码能够有效工作，但还可以进一步优化。正如前面所述，在MapReduce中，TextPair被用作键时，它必须被反序列化为要调用的compareTo()方法的对象。是否可以通过查看其序列化表示的方式来比较两个TextPair对象。</p>
<p>事实证明，我们可以这样做，因为TextPair由两个Text对象连接而成，二进制Text对象表示是一个可变长度的整型，包含UTF-8表示的字符串中的字节数，后跟UTF-8字节本身。关键在于读取开始的长度。从而得知第一个Text对象的字节表示有多长，然后可以委托Text对象的RawComparator，然后利用第一或者第二个字符串的偏移量来调用它。下面例子给出了具体方法(注意，该代码嵌套在TextPair类中)。</p>
<p></p>
<pre class="brush:cpp;">public static class Comparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public Comparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2) {
try {
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
if (cmp != 0) {
return cmp;
}
return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1,
b2, s2 + firstL2, l2 - firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
static {
WritableComparator.define(TextPair.class, new Comparator());
}</pre>事实上，我们一般都是继承WritableComparator，而不是直接实现RawComparator，因为它提供了一些便利的方法和默认实现。这段代码的精妙之处在于计算firstL1和firstL2,每个字节流中第一个Text字段的长度。每个都由可变长度的整型(由WritableUtils的decodeVIntSize()返回)和它的编码值(由readVInt()返问)组成。<p></p>
<p>静态代码块注册原始的comparator以便MapReduce每次看到TextPair类，就知道使用原始comparator作为其默认comparator。</p>
<p>自定义comparator</p>
<p>从TextPair可知，编写原始的cornparator比较费力，因为必须处理字节级别的细节。如果需要编写自己的实现，org.apache.hadoop.io包中Writable的某些前瞻性实现值得研究研究。WritableUtils的有效方法也比较非常方便。</p>
<p>如果可能，还应把自定义comparator写为RawComparators。这些comparator实现的排序顺序不同于默认comparator定义的自然排序顺序。下面的例子显示了TextPair的comparator，称为First Comparator。只考虑了一对Text对象中的第一个字符串。请注意，我们重写了compare()方法使其使用对象进行比较，所以两个compare()方法的语义是相同的。</p>
<p></p>
<pre class="brush:cpp;">public static class FirstComparator extends WritableComparator {
private static final Text.Comparator TEXT_COMPARATOR = new Text.Comparator();
public FirstComparator() {
super(TextPair.class);
}
@Override
public int compare(byte[] b1, int s1, int l1,
byte[] b2, int s2, int l2) {
try {
int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
return TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
if (a instanceof TextPair &amp;&amp; b instanceof TextPair) {
return ((TextPair) a).first.compareTo(((TextPair) b).first);
}
return super.compare(a, b);
}
}</pre>好了，Hadoop实现<b>自定义Writable</b>就介绍到这里，比较晚了，改天再研究。<br />
<p></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/12/hadoop-writable-interface-introduction.html]]></link>
<title><![CDATA[Hadoop序列化中的Writable接口概述]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Mon, 12 Dec 2011 09:58:47 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>在<b>Hadoop</b>中，<b>Writable</b>接口定义了两个方法：一个用于将其状态写入二进制格式的DataOutput流，另一个用于从二进制格式的DataInput流读取其态。</p>
<p></p>
<pre class="brush:java;">package org.apache.hadoop.io;
import java.io.DataOutput;
import java.io.DataInput;
import java.io.IOException;
public interface Writable {
    void write(DataOutput out) throws IOException;
    void readFields(DataInput in) throws IOException;
}</pre>让我们来看一个特别的<b>Writable</b>，看看可以对它进行哪些操作。我们要使用IntWritable，这是一个Java的int对象的封装。可以使用set()函数来创建和设置它的值：<br />
<pre class="brush:java;">IntWritable writable = new IntWritable();
writable.set(163);</pre>类似地，我们也可以使用构造函数:<br />
<pre class="brush:java;">IntWritable writable = new IntWritable(163);</pre>为了检查IntWritable的序列化形式，我们写一个小的辅助方法，它把一个java.io.ByteArrayOutputStream封装到java.io.DataOutputStream中（java.io.DataOutput的一个实现），以此来捕获序列化的数据流中的字节:<br />
<pre class="brush:java;">public static byte[] serialize(Writable writable) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    DataOutputStream dataOut = new DataOutputStream(out);
    writable.write(dataOut);
    dataOut.close();
    return out.toByteArray();
}</pre>整数用四个字节写入(我们使用JUnit 4断言):<br />
<pre class="brush:java;">byte[] bytes = serialize(writable);
assertThat(bytes.length, is(4));</pre>字节使用大端顺序写入(所以，最重要的字节写在数据流的开始处，这是由java.io.DataOutput接口规定的)，我们可以使用Hadoop的StringUtils方法看到它们的十六进制表示:<br />
<pre class="brush:java;">assertThat(StringUtils.byteToHexString(bytes), is("000000a3"));</pre>让我们再来试试反序列化。我们创建一个帮助方法来从一个字节数组读取一个Writable对象:<br />
<pre class="brush:java;">public static byte[] deserialize(Writable writable, byte[] bytes)
throws IOException {
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
    DataInputStream dataIn = new DataInputStream(in);
    writable.readFields(dataIn);
    dataIn.close();
    return bytes;
}</pre>我们构造一个新的、缺值的IntWritable，然后调用deserialize()方法来读取刚写入的输出流。然后发现它的值(使用get方法检索得到)还是原来的值163：<br />
<pre class="brush:java;">IntWritable newWritable = new IntWritable();
deserialize(newWritable, bytes);
assertThat(newWritable.get(), is(163));</pre><span style="font-weight:bold;">WritableComparable 和comparators</span><p></p>
<p>IntWritable实现了WritableComparable接口，后者是Writable和java.lang.Comparable接口的子接口。</p>
<pre class="brush:java;">package org.apache.hadoop.io;
public interface WritableComparable<t> extends Writable, Comparable<t> {
}</t></t></pre>类型的比较对MapReduce而言至关重要的，键和键之间的比较是在排序阶段完成。Hadoop提供的一个优化方法是从Java Comparator的RawComparator扩展：<br />
<pre class="brush:java;">package org.apache.hadoop.io;
import java.util.Comparator;
public interface RawComparator<t> extends Comparator<t> {
     public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);
}</t></t></pre>这个接口允许执行者比较从流中读取的未被反序列化为对象的记录，从而省去了创建对象的所有开销。例如，IntWritables的comparator使用原始的compare()方法从每个字节数组的指定开始位置（S1和S2）和长度（L1和L2）读取整数b1和b2然后直接进行比较。<p></p>
<p>WritableComparator是RawComparator对WritableComparable类的一个通用实现。它提供两个主要功能。首先，它提供了一个默认的对原始compare()函数的调用，对从数据流对要比较的对象进行反序列化，然后调用对象的compare()方法。其次，它充当的是RawComparator实例的一个工厂方法（Writable方法已经注册)。例如，为获得IntWritable的comparator，我们只需使用:</p>
<pre class="brush:java;">RawComparator<intwritable> comparator = WritableComparator.get(IntWritable.class);</intwritable></pre>comparator可以用来比较两个IntWritable：<br />
<pre class="brush:java;">IntWritable w1 = new IntWritable(163);
IntWritable w2 = new IntWritable(67);
assertThat(comparator.compare(w1, w2), greaterThan(0));</pre>或者它们的序列化描述：<br />
<pre class="brush:bash;">byte[] b1 = serialize(w1);
byte[] b2 = serialize(w2);
assertThat(comparator.compare(b1, 0, b1.length, b2, 0, b2.length), greaterThan(0));</pre>OK，到这里我们就对<b>Hadoop序列化</b>中的Writable接口介绍完了，比较基础，下次我们来讲讲如何在<b>Hadoop</b>中实现自定义的<b>Writable</b>，下回见！<p></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/10/hadoop-mapreduce-compression.html]]></link>
<title><![CDATA[Hadoop如何在MapReduce中使用压缩]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sat, 10 Dec 2011 11:38:18 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>在考虑如何压缩那些将由<b>MapReduce</b>处理的数据时，考虑压缩格式是否支持分割是很重要的。考虑存储在HDFS中的未<b>压缩</b>的文件，其大小为1GB，HDFS的块大小为64MB，所以该文件将被存储为16块，将此文件用作输入的<b>MapReduce</b>作业会创建1个输人分片（split ,也称为“分块”。对于block，我们统一称为“块”。）每个分片都被作为一个独立map任务的输入单独进行处理。</p>
<p>现在假设。该.文件是一个grip格式的压缩文件，压缩后的大小为1GB。和前面一样，<b>HDFS</b>将此文件存储为16块。然而，针对每一块创建一个分块是没有用的，因为不可能从gzip数据流中的任意点开始读取，map任务也不可能独立于其他分块只读取一个分块中的数据。gzip格式使用DEFLATE来存储压缩过的数据，DEFLATE将数据作为一系列压缩过的块进行存储。问题是，每块的开始没有指定用户在数据流中任意点定位到下一个块的起始位置，而是其自身与数据流同步。因此，gzip不支持分割(块)机制。</p>
<p>在这种情况下，MapReduce不分割gzip格式的文件，因为它知道输入是gzip压缩格式的(通过文件扩展名得知)，而gzip压缩机制不支持分割机制。这样是以牺牲本地化为代价:一个map任务将处理16个HDFS块。大都不是map的本地数据。与此同时，因为map任务少，所以作业分割的粒度不够细，从而导致运行时间变长。</p>
<p>在我们假设的例子中，如果是一个LZO格式的文件，我们会碰到同样的问题，因为基本压缩格式不为reader提供方法使其与流同步。但是，bzip2格式的压缩文件确实提供了块与块之间的同步标记(一个48位的PI近似值)，因此它支持分割机制。（<a target="_blank" href="http://www.itivy.com/arch/archive/2011/12/10/hadoop-codec-usage.html">上一篇</a>中我们列出了每种压缩格式是否支持分割。）</p>
<p>对于文件的收集，这些问题会稍有不同。ZIP是存档格式，因此它可以将多个文件合并为一个ZIP文件。每个文件单独压缩，所有文档的存储位置存储在ZIP文件的尾部。这个属性表明ZIP文件支持文件边界处分割，每个分片中包括ZIP压缩文件中的一个或多个文件。</p>
<p style="font-weight:bold;"><span style="color:#e53333;">在MapReduce我们应该使用哪种压缩格式？</span></p>
<p>根据应用的具体情况来决定应该使用哪种压缩格式。就个人而言，更趋向于使用最快的速度压缩，还是使用最优的空间压缩？一般来说，应该尝试不同的策略，并用具有代表性的数据集进行测试，从而找到最佳方法。对于那些大型的、没有边界的文件，如日志文件，有以下选项。</p>
<ul><li>存储未压缩的文件。</li>
<li>使用支持分割机制的压缩格式，如bzip2。</li>
<li>在应用中将文件分割成几个大的数据块，然后使用任何一种支持的压缩格式单独压缩每个数据块(可不用考虑压缩格式是否支持分割)。在这里，需要选择数据块的大小使压缩后的数据块在大小上相当于HDFS的块。</li>
<li>使用支持压缩和分割的Sequence File(序列文件)。</li>
</ul>
<p>对于大型文件，不要对整个文件使用不支持分割的压缩格式，因为这样会损失本地性优势，从而使降低MapReduce应用的性能。</p>
<p style="font-weight:bold;"><span style="color:#e53333;">那么我们如何在MapReduce中使用压缩呢？</span></p>
<p>如前所述，如果输入的文件是压缩过的，那么在被MapReduce读取时，它们会被自动解压，根据文件扩展名来决定应该使用哪一个压缩解码器。</p>
<p>如果要压缩MapReduce作业的输出，请在作业配置文件中将mapred.output.compress属性设置为true。将mapred.output.compression.codec属性设置为自己打算使用的压缩编码/解码器的类名，如下例所示，此应用程序运行最高气温作业从而产生压缩的输出结果：</p>
<pre class="brush:java;">public class MaxTemperatureWithCompression {
     public static void main(String[] args) throws IOException {
         if (args.length != 2) {
            System.err.println("Usage: MaxTemperatureWithCompression &lt;input path&gt; " + "&lt;output path&gt;");
            System.exit(-1);
         }
         JobConf conf = new JobConf(MaxTemperatureWithCompression.class);
         conf.setJobName("Max temperature with output compression");
         FileInputFormat.addInputPath(conf, new Path(args[0]));
         FileOutputFormat.setOutputPath(conf, new Path(args[1]));
         conf.setOutputKeyClass(Text.class);
         conf.setOutputValueClass(IntWritable.class);
         conf.setBoolean("mapred.output.compress", true);
         conf.setClass("mapred.output.compression.codec", GzipCodec.class,
         CompressionCodec.class);
         conf.setMapperClass(MaxTemperatureMapper.class);
         conf.setCombinerClass(MaxTemperatureReducer.class);
         conf.setReducerClass(MaxTemperatureReducer.class);
         JobClient.runJob(conf);
     }
}</pre>我们使用压缩过的输入来运行此应用程序(其实不必像它一样使用和输人相同的格式压缩输出)，如下所示:<p></p>
<p>% hadoop MaxTemperatureWithCompression input/ncdc/sample.txt.gz output</p>
<p>最终输出的每部分都是压缩过的。在本例中。只有一部分:</p>
<p>% gunzip -c output/part-00000.gz<br />
1949 111<br />
1950 22</p>
<p>如果为输出使用了一系列文件，可以设置mapred.output.compression.type属性来控制压缩类型，默认为RECORD，它压缩单独的记录。将它改为BLOCK，则可以压缩一组记录。由于它有更好的压缩比，所以推荐使用。</p>
<p style="font-weight:bold;">map作业输出结果的压缩</p>
<p>即使<b>MapReduce</b>应用使用非压缩的数据来读取和写入，我们也可以受益于压缩map阶段的中间输出。因为map作业的输出会被写入磁盘并通过网络传输到reducer节点，所以如果使用LZO之类的快速压缩，能得到更好的性能，因为传输的数据量大大减少了。下表显示了启用rnap输出压缩和设置压缩格式的配置属性。</p>
<p><table style="width:99%;" border="1" bordercolor="#000000" cellpadding="2" cellspacing="0"><tbody><tr><td>&nbsp;<span style="font-weight:bold;">属性名称</span></td>
<td>&nbsp;<span style="font-weight:bold;">类型</span></td>
<td>&nbsp;<span style="font-weight:bold;">默认值</span></td>
<td>&nbsp;<span style="font-weight:bold;">描述</span></td>
</tr>
<tr><td>&nbsp;mapred.compress.map.output</td>
<td>&nbsp;布尔</td>
<td>&nbsp;false</td>
<td>&nbsp;压缩map输出</td>
</tr>
<tr><td>&nbsp;mapred.map.output.compression.codec</td>
<td>&nbsp;类</td>
<td>&nbsp;org.apache.hadoop.io.compress.DefaultCodec</td>
<td>&nbsp;map输出使用的压缩编码/解码器</td>
</tr>
</tbody>
</table>
</p>
<p>下面几行代码用于在map作业中启用gzip格式来<b>压缩</b>输出结果:</p>
<p>conf.setCompressMapOutput(true);<br />
conf.setMapOutputCompressorClass(GzipCodec.class);</p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/12/10/hadoop-codec-usage.html]]></link>
<title><![CDATA[Hadoop中的编码器和解码器]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sat, 10 Dec 2011 01:17:27 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p><b>编码器</b>和<b>解码器</b>用以执行压缩解压算法。在<b>Hadoop</b>里，编码/解码器是通过一个压缩解码器接口实现的。因此，例如，GzipCodec封装了gzip压缩的压缩和解压算法。下表列出了Hadoop可用的编码/解码器。</p>
<p><table style="width:99%;" border="1" bordercolor="#000000" cellpadding="2" cellspacing="0"><tbody><tr><td>&nbsp;<span style="font-weight:bold;">压缩格式</span></td>
<td>&nbsp;<span style="font-weight:bold;">Hadoop压缩编码/解码器</span></td>
</tr>
<tr><td>&nbsp;DEFLATE</td>
<td>&nbsp;org.apache.hadoop.io.compress.DefaultCodec</td>
</tr>
<tr><td>&nbsp;gzip</td>
<td>&nbsp;org.apache.hadoop.io.compress.GzipCodec</td>
</tr>
<tr><td>&nbsp;bzip2</td>
<td>&nbsp;org.apache.hadoop.io.compress.BZip2Codec</td>
</tr>
<tr><td>&nbsp;LZO</td>
<td>&nbsp;com.hadoop.compression.lzo.LzopCodec</td>
</tr>
</tbody>
</table>
</p>
<p>LZO格式是基于GPL许可的，不能通过Apache来分发许可，基于此，它的hadoop}编码/解码器必须单独下载，地址是http://code.google.com/p/hadoop-gpl-compression/。lzop编码/解码器兼容干lzop工具，它其实就是LZO格式，但额外还有头部，它正是我们想要的。还有一个纯LZO格式的编码/解码器LzoCodec，它使用.lzo_deflate作为扩展名(根据DEFLATE类推，是没有头部的gzip格式）。</p>
<p>CompressionCodec对流进行压缩和解压缩</p>
<p>CompressionCodec有两个方法可以用于轻松地压缩或解压缩数据。要想对正在被写入一个输出流的数据进行压缩，我们可以使用createOutputStream(OutputStreamout)方法创建一个CompressionOutputStream（未压缩的数据将被写到此)，将其以压缩格式写入底层的流。相反，要想对从输入流读取而来的数据进行解压缩，则调用createInputStream(InputStreamin)函数，从而获得一个CompressionInputStream,，从而从底层的流读取未压缩的数据。CompressionOutputStream和CompressionInputStream类似干java.util.zip.DeflaterOutputStream和java.util.zip.DeflaterInputStream，前两者还可以提供重置其底层压缩和解压缩功能，当把数据流中的section压缩为单独的块时，这比较重要。比如SequenceFile。</p>
<p>下例中说明了如何使用API来压缩从标谁输入读取的数据及如何将它写到标准输出：</p>
<pre class="brush:java;">public class StreamCompressor {
public static void main(String[] args) throws Exception {
String codecClassname = args[0];
Class&lt;?&gt; codecClass = Class.forName(codecClassname);
Configuration conf = new Configuration();
CompressionCodec codec = (CompressionCodec)
ReflectionUtils.newInstance(codecClass, conf);
CompressionOutputStream out = codec.createOutputStream(System.out);
IOUtils.copyBytes(System.in, out, 4096, false);
out.finish();
}
}</pre>此应用需要压缩CompressionCodec的合法全名来作为命令行的第一个参数。我们使用ReflectionUtils来建立一个新的实例，然后获得一个压缩好的System.out。然后我们调用IOUtils上的公共方法copyBytes()将输入复制到经过CompressionOutputStream压缩的输出。最后，调用CompressionOutputStream.的finish()方法，从而向压缩程序表明结束向压缩流写入数据，但不关闭流。我们可以试试以下命令行，使用StreamCompressor程序与GzipCodec压缩字符串“Text”，然后使用gunzip从标准输入对它进行解压缩操作:<br />
<pre class="brush:bash;">% echo "Text" | hadoop StreamCompressor org.apache.hadoop.io.compress.GzipCodec \
| gunzip -
Text</pre>用CompressionCodecFactory方法来推断CompressionCodecs<p></p>
<p>在阅读一个压缩文件时，我们通常可以从其扩展名来推断出它的编码/解码器。以.gz结尾的文件可以用GzipCodec来阅读，如此类推。每个压缩格式的扩展名均以下表所示：</p>
<p><table style="width:99%;" border="1" bordercolor="#000000" cellpadding="2" cellspacing="0"><tbody><tr><td>&nbsp;<span style="font-weight:bold;">压缩格式</span></td>
<td>&nbsp;<span style="font-weight:bold;">工具</span></td>
<td>&nbsp;<span style="font-weight:bold;">算法</span></td>
<td>&nbsp;<span style="font-weight:bold;">文件扩展名</span></td>
<td>&nbsp;<span style="font-weight:bold;">多文件</span></td>
<td>&nbsp;<span style="font-weight:bold;">可分割性</span></td>
</tr>
<tr><td>&nbsp;DEFLATEa</td>
<td>&nbsp;无</td>
<td>&nbsp;DEFLATE</td>
<td>&nbsp;.deflate</td>
<td>&nbsp;不</td>
<td>&nbsp;不</td>
</tr>
<tr><td>&nbsp;gzip</td>
<td>&nbsp;gzip</td>
<td>&nbsp;DEFLATE</td>
<td>&nbsp;.gz</td>
<td>&nbsp;不</td>
<td>&nbsp;不</td>
</tr>
<tr><td>&nbsp;ZIP</td>
<td>&nbsp;zip</td>
<td>&nbsp;DEFLATE</td>
<td>&nbsp;.zip</td>
<td>&nbsp;是</td>
<td>&nbsp;是，在文件范围内</td>
</tr>
<tr><td>&nbsp;bzip2</td>
<td>&nbsp;bzip2</td>
<td>&nbsp;bzip2</td>
<td>&nbsp;.bz2</td>
<td>&nbsp;不</td>
<td>&nbsp;是</td>
</tr>
<tr><td>&nbsp;LZO</td>
<td>&nbsp;lzop</td>
<td>&nbsp;LZO</td>
<td>&nbsp;.lzo</td>
<td>&nbsp;不</td>
<td>&nbsp;不</td>
</tr>
</tbody>
</table>
</p>
<p>CompressionCodecFactory提供了getCodec()方法，从而将文件扩展名映射到相应的CompressionCodec。此方法接受一个Path对象。下面的例子显示了一个应用程序，此程序便使用这个功能来解压缩文件。</p>
<pre class="brush:java;">public class FileDecompressor {
    public static void main(String[] args) throws Exception {
       String uri = args[0];
       Configuration conf = new Configuration();
       FileSystem fs = FileSystem.get(URI.create(uri), conf);
       Path inputPath = new Path(uri);
       CompressionCodecFactory factory = new CompressionCodecFactory(conf);
       CompressionCodec codec = factory.getCodec(inputPath);
       if (codec == null) {
           System.err.println("No codec found for " + uri);
           System.exit(1);
       }
       String outputUri =
       CompressionCodecFactory.removeSuffix(uri, codec.getDefaultExtension());
       InputStream in = null;
       OutputStream out = null;
       try {
           in = codec.createInputStream(fs.open(inputPath));
           out = fs.create(new Path(outputUri));
           IOUtils.copyBytes(in, out, conf);
       } finally {
           IOUtils.closeStream(in);
           IOUtils.closeStream(out);
       }
    }
}</pre>编码/解码器一旦找到，就会被用来去掉文件名后缀生成输出文件名（通过CompressionCodecFactory的静态方法removeSuffix()来实现）。这样，如下调用程序便把一个名为file.gz的文件解压缩为file文件:<br />
<pre class="brush:bash;">% hadoop FileDecompressor file.gz</pre>CompressionCodecFactory从io.compression.codecs配置属性定义的列表中找到编码/解码器。默认情况下，这个列表列出了Hadoop提供的所有编码/解码器(见表4-3)，如果你有一个希望要注册的编码/解码器(如外部托管的LZO编码/解码器)你可以改变这个列表。每个编码/解码器知道它的默认文件扩展名，从而使CompressionCodecFactory可以通过搜索这个列表来找到一个给定的扩展名相匹配的编码/解码器(如果有的话)。<br />
<table style="width:99%;" border="1" bordercolor="#000000" cellpadding="2" cellspacing="0"><tbody><tr><td>&nbsp;<span style="font-weight:bold;">属性名</span></td>
<td>&nbsp;<span style="font-weight:bold;">类型</span></td>
<td>&nbsp;<span style="font-weight:bold;">默认值</span></td>
<td>&nbsp;<span style="font-weight:bold;">描述</span></td>
</tr>
<tr><td>&nbsp;io.compression.codecs</td>
<td>&nbsp;逗号分隔的类名</td>
<td>&nbsp;org.apache.hadoop.io.<br />
&nbsp;compress.DefaultCodec,<br />
&nbsp;org.apache.hadoop.io.<br />
&nbsp;compress.GzipCodec,<br />
&nbsp;org.apache.hadoop.io.<br />
&nbsp;compress.Bzip2Codec</td>
<td>&nbsp;用于压缩/解压的CompressionCodec列表</td>
</tr>
</tbody>
</table>
<p></p>
<p style="text-align:center;">表4-3</p>
<p>本地库</p>
<p>考虑到性能，最好使用一个本地库（native library）来压缩和解压。例如，在一个测试中，使用本地gzip压缩库减少了解压时间50%，压缩时间大约减少了10%(与内置的Java实现相比较)。表4-4展示了Java和本地提供的每个压缩格式的实现。井不是所有的格式都有本地实现(例如bzip2压缩)，而另一些则仅有本地实现（例如LZO）。</p>
<p><table style="width:99%;" border="1" bordercolor="#000000" cellpadding="2" cellspacing="0"><tbody><tr><td>&nbsp;压缩格式</td>
<td>&nbsp;Java实现</td>
<td>&nbsp;本地实现</td>
</tr>
<tr><td>&nbsp;DEFLATE</td>
<td>&nbsp;是</td>
<td>&nbsp;是</td>
</tr>
<tr><td>&nbsp;gzip</td>
<td>&nbsp;是</td>
<td>&nbsp;是</td>
</tr>
<tr><td>&nbsp;bzip2</td>
<td>&nbsp;是</td>
<td>&nbsp;否</td>
</tr>
<tr><td>&nbsp;LZO</td>
<td>&nbsp;否</td>
<td>&nbsp;是</td>
</tr>
</tbody>
</table>
</p>
<p>Hadoop带有预置的32位和64位Linux的本地压缩库，位于库/本地目录。对于其他平台，需要自己编译库，具体请参见Hadoop的维基百科http://wiki.apache.org/hadoop/NativeHadoop。</p>
<p>本地库通过Java系统属性java.library.path来使用。Hadoop的脚本在bin目录中已经设置好这个属性，但如果不使用该脚本，则需要在应用中设置属性。</p>
<p>默认情况下，Hadoop会在它运行的平台上查找本地库，如果发现就自动加载。这意味着不必更改任何配置设置就可以使用本地库。在某些情况下，可能希望禁用本地库，比如在调试压缩相关问题的时候。为此，将属性hadoop.native.lib设置为false，即可确保内置的Java等同内置实现被使用(如果它们可用的话)。</p>
<p>CodecPool(压缩解码池)</p>
<p>如果要用本地库在应用中大量执行压缩解压任务，可以考虑使用CodecPool，从而重用压缩程序和解压缩程序，节约创建这些对象的开销。</p>
<p>下例所用的API只创建了一个很简单的压缩程序，因此不必使用这个池。此应用程序使用一个压缩池程序来压缩从标准输入读入然后将其写入标准愉出的数据：</p>
<pre class="brush:java;">public class PooledStreamCompressor {
    public static void main(String[] args) throws Exception {
    String codecClassname = args[0];
    Class&lt;?&gt; codecClass = Class.forName(codecClassname);
    Configuration conf = new Configuration();
    CompressionCodec codec = (CompressionCodec)
    ReflectionUtils.newInstance(codecClass, conf);
    Compressor compressor = null;
    try {
        compressor = CodecPool.getCompressor(codec);
        CompressionOutputStream out = codec.createOutputStream(System.out, compressor);
        IOUtils.copyBytes(System.in, out, 4096, false);
        out.finish();
    } finally {
        CodecPool.returnCompressor(compressor);
    }
  }
}</pre>我们从缓冲池中为指定的CompressionCodec检索到一个Compressor实例，codec的重载方法createOutputStream()中使用的便是它。通过使用finally块，我们便可确保此压缩程序会被返回缓冲池，即使在复制数据流之间的字节期间抛出了一个IOException。<p></p>
<p>到此，就介绍完了<b>Hadoop</b>中的<b>编码器</b>和<b>解码器</b>，希望对大家有所帮助，下次我将继续为大家分享<b>Hadoop</b>的相关知识，再见！</p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/30/memcached-java-program.html]]></link>
<title><![CDATA[分布式缓存Memcached的Java客户端优化历程]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Wed, 30 Nov 2011 09:10:33 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>这是一篇比较老的文章了，对Memcached的JAVA客户端优化做了非常详细的总结。让我们认识到，要深入了解一样事物，必须深入去研究，而不能仅仅停留在使用的层面上。Memcached JAVA客户端优化过程原文如下：</p>
<h2>Memcached 是什么？</h2>
<p>Memcached是一种集中式Cache，支持分布式横向扩展。这里需要解释说明一下，很多开发者觉得Memcached是一种分布式缓存系统，
但是其实Memcached服务端本身是单实例的，只是在客户端实现过程中可以根据存储的主键做分区存储，而这个区就是Memcached服务端的一个或
者多个实例，如果将客户端也囊括到Memcached中，那么可以部分概念上说是集中式的。其实回顾一下集中式的构架，无非两种情况：一是节点均衡的网状
（JBoss Tree 
Cache），利用JGroup的多播通信机制来同步数据；二是Master-Slaves模式（分布式文件系统），由Master来管理Slave，比
如如何选择Slave，如何迁移数据等都是由Master来完成，但是Master本身也存在单点问题。下面再总结几个它的特点来理解一下其优点和限制。</p>
<p><strong>内存存储</strong>：不言而喻，速度快，但对于内存的要求高。这种情况对CPU要求很低，所以常常采用将
Memcached服务端和一些CPU高消耗内存、低消耗应用部署在一起。（我们的某个产品正好有这样的环境，我们的接口服务器有多台，它们对CPU要求
很高——原因在于WS-Security的使用，但是对于内存要求很低，因此可以用作Memcached的服务端部署机器）。</p>
<p><strong>集中式缓存（Cache）</strong>：避开了分布式缓存的传播问题，但是需要非单点来保证其可靠性，这个就是后面集成中所作的集群（Cluster）工作，可以将多个Memcached作为一个虚拟的集群，同时对于集群的读写和普通的Memcached的读写性能没有差别。</p>
<p><strong>分布式扩展</strong>：Memcached很突出的一个优点就是采用了可分布式扩展的模式。可以将部署在一台机器上的多个
Memcached服务端或者部署在多个机器上的Memcached服务端组成一个虚拟的服务端，对于调用者来说则是完全屏蔽和透明的。这样做既提高了单
机的内存利用率，也提供了向上扩容（Scale Out）的方式。</p>
<p><strong>Socket通信：</strong>这儿需要注意传输内容的大小和序列化的问题，虽然Memcached通常会被放置到内网作为
缓存，Socket传输速率应该比较高（当前支持TCP和UDP两种模式，同时根据客户端的不同可以选择使用NIO的同步或者异步调用方式），但是序列化
成本和带宽成本还是需要注意。这里也提一下序列化，对于对象序列化的性能往往让大家头痛，但是如果对于同一类的Class对象序列化传输，第一次序列化时
间比较长，后续就会优化，也就是说序列化最大的消耗不是对象序列化，而是类的序列化。如果穿过去的只是字符串，这种情况是最理想的，省去了序列化的操作，
因此在Memcached中保存的往往是较小的内容。</p>
<p><strong>特殊的内存分配机制：</strong>首先要说明的是Memcached支持最大的存储对象为1M。它的内存分配比较特殊，但是
这样的分配方式其实也是基于性能考虑的，简单的分配机制可以更容易回收再分配，节省对CPU的使用。这里用一个酒窖做比来说明这种内存分配机制，首先在
Memcached启动的时候可以通过参数来设置使用的所有内存——酒窖，然后在有酒进入的时候，首先申请（通常是1M）的空间，用来建酒架，而酒架根据
这个酒瓶的大小将自己分割为多个小格子来安放酒瓶，并将同样大小范围内的酒瓶都放置在一类酒架上面。例如20厘米半径的酒瓶放置在可以容纳20-25厘米
的酒架A上，30厘米半径的酒瓶就放置在容纳25-30厘米的酒架B上。回收机制也很简单，首先新酒入库，看看酒架是否有可以回收的地方，如果有就直接使
用，如果没有则申请新的地方，如果申请不到，就采用配置的过期策略。从这个特点来看，如果要放的内容大小十分离散，同时大小比例相差梯度很明显的话，那么
可能对于空间使用来说效果不好，因为很可能在酒架A上就放了一瓶酒，但却占用掉了一个酒架的位置。</p>
<p><strong>缓存机制简单：</strong>有时候很多开源项目做的面面俱到，但到最后因为过于注重一些非必要的功能而拖累了性能，这里提到
的就是Memcached的简单性。首先它没有什么同步，消息分发，两阶段提交等等，它就是一个很简单的缓存，把东西放进去，然后可以取出来，如果发现所
提供的Key没有命中，那么就很直白地告诉你，你这个Key没有任何对应的东西在缓存里，去数据库或者其他地方取；当你在外部数据源取到的时候，可以直接
将内容置入到缓存中，这样下次就可以命中了。这里介绍一下同步这些数据的两种方式：一种是在你修改了以后立刻更新缓存内容，这样就会即时生效；另一种是说
容许有失效时间，到了失效时间，自然就会将内容删除，此时再去取的时候就不会命中，然后再次将内容置入缓存，用来更新内容。后者用在一些实时性要求不高，
写入不频繁的情况。</p>
<p><strong>客户端的重要性：</strong>Memcached是用C写的一个服务端，客户端没有规定，反正是Socket传输，只要语言
支持Socket通信，通过Command的简单协议就可以通信。但是客户端设计的合理十分重要，同时也给使用者提供了很大的空间去扩展和设计客户端来满
足各种场景的需要，包括容错、权重、效率、特殊的功能性需求和嵌入框架等等。</p>
<p><strong>几个应用点：</strong>小对象的缓存（用户的Token、权限信息、资源信息）；小的静态资源缓存；SQL结果的缓存（这部分如果用的好，性能提高会相当大，同时由于Memcached自身提供向上扩容，那么对于数据库向上扩容的老大难问题无疑是一剂好药）；ESB消息缓存。</p>
<h2>优化MemCached系统Java客户端的原因</h2>
<p>MemCached在大型网站被应用得越来越广泛，不同语言的客户端也都在官方网站上有提供，但是Java开发者的选择并不多。由于现在的
MemCached服务端是用C写的，因此我这个C不太熟悉的人也就没有办法去优化它。当然对于它的内存分配机制等细节还是有所了解，因此在使用的时候也
会十分注意，这些文章在网络上有很多。这里我重点介绍一下对于MemCache系统的Java客户端优化的两个阶段。</p>
<h3>第一阶段：封装Whalin</h3>
<p>第一阶段主要是在官方推荐的Java客户端之一whalin开源实现基础上做再次封装。</p>
<ol><li><strong>缓存服务接口化</strong>：定义了IMemCache接口，在应用部分仅仅只是使用接口，为将来替换缓存服务实现提供基础。</li>
<li><strong>使用配置代替代码初始化客户端</strong>：通过配置客户端和SocketIO Pool属性，直接交由CacheManager来维护Cache Client Pool的生命周期，便于单元测试。</li>
<li>KeySet的实现：对于MemCached来说本身是不提供KeySet的方法的，在接口封装初期，同事向我提出这个需求的时候，我个
人觉得也是没有必要提供，因为缓存轮询是比较低效的，同时这类场景，往往可以去数据源获取KeySet，而不是从MemCached去获取。但是SIP的
一个场景的出现，让我不得不去实现了KeySet。<br />
 
SIP在作服务访问频率控制的时候需要记录在控制间隔期内的访问次数和流量，此时由于是集群，因此数据必须放在集中式的存储或者缓存中，数据库肯定撑不住
这样大数据量的更新频率，因此考虑使用Memcached的很出彩的操作——全局计数器
（storeCounter,getCounter,inc,dec），但是在检查计数器的时候如何去获取当前所有的计数器？我曾考虑使用DB或者文件，
但是效率有问题，同时如果放在一个字段中的话，还会存在并发问题。因此不得不实现了KeySet，在使用KeySet的时候有一个参数，类型是
Boolean，这个字段的存在是因为在Memcached中数据的删除并不是直接删除，而是标注一下，这样会导致实现keySet的时候取出可能已经删
除的数据。如果对于数据严谨性要求低，速度要求高，那么不需要再去验证Key是否真的有效，而如果要求Key必须正确存在，就需要再多一次的轮询查找。</li>
<li>集群的实现：Memcached作为集中式缓存，存在着集中式的致命问题：单点问题。虽然Memcached支持多Instance分布
在多台机器上，但仅仅只是解决了数据全部丢失的问题，当其中一台机器出错以后，还是会导致部分数据的丢失，一个篮子掉在地上还是会把部分的鸡蛋打破。因此
就需要实现一个备份机制，能够保证Memcached在部分失效以后，数据还能够依然使用，当然大家很多时候都用缓存不命中就去数据源获取的策略。然而在
SIP的场景中，如果部分信息找不到就去数据库查找，很容易将SIP弄垮，因此SIP对于Memcached中的数据认为是可信的，做集群也是必要的。</li>
<li>LocalCache结合Memcached使用，提高数据获取效率：在第一次压力测试过程中，发现和原先预料的一
样，Memcached并不是完全无损失的，Memcached是通过Socket数据交互来进行通信的，因此机器的带宽，网络IO，Socket连接数
都是制约Memcached发挥其作用的障碍。Memcache的一个突出优点就是Timeout的设置，也就是可以对放进去的数据设置有效期，从而在一
定的容忍时间内对那些不敏感的数据就可以不去更新，以提高效率。根据这个思想，其实在集群中的每一个Memcached客户端也可以使用本地的缓存，来存
储获取过的数据，设置一定的失效时间，来减少对于Memcached的访问次数，提高整体性能。</li>
</ol>
<p>因此，在每一个客户端中都内置了一个有超时机制的本地缓存（采用Lazy Timeout机制），在获取数据的时候，首先在本地查询数据是否存在，如果不存在则再向Memcache发起请求，获得数据以后，将其缓存在本地，并设置有效时间。方法定义如下：</p>
<pre class="brush:java;">/**

  * 降低memcache的交互频繁造成的性能损失，因此采用本地cache结合memcache的方式

  * @param key

  * @param 本地缓存失效时间单位秒

  * @return

**/

public Object get(String key,int localTTL);</pre><p></p>
<h3>第二阶段：优化</h3>
<p>第一阶段的封装基本上已经可以满足现有的需求，也被自己的项目和其他产品线所使用，但是不经意的一句话，让我开始了第二阶段的优化。有同事告诉我说
Memcached客户端的SocketIO代码里面有太多的Synchronized（同步），多多少少会影响性能。虽然过去看过这部分代码，但是当时
只是关注里面的Hash算法。根据同事所说的回去一看，果然有不少的同步，可能是作者当时写客户端的时候JDK版本较老的缘故造成的，现在
Concurrent包被广泛应用，因此优化并不是一件很难的事情。但是由于原有Whalin没有提供扩展的接口，因此不得不将Whalin除了
SockIO，其余全部纳入到封装过的客户端的设想，然后改造SockIO部分。</p>
<p>结果也就有了这个放在Google上的开源客户端：<a href="http://code.google.com/p/memcache-client-forjava/" target="_blank">http://code.google.com/p/memcache-client-forjava/</a>。</p>
<ol><li>优化Synchronized：在原有代码中SockIO的资源池被分成三个池（普通Map实现），——Free（闲）、Busy（忙）
和Dead（死锁），然后根据SockIO使用情况来维护这三个资源池。优化方式为首先简化资源池，只有一个资源池，设置一个状态池，在变更资源状态的过
程时仅仅变更资源池中的内容。然后用ConcurrentMap来替代Map，同时使用putIfAbsent方法来简化Synchronized，具体
的代码可参见Google上该软件的源文件。</li>
<li>原以为这次优化后，效率应该会有很大的提高，但是在初次压力测试后发现，并没有明显的提高，看来有其他地方的耗时远远大于连接池资源维
护，因此用JProfiler作了性能分析，发现了最大的一个瓶颈：Read数据部分。原有设计中读取数据是按照单字节读取，然后逐步分析，为的仅仅就是
遇到协议中的分割符可以识别。但是循环Read单字节和批量分页Read性能相差很大，因此我内置了读入缓存页（可设置大小），然后再按照协议的需求去读
取和分析数据，结果显示效率得到了很大的提高。具体的数据参见最后部分的压力测试结果。</li>
</ol>
<p>上面两部分的工作不论是否提升了性能，但是对于客户端本身来说都是有意义的，当然提升性能给应用带来的吸引力更大。这部分细节内容可以参看代码实现部分，对于调用者来说完全没有任何功能影响，仅仅只是性能。</p>
<h2>压力测试比较</h2>
<p>在这个压力测试之前，其实已经做过很多次压力测试了，测试中的数据本身并没有衡量Memcached的意义，因为测试是使用我自己的机器，其中性
能、带宽、内存和网络IO都不是服务器级别的，这里仅仅是将使用原有的第三方客户端和改造后的客户端作一个比较。场景就是模拟多用户多线程在同一时间发起
缓存操作，然后记录下操作的结果。</p>
<p>Client版本在测试中有两个：2.0和2.2。2.0是封装调用Whalin Memcached Client 
2.0.1版本的客户端实现。2.2是使用了新SockIO的无第三方依赖的客户端实现。checkAlive指的是在使用连接资源以前是否需要验证连接
资源有效（发送一次请求并接受响应），因此启用该设置对于性能来说会有不少的影响，不过建议还是使用这个检查。</p>
<p><strong>单个缓存服务端实例的各种配置和操作下比较：</strong></p>
<p><table style="width:489px;height:545px;" border="1" cellspacing="0">
    <tbody>
        <tr>
            <td width="104">缓存配置</td>
            <td width="34">用户</td>
            <td width="150">操作</td>
            <td width="52">客户端 版本</td>
            <td width="72">总耗时(ms)</td>
            <td width="54">单线程耗时(ms)</td>
            <td width="79">提高处理能力百分比</td>
        </tr>
        <tr>
            <td>checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
     1000 get simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>13242565<br />
     7772767</td>
            <td>132425<br />
     77727</td>
            <td>+41.3%</td>
        </tr>
        <tr>
            <td>No checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
    1000 put simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>7200285<br />
     4667239</td>
            <td>72002<br />
     46672</td>
            <td>+35.2%</td>
        </tr>
        <tr>
            <td>checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
     2000 get simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>20385457<br />
     11494383</td>
            <td>203854<br />
     114943</td>
            <td>+43.6%</td>
        </tr>
        <tr>
            <td>No checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
     2000 get simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>11259185<br />
     7256594</td>
            <td>112591<br />
     72565</td>
            <td>+35.6%</td>
        </tr>
        <tr>
            <td>checkAlive</td>
            <td>100</td>
            <td>1000 put complex obj<br />
     1000 get complex obj</td>
            <td>2.0<br />
     2.2</td>
            <td>15004906<br />
     9501571</td>
            <td>150049<br />
     95015</td>
            <td>+36.7%</td>
        </tr>
        <tr>
            <td>No checkAlive</td>
            <td>100</td>
            <td>1000 put complex obj<br />
     1000 put complex obj</td>
            <td>2.0<br />
     2.2</td>
            <td>9022578<br />
     6775981</td>
            <td>90225<br />
     67759</td>
            <td>+24.9%</td>
        </tr>
    </tbody>
</table>
</p>
<p>从上面的压力测试可以看出这么几点，首先优化SockIO提升了不少性能，其次SockIO优化的是get的性能，对于put没有太大的作用。原以为获取数据越大性能效果提升越明显，但结果并不是这样。</p>
<p><strong>单个缓存实例和双缓存实例的测试比较：</strong></p>
<p><table style="width:489px;height:252px;" border="1" cellspacing="0">
    <tbody>
        <tr>
            <td width="114">缓存配置</td>
            <td width="34">用户</td>
            <td width="167">操作</td>
            <td width="52">客户端 版本</td>
            <td width="72">总耗时(ms)</td>
            <td width="54">单线程耗时(ms)</td>
            <td width="79">提高处理能力百分比</td>
        </tr>
        <tr>
            <td>One Cache instance<br />
checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
     1000 get simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>13242565<br />
     7772767</td>
            <td>132425<br />
     77727</td>
            <td>+41.3%</td>
        </tr>
        <tr>
            <td>Two Cache instance<br />
checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
    1000 put simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>13596841<br />
     7696684</td>
            <td>135968<br />
     76966</td>
            <td>+43.4%</td>
        </tr>
    </tbody>
</table>
</p>
<p>结果显示，单个客户端对应多个服务端实例性能提升略高于单客户端对应单服务端实例。</p>
<p><strong>缓存集群的测试比较：</strong></p>
<p><table style="width:498px;height:249px;" border="1" cellspacing="0">
    <tbody>
        <tr>
            <td width="114">缓存配置</td>
            <td width="34">用户</td>
            <td width="167">操作</td>
            <td width="52">客户端 版本</td>
            <td width="72">总耗时(ms)</td>
            <td width="54">单线程耗时(ms)</td>
            <td width="79">提高处理能力百分比</td>
        </tr>
        <tr>
            <td>No Cluster<br />
     checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
     1000 get simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>13242565<br />
     7772767</td>
            <td>132425<br />
     77727</td>
            <td>+41.3%</td>
        </tr>
        <tr>
            <td>Cluster<br />
     checkAlive</td>
            <td>100</td>
            <td>1000 put simple obj<br />
    1000 put simple obj</td>
            <td>2.0<br />
     2.2</td>
            <td>25044268<br />
     8404606</td>
            <td>250442<br />
     84046</td>
            <td>+66.5%</td>
        </tr>
    </tbody>
</table>
</p>
<p>这部分和SocketIO优化无关。2.0采用的是向集群中所有客户端更新成功以后才返回的策略，2.2采用了异步更新，并且是分布式客户端节点获取的方式来分散压力，因此提升效率很多。</p>
<h2>开源代码下载</h2>
<p>其实封装后的客户端一直在内部使用，现在作了二次优化以后，觉得应该开源出来，一是可以完善自己的客户端代码，二是也可以和更多的开发者交流使用心
得。目前我已经在Google Code上传了应用的代码、范例和说明等，有兴趣的朋友可以下载下来测试一下，与现在用的Java 
Memcached客户端在易用性和性能方面是否有所提高，也期待更多对于这部分开源内容的反馈，能够将它做的更好。</p>
<p>链接地址：<a href="http://code.google.com/p/memcache-client-forjava/" target="_blank">http://code.google.com/p/memcache-client-forjava/</a>。</p>
<p><a target="_blank" href="http://www.infoq.com/cn/articles/memcached-java">原文链接</a></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/27/soa-and-platform.html]]></link>
<title><![CDATA[浅析SOA和平台]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sun, 27 Nov 2011 10:35:47 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>在谈这个之前，还得再说下SOA和平台。SOA做两件事情，一个是解耦并识别可重用的服务，一个是对服务进行灵活组装和编排满足业务需求，SOA核心是业
务和技术的解耦，服务和能力的复用。而在IT领域的平台平台的概念目前基本上有三种，一种是基于快速开发目的技术平台，第二种是基于业务逻辑复用的业务平
台。第三种平台基于系统自维护，自扩展的应用平台。技术平台和业务平台都是软件开发人员使用的平台，而应用平台则是应用软件用户使用的平台。<br />
<br />
<span style="font-weight:bold;">SOA本身是一个平台</span></p>
<p><img src="/Upload/EditorImage/image/arch/201111/20111127103222_6373.jpg" alt="" border="0" /></p>
<p>首先要认识到SOA产品本身就是一个集成平台，为了完成数据集成，应用集成和流程集成，我们需要一个基础平台来集中统一的完成这个事情。因此SOA集成平
台重点则是将服务集成和服务组装的能力全部管理起来，从这个概念上理解自然涉及到SOA最核心的UDDI，ESB和BPEL等功能。这些集成的特性和功能
完全是在各个业务系统外的，又是大家都需要的基础公共功能，则应该纳入到平台集中管理。<br />
<br />
SOA称之为平台的原因则是它提供了业务系统都需要的通用基础架构和技术，这种能力是大家都需要的公共基础能力。但是如果我们买入一个SOA套装产品，这
个产品可能刚开始没有任何服务接入，也无法提供任何服务能力，那么这个产品本身就是一个技术平台。如果后面我们将数据服务，业务服务，流程服务全部识别，
开发后发布到平台上，那么SOA平台本身就是一个专属某个业务域的业务平台。<br />
<br />
<span style="font-weight:bold;">平台本身要考虑SOA化</span></p>
<p><img src="/Upload/EditorImage/image/arch/201111/20111127103256_0995.png" alt="" border="0" /></p>
<p>平台的核心是它是一个公共的基础能力提供中心，平台本身已经集中了所有可复用的能力，开发框架和技术。通过平台我们可以快速的开发平台和应用。<br />
<br />
在软件开发里面我们比较关注技术平台，而技术平台简单理解就是所有业务系统标准的技术能力的抽象和封装形成了一个不承载具体业务的空框架。这个空框架不仅仅是简单的技术开发框架，而是融入了更多的基础服务能力。<br />
<br />
基础服务能力包括两个层面，一个是纯粹技术方面的基础服务能力，包括日志，异常，消息，事务，国际化，缓存等内容。一个是业务抽象后和业务无关的技术能
力，包括通用权限模型，通用流程模型，邮件短信组件，常用UI组件。有了这些，你会发现技术平台本身就是一个完完整整可以独立运行起来的应用，除了没有具
体业务功能外其他一应俱全。<br />
<br />
基于该平台我们可以开发业务应用，那如果平台本身是基于SOA架构的，那么开发的应用自然也就满足SOA架构风格。一个应用功能应该包括资源层，技术组
件，服务组件层，流程层等。一个应用功能在实现业务过程中能够看到业务和技术实现是剥离和松耦合的。一个应用它本身是基于在技术平台上开发的多个业务组件
构成的，业务组件间本身松耦合，业务组件间通过服务进行交互。<br />
<br />
基于平台开发的业务应用，本身业务组件之间可能基于ESB总线方式进行消息交换，我们可以联想下java里面的IOC控制反转模式，和通过ESB总线进行交互的思路是很类似的，只有这样才能够实现组件之间进一步的解耦。<br />
<br />
传统的很多快速软件开发平台是完全不能符合SOA架构风格要求的，因为基于某些快速开发平台开发出来的应用本身就是紧耦合和封闭的应用。而基于SOA架构
风格的平台开发出来的应用本身就是完全组件化和服务化的应用，应用内部SOA化和松耦合，应用和应用之间也松耦合，应用本身可复用的能力很容易通过服务方
式暴露出来。<br />
<br />
<span style="font-weight:bold;">技术平台和业务平台</span></p>
<p><img src="/Upload/EditorImage/image/arch/201111/20111127103351_3862.jpg" alt="" border="0" /></p>
<p>首先要说明技术平台和业务平台都是开发人员关注的平台，而应用平台可能是最终用户关注的平台。可以看到技术平台和业务平台的划分正好体现了业务和技术本身的一次解耦。<br />
<br />
技术平台只关注和业务完全无关的技术本身，将各种技术能力组件化和服务化，提供各种通用的基础服务和技术服务。技术平台提供一套完整的开发框架和开发环境，同时也提供一套完整的执行环境。技术平台既为产品服务，也为业务平台服务。<br />
<br />
而细分到某个专业的业务域后可能出现业务平台，业务平台也可以叫做产品平台，比如电信的BOSS平台，网管平台都是很常见的业务平台。那业务平台和技术平
台最大的不同是业务平台本身提供了开发多个产品都可复用的业务组件和业务能力。业务平台本身是对多个同类产品通用业务能力的抽取，提炼和下沉。<br />
<br />
应用产品-&gt;产品平台-&gt;技术平台，正好体现了可复用能力的逐层下移，下移的目的只有一个即最大限度的复用，而复用的好处是对通用能力统一管
理和统一维护。我们可以试想下，如果没有这种平台，当我们面对N个产品开发对应N套产品开发源代码的时候，对用通用功能的修改调整和升级将是一件巨大的麻
烦。</p>
<p>作者：人月神话&nbsp;&nbsp; 来源：新浪博客&nbsp;&nbsp; <a target="_blank" href="http://blog.sina.com.cn/s/blog_493a84550100tsh7.html">原文链接</a></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/27/rely-and-joins.html]]></link>
<title><![CDATA[解依赖与接缝]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sun, 27 Nov 2011 10:11:30 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[接缝（seam）是Michael C. Feathers提出的概念。Feathers在Working Effectively with Legacy Code一书中对接缝的定义如下：<blockquote>
<p>接缝，顾名思义，就是指程序中的一些特殊的点，在这些点上你无需作任何修改就可以达到改动程序行为的目的。</p>
</blockquote>
<p>“接缝”这个词语不太好理解，根据我的理解，大约还是依赖点的含义。通过事先找到依赖点，并采取一定方式解除依赖，就能够改善代码质量，尤其是
针对遗留代码而言。准确而言，我们寻找接缝以及解依赖，就是为了代码能够具有好的可重用性与可扩展性，尤其是当我们能解除对其他外部服务的依赖时，可以带
来程序的可测试性。</p>
<p>最近项目组的同事和我讨论了这样一个满足可测试性的问题。项目中需要对返回的响应信息PlatformResponse进行处理，这些信息会根
据不同的StatusCode，得到不同的提示或出错信息。为了避免分支语句的判断，同事利用hash 
table将StatusCode与提示（出错）信息进行了映射，然后根据当前的StatusCode就可以返回对应的结果。返回结果后，还需要调用外部
服务对消息进行处理，例如消息的输出。由于之前相关的类PlatformResponse并没有提供这一逻辑，相关服务要返回消息时，直接返回了
PlatformResponse对象，然后再由客户端根据当前的StatusCode来判断，输出相关的提示信息，所以同事将这些逻辑写到了扩展方法
中，例如定义PlatformResponseHelper静态类：</p>
<pre class="brush:csharp;">public static class PlatformResponseHelper {
    private static HashTable<string,string> messageMapping = //此处略
    public static void Output(this PlatformResponse response) {
        ServiceLocator.Lookup<imessagewriter>.Write(messageMapping[response.StatusCode]);
    }
}</imessagewriter></string,string></pre>通过引入扩展方法，Controller得到的PlatformResponse对象就可以通过调用扩展方法Output()输出获得的提示(出
错)信息。注意，在上面的代码中，ServiceLocator是一个单例的服务定位器对象，通过它可以获得注册的服务。在Controller中，同样
调用了ServiceLocator来获得它所需的业务服务。<p></p>
<p>现在，我们需要进行单元测试。项目之前已经为ServiceLocator提供了Mock对象，并且该对象在Controller中也是通过依
赖注入的方式获得的。所以，在测试Controller时，可以通过注入模拟的ServiceLocator对象进行测试，从而解除与外部服务之间的依赖
关系。现在，在增加了PlatformResponse的扩展方法时，遇到了难题，即如何解除扩展方法与ServiceLocator之间的依赖关系？</p>
<p>显然，这里的ServiceLocator.Lookup<imessagewriter>.Write()方法调用就是前面所说
的“接缝”。我们希望在单元测试中不依赖于ServiceLocator，这就需要解除PlatformResponse与ServiceLocator
之间的耦合关系。同事希望既能达到可测试性的目的，又要保障调用的简单。</imessagewriter></p>
<p>在面向对象设计中，最常见的解除依赖的方法是职责分离以及抽象，或者利用反射或IOC容器来解除具体依赖。由于要解除与
ServiceLocator的耦合关系，再加上调用PlatformResponse相关方法的Controller也是通过依赖注入
ServiceLocator对象的，所以我首先想到将ServiceLocator转移到扩展方法的外部，通过传入参数的方式注入依赖。由于这个对象是
单例的，因此Controller获得的ServiceLocator也就是PlatformResponse需要的对象。当我们调用
PlatformResponse的Output()方法时，可以将Controller获得的ServiceLocator对象作为方法参数传给
PlatformResponse。在Controller层，我们利用依赖注入注入Mock对象，就可以达到较好的可测试性了。</p>
<p>然而，倘若要这样做，就需要将调用代码改为：</p>
<pre class="brush:csharp;">businessService.Response.Output(serviceLocator)；</pre>同事觉得在调用Output()方法时，还需要传入ServiceLocator对象，实在不够优雅而简洁。<p></p>
<p>由于C#的扩展方法有很多限制，例如它要求必须是静态类和静态方法，很难利用OO的一些特性，所以我想到的第二个方案是不采用扩展方法，而是将
之前的逻辑直接封装到PlatformResponse中。我们可以将Output()方法定义为虚方法，然后再为测试定义
PlatformResponse的子类，它将作为测试使用的Mock类，重写Output()方法。遗憾的是，系统基本上都是在调用外部服务的时候才获
得的PlatformResponse对象。我们不可能去修改服务对象，使其在单元测试时返回该类的子类对象。</p>
<p>第三条路是转移职责，将扩展方法Output()转移到一个专门的类，例如OutputMessage，由它来负责管理StatusCode与提示（出错）消息之间的映射关系，以及消息的输出，然后由子类重写消息处理的逻辑，完成模拟。例如代码：</p>
<pre class="brush:csharp;">public class OutputMessage {
    private PlatformResponse response;
    private HashTable<string, string=""> messageMapping = //此处略
    public OutputMessage(PlatformResponse response) {
        this.response = response;
    }
    public void Output() {
        OutputInternal();
    }
    protected virtual void OutputInternal() {
        ServiceLocator.Lookup<imessagewriter>.Write(messageMapping[response.StatusCode]);
    }
}
public class MockOutputMessage {
    public MockOutputMessage(PlatformResponse response):base(response) {}
    protected override void OutputInternal() {
    //模拟国际化服务对消息进行处理;
   }
}</imessagewriter></string,></pre>实际上这一方案是第二种方案的一种变化。因为我们无法修改一个已经被广泛使用的类，所以只能在引入新职责的时候，通过引入新生类来完成职责的增加，并利用子类重写的方式达到可测试的目的。<p></p>
<p>可是这一方案实际上更无法达到同事的目标，因为改动后的调用变得比第一种方案更复杂：</p>
<pre class="brush:csharp;">new OutputMessage(businessService.Response).Output();</pre>我们必须考虑OutputMessage对象的创建，同事还需要将PlatformResponse对象传入，再调用它的Output()方法。虽然不需要传入方法参数，但对象的创建以及构造函数参数的传入，反而让事情变得更复杂。<p></p>
<p>那么，应该怎么办？同事的理想目标是调用简单。就目前而言，在C#中，只有扩展方法才能让我们对PlatformResponse对象的
message处理显得如此的自然而简洁。再加上PlatformMessage对象已经被广泛使用，因此从PlatformMessage类的角度进行
处理，就变得不再可能。</p>
<p>让我们再来仔细思考“接缝”的问题。是谁引入了依赖点？接缝是调用ServiceLocator这条语句，而它的目的实际上是需要获得
IMessageWriter。是这个外部服务成为测试的障碍。所以解决的重点应该是解除与IMessageWriter之间的依赖。要这样做，就需要修
改Output()扩展方法，使其能够传入IMessageWriter对象。这种改进事实上与第一种方案没有什么区别，唯一的不同是它依赖于更小的接
口，而不是全局的ServiceLocator对象。我认为，这已经是一个最好的方案了。但是同事依旧执著于调用的简单性。他认为，不能为了单元测试，而
改变客户端调用的方式。</p>
<p>现在，我们已经明白扩展方法是最简单的实现方式，纠结仅仅在于IMessageWriter服务的获取方式而已。在产品代码中，我们可以通过
ServiceLocator来获得IMessageWriter对象，而在测试的时候，我们又需要模拟该服务对象。若要两全齐美，只有区分测试与生产环
境。事实上，这是我最初想到的做法，就是引入预定义来区分测试与真正的生产环境。但同事无法接受这种C++所主要采取的预定义做法。因此，我唯一能想到的
是修改PlatformMessage类的定义，提供设置IMessageWriter的属性（因为C#并不支持扩展属性），并在Output()方法中
判断IMessageWriter对象是否为null。如果为null，则说明它没有在测试环境下注入，这就需要通过ServiceLocator获得。</p>
<pre class="brush:csharp;">public class PlatformResponse {
    private IMessageWriter writer = null;
    public IMessageWriter MessageWriter {
        get; set;
    }
}
public static class PlatformResponseHelper {
    private static HashTable<string,string> messageMapping = //略去

    public static void Output(this PlatformResponse response) {
        String message = messageMapping[response.StatusCode];
        //如果不为null，说明是测试注入了该对象;
       if (response.MessageWriter !=null) {
           response.MessageWriter.Write(message);
       } else {
           ServiceLocator.Lookup<imessagewriter>.Write(message);
       }
    }
}</imessagewriter></string,string></pre>虽然我修改了PlatformResponse类的定义，但由于需要调用或创建PlatformResponse对象的外部服务并不需要新增加的
MessageWriter属性，因此这样的修改实际上是扩展，并不会影响到以前的代码。这就是我唯一能够想到的满足同事要求的方案。在测试时，我们可以
通过为PlatformResponse注入模拟的IMessageWriter对象，而在真正的产品代码中，则无需为它设置MessageWriter
属性，而是直接调用它的扩展方法Output()。美中不足之处在于它为调用者提供了一定的开放性，使得调用者能够自由设置MessageWriter属
性，破坏了对象的封装。为使这种破坏带来的影响降到最低，在单元测试放在同一个项目的前提下，可以考虑将MessageWriter属性定义为
internal。<p></p>
<p>我们常常需要在灵活性和简单性之间进行设计权衡。大多数情况下，都可能产生非此即彼的选择。要做到两全齐美真的很难。从面向对象的角度来看，我
不认为最后的方案是最佳方案。其实，通过方法参数注入IMessageWriter的做法已经足够好了，它并没有加大结构与调用的复杂性。无论怎样，设计
总是见仁见智的问题，就看大家的选择了。唯一需要遵循的原则，就是设计必须结合具体的场景来做出正确的决定。</p>
<p>来源: <a href="http://www.agiledon.com/" target="_blank">捷道</a>&nbsp;&nbsp;&nbsp; <a target="_blank" href="http://www.agiledon.com/?p=436">原文链接</a></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/26/talk-about-youku-architecture.html]]></link>
<title><![CDATA[优酷网架构学习笔记]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sat, 26 Nov 2011 11:14:55 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>记得以前给大家介绍过视频网站龙头老大<a target="_blank" href="http://www.itivy.com/ivy/archive/2011/3/6/634350416046298451.html">YouTube的技术架构</a>，
相信大家看了都会有不少的感触，互联网就是这么一个神奇的东西。今天我突然想到，优酷网在国内也算是视频网站的老大了，不知道他的架构相对于
YouTube是怎么样的，于是带着这个好奇心去网上找了优酷网架构的各方面资料，虽然谈得没有YouTube那么详细，但多少还是挖掘了一点，现在总结
一下，希望对喜欢架构的朋友有所帮助。</p>
<p style="border:1px dotted #aaaaaa;padding:5px;background-color:#fbfcf1;font-weight:bold;"><span style="color:#64451d;font-size:14px;">一、网站基本数据概览</span></p>
<ul><li>据2010年统计，优酷网日均独立访问人数（uv)达到了8900万，日均访问量（pv）更是达到了17亿，优酷凭借这一数据成为google榜单中国内视频网站排名最高的厂商。</li>
<li>硬件方面，优酷网引进的戴尔服务器主要以 PowerEdge 1950与PowerEdge 
860为主，存储阵列以戴尔MD1000为主，2007的数据表明，优酷网已有1000多台服务器遍布在全国各大省市，现在应该更多了吧。</li>
</ul>
<p style="border:1px dotted #aaaaaa;padding:5px;background-color:#fbfcf1;font-weight:bold;"><span style="color:#64451d;font-size:14px;">二、网站前端框架</span></p>
<p>从一开始，优酷网就自建了一套CMS来解决前端的页面显示，各个模块之间分离得比较恰当，前端可扩展性很好，UI的分离，让开发与维护变得十分简单和灵活，下图是优酷前端的模块调用关系：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20110910151338_0929.jpg" alt="" border="0" height="475" width="762" /></p>
<p>这样，就根据module、method及params来确定调用相对独立的模块，显得非常简洁。下面附一张优酷的前端局部架构图：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20111020120723_5812.jpg" alt="" border="0" height="490" width="760" /></p>
<p>&nbsp;</p>
<p style="border:1px dotted #aaaaaa;padding:5px;background-color:#fbfcf1;font-weight:bold;"><span style="color:#64451d;font-size:14px;">三、数据库架构</span></p>
<p>应该说优酷的数据库架构也是经历了许多波折，从一开始的单台MySQL服务器（Just Running）到简单的MySQL主从复制、SSD优化、垂直分库、水平sharding分库，这一系列过程只有经历过才会有更深的体会吧，就像<a target="_blank" href="http://www.itivy.com/ivy/archive/2011/3/7/634351257301504864.html">MySpace的架构经历</a>一样，架构也是一步步慢慢成长和成熟的。</p>
<p style="font-weight:bold;">1、简单的MySQL主从复制:</p>
<p>MySQL的主从复制解决了数据库的读写分离，并很好的提升了读的性能，其原来图如下：</p>
<p><img src="../Upload/EditorImage/20110910151549_5879.jpg" alt="" border="0" /></p>
<p>其主从复制的过程如下图所示：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20110910151641_1001.jpg" alt="" border="0" height="457" width="620" /></p>
<p>但是，主从复制也带来其他一系列性能瓶颈问题：</p>
<ol><li>写入无法扩展</li>
<li>写入无法缓存</li>
<li>复制延时</li>
<li>锁表率上升</li>
<li>表变大，缓存率下降</li>
</ol>
<p>那问题产生总得解决的，这就产生下面的优化方案，一起来看看。</p>
<p style="font-weight:bold;">2、MySQL垂直分区</p>
<p>如果把业务切割得足够独立，那把不同业务的数据放到不同的数据库服务器将是一个不错的方案，而且万一其中一个业务崩溃了也不会影响其他业务的正常进行，并且也起到了负载分流的作用，大大提升了数据库的吞吐能力。经过垂直分区后的数据库架构图如下：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20110910151747_8225.jpg" alt="" border="0" height="476" width="691" /></p>
<p>然而，尽管业务之间已经足够独立了，但是有些业务之间或多或少总会有点联系，如用户，基本上都会和每个业务相关联，况且这种分区方式，也不能解决单张表数据量暴涨的问题，因此为何不试试水平sharding呢？</p>
<p>&nbsp;</p>
<p style="font-weight:bold;">3、MySQL水平分片（Sharding）</p>
<p>这是一个非常好的思路，将用户按一定规则（按id哈希）分组，并把该组用户的数据存储到一个数据库分片中，即一个sharding，这样随着用户数量的增加，只要简单地配置一台服务器即可，原理图如下：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20110910151842_9228.jpg" alt="" border="0" height="408" width="690" /></p>
<p>如何来确定某个用户所在的shard呢，可以建一张用户和shard对应的数据表，每次请求先从这张表找用户的shard id，再从对应shard中查询相关数据，如下图所示：</p>
<p><img src="http://www.itivy.com/Upload/EditorImage/20110910152117_6934.jpg" alt="" border="0" height="461" width="688" /></p>
<p>但是，优酷是如何解决跨shard的查询呢，这个是个难点，据介绍优酷是尽量不跨shard查询，实在不行通过多维分片索引、分布式搜索引擎，下策是分布式数据库查询（这个非常麻烦而且耗性能）</p>
<p>&nbsp;</p>
<p style="border:1px dotted #aaaaaa;padding:5px;background-color:#fbfcf1;font-weight:bold;"><span style="color:#64451d;font-size:14px;">四、缓存策略</span></p>
<p>貌似大的系统都对“缓存”情有独钟，从http缓存到memcached内存数据缓存，但优酷表示没有用内存缓存，理由如下：</p>
<ol><li>避免内存拷贝，避免内存锁</li>
<li>如接到老大哥通知要把某个视频撤下来，如果在缓存里是比较麻烦的</li>
</ol>
<p>而且Squid 的 write() 用户进程空间有消耗，Lighttpd 1.5 的 AIO(异步I/O) 读取文件到用户内存导致效率也比较低下。</p>
<p>但为何我们访问优酷会如此流畅，与土豆相比优酷的视频加载速度略胜一筹？这个要归功于优酷建立的比较完善的内容分发网络（CDN），<span style="font-family:Verdana;">它
通过多种方式保证分布在全国各地的用户进行就近访问——用户点击视频请求后，优酷网将根据用户所处地区位置，将离用户最近、服务状况最好的视频服务器地址
传送给用户，从而保证用户可以得到快速的视频体验。这就是CDN带来的优势，就近访问，有关CDN的更多内容，请大家Google一下。</span></p>
<p style="border:1px dotted #aaaaaa;padding:5px;background-color:#fbfcf1;font-weight:bold;"><span style="color:#64451d;font-size:14px;">五、其他相关架构文章推荐</span></p>
<p><a href="http://www.itivy.com/ivy/archive/2011/3/7/634351294385186067.html">Flickr网站架构</a><br />
<a href="http://www.itivy.com/ivy/archive/2011/3/7/634351257301504864.html">回顾MySpace架构的坎坷之路</a><br />
<a target="_blank" href="http://www.itivy.com/ivy/archive/2011/8/16/the-architecture-of-yupoo.html">国内图片网站Yupoo架构</a><br />
<a href="http://www.itivy.com/ivy/archive/2011/8/14/the-architecture-of-twitter.html">twitter网站架构</a><br />
<a href="http://www.itivy.com/ivy/archive/2011/3/5/634349627089221280.html">PlentyOfFish.com .NET网站的又一传奇</a> </p>
<p>作者：王国峰&nbsp; 来源：<a target="_blank" href="http://www.itivy.com/">青藤园</a>&nbsp; <a target="_blank" href="http://www.itivy.com/ivy/archive/2011/8/13/the-architecture-of-youku.html">原文链接</a></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/26/talk-about-cloud-computing.html]]></link>
<title><![CDATA[闲谈：如何解释云计算]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Sat, 26 Nov 2011 10:51:43 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[近来一位朋友向我请教，什么是云计算？这个问题说难不难，说简单不简单。难是因为没有统一的认同的概念。容易回答，也是因为没统一定义，可以按自己的理解说，如果真有切身实践经验或想法的话。<p style="margin:4px 0px;padding:2px 0px;">
<strong>首先，你得看你的对象是谁。</strong></p>
<p style="margin:4px 0px;padding:2px 0px;">
1）如果面对的是一位中国的专家级人物，也许你可以讲一下中国云计算网的定义:</p>
<blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
<span style="font-family:FangSong_GB2312;">“云计算是并行计算、分布式计算和网格计算的发展，或者说是这些科学概念的商业实现。</span>”</p>
</blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
这类定义很中国特色，即“拿一个复杂的概念去解释一个更复杂的概念”(导师如是说)，反正到最后一般人还是不懂。做这个定义的人，也许也未必真的能讲清楚并行计算、分布式计算和网格计算。</p>
<p style="margin:4px 0px;padding:2px 0px;">
2）如果面对的是一位计算机专业或IT从业的人，但只是听过云计算，但没涉猎过。为了省事，给他看维基百科的<a href="http://http//zh.wikipedia.org/wiki/%E4%BA%91%E8%AE%A1%E7%AE%97" target="_self">云计算</a>​的定义吧，个人觉得讲得挺清楚的。</p>
<blockquote>
<p><span style="font-family:FangSong_GB2312;font-size:13px;"><strong>“云计算</strong>（<a title="英语" href="http://zh.wikipedia.org/wiki/%E8%8B%B1%E8%AF%AD" style="color:#0645ad;text-decoration:none;">英语</a>：<span lang="en"><strong>Cloud Computing</strong></span>），是一种基于<a title="互联网" href="http://zh.wikipedia.org/wiki/%E4%BA%92%E8%81%94%E7%BD%91" style="color:#0645ad;text-decoration:none;">互联网</a>的计算方式，通过这种方式，共享的软硬件资源和信息可以按需提供给计算机和其他设备。整个运行方式很像电网。</span></p>
<p><span style="font-family:FangSong_GB2312;font-size:13px;">云计算是继1980年代<a title="大型计算机" href="http://zh.wikipedia.org/wiki/%E5%A4%A7%E5%9E%8B%E8%AE%A1%E7%AE%97%E6%9C%BA" style="color:#0645ad;text-decoration:none;">大型计算机</a>到<a class="mw-redirect" title="客户端-服务器" href="http://zh.wikipedia.org/wiki/%E5%AE%A2%E6%88%B7%E7%AB%AF-%E6%9C%8D%E5%8A%A1%E5%99%A8" style="color:#0645ad;text-decoration:none;">客户端-服务器</a>的大转变之后的又一种巨变。用户不再需要了解“云”中基础设施的细节，不必具有相应的专业知识，也无需直接进行控制。<sup id="cite_ref-0" class="reference" style="white-space:nowrap;"><a href="http://zh.wikipedia.org/wiki/%E4%BA%91%E8%AE%A1%E7%AE%97#cite_note-0" style="color:#0645ad;text-decoration:none;">[1]</a></sup>&nbsp;云计算描述了一种基于互联网的新的IT服务增加、使用和交付模式，通常涉及通过互联网来提供动态<a title="可扩放性" href="http://zh.wikipedia.org/wiki/%E5%8F%AF%E6%89%A9%E6%94%BE%E6%80%A7" style="color:#0645ad;text-decoration:none;">易扩展</a>而且经常是<a class="mw-redirect" title="虚拟化" href="http://zh.wikipedia.org/wiki/%E8%99%9A%E6%8B%9F%E5%8C%96" style="color:#0645ad;text-decoration:none;">虚拟化</a>的资源。<sup id="cite_ref-gartner_1-0" class="reference" style="white-space:nowrap;"><a href="http://zh.wikipedia.org/wiki/%E4%BA%91%E8%AE%A1%E7%AE%97#cite_note-gartner-1" style="color:#0645ad;text-decoration:none;">[2]</a></sup><sup id="cite_ref-really_2-0" class="reference" style="white-space:nowrap;"><a href="http://zh.wikipedia.org/wiki/%E4%BA%91%E8%AE%A1%E7%AE%97#cite_note-really-2" style="color:#0645ad;text-decoration:none;">[3]</a></sup>&nbsp;云其实是网络、互联网的一种比喻说法。因为过去在图中往往用云来表示电信网，后来也用来表示<a class="mw-redirect" title="互联网" href="http://zh.wikipedia.org/wiki/%E7%B6%B2%E9%9A%9B%E7%B6%B2%E8%B7%AF" style="color:#0645ad;text-decoration:none;">互联网</a>和底层<a class="mw-redirect" title="基础设施" href="http://zh.wikipedia.org/wiki/%E5%9F%BA%E7%A4%8E%E8%A8%AD%E6%96%BD" style="color:#0645ad;text-decoration:none;">基础设施</a>的抽象。典型的云计算提供商往往提供通用的网络业务应用，可以通过浏览器等软件或者其他Web服务来访问，而软件和数据都存储在服务器上。云计算关键的要素，还包括个性化的用户体验。”</span></p>
</blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
&nbsp;&nbsp;&nbsp;&nbsp;</p>
<p style="margin:4px 0px;padding:2px 0px;">
​3）如果你自己都看不懂，或者别人看了还是不懂。那么我尝试给一种更浅白的解释：</p>
<p style="margin:4px 0px;padding:2px 0px;">
​云计算，就是“<strong>云态的网络设备</strong>” 向外提供的“<strong>按需动态变化的</strong>”一系列<strong>计算服务</strong>。</p>
<ul style="list-style-type:disc;"><li>
<p style="margin:4px 0px;padding:2px 0px;">
云态的网络端，指的是<strong>云计算服务提供者是一群协同工作的网络设备</strong>。云一方面说明了<strong>其规模化</strong>，另一方面云的特性（云碰到云变大云）说明这群协作的网络设备的规模是<strong>易于扩展的</strong>（从整一片云的角度看，虽说要求易于scale out，但并不关注scale in），再者对于观云者我们是看不到云里面的内容，这也说明<strong>云的内部对用户而言是不可见的</strong>。</p>
</li>
<li>
<p style="margin:4px 0px;padding:2px 0px;">
​按需动态变化的，表示提供的服务能力是动态变化的，还是按需提供的，同时云本身也是动态可变的。但目前而言，从自己了解的，觉得这个服务的动态调整的粒
度并不细。一开始还以为Amazon提供的CPU、存储等服务的粒度都是很很很细粒度的（比如具体用了多少就是多少），结果发现是我太天真了。</p>
</li>
<li>
<p style="margin:4px 0px;padding:2px 0px;">
​计算服务，在此是广义的计算服务，包括存储、CPU计算能力、网络能力等。</p>
</li>
</ul>
<p style="margin:4px 0px;padding:2px 0px;">
（​嗯，上面的内容，一般看过云计算内容的人都会明白的，甚至觉得讲得没什么信息了。嗯，其实我只想用自己的语言解释一遍而已。）</p>
<p style="margin:4px 0px;padding:2px 0px;">
<strong>​首先看对象，其次看你想往抓哪个点去讲。</strong></p>
<p style="margin:4px 0px;padding:2px 0px;">
其实这个跟对象也有关系，毕竟根据对象熟悉的点去讲，对方更容易接受。在此，自己谈谈本人对云计算更抽象化的认识——泛化的中间层理论。</p>
<p style="margin:4px 0px;padding:2px 0px;">
正如某句著名的话（突然忘了出处了）——“<strong>一切问题都可以通过增加一个中间层去解决</strong>”所述，我觉得云计算解决问题的方式也是如此的，加了一层，屏蔽了底下那层，向上提供更傻瓜化的服务，然后更多人能使用了。</p>
<p style="margin:4px 0px;padding:2px 0px;">
当年多了一层操作系统，尤其是图形化操作系统的出现，普通人能玩计算机了。Java在OS上加了层虚拟机，Java应用能跨平台了。现在多了云计
算，理想来说，我们不用买神马CPU，主板，存储等硬件了，只要联网就成了。但单纯这样还不够，似乎跟网络虚拟桌面区分不开呢！</p>
<blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
<span style="font-family:FangSong_GB2312;font-size:13px;">“这个网络虚拟桌面是什么呢？这个概念我也没研究过，只是上回说去某实验室交流云平台时，听那边一老师说，华为现在有些部门的人工作时前面就一个显示屏，一根网线，打开屏幕见登录界面，一登录就进入相应自己工作的操作系统环境了。”</span></p>
</blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
​如果只是登录一下，然后服务端读取用户信息，返回用户内容，个人觉得还不够云呢！要够云，就得体现按需动态提供的能力，比如默认配置了很拙的计
算能力（比如配了一个不给力的CPU），现在突然要进行一个高密度的计算任务，比如科学计算任务，那云应该按我的需求提供更强力的计算能力，可不能让我按
之前的CPU能力等上几月半年才算出来。呃，当然，这样最后结算也是得多给些money的。似乎我又说了些理想化的事情，目前而言，CPU类的计算能力以
及存储能力还是很难算流量那般结算的，相对而言，目前带宽还是有可能做到的。</p>
<p style="margin:4px 0px;padding:2px 0px;">
​所以，云计算就是增加了一个中间层然后解决相应了问题。I云（IaaS）多了设备虚拟层，用户在其上不用care硬件设备了；在P云上的用户不用care平台以下的东西；在S云上面的，用不着安装相应软件就能使了哦（呃，现在还是得装个浏览器的。）</p>
<p style="margin:4px 0px;padding:2px 0px;">
嗯嗯，似乎有些太抽象了。那试试讲云计算产品的特征，即如何去识别云计算。</p>
<p style="margin:4px 0px;padding:2px 0px;">
从云计算概念的发展历程，个人觉得云计算的服务也好，产品也好，一般都有以下两大特征：</p>
<ol style="list-style-type:decimal;"><li>
<p style="margin:4px 0px;padding:2px 0px;">
<strong>内容在网络端</strong>。很多云产品给用户的感觉就是从本地端迁移到网络端，比如I和S，其中一般会拿S做说明。比如一般会讲
Salesforce的故事，从基于CRM客户端提供服务到基于web 
CRM提供服务。内容在网络端，一般还会延伸出云计算许多卖点，如异地设备同步啊，多种终端访问啊。</p>
</li>
<li>
<p style="margin:4px 0px;padding:2px 0px;">
<strong>可按需调度计算资源</strong>。按需调度，即以合理分配的策略进行资源的动态调度。动态调度是云计算最主要的技术特征之一，云计算大多的技术点和学术研究点都基于此，比如多副本协作、虚拟机动态迁移等等。</p>
</li>
</ol>
<p style="margin:4px 0px;padding:2px 0px;">
这两个特征，应该是要并存的，不能只有1而没2（没有1只有2，呃，这就是本地设备了吧~）。比如电子邮件服务，能称作云服务的，我直接想到的是Gmail，在我的认识中，G的服务都是S。而以前的电子邮箱服务称不上云，主要是因为没有动态性。&nbsp;&nbsp;&nbsp;&nbsp;</p>
<p style="margin:4px 0px;padding:2px 0px;">
现在很多打着云的旗号的产品，我们很难知道它们是不是真的云产品，因为一般我们只能知道内容是放在网络端了，但云内是否有一套体现动态性的系统，我们不知道。所以有了以下问题：</p>
<p style="margin:4px 0px;padding:2px 0px;">
——​云是要给用户感知的？！</p>
<p style="margin:4px 0px;padding:2px 0px;">
本人不以为然。<strong>云给用户的体验应该是一直保持良好的可用性，所以云就应该让用户无法感知到它。</strong>比如一个基于
云的服务，在低访问量和高访问量的情况下，给用户的体验应该是一致的，但对于访问的用户而言，他并不知道服务端是否因访问量上升而增多服务的计算资源。如
果让用户感知到差异，只能表示这云做得不够好。所以云的提出是为了提供服务质量的，其本身便不是让用户体验的。因此面对这么多“云产品”，用户并不知道它
是不是真的云，尤其对于SaaS。只要用户觉得体验好，其实就足够了。</p>
<p style="margin:4px 0px;padding:2px 0px;">​呃，绕了一圈，得出用户无法感知云，表示也难以识别云，似乎有些坑人了。其实，我只想说明，不要轻易相信“云”，因为我们很难知道；同时，只要有“云”的良好体验，其实我们用户也用不着理会它是不是真的有一套复杂的动态调度机制去支撑的云。</p>
<p style="margin:4px 0px;padding:2px 0px;">
说了这么多，除了是为了总结自己的思考之外，还想说明的是，在理解云计算的时候，别人云亦云。在理解别人的说法的同时，要有自己的理解，同时更
新自己的理解。导师曾经说过，他认为的云计算的两大特征是动态化和服务化。咋一想，挺有道理的，导师再一讲，觉得更有道理了。要理解云计算，要找个适当的
点。</p>
<div>&nbsp;&nbsp;&nbsp;&nbsp;​最后想谈谈自己对维基百科上云计算特征的意见：</div>
<blockquote>
<div>
<p style="margin:0.4em 0px 0.5em;padding:2px 0px;line-height:1.5em;">
<span style="font-family:FangSong_GB2312;font-size:13px;">“互联网上的云计算服务特征和自然界的<a title="云" href="http://zh.wikipedia.org/wiki/%E4%BA%91" style="color:#0645ad;text-decoration:underline;">云</a>、<a title="水循环" href="http://zh.wikipedia.org/wiki/%E6%B0%B4%E5%BE%AA%E7%8E%AF" style="color:#0645ad;text-decoration:underline;">水循环</a>具有一定的相似性，因此，云是一个相当贴切的比喻。通常云计算服务应该具备以下几条特征：</span></p>
<ul style="padding:0px;line-height:1.5em;list-style-type:square;margin:0.3em 0px 0px 1.5em;"><li style="margin-bottom:0.1em;">
<p style="margin:4px 0px;padding:2px 0px;">
<strong><u><span style="font-family:FangSong_GB2312;font-size:13px;">基于虚拟化技术快速部署资源或获得服务</span></u></strong></p>
</li>
<li style="margin-bottom:0.1em;">
<p style="margin:4px 0px;padding:2px 0px;">
<span style="font-family:FangSong_GB2312;font-size:13px;">实现动态的、可伸缩的扩展</span></p>
</li>
<li style="margin-bottom:0.1em;">
<p style="margin:4px 0px;padding:2px 0px;">
<span style="font-family:FangSong_GB2312;font-size:13px;">……”</span></p>
</li>
</ul>
</div>
</blockquote>
<p style="margin:4px 0px;padding:2px 0px;">
说云计算服务特征与自然的云或水有相似性，这难以厚非，毕竟用云这个概念就是想用自然云去形容其特征。但特征里的第一条，“基于虚拟化技术”，个人表示很不解。</p>
<p style="margin:4px 0px;padding:2px 0px;">
<strong>云计算的实现，一定要用虚拟化吗？</strong>个人认为，虚拟化技术只是推进云计算的发展，因为虚拟化技术使实现云的规
模性变得简单了。如果没有虚拟化，以真机去实现云的规模是很吃力的，尤其当一般情况下，由于真机资源利用率低下，实现云浪费的资源是巨大。而虚拟化便是能
充分利用真机的资源，同时解决了大规模联机的问题。当物理主机本身资源利用率便很高的情况下，虚拟化的意义也不大。</p>
<p style="margin:4px 0px;padding:2px 0px;">
​再发散一下，如果有一台NB得不行的主机，同时能保证其资源的高利用率，那还需要做云吗？其实，云计算的网络端抽象成一个巨型主机的话，why
 not不直接用这样一个主机呢？ 
问题就是无法做到，才有的Google起初用各种廉价设备协同为世界提供强力搜索服务的美好故事，也有了现在云计算的需求。</p>
<h2>写在最后</h2>
<p style="margin:4px 0px;padding:2px 0px;">
写此文，最初只是自己想理清自己对云计算的思考。毕竟研一时自己看过些书，课程有相关内容，之后参与了实验室的云平台项目而且一直跟进着，自己觉得有必要总结总结，虽说比最初想写的多了许多内容，也显得零散许多。</p>
<p style="margin:4px 0px;padding:2px 0px;">
写得较杂，较乱，较浅，若有说的不是之处，望交流。</p>
<p style="margin:4px 0px;padding:2px 0px;">作者：<span>wenlele&nbsp; 来源：CSDN&nbsp; <a target="_blank" href="http://blog.csdn.net/wenlele/article/details/7002531">原文链接</a><br />
</span></p>]]></description>
</item>

<item>
<link><![CDATA[http://www.itivy.com/arch/archive/2011/11/25/memcached-test-in-cluster.html]]></link>
<title><![CDATA[集群架构实践 - 初试Memcached]]></title>
<author><![CDATA[架构点滴]]></author>
<category><![CDATA[]]></category>
<pubDate>Fri, 25 Nov 2011 23:19:47 GMT</pubDate>
<guid><![CDATA[]]></guid>
<description><![CDATA[<p>作者：<a target="_blank" href="http://www.itivy.com/ivy">王国峰</a>&nbsp; 来源：<a target="_blank" href="http://www.itivy.com/">青藤园</a>&nbsp; <a target="_blank" href="http://www.itivy.com/ivy/archive/2011/5/24/memcached-in-cluster-structure.html">原文链接</a></p>
<p>由于最近忙工作实习的事情，又要忙学校的毕业设计，所以很久没在博客上分享自己的技术实践成果了，真的很抱歉。今天我在整理自己毕业设计的时候，我
觉得有一样东西不得不推荐给大家，这个东西就叫Memcached。可能有些朋友已经对他非常熟悉，也可能已经用得非常溜，但我想对于像我一样的初学者来
说，这篇文章应该还是能帮助一些初学者朋友解决一些有关使用Memcached的问题的。</p>
<p>首先，我先简单介绍一下Memcached是什么样的东西。其实说白了他就是一个缓存框架，但与普通缓存不同的是他能支持分布式集群部署，想象一
下，当你需要缓存的数据很多时，你可能需要加入更多的服务器来满足你的数据缓存需求。下面是更专业的解释（其实太多的术语反而会让大家觉得这个东西很复
杂）。</p>
<blockquote>Memcached 
是一个高性能的分布式内存对象缓存系统，用于动态Web应用以减轻数据库负载。它通过在内存中缓存数据和对象来减少读取数据库的次数，从而提供动态、数据
库驱动网站的速度。Memcached基于一个存储键/值对的hashmap。其守护进程（daemon 
）是用C写的，但是客户端可以用任何语言来编写，并通过memcached协议与守护进程通信。</blockquote>
<p>很简单，他就是用来作缓存的，一般应用在你的应用和数据库之间，缓解数据库服务器的I/O负载压力。因为他能满足分布式集群的特性，所以我对他非常
感兴趣，于是自己就在.NET上简单实现了一个demo，顺便把实现的过程记录了下来，以便以后自己翻阅，当然也为那些初学者提供一个现成的思路。</p>
<p><span style="font-weight:bold;font-size:14px;color:#009900;">准备工作</span></p>
<p>下载Windows版本的Memcached服务器，下载地址：<a target="_blank" href="http://www.itivy.com/DownloadFile.ashx?id=634418560909724471">http://www.itivy.com/DownloadFile.ashx?id=634418560909724471</a></p>
<p>下载.NET中可用的Memcached客户端，这里我用了Enyim的客户端，下载地址：<a target="_blank" href="http://www.itivy.com/DownloadFile.ashx?id=634418565974010508">http://www.itivy.com/DownloadFile.ashx?id=634418565974010508</a></p>
<p><span style="font-weight:bold;color:#009900;font-size:14px;">配置工作</span></p>
<p style="font-weight:bold;">第一步先启动memcached服务：</p>
<p>将下载的压缩包解压到c:\</p>
<p>你可以直接双击memcached.exe图标运行memcached，我们还可以采取cmd命令行模式下启动memcached。</p>
<p><img src="/Upload/EditorImage/image/arch/201111/20111125231433_1148.jpg" alt="" border="0" /></p>
<p>这时候细心的你应该注意到你的taskmanager中多了一个memcached进程，是的，这时候memcached服务已经启动了。</p>
<p><span style="font-weight:bold;">第二步配置memcached客户端：</span></p>
<p>这里我们为了在演示中看到缓存的效果，故新建了两个网站，每一个网站都可以设置和读取缓存内容，如在webA中设置了缓存，可以在webB中获取由
webA设置的缓存内容。下面我们来配置一下这两个网站的web.config文件，在configuration节点下添加以下配置信息：</p>
<pre class="brush:xml;">&lt;configsections&gt;
    &lt;!--Memcached客户端配置信息--&gt;
    &lt;sectiongroup name="enyim.com"&gt;
      &lt;section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching"&gt;
    &lt;/section&gt;
    &lt;section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching"&gt;
  &lt;/section&gt;
  &lt;enyim.com&gt;
    &lt;memcached&gt;
      &lt;servers&gt;
        &lt;!-- put your own server(s) here--&gt;
        &lt;add address="192.168.1.150" port="11211"&gt;
      &lt;/add&gt;
      &lt;socketpool minpoolsize="10" maxpoolsize="100" connectiontimeout="00:00:00.5000000" deadtimeout="00:00:00.5000000"&gt;
    &lt;/socketpool&gt;
  &lt;/servers&gt;
  &lt;!--Memcached客户端配置信息--&gt;
  &lt;appsettings&gt;
    &lt;!--系统统一缓存策略--&gt;
    &lt;add key="CacheStrategy" value="MyCache.Core.MemcachedCacheStrategy,MyCache.Core"&gt;
    &lt;!--系统统一缓存策略的缓存时间--&gt;
    &lt;add key="CacheTimeOut" value="60"&gt;
  &lt;/add&gt;
&lt;connectionstrings&gt;
&lt;/connectionstrings&gt;
&lt;/add&gt;
&lt;/appsettings&gt;
&lt;/memcached&gt;
&lt;/enyim.com&gt;
&lt;/sectiongroup&gt;
&lt;/configsections&gt;</pre><p></p>
<p>这里可能你不太明白的是为什么在appSettings中加一个CacheStrategy，其实这个是为了抽象缓存接口，我们可以采用
Memcached缓存方案，也可以采用.NET自带的缓存方案，也可以是其他的缓存方案，所以还是让程序在运行时自动加载某种方案吧。这个我在最后会把
源代码发上来，你一看就明白怎么回事了。另外，配置中的IP自己设置，这种常识就不多说了。</p>
<p><span style="color:#009900;font-weight:bold;font-size:14px;">测试工作</span></p>
<p>OK，客户端也配置完成了，让我们来看看测试效果吧。在网站A，B中各设置两个按钮，一个用来设置缓存，一个用来获取缓存，按钮事件如下：</p>
<p>网站A设置缓存按钮代码：</p>
<pre class="brush:csharp;">protected void Button1_Click(object sender, EventArgs e)
{
     MyCache.Core.WebCache.Add("Test", "我是Test，我的值是A 来自 WEBA");
}</pre>网站A获取缓存按钮代码：<br />
<pre class="brush:csharp;">protected void Button2_Click(object sender, EventArgs e)
{
     Response.Write(MyCache.Core.WebCache.Retrieve("Test"));
}</pre>网站B设置缓存按钮代码：<br />
<pre class="brush:csharp;">protected void Button1_Click(object sender, EventArgs e)
{
     MyCache.Core.WebCache.Add("Test", "我是Test，我的值是B 来自 WEBB");
}</pre>网站B获取缓存按钮代码：<br />
<pre class="brush:csharp;">protected void Button2_Click(object sender, EventArgs e)
{
     Response.Write(MyCache.Core.WebCache.Retrieve("Test"));
}</pre>好了，开始测试吧，我们先在网站A中设置一下缓存，然后在网站A中获取缓存和在网站B中获取缓存，页面都显示如下：<p></p>
<p><img src="/Upload/EditorImage/image/arch/201111/20111125231737_3051.jpg" alt="" border="0" /></p>
<p>这说明我们在网站A中设置的缓存已经可以被网站A和网站B同时获取，OK，测试成功了。<img src="../Scripts/kind-editor/plugins/emoticons/42.gif" alt="" border="0" /></p>
<p>当然，我们在网站B中设置缓存也是一样，这里就不再赘述了。</p>
<p>最后附上测试源码，源码可以用vs2005及以上版本打开，<a target="_blank" href="http://www.itivy.com/DownloadFile.ashx?id=634418634686303270">点击下载源码</a>。希望本文能帮到一部分朋友。</p>]]></description>
</item>


</channel>
</rss>

