Zuul性能测试

环境准备

采用三台阿里云服务器作为测试
10.19.52.8 部署网关应用-gateway
10.19.52.9, 10.19.52.10 部署用于测试的业务系统
这里写图片描述

压测工具准备

选用ab作为压力测试的工具,为了方便起见,直接将ab工具安装在10.19.52.8这台机
测试命令如下:

1
ab -n 10000 -c 100 http://10.19.52.8:8080/hello/testOK?access_token=e0345712-c30d-4bf8-ae61-8cae1ec38c52

其中-n表示请求数,-c表示并发数,上面一条命令也就意味着,100个用户并发对http://10.19.52.8/hello/testOK累计发送了10000次请求。

服务器,网关配置

由于我们使用的tomcat容器,关于tomcat的一点知识总结如下:

查看更多

分享到

springcloud----Zuul动态路由

前言

Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。

不过这里并不想介绍整个网关的架构,而是想着重于讨论其中的一个关键点,并且也是经常在交流群中听人说起的:动态路由怎么做?

再阐释什么是动态路由之前,需要介绍一下架构的设计。

传统互联网架构图

这里写图片描述
上图是没有网关参与的一个最典型的互联网架构(本文中统一使用book代表应用实例,即真正提供服务的一个业务系统)

加入eureka的架构图

这里写图片描述
book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。

最终架构图

这里写图片描述
要强调的一点是,gateway最终也会部署多个实例,达到分布式的效果,在架构图中没有画出,请大家自行脑补。

本博客的示例使用最后一章架构图为例,带来动态路由的实现方式,会有具体的代码。

查看更多

分享到

分布式限流

前言

最近正在为本科论文的事感到心烦,一方面是在调研期间,发现大部分的本科论文都是以MVC为架构,如果是使用了java作为开发语言则又是千篇一律的在使用SSH,二方面是自己想就微服务,分布式方面写一篇论文,讲述一些技术点的实现,和一些中间件的使用,看到如八股文般的模板格式..不免让人望文生怯。退一步,投入模板化ssh-web项目的怀抱,落入俗套,可以省去自己不少时间,因为在外实习,琐事并不少;进一步,需要投入大量时间精力去研究,而且不成体系,没有论文参考。

突然觉得写博客,比写论文爽多了,可以写自己想写的,记录自己最真实的想法。可能会逐渐将之前博客维护的自己的一些想法,纳入到本科论文中去。

经典限流算法

说回正题,补上之前分布式限流的实现。先介绍一些现有的限流方案。

核心的算法主要就是四种:
A类:计数器法,滑动窗口法
B类:令牌桶法,漏桶法

这里的四种算法通常都是在应用级别讨论的,这里不重复介绍这四种算法的实现思路了,只不过我人为的将他们分成了A,B两类。

  • A类算法,是否决式限流。即如果系统设定限流方案是1分钟允许100次调用,那么真实请求1分钟调用200次的话,意味着超出的100次调用,得到的是空结果或者调用频繁异常。

  • B类算法,是阻塞式限流。即如果系统设定限流方案是1分钟允许100次调用,那么真实请求1分钟调用200次的话,意味着超出的100次调用,会均匀安排到下一分钟返回。(当然B类算法,也可以立即返回失败,也可以达到否决式限流的效果)

B类算法,如Guava包提供的RateLimiter,内部其实就是一个阻塞队列,达到阻塞限流的效果。然后分布式场景下,有一些思路悄悄的发生了变化。多个模块之间不能保证相互阻塞,共享的变量也不在一片内存空间中。为了使用阻塞限流的算法,我们不得不将统计流量放到redis一类的共享内存中,如果操作是一系列复合的操作,我们还不能使用redis自带的CAS操作(CAS操作只能保证单个操作的原子性)或者使用中间件级别的队列来阻塞操作,显示加分布式锁的开销又是非常的巨大。最终选择放弃阻塞式限流,而在分布式场景下,仅仅使用redis+lua脚本的方式来达到分布式-否决式限流的效果。redis执行lua脚本是一个单线程的行为,所以不需要显示加锁,这可以说避免了加锁导致的线程切换开销。

锁的演变

下面记录一下这个设计的演变过程。

  • 单体式应用中显示加锁
    首先还是回到单体应用中对共享变量进行+1的例子。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Integer count = 0;

    //sychronized锁
    public synchronized void synchronizedIncrement(){
    count++;
    }

    //juc中的lock
    Lock lock = new ReentrantLock();

    public void incrementByLock(){
    lock.lock();
    try{
    count++;
    }finally {
    lock.unlock();
    }

    }

用synchronized或者lock同步的方式进行统计,当单位时间内到达限定次数后否决执行。限制:单体应用下有效,分布式场景失效,显示加锁,开销大。

  • 单体式应用中CAS操作
1
2
3
4
5
public AtomicInteger atomicInteger = new AtomicInteger(0);

public increamt(){
atomicInteger.incrementAndGet();
}

虽然没有显示加锁,但是CAS操作有一定的局限性,限流中不仅要对计数器进行+1,而且还要记录时间段,所以复合操作,还是无法避免加锁。

  • 分布式应用中显示加锁
1
2
3
4
5
6
7
8
9
10
11
RedisDistributeLock lock = new RedisDistributeLock();

public void incrementByLock(){
lock.lock();
try{
count++;
}finally {
lock.unlock();
}

}

分布式阻塞锁的实现,可以参考我之前的博客。虽然能达到多个模块之间的同步,但还是开销过大。不得已时才会考虑使用。

  • redis+lua脚本限流(最终方案)
1
2
3
4
5
6
7
8
9
10
11
12
13
local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
redis.call("INCRBY", key,"1") -- 如果不需要统计真是访问量可以不加这行
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
if tonumber(ARGV[2]) > -1 then
redis.call("expire", key,tonumber(ARGV[2])) --时间窗口最大时间后销毁键
end
return 1
end

lua脚本返回值比较奇怪,用java客户端接受返回值,只能使用Long,没有去深究。这个脚本只需要传入key(url+时间戳/预设时间窗口大小),便可以实现限流。
这里也贴下java中配套的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package sinosoftgz.apiGateway.utils;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.Assert;

import java.util.Arrays;

/**
* Created by xujingfeng on 2017/3/13.
* <p>
* 基于redis lua脚本的线程安全的计数器限流方案
* </p>
*/
public class RedisRateLimiter {

/**
* 限流访问的url
*/
private String url;

/**
* 单位时间的大小,最大值为 Long.MAX_VALUE - 1,以秒为单位
*/
final Long timeUnit;

/**
* 单位时间窗口内允许的访问次数
*/
final Integer limit;

/**
* 需要传入一个lua script,莫名其妙redisTemplate返回值永远是个Long
*/
private RedisScript<Long> redisScript;

private RedisTemplate redisTemplate;

/**
* 配置键是否会过期,
* true:可以用来做接口流量统计,用定时器去删除
* false:过期自动删除,时间窗口过小的话会导致键过多
*/
private boolean isDurable = false;

public void setRedisScript(RedisScript<Long> redisScript) {
this.redisScript = redisScript;
}

public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public boolean isDurable() {
return isDurable;
}

public void setDurable(boolean durable) {
isDurable = durable;
}

public RedisRateLimiter(Integer limit, Long timeUnit) {
this.timeUnit = timeUnit;
Assert.isTrue(timeUnit < Long.MAX_VALUE - 1);
this.limit = limit;
}

public RedisRateLimiter(Integer limit, Long timeUnit, boolean isDurable) {
this(limit, timeUnit);
this.isDurable = isDurable;
}

public boolean acquire() {
return this.acquire(this.url);
}

public boolean acquire(String url) {
StringBuffer key = new StringBuffer();
key.append("rateLimiter").append(":")
.append(url).append(":")
.append(System.currentTimeMillis() / 1000 / timeUnit);
Integer expire = limit + 1;
String convertExpire = isDurable ? "-1" : expire.toString();
return redisTemplate.execute(redisScript, Arrays.asList(key.toString()), limit.toString(), convertExpire).equals(1l);
}

}

由此可以见,分布式场景下,一个小小的统计次数的需求,如果真想在分布式下做到最完善,需要花很大的精力。

分享到

DevOps的八荣八耻

前言

被群里的好友安利了一发,周日跑去参加了一个技术讲座《云上开发与运维最佳实践》,听完两个人的演讲之后才发现主题竟然是讲运维,好在有一个人干货不少,在此记录下所得。简单追溯了一下这个DevOps才发现并不是一个新的概念,早在2010年就能看到有相关的人在追捧这个概念了。DevOps 就是开发(Development)和运维(Operations)这两个领域的合并。(如果没错的话,DevOps还包括产品管理、QA、winces 甚至销售等领域)。这种理念和现如今流行的微服务架构以及分布式特性的相关理念不谋而合。这篇文章主要就是转载记录了当时又拍云运维总监的演讲稿。

DevOps的八荣八耻

DevOps这个思想提出来已经五六年了,一直都是呼声很高,落地很难,为什么呢?这可能与各个公司的业务情况和技术发展路线有或多或少的关系,比如说创业的最早技术合伙人是运维出身或者技术出身,但是水平不高,为了公司持续发展,引入新鲜血液时,就会存在技术的先进性跟解决遗留烂摊子的矛盾。又或者业务本身偏向于用户,导致技术被边缘化,产品又没有好的架构,限制了快速发展等;所以,DevOps的推进一定要自上而下,凭借挑战自我,颠覆传统的勇气才能去落实。

以可配置为荣,以硬编码为耻

  img

△ 以可配置为荣,以硬编码为耻

hardcoding一时爽,真正要做改动时,需要定位代码,做出调整,甚至可能会破坏功能。以下可以说是配置的一个进化史

• 本地配置,程序⽣生成 (txt/ini/cfg)
• 集中配置, 动态⽣生成(Yaml/Json)
• 环境变量量(代码⽆无侵⼊入&语⾔言⽆无关性)
• 服务⾃自动发现,⾃自动注册(zookeeper/consul)

以互备为荣,以单点为耻

  img

  △ 以互备为荣,以单点为耻

互容互备一直是优良架构的设计重点。

又拍云早期做架构设计,使用了LVS+Keeplived+VRRP做转换,这样可以方便负载均衡,动态升级,隔离故障。现在的又拍云第二代,已经在部分大节点使用OSPF和Quagga做等价路由的负载均衡和冗余保障。

Nginx可以加Haproxy或LVS做负载均衡。MySQL可以做主从切换,或者是MMM的高可用成熟解决方案。我们的消息队列之前用rabbitmq做,现在主要是redis和kafka集群化,其中kafka已经迁到了Mesos容器平台里。

服务的自动发现、注册,我们可以使用consul、etcd、doozer(Heroku公司产品),还有zookeeper。主要区别是算法不一样,zookeeper用的是paxos算法,而consul用的是raft算法。目前看来consul比较流行,因为consul的自动发现和自动注册更加容易使用。etcd主要是CoreOS在主推,CoreOS本身就是一个滚动发布的针对分布式部署的操作系统,大家可以去关注一下它。还有一个是hadoop和elk,大数据平台的可扩展性是标配,很容易互备。

上面是举了一些常见互备的软件组件的造型,那我们如何是设计一个无单点的架构呢?主要掌握以下几点:

1.无状态

无状态意味着没有竞争,很容易做负载均衡,负载均衡的方式有很多种,F5,LVS,Haproxy,总能找到一种适合你的方式。

2.无共享

以前我们很喜欢用内存来保持临时信息,如进程间的交换,这种方式虽然效率很高,但是对程序的扩展性没什么好处,尤其是现在的互联网体量,光靠单机或者高性能机器是明显玩不转的。所以我们现在就需要使用类似消息队列的组件,把数据共享出去,利用多台机器把负载给承担下来。

3.松耦合/异步处理

以前我们用Gearman这样的任务框架。大家可以把任务丢进任务池里,生成多个消费者去取任务。当我的消费不够用时,可以平滑增加我的work资源,让他从更快的去拿任务。运维平台这边以python/celery的组合使用更多。

4.分布式/集群协作

像Hadoop这样的天生大数据/数据仓库解决方案,由于先前设计比较成熟,一般都是通过很多台机器扩容来实现map/reduce的扩展计算能力。

以随时重启为荣,以不能迁移为耻

img

△ 以随时重启为荣,以不能迁移为耻

关于这个点,我们讲三个方面:

1.Pet到Cow观念的转变

以前我们说机器是pet,也就是宠物模式,然后花了几万块钱去买的服务器,当宝一般供奉。但事实上却并不是这样,任何电子设备、服务器只要一上线,便开始了一个衰老的过程,你根本不知道在运行过程中会发生什么事,比如说质量差的电容会老化爆浆,电子元器件在机房的恶劣环境里会加速损坏,这些变化都是我们无法参与控制的,所以无论我们怎么努力,都无法保障机器有多么的牢靠。

谷歌指出的Cow模式就是指农场模式。就是要把机器发生故障当做常态,打个比方,比如说这头牛死了,那我就不要了,因为我有很多这样的牛,或者是再拉一头新的牛。这就是我们软件开发和运维需要做的转变,去适应这种变化。

2.OpenStack虚拟机的编排

虚拟化是个好东西,通过OpenStack我们很容易就可以做出一些存储或者迁移的操作,但是在实施的过程中,也是一波三折的。

又拍云从2014年开始在内部推动OpenStack,当然我们也踩过OpenStack网络的坑,那时候我们用双千兆的卡做内网通讯,因为使用OpenStack实现虚拟化后,一切都变成了文件,在网络上传输的话,对网络的压力会非常大,结果就导致部分服务响应缓慢(因为本身就是实验性质,所以在硬件上没有足够投入,内测时也没有推广,所以影响不大)。

2015年又拍云再上的OpenStack,全部都用双万兆的网卡做bonding,交换机也是做了端口聚合和堆叠。目前来说,只有云存储没有上线,其它云处理,云网络的使用还是能够满足要求。

3.Docker的导入导出

Docker是更轻量级的资源隔离和复用技术,从2016年开始,又拍云同时也在尝试使用Mesos/Docker来实现云处理的业务迁移。

以整体交付为荣,以部分交付为耻

  img

  △ 以整体交付为荣,以部分交付为耻

以往开发运维要安装一个机器,首先要去申请采购,购买完了还要等待运输,在运输中要花去一天的时间,之后还需要配交换机和网络。在这个过程中你会发现,简单的给开发配台机器,光上架就涉及到运维的很多环节,更不要说系统安装,优化,软件配置等剩余工作了,所以大多数情况下你只能做到部分交付。

要如何解决这些问题?通过OpenStack可以做到云计算、云网络、云存储这三块搭建完成之后,进行整体交付。

根据一些经验总结,在整个云平台当中,云存储的坑最多,云计算、云网络相对来说比较成熟。现在云计算的硬件基本上是基于英特尔CPU的虚拟化技术来硬件指令穿透的,损耗大概2%~5%,这是可以接受的。至于云网络,刚才胡凯(B站运维总监)提到内网包转发效率,我做过一个测试,在OpenStack的内网中,如果MTU默认是1500,万兆网卡的转发率大概为6.7xxGbps。后来我在优化的过程中,也翻查一些文档,看到的数据是可以达到9.5xxGbps,通过不断的摸索,对比测试后发现,如果把内网的MTU搞成大包,如9000时,万兆网卡的存储量直接达到了9.72Gbps左右的。不过,这个MTU需要提前在宿主机上调整好,需要重启生效。所以,这个问题发现得越早越好,这样就可以做到统一调度,分配资源。

Docker的好处是可以做到Build、Shipand Run,一气呵成。无论是对开发,测试,还是运维来说,Docker都是同一份Dockerfile清单,所以使用Docker在公司里的推动就很顺畅。虽然OpenStack也可以一站式交付,整体交付,使用时非常方便。但是对开发来说,他还是拿到一台机器,还是需要去安装软件环境,配置,上线,运行,除了得到机器快一些,对上线服务没有什么大的帮助,所以又拍云现在的Openstack集群一般对内申请开发测试用,外网生产环境还是以Docker容器化部署为主,这也是大家都喜闻乐见的方式,但前提是开发那边能够适应编写Dockerfile(目前是我在内部推动这种变革,如新的项目就强制要求用docker)。

以无状态为荣,以有状态为耻

  img

  △ 以无状态为荣,以有状态为耻

有状态的服务真的很麻烦,无论是存在数据库、磁盘开销,还有各种锁等资源的竞争,横向扩展也很差,不能重启,也不能互备。所以,有姿态的服务对于扩展原则来说,就是一场恶梦。如果是说我们解决这个问题,那就要使用解耦和负载均衡的方法去解决问题。

1.使用可靠的中间件

中间件其实最早出现在金融公司、证券公司,后来随着互联网行业不断壮大以后,就用一些高可靠性的号称工业级的消息队列出现,如RabbitMQ,一出来以后,就把中间件拉下神坛。随着中间件民用化,互联网蓬勃发展,是可以把一些服务变成无状态,方便扩展。

2.公共资源池

我们可以通过各种云,容器云、弹性云,做计算单元的弹性扩展。

3.能够被计算

如果你不想存状态,那也可以被计算,比如说Ceph存储,它的创新在于每个数据块都是可计算出来的,这就类似无状态的,每次都算,反正现在的cpu都这么强悍了,所以,无状态是一个命题,在做架构的时候,你脑海里一定要有这个意念,然后再看你用什么样的方式开动脑筋,预先的跟开发,运维沟通好,把应用拆分成一种无状态的最佳组合。

以标准化为荣,以特殊化为耻

  img

△ 以标准化为荣,以特殊化为耻

在标准化方面,我们在这几个方面改良:

1.统一输入输出

统一入口是我加入又拍云后做的第一件事情,我们用一个统一的文本,到现在也在用,然后推送到所有的边缘,服务器上面的组件,要用到的参数,都能从配置里读出来。代码管理方面我们也使用git,git wiki,批量部署我们用ansible(早在2012年,我做了一些比较后,就在公司里推行ansible,看来还是很明智的决定)。

2.统一的流程管理

运维中使用python最多,所以我们使用了yaml和playbook。又拍云有自己的跳板机,通过VPN登陆,目前我们也在试用一个带有审计功能的堡垒机,可以把每个人的操作录制下来,然后再去回放观察,改进我们的工作流程。

3.抽象底层设计和复用组件

如果是开发者的话,就会写很多的复用函数,对于优秀的运维人员来说,也要有优秀的抽象业务的能力,也要去做一些重复工作的复用准备,如频繁的,繁琐易出错的手工操作抽象成若干运维的脚本化。

最后是巧妙的利用虚拟化、容器服务、server-less微服务,这些服务是可以被备份,还原的,可以保持一个相对稳定的状态,我们要拒绝多的特殊管理操作。香农-信息熵理论里说,变量的不确定性越大,熵就越大,把它搞清楚所需要的信息量也就越大。理论上来说,如果是一个孤立的系统,他就会变得越来越乱。

以自动化工具为荣,以手动和人肉为耻

img

  △ 以自动化工具为荣,以手动和人肉为耻

又拍云早期,用的是bash、sed、awk,因为我之前有搞嵌入式的背景和经验,对一个十几兆的嵌入式系统来说,上面是不可能有python/perl/nodejs等环境。所以我们把服务器批量安装,部署,上线,做成了嵌入式的系统后,只要点亮以后,运行一个硬件检测的程序,会把机器的CPU、内存、硬盘大小等都打印出来,供货商截图给我看,这个机器是否合格。合格的机器可以直接发到机房去,在机器到了机房通上网线以后会有一个ansibleplaybook的推动。

自从用了这种方法以后,我们在公司里面基本上没有见到服务器,一般直接产线上检测通过后发到机房。然后又拍云的运维人员就可以连上去远程管理,在过去的三年里我们服务器平均每年翻了三倍,节点翻了六倍多,但是人手并没有增加。

关于tgz、rpm、pkg的打包部署,我们用的是tgz的打包及docker镜像。优势在于,又拍云自有CDN网络,软件通过推动到CDN网络下可以加速下发。

关于集成测试、自动测试的发布,像ELK集中日志的分析、大数据的分析,我们现在使用ELK以后,只要有基础的运维技术知识便可看懂,不需要高深的运维知识和脚本编辑知识,大多数人都可以完成这份工作,好处就是你多了好多眼睛帮你一起来发现问题,定位问题。

最后是不要图形,不要交互,不要终端。一旦有了图形以后,很难实现自动化。原则就是,不要手工hack,最好是用程序生成程序的方式去完成这个步骤。

以无人值守为荣,以人工介入为耻

  img

  △ 以无人值守为荣,以人工介入为耻

运维部门要做的事情有三件:

1.运维自动化

要有一定的业务抽象能力,要有标准化的流程。没有好的自动化,就很难把运维的工作效率提升了,只要做好这些,就可以节省时间,从容应对业务增长。而且运维自动化的另一个好处就是运维不会因为人的喜怒哀乐而受到影响稳定性,比如说我今天心情不好,你让我装一台机器我还可以忍,你让我装十台一百台就不行了。但如果公司有了运维自动化的流程,这个事情就可以避免,因为谁做都一样。

2.监控要常态

2016年年初,又拍云特别成立大数据分析部门,我们把日志做了采样收集和过滤,通过大数据平台做日志的同构数据分析,重点关注4xx/5xx/2xx比例,响应时间分析如100毫秒、200毫秒、500毫秒,还有区域性的速率分布,讲真,这真是一个好东西。

3.性能可视化

数据的有效展示。现在ELK对我们的帮助很大,从监控图上来看相关的数据指标,一目了然。这里就不反复赘述了。

DevOps的本质

最后,我们谈一谈DevOps的本质。

  1. 弹性

    像亚马逊推云时,那个单词叫elastic,意思是,你要能够扩展,如横向扩展;你要能负载均衡,如果你是基于openstack/docker资源池,你的资源就可以复用,可以编排回滚。比如说OpenStack有模板,我打一个镜像包,稍微重了一点,Docker的就轻一点,Docker可以做一个滚动发布,可以保留原来的程序、原来的容器,你可以做快速切换,这也是一种变化的弹性。

  2. 无关性

    如果是虚拟化资源,一切都可以在模板里面设置,可以把底层的硬件、系统、网络抚平差异,比如说不管物理磁盘是1T(市面上缺货)/4T/6T的盘,都可以划分100G容量,所以当把一切变成按需申请的服务,无论是开发还是运维,工作都会比较简单,因为它的无关性。

  3. 不可变的基础设施

    这个对传统运维可能是一种打击,因为基础镜像可能已经做的足够安全,足够完美,足够精干,不需要基础运维过多的人工参与。但我认为恰恰能帮助传统运维减轻工作量,反而有更多的精力去迎接虚拟化、容器化,SDN的挑战,掌握了新技能后,就可以随取随用。

分享到

java并发实践--ConcurrentHashMap与CAS

前言

最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,以及CAS的一些概念。限流算法很多,我主要就以最简单的计数器法来做引。先抽象化一下需求:统计每个接口访问的次数。一个接口对应一个url,也就是一个字符串,每调用一次对其进行加一处理。可能出现的问题主要有三个:

  1. 多线程访问,需要选择合适的并发容器
  2. 分布式下多个实例统计接口流量需要共享内存
  3. 流量统计应该尽可能不损耗服务器性能

但这次的博客并不是想描述怎么去实现接口限流,而是主要想描述一下遇到的问题,所以,第二点暂时不考虑,即不使用redis。

说到并发的字符串统计,立即让人联想到的数据结构便是ConcurrentHashpMap<String,Long> urlCounter;

查看更多

分享到

volatile疑问记录

对java中volatile关键字的描述,主要是可见性有序性两方面。

一个很广泛的应用就是使得多个线程对共享资源的改动变得互相可见,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class TestVolatile extends Thread {
/*A*/
// public volatile boolean runFlag = true;
public boolean runFlag = true;

public boolean isRunFlag() {
return runFlag;
}

public void setRunFlag(boolean runFlag) {
this.runFlag = runFlag;
}

@Override
public void run() {
System.out.println("进入run");
while (isRunFlag()) {
/*B*/
// System.out.println("running");
}
System.out.println("退出run");
}

public static void main(String[] args) throws InterruptedException {
TestVolatile testVolatile = new TestVolatile();
testVolatile.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
testVolatile.setRunFlag(false);
System.out.println("main already set runflag to false");
new CountDownLatch(1).await();
}
}

在A处如果不将运行标记(runflag)设置成volatile,那么main线程对runflag的修改对于testVolatile线程将不可见。导致其一直不打印“退出run”这句。

但是如果在testVolatile线程的while()增加一句:B处打印语句,程序却达到了不使用volatile,修改也变得可见,不知道到底是什么原理。

只能大概估计是while()的执行过程中线程上下文进行了切换,使得重新去主存获取了runflag的最新值,从而退出了循环,暂时记录…

2017/3/8日更新
和群里面的朋友讨论了一下,发现同一份代码,不同的机器运行出了不一样的效果。又仔细翻阅了一下《effective java》,依稀记得当时好像遇到过这个问题,果然,在并发的第一张就对这个现象做出了解释。
关键就在于HotSpot Server VM对编译进行了优化,这种优化称之为提升(hoisting),结果导致了活性失败(liveness failure)

1
while (isRunFlag()) {}

会被优化成

1
2
3
if(isRunFlag()){
while(true)...
}

引用effective java这一节的原话:

简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步
如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。这样的失败是难以调式的。他们可能是间歇性的,且与时间相关,程序的行为在不同的VM上可能根本不同,如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但是正确的使用它可能需要一些技巧。

分享到

浅析java内存模型(JMM)

并发编程模型的分类

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Java内存模型的抽象

在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

img

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图来说明这两个步骤:

img

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

img

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

处理器重排序与内存屏障指令

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!为了具体说明,请看下面示例:

Processor A Processor B
a = 1; //A1x = b; //A2 b = 2; //B1y = a; //B2
初始状态:a = b = 0处理器允许执行后得到结果:x = y = 0

假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到x = y = 0的结果。具体的原因如下图所示:

img

这里处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。

从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了(处理器B的情况和处理器A一样,这里就不赘述了)。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操做重排序。

下面是常见处理器允许的重排序类型的列表:

Load-Load Load-Store Store-Store Store-Load 数据依赖
sparc-TSO N N N Y N
x86 N N N Y N
ia64 Y Y Y Y N
PowerPC Y Y Y Y N

上表单元格中的“N”表示处理器不允许两个操作重排序,“Y”表示允许重排序。

从上表我们可以看出:常见的处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖的操作做重排序。sparc-TSO和x86拥有相对较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区)。

※注1:sparc-TSO是指以TSO(Total Store Order)内存模型运行时,sparc处理器的特性。

※注2:上表中的x86包括x64及AMD64。

※注3:由于ARM处理器的内存模型与PowerPC处理器的内存模型非常类似,本文将忽略它。

※注4:数据依赖性后文会专门说明。

为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:

屏障类型 指令示例 说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore Barriers Store1; StoreStore; Store2 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad Barriers Store1; StoreLoad; Load2 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

happens-before

从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。happens- before的定义很微妙,后文会具体说明happens-before为什么要这么定义。

happens-before与JMM的关系如下图所示:

img

如上图所示,一个happens-before规则通常对应于多个编译器重排序规则和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,它避免程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。

原文地址

分享到

浅析项目中的并发

前言

控制并发的方法很多,我之前的两篇博客都有过介绍,从最基础的synchronized,juc中的lock,到数据库的行级锁,乐观锁,悲观锁,再到中间件级别的redis,zookeeper分布式锁。今天主要想讲的主题是“根据并发出现的具体业务场景,使用合理的控制并发手段”。

什么是并发

由一个大家都了解的例子引入我们今天的主题:并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo1 {

public Integer count = 0;

public static void main(String[] args) {
final Demo1 demo1 = new Demo1();
Executor executor = Executors.newFixedThreadPool(10);
for(int i=0;i<1000;i++){
executor.execute(new Runnable() {
@Override
public void run() {
demo1.count++;
}
});
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("final count value:"+demo1.count);
}
}

console:
final count value:973

这个过程中,类变量count就是共享资源,而++操作并不是线程安全的,而多个线程去对count执行++操作,并没有happens-before原则保障执行的先后顺序,导致了最终结果并不是想要的1000

查看更多

分享到

聊聊IT行业应届生求职

前言

回首大三下的暑假,那时候刚开始出来找实习,如今已经即将进入大四下学期,恍惚间,已经过去了8,9个月。写这篇文章的初衷就是想结合自己的经验给即将要出来找工作的应届生一些建议,想当初自己刚出来时,也得到过热心学长的教导,权当一种传递吧。

查看更多

分享到

《微服务》九大特性笔记

服务组件化

组件,是一个可以独立更换和升级的单元。就像PC中的CPU、内存、显卡、硬盘一样,独立且可以更换升级而不影响其他单元。

在“微服务”架构中,需要我们对服务进行组件化分解。服务,是一种进程外的组件,它通过http等通信协议进行协作,而不是传统组件以嵌入的方式协同工作。服务都独立开发、部署,可以有效的避免一个服务的修改引起整个系统的重新部署。

打一个不恰当的比喻,如果我们的PC组件以服务的方式构建,我们只维护主板和一些必要外设之后,计算能力通过一组外部服务实现,我们只需要告诉PC我们从哪个地址来获得计算能力,通过服务定义的计算接口来实现我们使用过程中的计算需求,从而实现CPU组件的服务化。这样我们原本复杂的PC服务得到了更轻量化的实现,我们甚至只需要更换服务地址就能升级我们PC的计算能力。

按业务组织团队

当我们开始决定如何划分“微服务”时,通常也意味着我们要开始对团队进行重新规划与组织。按以往的方式,我们往往会以技术的层面去划分多个不同的团队,比如:DBA团队、运维团队、后端团队、前端团队、设计师团队等等。若我们继续按这种方式组织团队来实施“微服务”架构开发时,当有一个有问题需要更改,可能是一个非常简单的变动,比如:对人物描述增加一个字段,这就需要从数据存储开始考虑一直到设计和前端,虽然大家的修改都非常小,但这会引起跨团队的时间和预算审批。

在实施“微服务”架构时,需要采用不同的团队分割方法。由于每一个微服务都是针对特定业务的宽栈或是全栈实现,既要负责数据的持久化存储,又要负责用户的接口定义等各种跨专业领域的职能。因此,面对大型项目时候,对于微服务团队拆分更加建议按业务线的方式进行拆分,一方面可以有效减少服务内部修改所产生的内耗;另一方面,团队边界可以变得更为清晰。

做“产品”的态度

实施“微服务”架构的团队中,每个小团队都应该以做产品的方式,对其产品的整个生命周期负责。而不是以项目的模式,以完成开发与交付并将成果交接给维护者为最终目标。

开发团队通过了解服务在具体生产环境中的情况,可以增加他们对具体业务的理解,比如:很多时候一些业务中发生的特殊或异常情况,很可能产品经理都并不知晓,但细心的开发者很容易通过生产环境发现这些特殊的潜在问题或需求。

所以,我们需要用做“产品”的态度来对待每一个“微服务”,持续关注服务的运作情况,并不断地分析帮助用户来提升业务功能。

智能端点与哑管道

在单体应用中,组件间直接通过函数调用的方式进行交互协作。而在“微服务”架构中,服务由于不在一个进程中,组件间的通信模式发生了改变,若仅仅将原本在进程内的方法调用改成RPC方式的调用,会导致微服务之间产生繁琐的通信,使得系统表现更为糟糕,所以,我们需要更粗粒度的通信协议。

在“微服务”架构中,通常会使用这两个服务调用方式:

第一种,使用HTTP协议的RESTful API或轻量级的消息发送协议,来实现信息传递与服务调用的触发。
第二种,通过在轻量级消息总线上传递消息,类似RabbitMQ等一些提供可靠异步交换的结构。

在极度强调性能的情况下,有些团队会使用二进制的消息发送协议,例如:protobuf。即使是这样,这些系统仍然会呈现出“智能端点和哑管道”的特点,为了在易读性与高效性之间取得平衡。当然大多数Web应用或企业系统并不需要作出在这两者间做出选择,能够获得易读性就已经是一个极大的胜利了。
——Martin Fowler

去中心化治理

当我们采用集中化的架构治理方案时,通常在技术平台上都会做同一的标准,但是每一种技术平台都有其短板,这会导致在碰到短板时,不得不花费大力气去解决,并且可能还是因为其底层原因解决的不是很好。

在实施“微服务”架构时,通过采用轻量级的契约定义接口,使得我们对于服务本身的具体技术平台不再那么敏感,这样我们整个“微服务”架构的系统中的组件就能针对其不同的业务特点选择不同的技术平台,终于不会出现杀鸡用牛刀或是杀牛用指甲钳的尴尬处境了。

不是每一个问题都是钉子,不是每一个解决方案都是锤子

去中心化管理数据

我们在实施“微服务”架构时,都希望可以让每一个服务来管理其自有的数据库,这就是数据管理的去中心化。

在去中心化过程中,我们除了将原数据库中的存储内容拆分到新的同平台的其他数据库实例中之外(如:把原本存储在MySQL中的表拆分后,存储多几个不同的MySQL实例中),也可以针对一些具有特殊结构或业务特性的数据存储到一些其他技术的数据库实例中(如:把日志信息存储到MongoDB中、把用户登录信息存储到Redis中)。

虽然,数据管理的去中心化可以让数据管理更加细致化,通过采用更合适的技术来让数据存储和性能达到最优。但是,由于数据存储于不同的数据库实例中后,数据一致性也成为“微服务”架构中急需解决的问题之一。分布式事务的实现,本身难度就非常大,所以在“微服务”架构中,我们更强调在各服务之间进行“无事务”的调用,而对于数据一致性,只要求数据在最后的处理状态是一致的效果;若在过程中发现错误,通过补偿机制来进行处理,使得错误数据能够达到最终的一致性。

基础设施自动化

近年来云计算服务与容器化技术的不断成熟,运维基础设施的工作变得越来越不那么难了。但是,当我们实施“微服务”架构时,数据库、应用程序的个头虽然都变小了,但是因为拆分的原因,数量成倍的增长。这使得运维人员需要关注的内容也成倍的增长,并且操作性任务也会成倍的增长,这些问题若没有得到妥善的解决,必将成为运维人员的噩梦。

所以,在“微服务”架构中,请务必从一开始就构建起“持续交付”平台来支撑整个实施过程,该平台需要两大内容,不可或缺:

自动化测试:每次部署前的强心剂,尽可能的获得对正在运行软件的信心。
自动化部署:解放繁琐枯燥的重复操作以及对多环境的配置管理。

容错设计

在单体应用中,一般不存在单个组件故障而其他还在运行的情况,通常是一挂全挂。而在“微服务”架构中,由于服务都运行在独立的进程中,所以是存在部分服务出现故障,而其他服务都正常运行的情况,比如:当正常运作的服务B调用到故障服务A时,因故障服务A没有返回,线程挂起开始等待,直到超时才能释放,而此时若触发服务B调用服务A的请求来自服务C,而服务C频繁调用服务B时,由于其依赖服务A,大量线程被挂起等待,最后导致服务A也不能正常服务,这时就会出现故障的蔓延。

所以,在“微服务”架构中,快速的检测出故障源并尽可能的自动恢复服务是必须要被设计和考虑的。通常,我们都希望在每个服务中实现监控和日志记录的组件,比如:服务状态、断路器状态、吞吐量、网络延迟等关键数据的仪表盘等。

演进式设计

通过上面的几点特征,我们已经能够体会到,要实施一个完美的“微服务”架构,需要考虑的设计与成本并不小,对于没有足够经验的团队来说,甚至要比单体应用发付出更多的代价。

所以,很多情况下,架构师们都会以演进的方式进行系统的构建,在初期系统以单体系统的方式来设计和实施,一方面系统体量初期并不会很大,构建和维护成本都不高。另一方面,初期的核心业务在后期通常也不会发生巨大的改变。随着系统的发展或者业务的需要,架构师们会将一些经常变动或是有一定时间效应的内容进行“微服务”处理,并逐渐地将原来在单体系统中多变的模块逐步拆分出来,而稳定不太变化的就形成了一个核心“微服务”存在于整个架构之中。

原文由 程序猿DD-翟永超 创作
转载自《微服务》九大特性笔记

分享到