Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

提高首屏页面加载速度,解决vue-cli打包后单个文件过大的问题 - 请叫我宋某某 - 博客园

$
0
0

本教程是针对vue-cli3以上的版本,其实原理都大同小异,这个demo为vue-cli直接创建的项目,并在main.js中引入了 echartelement-uilodash

首先看demo打包后生成的文件大小,这个demo里面什么业务都没写、仅仅引入了几个包,chunk-vendors.js就达到了1.6M之多,如果是写入了庞大的业务后没做任何优化处理,那么这个文件可能会达到10M之多,这发生在我真实的项目经历中

借助webpack-bundle-analyzer帮助分析

首先安装webpack-bundle-analyzer

yarn add webpack-bundle-analyzer -D

然后在项目根目录创建vue.config.js,然后在文件中写入以下代码

const WebpackBundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
    configureWebpack: {
        plugins: [new WebpackBundleAnalyzerPlugin()]
    }
}

然后执行 yarn build在浏览器会自动打开我们的使用包分析文件,大致如下图的样子

我们可以看到生成的最大文件为 chunk-vendors.js,这个文件主要又由 echartselement-uilodashzrender组成,其中 echartselement-uilodash为我们项目开发时必要引入的文件,所以我们如果把这写文件分离出来那么文件自然就会小了很多

如何分离这些文件

我们可以借助wepack配置项里面的 externals来达到目的,在 vue.config.js里面加入如下代码

module.exports = {

    // ... other code
    
    configureWebpack: {
    
    // ... other code
    
        externals: {
            "lodash": '_',"vue": 'Vue',"echarts": 'echarts',"element-ui": 'ELEMENT',
        },
        // ... other code
    }
    // ... other code
}

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。我们可以通过script标签引入这些资源,具体关于externals的介绍请点击 这里
然后我们再对应的 public -> index.html文件加入以下代码,其中BASE_URL指的是public目录,你需要从官网下载对应的资源放在对应的目录下

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><link rel="icon" href="<%= BASE_URL %>favicon.ico"><title>optimize_vue</title></head><body><noscript><strong>We're sorry but optimize_vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><!-- built files will be auto injected --><script src="<%= BASE_URL %>js/pace.min.js"></script><script src="<%= BASE_URL + (process.env.NODE_ENV === 'development' ? 'js/vue-2.6.0.js' : 'js/vue-2.6.0.min.js') %>"></script><script src="<%= BASE_URL %>js/element-ui-2.11.1.js"></script><script src="<%= BASE_URL %>js/lodash.min.js"></script><script src="<%= BASE_URL %>js/echarts.min-4.4.0.js"></script></body></html>

然后我们再看一下效果, lodash、echarts、elementui已经成功从webpack bundle分离出去了,打包后的 chunk-vendors.js也只有24kb的大小

使用路由懒加载

路由懒加载会只加载当前页面需要的资源,代码如下

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
// or
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

注意如果使用import方式的引入需要安装 @babel/plugin-syntax-dynamic-import,然后在babel.config.js中加入下面代码即可

module.exports = {
  // ... other code"plugins": ["@babel/plugin-syntax-dynamic-import"]
  // ... other code
}

关于process.env.NODE_ENV

相信大家在开发的时候一定会有跟多个开发环境,比如development/feature/sandbox/production,因为这玩意我真是吃了大亏,刚开始没看文档, 直接在执行package.json的script命令的时候通过cross-env NODE_ENV=xxx 来修改环境变量,导致无法使用vue-cli工具为我们提供的代码优化功能

注意: process.env_NODE_ENV的值只能为 developmentproduction,不要修改为其他值,不然可能会出现其他问题,如果你真的想在不同的环境使用不同的接口地址或者、其他的内容我们可以用vue-cli为我们提供的 --mode达到目的,比如

"scripts": {"serve": "vue-cli-service serve","build": "vue-cli-service build","build:sanbox": "vue-cli-service build --mode sanbox","build:feature": "vue-cli-service build --mode feature","lint": "vue-cli-service lint","precommit": "yarn lint"
}

这里vue-cli会读取项目根目录下的.env/.env. /.env..local相关文件 ***指的是--mode的值,--mode会修改process.env.NODE_ENV的值,我们需要再对应的env里面把NODE_ENV改写回来,比如一个 .env.sanbox环境代码如下

NODE_ENV=production
VUE_APP_ENV=sanbox

当我们执行 yarn build:sanbox的时候就会加载这个文件,我们可以通过 process.env.VUE_APP_ENV来访问对应设置的值,注意只有 VIE_APP_前缀的环境变量才会被 webpack.DefinePlugin静态嵌入到客户端侧的包中。

比如我们需要不同的构建命令构建不同的的publicPath的时候我们可以这样做

const map = {
    production: '/',
    feature: '/feature',
    sanbox: '/sanbox',
    development: '/development',
}
const env = process.env.VUE_APP_ENV
const publicPath =  map[env]
const PATH = require('path')
const WebpackBundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
    publicPath,
    configureWebpack: {
        externals: {
            "lodash": '_',"qiniu": 'qiniu',"vue": 'Vue',"echarts": 'echarts',"element-ui": 'ELEMENT',
        },
        resolve: {
            extensions: ['.js', '.vue', '.json', '.ts', '.tsx'],
            alias: {'$root': PATH.resolve(__dirname)
            }
        },
        plugins: [new WebpackBundleAnalyzerPlugin()]
    }
}

到这里就所有功能大功告成了,可以美滋滋的去喝一杯咖啡了。


如何从 0 到 1 构建个性化推荐? - DataFunTalk - 博客园

$
0
0

文章作者:曾钦榜 58同城 高级技术经理

编辑整理:周晓侠

内容来源:58技术沙龙

出品社区:DataFun

注:欢迎转载,转载请在留言区内留言。

导读:随着科学技术的飞速发展,互联网被广泛应用于各个领域,而以互联网为基础的招聘模式也越来越受到企业的青睐。互联网招聘具有不受地域限制、覆盖面广、招聘成本低、针对性强、方便快捷、时效性强等优点,现已得到广泛应用,其中,58招聘是互联网招聘行业中规模最大的平台。今天主要跟大家分享下58招聘如何通过个性化推荐技术服务大规模求职者和招聘企业。分享题目是从零到一构建58招聘个性化推荐,主要通过以下三方面进行介绍:

招聘业务介绍

个性化推荐实践

心得分享与规划

——招聘业务介绍——

  1. 58招聘业务简介

2018年我国全国总人口13.9亿多,其中就业人口7.7亿,招聘基数庞大。三大产业就业人口占比分别26.11%,27.57%,46.32%,其中第三大产业占比最大,部分发达国家第三大产业占比已达到70%~80%,随着经济的发展,我国未来就业市场和就业分布将发生大的变化。2019年8月城镇调查显示我国失业率为5.2%,其中25~59岁失业率4.5%,同时每年有800多万的应届生加入就职市场。58招聘作为我国互联网招聘行业之首,每天服务于千万级求职者和大中小企业,平台每天生成千万级连接,促成大量求职者求职成功。

58招聘平台主要服务于求职者和招聘方,接下来主要通过求职者的角度介绍用户在整个平台流转的大致流程,具体如下:

基于求职偏好搜索职位并点击查看详情。

投递有意向职位,或通过平台在线微聊工具、电话与招聘方做进一步沟通。

双方达成共识后,进行面试与入职。

相比传统推荐系统,58招聘的业务漏斗更长更深,并且有一部分转化平台无法完全捕捉,形成了58招聘个性化推荐开展的难点及挑战。

  1. 58招聘推荐场景类型

58招聘推荐场景主要面向 C 端求职者和 B 端企业,推荐内容主要包括:职位推荐、标签推荐、企业推荐、简历推荐。

C 端求职者的典型推荐场景包括:

App 首页招聘大类页:主要包括职位专区聚合、职位 Feed 流。

类目推荐: 用户点击某个类目后,进行相关职位推荐。

相似推荐: 用户点击某个具体职位后,在下方展现相似职位。

  1. 58招聘推荐主要问题

58招聘推荐相对其它行业主要存在以下典型问题:

海量数据计算:大多数公司都存在,此处不做详细说明。

冷启动问题: 58同城服务于多业务,包括招聘、房产、黄页、二手等,求职者进入招聘板块使用招聘功能,由于当前不强制用户填写简历,导致无简历用户冷启动问题。

稀疏性&实时性:58招聘的一部分群体为蓝领用户,他们在平台产生的行为是短时间的、连续的以及稀疏的,可能活跃两天找到工作后就不再活跃。其次,有些用户回到平台,求职意愿可能会发生变化,一部分可能想找别的工作(如之前是服务员,现在想找快递),另一部分可能是因为传统职业存在职位进阶的过程,这些都需要系统思考。

资源分配问题:第一,如何有效识别(企业,求职者)的真实意图,进而合理分配资源产生有效连接,针对不良意图进行差异化对待。第二,招聘对于 C 端和 B 端都是有限的资源,招聘方招聘职位有限,求职者与招聘方交互有限,很大程度上不同于淘宝推荐,因为后者的商品是无限供应的。

——招聘个性化推荐实现——

  1. 58招聘个性化推荐实现

58招聘个性化推荐的实现过程与大多数公司推荐模块基本相似,包括用户意图理解、内容召回、内容排序、内容展示四个核心模块。下面将结合业务特性,介绍每个模块实现的关键点。

  1. 如何理解用户?

58招聘用户理解主要通过“言”和“行”识别用户真实意图,重点关注的属性主要包括招聘领域求职意向、用户个人属性以及外在形象(如上图左边)。围绕求职者与招聘方在平台产生的内容及行为,我们构建了相应的知识图谱和用户画像。

2.1 无诚意用户识别

在理解用户之前,我们首先需要识别出无真实招聘/求职意图的用户,并进行差异对待。如频繁发布包含联系方式的导流信息、发布高薪诱惑等恶意虚假信息等,将用户引导至平台外进行转化。针对以上业务我们总结了一些特点,主要表现为:

暴露联系方式

内容不成句

高薪诱惑

在平台很“活跃”

针对以上业务特点,我们主要的识别方法包括:

传统的关键词+正则识别方法,如针对"微信"、"QQ"这类联系方式的相关关键词等。

针对变形联系方式,基于拼音+滑动窗口进行识别。

采用命名实体 NER 识别进行挖掘,如 BiLSTM+CRF。

采用相关分类算法进行识别,如 fastText,CNN。

在无诚意用户识别过程中,我们总结了以下心得:

举一反三:问题用户识别是典型的对抗场景,策略构建时需要更多思考对抗能力的刻画,将一些强对抗能力的特征加入到模型中(如文字变形、文字转拼音)。

刚柔并济:差异化惩处不同问题类型的用户。对平台其他用户伤害巨大的群体,结合法律手段严厉惩处;处于问题边界的,则主要通过较柔和的方式处理(如内容展示降权),减少剧烈对抗现象的产生。

2.2 知识图谱构建

知识图谱是一个非常复杂的系统,包括多元异构数据搜集->知识获取->知识融合表示->知识推理->知识管理多个部分,主题及时间因素,我们重点介绍下在 NER 方面的探索。招聘业务场景含有大量的文本内容,通过 NER 技术能够有效提取文本中的关键信息,进一步提高系统的结构化理解能力。

NER 开展经历了两个阶段:

第一阶段:基于平台已有的部分结构化实体词,以及不少半结构化组织的职位描述基础,我们采用 bootstrap 方法,快速迭代进行挖掘,并结合半人工标注,为深度学习构建更完整的样本数据集。

第二阶段:将第一阶段的内容作为 input,核心采用 BiLSTM+CRF 构建实体识别深度网络,有两个优化点取得了较好效果。第一个是输入层基于字到词的优化,构建招聘领域的专有词库。第二个是采用训练样本增强技术,将相近实体词和同类实体词进行替换扩大样本集,并将模型识别的结果有选择的放回训练集重新进行迭代训练,减弱对标注数据集的依赖。目前命名实体识别仍在不断优化,识别准确率平均达到0.75+,部分准确率可达到0.9+。

2.3 构建用户画像

用户画像是个性化推荐系统的基础模块,决定了对用户意图理解的准确与否。基于标签传递思想,我们通过统计规则、传统分类模型和深度模型多种算法结合捕获用户行为的兴趣表达,构建长短期用户画像。

基于统计规则:通过窗口形式,近实时对用户画像进行计算更新,计算时加入时间衰减因子、行为权重因子及标签置信度权重。深刻理解业务场景,进行合理数学设计是关键。如信息列表页的点击数据,在使用时要差异化处理列表页直接展示的显性标签及隐藏在详情页的兴趣标签,避免人为引入噪音。

基于传统分类预测:采用分类算法,应用到用户属性填充、异常用户/行为识别及用户分类多个方面。并非所有的求职用户都会留下较详尽的简历,我们借助历史的招聘简历与用户行为组织样本,可有效预测性别、年龄段、期望工作岗位等用户信息,优化简历缺失或不完善的冷启动问题。同时,针对用户行为的聚焦情况,通过模型能够有效识别出一些异常数据、识别求职目的明确型及发散型两类求职用户,进而剔除掉部分噪音数据提高样本精度,对不同用户定制差异化策略,提升推荐整体刻画能力。

基于行为序列预测:借助统计规则及传统分类,基本建设出一个可用画像,但对用户多个行为之间的信息捕获有限。我们将用户搜索浏览、简历投递、在线沟通等行为组织成行为事件序列,采用 LSTM、GRU、Attention 等训练模型预测用户兴趣,当前还在探索评估阶段。

  1. 召回模块

58招聘推荐围绕个体、群体、全局三个召回不断细化演进,不同召回满足不同需要,三者结合服务于各类场景。从2016年到现在,我们先后主要经历了基于上下文内容、协同过滤、精细画像、深度召回几个阶段,演变成当前以上下文与用户画像结合的精准召回、协同过滤召回及深度向量化召回为核心策略的召回模块。

3.1 基于上下文+用户画像的精准召回

该策略是业内十分常用的召回方法之一,核心在于结合用户画像对请求进行丰富改写。绝大部分场景,用户主动搜索或点选的条件有限,借助用户画像中的历史兴趣及知识图谱组织的实体关系,我们对岗位、工作地、薪资、行业等多个维度进行条件扩充或必要改写,多路召回匹配用户的职位内容。

该策略的主要优点:可解释性好、实现时间成本低,缺点和难点是过度依赖标签挖掘的准确性。

3.2 基于业务特殊性的协同过滤算法改进

协同过滤是推荐系统经典的召回方法,通过用户与物品的行为挖掘用户与用户、物品与物品之间的关联关系。招聘业务的求职者数量巨大,且是短时间的稀疏行为场景,我们采用基于物品的协同过滤,同时希望能近实时的将实时行为信息组织进服务。

在技术实现过程中,我们参考了腾讯2015年发表的Paper《TencentRec: Real-time Stream Recommendation in Practice》,赋予职位点击、投递、在线沟通等不同的行为权重进行多行为融合,基于用户行为序列的长度以及用户质量设计用户惩罚因子,同时通过时间衰减因子增强近期行为的表达,这三个因子的设计与 Paper 基本一致。另外针对业务特殊性,我们改进了职位相似度的计算,加入了职位相似度控制,避免求职目标发散的用户影响职位关系的组织。算法上线后,在点击率、投递率方面都取得了正向收益,其中详情页的相关职位推荐提升超过25%。

3.3 Embedding 深度召回探索

协同过滤虽然取得了不错的业务收益,但其依赖于用户与物品的行为矩阵,对于行为稀疏的场景天然表达有限。而恰好,58招聘业务的流量构成中,有一部分是三四五线城市,城市越下沉数据稀疏的情况也越凸显。针对这类问题,我们希望进一步挖掘行为数据的信息,很自然的想到基于深度学习的向量化 Embedding 召回。我们核心参考了 Youtube 的 DNN 召回思想,基于业务现状做了调整优化。

职位向量化:我们将求职者对职位的行为序列看作一系列上下文,利用 word2vector 思想进行向量化表达。Input 部分,包括职位特征、职位所属的企业特征和求职者反馈特征。Output 构建,业务漏斗越深的行为选择的窗口越大,并基于用户平均的行为长度作为窗口设定的参考值。针对无历史用户行为的新职位,使用职位的文本结构化信息,通过历史训练所得的标签向量表达经过 average-pooling 作为初始向量,解决冷启动。

用户向量化:构建一个多分类 NN 网络,Embedding 层将用户发生行为的职位向量化直接迁移过来使用,输入用户的简历及画像信息进行向量训练。最上层理想情况是一个极限分类,以用户真实发生行为的数据作为正样本,未发生行为的数据作为负样本,构建损失函数进行最优化训练。58招聘场景有千万级别的职位,极限分类需要巨大的计算消耗,当前资源无法满足。因此在负样本选择上,我们使用降采样机制,随机从求职者关注的城市及岗位下未发生行为的职位中按一定比例抽取负样本。线上会实时的采集用户行为,以窗口形式对用户向量进行更新。

线上服务:借鉴 Facebook 的 FAISS 实现,线上用户发起请求时,通过求职者的向量表达,去获取与其最相似的 TopN 职位,返回给推荐系统。

Embedding 向量化召回,还处于初期探索,仍需要在样本、输入特征及网络参数调优开展大量工作,期待有更显著的业务收益进一步与大家分享。

  1. 排序迭代历史

相比其他推荐场景,58招聘的漏斗更深,并且是典型的双边业务。系统不断优化提升求职者点击、投递职位的同时,还需要关注职位背后的招聘方是否反馈形成了有效双边连接,进而达到更接近求职链条的预测目的。结合不同时期的业务目标,我们先后经历了几个主要阶段。

第一阶段:以提升点击规模为主要目标,从零到一构建点击率预估模型,开发模型建设的基本框架,包括特征工程、AB 实验框架及线上 CTR 服务。该阶段在较少人员的情况下,建立了排序模型及服务的大体框架,在点击层面支撑业务增长。

第二阶段:业务目标深入,从点击过度到单边连接直至双边连接,在 CTR 预估模型的基础上,增加了 CVR 预估及 ROR 双边连接预估。同时在工具上开展了针对性建设,包括特征生产 Pipeline、AB 实验框架升级为可配置化中心及特征模型的可视化分析监控等,解耦算法和工程依赖,支持更多算法和工程人员的并行高效迭代。

第三阶段:围绕深度学习的算法探索,wide&deep、DeepFM、多任务学习、强化学习等,不断提升算法对高维特征的表达能力,提高预估模型的刻画能力。预计在2020年全面落地业务,达到更为理想的迭代状态。

4.1 连接转化预估模型

58招聘转化预估模型是多目标学习,设计实现如上图,底层共建样本及特征,使用不同算法对 CTR 点击率预估、CVR 单边连接预估、ROR 双边连接预估进行建模,最后对多个模型进行融合支撑线上排序。

整体排序实现是业务常见的方法,总结开展过程中比较关键的点:

样本处理:围绕减少样本噪音,我们开展了多个优化。去除异常用户及异常数据,包括非招聘意图的用户数据、误点击数据;增加真实曝光及停留时长埋点,去除用户下拉信息流过程中非真正看见的数据,将停留时长作为样本置信权重加入到模型训练中;基于求职者维度进行采样,去除对同一职位多次正负样本的矛盾可能。

特征工程:关注及监测特征显隐性的变化,尤其是信息列表展示样式的产品调整,需及时进行特征调整及模型迭代。58招聘业务的特殊性,实时类特征很重要,需要关注特征一致性方面的保障机制,避免发生特征穿越现象或线上线下特征不一致问题。

模型:重视模型认知,并不是简单的关注离线 AUC 或者线上转化率 AB 对比,在特征表达上多些分析,迭代过程中重视前后模型的特征比较,能够有效提高模型实验迭代的有效性。

4.2 特征生产实现

特征 Pipeline 的构建,减少了大量特征工程重复工作,显著提高模型迭代效率。其核心功能是实现配置化的方式,集成了样本采样、特征变换、特征组合、特征离散化,整合后得到训练样本,一方面输送给模型进行训练评估,另一方面也输出到分析平台支持可视化分析。

4.3 模型 serving 实现

线上模型服务有定期更新及大量 AB 实验的要求,随着服务演进构建了当前的模型 Serving 框架,实现了对模型的定期自动更新以及模型的自动加卸载功能,同时也具备了更强的扩展性,可接入不同算法的模型实现。离线部分,样本经过特征 Pipeline 构建增量训练数据,模型训练模块会获取 Base 模型文件初始化并进行增量模型训练,模型评估无异常,系统会将模型存储至模型仓库及 HDFS 文件。线上部分,模型仓库增删改模型后,会发起模型热加载或卸载指令更新至线上服务内存;对于线上的排序请求,实时修改相应使用模型的存储生命周期,对于长期无用的模型,模型仓库将自动删除。

模型 Serving 能够自动化管理线上模型,但我们也不能完全托管系统,依然需要关注模型变化。一方面在离线部分的模型评估环节,除了对 AUC 等评估指标的自动监测,也将模型内存大小、模型特征表达作为监测的一部分;另一方面线上监测业务转化指标的变化,当指标发生较大波动时发出警报,人工进行模型检查。

4.4 重排序机制

由于业务的特殊性,以 CTR 预估、CVR 单边连接预估、ROR 双边连接预估支撑排序仍然存在刻画能力不足的问题,体现在以下几个方面:

招聘关系到个人生计及国家民生,是件极为严肃的事情,内容质量是基础保障。但连接预估模型无法有效刻画质量问题,存在一些职位连接效率很不错但属于问题职位,因此推荐系统需要增加质量相关的因子。

转化率高不等同于双边匹配。线上招聘,无法很好追踪到面试及入职环节,求职者与招聘方形成的双边连接,可能是出于其他原因(如对自己或对方的错误判断)。因此,系统需要考虑匹配方面的控制。

资源浪费问题,对于绝大部分用户,求职及招聘都是周期性行为,一个已经招满人的职位可能依然在线上展示。系统还需要增加职位活跃度或周期方面的刻画,减少相应的资源浪费。

针对这些需要,系统增加了重排序机制,通过分段处理手段,在粗排阶段打压甚至过滤掉低质量内容,在重排序对不活跃/不匹配内容进行降权,达到保障平台质量生态、提高有效连接规模的目的。

  1. 列表展示内容控制

内容展示方面,我们也结合算法做了一些工作,来提高内容的可解释性、提供更多有价值的信息来辅助用户决策。结合个性化模型挖掘亮点标签,将更深预估模型的核心特征包装成标签形式展示在列表页,如距离多远、职位的福利标签、职位的热门情况等;使用 NLG 文本生成技术,自动生成简短描述进行展示,弥补标题及其简单职位的文本信息不足。

  1. AB 实验配置中心

推荐系统包括召回、过滤、排序、展示几个核心模块,且每个模块都有长期进行实验迭代的诉求。我们搭建了 AB 实验配置中心,实现可视化配置,与线上服务及数据分析平台联动,更灵活高效地开展实验迭代工作。

  1. 整体技术框架

58招聘个性化推荐经过不断演进,最终形成了如上图的技术框架。离线部分包含数据仓库层,知识图谱、用户画像、预测模型的挖掘层,知识数据存储层;线上部分包含数据服务及推荐引擎。线上产生的行为数据,实时流转至离线的计算挖掘模块,反馈到线上达到个性化体验效果。

——心得分享及规划——

58招聘推荐系统最近四年优化收益整理如上图,贡献大小依次是召回、特征、数据、算法、样式、工程。深入理解业务及算法、注重细节积累是做好算法工作的保证;前期在样本及特征上多下功夫,不仅能获得不错的业务增长,也是之后算法深入的基础;工具性建设尽可能先行,能够提高整体迭代效率。

未来的核心工作:

多任务学习、强化学习等的全面探索落地。

集公司内外资源,丰富招聘数据源,提高用户画像的覆盖率,更好的支持千人千面。

分享嘉宾

曾钦榜

58同城 | 高级技术经理
——END——
欢迎关注DataFunTalk同名公众号,收看第一原创技术文章。

微服务SpringCloud之GateWay熔断、限流、重试 - 社会主义接班人 - 博客园

$
0
0

纯洁的微笑的Spring Cloud系列博客终于学完了,也对Spring Cloud有了初步的了解。

修改请求路径的过滤器

StripPrefix Filter 是一个请求路径截取的功能,我们可以利用这个功能来做特殊业务的转发。

- id: StripPrefix
         uri: http://www.cnblogs.com
         predicates:
           - Path=/name/**
         filters:
           - StripPrefix=2

StripPrefix是当请求路径匹配到/name/**会将包含name和后边的字符串接去掉转发, StripPrefix=2就代表截取路径的个数,当访问 http://localhost:8081/name/aa/5ishare时会跳转到 https://www.cnblogs.com/5ishare页面。

 PrefixPath Filter 的作用和 StripPrefix 正相反,是在 URL 路径前面添加一部分的前缀。

- id: prefixpath_route
         uri: http://www.cnblogs.com
         predicates:
            - Method=GET
         filters:
            - PrefixPath=/5ishare

在浏览器输入 http://localhost:8081/p/11831586.html 时页面会跳转到  https://www.cnblogs.com/5ishare/p/11831586.html

 限速路由器

限速在高并发场景中比较常用的手段之一,可以有效的保障服务的整体稳定性,Spring Cloud Gateway 提供了基于 Redis 的限流方案。所以我们首先需要添加对应的依赖包spring-boot-starter-data-redis-reactive。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId><version>2.0.4.RELEASE</version></dependency>

配置文件中需要添加 Redis 地址和限流的相关配置

server:
  port: 8081
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8088/eureka/
logging:
  level:
    org.springframework.cloud.gateway: debug
spring:
  application:
    name: SpringCloudGatewayDemo
  redis:
    host: localhost
    password:
    port: 6379
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
       - id: requestratelimiter_route
         uri: http://example.org
         filters:
          - name: RequestRateLimiter
            args:
             redis-rate-limiter.replenishRate: 10
             redis-rate-limiter.burstCapacity: 20
              key-resolver:"#{@userKeyResolver}"
         predicates:
           - Method=GET
View Code

filter 名称必须是 RequestRateLimiter
redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
key-resolver:使用 SpEL 按名称引用 bean

项目中设置限流的策略,创建 Config 类。根据请求参数中的 user 字段来限流,也可以设置根据请求 IP 地址来限流,设置如下:

packagecom.example.demo;importorg.springframework.cloud.gateway.filter.ratelimit.KeyResolver;importorg.springframework.context.annotation.Bean;importreactor.core.publisher.Mono;publicclassConfig {
    @BeanpublicKeyResolver ipKeyResolver() {returnexchange ->Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }


    @Bean
    KeyResolver userKeyResolver() {returnexchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }
}
View Code

熔断路由器

Spring Cloud Gateway 也可以利用 Hystrix 的熔断特性,在流量过大时进行服务降级,同样我们还是首先给项目添加上依赖。

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId><version>2.1.3.RELEASE</version></dependency>
- id: hystrix_route
         uri: lb://spring-cloud-producer
         predicates:
           - Path=/consumingserviceendpoint
         filters:
           - name: Hystrix
             args:
              name: fallbackcmd
              fallbackUri: forward:/incaseoffailureusethis

fallbackUri: forward:/incaseoffailureusethis配置了 fallback 时要会调的路径,当调用 Hystrix 的 fallback 被调用时,请求将转发到/incaseoffailureuset这个 URI。

重试路由器

RetryGatewayFilter 是 Spring Cloud Gateway 对请求重试提供的一个 GatewayFilter Factory

- id: retry_test
         uri: lb://spring-cloud-producer
         predicates:
           - Path=/retry
         filters:
           - name: Retry
             args:
              retries: 3
              statuses: BAD_GATEWAY

retries:重试次数,默认值是 3 次
statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus
methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod
series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5 个值。

 

Redis cluster集群模式的原理 - __Meng - 博客园

$
0
0

 

redis cluster

  redis cluster是Redis的分布式解决方案,在3.0版本推出后有效地解决了redis分布式方面的需求

  自动将数据进行分片,每个master上放一部分数据

  提供内置的高可用支持,部分master不可用时,还是可以继续工作的

 

  支撑N个redis master node,每个master node都可以挂载多个slave node

  高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master

 

redis cluster vs. replication + sentinal

  如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了

  replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了

  redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster

 

数据分布算法

hash算法

  比如你有 N 个 redis实例,那么如何将一个key映射到redis上呢,你很可能会采用类似下面的通用方法计算 key的 hash 值,然后均匀的映射到到 N 个 redis上:

  hash(key)%N

  如果增加一个redis,映射公式变成了 hash(key)%(N+1)

  如果一个redis宕机了,映射公式变成了 hash(key)%(N-1)

  在这两种情况下,几乎所有的缓存都失效了。会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

 

一致性hash算法

  一个master宕机不会导致大部分缓存失效,可能存在缓存热点问题

 

用虚拟节点改进

 

 

 

redis cluster的hash slot算法

  redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot

  redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot

  hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去

  移动hash slot的成本是非常低的

  客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现

 

  127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000  可以将槽0-5000指派给节点7000负责。

  每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。

  客户端向节点发送键命令,节点要计算这个键属于哪个槽。

  如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。

 

 

redis cluster  多master的写入

  在redis cluster写入数据的时候,其实是你可以将请求发送到任意一个master上去执行

  但是,每个master都会计算这个key对应的CRC16值,然后对16384个hashslot取模,找到key对应的hashslot,找到hashslot对应的master

  如果对应的master就在自己本地的话,set mykey1 v1,mykey1这个key对应的hashslot就在自己本地,那么自己就处理掉了

  但是如果计算出来的hashslot在其他master上,那么就会给客户端返回一个moved error,告诉你,你得到哪个master上去执行这条写入的命令

 

  什么叫做多master的写入,就是每条数据只能存在于一个master上,不同的master负责存储不同的数据,分布式的数据存储

  100w条数据,5个master,每个master就负责存储20w条数据,分布式数据存储

 

  默认情况下,redis cluster的核心的理念,主要是用slave做高可用的,每个master挂一两个slave,主要是做数据的热备,还有master故障时的主备切换,实现高可用的

  redis cluster默认是不支持slave节点读或者写的,跟我们手动基于replication搭建的主从架构不一样的

 

  jedis客户端,对redis cluster的读写分离支持不太好的

  默认的话就是读和写都到master上去执行的

  如果你要让最流行的jedis做redis cluster的读写分离的访问,那可能还得自己修改一点jedis的源码,成本比较高

 

  读写分离,是为了什么,主要是因为要建立一主多从的架构,才能横向任意扩展slave node去支撑更大的读吞吐量

  redis cluster的架构下,实际上本身master就是可以任意扩展的,你如果要支撑更大的读吞吐量,或者写吞吐量,或者数据量,都可以直接对master进行横向扩展就可以了

 

 

 

 

 


节点间的内部通信机制

1、基础通信原理

(1)redis cluster节点间采取gossip协议进行通信

  跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的

  集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力

  gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后

 

(2)10000端口

  每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口

  每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong

 

(3)交换的信息

  故障信息,节点的增加和移除,hash slot信息,等等

 

2、gossip协议

  gossip协议包含多种消息,包括ping,pong,meet,fail,等等

  meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信

  redis-trib.rb add-node

  其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群

  ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据

  每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新

  pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新

  fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了

 

3、ping消息深入

  ping很频繁,而且要携带一些元数据,所以可能会加重网络负担

  每个节点每秒会执行10次ping,每次会选择5个最久没有通信的其他节点

  当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了

  比如说,两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题

  所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率

  每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换

  至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息

 

 

基于重定向的客户端

(1)请求重定向

  客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot

  如果在本地就在本地处理,否则返回moved给客户端,让客户端进行重定向

  cluster keyslot mykey,可以查看一个key对应的hash slot是什么

  用redis-cli的时候,可以加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令

 

(2)计算hash slot

  计算hash slot的算法,就是根据key计算CRC16值,然后对16384取模,拿到对应的hash slot

  用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和set mykey2:{100}

 

(3)hash slot查找

  节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上

 

smart jedis

(1)什么是smart jedis

  基于重定向的客户端,很消耗网络IO,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点

  所以大部分的客户端,比如java redis客户端,就是jedis,都是smart的

  本地维护一份hashslot -> node的映射表,缓存,大部分情况下,直接走本地缓存就可以找到hashslot -> node,不需要通过节点进行moved重定向

 

(2)JedisCluster的工作原理

  在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot -> node映射表,同时为每个节点创建一个JedisPool连接池

  每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后在本地映射表找到对应的节点

  如果那个node正好还是持有那个hashslot,那么就ok; 如果说进行了reshard这样的操作,可能hashslot已经不在那个node上了,就会返回moved

  如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新本地的hashslot -> node映射表缓存

  重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException

  jedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新hash slot,频繁ping节点检查活跃,导致大量网络IO开销

  jedis最新版本,对于这些过度的hash slot更新和ping,都进行了优化,避免了类似问题

 

(3)hashslot迁移和ask重定向

  如果hash slot正在迁移,那么会返回ask重定向给jedis

  jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存

  已经可以确定说,hashslot已经迁移完了,moved是会更新本地hashslot->node映射表缓存的

 

高可用性与主备切换原理

redis cluster的高可用的原理,几乎跟哨兵是类似的

1、判断节点宕机

  如果一个节点认为另外一个节点宕机,那么就是pfail,主观宕机

  如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown

  在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail

  如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail

 

2、从节点过滤

  对宕机的master node,从其所有的slave node中,选择一个切换成master node

  检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master

  这个也是跟哨兵是一样的,从节点超时过滤的步骤

 

3、从节点选举

  哨兵:对所有从节点进行排序,slave priority,offset,run id

  每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举

  所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master

  从节点执行主备切换,从节点切换为主节点

 

4、与哨兵比较

  整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能

 


到底是否应该使用“微服务架构”? - LinkinStar - 博客园

$
0
0

前言

经过当前服务端的洗礼之后,市场出现了一波微服务的热潮。然后就出现了很大的一个问题,无论什么项目,很多人想都不想,都直接开始说我们使用微服务架构来完成吧,用这个、这个组件很简单就能实现。。。而且,现在市场上很多学习教程都直接教授微服务的架构使用。很多学习的人看到这样的趋势就会随大流,就导致了当前的问题,炒作这样概念的人很多,很少人知其所以然。

经过一段时间的整理,梳理出了下面几个点,可供参考。

希望经过这些简短的参考能帮助你认识,技术的所以然。

 

什么是“微服务架构”

官方:一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制。这些服务围绕业务能力构建并且可以独立部署。依赖一个最小型的集中式管理。

总结为几点:

1、独立运行

2、独立部署

3、独立开发

4、轻量级通信

5、集中管理

这样说还是有点抽象,举个实际的例子来说,比如购物:我们可以拆分为,用户微服务,订单微服务,商品微服务…..

每个服务都有以上特点,之间独立,又可以通信,依赖一些管理的东西去管理他们。

中心思想:把大系统拆分成小型系统,把大事化小,以降低系统的复杂性

 

“微服务架构”的优点和缺点

如果只是说一个东西的优点是没用的,只有对比来看,所以我们对比单体应用来说明其优点。

1、部署:

单体应用部署肯定简单,一个包扔进去,容器启动就可以了。

微服务应用部署会负责,服务越多部署越麻烦,而且有些依赖与一些中间件,所以运维和部署的压力变大是肯定的。(这里并不是说一定的,已经有一些运维部署的软件方便了微服务的部署)

2、开发和维护:

单体应用如果要进行开发,代码即使分离的再好,那么还是在一起,所以会显得臃肿,维护起来不方便,如果需要改动一个点,整个服务必须全部重新启动。

微服务开发因为本身分离,所以显得清晰,维护起来方便,一个地方的服务出现问题,只需要改动对应服务并重启对应服务即可。

3、扩展:

单体应用扩展可想而知,受限并且压力很大,到最后很多人会发现,加入或者扩展功能时宁可新开发一个也不愿意去依赖原来的代码就怕改了原来的代码之前的代码出现问题。

微服务扩展能力较好,新加入一个功能不会对原来的系统造成影响。而且如果一个大的功能被禁用,直接停止对应服务即可。

4、通信:

对于单体应用来说,自己本身都是内部服务调用不存在通信问题,对于外部库来说,通信方式取决于外部库的依赖。

微服务之间的通信就需要依赖比较靠谱的通信系统了,因为难免服务与服务之间会有依赖,那么通信方式的选择就尤为重要了。

 

到底是否应该使用“微服务架构”?

最后我们再来看看我们一开始的问题,是不是就能总结出以下几个点了。然后我结合一些书本和经验做下面一个总结,希望对你有帮助。

1、系统大小

这是我们首要的考虑目标,如果一个系统很小,比如一个官网,那你说做微服务就是扯淡了。那么如何确定一个系统的大小呢?可以参考一下下面这个标准。

如果你的项目能分成三个或三个以上的耦合度很低的项目,那么就算大。

如果你的项目数据库表超过30张,且单表数据轻松百万,那么就算大。

如果你的项目之后会进行扩展,并且扩展之后会达到上面的如果,那么也算大。

虽然只是经验上的估计参考,但是也从侧面体现出,如果项目不大,那么真的就没必要。

 

2、技术能力

微服务依赖的能力有以下几点

拆分服务的能力

处理分布式问题(网络请求,分布式事务等)的能力

强大的运维能力

如果一个系统决定使用微服务架构,那么前期的拆分就显得非常重要,有经验的拆分可以让服务之间的耦合对降到最低,并且相应的业务没有问题。相应的,如果没有处理分布式问题的能力也是不行的,最后才是项目部署运维的能力。

 

3、团队规模和时间

如果你的团队规模不超过10人,那么除非你们能力都非常牛,而且都能独当一面,那么当我没说,理论上不建议。

在开发周期时间不允许的情况下不要执意去切换,从单体切换到微服务,因为两者区别不仅仅是在服务上,包括通信等等方面耗时都不短,测试上面就需要更加多的时间去测试。而且微服务的开发效率上面是一开始慢,到项目大了之后开发效率才慢慢的体现出来。

微服务毕竟存在通信,而且服务器想对多,项目稳定性上肯定要打折扣。你的团队需要提前了解到这样的问题,并做好遇到问题的准备和处理,这也是需要时间的。

团队之间的沟通,有通信必然有交流,不然别人怎么知道你的服务是怎么样的。那么接口文档编写的时间和对接接口的时间,调试的时间,剩下我就不多说了,你应该懂了。

 

总结

一个技术或者一个架构不是万能的,每个技术都有适用的场景,我们所要做的不是一味的追求最新,而是明白它的使用场景或者优点缺点,从而来考虑是否使用。

这里上面也只是抛砖引玉,所有的细节肯定不是一篇文章或者一本书能说完的,只要你去考虑了,借鉴一些别人的经验去发现可能存在的问题,那么即使最后出现问题也可以被解决。

 

参考文档:

《SpringCloud与Docker微服务架构实战》

http://www.infoq.com/cn/minibooks/microservice--from-zero

 

Kafka幂等性原理及实现剖析 - 哥不是小萝莉 - 博客园

$
0
0

1.概述

最近和一些同学交流的时候反馈说,在面试Kafka时,被问到Kafka组件组成部分、API使用、Consumer和Producer原理及作用等问题都能详细作答。但是,问到一个平时不注意的问题,就是Kafka的幂等性,被卡主了。那么,今天笔者就为大家来剖析一下Kafka的幂等性原理及实现。

2.内容

2.1 Kafka为啥需要幂等性?

Producer在生产发送消息时,难免会重复发送消息。Producer进行retry时会产生重试机制,发生消息重复发送。而引入幂等性后,重复发送只会生成一条有效的消息。Kafka作为分布式消息系统,它的使用场景常见与分布式系统中,比如消息推送系统、业务平台系统(如物流平台、银行结算平台等)。以银行结算平台来说,业务方作为上游把数据上报到银行结算平台,如果一份数据被计算、处理多次,那么产生的影响会很严重。

2.2 影响Kafka幂等性的因素有哪些?

在使用Kafka时,需要确保Exactly-Once语义。分布式系统中,一些不可控因素有很多,比如网络、OOM、FullGC等。在Kafka Broker确认Ack时,出现网络异常、FullGC、OOM等问题时导致Ack超时,Producer会进行重复发送。可能出现的情况如下:

 

 

2.3 Kafka的幂等性是如何实现的?

Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。那这两个概念的用途是什么呢?

  • ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,这个ProducerID对客户端使用者是不可见的。
  • SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应一个从0开始单调递增的SequenceNumber值。

2.3.1 幂等性引入之前的问题?

Kafka在引入幂等性之前,Producer向Broker发送消息,然后Broker将消息追加到消息流中后给Producer返回Ack信号值。实现流程如下:

 

上图的实现流程是一种理想状态下的消息发送情况,但是实际情况中,会出现各种不确定的因素,比如在Producer在发送给Broker的时候出现网络异常。比如以下这种异常情况的出现:

 

上图这种情况,当Producer第一次发送消息给Broker时,Broker将消息(x2,y2)追加到了消息流中,但是在返回Ack信号给Producer时失败了(比如网络异常) 。此时,Producer端触发重试机制,将消息(x2,y2)重新发送给Broker,Broker接收到消息后,再次将该消息追加到消息流中,然后成功返回Ack信号给Producer。这样下来,消息流中就被重复追加了两条相同的(x2,y2)的消息。

2.3.2 幂等性引入之后解决了什么问题?

面对这样的问题,Kafka引入了幂等性。那么幂等性是如何解决这类重复发送消息的问题的呢?下面我们可以先来看看流程图:

 

 同样,这是一种理想状态下的发送流程。实际情况下,会有很多不确定的因素,比如Broker在发送Ack信号给Producer时出现网络异常,导致发送失败。异常情况如下图所示:

 

 当Producer发送消息(x2,y2)给Broker时,Broker接收到消息并将其追加到消息流中。此时,Broker返回Ack信号给Producer时,发生异常导致Producer接收Ack信号失败。对于Producer来说,会触发重试机制,将消息(x2,y2)再次发送,但是,由于引入了幂等性,在每条消息中附带了PID(ProducerID)和SequenceNumber。相同的PID和SequenceNumber发送给Broker,而之前Broker缓存过之前发送的相同的消息,那么在消息流中的消息就只有一条(x2,y2),不会出现重复发送的情况。

2.3.3 ProducerID是如何生成的?

客户端在生成Producer时,会实例化如下代码:

//实例化一个Producer对象Producer<String, String> producer =newKafkaProducer<>(props);

在org.apache.kafka.clients.producer.internals.Sender类中,在run()中有一个maybeWaitForPid()方法,用来生成一个ProducerID,实现代码如下:

privatevoidmaybeWaitForPid() {if(transactionState ==null)return;while(!transactionState.hasPid()) {try{
                Node node=awaitLeastLoadedNodeReady(requestTimeout);if(node !=null) {
                    ClientResponse response=sendAndAwaitInitPidRequest(node);if(response.hasResponse() && (response.responseBody()instanceofInitPidResponse)) {
                        InitPidResponse initPidResponse=(InitPidResponse) response.responseBody();
                        transactionState.setPidAndEpoch(initPidResponse.producerId(), initPidResponse.epoch());
                    }else{
                        log.error("Received an unexpected response type for an InitPidRequest from {}. " +"We will back off and try again.", node);
                    }
                }else{
                    log.debug("Could not find an available broker to send InitPidRequest to. " +"We will back off and try again.");
                }
            }catch(Exception e) {
                log.warn("Received an exception while trying to get a pid. Will back off and retry.", e);
            }
            log.trace("Retry InitPidRequest in {}ms.", retryBackoffMs);
            time.sleep(retryBackoffMs);
            metadata.requestUpdate();
        }
    }

3.事务

与幂等性有关的另外一个特性就是事务。Kafka中的事务与数据库的事务类似,Kafka中的事务属性是指一系列的Producer生产消息和消费消息提交Offsets的操作在一个事务中,即原子性操作。对应的结果是同时成功或者同时失败。

这里需要与数据库中事务进行区别,操作数据库中的事务指一系列的增删查改,对Kafka来说,操作事务是指一系列的生产和消费等原子性操作。

3.1 Kafka引入事务的用途?

在事务属性引入之前,先引入Producer的幂等性,它的作用为:

  • Producer多次发送消息可以封装成一个原子性操作,即同时成功,或者同时失败;
  • 消费者&生产者模式下,因为Consumer在Commit Offsets出现问题时,导致重复消费消息时,Producer重复生产消息。需要将这个模式下Consumer的Commit Offsets操作和Producer一系列生产消息的操作封装成一个原子性操作。

产生的场景有:

比如,在Consumer中Commit Offsets时,当Consumer在消费完成时Commit的Offsets为100(假设最近一次Commit的Offsets为50),那么执行触发Balance时,其他Consumer就会重复消费消息(消费的Offsets介于50~100之间的消息)。

3.2 事务提供了哪些可使用的API?

Producer提供了五种事务方法,它们分别是:initTransactions()、beginTransaction()、sendOffsetsToTransaction()、commitTransaction()、abortTransaction(),代码定义在org.apache.kafka.clients.producer.Producer<K,V>接口中,具体定义接口如下:

//初始化事务,需要注意确保transation.id属性被分配voidinitTransactions();//开启事务voidbeginTransaction()throwsProducerFencedException;//为Consumer提供的在事务内Commit Offsets的操作voidsendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata>offsets,
                              String consumerGroupId)throwsProducerFencedException;//提交事务voidcommitTransaction()throwsProducerFencedException;//放弃事务,类似于回滚事务的操作voidabortTransaction()throwsProducerFencedException;

3.3 事务的实际应用场景有哪些?

在Kafka事务中,一个原子性操作,根据操作类型可以分为3种情况。情况如下:

  • 只有Producer生产消息,这种场景需要事务的介入;
  • 消费消息和生产消息并存,比如Consumer&Producer模式,这种场景是一般Kafka项目中比较常见的模式,需要事务介入;
  • 只有Consumer消费消息,这种操作在实际项目中意义不大,和手动Commit Offsets的结果一样,而且这种场景不是事务的引入目的。

4.总结

Kafka的幂等性和事务是比较重要的特性,特别是在数据丢失和数据重复的问题上非常重要。Kafka引入幂等性,设计的原理也比较好理解。而事务与数据库的事务特性类似,有数据库使用的经验对理解Kafka的事务也比较容易接受。

5.结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出书了《 Kafka并不难学》和《 Hadoop大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。 

微服务的4个设计原则和19个解决方案 - 晓晨Master - 博客园

$
0
0

本文转自: http://developer.51cto.com/art/201709/552085.htm

微服务架构现在是谈到企业应用架构时必聊的话题,微服务之所以火热也是因为相对之前的应用开发方式有很多优点,如更灵活、更能适应现在需求快速变更的大环境。
本文将介绍微服务架构的演进、优缺点和微服务应用的设计原则,然后着重介绍作为一个“微服务应用平台”需要提供哪些能力、解决哪些问题才能更好的支撑企业应用架构。
微服务平台也是我目前正在参与的,还在研发过程中的平台产品,平台是以SpringCloud为基础,结合了普元多年来对企业应用的理解和产品的设计经验,逐步孵化的一个微服务应用平台。

一、微服务架构演进过程

近年来我们大家都体会到了互联网、移动互联带来的好处,作为IT从业者,在生活中时刻感受互联网好处的同时,在工作中可能感受的却是来自自互联网的一些压力,那就是我们传统企业的IT建设也是迫切需要转型,需要面向外部客户,我们也需要应对外部环境的快速变化、需要快速创新,那么我们的IT架构也需要向互联网企业学习作出相应的改进,来支撑企业的数字化转型。
我们再看一下应用架构的演进过程,回忆一下微服务架构是如何一步一步进化产生的,最早是应用是单块架构,后来为了具备一定的扩展和可靠性,就有了垂直架构,也就是加了个负载均衡,接下来是前几年比较火的SOA,主要讲了应用系统之间如何集成和互通,而到现在的微服务架构则是进一步在探讨一个应用系统该如何设计才能够更好的开发、管理更加灵活高效。
微服务架构的基本思想就是“围绕业务领域组件来创建应用,让应用可以独立的开发、管理和加速”。

二、微服务架构的好处

我们总结了四个方面的优点,分别如下:
是每个微服务组件都是简单灵活的,能够独立部署。不再像以前一样,应用需要一个庞大的应用服务器来支撑。
可以由一个小团队负责更专注专业,相应的也就更高效可靠。
微服务之间是松耦合的,微服务内部是高内聚的,每个微服务很容易按需扩展。
微服务架构与语言工具无关,自由选择合适的语言和工具,高效的完成业务目标即可。
看到这里,大家会觉得微服务架构挺不错,然而还会有一些疑问,什么样的应用算是一个微服务架构的应用?该怎样设计一个微服务架构的应用?那我们来一起看看我们推荐的微服务应用的设计原则。

三、微服务应用4个设计原则

我们总结了四个原则推荐给大家:

  • AKF拆分原则
  • 前后端分离
  • 无状态服务
  • Restful通信风格

1.AKF拆分原则

AKF扩展立方体(参考《The Art of Scalability》),是一个叫AKF的公司的技术专家抽象总结的应用扩展的三个维度。理论上按照这三个扩展模式,可以将一个单体系统,进行无限扩展。
X 轴 :指的是水平复制,很好理解,就是讲单体系统多运行几个实例,做个集群加负载均衡的模式。
Z 轴 :是基于类似的数据分区,比如一个互联网打车应用突然或了,用户量激增,集群模式撑不住了,那就按照用户请求的地区进行数据分区,北京、上海、四川等多建几个集群。
Y 轴 :就是我们所说的微服务的拆分模式,就是基于不同的业务拆分。
场景说明:比如打车应用,一个集群撑不住时,分了多个集群,后来用户激增还是不够用,经过分析发现是乘客和车主访问量很大,就将打车应用拆成了三个乘客服务、车主服务、支付服务。三个服务的业务特点各不相同,独立维护,各自都可以再次按需扩展。

2.前后端分离

前后端分离原则,简单来讲就是前端和后端的代码分离也就是技术上做分离,我们推荐的模式是最好直接采用物理分离的方式部署,进一步促使进行更彻底的分离。不要继续以前的服务端模板技术,比如JSP ,把Java JS HTML CSS 都堆到一个页面里,稍复杂的页面就无法维护。这种分离模式的方式有几个好处:
前后端技术分离,可以由各自的专家来对各自的领域进行优化,这样前端的用户体验优化效果会更好。
分离模式下,前后端交互界面更加清晰,就剩下了接口和模型,后端的接口简洁明了,更容易维护。
前端多渠道集成场景更容易实现,后端服务无需变更,采用统一的数据和模型,可以支撑前端的web UI 移动App等访问。

3.无状态服务

对于无状态服务,首先说一下什么是状态:如果一个数据需要被多个服务共享,才能完成一笔交易,那么这个数据被称为状态。进而依赖这个“状态”数据的服务被称为有状态服务,反之称为无状态服务。
那么这个无状态服务原则并不是说在微服务架构里就不允许存在状态,表达的真实意思是要把有状态的业务服务改变为无状态的计算类服务,那么状态数据也就相应的迁移到对应的“有状态数据服务”中。
场景说明:例如我们以前在本地内存中建立的数据缓存、Session缓存,到现在的微服务架构中就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点。迁移后,就可以做到按需动态伸缩,微服务应用在运行时动态增删节点,就不再需要考虑缓存数据如何同步的问题。

4.Restful通信风格

作为一个原则来讲本来应该是个“无状态通信原则”,在这里我们直接推荐一个实践优选的Restful 通信风格 ,因为他有很多好处:
无状态协议HTTP,具备先天优势,扩展能力很强。例如需要安全加密是,有现成的成熟方案HTTPS可用。
JSON 报文序列化,轻量简单,人与机器均可读,学习成本低,搜索引擎友好。
语言无关,各大热门语言都提供成熟的Restful API框架,相对其他的一些RPC框架生态更完善。
当然在有些特殊业务场景下,也需要采用其他的RPC框架,如thrift、avro-rpc、grpc。但绝大多数情况下Restful就足够用了。

四、微服务架构带来的问题

做到了前面讲的四个原则,那么就可以说是构建了一个微服务应用,感觉上也不复杂。但实际上微服务也不是个万金油,也是有利有弊的,接下来我们来看看引入微服务架构后带来的问题有哪些。

依赖服务变更很难跟踪,其他团队的服务接口文档过期怎么办?依赖的服务没有准备好,如何验证我开发的功能。
部分模块重复构建,跨团队、跨系统、跨语言会有很多的重复建设。
微服务放大了分布式架构的系列问题,如分布式事务怎么处理?依赖服务不稳定怎么办?
运维复杂度陡增,如:部署物数量多、监控进程多导致整体运维复杂度提升。
上面这些问题我们应该都遇到过,并且也会有一些解决方案,比如提供文档管理、服务治理、服务模拟的工具和框架; 实现统一认证、统一配置、统一日志框架、分布式汇总分析; 采用全局事务方案、采用异步模拟同步;搭建持续集成平台、统一监控平台等等。
这些解决方案折腾到最后终于搞明白了,原来我们是需要一个微服务应用平台才能整体性的解决这些问题。

五、微服务平台的19个落地实践

1.企业IT建设的三大基础环境

我们先来宏观的看一下,一个企业的IT建设非常重要的三大基础环境:团队协作环境、个人基础环境、IT基础设施。

团队协作环境:主要是DevOps领域的范畴,负责从需求到计划任务,团队协作,再到质量管理、持续集成和发布。
个人基础环境:就是本文介绍的微服务应用平台,他的目标主要就是要支撑微服务应用的设计开发测试,运行期的业务数据处理和应用的管理监控。
IT基础设施:就是我们通常说的各种运行环境支撑如IaaS (VM虚拟化)和CaaS (容器虚拟化)等实现方式。

2.微服务应用平台总体架构

微服务应用平台的总体架构,主要是从开发集成、微服务运行容器与平台、运行时监控治理和外部渠道接入等维度来划分的。

  • 开发集成:主要是搭建一个微服务平台需要具备的一些工具和仓库
  • 运行时:要有微服务平台来提供一些基础能力和分布式的支撑能力,我们的微服务运行容器则会运行在这个平台之上。
  • 监控治理:则是致力于在运行时能够对受管的微服务进行统一的监控、配置等能力。
  • 服务网关: 则是负责与前端的WEB应用 移动APP 等渠道集成,对前端请求进行认真鉴权,然后路由转发。

3.微服务应用平台的运行视图

参考上图,在运行期,作为一个微服务架构的平台与业务系统,除了业务应用本身外,还需要有接入服务、统一门户、基础服务等平台级服务来保障业务系统的可靠运行。图中的公共服务就是业务处理过程中需要用到的一些可选服务。

4.微服务平台的设计目标

微服务平台的主要目标主要就是要支撑微服务应用的全生命周期管理,从需求到设计开发测试,运行期的业务数据处理和应用的管理监控等,后续将从应用生命周期的几个重要阶段切入,结合前面提到的设计原则和问题,介绍平台提供的能力支撑情况。

5.微服务开发:前端、后端、混合

我们一起看一下我们正在开发中的微服务应用平台EOS8.0的一些开发工具截图,了解一下开发期提供了哪些关键的能力支撑。
前面的设计原则中提到了一个前后端分离的原则,那么我们的开发环境中,目前支持创建前端项目、后端项目和混合项目。其中前端项目、后端项目就对应前后端分离的原则,利用平台中集成的开发工具和框架可以做到前后端开发分离,利用持续集成工具可以方便的将前端、后端项目编译打包成可独立运行的程序。混合项目则是为了兼容传统模式而保留的,为企业应用向微服务架构演进提供过渡方案。

6.服务契约与API管理

对于前面提到的微服务带来的依赖管理问题,我们可以通过平台提供的API管理能力来解决。说到API管理,那首先就用提到服务契约。平台开发工具中提供了方便的服务发布能力,能够快速的将业务功能对外发布,生成服务的规格契约,当然也可以先设计服务契约,在根据契约来生成服务的默认实现代码。
这里强调一下,我们提到的服务契约是一个很重要的东西,他有点类似web service的wsdl描述,主要描述服务接口的输入输出规格标准和其他一些服务调用集成相关的规格内容。

7.服务契约与服务模拟

有了服务契约,我们就可以根据契约自动生成服务的文档和服务模拟测试环境,这样,开发者就可以方便的获取到依赖服务变更的情况,能够及时的根据依赖服务的变化调整自己的程序,并且能够方便的进行模拟测试验证。

8.服务契约与服务编排

有了服务契约,那就有了服务接口的输入输出规格,那么restful的服务编排也就变得可行。在我们设计的契约标准中,还定义了调用集成相关的内容,比如服务支持的事务模式等等。通过这些约定,我们就可以采用简单图形化的方式来对业务服务流程进行编排。编排能够很大程度上简化分布式服务调用的复杂度,如同步、异步、异步模拟同步、超时重试、事务补偿等,均有服务编排引擎完成,不再完全依赖老师傅的编码能力。
服务编排的作用和意义很大,可以快速的将已经提供的微服务能力进行组合发布,非常适合业务的快速创新。
但是大家要注意,逻辑流编排的是业务流程,尽量能够简单明了,一眼看上去就明白业务含义。而业务规则推荐采用服务内部进行编码实现。千万不要将我们的 “逻辑流” 图形化服务编排完全取代程序编码,这样就会可能会走入另外一个极端,比如设计出像蜘蛛网一样的逻辑流图,简直就是灾难。

9.微服务容器

我们再来看一下微服务运行容器的一个逻辑图,大家可以看到,我们要做微服务架构的应用,可靠高效的微服务应用,实际上我们需要做的事情还是非常多的。如果没有一个统一的微服务容器,这些能力在每个微服务组件中都需要建设一遍,而且会五花八门,也很难集成到一起。有了统一的微服务运行容器和一些公共的基础服务,前面所提到的微服务架构下部分组件重复建设的问题也迎刃而解。

10.三方能力集成说明

我们的API管理契约文档API模拟我们是集成了Swagger的工具链。微服务应用平台的基础就是SpringCloud,从容器框架到注册发现再到安全认证这些基础方案均采用了他的能力来支撑。下面简单看下我们集成的一些开源框架和工具。

SpringCloud在微服务平台中的定位是基础框架,本文重点是要介绍一个企业级的微服务平台在落地过程中的一些设计原则和解决方案。具体Spring Cloud相关的技术就不在文中多做介绍了,大家可以在我们的公众号里面查看相关文章。

11.服务注册发现路由

接下来我们聊一下注册发现,以前的单块应用之间互相调用时配置个IP就行了,但在微服务架构下,服务提供者会有很多,手工配置IP地址又变成了一个不可行的事情。那么服务自动注册发现的方案就解决了这个问题。
我们的服务注册发现能力是依赖SpringCloud Eureka组件实现的。服务在启动的时候,会将自己要发布的服务注册到服务注册中心,运行时,如果需要调用其他微服务的接口,那么就要先到注册中心获取服务提供者的地址,拿到地址后,通过微服务容器内部的简单负载均衡期进行路由用。
一般情况,系统内微服务的调用都通过这种客户端负载的模式进行,否则就需要有很多的负载均衡进程。跨业务系统的服务调用,也可以采用这种去中心化的路由方式。当然采用SOA的模式,由中心化的服务网管来管理系统间的调用也是另一种选择,要结合企业的IT现状和需求来决定。

12.统一认证鉴权

安全认证方面,我们基于Spring Security结合Auth2再加上JWT(Json web token)做安全令牌,实现统一的安全认证与鉴权,使得微服务之间能够按需隔离和安全互通。后续在统一认证和权限方面我们产品会陆续推出较完善并且扩展性良好的微服务组件,可以作为微服务平台的公共的认证和鉴权服务。再啰嗦一句,认证鉴权一定是个公共的服务,而不是多个系统各自建设。

13.日志与流水设计

作为一个微服务应用平台除了提供支撑开发和运行的技术组件和框架之外,我们还提供一些运维友好的经验总结,我们一起来看一下我们推荐的日志与流水实现,先来看日志,平台默认回会提供的日志主要有三种,系统日志,引擎日志还有跟踪日志。有了这些日志,在出问题的时候能够帮助我们获取一些关键信息进行问题定位。
要想做到出了问题能够追根溯源,那么右边的这些流水号的设计也是非常重要的,日志与各种流水号配合,能够让我们快速定位问题发生的具体时间地点以及相关信息,能够快速还原业务交易全链路。对这些日志与流水的细节处理,对于系统运维问题定位有非常大的帮助,没有这些有用的日志内容,ELK日志收集套件搭建的再漂亮,收一对垃圾日志也是没用的。通常开源框架只是提供个框架有开发人员自由发挥,而设计一个平台则一定要考虑直接提供统一规范的基础能力。

14.集中配置管理

微服务分布式环境下,一个系统拆分为很多个微服务,一定要告别投产或运维手工修改配置配置的方式。需要采用集中配置管理的方式来提升运维的效率。
配置文件主要有运行前的静态配置和运行期的动态配置两种。静态配置通常是在编译部署包之前设置好。动态配置则是系统运行过程中需要调整的系统变量或者业务参数。要想做到集中的配置管理,那么需要注意以下几点。
是配置与介质分离,这个就需要通过制定规范的方式来控制。千万别把配置放在Jar包里。
是配置的方式要统一,格式、读写方式、变更热更新的模式尽量统一,要采用统一的配置框架
就是需要运行时需要有个配置中心来统一管理业务系统中的配置信息,这个就需要平台来提供配置中心服务和配置管理门户。

15.统一管理门户

微服务架构下,一个大的EAR、WAR应用被拆为了多个小的可独立运行的微服务程序,通常这些微服务程序都不再依赖应用服务器,不依赖传统应用服务器的话,应用服务器提供管理控制台也就没得用了,所以微服务的运行时管理需要有统一的管理门户来支撑。我们规划了的统一集中的微服务门户,可以支撑 应用开发、业务处理、应用管理、系统监控等。上图是应用管理页面,就是对我们传统意义上的业务系统进行管理,点击一个业务系统,我们就能够看到系统下有哪些微服务,每个微服务有几个节点实例再运行,可以监控微服务的子节点状态,对微服务进行配置管理和监控。

16.分布式事务问题

微服务架构的系统下,进程成倍增多,那么也分布式事务一致性的问题也就更加明显。我们这里说的事务一致性,不是传统说的基于数据库实现的技术事务。微服务之间是独立的、调用协议也是无状态的,因此数据库事务方案在一开始就已经不再我们考虑的范围内。我们要解决的是一定时间后的数据达到最终一致状态,准确的说就是采用传统的业务补偿与冲正方式。
推荐的事务一致性方案有三种:

  • 可靠事件模式:即事件的发送和接收保障高可靠性,来实现事务的一致性。
  • 补偿模式:Confirm Cancel ,如果确认失败,则全部逆序取消。
  • TCC模式:Try Confirm Cancel ,补偿模式的一种特殊实现 通常转账类交易会采用这种模式。

晓晨的补充:还有 最大努力型、异步确保、2PC、3PC

17.分布式同步调用问题

微服务架构下,相对于传统部署方式,存在更多的分布式调用,那么“如何在不确定的环境中交付确定的服务”,这句话可以简单理解为,我所依赖的服务的可靠性是无法保证的情况下,我如何保证自己能够正常的提供服务,不被我依赖的其他服务拖垮?
我们推荐SEDA架构来解决这个问题。

SEDA : staged event-driven architecture本质上就是采用分布式事件驱动的模式,用异步模拟来同步,无阻塞等待,再加上资源分配隔离结起来的一个解决方案。

18.持续集成与持续交付设计

在运维方面,首先我们要解决的就是持续集成和持续交付,而微服务应用平台的职责范围目前规划是只做持续集成,能够方便的用持续集成环境把程序编译成介质包和部署包。(目前规划持续部署由DevOps平台提供相应能力,微服务平台可与DevOps平台集成)
这里要厘清一个概念:介质,是源码编译后的产物,与环境无关,多环境下应该是可以共用的,如:jar、dockerfile;配置:则是环境相关的信息。配置+介质=部署包。
获取到部署包之后,微服务应用平台的职责就完成了,接下来就是运维人员各显神通来进行上线部署操作。

19.微服务平台与容器云、DevOps的关系

就微服务应用平台本身来说,并不依赖DevOps和容器云,开发好的部署包可以运行在物理机、虚拟机或者是容器中。
然而当微服务应用平台结合了DevOps和容器云之后,我们就会发现,持续集成和交付变成了一个非常简单便捷并且又可靠的过程。
简单几步操作,整套开发、测试、预发或者生产环境就能够搭建完成。整个过程的复杂度都由平台给屏蔽掉了,通过三大基础环境的整合,我们能够使分散的微服务组件更简单方便的进行统一管理和运维交付。

终于有人把“TCC分布式事务”实现原理讲明白了! - JaJian - 博客园

$
0
0

之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下。很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用。

所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务。

首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同学,可以参考之前的文章: 《拜托,面试请不要再问我Spring Cloud底层原理!》

业务场景介绍

咱们先来看看业务场景,假设你现在有一个电商系统,里面有一个支付订单的场景。

那对一个订单支付之后,我们需要做下面的步骤:

  • 更改订单的状态为“已支付”
  • 扣减商品库存
  • 给会员增加积分
  • 创建销售出库单通知仓库发货

这是一系列比较真实的步骤,无论大家有没有做过电商系统,应该都能理解。

进一步思考

好,业务场景有了,现在我们要更进一步,实现一个 TCC 分布式事务的效果。

什么意思呢?也就是说,[1] 订单服务-修改订单状态,[2] 库存服务-扣减库存,[3] 积分服务-增加积分,[4] 仓储服务-创建销售出库单。

上述这几个步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。

举个例子,现在订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。

结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑人么,当然不能允许这种情况发生了!

但是如果你不用 TCC 分布式事务方案的话,就用个 Spring Cloud 开发这么一个微服务系统,很有可能会干出这种事儿来。

我们来看看下面的这个图,直观的表达了上述的过程:

所以说,我们有必要使用 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。

上面那几个步骤,要么全部成功,如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。

比如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停止执行增加积分和通知出库两个操作。

说了那么多,老规矩,给大家上一张图,大伙儿顺着图来直观的感受一下:

落地实现 TCC 分布式事务

那么现在到底要如何来实现一个 TCC 分布式事务,使得各个服务,要么一起成功?要么一起失败呢?

大家稍安勿躁,我们这就来一步一步的分析一下。咱们就以一个 Spring Cloud 开发系统作为背景来解释。

TCC 实现阶段一:Try

首先,订单服务那儿,它的代码大致来说应该是这样子的:

public class OrderService {

    // 库存服务
    @Autowired
    private InventoryService inventoryService;

    // 积分服务
    @Autowired
    private CreditService creditService;

    // 仓储服务
    @Autowired
    private WmsService wmsService;

    // 对这个订单完成支付
    public void pay(){
        //对本地的的订单数据库修改订单状态为"已支付"
        orderDAO.updateStatus(OrderStatus.PAYED);

        //调用库存服务扣减库存
        inventoryService.reduceStock();

        //调用积分服务增加积分
        creditService.addCredit();

        //调用仓储服务通知发货
        wmsService.saleDelivery();
    }
}

如果你之前看过 Spring Cloud 架构原理那篇文章,同时对 Spring Cloud 有一定的了解的话,应该是可以理解上面那段代码的。

其实就是订单服务完成本地数据库操作之后,通过 Spring Cloud 的 Feign 来调用其他的各个服务罢了。

但是光是凭借这段代码,是不足以实现 TCC 分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点儿代码好不好。

首先,上面那个订单服务先把自己的状态修改为:OrderStatus.UPDATING。

这是啥意思呢?也就是说,在 pay() 那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。

这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。

然后呢,库存服务直接提供的那个 reduceStock() 接口里,也别直接扣减库存啊,你可以是冻结掉库存。

举个例子,本来你的库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!

你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。

积分服务的 addCredit() 接口也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。

比如:用户积分原本是 1190,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊!

你可以保持积分为 1190 不变,在一个预增加字段里,比如说 prepare_add_credit 字段,设置一个 10,表示有 10 个积分准备增加。

仓储服务的 saleDelivery() 接口也是同理啊,你可以先创建一个销售出库单,但是这个销售出库单的状态是“UNKNOWN”。

也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢!

上面这套改造接口的过程,其实就是所谓的 TCC 分布式事务中的第一个 T 字母代表的阶段,也就是 Try 阶段。

总结上述过程,如果你要实现一个 TCC 分布式事务,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个 Try 的操作。

这个操作,一般都是锁定某个资源,设置一个预备类的状态,冻结部分数据,等等,大概都是这类操作。

咱们来一起看看下面这张图,结合上面的文字,再来捋一捋整个过程:

TCC 实现阶段二:Confirm

然后就分成两种情况了,第一种情况是比较理想的,那就是各个服务执行自己的那个 Try 操作,都执行成功了,Bingo!

这个时候,就需要依靠 TCC 分布式事务框架来推动后续的执行了。这里简单提一句,如果你要玩儿 TCC 分布式事务,必须引入一款 TCC 分布式事务框架,比如国内开源的 ByteTCC、Himly、TCC-transaction。

否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了。

如果你在各个服务里引入了一个 TCC 分布式事务的框架,订单服务里内嵌的那个 TCC 分布式事务框架可以感知到,各个服务的 Try 操作都成功了。

此时,TCC 分布式事务框架会控制进入 TCC 下一个阶段,第一个 C 阶段,也就是 Confirm 阶段。

为了实现这个阶段,你需要在各个服务里再加入一些代码。比如说,订单服务里,你可以加入一个 Confirm 的逻辑,就是正式把订单的状态设置为“已支付”了,大概是类似下面这样子:

public class OrderServiceConfirm {

    public void pay(){
        orderDao.updateStatus(OrderStatus.PAYED);
    }
}

库存服务也是类似的,你可以有一个 InventoryServiceConfirm 类,里面提供一个 reduceStock() 接口的 Confirm 逻辑,这里就是将之前冻结库存字段的 2 个库存扣掉变为 0。

这样的话,可销售库存之前就已经变为 98 了,现在冻结的 2 个库存也没了,那就正式完成了库存的扣减。

积分服务也是类似的,可以在积分服务里提供一个 CreditServiceConfirm 类,里面有一个 addCredit() 接口的 Confirm 逻辑,就是将预增加字段的 10 个积分扣掉,然后加入实际的会员积分字段中,从 1190 变为 1120。

仓储服务也是类似,可以在仓储服务中提供一个 WmsServiceConfirm 类,提供一个 saleDelivery() 接口的 Confirm 逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理人员查看和使用,而不是停留在之前的中间状态“UNKNOWN”了。

好了,上面各种服务的 Confirm 的逻辑都实现好了,一旦订单服务里面的 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执行各个服务的 Confirm 逻辑。

订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进行通信,依次调用各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执行。

同样,给大家来一张图,顺着图一起来看看整个过程:

TCC 实现阶段三:Cancel

好,这是比较正常的一种情况,那如果是异常的一种情况呢?

举个例子:在 Try 阶段,比如积分服务吧,它执行出错了,此时会怎么样?

那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。

也就是说,会执行各个服务的第二个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加一些代码。

首先订单服务,它得提供一个 OrderServiceCancel 的类,在里面有一个 pay() 接口的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。

库存服务也是同理,可以提供 reduceStock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存里去,98 + 2 = 100。

积分服务也需要提供 addCredit() 接口的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。

仓储服务也需要提供一个 saleDelivery() 接口的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。

然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。

大家看看下面的图,直观的感受一下:

总结与思考

好了,兄弟们,聊到这儿,基本上大家应该都知道 TCC 分布式事务具体是怎么回事了!

总结一下,你要玩儿 TCC 分布式事务的话:首先需要选择某种 TCC 分布式事务框架,各个服务里就会有这个 TCC 分布式事务框架在运行。

然后你原本的一个接口,要改造为 3 个逻辑,Try-Confirm-Cancel:

  • 先是服务调用链路依次执行 Try 逻辑。
  • 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。
  • 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。

这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时:

  • 某个服务的数据库宕机了。
  • 某个服务自己挂了。
  • 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。
  • 某些资源不足了,比如说库存不够这些。

先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。

如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。

接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。

那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。

此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。

等一等,你有没有想到一个问题?如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?

所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。

问题还没完,万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢?

那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功!

当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的!

最后,再给大家来一张图,来看看给我们的业务,加上分布式事务之后的整个执行流程:

不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用,比如我们就是这样。

不过如果自己公司没有研发 TCC 分布式事务框架的话,那一般就会选用开源的框架。

这里笔者给大家推荐几个比较不错的框架,都是咱们国内自己开源出去的:ByteTCC,TCC-transaction,Himly。

大家有兴趣的可以去它们的 GitHub 地址,学习一下如何使用,以及如何跟 Spring Cloud、Dubbo 等服务框架整合使用。

只要把那些框架整合到你的系统里,很容易就可以实现上面那种奇妙的 TCC 分布式事务的效果了。

下面,我们来讲讲可靠消息最终一致性方案实现的分布式事务,同时聊聊在实际生产中遇到的运用该方案的高可用保障架构。

最终一致性分布式事务如何保障实际生产中 99.99% 高可用?

上面咱们聊了聊 TCC 分布式事务,对于常见的微服务系统,大部分接口调用是同步的,也就是一个服务直接调用另外一个服务的接口。

这个时候,用 TCC 分布式事务方案来保证各个接口的调用,要么一起成功,要么一起回滚,是比较合适的。

但是在实际系统的开发过程中,可能服务间的调用是异步的。也就是说,一个服务发送一个消息给 MQ,即消息中间件,比如 RocketMQ、RabbitMQ、Kafka、ActiveMQ 等等。

然后,另外一个服务从 MQ 消费到一条消息后进行处理。这就成了基于 MQ 的异步调用了。

那么针对这种基于 MQ 的异步调用,如何保证各个服务间的分布式事务呢?也就是说,我希望的是基于 MQ 实现异步调用的多个服务的业务逻辑,要么一起成功,要么一起失败。

这个时候,就要用上可靠消息最终一致性方案,来实现分布式事务。

大家看上图,如果不考虑各种高并发、高可用等技术挑战的话,单从“可靠消息”以及“最终一致性”两个角度来考虑,这种分布式事务方案还是比较简单的。

可靠消息最终一致性方案的核心流程

①上游服务投递消息

如果要实现可靠消息最终一致性方案,一般你可以自己写一个可靠消息服务,实现一些业务逻辑。

首先,上游服务需要发送一条消息给可靠消息服务。这条消息说白了,你可以认为是对下游服务一个接口的调用,里面包含了对应的一些请求参数。

然后,可靠消息服务就得把这条消息存储到自己的数据库里去,状态为“待确认”。

接着,上游服务就可以执行自己本地的数据库操作,根据自己的执行结果,再次调用可靠消息服务的接口。

如果本地数据库操作执行成功了,那么就找可靠消息服务确认那条消息。如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。

此时如果是确认消息,那么可靠消息服务就把数据库里的消息状态更新为“已发送”,同时将消息发送给 MQ。

这里有一个很关键的点,就是更新数据库里的消息状态和投递消息到 MQ。这俩操作,你得放在一个方法里,而且得开启本地事务。

啥意思呢?如果数据库里更新消息的状态失败了,那么就抛异常退出了,就别投递到 MQ;如果投递 MQ 失败报错了,那么就要抛异常让本地数据库事务回滚。这俩操作必须得一起成功,或者一起失败。

如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。

②下游服务接收消息

下游服务就一直等着从 MQ 消费消息好了,如果消费到了消息,那么就操作自己本地数据库。

如果操作成功了,就反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。

③如何保证上游服务对消息的 100% 可靠投递?

上面的核心流程大家都看完:一个很大的问题就是,如果在上述投递消息的过程中各个环节出现了问题该怎么办?

我们如何保证消息 100% 的可靠投递,一定会从上游服务投递到下游服务?别着急,下面我们来逐一分析。

如果上游服务给可靠消息服务发送待确认消息的过程出错了,那没关系,上游服务可以感知到调用异常的,就不用执行下面的流程了,这是没问题的。

如果上游服务操作完本地数据库之后,通知可靠消息服务确认消息或者删除消息的时候,出现了问题。

比如:没通知成功,或者没执行成功,或者是可靠消息服务没成功的投递消息到 MQ。这一系列步骤出了问题怎么办?

其实也没关系,因为在这些情况下,那条消息在可靠消息服务的数据库里的状态会一直是“待确认”。

此时,我们在可靠消息服务里开发一个后台定时运行的线程,不停的检查各个消息的状态。

如果一直是“待确认”状态,就认为这个消息出了点什么问题。此时的话,就可以回调上游服务提供的一个接口,问问说,兄弟,这个消息对应的数据库操作,你执行成功了没啊?

如果上游服务答复说,我执行成功了,那么可靠消息服务将消息状态修改为“已发送”,同时投递消息到 MQ。

如果上游服务答复说,没执行成功,那么可靠消息服务将数据库中的消息删除即可。

通过这套机制,就可以保证,可靠消息服务一定会尝试完成消息到 MQ 的投递。

④如何保证下游服务对消息的 100% 可靠接收?

那如果下游服务消费消息出了问题,没消费到?或者是下游服务对消息的处理失败了,怎么办?

其实也没关系,在可靠消息服务里开发一个后台线程,不断的检查消息状态。

如果消息状态一直是“已发送”,始终没有变成“已完成”,那么就说明下游服务始终没有处理成功。

此时可靠消息服务就可以再次尝试重新投递消息到 MQ,让下游服务来再次处理。

只要下游服务的接口逻辑实现幂等性,保证多次处理一个消息,不会插入重复数据即可。

⑤如何基于 RocketMQ 来实现可靠消息最终一致性方案?

在上面的通用方案设计里,完全依赖可靠消息服务的各种自检机制来确保:

  • 如果上游服务的数据库操作没成功,下游服务是不会收到任何通知。
  • 如果上游服务的数据库操作成功了,可靠消息服务死活都会确保将一个调用消息投递给下游服务,而且一定会确保下游服务务必成功处理这条消息。

通过这套机制,保证了基于 MQ 的异步调用/通知的服务间的分布式事务保障。其实阿里开源的 RocketMQ,就实现了可靠消息服务的所有功能,核心思想跟上面类似。

只不过 RocketMQ 为了保证高并发、高可用、高性能,做了较为复杂的架构实现,非常的优秀。有兴趣的同学,自己可以去查阅 RocketMQ 对分布式事务的支持。

可靠消息最终一致性方案的高可用保障生产实践

背景引入

上面那套方案和思想,很多同学应该都知道是怎么回事儿,我们也主要就是铺垫一下这套理论思想。

在实际落地生产的时候,如果没有高并发场景的,完全可以参照上面的思路自己基于某个 MQ 中间件开发一个可靠消息服务。

如果有高并发场景的,可以用 RocketMQ 的分布式事务支持上面的那套流程都可以实现。

今天给大家分享的一个核心主题,就是这套方案如何保证 99.99% 的高可用。

大家应该发现了这套方案里保障高可用性最大的一个依赖点,就是 MQ 的高可用性。

任何一种 MQ 中间件都有一整套的高可用保障机制,无论是 RabbitMQ、RocketMQ 还是 Kafka。

所以在大公司里使用可靠消息最终一致性方案的时候,我们通常对可用性的保障都是依赖于公司基础架构团队对 MQ 的高可用保障。

也就是说,大家应该相信兄弟团队,99.99% 可以保障 MQ 的高可用,绝对不会因为 MQ 集群整体宕机,而导致公司业务系统的分布式事务全部无法运行。

但是现实是很残酷的,很多中小型的公司,甚至是一些中大型公司,或多或少都遇到过 MQ 集群整体故障的场景。

MQ 一旦完全不可用,就会导致业务系统的各个服务之间无法通过 MQ 来投递消息,导致业务流程中断。

比如最近就有一个朋友的公司,也是做电商业务的,就遇到了 MQ 中间件在自己公司机器上部署的集群整体故障不可用,导致依赖 MQ 的分布式事务全部无法跑通,业务流程大量中断的情况。

这种情况,就需要针对这套分布式事务方案实现一套高可用保障机制。

基于 KV 存储的队列支持的高可用降级方案

大家来看看下面这张图,这是我曾经指导过朋友的一个公司针对可靠消息最终一致性方案设计的一套高可用保障降级机制。

这套机制不算太复杂,可以非常简单有效的保证那位朋友公司的高可用保障场景,一旦 MQ 中间件出现故障,立马自动降级为备用方案。

①自行封装 MQ 客户端组件与故障感知

首先第一点,你要做到自动感知 MQ 的故障接着自动完成降级,那么必须动手对 MQ 客户端进行封装,发布到公司 Nexus 私服上去。

然后公司需要支持 MQ 降级的业务服务都使用这个自己封装的组件来发送消息到 MQ,以及从 MQ 消费消息。

在你自己封装的 MQ 客户端组件里,你可以根据写入 MQ 的情况来判断 MQ 是否故障。

比如说,如果连续 10 次重新尝试投递消息到 MQ 都发现异常报错,网络无法联通等问题,说明 MQ 故障,此时就可以自动感知以及自动触发降级开关。

②基于 KV 存储中队列的降级方案

如果 MQ 挂掉之后,要是希望继续投递消息,那么就必须得找一个 MQ 的替代品。

举个例子,比如我那位朋友的公司是没有高并发场景的,消息的量很少,只不过可用性要求高。此时就可以使用类似 Redis 的 KV 存储中的队列来进行替代。

由于 Redis 本身就支持队列的功能,还有类似队列的各种数据结构,所以你可以将消息写入 KV 存储格式的队列数据结构中去。

PS:关于 Redis 的数据存储格式、支持的数据结构等基础知识,请大家自行查阅了,网上一大堆。

但是,这里有几个大坑,一定要注意一下:

第一个,任何 KV 存储的集合类数据结构,建议不要往里面写入数据量过大,否则会导致大 Value 的情况发生,引发严重的后果。

因此绝不能在 Redis 里搞一个 Key,就拼命往这个数据结构中一直写入消息,这是肯定不行的。

第二个,绝对不能往少数 Key 对应的数据结构中持续写入数据,那样会导致热 Key 的产生,也就是某几个 Key 特别热。

大家要知道,一般 KV 集群,都是根据 Key 来 Hash 分配到各个机器上的,你要是老写少数几个 Key,会导致 KV 集群中的某台机器访问过高,负载过大。

基于以上考虑,下面是笔者当时设计的方案:

  • 根据它们每天的消息量,在 KV 存储中固定划分上百个队列,有上百个 Key 对应。
  • 这样保证每个 Key 对应的数据结构中不会写入过多的消息,而且不会频繁的写少数几个 Key。
  • 一旦发生了 MQ 故障,可靠消息服务可以对每个消息通过 Hash 算法,均匀的写入固定好的上百个 Key 对应的 KV 存储的队列中。

同时需要通过 ZK 触发一个降级开关,整个系统在 MQ 这块的读和写全部立马降级。

③下游服务消费 MQ 的降级感知

下游服务消费 MQ 也是通过自行封装的组件来做的,此时那个组件如果从 ZK 感知到降级开关打开了,首先会判断自己是否还能继续从 MQ 消费到数据?

如果不能了,就开启多个线程,并发的从 KV 存储的各个预设好的上百个队列中不断的获取数据。

每次获取到一条数据,就交给下游服务的业务逻辑来执行。通过这套机制,就实现了 MQ 故障时候的自动故障感知,以及自动降级。如果系统的负载和并发不是很高的话,用这套方案大致是没问题的。

因为在生产落地的过程中,包括大量的容灾演练以及生产实际故障发生时的表现来看,都是可以有效的保证 MQ 故障时,业务流程继续自动运行的。

④故障的自动恢复

如果降级开关打开之后,自行封装的组件需要开启一个线程,每隔一段时间尝试给 MQ 投递一个消息看看是否恢复了。

如果 MQ 已经恢复可以正常投递消息了,此时就可以通过 ZK 关闭降级开关,然后可靠消息服务继续投递消息到 MQ,下游服务在确认 KV 存储的各个队列中已经没有数据之后,就可以重新切换为从 MQ 消费消息。

⑤更多的业务细节

上面说的那套方案是一套通用的降级方案,但是具体的落地是要结合各个公司不同的业务细节来决定的,很多细节多没法在文章里体现。

比如说你们要不要保证消息的顺序性?是不是涉及到需要根据业务动态,生成大量的 Key?等等。

此外,这套方案实现起来还是有一定的成本的,所以建议大家尽可能还是 Push 公司的基础架构团队,保证 MQ 的 99.99% 可用性,不要宕机。

其次就是根据大家公司实际对高可用的需求来决定,如果感觉 MQ 偶尔宕机也没事,可以容忍的话,那么也不用实现这种降级方案。

但是如果公司领导认为 MQ 中间件宕机后,一定要保证业务系统流程继续运行,那么还是要考虑一些高可用的降级方案,比如本文提到的这种。

最后再说一句,真要是一些公司涉及到每秒几万几十万的高并发请求,那么对 MQ 的降级方案会设计的更加的复杂,那就远远不是这么简单可以做到的。

来源:【微信公众号】石杉的架构笔记


VisualVM分析与HelloWorld、springBoot项目 - metabolism - 博客园

$
0
0

VisualVM分析与HelloWorld、springBoot项目

自从1995年第一个JDK版本JDKBeta发布,至今已经快25年,这些年来Java的框架日新月异,从最开始的Servlet阶段,到SSH,SSI,SSM,springboot等,还有一些其他方向的框架微服务SpringCloud、响应式编程Spring Reactor。零零总总 的框架,我们都需要去熟悉,那么怎么去快速熟悉呢,我觉得可以看源码,可以看博客,也可以根据内存分配去完善理解。

那么问题来了,一个Java项目在咱们平时启动项目的时候,究竟发生了什么,创建几个简单的项目,用VisualVM来分析一下~

Main

简单的项目,应该没有比HelloWorld更简单的了吧,按照老规矩,咱们就从HelloWorld开始分析!那么简单的项目大家都能闭着眼睛敲出来,是不是没分析的必要啊,别着急,写好HelloWorld咱们开始分析:

System.out.println("HelloWorld start");
// 这里让线程睡一会,方便分析
Thread.sleep(100000);
System.out.println("HelloWorld end");

运行main方法,打开VisualVM,发现事情并不简单哦,这个简单的项目有十六个线程维护,其中守护线程有十五个。

其中几大线程的内存分配情况如下:

这些线程都是干什么用的?写了那么多年HelloWorld没想到还有这种知识盲区:

  1. RMI TCP Connection(2)-10.128.227.33

    10.128.227.33是我本地的ip地址。正确而愚蠢的原因是因为开了VisualVM(JMX客户端),JVM需要把他的数据传递给这个客户端,就是使用的TCP传递,相同作用的线程还有 JMX server connection timeout:MAIN方法跑完了,JMX连接的心跳断开。 RMI TCP Connection(idle):用来在RMI连接池中创建线程。 *** Profiler Agent Communication Thread:Profiler代理通信线程。 RMI TCP Accept-0:进行JMX进行JMX监测。

  2. Attach Listener

    Attach Listener线程是负责接收到外部的命令,对该命令进行执行并把结果返回给发送者。通常我们会用一些命令去要求jvm给我们一些反馈信息,如:java -version、jmap、jstack等等。如果该线程在jvm启动的时候没有初始化,那么,则会在用户第一次执行jvm命令时,得到启动。

  3. main

    main线程,就是我们代码所写得代码对应线程

  4. Monitor Ctr-Break

    这应该是 IDEA 通过反射的方式,伴随你的程序一起启动的对你程序的监控线程。这也是一个默认全局线程

  5. Signal Dispatcher

    前面提到的Attach Listener线程职责是接收外部jvm命令,当命令接收成功后,就会交给signal dispather线程分发到各个不同的模块处理,并且返回处理结果。signal dispather线程是在第一次接收外部jvm命令时,才进行初始化工作。

  6. Finalizer

    这个线程是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:

    1. 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;

    2. 该线程是守护线程,因此如果虚拟机中没有其他非守护线程的线程,不管该线程有没有执行完finalize()方法,JVM也会退出;
    3. JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;
    4. JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难,所以单独创建了一个守护线程。

  7. Reference Handler

    VM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。

经过上面的分析可以看出来main本身程序的线程有:main线程,Reference Handler线程,Finalizer线程,Attach Listener线程,Signal Dispatcher线程。

java代码想要实现也很简单,如下即可:

// 获取java线程管理器MXBean,dumpAllThreads参数:
//                                  lockedMonitors参数表示是否获取同步的monitor信息
//                                  lockedSynchronizers表示是否获取同步的synchronizer
ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, false);
for (ThreadInfo threadInfo : threadInfos) {
    System.out.println(threadInfo.getThreadId() + " : " + threadInfo.getThreadName());
}

得到的打印结果为:

也就是说,写了那么多年的HelloWorld居然有五个线程来支撑,而我却一直被蒙在鼓里??谁能随时去关注项目有多少个线程啊,VIsualVM可以= =,虽然我觉得他一直起线程进行通信很蠢,但是项目结构大了就有必要了。

Spring-Boot

那么一个啥都没有的springBoot项目启动了之后,会有哪些线程呢?先看看他的pom文件:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.1.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.visual.vm.performance</groupId><artifactId>mock</artifactId><version>0.0.1-SNAPSHOT</version><name>mock</name><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

只引入了spring-boot-starter-web的依赖,其他的什么都没有,启动着试一下。共有27个线程,守护线程有23个。

不同的颜色对应着不同的状态,详情看右下角。这些线程很多都是熟悉的,Main方法分析过的,通过VisualVM工具进行JMX监视(RMI TCP...)开了些线程;IDEA(Monitor Ctrl-Break)开了些线程;垃圾回收(Finalizer,Reference Handler)开了些线程。着重讲一下没见过的线程。

  1. DestroyJavaVM

    所有 POJO应用程序都通过调用该 main方法开始。正常情况下,main 完成后,将告知JVM的DestroyJavaVM`线程来关闭JVM,该线程等待所有非守护进程线程完成后再进行工作。这是为了确保创建的所有非守护程序线程都可以在JVM拆除之前运行完毕。

    但是,带有GUI的应用程序通常以多个线程运行。用于监视系统事件,例如键盘或鼠标事件。JVM仍然会创建 DestroyJavaVM线程,且需要等待所有创建的线程完成,然后再拆除VM,然而应用并不会停止,所以DestoryJavaVM线程就会一直处于等待,直到应用运行完成。

    任何创建线程并仅依赖其功能的应用程序都会有一个 DestroyJavaVM线程,等待应用程序完成并关闭JVM。由于它等待所有其他线程执行完毕( join),因此它不会消耗任何资源。

  2. Http-nio-8080-AcceptorHttp-nio-8080-ClientPollerHttp-nio-8080-BlockPollerhttp-nio-8080-exec-1...10

    这些线程都有个特点,http-nio-8080开头。8080就是这个应用的端口,显然这是给容器使用的。项目引入的是spring-boot-starter-web依赖,也就是默认使用springBoot的内置tomcat容器启动,我们的maven下面也会有这样的几个包: tomcat-embed-coretomcat-embed-eltomcat-embed-websocket,我们所看到的线程都是由这几个包产生的。那么这些线程是干什么用的?

    解决这个问题之前,先看一下tomcat的总体架构:

    Tomcat由Connector和Container两个核心组件构成,Connector组件负责网络请求接入,目前支持BIO、NIO、APR三种模式,Tomcat5之后就支持了NIO,看我们的线程名也就是用的NIO;Container组件负责管理servlet容器。service服务将Container和Connector又包装了一层,使得外部可以直接获取。多个service服务运行在tomcat的Server服务器上,Server上有所有的service实例,并实现了LifeCycle接口来控制所有service的生命周期。

    而NIO对应线程主要是实现在Connector组件中,他负责接受浏览器发过来的tcp请求,创建一个Reuqest和Response对象用来请求和响应,然后产生一个线程,将Request和Response分发给他们对应处理的线程。

    终于看到了线程名中包含的Acceptor、Poller。他们都在Connector组件下的Http11NioProtocol下。着重介绍一下Http11NioProtocol下面的几个组件

    1. Acceptor:接受socket线程,接受的方法比较传统:serverSocket.accept(),得到SocketChannel对象并封装到NioChannel对象中。然后NioChannel对象封装在PollerEvent对象中,并放到events queue中。使用队列(生产者-消费者)和Poller组件交互,Acceptor是生产者,Poller是消费者,通过events queue通信。

      package org.apache.tomcat.util.net;
      
      public class Acceptor<U> implements Runnable {
            ...
          public void run() {
              byte errorDelay = 0;
              while(this.endpoint.isRunning()) {
                            ....
                  try {
                      this.endpoint.countUpOrAwaitConnection();
                      if (!this.endpoint.isPaused()) {
                          Object socket = null;
                          try {
                            // 这句会调用NioEndPoint类,底层是serverSock.accept()
                              socket = this.endpoint.serverSocketAccept();
                          } catch (Exception var6) {
                              ...
                          }
                                            ...
                      }
                  } catch (Throwable var7) {
                      ...
                  }
              }
      
              this.state = Acceptor.AcceptorState.ENDED;
          }
      }
    2. Poller:NIO选择器Selector用于检查一个或多个NIO Channel(通道)的状态是否可读、可写。如此可以实现单线程管理多个channels也就是可以管理多个网络线程。Poller是NIO实现的主要线程,首先从events queue队列中消费得到PollerEvent对象,再将此对象中的Channel以OP_READ事件注册到主Selector中,Selector执行select操作,遍历出可以读数据的socket,并从Worker线程池中拿到可用的Workrer线程,将可用的socket传递给Worker线程。

      package org.apache.tomcat.util.net;
      public class Poller implements Runnable {
           ...
           public void run() {
               while(true) {
                   boolean hasEvents = false;
                      label59: {
                          try {
                              if (!this.close) {
                                  hasEvents = this.events();
                                  if (this.wakeupCounter.getAndSet(-1L) > 0L) {
                                      this.keyCount = this.selector.selectNow();
                                  } else {
                                    // selector.select方法,接受acceptor的socket
                                      this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout);
                                  }
      
                                  this.wakeupCounter.set(0L);
                              }
      
                              if (!this.close) {
                                  break label59;
                              }
      
                              this.events();
                              this.timeout(0, false);
      
                              try {
                                  this.selector.close();
                              } catch (IOException var5) {
                                  NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorCloseFail"), var5);
                              }
                          } catch (Throwable var6) {
                              ExceptionUtils.handleThrowable(var6);
                              NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorLoopError"), var6);
                              continue;
                          }
      
                          NioEndpoint.this.getStopLatch().countDown();
                          return;
                      }
      
                      if (this.keyCount == 0) {
                          hasEvents |= this.events();
                      }
      
                      Iterator iterator = this.keyCount > 0 ? this.selector.selectedKeys().iterator() : null;
      
                      while(iterator != null && iterator.hasNext()) {
                          SelectionKey sk = (SelectionKey)iterator.next();
                          NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment();
                          if (socketWrapper == null) {
                              iterator.remove();
                          } else {
                              iterator.remove();
                            // 然后调用processKey方法,将socket传给worker线程进行处理
                              this.processKey(sk, socketWrapper);
                          }
                      }
      
                      this.timeout(this.keyCount, hasEvents);
                  }
              }
          }
    3. Worker:Worker线程从Poller传过来的socket后,将socket封装在SocketProcessor对象中,然后从Http11ConnectionHandler获取Http11NioProcessor对象,从Http11NioProcessor中调用CoyoteAdapter的逻辑(这就出了Http11NioProtocol组件,可以看上上图)。在Worker线程中,会完成从socket中读取http request,解析成HttpervletRequest对象,分派到相应的servlet并完成逻辑,然而将response通过socket发回client。

      package org.apache.tomcat.util.net;
      protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
              public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
                  super(socketWrapper, event);
              }
      
              protected void doRun() {
                // 这一句从Poller拿到socket,然后进行tomcat主线程处理流程
                  NioChannel socket = (NioChannel)this.socketWrapper.getSocket();
                  SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector());
                  NioEndpoint.Poller poller = NioEndpoint.this.poller;
                  if (poller == null) {
                      this.socketWrapper.close();
                  } else {
                      try {
                          int handshake = -1;
      
                          try {
                              if (key != null) {
                                  if (socket.isHandshakeComplete()) {
                                      handshake = 0;
                                  } else if (this.event != SocketEvent.STOP && this.event != SocketEvent.DISCONNECT && this.event != SocketEvent.ERROR) {
                                      handshake = socket.handshake(key.isReadable(), key.isWritable());
                                      this.event = SocketEvent.OPEN_READ;
                                  } else {
                                      handshake = -1;
                                  }
                              }
                          } catch (IOException var13) {
                              handshake = -1;
                              if (NioEndpoint.log.isDebugEnabled()) {
                                  NioEndpoint.log.debug("Error during SSL handshake", var13);
                              }
                          } catch (CancelledKeyException var14) {
                              handshake = -1;
                          }
      
                          if (handshake == 0) {
                              SocketState state = SocketState.OPEN;
                              if (this.event == null) {
                                  state = NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.OPEN_READ);
                              } else {
                                  state = NioEndpoint.this.getHandler().process(this.socketWrapper, this.event);
                              }
      
                              if (state == SocketState.CLOSED) {
                                  poller.cancelledKey(key, this.socketWrapper);
                              }
                          } else if (handshake == -1) {
                              NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.CONNECT_FAIL);
                              poller.cancelledKey(key, this.socketWrapper);
                          } else if (handshake == 1) {
                              this.socketWrapper.registerReadInterest();
                          } else if (handshake == 4) {
                              this.socketWrapper.registerWriteInterest();
                          }
                      } catch (CancelledKeyException var15) {
                          ...
                      } finally {
                          ...
      
                      }
      
                  }
              }
          }
    4. NioSelectorPool:NioEndPoint对象维护了一个NioSelectorPool对象,这个NioSelectorPool中又维护了一个BlockPoller线程(基于Selector进行NIO逻辑)。

总结

平时看起来很熟悉的代码,HelloWorld和SpringBoot初始化的项目。没想到背地里有那么多线程来支撑。装了VisualVM插件并不是让你蹭的就变强,但是可以给你提供一些进步的思路,引导你去思考去进步。下面还会继续带着分析更复杂的项目,不知道会不会有更多常见又未知的知识点等待我们去发现~

欢迎访问我的个人博客

微服务的脚手架Jhipster使用(一) - 陆陆起飞啦 - 博客园

$
0
0

随着微服务的普及以及docker容器的广泛应用,有传统的soa服务衍生出微服务的概念,微服务强调的是服务的独立性,屏蔽底层物理平台的差异,此时你会发现微服务跟容器技术完美契合。在此基础上衍生出的云原生以及DevOps的概念,废话不多说介绍一个非常牛叉的springCloud脚手架- -jhipster。

  

  •     安装 
  1. 安装Java 8 from  the Oracle website.
  2. 安装Node.js from  the Node.js website (请安装 64-bit version)
  3. 安装npm包:  npm install -g npm
  4. 如果你想使用jhipster应用市场, 请安装 Yeoman:  npm install -g yo
  5. 最后安装JHipster:  npm install -g generator-jhipster
  •          生成项目
  1. 选择一个空的文件夹打开cmd:jhipster  
  2. 根据一步步step提示选择构建自己的服务项目
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    <strong>windows下:</strong><br>以下demo选择微服务应用。实际中根据自己需求生产项目。
    1: Which *type* of application would you like to create? (Use arrow keys)
       Monolithic application (recommended for  simple projects)  //简单项目
       Microservice application // 微服务应用
       Microservice gateway // 微服务网关
       JHipster UAA server (for  microservice OAuth2 authentication) // 微服务认证
    2  :What is the base name of your application? (huhuawei)
        输入服务名称
    3: As you are running in a microservice architecture, on which port would like        
        your server to run? It should be unique to avoid port conflicts. (8081)
        设置服务的端口号
    4:What is your default  Java package  name? (com.mycompany.myapp)
        设置包名
    5:Which service discovery server do  you want to use? (Use arrow keys)
         JHipster Registry (uses Eureka)
         Consul
         No service discovery
        选择注册中心。一般选择Registry比较多
    6:Which *type* of authentication would you like to use? (Use arrow keys)
         JWT authentication (stateless, with a token)  // jwt
         OAuth 2.0  / OIDC Authentication (stateful, works with Keycloak and    
         Okta)//Oauth2 OIDC 认证服务
         Authentication with JHipster UAA server (the server must be generated 
         separately) // Oauth2+jwt Uaa认证服务
         选择授权中心
    7: Which *type* of database would you like to use?
          SQL (H2, MySQL, MariaDB, PostgreSQL, Oracle, MSSQL)
          MongoDB
          Couchbase
          No database
          Cassandra
          选择数据库支持Nosql跟常见RDMB数据库
    8:? Which *production* database would you like to use? (Use arrow keys)
          MySQL
          MariaDB
          PostgreSQL
          Oracle
          Microsoft SQL Server
          选择数据库,这边会出现两次第一次是production 第二次是devlopment
    9:Do you want to use the Spring cache abstraction?
         根据需求选择缓存
    10:Do you want to use Hibernate 2nd level cache? (Y/n)
         是否支持二级缓存
    11: Would you like to use Maven or Gradle for  building the backend? (Use
          arrow keys)
          Maven
          Gradle
    12:Which other technologies would you like to use?
           安装一些其他的组件。如ES,KAFKA之类的
    13:Would you like to enable internationalization support? (Y/n)
           支持国际化?
    14: Please choose the native  language of the application (Use arrow keys)
            English
            Estonian
            Farsi
            French
            Galician
            ........
            选择本地支持的语言包含中文
    15:Please choose additional languages to install
           可以额外安装其他语言
    16:Besides JUnit and Jest, which testing frameworks would you like to use?
             Gatling
             Cucumber   
           选择测试框架,针对微服务http接口测试,生成测试报告
    17:Would you like to install other generators from the JHipster Marketplace?
           从jhipster市场中选择组件安装
           
  3. 如果你觉得安装这些环境太麻烦,你又熟悉docker的基本命令,那建议使用docker去生成项目;
    复制代码
    选择linux服务器,安装docker;
    yum install -y yum-utils device-mapper-persistent-data lvm2
    yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    yum list docker-ce --showduplicates | sort -r
    sudo yum install -y docker-ce
    sudo systemctl start docker
    sudo systemctl enable docker
    
    拉取jhipster官方镜像
    docker pull jhipster/jhipster:master
    
    启动jhipster镜像,选择一个空文件/jhipster夹挂载到容器中
    docker container run --name jhipster
    -v  /jhipster:/home/jhipster/app 
    -v  ~/.m2:/home/jhipster/.m2 
    -p 8080:8080 
    -p 9000:9000 
    -p 3001:3001 
    -d  -t jhipster/jhipster
    
    进入容器中
    docker container exec -it --user root jhipster bash
    然后就可以生成项目了。与windows上操作无差别
    复制代码

     

  •          项目的组成简单介绍

  Gateway: springcloud Zuul Proxy  进行动态路由微服务

  Registry:主要封装了Eureka以及配置中心Config Server。

        Jhipster Console:封装了Elk监控 以及 sleuth zipkin 等分布式链路监控组件。

        Jhipster Uaa:  采用UAA用户登录认证 OAUTH2集中式认证,默认不使用的话则是JWT无状态认证

      

  •          总结

          上述仅仅是大体的架构,Jhipster内部使用了很多插件来进行敏捷开发,包括实体类JDL快速生成从数据库到Controller代码,开发效率非常之高。适合中小型企业采用,而且Jhipster支持DockerFile与Compose                    文件生成,可以帮助我们快速容器化服务部署。

微服务架构~BFF和网关是如何演化出来的 - 大大的橙子 - 博客园

$
0
0

 介绍

BFF(Backend for Frontend)和网关Gateway是微服务架构中的两个重要概念,这两个概念相对比较新,有些开发人员甚至是架构师都不甚理解。

本文用假想的公司案例+图示的方式,解释BFF和网关是什么,它们是怎么演化出来的。希望对架构师设计和落地微服务架构有所启发。

服务化架构V1

我们先把时间推回到大致2011年左右。假设有一家有一定业务体量的电商公司CoolShop,在这个时间点它已经完成单块应用的解构拆分,内部SOA服务化已经初步完成。这个时候它的无线应用还没有起步,前端用户体验层主要是传统的服务端Web应用,总体服务化架构V1如下图所示。

 

服务化架构V2

时间转眼来到2012年初,国内的无线应用开始起风,CoolShop公司也紧跟市场趋势,研发自己的无线原生App。为了能尽快上线,公司的架构师提出如下V2架构,让App直接调用内部的服务:

 

 

这个架构有如下问题:

  1. 无线App和内部微服务强耦合,任何一边的变化都可能对另外一边造成影响。

  2. 无线App需要知道内部服务的地址等细节。

  3. 无线App端需要开发大量的聚合裁剪和适配逻辑:

  • 聚合:某一个功能需要同时调用几个后端API进行组合,比如首页需要显示分类和产品细节,就要同时调用分类API和产品API,不能一次调用完成。

  • 裁剪:后端服务返回的Payload一般比较通用,App需要根据设备类型进行裁剪,比如手机屏幕小,需要多裁掉一些不必要的内容,Pad的屏幕比较大,可以少裁掉一些内容。

  • 适配:一种常见的适配场景是格式转换,比如有些后台服务比较老,只支持老的SOAP/XML格式,不支持新的JSON格式,则无线App需要适配处理不同数据格式。

  • 随着设备类型的增多(iPhone/Android/iPad/WindowsPhone),聚合裁剪和适配逻辑的开发会造成设备端的大量重复劳动。

  • 服务化架构V2.1

    V2架构问题太多,没有开发实施。为解决上述问题,架构师经过思考决定在外部设备和内部微服务之间引入一个新的角色~Mobile BFF。

    所谓BFF其实是Backend for Frontend的简称,中文翻译是为前端而开发的后端,它主要由前端团队开发(后端微服务一般由后端团队开发)。BFF可以认为是一种适配服务,将后端的微服务进行适配(主要包括聚合裁剪和格式适配等逻辑),向无线端设备暴露友好和统一的API,方便无线设备接入访问后端服务。

    新的V2.1架构如下图所以:

     

     

     

    这个架构的优势是:

    1. 无线App和内部微服务不耦合,通过引入BFF这层间接,使得两边可以独立变化:

    • 后端如果发生变化,通过BFF屏蔽,前端设备可以做到不受影响。

    • 前端如果发生变化,通过BFF屏蔽,后端微服务可以暂不变化。

    • 当无线App有新的需求时,通过BFF的屏蔽,可以减少前后端团队的沟通协调开销,很多需求由前端团队在BFF上就可以自己搞定。

  • 无线App只需要知道Mobile BFF的地址,并且服务接口是统一的,不需要知道内部复杂微服务的地址和细节。

  • 聚合裁剪和适配逻辑在Mobile BFF上实现,无线App端可以大大简化瘦身。

  • 服务化架构V3

    V2.1架构比较成功,实施落地以后支持了CoolShop公司早期无线业务的成长。随着业务量进一步增长,投入无线研发的团队也不断增加,V2.1架构也逐渐暴露出如下问题:

    1. 刚开始只有一个Mobile BFF,是个单块,但是无线研发团队在不断增加,分别对应多条业务线。根据康威法则,单块的无线BFF和多团队之间就出现不匹配问题,团队之间沟通协调成本高,交付效率低下。

    2. Mobile BFF里头不仅有各个业务线的聚合/裁剪/适配和业务逻辑,还引入了很多跨横切面逻辑,比如安全认证,日志监控,限流熔断等。随着时间的推移,代码变得越来越复杂,技术债越堆越多,开发效率不断下降,缺陷数量不断增加。

    3. Mobile BFF集群是个失败单点(Single Point of Failure),严重代码缺陷或者流量洪峰可能引发集群宕机,所有无线应用都不可用。

    为了解决上述问题,架构师经过思考决定在外部设备和内部BFF之间再引入一个新的角色~API Gateway,新的架构V3如下图所示:

     

     

    新的架构V3有如下调整:

    1. BFF按团队或业务线进行解耦拆分,拆分成若干个BFF微服务,每个业务线可以并行开发和交付各自负责的BFF微服务。

    2. 网关(一般由独立框架团队负责运维)专注跨横切面(Cross-Cutting Concerns)的功能,包括:

    • 路由,将来自无线设备的请求路由到后端的某个微服务BFF集群。

    • 认证,对涉及敏感数据的API访问进行集中认证鉴权。

    • 监控,对API调用进行性能监控。

    • 限流熔断,当出现流量洪峰,或者后端BFF/微服务出现延迟或故障,网关能够主动进行限流熔断,保护后端服务,并保持前端用户体验可以接受。

    • 安全防爬,收集访问日志,通过后台分析出恶意行为,并阻断恶意请求。

  • 网关在无线设备和BFF之间又引入了一层间接,让两边可以独立变化,特别是当后台BFF在升级或迁移时,可以做到用户端应用不受影响。

  • 在新的V3架构中,网关承担了重要的角色,它是解耦拆分和后续升级迁移的利器。在网关的配合下,单块BFF实现了解耦拆分,各业务线团队可以独立开发和交付各自的微服务,研发效率大大提升。另外,把跨横切面逻辑从BFF剥离到网关上去以后,BFF的开发人员可以更加专注业务逻辑交付,实现了架构上的关注分离(Separation of Concerns)。

    服务化架构V4

    业务在不断发展,技术架构也需要不断的调整来应对需求的变化。近年,CoolShop公司技术团队又迎来了新的业务和技术需求,主要是:

    1. 开放内部的业务能力,建设CoolShop Open API平台。借助第三方社区开发者的力量,在CoolShop平台上进行创新,进一步拓宽CoolShop的应用和业务形态。

    2. 废弃传统的服务端Web应用模式,引入前后分离架构,前端采用H5单页等技术给用户提供更好的体验。

    为满足业务需求,架构师对服务化架构又进行了拓展升级,新的V4新架构如下图所示:

     

     

     

    V4整体思路和V3类似,只是拓展了新的接入渠道:

    1. 引入面向第三方开放API的BFF层和配套的网关,支持第三方开发者在CoolShop Open API平台上开发应用。

    2. 引入面向H5应用的BFF层和配套的网关,支持前后分离和H5单页应用模式。

    V4是一个比较完整的现代微服务架构,从外到内依次分为:端用户体验层->网关层->BFF层->微服务层。整个架构层次清晰,职责分明,是一种灵活的能够支持业务不断创新的演化式架构。

    性能测试流程 - 十八岁 - 博客园

    $
    0
    0

    前段时间做了一个压测项目,对压测过程中学到的知识进行了总结,在此和大家分享下:

    一、确定压测的目的

    1. 通过不断加压,得到服务器峰值,找出系统瓶颈。

    2. 验证系统的稳定性。

    3. 确定系统各项指标是否满足上线预估目标。

    4. 为后期性能优化提供参考依据。‍

     

    二、解决环境问题

    压测时,要隔离线上环境,以免影响线上其他业务,主要关注以下三点:

    1. 如果有测试环境,首选测试环境。

    2. 如果只有线上环境,要确保线上环境没有其他业务。

    3. 要压测的环境所接入的第三方接口也要确定做到隔离线上环境。‍

     

    三、压测环境要求

    1. 稳定性。由于压测时会持续打压并保持一段时间,所以测试环境的稳定性尤为重要。测试环境的稳定性决定了测试结果的准确性。

    2. 独立性。在搭建环境时,要尽量保证测试环境的独立性,最好是测试环境不与其他系统共用,减少不确定的因素可能对测试过程的影响,导致测试结果不准确,以及避免压测对其他服务的影响。

    3. 可控性。在进行压测时,测试环境中的所有设备和资源应该是可以监测和控制的。以免出现异常而未察觉,造成不可挽回的失误。‍

     

    四、确定预期目标

    压测前,要与产品、运营、开发一起预估各项预期数据目标。

    预估之前,需要先考虑如下情况:

    1. 产品所依附的平台的用户数,访问量是多少?

    2. 产品是否会大力宣传推广?

    3. 产品是否会先灰度上线进行观察和监控?

    4. 该产品的目标用户数、访问量是多少?

    5. 用户对该产品的关注度怎么样?

    6. 会不会有其他产品影响到该产品的QPS?

    7. 产品是否存在使用高峰期?

    8. 产品上线的准备数据是否充足,操作方便,满足需求,能够吸引大量用户上线后瞬间频繁使用该产品或持续增加用户使用量(预估产品峰值是发生在刚上线的时候,还是通过用户的逐渐增加而产生)?‍

     

    五、预估方法

    1. 根据经验预估。

      一个做过多次性能测试的测试人员,基本清楚一个产品的QPS大概能达到多少,需要留有多少的容错空间。这是一种简单快速的预估方法。

    2. 参考同类产品或相似产品。

      如果有同类或相似产品,可以根据同类或相似产品的QPS进行预估,比如社区类的微博、电商类的淘宝等等。

    3. 灰度上线试行。

      灰度是指该产品只对某些用户可见及使用或抽出核心功能快速上线供用户使用,以此来统计及预估产品的各项数据。如果可以灰度上线试行进行预估,那就尽量用这种方式进行。这是最真实可靠的数据。

    4. 根据上一版本的QPS进行预估。

      如果是版本迭代的产品,可以根据上一版本的QPS及版本优化后预估增加或减少的QPS预估出新版本上线后的QPS。

    5. 如果以上方式都不适用,可采用8/2原则。

      8/2原则是指80%的请求访问在20%的时间内到达。是通用的预估方式。可根据系统pv测算出QPS值。峰值QPS=(总pv * 80%)/(60*60*24*20%)。‍

     

    六、制定压测方案

      根据压测目的和预期目标制定测试计划,包括压测时间和压测分组,其中压测分组又包括系统配置、数据来源、压测时长、压测步骤、测试结果。‍

     

    七、数据准备,脚本准备

      有了压测方案,接下来就要准备压测需要的数据及脚本。其中压测数据尽量模拟线上真实用户的数据,得出的结果更加准确。‍

     

    八、执行压测

    1. 根据测试方案里的分组分别进行测试。

    2. 首先通过向服务器不断加压,得到服务器峰值QPS(通过不断加压,平均响应时间及QPS趋于稳定,即为峰值QPS)。

    3. 对峰值QPS持续压测2个小时(视情况而定,一般是2个小时,如果预估系统会长时间处在高峰期,或者系统性能不是很稳定,可加长压测时间至24小时)来验证系统的稳定性。

    4. 通过向服务器不断加压,到达一定QPS,响应时间明显变慢或报错,这个拐点即是服务器的瓶颈。

    5. 压测结束后,分析压测结果,得到TP99和TP95。

    6. 根据压测结果,衡量性能是否符合预期,编写测试报告。‍

     

    九、测试报告

    测试报告一般包括如下几点:

    ① 测试结论。测试结论包含两部分:第一部分是将测试结果精简成一句话,并附上是否可以上线。例如“测试结果符合预期,可以上线。”第二部分是评估方法等一些支撑结论的数据。

    ② 风险备忘。根据测试结果,分析可能存在的风险,经与产品运营沟通,不影响上线,可列为风险备忘。

    ③ 优化建议。根据压测得到的系统瓶颈给出优化建议。

    ④ 测试结果。指每组测试的详细曲线图及结论。

    ⑤ 测试数据。指压测时传的参数来源,参数尽量用线上的真实数据,模拟线上真实用户的请求。

    ⑥ 测试分组。与压测方案里的测试分组是相同的概念,这里不再赘诉。‍

    JMeter非GUI模式执行测试-10999785-51CTO博客

    $
    0
    0

    实际压测时,强烈建议使用命令行模式,即非GUI模式,消耗压力机资源较低,可以支持较大并发。


    注意:如有必要,使用管理员权限打开命令提示符

              如有必要,重启master机或者slave机

              如有必要,重启jmeter

              windows可以直接在脚本目录,打开命令行:按住Shift键,鼠标右键选择“在此处打开命令窗口”

               以下命令在windows和linux下均适用


    1.命令解释

    jmeter -n -t xxx.jmx -r -l xxx.jtl 

    n表示无GUI运行,t表示要运行的jmx文件,r指远程将所有agent启动,l指生成的文件名称

    2.命令汇总

    jmeter -n -t xxx.jmx -l 001.jtl    支持

    jmeter -n -t xxx.jmx -r -l 001.jtl    支持

    jmeter -n -t xxx.jmx -Jthreads=10 -l 001.jtl    ----------这两条,指定线程数,但没有指定循环次数    支持

    jmeter -n -t xxx.jmx -Jthreads=10 -r -l 001.jtl----------所以适用于运行几分钟的情况     不支持

    jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -l 001.jtl    支持

    jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -r -l 001.jtl     不支持?

    jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -l 001.jtl -e -o output

    jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -r -l 001.jtl -e -o output     不支持?

    jmeter -g 002.jtl -o output


    疑惑:非GUI模式下+分布式模式下,不支持参数化线程数和循环数?

    3.命令使用场景

    (1) jmeter -n -t xxx.jmx -l 001.jtl

    image.png

    运行完毕,可以在jmeter中打开jtl文件查看结果


    (2)在命令行中对线程数和循环次数进行参数化:

        修改jmeter脚本

         image.png

        替换具体的线程数、循环数

         image.png

        命令行中增加 -J 参数

        jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -l 001.jtl


    (3)使用命令自动生成HTML性能报告和各种指标的图表

        修改jmeter.properties

                jmeter.save.saveservice.output_format=csv

                jmeter.save.saveservice.timestamp_format=yyyy/MM/dd HH:mm:ss

        修改user.properties统计间隔

                jmeter.reportgenerator.overall_granularity=1000


        生成HTML报告的两种方式:

            压测结束时生成HTML报告

                 jmeter -n -t xxx.jmx -Jthreads=10 -Jloops=100 -l 001.jtl-e -o output

            使用已有结果文件生成HTML报告

                 jmeter -g 002.jtl -o output

    Oracle大规模数据快速导出文本文件 - 王亨 - 博客园

    $
    0
    0

    哈喽,前几久,和大家分享过如何把文本数据快速导入数据库(点击即可打开),今天再和大家分享一个小技能,将Oracle数据库中的数据按照指定分割符、指定字段导出至文本文件。
    首先来张图,看看导出的数据是什么样子。

     

     

     

    用到的就是Oracle的spool命令,可以将数据库数据导出一个文本文件,而且也可以指定数据分隔符,其中!^是数据之间的分隔符。

    首先和大家分享一下,我的这个脚本是怎么写的,其中写select时,需要导出那些字段,直接写在select里面就可以了,此外,我也是在select里面指定了分割符!^。虽然可以用参数来指定分隔符,但用起来并不友好,结束时,我会演示的。

    set echo off
    set heading off
    set feedback off
    set termout on
    set trimspool off
    spool /home/oracle/Desktop/studentinfoSpool.dat --指定导出数据保存的文本文件
    select stuid||'!^'||stuname||'!^'||sex||'!^'||age from studentinfo; 
    spool off
    exit

      

    一个不超过10行的SQL脚本,设置四五个参数,一个select语句,就可以搞定导数这个问题,是不是,很简单。

    接下来对几个常用的参数进行解释一下。

    参数 作用
    set echo off 显示start启动脚本中的每个sql命令,默认为on。比如select语句
    set pagesize 0 设置每页的行数,默认为24,设置为0时为不用分页,一般需要分页
    set termout off 在电脑屏幕显示脚本中的命令执行结果,默认为on。
    set feedback off 显示本次sql命令处理的记录数,默认为on。
    set heading off 输出域标题,默认为on
    set trimspool off 去字段空格
    set linesize 50 每行允许的最大字符数,设置大些,如果太小,数据库会报错或者数据自动换行,但如果设置过大,文件也会变大
    set colsep ‘!^’; 用来设置分割符,但不建议使用是参数,建议   手动写分割符


    执行sql脚本

    sqlplus c##orcl/1234 @/home/oracle/Desktop/studentinfoSpool.sql
    说明:c##orcl是用户名,1234是密码,@后面是我们写的sql脚本,里面就是刚刚演示的SQL脚本。因为是本地数据库,所以没有写地址和端口等。

    spool命令就是这么简单,但也有几个需要注意的地方。

    注意事项:

    1、不建议使用colsep 设置分割符。

    可以把手动设置分隔符和使用colsep设置的结果对比一下。

     

     

     

     

    可以看到,使用colsep 设置分隔符的数据中间会出现很多空格,而手动设置的分隔符就很好。此外,如果最后一个字段后面也需要分割符,colsep 参数就无能为力。

    2、合理设置linesize 

    linesize 设置如果不当,会出现很多问题,如果太小,数据库可能会错,数据自动换行。太大的话,数据文件也会更大。

     

     

     

     原文链接:https://blog.csdn.net/wzgl__wh/article/details/102887557

    ASP.NET Core Web API 最佳实践指南 - hippieZhou - 博客园

    $
    0
    0

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide

    介绍

    当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求。

    但是,你难道不认为创建一个能正常工作的项目还不够吗?同时这个项目不应该也是可维护和可读的吗?

    事实证明,我们需要把更多的关注点放到我们项目的可读性和可维护性上。这背后的主要原因是我们或许不是这个项目的唯一编写者。一旦我们完成后,其他人也极有可能会加入到这里面来。

    因此,我们应该把关注点放到哪里呢?

    在这一份指南中,关于开发 .NET Core Web API 项目,我们将叙述一些我们认为会是最佳实践的方式。进而让我们的项目变得更好和更加具有可维护性。

    现在,让我们开始想一些可以应用到 ASP.NET Web API 项目中的一些最佳实践。

    Startup 类 和 服务配置

    STARTUP CLASS AND THE SERVICE CONFIGURATION

    Startup类中,有两个方法: ConfigureServices是用于服务注册, Configure方法是向应用程序的请求管道中添加中间件。

    因此,最好的方式是保持 ConfigureServices方法简洁,并且尽可能地具有可读性。当然,我们需要在该方法内部编写代码来注册服务,但是我们可以通过使用 扩展方法来让我们的代码更加地可读和可维护。

    例如,让我们看一个注册 CORS 服务的不好方式:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors(options => 
        {
            options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });
    }

    尽管这种方式看起来挺好,也能正常地将 CORS 服务注册成功。但是想象一下,在注册了十几个服务之后这个方法体的长度。

    这样一点也不具有可读性。

    一种好的方式是通过在扩展类中创建静态方法:

    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials());
            });
        }
    }

    然后,只需要调用这个扩展方法即可:

    public void ConfigureServices(IServiceCollection services)
    {
        services.ConfigureCors();
    }

    了解更多关于 .NET Core 的项目配置,请查看: .NET Core Project Configuration

    项目组织

    PROJECT ORGANIZATION

    我们应该尝试将我们的应用程序拆分为多个小项目。通过这种方式,我们可以获得最佳的项目组织方式,并能将关注点分离(SoC)。我们的实体、契约、访问数据库操作、记录信息或者发送邮件的业务逻辑应该始终放在单独的 .NET Core 类库项目中。

    应用程序中的每个小项目都应该包含多个文件夹用来组织业务逻辑。

    这里有个简单的示例用来展示一个复杂的项目应该如何组织:

    基于环境的设置

    ENVIRONMENT BASED SETTINGS

    当我们开发应用程序时,它处于开发环境。但是一旦我们发布之后,它将处于生产环境。因此,将每个环境进行隔离配置往往是一种好的实践方式。

    在 .NET Core 中,这一点很容易实现。

    一旦我们创建好了项目,就已经有一个 appsettings.json文件,当我们展开它时会看到 appsettings.Development.json文件:

    此文件中的所有设置将用于开发环境。

    我们应该添加另一个文件 appsettings.Production.json,将其用于生产环境:

    生产文件将位于开发文件下面。

    设置修改后,我们就可以通过不同的 appsettings 文件来加载不同的配置,取决于我们应用程序当前所处环境,.NET Core 将会给我们提供正确的设置。更多关于这一主题,请查阅: Multiple Environments in ASP.NET Core.

    数据访问层

    DATA ACCESS LAYER

    在一些不同的示例教程中,我们可能看到 DAL 的实现在主项目中,并且每个控制器中都有实例。我们不建议这么做。

    当我们编写 DAL 时,我们应该将其作为一个独立的服务来创建。在 .NET Core 项目中,这一点很重要,因为当我们将 DAL 作为一个独立的服务时,我们就可以将其直接注入到 IOC(控制反转)容器中。IOC 是 .NET Core 内置功能。通过这种方式,我们可以在任何控制器中通过构造函数注入的方式来使用。

    public class OwnerController: Controller
    {
        private readonly IRepository _repository;
        public OwnerController(IRepository repository)
        {
            _repository = repository;
        }
    }

    控制器

    CONTROLLERS

    控制器应该始终尽量保持整洁。我们不应该将任何业务逻辑放置于内。

    因此,我们的控制器应该通过构造函数注入的方式接收服务实例,并组织 HTTP 的操作方法(GET,POST,PUT,DELETE,PATCH...):

    public class OwnerController : Controller
    {
        private readonly ILoggerManager _logger;
        private readonly IRepository _repository;
        public OwnerController(ILoggerManager logger, IRepository repository)
        {
            _logger = logger;
            _repository = repository;
        }
    
        [HttpGet]
        public IActionResult GetAllOwners()
        {
        }
        [HttpGet("{id}", Name = "OwnerById")]
        public IActionResult GetOwnerById(Guid id)
        {
        }
        [HttpGet("{id}/account")]
        public IActionResult GetOwnerWithDetails(Guid id)
        {
        }
        [HttpPost]
        public IActionResult CreateOwner([FromBody]Owner owner)
        {
        }
        [HttpPut("{id}")]
        public IActionResult UpdateOwner(Guid id, [FromBody]Owner owner)
        {
        }
        [HttpDelete("{id}")]
        public IActionResult DeleteOwner(Guid id)
        {
        }
    }

    我们的 Action 应该尽量保持简洁,它们的职责应该包括处理 HTTP 请求,验证模型,捕捉异常和返回响应。

    [HttpPost]
    public IActionResult CreateOwner([FromBody]Owner owner)
    {
        try
        {
            if (owner.IsObjectNull())
            {
                return BadRequest("Owner object is null");
            }
            if (!ModelState.IsValid)
            {
                return BadRequest("Invalid model object");
            }
            _repository.Owner.CreateOwner(owner);
            return CreatedAtRoute("OwnerById", new { id = owner.Id }, owner);
        }
        catch (Exception ex)
        {
            _logger.LogError($"Something went wrong inside the CreateOwner action: { ex} ");
            return StatusCode(500, "Internal server error");
        }
    }

    在大多数情况下,我们的 action 应该将 IActonResult作为返回类型(有时我们希望返回一个特定类型或者是 JsonResult...)。通过使用这种方式,我们可以很好地使用 .NET Core 中内置方法的返回值和状态码。

    使用最多的方法是:

    • OK => returns the 200 status code
    • NotFound => returns the 404 status code
    • BadRequest => returns the 400 status code
    • NoContent => returns the 204 status code
    • Created, CreatedAtRoute, CreatedAtAction => returns the 201 status code
    • Unauthorized => returns the 401 status code
    • Forbid => returns the 403 status code
    • StatusCode => returns the status code we provide as input

    处理全局异常

    HANDLING ERRORS GLOBALLY

    在上面的示例中,我们的 action 内部有一个 try-catch代码块。这一点很重要,我们需要在我们的 action 方法体中处理所有的异常(包括未处理的)。一些开发者在 action 中使用 try-catch代码块,这种方式明显没有任何问题。但我们希望 action 尽量保持简洁。因此,从我们的 action 中删除 try-catch,并将其放在一个集中的地方会是一种更好的方式。.NET Core 给我们提供了一种处理全局异常的方式,只需要稍加修改,就可以使用内置且完善的的中间件。我们需要做的修改就是在 Startup类中修改 Configure方法:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseExceptionHandler(config => 
        {
            config.Run(async context => 
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "application/json";
    
                var error = context.Features.Get<IExceptionHandlerFeature>();
                if (error != null)
                {
                    var ex = error.Error;
                    await context.Response.WriteAsync(new ErrorModel
                    {
                        StatusCode = 500,
                        ErrorMessage = ex.Message
                    }.ToString());
                }
            });
        });
    
        app.UseRouting();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }

    我们也可以通过创建自定义的中间件来实现我们的自定义异常处理:

    // You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
    public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger<CustomExceptionMiddleware> _logger;
        public CustomExceptionMiddleware(RequestDelegate next, ILogger<CustomExceptionMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }
    
        public async Task Invoke(HttpContext httpContext)
        {
            try
            {
                await _next(httpContext);
            }
            catch (Exception ex)
            {
                _logger.LogError("Unhandled exception....", ex);
                await HandleExceptionAsync(httpContext, ex);
            }
        }
    
        private Task HandleExceptionAsync(HttpContext httpContext, Exception ex)
        {
            //todo
            return Task.CompletedTask;
        }
    }
    
    // Extension method used to add the middleware to the HTTP request pipeline.
    public static class CustomExceptionMiddlewareExtensions
    {
        public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<CustomExceptionMiddleware>();
        }
    }

    之后,我们只需要将其注入到应用程序的请求管道中即可:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseCustomExceptionMiddleware();
    }

    使用过滤器移除重复代码

    USING ACTIONFILTERS TO REMOVE DUPLICATED CODE

    ASP.NET Core 的过滤器可以让我们在请求管道的特定状态之前或之后运行一些代码。因此如果我们的 action 中有重复验证的话,可以使用它来简化验证操作。

    当我们在 action 方法中处理 PUT 或者 POST 请求时,我们需要验证我们的模型对象是否符合我们的预期。作为结果,这将导致我们的验证代码重复,我们希望避免出现这种情况,(基本上,我们应该尽我们所能避免出现任何代码重复。)我们可以在代码中通过使用 ActionFilter 来代替我们的验证代码:

    if (!ModelState.IsValid)
    {
        //bad request and logging logic
    }

    我们可以创建一个过滤器:

    public class ModelValidationAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ModelState.IsValid)
            {
                context.Result = new BadRequestObjectResult(context.ModelState);
            }
        }
    }

    然后在 Startup类的 ConfigureServices函数中将其注入:

    services.AddScoped<ModelValidationAttribute>();

    现在,我们可以将上述注入的过滤器应用到我们的 action 中。

    Microsoft.AspNetCore.All 元包

    MICROSOFT.ASPNETCORE.ALL META-PACKAGE

    注:如果你使用的是 2.1 和更高版本的 ASP.NET Core。建议使用 Microsoft.AspNetCore.App 包,而不是 Microsoft.AspNetCore.All。这一切都是出于安全原因。此外,如果使用 2.1 版本创建新的 WebAPI 项目,我们将自动获取 AspNetCore.App 包,而不是 AspNetCore.All。

    这个元包包含了所有 AspNetCore 的相关包,EntityFrameworkCore 包,SignalR 包(version 2.1) 和依赖框架运行的支持包。采用这种方式创建一个新项目很方便,因为我们不需要手动安装一些我们可能使用到的包。

    当然,为了能使用 Microsoft.AspNetCore.all 元包,需要确保你的机器安装了 .NET Core Runtime。

    路由

    ROUTING

    在 .NET Core Web API 项目中,我们应该使用属性路由代替传统路由,这是因为属性路由可以帮助我们匹配路由参数名称与 Action 内的实际参数方法。另一个原因是路由参数的描述,对我们而言,一个名为 "ownerId" 的参数要比 "id" 更加具有可读性。

    我们可以使用 [Route]属性来在控制器的顶部进行标注:

    [Route("api/[controller]")]
    public class OwnerController : Controller
    {
        [Route("{id}")]
        [HttpGet]
        public IActionResult GetOwnerById(Guid id)
        {
        }
    }

    还有另一种方式为控制器和操作创建路由规则:

    [Route("api/owner")]
    public class OwnerController : Controller
    {
        [Route("{id}")]
        [HttpGet]
        public IActionResult GetOwnerById(Guid id)
        {
        }
    }

    对于这两种方式哪种会好一些存在分歧,但是我们经常建议采用第二种方式。这是我们一直在项目中采用的方式。

    当我们谈论路由时,我们需要提到路由的命名规则。我们可以为我们的操作使用描述性名称,但对于 路由/节点,我们应该使用 NOUNS 而不是 VERBS。

    一个较差的示例:

    [Route("api/owner")]
    public class OwnerController : Controller
    {
        [HttpGet("getAllOwners")]
        public IActionResult GetAllOwners()
        {
        }
        [HttpGet("getOwnerById/{id}"]
        public IActionResult GetOwnerById(Guid id)
        {
        }
    }

    一个较好的示例:

    [Route("api/owner")]
    public class OwnerController : Controller
    {
        [HttpGet]
        public IActionResult GetAllOwners()
        {
        }
        [HttpGet("{id}"]
        public IActionResult GetOwnerById(Guid id)
        {
        }
    }

    更多关于 Restful 实践的细节解释,请查阅: Top REST API Best Practices

    日志

    LOGGING

    如果我们打算将我们的应用程序发布到生产环境,我们应该在合适的位置添加一个日志记录机制。在生产环境中记录日志对于我们梳理应用程序的运行很有帮助。

    .NET Core 通过继承 ILogger接口实现了它自己的日志记录。通过借助依赖注入机制,它可以很容易地使用。

    public class TestController: Controller
    {
        private readonly ILogger _logger;
        public TestController(ILogger<TestController> logger)
        {
            _logger = logger;
        }
    }

    然后,在我们的 action 中,我们可以通过使用 _logger 对象借助不同的日志级别来记录日志。

    .NET Core 支持使用于各种日志记录的 Provider。因此,我们可能会在项目中使用不同的 Provider 来实现我们的日志逻辑。

    NLog 是一个很不错的可以用于我们自定义的日志逻辑类库,它极具扩展性。支持结构化日志,且易于配置。我们可以将信息记录到控制台,文件甚至是数据库中。

    想了解更多关于该类库在 .NET Core 中的应用,请查阅: .NET Core series – Logging With NLog.

    Serilog 也是一个很不错的类库,它适用于 .NET Core 内置的日志系统。

    加密

    CRYPTOHELPER

    我们不会建议将密码以明文形式存储到数据库中。处于安全原因,我们需要对其进行哈希处理。这超出了本指南的内容范围。互联网上有大量哈希算法,其中不乏一些不错的方法来将密码进行哈希处理。

    但是如果需要为 .NET Core 的应用程序提供易于使用的加密类库,CryptoHelper 是一个不错的选择。

    CryptoHelper 是适用于 .NET Core 的独立密码哈希库,它是基于 PBKDF2 来实现的。通过创建 Data Protection栈来将密码进行哈希化。这个类库在 NuGet 上是可用的,并且使用也很简单:

    using CryptoHelper;
    
    // Hash a password
    public string HashPassword(string password)
    {
        return Crypto.HashPassword(password);
    }
    
    // Verify the password hash against the given password
    public bool VerifyPassword(string hash, string password)
    {
        return Crypto.VerifyHashedPassword(hash, password);
    }

    内容协商

    CONTENT NEGOTIATION

    默认情况下,.NET Core Web API 会返回 JSON 格式的结果。大多数情况下,这是我们所希望的。

    但是如果客户希望我们的 Web API 返回其它的响应格式,例如 XML 格式呢?

    为了解决这个问题,我们需要进行服务端配置,用于按需格式化我们的响应结果:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers().AddXmlSerializerFormatters();
    }

    但有时客户端会请求一个我们 Web API 不支持的格式,因此最好的实践方式是对于未经处理的请求格式统一返回 406 状态码。这种方式也同样能在 ConfigureServices 方法中进行简单配置:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers(options => options.ReturnHttpNotAcceptable = true).AddXmlSerializerFormatters();
    }

    我们也可以创建我们自己的格式化规则。

    这一部分内容是一个很大的主题,如果你希望了解更多,请查阅: Content Negotiation in .NET Core

    使用 JWT

    USING JWT

    现如今的 Web 开发中,JSON Web Tokens (JWT) 变得越来越流行。得益于 .NET Core 内置了对 JWT 的支持,因此实现起来非常容易。JWT 是一个开发标准,它允许我们以 JSON 格式在服务端和客户端进行安全的数据传输。

    我们可以在 ConfigureServices 中配置 JWT 认证:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options => 
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = _authToken.Issuer,
    
                    ValidateAudience = true,
                    ValidAudience = _authToken.Audience,
    
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authToken.Key)),
    
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
    
                    //others
                };
            });
    }

    为了能在应用程序中使用它,我们还需要在 Configure 中调用下面一段代码:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseAuthentication();
    }

    此外,创建 Token 可以使用如下方式:

    var securityToken = new JwtSecurityToken(
                    claims: new Claim[]
                    {
                        new Claim(ClaimTypes.NameIdentifier,user.Id),
                        new Claim(ClaimTypes.Email,user.Email)
                    },
                    issuer: _authToken.Issuer,
                    audience: _authToken.Audience,
                    notBefore: DateTime.Now,
                    expires: DateTime.Now.AddDays(_authToken.Expires),
                    signingCredentials: new SigningCredentials(
                        new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_authToken.Key)),
                        SecurityAlgorithms.HmacSha256Signature));
    
    Token = new JwtSecurityTokenHandler().WriteToken(securityToken)

    基于 Token 的用户验证可以在控制器中使用如下方式:

    var auth = await HttpContext.AuthenticateAsync();
    var id = auth.Principal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.NameIdentifier))?.Value;

    我们也可以将 JWT 用于授权部分,只需添加角色声明到 JWT 配置中即可。

    更多关于 .NET Core 中 JWT 认证和授权部分,请查阅: authentication-aspnetcore-jwt-1authentication-aspnetcore-jwt-2

    总结

    读到这里,可能会有朋友对上述一些最佳实践不是很认同,因为全篇都没有谈及更切合项目的实践指南,比如 TDDDDD等。但我个人认为上述所有的最佳实践是基础,只有把这些基础掌握了,才能更好地理解一些更高层次的实践指南。万丈高楼平地起,所以你可以把这看作是一篇面向新手的最佳实践指南。

    在这份指南中,我们的主要目的是让你熟悉关于使用 .NET Core 开发 web API 项目时的一些最佳实践。这里面的部分内容在其它框架中也同样适用。因此,熟练掌握它们很有用。

    非常感谢你能阅读这份指南,希望它能对你有所帮助。


    研发环境容器化实施过程(docker + docker-compose + jenkins) - 陈晨_软件五千言 - 博客园

    $
    0
    0

    背景介绍

    目前公司内部系统(代号GMS)研发团队,项目整体微服务规模大概是4+9+3的规模,4个内部业务微服务,9个是外部平台或者基础服务(文件资源/用户中心/网关/加密等),3个中间件服务(数据库/Redis/Nacos)。
    分为2个组,迭代周期为2周。需求和排期都是会有交叉,会保证每周都有迭代内容交付,另外技术部门也在进行性能优化以及代码规约的重构。我们的Git管理模型使用的是AoneFlow,意味着同一时间可能会有多个研发特性分支进行中。出现的问题就是CI,我们集成使用的Jenkins,原本研发环境就只有一套Jenkins来构建,后来出现并行的特性分支,为了支持开发联调工作就重新搭建了一套环境,但是后面出现了更多的并行需求(例如对接口压测的性能分支,底层基础架构的升级分支,代码规约调整的分支)。
    现在的痛点是需要部署一个环境的成本太高,基本需要一个高级研发对于所有组件都了解,对于Linux系统了解。整套环境部署可能需要2天左右,而且过程特别复杂容易出错。

    改造思路

    考虑是需要进行容器化改造,目前整个环境的管理还没有基于容器化来实施,所以我们希望这次也是给团队一个基本概念和练兵的机会。
    因为我们主要的诉求是环境部署,所以并没有按照容器推荐的那样,每个服务都单独建立docker,而是为了能够快速的部署和构建将所有服务和中间件进行分块。
    目前分块主要是分为中间件服务,业务服务,依赖/底层服务这么三大块。这么分的原因有下面一些:

    • 中间件包含数据库、Nacos、Redis。这么做的目的是因为Nacos强依赖数据库,数据库也是所有微服务的基础依赖之一。数据库结构和Nacos的配置实际上每个迭代会有一些变化,所以将这些内容打包在一起,以版本区分会更简单一些。
    • 依赖/底层服务包含非业务的服务(文件资源/用户中心/网关/加密)。这些都是外部服务,迭代过程中的变化是比较少的,可以每隔几个迭代打包一次。所以为了操作便利所以统一打包成了一个镜像。
    • 业务微服务,业务的微服务就是迭代开发过程中不断修改和测试的内容,所以这块是应该是要单独的容器,并且还要和Jenkins关联能够更新。

    这样基本的容器划分就确认了,整体使用docker-compose来进行容器管理,因为实际的镜像数量会稍微多一些,而且还有很多如端口等配置。

    容器构建

    思路确认之后就开始执行,我们将比较详细的描述各个镜像的构建过程。

    基础准备

    服务器上首先需要安装好 docker和docker-compose依赖。我们的docker的私服使用的Harbor。
    接下来我们基本都是在准备所有的dockerfile,所以会建立一个基础目录,在/root/docker/下建立 gms 文件夹用于存放各个镜像的dockerfile,以及docker-compose文件。
    提供一个基础镜像用于其他镜像生成,基础镜像需要包含java环境,以及一些环境基础插件和工具,我们来看一下Dockerfile

    FROM centos:7
    
    RUN mkdir -p /home/project/vv/log
    
    ADD jdk-8u211-linux-x64.tar.gz /home/project/
    RUN mv /home/project/jdk1.8.0_211 /home/project/java
    COPY entrypoint.sh /home/project/vv/
    ENV JAVA_HOME /home/project/java
    ENV CLASSPATH .:$JAVA_HOME/lib:$JAVA_HOME/jre/lib:$CLASSPATH
    ENV PATH $JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH
    # running required command
    WORKDIR /home/project/vv
    RUN yum install -y wget curl net-tools openssh-server telnet nc && chmod +x entrypoint.sh

    这里可以关注的点在于,我们在这个基础镜像的文件夹内实际上是要把我们需要使用的文件都存储好的,意思就是你 需要复制进镜像的文件都必须在你当前执行docker build命令的目录中
    然后就是这里会需要存在设置环境变量。

    接下来执行 docker build -t images:tag .
    不要漏掉最后的那个. 那个实际上就是指定当前目录。
    完成后执行 docker push
    这样基础镜像就构建完成,我们的命名为 172.16.6.248/gms-service/gms-base
    172.16.6.248是我们内部的Harbor服务器。

    中间件容器

    中间件容器需要包含数据库、Nacos、Redis。
    上面也说过,Nacos依赖与数据库,由于是研发环境Nacos使用的standalone模式。其中有一点需要注意,在Nacos中可能会设置数据库、Redis等连接,无论原本使用的是ip还是域名,在这里都需要改成是服务名称,由于我们是使用类docker-compose并且采用network组网的形式将相关的服务都放在同一个网络内进行多实例之间的隔离。Nacos指向的数据库连接改为本地。
    我们来看一下目录结构:

    -rw-r--r--. 1 root root 336 12月 17 17:58 Dockerfile
    -rw-r--r--. 1 root root 205 12月 17 18:34 gmsstore.sh
    -rw-r--r--. 1 root root 532 12月 6 15:31 my.cnf
    drwxr-xr-x. 12 root root 206 12月 6 15:24 mysql
    drwxr-xr-x. 17 root root 8192 12月 26 14:19 mysqldata
    drwxr-xr-x. 9 root root 125 12月 4 11:10 nacos
    drwxrwxr-x. 6 root root 4096 12月 11 15:21 redis

    Dockerfile不用解释,由于包含多个中间件,所以启动命令打包成了shell。
    mysql涉及到3个文件/文件夹,my.cnf是配置文件,mysql是程序本体,mysqldata是打包了所有相关库数据。
    nacos和redis文件夹也不用解释了。

    我们来看看这个Dockerfile:

    FROM 172.16.6.248/gms-service/gms-base
    
    RUN yum -y install libaio numactl
    
    COPY mysql /home/project/mysql
    COPY mysqldata /home/project/mysqldata
    COPY my.cnf /etc/my.cnf
    COPY nacos /home/project/nacos
    COPY redis /home/project/redis
    COPY gmsstore.sh /home/project
    WORKDIR /home/project/
    RUN chmod +x gmsstore.sh
    ENTRYPOINT ./gmsstore.sh

    这里特别的地方在于mysql8需要安装一些依赖才可以运行,所以我们安装了libaio numactl。

    外部依赖容器

    先来看下目录结构

    -rw-r--r--. 1 root root 358 12月 17 19:26 Dockerfile
    -rw-r--r--. 1 root root 764 12月 16 17:29 gmsdependency.sh
    -rw-r--r--. 1 root root 71509153 12月 16 17:30 vv-dict.jar
    -rw-r--r--. 1 root root 63880862 12月 16 17:29 vv-encryption.jar
    -rw-r--r--. 1 root root 51465237 12月 16 17:30 vv-gateway.jar
    -rw-r--r--. 1 root root 69535661 12月 16 17:29 vv-message.jar
    -rw-r--r--. 1 root root 171366034 12月 16 17:30 vv-resource.jar
    -rw-r--r--. 1 root root 78130738 12月 16 17:29 vv-user.jar

    这个套路和之前一样,相关的服务已经打出了jar包放到打包目录下,编写shell脚本作为所有应用启动的统一入口。
    接下来看下Dockerfile:

    FROM 172.16.6.248/gms-service/gms-base
    
    LABEL version="1.0"
    LABEL description="vv-gms-den"
    LABEL maintainer="liyonghua@vv.cn"
    COPY jar/* /home/project/vv/
    ENV JVM=""
    ENV NACOS="127.0.0.1:9002"
    RUN chmod +x /home/project/vv/gmsdependency.sh
    EXPOSE 7003
    EXPOSE 8100
    EXPOSE 7002
    EXPOSE 7004
    EXPOSE 7001
    WORKDIR /home/project/vv/
    ENTRYPOINT ./gmsdependency.sh

    在这里我们环境变量中设置Nacos的地址,实际上Nacos的地址会使用服务名的方式进行访问,在使用 java -jar 命令时直接设置到参数中,类似这样:

    java -jar vv-dict.jar --spring.cloud.nacos.config.server-addr=$NACOS --spring.cloud.nacos.discovery.server-addr=$NACOS

    业务应用容器

    业务应用容器反而是最没啥好说的,只有一个单jar文件,然后一个启动脚本。
    这里可能唯一需要注意一下的就是第一次启动的问题,由于业务应用依赖于中间件,当启动时mysql和Nacos可能还没有那么快启动起来,所以可能会引发业务应用连接不上中间件自动退出,需要写脚本检测。
    给出Dockerfile

    FROM 172.16.6.248/gms-service/gms-base
    
    LABEL version="1.0"
    LABEL description="vv-gms-core"
    LABEL maintainer="liyonghua@vv.cn"
    COPY jar/* /home/project/vv/
    ENV JVM=""
    ENV NACOS="127.0.0.1:9002"
    RUN chmod +x /home/project/vv/*.sh
    EXPOSE 8102
    WORKDIR /home/project/vv/

    没啥多解释的了。

    容器整合

    所有的docker镜像都已经构建完毕并且已经传输到了镜像服务器上。接下来就是如何整合容器了。
    之前已经说过本次的选型是docker-compose,没有上k8s是因为还没有和运维同学协调好,我们使用docker-compose先做可行性测试。
    docker-compose 的安装很多教程,我列一下基本命令

    yum -y install epel-release
    yum -y install python-pip
    yum -y install python-devel
    pip --version
    pip install --upgrade pip
    pip install docker-compose 
    docker-compose --version

    接下来看一下 docker-compose.yml

    version: '3'
    services:
      gms-dependency:
        image: 172.16.6.248/gms-service/gms-dependency
        ports:
          - 13004:7001
          - 13005:7003
          - 13006:7004
          - 13007:17002
          - 13008:8100
        networks:
          - gmsnetwork
        environment:
          JVM:
          NACOS: gms-store:9002
        depends_on:
          - gms-gateway
        volumes:
          - "/home/project/vv/log/:/home/project/vv/log/"
        entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c './gmsdependency.sh'
      gms-gateway:
        image: 172.16.6.248/gms-service/gms-gateway
        ports:
          - 13010:9001
        networks:
          - gmsnetwork
        depends_on:
          - gms-store
        volumes:
          - "/home/project/vv/log/:/home/project/vv/log/"
        entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c 'java -jar vv-gateway.jar --spring.cloud.nacos.config.server-addr=gms-store:9002 --spring.cloud.nacos.discovery.server-addr=gms-store:9002 >/dev/null 2>&1'
      gms-oacore:
        image: 172.16.6.248/gms-service/gms-oacore:1.2.7
        ports:
          - 13009:8102
        networks:
          - gmsnetwork
        environment:
          JVM:
          NACOS: gms-store:9002
        depends_on:
          - gms-gateway
        volumes:
          - "/home/project/vv/log/:/home/project/vv/log/"
        entrypoint: ./entrypoint.sh -d gms-store:3306,gms-store:9002 -c 'java -jar vv-oa-core.jar --spring.cloud.nacos.config.server-addr=gms-store:9002 --spring.cloud.nacos.discovery.server-addr=gms-store:9002 >/dev/null 2>&1'
      gms-store:
        image: 172.16.6.248/gms-service/gms-store:1.2.6
        ports:
          - 13001:3306
          - 13002:9002
          - 13003:6379
        networks:
          - gmsnetwork
        volumes:
          - "/home/project/vv/log/:/home/project/vv/log/"
      gms-oaweb:
        image: 172.16.6.248/gms-service/gms-oaweb:1.2.6
        ports:
          - 13018:80
        networks:
          - gmsnetwork
        depends_on:
          - gms-oacore
          - gms-dependency
          - gms-gateway
        volumes:
          - "/home/project/vv/log/:/home/project/vv/log/"
        entrypoint: /home/project/vv/entrypoint.sh -d gms-oacore:8102,gms-dependency:17002,gms-gateway:9001,gms-xxladmin:8103 -c 'nginx -g "daemon off;"'
    networks:
      gmsnetwork:
        driver: bridge

    这份文件中,其实是比较常规的docker-compose的格式,由于各个容器之间相互可能都有依赖,所以我们使用了内部网络,networks 这个特性,将相关应用放在同一个内部网络中互相访问。depends_on这个属性支持了启动的先后顺序,但是这个属性仅仅基于容器级别。也就是前置的容器只要启动后续就会启动,但是内部依赖的应用可能还没有启动完成,所以我们使用了shell脚本来检测应用启动完成后再实际的启动应用。最后就是我们使用volumes开放了可挂载目录,输出所有的日志文件便于查看。environment设置环境变量,将依赖的服务名和内部网络端口传递给不同容器中的应用。
    这样就完成了docker-compose的设计,然后我们使用 docker-compose up -d就可以启动 docker-compose stop可以关闭。但是切记 docker-compose命令必须在存在docker-compose.yml文件的目录下执行

    自动构建容器

    我们使用docker-compose已经启动了完整的环境,但是记得本次实践的目的在于研发环境的部署,研发环境是需要不断的更新代码进行调试的。所以我们需要引入jenkins来进行容器重新构建、推送、环境更新。

    Maven相关

    我们的项目是一个父子的Maven项目,父目录下会包含业务核心代码(core)、对外暴露API(api)等包。需要打包的镜像实际上是core中的完整jar包。
    Maven的插件我们使用的是

    <build><plugins><plugin><groupId>com.spotify</groupId><artifactId>dockerfile-maven-plugin</artifactId><version>1.4.10</version><configuration><repository>172.16.6.248/gms-service/gms-oacore</repository><force>true</force><forceCreation>true</forceCreation><tag>${dev.docker.tag}</tag><buildArgs><JAR_FILE>vv-oa-core/target/${project.build.finalName}.jar</JAR_FILE></buildArgs></configuration></plugin></plugins></build>

    这是github上的插件 地址,这里force这个标签是可以对同tag的镜像在构建时进行覆盖。
    Dockerfile建议放在子项目根目录下。
    在Jenkins构建时,我们是以父项目作为根目录执行 package 命令的。打包完成后,不能直接执行 dockerfile:build 命令。而是在Post Steps中定制脚本,cd进入到子项目之后,分别执行 dockerfile:build dockerfile:push,我们来看一下Jenkins中配置的脚本:

    cd gms-oacore
    
    nowtag=1.3.1
    nowpath=/root/docker/dockerbase/feature-VV-443
    nowprefix=feature-VV-443
    
    mvn -Dmaven.test.skip=true dockerfile:build -Ddev.docker.tag=$nowtag
    mvn -Dmaven.test.skip=true dockerfile:push -DpushImageTag
    
    ssh root@172.16.6.247 "/root/docker/dockerbase/jenkins-rebuild.sh $nowpath "$nowprefix"_gms-oacore_1 172.16.6.248/gms-service/gms-oacore:$nowtag gms-oacore"

    这里实际上是在Jenkins先打包,并且build和push镜像,ssh到目标服务器通过远程脚本来进行拉取构建的一些操作。
    针对docker-compose启动的容器,如果是要单独更新一个镜像,可以将容器stop之后rm掉,同时rmi对应镜像,最终使用 docker-compose --scale images:tag=1 重新拉取启动这个镜像。

    非Maven项目

    由于是前后端分离项目,所以我们还会有一个单独的前端项目,是直接挂载Nginx容器内。所以这部分没办法使用Maven插件,我们就采用shell直接调用docker命令的形式,这里放上差异的部分:

    docker build -t 172.16.6.248/gms-service/gms-oaweb:$nowtag .
    docker push 172.16.6.248/gms-service/gms-oaweb:$nowtag

    替换来mvn dockerfile相关的命令,其他基本相同。

    总结

    在这样的实践中,我们将项目拆分为合理粒度建立docker镜像,使用docker-compose将多个容器打包为一个完整环境运行,同时用内部网络的概念隔离多个环境在同一个宿主机器时的影响,最后使用Jenkins来进行自动化的构建和发布,完成了研发环境的完整闭环。效果也大大提高,原本需要花几天时间还不能很完整的部署好,现在只需要一个人15-30分钟就可以完整部署好一个环境。
    但是实际上问题也很多,由于整合来大量的环境,所以单个环境启动后,占用内存10G左右,实际上比较难单个宿主机器直接部署多套。
    另外大家也能发现,我们存在部分访问是通过ip来的,这是一个不好的习惯,建议尽量都改为内部域名的形式,避免后续服务器变更造成复杂影响。
    在实施过程中,我们还是手写了很多shell脚本作为中间粘合,这个对于环境的依赖会比较大,而且复用性其实是很低的,后续我们会考虑如何提高可复用性。
    最后还是要考虑实施k8s,这个应该在2020的Q1就会实施。

    容器化是为了能够让研发和运维对于应用的把握程度更高,避免大家花太多的时间在环境、部署之类问题上,也能够大大提高系统的稳定性和扩展性。但是会对DevOps提出更高的要求,研发和运维要更加紧密的配合,架构设计、部署方案等都需要共同讨论理解之后才能实施,但是我坚信这就是趋势,我们越早迎合越早能提升自己、整个团队和我们的产品。

    LSM-tree 基本原理及应用_LSM数,lsm,lsm-tire_永生只是一场幻梦-CSDN博客

    $
    0
    0

    LSM-tree 在 NoSQL 系统里非常常见,基本已经成为必选方案了。今天介绍一下 LSM-tree 的主要思想,再举一个 LevelDB 的例子。

    正文 3056 字,预计阅读时间 8 分钟。

     

    LSM-tree

    起源于 1996 年的一篇论文《The Log-Structured Merge-Tree (LSM-Tree)》,这篇论文 32 页,我一直没读,对 LSM 的学习基本都来自顶会论文的背景知识以及开源系统文档。今天的内容和图片主要来源于 FAST'16 的《WiscKey: Separating Keys from Values in SSD-conscious Storage》。

    先看名字,log-structured,日志结构的,日志是软件系统打出来的,就跟人写日记一样,一页一页往下写,而且系统写日志不会写错,所以不需要更改,只需要在后边追加就好了。各种数据库的写前日志也是追加型的,因此日志结构的基本就指代追加。注意他还是个 “Merge-tree”,也就是“合并-树”,合并就是把多个合成一个。

     

    LSM-tree 是专门为 key-value 存储系统设计的,key-value 类型的存储系统最主要的就两个个功能,put(k,v):写入一个(k,v),get(k):给定一个 k 查找 v。

    LSM-tree 最大的特点就是写入速度快,主要利用了磁盘的顺序写,pk掉了需要随机写入的 B-tree。关于磁盘的顺序和随机写可以参考:《硬盘的各种概念》

    下图是 LSM-tree 的组成部分,是一个多层结构,就更一个树一样,上小下大。首先是内存的 C0 层,保存了所有最近写入的 (k,v),这个内存结构是有序的,并且可以随时原地更新,同时支持随时查询。剩下的 C1 到 Ck 层都在磁盘上,每一层都是一个在 key 上有序的结构。

    写入流程:一个 put(k,v) 操作来了,首先追加到写前日志(Write Ahead Log,也就是真正写入之前记录的日志)中,接下来加到 C0 层。当 C0 层的数据达到一定大小,就把 C0 层 和 C1 层合并,类似归并排序,这个过程就是Compaction(合并)。合并出来的新的 new-C1 会顺序写磁盘,替换掉原来的 old-C1。当 C1 层达到一定大小,会继续和下层合并。合并之后所有旧文件都可以删掉,留下新的。

    注意数据的写入可能重复,新版本需要覆盖老版本。什么叫新版本,我先写(a=1),再写(a=233),233 就是新版本了。假如 a 老版本已经到 Ck 层了,这时候 C0 层来了个新版本,这个时候不会去管底下的文件有没有老版本,老版本的清理是在合并的时候做的。

    写入过程基本只用到了内存结构,Compaction 可以后台异步完成,不阻塞写入。

    查询流程:在写入流程中可以看到,最新的数据在 C0 层,最老的数据在 Ck 层,所以查询也是先查 C0 层,如果没有要查的 k,再查 C1,逐层查。

    一次查询可能需要多次单点查询,稍微慢一些。所以 LSM-tree 主要针对的场景是写密集、少量查询的场景。

    LSM-tree 被用在各种键值数据库中,如 LevelDB,RocksDB,还有分布式行式存储数据库 Cassandra 也用了 LSM-tree 的存储架构。

     

    LevelDB

    其实光看上边这个模型还有点问题,比如将 C0 跟 C1 合并之后,新的写入怎么办?另外,每次都要将 C0 跟 C1 合并,这个后台整理也很麻烦啊。这里以 LevelDB 为例,看一下实际系统是怎么利用 LSM-tree 的思想的。

    下边这个图是 LevelDB 的架构,首先,LSM-tree 被分成三种文件,第一种是内存中的两个 memtable,一个是正常的接收写入请求的 memtable,一个是不可修改的immutable memtable。

    另外一部分是磁盘上的 SStable (Sorted String Table),有序字符串表,这个有序的字符串就是数据的 key。SStable 一共有七层(L0 到 L6)。下一层的总大小限制是上一层的 10 倍。

    写入流程:首先将写入操作加到写前日志中,接下来把数据写到 memtable中,当 memtable 满了,就将这个 memtable 切换为不可更改的 immutable memtable,并新开一个 memtable 接收新的写入请求。而这个 immutable memtable 就可以刷磁盘了。这里刷磁盘是直接刷成 L0 层的 SSTable 文件,并不直接跟 L0 层的文件合并。

    每一层的所有文件总大小是有限制的,每下一层大十倍。一旦某一层的总大小超过阈值了,就选择一个文件和下一层的文件合并。就像玩 2048 一样,每次能触发合并都会触发,这在 2048 里是最爽的,但是在系统里是挺麻烦的事,因为需要倒腾的数据多,但是也不是坏事,因为这样可以加速查询。

    这里注意,所有下一层被影响到的文件都会参与 Compaction。合并之后,保证 L1 到 L6 层的每一层的数据都是在 key 上全局有序的。而 L0 层是可以有重叠的。

    上图是个例子,一个 immutable memtable 刷到 L0 层后,触发 L0 和 L1 的合并,假如黄色的文件是涉及本次合并的,合并后,L0 层的就被删掉了,L1 层的就更新了,L1 层还是全局有序的,三个文件的数据顺序是 abcdef。

    虽然 L0 层的多个文件在同一层,但也是有先后关系的,后面的同个 key 的数据也会覆盖前面的。这里怎么区分呢?为每个key-value加个版本号。所以在 Compaction 时候应该只会留下最新的版本。

    查询流程:先查memtable,再查 immutable memtable,然后查 L0 层的所有文件,最后一层一层往下查。

     

    LSM-tree读写放大

    读写放大(read and write amplification)是 LSM-tree 的主要问题,这么定义的:读写放大 = 磁盘上实际读写的数据量 / 用户需要的数据量。注意是和磁盘交互的数据量才算,这份数据在内存里计算了多少次是不关心的。比如用户本来要写 1KB 数据,结果你在内存里计算了1个小时,最后往磁盘写了 10KB 的数据,写放大就是 10,读也类似。

    写放大:我们以 RocksDB 的 Level Style Compaction 机制为例,这种合并机制每次拿上一层的所有文件和下一层合并,下一层大小是上一层的 r 倍。这样单次合并的写放大就是 r 倍,这里是 r 倍还是 r+1 倍跟具体实现有关,我们举个例子。

    假如现在有三层,文件大小分别是:9,90,900,r=10。又写了个 1,这时候就会不断合并,1+9=10,10+90=100,100+900=1000。总共写了 10+100+1000。按理来说写放大应该为 1110/1,但是各种论文里不是这么说的,论文里说的是等号右边的比上加号左边的和,也就是10/1 + 100/10 + 1000/100 = 30 = r * level。个人感觉写放大是一个过程,用一个数字衡量不太准确,而且这也只是最坏情况。

    读放大:为了查询一个 1KB 的数据。最坏需要读 L0 层的 8 个文件,再读 L1 到 L6 的每一个文件,一共 14 个文件。而每一个文件内部需要读 16KB 的索引,4KB的布隆过滤器,4KB的数据块(看不懂不重要,只要知道从一个SSTable里查一个key,需要读这么多东西就可以了)。一共 24*14/1=336倍。key-value 越小读放大越大。

     

    总结

    关于 LSM-tree 的内容和 LevelDB 的设计思想就介绍完了,主要包括写前日志 WAL,memtable,SStable 三个部分。逐层合并,逐层查找。LSM-tree 的主要劣势是读写放大,关于读写放大可以通过一些其他策略去降低。

     

     

     

     

    为什么 K8s 在阿里能成功?| 问底中国 IT 技术演进 - 阿里巴巴云原生 - 博客园

    $
    0
    0

    作者:
    曾凡松 阿里云云原生应用平台高级技术专家
    张振 阿里云云原生应用平台高级技术专家

    导读:本文描述了阿里巴巴在容器管理领域的技术演进历程,解读了为什么 K8s 最终能够大获成功的原因,以及到今年 双11 阿里巴巴内部的 K8s 应用情况。内容着重描述了阿里巴巴基于 K8s 的云原生改造实践过程的三大能力升级,在对应能力升级过程中沉淀的技术解决方案,以及通过这些能力升级所取得的业务价值。

    从 2015 年 Google 牵头成立 CNCF 以来,云原生技术开始进入公众的视线并取得快速的发展,到 2018 年包括 Google、AWS、Azure、Alibaba Cloud 等大型云计算供应商都加入了 CNCF,云原生技术也从原来的应用容器化发展出包括容器、Service Mesh、微服务、不可变基础设施、Serverless、FaaS 等众多技术方向,CFCF 旗下也囊括了越来多的开源项目。

    Kubernetes 作为 CNCF 的第一个项目从诞生之初就就令人瞩目,Kubernetes 由 Google 工程师基于 Google 内部多年集群管理系统 Borg 的设计经验,结合云计算时代的基础设施特点重新设计而得,旨在帮助企业解决大规模 IT 基础设施的应用容器编排难题。

    Google 在 2014 年 6 月开源 Kubernetes 以后,在 Redhat、Microsoft、Alibaba 等厂商和众多开源爱好者共同的努力下,成长为如今容器编排领域的事实标准,极大的推动了云原生领域的发展。

    今天为大家分享来自阿里云的 Kubernetes 大规模实践经验,展现阿里云如何基于 Kubernetes 推动阿里巴巴应用运维技术栈走向云原生,如何推动 Kubernetes自身的技术进步,充分挖掘云原生时代的红利助力阿里巴巴大幅降低 双11 的 IT 成本。

    容器在阿里巴巴的发展历程

    1.png

    在 2011 年之前,阿里巴巴使用 VM 虚拟化技术将一个物理机切分为 3 个虚拟机,用于部署淘宝服务,而随着淘宝业务的飞速发展,基于 VM 的技术方案在灵活性上跟不上业务的步伐。

    因此,阿里巴巴在 2011 年就开始探索基于 Linux lxc 的容器技术,用于替代传统基于 VM 的应用部署方案,到 2013 年,研发了基于 Linux lxc 的 T4 容器和 AI 容器编排系统。这在当时已是非常领先的技术方案,但自己研发的容器技术与基于 VM 时代的运维系统始终存在一些兼容性问题。

    在 2013 年随着 Docker 容器镜像方案的出现,阿里巴巴技术人员立即看到了基于容器 + Docker 镜像技术的未来,开始大力投入到这一领域的研究当中,到 2015 年 Aliswarm、Zeus、Hippo 等容器编排系统蓬勃发展,各自开疆扩土服务了阿里巴巴经济体的一部分业务。诸多的系统在解决了业务运维成本的同时,也带来了一定的重复建设成本,同时也导致了阿里巴巴内部的资源分布比较分散,无法统一调度多样的业务类型发挥出不同业务错峰使用资源的优势。

    正是在这样的背景下,Sigma 系统应运而出并在 2017 年统一了阿里巴巴的资源池,统一调度阿里巴巴所有的核心业务,并第一次支持将在线服务与离线作业运行在同一个物理机上,大幅提高数据中心的资源利用效率并降低了阿里巴巴的 IT 成本。

    随着云原生技术的高速发展,阿里巴巴也看到了云原生技术的潜力,以及未来企业 IT 全面上云的必然趋势,从 2018 年开始转型到 Kubernetes 技术,通过 Kubernetes 扩展能力将 Sigma 积累多年的调度能力通过 Kubernetes 的方式提供出来。

    在 2019 年阿里巴巴宣布全面上云,阿里巴巴开始全面拥抱 Kubernetes,并将 Sigma 调度系统全面的迁移到基于 Kubernetes 的调度系统,该系统也正是支持了今年最大规模 双11 电商交易系统的底层基础设施,稳定的支持了大促前后数百次的应用变更并提供极速的应用发布与扩容体验,为 双11 的顺畅的购物体验立下悍马功劳。

    为什么 K8s 在阿里能成功

    Kubernetes 在众多的技术中脱颖而出,概括起来可以归纳为以下三个方面。

    2.png

    • 首先是其在诞生之初就为云时代而生,拥有超前的眼光和先进的设计理念,加之最初由天才的 Google 工程师基于其内部 Borg 多年的经验设计而来,诞生之后就飞速发展;

    后来随着 RedHat、IBM、微软、Vmware、阿里云等来自全球的优秀工程师大力投入,打造了繁荣的社区和生态系统,成为企业容器编排系统的首选。

    阿里巴巴经济体拥有众多的子公司,这些子公司在加入阿里巴巴大家庭时或多或少都会有一套自有的容器编排系统,在融入阿里巴巴的基础设施过程中,Kubernetes 是最标准也最容易被经济体内外的客户所接受的一个方案。

    • 其次,Kubernetes 倡导的申明式 API 的设计理念,也贴合了阿里巴巴在应用运维领域的经验与教训;

    传统的运维系统通常是基于过程式的设计,而过程式的运维系统在较长的系统调用链路下,通常会出现因异常处理复杂而导致的系统效率低下。

    在大规模应用运维系统中复杂又繁多的状态处理也是一个大难题,基于过程式的系统设计很难确保系统的一致性,针对这些边界异常的处理通常又导致运维系统变得非常复杂,最终为异常兜底的只能依赖运维人员的人工操作。基本上可以认为基于过程式的运维系统难以应对超大规模的应用管理,而 Kubernetes 提供的申明式 API 却是解决应用运维状态轮转的一剂良药,是提高运维技术栈整体链路效率的最佳实践原则。

    • 第三,Kubernetes 模块化、可扩展的架构设计,满足阿里巴巴的定制化改造以支持众多业务运维场景的需求。

    在阿里巴巴内部,即有大量的无状态核心电商系统,也有大量的缓存、消息队列等中间件有状态系统,也包括大量带有倒排索引数据的检索系统,还有大量的 AI 计算任务,不用的应用类型对底层容器管理平台的要求也有所不同。

    因此,一个模块化方便迁入自定义应用管理策略、易于扩展调度模型的设计显得至关重要,是能够服务阿里内部众多应用形态、提供统一容器管理基础设施的关键,Kubernetes 基本上提供了这些关键基础能力,虽然在实际应用过程中仍然会遇到非常多的实际问题。

    阿里巴巴的 K8s 应用情况

    3.png

    在 2019 年 双11,阿里巴巴内部核心业务主要运行在神龙、ECS、ECI 三种资源类型的基础设施之上,而这些不同类型的基础设施资源均通过 Kubernetes 统一管理,以容器的形态提供给上层应用使用,完成了核心业务的支撑。

    有别于以往的 双11,今年核心电商业务应用大规模部署在神龙裸金属服务器上。如果有关注过阿里云技术的发展,应该不会对神龙服务器感到陌生,它是阿里云自主研发的新一代云服务器,通过“软硬一体”的技术开创性的将云计算的虚拟化开销分摊到低价硬件板卡上,彻底的释放 CPU 的计算能力,第一次真正的做到了云计算虚拟化的“零”开销。

    容器也是一种轻量级的虚拟化方案,神龙+容器+Kubernetes 的结合正是云原生时代的最佳拍档,支撑了今年最大规模的 双11,也将是未来的主流技术形态。

    阿里巴巴也在继续使用 ECS 作为 Kubernetes 的底层资源供给,ECS 作为传统的云计算虚拟化方式支撑了部门集团内部业务,同时结合灵活性更好的弹性容器实例 ECI 用于应对业务突发的流量峰值,为业务带来了云计算的弹性价值,真正实现了按需申请、释放资源的极致弹性能力,降低了业务需要提前规划资源所带来的成本。

    这些分布在海内外的数十万个节点的资源,被数十个 Kubernetes 集群托管,运行着阿里巴巴上万个应用,共计超过百万的容器,其规模之大前所未有。在今年的 双11 中,阿里巴巴内部最大的 Kubernetes 集群规模达到万级;当然这并不是Kubernetes 的技术极限,而是我们考虑数据中心资源效率与基础设施容灾能力之间所取的平衡,在将来如果有需要这个数字也可能变得更大。

    基于 K8s 的云原生改造实践

    Kubernetes 作为云原生技术的代表,已经成为了容器编排领域的事实标准,阿里巴巴自 2017 年开始探索,到 2018 年确认技术转型到使用 Kubernetes 来管理生产的容器。

    在落地 K8s 的过程中,我们主要面临着两大难题:

    4.png

    • 其一,上层多样的业务运维平台;

    为了支撑阿里巴巴内部多样的业务形态,在内部发展出来了多个典型的业务运维平台,每一个运维平台的基础设施、流程控制、应用发布策或多或少都会存在一些差别,缺少一个统一的应用运维标准。在调度与集群管理的技术演进过程中,如何牵引整个运维体系升级的同时并保持多个业务的平台及其上业务的稳定性,这是一个巨大的工程。

    • 其二,随着阿里巴巴经济体全面上云战略的实施,整个底层基础设施包括存储、网络、基础运维软件的技术演进也非常迅速。调度与集群管理需要在支持好基础设施快速演进的同时,迭代自身的技术架构,并同时保证业务的稳定性。

    基于 K8s 的云原生技术改造正是在这样的背景下诞生,发展到 2019 年 Kubernetes 在内部已大规模部署,所有的核心业务也都已经运行在 K8s 集群管理中。但在这几年的实践过程中,有一个问题始终萦绕在工程师头脑中,在阿里巴巴这么大体量、这么复杂的业务下,遗留了大量传统的运维习惯以及支撑这些习惯的运维体系,在这样的背景下落地Kubernetes (内部一个形象的比喻叫做给高速飞行的飞机更换发动机)到底是在坚持什么,哪些地方可以妥协,哪些地方必须改变?

    这一章节, 将为大家分享我们这几年对这个问题的一些思考,特别是经过了今年的 双11 考验后,这个问题的答案基本上得到了工程师群里的集体认可。

    负责顶层设计的架构师终于可以喘一口气:拥抱 Kubernetes 本身并不是目的,而通过拥抱 Kubernetes 翘动业务的云原生改造,通过 Kubernetes 的能力治理传统运维体系下的沉疴顽疾,真正释放云的弹性能力,为业务的应用交付解绑提速,才是这次技术变革的最大价值所在。

    5.png

    面向终态升级

    在传统的运维体系下,应用的变更都是运维通过创建操作工单发起工作流,继而对容器平台发起一个个的变更来完成的。比如升级一个服务下的 3000 个实例,工单会被提前计算并生成出多个批次的子任务,并逐个的调用容器平台的接口完成变更应用的变更。

    为了确保应用发布工单的顺利执行,在每一个子工单内部,每一个容器的发布也是一个工作流,包括监控开管、镜像拉取、容器启停、服务注册、配置推送等等,如果一切正常该流程会按预期有序的进行。

    6.png

    在大规模应用发布的场景中,诸如宿主机宕机、磁盘异常、IO 异常、网络异常、内核异常等几乎是必然存在的,如果发布流程中的某一个步骤出现了错误,通常情况下需要运维平台按照一定的策略来重试,直到超过该批次的超时阈值,这将会带来三个问题,下面逐一展开。

    • 其一是重试带来的效率问题;

    每一个子任务的执行时间将被任务内的长尾发布所拖累,假设将 3000 个容器分为 30 批次每批 100 个(仅为示意并非最佳实践),每一批次内出现一个容器发布异常时,该批次的发布时间将被重试拉长。

    • 其二是失败带来的一致性问题;

    对于发布异常的容器,在工单结束之后通常只能通过外围链路巡检的方式来治理,而事实上通常的巡检是依赖运维人员手工操作的,带来了极大的人工成本和不确定性。

    • 第三是应用并发变更冲突问题。

    如果在应用发布的过程中,同时提交了应用扩容的请求,由 3000 扩容到 3200 个实例,扩容的 200 个实例应该采用旧版本还是新版本,采用旧版本扩容将面临的问题是谁最终负责这 200 个旧版本实例的升级,采用新版本扩容将面临的是稳定性问题,如果新版本存在问题新扩容的实例将产生较大的影响。

    正是因为这些复杂的问题导致多数运维系统拒绝了并发的应用变更,导致并发操作效率非常底下。

    K8s 为应用管理所提供的申明式 API 的设计理念同时解决了解决了这三个问题,用户只需要描述期望的最终状态以及达成期望状态的过程中需要遵守的限制条件,达成终态所需要执行的复杂操作全部交由 K8s 的来完成。

    在应用发布过程中,通常情况下 K8s 通过控制并发度及最大不可用实例数来约束应用发布对服务的影响,对于发布过程中失败的实例通过最终一致的方式在系统内部解决。正是基于这一设计,用户发起服务变更时只是更新了应用的预期状态,并不需要等待任何任务的结束,一并解决了应用发布效率、线上配置的一致性和并发变更冲突效率的问题。

    7.png

    基于面向终态的理念管理应用,我们开发 Advanced StatefulSet 的应用管理工作模型,顾名思义它基于 Kubernetes 官方的 StatefulSet 扩展而来。

    在官方的工作模型中,应用通过滚动的方式完成版本升级,也就是创建新的 Pod 同时删除旧版本的 Pod,直到整个应用切换为新的版本。

    这种方式简单直接,但存在效率的问题,比如所有应用的 Pod 需要重新的调度,这在大规模应用发布场景将给调度器带来很大的压力;同时,因为新版本 Pod 为全新创建,需要重新分配 IP 并挂载远程卷,这对云计算网络、存储基础设施也将是很大的挑战;再者,因为容器是被全新调度出来的,在机器上需要重新下载新的应用镜像,这将大幅降低应用发布的效率。

    为了提高应用发布的效率和资源的确定性,开发了这一工作负载模型,它支持原地发布应用,应用发布前后应用所在的位置保持不变,同时支持了并发更新、容错暂停等丰富的发布策略,高效的满足了阿里巴巴内部电商应用的发布需求。因为应用发布前后位置不变,因此我们可以在灰度发布的过程中预先下载并解压即将要发布的容器镜像,从而大幅提高应用发布的效率。

    8.png

    在面向终态的应用管理中,复杂的运维过程被 K8s 内部所实现,K8s根据用户的期望及现状计算出需要执行的动作,并逐步的变更直到终态。面向终态带来了卓越的运维效率提升,但同时也为系统工程架构提出了更高的要求。

    我们知道在 K8s 内部是一个模块化、分布式的系统,通往终态的运维决策分散在内部的多个模块中,这些模块都有可能对容器发起一些运维动作,比如控制器、运维 Operator、重调度器甚至是 kubelet。在高度自动化的系统中,一旦出现预期外的异常,其杀伤力可能会对其上运行的业务造成灾难性的后果,加之 K8s 中决策分散在众多的模块中,所带来的问题是系统风险的控制变得更加困难,对这个系统设计的质量有很高的要求。

    为了控制整个系统的风险,如上图所示,我们在 K8s 系统的关键位置对关键行为行为进行了埋点,针对性的制定了限流及熔断的策略,使得整个系统即使在出现极端错误的场景下,也能够最大化的保护其上运行的业务。

    自愈能力升级

    9.png

    在阿里巴巴传统的运维体系下,容器平台仅生产资源,应用的启动以及服务发现是在容器启动后由运维平台系统来完成的,这种分层的方法给了运维系统最大的自由度,也在容器化后促进了阿里巴巴的容器生态繁荣。

    但是这种方式有一个严重的问题,因为容器调度平台无法自主地去触发容器的扩缩容,而需要和一个个运维平台来做复杂的联动,上层运维系统也因为需要感知到底层基础设施的信息,从而导致进行了很多重复建设的工作。

    在工程实践上,这些复杂性使得即使经过了细心的设计与大量的投入其工作效率也不高,严重妨碍宿主机发生故障、重启,容器中进程发生崩溃、卡住等异常时的自愈修复效率,同时也让应用弹性伸缩的实现变得非常的复杂和低效。

    10.png

    我们解决这一问题的思路是通过 K8s 中提供了容器命令以及生命周期钩子,将启动应用以及检查应用启动状态这一正个流程内置到 pod 中,包括与监控、VIP、服务中心、配置中心等基础设施的交互,通过 Pod 实现容器与应用实例的生命周期统一。

    容器平台不再是仅生产资源,而是交付可以直接为业务使用的服务,从而使得可以在 K8s 系统内部完成故障自愈闭环,极大地简化了应用故障自愈以及自动弹性扩缩容能力的建设。提高系统自愈的效率,实际上也是帮助业务获得更好的运行时稳定性和应用运维效率。

    11.png

    在完成了容器与应用实例的生命周期统一之后,我们正在打造一个统一控制器编程框架:Operator Platform。

    Operator Platform 由中心的控制组件与一个 sidecar 框架容器以及客户端代码组成,通过对通用的控制器能力的抽象,包括:事件通知、灰度管理、版本控制、缓存、命令管道等能力的封装集成,支持多语言编写operator,使得开发者无需要理解 K8s 的众多的接口细节及错误处理,从而降低了基于 operator 的自动化运维能力的开发难度,使得越来越多的运维能力通过operator 的方式沉淀到 K8s 生态体系中来,让更多的有状态应用能够自动化地部署,提高整个运维体系的运转效率。

    通过这种方式,构建了整个机器故障自愈的体系,高效的串联起包括机器锁定、应用驱逐、机器线下、异常修复等流程,确保了集群宿主机的在线率以及业务的可用性。未来,我们期望通过将 operator 编写标准化的方式去推动多个运维平台的基础运维能力能够被最大化的复用,减少重复建设的成本。

    不可变基础设施

    12.png

    第三个重要的能力升级是对不可变基础设施的升级。

    我知道 Docker 提供了一种统一的应用交付的形式,通过把应用的二进制、配置、依赖文件在构建过程中打到一个镜像中,从而确保了应用被一次构建出来之后在多个环境中交付的确定性,避免了环境不一致带来的诸多问题。

    而 K8s 更进一步,通过将不同用途的 Docker 容器组装在一起成为一个 pod,通常情况下在升级 pod 时需要整个的销毁重建,从而确保应用镜像、卷、资源规格的一致性。在落地 K8s 的过程中,坚持了不可变基础设施的设计理念,通过 K8s pod 将原本运行在一个富容器中的应用与运维基础组件分离到不同的容器中,并通过升级容器镜像的方式完成应用的升级。

    这里有一个概念需要澄清,并不是使用 K8s 就等于践行了不可变基础设施的理念,而是必须要确保应用运维通过镜像升级而不是动态发布文件的方式完成,而事实上因为一些历史原因,这一用法在行业中普遍存在。

    13.png

    当然,与 K8s 有所不同的是,我们并未强制坚持 pod 的不可变而是取了一个折中的方式,即坚持容器不可变。

    原因是我们将应用容器与运维基础设施容器分离之后,运维容器作为应用容器的 sidecar 容器,其拥有着不同的版本迭代策略。应用容器由应用运维人员负责发布,其策略因应用的不同而不同,比如电商应用使用 StatefulSet 而本地生活使用 Deployment 来管理应用,而基础设施容器则由基础设施运维负责,其发布策略与应用本身也存在一些差别。

    为了解决这个问题,我们开发了一个叫做 SidecarSet 的基础设施容器管理模型,它使用同一个集合统一管理多个应用的运维容器,将基础设施的变更与应用容器的变更进行分离,从而支持基础设施的快速演进。将基础设施容器定义从应用 pod 中抽离出来后,应用管理员不再关心基础容器的启动参数,而是交由基础设施运维人员通过配置 SidecarSet 自动为应用注入运维容器,简化了应用运维的复杂性。

    可以看到,这种关注点分离的设计,同时简化了应用运维与基础设施运维的负担。

    总结与展望

    阿里云通过落地 K8s 推动阿里巴巴运维体系走向云原生,在应用容器发布管理效率、服务稳定性以及企业 IT 成本方面取得了很大的突破。

    我们一直在思考,通过什么样的方式能够将阿里巴巴的应用管理经验输出到更多的场景,解决更多客户面临的应用管理难题,在企业全面云化这样的趋势下,如何解决企业在公有云、私有云、混合云以及多云场景的应用管理复杂性。

    14.png

    正是在这样的背景下,阿里云与微软在 2019 年 11 月联合推出了一款用于构建和交付云原生应用的标准规范,即  Open Application Model(简称 OAM)

    OAM 提出了一种通用的模型,让各平台可以在统一的高层抽象上透出应用部署和运维能力,解决跨平台的应用交付问题。同时, OAM以标准化的方式沟通和连接应用开发者、运维人员、应用基础设施,让云原生应用交付和管理流程更加连贯、一致。

    15.png

    通过应用交付标准化的方法,我们期望未来在云上部署一个应用,就像手机在应用商店中安装一个淘宝一样便捷与高效。

    最后,本文提到的阿里巴巴在云原生改造上完成的相关能力升级,我们都已经或者即将开源到 OpenKruise 项目中,欢迎大家关注与交流!


    使用filebeat收集kubernetes中的应用日志 - 宋净超的博客|Cloud Native|云原生布道师

    $
    0
    0

    前言

    本文已同步更新到Github仓库 kubernetes-handbook中。

    昨天写了篇文章 使用Logstash收集Kubernetes的应用日志,发现logstash十分消耗内存(大约500M),经人提醒改用filebeat(大约消耗10几M内存),因此重写一篇使用filebeat收集kubernetes中的应用日志。

    在进行日志收集的过程中,我们首先想到的是使用Logstash,因为它是ELK stack中的重要成员,但是在测试过程中发现,Logstash是基于JDK的,在没有产生日志的情况单纯启动Logstash就大概要消耗 500M内存,在每个Pod中都启动一个日志收集组件的情况下,使用logstash有点浪费系统资源,经人推荐我们选择使用 Filebeat替代,经测试单独启动Filebeat容器大约会消耗 12M内存,比起logstash相当轻量级。

    方案选择

    Kubernetes官方提供了EFK的日志收集解决方案,但是这种方案并不适合所有的业务场景,它本身就有一些局限性,例如:

    • 所有日志都必须是out前台输出,真实业务场景中无法保证所有日志都在前台输出
    • 只能有一个日志输出文件,而真实业务场景中往往有多个日志输出文件
    • Fluentd并不是常用的日志收集工具,我们更习惯用logstash,现使用filebeat替代
    • 我们已经有自己的ELK集群且有专人维护,没有必要再在kubernetes上做一个日志收集服务

    基于以上几个原因,我们决定使用自己的ELK集群。

    Kubernetes集群中的日志收集解决方案

    编号方案优点缺点
    1每个app的镜像中都集成日志收集组件部署方便,kubernetes的yaml文件无须特别配置,可以为每个app自定义日志收集配置强耦合,不方便应用和日志收集组件升级和维护且会导致镜像过大
    2单独创建一个日志收集组件跟app的容器一起运行在同一个pod中低耦合,扩展性强,方便维护和升级需要对kubernetes的yaml文件进行单独配置,略显繁琐
    3将所有的Pod的日志都挂载到宿主机上,每台主机上单独起一个日志收集Pod完全解耦,性能最高,管理起来最方便需要统一日志收集规则,目录和输出方式

    综合以上优缺点,我们选择使用方案二。

    该方案在扩展性、个性化、部署和后期维护方面都能做到均衡,因此选择该方案。

    logstash日志收集架构图

    我们创建了自己的logstash镜像。创建过程和使用方式见 https://github.com/rootsongjc/docker-images

    镜像地址: index.tenxcloud.com/jimmy/filebeat:5.4.0

    测试

    我们部署一个应用对logstash的日志收集功能进行测试。

    创建应用yaml文件 fielbeat-test.yaml

    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: filebeat-test
      namespace: default
    spec:
      replicas: 3
      template:
        metadata:
          labels:
            k8s-app: filebeat-test
        spec:
          containers:
          - image: sz-pg-oam-docker-hub-001.tendcloud.com/library/filebeat:5.4.0
            name: filebeat
            volumeMounts:
            - name: app-logs
              mountPath: /log
            - name: filebeat-config
              mountPath: /etc/filebeat/
          - image: sz-pg-oam-docker-hub-001.tendcloud.com/library/analytics-docker-test:Build_8
            name : app
            ports:
            - containerPort: 80
            volumeMounts:
            - name: app-logs
              mountPath: /usr/local/TalkingData/logs
          volumes:
          - name: app-logs
            emptyDir: {}
          - name: filebeat-config
            configMap:
              name: filebeat-config
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: filebeat-test
      labels:
        app: filebeat-test
    spec:
      ports:
      - port: 80
        protocol: TCP
        name: http
      selector:
        run: filebeat-test
    ---
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: filebeat-config
    data:
      filebeat.yml: |
        filebeat.prospectors:
        - input_type: log
          paths:
            - "/log/*"
            - "/log/usermange/common/*"
        output.elasticsearch:
          hosts: ["172.23.5.255:9200"]
        username: "elastic"
        password: "changeme"
        index: "filebeat-docker-test"

    注意事项

    • 将app的 /usr/local/TalkingData/logs目录挂载到logstash的 /log目录下。
    • Filebeat容器大概需要10M左右内存。
    • 该文件可以在 manifests/test/filebeat-test.yaml找到。
    • 我使用了自己的私有镜像仓库,测试时请换成自己的应用镜像。
    • filebeat镜像制作参考

    创建应用

    部署Deployment

    kubectl create -f filebeat-test.yaml

    查看 http://172.23.5.255:9200/_cat/indices将可以看到列表有这样的indices:

    green open filebeat-docker-test            7xPEwEbUQRirk8oDX36gAA 5 1   2151     0   1.6mb 841.8kb

    访问Kibana的web页面,查看 filebeat-docker-test的索引,可以看到filebeat收集到了app日志。

    Kibana页面

    「真诚赞赏,手留余香」


    定时任务莫名停止,Spring 定时任务存在 Bug?? - 楼下小黑哥 - 博客园

    $
    0
    0

    Hello~各位读者新年好!这里楼下小黑哥给大家拜个年,祝大家蒸蒸日上烫烫烫,年年有余屯屯屯。

    那年那 Bug

    春节放假,小黑哥坐上高铁回家,突然想到一次生产问题。那是小黑哥参加工作第一年,那一年国庆假期,小黑哥提前一天请假回家办个护照。那时候刚开始负责一个生产系统,所以工作日请假,还是有点担心,就怕 问题看小黑哥不在,悄然上门。

    哎,真实越怕什么,就来什么。

    高铁开到一半的时候,同事反馈系统不能获取最新的流水信息(流水信息通过 Spring定时任务定时拉取)。小黑哥心里一惊,立刻拔出电脑,连上 VPN,准备登上生产机器,查看系统情况。可是,高铁上网络大家也懂,很不稳定,连了好久连不上 VPN,只好远程指挥同事看一下系统日志。通过同事反馈的日志,发现拉取流水定时任务没有执行,进一步查看,小黑哥发现整个系统其他的定时任务也都停止了。。。

    这真是一个奇怪的的问题,这好端端的定时任务怎么会突然停止?

    暂时想不到解决办法,只好指挥同事先重启应用。重启之后,暂时解决问题,定时任务重新开始执行,也获取到最新的付款流水信息。

    问题排查

    到家之后,小黑哥立刻登上生产机器,查看系统日志,发现重启之前某一定时任务运行到一半,并且在这之后其他定时任务就没有再被执行。

    通过系统日志,定位到了有问题的代码。

    这里采用 重试补偿策略,防止查询流水信息因为网络等问题发生偶发的失败。这个策略面对偶发的失败没什么问题,但是如果查询银行流水服务一直失败,这段代码就会陷入死循环。恰巧那段时间网络出现一些问题,导致这里查询一直处于失败。

    增加最大重试次数,修复该 Bug

    修复之后,立刻将最新版本代码部署到生产系统,暂时解决了这个问题。

    知识点:面对一些失败,可以采用重试补偿策略,重新执行,最大可能保证执行成功,但是这里切记设置合适的的 重大的次数

    深入排查

    虽然问题解决了,但是小黑哥心里还是存在一个疑惑,为何一个定时任务发生了阻塞,就会影响执行其他定时任务。小黑哥最初的理解是不同的定时任务应该互相隔离,互不影响才对,真难到是 Spring定时任务的 Bug吗?

    想到这里,小黑哥决定写一个 Demo,复现问题,然后深入源码排查。

    启动程序,日志输出如下:

    image-20200124160151622

    从日志可以看到, fixDelayMethod方法执行之后进入休眠,直到休眠结束, cronMethod定时任务才有机会被执行。另外从上面可以看到,上述两个定时任务都由 pool-1-thread-1线程执行。从这点可以看出 Spring定时任务将会交给线程池执行。

    知识点: 线程池中线程默认命名策略为 pool-%poolNumber-thread-%num。

    如果线程池只有一个工作线程,该线程一旦被长时间阻塞,堆积的其他任务就没有机会被执行。

    那么是不是这个问题导致的 Sping定时任务停止执行?我们继续往下排查。

    图上日志绿色部分, ScheduledAnnotationBeanPostProcessor输出一个重要信息:

    No TaskScheduler/ScheduledExecutorService bean found for scheduled processing

    查看 Spring文档Spring内部将会通过调用 TaskScheduler执行定时任务,而另一个 ScheduledExecutorServiceJDK提供执行定时任务的执行器。记住这两者

    image-20200125140457573

    通过这段日志,使用 IDEA 的强大的 关键字搜索功能,定位到 ScheduledAnnotationBeanPostProcessor#finishRegistration方法。

    这个方法比较长,大家重点关注图中标示的几处。

    Spring启动之后将会扫描所有 Bean中带有 @Scheduled注解的方法,然后封装成 Task子类放置到 ScheduledTaskRegistrar

    这段代码位于 ScheduledAnnotationBeanPostProcessor#processScheduled,感兴趣的可以翻阅查看

    如果此时 ScheduledTaskRegistrar不存在定时任务或者 ScheduledTaskRegistrar中的 TaskScheduler不存在, finishRegistration将会多次调用 ScheduledAnnotationBeanPostProcessor#resolveSchedulerBean方法用以查找 TaskScheduler/ScheduledExecutorService

    接下去将会把获取到 Bean通过 setScheduler注入到 ScheduledTaskRegistrar中。

    如果获取的为 ScheduledExecutorService类型,将会将其封装到 taskScheduler中。

    最后还没找到,将会输出最刚开始见到的日志。然后 Spirng将会在 ScheduledTaskRegistrar#afterPropertiesSet创建一个单线程的定时任务执行器 ScheduledExecutorService,注入到 ConcurrentTaskScheduler中,然后通过 taskScheduler执行定时任务。

    image-20200125144040781

    交给 TaskScheduler的定时任务最后实际上还是通过 ScheduledExecutorService执行。

    这里可以得出一个 结论

    Spring定时任务实际上通过 JDK提供的 ScheduledExecutorService执行。默认情况下,Spring 将会生成一个单线程 ScheduledExecutorService执行定时任务。所以一旦某一个定时任务长时间阻塞这个执行线程,其他定时任务都将被影响,没有机会被执行线程执行。

    Spring 这种默认配置,在需要执行多个定时任务的情况,可能会是一个坑。我们可以通过改变配置,使 Spring 采用多线程执行定时任务。

    自定义配置

    Spring 可以通过多种方式改变默认配置。

    xml 配置

    通过 xml配置 TaskScheduler线程数。

    <task:annotation-driven  scheduler="myScheduler"/><task:scheduler id="myScheduler" pool-size="10"/>

    通过上面的配置,Spring 将会使用 TaskScheduler子类 ThreadPoolTaskScheduler,内部线程数为 pool-size数量,这个线程数将会直接设置 ScheduledExecutorService线程数量。

    注解配置

    在上面问题排查中,我们知道 Spring将会查找 TaskScheduler/ScheduledExecutorService,若存在将会使用。所以这里我们可以生成这些类的 Bean

    以上方式二选一即可

    SpringBoot 配置

    上面两种配置适用于普通 Spring,比较繁琐。相比而言 SpringBoot配置将会非常简单,只需要在启动配置文件加入如下配置即可。

    spring.task.scheduling.pool.size=10
    spring.task.scheduling.thread-name-prefix=task-test

    技术总结

    下面开始技术总结:

    1. Spring定时任务执行原理实际使用的是 JDK自带的 ScheduledExecutorService
    2. Spring默认配置下,将会使用具有单线程的 ScheduledExecutorService
    3. 单线程执行定时任务,如果某一个定时任务执行时间较长,将会影响其他定时任务执行
    4. 如果存在多个定时任务,为了保证定时任务执行时间的准确性,可以修改默认配置,使其使用多线程执行定时任务
    5. 面对偶发的失败,我们可以采用重试补偿策略,不过这里切记设置合适的最大重试次数

    随便聊聊

    对于常用的开源框架,我们不仅要掌握怎么用,还要熟悉相关的配置,最后还应该去了解其内部的使用的原理。这样出了问题,我们也能很快定位问题,找到问题的实际原因。

    帮助文档

    Spring scheduling-task-scheduler

    欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客: studyidea.cn


    Viewing all 532 articles
    Browse latest View live