可靠传输协议

协议

需要考虑底层信道的可靠性。

底层信道完全可靠


不需要考虑任何传输错误的情况,只需要向信道传输消息即可

底层信道可能出现bit差错(不会丢失分组)


这是一个停等协议
首先需要差错检验,传输方会计算检验和,并附加到传输数据上;接受房会计算并比较检验和,看数据是否正确。
其次,接收方需要反馈是否正确接收到数据。接受房接到数据后,会反馈发送方数据是否有问题,有问题发送一个NAK,没有问题发送一个ACK。当传输方收到NAK时,将会重传数据。
问题
接收到的NAK和ACK可能受损,有两个解决办法,第一个,使用足够多的bit为做检验和可以恢复bit差错。第一个,如果不使用可恢复bit差错的检验和,则需要重传分组,但这样会导致接收方不确定是新的包还是重传的包(ack/nak受损)。

改进的停等协议



当传输方检查到ack/nak受损后重传输分组。并使用0/1编号,当接收方重复收到编号相同的分组时,就知道这是一次重传。

底层信道可能出现bit差错,并丢包

在上面协议的基础上,需要考虑如何解决丢包的问题
可以考虑用定时器解决,传输方发送一个包之后启动一个定时器,当定时器超时后重传分组,只有收到正确的ack后,才发送下一个包。
问题是,这还是一个停等协议,效率较低。

流水线的传输方式(停等协议的改进)

必须增加序号范围,因为不是停等了,所以不用等之前的ACK,而顺序发送多个分组
接收方和发送方需要有缓冲,发送方至少需要缓冲没有确认过的分组,接受方也需要缓存,因为只有接收到连续的包之后才能将一组包传给上层。

总结

可靠传输协议面临的问题有 乱序,丢包,出错,可以通过编号,检验和,ack确认包来解决。

TCP的可靠传输

序列号

报文首部包含字节编号

确认号(累计确认)

接收方会发送序列号+1的序号,表示已经收到了该序号前的报文,需要确认号序号的报文。当传输方收到确认号之后,就表示该确认号以前的已经被正确接收了。
如果收到了失序的报文,比如收到了19,21,22,23的报文,当继续收到24的报文时,会继续发送20的确认号。
当传输方收到多个冗余的ACK后,会重传确认号指定序号的报文

缓存

接收方会缓存没有收到报文之后的报文,比如收到了19,21,22,23,则21,22,23都将被缓存,只有当收到20后,才会一起传给上层。
传输方同样会缓存没有被确认的报文,比如收到了19的确认号,已经传了20,21,22,23,由于没有收到任意一个之后的确认号,这些报文都会被缓存。

定时器

当收到确认号之后,会重启定时器,当发生超时事件后,TCP只会重传确认号所指示的那个报文段,然后重启定时器。

JS-常见内存泄漏处理

前言

计算机语言,比如C语言中,有专门的内存管理接口,像malloc()free()。开发人员使用这些方法从操作系统分配和释放内存。
同理,在js中创建事物(对象,字符串等)时分配内存,并且在不使用的时候“自动”释放,这个过程称为垃圾回收。这种看似“自动”的释放资源其本质是造成混乱的根源,并给js开发提供了错误的印象,他们可以选择不关心内存管理。这其实是一个大错误。

自动GC的问题

尽管GC很方便,但是我们不知道GC会在什么时候运行,这就意味着我们在使用了大量内存的时候,GC没有运行,或者说GC无法回收这些内存的情况下,程序可能出现假死,这个就需要我们在程序中手动触发内存回收。

什么是内存泄漏

下面是wiki的解释

在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

四种常见的内存泄漏案例及处理

1.全局变量

function foo(arg) {
  bar = "some text";
}

在js中处理未被声明的变量。在上面代码中的bar会被定义到全局对象中,在浏览器中就是window上。页面中的全局变量,只有在页面关闭后才会被销毁。所以这样会导致内存泄漏,虽然在例子中是简单的字符串,但是如果是实际代码中可能会更加的严重的情况。

下面是另外一种情况

function foo() {
  this.var = "some text";
}
foo();

在这种情况下调用foo,this被指向了全局变量window,意外的创建了全局变量。

上面是一些意外情况下被定义的全局变量,我们平时也有一些我们明确定义的全局变量,如果使用这些变量用来缓存的话,要在使用后,对其重新赋值为null。

2.未销毁的定时器和回调函数

如果项目中使用了观察者模式,都会提供回调方法,来调用一些回调函数。要记住得回收这些回调函数。比如

var serverData = loadData()
setInterval(function() {
  var renderer = document.getElementById('renderer');
  if (renderer) {
    renderer.innerHTML = JSON.stringify(serverData);
  }
}, 5000);

上面代码中如果后续的renderer元素被移除,整个定时器实际上没有任何作用。如果你没有回收定时器,它依旧有效,不但定时器无法被内存回收,定时器函数中的依赖也无法回收。

3.闭包

我们在日常开发中,经常会用到闭包,一个内部函数,有访问包含其的外部函数中的变量,下面情况中,闭包也会造成内存泄漏。

var theThing = null;
var replaceThing = function() {
  var originalThing = theThing;
  var unused = function() {
    if (originalThing) console.log('hi');
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function() {
      console.log('message');
    }
  };
};
setInterval(replaceThing, 1000);

上面代码中每次调用replaceThing时,theThing获得了包含一个巨大的数组和一个对于新闭包someMethod的对象,同时unused是一个引用了originalThing的闭包。
这个例子的关键是闭包之间是共享作用域的,尽管unused可能没有被调用过,但是someMethod可能会被调用,这样就会导致内存无法对其进行GC,当这段代码反复执行时内存会持续增长。

3.DOM引用

很多时候,我们对DOM的操作,会把DOM的引用保存在一个数组或者MAP中。

var elements = {
  image: document.getElementById('image')
};
function doStuff() {
  elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
  document.body.removeChild(document.getElementById('image'));
}

上面代码中,及时我们队image元素进行了移除,但是仍然有对image元素的引用,依然无法进行内存回收。

还有需要注意的一点是,对于一个DOM树的叶子节点的引用,比如 如果我们引用了一个表格中的td元素,一旦在DOM中删除了整个表格,我们直观的觉得内存回收还应该回收了除了被引用的td外的其他元素,但事实上,这个td元素是整个表格的子元素,并保留着对于其父元素的引用,这就会导致对于整个表格,都无法进行内存回收,所以我们要小心处理对于DOM元素的引用。

结尾

虽然现代浏览器以及我们用的框架帮我们做了很多优化,但是还是希望在我们平时代码中,尽量避免内存泄漏。

参考文章

How JavaScript works: memory management + how to handle 4 common memory leaks

golang git仓库搭建

1、请使用https

如果需要go get 使用http 协议需要增加 -insecure参数,否则是https

[fenggx@test shawn_go_testlib]$ go get -insecure git.pri.ibanyu.com/gerrit/service_base_pub
package git.pri.ibanyu.com/gerrit/service_base_pub: unrecognized import path “git.pri.ibanyu.com/gerrit/service_base_pub” (parse https://git.pri.ibanyu.com/gerrit/service_base_pub?go-get=1: no go-import meta tags ())

一直会提示这个错误

go get remote.host/.git – Google 网上论坛 里面提到了

到底是什么原因?
https://stackoverflow.com/questions/27500861/whats-the-proper-way-to-go-get-a-private-repository
这个回复和这个问题很贴切,但是解决不了

go help get 提到

For more about how ‘go get’ finds source code to
download, see ‘go help importpath’.

go help importpath 提到

The repo-root is the root of the version control system
containing a scheme and not containing a .vcs qualifier.

For example,

    import "example.org/pkg/foo"

will result in the following requests:

https://example.org/pkg/foo?go-get=1 (preferred)
http://example.org/pkg/foo?go-get=1 (fallback, only with -insecure)

If that page contains the meta tag

the go tool will verify that https://example.org/?go-get=1 contains the
same meta tag and then git clone https://code.org/r/p/exproj into
GOPATH/src/example.org.

New downloaded packages are written to the first directory listed in the GOPATH
environment variable (For more details see: ‘go help gopath’).

The go command attempts to download the version of the
package appropriate for the Go release being used.
Run ‘go help get’ for more.

Import path checking

When the custom import path feature described above redirects to a
known code hosting site, each of the resulting packages has two possible
import paths, using the custom domain or the known hosting site.

A package statement is said to have an “import comment” if it is immediately
followed (before the next newline) by a comment of one of these two forms:

    package math // import "path"
    package math /* import "path" */

The go command will refuse to install a package with an import comment
unless it is being referred to by that import path. In this way, import comments
let package authors make sure the custom import path is used and not a
direct path to the underlying code hosting site.

Import path checking is disabled for code found within vendor trees.
This makes it possible to copy code into alternate locations in vendor trees
without needing to update import comments.

See https://golang.org/s/go14customimport for details.

看看这个文档
https://golang.org/cmd/go/

2、请在路径增加.git

路径增加 .git就没有问题了,为什么?

[fenggx@test shawn_go_testlib]$ go get -insecure git.pri.ibanyu.com/gerrit/service_base_pub.git
如果拉取的仓库对应的层级没有package 命名,那么会提示
package git.pri.ibanyu.com/gerrit/service_base_pub.git: no Go files in /data/home/fenggx/shawn_go_testlib/src/git.pri.ibanyu.com/gerrit/service_base_pub.git

OpenGL绘制三角形

一、 图形绘制流程介绍
一个3D的物体在2D的屏幕上显示,是由OpenGL的图形渲染管线来管理的。图形渲染管线包括两步:第一步把物体的3D坐标转换为2D坐标;第二步把2D坐标转变为实际的有颜色的像素。下图是一个图形渲染管线的处理流程图。

1. 顶点数据(Vertex):物体的3D坐标的数据的集合。
2. 顶点着色器(Vertex Shader):把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。
3. 图元装配(Primitive Assembly):将顶点装配成形状。
4. 几何着色器(Geometry Shader):通过产生新顶点或取其他顶点来生成新形状。
5. 光栅化阶段(Rasterization Stage):将形状映射为最终屏幕上相应的像素,生成的东西叫片段(Fragment)。这个阶段会执行裁切,丢弃超出你的视图以外的所有像素,用来提升执行效率。
6. 片段着色器(Fragment Shader):计算一个像素的最终颜色。
7. Alpha测试和混合(Blending):进一步透明度、组合等处理。

对于大多数情况,只有三步需要我们操作:准备顶点数据,配置顶点着色器和片段着色器(因为GPU中没有默认的顶点/片段着色器)。
二、绘制一个三角形
1. 顶点数据
1)定义一个顶点数组变量

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

三排分别是三角形三个顶点的x、y、z坐标

2)顶点数组对象VAO和顶点缓冲对象VBO
用来将数据绑定和缓冲的,方便不同顶点数据和属性的切换,具体不怎么明白。

unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

3)链接顶点属性

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
  1. 顶点着色器
    1)定义一个顶点着色器
int vertexShader = glCreateShader(GL_VERTEX_SHADER);

2)顶点着色器源代码

#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

aPos是输入的一个点,vec3(3分量)表示这个点是个三维向量,gl_Position是着色器的输出,输出值用4分量表示,前三个是x、y、z三维坐标值,第4个参数w用来表示远近透视效果,x、y、z会被w除,w越大,得到的结果就越小,也就越接近0,看上去就越远。
定义一个顶点着色器源码变量

const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

3)用顶点着色器的源代码来编译这个着色器对象

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
  1. 片段着色器
    1)定义一个片段着色器
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);

2)片段着色器源代码

#version 330 core
out vec4 FragColor;
void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

得到的结果FragColor用一个4分量表示(r, g, b, a)红色、绿色、蓝色、透明度
定义一个片段着色器源码变量

const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

3)用片段着色器的源代码来编译这个着色器对象

glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
  1. 链接着色器
    创建一个着色器程序对象shaderProgram,链接顶点着色器和片段着色器,这个程序对象只是定义好了,执行在下面的步骤。
int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
  1. 删除着色器对象
    链接后,着色器对象不需要了,需要删除。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
  1. 绘制三角形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

三、完整的绘制三角形代码

#include <GLFW/glfw3.h>
#include "glad/glad.h"
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// 定义一个顶点着色器源码变量
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
// 定义一个片段着色器源码变量
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";


int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left
        0.5f, -0.5f, 0.0f, // right
        0.0f,  0.5f, 0.0f  // top
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0);

    // 顶点着色器
    int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // 片段着色器
    int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // link shaders
    int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 画三角形
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

持续交付平台 spinnaker

Spinnaker是什么

Spinnaker是一个伟大的企业级发布管理方案。

Spinnaker 是 Google 与 Netflix 发布的企业级持续交付平台,是一个持续交付平台,它定位于将产品快速且持续的部署到多种云平台上。Spinnaker有两个核心的功能集群管理和部署管理。Spinnaker通过将发布和各个云平台解耦,来将部署流程流水线化,从而降低平台迁移或多云平台部署应用的复杂度,它可以无缝集成如Git、Jenkins、Travis CI、Docker registry、cron调度器。

Spinnaker 组件

Deck: 基于浏览器的 Spinnaker UI。

Gate: API 调用者和 Spinnaker UI 通过 API 网关(这里称为 Gate )和 Spinnaker 服务器进行通信。

Orca: 是一个编排引擎,被称为 Orca,用来管理流水线和其他特定的操作。

Clouddriver: 已经部署资源的索引和缓存由 Clouddriver 进行管理。

Echo: 负责发送通知,也作为传入的 web 回调(webhook)。

Igor: 用于在 Jenkins 或 Travis CI 等系统中通过持续集成的 job 触发部署流水线,并且允许在部署流水线中使用 Jenkins/Travis 的某些阶段。

Front50: 用于存储 Spinnaker 的元数据。它将持久化存储所有资源的元数据,比如:流水线、项目、应用程序和通知消息。

Rosco: 用于管理虚拟机的镜像。

Rush: Spinnaker 的脚本执行引擎。

特点:
1、通过灵活和可配置的管道实现可重复的自动部署
2、提供一个所有环境的全局视图,一个应用程序可以看见自己的在所属管道中的状态
3、通过一致且可靠的API,提供可编程配置
4、易于配置、维护和扩展

Spinnaker主要包含2块内容:
1、集群管理
集群管理功能,主要用于管理云上的资源。
集群管理将云上资源做了逻辑划分:

机器组

安全组

负载均衡器

2、部署管理

部署管理功能用于创建一个持续交付流程。部署管理的核心是管道,在Spinnaker的定义中,管道由一系列的阶段(stages)组成。管道可以 由Jenkins、定时器、其他管道或者人工触发。同时,管道可以配置参数和通知,可以在管道一些节点上发出消息。Spinnaker已经内置了一些阶段,如执行自定义脚本、触发Jenkins任务等。

从源码的角度深入理解Java的动态代理机制(下)

        在上篇文章中我们讲解了如何通过动态代理解决在HuaWei手机Android 9.0版本中注册BroadcastReceiver超过1000引发应用crash的问题,但这种解决方式并不太合理,正确的做法是在注册完BroadcastReceiver后当不需要使用BroadcastReceiver时应及时反注册从而保证应用注册的BroadcastReceiver数量没有超过阀值。这篇文章我们就从源码的角度深入学习一下Java的动态代理机制。

        通过上篇文章我们知道使用Java的动态代理功能有两个前提条件:一个是有接口的存在;另一个是必须有接口的实现类和实例。只有满足了这两个条件,我们才能使用Java的动态代理功能对接口中的方法做代理。使用Java的动态代理功能就是使用Proxy和InvocationHandler这两个API,入口就是使用Proxy的newProxyInstance()方法,newProxyInstance()方法源码如下:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException {
    // h必须参数,为null就抛异常
    if (h == null) {
        throw new NullPointerException();
    }
    // 备份传递进来的interfaces数组
    final Class<?>[] intfs = interfaces.clone();
    final SecurityManager sm = System.getSecurityManager();
    // 权限检查,可忽略
    if (sm != null) {
        checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
    }

    /*
     * getProxyClass0()方法是重点,功能是从缓存中查找或者生成目标类,
     */
    Class<?> cl = getProxyClass0(loader, intfs);

    try {
        // 获取指定参数constructorParams的构造方法类
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        // 权限检查,最终也是调用newInstance()方法
        if (sm != null && ProxyAccessHelper.needsNewInstanceCheck(cl)) {
            return AccessController.doPrivileged(new PrivilegedAction<Object>() {
                public Object run() {
                    // 调用newInstance()方法,注意ih参数最终传递给了cons
                    return newInstance(cons, ih);
                }
            });
        } else {
            // 调用newInstance()方法,注意ih参数最终传递给了cons
            return newInstance(cons, ih);
        }
    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString());
    }
}

        Proxy的newProxyInstance()方法需要三个参数,分别表示如下:

  • loader 待生成代理类的类加载器
  • interfaces 待生成代理类需要实现的接口
  • h 执行代理类中的方式时对外提供的回调

        newProxyInstance()方法的流程先校验参数h是否为null,如果为null就直接抛异常,接着对参数interfaces做备份,最后调用getProxyClass0()方法获取一个Class类型的cl对象,获取到cl后调用cl含有一个InvocationHandler参数的构造方法并赋值给cons,最后调用newInstance(cons, ih)方法通过反射生成一个代理类对象并返回,根据以上流程我们可以得出生成代理对象的一些简单结论:该代理对象有一个带有InvocationHandler参数的构造方法,在生成代理对象的时候我们把外界的InvocationHandler实例ih传递给了该代理类。

        Proxy的newProxyInstance()方法流程很简单,重点在其内部的getProxyClass()方法,源码如下:

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    // 接口类数量不能超过65535,否则抛异常
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // 从缓存中查找,如果缓存中没有则调用ProxyClassFactory创建
    return proxyClassCache.get(loader, interfaces);
}

        getProxyClass0()方法先对参数interfaces数组做了长度校验,如果超过65535就抛异常(也就是说使用Java的动态代理功能一次性最多只能代理65535个接口),最后通过proxyClassCache的get()方法获取到代理Class,proxyClassCache是Proxy的内部静态属性,它是WeakCache类型,核心功能是提供缓存来提高效率,proxyClassCache的定义如下所示:

// proxyClassCache的初始化
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

// WeakCache构造方法
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                 BiFunction<K, P, V> valueFactory) {
    // subKeyFactory是KeyFactory实例
    this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
    // valueFactory是ProxyClassFactory实例
    this.valueFactory = Objects.requireNonNull(valueFactory);
}

        WeakCache对外提供的构造方法WeakCache(BiFunction subKeyFactory, BiFunction valueFactory)需要两个BiFunction类型的参数,其中subKeyFactory的作用是生成缓存key,valueFactory的作用是当缓存中不存在目标Class时则负责创建Class(从源码可知proxyClassCache在初始化的时候传递了KeyFactory和ProxyClassFactory实例对象)。接下来我们看一下WeakCache的get()方法,如下所示:

public V get(K key, P parameter) {
    // 对参数parameter做非空校验
    Objects.requireNonNull(parameter);

    expungeStaleEntries();
    // 生成一个特殊的key:cacheKey
    Object cacheKey = CacheKey.valueOf(key, refQueue);

    // 根据cacheKey从缓冲中查找
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    if (valuesMap == null) {
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
            = map.putIfAbsent(cacheKey,
                              valuesMap = new ConcurrentHashMap<>());
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }


    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    Supplier<V> supplier = valuesMap.get(subKey);
    Factory factory = null;

    while (true) {
        if (supplier != null) {
            // 调用supplier的get()方法
            V value = supplier.get();
            if (value != null) {
                return value;
            }
        }

        // 初始化factory
        if (factory == null) {
            factory = new Factory(key, parameter, subKey, valuesMap);
        }

        // 初始化supplier
        if (supplier == null) {
            supplier = valuesMap.putIfAbsent(subKey, factory);
            if (supplier == null) {
                supplier = factory;
            }
        } else {
            if (valuesMap.replace(subKey, supplier, factory)) {
                supplier = factory;
            } else {
                supplier = valuesMap.get(subKey);
            }
        }
    }
}

        WeakCache的get()方法核心就是从缓冲中查找目标Class对象,如果找到就返回,否则就调用ProxyClassFactory实例创建,ProxyClassFactory的源码如下:

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
    // 生成代理Class的前缀
    private static final String proxyClassNamePrefix = "$Proxy";

    // 通过序列号确保生成的代理Class名字唯一
    private static final AtomicLong nextUniqueNumber = new AtomicLong();

    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
        // 遍历数组做相关校验
        for (Class<?> intf : interfaces) {
            Class<?> interfaceClass = null;
            try {
                // 通过loader加载传递进来的接口Class
                interfaceClass = Class.forName(intf.getName(), false, loader);
            } catch (ClassNotFoundException e) {
            }
            // 验证是否是同一个类加载器,如果不是就抛异常
            if (interfaceClass != intf) {
                throw new IllegalArgumentException(
                    intf + " is not visible from class loader");
            }

            // 验证是否是接口
            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(
                    interfaceClass.getName() + " is not an interface");
            }
            // 禁止重出现复接口
            if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                throw new IllegalArgumentException(
                    "repeated interface: " + interfaceClass.getName());
            }
        }

        // 待生成代理Class的包名
        String proxyPkg = null;

        // 确定代理Class的包路径,如果接口数组中只有一个包访问权限的接口,则生成代理Class的包路径就是该接口的包路径,如果包含多个包访问权限的接口,则抛异常
        for (Class<?> intf : interfaces) {
            int flags = intf.getModifiers();
            if (!Modifier.isPublic(flags)) {
                String name = intf.getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException(
                        "non-public interfaces from different packages");
                }
            }
        }

        // 验证结束如果没有包权限的接口,则生成代理Class的包名为com.sun.proxy.
        if (proxyPkg == null) {
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        // 生成代理类的名字,格式为:com.sun.proxy.$Proxy0 ... com.sun.proxy.$ProxyN等
        long num = nextUniqueNumber.getAndIncrement();
        String proxyName = proxyPkg + proxyClassNamePrefix + num;

        // 调用ProxyGenerator的generateProxyClass()方法生成二进制文件
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
        try {
            // 调用native方法defineClass0()生成代理Class对象并返回
            return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
        } catch (ClassFormatError e) {
            throw new IllegalArgumentException(e.toString());
        }
    }
}

        ProxyClassFactory的apply()方法注解写的很详细,该方法的流程是先遍历数组参数判断接口类的合法性,然后确定待生成Class的包路径和名字,准备就绪之后调用ProxyGenerator的generateProxyClass()方法生成一个byte[]数组(byte[]数组内容就是待生成代理类的二进制文件),最后调用native方法defineClass0()生成一个Class对象并返回。

        ProxyGenerator类在sun.misc包下,其源码可参照这里http://www.docjar.com/html/api/sun/misc/ProxyGenerator.java.html, 它的功能就是根据JVM规范生成Class类的二进制文件,ProxyGenerator类中的具体流程就不再分析了,有感兴趣的可以自行分析,由于没法直接调用该方法,我通过反射方式调用了ProxyGenerator的generateProxyClass()方法后得到了byte[]数组并将该byte[]数组内容直接保存在.class文件中,最后反编译该文件并重写排版后,结果如下所示:

public final class $Proxy0 extends Proxy implements IActivityManager {

    // 基础方法
    private static Method m0;   // hashCode()方法
    private static Method m1;   // equals()  方法
    private static Method m2;   // toString()方法

    // 以下是IActivityManager接口中定义的方法
    private static Method m3;   // registerReceiver()方法
    private static Method m4;   // unregisterReceiver()方法
    // ......                    

    static {
        try {
            // 基础方法
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);

            // 接口中定义的方法
            m3 = Class.forName("android.app.IActivityManager").getMethod("registerReceiver", new Class[] { Class.forName("android.content.BroadcastReceiver"), Class.forName("android.content.IntentFilter") });
            m4 = Class.forName("android.app.IActivityManager").getMethod("unregisterReceiver", new Class[] { Class.forName("android.content.BroadcastReceiver") });

            // ......

        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    public Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final int hashCode() {
        try {
            return ((Integer) super.h.invoke(this, m0, (Object[]) null)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final boolean equals(Object var1) {
        try {
            return ((Boolean) super.h.invoke(this, m1, new Object[] { var1 })).booleanValue();
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Intent registerReceiver(BroadcastReceiver var1, IntentFilter var2) {
        try {
            return (Intent) super.h.invoke(this, m3, new Object[] { var1, var2 });
        } catch (RuntimeException | Error var4) {
            throw var4;
        } catch (Throwable var5) {
            throw new UndeclaredThrowableException(var5);
        }
    }

    public final void unregisterReceiver(BroadcastReceiver var1) {
        try {
            super.h.invoke(this, m4, new Object[] { var1 });
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    // 省略部分方法
}

        根据反编译的结果,我们发现ProxyGenerator生成的$Proxy0类和它的内部方法都有final修饰符,它继承了Proxy且实现了从外界传递进来的接口类IActivityManger。$Proxy0类中定义了一系列Method对象,这些Method对象是在static语句块中进行的初始化(放在static语句块中确保这些Method对象仅且仅初始化一次),通过这些Method的初始化我们发现它们分别代表了接口中定义的方法和toString()、equals()、hashCode()方法,我们拿registerReceiver()方法举例:

public final Intent registerReceiver(BroadcastReceiver var1, IntentFilter var2) {
    try {
        return (Intent) super.h.invoke(this, m3, new Object[] { var1, var2 });
    } catch (RuntimeException | Error var4) {
        throw var4;
    } catch (Throwable var5) {
        throw new UndeclaredThrowableException(var5);
    }
}

        registerReceiver()方法中调用的是父类的super.h的invoke()方法,h是父类Proxy类中定义的属性,它是InvocationHandler类型(根据前边$Proxy0的初始化可知h就是我们调用Proxy的newProxyInstance()方法时传递的第三个参数)。在调用invoke(Object proxy, Method method, Object[] args)方法时第一个参数proxy传递的this(也就是说把当前$Proxy0对象本身传递给了super.h的invoke()方法中),第二个参数method传递的是m3,简单理解就是把当前方法名传递进去了(可以看m3的初始化代码),第三个args参数把当前方法的参数作为新的参数传递给了invoke()方法中,之后把invoke()方法执行的结果返回了。这就是生成代理类对方法的处理,其它方法也是同样逻辑就不再叙述了。

        接下来我们看一下InvocationHandler的源码,如下:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

        InvocationHandler接口只定义了一个invoke()方法,该方法接收三个参数,其参数分别表示如下:

  • proxy 表示通过ProxyGenerator类动态创建出来的代理类的实例:$Proxy0、$Proxy1…$ProxyN
  • method 表示将要执行的方法,一般是接口中定义的方法或是toString(),equals(),hashCode()方法
  • args 表示执行method方法所需要的参数,如果method没有参数那么args为null

        分析到这里已经可以回答在上篇文章末尾遗留的问题了,调用Proxy的newProxyInstance(ClassLoader loader, Class[] interfaces, InvacationHandler h)方法返回一个Object对象,该对象就是$Proxy0、$Proxy1……$ProxyN,它们继承了Proxy类并且实现了参数interfaces中的接口,当调用这些代理对象的方法时又委托给我们从外界传递的InvocationHandler类型参数h从而允许我们有一个入口来对interfaces接口中的方法做拦截操作,仔细回味一下这种代码设计,真的很棒!!!

        好了,到这里已经分析完了Java的动态代理机制,由于篇幅原因部分细节没有做分析(特别是调用ProxyGenerator的generateProxyClass()方法),但这并不影响我们主流程的分析,有感兴趣的小伙伴可以自行分析,最后感谢收看(*^__^*) ……

Prometheus Alertmanager报警组件

Prometheus Alertmanager

概述

Alertmanager与Prometheus是相互分离的两个组件。Prometheus服务器根据报警规则将警报发送给Alertmanager,然后Alertmanager将silencing、inhibition、aggregation等消息通过电子邮件、PaperDuty和HipChat发送通知。

设置警报和通知的主要步骤:

  • 安装配置Alertmanager
  • 配置Prometheus通过-alertmanager.url标志与Alertmanager通信
  • 在Prometheus中创建告警规则

Alertmanager简介及机制

Alertmanager处理由例如Prometheus服务器等客户端发来的警报。它负责删除重复数据、分组,并将警报通过路由发送到正确的接收器,比如电子邮件、Slack等。Alertmanager还支持groups,silencing和警报抑制的机制。

分组

分组是指将同一类型的警报分类为单个通知。当许多系统同时宕机时,很有可能成百上千的警报会同时生成,这种机制特别有用。
例如,当数十或数百个服务的实例在运行,网络发生故障时,有可能一半的服务实例不能访问数据库。在prometheus告警规则中配置为每一个服务实例都发送警报的话,那么结果是数百警报被发送至Alertmanager。

但是作为用户只想看到单一的报警页面,同时仍然能够清楚的看到哪些实例受到影响,因此,可以通过配置Alertmanager将警报分组打包,并发送一个相对看起来紧凑的通知。

分组警报、警报时间,以及接收警报的receiver是在alertmanager配置文件中通过路由树配置的。

抑制(Inhibition)

抑制是指当警报发出后,停止重复发送由此警报引发其他错误的警报的机制。(比如网络不可达,导致其他服务连接相关警报)

例如,当整个集群网络不可达,此时警报被触发,可以事先配置Alertmanager忽略由该警报触发而产生的所有其他警报,这可以防止通知数百或数千与此问题不相关的其他警报。

抑制机制也是通过Alertmanager的配置文件来配置。

沉默(Silences)

Silences是一种简单的特定时间不告警的机制。silences警告是通过匹配器(matchers)来配置,就像路由树一样。传入的警报会匹配RE,如果匹配,将不会为此警报发送通知。

这个可视化编辑器可以帮助构建路由树。

silences报警机制可以通过Alertmanager的Web页面进行配置。

Alermanager的配置

Alertmanager通过命令行flag和一个配置文件进行配置。命令行flag配置不变的系统参数、配置文件定义的抑制(inhibition)规则、通知路由和通知接收器。

要查看所有可用的命令行flag,运行alertmanager -h。
Alertmanager支持在运行时加载配置,如果新配置语法格式不正确,更改将不会被应用,并记录语法错误。通过向该进程发送SIGHUP或向/-/reload端点发送HTTP POST请求来触发配置热加载。

配置文件

要指定加载的配置文件,需要使用-config.file标志。该文件使用YAML来完成,通过下面的描述来定义。带括号的参数表示是可选的,对于非列表的参数的值,将被设置为指定的缺省值。

通用占位符定义解释:

  • \ : 与正则表达式匹配的持续时间值,[0-9]+(ms|[smhdwy])
  • : 与正则表达式匹配的字符串,[a-zA-Z_][a-zA-Z0-9_]*
  • : unicode字符串
  • : 有效的文件路径
  • : boolean类型,true或者false
  • : 字符串
  • : 模板变量字符串

global全局配置文件参数在所有配置上下文生效,作为其他配置项的默认值,可被覆盖.

global:
  # ResolveTimeout is the time after which an alert is declared resolved
  # if it has not been updated.
  #解决报警时间间隔
  [ resolve_timeout: <duration> | default = 5m ]

  # The default SMTP From header field.
  [ smtp_from: <tmpl_string> ]
  # The default SMTP smarthost used for sending emails.
  [ smtp_smarthost: <string> ]
  # SMTP authentication information.
  [ smtp_auth_username: <string> ]
  [ smtp_auth_password: <string> ]
  [ smtp_auth_secret: <string> ]
  # The default SMTP TLS requirement.
  [ smtp_require_tls: <bool> | default = true ]

  # The API URL to use for Slack notifications.
  [ slack_api_url: <string> ]

  [ pagerduty_url: <string> | default = "https://events.pagerduty.com/generic/2010-04-15/create_event.json" ]
  [ opsgenie_api_host: <string> | default = "https://api.opsgenie.com/" ]

# Files from which custom notification template definitions are read.
# The last component may use a wildcard matcher, e.g. 'templates/*.tmpl'.
templates:
  [ - <filepath> ... ]

# The root node of the routing tree.
route: <route>

# A list of notification receivers.
receivers:
  - <receiver> ...

# A list of inhibition rules.
inhibit_rules:
  [ - <inhibit_rule> ... ]

路由(route)

路由块定义了路由树及其子节点。如果没有设置的话,子节点的可选配置参数从其父节点继承。

每个警报都会在配置的顶级路由中进入路由树,该路由树必须匹配所有警报(即没有任何配置的匹配器)。然后遍历子节点。如果continue的值设置为false,它在第一个匹配的子节点之后就停止;如果continue的值为true,警报将继续进行后续子节点的匹配。如果警报不匹配任何节点的任何子节点(没有匹配的子节点,或不存在),该警报基于当前节点的配置处理。

路由配置格式

#报警接收器
[ receiver:  ]

#分组
[ group_by: '[' , ... ']' ]

# Whether an alert should continue matching subsequent sibling nodes.
[ continue:  | default = false ]

# A set of equality matchers an alert has to fulfill to match the node.
#根据匹配的警报,指定接收器
match:
  [ : , ... ]

# A set of regex-matchers an alert has to fulfill to match the node.
match_re:
#根据匹配正则符合的警告,指定接收器
  [ : , ... ]

# How long to initially wait to send a notification for a group
# of alerts. Allows to wait for an inhibiting alert to arrive or collect
# more initial alerts for the same group. (Usually ~0s to few minutes.)
[ group_wait:  ]

# How long to wait before sending notification about new alerts that are
# in are added to a group of alerts for which an initial notification
# has already been sent. (Usually ~5min or more.)
[ group_interval:  ]

# How long to wait before sending a notification again if it has already
# been sent successfully for an alert. (Usually ~3h or more).
[ repeat_interval:  ]

# Zero or more child routes.
routes:
  [ -  ... ]

例子:

# The root route with all parameters, which are inherited by the child
# routes if they are not overwritten.
route:
  receiver: 'default-receiver'
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  group_by: [cluster, alertname]
  # All alerts that do not match the following child routes
  # will remain at the root node and be dispatched to 'default-receiver'.
  routes:
  # All alerts with service=mysql or service=cassandra
  # are dispatched to the database pager.
  - receiver: 'database-pager'
    group_wait: 10s
    match_re:
      service: mysql|cassandra
  # All alerts with the team=frontend label match this sub-route.
  # They are grouped by product and environment rather than cluster
  # and alertname.
  - receiver: 'frontend-pager'
    group_by: [product, environment]
    match:
      team: frontend

抑制规则 inhibit_rule

抑制规则,是存在另一组匹配器匹配的情况下,使其他被引发警报的规则静音。这两个警报,必须有一组相同的标签。

抑制配置格式

# Matchers that have to be fulfilled in the alerts to be muted.
##必须在要需要静音的警报中履行的匹配者
target_match:
  [ : , ... ]
target_match_re:
  [ : , ... ]

# Matchers for which one or more alerts have to exist for the
# inhibition to take effect.
#必须存在一个或多个警报以使抑制生效的匹配者。
source_match:
  [ : , ... ]
source_match_re:
  [ : , ... ]

# Labels that must have an equal value in the source and target
# alert for the inhibition to take effect.
#在源和目标警报中必须具有相等值的标签才能使抑制生效
[ equal: '[' , ... ']' ]

接收器(receiver)

顾名思义,警报接收的配置。

  • 通用配置格式
# The unique name of the receiver.
name: 

# Configurations for several notification integrations.
email_configs:
  [ - , ... ]
pagerduty_configs:
  [ - , ... ]
slack_config:
  [ - , ... ]
opsgenie_configs:
  [ - , ... ]
webhook_configs:
  [ - , ... ]
  • 邮件接收器email_config
# Whether or not to notify about resolved alerts.
#警报被解决之后是否通知
[ send_resolved:  | default = false ]

# The email address to send notifications to.
to: 
# The sender address.
[ from:  | default = global.smtp_from ]
# The SMTP host through which emails are sent.
[ smarthost:  | default = global.smtp_smarthost ]

# The HTML body of the email notification.
[ html:  | default = '{{ template "email.default.html" . }}' ] 

# Further headers email header key/value pairs. Overrides any headers
# previously set by the notification implementation.
[ headers: { : , ... } ]

  • Slcack接收器slack_config
# Whether or not to notify about resolved alerts.
[ send_resolved:  | default = true ]

# The Slack webhook URL.
[ api_url:  | default = global.slack_api_url ]

# The channel or user to send notifications to.
channel: 

# API request data as defined by the Slack webhook API.
[ color:  | default = '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}' ]
[ username:  | default = '{{ template "slack.default.username" . }}'
[ title:  | default = '{{ template "slack.default.title" . }}' ]
[ title_link:  | default = '{{ template "slack.default.titlelink" . }}' ]
[ pretext:  | default = '{{ template "slack.default.pretext" . }}' ]
[ text:  | default = '{{ template "slack.default.text" . }}' ]
[ fallback:  | default = '{{ template "slack.default.fallback" . }}' ]
  • Webhook接收器webhook_config
 # Whether or not to notify about resolved alerts.
[ send_resolved:  | default = true ]

 # The endpoint to send HTTP POST requests to.
url: 

Alertmanager会使用以下的格式向配置端点发送HTTP POST请求:

{
  "version": "3",
  "groupKey":      // key identifying the group of alerts (e.g. to deduplicate)
  "status": "",
  "receiver": ,
  "groupLabels": ,
  "commonLabels": ,
  "commonAnnotations": ,
  "externalURL": ,  // backling to the Alertmanager.
  "alerts": [
    {
      "labels": ,
      "annotations": ,
      "startsAt": "",
      "endsAt": ""
    },
    ...
  ]
}

可以添加一个钉钉webhook,通过钉钉报警,由于POST数据需要有要求,简单实现一个数据转发脚本。

from flask import Flask
from flask import request
from urllib2 import Request,urlopen

import json

app = Flask(__name__)

@app.route('/',methods=['POST'])
def send():
    if request.method == 'POST':
        post_data = request.get_data()
        alert_data(post_data)
    return

def alert_data(data):
    url = 'https://oapi.dingtalk.com/robot/send?access_token=xxxx'
    send_data = '{"msgtype": "text","text": {"content": %s}}' %(data)
    request = Request(url, send_data)
    request.add_header('Content-Type','application/json')
    return urlopen(request).read()

if __name__ == '__main__':
    app.run(host='0.0.0.0')

报警规则

报警规则允许你定义基于Prometheus表达式语言的报警条件,并发送报警通知到外部服务

定义报警规则

报警规则通过以下格式定义:

ALERT 
  IF 
  [ FOR  ]
  [ LABELS <label> ]
  [ ANNOTATIONS <label> ]
  • 可选的FOR语句,使得Prometheus在表达式输出的向量元素(例如高HTTP错误率的实例)之间等待一段时间,将警报计数作为触发此元素。如果元素是active,但是没有firing的,就处于pending状态。

  • LABELS(标签)语句允许指定一组标签附加警报上。将覆盖现有冲突的任何标签,标签值也可以被模板化。

  • ANNOTATIONS(注释)它们被用于存储更长的其他信息,例如警报描述或者链接,注释值也可以被模板化。

  • Templating(模板) 标签和注释值可以使用控制台模板进行模板化。$labels变量保存警报实例的标签键/值对,$value保存警报实例的评估值。

    # To insert a firing element's label values:
    {{ $labels. }}
    # To insert the numeric expression value of the firing element:
    {{ $value }}
    

报警规则示例:

# Alert for any instance that is unreachable for >5 minutes.
ALERT InstanceDown
  IF up == 0
  FOR 5m
  LABELS { severity = "page" }
  ANNOTATIONS {
    summary = "Instance {{ $labels.instance }} down",
    description = "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes.",
  }

# Alert for any instance that have a median request latency >1s.
ALERT APIHighRequestLatency
  IF api_http_request_latencies_second{quantile="0.5"} > 1
  FOR 1m
  ANNOTATIONS {
    summary = "High request latency on {{ $labels.instance }}",
    description = "{{ $labels.instance }} has a median request latency above 1s (current value: {{ $value }}s)",
  }

发送报警通知

Prometheus的警报rules可以很好的知道现在的故障情况,但还不是一个完整的通知解决方案。在简单的警报定义之上,需要另一层级来实现报警汇总,通知速率限制,silences等基于rules之上,在prometheus生态系统中,Alertmanager发挥了这一作用。因此,
Prometheus可以周期性的发送关于警报状态的信息到Alertmanager实例,然后Alertmanager调度来发送正确的通知。该Alertmanager可以通过-alertmanager.url命令行flag来配置。

参考文章:
– https://prometheus.io/docs/alerting/alertmanager/

聊聊全链路压测(一)

什么是全链路压测

全链路压测,是基于真实的生产环境来模拟海量的并发用户请求和数据,对整个业务链路进行压力测试,试图找到所有潜在性能瓶颈点并持续优化的实践。

最为典型的是淘宝的双11活动,每年到了双11,淘宝的整个系统都会面临极大的流量冲击,如果事先没有经过充分的测试和容量预估,很可能会在流量爆发时瘫痪。

从2013年开始,淘宝开始实施全面的全链路压测。由于在真正的双11到来前,淘宝内部已经模拟了比双11流量还要高的负载,并且逐个解决了已经发现的问题,因此真正双11到来的时候,就不会出现严重的问题了。

全链路压测的难点

目前来看,全链路压测有四个难点:
1、海量并发请求的发起;
2、全链路压测流量的隔离;
3、实际业务负载的模拟;
4、测试完成后的数据清理。

海量并发请求的发起

业界普遍采用免费的JMeter来完成全链路压测,在此过程中,有三个问题需要解决:

1、虽然采用了分布式的JMeter方案,并发数量也会存在上限,比如面对亿级的海量并发时,主要原因是分布式的JMeter方案中,Master节点会成为整个压测发起的瓶颈。为了解决这个难题,很多公司并不会直接采用分布式JMeter架构来完成海量并发,而是会使用Jenkins Job单独调用JMeter节点来控制和发起测试压力。这样就避免了Master节点引发的瓶颈问题。而且,由于各个JMeter是完全独立的,所以只要Jenkins Job足够多,并且网络带宽不会成为瓶颈的情况下,就能发起足够大的并发。

2、测试脚本、测试数据和测试结果在分布式JMeter环境中的分发难题。如果直接采用分布式的JMeter方案,测试脚本需要通过JMeter的Master节点来分发,测试数据文件则要用户自行上传至每套虚拟机,同时测试结果还要通过JMeter的Slave节点回传至Master节点。
所以,更好的做法是基于JMeter来搭建一个压测框架,诸如脚本分发、数据分发以及结果回传等工作,都由压测框架完成。

3、流量发起的地域要求。全链路压测流量的发起很多时候是有地理位置要求的,比如30%的压力负载来自上海、30%的压力负载来自北京等,这就要求我们在多个城市的数据中心都搭建JMeter Slave,以便可以发起来自多个地域的组合流量。

VUE 的 Render 函数


Vue.js 的 Render 函数是类似与Virtual DOM(虚拟 DOM)的语法,需要使用一些特定的选项,将 template 的内容改写成一个 JavaScript 对象。
一、Render 函数只有 3 个参数。
先看 template 和 Render 写法的对照:

//1.template
<template> 
    <div id="main" class="container" style="color: bule"> 
        <h4 v-if="show">show 1</h4 > 
        <h4 v-else>show 2</h4 > 
    </div> 
</template> 
<script> 
    export default { 
        data () { 
            return { 
                show: false 
            } 
        } 
    } 
</script> 
//2.render
export default { 
    data () { 
        return { show: false } 
    }, 
    render: (h) => { 
        let childNode; 
        if (this.shfuow) { 
            childNode = h('h4', 'show 1'); 
        } else { 
            childNode = h('h4', 'show 2'); 
        } 
        return h('div', { 
            attrs: { 
                id: 'main' 
            }, 
            class: { 
                container: true 
            }, 
            style: { 
                color: 'bule' 
            } 
        }, [childNode]); 
    } 
} 

这里的 h,即 createElement,是 Render 函数的核心。可以看到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替代了,那 v-for 自然也会被 for 语句替代。

其实这里的h 是有 3 个参数:
1.要渲染的元素或组件,可以是一个 html 标签、组件选项或一个函数(不常用),该参数为必填项

    // 1. html 标签 
        h('div'); 
    // 2. 组件选项 
        import Container from '../../containers/home/Index'; 
        h(Container);
        //我们经常用到的
        new Vue({
            el: '#container',
            render: h => h(Container)
        });

2.对应属性的数据对象,该参数是可选的。比如组件的 props、元素的 class、绑定的事件、slot、自定义指令等。
3.子节点,该参数是可选的,String 或 Array,它同样是一个 h

    [ 
        'show', 
        h('h4', 'show'), 
        h(Component, { 
            props: { 
                someProp: 'foo' 
            } 
        }) 
    ] 

Render函数的使用场景

一般情况下是不推荐直接使用 Render 函数的,使用 template 就可以,在 Vue.js 中,使用 Render 函数的场景,主要有以下 4 点:


1.使用两个相同 slot。在 template 中,Vue.js 不允许使用两个相同的 slot,比如下面的示例是错误的:

<template> 
    <div> 
        <slot></slot> 
        <slot></slot> 
    </div> 
</template> 

解决方案就是使用一个深度克隆 VNode 节点的方法。
对含有组件的 slot,需要将 slot 的每个子节点都克隆一份,例如:

...
    { 
        render: (h) => { 
            function cloneVNode (vnode) {
                // 递归遍历所有子节点,并克隆 
                const clonedChildren = vnode.children && vnode.children.map(vnode =>cloneVNode(vnode)); 
                const cloned = h(vnode.tag, vnode.data, clonedChildren); 
                cloned.text = vnode.text; 
                cloned.isComment = vnode.isComment; 
                cloned.componentOptions = vnode.componentOptions; 
                cloned.elm = vnode.elm; cloned.context = vnode.context; 
                cloned.ns = vnode.ns; 
                cloned.isStatic = vnode.isStatic; 
                cloned.key = vnode.key; 
                return cloned; 
            } 
            const vNodes = this.$slots.default === undefined ? [] : this.$slots.default; 
            const clonedVNodes = this.$slots.default === undefined ? [] : vNodes.map(vnode => cloneVNode(vnode)); 
            return h('div', [ 
                vNodes, 
                clonedVNodes 
            ]) 
        } 
    } 
    ...

在 Render 函数里创建了一个 cloneVNode 的工厂函数,通过递归将 slot 所有子节点都克隆了一份,并对 VNode 的关键属性也进行了复制。


另外重复渲染多个组件或元素,可以通过一个循环和工厂函数来解决:

const Child = { 
    render: (h) => { 
        return h('h4', 'text'); 
    } 
} 
export default { 
    render: (h) => { 
        const children = Array.apply(null, { 
            length: 5 
        }).map(() => { 
            return h(Child); 
        }); 
        return h('div', children); 
    } 
} 

所以注意:所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须唯一


MongoDB复制集成员状态详解

mongodb复制集成员状态一共有11种,mongodb将其分为三类:核心状态,其它状态,错误状态。下面分开细说:

核心状态

PRIMARY
处于该状态的成员接受所有的写请求,同时一个副本集最多只能有一个出于该状态,处于SECONDARY状态的成员可以通过选举到PRIMARY状态。该状态可以投票。

SECONDARY
处于该状态的成员通过oplog同步PRIMARY的数据(直接从PRIMARY同步或者从另一个SECONDARY同步)。SECONDARY成员默认是不可读写的,可以通过配置SECONDARY能够读数据,从而实现读写分离。一般驱动都会提供三种一致性级别:强一致性(读写都从PRIMARY节点),单调一致性(写请求通过PRIMARY节点,同一个session的读请求可以读到当前session最新的写请求的结果),最终一致性(写请求通过PRIMARY节点,读请求随机发送到一个SECONDARY节点)。该状态可以投票。

ARBITER
处于该状态的成员不同步数据,也不接受读写请求。这个状态的作用是用于打破平衡的。比如当前集群是一个PRIMARY,一个SECONDARY节点,这样数据是多副本的,但是不说高可用的,当集群中的任何一个节点宕机后,由于集群只剩下一个节点,不能达到当前集群成员半数以上的成员成活,这样当前集群中的PRIMARY节点会自动变更状态为SECONDARY,可用通过在一个新的节点上增加一个ARBITER来解决上面的问题。该状态可以投票。

其它状态

STARTUP
每一个新加入的成员,在还没有同步到副本集的配置的时候为STARTUP状态。该状态下的成员还不是一个公认的集群成员,所以该状态的成员不能投票。

STARTUP2
STARTUP状态的成员在同步完成副本集的配置后为变更为STARTUP2状态。在该状态下的成员已经是副本集的公认成员,所以该状态下的成员可以投票。如果当前成员是保存数据和索引的,那么该状态下的成员会去同步副本集的数据和索引,该状态一直维持到到数据和索引同步完成为止。

RECOVERING
RECOVERING状态的成员可以简单理解为一个oplog落后太多的SECONDARY节点,为来保证数据最终一致性的时间窗口不会太大,所以RECOVERING是不可读的。由于SECONDARY过载导致oplog同步落后太多或者新挂在的节点第一次sync后会变更为该状态,这样可以通过停止一个正常的SECONDARY节点,然后拷贝SECONDARY节点到数据到RECOVERING节点,重新启动RECOVERING节点来恢复状态为SECONDARY节点。RECOVERING是一个正常状态,所以该状态的成员可以投票。

错误状态

UNKNOWN
如果当前成员不能将自己的状态同步到副本集的成员节点,在集群的其它成员看来为UNKNOWN状态。很显然,该状态的成员不能投票。

DOWN
如果当前成员不能将连接到副本集的成员节点,在集群的其它成员看来为DOWN状态。很显然,该状态的成员不能投票。

REMOVED
被手动移除的成员会进入REMOVED状态,并且在日志中会打印回来。很显然,该状态的成员不能投票。

ROLLBACK
PRIMARY状态的成员由于宕机等原因被移除出集群,然后重新加入集群的时候,如果该成员宕机前还有未必同步到集群的数据,当前节点会进入ROLLBACK状态,回滚所以未同步的操作。该状态的成员是可以投票的。

FATAL
3 .0以及以后的版本已经删除,忽略。