2012年9月27日星期四

CAP设计原则

CAP设计原则,是Brewer教授在2000年提出的,它包括了三个方面:
         C(consistency)一致性:数据的一致性,多台服务器间的数据保持一致。
         A(Availability)可用性:即表示性能要求,响应速度快,效率高。
         P(Partition tolerance):分区容错性。一个服务需要在局部出错的情况下,没有出错的那部分被复制的数据分区仍然可以支持部分服务的操作,可以简单的理解为可以很容易的在线增减机器以达到更高的扩展性,即所谓的横向扩展能力。

以上的三点,同时最多只能满足两点。而分区容错是必须要被满足的。大部分情况都会在CA中间进行取舍。

        在海量数据访问环境下,会有一些基本的策略来提高服务质量,例如:

1. 数据sharding。数据分片能够将海量数据的单张数据表拆分,从而提高单表查询效率。但是由此带来的数据一致性维护开销会大大加大。
2. 缓存(cache)。增加cache会大大提高访问速度,但是cache的扩展又是一个头疼的问题,即使在使用一致性hash算法的前提下,扩展节点仍然会造成cache的失效问题。我们还会有一个监控系统,来监控每个节点的cache的状态,包括容量,命中率等。
3. 灰度发布:只开放部分用户验证系统的可用性,在服务稳定后再向外完全推出也是一个很好的策略方案。
4. 监控和报警。没有完美运行的服务,及时发现解决问题是必不可少的。良好的监控报警能够使服务不断的得到优化,从而越来越健壮。
5. 配置中心化:中心化的配置和监控也是很好的策略,能够帮助管理海量服务,并随时监控服务状态。
6. 自定义的协议。

2012年9月20日星期四

数据底层的多数据源和分布式事务

    SOA(面向服务的架构)在已经相当流行;在大数据处理的时候,分表,分区,分库等操作也一直伴随着一起发展,这里就涉及到了数个数据源,读写分离,事务等一系列的问题。
    本文主要记录了通过封装一个dao底层,来实现多数据源分库操作,通过分布式事务保持数据的强一致性。
    SOA的事务,也主要分为两个方向:
    1. 弱一致性,即单个的数据操作保持事务一致;这样可以保持高性能,在数据请求量很大的情况下对性能有保证;
    2. 强一致性,即一个service方法的数个操作在同一个事务中,由于数个操作的数据源可能不一样,也可能由多个dao service构成,这时就要通过分布式事务保持数据的一致,其中一个操作失败,所有的数据都回滚。这里适用于对数据一致性要求很强的场景。分布式事务会对性能有一定的影响。

    言归正传,本文主要使用了hibernate4,spring3.1,atomikos3.8来实现整体结构。设计的目的是一个方便的底层DAO的封装,而分布式事务会在上层的service控制,而不在DAO层控制,避免事务嵌套造成的问题。

第一部分:底层控制代码编写:

1. 配置dynamicDataSource,代码如下:
/**
 * 动态数据源
 * @author wenfengsun
 * @since 2012-9-19 下午3:47:15
 */
public class DynamicDataSource extends AbstractRoutingDataSource{
      @Override
      protected Object determineCurrentLookupKey() {
             String key = DataSourceContextHolder.getDataSource();
             if(key == null)
                   key = "ds0";
             return key;
      }
}
其中,DataSourceContextHolder利用了ThreadLocal为每个线程设置不同的变量值,DataSourceContextHolder的代码如下:
/**
 * 获取或设置当前线程的dataSource
 * @author wenfengsun
 * @since 2012-9-19 下午2:57:36
 */
public class DataSourceContextHolder {
     private static final ThreadLocal contextHolder = new ThreadLocal();  
     public static void setDataSource(String dsName){  
          contextHolder.set(dsName);  
     }  
     public static String getDataSource(){  
          return (String) contextHolder.get();  
     }  
     public static void clearDataSource(){  
          contextHolder.remove();  
     }  
}

解释:在每次请求来临时,根据不同的路由策略,设置不同的dataSource,从而达到分开存储的目的。
2. 配置sessionFactory和存储接口
public class Config {
       private static Config _instance;
       private SessionFactory sessionFactory;

       public static Config getInstance(){
              if(_instance == null){
                       synchronized (Config.class) {
                            _instance = new Config();
                       }
              }
              return _instance;
       }
       Config(){
              initSessionFactoryWithTransactionManager();
       }
}

我们定义了一个config,它是一个单例模式的实现,通过方法:initSessionFactoryWithTransactionManager来实现数据源和sessionFactory的初始化工作;
3. 方法initSessionFactoryWithTransactionManager的实现:
解释:首先建立了两个数据源,通过一个单例的map存储。并将数据源赋予给之前声明的dynamicDatasource,然后初始化sessionFactory,注意红色部分和删除的部分,不要写错,否则分布式事务会报错。
public void initSessionFactoryWithTransactionManager(){
    DynamicDataSource dds = new DynamicDataSource();
然后再增加一个save方法:
public Serializable save(Serializable id, Object obj) throws NotSupportedException, SystemException{
        long shardId = (Long)id;
        if(shardId%2==1){
              DataSourceContextHolder.setDataSource("ds1");
        }else
              DataSourceContextHolder.setDataSource("ds0");
        System.err.println("sessionFactory:"+sessionFactory);
        sessionFactory.openSession();
        Session session = sessionFactory.getCurrentSession();
     //  session.beginTransaction();
        Serializable ret = session.save(obj);
    //  session.getTransaction().commit();
        return ret;
}
此处,基数存储到数据源ds1,偶数存储到数据源ds2
请注意:删除的部分,由于要在service中控制事务,此处的session的事务代码务必删除,否则会报一个错误:

Cannot call method 'commit' while a global transaction is runing

好,至此我们底层的控制已经完成,现在新建一个接口Dao和一个实现DaoImpl,调用save方法存储对象;(代码掠过)

第二部分:上层service和分布式事务

第二部分的配置依赖于spring。
<bean id="dao" class="com.xx.dao.impl.DaoImpl"/>
<bean id="userService" class="com.xx.service.impl.UserServiceImpl">
          <property name="dao" ref="dao"/>
</bean>
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"  destroy-method="close">
          <property name="forceShutdown">
                   <value>true</value>
          </property>
</bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
          <property name="transactionTimeout" value="240"/>
</bean>
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
          <property name="transactionManager" ref="atomikosTransactionManager"/>
          <property name="userTransaction" ref="atomikosUserTransaction"/>
</bean>
<tx:annotation-driven  transaction-manager="transactionManager"/>


我们声明了Dao和Service,并且声明了transactionManager。在最后一行tx声明之后,可以在service的实现类中的方法加入@Transactional即可控制事务。例如:
@Transactional
public long save(User user){
       User u1 = new User();
       u1.setName("tttt---------------");
       long res = (Long)dao.save(10L, user);
       dao.save(11L, u1);
       return res;
}


当其中一个save失败时,整个事务会回滚;

Mac配置Dnsmasq自动获取被屏蔽网站的DNS

使用前提:有一个虚拟主机(EC2...);有openvpn,pptp等服务已经运行;有一台mac;
背景:GFW已经开始污染来自所有外部的DNS服务的返回结果,例如著名的8.8.8.8,8.8.6.6都已经无法针对twitter等网站返回正确的DNS。

1. 使用port安装dnsmasq(注:port的使用见之前的博客文章)
>sudo port install dnsmasq
2. dnsmasq默认安装在/opt/local/下,配置文件在/opt/local/etc/dnsmasq.conf;Dnsmasq启动请直接执行:sudo launchctl load -w /Library/LaunchDaemons/org.macports.dnsmasq.plist  (这个plist文件已经生成完毕)。启动之后netstat -na|grep LIST,发现53(dns)端口已经开始监听了。(注:修改配置文件需要unload之后在load一次)
3. 配置你的EC2主机,在EC2上安装dnsmasq。(我的是ubuntu,直接apt-get install即可)并且配置该DNS的解析走默认的amazon的DNSserver。这样我们就在EC2上启动了一个DNS服务;
4. 将你的EC2的主机的外网IP设置到你的vpn的路由表里,使53的DNS服务也从vpn路由出去,而不要通过直接连接的形式(直连也被污染)
5. 配置mac本地的Dnsmasq的配置文件,cp /etc/resolv.conf /etc/resolv.dnsmasq.conf拷贝你的本地DNS配置,然后修改dnsmasq的配置文件,设置:resolv-file=/etc/resolv.dnsmasq.conf
6. 设置你要解析的host:
server=/twitter.com/xxx.xxx.xxx.xxx(这里写你的EC2的外网IP)
...
7. 修改你当前网卡的DNS设置:修改到127.0.0.1
8. 删除之前配置的hosts
9. nslookup,可以看到twitter.com的host已经正常解析,无需配置hosts文件了。

2012年9月13日星期四

Ps拼接全景图

其实用PS拼接全景照片非常简单,File->Automate->Photomerge,选择要拼接的照片,然后选择默认的Auto模式即可,待拼接的照片需要是同一位置,同焦距拍摄的多张角度的照片,拼接会消耗一定的时间,拼接完成后是一张不规则形状的图片。剪裁一下,成为一张长方形图片即可。
附:厦门鼓浪屿日光岩拍摄的一组照片的合成全景图。

sed命令添加文字在行首或行尾

1. 在行首添加文字HEAD
sed 's/^/HEAD&/g' file
如果需要直接修改文件:
sed -i 's/^/HEAD&/g' file
在mac下稍有不同:
sed -i .bak 's/^/HEAD&/g' file
mac下会将原文件增加.bak后缀做备份,而普通linux不会。
2. 在行尾增加文字TAIL
sed 's/$/&TAIL/g' file
同理,加入-i参数会直接修改当前文件。

2012年9月7日星期五

UDP隧道打洞技术研究

        UDP在点对点通信中起到了很大的作用,比如skype在用户点对点通信的时候就使用了UDP打洞的技术;
        所谓打洞,实际就是绕过防火墙或路由器的拦截,实现用户终端的直接连接。这里就需要先介绍一下NAT(Network Address Translation)了。它的核心是在IP封包通过防火墙或路由器时,重写源IP地址或目的IP地址的技术;
        下图展示了一个带NAT的网络,路由器后的网络地址和路由器的网络地址是变化的。

       NAT设备有四种模式,还可以简化为两种类型,即对称模式和非对称模式。非对称模式还分为1. 自由模式 2. 受限IP模式 3. 受限IP和端口模式;
       1. 自由模式:凡是内网的一个IP+PORT发起的UDP广播,在经过路由器之后都会映射到一个固定的端口,并且外部所有的IP和端口都可以通过路由器映射的IP+Port回访数据;
       2. 受限IP模式:凡是内网的一个IP+PORT发起的UDP广播,在经过路由器之后都会映射到一个固定的端口,只有接收方的IP可以发送回访数据,端口不限;
       3. 受限IP+port模式:凡是内网的一个IP+PORT发起的UDP广播,在经过路由器之后都会映射到一个固定的端口,只有接收方的IP和指定的PORT发起的回访数据,会被接受;
       4. 对称模式:凡是内网的一个IP+PORT发起到指定IP+port的UDP广播,在经过路由器之后都会映射到一个固定的端口,只有接收方的IP和指定的PORT发起的回访数据,会被接受;

       换句话说,只有非对称模式的NAT设备才可以进行UDP打洞;

       UDP打洞原理:
       1. 有一个协调方,我们称之为server;
       2. A,B分别和S相连;S即得到了A,B在最外层路由的ip地址和端口号;
       3. A要向B发起连接,先向S请求连接,S将B的ip+端口发送给A;
       4. A向B的IP+端口发送UDP数据包,被B的防火墙或路由抛弃;
       5. A向S通知,已经向B发送过数据;
       6. S通知B,向B发送A的ip+port,并通知B向A发送数据;
       7. B向A发送数据,由于A向B发送过数据,因此A的NAT设备的session里已经记录了B的ip+port,B的数据过来后,会被A的设备放行,数据将被直接传递到A的终端;
       8. A收到B的数据后,回数据给B,此时B的设备也记录了A的ip+port,因此通信建立成功;
       
       测试了一下,发现公司的环境和家里的环境,全部是对称模式,抓包时发现端口号已变,无法打洞:(