amhere's blog


  • 首页

  • 归档

  • 标签

keycloak介绍和sso对接

发表于 2020-05-15 |

keycloak简介:

这里使用的keycloak作为单点登入服务器的应用,但是keycloak不仅于sso的功能

通过Keycloak处理用户认证,用户不需要再次登录Keycloak管理下的其它应用。实现一次登录,多处登录不同应用,一处登出,所有应用登出。

keycloak核心概念:

users:用户是一个可以登陆系统的实体,它可以拥有联系它们自身的属性,例如邮箱、用户名、地址、电话号码或生日等,可以为user分配组别或者角色。

authentication:相当于密码,可以验证和识别一个user。

authorization:给予用户访问的过程。

credentials:证书,可以供keycloak验证用户的东西,例如密码、一次性密码、证书、指纹等。

roles:相当于用户的一个分类 ,一个组织可能有Admin\user\manager\emplee等角色,应用程序经常会分配权限给角色,而不是用户,因为用户太难管理。

user role mapping:定义了一个用户及角色的关系,一个用户可以属于零个或多个角色,用户与角色的映射关系,这样就可以决定用在各种资源访问的权限管理。

composite roles:复合角色可以包含其他的角色,用户拥有了复合角色就相当于拥有了它下面的所有子角色。

groups:组可以一组的用户,也可以将角色映射到角色中,用户可以成为组员后继承用组的角色

realms:领域,领域管理着一批,用户、证书、角色、组等,一个用户只能属于且能登陆到一个域,域之间是互相独立的,域只能管理在它下面的用户。

clients:客户端是一个实体,可以请求keycloak对用户进行身份验证,大部分情况下,客户端是应用或服务希望使用keycloak来保护自己和提供一个单点登录的解决方案。客户端也可以是一个实体,请求身份信息或一个访问信息,这样就可以调用其他keycloak保护的应用或服务了。

单点登入协议protocols:

对比点 OAuth2| OpenID| SMAL

票据格式 JSON or SAML2| JSON| XML

支持授权 Yes| No| Yes

支持认证 “伪认证” |Yes| Yes

创建年份 2005| 2006| 2001

最新版本 OAuth2| OpenID Connect| SAML 2.0

传输方式 HTTP| HTTP GET and HTTP POST | HTTP重定向,SAML SOAP绑定,HTTP POST绑定等

安全弱点 不能抵抗网络钓鱼,OAuth没有使用数据签名和加密等措施,数据安全完全依赖TLS |不能抵抗网络钓鱼,一个钓鱼的IDP如果恶意记录下来用户的OpenID,将会造成很严重的隐私安全问题| XML签名存在漏洞,可能被伪造

使用场景 API 授权 商用应用的单点登录 企业级单点登录,但是对于移动端支持不是很好

  • openid connect

是openid的三代产品,是基于oauth协议,openid1和2只有认证(authenticate)功能,openid connect有了授权(authorize)功能

jwt简介

jwt有以下特性:

  • 自包含性:jwt本身解析出来就包换大量的信息,用户的信息,权限信息等

  • 紧凑性

  • 防篡改:jwt一般base64加密,为了信息不被篡改,有数字请签名等校验方式

jwt是数据形式,这里对jwt进行扩展,包含jws和jwe,jws相当于有数字签名,jwe更加复杂,计算繁琐,信息都是加密的,适合数据传输,不适合token认证。

客户端授权模式

implicit:简化模式,(不安全,适用于纯静态页面应用)

authorization code:授权码模式(功能最完整、流程最严密的授权模式,通常使用在公网的开放平台中)

resource owner password credentials:密码模式(一般在内部系统中使用,调用者是以用户为单位,用在系统间高度信任的场景,因为有密码泄露的风险。)

client credentials:客户端模式(一般在内部系统之间的API调用。两个平台之间调用。比如调用消息服务、日志模块等,调用者是以平台为单位。)

vue对接keycloak

keycloak官网有js的对接方案,这里使用了vue的解决方案,参考:https://github.com/dsb-norge/vue-keycloak-js

引入执行:

npm i --save @dsb-norge/vue-keycloak-js 

关键代码分析:

Vue.use(keycloak , {
    init: {
        onLoad: 'login-required'
    },
    config: {  // 这里配置keycloak的服务地址,和client信息
        url: 'http://192.168.1.186:8080/auth',
        realm: 'test',
        clientId: 'pandora'
    },
    onReady: (keycloak) => {
        // keycloak.token 是keycloak生成的token信息 jwt格式
        console.log("token:"+keycloak.token)
        keycloak.loadUserProfile().success((data) => {
            // 通过token检验用户信息,返回登入用户的username
            console.log("user:"+data.username);
            this.username = data.username;
            // 通过username获取pandora的token
            // generateToken(this.username);
        });
    }
});

注意事项:使用的时候使用的history的路由配置,使用hash会导致重定向问题,dsb-norge开源项目示例也是使用的history配置。

springboot对接keycloak

在pom.xml文件中插入

<!-- keycloak相关-->
    <dependency>
        <groupId>org.keycloak</groupId>
        <artifactId>keycloak-spring-boot-starter</artifactId>
        <version>10.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.keycloak.bom</groupId>
        <artifactId>keycloak-adapter-bom</artifactId>
        <version>10.0.1</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>

在application.yml中配置keycloak服务器和权限相关的

keycloak:
    realm: test
    auth-server-url: http://192.168.1.186:8080/auth
    ssl-required: external
    resource: springboot
    credentials:
        secret:
    use-resource-role-mappings: true
    security-constraints[0]:
        authRoles[0]: admin
        authRoles[1]: user
        securityCollections[0]:
            name: insecure stuff
            patterns[0]: /*

pandora请求接口

简要描述:

通过username换取平台token信息

请求URL:

  • /sso/username_login

请求方式:

  • get

参数:

  • 参数名:username

  • 类型:String

  • 示例:admin

返回示例

1
2
3
4
5
6
{
"msg": "success",
"code": 0,
"expire": 43200,
"token": "69e97d3500f06d594c1d0ba8950f2a76"
}

elasticsearch作为平台搜索在项目中的使用

发表于 2019-09-04 |

elasticsearch介绍:

Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均可被索引,而且每个字段的数据均可被搜索,ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。

项目使用es作为全平台搜索,考虑的是elasticsearch的良好的搜索性,支持中文分词、高亮显示、搜索排序算法、富文本标签过滤处理,大数据量搜索等的特性。

elasticsearch重要概念介绍:

  • index

index是索引,相似文档的集合。项目使用中把mysql中的表的作为各自的索引,这么使用的好处一个是业务逻辑清晰,还有相同文档放在一个index中,无论是同步和mapping都非常便捷,这个后续会提到。

  • document

文档,在项目使用中对应mysql的一行数据。

  • type

类型,这个要注意,在7.x之前版本type是可以有多个的,7.x之后版本,type只有一个,是_doc,所以在接口搜索上面会有区别。

  • Shards & Replicas

分片和副本,分片和副本是es分布式和高可用的体现。控制分片所在节点和控制分片大小来优化es集群,提高es吞吐能力。

  • field

字段,类似mysql中字段的概念。一个document中可以多个字段,字段有不用的类型:long、int、date、string等。

  • mapping

类似mysql的表结构定义。对index进行mapping,可以对document的field类型进行定义,使用的分词器定义、是否对字段进行索引 等功能。
目前5.x 、6.x 7.x的mapping一经定义,是不可以动态修改的。所以需要重新创建index才可以重新定义mapping。

elasticsearch项目中的运用

线上环境使用的是es6.4.3版本,自己测试的单机版本是最近的7.3.0版本,但是7.x版本包括spring-data-elasticsearch的api并不兼容,而且7.x取消了type可以设置多个,所以很多api或者查询并以一致,这里面有很多坑,要踩。

说下项目中的运用,之前是直接通过springboot-data-elasticsearch的api,来创建document,然后搜索也是调用springboot-data-elasticsearch的api来进行搜索。但是有几个问题存在:

  • es侵入到了业务代码中,比如个人文件从回收站回收要重新记录到es里面,但是协同工作,功能的更改,可以要修改es代码,未来维护起来比较麻烦。
  • 历史数据的导入问题,重新创建es索引,或者数据库本身就存在的历史数据、或者项目迁移,es环境更换,都会导致历史数据不能同步的问题

所以是用来logstash-input-jdbc的同步方案。

logstash-input-jdbc同步方案介绍

logstash-input-jdbc是logstash对接关系型数据库的插件,轻量的定时同步工具,logstash天然的和elasticsearch匹配的便利性。支持增量同步和全量同步。但是在实际的项目运行中遇到了一些相对麻烦需要研究的地方:

同步是根据sql来进行,所以设计到复杂数据结构和复杂的sql查询数据处理会比较复杂,要通过logstash的filter的各种插件来进行数据的转化,具体项目中使用的filter后续说明。

logstash-input-jdbc部署

  1. 安装logstash

  2. 安装logstash-input-jdbc插件

    cd bin
    ./logstash-plugin install logstash-input-jdbc
    
  1. 在logstash文件夹下创建:

    mkdir config-mysql
    cd config-mysql
    vim mysql.conf 
    
  1. 输入:(这个只是案例)

    input {
        stdin {
        }
        jdbc {
          # 数据库
          jdbc_connection_string => "jdbc:mysql://localhost:3306/test01"
          # 用户名密码(你的数据库的用户名和密码)
          jdbc_user => "root"
          jdbc_password => "password"
          # jar包的位置
          jdbc_driver_library => "/usr/local/logstash-5.6.3/mysql-connector-java-5.1.41.jar"
          # mysql的Driver
          jdbc_driver_class => "com.mysql.jdbc.Driver"
          jdbc_paging_enabled => "true"
          jdbc_page_size => "50000"
          # statement_filepath代码sql文件,statement是sql,二选一就可以
          #statement_filepath => "config-mysql/test02.sql"
          statement => "select * from test02"
          # schedule 定时器分别代表“分 小时 天  月 年”,最小的单位是1分钟
          schedule => "* * * * *"
          #索引的类型,在filter和output进行逻辑判断时候使用
          type => "test02"
        }
    }
    
    filter {
        json {
            source => "message"
            remove_field => ["message"]
        }
    }
    
    output {
        elasticsearch {
            hosts => "127.0.0.1:9200"
            # index名
            index => "test01"
            # 需要关联的数据库中有有一个id字段,对应索引的id号
            document_id => "%{id}"
        }
        stdout {
            codec => json_lines
        }
    }
    
  2. 启动:

    ./logstash -f config-mysql/mysql.conf
    

项目中对es Mapping的使用

项目最开始创建mapping是通过使用springboot-data-elasticsearch的注解方式来创建mapping

@Document(indexName = "article",type = "docs")
public class EsArticlesEntity {

    /**
     * id
     */
    @Id
    private Long id;

    /**
     * GUID
     */
    @Field(type=FieldType.Text)
    private String guid;

    /**
     * 标题
     */
    @Field(type=FieldType.Text, analyzer="ik_max_word",store=true)
    private String title;
}

但是在通过es的查询mapping 接口,发现只有index和field创建成功了,但是字段的类型并没有生效,而是自动生成的,spring-boot-elasticsearch api并没有完全紧跟es的更新步伐,可能是注解接口没有生效导致的。

这里使用了ik_max_word的analyzer(分析器),是通过es安装的ik分词器,ik有很好的中文分词效果,ik_max_word是把一段话用最小的颗粒度来进行分词,ik_smart是智能分词,颗粒度较大。

es安装ik:

cd elasticsearch-7.3.0/bin/
./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.3.0/elasticsearch-analysis-ik-7.3.0.zip
cd analysis-ik/
vim IKAnalyzer.cfg.xml

修改:

<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">extra_single_word_full.dic</entry>

重启即可

自定义analyzer

项目实际中有富文本,所以会有大量的标签页,对这些HTML标签需要过滤,否则会影响搜索的结果。所以需要使用自定义的分析器,analyzer由几部分构成:从文档中提取词元(Token)的算法称为分词器(Tokenizer),在分词前预处理的算法称为字符过滤器(Character Filter),进一步处理词元的算法称为词元过滤器(Token Filter),最后得到词(Term)。这整个分析算法称为分析器(Analyzer)。使用html_strip的过滤器可以对html标签进行过滤,不过这里在6.x版本需要注意,使用的时候需要先停用索引,创建好analyzer在开启索引

curl -XPOST 'localhost:9200/article/_close'

curl -XPUT "http://localhost:9200/article/_settings" -H 'Content-Type: application/json' -d'
{
  "index": {
    "analysis": {
      "analyzer": {
        "ik-html": {
          "char_filter": [ "html_strip" ],
          "tokenizer": "ik_max_word"
        }
      }
    }
  }
}'

curl -XPOST 'localhost:9200/article/_open'

ik-html是自己定义的名称,之后可以在创建mapping中可以使用ik-html这个analyzer了。

项目实际运用的重点代码梳理

概述

重要字段:

字段 | 类型 |  描述  
-    |-    |  -
id | long |  |
guid | text |  |
title | text |标题  |
subTitle | text |副标题  |
createTime | long |  |
content | text | 内容 |
organizationIds | long |可见组织(鉴权),是数组  |
hasOpen | integer |是否可见(鉴权)  |
validityDate | long | 有效期限(文章可见的条件)  |
validityDateType | integer | 是否永久 |
creatorName | text |  |

搜索需要根据title、subtitle、content这3个字段的来进行字符串搜索,通过organizationIds、hasopen、validitydate和validitydatetype字段来控制搜索范围,具有一定权限的和在有效期限的document才可以被搜索到。

springboot-data-elasticsearch重要代码

//用户所在组织是否有读取权限,获取用户所在组织id,组织builder
BoolQueryBuilder organizationBuilder = QueryBuilders.boolQuery();
Long[] organizations = null ;
if (StringUtils.isNotBlank(user.getOrganizations())) {
    String[] orgIds = user.getOrganizations().split(",");
    organizations = (Long[]) ConvertUtils.convert(orgIds, Long.class);
}
if(organizations!=null&&organizations.length>0){
    for(int i = 0;i<organizations.length;i++){
        organizationBuilder.should(QueryBuilders.termQuery("organizationIds",organizations[i]));
    }
}
organizationBuilder.should(QueryBuilders.termQuery("hasOpen",1));
organizationBuilder.should(QueryBuilders.termQuery("userIds",user.getUserId()));

// 判断文件是否在有效期内
BoolQueryBuilder validityTimeBuilder = QueryBuilders.boolQuery();
validityTimeBuilder.should(QueryBuilders.termQuery("validityDateType",1));
validityTimeBuilder.should(QueryBuilders.rangeQuery("validityDate").gt(new Date().getTime()));
//生成查询builder
QueryBuilder boolBuilder = QueryBuilders.boolQuery().must(organizationBuilder).must(validityTimeBuilder);

// 查询q,搜索title、内容、subtitle
if(StringUtils.isNotBlank(form.getQ())){
    //QueryBuilder stringBuilder = QueryBuilders.queryStringQuery(form.getQ()).field("title").field("content").field("subTitle");

    MultiMatchQueryBuilder matchQuery = QueryBuilders.multiMatchQuery(form.getQ(),"title","content","subTitle");

    ((BoolQueryBuilder) boolBuilder).must(matchQuery);
}

es搜索should相当于mysql的or,must相当于mysql的and

term搜索是精确查询,这里用来对权限和有效期间进行过滤,match是模糊查询,用来查询title、content、subtitle等字段

SearchQuery searchQuery = new NativeSearchQueryBuilder()
                //设置查询条件
                .withIndices(str)
                .withQuery(boolBuilder)
                // 高亮设置
                .withHighlightFields(new HighlightBuilder.Field("content")
                                .preTags("<em>")
                                .postTags("</em>"),
                        new HighlightBuilder.Field("title")
                                .preTags("<em>")
                                .postTags("</em>"),
                        new HighlightBuilder.Field("subTitle")
                                .preTags("<em>")
                                .postTags("</em>")
                )
                //设置分页信息
                .withPageable(pageable)
                //创建SearchQuery对象
                .build();

构建searchquery,对需要的高亮字段进行处理。

创建mapping

这里以article索引为例:

curl -XDELETE "http://localhost:9200/article"

curl -XPUT "http://localhost:9200/article/"

curl -XPOST 'localhost:9200/article/_close'

curl -XPUT "http://localhost:9200/article/_settings" -H 'Content-Type: application/json' -d'
{
  "index": {
    "analysis": {
      "analyzer": {
        "ik-html": {
          "char_filter": [ "html_strip" ],
          "tokenizer": "ik_max_word"
        }
      }
    }
  }
}'

curl -XPOST 'localhost:9200/article/_open'

curl -XPOST "http://localhost:9200/article/_mapping" -H 'Content-Type: application/json' -d'
{
    "properties" : {
          "content" : {
            "type" : "text",
            "analyzer": "ik-html",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "createTime" : {
            "type" : "date"
          },
     "validityDate" : {
            "type" : "date"
          },
          "creatorName" : {
            "type" : "text",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "guid" : {
            "type" : "text",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "hasOpen" : {
            "type" : "long"
          },
          "id" : {
            "type" : "long"
          },
          "subTitle" : {
            "type" : "text",
            "analyzer": "ik_max_word",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "title" : {
            "type" : "text",
            "analyzer": "ik_max_word",
            "fields" : {
              "keyword" : {
                "type" : "keyword",
                "ignore_above" : 256
              }
            }
          },
          "validityDateType" : {
            "type" : "long"
          }
        }
}'

logstash-input-jdbc重要片段

在mysql.conf的input jdbc中需要加上这段话,否者默认会有小写转化

lowercase_column_names => false

logstash配置的mysql.conf 文件,organizationIds需要把“11,22”的字符串转化成[11,22]的整形数组;

letterType存放json格式的文件,这里需要转化下,把1转成{“letterType”:1}的形式

filter {
    json {
        source => "message"
        remove_field => ["message"]
    }
    if[type] == "document"{
      mutate {
        split => ["organizationIds",","]  #organizationIds把“11,22”的字符串转化成[“11”,“22”]的字符串数组
      }
      mutate {
        convert => {"organizationIds" => "integer"}  #这里直接通过convert可以将字符串数组[“11”,“22”]转成整形数组[11,22]
      }
    }
    if[type] == "letter"{
      mutate {
        update => { "letterType" => "{\"letterType\":%{letterType}}"}
        split => ["userIds",","]
      }
      mutate {
        convert => {"userIds" => "integer"}
      }
    }
    。。。。。省略
}

mysql同步文件,最后一句通过和sql_last_value的比较进行增量查询,如果去掉的话,就是全量查询

SELECT
        oa_documents.document_id as id,
        document_guid as guid,
        document_name as title,
        oa_documents.create_time as createTime,
        expire_infinity as validityDateType,
        expire_time as validityDate,
                IF(b.organizationIds = 0, 1, 0) as hasOpen,
                b.organizationIds
FROM
        oa_documents,
        (
SELECT
        oa_documents.document_id,
        group_concat(oa_document_organization_rss.organization_id) organizationIds
FROM
        oa_documents
        LEFT JOIN oa_document_organization_rss ON oa_documents.document_id = oa_document_organization_rss.document_id
GROUP BY
        oa_documents.document_id
        ) b
WHERE
        oa_documents.document_id IN ( b.document_id ) and oa_documents.has_directory=2 and
        oa_articles.create_time >= :sql_last_value

后续

通过项目es的基本使用掌握差不多了,后续通过项目进展有2点改进方向:

  1. 搜索结果的优化:搜索结果的优化方式很多,可以通过调整分片、调整field得分比重等多个方面根据业务进行优化
  2. 联想搜索:对用户输入进行联想搜索,提高用户体验

elk日志系统搭建

发表于 2019-03-14 |

elk介绍:

elasticsearch+logstash+kibana作为业界主流的日志存储分析平台,是相当成熟的解决方案。logstash作为日志采集、处理工具,elasticsearch作为日志存储分析工具,kibana作为日志展示工具。对这套很多也进行优化,比如logstash比较消耗资源,用相对轻量的日志采集工具来替代,还有使用redis、kafka等缓存技术,解决logstash无缓存的功能。做到高可用和分布式的架构,很多使用logstash shipper(只做采集使用,不消耗目标节点上面运行的资源)+kafka(作为消息中间件,缓存使用)+logstash indexer(统一的消息收集、处理、创建索引,通畅单独部署)+elasticsearch集群+kibana。我这回部署的是基本的logstash shipper+elasticsearch+kibana。

elk部署:

搭建集群准备(5台CenterOS 7.2的机器):

  • 102:elasticsearch
  • 103:elasticsearch
  • 105:elasticsearch
  • 45:logstash shipper
  • 86:kibana

首先安装elasticsearch:

修改配置文件:

vim config/elasticsearch.yml

主要修改了2点:

# ---------------------------------- Network -----------------------------------
#
# Set the bind address to a specific IP (IPv4 or IPv6):
#
network.host: 0.0.0.0           ##服务器ip 本机
#
# Set a custom port for HTTP:
#
http.port: 9200                 ##服务端口
#
# For more information, consult the network module documentation.
#

启动之后报错:

java.lang.RuntimeException: can not run elasticsearch as root

是因为elasticsearch有用户输入的执行文件,所以为了安全考虑不让使用root账户,不过可以通过修改elasticsearch允许root运行,不过不推荐这种办法,建议创建用户

创建elsearch用户组及elsearch用户

[root@cty-kftest08 /usr]$ groupadd elasticsearch
[root@cty-kftest08 /usr]$ useradd es -g elasticsearch 
[root@cty-kftest08 /usr]$ passwd es  (设置的!QAZ2wsx)

更改elasticsearch文件夹及内部文件的所属用户及组为es:elasticsearch

[root@cty-kftest08 /usr]$ chown -R es:elasticsearch elasticsearch-6.6.1

Prometheus架构

查看下用户状态

Prometheus架构

Prometheus架构

切换es用户启动:

[root@cty-kftest08 /usr]$ su es
[es@cty-kftest08 /usr]$ cd elasticsearch-6.6.1/bin/
[es@cty-kftest08 /usr/elasticsearch-6.6.1/bin]$ ./elasticsearch

报错:

Prometheus架构

ERROR: [1] bootstrap checks failed
[1]: max file descriptors [65535] for elasticsearch process is too low, increase to at least [65536]

说的意思是最大文件数至少应该在65536

查询终端的最大句柄数:

Prometheus架构

发现确实有限制

解决办法:
修改linux的软硬件限制文件/etc/security/limits.conf
在文件尾部添加如下代码:

* soft nofile 131072
* hard nofile 131072

*代表所有的用户,而这里我们只修改es用户

Prometheus架构

查看下,改的生效了:

Prometheus架构

再次启动下,就可以了,成功

Prometheus架构

以上是部署了elasticsearch单机,接下来部署集群模式:

配置文件

Prometheus架构

Prometheus架构

分享别人的配置文件:

#集群的名称
cluster.name: es6.2
#节点名称,其余两个节点分别为node-2 和node-3
node.name: node-1
#指定该节点是否有资格被选举成为master节点,默认是true,es是默认集群中的第一台机器为master,如果这台机挂了就会重新选举master
node.master: true
#允许该节点存储数据(默认开启)
node.data: true
#索引数据的存储路径
path.data: /usr/local/elk/elasticsearch/data
#日志文件的存储路径
path.logs: /usr/local/elk/elasticsearch/logs
#设置为true来锁住内存。因为内存交换到磁盘对服务器性能来说是致命的,当jvm开始swapping时es的效率会降低,所以要保证它不swap
bootstrap.memory_lock: true
#绑定的ip地址
network.host: 0.0.0.0
#设置对外服务的http端口,默认为9200
http.por。、放在括号内。默认为127.0.0.1, [::1]
discovery.zen.ping.unicast.hosts: ["192.168.8.101:9300", "192.168.8.103:9300", "192.168.8.104:9300"]
#如果没有这种设置,遭受网络故障的集群就有可能将集群分成两个独立的集群 - 分裂的大脑 - 这将导致数据丢失
discovery.zen.minimum_master_nodes: 2

其他机器也同样去做,另外两台机器报错:

Prometheus架构

[root@cty-kftest10 elasticsearch-6.6.1]# vim /etc/sysctl.conf 

加入:

vm.max_map_count=262144

Prometheus架构

验证下:

Prometheus架构

输入: curl -XGET ‘http://172.29.4.103:9200/_cat?pretty‘ 来验证部署成功

安装logstash:

解压安装包:logstash

首先安装shipper,编辑一个shipper的文件

Prometheus架构

后台启动 :

nohup ./bin/logstash -f config/shipper.conf &

验证下:成功

Prometheus架构

Prometheus架构

安装kinaba:

vim config/kibana.yml

修改配置文件:

server.port: 5601

启动即可

需要在页面里配置下

在Management中Create index pattern 添加logstash-*,可以访问到

日志系统websocket服务

发表于 2018-12-27 |

websocket介绍

WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点:1.WebSocket是一种双向通信协议,在建立连接后,WebSocket服务器和客户端都能主动的向对方发送或接收数据,就像Socket一样,不同的是WebSocket是一种建立在Web基础上的一种简单模拟Socket的协议;2.WebSocket需要通过握手连接,类似于TCP它也需要客户端和服务器端进行握手连接,连接成功后才能相互通信。

websocket的springboot实现

创建一个maven工程,pom中加入依赖

如果是运行在tomcat下,主要引入javaee-api,不过使用springboot的内置tomcat就不需要了

<dependencies>    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
        <version>1.3.5.RELEASE</version>
    </dependency>
</dependencies>

配置端口和静态资源

创建application.properties文件,添加server端口和静态资源路径

server.port=8080
spring.resources.static-locations=classpath:/webapp/html/

websocket服务端的实现

首先是springboot的入口

package com.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
@ComponentScan
public class Application
{


  public static void main(String[] args)
  {
    SpringApplication.run(Application.class, args);
  }
}

注入ServerEndpointExporter的bean

package com.web;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

websocket的具体实现

package com.web;

import java.io.IOException;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.stereotype.Component;

@ServerEndpoint("/websocket")
@Component
public class MyWebSocket {

    @OnMessage
    public void onMessage(String message, Session session) 
        throws IOException, InterruptedException {

        // Print the client message for testing purposes
        System.out.println("Received: " + message);

        // Send the first message to the client
        session.getBasicRemote().sendText("This is the first server message");

        // Send 3 messages to the client every 5 seconds
        int sentMessages = 0;
        while(sentMessages < 3){
            Thread.sleep(5000);
            session.getBasicRemote().
                sendText("This is an intermediate server message. Count: " 
                    + sentMessages);
            sentMessages++;
        }

        // Send a final message to the client
        session.getBasicRemote().sendText("This is the last server message");
    }

    @OnOpen
    public void onOpen () {
        System.out.println("Client connected");
    }

    @OnClose
    public void onClose () {
        System.out.println("Connection closed");
    }
}

一个简单的demo,websocket一共有四种事件

  • onopen websocket建立连接完成
  • onclose websocket连接关闭
  • onmessage websocket接收到数据,发送数据调用socket.send方法
  • onerror websocket发生错误

websocket的客户端的实现

websocket两种实现方法,一种是sockjs,一种是HTML5,这里使用HTML5实现,创建一个index.html

<!DOCTYPE HTML>
<html>
<head>
    <title>My WebSocket</title>
</head>

<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button>    <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>

<script type="text/javascript">
    var websocket = null;

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8080/websocket");
    }
    else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(event){
        setMessageInnerHTML("open");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //关闭连接
    function closeWebSocket(){
        websocket.close();
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>
</html>

运行Application.java,页面访问http://127.0.0.1:8080/index.html,可以通过客户端发送消息,也可以接受服务端返回的消息

这样基本实现了简单的websocket通讯,接下来websocket的客户端接入echars的接口,实现日志系统的前端展示

websokcet安全模式

websocket的安全模式需要创建wss实例,wss是基于https握手协议的,我们的cf平台统一配置了https协议,所以不用再配置ssl,直接创建wss实例即可

if(window.location.protocol == 'http:'){
         websocket = new WebSocket("ws://localhost:8080/websocket");
}else{
         websocket = new WebSocket("wss://localhost:8080/websocket");
}

websocket的token验证

websocket的数据传输安全模式通过wss,客户端和服务端握手的时候采用https协议,这时的安全需要token验证

websocket = new WebSocket(“wss://localhost:8080/websocket?token=*“)
用户登入平台的时候获取token,服务端拿到token去平台验证

 private static void tokenAuth(String token) {
 //发送 GET 请求
 String s=HttpPost.sendGet("http://&&&&&", "token");
 }


 public static String sendGet(String url, String param) {
    String result = "";
    BufferedReader in = null;
    try {
        String urlNameString = url + "?" + param;
        URL realUrl = new URL(urlNameString);
        // 打开和URL之间的连接
        URLConnection connection = realUrl.openConnection();
        // 设置通用的请求属性
        connection.setRequestProperty("accept", "*/*");
        connection.setRequestProperty("connection", "Keep-Alive");
        connection.setRequestProperty("user-agent",
                "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
        // 建立实际的连接
        connection.connect();
        // 获取所有响应头字段
        Map<String, List<String>> map = connection.getHeaderFields();
        // 遍历所有的响应头字段
        for (String key : map.keySet()) {
            System.out.println(key + "--->" + map.get(key));
        }
        // 定义 BufferedReader输入流来读取URL的响应
        in = new BufferedReader(new InputStreamReader(
                connection.getInputStream()));
        String line;
        while ((line = in.readLine()) != null) {
            result += line;
        }
    } catch (Exception e) {
        System.out.println("发送GET请求出现异常!" + e);
        e.printStackTrace();
    }
    // 使用finally块来关闭输入流
    finally {
        try {
            if (in != null) {
                in.close();
            }
        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }
    return result;
}

JS Promise用法

发表于 2018-12-27 |

Promise概述

promise对象用于异步计算,相比于回调函数,promise对象除了规定的方法都是不可用的,牺牲了一定自由度的同时,可以将复杂的异步处理模式化。

句法

Promise( /* executor */ function(resolve, reject) { ... } );

new一个Promise构造器进行实例化,返回一个promise对象:

var promise = new Promise(function(resolve, reject) {
    // 异步处理
    // 处理结束后、调用resolve 或 reject
});

Promise 状态

一个promise实例有一下几个状态(默认是Promises/A+的描述状态):

  • pending:初始化阶段,没有fulfilled或者是rejected
  • fulfilled:操作成功完成(promise resolve)
  • rejected:操作失败(promise reject)

如果promise处于pending状态,处理promise的处理模块(handler)就会处于排队状态pending状态可以变成fulfilled或者rejected状态,不过fulfilled状态和rejected状态是不可相互转换的。promise从pending状态到fulfilled或者rejected状态,.then处理只会被调用一次,比如:

var promise = new Promise(function(resolve,reject){
var n = 6
if(n<3)
resolve(console.log('yes')); //成功了调用回调函数
else
reject(new Error(console.log('this is an error'))); //失败调用reject并抛出异常
})

var promise = new Promise(function(resolve,reject){
    var i = 2;
    if(i<3)
    resolve(setTimeout("alert('hello')",5000));
    else
    reject(new Error(console.log('no')));
})
promise.then(console.log('ok'));//这个片段不理解啦,怎么先执行的then,再执行的resolve回调函数呀

Promise 链

Promise.prototype.then()和Promise.prototype.then()方法返回promise对象,就成为promise链,如图:

chains

方法

  • Promise.all()

返回promise在迭代声明中是fulfilled还是reject,如果是fulfill,就返回一个数组,值是按照在迭代器中的顺序成功了的promises。如果返回不成功(reject)的promise,返回迭代器中第一个失败对象的原因。这个方法可以使用在有多个promise的情况下。

  • Promise.race()

如有有一个迭代中的promise被fulfilled或者rejected,这个方法立即返回处理值

  • Promise.reject()

返回被reject的原因

  • Promise.resolve()

返回处理成功的promise对象

例子

'use strict';
var promiseCount = 0;

function testPromise() {
    var thisPromiseCount = ++promiseCount;

    var log = document.getElementById('log');
    log.insertAdjacentHTML('beforeend', thisPromiseCount +
        ') Started (<small>Sync code started</small>)<br/>');

    // We make a new promise: we promise a numeric count of this promise, starting from 1 (after waiting 3s)
    var p1 = new Promise(
        // The resolver function is called with the ability to resolve or
        // reject the promise
        function(resolve, reject) {
            log.insertAdjacentHTML('beforeend', thisPromiseCount +
                ') Promise started (<small>Async code started</small>)<br/>');
            // This is only an example to create asynchronism
            window.setTimeout(
                function() {
                    // We fulfill the promise !
                    resolve(thisPromiseCount);
                }, Math.random() * 2000 + 1000);
        }
    );

    // We define what to do when the promise is resolved/fulfilled with the then() call,
    // and the catch() method defines what to do if the promise is rejected.
    p1.then(
        // Log the fulfillment value
        function(val) {
            log.insertAdjacentHTML('beforeend', val +
                ') Promise fulfilled (<small>Async code terminated</small>)<br/>');
        })
    .catch(
        // Log the rejection reason
        function(reason) {
            console.log('Handle rejected promise ('+reason+') here.');
        });

    log.insertAdjacentHTML('beforeend', thisPromiseCount +
        ') Promise made (<small>Sync code terminated</small>)<br/>');
}

浏览器兼容

Feature Chrome Edge Firefox InternetExplorer Opera Safari Servo
Promise 32.0 (Yes) 29.0 No support 19 7.1 No support

项目中的使用

项目中使用Angular,链式操作异步请求也是使用的promise($q)
代码片段:

applicationService.getApplications().then(function(resp){
        var data = resp.data.resources;
        var promises = [];
        $scope.total = resp.data.total_results;
        angular.forEach(data,function(app,i) {
            promises.push($scope.whatState(app));
        })
        $q.all(promises).then(function(){
            $scope.fillDatenation($scope.nrOfStartedApps,$scope.nrOfStoppedApps,$scope.nrOfAbnormal);
        })
    })

$scope.whatState = function(app) {
        var defer = $q.defer();
        if (app.entity.state === 'STOPPED') {
            $scope.nrOfStoppedApps++;
            defer.resolve();
        } else {
                applicationService.getInstanceInfo(app.metadata.guid).then(function (response) {
                var data = response.data;
                var n = 0;
                angular.forEach(data, function (_data) {
                    if (_data.state =="RUNNING") {
                        n++;
                    }
                })
                if (n > 0) {
                    $scope.nrOfStartedApps++;
                } else {
                    $scope.nrOfAbnormal++;
                }
                defer.resolve();
            })
        }
        return defer.promise;
    }

本文参考

MDN :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

JS Promise迷你书:http://www.kancloud.cn/kancloud/promises-book/44251

linux操作

发表于 2018-06-05 |

linux修改时区:

修改时区

export TZ='Asia/Shanghai'
source ~/.bashrc

更改 Linux 系统整个系统范围的时区可以使用如下命令

sudo rm -f /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

如果报错unknown hosts,需要修改linux host的名称

vi /etc/hosts

添加上127.0.0.1的主机别名

监控之prometheus

发表于 2018-06-01 |

首先安装prometheus,这步直接下载二进制包,解压,执行就可以了

在https://prometheus.io/download/下载最新版本的,下载后执行:

tar xzvf prometheus- .tar
cd prometheus...
nohup ./prometheus & 

之后访问localhost:9090/graph,可以看到图表

配置node:

在https://prometheus.io/download/#node_exporter 下载最新版本的tar包

tar -xzf node_exporter-0.15.2.darwin-amd64.tar.gz
cd node_exporter-0.15.2.darwin-amd64
cp node_exporter-0.15.2.darwin-amd64/node_exporter /usr/local/bin/
nohup node_exporter &

prometheus架构

Prometheus架构

prometheus server都是通过pull的方式来获取监听的exports,这样会有server网络请求压力过高的问题,因为需要定时不停的发送http请求,去请求数据,那么压力都在server这里

prometheus是一个时序数据库,存储在本地的磁盘,这里就会有个问题,就是存储性能和存储方式类别,以及是否定时的清理文件,还有查询效率的问题,查询历史监控信息的效率问题

export是通过http暴露给server,让server来拉取数据,两种export,一种是集成好的,例如:cAdvisor,Kubernetes,Etcd,Gokit,对prometheus是支持的,另外一种是不支持prometheus,需要自己需要通过Prometheus提供的Client Library来写export,例如:Mysql Exporter,JMX Exporter,Consul Exporter。

Promql

在形式上,所有的指标(Metric)都通过如下格式标示:

<metric name>{<label name>=<label value>, ...}

其中以作为前缀的标签,是系统保留的关键字,只能在系统内部使用。标签的值则可以包含任何Unicode编码的字符。在Prometheus的底层实现中指标名称实际上是以name__=的形式保存在数据库中的,因此以下两种方式均表示的同一条time-series:

api_http_requests_total{method="POST", handler="/messages"}
{__name__="api_http_requests_total",method="POST", handler="/messages"}

PromQL支持使用=和!=两种完全匹配模式:

PromQL还可以支持使用正则表达式作为匹配条件,多个表达式之间使用|进行分离:

使用label=~regx表示选择那些标签符合正则表达式定义的时间序列;
反之使用label!~regx进行排除;

http_requests_total{code=~"400|200"}

区间向量表达式:

http_requests_total{handler="prometheus"}[1m]

除了使用m表示分钟以外,PromQL的时间范围选择器支持其它时间单位:

  • s - 秒
  • m - 分钟
  • h - 小时
  • d - 天
  • w - 周
  • y - 年

http_request_total{} # 瞬时向量表达式,选择当前最新的数据

http_request_total{}[5m] # 区间向量表达式,选择以当前时间为基准,5分钟内的数据

而如果我们想查询,5分钟前的瞬时样本数据,或昨天一天的区间内的样本数据呢? 这个时候我们就可以使用位移操作,位移操作的关键字为offset。

http_requests_total{handler="prometheus"} offset 1d

比较 http_requests_total > bool 100 和 http_requests_total > 100

在PromQL操作符中优先级由高到低依次为:

  • ^
  • *, /, %
  • +, -
  • ==, !=, <=, <, >=, >
  • and, unless
  • or

聚合操作

sum(http_requests_total)  //请求总量
count_values("count", http_requests_total) //以count为标签,个数
topk(5, http_requests_total)  //排序
quantile(0.5, http_requests_total)  //中位数

prometheus告警

prometheus 自定义告警规则,如果触发告警规则,prometheus server把告警信息推送给单独的模块(AlertManager)进行处理

这是一条规则定义,如果触发,首先在(for)定义的时间范围内是pending状态,超过了这个时间,就是FIRING状态

Prometheus架构

Prometheus架构

'单线程'的浏览器

发表于 2018-05-08 |

之前一直会有疑问,js是单线程的,那如何实现的前端向后台发送多个http请求,还同时维护这么多请求的会话,今天就来深入探寻下浏览器线程和js线程

首先,js的单线程是指浏览器的js引擎是单线程的,不过浏览器还包含了其他的线程:

  • js引擎线程
  • 页面渲染线程
  • http请求线程
  • 事件触发线程
  • EventLoop轮询的处理线程

这几个线程的主要作用:

页面渲染线程:就是负责渲染页面的,因为js会影响页面的渲染,所以页面渲染线程和js引擎是互斥的

js引擎线程:js引擎线程也不是单线程的,而是包含主线程和其他线程,主线程是主要处理js任务的,是从消息队列里去拿任务进行单线程处理,那就需要一个线程负责把js任务放入到消息队列中,这就需要Eventloop的轮询,查看页面交互,点击事件,异步的http线程返回的结果,之后要处理,就会放到消息队列中

setTimeout(function () { while (true) { } }, 1000);
    setTimeout(function () { console.log('end 2'); }, 2000);
    setTimeout(function () { console.log('end 1'); }, 100);
    console.log('end');

这段代码中,会输出 end,end1,然后浏览器假死,为什么按照时间,console.log(‘end 2’);这个方法是在消息队列中排在while (true) { } 执行的后面,不过while (true) { }这个同步任务js的引擎线程会一直执行,所以永远执行不到console.log(‘end 2’),除非中间跳出任务,才会执行到下个任务

html5也推出了多线程功能,Worker类

//主线程
worker.addEventListener('message', function(e){
    //e.data为从worker线程得到的数据
});
worker.possMessage(...);//向worker线程发送数据


//工作线程
addEventListener('message', function(e){
    //e.data为从主线程得到的数据
});
possMessage(...);//向主线程发送数据

例如:

//worker.js
function calculate(){
  var ret = 0;
  for(var i = 1; i <= 1e9; i++){
    ret += i;
  }
  return ret;
}
postMessage(calculate());


//demo.js
var worker = new Worker('scripts/worker.js');
worker.addEventListener('message', function(e){
  console.log(e.data); //500000000067109000
}, false);        

监控之zabbix安装

发表于 2018-04-25 |

服务器:ubuntu14.04

其他组件版本:

  • apache : 2
  • php : 5
  • zabbix : 2.2.2
sudo apt-get install apache2      

sudo apt-get install php5  

sudo apt-get install libapache2-mod-php5  

sudo /etc/init.d/apache2 restart // 重启apache,此时php5已经可用了  

sudo apt-get install mysql-server//数据库账号:root密码:123456  

sudo apt-get install libapache2-mod-auth-mysql  

sudo apt-get install php5-mysql  

sudo /etc/init.d/apache2 restart // 再次重启apache,使新服务正常激活  

然后在终端输入:

sudo ls /etc/apache2/mods-enabled  

看看这个目录下(默认存在),有没有php5.conf 和 php5.load,如果没有则:

sudo a2enmod php5  

启用 php 模块,然后重启apache即可。OK,apache、php5、mysql都已经可用了。

编辑apache文件,配置serverName

vi /etc/apache2/apache2.conf  
ServerName 127.0.0.1

重启apache

sudo /etc/init.d/apache2 restart         

通过http://127.0.0.1 ,已经可以访问apache静态资源服务器了.

配置PHP参数

vi /etc/php5/apache2/php.ini

内容如下:

date.timezone = Asia/Shanghai  

max_input_time = 600  

max_execution_time = 600  

post_max_size =32M  

sudo /etc/init.d/apache2 restart

上面这么多都没有出问题的话 可以开始安装zabbix了

安装zabbix服务端

sudo apt-get install zabbix-server-mysql 

修改/etc/default/zabbix-server文件

START=yes

配置mysql的zabbix配置,根据这个文档

cat /usr/share/doc/zabbix-server-mysql/README.Debian

按照步骤来:

  • mysql -p -e “create database zabbix character set utf8”

  • mysql -p -e “grant all on zabbix.* to ‘zabbix’@’localhost’ identified by ‘zabbix’”

  • zcat /usr/share/zabbix-server-mysql/{schema,images,data}.sql.gz \
    | mysql -uzabbix -pzabbix zabbix

启动zabbix

/etc/init.d/zabbix-server start

安装zabbix 网站程序

apt-get install zabbix-frontend-php

装完以后再做一件事:

cp -r /usr/share/zabbix /var/www/html/zabbix
因为apt-get 安装完以后 默认到/usr/share/下了 这样就会造成 网页打不开了 ,所以复制到/var/www/html/下

重启zabbix

/etc/init.d/zabbix-server restart 

访问http://127.0.0.1/zabbix 就到了访问界面

到了这步:Check of pre-requisites,发现php有很多错误,重启一下apache就可以啦,再retry下

安装完以后更改下面这个文件

vi /etc/zabbix/zabbix_server.conf

检查一下DBuser 与你在WEB界面的配置相同
将DBpasswd前面的注释去掉,后面加上你数据库的密码 重新启动一下zabbix OK!即可监控自身

install阶段缺少配置文件,下载下来,再拷贝到目标服务器即可 /etc/zabbix/

安装成功之后提示登入:

默认的初始用户名和密码是:Admin zabbix

zabbix-agent 安装

apt-get install zabbix-agent
vi /etc/zabbix/zabbix_agent.conf
/usr/sbin/zabbix_agentd -c /etc/zabbix/zabbix_agentd.conf

日志系统之mongodb(一)

发表于 2018-04-24 |

PaaS平台处理大量的日志,文本格式的,主要是insert和select操作,我们采用mongodb的nosql数据库进行存储。

因为高峰的请求量大,所以我们做了分片处理,这样能减少请求的压力,为了保证数据的安全性,我们进行副本备份

12
amhere

amhere

19 日志
4 标签
© 2020 amhere
由 Hexo 强力驱动
主题 - NexT.Pisces