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

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


  • 首页

  • 归档

  • 搜索

大数据与云计算学习 Python网络数据采集 urllib

发表于 2017-11-29

本文将介绍网络数据采集的基本原理:

  • 如何用Python从网络服务器请求信息
  • 如何对服务器的响应进行基本处理
  • 如何以自动化手段与网站进行交互
  • 如何创建具有域名切换、信息收集以及信息存储功能的爬虫

urllib

这里我们先操练起来,写个测试爬虫

1
2
3
stylus复制代码from urllib.request import urlopen//查找python的urllib库的request模块,导出urlopen函数
html = urlopen("http://jxdxsw.com/")//urlopen用来打开并读取一个从网络获取的远程对象
print(html.read())

然后,把这段代码保存为`scrapetest.py`,终端中运行如下命令

1
vim复制代码python3 scrapetest.py

这里会输出http://jxdxsw/这个网页首页的全部HTML代码

鲸鱼注: 
Python 3.x中urllib分为子模块:
 - urllib.request

  • urllib.parse
  • urllib.error
  • urllib.robotparser

 urllib是python的标准库,它能够:

  • 从网络请求数据
  • 处理cookie
  • 改变 请求头和用户代理 等元数据的函数

更多查看python官方文档

BeatifulSoup

beatifulsoup非python标准库需要单独安装
安装使用详情
鲸鱼使用的是ubuntu所以一下几行命令即可

1
2
3
vim复制代码sudo apt-get install python-bs4
sudo apt-get install python3-pip //安装python包管理工具
pip3 install beautifulsoup4

使用BeautifulSoup解析这段代码,能够得到一个 BeautifulSoup 的对象,并能按照标准的缩进格式的结构输出:

1
2
3
4
5
6
7
8
9
10
11
stylus复制代码from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://jxdxsw.com/")
bsobj = BeautifulSoup(html.read())
print(bsobj.prettify())

print("-----------------------------我是分割线---------------------------")
print(bsobj.title)

print("-----------------------------我是分割线---------------------------")
print(bsobj.find_all('a'))

异常处理

1
abnf复制代码html = urlopen("http://jxdxsw.com/")

这行代码主要可能会发生两种异常:

  • 网页在服务器上不存在(或者获取页面的时候出现错误)
  • 服务器不存在

第一种异常会返回HTTP错误,如:”404 Page Not Found” “500 Internal Server Error”,所有类似情况, urlopen函数都会抛出“HTTPError”异常,遇到这种异常,我们可以这样处理:

1
2
3
4
5
6
7
8
python复制代码try:
html = urlopen("http://jxdxsw.com/")
except HTTPError as e:
print(e)
# 返回空值,中断程序,或者执行另一个方案
else:
# 程序继续。注意,如果你已经在上面异常捕获那段代码里返回或中断(break)
  #那就不需要使用else语句,这段代码也不会执行

第二种服务器不存在(就是说链接jxdxsw.com/打不开,或者url写错),urlopen 会返回一个None对象,这个对象与其他编程语言中的null类似

1
2
3
4
5
vim复制代码# 添加一个判断语句检测返回的html是不是None
if html is None:
print("URL is not found)
else:
#程序继续

参考

Python网络数据采集

本文转载自: 掘金

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

行人再识别中的迁移学习:图像风格转换(Learning vi

发表于 2017-11-29

论文:[1711.07027] Image-Image Domain Adaptation with Preserved Self-Similarity and Domain-Dissimilarity for Person Re-identification.

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

  1. 背景介绍

行人再识别(Person re-identification, re-ID)的核心目标是判断图像或者视频序列中是否存在特定行人。近年来,伴随着大数据集合(Market-1501【1】,DukeMTMC-reID 【2】等)的出现以及深度卷积神经网(ResNet【3】,GoogleNet【4】等)的发展,re-ID的性能不断攀升。那么,re-ID模型在跨数据集下的性能表现会是怎么样的?

由于不同数据集合之间的差异(dataset bias)【5】,在一个数据集合上训练的模型直接应用于另外一个数据集合上的时候,re-ID性能会出现大幅度的下降。《Image-Image Domain Adaptation with Preserved Self-Similarity and Domain-Dissimilarity for Person Re-identification》这篇论文探究re-ID模型在跨数据集合的性能表现,并构建了“Learning via Translation”的框架来进行不同数据集合之间的迁移学习。

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

2 . 方法概述

假设给定源域 S(source domain)上带标签的数据集合,以及目标域 T(target domain)上没有带标签的数据集合。迁移学习的目标是让在S域上训练的re-ID模型能很好地在T域上应用。为此,该论文提出“Learning via Translation”的框架。

图1 Learning via Translation框架流程图

图1为Learning via Translation框架流程图,该框架分为两个步骤:

  • Source-target Translation 首先,将S域上带标签的训练数据的风格迁移到T域的风格之上;
  • Feature Learning 其次,利用风格迁移后的训练数据,训练出一个re-ID模型。由于该模型是利用风格为T域上的数据训练的,所以能很好地在T域上使用。

该论文重点探究了第一个步骤,针对re-ID问题提出了Source-target Translation的方法:Similarity Preserving cycle-consistent Generative Adversarial Network (SPGAN)。由于迁移之后的图像是用于re-ID模型的训练,因此该论文提出了SPGAN。其的核心是:图像迁移前后能保持其ID信息不变。为了实现该目的,SPGAN构建了无监督的Self-Similarity和Domain-Dissimilarity关系来约束Source-target
Translation的学习。

下面将对SPGAN做一个详细介绍。

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

  1. SPGAN

3.1 图像风格化(Image-to-image translation)

图2 DukeMTMC-reID 和 Market-1501 图像示例

图2为分别来自于Duke和Market的图像示例,可以观察到两个数据集合有着很大的风格差异,具体体现在不同光照、背景、季节(duke拍摄于冬季、Market拍摄于夏季)等上。基于数据集合风格差异的观察,作者从图像风格迁移的角度来对re-ID的迁移学习进行探索:(1)、如果来自于源域S 的图像,风格迁移到目标域T 之后,迁移之后的图像的风格要和目标域的风格一致;

(2)、图像迁移前后需要保持它本身的ID信息不变。这种ID信息不是图像的背景或图像的风格,而是和ID信息有潜在关系的图像行人区域。(作者在文中是这样表述的: the visual content associated with the ID label of an image should be preserved after image-image translation. In our scenario, such visual content usually refers to the underlying (latent) ID information for a foreground pedestrian)

针对第一点,数据集合之间的Image-to-Image Translation,由于Duke和Market两个数据集合没有一一对应的标签信息(也就是来自一个数据的图像风格迁移到另外一个数据应该是什么样的图像对信息)。作者采用了CycleGAN【6】来实现Unpaired Image-to-Image Translation。(CycleGAN、DualGAN【7】、DiscoGAN【8】是
孪生三姐妹,同样采用cycle-consistent loss来约束两个数据分布之间转换关系的学习,这里不再赘述)

此外,作者还引入了 target domain identity loss【9】来进一步约束源域S 和目标域T 之间映射关系的学习 。该loss具体为:假定一个generator是从域S 映射到域T ,那么来自于域T 的样本通过该generator得到的还是该样本。公式1为target domain identity loss。作者实验发现,该loss能够使得迁移前后的图像在颜色组成上保持一致。

\mathcal{L}_{ide}(G, F, p_{x} , p_{y}) = \mathbb{E}_{x \sim p_{x}}{\Vert F(x) - x \Vert}_{1}+ \mathbb{E}_{y \sim p_{y}}{\Vert G(y) - y\Vert}_{1} (1)

3.2 SPGAN

针对第二点,基于re-ID问题,作者构建了无监督的Self-Similarity和Domain-Dissimilarity关系来约束Source-target Translation的学习。具体介绍如下:

  • Self-Similarity 一张图像迁移前后需要让ID相关的图像保持不变。那么,一张图迁移前和迁移后的特征距离需要越近越好。
  • Domain-Dissimilarity 针对re-ID的跨数据集合迁移问题,由于两个数据集合里面图像的ID是不一样的(也就是 source and target domains contain the different set of classes),那么一张图A从域S 迁移到域T 之后的图像G(A),要和域T 中的任意一张图像在特征距离上远离一些(同样,一张图B从域T 迁移到域S 之后的图像F(B),要和域S 中的任意一张图像在特征距离上远离一些)。图3给出了Self-Similarity和Domain-Dissimilarity关系的示意图。

图3 Self-Similarity和Domain-Dissimilarity关系示意图

基于上述两点,作者在CycleGAN的基础上嵌入了一个Siamese Network,Self-Similarity和Domain-Dissimilarity刚好可以采用contrastive loss【10】来进行训练。

公式2为contrastive loss, x_{1} 和  x_{2} 为输入的图像样本对, i 为输入图像对的标签, i 为1表示是输入的图像对是正例对, i 为0表示是输入的图像对是反例对, m 表示反例对之间距离margin, d 为两个输入样本的欧式距离:d(x_1, x_2) = \Vert{x_{1}-x_{2}\Vert}_{2} 。

L (i,x_{1},x_{2})=(1 - i){max(0, m-d)}^{2}+id (2)

值得注意的是,样本对的选取方式是无监督的。给定 G,将图像从源域S 映射到目标域T;给定F ,将图像从目标域T 映射到源域S ;有来自于目标域T 和源域S 的图像 x_\mathcal{S} 和 x_\mathcal{T} 。

正例对: x_\mathcal{S} 和 G(x_\mathcal{S} ) 、 x_\mathcal{T} 和 F(x_\mathcal{T} )

负例对: x_\mathcal{T} 和 G(x_\mathcal{S} ) 、 x_\mathcal{S} 和 F(x_\mathcal{T} )

每一次迭代的时候,都可以构造出上述的样本对,该样本对不需要额外的标记信息。SPGAN可以划分为三个部分:Discriminator、Generator、SiaNet,在训练的时候三个部分交替更新:更新D的时候固定G和S,更新G的时候固定D和S,更新S的时候固定D和G。

3.3 Feature Learning

Learning via Translation 框架的第二步是特征学习。将源域S 带标签的数据风格转换到目标T 之后,可以利用转换后的数据训练re-ID模型。作者的核心是第一个步骤,因此特征学习的方式,作者直接采用采用了IDE【11】来训练re-ID模型,IDE是基于ResNet-50修改的,只根据训练数据的类别数目修改了输出节点,其他的结构不变。训练好IDE之后,作者提取ResNet-50的Pool5特征来做图像的描述子,采用欧式距离进行检索。

此外作者提出了LMP (Local Max Pooling)来进一步提升re-ID模型在target dataset上性能的方式。LMP不需要训练,只需要在测试的时候直接使用就行。如图4所示,LMP先把ResNet-50的CONV5特征按overlap为一个像素的方式划分成P(图4中P为2)个部分,然后分别对P个部分做Global Max Pooling (GMP),最后concatenate P个pooling的结果作为最后的图像描述子。实验表明,P最大为6时性能最好,为此作者在实验时P都取6。(值得注意的是,当P=1且采用Global
Average Pooling的时候,得到的是ResNet-50的Pool5特征)

图 4 LMP示意图

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

  1. 实验探究

3.1 图像风格化结果

图5 不同模型图像风格相互迁移实例图

图5展示了不同模型在Marke和Duke图像之间风格相互迁移的效果图:(a)为输入图像、(b)为cyclegan模型的效果图、(c)为CycleGAN+L_{ide} 效果图、(d)为SPGAN的效果图。可以看出L_{ide} 能使得Cyclegan在图像迁移的时候,保持图像的色彩组成。SPGAN生成的图像和输入图像更加相似。

另外就是用SPGAN对Market图像和Duke图像风格相互迁移效果图,如图6所示。视觉感受上,能把Market和Duke的图像风格进行一个图像风格上的相互迁移。

图6 SPGAN对Market和Duke图像风格相互转换效果图

3.2 实验分析

下面介绍作者的定量实验。作者在Market-1501和DukeMTMC-reID两个数据集合上做了re-ID实验验证。

表1 Market和Duke跨域迁移性能对比表

从表1中,我们可以获取到很多信息,下面将相应阐述:

  1. 数据集合之间的dataset bias使得re-ID模型在跨数据集时的性能下降很剧烈 表1中,当Duke上训练的模型在Duke上测试的时候,rank-1 有66.7%,但是把Duke上训练的模型用在Market上测试的时候,性能只有43.1%。同样的情况也能从Market–>Duke上观察到;
  2. Learning via Translation方法的有效性 通过对比Direct transfer(在源域S 上训练的模型直接用于目标域T ),CycleGAN以及CycleGAN+L_{ide} 都有性能上的提升,这说明Learning via Translation的方式对于re-ID任务是有效的。此外,CycleGAN+
    L_{ide} 相比于CycleGAN,在Duke上性能基本持平而在Market上性能表现更好一些,原因可能是Duke–>Market的时候难度稍微大一些, L_{ide} 能帮助模型去实现图像风格的转换。
  3. SPGAN的有效性 相比于CycleGAN以及CycleGAN+L_{ide} ,SPGAN有者进一步的性能提升,这说明作者在训练CycleGAN的过程中加入Self-Similarity和Domain-Dissimilarity的有效性。另外作者还对公式3中的margin这个参数做了探究,发现m=2的时候性能会高一些,也就是要求负样本对距离要尽量远离。(当m=0的时候,相当于不考虑负样本对,这个时候loss退化到了content loss【13】,作者没有进一步探究)
  4. LMP能进一步提升re-ID性能 可以看出LMP能使训练好的模型在性能上进一步提升。(作者在文中也对比了全监督和迁移学习情况下,LMP的有效性,发现LMP只对迁移学习的情况有效)

更多实验请查看作者论文。

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

  1. 小结

  • re-ID中的迁移学习 由于数据集合间的差异,在一个数据集合上训练好的re-ID模型在另外一个数据性能上下降很厉害;其次,re-ID数据的标定很耗费人力物力,那么让在已有标记数据上训练好的模型能够用于其他场景符合实际的需求。迁移学习下的re-ID还是一个开放问题,期待更多工作对其进行相关研究;
  • Learning via Translation 作者针对re-ID的迁移学习,从图像风格转换角度构建了Learning via Translation的基础框架,并通过实验验证了该框架的有效性。此外,作者针对该框架的核心部分(第一步图像风格转换),提出了SPGAN,进一步提升框架的性能。那么,是否会存在其他re-ID迁移学习的解决方案呢?期待更多人回答。

扩展阅读,论文采用的图像风格转换是建立在两个数据集合上的,那么多个数据集合能不能同时进行转换呢?或许这个论文给出来答案:StarGAN【13】。

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

  1. 参考文献

【1】L. Zheng, L. Shen, L. Tian, S. Wang, J. Wang, and Q. Tian. Scalable person re-identification: A benchmark. In ICCV, 2015.

【2】Z. Zheng, L. Zheng, and Y. Yang. Unlabeled samples generated by gan improve the person re-identification baseline in vitro. In ICCV, 2017

【3】K. He, X. Zhang, S. Ren, and J. Sun. Deep residual learning for image recognition. In CVPR, 2016.

【4】C. Szegedy, W. Liu, Y. Jia, P. Sermanet, S. Reed, D. Anguelov, D. Erhan, V. Vanhoucke, and A. Rabinovich. Going deeper with convolutions. In CVPR, 2015.

【5】A. Torralba and A. A. Efros. Unbiased look at dataset bias. In CVPR, 2011

【6】J. Zhu, T. Park, P. Isola, and A. A. Efros. Unpaired imageto-image translation using cycle-consistent adversarial networks. ICCV, 2017.

【7】Dualgan: Unsupervised dual learning for image-to-image translation. In ICCV, 2017.

【8】T. Kim, M. Cha, H. Kim, J. K. Lee, and J. Kim. Learning to discover cross-domain relations with generative adversarial networks. In ICML, 2017.

【9】Y. Taigman, A. Polyak, and L. Wolf. Unsupervised crossdomain image generation. ICLR, 2016.

【10】R. Hadsell, S. Chopra, and Y. LeCun. Dimensionality reduction by learning an invariant mapping. In CVPR, 2006.

【11】L. Zheng, Y. Yang, and A. G. Hauptmann. Person reidentification: Past, present and future. arXiv preprint arXiv:1610.02984, 2016.

【12】L. A. Gatys, A. S. Ecker, and M. Bethge. Image style transfer using convolutional neural networks. In CVPR, 2016

【13】Choi, Yunjey and Choi, Minje and Kim, Munyoung and Ha, Jung-Woo and Kim, Sunghun and Choo, Jaegul. StarGAN: Unified Generative Adversarial Networks for Multi-Domain Image-to-Image Translation. arXiv preprint arXiv:1711.09020

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

  1. 更多re-ID相关文章

行人重识别:从哈利波特地图说起

从人脸识别 到 行人重识别,下一个风口

SVDNet for Pedestrian Retrieval:CNN到底认为哪个投影方向是重要的?

行人对齐+重识别网络:Pedestrian Alignment Network for Large-scale Person Re-identification

More on: 行人重识别

(还有一丢丢:第一次写知乎,有问题请各位指正!谢谢)


本文转载自: 掘金

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

浅谈NLP中条件语言模型(Conditioned Langu

发表于 2017-11-29

前言:条件语言模型(Conditioned LM,下文均用此词条表示条件语言模型)是在基于一般的语言模型s~P(x)(P为关于词库中单词的概率分布)上,进一步产生的条件概率分布,即s~P(Y|X),其中X为Y的上下文。众所周知一篇优美的文章在其句子的连贯性以及句子与句子之间的相互呼应上肯定注入了作者许多的想法和情感。本文以CMU CS 11-747( Neural Networks for NLP)课程中Conditioned Generation一节为主讲章节,第一部分介绍了如何利用Conditioned LM生成句子。比较了几种不同的生成方法及其优劣;第二部分介绍了模型的优化,主要叙述了一种名为Ensembling的模型集成方法;第三部分则是对于各种模型好坏的评估方法,主要包含了BLEU,METEOR等方法;最后一部分介绍了Conditioned LM的应用场景,如由结构化数据(Structured
Data)、输入序列+标签(Input+Label)、图片(Image)等生成句子。

本文作者:陈俊华,2017级研究生,目前研究方向为标签推荐、深度学习,来自中国人民大学大数据管理与分析方法研究北京市重点实验室。

一、公式化与建模化(Formulation and Modeling )

首先来回顾一下LM中一般计算一条句子可能性的公式:

图1-1 Calculating the Probability of a Sentence

在Conditioned LM中,在Context处多了一项X,表示已经生成的上一条句子。

图1-2 Conditional Language Models

在上几周的本知乎专栏中已经讲解了RNN+LSTM模型,最初的语言模型就是利用LSTM进行句子生成,这里不再赘述。

图1-3

对于Conditioned LM,NLP中有一套非常经典的框架,即Encoder-Decoder框架:

图1-4

如果你阅读过专栏前几期的关于LSTM的介绍,那么理解Encoder-Decoder框架也就相当轻松了,简要来说,Encoder层使用第一个LSTM将输入序列转换成一个向量表示,在Decoder层利用第二个LSTM对向量进行解析并生成对应的翻译序列。这也是机器翻译领域常用的经典框架。

二、生成句子的方法(Methods of Generation)

在图1-2中,我们粗略说明了如何生成一条句子的方式,即利用已经成的单词和已经生成的上一条句子来预测下一个应该生成的单词。这部分我们讲详细说明该公式的不同用法(即我们已经通过大规模预料得到了P(Y|X),X这里可以是句子,也可以是单词序列):

1.采样法(Ancesreal Sampling):

图2-1 Ancesreal Sampling

y_{0} 这里可以由我们进行自由输入,在没有生成句子终止符(这里设定终止符为)之前,在上述概率分布中随机采样一个单词作为输出。该方法优点在于不会冷落一些单词,做到大范围的覆盖;缺点就是可能会生成一些并不连贯或者与上文语义不连贯的句子。

2.贪心法(Greedy Search):

图2-2 Greedy Search

贪心法每次生成的都是是P概率最大的那个单词,保证了语句通顺,同时带来的问题就是容易产生一些相对出现频繁的单词或者简易词。

3.光束搜索(Beam Search):

图2-3 Beam Search

比起贪心法只取一个使P概率最大的单词,Beam Search每生成一个单词,都会随之产生一个代价值,这个代价值一旦超过了设定的阈值,那么就终止这条候选语句的产生。从图2-3可以看到,当最终生成终止符时,保留2个候选语句,此时再根据上下文来最终决定应该选择哪一条语句作为输出。

三、模型集成(Model Ensembling)

在前2个部分中,我们都是在已经默认P(Y|X)已经给定的情况下进行讨论的,但是,Conditioned LM最重要的部分也就是词语的概率分布表示,每一个模型都有优劣,那么能否将不同模型的优点都抽取出来进行融合集成呢?答案是肯定的。下图表示了LSTM1和LSTM2的分别预测值,集成后得到最终的预测值。

图3-1 Models Ensembling

集成的方法大致分为以下几种方法:

1.线性插值法(Linear Interpolation):

图3-2 Linear Interpolation

上图可以看到,每个模型我们都赋予了一个概率值,如果不考虑模型的重要程度,我们可以认为模型的取值概率为一个均匀分布(即进行M个模型的融合,那么取到每个模型的概率即为1/M)。上式可以看成对于一个单词的生成概率,是由M个模型共同决定的,也是加权平均的一种变形。

2.对数线性插值法(Log-linear Interpolation ):

图3-3 Log-linear Interpolation

上式和图3-2中的式子非常相似,不同的是将一个单词的概率对数化了,并将m的概率值变为一个插值系数函数 \lambda_{m} ,通常也设定为均匀分布函数。

观察,3-2和3-3,你也许会考虑一个问题,什么时候 y 应该用线性?什么时候用对数线性?其实,正是因为概率对数化,导致了一个模型如果模拟效果不好,那么在对数模型中,由于对数概率加法变化为概率乘法,导致整体的计算值将不可利用,而在线性模型中,即使一个模型的效果不好,由于加法的存在并不影响其他模型对于整体集成效果的作用。因此上述2个方法也就可以类比为逻辑与和逻辑或的关系。当你需要忽略不好的模型时,可选择线性模型,当你需要所有模型的共同作用时,可选择对数线性模型。

模型集成带了的其中一个问题就是参数数量的爆炸增长,一个方法是参数均化(Parameter Averaging),对所有的参数值取平均,免去复杂的更新过程。但这种效果带来的问题就是模型的精度会有所偏差,于是便有了模型压缩的想法。

3.集成精炼(Ensemble Distillation ):

Ensemble Distillation 的主要思想就是用一个简单的网络代替一个大型的复杂网络,做到丢失最少的精度。训练目标就是2个网络的交叉熵 L{kd} (2个网络的相似度)最小以及小型网络的负对数似然 L_{ll} (小型网络的模拟效果)最小:

因此目标函数约定为: L=(1-\alpha)L_{kd}+\alpha L_{ll} ,其中:

图3-4 Ensemble Distillation

s 是输入序列; t 是输出序列; y 是真实的输出序列; p 是小型网络的概率分布; q 是大型网络的概率分布。(以上资料参考CSDN博客Sequence-Level Knowledge Distillation)

4.堆叠(Stacking):

如果2种模型时非常迥异的,不同模型的参数间没有相似关系,那么进行集成的时候是否应该进行与上面不同的操作呢?Stacking讲述了在异质模型间的相互集成。

Stacking先从初始数据集训练出初级学习器,然后”生成”一个新数据集用于训练次级学习器,在这个新数据集中,初级学习期的输出被当做样例输入特征,而初始样本的标记仍被当做样例标记。Stacking的算法图如下:(Stacking算法参考自《机器学习》-周志华)

输入:训练集 D= { (x_{1},y_{1})...(x_{m},y_{m}) };

初级学习算法 \xi_{1},\xi_{2},...\xi_{m} ;

次级学习算法 \xi

过程:

1.for t = 1,2…,T do

  1. h_{t}=\xi_{t}(D) ;

3.end for

  1. D^{'}=\phi ;

5.for i = 1,2,….,m do

  1. for t=1,2…,T do
  1. z_{it}=h_{t}(x_{i}) ;
  1. end for;
  1. D^{'}=D^{'}\cup((z_{i1},...,z_{iT}),y_{i}) ;

10.end for

  1. h^{'}=\xi(D^{'})

输出: H(x)=h^{'}(h_{1}(x),...,h_{T}(x))

四、模型评估(Evaluation)

课程中主要对现在机器翻译中2种常用的评估方法做了介绍:

1.BLEU:用于分析候选译文和参考译文中n元组共同出现的程度。BLEU则按下式计算对应语句中语料库层面上的重合精度:

图4-1 重合精度

其中k标示了可能存在的n-grams序号 容易看出 CP_{n}(C,S) 是一个精确度度量,在语句较短时表现更好 。所以我们再引入一个惩罚因子BP(Brevity Penalty):

图4-2 惩罚因子

其中 l_{c} 表示候选译文 c_{i} 的长度, l_{s} 表示参考译文 s_{ij} 的有效长度(当存在多个参考译文时,选取和 l_{c} 最接近的长度) 本质上,BLEU是一个n-grams精确度的加权几何平均,按照下式计算:

图4-3 BLEU精度

n一般去1,2,3,4。而 w_{n} 一般对所有n取常值,即1/n。

2.METEOR:METEOR测度基于单精度的加权调和平均数和单字召回率,其目的是解决一些BLEU标准中固有的缺陷,METEOR也包括其他指标没有发现一些其他功能,如同义词匹配等。计算METEOR需要预先给定一组校准(alignment)m,而这一校准基于WordNet的同义词库,通过最小化对应语句中连续有序的块(chunks)ch来得出 则METEOR计算为对应最佳候选译文和参考译文之间的准确率和召回率的调和平均:

图4-4 METEOR精度

(有关BLEU以及METEOR的详细讲解请参考【NLP】机器翻译常用评价标准 (BLEU & METEOR),笔者上文叙述也均参考此博客)。

五、模型应用

Conditioned LM可应用与结构化数据输出(From Structured Data);输入标记转化(From Input + Labels);图片叙述(From Images)等。常见的还有语音识别(Speech Recognition),机器翻译(Machine Translation),文档总结(Summarization)。

本文转载自: 掘金

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

阿里巴巴正式开源其自研容器技术Pouch

发表于 2017-11-29

11 月 19 日上午,在中国开源年会现场,阿里巴巴正式开源了基于 Apache 2.0 协议的容器技术 Pouch。Pouch 是一款轻量级的容器技术,拥有快速高效、可移植性高、资源占用少等特性,主要帮助阿里更快的做到内部业务的交付,同时提高超大规模下数据中心的物理资源利用率。开源之后,Pouch 成为一项普惠技术,人人都可以在 GitHub 上获取,GitHub 项目地址:

github.com/alibaba/pou…

Pouch 的开源,是阿里看好容器技术的一个信号。时至今日,全球范围内,容器技术在大多数企业中落地,已然成为一种共识。如何做好容器的技术选型,如何让容器技术可控,相信是每一个企业必须考虑的问题。Pouch 无疑使得容器生态再添利器,在全球巨头垄断的容器开源生态中,为中国技术赢得了一块阵地。

Pouch 技术现状

此次开源 Pouch,相信行业中很多专家都会对阿里目前的容器技术感兴趣。到底阿里玩容器是一个侠之大者,还是后起之秀呢?以过去看未来,技术领域尤其如此,技术的沉淀与积累,大致可以看清一家公司的技术实力。

Pouch 演进

追溯 Pouch 的历史,我们会发现 Pouch 起源于 2011 年。当时,Linux 内核之上的 namespace、cgroup 等技术开始成熟,LXC 等工具也在同时期诞生不久。阿里巴巴作为一家技术公司,即基于 LXC 研发了容器技术 t4,并在当时以产品形态给集团内部提供服务。此举被视为阿里对容器技术的第一次探索,也为阿里的容器技术积淀了最初的经验。随着时间的推移,两年后,Docker 横空出世,其镜像技术层面,极大程度上解决了困扰行业多年的“软件封装”问题。镜像技术流行开来后,阿里没有理由不去融合这个给行业带来巨大价值的技术。于是,在
2015 年,t4 在自身容器技术的基础上,逐渐吸收社区中的 Docker 镜像技术,慢慢演变,打磨为 Pouch。

带有镜像创新的容器技术,似一阵飓风,所到之处,国内外无不叫好,阿里巴巴不外如是。2015 年末始,阿里巴巴集团内部在基础设施层面也在悄然发生变化。原因很多,其中最简单的一条,相信大家也不难理解,阿里巴巴体量的互联网公司,背后必定有巨大的数据中心在支撑,业务的爆炸式增长,必定导致基础设施需求的增长,也就造成基础设施成本的大幅提高。容器的轻量级与资源消耗低,加上镜像的快速分发,迅速让阿里巴巴下定决心,在容器技术领域加大投入,帮助数据中心全面升级。

阿里容器规模

经过两年多的投入,阿里容器技术 Pouch 已经在集团基础技术中,扮演着极其重要的角色。2017 年双 11,巨额交易 1682 亿背后,Pouch 在“超级工程”中做到了:

  • 100% 的在线业务 Pouch 化
  • 容器规模达到百万级

回到阿里集团内部,Pouch 的日常服务已经覆盖绝大部分的事业部,覆盖的业务场景包括:电商、广告、搜索等;覆盖技术栈包括:电商应用、数据库、大数据、流计算等;覆盖编程语言:Java、C++、NodeJS 等。

Pouch 技术优势

阿里巴巴容器技术如此之广的应用范围,对行业来说实属一大幸事,因为阿里已经用事实说明:容器技术已经在大规模生产环境下得到验证。然而,由于 Pouch 源自阿里,而非社区,因此在容器效果、技术实现等方面,两套体系存在差异。换言之,Pouch 存在不少独有的技术优势。

隔离性强

隔离性是企业走云化之路过程中,无法回避的一个技术难题。隔离性强,意味着技术具备了商用的初步条件;反之则几乎没有可能在业务线上铺开。哪怕是阿里巴巴这样的技术公司,实践容器技术伊始,安全问题都无法幸免。众所周知,行业中的容器方案大多基于 Linux 内核提供的 cgroup 和 namespace 来实现隔离,然后这样的轻量级方案存在弊端:

  • 容器间,容器与宿主间,共享同一个内核;
  • 内核实现的隔离资源,维度不足。

面对如此的内核现状,阿里巴巴采取了三个方面的工作,来解决容器的安全问题:

  • 用户态增强容器的隔离维度,比如网络带宽、磁盘使用量等;
  • 给内核提交 patch,修复容器的资源可见性问题,cgroup 方面的 bug;
  • 实现基于 Hypervisor 的容器,通过创建新内核来实现容器隔离。

容器安全的研究,在行业中将会持续相当长时间。而阿里在开源 Pouch 中,将在原有的安全基础上,继续融合 lxcfs 等特性与社区共享。同时阿里巴巴也在计划开源“阿里内核”,将多年来阿里对 Linux 内核的增强回馈行业。

P2P 镜像分发

随着阿里业务爆炸式增长,以及 2015 年之后容器技术的迅速普及,阿里容器镜像的分发也同时成为亟待解决的问题。虽然,容器镜像已经帮助企业在应用文件复用等方面,相较传统方法做了很多优化,但是在数以万计的集群规模下,分发效率依然令人抓狂。举一个简单例子:如果数据中心中有 10000 台物理节点,每个节点同时向镜像仓库发起镜像下载,镜像仓库所在机器的网络压力,CPU 压力可想而知。

基于以上场景,阿里巴巴镜像分发工具“蜻蜓”应运而生。蜻蜓是基于智能 P2P 技术的通用文件分发系统。解决了大规模文件分发场景下分发耗时、成功率低、带宽浪费等难题。大幅提升发布部署、数据预热、大规模容器镜像分发等业务能力。目前,“蜻蜓”和 Pouch 同时开源,项目地址为:

github.com/alibaba/dra…

Pouch 与蜻蜓的使用架构图如下:

富容器技术

阿里巴巴集团内部囊括了各式各样的业务场景,几乎每种场景都对 Pouch 有着自己的要求。如果使用外界“单容器单进程”的方案,在业务部门推行容器化存在令人难以置信的阻力。阿里巴巴内部,基础技术起着巨大的支撑作用,需要每时每刻都更好的支撑业务的运行。当业务运行时,技术几乎很难做到让业务去做改变,反过来适配自己。因此,一种对应用开发、应用运维都没有侵入性的容器技术,才有可能大规模的迅速铺开。否则的话,容器化过程中,一方面得不到业务方的支持,另一方面也需要投入大量人力帮助业务方,非标准化的实现业务运维。

阿里深谙此道,内部的 Pouch 技术可以说对业务没有任何的侵入性,也正是因为这一点在集团内部做到 100% 容器化。这样的容器技术,被无数阿里人称为“富容器”。

“富容器”技术的实现,主要是为了在 Linux 内核上创建一个与虚拟机体验完全一致的容器。如此一来,比一般容器要功能强大,内部有完整的 init 进程,以及业务应用需要的任何服务,当然这也印证了 Pouch 为什么可以做到对应用没有“侵入性”。技术的实现过程中,Pouch 需要将容器的执行入口定义为 systemd,而在内核态,Pouch 引入了 cgroup namespace 这一最新的内核 patch,满足 systemd 在富容器模式的隔离性。从企业运维流程来看,富容器同样优势明显。它可以在应用的
Entrypoint 启动之前做一些事情,比如统一要做一些安全相关的事情,运维相关的 agent 拉起。这些需要统一做的事情,倘若放到用户的启动脚本,或镜像中就对用户的应用诞生了侵入性,而富容器可以透明的处理掉这些事情。

内核兼容性

容器技术的井喷式发展,使得不少走在技术前沿的企业享受到技术红利。然后,“长尾效应”也注定技术演进存在漫长周期。Pouch 的发展也在规模化进程中遇到相同问题。

但凡规模达到一定量,“摩尔定律”决定了数据中心会存有遗留资源,如何利用与处理这些物理资源,是一个大问题。阿里集团内部也是如此,不管是不同型号的机器,还是从 2.6.32 到 3.10+ 的 Linux 内核,异构现象依然存在。倘若要使所有应用运行 Pouch 之中,Pouch 就必须支持所有内核版本,而现有的容器技术支持的 Linux 内核都在 3.10 以上。不过技术层面万幸的是,对 2.6.32 等老版本内核而言,namespace 的支持仅仅缺失 user namespace;其他
namespace 以及常用的 cgroup 子系统均存在;但是 /proc/self/ns 等用来记录 namespace 的辅助文件当时还不存在,setns 等系统调用也需要在高版本内核中才能支持。而阿里的技术策略是,通过一些其他的方法,来绕过某些系统调用,实现老版本内核的容器支持。

当然,从另一个角度而言,富容器技术也很大程度上,对老版本内核上的其他运维系统、监控系统、用户使用习惯等实现了适配,保障 Pouch 在内核兼容性方面的高可用性。

因此综合来看,在 Pouch 的技术优势之上,我们不难发现适用 Pouch 的应用场景:传统 IT 架构的的迅速容器化,企业大规模业务的部署,安全隔离要求高稳定性要求高的金融场景等。

Pouch 架构

凭借差异化的技术优势,Pouch 在阿里巴巴大规模的应用场景下已经得到很好的验证。然而,不得不说的是:目前阿里巴巴内部的 Pouch 与当前开源版本依然存在一定的差异。

虽然优势明显,但是如果把内部的 Pouch 直接开源,这几乎是一件不可能的事。多年的发展,内部 Pouch 在服务业务的同时,存在与阿里内部基础设施、业务场景耦合的情况。耦合的内容,对于行业来说通用性并不强,同时涉及一些其他问题。因此,Pouch 开源过程中,第一要务即解耦内部依赖,把最核心的、对社区同样有巨大价值的部分开源出来。同时,阿里希望在开源的最开始,即与社区站在一起,共建 Pouch 的开源社区。随后,以开源版本的 Pouch 逐渐替换阿里巴巴集团内部的 Pouch,最终达成 Pouch
内外一致的目标。当然,在这过程中,内部 Pouch 的解耦工作,以及插件化演进工作同样重要。而在 Pouch 的开源计划中,明年 3 月底会是一个重要的时间点,彼时 Pouch 的 1.0 版本将发布。

从计划开源的第一刻开始,Pouch 在生态中的架构图就设计如下:

Pouch 的生态架构可以从两个方面来看:第一,如何对接容器编排系统;第二,如何加强容器运行时。

容器编排系统的支持,是 Pouch 开源计划的重要板块。因此,设计之初,Pouch 就希望自身可以原生支持 Kubernetes 等编排系统。为实现这点,Pouch 在行业中率先支持 container 1.0.0。目前行业中的容器方案 containerd 主要停留在 0.2.3 版本,新版本的安全等功能还无法使用,而 Pouch 已经是第一个吃螃蟹的人。当前 Docker 依然是 Kubernetes 体系中较火的容器引擎方案,而 Kubernetes 在 runtime 层面的战略计划为使用
cri-containerd 降低自身与商业产品的耦合度,而走兼容社区方案的道路,比如 cri-containerd 以及 containerd 社区版。另外,需要额外提及的是,内部的 Pouch 是阿里巴巴调度系统 Sigma 的重要组成部分,同时支撑着“混部”工程的实现。Pouch 开源路线中,同样会以支持“混部”为目标。未来,Sigma 的调度(scheduling)以及混部(co-location)能力有望服务行业。

生态方面,Pouch 立足开放;容器运行时方面,Pouch 主张“丰富”与“安全”。runC 的支持,可谓顺其自然。runV 的支持,则表现出了和生态的差异性。虽然 docker 默认支持 runV,然而在 docker 的 API 中并非做到对“容器”与“虚拟机”的兼容,从而 Docker 并非是一个统一的管理入口。而据我们所知,现有企业中仍有众多存量虚拟机的场景,因此,在迎接容器时代时,如何通过统一的运维入口,同时管理容器和虚拟机,势必会是“虚拟机迈向容器”这个变迁过渡期中,企业最为关心的方案之一。Pouch
的开源形态,很好的覆盖了这一场景。runlxc 是阿里巴巴自研的 lxc 容器运行时,Pouch 对其的支持同时也意味着 runlxc 会在不久后开源,覆盖企业内部拥有大量低版本 Linux 内核的场景。

Pouch 对接生态的架构如下,而 Pouch 内部自身的架构可参考下图:

和传统的容器引擎方案相似,Pouch 也呈现出 C/S 的软件架构。命令行 CLI 层面,可以同时支持 Pouch CLI 以及 Docker CLI。对接容器 runtime,Pouch 内部通过 container client 通过 gRPC 调用 containerd。Pouch Daemon 的内部采取组件化的设计理念,抽离出相应的 System Manager、Container Manager、Image Manager、Network Manager、Volume Manager
提供统一化的对象管理方案。

写在最后

如今 Pouch 的开源,意味着阿里积累的容器技术将走出阿里,面向行业。而 Pouch 的技术优势,决定了其自身会以一个差异化的容器解决方案,供用户选择。企业在走云化之路,拥抱云原生(Cloud Native)时,Pouch 致力于成为一款强有力的软件,帮助企业的数字化转型做到最稳定的支持。

Pouch 目前已经在 GitHub 上开源,欢迎任何形式的开源参与。GitHub 地址为:

github.com/alibaba/pou…

作者介绍

孙宏亮,阿里巴巴技术专家,毕业于浙江大学,目前在阿里巴巴负责容器项目 Pouch 的开源建设。数年来一直从事云计算领域,是国内第一批研究和实践容器技术的工程师,在国内起到极为重要的容器技术布道作用。拥有著作《Docker 源码分析》,个人崇尚开源精神,同时是 Docker Swarm 项目的全球 Maintainer。

感谢郭蕾对本文的审校。

本文转载自: 掘金

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

有趣的二进制3—浮点数

发表于 2017-11-29

关于浮点数很多人都知道计算机会丢失精度的问题,那么是精度如何丢失的,为何要引入IEEE 754规范,以及非规范化浮点数有何用途?深究这些问题你会发现很难回答上来,这篇做个回顾,方便你更快的梳理这些关键知识点。

本篇是二进制系列第三篇,如若你有兴趣,请持续关注,后期会持续更新。其他文章列表如下:

有趣的二进制

有趣的二进制—高效位运算

一、 精度

如果你有看过《有趣的二进制》这篇文章,你就会明白进制(不局限于二进制)中的小数是如何表示。因为每种进制都有其局限性,也就是约数的问题,比如

  • 同样是1/3,在三进制下正好分干净,但在十进制下就总也分不完。十进制只能近似的表示1/3而无法精确的表示1/3。
  • 同样是0.1,在十进制下可以精确的表示为0.1,而在二进制下只能近似的表示0.00011001100110011…(循环0011)

计算机中,存储数据的方式是采用二进制,因此在有限的存储空间下,绝大部分的十进制小数都不能用二进制浮点数来精确表示。一般情况下,你输入的十进制浮点数仅由实际存储在计算机中的近似的二进制浮点数表示。不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。

需要说明的是,虽然目前计算机表示的精度多数都是近似值,但多数情况下都够用了,如果你对精度有更高要求,每一种语言都有自己实现和处理方式。特别要注意的是防止上溢和下溢现象的发生。

二 、 IEEE 754 标准

1、IEEE 754

IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。主要规范如下:

这个公式中:

  1. s(Sign):符号,表示是正数还是负数。0正数,1负数
  2. E(Exponent):指数域,E是2的指数,类似于科学计数法中(a×10^n)中的n,如此E是负数就可以表示小数了。不过这里E是一个无符号整数,32位的时候,E的占用8位,取值范围0-255,存储E的时候我们称为e,它是E的值与一个固定值(32位的情况是127)的和(e=E+bias,E=e-bias,bias=127)。采用这种方式表示是因为,指数的值可能为正也可能为负,如果采用补码表示的话,符号位S和Exp自身的符号位将导致不能简单的进行大小比较。
  3. M(Mantissa):尾数域,浮点数具体的数值. 1≤M<2,隐含的以1开头表示,M表示的二进制表达式为1.xxx…,第一位总是1,因此IEEE 754规定,保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。

2、示例

比如十进制的3.25,我们分为以下几步进行转换。

1、转换二进制

3.25 这整数部分3转换为二进制是11,小数部分0.25 转换为二进制为2^-2,也可以按照乘以 2 取整数位的方法:

1
2
3
4
> 复制代码(1) 0.25 x 2 = 0.5  取整数位 0 得 0.0
> (2) 0.5 x 2 = 1 取整数位 1 得 0.01
>
>

如此3.25转化为二进制为11.01

2、有效数(Mantissa)

上述规则我们知道,尾数部分

最高有效位(即整数字)是1,也就是尾数有一位隐含的二进制有效数字。因此我们转换尾数的时候,始终保持最高位为1(小数点左边),通过二进制科学计数法转换,我们得到11.01=1.101*2^1。

3、IEEE 754 规约形式

通过上述2步,得到1.101*2^1。我们采用规约形式获取填充3个部分

s(Sign):正数=0;

E(Exponent):E=1,e=E+bias=1+127=128。此处存储的e,是经过以127作为偏移量调整的。

M(Mantissa):1.101 中,我只获取有效数(舍去整数部分1,只取小数部分)101

因此存在计算机中的3.25 浮点数是:

3、特殊值

从wiki上可以看到依据指数是否为0 ,还可以分为一下几种情况

指数不全为0或者不全为1。此时为规约形式,如上述的示例。尾数部分,默认整数部分是1,不存储,获取后第一位默认为1。指数部分有偏移量

E全为0 。此时为非规约形式。为何要引入非规则浮点数,当小于)的数会下溢为0,对于高精度来说这是不能接受的,而引入不规则浮点数后,小于的数才会下溢为0 。

E全为1 。尾数为0,则表示无穷大。非零则表示NaN(浮点数排序这种特殊问题需要处理)

4、运算方式

一般浮点数的运算流程如下,非规则浮点计算加法时“对阶”计算有不同,不再细说。

  1. 指数项对齐。指数项对齐,小的向大的对齐,如果判断大小,则上述指数中的偏移量就起很大作用了,指数大的必然大,后续可以减少判断
  2. 尾数求和。对齐后,对尾数进行加减处理
  3. 规则化。对尾数进行截取,保证精度
  4. 舍入。判断丢失的数值,进行舍入
  5. 判断结果。判断结果是否溢出

三、非规范化浮点数

1、为何要引入非规范化浮点数

引入一个精度失准的事故:

On 25 February 1991, a loss of significance in a MIM-104 Patriot missile battery prevented it intercepting an incoming Scud missile in Dhahran, Saudi Arabia, contributing to the death of 28 soldiers from the U.S. Army’s 14th Quartermaster Detachment.[25] See also: Failure at Dhahran

1991年2月25日,在MIM-104爱国者导弹电池中,一个重要的精准丢失阻止了它在沙特阿拉伯达哈兰拦截一架新的飞毛腿导弹,造成美军第十四军区分离队28名士兵死亡。

对于规则浮点数而言,指数项范围为01-FE(1到254),当小于)的浮点数,用规格化数值表示,运算的时候会被电脑当作0来处理,如果精度能够再次提高一些的话,就不会出现这种情况了,因此引入不规则浮点数后,小于的数才会下溢为0 。

采用非规约浮点数,用来解决填补绝对值意义下最小规格数与零的距离,如上图所示,仅仅用规则浮点数的表示方式,0到最小正常数之间的间隔要远远大于最小正常数到次小正常数之间的间隔(2^-126 * 2^-23 = 2^-149),可以说是非常突然的下溢出到0,这是不满足我们的期望的。因此选择约定小数点前一位可以为0,剩下的一小段区间(即黄色括号)再均匀划分为)段,如此就多了2^23精度,可以精确到附近。

2、引入非规范化浮点数带来的问题

在《你应该知道的浮点数基础知识》引入一个算法题,很好了诠释导致计算速率方面的问题。我简单贴一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码 const float x=1.1;
const float z=1.123;
float y=x;
// 算法1
for(int j=0;j<90000000;j++)
{
y*=x;
y/=z;
y+=0.1f;
y-=0.1f;
}

// 算法2
for(int j=0;j<90000000;j++)
{
y*=x;
y/=z;
y+=0;
y-=0;
}

算法2中在进行上百次循环以后,y被标识为非规格化浮点,最终导致的结果是算法2比算法1慢了整整7倍左右。

这就是非规则浮点数的计算速度慢于规则浮点数,虽然下溢下沉了,但需要CPU额外进行解码和编码标识,如此,效率缓慢,极端情况下,规格化浮点数操作可能比硬件支持的非规格化浮点数操作快100倍。

另外非规则浮点数无法解决计算过程中下溢的产生,因为只是精度精确到附近,当有更小的浮点数时候,依然会下溢为0。

本文转载自: 掘金

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

深入理解JVM类加载机制

发表于 2017-11-29

简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

下面我们具体来看类加载的过程:

类的生命周期

类的生命周期

类从被加载到内存中开始,到卸载出内存,经历了加载、连接、初始化、使用四个阶段,其中连接又包含了验证、准备、解析三个步骤。这些步骤总体上是按照图中顺序进行的,但是Java语言本身支持运行时绑定,所以解析阶段也可以是在初始化之后进行的。以上顺序都只是说开始的顺序,实际过程中是交叉进行的,加载过程中可能就已经开始验证了。

类加载的时机

首先要知道什么时候类需要被加载,Java虚拟机规范并没有约束这一点,但是却规定了类必须进行初始化的5种情况,很显然加载、验证、准备得在初始化之前,下面具体来说说这5种情况:

类加载时机

类加载时机

其中情况1中的4条字节码指令在Java里最常见的场景是:
1 . new一个对象时
2 . set或者get一个类的静态字段(除去那种被final修饰放入常量池的静态字段)
3 . 调用一个类的静态方法

类加载的过程

下面我们一步一步分析类加载的每个过程

1. 加载

加载是整个类加载过程的第一步,如果需要创建类或者接口,就需要现在Java虚拟机方法区创建于虚拟机实现规定相匹配的内部表示。一般来说类的创建是由另一个类或者接口触发的,它通过自己的运行时常量池引用到了需要创建的类,也可能是由于调用了Java核心类库中的某些方法,譬如反射等。

一般来说加载分为以下几步:

  1. 通过一个类的全限定名获取此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

创建名字为C的类,如果C不是数组类型,那么它就可以通过类加载器加载C的二进制表示(即Class文件)。如果是数组,则是通过Java虚拟机创建,虚拟机递归地采用上面提到的加载过程不断加载数组的组件。

Java虚拟机支持两种类加载器:

  • 引导类加载器(Bootstrap ClassLoader)
  • 用户自定义类加载器(User-Defined Class Loader)

用户自定义的类加载器应该是抽象类ClassLoader的某个子类的实例。应用程序使用用户自定义的类加载器是为了扩展Java虚拟机的功能,支持动态加载并创建类。比如,在加载的第一个步骤中,获取二进制字节流,通过自定义类加载器,我们可以从网络下载、动态产生或者从一个加密文件中提取类的信息。

关于类加载器,会新开一篇文章描述。

2.验证

验证作为链接的第一步,用于确保类或接口的二进制表示结构上是正确的,从而确保字节流包含的信息对虚拟机来说是安全的。Java虚拟机规范中关于验证阶段的规则也是在不断增加的,但大体上会完成下面4个验证动作。

验证

验证

1 . 文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。
主要验证点:

  • 是否以魔数0xCAFEBABE开头
  • 主次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量是否有不被支持的类型 (检查常量tag标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
  • Class文件中各个部分及文件本身是否有被删除的或者附加的其他信息
    …
    实际上验证的不仅仅是这些,关于Class文件格式可以参考我的深入理解JVM类文件格式,这阶段的验证是基于二进制字节流的,只有通过文件格式验证后,字节流才会进入内存的方法区中进行存储。

2 . 元数据验证:主要对字节码描述的信息进行语义分析,以保证其提供的信息符合Java语言规范的要求。
主要验证点:

  • 该类是否有父类(只有Object对象没有父类,其余都有)
  • 该类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,出现不符合规则的方法重载,例如方法参数都一致,但是返回值类型却不同)
    …

3 . 字节码验证:主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,字节码验证将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
主要有:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似的情况:操作数栈里的一个int数据,但是使用时却当做long类型加载到本地变量中
  • 保证跳转不会跳到方法体以外的字节码指令上
  • 保证方法体内的类型转换是合法的。例如子类赋值给父类是合法的,但是父类赋值给子类或者其它毫无继承关系的类型,则是不合法的。
  1. 符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段解析阶段发生。符号引用是对类自身以外(常量池中的各种符号引用)的信息进行匹配校验。
    通常有:
  • 符号引用中通过字符串描述的全限定名是否找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、方法、字段的访问性(private,public,protected、default)是否可被当前类访问
    符号引用验证的目的是确保解析动作能够正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

验证阶段非常重要,但不一定必要,如果所有代码极影被反复使用和验证过,那么可以通过虚拟机参数-Xverify: none来关闭验证,加速类加载时间。

3.准备

准备阶段的任务是为类或者接口的静态字段分配空间,并且默认初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令,在初始化阶段才会显示的初始化这些字段,所以准备阶段不会做这些事情。假设有:

1
复制代码public static int value = 123;

value在准备阶段的初始值为0而不是123,只有到了初始化阶段,value才会为0。
下面看一下Java中所有基础类型的零值:

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

一种特殊情况是,如果字段属性表中包含ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性所指定的值,比如上面的value如果这样定义:

1
复制代码public static final int value = 123;

编译时,value一开始就指向ConstantValue,所以准备期间value的值就已经是123了。

4.解析

解析阶段是把常量池内的符号引用替换成直接引用的过程,符号引用就是Class文件中的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量。下面我们看符号引用和直接引用的定义。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中。

直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了。

以下Java虚拟机指令会将符号引用指向运行时常量池,执行任意一条指令都需要对它的符号引用进行解析:

引起解析的命令

引起解析的命令

对同一个符号进行多次解析请求是很常见的,除了invokedynamic指令以外,虚拟机基本都会对第一次解析的结果进行缓存,后面再遇到时,直接引用,从而避免解析动作重复。

对于invokedynamic指令,上面规则不成立。当遇到前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic指令同样生效。这是由invokedynamic指令的语义决定的,它本来就是用于动态语言支持的,也就是必须等到程序实际运行这条指令的时候,解析动作才会执行。其它的命令都是“静态”的,可以再刚刚完成记载阶段,还没有开始执行代码时就解析。

下面来看几种基本的解析:
类与接口的解析: 假设Java虚拟机在类D的方法体中引用了类N或者接口C,那么会执行下面步骤:

  1. 如果C不是数组类型,D的定义类加载器被用来创建类N或者接口C。加载过程中出现任何异常,可以被认为是类和接口解析失败。
  2. 如果C是数组类型,并且它的元素类型是引用类型。那么表示元素类型的类或接口的符号引用会通过递归调用来解析。
  3. 检查C的访问权限,如果D对C没有访问权限,则会抛出java.lang.IllegalAccessError异常。

字段解析:
要解析一个未被解析过的字段符号引用,首先会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,这边记不清的可以继续回顾深入理解JVM类文件格式,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段解析失败。如果解析完成,那将这个字段所属的类或者接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。

1 . 如果C本身包含了简单名称和字段描述符都与目标相匹配的字段,则直接返回这个字段的直接引用,查找结束。
2 . 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
3 . 再不然,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在类中包含
了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
4 . 如果都没有,查找失败退出,抛出java.lang.NoSuchFieldError异常。如果返回了引用,还需要检查访问权限,如果没有访问权限,则会抛出java.lang.IllegalAccessError异常。

在实际的实现中,要求可能更严格,如果同一字段名在C的父类和接口中同时出现,编译器可能拒绝编译。

类方法解析
类方法解析也是先对类方法表中的class_index项中索引的方法所属的类或接口的符号引用进行解析。我们依然用C来代表解析出来的类,接下来虚拟机将按照下面步骤对C进行后续的类方法搜索。
1 . 首先检查方法引用的C是否为类或接口,如果是接口,那么方法引用就会抛出IncompatibleClassChangeError异常
2 . 方法引用过程中会检查C和它的父类中是否包含此方法,如果C中确实有一个方法与方法引用的指定名称相同,并且声明是签名多态方法(Signature Polymorphic Method),那么方法的查找过程就被认为是成功的,所有方法描述符所提到的类也需要解析。对于C来说,没有必要使用方法引用指定的描述符来声明方法。
3 . 否则,如果C声明的方法与方法引用拥有同样的名称与描述符,那么方法查找也是成功。
4 . 如果C有父类的话,那么按照第2步的方法递归查找C的直接父类。
5 . 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C时一个抽象类,查找结束,并且抛出java.lang.AbstractMethodError异常。

  1. 否则,宣告方法失败,并且抛出java.lang.NoSuchMethodError。
    最后的最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,那么会抛出 java.lang.IllegalAccessError异常。

接口方法解析
接口方法也需要解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。
1 . 与类方法解析不同,如果在接口方法表中发现class_index对应的索引C是类而不是接口,直接抛出java.lang.IncompatibleClassChangeError异常。
2 . 否则,在接口C中查找是否有简单名称和描述符都与目标匹配的方法,如果有则直接返回这个方法的直接引用,查找结束。
3 . 否则,在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
4 . 否则,宣告方法失败,抛出java.lang.NoSuchMethodError异常。

由于接口的方法默认都是public的,所以不存在访问权限问题,也就基本不会抛出java.lang.IllegalAccessError异常。

5.初始化

初始化是类加载的最后一步,在前面的阶段里,除了加载阶段可以通过用户自定义的类加载器加载,其余部分基本都是由虚拟机主导的。但是到了初始化阶段,才开始真正执行用户编写的java代码了。

在准备阶段,变量都被赋予了初始值,但是到了初始化阶段,所有变量还要按照用户编写的代码重新初始化。换一个角度,初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static语句块)中的语句合并生成的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。

1
2
3
4
5
6
7
复制代码public class Test {
static {
i=0; //可以赋值
System.out.print(i); //编译器会提示“非法向前引用”
}
static int i=1;
}

<clinit>()方法与类的构造函数<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会宝成在子类的<clinit>()方法执行之前,父类的<clinit>()已经执行完毕,因此在虚拟机中第一个被执行的<clinit>()一定是java.lang.Object的。

也是由于<clinit>()执行的顺序,所以父类中的静态语句块优于子类的变量赋值操作,所以下面的代码段,B的值会是2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码static class Parent {
public static int A=1;
static {
A=2;
}
}

static class Sub extends Parent{
public static int B=A;
}

public static void main(String[] args) {
System.out.println(Sub.B);
}

<clinit>()方法对于类来说不是必须的,如果一个类中既没有静态语句块也没有静态变量赋值动作,那么编译器都不会为类生成<clinit>()方法。

接口中不能使用静态语句块,但是允许有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但是接口中的<clinit>()不需要先执行父类的,只有当父类中定义的变量使用时,父接口才会初始化。除此之外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中能被正确的枷锁、同步。如果多个线程初始化一个类,那么只有一个线程会去执行<clinit>()方法,其它线程都需要等待。

6.Java虚拟机退出

Java虚拟机退出的一般条件是:某些线程调用Runtime类或System类的exit方法,或者时Runtime类的halt方法,并且Java安全管理器也允许这些exit或者halt操作。
除此之外,在JNI(Java Native Interface)规范中还描述了当使用JNI API来加载和卸载(Load & Unload)Java虚拟机时,Java虚拟机退出过程。

本文转载自: 掘金

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

Python3 CookBook 数据结构和算法(一)

发表于 2017-11-29

欢迎关注我的微信公众号 AlwaysBeta,更多精彩内容等你来。

以下测试代码全部基于 Python3。

Python 提供了大量的内置数据结构,包括列表,集合以及字典。在工作和编码中,可以说天天和它们打交道,经常碰到查询,排序和过滤等等这些问题,虽然每次解决这些问题并不困难,但总感觉代码写的很麻烦,不够优雅。

最近通过阅读《Python3 CookBook》,了解了一些更优秀的方法,做一些简单记录,与大家分享。

1、解压可迭代对象赋值给多个变量

我们都知道,一个序列是可以赋值给多个变量的,就像下面这样:

1
2
3
4
5
6
复制代码In [7]: p = (1, 2, 3)

In [8]: x, y, z = p

In [9]: x
Out[9]: 1

但如果接收的变量个数和序列元素个数不一致,就会报错,如果你不知道元素个数的话,可以采用下面这样的方式:

1
2
3
4
复制代码In [10]: x, *y = p

In [11]: y
Out[11]: [2, 3]

通过这种星号的方式,就可以解压不确定个数或任意个数的可迭代对象了,是不是很棒呢?

那么,用这个方法可以解决哪些问题呢?

先来看一种情况,现在有一个序列,去掉第一个数和最后一个数,然后求剩下数的平均值。

这个问题很简单,我的第一反应是循环求和,然后计算平均值,显然很麻烦。这时候星号表达式就派上用场了:

1
2
3
复制代码def drop_first_last(items):
first, *middle, last = items
return avg(middle)

再看一种情况,比如字符串的分割:

1
2
3
4
5
6
7
8
9
复制代码In [12]: line = 'drwxr-xr-x  41 zyx  staff   1.4K 11 24 08:53 zyx'

In [13]: info, *fields, homedir = line.split(' ')

In [14]: info
Out[14]: 'drwxr-xr-x'

In [15]: homedir
Out[15]: 'zyx'

2、保留最后 N 个元素

这个问题也是经常会遇到的,比如只取文件中满足要求的前五行,或者只返回满足要求的最新十条数据。我的第一反应是列表,然后通过 push 和 pop 来操作列表来实现。

其实通过 collections.deque 可以很容易解决这个问题,使用 deque(maxlen=N) 构造函数新建一个固定大小的队列。当新元素加入并且这个队列已满时,最先进入队列的元素便会被移除,符合先进先出的原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码In [16]: from collections import deque

In [17]: q = deque(maxlen=3)

In [18]: q.append(1)

In [19]: q.append(2)

In [20]: q.append(3)

In [21]: q
Out[21]: deque([1, 2, 3])

In [22]: q.append(4)

In [23]: q
Out[23]: deque([2, 3, 4])

如果没有设置 maxlen 则是一个无限大小的队列,可以通过 appendleft 和 pop 在队首和队尾添加删除元素。

3、字典中的键映射多个值

现在有一个需求,构建一个字典,key 是用户 ID,value 为一个列表,列表元素可以是名字,电话等等,大概是这样:

1
复制代码d = {'id': ['name', 'phone']}

如果我们自己构建这个字典,可能会像下面这样来实现:

1
2
3
4
5
复制代码d = {}
for key, value in items:
if key not in d:
d[key] = value
d[key].append(value)

很麻烦,如果使用 collections 的 defaultdict 就很简单了。defaultdict 的一个特征就是它会自动初始化每个 key 刚开始对应的值,所以我们只关注添加元素操作就可以了。

优化后代码就变成了这样:

1
2
3
复制代码d = defaultdict(list)
for key, value in items:
d[key].append(value)

4、字典排序

字典是无序的,但如果要控制字典中元素的顺序呢?可以使用 colletions 中的 OrderedDict,如下:

1
2
3
4
5
6
7
8
9
复制代码d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
# Outputs "foo 1", "bar 2", "spam 3", "grok 4"

for key in d:
print(key, d[key])

OrderedDict 内部维护这一个根据键插入顺序排序的双向链表。每次新元素插入时,便会被放在链表尾部,对于已经存在的键,并不会改变键的顺序。

但需要注意的是,OrderedDict 的大小是普通字典的两倍,所以在构建一个需要大量 OrderedDict 实例的数据结构时,就要考虑大量内存消耗的影响了。

5、字典的运算

如何取出字典中的最小值,或者对字典进行排序呢?

首先我们来看看直接使用普通的数学运算函数

1
2
3
4
复制代码In [25]: d = {'a': 11, 'b': 43, 'c': 3, 'd': 65}

In [26]: min(d)
Out[26]: 'a'

它比较的逻辑是直接比较 key,然后取出对应的 key,但如果要比较 value 呢?

1
2
复制代码In [28]: min(d.values())
Out[28]: 3

结果是正确的,但似乎并不完美,如果键值一起返回就完美了。这时候就该 zip 登场了,它的作用是可以使键和值反转过来。

1
2
复制代码In [29]: min(zip(d.values(), d.keys()))
Out[29]: (3, 'c')

它直接返回了值最小的键和值,这样就很好了,不管需要哪个信息都可以直接使用。如果要对这个字典排序的话也很简单:

1
2
复制代码In [34]: sorted(zip(d.values(), d.keys()))
Out[34]: [(3, 'c'), (11, 'a'), (43, 'b'), (65, 'd')]

先写这么多吧,未完待续。。。

本文转载自: 掘金

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

concurrentcache--golang内存缓存 co

发表于 2017-11-29

concurrentcache是什么

concurrentcache是golang版的内存缓存库,采用多Segment设计,支持不同Segment间并发写入,提高读写性能。

设想场景

内存缓存的实现,防止并发写冲突,都需要先获取写锁,再写入。如果只有一个存储空间,那么并发写入的时候只能有一个go程在操作,其他的都需要阻塞等待。为了提高并发写的性能,把存储空间切分成多个Segment,每个Segment拥有一把写锁,那样,分布在不同Segment上的写操作就可以并发执行了。concurrentcache设计就是切分多个Segment,提高些并发写的效率。

concurrentcache设计思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码// ConcurrentCache结构体,包含一个ConcurrentCacheSegment切片
type ConcurrentCache struct {
segment []*ConcurrentCacheSegment
sCount uint32
}
// ConcurrentCacheSegment结构体,内部使用一个map存储数据
type ConcurrentCacheSegment struct {
sync.RWMutex
data map[string]*ConcurrentCacheNode
lvPool map[string]*ConcurrentCacheNode
dCount uint32
dLen uint32
pool *sync.Pool
hits uint64
miss uint64
now time.Time
}
type ConcurrentCacheNode struct {
V interface{}
visit uint32
lifeExp time.Duration
createTime time.Time
}

写

从ConcurrentCache结构体可以看出,包含一个ConcurrentCacheSegment切片,sCount表示切片的长度,这个切片的长度,在初始化的时候就确定,不再改变。ConcurrentCacheSegment内包含一个读写锁,写入时候根据key的hash值选取写入到哪一个Segment内,通过多Segment设计,来提高写并发效率。

读

为了提高读效率,尽可能多的减少读过程对内存缓存的修改,使用golang提供的原子操作来修改访问状态(visit、miss、hits等),遇到缓存过期的节点也不马上淘汰,因为读访问上的是读锁,要删除数据需要用到写互斥锁,这样会降低读的并发性,所以推迟删除过期的节点,当写数据,Segment的节点不够用的时候再去删除过期节点。

concurrentcache缓存淘汰方式

concurrentcache缓存淘汰方式采用随机选取3个节点,优先淘汰其中过期的一个节点(上一步说的推迟删除过期节点),如果没有过期节点就选取访问量最少(ConcurrentCacheNode结构体中的visit)的节点淘汰。这个思路是参考redis的缓存淘汰策略,redis并不是严格lru算法,采用的是随机选取样本的做法。这样做也是为了提高写性能。

concurrentcache & cache2go 性能对比

(MBP 16G版本)concurrentcache和cache2go进行了并发下的压测对比,对比结果,concurrentcache无论是执行时间还是内存占比,都比cache2go优

concurrentcache

1
2
3
4
5
6
7
复制代码
BenchmarkConcurrentCache_Set-8 3000000 573 ns/op 190 B/op 2 allocs/op
BenchmarkConcurrentCache_Set-8 2000000 634 ns/op 235 B/op 3 allocs/op
BenchmarkConcurrentCache_Set-8 3000000 535 ns/op 190 B/op 2 allocs/op
BenchmarkConcurrentCache_Get-8 5000000 235 ns/op 36 B/op 1 allocs/op
BenchmarkConcurrentCache_Get-8 5000000 234 ns/op 36 B/op 1 allocs/op
BenchmarkConcurrentCache_Get-8 5000000 234 ns/op

cache2go

1
2
复制代码BenchmarkCacheTable_Add-8        1000000          1858 ns/op         480 B/op         10 allocs/op
BenchmarkCacheTable_Value-8 5000000 300 ns/op 60 B/op 2 allocs/op

concurrentcache源码

本文转载自: 掘金

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

如何编写 Dockerfile 文件创建 Docker 镜像

发表于 2017-11-29

一、前言

承接上篇文章 docker 镜像与容器,本篇来讲讲如何创建 Dockerfile 来构建一个镜像。上篇文章有讲到构建一个自定义镜像是手动去构建的,虽然步骤清晰,但是操作比较繁琐,镜像分发起来也不是很方便,所以有必要用一种更好的办法去替换这种模式去创建自定义镜像,于是 Dockerfile 就是最优替代方案。废话少说,现在就来看看如何编写一个 Dockerfile 文件并创建容器镜像的,先说明一个本篇文章的运行环境吧,有看过上篇文章的朋友应该知道,我用的 docker 的镜像加速地址是阿里云的,我觉得这是我用 docker 最无痛的环境了。

二、Dockerfile 示例

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
复制代码# Base images 基础镜像
FROM centos

#MAINTAINER 维护者信息
MAINTAINER lorenwe

#ENV 设置环境变量
ENV PATH /usr/local/nginx/sbin:$PATH

#ADD 文件放在当前目录下,拷过去会自动解压
ADD nginx-1.13.7.tar.gz /tmp/

#RUN 执行以下命令
RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
&& yum update -y \
&& yum install -y vim less wget curl gcc automake autoconf libtool make gcc-c++ zlib zlib-devel openssl openssl-devel perl perl-devel pcre pcre-devel libxslt libxslt-devel \
&& yum clean all \
&& rm -rf /usr/local/src/*
RUN useradd -s /sbin/nologin -M www

#WORKDIR 相当于cd
WORKDIR /tmp/nginx-1.13.7

RUN ./configure --prefix=/usr/local/nginx --user=www --group=www --with-http_ssl_module --with-pcre && make && make install

RUN cd / && rm -rf /tmp/

COPY nginx.conf /usr/local/nginx/conf/

#EXPOSE 映射端口
EXPOSE 80 443

#ENTRYPOINT 运行以下命令
ENTRYPOINT ["nginx"]

#CMD 运行以下命令
CMD ["-h"]

以上代码示例是我编写的一个认为很有代表性的 dockerfile 文件,涉及到的内容不多,但基本上把所有 dockerfile 指令都用上了,也包含一些细节方面的东西,为了达到示例的效果所以并不是最简洁的 dockerfile,建立一个文件夹将以上 dockerfile 放在该文件内,再去 nginx 官网把 nginx 源码包下来放到该文件夹内,之后再在该文件夹内打开命令行窗口,最好是以管理员权限打开命令行窗口,以免出现一些权限问题的错误,此时的目录结构应该是以下样子的

dockerfile catalog

dockerfile catalog

三、指令分析

FROM 表示的是这个 dockerfile 构建镜像的基础镜像是什么,有点像代码里面类的继承那样的关系,基础镜像所拥有的功能在新构建出来的镜像中也是存在的,一般用作于基础镜像都是最干净的没有经过任何三方修改过的,比如我用的就是最基础的 centos,这里有必要说明一下,因为我用的镜像加速源是阿里云的,所以我 pull 下来的 centos 是自带阿里云的 yum 源的镜像,如果你用的不是阿里云的镜像加速源,pull 下来的镜像 yum 源也不一样,到时 yum 安装软件的时候可能会遇到很多问题(你懂得)。

MAINTAINER 就是维护者信息了,填自己名字就可了,不用说什么了

ENV 设置环境变量,简单点说就是设置这个能够帮助系统找到所需要运行的软件,比如我上面写的是 “ENV PATH /usr/local/nginx/sbin:$PATH”,这句话的意思就是告诉系统如果运行一个没有指定路径的程序时可以从 /usr/local/nginx/sbin 这个路径里面找,只有设置了这个,后面才可以直接使用 ngixn 命令来启动 nginx,不然的话系统会提示找不到应用。

ADD 顾名思义,就是添加文件的功能了,但是他比普通的添加做的事情多一点,源文件可以是一个文件,或者是一个 URL 都行,如果源文件是一个压缩包,在构建镜像的时候会自动的把压缩包解压开来,示例我写的是 ‘ADD nginx-1.13.7.tar.gz /tmp/’ 其中 nginx-1.13.7.tar.gz 这个压缩包是必须要在 dockefile 文件目录内的,不在 dockerfile 文件目录内的 比如你写完整路径 D:test/nginx-1.13.7.tar.gz 都是会提示找不到文件的。

RUN 就是执行命令的意思了,RUN 可以执行多条命令, 用 && 隔开就行,如果命令太长要换行的话在末尾加上 ‘\’ 就可以换行命令,RUN 的含义非常简单,就是执行命令,但其中有一些细节还是需要注意的,现在就通过上面的示例来看看需要注意的地方有哪些吧。其中 RUN rpm –import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 的作用就是导入软件包签名来验证软件包是否被修改过了,为做到安全除了系统要官方的之外软件也要保证是可信的。yum update -y 升级所有包,改变软件设置和系统设置,系统版本内核都升级,我们知道 linux 的软件存在依赖关系,有时我们安装新的软件他所依赖的工具软件也需要是最新的,如果没有用这个命令去更新原来的软件包,就很容易造成我们新安装上的软件出问题,报错提示不明显的情况下我们更是难找到问题了,为避免此类情况发生我们还是先更新一下软件包和系统,虽然这会使 docker 构建镜像时变慢但也是值得的,至于后面的命令自然是安装各种工具库了,接着来看这句 yum clean all ,把所有的 yum 缓存清掉,这可以减少构建出来的镜像大小,rm -rf /usr/local/src/ 清除用户源码文件,都是起到减少构建镜像大小的作用。RUN 指令是可以分步写的,比如上面的 RUN 可以拆成以下这样:

1
2
3
4
5
6
复制代码# 不推荐
RUN rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 \
RUN yum update -y \
RUN yum install -y vim less wget curl gcc automake autoconf libtool make gcc-c++ zlib zlib-devel openssl openssl-devel perl perl-devel pcre pcre-devel libxslt libxslt-devel \
RUN yum clean all \
RUN rm -rf /usr/local/src/*

这样也是可以的,但是最好不要这样,因为 dockerfile 构建镜像时每执行一个关键指令都会去创建一个镜像版本,这有点像 git 的版本管理,比如执行完第一个 RUN 命令后在执行第二个 RUN 命令时是会在一个新的镜像版本中执行,这会导致 yum clean all 这个命令失效,没有起到精简镜像的作用,虽然不推荐多写几个 RUN,但也不是说把所有的操作都放在一个 RUN 里面,这里有个原则就是把所有相关的操作都放在同一个 RUN 里面,就比如我把 yum 更新,安装工具库,清除缓存放在一个 RUN 里面,后面的编译安装 nginx 放在另外一个 RUN 里面。

WORKDIR 表示镜像活动目录变换到指定目录,就相当于 linux 里面 cd 到指定目录一样,其实完全没有必要使用这个指令的,在需要时可以直接使用 cd 命令就行,因为这里使用了 WORKDIR,所以后面的 RUN 编译安装 nginx 不用切换目录,讲到这里又想起了另外一个问题,如下:

1
2
3
4
复制代码
RUN cd /tmp/nginx-1.13.7

RUN ./configure

这样可不可以呢,我想前面看懂的朋友应该知道答案了吧,这里还是再啰嗦一下,这样是会报找不到 configure 文件错误的,原因很简单,因为这个两个命令都不是在同一个镜像中执行的,第一个镜像 cd 进入的目录并不代表后面的镜像也进入了。

COPY 这个指令很简单,就是把文件拷贝到镜像中的某个目录,注意源文件也是需要在 dockerfile 所在目录的,示例的意思是拷贝一份 nginx 配置文件,现在就在 dockerfile 所在目录创建这个文件

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
复制代码user  www;  
worker_processes  2;
daemon off;

pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

配置很简单,就是对官方的配置文件把注释去掉了,注意里面的 daemon off; 配置,意思是关闭 nginx 后台运行,原因在上一篇文章中讲过,这里再来絮叨一下,容器默认会把容器内部第一个进程是否活动作为docker容器是否正在运行的依据,如果 docker 容器运行完就退出了,那么docker容器便会直接退出,docker run 的时候把 command 作为容器内部命令,如果使用 nginx,那么 nginx 程序将后台运行,这个时候 nginx 并不是第一个执行的程序,而是执行的 bash,这个 bash 执行了 nginx 指令后就挂了,所以容器也就退出了,如果我们设置了 daemon off 后
启动 nginx 那么 nginx 就会一直占用命令窗口,自然 bash 没法退出了所以容器一直保持活动状态。

EXPOSE 示例注释写的是映射端口,但我觉得用暴露端口来形容更合适,因为在使用 dockerfile 创建容器的时候不会映射任何端口,映射端口是在用 docker run 的时候来指定映射的端口,比如我把容器的 80 端口映射到本机的 8080 端口,要映射成功就要先把端口暴露出来,有点类似于防火墙的功能,把部分端口打开。

ENTRYPOINT 和 CMD 要放在一起来说,这两者的功能都类似,但又有相对独特的地方,他们的作用都是让镜像在创建容器时运行里面的命令。当然前提是这个镜像是使用这个 dockerfile 构建的,也就是说在执行 docker run 时 ENTRYPOINT 和 CMD 里面的命令是会执行的,两者是可以单独使用,并不一定要同时存在,当然这两者还是有区别的。

先从 CMD 说吧,CMD 的一个特点就是可被覆盖,比如把之前的 dockerfile 的 ENTRYPOINT 这一行删除,留下 CMD 填写[“nginx”],构建好镜像后直接使用 docker run lorenwe/centos_nginx 命令执行的话通过 docker ps 可以看到容器正常运行了,启动命令也是 “ngixn”,但是我们使用 docker run lorenwe/centos_nginx bin/bash 来启动的话通过 docker ps 查看到启动命令变成了 bin/bash,这就说明了 dockerfile 的 CMD 指令是可被覆盖的,也可以把他看做是容器启动的一个默认命令,可以手动修改的。

而 ENTRYPOINT 恰恰相反,他是不能被覆盖,也就是说指定了值后再启动容器时不管你后面写的什么 ENTRYPOINT 里面的命令一定会执行,通常 ENTRYPOINT 用法是为某个镜像指定必须运行的应用,例如我这里构建的是一个 centos_nginx 镜像,也就是说这个镜像只运行 ngixn,那么我就可以在 ENTRYPOINT 写上[“nginx”],有些人在构建自己的基础镜像时(基础镜像只安装了一些必要的库)就只有 CMD 并写上 [‘bin/bash’],当 ENTRYPOINT 和 CMD 都存在时 CMD 中的命令会以 ENTRYPOINT 中命令的参数形式来启动容器,例如上面的示例 dockerfile,在启动容器时会以命令为 nginx -h 来启动容器,遗憾的是这样不能保持容器运行,所以可以这样启动 docker run -it lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf,那么容器启动时运行的命令就是 nginx -c /usr/local/nginx/conf/nginx.conf,是不是很有意思,可以自定义启动参数了。

当然还有一些没有用到的指令:

ARG,ARG指令用以定义构建时需要的参数,比如可以在 dockerfile中写上这句 ARG a_nother_name=a_default_value,ARG指令定义的参数,在docker build命令中以 –build -arg a_name=a_value 形式赋值,这个用的一般比较少。

VOLUME,VOLUME指令创建一个可以从本地主机或其他容器挂载的挂载点,用法是比较多的,都知道 docker 做应用容器比较方便,其实 docker 也可做数据容器,创建数据容器镜像的 dockerfile 就主要是用 VOLUME 指令,要讲明 VOLUME 用法有必要在开一篇文章,再此就不做介绍了,

USER,USER用来切换运行属主身份的。docker 默认是使用 root 用户,但若不需要,建议切换使用者身分,毕竟 root 权限太大了,使用上有安全的风险。LABEL,定义一个 image 标签。

四、构建演示

dockerfile 构建镜像的命令很简单,在我的示例中我的命令是 “docker build -t lorenwe/centos_nginx . “,注意后面的点不能省略,表示的从当前目录中寻找 dockerfile 来构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码D:\docker\lorenwe>docker build -t lorenwe/centos_nginx .
Sending build context to Docker daemon 995.8kB
Step 1/13 : FROM centos
---> d123f4e55e12
Step 2/13 : MAINTAINER lorenwe
---> Running in e5c7274f50e8
---> 606f7222e69a
Removing intermediate container e5c7274f50e8
Step 3/13 : ENV PATH /usr/local/nginx/sbin:$PATH
---> Running in 23716b428809
---> 5d8ee1b5a899
....
Successfully built eaee6b40b151
Successfully tagged lorenwe/centos_nginx:latest

看到以上内容就说明成功,构建过程可能需要一点点时间,毕竟要安装一些软件,如果你跟我一样是配置的阿里云的容器源构建时应该不会出现什么问题,因为我之前是有拉取过 centos ,所以在 build 时直接使用本地的 centos,如果你没有拉取过 centos,那么在 build 时还会把 centos 拉取下来

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
复制代码D:\docker\lorenwe>docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_nginx latest eaee6b40b151 7 minutes ago 427MB
lorenwe/centos_net_tools latest 35f8073cede1 6 days ago 277MB
centos latest d123f4e55e12 3 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB

D:\docker\lorenwe>docker run -itd --name nginx1 lorenwe/centos_nginx
15d4f108dab7c2f276209ebeb501cac0d3be828e1e81bae22d3fd97c617439eb

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

D:\docker\lorenwe>docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
15d4f108dab7 lorenwe/centos_nginx "nginx -h" nginx1

D:\docker\lorenwe>docker run -itd --name nginx2 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
b6b0e962ca3056d67c24145b08975ffddb9cc050fce5f09f65310fb323ffc1c3

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b6b0e962ca30 lorenwe/centos_nginx "nginx -c /usr/loc..." 80/tcp nginx2

D:\docker\lorenwe>docker run -itd -p 8080:80 --name nginx3 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
2f6997745641e3e3edbbfe5213e6235cab3b5a929f116a2c132df504156090c6

D:\docker\lorenwe>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2f6997745641 lorenwe/centos_nginx "nginx -c /usr/loc..." 0.0.0.0:8080->80/tcp nginx3
b6b0e962ca30 lorenwe/centos_nginx "nginx -c /usr/loc..." 80/tcp nginx2

D:\docker\lorenwe>docker stop nginx2
nginx2

其中 “docker run -itd -p 8080:80 –name nginx3 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf” 中的 -p 8080:80 表示把主机的 8080 端口映射到容器的 80 端口,因为之前我们在 dockerfile 中把 80 端口暴露出来了,做好端口映射后现在就可以在主机中打开浏览器访问 127.0.0.1:8080 就能看到 nginx 的欢迎页面了 (^v^).

1
2
复制代码D:\docker\lorenwe>docker run -itd -v D:/docker/lorenwe/html:/usr/local/nginx/html  -p 8081:80 --name nginx4 lorenwe/centos_nginx -c /usr/local/nginx/conf/nginx.conf
cd2d4eb70a39057aed3bfcb64e1f03433e2054d7ff5d50098f49d2e6f2d9e02e

我再在原来的参数中加入了 -v 参数,其作用就是把一个本地主机的目录挂载到容器内部,这个目录是一个共享的状态,两边都可以进行修改,这就是容器的共享卷,其作用就不言而喻了,现在我们在 D:\docker\lorenwe 的目录下新建一个叫 html 的文件夹,再在 html 文件夹内新建一个 index.html 随便写上一点内容后再去主机浏览器上访问一下 127.0.0.1:8081 看看是不是你想要看到内容。虽然通过 -v 参数可以满足大部分应用场景,但是 docker 的 VOLUME 还有其他更好用法,欲知后事如何,请看下回分解!

本文转载自: 掘金

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

如何做一个靠谱的发号器

发表于 2017-11-29

为什么需要一个发号器

在使用数据库时,表的主键经常会使用数据库的自增(auto_increment)来产生。这当然很方便也很高效。但是使用自增也会带来一些麻烦。如果从一个数据库以外的地方,也就是发号器来产生全局唯一 ID,这些问题就可以得到解决,生活就可以更美好。

  • 难以适应分片场景

在采用数据库分片时,如果使用数据库自增 ID,不同分片上会产生相同的 ID。单靠 ID 无法唯一标示一个对象,还需要额外加上分片字段才行。如果需要将 ID 用于其他对象的关联时,会麻烦很多。而采用发号器生成的是全局唯一的 ID,单靠 ID 就能实现关联。同时,这也使得采用 ID 作为分片字段成为可能。

  • 主备切换时数据冲突

在 MySQL 集群发生主备切换时,异步复制无法确保主从完全同步。在备库开放写入后,备库上产生的自增 ID 会和尚未同步的主库上的数据冲突。这样一来,即使原来的主库恢复了,也无法重新加入集群。数据修复也变成了一件非常困难的事情。引入发号器以后,备库上插入的 ID 和原来主库上的 ID 是不会重复的。因此,未复制的新增数据和对这些新增数据的修改就不会在备库发生冲突。

  • 网络异常时无法判断插入是否成功

当插入记录时,如果使用数据库自增 ID,在完成插入后,才能得到产生的 ID。如果在执行语句时发生网络中断,客户端无法知道事务是否成功,即使成功,也无法再获得产生的 ID。如果使用发号器,就可以在插入之前预先产生 ID。如果碰到网络中断,可以用已经获得的 ID 去尝试查询来判断之前的插入是否成功。

此外,一些业务 ID 会需要一个全局唯一的整数作为它的组成部分。其他的分布式系统可以用全局单调的唯一 ID 作为事务号。有一个现成的服务就不用各自实现了。

发号器的必要特性

既然叫发号器,首先就得保证 ID 的全局唯一。就是说保证无论什么情况下都不会发出重复的 ID。这看起来很简单,但是事实上,很多实现却上并没有做到这点。要真正做到全局唯一,发号器必须要实现 crash safe,并不受外部环境变化影响。

  • crash safe

首先是 crash safe。即得保证在服务崩溃重新恢复后,不会产生已经发过的 ID。在服务彻底完蛋时,也要能够在其他地方恢复出一个一定能用的。有的实现定期保存或者异步保存已经发过的 ID。如果发生崩溃,如果直接用保存过的 ID 继续发,就会发出已经发过的号。有的实现采用 MySQL 或 Redis 来产生 ID。由于 MySQL 和 Redis 的复制本身难以保证强一致,在发生主备切换时,备机尚未完全同步的话,还是会发出重复的 ID 来。有的实现没有使用副本,单纯靠分片来实现负载均衡和高可用,这时如果某个实例完蛋了,想要重新恢复一个就没法了。

  • 不受外部环境变化影响

很多发号器实现是基于时间戳的。但是有些实现直接采用了机器上的时间戳作为 ID 的一部分。如果机器时间发生回跳(不要认为这不可能),就会造成 ID 重复。使用时间戳同时也对机器时间的精度有了依赖。

要让发号器能真正有用,还得实现高可用,并能支撑足够大的吞吐量。不然发号器本身也会成为一个单点或瓶颈。

如何设计发号器

有赞同样有对发号器的需求。经过对现有实现的考察后,我们还是打算实现一个自己的发号器,我给它起了个名字:March。我们的发号器同样要解决这些问题。

持久化

要满足真正的全局唯一,持久化是必须的。而且持久化还必须是不会丢失的,强一致的。

如果发号器实现是分散在各个应用服务器上的,由于应用服务器的持久化能力是难以保证的,可靠性就会受影响。而且这样一来,每个应用服务器也要有一个终身及死后也全局唯一的 ID 作为产生的 ID 的一部分,来满足全局唯一,这就大大提高了部署和运维的门槛。所以,我们认为发号器最好还是集中式的。

在采用集中式的前提下,持久化的副本也是不可少的。要自己实现这样的一个持久化系统是很难的。所以,在持久化方案上,我们选择了现成的 etcd。etcd 能满足不会丢失的,多副本,强一致的全部需求。持久化就可以全部放到 etcd 中,发号器本身就可以是无状态的,这样一来,高可用的实现也会容易一些。

是否全局单调

是否全局单调其实是个权衡。在确定要高可用的前提下,全局单调和负载均衡是不可兼得的(可以想想为什么)。我们最终还是选择实现全局单调。全局单调的 ID 有额外的好处。作为主键时,可以直接代替时间字段排序。由于 MySQL 二级索引是指向主键的,使用主键排序通常可以避免排序操作,直接利用索引就能完成。另外,如果要实现一些分布式一致性系统,一个全局单调的 ID 生成器也是一个必备的组件。

高可用

由于采用了全局单调,高可用方案就只能是主备的。一个集群内,同时只能有一个实例对外提供服务。这时候就要考虑怎么实现选主和故障切换。既然我们用了 etcd,实现高可用的时候也正好可以用上它的 TTL、Watch 这些特性。然后也要能让客户端知道哪个实例才是主实例,可以自动切换访问路径。

ID 的形式

发号器产生的 ID 一般都是 64 位整数,这样对数据库比较友好,容量也能满足业务需求,不会哪天爆了。通常产生的 ID 可以分成两大类。一类是单纯的 Sequence,即一个不断递增的整数。另一类是基于 Timestamp 的,由于机器时间的精度限制,通常都会额外再加一段 Sequence。为了分布式,还经常会加上各种不同的标示实例的位。不同的实现无非就是这些东西的组合以及各段的长短的变化。有赞之前已经有了几个实现。新的发号器要落地,也得兼容现有的。所以不同的 ID 的形式还是都得支持。但是具体实现细节上,可以比原有的更进一步。

认证和权限控制

使用发号器的业务方会有很多。为了信息安全,和避免相互干扰,认证和权限控制功能也有了需求。March 可以设置多个用户,为每个用户分配访问不同的发号器的权限,以及其他的创建,管理类权限。用户信息同样不能丢,所以也持久化在 etcd 中。

通信协议

作为一个服务,就会有和客户端交互。有交互,就要有一个协议。我们希望尽量能采用一个现成的协议。这样对实现不同语言的客户端会方便很多。同时这个协议要足够轻量高效,也能具备扩展性。我们最后选择了 Redis 协议。Redis 协议很简单,协议本身的负担小。由于是个广泛使用的东西,各种语言都有它的库。这样在实现客户端 SDK 的时候,就有了个很好的起点。现成的一些命令,如 INCR,INCRBY,GET 等本身也很适合用于发号器。在需要一些特殊的功能时,也可以自己添加新命令。高可用方面,Redis
Cluster 的协议也可以用上。这样客户端的自动切换就不用自己实现了。对于服务端,好几个语言也都有现成的库。

发号器的实现

有赞的发号器 March 是用 Go 语言实现的。语言选择上其实没太大讲究。不过对于这类项目,Go 在开发效率,部署简便,和倾向低延迟的 gc 优化还是有一些优势。

ID生成

前面说过,发号器产生的 ID 可以分成两大类。一类是 Sequence,一类是基于 Timestamp 的。这两类有各自的实现。

  • Sequence

March 在启动时会从 etcd 中载入之前持久化的已经发过的 id 作为起点。然后执行一次持久化,将起始 id + batch 保存下来。 [ id, id + batch ) 的区间就是缓存。客户端请求时,下发的 id 都是从这个缓存中取的。同时启动一个 goroutine 来做持久化。在这个缓存的容量低于水位线(默认是 50%)时,会异步通知这个持久化 goroutine 进行持久化,将 id + batch * 2 保存下来。此时,缓存的上界就扩容到了 id

  • batch * 2,以此类推。由于持久化是异步的,所以一般情况下,并不会阻塞请求,造成请求延迟增大。但是有突发的并发时,在持久化没进行完,缓存就已经耗尽的情况下,为了保证正确性,才会发生阻塞,等待持久化完成。所以,对于高并发的应用,配置一个大的缓存区间可以获取更高的性能。比如将 batch 设为 10000,平均发出 10000 个号才需要持久化一次。备机平时是不提供服务的,在发生主备切换时,备机才会从持久化中重新载入配置。所以备机提升为主机以后,也可以保证不会发重,只是从客户端看来,会跳空一段
    id。不过这也算不上什么问题。
    • Timestamp

Timestamp 类型的 ID 分成 3 段:node,timestamp,sequence。通过配置各个段的长度和偏移,以及时间戳的精度,就可以兼容各种已有的基于时间戳的发号器实现。多个请求到来时,如果 timestamp 相同,会增长 sequence。timestamp 改变时,就清零 sequence。有一点特别的地方是,我们允许 sequence 段溢出。 溢出的部分会加到 timestamp 段上去。这样即使在时间戳精度范围内 sequence 耗尽了,也不用阻塞请求。Timestamp
类型持久化的是时间,保存的是当前的 timestamp + 提前量。这里的 timestamp 是包含 sequence 溢出的部分。Timestamp 类型的持久化是定时进行的。由于已持久化的时间戳总是大于当前时间的,因此等待持久化而造成的阻塞基本上是不会发生的。March 启动时,如果获取的当前时间大于保存的时间,就使用当前时间作为起点,否则就使用已保存的时间作为起点。每次请求获取时间时也是类似。如果发现获取的时间小于已经发过的 timestamp,就继续使用当前
timestamp。这样就确保了即使机器时间跳变时,发出的 id 也是单调增长的,绝对不会重复。同时由于允许溢出,也不会因为时间回跳而阻塞。当然这种方式带来的一个影响是,如果从获取的 id 里解析出时间,可能并不是准确的时间。由于切换或溢出,看到的时间可能会提前。不过本来也不应该依赖这些细节不是么。

高可用

March 的高可用是利用 etcd 的 ttl 和 watch 实现的。启动时,先尝试创建一个新的带 ttl 的 Node。如果成功,就成为了主节点;如果由于已存在而失败,就成为了备节点。

  • 主节点

定时用前一次请求返回的 index 刷新 Node 的 ttl,保持自己的主节点角色。发现刷新失败时,说明主节点角色已经被抢走,从抢主节点过程重新开始。与此同时,还会等待 demote 请求。收到 demote 请求时,会等待新的主节点信息,然后将自己置为备节点。

  • 备节点

先查询主节点的信息。在备节点收到发号请求时,会按 Redis Cluster 协议重定向到主节点。之后就开始 Watch Node 的变化。检测到变化后,也开始抢主节点过程。

这样,可以做到在主节点发生故障时,最多等待一个 ttl 就能检测到,并完成切换。而在主动切换时,结合客户端,可以做到完全无损,只有毫秒级的阻塞。

此外,每个节点都会存保存各自的带 ttl 的节点信息,同时定时刷新,用于返回给客户端集群信息。每个发号器在每次持久化时,也会携带上上一次持久化获得的 index。一旦不匹配出错,也会将自身重置为备节点。这可以避免网络堵塞或进程僵死造成原主失效而自身却不知道。在发生非预期错误时,HA goroutine 会等待 2 * ttl,以免不断出错造成死循环。此外,备节点也需要能够完成用户认证。但因为认证是不能重定向的,所以还需要检测 etcd 上的用户信息变化,重新同步用户数据。

小结

发号器看起来简单,但是要实现一个靠谱的,易用的,要考虑到的地方还是很多的。其实很多东西都是这样。我们还做了更多。为了更容易接入落地,我们在数据库中间件中也做了集成。配置后,执行 insert 时,会自动代入配置的自增字段和 id 值,让业务方完全无痛。

本文转载自: 掘金

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

1…926927928…956

开发者博客

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