Quantcast
Channel: IT瘾技术推荐
Viewing all 330 articles
Browse latest View live

用神经网络训练一个文本分类器

$
0
0

理解聊天机器人的工作原理是非常重要的。聊天机器人内部一个非常重要的组件就是文本分类器。我们看一下文本分类器的神经网络(ANN)的内部工作原理。

1-DpMaU1p85ZSgamwYDkzL-A

多层神经网络

我们将会使用2层网络(1个隐层)和一个“词包”的方法来组织我们的训练数据。文本分类有3个特点:模式匹配、算法、神经网络。虽然使用多项朴素贝叶斯算法的方法非常有效,但是它有3个致命的缺陷:

  • 这个算法输出一个分数而不是一个概率。我们可以使用概率来忽略特定阈值以下的预测结果。这类似于忽略收音机中的噪声。
  • 这个算法从一个样本中学习一个分类中包含什么,而不是一个分类中不包含什么。一个分类中不包含什么的的学习模式往往也很重要。
  • 不成比例的大训练集的分类将会导致扭曲的分类分数,迫使算法相对于分类规模来调整输出分数,这并不理想。

和它“天真”的对手一样,这种分类器并不试图去理解句子的含义,而仅仅对它进行分类。事实上,所谓的“人工智能聊天机器人”并不理解语言,但那是 另一个故事

如果你刚接触人工神经网络,这是它的 工作原理

理解分类算法,请看 这里

我们来逐个分析文本分类器的每个部分。我们将按照以下顺序:

  1. 引用需要的库
  2. 提供训练集
  3. 整理数据
  4. 迭代:编写代码+测试预测结果+调整模型
  5. 抽象

代码在这里,我们使用ipython notebook这个在数据科学项目上非常高效的工具。代码语法是python。

我们首先导入自然语言工具包。我们需要一个可靠的方法将句子切分成词并且将单词词干化处理。

# use natural language toolkit
import nltk
from nltk.stem.lancaster import LancasterStemmer
import os
import json
import datetime
stemmer = LancasterStemmer()

下面是我们的训练集,12个句子属于3个类别(“意图”)。

# 3 classes of training data
training_data = []
training_data.append({"class":"greeting", "sentence":"how are you?"})
training_data.append({"class":"greeting", "sentence":"how is your day?"})
training_data.append({"class":"greeting", "sentence":"good day"})
training_data.append({"class":"greeting", "sentence":"how is it going today?"})

training_data.append({"class":"goodbye", "sentence":"have a nice day"})
training_data.append({"class":"goodbye", "sentence":"see you later"})
training_data.append({"class":"goodbye", "sentence":"have a nice day"})
training_data.append({"class":"goodbye", "sentence":"talk to you soon"})

training_data.append({"class":"sandwich", "sentence":"make me a sandwich"})
training_data.append({"class":"sandwich", "sentence":"can you make a sandwich?"})
training_data.append({"class":"sandwich", "sentence":"having a sandwich today?"})
training_data.append({"class":"sandwich", "sentence":"what's for lunch?"})
print ("%s sentences in training data" % len(training_data))

12 sentences in training data

现在我们可以将数据结构组织为: documentsclasses 和 words.

words = []
classes = []
documents = []
ignore_words = ['?']
# loop through each sentence in our training data
for pattern in training_data:
    # tokenize each word in the sentence
    w = nltk.word_tokenize(pattern['sentence'])
    # add to our words list
    words.extend(w)
    # add to documents in our corpus
    documents.append((w, pattern['class']))
    # add to our classes list
    if pattern['class'] not in classes:
        classes.append(pattern['class'])

# stem and lower each word and remove duplicates
words = [stemmer.stem(w.lower()) for w in words if w not in ignore_words]
words = list(set(words))

# remove duplicates
classes = list(set(classes))

print (len(documents), "documents")
print (len(classes), "classes", classes)
print (len(words), "unique stemmed words", words)

12 documents
3 classes ['greeting', 'goodbye', 'sandwich']
26 unique stemmed words ['sandwich', 'hav', 'a', 'how', 'for', 'ar', 'good', 'mak', 'me', 'it', 'day', 'soon', 'nic', 'lat', 'going', 'you', 'today', 'can', 'lunch', 'is', "'s", 'see', 'to', 'talk', 'yo', 'what']

注意每个单词都是词根并且小写。词根有助于机器将“have”和“having”等同起来。同时我们也不关心大小写。 1-eUedufAl7_sI_QWSEIstZg

我们将训练集中的每个句子转换为词包。

# create our training data
training = []
output = []
# create an empty array for our output
output_empty = [0] * len(classes)

# training set, bag of words for each sentence
for doc in documents:
    # initialize our bag of words
    bag = []
    # list of tokenized words for the pattern
    pattern_words = doc[0]
    # stem each word
    pattern_words = [stemmer.stem(word.lower()) for word in pattern_words]
    # create our bag of words array
    for w in words:
        bag.append(1) if w in pattern_words else bag.append(0)

    training.append(bag)
    # output is a '0' for each tag and '1' for current tag
    output_row = list(output_empty)
    output_row[classes.index(doc[1])] = 1
    output.append(output_row)

# sample training/output
i = 0
w = documents[i][0]
print ([stemmer.stem(word.lower()) for word in w])
print (training[i])
print (output[i])

['how', 'ar', 'you', '?']
[0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[1, 0, 0]

上面的步骤是文本分类中的一个经典步骤:每个训练句子被转化为一个包含0和1的数组,而不是语料库中包含独特单词的数组。

['how', 'are', 'you', '?']

被词干化为:

['how', 'ar', 'you', '?']

然后转换为输入词包的形式: 1代表单词存在于词包中(忽略问号?)

[0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

输出:第一类

[1, 0, 0]

注意:一个句子可以有多个分类,也可以没有。确保理解上面的内容,仔细阅读代码直到你理解它。

机器学习的第一步是要有干净的数据

1-CcQPggEbLgej32mVF2lalg

接下来我们的学习2层神经网络的核心功能。

如果你是人工神经网络新手,这里是它的工作原理

我们使用 numpy,原因是它可以提供快速的矩阵乘法运算。

1-8SJcWjxz8j7YtY6K-DWxKw

我们使用sigmoid函数对值进行归一化,用其导数来衡量错误率。通过不断迭代和调整,直到错误率低到一个可以接受的值。

下面我们也实现了bag-of-words函数,将输入的一个句子转化为一个包含0和1的数组。这就是转换训练数据,得到正确的转换数据至关重要。

import numpy as np
import time

# compute sigmoid nonlinearity
def sigmoid(x):
    output = 1/(1+np.exp(-x))
    return output

# convert output of sigmoid function to its derivative
def sigmoid_output_to_derivative(output):
    return output*(1-output)

def clean_up_sentence(sentence):
    # tokenize the pattern
    sentence_words = nltk.word_tokenize(sentence)
    # stem each word
    sentence_words = [stemmer.stem(word.lower()) for word in sentence_words]
    return sentence_words

# return bag of words array: 0 or 1 for each word in the bag that exists in the sentence
def bow(sentence, words, show_details=False):
    # tokenize the pattern
    sentence_words = clean_up_sentence(sentence)
    # bag of words
    bag = [0]*len(words)  
    for s in sentence_words:
        for i,w in enumerate(words):
            if w == s: 
                bag[i] = 1
                if show_details:
                    print ("found in bag: %s" % w)

    return(np.array(bag))

def think(sentence, show_details=False):
    x = bow(sentence.lower(), words, show_details)
    if show_details:
        print ("sentence:", sentence, "n bow:", x)
    # input layer is our bag of words
    l0 = x
    # matrix multiplication of input and hidden layer
    l1 = sigmoid(np.dot(l0, synapse_0))
    # output layer
    l2 = sigmoid(np.dot(l1, synapse_1))
    return l2

现在我们对神经网络训练函数进行编码,创造连接权重。别太激动,这主要是矩阵乘法——来自中学数学课堂。

2017-06-03_151025

我们现在准备去构建我们的神经网络 模型,我们将连接权重保存为json文件。

你应该尝试不同的“α”(梯度下降参数),看看它是如何影响错误率。此参数有助于错误调整,并找到最低错误率:

synapse_0 += alpha * synapse_0_weight_update

1-HZ-YQpdBM4hDbh4Q5FcsMA

我们在隐藏层使用了20个神经元,你可以很容易地调整。这些参数将随着于您的训练数据规模的不同而不同,将错误率调整到低于10 ^ – 3是比较合理的。

X = np.array(training)
y = np.array(output)

start_time = time.time()

train(X, y, hidden_neurons=20, alpha=0.1, epochs=100000, dropout=False, dropout_percent=0.2)

elapsed_time = time.time() - start_time
print ("processing time:", elapsed_time, "seconds")

Training with 20 neurons, alpha:0.1, dropout:False 
Input matrix: 12x26    Output matrix: 1x3
delta after 10000 iterations:0.0062613597435
delta after 20000 iterations:0.00428296074919
delta after 30000 iterations:0.00343930779307
delta after 40000 iterations:0.00294648034566
delta after 50000 iterations:0.00261467859609
delta after 60000 iterations:0.00237219554105
delta after 70000 iterations:0.00218521899378
delta after 80000 iterations:0.00203547284581
delta after 90000 iterations:0.00191211022401
delta after 100000 iterations:0.00180823798397
saved synapses to: synapses.json
processing time: 6.501226902008057 seconds

synapse.json文件中包含了全部的连接权重, 这就是我们的模型

1-qYkCgPE3DD26VD-qDwsicA

一旦连接权重已经计算完成,对于分类来说只需要classify()函数了:大约15行代码

备注:如果训练集有变化,我们的模型需要重新计算。对于非常大的数据集,这需要较长的时间。

现在我们可以生成一个句子属于一个或者多个分类的概率了。它的速度非常快,这是因为我们之前定义的think()函数中的点积运算。

# probability threshold
ERROR_THRESHOLD = 0.2
# load our calculated synapse values
synapse_file = 'synapses.json' 
with open(synapse_file) as data_file: 
    synapse = json.load(data_file) 
    synapse_0 = np.asarray(synapse['synapse0']) 
    synapse_1 = np.asarray(synapse['synapse1'])

def classify(sentence, show_details=False):
    results = think(sentence, show_details)

    results = [[i,r] for i,r in enumerate(results) if r>ERROR_THRESHOLD ] 
    results.sort(key=lambda x: x[1], reverse=True) 
    return_results =[[classes[r[0]],r[1]] for r in results]
    print ("%s n classification: %s" % (sentence, return_results))
    return return_results

classify("sudo make me a sandwich")
classify("how are you today?")
classify("talk to you tomorrow")
classify("who are you?")
classify("make me some lunch")
classify("how was your lunch today?")
print()
classify("good day", show_details=True)

sudo make me a sandwich 
 [['sandwich', 0.99917711814437993]]how are you today? 
 [['greeting', 0.99864563257858363]]talk to you tomorrow 
 [['goodbye', 0.95647479275905511]]who are you? 
 [['greeting', 0.8964283843977312]]make me some lunch
 [['sandwich', 0.95371924052636048]]how was your lunch today? 
 [['greeting', 0.99120883810944971], ['sandwich', 0.31626066870883057]]

你可以用其它语句、不同概率来试验几次,也可以添加训练数据来改进/扩展当前的模型。尤其注意用很少的训练数据就得到稳定的预测结果。

有一些句子将会产生多个预测结果(高于阈值)。你需要给你的程序设定一个合适的阈值。并非所有的文本分类方案都是相同的: 一些预测情况比其他预测需要更高的置信水平

最后这个分类结果展示了一些内部的细节:

found in bag: good
found in bag: day
sentence: **good day** 
 bow: [0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
good day 
 [['greeting', 0.99664077655648697]]

从这个句子的词包中可以看到,有两个单词和我们的词库是匹配的。同时我们的神经网络从这些 0 代表的非匹配词语中学习了。

如果提供一个仅仅有一个常用单词 ‘a’ 被匹配的句子,那我们会得到一个低概率的分类结果A:

found in bag: a
sentence: **a burrito! **
 bow: [0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
a burrito! 
 [['sandwich', 0.61776860634647834]]

现在你已经掌握了构建聊天机器人的一些基础知识结构,它能处理大量不同的意图,并且对于有限或者海量的训练数据都能很好的适配。想要为某个意图添加一个或者多个响应实在轻而易举,就不必多讲了。

Enjoy!

用神经网络训练一个文本分类器,首发于 文章 - 伯乐在线


如何做有效的Code Review?我有这些建议

$
0
0

代码评审(Code review)是保证代码质量的一种有效手段,做得好的话,对公司来讲是一笔收益颇高的时间投资。但实践起来往往变成了炫耀编程技能、固执己见、恶言相向、同事关系恶化的事,这该如何是好?

往往代码评审过程中,评审者(Reviewer)往往会过于关心旁枝末节,而忽视主要问题,也就是所谓的自行车棚效应。在批准价值百亿的核电站的建设提案中,专家们往往会浪费大量时间纠结于厂内自行车棚(bikeshed)的颜色;因为核电站太大、太复杂,“专家们”未必真懂,但总不能不说话啊,那就从无关痛痒的自行车棚挑毛病吧。

有效的代码评审

代码评审是开发人员编写的代码由另一个人检查以查找缺陷和改进的过程。换句话说,开发人员大部分都是独立编写代码的,当代码完成之后,他们会召集一次评审。

代码评审是提高软件质量的有效途径。在Google,所有代码都要经过同行评审。引用《 代码大全(第2版)》中的几个例子:

  • IBM 的 Orbit 项目有 50 万行代码,使用了 11 级的检查。它提前交付,并且只有通常预期错误的百分之一。
  • 一份对 AT&T 的一个 200 多人组织的研究报告显示,在引入评审后,生产率提高了14%,缺陷减少了90%。
  • 喷气推进实验室估计,通过早期发现和修复缺陷,每次评审节省约 25,000 美元。

然而,不少团队在有效的代码评审争论中,失去了原本的效益。在功能失调的团队和组织中,对所有参与者来说它可以迅速变成一个 令人不快的经历

  • 它成为评审人员来展示技能的平台。他们在别人的代码中指出“错误”,并强加自己没有价值的“意见”。
  • 在代码完全就绪前,开发人员会非常抗拒别人审查他们的代码 ——可以说这可能是一件好事,但他没有真正理解评审的意义。
  • 开发人员放弃代码所有权,并开始依赖他人查找问题。

在这篇文章中,我将讨论团队和组织可以做的一些事情,让代码评审为所有参与者带来愉快的体验。

对管理层的建议:营造健康文化

有效的代码评审,需要一个重视质量和卓越的健康文化。如果团队不以提供高质量的产品为信仰,代码评审将不会给您所期望的结果。你需要一个人人参与的积极文化—立足于建设性批评,智者胜。

除了创造一个健康文化,并允许花时间和资源进行评审,管理者在代码评审中应保持低姿态。大多数人不想在上司面前暴露自己的秘密,这已是一种文化。代码评审最好由同行进行,管理层不应该询问可以用来评价人的细节。的确有一些管理人员索要检查表和成绩,以便他们可以“衡量”并评价人。

可能你已经有一个健康的文化(算你幸运)。这还不够,营造健康的文化取决于许多因素(团队和组织内部)。这是非常具有挑战性的,没有灵丹妙药。没有正确的文化,代码评审不会带来期望的收获,甚至在极端情况下可能会适得其反。

对个人的建议:换位思考

Karl Wiegers 在他的《Peer Reviews in Software: A Practical Guide》中写道:

产品的作者与评审者之间的互动至关重要。作者必须足够信任和尊重评审者才能接受他们的意见。 同样,评审者必须尊重作者的才华和辛勤工作。评审者应谨慎选择他们用来提出问题的词汇,重点关注他们对产品的观察。说『我没有看到这些变量被初始化 』可能会引发建设性的反馈, 而『你没有初始化这些变量』可能会让作者非常不爽。

关注代码很容易,但不要忘记,桌子(或计算机)的另一端有一个人。他有主见有“自我”。请记住,解决问题的方式有很多。

  • 要谦虚。我既见过非常高效的评审,也见过因为吹毛求疵而非常低效的评审。不要吹毛求疵!!!
  • 确保您有编码标准。编码标准是在组织中共享的人人都认同的一套准则。如果你没有编码标准,那么不要让讨论变成一个比较编码风格的比赛(大括号  { 在同一行还是下一行!)如果你遇到这样的情况,请在编码标准论坛上离线讨论。
  • 学会良好地沟通。你必须能够清楚地表达想法和理由。
  • 编程策略是一个仁者见仁智者见智的问题。评审者和开发人员应该寻求理解彼此的观点,而不应该成为哲学辩论

对评审者的建议:谦虚

  • 开发者不是冤大头。评审的目的不是证明谁是更优秀的程序员而是查找缺陷,并确保代码是简单和可维护的。
  • 问问题。不要提出可能听起来带指责的要求或言论。例如,不要说:『你没有遵循标准XYZ』。更好的方式是真正寻求理解开发者的观点:『你对标准 XYZ 有什么看法,它是否适用于这里?』,这可以引到我的下一个观点。
  • 避免『你为什么』,『你为什么不』风格的问题。它会使人对立。 『为什么把它声明为全局变量?』可以更好地表达为『我不明白为什么这里用一个全局变量』。寻找方法来简化代码。代码评审的目标之一是创建“可维护”软件。
  • 记住要欣赏并感谢对方。人们经常忘记一句简单的『干得好』或『它看起来很棒』的影响有多大。

有些事情,如果看起来像排练过的或以讽刺的语气说出来,就不会奏效。像正常对话一样对待代码评审。你正在聆听他人,应该真正寻求理解他们的观点。需要时提供建议和提示。如果代码很棒,不要强迫找一些消极的来说。

对开发者的建议:它不是个人的事情

  • 不要感情用事。记住,别人说的不是,是代码中的缺陷或不足,而不是你。
  • 意识到你和你的代码是捆绑在一起是正常的事情。如果你为自己的工作感到自豪,那是一个很好的迹象,说明你是一个关心作品的人。
  • 有适当的自我。足够信任和捍卫自己的观点,但又不至于盲目拒绝对方的好建议和意见。
  • 人非圣贤孰能无过。评审者作为第二双眼,可以指出你可能忽略的事情。问题与具体建议一样有价值。
  • 提问题要有针对性。『将所有这些类纳入它们自己的软件包中是否更有意义』
  • 感谢审稿人的时间和他们可能提供的任何反馈。

总结

同行评审是人们互相交流,而无效(ineffectiveness)则源于社会学问题。然而,管理者花费大量时间在操心使用哪些惊艳的工具。工具会有所帮助,但只有工具并不会神奇地带来结果。立足于正确的文化同时…换位思考,以人为本! :-)

如何做有效的Code Review?我有这些建议,首发于 文章 - 伯乐在线

基于 Redis 实现分布式应用限流

$
0
0

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。

前几天在DD的公众号,看了一篇关于使用 瓜娃 实现单应用限流的方案,参考《redis in action》 实现了一个jedis版本的,都属于业务层次限制。 实际场景中常用的限流策略:

  • Nginx接入层限流
    按照一定的规则如帐号、IP、系统调用逻辑等在Nginx层面做限流
  • 业务应用系统限流
    通过业务代码控制流量这个流量可以被称为信号量,可以理解成是一种锁,它可以限制一项资源最多能同时被多少进程访问。

代码实现

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.ZParams;

import java.util.List;
import java.util.UUID;

/**
 *   @email wangiegie@gmail.com
 * @data 2017-08
 */
public class RedisRateLimiter {
    private static final String BUCKET = "BUCKET";
    private static final String BUCKET_COUNT = "BUCKET_COUNT";
    private static final String BUCKET_MONITOR = "BUCKET_MONITOR";

    static String acquireTokenFromBucket(
            Jedis jedis, int limit, long timeout) {
        String identifier = UUID.randomUUID().toString();
        long now = System.currentTimeMillis();
        Transaction transaction = jedis.multi();

        //删除信号量
        transaction.zremrangeByScore(BUCKET_MONITOR.getBytes(), "-inf".getBytes(), String.valueOf(now - timeout).getBytes());
        ZParams params = new ZParams();
        params.weightsByDouble(1.0,0.0);
        transaction.zinterstore(BUCKET, params, BUCKET, BUCKET_MONITOR);

        //计数器自增
        transaction.incr(BUCKET_COUNT);
        List<Object> results = transaction.exec();
        long counter = (Long) results.get(results.size() - 1);

        transaction = jedis.multi();
        transaction.zadd(BUCKET_MONITOR, now, identifier);
        transaction.zadd(BUCKET, counter, identifier);
        transaction.zrank(BUCKET, identifier);
        results = transaction.exec();
        //获取排名,判断请求是否取得了信号量
        long rank = (Long) results.get(results.size() - 1);
        if (rank < limit) {
            return identifier;
        } else {//没有获取到信号量,清理之前放入redis 中垃圾数据
            transaction = jedis.multi();
            transaction.zrem(BUCKET_MONITOR, identifier);
            transaction.zrem(BUCKET, identifier);
            transaction.exec();
        }
        return null;
    }
}

调用

测试接口调用
@GetMapping("/")
public void index(HttpServletResponse response) throws IOException {
    Jedis jedis = jedisPool.getResource();
    String token = RedisRateLimiter.acquireTokenFromBucket(jedis, LIMIT, TIMEOUT);
    if (token == null) {
        response.sendError(500);
    }else{
        //TODO 你的业务逻辑
    }
    jedisPool.returnResource(jedis);
}

优化

使用拦截器 + 注解优化代码

拦截器

@Configuration
static class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    private Logger logger = LoggerFactory.getLogger(WebMvcConfigurer.class);
    @Autowired
    private JedisPool jedisPool;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptorAdapter() {
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                                     Object handler) throws Exception {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Method method = handlerMethod.getMethod();
                RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);

                if (rateLimiter != null){
                    int limit = rateLimiter.limit();
                    int timeout = rateLimiter.timeout();
                    Jedis jedis = jedisPool.getResource();
                    String token = RedisRateLimiter.acquireTokenFromBucket(jedis, limit, timeout);
                    if (token == null) {
                        response.sendError(500);
                        return false;
                    }
                    logger.debug("token -> {}",token);
                    jedis.close();
                }
                return true;
            }
        }).addPathPatterns("/*");
    }
}

定义注解

/**
 *   @email wangiegie@gmail.com
 * @data 2017-08
 * 限流注解
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    int limit() default 5;
    int timeout() default 1000;
}

使用

@RateLimiter(limit = 2, timeout = 5000)
@GetMapping("/test")
public void test() {
}

并发测试

工具:apache-jmeter-3.2
说明: 没有获取到信号量的接口返回500,status是红色,获取到信号量的接口返回200,status是绿色。
当限制请求信号量为2,并发5个线程: image
当限制请求信号量为5,并发10个线程:
image

资料

基于reids + lua的实现

总结

  1. 对于信号量的操作,使用事务操作。
  2. 不要使用时间戳作为信号量的排序分数,因为在分布式环境中,各个节点的时间差的原因,会出现不公平信号量的现象。
  3. 可以使用把这块代码抽成@rateLimiter注解,然后再方法上使用就会很方便啦
  4. 不同接口的流控,可以参考源码的里面RedisRateLimiterPlus,无非是每个接口生成一个监控参数
  5. 源码 http://git.oschina.net/boding1/pig-cloud

基于 Redis 实现分布式应用限流,首发于 文章 - 伯乐在线

手把手教你 Spark 性能调优

$
0
0

0、背景

上周四接到反馈,集群部分 spark 任务执行很慢,且经常出错,参数改来改去怎么都无法优化其性能和解决频繁随机报错的问题。

看了下任务的历史运行情况,平均时间 3h 左右,而且极其不稳定,偶尔还会报错:

 

1、优化思路

任务的运行时间跟什么有关?

(1)数据源大小差异

在有限的计算下,job的运行时长和数据量大小正相关,在本例中,数据量大小基本稳定,可以排除是日志量级波动导致的问题:

(2)代码本身逻辑缺陷

比如代码里重复创建、初始化变量、环境、RDD资源等,随意持久化数据等,大量使用 shuffle 算子等,比如reduceByKey、join等算子。

在这份100行的代码里,一共有 3 次 shuffle 操作,任务被 spark driver 切分成了 4 个 stage 串行执行,代码位置如下:

咱们需要做的就是从算法和业务角度尽可能减少 shuffle 和 stage,提升并行计算性能,这块是个大的话题,本次不展开详述。

(3)参数设置不合理

这块技巧相对通用,咱们来看看之前的核心参数设置:

num-executors=10 || 20 ,executor-cores=1 || 2, executor-memory= 10 || 20,driver-memory=20,spark.default.parallelism=64

假设咱们的 spark 队列资源情况如下:

memory=1T,cores=400

参数怎么设置在这里就有些技巧了,首先得明白 spark 资源的分配和使用原理:

在默认的非动态资源分配场景下, spark 是预申请资源,任务还没起跑就独占资源,一直到整个 job 所有 task 结束,比如你跳板机起了一个 spark-shell 一直没退出,也没执行任务,那也会一直占有所有申请的资源。(如果设置了 num-executors,动态资源分配会失效)

注意上面这句话,spark 的资源使用分配方式和 mapreduce/hive 是有很大差别的,如果不理解这个问题就会在参数设置上引发其它问题。

比如 executor-cores 设多少合适?少了任务并行度不行,多了会把整个队列资源独占耗光,其他同学的任务都无法执行,比如上面那个任务,在 num-executors=20 executor-cores=1 executor-memory= 10 的情况下,会独占20个cores,200G内存,一直持续3个小时。

那针对本case中的任务,结合咱们现有的资源,如何设置这 5 个核心参数呢?

1) executor_cores*num_executors 不宜太小或太大!一般不超过总队列 cores 的 25%,比如队列总 cores 400,最大不要超过100,最小不建议低于 40,除非日志量很小。

2) executor_cores 不宜为1!否则 work 进程中线程数过少,一般 2~4 为宜。

3) executor_memory 一般 6~10g 为宜,最大不超过 20G,否则会导致 GC 代价过高,或资源浪费严重。

4) spark_parallelism 一般为 executor_cores*num_executors 的 1~4 倍,系统默认值 64,不设置的话会导致 task 很多的时候被分批串行执行,或大量 cores 空闲,资源浪费严重。

5) driver-memory 早前有同学设置 20G,其实 driver 不做任何计算和存储,只是下发任务与yarn资源管理器和task交互,除非你是 spark-shell,否则一般 1-2g 就够了。

Spark Memory Manager:

6)spark.shuffle.memoryFraction(默认 0.2) ,也叫 ExecutionMemory。这片内存区域是为了解决 shuffles,joins, sorts and aggregations 过程中为了避免频繁IO需要的buffer。如果你的程序有大量这类操作可以适当调高。

7)spark.storage.memoryFraction(默认0.6),也叫 StorageMemory。这片内存区域是为了解决 block cache(就是你显示调用dd.cache, rdd.persist等方法), 还有就是broadcasts,以及task results的存储。可以通过参数,如果你大量调用了持久化操作或广播变量,那可以适当调高它。

8)OtherMemory,给系统预留的,因为程序本身运行也是需要内存的, (默认为0.2)。Other memory在1.6也做了调整,保证至少有300m可用。你也可以手动设置 spark.testing.reservedMemory . 然后把实际可用内存减去这个reservedMemory得到 usableMemory。 ExecutionMemory 和 StorageMemory 会共享usableMemory * 0.75的内存。0.75可以通过 新参数 spark.memory.fraction 设置。目前spark.memory.storageFraction 默认值是0.5,所以ExecutionMemory,StorageMemory默认情况是均分上面提到的可用内存的。

例如,如果需要加载大的字典文件,可以增大executor中 StorageMemory 的大小,这样就可以避免全局字典换入换出,减少GC,在这种情况下,我们相当于用内存资源来换取了执行效率。

最终优化后的参数如下:

效果如下:

(4)通过执行日志分析性能瓶颈

最后的任务还需要一个小时,那这一个小时究竟耗在哪了?按我的经验和理解,一般单天的数据如果不是太大,不涉及复杂迭代计算,不应该超过半小时才对。

由于集群的 Spark History Server 还没安装调试好,没法通过 spark web UI 查看历史任务的可视化执行细节,所以我写了个小脚本分析了下前后具体的计算耗时信息,可以一目了然的看到是哪个 stage 的问题,有针对性的优化。

可以看到优化后的瓶颈主要在最后写 redis 的阶段,要把 60G 的数据,25亿条结果写入 redis,这对 redis 来说是个挑战,这个就只能从写入数据量和 kv 数据库选型两个角度来优化了。

(5)其它优化角度

当然,优化和高性能是个很泛、很有挑战的话题,除了前面提到的代码、参数层面,还有怎样防止或减少数据倾斜等,这都需要针对具体的场景和日志来分析,此处也不展开。

2、spark 初学者的一些误区

对于初学者来说 spark 貌似无所不能而且高性能,甚至在某些博客、技术人眼里 spark 取代 mapreduce、hive、storm 分分钟的事情,是大数据批处理、机器学习、实时处理等领域的银弹。但事实确实如此吗?

从上面这个 case 可以看到,会用 spark、会调 API 和能用好 spark,用的恰到好处是两码事,这要求咱们不仅了解其原理,还要了解业务场景,将合适的技术方案、工具和合适的业务场景结合——这世上本就不存在什么银弹。。。

说道 spark 的性能,想要它快,就得充分利用好系统资源,尤其是内存和CPU:核心思想就是能用内存 cache 就别 spill 落磁盘,CPU 能并行就别串行,数据能 local 就别 shuffle。

Refer:

[1] spark 内存管理

https://zhangyi.gitbooks.io/spark-in-action/content/chapter2/memory_management.html

[2] Spark Memory解析

https://github.com/ColZer/DigAndBuried/blob/master/spark/spark-memory-manager.md

[3] Spark1.6内存管理模型设计稿-翻译

http://ju.outofmemory.cn/entry/240714

[4] Spark内存管理

http://blog.csdn.net/vegetable_bird_001/article/details/51862422

[5] Apache Spark 内存管理详解

https://www.ibm.com/developerworks/cn/analytics/library/ba-cn-apache-spark-memory-management/index.html

相关文章

如何正确实现 Java 中的 HashCode

$
0
0

相等 和 Hash Code

从一般角度来看,Equality 是不错的,但是 hash code 更则具技巧性。如果我们在 hash code上多下点功夫,我们就能了解到 hash code 就是用在细微处去提升性能的。

大部分的数据结构使用equals去检查是否他们包含一个元素。例如:

List<String> list = Arrays.asList("a", "b", "c");
boolean contains = list.contains("b");

这个变量 contains 是true。因为他们是相等的,虽然b的实例化(instance)虽然不完全一样(再说一次,忽略String interning)。

将传递给 contains 的实例与每个元素进行比较很浪费时间。还好,整个这类数据结构使用了一种更高效的方法。它不会将请求的实例与每个元素比较,而是使用捷径,找到可能与之相等的实例,然后只比较这几项。

这个捷径就是哈希码——从对象计算出来的一个能代表该对象的整数值。与哈希码相同的实例不必相等,但相等的实例一定有相同的哈希码。(或者说应该有,我们稍后会对这个问题进行简单讨论)。这类的数据结构常常使用这种技术命名,在名称中加入 Hash 以便识别,其中最具代表性的就是 HashMap。

一般情况下它们会这样进行:

  • 添加一个元素的时候,使用它的哈希码来计算存放在内部数组(称为桶)中的位置(序号)。
  • 另一个不等同的元素如果具有相同的哈希码,它会被放在同一个桶中,与原来那个放在一起,比如把它们放在一个列表中。
  • 如果传递一个实例给 contains 方法,会先计算它的哈希码来找到桶,只有同一个桶中的元素需要与这个实例进行比较。

使用这种方法实现 contains 的情况很少,在理想的状态下根本不需要 equals 比较。

将 equals、hashCode 定义在 Object 中。

关于哈希的一些思考

如果把 hashCode 作为一种快捷方式取决于其是否相等,那么只有一件事情我们需要关心:相等的对象应该有一致的哈希码。

这也是为什么,如果我们覆写 equals 方法,就必须创建一个匹配的 hashCode 实现!此外,实现 equal 应该是依据我们的实现而实现的,这可能会导致没有相同的哈希码,因为他们使用的是 Object 的实现。

hashCode 约定

从原文档引用:

对于 hashCode 的一般约定:

  • 在 Java 应用程序中,任何时候对同一对象多次调用 hashCode 方法,都必须一直返回同样的整数,对它提供的信息也用于对象的相等比较,且不会被修改。这个整数在两次对同一个应用程序的执行不中不需要保持一致。
  • 如果两个对象通过 equals(Object) 方法来比较相等,那么这两个对象的 hashCode 方法必须产生同样的整型结果。
  • 如果两个对象通过 equals(Object) 方法比较结果不等,这两个对象的 hashCode 不必产生同不整型结果。然而,开发者应该了解对不等的对象产生不同的整型结果有助于提高哈希表的性能。

第一条反映了 equals 的一致性。第二条是我们在上面提到的要求。第三条陈述了我们下面要讨论的一个重要细节。

实现 hashCode

Person.hashCode 有个很简单的实现:

@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}

通过计算相关字段的哈希码,再把这些哈希码组合起来得到 person 的哈希码。它们用 Object 的工具函数 hash 来参与计算。

选择字段

然而什么字段才是相关的?这些要求有助于回答这个问题:如果相等的对象必须有相同的哈希码,那么在计算哈希码的时候就不应该使用那些不用于相等性检查的字段。(否则,如果两个对象只有那些字段不同的话,它们会相等但哈希码不同。)

所以用于计算哈希码的那些字段应该是用于相等性比较的那些字段的子集。默认情况下,它们会使用相同的字段,但有几个细节需要考虑。

一致性

第一是一致性要求。它应该经过非常严格的计算。如果有字段产生了变化,哈希码也应该允许变化(对于可变类来说,这往往是不可避免的),依赖哈希的数据结构并未准备应付这种情况。

正如我们在上面看到的那样,哈希码用于确定一个元素的桶,但是如果哈希相关的字段发生变化,并不会立即重新计算哈希码,而且内部的数组也不会更新。

这就意味着,再对一个相等的对象甚至同一个对象的查询会失败!这个数据结构会计算当前的哈希码,这个哈希码与实例存入时的哈希码并不相同,这直接导致找错了桶。

小结:最好不要用可变的字段来计算哈希码!

性能

哈希码可能最终会在每次调用 equals 的时候计算,这可能正好发生在代码中性能极为关键的部分,所以考虑性能是很有意义的。相比之下 equals 的优化空间就非常小。

除非是使用了复杂的算法,或者使用的字段非常非常多,组合他们哈希码的计算成本可以忽略不计,因为这不可避免。但是应该考虑是否所有字段都需要包含在计算中!尤其应该以审视的眼光来看待集合,例如计算列表和集合中所有元素的哈希码。需要根据不同的情况来考虑是否需要它们参与计算。

如果性能是关键,使用 Object.hash 就可能不是最好的选择,因为它会为可变参数创建数组。

一般的优化原则是:谨慎处理!使用一个公共哈希算法的,可能需要放弃集合,并在分析可能的改进之后进行优化。

碰撞

如果只关注性能,下面这个实例怎么样?

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

毫无疑问,它很快。而且相等的对象会有相同的哈希码,这也让我们觉得不错。还有个亮点,它不涉及可变的字段!

但是,想想我们提到的桶是什么?这种情况下所有实例会被装进同一个桶中!通常这会导致使用一个链表来容纳所有元素,这样的性能太糟糕了——比如,每次执行 contains 都会对列表进行线性扫描。

因此,我们得让每个桶里的内容尽可能的少!一个即使对非常相似的对象计算的哈希码也大不相同的算法,会是一个不错的开始。

如何取得,一定程度上取决于选择的字段。我们用于计算的细节,更多时候是为了生成不同的哈希码。注意,这与我们对性能的想法完全相反。结果很有趣,用太多或者太少字段都会导致性能不佳。

防止碰撞的算法是哈希算法的另一部分。

计算哈希

计算字段的哈希码最简单的办法就是直接调用这个字段的 `hashCode`。可以手工来进行合并。一个公共算法是从任意的某个数开始,让它与另一个数(通常是一个小素数)相乘,再加上一个字段的哈希码,然后重复:

int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
return result;

这有可能造成溢出,但这不是什么大问题,因为在 Java 中不会引发异常。

注意,如果输入数据有着特定的模式,最好的哈希算法都可能出现异常频繁的碰撞。举个简单的例子,假设我们用一个点的 x 坐标和 y 坐标来计算哈希。一开始不太糟,直到我们发现这样一条直线上的点:f(x) = -x,这些点的 x + y = 0。就会发生大量的碰撞!

相关文章

区块链原理最清晰最直观的解释

$
0
0

(A minimal blockchain command-line interface.)

维基百科上对区块链的描述:

维护不断增长的记录(称作区块)的分布式数据库。

听上去很简单,但到底是怎么回事呢?

我们用一款开源命令行界面 Blockchain CLI来详细说明区块链。我也做了一个 浏览器可以访问的在线版

安装命令行界面

首先请确保安装 Node.js

然后在终端里运行下面命令:

npm install blockchain-cli -g
blockchain

你将看到  Welcome to Blockchain CLI!blockchain →提示已准备好接受命令。

区块是什么样子的?

你可以在命令行中输入 blockchainbc来查看你当前的区块链。你将看到下图类似的区块。

  • 索引(区块):这是哪个区块?(初始区块索引为 0)
  • 哈希:区块有效吗?
  • 前个哈希:之前一个区块有效吗?
  • 时间戳:区块什么时候添加的?
  • 数据:区块中存的什么信息?
  • 随机数(Nonce):我们重复了多少次才找到有效的区块?

初始区块

每个区块链都会以一个  Genesis Block作为开始。你接下来将会看到每个区块都关联前一个区块。所以我们开采第一个区块前,要有初始区块。

当一个新的区块被开采出来会发生什么?

让我们来开采我们的第一个区块,在提示框输入 mine freeCodeCamp♥︎命令。 区块链根据最后一个区块生成当前索引和前个哈希。我们现在的区块链最后一个区块就是初始区块。

  • 索引:o+1 = 1
  • 前个哈希:0000018035a828da0…
  • 时间戳:区块什么时候添加的?
  • 数据:freeCodeCamp❤
  • 哈希:??
  • 随机数(Nonce):??

哈希值如何计算?

哈希值是固定长度的数值,用来标识唯一数据。

哈希通过将索引、前个哈希、时间戳、数据、随机数作为输入后计算得出。

CryptoJS.SHA256(index + previousHash + timestamp + data + nonce)

SHA256 算法通过给定的输入,计算出一个唯一的哈希。相同的输入总会生成相同的哈希。

你注意到哈希开头的四个 0 了吗?

开头的四个 0 是有效哈希的基本要求。开头 0 的个数被称为难度值(difficulty)。

function isValidHashDifficulty(hash, difficulty) {
  for (var i = 0, b = hash.length; i < b; i ++) {
      if (hash[i] !== '0') {
          break;
      }
  }
  return i >= difficulty;
}

这就是众所周知的 工作量证明系统(Proof-of-Work)。

什么是随机数?

随机数是用来寻找有效哈希的一个数字。

let nonce = 0;
let hash;
let input;
while(!isValidHashDifficulty(hash)) {     
  nonce = nonce + 1;
  input = index + previousHash + timestamp + data + nonce;
  hash = CryptoJS.SHA256(input)
}

随机数不断迭代,直到哈希有效。在我们的例子中,有效的哈希值至少要四个 0 开头。寻找有效哈希对应随机数的过程就称为开采(挖矿)。

随着难度值的提升,有效哈希的数量逐步减少,我们需要投入更多资源来找到一个有效哈希。

为什么这很重要?

因为它确保了区块链不可变。

如果我们有一个这样的区块链 A → B → C,有人想修改区块 A 上的数据。会发生下面情况:

  1. 修改区块 A 上的数据。
  2. 区块 A 的哈希变动,因为计算哈希所用的数据变化了。
  3. 区块 A 无效,因为它的哈希不是四个 0 开头。
  4. 区块 B 的哈希变动,因为计算区块 B 的哈希所用到的区块 A 的哈希值变化了。
  5. 区块 B 无效,因为它的哈希不是四个 0 开头。
  6. 区块 C 的哈希变动,因为计算区块 C 的哈希所用到的区块 B 的哈希值变化了。
  7. 区块 C 无效,因为它的哈希不是四个 0 开头。

修改一个区块的唯一方式就是重新开采这个区块以及它之后的所有区块。因为新的区块不断增加,基本不可能修改区块链。

我希望本文对你有帮助。

如果你想 checkout 在线版本的例子,移步 http://blockchaindemo.io

区块链原理最清晰最直观的解释,首发于 文章 - 伯乐在线

Web GIS 离线解决方案

$
0
0

1、背景

在离线环境下(局域网中)的GIS系统中如何使用地图?这里的地图主要指的是地图底图,有了底图切片数据,我们就可以看到地图,在上面加上自己的业务数据图层,进行相关操作。

要在离线环境下看到GIS地图,就要有底图切片数据,地图的底图切片数据在一定时间内是不会变化的,可以使用一些地图下载器下载地图切片,如这个 地图下载器

在CS系统中可以基于GMap.Net来做,参考《 百度谷歌离线地图解决方案》。

下面介绍下Web系统如何使用GIS切片数据,开发web GIS系统。

2、使用GeoWebCache发布WMS服务

Geowebcache是基于Java的Web开源项目,主要用于缓存各种WMS数据源的地图瓦片,它实现了多种服务接口,包括WMS-C,WMTS,TMS,KML。

Geowebcache作为一个独立的开源项目,在最近被Geosever的几个版本所集成,主要是对发布的WMS图层建立缓存切片。

服务发布步骤:

1)官网下载 geowebcache-1.8.0-war.zip,直接解压得到geowebcache.war文件,将该文件直接拷贝至tomcat目录下的webapps下即可,启动tomcat会对war包进行解压。

2)修改geowebcache的配置文件geowebcache-core-context.xml。该文件在Tomcat的webapps\geowebcache\WEB-INF下,修改如下:

<bean id="gwcXmlConfig" class="org.geowebcache.config.XMLConfiguration"><constructor-arg ref="gwcAppCtx" /><!--<constructor-arg ref="gwcDefaultStorageFinder" />--><constructor-arg value="D:\\GisMap\\" /><!-- By default GWC will look for geowebcache.xml in {GEOWEBCACHE_CACHE_DIR},
         if not found will look at GEOSEVER_DATA_DIR/gwc/
         alternatively you can specify an absolute or relative path to a directory
         by replacing the gwcDefaultStorageFinder constructor argument above by the directory
         path, like constructor-arg value="/etc/geowebcache"     
    --><property name="template" value="/geowebcache.xml"><description>Set the location of the template configuration file to copy over to the
        cache directory if one doesn't already exist.</description></property></bean>

修改gwcXmlConfig实例化时使用固定路径,该路径可以为任意新建路径文件夹。Geowebcache启动之后会检查此文件夹下是否存在gewebcache.xml文件,如果不存在则按模板新建立并读取使用,如果存在则直接读取使用。

3)修改第2步中的gewebcache.xml文件:

<layers><arcgisLayer><name>ARCGIS-Demo</name><tilingScheme>D:\\GisMap\\Layer\\conf.xml</tilingScheme><tileCachePath>D:\\GisMap\\Layer\\_alllayers</tileCachePath></arcgisLayer></layers>

在layers节点里添加arcgisLayer节点(默认生成的gewebcache.xml的layers节点有许多其他冗余数据,可删除可保留)。Name节点表示待添加图层的名称(这里配置为ARCGIS-Demo),titlingscheme节点为conf.xml文件的路径,tileCachePath为瓦片数据的路径。

4)瓦片地图的准备

其中conf.xml为配置文件,conf.cdi为显示区域约束文件,_alllayers文件夹下则存放了切片数据,Status.gdb为切片状态情况记录(可直接删除)。

通过瓦片下载器下载瓦片地图,然后生成的切片数据_alllayers文件夹:

L01-L10表示地图缩放级数,按照ArcGIS切片目录组织,切片命名规则也和ArcGIS切片数据命名规则一致。(conf.xml、conf.cdi和_alllayers在同级目录)。

5)启动tomcat,继而启动Geowebcache服务,浏览器访问 localhost:8080/geowebcache,如果一切正确的话可以看到下面的页面

该页面简单说明了Geowebcache的一些情况。

点击“A list of all the layers and automatic demos”连接可以看到下面:

该页面显示了geowebcache.xml配置的图层信息。图中可以看到只配置了一个名字为ARCGIS-Demo的图层,使用的EPSG3857坐标系,发布的图片格式为png格式,点击png链接即可看到瓦片地图。

这里地图显示的级别和坐标系配置都来自conf.xml文件。这里的前端js使用的是Openlayers。查看网页源码:

<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="imagetoolbar" content="no"><title>ARCGIS-Demo EPSG:3857_ARCGIS-Demo image/png</title><style type="text/css">
body { font-family: sans-serif; font-weight: bold; font-size: .8em; }
body { border: 0px; margin: 0px; padding: 0px; }
#map { width: 85%; height: 85%; border: 0px; padding: 0px; }</style><script src="../openlayers/OpenLayers.js"></script>    <script type="text/javascript">               
var map, demolayer;                               
  // sets the chosen modifiable parameter        
  function setParam(name, value){                
   str = "demolayer.mergeNewParams({" + name + ": '" + value + "'})" 
   // alert(str);                                   
   eval(str);                                    
  }                                              
OpenLayers.DOTS_PER_INCH = 96.0;
OpenLayers.Util.onImageLoadErrorColor = 'transparent';
function init(){
var mapOptions = { 
resolutions: [156543.033928, 78271.5169639999, 39135.7584820001, 19567.8792409999, 9783.93962049996, 4891.96981024998, 2445.98490512499, 1222.99245256249, 611.49622628138, 305.748113140558, 152.874056570411, 76.4370282850732, 38.2185141425366, 19.1092570712683, 9.55462853563415, 4.77731426794937, 2.38865713397468, 1.19432856685505, 0.597164283559817, 0.298582141647617],
projection: new OpenLayers.Projection('EPSG:3857'),
maxExtent: new OpenLayers.Bounds(-20037508.342787,-20037508.342780996,20037508.342780996,20037508.342787),
units: "meters",
controls: []
};
map = new OpenLayers.Map('map', mapOptions );
map.addControl(new OpenLayers.Control.PanZoomBar({
        position: new OpenLayers.Pixel(2, 15)
}));
map.addControl(new OpenLayers.Control.Navigation());
map.addControl(new OpenLayers.Control.Scale($('scale')));
map.addControl(new OpenLayers.Control.MousePosition({element: $('location')}));
demolayer = new OpenLayers.Layer.WMS("ARCGIS-Demo","../service/wms",
{layers: 'ARCGIS-Demo', format: 'image/png' },
{ tileSize: new OpenLayers.Size(256,256),
 tileOrigin: new OpenLayers.LonLat(-2.0037508342787E7, 2.0037508342787E7)});
map.addLayer(demolayer);
map.zoomToExtent(new OpenLayers.Bounds(-20037497.2108,-19929239.113399997,20037497.2108,18379686.9965));
// The following is just for GetFeatureInfo, which is not cached. Most people do not need this 
map.events.register('click', map, function (e) {
  document.getElementById('nodelist').innerHTML = "Loading... please wait...";
  var params = {
    REQUEST: "GetFeatureInfo",
    EXCEPTIONS: "application/vnd.ogc.se_xml",
    BBOX: map.getExtent().toBBOX(),
    X: e.xy.x,
    Y: e.xy.y,
    INFO_FORMAT: 'text/html',
    QUERY_LAYERS: map.layers[0].params.LAYERS,
    FEATURE_COUNT: 50,
    Layers: 'ARCGIS-Demo',
    Styles: '',
    Srs: 'EPSG:3857',
    WIDTH: map.size.w,
    HEIGHT: map.size.h,
    format: "image/png" };
  OpenLayers.loadURL("../service/wms", params, this, setHTML, setHTML);
  OpenLayers.Event.stop(e);
  });
}
function setHTML(response){
    document.getElementById('nodelist').innerHTML = response.responseText;
};</script></head><body onload="init()"><div id="params"></div><div id="map"></div><div id="nodelist"></div></body></html>

个人比较喜欢leaflet这个GIS javascript库,使用leaflet加载GeoWebCache发布的这个服务:

<!DOCTYPE html><html><head><title>Leaflet - Offline Demo</title><link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" /><script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script></head><body><div id="map" style="height:100vh;" ></div><script type="text/javascript">
    var mapCenter = new L.LatLng(32.1280, 118.7742); //南京
        
    var map = new L.Map('map', {
        center : mapCenter,
        zoom : 4
    });

    var wmsLayer = L.tileLayer.wms("http://localhost:8080/geowebcache/service/wms", {
        layers: 'ARCGIS-Demo',
        format: 'image/png'
    });
    wmsLayer.addTo(map);

    var marker = new L.Marker(mapCenter);
    map.addLayer(marker);
    marker.bindPopup("<p>Hello! ;}</p>").openPopup();</script></body></html>

3、使用自定义的Http服务

GeowebCache本质上就是个Http服务,通过请求参数获取配置文件中的路径中的切片数据,返回给请求方。

我们可以自己写个独立的Http服务,从数据库中读取切片数据返回给请求方。

切片请求地址类似:http://localhost:8899/1818940751/{z}/{x}/{y}

其中“1818940751”是下载器下载的地图类型,z/x/y分别是zoom和地图切片行列号。

前端js使用leaflet加载:

var amapNormalUrl = 'http://localhost:8899/788865972/{z}/{x}/{y}';
var amapNormalLayer = new L.TileLayer(amapNormalUrl, {
    minZoom : 1,
    maxZoom : 18,
    attribution : '高德普通地图'
});

var mapCenter = new L.LatLng(32.1280, 118.7742); //南京
var map = new L.Map('map', {
        center : mapCenter,
        zoom : 9,
        minZoom: 1,
        maxZoom: 18,
        layers : [ amapNormalLayer ]
});

前端js可以自定义投影Projection算法,而国内google地图、高德地图和腾讯地图都是标准的墨卡托投影,可以直接用leaflet加载。

配合一些画图插件,再配合一些后台POI检索服务,如:

使用Lucene索引和检索POI数据

使用Solr进行空间搜索

则能做出如下效果:

总结:介绍了如何使用下载的离线切片数据在局域网环境下发布Web GIS地图服务,前端配合使用一些js插件,实现web下空间数据的检索。

自定义的Http地图服务代码:https://github.com/luxiaoxun/Code4Java

附件:

conf.cdi

<?xml version="1.0" encoding="utf-8" ?><EnvelopeN xsi:type='typens:EnvelopeN' 
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' 
    xmlns:xs='http://www.w3.org/2001/XMLSchema' 
    xmlns:typens='http://www.esri.com/schemas/ArcGIS/10.1'><XMin>-20037497.2108</XMin><YMin>-19929239.113399997</YMin><XMax>20037497.2108</XMax><YMax>18379686.9965</YMax></EnvelopeN>

conf.xml

<?xml version="1.0" encoding="utf-8"?><CacheInfo xsi:type="typens:CacheInfo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:typens="http://www.esri.com/schemas/ArcGIS/10.1"><TileCacheInfo xsi:type="typens:TileCacheInfo"><SpatialReference xsi:type="typens:ProjectedCoordinateSystem"><WKT>PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0],AUTHORITY["EPSG",3857]]</WKT><XOrigin>-20037700</XOrigin><YOrigin>-30241100</YOrigin><XYScale>148923141.92838538</XYScale><ZOrigin>-100000</ZOrigin><ZScale>10000</ZScale><MOrigin>-100000</MOrigin><MScale>10000</MScale><XYTolerance>0.001</XYTolerance><ZTolerance>0.001</ZTolerance><MTolerance>0.001</MTolerance><HighPrecision>true</HighPrecision><WKID>3857</WKID></SpatialReference><TileOrigin xsi:type="typens:PointN"><X>-20037508.342787001</X><Y>20037508.342787001</Y></TileOrigin><TileCols>256</TileCols><TileRows>256</TileRows><DPI>96</DPI><LODInfos xsi:type="typens:ArrayOfLODInfo"><LODInfo xsi:type="typens:LODInfo"><LevelID>0</LevelID><Scale>591657527.591555</Scale><Resolution>156543.03392799999</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>1</LevelID><Scale>295828763.79577702</Scale><Resolution>78271.516963999893</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>2</LevelID><Scale>147914381.89788899</Scale><Resolution>39135.758482000099</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>3</LevelID><Scale>73957190.948944002</Scale><Resolution>19567.879240999901</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>4</LevelID><Scale>36978595.474472001</Scale><Resolution>9783.9396204999593</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>5</LevelID><Scale>18489297.737236001</Scale><Resolution>4891.9698102499797</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>6</LevelID><Scale>9244648.8686180003</Scale><Resolution>2445.9849051249898</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>7</LevelID><Scale>4622324.4343090001</Scale><Resolution>1222.9924525624899</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>8</LevelID><Scale>2311162.2171550002</Scale><Resolution>611.49622628138002</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>9</LevelID><Scale>1155581.108577</Scale><Resolution>305.74811314055802</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>10</LevelID><Scale>577790.55428899999</Scale><Resolution>152.874056570411</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>11</LevelID><Scale>288895.27714399999</Scale><Resolution>76.437028285073197</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>12</LevelID><Scale>144447.638572</Scale><Resolution>38.218514142536598</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>13</LevelID><Scale>72223.819285999998</Scale><Resolution>19.109257071268299</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>14</LevelID><Scale>36111.909642999999</Scale><Resolution>9.5546285356341496</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>15</LevelID><Scale>18055.954822</Scale><Resolution>4.7773142679493699</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>16</LevelID><Scale>9027.9774109999998</Scale><Resolution>2.38865713397468</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>17</LevelID><Scale>4513.9887049999998</Scale><Resolution>1.1943285668550501</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>18</LevelID><Scale>2256.994353</Scale><Resolution>0.59716428355981699</Resolution></LODInfo><LODInfo xsi:type="typens:LODInfo"><LevelID>19</LevelID><Scale>1128.4971760000001</Scale><Resolution>0.29858214164761698</Resolution></LODInfo></LODInfos></TileCacheInfo><TileImageInfo xsi:type="typens:TileImageInfo"><CacheTileFormat>PNG</CacheTileFormat><CompressionQuality>0</CompressionQuality><Antialiasing>false</Antialiasing></TileImageInfo><CacheStorageInfo xsi:type="typens:CacheStorageInfo"><StorageFormat>esriMapCacheStorageModeExploded</StorageFormat><PacketSize>0</PacketSize></CacheStorageInfo></CacheInfo>

参考:

  • http://leafletjs.com/
  • http://leafletjs.com/examples/quick-start/
  • http://www.cnblogs.com/luxiaoxun/p/4454880.html
  • http://www.cnblogs.com/luxiaoxun/p/5020247.html

可能感兴趣的文章

RESTful API 设计最佳实践

$
0
0

项目资源的URL应该如何设计?用名词复数还是用名词单数?一个资源需要多少个URL?用哪种HTTP方法来创建一个新的资源?可选参数应该放在哪里?那些不涉及资源操作的URL呢?实现分页和版本控制的最好方法是什么?因为有太多的疑问,设计RESTful API变得很棘手。在这篇文章中,我们来看一下RESTful API设计,并给出一个最佳实践方案。

每个资源使用两个URL

资源集合用一个URL,具体某个资源用一个URL:

/employees         #资源集合的URL
/employees/56      #具体某个资源的URL

用名词代替动词表示资源

这让你的API更简洁,URL数目更少。不要这么设计:

/getAllEmployees
/getAllExternalEmployees
/createEmployee
/updateEmployee

更好的设计:

GET /employees
GET /employees?state=external
POST /employees
PUT /employees/56

用HTTP方法操作资源

使用URL指定你要用的资源。使用HTTP方法来指定怎么处理这个资源。使用四种HTTP方法POST,GET,PUT,DELETE可以提供CRUD功能(创建,获取,更新,删除)。

  • 获取:使用GET方法获取资源。GET请求从不改变资源的状态。无副作用。GET方法是幂等的。GET方法具有只读的含义。因此,你可以完美的使用缓存。
  • 创建:使用POST创建新的资源。
  • 更新:使用PUT更新现有资源。
  • 删除:使用DELETE删除现有资源。

2个URL乘以4个HTTP方法就是一组很好的功能。看看这个表格:

POST(创建)GET(读取)PUT(更新)DELETE(删除)

/employees创建一个新员工列出所有员工批量更新员工信息删除所有员工
/employees/56(错误)获取56号员工的信息更新56号员工的信息删除56号员工

对资源集合的URL使用POST方法,创建新资源

创建一个新资源的时,客户端与服务器是怎么交互的呢?

在资源集合URL上使用POST来创建新的资源过程

  1. 客户端向资源集合URL /employees发送POST请求。HTTP body 包含新资源的属性 “Albert Stark”。
  2. RESTful Web服务器为新员工生成ID,在其内部模型中创建员工,并向客户端发送响应。这个响应的HTTP头部包含一个Location字段,指示创建资源可访问的URL。

对具体资源的URL使用PUT方法,来更新资源

使用PUT更新已有资源

  1. 客户端向具体资源的URL发送PUT请求 /employee/21。请求的HTTP body中包含要更新的属性值(21号员工的新名称“Bruce Wayne”)。
  2. REST服务器更新ID为21的员工名称,并使用HTTP状态码200表示更改成功。

推荐用复数名词

推荐:

/employees
/employees/21

不推荐:

/employee
/employee/21

事实上,这是个人爱好问题,但复数形式更为常见。此外,在资源集合URL上用GET方法,它更直观,特别是 GET /employees?state=externalPOST /employeesPUT /employees/56。但最重要的是:避免复数和单数名词混合使用,这显得非常混乱且容易出错。

对可选的、复杂的参数,使用查询字符串(?)。

不推荐做法:

GET /employees
GET /externalEmployees
GET /internalEmployees
GET /internalAndSeniorEmployees

为了让你的URL更小、更简洁。为资源设置一个基本URL,将可选的、复杂的参数用查询字符串表示。

GET /employees?state=internal&maturity=senior

使用HTTP状态码

RESTful Web服务应使用合适的HTTP状态码来响应客户端请求

  • 2xx – 成功 – 一切都很好
  • 4xx – 客户端错误 – 如果客户端发生错误(例如客户端发送无效请求或未被授权)
  • 5xx – 服务器错误 – 如果服务器发生错误(例如,尝试处理请求时出错) 参考 维基百科上的HTTP状态代码。但是,其中的大部分HTTP状态码都不会被用到,只会用其中的一小部分。通常会用到一下几个:2xx:成功3xx:重定向4xx:客户端错误5xx:服务器错误
    200 成功301 永久重定向400 错误请求500 内部服务器错误
    201 创建304 资源未修改401未授权
    403 禁止
    404 未找到

返回有用的错误提示

除了合适的状态码之外,还应该在HTTP响应正文中提供有用的错误提示和详细的描述。这是一个例子。 请求:

GET /employees?state=super

响应:

// 400 Bad Request
{"message": "You submitted an invalid state. Valid state values are 'internal' or 'external'","errorCode": 352,"additionalInformation" : "http://www.domain.com/rest/errorcode/352"
}

使用小驼峰命名法

使用小驼峰命名法作为属性标识符。

{ "yearOfBirth": 1982 }

不要使用下划线( year_of_birth)或大驼峰命名法( YearOfBirth)。通常,RESTful Web服务将被JavaScript编写的客户端使用。客户端会将JSON响应转换为JavaScript对象(通过调用 var person = JSON.parse(response)),然后调用其属性。因此,最好遵循JavaScript代码通用规范。
对比:

person.year_of_birth // 不推荐,违反JavaScript代码通用规范
person.YearOfBirth // 不推荐,JavaScript构造方法命名
person.yearOfBirth // 推荐

在URL中强制加入版本号

从始至终,都使用版本号发布您的RESTful API。将版本号放在URL中以是必需的。如果您有不兼容和破坏性的更改,版本号将让你能更容易的发布API。发布新API时,只需在增加版本号中的数字。这样的话,客户端可以自如的迁移到新API,不会因调用完全不同的新API而陷入困境。
使用直观的 “v” 前缀来表示后面的数字是版本号。

/v1/employees

你不需要使用次级版本号(“v1.2”),因为你不应该频繁的去发布API版本。

提供分页信息

一次性返回数据库所有资源不是一个好主意。因此,需要提供分页机制。通常使用数据库中众所周知的参数offset和limit。

/employees?offset=30&limit=15       #返回30 到 45的员工

如果客户端没有传这些参数,则应使用默认值。通常默认值是 offset = 0limit = 10。如果数据库检索很慢,应当减小 limit值。

/employees       #返回0 到 10的员工

此外,如果您使用分页,客户端需要知道资源总数。例: 请求:

GET /employees

响应:

{"offset": 0,"limit": 10,"total": 3465,"employees": [
    //...
  ]
}

非资源请求用动词

有时API调用并不涉及资源(如计算,翻译或转换)。例:

GET /translate?from=de_DE&to=en_US&text=Hallo
GET /calculate?para2=23&para2=432

在这种情况下,API响应不会返回任何资源。而是执行一个操作并将结果返回给客户端。因此,您应该在URL中使用动词而不是名词,来清楚的区分资源请求和非资源请求。

考虑特定资源搜索和跨资源搜索

提供对特定资源的搜索很容易。只需使用相应的资源集合URL,并将搜索字符串附加到查询参数中即可。

GET /employees?query=Paul

如果要对所有资源提供全局搜索,则需要用其他方法。前文提到,对于非资源请求URL,使用动词而不是名词。因此,您的搜索网址可能如下所示:

GET /search?query=Paul   //返回 employees, customers, suppliers 等等.

在响应参数中添加浏览其它API的链接

理想情况下,不会让客户端自己构造使用REST API的URL。让我们思考一个例子。 客户端想要访问员工的薪酬表。为此,他必须知道他可以通过在员工URL(例如 /employees/21/salaryStatements)中附加字符串“salaryStatements”来访问薪酬表。这个字符串连接很容易出错,且难以维护。如果你更改了访问薪水表的REST API的方式(例如变成了 /employees/21/salary-statement/employees/21/paySlips),所有客户端都将中断。 更好的方案是在响应参数中添加一个 links字段,让客户端可以自动变更。
请求:

GET /employees/

响应:

//...
   {"id":1,"name":"Paul","links": [
         {"rel": "salary","href": "/employees/1/salaryStatements"
         }
      ]
   },
//...

如果客户端完全依靠 links中的字段获得薪资表,你更改了API,客户端将始终获得一个有效的URL(只要你更改了 link字段,请求的URL会自动更改),不会中断。另一个好处是,你的API变得可以自我描述,需要写的文档更少。
在分页时,您还可以添加获取下一页或上一页的链接示例。只需提供适当的偏移和限制的链接示例。

GET /employees?offset=20&limit=10

{"offset": 20,"limit": 10,"total": 3465,"employees": [
    //...
  ],"links": [
     {"rel": "nextPage","href": "/employees?offset=30&limit=10"
     },
     {"rel": "previousPage","href": "/employees?offset=10&limit=10"
     }
  ]
}

相关阅读

RESTful API 设计最佳实践,首发于 文章 - 伯乐在线


Netty SSL性能调优

$
0
0

嗯,这篇不长的文章,是一个晚上工作到三点的血泪加班史总结而成。多一个读,可能就少一个人加班。

《 TLS协议分析 与 现代加密通信协议设计》 首先要感谢这篇文章,如果没有它,我可能还要花更多的时间才能完成。文章有点长,能看多少是多少,每句都是收获。

1.背景知识

1.1 协议史

1996: SSL3.0. 写成RFC,开始流行。目前(2015年)已经不安全,必须禁用。

1999: TLS1.0. 互联网标准化组织ISOC接替NetScape公司,发布了SSL的升级版TLS 1.0版。

2006: TLS1.1. 作为 RFC 4346 发布。主要fix了CBC模式相关的如BEAST攻击等漏洞。

2008: TLS1.2. 作为RFC 5246发布 。增进安全性,目前的主流版本。

2015之后: TLS 1.3,还在制订中。

1.2 TLS算法组合

在TLS中,5类算法组合在一起,称为一个CipherSuite:

  • 认证算法
  • 加密算法
  • 消息认证码算法 简称MAC
  • 密钥交换算法
  • 密钥衍生算法

比较常见的算法组合是 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 和  TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 都是ECDHE 做密钥交换,使用RSA做认证,SHA256做PRF算法。

一个使用AES128-CBC做加密算法,用HMAC做MAC。

一个使用AES128-GCM做加密算法,MAC由于GCM作为一种AEAD模式并不需要。

两者的差别,在于 Block Cipher+HMAC 类的算法都爆出了各种漏洞,下一代的TLS v1.3干脆只保留了Authenticated-Encryption 类的算法,主要就是AES-GCM,AEAD模式(Authenticated-Encryption With Addtional data)里Encrypt和MAC直接集成为一个算法,在算法内部解决好安全问题。

1.3 Java 对SSL的支持

JDK7的client端只支持TLS1.0,服务端则支持TLS1.2。

JDK8完全支持TLS1.2。

JDK7不支持GCM算法。

JDK8支持GCM算法,但性能极差极差极差,按Netty的说法:

  •  Java 8u60以前多版本,只能处理1 MB/s。
  •  Java 8u60 开始,10倍的性能提升,10-20 MB/s。
  •  但比起 OpenSSL的 ~200 MB/s,还差着一个量级。

1.4 Netty 对SSL的支持

Netty既支持JDK SSL,也支持Google的boringssl, 这是OpenSSL 的一个fork,更少的代码,更多的功能。

依赖netty-tcnative-boringssl-static-linux-x86_64.jar即可,它里面已包含了相关的so文件,再也不用管Linux里装没装OpenSSL,OpenSSL啥版本了。

2. 性能问题的出现及调优

2.1 性能问题的出现

忘掉前面所有的背景知识,重新来到问题现场:

JDK7的JMeter HTTPS客户端,连接JDK8的Netty服务端时,速度还可以。

JDK8的JMeter HTTPS客户端,则非常慢,非常慢,非常吃客户端的CPU。

按套路,在JMeter端增加启动参数 -Djavax.net.debug=ssl,handshake  debug 握手过程。

(OpenSSL那边这个参数加了没用)

*** ClientHello, TLSv1.2,可以看到,Client端先发起协商,带了一堆可选协议

Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256…]

*** ServerHello, TLSv1.2 然后服务端回选定一个

Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

还可以看到,传输同样的数据,不同客户端/服务端组合下有不同的纪录:

Client: JDK7 JDK SSL + Server: JDK7/8 JDK SSL

**TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA

WRITE: TLSv1 Application Data, length = 32

WRITE: TLSv1 Application Data, length = 304

READ:  TLSv1 Application Data, length = 32

READ:  TLSv1 Application Data, length = 96

READ:  TLSv1 Application Data, length = 32

READ:  TLSv1 Application Data, length = 10336


Client: JDK8 JDK SSL + Server: JDK8 Open SSL

** TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

Thread Group 1-1, WRITE: TLSv1.2 Application Data, length = 300

Thread Group 1-1, READ: TLSv1.2 Application Data, length = 92

Thread Group 1-1, READ: TLSv1.2 Application Data, length = 10337

2.2 原因分析

带着上面的记录,经过一晚的奋战,得出了文章一开始的背景信息,再回头分析就很好理解了,JMeter Https 用的是JDK8 SSL,很不幸的和服务端的OpenSSL协商出一个JDK8实现超慢的TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256。

对于服务端/客户端都是基于Netty + boringssl的RPC框架,使用TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,毕竟更安全。

但Https接口,如果不确定对端的是什么,JDK7 SSL or JDK8 SSL or OpenSSL,为免协商出一个超慢的GCM算法,Server端需要通过配置,才决定要不要把GCM放进可选列表里。

2.3 实现

经过一轮学习,平时是这样写的:

SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())  .sslProvider( SslProvider. OPENSSL).build();

如果想不要开GCM,那把ReferenceCountedOpenSslContext里面的DEFAULT_CIPHERS抄出来,删掉两个GCM的。

List<String> ciphers = Lists.newArrayList(“ECDHE-RSA-AES128-SHA”, “ECDHE-RSA-AES256-SHA”, “AES128-SHA”, “AES256-SHA”, “DES-CBC3-SHA”);

SslContext sslContext = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())                               .sslProvider( SslProvider. OPENSSL).ciphers(ciphers).build();

3 结论

  • OpenSSL(boringssl)在我们的测试用例里,比JDK SSL 快10倍,10倍!!! 所以Netty下尽量都要使用OpenSSL。
  • 在确定两端都使用OpenSSL时,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 仍然是好的,毕竟更安全,也是主流。
  • 对端如果是JDK8 SSL时,Server端要把GCM算法从可选列表里拿掉。

可能感兴趣的文章

一个20秒SQL慢查询优化的经历与处理方案

$
0
0

背景

前几天在项目上线过程中,发现有一个页面无法正确获取数据,经排查原来是接口调用超时,而最后发现是因为SQL查询长达到20多秒而导致了问题的发生。

这里,没有高深的理论或技术,只是备忘一下经历和解读一些思想误区。

复杂SQL语句的构成

这里不过多对业务功能进行描述,但为了突出问题所在,会用类比的语句来描述当时的场景。复杂的SQL语句可以表达如下:

SELECT * FROM a_table AS a 
LEFT JOIN b_table AS b ON a.id=b.id 
WHERE a.id IN (
    SELECT DISTINCT id FROM a_table 
    WHERE user_id IN (100,102,103) GROUP BY user_id HAVING count(id) > 3
)

关联查询

从上面简化的SQL语句,可以看出,首先进行的是关联查询。

子查询

其次,是嵌套的子查询。此子查询是为了找出多个用户共同拥有的组ID。所以语句中的“100,102,103”是根据场景来定的,并且需要和后面“count(id) > 3”的个数对应。简单来说,就是找用户交集的组ID。

耗时在哪?

假设现在a_table表的数据量为20W,而b_table的数据量为2000W。大家可以想一下,你觉得主要的耗时是在关联查询部分,还是在子查询部分?

(思考空间。。。。)

(思考空间。。。。 。。。)

(思考空间。。。。 。。。 。。。)

问题定位

对于SQL底层的原理和高深的理论,我暂时掌握不够深入。但我知道可以通过类比和简单的测试来验证是哪一块环节出了问题。

初步断定

首先,对于只有一个用户ID时,我会把上面的语句简化成:

SELECT * FROM a_table AS a 
LEFT JOIN b_table AS b ON a.id=b.id 
WHERE user_id IN (100)

所以,初步断定应该是嵌套的子查询部分占用了大部分的时间。

再进一步验证

既然定位到了是嵌套的子查询语句的问题,那又要分为两块待排查的区域:是子查询本身耗时大,还是嵌套而导致慢查询?

结果很容易发现,当我把子查询单独在DB中执行时,是非常快的。所以排除。

剩下的不言而喻, 20秒的慢查询是嵌套引起的。

但因为处于上线紧急的过程中,为了确保,我快速地验证了我的结论:

1、将子查询的ID单独执行,并把得到的结果序列手动拼成一段ID,如:1,2,3,4, … , 999

2、将上面得到的序列ID,手动替换到原来的SQL语句

3、执行,发现,很快!只用了约 150 ms

Well Done!  准备修复上线!

解决方案

线上的问题,很多时间都是在定位问题和分析原因,既然问题找到了,原因也找到了,解决方案不言而喻。代码简单处理即可。

另外一个需要注意的点

当前,实际的SQL语句,会比这个更为复杂,但已足以表达问题所在。但在前期,笔者也做了一些SQL的代码。

因为b_table比a_table大,所以一开始 b_table 左关联 a_table 时,很慢,大概是1秒多,而且数据量是很少的;但若反过来,a_table 左关联 b_table 时,则很快,大概是100毫秒。

所以,又发现一个有趣的现象:

大表 左关联 小表,很慢;小表 左关联 大表,很快。

当然,这些我们理论上都知道,但实际开发会忘却。又或者一开始两个表都为空时,而又没考虑到后期这两个表增长的速度时,日后就会埋下坑了。

总结

首先,嵌套的子查询是很慢的。

原因,我还没仔细去研究,但在下班的路上和我的同事交流时,他说曾经看过这方面相关的书籍,是说每一次的子查询都会产生一个SQL语句,所以就N次查询了。而另外一位资深的QA同事则跟我说,应该是M*N的问题。

其次,我一开始使用嵌套子查询,是存在这样一个 误区:我觉得将这些操作交给MySQL自身来处理会更高效,毕竟DB内部会有良好的机制来执行这些查询由。

然后,实际表白,我错了。因为这不是简单的合并MC批量查询。

当我们决定使用一些底层的技术时,只有当我们理解透彻了,才能使用更为恰当。而因为无知就断定工具、框架、底层无所不能时,往往就会中招。

一个20秒SQL慢查询优化的经历与处理方案,首发于 文章 - 伯乐在线

JVM上最快的Bloom filter实现

$
0
0

英文原始出处:  Bloom filter for Scala, the fastest for JVM

本文介绍的是我用Scala实现的Bloom filter。 源代码在 github上。依照 性能测试结果,它是JVM上的 最快的Bloom filter实现。零分配(Zero-allocation)和高度优化的代码。 无内存限制,所以没有包含元素的数量限制和可控的误报率(false positive rate)。
扩展:可插拔的Hash算法,任意的元素类型。
没错,它使用 sun.misc.unsafe

1 介绍

“A Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set. False positive matches are possible, but false negatives are not. In other words, a query returns either “possibly in set” or “definitely not in set”. Elements can be added to the set, but not removed,” says  Wikipedia.

Bloom filter 是由 Howard Bloom 在 1970 年提出的二进制向量数据结构,它具有很好的空间和时间效率,被用来检测一个元素是不是集合中的一个成员。如果检测结果为是,该元素不一定在集合中;但如果检测结果为否,该元素一定不在集合中。因此Bloom filter具有100%的召回率。这样每个检测请求返回有“在集合内(可能错报)”和“不在集合内(绝对不在集合内)”两种情况,可见 Bloom filter 是牺牲了正确率和时间以节省空间。 引自  百度百科

简而言之,Bloom filter是:

  • 优化内存占用, 当整个集合太大而不能全部放到内存中。Optimization for memory. It comes into play when you cannot put whole set into memory.
  • 解决成员存在性的问题。它可以回答下面的问题:一个元素属于一个集合还是不属于?
  • 概率(有损)数据结构。它可以返回一个元素有多大的概率属于一个集合

后面这篇文章介绍的Bloom filter很详尽 -  “What are Bloom filters, and why are they useful?” by  @Max Pagels。我没必要再献丑了,如果你还不熟悉Bloom filter不妨看一看。

2 为何再造轮子?

因为性能或者内存限制的原因,已有的Bloom filter并不能满足我们的需求,或者你发现你可以做的更好。坦率的说,都不是。只不过有时候你厌倦了而已。(作者吐槽,可忽略之)

主要的原因是性能。当开发高性能和低延迟的系统的时候,你可不想被外部的库所拖累,甚至分配了很多的内存。你的注意力应该集中在业务逻辑上,依赖的库应该尽可能的有效。

另一个原因还是内存限制。所有的实现都会因为JVM数组的大小的限制而受限制。JVM中,数字使用整数integer做索引,所以数组的最大长度也就是整数的最大值 2147483647。如果我们创建一个元素类型为long的数组存储比特位bit的值,那么最多我们可以存储64 bit * 2147483647 = 137438953408 bits,大概需要15 GB左右的内存。你可以放入大约10000000000左右的元素到误报率为0.1%的Bloom filter。这对于大部分软件来说足够了,但是当你处理大数据,比如URL,图标广告,实时竞价请求或者是事件流的时候,100亿的数据只是一个起步量。当然你可以有一些变通的办法:部署多个Bloom filter,将它们分布到多个节点,或者设计你的软件适应这些限制,但这些办法并不总是有效,可能花费较高护着不满足你的架构。

让我们看看当前已有的一些Bllom filter的实现。

2.1 Google guava

Guava是Google开发的一个高质量的核心库,它包含集合、基本数据、并发、I/O、Cache等模块。 它也包含一个 Bloom filter实现。Guava是我的初始选择,它经受考验、也很快,但是……

令人咂舌的是,它会额外分配内存。我使用Google的 Allocation Instrumenter监控所有的分配allocation。下面的分配监控显示了检查包含100字符的字符串是否存在于一个Bloom filter中:

I just allocated the object [B@39420d59 of type byte whose size is 40 It's an array of size 23
I just allocated the object java.nio.HeapByteBuffer[pos=0 lim=23 cap=23] of type java/nio/HeapByteBuffer whose size is 48
I just allocated the object com.google.common.hash.Murmur3_128HashFunction$Murmur3_128Hasher@5dd227b7 of type com/google/common/hash/Murmur3_128HashFunction$Murmur3_128Hasher whose size is 48
I just allocated the object [B@3d3b852e of type byte whose size is 24 It's an array of size 1
I just allocated the object [B@14ba7f15 of type byte whose size is 24 It's an array of size 1
I just allocated the object sun.nio.cs.UTF_8$Encoder@55cb3b7 of type sun/nio/cs/UTF_8$Encoder whose size is 56
I just allocated the object [B@497fd334 of type byte whose size is 320 It's an array of size 300
I just allocated the object [B@280c3dc0 of type byte whose size is 312 It's an array of size 296
I just allocated the object java.nio.HeapByteBuffer[pos=0 lim=296 cap=296] of type java/nio/HeapByteBuffer whose size is 48
I just allocated the object [B@6f89ad03 of type byte whose size is 32 It's an array of size 16
I just allocated the object java.nio.HeapByteBuffer[pos=0 lim=16 cap=16] of type java/nio/HeapByteBuffer whose size is 48
I just allocated the object 36db757cdd5ae408ef61dca2406d0d35 of type com/google/common/hash/HashCode$BytesHashCode whose size is 16

一共1016个字节。想象一下,我们计算一个短字符串的hash值,检查它相应的bit位设置已经设置,它需要分配大于1Kb的数据。太多了。那你可能会说内存占用已经很小了,好吧,当你做一个单独的微性能测试的时候,影响不是很大,但是在产品级的环境中,它会变得更糟:它会影响GC,导致分配变慢,触发GC,导致更高的延迟等。

不管怎样,review一下代码会很有趣,有时候你会发现一些复活节彩蛋在里面,比如下面的例子:

这些注释行来自Naughty by Nature说唱组合的歌曲“O.P.P.”,在上世纪90年代早期很流行。这段代码的开发者可能那时是四五十岁的人(偏题了)。

2.2 Twitter Algebird

Algebird “为Scala提供的抽象代数库,这些代码主要是用于建立聚合系统(通过Scalding或Storm)。 它是函数式functional,不可变
immutable, monadic,但是非常非常非常慢,并且仅仅支持字符串作为元素类型。字符串是万能的数据格式,你可硬用它存任何值 :) 。

它使用人人皆爱的MurmurHash3算法,它是最好的通用的hash算法。它计算出128-bit的 hash值,分割成4个32-bit的数字。然后它为每个32-bit的数字设置相应的位,而不是整个的hash值。这是相当有争议的设计,我进行了粗略的测试,测试表明Teitter Bloom filter有超过 10% 的误报率。

更深一步,有趣的是Twitter Bloom filter 底层使用  EWAHCompressedBitmap,它是一个压缩的可替代BitSet的实现。它专门为内存占用而优化,适合稀疏数据的场景。比如,如果你的位数从1000000开始,EWAH可以优化set而不会为前面的0位分配内存。集合的操作如交集、并集和差也更快。但是随机访问却很慢。 而且hash的目标就是有一个均匀分布的hash值,越均匀越好。这两点就排除了使用压缩bitset的好处。我做了一点点测试来检查整个的内存分配,结果显示Twitter Bloom filter比我的实现还要分配更多的内存。 同样,在我看来,Twitter的实现也是相当有争议。

内存检查的结果很长我就不贴了。为包含100个字符的字符串的检查要分配 1808字节,我哭!

同样,它是函数式functional, 不可变immutable, 使用持久化数据结构, monad, 但这些不足以让我们使用它。 大话说在前, 它的读性能要比我的实现慢10倍,写要慢100倍。

2.3 ScalaNLP’s Breeze

Breeze is a generic, clean and powerful Scala numerical processing library… Breeze is a part of ScalaNLP project, a scientific computing platform for Scala

Breeze的介绍看起来很有吸引力,如清爽的新风,但是,有一个 花招在它的实现里。它直接使用对象的hash值。 “WTF,我钟爱的MurmurHash3哪去了”,你可能会问。MurmurHash3仅仅用来计算最终的对象的hash值,没错,它可以和任意类型一起工作,但是你不会知道你的大数据集的细微差别(编者按:较难理解,需要配合代码一起理解。 英文原意为:It’s used only for “finalizing” the object’s hash. Yeah, it works with any type out-of-the-box but if you don’t know that little nuance you are done with large datasets.)

测试中它会分配544字节,看看代码你会发现通用的Scala的问题:

for {
  i <- 0 to numHashFunctions
} yield {
  val h = hash1 + i * hash2
  val nextHash = if (h < 0) ~h else h
  nextHash % numBuckets
}

看起来很简洁:for语句,延迟计算,漂亮的DSL。但是当它编译成Java代码的时候就不那么好看了,它会分配很多对象: intWrapper(), RichInt, Range.Inclusive, VectorBuilder/Vector, boxing/unboxing 等等:

return (IndexedSeq)RichInt$.MODULE$.to$extension0(Predef$.MODULE$.intWrapper(0), numHashFunctions()).map(new Serializable(hash1, hash2) {
    public final int apply(int i)
    {
        return apply$mcII$sp(i);
    }
    public int apply$mcII$sp(int i)
    {
        int h = hash1$1 + i * hash2$1;
        int nextHash = h >= 0 ? h : ~h;
        return nextHash % $outer.numBuckets();
    }
    public final volatile Object apply(Object v1)
    {
        return BoxesRunTime.boxToInteger(apply(BoxesRunTime.unboxToInt(v1)));
    }
    public static final long serialVersionUID = 0L;
    private final BloomFilter $outer;
    private final int hash1$1;
    private final int hash2$1;
    public
    {
        if(BloomFilter.this == null)
        {
            throw null;
        } else
        {
            this.$outer = BloomFilter.this;
            this.hash1$1 = hash1$1;
            this.hash2$1 = hash2$1;
            super();
            return;
        }
    }
}
, IndexedSeq$.MODULE$.canBuildFrom());

震撼吗?我想你被震惊了。接下来看看我的实现。

3 我是如何实现的?

一句话,我重新实现了Bloom filter的数据结构。源代码在 github上,可以通过 maven repository引用:

libraryDependencies += "com.github.alexandrnikitin" %% "bloom-filter" % "0.3.1"

下面是使用的例子:

import bloomfilter.mutable.BloomFilter
val expectedElements = 1000
val falsePositiveRate = 0.1
val bf = BloomFilter[String](expectedElements, falsePositiveRate)
bf.add("some string")
bf.mightContain("some string")
bf.dispose()

3.1 Unsafe

一个重要的设计就是底层使用 sun.misc.unsafe包。使用它分配一块内存来保存bit,所以你需要主动dispose Bloom filter 实例和不受管的内存释放。而且我的实现还使用  usafe做了一些花招以避免内存分配,比如直接访问字符串内部的char数组。

3.2 type class模式

我的实现是可扩展的,你可以为任意类型使用任意的hash算法。它通过 type class模式实现。如果你不熟悉它,你可以阅读 @Daniel Westheide的文章  “The Neophyte’s Guide to Scala”

基本上,你所需的就是实现 CanGenerateHashFrom[From] trait,就像这样:

trait CanGenerateHashFrom[From] {
  def generateHash(from: From): Long
}

不幸的是,它是 invariant不变类型。我想实现为逆变类型contravariant但是Scala编译器不能正确的解决contravariant implicits,将来在 Dotty编译器中会支持。

缺省地提供了一个 MurmurHash3的通用实现。我使用Scala实现了它,比Guava、Algebird、Cassandra的实现更快(希望我没有犯错)。为 LongStringArray[Byte]提供可开箱即用的库。作为一个福利,为无限唯一性(unlimited uniqueness)提供了128bit的版本。

3.3 零分配Zero-allocation

我的Bloom filter实现没有分配任何对象,代码被高度优化。我计划写一篇独立的文章来描述这些优化,敬请关注。通过一系列的 unsafe技巧来实现的。下面是为String类型实现的  CanGenerateHashFrom trait:

implicit object CanGenerateHashFromString extends CanGenerateHashFrom[String] {
  import scala.concurrent.util.Unsafe.{instance => unsafe}
  private val valueOffset = unsafe.objectFieldOffset(classOf[String].getDeclaredField("value"))
  override def generateHash(from: String): Long = {
    val value = unsafe.getObject(from, valueOffset).asInstanceOf[Array[Char]]
    MurmurHash3Generic.murmurhash3_x64_64(value, 0, from.length, 0)
  }
}

使用 unsafe.objectFieldOffset()方法获取String类型的value字段,它是字符串底层的char数组。然后使用 unsafe.getObject()方法访问字符数组,用来计算hash值。

不幸的是,128-bit的实现会分配一个对象。我在 (Long, Long) tuple和  ThreadLocal的字段选择上很犹豫,对于整体的性能,没有影响,有什么意见吗?在我的有生之年我希望能看到 JVM的值类型@Gil TeneObjectLayout尝试实现它。

限制

你可能已经注意到了,当前实现有一些限制。 CanGenerateHashFrom[From] trait是不可变的invariant,它不允许回退到对象的 hashCode()方法。你需要为你的类型实现它的hash算法。但我相信,为了性能这也是值得的。

并不是所有的JVM都支持,因为底层使用了“unsafe” 包,而且这也没有退路(fallback )的实现。

sun.misc.Unsafe至少从2004年Java1.4开始就存在于Java中了。在Java9中,为了提高JVM的可维护性,Unsafe和许多其他的东西一起都被作为内部使用类隐藏起来了。但是究竟是什么取代Unsafe不得而知。 摘自:  http://www.importnew.com/14511.html

可以在Java中用它吗?

可以,但是代码不会和Scala一样漂亮,当然你已经习惯了这一切。Java中没有implicit,而且Java编译器也不会帮你调用它。在Java中使用它很丑但是能工作:

import bloomfilter.CanGenerateHashFrom;
import bloomfilter.mutable.BloomFilter;
long expectedElements = 10000000;
double falsePositiveRate = 0.1;
BloomFilter<byte[]> bf = BloomFilter.apply(
        expectedElements,
        falsePositiveRate,
        CanGenerateHashFrom.CanGenerateHashFromByteArray$.MODULE$);
byte[] element = new byte[100];
bf.add(element);
bf.mightContain(element);
bf.dispose();

4 性能benchmark

我们都喜欢性能基准数据,对不?令人兴奋的数字在空中游荡,是那么的迷人。如果你准备写性能基准的测试,请使用 JMH。 它是Oracle的性能工程师  @Aleksey Shipilev创建的一个微性能基准库: “for building, running, and analyzing nano/micro/milli/macro benchmarks written in Java and other languages targeting the JVM.”,  @Konrad Malawski写了一个 SBT的插件

下面是一个 String类型的基准测试,其它类型的测试结果和此类似:

[info] Benchmark                                              (length)   Mode  Cnt          Score         Error  Units
[info] alternatives.algebird.StringItemBenchmark.algebirdGet      1024  thrpt   20    1181080.172 ▒    9867.840  ops/s
[info] alternatives.algebird.StringItemBenchmark.algebirdPut      1024  thrpt   20     157158.453 ▒     844.623  ops/s
[info] alternatives.breeze.StringItemBenchmark.breezeGet          1024  thrpt   20    5113222.168 ▒   47005.466  ops/s
[info] alternatives.breeze.StringItemBenchmark.breezePut          1024  thrpt   20    4482377.337 ▒   19971.209  ops/s
[info] alternatives.guava.StringItemBenchmark.guavaGet            1024  thrpt   20    5712237.339 ▒  115453.495  ops/s
[info] alternatives.guava.StringItemBenchmark.guavaPut            1024  thrpt   20    5621712.282 ▒  307133.297  ops/s

// My Bloom filter
[info] bloomfilter.mutable.StringItemBenchmark.myGet              1024  thrpt   20   11483828.730 ▒  342980.166  ops/s
[info] bloomfilter.mutable.StringItemBenchmark.myPut              1024  thrpt   20   11634399.272 ▒   45645.105  ops/s
[info] bloomfilter.mutable._128bit.StringItemBenchmark.myGet      1024  thrpt   20   11119086.965 ▒   43696.519  ops/s
[info] bloomfilter.mutable._128bit.StringItemBenchmark.myPut      1024  thrpt   20   11303765.075 ▒   52581.059  ops/s

我的实现大致要比Goole Guava的实现快2倍, 比Twitter Algebird快10 ~ 80倍,其它的benchmark你可以在 github上的“benchmarks’模块找到。

警告:这是在独立环境中的综合测试。通常吞吐率和延迟的差别要比产品环境中要大,因为它会对GC有压力,导致分配很慢,更高的延迟,触发GC等。

5 用在哪里?

高性能和低延迟系统。

大数据和机器学习系统,有巨量唯一的数据。

5.1 什么时候不用它?

如果你当前的解决方案已满足需求,大部分软件都不需要这么快。

你只信任那些大公司如Google、Twitter出品的已被证明的、经受考验的库。

你想要开箱即用的库。

6 下一步

欢迎你的意见和建议。下一步我会实现一个稳定的 (Stable) Bloom filter 数据结构,因为目前没有好的实现。我计划研究一下  Cuckoo filer 数据结构。对此有何经验吗?

相关文章

从Gitlab误删除数据库想到的

$
0
0

昨天,Gitlab.com发生了一个大事,某同学误删了数据库,这个事看似是个低级错误,不过,因为Gitlab把整个过程的细节都全部暴露出来了,所以,可以看到很多东西,而对于类似这样的事情,我自己以前也干过,而在最近的两公司中我也见过(Amazon中见过一次,阿里中见过至少四次),正好通过这个事来说说一下自己的一些感想和观点吧。 我先放个观点:你觉得有备份系统就不会丢数据了吗?

事件回顾

整个事件的回顾Gitlab.com在第一时间就放到了 Google Doc上,事后,又发了 一篇Blog来说明这个事,在这里,我简单的回顾一下这个事件的过程。

首先,一个叫YP的同学在给gitlab的线上数据库做一些负载均衡的工作,在做这个工作时的时候突发了一个情况,Gitlab被DDoS攻击,数据库的使用飙高,在block完攻击者的IP后,发现有个staging的数据库(db2.staging)已经落后生产库4GB的数据,于是YP同学在Fix这个staging库的同步问题的时候,发现db2.staging有各种问题都和主库无法同步,在这个时候,YP同学已经工作的很晚了,在尝试过多个方法后,发现db2.staging都hang在那里,无法同步,于是他想把db2.staging的数据库删除了,这样全新启动一个新的复制,结果呢,删除数据库的命令错误的敲在了生产环境上(db1.cluster),结果导致整个生产数据库被误删除。( 陈皓注:这个失败基本上就是 “工作时间过长” + “在多数终端窗口中切换中迷失掉了”

在恢复的过程中,他们发现只有db1.staging的数据库可以用于恢复,而其它的5种备份机制都不可用,第一个是数据库的同步,没有同步webhook,第二个是对硬盘的快照,没有对数据库做,第三个是用pg_dump的备份,发现版本不对(用9.2的版本去dump 9.6的数据)导致没有dump出数据,第四个S3的备份,完全没有备份上,第五个是相关的备份流程是问题百出的,只有几个粗糙的人肉的脚本和糟糕的文档,也就是说,不但是是人肉的,而且还是完全不可执行的。( 陈皓注:就算是这些备份机制都work,其实也有问题,因为这些备份大多数基本上都是24小时干一次,所以,要从这些备份恢复也一定是是要丢数据的了,只有第一个数据库同步才会实时一些

最终,gitlab从db1.staging上把6个小时前的数据copy回来,结果发现速度非常的慢,备份结点只有60Mbits/S,拷了很长时间( 陈皓注:为什么不把db1.staging给直接变成生产机?因为那台机器的性能很差)。数据现在的恢复了,不过,因为恢复的数据是6小时前的,所以,有如下的数据丢失掉了:

  • 粗略估计,有4613 的项目, 74 forks,  和 350 imports 丢失了;但是,因为Git仓库还在,所以,可以从Git仓库反向推导数据库中的数据,但是,项目中的issues等就完全丢失了。
  • 大约有±4979 提交记录丢失了(陈皓注:估计也可以用git仓库中反向恢复)。
  • 可能有 707  用户丢失了,这个数据来自Kibana的日志。
  • 在1月31日17:20 后的Webhooks 丢失了。

因为Gitlab把整个事件的细节公开了出来,所以,也得到了很多外部的帮助,2nd Quadrant的CTO –  Simon Riggs 在他的blog上也发布文章 Dataloss at Gitlab 给了一些非常不错的建议:

  • 关于PostgreSQL 9.6的数据同步hang住的问题,可能有一些Bug,正在fix中。
  • PostgreSQL有4GB的同步滞后是正常的,这不是什么问题。
  • 正常的停止从结点,会让主结点自动释放WALSender的链接数,所以,不应该重新配置主结点的 max_wal_senders 参数。但是,停止从结点时,主结点的复数连接数不会很快的被释放,而新启动的从结点又会消耗更多的链接数。他认为,Gitlab配置的32个链接数太高了,通常来说,2到4个就足够了。
  • 另外,之前gitlab配置的max_connections=8000太高了,现在降到2000个是合理的。
  • pg_basebackup 会先在主结点上建一个checkpoint,然后再开始同步,这个过程大约需要4分钟。
  • 手动的删除数据库目录是非常危险的操作,这个事应该交给程序来做。推荐使用刚release 的 repmgr
  • 恢复备份也是非常重要的,所以,也应该用相应的程序来做。推荐使用 barman (其支持S3)
  • 测试备份和恢复是一个很重要的过程。

看这个样子,估计也有一定的原因是——Gitlab的同学对PostgreSQL不是很熟悉。

随后,Gitlab在其网站上也开了一系列的issues,其issues列表在这里 Write post-mortem (这个列表可能还会在不断更新中)

从上面的这个列表中,我们可以看到一些改进措施了。挺好的,不过我觉得还不是很够。

相关的思考

因为类似这样的事,我以前也干过(误删除过数据库,在多个终端窗口中迷失掉了自己所操作的机器……),而且我在amazon里也见过一次,在阿里内至少见过四次以上(在阿里人肉运维的误操作的事故是我见过最多的),但是我无法在这里公开分享,私下可以分享。在这里,我只想从非技术和技术两个方面分享一下我的经验和认识。

技术方面

人肉运维

一直以来,我都觉得直接到生产线上敲命令是一种非常不好的习惯。我认为, 一个公司的运维能力的强弱和你上线上环境敲命令是有关的,你越是喜欢上线敲命令你的运维能力就越弱,越是通过自动化来处理问题,你的运维能力就越强。理由如下:

其一,如果说对代码的改动都是一次发布的话,那么,对生产环境的任何改动(包括硬件、操作系统、网络、软件配置……),也都算是一次发布。那么这样的发布就应该走发布系统和发布流程,要被很好的测试、上线和回滚计划。关键是,走发布过程是可以被记录、追踪和回溯的,而在线上敲命令是完全无法追踪的。没人知道你敲了什么命令。

其二,真正良性的运维能力是——人管代码,代码管机器,而不是人管机器。你敲了什么命令没人知道,但是你写个工具做变更线上系统,这个工具干了什么事,看看工具的源码就知道了。

另外、有人说,以后不要用rm了,要用mv,还有人说,以后干这样的事时,一个人干,另一个人在旁边看,还有人说,要有一个checklist的强制流程做线上的变更,还有人说要增加一个权限系统。我觉得,这些虽然可以work,但是依然不好,再由如下:

其一、如果要解决一个事情需要加更多的人来做的事,那这事就做成劳动密集型了。今天我们的科技就是在努力消除人力成本,而不是在增加人力成本。而做为一个技术人员,解决问题的最好方式是努力使用技术手段,而不是使用更多的人肉手段。 人类区别于动物的差别就是会发明和使用现代化的工具,而不是使用更多的人力。另外, 这不仅仅因为是,人都是会有这样或那样的问题(疲惫、情绪化、急燥、冲动……),而机器是单一无脑不知疲惫的,更是因为,机器干活的效率和速度是比人肉高出N多倍的

其二、增加一个权限系统或是别的一个watch dog的系统完全是在开倒车,权限系统中的权限谁来维护和审批?不仅仅是因为多出来的系统需要多出来的维护,关键是这个事就没有把问题解决在root上。除了为社会解决就业问题,别无好处,故障依然会发生,有权限的人一样会误操作。对于Gitlab这个问题,正如2nd Quadrant的CTO建议的那样,你需要的是一个自动化的备份和恢复的工具,而不是一个权限系统。

其三、像使用mv而不rm,搞一个checklist和一个更重的流程,更糟糕。这里的逻辑很简单,因为,1)这些规则需要人去学习和记忆,本质上来说,你本来就不相信人,所以你搞出了一些规则和流程,而这些规则和流程的执行,又依赖于人,换汤不换药,2)另外, 写在纸面上的东西都是不可执行的,可以执行的就是只有程序,所以,为什么不把checklist和流程写成代码呢?(你可能会说程序也会犯错,是的,程序的错误是consistent,而人的错误是inconsistent)

最关键的是, 数据丢失有各种各样的情况,不单单只是人员的误操作,比如,掉电、磁盘损坏、中病毒等等,在这些情况下,你设计的那些想流程、规则、人肉检查、权限系统、checklist等等统统都不管用了,这个时候,你觉得应该怎么做呢?是的,你会发现,你不得不用更好的技术去设计出一个高可用的系统!别无它法。

关于备份

一个系统是需要做数据备份的,但是,你会发现, Gitlab这个事中,就算所有的备份都可用,也不可避免地会有数据的丢失,或是也会有很多问题。理由如下:

1)备份通常来说都是周期性的,所以,如果你的数据丢失了,从你最近的备份恢复数据里,从备份时间到故障时间的数据都丢失了。

2)备份的数据会有版本不兼容的问题。比如,在你上次备份数据到故障期间,你对数据的scheme做了一次改动,或是你对数据做了一些调整,那么,你备份的数据就会和你线上的程序出现不兼容的情况。

3)有一些公司或是银行有灾备的数据中心,但是灾备的数据中心没有一天live过。等真正灾难来临需要live的时候,你就会发现,各种问题让你live不起来。你可以读一读几年前的这篇报道好好感受一下《 以史为鉴 宁夏银行7月系统瘫痪最新解析

所以,在灾难来临的时候,你会发现你所设计精良的“备份系统”或是“灾备系统”就算是平时可以工作,但也会导致数据丢失,而且可能长期不用的备份系统很难恢复(比如应用、工具、数据的版本不兼容等问题)。

我之前写过一篇《 分布式系统的事务处理》,你还记得下面这张图吗?看看 Data Loss 那一行的,在Backups, Master/Slave 和 Master/Master的架构下,都是会丢的。

所以说, 如果你要让你的备份系统随时都可以用,那么你就要让它随时都Live着,而随时都Live着的多结点系统,基本上就是一个分布式的高可用的系统。因为 ,数据丢失的原因有很多种,比如掉电、磁盘损坏、中病毒等等,而那些流程、规则、人肉检查、权限系统、checklist等等都只是让人不要误操作,都不管用,这个时候,你不得不用更好的技术去设计出一个高可用的系统!别无它法。(重要的事,得再说一篇)

另外,你可以参看我的另一篇《 关于高可用系统》,这篇文章中以MySQL为例,数据库的replication也只能达到 两个9。

AWS 的 S3 的的高可用是4个加11个9的持久性(所谓11个9的持久性durability,AWS是这样定义的,如果你存了1万个对象,那么丢一个的时间是1000万年 ),这意味着,不仅仅只是硬盘坏,机器掉电,整个机房挂了,其保证可以承受有两个设施的数据丢失,数据还是可用的。试想,如果你把数据的可用性通过技术做到了这个份上,那么,你还怕被人误删一个结点上的数据吗?

非技术方面

故障反思

一般说来,故障都需要反思,在Amazon,S2以上的故障都需要写COE(Correction of Errors),其中一节就是需要Ask 5 Whys,我发现在Gitlab的故障回顾的blog中第一段中也有说要在今天写个Ask 5 Whys。关于Ask 5 Whys,其实并不是亚马逊的玩法,这还是算一个业内常用的玩法,也就是说不断的为自己为为什么,直到找到问题的概本原因,这会逼着所有的当事人去学习和深究很多东西。在Wikipedia上有相关的词条 5 Whys,其中罗列了14条规则:

  1. 你需要找到正确的团队来完成这个故障反思。
  2. 使用纸或白板而不是电脑。
  3. 写下整个问题的过程,确保每个人都能看懂。
  4. 区别原因和症状。
  5. 特别注意因果关系。
  6. 说明Root Cause以及相关的证据。
  7. 5个为什么的答案需要是精确的。
  8. 寻找问题根源的频,而不是直接跳到结论。
  9. 要基础客观的事实、数据和知识。
  10. 评估过程而不是人。
  11. 千万不要把“人为失误”或是“工作不注意”当成问题的根源。
  12. 培养信任和真诚的气氛和文化。
  13. 不断的问“为什么”直到问题的根源被找到。这样可以保证同一个坑不会掉进去两次。
  14. 当你给出“为什么”的答案时,你应该从用户的角度来回答。

工程师文化

上述的这些观点,其实,我在我的以住的博客中都讲过很多遍了,你可以参看《 什么是工程师文化?》以及《 开发团队的效率》。其实,说白了就是这么一个事—— 如果你是一个技术公司,你就会更多的相信技术而不是管理。相信技术会用技术来解决问题,相信管理,那就只会有制度、流程和价值观来解决问题

这个道理很简单, 数据丢失有各种各样的情况,不单单只是人员的误操作,比如,掉电、磁盘损坏、中病毒等等,在这些情况下,你设计的那些流程、规则、人肉检查、权限系统、checklist等等统统都不管用,这个时候,你觉得应该怎么做呢?是的,你会发现,你不得不用更好的技术去设计出一个高可用的系统!别无它法。(重要的事得说三遍)

事件公开

很多公司基本上都是这样的套路,首先是极力掩盖,如果掩盖不了了就开始撒谎,撒不了谎了,就“文过饰非”、“避重就轻”、“转移视线”。然而,面对危机的最佳方法就是——“多一些真诚,少一些套路”, 所谓的“多一些真诚”的最佳实践就是——“透明公开所有的信息”,Gitlab此次的这个事给大家树立了非常好的榜样。AWS也会把自己所有的故障和细节都批露出来。

事情本来就做错了,而公开所有的细节,会让大众少很多猜测的空间,有利于抵制流言和黑公关,同时,还会赢得大众的理解和支持。看看Gitlab这次还去YouTube上直播整个修复过程,是件很了不起的事,大家可以到他们的blog上看看,对于这样的透明和公开,一片好评。

(全文完)


关注CoolShell微信公众账号可以在手机端搜索文章

(转载本站文章请注明作者和出处 酷 壳 – CoolShell,请勿用于任何商业用途)

——=== 访问 酷壳404页面寻找遗失儿童。 ===——

分布式系统中唯一 ID 的生成方法

$
0
0

本文主要介绍在一个分布式系统中, 怎么样生成全局唯一的 ID

一, 问题描述

在分布式系统存在多个 Shard 的场景中, 同时在各个 Shard 插入数据时, 怎么给这些数据生成全局的 unique ID?

在单机系统中 (例如一个 MySQL 实例), unique ID 的生成是非常简单的, 直接利用 MySQL 自带的自增 ID 功能就可以实现.

但在一个存在多个 Shards 的分布式系统 (例如多个 MySQL 实例组成一个集群, 在这个集群中插入数据), 这个问题会变得复杂, 所生成的全局的 unique ID 要满足以下需求:

  1. 保证生成的 ID 全局唯一
  2. 今后数据在多个 Shards 之间迁移不会受到 ID 生成方式的限制
  3. 生成的 ID 中最好能带上时间信息, 例如 ID 的前 k 位是 Timestamp, 这样能够直接通过对 ID 的前 k 位的排序来对数据按时间排序
  4. 生成的 ID 最好不大于 64 bits
  5. 生成 ID 的速度有要求. 例如, 在一个高吞吐量的场景中, 需要每秒生成几万个 ID (Twitter 最新的峰值到达了 143,199 Tweets/s, 也就是 10万+/秒)
  6. 整个服务最好没有单点

如果没有上面这些限制, 问题会相对简单, 例如:

  1. 直接利用 UUID.randomUUID() 接口来生成 unique ID (http://www.ietf.org/rfc/rfc4122.txt). 但这个方案生成的 ID 有 128 bits, 另外, 生成的 ID 中也没有带 Timestamp
  2. 利用一个中心服务器来统一生成 unique ID. 但这种方案可能存在单点问题; 另外, 要支持高吞吐率的系统, 这个方案还要做很多改进工作 (例如, 每次从中心服务器批量获取一批 IDs, 提升 ID 产生的吞吐率)
  3. Flickr 的做法 (http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/). 但他这个方案 ID 中没有带 Timestamp, 生成的 ID 不能按时间排序

在要满足前面 6 点要求的场景中, 怎么来生成全局 unique ID 呢?

Twitter 的 Snowflake 是一种比较好的做法. 下面主要介绍 Twitter Snowflake, 以及它的变种

二, Twitter Snowflake

https://github.com/twitter/snowflake

Snowflake 生成的 unique ID 的组成 (由高位到低位):

  • 41 bits: Timestamp (毫秒级)
  • 10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits)
  • 12 bits: sequence number

一共 63 bits (最高位是 0)

unique ID 生成过程:

  • 10 bits 的机器号, 在 ID 分配 Worker 启动的时候, 从一个 Zookeeper 集群获取 (保证所有的 Worker 不会有重复的机器号)
  • 41 bits 的 Timestamp: 每次要生成一个新 ID 的时候, 都会获取一下当前的 Timestamp, 然后分两种情况生成 sequence number:
  • 如果当前的 Timestamp 和前一个已生成 ID 的 Timestamp 相同 (在同一毫秒中), 就用前一个 ID 的 sequence number + 1 作为新的 sequence number (12 bits); 如果本毫秒内的所有 ID 用完, 等到下一毫秒继续 (这个等待过程中, 不能分配出新的 ID)
  • 如果当前的 Timestamp 比前一个 ID 的 Timestamp 大, 随机生成一个初始 sequence number (12 bits) 作为本毫秒内的第一个 sequence number

整个过程中, 只是在 Worker 启动的时候会对外部有依赖 (需要从 Zookeeper 获取 Worker 号), 之后就可以独立工作了, 做到了去中心化.

异常情况讨论:

  • 在获取当前 Timestamp 时, 如果获取到的时间戳比前一个已生成 ID 的 Timestamp 还要小怎么办? Snowflake 的做法是继续获取当前机器的时间, 直到获取到更大的 Timestamp 才能继续工作 (在这个等待过程中, 不能分配出新的 ID)

从这个异常情况可以看出, 如果 Snowflake 所运行的那些机器时钟有大的偏差时, 整个 Snowflake 系统不能正常工作 (偏差得越多, 分配新 ID 时等待的时间越久)

从 Snowflake 的官方文档 (https://github.com/twitter/snowflake/#system-clock-dependency) 中也可以看到, 它明确要求 “You should use NTP to keep your system clock accurate”. 而且最好把 NTP 配置成不会向后调整的模式. 也就是说, NTP 纠正时间时, 不会向后回拨机器时钟.

三, Snowflake 的其他变种

Snowflake 有一些变种, 各个应用结合自己的实际场景对 Snowflake 做了一些改动. 这里主要介绍 3 种.

1. Boundary flake

http://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang/

变化:

  • ID 长度扩展到 128 bits:
  • 最高 64 bits 时间戳;
  • 然后是 48 bits 的 Worker 号 (和 Mac 地址一样长);
  • 最后是 16 bits 的 Seq Number
  • 由于它用 48 bits 作为 Worker ID, 和 Mac 地址的长度一样, 这样启动时不需要和 Zookeeper 通讯获取 Worker ID. 做到了完全的去中心化
  • 基于 Erlang

它这样做的目的是用更多的 bits 实现更小的冲突概率, 这样就支持更多的 Worker 同时工作. 同时, 每毫秒能分配出更多的 ID

2. Simpleflake

http://engineering.custommade.com/simpleflake-distributed-id-generation-for-the-lazy/

Simpleflake 的思路是取消 Worker 号, 保留 41 bits 的 Timestamp, 同时把 sequence number 扩展到 22 bits;

Simpleflake 的特点:

  • sequence number 完全靠随机产生 (这样也导致了生成的 ID 可能出现重复)
  • 没有 Worker 号, 也就不需要和 Zookeeper 通讯, 实现了完全去中心化
  • Timestamp 保持和 Snowflake 一致, 今后可以无缝升级到 Snowflake

Simpleflake 的问题就是 sequence number 完全随机生成, 会导致生成的 ID 重复的可能. 这个生成 ID 重复的概率随着每秒生成的 ID 数的增长而增长.

所以, Simpleflake 的限制就是每秒生成的 ID 不能太多 (最好小于 100次/秒, 如果大于 100次/秒的场景, Simpleflake 就不适用了, 建议切换回 Snowflake).

3. instagram 的做法

先简单介绍一下 instagram 的分布式存储方案:

  • 先把每个 Table 划分为多个逻辑分片 (logic Shard), 逻辑分片的数量可以很大, 例如 2000 个逻辑分片
  • 然后制定一个规则, 规定每个逻辑分片被存储到哪个数据库实例上面; 数据库实例不需要很多. 例如, 对有 2 个 PostgreSQL 实例的系统 (instagram 使用 PostgreSQL); 可以使用奇数逻辑分片存放到第一个数据库实例, 偶数逻辑分片存放到第二个数据库实例的规则
  • 每个 Table 指定一个字段作为分片字段 (例如, 对用户表, 可以指定 uid 作为分片字段)
  • 插入一个新的数据时, 先根据分片字段的值, 决定数据被分配到哪个逻辑分片 (logic Shard)
  • 然后再根据 logic Shard 和 PostgreSQL 实例的对应关系, 确定这条数据应该被存放到哪台 PostgreSQL 实例上

instagram unique ID 的组成:

  • 41 bits: Timestamp (毫秒)
  • 13 bits: 每个 logic Shard 的代号 (最大支持 8 x 1024 个 logic Shards)
  • 10 bits: sequence number; 每个 Shard 每毫秒最多可以生成 1024 个 ID

生成 unique ID 时, 41 bits 的 Timestamp 和 Snowflake 类似, 这里就不细说了.

主要介绍一下 13 bits 的 logic Shard 代号 和 10 bits 的 sequence number 怎么生成.

logic Shard 代号:

  • 假设插入一条新的用户记录, 插入时, 根据 uid 来判断这条记录应该被插入到哪个 logic Shard 中.
  • 假设当前要插入的记录会被插入到第 1341 号 logic Shard 中 (假设当前的这个 Table 一共有 2000 个 logic Shard)
  • 新生成 ID 的 13 bits 段要填的就是 1341 这个数字

sequence number 利用 PostgreSQL 每个 Table 上的 auto-increment sequence 来生成:

  • 如果当前表上已经有 5000 条记录, 那么这个表的下一个 auto-increment sequence 就是 5001 (直接调用 PL/PGSQL 提供的方法可以获取到)
  • 然后把 这个 5001 对 1024 取模就得到了 10 bits 的 sequence number

instagram 这个方案的优势在于:

  • 利用 logic Shard 号来替换 Snowflake 使用的 Worker 号, 就不需要到中心节点获取 Worker 号了. 做到了完全去中心化
  • 另外一个附带的好处就是, 可以通过 ID 直接知道这条记录被存放在哪个 logic Shard 上

同时, 今后做数据迁移的时候, 也是按 logic Shard 为单位做数据迁移的, 所以这种做法也不会影响到今后的数据迁移

分布式系统中唯一 ID 的生成方法,首发于 文章 - 伯乐在线

每天自动备份MySQL数据库的shell脚本

$
0
0

经常备份数据库是一个好习惯,虽然数据库损坏或数据丢失的概率很低,但一旦发生这种事情,后悔是没用的。一般网站或应用的后台都有备份数据库的功能按钮,但需要去手工执行。我们需要一种安全的,每天自动备份的方法。下面的这个shell脚本就是能让你通过过设定Crontab来每天备份MySQL数据库的方法。

#!/bin/bash
# 数据库认证
 user=""
 password=""
 host=""
 db_name=""
# 其它
 backup_path="/path/to/your/home/_backup/mysql"
 date=$(date +"%d-%b-%Y")
# 设置导出文件的缺省权限
 umask 177
# Dump数据库到SQL文件
 mysqldump --user=$user --password=$password --host=$host $db_name > $backup_path/$db_name-$date.sql

通过上面的脚本,我们可以每天导出一份sql备份文件,文件的名称按当日日期生成。日积月累,这样的文件会生成很多,有必要定时删除一些老旧的备份的文件,下面的这行命令就是做这个任务的,你可以把它加在上面的脚本后面。

# 删除30天之前的就备份文件
 find $backup_path/* -mtime +30 -exec rm {} \;

我在使用上面的脚本时曾经遇到过一个问题,Crontab定时执行脚本导出没有报错,但导出的是空的SQL文件,但登录到控制台手工执行这个脚本是备份成功的。后来发现是Crontab执行脚本是缺少系统环境信息,找不到 mysqldump,改正的方法是使用 mysqldump全路径就行了。而之所以没有报错信息,是因为 mysqldump把错误信息输出到了 stderr。在命令的后面末尾接 “2>&1” 这样一个信息重定向命令就可以看到错误信息了:

mysqldump -ujoe -ppassword > /tmp/somefile 2>&1

Linux下正确删除海量文件的姿势

$
0
0

这里说的“海量”并不是指体积大,而是指数量,比如一个目录下有数百万个小文件。

最近在优化服务器时发现postfix下的maildrop目录和clientmqueue目录下发现有大量的文件,进入这些目录里使用ls命令是愚蠢的做法,而直接执行 rm *,没有任何反应,文件数量也没有减少,也就是说,在海量文件目录里直接使用rm命令进行删除是无效的。

那么正确的方法是什么呢?有两种方法可选:
第一种:

find /path/to/directory -type f -exec rm {} \;

第二种:

ls -1 /path/to/directory | xargs -I{} rm {}

上面这两种方法可以成功的删除海量文件,速度也很快。但还有一种更好的方法,比如要删除上面提到的clientmqueue目录,里面全部是一个一个的邮件,用下面的方法:

service sendmail stop
cd /var/spool
mv clientmqueue clientmqueue-todelete
mkdir clientmqueue
chown --reference=clientmqueue-todelete clientmqueue
chmod --reference=clientmqueue-todelete clientmqueue
service sendmail start
rm -rf clientmqueue-todelete

上面的方法是将目录重命名,然后使用了 --reference引用参数来重建目录,然后删除重命名的目录。直接删除目录的方法速度是十分的快。也可以留着备份不删。更安全。


一个轻量级分布式 RPC 框架 — NettyRpc

$
0
0

1、背景

最近在搜索Netty和Zookeeper方面的文章时,看到了这篇文章《 轻量级分布式 RPC 框架》,作者用Zookeeper、Netty和Spring写了一个轻量级的分布式RPC框架。花了一些时间看了下他的代码,写的干净简单,写的RPC框架可以算是一个简易版的 dubbo。这个RPC框架虽小,但是麻雀虽小,五脏俱全,有兴趣的可以学习一下。

本人在这个简易版的RPC上添加了如下特性:

  • 服务异步调用的支持,回调函数callback的支持
  • 客户端使用长连接(在多次调用共享连接)
  • 服务端异步多线程处理RPC请求

项目地址:https://github.com/luxiaoxun/NettyRpc

2、简介

RPC,即 Remote Procedure Call(远程过程调用),调用远程计算机上的服务,就像调用本地服务一样。RPC可以很好的解耦系统,如WebService就是一种基于Http协议的RPC。

这个RPC整体框架如下:

这个RPC框架使用的一些技术所解决的问题:

服务发布与订阅:服务端使用Zookeeper注册服务地址,客户端从Zookeeper获取可用的服务地址。

通信:使用Netty作为通信框架。

Spring:使用Spring配置服务,加载Bean,扫描注解。

动态代理:客户端使用代理模式透明化服务调用。

消息编解码:使用Protostuff序列化和反序列化消息。

3、服务端发布服务

使用注解标注要发布的服务

服务注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {
    Class<?> value();
}

一个服务接口:

public interface HelloService {

    String hello(String name);

    String hello(Person person);
}

一个服务实现:使用注解标注

@RpcService(HelloService.class)
public class HelloServiceImpl implements HelloService {

    @Override
    public String hello(String name) {
        return "Hello! " + name;
    }

    @Override
    public String hello(Person person) {
        return "Hello! " + person.getFirstName() + " " + person.getLastName();
    }
}

服务在启动的时候扫描得到所有的服务接口及其实现:

@Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        Map<String, Object> serviceBeanMap = ctx.getBeansWithAnnotation(RpcService.class);
        if (MapUtils.isNotEmpty(serviceBeanMap)) {
            for (Object serviceBean : serviceBeanMap.values()) {
                String interfaceName = serviceBean.getClass().getAnnotation(RpcService.class).value().getName();
                handlerMap.put(interfaceName, serviceBean);
            }
        }
    }

在Zookeeper集群上注册服务地址:

public class ServiceRegistry {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceRegistry.class);

    private CountDownLatch latch = new CountDownLatch(1);

    private String registryAddress;

    public ServiceRegistry(String registryAddress) {
        this.registryAddress = registryAddress;
    }

    public void register(String data) {
        if (data != null) {
            ZooKeeper zk = connectServer();
            if (zk != null) {
                AddRootNode(zk); // Add root node if not exist
                createNode(zk, data);
            }
        }
    }

    private ZooKeeper connectServer() {
        ZooKeeper zk = null;
        try {
            zk = new ZooKeeper(registryAddress, Constant.ZK_SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        latch.countDown();
                    }
                }
            });
            latch.await();
        } catch (IOException e) {
            LOGGER.error("", e);
        }
        catch (InterruptedException ex){
            LOGGER.error("", ex);
        }
        return zk;
    }

    private void AddRootNode(ZooKeeper zk){
        try {
            Stat s = zk.exists(Constant.ZK_REGISTRY_PATH, false);
            if (s == null) {
                zk.create(Constant.ZK_REGISTRY_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException e) {
            LOGGER.error(e.toString());
        } catch (InterruptedException e) {
            LOGGER.error(e.toString());
        }
    }

    private void createNode(ZooKeeper zk, String data) {
        try {
            byte[] bytes = data.getBytes();
            String path = zk.create(Constant.ZK_DATA_PATH, bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            LOGGER.debug("create zookeeper node ({} => {})", path, data);
        } catch (KeeperException e) {
            LOGGER.error("", e);
        }
        catch (InterruptedException ex){
            LOGGER.error("", ex);
        }
    }
}

ServiceRegistry

这里在原文的基础上加了AddRootNode()判断服务父节点是否存在,如果不存在则添加一个PERSISTENT的服务父节点,这样虽然启动服务时多了点判断,但是不需要手动命令添加服务父节点了。

关于Zookeeper的使用原理,可以看这里《 ZooKeeper基本原理》。

4、客户端调用服务

使用代理模式调用服务:

public class RpcProxy {

    private String serverAddress;
    private ServiceDiscovery serviceDiscovery;

    public RpcProxy(String serverAddress) {
        this.serverAddress = serverAddress;
    }

    public RpcProxy(ServiceDiscovery serviceDiscovery) {
        this.serviceDiscovery = serviceDiscovery;
    }

    @SuppressWarnings("unchecked")
    public <T> T create(Class<?> interfaceClass) {
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        RpcRequest request = new RpcRequest();
                        request.setRequestId(UUID.randomUUID().toString());
                        request.setClassName(method.getDeclaringClass().getName());
                        request.setMethodName(method.getName());
                        request.setParameterTypes(method.getParameterTypes());
                        request.setParameters(args);

                        if (serviceDiscovery != null) {
                            serverAddress = serviceDiscovery.discover();
                        }
                        if(serverAddress != null){
                            String[] array = serverAddress.split(":");
                            String host = array[0];
                            int port = Integer.parseInt(array[1]);

                            RpcClient client = new RpcClient(host, port);
                            RpcResponse response = client.send(request);

                            if (response.isError()) {
                                throw new RuntimeException("Response error.",new Throwable(response.getError()));
                            } else {
                                return response.getResult();
                            }
                        }
                        else{
                            throw new RuntimeException("No server address found!");
                        }
                    }
                }
        );
    }
}

这里每次使用代理远程调用服务,从Zookeeper上获取可用的服务地址,通过RpcClient send一个Request,等待该Request的Response返回。这里原文有个比较严重的bug,在原文给出的简单的Test中是很难测出来的,原文使用了obj的wait和notifyAll来等待Response返回,会出现“假死等待”的情况:一个Request发送出去后,在obj.wait()调用之前可能Response就返回了,这时候在channelRead0里已经拿到了Response并且obj.notifyAll()已经在obj.wait()之前调用了,这时候send后再obj.wait()就出现了假死等待,客户端就一直等待在这里。使用CountDownLatch可以解决这个问题。

注意:这里每次调用的send时候才去和服务端建立连接,使用的是短连接,这种短连接在高并发时会有连接数问题,也会影响性能。

从Zookeeper上获取服务地址:

public class ServiceDiscovery {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceDiscovery.class);

    private CountDownLatch latch = new CountDownLatch(1);

    private volatile List<String> dataList = new ArrayList<>();

    private String registryAddress;

    public ServiceDiscovery(String registryAddress) {
        this.registryAddress = registryAddress;
        ZooKeeper zk = connectServer();
        if (zk != null) {
            watchNode(zk);
        }
    }

    public String discover() {
        String data = null;
        int size = dataList.size();
        if (size > 0) {
            if (size == 1) {
                data = dataList.get(0);
                LOGGER.debug("using only data: {}", data);
            } else {
                data = dataList.get(ThreadLocalRandom.current().nextInt(size));
                LOGGER.debug("using random data: {}", data);
            }
        }
        return data;
    }

    private ZooKeeper connectServer() {
        ZooKeeper zk = null;
        try {
            zk = new ZooKeeper(registryAddress, Constant.ZK_SESSION_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        latch.countDown();
                    }
                }
            });
            latch.await();
        } catch (IOException | InterruptedException e) {
            LOGGER.error("", e);
        }
        return zk;
    }

    private void watchNode(final ZooKeeper zk) {
        try {
            List<String> nodeList = zk.getChildren(Constant.ZK_REGISTRY_PATH, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getType() == Event.EventType.NodeChildrenChanged) {
                        watchNode(zk);
                    }
                }
            });
            List<String> dataList = new ArrayList<>();
            for (String node : nodeList) {
                byte[] bytes = zk.getData(Constant.ZK_REGISTRY_PATH + "/" + node, false, null);
                dataList.add(new String(bytes));
            }
            LOGGER.debug("node data: {}", dataList);
            this.dataList = dataList;
        } catch (KeeperException | InterruptedException e) {
            LOGGER.error("", e);
        }
    }
}

ServiceDiscovery

每次服务地址节点发生变化,都需要再次watchNode,获取新的服务地址列表。

5、消息编码

请求消息:

public class RpcRequest {

    private String requestId;
    private String className;
    private String methodName;
    private Class<?>[] parameterTypes;
    private Object[] parameters;

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Class<?>[] getParameterTypes() {
        return parameterTypes;
    }

    public void setParameterTypes(Class<?>[] parameterTypes) {
        this.parameterTypes = parameterTypes;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

RpcRequest

响应消息:

public class RpcResponse {

    private String requestId;
    private String error;
    private Object result;

    public boolean isError() {
        return error != null;
    }

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public Object getResult() {
        return result;
    }

    public void setResult(Object result) {
        this.result = result;
    }
}

RpcResponse

消息序列化和反序列化工具:(基于 Protostuff 实现)

public class SerializationUtil {

    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<>();

    private static Objenesis objenesis = new ObjenesisStd(true);

    private SerializationUtil() {
    }

    @SuppressWarnings("unchecked")
    private static <T> Schema<T> getSchema(Class<T> cls) {
        Schema<T> schema = (Schema<T>) cachedSchema.get(cls);
        if (schema == null) {
            schema = RuntimeSchema.createFrom(cls);
            if (schema != null) {
                cachedSchema.put(cls, schema);
            }
        }
        return schema;
    }

    /**
     * 序列化(对象 -> 字节数组)
     */
    @SuppressWarnings("unchecked")
    public static <T> byte[] serialize(T obj) {
        Class<T> cls = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(cls);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    /**
     * 反序列化(字节数组 -> 对象)
     */
    public static <T> T deserialize(byte[] data, Class<T> cls) {
        try {
            T message = (T) objenesis.newInstance(cls);
            Schema<T> schema = getSchema(cls);
            ProtostuffIOUtil.mergeFrom(data, message, schema);
            return message;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

SerializationUtil

由于处理的是TCP消息,本人加了TCP的粘包处理Handler

channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(65536,0,4,0,0))

消息编解码时开始4个字节表示消息的长度,也就是消息编码的时候,先写消息的长度,再写消息。

6、性能改进

1)服务端请求异步处理

Netty本身就是一个高性能的网络框架,从网络IO方面来说并没有太大的问题。

从这个RPC框架本身来说,在原文的基础上把Server端处理请求的过程改成了多线程异步:

public void channelRead0(final ChannelHandlerContext ctx,final RpcRequest request) throws Exception {
        RpcServer.submit(new Runnable() {
            @Override
            public void run() {
                LOGGER.debug("Receive request " + request.getRequestId());
                RpcResponse response = new RpcResponse();
                response.setRequestId(request.getRequestId());
                try {
                    Object result = handle(request);
                    response.setResult(result);
                } catch (Throwable t) {
                    response.setError(t.toString());
                    LOGGER.error("RPC Server handle request error",t);
                }
                ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture channelFuture) throws Exception {
                        LOGGER.debug("Send response for request " + request.getRequestId());
                    }
                });
            }
        });
    }

Netty 4中的Handler处理在IO线程中,如果Handler处理中有耗时的操作(如数据库相关),会让IO线程等待,影响性能。

2)服务端长连接的管理

客户端保持和服务进行长连接,不需要每次调用服务的时候进行连接,长连接的管理(通过Zookeeper获取有效的地址)。

通过监听Zookeeper服务节点值的变化,动态更新客户端和服务端保持的长连接。这个事情现在放在客户端在做,客户端保持了和所有可用服务的长连接,给客户端和服务端都造成了压力,需要解耦这个实现。

3)客户端请求异步处理

客户端请求异步处理的支持,不需要同步等待:发送一个异步请求,返回Feature,通过Feature的callback机制获取结果。

IAsyncObjectProxy client = rpcClient.createAsync(HelloService.class);
RPCFuture helloFuture = client.call("hello", Integer.toString(i));
String result = (String) helloFuture.get(3000, TimeUnit.MILLISECONDS);

个人觉得该RPC的待改进项:

编码序列化的多协议支持。 

项目持续更新中。

项目地址:https://github.com/luxiaoxun/NettyRpc

参考:

  • 轻量级分布式 RPC 框架:http://my.oschina.net/huangyong/blog/361751
  • 你应该知道的RPC原理:http://www.cnblogs.com/LBSer/p/4853234.html

相关文章

自建一个电话呼叫中心要多少钱?

$
0
0

我十分看不惯任何行业的潜规则行为。自建一个电话呼叫中心的报价是多少钱?没有人敢公开报价。我明说吧,自建一个电话呼叫中心,只需要3万元左右,而且还能更省钱。

这个报价是针对小型企业的,也就是广大人民群众。至于大型企业,它们自己去定制,钱不是问题。

3万元建一个电话呼叫中心,包括什么?包括硬件设备,软件。软件是硬件设备上免费赠送的,不要钱!有了这个呼叫中心,你可以有语音导航功能(也就是按0转人工客服),还有人工客服排队,电话录音。够用了,中小企业没那么多花哨的需求。

其它的什么狗屁工单,CRM,根本没人用,没这个需求,不会有人愿意付费的。这年头,开发一个工单系统CRM系统根本就是几块钱的事,再说,现在的公司哪个没有自己的CRM工作流?没有人愿意为这些鸡肋功能付费,所以很多厂商逮着一个人就漫天报价,10万,20万,100万!就想着坑到一个是一个。

Related posts:

  1. 音频编码的一些笔记
  2. SIP tag 和 Call-ID 的区别
  3. SIP报文Via和Contact的区别
  4. 让他们来告我吧!
  5. 史上最强大的PHP Web面试题(会做就能进百度)

谈一下我们是如何开展code review的

$
0
0

众所周知,代码审查是软件开发过程中十分重要的环节,楼主结合自己的实际工作经验,和大家分享一下在实际工作中代码审查是如何开展的。

笔者水平有限,若有错误和纰漏,还请大家指正。

代码审查的阻力

我想不通公司不同部门对代码审查这项工作的重视程度还是不一样的,对于代码审查的阻力总结了以下几点:

  • 国内的整体环境,国内的公司,尤其是互联网公司,讲究速度致上,软件开发的迭代周期周期短,速度快,因为竞争太大,开发的产品要求快速上线,对代码审查不是很重视,先上线,出了问题再解决。
  • 公司的规模,大公司重视流程,把代码审查作为软件开发中的重要一环,甚至计入考核,不管什么一旦成为制度,开展起来就相对容易了。小公司则不然,尤其是刚起步的,可能觉的代码审查没有必要。
  • 和你的领导有关系,就和上面说的,代码审查如果没有形成制度,如果你的领导是技术出身,明白代码审查的重要性,那么会要求你去做。如果是来自别的领域,可能认识不到它的重要性,觉的代码审查是浪费时间(就和代码重构一个道理)。
  • 个人原因,尤其是刚刚进入公司的员工,大学的软件工程课里面好像是没有介绍代码审查的,就是有,没有实际经验,也体会不到它的重要性,笔者刚入职时就是这么认为的。

代码审查的重要性

说了代码审查工作的开展遇到的阻力,下面说一下为什么代码审查是重要的。

  • 代码审查是保证代码质量的重要手段。软件缺陷可能隐藏在各个地方,测试是发现缺陷的重要方法,但专业的测试人员更多的可能是黑盒测试,他们不去关注代码内部的逻辑,只去关注代码实现的功能,有人说测试代码中的逻辑需要开发人员进行单元测试,一方面,单元测试覆盖率基本上不可能达到100%,另一方面,毕竟是单元测试,测试场景简单,有些复杂的场景有可能会测不到。各种测试完成后,如果还有缺陷,那只能让客户充当我们的“终极测试”了。抱怨会接踵而来,客户满意度会越来越低。所以,我们要想出一切可以使用的方法来进一步提高代码质量的方法,还有代码审查么,测试发现不了的问题,通过代码审查也许你能够发现。
  • 代码审查是熟悉软件架构,了解软件业务逻辑的好方法。学习代码是需要切入点的,一个上百万行代码的系统,从哪里开始着手,只能一个模块一个模块,一个组件一个组件的来熟悉,掌握。实现一个比较大的功能,你应该不会是唯一的开发人员,从系统架构师输出的系统设计,然后到各个团队中技术Lead输出的component级别的设计,到开始实现时,应该会把功能分为不同的模块有不同的开发人员协同实现。这是个学习的机会,不要只局限于自己这部分,为了了解这个大的功能,甚至和这个功能相关的其他已经实现的功能,你同样需要关注其他人的工作。有目的的看代码和漫无目的的浏览效果是不一样的,你已经对新功能有所了解,审查代码之前,你认为代码会怎么写,别人哪里和你想的不一样,旧功能和新功能是如何相互影响的等等,心里怀着问题,你的学习速度会更快,记得更加深刻。
  • 代码审查是你提高自己的好方法。前提是team中有经验丰富的开发人员的存在。也就是大牛,不要错过让他看你代码的机会,不要害怕他会为你写的代码挑出一大堆问题,有人说你自己写的代码就像自己的孩子,见不得别人说半点不字,不要固执,要内心平静的,客观的去看待你所写的代码,发现并解决问题才能提高你自己。也不要错过去review大牛代码的机会,看看大牛写出来的代码是怎样的,你可以取其精华。
  • 代码审查是需要功力的。网上有帖子说程序员的资深与否和工作年限没有必然联系,你是5年工作经验还是一个经验用了5年,这需要你去刻意练习,刚开始reveiew代码的时候你可能不习惯,也可能很痛苦,面对的一屏幕的代码不知如何下眼。但有一句话,如果你觉的内心很舒服,你就是在原地踏步。觉的痛苦说明你是在爬坡,刻意的去联系自己的大脑吧,今天你看一页代码可能用了一个小时,没有发现问题,但是坚持一个月甚至三个月之后,你看一眼就能够发现代码中的缺陷,恭喜你,你的功力加深了。

我们是如何开展代码审查的

好了。罗嗦了半天。下面开始说一下在楼主参与的项目中是如果开展code review的。

第一家公司,是一家国内的大公司,就不说名字了,我所在的部门开发的产品众多,换项目很频繁,我参与的有3,4个吧,开发流程不规范,部门老大没有对代码审查有硬性要求。但带我的老师,也是项目经理(但是主要做技术,所以也可以说是技术经理)是一个非常热衷于技术的人,应该说明白代码review的重要性,我们敏捷团队有4个开发,每次写完代码后,都会进行team review。把代码投到大屏幕上,然后老师带我们去review代码。印象深刻的一次是一个同事着急回家过年,草草把代码就提交走人了,被师傅挑出来很多问题。换了项目和项目经理之后,代码review就不了了之了。

第二家公司,是一个外企,有几十年的历史了,开发流程算是比较规范了,而且分工明确。在这家公司我们的大老板(也就是技术经理的上司)对代码review是有要求的,下面详细说明我们的代码审查是如何一步一步演进的。

  • 第一阶段   team review + TFVC

先简单介绍下我们的版本控制工具:微软的TFVC,代码的branch是按如下图创建的,有一个main branch每个scrum team一个branch,出release之前把各个team的branch merge回main,最后出release branch,release branch上修复的bug也要最终回main。

开始的时候我们是没有peer review的,每两周开一次team review。一个主持人,负责预定会议室,操作visual studio查看最近两周提交的changeset,一个记录员,负责记录发现的问题,相关功能的开发人员负责讲解和解答疑问。最后记录员将review结果记录到wiki中并发送到整个开发部门。

  • 第二阶段 自律TFVC + peer review + team review

记不太清是从哪个visual studio版本开始支持code review了,好像是VS2012。在提交之前每个开发人员需要将代码提交给至少一个人进行review,然后生成一个code review的work item。你需要将这个work item链接到你的changeset中才能check in代码,不然我们公司自定义的policy会发出警告。这些警告是可以被忽略的,然后也能强制提交。前面说过部分老大对code review是很重视的,如何才能检查peer review的结果呢?对,将这些code review的work item数据进行查询,将没有链接work item的changeset过滤出来,然后将结果显示。技术经理和老大一眼就能看到谁没有遵守这个流程。尽管这么做了,开始执行的时候还是有不少的人出现在查询结果中。

说一下自律的问题,公司添加这个查询review结果的措施是手段,只是在某种程度上保证了流程,但目的是什么?目的是需要收到review请求的成员认认真真的review代码,而不是随便的走一下流程就OK。如果你认识到review的重要性,你可能会用心一点吧。

我们的team review 会议依然在进行,和peer review的区别就是peer review只给一个人或者少数的人进行review,而team review 是在整个scrum team间进行。

  • 第三阶段 GIT + peer review + team review

我们的公司虽然历史悠久,但对一些流程的工具和技术还是极力推崇的。大家都知道GIT是非常流行的版本控制工具,visual studio 2012也开始支持GIT,我们也一步一步的 将source code移到了TFS-GIT中。

和TFVC相比,GIT的branch是非常轻量级的,你可以很容易并且快速的创建一个branch。所以我们现在可以将branch进行细分了。TFVC和GIT的代码提交也不一样,TFVC是集中式的,最全的代码放在server上,你需要一个branch的code时要将其check out到本地。每次提交都是把代码从local一次性merge到server,如果出现conflicts,你需要在本地处理然后check in。GIT是分布式的,每个人clone的时候都会把所有分支download到本地,代码提交是通过pull request来进行的,也就是通过branch之间的merge来进行,这一点刚从TFVC转到GIT的时候很难理解。这样就得为每个人创建一个临时branch,注意这个branch在本地和server端同时存在,我们用这个branch开发自己的代码并用这个branch进行merge code。这里的pull request就相当于TFVC中的code review,TFVC你还可以偷懒忽略code review的work item,在这里就是强制性的了,没有pull request,别人不会approve你的代码,你根本就没有方法将你的代码merge到feature branch中。

还有team review会议也是照常进行的。

谈一下我们是如何开展code review的,首发于 文章 - 伯乐在线

欢迎来到后 ASO 时代

$
0
0

6 月 WWDC 上所宣布的「App Store 将迎来大改版」的消息,给 ASO 界砸下了一枚重磅炸弹。虽说 iOS11 要到今年秋季才会正式推送,且正式版面世到大面积使用还需要一定时间,到底会不会迎来一个新的 ASO 时代,目前尚不可知。

为了做好迎接新时代的准备,咱们先来看看苹果砸下的到底是一枚什么样的「炸弹」。

搜索改动还算小

「搜索」入口所带来的可观流量,是我们「做关键词」的立足点。ASOer 的主要工作之一就是,做到当用户搜索相关关键词的时候,我们的 应用会出现在搜索结果中且排名前列

到了 iOS11 之后搜索将会发生哪些变化呢?我们就按照「搜索 -> 应用详情 -> 下载」这条路径来看看。

  1. 搜索入口
    •  搜索入口从右二被挪到到右一的位置
    • 热搜词从 10 个降为 7 个

    虽然官方从未公开过热搜词的筛选算法,但根据长期观察,我们发现 热搜词会受到搜索频次、短期下载次数、社会化分享、用户评分评论和苹果人为干涉等影响

    可以发现除了苹果人为干涉之外,其他几个影响热搜词的因素都是可控的,所以刷榜或是积分墙依然有存在意义,也将无法杜绝。

  2. 搜索结果
    • 应用名不折行,「副标题」可能显示不全
    • 应用名下方默认展示应用所在的次分类(是的每个应用可以设置主分类和次分类)
    • 应用截图展示三张,应用视频可以展示三个

    目前,为了扩张词库、增加关键词权重,我们所谓的副标题其实是在「应用名称」的位置,用连字符与应用名区分开。从本质上来说“企鹅FM-做电台直播, 听有声书情感音乐广播剧”应该都是算作「应用名称」。 在新版搜索结果中设置过副标题的应用名基本显示不全

    还好,此次大改版 新增了“subtitle”字段(注:后文均用 subtitle 表示苹果规定的副标题,以区分人为设置的副标题),也就是 App Store 的「亲生副标题」。如果设置了,它会出现在应用名称下方,也就是上图中应用次分类的位置。subtitle 对于关键词收录和用户查看应用详情页的可能性都会有影响。它似乎和安卓平台上的一句话简介有了相似的作用。

    应用截图二变三、视频一变三,换言之,搜索结果中能传达给用户的信息更多了。听起来是个好事,但多不一定是好,也可能是一个坑。虽然系统升级了,但多数用户的硬件并没有升级,要在 iPhone6 或者 iPhone7 的屏幕上多塞入一张截图,就需要运营和视觉把控好传达的信息。画布没有变大,但能承载的信息变多了,也可以算是一种诱惑吧。

  3. 应用详情页
    • 自然,应用名称显示完整。应用名下方默认显示次分类,有 subtitle 则显示 subtitle
    • What’s new 被放到了第一屏,默认显示前三行
    • 应用详情、评分评论和相关应用依次排列在应用截图之后,相关应用推荐甚至到了最后一屏

    值得注意的是,原来被排在描述之后的 What’s new ,在大改版中突然翻身做主,坐到了黄金位置,虽然不知道官方的意图,但这无疑又是一块可运营的空间,值得思考如何将它变成一个拉新工具。

    其实评分在 What’s new 上方也有,但是用户评论是在第二屏位置。笔者对于描述和用户评论无甚想法,但对被放到了最后一屏的相关应用推荐,就略有担忧。毕竟通过友链还是能引一部分流量的,现在位置被调整到了犄角旮旯,来自于此的流量多少将会受到影响。

 

榜单 Jobs 估计都不认识了

榜单改动虽大,但影响不及搜索。假如幸运地被推荐,很是可以捧着当日新增笑了。

  1. tab 换血
    • 「今天」取代「精品推荐」
    • 「游戏」成为与 APP 同级的入口
    • 「类别」和「排行榜」不再是一级入口

    值得一说的是,「游戏」被升级为一个单独的 tab。笔者认为这可能是苹果在平衡 App Store 的公平性和调整营收力度:其他互联网产品的流量和游戏的流量都不在一个量级上,而游戏 App 所带来的营收也不是其他产品可以拍马追上的。

  2. 每日更新的「今天」
    • 卡片式设计风格
    • 庞大的人工编辑团队
    • 从原来每周更新到每日更新

    目前公认的未来最大流量入口就是「今天」,除了推荐 App 之外,还有专题、文章……这不仅仅是一个卖应用更新应用的杂货铺,是要发展成能看电影吃饭的购物商场,将用户更长久地留在 App Store 中。业界对于上推荐位的普遍看法是,如果 应用中使用到苹果主推的新技术(比如 AR)或者新 API,那么上推荐位的几率将大大提高

  3. 收归了「类别」和「排行榜」的 「APP」
    • 取消「畅销榜」
    • 「付费榜」、「免费榜」和「类别」依次在倒数第二屏到最后一屏的位置
    • 「付费榜」和「免费榜」默认展示前三位,可左右滑动或点右上角「查看全部」查看榜单

    传言取消畅销榜是因为刷榜太多,规则玩崩了,所以苹果直接取消畅销榜让刷榜没得玩。不过这个事情…笔者认为刷榜公司还是能够找到对策的。

    对于不刷榜的我们受到更大影响的可能是「类别」的移动,这一举动相当于从一级入口到了三级入口(毕竟是最后一屏)。来自分类的流量将会受到一定影响,所以更要通过把握搜索来挽回损失的流量。

 

其他

除新增的 subtitle 字段之外,App Store 还新增了「宣传文本」字段,限制 170 字, 可以随时更改不需要审核。成功提交后,这段文字会出现在应用描述之上,应用截图之下,大概第二屏的位置。通常应用截图在第一屏是无法显示完整的,用户大概率上会看到第二屏,也就 很容易看到「宣传文本」

这个新增字段对重运营的产品,可是个好消息。通常一个版本里运营会推好几拨活动,可惜描述不能随时更改,活动也无法同步到 App Store。「宣传文本」的存在,让 运营也能在 App Store 都做上文案推广啦

总结

秋季 iOS11 才会正式推出,到完成市场占有还有挺长一段时间,但 iTunes Connect 已经可以提交这些新字段的内容了,各位 ASOer 做好如何准备准备,相信能够轻松平稳过渡到后 ASO 时代:

  1. 提交新字段「subtitle」,同时兼顾好副标题的展现效果
  2. 可以提供适配三张应用截图/三个应用视频的设计方案
  3. 根据运营节奏更新「宣传文本」字段
  4. 来自榜单和类别的流量可能减少,要抓紧搜索入口,可以从技术手段上争取苹果的推荐位

笔者认为 App Store 的大改版至少看到了官方的两个态度:打击刷榜;强调营收。最终目的都是调整流量。

打击刷榜就好比游戏公司不许外挂了,人民币玩家会不爽,但对于从不用外挂的普通玩家而言,目前还算是好消息。

虽然独立的「游戏」,调整了入口的「类别」和「排行榜」多少都会影响到流量的导向,但新出的 subtitle、「宣传文本」和应用截图展现等等都扩大了运营空间。

 

参考资料

  1. WWDC2017:消失的榜单忧伤了苦逼的ASO
  2. 治大国如烹小鲜,苹果WWDC 2017之后App Store流量怎么玩?
  3. WWDC2017已结束,CP需要关注iTunes Connect开发者后台的重大调整!
  4. 刷榜公司哭了,App Store大改版,必将颠覆iOS的游戏玩法
  5. 【业界观点】苹果App Store大改版的三大疑问
  6. 如何上热搜?揭秘App Store热门搜索
  7. App Store排行榜从首页消失 刷榜还有用么
  8. 苹果App Store史上最大改版背后:刷榜生意难以为继!
  9. 如何评价 WWDC 2017 中发布的 App Store 的改版?
  10. App Store 2.0: New face of Apple App Store (WWDC 2017)

如何理解并正确使用 MySQL 索引

$
0
0

1、概述

索引是存储引擎用于快速查找记录的一种数据结构,通过合理的使用数据库索引可以大大提高系统的访问性能,接下来主要介绍在MySql数据库中索引类型,以及如何创建出更加合理且高效的索引技巧。

注:这里主要针对的是InnoDB存储引擎的B+Tree索引数据结构

2、索引的优点

1、大大减轻了服务器需要扫描的数据量,从而提高了数据的检索速度

2、帮助服务器避免排序和临时表

3、可以将随机I/O变为顺序I/O

3、索引的创建

3.1、主键索引

ALTER TABLE 'table_name' ADD PRIMARY KEY 'index_name' ('column');

3.2、唯一索引

ALTER TABLE 'table_name' ADD UNIQUE 'index_name' ('column');

3.3、普通索引

ALTER TABLE 'table_name' ADD INDEX 'index_name' ('column');

3.4、全文索引

ALTER TABLE 'table_name' ADD FULLTEXT 'index_name' ('column');

3.5、组合索引

ALTER TABLE 'table_name' ADD INDEX 'index_name' ('column1', 'column2', ...);

4、B+Tree的索引规则

创建一个测试的用户表

DROP TABLE IF EXISTS user_test;
CREATE TABLE user_test(
	id int AUTO_INCREMENT PRIMARY KEY,
	user_name varchar(30) NOT NULL,
	sex bit(1) NOT NULL DEFAULT b'1',
	city varchar(50) NOT NULL,
	age int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建一个组合索引: ALTER TABLE user_test ADD INDEX idx_user(user_name , city , age);

4.1、索引有效的查询

4.1.1、全值匹配

全值匹配指的是和索引中的所有列进行匹配,如:以上面创建的索引为例,在where条件后可同时查询(user_name,city,age)为条件的数据。

注:与where后查询条件的顺序无关,这里是很多同学容易误解的一个地方

SELECT * FROM user_test WHERE user_name = 'feinik' AND age = 26 AND city = '广州';

4.1.2、匹配最左前缀

匹配最左前缀是指优先匹配最左索引列,如:上面创建的索引可用于查询条件为:(user_name )、(user_name, city)、(user_name , city , age)

注:满足最左前缀查询条件的顺序与索引列的顺序无关,如:(city, user_name)、(age, city, user_name)

4.1.3、匹配列前缀

指匹配列值的开头部分,如:查询用户名以feinik开头的所有用户

SELECT * FROM user_test WHERE user_name LIKE 'feinik%';

4.1.4、匹配范围值

如:查询用户名以feinik开头的所有用户,这里使用了索引的第一列

SELECT * FROM user_test WHERE user_name LIKE 'feinik%';

4.2、索引的限制

1、where查询条件中不包含索引列中的最左索引列,则无法使用到索引查询,如:

SELECT * FROM user_test WHERE city = '广州';

SELECT * FROM user_test WHERE age= 26;

SELECT * FROM user_test WHERE city = '广州' AND age = '26';

2、即使where的查询条件是最左索引列,也无法使用索引查询用户名以feinik结尾的用户

SELECT * FROM user_test WHERE user_name like '%feinik';

3、如果where查询条件中有某个列的范围查询,则其右边的所有列都无法使用索引优化查询,如:

SELECT * FROM user_test WHERE user_name = 'feinik' AND city LIKE '广州%' AND age = 26;

5、高效的索引策略

5.1、索引列不能是表达式的一部分,也不能作为函数的参数,否则无法使用索引查询。

SELECT * FROM user_test WHERE user_name = concat(user_name, ‘fei’);

5.2、前缀索引

有时候需要索引很长的字符列,这会增加索引的存储空间以及降低索引的效率,一种策略是可以使用哈希索引,还有一种就是可以使用前缀索引,前缀索引是选择字符列的前n个字符作为索引,这样可以大大节约索引空间,从而提高索引效率。

5.2.1、前缀索引的选择性

前缀索引要选择足够长的前缀以保证高的选择性,同时又不能太长,我们可以通过以下方式来计算出合适的前缀索引的选择长度值:

(1)

SELECT COUNT(DISTINCT index_column)/COUNT(*) FROM table_name; -- index_column代表要添加前缀索引的列

注:通过以上方式来计算出前缀索引的选择性比值,比值越高说明索引的效率也就越高效。

(2)

SELECT

COUNT(DISTINCT LEFT(index_column,1))/COUNT(*),

COUNT(DISTINCT LEFT(index_column,2))/COUNT(*),

COUNT(DISTINCT LEFT(index_column,3))/COUNT(*)

...

FROM table_name;

注:通过以上语句逐步找到最接近于(1)中的前缀索引的选择性比值,那么就可以使用对应的字符截取长度来做前缀索引了

5.2.2、前缀索引的创建

ALTER TABLE table_name ADD INDEX index_name (index_column(length));

5.2.3、使用前缀索引的注意点

前缀索引是一种能使索引更小,更快的有效办法,但是MySql无法使用前缀索引做ORDER BY 和 GROUP BY以及使用前缀索引做覆盖扫描。

5.3、选择合适的索引列顺序

在组合索引的创建中索引列的顺序非常重要,正确的索引顺序依赖于使用该索引的查询方式,对于组合索引的索引顺序可以通过经验法则来帮助我们完成:将选择性最高的列放到索引最前列,该法则与前缀索引的选择性方法一致,但并不是说所有的组合索引的顺序都使用该法则就能确定,还需要根据具体的查询场景来确定具体的索引顺序。

5.4 聚集索引与非聚集索引

1、聚集索引

聚集索引决定数据在物理磁盘上的物理排序,一个表只能有一个聚集索引,如果定义了主键,那么InnoDB会通过主键来聚集数据,如果没有定义主键,InnoDB会选择一个唯一的非空索引代替,如果没有唯一的非空索引,InnoDB会隐式定义一个主键来作为聚集索引。

聚集索引可以很大程度的提高访问速度,因为聚集索引将索引和行数据保存在了同一个B-Tree中,所以找到了索引也就相应的找到了对应的行数据,但在使用聚集索引的时候需注意避免随机的聚集索引(一般指主键值不连续,且分布范围不均匀),如使用UUID来作为聚集索引性能会很差,因为UUID值的不连续会导致增加很多的索引碎片和随机I/O,最终导致查询的性能急剧下降。

2、非聚集索引

与聚集索引不同的是非聚集索引并不决定数据在磁盘上的物理排序,且在B-Tree中包含索引但不包含行数据,行数据只是通过保存在B-Tree中的索引对应的指针来指向行数据,如:上面在(user_name,city, age)上建立的索引就是非聚集索引。

5.5、覆盖索引

如果一个索引(如:组合索引)中包含所有要查询的字段的值,那么就称之为覆盖索引,如:

SELECT user_name, city, age FROM user_test WHERE user_name = 'feinik' AND age > 25;

因为要查询的字段(user_name, city, age)都包含在组合索引的索引列中,所以就使用了覆盖索引查询,查看是否使用了覆盖索引可以通过执行计划中的Extra中的值为Using index则证明使用了覆盖索引,覆盖索引可以极大的提高访问性能。

5.6、如何使用索引来排序

在排序操作中如果能使用到索引来排序,那么可以极大的提高排序的速度,要使用索引来排序需要满足以下两点即可。

  • 1、ORDER BY子句后的列顺序要与组合索引的列顺序一致,且所有排序列的排序方向(正序/倒序)需一致
  • 2、所查询的字段值需要包含在索引列中,及满足覆盖索引

通过例子来具体分析

在user_test表上创建一个组合索引

ALTER TABLE user_test ADD INDEX index_user(user_name , city , age);

可以使用到索引排序的案例

1、SELECT user_name, city, age FROM user_test ORDER BY user_name;

2、SELECT user_name, city, age FROM user_test ORDER BY user_name, city;

3、SELECT user_name, city, age FROM user_test ORDER BY user_name DESC, city DESC;

4、SELECT user_name, city, age FROM user_test WHERE user_name = 'feinik' ORDER BY city;

注:第4点比较特殊一点,如果where查询条件为索引列的第一列,且为常量条件,那么也可以使用到索引

无法使用索引排序的案例

1、sex不在索引列中

SELECT user_name, city, age FROM user_test ORDER BY user_name, sex;

2、排序列的方向不一致

SELECT user_name, city, age FROM user_test ORDER BY user_name ASC, city DESC;

3、所要查询的字段列sex没有包含在索引列中

SELECT user_name, city, age, sex FROM user_test ORDER BY user_name;

4、where查询条件后的user_name为范围查询,所以无法使用到索引的其他列

SELECT user_name, city, age FROM user_test WHERE user_name LIKE 'feinik%' ORDER BY city;

5、多表连接查询时,只有当ORDER BY后的排序字段都是第一个表中的索引列(需要满足以上索引排序的两个规则)时,方可使用索引排序。如:再创建一个用户的扩展表user_test_ext,并建立uid的索引。

DROP TABLE IF EXISTS user_test_ext;

CREATE TABLE user_test_ext(

    id int AUTO_INCREMENT PRIMARY KEY,

    uid int NOT NULL,

    u_password VARCHAR(64) NOT NULL

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

ALTER TABLE user_test_ext ADD INDEX index_user_ext(uid);

走索引排序

SELECT user_name, city, age FROM user_test u LEFT JOIN user_test_ext ue ON u.id = ue.uid ORDER BY u.user_name;

不走索引排序

SELECT user_name, city, age FROM user_test u LEFT JOIN user_test_ext ue ON u.id = ue.uid ORDER BY ue.uid;

6、总结

本文主要讲了B+Tree树结构的索引规则,不同索引的创建,以及如何正确的创建出高效的索引技巧来尽可能的提高查询速度,当然了关于索引的使用技巧不单单只有这些,关于索引的更多技巧还需平时不断的积累相关经验。

如何理解并正确使用 MySQL 索引,首发于 文章 - 伯乐在线

Viewing all 330 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>