高可用架构(第一卷)

Posted by kyle on June 12, 2018

首先,感谢公司建立的产研书吧,让产品和研发部门的小伙伴能够有更好的机会读一些好书,提升自己专业技能的同时,也让我们时刻保持向上学习的动力。此外,感谢HRBP小伙伴,让我有机会在阅读之后,发表一些个人的拙见。

我觉得读的书,从个人目的上可以大致分为两类:一类是用以解决当下的困惑,另一类则是用以开拓眼界。开始我想阅读这本《高可用架构》的时候,我是抱着后一种目的的。我希望能从这本书中汲取些比较实用的架构思想,用以帮助自己找到一些灵感去开拓成长为架构师的路。

这本书是个合集,它辑录了很多知名互联网公司专家的技术分享与交流。从日志系统、IM系统的基本架构,到高可用架构、分布式系统的实践,以及容器、大数据相关的架构设计均有涉猎。书中大大小小50来个架构案例,如果没有足够多的知识储备、亲手实践过其中的某一部分内容,是无法体会其中的精髓的。

在这里,我对我个人所感兴趣的点进行一些展开。在讲雪球的IM系统架构的时候,文中介绍到,雪球的IM系统通过Netty+自定义网络协议的方式进行消息的网络传输,然后客户端用Akka做消息同步。接下来我就稍稍分享下我对Akka这个框架的理解。

Akka是帮助构建高并发、分布式和具有容错性的事件驱动应用程序的工具包。它包含Java和Scala两种API。Akka的最小计算单元是Actor。Actor模型是由Carl Hewitt在1973年提出的。

那什么又是Actor呢?在使用Java进行并发编程时需要特别的关注锁和内存原子性等一系列线程问题,而Actor模型内部的状态是由它自己维护的,Actor与Actor之间通过消息传递来进行状态修改。Actor由状态(State)、行为(Behavior)和邮箱(MailBox)三部分组成:

组成 含义
状态(State) Actor中的状态指的是Actor对象的变量信息,状态由Actor自己管理,避免了并发环境下的锁和内存原子性等问题
行为(Behavior) 行为指定的是Actor中计算逻辑,通过Actor接收到消息来改变Actor的状态
邮箱(MailBox) 邮箱是Actor和Actor之间的通信桥梁,邮箱内部通过FIFO消息队列来存储发送方Actor消息,接受方Actor从邮箱队列中获取消息

下面这张图可以简单描述Actor的工作原理:

为了更好地理解Actor模型与Akka的应用,在这里,我分别用传统Java内部锁机制与Akka来实现“转账”功能。

首先,对于单个账户来讲,要保证扣款与入账操作的原子性,所以扣款、入账都得用内部锁作同步:

public class Account {
	private int amount;
	public synchronized void deduct(int amount) {
		this.amount += amount;
	}
	public synchronized void add(int amount) {
		this.amount -= amount;
	}
}

然后, 在执行转账操作的时候,把两个账户都加上锁:

public void transfer(Account from, Account to, int amount) {
	synchronized(from) {
		synchronized(to) {
			from.deduct(amount);
			to.add(amount);
		}
	}
}

现在,我们该加锁的地方都加了,但是,这样的程序还是有问题的。假设线程1在执行账户A给账户B转账操作的时候,线程1已经抢占了账户A的锁,而另外有一线程2执行的是账户B给账户A转账的操作,且抢占到了账户B的锁。此时线程1、2都渴望抢夺到已经被对方抢占的锁,于是就发生了死锁。

解决的方案也是有的,可以给账户排序,加锁的时候按照账户顺序来抢占锁,这样便破坏了死锁必要条件中的“循环等待条件”,预防了死锁。

下面我们来看看Akka是如何实现转账功能的。我们为需要转账的账户各建立一个Actor,那么接下来的问题就在于两个Actor是如何通信的了。由于Actor与Actor之间是通过消息来通信的,那么我们首先得定义好消息,包括两种消息,一是扣款消息,二是入账消息:

/**
 * 扣款消息
 */
static public class DeductMsg {
    private final Integer money;
    public DeductMsg(Integer money) {
        this.money = money;
    }
    public Integer getMoney() {
        return money;
    }
}
/**
 * 入账消息
 */
static public class AddMsg {
    private final Integer money;
    public AddMsg(Integer money) {
        this.money = money;
    }
    public Integer getMoney() {
        return money;
    }
}

对于一个账户的Actor来说,它是按照FIFO的顺序来处理自身的消息的,账户内部是能够保持同步的,所以实际上就只需要先向账户A发送扣款的消息,执行成功后再向账户B发送入账消息。在这里,为了整体控制转账流程,我还专门创建了个协作Actor,当这个协作Actor接收到转账消息时,会对相应的账户Actor发送消息:

/**
 * 转账消息
 */
static public class TransferMsg {
    private final ActorRef fromAccount;
    private final ActorRef toAccount;
    private final Integer money;
    
    public TransferMsg(ActorRef fromAccount, ActorRef toAccount, Integer money) {
        this.fromAccount = fromAccount;
        this.toAccount = toAccount;
        this.money = money;
    }
    public ActorRef getFromAccount() {
        return fromAccount;
    }
    public ActorRef getToAccount() {
        return toAccount;
    }
    public Integer getMoney() {
        return money;
    }
}

// 协作Actor接收到转账消息
@Override
public Receive createReceive() {
    return receiveBuilder()
            .match(TransferMsg.class, transferMsg -> {
                // 发送扣款消息
                transferMsg.getFromAccount().tell(new AccountActor.DeductMsg(transferMsg.getMoney()), getSelf());
                // 发送入账消息
                transferMsg.getToAccount().tell(new AccountActor.AddMsg(transferMsg.getMoney()), getSelf());
            })
            .build();
}

由于代码稍稍有点多,这里就不全部贴出来了,感兴趣的话可以查看https://github.com/sofkyle/Akka-Sample下的Deal实例。

关于Akka还值得一提的是,Actor不单可以在当前JVM中运行,也可以跨JVM在任何机器上运行。我个人觉得在某些方面它可以作为替代Netty+Zookeeper集群的方案,因为Akka有自己的集群实现而不需要依赖其他中间件。

关于Akka就介绍就介绍这么多,如果想了解更多,可以参考文章末尾的参考资料。

回到《高可用架构》这本书本身来,补充一些我学到的小知识。书中在介绍百姓网的亿级规模Elasticsearch优化实战的时候,提到了JVM 32G现象:当JVM堆大小超过32G时,哪怕用更多的内存,效果都不如31G。除此之外,书中还有章节介绍NoSQL的发展历史。在了解到了NoSQL的发展史之后,我丝毫不会再去疑惑“NoSQL是否会最终替代RDB”这样的问题。NoSQL和RDB都有各自适合的场景,我觉得学会根据现实状况做取舍是成长为架构师的第一步。

总而言之,书中所包含内容颇丰,具体涉及到架构案例的章节,也有足够的深度与广度。所以我觉得这本书不适合用来“找灵感”,倒更适合于在具体实践某种架构方案的时候,掌握一些他人已实践过的可能性。

下面说点题外话。《极客与团队》一书里面提到,程序员之间其实是需要多多交流的,因为单打独斗不光是增加了自己失败的风险,而且浪费了自己成长的可能性。因为我们无法知道,我们自己选的路是不是对的。所以,我们要适当的把自己的问题暴露出来。我希望能有更多小伙伴参与到书吧的分享活动中来,一起来分享自己的学习、读书心得。

最后的最后,让我用《高可用架构》里面的一碗毒鸡汤来结束我的这次分享,与诸君共勉:

  • 不要奢望其他人会写出高质量的代码。
  • 不要以为自己写出来的是高质量的代码。

参考资料:

[1] 高可用架构社区. 高可用架构.第1卷. 北京:电子工业出版社,2017.11

[2] 当多线程并发遇到Actor (https://mp.weixin.qq.com/s/mzZatZ10Rh19IEgQvbhGUg)

[3] Netty和Akka有什么不同? (https://www.cnblogs.com/WangBoBlog/p/7843688.html)

[4] Actor模型 (https://blog.csdn.net/gulianchao/article/details/7249117)

[5] 使用Akka的远程调用 (https://blog.csdn.net/beliefer/article/details/53841919)

[6] Actor模型原理 (https://www.cnblogs.com/MOBIN/p/7236893.html)

[7] Akka Quickstart with Java (https://developer.lightbend.com/guides/akka-quickstart-java/)