drools 用户指南 ----stateless session(无状态会话)的使用

stateless session 无状态会话

Drools 规则引擎中有如此多的用例和诸多功能,它变得令人难以置信。不过不用担心,复杂性是分层的,你可以用简单的用例来逐步了解 drools。

无状态会话,不使用推理,形成最简单的用例。无状态会话可以被称为函数传递一些数据,然后再接收一些结果。无状态会话的一些常见用例有以下但不限于:

  1. 验证
    这个人有资格获得抵押吗?
  2. 计算
    计算抵押保费。
  3. 路由和过滤
    将传入的邮件(如电子邮件)过滤到文件夹中。
    将传入的邮件发送到目的地。

所以让我们从使用驾驶执照应用程序的一个非常简单的例子开始吧。

1
2
3
4
5
6
public class Applicant {
private String name;
private int age;
private boolean valid;
// getter and setter methods here
}

现在我们有了我们的数据模型,我们可以写出我们的第一个规则。我们假设应用程序使用规则来拒绝不符合规则的申请。由于这是一个简单的验证用例,我们将添加一条规则来取消任何 18 岁以下的申请人的资格。

1
2
3
4
5
6
7
8
package com.company.license

rule "Is of valid age"
when
$a : Applicant(age < 18)
then
$a.setValid(false);
end

查看更多

分享到

drools 用户指南 ----stateful session(有状态会话)的使用

stateful session 有状态会话

有状态会话长期存在,并允许随着时间的推移进行迭代更改。 有状态会话的一些常见用例包括但不限于:

  1. 监测
    半自动买入股票市场监控与分析。
  2. 诊断
    故障查找,医疗诊断
  3. 物流
    包裹跟踪和送货配置
  4. 合规
    验证市场交易的合法性。

与无状态会话相反,必须先调用 dispose() 方法,以确保没有内存泄漏,因为 KieBase 包含创建状态知识会话时的引用。 由于状态知识会话是最常用的会话类型,所以它只是在 KIE API 中命名为 KieSession。 KieSession 还支持 BatchExecutor 接口,如 StatelessKieSession,唯一的区别是 FireAllRules 命令在有状态会话结束时不被自动调用。

我们举例说明了用于提高火灾报警器的监控用例。 只使用四个类,我们假设 Room 代表房子里的房间,每个 Room 都有一个喷头 Sprinkler。 如果在房间里发生火灾,我们用一个 Fire 实例来表示, 用 Alarm 代表警报 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Room {
private String name
// getter and setter methods here
}

public class Sprinkler {
private Room room;
private boolean on;
// getter and setter methods here
}

public class Fire {
private Room room;
// getter and setter methods here
}

public class Alarm {
}

在上一节无状态会话中介绍了插入和匹配数据的概念。 这个例子假设每个对象类型的都是单个实例被插入的,因此只使用了字面约束。 然而,房子有许多房间,因此 rules 必须表达实体类之间的关系,例如在某个房间内的喷洒器。 这最好通过使用绑定变量作为模式中的约束来完成。 这种“加入”过程产生了所谓的“cross products”,这在下一节中将会介绍。

查看更多

分享到

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,总能找到一种适合你的方式。

  1. 无共享

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

  1. 松耦合 / 异步处理

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

  1. 分布式 / 集群协作

像 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,一出来以后,就把中间件拉下神坛。随着中间件民用化,互联网蓬勃发展,是可以把一些服务变成无状态,方便扩展。

  1. 公共资源池

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

  1. 能够被计算

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

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

  img

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

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

  1. 统一输入输出

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

  1. 统一的流程管理

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

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

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

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

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

img

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

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

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

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

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

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

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

  img

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

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

  1. 运维自动化

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

  1. 监控要常态

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

  1. 性能可视化

数据的有效展示。现在 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

查看更多

分享到