开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

如何使用Python编写vim插件

发表于 2017-11-28

前言

vim是个伟大的编辑器,不仅在于她特立独行的编辑方式,还在于她强大的扩展能力。然而,vim自身用于写插件的语言vimL功能有很大的局限性,实现功能复杂的插件往往力不从心,而且运行效率也不高。幸好,vim早就想到了这一点,她提供了很多外部语言接口,比如Python,ruby,lua,Perl等,可以很方便的编写vim插件。本文主要介绍如何使用Python编写vim插件。

准备工作

1. 编译vim,使vim支持Python

在编译之前,configure的时候加上--enable-pythoninterp和--enable-python3interp选项,使之分别支持Python2和Python3
编译好之后,可以通过vim --version | grep +python来查看是否已经支持Python,结果中应该包含+python和 +python3,当然也可以编译成只支持Python2或Python3。

现在好多平台都有直接编译好的版本,已经包含Python支持,直接下载就可以了:

  • Windows:可以在这里下载。
  • Mac OS:可以直接brew install vim来安装。
  • Linux:也有快捷的安装方式,就不赘言了。

2. 如何让Python能正常工作

虽然vim已经支持Python,但是可能:echo has("python")或:echo has("python3")的结果仍是0,说明Python还不能正常工作。
此时需要检查:

  1. 系统上是否装了Python?
  2. Python是32位还是64位跟vim是否匹配?
  3. Python的版本跟编译时的版本是否一致(编译时的版本可以使用:version查看)
  4. 通过pythondll和pythonthreedll来分别指定Python2和Python3所使用的动态库。
    例如,可以在vimrc里添加
    set pythondll=/Users/yggdroot/.python2.7.6/lib/libpython2.7.so

经此4步,99%能让Python工作起来,剩下的1%就看人品了。

补充一点:
对于neovim,执行

1
2
crmsh复制代码pip2 install --user --upgrade neovim
pip3 install --user --upgrade neovim

就可以添加Python2和Python3的支持,具体参见:h provider-python。

从hello world开始

在命令行窗口执行:pyx print("hello world!"),输出“hello world!”,说明Python工作正常,此时我们已经可以使用Python来作为vim的EX命令了。

操作vim像vimL一样容易

怎么用Python来访问vim的信息以及操作vim呢?很简单,vim的Python接口提供了一个叫vim的模块(module)。vim模块是Python和vim沟通的桥梁,通过它,Python可以访问vim的一切信息以及操作vim,就像使用vimL一样。所以写脚本,首先要import vim。

vim模块

vim模块提供了两个非常有用的函数接口:

  • vim.command(str)
    执行vim中的命令str(ex-mode),返回值为None,例如:
1
2
3
vim复制代码:py vim.command("%s/\s\+$//g")
:py vim.command("set shiftwidth=4")
:py vim.command("normal! dd")
  • vim.eval(str)
    求vim表达式str的值,(什么是vim表达式,参见:h expr),返回结果类型为:
+ `string`: 如果vim表达式的值的类型是`string`或`number`
+ `list`:如果vim表达式的值的类型是一个vim list(`:h list`)
+ `dictionary`:如果vim表达式的值的类型是一个vim dictionary(`:h dict`)例如:
1
2
3
vim复制代码:py sw = vim.eval("&shiftwidth")
:py print vim.eval("expand('%:p')")
:py print vim.eval("@a")

vim模块还提供了一些有用的对象:

  • Tabpage对象(:h python-tabpage)
    一个Tabpage对象对应vim的一个Tabpage。
  • Window对象(:h python-window)
    一个Window对象对应vim的一个Window。
  • Buffer对象(:h python-buffer)
    一个Buffer对象对应vim的一个buffer,Buffer对象提供了一些属性和方法,可以很方便操作buffer。
    例如 (假定b是当前的buffer) :
1
2
3
4
5
6
7
8
9
10
11
12
13
asciidoc复制代码:py print b.name            # write the buffer file name
:py b[0] = "hello!!!" # replace the top line
:py b[:] = None # delete the whole buffer
:py del b[:] # delete the whole buffer
:py b[0:0] = [ "a line" ] # add a line at the top
:py del b[2] # delete a line (the third)
:py b.append("bottom") # add a line at the bottom
:py n = len(b) # number of lines
:py (row,col) = b.mark('a') # named mark
:py r = b.range(1,5) # a sub-range of the buffer
:py b.vars["foo"] = "bar" # assign b:foo variable
:py b.options["ff"] = "dos" # set fileformat
:py del b.options["ar"] # same as :set autoread<
  • vim.current对象(:h python-current)
    vim.current对象提供了一些属性,可以方便的访问“当前”的vim对象
属性 含义 类型
vim.current.line The current line (RW) String
vim.current.buffer The current buffer (RW) Buffer
vim.current.window The current window (RW) Window
vim.current.tabpage The current tab page (RW) TabPage
vim.current.range The current line range (RO) Range

python访问vim中的变量

访问vim中的变量,可以通过前面介绍的vim.eval(str)来访问,例如:

1
vim复制代码:py print vim.eval("v:version")

但是, 还有更pythonic的方法:

  • 预定义vim变量(v:var)
    可以通过vim.vvars来访问预定义vim变量,vim.vvars是个类似Dictionary的对象。例如,访问v:version:
1
vim复制代码:py print vim.vvars["version"]
  • 全局变量(g:var)
    可以通过vim.vars来访问全局变量,vim.vars也是个类似Dictionary的对象。例如,改变全局变量g:global_var的值:
1
stylus复制代码:py vim.vars["global_var"] = 123
  • tabpage变量(t:var)
    例如:
1
stylus复制代码:py vim.current.tabpage.vars["var"] = "Tabpage"
  • window变量(w:var)
    例如:
1
stylus复制代码:py vim.current.window.vars["var"] = "Window"
  • buffer变量(b:var)
    例如:
1
stylus复制代码:py vim.current.buffer.vars["var"] = "Buffer"

python访问vim中的选项(options)

访问vim中的选项,可以通过前面介绍的vim.command(str)和vim.eval(str)来访问,例如:

1
2
vim复制代码:py vim.command("set shiftwidth=4")
:py print vim.eval("&shiftwidth")

当然, 还有更pythonic的方法:

  • 全局选项设置(:h python-options)
    例如:
1
vim复制代码:py vim.options["autochdir"] = True

注意:如果是window-local或者buffer-local选项,此种方法会报KeyError异常。对于window-local和buffer-local选项,请往下看。

  • window-local选项设置
    例如:
1
pgsql复制代码:py vim.current.window.options["number"] = True
  • buffer-local选项设置
    例如:
1
stylus复制代码:py vim.current.buffer.options["shiftwidth"] = 4

两种方式写vim插件

  • 内嵌式
1
2
3
dust复制代码py[thon] << {endmarker}
{script}
{endmarker}

{script}中的内容为Python代码,{endmarker}是一个标记符号,可以是任何字符串,不过{endmarker}前面不能有任何的空白字符,也就是要顶格写。
例如,写一个函数,打印出当前buffer所有的行(Demo.vim):

1
2
3
4
5
6
7
8
vim复制代码function! Demo()
py << EOF
import vim
for line in vim.current.buffer:
print line
EOF
endfunction
call Demo()

运行:source %查看结果。

  • 独立式
    把Python代码写到*.py中,vimL只用来定义全局变量、map、command等,LeaderF就是采用这种方式。个人更喜欢这种方式,可以把全部精力集中在写Python代码上。

异步

  • 多线程
    可以通过Python的threading模块来实现多线程。但是,线程里面只能实现与vim无关的逻辑,任何试图在线程里面操作vim的行为都可能(也许用“肯定会”更合适)导致vim崩溃,甚至包括只读一个vim选项。虽然如此,也比vimL好多了,毕竟聊胜于无。
  • subprocess
    可以通过Python的subprocess模块来调用外部命令。
    例如:
1
2
routeros复制代码:py import subprocess
:py print subprocess.Popen("ls -l", shell=True, stdout=subprocess.PIPE).stdout.read()

也就是说,从支持Python起,vim就已经支持异步了(虽然直到vim7.4才基本没有bug),Neovim所增加的异步功能,对用Python写插件的小伙伴来说,没有任何吸引力。好多Neovim粉竟以引入异步(job)而引以为傲,它什么时候能引入真正的多线程支持我才会服它。

案例

著名的补全插件YCM和模糊查找神器LeaderF都是使用Python编写的。

缺陷

由于GIL的原因,Python线程无法并行处理;而vim又不支持Python的进程(github.com/vim/vim/iss…),计算密集型任务想利用多核来提高性能已不可能。

奇技淫巧

  • 把buffer中所有单词首字母变为大写字母
1
ruby复制代码:%pydo return line.title()
  • 把buffer中所有的行镜像显示

例如,把

1
2
3
4
vim复制代码vim is very useful
123 456 789
abc def ghi
who am I

变为

1
2
3
4
apache复制代码lufesu yrev si miv
987 654 321
ihg fed cba
I ma ohw

可以执行此命令::%pydo return line[::-1]

总结

以上只是简单的介绍,更详细的资料可以参考:h python。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

轻松初探 Python 篇(四)—list tuple ra

发表于 2017-11-28

这是「AI 学习之路」的第 4 篇,「Python 学习」的第 4 篇
小之的公众号 : WeaponZhi

今天的主题是 Python 的序列类型(Sequence Types),内容很多,干货很足,也是我们平时经常使用的,大家准备好小板凳纸笔吧!

注意,我不准备再将循环语句和条件语句方面的知识了,比较简单,每种语言这方面的写法区分不大,有兴趣的大家可以自行去查阅一下。

list

list 是一种有序集合,在很多语言里面都有支持,像 Java 中的 List ,可以简单理解 List 是一个顺序表,可以对它进行添加和删除元素的操作,但和 Java 不同,Java 的 List 并不是内置的,它的一些实现类比如 ArrayList 是用 Java 代码另外实现的,而 list 在 Python 中是一种内置数据类型,它是和语言本身是一体的。

list 的使用很简单,用方括号[]表示,内部的元素用逗号「,」区分。比如,我们用 list 来表示我国的几个城市的名字:

1
2
3
复制代码>>> city = ['北京','上海','天津','重庆']
>>> city
['北京','上海','天津','重庆']

我们可以用索引来访问 list 中指定位置的元素,这部分有点像 Java 的数组

1
2
3
4
5
6
7
8
复制代码>>> city[0]
'北京'
>>> city[3]
'重庆'
>>> len(city)
4
>>> city[4]
IndexError:list index out of range

这里要注意下,索引是从 0 开始,而不是从 1,所以最后一个重庆的索引实际上是 3,当我们试图访问位置为 4 的时候,就会产生索引越界的错误。

通过 len() 函数可以获取 list 的长度,所以实际上 list 的最后一个元素的索引是len(city)-1。

空的 list 长度为 0:

1
2
3
复制代码>>> L = []
>>> len(L)
0

当然我们也可以从后往前进行索引,-1代表最后一个,-2代表倒数第二个,以此类推。

1
2
3
4
5
6
复制代码>>> city[-1]
重庆
>>> city[-2]
天津
>>> city[-5]
IndexError:list index out of range

list 中元素的数据类型是不一定是一样的,甚至可以再嵌套一个 list:

1
2
3
复制代码>>> L = ['小之',23,False,['Android','Python','Java']]
>>> len(L)
4

L 中的另一个 list 整体只算一个元素,你可以把它理解为一个二维数组,可以通过L[3][1]调用到这个 list 中的Python元素。

list 常用的函数

list 是一个可变有序表,所以可以往 list 中塞数据,使用append()函数将把元素塞到末尾

1
2
3
复制代码>>> city.append('南京')
>>> city
['北京','上海','天津','重庆','南京']

我们还可以使用insert()来把元素插入到指定位置

1
2
3
复制代码>>> city.insert(2,'苏州')
>>> city
['北京','上海','苏州','天津','重庆','南京']

通过pop()可以获取 list 末尾的元素,并且删除该元素,pop(i)可以删除并返回指定位置的元素

1
2
3
4
5
6
复制代码>>> city.pop()
'南京'
>>> city.pop(0)
'北京'
>>> city
['上海','苏州','天津','重庆']

使用remove(x)来移除某一个特定的元素x

1
2
3
复制代码>>> city.remove('上海')
>>> city
['苏州','天津','重庆']

使用reverse()来将 list 逆序

1
2
3
复制代码>>> city.reverse()
>>> city
['重庆','天津','苏州']

如果想作拼接可以有两种方式,一种是通过extend(),一种是通过+

1
2
3
4
5
6
复制代码>>> L2 = ['北京']
>>> L+L2
['重庆','天津','苏州','北京']
>>> L.extend(L2)
>>> L
['重庆','天津','苏州','北京']

注意,L+L2并不会改变 L 的值,而extend是会改变调用 list 本身的值的,所以extend实际上可以和L += L2等价

你还可以通过city *= n来将 list 重复 n 次塞入到本身

1
2
3
4
复制代码>>> L = ['1','2']
>>> L *= 3
>>> L
['1','2','1','2','1','2']

tuple

tuple 叫元祖,是另一种有序列表,tuple 和 list 很像,但是 tuple 初始化后就不能再被修改了,除了一些插入删除操作比如append,pop不能使用,其他的使用和 list 基本一致。

1
2
3
4
5
复制代码>>> city = ('北京','上海','南京')
>>> city[0]
'北京'
>>> city[-1]
'南京'

注意,在定义一个 tuple 的时候,tuple 的元素就被确定下来了,之后你将无法改变它,这样做的意义是 tuple 的不可变性可以使得代码更加安全,如果可以,你应该尽量使用 tuple 来替代 list。

当你定义只有一个元素的 tuple 的时候,注意需要在后面加一个「,」号,以免引起歧义。(1)是可以代表数字 1 的。

1
2
3
4
5
6
复制代码>>> t = (1)
>>> t
1
>>> t = (1,)
>>> t
(1,)

tuple 的「不可变」并不意味着元素完全不可变,而是代表 tuple 元素的指向是不可变的,tuple 的元素可以是一些「可变的」元素,比如 list:

1
2
3
4
复制代码>>> t = ('北京','上海',['重庆','南京'])
>>> t[2][0] = '天津'
>>> t
('北京','上海',['天津','南京'])

我们可以看到 tuple 中的 list 元素已经改变了,但实际上 tuple 的元素指向的仍然还是这个 list,总而言之,tuple 需要保证的是「指向不变」,而如果你需要保证内容不变,那就得保证元素本身是不可变的。

range

我们最后介绍一下range。range 也是一个不可变的序列,它通常是使用在一些循环语句中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码>>> list(range(10))
[0,1,2,3,4,5,6,7,8,9]
>>> list(range(1,11))
[1,2,3,4,5,6,7,8,9,10]
>>> list(range(0,30,5))
[0,5,10,15,20,25]
>>> list(range(0,10,3))
[0,3,6,9]
>>> list(range(0,-10,-1))
[0,-1,-2,-3,-4,-5,-6,-7,-8,-9]
>>> list(range(0))
[]
>>> list(range(1,0))
[]

list 函数将把可遍历的数据类型转化为 list。我们通过以上代码发现,range(10)是 0-9 的数列,在一个参数的时候是从 0 开始的序列。range(1,11)代表从第一个参数开始,第二个参数为止的数列,注意,这里的范围是[1,11),包括第一个参数,不包括第二个参数。三个参数,在两个参数的基础上增加了一个 step 用来表示每次增幅的数量,如果某一次增幅超过了一边范围,则割舍掉这个值。

如果没有第三个参数,则默认所有的 step 都是 +1,如果开始值和结束值大小写反了,将会返回一个空列表。

一般我们会在 for 语句中使用 range()

1
2
3
4
5
6
复制代码>>> L = []
>>> for i in range(10):
... L.append(i)
...
>>> L
[0,1,2,3,4,5,6,7,8,9]

序列类型的一些其他用法

通用序列操作

下面说的操作和函数都是通用操作,不管是不是可变序列,都可以使用。我们先定义一个测试列表:

1
复制代码L = [1,2,3,4,5]

x in L, x not in L

x 代表元素,L 代表列表,x in L 中如果 L 中有元素和 x 相等,就返回 True,否则返回 False,x not in L 相反。

1
2
3
4
5
6
7
8
复制代码>>> 1 in L
True
>>> 0 in L
False
>>> 1 not in L
False
>>> 0 not in L
True

L + T

这个我们之前也讲过了,通过「+」可以拼接两个序列

1
2
3
复制代码>>> T = [6]
>>> L + T
[1,2,3,4,5,6]

L * n

将 L 重复塞入自己 n 次,得到一个新的列表

1
2
复制代码>>> L * 2 
[1,2,3,4,5,6,1,2,3,4,5,6]

len(L)

获取列表长度

1
2
复制代码>>> len(L)
12

min(L)、max(L)

获取列表中的最大最小值

1
2
3
4
复制代码>>> min(L)
1
>>> max(L)
6

L.index(x)

获取 x 在 L 中第一次出现的位置

1
2
3
复制代码>>> L = (1,2,1,3)
>>> L.index(1)
0

index 还有两个变种,可以添加两个参数,index(x,i)和 index(x,i,j)。

如果只增加一个参数,那就是获取从这个参数位置开始到列表结尾这个范围中,第一次出现 x 的位置,如果增加两个参数,则返回从 i 至 j (不包括 j)范围中第一次出现 x 的位置。

1
2
3
4
复制代码>>> L.index(1,1)
2
>>> L.index(1,1,3)
2

如果找不到,将会抛出ValueError错误

L.count(x)

返回 x 在 L 中出现的次数

1
2
复制代码>>> L.count(1)
2

可变序列操作

下面介绍一些可变序列独有的操作,tuple 是无法使用的。

除了之前介绍 list 时提到的诸如append、remove以外,还有一些比较有用的函数。

L.clear()

清空列表中所有的元素

1
2
3
复制代码>>> L.clear()
>>> L
[]

L.copy

新建一份相同的列表。

1
2
3
4
5
6
7
8
9
复制代码>>> L = [1,2,3]
>>> A = L.copy()
>>> A
[1,2,3]
>>> A.append(1)
>>> A
[1,2,3,1]
>>> L
[1,2,3]

以上介绍了一些序列类型的一些基本操作,还有一些类似「切片」、「迭代」、「生成器」等高级操作,将在后续的文章中为大家介绍。


欢迎关注我的公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

部署微服务:Spring Cloud 和 Kubernete

发表于 2017-11-28

【编者的话】本文比较了当下最火热的两大微服务开发平台:Spring Cloud和Kubernetes的优缺点,并指出结合二者的优点来组合使用,将在微服务旅程上获得更大成功。

当我们需要部署微服务的时候,哪个更好?Spring Cloud还是Kubernetes?答案是都可以,只是各自有其优势。

Spring Cloud 和 Kubernetes 都宣称自己是开发和运行微服务的最佳环境,但是它们的本质非常不一样,所追求的目标也不同。本文我们分析一下两个平台是如何在其擅长的、实现基于微服务的架构(MSA)上起到作用的,并判断如何利用两者的强项,来帮助我们在微服务旅程上获得成功。

背景故事

最近我读了A. Lukyanchikov写的一篇非常精彩的文章,讲的是用Spring Cloud和Docker来构建微服务架构。如果你还没读过,你应该读一下,因为它给出了一个关于如何利用Spring Cloud来创建一个简单的基于微服务的系统的综合视角。为了构建一个能扩展到数千个服务的可扩展且有弹性的微服务系统,它就必须有一套拥有广泛的构建时和运行时能力的工具集来帮助管理和控制。使用Spring
Cloud,既能实现功能型服务(比如统计服务,账号服务以及通知服务),又能实现基础架构服务(比如日志分析,配置服务器,服务发现,认证服务等)。描述这样一个使用Spring Cloud构造的微服务架构(MSA)图如下所示:
365c0d94-eefa-11e5-90ad-9d74804ca412-2.png
MSA with Spring Cloud (by A. Lukyanchikov)

该图包含了运行时视角,但是它不包含打包,持续集成,扩展性,高可用,自愈等在MSA世界中同样重要的功能。本文假设大部分Java开发者都熟悉Spring Cloud,我们来做个对比,看看Kubernetes是如何提出这些额外的概念,以关联到Spring Cloud的。

微服务概念

我们不会针对两者一个一个概念的比对,而是根据更广阔的微服务概念,来看看Spring Cloud和Kubernetes分别是如何实现他们的。今天有关MSA的一个好事是,它是一个有着易于理解的优缺点评估的架构风格。微服务能加强模块边界,各模块可以有独立的部署和技术差异。但是同时也带来了代价,需要开发分布式系统以及
明显增加操作成本。一个关键的成功因素是聚焦于使用一套能帮助你实现尽可能多的MSA概念的工具。能使得启动过程迅速且容易是很重要的,但是通向产品化的旅程是很漫长的,你需要达到这样的高度才能到达那里。
Screen_Shot_2016-12-03_at_09.32_.38_.png
Microservices concerns

如上图所示,我们可以看到MSA中必须实现的那些最普遍的技术概念(我们将不会关注非技术概念,比如组织结构,文化等)。

技术匹配

Sping Cloud和Kubernetes这两个平台非常不一样,并且它们之间也没有直接的等价特性。如果我们将每个MSA概念匹配到这两个平台使用的实现这些概念的技术/项目,我们能得出下表。
Screen_Shot_2016-11-30_at_08.45_.11_.png
Spring Cloud and Kubernetes technologies

从上表中能快速得出的结论是:

  • Spring Cloud有一个丰富的集合,完备整合了Java库,以实现各种运行时概念,作为应用栈的一部分。因此,这些微服务自身有库和运行时代理,来做客户端的服务发现,负载均衡,配置更新,度量跟踪等。各种模式,比如单集群服务和批量任务,也是由JVM来管理的。
  • Kubernetes兼容多种语言,目标不止Java平台,并且以一种通用方式为所有语言实现了分布式的计算挑战。它提供了平台级以及应用栈之外的服务,比如配置管理,服务发现,负载均衡,追踪,度量,单例,调度任务。应用不需要任何库或者代理来实现客户端逻辑,且可用任何语言来写。
  • 在某些领域,两个平台都依赖相似的第三方工具。比如,ELK和EFK栈,追踪库等。某些库,比如Hystric和Spring Boot,在两个环境上都非常有用。有些领域两个平台是互补的,整合到一起能创造出一个更强大的解决方案(KubeFlix和Spring Cloud Kubernetes 就是这样的例子)。

微服务需求

为了画出每个项目的范围,这里有张表列出了几乎是端到端的MSA需求,从最底层的硬件,到最上层的DevOps和自服务经验,并且列出了如何关联到Spring Cloud和Kubernetes平台。
Screen_Shot_2016-12-03_at_14.09_.41_.png
Microservices requirements

在某些场景下,两个项目使用不同的方法达成了相同的需求,在某些领域,一个项目可能会强于另一个。但是在某些情况下,两个项目是互补的,可以组合起来达成高级的微服务经验。例如,Spring Boot提供了Maven插件来构建单个JAR应用包。这样就可以结合Docker和Kubernetes的声明式部署和调度能力,使得运行微服务变得轻而易举。同样的,Spring Cloud有个内嵌的应用库,可以利用Hystric(自带隔离和熔断模式)和Ribbon(用来负载均衡)来创建有弹性的,容错的微服务。但是光靠这个还不够,当这个能力和Kubernetes的健康检查,进程重启,以及自动伸缩能力结合在一起时,微服务才能成为一个反脆弱系统。

优点和缺点

因为两个平台不能直接用每个特性来比较,我们也不打算针对每个平台都深入每个特性,因此这里就以总结性方式来说明一下两个平台的优缺点。

Spring Cloud

Spring Cloud给开发者提供了一个工具,能在分布式系统中快速构建例如配置管理,服务发现,熔断,路由等通用模式。这是基于Netflix的OSS库,它们是用Java写的,面向Java开发者。

优势

  • 由Spring平台自身来提供统一的编程模型,加上Spring Boot的快速创建应用的能力,可以给开发者大量的微服务开发经验。例如,只要极少量的标签,你就可以创建一个配置服务器,再加一些标签,你就可以得到一个客户端库来配置你的服务。
  • 有大量的覆盖了大多数运行时概念的库可供选择。因为所有的库都是用Java写的,它能提供更多的特性,更强的控制,以及更好的一致性选项。
  • 不同的Spring Cloud库都可以很好的整合在一起。例如,Feign客户端也可以使用Hystrix作为熔断器,以及Ribbon作为请求负载均衡器。每一个都是标签驱动的,这样对Java开发者来说就很容易开发。

缺点

  • Spring Cloud其中一个最主要的优点也是它的缺点,即它只针对Java。MSA 的强烈的目标是具备互换技术栈,库,必要的时候甚至是语言的能力。而这对Spring Cloud来说不可能。如果你想要消费Spring Cloud/Netflix OSS基础服务,比如配置管理,服务发现,或者负载均衡,解决方案不是很优雅。Netflix Prana 项目实现了sidecar模式,让Java客户端库基于HTTP协议,使得那些非JVM语言写的应用也可以存在于NetflixOSS系统中,但是这种方式不是很优雅。
  • Java开发者需要关注非常多的事情,Java应用需要处理非常多的事情。每个微服务都需要运行各种客户端来获得配置恢复、服务发现、负载均衡等功能。这些客户端很容易建立,但是它们没有隐藏环境的构建时和运行时依赖。例如,开发者可以用@EnableConfigServer标签创建一个配置服务器,但是这也是唯一路径。每次开发者想要运行一个单一的微服务,他们需要使配置服务器正常运行。对一个受控环境,开发者不得不考虑让配置服务器高可用,且因为它可以由Git或者Svn来支持,他们还需要一个共享的文件系统。同样的,对服务发现来说,开发者首先需要启动一个Eureka服务器。对一个受控环境,他们需要在每个AZ上都用多个实例来实现集群。这有点类似于,除了实现所有的功能性服务外,Java开发者还需要构建和管理一个非试用型的微服务平台。
  • 单独使用Spring Cloud在微服务旅程上无法走得很长远,在一个完整的微服务经历中,开发者还需要考虑自动化部署,调度,资源管理,进程隔离,自愈,构建流水线等功能。在这点上,我觉得单独拿Spring Cloud和Kubernetes来比较不太公平,更公正的比较应该是Spring Cloud + Cloud Foundry(或者 Docker Swarm)和Kubernetes来比较。但是那也说明,对一个完整的端到端的微服务经历,Spring
    Cloud还需要补充一个应用平台,就像Kubernetes那样。

Kubernetes

Kubernetes 是一个针对容器应用的自动化部署,伸缩和管理的开源系统。它兼容多种语言且提供了创建,运行,伸缩以及管理分布式系统的原语。

优势

  • Kubernetes是语言不感知的容器管理平台,能兼容运行原生云应用和传统的容器化应用。它提供的服务,比如配置管理,服务发现,负载均衡,度量收集,以及日志聚合,能被各种语言使用。这使得组织可以只提供一个平台供多个项目组使用(包括使用Spring的Java开发者),并且提供多种目的:应用开发,测试环境,构建环境(运行资源控制系统,构建服务器,人工仓库)等。
  • 和Spring Cloud相比,Kubernetes实现了更广阔的MSA概念集合。除了提供运行时服务,Kubernetes也允许我们提供环境变量,设置资源限制,RBAC,管理应用生命周期,使能自动伸缩和自愈(表现为就像一个反脆弱平台)。
  • Kubernetes的技术是基于Google 15年的管理容器的研究和开发经验。除此以外,还有将近1000个 committer,它几乎是GitHub上开源社区最活跃的项目。

缺点

  • Kubernetes是兼容多种语言的,因此它的服务和原语是通用的,不像Spring Cloud对JVM那样,没有针对不同的平台做优化。例如,配置是通过环境变量或者挂载文件系统传递给应用的。它没有Spring Cloud配置提供的那样精妙的配置更新能力。
  • Kubernetes不是一个针对开发者的平台。它的目的是供有DevOps思想的IT人员使用。因此,Java开发者需要学习一些新的概念,并更开放得学习新的解决问题的方式。不管使用MiniKube来部署一个Kubernetes开发实例是多么得容易,手工安装一个高可用的Kubernetes集群是有明显的操作成本的。
  • Kubernetes仍是一个相对较新的平台(2年),它也还在活跃得开发和生长中。因此每个版本都会有许多新的特性,使得我们很难去一直跟踪。好消息是这个问题已经被正视,API做成了可扩展且是后向兼容的。

两个平台的最佳实践

正如你所看到的,两个平台都有各自的强项,也有需要提高的地方。Spring Cloud 是一个容易上手的,开发者友好的平台。而Kubernetes是DevOps友好的,有着陡峭的学习曲线,但是包含了更广泛的微服务概念。这里是针对这几点的总结。
Screen_Shot_2016-12-03_at_15.45_.54_.png
Strengths and weaknesses

两个框架实现了不同范围的MSA概念,使用的是从根源上就有区别的方式。Spring Cloud方式是尽力在JVM范畴内来解决每个MSA的挑战,而Kubernetes的方式是尽力为开发者在平台层面消除这些问题。Spring Cloud在JVM内非常强大,Kubernetes在管理这些JVM上非常强大。因此,整合这两者取它们的最佳部分,是一个很自然的进步过程。
spring_cloud_and_kubernetes_mixed_-_Page_1(1).png
Spring Cloud backed by Kubernetes

有了这样一个整合,Spring提供应用的打包,Docker和Kubernetes提供部署和调度。Spring通过Hystrix线程池提供应用内的隔离,而Kubernetes通过资源,进程和命名空间来提供隔离。Spring为每个微服务提供健康终端,而Kubernetes执行健康检查,且把流量导到健康服务。Spring外部化配置并更新它们,而Kubernetes分发配置到每个微服务。这个列表将一直持续。
stack_-_Page_1.png
My favorite microservices stack

我最喜欢的微服务平台是哪个?我两个都喜欢。我喜欢Spring 框架提供的开发者经验。它是标签驱动的,并且拥有包含了各种功能需求的库。跟任何整合相关的,我喜欢Apache Camel(而不是Spring
Integration),它能提供应用级别的连接器,消息,路由,可靠性和容错功能。而对跟集群和管理多应用实例相关的,我更喜欢魔法般的Kubernetes能力。不论何时,有重复功能,比如服务发现,负载均衡,配置管理,我尽量使用Kubernetes提供的跟语言无关的原语。

原文链接:Deploying Microservices: Spring Cloud vs. Kubernetes(翻译:池剑锋)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何设计接口的测试用例 边界值测试 组合条件测试

发表于 2017-11-28

这篇文章简单总结下我是如何设计接口测试用例的。

今天在帮同事review代码的时候,发现他的代码遗漏了一些场景的处理,就顺便跟他多聊了些为对这个话题的看法。

在这里假设一个接口设计如下:

1
复制代码UserInfoDTO listUserInfoByUserIds(UserInfoQueryParam param);

其中UserInfoQueryParam的定义如下:

1
2
3
4
5
复制代码public class UserInfoQueryParam {
//省略序列化ID
List<Long> userIds;
//...省略其他字段
}

边界值测试

这种方法,一般用于测试一个接口的健壮性;针对userIds这个属性,我会构建如下测试用例:

  1. userIds=null
  2. userIds=EmptyList
  3. userIds的size等于批量接口的限定值
  4. userIds的size大于批量接口的限定值
  5. userIds中的元素有null的情况
  6. userIds中的元素全部为null的情况
  7. userIds中的元素有0(或负数)的情况
  8. userIds中的元素全部为0(或负数)的情况

组合条件测试

这种方法,一般用于测试不同情况下的业务处理逻辑是否符合预期。在这个例子中,userIds可能有两种类型,但是我们这个接口需要支持这两种类型,因此测试用例设计如下:

  1. userIds中为纯粹的类型1的数据
  2. userIds中为纯粹的类型2的数据
  3. userIds中为类型1和类型2中的数据的混合情况

以上就是我在构建一个接口的测试用例时候思路,欢迎大家讨论。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中的逆变与协变 1 逆变与协变 2 泛型中的通配

发表于 2017-11-28

看下面一段代码

1
2
3
4
5
6
复制代码`Number num = new Integer(1);` 
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch

List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
list.add(new Float(1.2f)); //error

有人会纳闷,为什么Number的对象可以由Integer实例化,而ArrayList<Number>的对象却不能由ArrayList<Integer>实例化?list中的<? extends Number>声明其元素是Number或Number的派生类,为什么不能add Integer和Float?为了解决这些问题,我们需要了解Java中的逆变和协变以及泛型中通配符用法。

  1. 逆变与协变

在介绍逆变与协变之前,先引入Liskov替换原则(Liskov Substitution Principle, LSP)。

Liskov替换原则

LSP由Barbara Liskov于1987年提出,其定义如下:

所有引用基类(父类)的地方必须能透明地使用其子类的对象。

LSP包含以下四层含义:

  • 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
  • 子类中可以增加自己的方法。
  • 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
  • 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。

前面的两层含义比较好理解,后面的两层含义会在下文中详细解释。根据LSP,我们在实例化对象的时候,可以用其子类进行实例化,比如:

1
复制代码Number num = new Integer(1);

定义

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果(A)、(B)表示类型,(f(\cdot))表示类型转换,(\leq)表示继承关系(比如, (A \leq B)表示(A)是由(B)派生出来的子类);

  • (f(\cdot))是逆变(contravariant)的,当(A \leq B)时有(f(B) \leq f(A))成立;
  • (f(\cdot))是协变(covariant)的,当(A \leq B)时有(f(A) \leq f(B)成立);
  • (f(\cdot))是不变(invariant)的,当(A \leq B)时上述两个式子均不成立,即(f(A))与(f(B))相互之间没有继承关系。

类型转换

接下来,我们看看Java中的常见类型转换的协变性、逆变性或不变性。

泛型

令f(A)=ArrayList<A>,那么(f(\cdot))时逆变、协变还是不变的呢?如果是逆变,则ArrayList<Integer>是ArrayList<Number>的父类型;如果是协变,则ArrayList<Integer>是ArrayList<Number>的子类型;如果是不变,二者没有相互继承关系。开篇代码中用ArrayList<Integer>实例化list的对象错误,则说明泛型是不变的。

数组

令f(A)=[]A,容易证明数组是协变的:

1
复制代码Number[] numbers = new Integer[3];

方法

方法的形参是协变的、返回值是逆变的:


通过与网友iamzhoug37的讨论,更新如下。

调用方法result = method(n);根据Liskov替换原则,传入形参n的类型应为method形参的子类型,即typeof(n)≤typeof(method's parameter);result应为method返回值的基类型,即typeof(methods's return)≤typeof(result):

1
2
3
4
5
6
7
复制代码`static Number method(Number num) {`
return 1;
}

Object result = method(new Integer(2)); //correct
Number result = method(new Object()); //error
Integer result = method(new Integer(2)); //error

在Java 1.4中,子类覆盖(override)父类方法时,形参与返回值的类型必须与父类保持一致:

1
2
3
4
5
6
7
8
复制代码`class Super {`
Number method(Number n) { ... }
}

class Sub extends Super {
@Override
Number method(Number n) { ... }
}

从Java 1.5开始,子类覆盖父类方法时允许协变返回更为具体的类型:

1
2
3
4
5
6
7
8
复制代码`class Super {`
Number method(Number n) { ... }
}

class Sub extends Super {
@Override
Integer method(Number n) { ... }
}
  1. 泛型中的通配符

实现泛型的协变与逆变

Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场:

  • <? extends>实现了泛型的协变,比如:
1
复制代码List<? extends Number> list = new ArrayList<Integer>();
  • <? super>实现了泛型的逆变,比如:
1
复制代码List<? super Number> list = new ArrayList<Object>();

extends与super

为什么(开篇代码中)List<? extends Number> list在add Integer和Float会发生编译错误?首先,我们看看add的实现:

1
2
3
复制代码`public interface List<E> extends Collection<E> {`
boolean add(E e);
}

在调用add方法时,泛型E自动变成了<? extends Number>,其表示list所持有的类型为在Number与Number派生子类中的某一类型,其中包含Integer类型却又不特指为Integer类型(Integer像个备胎一样!!!),故add Integer时发生编译错误。为了能调用add方法,可以用super关键字实现:

1
2
3
复制代码`List<? super Number> list = new ArrayList<Object>();`
list.add(new Integer(1));
list.add(new Float(1.2f));

<? super Number>表示list所持有的类型为在Number与Number的基类中的某一类型,其中Integer与Float必定为这某一类型的子类;所以add方法能被正确调用。从上面的例子可以看出,extends确定了泛型的上界,而super确定了泛型的下界。

PECS

现在问题来了:究竟什么时候用extends什么时候用super呢?《Effective Java》给出了答案:

PECS: producer-extends, consumer-super.

比如,一个简单的Stack API:

1
2
3
4
5
6
复制代码`public class  Stack<E>{`
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
}

要实现pushAll(Iterable<E> src)方法,将src的元素逐一入栈:

1
2
3
4
复制代码`public void pushAll(Iterable<E> src){`
for(E e : src)
push(e)
}

假设有一个实例化Stack<Number>的对象stack,src有Iterable<Integer>与 Iterable<Float>;在调用pushAll方法时会发生type mismatch错误,因为Java中泛型是不可变的,Iterable<Integer>与 Iterable<Float>都不是Iterable<Number>的子类型。因此,应改为

1
2
3
4
5
复制代码`// Wildcard type for parameter that serves as an E producer`
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}

要实现popAll(Collection<E> dst)方法,将Stack中的元素依次取出add到dst中,如果不用通配符实现:

1
2
3
4
5
复制代码`// popAll method without wildcard type - deficient!`
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}

同样地,假设有一个实例化Stack<Number>的对象stack,dst为Collection<Object>;调用popAll方法是会发生type mismatch错误,因为Collection<Object>不是Collection<Number>的子类型。因而,应改为:

1
2
3
4
5
复制代码`// Wildcard type for parameter that serves as an E consumer`
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}

在上述例子中,在调用pushAll方法时生产了E 实例(produces E instances),在调用popAll方法时dst消费了E 实例(consumes E instances)。Naftalin与Wadler将PECS称为Get and Put Principle。

java.util.Collections的copy方法(JDK1.7)完美地诠释了PECS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码`public static <T> void copy(List<? super T> dest, List<? extends T> src) {`
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}

PECS总结:

  • 要从泛型类取数据时,用extends;
  • 要往泛型类写数据时,用super;
  • 既要取又要写,就不用通配符(即extends与super都不用)。
  1. 参考资料

[1] meriton, Covariance, Invariance and Contravariance explained in plain English?.
[2] Bert F, Difference between <? super T> and <? extends T> in Java.
[3] Joshua Bloch, Effective Java.

如需转载,请注明作者及出处. 作者:Treant 出处:www.cnblogs.com/en-heng/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 设计模式之模板方法模式(十三)

发表于 2017-11-28

一、前言

上篇《Java 设计模式之代理模式(十二)》 为 Java 设计模式中结构型模式的最后一章,今天开始介绍 Java 设计模式中的行为型模式的第一种模式–模板方法模式。

二、简单介绍

# 2.1 定义

模板方法( Template Method)模式是行为模式之一,它把具有特定步骤算法中的某些必要的处理委让给抽象方法,通过子类继承对抽象方法的不同实现改变整个算法的行为。

# 2.2 应用场景

  1. 具有统一的操作步骤或操作过程。
  2. 具有不同的操作细节。

三、实现方式

我们以在银行办理业务为例,张三和李四去银行办理业务。张三办理存钱业务,李四办理取钱业务。

实体类:

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
复制代码public class People {



private String name;



public People(String name) {

this.name = name;

}



public void queueUp() {

System.out.println(this.name + "排队取号等候");

}



public void service(String type) {

System.out.println(this.name + "办理" + type + "业务");

}



public void evaluate() {

System.out.println(this.name + "反馈评分");

}

}

客户端:

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
复制代码public class Client {



public static void main(String[] args) {

People p1 = new People("张三");

p1.queueUp();

p1.service("存钱");

p1.evaluate();



System.out.println("==============");



People p2 = new People("李四");

p2.queueUp();

p2.service("取钱");

p2.evaluate();

}

}

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码张三排队取号等候

张三办理存钱业务

张三反馈评分

==============

李四排队取号等候

李四办理取钱业务

李四反馈评分

在银行办理业务需要 3 个步骤,即 “排队取号” -> “办理业务” -> “反馈评分”,我们发现 “排队取号” 和 “反馈评分” 的操作(逻辑代码)基本是一致的,只有客户办理的业务是有所差异的。但是,在客户端中,每个人办理业务需要调用 3 次方法,不够简便。

通过分析,现在的案例符合运用模板方法模式的两个场景:相同步骤(都需要调用 queueUp、service 和 evaluate)和不同操作细节(service)。

正如上文描述的,我们可以讲公共的代码(queueUp 和 evaluate)放到父类中,同时,由于父类中有部分不能复用的方法(service ),将其实现的责任延迟到子类中进行即可。

实体类:

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
96
97
98
99
100
101
102
103
复制代码public abstract class People {



private String name;



public People(String name) {

this.name = name;

}



private void queueUp() {

System.out.println(this.name + "排队取号等候");

}



protected abstract void service();



private void evaluate() {

System.out.println(this.name + "反馈评分");

}



public void work() {

this.queueUp();

this.service();

this.evaluate();

}



public String getName() {

return name;

}

}



class ZhangSan extends People {



public ZhangSan(String name) {

super(name);

}



@Override

protected void service() {

System.out.println(this.getName() + "办理存钱业务");

}

}



class LiSi extends People {



public LiSi(String name) {

super(name);

}



@Override

protected void service() {

System.out.println(this.getName() + "办理取钱业务");

}

}

其中,work 方法就是一个模板方法,该方法不管方法实现,只负责调用方法顺序。子类继承 service 方法实现不同的业务需求。

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码public class Client {



public static void main(String[] args) {

People p1 = new ZhangSan("张三");

p1.work();



System.out.println("===========");



People p2 = new LiSi("李四");

p2.work();

}

}

运行结果与上文的一致。现在,客户端代码精简许多。

UML 类图表示如下:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java Fork/Join 框架

发表于 2017-11-28

译序

Doug Lea 大神关于Java 7引入的他写的Fork/Join框架的论文。

响应式编程(Reactive Programming / RP)作为一种范式在整个业界正在逐步受到认可和落地,是对过往系统的业务需求理解梳理之后对系统技术设计/架构模式的提升总结。Java作为一个成熟平台,对于趋势一向有些稳健的接纳和跟进能力,有着令人惊叹的生命活力:

Java 7提供了ForkJoinPool,支持了Java 8提供的Stream(Reactive Stream是RP的一个核心组件)。
另外Java 8还提供了Lamda(有效地表达和使用RP需要FP的语言构件和理念)。
有了前面的这些稳健但不失时机的准备,在Java 9中提供了面向RP的Flow API,为Java圈子提供了官方的RP API,标志着RP由集市式的自由探索阶段 向 教堂式的统一使用的转变。

通过上面这些说明,可以看到ForkJoinPool的基础重要性。

对了,另外提一下Java 9的Flow API的@author也是 Doug Lee 哦~

PS:基于Alex/萧欢 翻译、方腾飞 校对的译文稿:Java Fork Join 框架,补译『结论』之后3节,调整了格式和一些用词,整理成完整的译文。译文源码在GitHub的这个仓库中,可以提交Issue/Fork后提交代码来建议/指正。

  1. 摘要

这篇论文描述了Fork/Join框架的设计、实现以及性能,这个框架通过(递归的)把问题划分为子任务,然后并行的执行这些子任务,等所有的子任务都结束的时候,再合并最终结果的这种方式来支持并行计算编程。总体的设计参考了为Cilk设计的work-stealing框架。就设计层面来说主要是围绕如何高效的去构建和管理任务队列以及工作线程来展开的。性能测试的数据显示良好的并行计算程序将会提升大部分应用,同时也暗示了一些潜在的可以提升的空间。

校注1: Cilk是英特尔Cilk语言。英特尔C++编辑器的新功能Cilk语言扩展技术,为C/C++语言增加了细粒度任务支持,使其为新的和现有的软件增加并行性来充分发掘多处理器能力变得更加容易。

  1. 简介

Fork/Join并行方式是获取良好的并行计算性能的一种最简单同时也是最有效的设计技术。Fork/Join并行算法是我们所熟悉的分治算法的并行版本,典型的用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码Result solve(Problem problem) {
 
    if (problem is small) {
 
        directly solve problem
 
    } else {
 
        split problem into independent parts
        fork new subtasks to solve each part
        join all subtasks
        compose result from subresults
    }
}

fork操作将会启动一个新的并行Fork/Join子任务。join操作会一直等待直到所有的子任务都结束。Fork/Join算法,如同其他分治算法一样,总是会递归的、反复的划分子任务,直到这些子任务可以用足够简单的、短小的顺序方法来执行。

一些相关的编程技术和实例在《Java并发编程 —— 设计原则与模式 第二版》[7] 4.4章节中已经讨论过。这篇论文将讨论FJTask的设计(第2节)、实现(第3节)以及性能(第4节),它是一个支持并行编程方式的Java™框架。FJTask 作为util.concurrent软件包的一部分,目前可以在 http://gee.cs.oswego.edu/ 获取到。

  1. 设计

Fork/Join程序可以在任何支持以下特性的框架之上运行:框架能够让构建的子任务并行执行,并且拥有一种等待子任务运行结束的机制。然而,java.lang.Thread类(同时也包括POSIX pthread,这些也是Java线程所基于的基础)对Fork/Join程序来说并不是最优的选择:

Fork/Join任务对同步和管理有简单的和常规的需求。相对于常规的线程来说,Fork/Join任务所展示的计算布局将会带来更加灵活的调度策略。例如,Fork/Join任务除了等待子任务外,其他情况下是不需要阻塞的。因此传统的用于跟踪记录阻塞线程的代价在这种情况下实际上是一种浪费。
对于一个合理的基础任务粒度来说,构建和管理一个线程的代价甚至可以比任务执行本身所花费的代价更大。尽管粒度是应该随着应用程序在不同特定平台上运行而做出相应调整的。但是超过线程开销的极端粗粒度会限制并行的发挥。
简而言之,Java标准的线程框架对Fork/Join程序而言太笨重了。但是既然线程构成了很多其他的并发和并行编程的基础,完全消除这种代价或者为了这种方式而调整线程调度是不可能(或者说不切实际的)。

尽管这种思想已经存在了很长时间了,但是第一个发布的能系统解决这些问题的框架是Cilk[5]。Cilk和其他轻量级的框架是基于操作系统的基本的线程和进程机制来支持特殊用途的Fork/Join程序。这种策略同样适用于Java,尽管Java线程是基于低级别的操作系统的能力来实现的。创造这样一个轻量级的执行框架的主要优势是能够让Fork/Join程序以一种更直观的方式编写,进而能够在各种支持JVM的系统上运行。

FJTask框架是基于Cilk设计的一种演变。其他的类似框架有Hood[4]、Filaments[8]、Stackthreads[10]以及一些依赖于轻量级执行任务的相关系统。所有这些框架都采用和操作系统把线程映射到CPU上相同的方式来把任务映射到线程上。只是他们会使用Fork/Join程序的简单性、常规性以及一致性来执行这种映射。尽管这些框架都能适应不能形式的并行程序,他们优化了Fork/Join的设计:

  • 一组工作者线程池是准备好的。每个工作线程都是标准的(『重量级』)处理存放在队列中任务的线程(这地方指的是Thread类的子类FJTaskRunner的实例对象)。通常情况下,工作线程应该与系统的处理器数量一致。对于一些原生的框架例如说Cilk,他们首先将映射成内核线程或者是轻量级的进程,然后再在处理器上面运行。在Java中,虚拟机和操作系统需要相互结合来完成线程到处理器的映射。然后对于计算密集型的运算来说,这种映射对于操作系统来说是一种相对简单的任务。任何合理的映射策略都会导致线程映射到不同的处理器。
  • 所有的Fork/Join任务都是轻量级执行类的实例,而不是线程实例。在Java中,独立的可执行任务必须要实现Runnable接口并重写run方法。在FJTask框架中,这些任务将作为子类继承FJTask而不是Thread,它们都实现了Runnable接口。(对于上面两种情况来说,一个类也可以选择实现Runnable接口,类的实例对象既可以在任务中执行也可以在线程中执行。因为任务执行受到来自FJTask方法严厉规则的制约,子类化FJTask相对来说更加方便,也能够直接调用它们。)
  • 我们将采用一个特殊的队列和调度原则来管理任务并通过工作线程来执行任务。这些机制是由任务类中提供的相关方式实现的:主要是由fork、join、isDone(一个结束状态的标示符),和一些其他方便的方法,例如调用coInvoke来分解合并两个或两个以上的任务。
  • 一个简单的控制和管理类(这里指的是FJTaskRunnerGroup)来启动工作线程池,并初始化执行一个由正常的线程调用所触发的Fork/Join任务(就类似于Java程序中的main方法)。

作为一个给程序员演示这个框架如何运行的标准实例,这是一个计算法斐波那契函数的类。

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
复制代码class Fib extends FJTask {
    static final int threshold = 13;
    volatile int number; // arg/result
 
    Fib(int n) {
        number = n;
    }
 
    int getAnswer() {
        if (!isDone())
            throw new IllegalStateException();
        return number;
    }
 
    public void run() {
        int n = number;
        if (n <= threshold) // granularity ctl
            number = seqFib(n);
        else {
            Fib f1 = new Fib(n - 1);
            Fib f2 = new Fib(n - 2);
            coInvoke(f1, f2);
            number = f1.number + f2.number;
        }
    }
 
    public static void main(String[] args) {
        try {
            int groupSize = 2; // for example
            FJTaskRunnerGroup group = new FJTaskRunnerGroup(groupSize);
            Fib f = new Fib(35); // for example
            group.invoke(f);
            int result = f.getAnswer();
            System.out.println("Answer: " + result);
        } catch (InterruptedException ex) {
        }
    }
 
    int seqFib(int n) {
        if (n <= 1) return n;
        else return seqFib(n − 1) + seqFib(n − 2);
    }
}

这个版本在第4节中所提到的平台上的运行速度至少比每个任务都在Thread类中运行快30倍。在保持性能的同时这个程序仍然维持着Java多线程程序的可移植性。对程序员来说通常有两个参数值的他们关注:

  • 对于工作线程的创建数量,通常情况下可以与平台所拥有的处理器数量保持一致(或者更少,用于处理其他相关的任务,或者有些情况下更多,来提升非计算密集型任务的性能)。
  • 一个粒度参数代表了创建任务的代价会大于并行化所带来的潜在的性能提升的临界点。这个参数更多的是取决于算法而不是平台。通常在单处理器上运行良好的临界点,在多处理器平台上也会发挥很好的效果。作为一种附带的效益,这种方式能够与Java虚拟机的动态编译机制很好的结合,而这种机制在对小块方法的优化方面相对于单块的程序来说要好。这样,加上数据本地化的优势,Fork/Join算法的性能即使在单处理器上面的性能都较其他算法要好。

2.1 work−stealing

Fork/Join框架的核心在于轻量级调度机制。FJTask采用了Cilk的work-stealing所采用的基本调度策略:

  • 每一个工作线程维护自己的调度队列中的可运行任务。
  • 队列以双端队列的形式被维护(注:deques通常读作『decks』),不仅支持后进先出 —— LIFO的push和pop操作,还支持先进先出 —— FIFO的take操作。
  • 对于一个给定的工作线程来说,任务所产生的子任务将会被放入到工作者自己的双端队列中。
  • 工作线程使用后进先出 —— LIFO(最新的元素优先)的顺序,通过弹出任务来处理队列中的任务。
  • 当一个工作线程的本地没有任务去运行的时候,它将使用先进先出 —— FIFO的规则尝试随机的从别的工作线程中拿(『窃取』)一个任务去运行。
  • 当一个工作线程触及了join操作,如果可能的话它将处理其他任务,直到目标任务被告知已经结束(通过isDone方法)。所有的任务都会无阻塞的完成。
  • 当一个工作线程无法再从其他线程中获取任务和失败处理的时候,它就会退出(通过yield、sleep和/或者优先级调整,参考第3节)并经过一段时间之后再度尝试直到所有的工作线程都被告知他们都处于空闲的状态。在这种情况下,他们都会阻塞直到其他的任务再度被上层调用。

使用后进先出 —— LIFO用来处理每个工作线程的自己任务,但是使用先进先出 —— FIFO规则用于获取别的任务,这是一种被广泛使用的进行递归Fork/Join设计的一种调优手段。引用[5]讨论了详细讨论了里面的细节。

让窃取任务的线程从队列拥有者相反的方向进行操作会减少线程竞争。同样体现了递归分治算法的大任务优先策略。因此,更早期被窃取的任务有可能会提供一个更大的单元任务,从而使得窃取线程能够在将来进行递归分解。

作为上述规则的一个后果,对于一些基础的操作而言,使用相对较小粒度的任务比那些仅仅使用粗粒度划分的任务以及那些没有使用递归分解的任务的运行速度要快。尽管相关的少数任务在大多数的Fork/Join框架中会被其他工作线程窃取,但是创建许多组织良好的任务意味着只要有一个工作线程处于可运行的状态,那么这个任务就有可能被执行。

  1. 实现

这个框架是由大约800行纯Java代码组成,主要的类是FJTaskRunner,它是java.lang.Thread的子类。FJTask自己仅仅维持一个关于结束状态的布尔值,所有其他的操作都是通过当前的工作线程来代理完成的。JFTaskRunnerGroup类用于创建工作线程,维护一些共享的状态(例如:所有工作线程的标示符,在窃取操作时需要),同时还要协调启动和关闭。

更多实现的细节文档可以在util.concurrent并发包中查看。这一节只着重讨论两类问题以及在实现这个框架的时候所形成的一些解决方案:支持高效的双端列表操作(push、pop和take), 并且当工作线程在尝试获取新的任务时维持窃取的协议。

3.1 双端队列

(校注:双端队列中的元素可以从两端弹出,其限定插入和删除操作在队列的两端进行。)

为了能够获得高效以及可扩展的执行任务,任务管理需要越快越好。创建、发布、和弹出(或者出现频率很少的获取)任务在顺序编程模式中会引发程序调用开销。更低的开销可以使得程序员能够构建更小粒度的任务,最终也能更好的利用并行所带来的益处。

Java虚拟机会负责任务的内存分配。Java垃圾回收器使我们不需要再去编写一个特殊的内存分配器去维护任务。相对于其他语言的类似框架,这个原因使我们大大降低了实现FJTask的复杂性以及所需要的代码数。

双端队列的基本结构采用了很常规的一个结构 —— 使用一个数组(尽管是可变长的)来表示每个队列,同时附带两个索引:top索引就类似于数组中的栈指针,通过push和pop操作来改变。base索引只能通过take操作来改变。鉴于FJTaskRunner操作都是无缝的绑定到双端队列的细节之中,(例如,fork直接调用push),所以这个数据结构直接放在类之中,而不是作为一个单独的组件。

但是双端队列的元素会被多线程并发的访问,在缺乏足够同步的情况下,而且单个的Java数组元素也不能声明为volatile变量(校注:声明成volatile的数组,其元素并不具备volatile语意),每个数组元素实际上都是一个固定的引用,这个引用指向了一个维护着单个volatile引用的转发对象。一开始做出这个决定主要是考虑到Java内存模型的一致性。但是在这个级别它所需要的间接寻址被证明在一些测试过的平台上能够提升性能。可能是因为访问邻近的元素而降低了缓存争用,这样内存里面的间接寻址会更快一点。

实现双端队列的主要挑战来自于同步和他的撤销。尽管在Java虚拟机上使用经过优化过的同步工具,对于每个push和pop操作都需要获取锁还是让这一切成为性能瓶颈。然后根据以下的观察结果我们可以修改Clik中的策略,从而为我们提供一种可行的解决方案:

  • push和pop操作仅可以被工作线程的拥有者所调用。
  • 对take的操作很容易会由于窃取任务线程在某一时间对take操作加锁而限制。(双端队列在必要的时间也可以禁止take操作。)这样,控制冲突将被降低为两个部分同步的层次。
  • pop和take操作只有在双端队列为空的时候才会发生冲突,否则的话,队列会保证他们在不同的数组元素上面操作。

把top和base索引定义为volatile变量可以保证当队列中元素不止一个时,pop和take操作可以在不加锁的情况下进行。这是通过一种类似于Dekker算法来实现的。当push预递减到top时:

1
复制代码if (–top >= base) ...

和take预递减到base时:

1
复制代码if (++base < top) ...

在上述每种情况下他们都通过比较两个索引来检查这样是否会导致双端队列变成一个空队列。一个不对称的规则将用于防止潜在的冲突:pop会重新检查状态并在获取锁之后继续(对take所持有的也一样),直到队列真的为空才退出。而take操作会立即退出,特别是当尝试去获得另外一个任务。与其他类似使用Clik的THE协议一样,这种不对称性是唯一重要的改变。

使用volatile变量索引push操作在队列没有满的情况下不需要同步就可以进行。如果队列将要溢出,那么它首先必须要获得队列锁来重新设置队列的长度。其他情况下,只要确保top操作排在队列数组槽盛在抑制干涉带之后更新。

在随后的初始化实现中,发现有好几种JVM并不符合Java内存模型中正确读取写入的volatile变量的规则。作为一个工作区,pop操作在持有锁的情况下重试的条件已经被调整为:如果有两个或者更少的元素,并且take操作加了第二把锁以确保内存屏障效果,那么重试就会被触发。只要最多只有一个索引被拥有者线程丢失这就是满足的,并且只会引起轻微的性能损耗。

3.2 抢断和闲置

在抢断式工作框架中,工作线程对于他们所运行的程序对同步的要求一无所知。他们只是构建、发布、弹出、获取、管理状态和执行任务。这种简单的方案使得当所有的线程都拥有很多任务需要去执行的时候,它的效率很高。然而这种方式是有代价的,当没有足够的工作的时候它将依赖于试探法。也就是说,在启动一个主任务,直到它结束,在有些Fork/Join算法中都使用了全面停止的同步指针。

主要的问题在于当一个工作线程既无本地任务也不能从别的线程中抢断任务时怎么办。如果程序运行在专业的多核处理器上面,那么可以依赖于硬件的忙等待自旋循环的去尝试抢断一个任务。然而,即使这样,尝试抢断还是会增加竞争,甚至会导致那些不是闲置的工作线程降低效率(由于锁协议,3.1节中)。除此之外,在一个更适合此框架运行的场景中,操作系统应该能够很自信的去运行那些不相关并可运行的进程和线程。

Java中并没有十分健壮的工作来保证这个,但是在实际中它往往是可以让人接受的。一个抢断失败的线程在尝试另外的抢断之前会降低自己的优先级,在尝试抢断之间执行Thread.yeild操作,然后将自己的状态在FJTaskRunnerGroup中设置为不活跃的。他们会一直阻塞直到有新的主线程。其他情况下,在进行一定的自旋次数之后,线程将进入休眠阶段,他们会休眠而不是放弃抢断。强化的休眠机制会给人造成一种需要花费很长时间去划分任务的假象。但是这似乎是最好的也是通用的折中方案。框架的未来版本也许会支持额外的控制方法,以便于让程序员在感觉性能受到影响时可以重写默认的实现。

  1. 性能

如今,随着编译器与Java虚拟机性能的不断提升,性能测试结果也仅仅只能适用一时。但是,本节中所提到的测试结果数据却能揭示Fork/Join框架的基本特性。

下面表格中简单介绍了在下文将会用到的一组Fork/Join测试程序。这些程序是从util.concurrent包里的示例代码改编而来,用来展示Fork/Join框架在解决不同类型的问题模型时所表现的差异,同时得到该框架在一些常见的并行测试程序上的测试结果。

程序 描述
Fib(菲波那契数列) 如第2节所描述的Fibonnaci程序,其中参数值为47阀值为13
Integrate(求积分) 使用递归高斯求积对公式 (2 \cdot i - 1) \cdot x ^ {(2 \cdot i - 1)} 求-47到48的积分,i 为1到5之间的偶数
Micro(求微分) 对一种棋盘游戏寻找最好的移动策略,每次计算出后面四次移动
Sort(排序) 使用合并/快速排序算法对1亿数字进行排序(基于Cilk算法)
MM(矩阵相乘) 2048 X 2048的double类型的矩阵进行相乘
LU(矩阵分解) 4096 X 4096的double类型的矩阵进行分解
Jacobi(雅克比迭代法) 对一个4096 X 4096的double矩阵使用迭代方法进行矩阵松弛,迭代次数上限为100

下文提到的主要的测试,其测试程序都是运行在Sun Enterprise 10000服务器上,该服务器拥有30个CPU,操作系统为Solaris 7系统,运行Solaris商业版1.2 JVM(2.2.2_05发布版本的一个早期版本)。同时,Java虚拟机的关于线程映射的环境参数选择为『bound threads』(译者注:XX:+UseBoundThreads,绑定用户级别的线程到内核线程,只与Solaris有关),而关于虚拟机的内存参数设置在4.2章节讨论。另外,需要注意的是下文提到的部分测试则是运行在拥有4
CPU的Sun Enterprise 450服务器上。

为了降低定时器粒度以及Java虚拟机启动因素对测试结果的影响,测试程序都使用了数量巨大的输入参数。而其它一些启动因素我们通过在启动定时器之前先运行初始化任务来进行屏蔽。所得到的测试结果数据,大部分都是在三次测试结果的中间值,然而一些测试数据仅仅来自一次运行结果(包括4.2 ~ 4.4章节很多测试),因此这些测试结果会有噪音表现。

4.1 加速效果

通过使用不同数目(1 ~ 30)的工作线程对同一问题集进行测试,用来得到框架的扩展性测试结果。虽然我们无法保证Java虚拟机是否总是能够将每一个线程映射到不同的空闲CPU上,同时,我们也没有证据来证明这点。有可能映射一个新的线程到CPU的延迟会随着线程数目的增加而变大,也可能会随不同的系统以及不同的测试程序而变化。但是,所得到的测试结果的确显示出增加线程的数目确实能够增加使用的CPU的数目。

加速比通常表示为 Timen / Time1。如上图所示,其中求积分的程序表现出最好的加速比(30个线程的加速比为28.2),表现最差的是矩阵分解程序(30线程是加速比只有15.35)

另一种衡量扩展性的依据是:任务执行率,及执行一个单独任务(这里的任务有可能是递归分解节点任务也可能是根节点任务)所开销的平均时间。下面的数据显示出一次性执行各个程序所得到的任务执行率数据。很明显,单位时间内执行的任务数目应该是固定常量。然而事实上,随着线程数目增加,所得到的数据会表现出轻微的降低,这也表现出其一定的扩展性限制。这里需要说明的是,之所以任务执行率在各个程序上表现的巨大差异,是因其任务粒度的不同造成的。任务执行率最小的程序是Fib(菲波那契数列),其阀值设置为13,在30个线程的情况下总共完成了280万个单元任务。

导致这些程序的任务完成率没有表现为水平直线的因素有四个。其中三个对所有的并发框架来说都是普遍原因,所以,我们就从对FJTask框架(相对于Cilk等框架)所特有的因素说起,即垃圾回收。

4.2 垃圾回收

总的来说,现在的垃圾回收机制的性能是能够与Fork/Join框架所匹配的:Fork/Join程序在运行时会产生巨大数量的任务单元,然而这些任务在被执行之后又会很快转变为内存垃圾。相比较于顺序执行的单线程程序,在任何时候,其对应的F’
;%ވZh8HE’
;%ވZh最多p倍的内存空间(其中p为线程数目)。基于分代的半空间拷贝垃圾回收器(也就是本文中测试程序所使用的Java虚拟机所应用的垃圾回收器)能够很好的处理这种情况,因为这种垃圾回收机制在进行内存回收的时候仅仅拷贝非垃圾内存单元。这样做,就避免了在手工并发内存管理上的一个复杂的问题,即跟踪那些被一个线程分配却在另一个线程中使用的内存单元。这种垃圾回收机制并不需要知道内存分配的源头,因此也就无需处理这个棘手的问题。

这种垃圾回收机制优势的一个典型体现:使用这种垃圾回收机制,四个线程运行的Fib程序耗时仅为5.1秒钟,而如果在Java虚拟机设置关闭代拷贝回收(这种情况下使用的就是标记清除(mark−sweep)垃圾回收机制了),耗时需要9.1秒钟。

然而,只有内存使用率只有达到一个很高的值的情况下,垃圾回收机制才会成为影响扩展性的一个因素,因为这种情况下,虚拟机必须经常停止其他线程来进行垃圾回收。以下的数据显示出在三种不同的内存设置下(Java虚拟机支持通过额外的参数来设置内存参数),加速比所表现出的差异:默认的4M的半空间,64M的半空间,另外根据线程数目按照公式(2 + 2p)M设置半空间。使用较小的半空间,在额外线程导致垃圾回收率攀高的情况下,停止其他线程并进行垃圾回收的开销开始影响加封。

鉴于上面的结果,我们使用64M的半空间作为其他测试的运行标准。其实设置内存大小的一个更好的策略就是根据每次测试的实际线程数目来确定。(正如上面的测试数据,我们发现这种情况下,加速比会表现的更为平滑)。相对的另一方面,程序所设定的任务粒度的阀值也应该随着线程数目成比例的增长。

4.3 内存分配和字宽

在上文提到的测试程序中,有四个程序会创建并操作数量巨大的共享数组和矩阵:数字排序,矩阵相乘/分解以及松弛。其中,排序算法应该是对数据移动操作(将内存数据移动到CPU缓存)以及系统总内存带宽,最为敏感的。为了确定这些影响因素的性质,我们将排序算法sort改写为四个版本,分别对byte字节数据,short型数据,int型数据以及long型数据进行排序。这些程序所操作的数据都在0 ~ 255之间,以确保这些对比测试之间的平等性。理论上,操作数据的字宽越大,内存操作压力也相应越大。

测试结果显示,内存操作压力的增加会导致加速比的降低,虽然我们无法提供明确的证据来证明这是引起这种表现的唯一原因。但数据的字宽的确是影响程序的性能的。比如,使用一个线程,排序字节byte数据需要耗时122.5秒,然而排序long数据则需要耗时242.5秒。

4.4 任务同步

正如3.2章节所讨论的,任务窃取模型经常会在处理任务的同步上遇到问题,如果工作线程获取任务的时候,但相应的队列已经没有任务可供获取,这样就会产生竞争。在FJTask框架中,这种情况有时会导致线程强制睡眠。

从Jacobi程序中我们可以看到这类问题。Jacobi程序运行100步,每一步的操作,相应矩阵点周围的单元都会进行刷新。程序中有一个全局的屏障分隔。为了明确这种同步操作的影响大小。我们在一个程序中每10步操作进行一次同步。如图中表现出的扩展性的差异说明了这种并发策略的影响。也暗示着我们在这个框架后续的版本中应该增加额外的方法以供程序员来重写,以调整框架在不同的场景中达到最大的效率。(注意,这种图可能对同步因素的影响略有夸大,因为10步同步的版本很可能需要管理更多的任务局部性)

4.5 任务局部性

FJTask,或者说其他的Fork/Join框架在任务分配上都是做了优化的,尽可能多的使工作线程处理自己分解产生的任务。因为如果不这样做,程序的性能会受到影响,原因有二:

  • 从其他队列窃取任务的开销要比在自己队列执行pop操作的开销大。
  • 在大多数程序中,任务操作操作的是一个共享的数据单元,如果只运行自己部分的任务可以获得更好的局部数据访问。

如上图所示,在大多数程序中,窃取任务的相对数据都最多维持在很低的百分比。然后其中LU和MM程序随着线程数目的增加,会在工作负载上产生更大的不平衡性(相对的产生了更多的任务窃取)。通过调整算法我们可以降低这种影响以获得更好的加速比。

4.6 与其他框架比较

与其他不同语言的框架相比较,不太可能会得到什么明确的或者说有意义的比较结果。但是,通过这种方法,最起码可以知道FJTask在与其他语言(这里主要指的是C和C++)所编写的相近框架比较所表现的优势和限制。下面这个表格展示了几种相似框架(Cilk、Hood、Stackthreads以及Filaments)所测试的性能数据。涉及到的测试都是在4 CPU的Sun Enterprise 450服务器运行4个线程进行的。为了避免在不同的框架或者程序上进行重新配置,所有的测试程序运行的问题集都比上面的测试稍小些。得到的数据也是取三次测试中的最优值,以确保编译器或者说是运行时配置都提供了最好的性能。其中Fib程序没有指定任务粒度的阀值,也就是说默认的1。(这个设置在Filaments版的Fib程序中设置为1024,这样程序会表现的和其它版本更为一致)。

在加速比的测试中,不同框架在不同程序上所得到的测试结果非常接近,线程数目1 ~ 4,加速比表现在(3.0 ~ 4.0之间)。因此下图也就只聚焦在不同框架表现的不同的绝对性能上,然而因为在多线程方面,所有的框架都是非常快的,大多数的差异更多的是有代码本身的质量,编译器的不同,优化配置项或者设置参数造成的。实际应用中,根据实际需要选择不同的框架以弥补不同框架之间表现的巨大差异。

FJTask在处理浮点数组和矩阵的计算上性能表现的比较差。即使Java虚拟机性能不断的提升,但是相比于那些C和C++语言所使用的强大的后端优化器,其竞争力还是不够的。虽然在上面的图表中没有显示,但FJTask版本的所有程序都要比那些没有进行编译优化的框架还是运行的快的。以及一些非正式的测试也表明,测试所得的大多数差异都是由于数组边界检查,运行时义务造成的。这也是Java虚拟机以及编译器开发者一直以来关注并持续解决的问题。

相比较,计算敏感型程序因为编码质量所引起的性能差异却是很少的。

  1. 结论

本论文阐述了使用纯Java实现支持可移植的(portable)、高效率的(efficient)和可伸缩的(scalable)并行处理的可能性,并提供了便利的API让程序员可以遵循很少几个设计规则和模式(参考资料[7]中有提出和讨论)就可以利用好框架。从本文的示例程序中观察分析到的性能特性也同时为用户提供了进一步的指导,并提出了框架本身可以潜在改进的地方。

尽管所展示的可伸缩性结果针对的是单个JVM,但根据经验这些主要的发现在更一般的情况下应该仍然成立:

  • 尽管分代GC(generational GC)通常与并行协作得很好,但当垃圾生成速度很快而迫使GC很频繁时会阻碍程序的伸缩性。在这样的JVM上,这个底层原因看起来会导致为了GC导致停止线程的花费的时间大致与运行的线程数量成正比。因为运行的线程越多那么单位时间内生成的垃圾也就越多,开销的增加大致与线程数的平方。即使如此,只有在GC频度相对高时,才会对性能有明显的影响。当然,这个问题需要进一步的研究和开发并行GC算法。本文的结果也说明了,在多处理器JVM上提供优化选项(tuning options)和适应机制(adaptive
    mechanisms)以让内存可以按活跃CPU数目扩展是有必要的。
  • 大多数的伸缩性问题只有当运行的程序所用的CPU多于多数设备上可用CPU时,才会显现出来。FJTask(以及其它Fork/Join框架)在常见的2路、4路和8路的SMP机器上表现出接近理想情况加速效果。对于为stock multiprocessor设计的运行在多于16个CPU上的Fork/Join框架,本文可能是第一篇给出系统化报告结果的论文。在其它框架中这个结果中的模式是否仍然成立需要进一步的测量。
  • 应用程序的特征(包括内存局部性、任务局部性和全局同步的使用)常常比框架、JVM或是底层OS的特征对于伸缩性和绝对性能的影响更大。举个例子,在非正式的测试中可以看到,精心避免deques上同步操作(在3.1节中讨论过)对于生成任务相对少的程序(如LU)完全没有改善。然而,把任务管理上开销减至最小却可以拓宽框架及其相关设计和编程技巧的适用范围和效用。

除了对于框架做渐进性的改良,未来可以做的包括在框架上构建有用的应用(而不是Demo和测试)、在生产环境的应用负载下的后续评估、在不同的JVM上测量以及为搭载多处理器的集群的方便使用开发扩展。

  1. 致谢

本文的部分工作受到来自Sun实验室的合作研究资助的支持。感谢Sun实验室Java课题组的 Ole Agesen、Dave Detlefs、Christine Flood、Alex Garthwaite 和 Steve Heller 的建议、帮助和评论。David Holmes、Ole Agesen、Keith Randall、Kenjiro Taura 以及哪些我不知道名字的审校人员为本论文的草稿提供的有用的评论。Bill Pugh 指出了在3.1节讨论到的JVM的写后读的局限(read−after−write
limitations)。特别感谢 Dave Dice 抽出时间在30路企业机型上执行了测试。

  1. 参考文献

[1] Agesen, Ole, David Detlefs, and J. Eliot B. Moss. Garbage Collection and Local Variable Type−Precision and Liveness in Java Virtual Machines. In Proceedings of 1998 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 1998.
[2] Agesen, Ole, David Detlefs, Alex Garthwaite, Ross Knippel, Y.S. Ramakrishna, and Derek White. An Efficient Meta−lock for Implementing Ubiquitous Synchronization. In Proceedings of OOPSLA ’99, ACM, 1999.
[3] Arora, Nimar, Robert D. Blumofe,
and C. Greg Plaxton. Thread Scheduling for Multiprogrammed Multiprocessors. In Proceedings of the Tenth Annual ACM Symposium on Parallel Algorithms and Architectures (SPAA), Puerto Vallarta, Mexico, June 28 − July 2, 1998.
[4] Blumofe, Robert
D. and Dionisios Papadopoulos. Hood: A User−Level Threads Library for Multiprogrammed Multiprocessors. Technical Report, University of Texas at Austin, 1999.
[5] Frigo, Matteo, Charles Leiserson, and Keith Randall. The Implementation of the Cilk−5
Multithreaded Language. In Proceedings of 1998 ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI), 1998.
[6] Gosling, James, Bill Joy, and Guy Steele. The Java Language Specification, Addison−Wesley, 1996.
[7]
Lea, Doug. Concurrent Programming in Java, second edition, Addison−Wesley, 1999.
[8] Lowenthal, David K., Vincent W. Freeh, and Gregory R. Andrews. Efficient Fine−Grain Parallelism on Shared−Memory Machines. Concurrency−Practice and Experience,
10,3:157−173, 1998.
[9] Simpson, David, and F. Warren Burton. Space efficient execution of deterministic parallel programs. IEEE Transactions on Software Engineering, December, 1999.
[10] Taura, Kenjiro, Kunio Tabata, and Akinori Yonezawa.
“Stackthreads/MP: Integrating Futures into Calling Standards.” In Proceedings of ACM SIGPLAN Symposium on Principles & Practice of Parallel Programming (PPoPP), 1999.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Gotorch - 多机定时任务管理系统 前言 cron+

发表于 2017-11-28

前言

最近在学习 Go 语言,遵循着 “学一门语言最好的方式是使用它” 的理念,想着用 Go 来实现些什么,刚好工作中一直有一个比较让我烦恼的问题,于是用 Go 解决一下,即使不在生产环境使用,也可以作为 Go 语言学习的一种方式。

先介绍下问题:

组内有十来台机器,上面用 cron 分别定时执行着一些脚本和 shell 命令,一开始任务少的时候,大家都记得哪台机器执行着什么,随着时间推移,人员几经变动,任务也越来越多,再也没人能记得清哪些任务在哪些机器上执行了,排查和解决后台脚本的问题也越来越麻烦。

解决这个问题也不是没有办法:

  • 维护一个 wiki,一旦任务有变动就更新 wiki,但一旦忘记更新 wiki,任务就会变成孤儿,什么时候出了问题更不好查。
  • 布置一台机器,定时拉取各机器的 cron 配置文件,进行对比统计,再将结果汇总展示,但命令的写法各式各样,对比命令也是个没头脑的事。
  • 使用开源分布式任务调度任务,比较重型,而且一般要布置数据库、后台,比较麻烦。

除此之外,任务的修改也非常不方便,如果想给在 crontab 里修改某一项任务,还需要找运维操作。虽然解决这个问题也有办法,使用 crontab cronfile.txt 直接让 crontab 加载文件,但引入新的问题:任务文件加载的实时性不好控制。

为了解决以上问题,我结合 cron 和任务管理,每天下班后花一点时间,实现一个小功能,最后完成了 gotorch 的可用版。看着 GitHub 的 commit 统计,还挺有成就感的~

这里放上 GitHub 链接地址: GitHub-zhenbianshu-gotorch ,欢迎 star/fork/issue。

介绍一下特色功能:

  • cron+,秒级定时,使任务执行更加灵活;
  • 任务列表文件路径可以自定义,建议使用版本控制系统;
  • 内置日志和监控系统,方便各位同学任意扩展;
  • 平滑重加载配置文件,一旦配置文件有变动,在不影响正在执行的任务的前提下,平滑加载;
  • IP、最大执行数、任务类型配置,支持更灵活的任务配置;

下面说一下功能实现的技术要点:

文章欢迎转载,但请带上本文源地址:http://www.cnblogs.com/zhenbianshu/p/7905678.html,谢谢。


cron+

在实现类似 cron 的功能之前,我简单地看了一下 cron 的源码,源码在 busybox.net/downloads/ 可以下载,解压后文件在miscutils > crond.c。

cron 的实现设计得很巧妙的,大概如下:

数据结构:

  1. cron 拥有一个全局结构体 global ,保存着各个用户的任务列表;
  2. 每一个任务列表是一个结构体 CronFile, 保存着用户名和任务链表等;
  3. 每一个任务 CronLine 有 shell 命令、执行 pid、执行时间数组 cl_Time 等属性;
  4. 执行时间数组的最大长度根据 “分时日月周” 的最大值确定,将可执行时间点的值置为 true,例如 在每天的 3 点执行则 cl_Hrs[3]=true;

执行方式:

  1. cron是一个 while(true) 式的长循环,每次 sleep 到下一分钟的开始。
  2. cron 在每分钟的开始会依次遍历检查用户 cron 配置文件,将更新后的配置文件解析成任务存入全局结构体,同时它也定期检查配置文件是否被修改。
  3. 然后 cron 会将当前时间解析为 第 n 分/时/日/月/周,并判断 cal_Time[n] 全为 true 则执行任务。
  4. 执行任务时将 pid 写入防止重复执行;
  5. 后续 cron 还会进行一些异常检测和错误处理操作。

明白了 cron 的执行方式后,感觉每个时间单位都遍历任务进行判断于性能有损耗,而且我实现的是秒级执行,遍历判断的性能损耗更大,于是考虑优化成:

给每个任务设置一个 next_time 的时间戳,在一次执行后更新此时间戳,每个时间单位只需要判断 task.next_time == current_time。

后来由于 “秒分时日月周” 的日期格式进位不规则,代码太复杂,实现出来效率也不比原来好,终于放弃了这种想法。。采用了跟 cron 一样的执行思路。

此外,我添加了三种限制任务执行的方式:

  • IP:在服务启动时获取本地内网 IP,执行前校验是否在任务的 IP 列表中;
  • 任务类型:任务为 daemon 的,当任务没有正在执行时则中断判断直接启动;
  • 最大执行数:在每个任务上设置一个执行中任务的 pid 构成的 slice,每次执行前校验当前执行数。

而任务启动方式,则直接使用 goroutine 配合 exec 包,每次执行任务都启动一个新的 goroutine,保存 pid,同时进行错误处理。由于服务可能会在一秒内多次扫描任务,我给每个任务添加了一个进程上次执行时间戳的属性,待下次执行时对比,防止任务在一秒内多次扫描执行了多次。


守护进程

本服务是做成了一个类似 nginx 的服务,我将进程的 pid 保存在一个临时文件中,对进程操作时通过命令行给进程发送信号,只需要注意下异常情况下及时清理 pid 文件就好了。

这里说一下 Go 守护进程的创建方式:

由于 Go 程序在启动时 runtime 可能会创建多个线程(用于内存管理,垃圾回收,goroutine管理等),而 fork 与多线程环境并不能和谐共存,所以 Go 中没有 Unix 系统中的 fork 方法;于是启动守护进程我采用 exec 之后立即执行,即 fork and exec 的方式,而 Go 的 exec 包则支持这种方式。

在进程最开始时获取并判断进程 ppid 是否为1 (守护进程的父进程退出,进程会被“过继”给 init 进程,其进程号为1),在父进程的进程号不为1时,使用原进程的所有参数 fork and exec 一个跟自己相同的进程,关闭新进程与终端的联系,并退出原进程。

1
2
3
4
5
6
7
复制代码 `filePath, _ := filepath.Abs(os.Args[0]) // 获取服务的命令路径`
cmd := exec.Command(filePath, os.Args[1:]...) // 使用自身的命令路径、参数创建一个新的命令
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil // 关闭进程标准输入、标准输出、错误输出
cmd.Start() // 新进程执行
return // 父进程退出

信号量处理

将进程制作为守护进程之后,进程与外界的通信就只好依靠信号量了,Go 的 signal 包搭配 goroutine 可以方便地监听、处理信号量。同时我们使用 syscall 包内的 Kill 方法来向进程发送信号量。

我们监听 Kill 默认发送的信号量 SIGTERM,用来处理服务退出前的清理工作,另外我还使用了用户自定义信号量 SIGUSR2 用来作为终端通知服务重启的消息。

一个信号量从监听到捕捉再到处理的完整流程如下:

  1. 首先我们使用创建一个类型为 os.Sygnal 的无缓冲channel,来存放信号量。
  2. 使用 signal.Notify() 函数注册要监听的信号量,传入刚创建的 channel,在捕捉到信号量时接收信号量。
  3. 创建一个 goroutine,在 channel 中没有信号时 signal := <-channel 会阻塞。
  4. Go 程序一旦捕捉到正在监听的信号量,就会把信号量通过 channel 传递过来,此时 goroutine 便不会继续阻塞。
  5. 通过后面的代码处理对应的信号量。

对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码 `c := make(chan os.Signal)`
signal.Notify(c, syscall.SIGTERM, syscall.SIGUSR2)

// 开启一个goroutine异步处理信号
go func() {
s := <-c
if s == syscall.SIGTERM {
task.End()
logger.Debug("bootstrap", "action: end", "pid "+strconv.Itoa(os.Getpid()), "signal "+fmt.Sprintf("%d", s))
os.Exit(0)
} else if s == syscall.SIGUSR2 {
task.End()
bootStrap(true)
}
}()

小结

gotorch 的开发共花了三个月,每天半小时左右,1~3 个 commits,经历了三次大的重构,特别是在代码格式上改得比较频繁。 不过使用 Go 开发确实是挺舒心的,Go 的代码很简洁, gofmt 用着非常方便。另外 Go 的学习曲线也挺平滑,熟悉各个常用标准包后就能进行简单的开发了。 简单易学、高效快捷,难怪 Go 火热得这么快了。

关于本文有什么问题可以在下面留言交流,如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我,博客一直在更新,欢迎 关注 。

参考:

论fork()函数与Linux中的多线程编程

linux 信号量之SIGNAL详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

算法-排序算法思想及实现

发表于 2017-11-28

排序算法主要有:插入排序,选择排序,冒泡排序,希尔排序,归并排序,快速排序,堆排序。这里的排序指的是内部排序,也就是基于内存的排序,基于内存的排序是基于大O模型的,可以使用大O模型来衡量算法的性能
摘自我自己的博客园:www.cnblogs.com/myadmin/p/5… 中的部分排序算法。

插入排序

基本思想:每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。

1
2
3
4
复制代码原始:4 3 1 2
1) 3 4 1 2
2) 1 3 4 2
3) 1 2 3 4

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码/**
* 插入排序
*/
public static int[] insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int index = i;// index当前扫描到的元素下标
int temp = arr[i];
// 寻找插入的位置
while (index > 0 && temp < arr[index - 1]) {
arr[index] = arr[index - 1];
index--;
}
arr[index] = temp;
}
return arr;
}

选择排序

基本思想:从所有序列中先找到最小的,然后放到第一个位置。之后再看剩余元素中最小的,放到第二个位置……以此类推,就可以完成整个的排序工作了。可以很清楚的发现,选择排序是固定位置,找元素。相比于插入排序的固定元素找位置,是两种思维方式。

1
2
3
4
5
6
7
8
9
复制代码3 2 1 4 6 5

初始化索引位置为0
寻找最小值所在位置交换:1 2 3 4 6 5

初始化索引位置为1
寻找最小值所在位置交换:1 2 3 4 6 5

依次类推!

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* 选择排序
*/
public static int[] selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minVal = arr[i];
int index = i;
for (int j = i + 1; j < arr.length; j++) {// 找到最小元素
if (arr[j] < minVal) {
minVal = arr[j];
index = j;
}
}
arr[index] = arr[i];
arr[i] = minVal;
}
return arr;
}

冒泡排序

基本思想:原理是临近的数字两两进行比较,按照从小到大或者从大到小的顺序进行交换。
核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码/**
* 冒泡排序
*
* @param arr
* 输入的待排数组
* @return 返回排序号的数组
*/
public static int[] bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 1; j < arr.length; j++) {
if (arr[j - 1] > arr[j]) {
int temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}

希尔排序

基本思想:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 =1( < …<d2<d1),即所有记录放在同一组中进行直接插入排序为止。(下图来自百度图片)

核心代码:

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
复制代码/**
* 希尔排序
*
* @author sgl
*
*/
public class ShellSort {

public static int[] shellSort(int[] arr) {
int step = arr.length / 2;// 初始步长

while (1 <= step) {
for (int i = step; i < arr.length; i++) {
if (arr[i] < arr[i - step]) {
int temp = arr[i];
arr[i] = arr[i - step];
arr[i - step] = temp;
}
}
step = step / 2;
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+",");
}
System.out.println();
}

return arr;
}

归并排序

基本思想:将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列。
归并排序其实要做两件事:
(1)“分解”——将序列每次折半划分。
(2)“合并”——将划分后的序列段两两合并后排序。

核心代码:

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
复制代码public static int[] sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
sort(nums, low, mid);// 左边
sort(nums, mid + 1, high);// 右边
merge(nums, low, mid, high);// 左右归并
}
return nums;
}
public static void merge(int[] nums, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;// 左指针
int j = mid + 1;// 右指针
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (nums[i] < nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = nums[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = nums[j++];
}
// 把新数组中的数覆盖nums数组
for (int k2 = 0; k2 < temp.length; k2++) {
nums[k2 + low] = temp[k2];
}
}

}

快速排序

基本思想:快速排序采用的思想是分治思想。
快速排序是找出一个元素(理论上可以随便找一个)作为基准,然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。递归快速排序,将其他n-1个元素也调整到排序后的正确位置。最后每个元素都是在排序后的正 确位置,排序完成。所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

核心代码:

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
复制代码/**
* 快速排序
*
* @author sgl
*
*/
public class QuickSort {
static void quicksort(int n[], int left, int right) {
int dp;
if (left < right) {
dp = partition(n, left, right);
quicksort(n, left, dp - 1);
quicksort(n, dp + 1, right);
}
}

static int partition(int n[], int left, int right) {
int pivot = n[left];
while (left < right) {
while (left < right && n[right] >= pivot)
right--;
if (left < right)
n[left++] = n[right];
while (left < right && n[left] <= pivot)
left++;
if (left < right)
n[right--] = n[left];
}
n[left] = pivot;
return left;
}

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

sbc(六) Zuul GateWay 网关应用 前言 集成

发表于 2017-11-28

前言

看过之前SBC系列的小伙伴应该都可以搭建一个高可用、分布式的微服务了。 目前的结构图应该如下所示:

各个微服务之间都不存在单点,并且都注册于 Eureka ,基于此进行服务的注册于发现,再通过 Ribbon 进行服务调用,并具有客户端负载功能。

一切看起来都比较美好,但这里却忘了一个重要的细节:

当我们需要对外提供服务时怎么处理?

这当然也能实现,无非就是将我们具体的微服务地址加端口暴露出去即可。

那又如何来实现负载呢?

简单!可以通过 Nginx F5 之类的工具进行负载。

但是如果系统庞大,服务拆分的足够多那又有谁来维护这些路由关系呢?

当然这是运维的活,不过这时候运维可能就要发飙了!

并且还有一系列的问题:

  • 服务调用之间的一些鉴权、签名校验怎么做?
  • 由于服务端地址较多,客户端请求难以维护。

针对于这一些问题 SpringCloud 全家桶自然也有对应的解决方案: Zuul。
当我们系统整合 Zuul 网关之后架构图应该如下所示:

我们在所有的请求进来之前抽出一层网关应用,将服务提供的所有细节都进行了包装,这样所有的客户端都是和网关进行交互,简化了客户端开发。

同时具有如下功能:

  • Zuul 注册于 Eureka 并集成了 Ribbon 所以自然也是可以从注册中心获取到服务列表进行客户端负载。
  • 功能丰富的路由功能,解放运维。
  • 具有过滤器,所以鉴权、验签都可以集成。

基于此我们来看看之前的架构中如何集成 Zuul 。

集成 Zuul

为此我新建了一个项目 sbc-gateway-zuul 就是一个基础的 SpringBoot 结构。其中加入了 Zuul 的依赖:

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

由于需要将网关也注册到 Eureka 中,所以自然也需要:

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

紧接着配置一些项目基本信息:

1
2
3
4
5
6
7
8
复制代码# 项目配置  
spring.application.name=sbc-gateway-zuul
server.context-path=/
server.port=8383

# eureka地址
eureka.client.serviceUrl.defaultZone=http://node1:8888/eureka/
eureka.instance.prefer-ip-address=true

在启动类中加入开启 Zuul 的注解,一个网关应用就算是搭好了。

1
2
3
4
5
6
复制代码@SpringBootApplication

//开启zuul代理
@EnableZuulProxy
public class SbcGateWayZuulApplication {
}

启动 Eureka 和网关看到已经注册成功那就大功告成了:

服务路由

路由是网关的核心功能之一,可以使系统有一个统一的对外接口,下面来看看具体的应用。

传统路由

传统路由非常简单,和 Nginx 类似,由开发、运维人员来维护请求地址和对应服务的映射关系,类似于:

1
2
复制代码zuul.routes.user-service.path=/user-service/**  
zuul.routes.user-sercice.url=http://localhost:8080/

这样当我们访问 http://localhost:8383/user-service/getUserInfo/1 网关就会自动给我们路由到 http://localhost:8080/getUserInfo/1 上。

可见只要我们维护好这个映射关系即可自由的配置路由信息(user-sercice 可自定义),但是很明显这种方式不管是对运维还是开发都不友好。由于实际这种方式用的不多就再过多展开。

服务路由

对此 Zuul 提供了一种基于服务的路由方式。我们只需要维护请求地址与服务 ID 之间的映射关系即可,并且由于集成了 Ribbon , Zuul 还可以在路由的时候通过 Eureka 实现负载调用。

具体配置:

1
2
复制代码zuul.routes.sbc-user.path=/api/user/**  
zuul.routes.sbc-user.serviceId=sbc-user

这样当输入 http://localhost:8383/api/user/getUserInfo/1 时就会路由到注册到 Eureka 中服务 ID 为 sbc-user 的服务节点,如果有多节点就会按照 Ribbon 的负载算法路由到其中一台上。

以上配置还可以简写为:

1
2
复制代码# 服务路由 简化配置  
zuul.routes.sbc-user=/api/user/**

这样让我们访问 http://127.0.0.1:8383/api/user/userService/getUserByHystrix 时候就会根据负载算法帮我们路由到 sbc-user 应用上,如下图所示:

启动了两个 sbc-user 服务。
请求结果:

一次路由就算完成了。

在上面的配置中有看到 /api/user/** 这样的通配符配置,具体有以下三种配置需要了解:

  • ? 只能匹配任意的单个字符,如 /api/user/? 就只能匹配 /api/user/x /api/user/y /api/user/z 这样的路径。
  • * 只能匹配任意字符,如 /api/user/* 就只能匹配 /api/user/x /api/user/xy /api/user/xyz。
  • ** 可以匹配任意字符、任意层级。结合了以上两种通配符的特点,如 /api/user/** 则可以匹配 /api/user/x /api/user/x/y /api/user/x/y/zzz这样的路径,最简单粗暴!

谈到通配符匹配就不得不提到一个问题,如上面的 sbc-user 服务由于后期迭代更新,将 sbc-user 中的一部分逻辑抽成了另一个服务 sbc-user-pro。新应用的路由规则是 /api/user/pro/**,如果我们按照:

1
2
复制代码zuul.routes.sbc-user=/api/user/**  
zuul.routes.sbc-user-pro=/api/user/pro/**

进行配置的话,我们想通过 /api/user/pro/ 来访问 sbc-user-pro 应用,却由于满足第一个路由规则,所以会被 Zuul 路由到 sbc-user 这个应用上,这显然是不对的。该怎么解决这个问题呢?

翻看路由源码 org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator 中的 locateRoutes() 方法:

1
2
3
4
5
6
7
8
9
10
11
复制代码    /**
* Compute a map of path pattern to route. The default is just a static map from the
* {@link ZuulProperties}, but subclasses can add dynamic calculations.
*/
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}

发现路由规则是遍历配置文件并放入 LinkedHashMap 中,由于 LinkedHashMap 是有序的,所以为了达到上文的效果,配置文件的加载顺序非常重要,因此我们只需要将优先匹配的路由规则放前即可解决。

过滤器

过滤器可以说是整个 Zuul 最核心的功能,包括上文提到路由功能也是由过滤器来实现的。

摘抄官方的解释: Zuul 的核心就是一系列的过滤器,他能够在整个 HTTP 请求、响应过程中执行各样的操作。

其实总结下来就是四个特征:

  • 过滤类型
  • 过滤顺序
  • 执行条件
  • 具体实现

其实就是 ZuulFilter 接口中所定义的四个接口:

1
2
3
4
5
6
7
复制代码String filterType();

int filterOrder();

boolean shouldFilter();

Object run();

官方流程图(生命周期):

简单理解下就是:

当一个请求进来时,首先是进入 pre 过滤器,可以做一些鉴权,记录调试日志等操作。之后进入 routing 过滤器进行路由转发,转发可以使用 Apache HttpClient 或者是 Ribbon 。
post 过滤器呢则是处理服务响应之后的数据,可以进行一些包装来返回客户端。 error 则是在有异常发生时才会调用,相当于是全局异常拦截器。

自定义过滤器

接下来实现一个文初所提到的鉴权操作:

新建一个 RequestFilter 类继承与 ZuulFilter 接口

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
复制代码/**
* Function: 请求拦截
*
* @author crossoverJie
* Date: 2017/11/20 00:33
* @since JDK 1.8
*/
public class RequestFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(RequestFilter.class) ;
/**
* 请求路由之前被拦截 实现 pre 拦截器
* @return
*/
@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {

RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = request.getParameter("token");
if (StringUtil.isEmpty(token)){
logger.warn("need token");
//过滤请求
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(400);
return null ;
}
logger.info("token ={}",token) ;

return null;
}
}

非常 easy,就简单校验下请求中是否包含 token,不包含就返回 401 code。

不但如此,还需要将该类加入到 Spring 进行管理:

新建了 FilterConf 类:

1
2
3
4
5
6
7
8
9
复制代码@Configuration
@Component
public class FilterConf {

@Bean
public RequestFilter filter(){
return new RequestFilter() ;
}
}

这样重启之后就可以看到效果了:

不传 token 时:

传入 token 时:

可见一些鉴权操作是可以放到这里来进行统一处理的。

其余几个过滤器也是大同小异,可以根据实际场景来自定义。

Zuul 高可用

Zuul 现在既然作为了对外的第一入口,那肯定不能是单节点,对于 Zuul 的高可用有以下两种方式实现。

Eureka 高可用

第一种最容易想到和实现:
我们可以部署多个 Zuul 节点,并且都注册于 Eureka ,如下图:

这样虽然简单易维护,但是有一个严重的缺点:那就是客户端也得注册到 Eureka 上才能对 Zuul 的调用做到负载,这显然是不现实的。

所以下面这种做法更为常见。

基于 Nginx 高可用

在调用 Zuul 之前使用 Nginx 之类的负载均衡工具进行负载,这样 Zuul 既能注册到 Eureka ,客户端也能实现对 Zuul 的负载,如下图:

总结

这样在原有的微服务架构的基础上加上网关之后另整个系统更加完善了,从网关的设计来看:大多数系统架构都有分层的概念,不能解决问题那就多分几层🤓。

项目:github.com/crossoverJi…

博客:crossoverjie.top。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…928929930…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%